前期回顾
Pytorch:简介、张量、简单操作和回归模型
Pytorch:数据读取机制(DataLoader与Dataset)
Pytorch:数据增强
Pytorch:模型创建(Module)、模型容器(Containers)、AlexNet构建
Pytorch:网络层介绍(卷积层、池化层、线性层、激活函数层)和多层感知机
一、权值初始化
在网络模型搭建完成之后,对网络中的权重进行恰当的初始化是非常重要的,深度模型有关数值稳定性的典型问题是衰减(vanishing)和爆炸(explosion),之后再来看看常用的权值初始化的方法。
1.1 梯度消失与梯度爆炸
先回顾一下线性层的概念图,如下所示:
然后我们给出梯度消失和梯度爆炸的概念解释:
梯度消失:如果导数(后面会解释这个导数的含义)小于1,那么随着网络层数的增加,梯度更新信息会朝着指数衰减的方式减少这就是梯度消失。梯度消失时,越靠近输入层的参数w越是几乎不动。
梯度爆炸:在反向传播过程中需要对激活函数进行求导,如果导数大于1,那么随着网络层数的增加,梯度更新将会朝着指数爆炸的方式增加。梯度爆炸时,越是靠近输入层的参数w变化越快。
二者问题问题都是因为网络太深,网络权值更新不稳定造成的。本质上是因为梯度反向传播中的连乘效应(小于1连续相乘多次)。下面就做一个详细的解释。
如上图所示,假设要算 W 2 W2 W2的梯度,则根据链式法有: H 2 = H 1 ∗ W 2 \mathrm{H}_2=\mathrm{H}_1*\mathrm{W}_2 H2=H1∗W2 Δ W 2 = ∂ L o s s ∂ W 2 = ∂ L o s s ∂ o u t ∗ ∂ o u t ∂ H 2 ∗ ∂ H 2 ∂ w 2 = ∂ Loss ∂ out ∗ ∂ out ∂ H 2 ∗ H 1 \begin{aligned} \Delta\text{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\text{Loss}}{ \partial\text{out }} * \frac { \partial\text{out}}{ \partial\text{H}_2}*\text{H}_1 \end{aligned} ΔW2=∂W2∂Loss=∂out∂Loss∗∂H2∂out∗∂w2∂H2=∂out ∂Loss∗∂H2∂out∗H1当神经网络的层数较多时,模型的数值稳定性容易变差。假设一个层数为 L L L的多层感知机的第 l l l层 H ( l ) \boldsymbol{H}^{(l)} H(l)的权重参数为 W ( l ) \boldsymbol{W}^{(l)} W(l),输出层 H ( L ) \boldsymbol{H}^{(L)} H(L)的权重参数为 W ( L ) \boldsymbol{W}^{(L)} W(L)。为了便于讨论,不考虑偏差参数,且设所有隐藏层的激活函数为恒等映射(identity mapping) ϕ ( x ) = x \phi(x) = x ϕ(x)=x。给定输入 X \boldsymbol{X} X,多层感知机的第 l l l层的输出 H ( l ) = X W ( 1 ) W ( 2 ) … W ( l ) \boldsymbol{H}^{(l)} = \boldsymbol{X} \boldsymbol{W}^{(1)} \boldsymbol{W}^{(2)} \ldots \boldsymbol{W}^{(l)} H(l)=XW(1)W(2)…W(l)。此时,如果层数 l l l较大, H ( l ) \boldsymbol{H}^{(l)} H(l)的计算可能会出现衰减或爆炸。
举个例子,假设输入和所有层的权重参数都是标量,如权重参数为0.2和5,多层感知机的第30层输出为输入 X \boldsymbol{X} X分别与 0. 2 30 ≈ 1 × 1 0 − 21 0.2^{30} \approx 1 \times 10^{-21} 0.230≈1×10−21(衰减)和 5 30 ≈ 9 × 1 0 20 5^{30} \approx 9 \times 10^{20} 530≈9×1020(爆炸)的乘积。类似地,当层数较多时,梯度的计算也更容易出现衰减或爆炸。
如果发生梯度消失或者爆炸, 就会导致模型无法训练,为了避免这个问题,我们可以想办法将网络的输出层控制在一定范围内,这样就可以使模型正常训练了,下面便来分析初始化取值对模型的影响,测试代码如下:
import os
import torch
import random
import numpy as np
import torch.nn as nn
# from tools.common_tools import set_seed
def set_seed(seed=1): # 设置随机种子
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed(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)
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
def initialize(self):
for m in self.modules():
if isinstance(m, nn.Linear):
nn.init.normal_(m.weight.data) # 用标准正态分布做初始化
layer_nums = 100
neural_nums = 256
batch_size = 16
net = MLP(neural_nums, layer_nums) # 创建类
net.initialize() # 模型初始化
inputs = torch.randn((batch_size, neural_nums)) # mean=0, std=1
output = net(inputs) # 将inputs传入网络
print(output)
运行测试代码后会发现,在33层和34层的时候,神经网络的输出就成了inf,甚至在35层的时候,神经网络的输出成了nan。根据上面的权重推导的公式,如果出现nan的话,在反向传播的时候,这些权重就不能够被更新,这就是所谓的梯度爆炸现象。
这是因为正向传播的时候,权重会影响到每一层的输出。 下面推导正向传播中每一层输出的方差是如何变化。首先给出统计学关于期望和方差的基本公式:
- E [ X Y ] = E [ X ] E [ Y ] E[XY] = E[X]E[Y] E[XY]=E[X]E[Y],如果X和Y相互独立
- D [ X ] = E [ X 2 ] − ( E [ X ] ) 2 D[X] = E[X^2]-(E[X])^2 D[X]=E[X2]−(E[X])2
-
D
[
X
+
Y
]
=
D
[
X
]
+
D
[
Y
]
D[X+Y] = D[X]+D[Y]
D[X+Y]=D[X]+D[Y],如果X和Y相互独立
那么有 D ( X Y ) = E [ X Y − E ( X Y ) ] 2 = E X 2 Y 2 − 2 X Y E ( X Y ) + E 2 ( X Y ) = E ( X 2 ) E ( Y 2 ) − 2 E 2 ( X ) E 2 ( Y ) + E 2 ( X ) E 2 ( Y ) = E ( X 2 ) E ( Y 2 ) − E 2 ( X ) E 2 ( Y ) = D ( X ) D ( Y ) + D ( X ) [ E ( Y ) ] 2 + D ( Y ) [ E ( X ) ] 2 \begin{aligned} &\mathrm{D}(\text{X Y }) \\ &=\mathrm{E}[\mathrm{X}\mathrm{Y}-\mathrm{E}(\mathrm{X}\mathrm{Y})]^2 \\ &=\mathrm{EX}^2\mathrm{Y}^2-2\mathrm{X}\mathrm{Y}\mathrm{E}(\mathrm{X}\mathrm{Y})+\mathrm{E}^2(\mathrm{X}\mathrm{Y}) \\ &\mathrm{=E(X^2)E(Y^2)-2E^2(X)E^2(Y)+E^2(X)E^2(Y)} \\ &\mathrm{=E(X^2)E(Y^2)-E^2(X)E^2(Y)} \\ &\mathrm{=D(X)D(Y)+D(X)[E(Y)]^2+D(Y)[E(X)]^2} \end{aligned} D(X Y )=E[XY−E(XY)]2=EX2Y2−2XYE(XY)+E2(XY)=E(X2)E(Y2)−2E2(X)E2(Y)+E2(X)E2(Y)=E(X2)E(Y2)−E2(X)E2(Y)=D(X)D(Y)+D(X)[E(Y)]2+D(Y)[E(X)]2如果 E [ X ] = 0 E[X]=0 E[X]=0, E [ Y ] = 0 E[Y]=0 E[Y]=0,则有 D [ X Y ] = D [ X ] D [ Y ] D[XY] = D[X]D[Y] D[XY]=D[X]D[Y]
仍然以下图为例,看看每层之间方差的关系:
第一个隐层的方差如下计算:
H
11
=
∑
i
=
0
n
X
i
∗
W
1
i
D
(
H
11
)
=
∑
i
=
0
n
D
(
X
i
)
∗
D
(
W
11
)
=
n
∗
(
1
∗
1
)
=
n
s
t
d
(
H
11
)
=
D
(
H
11
)
=
n
\begin{aligned} &\mathrm{H_{11}=\sum_{i=0}^nX_i*W_{1i}} \\ \mathrm{D}\left(\mathrm{H}_{11}\right)&=\sum_{\mathrm{i}=0}^\mathrm{n}\mathrm{D}\left(\mathrm{X}_{\mathrm{i}}\right)*\mathrm{D}\left(\mathrm{W}_{11}\right)\\ &=\text{n}*(1*1) \\ &=\text{n} \\ \mathrm{std~(H_{11})}&=\sqrt{\mathrm{D}\left(\mathrm{H}_{11}\right)}=\sqrt{\mathrm{n}} \end{aligned}
D(H11)std (H11)H11=i=0∑nXi∗W1i=i=0∑nD(Xi)∗D(W11)=n∗(1∗1)=n=D(H11)=n于是由于输入数据和权重都是均值为0,方差为1的标准正态。 由上面公式的推导可以知道,经过一个网络层方差会扩大
n
n
n倍。如果深度神经网络的层数过多,那么这方差将会以指数级别增长,于是才到了35层就出现了nan的情况。
搞清楚上述原因后呢,我们只需要让网络层的输出方差保持尺度不变就可以避免发生取值为nan的情况了。由上图可以知道,每一层的输出方差和每一层神经元个数以及前一层输出方差和本层权重的方差有关,最简单的一种方式就是控制每一层输出的方差都为1,也就是人为地给每一个层输出方差都加上一个系数。首先, 每一层神经元个数没法变, 而前一层输出方差是1又涉及到了上上层的方差, 所以最简单的就是变动权重的方差,即: D ( H 1 ) = n × D ( X ) × D ( W ) = 1 D ( W ) = 1 n = > s t d ( W ) = 1 n \begin{aligned}&\mathrm{D(H_1)=n\times D(X)\times D(W)=1}\\&\mathrm{D(W)=\frac1n~=>std(W)=\sqrt{\frac1n}}\end{aligned} D(H1)=n×D(X)×D(W)=1D(W)=n1 =>std(W)=n1这样,如果每层权重在初始化的时设置为 s t d ( W ) = 1 n std(W)=\sqrt{\frac{1}{n}} std(W)=n1,那么每一层的输出方差都为1,这样便不会发生nan的情况了。要实现这个效果,只需要修改上述测试代码中初始化的一行:
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)) # 权重方差改成了np.sqrt(1/self.neural_num))
所以只要采用恰当的权值初始化方法,就可以实现多层神经网络的输出值的尺度维持在一定范围内, 这样在反向传播的时候,就有利于缓解梯度消失或者爆炸现象的发生,测试结果如下:
1.2 Xavier初始化
方差一致性:保持数据尺度范围维持在恰当范围, 通常方差为1。 然后上面只探讨了线性层的情况下的控制方差变化尺度的方式,但如果有了激活函数应该怎么对权重进行初始化呢?其实这个问题已经有学者回答了。
2010年Xavier发表了一篇文章(Understanding the difficuty of training deep feedforward neural networks),详细探讨了如果有激活函数的时候,如何进行权重初始化, 文章中也是运用的方差一致性原则, 但考虑的是饱和激活函数, 如sigmoid和tanh。 文章中的公式推导如下:
n
i
∗
D
(
W
)
=
1
n
i
+
1
∗
D
(
W
)
=
1
⇒
D
(
W
)
=
2
n
i
+
n
i
+
1
\begin{aligned} &\boldsymbol{n_\mathrm{i}}*\mathrm{D}(\mathcal{W})=1 \\ &n_{\mathrm{i}+1}*\mathrm{D}(\mathrm{W})=1 \\ &\mathrm{\Rightarrow D(W)=\frac2{n_i+n_{i+1}}} \end{aligned}
ni∗D(W)=1ni+1∗D(W)=1⇒D(W)=ni+ni+12其中这里的
n
i
n_{i}
ni,
n
i
+
1
n_{i+1}
ni+1分别指的输入层和输出层神经元个数。通常情况下,Xavier采用均匀分布对权重进行初始化,于是可以利用均匀分布的方差公式来推导一下均匀分布的上限和下限:
W
∼
U
[
−
a
,
a
]
D
(
W
)
=
(
−
a
−
a
)
2
12
=
(
2
a
)
2
12
=
a
2
3
\begin{aligned}&\mathrm{W}\sim\boldsymbol{U}[-\mathrm{a},\mathrm{a}]\\&\mathrm{D(W)}=\frac{(-\mathrm{a}-\mathrm{a})^2}{12}=\frac{(2\mathrm{a})^2}{12}=\frac{\mathrm{a}^2}3\end{aligned}
W∼U[−a,a]D(W)=12(−a−a)2=12(2a)2=3a2令两个D ( W ) D(W)D(W)相等就会得到:
2
n
i
+
n
i
+
1
=
a
2
3
⇒
a
=
6
n
i
+
n
i
+
1
⇒
W
∼
U
[
−
6
n
i
+
n
i
+
1
,
6
n
i
+
n
i
+
1
]
\begin{aligned}&\mathrm{\frac2{n_i+n_{i+1}}~=\frac{a^2}3\Rightarrow a=\frac{\sqrt{6}}{\sqrt{n_i+n_{i+1}}}}\\&\mathrm{\Rightarrow W~\sim U\left[-\frac{\sqrt{6}}{\sqrt{n_i+n_{i+1}}}~,\frac{\sqrt{6}}{\sqrt{n_i+n_{i+1}}}~\right]}\end{aligned}
ni+ni+12 =3a2⇒a=ni+ni+16⇒W ∼U[−ni+ni+16 ,ni+ni+16 ]上面便是Xavier初始化方法简要推导, 我们只需要修改测试代码中参数初始化几行即可(调用nn.init.xavier_uniform_
):
def initialize(self):
for m in self.modules():
if isinstance(m, nn.Linear):
# Xavier初始化权重
tanh_gain = nn.init.calculate_gain('tanh')
nn.init.xavier_uniform_(m.weight.data, gain=tanh_gain)
代码里面用到一个函数nn.init.calculate_gain(nonlinearity,param=**None**)
作用:是计算激活函数的方差变化尺度,用人话说就是输入数据的方差除以经过激活函数之后的输出数据的方差。
参数 | 作用 |
---|---|
nonlinearity | 表示激活函数的名称,如tanh。 |
param | 表示激活函数的参数,如Leaky ReLU的negative_slop。 |
完整的测试代码如下,不要忘了Xavier初始化方法的适用激活函数。
import os
import torch
import random
import numpy as np
import torch.nn as nn
# from tools.common_tools import set_seed
def set_seed(seed=1): # 设置随机种子
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed(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)
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
def initialize(self):
for m in self.modules():
if isinstance(m, nn.Linear):
# Xavier初始化权重
tanh_gain = nn.init.calculate_gain('tanh')
nn.init.xavier_uniform_(m.weight.data, gain=tanh_gain)
layer_nums = 100
neural_nums = 256
batch_size = 16
net = MLP(neural_nums, layer_nums)
net.initialize()
inputs = torch.randn((batch_size, neural_nums)) # normal: mean=0, std=1
Xavier初始化测试代码运行结果如下:
1.3 Kaiming初始化
2012年AlexNet出现之后,非饱和函数relu也用到了神经网络中,而Xavier初始化对于非饱和函数的实现效果却不好,读者可以将Xavier初始化测试代码中的激活函数改为tanh后自行检验。为了应对非饱和函数带来的困难,学者在2015年的Delving Deep into Rectifiers: Surpassing Human-Level Performance on ImageNet Classification提出了Kaiming初始化的方法。
该方法仍然考虑了方差一致性原则,针对的激活函数是非饱和函数ReLU及其变种。类似于Xavier,经过公示推导,最后的权值标准差是这样的:
D
(
W
)
=
2
n
i
D
(
W
)
=
2
(
1
+
a
2
)
+
n
i
s
t
d
(
W
)
=
2
(
1
+
a
2
)
∗
n
i
\begin{aligned} &\mathbf{D}(\text{W })=\frac2{\boldsymbol{n_\mathrm{i}}} \\ &\mathbf{D}(\text{W})=\frac2{(1+\mathbf{a}^2)+\boldsymbol{n}_\mathrm{i}} \\ &\mathrm{std(W)}=\sqrt{\frac2{(1+a^2)*n_i}} \end{aligned}
D(W )=ni2D(W)=(1+a2)+ni2std(W)=(1+a2)∗ni2类似于Xavier初始化方法的调用, 我们只需要修改测试代码中参数初始化几行即可(调用nn.init.kaiming_normal_
):
def initialize(self):
for m in self.modules():
if isinstance(m, nn.Linear):
# Kaiming初始化权重
nn.init.kaiming_normal_(m.weight.data)
运行结果如下:
所以从上面的学习中,我们知道如果避免梯度爆炸或者梯度消失的现象,就得控制输出层的值的范围尺度,这要求我们采取合理的权重初始化方法,有了合理的权重初始化方法就再也不用害怕nan了。
1.4 十种权重初始化方法
除了上面的两种初始化方法之外,Pytorch里面提供了很多权重初始化的方法,可以分为下面的四大类:
- 针对饱和激活函数(如sigmoid, tanh):Xavier均匀分布, Xavier正态分布
- 针对非饱和激活函数(relu及变种):Kaiming均匀分布, Kaiming正态分布
- 三个常用的分布初始化方法:均匀分布,正态分布,常数分布
- 三个特殊的矩阵初始化方法:正交矩阵初始化,单位矩阵初始化,稀疏矩阵初始化
详情可以查阅官Pytorch方文档
二、损失函数
2.1 损失函数初步介绍
损失函数: 衡量模型输出与真实标签的差异。而当我们谈及损失函数的时候,往往会有三个概念: 损失函数, 代价函数, 目标函数。 实际上,这三者并不是同一个东西,下面简要了解下:
-
Loss Function:计算一个样本的模型输出与真实标签的差异 L o s s = f ( y ^ , y ) Loss=f(\hat{y},y) Loss=f(y^,y)
-
Cost Function:计算整个样本集的模型输出与真实标签的差异,是所有样本Loss function的平均值。即 c o s t = 1 N ∑ i = 1 N f ( y ^ , y ) cost=\frac{1}{N}\sum_{i=1}^{N}f(\hat{y},y) cost=N1∑i=1Nf(y^,y)
-
Objective Function:就是Cost Function加上正则项,即Obj=Cost+Regularization
(实际上呢,我们一般在衡量模型输出和真实标签的差异的时候,往往都直接说成损失函数,没啥区别,了解一下就好不必深究~。)
关于损失函数的初始化和使用方法的运行机制,感兴趣的读者可以从参考博客中进一步查阅。我呢就做个简单的概括吧,实际上损失函数也是一个Module,那么初始化后就有Module的8个属性字典,使用的方法依然是定义在forward函数中。还记得我们在Tensor张量介绍中的Softmax回归模型吗?在那一节中我就简要地说到了交叉熵函数,但当时是自定义实现的,下面我们就详细的学习pytorch中自带的nn.CrossEntropyLoss
, 这个函数在分类任务中很常用, 所以得重点掌握哦~
2.2 交叉熵损失CrossEntropyLoss
功能:nn.LogSoftmax()
与nn.NULLLoss()
结合,进行交叉熵计算
参数 | 作用 |
---|---|
weight | 各类别的loss设置权限 |
ignore_index | 忽略某个类别 |
reduction | 计算模式,可为none/sum/mean。none:逐个元素计算;sum :返回所有元素求和,返回标量;mean :加权平均,返回标量(默认) |
实际上这里的交叉熵损失函数并不是公式意义上的交叉熵损失函数,而是有一些区别。普通的交叉熵损失函数如下:
H
(
P
,
Q
)
=
−
∑
i
=
1
N
P
(
x
i
)
log
Q
(
x
i
)
\mathrm{H}(\boldsymbol{P},\boldsymbol{Q})=-\sum_{\mathrm{i}=1}^\mathrm{N}\boldsymbol{P}\left(\boldsymbol{x}_\mathrm{i}\right)\log\boldsymbol{Q}\left(\boldsymbol{x}_\mathrm{i}\right)
H(P,Q)=−i=1∑NP(xi)logQ(xi)其中
P
P
P表示数据的原始分布,
Q
Q
Q表示模型输出的分布,而交叉熵损失用来衡量两个分布之间的差异程度:交叉熵越低则两个分布越近。nn.CrossEntropyLoss
一个不同就是先用nn.LogSoftmax()
把模型的输出值归一化成了概率分布的形式,然后是单个样本的输出,其中并且没有求和符号。
可能有读者想知道为什么交叉熵可以衡量两个分布的差异,交叉熵究竟是个什么东西呢? 为了回答这样的问题,就得溯源到一些基本的概念了:
熵用来描述事件的不确定性,事件 P P P发生的不确定程度记作 H ( P ) H(P) H(P)。一个事物不确定性越大,熵就越大。熵的公式如下: H ( P ) = E x ∼ p [ I ( x ) ] = − ∑ i N P ( x i ) l o g P ( x i ) \mathrm{H(P)=E_{x\sim p}\left[I(x)\right]=-\sum_{i}^{N}P\left(x_{i}\right)logP\left(x_{i}\right)} H(P)=Ex∼p[I(x)]=−i∑NP(xi)logP(xi)当然这只是离散形式,类似地也能够写出连续形式。
其中 I ( X ) I(X) I(X)被称作自信息,自信息的公式定义如下:
原来这个熵是自信息的一个期望, 那么就得先看看自信息是什么东西?下面是自信息的公式:
I ( x ) = − l o g [ p ( x ) ] I(x)=−log[p(x)] I(x)=−log[p(x)]也就是说自信息就是一个事件发生的概率,然后取对数再取反。 根据这个定义式我们可以如果知道一个事件发生的概率越大,那么自信息就会少。所有事件发生的概率都很大,那么熵就会小,则事件的不确定性就小。 可能有点抽象,举个例子,比如说我呢中奖的概率是90%,那么我中奖的概率就是非常高的,于是关于我中奖这件事情的不确定性程度就很低;但反之,我不中奖的概率是10%,于是我不中奖这个事件的不确定性就很高。一个不确定性很高的事件的对立事件是不确定性很低的事件,那么二者的和什么时候才是最小的呢?其实这个我们我们可以用凸优化的方法来回答,利用拉格朗日数乘法计算出极值点后,可以知道当每个事件发生的概率相等时,所有事件的不确定性和最大,如下图所示:(这是只有两个事件的情况,且二者为对立事件,横轴为事件 P P P发生的概率)
这个图像在二分类模型中经常会碰到
下一个要说到的概念呢是相对熵,相对熵又称为KL散度,用来衡量两个分布之间的差异。先来看看公式的定义: D K L ( P , Q ) = E x ∼ p [ l o g P ( x ) Q ( x ) ] \boldsymbol{D}_{\mathrm{KL}}\left(\boldsymbol{P},\boldsymbol{Q}\right)=\boldsymbol{E}_{\boldsymbol{x}\sim\boldsymbol{p}}\left[\mathrm{log}\frac{\boldsymbol{P}(\boldsymbol{x})}{\boldsymbol{Q}(\boldsymbol{x})}\right] DKL(P,Q)=Ex∼p[logQ(x)P(x)]可以发现是KL散度不同于距离函数,因为KL散度不具有对称性。这是因为 P P P是数据的真实分布, Q Q Q是模型输出的分布,公式中是用Q的分布去逼近P的分布,所以这不具备对称性。
而交叉熵可以用上面的概念定义,即交叉熵=信息熵+相对熵, 公式如下: H ( P , Q ) = − ∑ i = 1 N P ( x i ) log Q ( x i ) \mathrm{H}(\boldsymbol{P},\boldsymbol{Q})=-\sum_{\mathrm{i}=1}^\mathrm{N}\boldsymbol{P}\left(\boldsymbol{x_\mathrm{i}}\right)\log\boldsymbol{Q}\left(\boldsymbol{x_\mathrm{i}}\right) H(P,Q)=−i=1∑NP(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 ) \begin{aligned} \boldsymbol{D_\mathrm{KL}}\left(\boldsymbol{P},\boldsymbol{Q}\right)& =\boldsymbol{E_\mathrm{x\sim p}}\left[\log\frac{\boldsymbol{P}(\boldsymbol{x})}{\mathrm{Q}(\boldsymbol{x})}\right] \\ &=\boldsymbol{E}_{\boldsymbol{x}\sim\boldsymbol{p}}[\log\boldsymbol{P}(\boldsymbol{x})-\log\boldsymbol{Q}(\boldsymbol{x})] \\ &=\sum_{\mathrm{i}=1}^\mathrm{N}\boldsymbol{P}\left(\boldsymbol{x}_\mathrm{i}\right)\left[\log\boldsymbol{P}\left(\boldsymbol{x}_\mathrm{i}\right)-\log\boldsymbol{Q}\left(\boldsymbol{x}_\mathrm{i}\right)\right] \\ &=\sum_{\mathrm{i}=1}^\mathrm{N}\boldsymbol{P}\left(\boldsymbol{x}_\mathrm{i}\right)\log\boldsymbol{P}\left(\boldsymbol{x}_\mathrm{i}\right)-\sum_{\mathrm{i}=1}^\mathrm{N}\boldsymbol{P}\left(\boldsymbol{x}_\mathrm{i}\right)\log\boldsymbol{Q}\left(\boldsymbol{x}_\mathrm{i}\right) \end{aligned} DKL(P,Q)=Ex∼p[logQ(x)P(x)]=Ex∼p[logP(x)−logQ(x)]=i=1∑NP(xi)[logP(xi)−logQ(xi)]=i=1∑NP(xi)logP(xi)−i=1∑NP(xi)logQ(xi)移项即证。
于是根据上面补充的信息以及推导过程,我们有:
H
(
P
,
Q
)
=
D
K
L
(
P
,
Q
)
+
H
(
P
)
H ( P , Q ) = D _{K L} ( P , Q ) + H ( P )
H(P,Q)=DKL(P,Q)+H(P)其中
P
P
P是数据的真实分布,
Q
Q
Q是模型输出的分布。在机器学习中,由于训练集是给定的,于是
H
(
P
)
H(P)
H(P)就是已经知道的常数,所以最小化交叉熵等价于最小化相对熵。
在机器学习模型中,我们最小化交叉熵,其实就是最小化相对熵,因为我们训练集取出来之后就是固定的了,熵就是一个常数。按照交叉熵的定义式,交叉熵损失函数的取值范围是 [ 0 , + ∞ ] [0,+\infty] [0,+∞], 它的取值范围受到模型输出与实际输出之间的误差程度的影响。
所以如果对交叉熵使用softmax,就可以将一个输出值(取值为
[
0
,
+
∞
]
[0,+\infty]
[0,+∞])转换到概率取值的一个范围。下面我们来看看nn.CrossEntropyLoss
的交叉熵是如何定义的。
l
o
s
s
(
x
,
c
l
a
s
s
)
=
−
log
(
exp
(
x
[
c
l
a
s
s
]
)
∑
j
exp
(
x
[
j
]
)
)
=
−
x
[
c
l
a
s
s
]
+
log
(
∑
j
exp
(
x
[
j
]
)
)
\mathrm{loss(x,~class~)=-\log\left(\frac{\exp(x[~class~])}{\sum_j\exp(x[j])}\right)=-x[~class~]+\log\left(\sum_j\exp(x[j])\right)}
loss(x, class )=−log(∑jexp(x[j])exp(x[ class ]))=−x[ class ]+log
j∑exp(x[j])
可以看到nn.CrossEntropyLoss
首先执行了一个
s
o
f
t
m
a
x
\mathrm{softmax}
softmax运算,把某个神经元的输出归一化为概率取值后,然后再对这个概率取值去了对数后又添加上了负号一下。其中
x
\mathrm{x}
x是样本,
c
l
a
s
s
\mathrm{class}
class就是某一个类别,
x
[
c
l
a
s
s
]
\mathrm{x[class]}
x[class]表示样本x在类别[class]中的概率。我们可以对比一下普通的交叉熵公式:
H
(
P
,
Q
)
=
−
∑
i
=
1
N
P
(
x
i
)
log
Q
(
x
i
)
\mathrm{H}(\boldsymbol{P},\boldsymbol{Q})=-\sum_{\mathrm{i}=1}^\mathrm{N}\boldsymbol{P}\left(\boldsymbol{x}_\mathrm{i}\right)\log\boldsymbol{Q}\left(\boldsymbol{x}_\mathrm{i}\right)
H(P,Q)=−i=1∑NP(xi)logQ(xi)由于样本x必然是某个类别,不妨设
P
(
x
i
)
=
1
\boldsymbol{P}\left(\boldsymbol{x}_\mathrm{i}\right)=1
P(xi)=1,则
P
(
x
j
≠
i
)
=
0
\boldsymbol{P}\left(\boldsymbol{x}_\mathrm{j \ne i}\right)=0
P(xj=i)=0,那么就可以去掉求和符号。在交叉熵损失函数nn.CrossEntropyLoss
中,使用nn.LogSoftmax()
将概率归一化,然后添加了一个负号(nn.NLLoss
用于实现负对数似然函数里面的负号功能),于是便得到了交叉熵损失函数
l
o
s
s
(
x
,
c
l
a
s
s
)
\mathrm{loss(x,class)}
loss(x,class)
下面就回顾一下交叉熵损失函数表格里参数的作用, 第一个参数weight
用于各类别的loss设置权值, 主要用于类别不均匀的情况,加上weight后损失函数变成这样:
loss(x, class )= weight [ class ]
(
−
x[ class ]
+
log
(
∑
j
exp
(
x
[
j
]
)
)
)
\text{loss(x, class )= weight [ class ]}\left(-\text{x[ class ]}+\log\left(\sum_\mathrm{j}\exp(\mathrm{x[j]})\right)\right)
loss(x, class )= weight [ class ]
−x[ class ]+log
j∑exp(x[j])
于是我们可以更改权值的分配使得模型关注不同的类别。
第二个参数ignore_index
表示某个类别不去计算loss。而关于第三个参数reduction
在表格中已有,不再详述,主要还是从如下测试代码中看看区别:
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
# 构建虚拟数据
inputs = torch.tensor([[1, 2], [1, 3], [1, 3]], dtype=torch.float) # 这里就是模型预测的输出, 这里是两个类,可以看到模型输出是数值,我们得softmax一下转成分布
target = torch.tensor([0, 1, 1], dtype=torch.long) # 标签。这里的类型必须是long, 两个类0和1
# ----------------------------------- CrossEntropy loss: reduction -----------------------------------
# 三种模式损失函数
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)
import torch
# --------------------------------- compute by hand
# 这一部分是自己实现一边交叉熵损失函数的计算
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)
运行结果如下:
可以看到none模式下是输出三个损失, sum下是三个损失求和,mean下是三个损失求平均。这里还要注意一下这里的target, 这是给出每个样本属于哪一个类,类型必须是torch.long。下面实现一下带权值的交叉熵损失函数,只需要将定义损失函数部分修改如下:
# def loss function
weights = torch.tensor([1, 2], 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')
运行结果:
下面看看这个加权后是怎么在mean模式下计算损失。首先发现,给类别加上权值之后,对应样本的损失就会相应的加倍。三个样本中,第一个权值为1, 后两个权值为2, 所以分母应该是5而不是3。所以mean模式下求平均不是除以样本的个数,而是样本所占的权值的总份数。
nn.CrossEntropyLoss
2.3 剩余的17种损失函数介绍
(1)nn.NLLLoss
功能:实现负对数似然函数中的负号功能
参数 | 作用 |
---|---|
weight | 各类别的loss设置权限 |
ignore_index | 忽略某个类别 |
reduction | 计算模式,可为none/sum/mean。none:逐个元素计算;sum :返回所有元素求和,返回标量;mean :加权平均,返回标量(默认) |
测试代码:
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
import random
inputs = torch.tensor([[1, 2], [1, 3], [1, 3]], dtype=torch.float)
target = torch.tensor([0, 1, 1], dtype=torch.long)
network = nn.LogSoftmax()
out = network(inputs)
print(out)
NLloss = nn.NLLLoss(reduction='none')
output = NLloss(network(inputs), target)
print(output)
(2)nn.BCELoss
功能:二分类交叉熵
参数 | 作用 |
---|---|
weight | 各类别的loss设置权限 |
ignore_index | 忽略某个类别 |
reduction | 计算模式,可为none/sum/mean。none:逐个元素计算;sum :返回所有元素求和,返回标量;mean :加权平均,返回标量(默认) |
注意事项:输入值取值在[0,1],需要符合概率取值
测试代码:
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
import random
# ----------------------------------- 2 NLLLoss -----------------------------------
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)
weights = torch.tensor([1, 1], dtype=torch.float)
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)
运行结果如下:
首先注意到的就是target的不同, 第一是类型变成了float,第二是每个样本属于哪一类的时候被写成独热编码的形式,这是因为每个神经元一一对应的去计算loss,而不是一个整的神经元向量去计算loss。根据inputs,这里是两个神经元的。
(3)nn.BCEWithLogists Loss
在BCELoss中我们会发现,如果输入数据不在[0,1]区间内会报错,针对这一问题提出了nn.BCEWithLogists Loss
功能:结合Sigmoid与二分类交叉熵
参数 | 作用 |
---|---|
pos_weight | 正样本的权值 |
weight | 各类别的loss设置权限 |
ignore_index | 忽略某个类别 |
reduction | 计算模式,可为none/sum/mean。none:逐个元素计算;sum :返回所有元素求和,返回标量;mean :加权平均,返回标量(默认) |
参数多了一个pow_weight
,该参数是平衡正负样本的权值用的。比如正样本有100个,负样本有300个,那么这个数可以设置为3。
注意事项:网络最后不加sigmoid函数
测试代码:
import torch
import torch.nn as nn
import torch.nn.functional as F
import matplotlib.pyplot as plt
import numpy as np
import random
def set_seed(seed=1): # 设置随机种子
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
# ----------------------------------- BCE with Logis Loss -----------------------------------
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)
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)
(4)nn.L1Loss
功能:计算input与target之差的绝对值
参数 | 作用 |
---|---|
reduction | 计算模式,可为none/sum/mean。none:逐个元素计算;sum :返回所有元素求和,返回标量;mean :加权平均,返回标量(默认) |
import torch
import torch.nn as nn
import torch.nn.functional as F
import matplotlib.pyplot as plt
import numpy as np
import random
def set_seed(seed=1): # 设置随机种子
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
# ------------------------------------------------- L1 loss ----------------------------------------------
inputs = torch.ones((2, 2))
target = torch.ones((2, 2)) * 3
loss_f = nn.L1Loss(reduction='none')
loss = loss_f(inputs, target)
print("input:{}\ntarget:{}\nL1 loss:{}".format(inputs, target, loss))
(5)nn.MSELoss
功能:计算input与target之差的平方
参数 | 作用 |
---|---|
reduction | 计算模式,可为none/sum/mean。none:逐个元素计算;sum :返回所有元素求和,返回标量;mean :加权平均,返回标量(默认) |
import torch
import torch.nn as nn
import torch.nn.functional as F
import matplotlib.pyplot as plt
import numpy as np
import random
def set_seed(seed=1): # 设置随机种子
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
# ------------------------------------------------- MSE loss ----------------------------------------------
inputs = torch.ones((2, 2))
target = torch.ones((2, 2)) * 3
loss_f_mse = nn.MSELoss(reduction='none')
loss_mse = loss_f_mse(inputs, target)
print("MSE loss:{}".format(loss_mse))
(6)nn.SmoothL1Loss
功能:创建一个标准,如果绝对元素误差低于β,则使用平方项,否则使用L1项。它对异常值的敏感度低于torch.nn.MSELoss,并且在某些情况下可以防止爆炸梯度
参数 | 作用 |
---|---|
reduction | 计算模式,可为none/sum/mean。 |
none | 逐个元素计算; |
sum | 返回所有元素求和,返回标量; |
mean | 加权平均,返回标量(默认) |
测试代码:
import torch
import torch.nn as nn
import torch.nn.functional as F
import matplotlib.pyplot as plt
import numpy as np
import random
def set_seed(seed=1): # 设置随机种子
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
# ------------------------------------------------- Smooth L1 loss ----------------------------------------------
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()
采用这种平滑的损失函数可以减轻离群点带来的影响
(7)nn.PoissonNLLLoss
功能:泊松分布的负对数似然损失函数, 分类里面如果发现数据的类别服从泊松分布,可以使用这个损失函数
参数 | 作用 |
---|---|
log_input | 输入是否为对数形式,决定计算公式 |
full | 计算所有loss,默认为False |
eps | 修正项,避免log(input)为nan |
测试代码:
import torch
import torch.nn as nn
import torch.nn.functional as F
import matplotlib.pyplot as plt
import numpy as np
import random
def set_seed(seed=1): # 设置随机种子
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
# ------------------------------------------------- Poisson NLL loss ----------------------------------------------
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))
(8)nn.KLDivLoss
功能:计算KLD(divergence),KL散度,相对熵
参数 | 作用 |
---|---|
reduction | 计算模式,可为none/sum/mean/batchmean |
batchmean | batchsize维度求平均值 |
none | 逐个元素计算 |
sum | 返回所有元素求和,返回标量 |
mean | 加权平均,返回标量(默认) |
注意事项:需提前将输入计算log-probabilities,如通过nn.logsoftmax()
测试代码:
import torch
import torch.nn as nn
import torch.nn.functional as F
import matplotlib.pyplot as plt
import numpy as np
import random
def set_seed(seed=1): # 设置随机种子
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
# ------------------------------------------------- KL Divergence loss ----------------------------------------------
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))
(9)nn.MarginRankingLoss
功能:计算两个向量之间的相似度,用于排序任务
特别说明:该方法计算两组数据之间的差异,返回一个n*n的loss矩阵,类似于相关性矩阵那种。
参数 | 作用 |
---|---|
margin | 边界值,x1与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 ) \mathrm{loss(x,y)=max(0,-y*(x1-x2)+margin)} loss(x,y)=max(0,−y∗(x1−x2)+margin)y = 1时,希望x1比x2大,当x1>x2时,不产生loss;y = -1时,希望x2比x1大,当x2>x1时,不产生loss
测试代码:
import torch
import torch.nn as nn
import torch.nn.functional as F
import matplotlib.pyplot as plt
import numpy as np
import random
def set_seed(seed=1): # 设置随机种子
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
# ---------------------------------------------- 10 Margin Ranking 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)
(10)nn.MultiLabelMarginLoss
功能:多标签边界损失函数, 这是一个多标签分类,就是一个样本可能属于多个类,和多分类任务还不一样。(多标签问题)
计算公式如下:
l
o
s
s
(
x
,
y
)
=
∑
i
j
max
(
0
,
1
−
(
x
[
y
[
j
]
]
−
x
[
i
]
)
)
x
⋅
s
i
z
e
(
0
)
\mathrm{loss(x,y)=\sum_{ij}\frac{\max(0,1-(x[y[j]]-x[i]))}{x\cdot size(0)}}
loss(x,y)=ij∑x⋅size(0)max(0,1−(x[y[j]]−x[i]))举例:四分类任务,样本x属于0类和3类,标签:[0,3,-1,-1],不是[1,0,0,1]
参数 | 作用 |
---|---|
reduction | 计算模式,可为none/sum/mean |
测试代码:
import torch
import torch.nn as nn
import torch.nn.functional as F
import matplotlib.pyplot as plt
import numpy as np
import random
def set_seed(seed=1): # 设置随机种子
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
# ---------------------------------------------- Multi Label Margin Loss -----------------------------------------
x = torch.tensor([[0.1, 0.2, 0.4, 0.8]])
y = torch.tensor([[0, 3, -1, -1]], dtype=torch.long)
loss_f = nn.MultiLabelMarginLoss(reduction='none')
loss = loss_f(x, y)
print(loss)
(11)nn.SoftMarginLoss
功能:计算二分类的logistic损失
测试代码:
import torch
import torch.nn as nn
import torch.nn.functional as F
import matplotlib.pyplot as plt
import numpy as np
import random
def set_seed(seed=1): # 设置随机种子
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
# ---------------------------------------------- SoftMargin Loss -----------------------------------------
inputs = torch.tensor([[0.3, 0.7], [0.5, 0.5]])
target = torch.tensor([[-1, 1], [1, -1]], dtype=torch.float)
loss_f = nn.SoftMarginLoss(reduction='none')
loss = loss_f(inputs, target)
print("SoftMargin: ", loss)
(12)nn.MultiLabelSortMarginLoss
功能:SoftMarginLoss多标签版本
参数 | 作用 |
---|---|
weight | 各类别的loss设置权值 |
reduction | 计算模式,可为none/sum/mean |
测试代码:
import torch
import torch.nn as nn
import torch.nn.functional as F
import matplotlib.pyplot as plt
import numpy as np
import random
def set_seed(seed=1): # 设置随机种子
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
# ---------------------------------------------- MultiLabel SoftMargin Loss -----------------------------------------
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)
(13)nn.MultiMarginLoss(hingLoss)
功能:计算多分类的折页损失
计算公式如下:
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)=\frac{\sum_i\max(0,margin-x[y]+x[i]))^p}{x\cdot size(0)}}
loss(x,y)=x⋅size(0)∑imax(0,margin−x[y]+x[i]))p
参数 | 作用 |
---|---|
p | 可选1或2 |
weight | 各类别的loss设置权值 |
margin | 边界值 |
reduction | 计算模式,可为none/sum/mean |
测试代码:
import torch
import torch.nn as nn
import torch.nn.functional as F
import matplotlib.pyplot as plt
import numpy as np
import random
def set_seed(seed=1): # 设置随机种子
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
# ---------------------------------------------- Multi Margin Loss -----------------------------------------
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)
(14)nn.TripletMarginLoss
功能:计算三元组损失,人脸验证中常用
参数 | 作用 |
---|---|
p | 范数的阶,默认为2 |
margin | 边界值 |
reduction | 计算模式,可为none/sum/mean |
计算公式如下: L ( a , p , n ) = m a x { d ( a i , p i ) − d ( a i , n i ) + m a r g i n , 0 } \mathrm{L(a,p,n)=max\left\{d\left(a_i,p_i\right)-d\left(a_i,n_i\right)+margin,0\right\}} L(a,p,n)=max{d(ai,pi)−d(ai,ni)+margin,0}简单来说三元组损失在做这个事情, 我们在做人脸识别训练模型的时候,往往需要把训练集做成三元组(A, P, N), A和P是同一个人, A和N不是同一个, 然后训练模型。
想让模型把A和P看成一样的,也就是争取让A和P之间的距离小,而A和N之间的距离大,那么模型就能够进行人脸识别任务了。
测试代码:
import torch
import torch.nn as nn
import torch.nn.functional as F
import matplotlib.pyplot as plt
import numpy as np
import random
def set_seed(seed=1): # 设置随机种子
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
# ---------------------------------------------- Triplet Margin Loss -----------------------------------------
anchor = torch.tensor([[1.]])
pos = torch.tensor([[2.]])
neg = torch.tensor([[0.5]])
loss_f = nn.TripletMarginLoss(margin=1.0, p=1)
loss = loss_f(anchor, pos, neg)
print("Triplet Margin Loss", loss)
(15)nn.HingeEmbeddingLoss
功能:计算两个输入的相似性,常用于非线性embedding和半监督学习
参数 | 作用 |
---|---|
margin | 边界值 |
reduction | 计算模式,可为none/sum/mean |
计算公示如下:
l
n
=
{
x
n
,
i
f
y
n
=
1
max
{
0
,
Δ
−
x
n
}
,
i
f
y
n
=
−
1
.
}
\left.\mathrm{l_n~=~\left\{\begin{array}{ll}x_n,&\quad\mathrm{if~y_n~=~1}\\\max\left\{0,\Delta-x_n\right\},&\quad\mathrm{if~y_n~=~-1}\end{array}\right. .}\right\}
ln = {xn,max{0,Δ−xn},if yn = 1if yn = −1.}
测试代码:
import torch
import torch.nn as nn
import torch.nn.functional as F
import matplotlib.pyplot as plt
import numpy as np
import random
def set_seed(seed=1): # 设置随机种子
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
# ---------------------------------------------- Hinge Embedding Loss -----------------------------------------
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)
(16)nn.CosineEmbeddingLoss
功能:采用余弦相似度计算两个输入的相似性
参数 | 作用 |
---|---|
margin | 可取值[-1,1],推荐为[0,0.5] |
reduction | 计算模式,可为none/sum/meam |
计算公式如下: l o s s ( x , y ) = { 1 − cos ( x 1 , x 2 ) , if y = 1 max ( 0 , cos ( x 1 , x 2 ) − margin ) , if y = − 1 \left.\mathrm{loss(x,y)}=\left\{\begin{array}{ll}1-\cos\left(\mathrm{x}_1,\mathrm{x}_2\right),&\quad\text{if y}=1\\\max\left(0,\cos\left(\mathrm{x}_1,\mathrm{x}_2\right)-\text{margin}\right),&\quad\text{if y}=-1\end{array}\right.\right. loss(x,y)={1−cos(x1,x2),max(0,cos(x1,x2)−margin),if y=1if y=−1之所以用 c o s cos cos, 希望关注于这两个输入方向上的一个差异,而不是距离上的差异, c o s cos cos函数如下: 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)=\frac{\mathrm{A}\cdot\mathrm{B}}{\|\mathrm{A}\|\|\mathrm{B}\|}=\frac{\mathrm{\sum_{i=1}^nA_i}\times\mathrm{B_i}}{\sqrt{\sum_{\mathrm{i}=1}^n\left(\mathrm{A_i}\right)^2}\times\sqrt{\sum_{\mathrm{i}=1}^n\left(\mathrm{B_i}\right)^2}} cos(θ)=∥A∥∥B∥A⋅B=∑i=1n(Ai)2×∑i=1n(Bi)2∑i=1nAi×Bi
测试代码:
import torch
import torch.nn as nn
import torch.nn.functional as F
import matplotlib.pyplot as plt
import numpy as np
import random
def set_seed(seed=1): # 设置随机种子
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
# ---------------------------------------------- Cosine Embedding Loss -----------------------------------------
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)
(17)nn.CTCLoss
功能:计算CTC损失,解决时序类数据的分类
参数 | 作用 |
---|---|
blank | blank label |
zero_infinity | 无穷大的值或梯度置0 |
reduction | 计算模式,可为none/sum/mean |
测试代码:
import torch
import torch.nn as nn
import torch.nn.functional as F
import matplotlib.pyplot as plt
import numpy as np
import random
def set_seed(seed=1): # 设置随机种子
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
# ---------------------------------------------- 18 CTC Loss -----------------------------------------
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)
到这里,损失函数就介绍完了,实际上我们只需要对这些损失函数从任务的角度分分类,需要用的时候再去查具体用法就可以啦。
- 分类问题
- 二分类单标签问题: nn.BCELoss, nn.BCEWithLogitsLoss, nn.SoftMarginLoss
- 二分类多标签问题:nn.MultiLabelSoftMarginLoss
- 多分类单标签问题: nn.CrossEntropyLoss, nn.NLLLoss, nn.MultiMarginLoss
- 多分类多标签问题: nn.MultiLabelMarginLoss,
- 不常用:nn.PoissonNLLLoss, nn.KLDivLoss
- 回归问题: nn.L1Loss, nn.MSELoss, nn.SmoothL1Loss
- 时序问题:nn.CTCLoss
- 人脸识别问题:nn.TripletMarginLoss
- 半监督Embedding问题(输入之间的相似性): nn.MarginRankingLoss, nn.HingeEmbeddingLoss, nn.CosineEmbeddingLoss
三、总结
这次损失函数整理的内容还是很多的, 主要分为两大块:权重初始化和损失函数, 第一部分是权重初始化方法,而第二部分是损失函数的介绍以及了解了几种损失函数的实现
首先,我们在第一节中解决了模型模块的遗留问题—— 权重的初始化方法,了解了神经网络中梯度消失和梯度爆炸的原理,也知道了权重初始化的重要性,针对各种情况学习了不同的初始化方法,重要的是Xavier初始化和Kaiming初始化方法, 分别针对非饱和激活函数和包含激活函数的网络。
然后在第二节回顾了softmax中交叉熵的概念,对损失函数有了初步了解,知道了损失函数是一个Module。 然后学习了交叉熵损失函数及四个特例, 交叉熵损失函数比较重要,所以补充了几点信息学的内容。