ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

合成数据驱动泰文OCR:数据生成、增广与训练实战

合成数据驱动泰文OCR:数据生成、增广与训练实战 做OCR这些年我有个越来越强烈的感受真正让识别系统变强的往往不是网络结构又改了多少层而是训练数据本身能覆盖多少真实世界的噪声。尤其是在小语种场景里这种感受会更明显。泰文是个典型例子它不像英文、中文那样有海量公开数据集想要规模化标注泰文图片成本高到离谱而且专业标注员本身就难找。所以很多团队会走一条更务实的路——合成数据。但如果只是想当然地拿字体渲染几张图片就去训练结果通常会非常难看。泰文的书写体系非常特殊元音、声调符号在字符的上、下、左、右都能出现组合起来之后还能发生变形和位移。这带来一个很本质的问题合成数据必须足够了解泰文的文字规则否则生成的数据再多也只是在固定的模板里打转模型根本学不到真实场景里的泛化能力。我一直觉得这类项目的核心命题不是“要不要用合成数据”而是“合成数据能做到什么程度、边界在哪里”。这篇文章就基于我做泰文OCR的一点实践经验聊聊合成数据在泰文识别里的价值、生成细节、训练技巧以及那些容易踩进去的坑。1. 泰文OCR的痛点为什么非走合成数据这条路1.1 泰文本身的语言学难点泰文和拉丁字母系语言完全不是一回事。泰文是一种元音附标文字基本辅音字母 上方元音 下方元音 声调符号可以叠成一个复杂的组合。比如“คำ”这个词看起来是三个字符但实际上由辅音“ค”、上方元音“ำ”和尾音符号组合而成字母形变和位置关系非常紧密。更麻烦的是泰文不像英文那样有明确的空格分词。英文单词之间有空格OCR模型很容易学到一个词从哪里开始、到哪里结束。泰文整一句话可以连在一起写中间偶尔出现的空格只是用来断句而不是分词。这意味着模型必须理解音节边界和上下文才能准确切分和识别。还有一个关键点是泰文的字形高度依赖位置。同一个元音符号“ิ”短音i放在不同辅音上方时它的位置会随着字母的高低、宽窄发生变化甚至会有连写、变形的情况。这种非线性的排版方式直接用标准OCR框架去搞很容易出现一个字切得七零八落、另一个字又粘连在一起。1.2 真实标注数据的成本困境想做泰文OCR最理想的情况当然是收集几百万张真实拍摄的街景招牌、文档扫描件、身份证复印件然后找母语者逐字标注。但实际做下来会发现这条路几乎走不通。泰文标注员的稀缺性不用多说而且泰文的标注难度本身就比拉丁语系高很多。标注员要懂得元音和声调符号的位置规则、要能区分同一字形在不同上下文里的变体、还要知道哪些字符组合属于同一个音节。一套流程下来单张图片的成本往往是英文场景的好几倍而且质量还很难保证。我的实测经验是标注质量高的泰文图一千张的采购和清洗成本足以让我用这个预算去渲染几百万张合成图。虽然合成图和真实图有差距但量变往往会带来质变尤其在模型参数量不大、任务相对聚焦的情况下合成数据完全可以作为主力训练集来用。1.3 合成数据的路径选择逻辑既然真实数据太贵合成数据自然就成了替代方案。但这里需要说明一点合成数据的目的不是“替代”真实数据而是把模型训练的前期阶段撑起来让模型先学会基本的字形、常见的排版规律、基础的噪声鲁棒性。后续再用少量真实数据做微调效果会好很多。我通常把合成数据的使用分成三个层级第一层用字体渲染生成最基本的字符和音节组合让模型建立对泰文字形的基础认知第二层叠加各种背景、模糊、透视、噪声让模型适应拍摄环境的多样性第三层针对性地合成现实中常见的困难样本比如模糊文字、弯曲表面、遮挡部分让模型在极端条件下也不至于彻底摆烂。三层递进下来合成数据的利用率会明显提升。2. 高质量泰文合成数据的制作细节2.1 字体资源与字符集选择很多人做合成数据的第一步就翻车了因为只找了一两款开源字体就开始渲染。泰文字符在不同字体下的差异比拉丁字母大得多。以“ก”这个字母为例在有的字体里底部是一条直线在另外的字体里会有明显的卷曲模型看到一个变体就会懵。我的建议是字体数量至少要覆盖20款以上尽量保持风格差异大。除了常见的系统字体还需要加入一些手写风格的字体、带衬线的字体、广告风格字体。如果项目里要识别身份证、发票这类正式文档还得专门找政府、银行常用的字体。这个信息通常可以从相关文档规范里查到或者通过收集样本自行比对字体。字符集上需要注意泰文不是只有泰文部分还有隐藏的梵文转写字符、古泰文符号以及常用的数字和标点。如果场景里有泰国地址、车牌等内容数字的识别准确率同样重要。我一般会把字符集做成可配置项按业务场景动态生成。2.2 泰文的组合规则不能随机拼字符这是整个合成流程中最关键的一步。泰文不是把字符符拉到一行上就完事了字符之间的上下叠放、左右拼接都必须遵循严格的规则。强行随机拼接会出现一些泰文里根本不会出现的组合模型学到这些错误规律后真实场景里会疯狂误报。泰文里一个完整的组合通常由基础辅音、元音、声调符号按固定顺序构成。书写时前方元音比如“เ”“แ”“โ”“ใ”“ไ”会出现在辅音左边但键盘输入顺序其实是辅音先输入、元音后输入。这种存储顺序和显示顺序的不一致对渲染引擎提出了要求如果直接把Unicode字符串画出来引擎会按显示规则自动排版这没问题但如果合成逻辑里手动移动字符位置就需要自己理清每个符号的相对坐标。我实际做的时候会根据Unicode的泰文组合规则建立一套合法组合表。比如“เ”只能和哪些辅音组合、“ิ”能放在哪些字母上方、声调符号跟哪些元音组合会出现位置冲突全部预处理成字典渲染时只在合法组合里抽。这样生成的样本模型看到的才是“地道”的泰文而不是一堆字符堆出来的四不像。2.3 从“干净样本”到“真实感样本”的增广策略字体渲染出来的图片是非常干净的如果直接用模型会严重过拟合于纯色背景、横平竖直的排列。要让合成数据起作用关键词是“增广”。我常用的增广策略分几类。几何增广包括透视变换、随机旋转、局部拉伸幅度不能太大否则文字本身都已经扭曲得不可读了。模拟拍摄环境的增广包括光照不均、边缘阴影、离焦模糊、运动模糊这类增广对模型泛化能力帮助很大。噪声和纹理增广包括高斯噪声、椒盐噪声、随机线条、背景纹理叠加可以模拟低分辨率摄像头和复杂背景的情况。需要特别提醒的是泰文的上下部符号对模糊非常敏感。“ิ”“ุ”这类小符号在模糊之后很容易被抹平模型可能直接忽略它们。所以模糊增广的强度要适度或者采用“局部模糊整体清晰”的策略保证模型既能学到模糊不变性又不会丢失对上下符号的敏感度。我一般用这样的思路做增广先合成干净图然后随机决定这一张要加哪些扰动最后以8:2的比例混合“较干净样本”和“严重退化样本”。这比所有样本都统一加中等强度的扰动效果更好因为模型会同时看到两个极端更容易学出鲁棒的特征。2.4 代码落地一个可用的合成管线参考这里给一个简化的合成管线思路方便你理解整个流程是怎么跑的。import random from PIL import Image, ImageDraw, ImageFont, ImageFilter import numpy as np # 合法组合表preceding_vowel consonant above_vowel below_vowel tone valid_combos load_thai_combo_table() def render_word(word, font_path, font_size, bg_color, text_color): font ImageFont.truetype(font_path, font_size) img Image.new(RGB, (512, 128), bg_color) draw ImageDraw.Draw(img) draw.text((30, 30), word, fontfont, filltext_color) return img.crop(img.getbbox()) def augment(img): if random.random() 0.7: img img.transform( (img.width, img.height), Image.QUAD, quad_coords_from_perspective(img.width, img.height), Image.BICUBIC ) if random.random() 0.5: img img.filter(ImageFilter.GaussianBlur(radiusrandom.uniform(0.2, 1.2))) if random.random() 0.3: bg Image.fromarray(np.random.randint(120, 200, (img.height, img.width), dtypenp.uint8)) img Image.blend(img.convert(L).convert(RGB), bg, alpha0.15) return img def generate_sample(): combo random.choice(valid_combos) word build_word_from_combo(combo) # 按显示顺序输出 font_path random.choice(font_list) font_size random.randint(28, 48) base_img render_word(word, font_path, font_size, bg_colorrandom_pastel(), text_color(30, 30, 30)) final_img augment(base_img) return final_img, combo这个流程可以跑通但真实工程会比这个复杂得多。比如背景颜色需要和字体颜色保持一定的对比度区间太相近了模型学不到边比如透视变换的坐标范围要控制好避免文字扭曲到语义不可辨比如生成出来的图片要做清洗过滤掉极端模糊或者文字被截断的样本。这些细节直接决定训练出来的模型质量。实际项目中管线还需要加入“真实感”的环节。一种简单有效的做法是收集一批无标注的真实背景图把渲染好的文字随机贴到背景上再进行颜色调整、光影融合。这样能模拟出很多真实场景的纹理让模型的泛化能力上一个台阶。3. 从合成数据到可用的识别系统3.1 模型的选型与输入处理合成数据确定之后模型的选型反而相对常规。泰文OCR最常用的方案是基于CNNLSTMCTC的序列识别结构也就是经典CRNN路线或者直接用基于Transformer的OCR模型。其实对大部分场景来说模型复杂度的优先级远没有数据质量的优先级高。我的经验是先在中等规模的模型上做基线验证比如ResNet18作为骨干的CRNN输入高度设为64像素宽度动态调整。泰文上下部符号太多输入高度太低会把元音和声调符号直接压没我试过32像素高度的效果识别准确率会比64像素低明显一截尤其对带上下标音符号的组合。所以泰文OCR的输入分辨率必须给足。另外泰文OCR的标签空间要额外注意。泰文Unicode的码位比较分散一些组合字符在存储时是多字节的训练前需要统一做标准化。如果不做标准化同一个词的不同编码形式会被当成两个不同的标签数据量直接打折扣。3.2 训练与评估合成数据能做多好我自己跑过一组对比实验。纯合成数据训练出的模型在合成测试集上的准确率可以达到95%以上但拿到真实场景里测一开始准确率只有60%-70%。这说明合成数据在“内部世界”里的表现容易产生幻觉必须引入真实数据来校准。引入大概几千张真实标注图做微调之后模型在真实测试集上的准确率可以迅速提升到85%-90%。如果你的真实数据来自目标场景比如专门收集街景门牌照片那准确率还能进一步上涨。这个结果其实印证了一个观点合成数据负责“广度”真实数据负责“精度”两者缺一不可。当然准确率不是唯一指标。泰文OCR还需要关注字符级别的错误率尤其要区分哪些错误是“可接受的”比如把某个字体变体识别成同一个字符、“不可接受的”比如漏掉声调符号导致语义完全改变。我习惯在测试阶段单独计算元音、声调符号的召回率因为这个指标直接关系到泰文单词是否能被正确“读”出来比单纯看整词准确率更敏感。3.3 合成数据的天花板哪些问题它解决不了合成数据确实强大但它有先天的局限。最典型的就是真实世界里的文字并不总是干净、平整、正对着镜头的。比如弯曲的饮料瓶身、带褶皱的塑料袋、反光的金属招牌这类纹理和光照干扰纯靠字体渲染几乎无法模拟。你可以在增广阶段加透视变换但瓶身圆柱曲面带来的非线性畸变不是简单透视能模拟出来的。另一个问题是字体之外的手写体。合成数据可以轻松生成印刷体但泰文手写体的变形非常自由每个人写出来的“บ”都可以不一样而且连笔现象严重。合成数据在这个领域只能做到“启蒙”真正要识别手写体最终还是要靠大规模的真人手写样本。这并不意味着合成数据没有意义。正常项目里合成数据可以覆盖80%的基础场景剩下20%的极端场景用真实数据慢慢补。知道天花板在哪里就能在水位以下把合成数据的价值榨到最干。4. 常见问题与排查技巧实录4.1 泰文OCR典型问题速查表这里整理一份我自己在项目里反复用到的排查表覆盖了从数据合成到模型训练的大部分问题。问题表现可能原因解决思路元音符号频繁漏识别“คำ”识别成“ค”输入分辨率太低上下部符号被压缩提高输入高度保持上下符号充足像素模型把“เ”误判成“แ”前方元音混淆合成数据里字体太少字形变化不够扩充字体库尤其是带衬线风格字体真实场景准确率低印刷体准、拍摄体差增广强度不够模型没见过复杂背景增加透视变换、光照不均、背景纹理模拟字符粘连一个词识别成一个乱码泰文组合显示顺序与存储顺序冲突预处理时根据Unicode规则重排合成顺序模型训练不收敛loss高居不下标签标准化没做同词不同编码统一用NFC/NFD规范做Unicode标准化模型频繁输出不存在字符标签空间出现非法组合合成时随机拼字符生成非法泰文组合建立合法组合字典只渲染合法组合这个表看起来简单但每一条背后都是我踩过的坑。尤其是第一条“输入分辨率太低”很多人会忽略因为英文OCR用32像素高度已经够了泰文直接套用就会出问题。4.2 合成阶段容易踩的坑合成数据最常见的坑之一是背景与字体的对比度失控。泰文字形相对复杂笔画密集、上下部符号多如果背景纹理太花、颜色与字体太接近模型学到的特征会非常杂。我的经验是先确保“清晰可读”是底线再叠加干扰项。不能本末倒置让模型一开始就在模糊不清的数据里学。另一个容易被忽视的问题是样本失衡。泰文中有些字符使用频率极高比如“ก”有些则非常罕见。如果合成数据完全随机生成高频字符会占掉大部分样本空间模型对低频字符的识别能力会很弱。我的做法是按字符的现实中真实分布来设定采样概率同时对低频字符做额外过采样。这样才能让每个字符都有足够多的曝光机会。还有一个细节是渲染时的字体大小范围。字体大小选择太集中模型对尺度的适应性就差真实图片里文字大小一变就识别不出来。我一般会让字体大小在28到48像素之间随机分布同时配合视图缩放让模型在不同尺度下都能工作。这里需要解释一下字体大小指的是渲染时文本的磅值视图缩放是最后整图缩放时产生的像素尺寸差异两者是不同层次的尺度变化结合起来能更好模拟真实拍摄距离远近的问题。4.3 数据质量验证的必要动作合成数据制作完之后不要直接拿去训练先做一轮“质量抽检”非常有必要。我的习惯是随机抽200张图人工快速扫一遍确认没有出现字体重叠、字符被裁切、背景和文字糊成一团等问题。虽然这个步骤看起来土但它的效率异常高能拦住90%的管线低级错误。除了人工抽检还可以做一个更系统的小实验用这些合成数据训练一个轻量模型然后在真实数据上测试看哪些类别是稳定崩溃的。如果模型反复漏掉某个元音符号那大概率是合成数据里这种符号出现的形态太少或者位置关系没处理好。通过这种方式反推合成策略比盲目加数据量有效率得多。我还会监控合成数据中每个字符的覆盖频率生成一份频率表检查低频字符是否过少。遇到低频字符直接手动大量补充。经过几轮这样的迭代之后合成数据的质量会趋于稳定训练出的模型在真实场景里才不会到处漏水。5. 一点个人经验和体会合成数据在泰文OCR里到底能走多远我的答案取决于愿不愿意在细节上花时间。只是用现成OCR工具渲染一批泰文字符那绝对走不远但如果你研究泰文组合规则精心设计增广策略再搭配少量真实数据做微调那合成数据完全可以撑起一套工程上够用的泰文识别系统。在我自己的项目里最后成型的那条数据管线合成数据和真实数据的比例大概在10:1。合成数据提供海量字形变化和背景多样性真实数据提供目标场景的“灵魂”。两者结合模型在真实测试集上的字符准确率从纯合成时的74%提升到了接近92%元音和声调符号的召回率也从62%涨到了88%。这个提升幅度说明问“合成数据能走多远”不如问“合成数据能带着真实数据走到多远”。先靠合成数据把底子打厚再让真实数据把精度拉满才是泰文OCR落地最现实的路径。最后再分享一个小技巧。如果你有足够多的无标注真实图片试着把它们混入合成管线的背景池让文字随机渲染在这些真实图片上不贴任何标签也不做任何后处理只当背景纹理来用。这一步处理过之后模型在真实场景里的误检率能下降不少而且几乎不增加标注成本。原理不复杂真实背景的纹理和光照分布是纯程序生成很难完全模拟的把真实背景当素材直接喂给合成管线相当于让模型提前见到更多目标场景的底层视觉特征。这个小改动效果立竿见影。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进