How the makefile in tesstrain-win work
Train Tesseract LSTM methods Comparison
Train Tesseract LSTM with tesstrain.sh on Windows
Train Tesseract LSTM with make on Windows
Win10 Tesseract4.1 LSTM training
tesstrain-win可以在Windows下,根据图像及其对应的文本进行Tesseract LSTM with make的训练。它来源于Tesseract-ocr/tesstrain,makefile以及文件结构有一些改动。本文以tesstrain-win中的makefile为例,记录Train Tesseract LSTM with make训练流程与工作原理。
注:最近为了在windows下进行Tesseract LSTM with make的训练,首次接触makefile,属于初学者,本文属于个人学习笔记与心得记录,请谨慎参考,若有大神愿意对文中的错误或误解之处指点一二,将不甚感激。
References
makefile基本原理
代码变成可执行文件,叫做编译(compile);先编译这个,还是先编译那个(即编译的安排),叫做构建(build)。Make是最常用的构建工具,诞生于1977年,主要用于C语言的项目。但是实际上 ,任何只要某个文件有变化,就要重新构建的项目,都可以用Make构建。
无规矩不成方圆,make的构建规则都写在Makefile文件里面。
Makefile文件由一系列规则(rules)构成。每条规则的形式如下。
1 2 3 |
<target> : <prerequisites> [tab] <commands> |
上面第一行冒号前面的部分,叫做”目标”(target),冒号后面的部分叫做”前置条件”(prerequisites);第二行必须由一个tab键起首,后面跟着”命令”(commands)。
“目标”是必需的,不可省略;”前置条件”和”命令”都是可选的,但是两者之中必须至少存在一个。
井号(#)在Makefile中表示注释。
每条规则就明确两件事:构建目标的前置条件是什么,以及如何构建。
前置条件通常是一组文件名,之间用空格分隔。它指定了”目标”是否重新构建的判断标准:只要有一个前置文件不存在,或者有过更新(前置文件的last-modification时间戳比目标的时间戳新),”目标”就需要重新构建。
target这一个或多个的目标文件依赖于prerequisites中的文件,其生成规则定义在command中。prerequisites中如果有一个以上的文件比target文件要新的话,command所定义的命令就会被执行。这就是Makefile的规则。也就是Makefile中最核心的内容。
How the makefile in tesstrain-win work
由GNU make中可知,“–trace”可以在执行过程中同时在命令行打印出正在执行的命令与结果。由此,我们可以大致了解一下Train Tesseract LSTM with make的命令执行顺序。
1. make training –trace
在Train Tesseract LSTM with make时,做好准备工作之后,我们会在命令行运行如下命令:
1 |
make training –trace |
根据makefile的基本规则,首先要构建的目标是”training”,
1 |
training: $(OUTPUT_DIR).traineddata |
它并没有命令行,其前置条件是data/foo.traineddata,该前置条件不存在。因此顺序往下执行。
2. $(ALL_GT): $(wildcard $(GROUND_TRUTH_DIR)/*.gt.txt)
1 2 3 4 5 6 7 |
$(ALL_GT): $(wildcard $(GROUND_TRUTH_DIR)/*.gt.txt) #若不存在data/foo目录,则创建该目录 @mkdir -p $(OUTPUT_DIR) #GROUND_TRUTH_DIR路径下找到所有的*.gt.txt文件,并取出相应文件的内容按顺序存储到data/foo/all-gt文件中,且去掉重复的内容。 find $(GROUND_TRUTH_DIR) -name '*.gt.txt' | xargs cat | sort | uniq > "$@" |
$(ALL_GT) = data/foo/all-gt 该文件当前不存在,但其前置条件已存在。在data/foo-ground-truth中有已准备好的训练数据集,其中包含.gt.txt文件。
按照makefile基本规则,前置条件的时间戳比目标时间戳新,因此其命令行会执行,重新构建目标。
命令行完成的任务:在GROUND_TRUTH_DIR路径下找到所有后缀名为*.gt.txt的文件,并取出相应文件的内容按顺序存储到data/foo/all-gt文件中,且去掉重复的内容。
3. ifdef START_MODEL
因$(ALL_GT) 作为此条件下两个目标的前置,因此接下来会执行到这里。
1 2 3 4 5 6 7 8 9 10 11 |
ifdef START_MODEL $(OUTPUT_DIR)/unicharset: $(ALL_GT) @mkdir -p data/$(START_MODEL) combine_tessdata -u $(TESSDATA)/$(START_MODEL).traineddata data/$(START_MODEL)/$(MODEL_NAME) unicharset_extractor --output_unicharset "$(OUTPUT_DIR)/my.unicharset" --norm_mode $(NORM_MODE) "$(ALL_GT)" merge_unicharsets data/$(START_MODEL)/$(MODEL_NAME).lstm-unicharset $(OUTPUT_DIR)/my.unicharset "$@" else $(OUTPUT_DIR)/unicharset: $(ALL_GT) @mkdir -p $(OUTPUT_DIR) unicharset_extractor --output_unicharset "$@" --norm_mode $(NORM_MODE) "$(ALL_GT)" endif |
若START_MODEL已赋值,则分别执行combine_tessdata/unicharset_extractor/merge_unicharsets命令;若START_MODEL未赋值,则仅执行unicharset_extractor。
combine_tessdata:用来合并/提取/覆盖/list/压缩 [lang].traineddata files 中的tessdata组件。通过 -u 指令,可以将所有组件解压到指定路径,这些组件包含.lstm/.lstm-number-dawg/.lstm-punc-dawg/.lstm-recorder/.lstm-unicharset/.lstm-word-dawg/.version。
unicharset_extractor:根据设定参数,从.box或纯文本文件以提取字符集unicharset
merge_unicharsets:将参数中设定的两个字符集合并为一个,并存储在data/foo/unicharset中。
4. .PRECIOUS: %.box
第3步执行完成后,无前置关系跳转,故从第2步开始跳转的代码处顺序执行此语句。
.PRECIOUS所依赖的目标具有以下特殊处理:如果make在执行其配方期间被杀死或中断,则不会删除目标。如果目标是中间文件,则不再需要它后,将不会删除它。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
.PRECIOUS: %.box %.box: %.png %.gt.txt PYTHONIOENCODING=utf-8 python generate_line_box.py -i "$*.png" -t "$*.gt.txt" > "$@" %.box: %.bin.png %.gt.txt PYTHONIOENCODING=utf-8 python generate_line_box.py -i "$*.bin.png" -t "$*.gt.txt" > "$@" %.box: %.nrm.png %.gt.txt PYTHONIOENCODING=utf-8 python generate_line_box.py -i "$*.nrm.png" -t "$*.gt.txt" > "$@" %.box: %.tif %.gt.txt PYTHONIOENCODING=utf-8 python $(GENERATE_BOX_SCRIPT) -i "$*.tif" -t "$*.gt.txt" > "$@" |
这一段代码针对不同的图像格式进行不同的处理,我们的测试数据是.tif和.gt.txt,因此最终执行的代码为:
1 |
PYTHONIOENCODING=utf-8 python generate_line_box.py -i "data/foo-ground-truth/FILENAME.tif" -t "data/foo-ground-truth/FILENAME.gt.txt" > "data/foo-ground-truth/FILENAME.box" |
其用途是调用generate_line_box.py,根据.tif和.gt.txt文件生成相应的.box文件,相对于 jTessBoxEditor + lstmtraining的方案,该代码替代了人工通过jTessBoxEditor来调整和生成box的步骤。但是这里生成的.box文件并不是单个字符所在的位置信息,而是单行文本整体的位置信息。
5. %.lstmf: %.box
%.box作为目标%.lstmf的前置条件已更新,因此接下来会该行代码开始执行。
1 2 3 4 5 6 7 8 9 10 11 12 |
@if test -f "$*.png"; then \ image="$*.png"; \ elif test -f "$*.bin.png"; then \ image="$*.bin.png"; \ elif test -f "$*.nrm.png"; then \ image="$*.nrm.png"; \ else \ image="$*.tif"; \ fi; \ set -x; \ tesseract "$${image}" $* --psm $(PSM) lstm.train |
此处先根据测试数据集中不同的图像格式为变量image赋值。
set -x; 从这里开始打印执行的命令及其参数。
根据设定参数执行tesseract-ocr的训练命令tesseract,利用.tif和.box文件生成.lstmf文件用于lstm训练。
此句执行完成后,重新回到第4步,处理下一张图片,以此类推,循环执行,直到data/foo-ground-truth中的所有图片均已生成相应的.box和lstm文件。
6.$(ALL_LSTMF)
1 2 3 4 5 |
$(ALL_LSTMF): $(patsubst %.gt.txt,%.lstmf,$(wildcard $(GROUND_TRUTH_DIR)/*.gt.txt)) @mkdir -p $(OUTPUT_DIR) find $(GROUND_TRUTH_DIR) -name '*.lstmf' -exec echo {} \; | sort -R -o "$@" |
%.lstmf作为该行命令的前置条件之一,第4,5循环执行完成之后,会执行到此处。
在解释该行代码之前,我们先学习一下:
1 |
$(patsubst pattern,replacement,text) |
patsubst 意义:查找text中的单词(单词以“空格”、“Tab”或“回车”“换行”分隔)是否符合模式pattern,如果匹配的话,则以replacement替换。这里,pattern可以包括通配符“%”,表示任意长度的字串。如果replacement中也包含“%”,那么,replacement中的这个“%”将是pattern中的那个“%”所代表的字串。
函数返回被替换过后的字符串。
示例:
1 |
$(patsubst %.c,%.o, a.c b.c) |
把字串“a.c b.c”符合模式[%.c]的单词替换成[%.o],返回结果是“a.o b.o”
1 |
$(patsubst %.gt.txt,%.lstmf,$(wildcard $(GROUND_TRUTH_DIR)/*.gt.txt)) |
根据以上解释,makefile中的patsubst所在代码的意义为:将符合模式【%.gt.txt】的文件替换为【%.lstmf】,text内容为相应.gt.txt的文件名。
1 |
find $(GROUND_TRUTH_DIR) -name '*.lstmf' -exec echo {} \; | sort -R -o "$@" |
上面的代码实际执行代码为:
1 |
find data/foo-ground-truth -name '*.lstmf' -exec echo {} \; | sort -R -o "data/foo/all-lstmf" |
找到路径data/foo-ground-truth下的所有后缀为”.lstmf”的文件,并将这些文件的文件名写入data/foo/all-lstmf中,该行代码执行完成后,all-lstmf中的部分内容为:
1 2 3 |
data/ground-truth/alexis_ruhe01_1852_0018_022.lstmf data/ground-truth/alexis_ruhe01_1852_0035_019.lstmf data/ground-truth/alexis_ruhe01_1852_0087_027.lstmf |
7. $(OUTPUT_DIR)/list.train: $(ALL_LSTMF)
根据目标与前置条件的关系,接下来执行此行代码,这一块代码中,两个目标有一个共同的前置条件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
$(OUTPUT_DIR)/list.eval \ $(OUTPUT_DIR)/list.train: $(ALL_LSTMF) @mkdir -p $(OUTPUT_DIR) @total=$$(wc -l < $(ALL_LSTMF)); \ train=$$(echo "$$total * $(RATIO_TRAIN) / 1" | bc); \ test "$$train" = "0" && \ echo "Error: missing ground truth for training" && exit 1; \ eval=$$(echo "$$total - $$train" | bc); \ test "$$eval" = "0" && \ echo "Error: missing ground truth for evaluation" && exit 1; \ set -x; \ head -n "$$train" $(ALL_LSTMF) > "$(OUTPUT_DIR)/list.train"; \ tail -n "$$eval" $(ALL_LSTMF) > "$(OUTPUT_DIR)/list.eval" |
在上面的代码段中,wc –l 用途是计算一个文件的行数。
因此,该段代码的大意是:计算ALL_LSTMF中的行数,并根据变量RATIO_TRAIN的设定值分配训练数据量train与评估数据量eval,train和eval任意一个为0,makefile的执行均会终止并报错。
若train和eval符合条件,则将ALL_LSTMF的前train行写入文件data/foo/list.train中,ALL_LSTMF的后eval行写入文件data/foo/list.eval中。
8. $(PROTO_MODEL)
第7步生成的list.train和list.eval未作为其他目标的前置条件,因此从第5步结束的地方开始顺序执行。
不过我不确定第206~225行是否有执行,何时执行的,作何用,但是make trainging –trace均未打印出此部分的代码,有知道的同学还请不吝赐教。
因此我们这里直接开始看第228行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
proto-model: $(PROTO_MODEL) $(PROTO_MODEL): $(OUTPUT_DIR)/unicharset data/radical-stroke.txt combine_lang_model \ --input_unicharset $(OUTPUT_DIR)/unicharset \ --script_dir data \ --numbers $(NUMBERS_FILE) \ --puncs $(PUNC_FILE) \ --words $(WORDLIST_FILE) \ --output_dir data \ $(RECODER) \ --lang $(MODEL_NAME) |
该段代码用于生成一个初始的训练数据文件(.traineddata文件),该文件可用于训练基于LSTM的神经网络模型,它以一个unicharset和一组可选的单词列表作为输入。
$(PROTO_MODEL): $(OUTPUT_DIR)/unicharset data/radical-stroke.txt :makefile会在路径data下寻找radical-stroke.txt文件,如果没有会自动通过wget下载,但为了顺利的进行训练,建议提前下载相应的文件放在执行路径下。
script_dir data :应该指向包含* .unicharset文件的目录。 对于基于英语和其他基于拉丁语的脚本,文件为Latin.unicharset。建议提前下载并放在指定路径下。
有的同学用此方式训练完成后,可能会出现如下警告:
Failed to load any lstm-specific dictionaries for lang led!!
该警告与本代码段中numbers/puncs/words这几行代码相关,相关讨论可以参考这里Failed to load any lstm-specific dictionaries for lang xxx。
我个人认为Tesseract-ocr/tesstrain的makefile中,此处是有BUG的。虽然numbers/puncs/words这几个参数对应的文件是可选项,但若这几个文件缺失,训练得到的字库虽可以正常识别,但是会报出“Failed to load any lstm-specific dictionaries for lang led!!”警告。在makefile中,若START_MODEL未赋值,makefile并不会自动产生任何相关文件;若已赋值,makefile生成的是.lstm-number-dawg/.lstm-punc-dawg/.lstm-word-dawg等为后缀名的文件,这些文件并不是训练时所需要的文件,它们需要更多的命令来将其转换为正确的文件。
在tesstrain-win中,我也没有修改此处,不过大家在训练时自行准备相应基础字库的numbers/puncs/words相关文件,并放入tesstrain-win的makefile指定路径中,即可避开此处的BUG。
9 lstmtraining
第8步生成的文件为以下代码段的前置,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
$(LAST_CHECKPOINT): unicharset lists $(PROTO_MODEL) @mkdir -p $(OUTPUT_DIR)/checkpoints lstmtraining \ --debug_interval $(DEBUG_INTERVAL) \ --traineddata $(PROTO_MODEL) \ --old_traineddata $(TESSDATA)/$(START_MODEL).traineddata \ --continue_from data/$(START_MODEL)/$(MODEL_NAME).lstm \ --model_output $(OUTPUT_DIR)/checkpoints/$(MODEL_NAME) \ --train_listfile $(OUTPUT_DIR)/list.train \ --eval_listfile $(OUTPUT_DIR)/list.eval \ --max_iterations $(MAX_ITERATIONS) |
该段代码正式开始训练LSTM,实际执行语句根据START_MODEL是否有赋值会略有差异,以上代码是START_MODEL已赋值会执行的代码。其中需要注意的参数:
–traineddata:该参数对应combine_lang_model生成的初始训练文件的路径;
–old_traineddata:该参数对应的是基础字库文件的路径,本文中对应的是tessdata/eng.traineddata.
若START_MODEL已赋值,当前训练有基础字库,属于Fine-tune;若START_MODEL未赋值,则属于TRAIN FROM SCRATCH.
两种方式相比,会有以下差异:
Fine-tune特有的两个参数,分别是基础字库的路径和训练的起始点,训练会从基础字库的.lstm文件开始。
1 2 |
--old_traineddata $(TESSDATA)/$(START_MODEL).traineddata \ --continue_from data/$(START_MODEL)/$(MODEL_NAME).lstm \ |
TRAIN FROM SCRATCH特有的两个参数,分别用于设置神经网络的相关参数和训练过程中可占用的内存比例。
1 2 3 |
--net_spec "$(subst c###,c`head -n1 $(OUTPUT_DIR)/unicharset`,$(NET_SPEC))" \ --learning_rate 20e-4 \ |
10 stop_training
训练完成后,将checkpoint文件和.traineddata文件合并成新的.traineddata文件,训练大功告成。
1 2 3 4 5 6 7 8 9 |
lstmtraining \ --stop_training \ --continue_from $(LAST_CHECKPOINT) \ --traineddata $(PROTO_MODEL) \ --model_output $@ |
本文到此结束,感谢阅读,谢谢支持。