神经网络如何学习?从梯度下降到反向传播的完整解析(附 MNIST 实战)
本文参考深度学习经典入门教材 Neural Networks and Deep Learning 中第1章 使用神经网络识别手写数字 代码文件 run_p32.py 利用基础神经网络训练 MNIST,完整代码可查看文章末尾链接。
神经网络学习算法详解
前言:训练效果示意图

经过 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 数字)
训练流程示意图

注:本图为简化示意图,实际每一层的每个节点都与下一层所有节点相连(全连接)。
二、梯度下降算法 (Gradient Descent)
1. 核心思想
梯度下降是一种优化算法,用于最小化代价函数。基本思想是:
沿着梯度的反方向更新参数,逐步逼近最优解。
2. 数学原理
代价函数(二次代价函数):
其中:
- \(w\):权重矩阵
- \(b\):偏置向量
- \(n\):训练样本数
- \(y(x)\):真实标签
- \(a^L(x)\):网络输出
参数更新规则:
其中:
-
\(\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)}")
示意图:

注:小批量梯度下降在损失曲面上的路径(含中心区域放大)。红色路径展示了参数更新时的震荡特性,右下角放大图显示了收敛到最低点附近的细节。
绘图代码如下:
点击查看代码
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 = self.cost_derivative(activations[-1], y) * sigmoid_prime(zs[-1])
其中:
-
\(\nabla_a C = (a^L - y)\):代价函数对输出的偏导
-
\(\sigma'(z^L)\):激活函数的导数
-
\(\odot\):Hadamard 积(逐元素相乘)
方程 2:误差在第 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:代价函数对偏置的梯度
代码实现:
nabla_b[-l] = delta
注:代码中 \(-l\) 表示 Python 负索引,即从后往前数第 \(l\) 层
这个方程说明:
- 代价函数对偏置的梯度等于该层的误差
方程 4:代价函数对权重的梯度
代码实现:
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)]
更新公式:
其中 \(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. 训练完成,输出最终准确率
梯度下降路径与损失函数曲线示意图

反向传播的流程

六、关键概念、函数对比
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. 与线性回归中的二次代价(标量输出)函数对比
这两个函数在数学本质上是一样的,都是半均方误差(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/2 × 平均平方误差”。
- 目的相同:最小化预测值与真实值之间的差异。
- 导数简洁:前面的 \(\frac{1}{2}\) 因子使得对单个样本的误差求导后恰好为 \((预测-真实)\),没有多余的系数2。
- 梯度下降更新规则相同:\(\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)。
七、为什么反向传播如此重要?
- 高效计算梯度
- 避免了对每个权重单独计算偏导数
- 时间复杂度从 \(O(n^2)\) 降低到 \(O(n)\)
- 链式法则的完美应用
- 利用复合函数求导法则
- 从输出层向输入层逐层传播误差
- 深度学习的基石
- 所有现代神经网络都依赖此算法
- 使得训练深层网络成为可能
- 数学优雅性
- 四个简洁的方程
- 统一了各种神经网络的训练方法
八、激活函数:Sigmoid
本代码使用 Sigmoid 作为激活函数:
导数:
代码实现:
def sigmoid(z):
"""Sigmoid 函数"""
return 1.0 / (1.0 + np.exp(-z))
def sigmoid_prime(z):
"""Sigmoid 函数的导数"""
return sigmoid(z) * (1 - sigmoid(z))
示意图:

九、总结
梯度下降 vs 反向传播
| 对比项 | 梯度下降 | 反向传播 |
|---|---|---|
| 角色 | 优化算法 | 梯度计算算法 |
| 作用 | 更新参数最小化代价函数 | 高效计算代价函数的梯度 |
| 关系 | 需要梯度才能工作 | 为梯度下降提供梯度 |
| 核心公式 | \(w = w - \eta \nabla C\) | \(\delta^l = ((w^{l+1})^T \delta^{l+1}) \odot \sigma'(z^l)\) |
完整学习流程
输入数据 → 前向传播 → 计算输出 → 计算代价
↓
更新参数 ← 使用梯度 ← 计算梯度 ← 反向传播

梯度下降告诉我们要往哪个方向更新参数,反向传播告诉我们如何高效计算梯度。两者结合,构成了神经网络训练的核心!
十、实验结果
根据之前的运行结果,该神经网络在 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辅助工具进行资料整理与初稿生成,所有内容均经过作者本人的详细核对、修改与编排,文责自负。

浙公网安备 33010602011771号