
简介面向Python与CNN初学者这是一套基于PyTorch和AlexNet架构的动漫角色识别代码专为需要快速动手实践图像分类任务的读者设计。项目不含数据集图片下载后可自行搜集角色图片放入对应文件夹每类文件夹内附位置提示图降低搭建成本。代码共3个Python文件分别承担数据集路径生成、模型训练与PyQt界面识别自动划分训练集和验证集训练过程显示进度条、准确率与损失值并保存日志和model.ckpt权重新增分类文件夹时也无需修改代码即可训练。包内还附带依赖文件requirement.txt、说明文档docx以及4张分类示例图共9个文件压缩包仅231KB通过requirements.txt可快速配置环境结构清晰。目前已有160人学习下载非常适合希望结合逐行注释与文档快速掌握CNN训练全流程的入门者。1. 一个动漫角色识别项目为什么非要用 AlexNet如果你手头碰巧有一份名为“alexnet模型-通过CNN卷积神经网络的动漫角色识别-不含数据集图片-含逐行注释和说明文档”的压缩包打开之后大概率会发现代码能跑通但效果离“开箱即用”差得远。这不是代码的错而是动漫角色识别本身就是一个比猫狗分类难得多的任务——同一角色的不同画风、不同服装、不同姿势在像素层面可能比不同角色还要相似。选 AlexNet 作为主干网络不是因为它在 2025 年还能打榜而是因为它的结构足够简单、参数足够少、逐行读懂的难度足够低非常适合作为理解和魔改 CNN 的起点。这篇笔记就是围绕这个压缩包展开的网络结构怎么拆、数据怎么造、训练参数怎么调、哪些坑我替你踩过了。2. AlexNet 网络结构把 6500 万参数拆开看2.1 为什么图像处理要用 CNN而不是前馈神经网络很多第一次接触深度学习的读者会问一个问题图像处理为啥用 CNN 不用前馈神经网络答案在于图像的局部相关性和参数爆炸之间的矛盾。一张 224×224 的 RGB 图片展开成一维向量是 15 万个输入维度如果用全连接网络接一层 1024 个神经元的隐层光这一层就有 1.5 亿个参数——训练不动也存不下。CNN 通过卷积核的权值共享和局部感受野把参数量从亿级压到万级同时保留了像素之间的空间结构。AlexNet 是 2012 年 ImageNet 竞赛的冠军它的历史意义恰恰在于证明了 CNN 在大规模图像任务上可以碾压手工特征方法这也是这份代码包选择它来做动漫角色识别的原因。2.2 从代码里逐行读 AlexNet 的五层卷积打开压缩包里的模型定义文件核心是一个继承自nn.Module的类。逐行去读你会发现 AlexNet 的结构其实非常规整5 层卷积 3 层全连接中间穿插 ReLU、LRN局部响应归一化、MaxPooling 和 Dropout。我用最常见的 PyTorch 复现版来说明import torch.nn as nn class AlexNet(nn.Module): def __init__(self, num_classes10): super(AlexNet, self).__init__() # 第一层输入 3x224x224输出 96 个特征图 self.conv1 nn.Conv2d(3, 96, kernel_size11, stride4, padding2) self.relu nn.ReLU(inplaceTrue) self.pool nn.MaxPool2d(kernel_size3, stride2) # 第二层96 - 256kernel 5x5 self.conv2 nn.Conv2d(96, 256, kernel_size5, padding2) # 第三层256 - 384kernel 3x3 self.conv3 nn.Conv2d(256, 384, kernel_size3, padding1) # 第四层384 - 384 self.conv4 nn.Conv2d(384, 384, kernel_size3, padding1) # 第五层384 - 256 self.conv5 nn.Conv2d(384, 256, kernel_size3, padding1) # 全连接层 self.fc1 nn.Linear(256 * 6 * 6, 4096) self.fc2 nn.Linear(4096, 4096) self.fc3 nn.Linear(4096, num_classes) self.dropout nn.Dropout(p0.5) def forward(self, x): x self.pool(self.relu(self.conv1(x))) x self.pool(self.relu(self.conv2(x))) x self.relu(self.conv3(x)) x self.relu(self.conv4(x)) x self.pool(self.relu(self.conv5(x))) x x.view(x.size(0), -1) x self.relu(self.fc1(x)) x self.dropout(x) x self.relu(self.fc2(x)) x self.dropout(x) x self.fc3(x) return x这段代码里有几个参数值得单独拎出来说。kernel_size11, stride4是第一层卷积的关键224×224 的输入经过 11×11 卷积核、步长 4 滑窗后特征图直接缩到 55×55这是 AlexNet 刻意设计的高下采样率目的是快速降低分辨率、减少后续计算量。padding2是为了保证边界像素也能被卷积核覆盖到但注意它只补偿了一部分边缘损失。最后一层卷积输出的 256×6×6 是输入尺寸经过 3 次 MaxPooling每次尺寸减半之后的结果如果你自己改输入分辨率这个 6×6 必须重新算。LRN 层在原版 AlexNet 里是放在 ReLU 之后的作用是局部归一化让同一个位置跨通道的响应互相抑制。但后续研究包括 ResNet 和 VGG基本都弃用了 LRN因为收益太小且增加计算量。如果你的代码包里还保留了 LRN可以在训练时尝试去掉正常不会掉点甚至可能微涨。2.3 AlexNet 竞赛成绩之外的选型理由AlexNet 在 2012 年 ImageNet 竞赛上以巨大优势夺冠top-5 错误率 15.3%比第二名低 10 个百分点以上——这是它历史地位的来源。但选它做动漫角色识别更实际的理由是三个第一模型小参数量约 6100 万在单张消费级显卡上训练十来个类别的分类任务几分钟一个 epoch第二结构规整每一层干了什么一目了然方便你打印中间特征图做可视化第三和 ResNet 这类深网络相比AlexNet 的拟合能力弱一些反而在小数据集上不那么容易过拟合——当然这是相对的该过拟合还是过拟合后面避坑章节会细说。3. 数据准备没有数据集图片就从零造一个可用数据集3.1 压缩包里没有图片训练数据从哪来标题写得很清楚——“不含数据集图片”。这意味着压缩包里只有模型代码、训练脚本、逐行注释和说明文档图片得自己准备。我一般会把这当成一件好事亲自整理数据的过程会让你对任务难度的理解远超直接跑通一个现成数据集。主流做法是去开源动漫标注站或图片爬取脚本自己抓图但这里不展开爬虫细节只假设你已经有了按类别分好文件夹的图片比如data/train/下有rem、ram、saber等子目录。每个类别至少 200 张图越少越要小心过拟合。3.2 数据预处理和加载把散图切成模型能吃的张量Torchvision 的ImageFolder是按目录结构自动打标签的省去手写 CSV 标签的功夫。下面是我针对动漫图片整理的预处理流程from torchvision import datasets, transforms from torch.utils.data import DataLoader train_transform transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomCrop(224), transforms.RandomHorizontalFlip(p0.5), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_transform transforms.Compose([ transforms.Resize((256, 256)), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) train_dataset datasets.ImageFolder(rootdata/train, transformtrain_transform) val_dataset datasets.ImageFolder(rootdata/val, transformval_transform) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers4) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers4)这里要解释两个容易被忽略的参数。RandomCrop(224)配合Resize((256, 256))是经典的训练期数据增强组合先放大到 256 再随机裁 224相当于给模型提供小幅平移和缩放的鲁棒性。CenterCrop(224)在验证集上只取中心区域保证每次评估看到的是同一张图结果才有可比性。ColorJitter对动漫图特别有价值同人图的色调五花八门模型见过不同配色后对线条特征而非颜色本身的依赖会更强。关于Normalize的均值和标准差代码里用的是 ImageNet 的统计值——这组数值对大多数自然图像适用对动漫图稍有偏差但不是大问题。如果你的模型是从零训练且效果始终不理想可以试着用训练集的真实均值标准差替代代码就三行import torch mean torch.zeros(3) std torch.zeros(3) for img, _ in train_dataset: # 注意这里要先 ToTensor 再做归一化否则数值范围不对 mean img.mean(dim(1, 2)) std img.std(dim(1, 2))但说实话在数据量不够大的时候直接用 ImageNet 的统计值往往比自算的更好——因为预训练权重的分布就是按这个归一化方式对齐的。3.3 类别不平衡动漫角色识别的第一个暗坑动漫角色数据集的天然问题是不平衡热门角色图多冷门角色图少差距可能到 10 倍以上。ImageFolder 不会帮你处理这个。最简单的办法是分层采样每个 batch 里按类别均匀抽取from torch.utils.data import WeightedRandomSampler labels [sample[1] for sample in train_dataset.samples] class_counts torch.bincount(torch.tensor(labels)) weights 1.0 / class_counts.float() sample_weights weights[labels] sampler WeightedRandomSampler(sample_weights, num_sampleslen(sample_weights), replacementTrue) train_loader DataLoader(train_dataset, batch_size32, samplersampler, num_workers4)WeightedRandomSampler的原理是给样本数量少的类别分配更高的采样概率每个 epoch 里小类别的图片会被反复抽到从而缓解模型偏向大类别的倾向。需要注意replacementTrue意味着同一个 batch 里可能出现完全相同的图片这在数据增强的配合下问题不大但如果你的增强很弱模型容易记住重复样本。4. 训练配置从学习率到 warmup让损失曲线按预期下降4.1 最小可运行训练脚本参数怎么设、为什么压缩包里的训练脚本大概率已经写好了循环但参数大概率不是针对你的数据调的。下面这个脚本片段是我基于 AlexNet 调动漫分类时常用的起点配置import torch.optim as optim from torch.optim.lr_scheduler import StepLR device torch.device(cuda if torch.cuda.is_available() else cpu) model AlexNet(num_classeslen(train_dataset.classes)).to(device) criterion nn.CrossEntropyLoss() optimizer optim.SGD(model.parameters(), lr0.01, momentum0.9, weight_decay5e-4) scheduler StepLR(optimizer, step_size10, gamma0.1) def train_one_epoch(model, loader, optimizer, criterion, device): model.train() running_loss 0.0 correct 0 total 0 for images, labels in loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() * images.size(0) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() epoch_loss running_loss / total epoch_acc correct / total return epoch_loss, epoch_acc for epoch in range(30): train_loss, train_acc train_one_epoch(model, train_loader, optimizer, criterion, device) print(fEpoch {epoch1}/{30} | Loss: {train_loss:.4f} | Acc: {train_acc:.4f}) scheduler.step()关键参数我逐个说明。学习率0.01配合 SGD 动量 0.9 是 AlexNet 原论文的标准配置在 ImageNet 上有效在小规模动漫数据集上大概率也适用——但这是“大概率”不是“一定”。如果你的训练集只有几百张图建议从0.001起调否则前几个 epoch 损失可能直接发散。weight_decay5e-4是 L2 正则项能压制过拟合但数据量特别小的时候可以加到1e-3。step_size10, gamma0.1的意思是每 10 个 epoch 学习率乘以 0.1这个衰减节奏对大数据集合理小数据集建议改成每 5 个 epoch 衰减一次因为小数据集收敛更快学习率不及时降下来会在后期来回震荡。4.2 损失不降、过拟合、收敛太慢用曲线判断该调什么训练跑起来之后不要只盯着一行行打印的数字看。把训练损失和验证损失画到同一张图上三个典型模式对应三种处理思路。Loss 完全不降一直在初始值附近打转先看是不是学习率太大导致梯度震荡把 lr 降到 0.001 再试再看数据标签有没有错位——ImageFolder 按字母序给类别编号如果训练集和验证集的文件夹命名方式不同标签顺序就对不上。训练损失降到很低但验证损失反弹这是典型的过拟合。优先增加数据增强强度比如把RandomHorizontalFlip加上RandomRotation(degrees5)或者在全连接层前加一层 Dropout 并把 p 从 0.5 提到 0.7。收敛太慢损失在缓慢下降但没有明显加速。常见原因是学习率太小或 batch size 太大。小数据集上 batch size 超过 64 时梯度估计过于平滑收敛速度会明显变慢建议回到 32。4.3 迁移学习不从头训也能达到可用的精度如果你只有几百张图从零训练 AlexNet 的收敛效果通常不太理想。这时应该用 ImageNet 预训练权重做迁移学习。做法很直接加载预训练模型替换最后一层全连接的输出维度然后分两阶段训练。第一阶段冻结所有卷积层只训练全连接层学习率设 0.001。因为卷积层提取的是通用视觉特征——边缘、纹理、形状这些对动漫图同样有效。第二阶段解冻所有层全模型微调学习率降到 0.0001防止破坏已经学好的底层特征。这个策略几乎总是能比从零训练多出 10 到 20 个百分点的准确率前提是你的数据集和 ImageNet 的分布差异没有大到离谱——动漫图虽然风格特殊但底层特征仍然通用。代码上就三处改动import torchvision.models as models # 加载预训练权重 model models.alexnet(weightsmodels.AlexNet_Weights.IMAGENET1K_V1) # 替换最后一层把 1000 类换成自己的类别数 model.classifier[6] nn.Linear(4096, num_classes) # 冻结卷积层requires_grad 设为 False for param in model.features.parameters(): param.requires_grad False注意这里替换的是classifier[6]因为 PyTorch 里 AlexNet 的classifier是一个Sequential索引 6 对应最后一个全连接层。如果代码包里的实现是自己手写的类而不是用torchvision.models那就直接替换self.fc3。冻结卷积层后优化器应只传入需要训练的参数optimizer optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr0.001)filter(lambda p: p.requires_grad, ...)这行是必须的否则优化器会把冻结参数也传进去虽然梯度为 0 不影响更新但会白占显存和计算时间。5. 避坑手册动漫角色识别最常见的 5 个踩坑记录5.1 同一个角色不同画风被分成两类现象验证集上同一个角色在动画截图、官方立绘、同人手绘三种画风下的预测结果不一致模型把画风差异当成了角色差异。原因动漫角色数据集中同人图的风格多样性远大于角色本身的类间差异。模型没有足够的同类样本去学习“不变特征”只能拟合画风特征。解决按画风分组做数据增强。把训练集每个类别的图片按画风拆成三份训练时每个 batch 强制从不同画风里各抽一部分。实现上用ConcatDataset或自己写采样逻辑确保每个 batch 里的同一角色至少覆盖两种画风。5.2 输入分辨率低导致细节特征丢失现象角色眼睛、发饰等关键辨识特征在 224×224 的输入下只有几个像素宽模型学不到这些细节准确率卡在 70% 左右上不去。原因动漫角色的辨识往往依赖极细小的特征——瞳孔颜色、发色渐变、标志性头饰。这些特征在原始大图上清晰但缩放到 224×224 后信息量严重损失。解决先把图缩放到 384×384再随机裁 224。同时把第一层卷积的stride从 4 改成 2让输入特征图分辨率翻倍。注意改完stride后后续特征图的尺寸和全连接层的输入维度都要重新计算最容易翻车的就是忘了改fc1的输入维度。5.3 数据集太小验证集上准确率忽高忽低现象每次跑同一个测试集准确率波动超过 5 个百分点无法判断哪次改动是有效的。原因验证集只有几十张图随机抽样的偏差太大评价指标本身方差很高。解决用 K 折交叉验证代替单次划分或者至少保证每类至少有 20 张验证图。如果数据总量不够就用 5 折交叉验证取平均准确率作为最终指标。5.4 训练时显存不够batch size 一降再降现象显存溢出报错被迫把 batch size 降到 8模型收敛变慢且效果变差。原因AlexNet 第一层卷积的参数量和中间特征图都很大显存占用主要来自激活值而不是参数。解决开 AMP混合精度训练PyTorch 自带的torch.cuda.amp能省接近一半显存对准确率几乎没有影响。另外可以输入尺寸从 224×224 临时降到 192×192观察准确率下降幅度如果不大就保持小尺寸训练。5.5 随机种子没固定实验结果对不上现象同一份代码、同一个数据集两次训练结果差异很大没法复现自己的实验。原因PyTorch、NumPy、Python 自带的random模块各自有随机状态没有统一固定种子。解决训练脚本开头固定所有随机源代码就四行import random import numpy as np import torch random.seed(42) np.random.seed(42) torch.manual_seed(42) if torch.cuda.is_available(): torch.cuda.manual_seed_all(42)注意固定种子之后还要把DataLoader的shuffle设为True否则每次迭代的样本顺序从未固定状态出发复现依然不稳定。至于DataLoader本身的随机采样PyTorch 会沿用全局随机种子不需要额外设generator参数。6. 只训练一个模型用集成学习和特征可视化把准确率再往上推6.1 三模型投票从单模型 85% 到集成 89%在你的 AlexNet 已经稳定在 85% 准确率之后想再往上走最直接的手段是训练三个结构相同但随机种子不同的模型推理时取三个模型 softmax 输出的平均。这样做之所以有效是因为不同随机种子对应不同的初始化参数和数据采样顺序模型收敛到的局部最优解各不相同它们的错误模式也各不相同平均之后可以互相抵消一部分错误。实现不复杂推理代码改成model1.load_state_dict(torch.load(best_model_seed1.pth)) model2.load_state_dict(torch.load(best_model_seed2.pth)) model3.load_state_dict(torch.load(best_model_seed3.pth)) model1.eval(); model2.eval(); model3.eval() with torch.no_grad(): output1 torch.softmax(model1(images), dim1) output2 torch.softmax(model2(images), dim1) output3 torch.softmax(model3(images), dim1) ensemble_output (output1 output2 output3) / 3 _, predicted torch.max(ensemble_output, 1)注意要取 softmax 之后再平均而不是直接平均 logits——因为不同模型的 logits 尺度可能有差异直接平均会让某个模型主导结果。6.2 用特征图可视化确认模型到底在看什么准确率之外我们还希望确认模型不是靠撞运气猜对的。打印最后一层卷积的输出特征图可以直观看到模型关注了图像的哪些位置。from torchvision import transforms import matplotlib.pyplot as plt def visualize_feature_maps(model, image_path, layer_idx4): image Image.open(image_path).convert(RGB) 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]) ]) input_tensor transform(image).unsqueeze(0) activation {} def hook_fn(module, input, output): activation[value] output.detach() # 注册 forward hook第 4 层是最后一个卷积层 model.conv5.register_forward_hook(hook_fn) with torch.no_grad(): model(input_tensor) feature_maps activation[value].squeeze(0) # (256, 6, 6) fig, axes plt.subplots(8, 8, figsize(12, 12)) for i in range(64): ax axes[i // 8][i % 8] ax.imshow(feature_maps[i].cpu().numpy(), cmapgray) ax.axis(off) plt.savefig(feature_maps.png)代码里用register_forward_hook在conv5之后捕获特征图squeeze(0)去掉 batch 维度后得到形状为(256, 6, 6)的张量——256 个 6×6 的特征图。每张图上亮的位置表示该通道的卷积核在这个区域有较强响应。观察时重点看两点一是聚焦位置是否集中在角色面部和发饰等关键部位二是如果大量特征图都集中在背景上说明模型学到的是背景特征而非角色特征这时需要检查训练集里是否每个类别的图片背景过于一致。6.3 对代码包本身的改造建议说明文档之外的常见补充压缩包里既然带了逐行注释和说明文档那说明作者的本意是让你读懂之后去改、去扩而不是当成一个黑匣子工具用。我会建议在代码包里补上三个在工程上几乎必需但经常被省略的部分导出 ONNX 格式的脚本、一份按类别输出置信度排名的推理 demo、以及一份记录每次实验的超参和结果对照表。第一项是为了部署时不被 PyTorch 版本绑定第二项是为了快速检查单张图片的实际表现第三项是为了不重复踩自己踩过的坑。一个习惯层面的提醒动漫角色识别项目的迭代速度取决于你的标注质量。标注准确率不高再好的模型结构也难以弥补。我最早一次跑这个项目因为训练集里混入了几张角色名字相近的图导致模型在验证集上反复出现的错误始终无法解释——排查了两天才定位到是数据问题。后来我把每次数据更新的变更记录写进说明文档里类似问题再也没困扰过我。希望帮到你。本文还有配套的精品资源点击获取