这篇文章主要负责记录自己在转PaddleOCR 模型过程中遇到的问题,以供大家参考。

重要的话说在最前面,以免大家不往下看:

本篇文章是把 “整个” ppocr 模型 转成了 pytorch,不是只转了backbone

本篇文章将分为以下几个部分:

  • 1. PaddleOCR 识别模型的网络结构分析
  • 2.PaddleOCR卷积部分转Pytorch的注意事项
  • 3.PaddleOCR LSTM 部分转Pytorch记录
  • 4.PaddleOCR FC层转 Pytorch 的注意事项

1. PaddleOCR 识别模型的网络结构分析

我用来测试转化脚本的模型为pp ocr 提供的轻量模型

1.1 模型的网络结构

  • Model

    • MobileNet small 50
    • BiLSTM
    • CTC

模型的参数可以通过如下链接提供的函数进行加载:maomaoyuchengzi/paddlepaddle_param_to_pyotrch模型的参数可以通过如下链接提供的函数进行加载:

maomaoyuchengzi/paddlepaddle_param_to_pyotrch​github.com

def _load_state(path):"""记载paddlepaddle的参数:param path::return:"""if os.path.exists(path + '.pdopt'):# XXX another hack to ignore the optimizer statetmp = tempfile.mkdtemp()dst = os.path.join(tmp, os.path.basename(os.path.normpath(path)))shutil.copy(path + '.pdparams', dst + '.pdparams')state = fluid.io.load_program_state(dst)shutil.rmtree(tmp)else:state = fluid.io.load_program_state(path)return state

1.2 模型的参数解析

通过遍历加载的模型的参数,可以发现模型的参数大致分为三个部分:

  • cnn:

    • conv1_xxx
    • ...
    • conv12_xxx
    • conv_last
  • lstm
    • lstm_xxx
  • ctc
    • ctc_fc_xxx

这里有如下几个内容稍微需要注意:

conv 的含义

  • 可以注意到conv层一共有12层(conv1-conv12) ,和一个最后的conv_last,其中:
  • conv1 对应 MobileNet v3 当中的第一个卷积
  • conv2-conv12 对应着 MobileNet v3 当中的11个ResidualUnit
  • conv_last 对应着MobileNet v3 最后的一个卷积

lstm 的含义

  • 注意到 在 ppocr 当中,使用了 paddle 库当中的 dynamic lstm
  • 所以这里看到的 lstm 的参数是无法直接加载到 pytorch 当中的

ctc 部分

  • 这部分就是标准的FC, 所以转换的难度不大

我们基本的转换思路如下:

  1. 首先基于 depp-text-recognition 库搭建出整个网络结构
  2. 其次,将 ppocr 的权重按照名称和 搭建的网络的权重进行对应,使得网络能够加载ppocr 的参数

2.PaddleOCR卷积部分转Pytorch的注意事项

2.1 MobileNetV3 的网络搭建

这部分的工作其实已经有人已经做得很好了:

https://github.com/WenmuZhou/PytorchOCR​github.com

在这个PytorchOCR 的项目里,已经给出了一个能够完全照搬参数的网络结构:

https://github.com/WenmuZhou/PytorchOCR/blob/master/torchocr/networks/backbones/RecMobileNetV3.py​github.com

只需要按照上述的结构进行Backbone 的搭建就好了

2.2 搭建网络中需要注意的点

有一个特别需要注意的点在于 ,paddle paddle 的 HardSigmoid 实现和 其他地方提到的 HardSigmoid 的定义略有不同:

在网上能够查到的实现当中:

但是在 paddle paddle 给出的文档说明中:

看上去没有任何的问题对不对?但是注!意!到!,paddle paddle 当中的这两参数的值设置的是:

对应到代码上需要改写成这样(注意到这里从 F.relu(x+3) / 6 改成如下代码,多加了个1.2):

2.3 参数转换需要注意的点:

没啥说的,就按照字典当中的关系进行对应就好了

这里需要特别注意的一点是,在 pytorch 的MobileNet 的实现当中,ResiduleUnit是包含在若干个Stage 当中的,11个 ResiduleUnit 按照 [3,5,3] 的顺序被分配到了3个stage当中

这里给出一个映射关系的表,供大家进行参数的转换

    state = weightidx = 2bdx = 0key_mapping = OrderedDict()for b in [3, 5, 3]:for i in range(b):key = f'conv{idx}_'value = f'stages.{bdx}.{i}.'key_mapping[key] = valueidx += 1bdx += 1key_mapping.update({"conv1_": "conv1.",'expand_': 'conv0.','depthwise_': 'conv1.','linear_': 'conv2.','weights': 'conv.weight','bn_scale': 'bn.weight','bn_offset': 'bn.bias','bn_mean': 'bn.running_mean','bn_variance': 'bn.running_var','se_1': 'se.conv1','se_2': 'se.conv2','conv1_conv': 'conv1','conv2_conv': 'conv2','conv1_offset': 'conv1.bias','conv2_offset': 'conv2.bias','conv_last_': 'conv2.'})ignored_keys = ['moment', 'pow_acc', 'LR_DECAY_COUNTER', 'learning_rate']

转换完成后,在官方给出的测试图片('doc/imgs_words/ch/word_1.jpg')上

能够看到pytorch backbone 跑出来的误差和 ppocr 的误差在可接受的范围内:

pp_predict, img = run_pp_predict(config, eval_prog, exe, fetch_varname_list) torch_res = torchnet.FeatureExtraction.ConvNet(torch.tensor(img)).detach().cpu().numpy()max_diff = (np.array(pp_predict[2]) - torch_res).max()# 6.2704086e-05

3.PaddleOCR LSTM 部分转Pytorch 的注意事项

上述的代码库当中,在这个部分的实现不是特别的好,主要原因是 PPOCR 当中使用了一个非常难转换的操作 dynamic_lstm ,这个算子使得转换的操作变的及其的困难,在踩过了非常多的坑之后,这里我给出一个基本的解决方案,使得能够将 lstm 操作也搬运过来

3.1 LSTM 操作的细节:

需要搬运这个内容,首先需要对LSTM 这个东西本身具有十分的了解,我们以Pytorch的一个LSTM为例子,介绍LSTM 是如何工作的:

lstm = nn.LSTM(288, 48, num_layers=1, batch_first=True)
for key, value in lstm.state_dict().items():print(key, value.shape)

返回的结果有如下四个:

# 和 input 有关的参数
weight_ih_l0 torch.Size([192, 288])
bias_ih_l0 torch.Size([192])# 和 hidden layer 有关的参数
weight_hh_l0 torch.Size([192, 48])
bias_hh_l0 torch.Size([192])

这里的参数可以分成两组,一组是和 input 相关的,一组是和 hidden layer 相关的

不熟悉参数结构的人可能会有疑问:

明明设置的是 288 -> 48 的参数, 为啥 weight_ih_l0 的维度是 (192, 288) ?

以及不是应该有“四个门”,为什么这里和 x 相关的weight 只有1个?

关于这些问题,都可以参考:

Pytorch源码理解: RNNbase LSTM​blog.csdn.net

简单来说,回顾LSTM 的公式:

按照公式,其实可以将LSTM 的计算流程转化为如下的流程:

  • 首先用一个拼接的weight 和 拼接的 bias 和 x 进行运算(对h 的运算也类似)
  • 其次在需要进行 lstm 运算的地方再将其拆开

上述两个步骤分别对应着下面代码的 第一点和 第二点

了解了 LSTM ,再来看 PPOCR 当中的 的 “双向LSTM”实现:

注意到,在PPOCR 中,“双向LSTM” 是由一个正向的 两层LSTM 和一个反向的两层LSTM 实现的,和Pytorch 当中的 nn.LSTM(bidirectional = True, num_layers = 2) 是不一样的!

PPOcr当中的实现如下:

class EncoderWithRNN(object):def __init__(self, params):super(EncoderWithRNN, self).__init__()self.rnn_hidden_size = params['SeqRNN']['hidden_size']def __call__(self, inputs):lstm_list = []name_prefix = "lstm"rnn_hidden_size = self.rnn_hidden_sizefor no in range(1, 3):if no == 1:is_reverse = Falseelse:is_reverse = Truename = "%s_st1_fc%d" % (name_prefix, no)fc = layers.fc(input=inputs,size=rnn_hidden_size * 4,param_attr=fluid.ParamAttr(name=name + "_w"),bias_attr=fluid.ParamAttr(name=name + "_b"),name=name)name = "%s_st1_out%d" % (name_prefix, no)lstm, _ = layers.dynamic_lstm(input=fc,size=rnn_hidden_size * 4,is_reverse=is_reverse,param_attr=fluid.ParamAttr(name=name + "_w"),bias_attr=fluid.ParamAttr(name=name + "_b"),use_peepholes=False)name = "%s_st2_fc%d" % (name_prefix, no)fc = layers.fc(input=lstm,size=rnn_hidden_size * 4,param_attr=fluid.ParamAttr(name=name + "_w"),bias_attr=fluid.ParamAttr(name=name + "_b"),name=name)name = "%s_st2_out%d" % (name_prefix, no)lstm, _ = layers.dynamic_lstm(input=fc,size=rnn_hidden_size * 4,is_reverse=is_reverse,param_attr=fluid.ParamAttr(name=name + "_w"),bias_attr=fluid.ParamAttr(name=name + "_b"),use_peepholes=False)lstm_list.append(lstm)return lstm_list

可以看到:

  • for 循环执行两次
  • nn.LSTM 在 paddle paddle 当中的 layers.fc + layers.dynamic_lstm 实现
  • fc 对应着上述 所说的fc 操作
  • dynamic_lstm 对应着上述的 lstm 操作
  • 第一次 reversed = False , 执行正向的 LSTM
  • 第二次 reversed = True , 执行反向的LSTM

(paddle paddle 真的是老信条了....)

3.2 PPBiLSTM 的改写:

没啥说的,这里就按照上述的说明,把网络搭建出来:

注意,这里使用了FLIP 来代替 ppocr 当中的reverse

这里为什么不能用 nn.LSTM(bidirectional = True, num_layers = 2) ,感兴趣的同学可以自行尝试将参数的维度打印出来比较一下就知道为什么了

3.3 Paddle Paddle 制造的超级无敌大坑

到这一步,很多人应该是觉得胜利在望了,无非就是把参数像Backbone 一样,转换一下就可以了,因为至少从维度上来说是对的上的,比如上述结构当中,RNN1 的权重为:

而对应的,在 ppocr 的参数中,有维度刚刚好完全对的上的一些参数:

简直是开心的不得了!然而当你心满意足的将这些参数加载进去的时候,你会发现预测的结果“完全不一样!”

很多人到这一步就放弃了,但是不要害怕,既然维度对的上,那么结果就一定对的上!

只需要找到正确的方法!

3.4 Paddle Paddle 的LSTM 解析

基于上述维度对的上的分析,我们很自然的想到问题可能来自于如下的原因:

在pytorch当中:

FC 结果的解析是按照:

InputGate , ForgetGate , CellGate , outGate 的顺序来解析FC 的结果的,那么Paddle Paddle 是按照这个顺序么?“根本不是!”

想了解PP是如何解析的,需要参考如下两个文件:

首先,在如下文件当中定义了拆分FC 的方式:

https://github.com/PaddlePaddle/Paddle/blob/master/paddle/fluid/operators/math/detail/lstm_cpu_kernel.h​github.com

这里gate_value 指向了FC 的结果,可以看到, FC的结果被拆分成了 in , ig ,fg , og 四个内容

根据名字一猜:

  • fg 指的是 ForgetGate
  • og 指的是 OutputGate
  • ig 指的是 InputGate
  • in 指的是 Input
  • 简直开心!

之后再看到如下文件当中,所有的这些变量的使用方法:

https://github.com/PaddlePaddle/Paddle/blob/master/paddle/fluid/operators/math/detail/lstm_kernel.h​github.com

可以看到, ig,fg,og 使用的activation 都是 active_gate ,即 sigmoid , 而 in 使用的激活函数是 active_gate , 是 tanh , 至此,就知道了 ppocr 当中的 lstm 是如何解析 FC 的结果的了

至此,我们发现了Pytorch 和 PaddlePaddle 解析FC 的方式不一样了,所以,我们在将PPOCR 当中的LSTM 参数加载进 Pytorch 前,需要首先置换PPOCR 参数的顺序:

    def igfo_to_gfio(weight):if len(weight.shape) == 2:# 比如 288 , 192# 首先需要 切分成 (288 , 48) 的 4段 ,分别对应 in , ig , fg , ogi_, g_, f_, o_ = np.split(weight, 4, 1)# 其次,按照 ig, fg , in , og 的顺序组成成 pytorch 需要的形式trans_weight = np.concatenate([g_, f_, i_, o_], 1)return torch.tensor(trans_weight)else:i_, g_, f_, o_ = np.split(weight, 4, 0)trans_weight = np.concatenate([g_, f_, i_, o_])return torch.tensor(trans_weight)

然后再进行参数的名称的转换:

    to_load_state_dict = OrderedDict()for i in range(1, 3):to_load_state_dict[f'rnn{i}.weight_ih_l0'] = igfo_to_gfio(weight[f'lstm_st1_fc{i}_w']).Tto_load_state_dict[f'rnn{i}.weight_hh_l0'] = igfo_to_gfio(weight[f'lstm_st1_out{i}_w']).Tto_load_state_dict[f'rnn{i}.bias_ih_l0'] = igfo_to_gfio(weight[f'lstm_st1_fc{i}_b'].reshape(-1))to_load_state_dict[f'rnn{i}.bias_hh_l0'] = igfo_to_gfio(weight[f'lstm_st1_out{i}_b'].reshape(-1))to_load_state_dict[f'rnn{i}.weight_ih_l1'] = igfo_to_gfio(weight[f'lstm_st2_fc{i}_w']).Tto_load_state_dict[f'rnn{i}.weight_hh_l1'] = igfo_to_gfio(weight[f'lstm_st2_out{i}_w']).Tto_load_state_dict[f'rnn{i}.bias_ih_l1'] = igfo_to_gfio(weight[f'lstm_st2_fc{i}_b'].reshape(-1))to_load_state_dict[f'rnn{i}.bias_hh_l1'] = igfo_to_gfio(weight[f'lstm_st2_out{i}_b'].reshape(-1))for key in to_load_state_dict:to_load_state_dict[key] = torch.tensor(to_load_state_dict[key])seq_state = net.SequenceModeling.state_dict()for key, value in to_load_state_dict.items():if key in seq_state:print(key, value.shape, seq_state[key].shape)net.SequenceModeling.load_state_dict(to_load_state_dict)

至此,LSTM 部分的转换就已经完成了,和原始pp 的结果进行 lstm 输出的比较:

# pp_predict 是用paddle paddle 预测的结果
# 第2个是我返回了cnn 部分输出额结果
# 第3个是我返回了rnn 部分的结果
backbone_res = torch.tensor(np.array(pp_predict[2]))
backbone_res = backbone_res.permute(0, 3, 1, 2).squeeze(3)
max_diff = (torchnet.SequenceModeling(backbone_res)[0].detach() - np.array(pp_predict[3])).max()# tensor(2.6822e-07)

4.PaddleOCR FC层转 Pytorch 的注意事项

没啥注意的,前两步能搞定到这里都乐开花了

最后给一个转换后的误差吧

max_diff = ( torchnet(torch.tensor(img)).detach().softmax(-1) -  np.array(pp_predict[1]) ).max()# tensor(3.7893e-07)


码字不易,看paddle 源码更不易,搞定lstm 更更不易,记录在这里,供大家参考和学习,欢迎大家给个赞赏支持和鼓励一下,谢谢!

object怎么转list_PaddleOCR识别模型转Pytorch全流程记录相关推荐

  1. 用自建kinetics-skeleton行为识别数据集训练st-gcn网络流程记录

    用自建kinetics-skeleton行为识别数据集训练st-gcn网络流程记录 0. 准备工作 1. 下载/裁剪视频 2. 利用OpenPose提取骨骼点数据,制作kinetics-skeleto ...

  2. 路面病害检测-从数据清洗到模型部署的全流程方案

    转自AI Studio,原文链接: 路面病害检测-从数据清洗到模型部署的全流程方案 - 飞桨AI Studio 1. 项目说明 无论是水泥还是沥青路面,在通车使用一段时间之后,都会陆续出现各种损坏.变 ...

  3. 【人脸识别】MTCNN + Arcface全流程详解 Pytorch代码 损失函数发展

    目录: 人脸识别介绍 损失函数发展 Softmax loss Center loss Triplet loss L-softmax loss SphereFace(A-Softmax loss) Co ...

  4. 利用python实现深度学习生成对抗样本模型,为任一图片加扰动并恢复原像素的全流程记录

    利用python实现深度学习生成对抗样本,为任一图片加扰动并恢复原像素 一.前言 (一)什么是深度学习 (二)什么是样本模型 (三)什么是对抗样本 1.对抗的目的 2.谁来对抗? 3.对抗的敌人是谁? ...

  5. ARMA模型时间序列分析全流程(附python代码)

    ARMA模型建模流程 建模流程 1)平稳性检验 原始数据data经过清洗得到data_new,然后进行平稳性检验,非平稳数据无法采用ARMA模型进行预测,ADF检验可以用来确定数据的平稳性,这里导入的 ...

  6. 数据挖掘之时间序列模型(最全流程分析)

    时间序列模型 一.获取数据源 二.缺失值处理 三.检验序列的稳定性 四.序列平稳化 五.参数寻优 六.建立模型 七.模型检验 八.模型预测 美股封盘(close)数据 获取数据源->缺失值处理- ...

  7. 【深度学习】深度学习模型训练全流程!

    Datawhale干货 作者:黄星源.奉现,Datawhale优秀学习者 本文从构建数据验证集.模型训练.模型加载和模型调参四个部分对深度学习中模型训练的全流程进行讲解. 一个成熟合格的深度学习训练流 ...

  8. 加载tf模型 正确率很低_深度学习模型训练全流程!

    ↑↑↑关注后"星标"Datawhale 每日干货 & 每月组队学习,不错过 Datawhale干货 作者:黄星源.奉现,Datawhale优秀学习者 本文从构建数据验证集. ...

  9. 深度学习模型训练全流程!

    ↑↑↑关注后"星标"Datawhale 每日干货 & 每月组队学习,不错过 Datawhale干货 作者:黄星源.奉现,Datawhale优秀学习者 本文从构建数据验证集. ...

最新文章

  1. 【中文】Joomla1.7扩展介绍之Fabrik (强大的表单处理能力)
  2. 移动端页面自适应解决方案—rem布局
  3. ppt的一些基础操作
  4. macbook服务器文件,使用MacBook生成服务器使用的p12证书文件
  5. 刀剑无双服务器显示404,刀剑无双如何开启GM命令 刀剑无双GM指令修改
  6. 数据类型和Json格式
  7. 关于类的入门例子(1): 继承窗体
  8. android之camera2预览
  9. Fish for mac安装 fish+on my fish ---(powerline主题)美化
  10. Bat批处理命令大全
  11. scratch双语教师课件文档手册 2.scratch模块介绍
  12. springboot后端数据校验以及异常处理
  13. 微信公众号 - Java推送公众号模板消息给用户
  14. 从 ES6 到 ES10 的新特性万字大总结
  15. RSD和wlwmanifest是什么
  16. Anemometer让慢查询可视化
  17. 2022年值得关注的5个区块链项目 数字藏品平台开发搭建
  18. 什么是Spring IoC容器?
  19. ieda ts文件报错_Intellij IDEA就这样配置,快到飞起!
  20. Js逆向-猿人学(1)源码混淆

热门文章

  1. 安卓application_安卓系统蓝牙配对流程分析
  2. qt lineedit获取内容_Qt开发中的几个关键知识点,收藏以备参考
  3. 外贸建站前必做的SEO优化?
  4. mysql语句6_MySQL的SQL语句 - 数据操作语句(6)- INSERT 语句
  5. Leetcode每日一题:147.insertion-sort-list(对链表进行插入排序)
  6. CS230+deeplearning.ai专项课程笔记及作业目录
  7. centos6.8安装xfce+vnc
  8. linux脚本程序是什么意思,什么是shell脚本编程?
  9. linux能运行安卓模拟器吗,Ubuntu 14.04中使用模拟器运行Android系统
  10. Echarts.js+jquery.js+china.js实现中国疫情地图