ResNet-18 与 CIFAR-10 实现:参考答案

目录

\[ \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} \]

ResNet-18 与 CIFAR-10 实现:参考答案#

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

说明#

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

  1. 初始卷积计 1 层;四个阶段共有 \(4(2)=8\) 个 BasicBlock,每块残差分支含 2 个卷积,共 16 层;最后线性层计 1 层,因此 \(1+16+1=18\)⁠。直连分支中的投影卷积只在维度变化时帮助两条分支完成相加,经典命名没有把它计入从输入到输出的主要计算层数,所以名称仍为 ResNet-18;统计实现的总参数量时则必须计入投影参数。

  2. 初始卷积和第一阶段输出均为 \(m\times64\times32\times32\)⁠;第二、三、四阶段依次为 \(m\times128\times16\times16\)⁠、\(m\times256\times8\times8\)⁠、\(m\times512\times4\times4\)⁠。全局平均池化得到 \(m\times512\times1\times1\)⁠,展平为 \(m\times512\)⁠,线性层输出 \(m\times10\)⁠。

  3. 64 通道块的两个卷积各有 \(64(64)(3)(3)=36864\) 个参数,两个 BatchNorm 各有 \(2(64)=128\) 个可训练参数,合计 \(2(36864)+2(128)=73984\)⁠。下采样块的残差分支卷积参数量分别为 \(128(64)(9)=73728\)\(128(128)(9)=147456\)⁠,直连分支的投影卷积参数量为 \(128(64)=8192\)⁠;三个 BatchNorm 共有 \(3[2(128)]=768\) 个可训练参数,总计 230144。

  4. \(t=0\) 时余弦为 1,得到 \(\eta_0\)⁠;\(t=T/2\) 时余弦为 0,得到 \((\eta_0+\eta_{\min})/2\)⁠;\(t=T\) 时余弦为 \(-1\)⁠,得到 \(\eta_{\min}\)⁠。普通 CosineAnnealingLR 完成计划的退火过程后不会自动跳回初始学习率;周期性重新提高学习率需要 CosineAnnealingWarmRestarts 或等价的明确设置。

  5. 阶段构建时只有每阶段首块使用指定步幅:

    def make_stage(self, cout, blocks, first_stride):
        strides = [first_stride] + [1] * (blocks - 1)
        layers = []
        for stride in strides:
            layers.append(BasicBlock(self.in_channels, cout, stride))
            self.in_channels = cout
        return nn.Sequential(*layers)
    
    def forward(self, x, return_shapes=False):
        shapes = []
        x = F.relu(self.bn1(self.conv1(x)))
        for stage in (self.layer1, self.layer2, self.layer3, self.layer4):
            x = stage(x); shapes.append(tuple(x.shape))
        x = F.adaptive_avg_pool2d(x, 1).flatten(1)
        out = self.linear(x)
        return (out, shapes) if return_shapes else out
    

    BasicBlock 内部在相加前比较残差分支的 residual.shape 与直连分支的 shortcut.shape

  6. 参考块和自编块必须使用相同的训练或评估模式,并复制卷积、BatchNorm 参数和运行统计量;固定输入与传入该块的梯度后,比较输出、输入梯度和各参数梯度。要构造零残差,可将残差分支卷积权重置零,并确保最后一个 BatchNorm 输出为 0。直连分支无投影时,相加前的结果等于输入;有投影时,相加前的结果等于投影输出。最终 ReLU 函数会截断负数,因此恒等性质应在激活函数之前检查。

  7. 按照从输入到输出的主要计算过程统计含参数层,应得到 18;可以记录各层输出并确认维度符合第 2 题;改变批量大小时只能改变第 0 个维度;每个块相加前的残差分支与直连分支维度必须完全相同;零残差检查应复现第 6 题;手工参数统计与 sum(p.numel()) 必须一致;交叉熵后向传播后,所有梯度都应具有正确维度并且是有限数值。训练模式的前向传播应更新 BatchNorm 运行统计量,评估模式则不更新,而且单个样本的预测不应依赖同一批次中的其他样本。

  8. 普通网络与 ResNet 使用相同的残差分支卷积层数和通道数,直连分支投影带来的参数差异应单独报告。两组使用相同的数据划分、数据增强、随机种子、初始化方法、优化器尝试次数和更新次数,只改变是否使用残差连接。ResNet 通常能提供更直接的梯度路径,但它是否表现更好,应根据本题实际测得的指标判断。报告任务指标及其波动、逐层梯度范数、训练时间、固定批量预测时间、参数量和峰值内存;预测所需计算也会受到分支相加与投影卷积的影响。

  9. 四种学习率调度方法必须使用相同的一组初始学习率和总更新次数;每种调度都在验证集上尝试相同数量的初始学习率,并为每个候选使用相同的训练量。还应根据训练日志核对实际使用的学习率序列。阶梯衰减会分阶段降低学习率,普通余弦调度会在一个周期内平滑降到最小值,warm restarts 会周期性提高学习率。报告多个随机种子下的任务性能、训练时间、固定批量预测时间、参数量和峰值内存;调度方法不改变网络参数量和预测时的计算过程,因此预测时间的少量差异通常来自计时波动。

  10. 窄版减少各阶段的通道数,更深版增加残差块数量;即使参数量接近,两者的串行深度、激活值内存和乘加计算分布仍然不同。所有模型使用相同的数据、随机种子、数据增强和更新次数,并尝试相同数量的超参数设置。报告测试指标的均值与标准差、训练秒数、固定批量预测时间、参数量、乘加量及峰值内存。更深的模型可能更难训练,也可能具有更强的表示能力;更宽的模型可能更适合并行计算。观察到的结果只适用于本题实际使用的硬件、数据和训练量。