🧪 MNIST先导课程实验报告

课程名称 课程5:两层神经网络(隐藏层 + ReLU) 日期
学生姓名 班级/学号

🎯 一、实验目的

  1. 理解两层神经网络的结构:输入层 → 隐藏层(含激活函数) → 输出层。
  2. 掌握前向传播的计算流程(线性变换 + ReLU + 线性变换)。
  3. 掌握反向传播的链式法则应用,计算各参数梯度(\(w_1, b_1, w_2, b_2, w_3, w_4, b_3\))。
  4. 观察学习率、初始参数、激活函数对训练收敛的影响。
  5. 对比单层网络与两层网络的能力差异,初步认识多层网络的优势与挑战。

🔧 二、实验参数设置

参数名 符号 说明
输入线索 \(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

🎉 五、观察与分析

  1. 学习率的影响:对比不同学习率下的损失下降曲线,你观察到什么?是否存在震荡或发散?学习率多大时开始不稳定?
  2. 初始参数的影响:初始 \(w_1, w_2\) 的正负对隐藏神经元的激活状态有何影响?是否会出现某个神经元一直“死亡”(ReLU输出恒为0)?这如何影响训练?
  3. 激活函数的影响:如果将 ReLU 换成 Sigmoid,梯度大小和收敛速度有何变化?是否出现梯度饱和?
  4. 输入尺度的影响:当 \(x\) 非常大(如100)时,梯度会发生什么变化?是否出现梯度爆炸?如何缓解?
  5. 与单层网络对比:两层网络比课程2的单层网络在表达能力上有什么优势?从手算结果中能否看出两个神经元如何协同?
  6. 其他发现(如梯度消失、神经元死亡现象的具体表现):

✨ 六、实验结论

(总结本次实验的核心发现,例如:两层网络通过隐藏层和激活函数能够学习更复杂的映射;学习率需要适中以避免震荡或收敛过慢;ReLU 能缓解梯度消失但可能导致神经元死亡;输入数据尺度对训练稳定性有重要影响等)


💫 七、思考题

  1. 如果隐藏层两个神经元的 ReLU 都输出为0(即 \(h_1=0, h_2=0\)),那么反向传播中 \(w_1, w_2, w_3, w_4\) 的梯度会是什么?参数还能更新吗?这种现象在实际训练中如何避免?
  2. 为什么计算 \(w_1\) 的梯度需要用到 \(w_3\)?请用链式法则解释。如果 \(w_3=0\),对 \(w_1\) 的更新有什么影响?
  3. 在本实验中,我们使用了单特征输入(\(x\) 是标量)。如果要处理 MNIST 的 784 维输入,模型结构应该如何扩展?参数数量会变成多少?
  4. 什么是梯度消失?在深层网络中,梯度消失会导致什么问题?ReLU 为什么能部分缓解梯度消失?
  5. 在实际项目中,你会如何选择合适的层数、神经元数量和激活函数?可以从任务复杂度、数据量、计算资源等方面考虑。

八、附录:实验代码

(可附上本次实验的核心代码片段,或说明代码存放位置。建议包含纯 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)

备注:实验报告完成后请将电子版或纸质版交给老师,并在课堂上分享你的发现。鼓励尝试更多的参数组合并记录现象!