前面介绍了神经网络,并通过数值微分计算了神经网络的权重参数以及偏置量(bias)。
虽然数值微分实现起来比较容易,但是在计算上花费的时间却比较多。
下面重点介绍一个高效计算权重以及偏置量的梯度方法——误差反向传播法。
要点具体如下。
·激活函数层的实现。
·Affine层的实现。
·Softmax层的实现。
·整体实现。
·正则化惩罚。
激活函数层的实现 通过计算图来理解误差反向传播法这个思想是参考了CS231n(斯坦福大学的深度学习课程),计算图被定义为有向图,其中,节点对应于数学运算,计算图是表达和评估数学表达式的一种方式。
例如, 我们可以绘制上述数学等式的计算图具有一个加法节点,这个节点有两个输入变量x和y,以及一个输出q。
下面我们再来列举一个示例,稍微复杂一些,等式的计算图,(x+y)*z的计算图ReLU反向传播实现 现在,我们利用计算图的思路来实现ReLU激活函数的反向传播,首先我们回顾一下激活函数ReLU的前向传播:
如果前向传播时的输入x大于0,则将这个x原封不动地传给下一层;如果输入的x小于0,则将0传给下一层。具体表达方程式如下:
通过上述方程式,我们可以求出y关于x的导数,其中,dout为上一层传过来的导数:
ReLU前向传播利用Python实现的代码如下:
class Relu:
def __init__(self):
self.x = None
def forward(self,x):
self.x = np.maximum(0,x)
out = self.x
return out
def backward(self,dout):
dx = dout
dx[self.x <=0] = 0
return dx
Sigmoid反向传播实现 接下来,我们来实现Sigmoid函数的反向传播,Sigmoid函数公式如下所示:
如果使用计算图来表示的话,Sigmoid计算图 现在,从右向左依次解说如下。 对于第一个步骤y=1/1+exp(-x),可以设置为x=1+exp(-x),那么,又因为,所以最后。
对于第二个步骤1+exp(-x),进行反向传播时,会将上游的值-y2乘以本阶段的导数,对于1+exp(-x)求导得到的导数为-exp(-x),因为e-x的导数为-e-x。
所以第二步的导数为-y2*(-e-x)=y2*(e-x)。 第三个步骤的加法运算不会改变导数值,接着-x的导数为-1,所以对于这个阶段需要将y2*(e-x)*-1,最后乘法运算还需要乘以-1。
所以最终求得的导数为y2*exp(-x),进行一下整理得到的输出为y(1-y),最后乘以上一层的求导结果,就会作为本阶段Sigmoid函数的求导结果了,最后将这个结果传给下一层(一般来说应该是Affine层)。
对于Python实现来说,具体实现代码如下。
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 *self.out*(1-self.out)
return dx
Affine层的实现 Affine的英文翻译是神经网络中的一个全连接层。
仿射(Affine)的意思是前面一层中的每一个神经元都连接到当前层中的每一个神经元。在许多方面,这是神经网络的“标准”层
仿射层通常被加在卷积神经网络或循环神经网络中作为最终预测前的输出的顶层。
仿射层的一般形式为y=f(W*x+b),其中,x是层输入,w是参数,b是一个偏置量,f是一个非线性激活函数。
对X(矩阵)的求导,可以参看如下公式(此处省略推导过程)需要注意的是,X和形状相同,W和的形状相同): 之前列举的示例是对于一个X,如果是多个X,那么其形状将从原来的(2,)变为(N,2)。
如果加上偏置量的话,偏置量会被加到各个X·W中去,比如N=3(数据为3个的时候),偏置量会被分别加到这3个数据中去,因此偏置量,f是一个非线性激活函数。
对X(矩阵)的求导,可以参看如下公式(此处省略推导过程)需要注意的是,X和形状相同,W和的形状相同): 之前列举的示例是对于一个X,如果是多个X,那么其形状将从原来的(2,)变为(N,2)。
如果加上偏置量的话,偏置量会被加到各个X·W中去,比如N=3(数据为3个的时候),偏置量会被分别加到这3个数据中去,因此偏置量的反向传播会对这三个数据的导数按照第0轴的方向上的元素进行求和。
其中每一个偏置量的求导公式都可以表示为: 对于上述所讲的内容,其Python实现代码如下:
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
Softmaxwithloss层的实现 假设网络最后一层的输出为z,经过Softmax后输出为p,真实标签为y(one-hot编码),其中,C表示共有C个类别,那么损失函数为:
因为p是z经过Softmax函数计算后的输出,即p=softmax(z)。其中, 求导过程分为i=j和i!=j两种情况,分别如下:
当i=j的时候,得到的求导解为pj(1-pj)。 当i!=j的时候,得到的求导解为-pipj。 最终整理一下可以得到,Loss对z的求导为: 其Python的实现代码具体如下:
class SoftmaxWithLoss:
def __init__(self):
self.loss = None #损失
self.p = None # Softmax的输出
self.y = None #监督数据代表真值,one-hot vector
def forward(self,x,y):
self.y = y
self.p = softmax(x)
self.loss = cross_entropy_error(self.p,self.y)
return self.loss
def backward(self,dout=1):
batch_size = self.y.shape[0]
dx = (self.p - self.y) / batch_size
return dx
上述代码实现是利用了之前实现的Softmax和cross_entropy_error函数,值得注意的是,进行反向传播的时候,应将需要传播的值除以批的大小(batch_size),并将单个数据的误差传递给前面的层。
基于数值微分和误差反向传播的比较 到目前为止,我们介绍了两种求梯度的方法:
一种是基于数值微分的方法,另一种是基于误差反向传播的方法,对于数值微分来说,它的计算非常耗费时间,如果读者对于误差反向传播掌握得非常好的话,那么根本就没有必要使用到数值微分。现在的问题是,我们为什么要介绍数值微分呢?
原因很简单,数值微分的优点就在于其实现起来非常简单,一般情况下,数值微分实现起来不太容易出错,而误差反向传播法的实现就非常复杂,且很容易出错,所以经常会比较数值微分和误差反向传播的结果(两者的结果应该是非常接近的),以确认我们书写的反向传播逻辑是正确的。这样的操作就称为梯度确认(gradientcheck)。
数值微分和误差反向传播这两者的比较误差应该是非常小的,实现代码具体如下:
from collections import OrderedDict
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)
#生成层
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.layers['Relu2'] = Relu()
self.lastLayer = SoftmaxWithLoss()
def predict(self, x):
for layer in self.layers.values():
x = layer.forward(x)
return x
# x:输入数据, y:监督数据
def loss(self, x, y):
p = self.predict(x)
return self.lastLayer.forward(p, y)
def accuracy(self, x, y):
p = self.predict(x)
p = np.argmax(y, axis=1)
if y.ndim != 1 : y = np.argmax(y, axis=1)
accuracy = np.sum(p == y) / float(x.shape[0])
return accuracy
# x:输入数据, y:监督数据
def numerical_gradient(self, x, y):
loss_W = lambda W: self.loss(x, y)
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, y):
# forward
self.loss(x, y)
# backward
dout = 1
dout = self.lastLayer.backward(dout)
layers = list(self.layers.values())
layers.reverse()
for layer in layers:
dout = layer.backward(dout)
#设定
grads = {}
grads['W1'], grads['b1'] = self.layers['Affine1'].dW, self.layers['Affine1'].db
grads['W2'], grads['b2'] = self.layers['Affine2'].dW, self.layers['Affine2'].db
return grads
network = TwoLayerNet(input_size=784,hidden_size=50,output_size=10)
x_batch = x_train[:100]
y_batch = y_train[:100]
grad_numerical = network.numerical_gradient(x_batch,y_batch)
grad_backprop = network.gradient(x_batch,y_batch)
for key in grad_numerical.keys():
diff = np.average( np.abs(grad_backprop[key] - grad_numerical[key]) )
print(key + ":" + str(diff)) 从以下输出结果中,我们可以观察到,它们两者的差值并不是很大。
W1:5.9329106471124405e-05
b1:1.844024470884823e-09
W2:0.0007755803070111151
b2:9.234723605880401e-08
这里需要补充一点的是,我们在代码中使用了OrderedDict这个类,OrderedDict是有序字典,“有序”是指它可以“记住”我们向这个类里添加元素的顺序,因此神经网络的前向传播只需要按照添加元素的顺序调用各层的Forward方法即可完成处理,而相对的误差反向传播则只需要按照前向传播相反的顺序调用各层的backward方法即可。
网友评论