查看全课大纲(第 29 / 93 节)
  1. 1.计算机视觉概述
  2. 2.计算机视觉应用场景
  3. 3.困难与挑战
  4. 4.开源库介绍
  5. 5.图像显示与存储
  6. 6.CLAHE
  7. 7.腐蚀膨胀与开闭运算
  8. 8.滤波与边缘填充
  9. 9.各种滤波
  10. 10.图像金字塔
  11. 11.傅里叶变换
  12. 12.短时傅里叶变换与小波变换
  13. 13.颜色特征与颜色直方图
  14. 14.几何特征_边缘
  15. 15.几何特征-角点与斑点
  16. 16.局部特征-SIFT特征描述子
  17. 17.局部特征-SURF特征描述子
  18. 18.局部特征-ORB特征描述子
  19. 19.其他特征描述子
  20. 20.传统图像分割技术
  21. 21.GraphCut
  22. 22.GrabCut
  23. 23.人脸检测的传统方法
  24. 24.行人检测的传统方法HOG+SVM
  25. 25.行人检测DPM
  26. 26.人工神经网络的架构
  27. 27.目标函数与梯度下降
  28. 28.误差反向传播
  29. 29.可手算的神经网络
  30. 30.改进目标函数与克服梯度消散
  31. 31.神经网络之传统神经网络与深度网络的差异
  32. 32.各种梯度下降算法
  33. 33.如何选择合适的梯度下降算法
  34. 34.避免过适应
  35. 35.CNN的架构
  36. 36.CNN的误差反向传播_卷积层
  37. 37.CNN的误差反向传播_池化层
  38. 38.分类所用的数据集
  39. 39.Alexnet详解
  40. 40.VGG详解
  41. 41.GoogLeNet_v1
  42. 42.BatchNormalization
  43. 43.GoogLeNet_v3v4
  44. 44.ResNet
  45. 45.ResNeXt
  46. 46.CNN设计准则
  47. 47.图像检索与推土机距离
  48. 48.基于颜色相似性
  49. 49.基于形状相似性
  50. 50.基于纹理相似性
  51. 51.基于局部特征相似性
  52. 52.DL的二值哈希编码
  53. 53.高维空间中检索KDTree
  54. 54.高维空间中检索LSH
  55. 55.目标识别任务综述
  56. 56.RCNN
  57. 57.SPP-Net
  58. 58.Fast R-CNN
  59. 59.Faster R-CNN
  60. 60.R-CNN系列模型回顾
  61. 61.R-FCN的设计思路
  62. 62.R-FCN的实现
  63. 63.YOLOv1
  64. 64.YOLOv2_9000
  65. 65.YOLOv3
  66. 66.图像分割应用场景与数据集
  67. 67.上采样的实现方式
  68. 68.FCN网络架构的由来
  69. 69.FCN构建细节
  70. 70.DeepLabv1
  71. 71.CRF
  72. 72.DeepLabv2
  73. 73.DeepLabv3
  74. 74.特定的医学影像分割任务
  75. 75.U-Net
  76. 76.3D U-Net
  77. 77.V-Net
  78. 78.DenseNet
  79. 79.病理切片分析任务及数据集
  80. 80.病理切片分析
  81. 81.图说任务及其传统处理方法
  82. 82.深度语言模型与RNN
  83. 83.LSTM及其变种
  84. 84.NIC模型
  85. 85.SAT模型
  86. 86.图说的性能指标
  87. 87.判别模型与生成模型
  88. 88.KL散度与JS散度
  89. 89.VAE
  90. 90.GAN的基本原理
  91. 91.DCGAN与CGAN
  92. 92.Wasserstein GAN
  93. 93.SRGAN

可手算的神经网络

小象实战讲义 · 基于深度学习的计算机视觉

讲完了误差反向传播(BP)的理论,我们来看一个可以手算的简单神经网络例子。通过这个例子,你将直观地理解前向传播、误差计算、反向传播以及权重更新的完整过程,把抽象的理论落实到具体的数字运算上。

💡 核心导读

  • 构造最简单的带隐藏层神经网络:输入层 2 个节点,隐藏层 2 个节点,输出层 1 个节点。
  • 完成一次完整的前向与反向传播:使用 Sigmoid 激活函数,从随机初始权重开始,计算输出误差,并反向传播误差以更新所有权重。
  • 理解权重更新的方向:通过手算验证,更新后的权重能使网络输出更接近期望值。
  • 认识梯度下降的三种模式:批量梯度下降(BGD)、随机梯度下降(SGD)与小批次梯度下降(MBGD),并理解 MBGD 为何成为实际训练中的主流选择。

一个最简单的神经网络架构

我们首先构建一个可能的最简单的、带有一个隐藏层的前馈神经网络。为演示隐藏层中误差如何分配到多条连接,本例采用 2-2-1 结构,这是能展示隐藏层作用的最小配置之一。

这个最简单的网络结构如下:

  • 输入层:两个神经元,分别接收一个样本的两个特征值 A 和 B。本例中,样本为 [A=0.35, B=0.9]。
  • 隐藏层:两个神经元,记为 P 和 Q。
  • 输出层:一个神经元,记为 O,输出最终的预测值。
  • 连接权重:共有 6 个权重,我们随机给定一组较小的初始值,这是神经网络训练的标准起点。

下图清晰地展示了这个网络的结构、输入值以及初始权重:

输入层            隐藏层              输出层
A=0.35 ─w_AP=0.1→ P
B=0.9  ─w_BP=0.8→ P
A=0.35 ─w_AQ=0.4→ Q
B=0.9  ─w_BQ=0.6→ Q
P ─w_PO=0.3→ O      (期望输出: 0.5)
Q ─w_QO=0.9→ O

注意:上图中,A 和 B 是输入节点,它们本身不进行加权和与激活函数计算,只是数据的入口。真正的“神经元”是 P、Q 和 O,它们会执行“加权和 + 激活函数”的操作。

前向传播:计算当前输出

前向传播就是数据从输入层流向输出层的过程。每个神经元(P, Q, O)的计算分为两步:

  1. 计算净输入(net):所有输入乘以其对应权重的和。
  2. 计算输出(out):将净输入通过激活函数。本例中,三个神经元均使用 Sigmoid 函数,其表达式为 f(x)=11+e−xf(x) = \frac{1}{1 + e^{-x}}。

Sigmoid 函数有一个非常好的性质:它的导数可以用其输出值 yy 简洁地表示:f′(x)=y(1−y)f'(x) = y(1 - y)。这个性质将在反向传播中大大简化我们的计算。

现在,让我们用代码实现一次前向传播,看看在随机初始权重下,网络对于输入 [0.35, 0.9] 的实际输出是多少。

import numpy as np

# 定义Sigmoid函数及其导数(备用)
def sigmoid(x):
    return 1 / (1 + np.exp(-x))

def sigmoid_derivative(y):
    return y * (1 - y)

# 输入样本 (一个样本的两个特征)
A = 0.35
B = 0.9

# 初始权重 (随机给定)
w_AP = 0.1
w_BP = 0.8
w_AQ = 0.4
w_BQ = 0.6
w_PO = 0.3
w_QO = 0.9

print("初始权重:")
print(f"w_AP={w_AP}, w_BP={w_BP}, w_AQ={w_AQ}, w_BQ={w_BQ}, w_PO={w_PO}, w_QO={w_QO}")

# --- 前向传播计算 ---
# 隐藏层 P 和 Q
net_P = A * w_AP + B * w_BP
out_P = sigmoid(net_P)

net_Q = A * w_AQ + B * w_BQ
out_Q = sigmoid(net_Q)

# 输出层 O
net_O = out_P * w_PO + out_Q * w_QO
out_O = sigmoid(net_O)

print("\n前向传播结果:")
print(f"隐藏层 P 输出: out_P = {out_P:.4f}")
print(f"隐藏层 Q 输出: out_Q = {out_Q:.4f}")
print(f"最终输出层 O 输出: out_O = {out_O:.4f}")
初始权重:
w_AP=0.1, w_BP=0.8, w_AQ=0.4, w_BQ=0.6, w_PO=0.3, w_QO=0.9

前向传播结果:
隐藏层 P 输出: out_P = 0.6803
隐藏层 Q 输出: out_Q = 0.6637
最终输出层 O 输出: out_O = 0.6903

计算结果显示,网络当前的输出是 0.6903。而我们的期望输出(即样本的标签)是 0.5。这就产生了误差。我们的目标就是通过调整那 6 个权重,使得网络输出尽可能接近 0.5。

反向传播:计算权重更新量

误差产生了,接下来就要用误差反向传播算法来指导权重的更新。我们采用平方误差损失函数,对于单个样本,损失为 J=12(target−outO)2J = \frac{1}{2}(target - out_O)^2。我们的目标是求损失函数 JJ 对各个权重 ww 的偏导数 ∂J∂w\frac{\partial J}{\partial w},然后按照梯度下降的方向更新权重:wnew=wold−η⋅∂J∂ww_{new} = w_{old} - \eta \cdot \frac{\partial J}{\partial w}。其中 η\eta 是学习率,为了简化手算,本例中设 η=1\eta = 1。

反向传播是链式求导法则的经典应用,其核心步骤是从输出层向输入层逐层计算“残差”,再用残差与当前层的输入相乘得到权重的梯度。这里沿用上一节的符号约定:残差中的误差写作(期望 − 实际),因此梯度公式前会带一个负号。

1. 输出层权重的更新

首先更新离误差最近的权重:wPOw_{PO} 和 wQOw_{QO}。

  • 计算输出层残差 δO\delta_O:δO=(target−outO)⋅f′(netO)=(target−outO)⋅outO(1−outO)\delta_O = (target - out_O) \cdot f'(net_O) = (target - out_O) \cdot out_O(1 - out_O)
  • 计算梯度:∂J∂wPO=−δO⋅outP\frac{\partial J}{\partial w_{PO}} = -\delta_O \cdot out_P, ∂J∂wQO=−δO⋅outQ\frac{\partial J}{\partial w_{QO}} = -\delta_O \cdot out_Q
  • 更新权重:wPOnew=wPO−η⋅∂J∂wPO=wPO+ηδOoutPw_{PO_new} = w_{PO} - \eta \cdot \frac{\partial J}{\partial w_{PO}} = w_{PO} + \eta \delta_O out_P, wQOneww_{QO_new} 同理。

2. 隐藏层权重的更新

然后更新隐藏层的权重:wAPw_{AP}, wBPw_{BP}, wAQw_{AQ}, wBQw_{BQ}。

  • 计算隐藏层残差(例如 δP\delta_P):隐藏层神经元的误差不是直接与目标值比较,而是来自于它连接的后一层(输出层)残差的加权和。 δP=f′(netP)⋅(δO⋅wPO)=outP(1−outP)⋅(δO⋅wPO)\delta_P = f'(net_P) \cdot (\delta_O \cdot w_{PO}) = out_P(1 - out_P) \cdot (\delta_O \cdot w_{PO}) δQ\delta_Q 的计算同理,使用 wQOw_{QO}。
  • 计算梯度:∂J∂wAP=−δP⋅A\frac{\partial J}{\partial w_{AP}} = -\delta_P \cdot A, ∂J∂wBP=−δP⋅B\frac{\partial J}{\partial w_{BP}} = -\delta_P \cdot B, ∂J∂wAQ=−δQ⋅A\frac{\partial J}{\partial w_{AQ}} = -\delta_Q \cdot A, ∂J∂wBQ=−δQ⋅B\frac{\partial J}{\partial w_{BQ}} = -\delta_Q \cdot B
  • 更新权重。

下面我们用代码实现这一过程:

import numpy as np

# 重新定义输入、权重和函数,确保本代码块自包含
A = 0.35
B = 0.9
target = 0.5

w_AP = 0.1; w_BP = 0.8; w_AQ = 0.4; w_BQ = 0.6; w_PO = 0.3; w_QO = 0.9

def sigmoid(x): return 1 / (1 + np.exp(-x))
def sigmoid_derivative(y): return y * (1 - y)

# 前向传播(使用初始权重)
out_P = sigmoid(A * w_AP + B * w_BP)
out_Q = sigmoid(A * w_AQ + B * w_BQ)
out_O = sigmoid(out_P * w_PO + out_Q * w_QO)

print(f"初始输出 out_O = {out_O:.4f}, 目标 target = {target}")

# --- 反向传播 ---
# 1. 输出层(残差约定:误差写作 期望 - 实际)
delta_O = (target - out_O) * sigmoid_derivative(out_O)
grad_w_PO = -delta_O * out_P
grad_w_QO = -delta_O * out_Q

# 更新输出层权重 (学习率 eta = 1)
w_PO_new = w_PO - grad_w_PO
w_QO_new = w_QO - grad_w_QO

print(f"\n输出层残差 delta_O = {delta_O:.6f}")
print(f"权重梯度: grad_w_PO = {grad_w_PO:.6f}, grad_w_QO = {grad_w_QO:.6f}")
print(f"更新后权重: w_PO_new = {w_PO_new:.4f}, w_QO_new = {w_QO_new:.4f}")

# 2. 隐藏层
delta_P = sigmoid_derivative(out_P) * (delta_O * w_PO)  # 注意:这里使用更新前的w_PO
delta_Q = sigmoid_derivative(out_Q) * (delta_O * w_QO)  # 注意:这里使用更新前的w_QO

grad_w_AP = -delta_P * A
grad_w_BP = -delta_P * B
grad_w_AQ = -delta_Q * A
grad_w_BQ = -delta_Q * B

# 更新隐藏层权重
w_AP_new = w_AP - grad_w_AP
w_BP_new = w_BP - grad_w_BP
w_AQ_new = w_AQ - grad_w_AQ
w_BQ_new = w_BQ - grad_w_BQ

print(f"\n隐藏层残差: delta_P = {delta_P:.6f}, delta_Q = {delta_Q:.6f}")
print(f"权重梯度: grad_w_AP={grad_w_AP:.6f}, grad_w_BP={grad_w_BP:.6f}, grad_w_AQ={grad_w_AQ:.6f}, grad_w_BQ={grad_w_BQ:.6f}")
print(f"更新后权重: w_AP_new={w_AP_new:.5f}, w_BP_new={w_BP_new:.5f}, w_AQ_new={w_AQ_new:.5f}, w_BQ_new={w_BQ_new:.5f}")
初始输出 out_O = 0.6903, 目标 target = 0.5

输出层残差 delta_O = -0.040681
权重梯度: grad_w_PO = 0.027674, grad_w_QO = 0.027002
更新后权重: w_PO_new = 0.2723, w_QO_new = 0.8730

隐藏层残差: delta_P = -0.002654, delta_Q = -0.008172
权重梯度: grad_w_AP=0.000929, grad_w_BP=0.002389, grad_w_AQ=0.002860, grad_w_BQ=0.007354
更新后权重: w_AP_new=0.09907, w_BP_new=0.79761, w_AQ_new=0.39714, w_BQ_new=0.59265

验证:更新权重后的效果

权重已经更新完毕,现在我们将新的权重代入网络,再次进行前向传播,观察输出是否如我们预期的那样,更接近目标值 0.5。

import numpy as np

# 使用上一步计算得到的新权重
w_AP_new = 0.09907; w_BP_new = 0.79761; w_AQ_new = 0.39714; w_BQ_new = 0.59265; w_PO_new = 0.2723; w_QO_new = 0.8730
A = 0.35; B = 0.9

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

# 使用新权重进行前向传播
out_P_new = sigmoid(A * w_AP_new + B * w_BP_new)
out_Q_new = sigmoid(A * w_AQ_new + B * w_BQ_new)
out_O_new = sigmoid(out_P_new * w_PO_new + out_Q_new * w_QO_new)

print("使用新权重进行前向传播:")
print(f"新隐藏层输出: out_P_new = {out_P_new:.4f}, out_Q_new = {out_Q_new:.4f}")
print(f"新最终输出: out_O_new = {out_O_new:.4f}")
print(f"与目标值 0.5 的差距: {abs(out_O_new - 0.5):.4f}")
使用新权重进行前向传播:
新隐藏层输出: out_P_new = 0.6797, out_Q_new = 0.6620
新最终输出: out_O_new = 0.6820
与目标值 0.5 的差距: 0.1820

可以看到,经过一次权重更新后,网络输出从 0.6903 变成了 0.6820,虽然距离目标 0.5 仍有差距,但确实朝着 0.5 的方向靠近了。这就是梯度下降法在起作用:每次更新都使损失函数值减小一点。只要重复这个过程成百上千次,最终输出就会非常接近 0.5。

从单个样本到海量数据:梯度下降的三种模式

上面的例子只使用了一个样本进行训练。但在实际中,我们拥有成千上万的训练样本。如何利用所有样本来指导网络学习呢?这就引出了梯度下降的三种策略:

  1. 批量梯度下降(Batch Gradient Descent, BGD):

    • 做法:每次权重更新,都要计算所有训练样本的损失,然后求平均损失对权重的梯度。
    • 优点:每次更新都指向整个数据集上的全局最优方向,收敛稳定。
    • 缺点:计算开销巨大,尤其当数据集庞大时,一次更新都可能慢得无法接受。
  2. 随机梯度下降(Stochastic Gradient Descent, SGD):

    • 做法:每次权重更新,只随机使用一个样本来计算梯度并立即更新。
    • 优点:更新速度极快,对于海量数据,可能只用一部分样本就达到了不错的解。
    • 缺点:单个样本的梯度方向噪音很大,并不是每次都朝向整体最优解,导致收敛过程波动剧烈,甚至可能因异常样本而偏离正确方向。
  3. 小批次梯度下降(Mini-batch Gradient Descent, MBGD):

    • 做法:这是 BGD 和 SGD 的折中。每次更新,随机选取一个小批次(Mini-batch) 的样本(例如 32、64、128个)来计算平均梯度。
    • 优点:兼顾了两者的优势。相比 BGD,计算效率高;相比 SGD,梯度估计更准确,收敛更平稳。
    • 现状:如今在深度学习中,当人们提到“随机梯度下降(SGD)”时,通常指的就是这种小批次梯度下降(MBGD)。

其更新公式可以统一表示为:

θnew=θold−η⋅1m∑i=1m∇θL(f(x(i);θ),y(i))\theta_{new} = \theta_{old} - \eta \cdot \frac{1}{m} \sum_{i=1}^{m} \nabla_{\theta} L(f(x^{(i)}; \theta), y^{(i)})

其中:

  • θ\theta 代表所有待优化的权重参数。
  • η\eta 是学习率。
  • mm 是批次大小(batch size)。当 mm 等于整个训练集大小时,就是 BGD;当 m=1m=1 时,就是 SGD;当 1<m≪1 < m \ll 总样本数时,就是 MBGD。
  • LL 是损失函数,f(x(i);θ)f(x^{(i)}; \theta) 是网络对第 ii 个样本的预测,y(i)y^{(i)} 是真实标签。

📝 动手练一练

  1. 概念辨析:假设你有一个包含 10,000 张图片的数据集,使用批量大小为 100 的小批次梯度下降进行训练。请问,遍历一遍全部数据(一个 epoch),权重会被更新多少次?如果使用真正的随机梯度下降(m=1)呢?
👉 点击查看参考答案
  • 小批次梯度下降 (batch size=100):权重更新次数 = 总样本数 / 批次大小 = 10,000 / 100 = 100次。
  • 随机梯度下降 (m=1):权重更新次数 = 总样本数 = 10,000次。 这意味着,在同样的计算资源下,SGD 在一个 epoch 内能进行更多次的参数调整,但每次调整的方向可能噪音较大。
  1. 手算验证:在本节的例子中,我们进行了一次反向传播并更新了权重。请尝试不看代码,手动计算(或编写一个小程序验证)更新后的权重 w_AQ_new 是否等于 0.39714。(提示:out_O=0.6903, out_P=0.6803, out_Q=0.6637, target=0.5)
👉 点击查看参考答案

计算步骤如下:

  1. 计算输出层残差 δO=(target−outO)∗outO(1−outO)=(0.5−0.6903)∗0.6903∗(1−0.6903)≈−0.1903∗0.2138≈−0.0407\delta_O = (target - out_O) * out_O(1-out_O) = (0.5-0.6903) * 0.6903*(1-0.6903) ≈ -0.1903 * 0.2138 ≈ -0.0407。
  2. 计算隐藏层 Q 的残差 δQ=outQ(1−outQ)∗(δO∗wQO)=0.6637∗(1−0.6637)∗(−0.0407∗0.9)≈0.2232∗(−0.0366)≈−0.00817\delta_Q = out_Q(1-out_Q) * (\delta_O * w_{QO}) = 0.6637*(1-0.6637) * (-0.0407*0.9) ≈ 0.2232 * (-0.0366) ≈ -0.00817。
  3. 计算 wAQw_{AQ} 的梯度 ∂J∂wAQ=−δQ∗A=−(−0.00817)∗0.35≈0.00286\frac{\partial J}{\partial w_{AQ}} = -\delta_Q * A = -(-0.00817) * 0.35 ≈ 0.00286。
  4. 更新权重 wAQnew=wAQ−η∗∂J∂wAQ=0.4−1∗0.00286=0.39714w_{AQ_new} = w_{AQ} - \eta * \frac{\partial J}{\partial w_{AQ}} = 0.4 - 1 * 0.00286 = 0.39714。 手动计算的结果 0.39714 与代码结果 0.39714 一致(末位差异来自中间步骤的舍入),这验证了反向传播公式与代码实现的一致性。

本章小结

通过这个可以手算的简单神经网络示例,我们完整地走了一遍监督学习的关键流程:前向传播计算输出 -> 计算损失 -> 反向传播计算梯度 -> 更新权重。你不仅看到了理论公式如何落地为具体的数字运算,也理解了 Sigmoid 函数导数表达的精妙之处,它让反向传播的计算变得非常简洁。

更重要的是,我们认识到,对于单个样本的优化只是万里长征的第一步。在实际的深度学习训练中,我们需要用小批次梯度下降(MBGD) 这种高效且稳定的策略,利用海量数据反复迭代,才能让神经网络学习到真正有效的特征表示,最终解决复杂的视觉任务。

📋 行动清单

  • 在纸上或编辑器中,完全脱离参考,推导一遍本节示例中 6 个权重的更新公式。
  • 思考:如果隐藏层或输出层使用的激活函数不是 Sigmoid,而是 ReLU(f(x)=max(0,x)f(x)=max(0, x)),反向传播的残差计算公式应该如何修改?
  • 了解 Geoffrey Hinton 等人在 1986 年发表 Backpropagation 论文的历史背景,理解这一基础算法为何是深度学习复兴的重要基石。

—— 小象教研组