强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第 6 章:模型训练

第 6 章:模型训练

从零开始训练或微调 Tesseract LSTM 模型。

6.1 训练概述

训练流程总览
├── 1. 数据准备
│   ├── 收集图片
│   ├── 生成 Ground Truth
│   └── 数据增强
├── 2. 训练配置
│   ├── 设置参数
│   ├── 准备语言文件
│   └── 创建训练脚本
├── 3. 模型训练
│   ├── LSTM 训练
│   ├── 旧引擎训练(可选)
│   └── 合并模型
├── 4. 评估迭代
│   ├── 计算准确率
│   ├── 分析错误
│   └── 调整参数
└── 5. 部署使用
    └── 安装到 tessdata

6.1.1 何时需要训练

场景是否需要训练替代方案
标准印刷体英文使用默认模型
标准印刷体中文使用默认模型
特殊字体(如等宽、手写)微调模型
古籍/历史文档专用训练
特定领域(医学、法律)领域微调
新语言/方言从零训练

6.1.2 训练类型

类型数据量时间精度适用场景
从零训练大(10万+)天级需迭代新语言
微调中(1千-1万)小时级较好特定字体/领域
合并训练小时级多种场景混合

6.2 环境准备

6.2.1 安装训练工具

# Ubuntu/Debian
sudo apt install tesseract-ocr tesseract-ocr-dev leptonica-progs

# 从源码编译训练工具
git clone https://github.com/tesseract-ocr/tesseract.git
cd tesseract
./autogen.sh
mkdir build && cd build
../configure
make -j$(nproc)
sudo make install
sudo make training-install
sudo ldconfig

# 验证训练工具
lstmtraining --version
combine_tessdata --version
unicharset_extractor --version

6.2.2 辅助工具

# 安装 Python 依赖
pip install pytesseract Pillow opencv-python numpy tqdm

# 安装 jTessBoxEditor(GUI 标注工具)
# 下载地址:https://github.com/nguyenq/jTessBoxEditorFX

6.2.3 训练数据目录结构

training/
├── ground-truth/          # Ground Truth 文件
│   ├── 0001.tif           # 训练图片
│   ├── 0001.gt.txt        # 对应文本
│   ├── 0002.tif
│   ├── 0002.gt.txt
│   └── ...
├── eval/                  # 评估数据
│   ├── eval_0001.tif
│   ├── eval_0001.gt.txt
│   └── ...
├── output/                # 输出目录
└── langdata/              # 语言数据
    ├── mylang.config
    ├── mylang.punc
    ├── mylang.numbers
    └── mylang.wordlist

6.3 数据准备

6.3.1 收集训练图片

图片要求

要求推荐值最小值
分辨率300 DPI200 DPI
图片数量1000+100
文字大小20-40px12px
格式TIFF/PNGPNG
颜色灰度/二值灰度
# 图片批量转换为 TIFF
import os
from PIL import Image

def convert_to_tiff(input_dir, output_dir):
    """批量转换图片为 TIFF 格式"""
    os.makedirs(output_dir, exist_ok=True)
    
    for i, filename in enumerate(sorted(os.listdir(input_dir))):
        if filename.endswith(('.png', '.jpg', '.jpeg', '.bmp')):
            img = Image.open(os.path.join(input_dir, filename))
            img = img.convert('L')  # 转为灰度
            
            output_path = os.path.join(output_dir, f'{i+1:04d}.tif')
            img.save(output_path, compression='tiff_lzw')
            print(f"转换: {filename}{output_path}")

convert_to_tiff('./raw_images', './ground-truth')

6.3.2 生成 Ground Truth 文本

Ground Truth 文件是每张图片的正确文本,命名规则:{image_name}.gt.txt

# 示例:0001.tif 对应 0001.gt.txt
echo "Hello World" > ground-truth/0001.gt.txt
echo "这是测试文本" > ground-truth/0002.gt.txt
# 批量生成 Ground Truth 模板
import os

def create_gt_template(image_dir):
    """为图片创建空的 Ground Truth 模板"""
    for filename in sorted(os.listdir(image_dir)):
        if filename.endswith('.tif'):
            gt_filename = filename.replace('.tif', '.gt.txt')
            gt_path = os.path.join(image_dir, gt_filename)
            
            if not os.path.exists(gt_path):
                with open(gt_path, 'w', encoding='utf-8') as f:
                    f.write('')  # 空文件,待手动填写
                print(f"创建模板: {gt_filename}")

create_gt_template('./ground-truth')

6.3.3 数据增强

import cv2
import numpy as np
from PIL import Image, ImageEnhance, ImageFilter
import random

def augment_image(image_path, output_dir, num_augments=5):
    """数据增强:生成多种变体"""
    img = Image.open(image_path)
    base_name = os.path.splitext(os.path.basename(image_path))[0]
    
    for i in range(num_augments):
        aug_img = img.copy()
        
        # 随机选择增强方式
        choice = random.choice(['rotate', 'noise', 'blur', 'contrast', 'scale'])
        
        if choice == 'rotate':
            angle = random.uniform(-5, 5)
            aug_img = aug_img.rotate(angle, fillcolor='white')
        
        elif choice == 'noise':
            np_img = np.array(aug_img)
            noise = np.random.normal(0, 15, np_img.shape).astype(np.uint8)
            np_img = np.clip(np_img + noise, 0, 255).astype(np.uint8)
            aug_img = Image.fromarray(np_img)
        
        elif choice == 'blur':
            radius = random.uniform(0.5, 1.5)
            aug_img = aug_img.filter(ImageFilter.GaussianBlur(radius))
        
        elif choice == 'contrast':
            factor = random.uniform(0.8, 1.3)
            enhancer = ImageEnhance.Contrast(aug_img)
            aug_img = enhancer.enhance(factor)
        
        elif choice == 'scale':
            scale = random.uniform(0.9, 1.1)
            w, h = aug_img.size
            aug_img = aug_img.resize((int(w*scale), int(h*scale)))
        
        # 保存
        output_path = os.path.join(output_dir, f'{base_name}_aug{i+1}.tif')
        aug_img.save(output_path, compression='tiff_lzw')
        
        # 复制 Ground Truth
        gt_path = image_path.replace('.tif', '.gt.txt')
        gt_output = output_path.replace('.tif', '.gt.txt')
        if os.path.exists(gt_path):
            import shutil
            shutil.copy(gt_path, gt_output)

6.3.4 使用现有文本生成训练图片

from PIL import Image, ImageDraw, ImageFont
import os

def text_to_image(text, font_path, output_path, font_size=32):
    """从文本生成训练图片"""
    # 创建图像
    img = Image.new('L', (800, 60), color=255)
    draw = ImageDraw.Draw(img)
    
    # 加载字体
    font = ImageFont.truetype(font_path, font_size)
    
    # 绘制文本
    draw.text((10, 10), text, fill=0, font=font)
    
    # 保存
    img.save(output_path, compression='tiff_lzw')

def generate_training_data(text_file, font_dir, output_dir):
    """从文本文件批量生成训练数据"""
    os.makedirs(output_dir, exist_ok=True)
    
    # 读取文本
    with open(text_file, 'r', encoding='utf-8') as f:
        lines = [line.strip() for line in f if line.strip()]
    
    # 获取可用字体
    fonts = [os.path.join(font_dir, f) for f in os.listdir(font_dir) 
             if f.endswith(('.ttf', '.otf'))]
    
    idx = 1
    for line in lines:
        for font_path in fonts:
            output_path = os.path.join(output_dir, f'{idx:04d}.tif')
            text_to_image(line, font_path, output_path)
            
            # 写入 Ground Truth
            gt_path = output_path.replace('.tif', '.gt.txt')
            with open(gt_path, 'w', encoding='utf-8') as f:
                f.write(line)
            
            idx += 1

# 使用示例
generate_training_data(
    text_file='corpus.txt',
    font_dir='/usr/share/fonts/truetype/',
    output_dir='./ground-truth'
)

6.4 语言配置文件

6.4.1 创建语言配置

# mylang.config
mkdir -p langdata

cat > langdata/mylang.config << 'EOF'
mylang
mylang
mylang
mylang
mylang
EOF

6.4.2 创建标点符号文件

cat > langdata/mylang.punc << 'EOF'
.
,
!
?
;
:
'
"
(
)
[
]
{
}
-
_
/
\
@
#
$
%
&
*
+
=
<
>
|
~
^
EOF

6.4.3 创建数字文件

cat > langdata/mylang.numbers << 'EOF'
0
1
2
3
4
5
6
7
8
9
EOF

6.4.4 创建词表

# 从语料库生成词表
cat corpus.txt | tr ' ' '\n' | sort | uniq -c | sort -rn | head -10000 > langdata/mylang.wordlist

6.5 LSTM 训练

6.5.1 从现有模型微调

#!/bin/bash
# finetune.sh - 微调现有模型

LANG="mylang"
TESSDATA="/usr/share/tesseract-ocr/5/tessdata"
TRAINING_DIR="./ground-truth"
OUTPUT_DIR="./output"
START_MODEL="eng"  # 基础模型

mkdir -p "$OUTPUT_DIR"

# 1. 生成训练文件列表
ls "$TRAINING_DIR"/*.tif > "$OUTPUT_DIR/training_files.txt"

# 2. 提取 LSTM 模型
combine_tessdata -e "$TESSDATA/$START_MODEL.traineddata" \
    "$OUTPUT_DIR/$START_MODEL.lstm"

# 3. 准备训练数据
tesstrain \
    --fonts_dir /usr/share/fonts \
    --fontlist "Arial" "Times New Roman" \
    --lang "$LANG" \
    --linedata_only \
    --noextract_font_properties \
    --output_dir "$OUTPUT_DIR" \
    --maxpages 100

# 4. 训练(微调)
lstmtraining \
    --model_output "$OUTPUT_DIR/$LANG" \
    --continue_from "$OUTPUT_DIR/$START_MODEL.lstm" \
    --traineddata "$TESSDATA/$START_MODEL.traineddata" \
    --train_listfile "$OUTPUT_DIR/training_files.txt" \
    --max_iterations 1000 \
    --learning_rate 0.001

# 5. 合并到最终模型
lstmtraining \
    --stop_training \
    --continue_from "$OUTPUT_DIR/$LANG_checkpoint" \
    --traineddata "$TESSDATA/$START_MODEL.traineddata" \
    --model_output "$OUTPUT_DIR/$LANG.traineddata"

echo "训练完成: $OUTPUT_DIR/$LANG.traineddata"

6.5.2 使用 tesstrain 脚本

# 克隆 tesstrain 工具
git clone https://github.com/tesseract-ocr/tesstrain.git
cd tesstrain

# 准备 Ground Truth
# 将训练数据放在 data/mylang-ground-truth/

# 开始训练
make training \
    MODEL_NAME=mylang \
    START_MODEL=eng \
    TESSDATA=/usr/share/tesseract-ocr/5/tessdata \
    GROUND_TRUTH_DIR=./data/mylang-ground-truth \
    MAX_ITERATIONS=5000

6.5.3 训练参数说明

参数说明推荐值
--max_iterations最大迭代次数1000-10000
--learning_rate学习率0.001-0.01
--learning_rate_min最小学习率0.00001
--momentum动量0.5-0.9
--net_spec网络结构‘[Lfx128]’
--num_hidden隐藏层大小128-256

6.5.4 训练过程监控

# 查看训练日志
tail -f output/mylang_checkpoint.log

# 输出示例
# Line 0: 0.1 loss
# Line 100: 0.05 loss
# Line 200: 0.03 loss
# ...

# 使用 lstmtraining 查看进度
lstmtraining --model_output output/mylang \
    --continue_from output/mylang_checkpoint \
    --traineddata /usr/share/tesseract-ocr/5/tessdata/eng.traineddata \
    --train_listfile output/training_files.txt \
    --max_iterations 5000 \
    --learning_rate 0.001 \
    --debug_level 1 2>&1 | tee training.log

6.6 评估与迭代

6.6.1 计算准确率

import pytesseract
from PIL import Image
import os

def evaluate_model(model_path, eval_dir, lang='mylang'):
    """评估模型准确率"""
    total_chars = 0
    correct_chars = 0
    total_words = 0
    correct_words = 0
    
    for filename in sorted(os.listdir(eval_dir)):
        if filename.endswith('.tif'):
            # 读取 Ground Truth
            gt_path = os.path.join(eval_dir, filename.replace('.tif', '.gt.txt'))
            with open(gt_path, 'r', encoding='utf-8') as f:
                gt_text = f.read().strip()
            
            # OCR 识别
            img_path = os.path.join(eval_dir, filename)
            img = Image.open(img_path)
            pred_text = pytesseract.image_to_string(img, lang=lang).strip()
            
            # 字符级准确率
            for gt_char, pred_char in zip(gt_text, pred_text):
                total_chars += 1
                if gt_char == pred_char:
                    correct_chars += 1
            
            # 词级准确率
            gt_words = gt_text.split()
            pred_words = pred_text.split()
            for gt_word, pred_word in zip(gt_words, pred_words):
                total_words += 1
                if gt_word == pred_word:
                    correct_words += 1
    
    char_acc = correct_chars / total_chars * 100 if total_chars > 0 else 0
    word_acc = correct_words / total_words * 100 if total_words > 0 else 0
    
    print(f"字符准确率: {char_acc:.2f}%")
    print(f"词准确率: {word_acc:.2f}%")
    
    return char_acc, word_acc

# 使用
evaluate_model('output/mylang.traineddata', './eval')

6.6.2 错误分析

def error_analysis(eval_dir, lang='mylang'):
    """分析识别错误"""
    errors = []
    
    for filename in sorted(os.listdir(eval_dir)):
        if filename.endswith('.tif'):
            gt_path = os.path.join(eval_dir, filename.replace('.tif', '.gt.txt'))
            with open(gt_path, 'r', encoding='utf-8') as f:
                gt_text = f.read().strip()
            
            img_path = os.path.join(eval_dir, filename)
            img = Image.open(img_path)
            pred_text = pytesseract.image_to_string(img, lang=lang).strip()
            
            if gt_text != pred_text:
                errors.append({
                    'file': filename,
                    'expected': gt_text,
                    'actual': pred_text
                })
    
    # 输出错误统计
    print(f"总错误数: {len(errors)}")
    print("\n常见错误示例:")
    for err in errors[:10]:
        print(f"  {err['file']}:")
        print(f"    期望: {err['expected']}")
        print(f"    实际: {err['actual']}")
    
    return errors

6.6.3 迭代优化策略

问题解决方案
特定字符识别差增加该字符的训练样本
某种字体识别差增加该字体的训练样本
过拟合增加数据量、降低迭代次数
欠拟合增加迭代次数、调整学习率
混淆字符增加相似字符的对比样本

6.7 模型部署

6.7.1 安装自定义模型

# 复制训练好的模型到 tessdata
sudo cp output/mylang.traineddata /usr/share/tesseract-ocr/5/tessdata/

# 验证
tesseract --list-langs | grep mylang

# 使用
tesseract image.png stdout -l mylang

6.7.2 模型打包

# 使用 combine_tessdata 合并组件
combine_tessdata output/mylang.

# 检查模型内容
combine_tessdata -d /usr/share/tesseract-ocr/5/tessdata/mylang.traineddata

6.8 常见问题

问题原因解决方案
训练不收敛学习率过大降低学习率
训练太慢数据量太大减少数据或使用 GPU
模型文件过大组件过多只保留必要组件
识别精度下降过拟合减少迭代、增加数据
新字符不识别unicharset 缺失重建 unicharset

6.9 本章小结

要点说明
何时训练特殊字体、新语言、领域适配
数据要求100+ 图片,300 DPI,高质量 GT
训练方式微调(推荐)或从零训练
关键参数learning_rate、max_iterations
评估方法字符准确率、词准确率

6.10 扩展阅读


上一章: 多语言支持 | 下一章: PDF 处理