卷积三钱、全联二钱,盗梦半钱,极池一分。四味和匀,先大火煮沸,再以文火慢炖七日七夜。择良辰停火取药,滤去西梯西、弃参等杂质,以叉八六泰格拉送服。可以开天窍、晓阴阳、发混蒙、妙不可言。——知乎
Yas-train的作用是为yas能识别圣遗物的名称和属性训练一个基于CRNN的文字识别模型。众所周知,深度学习如同炼丹,玄之又玄。本文总结了笔者在炼丹过程中的一些经验以供参考。
yas采用的是CRNN模型。该模型识别长重复序列比较难,容易出现如下错误(1. 把“115”识别成“1115”; 2. 把“115”识别成“15”)。这也是不定长文本OCR识别的难点。本文不会讨论如何克服模型的弱点,只会讨论如何最大程度的规避这种弱点。Yas需要处理的汉字比较少有叠词(例子: “瑶瑶”),数字往往也不太长,出现长重复数字的位置一般只有两种(圣遗物总数识别以及圣遗物生命值副词条)。故这个缺点影响不是特别大,因为重要的词条都是带百分号的。
有一个简单粗暴的方法可以提升这些情况的识别准确率,就是提升datagen生成这些情况的概率,比如“圣遗物 X/1800”的权重已经从原版的0.02提升到了0.1。
数据对于是炼丹最重要的。获得数据的最理想方法是采集真实数据并人工标注,其次是仿照真实场景合成数据。在yas-train中我们可以通过datagen来大量获得合成数据,也可以基于yas的dump模式导出的图片来进行人工标注获得真实数据。
注意无论是人工标注的真实数据,还是生成的数据,其参数(从词典库大小到二值化阈值)都需要和模型对应起来使用。
如何加快人工标注的效率?我们可以把数据先通过yas进行预标注(假设yas的正确率还挺高),然后把结果导入excel,在excel中可以根据预标注的结果进行过滤和排序来加速人工标注的过程。具体流程请参考其他地方的README (在experimental/datacleaner)。
合成数据的挑战在于如何接近真实游戏数据。具体来说这里有几个挑战:
- 首先Pillow库的文字次像素渲染方法跟原神的Unity有点不同
- 其次原神圣遗物的背景色不好模仿,而且有时是渐变色
经过对不同字号像素长宽的测量,900p分辨率下最小的字大约是17.5号,4k分辨率下最大的字大约是88号。并且由于Unity和Pillow库渲染方法不同,同样的预处理手法得到的结果会不同(见下文)。在datagen中,我们采用放宽范围的办法来保证生成的数据可以覆盖更多的情形(可以理解为生成更多样的数据来让模型更泛化)。
具体而言,datagen现在会随机改变以下参数:
- 前景色
- 背景色
- 字号 (15-90)
- 二值化阈值(0.5-0.6)
参数动态改变的范围越大,生成的数据越杂,模型越通用,但也越难训练。
另外datagen的性能也堪忧,尤其是随着最大字体上限的增加(原版是最大40号字),渲染的计算量变大了很多。datagen生成随机数据的速度成为了大规模在线训练的瓶颈(见下文)。
(这个部分本来想贴些图片,太麻烦放弃了)
图像都要进行预处理。二值化是一种很适合文字识别的预处理方法。yas-train和yas的预处理主要是把图片转换成灰度图,并归一化,并自适应取反色(因为游戏有时是白底黑字有时黑底白字),最后二值化。
因为游戏背景有渐变色,以及Unity和Pillow的次像素渲染方法的不同,在同样的二值化阈值下,两边同样文字最终得到的结果会有区别。最初的yas-train和yas的二值化阈值不同,也许可以起到一点配平的作用。(版本号)的yas-train采用了中等范围的随机二值化阈值,意图通过泛化来弥补两者的差距。
具体二值化阈值的选择也十分重要,实际测试发现,游戏中(Unity渲染下)采用0.6阈值的话,900p下“雷”字会中间的点在二值化后会消失。这是因为偏高的阈值导致文字边缘被吃掉。而如果采用0.5阈值,“暴”字则会完全糊成一团。经测试,最终选择了0.53作为二值化的阈值。
动态阈值二值化可以更好的适应不同的背景颜色以及背景渐变,未来可以探索。
训练的过程很枯燥,重点是就是设置合适的超参数例如batch_size,并且根据自己内存的大小选择合适的训练集和验证集的大小。数据集当然是越大越好,经测试8GB显存可以勉强容纳200k数据。
yas-train的训练目标是达到尽可能高的准确度,在训练集上达到100%准确度后(此时并不意味着模型有100%的准确度)yas-train会将模型保存,但不会停止运行。这样一次训练会得到多个验证准确率为100%的模型,然后我们会在下文讲如何筛选这个模型。
因为验证集的大小有限,在10k个数据点的验证集上达到100%准确率,可以认为错误率大约小于万分之一。而随着验证集的大小变大,对于模型精度的判断也更准确,但一次验证所需要的时间也越久。
为了节省内存和显存,并使得可以使用更大的验证集验证,yas-train引入了在线学习。只要每次访问数据集的时候动态生成数据,就可以近似于在无限大的数据集上进行训练和验证(且不需要无限大的内存和显存)。这里要注意这是近似于。完全采用在线学习的话,每个epoch模型见到的数据都是完全不同的(只是服从同一分布),对于收敛可能会有影响。
最近的几次模型是非在线的训练集(1M的训练集)和在线的验证集上得到的。笔者尝试了完全在线训练,但效果最好的模型是来自于预先生成好的非在线数据集。
yas-train采用了加入高斯噪声和随机剪切等常用的图片数据增强方法。
一次成功的训练会产生很多在训练集上准确率达到100%的模型。接下来我们需要在其中选出最好的那一个,也就是在一个验证集上验证并对比错误率。因为模型训练的很好,所以经常是数百万个数据点都难找出一个错误。因此这一步需要的(等效)数据集大小会很大。
这时可以采用分阶段筛选的办法。先用1M数据点来对比8个备选模型,挑出其中最好的几个模型。可能在1M数据点下他们的错误数都是1-2个,以至于无法真正分辨好坏。然后再加大数据量对于进入决赛的模型进行筛选。
以某个commit版本训练的模型为例,笔者最终采用了40M个数据点来对比,筛选出了错误率小于10/40M的模型。
在验证多个模型的时候,如果顺序验证他们,则意味着每次验证都要重新生成数据,有点浪费。比如我用1M数据验证10个模型,我实际生成了10M数据。一个更高效的办法是一边生成1M数据,一边同时把数据喂给10个模型进行验证。笔者有一个可以跑但不完美的代码,参考experimental/par_online/eval.py。
可以采用GAN来调整训练中生成的数据的特征分布,使得更容易出错的那一类(也就是模型学习效果不好的那一类)数据生成的更多。一个例子:这个模型比较难学习长数字比如“1115”,笔者只得手动提高datagen生成数字的概率。这个工作交给GAN去做更好。