查看全课大纲(第 29 / 93 节)
- 1.计算机视觉概述
- 2.计算机视觉应用场景
- 3.困难与挑战
- 4.开源库介绍
- 5.图像显示与存储
- 6.CLAHE
- 7.腐蚀膨胀与开闭运算
- 8.滤波与边缘填充
- 9.各种滤波
- 10.图像金字塔
- 11.傅里叶变换
- 12.短时傅里叶变换与小波变换
- 13.颜色特征与颜色直方图
- 14.几何特征_边缘
- 15.几何特征-角点与斑点
- 16.局部特征-SIFT特征描述子
- 17.局部特征-SURF特征描述子
- 18.局部特征-ORB特征描述子
- 19.其他特征描述子
- 20.传统图像分割技术
- 21.GraphCut
- 22.GrabCut
- 23.人脸检测的传统方法
- 24.行人检测的传统方法HOG+SVM
- 25.行人检测DPM
- 26.人工神经网络的架构
- 27.目标函数与梯度下降
- 28.误差反向传播
- 29.可手算的神经网络
- 30.改进目标函数与克服梯度消散
- 31.神经网络之传统神经网络与深度网络的差异
- 32.各种梯度下降算法
- 33.如何选择合适的梯度下降算法
- 34.避免过适应
- 35.CNN的架构
- 36.CNN的误差反向传播_卷积层
- 37.CNN的误差反向传播_池化层
- 38.分类所用的数据集
- 39.Alexnet详解
- 40.VGG详解
- 41.GoogLeNet_v1
- 42.BatchNormalization
- 43.GoogLeNet_v3v4
- 44.ResNet
- 45.ResNeXt
- 46.CNN设计准则
- 47.图像检索与推土机距离
- 48.基于颜色相似性
- 49.基于形状相似性
- 50.基于纹理相似性
- 51.基于局部特征相似性
- 52.DL的二值哈希编码
- 53.高维空间中检索KDTree
- 54.高维空间中检索LSH
- 55.目标识别任务综述
- 56.RCNN
- 57.SPP-Net
- 58.Fast R-CNN
- 59.Faster R-CNN
- 60.R-CNN系列模型回顾
- 61.R-FCN的设计思路
- 62.R-FCN的实现
- 63.YOLOv1
- 64.YOLOv2_9000
- 65.YOLOv3
- 66.图像分割应用场景与数据集
- 67.上采样的实现方式
- 68.FCN网络架构的由来
- 69.FCN构建细节
- 70.DeepLabv1
- 71.CRF
- 72.DeepLabv2
- 73.DeepLabv3
- 74.特定的医学影像分割任务
- 75.U-Net
- 76.3D U-Net
- 77.V-Net
- 78.DenseNet
- 79.病理切片分析任务及数据集
- 80.病理切片分析
- 81.图说任务及其传统处理方法
- 82.深度语言模型与RNN
- 83.LSTM及其变种
- 84.NIC模型
- 85.SAT模型
- 86.图说的性能指标
- 87.判别模型与生成模型
- 88.KL散度与JS散度
- 89.VAE
- 90.GAN的基本原理
- 91.DCGAN与CGAN
- 92.Wasserstein GAN
- 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)的计算分为两步:
- 计算净输入(net):所有输入乘以其对应权重的和。
- 计算输出(out):将净输入通过激活函数。本例中,三个神经元均使用 Sigmoid 函数,其表达式为 。
Sigmoid 函数有一个非常好的性质:它的导数可以用其输出值 简洁地表示:。这个性质将在反向传播中大大简化我们的计算。
现在,让我们用代码实现一次前向传播,看看在随机初始权重下,网络对于输入 [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。
反向传播:计算权重更新量
误差产生了,接下来就要用误差反向传播算法来指导权重的更新。我们采用平方误差损失函数,对于单个样本,损失为 。我们的目标是求损失函数 对各个权重 的偏导数 ,然后按照梯度下降的方向更新权重:。其中 是学习率,为了简化手算,本例中设 。
反向传播是链式求导法则的经典应用,其核心步骤是从输出层向输入层逐层计算“残差”,再用残差与当前层的输入相乘得到权重的梯度。这里沿用上一节的符号约定:残差中的误差写作(期望 − 实际),因此梯度公式前会带一个负号。
1. 输出层权重的更新
首先更新离误差最近的权重: 和 。
- 计算输出层残差 :
- 计算梯度:,
- 更新权重:, 同理。
2. 隐藏层权重的更新
然后更新隐藏层的权重:, , , 。
- 计算隐藏层残差(例如 ):隐藏层神经元的误差不是直接与目标值比较,而是来自于它连接的后一层(输出层)残差的加权和。 的计算同理,使用 。
- 计算梯度:, , ,
- 更新权重。
下面我们用代码实现这一过程:
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。
从单个样本到海量数据:梯度下降的三种模式
上面的例子只使用了一个样本进行训练。但在实际中,我们拥有成千上万的训练样本。如何利用所有样本来指导网络学习呢?这就引出了梯度下降的三种策略:
批量梯度下降(Batch Gradient Descent, BGD):
- 做法:每次权重更新,都要计算所有训练样本的损失,然后求平均损失对权重的梯度。
- 优点:每次更新都指向整个数据集上的全局最优方向,收敛稳定。
- 缺点:计算开销巨大,尤其当数据集庞大时,一次更新都可能慢得无法接受。
随机梯度下降(Stochastic Gradient Descent, SGD):
- 做法:每次权重更新,只随机使用一个样本来计算梯度并立即更新。
- 优点:更新速度极快,对于海量数据,可能只用一部分样本就达到了不错的解。
- 缺点:单个样本的梯度方向噪音很大,并不是每次都朝向整体最优解,导致收敛过程波动剧烈,甚至可能因异常样本而偏离正确方向。
小批次梯度下降(Mini-batch Gradient Descent, MBGD):
- 做法:这是 BGD 和 SGD 的折中。每次更新,随机选取一个小批次(Mini-batch) 的样本(例如 32、64、128个)来计算平均梯度。
- 优点:兼顾了两者的优势。相比 BGD,计算效率高;相比 SGD,梯度估计更准确,收敛更平稳。
- 现状:如今在深度学习中,当人们提到“随机梯度下降(SGD)”时,通常指的就是这种小批次梯度下降(MBGD)。
其更新公式可以统一表示为:
其中:
- 代表所有待优化的权重参数。
- 是学习率。
- 是批次大小(batch size)。当 等于整个训练集大小时,就是 BGD;当 时,就是 SGD;当 总样本数时,就是 MBGD。
- 是损失函数, 是网络对第 个样本的预测, 是真实标签。
📝 动手练一练
- 概念辨析:假设你有一个包含 10,000 张图片的数据集,使用批量大小为 100 的小批次梯度下降进行训练。请问,遍历一遍全部数据(一个 epoch),权重会被更新多少次?如果使用真正的随机梯度下降(m=1)呢?
👉 点击查看参考答案
- 小批次梯度下降 (batch size=100):权重更新次数 = 总样本数 / 批次大小 = 10,000 / 100 = 100次。
- 随机梯度下降 (m=1):权重更新次数 = 总样本数 = 10,000次。 这意味着,在同样的计算资源下,SGD 在一个 epoch 内能进行更多次的参数调整,但每次调整的方向可能噪音较大。
- 手算验证:在本节的例子中,我们进行了一次反向传播并更新了权重。请尝试不看代码,手动计算(或编写一个小程序验证)更新后的权重
w_AQ_new是否等于0.39714。(提示:out_O=0.6903,out_P=0.6803,out_Q=0.6637,target=0.5)
👉 点击查看参考答案
计算步骤如下:
- 计算输出层残差 。
- 计算隐藏层 Q 的残差 。
- 计算 的梯度 。
- 更新权重 。 手动计算的结果
0.39714与代码结果0.39714一致(末位差异来自中间步骤的舍入),这验证了反向传播公式与代码实现的一致性。
本章小结
通过这个可以手算的简单神经网络示例,我们完整地走了一遍监督学习的关键流程:前向传播计算输出 -> 计算损失 -> 反向传播计算梯度 -> 更新权重。你不仅看到了理论公式如何落地为具体的数字运算,也理解了 Sigmoid 函数导数表达的精妙之处,它让反向传播的计算变得非常简洁。
更重要的是,我们认识到,对于单个样本的优化只是万里长征的第一步。在实际的深度学习训练中,我们需要用小批次梯度下降(MBGD) 这种高效且稳定的策略,利用海量数据反复迭代,才能让神经网络学习到真正有效的特征表示,最终解决复杂的视觉任务。
📋 行动清单
- 在纸上或编辑器中,完全脱离参考,推导一遍本节示例中 6 个权重的更新公式。
- 思考:如果隐藏层或输出层使用的激活函数不是 Sigmoid,而是 ReLU(),反向传播的残差计算公式应该如何修改?
- 了解 Geoffrey Hinton 等人在 1986 年发表 Backpropagation 论文的历史背景,理解这一基础算法为何是深度学习复兴的重要基石。
—— 小象教研组