在进行深度学习入门的过程中,我阅读的书籍作者为了演示最基础的神经网络的搭建,展示了斯坦福大学计算机系的作业源码。但是这段代码在初看时其实会有不少不容易理解的地方。所以,为了理清自己的思路以及帮到和我同样对此有些凌乱的人,现在我将尽可能全面深入的解释一下TwoLayerNet代码的实现。
部分源码:
import sys, os
sys.path.append(os.pardir)
from common.functions import *
from common.gradient import numerical_gradient
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
# x:输入数据, t:监督数据
def loss(self, x, t):
y = self.predict(x)
return cross_entropy_error(y, t)
# x:输入数据, t:监督数据
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
首先,初始化权重矩阵的方式自然不必多说,设置好输入层、隐藏层和输出层的神经元大小,并利用高斯分布随机数生成初始的权重矩阵。不过因为这个可学习的权重矩阵就是我们要不断训练并优化的对象或者说策略,所以它的初始化不必在意太多。
接着,我们需要先一步理清楚 predict 和 loss 这两个函数。
predict函数很简单,就是获取实例变量里存储的权重矩阵W1、W2和偏置矩阵b1、b2,将他们和输入数据进行“点积 + 偏置”运算,并用sigmoid和softmax函数进行激活,从而完成从输入层到隐藏层、隐藏层到输出层的学习过程。
值得注意的是(顺带着复习一下),在这里,利用矩阵乘法通过“并行计算”(一次计算多个输入数据,对应行数)加速神经网络的训练,是神经网络层与层之间运算的基础。偏置(Bias)可以被描述为神经元激活的容易程度,实际上它是用来调整神经元的输出,使其不完全依赖于输入值,帮助模型更好地拟合数据,特别是在输入为零时,偏置能确保神经元的激活值不总是零。激活函数sigmoid(x) = 1 / 1 + e^-x,softmax(x) = e^xi / sum(e^xi)。
loss函数内部调用了predict函数并将输入数据x传入进行推理,并将得出的结果y和监督数据(可以当成标签)t传入交叉熵误差函数cross_entropy_error中得到误差值。这里格外注意一下,loss函数的参数是输入数据x和监督数据t,返回值是交叉熵误差,这个在后面要分清。
注:交叉熵误差公式:E = -sum(tk * lnyk)。这个式子实际上只计算对应正确解标签的输出的自然对数。代码如下:
现在我们来研究核心之一的求梯度函数numerical_gradient(self, x, t)。这个函数的第一个坑就是最外层定义的numerical_gradient(self, x, t)和该函数内部调用的numerical_gradient(loss_W, self.params[‘W1’])不是同一个函数,实际上是发生了函数重载。numerical_gradient(loss_W, self.params[‘W1’])函数的定义源码如下:def numerical_gradient(f, x):
h = 1e-4
grad = np.zeros_like(x)
for idx in range(x.size):
tmp_val = x[idx]
x[idx] = tmp_val + h
fxh1 = f(x)
x[idx] = tmp_val - h
fxh2 = f(x)
grad[idx] = (fxh1 - fxh2) / (2*h)
x[idx] = tmp_val
return grad
同时,一个必须先理解的地方是:为什么在调用梯度函数numerical_gradient时传入的参数是权重矩阵W?在这里传入权重矩阵W有什么用?
这是因为numerical_gradient这个求梯度的方法会回调你传入的这个loss_W函数,也就是调用loss_W(W)函数,而loss_W函数内部执行的实际是loss(x,t)函数,loss(x,t)函数是没有也不需要直接用到W这个权重矩阵参数的。所以,W在这里是一个伪参数,为了与之兼容而定义了f(W),也就是loss_W = lambda W: self.loss(x, t)这个函数。
现在回到numerical_gradient(f, x)这个函数,传入loss_W作为f,W作为x,利用数值微分一行一行地去求权重矩阵或偏置矩阵关于loss函数的梯度grad。
以上就是TwoLayerNet的部分源码解释,难点可能主要集中在梯度法那一块,不过理清楚以后,再回看就不那么难了。
花了不少时间,不过还算值得 :)。