简介一份基于深度学习的皮肤病识别系统完整项目面向毕业设计、课程设计与期末大作业场景适合需要快速搭建图像识别应用的开发者。项目以CNN为特征提取基础结合YOLO目标检测思路覆盖数据准备、模型训练、服务端推理以及微信小程序客户端展示的完整链路。压缩包共49个文件、约285KB主要包含Python训练与预测脚本、服务端接口、微信小程序前端页面js/wxml/wxss/json、ISIC2018数据集标注csv及训练测试文本划分文件结构上分为服务端与微信客户端两部分便于按模块查阅。目前已有88人学习下载。对需要参考完整可运行方案、理解深度学习识别系统前后端协作方式的读者这套代码能提供直接的工程实现与项目组织思路尤其适合作为课程或毕业设计的起点。1. 皮肤病变识别为什么非深度学习不可从一张照片到一类风险皮肤科医生每天面对几十张皮肤病变照片时靠颜色、边界、对称性这些形态学特征做判断而这些特征在不同肤色、不同拍摄光线、不同焦段下变化极大。传统程序化识别用颜色直方图加边缘检测做分类在严格光照的实验室数据集上能跑一换到手机随手拍的照片就彻底翻车——边界检测把毛发当成病灶轮廓颜色阈值在暗光下压根分不清痣和黑色素瘤。深度学习做皮肤病识别系统本质是把这件事从「人写规则」变成「让网络从大量已标注病例里自己学特征」从几十万张已确诊的皮肤镜图像里提炼出医生反复训练多年才具备的形态学直觉然后压缩成一套可部署的神经网络的权重。这套基于深度学习的皮肤病识别系统解决的是一个典型的单标签图像分类问题输入一张皮肤病变区域的照片输出该病变最可能对应的疾病类别或良恶性概率。它的价值不是替代皮肤科医生而是给基层诊所、体检机构和普通人提供一个低成本初筛工具。你拿到一个zip包里面通常有代码、预训练权重和数据集说明照着一个可复现的流程就能在本地跑起来。读者里既有想复现项目完成课程设计的学生也有想做辅助诊断原型的团队这篇笔记把从解压zip到模型部署全链路的关键决策和坑说透。2. 先定技术栈再动手模型架构、损失函数和数据的三个选择2.1 为什么是迁移学习而不是从零训练CNN小样本医学影像的必然选择皮肤病识别系统的训练集规模公开数据集里最大的也就几万张按病变类别一摊每个类别常常不到一千张。拿这样的数据量从零训练一个ResNet结果几乎必然是过拟合。深度学习在ImageNet上能成功靠的是一千多万张图片和大量训练时间医学影像没有这个条件。所以迁移学习不是一种优化选择而是做皮肤病识别系统能落地的必要前提。迁移学习的做法很直接拿一个在ImageNet上预训练好的模型ResNet34、ResNet50、EfficientNet都是常见选择把它的卷积层当作一个训练好的特征提取器只替换最后的全连接分类层让它输出我们定义的皮肤病类别数。微调时有两个常见策略第一种是冻结backbone所有参数只训练新加的全连接层适合数据集很小每类几百张的情况第二种是解冻backbone的后半部分比如最后两个Stage用一个较小的学习率参与训练适合数据量中等且和ImageNet域差异较大的情况。皮肤病图像虽然也是自然图像但纹理细节和ImageNet里的日常物体差异不小我一般会先冻结训练几个epoch等分类层收敛后再解冻后半段微调效果比一步到位要好。模型选择上新手优先用ResNet34而非ResNet50。两者在皮肤病识别这种细粒度分类任务上精度差距很小——可能就一两个百分点——但ResNet34显存占用少、训练速度快、调试迭代周期短。EfficientNet系列在参数效率上更优但训练时对数据增强和正则化更敏感新手容易在调参上栽跟头。先把ResNet34跑通全流程再换EfficientNet-B4追求更高精度这是常见做法。2.2 损失函数怎么选CrossEntropy不够用类别不平衡是皮肤病数据的常态皮肤病数据集几乎永远是不平衡的。拿公开数据集举例良性痣和脂溢性角化病的样本数可能是黑色素瘤的十倍以上。如果直接拿CrossEntropyLoss训练模型学到的会是一个「大部分时候猜良性就够」的懒分类器在罕见病类别上准确率惨不忍睹。处理不平衡有两个常用手段。一是给损失函数加类别权重权重和样本数成反比。PyTorch里torch.nn.CrossEntropyLoss自带weight参数先统计每个类别的样本数把频率倒数和归一化值填进去就行。二是换Focal LossPyTorch官方没有直接实现需要手写十几行。Focal Loss在CrossEntropy基础上加了一个调制因子(1-p)^gamma让模型把注意力放在难分类的样本上gamma一般取2。我的经验是先试加权CrossEntropy因为调起来快、行为可预期Focal Loss在后处理阶段确实能拉高罕见类别的召回但训练过程更容易震荡需要配更小的学习率。还有一个容易被忽略的点皮肤病分类的标签不是完全互斥的。同一张照片可能同时有炎症和色素性病变的特征但多数数据集的标注方式都是单标签——标注者选一个最主要诊断。这个标注噪声会直接变成模型性能的天花板所以损失函数设计得再好都补不了训练集标签本身的模糊性。做系统时输出层用多输出头的思路一个头分类良恶性一个头细分疾病类别有时比单一Softmax更稳健。2.3 数据集准备与标签清洗从公开数据集到自己的拍摄样本训练数据一般来自两类渠道公开数据集ISIC、HAM10000、DermNet的子集和自建样本医院脱敏数据或自己拍摄的照片。主流做法是先拿公开数据集预训练再用自建样本做微调因为公开数据集的皮肤镜图像和手机实拍照片差距很大直接训练泛化效果差。数据准备阶段的目录结构PyTorch的torchvision.datasets.ImageFolder要求图片按类别放在子目录下目录名就是类别名。拿到zip包先别急着训练用下面这段代码检查一下数据集分布确认每个类别的样本量和图片尺寸import os from collections import Counter from PIL import Image data_root ./skin_data exts (.jpg, .jpeg, .png, .bmp) for split in [train, val]: split_root os.path.join(data_root, split) if not os.path.isdir(split_root): continue counter Counter() size_counter Counter() for cls in os.listdir(split_root): cls_dir os.path.join(split_root, cls) if not os.path.isdir(cls_dir): continue for fname in os.listdir(cls_dir): if fname.lower().endswith(exts): counter[cls] 1 with Image.open(os.path.join(cls_dir, fname)) as im: size_counter[im.size] 1 print(f[{split}] 类别分布: {dict(counter)}) print(f[{split}] 图片尺寸分布: {size_counter.most_common(5)})这段代码做的事很简单统计每个子目录的图片数量看看类别分布是否极端统计图片尺寸看是否需要在预处理里统一Resize。皮肤病图像的原始尺寸跨度很大皮肤镜图像可能是1024x1024手机照片可能是3024x4032而模型输入通常是224x224或256x256直接暴力Resize会把病灶细节压扁有些细碎的纹理特征就丢了。我的做法是如果图片长宽比接近1:1直接用Resize((256, 256))加CenterCrop(224)如果图片是明显长方形手机拍摄常见先按短边Resize到256再做CenterCrop避免各方面被非均匀压缩。另外检查是否有损坏的图片文件——PIL在打开损坏文件时会抛异常或返回None如果一个类别里混了几张坏图训练时会随机报错。3. 跑通zip包内的训练流程从解压环境到模型产出3.1 环境搭建与目录确认先看清zip包里的固定结构拿到zip包先解压然后看目录结构。一般一个标准项目包里至少包含data/数据集或其软链接、models/网络定义、utils/工具函数、train.py、infer.py或predict.py、requirements.txt。有些包的requirements.txt写得比较随意Python版本和PyTorch版本不匹配跑起来才发现问题。推荐用conda新建一个独立环境Python 3.10配PyTorch 2.x是目前最稳的组合。安装依赖时不要把整个requirements.txt一把装完有些包会冲突。先装核心依赖跑一次再说conda create -n skin python3.10 conda activate skin pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install numpy pandas pillow matplotlib tqdm scikit-learntorch和torchvision分开装是关键。requirements.txt里锁定的版本经常和你的CUDA版本对不上这种情况下官方index-url指定CUDA版本的装法最省事。cu118对应CUDA 11.8如果你本机是CUDA 12.x换成cu121或cu124。装完之后用python -c import torch; print(torch.__version__, torch.cuda.is_available())验证一下输出True再继续。3.2 数据加载和数据增强在线增强在皮肤病场景下怎么调参数用ImageFolder加载数据集最省事。torchvision的transforms.Compose把若干个图像变换串成一条流水线。训练集和验证集的transform必须分开写训练集要加随机增强验证集只做Resize和归一化。这是最常见的翻车点——有人把增强写进验证集导致验证集每次evaluate的图片都不一样指标波动巨大。from torchvision import transforms from torch.utils.data import DataLoader from torchvision.datasets import ImageFolder train_transform transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(15), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.1), transforms.CenterCrop(224), 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_loader DataLoader( ImageFolder(./skin_data/train, transformtrain_transform), batch_size32, shuffleTrue, num_workers4, pin_memoryTrue ) val_loader DataLoader( ImageFolder(./skin_data/val, transformval_transform), batch_size32, shuffleFalse, num_workers4, pin_memoryTrue )这里组合同行的两个常用参数值得留意。RandomHorizontalFlip和RandomRotation(15)——皮肤病图像有方向性吗严格说几乎没有旋转一个小角度不影响视觉判断所以这两个增强比较安全。ColorJitter要小心亮度变化太大可能让模型过拟合「照片很暗」这个无关特征我一般把亮度范围压到0.15-0.2。Normalize用的是ImageNet预训练模型的默认均值和标准差迁移学习时必须用它不能用自己数据集的均值和标准差计算——那样会破坏预训练权重的分布效果直接崩掉。batch_size32在ResNet34上比较均衡显存不够就降到16同时学习率也得往下调。3.3 训练循环和超参调节冻结特征层、解冻微调、找最大学习率训练部分的常见做法是用torchvision.models.resnet34(weightsResNet34_Weights.IMAGENET1K_V1)加载预训练权重然后替换分类层import torch import torch.nn as nn from torchvision import models model models.resnet34(weightsmodels.ResNet34_Weights.IMAGENET1K_V1) num_classes 5 model.fc nn.Linear(model.fc.in_features, num_classes) # 先冻结 backbone只训练分类层 for param in model.parameters(): param.requires_grad False for param in model.fc.parameters(): param.requires_grad True model model.cuda() optimizer torch.optim.Adam(model.fc.parameters(), lr1e-3)冻结backbone只训练分类层这个阶段通常跑5到8个epoch。分类层是从随机初始化的全连接开始的如果一开始就让它和backbone一起训练梯度很容易把预训练特征冲乱。等分类层的loss降到接近稳定再做阶段二——解冻backbone的最后一个残差Stage把学习率降到1e-4左右用更低的动量继续微调# 阶段二解冻最后两个残差层layer3 和 layer4 for name, param in model.named_parameters(): if layer3 in name or layer4 in name: param.requires_grad True optimizer torch.optim.SGD([ {params: model.fc.parameters(), lr: 1e-4}, {params: model.layer3.parameters(), lr: 1e-5}, {params: model.layer4.parameters(), lr: 1e-5}, ], momentum0.9, weight_decay1e-4)阶段二用SGD而不用Adam这是长期实践出来的选择。Adam在分类层上收敛快、调参省心但在解冻后的卷积层上容易把权重更新得过于激进有时验证指标不升反降。SGD加上momentum0.9和weight_decay1e-4更稳重适合微调场景。至于学习率新手最容易犯的错误是直接取一个固定值。先跑一段学习率扫描用一个极小的lr1e-5开始训练每个batch按指数增加到一个较大的lr1e-1把loss下降最陡的区间作为lr的选择依据。这个方法在几行代码就能实现效果比拍脑袋设lr0.001靠谱得多。3.4 训练过程的监控和最佳权重保存不要看最后一个epoch的权重训练时每个epoch都要在验证集上evaluate一次只看训练集loss是没有意义的。皮肤病数据集过拟合速度比普通图像数据集快得多——经常训练集accuracy已经到98%了验证集还在60%徘徊。所以save checkpoint的逻辑必须以验证集指标为准准确率或f1分数涨了才覆盖保存而不是每个epoch都存。from sklearn.metrics import f1_score import copy best_f1 0.0 best_state None for epoch in range(total_epochs): train_loss train_one_epoch(model, train_loader, optimizer, criterion) val_f1, val_loss evaluate(model, val_loader) if val_f1 best_f1: best_f1 val_f1 best_state copy.deepcopy(model.state_dict()) print(fEpoch {epoch1}: train_loss{train_loss:.4f}, val_loss{val_loss:.4f}, val_f1{val_f1:.4f}, best_f1{best_f1:.4f}) torch.save(best_state, best_model.pt)打印每个epoch的验证F1而不是只打印accuracy因为accuracy在类别不平衡时是骗人的。类别分布大约是良性占70%时分类器全部输出良性accuracy也有70%但F1分数会直接暴露问题。我通常用macro-F1所有类别的F1取算术平均作为保存权重的主要依据它不会让多数类淹没了少数类的表现。4. 模型落地为识别系统推理接口、置信度过滤和Web部署4.1 加载训练好的模型先做一次不经过Web框架的裸推理训练完的权重文件是一个checkpoint里面存的是PyTorch模型的状态字典。做推理时要先重建模型结构再加载权重最后切换到eval模式。漏掉model.eval()是最常见的推理错误——不调用它模型里的Dropout和BatchNorm还在用训练时的行为推理结果每次跑都不一样。import torch from torchvision import models, transforms from PIL import Image import numpy as np model models.resnet34(weightsNone) num_classes 5 model.fc torch.nn.Linear(model.fc.in_features, num_classes) model.load_state_dict(torch.load(best_model.pt, map_locationcpu)) model.eval() 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]) ]) img Image.open(test_lesion.jpg).convert(RGB) input_tensor transform(img).unsqueeze(0) with torch.no_grad(): logits model(input_tensor) probs torch.softmax(logits, dim1).squeeze(0).numpy() class_names [melanocytic_nevus, basal_cell_carcinoma, actinic_keratosis, dermatofibroma, melanoma] # 按训练时类别顺序 for name, p in zip(class_names, probs): print(f{name}: {p:.4f})软分类结果是关键——拿到概率向量而不是直接argmax后面做置信度过滤、阈值调整、结果解释都依赖它。训练时的类别顺序要和推理时保持一致系统的class_names必须严格按训练数据集的目录字母顺序排列ImageFolder默认按字母序给类别编号否则输出的结果就是错的。4.2 用FastAPI搭一个推理接口批量推理和进程避坑单张图片测试通过后就要把模型封装成一个推理服务。最常见的做法是FastAPI性能好、自带文档界面配一个全局模型实例。这里有一个必须注意的坑PyTorch的模型实例不要在每个请求里重复加载——加载一次是秒级重复加载会让并发请求互相阻塞。from fastapi import FastAPI, UploadFile, File from pydantic import BaseModel import torch from torchvision import models, transforms from PIL import Image import io import numpy as np app FastAPI(titleSkin Lesion Classifier) model models.resnet34(weightsNone) num_classes 5 model.fc torch.nn.Linear(model.fc.in_features, num_classes) model.load_state_dict(torch.load(best_model.pt, map_locationcpu)) model.eval() 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]) ]) class_names [melanocytic_nevus, basal_cell_carcinoma, actinic_keratosis, dermatofibroma, melanoma] class PredictionResponse(BaseModel): top_class: str top_prob: float all_probs: dict app.post(/predict, response_modelPredictionResponse) async def predict(file: UploadFile File(...)): img_bytes await file.read() img Image.open(io.BytesIO(img_bytes)).convert(RGB) input_tensor transform(img).unsqueeze(0) with torch.no_grad(): logits model(input_tensor) probs torch.softmax(logits, dim1).squeeze(0).numpy() top_idx int(np.argmax(probs)) return PredictionResponse( top_classclass_names[top_idx], top_probfloat(probs[top_idx]), all_probs{name: float(p) for name, p in zip(class_names, probs)} ) # 启动uvicorn main:app --host 0.0.0.0 --port 8000这个接口接受图片上传返回最可能的类别、对应概率和所有类别的完整概率分布。返回完整分布而不是只给最高一项是有意的设计——前端的展示和后处理逻辑可以用它实现很多策略比如「没有一个类别超过0.6就提示用户重新拍一张」而不是强行给一个不靠谱的分类结果。进程管理的坑FastAPI默认开Uvicorn单进程就够了千万不要在没加gunicorn或--workers的情况下自信开多进程做「性能优化」每个进程都会加载一份模型副本显存瞬间耗尽。有并发需求时用gunicorn -w 2 -k uvicorn.workers.UvicornWorkerworkers数建议不超过2。4.3 置信度输出和产品交互给用户一个可行动的指示而不是一个裸诊断模型输出melanoma: 0.85对患者没有直观意义对医生又不够精确。产品化的常见做法是设置两级策略高置信度比如max_prob 0.8时给出对应的建议文案低置信度时明确告诉用户「这张照片的特征不够明确建议去皮肤科就诊或重新拍摄」。这个阈值不是随便设的应该在验证集上做一次阈值扫描画出准确率-覆盖率曲线来定。另一个值得做的事是保存推理日志。字段包括图片哈希、原始文件名、模型输出的概率分布、用户是否回传了后续诊断结果。这个日志在灰度阶段是宝贵的真实场景验证数据。皮肤病识别的模型精度天花板不只取决于网络结构还取决于用户拍的图像质量把这个反馈闭环跑起来系统才会越来越稳定。5. 皮肤病识别系统的避坑清单5个必须躲开的坑5.1 过拟合训练集accuracy 99%验证集却只有60%现象训练到第10个epoch训练集accuracy到了99%验证集accuracy停在61%不再上升loss差距越拉越大。原因皮肤病数据集样本少、图像背景相对单一网络很容易把训练集里的噪声比如某张图的偏色、某位摄影师的水印当作特征记住。解决换更强的数据增强RandomErasing、Cutout模型换成带Dropout或加DropPath的版本或者用早停patience8个epoch训练集loss不降就停止训练。我见过有人靠增加训练轮数想「再救一救」结果越救越糟——过拟合在皮肤病识别上是完全不可逆的劣化一旦权重开始记住噪声特征后面所有微调都只会越陷越深。5.2 类别不平衡导致罕见病全被识别为良性现象训练完成后黑色素瘤类别的召回率只有15%而权威数据集的同类基准在60%以上。原因黑色素瘤在训练集中占比太低几十张对几千张Softmax分类器不需要学会识别它因为永远把输入判成良性类总损失已经很低了。解决先做加权CrossEntropyLoss再做过采样或Focal Loss。注意加权后学习率要调低否则权重大的类别更新幅度太猛会导致震荡。判断是否修好的指标用recall和specificity看类别明细不看整体accuracy。5.3 皮肤镜图像和手机照片的域差异模型在训练集上炫耀在真实用户照片上翻车现象在ISIC数据集上验证准确率89%部署后拿用户手机拍的照片测试准确率掉到50%以下。原因皮肤镜图像是在标准光源、固定倍率、均匀背景下拍的手机照片有环境光干扰、阴影、毛发遮挡、运动模糊域差异非常大。解决如果有条件用目标域的少量真实照片对模型做二次微调哪怕只有每类50张也能显著拉回精度。如果拿不到真实数据至少要在训练集上叠加随机噪声、模糊、亮度变化等模拟手机照片的增强。这是一个方向性问题。如果系统的目标用户是屏幕另一端用手机拍照的人不能用皮肤镜数据集作为最终验证集验证集必须模拟真实使用场景。我在做这类系统时会故意把验证集换成一批分辨率参差不齐的照片——切掉exif信息、缩放到800像素宽、甚至模拟室内暗光——验证集和你最终交付的目标不一致时测试指标完全不可信。5.4 zip解压后文件缺失或目录错位现象把项目zip解压后直接跑python train.py报错FileNotFoundError: [Errno 2] No such file or directory: skin_data/train/...。原因zip包在打包时保留了相对路径解压后多了一个嵌套目录比如skin-classifier/skin_data代码里写的是相对路径./skin_data找不到文件。另一类情况是.gitignore把大文件过滤了数据集的图片一个都没进zip包。解决解压后先find . -type d | head -20看目录树确认数据集实际位置如果代码支持命令行参数直接用--data_root指定实际路径。顺便养成一个好习惯——项目里的所有数据路径一律用相对路径或环境变量不写死绝对路径这样换机器时不用改代码。5.5 类别标签映射错位现象推理时模型预测一个类别比对后发现输出对应的名称和训练时的语义完全不对。原因训练数据在整理时某个子目录放错了图片或者训练代码和推理代码的class_names顺序不一致。ImageFolder按字母顺序给类别编号而推理时如果用人工指定的顺序去对应fc层的输出只要有两处对不上结果就全乱了。解决训练完成后把class_to_idx保存成一个JSON文件import json dataset ImageFolder(./skin_data/train) with open(class_to_idx.json, w) as f: json.dump(dataset.class_to_idx, f, indent2)推理时直接读这个文件不做任何手动映射。这是个小操作但血泪地说它比任何模型调参都值得更早落实——「模型有90%准确率但类别名对错了」是这类系统最尴尬的翻车事故。6. 把这个系统做得更靠谱置信度校准、可解释性和灰度发布先关注置信度校准。神经网络的Softmax输出不是真正的概率——训练好的模型经常过度自信给出一批0.95以上的预测其中实际正确的比例并没有95%。皮肤病识别这种高风险决策场景置信度不准比类别错更麻烦因为产品逻辑完全依赖阈值判断。做校准的常见方法是温度标度Temperature Scaling在验证集上找一组温度参数对模型输出的logits做一个标量除法使输出的Softmax分布和真实准确率尽可能一致。import torch from torch.nn import functional as F from scipy.optimize import minimize_scalar def temperature_scale(logits, labels): # logits: (N, C) 的原始输出 # labels: (N,) 的真实标签 def ce_loss(t): scaled logits / t nll F.cross_entropy(scaled, labels) return nll.item() result minimize_scalar(ce_loss, bounds(0.2, 5.0), methodbounded) return result.x # 用验证集batch收集logits和labels后调用 # temp temperature_scale(val_logits, val_labels)温度大于1表示模型过度自信把温度调到1.5到2.0之间Softmax输出会更平滑决策阈值也就有了实际意义。校准后的概率分布和准确率的对应关系必须用可靠性图reliability diagram验证一遍——把预测概率分成10个桶每桶内实际正确率和平均预测概率画在同一个坐标轴上曲线接近对角线就说明校准到位了。再配上可解释性输出。只给类别名称的识别系统医生不敢用也懒得用。Grad-CAM可以在推理时生成热力图告诉使用者「模型为什么这样判断」。用PyTorch实现时注册目标层一般是layer4的最后一个卷积层的前向钩子和反向钩子提取梯度做全局平均池化再和特征图相乘得到形状与特征图一致的热力图。热力图叠加在原图上能直观看出模型是否聚焦在病灶区域——如果模型盯着照片角落的光斑判断疾病这份输出就是安全的警告信号。灰度上线和持续验证是最后一道防线。我的习惯是模型在公开数据集上精度再高也不直接全部流量上线。先让系统在内部使用或小范围灰度同时让皮肤科医生抽样评估模型的错误样本——重点看假阴性的形态是否一致比如是否集中在某类颜色不典型的黑色素瘤上这比任何离线指标都更能说明系统在真实场景中的行为。连续做两周灰度收集足够的反馈后再决定是否全量放开。最后说一个我的教训做皮肤病识别系统最容易犯的错是把「模型准确率」和「系统价值」划等号。准确率再高如果用户不知道什么照片能拍什么不能拍、不知道置信度低的含义、不知道该什么时候找医生这套系统就只是一堆浮在云端的数字。把交互策略设计好把数据反馈闭环跑起来比多调0.5个百分点的准确率有用得多。希望这篇笔记帮你在做皮肤病识别系统时少走几步弯路。本文还有配套的精品资源点击获取