资讯中心

Python卷积神经网络实战:牙齿健康识别从数据集到Web部署

📅 2026/9/28 16:43:47
Python卷积神经网络实战:牙齿健康识别从数据集到Web部署
简介这份资源面向具备一定Python与深度学习基础、希望上手图像分类实战的开发者与学习者提供了一套基于PyTorch卷积神经网络识别牙齿是否健康的完整代码与图片数据集。压缩包共269个文件以262张jpg牙齿图像为核心数据辅以3个py脚本、3个txt说明与1个html页面整体约13.89MB体积轻便便于本地部署。资源按流程拆分为数据集文本生成、模型训练与网页服务三个脚本训练过程会输出日志记录每个epoch的验证集损失与准确率并保存本地模型最终通过html服务生成可交互的网页地址在浏览器中即可完成识别体验。目前已有108人学习关注适合想快速跑通从数据准备、模型训练到网页端推理全链路的读者参考也可作为课程设计或毕业项目的实践素材。1. 牙齿健康识别项目从一张口腔照片到 CNN 判断结果中间到底发生了什么牙科诊所里最常见的场景不是拔牙而是医生拿着口镜看了一圈凭经验说“这颗牙有点问题先观察”。问题是不同医生的“有点问题”标准并不一致患者换一家诊所可能得到完全不同的结论。这个项目要解决的就是这件事用 Python 搭一个卷积神经网络把牙齿照片分成健康和不健康两类再套一个 HTML 网页界面让非技术人员也能上传图片直接看结果。整套东西包含图片数据集、训练脚本和 Web 前端属于典型的“深度学习 Web 部署”组合项目。适合谁做如果你正在学 Python 和卷积神经网络想找一个有真实数据集、能跑通完整流程、最后还能看到网页效果的练手项目这个方向比手写数字识别有意思得多。如果你是从业者想快速验证一个医学图像二分类的可行性这套流程也能直接复用。下面按“数据怎么准备 → 模型怎么搭 → 网页怎么接 → 坑在哪”的顺序拆开讲。2. 牙齿数据集怎么整理从原始照片到 CNN 能吃的张量2.1 先搞清楚数据集的目录结构和分类标准拿到一个牙齿图片数据集第一件事不是写代码而是打开文件夹看结构。常见的组织方式有两种一种是按类别分文件夹比如healthy/和unhealthy/各放一堆图片另一种是图片全混在一起用 CSV 文件标注每张图的类别。这个项目大概率是前者因为标题里写的是“含图片数据集”通常就是按文件夹分好的。先跑一段脚本统计一下每个类别的图片数量和尺寸分布这一步能帮你提前发现数据不平衡和尺寸混乱的问题import os from PIL import Image from collections import Counter data_dir dataset # 数据集根目录 for split in [train, val, test]: split_dir os.path.join(data_dir, split) if not os.path.exists(split_dir): continue for cls in os.listdir(split_dir): cls_dir os.path.join(split_dir, cls) if not os.path.isdir(cls_dir): continue imgs [f for f in os.listdir(cls_dir) if f.lower().endswith((.jpg, .png, .jpeg))] sizes Counter() for f in imgs[:50]: # 抽样看尺寸不用全量 with Image.open(os.path.join(cls_dir, f)) as im: sizes[im.size] 1 print(f{split}/{cls}: {len(imgs)} 张, 尺寸分布(前50张): {sizes.most_common(3)})这段代码做三件事遍历 train/val/test 三个子集统计每个类别的图片数量抽样查看图片尺寸。如果发现健康类有 800 张、不健康类只有 200 张那训练时就要考虑类别权重或者过采样。如果尺寸从 200x200 到 2000x1500 都有后面统一 resize 到 224x224 就行不用太纠结原始尺寸。提示如果数据集没有预先分好 train/val/test需要自己按 7:1.5:1.5 的比例切分。切分时注意同一个人的牙齿照片不要同时出现在训练集和验证集里否则验证准确率会虚高。2.2 用 ImageDataGenerator 做在线增强别把增强后的图存回硬盘数据量不大的时候增强是必须的。但很多人会把增强后的图片生成出来存成新文件再重新读一遍这纯属浪费时间。Keras 的ImageDataGenerator可以在训练时实时增强内存里直接完成不占硬盘from tensorflow.keras.preprocessing.image import ImageDataGenerator IMG_SIZE (224, 224) BATCH_SIZE 32 train_datagen ImageDataGenerator( rescale1.0 / 255, # 像素归一化到 [0,1] rotation_range15, # 随机旋转 ±15 度 width_shift_range0.1, # 水平平移 10% height_shift_range0.1, # 垂直平移 10% horizontal_flipTrue, # 水平翻转牙齿左右对称可以翻 zoom_range0.1, # 随机缩放 10% fill_modenearest # 旋转后空白用最近像素填充 ) val_datagen ImageDataGenerator(rescale1.0 / 255) # 验证集只归一化不增强 train_gen train_datagen.flow_from_directory( dataset/train, target_sizeIMG_SIZE, batch_sizeBATCH_SIZE, class_modebinary # 二分类用 binary ) val_gen val_datagen.flow_from_directory( dataset/val, target_sizeIMG_SIZE, batch_sizeBATCH_SIZE, class_modebinary )参数说明rotation_range设 15 是因为牙齿照片通常拍摄角度比较正旋转太多反而不真实。horizontal_flip可以开牙齿左右翻转不影响健康判断。fill_mode用nearest比默认的constant好不会在边缘引入黑边。class_mode选binary对应二分类如果后面要扩展到多种牙病类型改成categorical。注意验证集的ImageDataGenerator只做rescale不要加任何增强参数。否则验证指标会波动很大你根本不知道模型是真的变好了还是增强的随机性导致的。3. 卷积神经网络怎么搭从零写一个还是用预训练模型3.1 小数据集从零训练 CNN 的结构设计如果数据集只有几千张图从零搭一个 4 层卷积的 CNN 就够了不需要上 ResNet。结构大概是卷积 → 池化 → 卷积 → 池化 → 卷积 → 池化 → 全局平均池化 → 全连接 → 输出。每层卷积后面加 BatchNormalization 和 ReLU最后用 sigmoid 输出二分类概率from tensorflow.keras import layers, models def build_cnn(input_shape(224, 224, 3)): model models.Sequential([ layers.Conv2D(32, (3, 3), paddingsame, input_shapeinput_shape), layers.BatchNormalization(), layers.Activation(relu), layers.MaxPooling2D((2, 2)), layers.Conv2D(64, (3, 3), paddingsame), layers.BatchNormalization(), layers.Activation(relu), layers.MaxPooling2D((2, 2)), layers.Conv2D(128, (3, 3), paddingsame), layers.BatchNormalization(), layers.Activation(relu), layers.MaxPooling2D((2, 2)), layers.Conv2D(128, (3, 3), paddingsame), layers.BatchNormalization(), layers.Activation(relu), layers.GlobalAveragePooling2D(), layers.Dropout(0.5), layers.Dense(64, activationrelu), layers.Dense(1, activationsigmoid) # 二分类输出一个概率值 ]) return model model build_cnn() model.compile( optimizeradam, lossbinary_crossentropy, metrics[accuracy, auc] ) model.summary()关键点GlobalAveragePooling2D替代Flatten参数量少很多不容易过拟合。Dropout(0.5)放在全连接层前面训练时随机丢弃一半神经元。输出层用sigmoid把值压到 0 到 1 之间大于 0.5 判为不健康小于 0.5 判为健康。metrics里加auc是因为医学图像经常类别不平衡准确率会骗人AUC 更可靠。3.2 用预训练模型做迁移学习什么时候该冻结、什么时候该解冻如果数据集只有几百张图从零训练基本没戏这时候用预训练的 MobileNetV2 或 EfficientNetB0 更实际。做法是加载 ImageNet 预训练权重去掉顶层分类头换成自己的二分类输出from tensorflow.keras.applications import MobileNetV2 base_model MobileNetV2( input_shape(224, 224, 3), include_topFalse, # 去掉 ImageNet 的 1000 类分类头 weightsimagenet # 加载预训练权重 ) base_model.trainable False # 第一阶段冻结所有卷积层 model models.Sequential([ base_model, layers.GlobalAveragePooling2D(), layers.Dropout(0.3), layers.Dense(1, activationsigmoid) ]) model.compile(optimizeradam, lossbinary_crossentropy, metrics[accuracy, auc])第一阶段先冻结 base_model只训练顶部分类头学习率用默认的 1e-3。跑 10 个 epoch 后如果验证集准确率不再提升再解冻最后 20 层做微调学习率降到 1e-5。这个“先冻结再解冻”的策略是迁移学习的标准操作能避免随机初始化的分类头把预训练权重带偏。提示解冻微调时BatchNormalization 层要保持冻结状态否则均值和方差会被小批量数据带偏验证集表现会突然掉下去。这是很多人踩过的坑。4. 训练完的模型怎么接到 HTML 网页上Flask 做后端前端只管传图4.1 用 Flask 写一个接收图片并返回预测结果的接口模型训练完保存成.h5文件接下来要让它能被网页调用。最轻量的方案是 Flask不需要装一堆依赖几十行代码就能跑起来from flask import Flask, request, jsonify, render_template from tensorflow.keras.models import load_model from tensorflow.keras.preprocessing import image import numpy as np import io app Flask(__name__) model load_model(teeth_cnn.h5) # 加载训练好的模型 app.route(/) def index(): return render_template(index.html) # 返回前端页面 app.route(/predict, methods[POST]) def predict(): file request.files.get(file) if not file: return jsonify({error: 没有收到图片}), 400 img_bytes file.read() img image.load_img(io.BytesIO(img_bytes), target_size(224, 224)) x image.img_to_array(img) / 255.0 x np.expand_dims(x, axis0) # 增加 batch 维度 pred model.predict(x)[0][0] # sigmoid 输出0~1 之间 label 不健康 if pred 0.5 else 健康 confidence float(pred) if pred 0.5 else float(1 - pred) return jsonify({label: label, confidence: round(confidence, 4)}) if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse)逻辑说明/predict接口接收 POST 请求从request.files里取出图片文件用 Keras 的load_img读成 224x224 的 RGB 数组归一化后加一个 batch 维度变成(1, 224, 224, 3)然后送进模型。model.predict返回一个 0 到 1 之间的浮点数大于 0.5 判为不健康同时把置信度也返回给前端。参数说明host0.0.0.0让局域网内其他设备也能访问debugFalse在生产环境必须关掉否则会暴露源码。端口 5000 是 Flask 默认端口如果被占用改成 5001 或 8080。4.2 HTML 前端页面上传、预览、显示结果三件事前端不需要框架一个 HTML 文件加几十行 JavaScript 就够了。核心是三块文件选择框、图片预览区、结果展示区!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title牙齿健康检测/title style body { font-family: sans-serif; max-width: 600px; margin: 40px auto; padding: 0 16px; } #preview { max-width: 100%; margin-top: 16px; display: none; } #result { margin-top: 20px; font-size: 1.2em; font-weight: bold; } button { padding: 10px 24px; font-size: 1em; cursor: pointer; } /style /head body h2上传牙齿照片检测是否健康/h2 input typefile idfileInput acceptimage/* button onclickupload()开始检测/button img idpreview alt预览 div idresult/div script const fileInput document.getElementById(fileInput); const preview document.getElementById(preview); const result document.getElementById(result); fileInput.addEventListener(change, () { const file fileInput.files[0]; if (file) { preview.src URL.createObjectURL(file); preview.style.display block; result.textContent ; } }); async function upload() { const file fileInput.files[0]; if (!file) { alert(请先选择一张图片); return; } const formData new FormData(); formData.append(file, file); result.textContent 检测中...; try { const resp await fetch(/predict, { method: POST, body: formData }); const data await resp.json(); if (data.error) { result.textContent 出错 data.error; } else { result.textContent 结果${data.label}置信度 ${(data.confidence * 100).toFixed(1)}%; } } catch (e) { result.textContent 请求失败请检查后端是否启动; } } /script /body /html这段 HTML 做了三件事input typefile让用户选图片change事件触发后用URL.createObjectURL在页面上预览点击按钮后通过fetch把图片以FormData格式 POST 到/predict接口拿到 JSON 结果后显示在页面上。整个交互不需要刷新页面用户体验比传统表单提交好很多。注意fetch请求的路径是/predict如果 Flask 跑在 5000 端口前端页面也由 Flask 的render_template返回那路径直接写/predict就行。如果前端单独用其他方式打开需要写完整地址http://localhost:5000/predict否则会跨域报错。5. 训练和部署过程中最容易翻车的几个地方5.1 验证集准确率很高但实际用起来一塌糊涂现象训练时验证集准确率到了 95%但拿新照片去测结果乱七八糟。原因数据集里健康和不健康的图片可能来自不同的拍摄设备或拍摄环境模型学到的是“设备特征”而不是“牙齿特征”。比如健康组的照片都是某台相机拍的偏冷色调不健康组是另一台拍的偏暖色调模型只要判断色调就能“作弊”。解决把数据集按拍摄来源分层划分确保训练集和验证集里都有不同来源的图片。如果做不到至少做一次交叉验证看不同折之间的准确率波动大不大。波动超过 10% 就说明数据分布有问题。5.2 模型预测结果全是同一个类别现象不管传什么图片模型都输出“健康”或都输出“不健康”。原因类别极度不平衡。比如健康图片 2000 张不健康只有 100 张模型只要全猜健康就能拿到 95% 的准确率梯度下降会直接把这个“捷径”学会。解决在model.fit里加class_weight参数给少数类更高的权重from sklearn.utils.class_weight import compute_class_weight import numpy as np labels train_gen.classes # 0 和 1 的数组 weights compute_class_weight(balanced, classesnp.unique(labels), ylabels) class_weight dict(enumerate(weights)) # 训练时传入 # model.fit(train_gen, validation_dataval_gen, epochs20, class_weightclass_weight)compute_class_weight会自动算出反比权重少数类的权重会更大模型不敢随便全猜多数类。5.3 Flask 上传大图时内存爆掉现象上传一张 4000x3000 的手机照片Flask 进程直接卡死或报 MemoryError。原因image.load_img会先把整张图读进内存再 resize大图占的内存是原始尺寸的几倍。如果同时有几个请求内存直接撑爆。解决在读取之前先限制文件大小Flask 里可以设MAX_CONTENT_LENGTHapp.config[MAX_CONTENT_LENGTH] 5 * 1024 * 1024 # 限制 5MB超过 5MB 的请求会直接返回 413 错误不会进入模型推理。另外可以在前端用 canvas 先压缩再上传减少传输量。5.4 模型文件加载慢导致网页首屏卡顿现象Flask 启动后第一次请求要等十几秒才返回结果。原因load_model放在请求处理函数里每次请求都重新加载一遍模型。解决把load_model放在模块顶层Flask 启动时只加载一次后续请求直接复用。上面第 4 章的代码已经是这样写的但很多人会不小心把它写进app.route里面。5.5 前端上传后预览正常但后端收到空文件现象页面上图片预览没问题但后端request.files.get(file)返回None。原因FormData的字段名和 Flask 里取的字段名不一致。前端formData.append(file, file)写的是file后端也必须用request.files.get(file)大小写和拼写都要对上。解决前后端字段名统一用file不要一边写image一边写file。调试时可以在后端打印request.files的 keys 确认。6. 把模型准确率再往上推一截的实用技巧训练完第一版模型后如果 AUC 卡在 0.85 左右上不去可以试几个成本低但效果明显的操作。第一个是调整输入分辨率224x224 对牙齿细节来说可能不够改成 320x320 或 448x448 通常能涨 2 到 3 个点代价是训练时间变长。第二个是换优化器Adam 换成 AdamW权重衰减能抑制过拟合尤其在全连接层参数量大的时候效果明显。第三个是学习率调度用ReduceLROnPlateau在验证损失不下降时自动降学习率比固定学习率稳得多。from tensorflow.keras.callbacks import ReduceLROnPlateau, EarlyStopping callbacks [ ReduceLROnPlateau(monitorval_loss, factor0.5, patience3, min_lr1e-6), EarlyStopping(monitorval_auc, patience8, restore_best_weightsTrue, modemax) ] # model.fit(train_gen, validation_dataval_gen, epochs50, callbackscallbacks)ReduceLROnPlateau在验证损失连续 3 个 epoch 不降时把学习率砍半EarlyStopping在验证 AUC 连续 8 个 epoch 不升时停止训练并恢复最佳权重。这两个回调一起用基本不用手动调 epoch 数。验证模型是否真的可用的方法找 20 张训练集里完全没有的图片人工标注健康/不健康然后跑一遍看准确率。如果和验证集准确率差距在 5 个点以内说明模型没有严重过拟合可以继续优化。差距超过 15 个点说明数据划分或增强策略有问题得回头检查。我自己做这类项目最大的教训是不要一上来就调模型结构。先把数据看一遍把类别平衡、尺寸统一、训练验证划分这三件事做扎实比换什么网络都管用。我见过太多人花一周调 ResNet 层数结果发现数据集里有一半图片是重复的。希望帮到你。本文还有配套的精品资源点击获取

看完文章,想为自己的企业也做一次专业网站诊断?

尧图顾问免费为您评估现有网站,并给出建站/改版建议与报价方案。

免费获取方案