举个例子 最早的卷积网络lenet[LeNet在Fashion-MNIST数据集上的表现]nn.Flatten是保留第一维批量维度的情况下其他维度全部展平import torch from torch import nn from d2l import torch as d2l net nn.Sequential( nn.Conv2d(1, 6, kernel_size5, padding2), nn.Sigmoid(), nn.AvgPool2d(kernel_size2, stride2), nn.Conv2d(6, 16, kernel_size5), nn.Sigmoid(), nn.AvgPool2d(kernel_size2, stride2), nn.Flatten(), nn.Linear(16 * 5 * 5, 120), nn.Sigmoid(), nn.Linear(120, 84), nn.Sigmoid(), nn.Linear(84, 10))16*5*5是展平后所有的特征输入注意pytorch中使用交叉熵作为损失函数会自动在最后加上softmax不用自己定义84是lenet模型作者原先定义的你也可以根据你自己定义的模型修改结果:查看数据batch_size 256 train_iter, test_iter d2l.load_data_fashion_mnist(batch_sizebatch_size)求精度 不需要梯度 所以eval 外加 with torch.no_grad() 该函数在训练函数中被调用来统计:def evaluate_accuracy_gpu(net, data_iter, deviceNone): #save 使用GPU计算模型在数据集上的精度 if isinstance(net, nn.Module): net.eval() # 设置为评估模式 if not device: device next(iter(net.parameters())).device #如果没传设备就找网络的第一个参数的设备 # 正确预测的数量总预测的数量 metric d2l.Accumulator(2) with torch.no_grad(): for X, y in data_iter: if isinstance(X, list): # BERT微调所需的之后将介绍 X [x.to(device) for x in X] else: X X.to(device) y y.to(device) metric.add(d2l.accuracy(net(X), y), y.numel()) return metric[0] / metric[1]逐行解释 def evaluate_accuracy_gpu(net, data_iter, deviceNone): 定义函数接收三个参数 net待评估的神经网络模型。 data_iter数据迭代器提供测试集的批量数据。 device指定运行的设备CPU或GPU若不提供则自动从模型参数推断。 if isinstance(net, nn.Module): 检查 net是否是 nn.Module的实例几乎总是 True。 net.eval() 将模型切换到评估模式。这会关闭 Dropout、BatchNorm 的训练行为如 BatchNorm 停止更新均值和方差使用固定的 running stats。 if not device: 如果没有指定设备则自动获取模型参数的设备next(iter(net.parameters())).device。 metric d2l.Accumulator(2) 创建一个累加器用于累计两个数值正确预测的数量accuracy 返回值和总样本数y.numel()。Accumulator是 d2l 提供的工具类支持 add方法累加通过下标 [0]、[1]访问。 with torch.no_grad(): 在该上下文内所有计算都不会记录梯度节省显存并加速推理。 for X, y in data_iter: 遍历测试集的每个批次。 if isinstance(X, list): 某些模型如 BERT的输入可能是一个列表例如 token_ids, attention_mask 等需要逐个移到设备上。否则直接 X X.to(device)。 y y.to(device) 标签也移到相同设备。 metric.add(d2l.accuracy(net(X), y), y.numel()) net(X)前向传播得到预测 logits。 d2l.accuracy(net(X), y)计算该批次中预测正确的样本数返回整数。 y.numel()该批次的样本总数。 metric.add(...)将这两个数值累加到累加器中。 return metric[0] / metric[1] 返回总体准确率 总正确数 / 总样本数。训练函数#save def train_ch6(net, train_iter, test_iter, num_epochs, lr, device): 用GPU训练模型(在第六章定义) def init_weights(m): if type(m) nn.Linear or type(m) nn.Conv2d: nn.init.xavier_uniform_(m.weight) net.apply(init_weights) print(training on, device) net.to(device) optimizer torch.optim.SGD(net.parameters(), lrlr) loss nn.CrossEntropyLoss() animator d2l.Animator(xlabelepoch, xlim[1, num_epochs], legend[train loss, train acc, test acc]) timer, num_batches d2l.Timer(), len(train_iter) for epoch in range(num_epochs): # 训练损失之和训练准确率之和样本数 metric d2l.Accumulator(3) net.train() for i, (X, y) in enumerate(train_iter): timer.start() optimizer.zero_grad() X, y X.to(device), y.to(device) y_hat net(X) l loss(y_hat, y) l.backward() optimizer.step() with torch.no_grad(): metric.add(l * X.shape[0], d2l.accuracy(y_hat, y), X.shape[0]) timer.stop() train_l metric[0] / metric[2] train_acc metric[1] / metric[2] if (i 1) % (num_batches // 5) 0 or i num_batches - 1: animator.add(epoch (i 1) / num_batches, (train_l, train_acc, None)) test_acc evaluate_accuracy_gpu(net, test_iter) animator.add(epoch 1, (None, None, test_acc)) print(floss {train_l:.3f}, train acc {train_acc:.3f}, ftest acc {test_acc:.3f}) print(f{metric[2] * num_epochs / timer.sum():.1f} examples/sec fon {str(device)})逐段解释 函数头部与初始化 def train_ch6(net, train_iter, test_iter, num_epochs, lr, device): 参数 net模型 train_iter训练数据迭代器 test_iter测试数据迭代器 num_epochs训练的轮数 lr学习率 device设备 def init_weights(m): 定义一个内部函数用于初始化模型各层的权重。若层是 Linear或 Conv2d则使用 Xavier 均匀初始化xavier_uniform_。 net.apply(init_weights) 递归地将 init_weights应用到 net的每个子模块上完成权重初始化。 net.to(device) 将模型移动到指定设备GPU或CPU。 optimizer torch.optim.SGD(net.parameters(), lrlr) 使用 SGD 优化器学习率为 lr。 loss nn.CrossEntropyLoss() 交叉熵损失函数适用于多分类任务。 animator d2l.Animator(...) d2l 的可视化工具用于实时绘制训练曲线损失、训练准确率、测试准确率。 timer, num_batches d2l.Timer(), len(train_iter) 计时器和训练批次总数。 主训练循环 for epoch in range(num_epochs): 外层循环每个 epoch 遍历一次训练集。 metric d2l.Accumulator(3) 累加器记录三个值总损失loss * batch_size、正确预测数、总样本数。 net.train() 切换到训练模式启用 Dropout、BatchNorm 等训练行为。 for i, (X, y) in enumerate(train_iter): 内层循环遍历每个 mini-batch。 timer.start() 开始计时用于统计每秒处理的样本数。 optimizer.zero_grad() 清空上一轮的梯度。 X, y X.to(device), y.to(device) 将数据移到 GPU。 y_hat net(X) 前向传播得到预测 logits。 l loss(y_hat, y) 计算该批次的损失标量。 l.backward() 反向传播计算梯度。 optimizer.step() 更新模型参数。 with torch.no_grad(): 以下操作不追踪梯度仅用于统计。 metric.add(l * X.shape[0], d2l.accuracy(y_hat, y), X.shape[0]) l * X.shape[0]该批次的总损失因为 l是平均损失乘以样本数得到总和。 d2l.accuracy(y_hat, y)该批次正确预测的样本数。 X.shape[0]该批次的样本数。 三者累加。 timer.stop() 停止计时实际累加时间。 train_l metric[0] / metric[2] 当前 epoch 的平均训练损失 总损失 / 总样本数。 train_acc metric[1] / metric[2] 当前 epoch 的训练准确率。 if (i 1) % (num_batches // 5) 0 or i num_batches - 1: 每完成大约 1/5 的批次或最后一个批次时更新动画曲线显示训练损失和准确率测试准确率暂时为 None。 每个 epoch 结束后 test_acc evaluate_accuracy_gpu(net, test_iter) 调用之前定义的评估函数计算在整个测试集上的准确率。 animator.add(epoch 1, (None, None, test_acc)) 在动画中添加当前 epoch 的测试准确率点。 训练结束后的输出 print(floss {train_l:.3f}, train acc {train_acc:.3f}, test acc {test_acc:.3f}) 打印最终的损失、训练准确率和测试准确率。 print(f{metric[2] * num_epochs / timer.sum():.1f} examples/sec on {str(device)}) 打印训练速度总处理样本数 / 总耗时单位样本/秒。使用gpulr, num_epochs 0.9, 10 train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())概念作用net.eval()vsnet.train()控制 Dropout/BatchNorm 的行为。评估时禁用 dropoutBN 使用固定均值方差。torch.no_grad()禁用梯度计算减少内存消耗加速推理。Accumulator自定义的累加器用于累积多个数值便于计算平均值。xavier_uniform_权重初始化方法使每层输出的方差保持一致缓解梯度消失/爆炸。CrossEntropyLoss内部包含Softmax 负对数似然适用于多分类。Animatord2l 的可视化工具动态绘制训练曲线。eval就是关闭模型中的dropout功能调到评价模式与之相对的是train()eval的作用是把模型调整到验证状态不调整权重with no grad 用在测试层不需要梯度下降求导减少内存开销eval也用于测试层但不一样的理解基本正确但有一些细微之处需要澄清。下面是详细的区别说明1.model.eval()的作用本质切换模型的运行模式训练模式 → 评估模式。影响Dropout关闭。在评估模式下Dropout 不再随机丢弃神经元而是保留所有神经元但输出会乘以保留概率等同于缩放。BatchNorm使用训练阶段积累的全局均值和方差而不是当前 batch 的统计量同时停止更新 running_mean/running_var。其他层某些自定义层也可能依赖此标志。注意eval()不会禁用梯度计算也不会清空梯度。它只改变特定层的行为。在评估模式下如果仍然执行反向传播梯度依然会计算虽然通常没必要。2.torch.no_grad()的作用本质一个上下文管理器在其作用域内所有张量操作都不再跟踪梯度。影响梯度计算完全禁用 autograd 引擎任何操作都不会构建计算图因此不会占用显存存储中间变量。性能大幅降低内存消耗加速前向推理因为无需记录梯度相关信息。反向传播不可能在此作用域内调用.backward()因为没有计算图。注意no_grad()不会影响 Dropout 或 BatchNorm 的行为——它只控制梯度追踪。即使在model.eval()模式下如果不使用no_grad()梯度依然会被记录尽管通常你不会去用它。3. 常见误区纠正“eval()会清零梯度” ❌梯度清零需要手动调用optimizer.zero_grad()或model.zero_grad()eval()不做这件事。“eval()会禁用梯度计算” ❌在eval()模式下如果你不小心调用了.backward()梯度仍然会被计算虽然逻辑上不应该这样做。“no_grad()会关闭 Dropout” ❌在no_grad()下如果模型处于train()模式Dropout 依然会生效随机丢弃但这在推理时通常是不希望的。因此需要先用eval()关闭 Dropout。现代卷积神经网络:残差网络ResNet-18何恺明等人 2015年的ImageNet图像识别挑战赛夺魁它通过残差块构建跨层的数据通道是计算机视觉中最流行的体系架构包含了残差块:import torch from torch import nn from torch.nn import functional as F from d2l import torch as d2l class Residual(nn.Module): #save def __init__(self, input_channels, num_channels, use_1x1convFalse, strides1): super().__init__() self.conv1 nn.Conv2d(input_channels, num_channels, kernel_size3, padding1, stridestrides) self.conv2 nn.Conv2d(num_channels, num_channels, kernel_size3, padding1) if use_1x1conv: self.conv3 nn.Conv2d(input_channels, num_channels, kernel_size1, stridestrides) else: self.conv3 None self.bn1 nn.BatchNorm2d(num_channels) self.bn2 nn.BatchNorm2d(num_channels) def forward(self, X): Y F.relu(self.bn1(self.conv1(X))) Y self.bn2(self.conv2(Y)) if self.conv3: X self.conv3(X) Y X return F.relu(Y) init: input_channels输入特征图的通道数。 num_channels输出特征图的通道数也是中间卷积层的输出通道数。 use_1x1conv是否使用 1×1 卷积来调整跳跃连接的通道数或空间尺寸默认 False。 strides第一个卷积层的步长默认 1可用于下采样。 conv1第一个 3×3 卷积将通道数从 input_channels变为 num_channels步长为 strides可能下采样。 conv2第二个 3×3 卷积保持通道数不变num_channels → num_channels步长为 1padding1 保证空间尺寸不变。 当 use_1x1convTrue时创建一个 1×1 卷积层用于将输入 X的通道数从 input_channels变为 num_channels且步长与 conv1相同strides。这样可以使跳跃连接的输出与主路径的输出形状一致便于相加。 如果 use_1x1convFalse则 conv3 None此时要求 input_channels num_channels且 strides 1否则无法直接相加形状不匹配。 两个 BatchNorm 层分别跟在 conv1和 conv2之后用于加速训练和稳定梯度。 前向传播: 主路径 X经过 conv1→ bn1→ ReLU得到 Y。 Y再经过 conv2→ bn2得到 Y此处不加 ReLU因为后面还要加跳跃连接后再激活。 跳跃连接 如果 conv3存在即 use_1x1convTrue则将原始输入 X通过 conv3调整形状得到与 Y相同尺寸的张量。 否则直接使用原始 X要求形状已匹配。 残差相加Y X逐元素相加。 最终激活F.relu(Y)输出。blk Residual(3, 3) # input_channels3, num_channels3, use_1x1convFalse, strides1 X torch.rand(4, 3, 6, 6) # 批量大小4通道3高6宽6 Y blk(X) Y.shape # 输出形状如果使用use_1x1convTrue或strides1blk2 Residual(3, 6, use_1x1convTrue, strides2) X2 torch.rand(4, 3, 12, 12) Y2 blk2(X2) Y2.shape # torch.Size([4, 6, 6, 6])输入(4, 3, 12, 12)经过conv1步长2 →输出(4, 6, 6, 6)空间减半通道翻倍。conv2保持(4, 6, 6, 6)。跳跃连接conv31×1步长2将输入(4, 3, 12, 12)调整为(4, 6, 6, 6)。相加后输出(4, 6, 6, 6)。跳跃连接恒等映射或 1×1 投影/卷积让梯度可以直接流过缓解退化问题use_1x1conv当通道数或空间尺寸不匹配时用 1×1 卷积对齐模型代码:(经典)b1 nn.Sequential(nn.Conv2d(1, 64, kernel_size7, stride2, padding3), nn.BatchNorm2d(64), nn.ReLU(), nn.MaxPool2d(kernel_size3, stride2, padding1)) nn.BatchNorm2d(num_features) # 用于卷积层输出的归一化输入形状: (N, C, H, W) 其他常见归一化层 nn.BatchNorm1d(num_features)– 用于全连接层或1D数据。 nn.LayerNorm(normalized_shape)– 层归一化常用于 Transformer。 nn.InstanceNorm2d(num_features)– 实例归一化风格迁移常用。 nn.GroupNorm(num_groups, num_channels)– 分组归一化。 def resnet_block(input_channels, num_channels, num_residuals, first_blockFalse): blk [] for i in range(num_residuals):#表示残差块组 if i 0 and not first_block: # 如果不是第一个块组first_blockFalse且是该块组的第一个残差块i0 # 则需要下采样strides2并用 1x1 卷积调整通道数 blk.append(Residual(input_channels, num_channels, use_1x1convTrue, strides2)) else: # 否则要么是第一个块组first_blockTrue的所有残差块 # 要么是其他块组中非第一个残差块i0 # 都保持空间尺寸和通道数不变 blk.append(Residual(num_channels, num_channels)) return blk i 0判断当前是否是该块组内的第一个残差块局部位置。 first_block判断当前块组是否是整个网络的第一个残差块组全局标识。 2. 为什么需要同时使用 目标只在非第一个块组的第一个残差块中进行下采样和通道变换。 如果是第一个块组first_blockTrue那么即使 i0也不下采样因为 b1已经做过下采样了b2需要保持尺寸。 如果是后续块组first_blockFalse则只在 i0时下采样后续残差块i0保持尺寸。 b2 nn.Sequential(*resnet_block(64, 64, 2, first_blockTrue)) b3 nn.Sequential(*resnet_block(64, 128, 2)) b4 nn.Sequential(*resnet_block(128, 256, 2)) b5 nn.Sequential(*resnet_block(256, 512, 2)) 这里的 *是 Python 的一个内置运算符叫做解包运算符Unpacking Operator。 简单来说它的作用是把列表List或元组Tuple里的元素一个个拿出来作为独立的参数传给函数。 1. 为什么要用 * 这是由 PyTorch 的 nn.Sequential类的特性决定的。 nn.Sequential的要求它希望你传入的是一连串的独立层Layer比如 nn.Conv2d(...), nn.ReLU(), nn.Conv2d(...)。 resnet_block的返回看上一行代码 return blkblk是一个列表List里面装着两个 Residual对象比如 [layer1, layer2]。 如果你直接写 nn.Sequential(resnet_block(...))相当于把整个列表当作第一个元素塞进去了PyTorch 会报错因为它期望里面是层而不是一个列表。 假设 blk的内容是 [A, B]。 ❌ 不使用 * nn.Sequential(blk) 实际传入的是nn.Sequential([[A, B]]) 结果PyTorch 认为第一个模块是一个包含 A 和 B 的列表不是网络层报错。 net nn.Sequential(b1, b2, b3, b4, b5, nn.AdaptiveAvgPool2d((1,1)), nn.Flatten(), nn.Linear(512, 10)) nn.AdaptiveAvgPool2d((1, 1))是 PyTorch 中的自适应平均池化层它的作用是将任意大小的输入特征图强制池化为指定的输出尺寸这里是 1×1。它通过自动计算池化窗口大小和步长来实现无需手动指定 kernel_size 和 stride。 常用于分类网络的最后将特征图压缩成固定长度的向量再接入全连接层。 import os os.environ[PYTORCH_CUDA_ALLOC_CONF] expandable_segments:True lr, num_epochs, batch_size 0.05, 10, 256 train_iter, test_iter d2l.load_data_fashion_mnist(batch_size, resize96) d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())模块下采样次数通道变化目的b12 次卷积池化1→64快速降分辨率提取低级特征b20 次64→64在中等分辨率下深化特征不做下采样b3/b4/b5各 1 次64→128→256→512逐步降低分辨率、增加通道数提取高级语义从b2开始每个block有两个残差块除了第一个block两个残差块的输入与输出通道相同其余的block都是第一个残差块通道加倍大小减半第二个残差块输入输出通道不变使用gpu常见错误:1 OutOfMemoryError:CUDA out of memory. Tried to allocate 144.00 MiB. GPU 0 has a total capacity of 8.00 GiB of which 6.87 GiB is free. Of the allocated memory 51.69 MiB is allocated by PyTorch, and 12.31 MiB is reserved by PyTorch but unallocated. If reserved but unallocated memory is large try setting PYTORCH_CUDA_ALLOC_CONFexpandable_segments:True to avoid fragmentation. See documentation for Memory Management (https://pytorch.org/docs/stable/notes/cuda.html#environment-variables)CUDA out of memory错误是因为 显存不足。从错误信息看你的 GPU 总容量为 8 GB空闲约 6.87 GB但训练时需要额外分配 144 MB 时失败了。这通常是由于模型或数据太大、batch size 过大导致的解决:import osos.environ[PYTORCH_CUDA_ALLOC_CONF] expandable_segments:True3. 使用torch.cuda.memory_summary()诊断可以打印详细的显存使用情况帮助定位哪个操作占用最多print(torch.cuda.memory_summary())4. 减少网络中的中间变量使用inplaceTrue的激活函数如F.relu(..., inplaceTrue)可节省显存。使用checkpoint梯度检查点技术用时间换空间。import osos.environ[PYTORCH_CUDA_ALLOC_CONF] expandable_segments:Truelr, num_epochs, batch_size 0.05, 10, 256train_iter, test_iter d2l.load_data_fashion_mnist(batch_size, resize96)d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())2 RuntimeError: DataLoader worker (pid(s) 8568, 25304, 12044, 29264) exited unexpectedly遇到的DataLoader worker意外退出根本原因仍然是内存不足这次可能是系统 RAM 不够而非 GPU 显存。你仍然在使用batch_size256和resize96这对你的硬件8GB GPU 推测有限的系统内存来说负担太重错误链分析DataLoader 使用多进程加载数据默认num_workers4每个 worker 子进程需要将图像从 28×28 放大到 96×96并批量组织成 tensor。每个 worker 都需要占用一定量的系统内存RAM来缓存预处理后的图像。当 batch_size 很大256且图像尺寸放大后每个 worker 的内存需求急剧上升。系统内存RAM耗尽导致 worker 进程被操作系统强制终止exited unexpectedly。主进程检测到 worker 死亡抛出RuntimeError。为什么expandable_segments没用该选项只针对 CUDA 显存管理且你的 Windows 平台不支持警告已提示。当前错误是系统内存RAM不足与 GPU 显存无关。错误总结:错误根本原因解决措施CUDA OOMGPU 显存碎片化 大尺寸/大 batch减小 resize 和 batch_sizeDataLoader worker 退出系统 RAM 不足减小 batch_size、减小 resize、减少 num_workers
学习卷积神经网络
举个例子 最早的卷积网络lenet[LeNet在Fashion-MNIST数据集上的表现]nn.Flatten是保留第一维批量维度的情况下其他维度全部展平import torch from torch import nn from d2l import torch as d2l net nn.Sequential( nn.Conv2d(1, 6, kernel_size5, padding2), nn.Sigmoid(), nn.AvgPool2d(kernel_size2, stride2), nn.Conv2d(6, 16, kernel_size5), nn.Sigmoid(), nn.AvgPool2d(kernel_size2, stride2), nn.Flatten(), nn.Linear(16 * 5 * 5, 120), nn.Sigmoid(), nn.Linear(120, 84), nn.Sigmoid(), nn.Linear(84, 10))16*5*5是展平后所有的特征输入注意pytorch中使用交叉熵作为损失函数会自动在最后加上softmax不用自己定义84是lenet模型作者原先定义的你也可以根据你自己定义的模型修改结果:查看数据batch_size 256 train_iter, test_iter d2l.load_data_fashion_mnist(batch_sizebatch_size)求精度 不需要梯度 所以eval 外加 with torch.no_grad() 该函数在训练函数中被调用来统计:def evaluate_accuracy_gpu(net, data_iter, deviceNone): #save 使用GPU计算模型在数据集上的精度 if isinstance(net, nn.Module): net.eval() # 设置为评估模式 if not device: device next(iter(net.parameters())).device #如果没传设备就找网络的第一个参数的设备 # 正确预测的数量总预测的数量 metric d2l.Accumulator(2) with torch.no_grad(): for X, y in data_iter: if isinstance(X, list): # BERT微调所需的之后将介绍 X [x.to(device) for x in X] else: X X.to(device) y y.to(device) metric.add(d2l.accuracy(net(X), y), y.numel()) return metric[0] / metric[1]逐行解释 def evaluate_accuracy_gpu(net, data_iter, deviceNone): 定义函数接收三个参数 net待评估的神经网络模型。 data_iter数据迭代器提供测试集的批量数据。 device指定运行的设备CPU或GPU若不提供则自动从模型参数推断。 if isinstance(net, nn.Module): 检查 net是否是 nn.Module的实例几乎总是 True。 net.eval() 将模型切换到评估模式。这会关闭 Dropout、BatchNorm 的训练行为如 BatchNorm 停止更新均值和方差使用固定的 running stats。 if not device: 如果没有指定设备则自动获取模型参数的设备next(iter(net.parameters())).device。 metric d2l.Accumulator(2) 创建一个累加器用于累计两个数值正确预测的数量accuracy 返回值和总样本数y.numel()。Accumulator是 d2l 提供的工具类支持 add方法累加通过下标 [0]、[1]访问。 with torch.no_grad(): 在该上下文内所有计算都不会记录梯度节省显存并加速推理。 for X, y in data_iter: 遍历测试集的每个批次。 if isinstance(X, list): 某些模型如 BERT的输入可能是一个列表例如 token_ids, attention_mask 等需要逐个移到设备上。否则直接 X X.to(device)。 y y.to(device) 标签也移到相同设备。 metric.add(d2l.accuracy(net(X), y), y.numel()) net(X)前向传播得到预测 logits。 d2l.accuracy(net(X), y)计算该批次中预测正确的样本数返回整数。 y.numel()该批次的样本总数。 metric.add(...)将这两个数值累加到累加器中。 return metric[0] / metric[1] 返回总体准确率 总正确数 / 总样本数。训练函数#save def train_ch6(net, train_iter, test_iter, num_epochs, lr, device): 用GPU训练模型(在第六章定义) def init_weights(m): if type(m) nn.Linear or type(m) nn.Conv2d: nn.init.xavier_uniform_(m.weight) net.apply(init_weights) print(training on, device) net.to(device) optimizer torch.optim.SGD(net.parameters(), lrlr) loss nn.CrossEntropyLoss() animator d2l.Animator(xlabelepoch, xlim[1, num_epochs], legend[train loss, train acc, test acc]) timer, num_batches d2l.Timer(), len(train_iter) for epoch in range(num_epochs): # 训练损失之和训练准确率之和样本数 metric d2l.Accumulator(3) net.train() for i, (X, y) in enumerate(train_iter): timer.start() optimizer.zero_grad() X, y X.to(device), y.to(device) y_hat net(X) l loss(y_hat, y) l.backward() optimizer.step() with torch.no_grad(): metric.add(l * X.shape[0], d2l.accuracy(y_hat, y), X.shape[0]) timer.stop() train_l metric[0] / metric[2] train_acc metric[1] / metric[2] if (i 1) % (num_batches // 5) 0 or i num_batches - 1: animator.add(epoch (i 1) / num_batches, (train_l, train_acc, None)) test_acc evaluate_accuracy_gpu(net, test_iter) animator.add(epoch 1, (None, None, test_acc)) print(floss {train_l:.3f}, train acc {train_acc:.3f}, ftest acc {test_acc:.3f}) print(f{metric[2] * num_epochs / timer.sum():.1f} examples/sec fon {str(device)})逐段解释 函数头部与初始化 def train_ch6(net, train_iter, test_iter, num_epochs, lr, device): 参数 net模型 train_iter训练数据迭代器 test_iter测试数据迭代器 num_epochs训练的轮数 lr学习率 device设备 def init_weights(m): 定义一个内部函数用于初始化模型各层的权重。若层是 Linear或 Conv2d则使用 Xavier 均匀初始化xavier_uniform_。 net.apply(init_weights) 递归地将 init_weights应用到 net的每个子模块上完成权重初始化。 net.to(device) 将模型移动到指定设备GPU或CPU。 optimizer torch.optim.SGD(net.parameters(), lrlr) 使用 SGD 优化器学习率为 lr。 loss nn.CrossEntropyLoss() 交叉熵损失函数适用于多分类任务。 animator d2l.Animator(...) d2l 的可视化工具用于实时绘制训练曲线损失、训练准确率、测试准确率。 timer, num_batches d2l.Timer(), len(train_iter) 计时器和训练批次总数。 主训练循环 for epoch in range(num_epochs): 外层循环每个 epoch 遍历一次训练集。 metric d2l.Accumulator(3) 累加器记录三个值总损失loss * batch_size、正确预测数、总样本数。 net.train() 切换到训练模式启用 Dropout、BatchNorm 等训练行为。 for i, (X, y) in enumerate(train_iter): 内层循环遍历每个 mini-batch。 timer.start() 开始计时用于统计每秒处理的样本数。 optimizer.zero_grad() 清空上一轮的梯度。 X, y X.to(device), y.to(device) 将数据移到 GPU。 y_hat net(X) 前向传播得到预测 logits。 l loss(y_hat, y) 计算该批次的损失标量。 l.backward() 反向传播计算梯度。 optimizer.step() 更新模型参数。 with torch.no_grad(): 以下操作不追踪梯度仅用于统计。 metric.add(l * X.shape[0], d2l.accuracy(y_hat, y), X.shape[0]) l * X.shape[0]该批次的总损失因为 l是平均损失乘以样本数得到总和。 d2l.accuracy(y_hat, y)该批次正确预测的样本数。 X.shape[0]该批次的样本数。 三者累加。 timer.stop() 停止计时实际累加时间。 train_l metric[0] / metric[2] 当前 epoch 的平均训练损失 总损失 / 总样本数。 train_acc metric[1] / metric[2] 当前 epoch 的训练准确率。 if (i 1) % (num_batches // 5) 0 or i num_batches - 1: 每完成大约 1/5 的批次或最后一个批次时更新动画曲线显示训练损失和准确率测试准确率暂时为 None。 每个 epoch 结束后 test_acc evaluate_accuracy_gpu(net, test_iter) 调用之前定义的评估函数计算在整个测试集上的准确率。 animator.add(epoch 1, (None, None, test_acc)) 在动画中添加当前 epoch 的测试准确率点。 训练结束后的输出 print(floss {train_l:.3f}, train acc {train_acc:.3f}, test acc {test_acc:.3f}) 打印最终的损失、训练准确率和测试准确率。 print(f{metric[2] * num_epochs / timer.sum():.1f} examples/sec on {str(device)}) 打印训练速度总处理样本数 / 总耗时单位样本/秒。使用gpulr, num_epochs 0.9, 10 train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())概念作用net.eval()vsnet.train()控制 Dropout/BatchNorm 的行为。评估时禁用 dropoutBN 使用固定均值方差。torch.no_grad()禁用梯度计算减少内存消耗加速推理。Accumulator自定义的累加器用于累积多个数值便于计算平均值。xavier_uniform_权重初始化方法使每层输出的方差保持一致缓解梯度消失/爆炸。CrossEntropyLoss内部包含Softmax 负对数似然适用于多分类。Animatord2l 的可视化工具动态绘制训练曲线。eval就是关闭模型中的dropout功能调到评价模式与之相对的是train()eval的作用是把模型调整到验证状态不调整权重with no grad 用在测试层不需要梯度下降求导减少内存开销eval也用于测试层但不一样的理解基本正确但有一些细微之处需要澄清。下面是详细的区别说明1.model.eval()的作用本质切换模型的运行模式训练模式 → 评估模式。影响Dropout关闭。在评估模式下Dropout 不再随机丢弃神经元而是保留所有神经元但输出会乘以保留概率等同于缩放。BatchNorm使用训练阶段积累的全局均值和方差而不是当前 batch 的统计量同时停止更新 running_mean/running_var。其他层某些自定义层也可能依赖此标志。注意eval()不会禁用梯度计算也不会清空梯度。它只改变特定层的行为。在评估模式下如果仍然执行反向传播梯度依然会计算虽然通常没必要。2.torch.no_grad()的作用本质一个上下文管理器在其作用域内所有张量操作都不再跟踪梯度。影响梯度计算完全禁用 autograd 引擎任何操作都不会构建计算图因此不会占用显存存储中间变量。性能大幅降低内存消耗加速前向推理因为无需记录梯度相关信息。反向传播不可能在此作用域内调用.backward()因为没有计算图。注意no_grad()不会影响 Dropout 或 BatchNorm 的行为——它只控制梯度追踪。即使在model.eval()模式下如果不使用no_grad()梯度依然会被记录尽管通常你不会去用它。3. 常见误区纠正“eval()会清零梯度” ❌梯度清零需要手动调用optimizer.zero_grad()或model.zero_grad()eval()不做这件事。“eval()会禁用梯度计算” ❌在eval()模式下如果你不小心调用了.backward()梯度仍然会被计算虽然逻辑上不应该这样做。“no_grad()会关闭 Dropout” ❌在no_grad()下如果模型处于train()模式Dropout 依然会生效随机丢弃但这在推理时通常是不希望的。因此需要先用eval()关闭 Dropout。现代卷积神经网络:残差网络ResNet-18何恺明等人 2015年的ImageNet图像识别挑战赛夺魁它通过残差块构建跨层的数据通道是计算机视觉中最流行的体系架构包含了残差块:import torch from torch import nn from torch.nn import functional as F from d2l import torch as d2l class Residual(nn.Module): #save def __init__(self, input_channels, num_channels, use_1x1convFalse, strides1): super().__init__() self.conv1 nn.Conv2d(input_channels, num_channels, kernel_size3, padding1, stridestrides) self.conv2 nn.Conv2d(num_channels, num_channels, kernel_size3, padding1) if use_1x1conv: self.conv3 nn.Conv2d(input_channels, num_channels, kernel_size1, stridestrides) else: self.conv3 None self.bn1 nn.BatchNorm2d(num_channels) self.bn2 nn.BatchNorm2d(num_channels) def forward(self, X): Y F.relu(self.bn1(self.conv1(X))) Y self.bn2(self.conv2(Y)) if self.conv3: X self.conv3(X) Y X return F.relu(Y) init: input_channels输入特征图的通道数。 num_channels输出特征图的通道数也是中间卷积层的输出通道数。 use_1x1conv是否使用 1×1 卷积来调整跳跃连接的通道数或空间尺寸默认 False。 strides第一个卷积层的步长默认 1可用于下采样。 conv1第一个 3×3 卷积将通道数从 input_channels变为 num_channels步长为 strides可能下采样。 conv2第二个 3×3 卷积保持通道数不变num_channels → num_channels步长为 1padding1 保证空间尺寸不变。 当 use_1x1convTrue时创建一个 1×1 卷积层用于将输入 X的通道数从 input_channels变为 num_channels且步长与 conv1相同strides。这样可以使跳跃连接的输出与主路径的输出形状一致便于相加。 如果 use_1x1convFalse则 conv3 None此时要求 input_channels num_channels且 strides 1否则无法直接相加形状不匹配。 两个 BatchNorm 层分别跟在 conv1和 conv2之后用于加速训练和稳定梯度。 前向传播: 主路径 X经过 conv1→ bn1→ ReLU得到 Y。 Y再经过 conv2→ bn2得到 Y此处不加 ReLU因为后面还要加跳跃连接后再激活。 跳跃连接 如果 conv3存在即 use_1x1convTrue则将原始输入 X通过 conv3调整形状得到与 Y相同尺寸的张量。 否则直接使用原始 X要求形状已匹配。 残差相加Y X逐元素相加。 最终激活F.relu(Y)输出。blk Residual(3, 3) # input_channels3, num_channels3, use_1x1convFalse, strides1 X torch.rand(4, 3, 6, 6) # 批量大小4通道3高6宽6 Y blk(X) Y.shape # 输出形状如果使用use_1x1convTrue或strides1blk2 Residual(3, 6, use_1x1convTrue, strides2) X2 torch.rand(4, 3, 12, 12) Y2 blk2(X2) Y2.shape # torch.Size([4, 6, 6, 6])输入(4, 3, 12, 12)经过conv1步长2 →输出(4, 6, 6, 6)空间减半通道翻倍。conv2保持(4, 6, 6, 6)。跳跃连接conv31×1步长2将输入(4, 3, 12, 12)调整为(4, 6, 6, 6)。相加后输出(4, 6, 6, 6)。跳跃连接恒等映射或 1×1 投影/卷积让梯度可以直接流过缓解退化问题use_1x1conv当通道数或空间尺寸不匹配时用 1×1 卷积对齐模型代码:(经典)b1 nn.Sequential(nn.Conv2d(1, 64, kernel_size7, stride2, padding3), nn.BatchNorm2d(64), nn.ReLU(), nn.MaxPool2d(kernel_size3, stride2, padding1)) nn.BatchNorm2d(num_features) # 用于卷积层输出的归一化输入形状: (N, C, H, W) 其他常见归一化层 nn.BatchNorm1d(num_features)– 用于全连接层或1D数据。 nn.LayerNorm(normalized_shape)– 层归一化常用于 Transformer。 nn.InstanceNorm2d(num_features)– 实例归一化风格迁移常用。 nn.GroupNorm(num_groups, num_channels)– 分组归一化。 def resnet_block(input_channels, num_channels, num_residuals, first_blockFalse): blk [] for i in range(num_residuals):#表示残差块组 if i 0 and not first_block: # 如果不是第一个块组first_blockFalse且是该块组的第一个残差块i0 # 则需要下采样strides2并用 1x1 卷积调整通道数 blk.append(Residual(input_channels, num_channels, use_1x1convTrue, strides2)) else: # 否则要么是第一个块组first_blockTrue的所有残差块 # 要么是其他块组中非第一个残差块i0 # 都保持空间尺寸和通道数不变 blk.append(Residual(num_channels, num_channels)) return blk i 0判断当前是否是该块组内的第一个残差块局部位置。 first_block判断当前块组是否是整个网络的第一个残差块组全局标识。 2. 为什么需要同时使用 目标只在非第一个块组的第一个残差块中进行下采样和通道变换。 如果是第一个块组first_blockTrue那么即使 i0也不下采样因为 b1已经做过下采样了b2需要保持尺寸。 如果是后续块组first_blockFalse则只在 i0时下采样后续残差块i0保持尺寸。 b2 nn.Sequential(*resnet_block(64, 64, 2, first_blockTrue)) b3 nn.Sequential(*resnet_block(64, 128, 2)) b4 nn.Sequential(*resnet_block(128, 256, 2)) b5 nn.Sequential(*resnet_block(256, 512, 2)) 这里的 *是 Python 的一个内置运算符叫做解包运算符Unpacking Operator。 简单来说它的作用是把列表List或元组Tuple里的元素一个个拿出来作为独立的参数传给函数。 1. 为什么要用 * 这是由 PyTorch 的 nn.Sequential类的特性决定的。 nn.Sequential的要求它希望你传入的是一连串的独立层Layer比如 nn.Conv2d(...), nn.ReLU(), nn.Conv2d(...)。 resnet_block的返回看上一行代码 return blkblk是一个列表List里面装着两个 Residual对象比如 [layer1, layer2]。 如果你直接写 nn.Sequential(resnet_block(...))相当于把整个列表当作第一个元素塞进去了PyTorch 会报错因为它期望里面是层而不是一个列表。 假设 blk的内容是 [A, B]。 ❌ 不使用 * nn.Sequential(blk) 实际传入的是nn.Sequential([[A, B]]) 结果PyTorch 认为第一个模块是一个包含 A 和 B 的列表不是网络层报错。 net nn.Sequential(b1, b2, b3, b4, b5, nn.AdaptiveAvgPool2d((1,1)), nn.Flatten(), nn.Linear(512, 10)) nn.AdaptiveAvgPool2d((1, 1))是 PyTorch 中的自适应平均池化层它的作用是将任意大小的输入特征图强制池化为指定的输出尺寸这里是 1×1。它通过自动计算池化窗口大小和步长来实现无需手动指定 kernel_size 和 stride。 常用于分类网络的最后将特征图压缩成固定长度的向量再接入全连接层。 import os os.environ[PYTORCH_CUDA_ALLOC_CONF] expandable_segments:True lr, num_epochs, batch_size 0.05, 10, 256 train_iter, test_iter d2l.load_data_fashion_mnist(batch_size, resize96) d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())模块下采样次数通道变化目的b12 次卷积池化1→64快速降分辨率提取低级特征b20 次64→64在中等分辨率下深化特征不做下采样b3/b4/b5各 1 次64→128→256→512逐步降低分辨率、增加通道数提取高级语义从b2开始每个block有两个残差块除了第一个block两个残差块的输入与输出通道相同其余的block都是第一个残差块通道加倍大小减半第二个残差块输入输出通道不变使用gpu常见错误:1 OutOfMemoryError:CUDA out of memory. Tried to allocate 144.00 MiB. GPU 0 has a total capacity of 8.00 GiB of which 6.87 GiB is free. Of the allocated memory 51.69 MiB is allocated by PyTorch, and 12.31 MiB is reserved by PyTorch but unallocated. If reserved but unallocated memory is large try setting PYTORCH_CUDA_ALLOC_CONFexpandable_segments:True to avoid fragmentation. See documentation for Memory Management (https://pytorch.org/docs/stable/notes/cuda.html#environment-variables)CUDA out of memory错误是因为 显存不足。从错误信息看你的 GPU 总容量为 8 GB空闲约 6.87 GB但训练时需要额外分配 144 MB 时失败了。这通常是由于模型或数据太大、batch size 过大导致的解决:import osos.environ[PYTORCH_CUDA_ALLOC_CONF] expandable_segments:True3. 使用torch.cuda.memory_summary()诊断可以打印详细的显存使用情况帮助定位哪个操作占用最多print(torch.cuda.memory_summary())4. 减少网络中的中间变量使用inplaceTrue的激活函数如F.relu(..., inplaceTrue)可节省显存。使用checkpoint梯度检查点技术用时间换空间。import osos.environ[PYTORCH_CUDA_ALLOC_CONF] expandable_segments:Truelr, num_epochs, batch_size 0.05, 10, 256train_iter, test_iter d2l.load_data_fashion_mnist(batch_size, resize96)d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())2 RuntimeError: DataLoader worker (pid(s) 8568, 25304, 12044, 29264) exited unexpectedly遇到的DataLoader worker意外退出根本原因仍然是内存不足这次可能是系统 RAM 不够而非 GPU 显存。你仍然在使用batch_size256和resize96这对你的硬件8GB GPU 推测有限的系统内存来说负担太重错误链分析DataLoader 使用多进程加载数据默认num_workers4每个 worker 子进程需要将图像从 28×28 放大到 96×96并批量组织成 tensor。每个 worker 都需要占用一定量的系统内存RAM来缓存预处理后的图像。当 batch_size 很大256且图像尺寸放大后每个 worker 的内存需求急剧上升。系统内存RAM耗尽导致 worker 进程被操作系统强制终止exited unexpectedly。主进程检测到 worker 死亡抛出RuntimeError。为什么expandable_segments没用该选项只针对 CUDA 显存管理且你的 Windows 平台不支持警告已提示。当前错误是系统内存RAM不足与 GPU 显存无关。错误总结:错误根本原因解决措施CUDA OOMGPU 显存碎片化 大尺寸/大 batch减小 resize 和 batch_sizeDataLoader worker 退出系统 RAM 不足减小 batch_size、减小 resize、减少 num_workers