神经网络如何学习?从梯度下降到反向传播的完整解析(附 MNIST 实战)

本文参考深度学习经典入门教材 Neural Networks and Deep Learning 中第1章 使用神经网络识别手写数字 代码文件 run_p32.py 利用基础神经网络训练 MNIST,完整代码可查看文章末尾链接。

神经网络学习算法详解

前言:训练效果示意图

training_curves

经过 30 轮迭代,测试准确率的变化从 53.8% 提升至 92.2%。

一、项目代码运行说明

在项目目录下运行命令

cd mnielsen_nnadl-master\mnielsen_nnadl-master\src\run # Windows 路径
python run_p32.py

代码结构

def main():
    print("Run code on Page 32 ...")
    # 1. 加载 MNIST 数据集
    training_data, validation_data, test_data = mnist_loader.load_data_wrapper()
    # 2. 创建神经网络 [784, 100, 20, 10]
    net = network.Network([784, 100, 20, 10])
    # 3. 使用随机梯度下降训练
    net.SGD(training_data, 30, 10, 0.1, test_data=test_data)

网络架构

输入层:  784 神经元 (28×28 像素)
    ↓
隐藏层1: 100 神经元
    ↓
隐藏层2:  20 神经元
    ↓
输出层:  10 神经元 (0-9 数字)

训练流程示意图

network_architecture

注:本图为简化示意图,实际每一层的每个节点都与下一层所有节点相连(全连接)。


二、梯度下降算法 (Gradient Descent)

1. 核心思想

梯度下降是一种优化算法,用于最小化代价函数。基本思想是:
沿着梯度的反方向更新参数,逐步逼近最优解。

2. 数学原理

代价函数(二次代价函数):

\[C(w,b) = \frac{1}{2n} \sum_{x} \|y(x) - a^L(x)\|^2 \]

其中:

  • \(w\):权重矩阵
  • \(b\):偏置向量
  • \(n\):训练样本数
  • \(y(x)\):真实标签
  • \(a^L(x)\):网络输出

参数更新规则

\[w_k \leftarrow w_k - \eta \frac{\partial C}{\partial w_k} \]

\[b_l \leftarrow b_l - \eta \frac{\partial C}{\partial b_l} \]

其中:

  • \(\eta\):学习率(learning rate),控制每次更新的步长

  • \(\dfrac{\partial C}{\partial w_k}\):代价函数对权重的偏导数(梯度)

3. 随机梯度下降 (SGD)

标准梯度下降的问题:

  • 需要遍历所有训练样本才能更新一次参数
  • 训练速度慢,特别是大数据集

SGD 的改进

  • 使用小批量数据(mini-batch)而非全部数据
  • 每个小批量更新一次参数
  • 加快训练速度,有助于跳出局部最优

SGD 算法流程


def SGD(self, training_data, epochs, mini_batch_size, eta, test_data=None):

    """
    参数说明:
    - training_data: 训练数据集
    - epochs: 迭代次数(完整遍历训练集的次数)
    - mini_batch_size: 小批量大小
    - eta: 学习率
    - test_data: 测试数据(可选,用于评估)
    """

    n = len(training_data)

    for j in range(epochs):           # 迭代 epochs 次

        random.shuffle(training_data) # 打乱训练数据

        # 分成多个小批量
        mini_batches = [
            training_data[k:k+mini_batch_size]
            for k in range(0, n, mini_batch_size)
        ]

        # 对每个小批量更新权重
        for mini_batch in mini_batches:
            self.update_mini_batch(mini_batch, eta)

        # 每轮结束后在测试集上评估
        if test_data:
            print(f"Epoch {j}: {self.evaluate(test_data)} / {len(test_data)}")

示意图:
image

注:小批量梯度下降在损失曲面上的路径(含中心区域放大)。红色路径展示了参数更新时的震荡特性,右下角放大图显示了收敛到最低点附近的细节。

绘图代码如下:

点击查看代码
import numpy as np  
import matplotlib.pyplot as plt  
from matplotlib import rcParams  
from mpl_toolkits.axes_grid1.inset_locator import inset_axes, mark_inset  
  
rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei']  
rcParams['axes.unicode_minus'] = False  
  
def loss_function(w1, w2):  
    return w1**2 + w2**2 + 0.3 * np.sin(3*w1) * np.sin(3*w2)  
  
def gradient(w1, w2):  
    grad_w1 = 2*w1 + 0.9 * np.cos(3*w1) * np.sin(3*w2)  
    grad_w2 = 2*w2 + 0.9 * np.sin(3*w1) * np.cos(3*w2)  
    return grad_w1, grad_w2  
  
def mini_batch_gradient(w1, w2, noise_std=0.25):  
    true_grad = gradient(w1, w2)  
    return (true_grad[0] + noise_std * np.random.randn(),  
            true_grad[1] + noise_std * np.random.randn())  
  
learning_rate = 0.1  
steps = 60  
start_point = (1.2, 1.2)  
  
trajectory = [start_point]  
w1, w2 = start_point  
for _ in range(steps):  
    g1, g2 = mini_batch_gradient(w1, w2, noise_std=0.25)  
    w1 -= learning_rate * g1  
    w2 -= learning_rate * g2  
    trajectory.append((w1, w2))  
trajectory = np.array(trajectory)  
  
fig, ax = plt.subplots(figsize=(10, 8))  
  
w1_vals = np.linspace(-1.5, 1.8, 200)  
w2_vals = np.linspace(-1.5, 1.8, 200)  
W1, W2 = np.meshgrid(w1_vals, w2_vals)  
Z = loss_function(W1, W2)  
contour = ax.contour(W1, W2, Z, levels=30, cmap='viridis', alpha=0.7)  
ax.clabel(contour, inline=True, fontsize=8)  
  
ax.plot(trajectory[:,0], trajectory[:,1], 'r-o', markersize=3, linewidth=1.5,  
        label='小批量梯度下降路径', alpha=0.7)  
ax.scatter(trajectory[0,0], trajectory[0,1], color='green', s=80, label='起点', zorder=5)  
ax.scatter(trajectory[-1,0], trajectory[-1,1], color='red', s=80, label='终点', zorder=5)  
  
for i in range(0, len(trajectory)-1, 8):  
    dx = trajectory[i+1,0] - trajectory[i,0]  
    dy = trajectory[i+1,1] - trajectory[i,1]  
    ax.arrow(trajectory[i,0], trajectory[i,1], dx, dy,  
             head_width=0.05, head_length=0.08, fc='red', ec='red', alpha=0.5)  
  
ax.set_xlim(-1.5, 1.8)  
ax.set_ylim(-1.5, 1.8)  
ax.set_xlabel('参数 w1', fontsize=12)  
ax.set_ylabel('参数 w2', fontsize=12)  
ax.set_title('小批量梯度下降路径示意图(含中心区域放大)', fontsize=14, fontweight='bold')  
ax.legend()  
ax.grid(True, alpha=0.3)  
  
# 放大区域  
x1, x2 = -0.5, 0.5  
y1, y2 = -0.5, 0.5  
  
axins = inset_axes(ax, width="35%", height="35%", loc='lower right',  
                   bbox_to_anchor=(0.05, 0.05, 0.9, 0.9), bbox_transform=ax.transAxes)  
  
w1_sub = np.linspace(x1, x2, 100)  
w2_sub = np.linspace(y1, y2, 100)  
W1_sub, W2_sub = np.meshgrid(w1_sub, w2_sub)  
Z_sub = loss_function(W1_sub, W2_sub)  
axins.contour(W1_sub, W2_sub, Z_sub, levels=20, cmap='viridis', alpha=0.8)  
  
mask = (trajectory[:,0] >= x1) & (trajectory[:,0] <= x2) & (trajectory[:,1] >= y1) & (trajectory[:,1] <= y2)  
traj_in_zoom = trajectory[mask]  
if len(traj_in_zoom) > 0:  
    axins.plot(traj_in_zoom[:,0], traj_in_zoom[:,1], 'r-o', markersize=4, linewidth=2, alpha=1)  
    if mask[0]:  
        axins.scatter(trajectory[0,0], trajectory[0,1], color='green', s=60)  
    if mask[-1]:  
        axins.scatter(trajectory[-1,0], trajectory[-1,1], color='red', s=60)  
  
axins.set_xlim(x1, x2)  
axins.set_ylim(y1, y2)  
axins.set_xticks([-0.5, 0, 0.5])  
axins.set_yticks([-0.5, 0, 0.5])  
axins.grid(True, alpha=0.3)  
axins.set_title('中心区域放大', fontsize=10)  
  
rect = plt.Rectangle((x1, y1), x2-x1, y2-y1, fill=False, edgecolor='blue', linewidth=1.5, linestyle='--')  
ax.add_patch(rect)  
mark_inset(ax, axins, loc1=1, loc2=3, fc="none", ec="blue", linewidth=1)  
  
plt.tight_layout()  
plt.savefig('mini_batch_gd_path_zoom.png', dpi=150, bbox_inches='tight')  
plt.show()

三、反向传播算法 (Backpropagation)

1. 为什么需要反向传播?

如何高效计算代价函数对所有权重和偏置的梯度?
反向传播通过 一次前向 + 一次反向 传播同时算出所有梯度,相比逐参数求导效率提升巨大。
对比如下:

naive 方法

  • 对每个权重单独计算偏导数
  • 计算复杂度:\(O(n^2)\),太慢!

反向传播的优势

  • 利用链式法则高效计算梯度
  • 计算复杂度:\(O(n)\)
  • 深度学习的核心算法

2. 反向传播的四个基本方程

符号说明

  • \(z^l = w^l a^{l-1} + b^l\):第 \(l\) 层的加权输入

  • \(a^l = \sigma(z^l)\):第 \(l\) 层的激活输出

  • \(\delta^l\):第 \(l\) 层的误差(误差是代价函数对加权输入 \(z^l\) 的偏导数)

方程 1:输出层的误差

\[\delta^L = \nabla_a C \odot \sigma'(z^L) \]

代码实现

# 计算输出层误差
delta = self.cost_derivative(activations[-1], y) * sigmoid_prime(zs[-1])

其中:

  • \(\nabla_a C = (a^L - y)\):代价函数对输出的偏导

  • \(\sigma'(z^L)\):激活函数的导数

  • \(\odot\):Hadamard 积(逐元素相乘)

方程 2:误差在第 l 层的传播

\[\delta^l = ((w^{l+1})^T \delta^{l+1}) \odot \sigma'(z^l) \]

代码实现

# 从后向前逐层传播误差
for l in range(2, self.num_layers):
    z = zs[-l]
    sp = sigmoid_prime(z)
    delta = np.dot(self.weights[-l+1].transpose(), delta) * sp
    nabla_b[-l] = delta
    nabla_w[-l] = np.dot(delta, activations[-l-1].transpose())

这个方程说明:

  • \(l\) 层的误差可以通过第 \(l+1\) 层的误差计算
  • 使用转置权重矩阵反向传播误差

方程 3:代价函数对偏置的梯度

\[\frac{\partial C}{\partial b_j^l} = \delta_j^l \]

代码实现

nabla_b[-l] = delta 

注:代码中 \(-l\) 表示 Python 负索引,即从后往前数第 \(l\)

这个方程说明:

  • 代价函数对偏置的梯度等于该层的误差

方程 4:代价函数对权重的梯度

\[\frac{\partial C}{\partial w_{jk}^l} = a_k^{l-1} \delta_j^l \]

代码实现

nabla_w[-l] = np.dot(delta, activations[-l-1].transpose())

注:代码中 \(-l\) 表示 Python 负索引,即从后往前数第 \(l\)

这个方程说明:

  • 梯度 = 前一层的激活值 × 当前层的误差

3. 反向传播完整算法

def backprop(self, x, y):

    """
    返回 (nabla_b, nabla_w),表示代价函数的梯度
    """

    nabla_b = [np.zeros(b.shape) for b in self.biases]
    nabla_w = [np.zeros(w.shape) for w in self.weights]

    # ===== 1. 前向传播 =====
    
    activation = x
    activations = [x]  # 存储每层的激活值
    zs = []            # 存储每层的加权输入
    
    for b, w in zip(self.biases, self.weights):
        z = np.dot(w, activation) + b  # z = w·a + b
        zs.append(z)
        activation = sigmoid(z)        # a = σ(z)
        activations.append(activation)

    # ===== 2. 反向传播 =====

    # 2.1 计算输出层误差(方程 1)
    delta = self.cost_derivative(activations[-1], y) * sigmoid_prime(zs[-1])
    nabla_b[-1] = delta                          # 方程 3
    nabla_w[-1] = np.dot(delta, activations[-2].transpose())  # 方程 4

    # 2.2 从后向前逐层传播误差(方程 2)

    for l in range(2, self.num_layers):
        z = zs[-l]
        sp = sigmoid_prime(z)
        delta = np.dot(self.weights[-l+1].transpose(), delta) * sp
        nabla_b[-l] = delta
        nabla_w[-l] = np.dot(delta, activations[-l-1].transpose())

    return (nabla_b, nabla_w)


四、权重更新

计算完梯度后,更新权重和偏置:


def update_mini_batch(self, mini_batch, eta):

    # 初始化梯度累加器

    nabla_b = [np.zeros(b.shape) for b in self.biases]
    nabla_w = [np.zeros(w.shape) for w in self.weights]

    # 对每个样本计算梯度并累加

    for x, y in mini_batch:
        delta_nabla_b, delta_nabla_w = self.backprop(x, y)
        nabla_b = [nb+dnb for nb, dnb in zip(nabla_b, delta_nabla_b)]
        nabla_w = [nw+dnw for nw, dnw in zip(nabla_w, delta_nabla_w)]

    # 更新权重和偏置

    self.weights = [w - (eta/len(mini_batch)) * nw
                    for w, nw in zip(self.weights, nabla_w)]
    self.biases = [b - (eta/len(mini_batch)) * nb
                   for b, nb in zip(self.biases, nabla_b)]

更新公式

\[W = W - \frac{\eta}{m} \sum_{i=1}^{m} \nabla W_i \]

其中 \(m\) 是小批量大小(本例中为 10)。


五、完整训练流程

开始

  ↓

1. 加载 MNIST 数据集
   - 训练集:50,000 张图片
   - 测试集:10,000 张图片

  ↓

2. 初始化神经网络 [784, 100, 20, 10]
   - 随机初始化权重和偏置

  ↓

3. for epoch in 1..30:
   │
   ├─ 3.1 打乱训练数据
   │
   ├─ 3.2 分成小批量(每批 10 个样本)
   │
   ├─ 3.3 for 每个小批量:
   │     │
   │     ├─ 前向传播
   │     │   z = w·a + b
   │     │   a = σ(z)
   │     │
   │     ├─ 反向传播(计算梯度)
   │     │   δ^L = ∇_a C ⊙ σ'(z^L)
   │     │   δ^l = ((w^{l+1})^T δ^{l+1}) ⊙ σ'(z^l)
   │     │   ∇C/∂w = a^{l-1} · δ^l
   │     │   ∇C/b = δ^l
   │     │
   │     └─ 更新权重和偏置
   │         w = w - η·∇C/w
   │         b = b - η·∇C/∂b
   │
   └─ 3.4 在测试集上评估准确率
       准确率 = 正确分类数 / 总样本数

  ↓

4. 训练完成,输出最终准确率

梯度下降路径与损失函数曲线示意图

gradient_descent

反向传播的流程

backpropagation_flow


六、关键概念、函数对比

1. 本文关键概念

概念 作用 公式
前向传播 计算网络输出 \(a^l = \sigma(w^l a^{l-1} + b^l)\)
代价函数 衡量预测误差 \(C = \frac{1}{2n}\sum|y-a^L|^2\)
梯度 指示更新方向 \(\nabla C = \frac{\partial C}{\partial w}\)
反向传播 高效计算梯度 \(\delta^l = ((w^{l+1})^T\delta^{l+1})\odot\sigma'(z^l)\)
学习率 控制更新步长 \(\eta = 0.1\)
小批量 加速训练 \(m = 10\)

2. 与线性回归中的二次代价(标量输出)函数对比

\[J\left( \theta  \right)=\frac{1}{2m}\sum\limits_{i=1}^{m}{{{\left( {{h}_{\theta }}\left( {{x}^{(i)}} \right)-{{y}^{(i)}} \right)}^{2}}} \]

这两个函数在数学本质上是一样的,都是半均方误差(Half Mean Squared Error),只是符号表示和适用场景略有不同。

关于线性回归二次代价详细内容,可以阅读 机器学习入门:用 Python 实现梯度下降线性回归一、1.2 代价函数初始值

详细对比

特性 本文函数(神经网络二次代价 / 向量二次代价) 线性回归中的二次代价函数(标量输出)
函数形式 \(C(w,b)=\frac{1}{2n}\sum_x |y(x)-a^L(x)|^2\) \(J(\theta)=\frac{1}{2m}\sum_{i=1}^m (h_\theta(x^{(i)})-y^{(i)})^2\)
样本数符号 \(n\) \(m\)
预测值符号 \(a^L(x)\)(神经网络输出层激活值) \(h_\theta(x)\)(线性模型假设函数)
误差度量 范数平方 \(|\cdot|^2\)(可处理多维输出) 普通平方 \((\cdot)^2\)(通常针对标量输出)
求和方式 \(\sum_x\)(遍历所有样本) \(\sum_{i=1}^m\)(显式索引)
系数 \(\frac{1}{2n}\) \(\frac{1}{2m}\)

核心相同点

  1. 结构完全一致:都是“1/2 × 平均平方误差”。
  2. 目的相同:最小化预测值与真实值之间的差异。
  3. 导数简洁:前面的 \(\frac{1}{2}\) 因子使得对单个样本的误差求导后恰好为 \((预测-真实)\),没有多余的系数2。
  4. 梯度下降更新规则相同\(\theta \leftarrow \theta - \eta \cdot \frac{1}{m}\sum (h_\theta(x^{(i)})-y^{(i)}) \cdot \frac{\partial h_\theta}{\partial \theta}\)(对于线性模型,\(\frac{\partial h_\theta}{\partial \theta}=x^{(i)}\))。

细微差别

  • 输出维度:第一个函数用范数平方,可以隐含地处理向量输出(如多分类或回归任务输出多个值);第二个函数写成平方形式,通常默认输出是标量。但如果将第二个函数中的平方理解为向量内积(即范数平方),则两者完全等价。
  • 符号习惯:神经网络文献喜欢用 \(n\) 表示样本数、\(w,b\) 表示参数;线性回归常用 \(m\) 表示样本数、\(\theta\) 表示参数。

结论

两个函数核心是一样的。可以认为 线性回归二次代价函数神经网络二次代价标量输出、线性模型 下的特例。两者都是梯度下降法中经典的代价函数形式。

注:引入 \(\dfrac{1}{2}\) 纯粹是为了求导后形式简洁(消去平方产生的因子2)。


七、为什么反向传播如此重要?

  1. 高效计算梯度

- 避免了对每个权重单独计算偏导数
   - 时间复杂度从 \(O(n^2)\) 降低到 \(O(n)\)

  1. 链式法则的完美应用

- 利用复合函数求导法则
   - 从输出层向输入层逐层传播误差

  1. 深度学习的基石

- 所有现代神经网络都依赖此算法
   - 使得训练深层网络成为可能

  1. 数学优雅性

- 四个简洁的方程
   - 统一了各种神经网络的训练方法


八、激活函数:Sigmoid

本代码使用 Sigmoid 作为激活函数:

\[\sigma(z) = \frac{1}{1 + e^{-z}} \]

导数

\[\sigma'(z) = \sigma(z)(1 - \sigma(z)) \]

代码实现

def sigmoid(z):

    """Sigmoid 函数"""

    return 1.0 / (1.0 + np.exp(-z))

def sigmoid_prime(z):

    """Sigmoid 函数的导数"""

    return sigmoid(z) * (1 - sigmoid(z))

示意图:

activation_functions


九、总结

梯度下降 vs 反向传播

对比项 梯度下降 反向传播
角色 优化算法 梯度计算算法
作用 更新参数最小化代价函数 高效计算代价函数的梯度
关系 需要梯度才能工作 为梯度下降提供梯度
核心公式 \(w = w - \eta \nabla C\) \(\delta^l = ((w^{l+1})^T \delta^{l+1}) \odot \sigma'(z^l)\)

完整学习流程

输入数据 → 前向传播 → 计算输出 → 计算代价

                                    ↓

更新参数 ← 使用梯度 ← 计算梯度 ← 反向传播

summary_diagram

梯度下降告诉我们要往哪个方向更新参数反向传播告诉我们如何高效计算梯度。两者结合,构成了神经网络训练的核心!


十、实验结果

根据之前的运行结果,该神经网络在 MNIST 测试集上达到了约 92% 的准确率,证明了梯度下降和反向传播算法的有效性!

对于这个简单的全连接网络 [784, 100, 20, 10] 来说 92% 是合理的基线表现,但现代 CNN 可以达到 99%+。

附:完整代码及数据可从 Gitee 仓库 获取。

文章绘图代码如下:

点击查看代码
#!/usr/bin/env python  
# -*- coding: utf-8 -*-  
"""  
神经网络学习算法可视化  
基于 run_p32.py 的训练过程生成示意图  
"""  
  
import os  
import sys  
  
curdir = os.path.dirname(os.path.abspath(__file__))  
sys.path.append(os.path.join(curdir, os.pardir))  
  
import matplotlib.pyplot as plt  
import numpy as np  
from matplotlib.patches import FancyBboxPatch, FancyArrowPatch, Circle  
from matplotlib import rcParams  
  
# 设置中文字体  
rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'Arial Unicode MS']  
rcParams['axes.unicode_minus'] = False  
  
# 导入项目模块  
import mnist_loader  
import network  
  
  
def draw_network_architecture():  
    """绘制神经网络架构图"""  
    fig, ax = plt.subplots(figsize=(12, 6))  
    ax.set_xlim(0, 10)  
    ax.set_ylim(0, 8)  
    ax.axis('off')  
    ax.set_title('神经网络架构示意图 [784-100-20-10]', fontsize=14, fontweight='bold', pad=20)  
  
    # 定义层的位置和节点数(简化显示)  
    layers = [  
        {'name': '输入层', 'x': 1, 'nodes': 8, 'label': '784 个节点\n(28×28 像素)'},  
        {'name': '隐藏层 1', 'x': 3.5, 'nodes': 6, 'label': '100 个节点'},  
        {'name': '隐藏层 2', 'x': 6, 'nodes': 5, 'label': '20 个节点'},  
        {'name': '输出层', 'x': 8.5, 'nodes': 10, 'label': '10 个节点\n(0-9 数字)'}  
    ]  
  
    node_positions = []  
    # 绘制节点(添加省略号表示更多节点)  
    for layer in layers:  
        y_positions = np.linspace(1, 7, layer['nodes'])  
        node_positions.append([(layer['x'], y) for y in y_positions])  
        for i, (x, y) in enumerate(node_positions[-1]):  
            circle = Circle((x, y), radius=0.15, color='skyblue', ec='navy', lw=1.5, zorder=2)  
            ax.add_patch(circle)  
  
        # 添加省略号表示还有更多节点  
        if layer['nodes'] < 10:  # 只有当显示的节点数少于实际节点数时才添加  
            mid_y = (y_positions[0] + y_positions[-1]) / 2  
            # 使用三个点替代省略号,避免字体问题  
            ax.text(layer['x'], mid_y, '...', ha='center', va='center', fontsize=20, color='navy', fontweight='bold')  
  
        # 层标签  
        ax.text(layer['x'], 7.5, layer['name'], ha='center', fontsize=10, fontweight='bold')  
        ax.text(layer['x'], 0.5, layer['label'], ha='center', fontsize=8, style='italic', color='gray')  
  
    # 绘制连接线(简化,只连接部分节点)  
    for i in range(len(layers) - 1):  
        for pos_from in node_positions[i][::2]:  # 每隔一个节点连接  
            for pos_to in node_positions[i + 1][::2]:  
                line = plt.Line2D([pos_from[0], pos_to[0]], [pos_from[1], pos_to[1]],  
                                  color='gray', alpha=0.3, lw=0.5, zorder=1)  
                ax.add_line(line)  
  
    # 添加前向传播箭头  
    arrow = FancyArrowPatch((2.2, 4), (2.8, 4), arrowstyle='->', mutation_scale=20, lw=2, color='red')  
    ax.add_patch(arrow)  
    ax.text(2.5, 4.3, '前向传播', ha='center', fontsize=9, color='red')  
  
    arrow2 = FancyArrowPatch((4.7, 4), (5.3, 4), arrowstyle='->', mutation_scale=20, lw=2, color='red')  
    ax.add_patch(arrow2)  
    ax.text(5, 4.3, '前向传播', ha='center', fontsize=9, color='red')  
  
    arrow3 = FancyArrowPatch((7.2, 4), (7.8, 4), arrowstyle='->', mutation_scale=20, lw=2, color='red')  
    ax.add_patch(arrow3)  
    ax.text(7.5, 4.3, '前向传播', ha='center', fontsize=9, color='red')  
  
    plt.tight_layout()  
    plt.savefig('network_architecture.png', dpi=150, bbox_inches='tight')  
    print("[OK] 已保存:network_architecture.png")  
    plt.close()  
  
  
def draw_gradient_descent():  
    """绘制梯度下降示意图"""  
    fig, ax = plt.subplots(figsize=(10, 6))  
  
    # 模拟一个复杂的损失曲面  
    x = np.linspace(-3, 3, 100)  
    y = x ** 2 + 0.5 * np.sin(3 * x)  
  
    ax.plot(x, y, 'b-', lw=2, label='损失函数曲线')  
  
    # 模拟梯度下降的步骤  
    steps_x = [2.5, 1.8, 1.0, 0.3, -0.2, -0.5, -0.7, -0.8]  
    steps_y = [step ** 2 + 0.5 * np.sin(3 * step) for step in steps_x]  
  
    ax.plot(steps_x, steps_y, 'ro-', markersize=6, label='梯度下降路径')  
  
    # 找到最低点(真正的最优点)  
    min_idx = np.argmin(y)  
    min_x = x[min_idx]  
    min_y = y[min_idx]  
  
    # 标注起点和终点  
    ax.annotate('起点', xy=(steps_x[0], steps_y[0]), xytext=(2, 8),  
                arrowprops=dict(arrowstyle='->', color='green', lw=1.5), fontsize=10, color='green')  
    # 标注最优点(最低点)  
    ax.annotate('最优点\n(损失最小)', xy=(min_x, min_y), xytext=(min_x + 0.3, min_y + 1),  
                arrowprops=dict(arrowstyle='->', color='red', lw=1.5), fontsize=10, color='red')  
  
    # 在几个点处绘制切线(梯度方向)  
    for i, step_x in enumerate(steps_x[::2]):  
        grad = 2 * step_x + 1.5 * np.cos(3 * step_x)  # 导数  
        tangent_x = np.linspace(step_x - 0.8, step_x + 0.8, 10)  
        tangent_y = steps_y[steps_x.index(step_x)] + grad * (tangent_x - step_x)  
        ax.plot(tangent_x, tangent_y, 'k--', alpha=0.5, lw=1)  
  
    ax.set_xlabel('参数 (θ)', fontsize=12)  
    ax.set_ylabel('损失值 J(θ)', fontsize=12)  
    ax.set_title('梯度下降:沿着梯度的反方向逼近最优解', fontsize=14, fontweight='bold')  
    ax.legend()  
    ax.grid(True, alpha=0.3)  
  
    plt.tight_layout()  
    plt.savefig('gradient_descent.png', dpi=150, bbox_inches='tight')  
    print("[OK] 已保存:gradient_descent.png")  
    plt.close()  
  
  
def draw_backpropagation_flow():  
    """绘制反向传播流程图"""  
    fig, ax = plt.subplots(figsize=(12, 6))  
    ax.set_xlim(0, 10)  
    ax.set_ylim(0, 6)  
    ax.axis('off')  
  
    # 定义块的位置  
    blocks = {  
        'input': (1, 2.5),  
        'layer1': (3, 2.5),  
        'layer2': (5, 2.5),  
        'output': (7, 2.5),  
        'loss': (9, 2.5)  
    }  
  
    # 绘制块  
    for name, (x, y) in blocks.items():  
        width = 1.2  
        height = 1.5  
        rect = FancyBboxPatch((x - width / 2, y - height / 2), width, height,  
                              boxstyle="round,pad=0.1", facecolor='lightblue', edgecolor='black', lw=1.5)  
        ax.add_patch(rect)  
        if name == 'input':  
            ax.text(x, y, '输入数据\nx', ha='center', va='center', fontsize=10)  
        elif name == 'layer1':  
            ax.text(x, y, '隐藏层 1\n(100)', ha='center', va='center', fontsize=10)  
        elif name == 'layer2':  
            ax.text(x, y, '隐藏层 2\n(20)', ha='center', va='center', fontsize=10)  
        elif name == 'output':  
            ax.text(x, y, '输出层\n(10)', ha='center', va='center', fontsize=10)  
        elif name == 'loss':  
            ax.text(x, y, '损失函数\nC(w,b)', ha='center', va='center', fontsize=10)  
  
    # 前向传播 (红色实线)  
    fwd_arrows = [  
        (blocks['input'], blocks['layer1']),  
        (blocks['layer1'], blocks['layer2']),  
        (blocks['layer2'], blocks['output']),  
        (blocks['output'], blocks['loss'])  
    ]  
    for start, end in fwd_arrows:  
        arrow = FancyArrowPatch(start, end, arrowstyle='->', mutation_scale=20,  
                                lw=2, color='red', connectionstyle="arc3,rad=0.1")  
        ax.add_patch(arrow)  
  
    # 反向传播 (蓝色虚线) - 从右到左的反向箭头  
    bwd_arrows = [  
        (blocks['loss'], blocks['output']),  
        (blocks['output'], blocks['layer2']),  
        (blocks['layer2'], blocks['layer1'])  
    ]  
    for start, end in bwd_arrows:  
        # 从 start 指向 end 的反向箭头  
        arrow = FancyArrowPatch(start, end, arrowstyle='->', mutation_scale=20,  
                                lw=2, color='blue', linestyle='--', connectionstyle="arc3,rad=-0.1")  
        ax.add_patch(arrow)  
  
    # 添加图例和标签  
    ax.text(1, 4.5, '前向传播:计算预测值', fontsize=10, color='red', weight='bold')  
    ax.text(1, 4.0, '反向传播:计算梯度', fontsize=10, color='blue', weight='bold')  
    ax.text(5, -0.5, '反向传播的核心:链式法则与误差回传', fontsize=12, ha='center', weight='bold',  
            bbox=dict(boxstyle="round,pad=0.3", facecolor='yellow', alpha=0.3))  
  
    plt.tight_layout()  
    plt.savefig('backpropagation_flow.png', dpi=150, bbox_inches='tight')  
    print("[OK] 已保存:backpropagation_flow.png")  
    plt.close()  
  
  
def draw_training_curves():  
    """绘制训练准确率曲线"""  
    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))  
  
    # 提供的数据  
    accuracies = [5377, 7215, 7881, 8238, 8415, 8549, 8633, 8714, 8770, 8843,  
                  8878, 8909, 8952, 8979, 8995, 9031, 9042, 9062, 9070, 9084,  
                  9115, 9119, 9133, 9150, 9163, 9164, 9171, 9198, 9202, 9217]  
    accuracy_percent = [x / 100.0 for x in accuracies]  
    epochs = np.arange(30)  
  
    # 子图 1: 准确率曲线  
    ax1.plot(epochs, accuracy_percent, 'b-o', linewidth=2, markersize=4, label='测试准确率')  
    ax1.axhline(y=90, color='r', linestyle='--', alpha=0.7, label='90% 目标线')  
    ax1.set_xlabel('迭代次数 (Epoch)', fontsize=12)  
    ax1.set_ylabel('准确率 (%)', fontsize=12)  
    ax1.set_title('MNIST 手写数字识别 - 训练曲线', fontsize=12, fontweight='bold')  
    ax1.legend()  
    ax1.grid(True, alpha=0.3)  
    ax1.set_ylim(50, 95)  
  
    # 标注起点和终点  
    ax1.annotate(f'初始:{accuracy_percent[0]:.1f}%', xy=(0, accuracy_percent[0]),  
                 xytext=(2, 70), arrowprops=dict(arrowstyle='->'), fontsize=9)  
    ax1.annotate(f'最终:{accuracy_percent[-1]:.1f}%', xy=(29, accuracy_percent[-1]),  
                 xytext=(24, 94), arrowprops=dict(arrowstyle='->'), fontsize=9)  
  
    # 子图 2: 每轮提升幅度  
    improvements = np.diff(accuracy_percent)  
    colors = ['green' if x > 0 else 'red' for x in improvements]  
    ax2.bar(epochs[1:], improvements, color=colors, alpha=0.7, edgecolor='black')  
    ax2.set_xlabel('迭代次数 (Epoch)', fontsize=12)  
    ax2.set_ylabel('准确率提升 (%)', fontsize=12)  
    ax2.set_title('每轮迭代的准确率提升', fontsize=12, fontweight='bold')  
    ax2.grid(True, alpha=0.3, axis='y')  
    ax2.axhline(y=0, color='black', lw=0.5)  
  
    plt.tight_layout()  
    plt.savefig('training_curves.png', dpi=150, bbox_inches='tight')  
    print("[OK] 已保存:training_curves.png")  
    plt.close()  
  
  
def draw_activation_functions():  
    """绘制激活函数及其导数"""  
    fig, ax = plt.subplots(figsize=(10, 6))  
  
    x = np.linspace(-7, 7, 200)  
    sigmoid = 1 / (1 + np.exp(-x))  
    sigmoid_prime = sigmoid * (1 - sigmoid)  
  
    ax.plot(x, sigmoid, 'b-', lw=2.5, label='Sigmoid 函数 $\\sigma(z)$')  
    ax.plot(x, sigmoid_prime, 'r--', lw=2, label='Sigmoid 导数 $\\sigma\'(z) = \\sigma(z)(1-\\sigma(z))$')  
  
    # 标注关键点  
    ax.axhline(y=0, color='gray', lw=0.8)  
    ax.axhline(y=1, color='gray', lw=0.8, linestyle=':')  
    ax.axhline(y=0.5, color='gray', lw=0.8, linestyle=':')  
    ax.axvline(x=0, color='gray', lw=0.8)  
  
    # 标注最大梯度点  
    ax.plot(0, 0.5, 'ko', markersize=6)  
    ax.annotate('最大梯度点\n(z=0, 梯度=0.25)', xy=(0, 0.5), xytext=(1.5, 0.7),  
                arrowprops=dict(arrowstyle='->'), fontsize=9)  
  
    ax.set_xlabel('z (加权输入)', fontsize=12)  
    ax.set_ylabel('激活值 / 梯度', fontsize=12)  
    ax.set_title('Sigmoid 激活函数及其导数', fontsize=14, fontweight='bold')  
    ax.legend(fontsize=10)  
    ax.grid(True, alpha=0.3)  
    ax.set_ylim(-0.1, 1.1)  
  
    plt.tight_layout()  
    plt.savefig('activation_functions.png', dpi=150, bbox_inches='tight')  
    print("[OK] 已保存:activation_functions.png")  
    plt.close()  
  
  
def draw_summary_diagram():  
    """绘制总结图"""  
    fig, ax = plt.subplots(figsize=(10, 6))  
    ax.set_xlim(0, 10)  
    ax.set_ylim(0, 8)  
    ax.axis('off')  
  
    # 左侧:梯度下降  
    rect1 = FancyBboxPatch((0.5, 4), 4, 3, boxstyle="round,pad=0.2", facecolor='lightgreen', edgecolor='green', lw=2)  
    ax.add_patch(rect1)  
    ax.text(2.5, 6.5, '梯度下降 (Gradient Descent)', ha='center', fontsize=12, weight='bold', color='darkgreen')  
    ax.text(2.5, 5.5, '优化算法', ha='center', fontsize=10, style='italic')  
    ax.text(2.5, 4.8, '作用:更新参数,最小化损失', ha='center', fontsize=9)  
    ax.text(2.5, 4.3, '公式:$w \\leftarrow w - \\eta \\nabla C$', ha='center', fontsize=9)  
  
    # 右侧:反向传播  
    rect2 = FancyBboxPatch((5.5, 4), 4, 3, boxstyle="round,pad=0.2", facecolor='lightblue', edgecolor='blue', lw=2)  
    ax.add_patch(rect2)  
    ax.text(7.5, 6.5, '反向传播 (Backpropagation)', ha='center', fontsize=12, weight='bold', color='darkblue')  
    ax.text(7.5, 5.5, '梯度计算算法', ha='center', fontsize=10, style='italic')  
    ax.text(7.5, 4.8, '作用:高效计算梯度', ha='center', fontsize=9)  
    ax.text(7.5, 4.3, '核心:链式法则 + 误差回传', ha='center', fontsize=9)  
  
    # 中间的连接箭头 - 改为两个单向箭头  
    arrow1 = FancyArrowPatch((4.5, 5.7), (5.3, 5.7), arrowstyle='->', mutation_scale=20, lw=2, color='gray')  
    ax.add_patch(arrow1)  
    ax.text(4.9, 6.0, '协同工作', ha='center', fontsize=10, weight='bold')  
  
    arrow2 = FancyArrowPatch((5.5, 5.3), (4.7, 5.3), arrowstyle='->', mutation_scale=20, lw=2, color='gray')  
    ax.add_patch(arrow2)  
    ax.text(5.1, 4.9, '提供梯度', ha='center', fontsize=9)  
  
    # 底部:完整流程  
    rect3 = FancyBboxPatch((2, 0.5), 6, 2, boxstyle="round,pad=0.2", facecolor='#FFE4B5', edgecolor='orange', lw=2)  
    ax.add_patch(rect3)  
    ax.text(5, 2.0, '完整训练流程', ha='center', fontsize=12, weight='bold')  
    ax.text(5, 1.5, '前向传播 → 计算损失 → 反向传播 (计算梯度) → 梯度下降 (更新参数)', ha='center', fontsize=10)  
    ax.text(5, 1.0, '循环迭代直至损失收敛', ha='center', fontsize=9, style='italic', color='gray')  
  
    # 从上方两个模块向下指向底部模块的箭头  
    arrow_down1 = FancyArrowPatch((2.5, 4), (3.5, 2.5), arrowstyle='->', mutation_scale=15, lw=1.5, color='gray')  
    ax.add_patch(arrow_down1)  
    arrow_down2 = FancyArrowPatch((7.5, 4), (6.5, 2.5), arrowstyle='->', mutation_scale=15, lw=1.5, color='gray')  
    ax.add_patch(arrow_down2)  
  
    plt.tight_layout()  
    plt.savefig('summary_diagram.png', dpi=150, bbox_inches='tight')  
    print("[OK] 已保存:summary_diagram.png")  
    plt.close()  
  
  
def main():  
    """主函数:生成所有示意图"""  
    print("=" * 60)  
    print("开始生成神经网络学习算法示意图...")  
    print("=" * 60)  
  
    draw_network_architecture()  
    draw_gradient_descent()  
    draw_backpropagation_flow()  
    draw_training_curves()  
    draw_activation_functions()  
    draw_summary_diagram()  
  
    print("\n" + "=" * 60)  
    print("所有图表已生成完毕!")  
    print("=" * 60)  
    print("\n生成的文件:")  
    print("  - network_architecture.png  (神经网络架构)")  
    print("  - gradient_descent.png      (梯度下降)")  
    print("  - backpropagation_flow.png  (反向传播流程)")  
    print("  - training_curves.png       (训练准确率曲线)")  
    print("  - activation_functions.png  (Sigmoid 函数)")  
    print("  - summary_diagram.png       (总结图)")  
  
  
if __name__ == "__main__":  
    main()

📢 声明:本文借助AI辅助工具进行资料整理与初稿生成,所有内容均经过作者本人的详细核对、修改与编排,文责自负。

posted @ 2026-04-14 19:32  Lyn_Li  阅读(68)  评论(0)    收藏  举报