森说AI:用Paddle2.x完成resnet50模型转写并实现前向对齐

Posted 神佑我调参侠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了森说AI:用Paddle2.x完成resnet50模型转写并实现前向对齐相关的知识,希望对你有一定的参考价值。


我的ai studio项目链接:https://aistudio.baidu.com/aistudio/projectdetail/2284432

前言

上篇文章我们在复现论文课程的时候呢学习了ResNet和卷积神经网络的基础,但是后面对应torch论文的时候就很费劲,所以这篇文章我们来继续搞

项目简介

这个项目以前向对齐resnet为例,详细讲解(一行行代码去理解)如何去理解torch代码,并且如何获得权重等,并且将resnet的torch代码通过API对照表转化成Paddle代码,并且最后比较两者输出。

从中你可以学到:需要哪些环境,通过一个案例去体会前向对齐,更加熟练Paddle的使用等!

效果展示

环境安装

今天我的电脑重装系统了,然后啥也没有了都得重新装,我先得下载pycharm,然后安装个解释器环境去!这里就不说怎么进行安装了,因为网上的教程太多了。这里我附上百度网盘的资源,需要的可以去取!

pycharm安装

我安装的(网盘里那个)这个直接就是破解版的了,很方便,不用破解了

现在的话pycharm安装完毕,下一步去安装解释器。

anaconda安装

这个我是在清华镜像源安装的

上面那两个我安了很多遍了,就不细讲了,然后将上面两个必要的安装好了我们就可以来安装飞桨了,先安飞桨然后在安torch

paddlepaddle安装


在安装之前要新建一个虚拟环境,这里我是直接用anaconda创建的,当然也可以用命令行创建。

conda install paddlepaddle --channel https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/Paddle/

然后这里我们已经安装完毕了,下一步安装torch

torch安装

python3 -m pip install torch torchvision -i https://pypi.tuna.tsinghua.edu.cn/simple

然后这里我们已经安装完毕了,看看最后的效果图:

这里我们先说一下思路,就是先导入预训练模型,然后导出权重参数和npy输出,然后再将torch的代码对应的改写成paddle的代码,导入权重参数,输出npy文件,最后对比着两个npy文件即可

使用resnet_torch.py导出预训练模型权重参数文件并输出结果npy文件

这里说下,我们的代码要在本地运行,所以一定要保证环境正确
参考:https://aistudio.baidu.com/aistudio/projectdetail/2288176?channelType=0&channel=0
然后也在ai studio上放上了代码,运行不了,因为上面无法运行torch代码,详见resnet50_torch.py,我也是会照着这个代码说的。

torchvision安装

这里我们先要去安装torch的视觉包,因为我们安装torch的时候没有直接将它也安装了。安装也很简单,如果你的电脑中有了,那么直接跳过

pip install torchvision

resnet50_torch.py代码讲解

这个文件也在项目中也有,作用是将torch的字典转化成paddle的字典名称,阅读时需要注意fc层有一个转置,因为paddle与torch的不同,所以要单独处理,其余就是对字符串的处理等等,详见下文:

我们先导入预训练模型,然后开启预测模式(为了后面导出权重做准备)

model = torchvision.models.resnet50(pretrained=True)
model.eval()


然后看这些代码,这里是先定义了一个空列表,然后通过named_modules函数(获取网络结构)遍历所有的层,如果是线性层的话就会添加到这个列表中,还有这个为什么要加上“.weight”因为后面模型字典参数就有,为后续做准备
参考:https://blog.csdn.net/watermelon1123/article/details/98036360

named_modules()函数


这里我先在本地跑一下看一下效果:

print(model.named_modules())


可以看到这里面返回的是一个生成器对象,那么我遍历一下看看:

我将结果生成了一个txt文件,可以在项目中直接查看,从中我们可以看到通过这个函数我们可以知道模型的结构和每层的各种参数。

isinstance函数

然后我们看下这个函数,这个函数和type很像,也是返回类型,但是type不会考虑继承的问题,所以如果涉及到类的判断的话,这里就要去使用这个函数。

然后这里我们先看下上面那几行代码的效果,就是打印了一下那个列表

可以看到这里面就是输出了一个全连接层是因为resnet50在最后有一个fc层,这里单独提取fc层是为面做准备,那么我们先来回顾一下fc层

全连接层(fc)

如果说卷积,池化,激活函数是为了去得到特征的话,那么全连接层就是对这些特征进行了整合。也就是说卷积后的得到的是特征,然后fc可以对这些特征进行整合,必然说对于分类,这里面就可以用fc去生成概率。

model.state_dict()

这个函数是可以去获得模型的状态字典,这个字典是在定义后模型后自动生成的,然后这里我们打印一下输出,看看效果

可以看到包括了每个层的一些信息,这是我们再来看下有哪些信息?

print(model_state_dict.keys())

接下来我们就要来看一下最重要的函数了:

convert_param_dict(model_dict, trans_weights)

我们先看下面这几行函数:

在上面我们已经得到了trans_weights为fc.weight,这里先定义一个空字典,然后我们遍历字典参数的键值对(item是获得键值对),然后到fc层的时候将值(dict(key)就是取值)进行转化成numpy的格式并且进行转置(因为torch和Paddle的思想不同,所以这里我们向Paddle对齐)。

因为需要转置,这也是为什么在前面要将fc层单独提取并且处理的原因

接下来的代码可能看起来很繁琐,但是看起来像个模板似的,以后如果你想转其他模型的话,也可以直接使用。首先我们要知道为什么要使用这个,为什么不能直接将模型保存呢?

重点:torch与Paddle中层的参数的状态字典的命名有些许不同,所以要改下

那么这里我们可以用来看看Paddle的状态字典是什么样的?

然后我们再来看下torch中的状态字典:

从中我们可以看到状态参数的命名是不一样的,所以这里我们要是将torch中预训练模型的权重参数使用到Paddle中,这里我们必须进行转化,也就是上面那些代码。这些代码这里就不细讲了,就是对于字符串的操作,如果你想去使用也可以不用造轮子们可以直接套用

保存参数字典


这里是使用了一个pickle模块,这里学一下这个模块吧

pickle模块

pickle提供了一个简单的持久化功能。可以将对象以文件的形式存放在磁盘上。

pickle模块只能在python中使用,python中几乎所有的数据类型(列表,字典,集合,类等)都可以用pickle来序列化,

保存输出


这里我们先设置一个输入,然后投喂到模型中,最后保存输出

最后得到这两个文件

到这里这个部分就结束了,下个部分开始:

对照torch代码转换成paddle代码

API对应表

这是我们就不得不得知道如何将torch代码转换成paddle的代码,其实是有一个API,下面是链接:
https://www.paddlepaddle.org.cn/documentation/docs/zh/guides/08_api_mapping/pytorch_api_mapping_cn.html#x2paddle

库的导入

第一步肯定导入对应的库呀,我们先看一下torch中的前三行代码:

import torch
from torch import Tensor
import torch.nn as nn

然后我们对照表开始转化:第一行肯定都知道吧

import paddle

然后第二行:

from paddle import to_tensor

第三行可以直接转,因为在paddle中也是用的nn

import paddle.nn as nn

接下来的两行直接复制,因为没有涉及到转换,但是既然我们看到了还是去看一下代码写的什么吧?

import paddle
from paddle import to_tensor
import paddle.nn as nn
from typing import Type, Any, Callable, Union, List, Optional

代码对应改写(这篇文章只是会说如何转,不会从零去搭建网络的)

文件分别是resnet.py和resnet_paddle.py,在ai studio项目上也有的,阅读的时候对照resnet.py去转(这里面只做了resnet50的)。

对于下面两个函数就没有必要改写了,因为在Paddle中有对应的API

BasicBlock类的改写

首先我们先改写这里面nn.Module,torch与paddle在这点很像都是通过搭建层去构造网络,但是这里的名字不一样,是你nn.layrer,并且int这里我们可以省略

class BasicBlock(nn.Layer):
    expansion = 1

然后是初始化,这个几乎没有变化:

def __init__(self,
             inplanes,
             planes,
             stride=1,
             downsample=None,
             groups=1,
             base_width=64,
             dilation=1,
             norm_layer=None):
    super(BasicBlock, self).__init__()


对于这些代码,这里只改写一个API即可:看下这个对应

所以这里就将d改成D即可

        if norm_layer is None:
            norm_layer = nn.BatchNorm2D
        if groups != 1 or base_width != 64:
            raise ValueError('BasicBlock only supports groups=1 and base_width=64')
        if dilation > 1:
            raise NotImplementedError(
                "Dilation > 1 not supported in BasicBlock")

然后就是对这个类的属性进行改写

这个的话完全就是API的转化了,先是卷积层,在Paddle中卷积层是用conv2D这个函数,然后这里面我们要去参照前面的conv3x3的函数进行改写,当然也可以和resnet的写法一样,下一个类我再用那种方法来一遍!


之后的API对应:
这里注意一下,在Paddle中Relu这个API没有inplace这个参数。

 self.conv1 = nn.Conv2D(
     inplanes, planes, 3, padding=dilation, stride=stride, bias_attr=False)
 self.bn1 = norm_layer(planes)
 self.relu = nn.ReLU()
 self.conv2 = nn.Conv2D(planes, planes, 3, padding=dilation, bias_attr=False)
 self.bn2 = norm_layer(planes)
 self.downsample = downsample
 self.stride = stride

然后我们的前向传播函数直接复制过来,因为没有涉及到的API已经转好了

    def forward(self, x):
        identity = x

        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)
        out = self.conv2(out)
        out = self.bn2(out)
        
        if self.downsample is not None:
            identity = self.downsample(x)
        out += identity
        out = self.relu(out)
        return out

Bottleneck与ResNet类的改写

这些的话跟上面的方法一样也没啥说的,写法几乎没有改变,然后我分享一个在本地改写的小技巧:

可以看到上图中的出现一个绿色的框的就是API对不上,直接进行更改即可,是不是很方便呢?更改好的代码我会放到最后也会放在对应的py文件中,这里可以自己去练习一下,真的不是很难。就参照我上面改API的方法来改写即可。

说一下几个需要注意的点,就是_resnet这个函数中将if去掉了,因为我们没有从网上下载预训练模型,然后就是将变量后面的声明都给去掉了,还有就是将下面这些也直接去掉了,原因是我没有找到对应的API,但是最后发现去掉并没有造成影响。

删掉的代码

生成对应的npy文件

if __name__ == "__main__":
    dummy_input = [paddle.ones(shape=[3, 224, 224])]
    model = resnet50()

    with open('torch_resnet50.pkl', 'rb') as f:
        param2 = pickle.load(f)
    model.set_state_dict(param2)
    model.eval()

    output = model(paddle.to_tensor(dummy_input))
    np.save('paddle_resnet50.npy', output.numpy())

对比

import numpy as np

paddle_output=np.load('paddle_resnet50.npy')
torch_output=np.load('torch_resnet50.npy')
print(np.allclose(paddle_output,  torch_output, atol=1e-5))

最后就是对比了,加载最两个npy文件,然后使用一个np.allclose函数进行对比,这个函数比较两个array是不是每一元素都相等,默认在1e-05的误差范围内,返回True就说明我们成功了

总结

做完在回想这个项目其实要做的事还是很少的,代码转写现在一想还是很简单的,但是因为我的基础很差,这里补了很多的基础,然后也是了解到了torch和paddle的一些不同,比如转置和状态字典等的不同,了解到了resnet的网络结构,知道了残差网络是怎么回事,明白前向对齐是怎么回事等等,但是这只是论文复现上的第一步,还是要继续加油。

以上是关于森说AI:用Paddle2.x完成resnet50模型转写并实现前向对齐的主要内容,如果未能解决你的问题,请参考以下文章

森说AI:用Paddle2.x完成resnet50模型转写并实现前向对齐

森说AI:用Paddle2.x完成resnet50模型转写并实现前向对齐

森说AI:AI创造营大作业--应用paddlex完成对自定义车道路的语义分割

森说AI:AI创造营大作业--应用paddlex完成对自定义车道路的语义分割

森说AI:从零开始应用paddlehub转换手写数字识别模型并完成部署:使用paddle2.xAPI简易实现手写数字识别模型

森说AI:从零开始应用paddlehub转换手写数字识别模型并完成部署:使用paddle2.xAPI简易实现手写数字识别模型