Advertisement

自己训练 PaddleOCR PP_OCRv4

阅读量:

文章目录

  • 打标工具

    • 数据准备
    • 小坑。。。
  • Docker

    • 训练
    • Study Case
  • 模型的核心算法

  • 固定主干网络

  • 固定头部结构

  • 执行参数优化过程

  • 执行预测结果的生成,并将生成的数据与基准进行对比分析。

  • 我们的训练经验Tricks

    • PP-LCNet

    • 基础卷积层

      • 门控线性单元(h-swish)

      • 门控sigmoid单元(h-sigmoid)

      • 深度可分离卷积(DepthSepConv)

        • 深度卷积(depth-wise convolution)与逐点卷积(point-wise convolution)结合使用
      • 参数规模对比分析

      • GAP

      • Paddle V3 怎么支持variable input的?

    • SVTR

    • Loss

      • CTC
      • NRTRLoss
  • Others

      • Adam Weighted
      • Cosine Learning Rate
      • Pre-warmup步骤
      • PyTorch.gather函数
      • 原地操作实现
      • 静态推理图构建

打标工具

致谢一位细心的网民分享了此标记工具,请注意使用时确保准确性。

数据准备

类似的如下,程序已经帮你自动抠图了

小坑。。。

只是这个工具有个小坑get_rotate_crop_image()

我的标注数据导出时,很多数据变成倒的

请添加图片描述

hmmmm, 你管我~

复制代码
    if dst_img_height * 1.0 / dst_img_width >= 1.5:
     dst_img = np.rot90(dst_img)
    
    
      
      
    
    代码解读

这个代码表现得相当不错。原本旨在对带有方向性的数据进行校准的功能,在实际应用中受到了条件限制的程度较高。因此,在您的数据结构类似于我的案例时,请确信这些框都是水平放置的而非呈斜向排列的。建议您稳妥地注释掉这两行代码,并观察优化后的结果会令人满意。

考虑到在模型训练过程中加入旋转数据增强措施,在实际应用中发现当旋转角度过大时,则会显著提高模型的训练难度。这种做法可能会导致原本较为准确识别的对象出现误判。因此建议根据实际情况来决定:若具备充足的时间资源,则可适当增加训练轮次;若时间较为紧张,则应避免过度提升模型的训练负担。

Docker

下载官方的docker:

复制代码
    docker pull registry.baidubce.com/paddlepaddle/paddle:2.4.2-gpu-cuda11.7-cudnn8.4-trt8.4
    
    
      
    
    代码解读

当已有一个与当前相同的image存在时,则应执行重命名为NewName的操作,并使用$ docker tag imageID NewName:ID

复制代码
    docker tag 269876gn389 ocr_rec:2.4.2
    
    
      
    
    代码解读

好了,接下来就可以正常套路走了:

复制代码
    docker run -d --gpus all --shm-size=128m --name ppocr_rec -v "$PWD:/home" ocr_rec:2.4.2 /bin/bash
    
    
      
    
    代码解读

其中,在使用命令行参数时设置共享内存变量--shm-size,默认情况下该参数未被配置(未成功实现),这可能导致出现BUS错误(bus error)。为了避免这一问题,请尝试将共享内存大小设为64GB进行测试(test)

Error: An unexpected BUS error occurred within the DataLoader worker. This could be attributed to insufficient shared memory (shm), suggesting it’s advisable to verify if the use_shared_memory flag is enabled and if sufficient storage space exists on /dev/shm.

$PWD

训练

通常建议在配置文件中将workers的数量设为零;如果不这样做可能会导致shm错误。

复制代码
    python tools/train.py -c configs/rec/PP-OCRv4/en_PP-OCRv4_rec.yml -o Global.pretrained_model=./pretrain_models/en_PP-OCRv4_rec_train/best_accuracy
    
    
      
    
    代码解读

没事,没有训练到能差不多能用的模型前,这个不用在意可以忽略:

复制代码
    python tools/eval.py -c configs/rec/PP-OCRv4/en_PP-OCRv4_rec.yml -o Global.checkpoints=./output/rec_ppocr_v4/latest
    
    
      
    
    代码解读

这个用于检测识别模型如何的:

复制代码
    python tools/infer_rec.py -c configs/rec/PP-OCRv4/en_PP-OCRv4_rec.yml -o Global.pretrained_model=./output/rec_ppocr_v4/latest Global.infer_img=doc/imgs_words/en/word_1.png
    
    
      
    
    代码解读

导出成我们日常从官网下载的格式:

复制代码
    python tools/export_model.py -c configs/rec/PP-OCRv4/en_PP-OCRv4_rec.yml -o Global.pretrained_model=./output/rec_ppocr_v4/latest Global.save_inference_dir=./inference/en_PP-OCRv4_rec/
    
    
      
    
    代码解读

如果很不幸,你只能用CPU,那就导出ONNX:

复制代码
    paddle2onnx --model_dir ./inference/en_PP-OCRv4_rec_migu/ --model_filename inference.pdmodel --params_filename inference.pdiparams --save_file ./output/rec_onnx_migu/infer.onnx --opset_version 11 --enable_onnx_checker True
    
    
      
    
    代码解读

Study Case

在这里插入图片描述

采用了多种来源的数据进行实验研究,在实验集中共有8298张样本(其中官方推荐的ICDAR2015数据集占据较大比例)。目前计划进行100个epoch的训练过程,在该过程中每隔1千步就能评估一次模型性能。当前模型的准确率维持在9O%左右水平,并因此需要继续进行更高轮次的断点检查以优化模型性能。对默认的数据增强策略进行了初步验证

在这里插入图片描述

模型算法

在这里插入图片描述

以下是我修改的 en_PP-OCRv4_rec.yml

复制代码
    name: AdamW
    beta1: 0.9
    beta2: 0.999
    epsilon: 1.e-8
    no_weight_decay_name: norm
    one_dim_param_no_weight_decay: True
    - RecConAug:
     prob: 0.5
     ext_data_num: 2
     image_shape:
       - 48
       - 320
       - 3
     max_text_length: 25
    - RecAug:
    tia_prob: 0.4
    crop_prob: 0.0
    reverse_prob: 0.4
    noise_prob: 0.4
    jitter_prob: 0.5
    blur_prob: 0.4
    hsv_aug_prob: 0.5
    - SVTRRecAug:
     aug_type: 0
     geometry_p: 0.5
     deterioration_p: 0.25
     colorjitter_p: 0.1
    - CTCLoss:
    use_focal_loss: true
    weight: 2
    
    
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
    
    代码解读

冻结backbone

找到文件:PaddleOCR-main\ppocr\modeling\architectures\base_model.py

复制代码
    def forward(self, x, data=None):
    y = dict()
    if self.use_transform:
        x = self.transform(x)
    if self.use_backbone:
        x = self.backbone(x)
        for xx in x:            
            xx.stop_gradient = True
    
    
      
      
      
      
      
      
      
      
    
    代码解读

冻结Head

复制代码
    if self.use_head:
       x = self.head(x, targets=data)
    if isinstance(x,dict):
        for xx in x.keys():
            # x[xx].stop_gradient = True
            if xx == "gtc":
                x[xx].stop_gradient = True
    
    
      
      
      
      
      
      
      
    
    代码解读

测试

在当前阶段,我们主要依赖于eval.py来进行程序运行,并开展测试工作哦。这是因为通过现有的代码库我们可以有效地评估系统的性能指标。

由于我们无法预知未来场景的变化,在评估模型鲁棒性时可以尝试向测试用例中添加一些未曾出现在训练数据中的内容以验证其适应能力

要更改yml文件里的:

复制代码
    Eval:
      dataset:
    name: SimpleDataSet
    data_dir: ./test_data/
    label_file_list:
    - ./test_data/rec_gt.txt
    
    
      
      
      
      
      
      
    
    代码解读

为了使所有数据都能被成功测试,在计算时将数据加载器的数量减少一个单位。

复制代码
    max_iter = (
    len(valid_dataloader)
    if platform.system() == "Windows"
    else len(valid_dataloader)
    )
    
    
      
      
      
      
      
    
    代码解读
请添加图片描述
请添加图片描述

测试输出预测的结果Excel并能比较

复制代码
    def eval(
    model,
    valid_dataloader,
    post_process_class,
    eval_class,
    model_type=None,
    extra_input=False,
    scaler=None,
    amp_level="O2",
    amp_custom_black_list=[],
    amp_custom_white_list=[],
    amp_dtype="float16",
    ):
    model.eval()
    with paddle.no_grad():
        total_frame = 0.0
        total_time = 0.0
        pbar = tqdm(
            total=len(valid_dataloader), desc="eval model:", position=0, leave=True
        )
        max_iter = (
            len(valid_dataloader)
            if platform.system() == "Windows"
            else len(valid_dataloader)
        )
        sum_images = 0
        predicted_data = []
        original_data = []
        for idx, batch in enumerate(valid_dataloader):
            if idx >= max_iter:
                break
            images = batch[0]
            start = time.time()
    
            # use amp
            if scaler:
                with paddle.amp.auto_cast(
                    level=amp_level,
                    custom_black_list=amp_custom_black_list,
                    dtype=amp_dtype,
                ):
                    if model_type == "table" or extra_input:
                        preds = model(images, data=batch[1:])
                    elif model_type in ["kie"]:
                        preds = model(batch)
                    elif model_type in ["can"]:
                        preds = model(batch[:3])
                    elif model_type in ["sr"]:
                        preds = model(batch)
                        sr_img = preds["sr_img"]
                        lr_img = preds["lr_img"]
                    else:
                        preds = model(images)
                preds = to_float32(preds)
            else:
                if model_type == "table" or extra_input:
                    preds = model(images, data=batch[1:])
                elif model_type in ["kie"]:
                    preds = model(batch)
                elif model_type in ["can"]:
                    preds = model(batch[:3])
                elif model_type in ["sr"]:
                    preds = model(batch)
                    sr_img = preds["sr_img"]
                    lr_img = preds["lr_img"]
                else:
                    preds = model(images)
    
            batch_numpy = []
            for item in batch:
                if isinstance(item, paddle.Tensor):
                    batch_numpy.append(item.numpy())
                else:
                    batch_numpy.append(item)
            # Obtain usable results from post-processing methods
            total_time += time.time() - start
            # Evaluate the results of the current batch
            if model_type in ["table", "kie"]:
                if post_process_class is None:
                    eval_class(preds, batch_numpy)
                else:
                    post_result = post_process_class(preds, batch_numpy)
                    eval_class(post_result, batch_numpy)
            elif model_type in ["sr"]:
                eval_class(preds, batch_numpy)
            elif model_type in ["can"]:
                eval_class(preds[0], batch_numpy[2:], epoch_reset=(idx == 0))
            else:
                post_result = post_process_class(preds, batch_numpy[1])
                predicted_data.extend(post_result[0])
                original_data.extend(post_result[1])
                eval_class(post_result, batch_numpy)
    
            pbar.update(1)
            total_frame += len(images)
            sum_images += 1
        # Get final metric,eg. acc or hmean
        metric = eval_class.get_metric()
        data = {"Predict": predicted_data, "Original": original_data}
        df = pd.DataFrame(data)
        df.to_excel("test.xlsx", index=False)
    
    pbar.close()
    model.train()
    metric["fps"] = total_frame / total_time
    return metric
    
    
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
    
    代码解读
在这里插入图片描述

我们的训练经验Tricks

对于意外cases:原始 > 全参数训练 > 冻结

对于已知的的cases:冻结 >= 全参数 > 原始

但是不管怎样,如果不到万不得已,能全参数train就还是全参数

接着

PP-LCNet

Stem Conv

这种技术可被视为一种在行业内被广泛采用的术语(extract\ image\ features),其主要用于提取图像特征(convolution)。其中一种实现方式是通过卷积操作完成图像特征提取(convolutional\ operations),这一过程对于计算机视觉任务具有重要意义(importance

Typically, the stem module refers to an N × N convolutional layer with a stride of two, which reduces both the height and width of an input image by half.

h-swish

该函数swish定义为x \times sigmoid(x), 其受自变量x的影响显著。当x为负值时, 无论sigmoid的结果如何, 结果必然是负数。

sigmoid = 1 / (1 + exp(-x)) : 这货就是 【0,无穷大】

h_swish = x * \frac{ReLU6(x+3)}{6}

RELU 6: f(x) = min(max(0, x), 6);仍然是那种 relu;只是将输出结果限定在 [0-6] 范围内;这种变体非常适合我的需求;计算资源有限的情况下;尤其在精度较低的情况下;可以帮助网络训练保持较好的稳定性

所以总的来说,对于h-swish, 就是将之前的sigmoid 替换成了 Relu

h-sigmoid

虽然嵌套使用min和max函数显得有些复杂;但实际上它能有效地将输入值限制在[0,1]区间内

DepthSepConv

The MobileNetV1 architecture [14] utilizes depthwise and pointwise convolutions in place of standard convolutions, effectively reducing both the number of parameters and FLOPs within the model architecture.

PP-LCNet 的作者表明,该方法仅采用了MobileNetV1这一结构。针对小型模型而言,在添加如连接层等操作的同时,并未显著提升性能表现。尽管在准确率上仍有提升空间,但可能会导致速度受限。

DW(depth-wise convolution) + PW (point-wise convolution)

这张图初次见到令人感到困惑。然而,在讨论DW(Difference Wavenumber)时,则发现其背后的原因或许并非仅在于各通道(如RGB)在卷积核参数设置上存在差异。例如一张RGB图像中R、G、B通道分别采用不同的stride值(如R stride为1、G stride为2等),从而各自进行卷积操作而非一次性地全部处理这确实带来了较高的计算负担。

  • 在各层中设置kernel时并非采取不同的策略;相反,则采用统一的方法,并确保所有层间步长一致(传统方法如DW),否则可能导致计算过程出现问题。
  • 对于每一层都进行卷积操作确实是可行的选择;这不仅能够提高处理效率而且还能有效降低所需资源消耗。

参数量比较

K: 卷积核的宽度和高度
C: 输入通道数
O: 输出通道数

  • 常规卷积: K * K * C * O
  • 深度可分离卷积: (K * K * C) + (C * O) = C*(K*K+O)
复制代码
    #https://www.paepper.com/blog/posts/depthwise-separable-convolutions-in-pytorch/
    from torch.nn import Conv2d
    conv = Conv2d(in_channels=10, out_channels=32, kernel_size=3)
    params = sum(p.numel() for p in conv.parameters() if p.requires_grad)
    x = torch.rand(5, 10, 50, 50)
    out = conv(x)
    depth_conv = Conv2d(in_channels=10, out_channels=10, kernel_size=3, groups=10)
    point_conv = Conv2d(in_channels=10, out_channels=32, kernel_size=1)
    depthwise_separable_conv = torch.nn.Sequential(depth_conv, point_conv)
    params_depthwise = sum(p.numel() for p in depthwise_separable_conv.parameters() if p.requires_grad)
    out_depthwise = depthwise_separable_conv(x)
    print(f"The standard convolution uses {params} parameters.")
    print(f"The depthwise separable convolution uses {params_depthwise} parameters.")
    assert out.shape == out_depthwise.shape, "Size mismatch"
    
    
      
      
      
      
      
      
      
      
      
      
      
      
      
      
    
    代码解读

GAP

Global Average Pooling represents a pooling operation designed to substitute traditional fully connected layers in conventional CNN architectures.
The underlying concept involves generating a distinct feature map for every category associated with the classification task within the final mlpconv layer.
Rather than constructing overlying structures on top of these feature maps, we opt to compute the average value across each feature map, with the resultant vector being directly inputted into the softmax layer.

优点在于降低网络由于复杂度(参数数量多)而导致的过拟合现象,并促使使features与类别产生关联。

在这里插入图片描述
复制代码
    import torch.nn.functional as F
    x = torch.randn(16, 14, 14)
    out = F.max_pool2d(x, kernel_size=input.size()[2:]) 
    out = F.adaptive_max_pool2d(x.unsqueeze(0), output_size=1) #or
    
    
      
      
      
      
    
    代码解读

Paddle V3 怎么支持variable input的?

它的input永远其实都限定在:(48,3,64,320)

SVTR

该模块位于模型头部位置,并且当从PP-LCNet获取特征时将被传递到这里用于CTC编码器与SVRT结合处理

复制代码
    def forward(self, x):
     # for use guide
     if self.use_guide:
         z = x.clone()
         z.stop_gradient = True
     else:
         z = x
     # for short cut
     h = z
     # reduce dim
     z = self.conv1(z)
     z = self.conv2(z)
     # SVTR global block
     B, C, H, W = z.shape
     z = z.flatten(2).transpose([0, 2, 1])
     for blk in self.svtr_block:
         z = blk(z)
     z = self.norm(z)
     # last stage
     z = z.reshape([0, H, W, C]).transpose([0, 3, 1, 2])
     z = self.conv3(z)
     z = paddle.concat((h, z), axis=1)
     z = self.conv1x1(self.conv4(z))
     return z
    
    
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
    
    代码解读

该模块中包含了对QKV Attention机制的处理,在这一操作基础上进行了相应的参数配置:输入通道数为120、输出通道数为360;此外,在该模块中还设置了相应的缩放因子设定为约0.23

复制代码
    def forward(self, x):
        qkv = (
            self.qkv(x) # linear
            .reshape((0, -1, 3, self.num_heads, self.head_dim))
            .transpose((2, 0, 3, 1, 4))
        )
        q, k, v = qkv[0] * self.scale, qkv[1], qkv[2]
    
        attn = q.matmul(k.transpose((0, 1, 3, 2)))
        if self.mixer == "Local":
            attn += self.mask
        attn = nn.functional.softmax(attn, axis=-1)
        attn = self.attn_drop(attn) #DropOut
    
        x = (attn.matmul(v)).transpose((0, 2, 1, 3)).reshape((0, -1, self.dim))
        x = self.proj(x) #linear
        x = self.proj_drop(x) #Dropout
        return x
    
    
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
    
    代码解读

为什么采用Dropout呢? dropout通常是用来随机卸载权重(即重置为零),但经过查阅资料发现是为了防止复杂网络结构过拟合的问题。 事实上,在这个过程中仍然有许多地方采用了 Dropout技术来辅助模型训练与优化。 其中 SVTR 由 CTC 编码器和 CTC 解码器组成,并采用了线性层进行处理。 随后是一个 GTC_head 模块 ,这里直接就是 Transformer 层 。 我的老天啊! 这个设计是否过于复杂呢? 我靠!

Loss

Total loss = CTC loss + NRTRLoss

CTC

以下是用到的CTC,还包括了Focal Loss

复制代码
     if name == "CTCLoss":
     loss = (
         loss_func(predicts["ctc"], batch[:2] + batch[3:])["loss"]
         * self.weight_1
     )
    
    
      
      
      
      
      
    
    代码解读
复制代码
    def forward(self, predicts, batch):
    if isinstance(predicts, (list, tuple)):
        predicts = predicts[-1]
    predicts = predicts.transpose((1, 0, 2))
    N, B, _ = predicts.shape
    preds_lengths = paddle.to_tensor(
        [N] * B, dtype="int64", place=paddle.CPUPlace()
    )
    labels = batch[1].astype("int32")
    label_lengths = batch[2].astype("int64")
    loss = self.loss_func(predicts, labels, preds_lengths, label_lengths)
    if self.use_focal_loss:
        weight = paddle.exp(-loss)
        weight = paddle.subtract(paddle.to_tensor([1.0]), weight)
        weight = paddle.square(weight)
        loss = paddle.multiply(loss, weight)
    loss = loss.mean()
    return {"loss": loss}
    
    
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
    
    代码解读
复制代码
    paddle.nn.functional.ctc_loss(
            log_probs,
            labels,
            input_lengths,
            label_lengths,
            self.blank,
            self.reduction,
            norm_by_times=norm_by_times,
        )
    
    
      
      
      
      
      
      
      
      
      
    
    代码解读

我发现曾设置过focal loss的weight参数为2值 并未取得理想的效果 尽管这一设定并未达到预期效果 该weight值实际上是通过对损失函数进行了指数运算得到的 并且表现出了显著而动态的特点

NRTRLoss

复制代码
    loss = (
      loss_func(predicts["gtc"], batch[:1] + batch[2:])["loss"]
      * self.weight_2
      )
    
    
      
      
      
      
    
    代码解读
复制代码
    def forward(self, pred, batch):
    max_len = batch[2].max()
    tgt = batch[1][:, 1 : 2 + max_len]
    pred = pred.reshape([-1, pred.shape[2]])
    tgt = tgt.reshape([-1])
    if self.smoothing:
        eps = 0.1
        n_class = pred.shape[1]
        one_hot = F.one_hot(tgt, pred.shape[1])
        one_hot = one_hot * (1 - eps) + (1 - one_hot) * eps / (n_class - 1)
        log_prb = F.log_softmax(pred, axis=1)
        non_pad_mask = paddle.not_equal(
            tgt, paddle.zeros(tgt.shape, dtype=tgt.dtype)
        )
        loss = -(one_hot * log_prb).sum(axis=1)
        loss = loss.masked_select(non_pad_mask).mean()
    else:
        loss = self.loss_func(pred, tgt)
    return {"loss": loss}
    
    
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
    
    代码解读

Others

Adam W

Consine learning rate

warm-ups

Torch.gather

复制代码
    input = torch.tensor([[1, 2, 3], [4, 5, 6]])
    index = torch.tensor([[0, 1], [1, 2]])
    output = torch.gather(input, 1, index)
    
    
      
      
      
    
    代码解读

第一行提取第 0 列和第 1 列的元素,得到 [1, 2]
第二行提取第 1 列和第 2 列的元素,得到 [5, 6]

复制代码
    input = torch.tensor([[1, 2, 3], [4, 5, 6]])
    index = torch.tensor([[0, 1], [1, 0]])
    output = torch.gather(input, 0, index)
    
    
      
      
      
    
    代码解读

第一列提取第 0 行和第 1 行的元素,得到 [1, 4]。
第二列提取第 1 行和第 0 行的元素,得到 [5, 2]。

inplace

x.add_(y) 是实现加法操作的一种方式
任何一个带有下划线符号的函数都会直接修改张量的内容而无需复制
使用 x = x + y 并不会是原地操作。而使用 x += y 则会执行原地操作。

但是这个东西不建议用在需要做gradient的地方

静态图

TensorFlow 采用静态计算图(heard that there's some developments, but I'm mainly focused on PyTorch, which is a bit tiring); PyTorch 支持动态计算图; Paddle 两种架构均支持

在这里插入图片描述
请添加图片描述

全部评论 (0)

还没有任何评论哟~