本节将从梯度下降法向外拓展,介绍更常用的优化算法,实现神经网络的学习和迭代。在本节课结束将完整实现一个神经网络训练的全流程。
对于像神经网络这样的复杂模型,可能会有数百个
w
w
w的存在,同时如果我们使用的是像交叉熵这样复杂的损失函数(机器学习中还有更多更加复杂的函数),令所有权重的导数为0
并一个个求解方程的难度很大、工作量也很大。因此我们转换思路,不追求一步到位,而使用 迭代 的方式逐渐接近损失函数的最小值。这就是优化算法的具体工作,优化算法的相关知识也都是关于“逐步迭代到损失函数最小值”的具体操作。
41 梯度下降中的两个关键问题
简单复习一下梯度下降的核心流程。观察SSE
和交叉熵损失:
C
E
L
o
s
s
=
−
∑
i
=
1
m
y
i
(
k
=
j
)
ln
σ
i
其中
σ
=
s
o
f
t
m
a
x
(
z
)
,
z
=
W
X
S
S
E
=
1
m
∑
i
=
1
m
(
z
i
−
z
^
i
)
2
其中
z
=
X
w
CELoss = -\sum_{i=1}^{m} y_{i(k=j)} \ln \sigma_i \quad \text{其中} \quad \sigma = softmax(z), \quad z = WX \\ SSE = \frac{1}{m} \sum_{i=1}^{m} (z_i - \hat{z}_i)^2 \quad \text{其中} \quad z = Xw
CELoss=−i=1∑myi(k=j)lnσi其中σ=softmax(z),z=WXSSE=m1i=1∑m(zi−z^i)2其中z=Xw
不难发现,任意损失函数中总是包含特征张量
X
X
X,真实标签
y
y
y或
z
z
z,权重矩阵或权重向量
w
w
w三个元素,其中特征张量
X
X
X和真实标签来自我们的数据,所以只要给定一组权重
w
w
w,就一定能够得出损失函数的具体数值。
假设数据只有一个标签,因此只有一个权重 w w w,我们常常会绘制以 w w w为横坐标, L ( w ) L(w) L(w)为纵坐标的图像:
在梯度下降最开始时,我们会随机设定初始权重 w ( 0 ) w_{(0)} w(0),对应的纵坐标 L ( w 0 ) L(w_{0}) L(w0)就是初始的损失函数值,坐标点( w ( 0 ) , L ( w ( 0 ) ) w_{(0)},L(w_{(0)}) w(0),L(w(0)))就是梯度下降的起始点。
接下来,我们从起始点开始,让自变量 w w w向损失函数 L ( w ) L(w) L(w)减小最快的方向移动 。以上方的图像为例,我们一眼就能看出减小损失函数最快的方向是横坐标的右侧(也就是 w w w逐渐变大的方向),但起始点是一个“盲人”,它看不到也听不到全局,每次走之前得用“拐杖”探探路,确定下方向。
并且,对于复杂的图像而言(比如说下面这张图),最开始时 L ( w ) L(w) L(w)减小最快的方向(红色箭头指示的方向),不一定指向函数真正的最小值。如果一条路走到黑,最后反而可能找不到最小值点,所以”盲人“起始点每次只敢走一小段距离。每走一段距离,就要重新确认一下方向对不对,走很多步之后,才能够到达或接近损失函数的最小值。
在这个过程中,每步的方向就是当前坐标点对应的梯度向量的反方向 ,每步的举例就是步长×当前坐标点所对应的梯度向量的大小(也就是梯度向量的模长),梯度向量的性质保证了沿着其反方向、按照其大小进行移动,就能够接近损失函数的最小值。在这个过程中存在两个关键问题:
- 怎么找出梯度向量的方向和大小?
- 怎么让坐标点按照梯度向量的反方向,移动与梯度向量大小相等的距离?
41.1 找出梯度向量的方向和大小
梯度向量是多元函数上,各个自变量的偏导数组成的向量,比如损失函数是 L ( w 1 , w 2 , b ) L(w_1,w_2,b) L(w1,w2,b),在损失函数上对 w 1 , w 2 , b w_1,w_2,b w1,w2,b这三个自变量求偏导数,求得的梯度向量的表达式就是 [ ∂ L ∂ w 1 , ∂ L ∂ w 2 , ∂ L ∂ b ] T \left[ \frac{\partial L}{\partial w_1}, \frac{\partial L}{\partial w_2}, \frac{\partial L}{\partial b} \right]^T [∂w1∂L,∂w2∂L,∂b∂L]T,简写为 g r a d L ( w 1 , w 2 ) grad L(w_1,w_2) gradL(w1,w2)或者 ∇ L ( w 1 , w 2 ) \nabla L(w_1,w_2) ∇L(w1,w2)。
对于任意向量来说,其大小和方向都是依赖于向量元素具体的值而确定。比如向量(1,2)的方向就是从原点(0,0)指向点(1,2)的方向,大小就是 1 2 + 2 2 \sqrt{1^2+2^2} 12+22(向量的大小也叫模长),梯度向量的大小和方向也是如此计算。梯度向量中的具体元素就是各个自变量的偏导数,这些偏导数的具体值必须依赖于当前所在坐标点的值进行计算。 所以:
- 梯度的大小和方向对每个坐标点而言是独一无二的。坐标点一旦变化,梯度向量的方向和大小也会变化。
- 每走到一个新的点,读取该点的坐标并带入梯度向量的表达式进行计算,就可以得到当前点对应的梯度向量的方向和大小。
而我们现在的坐标空间是由损失函数 L ( w ) L(w) L(w)和权重 w w w构成,只要得到一组坐标值,就可以求解当前的梯度向量。这一步骤中,最大的难点在于如何获得梯度向量的表达式——也就是损失函数对各个自变量求偏导后的表达式。
41.2 让坐标点移动起来(进行一次迭代)
有了大小和方向,来看第二个问题:怎么让坐标点按照梯度向量的反方向,移动与梯度向量大小相等的距离?
假设现在我们有坐标点A(10, 7.5),向量 g ⃗ \vec{g} g为(5, 5),大小为 5 2 5\sqrt{2} 52。现在我们让点A向 g ⃗ \vec{g} g的反方向移动 5 2 5\sqrt{2} 52的距离,如图所示:
首先将向量和点都展示在同一坐标系中,然后找出向量反方向、同等长度的向量(红色箭头)。然后将反方向的向量平移,让其起点落在A点上。平移过后,反方向的向量所指向的新的点被命名为 ,从A点到A‘点的过程,就是让A向 g ⃗ \vec{g} g的反方向移动 5 2 5\sqrt{2} 52的距离的结果。不难发现,其实从A点移动到A’的过程,改变的是A的两个坐标值——两个坐标值分别被减去了5,就得到了A‘点。
现在,假设我们的初始点是(
w
1
(
0
)
,
w
2
(
0
)
w_{1(0)},w_{2(0)}
w1(0),w2(0)),梯度向量是(
∂
L
∂
w
1
,
∂
L
∂
w
2
\frac{\partial L}{\partial w_1}, \frac{\partial L}{\partial w_2}
∂w1∂L,∂w2∂L),那让坐标点按照梯度向量的反方向移动的方法如下:
w
1
(
1
)
=
w
1
(
0
)
−
∂
L
∂
w
1
w
2
(
1
)
=
w
2
(
0
)
−
∂
L
∂
w
2
w_{1(1)} = w_{1(0)} - \frac{\partial L}{\partial w_1} \\ w_{2(1)} = w_{2(0)} - \frac{\partial L}{\partial w_2}
w1(1)=w1(0)−∂w1∂Lw2(1)=w2(0)−∂w2∂L
将两个
w
w
w写在同一个权重向量里,用
t
t
t代表走到了第
t
t
t步(即进行第
t
t
t次迭代),则有:
w
(
t
+
1
)
=
w
(
t
)
−
∂
L
∂
w
w_{(t+1)} = w_{(t)} - \frac{\partial L}{\partial w}
w(t+1)=w(t)−∂w∂L
为了控制每次走的距离的大小,我们将步长
η
\eta
η(又叫做学习率)加入公式,就可以将上面的式子改写为:
w
(
t
+
1
)
=
w
(
t
)
−
η
∂
L
∂
w
w_{(t+1)} = w_{(t)} - \eta \frac{\partial L}{\partial w}
w(t+1)=w(t)−η∂w∂L
这就是我们迭代权重的迭代公式,其中偏导数的大小影响整体梯度向量的大小,偏导数前的减号影响整体梯度向量的方向。 当我们将 设置得很大,梯度下降的每一个步子就迈得很大,走得很快,当我们将步长设置较小,梯度下降就走得很慢。我们使用一个式子,就同时控制了大小和方向。
当然了,一旦迭代 ,我们的损失函数也会发生变化。在整个损失函数的图像上,红色箭头就是梯度向量的反方向,灰色箭头就是每个权重对应的迭代。
42 找出距离和方向:反向传播
42.1 反向传播的定义与价值
在梯度下降的最初,我们需要先找出坐标点对应的梯度向量。梯度向量是各个自变量求偏导后的表达式再带入坐标点计算出来的,在这一步骤中,最大的难点在于如何获得梯度向量的表达式——也就是损失函数对各个自变量求偏导后的表达式。在单层神经网络,例如逻辑回归(二分类单层神经网络)中,我们有如下计算:
其中BCEloss
是二分类交叉熵损失函数。在这个计算图中,从左向右计算以此的过程就是正向传播,因此进行以此计算后,我们会获得所有节点上的张量的值(z、sigma以及loss) 。根据梯度向量的定义,在这个计算过程中我们要求的是损失函数对
w
w
w的导数,所以求导过程需要涉及到的链路如下:
用公式来表示则为在以下式子上求解对
w
w
w的导数:
∂
L
o
s
s
∂
w
,
其中
L
o
s
s
=
−
∑
i
=
1
m
(
y
i
⋅
ln
(
σ
i
)
+
(
1
−
y
i
)
⋅
ln
(
1
−
σ
i
)
)
=
−
∑
i
=
1
m
(
y
i
⋅
ln
(
1
1
+
e
−
X
i
w
)
+
(
1
−
y
i
)
⋅
ln
(
1
−
1
1
+
e
−
X
i
w
)
)
\begin{align*} \frac{\partial Loss}{\partial w}, \text{其中} \\ Loss &= -\sum_{i=1}^{m} (y_i \cdot \ln(\sigma_i) + (1 - y_i) \cdot \ln(1 - \sigma_i)) \\ &= -\sum_{i=1}^{m} \left( y_i \cdot \ln\left(\frac{1}{1 + e^{-X_i w}}\right) + (1 - y_i) \cdot \ln\left(1 - \frac{1}{1 + e^{-X_i w}}\right) \right) \end{align*}
∂w∂Loss,其中Loss=−i=1∑m(yi⋅ln(σi)+(1−yi)⋅ln(1−σi))=−i=1∑m(yi⋅ln(1+e−Xiw1)+(1−yi)⋅ln(1−1+e−Xiw1))
可以看出,已经很复杂了。
更夸张的是,在双层的、各层激活函数都是sigmoid
的二分类神经网络上,我们有如下计算流程:
同样的,进行从左到右的正向传播之后,我们会获得所有节点上的张量。其中涉及到的求导链路如下:
用公式来表示,对
w
(
1
→
2
)
w^{(1\rightarrow2)}
w(1→2)我们有:
∂
L
o
s
s
∂
w
(
1
→
2
)
,
其中
L
o
s
s
=
−
∑
i
=
1
m
(
y
i
⋅
ln
(
σ
i
(
2
)
)
+
(
1
−
y
i
)
⋅
ln
(
1
−
σ
i
(
2
)
)
)
=
−
∑
i
=
1
m
(
y
i
⋅
ln
(
1
1
+
e
−
σ
i
(
1
)
w
(
1
→
2
)
)
+
(
1
−
y
i
)
⋅
ln
(
1
−
1
1
+
e
−
σ
i
(
1
)
w
(
1
→
2
)
)
)
\frac{\partial Loss}{\partial w^{(1\rightarrow 2)}}, \text{其中} \\ Loss = -\sum_{i=1}^{m} \left( y_i \cdot \ln(\sigma_i^{(2)}) + (1 - y_i) \cdot \ln(1 - \sigma_i^{(2)}) \right) \\ = -\sum_{i=1}^{m} \left( y_i \cdot \ln\left(\frac{1}{1 + e^{-\sigma_i^{(1)} w^{(1\rightarrow 2)}}}\right) + (1 - y_i) \cdot \ln\left(1 - \frac{1}{1 + e^{-\sigma_i^{(1)} w^{(1\rightarrow 2)}}}\right) \right)
∂w(1→2)∂Loss,其中Loss=−i=1∑m(yi⋅ln(σi(2))+(1−yi)⋅ln(1−σi(2)))=−i=1∑m(yi⋅ln(1+e−σi(1)w(1→2)1)+(1−yi)⋅ln(1−1+e−σi(1)w(1→2)1))
对
w
(
0
→
1
)
w^{(0\rightarrow1)}
w(0→1)我们有:
∂
L
o
s
s
∂
w
(
0
→
1
)
其中
L
o
s
s
=
−
∑
i
=
1
m
(
y
i
⋅
ln
(
σ
i
(
2
)
)
+
(
1
−
y
i
)
⋅
ln
(
1
−
σ
i
(
2
)
)
)
=
−
∑
i
=
1
m
(
y
i
⋅
ln
(
1
1
+
e
−
1
1
+
e
−
X
i
w
(
0
→
1
)
w
(
1
→
2
)
)
+
(
1
−
y
i
)
⋅
ln
(
1
−
1
1
+
e
−
1
1
+
e
−
X
i
w
(
0
→
1
)
w
(
1
→
2
)
)
)
\frac{\partial Loss}{\partial w^{(0 \rightarrow 1)}} \\ 其中 Loss = -\sum_{i=1}^{m}(y_i \cdot \ln(\sigma_i^{(2)}) + (1 - y_i) \cdot \ln(1 - \sigma_i^{(2)})) \\ = -\sum_{i=1}^{m}(y_i \cdot \ln(\frac{1}{1 + e^{-\frac{1}{1 + e^{-X_i w^{(0 \rightarrow 1)}} w^{(1 \rightarrow 2)}}}}) + (1 - y_i) \cdot \ln(1 - \frac{1}{1 + e^{-\frac{1}{1 + e^{-X_i w^{(0 \rightarrow 1)}} w^{(1 \rightarrow 2)}}}}))
∂w(0→1)∂Loss其中Loss=−i=1∑m(yi⋅ln(σi(2))+(1−yi)⋅ln(1−σi(2)))=−i=1∑m(yi⋅ln(1+e−1+e−Xiw(0→1)w(1→2)11)+(1−yi)⋅ln(1−1+e−1+e−Xiw(0→1)w(1→2)11))
对于需要对这个式子求导,大家感受如何?而这只是一个两层的二分类神经网络,对于复杂神经网络来说,所需要做得求导工作是无法想象的。
求导过程的复杂是神经网络历史上的一大难题,这个难题直到1986年才真正被解决。1986年,Rumelhart、Hinton和Williams提出了反向传播算法(Backpropagation algorithm,又叫做Delta法则),利用链式法则成功实现了复杂网络求导过程的简单化。
接下来,我们就来看看反向传播是怎么解决复杂求导问题的。
在高等数学中,存在着如下规则:
假设有函数
u
=
h
(
z
)
,
z
=
f
(
w
)
,
且两个函数在各自自变量的定义域上都可导,则有:
∂
u
∂
w
=
∂
u
∂
z
⋅
∂
z
∂
w
假设有函数u = h(z),z = f(w),且两个函数在各自自变量的定义域上都可导,则有: \\ \frac{\partial u}{\partial w} = \frac{\partial u}{\partial z} \cdot \frac{\partial z}{\partial w}
假设有函数u=h(z),z=f(w),且两个函数在各自自变量的定义域上都可导,则有:∂w∂u=∂z∂u⋅∂w∂z
感性(但不严谨)地来说,当一个函数是由多个函数嵌套而成,最外层函数向最内层自变量求导的值,等于外层函数对外层自变量求导的值 ***** 内层函数对内层自变量求导的值。 这就是链式法则。当函数之间存在复杂的嵌套关系,并且我们需要从最外层的函数向最内层的自变量求导时,链式法则可以让求导过程变得异常简单。
我们可以用链式法则将
∂
L
o
s
s
∂
w
(
1
→
2
)
\frac{\partial Loss}{\partial w^{(1\rightarrow 2)}}
∂w(1→2)∂Loss拆解为如下结构:
∂
L
o
s
s
∂
w
(
1
→
2
)
=
∂
L
(
σ
)
∂
σ
∗
∂
σ
(
z
)
∂
z
∗
∂
z
(
w
)
∂
w
其中,
∂
L
(
σ
)
∂
σ
=
∂
(
−
∑
i
=
1
m
(
y
i
∗
ln
(
σ
i
)
+
(
1
−
y
i
)
∗
ln
(
1
−
σ
i
)
)
)
∂
σ
=
∑
i
=
1
m
∂
(
−
(
y
i
∗
ln
(
σ
i
)
+
(
1
−
y
i
)
∗
ln
(
1
−
σ
i
)
)
)
∂
σ
求导不影响加和符号,因此暂时不看加和符号:
=
−
(
y
∗
1
σ
+
(
1
−
y
)
∗
1
1
−
σ
∗
(
−
1
)
)
=
−
(
y
σ
+
y
−
1
1
−
σ
)
=
−
(
y
(
1
−
σ
)
+
(
y
−
1
)
σ
σ
(
1
−
σ
)
)
=
−
(
y
−
y
σ
+
y
σ
−
σ
σ
(
1
−
σ
)
)
=
σ
−
y
σ
(
1
−
σ
)
\frac{\partial Loss}{\partial w^{(1 \rightarrow 2)}} = \frac{\partial L(\sigma)}{\partial \sigma} * \frac{\partial \sigma(z)}{\partial z} * \frac{\partial z(w)}{\partial w} \\ 其中, \\ \frac{\partial L(\sigma)}{\partial \sigma} = \frac{\partial \left(-\sum_{i=1}^{m}(y_i * \ln(\sigma_i) + (1 - y_i) * \ln(1 - \sigma_i))\right)}{\partial \sigma} \\ = \sum_{i=1}^{m} \frac{\partial \left(-(y_i * \ln(\sigma_i) + (1 - y_i) * \ln(1 - \sigma_i))\right)}{\partial \sigma} \\ 求导不影响加和符号,因此暂时不看加和符号: \\ = -(y * \frac{1}{\sigma} + (1 - y) * \frac{1}{1 - \sigma} * (-1)) \\ = -\left(\frac{y}{\sigma} + \frac{y - 1}{1 - \sigma}\right) \\ = -\left(\frac{y(1 - \sigma) + (y - 1)\sigma}{\sigma(1 - \sigma)}\right) \\ = -\left(\frac{y - y\sigma + y\sigma - \sigma}{\sigma(1 - \sigma)}\right) \\ = \frac{\sigma - y}{\sigma(1 - \sigma)}
∂w(1→2)∂Loss=∂σ∂L(σ)∗∂z∂σ(z)∗∂w∂z(w)其中,∂σ∂L(σ)=∂σ∂(−∑i=1m(yi∗ln(σi)+(1−yi)∗ln(1−σi)))=i=1∑m∂σ∂(−(yi∗ln(σi)+(1−yi)∗ln(1−σi)))求导不影响加和符号,因此暂时不看加和符号:=−(y∗σ1+(1−y)∗1−σ1∗(−1))=−(σy+1−σy−1)=−(σ(1−σ)y(1−σ)+(y−1)σ)=−(σ(1−σ)y−yσ+yσ−σ)=σ(1−σ)σ−y
假设我们已经进行过以此正向传播,那此时的
σ
\sigma
σ就是
σ
(
2
)
\sigma^{(2)}
σ(2),
y
y
y就是真实标签,我们可以很容易计算出
σ
−
y
σ
(
1
−
σ
)
\frac{\sigma - y}{\sigma(1 - \sigma)}
σ(1−σ)σ−y的数值。
再来看剩下的两部分:
∂
σ
(
z
)
∂
z
=
∂
1
1
+
e
−
z
∂
z
=
∂
(
1
+
e
−
z
)
−
1
∂
z
=
−
1
⋅
(
1
+
e
−
z
)
−
2
⋅
e
−
z
⋅
(
−
1
)
=
e
−
z
(
1
+
e
−
z
)
2
=
1
+
e
−
z
−
1
(
1
+
e
−
z
)
2
=
1
+
e
−
z
(
1
+
e
−
z
)
2
−
1
(
1
+
e
−
z
)
2
=
1
1
+
e
−
z
−
1
(
1
+
e
−
z
)
2
=
1
1
+
e
−
z
(
1
−
1
1
+
e
−
z
)
=
σ
(
1
−
σ
)
\frac{\partial \sigma(z)}{\partial z} = \frac{\partial \frac{1}{1 + e^{-z}}}{\partial z} \\ = \frac{\partial (1 + e^{-z})^{-1}}{\partial z} \\ = -1 \cdot (1 + e^{-z})^{-2} \cdot e^{-z} \cdot (-1) \\ = \frac{e^{-z}}{(1 + e^{-z})^2} \\ = \frac{1 + e^{-z} - 1}{(1 + e^{-z})^2} \\ = \frac{1 + e^{-z}}{(1 + e^{-z})^2} - \frac{1}{(1 + e^{-z})^2} \\ = \frac{1}{1 + e^{-z}} - \frac{1}{(1 + e^{-z})^2} \\ = \frac{1}{1 + e^{-z}} \left(1 - \frac{1}{1 + e^{-z}}\right) \\ = \sigma(1 - \sigma)
∂z∂σ(z)=∂z∂1+e−z1=∂z∂(1+e−z)−1=−1⋅(1+e−z)−2⋅e−z⋅(−1)=(1+e−z)2e−z=(1+e−z)21+e−z−1=(1+e−z)21+e−z−(1+e−z)21=1+e−z1−(1+e−z)21=1+e−z1(1−1+e−z1)=σ(1−σ)
此时的
σ
\sigma
σ还是
σ
(
2
)
\sigma^{(2)}
σ(2)。接着:
∂
z
(
w
)
∂
w
=
∂
σ
(
1
)
w
∂
w
=
σ
(
1
)
\frac{\partial z(w)}{\partial w} = \frac{\partial \sigma^{(1)} w}{\partial w} \\ = \sigma^{(1)}
∂w∂z(w)=∂w∂σ(1)w=σ(1)
对任意一个特征权重
w
w
w而言,
∂
z
(
w
)
∂
w
\frac{\partial z(w)}{\partial w}
∂w∂z(w)的值就等于其对应的输入值,所以如果是对于单层逻辑回归而言,这里的求导结果应该是
x
x
x。不过现在我们是对于双层神经网络的输出层而言,所以这个输入就是从中间层传过来的
σ
1
\sigma^1
σ1。现在将三个导数公式整合:
∂
L
o
s
s
∂
w
(
1
→
2
)
=
∂
L
(
σ
)
∂
σ
∗
∂
σ
(
z
)
∂
z
∗
∂
z
(
w
)
∂
w
=
σ
(
2
)
−
y
σ
2
(
1
−
σ
(
2
)
)
∗
σ
(
2
)
(
1
−
σ
(
2
)
)
∗
σ
(
1
)
=
σ
(
1
)
(
σ
(
2
)
−
y
)
\frac{\partial Loss}{\partial w^{(1 \rightarrow 2)}} = \frac{\partial L(\sigma)}{\partial \sigma} * \frac{\partial \sigma(z)}{\partial z} * \frac{\partial z(w)}{\partial w} \\ = \frac{\sigma^{(2)} - y}{\sigma^2 (1 - \sigma^{(2)})} * \sigma^{(2)} (1 - \sigma^{(2)}) * \sigma^{(1)} \\ = \sigma^{(1)} (\sigma^{(2)} - y)
∂w(1→2)∂Loss=∂σ∂L(σ)∗∂z∂σ(z)∗∂w∂z(w)=σ2(1−σ(2))σ(2)−y∗σ(2)(1−σ(2))∗σ(1)=σ(1)(σ(2)−y)
可以发现,将三个偏导数相乘之后,得到的最终的表达式其实非常简单。并且,其中所需要的数据都是我们在正向传播过程中已经计算出来的节点上的张量。同理,我们也可以得到对
w
(
0
→
1
)
w^{(0\rightarrow1)}
w(0→1)的导数。
根据链式法则,就有:
∂
L
o
s
s
∂
w
(
0
→
1
)
=
∂
L
(
σ
)
∂
σ
(
2
)
∗
∂
σ
(
z
)
∂
z
(
2
)
∗
∂
z
(
σ
)
∂
σ
(
1
)
∗
∂
σ
(
z
)
∂
z
(
1
)
∗
∂
z
(
w
)
∂
w
(
0
→
1
)
\frac{\partial Loss}{\partial w^{(0 \rightarrow 1)}} = \frac{\partial L(\sigma)}{\partial \sigma^{(2)}} * \frac{\partial \sigma(z)}{\partial z^{(2)}} * \frac{\partial z(\sigma)}{\partial \sigma^{(1)}} * \frac{\partial \sigma(z)}{\partial z^{(1)}} * \frac{\partial z(w)}{\partial w^{(0 \rightarrow 1)}}
∂w(0→1)∂Loss=∂σ(2)∂L(σ)∗∂z(2)∂σ(z)∗∂σ(1)∂z(σ)∗∂z(1)∂σ(z)∗∂w(0→1)∂z(w)
其中前两项是在求解
w
(
1
→
2
)
w^{(1 \rightarrow 2)}
w(1→2)时求解过的,而后三项的求解结果都显而易见:
=
(
σ
(
2
)
−
y
)
∗
∂
z
(
σ
)
∂
σ
(
1
)
∗
∂
σ
(
z
)
∂
z
(
1
)
∗
∂
z
(
w
)
∂
w
(
0
→
1
)
=
(
σ
(
2
)
−
y
)
∗
w
1
→
2
∗
(
σ
(
1
)
(
1
−
σ
(
1
)
)
)
∗
X
= (\sigma^{(2)} - y) * \frac{\partial z(\sigma)}{\partial \sigma^{(1)}} * \frac{\partial \sigma(z)}{\partial z^{(1)}} * \frac{\partial z(w)}{\partial w^{(0 \rightarrow 1)}} \\ = (\sigma^{(2)} - y) * w^{1 \rightarrow 2} * (\sigma^{(1)}(1 - \sigma^{(1)})) * X
=(σ(2)−y)∗∂σ(1)∂z(σ)∗∂z(1)∂σ(z)∗∂w(0→1)∂z(w)=(σ(2)−y)∗w1→2∗(σ(1)(1−σ(1)))∗X
同样,这个表达式现在变得非常简单,并且,这个表达式中所需要的全部张量,都是我们在正向传播中已经计算出来储存好的,或者再模型建立之初就设置好的,因此在计算
w
(
0
→
1
)
w^{(0\rightarrow1)}
w(0→1)的导数时,无需再重新计算如
σ
(
2
)
\sigma^{(2)}
σ(2)这样的张量,这就为神经网络计算导数节省了时间。你是否注意到,我们是从左向右,从输出向输入,逐渐往前求解导数的表达式,并且我们所使用的节点上的张量,也是从后向前逐渐用到,这和我们正向传播的过程完全相反。
这种从左到右,不断使用正向传播中的元素对梯度向量进行计算的方式,就是反向传播。
42.2 Pytorch实现反向传播
在梯度下降中,每走一步都需要更新梯度,所以计算量是巨大的。幸运的是,PyTorch
可以帮助我们自动计算梯度,我们只需要提取梯度向量的值来进行迭代就可以了。在PyTorch
中,我们有两种方式实现梯度计算。一种是使用我们之前已经学过的AutoGrad
。在使用AutoGrad
时,我们可以使用torch.autograd.grad()
函数计算出损失函数上具体某个点/某个变量的导数,当我们需要了解具体某个点的导数值时autograd
会非常关键,比如:
import torch
# requires_grad,表示允许对X进行梯度计算
x = torch.tensor(1.,requires_grad = True)
y = x ** 2
#这里返回的是在函数y=x**2上,x=1时的导数值。
torch.autograd.grad(y, x)
对于单层神经网络,autograd.grad
会非常有效。但深层神经网络就不太适合使用grad
函数了。对于深层神经网络,我们需要一次性计算大量权重对应的导数值,并且这些权重是以层为单位组织成一个个权重的矩阵,要一个个放入autograd
来进行计算有点麻烦。所以我们会直接使PyTorch
提供的基于autograd
的反向传播功能,lossfunction.backward()
来进行计算。
注意,在实现反向传播之前,首先要完成模型的正向传播,并且要定义损失函数,因此我们会借助之前的课程中我们完成的三层神经网络的类和数据(500
行,20
个特征的随机数据)来进行正向传播。
我们来看具体的代码:
# 导入库、数据、定义神经网络类,完成正向传播
import torch
import torch.nn as nn
from torch.nn import functional as F
# 确定数据
torch.manual_seed(420)
X = torch.rand((500, 20), dtype = torch.float32)*100
y = torch.randint(low = 0, high = 3, size = (500, 1), dtype = torch.float32)
# 定义神经网络的架构
class Model(nn.Module):
def __init__(self, in_features, out_features):
super(Model,self)__init__()
self.linear1 = nn.Linear(in_features, 13, bias = True)
self.linear2 = nn.Linear(13, 8, bias = True)
self.output = nn.Linear(8, out_features, bias = True)
def forward(self, x):
z1 = self.linear(x)
sigma1 = torch.relu(z1)
z2 = self.linear2(sigma1)
sigma2 = torch.sigmoid(z2)
zhat = self.output(sigma2)
"""
这是一个三分类的神经网络,因此需要调用的损失函数为多分类交叉熵CEL
CEL类已经内置了sigmoid功能,因此删除输出层上的sigmoid函数,并将最终输出改为zhat
"""
# sigma3 = F.softmax(z3, dim = 1)
return zhat
input_ = X.shape[1] # 特征数目
output_ = len(y.unique()) # 分类数目
# 实例化神经网络
torch.manual_seed(420)
net = Model(in_features = input_, out_features = output_)
# 前向传播
zhat = net.forward(X)
# 定义损失函数
criterion = nn.CrossEntropyLoss()
loss = criterion(zhat, y.shape(500).long())
# 不会返回任何值
net.linear1.weight.grad
# 反向传播,backward是任意损失函数类都可以调用的方法,对任意损失函数,backward都会求解其中全部w的梯度
loss.backward()
# 返回相应的梯度
net.linear1.weight.grad
# 与可以重复进行的正向传播不同,一次正向传播后,反向传播只能进行一次
# 如果希望能够重复进行反向传播,可以在进行第一次反向传播的时候加上参数retain_graph
loss.backward(retain_graph=True)
backward
求解出的结果的结构与对应的权重矩阵的结构一模一样,因为一个权重就对应了一个偏导数。
唯一需要说明的点是,在使用autograd
的时候,我们强调了requires_grad
的用法,但在定义打包好的类以及使用loss.backward()
的时候,我们却没有给任何数据定义requires_grad=True
。这是因为:
-
当使用nn.Module继承后的类进行正向传播时,我们的权重 w w w是自动生成的,在生成时就被自动设置为允许计算梯度(requires_grad=True),所以不需要我们自己去设置
-
同时,观察我们的反向传播过程:
不难发现,我们的特征张量 X X X与真实标签 y y y都不在反向传播的过程当中,但是 X X X与 y y y其实都是损失函数计算需要用的值,在计算图上,这些值都位于叶子节点上,我们在定义损失函数时,并没有告诉损失函数哪些值是自变量,哪些是常数,那
backward
函数是怎么判断具体求解哪个对象的梯度的呢?其实就是靠
requires_grad
。首先backward
值会识别叶子节点,不在叶子上的变量是不会被backward
考虑的。对于全部叶子节点来说,只有属性requires_grad=True
的节点,才会被计算。在设置 X X X与 y y y时,我们都没有写requires_grad
参数,也就是默认让“允许求解梯度”这个选项为False
,所以backward
在计算的时候就只会计算关于 w w w的部分。当然,我们也可以将 X X X和 y y y或者任何除了权重以及截距的量的
requires_grad
打开,一旦我们设置为True
,backward
就会在帮助我们计算 的导数的同时,也帮我们计算以 X X X或 y y y为自变量的导数。在正常的梯度下降和反向传播过程中,我们是不需要这些导数的,因此我们一律不去管requires_grad
的设置,就让它默认为False
,以节约计算资源。当然,如果你的 w w w是自己设置的,千万记得一定要设置requires_grad=True
。
43 移动坐标点
43.1 走出第一步
有了大小和方向,接下来就可以开始走出我们的第一步了。来看权重的迭代公式:
w
(
t
+
1
)
=
w
(
t
)
−
η
∂
L
(
w
)
∂
w
\mathbf{w}_{(t+1)} = \mathbf{w}_{(t)} - \eta \frac{\partial L(\mathbf{w})}{\partial \mathbf{w}}
w(t+1)=w(t)−η∂w∂L(w)
现在我们的偏导数部分已经计算出来了,就是我们使用backward
求解出的结果。而
η
\eta
η学习率,或者步长,是我们在迭代开始之前就人为设置好的,一般是0.01~0.5
之间的某个小数。因此现在我们已经可以无障碍地使用代码实现权重的迭代了:
# 在这里,数据是生成的随机数,为了显示效果,设置了步长为10,正常不会使用这么大的步长
# 步长、学习率的英文是learning rate,所以常常简写为lr
lr = 10
dw = net.linear1.weight.grad
w = net.linear1.weight.data
# 对任意w可以有
w -= lr * dw
普通梯度下降就是在重复正向传播、计算梯度、更新权重的过程,但这个过程往往非常漫长。如大家所见,步长设置为0.001
时,我们看不到
w
w
w任何的变化,只有当步长设置得非常巨大,我们才能够看到一些改变,但是巨大的步长可能会让我们跳过真正的最小值,所以我们无法将步长设置得很大,无论如何,梯度下降都是一个缓慢的过程。
在这个基础上,我们提出了加速迭代的数个方法,其中一个很关键的方法,就是使用动量Momentum
。
43.2 从第一步到第二步:动量法Momentum
之前我们说过,在梯度下降过程中,起始点是一个“盲人”,它看不到也听不到全局,所以我们每移动一次都要重新计算方向与距离,并且每次只能走一小步。但不只限于此,起始点不仅看不到前面的路,也无法从过去走的路中学习。
想象一下,我们被蒙上眼睛,由另一个人喊口号来给与我们方向让我们移动,假设喊口号的人一直喊:”向前,向前,向前。“因为我们看不见,在最初的三四次,我们可能都只会向前走一小步,但如果他一直让我们向前,我们就会失去耐心,转而向前走一大步,因为我们可以预测:前面很长一段路大概都是需要向前的。对梯度下降来说,也是同样的道理——如果在很长一段时间内,起始点一直向相似的方向移动,那按照步长一小步一小步地磨着向前是没有意义的,既浪费计算资源又浪费时间,此时就应该大胆地照着这个方向走一大步。相对的,如果我们很长时间都走向同一个方向,突然让我们转向,那我们转向的第一步就应该非常谨慎,走一小步。
不难发现,真正高效的方法是:在历史方向与现有方向相同的情况下,迈出大步子,在历史方向与现有方向相反的情况下,迈出小步子。那要怎么才能让起始点了解过去的方向呢?我们让上一步的梯度向量与现在这一点的梯度向量以加权的方式求和,求解出受到上一步大小和方向影响的真实下降方向,再让坐标点向真实下降方向移动。 在坐标轴上,可以表示为:
其中,对上一步的梯度向量加上的权重被称为动量参数(也叫做衰减力度,通常使用
γ
\gamma
γ进行表示),对这一点的梯度向量加上的权重就是步长(依然表示为
η
\eta
η),真实移动的向量为
v
v
v,被称为”动量“(Momentum
)。将上述过程使用公式表示,则有:
v
(
t
)
=
γ
v
(
t
−
1
)
−
η
∂
L
∂
w
w
(
t
+
1
)
=
w
(
t
)
+
v
(
t
)
v_{(t)} = \gamma v_{(t-1)} - \eta \frac{\partial L}{\partial \mathbf{w}} \\ \mathbf{w}_{(t+1)} = \mathbf{w}_{(t)} + v_{(t)}
v(t)=γv(t−1)−η∂w∂Lw(t+1)=w(t)+v(t)
在第一步中,没有历史梯度方向,因此第一步的真实方向就是起始点梯度的反方向,
v
0
=
0
v_0=0
v0=0。其中
v
(
t
−
1
)
v_{(t-1)}
v(t−1)代表了之前所有步骤所累积的动量和。在这种情况下,梯度下降的方向有了“惯性”,受到历史累计动量的影响,当新坐标点的梯度反方向与历史累计动量的方向一致时,历史累计动量会加大实际方向的步子,相反,当新坐标点的梯度反方向与历史累计动量的方向不一致时,历史累计动量会减小实际方向的步子。
我们可以很容易地在PyTorch
中实现动量法:
lr = 0.1 # 学习率
gamma = 0.9 # 衰减力度
dw = net.linear1.weight.grad
w = net.linear1.weight.data
# v要能够跟dw相减,因此必须和dw保持相同的结构,初始v为0,但后续v会越来越大
v = torch.zeros(dw.shape[0],dw.shape[1])
# 对任意w可以有
v = gamma * v - lr * dw
w-=v
当加入gamma
之后,即便是较小的步长,也可以让
w
w
w发生变化
43.3 torch.optim实现带动量的梯度下降
在PyTorch
库的架构中,拥有专门实现优化算法的模块torch.optim
。之前所说的迭代流程,都可以通过torch.optim
模块来简单地实现。
接下来,我们就基于之前定义的类Model
来实现梯度下降的一轮迭代:
# 导入库
import torch
import torch.nn.as nn
import torch.optim as optim
from torch.nn import functional as F
# 确定数据、确定优先需要设置的值
lr = 0.1
gamma = 0.9
torch.manual_seed(420)
X = torch.rand((500, 20), dtype = float32) * 100
y = torch.randint(low = 0, high = 3, size = (500, 1), dtype = torch.float32)
input_ = X.shape[1] # 特征的数目
output_ = len(y.unique()) # 分类的数目
# 定义神经网络的架构
class Model(nn.Module):
def __init__(self, in_features, out_features):
super(Model, self).__init__()
self.linear1 = nn.Linear(in_features, 13, bias = True)
self.linear2 = nn.Linear(13, 8, bias = True)
self.output = nn.Linear(8, out_features, bias = True)
def forward(self, x):
z1 = self.linear1(x)
sigma1 = torch.relu(z1)
z2 = self.linear2(sigma1)
sigma2 = torch.sigmoid(z2)
z3 = self.output(sigma2)
# sigma3 = F.softmax(z3, dim = 1)
return z3
# 实例化神经网络,调用优化算法需要的参数
torch.manual_seed(420)
net = Model(in_features = input_, out_features = output_)
# 定义损失函数
criterion = nn.CrossEntropyLoss()
# 定义优化算法
opt = optim.SGD(net.parameters(), lr = lr, momentum = gamma)
# net.parameters():一次性导出所有神经网络架构下全部的权重和截距
接下来开始进行一轮梯度下降:
# 向前传播
zhat = net.forward(X)
# 损失函数值
loss = criterion(zhat, y.reshape(500).long())
# 反向传播
loss.backward()
# 更新权重w,从这一瞬间开始,坐标点就发生了变化,所有的梯度必须重新计算
opt.step() #更新w和v
# 清除原来储存好的,基于上一个坐标点计算的梯度,为下一次计算梯度腾出空间
opt.zero_grad()
44 开始迭代:batch_size与epoches
44.1 为什么要有小批量?
在实现一轮梯度下降之后,只要在梯度下降的代码外面加上一层循环,就可以顺利实现迭代多次的梯度下降了。但在那之前,还有另外一个问题。为了提升梯度下降的速度,我们在使用了动量法,同时,我们也要在使用的数据上下功夫。
在深度学习的世界中,神经网络的训练对象往往是图像、文字、语音、视频等非结构化数据,这些数据的特点之一就是特征张量一般都是大量高维的数据。比如在深度学习教材中总是被用来做例子的入门级数据MNIST
,其训练集的结构为(60000,784)
。对比机器学习中的入门级数据鸢尾花(结构为(150,4)
),两者完全不在一个量级上。在深度学习中,如果梯度下降的每次迭代都使用全部数据,将会非常耗费计算资源,且样本量越大,计算开销越高。虽然PyTorch
被设计成天生能够处理巨量数据,但我们还是需要在数据量这一点上下功夫。
这一节,我们开始介绍小批量随机梯度下降(mini-batch stochastic gradient descent
,简写为mini-batch SGD
)。
小批量随机梯度下降是深度学习入门级的优化算法(梯度下降是入门级之下的),其求解与迭代流程与传统梯度下降(GD
)基本一致,不过二者在迭代权重时使用的数据 这一点上存在巨大的不同。
传统梯度下降在每次进行权重迭代(即循环)时都使用全部数据 ,每次迭代所使用的数据也都一致。而mini-batch SGD
是每次迭代前都会从整体采样一批固定数目的样本组成批次(batch
)
B
B
B,并用
B
B
B中的样本进行梯度计算,以减少样本量。
为什么会选择
mini-batch SGD
作为神经网络的入门级优化算法呢?
有两个比较主流的原因。第一个是,比起传统梯度下降,mini-batch SGD更可能找到全局最小值 。
梯度下降是通过最小化损失函数来找对应参数向量的优化算法。对于任意损失函数
L
(
w
)
L(w)
L(w)而言,如果
L
(
w
)
L(w)
L(w)在其他点上的值比在
w
∗
w^*
w∗上的值更小,那么
L
(
w
∗
)
L(w^*)
L(w∗)很可能就是一个局部最小值(local minimum
)。
如果
L
(
w
)
L(w)
L(w)在
w
∗
w^*
w∗上的值是目标函数在整个定义域上的最小值,那么
L
(
w
∗
)
L(w^*)
L(w∗)就是全局最小值(global minimum
)。
尽可能找到全局最优一直都是优化算法的目标。为什么说mini-batch SGD
更容易找到全局最优呢?
传统梯度下降是每次迭代时都使用全部数据的梯度下降,所以每次使用的数据是一致的,因此梯度向量的方向和大小都只受到权重
w
w
w的影响,所以梯度方向的变化相对较小,很多时候看起来梯度甚至是指向一个方向(如上图所示
)。这样带来的优势是可以使用较大的步长,快速迭代直到找到最小值。但是缺点也很明显,由于梯度方向不容易发生巨大变化,所以一旦在迭代过程中落入局部最优的范围,传统梯度下降就很难跳出局部最优,再去寻找全局最优解了。
而mini-batch SGD
在每次迭代前都会随机抽取一批数据,所以每次迭代时带入梯度向量表达式的数据是不同的,梯度的方向同时受到系数
w
,
b
w,b
w,b和带入的训练数据的影响,因此每次迭代时梯度向量的方向都会发生较大变化。并且,当抽样的数据量越小,本次迭代中使用的样本数据与上一次迭代中使用的样本数据之间的差异就可能越大,这也导致本次迭代中梯度的方向与上一次迭代中梯度的方向有巨大差异。所以对于mini-batch SGD
而言,它的梯度下降路线看起来往往是曲折的折线(如上图所示)。
极端情况下,当我们每次随机选取的批量中只有一个样本时,梯度下降的迭代轨迹就会变得异常不稳定(如上图所示)。我们称这样的梯度下降为随机梯度下降(stochastic gradient descent,SGD
)。
mini-batch SGD
的优势是算法不会轻易陷入局部最优,由于每次梯度向量的方向都会发生巨大变化,因此一旦有机会,算法就能够跳出局部最优,走向全局最优(当然也有可能是跳出一个局部最优,走向另一个局部最优)。不过缺点是,需要的迭代次数变得不明。如果最开始就在全局最优的范围内,那可能只需要非常少的迭代次数就收敛,但是如果最开始落入了局部最优的范围,或全局最优与局部最优的差异很小,那可能需要花很长的时间、经过很多次迭代才能够收敛,毕竟不断改变的方向会让迭代的路线变得曲折。
从整体来看,为了mini-batch SGD
这“不会轻易被局部最优困住”的优点,我们在神经网络中使用它作为优化算法(或优化算法的基础)。当然,还有另一个流传更广、更为认知的理由支持我们使用mini-batch SGD
:mini-batch SGD可以提升神经网络的计算效率,让神经网络计算更快。
为了解决计算开销大的问题,我们要使用mini-batch SGD
。考虑到可以从全部数据中选出一部分作为全部数据的“近似估计",然后用选出的这一部分数据来进行迭代,每次迭代需要计算的数据量就会更少,计算消耗也会更少,因此神经网络的速度会提升。当然了,这并不是说使用1001个样本进行迭代一定比使用1000个样本进行迭代速度要慢,而是指每次迭代中计算上十万级别的数据,会比迭代中只计算一千个数据慢得多。
44.2 batch_size与epoches
在mini-batch SGD
中,我们选择的批量batch
含有的样本数被称为batch_size
,批量尺寸,这个尺寸一定是小于数据量的某个正整数值。每次迭代之前,我们需要从数据集中抽取batch_size
个数据用于训练。
在普通梯度下降中,因为没有抽样,所以每次迭代就会将所有数据都使用一次,迭代了t
次时,算法就将数据学习了t
次。可以想象,对同样的数据,算法学习得越多,也有应当对数据的状况理解得越深,也就学得越好。然而,并不是对一个数据学习越多越好,毕竟学习得越多,训练时间就越长,同时,我们能够收集到的数据只是“样本”,并不能够代表真实世界的客观情况。
例如,我们从几万张猫与狗的照片中学到的内容,并不一定就能适用于全世界所有的猫和狗。如果我们的照片中猫咪都是有毛的,那神经网络对数据学习的程度越深,它就越有可能认不出无毛猫。
因此,虽然我们希望算法对数据了解很深,但我们也希望算法不要变成”书呆子“,要保留一些灵活性(保留一些泛化能力)。关于这一点,我们之后会详细展开来说明,但大家现在需要知道的是,算法对同样的数据进行学习的次数并不是越多越好。
在mini-batch SGD
中,因为每次迭代时都只使用了一小部分数据,所以它迭代的次数并不能代表全体数据一共被学习了多少次。所以我们需要另一个重要概念:epoch
,读音/ˈepək/
,来定义全体数据一共被学习了多少次。
epoch
是衡量训练数据被使用次数的单位,一个epoch表示优化算法将全部训练数据都使用了一次。 它与梯度下降中的迭代次数有非常深的关系,我们常使用“完成1个epoch需要n次迭代“这样的语言。
假设一个数据集总共有m
个样本,我们选择的batch_size
是
N
B
N_B
NB,即每次迭代时都使用
N
B
N_B
NB个样本,则一个epoch
所需的迭代次数的计算公式如下:
完成一个
e
p
o
c
h
所需要的迭代次数
n
=
m
N
B
完成一个epoch所需要的迭代次数n = \frac{m}{N_B}
完成一个epoch所需要的迭代次数n=NBm
在深度学习中,我们常常定义num_epoches
作为梯度下降的最外层循环,batch_size
作为内层循环。有时候,我们希望数据被多学习几次,来增加模型对数据的理解。有时候,我们会控制模型对数据的训练。总之,我们会使用epoch
和batch_size
来控制训练的节奏。接下来,我们就用代码来实现一下。
44.3 TensorDataset与DataLoader
要使用小批量随机梯度下降,我们就需要对数据进行采样、分割等操作。在PyTorch
中,操作数据所需要使用的模块是torch.utils
,其中utils.data
类下面有大量用来执行数据预处理的工具。在MBSGD
中,我们需要将数据划分为许多组特征张量+对应标签的形式,因此最开始我们要将数据的特征张量与标签打包成一个对象。之前我们提到过,深度学习中的特征张量维度很少是二维,因此其特征张量与标签几乎总是分开的,不像机器学习中标签常常出现在特征矩阵的最后一列或第一列。在我们之前的例子中,我们是单独生成了标签与特征张量,所以也需要合并,如果你的数据本来就是特征张量与标签在同一个张量中,那你可以跳过这一步。
合并张量与标签,我们所使用的类是utils.data.TensorDataset
,这个功能类似于python
中的zip
,可以将最外面的维度一致的tensor
进行打包,也就是将第一个维度一致的tensor
进行打包。我们来看一下:
import torch
from torch.util.data import TensorDataset
a = torch.randn(500, 2, 3) # 相当于五百个二维数据 - 表格
b = torch.randn(500, 3, 4, 5) # 相当于五百个三维数据 - 图像
c = torch.randn(500, 1) # 标签
#被合并的对象第一维度上的值相等
TensorDataset(a, b, c)[0]
# 查看数据
for x in TensorDataset(b,c):#generator
print(x)
break
# output :
(tensor([[[ 0.3641, 1.1461, 1.3315, -0.6803, -0.1573],
[-0.3813, 0.0569, 1.4741, -0.2237, 0.4374],
[ 0.4338, 0.7315, -0.2749, 0.0160, -0.2451],
[-0.5867, -0.5889, 1.8905, -0.7718, -1.7899]],
[[-0.4048, 0.7898, 0.3773, 0.7166, 0.0490],
[-0.9121, -0.0489, -0.8179, -1.8548, -0.3418],
[ 0.0873, 0.3071, -0.9272, 1.4546, -0.8360],
[ 1.2235, 1.2197, -0.5222, 0.2297, -0.8180]],
[[ 0.4578, -2.0396, -0.1589, -0.3033, -0.6102],
[ 1.1299, 0.8919, -0.5627, 0.4364, -0.2321],
[ 0.1634, 1.4667, -0.7651, -0.6503, 0.0228],
[ 0.8123, 0.9057, 1.3573, -0.3826, 0.2580]]]), tensor([-0.6932]))
当我们将数据打包成一个对象之后,我们需要使用划分小批量的功能DataLoader
。DataLoader
是处理训练前专用的功能,它可以接受任意形式的数组、张量作为输入,并把他们一次性转换为神经网络可以接入的tensor
。
from torch.utils.data import DataLoader
bs = 120 # 每批次多少数据量
dataset = DataLoader(data,
batch_size = bs,
shuffle = True, # 划分小批量之前随机打乱数据
drop_last = True # 需不需要舍弃最后一个batch
)
for i in dataset:
print(i[0].shape)
# output :
torch.Size([120, 3, 4, 5])
torch.Size([120, 3, 4, 5])
torch.Size([120, 3, 4, 5])
torch.Size([120, 3, 4, 5])
# 一共有多少个batch
len(dataset)
# output :
4
# 展示里面全部的数据
len(dataset.dataset)
# output :
500
# 单个样本
dataset.dataset[0]
# output :
(tensor([[[ 0.3641, 1.1461, 1.3315, -0.6803, -0.1573],
[-0.3813, 0.0569, 1.4741, -0.2237, 0.4374],
[ 0.4338, 0.7315, -0.2749, 0.0160, -0.2451],
[-0.5867, -0.5889, 1.8905, -0.7718, -1.7899]],
[[-0.4048, 0.7898, 0.3773, 0.7166, 0.0490],
[-0.9121, -0.0489, -0.8179, -1.8548, -0.3418],
[ 0.0873, 0.3071, -0.9272, 1.4546, -0.8360],
[ 1.2235, 1.2197, -0.5222, 0.2297, -0.8180]],
[[ 0.4578, -2.0396, -0.1589, -0.3033, -0.6102],
[ 1.1299, 0.8919, -0.5627, 0.4364, -0.2321],
[ 0.1634, 1.4667, -0.7651, -0.6503, 0.0228],
[ 0.8123, 0.9057, 1.3573, -0.3826, 0.2580]]]),
tensor([-0.6932]))
dataset.dataset[0][1]
# output :
tensor([-0.6932])
# 查看现有的batch_size
dataset.batch_size
# output :
120
45 在MINST-FASHION上实现神经网络的学习流程
本节课我们讲解了神经网络使用小批量随机梯度下降进行迭代的流程,现在我们要整合本节课中所有的代码实现一个完整的训练流程。首先要梳理一下整个流程:
- 设置步长 l r lr lr,动量值 g a m m a gamma gamma,迭代次数 e p o c h s epochs epochs, b a t c h _ s i z e batch\_size batch_size等信息,(如果需要)设置初始权重 w 0 w_0 w0
- 导入数据,将数据切分成 b a t c h e s batches batches
- 定义神经网络架构
- 定义损失函数 L ( w ) L(w) L(w),如果需要的话,将损失函数调整成凸函数,以便求解最小值
- 定义所使用的优化算法
- 开始在epoches和batch上循环,执行优化算法:
- 调整数据结构,确定数据能够在神经网络、损失函数和优化算法中顺利运行
- 完成向前传播,计算初始损失
- 利用反向传播,在损失函数 L ( w ) L(w) L(w)上对每一个 w w w求偏导数
- 迭代当前权重
- 清空本轮梯度
- 完成模型进度与效果监控
- 输出结果
45.1 导库,设置各种初始值
import torch
from torch import nn
from torch import optim
from torch.nn import functional as F
from torch.utils.data import TensorDataset # 将特征与标签合并到同一个对象中
from torch.utils.data import DataLoader # 帮助我们进行小批量的分割
# 确定数据、确定优先需要设置的值
lr = 0.15
gamma = 0
epochs = 10
bs = 128
45.2 导入数据,分割小批量
以往我们的做法是:
# torch.manual_seed(420)
# X = torch.rand((50000,20),dtype=torch.float32) * 100
# y = torch.randint(low=0,high=3,size=(50000,1),dtype=torch.float32)
# data = TensorDataset(X,y)
# data_withbatch = DataLoader(data,batch_size=bs, shuffle = True)
这次我们要使用PyTorch
中自带的数据,MINST-FATION
。
import torchvision
import torchvision.transforms as transforms
# 初次运行时下载,需要等待较长时间
mnist = torchvision.datasets.FashionMNIST(
root = 'C:\Pythonwork\DEEP LEARNING\WEEK 3\Datasets\FashionMNIST',
train = True,
download = True,
transform = transforms.ToTensor()
)
"""
1. mnist = torchvision.datasets.FashionMNIST(...)
这行代码通过 torchvision.datasets.FashionMNIST 创建了一个 FashionMNIST 数据集对象,并将其赋值给变量 mnist。
FashionMNIST 是一个标准的图像分类数据集,包含 70,000 张 28×28 的灰度图像,分为 10 个类别(如 T 恤、裤子、连衣裙等)。
2. 参数解释
root = 'C:\Pythonwork\DEEP LEARNING\WEEK 3\Datasets\FashionMNIST'
root 参数指定了数据集存储的路径。
如果数据集已经下载到该路径下,则直接加载;如果没有下载,则会自动下载到该路径。
注意:路径字符串中使用了双反斜杠 \\,这是因为反斜杠 \ 在 Python 中是转义字符,需要用双反斜杠来表示普通的文件路径分隔符。
train = True
train 参数用于指定加载的是训练集(True)还是测试集(False)。
在这里,train=True 表示加载的是训练集,FashionMNIST 的训练集包含 60,000 张图像。
download = True
download 参数用于指定是否自动下载数据集。
如果设置为 True,当指定路径下没有数据集时,程序会自动从网络上下载数据集并保存到 root 指定的路径。
如果设置为 False,则不会自动下载,如果数据集不存在,程序会报错。
transform = transforms.ToTensor()
transform 参数用于指定对数据集中的图像进行何种预处理操作。
在这里,使用了 transforms.ToTensor(),它会将图像从 PIL 图像格式(或 NumPy 数组格式)转换为 PyTorch 的 Tensor 格式。
转换后的图像数据范围会从 [0, 255] 转换为 [0.0, 1.0],并且将图像的维度从 (H, W, C)(高度、宽度、通道数)转换为 (C, H, W)(通道数、高度、宽度),这是 PyTorch 中张量的标准格式。
"""
len(mnist)
# output :
60000
# 查看特征张量
mnist.data.shape
# output :
torch.Size([60000, 28, 28])
# 查看标签的类别
mnist.classes
# output :
['T-shirt/top',
'Trouser',
'Pullover',
'Dress',
'Coat',
'Sandal',
'Shirt',
'Sneaker',
'Bag',
'Ankle boot']
# 查看图像的模样
import matplotlib.pyplot import plt
plt.imshow(mnist[0][0].view((28,28)).numpy()) # imageshow
# 分割batch
batchdata = DataLoader(mnist, batch_size = bs, shuffle = True)
# 查看会放入进行迭代的数据结构
for x,y in batchdata:
print(x.shape)
print(y.shape)
break
# output :
torch.Size([128, 1, 28, 28]) # 每个batch128条数据
torch.Size([128]) # 对应128个分类标签
# 特征的数目,一般是第一维之外的所有维度相乘的数
input_ = mnist.data[0].numel()
# 分类的数目
output_ = len(mnist.targets.unique())
45.3 定义神经网络的架构
class Model(nn.Module):
def __init__(self, in_features, out_features):
super().__init__()
self.linear1 = nn.Linear(in_features, 128, bias = False)
self.output = nn.Linear(128, out_features, bias = False)
def forward(self, x):
# -1表示该维度的大小由其他维度的大小和总元素个数自动确定。它是一个占位符,用于自动计算该维度的大小。
x = x.view(-1, 28*28)
sigma1 = torch.relu(self.linear1(x))
z2 = self.output(sigma1)
sigma2 = F.log_softmax(z2, dim = 1)
return sigma2
45.4 定义训练函数
def fit(net, batchdata, lr, epochs, gamma):
# 定义损失函数
criterion = nn.NLLLoss()
# 定义优化算法
opt = optim.SGD(net.parameters(), lr = lr, momentum = gamma)
correct = 0 # 用于记录正确分类的样本数量
samples = 0 # 用于记录处理的总样本数量
for epoch in range(epochs):
for batch_idx, (x, y) in enumerate(batchdata):
y = y.view(x.shape[0])
sigma = net.forward(x)
loss = criterion(sigma, y)
loss.backward()
opt.step()
opt.zero_grad()
# 求解准确率
yhat = torch.max(sigma, 1)[1]
"""
torch.max(sigma, 1)
sigma:通常是一个二维张量,表示模型对每个样本的类别预测分数(或概率)。
1:表示沿着第二个维度(即每一行)查找最大值。对于分类任务,每一行代表一个样本的类别预测分数。
返回值是一个元组:
第一个元素:每一行的最大值(即每个样本的最高预测分数)。
第二个元素:每一行最大值的索引(即每个样本的预测类别)
"""
correct += torch.sum(yhat == y)
samples += x.shape[0]
if (batch_idx+1) % 125 == 0 or batch_idx == len(batchdata)-1:
print('Epoch{}:[{}/{}({:.0f}%)],Loss:{:.6f},Accuracy:{:.3f}').format(
epoch+1,
samples,
len(batchdata.dataset) * epochs,
100 * samples / (batchdata.dataset) * epochs,
loss.data.item()
float(correct * 100 / samples)
)
45.5 进行训练与评估
# 实例化神经网络,调用优化算法需要的参数
torch.manual_seed(420)
net = Model(in_features = input_, out_features = output_)
fit(net, batchdata, lr = lr, epochs = epochs, gamma = gamma)
# output :
Epoch1:[16000/600000(3%)],Loss:0.727888,Accuracy:68.819
Epoch1:[32000/600000(5%)],Loss:0.513726,Accuracy:73.550
Epoch1:[48000/600000(8%)],Loss:0.459364,Accuracy:76.173
Epoch1:[60000/600000(10%)],Loss:0.563562,Accuracy:77.373
Epoch2:[76000/600000(13%)],Loss:0.433777,Accuracy:78.687
Epoch2:[92000/600000(15%)],Loss:0.363204,Accuracy:79.634
Epoch2:[108000/600000(18%)],Loss:0.443800,Accuracy:80.304
Epoch2:[120000/600000(20%)],Loss:0.442278,Accuracy:80.723
Epoch3:[136000/600000(23%)],Loss:0.543707,Accuracy:81.285
Epoch3:[152000/600000(25%)],Loss:0.354620,Accuracy:81.691
Epoch3:[168000/600000(28%)],Loss:0.526626,Accuracy:82.088
Epoch3:[180000/600000(30%)],Loss:0.411618,Accuracy:82.367
Epoch4:[196000/600000(33%)],Loss:0.350448,Accuracy:82.678
Epoch4:[212000/600000(35%)],Loss:0.345022,Accuracy:83.008
Epoch4:[228000/600000(38%)],Loss:0.474553,Accuracy:83.263
Epoch4:[240000/600000(40%)],Loss:0.324190,Accuracy:83.479
Epoch5:[256000/600000(43%)],Loss:0.317327,Accuracy:83.738
Epoch5:[272000/600000(45%)],Loss:0.369660,Accuracy:83.957
Epoch5:[288000/600000(48%)],Loss:0.341690,Accuracy:84.139
Epoch5:[300000/600000(50%)],Loss:0.547992,Accuracy:84.282
Epoch6:[316000/600000(53%)],Loss:0.292443,Accuracy:84.497
Epoch6:[332000/600000(55%)],Loss:0.276111,Accuracy:84.649
Epoch6:[348000/600000(58%)],Loss:0.318102,Accuracy:84.797
Epoch6:[360000/600000(60%)],Loss:0.308750,Accuracy:84.874
Epoch7:[376000/600000(63%)],Loss:0.261769,Accuracy:85.029
Epoch7:[392000/600000(65%)],Loss:0.467604,Accuracy:85.163
Epoch7:[408000/600000(68%)],Loss:0.374349,Accuracy:85.302
Epoch7:[420000/600000(70%)],Loss:0.374845,Accuracy:85.384
Epoch8:[436000/600000(73%)],Loss:0.292379,Accuracy:85.498
Epoch8:[452000/600000(75%)],Loss:0.274055,Accuracy:85.624
Epoch8:[468000/600000(78%)],Loss:0.326614,Accuracy:85.734
Epoch8:[480000/600000(80%)],Loss:0.377757,Accuracy:85.793
Epoch9:[496000/600000(83%)],Loss:0.345958,Accuracy:85.884
Epoch9:[512000/600000(85%)],Loss:0.334107,Accuracy:85.993
Epoch9:[528000/600000(88%)],Loss:0.161249,Accuracy:86.080
Epoch9:[540000/600000(90%)],Loss:0.305421,Accuracy:86.151
Epoch10:[556000/600000(93%)],Loss:0.309134,Accuracy:86.249
Epoch10:[572000/600000(95%)],Loss:0.341859,Accuracy:86.330
Epoch10:[588000/600000(98%)],Loss:0.308504,Accuracy:86.419
Epoch10:[600000/600000(100%)],Loss:0.214857,Accuracy:86.484