
这次我们来看一个覆盖面非常典型的深度学习实战主题VGG/ResNet 图像分类、图像风格迁移、TextCNN 文本分类、图像字幕生成。这四个项目分别对应视觉基础模型、图像生成、文本分类、多模态理解四个方向恰好是算法工程师日常工作和面试中最常被问到的能力点。先给结论这四个项目都不需要特别夸张的硬件。一张 6G 显存的消费级显卡就能完成绝大部分训练和推理实验如果没有 GPUCPU 也能跑推理和少量轮次的训练只是速度会明显变慢。本文按“原理简介 环境准备 代码实现 效果验证 常见坑”的顺序逐个拆解每个项目都给出可运行的 PyTorch 代码骨架。文章偏工程落地不绕理论细节目标是让你看完之后能自己把四个项目跑起来并且知道每一步在验证什么。需要说明的是这四个项目不是某个统一仓库里的固定代码而是深度学习社区里非常成熟的四类经典任务。代码实现可以用 PyTorch、TensorFlow 或 PaddlePaddle本文以 PyTorch 2.x 为例因为它在学术复现和工程落地中使用最广泛。1. 核心能力速览项目技术方向核心模型典型任务硬件门槛关键验证点VGG/ResNet 图像分类CV 基础VGG16/19、ResNet18/50图像识别、迁移学习6G 显存够用CPU 可推理分类准确率、过拟合控制图像风格迁移CV 生成VGG19 优化/AdaIN艺术风格转换、内容保持6G 显存够用内容结构保留、风格纹理迁移TextCNN 文本分类NLP 基础TextCNN情感分析、意图识别CPU 即可完成大部分实验准确率、多分类效果图像字幕生成多模态CNN LSTM/Transformer图像描述、辅助阅读8G 显存起步建议 12G字幕语义正确性、BLEU 指标这四类项目有一个共同点它们的模型骨架都非常成熟重点不在于从零发明网络结构而在于数据准备、训练策略和结果验证。实际工程中绝大多数场景都是加载预训练权重做迁移学习或微调很少需要从头训练。2. 适用场景与使用边界这四个项目分别适合不同的人群和场景。VGG/ResNet 图像分类适合准备算法岗面试需要快速复现经典 CNN 结构。做安防、工业质检、遥感影像等领域的分类任务。理解迁移学习把 ImageNet 预训练权重迁移到自己的小数据集上。图像风格迁移适合做艺术滤镜类产品原型。学习感知损失Perceptual Loss和 Gram 矩阵的原理。快速生成风格化图片素材但商用前需要确认原图版权。TextCNN 文本分类适合NLP 入门者理解卷积在文本上的应用。舆情分析、垃圾评论过滤、情感分析等轻量级任务。对推理速度要求较高的场景TextCNN 比 BERT 快很多。图像字幕生成适合多模态方向入门。图像检索、辅助阅读、内容审核的辅助模块。需要理解 Encoder-Decoder 和注意力机制的项目。边界和合规提醒也要说清楚。第一风格迁移的输入图片涉及肖像或他人作品时需要获得授权。生成结果如果用于商用原图版权问题必须确认。第二TextCNN 训练的语料如果包含个人信息要做脱敏处理避免隐私泄露。第三图像字幕生成模型的可解释性有限在医疗、安防等高风险场景不能直接作为决策依据只能作为辅助提示。第四模型权重有各自的 License。例如 ImageNet 预训练权重和 torchvision 模型遵循相应许可商用前要确认合规性。3. 环境准备与前置条件3.1 操作系统与语言版本推荐 Ubuntu 20.04/22.04 或 Windows 10/11。macOS 可以跑部分实验但 GPU 加速受限。Python 建议 3.9 到 3.11。PyTorch 2.x 对这三个版本支持最稳定。3.2 CUDA 与 PyTorch 安装先确认显卡驱动和 CUDA 版本。在命令行执行nvidia-smi看到右上角 CUDA Version 之后去 PyTorch 官网选择对应的安装命令。以 CUDA 11.8 为例pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118如果是纯 CPU 环境pip install torch torchvision torchaudio安装完成后验证 GPU 是否可用python -c import torch; print(torch.__version__); print(torch.cuda.is_available())输出True说明 GPU 可用。3.3 依赖库清单pip install numpy pandas matplotlib pillow scikit-learn tqdm jieba其中jieba是中文分词工具TextCNN 项目会用到。其余是通用数据处理和可视化库。3.4 数据集准备建议准备图像分类CIFAR-10 或自定义小数据集。torchvision 可以直接下载 CIFAR-10。风格迁移一张内容图、一张风格图即可。TextCNN中文情感分类数据集如酒店评论、电商评论。图像字幕生成Flickr8k 或 COCO Captions 的子集。Flickr8k 体积较小适合实验。4. VGG/ResNet 图像分类实战4.1 原理速记VGG 的核心思路是用多个小卷积核堆叠替代大卷积核。VGG16 使用 13 个卷积层和 3 个全连接层全部卷积核都是 3x3。堆叠两个 3x3 卷积等价于一个 5x5 卷积的感受野堆叠三个等价于 7x7但参数量更少、非线性更强。ResNet 解决的是网络退化问题当网络层数增加到一定深度后训练集准确率反而下降。ResNet 引入残差连接输出 F(x) x其中F(x)是卷积层要学习的残差映射。当F(x) 0时网络至少可以保持恒等映射所以深层网络的性能不会差于浅层。4.2 加载预训练模型做迁移学习实际项目中不建议从零训练 ResNet18。正确做法是加载 ImageNet 预训练权重替换最后全连接层在自己的数据集上微调。import torch import torch.nn as nn import torchvision.models as models # 加载预训练 ResNet18 model models.resnet18(pretrainedTrue) # 获取最后一层全连接输入维度 num_features model.fc.in_features # 替换分类头假设数据集有 10 类 model.fc nn.Linear(num_features, 10) # 移动设备 device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device)这里也可以用 VGG16model_vgg models.vgg16(pretrainedTrue) # VGG 的分类头是 Sequential需要整体替换 model_vgg.classifier[6] nn.Linear(4096, 10)4.3 训练流程框架import torch.optim as optim from torchvision import datasets, transforms from torch.utils.data import DataLoader transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) train_dataset datasets.CIFAR10(root./data, trainTrue, downloadTrue, transformtransform) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers2) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr1e-4) for epoch in range(10): model.train() running_loss 0.0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) outputs model(images) loss criterion(outputs, labels) optimizer.zero_grad() loss.backward() optimizer.step() running_loss loss.item() print(fEpoch {epoch1}, Loss: {running_loss / len(train_loader):.4f})4.4 效果验证训练完成后在测试集上评估model.eval() correct 0 total 0 with torch.no_grad(): for images, labels in test_loader: images, labels images.to(device), labels.to(device) outputs model(images) _, predicted torch.max(outputs.data, 1) total labels.size(0) correct (predicted labels).sum().item() print(fTest Accuracy: {100 * correct / total:.2f}%)判断成功的标准在 CIFAR-10 上微调后的 ResNet18 通常能达到 90% 以上的测试准确率。VGG16 略低一点但差别不大。如果准确率低于 85%优先检查学习率、batch size 和数据增强配置。5. 图像风格迁移实战图像风格迁移有两种主流方案基于优化的方法Gatys 等和基于生成模型的方法Fast Neural Style、AdaIN。本文先讲基于优化的方法因为原理清晰、代码量少。5.1 原理速记这个方法使用 VGG19 作为特征提取器。定义两个损失内容损失生成图与内容图在某一深度层的特征图之间的 MSE 距离。特征距离越小内容结构越接近。风格损失计算生成图与风格图在各层的 Gram 矩阵差异。Gram 矩阵统计的是特征通道之间的相关性能捕捉纹理和颜色分布。总损失 内容损失权重 × 内容损失 风格损失权重 × 风格损失。5.2 代码实现import torch import torch.nn as nn import torch.optim as optim import torchvision.transforms as transforms import torchvision.models as models from PIL import Image # 加载 VGG19 特征提取层 class VGGFeatures(nn.Module): def __init__(self): super().__init__() vgg models.vgg19(pretrainedTrue).features self.blocks nn.ModuleList() for layer in vgg: if isinstance(layer, nn.MaxPool2d): # 遇到池化层时结束当前 block continue self.blocks.append(layer) def forward(self, x, layers): features [] for layer in self.blocks: x layer(x) if isinstance(layer, nn.Conv2d) and layer in layers: features.append(x) return features实际使用时更稳妥的方式是选取固定层例如content_layers {conv4_2} style_layers {conv1_1, conv2_1, conv3_1, conv4_1, conv5_1}5.3 损失计算def gram_matrix(feature): b, c, h, w feature.size() feature feature.view(b, c, h * w) gram torch.bmm(feature, feature.transpose(1, 2)) return gram / (c * h * w) def style_loss(gen_feat, style_feat): return torch.mean((gram_matrix(gen_feat) - gram_matrix(style_feat)) ** 2) def content_loss(gen_feat, content_feat): return torch.mean((gen_feat - content_feat) ** 2)5.4 迭代优化过程content_img load_image(content.jpg) style_img load_image(style.jpg) # 用内容图初始化生成图并设置 requires_grad gen_img content_img.clone().requires_grad_(True) optimizer optim.LBFGS([gen_img]) for step in range(300): def closure(): optimizer.zero_grad() gen_features vgg(gen_img, all_layers) loss content_weight * content_loss(gen_features[conv4_2], content_target[conv4_2]) for layer in style_layers: loss style_weight * style_loss(gen_features[layer], style_target[layer]) loss.backward() return loss optimizer.step(closure)这段代码用了LBFGS优化器收敛效果比 Adam 好。5.5 效果验证判断风格迁移成功的标准内容图的主体结构、轮廓、语义信息没有被破坏。风格图的颜色分布、纹理、笔触感迁移到了生成图上。没有出现明显的棋盘伪影或大范围噪声。迭代过程中 loss 稳定下降最后趋于平稳。如果效果不好优先调整三个超参数风格权重、内容权重、迭代次数。风格权重调大更接近风格图内容权重调大更保留原图结构。6. TextCNN 文本分类实战6.1 原理速记TextCNN 把文本看作一维序列。核心流程将词映射为词向量。用多个不同尺寸的卷积核并行提取 n-gram 特征。对卷积结果做全局最大池化。拼接特征并接全连接分类器。卷积核尺寸为 2 时提取二元词组特征尺寸为 3 时提取三元词组特征尺寸为 4 时提取四元词组特征。这是 TextCNN 比单尺寸卷积效果好的关键原因。6.2 中文数据预处理中文文本需要先分词。最简单的做法是使用jieba分词和词表构建import jieba from collections import Counter def tokenize_chinese(text): return list(jieba.cut(text)) # 构建词表 all_tokens [] for text in train_texts: all_tokens.extend(tokenize_chinese(text)) word_count Counter(all_tokens) vocab {word: idx 2 for idx, (word, _) in enumerate(word_count.most_common(50000))} vocab[PAD] 0 vocab[UNK] 1句子定长填充MAX_LEN 128 def encode_text(text, vocab, max_lenMAX_LEN): tokens tokenize_chinese(text) ids [vocab.get(w, vocab[UNK]) for w in tokens] ids ids[:max_len] ids ids [vocab[PAD]] * (max_len - len(ids)) return ids6.3 TextCNN 模型实现import torch import torch.nn as nn import torch.nn.functional as F class TextCNN(nn.Module): def __init__(self, vocab_size, embedding_dim, num_classes, num_filters256): super().__init__() self.embedding nn.Embedding(vocab_size, embedding_dim, padding_idx0) self.convs nn.ModuleList([ nn.Conv2d(1, num_filters, kernel_size(kernel_size, embedding_dim)) for kernel_size in [2, 3, 4] ]) self.fc nn.Linear(num_filters * 3, num_classes) self.dropout nn.Dropout(0.5) def forward(self, x): # x: (batch, seq_len) x self.embedding(x) # (batch, seq_len, embedding_dim) x x.unsqueeze(1) # (batch, 1, seq_len, embedding_dim) conv_results [] for conv in self.convs: conv_out conv(x) # (batch, num_filters, conv_len, 1) conv_out conv_out.squeeze(3) # (batch, num_filters, conv_len) pooled F.max_pool1d(conv_out, conv_out.size(2)).squeeze(2) conv_results.append(pooled) x torch.cat(conv_results, dim1) x self.dropout(x) return self.fc(x)6.4 训练与验证训练代码与图像分类类似这里只列出关键差异from torch.utils.data import Dataset, DataLoader class TextDataset(Dataset): def __init__(self, texts, labels, vocab): self.data [torch.tensor(encode_text(t, vocab)) for t in texts] self.labels torch.tensor(labels) def __len__(self): return len(self.labels) def __getitem__(self, idx): return self.data[idx], self.labels[idx]训练完成后除了准确率还要关注每个类别的 Precision、Recall、F1。文本分类中如果类别不均衡只看整体准确率会误导。判断成功的标准在中文情感二分类数据集上TextCNN 通常能到 90% 左右的准确率。多分类场景下 F1 值更有参考意义。如果效果不好优先检查分词质量、词表大小、卷积核数量。7. 图像字幕生成实战7.1 原理速记图像字幕生成是典型的多模态任务。整体结构分两部分编码器CNN如 ResNet提取图像特征得到一个特征向量序列或全局特征。解码器LSTM 或 Transformer 逐个生成字幕单词。训练时的输入是“图像特征 前一个单词”输出是“当前单词的概率分布”。推理时用贪心搜索或 beam search 逐个生成单词直到遇到EOS结束符。7.2 编码器实现import torch import torch.nn as nn import torchvision.models as models class ImageEncoder(nn.Module): def __init__(self, embed_size256): super().__init__() resnet models.resnet50(pretrainedTrue) # 去掉最后的全连接层和池化层保留卷积特征 self.conv_layers nn.Sequential(*list(resnet.children())[:-2]) self.fc nn.Linear(2048, embed_size) self.adaptive_pool nn.AdaptiveAvgPool2d((1, 1)) def forward(self, x): features self.conv_layers(x) # (batch, 2048, 7, 7) pooled self.adaptive_pool(features).squeeze(3).squeeze(2) # (batch, 2048) return self.fc(pooled) # (batch, embed_size)如果要用注意力机制就不做全局池化而是保留 7x7 的空间特征让解码器在每一步关注不同区域。7.3 解码器实现class DecoderLSTM(nn.Module): def __init__(self, embed_size, hidden_size, vocab_size, num_layers1): super().__init__() self.embed nn.Embedding(vocab_size, embed_size) self.lstm nn.LSTM(embed_size, hidden_size, num_layers, batch_firstTrue) self.fc nn.Linear(hidden_size, vocab_size) def forward(self, image_features, captions): # image_features: (batch, embed_size) embeddings self.embed(captions) # (batch, seq_len, embed_size) # 将图像特征作为 LSTM 的第一步输入 features image_features.unsqueeze(1) # (batch, 1, embed_size) inputs torch.cat([features, embeddings], dim1) # (batch, seq_len1, embed_size) lstm_out, _ self.lstm(inputs) logits self.fc(lstm_out) # (batch, seq_len1, vocab_size) return logits[:, :-1, :]7.4 推理生成def generate_caption(model, encoder, image, vocab, max_len30): model.eval() encoder.eval() with torch.no_grad(): features encoder(image.unsqueeze(0)) # (1, embed_size) captions [vocab[START]] hidden None for _ in range(max_len): x torch.tensor([[captions[-1]]]) embed model.embed(x) lstm_out, hidden model.lstm(features.unsqueeze(1), hidden) # 简化写法仅示意 logits model.fc(lstm_out) next_word logits.argmax(dim-1).item() if next_word vocab[EOS]: break captions.append(next_word) return [vocab_reverse[i] for i in captions]这里注意实际训练时 decoder 的 forward 已经处理了把图像特征拼在序列前面的逻辑推理时也需要保持一致。7.5 效果验证视觉上直接看生成的句子是否描述了图片内容。量化指标常用 BLEU 和 CIDEr。BLEU 衡量生成句子与人工标注句子之间的 n-gram 重合度。实现可以直接用torchtext或nltk的 BLEU 计算函数。判断成功的标准生成的句子主谓宾结构正确。描述的主体对象与图像主要内容一致。颜色、数量、位置等属性词基本准确。BLEU-1 在 Flickr8k 数据集上达到 0.5 以上是较正常水平具体取决于模型规模和训练时长。这个项目是最容易显存不足的。训练时建议先测试 batch size 为 16 是否能跑通再把 batch size 调整到显存能容纳的上限。8. 接口 API 与批量任务这四个项目完成训练后都可以封装成 HTTP 接口方便接入业务系统。这里给出一个通用的 FastAPI 封装示例以图像分类为例from fastapi import FastAPI, UploadFile, File from PIL import Image import torch import io app FastAPI() model None device torch.device(cuda if torch.cuda.is_available() else cpu) app.post(/predict) async def predict(file: UploadFile File(...)): image_data await file.read() image Image.open(io.BytesIO(image_data)).convert(RGB) # 预处理需要和训练时保持一致 tensor transform(image).unsqueeze(0).to(device) with torch.no_grad(): outputs model(tensor) pred outputs.argmax(dim1).item() return {class_id: pred, score: outputs.softmax(dim1).max().item()}批量任务的思路先准备一批输入文件逐条送入模型保存结果。注意批量任务的显存和内存管理建议用小 batch 循环并加入失败重试。import os import json from tqdm import tqdm input_dir ./test_images output_dir ./results os.makedirs(output_dir, exist_okTrue) for img_name in tqdm(os.listdir(input_dir)): try: image_path os.path.join(input_dir, img_name) result predict_single_image(image_path) with open(os.path.join(output_dir, img_name.split(.)[0] .json), w) as f: json.dump(result, f, ensure_asciiFalse) except Exception as e: print(fFailed: {img_name}, {e})批处理建议加日志记录方便定位哪一张图处理失败而不是直接中断。9. 资源占用与性能观察显存占用主要看三件事输入分辨率、batch size、模型参数量。VGG16 的参数量约 1.38 亿ResNet50 约 2556 万。同样的输入分辨率下VGG 显存占用明显更高。实测经验是224x224 输入、batch size 32 的 ResNet50 训练8G 显存可以跑通换成 VGG16 就需要下降到 16 甚至 8。风格迁移的显存占用很低。以 512x512 的内容图为例VGG19 前向传播一次通常不超过 2G 显存。因为整个过程只更新生成图不更新模型参数反向传播的内存开销比训练要小。TextCNN 在 CPU 上就能完成。词表 5 万、embedding 维度 300、卷积核 256 的情况下CPU 训练一个 epoch 通常在几分钟到十几分钟取决于数据量。图像字幕生成是四个项目里最吃显存的。ResNet50 编码器加上 LSTM 解码器如果同时训练编码器和解码器8G 显存建议 batch size 从 16 开始测试。如果显存不够可以冻结编码器只训练解码器这条经验在迁移学习中非常实用。如何实时观察显存占用命令行执行nvidia-smi -l 2每 2 秒刷新一次。训练时观察 GPU Memory Usage 是否接近上限。如果out of memory按顺序做三件事减小 batch size、降低输入分辨率、使用torch.cuda.empty_cache()释放缓存。10. 常见问题与排查方法问题现象可能原因排查方式解决方案PyTorch 安装后 CUDA 不可用驱动版本过低或安装的 CPU 版python -c import torch; print(torch.cuda.is_available())安装匹配 CUDA 版本的 PyTorch训练 loss 不下降学习率过大或过小打印前几个 batch 的 loss将学习率调整到 1e-4 到 1e-3 区间ResNet 准确率远低于预期数据预处理和训练时不一致检查 transform 是否完全一致确保测试集使用相同 Normalize 参数风格迁移出现彩色噪点迭代次数过少或风格权重过小可视化中间生成图提高风格权重或增加迭代次数TextCNN 显存溢出句子长度过长或 batch 过大查看 MAX_LEN 和 batch_size 设置调低 MAX_LEN 或 batch_size图像字幕生成效果差训练数据量不足检查数据集大小使用预训练 word embedding 或数据增强API 并发请求卡死没有用线程池或队列压测接口使用 FastAPI 异步或加队列批量任务中途失败部分文件损坏或格式不支持查日志定位失败文件增加异常捕获跳过并记录11. 最佳实践与使用建议把这四个项目跑通之后可以沉淀一套通用的深度学习项目结构project/ ├── config.yaml # 模型参数、路径配置 ├── data/ # 数据集和预处理脚本 ├── models/ # 网络结构定义 ├── scripts/ # 训练和推理脚本 ├── utils/ # 数据处理工具 └── output/ # 日志、权重、结果训练时第一个 epoch 先用很小的 batch 和很小步数验证代码能跑通再开完整训练。这个习惯能帮你省下大量排查时间。模型文件管理建议按“数据集 模型名 时间”命名例如cifar10_resnet18_20240101.pth。批量实验时每个实验记录当时的学习率、batch size、数据增强配置方便复现。接口服务部署时绑定到127.0.0.1而不是0.0.0.0避免外部网络直接访问。如果多个进程共用一张显卡建议加一个简单的请求队列防止显存瞬间打满。四个项目中最值得先跑的是 VGG/ResNet 图像分类因为代码最短、验证最快跑通之后你就理解了迁移学习、数据加载、训练循环、评估指标这一套完整流程后面三个项目都是在此基础上加特定模块。12. 总结与下一步这四类项目放在一起看其实是一条清晰的进阶路径图像分类是视觉基础风格迁移是理解特征和损失函数TextCNN 是 NLP 入门图像字幕生成是视觉和文本的交叉。跑通这四件事面试和工程里常见的“你有没有做过 CV/NLP/多模态项目”这个问题就有内容可讲了。建议先做一件事把第 4 节的 ResNet18 分类代码完整跑一遍。这一步打通之后剩余项目在数据准备和训练循环上都能复用。最容易踩的坑集中在环境安装和数据预处理网络结构本身很少出错。如果项目跑到一半卡住优先检查输入数据的 shape 是否与模型期望一致。下一步的扩展方向可以从两条线走。一条是模型升级把 ResNet 换成 ViT、把 TextCNN 换成 BERT、把 LSTM 解码器换成 Transformer。另一条是工程化给模型接入 FastAPI 接口、加批量任务队列、用 ONNX 导出加速推理。把这个主题里的基础能力消化完再往这两个方向延伸会比较顺手。