PyTorch深度学习笔记(5):神经网络的训练--损失函数

在前面的文章中,我们介绍了单层和多层神经网络的正向传播过程,同时也使用了torch.nn来实现这一过程。不难发现,模型中的权重\boldsymbol{w}对模型输出的预测结果起着很重要的作用。而前面我们在实现正向传播的过程中,手动设定或者随机生成了模型参数的值,并没有对其进行训练和优化。从这篇文章开始,我们将重点讲解神经网络的训练,本文将主要介绍神经网络训练中的损失函数

模型训练的一般过程包含以下几个步骤:

  1. 定义模型;
  2. 定义损失函数/目标函数:有目标才能指导模型训练优化的方向;
  3. 确定优化算法:不同的优化算法迭代效率和效果会有不同;
  4. 利用优化算法,最小化损失函数,求解最佳模型参数。

在机器学习和深度学习中,我们通过比较预测值和真实值的差异,来衡量一个模型表现的好坏。而预测值和真实值的差异,可以通过定义损失函数来表示。下面我们将介绍在回归问题、二分类问题、多分类问题中,所使用的损失函数。由于损失函数是与模型权重\boldsymbol{w}相关的,我们使用标记符号L(\boldsymbol{w})来代表。

误差平方和 Sum of Squared Error (SSE)

在回归问题中,我们可以用误差平方和SSE来衡量预测值与真实值的差异,其定义为:
SSE = \sum_{i=1}^{m} (\hat{z}_{i} - z_{i})^{2}

其中,m为样本个数,\hat{z}_{1}为样本预测值,z_{i}为真实值。可以看出,误差平方和SSE的值与样本个数m有关,为了消除样本数量的影响,我们可以使用均方误差MSE (Mean Squared Error),其定义如下:
MSE = \frac{1}{m} \sum_{i=1}^{m} (\hat{z}_{i} - z_{i})^{2}

  • 注:在有些教材中,MSE定义中前面的系数为\frac{1}{2m},这是为了在计算MSE导数时,可以消除系数2的影响;以上两种定义均可使用,只需在同一项目中保持一致即可。

用torch.nn中的类实现MSE

import torch
from torch.nn import MSELoss

y_hat = torch.randn(size=(500,), dtype=torch.float32) # 预测值
y = torch.randn(size=(500,), dtype=torch.float32) # 真实值

criterion = MSELoss(reduction="mean") # 实例化一个MSELoss类
loss = criterion(y_hat, y)
loss
在`MSELoss`类中有个重要参数是:reduction(默认值为"mean")
- `reduction="mean"`输出MSE;
- `reduction="sum"`输出SSE;
criterion = MSELoss(reduction="sum") # 实例化一个MSELoss类
loss = criterion(y_hat, y)
loss

tensor(2.0436)

二分类交叉熵损失函数 Binary Cross Entropy Loss

对于二分类问题,我们可以使用二分类交叉熵损失函数(Binary Cross Entropy Loss),其公式如下:
L(\boldsymbol{w}) = - \sum_{i=1}^{m} \left ( y_{i} \log(\sigma_{i}) + (1-y_{i})log(1-\sigma_{i}) \right)

二分类交叉熵损失函数来源于统计学上的极大似然估计(Maximun Likelihood Estimate)。下面是对如何从极大似然估计获得二分类交叉熵损失函数的解释说明,该部分内容需要一定的统计学基础,也可直接跳过。

假设我们的样本满足以下两个条件:

1) 每一个样本是独立的;

2) 对于样本i,它的值Y_{i}是一个随机变量,且服从伯努利分布:
Y_{i} \sim Bernoulli(\sigma_{i}),
服从上述概率分布的Y_{i}:取值为1的概率是\sigma_{i};取值为0的概率是1-\sigma_{i}。即:
P(Y_{i}=y_{i}) = \sigma^{y_{i}}(1-\sigma_{i})^{1-y_{i}}
其中P(Y_{i}=y_{i})表示Y_{i}=y_{i}的概率,y_{i}的值可带入0或1。

对于所有的样本i = 1, 2, \cdots, m,似然函数 Likelihood 为:
\begin{aligned} Likelihood &= \prod_{i=1}^{m} P(Y_i = y_i) \\ &=\prod_{i=1}^{m} \sigma_{i}^{y_{i}} (1-\sigma_{i})^{1-y_{i}} \end{aligned}

对数似然函数 Log-Likelihood 为:
\begin{aligned} LL &= \log(Likelihood) \\ &= \log \left ( \prod_{i=1}^{m} \sigma_{i}^{y_{i}} (1-\sigma_{i})^{1-y_{i}} \right) \\ &=\sum_{i=1}^{m} \left ( y_{i} \log(\sigma_{i}) + (1-y_{i}) \log(1-\sigma_{i}) \right ) \end{aligned}

由于\sigma_{i} = sigmoid(z_{i}),而z_{i}是由样本特征及权重\boldsymbol{w}构成,因此,Log-Likelihood函数值与权重\boldsymbol{w}有关。极大似然估计,就是求解能使Log-Likelihood函数取得最大值的那一个权重\boldsymbol{w}

我们的二分类交叉熵损失函数L(\boldsymbol{w}),就是取负数的Log-Likelihood函数,也可以叫做 Negtive Log-Likelihood(NLL)函数:
L(\boldsymbol{w}) = -\log(Likelihood) = - \sum_{i=1}^{m} \left ( y_{i} \log(\sigma_{i}) + (1-y_{i}) \log(1-\sigma_{i}) \right )

使Log-Likelihood取得最大值的\boldsymbol{w},就是使二分类交叉熵损失函数取得最小值\boldsymbol{w},也就是我们所需要的\boldsymbol{w}的最优解。

用tensor自定义二分类交叉熵损失函数

import torch
import time

m = 3*pow(10, 3)
torch.manual_seed(516)

X = torch.randn((m,4), dtype=torch.float32)
w = torch.randn((4,1), dtype=torch.float32, requires_grad=True)
y = torch.randint(low=0, high=2, size=(m,1), dtype=torch.float32)

z_hat = torch.mm(X, w)
sigma = torch.sigmoid(z_hat)

loss = -1/m*torch.sum(y*torch.log(sigma) + (1-y)*torch.log(1-sigma))
loss

注意,在用tensor手写损失函数等复杂函数时,除了简单的加减乘除,其余运算都需要使用tensor中定义的运算,这样大大加快运算速度。我们可以对比在数据样本量大的时候,在定义损失函数中使用torch.sum()和使用sum()的区别。

import torch
import time

m = 3*pow(10, 6) # 300万的样本量
torch.manual_seed(516)

X = torch.randn((m,4), dtype=torch.float32)
w = torch.randn((4,1), dtype=torch.float32, requires_grad=True)
y = torch.randint(low=0, high=2, size=(m,1), dtype=torch.float32)

z_hat = torch.mm(X, w)
sigma = torch.sigmoid(z_hat)

start = time.time()
loss1 = -1/m*torch.sum(y*torch.log(sigma) + (1-y)*torch.log(1-sigma))
end = time.time()
print("使用torch.sum()时间:", end-start)

start = time.time()
loss2 = -1/m*sum(y*torch.log(sigma) + (1-y)*torch.log(1-sigma))
end = time.time()
print("使用sum()时间:", end-start)

使用torch.sum()时间: 0.0221560001373291

使用sum()时间: 16.319851875305176

可以看出,使用了torch.sum()函数大大节约了运算时间。

用torch.nn中的类实现二分类交叉熵损失函数

  • BCEWithLogitsLoss:内置了sigmoid函数,参数输入线性加和值z_hat和真实值y
  • BCELoss:参数输入sigma和真实值y
import torch
from torch.nn import BCEWithLogitsLoss
from torch.nn import BCELoss

m = 3*pow(10, 3)
torch.manual_seed(516)

X = torch.randn((m,4), dtype=torch.float32)
w = torch.randn((4,1), dtype=torch.float32, requires_grad=True)
y = torch.randint(low=0, high=2, size=(m,1), dtype=torch.float32)

z_hat = torch.mm(X, w)
sigma = torch.sigmoid(z_hat)

criterion1 = BCEWithLogitsLoss()
loss1 = criterion1(z_hat, y)
print(loss1)

criterion2 = BCELoss()
loss2 = criterion2(sigma, y)
print(loss2)

tensor(0.8185, grad_fn=<BinaryCrossEntropyWithLogitsBackward>)

tensor(0.8185, grad_fn=<BinaryCrossEntropyBackward>)

PyTorch官方更推荐使用BCEWithLogitsLoss这个内置了sigmoid函数的类。内置的sigmoid函数可以让精度问题被缩小(因为将指数运算包含在了内部),以维持算法运行时的稳定性。所以,当我们的输出层使用sigmoid函数时,我们就可以使用BCEWithLogitsLoss作为损失函数。

类似MSELoss类,BCEWithLogitsLossBCELoss中也有参数reduction(默认值为"mean"),可以替换成"sum"或"none"。

多分类交叉熵损失函数 Cross Entropy Loss

在多分类问题中,我们需要预测的样本的标签是一个类别代号。比如,在一个水果分类问题中,我们需要区分葡萄、杏子、和荔枝三类水果,我们可以用类别1,类别2,类别3 来分别代表葡萄杏子荔枝

样本 类别
样本1 1
样本2 2
样本3 3
样本4 2
... ...
样本m 1

在给多分类问题定义损失函数时,我们需要将上述每个样本的标量标签,拓展为一个向量标签,拓展方式如下表:

样本 类别1 类别2 类别3
样本1 1 0 0
样本2 0 1 0
样本3 0 0 1
样本4 0 1 0
... ... ... ...
样本m 1 0 0

每个样本的标签是由一个向量构成,向量的长度等于类别总数,向量的第j个元素对应分类类别j,向量元素的值由0和1构成,元素值为1所对应的类别编号即为该样本的类别编号。比如上表中,样本m的向量标签为[1, 0, 0],在类别1的位置元素值为1,表示样本m的分类类别为1。

至此,我们可以使用交叉熵损失函数(Cross Entropy Loss)来衡量多分类问题中预测值与真实值的差异,其公式如下:
L(\boldsymbol{w}) = - \sum_{i=1}^{m} y_{i(k=j)} \log(\sigma_{i(k=j)})

其中,j为样本i对应的真实类别编号。

同样的,交叉熵损失函数也是由极大似然估计推导出来的。不同于二分类问题,多分类问题假设样本的标签服从多项式分布。下面是推导过程的简要概述,不感兴趣的也可直接跳过。

对于样本i,其标签概率分布满足:
\begin{aligned} P \left \{ y_{i(k=1)}, y_{i(k=2)}, \cdots, y_{i(k=K)} \right\} &= \sigma_{i(k=1)}^{y_{i(k=1)}} \sigma_{i(k=2)}^{y_{i(k=2)}} \cdots \sigma_{i(k=K)}^{y_{i(k=K)}} \end{aligned}

j为样本i对应的真实类别编号,即y_{i(k=j)} = 1y_{i(k \neq j)} = 0,上述式子可以简化为:
\begin{aligned} P \left \{ y_{i(k=1)}, y_{i(k=2)}, \cdots, y_{i(k=K)} \right\} &= \sigma_{i(k=j)}^{y_{i(k=j)}} \end{aligned}

似然函数 Likelihood 为:
\begin{aligned} Likelihood &= \prod_{i=1}^{m} P \left \{ y_{i(k=1)}, y_{i(k=2)}, \cdots, y_{i(k=K)} \right\} \\ &=\prod_{i=1}^{m} \sigma_{i(k=j)}^{y_{i(k=j)}} \end{aligned}

对数似然函数 Log-Likelihood 为:
\begin{aligned} LL &= \log(Likelihood) \\ &= \log \left ( \prod_{i=1}^{m} \sigma_{i(k=j)}^{y_{i(k=j)}} \right) \\ &=\sum_{i=1}^{m} {y_{i(k=j)}} \log(\sigma_{i(k=j)}) \end{aligned}

L(\boldsymbol{w}) = -\log(Likelihood) = - \sum_{i=1}^{m} y_{i(k=j)} \log(\sigma_{i(k=j)})

注:在上述推导中,为了公式的简化,有一些不严谨之处,

  • k为类别取值,大K代表总共有K个分类(按道理说若K代表总共有K个类别,则不应该再使用K代表某个具体类别,但我们使用的类别编号与类别本身相同,因此简化了表达);
  • 对于不同的样本i,真实类别j是不同的,因此j不是一个固定的值,而公式中并未特别体现这一点。

用torch.nn中的类实现交叉熵损失函数

使用NLLLossLogSoftmax实现

import torch 
from torch.nn import NLLLoss
from torch.nn import LogSoftmax

m = 3*pow(10, 2)
torch.manual_seed(516)

X = torch.randn((m,4), dtype=torch.float32)
w = torch.randn((4,3), dtype=torch.float32, requires_grad=True)
y = torch.randint(low=0, high=2, size=(m,1), dtype=torch.float32)

z_hat = torch.mm(X, w)
logsm = LogSoftmax(dim=1) # 实例化;dim=1: 对矩阵中每一行数据求解softmax
logsigma = logsm(z_hat)

criterion = NLLLoss()
loss = criterion(logsigma, y)
loss 

RuntimeError: 1D target tensor expected, multi-target not supported

从报错信息中可以看出,在NLLLoss中,只接受一维tensor的y值。因此,我们在下面代码中修改y的定义。

import torch 
from torch.nn import NLLLoss
from torch.nn import LogSoftmax

m = 3*pow(10, 2)
torch.manual_seed(516)

X = torch.randn((m,4), dtype=torch.float32)
w = torch.randn((4,3), dtype=torch.float32, requires_grad=True)
y = torch.randint(low=0, high=2, size=(m,), dtype=torch.float32)
# y被定义为一维张量

z_hat = torch.mm(X, w)
logsm = LogSoftmax(dim=1) # 实例化;dim=1: 对矩阵中每一行数据求解softmax
logsigma = logsm(z_hat)

criterion = NLLLoss()
loss = criterion(logsigma, y)
loss 

RuntimeError: expected scalar type Long but found Float

从报错信息中可以看出,在NLLLoss中,只接受整型类型的y值(交叉熵损失需要将标签转化为独热形式,因此不接受浮点数作为标签的输入),因此,我们在计算NLLLoss值时,使用转换为整型的y。

criterion = NLLLoss()
loss = criterion(logsigma, y.long())
loss

tensor(2.5821, grad_fn=<NllLossBackward>)

为何我们在定义y维度时,直接将二维张量改为一维张量,而在定义y数据类型时,不直接将其定义为整型呢?

  • 将y定义为一维张量,可以适应绝大多数pytorch中的运算而不报错,相反,若每次调用都需要转换维度,将会大大降低运算效率;
  • 在大多数情况下,pytorch运算中都要求保持张量数据类型的一致,而特征X和权重w通常是float类型的。因此,我们只在需要将y的数据类型转换时转换。

直接使用CrossEntroyLoss实现

import torch 
from torch.nn import CrossEntropyLoss

m = 3*pow(10, 2)
torch.manual_seed(516)

X = torch.randn((m,4), dtype=torch.float32)
w = torch.randn((4,3), dtype=torch.float32, requires_grad=True)
y = torch.randint(low=0, high=2, size=(m,), dtype=torch.float32)

z_hat = torch.mm(X, w)

criterion = CrossEntropyLoss()
loss = criterion(z_hat, y.long())
loss 

tensor(2.5821, grad_fn=<NllLossBackward>)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,539评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,911评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,337评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,723评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,795评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,762评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,742评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,508评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,954评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,247评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,404评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,104评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,736评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,352评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,557评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,371评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,292评论 2 352

推荐阅读更多精彩内容