ch04-损失优化

ch04-损失优化

    • 0.引言
    • 1.权值初始化
      • 1.1. 梯度消失与爆炸
      • 1.2. Xavier 初始化
      • 1.3. Kaiming 初始化
      • 1.4. 常用的权值始化方法
      • 1.5. nn.init.calculate_gain
      • 1.6. 总结
    • 2.损失函数 (一)
      • 2.1. 损失函数的概念
      • 2.2. 交叉熵损失函数 nn.CrossEntropyLoss
      • 2.3. NLL/BCE/BCEWithLogits Loss
      • 2.4. 总结
    • 3.损失函数 (二)
      • 3.1. PyTorch 中的损失函数
      • 3.2. 总结
    • 4.优化器(一)
      • 4.1. 什么是优化器
      • 4.2. Optimizer 的属性
      • 4.3. Optimizer 的方法
        • 4.3.1.step()
        • 4.3.2.zero_grad ()
        • 4.3.3.add_param_group ()
        • 4.3.4.state_dict() 和 load_state_dict()
      • 4.4. 总结
    • 5.优化器 (二)
      • 5.1. 学习率
      • 5.2. 动量
      • 5.3. PyTorch 中的常用优化器
        • 5.3.1.optim.SGD
        • 5.3.2.PyTorch 中的 10 种常用优化器
      • 5.4. 总结

0.引言

1.权值初始化

在前几节中,我们学习了如何搭建网络模型。在网络模型搭建好之后,有一个非常重要的步骤,就是对模型中的权值进行初始化:正确的权值初始化可以加快模型的收敛,而不适当的权值初始化可以会引发梯度消失或者爆炸,最终导致模型无法训练。本节课,我们将学习如何进行权值初始化。

1.1. 梯度消失与爆炸

这里,我们以上节中提到的一个三层的全连接网络为例。我们来看一下第二个隐藏层中的权值 W 2 W_2 W2 的梯度是怎么求取的。
H 1 = X × W 1 , H 2 = H 1 × W 2 , O u t = H 2 × W 3 H_{1}=X \times W_{1},H_{2}=H_{1} \times W_{2},Out=H_{2} \times W_{3} H1=X×W1H2=H1×W2Out=H2×W3

其中第 2 层的权重梯度如下:

Δ W 2 = ∂ L o s s ∂ W 2 = ∂ L o s s ∂ o u t ∗ ∂ o u t ∂ H 2 ∗ ∂ H 2 ∂ w 2 = ∂ L o s s ∂ o u t ∗ ∂ o u t ∂ H 2 ∗ H 1 \begin{aligned} \Delta \mathrm{W}_{2} &=\frac{\partial \mathrm{Loss}}{\partial \mathrm{W}_{2}}=\frac{\partial \mathrm{Loss}}{\partial \mathrm{out}} * \frac{\partial \mathrm{out}}{\partial \mathrm{H}_{2}} * \frac{\partial \mathrm{H}_{2}}{\partial \mathrm{w}_{2}} \\ &=\frac{\partial \mathrm{Loss}}{\partial \mathrm{out}} * \frac{\partial \mathrm{out}}{\partial \mathrm{H}_{2}} * \mathrm{H}_{1} \end{aligned} ΔW2=W2Loss=outLossH2outw2H2=outLossH2outH1
ch04-损失优化_第1张图片

从公式角度来看, Δ W 2 \Delta \mathrm{W}_{2} ΔW2 依赖于前一层的输出 H 1 H_{1} H1

  • 如果 H 1 H_{1} H1 趋近于零,那么 Δ W 2 \Delta \mathrm{W}_{2} ΔW2 也接近于 0,造成梯度消失。
  • 如果 H 1 H_{1} H1 趋近于无穷大,那么 Δ W 2 \Delta \mathrm{W}_{2} ΔW2 也接近于无穷大,造成梯度爆炸。

要避免梯度爆炸或者梯度消失,就要严格控制网络层输出的数值范围,即每个网络层的输出值不能太大或者太小。

下面构建 100 层全连接网络,先不使用非线性激活函数,每层的权重初始化为服从 N(0,1) 的正态分布,输出数据使用随机初始化的数据。

import torch
import torch.nn as nn
from tools.common_tools import set_seed

set_seed(1)  # 设置随机种子


class MLP(nn.Module):
    def __init__(self, neural_num, layers):
        super(MLP, self).__init__()
        self.linears = nn.ModuleList([nn.Linear(neural_num, neural_num, bias=False) for i in range(layers)])
        self.neural_num = neural_num

    def forward(self, x):
        for (i, linear) in enumerate(self.linears):
            x = linear(x)
        return x

    def initialize(self):
        for m in self.modules():
            if isinstance(m, nn.Linear):
                nn.init.normal_(m.weight.data)  # normal: mean=0, std=1


layer_nums = 100  # 网络层数
neural_nums = 256  # 每层的神经元个数
batch_size = 16  # 输入数据的 batch size

net = MLP(neural_nums, layer_nums)
net.initialize()

inputs = torch.randn((batch_size, neural_nums))  # normal: mean=0, std=1
output = net(inputs)
print(output)

输出结果:

tensor([[nan, nan, nan,  ..., nan, nan, nan],
        [nan, nan, nan,  ..., nan, nan, nan],
        [nan, nan, nan,  ..., nan, nan, nan],
        ...,
        [nan, nan, nan,  ..., nan, nan, nan],
        [nan, nan, nan,  ..., nan, nan, nan],
        [nan, nan, nan,  ..., nan, nan, nan]], grad_fn=)

我们发现 output 中的每一个值都是 nan,这表明我们的数据值可能非常大或者非常小,已经超出了当前精度能够表示的范围。我们可以修改一下 forward 函数,来看一下什么时候我们的数据变为了 nan。这里,我们采用标准差作为指标来衡量数据的尺度范围。首先我们打印出每层的标准差,接着进行一个 if 判断,如果 x 的标准差变为 nan 了则停止前向传播。

def forward(self, x):
    for (i, linear) in enumerate(self.linears):
        x = linear(x)

        print("layer:{}, std:{}".format(i, x.std()))  # 打印每层的标准差
        if torch.isnan(x.std()):
            print("output is nan in {} layers".format(i))
            break  # 如果 x 的标准差变为 nan 则停止前向传播

        return x

输出结果:

layer:0, std:15.959932327270508
layer:1, std:256.6237487792969
layer:2, std:4107.24560546875
.
.
.
layer:29, std:1.322983152787379e+36
layer:30, std:2.0786820453988485e+37
layer:31, std:nan
output is nan in 31 layers

可以看到,当进行到 31 层的时候,数据的标准差就已经变为 nan 了。我们看到,在第 31 层的时候,数据的值都非常大或者非常小,再往后传播,计算机当前的精度就已经没办法去表示这些特别大或者特别小的数据了。另外,可以看到每一层网络的标准差都是逐渐增大的,直到第 31 层,大约在 1 0 37 − 1 0 38 10^{37} - 10^{38} 10371038 之间,而这已经超出了我们当前精度可以表示的数据范围。

下面我们通过方差的公式推导来观察为什么网络层输出的标准差会越来越大, 最终超出可表示的范 围。假设 X X X Y Y Y 是两个相互独立的随机变量, 我们知道:
E ( X ∗ Y ) = E ( X ) ∗ E ( Y ) Var ⁡ ( X ) = E ( X 2 ) − [ E ( X ) ] 2 Var ⁡ ( X + Y ) = Var ⁡ ( X ) + Var ⁡ ( Y ) \begin{aligned} \mathrm{E}(X * Y) & =\mathrm{E}(X) * \mathrm{E}(Y) \\ \operatorname{Var}(X) & =\mathrm{E}\left(X^2\right)-[\mathrm{E}(X)]^2 \\ \operatorname{Var}(X+Y) & =\operatorname{Var}(X)+\operatorname{Var}(Y) \end{aligned} E(XY)Var(X)Var(X+Y)=E(X)E(Y)=E(X2)[E(X)]2=Var(X)+Var(Y)
然后, 我们有:
Var ⁡ ( X ∗ Y ) = Var ⁡ ( X ) ∗ Var ⁡ ( Y ) + Var ⁡ ( X ) ∗ [ E ( Y ) ] 2 + Var ⁡ ( Y ) ∗ [ E ( X ) ] 2 \operatorname{Var}(X * Y)=\operatorname{Var}(X) * \operatorname{Var}(Y)+\operatorname{Var}(X) *[\mathrm{E}(Y)]^2+\operatorname{Var}(Y) *[\mathrm{E}(X)]^2 Var(XY)=Var(X)Var(Y)+Var(X)[E(Y)]2+Var(Y)[E(X)]2
如果 E ( X ) = 0 , E ( Y ) = 0 \mathrm{E}(X)=0, \mathrm{E}(Y)=0 E(X)=0,E(Y)=0, 那么我们有:
Var ⁡ ( X ∗ Y ) = Var ⁡ ( X ) ∗ Var ⁡ ( Y ) \operatorname{Var}(X * Y)=\operatorname{Var}(X) * \operatorname{Var}(Y) Var(XY)=Var(X)Var(Y)

下面我们来计算网络层神经元的标准差:

ch04-损失优化_第2张图片
我们以输入层第一个神经元为例:

H 11 = ∑ i = 0 n X i ∗ W 1 i H_{11}=\sum_{i=0}^n X_i * W_{1 i} H11=i=0nXiW1i

其中输入 X X X 和权值 W W W 都是服从 N ( 0 , 1 ) N(0,1) N(0,1) 的正态分布,所以这个神经元的方差为:

Var ⁡ ( H 11 ) = ∑ i = 0 n Var ⁡ ( X i ) ∗ Var ⁡ ( W 1 i ) = n ∗ ( 1 ∗ 1 ) = n \begin{aligned} \operatorname{Var}\left(H_{11}\right) & =\sum_{i=0}^n \operatorname{Var}\left(X_i\right) * \operatorname{Var}\left(W_{1 i}\right) \\ & =n *(1 * 1) \\ & =n \end{aligned} Var(H11)=i=0nVar(Xi)Var(W1i)=n(11)=n
所以,
Std ⁡ ( H 11 ) = Var ⁡ ( H 11 ) = n \operatorname{Std}\left(H_{11}\right)=\sqrt{\operatorname{Var}\left(H_{11}\right)}=\sqrt{n} Std(H11)=Var(H11) =n
可以看到, 第一个隐藏层中神经元的方差变为了 n n n, 而输入 X X X 的方差为 1 。也就是说, 经过第一个 网络层 H 1 H_1 H1 的前向传播, 数据的方差扩大了 n n n 倍, 标准差扩大了 n \sqrt{n} n 倍。同理, 如果继续传播到下 一个隐藏层 H 2 H_2 H2, 通过公式推导, 可知该层神经元的标准差为 n 0 n_0 n0 这样不断传播下去, 每经过一层, 输出数据的尺度范围都将不断扩大 n \sqrt{n} n 倍, 最冬将超出我们的精度可表示的范围, 变为 nan。
在代码中, 我们设置的每层网络中神经元个数 n = 256 n=256 n=256, 所以 n = 16 \sqrt{n}=16 n =16 。我们来看一下前面输出 结果中的每个网络层输出的标准差是否符合这一规律:

  • 第 0 层数据标准差为 15.959932327270508 ≈ 16 15.959932327270508 \approx 16 15.95993232727050816
  • 第 1 层数据标准差为 256.6237487792969 ≈ 1 6 2 = 256 256.6237487792969 \approx 16^2=256 256.6237487792969162=256
  • 第 2 层数据标准差为 4107.24560546875 ≈ 1 6 3 = 4096 4107.24560546875 \approx 16^3=4096 4107.24560546875163=4096
  • 第 3 层数据标准差为 65576.8125 ≈ 1 6 4 = 65536 65576.8125 \approx 16^4=65536 65576.8125164=65536

每经过一层, 数据的标准差都会扩大 16 倍, 经过一层层传播后, 数据的标准差将变得非常大, 最终 在第 31 层时超出了精度可表示的范围,即为 nan。
下面我们将每层神经元个数修改为 n = 400 n=400 n=400, 所以 n = 20 \sqrt{n}=20 n =20, 观察结果是否会发生相应的变化:

layer_nums = 100  # 网络层数
neural_nums = 400  # 每层的神经元个数
batch_size = 16  # 输入数据的 batch size

输出结果:

layer:0, std:20.191545486450195
layer:1, std:406.2967834472656
layer:2, std:8196.0322265625
layer:3, std:164936.546875
layer:4, std:3324399.75
layer:5, std:65078964.0
layer:6, std:1294259712.0
layer:7, std:25718734848.0
layer:8, std:509478502400.0
layer:9, std:10142528569344.0
layer:10, std:204187744862208.0
layer:11, std:4146330289045504.0
layer:12, std:8.175371463688192e+16
layer:13, std:1.6178185228915835e+18
layer:14, std:3.201268126493075e+19
layer:15, std:6.43244420071468e+20
layer:16, std:1.2768073112864894e+22
layer:17, std:2.5327442663597998e+23
layer:18, std:4.97064812888673e+24
layer:19, std:9.969679340542473e+25
layer:20, std:1.9616922876332235e+27
layer:21, std:3.926491184057203e+28
layer:22, std:7.928349353787082e+29
layer:23, std:1.5731294716685355e+31
layer:24, std:3.156214979388958e+32
layer:25, std:6.18353463606124e+33
layer:26, std:1.2453666891690611e+35
layer:27, std:2.467429285844339e+36
layer:28, std:4.977222187097705e+37
layer:29, std:nan
output is nan in 29 layers
tensor([[-inf, inf, inf,  ..., -inf, nan, nan],
        [nan, nan, inf,  ..., -inf, -inf, nan],
        [nan, -inf, nan,  ..., inf, nan, nan],
        ...,
        [nan, -inf, -inf,  ..., -inf, nan, nan],
        [inf, -inf, nan,  ..., inf, -inf, nan],
        [inf, nan, inf,  ..., inf, nan, inf]], grad_fn=)

可以看到:

  • 第 0 层数据标准差为 20.191545486450195 ≈ 20 20.191545486450195 \approx 20 20.19154548645019520
  • 第 1 层数据标准差为 406.2967834472656 ≈ 2 0 2 = 400 406.2967834472656 \approx 20^2=400 406.2967834472656202=400
  • 第 2 层数据标准差为 8196.0322265625 ≈ 2 0 3 = 8000 8196.0322265625 \approx 20^3=8000 8196.0322265625203=8000
  • 第 3 层数据标准差为 164936.546875 ≈ 2 0 4 = 160000 164936.546875 \approx 20^4=160000 164936.546875204=160000

每经过一层, 数据的标准差大约会扩大 20 倍, 最终在第 29 层时超出了腈度可表示的范围, 变为 nan .从前面的公式中可以看到, 每个网络层输出数据的标准差由三个因素决定: 网络层的神经元个数 n n n 、 输入值 X X X 的方差 Var ⁡ ( X ) \operatorname{Var}(X) Var(X), 以及网络层权值 W W W 的方差 Var ⁡ ( W ) \operatorname{Var}(W) Var(W) 。因此,如果我们希望让网络层输出数据的方差保持尺度不变, 那么我们必须令其方差等于 1 , 即:
Var ⁡ ( H 1 ) = n ∗ Var ⁡ ( X ) ∗ Var ⁡ ( W ) = 1 \operatorname{Var}\left(H_1\right)=n * \operatorname{Var}(X) * \operatorname{Var}(W)=1 Var(H1)=nVar(X)Var(W)=1
因此,
Var ⁡ ( W ) = 1 n ⟹ Std ⁡ ( W ) = 1 n \operatorname{Var}(W)=\frac{1}{n} \quad \Longrightarrow \quad \operatorname{Std}(W)=\sqrt{\frac{1}{n}} Var(W)=n1Std(W)=n1
所以, 当我们将网络层权值的标准差设为 1 n \sqrt{\frac{1}{n}} n1 时, 输出数据的标准差将变为 1 。
下面我们修改代码, 使用一个均值为 0 , 标准差为 1 n \sqrt{\frac{1}{n}} n1 的分布来初始化权值矩阵 W W W, 观察网络层 输出数据的标准差会如何变化:

 def initialize(self):
    for m in self.modules():
        if isinstance(m, nn.Linear):
            nn.init.normal_(m.weight.data, std=np.sqrt(1/self.neural_num))    # normal: mean=0, std=sqrt(1/n)

输出结果:

layer:0, std:0.9974957704544067
layer:1, std:1.0024365186691284
layer:2, std:1.002745509147644
layer:3, std:1.0006227493286133
layer:4, std:0.9966009855270386
layer:5, std:1.019859790802002
layer:6, std:1.026173710823059
layer:7, std:1.0250457525253296
layer:8, std:1.0378952026367188
layer:9, std:1.0441951751708984
layer:10, std:1.0181655883789062
layer:11, std:1.0074602365493774
layer:12, std:0.9948930144309998
layer:13, std:0.9987586140632629
layer:14, std:0.9981392025947571
layer:15, std:1.0045733451843262
layer:16, std:1.0055204629898071
layer:17, std:1.0122840404510498
layer:18, std:1.0076017379760742
layer:19, std:1.000280737876892
layer:20, std:0.9943006038665771
layer:21, std:1.012800931930542
layer:22, std:1.012657642364502
layer:23, std:1.018149971961975
layer:24, std:0.9776086211204529
layer:25, std:0.9592394828796387
layer:26, std:0.9317858815193176
layer:27, std:0.9534041881561279
layer:28, std:0.9811319708824158
layer:29, std:0.9953019022941589
layer:30, std:0.9773916006088257
layer:31, std:0.9655940532684326
layer:32, std:0.9270440936088562
layer:33, std:0.9329946637153625
layer:34, std:0.9311841726303101
layer:35, std:0.9354336261749268
layer:36, std:0.9492132067680359
layer:37, std:0.9679954648017883
layer:38, std:0.9849981665611267
layer:39, std:0.9982335567474365
layer:40, std:0.9616852402687073
layer:41, std:0.9439758658409119
layer:42, std:0.9631161093711853
layer:43, std:0.958673894405365
layer:44, std:0.9675614237785339
layer:45, std:0.9837557077407837
layer:46, std:0.9867278337478638
layer:47, std:0.9920817017555237
layer:48, std:0.9650403261184692
layer:49, std:0.9991624355316162
layer:50, std:0.9946174025535583
layer:51, std:0.9662044048309326
layer:52, std:0.9827387928962708
layer:53, std:0.9887880086898804
layer:54, std:0.9932605624198914
layer:55, std:1.0237400531768799
layer:56, std:0.9702046513557434
layer:57, std:1.0045380592346191
layer:58, std:0.9943899512290955
layer:59, std:0.9900636076927185
layer:60, std:0.99446702003479
layer:61, std:0.9768352508544922
layer:62, std:0.9797843098640442
layer:63, std:0.9951220750808716
layer:64, std:0.9980446696281433
layer:65, std:1.0086933374404907
layer:66, std:1.0276142358779907
layer:67, std:1.0429234504699707
layer:68, std:1.0197855234146118
layer:69, std:1.0319130420684814
layer:70, std:1.0540012121200562
layer:71, std:1.026781439781189
layer:72, std:1.0331352949142456
layer:73, std:1.0666675567626953
layer:74, std:1.0413838624954224
layer:75, std:1.0733673572540283
layer:76, std:1.0404183864593506
layer:77, std:1.0344083309173584
layer:78, std:1.0022705793380737
layer:79, std:0.99835205078125
layer:80, std:0.9732587337493896
layer:81, std:0.9777462482452393
layer:82, std:0.9753198623657227
layer:83, std:0.9938382506370544
layer:84, std:0.9472599029541016
layer:85, std:0.9511011242866516
layer:86, std:0.9737769961357117
layer:87, std:1.005651831626892
layer:88, std:1.0043526887893677
layer:89, std:0.9889539480209351
layer:90, std:1.0130352973937988
layer:91, std:1.0030947923660278
layer:92, std:0.9993206262588501
layer:93, std:1.0342745780944824
layer:94, std:1.031973123550415
layer:95, std:1.0413124561309814
layer:96, std:1.0817031860351562
layer:97, std:1.128799557685852
layer:98, std:1.1617802381515503
layer:99, std:1.2215303182601929
tensor([[-1.0696, -1.1373,  0.5047,  ..., -0.4766,  1.5904, -0.1076],
        [ 0.4572,  1.6211,  1.9659,  ..., -0.3558, -1.1235,  0.0979],
        [ 0.3908, -0.9998, -0.8680,  ..., -2.4161,  0.5035,  0.2814],
        ...,
        [ 0.1876,  0.7971, -0.5918,  ...,  0.5395, -0.8932,  0.1211],
        [-0.0102, -1.5027, -2.6860,  ...,  0.6954, -0.1858, -0.8027],
        [-0.5871, -1.3739, -2.9027,  ...,  1.6734,  0.5094, -0.9986]],
       grad_fn=)

可以看到,第 99 层的输出值都在一个比较正常的范围,并且每一层输出数据的标准差都在 1 附近,所以现在我们得到了一个比较理想的输出数据分布。代码实验的结果也验证了我们前面公式推导的正确性:通过采用合适的权值初始化方法,可以使得多层全连接网络的输出值的数据尺度维持在一定范围内,而不会变得过大或者过小。

在上面的例子中,我们通过权重初始化保证了每层输出数据的方差为 1 ,但是这里我们还没有考虑激活函数的存在。下面我们看一下 具有激活函数时的权值初始化。我们在前向传播 forward 函数中加入 tanh 激活函数:

def forward(self, x):
    for (i, linear) in enumerate(self.linears):
        x = linear(x)
        x = torch.tanh(x)

        print("layer:{}, std:{}".format(i, x.std()))  # 打印每层的标准差
        if torch.isnan(x.std()):
            print("output is nan in {} layers".format(i))
            break

    return x

输出结果:

layer:0, std:0.6273701786994934
layer:1, std:0.48910173773765564
layer:2, std:0.4099564850330353
layer:3, std:0.35637012124061584
layer:4, std:0.32117360830307007
layer:5, std:0.2981105148792267
layer:6, std:0.27730831503868103
layer:7, std:0.2589356303215027
layer:8, std:0.2468511462211609
layer:9, std:0.23721906542778015
layer:10, std:0.22171513736248016
layer:11, std:0.21079954504966736
layer:12, std:0.19820132851600647
layer:13, std:0.19069305062294006
layer:14, std:0.18555502593517303
layer:15, std:0.17953835427761078
layer:16, std:0.17485806345939636
layer:17, std:0.1702701896429062
layer:18, std:0.16508983075618744
layer:19, std:0.1591130942106247
layer:20, std:0.15480300784111023
layer:21, std:0.15263864398002625
layer:22, std:0.148549422621727
layer:23, std:0.14617665112018585
layer:24, std:0.13876432180404663
layer:25, std:0.13316625356674194
layer:26, std:0.12660598754882812
layer:27, std:0.12537942826747894
layer:28, std:0.12535445392131805
layer:29, std:0.12589804828166962
layer:30, std:0.11994210630655289
layer:31, std:0.11700887233018875
layer:32, std:0.11137297749519348
layer:33, std:0.11154612898826599
layer:34, std:0.10991233587265015
layer:35, std:0.10996390879154205
layer:36, std:0.10969001054763794
layer:37, std:0.10975216329097748
layer:38, std:0.11063200235366821
layer:39, std:0.11021336913108826
layer:40, std:0.10465587675571442
layer:41, std:0.10141163319349289
layer:42, std:0.1026025265455246
layer:43, std:0.10079070925712585
layer:44, std:0.10096712410449982
layer:45, std:0.10117629915475845
layer:46, std:0.10145658999681473
layer:47, std:0.09987485408782959
layer:48, std:0.09677786380052567
layer:49, std:0.099615179002285
layer:50, std:0.09867013245820999
layer:51, std:0.09398546814918518
layer:52, std:0.09388342499732971
layer:53, std:0.09352942556142807
layer:54, std:0.09336657077074051
layer:55, std:0.0948176234960556
layer:56, std:0.08856320381164551
layer:57, std:0.09024856984615326
layer:58, std:0.088644839823246
layer:59, std:0.08766943216323853
layer:60, std:0.08726289123296738
layer:61, std:0.08623495697975159
layer:62, std:0.08549778908491135
layer:63, std:0.0855521708726883
layer:64, std:0.0853666365146637
layer:65, std:0.08462794870138168
layer:66, std:0.0852193832397461
layer:67, std:0.08562126755714417
layer:68, std:0.08368431031703949
layer:69, std:0.08476374298334122
layer:70, std:0.0853630006313324
layer:71, std:0.08237560093402863
layer:72, std:0.08133518695831299
layer:73, std:0.08416958898305893
layer:74, std:0.08226992189884186
layer:75, std:0.08379074186086655
layer:76, std:0.08003697544336319
layer:77, std:0.07888862490653992
layer:78, std:0.07618380337953568
layer:79, std:0.07458437979221344
layer:80, std:0.07207276672124863
layer:81, std:0.07079190015792847
layer:82, std:0.0712786465883255
layer:83, std:0.07165777683258057
layer:84, std:0.06893909722566605
layer:85, std:0.0690247192978859
layer:86, std:0.07030878216028214
layer:87, std:0.07283661514520645
layer:88, std:0.07280214875936508
layer:89, std:0.07130246609449387
layer:90, std:0.07225215435028076
layer:91, std:0.0712454542517662
layer:92, std:0.07088854163885117
layer:93, std:0.0730612576007843
layer:94, std:0.07276967912912369
layer:95, std:0.07259567081928253
layer:96, std:0.07586522400379181
layer:97, std:0.07769150286912918
layer:98, std:0.07842090725898743
layer:99, std:0.08206238597631454
tensor([[-0.1103, -0.0739,  0.1278,  ..., -0.0508,  0.1544, -0.0107],
        [ 0.0807,  0.1208,  0.0030,  ..., -0.0385, -0.1887, -0.0294],
        [ 0.0321, -0.0833, -0.1482,  ..., -0.1133,  0.0206,  0.0155],
        ...,
        [ 0.0108,  0.0560, -0.1099,  ...,  0.0459, -0.0961, -0.0124],
        [ 0.0398, -0.0874, -0.2312,  ...,  0.0294, -0.0562, -0.0556],
        [-0.0234, -0.0297, -0.1155,  ...,  0.1143,  0.0083, -0.0675]],
       grad_fn=)

可以看到,随着网络层的前向传播,每层输出值的标准差越来越小,最终可能会导致 梯度消失,这并不是我们所希望看到的。

1.2. Xavier 初始化

  • 参考文献:Understanding the difficulty of training deep feedforward neural networks

针对上面具有激活函数情况的问题,2010 年 Xavier 在一篇论文中详细探讨了在具有激活函数的情况下应该如何初始化的问题。在文献中,结合方差一致性原则 (即让每层网络输出值的方差尽量在 1 附近),同时作者对 Sigmoid、tanh 这类饱和激活函数进行分析。

  • 方差一致性:保持数据尺度维持在恰当范围,通常方差为 1 。

  • 激活函数:饱和函数,如 Sigmoid、Tanh。

通过论文中的公式推导, 我们可以得到以下两个等式:
n i ∗ Var ⁡ ( W ) = 1 n i + 1 ∗ Var ⁡ ( W ) = 1 \begin{gathered} n_i * \operatorname{Var}(W)=1 \\ n_{i+1} * \operatorname{Var}(W)=1 \end{gathered} niVar(W)=1ni+1Var(W)=1
其中, n i n_i ni 是输入层的神经元个数, n i + 1 n_{i+1} ni+1 是输出层的神经元个数。即我们同时考虑了前向传播和反向 传播过程中的数据尺度问题。

同时, 结合方差一致性原则, 我们可以得到权值 W W W 的方差为:
Var ⁡ ( W ) = 2 n i + n i + 1 \operatorname{Var}(W)=\frac{2}{n_i+n_{i+1}} Var(W)=ni+ni+12
通常, Xavier 采用的是均匀分布。下面我们来推导均匀分布是上限和下限, 这里我们假设上限是 a a a, 那么下限为 − a -a a, 因为我们通常采用的是零均值, 所以上下限之间是对称关系。
W ∼ U [ − a , a ] W \sim U[-a, a] WU[a,a]
根据均匀分布的方差公式,我们得到:
Var ⁡ ( W ) = ( − a − a ) 2 12 = ( 2 a ) 2 12 = a 2 3 \operatorname{Var}(W)=\frac{(-a-a)^2}{12}=\frac{(2 a)^2}{12}=\frac{a^2}{3} Var(W)=12(aa)2=12(2a)2=3a2
然后, 我们有:
2 n i + n i + 1 = a 2 3 ⟹ a = 6 n i + n i + 1 \frac{2}{n_i+n_{i+1}}=\frac{a^2}{3} \quad \Longrightarrow \quad a=\frac{\sqrt{6}}{\sqrt{n_i+n_{i+1}}} ni+ni+12=3a2a=ni+ni+1 6
所以,
W ∼ U [ − 6 n i + n i + 1 , 6 n i + n i + 1 ] W \sim U\left[-\frac{\sqrt{6}}{\sqrt{n_i+n_{i+1}}}, \frac{\sqrt{6}}{\sqrt{n_i+n_{i+1}}}\right] WU[ni+ni+1 6 ,ni+ni+1 6 ]

代码示例:

我们可以通过手动计算实现 Xavier 初始化:

def initialize(self):
    for m in self.modules():
        if isinstance(m, nn.Linear):
            a = np.sqrt(6 / (self.neural_num + self.neural_num))
            tanh_gain = nn.init.calculate_gain('tanh')  # 计算激活函数的增益
            a *= tanh_gain
            nn.init.uniform_(m.weight.data, -a, a)

另外,PyTorch 中也内置了 Xavier 初始化方法,其结果和我们手动计算的结果是一致的:

def initialize(self):
    for m in self.modules():
        if isinstance(m, nn.Linear):
            tanh_gain = nn.init.calculate_gain('tanh')
            nn.init.xavier_uniform_(m.weight.data, gain=tanh_gain)

输出结果:

layer:0, std:0.7571136355400085
layer:1, std:0.6924336552619934
layer:2, std:0.6677976846694946
layer:3, std:0.6551960110664368
layer:4, std:0.655646800994873
layer:5, std:0.6536089777946472
layer:6, std:0.6500504612922668
layer:7, std:0.6465446949005127
layer:8, std:0.6456685662269592
layer:9, std:0.6414617896080017
layer:10, std:0.6423627734184265
layer:11, std:0.6509683728218079
layer:12, std:0.6584846377372742
layer:13, std:0.6530249118804932
layer:14, std:0.6528729796409607
layer:15, std:0.6523412466049194
layer:16, std:0.6534921526908875
layer:17, std:0.6540238261222839
layer:18, std:0.6477403044700623
layer:19, std:0.6469652652740479
layer:20, std:0.6441705822944641
layer:21, std:0.6484488248825073
layer:22, std:0.6512865424156189
layer:23, std:0.6525684595108032
layer:24, std:0.6531476378440857
layer:25, std:0.6488809585571289
layer:26, std:0.6533839702606201
layer:27, std:0.6482065320014954
layer:28, std:0.6471589803695679
layer:29, std:0.6553042531013489
layer:30, std:0.6560811400413513
layer:31, std:0.6522760987281799
layer:32, std:0.6499098539352417
layer:33, std:0.6568747758865356
layer:34, std:0.6544532179832458
layer:35, std:0.6535674929618835
layer:36, std:0.6508696675300598
layer:37, std:0.6428772807121277
layer:38, std:0.6495102643966675
layer:39, std:0.6479291319847107
layer:40, std:0.6470604538917542
layer:41, std:0.6513484716415405
layer:42, std:0.6503545045852661
layer:43, std:0.6458993554115295
layer:44, std:0.6517387628555298
layer:45, std:0.6520006060600281
layer:46, std:0.6539937257766724
layer:47, std:0.6537032723426819
layer:48, std:0.6516646146774292
layer:49, std:0.6535552740097046
layer:50, std:0.6464877724647522
layer:51, std:0.6491119265556335
layer:52, std:0.6455202102661133
layer:53, std:0.6520237326622009
layer:54, std:0.6531855463981628
layer:55, std:0.6627183556556702
layer:56, std:0.6544181108474731
layer:57, std:0.6501768827438354
layer:58, std:0.6510448455810547
layer:59, std:0.6549468040466309
layer:60, std:0.6529951691627502
layer:61, std:0.6515748500823975
layer:62, std:0.6453633904457092
layer:63, std:0.644793689250946
layer:64, std:0.6489539742469788
layer:65, std:0.6553947925567627
layer:66, std:0.6535270810127258
layer:67, std:0.6528791785240173
layer:68, std:0.6492816209793091
layer:69, std:0.6596571207046509
layer:70, std:0.6536712646484375
layer:71, std:0.6498764157295227
layer:72, std:0.6538681387901306
layer:73, std:0.64595627784729
layer:74, std:0.6543275117874146
layer:75, std:0.6525828838348389
layer:76, std:0.6462088227272034
layer:77, std:0.6534948945045471
layer:78, std:0.6461930871009827
layer:79, std:0.6457878947257996
layer:80, std:0.6481245160102844
layer:81, std:0.6496317386627197
layer:82, std:0.6516988277435303
layer:83, std:0.6485154032707214
layer:84, std:0.6395408511161804
layer:85, std:0.6498249173164368
layer:86, std:0.6510564088821411
layer:87, std:0.6505221724510193
layer:88, std:0.6573457717895508
layer:89, std:0.6529723405838013
layer:90, std:0.6536353230476379
layer:91, std:0.6497699022293091
layer:92, std:0.6459059715270996
layer:93, std:0.6459072232246399
layer:94, std:0.6530925631523132
layer:95, std:0.6515892148017883
layer:96, std:0.6434286832809448
layer:97, std:0.6425578594207764
layer:98, std:0.6407340168952942
layer:99, std:0.6442393660545349
tensor([[ 0.1133,  0.1239,  0.8211,  ...,  0.9411, -0.6334,  0.5155],
        [-0.9585, -0.2371,  0.8548,  ..., -0.2339,  0.9326,  0.0114],
        [ 0.9487, -0.2279,  0.8735,  ..., -0.9593,  0.7922,  0.6263],
        ...,
        [ 0.7257,  0.0800, -0.4440,  ..., -0.9589,  0.2604,  0.5402],
        [-0.9572,  0.5179, -0.8041,  ..., -0.4298, -0.6087,  0.9679],
        [ 0.6105,  0.3994,  0.1072,  ...,  0.3904, -0.5274,  0.0776]],
       grad_fn=)

可以看到,每层网络输出值的标准差都在 1 左右,这表明每层网络的输出值都不会过大或者过小。并且,最后第 99 层的输出值也在一个比较正常的范围内。

1.3. Kaiming 初始化

虽然在 2010 年,Xavier 针对诸如 Sigmoid、tanh 这类饱和激活函数给出了有效的初始化方法。但是,也是在同一年 Xnet 出现之后,非饱和激活函数 ReLU 被广泛使用,由于非饱和函数的性质,Xavier 初始化方法将不再适用。

在下面的代码中,将激活函数改为 ReLU,并且仍然使用 Xavier 初始化方法,我们来观察一下网络层的输出:

def forward(self, x):
    for (i, linear) in enumerate(self.linears):
        x = linear(x)
        x = torch.relu(x)

        print("layer:{}, std:{}".format(i, x.std()))  # 打印每层的标准差
        if torch.isnan(x.std()):
            print("output is nan in {} layers".format(i))
            break

    return x

输出结果:

layer:0, std:0.9689465165138245
layer:1, std:1.0872339010238647
layer:2, std:1.2967971563339233
layer:3, std:1.4487521648406982
layer:4, std:1.8563750982284546
layer:5, std:2.2424941062927246
layer:6, std:2.679966449737549
layer:7, std:3.2177586555480957
layer:8, std:3.8579354286193848
layer:9, std:4.413454532623291
layer:10, std:5.518202781677246
layer:11, std:6.072154521942139
layer:12, std:7.441657543182373
layer:13, std:8.963356971740723
layer:14, std:10.747811317443848
layer:15, std:13.216470718383789
layer:16, std:15.070353507995605
layer:17, std:17.297853469848633
layer:18, std:19.603160858154297
layer:19, std:22.72492218017578
layer:20, std:27.811525344848633
layer:21, std:34.64209747314453
layer:22, std:43.16114044189453
layer:23, std:51.901859283447266
layer:24, std:59.3619384765625
layer:25, std:63.65275955200195
layer:26, std:61.95321273803711
layer:27, std:79.72232055664062
layer:28, std:99.41972351074219
layer:29, std:118.13148498535156
layer:30, std:128.12930297851562
layer:31, std:140.68907165527344
layer:32, std:165.6183319091797
layer:33, std:196.19956970214844
layer:34, std:214.2675323486328
layer:35, std:282.7183532714844
layer:36, std:317.1474304199219
layer:37, std:373.9003601074219
layer:38, std:412.70892333984375
layer:39, std:529.4519653320312
layer:40, std:532.9295654296875
layer:41, std:630.9380493164062
layer:42, std:762.7489624023438
layer:43, std:813.0692138671875
layer:44, std:1022.0352783203125
layer:45, std:1363.53759765625
layer:46, std:1734.6246337890625
layer:47, std:1899.3427734375
layer:48, std:2251.1640625
layer:49, std:2680.478759765625
layer:50, std:3370.64794921875
layer:51, std:4003.856201171875
layer:52, std:4598.98779296875
layer:53, std:5199.58447265625
layer:54, std:6399.32568359375
layer:55, std:8127.16064453125
layer:56, std:9794.875
layer:57, std:11728.7431640625
layer:58, std:15471.70703125
layer:59, std:19942.44921875
layer:60, std:22642.23046875
layer:61, std:28904.16796875
layer:62, std:37538.265625
layer:63, std:41843.19921875
layer:64, std:48306.828125
layer:65, std:56072.6171875
layer:66, std:59877.83984375
layer:67, std:57911.44140625
layer:68, std:68525.5859375
layer:69, std:78614.9609375
layer:70, std:103845.0
layer:71, std:121762.9375
layer:72, std:128452.984375
layer:73, std:146725.484375
layer:74, std:168575.125
layer:75, std:176617.84375
layer:76, std:202430.046875
layer:77, std:247756.625
layer:78, std:310793.875
layer:79, std:374327.34375
layer:80, std:456118.53125
layer:81, std:545246.25
layer:82, std:550071.8125
layer:83, std:653713.0
layer:84, std:831133.9375
layer:85, std:1045186.3125
layer:86, std:1184264.0
layer:87, std:1334159.5
layer:88, std:1589417.75
layer:89, std:1783507.25
layer:90, std:2239068.0
layer:91, std:2429546.0
layer:92, std:2928562.0
layer:93, std:2883037.75
layer:94, std:3230928.75
layer:95, std:3661650.75
layer:96, std:4741352.0
layer:97, std:5300345.0
layer:98, std:6797732.0
layer:99, std:7640650.0
tensor([[       0.0000,  3028736.2500, 12379591.0000,  ...,
          3593889.2500,        0.0000, 24658882.0000],
        [       0.0000,  2758786.0000, 11016991.0000,  ...,
          2970399.7500,        0.0000, 23173860.0000],
        [       0.0000,  2909416.2500, 13117423.0000,  ...,
          3867089.2500,        0.0000, 28463550.0000],
        ...,
        [       0.0000,  3913293.2500, 15489672.0000,  ...,
          5777762.0000,        0.0000, 33226552.0000],
        [       0.0000,  3673798.2500, 12739622.0000,  ...,
          4193501.0000,        0.0000, 26862400.0000],
        [       0.0000,  1913917.0000, 10243700.0000,  ...,
          4573404.0000,        0.0000, 22720538.0000]],
       grad_fn=)

可以看到,当激活函数改为 ReLU 之后,如果我们仍然采用 Xavier 初始化,那么每一层的输出值标准差将逐渐增大:由最初第 0 层的 1 左右逐渐增大到最终第 99 层的 764 万。这并不是我们所希望的。

针对这一问题,在 2015 年,何凯明等人在一篇论文中提出了解决方法:

参考文献:Delving deep into rectifiers: Surpassing human-level performance on ImageNet classification

方差一致性:保持数据尺度维持在恰当范围,通常方差为 1 。

激活函数:ReLU 及其变种。

在论文中, 我们同样遵循方差一致性原则, 即使得输出层方差为 1 。论文中针对 ReLU 激活函数, 通 过公式推导,得到权值 W W W 的方差为:
Var ⁡ ( W ) = 2 n i \operatorname{Var}(W)=\frac{2}{n_i} Var(W)=ni2
其中, n i n_i ni 为输入层的神经元个数。
进一步地, 针对 ReLU 的变种, 即在激活函数的负半轴上给予一定斜率的情况下, 权值 W W W 的方差 为:
Var ⁡ ( W ) = 2 ( 1 + a 2 ) ∗ n i \operatorname{Var}(W)=\frac{2}{\left(1+a^2\right) * n_i} Var(W)=(1+a2)ni2
其中, a a a 为激活函数在负半轴上的斜率。 a = 0 a=0 a=0 时激活函数即为原始的 ReLU。
由此得到权值 W W W 的标准差为:
Std ⁡ ( W ) = 2 ( 1 + a 2 ) ∗ n i \operatorname{Std}(W)=\sqrt{\frac{2}{\left(1+a^2\right) * n_i}} Std(W)=(1+a2)ni2

下面我们根据这一公式进行权值初始化,并观察各网络层的输出。我们可以采用手动计算实现 Kaiming 初始化方法:

def initialize(self):
    for m in self.modules():
        if isinstance(m, nn.Linear):
            nn.init.normal_(m.weight.data, std=np.sqrt(2 / self.neural_num))

或者直接使用 PyTorch 中内置的 Kaiming 初始化方法,两者的结果是一致的:

def initialize(self):
    for m in self.modules():
        if isinstance(m, nn.Linear):
            nn.init.kaiming_normal_(m.weight.data)

输出结果:

layer:0, std:0.826629638671875
layer:1, std:0.8786815404891968
layer:2, std:0.9134422540664673
layer:3, std:0.8892471194267273
layer:4, std:0.834428071975708
layer:5, std:0.874537467956543
layer:6, std:0.7926971316337585
layer:7, std:0.7806458473205566
layer:8, std:0.8684563636779785
layer:9, std:0.9434137344360352
layer:10, std:0.964215874671936
layer:11, std:0.8896796107292175
layer:12, std:0.8287257552146912
layer:13, std:0.8519769906997681
layer:14, std:0.8354345560073853
layer:15, std:0.802306056022644
layer:16, std:0.8613607287406921
layer:17, std:0.7583686709403992
layer:18, std:0.8120225071907043
layer:19, std:0.791111171245575
layer:20, std:0.7164372801780701
layer:21, std:0.778393030166626
layer:22, std:0.8672043085098267
layer:23, std:0.874812662601471
layer:24, std:0.9020991325378418
layer:25, std:0.8585715889930725
layer:26, std:0.7824353575706482
layer:27, std:0.7968912720680237
layer:28, std:0.8984369039535522
layer:29, std:0.8704465627670288
layer:30, std:0.9860473275184631
layer:31, std:0.9080777168273926
layer:32, std:0.9140636920928955
layer:33, std:1.009956955909729
layer:34, std:0.9909380674362183
layer:35, std:1.0253208875656128
layer:36, std:0.849043607711792
layer:37, std:0.703953742980957
layer:38, std:0.7186155319213867
layer:39, std:0.7250635027885437
layer:40, std:0.7030817270278931
layer:41, std:0.6325559020042419
layer:42, std:0.6623690724372864
layer:43, std:0.6960875988006592
layer:44, std:0.7140733003616333
layer:45, std:0.632905125617981
layer:46, std:0.6458898186683655
layer:47, std:0.7354375720024109
layer:48, std:0.6710687279701233
layer:49, std:0.6939153671264648
layer:50, std:0.6889258027076721
layer:51, std:0.6331773996353149
layer:52, std:0.6029313206672668
layer:53, std:0.6145528554916382
layer:54, std:0.6636686325073242
layer:55, std:0.7440094947814941
layer:56, std:0.7972175478935242
layer:57, std:0.7606149911880493
layer:58, std:0.696868360042572
layer:59, std:0.7306802272796631
layer:60, std:0.6875627636909485
layer:61, std:0.7171440720558167
layer:62, std:0.7646605372428894
layer:63, std:0.7965086698532104
layer:64, std:0.8833740949630737
layer:65, std:0.8592952489852905
layer:66, std:0.8092936873435974
layer:67, std:0.806481122970581
layer:68, std:0.6792410612106323
layer:69, std:0.6583346128463745
layer:70, std:0.5702278017997742
layer:71, std:0.5084435939788818
layer:72, std:0.4869326055049896
layer:73, std:0.46350404620170593
layer:74, std:0.4796811640262604
layer:75, std:0.47372108697891235
layer:76, std:0.45414549112319946
layer:77, std:0.4971912205219269
layer:78, std:0.492794930934906
layer:79, std:0.4422350823879242
layer:80, std:0.4802998900413513
layer:81, std:0.5579248666763306
layer:82, std:0.5283755660057068
layer:83, std:0.5451980829238892
layer:84, std:0.6203726530075073
layer:85, std:0.6571893095970154
layer:86, std:0.703682005405426
layer:87, std:0.7321067452430725
layer:88, std:0.6924356818199158
layer:89, std:0.6652532815933228
layer:90, std:0.6728308796882629
layer:91, std:0.6606621742248535
layer:92, std:0.6094604730606079
layer:93, std:0.6019102334976196
layer:94, std:0.595421552658081
layer:95, std:0.6624555587768555
layer:96, std:0.6377885341644287
layer:97, std:0.6079285740852356
layer:98, std:0.6579315066337585
layer:99, std:0.6668476462364197
tensor([[0.0000, 1.3437, 0.0000,  ..., 0.0000, 0.6444, 1.1867],
        [0.0000, 0.9757, 0.0000,  ..., 0.0000, 0.4645, 0.8594],
        [0.0000, 1.0023, 0.0000,  ..., 0.0000, 0.5148, 0.9196],
        ...,
        [0.0000, 1.2873, 0.0000,  ..., 0.0000, 0.6454, 1.1411],
        [0.0000, 1.3589, 0.0000,  ..., 0.0000, 0.6749, 1.2438],
        [0.0000, 1.1807, 0.0000,  ..., 0.0000, 0.5668, 1.0600]],
       grad_fn=)

可以看到,现在每个网络层输出值的标准差都能维持在一个相同的尺度上,不会过大或者过小,并且输出值也基本都在正常范围内。

1.4. 常用的权值始化方法

通过前面的例子,我们对于权值的初始化方法有了清晰的认识。我们知道,不适当的权值初始化方法会引起网络层的输出值过大或者过小,从而引发梯度的消失或者爆炸,最终导致我们的模型无法正常训练。为了避免这种现象的发生,我们必须控制各网络层输出值的尺度范围。根据公式的推断过程我们知道,我们必须使每个网络层的输出值的方差尽量在
附近,即遵循方差一致性原则,使得输出值方差不会过大或者过小。

下面我们来学习 PyTorch 中提供的 10 种常用的权值初始化方法:

  • 1.Xavier 均匀分布
  • 2.Xavier 正态分布
  • 3.Kaiming 均匀分布
  • 4.Kaiming 正态分布
  • 5.均匀分布
  • 6.正态分布
  • 7.常数分布
  • 8.正交矩阵初始化
  • 9.单位矩阵初始化
  • 10.稀疏矩阵初始化

这里可以分为 4 大类:Xavier 初始化、Kaiming 初始化、三种常用分布初始化,以及三种特殊的矩阵初始化。PyTorch 中提供了这些方法,方便我们进行权值初始化。那么,在进行权值初始化的时候,我们应该选择哪种初始化方法呢?这需要具体问题具体分析,但是无论我们使用哪种初始化方法,都需要遵循方差一致性原则,即每层输出值的方差不能太大或者太小,尽量保持在 1 附近。

1.5. nn.init.calculate_gain

下面我们来学习一个特殊的函数 calculate_gain,它被用于计算激活函数的方差变化尺度。

  • 功能:计算激活函数的 方差变化尺度。
nn.init.calculate_gain(nonlinearity, param=None)

主要参数:

  • nonlinearity:激活函数名称,例如:tanh、Sigmoid、ReLU 等等。
  • param:激活函数的参数,例如:Leaky ReLU 中的 negative_slop。

实际上,该函数计算的是输入数据的方差除以输出数据的方差,即方差变化的比例。

代码示例:

x = torch.randn(10000)  # 通过标准正态分布创建 10000 个数据点
out = torch.tanh(x)  # 将数据输入 tanh 函数

gain = x.std() / out.std()  # 手动计算激活函数的标准差变化尺度
print('gain:{}'.format(gain))

tanh_gain = nn.init.calculate_gain('tanh')  # calculate_gain 计算的激活函数的标准差增益
print('tanh_gain in PyTorch:', tanh_gain)

输出结果:

gain:1.5982500314712524
tanh_gain in PyTorch: 1.6666666666666667

可以看到,tanh 激活函数的标准差增益在 1.6 左右,也就是说,对于均值为 0,标准差为 1 的数据,经过 tanh 函数之后,数据的标准差会减小 1.6 倍左右。

1.6. 总结

本节课中,我们学习了权值初始化方法的准则 —— 方差一致性原则,以及 Xavier 和 Kaiming 权值初始化方法。在下节课中,我们将学习损失函数。

2.损失函数 (一)

在前几节中,我们学习了模型模块中的一些知识,包括如何构建模型以及怎样进行模型初始化。本节我们将开始学习损失函数模块。

2.1. 损失函数的概念

损失函数 (Loss Function):衡量模型输出与真实标签之间的差异。

下面是一个一元线性回归的拟合过程示意图:

ch04-损失优化_第3张图片

图中的:

  • 绿色方块代表训练样本点 ( x i , y i ) \left(x_i, y_i\right) (xi,yi),
  • 蓝色直线代表训练得到的模型 y ^ = w 0 + w 1 x \hat{y}=w_0+w_1 x y^=w0+w1x, 其中,
    • w 0 w_0 w0 代表截距,
    • w 1 = Δ y / Δ x w_1=\Delta y / \Delta x w1=Δyx 代表斜率。

可以看到, 模型并没有完美地拟合每一个数据点, 所以数 据点和模型之间存在一个损失 (Loss), 这里我们采用垂直方向上模型输出与真实数据点之差的绝对 值 ∣ y ^ − y ∣ |\hat{y}-y| y^y 作为损失函数的度量。

另外,当我们谈到损失函数时,经常会涉及到以下三个概念:

  • 损失函数 (Loss Function): 计算单个样本的差异。
     Loss  = f ( y ^ , y ) \text { Loss }=f(\hat{y}, y)  Loss =f(y^,y)
  • 代价函数 (Cost Function):计算整个训练集 Loss 的平均值。
     Cost  = 1 n ∑ i = 1 n f ( y ^ i , y i ) \text { Cost }=\frac{1}{n} \sum_{i=1}^n f\left(\hat{y}_i, y_i\right)  Cost =n1i=1nf(y^i,yi)
  • 目标函数 (Objective Function):最终需要优化的目标,通常包含代价函数和正则项。
     Obj  =  Cost  +  Regularization  \text { Obj }=\text { Cost }+ \text { Regularization }  Obj = Cost + Regularization 

注意, 代价函数并不是越小越好, 因为存在过拟合的风险。所以我们需要加上一些约束 (即正则项) 来防止模型变得过于复杂而导致过拟合, 常用的有 L 1 L 1 L1 L 2 L 2 L2 正则项。因此, 代价函数和正则项最终 构成了我们的目标函数。

在 PyTorch 中的损失函数也是继承于nn.Module,所以损失函数也可以看作网络层。下面我们来看一下 PyTorch 中的 _Loss 类:

class _Loss(Module):
    def __init__(self, size_average=None, reduce=None, reduction='mean'):
        super(_Loss, self).__init__()
        if size_average is not None or reduce is not None:
            self.reduction = _Reduction.legacy_get_string(size_average, reduce)
        else:
            self.reduction = reduction

可以看到,_Loss 是继承于 Module 类的,所以从某种程度上我们可以将 _Loss 也视为一个网络层。

它的初始化函数中主要有 3 个参数,其中 size_averagereduce 这两个参数即将在后续版本中被舍弃,因为 reduction 参数已经可以实现前两者的功能。

2.2. 交叉熵损失函数 nn.CrossEntropyLoss

在分类任务中,我们经常采用的是交叉熵损失函数。在分类任务中我们常常需要计算不同类别的概率值,所以交叉熵可以用来衡量两个概率分布之间的差异,交叉熵值越低说明两个概率分布越接近。

那么为什么交叉熵值越低, 两个概率分布越接近呢? 这需要从它与信息熵和相对熵之间的关系说起:

交叉熵 = 信息熵 + 相对熵 交叉熵 = 信息熵 + 相对熵 交叉熵=信息熵+相对熵

我们先来看最基本的 熵 (Entropy) 的概念:樀准确来说应该叫做 信息熵 (Information Entropy), 它 是由信息论之父香农从热力学中借鉴过来的一个概念, 用于描述某个事件的不确定性:某个事件不确 定性越高, 它的熵就越大。例如: “明天下雨” 这一事件要比 “明天太阳会升起" 这一事件的樀大得多, 因为前者的不确定性较高。这里我们需要引入 自信息 的概念。

  • 自信息 (Self-information): 用于衡量单个事件的不确定性。
    I ( X ) = − log ⁡ [ P ( X ) ] I(X)=-\log [P(X)] I(X)=log[P(X)]
    其中, P ( X ) P(X) P(X) 为事件 X X X 的概率。
  • 熵 (Entropy): 自信息的期望, 用于描述整个概率分布的不确定性。事件的不确定性越高, 它 的熵就越大。
    H ( P ) = E X ∼ P [ I ( X ) ] = ∑ i = 1 n P ( x i ) log ⁡ P ( x i ) H(P)=\mathrm{E}_{X \sim P}[I(X)]=\sum_{i=1}^n P\left(x_i\right) \log P\left(x_i\right) H(P)=EXP[I(X)]=i=1nP(xi)logP(xi)

为了更好地理解熵与事件不确定性的关系,我们来看一个示意图:

ch04-损失优化_第4张图片

上面是伯努利分布 (两点分布) 的信息熵,可以看到,当事件概率为 0.5 时,它的信息熵最大,大约在 0.69 附近,即此时该事件的不确定性是最大的。注意,这里的 0.69 是在二分类模型训练过程中经常会碰到的一个 Loss 值:有时在模型训练出问题时,无论我们如何进行迭代,模型的 Loss 值始终恒定在 0.69 ;或者在模型刚初始化完成第一次迭代后,其 Loss 值也很可能是 0.69 ,这表明我们的模型当前是不具备任何判别能力的,因为其对于两个类别中的任何一个都认为概率是 0.5。

下面我们来看一下相对熵的概念:

  • 相对熵 (Relative Entropy):又称 KL 散度 (Kullback-Leibler Divergence, KLD), 用于衡量 两个概率分布之间的差异 (或者说距离)。注意,虽然 KL 散度可以衡量两个分布之间的距离, 但它本身并不是一个距离函数, 因为距离函数具有对称性, 即 P P P Q Q Q 的距离必须等于 Q Q Q P P P 的距离,而相对熵不具备这种对称性。
    D K L ( P , Q ) = E X ∼ P [ log ⁡ P ( X ) Q ( X ) ] D_{\mathrm{KL}}(P, Q)=\mathrm{E}_{X \sim P}\left[\log \frac{P(X)}{Q(X)}\right] DKL(P,Q)=EXP[logQ(X)P(X)]
    其中, P P P 是数据的真实分布, Q Q Q 是模型拟合的分布, 二者定义在相同的概率空间上。我们需要 用拟合分布 Q Q Q 去逼近真实分布 P P P, 所以相对熵不具备对称性。
    下面我们再来看一下交叉熵的公式:
  • 交叉熵 (Cross Entropy):用于衡量两个分布之间的相似度。
    H ( P , Q ) = − ∑ i = 1 n P ( x i ) log ⁡ Q ( x i ) H(P, Q)=-\sum_{i=1}^n P\left(x_i\right) \log Q\left(x_i\right) H(P,Q)=i=1nP(xi)logQ(xi)
    下面我们对相对熵的公式进行展开推导变换,来观察一下相对樀与信息樀和交叉樀之间的关系:
    D K L ( P , Q ) = E X ∼ P [ log ⁡ P ( X ) Q ( X ) ] = E X ∼ P [ log ⁡ P ( X ) − log ⁡ Q ( X ) ] = ∑ i = 1 n P ( x i ) [ log ⁡ P ( x i ) − log ⁡ Q ( x i ) ] = ∑ i = 1 n P ( x i ) log ⁡ P ( x i ) − ∑ i = 1 n P ( x i ) log ⁡ Q ( x i ) = H ( P , Q ) − H ( P ) \begin{aligned} D_{\mathrm{KL}}(P, Q) & =\mathrm{E}_{X \sim P}\left[\log \frac{P(X)}{Q(X)}\right] \\ & =\mathrm{E}_{X \sim P}[\log P(X)-\log Q(X)] \\ & =\sum_{i=1}^n P\left(x_i\right)\left[\log P\left(x_i\right)-\log Q\left(x_i\right)\right] \\ & =\sum_{i=1}^n P\left(x_i\right) \log P\left(x_i\right)-\sum_{i=1}^n P\left(x_i\right) \log Q\left(x_i\right) \\ & =H(P, Q)-H(P) \end{aligned} DKL(P,Q)=EXP[logQ(X)P(X)]=EXP[logP(X)logQ(X)]=i=1nP(xi)[logP(xi)logQ(xi)]=i=1nP(xi)logP(xi)i=1nP(xi)logQ(xi)=H(P,Q)H(P)
    所以,交叉樀等于信息樀加上相对樀:
    H ( P , Q ) = H ( P ) + D K L ( P , Q ) H(P, Q)=H(P)+D_{\mathrm{KL}}(P, Q) H(P,Q)=H(P)+DKL(P,Q)
    这里, P P P 为训练集中的样本分布, Q Q Q 为模型给出的分布。所以在机器学习中, 我们最小化交叉樀实 际上等价于最小化相对熵, 因为训练集是固定的, 所以 H ( P ) H(P) H(P) 在这里是一个常数。

(1)nn.CrossEntropyLoss

  • 功能:nn.LogSoftmax() 与 nn.NLLLoss() 结合,进行交叉熵计算。
nn.CrossEntropyLoss(
    weight=None,
    size_average=None,
    ignore_index=-100,
    reduce=None,
    reduction='mean'
)

主要参数:

  • weight:各类别的 loss 设置权值。
  • ignore_index:忽略某个类别,不计算其 loss。
  • reduction:计算模式,可为 none/sum/mean。
  • none:逐个元素计算。
  • sum:所有元素求和,返回标量。
  • mean:加权平均,返回标量。

PyTorch 中 nn.CrossEntropyLoss 的交叉熵计算公式:

  • 没有针对各类别 loss 设置权值的情况:
    loss ⁡ ( x ,  class  ) = − log ⁡ ( exp ⁡ ( x [  class  ] ) ∑ j exp ⁡ ( x [ j ] ) ) = − x [  class  ] + log ⁡ ( ∑ j exp ⁡ ( x [ j ] ) ) \operatorname{loss}(x, \text { class })=-\log \left(\frac{\exp (x[\text { class }])}{\sum_j \exp (x[j])}\right)=-x[\text { class }]+\log \left(\sum_j \exp (x[j])\right) loss(x, class )=log(jexp(x[j])exp(x[ class ]))=x[ class ]+log(jexp(x[j]))
  • 对各类别 loss 设置权值的情况:
    loss ⁡ ( x ,  class  ) =  weight  [  class  ] ( − x [  class  ] + log ⁡ ( ∑ j exp ⁡ ( x [ j ] ) ) ) \operatorname{loss}(x, \text { class })=\text { weight }[\text { class }]\left(-x[\text { class }]+\log \left(\sum_j \exp (x[j])\right)\right) loss(x, class )= weight [ class ](x[ class ]+log(jexp(x[j])))
    注意, 这里的计算过程和交叉熵公式存在一些差异:
    H ( P , Q ) = − ∑ i = 1 n P ( x i ) log ⁡ Q ( x i ) H(P, Q)=-\sum_{i=1}^n P\left(x_i\right) \log Q\left(x_i\right) H(P,Q)=i=1nP(xi)logQ(xi)
    因为这里我们已经将一个具体数据点取出, 所以这里 Σ \Sigma Σ 求和式不再需要, 并且 P ( x i ) = 1 P\left(x_i\right)=1 P(xi)=1, 因此公 式变为:
    H ( P , Q ) = − log ⁡ Q ( x i ) H(P, Q)=-\log Q\left(x_i\right) H(P,Q)=logQ(xi)
    然后, 为了使输出概率在 [ 0 , 1 ] [0,1] [0,1] 之间, PyTorch 在这里使用了一个 Softmax 函数对数据进行了归一化 处理, 使其落在一个正常的概率值范围内。

代码示例:

import torch
import torch.nn as nn
import numpy as np

# fake data
inputs = torch.tensor([[1, 2], [1, 3], [1, 3]], dtype=torch.float)
target = torch.tensor([0, 1, 1], dtype=torch.long)  # 注意 label 在这里必须设置为长整型

# ------------------------ CrossEntropy loss: reduction ----------------------
# def loss function
loss_f_none = nn.CrossEntropyLoss(weight=None, reduction='none')
loss_f_sum = nn.CrossEntropyLoss(weight=None, reduction='sum')
loss_f_mean = nn.CrossEntropyLoss(weight=None, reduction='mean')

# forward
loss_none = loss_f_none(inputs, target)
loss_sum = loss_f_sum(inputs, target)
loss_mean = loss_f_mean(inputs, target)

# view
print("Cross Entropy Loss:\n ", loss_none, loss_sum, loss_mean)

输出结果:

Cross Entropy Loss:
  tensor([1.3133, 0.1269, 0.1269]) tensor(1.5671) tensor(0.5224)

可以看到, reduction 参数项

  • 在 none 模式下, 计算出的 3 个样本的 loss 值分别为 1.3133 、 0.1269 和 0.1269 ;
  • 在 sum 模式下, 计算出 3 个样本的 loss 之和为 1.5671 ;
  • 在 mean 模式下, 计算出 3 个样本的 loss 平均为 0.5224 。

下面我们以第一个样本的 loss 值为例, 通过手动计算来验证一下我们前面推导出的公式的正确性:
loss ⁡ ( x , c l a s s ) = − x [ c l a s s ] + log ⁡ ( ∑ j exp ⁡ ( x [ j ] ) ) \operatorname{loss}(x, c l a s s)=-x[c l a s s]+\log \left(\sum_j \exp (x[j])\right) loss(x,class)=x[class]+log(jexp(x[j]))

idx = 0

input_1 = inputs.detach().numpy()[idx]      # [1, 2]
target_1 = target.numpy()[idx]              # [0]

# 第一项
x_class = input_1[target_1]

# 第二项
sigma_exp_x = np.sum(list(map(np.exp, input_1)))
log_sigma_exp_x = np.log(sigma_exp_x)

# 输出loss
loss_1 = -x_class + log_sigma_exp_x

print("第一个样本的 loss 为: ", loss_1)

输出结果:

第一个样本的 loss 为:  1.3132617

下面我们来看一下针对各类别 loss 设置权重的情况:

# def loss function
# 向量长度应该与类别数量一致,如果 reduction 参数为 'mean',那么我们不需要关注
# weight 的尺度,只需要关注各类别的 weight 比例即可。
weights = torch.tensor([1, 2], dtype=torch.float)
# weights = torch.tensor([0.7, 0.3], dtype=torch.float)

loss_f_none_w = nn.CrossEntropyLoss(weight=weights, reduction='none')
loss_f_sum = nn.CrossEntropyLoss(weight=weights, reduction='sum')
loss_f_mean = nn.CrossEntropyLoss(weight=weights, reduction='mean')

# forward
loss_none_w = loss_f_none_w(inputs, target)
loss_sum = loss_f_sum(inputs, target)
loss_mean = loss_f_mean(inputs, target)

# view
print("\nweights: ", weights)
print(loss_none_w, loss_sum, loss_mean)

输出结果:

weights:  tensor([1., 2.])
tensor([1.3133, 0.2539, 0.2539]) tensor(1.8210) tensor(0.3642)

对比之前没有设置权值的结果,我们发现,

  • 在 none 模式下,由于第一个样本类别为 0,而其权值为 1 ,所以结果和之前一样,都是 1.3133 。而第二个和第三个样本类别为 1 ,权值为 2 ,所以这里的 loss 是之前的 2 倍,即 0.2539 。
  • 对于 sum 模式,其结果为三个样本的 loss 之和,即 1.8210 。
  • 而对于 mean 模式,现在不再是简单地将三个 loss 相加求平均,而是采用了加权平均的计算方式:因为第一个样本权值为 1 ,第二个和第三个样本权值都是 2 ,所以一共有 1 + 2 + 2 = 5 份,loss 的加权均值为 1.8210/5 = 0.3642。

下面我们通过手动计算来验证在设置权值的情况下,mean 模式下的 loss 计算方式是否正确:

weights = torch.tensor([1, 2], dtype=torch.float)
weights_all = np.sum(list(map(lambda x: weights.numpy()[x], target.numpy())))

mean = 0
loss_sep = loss_none.detach().numpy()

for i in range(target.shape[0]):
    x_class = target.numpy()[i]
    tmp = loss_sep[i] * (weights.numpy()[x_class] / weights_all)
    mean += tmp

print(mean)

输出结果:

0.3641947731375694

可以看到,手动计算的结果和 PyTorch 中自动求取的结果一致,所以对于设置权值的情况,mean 模式下的 loss 不是简单的求和之后除以样本个数,而是除以权值的份数,即实际计算的是加权均值。

2.3. NLL/BCE/BCEWithLogits Loss

(2)nn.NLLLoss

  • 功能:实现负对数似然函数中的 负号功能。
nn.NLLLoss(
    weight=None,
    size_average=None,
    ignore_index=-100,
    reduce=None,
    reduction='mean'
)

主要参数:

  • weight:各类别的 loss 设置权值。
  • ignore_index:忽略某个类别。
  • reduction:计算模式,可为 none/sum/mean。
  • none:逐个元素计算。
  • sum:所有元素求和,返回标量。
  • mean:加权平均,返回标量。

计算公式:

ℓ ( x , y ) = L = { l 1 , … , l N } T , l n = − w y n x n , y n \ell(x, y)=L=\left\{l_1, \ldots, l_N\right\}^{\mathrm{T}}, \quad l_n=-w_{y_n} x_{n, y_n} (x,y)=L={l1,,lN}T,ln=wynxn,yn

代码示例:

# fake data, 这里我们使用的还是之前的数据,注意 label 在这里必须设置为 long
inputs = torch.tensor([[1, 2], [1, 3], [1, 3]], dtype=torch.float)
target = torch.tensor([0, 1, 1], dtype=torch.long)

# weights
weights = torch.tensor([1, 1], dtype=torch.float)

# NLL loss
loss_f_none_w = nn.NLLLoss(weight=weights, reduction='none')
loss_f_sum = nn.NLLLoss(weight=weights, reduction='sum')
loss_f_mean = nn.NLLLoss(weight=weights, reduction='mean')

# forward
loss_none_w = loss_f_none_w(inputs, target)
loss_sum = loss_f_sum(inputs, target)
loss_mean = loss_f_mean(inputs, target)

# view
print("\nweights: ", weights)
print("NLL Loss", loss_none_w, loss_sum, loss_mean)

输出结果:

weights:  tensor([1., 1.])
NLL Loss tensor([-1., -3., -3.]) tensor(-7.) tensor(-2.3333)

注意, 这里 nn.NLLLoss 实际上只是实现了一个负号的功能

  • 对于 none 模式:

    • 这里第一个样本是 第 0 类, 所以我们这里只对第一个神经元进行计算, 取负号得到 NLL Loss 为 -1 ;
    • 第二个样本是第 1 类, 我们对第二个神经元进行计算, 取负号得到 NLL Loss 为 -3 ;
    • 第三个样本也是第 1 类, 我们 对第二个神经元进行计算, 取负号得到 NLL Loss 为 -3 。
  • 对于 sum 模式, 将三个样本的 NLL Loss 求和, 得到 -7 。

  • 对于 mean 模式, 将三个样本的 NLL Loss 加权平均, 得到 -2.3333 。

(3)nn.BCELoss

  • 功能:二分类交叉熵
nn.BCELoss(
    weight=None,
    size_average=None,
    reduce=None,
    reduction='mean'
)

主要参数:

  • weight:各类别的 loss 设置权值。
  • ignore_index:忽略某个类别。
  • reduction:计算模式,可为 none/sum/mean。
  • none:逐个元素计算。
  • sum:所有元素求和,返回标量。
  • mean:加权平均,返回标量。

计算公式:
l n = − w n [ y n ⋅ log ⁡ x n + ( 1 − y n ) ⋅ log ⁡ ( 1 − x n ) ] l_n=-w_n\left[y_n \cdot \log x_n+\left(1-y_n\right) \cdot \log \left(1-x_n\right)\right] ln=wn[ynlogxn+(1yn)log(1xn)]
注意事项:由于交叉熵是衡量两个概率分布之间的差异, 因此输入值取值必须在 [ 0 , 1 ] [0,1] [0,1]

代码示例:

# fake data, 这里我们设置 4 个样本,注意 label 在这里必须设置为 float
inputs = torch.tensor([[1, 2], [2, 2], [3, 4], [4, 5]], dtype=torch.float)
target = torch.tensor([[1, 0], [1, 0], [0, 1], [0, 1]], dtype=torch.float)

target_bce = target

# itarget
inputs = torch.sigmoid(inputs)  # 利用 Sigmoid 函数将输入值压缩到 [0,1]

# weights
weights = torch.tensor([1, 1], dtype=torch.float)

# BCE loss
loss_f_none_w = nn.BCELoss(weight=weights, reduction='none')
loss_f_sum = nn.BCELoss(weight=weights, reduction='sum')
loss_f_mean = nn.BCELoss(weight=weights, reduction='mean')

# forward
loss_none_w = loss_f_none_w(inputs, target_bce)
loss_sum = loss_f_sum(inputs, target_bce)
loss_mean = loss_f_mean(inputs, target_bce)

# view
print("\nweights: ", weights)
print("BCE Loss", loss_none_w, loss_sum, loss_mean)

输出结果:

weights:  tensor([1., 1.])
BCE Loss tensor([[0.3133, 2.1269],
        [0.1269, 2.1269],
        [3.0486, 0.0181],
        [4.0181, 0.0067]]) tensor(11.7856) tensor(1.4732)

由于这里我们有 4 个样本,每个样本有 2 个神经元,因此

  • 在 none 模式下我们这里得到 8 个 loss,即每一个神经元会一一对应地计算 loss。而
  • sum 模式就是简单地将这 8 个 loss 进行相加,
  • mean 模式就是对这 8 个 loss 求加权均值。

下面我们通过手动计算来验证第一个神经元的 BCE loss 值是否等于 0.3133:

idx = 0

x_i = inputs.detach().numpy()[idx, idx]  # 获取第一个神经元的输出值
y_i = target.numpy()[idx, idx]  # 获取第一个神经元的标签

# loss
# l_i = -[ y_i * np.log(x_i) + (1-y_i) * np.log(1-y_i) ]      # np.log(0) = nan
l_i = -y_i * np.log(x_i) if y_i else -(1-y_i) * np.log(1-x_i)

# 输出loss
print("BCE inputs: ", inputs)
print("第一个 loss 为: ", l_i)

输出结果:

BCE inputs:  tensor([[0.7311, 0.8808],
        [0.8808, 0.8808],
        [0.9526, 0.9820],
        [0.9820, 0.9933]])
第一个 loss 为:  0.31326166

可以看到,手动计算的结果与 PyTorch 中 nn.BCELoss 的计算结果一致。

(4)nn.BCEWithLogitsLoss

  • 功能:结合 Sigmoid 与 二分类交叉熵。
nn.BCEWithLogitsLoss(
    weight=None,
    size_average=None,
    reduce=None,
    reduction='mean',
    pos_weight=None
)

主要参数:

  • pos_weight:正样本的权值,用于平衡正负样本。

    • 例如:正样本有 100 个,负样本有 300 个,正负样本比例为 1:3 。因此我们可以将该项设为 3,这样即等价于正负样本各 300 个。
  • weight:各类别的 loss 设置权值。

  • ignore_index:忽略某个类别。

  • reduction:计算模式,可为 none/sum/mean。

  • none:逐个元素计算。

  • sum:所有元素求和,返回标量。

  • mean:加权平均,返回标量。

计算公式:
l n = − w n [ y n ⋅ log ⁡ σ ( x n ) + ( 1 − y n ) ⋅ log ⁡ ( 1 − σ ( x n ) ) ] l_n=-w_n\left[y_n \cdot \log \sigma\left(x_n\right)+\left(1-y_n\right) \cdot \log \left(1-\sigma\left(x_n\right)\right)\right] ln=wn[ynlogσ(xn)+(1yn)log(1σ(xn))]
注意事项:网络最后不加 Sigmoid 函数。
代码示例:

inputs = torch.tensor([[1, 2], [2, 2], [3, 4], [4, 5]], dtype=torch.float)
target = torch.tensor([[1, 0], [1, 0], [0, 1], [0, 1]], dtype=torch.float)

target_bce = target

# inputs = torch.sigmoid(inputs)  # 这里增加 sigmoid 会使得计算不准确,因为相当于加了两层 sigmoid

weights = torch.tensor([1, 1], dtype=torch.float)

loss_f_none_w = nn.BCEWithLogitsLoss(weight=weights, reduction='none')
loss_f_sum = nn.BCEWithLogitsLoss(weight=weights, reduction='sum')
loss_f_mean = nn.BCEWithLogitsLoss(weight=weights, reduction='mean')

# forward
loss_none_w = loss_f_none_w(inputs, target_bce)
loss_sum = loss_f_sum(inputs, target_bce)
loss_mean = loss_f_mean(inputs, target_bce)

# view
print("\nweights: ", weights)
print(loss_none_w, loss_sum, loss_mean)

输出结果:

weights:  tensor([1., 1.])
tensor([[0.3133, 2.1269],
        [0.1269, 2.1269],
        [3.0486, 0.0181],
        [4.0181, 0.0067]]) tensor(11.7856) tensor(1.4732)

我们来看一下 pos_weight 的设置:

inputs = torch.tensor([[1, 2], [2, 2], [3, 4], [4, 5]], dtype=torch.float)
target = torch.tensor([[1, 0], [1, 0], [0, 1], [0, 1]], dtype=torch.float)

target_bce = target

weights = torch.tensor([1], dtype=torch.float)
pos_w = torch.tensor([1], dtype=torch.float)  # 将 pos_weight 设为 1 

loss_f_none_w = nn.BCEWithLogitsLoss(weight=weights, reduction='none', pos_weight=pos_w)
loss_f_sum = nn.BCEWithLogitsLoss(weight=weights, reduction='sum', pos_weight=pos_w)
loss_f_mean = nn.BCEWithLogitsLoss(weight=weights, reduction='mean', pos_weight=pos_w)

# forward
loss_none_w = loss_f_none_w(inputs, target_bce)
loss_sum = loss_f_sum(inputs, target_bce)
loss_mean = loss_f_mean(inputs, target_bce)

# view
print("\npos_weights: ", pos_w)
print(loss_none_w, loss_sum, loss_mean)

输出结果:

pos_weights:  tensor([1.])
tensor([[0.3133, 2.1269],
        [0.1269, 2.1269],
        [3.0486, 0.0181],
        [4.0181, 0.0067]]) tensor(11.7856) tensor(1.4732)

可以看到,当 pos_weight 设为
时,计算的 loss 结果与之前一样。接下来我们将 pos_weight 改为 3 来看一下结果会如何变化:

pos_w = torch.tensor([3], dtype=torch.float)  # 将 pos_weight 设为 3

输出结果:

pos_weights:  tensor([3.])
tensor([[0.9398, 2.1269],
        [0.3808, 2.1269],
        [3.0486, 0.0544],
        [4.0181, 0.0201]]) tensor(12.7158) tensor(1.5895)

可以看到, 当 pos_weight 设为 3 时, 第一个样本 [ 1 , 2 ] [1,2] [1,2] 的标签为 [ 1 , 0 ] [1,0] [1,0], 它的第一个神经元标签 1 对应的 loss 变为了之前的 3 倍, 即 0.3133 × 3 = 0.9398 0.3133 \times 3=0.9398 0.3133×3=0.9398; 第二个神经元标签 0 对应的 loss 和 之前一样, 为 2.1269 。其余三个样本的 loss 变化同理。

2.4. 总结

本节中,我们学习了损失函数的概念,以及 4 种不同的损失函数。下节课中,我们将继续学习 PyTorch 中其余 14 种损失函数。

3.损失函数 (二)

上节中,我们学习了损失函数的概念以及四种不同的损失函数。这节我们继续学习 PyTorch 中提供的另外十四种损失函数。

3.1. PyTorch 中的损失函数

首先我们来看在回归任务中常用的两个损失函数 nn.L1Lossnn.MSELoss

(5)nn.L1Loss

功能:计算 inputs 与 target 之差的绝对值。

nn.L1Loss(
    size_average=None,
    reduce=None,
    reduction='mean'
)

主要参数:

  • reduction:计算模式,可为 none/sum/mean。
  • none:逐个元素计算。
  • sum:所有元素求和,返回标量。
  • mean:加权平均,返回标量。

计算公式:

l n = ∣ x n − y n ∣ l_n = |x_n - y_n| ln=xnyn

(6)nn.MSELoss
功能:计算 inputs 与 target 之差的平方。

nn.MSELoss(
    size_average=None,
    reduce=None,
    reduction='mean'
)

主要参数:

  • reduction:计算模式,可为 none/sum/mean。
  • none:逐个元素计算。
  • sum:所有元素求和,返回标量。
  • mean:加权平均,返回标量。

计算公式:

l n = ( x n − y n ) 2 l_n = (x_n - y_n)^2 ln=(xnyn)2

代码示例:

import torch
import torch.nn as nn
import matplotlib.pyplot as plt
import numpy as np
from tools.common_tools import set_seed

set_seed(1)  # 设置随机种子

inputs = torch.ones((2, 2))
target = torch.ones((2, 2)) * 3

# ------------------------------------ L1 loss ----------------------------------
loss_f = nn.L1Loss(reduction='none')
loss = loss_f(inputs, target)

print("input:{}\ntarget:{}\nL1 loss:{}".format(inputs, target, loss))

# ------------------------------------ MSE loss ---------------------------------
loss_f_mse = nn.MSELoss(reduction='none')
loss_mse = loss_f_mse(inputs, target)

print("MSE loss:{}".format(loss_mse))

输出结果:

input:tensor([[1., 1.],
        [1., 1.]])
target:tensor([[3., 3.],
        [3., 3.]])
L1 loss:tensor([[2., 2.],
        [2., 2.]])
MSE loss:tensor([[4., 4.],
        [4., 4.]])

可以看到, 这里我们的每个神经元的输入为 x i = 1 x_i=1 xi=1, 输出为 y i = 3 y_i=3 yi=3 。所以, 每个神经元的 L1 loss 为 ∣ x i − y i ∣ = ∣ 1 − 3 ∣ = 2 \left|x_i-y_i\right|=|1-3|=2 xiyi=∣13∣=2, MSE loss 为 ( x i − y i ) 2 = ( 1 − 3 ) 2 = 4 \left(x_i-y_i\right)^2=(1-3)^2=4 (xiyi)2=(13)2=4

(7)nn.SmoothL1Loss
功能:平滑的 L1 Loss,可以减轻离群点带来的影响。

nn.SmoothL1Loss(
    size_average=None,
    reduce=None,
    reduction='mean'
)

主要参数:

  • reduction:计算模式,可为 none/sum/mean。
  • none:逐个元素计算。
  • sum:所有元素求和,返回标量。
  • mean:加权平均,返回标量。

计算公式:
loss ⁡ ( x , y ) = 1 n ∑ i = 1 n z i \operatorname{loss}(x, y)=\frac{1}{n} \sum_{i=1}^n z_i loss(x,y)=n1i=1nzi
其中,
z i = { 0.5 ( x i − y i ) 2 ,  if  ∣ x i − y i ∣ < 1 ∣ x i − y i ∣ − 0.5 ,  otherwise  z_i= \begin{cases}0.5\left(x_i-y_i\right)^2, & \text { if }\left|x_i-y_i\right|<1 \\ \left|x_i-y_i\right|-0.5, & \text { otherwise }\end{cases} zi={0.5(xiyi)2,xiyi0.5, if xiyi<1 otherwise 

代码示例:

inputs = torch.linspace(-3, 3, steps=500)
target = torch.zeros_like(inputs)

loss_f = nn.SmoothL1Loss(reduction='none')
loss_smooth = loss_f(inputs, target)

loss_l1 = np.abs(inputs.numpy())

plt.plot(inputs.numpy(), loss_smooth.numpy(), label='Smooth L1 Loss')
plt.plot(inputs.numpy(), loss_l1, label='L1 loss')
plt.xlabel('x_i - y_i')
plt.ylabel('loss value')
plt.legend()
plt.grid()
plt.show()

输出结果:
ch04-损失优化_第5张图片

(8)PoissonNLLLoss
功能:泊松分布的负对数似然损失函数。

nn.PoissonNLLLoss(
    log_input=True,
    full=False,
    size_average=None,
    eps=1e-08,
    reduce=None,
    reduction='mean'
)

主要参数:

  • log_input:输入是否为对数形式,决定计算公式。
  • full:计算所有 loss,默认为 False。
  • eps:修正项,避免 input 为 0 时,log(input) 为 nan 的情况。

计算公式:

  • 当 log_input=True 时: l o s s ( x n , y n ) = e x n − x n ⋅ y n loss(x_n, y_n) = e^{x_n} - x_n·y_n loss(xn,yn)=exnxnyn

  • 当 log_input=False 时: l o s s ( x n , y n ) = x n − y n ⋅ l o g ( x n + e p s ) loss(x_n, y_n) = x_n - y_n·log(x_n + eps) loss(xn,yn)=xnynlog(xn+eps)

代码示例:

inputs = torch.randn((2, 2))
target = torch.randn((2, 2))

loss_f = nn.PoissonNLLLoss(log_input=True, full=False, reduction='none')
loss = loss_f(inputs, target)

print("input:{}\ntarget:{}\nPoisson NLL loss:{}".format(inputs, target, loss))

输出结果:

input:tensor([[0.6614, 0.2669],
        [0.0617, 0.6213]])
target:tensor([[-0.4519, -0.1661],
        [-1.5228,  0.3817]])
Poisson NLL loss:tensor([[2.2363, 1.3503],
        [1.1575, 1.6242]])

下面我们以第一个神经元的 loss 为例,通过手动计算来验证我们前面的公式是否正确:

idx = 0
loss_1 = torch.exp(inputs[idx, idx]) - target[idx, idx]*inputs[idx, idx]

print("第一个元素的 loss 为:", loss_1)

输出结果:

第一个元素的 loss 为: tensor(2.2363)

可以看到,由于这里我们的 log_input=True,默认输入为对数形式,计算出的第一个神经元的 loss 为 2.2363,与前面 PyTorch 中 nn.PoissonNLLLoss 的计算结果一致。

(9)nn.KLDivLoss

功能:计算 KL 散度 (KL divergence, KLD),即相对熵。

nn.KLDivLoss(
    size_average=None,
    reduce=None,
    reduction='mean'
)

主要参数:

  • reduction:计算模式,可为 none/sum/mean/batchmean。
  • none:逐个元素计算。
  • sum:所有元素求和,返回标量。
  • mean:加权平均,返回标量。
  • batchmean:batchsize 维度求平均值。

计算公式:
D K L ( P , Q ) = E X ∼ P [ log ⁡ P ( X ) Q ( X ) ] = E X ∼ P [ log ⁡ P ( X ) − log ⁡ Q ( X ) ] = ∑ i = 1 n P ( x i ) ( log ⁡ P ( x i ) − log ⁡ Q ( x i ) ) \begin{aligned} D_{\mathrm{KL}}(P, Q)=\mathrm{E}_{X \sim P}\left[\log \frac{P(X)}{Q(X)}\right] & =\mathrm{E}_{X \sim P}[\log P(X)-\log Q(X)] \\ & =\sum_{i=1}^n P\left(x_i\right)\left(\log P\left(x_i\right)-\log Q\left(x_i\right)\right) \end{aligned} DKL(P,Q)=EXP[logQ(X)P(X)]=EXP[logP(X)logQ(X)]=i=1nP(xi)(logP(xi)logQ(xi))
其中, P P P 为数据的真实分布, Q Q Q 为模型拟合的分布。
PyTorch 中的计算公式:
l n = y n ⋅ ( log ⁡ y n − x n ) l_n=y_n \cdot\left(\log y_n-x_n\right) ln=yn(logynxn)
由于 PyTorch 是逐个元素计算的, 因此可以移除 Σ \Sigma Σ 求和项。而括号中第二项这里是 x n x_n xn, 而不是 log ⁡ Q ( x n ) \log Q\left(x_n\right) logQ(xn), 因此我们需要提前计算输入的对数概率。
注意事项:需提前将输入计算 log-probabilities,例如通过 nn.logsoftmax() 计算。

代码示例:

inputs = torch.tensor([[0.5, 0.3, 0.2], [0.2, 0.3, 0.5]])
inputs_log = torch.log(inputs)
target = torch.tensor([[0.9, 0.05, 0.05], [0.1, 0.7, 0.2]], dtype=torch.float)

loss_f_none = nn.KLDivLoss(reduction='none')
loss_f_mean = nn.KLDivLoss(reduction='mean')
loss_f_bs_mean = nn.KLDivLoss(reduction='batchmean')

loss_none = loss_f_none(inputs, target)
loss_mean = loss_f_mean(inputs, target)
loss_bs_mean = loss_f_bs_mean(inputs, target)

print("loss_none:\n{}\nloss_mean:\n{}\nloss_bs_mean:\n{}".format(loss_none, loss_mean, loss_bs_mean))

输出结果:

loss_none:
tensor([[-0.5448, -0.1648, -0.1598],
        [-0.2503, -0.4597, -0.4219]])
loss_mean:
-0.3335360586643219
loss_bs_mean:
-1.000608205795288

由于我们的输入是一个 23 的 Tensor,所以我们的 loss 也是一个 23 的 Tensor。在 mean 模式下,我们得到 6 个 loss 的均值为 -0.3335 ;而 batchmean 模式下是 6 个 loss 相加再除以 2,所以得到 1.0006。

下面我们以第一个神经元的 loss 为例,通过手动计算来验证 PyTorch 中的公式是否和我们之前提到的一致:

idx = 0
loss_1 = target[idx, idx] * (torch.log(target[idx, idx]) - inputs[idx, idx])  #  注意,这里括号中第二项没有取 log

print("第一个元素的 loss 为:", loss_1)

输出结果:

第一个元素的 loss 为: tensor(-0.5448)

可以看到,手动计算的结果与前面 PyTorch 中的 nn.KLDivLoss 的结果一致。

(10)nn.MarginRankingLoss

功能:计算两个向量之间的相似度,用于 排序任务。该方法计算两组数据之间的差异,返回一个
的 loss 矩阵。

nn.MarginRankingLoss(
    margin=0.0,
    size_average=None,
    reduce=None,
    reduction='mean'
)

主要参数:

  • margin:边界值, x 1 x_1 x1 x 2 x_2 x2 之间的差异值。
  • reduction:计算模式,可为 none/sum/mean。

计算公式:

l o s s ( x , y ) = m a x ( 0 , − y ⋅ ( x 1 − x 2 ) + m a r g i n ) loss(x, y) = max(0, -y·(x_1 - x_2)+ margin) loss(x,y)=max(0,yx1x2+margin)

  • y = 1 y = 1 y=1 时,我们希望 x 1 x_1 x1 x 2 x_2 x2 大,当 x 1 > x 2 x_1 > x_2 x1>x2 时, 不产生 loss。
  • y = − 1 y = -1 y=1 时,我们希望 x 2 x_2 x2 x 1 x_1 x1 大,当 x 2 > x 1 x_2 > x_1 x2>x1 时, 不产生 loss。

代码示例:

x1 = torch.tensor([[1], [2], [3]], dtype=torch.float)
x2 = torch.tensor([[2], [2], [2]], dtype=torch.float)

target = torch.tensor([1, 1, -1], dtype=torch.float)

loss_f_none = nn.MarginRankingLoss(margin=0, reduction='none')
loss = loss_f_none(x1, x2, target)

print(loss)

输出结果:

tensor([[1., 1., 0.],
        [0., 0., 0.],
        [0., 0., 1.]])

由于这里我们的输入是两个长度为 3 的向量, 所以输出的是一个 3 × 3 3 \times 3 3×3 的 loss 矩阵。该矩阵中的第 一行是由 x 1 \mathrm{x} 1 x1 中的第一个元素与 x 2 \mathrm{x} 2 x2 中的三个元素计算得到的 loss。当 y = 1 y=1 y=1 时, x 1 = 1 x_1=1 x1=1, x 2 = 2 , x 1 x_2=2, x_1 x2=2,x1 并没有大于 x 2 x_2 x2, 因此会产生 loss 为 max ⁡ ( 0 , − 1 × ( 1 − 2 ) ) = 1 \max (0,-1 \times(1-2))=1 max(0,1×(12))=1, 所以输出矩阵中 第一行的第一个元素为 1 ; 第一行中的第二个元素同理。对于第一行中的第三个元素, y = − 1 y=-1 y=1, x 1 = 1 , x 2 = 2 x_1=1, x_2=2 x1=1,x2=2, 满足 x 2 > x 1 x_2>x_1 x2>x1, 因此不会产生 loss, 即 loss 为 max ⁡ ( 0 , 1 × ( 1 − 2 ) ) = 0 \max (0,1 \times(1-2))=0 max(0,1×(12))=0, 所以输出矩阵中第一行的第三个元素为 0 。

(11)nn. MultiLabelMarginLoss

功能:多标签边界损失函数。例如四分类任务, 样本 x x x 属于第 0 类和第 3 类, 注意这里标签为 [ 0 , 3 , − 1 , − 1 ] [0,3,-1,-1] [0,3,1,1], 而不是 [ 1 , 0 , 0 , 1 ] [1,0,0,1] [1,0,0,1]

nn.MultiLabelMarginLoss(
    size_average=None,
    reduce=None,
    reduction='mean'
)

主要参数:

  • reduction:计算模式, 可为 none/sum/mean。
    计算公式:
    loss ⁡ ( x , y ) = ∑ i j max ⁡ ( 0 , 1 − x [ y [ j ] ] − x [ i ] ) x ⋅ size ⁡ ( 0 ) \operatorname{loss}(x, y)=\sum_{i j} \frac{\max (0,1-x[y[j]]-x[i])}{x \cdot \operatorname{size}(0)} loss(x,y)=ijxsize(0)max(0,1x[y[j]]x[i])
    其中, i = 0 , … , x . size ⁡ ( 0 ) , j = 0 , … , y i=0, \ldots, x . \operatorname{size}(0), j=0, \ldots, y i=0,,x.size(0),j=0,,y. size ⁡ ( 0 ) \operatorname{size}(0) size(0), 对于所有的 i i i j j j, 都有 y [ j ] ≥ 0 y[j] \geq 0 y[j]0 并且 i ≠ y [ j ] 0 i \neq y[j]_0 i=y[j]0

代码示例:

x = torch.tensor([[0.1, 0.2, 0.4, 0.8]])  # 一个四分类样本的输出概率
y = torch.tensor([[0, 3, -1, -1]], dtype=torch.long)  # 标签,该样本属于第 0 类和第 3 类

loss_f = nn.MultiLabelMarginLoss(reduction='none')
loss = loss_f(x, y)

print(loss)

输出结果:

tensor([0.8500])

下面我们通过手动计算来验证前面计算公式的正确性:

x = x[0]
item_1 = (1-(x[0] - x[1])) + (1 - (x[0] - x[2]))    # 第 0 类标签的 loss
item_2 = (1-(x[3] - x[1])) + (1 - (x[3] - x[2]))    # 第 3 类标签的 loss

loss_h = (item_1 + item_2) / x.shape[0]

print(loss_h)

输出结果:

tensor(0.8500)

可以看到,手动计算的结果与 PyTorch 中的 nn.MultiLabelMarginLoss 的结果一致。

(12)nn.SoftMarginLoss

功能:计算二分类的 logistic 损失。

nn.SoftMarginLoss(
    size_average=None,
    reduce=None,
    reduction='mean'
)

主要参数:

  • reduction:计算模式,可为 none/sum/mean。

计算公式:

l o s s ( x , y ) = ∑ i log ⁡ ( 1 + exp ⁡ ( − y [ i ] ⋅ x [ i ] ) ) x . n e l e m e n t ( ) \mathrm{loss}(x,y) = \sum_i \dfrac{\log(1 + \exp (-y[i] \cdot x[i]))}{x.\mathrm{nelement()}} loss(x,y)=ix.nelement()log(1+exp(y[i]x[i]))
其中, x . n e l e m e n t ( ) x.\mathrm{nelement()} x.nelement() 为输入 x 中的样本个数。注意这里 y 也有 1 和 -1 两种模式。

代码示例:

inputs = torch.tensor([[0.3, 0.7], [0.5, 0.5]])  # 两个样本,两个神经元
target = torch.tensor([[-1, 1], [1, -1]], dtype=torch.float)  # 该 loss 为逐个神经元计算,需要为每个神经元单独设置标签

loss_f = nn.SoftMarginLoss(reduction='none')
loss = loss_f(inputs, target)

print("SoftMargin: ", loss)

输出结果:

SoftMargin:  tensor([[0.8544, 0.4032],
        [0.4741, 0.9741]])

下面我们以第一个神经元的 loss 为例,采用手动计算来验证上面公式的正确性:

idx = 0
inputs_i = inputs[idx, idx]
target_i = target[idx, idx]

loss_h = np.log(1 + np.exp(-target_i * inputs_i))

print(loss_h)

输出结果:

tensor(0.8544)

可以看到,手动计算的结果与 PyTorch 中的 nn.SoftMarginLoss 的结果一致。

(13)nn.MultiLabelSoftMarginLoss

功能:SoftMarginLoss 的多标签版本。

nn.MultiLabelSoftMarginLoss(
    weight=None,
    size_average=None,
    reduce=None,
    reduction='mean')

主要参数:

  • weight:各类别的 loss 设置权值。
  • reduction:计算模式,可为 none/sum/mean。

计算公式:

l o s s ( x , y ) = − 1 C ⋅ ∑ i y [ i ] ⋅ log ⁡ ( 1 1 + exp ⁡ ( − x [ i ] ) ) ) + ( 1 − y [ i ] ) ⋅ log ⁡ ( exp ⁡ ( − x [ i ] ) 1 + exp ⁡ ( − x [ i ] ) ) ) \mathrm{loss}(x,y)= - \dfrac{1}{C} \cdot \sum_i y[i] \cdot \log\left(\dfrac{1}{1+\exp(-x[i]))} \right) + (1-y[i]) \cdot \log \left(\dfrac{\exp(-x[i])}{1+\exp(-x[i]))} \right) loss(x,y)=C1iy[i]log(1+exp(x[i]))1)+(1y[i])log(1+exp(x[i]))exp(x[i]))
其中,C 是标签类别的数量,i 表示第 i 个神经元,这里标签取值为 0 或者 1,例如在一个四分类任务中,某样本标签为第 0 类和第 3 类,那么该样本的标签向量为 [1,0,0,1]。

代码示例:

inputs = torch.tensor([[0.3, 0.7, 0.8]])
target = torch.tensor([[0, 1, 1]], dtype=torch.float)

loss_f = nn.MultiLabelSoftMarginLoss(reduction='none')
loss = loss_f(inputs, target)

print("MultiLabel SoftMargin: ", loss)

输出结果:

MultiLabel SoftMargin:  tensor([0.5429])

下面我们通过手动计算验证上面公式的正确性:

i_0 = torch.log(torch.exp(-inputs[0, 0]) / (1 + torch.exp(-inputs[0, 0])))
i_1 = torch.log(1 / (1 + torch.exp(-inputs[0, 1])))
i_2 = torch.log(1 / (1 + torch.exp(-inputs[0, 2])))

loss_h = (i_0 + i_1 + i_2) / -3

print(loss_h)

输出结果:

tensor(0.5429)

可以看到,手动计算的结果与 PyTorch 中的 nn.MultiLabelSoftMarginLoss 的结果一致。

(14)nn.MultiMarginLoss

功能:计算多分类的折页损失。

nn.MultiMarginLoss(
    p=1,
    margin=1.0,
    weight=None,
    size_average=None,
    reduce=None,
    reduction='mean'
)

主要参数:

  • p:可选 1 或 2。
  • weight:各类别的 loss 设置权值。
  • margin:边界值。
  • reduction:计算模式,可为 none/sum/mean。

计算公式:

l o s s ( x , y ) = ∑ i max ⁡ ( 0 , m a r g i n − x [ y ] + x [ i ] ) p x . s i z e ( 0 ) \mathrm{loss}(x,y) = \dfrac{\sum_i \max(0,\mathrm{margin}-x[y] + x[i])^p}{x.\mathrm{size(0)}} loss(x,y)=x.size(0)imax(0,marginx[y]+x[i])p
其中, x ∈ { 0 , … , x . s i z e ( 0 ) − 1 }    ,    y ∈ { 0 , … , y . s i z e ( 0 ) − 1 } x\in \{0,\dots,x.\mathrm{size}(0)-1\}\;,\; y\in \{0,\dots,y.\mathrm{size}(0)-1\} x{0,,x.size(0)1},y{0,,y.size(0)1},并且对于所有的 i i i j j j,都有 0 ≤ y [ j ] ≤ x . s i z e ( 0 ) − 1 0 \le y[j] \le x.\mathrm{size}(0)-1 0y[j]x.size(0)1,以及 i ≠ y [ j ] i \ne y[j] i=y[j]

代码示例:

x = torch.tensor([[0.1, 0.2, 0.7], [0.2, 0.5, 0.3]])
y = torch.tensor([1, 2], dtype=torch.long)

loss_f = nn.MultiMarginLoss(reduction='none')
loss = loss_f(x, y)

print("Multi Margin Loss: ", loss)

输出结果:

Multi Margin Loss:  tensor([0.8000, 0.7000])

下面我们以第一个样本的 loss 为例,通过手动计算验证上面公式的正确性:

x = x[0]
margin = 1

i_0 = margin - (x[1] - x[0])
i_2 = margin - (x[1] - x[2])

loss_h = (i_0 + i_2) / x.shape[0]

print(loss_h)

输出结果:

tensor(0.8000)

可以看到,手动计算的结果与 PyTorch 中的 nn.MultiMarginLoss 的结果一致。

(15)nn.TripletMarginLoss
功能:计算三元组损失,常用于人脸识别验证。

三元组损失:

ch04-损失优化_第6张图片

我们希望通过学习,使得 Anchor 与 Posttive 之间的距离小于 Anchor 与 Negative 之间的距离。

nn.TripletMarginLoss(
    margin=1.0,
    p=2.0,
    eps=1e-06,
    swap=False,
    size_average=None,
    reduce=None,
    reduction='mean'
)

主要参数:

  • p:范数的阶,默认为 2。
  • margin:边界值。
  • reduction:计算模式,可为 none/sum/mean。

计算公式:

L ( a , p , n ) = max ⁡ { d ( a i , p i ) − d ( a i , n i ) + m a r g i n , 0 } L(a,p,n) = \max \{d(a_i, p_i) - d(a_i, n_i) + \mathrm{margin}, 0\} L(a,p,n)=max{d(ai,pi)d(ai,ni)+margin,0}
其中, d ( x i , y i ) = ∥ x i − y i ∥ p d(x_i, y_i) = \|\mathbf{x}_i - \mathbf{y}_i \|_p d(xi,yi)=xiyip

代码示例:

anchor = torch.tensor([[1.]])
pos = torch.tensor([[2.]])
neg = torch.tensor([[0.5]])

loss_f = nn.TripletMarginLoss(margin=1.0, p=1)  # 范数为 1,即计算两者之差的绝对值
loss = loss_f(anchor, pos, neg)

print("Triplet Margin Loss", loss)

输出结果:

Triplet Margin Loss tensor(1.5000)

下面我们通过手动计算验证上面公式的正确性:

margin = 1
a, p, n = anchor[0], pos[0], neg[0]

d_ap = torch.abs(a-p)
d_an = torch.abs(a-n)

loss = d_ap - d_an + margin

print(loss)

输出结果:

tensor([1.5000])

可以看到,手动计算的结果与 PyTorch 中的 nn.TripletMarginLoss 的结果一致。

(16)nn.HingeEmbeddingLoss

功能:计算两个输入的相似性,常用于非线性 embedding 和半监督学习。

nn.HingeEmbeddingLoss(
    margin=1.0,
    size_average=None,
    reduce=None,
    reduction='mean'
)

主要参数:

  • margin:边界值。
  • reduction:计算模式,可为 none/sum/mean。

计算公式:

l n = { x n   , if  y n = 1 max ⁡ { 0 , Δ − x n }   , if  y n = − 1 l_n = \begin{cases}x_n\, , & \text{if } y_n = 1 \\[2ex] \max\{0,\Delta - x_n\}\, , & \text{if } y_n = -1\end{cases} ln= xn,max{0,Δxn},if yn=1if yn=1
注意事项:输入 x x x 应为两个输入之差的绝对值。

代码示例:

inputs = torch.tensor([[1., 0.8, 0.5]])
target = torch.tensor([[1, 1, -1]])

loss_f = nn.HingeEmbeddingLoss(margin=1, reduction='none')
loss = loss_f(inputs, target)

print("Hinge Embedding Loss", loss)

输出结果:

Hinge Embedding Loss tensor([[1.0000, 0.8000, 0.5000]])

下面我们通过手动计算验证上面公式的正确性:

margin = 1.
loss_0 = inputs.numpy()[0, 0]
loss_1 = inputs.numpy()[0, 1]
loss_2 = max(0, margin - inputs.numpy()[0, 2])

print(loss_0, loss_1, loss_2)

输出结果:

1.0 0.8 0.5

可以看到,手动计算的结果与 PyTorch 中的 nn.HingeEmbeddingLoss 的结果一致。

(17)nn.CosineEmbeddingLoss

功能:采用余弦相似度计算两个输入的相似性。

nn.CosineEmbeddingLoss(
    margin=0.0,
    size_average=None,
    reduce=None,
    reduction='mean'
)

主要参数:

  • margin:可取值 [-1, 1],推荐为 [0, 0.5]。
  • reduction:计算模式,可为 none/sum/mean。

计算公式:

$ l o s s ( x , y ) = { 1 − cos ⁡ ( x 1 , x 2 )   , if  y = 1 max ⁡ { 0 , cos ⁡ ( x 1 , x 2 ) − m a r g i n }   , if  y = − 1 \mathrm{loss}(x,y) = \begin{cases}1-\cos(x_1,x_2)\, , & \text{if } y = 1 \\[2ex] \max\{0,\cos(x_1,x_2)-\mathrm{margin}\}\, , & \text{if } y = -1\end{cases} loss(x,y)= 1cos(x1,x2),max{0,cos(x1,x2)margin},if y=1if y=1
其中,

cos ⁡ ( θ ) = A ⋅ B ∥ A ∥ ∥ B ∥ = ∑ i = 1 n A i × B i ∑ i = 1 n ( A i ) 2 × ∑ i = 1 n ( B i ) 2 \cos(\theta) = \dfrac{A\cdot B}{\|A\| \|B\|} = \dfrac{\sum_{i=1}^{n}A_i \times B_i}{\sqrt{\sum_{i=1}^{n}(A_i)^2} \times \sqrt{\sum_{i=1}^{n}(B_i)^2}} cos(θ)=A∥∥BAB=i=1n(Ai)2 ×i=1n(Bi)2 i=1nAi×Bi

代码示例:

x1 = torch.tensor([[0.3, 0.5, 0.7], [0.3, 0.5, 0.7]])
x2 = torch.tensor([[0.1, 0.3, 0.5], [0.1, 0.3, 0.5]])
target = torch.tensor([[1, -1]], dtype=torch.float)

loss_f = nn.CosineEmbeddingLoss(margin=0., reduction='none')
loss = loss_f(x1, x2, target)

print("Cosine Embedding Loss", loss)

输出结果:

Cosine Embedding Loss tensor([[0.0167, 0.9833]])

下面我们通过手动计算验证上面公式的正确性:

margin = 0.

def cosine(a, b):
    numerator = torch.dot(a, b)
    denominator = torch.norm(a, 2) * torch.norm(b, 2)
    return float(numerator/denominator)

l_1 = 1 - (cosine(x1[0], x2[0]))
l_2 = max(0, cosine(x1[0], x2[0]) - margin)

print(l_1, l_2)

输出结果:

0.016662120819091797 0.9833378791809082

可以看到,手动计算的结果与 PyTorch 中的 nn.CosineEmbeddingLoss 的结果一致。

(18)nn.CTCLoss

  • 参考文献:A. Graves et al.: Connectionist Temporal Classification: Labelling Unsegmented Sequence Data with Recurrent Neural Networks

功能:计算 CTC (Connectionist Temporal Classification) 损失,用于解决时序类数据的分类。

torch.nn.CTCLoss(
    blank=0,
    reduction='mean',
    zero_infinity=False
)

主要参数:

  • blank:blank label。
  • zero_infinity:无穷大的值或梯度值为 0。
  • reduction:计算模式,可为 none/sum/mean。

代码示例:

T = 50      # Input sequence length
C = 20      # Number of classes (including blank)
N = 16      # Batch size
S = 30      # Target sequence length of longest target in batch
S_min = 10  # Minimum target length, for demonstration purposes

# Initialize random batch of input vectors, for *size = (T,N,C)
inputs = torch.randn(T, N, C).log_softmax(2).detach().requires_grad_()

# Initialize random batch of targets (0 = blank, 1:C = classes)
target = torch.randint(low=1, high=C, size=(N, S), dtype=torch.long)

input_lengths = torch.full(size=(N,), fill_value=T, dtype=torch.long)
target_lengths = torch.randint(low=S_min, high=S, size=(N,), dtype=torch.long)

ctc_loss = nn.CTCLoss()
loss = ctc_loss(inputs, target, input_lengths, target_lengths)

print("CTC loss: ", loss)

输出结果:

CTC loss:  tensor(7.5385, grad_fn=)

3.2. 总结

到这里,我们已经学习完了 PyTorch 中的 18 种损失函数:

  • nn.CrossEntropyLoss
  • nn.NLLLoss
  • nn.BCELoss
  • nn.BCEWithLogitsLoss
  • nn.L1Loss
  • nn.MSELoss
  • nn.SmoothL1Loss
  • nn.PoissonNLLLoss
  • nn.KLDivLoss
  • nn.MarginRankingLoss
  • nn.MultiLabelMarginLoss
  • nn.SoftMarginLoss
  • nn.MultiLabelSoftMarginLoss
  • nn.MultiMarginLoss
  • nn.TripletMarginLoss
  • nn.HingeEmbeddingLoss
  • nn.CosineEmbeddingLoss
  • nn.CTCLoss

4.优化器(一)

前两节中,我们学习了损失函数的概念以及 PyTorch 中的一系列损失函数方法,我们知道了损失函数的作用是衡量模型输出与真实标签之间的差异。在得到了 loss 函数之后,我们应该如何去更新模型参数,使得 loss 逐步降低呢?这正是优化器的工作。本节课我们开始学习优化器模块。

4.1. 什么是优化器

在学习优化器模块之前,我们先回顾一下机器学习模型训练的 5 个步骤:

ch04-损失优化_第7张图片

我们看到,优化器是第 4 个模块,那么它的作用是什么呢?我们知道,在前一步的损失函数模块中,我们会得到一个 loss 值,即模型输出与真实标签之间的差异。

有了 loss 值之后,我们一般会采用 PyTorch 中的 AutoGrid 自动梯度求导模块对模型中参数的梯度进行求导计算,之后优化器会拿到这些梯度值并采用一些优化策略去更新模型参数,使得 loss 值下降。

因此,优化器的作用就是利用梯度来更新模型中的可学习参数,使得模型输出与真实标签之间的差异更小,即让 loss 值下降。

PyTorch 的优化器:管理更新 模型中可学习参数 (权值或偏置) 的值,使得模型输出更接近真实标签。

  • 导数:函数在指定坐标轴上的变化率。
  • 方向导数:指定方向上的变化率。
  • 梯度:一个向量,方向为方向导数取得最大值的方向。

4.2. Optimizer 的属性

PyTorch 中的 Optimizer 类:

class Optimizer(object):
    def __init__(self, params, defaults):
        self.defaults = defaults
        self.state = defaultdict(dict)
        self.param_groups = []
        
        param_groups = [{'params': param_groups}]

基本属性:

  • defaults:优化器的超参数,如 weight_decay,momentum。
  • state:参数的缓存,如 momentum 中需要用到前几次的梯度,就缓存在这个变量中。
  • params_groups:管理的参数组,是一个 list,其中每个元素是字典,包括 momentum、lr、weight_decay、params 等。
  • _ step_count:记录更新次数,学习率调整中使用。

4.3. Optimizer 的方法

class Optimizer(object):
    def zero_grad(self):
        for group in self.param_groups:
            for p in group['params']:
                if p.grad is not None:
                    p.grad.detach_()
                    p.grad.zero_()

    def add_param_group(self, param_group):
        for group in self.param_groups:
            param_set.update(set(group['params’]))
        self.param_groups.append(param_group)
    
    def state_dict(self):
        return {'state': packed_state, 'param_groups': param_groups,}

    def load_state_dict(self, state_dict):

基本方法:

  • zero_grad():清空所管理参数的梯度 (PyTorch 特性:张量梯度不自动清零)。由于 PyTorch 的特性是张量的梯度不自动清零,因此每次反向传播之后都需要清空梯度。。
  • step():执行一步梯度更新。
  • add_param_group():添加参数组。
  • state_dict():获取优化器当前状态信息 字典
  • load_state_dict():加载状态信息字典。

例子:人民币二分类

# -*- coding: utf-8 -*-

import os
import numpy as np
import torch
import torch.nn as nn
from torch.utils.data import DataLoader
import torchvision.transforms as transforms
import torch.optim as optim
from matplotlib import pyplot as plt
from model.lenet import LeNet
from tools.my_dataset import RMBDataset
from tools.common_tools import transform_invert, set_seed

set_seed(1)  # 设置随机种子
rmb_label = {"1": 0, "100": 1}

# 参数设置
MAX_EPOCH = 10
BATCH_SIZE = 16
LR = 0.01
log_interval = 10
val_interval = 1

# ============================ step 1/5 数据 ============================

split_dir = os.path.join("..", "..", "data", "rmb_split")
train_dir = os.path.join(split_dir, "train")
valid_dir = os.path.join(split_dir, "valid")

norm_mean = [0.485, 0.456, 0.406]
norm_std = [0.229, 0.224, 0.225]

train_transform = transforms.Compose([
    transforms.Resize((32, 32)),
    transforms.RandomCrop(32, padding=4),
    transforms.RandomGrayscale(p=0.8),
    transforms.ToTensor(),
    transforms.Normalize(norm_mean, norm_std),
])

valid_transform = transforms.Compose([
    transforms.Resize((32, 32)),
    transforms.ToTensor(),
    transforms.Normalize(norm_mean, norm_std),
])

# 构建MyDataset实例
train_data = RMBDataset(data_dir=train_dir, transform=train_transform)
valid_data = RMBDataset(data_dir=valid_dir, transform=valid_transform)

# 构建DataLoder
train_loader = DataLoader(dataset=train_data, batch_size=BATCH_SIZE, shuffle=True)
valid_loader = DataLoader(dataset=valid_data, batch_size=BATCH_SIZE)

# ============================ step 2/5 模型 ============================

net = LeNet(classes=2)
net.initialize_weights()

# ============================ step 3/5 损失函数 ============================
criterion = nn.CrossEntropyLoss()                                                   # 选择损失函数

# ============================ step 4/5 优化器 ============================
optimizer = optim.SGD(net.parameters(), lr=LR, momentum=0.9)                        # 选择优化器
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.1)     # 设置学习率下降策略

# ============================ step 5/5 训练 ============================
train_curve = list()
valid_curve = list()

for epoch in range(MAX_EPOCH):

    loss_mean = 0.
    correct = 0.
    total = 0.

    net.train()
    for i, data in enumerate(train_loader):

        # forward
        inputs, labels = data
        outputs = net(inputs)

        # backward
        optimizer.zero_grad()
        loss = criterion(outputs, labels)
        loss.backward()

        # update weights
        optimizer.step()

        # 统计分类情况
        _, predicted = torch.max(outputs.data, 1)
        total += labels.size(0)
        correct += (predicted == labels).squeeze().sum().numpy()

        # 打印训练信息
        loss_mean += loss.item()
        train_curve.append(loss.item())
        if (i+1) % log_interval == 0:
            loss_mean = loss_mean / log_interval
            print("Training:Epoch[{:0>3}/{:0>3}] Iteration[{:0>3}/{:0>3}] Loss: {:.4f} Acc:{:.2%}".format(
                epoch, MAX_EPOCH, i+1, len(train_loader), loss_mean, correct / total))
            loss_mean = 0.

    scheduler.step()  # 更新学习率

    # validate the model
    if (epoch+1) % val_interval == 0:

        correct_val = 0.
        total_val = 0.
        loss_val = 0.
        net.eval()
        with torch.no_grad():
            for j, data in enumerate(valid_loader):
                inputs, labels = data
                outputs = net(inputs)
                loss = criterion(outputs, labels)

                _, predicted = torch.max(outputs.data, 1)
                total_val += labels.size(0)
                correct_val += (predicted == labels).squeeze().sum().numpy()

                loss_val += loss.item()

            valid_curve.append(loss_val)
            print("Valid:\t Epoch[{:0>3}/{:0>3}] Iteration[{:0>3}/{:0>3}] Loss: {:.4f} Acc:{:.2%}".format(
                epoch, MAX_EPOCH, j+1, len(valid_loader), loss_val, correct / total))


train_x = range(len(train_curve))
train_y = train_curve

train_iters = len(train_loader)
valid_x = np.arange(1, len(valid_curve)+1) * train_iters*val_interval # 由于valid中记录的是epochloss,需要对记录点进行转换到iterations
valid_y = valid_curve

plt.plot(train_x, train_y, label='Train')
plt.plot(valid_x, valid_y, label='Valid')

plt.legend(loc='upper right')
plt.ylabel('loss value')
plt.xlabel('Iteration')
plt.show()

# ============================ inference ============================

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
test_dir = os.path.join(BASE_DIR, "test_data")

test_data = RMBDataset(data_dir=test_dir, transform=valid_transform)
valid_loader = DataLoader(dataset=test_data, batch_size=1)

for i, data in enumerate(valid_loader):
    # forward
    inputs, labels = data
    outputs = net(inputs)
    _, predicted = torch.max(outputs.data, 1)

    rmb = 1 if predicted.numpy()[0] == 0 else 100

    img_tensor = inputs[0, ...]  # C H W
    img = transform_invert(img_tensor, train_transform)
    plt.imshow(img)
    plt.title("LeNet got {} Yuan".format(rmb))
    plt.show()
    plt.pause(0.5)
    plt.close()

下面我们来看一下优化器中的 5 种基本方法的具体使用方式:

4.3.1.step()

为了方便计算,我们先设置学习率 lr=1

import os
import torch
import torch.optim as optim
from tools.common_tools import set_seed

BASE_DIR = os.path.dirname(os.path.abspath(__file__))

set_seed(1)  # 设置随机种子

weight = torch.randn((2, 2), requires_grad=True)
weight.grad = torch.ones((2, 2))

optimizer = optim.SGD([weight], lr=1)

print("weight before step:{}".format(weight.data))
optimizer.step()        # 修改lr=1 0.1观察结果
print("weight after step:{}".format(weight.data))

输出结果:

weight before step:tensor([[0.6614, 0.2669],
        [0.0617, 0.6213]])
weight after step:tensor([[-0.3386, -0.7331],
        [-0.9383, -0.3787]])

可以看到,第一个梯度在更新之前的值为 0.6614,更新之后的值为 0.6614 - 1 = -0.3386。现在,我们将学习率设置为 lr=0.1,观察结果是否发生变化:

optimizer = optim.SGD([weight], lr=0.1)

输出结果:

weight before step:tensor([[0.6614, 0.2669],
        [0.0617, 0.6213]])
weight after step:tensor([[ 0.5614,  0.1669],
        [-0.0383,  0.5213]])

可以看到,第一个梯度更新后的值变为了 0.6614 - 0.1 = 0.5614。这就是 step() 方法的一步更新。

4.3.2.zero_grad ()

print("weight before step:{}".format(weight.data))
optimizer.step()        # 修改lr=1 0.1观察结果
print("weight after step:{}".format(weight.data))

print("weight in optimizer:{}\nweight in weight:{}\n".format(id(optimizer.param_groups[0]['params'][0]), id(weight)))

print("weight.grad is {}\n".format(weight.grad))
optimizer.zero_grad()
print("after optimizer.zero_grad(), weight.grad is\n{}".format(weight.grad))

输出结果:

weight before step:tensor([[0.6614, 0.2669],
        [0.0617, 0.6213]])
weight after step:tensor([[ 0.5614,  0.1669],
        [-0.0383,  0.5213]])
weight in optimizer:4729862544
weight in weight:4729862544
weight.grad is tensor([[1., 1.],
        [1., 1.]])
after optimizer.zero_grad(), weight.grad is
tensor([[0., 0.],
        [0., 0.]])

可以看到,在执行 zero_grad() 之前,我们的梯度为 [[1., 1.],[1., 1.]],执行之后变为了 [[0., 0.],[0., 0.]]。另外,我们看到,optimizer 中管理的 weight 的内存地址和真实的 weight 地址是相同的,所以我们在优化器中保存的是参数的地址,而不是拷贝的参数的值,这样可以节省内存消耗。

4.3.3.add_param_group ()

我们同样采用上面的优化器,该优化器当前已经管理了一组参数,就是我们的 weight。现在我们希望再增加一组参数,并且我们将该组参数的学习率设置的更小一些 lr=0.0001。

首先,我们需要构建这样一组参数的字典,字典的 key 设置为 params,其值为新的一组参数 w2;然后可以设置一些超参数,例如学习率 ‘lr’ 等。然后我们使用 add_param_group () 将这组参数加入优化器中。

print("optimizer.param_groups is\n{}".format(optimizer.param_groups))
w2 = torch.randn((3, 3), requires_grad=True)
optimizer.add_param_group({"params": w2, 'lr': 0.0001})
print("optimizer.param_groups is\n{}".format(optimizer.param_groups))

输出结果:

optimizer.param_groups is
[{'params': [tensor([[0.6614, 0.2669],
        [0.0617, 0.6213]], requires_grad=True)], 'lr': 0.1, 'momentum': 0, 'dampening': 0, 'weight_decay': 0, 'nesterov': False}]
optimizer.param_groups is
[{'params': [tensor([[0.6614, 0.2669],
        [0.0617, 0.6213]], requires_grad=True)], 'lr': 0.1, 'momentum': 0, 'dampening': 0, 'weight_decay': 0, 'nesterov': False}, {'params': [tensor([[-0.4519, -0.1661, -1.5228],
        [ 0.3817, -1.0276, -0.5631],
        [-0.8923, -0.0583, -0.1955]], requires_grad=True)], 'lr': 0.0001, 'momentum': 0, 'dampening': 0, 'weight_decay': 0, 'nesterov': False}]

可以看到,在加入新参数之前,我们的优化器中只有一组参数,以一个列表形式呈现,里面只有一个字典元素。

当我们使用 add_param_group () 之后,列表中有了两个字典元素。

可以看到,两组参数的学习率是不同的,所以通过这种方式我们可以为不同的参数组设置不同的学习率,这在模型拟合过程中是一种非常实用的方法。

4.3.4.state_dict() 和 load_state_dict()

这两个函数用于保存优化器的状态信息,通常用于断点的继续训练。

(1)保存状态信息:

optimizer = optim.SGD([weight], lr=0.1, momentum=0.9)
opt_state_dict = optimizer.state_dict()

print("state_dict before step:\n", opt_state_dict)

for i in range(10):
    optimizer.step()

print("state_dict after step:\n", optimizer.state_dict())

torch.save(optimizer.state_dict(), os.path.join(BASE_DIR, "optimizer_state_dict.pkl"))

输出结果:

state_dict before step:
 {'state': {}, 'param_groups': [{'lr': 0.1, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0, 'nesterov': False, 'params': [4745884296]}]}
state_dict after step:
 {'state': {4745884296: {'momentum_buffer': tensor([[6.5132, 6.5132],
        [6.5132, 6.5132]])}}, 'param_groups': [{'lr': 0.1, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0, 'nesterov': False, 'params': [4745884296]}]}

可以看到,在更新之前,‘state’ 里的值是一个空字典。在经过 10 步更新之后,‘state’ 字典中有了一些值,它的 key 是 4745884296,即参数地址,而它的值也是一个字典。其中 ‘momentum_buffer’ 是动量中会使用的一些缓存信息。

所以,在 ‘state’ 中我们是通过地址去匹配参数的缓存的。然后,我们使用 torch.save 对字典进行序列化,将其保存为一个 pkl 的形式,可以看到当前文件夹下多了一个 optimizer_state_dict.pkl 的文件。

(2)读取状态信息:

之前我们的模型已经训练了 10 次,假设我们总共需要训练 100 次,我们不希望再从头训练,而是希望能够接着之前第 10 次的状态继续训练,我们可以利用 load_state_dict 加载前面保存的 optimizer_state_dict.pkl 文件,并将其读取加载到优化器中继续训练:

optimizer = optim.SGD([weight], lr=0.1, momentum=0.9)
state_dict = torch.load(os.path.join(BASE_DIR, "optimizer_state_dict.pkl"))

print("state_dict before load state:\n", optimizer.state_dict())
optimizer.load_state_dict(state_dict)
print("state_dict after load state:\n", optimizer.state_dict())

输出结果:

state_dict before load state:
 {'state': {}, 'param_groups': [{'lr': 0.1, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0, 'nesterov': False, 'params': [4684949616]}]}
state_dict after load state:
 {'state': {4684949616: {'momentum_buffer': tensor([[6.5132, 6.5132],
        [6.5132, 6.5132]])}}, 'param_groups': [{'lr': 0.1, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0, 'nesterov': False, 'params': [4684949616]}]}

可以看到,在加载之前,‘state’ 里的值是一个空字典。在使用 load_state_dict 加载之后,我们得到了之前第 10 次的参数状态,然后继续在此基础上进行训练即可。

4.4. 总结

本节中,我们学习了优化器 Optimizer 的概念和一些基本属性及方法。在下节中,我们将继续学习 PyTorch 中的一些常用的优化方法 (优化器)。

5.优化器 (二)

上节我们学习了 PyTorch 中优化器的主要属性和基本方法。我们知道优化器的主要作用是管理并更新我们的参数,并且在更新时会利用到参数的梯度信息,然后采用一定的更新策略来更新我们的参数。本节我们将学习一些最常用的更新策略,例如随机梯度下降法等。

5.1. 学习率

梯度下降中的参数更新过程:

w i + 1 = w i − g ( w i ) w_{i+1} = w_i - g(w_i) wi+1=wig(wi)
其中, g ( w i ) g(w_i) g(wi) 表示 w i w_i wi 的梯度。

下面我们通过一个例子来观察梯度下降的过程以及可能存在的问题:

ch04-损失优化_第8张图片

假设我们现在有一个函数:

y = f ( x ) = 4 x 2 y=f(x) = 4x^2 y=f(x)=4x2

假设我们的起始点为 x 0 = 2 x_0=2 x0=2,现在我们采用梯度下降法更新函数值 y y y 使其达到其极小值点 x = 0 x=0 x=0。首先我们求取 y y y 的导函数:

y ′ = f ′ ( x ) = 8 x y' = f'(x) = 8x y=f(x)=8x
我们从起始点 x 0 = 2 x_0=2 x0=2 开始沿负梯度方向更新 y y y 值:

  • x 0 = 2 ,    y 0 = 16 ,    f ’ ( x 0 ) = 16 x_0 = 2, \; y_0 = 16,\; f’(x_0) = 16 x0=2,y0=16,f(x0)=16
    x 1 = x 0 − f ’ ( x 0 ) = 2 − 16 = − 14 x_1 = x_0 - f’(x_0) = 2 -16 = -14 x1=x0f(x0)=216=14
  • x 1 = − 14 ,    y 1 = 784 ,    f ’ ( x 1 ) = − 112 x_1 = -14, \; y_1 = 784,\; f’(x_1) = -112 x1=14,y1=784,f(x1)=112
    x 2 = x 1 − f ’ ( x 1 ) = − 14 + 112 = 98 ,    y 2 = 38416 x_2 = x_1 - f’(x_1) = -14 + 112 = 98,\; y_2 = 38416 x2=x1f(x1)=14+112=98,y2=38416
  • ……

我们发现,y 值不但没有减小,反而越来越大了。这是什么原因导致的呢?下面我们先通过代码来演示这一过程,然后再分析导致该问题的原因。

首先,我们先绘制出函数曲线:

import torch
import numpy as np
import matplotlib.pyplot as plt

torch.manual_seed(1)

def func(x_t):
    """
    y = (2x)^2 = 4*x^2      dy/dx = 8x
    """
    return torch.pow(2*x_t, 2)

# init
x = torch.tensor([2.], requires_grad=True)

# plot data
x_t = torch.linspace(-3, 3, 100)
y = func(x_t)
plt.plot(x_t.numpy(), y.numpy(), label="y = 4*x^2")
plt.grid()
plt.xlabel("x")
plt.ylabel("y")
plt.legend()
plt.show()

输出结果:

ch04-损失优化_第9张图片

下面我们通过代码来演示一下前面例子中的梯度下降过程:

iter_rec, loss_rec, x_rec = list(), list(), list()

lr = 1.
max_iteration = 4

for i in range(max_iteration):
    y = func(x)
    y.backward()

    print("Iter:{}, X:{:8}, X.grad:{:8}, loss:{:10}".format(
        i, x.detach().numpy()[0], x.grad.detach().numpy()[0], y.item()))

    x_rec.append(x.item())

    x.data.sub_(lr * x.grad)    # x -= x.grad  数学表达式意义:  x = x - x.grad
    x.grad.zero_()

    iter_rec.append(i)
    loss_rec.append(y)

plt.subplot(121).plot(iter_rec, loss_rec, '-ro')
plt.xlabel("Iteration")
plt.ylabel("Loss value")

x_t = torch.linspace(-3, 3, 100)
y = func(x_t)
plt.subplot(122).plot(x_t.numpy(), y.numpy(), label="y = 4*x^2")
plt.grid()
y_rec = [func(torch.tensor(i)).item() for i in x_rec]
plt.subplot(122).plot(x_rec, y_rec, '-ro')
plt.legend()
plt.show()

输出结果:

Iter:0, X:     2.0, X.grad:    16.0, loss:      16.0
Iter:1, X:   -14.0, X.grad:  -112.0, loss:     784.0
Iter:2, X:    98.0, X.grad:   784.0, loss:   38416.0
Iter:3, X:  -686.0, X.grad: -5488.0, loss: 1882384.0

ch04-损失优化_第10张图片

左边是 loss 曲线图,横轴是迭代次数,纵轴是 loss 值;右边是函数曲线图,由于尺度过大这里暂时看不出来函数形状。

从打印信息可以看到,在第 0 次迭代时,x 的初始值为 2,对应梯度为 16,loss 值也是 16。随着迭代次数的增加,我们发现 loss 值激增到 1882384。所以,y 并没有减小,反而是激增的,而梯度也达到了 10^3 数量级,所以存在梯度爆炸的问题。

回到前面的梯度更新公式:

w i + 1 = w i − g ( w i ) w_{i+1} = w_i - g(w_i) wi+1=wig(wi)
这里可能存在一个问题,我们目前是直接减去梯度项 g ( w i ) g(w_i) g(wi),而这里减去的梯度项可能由于其尺度过大从而导致参数项越来越大,从而导致函数值无法降低。因此,通常我们会在梯度项前面乘以一个系数,用于缩减尺度:

w i + 1 = w i − L R ⋅ g ( w i ) w_{i+1} = w_i - \mathrm{LR}\cdot g(w_i) wi+1=wiLRg(wi)
这里,我们将系数 L R \mathrm{LR} LR 称为 学习率 (learning rate),它被用来控制更新的步伐。

下面我们在代码中调整学习率,观察函数值的变化:

L R = 0.5 \mathrm{LR} = 0.5 LR=0.5

lr = 0.5

输出结果:

Iter:0, X:     2.0, X.grad:    16.0, loss:      16.0
Iter:1, X:    -6.0, X.grad:   -48.0, loss:     144.0
Iter:2, X:    18.0, X.grad:   144.0, loss:    1296.0
Iter:3, X:   -54.0, X.grad:  -432.0, loss:   11664.0

ch04-损失优化_第11张图片

可以看到,loss 值仍然呈激增趋势,但是情况有所缓解,尺度比之前小了很多。

L R = 0.2 \mathrm{LR} = 0.2 LR=0.2

lr = 0.2

输出结果:

Iter:0, X:     2.0, X.grad:    16.0, loss:      16.0
Iter:1, X:-1.2000000476837158, X.grad:-9.600000381469727, loss:5.760000228881836
Iter:2, X:0.7200000286102295, X.grad:5.760000228881836, loss:2.0736000537872314
Iter:3, X:-0.4320000410079956, X.grad:-3.456000328063965, loss:0.7464961409568787

ch04-损失优化_第12张图片

可以看到,现在 loss 值呈下降趋势,同时右图也可以看到正常的函数图像了。当前学习率为 0.2,从右图可以看到:初始点为 x=2,loss 值为 16;经过一步更新后来到点 x = -1.2,此时 loss 值为 5.76;然后再次迭代后来到 x=0.72,loss 值为 2.07;第三次迭代后,x = -0.43,loss 值为 0.75。

现在我们将增加迭代次数增加到 20 次,来观察函数是否能够到达极小值点 x=0 附近:

max_iteration = 20

输出结果:

Iter:0, X:     2.0, X.grad:    16.0, loss:      16.0
Iter:1, X:-1.2000000476837158, X.grad:-9.600000381469727, loss:5.760000228881836
Iter:2, X:0.7200000286102295, X.grad:5.760000228881836, loss:2.0736000537872314
Iter:3, X:-0.4320000410079956, X.grad:-3.456000328063965, loss:0.7464961409568787
Iter:4, X:0.2592000365257263, X.grad:2.0736002922058105, loss:0.26873862743377686
Iter:5, X:-0.1555200219154358, X.grad:-1.2441601753234863, loss:0.09674590826034546
Iter:6, X:0.09331201016902924, X.grad:0.7464960813522339, loss:0.03482852503657341
Iter:7, X:-0.05598720908164978, X.grad:-0.44789767265319824, loss:0.012538270093500614
Iter:8, X:0.03359232842922211, X.grad:0.26873862743377686, loss:0.004513778258115053
Iter:9, X:-0.020155396312475204, X.grad:-0.16124317049980164, loss:0.0016249599866569042
Iter:10, X:0.012093238532543182, X.grad:0.09674590826034546, loss:0.0005849856534041464
Iter:11, X:-0.007255943492054939, X.grad:-0.058047547936439514, loss:0.000210594866075553
Iter:12, X:0.0043535660952329636, X.grad:0.03482852876186371, loss:7.581415411550552e-05
Iter:13, X:-0.0026121395640075207, X.grad:-0.020897116512060165, loss:2.729309198912233e-05
Iter:14, X:0.001567283645272255, X.grad:0.01253826916217804, loss:9.825512279348914e-06
Iter:15, X:-0.0009403701405972242, X.grad:-0.007522961124777794, loss:3.537184056767728e-06
Iter:16, X:0.0005642221076413989, X.grad:0.004513776861131191, loss:1.2733863741232199e-06
Iter:17, X:-0.00033853325294330716, X.grad:-0.0027082660235464573, loss:4.584190662626497e-07
Iter:18, X:0.00020311994012445211, X.grad:0.001624959520995617, loss:1.6503084054875217e-07
Iter:19, X:-0.00012187196989543736, X.grad:-0.0009749757591634989, loss:5.941110714502429e-08

可以看到,在迭代 5 到 7 次之后,左图中的 loss 曲线已经趋近于零了,即已经达到收敛,同时右图可以看到最后几次迭代都在极小值点附近来回振动,这说明我们的学习率是比较合理的。

L R = 0.1 \mathrm{LR} = 0.1 LR=0.1
那么,是否还存在更好的学习率呢?我们尝试将学习率调整到 0.1,观察函数值的变化:

lr = 0.1

输出结果:

Iter:0, X:     2.0, X.grad:    16.0, loss:      16.0
Iter:1, X:0.3999999761581421, X.grad:3.1999998092651367, loss:0.6399999260902405
Iter:2, X:0.07999998331069946, X.grad:0.6399998664855957, loss:0.025599990040063858
Iter:3, X:0.015999995172023773, X.grad:0.12799996137619019, loss:0.0010239994153380394
Iter:4, X:0.0031999992206692696, X.grad:0.025599993765354156, loss:4.0959981561172754e-05
Iter:5, X:0.0006399997510015965, X.grad:0.005119998008012772, loss:1.6383987713197712e-06
Iter:6, X:0.00012799992691725492, X.grad:0.0010239994153380394, loss:6.553592868385749e-08
Iter:7, X:2.5599983928259462e-05, X.grad:0.0002047998714260757, loss:2.621436623329032e-09
Iter:8, X:5.1199967856518924e-06, X.grad:4.095997428521514e-05, loss:1.0485746992916489e-10
Iter:9, X:1.0239991752314381e-06, X.grad:8.191993401851505e-06, loss:4.194297253262702e-12
Iter:10, X:2.047998464149714e-07, X.grad:1.6383987713197712e-06, loss:1.6777191073034936e-13
Iter:11, X:4.095996075648145e-08, X.grad:3.276796860518516e-07, loss:6.710873481539318e-15
Iter:12, X:8.191992861839026e-09, X.grad:6.553594289471221e-08, loss:2.6843498478959363e-16
Iter:13, X:1.6383983059142793e-09, X.grad:1.3107186447314234e-08, loss:1.0737395785076275e-17
Iter:14, X:3.2767966118285585e-10, X.grad:2.621437289462847e-09, loss:4.294958520825663e-19
Iter:15, X:6.55359377876863e-11, X.grad:5.242875023014903e-10, loss:1.7179836926736008e-20
Iter:16, X:1.3107185475869088e-11, X.grad:1.048574838069527e-10, loss:6.871932690625968e-22
Iter:17, X:2.62143692170147e-12, X.grad:2.097149537361176e-11, loss:2.748772571379408e-23
Iter:18, X:5.242874277083809e-13, X.grad:4.194299421667047e-12, loss:1.0995092021011623e-24
Iter:19, X:1.0485747469965445e-13, X.grad:8.388597975972356e-13, loss:4.398036056521599e-26

ch04-损失优化_第13张图片

可以看到,当学习率调整为 0.1 时,loss 曲线也可以快速收敛。

L R = 0.125 \mathrm{LR} = 0.125 LR=0.125
那么,有没有能够使得收敛速度更快的学习率呢?我们尝试将学习率设置为 0.125:

lr = 0.125

输出结果:

Iter:0, X:     2.0, X.grad:    16.0, loss:      16.0
Iter:1, X:     0.0, X.grad:     0.0, loss:       0.0
Iter:2, X:     0.0, X.grad:     0.0, loss:       0.0
Iter:3, X:     0.0, X.grad:     0.0, loss:       0.0
Iter:4, X:     0.0, X.grad:     0.0, loss:       0.0
Iter:5, X:     0.0, X.grad:     0.0, loss:       0.0
Iter:6, X:     0.0, X.grad:     0.0, loss:       0.0
Iter:7, X:     0.0, X.grad:     0.0, loss:       0.0
Iter:8, X:     0.0, X.grad:     0.0, loss:       0.0
Iter:9, X:     0.0, X.grad:     0.0, loss:       0.0
Iter:10, X:     0.0, X.grad:     0.0, loss:       0.0
Iter:11, X:     0.0, X.grad:     0.0, loss:       0.0
Iter:12, X:     0.0, X.grad:     0.0, loss:       0.0
Iter:13, X:     0.0, X.grad:     0.0, loss:       0.0
Iter:14, X:     0.0, X.grad:     0.0, loss:       0.0
Iter:15, X:     0.0, X.grad:     0.0, loss:       0.0
Iter:16, X:     0.0, X.grad:     0.0, loss:       0.0
Iter:17, X:     0.0, X.grad:     0.0, loss:       0.0
Iter:18, X:     0.0, X.grad:     0.0, loss:       0.0
Iter:19, X:     0.0, X.grad:     0.0, loss:       0.0

ch04-损失优化_第14张图片

可以看到,当学习率为 0.125 时,仅经过一次迭代,loss 值就已经达到收敛。那么,这个 0.125 是如何得到的呢?如果我们不知道函数表达式,我们是没办法直接计算出最佳学习率的。所以,通常我们会尝试性地设置一系列的学习率,以找到最佳学习率。下面我们来观察设置多个学习率时的 loss 变化情况。

设置多个学习率

我们在 0.01 到 0.5 之间线性地设置 10 个学习率:

iteration = 100
num_lr = 10
lr_min, lr_max = 0.01, 0.5

lr_list = np.linspace(lr_min, lr_max, num=num_lr).tolist()
loss_rec = [[] for l in range(len(lr_list))]
iter_rec = list()

for i, lr in enumerate(lr_list):
    x = torch.tensor([2.], requires_grad=True)
    for iter in range(iteration):

        y = func(x)
        y.backward()
        x.data.sub_(lr * x.grad)  # x.data -= x.grad
        x.grad.zero_()

        loss_rec[i].append(y.item())

for i, loss_r in enumerate(loss_rec):
    plt.plot(range(len(loss_r)), loss_r, label="LR: {}".format(lr_list[i]))
plt.legend()
plt.xlabel('Iterations')
plt.ylabel('Loss value')
plt.show()

输出结果:

ch04-损失优化_第15张图片

我们得到了 10 个不同的 loss 曲线,横轴表示迭代次数,纵轴表示 loss 值。可以看到,loss 值的尺度是 1 0 38 10^{38} 1038,这是一个非常大的数字,不是我们所希望的。可以看到,从 0.5 到 0.337 这 4 条曲线都存在激增趋势。这表明我们的学习率上限设置得过大了,导致了 loss 激增和梯度爆炸。现在,我们将学习率上限改为 0.3,观察一下 loss 曲线的变化情况:

lr_min, lr_max = 0.01, 0.3

输出结果:

ch04-损失优化_第16张图片

可以看到,在学习率取到最大值 0.3 时,loss 值尺度为 1 0 30 10^{30} 1030,相比之前 1 0 38 10^{38} 1038 有所下降,但是 loss 值仍然存在激增现象。下面我们将学习率上限改为 0.2,观察一下 loss 曲线的变化情况:

lr_min, lr_max = 0.01, 0.2

输出结果:
ch04-损失优化_第17张图片

可以看到,现在的 10 条曲线都呈现下降趋势,这正是我们所期望的。最右边的蓝色曲线对应最小学习率 0.01,其收敛速度也是最慢的,大约为 30 次。右数第二条橙色曲线对应第二小的学习率 0.03,其收敛速度也是第二慢的。那么,是否学习率越大,收敛越快呢?我们看到,收敛最快的曲线并不是最大学习率 0.2 对应的青色曲线,而是学习率 0.136 对应的粉色曲线。回忆一下,前面我们提到的最佳学习率 0.125,这些学习率中与其距离最近的正是 0.136。因此,当学习率距离最优学习率最近时,收敛速度最快。但是,我们没有上帝视角,无法提前知道最优学习率,所以我们通常会设置诸如 0.01 这样非常小的学习率,以达到收敛效果,其代价就是收敛速度可能会较慢。

综上所述,设置学习率时不能过大,否则将导致 loss 值激增,并且引发梯度爆炸;同时也不能过小,否则会导致收敛速度过慢,时间成本增加。通过将学习率设置为 0.01 这样较小的值,就可以使得我们的 loss 值逐渐下降直到收敛。

5.2. 动量

Momentum (动量/冲量):结合当前梯度与上一次更新信息,用于当前更新。不仅考虑当前的梯度,还会结合前面的梯度。

指数加权更新:求取当前时刻的平均值,常用于时间序列分析。主要思想为对于那些距离当前时刻越近的参数值,它们的参考性越大,所占的权重也越大,而权重随时间间隔的增大呈指数下降。

v t = β ⋅ v t − 1 + ( 1 − β ) ⋅ θ t v_t = \beta \cdot v_{t-1} + (1-\beta) \cdot \theta_t vt=βvt1+(1β)θt
其中, v t v_t vt 是当前时刻的平均值, v t − 1 v_{t-1} vt1 是上一个时刻的指数加权平均, θ t \theta_t θt 是当前时刻的参数值,\beta 是权重参数,一般小于 1。指数加权平均常用于时间序列求平均值。

例子:数据集为连续多天的温度值:

ch04-损失优化_第18张图片

假设现在我们要求取第 100 天的温度平均值:

v 100 = β ⋅ v 99 + ( 1 − β ) ⋅ θ 100 = ( 1 − β ) ⋅ θ 100 + β ⋅ ( β ⋅ v 98 + ( 1 − β ) ⋅ θ 99 ) = ( 1 − β ) ⋅ θ 100 + ( 1 − β ) ⋅ β ⋅ θ 99 + β 2 ⋅ v 98 = ( 1 − β ) ⋅ θ 100 + ( 1 − β ) ⋅ β ⋅ θ 99 + ( 1 − β ) ⋅ β 2 ⋅ θ 98 + β 3 ⋅ v 97 = ⋯ = ∑ i = 0 100 ( 1 − β ) ⋅ β i ⋅ θ n − i \begin{aligned} v_{100} &= \beta \cdot v_{99} + (1-\beta) \cdot \theta_{100} \\[2ex] &= (1-\beta) \cdot \theta_{100} + \beta \cdot (\beta \cdot v_{98} + (1-\beta) \cdot \theta_{99}) \\[2ex] &= (1-\beta) \cdot \theta_{100} + (1-\beta) \cdot \beta \cdot \theta_{99} + \beta^2 \cdot v_{98} \\[2ex] &= (1-\beta) \cdot \theta_{100} + (1-\beta) \cdot \beta \cdot \theta_{99} + (1-\beta) \cdot \beta^2 \cdot \theta_{98} + \beta^3 \cdot v_{97} \\[2ex] &= \cdots \\[2ex] &= \sum_{i=0}^{100} (1-\beta) \cdot \beta^i \cdot \theta_{n-i} \end{aligned} v100=βv99+(1β)θ100=(1β)θ100+β(βv98+(1β)θ99)=(1β)θ100+(1β)βθ99+β2v98=(1β)θ100+(1β)βθ99+(1β)β2θ98+β3v97==i=0100(1β)βiθni

代码示例:

import torch
import numpy as np
import torch.optim as optim
import matplotlib.pyplot as plt

torch.manual_seed(1)


def exp_w_func(beta, time_list):
    return [(1 - beta) * np.power(beta, exp) for exp in time_list]


beta = 0.9
num_point = 100
time_list = np.arange(num_point).tolist()

weights = exp_w_func(beta, time_list)    # 指数权重

plt.plot(time_list, weights, '-ro', label="Beta: {}\ny = B^t * (1-B)".format(beta))
plt.xlabel("time")
plt.ylabel("weight")
plt.legend()
plt.title("exponentially weighted average")
plt.show()

print(np.sum(weights))

输出结果:

0.9999734386011124

ch04-损失优化_第19张图片

可以看到,权重随时间间隔的增加呈指数下降。下面我们尝试调整超参数 β \beta β 的值,来观察一下权重的变化:

# 多个权重曲线
beta_list = [0.98, 0.95, 0.9, 0.8]
w_list = [exp_w_func(beta, time_list) for beta in beta_list]

for i, w in enumerate(w_list):
    plt.plot(time_list, w, label="Beta: {}".format(beta_list[i]))
    plt.xlabel("time")
    plt.ylabel("weight")

plt.legend()
plt.show()

输出结果:

ch04-损失优化_第20张图片

可以看到,随着权重参数 β \beta β 的增大,权重曲线逐渐变得平缓。我们可以将其理解为某种记忆周期, β \beta β 值越小,其记忆周期越短,对于较长时间间隔参数的关注越少。通常我们将 β \beta β 设置为 0.9,即权重曲线将更加关注距离当前时间 1 / ( 1 − β ) = 10 1/(1-\beta) = 10 1/(1β)=10 天以内的数据。

我们已经了解了指数加权平均中的权重参数 β \beta β,在梯度下降中它对应的就是 momentum 系数。

梯度下降:

w i + 1 = w i − L R ⋅ g ( w i ) w_{i+1} = w_i - \mathrm{LR} \cdot g(w_i) wi+1=wiLRg(wi)
PyTorch 中的更新公式:

v i = m ⋅ v i − 1 + g ( w i ) v_i = m \cdot v_{i-1} + g(w_i) vi=mvi1+g(wi)
w i + 1 = w i − L R ⋅ v i w_{i+1} = w_i - \mathrm{LR} \cdot v_i wi+1=wiLRvi
其中,

  • w i + 1 w_{i+1} wi+1 是第 i + 1 i+1 i+1 次更新的参数,
  • L R \mathrm{LR} LR 是学习率,
  • g ( w i ) g(w_i) g(wi) w i w_i wi 的梯度,
  • v i v_i vi 是更新量,
  • m m m 是 momentum 系数。

例如:

v 100 = m ⋅ v 99 + g ( w 100 ) = g ( w 100 ) + m ⋅ ( m ⋅ v 98 + g ( w 99 ) ) = g ( w 100 ) + m ⋅ g ( w 99 ) + m 2 ⋅ v 98 = g ( w 100 ) + m ⋅ g ( w 99 ) + m 2 ⋅ g ( w 98 ) + m 3 ⋅ v 97 = ⋯ \begin{aligned} v_{100} &= m \cdot v_{99} + g(w_{100}) \\[2ex] &= g(w_{100}) + m \cdot (m \cdot v_{98} + g(w_{99})) \\[2ex] &= g(w_{100}) + m \cdot g(w_{99}) + m^2 \cdot v_{98} \\[2ex] &= g(w_{100}) + m \cdot g(w_{99}) + m^2 \cdot g(w_{98}) + m^3 \cdot v_{97} \\[2ex] &= \cdots \end{aligned} v100=mv99+g(w100)=g(w100)+m(mv98+g(w99))=g(w100)+mg(w99)+m2v98=g(w100)+mg(w99)+m2g(w98)+m3v97=
可以看到,momentum 系数的作用就是当前更新不仅考虑了当前的梯度信息,同时也考虑了之前几次的梯度信息。由于 momentum 系数取值在 [0,1],所以时间间隔越长的梯度信息所占权重越低。

代码示例:

下面我们设置两个学习率,在都不加上 momentum 系数的情况下,对比两者的权重曲线:

def func(x):
    return torch.pow(2*x, 2)    # y = (2x)^2 = 4*x^2        dy/dx = 8x

iteration = 100

m = 0.    # Momentum 系数
lr_list = [0.01, 0.03]    # 学习率

momentum_list = list()
loss_rec = [[] for l in range(len(lr_list))]
iter_rec = list()

for i, lr in enumerate(lr_list):
    x = torch.tensor([2.], requires_grad=True)

    momentum = 0. if lr == 0.03 else m
    momentum_list.append(momentum)

    optimizer = optim.SGD([x], lr=lr, momentum=momentum)

    for iter in range(iteration):

        y = func(x)
        y.backward()

        optimizer.step()
        optimizer.zero_grad()

        loss_rec[i].append(y.item())

for i, loss_r in enumerate(loss_rec):
    plt.plot(range(len(loss_r)), loss_r, label="LR: {} M:{}".format(lr_list[i], momentum_list[i]))

plt.legend()
plt.xlabel('Iterations')
plt.ylabel('Loss value')
plt.show()

输出结果:

ch04-损失优化_第21张图片

可以看到,学习率为 0.03 的橙色曲线要比学习率为 0.01 的蓝色曲线的收敛速度更快。下面我们为较小的学习率 0.01 增加一个 momentum 系数,与不加 momentum 系数的学习率 0.03 对比:

m = 0.9    # 为学习率 0.01 增加一个 momentum 系数 0.9

输出结果:

ch04-损失优化_第22张图片

可以看到,在增加了一个 momentum 系数 0.9 后,学习率 0.01 对应的蓝色曲线要比学习率为 0.03 但是没有增加 momentum 系数的橙色曲线更快到达最低点。另外,蓝色曲线前期呈现震荡趋势,这是由于我们的momentum 系数设置过大,虽然我们当前梯度很小,但是之前的梯度很大,导致在到达极小值后受到前几个时刻的梯度信息的影响而反弹,如此往复震荡。我们可以尝试修改 momentum 系数的值:

m = 0.63    # 为学习率 0.01 增加一个 momentum 系数 0.63

输出结果:

ch04-损失优化_第23张图片

可以看到,通过合理地设置 momentum 系数,结合之前的梯度信息,我们可以让 loss 曲线更快收敛到极小值点。不过大部分情况下,我们通常会将 momentum 系数设置为 0.9。

5.3. PyTorch 中的常用优化器

5.3.1.optim.SGD

功能:随机梯度下降。

optim.SGD(
    params,
    lr=<object object>,
    momentum=0,
    dampening=0,
    weight_decay=0,
    nesterov=False
)

主要参数:

  • params:管理的参数组。
  • lr:初始学习率。
  • momentum:动量系数 \beta。
  • weight_decay:L2 正则化系数。
  • nesterov:是否采用 NAG。
  • NAG 参考文献:On the importance of initialization and momentum in deep learning

5.3.2.PyTorch 中的 10 种常用优化器

  • optim.SGD:随机梯度下降法。
  • optim.Adagrad:自适应学习率梯度下降法。
  • optim.RMSprop:Adagrad 的改进。
  • optim.Adadelta:Adagrad 的改进。
  • optim.Adam:RMSprop 结合 Momentum。
  • optim.Adamax:Adam 增加学习率上限。
  • optim.SparseAdam:稀疏版的 Adam。
  • optim.ASGD:随机平均梯度下降。
  • optim.Rprop:弹性反向传播。
  • optim.LBFGS:BFGS 的改进。

参考文献:

  • optim.SGD:On the importance of initialization and momentum in deep learning

  • optim.Adagrad:Adaptive subgradient methods for online learning and stochastic optimization

  • optim.RMSprop:RMSProp

  • optim.Adadelta:ADADELTA: An Adaptive Learning Rate Method

  • optim.Adam:Adam: A Method for Stochastic Optimization

  • optim.Adamax:Adam: A Method for Stochastic Optimization

  • optim.SparseAdam:稀疏版的 Adam。

  • optim.ASGD:Accelerating Stochastic Gradient Descent using Predictive Variance Reduction

  • optim.Rprop:RPROP-A Fast Adaptive Learning Algorithm

  • optim.LBFGS:BDGS 的改进。

5.4. 总结

本节中,我们学习了优化器 Optimizer 中的两个主要参数:学习率和 momentum。下节中,我们将学习关于学习率的调整策略。

你可能感兴趣的:(PyTorch,python,深度学习,pytorch)