基于深度学习毕业设计:新手入门实战指南与避坑清单
背景痛点:为什么“跑通”比“跑快”更难
第一次把“深度学习”四个字写进毕业设计任务书时,我满脑子都是“高大上”——直到真正动手才发现,拦路虎从第 0 天就开始排队:
- 选题阶段:导师一句“要有创新点”,结果网上一搜全是“猫狗分类”“手写数字”,想换个医学影像却找不到开源数据,瞬间陷入“高不成低不就”的尴尬。
- 环境配置:实验室电脑显卡是 RTX 3060,回宿舍笔记本只有核显,两份代码两份环境,conda 环境复制过去直接 CUDA 版本不匹配,报错信息红到发紫。
- 模型调优:训练 accuracy 飙到 95%,一验证掉到 65%,怀疑人生后发现训练集里 30% 图片是“复制粘贴”扩增出来的,模型把背景纹理背得滚瓜烂熟。
- 部署答辩:本地
.pth文件 120 M,导师一句“能在网页里演示吗?”当场傻眼,连夜搜“PyTorch 转 ONNX 再转 TensorFlow.js”,踩坑踩到天亮。
技术选型:PyTorch vs TensorFlow/Keras——教学场景下谁更香
我把两门课都上过,实验室里师兄师姐也各占半壁江山,体验下来差异最直观的三点:
- 学习曲线:PyTorch 的“即时执行”像写 Python 脚本,debug 时
pdb一路步进就能看到张量形状;TensorFlow 2.x 虽然也 eager,但一打开tf.data的map|batch|prefetch链式 API,新手容易在“括号海洋”里迷路。 - 社区支持:论文开源代码 PyTorch 占比明显高,想复现最新模型,GitHub 直接
git clone就能跑;TensorFlow 官方 example 仓库维护规范,但偏工业,本科生想魔改网络层时,PyTorch 的“继承nn.Module重写forward”模式更直观。 - 调试便利性:PyTorch 出错栈会精确到
.py第几行;TensorFlow 的 Graph 模式下报错经常只给“op: ‘Conv2D’”,定位全靠猜,教学场景下时间成本翻倍。
结论:零项目经验、以“毕业设计”为第一目标,优先 PyTorch;如果实验室已有 TensorFlow 祖传代码且导师要求必须沿用,再考虑 Keras 高层 API。
核心实现:用图像分类把“数据-模型-训练-验证”跑通
下面以“10 类水果分类”为例,给出最小可运行骨架,数据集用 Kaggle 的 Fruits-360,硬盘占用 1.3 G,笔记本也能跑。
1. 数据加载与增强
- 统一尺寸 224×224,训练集用随机水平翻转、ColorJitter、随机旋转 15°;验证集只做中心裁剪,防止信息泄露。
- 用
ImageFolder直接读文件夹,省去写CSV的麻烦;num_workers设 4,Windows 下如果报错改成 0。 - 类别不平衡时,用
WeightedRandomSampler给少样本加权重,别让模型把苹果学成龙眼。
2. 模型定义
- 别一上来就
torchvision.models.resnet50,毕业设计 8 G 显存可能撑不住;用resnet18足够,最后全连接层fc输出改 10 类。 - 把网络结构、预训练权重加载、冻结层逻辑拆成
model.py,主训练脚本只负责“拿模型”,后续换 Backbone 改一行即可。
3. 训练循环
- 三件套:loss 用
CrossEntropyLoss,optimizer 用AdamW+cosineLR,metric 用accuracy和confusion_matrix。 - 每个 epoch 结束后把验证集走一遍,早停 patience 设 10,防止通宵跑实验把显卡“跑废”。
- 实时打印 loss 和 lr,Jupyter 里能一眼看出是否震荡;服务器后台跑则用
tqdm写日志文件,回宿舍也能手机 tail。
4. 验证逻辑
- 把
model.eval()和torch.no_grad()写进同一上下文,避免 BN 层统计值污染。 - 计算完混淆矩阵后,用
seaborn.heatmap画出来,答辩 PPT 直接贴图,老师秒懂哪两类最容易分错。
完整代码:Clean Code 版 PyTorch 骨架
以下代码按“文件-函数-行”三级注释,全部可复现,保存为四个文件即可跑通。
project/
project/ ├── data.py # 数据加载与增强 ├── model.py # 网络定义 ├── train.py # 训练脚本 ├── utils.py # 工具函数 └── README.mddata.py
import os, torch from torchvision import datasets, transforms from torch.utils.data import DataLoader def get_loaders(root:str, batch_size:int=32, num_workers:int=4): train_dir = os.path.join(root, 'train') val_dir = os.path.join(root, 'val') train_tf = transforms.Compose([ transforms.Resize(256), transforms.RandomResizedCrop(224), transforms.RandomHorizontalFlip(), transforms.ColorJitter(0.1,0.1,0.1), transforms.ToTensor(), transforms.Normalize([0.485,0.456,0.406], [0.229,0.224,0.225]) ]) val_tf = transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize([0.485,0.456,0.406], [0.229,0.224,0.225]) ]) train_ds = datasets.ImageFolder(train_dir, transform=train_tf) val_ds = datasets.ImageFolder(val_dir , transform=val_tf) train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=True, num_workers=num_workers, pin_memory=True) val_loader = DataLoader(val_ds, batch_size=batch_size, shuffle=False, num_workers=num_workers, pin_memory=True) return train_loader, val_loader, train_ds.classesmodel.py
import torch.nn as nn from torchvision.models import resnet18 class FruitCNN(nn.Module): def __init__(self, num_classes:int=10, pretrained:bool=True): super().__init__() self.backbone = resnet18(pretrained=pretrained) # 冻结前面两层,减少显存占用 for layer in [self.backbone.conv1, self.backbone.bn1, self.backbone.layer1, self.backbone.layer2]: for p in layer.parameters(): p.requires_grad = False in_features = self.backbone.fc.in_features self.backbone.fc = nn.Linear(in_features, num_classes) def forward(self, x): return self.backbone(x)train.py
import torch, os, time, random, numpy as np from torch import nn, optim from torch.utils.tensorboard import SummaryWriter from sklearn.metrics import accuracy_score, confusion_matrix from data import get_loaders from model import FruitCNN from utils import set_seed, save_ckpt def main(args): set_seed(42) device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') train_loader, val_loader, class_names = get_loaders(args.data_root, args.bs) model = FruitCNN(num_classes=len(class_names)).to(device) criterion = nn.CrossEntropyLoss() optimizer = optim.AdamW(model.parameters(), lr=args.lr, weight_decay=1e-4) scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=args.epochs) best_acc = 0.0 writer = SummaryWriter(log_dir=args.log_dir) for epoch in range(1, args.epochs+1): # train model.train() running_loss, running_correct = 0., 0. for x, y in train_loader: x, y = x.to(device), y.to(device) optimizer.zero_grad() logits = model(x) loss = criterion(logits, y) loss.backward() optimizer.step() running_loss += loss.item() * x.size(0) running_correct += (logits.argmax(1) == y).sum().item() train_loss = running_loss / len_train train_acc = running_correct / len_train writer.add_scalar('Loss/train', train_loss, epoch) writer.add_scalar('Acc/train', train_acc, epoch) # val model.eval() y_true, y_pred = [], [] with torch.no_grad(): for x, y in val_loader: x, y = x.to(device), y.to(device) logits = model(x) y_true.extend(y.cpu().numpy()) y_pred.extend(logits.argmax(1).cpu().numpy()) val_acc = accuracy_score(y_true, y_pred) writer.add_scalar('Acc/val', val_acc, epoch) print(f'Epoch {epoch:03d} | train loss {train_loss:.4f} ' f'acc {train_acc:.4f} | val acc {val_acc:.4f}') if val_acc > best_acc: best_acc = val_acc save_ckpt(model, optimizer, epoch, best_acc, args.ckpt_dir) scheduler.step() writer.close() print('Finished, best val acc:', best_acc) if __name__ == '__main__': import argparse parser = argparse.ArgumentParser() parser.add_argument('--data_root', type=str, required=True) parser.add_argument('--bs', type=int, default=32) parser.add_argument('--lr', type=float, default=3e-4) parser.add_argument('--epochs', type=int, default=50) parser.add_argument('--log_dir', type=str, default='runs') parser.add_argument('--ckpt_dir', type=str, default='ckpts') args = parser.parse_args() os.makedirs(args.ckpt_dir, exist_ok=True) main(args)utils.py
import torch, random, numpy as np, os, shutil def set_seed(seed=42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic = True torch.backends.cudnn.benchmark = False def save_ckpt(model, optimizer, epoch, best_acc, ckpt_dir): path = os.path.join(ckpt_dir, f'best_{best_acc*100:.2f}.pth') torch.save({'model': model.state_dict(), 'optimizer': optimizer.state_dict(), 'epoch': epoch}, path) print('Saved checkpoint ->', path)性能与安全:小样本场景下的“过拟合三件套”
- 固定随机种子:见
utils.set_seed,确保每次重跑结果一致,方便和导师“对齐”。 - 数据增强 + 正则化:翻转、裁剪、ColorJitter 之外,再加
LabelSmoothingCrossEntropy(PyTorch 1.10+ 已内置),把“硬标签”软化,实测 val acc 提升 1-2 点。 - 早停 + 断点保存:每提升一次就覆盖
best.pth,防止断电白跑;同时把optimizer.state_dict一起保存,后续可接断点继续训练。
生产环境避坑:从.pth到网页演示的“最后一公里”
- CUDA 版本冲突:实验室 3060 驱动 525,宿舍笔记本 470,直接复制环境会
cudart64_110.dll not found。解决:用conda-pack把环境打包成 tar,另一台机解压后conda-unpack即可。 requirements.txt规范:除了torch==2.0.0+cu117,再加--extra-index-url https://download.pytorch.org/whl/cu117,避免 pip 去 PyPI 拉 CPU 版本。- ONNX 导出注意:
- 动态 batch 维度
dynamic_axes={'input':{0:'batch'}, 'output':{0:'batch'}},方便网页端一次传多张图。 - 若用
resnet18自带的nn.AdaptiveAvgPool2d,导出会警告“opset 11 不支持”,把opset_version=12即可。 - 转
tfjs前先onnx-tf convert,再tensorflowjs_converter --input_format=tf_saved_model,模型体积从 120 M 压缩到 28 M,HTTP 加载 3 s 内完成。
- 动态 batch 维度
结尾:先跑通,再扩展
把上面仓库git clone下来,改一条数据路径,不出意外 30 min 内能看到 val acc 跳到 90%+。接下来你可以:
- 把
resnet18换成efficientnet_b0,对比参数量和显存占用; - 把图像分类换成目标检测,用
yolov5的 PyTorch 版,数据标注工具用Labelme,毕业设计秒变“水果瑕疵检测”; - 把 ONNX 模型部署到微信小程序,答辩现场扫码即测,老师想不给你优秀都难。
真正动手才是毕业设计的开始,祝你早日“脱坑”,顺利把深度学习变成简历上的亮点。