神经网络学习_反向传播与梯度下降(2)

前言

本节数学推导较多,不过看了文末代码再看数学推导会更为轻松。应用不需要知道太多公式怎么来的,但是在这个智能的时代,“知其所以然”也是提升自己的道路之一。

本节的数学推导留给有兴趣的读者作为参考,熟悉大一的线性代数看起来会十分轻松,笔者水平有限,有错误之处敬请指正。

前向计算

我们从一个简单的神经元模型开始:

NeutralCell.png

从输入到输出的计算过程是这样的:

Z=\sum_{i=1}^{3}W_iX_i+b

A=Activation(Z)

神经元数量一多,整体的计算就显得特别复杂,因此我们采用矩阵乘法来简化计算:

formula_3.png

左行右列,对应相乘。

对于单层两个神经元:

第一个:

formula_4.png

第二个:

formula_5.png

NeutralCell_2.png

把两个结合到一起:

formula_6.png

quote_1.png

注意观察这些矩阵的行数与列数。

进而可以推广到含有 n_L 个神经元的第 L 层的前向计算:

table_1.png

Z^{(l)}=(W^{(l)})^\top Y^{(l-1)}+B^{(l)}

Y^{(l)}=Act(Z^{(l)})

反向传播

我们来看一个简单的三层神经网络。其中,输入层只负责将输入数据传递给隐藏层。

NeutralNetwork_1.png

table_2.png

quote_2.png

简单的例子

目标

quote_7.png

方法

formula_10.png

计算

梯度的计算:

\nabla Loss(Y,\widehat Y)=\frac{\partial{Loss(Y,\widehat Y)}}{\partial{w^{(2)}_{11}}}

quote_3.png

规定:

table_3.png

则:

\frac{\partial{Loss(y,\widehat y)}}{\partial{\widehat y}}=-(y-\widehat y)

\frac{\partial{Act(x)}}{\partial{x}}=Act(x)\cdot(1-Act(x))

formula_17.png

转化为矩阵形式:

W^{(2)'}=W^{(2)}+\lambda\cdot(Y-\widehat Y)\cdot Act(Z^{(2)})[1-Act(Z^{(2)})]\cdot Y^{(1)}

quote_4.png

通用的公式更为复杂,但这里并不涉及与之相关的矩阵求导。

又一个简单的例子

quote_5.png

需要注意的是:


\widehat Y=Act(Z^{(2)})=Act((W^{(2)})^\top Y^{(1)}+B^{(2)})
Y^{(1)}=Act(Z^{(1)})=Act((W^{(1)})^\top X+B^{(1)})

也就是说,Loss 并不是直接由 W^{(1)} 的计算结果得来的,它们中间还隔了一层。

NeutralNetwork_2.png

计算如下:
\nabla Loss(Y,\widehat Y)=\frac{\partial{Loss(Y,\widehat Y)}}{\partial{W^{(1)}}}=\frac{\partial{Loss(Y,\widehat Y)}}{\partial{}Z^{(1)}}\cdot\frac{\partial{Z^{(1)}}}{\partial{W^{(1)}}}

\frac{\partial{Loss(Y,\widehat Y)}}{\partial{Z^{(1)}}}=\frac{\partial{Loss(Y,\widehat Y)}}{\partial{Z^{(2)}}}\cdot\frac{\partial{Z^{(2)}}}{\partial{Y^{(1)}}}\cdot\frac{\partial{Y^{(1)}}}{\partial{Z^{(1)}}}

\nabla Loss(Y,\widehat Y)=\frac{\partial{Loss(Y,\widehat Y)}}{\partial{W^{(1)}}}
=\frac{\partial{Loss(Y,\widehat Y)}}{\partial{Z^{(2)}}}\cdot\frac{\partial{Z^{(2)}}}{\partial{Y^{(1)}}}\cdot\frac{\partial{Y^{(1)}}}{\partial{Z^{(1)}}}\cdot\frac{\partial{Z^{(1)}}}{\partial{W^{(1)}}}$
=\frac{\partial{Loss(Y,\widehat Y)}}{\partial{Z^{(2)}}}\cdot (W^{(2)})^\top\cdot\frac{\partial{Y^{(1)}}}{\partial{Z^{(1)}}}\cdot\frac{\partial{Z^{(1)}}}{\partial{W^{(1)}}}

注意到:

quote_6.png

针对我们选取的激活函数和损失函数,具体形式推导如下:
W^{(1)'}
=W^{(1)}-\lambda\nabla Loss(Y,\widehat Y)
=W^{(1)}+\lambda\cdot\frac{\partial{Loss(Y,\widehat Y)}}{\partial{Z^{(2)}}}\cdot (W^{(2)})^\top\cdot\frac{\partial{Y^{(1)}}}{\partial{Z^{(1)}}}\cdot\frac{\partial{Z^{(1)}}}{\partial{W^{(1)}}}

带入具体函数的偏导数,得到:

W^{(1)'}=W^{(1)}+\lambda\cdot(Y-\widehat Y)\cdot(W^{(2)})^\top\cdot Act(Z^{(1)})[1-Act(Z^{(1)})]\cdot X

这就是我们的三层神经网络中,输入层与隐藏层之间的权重矩阵的调整的公式。

上面的推导并没有很好地显示出反向传播的含义。更为专业地推导可以参见CMU等的公开课。

小小的总结

回顾一下我们对于激活函数和损失函数的设定:

table_3.png

通过上面两个例子,我们得到了三层神经网络中,在梯度下降算法下,两个权重矩阵如何进行调整:

W^{(2)'}=W^{(2)}+\lambda\cdot(Y-\widehat Y)\cdot Act(Z^{(2)})[1-Act(Z^{(2)})]\cdot Y^{(1)}

W^{(1)'}=W^{(1)}+\lambda\cdot\Big[(Y-\widehat Y)\cdot(W^{(2)})^\top\Big]\cdot Act(Z^{(1)})[1-Act(Z^{(1)})]\cdot X

接下来便是代码实现了。

代码实现

上一节中,我们定义了神经网络类,但是还没有写它的方法。下面就添加进训练及查询的方法。需要注意的是,数学公式很简洁(虽然笔者的很丑陋。。。),实现起来很复杂。因为你不但需要考虑 Python 中的数据类型,还要考虑各种可能对神经网络的效果有很大影响的细节。

# 定义神经网络类
class neuralNetwork:

# 初始化神经网络
def __init__(self, InputNodes, HiddenNodes, OutputNodes, LearningRate):
    # 设置输入层、隐藏层、输出层感知机数量
    self.in_nodes = InputNodes
    self.hid_nodes = HiddenNodes
    self.out_nodes = OutputNodes
    # 学习率
    self.LR = LearningRate
    # 权重矩阵W_ih, W_ho表示Input到Hidden,Hidden到Output的权重矩阵
    # 形式如下:
    # w_11 w_12
    # w_21 w_22
    # w_ij表示上一层第i结点到下一层第j结点的权重值
    self.W_ih = np.random.normal(0.0, pow(self.hid_nodes, -0.5), (self.in_nodes, self.hid_nodes))
    self.W_ho = np.random.normal(0.0, pow(self.out_nodes, -0.5), (self.hid_nodes, self.out_nodes))

    # 激活函数为sigmoid函数
    self.activate_func = lambda x:scipy.special.expit(x)
    self.inv_activate_func = lambda x: scipy.special.logit(x)


# 训练神经网络,输入+目标输出
def train(self, inputs_list, targets_list):
    # 将列表输入转换为矩阵
    inputs = np.array(inputs_list, ndmin = 2).T
    targets = np.array(targets_list, ndmin = 2).T
    # 计算隐藏层输入: W_ih·inputs
    hidden_inputs = np.dot(self.W_ih.T, inputs)
    # 计算隐藏层输出
    hidden_outputs = self.activate_func(hidden_inputs)

    # 计算输出层输入: W_ho·hidden_outputs
    final_inputs = np.dot(self.W_ho.T, hidden_outputs)
    # 计算输出层输出
    final_outputs = self.activate_func(final_inputs)

    # 输出层结点误差errors,误差函数为误差平方和sum(errors^2)
    output_errors = targets - final_outputs
    # 隐藏层节点误差,通过权重分配
    hidden_errors = np.dot(self.W_ho, output_errors)

    # 更新隐藏层与输出层之间的权重
    self.W_ho += (self.LR * np.dot((output_errors * final_outputs * (1.0 - final_outputs)), hidden_outputs.T)).T

    # print((hidden_errors * hidden_outputs * (1.0 - hidden_outputs)).shape)
    # 更新输出层与隐藏层之间的权重
    self.W_ih += (self.LR * np.dot((hidden_errors * hidden_outputs * (1.0 - hidden_outputs)), inputs.T)).T
    

# 查询函数,训练完后进行验证
def query(self, inputs_list):
    # 将列表输入转为矩阵
    inputs = np.array(inputs_list, ndmin = 2).T

    # 计算隐藏层输入
    hidden_inputs = np.dot(self.W_ih.T, inputs)
    # 计算隐藏层输出
    hidden_outputs = self.activate_func(hidden_inputs)

    # 计算输出层输入
    final_inputs = np.dot(self.W_ho.T, hidden_outputs)
    # 计算输出层输出
    final_outputs = self.activate_func(final_inputs)

    return final_outputs

后记

不得不说,自己推导真累。。。

前后纠结了两天,补了补矩阵运算😱(我已经忘得差不多了)

下一节将是基于 MNIST 数据集进行训练,并测试其性能的内容。

(我终于放弃了用 wordpress 文章编辑器编辑 数学公式了இ௰இ,生活太南了)

     

发表评论

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据

购物车
There are no products in the cart!
Subtotal
¥0.00
Total
¥0.00
Continue Shopping
0