简介:这份深度学习项目实例合集面向想动手实践神经网络的初学者与进阶者,以21个完整项目串起数据预处理、模型构建、训练调优与评估部署的典型流程,覆盖DNN、CNN、RNN/LSTM、自编码器及GAN等主流模型。资源共911个文件,压缩包约55.81MB;511个Python脚本构成核心代码,辅以ipynb交互式笔记本、md文档、jpg/png图像样本,以及dockerfile、proto、pbtxt、yml等配置与部署文件,便于对照学习与二次修改。包内还包含src/tgt、label/ref等序列数据,以及TensorFlow相关格式文件,可支撑序列任务演练与模型导出场景。已有6702人学习下载。跟随这些项目完整走一遍,可同时积累激活函数、反向传播、损失计算、优化器选择、超参数调优与结果评估等关键能力,为后续独立开展深度学习实验打下扎实基础。
1. 深度学习项目实例不是看完的,是照着敲完的
第一次拿到这份“深度学习21个项目实例”时,我并没有急着写代码,而是花了一个晚上把每个项目的任务类型和数据集结构过了一遍。原因很简单:学深度学习的坑几乎都在数据准备和训练调参上,模型结构反而是最不费劲的部分。这份资源的好处是它把常见的图像分类、目标检测、文本处理和生成式任务都串了一遍,适合已经掌握 Python 基础、想用真实项目过渡到完整训练流程的人。如果你能接受“先复现再理解”的学习方式,它比单独看论文或刷教程的效率高得多。这 21 个实例覆盖了从入门到进阶的完整梯度,值得你花时间一个个敲完。
2. 项目清单与框架选型:先看清 21 个项目各自解决什么问题
2.1 按任务类型拆解,训练侧重点完全不同
这 21 个项目如果按任务类型划分,大致可以分成四类:图像类任务、文本与序列类任务、生成式任务和综合项目。
图像类任务大约占了 8 个左右。入门项目是一个简单的图像分类,数据集用的是某个开源小规模图像集,每个类别只有几百张图。这个项目的目的不是刷精度,而是让你完整走一遍数据读取、模型定义、训练和验证的闭环。接下来是目标检测、图像分割、人脸检测和图像风格迁移,其中行人检测和语义分割是两个比较有代表性的项目。这类任务练的是数据管线和训练流程的掌控力。
文本与序列类任务大约有 6 个,包含情感分析、中文文本分类、文本生成、命名实体识别等。核心都围绕 Tokenization、Embedding 和序列模型展开。这类项目里最容易踩坑的地方是文本的 padding 和 mask 处理,后面我会专门提到。
生成式任务有 4 个,主要是 GAN 的变体,包括手写数字生成、人像生成和超分辨率重建。这类项目除了模型结构,还涉及训练稳定性问题——生成器和判别器的平衡非常敏感,适合在完成基础任务后挑战。最后还有几个综合项目,比如一个跨平台系统的端到端实现,会把前面拆开的视觉、文本模块拼到一起,练的是模块化整合能力。
这种按任务类型区分的方式很关键,因为它决定了你复现的时候应该关注什么。图像分类项目最值得学习的是数据管线和训练循环怎么写;目标检测项目重点在于锚框机制和后处理 NMS;GAN 项目则要关注训练稳定性。先把每个项目的技术侧重点列出来,再动手,效率会高很多。
2.2 框架选型:PyTorch 还是 TensorFlow?
这 21 个项目里大部分代码用的是 PyTorch,少量老项目是 TensorFlow 1.x。如果你现在才开始复现,我建议统一在 PyTorch 环境里跑,原因有三条:第一,PyTorch 的资料和社区讨论是当前最活跃的,遇到报错好搜;第二,项目中的代码多数是从旧版升级过来的,PyTorch 的 API 变动比 TensorFlow 1.x 到 2.x 的迁移成本低;第三,调试体验好——你可以直接打进训练循环里看中间张量的 shape 和数值,这在静态图模式下要麻烦得多。
环境配置的常见做法是用 Anaconda 建独立环境,把系统 Python 隔离在项目之外。下面是我一般会用的命令:
conda create -n dl21 python=3.8 conda activate dl21 pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118 pip install numpy pandas matplotlib scikit-learn opencv-python tqdmPython 3.8 是我推荐的版本,因为项目里的代码大多是在 3.6 到 3.9 之间写的,兼容性最好。PyTorch 的安装命令需要根据你的显卡驱动版本选择 CUDA 版本,如果不确定就先不装 GPU 版,把项目流程跑通再考虑加速。记住一个原则:先让代码跑起来,再追求训练速度。
2.3 代码结构:通用的 train/val/test 三段式
把这 21 个项目打开看下来,你会发现绝大多数代码组织是高度一致的:项目文件夹下分为 data、models、utils、checkpoints 四个子目录。data 目录存放原始数据和预处理脚本,models 目录放网络结构定义,utils 目录放各种工具函数,checkpoints 目录保存训练好的权重文件。
project_root/ ├── config.py # 全局配置:路径、超参数 ├── data/ │ ├── dataset.py # 数据加载与预处理 │ └── preprocess.py # 划分训练集/验证集 ├── models/ │ ├── cnn.py # 网络结构定义 │ └── resnet.py # 预训练模型封装 ├── utils/ │ ├── metrics.py # 准确率、F1等评估指标 │ └── visualize.py # 训练曲线绘图 └── checkpoints/ └── best_model.pth # 最优权重这种结构的优点在于,不管做的是图像任务还是文本任务,训练脚本的主流程都遵循一个固定模板:
for epoch in range(epochs): model.train() for batch_x, batch_y in train_loader: optimizer.zero_grad() outputs = model(batch_x) loss = criterion(outputs, batch_y) loss.backward() optimizer.step() # 每个epoch结束后在验证集上评估一次 val_loss, val_acc = evaluate(model, val_loader) print(f"Epoch {epoch + 1}: val_loss={val_loss:.4f}, val_acc={val_acc:.4f}")这个模板看起来简单,但它是深度学习项目中最容易被忽略的部分。很多新手在写训练循环时会把模型评估漏掉,结果训练完才发现没有验证集的评估逻辑,只能重新跑一遍。所以我在复现每个项目之前,都会先确认项目的 train 脚本里是不是有 eval 这一步,没有就自己补上。验证集的存在很重要——它决定了你是在调模型还是在盲调超参数。
3. 复现一个图像分类项目:从数据增强到准确率曲线
3.1 数据加载:手写预处理脚本还是用官方接口?
图像分类项目是最适合第一个复现的,因为流程最短、最容易看到结果。我一般会把训练数据先做一次统计分析,看看每个类别的样本数是否平衡。不平衡的数据会直接影响模型收敛速度,比如某个类别只有几十张图,另一个类别有几百张,模型会对大类别产生严重偏向。这种情况下要么做数据增强补偿,要么在损失函数里设置类别权重。
数据加载部分,项目里的常见做法是用 PyTorch 的 Dataset 和 DataLoader 来构建数据管道。下面是一段简化后的数据加载代码:
import os import torch from torch.utils.data import Dataset, DataLoader from PIL import Image import torchvision.transforms as T class ImageFolderDataset(Dataset): def __init__(self, root_dir, transform=None): self.root_dir = root_dir self.transform = transform self.classes = sorted(os.listdir(root_dir)) self.class_to_idx = {cls: idx for idx, cls in enumerate(self.classes)} self.samples = [] for cls_name in self.classes: cls_path = os.path.join(root_dir, cls_name) for file_name in os.listdir(cls_path): if file_name.endswith(('.jpg', '.png')): self.samples.append((os.path.join(cls_path, file_name), self.class_to_idx[cls_name])) def __len__(self): return len(self.samples) def __getitem__(self, idx): path, label = self.samples[idx] image = Image.open(path).convert('RGB') if self.transform: image = self.transform(image) return image, label transform_train = T.Compose([ T.Resize((224, 224)), T.RandomHorizontalFlip(p=0.5), T.ColorJitter(brightness=0.2, contrast=0.2), T.ToTensor(), T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) train_dataset = ImageFolderDataset('data/train', transform=transform_train) train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=4)这个数据集类的写法是手工扫描文件夹结构,每个文件夹名对应一个类别标签。__getitem__返回的是张量化的图像和整数标签,DataLoader 负责把单个样本组成批次。参数说明:num_workers=4表示用 4 个子进程加载数据,如果你的机器 CPU 核心少可以改成 2;如果内存不够就改成 0,退回主进程加载。
训练和验证用的 transforms 通常不同。训练时用 RandomHorizontalFlip 做随机翻转,用 ColorJitter 做颜色扰动,这属于数据增强;验证时只用 Resize 和 Normalize,不做随机变换,否则评估结果不稳定。这一点很多新手容易踩坑,把增强用到了验证集上,导致每个 epoch 评估的准确率忽高忽低。
3.2 模型与训练超参数:先跑通再调优
图像分类项目里,最基础的模型可以从一个两层卷积网络开始,然后再切换到预训练的 ResNet。两层卷积网络代码量少,适合调试训练流程:
import torch.nn as nn class SimpleCNN(nn.Module): def __init__(self, num_classes=10): super().__init__() self.features = nn.Sequential( nn.Conv2d(3, 32, kernel_size=3, padding=1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.ReLU(), nn.MaxPool2d(2), ) self.classifier = nn.Sequential( nn.Flatten(), nn.Linear(64 * 56 * 56, 128), nn.ReLU(), nn.Dropout(0.5), nn.Linear(128, num_classes) ) def forward(self, x): x = self.features(x) return self.classifier(x)输入是 224x224 的 RGB 图像,经过两次卷积和池化后,特征图尺寸变成 56x56,通道数是 64,所以全连接层的输入维度是 645656=200704。这个数字要根据你的输入尺寸重新算,改输入尺寸时最容易在这里翻车——特征图尺寸和全连接层维度不匹配,PyTorch 会直接报 shape mismatch 错误。所以当你想把输入改成 128x128 或 512x512 时,记得同步改全连接层的输入维度。
训练超参数的建议:初始学习率 1e-3,使用 Adam 优化器,权重衰减设为 1e-4。批次大小先定 32,如果显存不够就降到 16。训练轮数先跑 20 轮,每轮打印一次验证集表现,如果验证准确率还在上升就继续加轮数。这种先跑通再调优的思路,比一开始就追求大模型快得多。
3.3 训练曲线可视化:判断模型是否真的在学
项目里通常带一个简单的可视化脚本,用来记录训练损失和验证准确率。这一步虽然简单,但对判断模型状态非常关键:
import matplotlib.pyplot as plt def plot_curves(train_losses, val_accs, save_path='curves.png'): fig, ax1 = plt.subplots(figsize=(10, 4)) ax1.plot(train_losses, label='Train Loss', color='tab:red') ax1.set_xlabel('Epoch') ax1.set_ylabel('Loss', color='tab:red') ax2 = ax1.twinx() ax2.plot(val_accs, label='Val Acc', color='tab:blue') ax2.set_ylabel('Accuracy', color='tab:blue') plt.tight_layout() plt.savefig(save_path)看完曲线的判断逻辑是:训练损失下降但验证准确率不涨,大概率是过拟合或者学习率过大;训练损失和验证准确率都不动,大概率是数据预处理或模型结构有问题,不是调参能解决的;训练损失下降很快但验证准确率震荡大,大概率是验证集太小或批次大小不稳定。这三个情况对应的处理方式完全不同,所以曲线可视化不是可有可无的装饰,而是排错的第一手证据。每训练完一个项目,我都会把曲线存成一个文件,方便后面对比不同实验设置的效果。
4. 目标检测项目实战:标注格式、锚框参数与置信度阈值
目标检测项目比图像分类复杂一个量级。我在复现数据集里的行人检测项目时,卡了相当久的时间,主要原因倒不是模型结构,而是数据标注格式和训练参数之间的匹配问题。如果你跳过图像分类直接上手目标检测,很容易被这一堆概念绕晕。建议至少先跑通一个分类项目再进这一部分,代码能力和调参手感都会好很多。
4.1 标注格式转换:从 XML 到统一 JSON
项目里的目标检测数据集通常以 VOC 格式存放,每张图像对应一个 XML 标注文件,里面是目标的类别和边界框坐标。但是训练脚本需要的往往是一个统一的 JSON 文件或者 CSV 文件,所以第一步是把标注信息提取出来。
import xml.etree.ElementTree as ET import os import json def voc_to_json(xml_dir, image_dir, output_path): labels = [] class_names = ['person', 'car', 'bicycle'] for xml_name in os.listdir(xml_dir): if not xml_name.endswith('.xml'): continue tree = ET.parse(os.path.join(xml_dir, xml_name)) root = tree.getroot() image_name = root.find('filename').text size = root.find('size') width = int(size.find('width').text) height = int(size.find('height').text) boxes = [] for obj in root.findall('object'): cls = obj.find('name').text if cls not in class_names: continue bndbox = obj.find('bndbox') xmin = float(bndbox.find('xmin').text) ymin = float(bndbox.find('ymin').text) xmax = float(bndbox.find('xmax').text) ymax = float(bndbox.find('ymax').text) boxes.append({'class': cls, 'bbox': [xmin, ymin, xmax, ymax]}) labels.append({'image': os.path.join(image_dir, image_name), 'width': width, 'height': height, 'boxes': boxes}) with open(output_path, 'w', encoding='utf-8') as f: json.dump(labels, f, ensure_ascii=False, indent=2)这段代码的作用是把散落在 XML 文件里的标注统一成一份 JSON,后面训练脚本直接从 JSON 读数据。参数说明:class_names列表要和你数据集里的类别完全一致,漏掉一个类别训练脚本就会在读取时直接报键错误。还有一个容易被忽略的点:有的 XML 文件的 filename 字段只写了文件名,没有目录前缀,最好统一处理成完整的相对路径,否则后面打开图片时找不到文件。
图像尺寸方面,项目里原始图像一般是接近高清的分辨率,但训练时会把图像缩放到固定尺寸如 640x640。缩放的逻辑在数据加载脚本里,坐标需要同步缩放,否则边界框会和图像内容错位。常见做法是记录原始尺寸和缩放后的尺寸比例,所有坐标都乘以这个比例。这个步骤如果你偷懒不做,训练 loss 会表现得很诡异——看起来在下降,但推理结果完全不对。
4.2 预训练权重与锚框参数调整
目标检测模型在数据集规模不大时,强烈建议加载预训练权重。这个项目提供的预训练权重是在大规模通用数据集上训练好的,直接从头训练效果会差很多,收敛速度也慢。加载权重的常见做法是:
model = get_detection_model(num_classes=len(class_names)) state_dict = torch.load('weights/pretrained.pth') model.load_state_dict(state_dict, strict=False)strict=False是关键参数。预训练模型的类别数和你的任务不一定相同,比如预训练权重有 80 类,你只需要 3 类,最后一层结构不一致。strict=False会让 PyTorch 跳过不匹配的层,把骨干网络的参数加载进来,只随机初始化分类头。如果不加这个参数,大概率报错说键不匹配,这是第一个坑。
锚框参数是另一个需要动手调整的地方。默认锚框是为通用检测任务设计的,尺寸偏大,如果检测目标是小物体,需要缩小锚框。
# 训练配置示例 anchor_sizes = [[16, 24], [32, 48], [64, 96]] # 每层特征图上的锚框尺寸 anchor_strides = [4, 8, 16] # 锚框的采样步长 iou_threshold = 0.5 # 正样本匹配阈值锚框尺寸的含义是:模型会在特征图的每个位置生成多个不同大小的预测框,这些预测框的实际物理尺寸要乘以步长。如果你的目标通常是行人,尺寸约在 40x100 像素左右,那锚框就应该围绕这个区间来设置,而不是沿用默认的通用参数。判断锚框是否合适的粗暴办法是统计训练集标注框的平均宽高,让锚框覆盖这个分布。统计代码几行就能写完,但能省掉后面大量的无效调试时间。
4.3 后处理与置信度阈值:NMS 和推理效果的平衡
目标检测推理阶段有两个关键参数:置信度阈值和 IoU 阈值。置信度阈值太低会输出大量假阳性框,太高会漏检;NMS 的 IoU 阈值决定相邻框是保留还是合并。
def postprocess(predictions, conf_threshold=0.5, nms_iou_threshold=0.4): filtered_boxes = [] filtered_scores = [] filtered_labels = [] for pred in predictions: boxes, scores, labels = pred keep = scores > conf_threshold boxes = boxes[keep] scores = scores[keep] labels = labels[keep] for cls in set(labels.tolist()): cls_mask = labels == cls keep_idx = torchvision.ops.nms( boxes[cls_mask], scores[cls_mask], nms_iou_threshold) filtered_boxes.extend(boxes[cls_mask][keep_idx].tolist()) filtered_scores.extend(scores[cls_mask][keep_idx].tolist()) filtered_labels.extend([cls] * len(keep_idx)) return filtered_boxes, filtered_scores, filtered_labels这里 NMS 按类别分别执行,是因为不同类别的目标可能重叠出现在同一位置,比如人和他骑的自行车,如果跨类别执行 NMS 会把其中一个框误删掉。参数说明:conf_threshold一般先用 0.5,然后根据可视化结果微调。假阳性多就调高到 0.6 或 0.7,漏检多就调低到 0.3 左右。nms_iou_threshold默认 0.4 到 0.5 之间,超过 0.5 会导致相邻重叠框被保留,输出看起来杂乱。调这个阈值时注意观察可视化结果,而不是只用 mAP 数字来判断。
5. 避坑指南:复现这 21 个项目最常见的五个翻车现场
5.1 验证集准确率一直是随机水平
现象:复现第一个图像分类项目时,某开发者训练了 15 个 epoch,验证准确率一直在 10% 左右徘徊(数据集有 10 个类别,随机猜也是 10%)。模型完全没学到东西。
原因:排查后发现,数据加载代码里忘记做 Normalize,图像像素值范围是 0 到 255,而模型期望的是 0 到 1。输入分布和模型期望差异太大,梯度计算方向基本是随机的,模型自然学不了东西。
解决:把 Normalize 加回 transforms 里,同时检查 mean 和 std 参数是否合理。从那以后我每开始一个新项目,第一件事就是打印一批数据的 mean 和 std,确认输入分布符合预期。
5.2 显存溢出报错 CUDA out of memory
现象:训练目标检测项目时,batch_size 设成 16,输入图像 640x640,模型体积比较大,运行十几个 step 之后就报 CUDA out of memory。
原因:不是模型太大,而是 batch_size 和输入尺寸的组合超过了显存容量。可能还有一个隐藏原因——验证阶段同时占着显存,训练和验证交替时峰值显存翻倍。
解决:先把 batch_size 降到 8 或 4,如果还是溢出再检查图像尺寸是否被意外放大。推理和验证时记得加with torch.no_grad(),这在大多数训练框架里是标配,但新手容易漏。如果用了梯度累积,那么每 N 个 step 累积一次,等效批次大小是batch_size * N,显存占用不会因此降低,所以梯度累积不是为了省显存,而是为了模拟更大的批次。
5.3 加载预训练权重时报键不匹配
现象:报错信息大概是size mismatch for classifier.weight: copying a param with shape torch.Size([1000, 512]) from checkpoint, the shape in current model is torch.Size([3, 512])。
原因:预训练模型是在 1000 类任务上训练的,分类头输出是 1000 维,你的任务只有 3 个类别。
解决:用load_state_dict(state_dict, strict=False)让 PyTorch 只加载形状匹配的层,分类头用随机初始化。另外,如果连骨干网络的层名都不一致,说明代码里的模型结构和预训练权重不是同一个框架,需要检查主干网络名称是否一致。这种情况就要回到项目里看模型的实现细节,不完全是权重加载的问题。
5.4 训练 loss 下降但验证 loss 不降反升
现象:训练集 loss 稳步下降,但每个 epoch 结束后的验证 loss 不降反升,差距越来越大。
原因:可能是过拟合,也可能只是学习率太大导致在最优解附近震荡。先看训练集 loss 是否也在震荡,如果训练集 loss 平稳下降、验证集 loss 上升,那基本就是过拟合;如果训练集 loss 也在波动,那就是学习率问题。
解决:过拟合最直接的手段是增强数据、增加 Dropout、或加早停。如果训练 loss 也有震荡,把学习率降到原来的五分之一,比如从 1e-3 降到 2e-4,再观察两个 epoch。早停这个技巧在不少项目里被忽略了,但它简单有效,我一般会在验证集表现连续 5 轮没有提升时,直接保存当前最优权重然后停止训练,能给复现流程省下不少时间。
5.5 随机种子没固定,结果不可复现
现象:训练脚本里没有固定随机种子,同一份代码跑两次,验证准确率差 0.3 个百分点。这在调试时很容易让人误判为代码逻辑问题,其实只是随机性在作怪。
原因:深度学习训练过程涉及 Dropout 的随机屏蔽、数据加载的随机 shuffle、权重初始化等多个随机源。没有固定种子,每次运行的结果都会不同。
解决:固定所有随机源,下面是完整写法:
import random import numpy as np import torch 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注意cudnn.deterministic=True会牺牲一点性能,换取计算结果的可重复性。如果训练速度变得很慢,可以在正式训练时把它关掉,只在调试时开启。那之后我每开始一个新项目,第一行代码就是调用set_seed(),这个习惯值得养成。
6. 把 21 个项目吃透后的进阶技巧:把训练好的模型搬到自己的数据集时,只需要改三个东西
等你把 21 个项目逐个跑通之后,会发现绝大多数脚本的核心逻辑都一样,真正需要改的只有三个地方:数据加载、类别映射和输出层。掌握了这三处,你就能把别人的项目迁移到自己的场景里。
第一个改数据加载。不同数据集有不同目录结构和文件格式,但加载逻辑本质上就是实现一个 Dataset 类,提供__len__和__getitem__两个方法。把原始数据读进来,做图像预处理,返回张量和标签,就够了。如果你要处理的是文本数据,把图像读取换成 tokenizer 调用即可。
第二个改类别映射。把原来项目里的class_names替换成你自己的类别清单。注意保留原来的class_to_idx映射方式,保证输出的整数标签和模型输出的索引一一对应。漏改或者改错顺序,训练不会报错,但推理结果的标签会完全错位,这种 bug 很难排查。
第三个改输出层。原项目模型的最后一层输出维度通常和它原本的类别数一致。你换了任务之后,把最后的全连接层或分类头的输出维度改成你自己的类别数,再加载预训练权重时用strict=False,就能保留骨干网络的参数。如果你的数据集比较小,比如每个类别只有一两百张图,建议固定骨干网络参数,只训练分类头。方法很简单:对骨干网络的参数设置requires_grad = False,优化器只传入分类头的参数。
分享一个具体的习惯:我后来每次迁移项目到新数据集时,都会强制走一遍这个流程——先统计数据分布、再画一条训练曲线、最后固定随机种子。这三步构成了一个不太容易出错的底盘。有一次我以为自己已经很熟这个流程了,跳过数据分布统计直接训练,结果类别不平衡导致准确率虚高,差一点就把一个明显有偏的模型带到了演示现场。从那以后,我再也不敢跳过数据检查这一步。这 21 个项目教会我的最重要的一件事,不是某个模型怎么写,而是每次训练前都把数据、映射和输出这三件事检查到位。希望帮到你。
本文还有配套的精品资源,点击获取