使用PyTorch解决优化问题

PyTorch作为一款用于深度学习的计算框架,其不但提供了常用的各种深度学习模型,同时提供了多维数组(张量)及运算,线性代数和自动微分相关的特性。使得其不但可以用于深度神经网络的建模,同时还可以直接作为优化器使用,其实深度神经网络训练的过程就是对其参数优化的过程。

1. 自动微分

微分操作可以通过手动微分、数值微分、符号微分、自动微分实现,其中自动微分通过计算过程中对变量的跟踪实现对导数的计算,在深度学习框架中使用的为反向传播自动微分。在PyTorch中需要跟踪计算导数的变量需要在声明是增加requires_grad,相当于明确该变量为函数的自变量而非参数变量

例如计算函数 \( f(x, y) = x^2 + 2xy \)在点(1, 2)处的导数(梯度) \[\nabla f\ = [\frac{\partial f}{\partial x}, \frac{\partial f}{\partial y}] = [2x + 2y, 2x]\] \[\nabla f|_{(1, 2)}=[6, 4]\] 使用PyTorch计算的程序如下

import torch;
#声明两个需要跟踪导数的变量
x = torch.tensor((1.), requires_grad=True);
y = torch.tensor((2.), requires_grad=True);
#计算函数值
z = x ** 2 + 2 * x * y;
#反向传播
z.backward();
#输出导数
print(x.grad, y.grad);

最终的输出为,与手动微分计算的结果一致

tensor(6.) tensor(2.)

2. 优化问题

所谓优化问题,本质就是求函数的极值问题,对于连续可微问题则极值点的导数为零,优化问题将转换为方程求根问题。常用的方法为梯度下降法,即沿着梯度下降的方向寻找下一个位置。PyTorch中提供了各种常用的优化算法,在torch.optim包中,其中包含了部分自适应步长的算法。

例如计算函数\( f(x, y) = x^2 + 2x + y^2 \)的极小值

import torch;

x = torch.tensor((1.), requires_grad=True);
y = torch.tensor((2.), requires_grad=True);
#定义优化器,x y为进行优化的变量
optim = torch.optim.Adam([x, y], lr=1e-1);
#迭代优化
for i in range(1, 2000):
    #计算函数值
    z = x ** 2 + 2 * x + y ** 2;
    #梯度置零
    optim.zero_grad();
    #反向传播
    z.backward();
    #跟新优化变量
    optim.step();
    print(x.item(), y.item(), z.item());

最终得到的输出为

-1.0 0.0 -1.0

对于更一般的优化问题,除了函数本身还会有一些约束条件,对于带有约束条件的问题,在数学分析中可通过增加拉格朗日乘子的方法将其转换为不含约束的优化问题。

然而如果将乘子也作为其中的一个优化变量会导致构造的带有乘子的函数梯度为零的位置不是带有乘子函数的极值,考虑以下问题

求函数的极值 \[ f(x, y) = x^2 + y^2 \] 约束条件: \[ x + y - 1 = 0 \] 若采用乘子法,则 \[L = x^2 + y^2 + \lambda(x + y - 1)\] \[\frac{\partial L}{\partial x} = 2x + \lambda = \frac{\partial L}{\partial y} = 2y + \lambda = \frac{\partial L}{\partial \lambda} = x + y - 1 = 0\] 得到\(x = y = \frac{1}{2}, \lambda = -1\)

而如果使用以下程序处理该问题,会出现无法收敛的情况,说明梯度为零的点并不是函数L的极值点

import torch;

x = torch.tensor((1.), requires_grad=True);
y = torch.tensor((2.), requires_grad=True);
a = torch.tensor((0.), requires_grad=True);
optim = torch.optim.Adam([x, y, a], lr=1e-1);
for i in range(1, 2000):
    z = x ** 2 + y ** 2 + a * (x + y - 1)
    optim.zero_grad();
    z.backward();
    optim.step();
    print(x.item(), y.item(), a.item(), z.item());

为此为了程序实现上的便利可以采用罚函数法,考虑以下函数 \[ L = x^2 + y^2 + M(x + y - 1)^2\] 其中M为一个非常大的正数,此时当约束条件不满足时,函数L就会大幅增大,迫使沿着梯度下降时会自动落入满足边界条件的区域内,如此处理后程序变为

import torch;

x = torch.tensor((0.), requires_grad=True);
y = torch.tensor((0.), requires_grad=True);
optim = torch.optim.Adam([x, y], lr=1e-1);
for i in range(1, 2000):
    #增加约束条件的罚函数
    z = x ** 2 + y ** 2 + 1e5 * (x + y - 1) ** 2;
    optim.zero_grad();
    z.backward();
    optim.step();
    print(x.item(), y.item(), z.item());

最后将得到结果,非常接近使用解析的方法得到的结果

0.49999749660491943 0.49999749660491943 0.49999749660491943