ANN 人工神经网络数学推导及 Python 实现

* 以下内容,未经允许,禁止复制转载。但可以在注明出处前提下引用。

人工神经网络是深度学习的基石,也是机器学习中一种十分重要的算法。与此同时,反向传播算法又是人工神经网络的核心。本文尝试通过数学矩阵完成神经网络的推导,并使用 Python 实现一个简单神经网络的完整结构。

定义神经网络结构

为了让推导过程足够清晰,这里我们只构建包含 1 个隐含层的人工神经网络结构。其中,输入层为 2 个神经元,隐含层为 3 个神经元,并通过输出层实现 2 分类问题的求解。该神经网络的结构如下:

anns_new

本次实验中,我们使用的激活函数为 $sigmoid$ 函数:

equation-1

由于下面要使用 $sigmoid$ 函数的导数,所以同样将其导数公式写出来:

equation--1--1

首先,我们通过 Python 实现公式 $(1)$:

# sigmoid 函数
def sigmoid(x):
    return 1 / (1 + np.exp(-x))

# sigmoid 函数求导
def sigmoid_derivative(x):
    return sigmoid(x) * (1 - sigmoid(x))

前向传播

前向(正向)传播中,每一个神经元的计算流程为:线性变换 → 激活函数 → 输出值

在此,我们约定:

  • $Z$ 表示隐含层输出,$Y$ 则为输出层最终输出。
  • $w_{ij}$ 表示从第 $i$ 层的第 $j$ 个权重。

于是,上图中的前向传播的代数计算过程如下。

神经网络的输入 $X$,第一层权重 $W_1$,第二层权重 $W_2$。为了演示方便,$X$ 为单样本,因为是矩阵运算,我们很容易就能扩充为多样本输入。

equation--2--2

接下来,计算隐含层神经元输出 $Z$(线性变换 → 激活函数)。同样,为了使计算过程足够清晰,我们这里将截距项置为 0。

equation--3--1

最后,计算输出层 $Y$(线性变换 → 激活函数):

equation--4--2

至此,已经完成了前向传播的推导。

反向传播

接下来,我们使用梯度下降法的方式来优化神经网络的参数。那么首先需要定义损失函数,然后计算损失函数关于神经网络中各层的权重的偏导数(梯度)。

此时,设神经网络的输出值为 $Y$,真实值为 $y$。然后,定义平方损失函数如下:

equation--5--1

接下来,求解梯度 $\frac{\partial Loss(y, Y)}{\partial{W_2}}$,需要使用链式求导法则:

equation--6--1

equation--7--1

同理,梯度 $\frac{\partial Loss(y, Y)}{\partial{W_1}}$ 得:

equation--8--1

equation--9--1

其中,$\frac{\partial Y}{\partial{W_2}}$,$\frac{\partial Y}{\partial{W_1}}$ 分别通过公式 $(5)$ 和 $(6)$ 求得。

然后,就可以设置学习率 $lr$,并对 $W_1$, $W_2$ 进行一次更新了。

equation--10--1

equation--11--1

以上,我们就实现了单个样本在神经网络中的 1 次前向 → 反向传递,并使用梯度下降完成 1 次权重更新。那么,下面我们完整实现该网络,并对多样本数据集进行学习。

# 示例神经网络完整实现
class NeuralNetwork:
    
    # 初始化参数
    def __init__(self, X, y, lr):
        self.input_layer = X
        self.W1 = np.random.rand(self.input_layer.shape[1], 3) # 注意形状
        self.W2 = np.random.rand(3, 1)
        self.y = y
        self.lr = lr
        self.output_layer = np.zeros(self.y.shape)
    
    # 前向传播
    def forward(self):
    
        # 实现公式 2,3
        self.hidden_layer = sigmoid(np.dot(self.input_layer, self.W1))
        self.output_layer = sigmoid(np.dot(self.hidden_layer, self.W2))
    
    # 反向传播
    def backward(self):
    
        # 实现公式 5
        d_W2 = np.dot(self.hidden_layer.T, (2 * (self.output_layer - self.y) *
                      sigmoid_derivative(np.dot(self.hidden_layer, self.W2))))
        
        # 实现公式 6
        d_W1 = np.dot(self.input_layer.T, (
               np.dot(2 * (self.output_layer - self.y) * sigmoid_derivative(
               np.dot(self.hidden_layer, self.W2)), self.W2.T) * sigmoid_derivative(
               np.dot(self.input_layer, self.W1))))
        
        # 参数更新,实现公式 7
        # 因上方是 output_layer - y,此处为 -= 以保证符号一致
        self.W1 -= self.lr * d_W1
        self.W2 -= self.lr * d_W2

测试网络

接下来,我们使用一组测试数据,并迭代 1000 次:

import numpy as np
from matplotlib import pyplot as plt

# 测试数据
X = np.array([
    [1, 0],
    [0, 1],
    [1, 0],
    [1, 1],
])

y = np.array([[0], [1], [0], [1]])

nn = NeuralNetwork(X, y, lr=0.1) # 定义模型
loss_list = [] # 存放损失数值变化

for i in range(1000):
    nn.forward() # 前向传播
    nn.backward() # 反向传播
    loss = np.sum((y - nn.output_layer) ** 2) # 计算平方损失
    loss_list.append(loss)

print("final loss:", loss)
plt.plot(loss_list) # 绘制 loss 曲线变化图

测试结果如下:

ann_loss

可以看到,损失逐渐减小并接近收敛,本实验重点再于搞清楚 BP 的中间过程,因网络简单,无法直接照搬使用。需要注意的是由于权重是随机初始化,多次运行的结果会不同。