ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

深度学习四大经典项目实战:图像分类、风格迁移、TextCNN与图像字幕生成

深度学习四大经典项目实战:图像分类、风格迁移、TextCNN与图像字幕生成 很多想入门深度学习的读者经常卡在“看了很多理论但不知道代码怎么写”这个阶段。尤其是面试和实际业务中经常遇到的几类任务图像分类、图像风格迁移、文本分类、图像字幕生成很多人的痛点在于——每个方向都有一堆零散资料但缺少一条能把模型原理和工程代码串起来的主线。本文围绕 VGG/ResNet、图像风格迁移、TextCNN 文本分类、图像字幕生成四个经典深度学习项目从环境配置、模型设计到核心代码实战一次讲透适合目前正在学习深度学习或者准备算法岗的读者。1. 背景与核心概念1.1 为什么选这四个项目深度学习发展到现在算法岗位已经不再要求你只会调用某个框架而是希望你理解经典模型之后能把它迁移到不同任务上。VGG 和 ResNet 是 CNN 的两块基石。VGG 让“小卷积核 更深的网络”成为主流ResNet 通过残差连接解决了网络加深后出现的退化问题。图像风格迁移是生成任务的入门它涉及“内容”和“风格”的分离能帮助理解特征图在卷积网络内部是怎么表达的。TextCNN 把 CNN 用到文本领域是 NLP 分类任务的经典套路适合理解“如何把文本转换成模型可处理的矩阵”。图像字幕生成属于跨模态任务输入图片输出文字描述它需要把 CNN 和 RNN/Transformer 组合起来是理解 encoder-decoder 架构的绝佳案例。这四个项目正好覆盖了视觉、文本、跨模态三个方向在简历和实际业务中都很有分量。1.2 你需要掌握的基础知识开始之前建议你具备以下基础会使用 Python了解 numpy 的基本操作。了解深度学习基本概念卷积、池化、全连接、反向传播、梯度下降。能读懂 PyTorch 的基本代码Dataset、DataLoader、Module、优化器。了解训练集、验证集、测试集的划分思路。如果你还不太熟悉 PyTorch也没关系本文会给你完整的可运行代码你可以先照着跑通再逐步理解每一行。2. 环境准备与版本说明2.1 运行环境不同操作系统和显卡环境下CUDA、PyTorch 版本存在差异。下面给出一个常见组合你可以根据自己机器的实际情况调整操作系统Ubuntu 20.04 或 Windows 10/11Python 版本3.8 或 3.9、3.10CUDA11.x 或 12.x如果没有 NVIDIA GPU可以使用 CPU 版本PyTorch2.xtorchvision与 PyTorch 版本对齐如果你使用的是 Windows在安装 PyTorch 时建议到 PyTorch 官网通过命令自动生成合适的安装方式。例如pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118这里以 CUDA 11.8 为例。如果使用 CPU 环境则安装 CPU 版本pip install torch torchvision torchaudio安装完成后可以用下面命令验证版本python -c import torch, torchvision; print(torch.__version__, torchvision.__version__)2.2 项目结构建议为了每个项目都能独立运行建议按下面的目录结构组织代码deep_learning_projects/ ├── 01_visual_classification/ │ ├── train.py │ └── model.py ├── 02_style_transfer/ │ ├── style_transfer.py ├── 03_textcnn_classification/ │ ├── data_loader.py │ ├── model.py │ └── train.py └── 04_image_captioning/ ├── model.py └── train_demo.py3. 核心模型原理解读3.1 VGG用卷积深度换精度VGG 的精髓是连续堆叠 3x3 的小卷积核。两个 3x3 卷积叠加感受野相当于一个 5x5 卷积三个 3x3 卷积叠加感受野相当于一个 7x7 卷积。但是小卷积核参数更少非线性更强所以 VGG 通过增加深度来提升表达力。VGG 网络结构清晰常用版本是 VGG16 和 VGG19。它的缺点是参数量很大前两个全连接层各占了很多参数所以后续很多模型用全局平均池化代替全连接。3.2 ResNet解决网络退化问题网络越深梯度消失和退化问题越严重。ResNet 引入残差结构[ y F(x) x ]其中 (F(x)) 是经过卷积、归一化、激活函数之后的结果(x) 是输入。这样求梯度时即使 (F(x)) 的梯度很小还有一个恒等映射的梯度可以直接传递到前面从而保证深层网络也能正常训练。ResNet 常见版本有 ResNet18、ResNet34、ResNet50、ResNet101 等。数字越大层数越深计算量也越大。在图像分类任务中通常先在 ImageNet 上预训练再迁移到自己的数据集上做微调。3.3 图像风格迁移内容与风格分离图像风格迁移的目标是把一张内容图片的内容和一张风格图片的风格合在一起生成一张新图片。它利用的是 CNN 中不同层次的特征图浅层特征保留较多空间结构和边缘信息适合表达“内容”。深层特征保留更多语义信息也包含部分纹理模式。风格通过 Gram 矩阵来刻画它计算特征图通道之间的相关性能捕捉纹理和颜色分布。内容损失使用生成图与内容图在指定层特征之间的 L2 距离风格损失使用生成图与风格图的 Gram 矩阵之间的 L2 距离。最终总损失为两者加权和。3.4 TextCNN用卷积处理文本TextCNN 把每个词映射成词向量得到一个[batch_size, seq_len, embedding_dim]的张量然后在句子长度方向上做一维卷积。它通常使用多个不同大小的卷积核例如 3、4、5相当于在捕捉不同长度的 n-gram 信息。每个卷积核会在整个序列上滑动提取局部特征经过池化后拼接起来最后接全连接层完成分类。TextCNN 结构简单、训练速度快适合情感分类、意图识别、文本匹配等任务。它的缺点是缺少上下文依赖建模不像 BERT 那样能捕捉复杂的语义关系。3.5 图像字幕生成CNN RNN图像字幕生成Image Captioning需要模型把图片转换为一段自然语言描述。常见结构是Encoder使用 CNN如 ResNet提取图片特征通常取最后一个卷积层的特征图或者用全局平均池化得到图片的向量表示。Decoder使用 RNN、LSTM 或 Transformer 逐词生成描述。训练时将图片特征作为 Decoder 的初始状态或每步输入的一部分让模型根据已生成的词和图片特征预测下一个词。经典论文 Show and Tell 使用的是 CNN LSTM后来很多模型引入 Attention 机制让 Decoder 在生成每个词时关注图片中不同区域。4. 完整实战案例4.1 使用 ResNet 进行图像分类4.1.1 数据准备这里使用 torchvision 内置的 CIFAR-10 数据集做示例。如果你使用自己的数据只需要把数据整理成普通文件夹结构即可data/ ├── train/ │ ├── cat/ │ │ ├── cat_001.jpg │ └── dog/ └── val/ ├── cat/ └── dog/CIFAR-10 数据集的加载如下import torchvision import torchvision.transforms as transforms 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 torchvision.datasets.CIFAR10( root./data, trainTrue, transformtransform, downloadTrue ) val_dataset torchvision.datasets.CIFAR10( root./data, trainFalse, transformtransform, downloadTrue ) train_loader torch.utils.data.DataLoader( train_dataset, batch_size64, shuffleTrue, num_workers4 ) val_loader torch.utils.data.DataLoader( val_dataset, batch_size64, shuffleFalse, num_workers4 )这里使用了 ImageNet 数据集的均值和标准差做归一化因为接下来要加载在 ImageNet 上预训练的模型。4.1.2 加载预训练 ResNetimport torch import torch.nn as nn import torchvision.models as models model models.resnet18(pretrainedTrue) # 将最后的全连接层改成 10 类 model.fc nn.Linear(model.fc.in_features, 10) model model.cuda()注意pretrainedTrue在新版本 torchvision 中可能会推荐使用weightsmodels.ResNet18_Weights.DEFAULT建议根据 torchvision 版本调整model models.resnet18(weightsmodels.ResNet18_Weights.DEFAULT)4.1.3 训练代码下面是一个简化版训练流程import torch.optim as optim criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr1e-4) def train_one_epoch(model, loader, criterion, optimizer): model.train() total_loss 0 correct 0 total 0 for images, labels in loader: images, labels images.cuda(), labels.cuda() optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() total_loss loss.item() _, predicted outputs.max(1) total labels.size(0) correct predicted.eq(labels).sum().item() return total_loss / len(loader), 100.0 * correct / total for epoch in range(3): loss, acc train_one_epoch(model, train_loader, criterion, optimizer) print(fEpoch {epoch1}: loss{loss:.4f}, acc{acc:.2f}%)这个示例只训练 3 个 epoch效果不一定好但已经足够说明训练流程。实际项目中会训练几十个 epoch并配合学习率衰减。4.2 图像风格迁移实战4.2.1 加载预训练 VGG图像风格迁移一般使用 VGG19 的某些层特征。这里需要从 torchvision 中加载预训练模型并提取指定层的输出。import torch import torch.nn as nn import torchvision.models as models import torchvision.transforms as transforms from PIL import Image import copy device torch.device(cuda if torch.cuda.is_available() else cpu) cnn models.vgg19(pretrainedTrue).features.to(device).eval()4.2.2 定义损失模块我们需要一个模块在向前计算时保存指定层的特征图class VGGFeatures(nn.Module): def __init__(self, cnn, content_layers, style_layers): super().__init__() self.layers nn.ModuleList() self.content_layers content_layers self.style_layers style_layers model cnn self.slice1 nn.Sequential() self.slice2 nn.Sequential() self.slice3 nn.Sequential() self.slice4 nn.Sequential() for name, layer in model._modules.items(): if name 0: self.slice1.add_module(name, layer) elif name in [1, 2, 3]: self.slice2.add_module(name, layer) elif name in [4, 5, 6]: self.slice3.add_module(name, layer) elif name in [7, 8]: self.slice4.add_module(name, layer) def forward(self, x): h x outputs {} h self.slice1(h) outputs[conv1_1] h h self.slice2(h) outputs[conv2_1] h h self.slice3(h) outputs[conv3_1] h h self.slice4(h) outputs[conv4_1] h return outputs为了更简洁你也可以直接使用预训练模型中一个nn.ModuleList在 forward 里判断当前层名称class VGGFeatures(nn.Module): def __init__(self, cnn, layer_names): super().__init__() self.features cnn self.layer_names layer_names def forward(self, x): outputs {} for name, layer in self.features._modules.items(): x layer(x) if name in self.layer_names: outputs[name] x return outputs这种写法更容易理解。我们需要的内容层和风格层名称如下content_layers [conv4_2] style_layers [conv1_1, conv2_1, conv3_1, conv4_1, conv5_1]4.2.3 Gram 矩阵def gram_matrix(x): batch_size, channels, h, w x.size() features x.view(batch_size, channels, h * w) gram torch.bmm(features, features.transpose(1, 2)) return gram / (channels * h * w)4.2.4 训练循环风格迁移中需要优化的不是网络参数而是输入图片本身。先给输入图片加上梯度input_img content_img.clone().requires_grad_(True) optimizer torch.optim.LBFGS([input_img])对于 LBFGS 优化器需要多次调用optimizer.stepdef train_step(): optimizer.zero_grad() outputs vgg(input_img) content_loss 0 for name in content_layers: content_loss nn.functional.mse_loss( outputs[name], content_outputs[name] ) style_loss 0 for name in style_layers: target_gram style_grams[name] gen_gram gram_matrix(outputs[name]) style_loss nn.functional.mse_loss(gen_gram, target_gram) total_loss content_weight * content_loss style_weight * style_loss total_loss.backward() return total_loss optimizer.step(train_step)每次生成新图后可以限制像素范围在 [0, 1] 之间保持视觉合理。4.3 TextCNN 文本分类实战4.3.1 数据预处理这里以中文或英文短文本为例。为了让代码完整我们用一个简单的情感分类示例。先对文本进行分词并构建词表import torch from torch.utils.data import Dataset, DataLoader class TextDataset(Dataset): def __init__(self, texts, labels, word2idx, max_len32): self.texts texts self.labels labels self.word2idx word2idx self.max_len max_len def __len__(self): return len(self.texts) def __getitem__(self, idx): text self.texts[idx] tokens text.split() # 简单按空格分词 ids [self.word2idx.get(w, 1) for w in tokens[:self.max_len]] ids ids [0] * (self.max_len - len(ids)) label self.labels[idx] return torch.tensor(ids, dtypetorch.long), torch.tensor(label, dtypetorch.long)这里word2idx需要提前构建0表示 pad1表示 unknown。4.3.2 TextCNN 模型定义import torch.nn as nn import torch.nn.functional as F class TextCNN(nn.Module): def __init__(self, vocab_size, embedding_dim100, num_filters128, filter_sizes[3, 4, 5], num_classes2): super().__init__() self.embedding nn.Embedding(vocab_size, embedding_dim, padding_idx0) self.convs nn.ModuleList([ nn.Conv2d(1, num_filters, (kernel_size, embedding_dim)) for kernel_size in filter_sizes ]) self.fc nn.Linear(len(filter_sizes) * num_filters, num_classes) def forward(self, x): # x: [batch_size, seq_len] x self.embedding(x) # [batch_size, seq_len, embedding_dim] x x.unsqueeze(1) # [batch_size, 1, seq_len, embedding_dim] conv_outs [] for conv in self.convs: conv_out conv(x) # [batch_size, num_filters, conv_seq_len, 1] conv_out conv_out.squeeze(3) # [batch_size, num_filters, conv_seq_len] pooled F.max_pool1d(conv_out, conv_out.size(2)).squeeze(2) conv_outs.append(pooled) x torch.cat(conv_outs, dim1) # [batch_size, num_filters * len(filter_sizes)] x self.fc(x) return x这段代码的核心是二维卷积。把文本矩阵看作“一张图”宽度是词向量维度embedding_dim高度是序列长度seq_len卷积核大小是(kernel_size, embedding_dim)这样卷积核在序列方向上滑动本质是一维卷积。4.3.3 训练评估model TextCNN(vocab_sizelen(word2idx)) optimizer torch.optim.Adam(model.parameters(), lr1e-3) criterion nn.CrossEntropyLoss() model.train() for epoch in range(5): for inputs, labels in train_loader: optimizer.zero_grad() outputs model(inputs) loss criterion(outputs, labels) loss.backward() optimizer.step() print(fEpoch {epoch1} loss: {loss.item():.4f})如果你希望训练过程更稳定可以加上学习率衰减、早停、验证集评估等机制。4.4 图像字幕生成实战4.4.1 模型结构这里用一个简化的 CNN-LSTM 结构。Encoder 使用 ResNet 提取图片特征Decoder 使用 LSTM 生成文本序列。import torch import torch.nn as nn import torchvision.models as models class EncoderCNN(nn.Module): def __init__(self, embed_size): super().__init__() resnet models.resnet50(weightsmodels.ResNet50_Weights.DEFAULT) self.conv nn.Sequential(*list(resnet.children())[:-1]) self.fc nn.Linear(resnet.fc.in_features, embed_size) def forward(self, images): features self.conv(images) features features.view(features.size(0), -1) features self.fc(features) return features class DecoderLSTM(nn.Module): def __init__(self, embed_size, hidden_size, vocab_size, num_layers1): super().__init__() self.embedding 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, features, captions): embeddings self.embedding(captions) # [batch_size, seq_len, embed_size] features features.unsqueeze(1) # [batch_size, 1, embed_size] context torch.cat([features, embeddings], dim1) # 把图片特征作为第一步输入 outputs, _ self.lstm(context) outputs self.fc(outputs) return outputs这种实现把图片特征拼到序列最前面让 LSTM 先“看过”图片再逐个生成单词。完整项目中会使用 Beam Search 进行更高质量的解码这里只演示训练阶段。4.4.2 训练循环假设你已经准备了image_paths和captions并构造了caption_loaderencoder EncoderCNN(embed_size256) decoder DecoderLSTM(embed_size256, hidden_size256, vocab_sizelen(vocab), num_layers1) encoder encoder.cuda() decoder decoder.cuda() criterion nn.CrossEntropyLoss(ignore_index0) params list(decoder.parameters()) list(encoder.fc.parameters()) optimizer torch.optim.Adam(params, lr1e-3) for epoch in range(5): for images, captions in caption_loader: images, captions images.cuda(), captions.cuda() optimizer.zero_grad() features encoder(images) outputs decoder(features, captions[:, :-1]) # 输入不包括最后一个词 loss criterion( outputs.reshape(-1, len(vocab)), captions[:, 1:].reshape(-1) # 目标不包括第一个词 ) loss.backward() torch.nn.utils.clip_grad_norm_(decoder.parameters(), 0.5) optimizer.step() print(fEpoch {epoch1}, loss: {loss.item():.4f})这里captions[:, :-1]去掉最后一个词作为输入captions[:, 1:]去掉起始符作为目标是序列生成任务的标准做法。ignore_index0是为了跳过 padding 的 loss。实际项目中图像字幕生成还需要处理中文分词、词表构建、Beam Search 解码等这里给出的是最小可运行 demo。5. 常见问题与排查思路问题现象常见原因解决思路训练时报 CUDA out of memorybatch_size 过大或输入图片分辨率过高调小 batch_size或降低图片分辨率使用 gradient accumulation加载预训练模型时提示 URL 下载失败网络环境问题手动下载权重文件放到指定缓存目录或使用weights参数指定本地路径TextCNN 模型报维度不匹配seq_len小于最大卷积核尺寸预处理时统一max_len确保大于 filter_sizes 最大值风格迁移生成的图片有过多噪声风格权重过高或内容权重过低调整style_weight和content_weight的比值增加内容损失比例图像字幕生成训练时 loss 为 NaN学习率过大或序列过长导致梯度爆炸调低学习率加入梯度裁剪batch 之间数据形状不一致文本序列未 padding或图片未 resize在 Dataset 内部统一 max_len 和 resize 尺寸另外很多读者会遇到预训练模型下载慢的问题。可以在命令行中配置镜像源例如使用国内 pip 镜像但模型权重下载地址属于外部网络资源最好通过提前下载或使用离线权重文件。6. 最佳实践与工程建议6.1 数据增强要针对任务设计图像分类任务中随机裁剪、水平翻转、颜色抖动能有效提升模型泛化能力。但风格迁移中对输入图像做增强会导致内容不稳定所以不建议使用随机裁剪。TextCNN 中常见的增强手段是同义词替换或随机 dropout而不是图像那种几何增强。6.2 训练策略比网络结构更重要先用小规模数据验证代码能跑通再上全量数据。使用学习率调度器比如StepLR或ReduceLROnPlateau。尝试在预训练模型上冻结前面若干层只微调靠后的层。每个 epoch 结束后保存 checkpoint包括模型参数、优化器状态、epoch 数方便断点续训。6.3 代码可复现性固定随机种子import random import numpy as np import torch def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed)同时建议在训练脚本里记录以下信息当前 commit 或代码版本。数据集版本。超参数配置可以保存成 JSON 文件。训练日志便于复盘。6.4 工程安全与权限如果你在公司内部服务器上训练模型注意使用独立虚拟环境避免污染服务器上的其他项目。涉及数据访问时确保有权限不要随意拷贝敏感数据集。调试时不要直接删除别人的数据和日志。如果训练任务长时间占用 GPU及时使用nvidia-smi监控显存和 GPU 使用率。6.5 模型部署考虑训练结束后的模型只是第一步。实际业务往往需要把模型导出为 ONNX 或 TensorRT部署到服务和边缘设备上。这时要注意预处理逻辑必须与训练时一致例如归一化的 mean/std。模型输入尺寸要固定或者使用动态维度。对文本模型词表文件要一并保存。7. 总结与学习路线这篇教程把四个经典深度学习项目串在了一起VGG/ResNet 图像分类让你理解 CNN 基础图像风格迁移让你理解特征图的内容和风格表达TextCNN 让你掌握文本分类的卷积做法图像字幕生成则让你初步接触跨模态的 encoder-decoder 结构。下一步可以沿着两个方向扩展一是把 TextCNN 替换成 Transformer 或预训练语言模型比如 BERT二是把图像字幕生成的 LSTM 解码器替换成注意力机制或 Transformer Decoder并尝试在 COCO 数据集上做完整训练。动手写代码比单纯看博客收获大得多建议你先把文章里的代码逐行跑通再尝试修改网络结构、调整超参数最后形成一个属于自己的项目和实验记录。这套流程才是 AI 算法工程师学习和面试准备中最有效的方法。
返回列表