文章目录
前言
主要对神经网络多分类与反向传播算法做进一步阐述。
正文
多分类代价函数
假设神经网络的训练样本有
m
m
m个,每个包含一组输入
x
x
x和一组输出信号
y
y
y,
L
L
L表示神经网络层数,
S
l
S_l
Sl表示输出层神经元个数,
S
L
S_L
SL代表最后一层中处理单元的个数。
如图L=4,
S
1
S_1
S1 =3,
S
2
S_2
S2=5,
S
L
S_L
SL=4。 注意不包含偏置层。
-
将神经网络的分类定义为两种情况:二类分类和多类分类,
-
二类分类: S L = 0 , y = 0 o r 1 S_L=0, y=0\, or\, 1 SL=0,y=0or1表示哪一类;
二分类 S L S_L SL=1
二分类代价函数如下:
我们回顾逻辑回归问题中我们的代价函数为:
J ( θ ) = 1 m ∑ i = 1 m [ − y ( i ) log ( h θ ( x ( i ) ) ) − ( 1 − y ( i ) ) log ( 1 − h θ ( x ( i ) ) ) ] + λ 2 m ∑ j = 1 n θ j 2 J\left( \theta \right)=\frac{1}{m}\sum\limits_{i=1}^{m}{[-{{y}^{(i)}}\log \left( {h_\theta}\left( {{x}^{(i)}} \right) \right)-\left( 1-{{y}^{(i)}} \right)\log \left( 1-{h_\theta}\left( {{x}^{(i)}} \right) \right)]}+\frac{\lambda }{2m}\sum\limits_{j=1}^{n}{\theta _{j}^{2}} J(θ)=m1i=1∑m[−y(i)log(hθ(x(i)))−(1−y(i))log(1−hθ(x(i)))]+2mλj=1∑nθj2
也可写成:
-
K
K
K类分类:
S
L
=
k
,
y
i
=
1
S_L=k, y_i = 1
SL=k,yi=1表示分到第
i
i
i类;
(
k
>
2
)
(k>2)
(k>2)
多分类 S L S_L SL=K
多分类的代价函数看起来有些复杂:
\newcommand{\subk}[1]{ #1_k }
h θ ( x ) ∈ R K h_\theta\left(x\right)\in \mathbb{R}^{K} hθ(x)∈RK ( h θ ( x ) ) i = i t h output {\left({h_\theta}\left(x\right)\right)}_{i}={i}^{th} \text{output} (hθ(x))i=ithoutput
不要忘了输出是这种:
( h θ ( x ) ) 1 {\left({h_\theta}\left(x\right)\right)}_{1} (hθ(x))1 = [1 , 0, 0, 0] (对四分类而言)
这个看起来复杂很多的代价函数背后的思想还是一样的,我们希望通过代价函数来观察算法预测的结果与真实情况的误差有多大,唯一不同的是,对于每一行特征,我们都会给出 K K K个预测,基本上我们可以利用循环,对每一行特征都预测 K K K个不同结果,然后在利用循环在 K K K个预测中选择可能性最高的一个,将其与 y y y中的实际数据进行比较。
前面的一项比较好理解,就是K个输出层的交叉熵之和,后面的正则化我是这样理解的。L-1是因为不包含最后一层,假如是3层只会有两层有 θ \theta θ, s l s_l sl不是一个固定的数,假如是 s 1 s_1 s1, 那么 s l + 1 s_{l+1} sl+1就是 s 2 s_2 s2, 就是第二层有多少神经元。正则化是排除了每一层 θ 0 \theta_0 θ0后,每一层的 θ \theta θ 矩阵的和。最里层的循环 j j j循环所有的行(由 s l + 1 s_{l+1} sl+1 层的激活单元数决定),循环 i i i则循环所有的列,由该层( s l s_l sl层)的激活单元数所决定。
总之: h θ ( x ) h_\theta(x) hθ(x)与真实值之间的距离为每个样本-每个类输出的加和,对参数进行regularization的bias项处理所有参数的平方和。
反向传播算法
之前我们在计算神经网络预测结果的时候我们采用了一种正向传播方法,我们从第一层开始正向一层一层进行计算,直到最后一层的 h θ ( x ) h_{\theta}\left(x\right) hθ(x)。
现在,为了计算代价函数的偏导数 ∂ ∂ Θ i j ( l ) J ( Θ ) \frac{\partial}{\partial\Theta^{(l)}_{ij}}J\left(\Theta\right) ∂Θij(l)∂J(Θ),我们需要采用一种反向传播算法,也就是首先计算最后一层的误差,然后再一层一层反向求出各层的误差,直到倒数第二层。
要正确理解误差反向传播法,我个人认为有两种方法:一种是基于数学式;
另一种是基于计算图(computational graph)。但是说实话,这些数学的东西我记不住,总是忘,所以记录一下第二种方法的学习。
1. 了解计算图
- 问题1: 太郎在超市买了2 个100 日元一个的苹果,消费税是10%,请计
算支付金额。
计算图通过节点和箭头表示计算过程。节点用○表示,○中是计算的内
容。将计算的中间结果写在箭头的上方,表示各个节点的计算结果从左向右
传递。如图:
虽然图中把“× 2”, “× 1.1”等作为一个运算整体用○括起来了,不过
只用○表示乘法运算“×”也是可行的。此时,如下图所示,可以将“2”和
“1.1”分别作为变量“苹果的个数”和“消费税”标在○外面。
- 问题2: 太郎在超市买了2 个苹果、3 个橘子。其中,苹果每个100 日元,
橘子每个150 日元。消费税是10%,请计算支付金额。
综上,用计算图解题的情况下,需要按如下流程进行。
- 构建计算图。
- 在计算图上,从左向右进行计算。
这里的第2 歩“从左向右进行计算”是一种正方向上的传播,简称为正
向传播(forward propagation),当然也可以考虑反向(从图上看的话,就是从右向左)
的传播。实际上,这种传播称为反向传播(backward propagation)
2. 计算图的特征:局部计算
计算图的特征是可以通过传递“局部计算”获得最终结果。“局部”这个
词的意思是“与自己相关的某个小范围”。局部计算是指,无论全局发生了什么,
都能只根据与自己相关的信息输出接下来的结果。
我们用一个具体的例子来说明局部计算。比如,在超市买了2 个苹果和
其他很多东西。
这里的重点是,各个节点处的计算都是局部计算。这意味着,例
如苹果和其他很多东西的求和运算(4000 + 200→4200)并不关心4000 这个
数字是如何计算而来的,只要把两个数字相加就可以了。换言之,各个节点
处只需进行与自己有关的计算(在这个例子中是对输入的两个数字进行加法
运算),不用考虑全局。
3. 计算图的优点
- 局部计算
- 保存中间结果
比如,计算进行到2 个苹果时的金额是200 日元、加上消费税之前的金额650 日元等 - 可以通过反向传播高效计算导数
- 假设我们想知道苹果价格的上涨会在多大程度上影响最终的支付金额,即求“支付金
额关于苹果的价格的导数”。设苹果的价格为x,支付金额为L,则相当于求
δ L δ x \frac{\delta L}{\delta x} δxδL。 这个导数的值表示当苹果的价格稍微上涨时,支付金额会增加多少。
先来看一下结果:
反向传播使用与正方向相反的箭头(粗线)表示。反向传
播传递“局部导数”,将导数的值写在箭头的下方。在这个例子中,反向传
播从右向左传递导数的值(1→1.1→2.2)。从这个结果中可知,“支付金额
关于苹果的价格的导数”的值是2.2。这意味着,如果苹果的价格上涨1 日元,
最终的支付金额会增加2.2 日元
4. 计算图的反向传播
传递这个局部导数的原理,是基于链式法则(chain rule)的。让我们先来看一个使用计算图的反向传播的例子。假设存在y = f(x) 的计算,这个计算的反向传播如图所示:
如图所示,反向传播的计算顺序是,将信号E乘以节点的局部导数
δ
y
δ
x
\frac{\delta y}{\delta x}
δxδy,然后将结果传递给下一个节点。。这里所说的局部导数是指正向传播
中y = f(x) 的导数,也就是y 关于x的导数, 比如,假设y = f(x) = x2,
则局部导数为
δ
y
δ
x
\frac{\delta y}{\delta x}
δxδy = 2x。把这个局部导数乘以上游传过来的值(本例中为E),
然后传递给前面的节点。
链式法则
- 复合函数是由多个函数构成的函数。比如, z = ( x + y ) 2 z = (x + y)^2 z=(x+y)2 是由图所示的两个式子构成的。
链式法则是关于复合函数的导数的性质,定义如下。
如果某个函数由复合函数表示,则该复合函数的导数可以用构成复合函数的各个函数的导数的乘积表示。
- 式子的导数如图
链式法则和计算图
现在我们尝试将式子的链式法则的计算用计算图表示出来。如果用
“**2” 节点表示平方运算的话,则计算图如下图所示:
图中反向传播最开始的信号在前面的数学式中没有出现,这是因为
δ
z
δ
z
\frac{\delta z}{\delta z}
δzδz=1,所以在刚才的式子中被省略了。最后得到:
5. 节点的反向传播
加法节点的反向传播
以z = x + y 为对象,观察它的反向传播。z = x + y的导数可由下式(解析性地)计算出来。
用计算图表示的话,如图所示:
反向传播将从上游传过来的导数(本例中是
δ
L
δ
z
\frac{\delta L}{\delta z}
δzδL)乘以1,然后传向下游。也就是说,因为加法节点的反向传播只乘以1,所以输入的值会原封不动地流向下一个节点。
- 现在来看一个加法的反向传播的具体例子。假设有“10 + 5=15”这一计算,反向传播时,从上游会传来值1.3。用计算图表示的话,如图所示:
乘法节点的反向传播
- 这里我们考虑z = xy。这个式子的导数如图所示:
乘法的反向传播会将上游的值乘以正向传播时的输入信号的“翻转值”后传递给下游。翻转值表示一种翻转关系,如下图所示,正向传播时信号是x的话,反向传播时则是y;正向传播时信号是y 的话,反向传播时则是x。
我们来看一个具体的例子。比如,假设有“10 × 5 = 50”这一计算,反向传播时,从上游会传来值1.3。用计算图表示的话,如图所示:
再来思考一下最开始举的购买苹果的例子(2 个苹果和消费税),三个变量的导数的计算图如下图所示:
6. 乘法层的代码实现
class MulLayer:
def __init__(self):
self.x = None
self.y = None
def forward(self, x, y):
self.x = x
self.y = y
out = x * y
return out
def backward(self, dout):
dx = dout * self.y # 翻转x和y
dy = dout * self.x
return dx, dy
init()中会初始化实例变量x和y,它们用于保存正向传播时的输入值。
forward()接收x和y两个参数,将它们相乘后输出。backward()将从上游传
来的导数(dout)乘以正向传播的翻转值,然后传给下游。
7. 激活函数层的代码实现
Relu层
class Relu:
def __init__(self):
self.mask = None
def forward(self, x):
self.mask = (x <= 0)
out = x.copy()
out[self.mask] = 0
return out
def backward(self, dout):
dout[self.mask] = 0
dx = dout
return dx
从代码也可以看出来必须有前向传播之后才可以反向传播, pytorch等框架也是这样设置的
Relu类有实例变量mask。这个变量mask是由True/False构成的NumPy数组,它会把正向传播时的输入x的元素中小于等于0 的地方保存为True,其他地方(大于0 的元素)保存为False。如下图:
如果正向传播时的输入值小于等于0,则反向传播的值为0。
因此,反向传播中会使用正向传播时保存的mask,将从上游传来的dout的
mask中的元素为True的地方设为0。
Sigmoid 层
用计算图来表示,如图:
假设上游值是
δ
L
δ
y
\frac{\delta L}{\delta y}
δyδL, 反向传播如图
其中,
δ
L
δ
y
\frac{\delta L}{\delta y}
δyδL
y
2
y^2
y2
e
x
p
(
−
x
)
exp(-x)
exp(−x)可以进一步整理:
所以Sigmoid 层的反向传播,只根据正向传播的输出就能计算出来。
- 代码实现
class Sigmoid:
def __init__(self):
self.out = None
def forward(self, x):
out = 1 / (1 + np.exp(-x))
self.out = out
return out
def backward(self, dout):
dx = dout * (1.0 - self.out) * self.out
return dx
正向传播时将输出保存在了实例变量out中。然后,反向传播时,使用该变量out进行计算
8. Affine层
神经网络的正向传播中进行的矩阵的乘积运算在几何学领域被称为“仿射变换,因此,这里将进行仿射变换的处理实现为“Affine层”。几何中,仿射变换包括一次线性变换和一次平移,分别对应神经网络的加权和运算与加偏置运算。
现在将这里进行的求矩阵的乘积与偏置的和的运算用计算图表示出来。将乘积运算用“dot”节点表示的话,则np.dot(X, W) + B的运算可用下图所示的计算图表示出来。另外,在各个变量的上方标记了它们的形状(比如,计算图上显示了X的形状为(2,),X·W的形状为(3,) 等)
现在考虑此图的反向传播,可以得到下式:
其实比较容易理解,矩阵的dot相乘也可以理解为是转置后按位置相乘,等同于numpy里的multiply函数
结果如图:
我们看一下图中的计算图中各个变量的形状可以发现,X和 δ L δ X \frac{\delta L}{\delta X} δXδL形状相同,W和 δ L δ W \frac{\delta L}{\delta W} δWδL形状相同
9. 批(batch)版本的Affine 层
先给出批版本的Affine层的计算图,如图所示。
加上偏置时,需要特别注意。正向传播时,偏置被加到X·W的各个数据上。比如,N = 2(数据为2 个)时,偏置会被分别加到这2 个数据(各自的计算结果)上,具体的例子如下所示。
正向传播时,偏置会被加到每一个数据(第1 个、第2 个……)上。因此,反向传播时,各个数据的反向传播的值需要汇总为偏置的元素。用代码表示的话,如下所示。
如果不理解还可以这样想:因为已经知道反向传播时矩阵的导数和矩阵本身维度一致,既然偏置没有N,那么偏置就是第0维度的求和。
- 代码实现
class Affine:
def __init__(self, W, b):
self.W = W
self.b = b
self.x = None
self.dW = None
self.db = None
def forward(self, x):
self.x = x
out = np.dot(x, self.W) + self.b
return out
def backward(self, dout):
dx = np.dot(dout, self.W.T)
self.dW = np.dot(self.x.T, dout)
self.db = np.sum(dout, axis=0)
return dx
10.Softmax-with-Loss 层
最后介绍一下输出层的softmax 函数,softmax 函数会将输入值正规化之后再输出。比如手写数字识别时,Softmax 层的输出如图所示:
下面来实现Softmax 层。考虑到这里也包含作为损失函数的交叉熵误差(cross entropy error),所以称为“Softmax-with-Loss 层”。 计算过程如图:
这里只展示了结果,细节放在附录。
Softmax 层的反向传播得到了(y1 − t1, y2 − t2, y3 − t3)这样“漂亮”的结果。由于(y1, y2, y3)是Softmax 层的输出,(t1, t2, t3)是监督数据,所以(y1 − t1, y2 − t2, y3 − t3)是Softmax 层的输出和教师标签的差分。神经网络的反向传播会把这个差分表示的误差传递给前面的层,这是神经网络学习中的重要性质。
这里考虑一个具体的例子,比如思考教师标签是(0, 1, 0),Softmax 层
的输出是(0.3, 0.2, 0.5) 的情形。因为正确解标签处的概率是0.2(20%),这个
时候的神经网络未能进行正确的识别。此时,Softmax 层的反向传播传递的
是(0.3, −0.8, 0.5) 这样一个大的误差。因为这个大的误差会向前面的层传播,
所以Softmax层前面的层会从这个大的误差中学习到“大”的内容。
再举一个例子,比如思考教师标签是(0, 1, 0),Softmax层的输出是(0.01,
0.99, 0)的情形(这个神经网络识别得相当准确)。此时Softmax层的反向传播
传递的是(0.01, −0.01, 0) 这样一个小的误差。这个小的误差也会向前面的层
传播,因为误差很小,所以Softmax层前面的层学到的内容也很“小”。
- 代码实现
class SoftmaxWithLoss:
def __init__(self):
self.loss = None # 损失
self.y = None # softmax的输出
self.t = None # 监督数据(one-hot vector)
def forward(self, x, t):
self.t = t
self.y = softmax(x)
self.loss = cross_entropy_error(self.y, self.t)
return self.loss
def backward(self, dout=1):
batch_size = self.t.shape[0]
dx = (self.y - self.t) / batch_size
return dx
请注意反向传播时,将要传播
的值除以批的大小(batch_size)后,传递给前面的层的是单个数据的误差。
附录
Softmax-with-Loss 层的计算图
简化的反向传播如图。正向传播时先softmax,softmax 函数可由下式表示:
接下来是Cross Entropy Error 层。交叉熵误差可由下式表示。
仅展示正向传播:
先看交叉熵的反向传播
然后再往前走
“×”节点将正向传播的值翻转后相乘。这个过程中会进行下面的计算。
注意y1代表的值。再往前走:
正向传播时若有分支流出,则反向传播时它们的反向传播的值会相加。
因此,这里分成了三支的反向传播的值(−
t
1
S
t_1S
t1S, −
t
2
S
t_2S
t2S, −
t
3
S
t_3S
t3S) 会被求和。然后,
还要对这个相加后的值进行“/”节点的反向传播,结果为
1
S
\frac{1}{S}
S1(
t
1
t_1
t1+
t
2
t_2
t2+
t
3
t_3
t3
)
)
)。
这里,( t 1 t_1 t1, t 2 t_2 t2, t 3 t_3 t3) 是教师标签,也是one-hot 向量。one-hot 向量意味着( t 1 t_1 t1, t 2 t_2 t2, t 3 t_3 t3)中只有一个元素是1,其余都是0。因此,( t 1 t_1 t1, t 2 t_2 t2, t 3 t_3 t3) 的和为1。
然后是另一个分支:
最后:
这是怎么来的呢?因为分支要相加,所以有:
剩下的a2、a3 也可以按照相同的步骤求出来(结果分别为
y
2
y_2
y2 −
t
2
t_2
t2 和
y
3
y_3
y3 −
t
3
t_3
t3)