
简介面向初涉深度学习图像分类的开发者这份资源以“常见鱼类识别”为应用场景给出了基于PythonCNN的网页交互式项目适合课程设计、毕业设计或算法入门练习。压缩包共368个文件约23.11MB包括361张按类别组织的鱼类图片含翻转、旋转等增强样本、3个Python脚本分别负责数据集文本生成、模型训练以及本地网页服务、3个文本文件含标签映射或训练验证集划分和1个HTML展示页面形成完整可运行的图像分类流程。训练环节会保存本地模型并输出记录每个epoch验证集损失值与准确率的log日志便于观察模型收敛情况。压缩包内附requirements.txt环境依赖清单需自行配置PyTorch环境适合已有一定Python基础的学习者。目前已有157人学习下载可作为搭建CNN识别项目的直接参考也可作为学习PyTorch图像分类流程的实用样例。1. 基于 Python CNN 的鱼类分类识别一个小而完整的网页化落地样本这套资源的核心是用 PyTorch 搭建一个 CNN 卷积神经网络对常见鱼类图片做分类识别再把训练好的模型通过一个 HTML 网页暴露出来——你打开浏览器上传一张鱼的照片页面就能告诉你它属于哪个类别。整个过程从图片数据集、txt 标注文件生成到模型训练、本地 Web 服务是一条完整的闭环。适合刚学完 CNN 理论、想看看完整工程长什么样的新手也适合需要快速搭一个图像分类 Demo 的从业者——它不需要 GPU纯 CPU 也能训练和推理数据集规模也控制在小体量跑起来不吃力。我拆完这套资源最直接的感受是麻雀虽小但该有的环节一个不落尤其适合拿来做「从零到网页识别」的路线参照。2. 图片数据集与 txt 标签生成flip、rotated45 增强里的门道2.1 数据集目录结构与类别组织方式打开资源包之后第一眼看到的是index.html、一堆以 UUID 命名的 jpg 图片以及三个核心 Python 脚本01数据集文本生成制作.py、02深度学习模型训练.py、03html_server.py。如果你只是急着跑通可能会跳过数据集直接看训练代码——但这一步恰恰是整个项目的地基。数据集的命名方式很有意思比如181986ae-ddf1-4134-b050-05529630c5c1-290mm.jpg对应原图181986ae-ddf1-4134-b050-05529630c5c1-290m_flip.jpg对应水平翻转后的图181986ae-ddf1-4134-b050-05529630c5c1-290m_rotated45.jpg对应旋转 45 度后的图。从命名能看出作者做的数据增强策略每个原始样本派生出了flip水平翻转和rotated45旋转 45 度两个变体把有限的小样本扩充到了三倍。这是小数据集图像分类的常见操作CNN 本身不具备平移、翻转不变性通过几何增强可以让模型对鱼的不同姿态、拍摄角度更鲁棒。实际的类别组织方式是典型的「一个文件夹一个类别」。数据集文件夹下按鱼的种类分目录每个目录里放该类别所有图片这种做法在 PyTorch 的ImageFolder里可以直接被识别但这个项目用的是自己生成 txt 的方式意图在于把训练集和验证集的划分显式地写进文本里方便后续反复调整比例同时也能看到每张图片到底被归到了哪个集合。2.2 01数据集文本生成.py路径与标签如何落盘01脚本做的事情很直接遍历数据集下每个类别文件夹把图片路径和对应的标签写入 txt 文件。伪代码大致是下面这个结构import os import random dataset_root 数据集 # 数据集根目录 train_lines [] val_lines [] # 收集所有类别按文件夹名排序保证标签顺序稳定 categories sorted(os.listdir(dataset_root)) label_map {cat: idx for idx, cat in enumerate(categories)} for cat in categories: cat_dir os.path.join(dataset_root, cat) imgs [f for f in os.listdir(cat_dir) if f.endswith(.jpg)] for img in imgs: path os.path.join(cat_dir, img) label label_map[cat] # 按 8:2 划分训练集和验证集 if random.random() 0.8: train_lines.append(f{path} {label}\n) else: val_lines.append(f{path} {label}\n) with open(train.txt, w, encodingutf-8) as f: f.writelines(train_lines) with open(val.txt, w, encodingutf-8) as f: f.writelines(val_lines) print(ftrain: {len(train_lines)}, val: {len(val_lines)})逻辑不复杂先拿到所有类别名为每个类别分配一个从 0 开始的整数标签然后遍历每个类别文件夹里的 jpg 图片按随机数决定进训练集还是验证集最后把「图片路径 空格 标签」写进 txt。这里有两个参数直接决定后续训练质量。第一个是划分比例0.8在这个比例下训练集和验证集的比例是 8:2如果你的数据集本身图片很少比如几十张可以改成 0.9 甚至 0.95让训练数据更充足但注意验证集也不能太少否则验证准确率波动会非常剧烈。第二个是random.random()——这段代码没有设置随机种子每次运行划分结果都会不一样如果你想让实验可复现应该在脚本开头加一行random.seed(42)。我一般会顺手在划分之后打印一下每个类别的样本数检查类别是否均衡防止某个类别占了绝大多数导致模型学偏。txt 文件的格式是纯文本、每行一条记录路径和标签用空格分隔。这种格式在 PyTorch 里解析成本很低读取时按行 split 就能拿到路径和标签。作者选择这种方案而不是直接用ImageFolder好处是训练集、验证集的划分看得见摸得着你随时可以打开 txt 检查哪张图进了哪个集合排查数据泄漏也更方便。3. PyTorch CNN 训练模型结构、超参与日志解读3.1 CNN 分类模型的选型理由鱼类图片分类是一个典型的细粒度图像识别任务。不同种类的鱼在体形、纹理、鳍条位置上的差异往往不算大这对模型提取局部特征的能力提出了要求。CNN 通过卷积核在图像上滑动逐层提取从边缘、纹理到部件级的语义特征天然适合这种任务。在这个项目里CPU 环境是小数据集训练的现实约束所以模型结构不会太大。一个标准的入门级 CNN 分类网络大致是卷积层提取特征 → ReLU 激活 → 池化下采样 → 重复若干次 → Flatten 展平 → 全连接层输出类别概率。这种结构对基础鱼类分类任务已经够用不需要搬出 ResNet、EfficientNet 这类大模型——在 CPU 上训练反而会很慢而且小数据集上大模型更容易过拟合。损失函数用交叉熵CrossEntropyLoss这是多分类问题的标准选择。优化器常见的是 Adam 或 SGDAdam 收敛快、对学习率不敏感更适合新手SGD 需要更多调参技巧但泛化能力通常略好。如果你只是在 CPU 上跑小数据集Adam 是更稳妥的起点。3.2 02深度学习模型训练.py训练流程的关键段落训练脚本的核心流程是读取 train.txt 和 val.txt → 构建 Dataset 和 DataLoader → 定义 CNN 模型 → 定义损失函数和优化器 → 迭代训练 → 保存模型和日志。下面是一段与项目思路同构的简化版代码关键参数都标了注释import torch import torch.nn as nn from torch.utils.data import Dataset, DataLoader from PIL import Image import torchvision.transforms as transforms # ---------- 自定义 Dataset从 txt 读取路径和标签 ---------- class FishDataset(Dataset): def __init__(self, txt_path, transformNone): self.samples [] with open(txt_path, r, encodingutf-8) as f: for line in f.readlines(): path, label line.strip().split( ) self.samples.append((path, int(label))) self.transform transform def __len__(self): return len(self.samples) def __getitem__(self, idx): path, label self.samples[idx] img Image.open(path).convert(RGB) if self.transform: img self.transform(img) return img, label # ---------- 数据预处理统一尺寸 转 Tensor 归一化 ---------- transform transforms.Compose([ transforms.Resize((224, 224)), # 所有图片统一缩放到 224x224 transforms.ToTensor(), # 转为 Tensor像素值归一到 [0,1] transforms.Normalize( # 按 ImageNet 均值标准差归一化 mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225] ) ]) train_ds FishDataset(train.txt, transformtransform) val_ds FishDataset(val.txt, transformtransform) train_loader DataLoader(train_ds, batch_size16, shuffleTrue, num_workers0) val_loader DataLoader(val_ds, batch_size16, shuffleFalse, num_workers0) # ---------- 定义一个小型 CNN ---------- class SimpleCNN(nn.Module): def __init__(self, num_classes): super().__init__() self.features nn.Sequential( nn.Conv2d(3, 16, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # 112x112 nn.Conv2d(16, 32, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # 56x56 nn.Conv2d(32, 64, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), # 28x28 ) self.classifier nn.Sequential( nn.Flatten(), nn.Linear(64 * 28 * 28, 128), nn.ReLU(inplaceTrue), nn.Dropout(0.5), nn.Linear(128, num_classes) ) def forward(self, x): return self.classifier(self.features(x)) model SimpleCNN(num_classeslen(categories)) criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr0.001) # ---------- 训练循环 验证 ---------- epochs 30 best_acc 0.0 for epoch in range(epochs): model.train() train_loss 0.0 for imgs, labels in train_loader: optimizer.zero_grad() outputs model(imgs) loss criterion(outputs, labels) loss.backward() optimizer.step() train_loss loss.item() * imgs.size(0) # 每个 epoch 结束后在验证集上评估 model.eval() correct 0 total 0 val_loss 0.0 with torch.no_grad(): for imgs, labels in val_loader: outputs model(imgs) loss criterion(outputs, labels) val_loss loss.item() * imgs.size(0) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() train_loss train_loss / len(train_ds) val_loss val_loss / len(val_ds) val_acc correct / total print(fEpoch {epoch1}/{epochs} | train_loss: {train_loss:.4f} | fval_loss: {val_loss:.4f} | val_acc: {val_acc:.4f}) # 保存验证集准确率最高的模型 if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best_model.pth) print(f - saved best model, acc{val_acc:.4f})几个关键参数值得展开说。batch_size16在 CPU 上是个平衡值太小比如 2 或 4会让训练震荡且速度慢太大会直接把内存吃满。lr0.001是 Adam 的常用默认值如果发现 loss 震荡不降可以降到 0.0003 或 0.0001。epochs30对于小数据集来说通常够用但如果发现 val_acc 还在持续上升可以加大到 50 或 80如果 val_loss 已经开始反弹而 train_loss 还在降说明过拟合了要提前停。torch.save(model.state_dict(), best_model.pth)保存的是模型权重而不是整个模型这样做的好处是文件小、加载灵活使用时需要先实例化一个同结构的模型再load_state_dict。保存逻辑里有一个best_acc判断只在验证集准确率创新高时覆盖保存避免最后一个 epoch 的模型反而是过拟合后的劣化版本。日志打印了每个 epoch 的训练损失、验证损失和验证准确率这些数据正是判断模型收敛状态的第一手材料。3.3 训练日志与准确率观察训练完成后本地会多出一个日志文件记录了每个 epoch 的验证集损失值和准确率。看日志有一个经验法则训练损失持续下降而验证损失下降缓慢说明模型还在学习可以继续训两者同时下降是最好的状态训练损失降、验证损失开始涨就是过拟合的明确信号这时候再多的 epoch 只会让模型越来越差。验证准确率本身也需要结合数据量来看。如果验证集只有几十张图单张图片预测错都会让准确率剧烈波动不要因为某个 epoch 的 acc 掉下来就急着调参看整体趋势更靠谱。这个项目在 CPU 上跑单 epoch 时间不算长多跑几个 epoch 观察趋势的成本很低。4. HTML 交互页面与本地服务03html_server.py 跑通整个闭环4.1 Web 页面与 Python 服务端的通信方式03html_server.py的角色是一个本地 Web 服务器它加载训练好的模型对外提供一个 HTTP 接口index.html页面通过这个接口上传图片并拿到识别结果。整个交互不需要联网在http://127.0.0.1:4399上完成典型的本地 Demo 架构。服务端的核心思路是这样用 Python 内置的 HTTP 库或轻量级 Web 框架Flask 是这类场景最常见的方案起一个服务监听 4399 端口定义两个路由一个返回 HTML 页面一个接收图片并返回预测结果。前端把用户选择的图片通过表单或 fetch 发送给后端后端用训练好的模型做推理再把类别名称返回给页面显示。4.2 从训练模型到网页预测的完整链路网页预测的完整链路可以拆成四步。第一步用户在页面上选择一张鱼类的图片第二步前端把图片二进制数据 POST 到服务端的预测接口第三步服务端接收图片做与训练时完全一致的预处理Resize、ToTensor、Normalize然后喂给模型推理得到各类别的概率分布第四步取概率最大的类别把类别名和置信度返回给前端展示。这里最容易踩坑的一点是预处理必须和训练时保持一致。训练时用了Resize((224, 224))推理时也必须用同样的尺寸训练时用了那组 ImageNet 均值和标准差做归一化推理时也必须用完全相同的一组。如果两边不一致模型的输入分布就和训练时不同识别效果会明显下降。很多从网上复制的代码跑出来效果差问题往往就出在这一步。03脚本运行后在控制台打印出 URL复制到浏览器打开页面长什么样由index.html决定。作者在说明里特别强调「这个 url 切记不要输错」——127.0.0.1是本地回环地址如果你手动输入时漏了一个数字或者打成127.0.0.0浏览器会直接拒绝连接。这个问题看起来低级但在新手手里出现频率极高后面避坑章节我会专门展开。一个完整的服务端推理部分核心逻辑大致如下# 加载训练好的模型权重 model SimpleCNN(num_classeslen(categories)) model.load_state_dict(torch.load(best_model.pth, map_locationcpu)) model.eval() # 预处理与训练保持一致 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]) ]) def predict(image_bytes): img Image.open(BytesIO(image_bytes)).convert(RGB) img transform(img).unsqueeze(0) # 增加 batch 维度 with torch.no_grad(): outputs model(img) probs torch.softmax(outputs, dim1) conf, idx torch.max(probs, dim1) return categories[idx.item()], conf.item()model.eval()这一行很多人会漏掉。在推理模式下Dropout 层会被关闭BatchNorm 层会切换到使用全局统计量如果不调用eval()同一个图片每次预测的结果都可能不同——尤其是模型里用了 Dropout 的情况下预测结果会带随机性。加载权重时加map_locationcpu是为了防止在 GPU 机器上训练的权重在纯 CPU 环境加载时找不到 CUDA 设备报错。unsqueeze(0)是给单张图片加一个 batch 维度。模型训练时的输入是四维张量(batch, channel, height, width)单张图片读进来只有(channel, height, width)少了 batch 维不补上就会在 forward 时报维度不匹配的错误。torch.no_grad()则告诉 PyTorch 不要计算梯度推理阶段用不到梯度关掉可以省内存、提速。5. 配置与运行避坑环境、路径、URL 三座山5.1 环境装完就跑不起来版本不匹配是头号杀手现象按 requirements.txt 安装完依赖运行02深度学习模型训练.py直接报ModuleNotFoundError或者 CUDA 相关的错误。原因大多数情况是 PyTorch 安装版本和本机环境不匹配最常见的是在纯 CPU 机器上装到了 CUDA 版本的 PyTorch或者 Python 版本太新/太老导致某些依赖装不上。requirements.txt 只列出包名和版本号但 PyTorch 的安装方式和机器有没有 NVIDIA 显卡强相关。解决先确认自己的机器有没有可用的 NVIDIA GPU没有就装 CPU 版 PyTorch通常做法是pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu环境安装的具体步骤可参照项目内附的说明。装完后在 Python 里执行import torch; print(torch.__version__)确认能正常导入再做下一步。这一步是后面所有操作的前提环境不对后面全是白费功夫。5.2 数据集路径带中文或空格FileNotFoundError 反复出现现象运行01脚本时一切正常但跑02训练脚本时报FileNotFoundError或者提示图片文件不存在明明路径看着没问题。原因Windows 下 txt 里存的是形如数据集\某类别\图片.jpg的路径Linux 或 Mac 下路径分隔符变成/;另外如果项目所在路径包含中文、空格某些图像处理库在读取时容易出问题。作者给出的是 Win 下开发的使用方式换了系统就要适配。解决项目整体放在纯英文、无空格的路径下比如D:\fish_projecttxt 里的路径分隔符统一处理一下可以用path.replace(\\, /)转成 Linux 风格或者相反以你的操作系统为准。我每次拿到新环境都会先打印train.txt的前三行看一眼路径格式确认无误再跑训练。5.3 训练时报 OOM 或内存暴涨batch_size 是第一嫌疑现象训练到第二个或第三个 batch 时内存占用飙升CPU 环境下直接卡死或者报内存不足错误。原因图片统一 Resize 到 224x224 后每张图在 Tensor 里是 3x224x224 的 float 数组占约 600KBbatch_size32 就意味着一个 batch 约 19MB加上中间特征图的计算内存压力成倍放大。CPU 训练不像 GPU 有显存上限报错往往是整个系统内存被耗尽才暴露出来。解决把DataLoader里的batch_size调小16 不行就 88 不行就 4。如果数据集图片较大也可以考虑把 Resize 的目标尺寸从 224 降到 128训练速度会明显提升代价是准确率可能小幅下降。对于这个体量的数据集4~16 的 batch_size 完全够用。5.4 URL 打不开端口被占用或地址输错现象03脚本正常运行控制台打印了http://127.0.0.1:4399但浏览器打开显示无法访问或者打开了却是别的内容。原因两种情况最常见。一是 4399 端口已经被其他程序占用服务实际没起来二是手动输入 URL 时把127.0.0.1打成了127.0.0.0或漏了数字这类错误在浏览器里不会给你任何提示只会显示无法访问。解决先确认服务真的在监听端口Windows 下用netstat -ano | findstr 4399Linux/Mac 用lsof -i:4399看到监听记录说明服务起来了。如果占用了在服务端代码里换一个端口比如 8080或者直接关掉占用程序。URL 输入我每次都直接复制控制台输出的完整地址不手动敲省得因为手误浪费时间排查。5.5 验证准确率虚高或忽高忽低数据集划分没固定种子现象同一份代码跑两次训练得到的最终准确率差很多甚至第二次的结果明显比第一次好但你什么都没改。原因01脚本在划分训练集和验证集时用了random.random()但没有设置随机种子每次运行划分结果都不同运气好的时候验证集里简单样本多准确率就高;运气差的时候验证集里全是难样本准确率就低。另外训练过程中模型初始化和数据加载顺序也有随机性加剧了结果波动。解决在01脚本最开头加random.seed(42)确保每次划分结果一致;在02训练脚本里同样给 PyTorch 设置随机种子torch.manual_seed(42)。这样以后任何一次复现实验结果结果都是可对比的。对做实验的人来说这一步是基本素养。6. 进阶调参与落盘验证怎么判断模型真的可用模型训练完、网页能出结果只能算「跑通了」不算「能用」。我这里分享一套自己常用的验证流程和调参思路。第一步是检查日志的收敛趋势。打开训练日志重点看 val_loss 有没有先降后升的拐点。如果 val_loss 在中间某个 epoch 开始反弹说明从那之后模型就在过拟合最优模型应该是拐点之前的某一版。因为项目里的保存策略是「验证准确率创新高才覆盖保存」所以best_model.pth大概率是拐点附近的模型但如果你想更精细可以手动回看拐点对应的 epoch重新跑一次只训练到那个 epoch。第二步是做多次真实图片推理验证。不要只在网页上测训练集里出现过的图从网上下载几张不同角度、不同光线下的同种类鱼图片分别测一遍。如果训练集里的图识别得很好换了实拍图就翻车说明模型过拟合了训练集泛化能力不够。这时候优先做两件事一是把Dropout的比例从 0.5 调到 0.6~0.7让模型在训练时更「保守」二是加大数据增强的强度原项目只有 flip 和 rotated45你可以自己再加transforms.ColorJitter随机调亮度、对比度、饱和度和transforms.RandomRotation随机旋转 10 度以内让小数据集呈现更丰富的分布。注意加了增强之后训练集和验证集的预处理要同步更新验证集不要加随机增强只做 Resize 和 Normalize。第三步是检查类别间的混淆情况。在验证集上跑一遍推理把被分错的图片单独挑出来看。如果模型老是分不清某两个类别通常是这两个类别的鱼本身长得像或者某一类的训练样本明显偏少。前者可以尝试增大图片输入尺寸Resize 到 299 或 384让模型看到更多细节后者需要补充该类别的样本没有新样本就把该类别的图片多做几种几何变换比如旋转 90 度、180 度、270 度各生成一份尽量把数量补到和其他类别接近。最后一步才是调学习率。如果 val_loss 在前几个 epoch 下降很慢把学习率从 0.001 调到 0.003 试试如果损失震荡不降调到 0.0003。每次只调一个参数改完跑一轮完整训练对比日志不要同时动多个参数否则出了问题根本不知道是哪一步造成的。从那以后我每次拿到类似的训练脚本都会先把随机种子固定、在纯英文路径下运行、跑完后用十张训练集外的图片做一轮实测这三步走完才敢把模型接到网页上给别人用。这套习惯帮我避开了很多「训练准确率挺高、一上真实场景就翻车」的问题希望帮到你。本文还有配套的精品资源点击获取