
简介面向牙齿图像分割与病变检测方向的这份数据集说明资料围绕牙科影像中牙齿及多种病变的标注组织内容适合从事医学图像语义分割、实例分割研究及课程实践的学习者参考。交付包共1个docx文档约2.22MB用于汇总数据集的核心信息与使用说明。数据主体包含2616张jpg图像及对应labelme格式json标注覆盖Tooth、Caries、Cavity、Crack、calculus、inflamation共6类各类别标注数量明确采用labelme 5.5.0多边形标注便于读者据此了解标注规模与类别分布。需要说明的是该数据集不含mask文件若用于语义或实例分割需自行将json转换为mask、YOLO或COCO格式部分牙齿未标注主要服务于病变检测目的。已有122人学习关注可为牙科病变识别实验的数据准备、标注核查与格式转换提供参考。1. 牙齿分割与病变分割数据集2616 张 labelme 标注能撑起什么活牙科影像的分割项目卡脖子的往往不是网络结构而是能用的像素级标注。手上这份牙齿分割、牙齿病变分割数据集给的是 2616 张 jpg 配 2616 个 labelme 格式 json6 个类别全部按 polygon 描边Tooth、Caries、Cavity、Crack、calculus、inflamation。它不附带 mask也不对训练精度打包票拿到手是一堆原始标注文件转掩码、转 COCO、切分训练验证集全得自己动手。适合两类人一是想快速跑通牙齿/龋齿分割基线、手里又没有口腔标注的算法同学二是做口腔影像筛查 Demo需要真实病灶轮廓而不是合成数据的工程团队。不适合指望开箱即用 tfrecord 的人——这份数据集的价值在于标注本身而不是封装。2. 解压后的第一件事把 2616 张图和 6 类标注清点成表labelme 导出的目录往往长得很随意图片和 json 混在一个文件夹、文件名带空格或中文括号、有人用 .JPG 有人用 .jpg。直接开训之前必须先做一次清点否则后面 mask 转换会静默丢样本训练集莫名其妙少几百张你还以为是 DataLoader 的问题。2.1 先读懂一个 json 里到底存了什么labelme 5.5.0 的单个 json 结构基本固定关键字段就那么几个version、imagePath、imageHeight、imageWidth、imageData、shapes。其中shapes是列表每一项包含label类别名、points多边形顶点图像坐标系原点在左上角单位像素、shape_type这份数据统一是 polygon、group_id、flags。有两个字段值得单独说。imageData在保存时如果勾选了内嵌图像会是 base64 编码的原图单个 json 能膨胀到几 MB批量解析时非常拖速度我一般先用labelme_JSON之类的方式把它剔掉再读imagePath只记录相对路径跟当前目录不一定对得上所以配对时不能信它要用文件名主干stem去对。配对检查用十几行就够from pathlib import Path img_dir Path(images) # 建议先把 jpg 收进 images/ lbl_dir Path(labels) # json 收进 labels/ # 用文件名主干配对忽略扩展名大小写差异 imgs {p.stem for p in img_dir.iterdir() if p.suffix.lower() in (.jpg, .jpeg)} lbls {p.stem for p in lbl_dir.iterdir() if p.suffix.lower() .json} print(f图片 {len(imgs)} 张标注 {len(lbls)} 个) print(有图无标注:, sorted(imgs - lbls)[:10]) print(有标注无图:, sorted(lbls - imgs)[:10])这段脚本的产出应该是 2616 对 2616、两个差集都为空。如果差集非空先解决配对问题再往下走——单边文件留着只会污染统计。参数上没什么可调的唯一要注意的是.stem对a.b.jpg会得到a.b如果文件名里本身带点建议改用p.name.rsplit(., 1)[0]。2.2 用一段脚本把 6 类标注和图数对平配对之后要做的是分布统计。这份数据公布的类别计数是 Tooth 1709、Caries 2913、Cavity 1099、Crack 139、calculus 1207、inflamation 620合计 7687 个多边形。这里有个很容易被忽略的推论Tooth 只有 1709 个而图片有 2616 张说明至少有一部分图里牙齿本体没有被描边——这正好对应数据集说明里的「部分牙齿没标注分割目的主要是查病变」。理解这一点后面设计损失函数时才不会把未标注区域当成负样本硬压。统计脚本长这样import json from collections import Counter, defaultdict from pathlib import Path CLASSES [Tooth, Caries, Cavity, Crack, calculus, inflamation] cnt Counter() # 各类多边形总数 per_img defaultdict(set) # 每张图出现了哪些类 pts_stat defaultdict(list) # 每类多边形顶点数用于判断描边精细度 empty, bad_label [], Counter() for jp in Path(labels).glob(*.json): d json.loads(jp.read_text(encodingutf-8)) shapes d.get(shapes, []) if not shapes: empty.append(jp.stem) # 空标注文件单独记录 continue for s in shapes: lb s[label] if lb not in CLASSES: bad_label[lb] 1 # 拼写/大小写不符的类别名绝不能静默放过 continue cnt[lb] 1 per_img[jp.stem].add(lb) pts_stat[lb].append(len(s[points])) print(多边形总数:, sum(cnt.values())) for c in CLASSES: print(f{c:12s} {cnt[c]:5d} 出现图数 {sum(c in v for v in per_img.values()):5d}) print(空标注文件数:, len(empty)) print(未登记类别名:, dict(bad_label)) for c in CLASSES: v pts_stat[c] if v: print(f{c:12s} 顶点数 min/median/max {min(v)}/{sorted(v)[len(v)//2]}/{max(v)})这段的核心不是打印而是三个输出。第一bad_label必须为空——只要出现一个拼错的名字就说明你的类别映射表和数据不是一套后面转 mask 会整类丢失。第二empty列表要单独保存空 json 在语义分割里可以当纯背景图用但在实例分割里必须剔除或者归到负样本集别混进正样本统计。第三「出现图数」比「多边形数」更能反映长尾Caries 有 2913 个多边形如果集中在少数图上实际覆盖的图数会远小于想象。顶点数的分布也有用。多边形顶点太少比如 5 个点以下通常意味着标注员在偷懒一个龋洞被画成粗糙的五边形边界误差可能有十几个像素顶点数爆炸几百个则可能是沿着牙齿边缘一路点下来的整颗牙这类样本在转 mask 时要注意内存。2.3 抽样可视化把 polygon 画回原图看一遍数字对得再齐也替代不了肉眼。抽 20 到 30 张把 json 里的多边形按类别上色画回原图看完再决定要不要清洗。import json, random import cv2 import numpy as np from pathlib import Path COLORS { # BGR Tooth: (200, 200, 200), Caries: (0, 0, 255), Cavity: (0, 128, 255), Crack: (255, 0, 0), calculus: (0, 255, 0), inflamation: (255, 0, 255), } samples random.sample(sorted(Path(labels).glob(*.json)), 24) for jp in samples: d json.loads(jp.read_text(encodingutf-8)) img cv2.imread(str(Path(images) / (jp.stem .jpg))) if img is None: print(读图失败:, jp.stem); continue overlay img.copy() for s in d[shapes]: pts np.asarray(s[points], dtypenp.int32) cv2.fillPoly(overlay, [pts], COLORS.get(s[label], (0, 255, 255))) # alpha 控制透明度0.4 左右既能看清病灶又能看清底图纹理 vis cv2.addWeighted(overlay, 0.4, img, 0.6, 0) cv2.imwrite(fvis/{jp.stem}.jpg, vis)addWeighted的两个系数加起来最好等于 1否则整体亮度会漂。COLORS里没登记的类别走默认黄色正好当告警色用。看的时候重点盯三件事病变多边形是不是落在牙齿轮廓内、有没有大块区域被漏标、边缘有没有明显的手抖锯齿。这一步花二十分钟能省掉后面两天调 loss 的功夫。3. labelme json 转语义分割 mask六类别索引表与批处理脚本语义分割要的是一张单通道 label map每个像素存类别索引。这份数据给的是多边形转换本身不复杂难的是「一个像素同时属于 Tooth 和 Caries」这种重叠情况怎么定优先级以及未标注区域该给什么值。定错了模型学到的就是一团浆糊。3.1 六类别到索引和调色板的映射设计先定死映射表写进配置文件所有脚本共用一份不要在每个脚本里各写一遍。类别名索引建议 RGB多边形数在任务里的角色背景0(0,0,0)-未标注区域也落在这里需单独处理Tooth1(128,128,128)1709解剖结构覆盖面最大Caries2(255,0,0)2913主病灶数量最多Cavity3(255,128,0)1099龋洞常嵌在 Caries 内Crack4(0,0,255)139细长条最稀有calculus5(0,255,0)1207牙结石多沿牙颈部inflamation6(255,0,255)620炎症区边界模糊注意Tooth首字母大写、calculus全小写、inflamation少一个 m这三个不统一是原始标注的历史遗留脚本里必须一字不差地照抄不要顺手修正否则整类对不上。重叠优先级我一般这么排病灶类 解剖类。也就是先画 Tooth 打底再让 Caries / Cavity / Crack / calculus / inflamation 覆盖上去。理由是下游如果是病灶检测模型更关心病灶边界而 Tooth 的面积大、边界清楚被覆盖掉一小块不影响它的整体形状学习。如果下游要做牙齿实例计数那就反过来或者干脆输出多通道——每个类别一张二值图让模型自己学关系。这两种输出方式没有优劣取决于任务是「查病」还是「数牙」这份数据的设计意图明显偏前者。3.2 批量转换脚本import json import cv2 import numpy as np from pathlib import Path CLASSES [Tooth, Caries, Cavity, Crack, calculus, inflamation] NAME2ID {n: i 1 for i, n in enumerate(CLASSES)} # 数值大的先画、数值小的后画会被覆盖所以先画 Tooth 再画病灶 DRAW_ORDER sorted(CLASSES, keylambda n: 0 if n Tooth else 1) def poly_area(pts): p np.asarray(pts, dtypenp.float64) x, y p[:, 0], p[:, 1] return 0.5 * abs(np.dot(x, np.roll(y, 1)) - np.dot(y, np.roll(x, 1))) def convert(json_path: Path, out_png: Path): d json.loads(json_path.read_text(encodingutf-8)) h, w int(d[imageHeight]), int(d[imageWidth]) mask np.zeros((h, w), dtypenp.uint8) # 0 背景/未标注 shapes [s for s in d[shapes] if s[label] in NAME2ID] # 同一优先级内面积大的先画避免大轮廓盖掉小病灶 shapes.sort(keylambda s: (DRAW_ORDER.index(s[label]), -poly_area(s[points]))) for s in shapes: pts np.asarray(s[points], dtypenp.float32) cid NAME2ID[s[label]] cv2.fillPoly(mask, [np.round(pts).astype(np.int32)], int(cid)) ok, buf cv2.imencode(.png, mask) # 绕开中文路径写入失败 if not ok: raise RuntimeError(f编码失败: {json_path.name}) buf.tofile(str(out_png)) return {file: json_path.stem, h: h, w: w, n_shape: len(shapes), n_ignored: len(d[shapes]) - len(shapes)} if __name__ __main__: Path(masks).mkdir(exist_okTrue) for jp in sorted(Path(labels).glob(*.json)): info convert(jp, Path(masks) / (jp.stem .png)) if info[n_ignored]: print(有未匹配类别被跳过:, info)几个参数决定了输出质量。dtypenp.uint8是因为类别只有 7 个值用 8 位足够存 PNG 体积小、读取快将来如果类别扩到 256 以上再换 uint16。np.round而不是直接astype(np.int32)是因为 labelme 的坐标是浮点直接截断会让所有多边形整体左上半像素多类边界处容易出现一像素缝隙。cv2.fillPoly的第三个参数是颜色值这里直接填索引不需要真的填 RGB。cv2.imencode tofile这一手是绕坑用的在部分系统上cv2.imwrite遇到中文或空格路径会静默失败返回 False你只会发现 masks 目录空空如也却没有任何报错。tofile走的是 numpy 的字节写入没有编码问题。如果要输出未标注的忽略区域ignored region思路是额外维护一张已标注牙齿的二值图凡是被任何 Tooth 多边形覆盖的像素算已标注其余算未标注未标注像素在 mask 里写成 255。这一步不是必须的但对这份部分牙齿没标注的数据非常有用下一章会讲它在损失里怎么用。3.3 转换后的三项校验转完不要直接拿去训三项对账。第一尺寸一致。mask 的(h, w)必须等于 json 里的imageHeight/imageWidth也必须等于原图尺寸。第二类别存在性。统计每类像素占比Caries、Tooth 这类大面积类别不可能为 0如果某类占比恰好是 0说明映射表写错了。第三空掩码清单。全 0 的 mask 只有两种合理来源图里确实没有标注对应空 json或者你漏了转换。把全 0 文件名拉出来和 2.2 里的empty列表比对数量对得上才能过。import numpy as np from pathlib import Path stat np.zeros(7, dtypenp.int64) all_zero [] for mp in Path(masks).glob(*.png): m np.fromfile(str(mp), dtypenp.uint8) # 与 tofile 对应的读法 if m.max() 0: all_zero.append(mp.stem) stat np.bincount(m, minlength7) total stat.sum() for i, name in enumerate([bg] CLASSES): print(f{name:12s} {stat[i]:12d} {stat[i]/total:7.4%}) print(全零 mask 数量:, len(all_zero))np.fromfile读回来是一维的做像素统计够用要还原成二维图做可视化时用cv2.imdecode(np.fromfile(path, np.uint8), cv2.IMREAD_UNCHANGED)。这里的百分比数字后面有大用它就是类别不均衡的量化依据Crack 的占比大概率在万分之几直接决定你的损失函数怎么选。4. 转 COCO / YOLO 做实例分割polygon 到 annotation 的字段落地语义分割给的是这块像素是什么实例分割要的是这是第几颗、什么类型。这份数据的多边形天生适合实例分割——每个 shape 天然就是一个实例不需要额外做连通域拆分。麻烦在于 COCO 的字段约定很死填错一个area或少一个iscrowdpycocotools评估出来的 mAP 就是错的。4.1 labelme 字段到 COCO 字段的对应关系labelme 字段COCO 位置处理要点文件名主干images[].file_name补上 .jpg 后缀和磁盘一致imageHeight/imageWidthimages[].height/width必须和真实图片一致否则评估全错shapes[].labelannotations[].category_id走自定义映射从 1 开始shapes[].pointsannotations[].segmentation展平成一维[x1,y1,x2,y2,...]再套一层列表由 points 计算annotations[].area鞋带公式算多边形面积不是 bbox 面积由 points 计算annotations[].bbox[xmin, ymin, w, h]左上角加宽高固定值annotations[].iscrowd一律 0多边形标注不参与 crowd 评估shapes[].group_id无这份数据基本为空忽略即可segmentation的嵌套层级是最容易搞错的地方单个多边形是[[x1, y1, x2, y2, ...]]外面这层列表代表这个实例由几个多边形组成。如果你写成[x1, y1, ...]平铺pycocotools 在loadRes时会直接抛异常或者更糟——不抛异常但算出垃圾结果。4.2 polygon 转 COCO 的核心代码import json from pathlib import Path CLASSES [Tooth, Caries, Cavity, Crack, calculus, inflamation] CAT_ID {n: i 1 for i, n in enumerate(CLASSES)} def poly_area(pts): x [p[0] for p in pts]; y [p[1] for p in pts] s 0.0 for i in range(len(pts)): j (i 1) % len(pts) s x[i] * y[j] - x[j] * y[i] return abs(s) / 2.0 def build_coco(label_dir, img_dir, out_json, min_area30.0, min_pts6): coco {images: [], annotations: [], categories: []} for i, n in enumerate(CLASSES, start1): coco[categories].append({id: i, name: n, supercategory: tooth}) ann_id 1 for img_id, jp in enumerate(sorted(Path(label_dir).glob(*.json)), start1): d json.loads(jp.read_text(encodingutf-8)) coco[images].append({id: img_id, file_name: jp.stem .jpg, height: int(d[imageHeight]), width: int(d[imageWidth])}) for s in d[shapes]: name s[label] if name not in CAT_ID: continue pts s[points] if len(pts) min_pts: continue # 顶点太少的粗标丢弃 area poly_area(pts) if area min_area: continue # 极小碎片多半是误点 xs [p[0] for p in pts]; ys [p[1] for p in pts] flat [round(float(v), 2) for p in pts for v in p] coco[annotations].append({ id: ann_id, image_id: img_id, category_id: CAT_ID[name], segmentation: [flat], area: round(area, 2), iscrowd: 0, bbox: [round(min(xs), 2), round(min(ys), 2), round(max(xs) - min(xs), 2), round(max(ys) - min(ys), 2)], }) ann_id 1 Path(out_json).write_text(json.dumps(coco, ensure_asciiFalse), encodingutf-8) return coco if __name__ __main__: c build_coco(labels, images, annotations/train.json) print(images, len(c[images]), annotations, len(c[annotations]))min_area30和min_pts6是两个必须自己调的门槛。30 平方像素大约相当于 5×6 的一块比这还小的多边形基本是人手误点6 个点是能构成有意义轮廓的下限低于它描述不了龋洞形状。这两个阈值不要设得太狠Crack 总共才 139 个标注再筛掉一批这类就彻底没样本了。我的习惯是先跑一遍看被筛掉多少如果某一类被筛掉超过 20%就把阈值降到刚好保住它。坐标保留两位小数是为了减小 json 体积2616 张图、近 7700 个多边形保留六位小数文件能到几十 MB读取解析明显变慢而两位小数的量化误差远小于标注本身的抖动。4.3 用 pycocotools 回读验证生成完一定要用官方库读一遍能加载、能算 mAP 才算合格from pycocotools.coco import COCO from collections import Counter c COCO(annotations/train.json) print(图片数:, len(c.imgs), 标注数:, len(c.anns)) print(类别分布:, Counter(a[category_id] for a in c.anns.values())) c.loadAnns(c.getAnnIds(imgIds1)) # 随便取一张能返回不报错即通过如果COCO()构造时不报错、loadAnns返回正常基本说明结构没问题。更保险的做法是再调一次c.computeImgs之类的内部方法或者干脆拿pycocotools.cocoeval.COCOeval塞个空结果跑一遍——空结果会打印一堆 0但流程能走完就证明标注结构合法。想把 COCO 再转成 YOLO 的 txt用 ultralytics 的convert_coco一行命令但注意它默认只处理 bbox做实例分割要加use_segmentsTrue并且转换后类别索引会被重排成 0 起始训练时记得改nc和names这个错位是新手最常见的翻车点之一。5. 常见问题排查转换和训练里反复踩的五个坑以下五条都是在这类半标注 长尾数据上实际遇到的按现象、原因、解决三段写遇到对得上的直接查。5.1 坑一转出来的 mask 全是 0训练 loss 一动不动现象脚本跑完没报错mask 目录里文件数量也对但打开看全是黑的或者统计出来只有背景类。训练时 loss 卡在一个值不下降预测全黑。原因类别名匹配失败而且失败得很安静。数据里的类别是inflamation少一个 m和calculus全小写如果映射表写成Inflammation、CalculusNAME2ID.get(label)返回 None脚本里一句if...continue就把整类跳过了不抛异常。同理如果用了严格的大小写比较Tooth写成tooth也会整类丢。解决转换脚本里凡是continue的地方都必须计数并打日志。跑完把被跳过的类别名打印出来非空就停下来修映射表不要往下走。另外在 3.3 的校验里加一条硬断言Tooth和Caries的像素占比必须大于 0否则直接 raise别让它静默产出垃圾数据。5.2 坑二整张图只剩一个类别牙和病灶互相覆盖现象可视化 mask 时发现某些图里明明有龋齿但牙齿轮廓整个不见了或者反过来龋洞被牙齿盖住。原因fillPoly是后画覆盖先画shapes列表的顺序完全取决于标注员的点击顺序没有语义。同一张图上 Tooth 和 Caries 高度重叠谁在后面谁留下。解决显式定义绘制顺序按先解剖后病灶的优先级排序同一优先级内按面积从大到小画。这一步不能省也不能依赖数据原始顺序。如果下游任务需要两类同时可见就改输出多通道二值 mask每个类别一张让模型自己去学层间关系——代价是显存和输入通道数翻几倍。5.3 坑三Crack 类的 mAP 接近 0验证集永远没有正样本现象训练跑完其他类 IoU 尚可Crack 的指标是 0 或者干脆没算出来个别 fold 里验证集完全没有 Crack 样本。原因139 个标注分布在 2616 张图里平均 19 张图才有 1 个实例随机切分时验证集抽不到甚至抽不到 5 个指标自然没有统计意义。另外裂纹是细长条面积占比极小按像素平均的损失会被牙齿背景彻底淹没。解决切分必须分层采样按每张图包含哪些类别分桶后再切保证验证集里有足够 Crack 和 inflammation 样本。损失上改用 Dice 或 Tversky 一类对前景敏感的目标或者对稀有类做像素级加权。评估时把 Crack 单列出来看召回率别只盯总体 mIoU 那个平均数它会把长尾类的糟糕表现抹平。5.4 坑四未标注的牙齿被模型学成了背景现象推理时发现标注完整的牙齿能被分出来但那些没标注的牙齿被预测成背景甚至侵蚀了相邻的病灶区域。模型在病变检测上表现不错牙齿整体轮廓却一塌糊涂。原因数据集明确说明部分牙齿没有标注。如果直接把 mask 里的 0 当作背景参与损失计算模型学到的是长得像牙但不是标注牙的区域 背景这在逻辑上是错的——它们是未知而不是负样本。解决生成一个标注有效区掩码。凡是被任意 Tooth 或病灶多边形覆盖的像素为有效其余像素在损失里当 ignore 处理CrossEntropy 的ignore_index255或者手动把对应位置的 loss 权重置 0。这样做之后牙齿整体分割的稳定性会明显提升病灶的召回也不会掉。代价是有效监督像素变少batch 要相应调大一点否则梯度噪声偏大。5.5 坑五路径带中文或空格转换脚本静默失败现象换一台机器跑同样的脚本masks 目录是空的控制台没有任何报错退出码还是 0。原因cv2.imread/cv2.imwrite在部分环境下对非 ASCII 路径支持不好读图直接返回 None写图返回 False。脚本里如果没检查返回值就会一路空跑到结束。Windows 上尤其常见。解决把所有读写都换成显式检查版本。读图用cv2.imdecode(np.fromfile(p, dtypenp.uint8), cv2.IMREAD_COLOR)写图用cv2.imencode(...)[1].tofile(p)每一步都判空并 raise。别用文件名统一改成 md5这种回避方案样本对不上号之后排查成本更高。6. 进阶验证把 mask 和 json 的面积对一遍再谈训练转换脚本跑通不代表转换正确。最省事的量化校验是面积对账json 里第 i 类的所有多边形面积之和应该和 mask 里第 i 类像素数在同一量级。差值的主要来源是多边形重叠——重叠区域的像素只能存一个索引所以 mask 面积通常会略小于 json 的加和偏差为负且幅度和重叠程度相关属于正常反过来mask 面积大于 json 加和几乎一定是索引写串了。import json, cv2, numpy as np from pathlib import Path CLASSES [Tooth, Caries, Cavity, Crack, calculus, inflamation] def poly_area(pts): p np.asarray(pts, dtypenp.float64) x, y p[:, 0], p[:, 1] return 0.5 * abs(np.dot(x, np.roll(y, 1)) - np.dot(y, np.roll(x, 1))) for jp in sorted(Path(labels).glob(*.json))[:200]: # 先抽 200 张 d json.loads(jp.read_text(encodingutf-8)) mask cv2.imdecode(np.fromfile(fmasks/{jp.stem}.png, dtypenp.uint8), cv2.IMREAD_UNCHANGED) if mask is None or mask.shape ! (int(d[imageHeight]), int(d[imageWidth])): print(尺寸异常:, jp.stem); continue for cid, name in enumerate(CLASSES, start1): a_json sum(poly_area(s[points]) for s in d[shapes] if s[label] name) a_mask int((mask cid).sum()) if a_json 0: if a_mask: print(f{jp.stem} {name} json 无标注但 mask 有像素) continue dev (a_mask - a_json) / a_json if dev 0.02 or dev -0.35: # 正值异常负值超过 35% 也要看 print(f{jp.stem} {name} json{a_json:.0f} mask{a_mask} 偏差{dev:.1%})抽 200 张足够发现问题分布不用全量跑。阈值 -0.35 是我在重度重叠样本上试出来的经验值Tooth 被病灶大面积覆盖时负偏差能到 30% 上下如果某张图偏差超过这个数多半是绘制顺序反了或者某类多边形丢了。对账通过之后才轮到训练参数。这份数据我的默认起手式是输入短边缩放到 768 再随机裁 512牙齿和病灶的边界细节在这个尺度下基本保得住优化器 AdamW学习率 1e-4 配 cosine损失用 CrossEntropyignore_index255 Dice 各半前者稳住背景后者拉稀有类稀有类按出现频率倒数加权但权重上限压到 10 倍超过容易过拟合到那几个样本上。有个习惯从那以后我一直保持每接入一份新标注数据先跑配对检查、类别计数、面积对账这三步任何一步有异常就停下来查绝不带着未知的脏数据开训。早期吃过一次亏——训练两天、调了三版结构最后发现是类别名拼写对不上导致整整一类没进模型那次之后我就把这三步写成了固定脚本。希望帮到你。本文还有配套的精品资源点击获取