
简介一套基于Python与PyTorch框架的CNN常见鱼类分类识别系统前端采用网页HTML交互界面面向深度学习初学者及计算机视觉爱好者覆盖图像分类从数据准备、模型训练到Web端识别的完整流程。压缩包共368个文件包括361张已按类别归档的鱼类图片数据集、3个Python脚本01数据集文本生成、02CNN模型训练、03HTML服务启动、3个txt文本含环境依赖配置以及1个index.html交互页面整体约23.11MB。脚本按编号依次运行即可完成样本路径与标签读取、模型训练与验证、本地网页服务启动等环节训练产物包括模型权重文件和记录每轮损失值、准确率的日志便于后续调参与评估。目前已有157人下载学习适合希望快速上手PyTorch图像分类实战并获得可视化交互体验的学习者参考。1. 这个标题不是一个 Demo而是一套能复用的完整鱼种识别 Web 项目如果你在找一个「能跑通、能交作业、能后续换数据集」的 CNN 卷积神经网络 Web 项目这个标题正好切中需求Python 负责训练和推理CNN 负责从鱼图里提取纹理和轮廓特征HTML 网页负责上传图片并展示识别结果图片数据集则是训练和验证的原料。这套组合几乎是课程设计、毕设和入门深度学习最常见的落地形态做完之后换个植物、车标、垃圾图片数据集结构照样能用。适合刚配好 Python 环境、略懂一点图像处理但没独立搭过 Web 服务的人。它的核心价值不是模型精度有多高而是把「训练脚本 后端接口 前端页面」三者串成一个闭环让你第一次真切感受到模型和网页之间是怎么配合的。2. 跑通之前先看懂架构CNN 为什么适合鱼图分类以及这个项目的四个组成件2.1 CNN 卷积神经网络为什么适合做鱼图分类从特征提取到全连接决策常见鱼类分类识别的难点在于鱼的外观差异大同一种鱼在不同光照、角度、背景下拍出来可能差别很大而不同种的鱼有时轮廓又接近。传统图像处理靠人工设计颜色直方图、形状描述子泛化能力有限。CNN 卷积神经网络的做法是让网络自己从图片里逐层学习特征浅层卷积核抓边缘、纹理、斑点深层卷积核组合出鳍形、体态、色斑等更抽象的语义特征最后用全连接层把特征映射到类别概率。对「常见鱼类」这种规模的任务不太需要用超大模型。类别数在十几类以内、每类几十张图的情况下一个 3 到 4 层卷积的小网络就够起步训练时间在 CPU 上也能接受。因为数据集不大模型太大反而容易过拟合把训练集背下来验证集却一塌糊涂。所以这里选 CNN 不是为了追新而是因为它在中小规模图像分类上比传统方法可靠又比 ResNet 这种深模型更容易调。2.2 web 项目里的三层结构图片数据集、训练脚本、Flask 与 HTML 前端这类标题里出现「web 网页 html 版」意味着交付物不是一串在 Jupyter 里跑的代码而是一个能在浏览器里操作的系统。常见做法是做成三层第一层是图片数据集按类别分目录存放训练脚本用目录名作为标签。第二层是训练和推理脚本训练完导出模型文件Keras 的 .h5 或 .keras 格式。第三层是 Flask 后端和 HTML 前端后端加载模型、接收上传图片、做预处理和预测前端提供上传按钮和结果显示区域。也可以不用 Flask直接把模型转成 TensorFlow.js 扔给纯 HTML 页面跑但对入门项目来说Flask 更贴近真实 Web 项目结构后端处理模型、前端只负责交互后续加用户记录、批量识别都方便。这个标题里既然强调「html 版」重点是把 HTML 页面做完整而不是让浏览器直接跑模型。2.3 运行环境Python 版本选择、依赖安装与图片数据集目录我一般建议用 Python 3.8 到 3.10 之间搭配 TensorFlow 2.x。TensorFlow 的依赖链比较重装新不装旧容易踩坑装太老又可能和 Python 版本冲突。这里给一组最简依赖tensorflow、flask、pillow、numpy。没有 GPU 时就用 CPU 版 TensorFlow鱼种分类的图片量级训练一轮几分钟到十几分钟完全能接受。安装依赖最稳的方式是用虚拟环境避免把系统 Python 弄乱。常见做法是python -m venv fish_env source fish_env/bin/activate # Windows 下用 fish_env\Scripts\activate pip install tensorflow flask pillow numpy这段命令先创建虚拟环境再把核心依赖装进去。注意 Windows 和 Linux 的激活命令不一样如果激活后命令行提示符前面出现(fish_env)说明环境已生效。装 TensorFlow 时如果速度慢或超时可以用国内镜像源例如pip install tensorflow -i https://pypi.tuna.tsinghua.edu.cn/simple。装完之后用python -c import tensorflow as tf; print(tf.__version__)验证能打印版本号就说明环境没问题。提示不要在图省事的情况下直接pip install到全局环境。后面换数据集、试不同版本时虚拟环境能给你一颗后悔药。图片数据集目录建议放在项目根目录下命名为dataset/里面按类名建子目录比如dataset/鲤鱼、dataset/草鱼、dataset/鲫鱼。训练脚本会通过image_dataset_from_directory自动读取类名作为标签这样后期增删类别不用改代码。3. 组织图片数据集并训练 CNN可抄作业的命令与参数3.1 图片数据集目录怎么摆train/validation/test 划分与文件名规范拿到图片数据集后很多人第一步就是把所有图片丢进一个文件夹然后问为什么训练脚本报错。正确做法是先按类别分目录再按比例划分训练和验证子集。一个省事的方案是把数据组织成如下结构dataset/ ├── train/ │ ├── 鲤鱼/ # 30张 │ ├── 草鱼/ # 30张 │ └── 鲫鱼/ # 30张 └── validation/ ├── 鲤鱼/ # 8张 ├── 草鱼/ # 8张 └── 鲫鱼/ # 8张训练集用于更新模型参数验证集用于每轮训练后检查模型有没有过拟合。如果原数据集只有 train 这一份可以按 8:2 或者 7:3 手动分我一般写个小脚本按比例随机挪图片而不是用工具盲目复制避免验证集和训练集里出现同一张图。文件名本身不需要特定格式但强烈建议全部改成英文字母加数字例如carp_001.jpg。中文文件名在 Windows 上容易引发编码问题而且后面 Flask 上传时文件名会经过浏览器编码排查起来更麻烦。3.2 训练脚本拆解数据增强、模型定义、训练与保存下面这个脚本是这类鱼类分类项目的核心骨架。它读取目录结构做数据增强定义一个小型 CNN训练并保存模型。直接复制后改成自己的数据集路径就能跑。# train_fish.py import tensorflow as tf from tensorflow.keras import layers, models from tensorflow.keras.preprocessing import image_dataset_from_directory # 数据路径和超参数 train_dir dataset/train val_dir dataset/validation img_height, img_width 160, 160 batch_size 16 num_epochs 10 # 从目录加载训练集和验证集 train_ds image_dataset_from_directory( train_dir, label_modecategorical, # 输出one-hot标签 image_size(img_height, img_width), batch_sizebatch_size, shuffleTrue, seed42, interpolationbilinear, ) val_ds image_dataset_from_directory( val_dir, label_modecategorical, image_size(img_height, img_width), batch_sizebatch_size, shuffleFalse, ) # 数据增强随机翻转和亮度变化 data_augmentation tf.keras.Sequential([ layers.RandomFlip(horizontal), layers.RandomBrightness(0.2), ]) # 构建CNN模型 model models.Sequential([ data_augmentation, layers.Rescaling(1.0 / 255), layers.Conv2D(32, (3, 3), activationrelu, paddingsame), layers.MaxPooling2D((2, 2)), layers.Conv2D(64, (3, 3), activationrelu, paddingsame), layers.MaxPooling2D((2, 2)), layers.Conv2D(128, (3, 3), activationrelu, paddingsame), layers.MaxPooling2D((2, 2)), layers.Flatten(), layers.Dropout(0.5), # 防过拟合 layers.Dense(128, activationrelu), layers.Dense(len(train_ds.class_names), activationsoftmax), ]) model.compile( optimizertf.keras.optimizers.Adam(learning_rate0.001), losscategorical_crossentropy, metrics[accuracy], ) # 训练并保存 history model.fit( train_ds, validation_dataval_ds, epochsnum_epochs, ) model.save(fish_model.keras) print(类别列表:, train_ds.class_names)这段代码里最关键的是image_dataset_from_directory会返回两个东西数据集本身和class_names属性后者就是按目录名排序后的类别列表。这个列表必须保存下来后面 Flask 预测时要把模型输出的 0、1、2 这类数字索引映射回「鲤鱼」「草鱼」这样的中文名否则前端只能显示数字。数据增强里的RandomFlip和RandomBrightness是针对鱼图的特点加的鱼在拍摄时方向不固定水体光照也经常变增强后的数据相当于变相扩充了训练集让模型对翻转和明暗变化更不敏感。注意Rescaling(1.0/255)必须放在增强之后、第一个卷积层之前作用是把 0 到 255 的像素值压缩到 0 到 1这是卷积网络能稳定训练的前提。3.3 三个必调参数img_height、epochs、batch_size 怎么设img_height/img_width图片送入模型前的统一尺寸。设 160 或 224 都可以越大保留细节越多但训练越慢、内存占用越高。常见鱼类数据集图片原始尺寸参差不齐统一缩放到 160 对大多数任务已经够用。epochs训练轮数。先设 10 轮看趋势如果验证准确率还在明显上升就加到 20、30 轮。不要一上来就 50 轮小数据集上很可能第 10 轮就过拟合了。batch_size每批图片数量。CPU 训练建议 8 到 16GPU 可以 32 或 64。batch 太大容易把内存打满太小则训练震荡。我从经验上讲先小 batch 跑通再逐步调大。训练完成后模型文件fish_model.keras就是后面 Flask 要加载的东西。同时建议把class_names写到一个文本文件里with open(class_names.txt, w, encodingutf-8) as f: f.write(\n.join(train_ds.class_names))这样 Web 后端读取类别名称时就不用依赖训练脚本的上下文了避免每次启动都要重新训练一遍。这也体现了工程习惯训练的产物要包含模型本身和使用说明。4. 用 Flask 把模型包成后端接口用 HTML 网页做识别入口4.1 Flask 应用最小骨架加载模型、预处理图片、返回 JSON训练完模型下一步是让网页能调用它。Flask 在这里充当一个 Web 服务器接收前端传来的图片文件用 Pillow 做预处理再交给模型预测。注意预处理必须和训练时保持一致同样缩放到 160x160、同样除以 255、同样按 RGB 读图。# app.py import io import numpy as np from flask import Flask, request, jsonify, render_template from PIL import Image import tensorflow as tf app Flask(__name__) # 加载模型和类别 model tf.keras.models.load_model(fish_model.keras) with open(class_names.txt, r, encodingutf-8) as f: class_names [line.strip() for line in f.readlines() if line.strip()] def preprocess_image(image_bytes): img Image.open(io.BytesIO(image_bytes)).convert(RGB) img img.resize((160, 160)) arr np.array(img, dtypenp.float32) / 255.0 arr np.expand_dims(arr, axis0) # 增加batch维度 return arr app.route(/, methods[GET]) def index(): return render_template(index.html) app.route(/predict, methods[POST]) def predict(): if file not in request.files: return jsonify({error: 没有上传文件}), 400 file request.files[file] try: arr preprocess_image(file.read()) probs model.predict(arr, verbose0)[0] pred_idx int(np.argmax(probs)) # 返回概率前三的类别 top3 sorted( [(class_names[i], float(probs[i])) for i in range(len(class_names))], keylambda x: x[1], reverseTrue, )[:3] return jsonify({ prediction: class_names[pred_idx], confidence: float(probs[pred_idx]), top3: [{name: n, prob: round(p, 4)} for n, p in top3], }) except Exception as e: return jsonify({error: str(e)}), 500 if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse)这段代码有两个容易忽略的点。第一是preprocess_image里必须.convert(RGB)否则遇到 PNG 带透明通道或者灰度图时数组形状会和模型输入不一致预测直接报错。第二是model.predict返回的是二维数组第一个维度是批次所以要取[0]。这里返回 top3 而不是只返回最大值能让用户在结果页看到「第二可能是什么」对识别相似鱼种很有参考价值。4.2 前端 HTML 页面上传、预览、结果展示与错误提示HTML 页面放在 Flask 默认的templates/index.html下用 fetch 异步调用后端的/predict接口。页面上需要三类功能块文件选择输入、图片预览、结果展示。下面给一个精简的关键版实际使用时可以把样式换成自己的。!DOCTYPE html html langzh-CN head meta charsetutf-8 title常见鱼类分类识别/title style body { font-family: sans-serif; max-width: 640px; margin: 40px auto; } .result { margin-top: 20px; } /style /head body h2上传鱼类图片识别常见鱼种/h2 input typefile idfileInput acceptimage/* img idpreview stylemax-width: 300px; display: none; button idbtnPredict开始识别/button div classresult idresult/div script document.getElementById(fileInput).addEventListener(change, function (e) { const file e.target.files[0]; if (!file) return; const reader new FileReader(); reader.onload function (ev) { const preview document.getElementById(preview); preview.src ev.target.result; preview.style.display block; }; reader.readAsDataURL(file); }); document.getElementById(btnPredict).addEventListener(click, function () { const fileInput document.getElementById(fileInput); if (fileInput.files.length 0) { alert(请先选择一张图片); return; } const formData new FormData(); formData.append(file, fileInput.files[0]); fetch(/predict, { method: POST, body: formData }) .then(resp resp.json()) .then(data { if (data.error) throw new Error(data.error); const resultDiv document.getElementById(result); resultDiv.innerHTML pstrong识别结果/strong data.prediction /p p置信度 (data.confidence * 100).toFixed(2) %/p p其他可能/pul data.top3.slice(1).map(function(item) { return li item.name ( (item.prob * 100).toFixed(2) %)/li; }).join() /ul; }) .catch(err { document.getElementById(result).innerHTML p stylecolor:red;识别失败 err.message /p; }); }); /script /body /html这里用FormData上传文件不需要手工设置 Content-Type浏览器的 fetch 会自动加 multipart 边界。前端先预览再提交体验上更接近真实 Web 项目。你需要知道一个容易踩的兼容性问题老版本 Safari 对acceptimage/*支持不彻底用户仍可能选到非图片文件所以后端要有 try-except 兜底像上面app.py那样在预处理时捕获异常。4.3 启动 web 服务从命令行到浏览器访问的完整路径启动服务只需要在项目根目录执行python app.py看到 Flask 打印的Running on http://127.0.0.1:5000后浏览器访问http://127.0.0.1:5000页面就出来了。这时可以选一张测试图片上传如果返回结果正常项目闭环就走通了。打算在同一局域网内用手机测试时要把app.run的 host 设为0.0.0.0这样 Flask 会监听所有网卡然后手机访问电脑的局域网 IP例如http://192.168.1.10:5000。注意 Windows 防火墙会弹出拦截提示要选择「允许访问」。如果想要整洁的部署目录常见做法是在项目根目录建一个templates/放 HTML、static/放 CSS 和 JS、训练脚本放根目录或scripts/。5. 避坑排查从「图片数据集乱」到「网页转圈」的 5 个高频问题5.1 图片数据集类别不均衡模型只会猜大头现象训练准确率很高但验证集里小类别几乎全错预测结果总是偏向样本最多的那类鱼。原因当「鲤鱼」有 300 张、「鳜鱼」只有 20 张时模型只要把所有图片都预测成鲤鱼就能获得很高的训练准确率网络根本不会认真学鳜鱼的特征。解决先看类别分布像我前面给出的目录结构那样统计每类张数。最简单的手段是给每类设上限把大类别随机抽样到和小类别接近的数量或者对小类别做更重的数据增强。还有一个容易被忽略的点image_dataset_from_directory默认不按类别做频率平衡如果类别不均衡严重要在训练时给class_weight参数。从长远看采集新数据或对大类降采样比调损失函数更可靠。5.2 中文路径与文件名乱码训练能过预测崩溃现象Flask 启动正常但网页上传图片后报错日志里出现 UnicodeDecodeError或者类名显示成乱码。原因Windows 控制台编码和 Python 默认的 UTF-8 不一致分类目录是中文时class_names.txt写入或读出时编码不对。解决训练脚本里写文件时指定encodingutf-8读取时同样指定。同时把 Windows 控制台代码页切到 UTF-8执行chcp 65001再启动 Flask。如果你的类名是中文最稳妥的方案是内部用拼音或英文作目录名例如carp、grass_carp、crucian只在 HTML 页面显示时映射成中文这样能彻底绕开编码问题。这个项目里如果你拿到的是「常见鱼类图片数据集」建议拿到后先检查目录名编码必要时用脚本统一改一次。5.3 Flask 找不到模型文件明明保存了却报“No such file”现象在 Python 里能加载模型但启动 Flask 后报错找不到fish_model.keras。原因常见的多是因为执行python app.py时当前工作目录不在项目根目录比如在 VSCode 里按了运行按钮工作目录指向了项目子目录。相对路径是跟着「当前工作目录」走的不是跟着 app.py 文件位置走的。解决不依赖相对路径在app.py顶部写import os BASE_DIR os.path.dirname(os.path.abspath(__file__)) model_path os.path.join(BASE_DIR, fish_model.keras)这样无论从哪个目录启动都会找到 app.py 同目录下的模型文件。这是个很小但实战中非常常见的坑在部署到服务器时几乎一定会碰到先改了可以少一次翻车。5.4 网页上传后一直转圈前端在等后端其实早就挂了现象图片选好了点识别按钮后页面一直 loading几分钟都没反应控制台也没输出。原因后端进程崩了或者模型预测卡住。小数据集训练出的模型预测很快通常不超过 1 秒转圈超过 10 秒基本可以断定是后端异常或模型加载失败。解决先看终端窗口有没有异常堆栈。如果是verbose0把 TensorFlow 日志压掉了先临时改成verbose1在预测时打印进度方便判断。另一个常见原因是程序抛异常后 Flask 在 debugFalse 下只打印在终端前端收到 500 状态码但 fetch 代码没有处理非 JSON 响应。建议把 catch 分支改成先resp.text看原始内容再解析 JSON。前端还有一个经验技巧点击识别后禁用按钮避免用户重复提交。5.5 训练时内存溢出图片没多大内存却爆了现象训练刚开始没多久进程就被系统杀掉或者 CPU 占用 100% 后 Python 直接退出。原因数据集图片全部被读入内存加上数据增强在内存里生成中间图叠加多进程读取内存峰值飙升。解决优先减小batch_size从 16 改到 8。其次检查image_dataset_from_directory的shuffle参数shuffleTrue时数据集会默认在内存里建一个 shuffle 缓冲区图片多时很耗内存改成shuffleFalse或者调小buffer_size。另外图片处理时不要用原图分辨率读取image_size调小到 128 也能缓解。对于十几类的入门规模这批问题不算致命但换到大数据集时这些经验能让训练不至于中途暴毙。6. 让识别结果更可信置信度阈值、迁移学习与实测验证6.1 在 HTML 页面里显示置信度和候选类别前面的 Flask 接口已经返回了 top3前端也展示了置信度。实战时你会发现一个关键问题模型对一张「不算鱼」的图片也会硬给一个最高概率比如手写数字图可能被预测成「草鱼」并给 70% 置信度。改进办法是加一个置信度阈值低于阈值时前端提示「无法可靠识别请换一张更清晰、目标居中的图片」。常见做法是在后端判断if float(probs[pred_idx]) 0.6: return jsonify({prediction: 无法识别, confidence: float(probs[pred_idx]), top3: top3}), 200阈值不是越高越好。阈值 0.9 时很多正常图片都会被拒用户体验差0.6 到 0.7 对常见鱼类这种类别间差异较大的任务比较稳妥。这个技巧能显著减少误判给人的挫败感比你一味调模型更见效。6.2 用迁移学习替换自建 CNN 的最后一层如果数据集的图片数量少、自建 CNN 的验证准确率一直上不去最有效的升级方案是迁移学习。用 MobileNetV2 或 ResNet50 作为特征提取器冻结前面的卷积层只训练最后的全连接层。TensorFlow 2.x 里写法很直观base_model tf.keras.applications.MobileNetV2( input_shape(160, 160, 3), include_topFalse, weightsimagenet, ) base_model.trainable False model models.Sequential([ layers.Rescaling(1.0 / 255), base_model, layers.GlobalAveragePooling2D(), layers.Dropout(0.5), layers.Dense(len(class_names), activationsoftmax), ])注意这里用了Rescaling因为 MobileNetV2 的输入期望在 0 到 1 区间另外要确保和训练时的预处理一致。迁移学习的实际收益在于ImageNet 上预训练的卷积核已经学会通用纹理和形状特征你的鱼图只需要微调最后的分类层通常几轮就有不错的准确率。但代价是模型文件变大、CPU 预测速度变慢所以小项目里先跑自建 CNN确认不够用再迁移是合理顺序。6.3 自己跑一轮 30 张真实图片的回归测试交项目前我习惯准备一个 30 张左右的真实测试集——不是从网上批量爬的而是实际用手机拍的、各种光线和角度都有的图片每类 3 到 5 张。把图片逐张上传到网页记录识别结果和置信度。如果某一类鱼频繁识别成另一类回看训练集里这两类的图片大概率是背景太相似或者其中一类样本太少。最后一个小习惯测试完后把每一张图的识别情况和置信度记录成一个 markdown 表格当作验收说明。这样不管是自己回头看还是交给别人评审都有据可查。这个项目真正教会我的事情是深度学习项目的完成度不只看模型训练代码更要看模型到 Web 页面之间的链路稳不稳。当我第一次因为中文路径问题调试了很久、又被置信度阈值救了一次后我就养成了「先跑通最小闭环再谈精度优化」的习惯。希望这次分享能帮你在做鱼类分类识别的路上少走几个弯路。本文还有配套的精品资源点击获取