锐单电子商城 , 一站式电子元器件采购平台!
  • 电话:400-990-0325

AI部署:聊一聊深度学习中的模型权重

时间:2023-12-06 01:37:01 传感器1130

点击上方“3D视觉车间,选择星标

第一时间送达干货

作者丨Oldpan

来源丨Oldpan博客

极市平台编辑

导读

本文简要介绍了模型权重的统计方法和caffe,pytorch,TensorRT如何转移权重,附有相关代码。

今天简单说聊模型权重,俗称我们weight

通过深度学习,我们一直在训练模型反向传播求导更新模型的权重最终得到一个泛化能力强模型。同样,如果我们不训练,只是随机初始化权重,我们也可以得到相同大小的模型。虽然两者的大小相同,但权重信息的分布将非常不同。一个大脑充满了知识,一个大脑充满了水,这几乎意味着。

所谓的AI说白了,模型部署阶段就是把训练好的权重移到另一个地方跑。一般来说,权重信息和权重分布不会改变(精度可能会改变,部分权重可能会合并)。

但执行模型操作(卷积、全连接、反卷积)的算子可能会发生变化Pytorch->TensorRT或者TensorFlow->TFLITE,也就是说,实现算子的方式发生了变化,在同一卷积操作中Pytorch在框架中是一种实现TensorRT另一种时间,两者的基本原理是一样的,但精度和速度不同,TensorRT可以借助Pytorch训练好的卷积权重,实现和Pytorch操作相同,但可能会更快。

权重/Weight/CheckPoint

那么权重是什么呢?他们长什么样?

这还真不好描述...其实是一堆数据。是的,我们千辛万苦不断调优训练的重量只是一堆数据。也就是说,这个神奇的数据,结合各种神经网络算子,可以实现各种检测、分类和识别任务。

比如上图,我们用Netron检查某个工具ONNX模型的第一个卷积权重。很显然这个卷积只有一个W权重,没有偏置b。卷积权重值的维度是[64,3,7,7],即输入通道3,输出通道64,卷积核大小7x7

仔细看,其实这个权重的值范围还是很不一样的,最大的是0.1级。但是最小的,肉眼看(其实应该算一波),最小的有1e-10级别。

一般来说,当我们训练时,输入权重是0-1,当然也有0-255但无论是0-1还是0-255,只要精度上限和下限不溢出,都没有问题。FP32来说,1e-10是小case,但是对于FP不一定是16。

我们知道FP一般精度为16~5.96e?8 (6.10e?5) … 65504,更不用说具体的精度细节了,但可以清楚地看到上述细节1e-10精度已经溢出FP16精度下限。如果模型中的权重分布大多处于溢出边缘,然后转换模型FP16精度的模型指标可能会大大下降。

除了FP16,当然还有很多其他的精度(TF32、BF16、IN这里暂且不谈,然而,有一篇文章讨论了各种精度:https://moocaholic.medium.com/fp64-fp32-fp16-bfloat16-tf32-and-other-members-of-the-zoo-a1ca7897d407

换句话说,我们应该如何统计这一层的权重信息?Pytorch可以实现中原生代码:

#假设v是某一层conv的权重,我们可以简单通过以下命令查看到该权重的分布 v.max() tensor(0.8559) v.min() tensor(-0.9568) v.abs() tensor([[0.0314,0.0045,0.0182,...,0.0309,0.0204,0.0345], [0.0295,0.0486,0.0746,...,0.0363,0.0262,0.0108], [0.0328,0.0582,0.0149,...,0.0932,0.0444,0.0221], ..., [0.0337,0.0518,0.0280,...,0.0174,0.0078,0.0010], [0.0022,0.0297,0.0167,...,0.0472,0.0006,0.0128], [0.0631,0.0144,0.0232,...,0.0072,0.0704,0.0479]]) v.abs().min()#权重绝对值的最小值为1e-10级别 tensor(2.0123e-10) v.abs().max() tensor(0.9568) torch.histc(v.abs())#统计权重分布,最小最大分为100份[-0.9558,0.8559] tensor([3.3473e 06,3.2437e 06,3.0395e 06,2.7606e 06,2.4251e 06,2.0610e 06, 1.6921e 06,1.3480e 06,1.0352e 06,7.7072e 05,5.5376e 05,3.8780e 05, 2.6351e 05,1.7617e 05,1.1414e 05,7.3327e 04,4.7053e 04,3.0016e 04, 1.9576e 04,1.3106e 04,9.1220e 03,6.4780e 03,4.6940e 03,3.5140e 03, 2.8330e 03,2.2040e 03,1.7220e 03,1.4020e 03,1.1130e 03,1.0200e 03, 8.2400e 02,7.0600e 02,5.7900e 02,4.6400e 02,4.1600e 02,3.3400e 02, 3.0700e 02,2.4100e 02,2.3200e 02,1.9000e 02,1.5600e 02,1.1900e 02, 1.0800e 02,9.9000e 01,6.9000e 01,5.2000e 01,4.9000e 01,2.2000e 01, 1.8000e 01,2.8000e 01,1.2000e 01,1.3000e 01,8.0000e 00,3.0000e 00, 4.0000e 00,3.0000e 00,1.0000e 00,1.0000e 00,0.0000e 00,1.0000e 00, 1.0000e 00,0.0000e 00,0.0000e 00,0.0000e 00,0.0000e 00,0.0000e 00, &nbp;      1.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00, 2.0000e+00,
        0.0000e+00, 2.0000e+00, 1.0000e+00, 0.0000e+00, 1.0000e+00, 0.0000e+00,
        2.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00,
        0.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00, 1.0000e+00,
        0.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00,
        0.0000e+00, 0.0000e+00, 0.0000e+00, 1.0000e+00])

这样看如果觉着不是很直观,那么也可以自己画图或者通过Tensorboard来时候看。

img

那么看权重分布有什么用呢?

肯定是有用处的,训练和部署的时候权重分布可以作为模型是否正常,精度是否保持的一个重要信息。不过这里先不展开说了。

有权重,所以重点关照

在模型训练过程中,有很多需要通过反向传播更新的权重,常见的有:

  • 卷积层

  • 全连接层

  • 批处理化层(BN层、或者各种其他LN、IN、GN)

  • transformer-encoder层

  • DCN层

这些层一般都是神经网络的核心部分,当然都是有参数的,一定会参与模型的反向传播更新,是我们在训练模型时候需要注意的重要参数。

# Pytorch中conv层的部分代码,可以看到参数的维度等信息
self._reversed_padding_repeated_twice = _reverse_repeat_tuple(self.padding, 2)
if transposed:
    self.weight = Parameter(torch.Tensor(
        in_channels, out_channels // groups, *kernel_size))
else:
    self.weight = Parameter(torch.Tensor(
        out_channels, in_channels // groups, *kernel_size))
if bias:
    self.bias = Parameter(torch.Tensor(out_channels))

也有不参与反向传播,但也会随着训练一起更新的参数。比较常见的就是BN层中的running_meanrunning_std

# 截取了Pytorch中BN层的部分代码
def __init__(
    self,
    num_features: int,
    eps: float = 1e-5,
    momentum: float = 0.1,
    affine: bool = True,
    track_running_stats: bool = True
) -> None:
    super(_NormBase, self).__init__()
    self.num_features = num_features
    self.eps = eps
    self.momentum = momentum
    self.affine = affine
    self.track_running_stats = track_running_stats
    if self.affine:
        self.weight = Parameter(torch.Tensor(num_features))
        self.bias = Parameter(torch.Tensor(num_features))
    else:
        self.register_parameter('weight', None)
        self.register_parameter('bias', None)
    if self.track_running_stats:
        # 可以看到在使用track_running_stats时,BN层会更新这三个参数
        self.register_buffer('running_mean', torch.zeros(num_features))
        self.register_buffer('running_var', torch.ones(num_features))
        self.register_buffer('num_batches_tracked', torch.tensor(0, dtype=torch.long))
    else:
        self.register_parameter('running_mean', None)
        self.register_parameter('running_var', None)
        self.register_parameter('num_batches_tracked', None)
    self.reset_parameters()

可以看到上述代码的注册区别,对于BN层中的权重和偏置使用的是register_parameter,而对于running_meanrunning_var则使用register_buffer,那么这两者有什么区别呢,那就是注册为buffer的参数往往不会参与反向传播的计算,但仍然会在模型训练的时候更新,所以也需要认真对待。

关于BN层,转换模型和训练模型的时候会有暗坑,需要注意一下。

刚才描述的这些层都是有参数的,那么还有一些没有参数的层有哪些呢?当然有,我们的网络中其实有很多op,仅仅是做一些维度变换、索引取值或者上/下采样的操作,例如:

  • Reshape

  • Squeeze

  • Unsqueeze

  • Split

  • Transpose

  • Gather

等等等等,这些操作没有参数仅仅是对上一层传递过来的张量进行维度变换,用于实现一些”炫技“的操作。至于这些炫技吗,有些很有用有些就有些无聊了。

上图这一堆乱七八槽的op,如果单独拆出来都认识,但是如果都连起来(像上图这样),估计连它爸都不认识了。

开个玩笑,其实有时候在通过Pytorch转换为ONNX的时候,偶尔会发生一些转换诡异的情况。比如一个简单的reshape会四分五裂为gather+slip+concat,这种操作相当于复杂化了,不过一般来说这种情况可以使用ONNX-SIMPLIFY去优化掉,当然遇到较为复杂的就需要自行优化了。

哦对了,对于这些变形类的操作算子,其实有些是有参数的,例如下图的reshap:

像这种的op,怎么说呢,有时候会比较棘手。 如果我们想要将这个ONNX模型转换为TensorRT,那么100%会遇到问题,因为TensorRT的解释器在解析ONNX的时候,不支持reshape层的shape是输入TensorRT,而是把这个shape当成 attribute 来处理,而ONNX的推理框架Inference则是支持的。

不过这些都是小问题,大部分情况我们可以通过改模型或者换结构解决,而且成本也不高。但是还会有一些其他复杂的问题,可能就需要我们重点研究下了。

提取权重

想要将训练好的模型从这个平台部署至另一个平台,那么首要的就是转移权重。不过实际中大部分的转换器都帮我们做好了(比如onnx-TensorRT),不用我们自己操心!

onnx-TensorRT:https://github.com/onnx/onnx-tensorrt

不过如果想要对模型权重的有个整体认知的话,还是建议自己亲手试一试。

Caffe2Pytorch

先简单说下Caffe和Pytorch之间的权重转换。这里推荐一个开源仓库Caffe-python(https://github.com/marvis/pytorch-caffe),已经帮我们写好了提取Caffemodel权重和根据prototxt构建对应Pytorch模型结构的过程,不需要我们重复造轮子。

我们都知道Caffe的权重使用 Caffemodel 表示,而相应的结构是 prototxt 。 如上图,左面是 prototxt 右面是 caffemodel ,而caffemodel使用的是protobuf这个数据结构表示的。 我们当然也要先读出来:
model = caffe_pb2.NetParameter()
print('Loading caffemodel: ' + caffemodel)
with open(caffemodel, 'rb') as fp:
    model.ParseFromString(fp.read())

caffe_pb2就是caffemodel格式的protobuf结构,具体的可以看上方老潘提供的库,总之就是定义了一些Caffe模型的结构。

而提取到模型权重后,通过prototxt中的模型信息,挨个从caffemodel的protobuf权重中找,然后复制权重到Pytorch端,仔细看这句caffe_weight = torch.from_numpy(caffe_weight).view_as(self.models[lname].weight),其中self.models[lname]就是已经搭建好的对应Pytorch的卷积层,这里取weight之后通过self.models[lname].weight.data.copy_(caffe_weight)将caffe的权重放到Pytorch中。

很简单吧。

if ltype in ['Convolution', 'Deconvolution']:
    print('load weights %s' % lname)
    convolution_param = layer['convolution_param']
    bias = True
    if 'bias_term' in convolution_param and convolution_param['bias_term'] == 'false':
        bias = False
    # weight_blob = lmap[lname].blobs[0]
    # print('caffe weight shape', weight_blob.num, weight_blob.channels, weight_blob.height, weight_blob.width)
    caffe_weight = np.array(lmap[lname].blobs[0].data)
    caffe_weight = torch.from_numpy(caffe_weight).view_as(self.models[lname].weight)
    # print("caffe_weight", caffe_weight.view(1,-1)[0][0:10])
    self.models[lname].weight.data.copy_(caffe_weight)
    if bias and len(lmap[lname].blobs) > 1:
        self.models[lname].bias.data.copy_(torch.from_numpy(np.array(lmap[lname].blobs[1].data)))
        print("convlution %s has bias" % lname)

Pytorch2TensorRT

先举个简单的例子,一般我们使用Pytorch模型进行训练。训练得到的权重,我们一般都会使用torch.save()保存为.pth的格式。

PTH是Pytorch使用python中内置模块pickle来保存和读取,我们使用netron看一下pth长什么样。。

可以看到只有模型中有参数权重的表示,并不包含模型结构。不过我们可以通过.py的模型结构一一加载.pth的权重到我们模型中即可。

img

看一下我们读取.pth后,state_dictkey。这些key也就对应着我们在构建模型时候注册每一层的权重名称和权重信息(也包括维度和类型等)。

当然这个pth也可以包含其他字符段{'epoch': 190, 'state_dict': OrderedDict([('conv1.weight', tensor([[...,比如训练到多少个epoch,学习率啥的。

对于pth,我们可以通过以下代码将其提取出来,存放为TensorRT的权重格式。

def extract_weight(args):
    # Load model
    state_dict = torch.load(args.weight)
    with open(args.save_path, "w") as f:
        f.write("{}\n".format(len(state_dict.keys())))
        for k, v in state_dict.items():
            vr = v.reshape(-1).cpu().numpy()
            f.write("{} {} ".format(k, len(vr)))
            for vv in vr:
                f.write(" ")
                f.write(struct.pack(">f", float(vv)).hex())
            f.write("\n")

需要注意,这里的TensorRT权重格式指的是在build之前的权重,TensorRT仅仅是拿来去构建整个网络,将每个解析到的层的权重传递进去,然后通过TensorRT的network去build好engine

// Load weights from files shared with TensorRT samples.
// TensorRT weight files have a simple space delimited format:
// [type] [size] 
std::map loadWeights(const std::string file)
{
    std::cout << "Loading weights: " << file << std::endl;
    std::map weightMap;

    // Open weights file
    std::ifstream input(file);
    assert(input.is_open() && "Unable to load weight file.");

    // Read number of weight blobs
    int32_t count;
    input >> count;
    assert(count > 0 && "Invalid weight map file.");

    while (count--)
    {
        Weights wt{DataType::kFLOAT, nullptr, 0};
        uint32_t size;

        // Read name and type of blob
        std::string name;
        input >> name >> std::dec >> size;
        wt.type = DataType::kFLOAT;

        // Load blob
        uint32_t *val = reinterpret_cast(malloc(sizeof(val) * size));
        for (uint32_t x = 0, y = size; x < y; ++x)
        {
            input >> std::hex >> val[x];
        }
        wt.values = val;
        wt.count = size;
        weightMap[name] = wt;
    }
    std::cout << "Finished Load weights: " << file << std::endl;
    return weightMap;
}

那么被TensorRT优化后?模型又长什么样子呢?我们的权重放哪儿了呢?

肯定在build好后的engine里头,不过这些权重因为TensorRT的优化,可能已经被合并/移除/merge了。

模型参数的学问还是很多,近期也有很多相关的研究,比如参数重参化,是相当solid的工作,在很多训练和部署场景中经常会用到。

后记

先说这些吧,比较基础,也偏向于底层些。神经网络虽然一直被认为是黑盒,那是因为没有确定的理论证明。但是训练好的模型权重我们是可以看到的,模型的基本结构我们也是可以知道的,虽然无法证明模型为什么起作用?为什么work?但通过结构和权重分布这些先验知识,我们也可以大概地对模型进行了解,也更好地进行部署。

至于神经网络的可解释性,这就有点玄学了,我不清楚这里也就不多说了~

本文仅做学术分享,如有侵权,请联系删文。

下载1

在「3D视觉工坊」公众号后台回复:3D视觉即可下载 3D视觉相关资料干货,涉及相机标定、三维重建、立体视觉、SLAM、深度学习、点云后处理、多视图几何等方向。

下载2

在「3D视觉工坊」公众号后台回复:3D视觉github资源汇总即可下载包括结构光、标定源码、缺陷检测源码、深度估计与深度补全源码、点云处理相关源码、立体匹配源码、单目、双目3D检测、基于点云的3D检测、6D姿态估计源码汇总等。

下载3

在「3D视觉工坊」公众号后台回复:相机标定即可下载独家相机标定学习课件与视频网址;后台回复:立体匹配即可下载独家立体匹配学习课件与视频网址。

重磅!3DCVer-学术论文写作投稿 交流群已成立

扫码添加小助手微信,可申请加入3D视觉工坊-学术论文写作与投稿 微信交流群,旨在交流顶会、顶刊、SCI、EI等写作与投稿事宜。

同时也可申请加入我们的细分方向交流群,目前主要有3D视觉CV&深度学习SLAM三维重建点云后处理自动驾驶、多传感器融合、CV入门、三维测量、VR/AR、3D人脸识别、医疗影像、缺陷检测、行人重识别、目标跟踪、视觉产品落地、视觉竞赛、车牌识别、硬件选型、学术交流、求职交流、ORB-SLAM系列源码交流、深度估计等微信群。

一定要备注:研究方向+学校/公司+昵称,例如:”3D视觉 + 上海交大 + 静静“。请按照格式备注,可快速被通过且邀请进群。原创投稿也请联系。

▲长按加微信群或投稿

▲长按关注公众号

3D视觉从入门到精通知识星球:针对3D视觉领域的视频课程(三维重建系列、三维点云系列、结构光系列、手眼标定、相机标定、orb-slam3等视频课程)、知识点汇总、入门进阶学习路线、最新paper分享、疑问解答五个方面进行深耕,更有各类大厂的算法工程人员进行技术指导。与此同时,星球将联合知名企业发布3D视觉相关算法开发岗位以及项目对接信息,打造成集技术与就业为一体的铁杆粉丝聚集区,近2000星球成员为创造更好的AI世界共同进步,知识星球入口:

学习3D视觉核心技术,扫描查看介绍,3天内无条件退款

 圈里有高质量教程资料、答疑解惑、助你高效解决问题

觉得有用,麻烦给个赞和在看~  

锐单商城拥有海量元器件数据手册IC替代型号,打造电子元器件IC百科大全!

相关文章