🧪 MNIST先导课程实验报告
| 课程名称 | 课程5:两层神经网络(隐藏层 + ReLU) | 日期 | |
|---|---|---|---|
| 学生姓名 | 班级/学号 |
🎯 一、实验目的
- 理解两层神经网络的结构:输入层 → 隐藏层(含激活函数) → 输出层。
- 掌握前向传播的计算流程(线性变换 + ReLU + 线性变换)。
- 掌握反向传播的链式法则应用,计算各参数梯度(\(w_1, b_1, w_2, b_2, w_3, w_4, b_3\))。
- 观察学习率、初始参数、激活函数对训练收敛的影响。
- 对比单层网络与两层网络的能力差异,初步认识多层网络的优势与挑战。
🔧 二、实验参数设置
| 参数名 | 符号 | 说明 |
|---|---|---|
| 输入线索 | \(x\) | 标量输入(为简化手算,仍用单特征) |
| 目标值 | \(t\) | 期望输出 |
| 学习率 | \(\eta\) | 控制参数更新步长 |
| 第一层权重 | \(w_1, w_2\) | 连接输入到两个隐藏神经元的权重 |
| 第一层偏置 | \(b_1, b_2\) | 两个隐藏神经元的偏置 |
| 第二层权重 | \(w_3, w_4\) | 连接隐藏层到输出神经元的权重 |
| 第二层偏置 | \(b_3\) | 输出神经元的偏置 |
建议探究的实验组合
- 实验1(基础手算):使用课程文档中给定的参数(\(x=2.0, t=3.0, \eta=0.1\),初始参数见表格),手工计算前向传播与反向传播,验证代码结果。
- 实验2(不同学习率):固定初始参数,尝试 \(\eta = 0.01, 0.1, 0.5\),观察收敛速度与稳定性。
- 实验3(不同初始化):改变第一层权重(如 \(w_1, w_2\) 全为负值或一正一负),观察对隐藏神经元激活状态的影响。
- 实验4(激活函数替换):将 ReLU 替换为 Sigmoid(可自行修改代码),对比收敛行为与梯度大小。
- 实验5(增加神经元):将隐藏层神经元数改为 3(需要调整代码),观察参数数量增加后的训练效果。
- 实验6(梯度消失/爆炸):尝试极大或极小的输入 \(x\)(如 \(x=100\) 或 \(x=0.01\)),观察梯度变化。
📋 三、手工计算表格(两层网络)
直接在计算图中计算
给定参数:
- \(x = 2.0\)
- 初始:\(w_1 = 0.8, b_1 = -0.5, w_2 = -0.3, b_2 = 1.2, w_3 = 1.2, w_4 = 0.7, b_3 = 0.3\)
提示:完成手算后,可以与代码运行的第一轮输出对比,验证反向传播是否正确。
四、代码运行结果
📊 4.1 训练过程中的损失变化曲线

点击放大显示 损失变化曲线
📊 4.2 梯度变化曲线(可选)

点击放大显示 损失变化曲线
📝 4.3 训练过程数据表格(前3轮 + 后3轮,或 Early Stop 时的最后几轮)
实验组合1(基础参数)
- \(x=2.0, t=3.0, \eta=0.1\)
- 初始参数:\(w_1=0.8, b_1=-0.5, w_2=-0.3, b_2=1.2, w_3=1.2, w_4=0.7, b_3=0.3\)
| 轮次 | \(w_1\) | \(b_1\) | \(w_2\) | \(b_2\) | \(w_3\) | \(w_4\) | \(b_3\) | \(y\) | Loss |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 0.8000 | -0.5000 | -0.3000 | 1.2000 | 1.2000 | 0.7000 | 0.3000 | 2.0400 | 0.9216 |
| 1 | 0.8000 | -0.5000 | -0.3000 | 1.2000 | 1.4112 | 0.8152 | 0.4920 | 2.5334 | 0.2177 |
| 2 | 0.8000 | -0.5000 | -0.3000 | 1.2000 | 1.5138 | 0.8712 | 0.5853 | 2.7733 | 0.0514 |
| … | |||||||||
| 5 | 0.8000 | -0.5000 | -0.3000 | 1.2000 | 1.5998 | 0.9180 | 0.6634 | 2.9740 | 0.0007 |
| 6 | 0.8000 | -0.5000 | -0.3000 | 1.2000 | 1.6055 | 0.9212 | 0.6686 | 2.9874 | 0.0002 |
| 7 | 0.8000 | -0.5000 | -0.3000 | 1.2000 | 1.6083 | 0.9227 | 0.6711 | 2.9939 | 0.0000 |
实验组合2(例如学习率 \(\eta=0.5\),其他参数同实验1)
| 轮次 | \(w_1\) | \(b_1\) | \(w_2\) | \(b_2\) | \(w_3\) | \(w_4\) | \(b_3\) | \(y\) | Loss |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 0.8000 | -0.5000 | -0.3000 | 1.2000 | 1.2000 | 0.7000 | 0.3000 | 2.0400 | 0.9216 |
| 1 | 0.8000 | -0.5000 | -0.3000 | 1.2000 | 2.2560 | 1.2760 | 1.2600 | 4.5072 | 2.2717 |
| 2 | -0.2000 | -1.0000 | -1.3000 | 0.7000 | 0.5981 | 0.3717 | -0.2472 | -0.2472 | 10.5443 |
| 3 | -0.2000 | -1.0000 | -1.3000 | 0.7000 | 0.5981 | 0.3717 | 3.0000 | 3.0000 | 0.0000 |
实验组合3(例如学习率 \(\eta=0.9\),其他参数同实验1)
| 轮次 | \(w_1\) | \(b_1\) | \(w_2\) | \(b_2\) | \(w_3\) | \(w_4\) | \(b_3\) | \(y\) | Loss |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 0.8000 | -0.5000 | -0.3000 | 1.2000 | 1.2000 | 0.7000 | 0.3000 | 2.0400 | 0.9216 |
| 1 | 0.8000 | -0.5000 | -0.3000 | 1.2000 | 3.1008 | 1.7368 | 2.0280 | 6.4810 | 12.1171 |
| 2 | -1.0000 | -1.4000 | -2.1000 | 0.3000 | -3.7915 | -2.0226 | -4.2377 | -4.2377 | 52.3847 |
| 17 | -15.4000 | -8.6000 | -16.5000 | -6.9000 | -3.7915 | -2.0226 | 3.2547 | 3.2547 | 0.0648 |
| 18 | -15.4000 | -8.6000 | -16.5000 | -6.9000 | -3.7915 | -2.0226 | 2.7963 | 2.7963 | 0.0415 |
| 19 | -17.2000 | -9.5000 | -18.3000 | -7.8000 | -3.7915 | -2.0226 | 3.1630 | 3.1630 | 0.0266 |
🎉 五、观察与分析
- 学习率的影响:对比不同学习率下的损失下降曲线,你观察到什么?是否存在震荡或发散?学习率多大时开始不稳定?
- 初始参数的影响:初始 \(w_1, w_2\) 的正负对隐藏神经元的激活状态有何影响?是否会出现某个神经元一直“死亡”(ReLU输出恒为0)?这如何影响训练?
- 激活函数的影响:如果将 ReLU 换成 Sigmoid,梯度大小和收敛速度有何变化?是否出现梯度饱和?
- 输入尺度的影响:当 \(x\) 非常大(如100)时,梯度会发生什么变化?是否出现梯度爆炸?如何缓解?
- 与单层网络对比:两层网络比课程2的单层网络在表达能力上有什么优势?从手算结果中能否看出两个神经元如何协同?
- 其他发现(如梯度消失、神经元死亡现象的具体表现):
✨ 六、实验结论
(总结本次实验的核心发现,例如:两层网络通过隐藏层和激活函数能够学习更复杂的映射;学习率需要适中以避免震荡或收敛过慢;ReLU 能缓解梯度消失但可能导致神经元死亡;输入数据尺度对训练稳定性有重要影响等)
💫 七、思考题
- 如果隐藏层两个神经元的 ReLU 都输出为0(即 \(h_1=0, h_2=0\)),那么反向传播中 \(w_1, w_2, w_3, w_4\) 的梯度会是什么?参数还能更新吗?这种现象在实际训练中如何避免?
- 为什么计算 \(w_1\) 的梯度需要用到 \(w_3\)?请用链式法则解释。如果 \(w_3=0\),对 \(w_1\) 的更新有什么影响?
- 在本实验中,我们使用了单特征输入(\(x\) 是标量)。如果要处理 MNIST 的 784 维输入,模型结构应该如何扩展?参数数量会变成多少?
- 什么是梯度消失?在深层网络中,梯度消失会导致什么问题?ReLU 为什么能部分缓解梯度消失?
- 在实际项目中,你会如何选择合适的层数、神经元数量和激活函数?可以从任务复杂度、数据量、计算资源等方面考虑。
八、附录:实验代码
(可附上本次实验的核心代码片段,或说明代码存放位置。建议包含纯 Python 实现和 NumPy 向量化实现,并标注关键计算步骤。)
from pydraw import pydraw
from datetime import datetime
def ReLU(n:int):
return n if n >= 0 else 0
def invReLU(n:int):
return 1 if n >= 0 else 0
def trainmodule5(clue:int, target:int, lr:float, w1:float, w2:float, w3:float, w4:float, b1:float, b2:float, b3:float):
draw = pydraw()
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
draw.xlabel = "Epoch"
draw.ylabel = "Value"
draw.title = "课程 5:两层神经网络"
found = None
desc = f"clue={clue}, t={target}, lr={lr}, w1={w1}, b1={b1}, w2={w2}, b2={b2}, w3={w3}, w4={w4}, b3={b3}"
for epoch in range(20):
z1 = b1+clue*w1
z2 = b2+clue*w2
h1 = ReLU(z1)
h2 = ReLU(z2)
y = h1*w3+h2*w4+b3
# backward propogage
loss = (y-target) ** 2
gy = 2*(y-target)
gb3 = gy
gw3 = gy*h1
gh1 = gy*w3
gw4 = gy*h2
gh2 = gy*w4
gz1 = invReLU(gh1)
gz2 = invReLU(gh2)
gb1 = gz1
gb2 = gz2
gw1 = gz1*clue
gc1 = gz1*w1
gw2 = gz2*clue
gc2 = gz2*w2
gc = gc1+gc2
print(f"| {epoch} | {w1:.4f} | {b1:.4f} | {w2:.4f} | {b2:.4f} | {w3:.4f} | {w4:.4f} | {b3:.4f} | {y:.4f} | {loss:.4f} |")
draw.add(gy,"graident loss of pred")
#renew
b3 = b3-(lr*gb3)
w3 = w3-(lr*gw3)
w4 = w4-(lr*gw4)
b1 = b1-(lr*gb1)
b2 = b2-(lr*gb2)
w1 = w1-(lr*gw1)
w2 = w2-(lr*gw2)
if found is None and loss < 0.0001:
draw.annotate ("Loss<0.0001", xy=(epoch, loss))
found = epoch
break
draw.description = desc + f", Loss<0.0001 on epoch={found}, final loss={loss:.8f}" + ", " + timestamp
draw.export("trainingcurves-5-2.png")
draw.draw()
trainmodule5(2,3,0.9,0.8,-0.3,1.2,0.7,-0.5,1.2,0.3)
备注:实验报告完成后请将电子版或纸质版交给老师,并在课堂上分享你的发现。鼓励尝试更多的参数组合并记录现象!
浙公网安备 33010602011771号