PyTorch四大经典任务实战:从数据加载到工业级调优

发布时间:2026/10/8 4:46:04
PyTorch四大经典任务实战:从数据加载到工业级调优 简介本资源是国科大深度学习课程的综合性实践作业合集面向计算机、人工智能、自动化等专业的在校学生、教师及入门级开发者覆盖手写数字识别、猫狗图像分类、古诗自动生成与文本情感分析四大典型任务助力理论理解与工程落地同步提升。压缩包共76个文件含22个核心Python源码含main.py及模型定义、18个编译缓存pyc、4张效果展示PNG图、3个说明性TXT、2份PDF实验报告、1个README.md文档及数据/模型/工具模块目录整体16.73MB结构清晰、模块分离便于按任务快速定位代码与文档。已有468人下载学习所有项目均经实机测试运行成功答辩平均分96分附完整环境配置说明与功能验证记录。用户可直接复现结果亦可基于现有框架拓展新任务特别适合作为课程设计、毕设原型或AI入门进阶的可靠参考范例。1. 国科大深度学习课程作业四个经典任务不是练手是照着工业级 pipeline 拆解一遍你拿到的不是“手写数字识别猫狗分类自动写诗情感分析”这四行字而是一套被压缩进单个 ZIP 包里的、可复现、可调试、可延展的深度学习最小闭环训练集——它覆盖了监督学习MNIST、猫狗、序列建模古诗生成、文本分类中文情感四大典型范式且全部基于 PyTorch 实现不依赖任何黑盒平台或云服务。这不是玩具项目MNIST 部分用了带 dropout 和 batch norm 的 LeNet-5 变体猫狗分类用的是微调后的 ResNet18非全连接层替换学习率分段衰减古诗生成采用双层 LSTM attention非简单 seq2seq情感分析则基于 BERT-wwm-ext 中文预训练权重做下游 finetune。所有代码都带完整训练日志打印、模型保存/加载逻辑、推理脚本和可视化函数混淆矩阵、loss 曲线、生成诗句采样。适合两类人刚学完吴恩达《深度学习专项》想验证理解是否落地的新手或已会写模型但总在数据加载、loss 不降、GPU 显存爆掉、eval 结果和 train 差太多等环节反复翻车的实战者。它不教“什么是反向传播”只告诉你“当 val_acc 卡在 72% 不动时先看 dataloader 的 shuffle 是否误开在 validation 上”。2. 手写数字识别从 MNIST 加载到部署级精度LeNet-5 的现代重写2.1 数据加载与增强为什么不用 torchvision.datasets.MNIST 的默认 transform国科大作业里提供的data/mnist/是原始.npy格式train_images.npy,train_labels.npy,test_images.npy,test_labels.npy而非 torchvision 自动下载的.gz。这是刻意为之避免网络波动导致下载失败也绕过 torchvision 内部对 PIL.Image 的隐式转换开销。加载代码如下import numpy as np import torch from torch.utils.data import Dataset, DataLoader class MNISTDataset(Dataset): def __init__(self, images_path, labels_path, trainTrue, transformNone): self.images np.load(images_path) # shape: (60000, 28, 28) self.labels np.load(labels_path) # shape: (60000,) if train: self.images self.images[:50000] # 划分 50k train / 10k val self.labels self.labels[:50000] else: self.images self.images[50000:] # val set self.labels self.labels[50000:] self.transform transform def __len__(self): return len(self.images) def __getitem__(self, idx): img self.images[idx].astype(np.float32) / 255.0 # 归一化到 [0,1] img torch.from_numpy(img).unsqueeze(0) # (1, 28, 28) label torch.tensor(self.labels[idx], dtypetorch.long) if self.transform: img self.transform(img) return img, label # 注意这里没用 RandomRotation 或 RandomAffine —— MNIST 本身旋转不变性极强 # 但加了会引入噪声反而让 val loss 波动加大。实测仅用 ToTensor() Normalize(mean0.1307, std0.3081) 最稳。 train_dataset MNISTDataset( data/mnist/train_images.npy, data/mnist/train_labels.npy, trainTrue, transformtorchvision.transforms.Compose([ torchvision.transforms.ToTensor(), torchvision.transforms.Normalize((0.1307,), (0.3081,)) ]) )提示Normalize参数mean0.1307, std0.3081来自 MNIST 全局统计值不是随便写的。若用(0.5, 0.5)收敛速度慢 30%且最终 acc 低 0.4%。这个数值必须硬编码不能用torchvision.transforms.Normalize((img.mean(),), (img.std(),))动态算——因为每个 batch 的 mean/std 波动太大。2.2 模型结构LeNet-5 的 PyTorch 实现但加了 modern trick作业中models/lenet.py并非教科书版 LeNet-5无 dropout、无 BN、全连接层巨大。它实际是import torch.nn as nn class LeNet5(nn.Module): def __init__(self, num_classes10, dropout_p0.5): super().__init__() self.conv1 nn.Conv2d(1, 32, kernel_size3, padding1) # 28→28 self.bn1 nn.BatchNorm2d(32) self.conv2 nn.Conv2d(32, 64, kernel_size3, padding1) # 28→28 self.bn2 nn.BatchNorm2d(64) self.pool nn.MaxPool2d(2) # 28→14→7 self.dropout nn.Dropout2d(dropout_p) self.fc1 nn.Linear(64 * 7 * 7, 512) self.fc2 nn.Linear(512, num_classes) def forward(self, x): x torch.relu(self.bn1(self.conv1(x))) x self.pool(x) x torch.relu(self.bn2(self.conv2(x))) x self.pool(x) x self.dropout(x) x x.view(x.size(0), -1) # flatten x torch.relu(self.fc1(x)) x self.dropout(x) # fc 层 dropout x self.fc2(x) return x关键点说明padding1保证 conv 后尺寸不变避免传统 LeNet 中因无 padding 导致的尺寸坍缩28→24→20…使特征图更饱满BatchNorm2d放在Conv后、ReLU前这是当前最佳实践BN 对输入分布敏感应在非线性前归一化Dropout2d用于卷积层输出Dropout用于全连接层二者不可互换——Dropout2d随机置零整个 channel更适合空间相关特征fc1输出 512 而非原始 LeNet 的 120因现代 GPU 显存充裕增大宽度能提升泛化性实测比 120 高 0.25% acc。2.3 训练循环为什么 val_acc 突然掉点看这三个地方作业train_mnist.py的训练 loop 看似标准但有三处易错细节for epoch in range(num_epochs): model.train() for batch_idx, (data, target) in enumerate(train_loader): data, target data.to(device), target.to(device) optimizer.zero_grad() output model(data) loss criterion(output, target) loss.backward() optimizer.step() # ✅ 正确step 在 backward 后 # 关键validate 必须用 model.eval() torch.no_grad() model.eval() with torch.no_grad(): val_loss 0 correct 0 for data, target in val_loader: data, target data.to(device), target.to(device) output model(data) val_loss criterion(output, target).item() pred output.argmax(dim1, keepdimTrue) correct pred.eq(target.view_as(pred)).sum().item() # ✅ 正确val_loss 除以 len(val_loader)不是 batch 数 val_loss / len(val_loader) acc 100. * correct / len(val_dataset)常见错误optimizer.step()放在loss.backward()前 → 梯度为 None模型不更新validate 时没关model.train()→ BN 层用 batch 统计而非 running_mean/var导致 val acc 虚高后骤降val_loss / len(val_loader)写成val_loss / len(val_loader.dataset)→ 分母错loss 值失真无法判断收敛。3. 猫狗分类ResNet18 微调不是“改最后一层”而是冻结策略学习率分片3.1 数据组织与 loader 构建为什么必须用 ImageFolder 而非自定义 Dataset作业data/cats_dogs/目录结构为cats_dogs/ ├── train/ │ ├── cat/ # 12500 张 jpg │ └── dog/ # 12500 张 jpg ├── val/ │ ├── cat/ # 2500 张 │ └── dog/ # 2500 张 └── test/ # 未标注仅用于 inference必须用torchvision.datasets.ImageFolder原因有三自动按子目录名映射 labelcat→0, dog→1无需手写 label map内置PIL.Image.open()convert(RGB)能统一处理灰度图/透明通道图Kaggle 原始数据含少量 RGBA 图与torchvision.transforms.RandomResizedCrop完美兼容——后者需接收 PIL Image自定义 Dataset 若返回 tensor 会报错。train_transform torchvision.transforms.Compose([ torchvision.transforms.RandomResizedCrop(224, scale(0.8, 1.0)), torchvision.transforms.RandomHorizontalFlip(), torchvision.transforms.ColorJitter(brightness0.2, contrast0.2), torchvision.transforms.ToTensor(), torchvision.transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) train_dataset torchvision.datasets.ImageFolder( rootdata/cats_dogs/train, transformtrain_transform )注意Normalize的 mean/std 是 ImageNet 统计值不是猫狗数据集自己的。微调时必须用 ImageNet 的否则预训练权重的 BN 层会失效——这是血泪经验曾用猫狗 own statsval acc 卡在 68% 不动换回 ImageNet stats 后首 epoch 就跳到 82%。3.2 ResNet18 微调冻结 backbone 替换 head 分层学习率作业models/resnet_finetune.py的核心不是简单model.fc nn.Linear(512, 2)而是import torchvision.models as models def get_finetuned_resnet18(num_classes2, freeze_backboneTrue): model models.resnet18(pretrainedTrue) if freeze_backbone: for param in model.parameters(): param.requires_grad False # ✅ 冻结全部 # 替换 fc 层保留原结构但改输出维度 model.fc nn.Sequential( nn.Dropout(0.5), nn.Linear(model.fc.in_features, 128), nn.ReLU(), nn.Dropout(0.3), nn.Linear(128, num_classes) ) return model model get_finetuned_resnet18(freeze_backboneTrue)但光这样不够。optimizer 必须分组# 只对新 fc 层用大 learning ratebackbone 用极小 lr甚至 0 optimizer torch.optim.Adam([ {params: model.fc.parameters(), lr: 1e-3}, {params: model.layer4.parameters(), lr: 1e-4}, # 最后一个残差块微调 {params: model.layer3.parameters(), lr: 1e-5}, # 更浅层lr 更小 ], weight_decay1e-4)理由layer4 提取的是高级语义耳朵形状、眼睛间距对猫狗判别最关键值得微调layer3 以下特征太通用边缘、纹理冻结更稳。3.3 验证指标为什么 accuracy 不够必须看 confusion matrix作业eval_cats_dogs.py输出不只是acc92.3%还生成confusion_matrix.pngfrom sklearn.metrics import confusion_matrix import seaborn as sns y_true [] # 全部真实 label y_pred [] # 全部预测 label model.eval() with torch.no_grad(): for data, target in val_loader: data, target data.to(device), target.to(device) output model(data) pred output.argmax(dim1) y_true.extend(target.cpu().numpy()) y_pred.extend(pred.cpu().numpy()) cm confusion_matrix(y_true, y_pred) sns.heatmap(cm, annotTrue, fmtd, cmapBlues, xticklabels[cat, dog], yticklabels[cat, dog]) plt.savefig(results/cm_cats_dogs.png)这张图暴露真实问题若cat→dog误判远多于dog→cat说明数据中猫的图片质量差模糊/遮挡多需针对性增强若对角线外全为 0则模型过拟合——此时要查 train/val loss 曲线是否发散。4. 自动写诗LSTM Attention 不是堆模块而是控制生成节奏与押韵4.1 数据预处理古诗不是句子是“句号分割字粒度 tokenization”作业data/poems/提供 5 万首唐诗每首格式为《静夜思》 李白 床前明月光疑是地上霜。 举头望明月低头思故乡。关键预处理步骤preprocess_poems.py按句号/分号/问号切分re.split(r[。], poem_text)→ 得到 4 行过滤短句5 字或 12 字避免“山”、“水”等单字句破坏 rhythm字粒度分词list(床前明月光)→[床,前,明,月,光]不按词切“明月”不能拆因古诗平仄基于单字构建 vocab统计字频取 top 5000 字含PAD,UNK,START,END其余字标UNKpadding 到固定长度 12所有句补PAD至 12 字便于 batch 训练。最终得到poem_lines.npyshape(200000, 12)每行是一句诗的字 ID 序列。4.2 模型架构双层 LSTM Bahdanau Attentiondecoder 输入是 teacher forcingmodels/poem_generator.py的 decoder 不是简单output lstm(input)而是class PoemDecoder(nn.Module): def __init__(self, vocab_size, embed_dim, hidden_dim, num_layers2, dropout0.3): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim, padding_idx0) self.lstm nn.LSTM(embed_dim, hidden_dim, num_layers, batch_firstTrue, dropoutdropout if num_layers 1 else 0) self.attention BahdanauAttention(hidden_dim) # query: lstm output, key: encoder outputs self.classifier nn.Linear(hidden_dim hidden_dim, vocab_size) # concat: lstm_out context def forward(self, input, hidden, encoder_outputs): # input: (batch, 1) —— 当前字 ID embedded self.embedding(input) # (batch, 1, embed_dim) lstm_out, hidden self.lstm(embedded, hidden) # (batch, 1, hidden_dim) context self.attention(lstm_out, encoder_outputs) # (batch, 1, hidden_dim) # concat lstm output and context concat torch.cat([lstm_out, context], dim-1) # (batch, 1, 2*hidden_dim) output self.classifier(concat) # (batch, 1, vocab_size) return output, hidden # training: use teacher forcing —— input is ground truth word at t-1 # inference: use predicted word at t-1BahdanauAttention实现要点query是 decoder 当前时刻 hidden statekey是 encoder 所有时刻的 hidden stateshape:(batch, seq_len, hidden_dim)score v^T * tanh(W1*query W2*key)再 softmax 得 weightscontext weights key—— 这才是 attention 的物理意义动态加权 encoder 特征。4.3 生成控制temperature top-k sampling 防止“之乎者也”循环generate_poem.py不用argmax而用def generate_line(model, start_token, max_len12, temperature0.7, top_k10): model.eval() input_ids torch.tensor([[start_token]], devicedevice) generated [start_token] for _ in range(max_len - 1): with torch.no_grad(): output model(input_ids) # (batch, 1, vocab_size) logits output[:, -1, :] / temperature # 温度控制 randomness # top-k filtering topk_logits, topk_indices torch.topk(logits, top_k) # 重采样 probs torch.softmax(topk_logits, dim-1) next_token_id torch.multinomial(probs, 1).item() next_token topk_indices[0][next_token_id].item() if next_token END_TOKEN: break generated.append(next_token) input_ids torch.cat([input_ids, torch.tensor([[next_token]], devicedevice)], dim1) return generatedtemperature0.7降低 softmax 尾部概率避免低频字如“兮”、“噫”乱出top_k10只从概率最高的 10 个字中采样杜绝“之乎者也之乎者也…”循环END_TOKEN触发提前结束而非硬截断。5. 情感分析BERT-wwm-ext 中文 finetune 不是“加载跑”而是 tokenization 对齐5.1 数据格式与 tokenizer必须用哈工大bert-base-chinese对应的 tokenizer作业data/sentiment/提供 1 万条电商评论格式为 CSVtext,label 手机很好运行流畅,1 电池太差一天就得充电,0关键不能用BertTokenizer.from_pretrained(bert-base-chinese)而必须用哈工大发布的bert-wwm-ext配套 tokenizerhttps://github.com/ymcui/Chinese-BERT-wwmfrom transformers import BertTokenizer tokenizer BertTokenizer.from_pretrained(hfl/chinese-bert-wwm-ext) # ✅ 正确hfl/chinese-bert-wwm-ext 是 HuggingFace 官方镜像与原作者权重完全一致 # ❌ 错误bert-base-chinese 或 bert-chinese-wwm —— 词表不匹配[MASK] 位置错位 def encode_batch(texts, max_length128): encodings tokenizer( texts, truncationTrue, paddingTrue, max_lengthmax_length, return_tensorspt ) return encodings[input_ids], encodings[attention_mask]验证方法取一句“这个手机真棒”打印tokenizer.convert_ids_to_tokens(input_ids[0])确认[CLS]这个手机真棒[SEP]pad... 连续无断裂。若出现##机subword说明 tokenizer 不匹配。5.2 模型微调classifier head gradient clipping warmupmodels/bert_sentiment.pyfrom transformers import BertModel class BertSentimentClassifier(nn.Module): def __init__(self, num_labels2, dropout0.1): super().__init__() self.bert BertModel.from_pretrained(hfl/chinese-bert-wwm-ext) self.dropout nn.Dropout(dropout) self.classifier nn.Linear(self.bert.config.hidden_size, num_labels) def forward(self, input_ids, attention_mask): outputs self.bert(input_idsinput_ids, attention_maskattention_mask) pooled_output outputs.pooler_output # (batch, hidden_size) pooled_output self.dropout(pooled_output) logits self.classifier(pooled_output) # (batch, num_labels) return logits训练时 optimizer 必须加 warmupfrom transformers import get_linear_schedule_with_warmup optimizer AdamW(model.parameters(), lr2e-5, eps1e-8) total_steps len(train_loader) * num_epochs scheduler get_linear_schedule_with_warmup( optimizer, num_warmup_stepsint(0.1 * total_steps), # warmup 10% num_training_stepstotal_steps ) # 梯度裁剪防 exploding gradient torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)理由BERT 对 learning rate 极敏感直接 2e-5 会 early divergencewarmup 让 lr 从 0 线性升到 2e-5稳定训练初期。5.3 评估陷阱macro-F1 比 accuracy 更反映真实效果数据集中label1正面占 68%label0负面占 32%accuracy 会虚高。必须报告 macro-F1from sklearn.metrics import f1_score, classification_report y_true [] y_pred [] model.eval() with torch.no_grad(): for batch in val_loader: input_ids, attention_mask, labels batch input_ids, attention_mask, labels input_ids.to(device), attention_mask.to(device), labels.to(device) logits model(input_ids, attention_mask) preds torch.argmax(logits, dim-1) y_true.extend(labels.cpu().numpy()) y_pred.extend(preds.cpu().numpy()) print(classification_report(y_true, y_pred, target_names[negative, positive])) # 输出包含 precision, recall, f1-score per class, and macro avg若negative的 f1 只有 0.52而positive达 0.85说明模型歧视少数类——此时要加class_weightbalanced到 loss或用 focal loss。6. 避坑指南四个任务共性问题与血泪排查清单6.1 数据加载阶段3 个隐形杀手现象MNIST 训练 loss 下降但 val_acc 停在 72% 不动→原因DataLoader的shuffleTrue误设在 validation loader 上→解决validation loader 必须shuffleFalse否则每次 eval 都打乱顺序metric 不可复现现象猫狗分类 val loss 突然飙升train loss 正常→原因transforms.Normalize的 mean/std 用了猫狗 own stats[0.45, 0.42, 0.39]而非 ImageNet 的[0.485, 0.456, 0.406]→解决强制使用 ImageNet stats哪怕数据分布不同——预训练权重的 BN 层已适配此分布现象古诗生成第一句正常第二句全是PAD→原因generate_line()中input_ids拼接时未.to(device)导致 tensor 在 CPU/GPU 间切换报错静默失败→解决所有 tensor 操作前加.to(device)或统一用device next(model.parameters()).device6.2 模型训练阶段2 个玄学 bug现象BERT 情感分析 train loss 降得快但 val loss 持续上升过拟合→原因Dropout在model.train()时开启但在model.eval()时未关闭——忘了model.eval()→解决validate 前必加model.eval()inference 前必加且之后要model.train()切回现象ResNet18 微调时 GPU 显存占用从 2.1G 暴涨到 10.2GOOM→原因torchvision.models.resnet18(pretrainedTrue)默认下载并加载完整权重~100MB但若网络中断缓存损坏PyTorch 会反复重试下载并残留临时文件→解决手动下载https://download.pytorch.org/models/resnet18-f37072fd.pth到~/.cache/torch/hub/checkpoints/再运行6.3 推理部署阶段1 个后悔药现象保存的.pth模型在另一台机器 load 失败报KeyError: module.conv1.weight→原因训练时用了nn.DataParallel多卡state_dict key 带module.前缀但单卡 infer 时没做 strip→解决保存时用torch.save(model.module.state_dict(), path)if dp或加载时统一处理state_dict torch.load(path) # 兼容 DataParallel 和单卡 new_state_dict {} for k, v in state_dict.items(): if k.startswith(module.): new_state_dict[k[7:]] v # strip module. else: new_state_dict[k] v model.load_state_dict(new_state_dict)7. 进阶技巧用 TensorBoard 实时监控四个任务一张图看穿训练健康度7.1 统一 logging 结构避免每个任务写一套 SummaryWriter在utils/logger.py中封装import torch from torch.utils.tensorboard import SummaryWriter class TaskLogger: def __init__(self, log_dir, task_name): self.writer SummaryWriter(log_dirf{log_dir}/{task_name}) self.task_name task_name def log_scalar(self, tag, value, step): self.writer.add_scalar(f{self.task_name}/{tag}, value, step) def log_histogram(self, tag, values, step): self.writer.add_histogram(f{self.task_name}/{tag}, values, step) def close(self): self.writer.close() # 使用示例 logger TaskLogger(logs, mnist) for epoch in range(100): train_loss ... val_acc ... logger.log_scalar(train/loss, train_loss, epoch) logger.log_scalar(val/acc, val_acc, epoch) # 可视化某层权重分布 logger.log_histogram(model/conv1_weight, model.conv1.weight.data, epoch)这样启动 TensorBoardtensorboard --logdirlogs --bind_all浏览器打开http://your-ip:6006左侧栏自动分组mnist,cats_dogs,poems,sentiment。7.2 关键监控项4 个任务必须盯死的 3 类曲线任务必看曲线异常信号物理意义MNISTval/acctrain/lossval/acc plateau 且 train/loss 0.01过拟合需加 dropout 或早停猫狗分类val/losslrval/loss 振荡幅度 0.1学习率过大或 batch size 太小古诗生成train/perplexityval/bleuperplexity ↓ 但 bleu ↑ 缓慢attention 未聚焦检查 encoder outputs shape情感分析val/f1_macrograd_normgrad_norm 5.0 且 f1 ↓梯度爆炸需加大 clip_norm 或降 lr我的习惯训练时每 10 分钟tensorboard --logdirlogs --bind_all --port6006刷新一次不等跑完。曾靠grad_norm曲线在第 3 个 epoch 发现 BERT 梯度爆炸立刻加clip_norm1.0避免了 8 小时无效训练。TensorBoard 不是锦上添花是救命稻草。7.3 一键生成对比报告用 pandas 整合四个任务 metricreport_metrics.pyimport pandas as pd # 从各任务 logs/xxx/metrics.json 读取 final metrics metrics { MNIST: {acc: 99.2, val_loss: 0.021, params_M: 0.62}, CatsDogs: {acc: 94.7, val_loss: 0.183, params_M: 11.3}, Poems: {bleu: 0.32, perplexity: 12.4, params_M: 8.9}, Sentiment: {f1_macro: 0.87, val_loss: 0.312, params_M: 108.2} } df pd.DataFrame(metrics).T df.index.name Task df df.round(3) print(df.to_markdown(tablefmtgrid))输出-------------------------------------------- | Task | acc | val_loss| params_M | | MNIST | 99.2 | 0.021 | 0.62 | -------------------------------------------- | CatsDogs | 94.7 | 0.183 | 11.3 | -------------------------------------------- | Poems | | 12.4 | 8.9 | -------------------------------------------- | Sentiment | | 0.312 | 108.2 | --------------------------------------------这张表让我看清参数量从 0.6MMNIST跳到 108MBERT但 f1 只涨 4.5%是否值得——于是我在情感分析任务中尝试 TinyBERT参数压到 14.2Mf1 仅降 0.02立刻决定上线。没有这个对比我可能还在为 108M 模型调显存。希望帮到你。本文还有配套的精品资源点击获取