在深度学习的实现中,经常出现数组和矩阵的计算。NumPy
的数组类(numpy.array)中提供了很多便捷的方法,在实现深度学习时,我们将使用这些方法。安装各种第三方库的方式详见:VS Code中安装Python机器学习与数据分析相关第三方模块教程。
导入NumPy:
import numpy as np
生成NumPy数组:
x = np.array([1.0, 2.0, 3.0])
print(x) # [1. 2. 3.]
NumPy数组算术运算:
x = np.array([1.0, 2.0, 3.0])
y = np.array([2.0, 4.0, 6.0])
x + y # [3. 6. 9.]
x - y # [-1. -2. -3.]
x * y # [2. 8. 18.]
x / y # [0.5 0.5 0.5]
x / 2.0 # [0.5 1. 1.5],广播功能
多维NumPy数组:
A = np.array([[1, 2], [3, 4]])
A.shape # (2, 2),查看A的形状
A.dtype # dtype('int64'),查看矩阵元素的数据类型
广播功能如下图所示:
访问矩阵元素的方式:
X = np.array([[51, 55], [14, 19], [0, 4]])
X[0] # array([51, 55]),第0行
X[0][1] # 55,(0, 1)位置的元素
X = X.flatten() # 将X转换为一维数组
print(X) # [51 55 14 19 0 4]
X[np.array([0, 2, 4])] # array([51, 14, 0]),获取索引为0、2、4的元素
从 X X X中抽取大于 15 15 15的元素:
X > 15 # array([True, True, False, True, False, False], dtype=bool)
X[X > 15] # array([51, 55, 19])
在深度学习的实验中,图形的绘制和数据的可视化非常重要。Matplotlib
是用于绘制图形的库,使用Matplotlib可以轻松地绘制图形和实现数据的可视化。
绘制 s i n sin sin函数图像:
import numpy as np
import matplotlib.pyplot as plt
# 生成数据
x = np.arange(0, 6, 0.1) # 以0.1为步长,生成[0, 6)的数据
y = np.sin(x)
# 绘制图形
plt.plot(x, y)
plt.show()
绘制结果如下图所示:
添加 c o s cos cos函数,并添加标题和 x x x轴标签名等其他功能:
import numpy as np
import matplotlib.pyplot as plt
# 生成数据
x = np.arange(0, 6, 0.1) # 以0.1为步长,生成[0, 6)的数据
y1 = np.sin(x)
y2 = np.cos(x)
# 绘制图形
plt.plot(x, y1, label="sin")
plt.plot(x, y2, linestyle="--", label="cos") # 用虚线绘制
plt.xlabel("x") # x轴标签
plt.ylabel("y") # y轴标签
plt.title('sin & cos') # 标题
plt.legend()
plt.show()
绘制结果如下图所示:
pyplot
中还提供了用于显示图像的方法imshow()
。另外,可以使用matplotlib.image
模块的imread()
方法读入图像:
import matplotlib.pyplot as plt
from matplotlib.image import imread
img = imread('lena.png') # 读入图像(设定合适的路径!这里假定图像lena.png在当前目录下)
plt.imshow(img)
plt.show()
运行上述代码后会显示下图所示的图像:
感知机接收多个输入信号,输出一个信号。下图是一个接收两个输入信号的感知机的例子, x 1 , x 2 x_1,x_2 x1,x2是输入信号, y y y是输出信号, w 1 , w 2 w_1,w_2 w1,w2是权重( w w w是 w e i g h t weight weight的首字母)。图中的圆圈称为“神经元”或者“节点”。输入信号被送往神经元时,会被分别乘以固定的权重( w 1 x 1 , w 2 x 2 w_1x_1,w_2x_2 w1x1,w2x2)。神经元会计算传送过来的信号的总和,只有当这个总和超过了某个界限值时,才会输出 1 1 1。这也称为“神经元被激活”。这里将这个界限值称为阈值,用符号 θ \theta θ表示。
把上述内容用数学式来表示就如下式所示:
感知机的多个输入信号都有各自固有的权重,这些权重发挥着控制各个信号的重要性的作用。也就是说,权重越大,对应该权重的信号的重要性就越高。
现在考虑使用感知机来实现 A N D g a t e AND\ gate AND gate,其真值表如下图所示:
满足上图条件的参数的选择方法有无数多个。当 ( w 1 , w 2 , θ ) = ( 0.5 , 0.5 , 0.7 ) (w_1,w_2,\theta )=(0.5,0.5,0.7) (w1,w2,θ)=(0.5,0.5,0.7)时,可以满足上图条件。设定这样的参数后,仅当 x 1 x_1 x1和 x 2 x_2 x2同时为 1 1 1时,信号的加权总和才会超过给定的阈值 θ \theta θ。
用Python来实现上述的逻辑电路:
def AND(x1, x2):
w1, w2, theta = 0.5, 0.5, 0.7
tmp = x1 * w1 + x2 * w2
if tmp <= theta:
return 0
elif tmp > theta:
return 1
我们将之前数学式中的 θ \theta θ换成 − b -b −b,便可用以下式子表示感知机:
此处, b b b称为偏置, w 1 w_1 w1和 w 2 w_2 w2称为权重。感知机会计算输入信号和权重的乘积,然后加上偏置,如果这个值大于 0 0 0则输出 1 1 1,否则输出 0 0 0。
使用权重和偏置,可以像下面这样实现与门:
def AND(x1, x2):
x = np.array([x1, x2])
w = np.array([0.5, 0.5])
b = -0.7
tmp = np.sum(w * x) + b
if tmp <= 0:
return 0
else:
return 1
请注意,偏置和权重 w 1 , w 2 w_1,w_2 w1,w2的作用是不一样的。具体地说, w 1 w_1 w1和 w 2 w_2 w2是控制输入信号的重要性的参数,而偏置是调整神经元被激活的容易程度(输出信号为 1 1 1的程度)的参数。
X O R g a t e XOR\ gate XOR gate真值表如下图所示:
用前面介绍的感知机是无法实现这个异或门的。以或门为例,当权重参数 ( b , w 1 , w 2 ) = ( − 0.5 , 1.0 , 1.0 ) (b,w_1,w_2)=(-0.5,1.0,1.0) (b,w1,w2)=(−0.5,1.0,1.0)时,可满足其真值表条件,此时感知机可用下式表示:
此时感知机会生成由直线 − 0.5 + x 1 + x 2 = 0 -0.5+x_1+x_2=0 −0.5+x1+x2=0分割开的两个空间。其中一个空间输出 1 1 1,另一个空间输出 0 0 0,如下图所示:
或门在 ( x 1 , x 2 ) = ( 0 , 0 ) (x_1,x_2)=(0,0) (x1,x2)=(0,0)时输出 0 0 0,在 ( x 1 , x 2 ) (x_1,x_2) (x1,x2)为 ( 0 , 1 ) , ( 1 , 0 ) , ( 1 , 1 ) (0,1),(1,0),(1,1) (0,1),(1,0),(1,1)时输出 1 1 1。上图中,圆圈表示 0 0 0,三角形表示 1 1 1。
异或门的输出如下图所示:
想要用一条直线将上图中的圆圈和三角形分开,无论如何都做不到。
使用曲线分割成的非线性空间即可实现以上条件:
感知机的绝妙之处在于它可以“叠加层”。异或门可以通过与门、与非门和或门组合进行实现,如下图所示:
假设这三种门均已实现,那么用Python实现异或门的代码如下:
def XOR(x1, x2):
s1 = NAND(x1, x2)
s2 = OR(x1, x2)
y = AND(s1, s2)
return y
下面我们试着用感知机的表示方法(明确地显示神经元)来表示这个异或门,如下图所示,异或门是一种多层结构的神经网络。这里,将最左边的一列称为第 0 0 0层,中间的一列称为第 1 1 1层,最右边的一列称为第 2 2 2层。
用图来表示神经网络的话,如下图所示,我们把最左边的一列称为输入层,最右边的一列称为输出层,中间的一列称为中间层(隐藏层)。
在上图的网络中,偏置 b b b并没有被画出来。如果要明确地表示出 b b b,可以像下图那样做。下图中添加了权重为 b b b的输入信号 1 1 1。这个感知机将 x 1 , x 2 , 1 x_1,x_2,1 x1,x2,1三个信号作为神经元的输入,将其和各自的权重相乘后,传送至下一个神经元。在下一个神经元中,计算这些加权信号的总和。如果这个总和超过 0 0 0,则输出 1 1 1,否则输出 0 0 0。另外,由于偏置的输入信号一直是 1 1 1,所以为了区别于其他神经元,我们在图中把这个神经元整个涂成灰色。
我们用一个函数来表示这种分情况的动作(超过 0 0 0则输出 1 1 1,否则输出 0 0 0),即 y = h ( b + w 1 x 1 + w 2 x 2 ) y=h(b+w_1x_1+w_2x_2) y=h(b+w1x1+w2x2),其中函数 h ( x ) h(x) h(x)如下式所示:
h ( x ) h(x) h(x)函数会将输入信号的总和转换为输出信号,这种函数一般称为激活函数。激活函数的作用在于决定如何来激活输入信号的总和。
可将以上式子细化为如下两个式子:
激活函数的计算过程如下图所示:
神经网络中经常使用的一个激活函数就是下式表示的 s i g m o i d sigmoid sigmoid函数:
现在,我们先尝试画出阶跃函数的图像,当输入超过 0 0 0时,输出 1 1 1,否则输出 0 0 0。可以像下面这样简单地实现阶跃函数:
# 参数只能为实数
def step_function(x):
if x > 0:
return 1
else:
return 0
# 参数可以为NumPy数组
def step_function(x):
y = x > 0
return y.astype(np.int)
接着进行函数图像的绘制:
import numpy as np
import matplotlib.pylab as plt
def step_function(x):
return np.array(x > 0, dtype=np.int)
x = np.arange(-5.0, 5.0, 0.1)
y = step_function(x)
plt.plot(x, y)
plt.ylim(-0.1, 1.1) # 指定y轴的范围
plt.show()
结果如下图所示:
接下来我们实现 s i g m o i d sigmoid sigmoid函数:
def sigmoid(x):
return 1 / (1 + np.exp(-x))
并绘制函数图像:
x = np.arange(-5.0, 5.0, 0.1)
y = sigmoid(x)
plt.plot(x, y)
plt.ylim(-0.1, 1.1) # 指定y轴的范围
plt.show()
结果如下图所示:
s i g m o i d sigmoid sigmoid函数是一条平滑的曲线,输出随着输入发生连续性的变化。而阶跃函数以 0 0 0为界,输出发生急剧性的变化。 s i g m o i d sigmoid sigmoid函数的平滑性对神经网络的学习具有重要意义。
相对于阶跃函数只能返回 0 0 0或 1 1 1, s i g m o i d sigmoid sigmoid函数可以返回$0.731\dots,0.880\dots $等实数(这一点和刚才的平滑性有关)。也就是说,感知机中神经元之间流动的是 0 0 0或 1 1 1的二元信号,而神经网络中流动的是连续的实数值信号。
阶跃函数和 s i g m o i d sigmoid sigmoid函数虽然在平滑性上有差异,但是它们具有相似的形状。两者的结构均是“输入小时,输出接近 0 0 0(为 0 0 0);随着输入增大,输出向 1 1 1靠近(变成 1 1 1)”。也就是说,当输入信号为重要信息时,阶跃函数和 s i g m o i d sigmoid sigmoid函数都会输出较大的值;当输入信号为不重要的信息时,两者都输出较小的值。
阶跃函数和 s i g m o i d sigmoid sigmoid函数还有其他共同点,就是两者均为非线性函数。神经网络的激活函数必须使用非线性函数。换句话说,激活函数不能使用线性函数。为什么不能使用线性函数呢?因为使用线性函数的话,加深神经网络的层数就没有意义了。
接下来介绍另一个十分重要的激活函数: R e L U ReLU ReLU函数。 R e L U ReLU ReLU函数在输入大于 0 0 0时,直接输出该值;在输入小于等于 0 0 0时,输出 0 0 0,可以用下式表示:
其代码实现以及函数图像如下:
def relu(x):
return np.maximum(0, x)
多维数组就是“数字的集合”,数字排成一列的集合、排成长方形的集合、排成三维状或者(更加一般化的) N N N维状的集合都称为多维数组。
A = np.array([1, 2, 3, 4])
np.ndim(A) # 1,获得数组的维数
A.shape # (4,)
A.shape[0] # 4
B = np.array([[1, 2], [3, 4], [5, 6]])
np.ndim(B) # 2
B.shape # (3, 2)
下面,我们来介绍矩阵(二维数组)的乘积。比如 2 × 2 2\times 2 2×2的矩阵,其乘积可以像下图这样进行计算:
矩阵的乘积是通过左边矩阵的行(横向)和右边矩阵的列(纵向)以对应元素的方式相乘后再求和而得到的。这个运算在Python中可以用如下代码实现:
A = np.array([[1, 2], [3, 4]])
A.shape # (2, 2)
B = np.array([[5, 6], [7, 8]])
B.shape # (2, 2)
np.dot(A, B) # array([[19, 22], [43, 50]]),dot()称为点积运算
需要注意的是,在多维数组的乘积运算中,必须使两个矩阵中的对应维度的元素个数一致,如下图所示:
下面我们使用NumPy矩阵来实现神经网络。这里我们以下图中的简单神经网络为对象。这个神经网络省略了偏置和激活函数,只有权重:
在介绍神经网络中的处理之前,我们先导入 w 12 ( 1 ) w_{12}^{(1)} w12(1)、 a 1 ( 1 ) a_{1}^{(1)} a1(1)等符号。请看下图,下图中只突出显示了从输入层神经元 x 2 x_2 x2到后一层的神经元 a 1 ( 1 ) a_{1}^{(1)} a1(1)的权重。权重和隐藏层的神经元的右上角有一个 “ ( 1 ) ” “(1)” “(1)”,它表示权重和神经元的层号(即第 1 1 1层的权重、第 1 1 1层的神经元)。此外,权重的右下角有两个数字,它们是后一层的神经元和前一层的神经元的索引号。比如, w 12 ( 1 ) w_{12}^{(1)} w12(1)表示前一层的第 2 2 2个神经元 x 2 x_2 x2到后一层的第 1 1 1个神经元 a 1 ( 1 ) a_{1}^{(1)} a1(1)的权重。
现在看一下从输入层到第 1 1 1层的第 1 1 1个神经元的信号传递过程,如下图所示:
现在用数学式表示 a 1 ( 1 ) a_{1}^{(1)} a1(1)。 a 1 ( 1 ) a_{1}^{(1)} a1(1)通过加权信号和偏置的和按该公式进行计算: a 1 ( 1 ) = w 11 ( 1 ) x 1 + w 12 ( 1 ) x 2 + b 1 ( 1 ) a_{1}^{(1)}=w_{11}^{(1)}x_1+w_{12}^{(1)}x_2+b_{1}^{(1)} a1(1)=w11(1)x1+w12(1)x2+b1(1)。
使用矩阵的乘法运算进行计算的过程如下:
下面我们用NumPy多维数组来实现上式,这里将输入信号、权重、偏置设置成任意值:
X = np.array([1.0, 0.5])
W1 = np.array([[0.1, 0.3, 0.5], [0.2, 0.4, 0.6]])
B1 = np.array([0.1, 0.2, 0.3])
print(W1.shape) # (2, 3)
print(X.shape) # (2,)
print(B1.shape) # (3,)
A1 = np.dot(X, W1) + B1
接下来,我们观察第 1 1 1层中激活函数的计算过程。如果把这个计算过程用图来表示的话,则如下图所示:
隐藏层的加权和(加权信号和偏置的总和)用 a a a表示,被激活函数转换后的信号用 z z z表示。此外,图中 h ( ) 4 表示激活函数,这里我们使用的是 h()4表示激活函数,这里我们使用的是 h()4表示激活函数,这里我们使用的是sigmoid$函数。用Python来实现,代码如下所示:
Z1 = sigmoid(A1)
print(A1) # [0.3, 0.7, 1.1]
print(Z1) # [0.57444252, 0.66818777, 0.75026011]
下面,我们来实现第 1 1 1层到第 2 2 2层的信号传递:
W2 = np.array([[0.1, 0.4], [0.2, 0.5], [0.3, 0.6]])
B2 = np.array([0.1, 0.2])
print(Z1.shape) # (3,)
print(W2.shape) # (3, 2)
print(B2.shape) # (2,)
A2 = np.dot(Z1, W2) + B2
Z2 = sigmoid(A2)
最后是第 2 2 2层到输出层的信号传递。输出层的实现也和之前的实现基本相同。不过,最后的激活函数和之前的隐藏层有所不同。这里我们定义了 i d e n t i t y _ f u n c t i o n ( ) identity\_function() identity_function()函数(也称为“恒等函数”),并将其作为输出层的激活函数。恒等函数会将输入按原样输出,因此,这个例子中没有必要特意定义 i d e n t i t y _ f u n c t i o n ( ) identity\_function() identity_function()。这里这样实现只是为了和之前的流程保持统一。
def identity_function(x):
return x
W3 = np.array([[0.1, 0.3], [0.2, 0.4]])
B3 = np.array([0.1, 0.2])
A3 = np.dot(Z2, W3) + B3
Y = identity_function(A3) # 或者Y = A3
至此,我们已经介绍完了 3 3 3层神经网络的实现。现在我们把之前的代码实现全部整理一下。这里,我们按照神经网络的实现惯例,只把权重记为大写字母 W 1 W1 W1,其他的(偏置或中间结果等)都用小写字母表示。
def init_network():
network = {}
network['W1'] = np.array([[0.1, 0.3, 0.5], [0.2, 0.4, 0.6]])
network['b1'] = np.array([0.1, 0.2, 0.3])
network['W2'] = np.array([[0.1, 0.4], [0.2, 0.5], [0.3, 0.6]])
network['b2'] = np.array([0.1, 0.2])
network['W3'] = np.array([[0.1, 0.3], [0.2, 0.4]])
network['b3'] = np.array([0.1, 0.2])
return network
def forward(network, x):
W1, W2, W3 = network['W1'], network['W2'], network['W3']
b1, b2, b3 = network['b1'], network['b2'], network['b3']
a1 = np.dot(x, W1) + b1
z1 = sigmoid(a1)
a2 = np.dot(z1, W2) + b2
z2 = sigmoid(a2)
a3 = np.dot(z2, W3) + b3
y = identity_function(a3)
return y
network = init_network()
x = np.array([1.0, 0.5])
y = forward(network, x)
print(y) # [ 0.31682708 0.69627909]
神经网络可以用在分类问题和回归问题上,不过需要根据情况改变输出层的激活函数。一般而言,回归问题用恒等函数,分类问题用 s o f t m a x softmax softmax函数。
恒等函数会将输入按原样输出,对于输入的信息,不加以任何改动地直接输出。分类问题中使用的 s o f t m a x softmax softmax函数可以用下面的式子表示:
用Python实现:
def softmax(a):
exp_a = np.exp(a)
sum_exp_a = np.sum(exp_a)
y = exp_a / sum_exp_a
return y
观察代码,我们发现由于指数函数存在溢出问题,如果在这些超大值之间进行除法运算,结果会出现“不确定”的情况。因此我们需要对公式进行改进:
因此在进行 s o f t m a x softmax softmax的指数函数的运算时,加上(或者减去)某个常数并不会改变运算的结果。这里的 C ′ C' C′可以使用任何值,但是为了防止溢出,一般会使用输入信号中的最大值。例如:
a = np.array([1010, 1000, 990])
np.exp(a) / np.sum(np.exp(a)) # softmax函数的运算
# 返回结果为array([nan, nan, nan]),没有被正确计算
c = np.max(a) # 1010
a - c # array([0, -10, -20])
np.exp(a - c) / np.sum(np.exp(a - c))
# 返回结果为array([9.99954600e-01, 4.53978686e-05, 2.06106005e-09])
综上,我们可以像下面这样实现 s o f t m a x softmax softmax函数:
def softmax(a):
c = np.max(a)
exp_a = np.exp(a - c) # 溢出对策
sum_exp_a = np.sum(exp_a)
y = exp_a / sum_exp_a
return y
s o f t m a x softmax softmax函数的输出是 0.0 0.0 0.0到 1.0 1.0 1.0之间的实数。并且, s o f t m a x softmax softmax函数的输出值的总和是 1 1 1。输出总和为 1 1 1是 s o f t m a x softmax softmax函数的一个重要性质。正因为有了这个性质,我们才可以把 s o f t m a x softmax softmax函数的输出解释为“概率”。
输出层的神经元数量需要根据待解决的问题来决定。对于分类问题,输出层的神经元数量一般设定为类别的数量。比如,对于某个输入图像,预测是图中的数字 0 0 0到 9 9 9中的哪一个的问题( 10 10 10类别分类问题),可以像下图这样,将输出层的神经元设定为 10 10 10个(假定 y 2 y_2 y2的输出值最大)。
假设神经网络的学习已经全部结束,我们使用学习到的参数,先实现神经网络的“推理处理”。这个推理处理也称为神经网络的前向传播。
这里使用的数据集是MNIST手写数字图像集。MNIST是机器学习领域最有名的数据集之一,被应用于从简单的实验到发表的论文研究等各种场合。
MNIST的图像数据是 28 × 28 28\times 28 28×28像素的灰度图像( 1 1 1通道),各个像素的取值在 0 0 0到 255 255 255之间。每个图像数据都相应地标有 “ 7 ” , “ 2 ” , “ 1 ” “7”,“2”,“1” “7”,“2”,“1”等标签。
假设已经提供了便利的Python脚本mnist.py
,该脚本支持从下载MNIST数据集到将这些数据转换成NumPy数组等处理,使用mnist.py
中的 l o a d _ m n i s t ( ) load\_mnist() load_mnist()函数,就可以按下述方式轻松读入MNIST数据。
import sys, os
sys.path.append('D:\VS Code Project\Deep Learning') # 为了导入父目录中的文件而进行的设定
from dataset.mnist import load_mnist
# 第一次调用会花费几分钟 ……
(x_train, t_train), (x_test, t_test) = load_mnist(flatten=True, normalize=False)
# 输出各个数据的形状
print(x_train.shape) # (60000, 784)
print(t_train.shape) # (60000,)
print(x_test.shape) # (10000, 784)
print(t_test.shape) # (10000,)
load_mnist
函数以(训练图像, 训练标签), (测试图像, 测试标签)
的形式返回读入的MNIST数据。此外,还可以像load_mnist(normalize=True, flatten=True, one_hot_label=False)
这样,设置 3 3 3个参数。第 1 1 1个参数normalize
设置是否将输入图像正规化为 0.0 ∼ 1.0 0.0\sim 1.0 0.0∼1.0的值。如果将该参数设置为False
,则输入图像的像素会保持原来的 0 ∼ 255 0\sim 255 0∼255。第 2 2 2个参数flatten
设置是否展开输入图像(变成一维数组)。如果将该参数设置为False
,则输入图像为 1 × 28 × 28 1\times 28\times 28 1×28×28的三维数组;若设置为True
,则输入图像会保存为由 784 784 784个元素构成的一维数组。第 3 3 3个参数one_hot_label
设置是否将标签保存为 o n e − h o t one-hot one−hot表示( o n e − h o t r e p r e s e n t a t i o n one-hot\ representation one−hot representation)。 o n e − h o t one-hot one−hot表示是仅正确解标签为 1 1 1,其余皆为 0 0 0的数组,就像[0,0,1,0,0,0,0,0,0,0]
这样。当one_hot_label
为False
时,只是像 7 , 2 7,2 7,2这样简单保存正确解标签;当one_hot_label
为True
时,标签则保存为 o n e − h o t one-hot one−hot表示。
接下来使用PIL模块显示训练图像的第一张图像:
import sys, os
sys.path.append('D:\VS Code Project\Deep Learning')
import numpy as np
from dataset.mnist import load_mnist
from PIL import Image
def img_show(img):
pil_img = Image.fromarray(np.uint8(img))
pil_img.show()
(x_train, t_train), (x_test, t_test) = load_mnist(flatten=True, normalize=False)
img = x_train[0]
label = t_train[0]
print(label) # 5
print(img.shape) # (784,)
img = img.reshape(28, 28) # 把图像的形状变成原来的尺寸
print(img.shape) # (28, 28)
img_show(img)
显示结果如下图所示:
神经网络的输入层有 784 784 784个神经元,输出层有 10 10 10个神经元。输入层的 784 784 784这个数字来源于图像大小的 28 × 28 = 784 28\times 28=784 28×28=784,输出层的 10 10 10这个数字来源于 10 10 10类别分类(数字 0 ∼ 9 0\sim 9 0∼9,共 10 10 10类别)。此外,这个神经网络有 2 2 2个隐藏层,第 1 1 1个隐藏层有 50 50 50个神经元,第 2 2 2个隐藏层有 100 100 100个神经元。这个 50 50 50和 100 100 100可以设置为任何值。
下面我们先定义 3 3 3个函数:
def get_data():
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, flatten=True, one_hot_label=False)
return x_test, t_test
def init_network():
with open("sample_weight.pkl", 'rb') as f:
network = pickle.load(f)
return network
def predict(network, x):
W1, W2, W3 = network['W1'], network['W2'], network['W3']
b1, b2, b3 = network['b1'], network['b2'], network['b3']
a1 = np.dot(x, W1) + b1
z1 = sigmoid(a1)
a2 = np.dot(z1, W2) + b2
z2 = sigmoid(a2)
a3 = np.dot(z2, W3) + b3
y = softmax(a3)
return y
init_network()
会读入保存在pickle
文件sample_weight.pkl
中的学习到的权重参数 A A A。这个文件中以字典变量的形式保存了权重和偏置参数。剩余的 2 2 2个函数和前面介绍的代码实现基本相同,无需再解释。现在,我们用这 3 3 3个函数来实现神经网络的推理处理,并评价它的识别精度:
x, t = get_data()
network = init_network()
accuracy_cnt = 0
for i in range(len(x)): # 逐一取出保存在x中的图像数据
y = predict(network, x[i])
p = np.argmax(y) # 获取概率最高的元素的索引
if p == t[i]:
accuracy_cnt += 1
print("Accuracy:" + str(float(accuracy_cnt) / len(x)))
# Accuracy:0.9352
下面我们使用Python解释器,输出刚才的神经网络的各层的权重的形状:
x, _ = get_data()
network = init_network()
W1, W2, W3 = network['W1'], network['W2'], network['W3']
x.shape # (10000, 784)
x[0].shape # (784,)
W1.shape # (784, 50)
W2.shape # (50, 100)
W3.shape # (100, 10)
确认矩阵的形状:
现在我们来考虑打包输入多张图像的情形。比如,我们想用predict()
函数一次性打包处理 100 100 100张图像。为此,可以把 x x x的形状改为 100 × 784 100\times 784 100×784,将 100 100 100张图像打包作为输入数据,这种打包式的输入数据称为批( b a t c h batch batch)。批有“捆”的意思,图像就如同纸币一样扎成一捆,如下图所示:
下面我们进行基于批处理的代码实现:
x, t = get_data()
network = init_network()
batch_size = 100 # 批数量
accuracy_cnt = 0
for i in range(0, len(x), batch_size):
x_batch = x[i:i + batch_size]
y_batch = predict(network, x_batch)
p = np.argmax(y_batch, axis=1)
accuracy_cnt += np.sum(p == t[i:i + batch_size])
print("Accuracy:" + str(float(accuracy_cnt) / len(x)))
神经网络的特征就是可以从数据中学习。所谓“从数据中学习”,是指可以由数据自动决定权重参数的值。利用特征量和机器学习的方法中,特征量仍是由人工设计的,而在神经网络中,连图像中包含的重要特征量也都是由机器来学习的。
机器学习中,一般将数据分为训练数据和测试数据两部分来进行学习和实验等。首先,使用训练数据进行学习,寻找最优的参数;然后,使用测试数据评价训练得到的模型的实际能力。为什么需要将数据分为训练数据和测试数据呢?因为我们追求的是模型的泛化能力。为了正确评价模型的泛化能力,就必须划分训练数据和测试数据。另外,训练数据也可以称为监督数据。
泛化能力是指处理未被观察过的数据(不包含在训练数据中的数据)的能力。获得泛化能力是机器学习的最终目标。只对某个数据集过度拟合的状态称为过拟合( o v e r f i t t i n g over\ fitting over fitting)。避免过拟合也是机器学习的一个重要课题。
神经网络的学习通过某个指标表示现在的状态。然后,以这个指标为基准,寻找最优权重参数。神经网络的学习中所用的指标称为损失函数( l o s s f u n c t i o n loss\ function loss function)。这个损失函数可以使用任意函数,但一般用均方误差和交叉熵误差等。
均方误差如下式所示,其中, y k y_k yk表示神经网络的输出, t k t_k tk表示监督数据, k k k表示数据的维数,均方误差会计算神经网络的输出和正确解监督数据的各个元素之差的平方,再求总和:
使用Python实现代码如下:
def mean_squared_error(y, t):
return 0.5 * np.sum((y - t) ** 2)
举个例子计算一下:
# 设2为正确解
t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]
# 例1:2的概率最高的情况(0.6)
y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]
mean_squared_error(np.array(y), np.array(t)) # 0.097500000000000031
# 例2:7的概率最高的情况(0.6)
y = [0.1, 0.05, 0.1, 0.0, 0.05, 0.1, 0.0, 0.6, 0.0, 0.0]
mean_squared_error(np.array(y), np.array(t)) # 0.59750000000000003
交叉熵误差如下式所示,其中, y k y_k yk表示神经网络的输出, t k t_k tk表示正确解标签,且 t k t_k tk中只有正确解标签的索引为 1 1 1,其他均为 0 0 0( o n e − h o t one-hot one−hot表示):
该式实际上只计算对应正确解标签的输出的自然对数。比如,假设正确解标签的索引是 “ 2 ” “2” “2”,与之对应的神经网络的输出是 0.6 0.6 0.6,则交叉熵误差是 − l o g 0.6 = 0.51 -log\ 0.6=0.51 −log 0.6=0.51;若 “ 2 ” “2” “2”对应的输出是 0.1 0.1 0.1,则交叉熵误差为 − l o g 0.1 = 2.30 -log\ 0.1=2.30 −log 0.1=2.30。也就是说,交叉熵误差的值是由正确解标签所对应的输出结果决定的。
使用Python实现代码如下:
def cross_entropy_error(y, t):
delta = 1e-7
return -np.sum(t * np.log(y + delta))
函数内部在计算np.log
时,加上了一个微小值delta
。这是因为,当出现np.log(0)
时,结果会变为负无穷大的,这样一来就会导致后续计算无法进行。
举个例子计算一下:
t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]
y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]
cross_entropy_error(np.array(y), np.array(t)) # 0.51082545709933802
y = [0.1, 0.05, 0.1, 0.0, 0.05, 0.1, 0.0, 0.6, 0.0, 0.0]
cross_entropy_error(np.array(y), np.array(t)) # 2.3025840929945458
使用训练数据进行学习,严格来说,就是针对训练数据计算损失函数的值,找出使该值尽可能小的参数。计算损失函数时必须将所有的训练数据作为对象。计算损失函数时必须将所有的训练数据作为对象,如果要求所有训练数据的损失函数的总和,以交叉熵误差为例,可以写成下面的式子:
假设数据有 N N N个, t n k t_{nk} tnk表示第 n n n个数据的第 k k k个元素的值( y n k y_{nk} ynk是神经网络的输出, t n k t_{nk} tnk是监督数据)。
由于MNIST数据集的训练数据有 60000 60000 60000个,如果以全部数据为对象求损失函数的和,则计算过程需要花费较长的时间。因此,我们从全部数据中选出一部分,作为全部数据的“近似”。神经网络的学习也是从训练数据中选出一批数据(称为 m i n i − b a t c h mini-batch mini−batch,小
批量),然后对每个 m i n i − b a t c h mini-batch mini−batch进行学习。
假设要从这个训练数据中随机抽取10笔数据,可以使用如下代码,使用np.random.choice()
可以从指定的数字中随机选择想要的数字。比如,np.random.choice(60000, 10)
会从 0 0 0到 59999 59999 59999之间随机选择 10 10 10个数字:
train_size = x_train.shape[0]
batch_size = 10
batch_mask = np.random.choice(train_size, batch_size)
x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]
改良交叉熵误差代码,实现一个可以同时处理单个数据和批量数据(数据作为 b a t c h batch batch集中输入)两种情况的函数:
def cross_entropy_error(y, t):
if y.ndim == 1:
t = t.reshape(1, t.size)
y = y.reshape(1, y.size)
batch_size = y.shape[0]
return -np.sum(t * np.log(y + 1e-7)) / batch_size
当监督数据是标签形式(非 o n e − h o t one-hot one−hot表示,而是像 “ 2 ” , “ 7 ” “2”,“7” “2”,“7”这样的标签)时,交叉熵误差可通过如下代码实现:
def cross_entropy_error(y, t):
if y.ndim == 1:
t = t.reshape(1, t.size)
y = y.reshape(1, y.size)
batch_size = y.shape[0]
return -np.sum(np.log(y[np.arange(batch_size), t] + 1e-7)) / batch_size
# 假设batch_size = 5, t = [2, 7, 0, 9, 4]
# 则y[np.arange(batch_size), t] = [y[0, 2], y[1, 7], y[2, 0], y[3, 9], y[4, 4]]
导数就是表示某个瞬间的变化量,它可以定义成下面的式子:
但是该式子计算时存在误差,因为真的导数是对应函数在 x x x处的斜率(称为切线),但上述实现中计算的导数对应的是 ( x + h ) (x+h) (x+h)和 x x x之间的斜率(如下图所示)。为了减小这个误差,我们可以计算函数 f f f在 ( x + h ) (x+h) (x+h)和 ( x − h ) (x-h) (x−h)之间的差分。因为这种计算方法以 x x x为中心,计算它左右两边的差分,所以也称为中心差分(而 ( x + h ) (x+h) (x+h)和 x x x之间的差分称为前向差分)。
使用Python实现代码如下:
def numerical_diff(f, x):
h = 1e-4 # 0.0001
return (f(x + h) - f(x - h)) / (2 * h)
现在假设我们有一个一元二次函数: y = 0.01 x 2 + 0.1 x y=0.01x^2+0.1x y=0.01x2+0.1x,使用Python实现该式如下:
def function_1(x):
return 0.01 * x ** 2 + 0.1 * x
我们来计算一下这个函数在 x = 5 x=5 x=5 和 x = 10 和x=10 和x=10处的导数:
numerical_diff(function_1, 5) # 0.1999999999990898
numerical_diff(function_1, 10) # 0.2999999999986347
接下来我们来看另一个函数: f ( x 0 , x 1 ) = x 0 2 + x 1 2 f(x_0,x_1)=x_0^2+x_1^2 f(x0,x1)=x02+x12,和上例不同的是,这里有两个变量,使用Python实现该式如下:
def function_2(x):
return x[0] ** 2 + x[1] ** 2
# 或者return np.sum(x ** 2)
因为该式有两个变量,因此有必要区分对哪个变量求导数,有多个变量的函数的导数称为偏导数。用数学式表示的话,可以写成 ∂ f ∂ x 0 , ∂ f ∂ x 1 \frac {\partial f}{\partial x_0},\frac {\partial f}{\partial x_1} ∂x0∂f,∂x1∂f。偏导数需要将多个变量中的某一个变量定为目标变量,并将其他变量固定为某个值(即常数)。
例如求 x 0 = 3 , x 1 = 4 x_0=3,x_1=4 x0=3,x1=4时,关于 x 0 x_0 x0的偏导数 ∂ f ∂ x 0 \frac {\partial f}{\partial x_0} ∂x0∂f:
def function_tmp1(x0):
return x0 * x0 + 4.0 ** 2.0
numerical_diff(function_tmp1, 3.0) # 6.00000000000378
像 ( ∂ f ∂ x 0 , ∂ f ∂ x 1 ) (\frac {\partial f}{\partial x_0},\frac {\partial f}{\partial x_1}) (∂x0∂f,∂x1∂f)这样的由全部变量的偏导数汇总而成的向量称为梯度( g r a d i e n t gradient gradient),梯度指示的方向是各点处的函数值减小最多的方向。梯度可以像下面这样来实现:
def numerical_gradient(f, x):
h = 1e-4 # 0.0001
grad = np.zeros_like(x) # 会生成一个形状和x相同,所有元素都为0的数组
it = np.nditer(x, flags = ['multi_index'], op_flags = ['readwrite'])
while not it.finished:
idx = it.multi_index
tmp_val = x[idx]
x[idx] = float(tmp_val) + h
fxh1 = f(x) # f(x + h)
x[idx] = float(tmp_val) - h
fxh2 = f(x) # f(x - h)
grad[idx] = (fxh1 - fxh2) / (2 * h)
x[idx] = tmp_val # 还原值
it.iternext()
return grad
神经网络必须在学习时找到最优参数(权重和偏置)。这里所说的最优参数是指损失函数取最小值时的参数,通过巧妙地使用梯度来寻找函数最小值(或者尽可能小的值)的方法就是梯度法。由于梯度表示的是各点处的函数值减小最多的方向,无法保证梯度所指的方向就是函数的最小值或者真正应该前进的方向。虽然梯度的方向并不一定指向最小值,但沿着它的方向能够最大限度地减小函数的值。
现在,我们尝试用数学式来表示梯度法,如下式所示:
式中的 η \eta η表示更新量,在神经网络的学习中,称为学习率( l e a r n i n g r a t e learning\ rate learning rate)。学习率决定在一次学习中,应该学习多少,以及在多大程度上更新参数。
学习率需要事先确定为某个值,比如 0.01 0.01 0.01或 0.001 0.001 0.001。一般而言,这个值过大或过小,都无法抵达一个“好的位置”。在神经网络的学习中,一般会一边改变学习率的值,一边确认学习是否正确进行了。
使用Python实现梯度下降法如下,其中参数 f f f是要进行最优化的函数, i n i t _ x init\_x init_x是初始值, l r lr lr是学习率 l e a r n i n g r a t e learning\ rate learning rate, s t e p _ n u m step\_num step_num是梯度法的重复次数:
def gradient_descent(f, init_x, lr=0.01, step_num=100):
x = init_x
for i in range(step_num):
grad = numerical_gradient(f, x)
x -= lr * grad
return x
实验结果表明,学习率过大的话,会发散成一个很大的值;反过来,学习率过小的话,基本上没怎么更新就结束了。也就是说,设定合适的学习率是一个很重要的问题。
神经网络中的梯度是指损失函数关于权重参数的梯度:
我们以一个简单的神经网络为例,来实现求梯度的代码。为此,我们要实现一个名为simpleNet
的类:
class simpleNet:
def __init__(self):
self.W = np.random.randn(2, 3) # 用高斯分布进行初始化
def predict(self, x):
return np.dot(x, self.W)
def loss(self, x, t):
z = self.predict(x)
y = softmax(z)
loss = cross_entropy_error(y, t)
return loss
试着用一下simpleNet
:
net = simpleNet()
x = np.array([0.6, 0.9])
p = net.predict(x)
print(p) # [1.05414809, 0.63071653, 1.1328074]
np.argmax(p) # 2,最大值的索引
t = np.array([0, 0, 1]) # 正确解标签
net.loss(x, t) # 0.92806853663411326
接下来求梯度:
def f(W):
return net.loss(x, t)
dW = numerical_gradient(f, net.W)
Python中如果定义的是简单的函数,可以使用 l a m b d a lambda lambda表示法。使用 l a m b d a lambda lambda的情况下,上述代码可以如下实现:
f = lambda w: net.loss(x, t)
dW = numerical_gradient(f, net.W)
我们先来确认一下神经网络的学习步骤,顺便复习一下这些内容:
神经网络的学习按照上面 4 4 4个步骤进行。这个方法通过梯度下降法更新参数,不过因为这里使用的数据是随机选择的 m i n i − b a t c h mini-batch mini−batch数据,所以又称为随机梯度下降法。
下面,我们来实现手写数字识别的 2 2 2层神经网络(隐藏层为 1 1 1层)。我们将这个 2 2 2层神经网络实现为一个名为TwoLayerNet
的类,实现过程如下所示:
class TwoLayerNet:
def __init__(self, input_size, hidden_size, output_size, weight_init_std = 0.01):
# 初始化权重
self.params = {}
self.params['W1'] = weight_init_std * np.random.randn(input_size, hidden_size)
self.params['b1'] = np.zeros(hidden_size)
self.params['W2'] = weight_init_std * np.random.randn(hidden_size, output_size)
self.params['b2'] = np.zeros(output_size)
def predict(self, x):
W1, W2 = self.params['W1'], self.params['W2']
b1, b2 = self.params['b1'], self.params['b2']
a1 = np.dot(x, W1) + b1
z1 = sigmoid(a1)
a2 = np.dot(z1, W2) + b2
y = softmax(a2)
return y
def loss(self, x, t):
y = self.predict(x)
return cross_entropy_error(y, t)
def accuracy(self, x, t):
y = self.predict(x)
y = np.argmax(y, axis = 1)
t = np.argmax(t, axis = 1)
accuracy = np.sum(y == t) / float(x.shape[0])
return accuracy
def numerical_gradient(self, x, t):
loss_W = lambda W: self.loss(x, t)
grads = {}
grads['W1'] = numerical_gradient(loss_W, self.params['W1'])
grads['b1'] = numerical_gradient(loss_W, self.params['b1'])
grads['W2'] = numerical_gradient(loss_W, self.params['W2'])
grads['b2'] = numerical_gradient(loss_W, self.params['b2'])
return grads
接下来,我们就以TwoLayerNet
类为对象,使用MNIST数据集进行学习:
import sys
sys.path.append('D:\VS Code Project\Deep Learning')
import numpy as np
import matplotlib.pylab as plt
from dataset.mnist import load_mnist
from two_layer_net import TwoLayerNet
# 读入数据
(x_train, t_train), (x_test, t_test) = load_mnist(normalize = True, one_hot_label = True)
network = TwoLayerNet(input_size = 784, hidden_size = 50, output_size = 10)
# 超参数
iters_num = 10000 # 适当设定循环的次数
train_size = x_train.shape[0]
batch_size = 100
learning_rate = 0.1 # 学习率
train_loss_list = []
train_acc_list = []
test_acc_list = []
iter_per_epoch = max(train_size / batch_size, 1)
for i in range(iters_num):
# 获取mini-batch
batch_mask = np.random.choice(train_size, batch_size)
x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]
# 计算梯度
grad = network.numerical_gradient(x_batch, t_batch)
# grad = network.gradient(x_batch, t_batch) # 误差反向传播法高速计算梯度
# 更新参数
for key in ('W1', 'b1', 'W2', 'b2'):
network.params[key] -= learning_rate * grad[key]
# 记录学习过程
loss = network.loss(x_batch, t_batch)
train_loss_list.append(loss)
if i % iter_per_epoch == 0:
train_acc = network.accuracy(x_train, t_train)
test_acc = network.accuracy(x_test, t_test)
train_acc_list.append(train_acc)
test_acc_list.append(test_acc)
print("train acc, test acc | " + str(train_acc) + ", " + str(test_acc))
# 绘制图形
markers = {'train': 'o', 'test': 's'}
x = np.arange(len(train_acc_list))
plt.plot(x, train_acc_list, label = 'train acc')
plt.plot(x, test_acc_list, label = 'test acc', linestyle = '--')
plt.xlabel("epochs")
plt.ylabel("accuracy")
plt.ylim(0, 1.0)
plt.legend(loc = 'lower right')
plt.show()
神经网络学习的最初目标是掌握泛化能力,因此,要评价神经网络的泛化能力,就必须使用不包含在训练数据中的数据。上述的代码在进行学习的过程中,会定期地对训练数据和测试数据记录识别精度。这里,每经过一个 e p o c h epoch epoch,我们都会记录下训练数据和测试数据的识别精度。 e p o c h epoch epoch是一个单位。一个 e p o c h epoch epoch表示学习中所有训练数据均被使用过一次时的更新次数。比如,对于 10000 10000 10000笔训练数据,用大小为 100 100 100笔数据的 m i n i − b a t c h mini-batch mini−batch进行学习时,重复随机梯度下降法 100 100 100次,就相当于所有的训练数据就都被“看过”了。
绘图结果如下图所示:
如上图所示,随着 e p o c h epoch epoch的前进(学习的进行),我们发现使用训练数据和测试数据评价的识别精度都提高了,并且,这两个识别精度基本上没有差异(两条线基本重叠在一起)。因此,可以说这次的学习中没有发生过拟合的现象。
先引入一个很简单的问题:在超市买了 2 2 2个 100 100 100元一个的苹果,消费税是 10 % 10\% 10%,请计算支付金额。我们画出计算图如下:
接着进行简单的修改,如下图所示:
现在我们换个问题:在超市买了 2 2 2个苹果、 3 3 3个橘子。其中,苹果每个 100 100 100元,橘子每个 150 150 150元。消费税是 10 % 10\% 10%,请计算支付金额。同样,我们画出相应的计算图如下:
在计算图上,我们是从左向右进行计算的,是一种正方向上的传播,简称为正向传播( f o r w a r d p r o p a g a t i o n forward\ propagation forward propagation)。既然有正向传播这个名称,当然也可以考虑反向(从图上看的话,就是从右向左)的传播。实际上,这种传播称为反向传播( b a c k w a r d p r o p a g a t i o n backward\ propagation backward propagation)。反向传播将在接下来的导数计算中发挥重要作用。
计算图的特征是可以通过传递局部计算获得最终结果。即无论全局发生了什么,都能只根据与自己相关的信息输出接下来的结果。比如,在超市买了 2 2 2个苹果和其他很多东西,可以用以下计算图表示:
这里的重点是,各个节点处的计算都是局部计算。这意味着,例如苹果和其他很多东西的求和运算( 4000 + 200 = 4200 4000+200=4200 4000+200=4200)并不关心 4000 4000 4000这个数字是如何计算而来的,只要把两个数字相加就可以了。此外,计算图的另一个优点是可以将中间的计算结果全部保存起来。
假设我们想知道苹果价格的上涨会在多大程度上影响最终的支付金额,即求支付金额关于苹果的价格的导数。设苹果的价格为 x x x,支付金额为 L L L,则相当于求 ∂ L ∂ x \frac {\partial L}{\partial x} ∂x∂L,这个导数的值表示当苹果的价格稍微上涨时,支付金额会增加多少。
这个值可以通过计算图的反向传播求出来。先看一下结果,如下图所示:
从这个结果中可知,“支付金额关于苹果的价格的导数”的值是 2.2 2.2 2.2。这意味着,如果苹果的价格上涨 1 1 1元,最终的支付金额会增加 2.2 2.2 2.2元。
反向传播将局部导数向正方向的反方向(从右到左)传递,其原理是基于链式法则的。我们先来看一个使用计算图的反向传播的例子。假设存在 y = f ( x ) y=f(x) y=f(x)的计算,其反向传播如下图所示:
反向传播的计算顺序是,将信号E乘以节点的局部导数 ∂ y ∂ x \frac {\partial y}{\partial x} ∂x∂y,然后将结果传递给下一个节点。这里所说的局部导数是指正向传播中 y = f ( x ) y=f(x) y=f(x)的导数。
介绍链式法则时,我们需要先从复合函数说起。复合函数是由多个函数构成的函数。比如, z = ( x + y ) 2 z=(x+y)^2 z=(x+y)2是由以下两个式子构成的:
链式法则是关于复合函数的导数的性质,定义为:如果某个函数由复合函数表示,则该复合函数的导数可以用构成复合函数的各个函数的导数的乘积表示。
以上式为例, ∂ z ∂ x \frac {\partial z}{\partial x} ∂x∂z可以用 ∂ z ∂ t \frac {\partial z}{\partial t} ∂t∂z和 ∂ t ∂ x \frac {\partial t}{\partial x} ∂x∂t的乘积表示,即:
现在我们尝试将以上链式法则的计算用计算图表示出来:
根据链式法则, ∂ z ∂ z ∂ z ∂ t ∂ t ∂ x = ∂ z ∂ t ∂ t ∂ x = ∂ z ∂ x \frac {\partial z}{\partial z}\frac {\partial z}{\partial t}\frac {\partial t}{\partial x}=\frac {\partial z}{\partial t}\frac {\partial t}{\partial x}=\frac {\partial z}{\partial x} ∂z∂z∂t∂z∂x∂t=∂t∂z∂x∂t=∂x∂z成立,对应“ z z z关于 x x x的导数”。也就是说,反向传播是基于链式法则的。
首先来考虑加法节点的反向传播。这里以 z = x + y z=x+y z=x+y为对象,观察它的反向传播。 z = x + y z=x+y z=x+y的导数可由下式(解析性地)计算出来:
我们假定一个最终输出值为 L L L的大型计算图。 z = x + y z=x+y z=x+y的计算位于这个大型计算图的某个地方,从上游会传来 ∂ L ∂ z \frac {\partial L}{\partial z} ∂z∂L的值,并向下游传递 ∂ L ∂ x \frac {\partial L}{\partial x} ∂x∂L和 ∂ L ∂ y \frac {\partial L}{\partial y} ∂y∂L,如下图所示:
用计算图表示的话,如下图所示,因为加法节点的反向传播只乘以 1 1 1,所以输入的值会原封不动地流向下一个节点:
接下来,我们看一下乘法节点的反向传播。这里我们考虑 z = x y z=xy z=xy。这个式子的导数用下式表示:
乘法的反向传播会将上游的值乘以正向传播时的输入信号的“翻转值”后传递给下游,如下图所示:
假设有 10 × 5 = 50 10\times 5=50 10×5=50这一计算,反向传播时,从上游会传来值 1.3 1.3 1.3。用计算图表示的话如下图所示:
乘法的反向传播需要正向传播时的输入信号值。因此,实现乘法节点的反向传播时,要保存正向传播的输入信号。
使用反向传播计算最初的两个问题时,可画出计算图如下:
我们把要实现的计算图的乘法节点称为“乘法层”(MulLayer),加法节点称为“加法层”(AddLayer)。层的实现中有两个共通的方法(接口)forward()
和backward()
。forward()
对应正向传播,backward()
对应反向传播。
现在来实现乘法层。乘法层作为MulLayer
类,其实现过程如下所示:
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
现在我们使用MulLayer
实现前面的购买苹果的例子:
apple = 100
apple_num = 2
tax = 1.1
# layer
mul_apple_layer = MulLayer()
mul_tax_layer = MulLayer()
# forward
apple_price = mul_apple_layer.forward(apple, apple_num)
price = mul_tax_layer.forward(apple_price, tax)
print(price) # 220
# backward
dprice = 1
dapple_price, dtax = mul_tax_layer.backward(dprice)
dapple, dapple_num = mul_apple_layer.backward(dapple_price)
print(dapple, dapple_num, dtax) # 2.2 110 200
接下来,我们实现加法节点的加法层:
class AddLayer:
def __init__(self):
pass # 加法层不需要特意进行初始化
def forward(self, x, y):
out = x + y
return out
def backward(self, dout):
dx = dout * 1
dy = dout * 1
return dx, dy
使用MulLayer
和AddLayer
实现前面的购买苹果和橘子的例子:
apple = 100
apple_num = 2
orange = 150
orange_num = 3
tax = 1.1
# layer
mul_apple_layer = MulLayer()
mul_orange_layer = MulLayer()
add_apple_orange_layer = AddLayer()
mul_tax_layer = MulLayer()
# forward
apple_price = mul_apple_layer.forward(apple, apple_num) # (1)
orange_price = mul_orange_layer.forward(orange, orange_num) # (2)
all_price = add_apple_orange_layer.forward(apple_price, orange_price) # (3)
price = mul_tax_layer.forward(all_price, tax) # (4)
# backward
dprice = 1
dall_price, dtax = mul_tax_layer.backward(dprice) # (4)
dapple_price, dorange_price = add_apple_orange_layer.backward(dall_price) # (3)
dorange, dorange_num = mul_orange_layer.backward(dorange_price) # (2)
dapple, dapple_num = mul_apple_layer.backward(dapple_price) # (1)
print(price) # 715
print(dapple_num, dapple, dorange, dorange_num, dtax) # 110 2.2 3.3 165 650
这里,我们把构成神经网络的层实现为一个类。先来实现激活函数的 R e L U ReLU ReLU层和 S i g m o i d Sigmoid Sigmoid层。 R e L U ReLU ReLU如下式所示:
可以求出 y y y关于 x x x的导数如下:
因此 R e L U ReLU ReLU层的计算图如下图所示:
使用Python实现代码如下:
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
Relu
类有实例变量mask
。这个变量是由True/False
构成的NumPy数组,它会把正向传播时的输入 x x x的元素中小于等于 0 0 0的地方保存为True
,其他地方(大于 0 0 0的元素)保存为False
。
接下来,我们来实现 S i g m o i d Sigmoid Sigmoid函数,其公式如下:
其正向传播的计算图如下图所示,除了 “ × ” “\times” “×”和 “ + ” “+” “+”节点外,还出现了新的 “ e x p ” “exp” “exp”和 “ / ” “/” “/”节点。 “ e x p ” “exp” “exp”节点会进行 y = e x p ( x ) y=exp(x) y=exp(x)的计算, “ / ” “/” “/”节点会进行 y = 1 x y=\frac {1}{x} y=x1的计算:
现在我们来分析其反向传播流程:
(1) “ / ” “/” “/”节点表示 y = 1 x y=\frac {1}{x} y=x1它的导数可以解析性地表示为下式:
反向传播时,会将上游的值乘以 − y 2 -y^2 −y2后,再传给下游,计算图如下图所示:
(2) “ + ” “+” “+”节点将上游的值原封不动地传给下游,计算图如下图所示:
(3) “ e x p ” “exp” “exp”节点表示 y = e x p ( x ) y=exp(x) y=exp(x),它的导数由下式表示:
计算图中,上游的值乘以正向传播时的输出(这个例子中是 e x p ( − x ) exp(-x) exp(−x))后,再传给下游,计算图如下图所示:
(4) “ × ” “×” “×”节点将正向传播时的值翻转后做乘法运算。因此,这里要乘以 − 1 -1 −1。至此, S i g m o i d Sigmoid Sigmoid层整体的计算图如下图所示:
从上图的结果可知,反向传播的输出为 ∂ L ∂ y y 2 e x p ( − x ) \frac {\partial L}{\partial y}y^2exp(-x) ∂y∂Ly2exp(−x),这个值会传播给下游的节点。这里要注意, ∂ L ∂ y y 2 e x p ( − x ) \frac {\partial L}{\partial y}y^2exp(-x) ∂y∂Ly2exp(−x)这个值只根据正向传播时的输入 x x x和输出 y y y就可以算出来。因此,以上的计算图可以画成下图所示的集约化的 “ S i g m o i d ” “Sigmoid” “Sigmoid”节点:
另外, ∂ L ∂ y y 2 e x p ( − x ) \frac {\partial L}{\partial y}y^2exp(-x) ∂y∂Ly2exp(−x)可以进一步整理如下:
使用Python实现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
神经网络的正向传播中,为了计算加权信号的总和,使用了矩阵的乘积运算。现在将这里进行的求矩阵的乘积与偏置的和的运算用计算图表示出来。将乘积运算用 “ d o t ” “dot” “dot”节点表示的话,则 n p . d o t ( X , W ) + B np.dot(X,W)+B np.dot(X,W)+B的运算可用下图所示的计算图表示出来(注意: X , W , B X,W,B X,W,B是矩阵):
神经网络的正向传播中进行的矩阵乘积运算的层就称为 A f f i n e Affine Affine层。
现在我们来考虑上图的反向传播。以矩阵为对象的反向传播,按矩阵的各个元素进行计算时,步骤和以标量为对象的计算图相同。实际写一下的话,可以得到下式:
式中 W T W^T WT的 T T T表示转置。转置操作会把 W W W的元素 ( i , j ) (i,j) (i,j)换成元素 ( j , i ) (j,i) (j,i)。用数学式表示的话,可以写成下面这样:
现在我们尝试写出计算图的反向传播,如下图所示:
我们看一下上图中各个变量的形状。尤其要注意, X X X和 ∂ L ∂ X \frac {\partial L}{\partial X} ∂X∂L形状相同, W W W和 ∂ L ∂ W \frac {\partial L}{\partial W} ∂W∂L形状相同。从下面的数学式可以很明确地看出 X X X和 ∂ L ∂ X \frac {\partial L}{\partial X} ∂X∂L形状相同。
为什么要注意矩阵的形状呢?因为矩阵的乘积运算要求对应维度的元素个数保持一致,通过确认一致性,就可以很容易地推导出公式,如下图所示:
现在我们考虑 N N N个数据一起进行正向传播的情况,也就是批版本的 A f f i n e Affine Affine层。先给出批版本的 A f f i n e Affine Affine层的计算图如下图所示:
加上偏置时,需要特别注意。正向传播时,偏置被加到 X ⋅ W X\sdot W X⋅W的各个数据上。比如, N = 2 N=2 N=2(数据为 2 2 2个)时,偏置会被分别加到这 2 2 2个数据(各自的计算结果)上,具体的例子如下所示:
X_dot_W = np.array([[0, 0, 0], [10, 10, 10]])
B = np.array([1, 2, 3])
X_dot_W + B # array([[1, 2, 3], [11, 12, 13]])
正向传播时,偏置会被加到每一个数据(第 1 1 1个、第 2 2 2个、 … \dots …)上。因此,反向传播时,各个数据的反向传播的值需要汇总为偏置的元素。用代码表示的话,如下所示:
dY = np.array([[1, 2, 3], [4, 5, 6]])
dB = np.sum(dY, axis=0) # array([5, 7, 9])
综上所述, A f f i n e Affine Affine的实现如下所示:
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
最后介绍一下输出层的 S o f t m a x Softmax Softmax函数。前面我们提到过, S o f t m a x Softmax Softmax函数会将输入值正规化之后再输出。比如手写数字识别时, S o f t m a x Softmax Softmax层的输出如下图所示:
下面来实现 S o f t m a x Softmax Softmax层。考虑到这里也包含作为损失函数的交叉熵误差( c r o s s e n t r o p y e r r o r cross\ entropy\ error cross entropy error),所以称为 S o f t m a x − w i t h − L o s s Softmax-with-Loss Softmax−with−Loss层,其化简后的计算图如下图所示:
图中要注意的是反向传播的结果, ( y 1 − t 1 , y 2 − t 2 , y 3 − t 3 ) (y_1-t_1,y_2-t_2,y_3-t_3) (y1−t1,y2−t2,y3−t3)是 S o f t m a x Softmax Softmax层的输出和教师标签的差分。神经网络的反向传播会把这个差分表示的误差传递给前面的层,这是神经网络学习中的重要性质。
现在来进行 S o f t m a x − w i t h − L o s s Softmax-with-Loss Softmax−with−Loss层的实现,实现过程如下所示,请注意反向传播时,将要传播的值除以批的大小( b a t c h _ s i z e batch\_size batch_size)后,传递给前面的层的是单个数据的误差:
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
现在我们可以通过上一节实现的层来构建神经网络,误差反向传播法会在上一章构建神经网络的步骤(2)中出现,之前利用数值微分求得了这个梯度,现在使用误差反向传播法可以更高效地求解梯度。
同样我们实现一个 2 2 2层的神经网络TwoLayerNet
:
class TwoLayerNet:
def __init__(self, input_size, hidden_size, output_size, weight_init_std=0.01):
# initialize the weight
self.params = {}
self.params['W1'] = weight_init_std * np.random.randn(input_size, hidden_size)
self.params['b1'] = np.zeros(hidden_size)
self.params['W2'] = weight_init_std * np.random.randn(hidden_size, output_size)
self.params['b2'] = np.zeros(output_size)
# creat the layers
self.layers = OrderedDict()
self.layers['Affine1'] = Affine(self.params['W1'], self.params['b1'])
self.layers['Relu1'] = Relu()
self.layers['Affine2'] = Affine(self.params['W2'], self.params['b2'])
self.lastLayer = SoftmaxWithLoss()
def predict(self, x):
for layer in self.layers.values():
x = layer.forward(x)
return x
def loss(self, x, t):
y = self.predict(x)
return self.lastLayer.forward(y, t)
def accuracy(self, x, t):
y = self.predict(x)
y = np.argmax(y, axis=1)
if t.ndim != 1:
t = np.argmax(t, axis=1)
accuracy = np.sum(y == t) / float(x.shape[0])
return accuracy
# 数值微分计算梯度
def numerical_gradient(self, x, t):
loss_W = lambda W: self.loss(x, t)
grads = {}
grads['W1'] = numerical_gradient(loss_W, self.params['W1'])
grads['b1'] = numerical_gradient(loss_W, self.params['b1'])
grads['W2'] = numerical_gradient(loss_W, self.params['W2'])
grads['b2'] = numerical_gradient(loss_W, self.params['b2'])
return grads
# 误差反向传播计算梯度
def gradient(self, x, t):
# forward
self.loss(x, t)
# backward
dout = 1
dout = self.lastLayer.backward(dout)
layers = list(self.layers.values())
layers.reverse() # 从后往前传播
for layer in layers:
dout = layer.backward(dout)
# setting grads
grads = {}
grads['W1'] = self.layers['Affine1'].dW
grads['b1'] = self.layers['Affine1'].db
grads['W2'] = self.layers['Affine2'].dW
grads['b2'] = self.layers['Affine2'].db
return grads
将神经网络的层保存为OrderedDict
这一点非常重要。OrderedDict
是有序字典,“有序”是指它可以记住向字典里添加元素的顺序。因此反向传播只需要按照相反的顺序调用各层即可。
最后,我们来看一下使用了误差反向传播法的神经网络的学习的实现,和之前的实现相比,不同之处仅在于通过误差反向传播法求梯度这一点:
# 导入模块
...
# 读入数据
(x_train, t_train), (x_test, t_test) = load_mnist(normalize = True, one_hot_label = True)
# 创建网络
network = TwoLayerNet(input_size = 784, hidden_size = 50, output_size = 10)
# 设定超参数
...
for i in range(iters_num):
# mini-batch
...
# 计算梯度
# grad = network.numerical_gradient(x_batch, t_batch)
grad = network.gradient(x_batch, t_batch) # 误差反向传播法计算梯度
# 更新参数
...
loss = network.loss(x_batch, t_batch)
train_loss_list.append(loss)
if i % iter_per_epoch == 0:
# 记录accuracy
...
# 绘制图形
...