1. 前言:从手写网络到 nn 模块

在前几篇文章中,我们了解了 Tensor 的基本操作,也接触了自动求导机制 autograd。现在假设我们要搭建一个简单的两层全连接网络,如果只使用基础 Tensor 和 autograd,代码大概是这样的:

import torch

# 手写权重与偏置
w1 = torch.randn(784, 256, requires_grad=True)
b1 = torch.zeros(256, requires_grad=True)
w2 = torch.randn(256, 10, requires_grad=True)
b2 = torch.zeros(10, requires_grad=True)

def manual_forward(x):
    x = x @ w1 + b1      # 第一层线性变换
    x = x.clamp(min=0)   # 手动实现 ReLU
    x = x @ w2 + b2      # 第二层线性变换
    return x

x = torch.randn(32, 784)
out = manual_forward(x)
print(out.shape)  # torch.Size([32, 10])

上面的代码虽然能跑,但问题非常明显:

  1. 每个权重都要手动定义,网络一深,代码量爆炸;
  2. 前向传播要手动实现矩阵运算,容易写错形状;
  3. 参数不便于统一管理,保存、加载、设备迁移都很麻烦;
  4. 缺少现成的层与训练组件,激活函数、损失函数、优化器都要自己造轮子。

PyTorch 为我们提供了 torch.nn 模块——一系列神经网络层的积木。同样的网络,用 nn 可以写成:

import torch.nn as nn

model = nn.Sequential(
    nn.Linear(784, 256),
    nn.ReLU(),
    nn.Linear(256, 10),
)

你可以像搭乐高一样,把「全连接层」「卷积层」「激活函数」「池化层」等积木拼装起来,构建出复杂的神经网络。nn 模块的价值不仅在于“省代码”,更在于它提供了一套统一、规范、可复用的建模方式:参数自动管理、模块任意嵌套、设备与训练模式一键切换。

本文将通过「积木搭建」的比喻,系统地讲解 nn 模块的核心组成:

nn.Module 模型基类

各类网络层

容器类

Linear / Conv2d / Pool / BatchNorm

Sequential / ModuleList / ModuleDict

激活函数

完整网络

损失函数

训练循环

优化器

2. nn.Module:所有神经网络的基类

torch.nn.Module 是 PyTorch 中所有神经网络模块的基类。任何自定义模型都应该继承它。它的核心作用有三个:

  1. 管理参数:自动追踪模块中所有可学习的参数(权重和偏置)。
  2. 组织前向传播:通过实现 forward 方法定义数据如何流经网络。
  3. 支持嵌套与注册:子模块可以层层嵌套,模块类型会被自动识别,方便统一管理。

一个最简单的自定义网络如下:

import torch
import torch.nn as nn

class SimpleNet(nn.Module):
    def __init__(self, input_size, hidden_size, output_size):
        super().__init__()
        # 在 __init__ 中定义“积木”
        self.fc1 = nn.Linear(input_size, hidden_size)
        self.relu = nn.ReLU()
        self.fc2 = nn.Linear(hidden_size, output_size)

    def forward(self, x):
        # 在 forward 中“拼装”积木
        x = self.fc1(x)
        x = self.relu(x)
        x = self.fc2(x)
        return x

net = SimpleNet(784, 256, 10)
print(net)

输出会展示网络的结构和每一层的参数形状,类似下面这样:

SimpleNet(
  (fc1): Linear(in_features=784, out_features=256, bias=True)
  (relu): ReLU()
  (fc2): Linear(in_features=256, out_features=10, bias=True)
)

这里有三个必须牢记的规则:

  • 必须在 __init__ 中先调用 super().__init__(),否则 nn.Module 的初始化机制不会生效,参数无法被正确注册。
  • 子模块必须赋值给 self 的属性(如 self.fc1),这样 nn.Module 才能通过 __setattr__ 机制自动发现并登记它们。
  • 不要直接调用 forward 方法,而是通过 net(x) 调用。nn.Module 内部实现了 __call__,在调用 forward 前后会执行注册的钩子(hook),直接调用 net.forward(x) 会绕过这些机制。

2.1 查看模型的参数

模型的所有可学习参数都保存在 parameters() 中,可以配合优化器使用:

for name, param in net.named_parameters():
    print(f"{name:20s} {param.shape}")

输出大致如下:

fc1.weight           torch.Size([256, 784])
fc1.bias              torch.Size([256])
fc2.weight            torch.Size([10, 256])
fc2.bias              torch.Size([10])

此外还可以统计参数量:

total_params = sum(p.numel() for p in net.parameters())
print(f"总参数量: {total_params}")  # 784*256+256 + 256*10+10

除了 parameters(),还常用 named_parameters() 拿到带名字的参数,用 children() 遍历直接子模块,用 modules() 遍历所有层级的子模块(包含自身)。

2.2 train() / eval() 切换模式

模型有两种模式,对 BatchNormDropout 等层影响很大:

net.train()  # 训练模式:Dropout 生效,BatchNorm 用当前 batch 的统计量
net.eval()   # 评估模式:Dropout 关闭,BatchNorm 用全局累计的统计量

在训练前调用 train(),在验证或推理前调用 eval(),这是保证结果正确的关键习惯。

2.3 参数迁移到 GPU

把模型和输入数据放到同一个设备上,才能正常计算:

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
net.to(device)          # 原位迁移所有参数
x = torch.randn(3, 784).to(device)
out = net(x)
print(out.device)       # cuda:0 或 cpu

3. 常用网络层:认识你的积木

nn 模块内置了大量现成的网络层,下面介绍最常用的几种。在学习每一个层时,请重点关注三点:输入输出形状核心参数适用场景

3.1 全连接层 nn.Linear

全连接层(也叫线性层)是最基础的积木,本质是对输入做线性变换:y = xW^T + b,也可以简写为 y = Wx + b

import torch
import torch.nn as nn

# in_features=输入维度, out_features=输出维度
fc = nn.Linear(in_features=10, out_features=5)
# 模拟一个 batch 大小为 3 的输入
x = torch.randn(3, 10)
y = fc(x)
print(y.shape)  # torch.Size([3, 5])

nn.Linear 内部自动创建了权重 weight(形状 [out_features, in_features],即 [5, 10])和偏置 bias(形状 [out_features],即 [5]),可以通过 fc.weightfc.bias 访问和修改:

print(fc.weight.shape)  # torch.Size([5, 10])
print(fc.bias.shape)    # torch.Size([5])

# 自定义初始化
nn.init.xavier_uniform_(fc.weight)
nn.init.zeros_(fc.bias)

注意权重形状是 [输出维度, 输入维度],不要记反。全连接层要求输入至少是二维 (batch, in_features),更多维的输入需要先用 Flatten 展平。

3.2 卷积层 nn.Conv2d

卷积层是卷积神经网络(CNN)的核心积木,用于提取图像的空间特征。它通过一个可学习的卷积核在图像上滑动,捕捉局部模式(如边缘、纹理)。

import torch
import torch.nn as nn

# in_channels=输入通道数, out_channels=输出通道数, kernel_size=卷积核大小
conv = nn.Conv2d(in_channels=3, out_channels=16, kernel_size=3, stride=1, padding=1)
# 模拟一张 RGB 图片:batch=1, 通道=3, 高=32, 宽=32
img = torch.randn(1, 3, 32, 32)
out = conv(img)
print(out.shape)  # torch.Size([1, 16, 32, 32])

参数说明:

  • stride:卷积核滑动的步长,默认 1。步长越大,输出尺寸越小。
  • padding:边缘填充的像素数。padding=1 配合 kernel_size=3 可以在 stride=1 时保持输出尺寸不变,这叫 “same padding”。
  • bias:是否使用偏置,默认为 True
  • dilation:膨胀卷积的间隔,默认 1,常用于扩大感受野而不增加参数。

输出尺寸的计算公式为:

H_out = (H_in + 2 * padding - dilation * (kernel_size - 1) - 1) / stride + 1
W_out = (W_in + 2 * padding - dilation * (kernel_size - 1) - 1) / stride + 1

举例:输入 32×32,kernel_size=3, padding=1, stride=1,代入得 (32 + 2 - 3 - 0)/1 + 1 = 32,所以输出仍是 32×32;若把 stride 改为 2,则输出变为 (32 + 2 - 3 - 0)/2 + 1 = 16(结果向下取整前的计算为 15.5 + 1,实际由 floor 规则得到 16)。

3.3 池化层 nn.MaxPool2d

池化层用于缩小特征图的空间尺寸,保留主要特征的同时减少计算量,并增强网络对小幅平移的不敏感性。

import torch
import torch.nn as nn

pool = nn.MaxPool2d(kernel_size=2, stride=2)
# 最大池化后尺寸减半
x = torch.randn(1, 16, 32, 32)
out = pool(x)
print(out.shape)  # torch.Size([1, 16, 16, 16])

最大池化在每个窗口内取最大值,相当于保留最强的响应。除了最大池化,nn.AvgPool2d(平均池化)取窗口内平均值,常用于网络末端的全局池化;nn.AdaptiveAvgPool2d 则可以把任意尺寸的输入直接映射到指定输出尺寸,在分类网络末尾格外常用:

gap = nn.AdaptiveAvgPool2d((1, 1))  # 全局平均池化
out = gap(torch.randn(2, 64, 7, 7))
print(out.shape)  # torch.Size([2, 64, 1, 1])

池化层没有可学习参数,只在空间维度下采样。

3.4 归一化层 nn.BatchNorm2d

批量归一化层通过对每个通道做标准化(减均值、除标准差,再做可学习的缩放和平移),把中间层的输出分布控制在较稳定的范围,从而加速训练、稳定梯度、允许使用更大学习率,常插在卷积层和激活函数之间。

import torch
import torch.nn as nn

bn = nn.BatchNorm2d(num_features=16)
x = torch.randn(4, 16, 32, 32)  # batch=4
out = bn(x)
print(out.shape)  # torch.Size([4, 16, 32, 32])

注意:BatchNorm 在训练和测试阶段的行为不同——

  • 训练时:使用当前 batch 的均值和方差进行归一化,同时更新全局统计量(running mean / running var)。
  • 测试时:使用训练阶段累积的全局统计量进行归一化,保证推理结果稳定。

这种差异由模型的 train() / eval() 模式自动切换,所以一定不要在验证和推理时忘了调用 eval()。另外,当 batch size 很小(如 1 或 2)时,batch 统计量波动很大,BatchNorm 效果会变差,这时可以考虑 GroupNormLayerNorm,其中 LayerNorm 在 Transformer 结构中非常常见。

4. 激活函数:给网络注入非线性

如果网络中只有线性层,那么无论堆叠多少层,整体仍然等价于一个线性变换,无法拟合复杂的非线性函数。激活函数就是引入非线性的关键积木。

import torch
import torch.nn as nn

# 常用激活函数
relu = nn.ReLU(inplace=False)      # 最常用,计算简单
sigmoid = nn.Sigmoid()             # 输出 0~1,适合二分类
tanh = nn.Tanh()                   # 输出 -1~1
leaky_relu = nn.LeakyReLU(0.01)    # 解决 ReLU 的“神经元死亡”问题
softmax = nn.Softmax(dim=1)        # 多分类输出概率分布

x = torch.tensor([-1.0, 0.0, 2.0])
print(relu(x))       # tensor([0., 0., 2.])
print(sigmoid(x))    # 所有值在 0~1 之间

各个激活函数的公式与特点:

  • ReLUf(x) = max(0, x)。计算简单、梯度不会饱和,是现代网络的默认选择;缺点是输入为负时梯度为 0,可能出现“神经元死亡”。
  • LeakyReLUf(x) = x if x > 0 else 0.01x。给负半轴一个微小斜率,缓解 ReLU 的死亡问题。
  • GELUf(x) ≈ xΦ(x)。在 Transformer 中大量使用,相比 ReLU 更平滑,效果通常略好。
  • Sigmoidσ(x) = 1 / (1 + e^(-x))。输出压缩到 0~1,可用于二分类概率,但存在两端梯度饱和、输出不以 0 为中心的问题。
  • Tanhtanh(x) = 2σ(2x) - 1。输出范围 -1~1,以 0 为中心,但仍会梯度饱和。
  • Softmax:将一组 logits 映射为概率分布,所有输出之和为 1,用于多分类。

选择建议:

  • 隐藏层默认使用 ReLU 及其变体(LeakyReLUGELU)。
  • 二分类输出层用 Sigmoid
  • 多分类输出层用 Softmax(实际训练中常配合 CrossEntropyLoss,它内部已包含 Softmax,因此模型最后一层不需要再单独加)。

如果你更倾向于函数式写法,也可以用 torch.relutorch.sigmoid 等函数,效果与 nn.ReLU() 等模块一致;区别在于无参数激活函数写成模块后可以在 Sequential 中直接串联。

5. 损失函数:衡量预测与真实值的差距

损失函数告诉网络「你错得有多离谱」,是优化器更新参数的依据。不同任务需要选择不同的损失函数。

import torch
import torch.nn as nn

# 回归任务常用均方误差
mse_loss = nn.MSELoss()
pred = torch.tensor([2.5, 0.0, 2.1])
target = torch.tensor([3.0, -0.5, 2.0])
loss = mse_loss(pred, target)
print(loss)

# 多分类任务常用交叉熵
ce_loss = nn.CrossEntropyLoss()
# logits: batch=2, 类别数=3(注意:不需要先做 softmax)
logits = torch.randn(2, 3)
labels = torch.tensor([0, 2])
loss = ce_loss(logits, labels)
print(loss)

常见损失函数一览:

  • nn.MSELoss:均方误差,loss = mean((pred - target)^2)。适合回归任务,对离群点较敏感。
  • nn.L1Loss:平均绝对误差,loss = mean(|pred - target|)。对离群点更鲁棒。
  • nn.CrossEntropyLoss:交叉熵损失,多分类任务的标准选择。输入必须是未经过 Softmax 的 logits,标签是类别索引(LongTensor)。
  • nn.BCELoss / nn.BCEWithLogitsLoss:二分类交叉熵。前者要求输入已经过 Sigmoid,后者内部自带 Sigmoid,数值更稳定,推荐后者。

关键点:CrossEntropyLoss 内部已经包含了 Softmax + 负对数似然,所以模型的最后一层不要再加 Softmax,直接输出原始分数(logits)即可。如果在输出层先做了 Softmax,再传入 CrossEntropyLoss,相当于做了两次 Softmax,不仅梯度会失真,训练效果也会变差。

另外注意标签类型:分类任务中 labels 必须是 torch.long,如果误用浮点标签会直接报错。

6. 优化器:让积木“活”起来

有了模型和损失函数,还需要优化器来根据梯度更新参数。损失函数计算出的梯度,需要由优化器按照特定算法应用到参数上。PyTorch 将优化算法封装在 torch.optim 模块中。

import torch
import torch.nn as nn
import torch.optim as optim

model = nn.Linear(10, 1)
loss_fn = nn.MSELoss()

# 传入模型参数和学习率
optimizer = optim.SGD(model.parameters(), lr=0.01)
# 也常用 Adam,收敛更快
# optimizer = optim.Adam(model.parameters(), lr=0.001)

# 模拟一个训练步骤
x = torch.randn(4, 10)
target = torch.randn(4, 1)

optimizer.zero_grad()   # 1. 清空上一步的梯度
pred = model(x)         # 2. 前向传播
loss = loss_fn(pred, target)
loss.backward()         # 3. 反向传播,计算梯度
optimizer.step()        # 4. 更新参数

print(f"loss: {loss.item():.4f}")

四个步骤缺一不可,尤其注意 zero_grad():PyTorch 的梯度默认是累加的,如果不手动清零,多次反向传播的梯度会越积越大,参数更新会出错。

常用优化器对比:

  • SGD:随机梯度下降,简单稳定,配合动量(momentum)后效果更好,但需要仔细调学习率。
  • Adam:自适应学习率,收敛快,是大多数任务的默认选择,适合快速验证模型。
  • AdamW:Adam 的改进版,把权重衰减与自适应学习率解耦,训练更稳定,是 Transformer 训练的标配。
  • RMSprop:适合循环神经网络等非平稳目标,现在使用较少。

实际选择时可以记住两句话:快速跑通用 Adam,追求最终效果或训练大模型用 AdamW 或带动量的 SGD

7. 容器类:Sequential 与 ModuleList

当网络结构是简单的「一层接一层」时,可以用容器类让代码更简洁;当结构出现分支、循环或动态变化时,则需要更灵活的容器。PyTorch 提供了 SequentialModuleListModuleDict 三种主要容器。

7.1 nn.Sequential:流水线式组装

import torch
import torch.nn as nn

model = nn.Sequential(
    nn.Linear(784, 256),
    nn.ReLU(),
    nn.Linear(256, 128),
    nn.ReLU(),
    nn.Linear(128, 10),
)

x = torch.randn(2, 784)
out = model(x)
print(out.shape)  # torch.Size([2, 10])

Sequential按顺序自动传递数据,非常适合构建简单的线性堆叠网络。它还可以用 OrderedDict 给每一层命名:

from collections import OrderedDict

model = nn.Sequential(OrderedDict([
    ("fc1", nn.Linear(784, 256)),
    ("relu1", nn.ReLU()),
    ("fc2", nn.Linear(256, 10)),
]))
print(model)

这样打印模型结构时会有更清晰的层名。Sequential 的局限是:每一层的输出只能直接作为下一层的输入,无法表达跳跃连接、多个输入或多个输出。

7.2 nn.ModuleList:可迭代的模块列表

ModuleList 本身不定义前向传播,只是一个存放模块的容器,适合动态管理多个层。

import torch
import torch.nn as nn

class MultiHeadNet(nn.Module):
    def __init__(self):
        super().__init__()
        # 存放多个分支的全连接层
        self.heads = nn.ModuleList([
            nn.Linear(128, 10) for _ in range(5)
        ])

    def forward(self, x):
        outputs = []
        for head in self.heads:
            outputs.append(head(x))
        return torch.stack(outputs, dim=1)

net = MultiHeadNet()
x = torch.randn(2, 128)
print(net(x).shape)  # torch.Size([2, 5, 10])

ModuleList 里的每个模块都有自己的独立参数,适合“多个并列分支”或“层数在循环中动态生成”的场景。

注意:不要用 Python 原生列表 [] 来存放网络层,否则这些层的参数不会被 nn.Module 自动追踪。务必使用 ModuleListModuleDict

7.3 nn.ModuleDict:可按键访问的模块字典

当你希望通过字符串名字动态访问子模块时,可以使用 ModuleDict

import torch
import torch.nn as nn

class SwitchNet(nn.Module):
    def __init__(self):
        super().__init__()
        self.branches = nn.ModuleDict({
            "linear": nn.Linear(64, 10),
            "mlp": nn.Sequential(nn.Linear(64, 32), nn.ReLU(), nn.Linear(32, 10)),
        })

    def forward(self, x, branch_name):
        return self.branches[branch_name](x)

net = SwitchNet()
x = torch.randn(2, 64)
print(net(x, "linear").shape)  # torch.Size([2, 10])
print(net(x, "mlp").shape)     # torch.Size([2, 10])

记忆这三者最直接的方式:

  • Sequential顺序执行,数据自动流动。
  • ModuleList像列表一样迭代,但前向传播要自己写。
  • ModuleDict像字典一样按键访问,前向传播也要自己写。

8. 实战:用积木搭建一个 CNN

下面把前面学到的积木拼装成一个完整的卷积神经网络,用来解决经典的 MNIST 手写数字分类(10 类)。MNIST 是 28×28 的单通道灰度图,标签为 0~9 的数字。

import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader

class CNN(nn.Module):
    def __init__(self, num_classes=10):
        super().__init__()
        # 特征提取部分
        self.features = nn.Sequential(
            nn.Conv2d(1, 32, kernel_size=3, padding=1),  # 28x28 -> 28x28
            nn.ReLU(),
            nn.MaxPool2d(2),                              # 28x28 -> 14x14
            nn.Conv2d(32, 64, kernel_size=3, padding=1),  # 14x14 -> 14x14
            nn.ReLU(),
            nn.MaxPool2d(2),                              # 14x14 -> 7x7
        )
        # 分类部分
        self.classifier = nn.Sequential(
            nn.Flatten(),                                 # 展平为 64*7*7
            nn.Linear(64 * 7 * 7, 128),
            nn.ReLU(),
            nn.Dropout(0.5),                              # 随机失活,防止过拟合
            nn.Linear(128, num_classes),
        )

    def forward(self, x):
        x = self.features(x)
        x = self.classifier(x)
        return x

model = CNN()
print(model)

# 验证前向传播
dummy_input = torch.randn(2, 1, 28, 28)
print(model(dummy_input).shape)  # torch.Size([2, 10])

下面逐步拆解这个网络:

  1. 特征提取 features:两个卷积块逐步把 1 通道的 28×28 图变成 64 通道的 7×7 特征图。padding=1 让卷积前后尺寸不变,MaxPool2d(2) 让尺寸减半。
  2. 展平 Flatten:把 (batch, 64, 7, 7) 展平成 (batch, 3136),也就是 64 * 7 * 7,为全连接层做准备。
  3. 分类头 classifier:两层全连接 + Dropout,最终输出 10 个 logits。
  4. Dropout:训练时以 0.5 的概率随机丢弃神经元,缓解过拟合。评估模式下会自动失效。

这个网络中包含了本文介绍的所有积木类型:卷积层、激活函数、池化层、全连接层、Dropout,以及 Sequential 容器。完整的训练循环如下:

def train_one_epoch(model, loader, optimizer, loss_fn):
    model.train()  # 切换到训练模式
    total_loss = 0
    correct = 0
    for images, labels in loader:
        optimizer.zero_grad()
        outputs = model(images)
        loss = loss_fn(outputs, labels)
        loss.backward()
        optimizer.step()
        total_loss += loss.item() * images.size(0)
        correct += (outputs.argmax(dim=1) == labels).sum().item()
    return total_loss / len(loader.dataset), correct / len(loader.dataset)

def evaluate(model, loader, loss_fn):
    model.eval()  # 切换到评估模式
    total_loss = 0
    correct = 0
    with torch.no_grad():  # 推理阶段不计算梯度
        for images, labels in loader:
            outputs = model(images)
            loss = loss_fn(outputs, labels)
            total_loss += loss.item() * images.size(0)
            correct += (outputs.argmax(dim=1) == labels).sum().item()
    return total_loss / len(loader.dataset), correct / len(loader.dataset)

# ===== 数据准备与训练入口示例 =====
# transform = transforms.Compose([
#     transforms.ToTensor(),
#     transforms.Normalize((0.1307,), (0.3081,))
# ])
# train_data = datasets.MNIST("./data", train=True, download=True, transform=transform)
# train_loader = DataLoader(train_data, batch_size=64, shuffle=True)
# test_data = datasets.MNIST("./data", train=False, download=True, transform=transform)
# test_loader = DataLoader(test_data, batch_size=64, shuffle=False)
#
# optimizer = optim.Adam(model.parameters(), lr=0.001)
# loss_fn = nn.CrossEntropyLoss()
#
# for epoch in range(5):
#     train_loss, train_acc = train_one_epoch(model, train_loader, optimizer, loss_fn)
#     test_loss, test_acc = evaluate(model, test_loader, optimizer, loss_fn)
#     print(f"epoch {epoch+1}: train acc {train_acc:.4f}, test acc {test_acc:.4f}")

几点实战建议:

  • 数据归一化:MNIST 常用均值 0.1307、标准差 0.3081 进行标准化,能让训练更稳定、收敛更快。
  • 评估时 with torch.no_grad():推理阶段不需要记录梯度,这样可以显著节省显存和计算量。
  • 先小样本验证:正式训练前,可以先在几百张图片上过拟合,确认损失能下降、代码没有逻辑问题,再上全量数据。

9. 总结

本文以「积木搭建」为主线,系统梳理了 nn 模块的核心组成:

  • nn.Module:所有模型的基类,负责管理参数、定义前向传播、支持嵌套与 train/eval 模式切换。
  • 网络层LinearConv2dMaxPool2dBatchNorm2d 等积木,各有明确的输入输出形状与适用场景。
  • 激活函数:为网络引入非线性,ReLU 是最常用的默认选择,GELU 在 Transformer 中更常见。
  • 损失函数:衡量预测与真实的差距,回归用 MSELoss,分类用 CrossEntropyLoss
  • 优化器:通过 zero_gradforwardbackwardstep 四步更新参数,快速上手用 Adam
  • 容器类Sequential 串联简单结构,ModuleList 管理动态多分支,ModuleDict 按键访问子模块。

理解并熟练使用这些「积木」,是之后学习更复杂网络结构(如 ResNet、Transformer)的基础。下一篇我们将深入讨论模型训练技巧,包括学习率调度、早停与模型保存等实战内容,敬请期待。

10. 思考与练习

  1. 修改第 8 节的 CNN,尝试将卷积核大小改为 5×5,观察参数量与输出尺寸的变化,并用输出尺寸公式验证你的结果。
  2. 使用 nn.Sequential 搭建一个纯全连接网络,并在随机数据上验证其输出形状。
  3. 为什么 CrossEntropyLoss 前面不需要加 Softmax?查阅文档并用自己的话解释。
  4. SimpleNet 上分别调用 net.train()net.eval(),打印 net.training 属性,观察两者区别。
  5. 用 Python 原生列表 [] 存两个 nn.Linear,再打印 net.parameters(),对比使用 nn.ModuleList 时的差异,思考为什么参数会“消失”。
  6. 给第 8 节的训练循环加上验证集,并在每个 epoch 结束后打印训练准确率和验证准确率。

更多推荐