2.9 PyTorch的损失函数和优化器
2.9.1 损失函数
在1.7节中,我们初步学习了深度学习中常用的损失函数和优化器的相关理论,下面介绍一下PyTorch中常用的损失函数。一般来说,PyTorch的损失函数有两种形式:函数形式和模块形式。前者调用的是torch.nn.functional库中的函数,通过传入神经网络预测值和目标值来计算损失函数,后者是torch.nn库里的模块,通过新建一个模块的实例,然后通过调用模块的方法来计算最终的损失函数。
由于训练数据一般以迷你批次的形式输入神经网络,最后预测的值也是以迷你批次的形式输出的,而损失函数最后的输出结果应该是一个标量张量。因此,对于迷你批次的归约一般有两种方法,第一种是对迷你批次的损失函数求和,第二种是对迷你批次的损失函数求平均。一般来说,也是默认和最常见的情景,最后输出的损失函数是迷你批次损失函数的平均。
前面已经介绍过神经网络处理的预测问题分为回归问题和分类问题两种。对于回归问题,一般情况下使用的是torch.nn.MSELoss模块,即前面所介绍的平方损失函数,通过创建这个模块的实例(一般使用默认参数,即在类的构造函数中不传入任何参数,这将会输出损失函数对迷你批次的平均,如果要输出迷你批次的每个损失函数,可以指定参数reduction='none',如果要输出迷你批次的损失函数的和,可以指定参数reduction='sum'),在实例中传入神经网络预测的值和目标值,能够计算得到最终的损失函数。具体的使用方法参考代码2.26。
代码2.26 损失函数模块的使用方法。
除回归问题外,关于分类问题,PyTorch也有和回归问题使用方法类似的损失函数。如果是二分类问题用到的交叉熵损失函数,可以使用torch.nn.BCELoss模块实现。同样,在初始化这个模块的时候可以用默认参数,输出所有损失函数的平均。该模块一般接受的是Sigmoid函数的输出,然后按照1.7节中的式(1.42)来计算损失函数。具体的使用方法可以参考代码2.26的相关部分。需要注意的是,这个损失函数接受两个张量,第一个张量是正分类标签的概率值,第二个张量是以0为负分类标签、1为正分类标签的目标数据值,这两个值都必须是浮点类型。另外一个经常用到的函数是对数(Logits)交叉熵损失函数torch.nn.BCEWithLogitsLoss,这个函数和前面函数的区别在于,可以直接省略Sigmoid函数的计算部分,不用计算概率,该损失函数会自动在损失函数内部的实现部分添加Sigmoid激活函数。在训练的时候使用这个损失函数可以增加计算的数值的稳定性,因为当概率接近0或者概率接近1的时候,二分类交叉熵函数的对数部分会很容易接近无穷大,这样会造成数值不稳定,通过在损失函数中加入Sigmoid函数并针对Sigmoid函数化简计算损失函数,能够有效地避免这两种情况下的数值不稳定。
和二分类的问题类似,在多分类情况下,也可以使用两个模块。第一个模块是torch.nn.NLLLoss,即负对数似然函数,这个损失函数的运算过程是根据预测值(经过Softmax的计算和对数计算)和目标值(使用独热编码)计算这两个值按照元素一一对应的乘积,然后对乘积求和,并取负值。因此,在使用这个损失函数之前必须先计算Softmax函数取对数的结果。PyTorch中有一个函数torch.nn.functional.log_softmax可以实现这个目的。第二个模块是torch.nn.CrossEntropyLoss,用于构建目标损失函数,这个损失函数可以避免LogSoftmax的计算,在损失函数里整合Softmax输出概率,以及对概率取对数输出损失函数。
2.9.2 优化器
有了损失函数之后,就可以使用优化器对模型进行优化。下面从最简单的随机梯度下降算法开始,结合代码2.20构造的线性回归模型,来介绍如何使用优化器优化模型的参数。这里会使用实际数据来演示如何拟合一个线性模型,具体的数据是波士顿地区的房价数据。关于这个数据,可以通过安装scikit-learn库(pip install scikit-learn)来载入相应的数据。载入数据后可以看到,该数据有13个特征,一共506条数据。为简便起见,这里不对数据做分割,直接使用全量数据来进行训练。另外,因为数据量比较小,每一次模型的优化都会使用全量数据,不使用迷你批次数据进行训练。代码2.27演示了如何使用优化器来优化简单线性回归模型。
代码2.27 简单线性回归函数和优化器。
从代码中可以看到,这里首先要定义输入数据和预测目标。因为数据需要输入代码2.19中构造的线性回归的实例代码中,这里首先构建有13个参数的线性回归模型LinearModel(13),然后构建损失函数的计算模块criterion,并将其设置为MSELoss模块的实例。同时,这里构建了一个随机梯度下降算法的优化器(torch.optim.SGD),这个优化器的第一个参数是线性回归模型参数的生成器(调用lm.parameters方法),第二个参数是学习率(lr)。接下来构建训练的输入特征(data)和预测目标(target),传入的参数是载入的波士顿房价的特征和预测目标的Numpy数组,因为这个数据是双精度类型,所以需要使用dtype=torch.float32将数据转换为单精度类型。为了实现前向和反向传播计算,在构建输入特征的时候需要设置requires_grad=True,这样就能在计算过程中构建计算图。接下来就是优化的过程,需要先获取当前参数下模型的预测结果,并且使用这个结果计算出损失函数,然后预先清空梯度(前面已经介绍过,多次计算梯度会导致梯度累积),损失函数调用反向传播方法,计算得到每个参数对应的梯度,最后执行一步优化的计算(调用optim.step方法)。从输出结果可以看到,损失函数在每一步优化的时候逐渐下降了。
正如前面演示的例子一样,PyTorch的随机梯度下降算法优化器torch.optim.SGD构建了一个方法,能够对传入参数生成器(以及传入列表或迭代器)中的每个参数进行优化(通过调用step方法)。这里需要注意的是,在优化之前,首先要执行两个步骤,第一步是调用zero_grad方法清空所有的参数前一次反向传播的梯度,第二步是调用损失函数的backward方法来计算所有参数的当前反向传播的梯度。除这两个参数外,随机梯度下降优化器还可以引入动量(Momentum)参数,通过在优化器类的初始化中指定momentum来得到。PyTorch的优化器对于不同的参数可以使用不同的学习率,如代码2.28所示,默认的学习率为10-2,默认的动量为0.9,但对于model.classifier子模块来说,它的学习率是10-3。通过使用字典的列表分别指定学习率,可以达到对不同的参数使用不同的学习率的目的。
代码2.28 PyTorch优化器对不同参数指定不同的学习率。
除随机梯度下降优化器以外,PyTorch还自带了很多其他的优化器。这里简单介绍一下(1.7节中已经介绍过相应的算法)常用的一些优化器。这些优化器类在构造时传入的第一个参数一般是模型参数的生成器(或者如代码2.27所示的参数组的字典列表),第二个参数一般是学习率。对于AdaGrad算法来说,可以使用torch.optim.Adagrad优化器来进行优化,其中,lr_decay参数指定了学习率的衰减速率,weight_decay参数指定了权重的衰减速率,initial_accumulator_value设置了梯度的初始累加值。如果要使用RMSProp算法,可以使用torch.optim.RMSprop类,alpha参数设置了指数移动平均的参数,eps参数设置是为了提高算法的数值稳定性,可以保持默认的值,weight_decay参数指定了权重的衰减速率,momentum指定了动量参数,centered参数如果设置为True,会对梯度做基于方差的归一化处理(默认为False),即梯度除以梯度方差估计值的平方根。如果要使用Adam算法,可以使用torch.optim.Adam类,其中的betas参数输入含有两个参数的一个元组,分别是梯度和梯度平方的指数移动平均的参数。eps参数设置是为了提高算法的数值稳定性,可以保持默认的值,weight_decay参数指定了权重的衰减速率,amsgrad参数指定是否使用AMSGrad算法作为Adam算法的变种。有关AMSGrad算法,可以参考相关的资料23。
在优化器以外,torch.optim包还提供了学习率衰减的相关类,这些类都在torch.optim的子包torch.optim.lr_scheduler中。举例来说,可以使用torch.optim.lr_scheduler.StepLR类来进行学习率衰减,具体如代码2.29所示。
代码2.29 PyTorch学习率衰减类示例。
从代码2.29可以看到,在使用的时候,需要传入具体的优化器,以及隔多少步进行学习率衰减(step_size)及其衰减的系数(gamma)。在代码2.29中,每次经过30个迭代期,学习率会变成原来的0.1倍,每次经过一个迭代期的时候都会调用梯度衰减类的step方法,学习率衰减类会记录当前的迭代期,并根据当前的迭代期决定是否会发生学习率的衰减。同样,如果需要在算法中使用余弦退火算法,可以使用torch.optim.lr_scheduler.CosineAnnealingLR类,并在这个类中传入最小的学习率eta_min,以及余弦退火算法的最大周期T_max,这样每次调用step方法时就会自动根据余弦退火算法和当前的迭代期数目决定当前的学习率。