反向传播

简单介绍一下BP算法:

  • 给定一个输入xx(列向量),前向传播得到预测值hW,b(x)h_{W,b}(x),其中W,bW,b是网络参数
  • 根据标签值yy与预测值的误差(此处以均方误差为例)对网络参数进行更新,该过程是从后向前的,称为反向传播
    图1. 反向传播示例(黑色箭头代表前向传播,红色箭头代表反向传播
详细推导

相关符号定义:

  • 网络的总层数设为nln_l,那么倒数第二层就是第nl1n_l-1层,网络层数ll取值范围为[1,nl][1,n_l],特别的,第11层为输入层,第nln_l层为输出层
  • 网络权重和偏置(W,b):=(W1,b1,W2,b2,...,Wnl1,bnl1)(W,b):=(W^1,b^1,W^2,b^2,...,W^{n_l-1},b^{n_l-1}),设Wi,jlW_{i,j}^l表示第ll层的第jj个节点与第l+1l+1层的第ii个节点之间的连接权重值,bilb_i^l表示第l+1l+1层的第ii个节点的偏置项
  • zilz_i^l为第ll层的第ii个节点的输入加权和(zil=j=1Sl1(Wijl1ajl1)z_i^{l}=\sum\limits_{j=1}^{S_l-1}(W_{ij}^{l-1}a_j^{l-1})),记aila_i^l为第ll层的第ii个节点的激活输出
  • SlS_l为第ll层的节点数

以第ll层为例,其输入是al1RSl1×1a^{l-1}\in R^{S_{l-1}\times 1},该层的权重矩阵为Wl1=[W11l1W21l1WSl,1l1W12l1W22l1WSl,2l1W1,Sl1l1W2,Sl1l1WSl,Sl1l1]RSl1×SlW^{l-1}=\begin{bmatrix}W_{11}^{l-1}&W_{21}^{l-1}&\cdots&W_{S_l,1}^{l-1}\\W_{12}^{l-1}&W_{22}^{l-1}&\cdots&W_{S_l,2}^{l-1}\\\vdots&\vdots&\ddots&\vdots\\W_{1,S_{l-1}}^{l-1}&W_{2,S_{l-1}}^{l-1}&\cdots&W_{S_l,S_{l-1}}^{l-1}\end{bmatrix}\in R^{S_{l-1}\times S_l},偏置项为bl1RSl×1b^{l-1}\in R^{S_l\times 1},于是第ll层的输出为al=σ(zl)=σ((Wl1)Tal1+bl1)RSl×1a^l=\sigma(z^l)=\sigma((W^{l-1})^Ta^{l-1}+b^{l-1})\in R^{S_l\times 1},其中σ()\sigma(·)为激活函数。事实上,要求损失函数L=12hW,b(x)y2L=\frac{1}{2}\|h_{W,b}(x)-y\|^2对权重的偏导,核心是计算其对zz(任意节点的输入加权和)的偏导,这又分为两种情况,一是输出层,二是非输出层,就是简单的链式求导:

  • 输出层
    Lzinl=zinl(12j=1Snl(ajnlyj)2)=(ainlyi)ainlzinl=(ainlyi)σ(zinl)\frac{\partial L}{\partial z_i^{n_l}}=\frac{\partial}{\partial z_i^{n_l}}(\frac{1}{2}\sum\limits_{j=1}^{S_{n_l}}(a_j^{n_l}-y_j)^2)=(a_i^{n_l}-y_i)\frac{\partial a_i^{n_l}}{\partial z_i^{n_l}}=(a_i^{n_l}-y_i)\sigma^{'}(z_i^{n_l})
  • 非输出层
    以倒数第二层为例,Lzinl1=Lz1nlz1nlzinl1+...+LzSnlnlzSnlnlzinl1=j=1Snl(Lzjnlzjnlzinl1)=j=1Snl(LzjnlWjinl1σ(zinl1))\frac{\partial L}{\partial z_i^{n_l-1}}=\frac{\partial L}{\partial z_1^{n_l}}\frac{\partial z_1^{n_l}}{\partial z_i^{n_l-1}}+...+\frac{\partial L}{\partial z_{S_{n_l}}^{n_l}}\frac{\partial z_{S_{n_l}}^{n_l}}{\partial z_i^{n_l-1}}=\sum\limits_{j=1}^{S_{n_l}}(\frac{\partial L}{\partial z_j^{n_l}}\frac{\partial z_j^{n_l}}{\partial z_i^{n_l-1}})=\sum\limits_{j=1}^{S_{n_l}}(\frac{\partial L}{\partial z_j^{n_l}}W_{ji}^{n_l-1}\sigma^{'}(z_i^{n_l-1})),其中zjnl=Wj1nl1σ(z1nl1)+...+Wj,Snl1nl1σ(zSnl1nl1)z_j^{n_l}=W_{j1}^{n_l-1}\sigma(z_1^{n_l-1})+...+W_{j,S_{n_l-1}}^{n_l-1}\sigma(z_{S_{n_l-1}}^{n_l-1})。一般的,对任意非输出层lll{2,...,nl1}l\in \{2,...,n_l-1\}),有Lzil=j=1Sl+1(Lzjl+1Wjil)σ(zil)\frac{\partial L}{\partial z_i^l}=\sum\limits_{j=1}^{S_{l+1}}(\frac{\partial L}{\partial z_j^{l+1}}W_{ji}^{l})\sigma^{'}(z_i^l)(这就是图1反向传播的原理)

现在可以轻松求解LWijl=Lzil+1zil+1Wijl=Lzil+1ajl\frac{\partial L}{\partial W_{ij}^l}=\frac{\partial L}{\partial z_i^{l+1}}\frac{\partial z_i^{l+1}}{\partial W_{ij}^l}=\frac{\partial L}{\partial z_i^{l+1}}a_j^l,以及Lbil=Lzil+1zil+1bil=Lzil+11\frac{\partial L}{\partial b_i^l}=\frac{\partial L}{\partial z_i^{l+1}}\frac{\partial z_i^{l+1}}{\partial b_i^l}=\frac{\partial L}{\partial z_i^{l+1}}111是一个全1列向量)

Pytorch标量对矩阵求导

举个例子,tr(AXBX)X=ATXTBT+BTXTAT\frac{\partial tr(AXBX)}{\partial X}=A^TX^TB^T+B^TX^TA^T(该解析解可根据“矩阵求导术”得出)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> import torch as pt
>>> X=pt.arange(12,dtype=pt.float32).view(3,4) #要使X支持求导,必须为浮点类型
>>> _=X.requires_grad_(True) #置requires_grad为True(表示可导),于是X上的梯度更新将累积到grad属性中
>>> A=pt.rand(4,3) #rand()的默认类型就是float32
>>> B=pt.rand(4,3)
>>> out=pt.trace(A.mm(X).mm(B).mm(X)) #out是一个复合函数,默认只会计算对叶子节点的导数,中间节点需要手动定义,譬如AX=A.mm(X)
>>> out.backward() #只要任意叶子节点(此处就是X,A,B)的requires_grad为True,则out也为True,执行一遍反向传播后,将可以得出out对X的偏导(存放在X.grad属性中)
>>> X.grad #由于梯度会累加,可以执行X.grad.data.zero_()对梯度清零
tensor([[35.3752, 34.2365, 9.8990, 22.8047],
[31.8588, 32.2588, 8.7556, 17.3359],
[24.6398, 21.3645, 6.5890, 17.4973]])
>>> pt.t(A).mm(pt.t(X)).mm(pt.t(B))+pt.t(B).mm(pt.t(X)).mm(pt.t(A)) #验证偏导是否计算正确(AᵀXᵀBᵀ+BᵀXᵀAᵀ),正确
tensor([[35.3752, 34.2365, 9.8990, 22.8047],
[31.8587, 32.2588, 8.7556, 17.3359],
[24.6398, 21.3645, 6.5890, 17.4973]], grad_fn=<AddBackward0>)

线性回归模型

简要回顾:

线性回归预测模型定义为y^=w1x1+w2x2+...+wnxn+b\hat{y}=w_1x_1+w_2x_2+...+w_nx_n+b,其中w=[w1,...,wn]T,bw=[w_1,...,w_n]^T,b为模型参数,分别称为权重和偏置项,x=[x1,...,xn]Tx=[x_1,...,x_n]^T为单个样本数据。对于有mm个样本的批量数据,其(预测模型)矩阵表示形式为:Y^=wTX+b\hat{Y}=w^TX+b(注意这边我们直接对一个向量和一个标量相加,发生广播),其中X=[x(1),...,x(m)]X=[x^{(1)},...,x^{(m)}]Y^=[y^(1),...,y^(m)]\hat{Y}=[\hat{y}^{(1)},...,\hat{y}^{(m)}],相应的,记真实值Y=[y(1),...,y(m)]Y=[y^{(1)},...,y^{(m)}]。假设我们使用均方误差(预测值与真实值差的平方)作为单样本(第ii个样本)损失函数:l(w,b)(i)=12(y^(i)y(i))2l_{(w,b)}^{(i)}=\frac{1}{2}(\hat{y}^{(i)}-y^{(i)})^2,而对mm个批量数据,我们用所有这些样本的平方误差和的均值作为总体损失:L(w,b)=1mi=1ml(w,b)(i)L_{(w,b)}=\frac{1}{m}\sum\limits_{i=1}^ml_{(w,b)}^{(i)},接下来要做的就是学习一组模型参数以最小化总体损失:w,b=argminw,bL(w,b)=argminw,b12mY^Y22w^*,b^*=\arg\min\limits_{w,b}L_{(w,b)}=\arg\min\limits_{w,b}\frac{1}{2m}\|\hat{Y}-Y\|_2^2,具体的,使用梯度下降算法更新参数以最小化损失:{wwrL(w,b)wbbrL(w,b)b\begin{cases}w\leftarrow w-r\frac{\partial L_{(w,b)}}{\partial w} \\ b\leftarrow b-r\frac{\partial L_{(w,b)}}{\partial b}\end{cases}(其中rr是学习率)

图2. 线性回归是一个单层神经网络[3]

编程实现(核心步骤):

  1. 定义网络模型
    定义网络的前向传播过程,并返回模型预测值,此处就是wX+bwX+b
  2. 定义损失函数
    损失函数用于衡量预测值与真实值的误差,此处使用的是均方误差,即12m(wX+b)Y2\frac{1}{2m}\|(wX+b)-Y\|^2
  3. 定义优化算法
    最常用且最简单的就是梯度下降算法,其他还有改进的Adam等,我们知道偏导数保存在张量对象的grad属性中(记得反向传播前清零),梯度下降要做的就是用模型参数减去这些导数值,然后将结果作为参数的新值

假设真实权重为w=[2,3.4]Tw=[2,3.4]^T,偏置为b=4.2b=4.2,生成一组数据:Y=wTX+b+ϵY=w^TX+b+\epsilon,其中ϵ\epsilon为服从均值0、标准差0.01的正态分布的随机误差项:

1
2
3
4
5
6
7
8
### 生成数据
import numpy as np
true_w=np.array([[2],[3.4]])
true_b=np.array([4.2])
samples_num=1000 #自定义样本数量
X=np.random.randn(len(true_w),samples_num)
Y=true_w.T@X+true_b
Y+=np.random.normal(0,0.01,size=Y.shape)

(三维)可视化数据:

1
2
3
4
5
6
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
ax=Axes3D(plt.gcf())
data=np.vstack((X,Y))
ax.scatter(*data)
plt.show()
图3. 准备的训练数据

在上述“核心步骤”中已经解释过了,直接看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
### 定义线性回归模型
import torch as pt
def linear_regress(X,w,b):
return pt.t(w).mm(X)+b #其实就是前向传播计算式

### 定义损失函数
def square_loss(Y_hat,Y):
return pt.sum((Y_hat-Y)**2/2) #注意我们没有在这里除以m(批量尺寸),而会在梯度下降时做,方便写代码

### 定义梯度下降算法
def sgd(params,learn_rate,batch_size):
for param in params:
param.data-=(learn_rate*param.grad/batch_size)

在开始训练前还要准备一些东西:训练数据的批量(batch)数据生成器、模型参数的初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
### 将ndarray对象转换为Tensor对象以供后续使用
X=pt.from_numpy(X).to(pt.float32) #注意from_numpy是共享内存的
Y=pt.from_numpy(Y).to(pt.float32)

### 批量读取数据的函数
def data_iter(X,Y,batch_size):
n=X.shape[1]
ind=np.arange(n)
np.random.shuffle(ind)
for i in range(0,n,batch_size):
j=pt.LongTensor(ind[i:min(i+batch_size,n)]) #最后一次可能不足一个batch
yield X.index_select(1,j),Y.index_select(1,j) #tensor.index_select(1,索引序列)表示沿张量的1轴索引元素

### 初始化模型参数
def init_params():
w=pt.tensor(np.random.normal(0,0.01,(X.shape[0],1)),dtype=pt.float32)
b=pt.zeros(1,dtype=pt.float32)
w.requires_grad_(requires_grad=True)
b.requires_grad_(requires_grad=True)
return w,b

开始训练:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
num_epoches=5 #自定义迭代周期
learn_rate=0.03 #自定义学习率
batch_size=10 #自定义批量大小
w,b=init_params()
for epoch in range(num_epoches):
for mini_X,mini_Y in data_iter(X,Y,batch_size): #小批量随机梯度下降算法
mini_Y_hat=linear_regress(mini_X,w,b) #前向传播计算预测值
loss=square_loss(mini_Y_hat,mini_Y) #计算损失值
loss.backward() #计算损失对参数的偏导数
sgd((w,b),learn_rate,batch_size) #根据梯度下降算法更新网络参数
_=w.grad.data.zero_() #梯度清零,防止累加
_=b.grad.data.zero_()
#计算当前epoch的损失(损失应该是递减的),理论上,当参数接近正确值时,损失也将接近0
print('epoch=%d,loss=%f'%(epoch+1,square_loss(linear_regress(X,w,b),Y))) #这里直接输出loss拉倒了
print('-'*22)
print('w:',w.data) #观察神经网络拟合出的参数
print('b:',b.data)

'''OUTPUT
epoch=1,loss=32.869465
epoch=2,loss=0.122995
epoch=3,loss=0.054227
epoch=4,loss=0.054130
epoch=5,loss=0.054148
----------------------
w: tensor([[2.0000],
[3.3990]])
b: tensor([4.1995])
'''

比较简单,直接看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
import torch as pt
import numpy as np

### 生成训练数据
def gen_data(true_w,true_b,samples_num):
'''参数:真实的权重(列向量),偏置(标量),训练数据个数(列代表样本)。Ndarray对象
返回:训练数据(dxn),值(标签,1xn)。Tensor/float32对象'''
X=np.random.randn(len(true_w),samples_num)
Y=true_w.T@X+true_b
Y+=np.random.normal(0,0.01,size=Y.shape)
return pt.from_numpy(X).to(pt.float32),pt.from_numpy(Y).to(pt.float32)

### 利用torch.utils.data工具包迭代产生小批量数据
import torch.utils.data as Data
def data_iter(features,labels,batch_size): #需要注意的是,features的形状为(n,d),labels的形状为(n,k),其中d为输入层节点数
#(样本维数),k为输出层节点数
#将训练数据的特征和标签组合
dataset = Data.TensorDataset(features, labels)
#随机小批量迭代生成器
iteration=Data.DataLoader(dataset, batch_size, shuffle=True)
return iteration

### 定义线性回归模型
import torch.nn as nn
linearNet=lambda input_num: nn.Sequential(nn.Linear(input_num,1))

### 定义损失函数,均方差损失
loss=nn.MSELoss()

### 定义优化算法,使用随机梯度下降
import torch.optim as optim
optimize=lambda params,lr:optim.SGD(params,lr=lr)

### 训练模型
from torch.nn import init
epoches=10
true_w=np.array([[2],[3.4]])
true_b=4.2
samples_num=1000
batch_size=10
train_X,train_Y=gen_data(true_w,true_b,samples_num)
net=linearNet(len(true_w))
_=init.normal_(net[0].weight, mean=0, std=0.01) #初始化模型参数,权重服从标准正态分布。net[0].weight/net[0].bias表示查看第一层网络的权重和偏置
_=init.constant_(net[0].bias, val=0) #后缀_表示原址修改,偏置设为0
opt=optimize(net.parameters(),0.01) #通过net.parameters()返回的生成器查看所有可学习参数
for epoch in range(epoches):
for mini_X,mini_Y in data_iter(pt.t(train_X),pt.t(train_Y),batch_size):
out=net(mini_X) #前向传播(需要注意的是,nn.Linear计算表达式为Xw^T+b,由于X的行为样本,所以输出结果会是一个列向量,
#我通常都是写成w^TX+b,其中X列为样本数据,所以输出结果是一个行向量)
l=loss(out,mini_Y.view(-1,1)) #计算损失
l.backward() #反向传播求偏导
opt.step() #执行优化,更新网络参数
net.zero_grad() #清空导数
print('epoch=%d,loss=%f'%(epoch+1,l.item()))
print('-'*21)
print(net[0].weight.data)
print(net[0].bias.data)

'''OUTPUT
epoch=1,loss=0.395253
epoch=2,loss=0.015482
epoch=3,loss=0.000321
epoch=4,loss=0.000112
epoch=5,loss=0.000151
epoch=6,loss=0.000066
epoch=7,loss=0.000070
epoch=8,loss=0.000097
epoch=9,loss=0.000113
epoch=10,loss=0.000144
---------------------
tensor([[2.0000, 3.3993]])
tensor([4.2006])
'''

Softmax分类模型

简要回顾(注意从本段开始,我会稍稍规范化latex公式的书写,会用大写和加粗来区别表示标量xx、向量x\mathbf{x}和矩阵X\mathbf{X},之前写的就算了,懒得改了):

介绍Softmax(多)分类模型之前先介绍一下Logistic(二)分类模型,Logistic分类模型的目的在于找到一个超平面wx+b=0\mathbf{w}\mathbf{x}+b=0,其中w=[w1,...,wn]R1×n\mathbf{w}=[w_1,...,w_n]\in R^{1\times n}x=[x1,...,xn]TRn×1\mathbf{x}=[x_1,...,x_n]^T\in R^{n\times 1}bRb\in R,用于在nn维空间中分割两种不同类别(用符号yy来标记)的数据,分别记为正类(类别1,y=1y=1)和负类(类别0,y=0y=0)。现给出Logistic分类预测模型:y^=hw,b(x)=σ(wx+b)\hat{y}=h_{\mathbf{w},b}(\mathbf{x})=\sigma(\mathbf{w}\mathbf{x}+b),其中σ(x)=11+ex\sigma(x)=\frac{1}{1+e^{-x}},有σ(x)=σ(x)(1σ(x))\sigma^{'}(x)=\sigma(x)(1-\sigma(x)),由σ()\sigma(·)函数的值域范围([0,1][0,1]区间),我们将模型预测结果y^\hat{y}视为“样本x\mathbf{x}属于正类的概率值P(y=1x,w,b)P(y=1|\mathbf{x},\mathbf{w},b)”,则1y^1-\hat{y}为样本x\mathbf{x}属于负类的概率,因此样本x\mathbf{x}属于类别y={1,0}y=\{1,0\}的概率为P(yx,w,b)=σ(wx+b)y(1σ(wx+b))1yP(y|\mathbf{x},\mathbf{w},b)=\sigma(\mathbf{w}\mathbf{x}+b)^{y}(1-\sigma(\mathbf{w}\mathbf{x}+b))^{1-y}(可视作单样本的目标函数,要最大化之),要求该问题中的未知参数w\mathbf{w}bb,可以采用极大似然法对其进行估计,也就是未知参数取多少时,样本取观测值的概率最大,设训练集总计有mm个样本{(x(1),y(1)),(x(2),y(2)),...,(x(m),y(m))}\{(\mathbf{x}^{(1)},y^{(1)}),(\mathbf{x}^{(2)},y^{(2)}),...,(\mathbf{x}^{(m)},y^{(m)})\},则其似然函数为(也就是总体目标函数,仍要最大化之):Lw,b=i=1mP(y(i)x(i),w,b)=i=1m[hw,b(x(i))y(i)(1hw,b(x(i)))1y(i)]L_{\mathbf{w},b}^{'}=\prod\limits_{i=1}^m P(y^{(i)}|\mathbf{x^{(i)}},\mathbf{w},b)=\prod\limits_{i=1}^m[h_{\mathbf{w},b}(\mathbf{x}^{(i)})^{y^{(i)}}(1-h_{\mathbf{w},b}(\mathbf{x}^{(i)}))^{1-y^{(i)}}],然后再取对数和负数,转换为极小值问题,由此得到我们所熟知的损失函数形式:Lw,b=1mlnLw,b=1mi=1m[y(i)ln(hw,b(x(i)))+(1y(i))ln(1hw,b(x(i)))]=1mi=1m[y(i)ln(y^(i))+(1y(i))ln(1y^(i))]L_{\mathbf{w},b}=-\frac{1}{m}\ln L_{\mathbf{w},b}^{'}=-\frac{1}{m}\sum\limits_{i=1}^m[y^{(i)}\ln(h_{\mathbf{w},b}(\mathbf{x}^{(i)}))+(1-y^{(i)})\ln(1-h_{\mathbf{w},b}(\mathbf{x}^{(i)}))]=-\frac{1}{m}\sum\limits_{i=1}^m[y^{(i)}\ln(\hat{y}^{(i)})+(1-y^{(i)})\ln(1-\hat{y}^{(i)})](某些地方还会看到另一种写法,若记f(x)=wx+bf(\mathbf{x})=\mathbf{w}\mathbf{x}+b,代入损失函数中可得:Lw,b=1mi=1m[y(i)ln(1+ef(x(i)))+(1y(i))ln(1+ef(x(i)))]=1mi=1mln(1+e(1)y(i)f(x(i)))L_{\mathbf{w},b}=\frac{1}{m}\sum\limits_{i=1}^m[y^{(i)}\ln(1+e^{-f(\mathbf{x}^{(i)})})+(1-y^{(i)})\ln(1+e^{f(\mathbf{x}^{(i)})})]=\frac{1}{m}\sum\limits_{i=1}^m\ln(1+e^{(-1)^{y^{(i)}}f(\mathbf{x}^{(i)})}),如果正负类别标签你不是记作1100,而是111-1,那么损失将写为Lw,b=1mi=1mln(1+ey(i)f(x(i)))L_{\mathbf{w},b}=\frac{1}{m}\sum\limits_{i=1}^m\ln(1+e^{-y^{(i)}f(\mathbf{x}^{(i)})})

图4. Softmax多分类模型是一个单层神经网络[3]

Softmax(多)分类是Logistic(二)分类的一个推广,或者说后者是前者的一个特例。和线性回归模型相比,Softmax也是一个单层神经网络,只不过输出层有多个节点(等于类别数)并且加了一个sigmoid激活函数。相比于二分类寻找单个超平面,多分类会寻找多个超平面用于分割多种(大于2)不同的类别:w1x+b1=0\mathbf{w}_1\mathbf{x}+b_1=0w2x+b2=0\mathbf{w}_2\mathbf{x}+b_2=0、…,为了方便推导,我们将超平面wx+b=0\mathbf{w}\mathbf{x}+b=0中的参数部分合起来(本节推导参考[2]),记θ=[b,w]TR(n+1)×1\boldsymbol{\theta}=[b,\mathbf{w}]^T\in R^{(n+1)\times 1}x=[1,x1,...,xn]R(n+1)×1\mathbf{x}=[1,x_1,...,x_n]\in R^{(n+1)\times 1},于是超平面变为θTx=0\boldsymbol{\theta}^T\mathbf{x}=0,假设是kk分类,全部未知参数记作Θ=[θ1,θ2,...,θk]R(n+1)×k\Theta=[\boldsymbol{\theta}_1,\boldsymbol{\theta}_2,...,\boldsymbol{\theta}_k]\in R^{(n+1)\times k},现给出Softmax分类预测模型:y^=hΘ(x)=softmax(ΘTx)Rk×1\hat{\mathbf{y}}=h_\Theta(\mathbf{x})=\textbf{softmax}(\Theta^T\mathbf{x})\in R^{k\times 1},其中softmax(x)=exp(x)exp(x)\textbf{softmax}(\mathbf{x})=\frac{\exp(\mathbf{x})}{\sum\exp(\mathbf{x})}(分母表示向量求和),其批量(mm个样本)损失函数直接参照二分类,有LΘ=1mi=1mj=1kyj(i)ln(y^j(i))L_\Theta=-\frac{1}{m}\sum\limits_{i=1}^m\sum\limits_{j=1}^k\mathbf{y}_j^{(i)}\ln(\hat{\mathbf{y}}_j^{(i)}),其中y(i)\mathbf{y}^{(i)}是样本x(i)\mathbf{x}^{(i)}所属标量类别y(i)y^{(i)}的one-hot编码向量)

Softmax(多)分类是Logistic(二)分类的一个推广,或者说后者是前者的一个特例。和线性回归模型相比,Softmax也是一个单层神经网络,只不过输出层有多个节点(等于类别数)并且加了一个sigmoid激活函数。相比于二分类寻找单个超平面,多分类会寻找多个超平面用于分割多种(大于2)不同的类别:w1x+b1=0\mathbf{w}_1\mathbf{x}+b_1=0w2x+b2=0\mathbf{w}_2\mathbf{x}+b_2=0、…,为了方便推导,我们将超平面wx+b=0\mathbf{w}\mathbf{x}+b=0中的参数部分合起来(本节推导参考[2],下面将纠正其中损失函数求偏导部分的一个因符号混乱引发的错误),记θ=[b,w]TR(n+1)×1\boldsymbol{\theta}=[b,\mathbf{w}]^T\in R^{(n+1)\times 1}x=[1,x1,...,xn]R(n+1)×1\mathbf{x}=[1,x_1,...,x_n]\in R^{(n+1)\times 1},于是超平面变为θTx=0\boldsymbol{\theta}^T\mathbf{x}=0,假设是kk分类,全部未知参数记作Θ=[θ1,θ2,...,θk]R(n+1)×k\Theta=[\boldsymbol{\theta}_1,\boldsymbol{\theta}_2,...,\boldsymbol{\theta}_k]\in R^{(n+1)\times k},现给出Softmax分类预测模型:y^=hΘ(x)=1i=1keθiTx[eθ1Txeθ2TxeθkTx]=softmax(ΘTx)[P(y=1x,Θ)P(y=2x,Θ)P(y=kx,Θ)]Rk×1\hat{\mathbf{y}}=h_\Theta(\mathbf{x})=\frac{1}{\sum\limits_{i=1}^ke^{\boldsymbol{\theta}_i^T\mathbf{x}}}\begin{bmatrix}e^{\boldsymbol{\theta}_1^T\mathbf{x}}\\e^{\boldsymbol{\theta}_2^T\mathbf{x}}\\\vdots\\e^{\boldsymbol{\theta}_k^T\mathbf{x}}\end{bmatrix}=\textbf{softmax}(\Theta^T\mathbf{x})\equiv \begin{bmatrix}P(y=1|\mathbf{x},\Theta)\\P(y=2|\mathbf{x},\Theta)\\\vdots\\P(y=k|\mathbf{x},\Theta)\end{bmatrix}\in R^{k\times 1},其中softmax(x)=exp(x)exp(x)\textbf{softmax}(\mathbf{x})=\frac{\exp(\mathbf{x})}{\sum\exp(\mathbf{x})}(分母表示向量求和)

和Logistic分类的联系:假设k=2k=2,则hΘ(x)=[eθ1Txeθ1Tx+eθ2Txeθ2Txeθ1Tx+eθ2Tx]=[11+e(θ2θ1)Txv1v]h_\Theta(\mathbf{x})=\begin{bmatrix}\frac{e^{\boldsymbol{\theta}_1^T\mathbf{x}}}{e^{\boldsymbol{\theta}_1^T\mathbf{x}}+e^{\boldsymbol{\theta}_2^T\mathbf{x}}}\\\frac{e^{\boldsymbol{\theta}_2^T\mathbf{x}}}{e^{\boldsymbol{\theta}_1^T\mathbf{x}}+e^{\boldsymbol{\theta}_2^T\mathbf{x}}}\end{bmatrix}=\begin{bmatrix}\frac{1}{1+e^{(\boldsymbol{\theta}_2-\boldsymbol{\theta}_1)^T\mathbf{x}}}\equiv v\\1-v\end{bmatrix},其中vv不就是σ((θ2θ1)Tx)\sigma((\boldsymbol{\theta}_2-\boldsymbol{\theta}_1)^T\mathbf{x})么,这正是Logistic分类预测模型的表达形式

至于Softmax分类模型的损失函数,对于mm个批量数据,直接仿照Logistic分类,给出总体损失函数:LΘ=1mi=1mj=1k[I{y(i)=j}lneθjTx(i)l=1keθlTx(i)]L_\Theta=-\frac{1}{m}\sum\limits_{i=1}^m\sum\limits_{j=1}^k[I\{y^{(i)}=j\}\ln\frac{e^{\boldsymbol{\theta}_j^T\mathbf{x}^{(i)}}}{\sum\limits_{l=1}^ke^{\boldsymbol{\theta}_l^T\mathbf{x}^{(i)}}}],其中II为指示函数,I{condition}={1,if condition is true0,if condition is falseI\{\text{condition}\}=\begin{cases}1,\text{if condition is true}\\0,\text{if condition is false}\end{cases}

参考书[2]错误纠正

对书中P29求导部分的更正:
θqLΘ=1mi=1mj=1k[I{y(i)=j}θq(lneθjTx(i)l=1keθlTx(i))]\nabla_{\boldsymbol{\theta}_q}^{L_\Theta}=-\frac{1}{m}\sum\limits_{i=1}^m\sum\limits_{j=1}^k[I\{y^{(i)}=j\}\frac{\partial}{\partial \boldsymbol{\theta}_q}(\ln\frac{e^{\boldsymbol{\theta}_j^T\mathbf{x}^{(i)}}}{\sum\limits_{l=1}^ke^{\boldsymbol{\theta}_l^T\mathbf{x}^{(i)}}})],由ln\ln函数求导规则ddxln(u)=1ududx\frac{d}{dx}\ln(u)=\frac{1}{u}\frac{du}{dx}以及分数求导(uv)=uvuvv2(\frac{u}{v}){'}=\frac{u{'}v-uv{'}}{v^2}以及eθjTx(i)θq={eθjTx(i)x(i),q=j0,qj=eθjTx(i)I{q=j}x(i)\frac{\partial e^{\boldsymbol{\theta}_j^T\mathbf{x}^{(i)}}}{\partial \boldsymbol{\theta}_q}=\begin{cases}e^{\boldsymbol{\theta}_j^T\mathbf{x}^{(i)}}\mathbf{x}^{(i)},q=j\\0,q\ne j\end{cases}=e^{\boldsymbol{\theta}_j^T\mathbf{x}^{(i)}}I\{q=j\}\mathbf{x}^{(i)}θqLΘ=1mi=1mj=1k[I{y(i)=j}l=1keθlTx(i)eθjTx(i)eθjTx(i)I{q=j}x(i)l=1keθlTx(i)eθjTx(i)x(i)eθqTx(i)(l=1keθlTx(i))2]=1mi=1mj=1k[I{y(i)=j}I{q=j}l=1keθlTx(i)eθqTx(i)l=1keθlTx(i)x(i)]=1mi=1m[I{q=y(i)}l=1keθlTx(i)eθqTx(i)l=1keθlTx(i)x(i)]=1mi=1m[(I{y(i)=q}P(y=qx(i),Θ))x(i)]\nabla_{\boldsymbol{\theta}_q}^{L_\Theta}=-\frac{1}{m}\sum\limits_{i=1}^m\sum\limits_{j=1}^k[I\{y^{(i)}=j\}\frac{\sum\limits_{l=1}^ke^{\boldsymbol{\theta}_l^T\mathbf{x}^{(i)}}}{e^{\boldsymbol{\theta}_j^T\mathbf{x}^{(i)}}}\frac{e^{\boldsymbol{\theta}_j^T\mathbf{x}^{(i)}}I\{q=j\}\mathbf{x}^{(i)}\sum\limits_{l=1}^ke^{\boldsymbol{\theta}_l^T\mathbf{x}^{(i)}}-e^{\boldsymbol{\theta}_j^T\mathbf{x}^{(i)}}\mathbf{x}^{(i)}e^{\boldsymbol{\theta}_q^T\mathbf{x}^{(i)}}}{(\sum\limits_{l=1}^ke^{\boldsymbol{\theta}_l^T\mathbf{x}^{(i)}})^2}]=-\frac{1}{m}\sum\limits_{i=1}^m\sum\limits_{j=1}^k[I\{y^{(i)}=j\}\frac{I\{q=j\}\sum\limits_{l=1}^ke^{\boldsymbol{\theta}_l^T\mathbf{x}^{(i)}}-e^{\boldsymbol{\theta}_q^T\mathbf{x}^{(i)}}}{\sum\limits_{l=1}^ke^{\boldsymbol{\theta}_l^T\mathbf{x}^{(i)}}}\mathbf{x}^{(i)}]=-\frac{1}{m}\sum\limits_{i=1}^m[\frac{I\{q=y^{(i)}\}\sum\limits_{l=1}^ke^{\boldsymbol{\theta}_l^T\mathbf{x}^{(i)}}-e^{\boldsymbol{\theta}_q^T\mathbf{x}^{(i)}}}{\sum\limits_{l=1}^ke^{\boldsymbol{\theta}_l^T\mathbf{x}^{(i)}}}\mathbf{x}^{(i)}]=-\frac{1}{m}\sum\limits_{i=1}^m[(I\{y^{(i)}=q\}-P(y=q|\mathbf{x}^{(i)},\Theta))\mathbf{x}^{(i)}]

上述给出的Softmax分类模型的损失其实就是所谓的交叉熵损失,下面给出简洁且常见的表示形式,对于单样本,其kk分类预测值为y^\hat{\mathbf{y}},真实值为y\mathbf{y}(所属标量类别yy经one-hot编码所得向量),于是有交叉熵损失:lΘ=i=1kyiln(y^i)l_\Theta=-\sum\limits_{i=1}^k\mathbf{y}_i\ln(\hat{\mathbf{y}}_i),对于mm个批量数据的总体交叉熵损失,求和取平均得到LΘ=1mi=1mj=1kyj(i)ln(y^j(i))=1mi=1m(y(i))Tln(y^(i))=1mtr([y(1),...,y(m)]T[ln(y^(1)),...,ln(y^(m))])L_\Theta=-\frac{1}{m}\sum\limits_{i=1}^m\sum\limits_{j=1}^k\mathbf{y}_j^{(i)}\ln(\hat{\mathbf{y}}_j^{(i)})=-\frac{1}{m}\sum\limits_{i=1}^m(\mathbf{y}^{(i)})^T\ln(\hat{\mathbf{y}}^{(i)})=-\frac{1}{m}tr([\mathbf{y}^{(1)},...,\mathbf{y}^{(m)}]^T[\ln(\hat{\mathbf{y}}^{(1)}),...,\ln(\hat{\mathbf{y}}^{(m)})]),通过最小化该损失函数,可以使样本的预测概率分布y^(i)\hat{\mathbf{y}}^{(i)}尽可能接近真实的标签概率分布y(i)\mathbf{y}^{(i)},其中i{1,...,m}i\in \{1,...,m\}(某些地方还会看到另一种写法,若记f(x)=ΘTxf(\mathbf{x})=\Theta^T\mathbf{x},称为“logit”,代入损失函数中可得:LΘ=1mi=1mj=1kyj(i)ln(ef(x(i))jl=1kef(x(i))l)L_\Theta=-\frac{1}{m}\sum\limits_{i=1}^m\sum\limits_{j=1}^k\mathbf{y}_j^{(i)}\ln(\frac{e^{f(\mathbf{x}^{(i)})_j}}{\sum_{l=1}^ke^{f(\mathbf{x}^{(i)})_l}}),由于y(i)\mathbf{y}^{(i)}是标量类别y(i)y^{(i)}的one-hot编码,因此y(i)\mathbf{y}^{(i)}只有在第y(i)y^{(i)}个下标位置处值为1,其余皆为0,因此损失函数继续等于1mi=1mln(1+l=1,ly(i)kef(x(i))lef(x(i))y(i))\frac{1}{m}\sum\limits_{i=1}^m\ln(1+\frac{\sum_{l=1,l\ne y^{(i)}}^ke^{f(\mathbf{x}^{(i)})_l}}{e^{f(\mathbf{x}^{(i)})_{y^{(i)}}}})

编程实现:

和线性回归示例一样,需要定义网络模型、定义损失函数、定义优化算法三大步骤。(批量mm个训练数据XRd×m\mathbf{X}\in R^{d\times m},且为kk分类)前向传播:Y^k×m=softmax(Wd×kTXd×m+Bk×1)\hat{\mathbf{Y}}_{k\times m}=\textbf{softmax}(\mathbf{W}_{d\times k}^T\mathbf{X}_{d\times m}+\mathbf{B}_{k\times 1})(上面给出了softmax\textbf{softmax}函数的定义,处理的向量,此处虽然接收的一个矩阵,但是是逐列分别计算的),损失函数:LW,B=1mtr(Yk×mTln(Y^k×m))L_{W,B}=-\frac{1}{m}tr(\mathbf{Y}_{k\times m}^T\ln(\hat{\mathbf{Y}}_{k\times m})),优化算法:小批量随机梯度下降

首先通过torchvision.datasets模块获取Fashion-MNIST图像分类数据集,并利用transforms.ToTensor()函数将所有图像数据(numpy对象,[0255][0-255]Uint8类型)转换成Tensor张量对象,且数值为介于[0.01.0][0.0-1.0]torch.float32类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
import torchvision
import torchvision.transforms as transforms

mnist_train = torchvision.datasets.FashionMNIST(root='./data/FashionMNIST', train=True, download=True, transform=transforms.ToTensor())
mnist_test = torchvision.datasets.FashionMNIST(root='./data/FashionMNIST', train=False, download=True, transform=transforms.ToTensor())
print(len(mnist_train),len(mnist_test))
feature,label=mnist_train[0] #通过下标索引第一个训练图像
print(feature.shape,label) #需要注意的是,类别是标量数据,后面训练的时候我再转换成one-hot编码形式

'''OUTPUT
60000 10000
torch.Size([1, 28, 28]) 9
'''

训练集和测试集中的每个类别的图像数分别为6,000和1,000,统共10个类别,为t-shirt(T恤)、trouser(裤子)、pullover(套衫)、dress(连衣裙)、coat(外套)、sandal(凉鞋)、shirt(衬衫)、sneaker(运动鞋)、bag(包)和ankle boot(短靴),因此总计有70,000张图像。图像数据的形状为(c,h,w),三个符号分别代表通道数、高度、宽度,根据上述输出可知,此处图像是单通道的

可视化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import matplotlib.pyplot as plt

def get_fashion_mnist_labels(labels):
text_labels = ['t-shirt', 'trouser', 'pullover', 'dress', 'coat',
'sandal', 'shirt', 'sneaker', 'bag', 'ankle boot']
return [text_labels[int(i)] for i in labels]

# 定义一个可以在一行里画出多张图像和对应标签的函数
def show_fashion_mnist(images,labels):
for i in range(len(images)):
ax=plt.subplot(1,len(images),i+1)
plt.imshow(np.squeeze(images[i].numpy()).reshape(28,28),cmap='gray')
plt.xlabel(get_fashion_mnist_labels([labels[i]])[0])
plt.xticks([])
plt.yticks([])
plt.show()

X, y = [], []
for i in range(5):
X.append(mnist_train[i][0])
y.append(mnist_train[i][1])
show_fashion_mnist(X,y)
图5. Fashion-MNIST数据集图例

首先定义前向传播时要用到的softmax函数:

1
2
3
def softmax(WX):
WX=WX.exp()
return WX/WX.sum()

再定义模型、损失和优化函数:

1
2
3
4
5
6
7
8
9
10
11
12
### 定义网络模型
def softmax_classify(X,W,B):
return softmax(pt.t(W).mm(X)+B)

### 定义损失函数
def cross_loss(Y_hat,Y):
return -pt.trace(pt.t(Y).mm(pt.log(Y_hat))) #注意这里并未除以m(批量数),我们会在更新参数的时候再做,同线性回归示例

### 定义优化算法
def sgd(params,learn_rate,batch_sise): #同线性回归示例
for param in params:
param.data-=(learn_rate*param.grad/batch_size)

在训练前还要准备一些东西:批量数据生成器、one-hot编码以及参数初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import time

#批量读取数据。mnist_train对象是torch.utils.data.Dataset的子类,所以我们可以将其传入torch.utils.data.DataLoader来创建一个读取小批量数据样本的DataLoader实例。PyTorch的DataLoader一个很好的功能是允许使用多进程来加速数据读取,这里我们通过参数num_workers来设置4个进程读取数据
batch_size = 256 #自定义批量尺寸
num_workers = 4 #0则表示不用额外的进程来加速读取数据
train_iter = pt.utils.data.DataLoader(mnist_train, batch_size=batch_size, shuffle=True, num_workers=num_workers) #定义批量数据迭代器
test_iter = pt.utils.data.DataLoader(mnist_test, batch_size=batch_size, shuffle=False, num_workers=num_workers)
#此处查看读取一遍训练数据需要的时间(作为对比,num_workers=0需4.64s,num_workers=4需要1.99s)
start = time.time()
for X, y in train_iter:
continue
print('%.2f sec' % (time.time() - start))

# labels转换为one-hot编码
def to_onehot(labels,N): #N为类别数(labels中标签标量取值范围应为[0,N-1])
return (pt.arange(N).view(N,1)==labels).to(pt.float32) #形状为(N,len(labels))

### 初始化模型参数
def init_params(d,k): #d是图像样本展开后的维度,也就是输入层的节点数量,k是分类数,即输出层节点数量
W=pt.tensor(np.random.normal(0,0.01,(d,k)),dtype=pt.float32)
B=pt.zeros(k,1,dtype=pt.float32)
W.requires_grad_(requires_grad=True)
B.requires_grad_(requires_grad=True)
return W,B

为了评判模型在测试集上的性能,在每一轮epoch结束后都会计算预测准确率,对于批量测试数据,网络的输出Y^\hat{\mathbf{Y}}是一个矩阵,每一列代表其中一个样本的预测概率分布,其中最大值所在位置就是预测的类别,同时已知的测试集真实值标签Y\mathbf{Y},和Y^\hat{\mathbf{Y}}是一个同形矩阵,含义一样,每一列都是其中一个样本的one-hot编码,于是精度计算代码就是(Y^.argmax(dim=0)==Y.argmax(dim=0)).mean()(\hat{\mathbf{Y}}.argmax(dim=0)==\mathbf{Y}.argmax(dim=0)).mean()

1
2
3
4
5
6
7
8
9
10
11
#计算单个批次数据的精确率
def acc(Y_hat,Y):
return (Y_hat.argmax(dim=0)==Y.argmax(dim=0)).float().mean().item()

#计算多个批次数据的精确率
def calc_accuracy(data_iter, net_with_wb):
acc_sum, n = 0.0, 0
for X, y in data_iter:
acc_sum += (net_with_wb(pt.t(X.view(-1,784))).argmax(dim=0)==y).float().sum().item()
n += len(y)
return acc_sum / n

开始训练:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from functools import partial

### 训练过程
start_time=time.time()
epoches=10 #自定义迭代周期
learn_rate=0.1 #自定义学习率
W,B=init_params(784,10) #784是图像一维化展开后的长度
for epoch in range(epoches):
for mini_samples,mini_labels in train_iter:
mini_X=pt.t(mini_samples.view(-1,784)) #将小批量训练数据转换到(d,batch_size)二维形状
mini_Y=to_onehot(mini_labels,10) #将标签向量转换成one-hot编码
mini_Y_hat=softmax_classify(mini_X,W,B) #前向传播
loss=cross_loss(mini_Y_hat,mini_Y) #得到损失
loss.backward() #反向传播求偏导
sgd((W,B),learn_rate,batch_size) #根据偏导以及梯度下降算法更新网络参数
_=W.grad.data.zero_()
_=B.grad.data.zero_()
print('epoch=%d,train_loss=%f,test_accuracy=%f'%(epoch+1,loss.data.item(),calc_accuracy(test_iter,partial(softmax_classify,W=W,B=B))))
print('Time: %f'%(time.time()-start_time))

'''OUTPUT
epoch=1,train_loss=513.109375,test_accuracy=0.795100
epoch=2,train_loss=505.963837,test_accuracy=0.811600
epoch=3,train_loss=506.121246,test_accuracy=0.819600
epoch=4,train_loss=499.465485,test_accuracy=0.821300
epoch=5,train_loss=491.453918,test_accuracy=0.823300
epoch=6,train_loss=500.663055,test_accuracy=0.826000
epoch=7,train_loss=496.945709,test_accuracy=0.820800
epoch=8,train_loss=493.426422,test_accuracy=0.830900
epoch=9,train_loss=493.445496,test_accuracy=0.829300
epoch=10,train_loss=494.146027,test_accuracy=0.825700
Time: 33.636117
'''

编写预测函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
### 预测函数
def predict(images,true_labels):
t=pt.cat(images,0)
t=t.view(t.shape[0],-1)
y=softmax_classify(pt.t(t),W,B).argmax(dim=0)
print('预测结果:')
show_fashion_mnist(images,y)
print('真实标签:',get_fashion_mnist_labels(true_labels))

X, y = [], []
for i in pt.randint(0,10000,(8,)):
X.append(mnist_test[i][0])
y.append(mnist_test[i][1])
predict(X,y)

比较简单,直接看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import torch as pt
import numpy as np

### 一些参数设置
batch_size=128
num_inputs=784
num_outputs=10

### 获取fashion-mnist训练数据以及小批量生成器
train_iter,test_iter=load_data_fashion_mnist(batch_size,root='./data/FashionMNIST') #注意迭代产生的训练样本的形状为(batch_size,1,h,w),标签形状为(batch_size,)

### 定义网络模型,定义展开层和线性层而无需定义softmax层(因为pytorch的softmax交叉熵损失nn.CrossEntropyLoss完成了这一步)
import torch.nn as nn
net=nn.Sequential(FlattenLayer(),nn.Linear(num_inputs,num_outputs))

### 定义损失函数
#分开定义softmax运算和交叉熵损失函数可能会造成数值不稳定。因此,PyTorch提供了一个包括softmax运算和交叉熵损失计算的函数。它的数值稳定性更好
loss=nn.CrossEntropyLoss()

### 定义优化算法
optimizer=pt.optim.SGD(net.parameters(), lr=0.1)

### 训练
from torch.nn import init
_=init.normal_(net[1].weight, mean=0, std=0.01) #初始化权重
_=init.constant_(net[1].bias, val=0)
num_epochs = 5
train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size, None, None, optimizer)

上述代码用到了几个别人写好的功能函数(后面还会用到,写在d2l.py中,作为模块调用):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import torch
import torchvision
import sys

def load_data_fashion_mnist(batch_size, resize=None, root='~/Datasets/FashionMNIST'):
"""Download the fashion mnist dataset and then load into memory."""
trans = []
if resize:
trans.append(torchvision.transforms.Resize(size=resize))
trans.append(torchvision.transforms.ToTensor())

transform = torchvision.transforms.Compose(trans)
mnist_train = torchvision.datasets.FashionMNIST(root=root, train=True, download=True, transform=transform)
mnist_test = torchvision.datasets.FashionMNIST(root=root, train=False, download=True, transform=transform)
if sys.platform.startswith('win'):
num_workers = 4 # 0表示不用额外的进程来加速读取数据
else:
num_workers = 6
train_iter = torch.utils.data.DataLoader(mnist_train, batch_size=batch_size, shuffle=True, num_workers=num_workers)
test_iter = torch.utils.data.DataLoader(mnist_test, batch_size=batch_size, shuffle=False, num_workers=num_workers)

return train_iter, test_iter

class FlattenLayer(torch.nn.Module):
def __init__(self):
super(FlattenLayer, self).__init__()
def forward(self, x): # x shape: (batch, *, *, ...)
return x.view(x.shape[0], -1)

def sgd(params, lr, batch_size):
# 为了和原书保持一致,这里除以了batch_size,但是应该是不用除的,因为一般用PyTorch计算loss时就默认已经
# 沿batch维求了平均了。
for param in params:
param.data -= lr * param.grad / batch_size # 注意这里更改param时用的param.data

def evaluate_accuracy_ch3(data_iter, net):
acc_sum, n = 0.0, 0
for X, y in data_iter:
acc_sum += (net(X).argmax(dim=1) == y).float().sum().item()
n += y.shape[0]
return acc_sum / n

def train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size,
params=None, lr=None, optimizer=None):
for epoch in range(num_epochs):
train_l_sum, train_acc_sum, n = 0.0, 0.0, 0
for X, y in train_iter:
y_hat = net(X)
l = loss(y_hat, y).sum()

# 梯度清零
if optimizer is not None:
optimizer.zero_grad()
elif params is not None and params[0].grad is not None:
for param in params:
param.grad.data.zero_()

l.backward()
if optimizer is None:
sgd(params, lr, batch_size)
else:
optimizer.step() # “softmax回归的简洁实现”一节将用到

train_l_sum += l.item()
train_acc_sum += (y_hat.argmax(dim=1) == y).sum().item()
n += y.shape[0]
test_acc = evaluate_accuracy_ch3(test_iter, net)
print('epoch %d, loss %.4f, train acc %.3f, test acc %.3f'
% (epoch + 1, train_l_sum / n, train_acc_sum / n, test_acc))

MLP多层感知机

简要回顾:

图6. 含单个隐层的多层感知机[3]

和线性回归、Softmax多分类相比,MLP增加了隐藏层的概念。给定批量nn个数据,对于任意第ll层节点,有Zl=(Wl1)TAl1+Bl\mathbf{Z}^l=(\mathbf{W}^{l-1})^T\mathbf{A}^{l-1}+\mathbf{B}^lAl=σ(Zl)\mathbf{A}^{l}=\sigma(\mathbf{Z}^l),其中Al1RSl1×n\mathbf{A}^{l-1}\in R^{S_{l-1}\times n}为上一层的输出、当前层的输入,BlRSl×1\mathbf{B}^l\in R^{S_l\times 1}为当前层偏置(广播为形状Sl×nS_l\times n),ZlRSl×n\mathbf{Z}^l\in R^{S_l\times n}为当前层的输入加权和,Wl1RSl1×Sl\mathbf{W}^{l-1}\in R^{S_{l-1}\times S_l}的第ii列代表第l1l-1层所有节点和第ll层第ii个节点的连线权重,AlRSl×n\mathbf{A}^l\in R^{S_l\times n}为当前层的输出,SlS_l为第ll层节点数,σ()\sigma(·)为激活函数,包括但不限于Sigmoid函数(sigmoid(x)=11+exp(x)sigmoid(x)=\frac{1}{1+exp(-x)})、ReLU函数(relu(x)=max(x,0)relu(x)=max(x,0))以及Tanh函数(tanh(x)=1exp(2x)1+exp(2x)tanh(x)=\frac{1-exp(-2x)}{1+exp(-2x)}

图7. 常见激活函数

编程实现:
以单隐层为例,模型总计三层,第一层是输入层,第二层是隐藏层,第三层是输出层,由于是多分类问题,所以第三层后面还要接一个Softmax层,注意pytorch为了数值稳定性(具体我也不清楚),前向传播时最后的输出并不会执行Softmax运算,而是留到计算交叉熵损失的时候再算(torch.nn.CrossEntropyLoss()),由于torch.utils.data.DataLoader迭代产生的批量样本数据的形状是(batch_size,dim),前述计算式中则是基于相反的形状,所以为了适用于已经定义好的d2l.train_ch3()等方法,我稍稍修改了该单隐层网络的前向传播计算式:Zn×S22=Xn×S1WS1×S21+B1×S22\mathbf{Z}_{n\times S_2}^2=\mathbf{X}_{n\times S_1}\mathbf{W}_{S_1\times S_2}^1+\mathbf{B}_{1\times S_2}^2An×S22=σ(Zn×S22)\mathbf{A}_{n\times S_2}^2=\sigma(\mathbf{Z}_{n\times S_2}^2)Zn×S33=An×S22WS2×S32+B1×S33\mathbf{Z}_{n\times S_3}^3=\mathbf{A}_{n\times S_2}^2\mathbf{W}_{S2\times S_3}^2+\mathbf{B}_{1\times S_3}^3Y^=softmax(Zn×S33)\hat{\mathbf{Y}}=\sout{\textbf{softmax}}(\mathbf{Z}_{n\times S_3}^3)

仍使用Fashion-MNIST数据集,同Softmax多分类

首先定义激活函数,作为网络中的激活层:

1
2
3
4
5
6
7
8
### 定义激活函数
import torch as pt
def sigmoid(Z):
return 1/(1+pt.exp(-Z))
def relu(Z):
return pt.max(Z,other=pt.Tensor([0.0])) #尽管ReLU并不完全处处可导,但是从实验看pytorch的自动求偏导机制并不会因此出错
def tanh(Z):
return (1-pt.exp(-2*Z))/(1+pt.exp(-2*Z))

优化函数当然还是简单的随机梯度下降,而将要调用的训练方法d2l.train_ch3()中已经定义了SGD过程:

1
2
3
4
5
6
7
8
### 定义网络模型
def MLP_1(X,W1,W2,B1,B2,activate):
X=X.view((-1,784)) #注意fashion-mnist的批量数据形状为(batch_size,1,28,28),形状重塑为(batch_sise,784)
return activate(X.mm(W1)+B1).mm(W2)+B2

### 定义损失函数
import torch.nn as nn
loss=nn.CrossEntropyLoss() #为了得到更好的数值稳定性,我们直接使用PyTorch提供的包括softmax运算和交叉熵损失计算的函数

训练前还是先准备数据生成器并作参数初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
### 获取fashion-mnist训练数据以及小批量生成器
import d2l
batch_size=256
train_iter,test_iter=d2l.load_data_fashion_mnist(batch_size,root='./data/FashionMNIST')

### 初始化模型参数
num_inputs=784 #输入层节点数
num_hiddens=256 #隐层节点数
num_outputs=10 #输出层节点数
W1 = pt.tensor(np.random.normal(0, 0.01, (num_inputs, num_hiddens)), dtype=pt.float)
B1 = pt.zeros(num_hiddens, dtype=pt.float)
W2 = pt.tensor(np.random.normal(0, 0.01, (num_hiddens, num_outputs)), dtype=pt.float)
B2 = pt.zeros(num_outputs, dtype=pt.float)
#设置权重/偏置参数可导
params = [W1, B1, W2, B2]
for param in params:
_=param.requires_grad_(requires_grad=True)

开始训练:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
### 训练
from functools import partial
num_epochs = 5
lr = 100.0 #注意交叉熵中已经除以了batch_size,而sgd中又除以batch_size,所以这边学习率设大一点以抵消其中一个除法
net = partial(MLP_1,W1=W1,W2=W2,B1=B1,B2=B2,activate=relu)
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size, params, lr)

'''OUTPUT
epoch 1, loss 0.0030, train acc 0.709, test acc 0.737
epoch 2, loss 0.0019, train acc 0.824, test acc 0.787
epoch 3, loss 0.0017, train acc 0.845, test acc 0.795
epoch 4, loss 0.0016, train acc 0.855, test acc 0.854
epoch 5, loss 0.0015, train acc 0.864, test acc 0.816
'''

train_iter,test_iter,loss都复用自前面的代码,此处简洁实现只要重新定义网络部分即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
### 定义模型
num_inputs, num_outputs, num_hiddens = 784, 10, 256
net=nn.Sequential(
d2l.FlattenLayer(),
nn.Linear(num_inputs,num_hiddens),
nn.ReLU(),
nn.Linear(num_hiddens,num_outputs)
)
#初始化模型参数
from torch.nn import init
for params in net.parameters():
_=init.normal_(params, mean=0, std=0.01)

### 定义损失函数、优化算法并训练模型
optimizer = pt.optim.SGD(net.parameters(), lr=0.5) #优化算法
num_epochs = 5
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size, None, None, optimizer)

'''OUTPUT
epoch 1, loss 0.0030, train acc 0.708, test acc 0.700
epoch 2, loss 0.0019, train acc 0.821, test acc 0.745
epoch 3, loss 0.0017, train acc 0.842, test acc 0.829
epoch 4, loss 0.0015, train acc 0.856, test acc 0.858
epoch 5, loss 0.0014, train acc 0.865, test acc 0.846
'''