PyTorch激活与损失函数

PyTorch中的激活函数损失函数都存在两种调用模式,nn.xx对象torch.xx类函数(或者定义在torch.nn.functional.xx中),前者是包装好的类,后者是可以直接调用的函数;nn.xx 类的 forward( ) 函数中调用了 torch.xx 函数。

激活函数

我们在构建深度神经网络时,如果没有激励函数,无论你神经网络有多少层,输出都是输入的线性组合,引入非线性函数作为激励函数,这样深层神经网络表达能力就更加强大(几乎可以逼近任意函数)。

1
2
3
4
import torch
import matplotlib.pyplot as plt
# 准备好数据
x = torch.linspace(-5, 5, 200)

sigmoid函数

该函数是将取值为 (−∞,+∞) 的数映射到 (0,1) 之间,其数学形式与图形为:

1
2
3
4
5
6
7
8
y_sigmoid = torch.sigmoid(x)
# sigmoid = torch.nn.Sigmoid() # 使用类-对象模式
# y_sigmoid = sigmoid(x)

plt.plot(x.numpy(), y_sigmoid.numpy(), c='red', label='sigmoid')
plt.ylim((-0.2, 1.2))
plt.legend(loc='best')
plt.show()

sigmoid 函数满足当输入很小时,输出接近于0;当输入很大时,输出值接近1。从它的图形中可以看出其具有以下几个缺点:

  • 当 x 值非常大或者非常小时,sigmoid函数的导数 g′(z) 将接近 0,这会导致权重参数 W 的梯度将接近 0 ,使得梯度更新十分缓慢,即梯度消失。
  • 非零中心化,也就是当输入为0时,输出不为0,因为每一层的输出都要作为下一层的输入,而未0中心化会直接影响梯度下降 。
  • 从其函数定义可以看出计算量比较大。

sigmoid 函数可用在网络最后一层,作为输出层进行二分类,但尽量不要使用在隐藏层。 

tanh函数

该函数是将取值为 (−∞,+∞) 的数映射到 (−1,1) 之间,其数学形式与图形为:

1
2
3
4
5
6
7
8
y_tanh = torch.tanh(x)
# tanh = torch.nn.Tanh() # 使用类-对象模式
# y_tanh = tanh(x)

plt.plot(x.numpy(), y_tanh.numpy(), c='red', label='tanh')
plt.ylim((-1.2, 1.2))
plt.legend(loc='best')
plt.show()

对比 tanh 函数与 sigmoid 函数的图形和定义可以看出,它是零中心化的,但仍然存在梯度消失和计算量大的缺点。

ReLU函数

是一种分段线性函数,$Relu(x)=max(0, x)$,该函数的图形为:

1
2
3
4
5
6
7
8
y_relu = torch.relu(x)
#relu = torch.nn.ReLU() # 使用类-对象模式
#y_relu = relu(x)

plt.plot(x.numpy(), y_relu.numpy(), c='red', label='relu')
plt.ylim((-1, 5))
plt.legend(loc='best')
plt.show()

ReLU 极大程度地弥补了 sigmoid 函数以及 tanh 函数的梯度消失问题(当输入为负数时也会导致梯度为零),计算速度也提升了许多。ReLU 目前仍是最常用的激活函数,在搭建人工神经网络的时候推荐优先尝试。当使用 ReLU 时要小心设置 learning rate,而且要注意不要让网络出现很多 “dead” 神经元,如果这个问题不好解决,那么可以试试 Leaky ReLU、PReLU 或者 Maxout。

softmax函数

与 sigmoid 函数类似,softmax 函数将每个单元的输出压缩为 (0, 1) 之间。然而,softmax操作还将每个输出除以所有输出的和,从而得到一个离散概率分布,除以 k 个可能的类,结果分布中的概率总和为 1。这对于解释分类任务的输出非常有用。

1
2
3
4
5
6
7
8
9
10
11
x = torch.rand(3)
print(x)
# 类函数模式在:torch.nn.functional.softmax()
softmax = torch.nn.Softmax(dim=0) # 参数指定在哪一个轴上进行归一化
y_softmax = softmax(x)
print(y_softmax)

'''
tensor([0.7768, 0.0511, 0.6187])
tensor([0.4278, 0.2070, 0.3652])
'''

结合 softmax 函数的公式与输出结果可以看出,由于使用了指数,可以让大的值更大,让小的更小,增加了区分对比度,提高学习效率。

损失函数

在深度学习中要用到各种各样的损失函数,这些损失函数可看作是一种特殊的 layer, PyTorch 中的损失函数一般在训练模型时候指定,调用时传入 y_pred 在前,y_true 在后。值得注意的是,大部分损失函数在定义时都有 size_average 和 reduce 两个布尔类型的参数,需要解释一下。因为一般损失函数都是直接计算 batch 的数据,因此返回的 loss 结果都是维度为 (batch_size, ) 的张量。

  • 如果 reduce = False,那么 size_average 参数失效,直接返回张量形式的 loss;
  • 如果 reduce = True,那么 loss 返回的是标量;
    • 如果 size_average = True,返回 loss.mean();
    • 如果 size_average = False,返回 loss.sum();

另外一个参数 reduction 有三个取值,分别为 ‘none’、’mean’、’sum’,也可以来指导规约计算的方式,所以如果想要返回的 loss 是规约后的标量,一般我们推荐:size_average 和 reduce 都取 None,通过更改 reduction 的值以达到不同的规约目标。尽管 PyTorch 提供如此多灵活的计算方式,但最终用于优化的损失必须是一个标量。

均方损失函数

对于回归模型,通常使用的是均方损失函数nn.MSELoss:

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

1
2
3
4
5
y_pred = torch.ones(2, 2, requires_grad=True) * 0.5
y_true = torch.ones(2, 2)

loss_func = torch.nn.MSELoss()
loss = loss_func(y_pred, y_true)

用于回归的损失函数还有,nn.L1Loss (L1 损失,也叫做绝对值误差损失)或者 nn.SmoothL1Loss (平滑 L1 损失,当输入在 -1 到 1 之间时,平滑为 L2 损失)。

二元交叉熵损失函数

对于二分类模型,通常使用的是二元交叉熵损失函数 nn.BCELoss (输入已经是 sigmoid 激活函数之后的结果) 或者 nn.BCEWithLogitsLoss (输入尚未经过 nn.Sigmoid 激活函数) :

torch.nn.BCELoss(weight=None, size_average=None, reduce=None, reduction=’mean’)
torch.nn.BCEWithLogitsLoss(weight=None, size_average=None, reduce=None, reduction=’mean’, pos_weight=None)

这里需要注意的是,y_pred 的形状与 y_true 是相同的,都是 (batch, *),其中 * 对应维度上的取值应该在 (0, 1) 之间(经过 sigmoid 激活),表示当前维度判断为 yes(二分类yes/no) 的概率。通常最简单的二分类数据格式,* 轴上对应的维度仅仅1维。

1
2
3
4
5
y_pred = torch.randn((3, 2)) # batch_size=3
y_true = torch.FloatTensor(3, 2).random_(2) # 各元素取值为0或1,但必须是浮点型

loss_func = torch.nn.BCELoss(reduce=False)
loss = loss_func(torch.sigmoid(y_pred), y_true)

交叉熵损失函数

对于多分类模型(同样适用于二分类),一般推荐使用交叉熵损失函数 nn.CrossEntropyLoss (y_true 需要是一阶的稀疏标签值,非 one-hot 形式;y_pred 是维度与类别数相等的张量,未经过 torch.nn.Softmax 激活,CrossEntropyLoss 会自动对其进行一个 Softmax() 操作)

torch.nn.CrossEntropyLoss(weight=None, size_average=None, ignore_index=-100, reduce=None, reduction=’mean’)

1
2
3
4
5
6
7
8
9
10
import numpy as np
# 假设一个3分类任务,batch_size=2,假设每个神经元输出都为0.5
y_pred = torch.ones(2, 3, requires_grad=True) * 0.5 # 形状是 (batch, category)
y_true = torch.from_numpy(np.array([0, 1])).type(torch.LongTensor)

loss_func = torch.nn.CrossEntropyLoss(reduce=False)
# 使用 weight 对第一个类别加权
# weight = torch.from_numpy(np.array([0.6, 0.2, 0.2])).float()
# loss_func = torch.nn.CrossEntropyLoss(weight=weight, reduce=False)
loss = loss_func(y_pred, y_true)

可选参数 weight 必须是一个 1 阶张量, 权重将被分配给各个类别. 对于不平衡的训练集非常有效。

此外,如果多分类的 y_pred 已经通过 nn.LogSoftmax 激活,则可以使用 nn.NLLLoss 损失函数(the Negative Log Likelihood Loss), 这种方法和直接使用 nn.CrossEntropyLoss 等价:

torch.nn.NLLLoss(weight=None, size_average=None, ignore_index=-100, reduce=None, reduction=’mean’)

1
2
3
4
5
logsoftmax = torch.nn.LogSoftmax(dim=1)
y_pred = logsoftmax(y_pred) # y_pred被LogSoftmax激活过

loss_func = torch.nn.NLLLoss(reduce=False)
loss = loss_func(y_pred, y_true) # 可以看到和上面的loss是一样的

自定义损失函数

如果有需要,也可以自定义损失函数,自定义损失函数需要接收两个张量 y_pred,y_true 作为输入参数,并输出一个标量作为损失函数值。也可以对 nn.Module 进行子类化,重写 forward() 方法实现损失的计算逻辑,从而得到损失函数的类的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class FocalLoss(nn.Module):

def __init__(self,gamma=2.0,alpha=0.75):
super().__init__()
self.gamma = gamma
self.alpha = alpha

def forward(self,y_pred,y_true):
bce = torch.nn.BCELoss(reduction = "none")(y_pred,y_true)
p_t = (y_true * y_pred) + ((1 - y_true) * (1 - y_pred))
alpha_factor = y_true * self.alpha + (1 - y_true) * (1 - self.alpha)
modulating_factor = torch.pow(1.0 - p_t, self.gamma)
loss = torch.mean(alpha_factor * modulating_factor * bce)
return loss

上面的代码是一个 Focal Loss 的自定义实现示范。Focal Loss是一种对binary_crossentropy的改进损失函数形式。它有两个可调参数,$\alpha$ 参数和 $\gamma$ 参数。其中 $\alpha$ 参数主要用于衰减负样本的权重,$\gamma$ 参数主要用于衰减容易训练样本的权重:

从而让模型更加聚焦在正样本和困难样本上,这就是为什么这个损失函数叫做 Focal Loss。