LeNet 风格网络代码实现:参考答案

目录

\[ \begin{align}\begin{aligned}\newcommand{\ba}{\boldsymbol{a}} \newcommand{\bb}{\boldsymbol{b}} \newcommand{\be}{\boldsymbol{e}} \newcommand{\bq}{\boldsymbol{q}} \newcommand{\bk}{\boldsymbol{k}} \newcommand{\bw}{\boldsymbol{w}} \newcommand{\bx}{\boldsymbol{x}} \newcommand{\by}{\boldsymbol{y}} \newcommand{\bz}{\boldsymbol{z}} \newcommand{\bd}{\boldsymbol{d}} \newcommand{\bv}{\boldsymbol{v}} \newcommand{\bs}{\boldsymbol{s}}\\\newcommand{\btheta}{\boldsymbol{\theta}} \newcommand{\bbeta}{\boldsymbol{\beta}} \newcommand{\bgamma}{\boldsymbol{\gamma}} \newcommand{\bsigma}{\boldsymbol{\sigma}} \newcommand{\md}{\mbox{d}} \newcommand{\bmu}{\boldsymbol{\mu}} \newcommand{\bone}{\boldsymbol{1}} \newcommand{\bzero}{\boldsymbol{0}} \newcommand{\bepsilon}{\boldsymbol{\epsilon}} \newcommand{\bphi}{\boldsymbol{\phi}} \newcommand{\bh}{\boldsymbol{h}} \newcommand{\bc}{\boldsymbol{c}} \newcommand{\br}{\boldsymbol{r}} \newcommand{\bQ}{\boldsymbol{Q}} \newcommand{\bK}{\boldsymbol{K}} \newcommand{\bV}{\boldsymbol{V}} \newcommand{\bSigma}{\boldsymbol{\Sigma}} \newcommand{\bg}{\boldsymbol{g}} \newcommand{\bxi}{\boldsymbol{\xi}} \newcommand{\bvarepsilon}{\boldsymbol{\varepsilon}} \newcommand{\bdelta}{\boldsymbol{\delta}} \newcommand{\bq}{\boldsymbol{q}} \newcommand{\bk}{\boldsymbol{k}} \newcommand{\bJ}{\boldsymbol{J}} \newcommand{\bp}{\boldsymbol{p}} \newcommand{\bi}{\boldsymbol{i}} \newcommand{\bo}{\boldsymbol{o}} \newcommand{\bE}{\boldsymbol{E}} \newcommand{\bH}{\boldsymbol{H}} \newcommand{\bL}{\boldsymbol{L}} \newcommand{\bu}{\boldsymbol{u}} \newcommand{\bLambda}{\boldsymbol{\Lambda}} \newcommand{\trans}{^{\rm\scriptsize T}} \newcommand{\var}{\mathrm{var}}\\\newcommand{\bA}{\boldsymbol{A}} \newcommand{\bB}{\boldsymbol{B}} \newcommand{\bC}{\boldsymbol{C}} \newcommand{\bD}{\boldsymbol{D}} \newcommand{\bG}{\boldsymbol{G}} \newcommand{\bI}{\boldsymbol{I}} \newcommand{\bM}{\boldsymbol{M}} \newcommand{\bP}{\boldsymbol{P}} \newcommand{\bS}{\boldsymbol{S}} \newcommand{\bU}{\boldsymbol{U}} \newcommand{\bW}{\boldsymbol{W}} \newcommand{\bX}{\boldsymbol{X}} \newcommand{\bY}{\boldsymbol{Y}} \newcommand{\bZ}{\boldsymbol{Z}} \newcommand{\cotp}{\textcolor[RGB]{48,209,88}{TP}} \newcommand{\cotn}{\textcolor[RGB]{100,210,255}{TN}} \newcommand{\cofp}{\textcolor[RGB]{94,92,230}{FP}} \newcommand{\cofn}{\textcolor[RGB]{191,90,242}{FN}}\\\newcommand{\numcotp}{\textcolor[RGB]{48,209,88}{50}} \newcommand{\numcotn}{\textcolor[RGB]{100,210,255}{30}} \newcommand{\numcofp}{\textcolor[RGB]{94,92,230}{10}} \newcommand{\numcofn}{\textcolor[RGB]{191,90,242}{10}} \DeclareMathOperator*{\argmin}{arg\,min}\end{aligned}\end{align} \]

LeNet 风格网络代码实现:参考答案#

返回正文练习 · 返回答案索引

说明#

以下答案与正文 10 道题逐题对应。推导题给出主要步骤;编程和实验题给出参考程序和检查方法,并说明结果适用于哪些条件。

  1. 当一个批量中有 \(m\) 个样本时,各层的数据维度依次为 \(m\times1\times32\times32\rightarrow m\times6\times28\times28\rightarrow m\times6\times14\times14\rightarrow m\times16\times10\times10\rightarrow m\times16\times5\times5\rightarrow m\times400\rightarrow m\times120\rightarrow m\times84\rightarrow m\times10\)⁠。参数量分别为 \(6(25+1)=156\)⁠、\(16[6(25)+1]=2416\)⁠、\(120(400+1)=48120\)⁠、\(84(120+1)=10164\)⁠、\(10(84+1)=850\)⁠,合计 61706;池化层没有可训练参数。经典 LeNet-5 使用平均型下采样和 tanh 函数,并用 120 个 \(5\times5\) 核得到 \(m\times120\times1\times1\)⁠;本节教学实现改用最大池化、ReLU 函数和 Linear(400, 120)所以应称为 LeNet 风格网络。

  2. 指标 \(c,u,v\) 在两个输入中重复,表示对输入通道与卷积核的两个空间轴求和;\(m,q\) 保留为输出指标,所以结果维度为 \(m\times d_C^{[l]}\)⁠。题中区域和卷积核的四个元素乘积之和为 4,再加偏置 2,唯一输出为 6。对每个空间位置执行同一运算即可组成卷积特征图。

  3. 填充 0 后边界窗口包含 \([-2,0]\)⁠,最大值变为 0,人工填充值错误地压过真实负响应。填充 \(-\infty\) 后最大值为 \(\max(-2,-\infty)=-2\)⁠,保持了有效输入的最大值。因此最大池化若允许填充,应使用不会成为最大值的 \(-\infty\)⁠;整数类型则需选择相应最小值或显式掩码。

  4. \(32\) 出发,两次无填充 \(5\times5\) 卷积和两次 \(2\times2\) 池化得到 \(32\rightarrow28\rightarrow14\rightarrow10\rightarrow5\)⁠,故展平为 \(16(5)(5)=400\)⁠。若输入为 28,则得到 \(28\rightarrow24\rightarrow12\rightarrow8\rightarrow4\)⁠,展平只有 \(16(4)(4)=256\)⁠;原线性层期望 400 个输入,矩阵内维不相等而报错。虚拟前向可避免手工写死该数值。

  5. 完整模型应让手写层像标准模块一样注册参数,并由虚拟输入计算展平维度:

    from copy import deepcopy
    
    class LeNetLike(nn.Module):
        def __init__(self, input_shape=(1, 32, 32)):
            super().__init__()
            self.features = nn.Sequential(MyConv2D(1, 6, 5), nn.ReLU(),
                                          MyMaxPool2D(2), MyConv2D(6, 16, 5),
                                          nn.ReLU(), MyMaxPool2D(2))
            with torch.no_grad():
                flat = self.features(torch.zeros(1, *input_shape)).flatten(1).shape[1]
            self.classifier = nn.Sequential(nn.Linear(flat, 120), nn.ReLU(),
                                            nn.Linear(120, 84), nn.ReLU(), nn.Linear(84, 10))
        def forward(self, x):
            parameter = next(self.parameters())
            if x.device != parameter.device or x.dtype != parameter.dtype:
                raise ValueError("输入与模型必须使用相同的设备和数据类型")
            return self.classifier(self.features(x).flatten(1))
    
    def evaluate(model, loader, criterion, device):
        model.eval()
        loss_sum = 0.0
        correct = 0
        sample_count = 0
        with torch.no_grad():
            for data, target in loader:
                data, target = data.to(device), target.to(device)
                output = model(data)
                loss_sum += criterion(output, target).item() * data.size(0)
                correct += (output.argmax(1) == target).sum().item()
                sample_count += data.size(0)
        return loss_sum / sample_count, correct / sample_count
    
    def fit(model, train_loader, val_loader, criterion,
            optimizer, epochs, device):
        model.to(device)
        best_state = deepcopy(model.state_dict())
        best_val_loss = float("inf")
        for _ in range(epochs):
            model.train()
            for data, target in train_loader:
                data, target = data.to(device), target.to(device)
                optimizer.zero_grad()
                loss = criterion(model(data), target)
                loss.backward()
                optimizer.step()
            val_loss, _ = evaluate(model, val_loader, criterion, device)
            if val_loss < best_val_loss:
                best_val_loss = val_loss
                best_state = deepcopy(model.state_dict())
        model.load_state_dict(best_state)
        return model
    

    fit 只根据验证损失保存最佳参数,训练结束后再加载该参数;测试集应在模型和训练方案确定以后使用。虚拟输入自动得到 flat因而输入尺寸改变时不必手工猜测第一个线性层的输入数。

  6. 按照 手算结果与可信实现对照 的原则,复制相同的 \(\bW^{[l]}\)⁠、\(\bb^{[l]}\)⁠、\(\bA^{[l-1]}\) 和池化参数,手写层与框架层输出应在允许误差内一致。卷积梯度对照使用标量 \(\sum(\bZ^{[l]}\odot\bG)\) 作为损失,其中 \(\bG\) 是固定的 \(\mathrm d\bZ^{[l]}\)⁠;分别比较 \(\mathrm d\bA^{[l-1]}\)⁠、\(\mathrm d\bW^{[l]}\)\(\mathrm d\bb^{[l]}\)⁠。dict(model.named_parameters()) 应包含手写层权重与偏置,优化器中的参数元素总数应等于模型可训练参数总数,否则即使能够自动求导,优化器也不会更新遗漏的参数。

  7. 用钩子检查正文维度链和总参数 61706;批量 \(1\)\(8\) 只改变第 0 轴;全负窗口复现第 3 题;模型与输入移动到同一设备后输出设备一致;一次交叉熵后向传播后,所有参数梯度都应与相应参数维度相同且为有限数;保存 state_dict扰动参数并重新加载后,固定输入的预测应恢复为保存前结果。没有 CUDA 时应跳过 GPU 测试,而不是将其判为失败。

  8. 三种实现先复制相同的权重,并检查前向结果与梯度是否一致,再使用相同的数据划分、随机种子、参数更新次数和硬件训练。循环版通常最慢,unfold 会增加保存滑动窗口所需的内存,框架卷积通常会调用经过优化的计算程序。结果表包括测试指标及其波动、训练时间、程序预先运行后测得的固定批量预测时间、61706 个参数和峰值内存。如果任务指标不同,应先检查三种实现是否执行了相同的计算,不能直接认为差异是运行速度造成的。

  9. 三个模型使用相同的归一化方法、数据划分、随机种子和参数更新次数;在验证集上尝试超参数的次数相同,并让每组超参数使用相同的训练量。逻辑回归的模型容量最小,MLP 不直接利用图像的局部空间关系,LeNet 风格网络则利用卷积的局部连接与权重共享;通常后者更适合图像任务,但仍应根据本题的实际结果判断。报告测试准确率或损失、训练时间、固定批量预测时间、实际参数量和峰值内存;参数量接近并不表示乘加次数或预测时间也接近。

  10. 应比较所有“网络宽度×学习率”组合,并检查学习率的影响是否会随网络宽度而改变。每个组合使用相同的数据划分、随机种子和批次顺序。加宽通常会增加参数量、训练时间和内存,也可能需要不同的学习率;过大的学习率可能使某些宽度下的训练不稳定。报告任务指标的均值与标准差、训练秒数、固定批量预测时间、参数量及峰值内存;各宽度应尝试相同数量的学习率,不能在看到结果后给某个宽度增加更多尝试。