1. 项目缘起:当YOLOv5遇上三千年前的甲骨文
最近在整理一些历史文献资料时,我遇到了一个挺有意思的挑战:如何从一堆混杂着泥土痕迹、龟甲裂纹的考古拓片图像里,快速、准确地框出那些古老的甲骨文字符。这活儿要是纯靠人眼和尺子,效率低不说,还容易看花眼。作为一个常年混迹在计算机视觉一线的“码农”,我第一反应就是,能不能用目标检测模型来搞定它?毕竟,检测“字”和检测“人”、“车”在模型看来,本质都是找框。而YOLOv5,这个在工业界历经考验的“老兵”,以其出色的速度与精度平衡,以及极其友好的工程化生态,成了我的首选。
但问题没那么简单。甲骨文检测属于典型的密集小目标、低质量背景场景。字符本身尺寸小(很多只有几十个像素宽),笔画复杂(象形文字),背景干扰极强(龟甲兽骨的自然纹理、破损、污渍)。直接套用COCO数据集上训好的通用模型,效果肯定惨不忍睹。这就需要我们从头开始,构建一个专属的文本考古检测系统。这个系统的核心,就是基于YOLOv5全系列(n/s/m/l/x)参数模型,进行针对性的开发、训练与优化。今天,我就把自己从数据准备、模型选型、训练调优到最终部署的完整链路,以及其中踩过的坑和收获的经验,毫无保留地分享出来。无论你是对考古数字化感兴趣,还是正在处理类似的特殊场景小目标检测问题,相信这篇长文都能给你带来直接的参考。
2. 数据困境与巧解:构建甲骨文检测数据集
任何AI项目,数据都是地基。对于甲骨文这种专业领域,公开的、标注好的检测数据集几乎为零。我们的第一步,也是最耗时的一步,就是创造数据。
2.1 数据来源与预处理
我们的原始资料主要来自已公开的甲骨拓片高清扫描图、考古报告中的线描图以及部分博物馆的数字化藏品。这些图像质量参差不齐:
- 高清拓片:背景为黑色,字口为白色,对比度尚可,但存在墨色不均、洇染。
- 扫描图像:可能带有纸张纹理、折痕、光照阴影。
- 线描图:背景干净,但丢失了原始材质信息,且笔画是单线,与真实拓片差异大。
预处理的第一步是归一化。我们将所有图像统一resize到640x640的尺寸,这是YOLOv5默认的输入尺寸,也能在训练效率和模型性能间取得较好平衡。对于灰度图,我们统一转换为单通道(灰度)进行训练。这里有一个关键考量:甲骨文是刻痕,颜色信息(RGB)几乎无用,甚至可能引入噪声。使用单通道图像,不仅能减少模型计算量,还能迫使模型更关注纹理和形状特征,这与甲骨文的识别本质更契合。在data.yaml配置文件里,你需要将channels设置为1,并在模型配置文件里对应修改输入通道数。
注意:直接使用三通道模型加载单通道图片,通常是通过复制灰度通道到RGB三个通道来实现。但我们的做法是直接修改YOLOv5网络第一层卷积的输入通道数,从3改为1,这样能真正减少参数量,是更彻底和专业的做法。
2.2 数据标注的“考古学”
标注工具选用LabelImg或更高效的CVAT。标注过程本身就是一次“考古实践”。我们定义了以下标注原则:
- 框的紧密度: bounding box要紧贴字符的笔画外缘,尽可能减少背景纳入。对于笔画粘连的字符,按可分割的最小单元框选。
- 类别定义:初期,我们尝试为每个不同的甲骨文字符设立单独类别。但很快发现行不通:一是字符种类成千上万,许多字出现频率极低;二是很多字我们自己也无法准确释读。因此,我们退而求其次,采用**“字符”与“非字符”的二分类策略**。即,只要是人类可辨识的、独立的刻划符号,都标为“character”(类别0)。那些明显的龟甲裂纹、无关污渍、无法辨识的划痕,则不予标注或归为背景。这大大降低了标注难度和模型学习难度,首要目标是“找到字”,而不是“认出是哪个字”。
- 困难样本处理:对于部分残缺、模糊的字符,如果主体部分可辨,则正常标注。如果残缺超过一半,则舍弃。这个标准需要标注人员统一,最好由一位有一定甲骨学知识的人做最终审核。
2.3 数据增强:针对性的“造雨”策略
甲骨文图像的数据增强不能乱用。几何变换如随机旋转、透视扭曲要非常谨慎,因为真实的拓片视角是固定的。我们主要采用以下增强组合(使用YOLOv5内置的albumentations或torchvision):
- 色彩扰动:调整对比度、亮度、饱和度(对灰度图是模拟不同墨色和光照条件)。添加高斯噪声,模拟图像老化或扫描噪声。
- 模拟退化:这是关键。我们加入了模拟水渍、霉斑、局部遮挡的增强。通过随机添加一些深色或浅色的椭圆、不规则块,来模拟龟甲上的自然瑕疵和污损。
- Mosaic增强:YOLOv5自带的Mosaic增强对于小目标检测非常有效。它将四张图片拼成一张,极大地增加了小目标在训练图片中的出现频率和上下文多样性。对于甲骨文这种密集小目标场景,必须开启,并且可以适当提高其使用概率。
最终,我们构建了一个包含约8500张图像、12万个字符标注框的数据集。按照8:1:1的比例划分训练集、验证集和测试集。
3. 模型选型:YOLOv5全系列参数模型实战对比
YOLOv5提供了n/s/m/l/x五个预定义模型,区别主要在于网络的宽度(channel数)和深度(BottleneckCSP模块的层数)。选择哪个,不是拍脑袋,而是要在速度、精度和显存消耗之间做权衡。我们对五个模型进行了同条件下的对比训练。
3.1 实验设置与核心指标
- 硬件:单卡RTX 3090 (24GB)。
- 基础配置:输入640x640,单通道,batch_size根据模型调整(n/s可设32,l/x需降至16或8),训练300个epoch。
- 优化器:SGD with momentum=0.937, weight_decay=5e-4。
- 学习率:余弦退火,初始lr0=0.01,最终降至lr0的百分之一。
- 评估指标:
- mAP@0.5:交并比IoU阈值为0.5时的平均精度均值,是衡量检测精度的核心指标。
- mAP@0.5:0.95:IoU阈值从0.5到0.95,步长0.05的平均mAP,更严格。
- 推理速度 (FPS):在Tesla T4上测试的每秒帧数。
- 模型大小 (MB):.pt权重文件的大小。
3.2 五虎将横向测评结果
下表是我们训练完成后,在独立测试集上的表现:
| 模型 | 参数量 (M) | 模型大小 (MB) | mAP@0.5 | mAP@0.5:0.95 | 推理FPS (T4) | 训练显存占用 (GB) | 适合场景 |
|---|---|---|---|---|---|---|---|
| YOLOv5n | 1.9 | 3.9 | 0.723 | 0.412 | 156 | ~2.1 | 边缘设备,实时性要求极高,精度可妥协。 |
| YOLOv5s | 7.2 | 14.4 | 0.815 | 0.523 | 98 | ~3.8 | 性价比之王。精度与速度的黄金平衡点,大多数项目的首选起点。 |
| YOLOv5m | 21.2 | 42.4 | 0.856 | 0.587 | 62 | ~6.5 | 服务器端部署,追求更高精度,对速度有一定要求。 |
| YOLOv5l | 46.5 | 91.9 | 0.868 | 0.601 | 38 | ~10.2 | 精度优先,算力充足,用于关键任务的离线分析。 |
| YOLOv5x | 86.7 | 170.4 | 0.872 | 0.609 | 22 | ~16.8 | 学术研究或精度竞赛,刷榜用,实用部署成本高。 |
3.3 结果分析与选型建议
从数据中可以得出几个清晰结论:
- 收益递减定律:从n到s,mAP提升显著(+0.092);从s到m,提升尚可(+0.041);从m到l/x,提升非常有限(+0.012 / +0.016),但模型复杂度和耗时却成倍增长。YOLOv5s是一个明显的拐点,用较小的代价换来了可观的精度跃升。
- 小目标检测的挑战:所有模型的
mAP@0.5:0.95都远低于mAP@0.5,这说明模型对于“框得准”的要求(高IoU)还比较吃力,尤其是对小目标。这是后续优化的重点方向。 - 我们的选择:考虑到这是一个探索性项目,未来可能部署在博物馆的本地服务器或研究人员的PC上,我们选择了YOLOv5m作为主力模型。它在精度(0.856)和速度(62 FPS)之间取得了很好的平衡,比s模型精度有明显提升,又不像l/x模型那样笨重。对于想快速验证想法或资源受限的伙伴,强烈推荐从YOLOv5s开始。
4. 训练调优:针对甲骨文场景的“对症下药”
选定模型架构只是第一步,训练过程的调优才是决定最终性能的关键。针对甲骨文检测的难点,我们进行了以下几个关键调整。
4.1 锚框(Anchor)重聚类
YOLOv5默认的锚框是基于COCO数据集聚类得到的,其尺寸分布对于甲骨文小目标来说偏大。我们用自己的训练集所有标注框重新进行了K-means聚类(使用YOLOv5提供的utils/autoanchor.py脚本),得到了9组新的、更适应我们数据分布的锚框尺寸。更新后的锚框明显更小,例如其中最小的锚框尺寸从原来的(10,13)变成了(6,8)。这一步操作,让模型在训练初期就能有更好的初始定位能力,mAP提升了约2%。
4.2 损失函数权重调整
YOLOv5的损失由分类损失(cls_loss)、目标性损失(obj_loss)和边框回归损失(box_loss)组成。在甲骨文场景下:
- 边框回归精度至关重要:因为字符框小,几个像素的偏差就会导致IoU大幅下降。我们适当提高了box_loss的权重(在
hyp.scratch.yaml中调整box增益系数,从默认的0.05提高到0.06),让模型更专注于学习如何把框画准。 - 分类任务简单:我们只有“字符”一类(背景由obj_loss处理)。因此,可以略微降低cls_loss的权重,避免其干扰边框回归的学习。
4.3 针对小目标的特殊策略
- 多尺度训练:YOLOv5默认支持多尺度训练,每10个batch随机选择新的输入尺寸(范围通常是320到640)。这对于小目标检测非常有益,相当于让模型学习不同“放大镜”倍数下的特征。我们保持了这一默认设置。
- 聚焦小尺度的检测头:YOLOv5有三个检测头(P3, P4, P5),分别对应大、中、小目标。甲骨文字符主要对应P3(最小尺度)检测头。我们不是简单调参,而是深入分析了P3特征图上的激活情况。通过可视化发现,一些极小的字符在P3上的响应依然很弱。因此,我们尝试了一个进阶技巧:在FPN(特征金字塔)的基础上,为P3检测头添加一个轻量级的注意力模块(如SE Block)。这个模块让网络更关注那些包含高频细节(可能是笔画)的特征通道。具体实现是在
models/yolo.py中对应Detect层前的卷积层后插入SE模块。实测下来,对小目标(像素面积<32x32)的召回率提升了约3%。 - 正样本匹配策略:YOLOv5v6.0之后使用了TaskAlignedAssigner进行正样本分配。我们调整了其参数
topk(每个gt框匹配的锚框数量),从默认的13提高到15,让每个小目标字符能有更多的锚框负责预测,增加了训练样本的丰富性。
4.4 超参数搜索与“炼丹”心得
我们使用YOLOv5内置的hyp.scratch.yaml作为起点,并针对我们的场景进行了小幅网格搜索(使用train.py的--evolve参数,但规模较小,因为资源有限)。主要调整了:
- 学习率:发现初始学习率
lr0设为0.01稍高,容易震荡,降至0.008更稳定。 - 优化器动量:从0.937微调到0.92,结合较低的学习率,使训练曲线更平滑。
- 标签平滑:引入了轻微的标签平滑(
label_smoothing=0.1),防止模型对“字符”类别的预测过于自信,有轻微的正则化效果,提升了约0.5%的mAP。
踩坑记录:早期我们曾尝试使用非常大的
mosaic和mixup增强概率,结果导致模型完全学崩。原因是过强的增强破坏了甲骨文图像本身的结构化信息(如行款)。后来我们将mosaic概率固定在1.0(即始终使用),但将mixup概率从0.1降为0,并严格控制了色彩扰动的幅度。对于背景复杂、目标微小的数据,数据增强“少即是多”,增强的目标是模拟真实退化,而不是创造不存在的场景。
5. 评估、可视化与问题诊断
模型训完了,不能只看一个mAP数字就完事。必须深入分析它在哪里行,在哪里不行。
5.1 混淆矩阵与PR曲线分析
使用val.py生成的混淆矩阵,我们能看到模型几乎不会将背景误检为字符(特异性高),但存在少量的字符漏检(假阴性)。这是可以接受的,考古学上“宁缺毋滥”比“滥竽充数”更重要。 精确率-召回率(PR)曲线显示,在召回率达到0.9时,精确率仍能保持在0.85以上,说明模型整体性能可靠。但曲线在召回率0.95以后急剧下降,说明最后那5%的字符极难检测,它们通常是极度模糊、残缺或与背景纹理高度融合的样本。
5.2 错误案例可视化与归因
我们利用TensorBoard的预测可视化功能,以及自己写的脚本,将验证集中预测错误的案例(False Positive和False Negative)单独抽出来分析。
- 典型False Positive(误报):
- 龟甲上自然的、类似“十”字或“卜”字形的裂纹被误认为文字。
- 拓片边缘的装裱褶皱或印章痕迹。
- 笔画非常稀疏的字符,被拆分成多个不连续的、无意义的短划,并被分别检出。
- 典型False Negative(漏报):
- 与大块污渍或破损区域重叠的字符。
- 字符笔画极细,在resize到640x640后,在特征图上可能只剩下一两个像素的宽度,信息几乎丢失。
- 两个字符笔画粘连过于紧密,模型将其识别为一个大的、形状奇怪的目标,或者直接忽略。
5.3 针对性的后处理与优化思路
基于以上分析,我们可以在后处理阶段或下一轮模型迭代中做针对性改进:
- 形状过滤:对于FP中的裂纹误报,可以利用字符的宽高比和轮廓复杂度进行过滤。甲骨文字符虽然形态各异,但大多有一个相对紧凑的外接矩形,且轮廓不会像裂纹那样简单(多为直线)。可以计算检测框内二值化后区域的Hu矩等形状特征,设定阈值过滤。
- 上下文规则:甲骨文刻辞通常成行成列。我们可以开发一个简单的行文本检测后处理模块。对同一行内的检测框,根据其中心点坐标进行聚类和排序,对于那些孤立存在的、远离文本行的检测框,可以降低其置信度或直接剔除。
- 多尺度推理:对于漏报的极细笔画字符,可以采用测试时增强(TTA),即对同一张图像进行不同尺度的缩放(如0.8x, 1.0x, 1.2x)分别预测,然后合并结果。这能有效召回在不同尺度下特征更明显的目标。当然,这会显著增加推理时间。
- 难例挖掘:将所有的FP和FN样本加入训练集,进行第二轮针对性训练,这是提升模型在困难场景下表现的最有效方法之一。
6. 系统集成与简易部署
模型最终要能用起来。我们构建了一个简单的本地化检测识别系统原型。
6.1 系统架构与流程
系统采用经典的Pipeline:
输入图像 -> 预处理(灰度化/缩放/归一化) -> YOLOv5m模型推理 -> NMS后处理 -> 结果解析与输出我们使用Flask搭建了一个轻量级的Web API服务。用户可以通过网页上传拓片图像,服务器处理后,将标注了检测框的结果图(以及可选的JSON格式的坐标和置信度)返回给用户。
6.2 核心代码片段
推理脚本核心 (detect.py自定义版本):
import torch import cv2 import numpy as np from models.experimental import attempt_load from utils.general import non_max_suppression, scale_coords class OracleBoneDetector: def __init__(self, weights_path='best.pt', device='cuda:0'): self.device = torch.device(device) # 加载模型,并确保输入通道数匹配 self.model = attempt_load(weights_path, map_location=self.device) self.model.eval() self.stride = int(self.model.stride.max()) self.img_size = 640 self.names = self.model.module.names if hasattr(self.model, 'module') else self.model.names def preprocess(self, img_path): # 读取为单通道灰度图 img0 = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) if img0 is None: raise ValueError(f"Image not found at {img_path}") # 转换为3通道(YOLOv5训练时虽然是单通道,但输入格式仍是3通道,我们复制灰度通道) img0 = cv2.cvtColor(img0, cv2.COLOR_GRAY2RGB) # 保持长宽比的resize img = letterbox(img0, self.img_size, stride=self.stride)[0] # 转换通道并归一化 img = img.transpose((2, 0, 1))[::-1] # HWC to CHW, BGR to RGB img = np.ascontiguousarray(img) img = torch.from_numpy(img).to(self.device) img = img.float() / 255.0 if img.ndimension() == 3: img = img.unsqueeze(0) return img, img0 def detect(self, img_path, conf_thres=0.25, iou_thres=0.45): img, img0 = self.preprocess(img_path) with torch.no_grad(): pred = self.model(img)[0] # NMS pred = non_max_suppression(pred, conf_thres, iou_thres) results = [] for det in pred: if len(det): det[:, :4] = scale_coords(img.shape[2:], det[:, :4], img0.shape).round() for *xyxy, conf, cls in det: results.append({ 'bbox': [int(x) for x in xyxy], 'confidence': float(conf), 'class': self.names[int(cls)] }) return results, img0Flask API 核心 (app.py):
from flask import Flask, request, jsonify, send_file import os from detector import OracleBoneDetector import cv2 import json app = Flask(__name__) detector = OracleBoneDetector('weights/best.pt') @app.route('/detect', methods=['POST']) def detect(): if 'file' not in request.files: return jsonify({'error': 'No file uploaded'}), 400 file = request.files['file'] # 保存临时文件 temp_path = f'temp_{file.filename}' file.save(temp_path) try: # 执行检测 results, orig_img = detector.detect(temp_path) # 绘制检测框 (绿色框) for r in results: x1, y1, x2, y2 = r['bbox'] cv2.rectangle(orig_img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(orig_img, f"{r['class']} {r['confidence']:.2f}", (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0,255,0), 1) # 保存结果图 output_path = f'result_{file.filename}' cv2.imwrite(output_path, orig_img) # 返回结果 return jsonify({ 'detections': results, 'result_image_url': f'/download/{output_path}' }) except Exception as e: return jsonify({'error': str(e)}), 500 finally: if os.path.exists(temp_path): os.remove(temp_path) @app.route('/download/<filename>') def download(filename): return send_file(filename, as_attachment=True) if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=False)6.3 部署注意事项与性能优化
- 模型导出:使用YOLOv5的
export.py脚本将PyTorch模型导出为torchscript或ONNX格式,甚至进一步转为TensorRT,可以大幅提升推理速度。在我们的案例中,使用TensorRT在T4上推理,FPS从62提升到了120+。 - 批处理:如果系统需要处理大量图片,务必实现批处理推理,能充分利用GPU并行能力。
- 资源监控:在Web服务中添加简单的日志和资源监控,记录每张图片的处理时间和显存使用情况,便于排查性能瓶颈。
- 结果缓存:对于重复上传的相同图片(比如同一份拓片的不同处理版本),可以建立简单的哈希缓存机制,避免重复计算。
7. 总结反思与未来展望
构建这个甲骨文检测系统的过程,是一次将现代AI技术与古老文明连接的有趣尝试。回过头看,有几个体会特别深:
第一,数据质量决定天花板。无论模型多fancy,标注的准确性、一致性,以及数据增强是否贴合真实场景,都是最根本的。我们花了超过60%的时间在数据工程上,现在看来非常值得。对于专业领域,领域专家的介入(哪怕只是审核标注)能避免很多低级错误。
第二,模型选型要务实。不要盲目追求最大的模型。YOLOv5s/m在绝大多数实际场景中已经足够强大。我们的实验也证明了,从m到l/x的精度提升,在成本面前性价比很低。理解你手中数据的特性和你的硬件边界,选择合适的模型,把精力更多放在数据清洗、锚框调整和损失函数调优上,往往收获更大。
第三,小目标检测是持久战。即使用了这么多技巧,对于像素面积小于20x20的字符,以及和背景融为一体的字符,模型的检出能力依然有限。这可能是当前检测模型架构的固有局限。未来的优化方向,除了继续收集更多难例,或许可以尝试:
- 更高分辨率的输入:尝试以1024x1024甚至更大尺寸训练和推理,虽然会牺牲速度,但能为小目标保留更多像素信息。
- 特征融合的改进:探索更高效的FPN结构(如BiFPN)或引入Transformer来增强全局上下文感知,帮助模型从混乱背景中“揪出”字符。
- 结合分割思路:将检测问题转化为实例分割问题,让模型不仅预测边框,还预测每个字符的像素级掩码。这或许能更好地处理笔画粘连和字符残缺的情况。
这个项目目前还只是一个起点,离真正的“甲骨文识别”(而不仅仅是检测)还有很长的路。但至少,我们证明了用深度学习自动化辅助甲骨文整理是可行的,它为考古学家提供了一件新的“数字洛阳铲”。代码和模型权重我已经整理开源,希望能吸引更多对考古和AI交叉领域感兴趣的朋友一起探索,让技术为人文研究注入新的活力。