好无聊,还是写博客吧 关于Padd了OCR的训练

概述

好久没写了,写一下。上周终于把这个OCR的项目大概是结束了。后面可能还有乱七八糟的根据意见改的环节。但是主体是结束了。记录一下训练的过程:

P1000544.JPG

一、环境准备

上一篇博客有写到安装标注工具,那时候环境就已经装完了 。

二、数据准备

数据分为两部分:合成数据和真实数据

合成数据以真实场景为背景,以真实的目标字符序列规则和相似的字体生成无序的裁切训练图片和标签。详情参照合成数据那篇博客,我大概合成了6W张图作。真实数据比较少,只有9000张,但实际上的目标字符只占2000。真实数据获取参考数据标注那篇文章。

三、训练计划

  1. 多次尝试以官方模型作为预训练模型。直接用真实数据微调训练 都是早早过拟合 训练效果极差,因为官方模型太大,我们真实数据太小。模型很容易就把我们的数据特征背下来。造成泛化差过拟合,成为废品
  2. 所以需要改变训练计划。

[计划]:

  1. 分三阶段训练

  2. 阶段1:冻结预训练模型的骨干网络,进行迁移训练。让模型适应我们的场景和背景

    1. 一方面官方的预训练模型已经通过大量的各式各样的数据集训练过,拥有很好的通用OCR提取能力。我们需要利用这些。
    2. 加速收敛,减小模型计参数量,节省资源
    3. 有效防止小数据过拟合
  3. 阶段1:取消数据增强,一方面加速,另一方面我们一阶段的任务不是增加泛化能力

  4. 阶段2:解冻训练,同样使用合成数据,开启数据增强。解冻训练可以调整模型的特征提取器。微调权重使模型能有适应我们的目标字符背景,纹理。

  5. 阶段3:真实数据训练,用真实数据训练 配置参数和阶段2差不多。学习率要低一个数量级

数量级要单独拿出来说一下。阶段一我用0.003.这个学习率对比目标检测这些仍然是一个很小的值。但是我们的目的是让模型平稳收敛以及防止过拟合。预训练模型很大,学习率过大大概可能在第一个epoch就会发生过拟合。合成数据也足够多。

第一阶段0.003,第二阶段0.0003,第三阶段0.00005。
这三个值分别减少一个数量级。
第一阶段学习率最大,方便模型从通用模型到我们的合成数据进行快速收敛,防止破坏特征提取器。
第二阶段更低的学习率但是预训练模型变为1阶段的训练结果。这样在1阶段的基础上模型已经对合成数据有了一个很好的基础。2阶段更低的学习率是为了缓慢微调模型特征提取器。防止破坏1阶段的训练成果
第三阶段又比第二阶段减小了一个数量级。此时我们的模型已经完成了在合成数据的训练。是用真实数据要弥合两者的鸿沟。更为了防止较大学习率冲刷掉在合成数据上的学习到的特征。保持平稳,防止过拟合

四、训练配置和代码

  1. 对了还要说一点,要使用自定义的字符集./ppocr/utils/dict/dict.txt 。字符集只包含我们真实可能遇到的字符。这很重要。一方面缩减模型参数,减小计算资源。另一方面减小模型识别错误的可能他,毕竟字符集内容越多模型识别一个字符的可能性越多。

一阶段

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
# 一阶段配置文件
Global:

  model_name: PP-OCRv5_mobile_rec # To use static model for inference.

  debug: false

  use_gpu: true

  epoch_num: 20

  log_smooth_window: 20

  print_batch_step: 10

  save_model_dir: ./output/PP-OCRv5_mobile_rec

  save_epoch_step: 10

  eval_batch_step: [0, 200]

  cal_metric_during_train: true

  pretrained_model: ./pretrained_model/PP-OCRv5_mobile_rec_pretrained.pdparams

  checkpoints:

  save_inference_dir:

  use_visualdl: false

  infer_img: doc/imgs_words/ch/word_1.jpg

  character_dict_path: ./ppocr/utils/dict/dict.txt

  max_text_length: &max_text_length 30

  infer_mode: false

  use_space_char: true

  distributed: true

  save_res_path: ./output/rec/predicts_ppocrv5.txt

  d2s_train_image_shape: [3, 48, 320]



Optimizer:

  name: Adam

  beta1: 0.9

  beta2: 0.999

  lr:

    name: Cosine

    learning_rate: 0.0003

    warmup_epoch: 3

  regularizer:

    name: L2

    factor: 3.0e-05




Architecture:

  model_type: rec

  algorithm: SVTR_LCNet

  Transform:

  Backbone:

    name: PPLCNetV3

    scale: 0.95

    freeze: true

  Head:

    name: MultiHead

    head_list:

      - CTCHead:

          Neck:

            name: svtr

            dims: 120

            depth: 2  

            hidden_dims: 120

            kernel_size: [1, 3]

            use_guide: True

          Head:

            fc_decay: 0.00001

      - NRTRHead:

          nrtr_dim: 384

          max_text_length: *max_text_length



Loss:

  name: MultiLoss

  loss_config_list:

    - CTCLoss:

    - NRTRLoss:



PostProcess:  

  name: CTCLabelDecode



Metric:

  name: RecMetric

  main_indicator: acc



Train:

  dataset:

    name: MultiScaleDataSet

    ds_width: false

    data_dir: ./train_data/

    ext_op_transform_idx: 1

    label_file_list:

    - ./train_data/train.txt

    transforms:

    - DecodeImage:

        img_mode: BGR

        channel_first: false

    # - RecConAug:

    #     prob: 0.5

    #     ext_data_num: 2

    #     image_shape: [48, 320, 3]

    #     max_text_length: *max_text_length

    # - RecAug:

    - MultiLabelEncode:

        gtc_encode: NRTRLabelEncode

    - KeepKeys:

        keep_keys:

        - image

        - label_ctc

        - label_gtc

        - length

        - valid_ratio

  sampler:

    name: MultiScaleSampler

    scales: [[320, 32], [320, 48], [320, 64]]

    first_bs: &bs 64

    fix_bs: false

    divided_factor: [8, 16] # w, h

    is_training: True

  loader:

    shuffle: true

    batch_size_per_card: *bs

    drop_last: true

    num_workers: 4

Eval:

  dataset:

    name: SimpleDataSet

    data_dir: ./train_data

    label_file_list:

    - ./train_data/val.txt

    transforms:

    - DecodeImage:

        img_mode: BGR

        channel_first: false

    - MultiLabelEncode:

        gtc_encode: NRTRLabelEncode

    - RecResizeImg:

        image_shape: [3, 48, 320]

    - KeepKeys:

        keep_keys:

        - image

        - label_ctc

        - label_gtc

        - length

        - valid_ratio

  loader:

    shuffle: false

    drop_last: false

    batch_size_per_card: 64

    num_workers: 8
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
## 一阶段的训练脚本:

import os

import sys

import subprocess

import argparse

import yaml



def train_step1(config_path, output_dir, pretrained_model=None):

    """

    执行第一阶段训练:冻结骨干网络,使用合成数据

    参数:

    config_path: 基础配置文件路径

    output_dir: 输出目录

    pretrained_model: 预训练模型路径(可选)

    """

    # 读取基础配置

    with open(config_path, 'r', encoding='utf-8') as f:

        config = yaml.safe_load(f)

    # 修改配置以适应第一阶段训练

    config['Global']['save_model_dir'] = os.path.join(output_dir, 'step1')

    config['Global']['epoch_num'] = 20

    # 设置冻结骨干网络

    config['Architecture']['Backbone']['freeze'] = True

    # 设置合成数据路径

    config['Train']['dataset']['label_file_list'] = ['./train_data/train.txt']

    config['Eval']['dataset']['label_file_list'] = ['./train_data/val.txt']

    # 如果有预训练模型,设置预训练模型路径

    if pretrained_model:

        config['Global']['pretrained_model'] = pretrained_model

    # 降低学习率,因为只训练头部

    config['Optimizer']['lr']['learning_rate'] = 0.0003

    # 保存修改后的配置

    step1_config_path = os.path.join(output_dir, 'step1_config.yml')

    with open(step1_config_path, 'w', encoding='utf-8') as f:

        yaml.dump(config, f, default_flow_style=False, allow_unicode=True)

    print(f"第一阶段配置文件已保存: {step1_config_path}")

    # 执行训练命令

    cmd = [

        'python', 'tools/train.py',

        '-c', step1_config_path,

        '-o', f"Global.use_gpu=true",

        '-o', f"Global.use_visualdl=true"  # 启用VisualDL监控

    ]



    # process = subprocess.Popen(

    #     cmd,

    #     stdout=subprocess.PIPE,

    #     stderr=subprocess.PIPE,

    #     encoding='utf-8' # <-- 添加这一行!

    # )

    print("开始第一阶段训练...")

    print("执行命令:", ' '.join(cmd))

    try:

        process = subprocess.Popen(

            cmd,

            stdout=subprocess.PIPE,

            stderr=subprocess.STDOUT # 这会将 stderr 合并到 stdout

        )



        # 循环读取子进程的输出 (现在是字节流)

        for byte_line in process.stdout: # 注意这里我改名为 byte_line,强调它是字节

            try:

                # 尝试用 UTF-8 解码,如果失败则忽略错误字节

                line = byte_line.decode('utf-8', errors='ignore').strip()

                # 如果发现 UTF-8 解码出的内容仍有乱码,可以尝试 GBK:

                # line = byte_line.decode('gbk', errors='ignore').strip()

                if line: # 只打印非空行

                    print(line)

            except Exception as e:


                print(f"Warning: Could not decode line: {byte_line}. Error: {e}")



        # 等待子进程完成并获取返回值

        process.wait()



        if process.returncode != 0:

            print(f"Error: Subprocess failed with exit code {process.returncode}")

            # raise RuntimeError(f"Training subprocess failed with exit code {process.returncode}")



        print("Training process finished.")

        return os.path.join(output_dir, 'best_accuracy')



    except Exception as e:

        print(f"An error occurred during training: {e}")

        raise

    if process.returncode == 0:

        print("第一阶段训练完成!")

        best_model_path = os.path.join(config['Global']['save_model_dir'], 'best_accuracy')

        print(f"最佳模型保存在: {best_model_path}")

        return best_model_path

    else:

        print("第一阶段训练失败!")

        return None



def main():

    parser = argparse.ArgumentParser(description="PaddleOCR第一阶段训练脚本")

    parser.add_argument("--config", type=str, default="config_rec/PP-OCRv5_mobile_rec1.yml",

                       help="基础配置文件路径")

    parser.add_argument("--output_dir", type=str, default="./output",

                       help="输出目录")

    parser.add_argument("--pretrained_model", type=str, default="./pretrained_model/PP-OCRv5_mobile_rec_pretrained.pdparams",

                       help="预训练模型路径(可选)")

    args = parser.parse_args()

    # 确保输出目录存在

    if not os.path.exists(args.output_dir):

        os.makedirs(args.output_dir)

    # 执行第一阶段训练

    best_model = train_step1(args.config, args.output_dir, args.pretrained_model)

    if best_model:

        print("\n" + "="*50)

        print("第一阶段训练成功完成!")

        print(f"最佳模型路径: {best_model}")

        print("下一步: 使用此模型作为第二阶段训练的预训练模型")

        print("="*50)

    else:

        print("第一阶段训练失败,请检查错误信息")



if __name__ == "__main__":

    main()

二阶段:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
#二阶段配置文件
Global:

  model_name: PP-OCRv5_mobile_rec # To use static model for inference.

  debug: false

  use_gpu: true

  epoch_num: 30

  log_smooth_window: 20

  print_batch_step: 10

  save_model_dir: ./output/PP-OCRv5_mobile_rec

  save_epoch_step: 10

  eval_batch_step: [0, 200]

  cal_metric_during_train: true

  pretrained_model:

  checkpoints:

  save_inference_dir:

  use_visualdl: false

  infer_img: doc/imgs_words/ch/word_1.jpg

  character_dict_path: ./ppocr/utils/dict/ppocrv5_dict.txt

  max_text_length: &max_text_length 30

  infer_mode: false

  use_space_char: true

  distributed: true

  save_res_path: ./output/rec/predicts_ppocrv5.txt

  d2s_train_image_shape: [3, 48, 320]




Optimizer:

  name: Adam

  beta1: 0.9

  beta2: 0.999

  lr:

    name: Cosine

    learning_rate: 0.00003

    warmup_epoch: 7

  regularizer:

    name: L2

    factor: 3.0e-05




Architecture:

  model_type: rec

  algorithm: SVTR_LCNet

  Transform:

  Backbone:

    name: PPLCNetV3

    scale: 0.95

  Head:

    name: MultiHead

    head_list:

      - CTCHead:

          Neck:

            name: svtr

            dims: 120

            depth: 2

            hidden_dims: 120

            kernel_size: [1, 3]

            use_guide: True

          Head:

            fc_decay: 0.00001

      - NRTRHead:

          nrtr_dim: 384

          max_text_length: *max_text_length



Loss:

  name: MultiLoss

  loss_config_list:

    - CTCLoss:

    - NRTRLoss:



PostProcess:  

  name: CTCLabelDecode



Metric:

  name: RecMetric

  main_indicator: acc



Train:

  dataset:

    name: MultiScaleDataSet

    ds_width: false

    data_dir: ./train_data/

    ext_op_transform_idx: 1

    label_file_list:

    - ./train_data/train.txt

    transforms:

    - DecodeImage:

        img_mode: BGR

        channel_first: false

    - RecConAug:

        prob: 0.3

        ext_data_num: 1

        image_shape: [48, 320, 3]

        max_text_length: *max_text_length

    # - RecAug:



    - MultiLabelEncode:

        gtc_encode: NRTRLabelEncode

    - KeepKeys:

        keep_keys:

        - image

        - label_ctc

        - label_gtc

        - length

        - valid_ratio

  sampler:

    name: MultiScaleSampler

    scales: [[320, 32], [320, 48], [320, 64]]

    first_bs: &bs 64

    fix_bs: false

    divided_factor: [8, 16] # w, h

    is_training: True

  loader:

    shuffle: true

    batch_size_per_card: *bs

    drop_last: true

    num_workers: 4

Eval:

  dataset:

    name: SimpleDataSet

    data_dir: ./train_data

    label_file_list:

    - ./train_data/val.txt

    transforms:

    - DecodeImage:

        img_mode: BGR

        channel_first: false

    - MultiLabelEncode:

        gtc_encode: NRTRLabelEncode

    - RecResizeImg:

        image_shape: [3, 48, 320]

    - KeepKeys:

        keep_keys:

        - image

        - label_ctc

        - label_gtc

        - length

        - valid_ratio

  loader:

    shuffle: false

    drop_last: false

    batch_size_per_card: 64

    num_workers: 8
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
#二阶段训练脚本

import os

import sys

import subprocess

import argparse

import yaml



def train_step2(base_config_path, output_root_dir,

                pretrained_model_path_stage1,

                synthetic_train_label, synthetic_eval_label):

    """

    执行第二阶段训练:解冻骨干网络,使用合成数据。

    参数:

    base_config_path: 基础配置文件路径

    output_root_dir: 所有输出的根目录 (例如 './output_multistage')

    pretrained_model_path_stage1: 第一阶段训练得到的最佳模型路径

    synthetic_train_label: 合成数据的训练标签文件路径

    synthetic_eval_label: 合成数据的验证标签文件路径

    """

    stage_name = 'step2'

    stage_output_dir = os.path.join(output_root_dir, stage_name)

    os.makedirs(stage_output_dir, exist_ok=True)



    print(f"\n" + "#"*50)

    print(f"### 开始 {stage_name} 阶段训练: 解冻骨干网络,使用合成数据 ###")

    print("#"*50)



    # 读取基础配置

    with open(base_config_path, 'r', encoding='utf-8') as f:

        config = yaml.safe_load(f)

    # --- 修改配置以适应第二阶段训练 ---

    config['Global']['save_model_dir'] = stage_output_dir

    config['Global']['epoch_num'] = 20 # 增加 epoch 数量,让整个网络有足够时间微调

    # 关键修改:解冻骨干网络

    config['Architecture']['Backbone']['freeze'] = False

    # 设置数据路径 (仍然是合成数据)

    config['Train']['dataset']['label_file_list'] = [synthetic_train_label]

    config['Eval']['dataset']['label_file_list'] = [synthetic_eval_label]

    # 加载第一阶段训练得到的最佳模型

    config['Global']['pretrained_model'] = pretrained_model_path_stage1

    # 降低学习率,避免破坏预训练特征


    config['Optimizer']['lr']['learning_rate'] = 0.00004

    # 保存修改后的配置

    stage_config_path = os.path.join(stage_output_dir, f'{stage_name}_config.yml')

    with open(stage_config_path, 'w', encoding='utf-8') as f:

        yaml.dump(config, f, default_flow_style=False, allow_unicode=True)

    print(f"[{stage_name}] 阶段配置文件已保存: {stage_config_path}")

    # 执行训练命令

    # 假设 train_step2.py 脚本和 tools 目录在同一父目录下

    train_script_path = os.path.join(os.path.dirname(__file__), 'tools', 'train.py')



    cmd = [

        'python', train_script_path,

        '-c', stage_config_path,

        '-o', f"Global.use_gpu=true",

        '-o', f"Global.use_visualdl=true"  # 启用VisualDL监控

    ]

    print(f"执行命令: {' '.join(cmd)}")

    try:

        process = subprocess.Popen(

            cmd,

            stdout=subprocess.PIPE,

            stderr=subprocess.STDOUT

        )



        for byte_line in process.stdout:

            try:

                line = byte_line.decode('utf-8', errors='ignore').strip()

                if line:

                    print(f"[{stage_name}] {line}")

            except Exception as e:

                print(f"[{stage_name}] Warning: Could not decode line: {byte_line}. Error: {e}")



        process.wait()



        if process.returncode != 0:

            print(f"[{stage_name}] Error: Subprocess failed with exit code {process.returncode}")

            raise RuntimeError(f"{stage_name} 阶段训练失败,请检查日志。")



        print(f"[{stage_name}] 阶段训练完成!")

        # 保存最佳模型通 save_model_dir/best_accuracy

        best_model_dir = os.path.join(config['Global']['save_model_dir'], 'best_accuracy')

        print(f"[{stage_name}] 最佳模型保存在: {best_model_dir}")

        return best_model_dir

    except Exception as e:

        print(f"[{stage_name}] An error occurred during training: {e}")

        raise



def main():

    parser = argparse.ArgumentParser(description="PaddleOCR第二阶段训练脚本")

    parser.add_argument("--base_config", type=str, default="config_rec/PP-OCRv5_mobile_rec2.yml",

                       help="基础配置文件路径")

    parser.add_argument("--output_root_dir", type=str, default="./output",

                       help="所有阶段输出的根目录")

    parser.add_argument("--pretrained_model_path_stage1", type=str,

                       default="./output/step1/best_model/model",

                       help="第一阶段训练得到的最佳模型路径")

    parser.add_argument("--synthetic_train_label", type=str, default="./train_data/train.txt",

                       help="合成数据的训练标签文件路径")

    parser.add_argument("--synthetic_eval_label", type=str, default="./train_data/val.txt",

                       help="合成数据的验证标签文件路径")

    args = parser.parse_args()

    # 确保总输出目录存在

    if not os.path.exists(args.output_root_dir):

        os.makedirs(args.output_root_dir)

    best_model_stage2_path = train_step2(

        base_config_path=args.base_config,

        output_root_dir=args.output_root_dir,

        pretrained_model_path_stage1=args.pretrained_model_path_stage1,

        synthetic_train_label=args.synthetic_train_label,

        synthetic_eval_label=args.synthetic_eval_label

    )

    if best_model_stage2_path:

        print("\n" + "="*50)

        print("第二阶段训练成功完成!")

        print(f"第二阶段最佳模型路径: {best_model_stage2_path}")

        print("="*50)

    else:

        print("第二阶段训练失败,请检查错误信息")



if __name__ == "__main__":

    main()

三阶段:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
Global:

  model_name: PP-OCRv5_mobile_rec # To use static model for inference.

  debug: false

  use_gpu: true

  epoch_num: 20

  log_smooth_window: 20

  print_batch_step: 10

  save_model_dir: ./output/PP-OCRv5_mobile_rec

  save_epoch_step: 10

  eval_batch_step: [0, 50]

  cal_metric_during_train: true

  pretrained_model:

  checkpoints:

  save_inference_dir: ./output/inference_model

  use_visualdl: false

  infer_img: Y:/train_data/crop/20250622_182327864__25204830030301.png

  character_dict_path: ./ppocr/utils/dict/dict.txt

  max_text_length: &max_text_length 30

  infer_mode: false

  use_space_char: true

  distributed: true

  save_res_path: ./output/rec/predicts_ppocrv5.txt

  d2s_train_image_shape: [3, 48, 320]




Optimizer:

  name: Adam

  beta1: 0.9

  beta2: 0.999

  lr:

    name: Cosine

    learning_rate: 0.00003

    warmup_epoch: 3

  regularizer:

    name: L2

    factor: 3.0e-05




Architecture:

  model_type: rec

  algorithm: SVTR_LCNet

  Transform:

  Backbone:

    name: PPLCNetV3

    scale: 0.95

  Head:

    name: MultiHead

    head_list:

      - CTCHead:

          Neck:

            name: svtr

            dims: 120

            depth: 2

            hidden_dims: 120

            kernel_size: [1, 3]

            use_guide: True

          Head:

            fc_decay: 0.00001

      - NRTRHead:

          nrtr_dim: 384

          max_text_length: *max_text_length



Loss:

  name: MultiLoss

  loss_config_list:

    - CTCLoss:

    - NRTRLoss:



PostProcess:  

  name: CTCLabelDecode



Metric:

  name: RecMetric

  main_indicator: acc



Train:

  dataset:

    name: MultiScaleDataSet

    ds_width: false

    data_dir: ./train_data/

    ext_op_transform_idx: 1

    label_file_list:

    - ./train_data/train.txt

    transforms:

    - DecodeImage:

        img_mode: BGR

        channel_first: false

    - RecConAug:

        prob: 0.3

        ext_data_num: 2

        image_shape: [48, 320, 3]

        max_text_length: *max_text_length

    - RecAug:

    - MultiLabelEncode:

        gtc_encode: NRTRLabelEncode

    - KeepKeys:

        keep_keys:

        - image

        - label_ctc

        - label_gtc

        - length

        - valid_ratio

  sampler:

    name: MultiScaleSampler

    scales: [[320, 32], [320, 48], [320, 64]]

    first_bs: &bs 64

    fix_bs: false

    divided_factor: [8, 16] # w, h

    is_training: True

  loader:

    shuffle: true

    batch_size_per_card: *bs

    drop_last: true

    num_workers: 8

Eval:

  dataset:

    name: SimpleDataSet

    data_dir: ./train_data

    label_file_list:

    - ./train_data/val.txt

    transforms:

    - DecodeImage:

        img_mode: BGR

        channel_first: false

    - MultiLabelEncode:

        gtc_encode: NRTRLabelEncode

    - RecResizeImg:

        image_shape: [3, 48, 320]

    - KeepKeys:

        keep_keys:

        - image

        - label_ctc

        - label_gtc

        - length

        - valid_ratio

  loader:

    shuffle: false

    drop_last: false

    batch_size_per_card: 64

    num_workers: 8
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
import os

import sys

import subprocess

import argparse

import yaml



def train_step3(base_config_path, output_root_dir,

                pretrained_model_path_stage2,

                real_train_label, real_eval_label):

    """

    执行第三阶段训练:在真实数据上微调。

    参数:

    base_config_path: 基础配置文件路径 (为第三阶段准备的配置文件)

    output_root_dir: 所有阶段输出的根目录 (例如 './output_multistage')

    pretrained_model_path_stage2: 第二阶段训练得到的最佳模型路径

    real_train_label: 真实数据的训练标签文件路径

    real_eval_label: 真实数据的验证标签文件路径

    """

    stage_name = 'step3'

    stage_output_dir = os.path.join(output_root_dir, stage_name)

    os.makedirs(stage_output_dir, exist_ok=True)



    print(f"\n" + "#"*50)

    print(f"### 开始 {stage_name} 阶段训练: 在真实数据上微调 ###")

    print("#"*50)



    # 读取基础配置

    with open(base_config_path, 'r', encoding='utf-8') as f:

        config = yaml.safe_load(f)

    # --- 第三阶段训练 ---

    config['Global']['save_model_dir'] = stage_output_dir

    # 真实数据较少,epoch不宜过多,并做好early stopping

    config['Global']['epoch_num'] = 20

    # 骨干网络在第二阶段已解冻,此处保持不冻结

    config['Architecture']['Backbone']['freeze'] = False

    # 设置真实数据路径

    config['Train']['dataset']['label_file_list'] = [real_train_label]

    config['Eval']['dataset']['label_file_list'] = [real_eval_label]

    # 加载第二阶段训练得到的最佳模型

    config['Global']['pretrained_model'] = pretrained_model_path_stage2

    config['Optimizer']['lr']['learning_rate'] = 0.00002

    # 保存修改后的配置

    stage_config_path = os.path.join(stage_output_dir, f'{stage_name}_config.yml')

    with open(stage_config_path, 'w', encoding='utf-8') as f:

        yaml.dump(config, f, default_flow_style=False, allow_unicode=True)

    print(f"[{stage_name}] 阶段配置文件已保存: {stage_config_path}")

    # 执行训练命令

    train_script_path = os.path.join(os.path.dirname(__file__), 'tools', 'train.py')



    cmd = [

        'python', train_script_path,

        '-c', stage_config_path,

        '-o', f"Global.use_gpu=true",

        '-o', f"Global.use_visualdl=true"

    ]

    print(f"执行命令: {' '.join(cmd)}")

    try:

        process = subprocess.Popen(

            cmd,

            stdout=subprocess.PIPE,

            stderr=subprocess.STDOUT

        )



        for byte_line in process.stdout:

            try:

                line = byte_line.decode('utf-8', errors='ignore').strip()

                if line:

                    print(f"[{stage_name}] {line}")

            except Exception as e:

                print(f"[{stage_name}] Warning: Could not decode line: {byte_line}. Error: {e}")



        process.wait()



        if process.returncode != 0:

            print(f"[{stage_name}] Error: Subprocess failed with exit code {process.returncode}")

            raise RuntimeError(f"{stage_name} 阶段训练失败,请检查日志。")



        print(f"[{stage_name}] 阶段训练完成!")

        # 最佳模型通常保存在 save_model_dir/best_accuracy

        best_model_dir = os.path.join(config['Global']['save_model_dir'], 'best_accuracy')

        print(f"[{stage_name}] 最终最佳模型保存在: {best_model_dir}")

        return best_model_dir

    except Exception as e:

        print(f"[{stage_name}] An error occurred during training: {e}")

        raise



def main():



    parser = argparse.ArgumentParser(description="PaddleOCR第三阶段训练脚本")

    parser.add_argument("--base_config", type=str, default="config_rec/PP-OCRv5_mobile_rec3.yml",

                       help="第三阶段的基础配置文件路径")

    parser.add_argument("--output_root_dir", type=str, default="./output",

                       help="所有阶段输出的根目录")

    parser.add_argument("--pretrained_model_path_stage2", type=str,

                       default="./output/step2/best_model/model", # 默认指向第二阶段的输出

                       help="第二阶段训练得到的最佳模型路径")

    parser.add_argument("--real_train_label", type=str, default="./train_data/train.txt",

                       help="真实数据的训练标签文件路径")

    parser.add_argument("--real_eval_label", type=str, default="./train_data/val.txt",

                       help="真实数据的验证标签文件路径")

    args = parser.parse_args()

    # 确保总输出目录存在

    if not os.path.exists(args.output_root_dir):

        os.makedirs(args.output_root_dir)

    final_best_model_path = train_step3(

        base_config_path=args.base_config,

        output_root_dir=args.output_root_dir,

        pretrained_model_path_stage2=args.pretrained_model_path_stage2,

        real_train_label=args.real_train_label,

        real_eval_label=args.real_eval_label

    )

    if final_best_model_path:

        print("\n" + "="*50)

        print("所有训练阶段成功完成!")

        print(f"最终最佳模型路径: {final_best_model_path}")

        print("========================================")

        print("您现在可以使用此模型进行推理或部署。")

        print("请记得使用独立的真实测试集来评估模型的最终性能!")

        print("="*50)

    else:

        print("第三阶段训练失败,请检查错误信息")



if __name__ == "__main__":

    main()

五、模型评估以及导出

这里直接参照官方给出的教程,用其他方式都会有解码问题。模型输出乱码

这文档还藏得挺深:Title Unavailable | Site Unreachable