简介:本资源是一个基于YOLO模型的发票识别系统完整实现,面向人工智能方向的本科生毕业设计、课程设计及深度学习初学者,解决财务场景中发票关键字段(如金额、日期、商户名)的自动化定位与识别问题。压缩包共100个文件,含46个Python源码(含核心app.py与训练/推理脚本)、35个编译后pyc文件、4张示例图像(含demo3.png)、3份Dockerfile(含PaddlePaddle专用版本)及配置类文件(yaml/yml/ini),整体仅3.89MB,轻量易部署。已有134人下载学习,资源结构清晰:Docker环境封装保障可复现性,test.http提供接口测试样例,README.md详述使用流程与依赖说明,requirements.txt明确PyTorch/YOLO生态依赖。读者可直接运行容器化服务、复现实时检测+OCR文本提取全流程,并基于现有代码快速适配其他票据类型。 做发票识别项目时,很多人第一反应是直接上OCR,把整张图丢给PaddleOCR或者Tesseract去识别。但真正做过落地项目的人都知道,发票这东西版面复杂、字段密集,直接OCR出来的结果根本没法用。我这次做“基于YOLO的发票识别”这个项目,核心思路就是先用YOLO把发票上的关键区域检测出来,再做字段级识别。这篇博文就把整个项目从数据准备、模型训练到部署落地的完整链路拆开讲,包括踩过的坑和调参经验,希望能给正在做类似项目的朋友一些参考。
这个项目适合谁看?如果你已经在用YOLO做过简单的目标检测,但想把它应用到文档类、票据类的细粒度识别场景;或者你手头有一堆发票图片,想提取结构化字段入库;又或者你只是好奇YOLO除了做行人、车辆检测之外,还能在OCR领域怎么玩——那这篇文章都值得你花十分钟看完。
1. 为什么选YOLO来做发票识别
1.1 发票识别的本质是目标检测而不是OCR
很多人搞混一个概念:发票识别不等于OCR。OCR解决的是“这一片区域里的字符是什么”,而发票识别首先要解决的是“哪些区域有我们需要的内容”。这两个问题是前后关系,不是并列关系。
你想想发票长什么样:发票代码在左上角、发票号码在右上角、开票日期、购买方信息、销售方信息、货物或应税劳务名称、金额、税额、价税合计……每个字段的位置虽然大体固定,但不同省份、不同版式的发票,位置会有偏移,而且拍照角度、折叠痕迹都会让位置发生形变。直接用OCR对整图识别,会出现两种情况:一是OCR把无关的装饰性文字、防伪码、花纹也识别出来,产生大量噪音;二是你拿到一堆识别结果之后,根本不知道哪个字符串对应发票号码、哪个对应金额。
所以发票识别的正确打开方式是:先用目标检测模型把每个关键字段的位置框出来,再对每个框里的内容做OCR识别。这就是我坚持用YOLO的原因——它在本项目里不是OCR的替代品,而是OCR的前置定位器。检测模型负责告诉你“发票号码在图片的哪个位置”,OCR模型负责告诉你“这个位置的数字是什么”。两个模型各干各的活,问题就被拆解得很干净。
1.2 YOLO系列选型:v8还是v11
做项目之前先选版本。项目启动的时候YOLOv8还是主流,v11刚发布不久,网上关于两者的对比讨论也很多。我当时的心态是:不追新,只选合适的。
从实际测试结果看,YOLOv8和YOLOv11在发票检测这个任务上,精度差距其实没有想象中那么大。v11的优势在于C3k2模块和C2PSA注意力机制,在COCO这类自然图像数据集上确实有所提升,但对于发票这种背景相对干净、目标结构相对规则的图像,提升幅度有限。反而是v8的生态更成熟,文档齐全,网上踩坑案例多,出了问题好查。所以我最终选用了YOLOv8n作为基线模型,后面根据效果决定是否升级到s或m版本。
另外还有一个考虑因素:模型大小。发票识别最终是要部署到业务系统里的,可能是服务端,也可能要跑到边缘设备上。YOLOv8n的参数量只有约320万,模型权重文件大约6MB,在CPU上也能跑到可接受的帧率。这对于一个需要高频调用的识别服务来说很关键。如果你的场景对精度要求更高,可以换YOLOv8s,但我觉得发票检测没那么复杂,nano版本完全够用。
1.3 项目整体架构拆解
整个项目的技术链路是这样的:
输入发票图片 → 预处理(缩放、角度矫正、去噪) → YOLO字段检测 → 按检测框裁剪 → 每个框单独做OCR识别 → 按字段名组装结构化结果 → 输出JSON
这个架构里,YOLO部分承担的是“字段定位”职责,定义了以下检测类别:
- invoice_code:发票代码
- invoice_number:发票号码
- invoice_date:开票日期
- buyer_name:购买方名称
- buyer_tax_id:购买方纳税人识别号
- seller_name:销售方名称
- seller_tax_id:销售方纳税人识别号
- amount:金额(小写)
- tax_amount:税额
- total_amount:价税合计
10个类别,覆盖了发票上最常用、业务最关心的字段。每个类别的定义要清晰,比如amount是“小写金额”,total_amount是“价税合计的小写数字”,如果一份发票里大小写金额都有,不要把它混成同一个类别,否则后面提取数值时还得二次判断,平白增加工作量。
数据标注的时候,每个类别还要规定“框住什么范围”。比如seller_name只框“销售方名称”这个字段对应的值区域,不含标签文字“销售方名称:”这五个字。这个规范必须在标注前定好,否则标注员理解不一致,模型学出来的特征就是乱的。
2. 数据集构建:发票标注的核心难点与实操
2.1 发票图像样本收集与预处理
做发票识别项目,数据往往是最难的一关。说实话,网上没有一个现成的、公开的、标注好的发票字段检测数据集,这个领域的优质数据基本都在各个做财税系统、报销系统的公司手里。所以只能自己收集。
我当时的数据来源主要有三个渠道:一是找财务同事要了一批真实报销的发票扫描件,这部分质量最高,是扫描仪扫描出来的,清晰、无遮挡、无畸变;二是自己用手机在不同光线、不同角度下拍摄打印出来的发票,这部分模拟了用户实际拍照上传的场景;三是从开源的票据数据集里筛选了一部分。
三种来源的比例我控制在5:3:2左右。为什么要掺入拍照数据?因为只训练扫描件的话,模型一遇到手机拍摄的高斯噪点、透视畸变就抓瞎。但真实业务里,用户报销时上传的绝大多数是手机照片,必须让模型见过这种数据。
收集完之后,第一件事不是标注,而是做一次质量过滤和预处理。分辨率太低看不清文字的图直接删掉,模糊的图可以留着作为困难样本但不要太多,倾斜角度超过45度的图先做一次旋转矫正。我用OpenCV做了一次直方图均衡化来增强对比度,然后统一缩放到合适尺寸。这里建议不要缩得太小,因为发票上的字本来就小,如果你把一张2000px的图缩到640px再去标注,小字段的框会非常难标,模型也学不好。
2.2 标注规范:用检测框框住什么
发票字段检测的标注,坑比想象中多。我总结了一个原则:框必须贴着文字的外边界,但不要试图去框单个字符。目标检测框的目标是“定位字段区域”,OCR会去处理框里的内容。所以框的边界应该刚好包裹住整个字段值,不要留白太多,也不要裁掉文字的边角。
特别要注意两类字段的标注差异:
- 多行文本:比如“货物或应税劳务名称”常常是一长串物品名,自动换行成两行甚至三行。这种字段我建议用一个大框把多行内容整体框住,而不是每行一个框。因为最终做OCR时,整块裁剪下来交给OCR让它自己处理换行,比拆成多个小框再拼接要稳定得多。
- 数值型字段:发票号码、金额这类字段,字符间距比较均匀,但框的时候要保证首尾字符都被完整包含进去。有时候发票号码后面有个“NO.”前缀或者校验符号,这种要明确标出来“只框数字部分,不框前缀”,规范统一。
标注工具我用的LabelImg,虽然界面比较老,但胜在稳定,而且直接支持YOLO格式导出。如果你有团队需要协作标注,可以用Label Studio,Web端的,支持多人任务分配,导出格式也能转成YOLO。总之一句话:标注工具不重要,标注规范才重要。开标注会的时候把每个类别的正例、反例、边界案例都截图列出来,让标注员照着标,能省很多返工时间。
2.3 数据增强与格式转换(xml转YOLO)
标注完成后,我统一导出了VOC格式的XML文件,因为这些标注工具默认支持VOC格式导出。但YOLO训练需要的是txt格式,每行一个目标的class_id、x_center、y_center、width、height,且都是归一化到0-1之间的浮点数。
写了一个Python脚本做格式转换,核心逻辑很简单:
import os import xml.etree.ElementTree as ET def convert_voc_to_yolo(xml_file, class_names, output_dir): tree = ET.parse(xml_file) root = tree.getroot() img_w = int(root.find('size/width').text) img_h = int(root.find('size/height').text) txt_name = os.path.splitext(os.path.basename(xml_file))[0] + '.txt' with open(os.path.join(output_dir, txt_name), 'w', encoding='utf-8') as f: for obj in root.iter('object'): cls_name = obj.find('name').text if cls_name not in class_names: continue cls_id = class_names.index(cls_name) bbox = obj.find('bndbox') x_min = float(bbox.find('xmin').text) y_min = float(bbox.find('ymin').text) x_max = float(bbox.find('xmax').text) y_max = float(bbox.find('ymax').text) # 转换为YOLO格式的归一化坐标 x_center = (x_min + x_max) / 2 / img_w y_center = (y_min + y_max) / 2 / img_h width = (x_max - x_min) / img_w height = (y_max - y_min) / img_h f.write(f"{cls_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}\n")这里有个容易踩的坑:XML里的坐标是整数,但转换时要除以原图的宽高做归一化。如果你的数据集里图片已经被预处理脚本改过尺寸了,那XML里的坐标其实已经对不上新图了。我的建议是格式转换之前,确认一下XML里size/width、height的值和实际图片尺寸一致,否则转换出来的归一化坐标虽然不会报错,但框的位置全部偏移,训练出来的模型就是废的。
数据增强方面,我用的都是YOLO官方训练流程里内置的增强参数,没有额外写增强脚本。关键参数是:
- hsv_h、hsv_s、hsv_v:小幅调整色彩,因为发票有红、蓝等底色,让模型对颜色变化不那么敏感
- degrees:旋转,但没给太大,5度以内就行。发票文字旋转太厉害会导致检测框和文字不对齐
- translate:平移,可以给到0.1,模拟发票在画面里位置偏移的情况
- scale:缩放,0.5左右。上采样放大能让模型看到更多文字细节
- mosaic:马赛克增强,建议关闭或者调低概率。因为发票的版面结构太特殊,四张图拼在一起会让模型产生严重的上下文混淆
这个mosaic的取舍很多人不知道。在自然图像检测里mosaic很有效,但在文档类图像里它反而有害——模型会学到“一张图里同时出现多个发票区域”的错误模式,推理时遇到单张发票反而会漏检。我后来把mosaic设成0,map反而涨了两个点。
3. 模型训练:关键参数与调优记录
3.1 环境准备与超参数选择
训练环境用的是一台单卡RTX 3090的Linux服务器,PyTorch版本2.0以上,CUDA 11.8,直接用ultralytics官方库训练。代码层面其实没什么可说的,ultralytics把训练流程封装得太好了,两三行代码就能起训练。
pip install ultralyticsfrom ultralytics import YOLO model = YOLO('yolov8n.pt') results = model.train( data='invoice.yaml', epochs=100, imgsz=640, batch=16, device=0, workers=8, optimizer='AdamW', lr0=0.001, lrf=0.01, patience=20, seed=42 )数据配置文件invoice.yaml里,最关键的是nc字段和names字段:
path: /data/invoice_dataset train: images/train val: images/val nc: 10 names: ['invoice_code', 'invoice_number', 'invoice_date', 'buyer_name', 'buyer_tax_id', 'seller_name', 'seller_tax_id', 'amount', 'tax_amount', 'total_amount']关于imgsz的选择,我多说一句。很多教程默认用640,但对于发票这种中小目标密集的图,640分辨率下很多字段只有十几个像素宽,检测头基本学不到有效特征。我实测过在同等条件下,imgsz从640提到1280,mAP50大约涨了5-8个点。代价是训练速度几乎减半、显存占用翻倍。如果你的显卡只有8GB显存,建议用640起步训练,等模型收敛后再用1280做精调,也就是两阶段训练法。
优化器我选的AdamW而不是默认的SGD。对于这种中小规模数据的检测任务,AdamW收敛更快、对初始学习率不那么敏感,发票检测的数据量一般就几千张,SGD那种需要精细调度学习率的优势发挥不出来。学习率用0.001,配合余弦退火,100个epoch足够。
3.2 训练指标阅读与异常排查
训练过程中,我发现自己经常被问到一个问题:YOLO训练指标全是0怎么办。这个现象其实很常见,尤其是第一次用YOLO训练自定义数据集的时候。指标全是0,一般是下面几个原因。
第一个原因最简单也最蠢:标注文件或配置文件里class数量对不上。你标注了10个类别,训练配置里的nc写成了80(COCO预训练模型的默认值),那模型只会预测前80类里的东西,你的类别一个都识别不了。这个排查方法很简单,训练日志启动的那几行会打印模型结构,看一眼就能发现。
第二个原因是数据路径配置错误,训练集和验证集都指向了空文件夹。检测任务里面map的计算是需要标签和预测框做匹配的,如果验证集里没有对应的标签文件,模型预测得再好也是0。我建议训练前写个Python脚本统计一下每个txt文件里的标注数量,确认图片名和txt文件名一一对应。
第三个原因是归一化坐标写错了。很多人在做xml转yolo的时候,把宽高信息直接写成了像素值而不是归一化值,YOLO训练时不报错,但模型看到框的宽高都是几百分之一甚至几千分之一,小到忽略不计,loss根本下不去,指标自然全是0。
如果以上都没问题但还是0,那就要看训练日志里的box_loss了。正常情况下,第一个epoch的box_loss应该在1.5-2.5之间,然后逐步下降。如果一开始就是0.8以下甚至接近0,大概率是标注文件里的框全都被过滤掉了,比如坐标越界、宽高为0。一个很隐蔽的场景是,某些标注工具在导出时有偏移,x_max大于了图片宽度,ultralytics会把越界的框过滤掉,但不给你任何警告,这种情况需要写脚本专门校验标注坐标的范围。
3.3 小样本与难例优化
财务场景的数据往往不多,尤其是某些特定版式的发票,可能只有几十张样本。小样本训练YOLO,有几个实用技巧可以分享。
第一个技巧是使用预训练权重做迁移学习。用COCO预训练的yolov8n.pt做初始化,比从头训练yolov8n.yaml效果要好很多,哪怕你的目标类别和COCO完全不相关,底层特征提取器学到的边缘、纹理、颜色特征都是通用的。这就是为什么上面的训练代码里model = YOLO(‘yolov8n.pt')而不是从零开始。
第二个技巧是类别权重平衡。如果10个类别里,invoice_code的样本数量是500张,而amount只有80张,模型天然会偏向学样本多的类别。你可以在数据层面做重采样:数量少的类别在训练时多复制几份增强样本加入数据池。或者简单粗暴一点,在推理阶段对小样本类别降低置信度阈值,比如默认conf=0.25,对小样本类别单独设成0.1。
第三个技巧是难例挖掘。第一轮模型训练完之后,跑一遍验证集,把预测错误的图片挑出来,看模型漏掉了哪些字段、误检了哪些区域。把错误case收集起来,人工复核、修正标注,再混入原始训练集重训。这个流程我跑了两轮,第一轮修掉了一批把“税率”误检成“金额”的样本,第二轮修掉了一批“价税合计”小写数字框不完整的问题。每轮大概能提升3-5个mAP,比盲目加数据管用得多。
这里也提一下“yolo 20x20小样本”这个搜索词。如果你只有20张样本,做什么模型都很难。至少要保证每个类别有30-50个正样本框,低于这个数就得靠预训练模型加上非常强的正则化,否则肯定过拟合。如果数据实在不够,另一个思路是把类别合并,比如把invoice_code和invoice_number合并成一个“invoice_identifier”类别,先做粗粒度检测,再用OCR区分具体是代码还是号码,这种降级方案在真实项目里非常常见。
4. 部署落地:从训练到生产环境的完整链路
4.1 模型导出与推理引擎选型
训练完成后,模型导出这一步很关键。ultralytics支持直接导出多种格式:
yolo export model=best.pt format=onnx opset=12 simplify=True导出ONNX之后,推理可以直接用onnxruntime,避免在服务器上装PyTorch全家桶。PyTorch的推理占用内存高、启动慢,在容器环境里镜像体积也大;而onnxruntime的CPU推理速度非常快,安装包小,部署起来省心不少。
如果你追求更高的推理性能,可以考虑TensorRT。但TensorRT有几个问题:一是只支持NVIDIA GPU,部署环境受限;二是模型转换过程中可能遇到算子不支持的问题,排查起来麻烦;三是TensorRT版本和CUDA、cuDNN版本强绑定,版本一换就得重新优化。我的建议是:初期先用onnxruntime把业务跑通,性能不够再考虑TensorRT。发票识别服务对延迟的要求通常是几百毫秒级别,onnxruntime的CPU推理完全能满足。
实际测试中,一张1920x1280的发票图片在CPU上做推理,YOLOv8n大约需要80-120ms,这在绝大多数业务场景里都是可以接受的。如果你有GPU资源,用GPU推理可以压到10ms以内。
4.2 后处理:从检测框到结构化字段
YOLO模型输出的是所有检测框的坐标、类别和置信度,这对业务来说还远远不够。后处理阶段要做的事情有两件:一是NMS去重,二是把检测结果按字段组装成结构化数据。
NMS不用自己写,ultralytics的推理流程里已经内置了。但如果用onnxruntime做推理,导出的ONNX模型输出的是原始预测结果,需要自己在代码里处理NMS。ONNX模型默认输出的张量形状是(1, 84, 8400),其中84=4个框坐标+80个COCO类别概率。但如果你导出时指定了nc=10,输出的形状就是(1, 14, 8400)。8400是YOLOv8三个尺度特征图的anchor总数,这个数字不受类别影响。
如果你用ultralytics的Python API做推理后处理,代码会简单很多:
from ultralytics import YOLO model = YOLO('best.pt') results = model.predict('invoice.jpg', conf=0.25, imgsz=1280) for r in results: boxes = r.boxes.xyxy.cpu().numpy() class_ids = r.boxes.cls.cpu().numpy().astype(int) confs = r.boxes.conf.cpu().numpy()拿到每个字段的框和类别之后,把对应区域裁剪出来:
import cv2 img = cv2.imread('invoice.jpg') for box, cls_id in zip(boxes, class_ids): x1, y1, x2, y2 = [int(v) for v in box] crop = img[y1:y2, x1:x2] # cv2.imwrite(f'crops/{class_names[cls_id]}_{i}.jpg', crop)然后把这个crop送到OCR引擎里。我们用的是PaddleOCR,它对中文、数字的识别效果很好,而且支持竖排文字。OCR识别出来的文本,直接作为该字段的值写入结果JSON。
这一步有个细节:检测框的置信度阈值不能设太高。发票上有些字段文字颜色浅、背景干扰多,YOLO给出0.3左右的置信度是正常现象。我们生产环境里conf设0.15作为“粗筛阈值”,取检测框后交给OCR,OCR识别的结果置信度再作为第二道关卡。两阶段各自把关,比单靠检测模型硬扛要稳妥得多。
4.3 一键部署脚本的设计思路
服务化部署的时候,我写了一个一键部署脚本,把整个流程串起来:环境准备、模型文件下载、服务启动、健康检查。
部署脚本的核心逻辑并不复杂,就是保证在目标机器上能一键跑通全部流程。一个典型流程是:
# 1. 创建虚拟环境 python -m venv venv && source venv/bin/activate # 2. 安装依赖 pip install -r requirements.txt # 3. 下载模型文件 wget https://your-storage-url/models/invoice_det_best.onnx wget https://your-storage-url/models/ocr_model.zip unzip ocr_model.zip -d ocr_models/ # 4. 启动服务 uvicorn app:app --host 0.0.0.0 --port 8080 & # 5. 健康检查 sleep 5 curl -X POST -F "image=@test_invoice.jpg" http://localhost:8080/predict服务本身用FastAPI封装,接口设计成一个POST接口:接收图片文件,返回JSON结构化的发票字段。核心代码结构大致是:
from fastapi import FastAPI, UploadFile import cv2 import numpy as np from detector import InvoiceDetector app = FastAPI() detector = InvoiceDetector() @app.post("/predict") async def predict(file: UploadFile): img_bytes = await file.read() img = cv2.imdecode(np.frombuffer(img_bytes, np.uint8), cv2.IMREAD_COLOR) result = detector.recognize(img) return result这里强烈建议在服务内部对输入图片做一次尺寸上限控制。如果用户上传的图片超过2000px,先等比缩小再送检测,这能显著降低推理时间和内存占用,且对最终效果几乎没有影响。反过来说,如果图片特别小、字段文字看不清,也要有个最低尺寸限制,比如不得低于480px,否则应该提示用户重新上传高清图。
5. 常见问题排查与避坑指南
5.1 训练阶段典型问题速查表
我把自动化测试和实际业务运行中遇到的高频问题整理成一张表,方便排查:
| 现象 | 可能原因 | 排查与解决方法 |
|---|---|---|
| mAP全程为0 | 数据路径或标签文件错误 | 检查image/txt文件是否一一对应,标签是否有空文件 |
| mAP全程为0 | nc类别数与标注类别不一致 | 查看训练日志模型输出头维度,确认nc是否正确 |
| box_loss不下降 | 输入分辨率过低,小字段特征丢失 | 调大imgsz到1280,或先640预训练再1280精调 |
| 训练loss很低但验证map低 | 过拟合,数据量太少 | 加数据增强、降低epoch、增加dropout、用预训练权重 |
| 验证时某类别完全没检出 | 该类别样本太少 | 重采样或合并类别,降低该类别推理置信度阈值 |
| 小字段(发票号码)漏检多 | 下采样倍数太大,小目标丢失 | 增大输入分辨率,或使用YOLO的P2检测层 |
| 检测框偏移、不贴文字 | 标注不规范,框范围不一致 | 重新审核标注,制定严格的框边界规范 |
每一条我都在项目里遇到过。“训练loss很低但验证map低”这个问题最有迷惑性,因为新手很容易被低loss骗到,以为模型很好,结果一预测全是错的。解决思路很简单:把训练集和验证集的loss做对比,如果训练loss持续下降而验证loss曲线反弹,就是典型的过拟合信号,这时候应该做的是减少训练轮数、增加数据增强强度,而不是继续跑下去。
5.2 推理阶段典型问题
推理阶段的问题往往和训练阶段不同,主要集中在以下几个方面。
第一个是速度问题。在纯CPU服务器上,如果每张发票的推理时间超过500ms,业务方就会不满意。可通过减小输入尺寸、使用INT8量化、换轻量级OCR模型等方式优化。我们实测下来,把YOLO输入从1280降到960,mAP只掉了约2个点,但推理速度快了40%左右,这个性价比很高。
第二个是旋转发票的检测问题。YOLO检测框是轴对齐的矩形,用户在手机上拍的发票往往有一定倾斜角度,检测框就会把背景文字也包进来,影响OCR效果。我的解决方案是在检测前先做一次霍夫变换找发票边缘,做透视矫正,把发票拉正再送入YOLO。也可以训练一个角度分类模型作为预处理,但这样做工程复杂度会高很多。
第三个是误检问题。当图片里同时出现其他票据、名片、宣传单时,模型可能会把上面的文字区域误检成发票字段。这类问题没有特别好的解决办法,只能靠增加负样本数据训练来解决。我在训练数据里专门加了500张“非发票但长得像发票”的票据图片,打上空标签,让模型学到“这些东西不是发票字段”。这个做法对降低误检率帮助非常明显。
第四个更隐蔽的问题是关于“yolo 20x20小样本”的一个衍生场景:当实际部署到生产环境后,你会收到大量用户上传的真实图片,这些图片会成为测试模型鲁棒性的天然数据集。强烈建议在正式上线前找100张真实用户图片作为黑盒测试集,评估模型的泛化能力,而不是只看验证集上的map指标。很多模型在整理过的验证集上表现优秀,一到脏乱差的真实数据上就原形毕露。
5.3 字段关联与后处理避坑
检测和OCR都完成后,最后一步是把OCR结果和字段名关联起来。这个步骤看似简单,实际有很多坑。
第一个坑是OCR识别出来的内容为空。可能是检测框裁得太小,也可能是这块区域本身没有文字。不要让OCR返回的错误结果覆盖掉空值,保持该字段缺失并返回一个显式标记,比如字段值设为“”,同时返回flag字段标识“该区域未识别到内容”,方便业务侧做兜底。
第二个坑是同一个字段被OCR识别出多行文本。比如“购买方名称”是一个很长的公司名,被OCR识别成了两行。这时候需要做一次简单的文本拼接,把多行结果用空格连接起来,但要小心不要误伤真正的多行字段。销售方地址这类字段本身应该保留多行结构,所以策略要分字段定义。
第三个坑是数值字段的清洗。金额、税额、价税合计这些字段,OCR识别出来的内容可能包含“¥”、“¥”、逗号、空格、全角字符等。需要在后处理里做一次清洗,只保留数字和小数点,然后转成float进行比较。如果识别结果是“1,200.50”,清洗后是1200.50;但如果清洗出了问题,把中文大写的“壹仟贰佰元整”也当金额解析,就会出错。所以建议对金额字段同时输出大小写两个版本的识别结果,让业务侧做交叉校验。
第四个坑是关于检测框重叠。发票上有些字段离得很近,比如“金额”和“税额”两个框有时会重叠。如果YOLO输出里两个框有重叠区域,可能导致OCR结果混乱。我在后处理代码里加了一个简单的iou判断,如果两个框的iou超过0.3,就认为可能是同一块区域被重复检测了,保留分数更高的那个框。
说到重复检测,这里还有一个经验:发票检测模型跑出来的结果,经常会有一个字段被检测两次,比如“发票号码”区域被一个大框套一个小框同时框住,两个框的iou很高。这种情况说明训练数据里出现了同类别目标两个框都贴住同一个字段的标注问题,最好回到数据里把这种重复标注修掉,而不是依赖后处理去过滤。
5.4 关于“xxx功能0.0.0更新”类部署脚本的提醒
现在网上有很多“一键部署脚本”的模板,比如“yolo最新版本更新内容”之类的文章,会给你一个看似全自动的部署脚本。我的经验是:拿来参考可以,直接在生产环境用,风险太大。
这些脚本的一个共性问题是对环境版本做了很强的假设。比如脚本里写死了pip install ultralytics==8.0.100,但你的机器Python版本可能不兼容;或者脚本里用wget去下载某个模型文件,但外网访问不稳定导致模型下载失败;还有的脚本会在系统目录装一堆依赖,把生产环境搅乱。
我自己的做法是:脚本只做三件事,一是创建虚拟环境,二是安装requirements.txt里的依赖(版本锁定,不带^号),三是拉起服务。剩下的环境检查、模型校验、数据备份,全部在脚本外手动确认。部署脚本越简单越不容易坏,这一点在踩过无数坑之后感触特别深。
如果你确实要用一键部署脚本,有一个保命技巧:在脚本开头加一段依赖版本自检:
import sys import torch assert sys.version_info >= (3, 9), "Python版本过低,请安装3.9及以上" assert torch.__version__ >= "2.0.0", "PyTorch版本过低,请安装2.0及以上" print("环境检查通过")这样即使脚本在新环境里跑挂了,也能在第一时间定位到问题原因,而不是面对一串不知所云的报错日志。
最后再分享一个我个人做这个项目时最实用的习惯:每跑一次实验,就把超参数、数据集规模、训练时间、最终指标这四个信息记录在一起。一开始我觉得这很麻烦,但当你调了两周参数之后,会发现没有记录,你根本记不清上一版模型是用了什么配置才涨了两个点的。这个记录习惯,对发票识别这种迭代频繁的项目来说,比任何模型技巧都重要。
本文还有配套的精品资源,点击获取