当前位置:   article > 正文

AI部署系列:模型权重的小秘密

因为tensorrt的解释器在解析onnx的时候,不支持reshape层的shape是输入tensorrt,而

点击下方卡片,关注“自动驾驶之心”公众号

ADAS巨卷干货,即可获取

点击进入→自动驾驶之心技术交流群

0807dd972fcd77dacf2598e14b2d28f0.gif

导读

网络训练时,通过大量数据可以训练出一个网络权重模型,但是模型权重为什么有用?内部的原理是什么?本文从模型部署的角度进行了分析,希望对大家有帮助!

今天简单聊聊模型权重,也就是我们俗称的weight

深度学习中,我们一直在训练模型,通过反向传播求导更新模型的权重,最终得到一个泛化能力比较强的模型。同样,如果我们不训练,仅仅随机初始化权重,同样能够得到一个同样大小的模型。虽然两者大小一样,不过两者其中的权重信息分布相差会很大,一个脑子装满了知识、一个脑子都是水,差不多就这个意思。

6c81eee83386ca17abb699cc2d030bf6.gif

所谓的AI模型部署阶段,说白了就是将训练好的权重挪到另一个地方去跑。一般来说,权重信息以及权重分布基本不会变(可能会改变精度、也可能会合并一些权重)。

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

权重/Weight/CheckPoint

那么权重都有哪些呢?他们长什么样?

这还真不好描述...其实就是一堆数据。对的,我们千辛万苦不断调优训练出来的权重,就是一堆数据而已。也就是这个神奇的数据,搭配各种神经网络的算子,就可以实现各种检测、分类、识别的任务。

d9536f1696c696eaa8837c5121872c16.png

Conv模型权重

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

再仔细看,其实这个权重的数值范围相差还是很大,最大的也就0.1的级别。但是最小的呢,肉眼看了下(其实应该统计一波),最小的竟然有1e-10级别。

9deca5956500c14b45ab46036f1e6838.png

部分模型权重值极小

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

我们知道FP16的普遍精度是~5.96e−8 (6.10e−5) … 65504,具体的精度细节先不说,但是可以很明显的看到,上述的1e-10的精度,已经溢出了FP16的精度下限。如果一个模型中的权重分布大部分都处在溢出边缘的话,那么模型转换完FP16精度的模型指标可能会大大下降。

b6165521dcc4e869015bab6ec1a85997.jpeg

除了FP16,当然还有很多其他精度(TF32、BF16、IN8),这里暂且不谈,不过有篇讨论各种精度的文章可以先了解下。

话说回来,我们该如何统计该层的权重信息呢?利用Pytorch中原生的代码就可以实现:

  1. # 假设v是某一层conv的权重,我们可以简单通过以下命令查看到该权重的分布
  2. v.max()
  3. tensor(0.8559)
  4. v.min()
  5. tensor(-0.9568)
  6. v.abs()
  7. tensor([[0.03140.00450.0182,  ..., 0.03090.02040.0345],
  8.         [0.02950.04860.0746,  ..., 0.03630.02620.0108],
  9.         [0.03280.05820.0149,  ..., 0.09320.04440.0221],
  10.         ...,
  11.         [0.03370.05180.0280,  ..., 0.01740.00780.0010],
  12.         [0.00220.02970.0167,  ..., 0.04720.00060.0128],
  13.         [0.06310.01440.0232,  ..., 0.00720.07040.0479]])
  14. v.abs().min() # 可以看到权重绝对值的最小值是1e-10级别
  15. tensor(2.0123e-10)
  16. v.abs().max()
  17. tensor(0.9568)
  18. torch.histc(v.abs()) # 这里统计权重的分布,分为100份,最小最大分别是[-0.9558,0.8559]
  19. tensor([3.3473e+063.2437e+063.0395e+062.7606e+062.4251e+062.0610e+06,
  20.         1.6921e+061.3480e+061.0352e+067.7072e+055.5376e+053.8780e+05,
  21.         2.6351e+051.7617e+051.1414e+057.3327e+044.7053e+043.0016e+04,
  22.         1.9576e+041.3106e+049.1220e+036.4780e+034.6940e+033.5140e+03,
  23.         2.8330e+032.2040e+031.7220e+031.4020e+031.1130e+031.0200e+03,
  24.         8.2400e+027.0600e+025.7900e+024.6400e+024.1600e+023.3400e+02,
  25.         3.0700e+022.4100e+022.3200e+021.9000e+021.5600e+021.1900e+02,
  26.         1.0800e+029.9000e+016.9000e+015.2000e+014.9000e+012.2000e+01,
  27.         1.8000e+012.8000e+011.2000e+011.3000e+018.0000e+003.0000e+00,
  28.         4.0000e+003.0000e+001.0000e+001.0000e+000.0000e+001.0000e+00,
  29.         1.0000e+000.0000e+000.0000e+000.0000e+000.0000e+000.0000e+00,
  30.         1.0000e+000.0000e+000.0000e+000.0000e+000.0000e+002.0000e+00,
  31.         0.0000e+002.0000e+001.0000e+000.0000e+001.0000e+000.0000e+00,
  32.         2.0000e+000.0000e+000.0000e+000.0000e+000.0000e+000.0000e+00,
  33.         0.0000e+000.0000e+000.0000e+000.0000e+000.0000e+001.0000e+00,
  34.         0.0000e+000.0000e+000.0000e+000.0000e+000.0000e+000.0000e+00,
  35.         0.0000e+000.0000e+000.0000e+001.0000e+00])

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

0cc657caf4f9a659e4d0a18c81a9e070.png

Tensorboard查看权重分布

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

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

有权重,所以重点关照

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

  • 卷积层

  • 全连接层

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

  • transformer-encoder层

  • DCN层

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

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

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

  1. # 截取了Pytorch中BN层的部分代码
  2. def __init__(
  3.     self,
  4.     num_features: int,
  5.     eps: float = 1e-5,
  6.     momentum: float = 0.1,
  7.     affine: bool = True,
  8.     track_running_stats: bool = True
  9. ) -> None:
  10.     super(_NormBase, self).__init__()
  11.     self.num_features = num_features
  12.     self.eps = eps
  13.     self.momentum = momentum
  14.     self.affine = affine
  15.     self.track_running_stats = track_running_stats
  16.     if self.affine:
  17.         self.weight = Parameter(torch.Tensor(num_features))
  18.         self.bias = Parameter(torch.Tensor(num_features))
  19.     else:
  20.         self.register_parameter('weight', None)
  21.         self.register_parameter('bias', None)
  22.     if self.track_running_stats:
  23.         # 可以看到在使用track_running_stats时,BN层会更新这三个参数
  24.         self.register_buffer('running_mean', torch.zeros(num_features))
  25.         self.register_buffer('running_var', torch.ones(num_features))
  26.         self.register_buffer('num_batches_tracked', torch.tensor(0, dtype=torch.long))
  27.     else:
  28.         self.register_parameter('running_mean', None)
  29.         self.register_parameter('running_var', None)
  30.         self.register_parameter('num_batches_tracked', None)
  31.     self.reset_parameters()

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

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

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

  • Reshape

  • Squeeze

  • Unsqueeze

  • Split

  • Transpose

  • Gather

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

4bf919bdb7d38167eabc48876ceacce7.png

比较繁琐变换维度的op

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

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

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

728008f9877ab7acac7629ed6a66bd82.png

reshape也可能有参数

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

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

提取权重

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

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

Caffe2Pytorch

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

7c610bd2f734b0233888fb8f87dc504d.png

caffe结构与权重

我们都知道Caffe的权重使用Caffemodel表示,而相应的结构是prototxt。如上图,左面是prototxt右面是caffemodel,而caffemodel使用的是protobuf这个数据结构表示的。我们当然也要先读出来:

  1. model = caffe_pb2.NetParameter()
  2. print('Loading caffemodel: ' + caffemodel)
  3. with open(caffemodel, 'rb') as fp:
  4.     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中。

很简单吧。

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

Pytorch2TensorRT

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

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

e0940faec71fd3e10ba9bd0bf9c4cc84.png

看看PTH模型的BN层

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

ea2b6afb1974c36e7a467a0f1457352c.png

torch.load细节

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

4b8ca5309c5fa083e3d625ce441ec784.png

keys

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

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

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

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

  1. // Load weights from files shared with TensorRT samples.
  2. // TensorRT weight files have a simple space delimited format:
  3. // [type] [size] <data x size in hex>
  4. std::map<std::string, Weights> loadWeights(const std::string file)
  5. {
  6.     std::cout << "Loading weights: " << file << std::endl;
  7.     std::map<std::string, Weights> weightMap;
  8.     // Open weights file
  9.     std::ifstream input(file);
  10.     assert(input.is_open() && "Unable to load weight file.");
  11.     // Read number of weight blobs
  12.     int32_t count;
  13.     input >> count;
  14.     assert(count > 0 && "Invalid weight map file.");
  15.     while (count--)
  16.     {
  17.         Weights wt{DataType::kFLOAT, nullptr, 0};
  18.         uint32_t size;
  19.         // Read name and type of blob
  20.         std::string name;
  21.         input >> name >> std::dec >> size;
  22.         wt.type = DataType::kFLOAT;
  23.         // Load blob
  24.         uint32_t *val = reinterpret_cast<uint32_t *>(malloc(sizeof(val) * size));
  25.         for (uint32_t x = 0, y = size; x < y; ++x)
  26.         {
  27.             input >> std::hex >> val[x];
  28.         }
  29.         wt.values = val;
  30.         wt.count = size;
  31.         weightMap[name] = wt;
  32.     }
  33.     std::cout << "Finished Load weights: " << file << std::endl;
  34.     return weightMap;
  35. }

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

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

73241612c9d13587a1f1b810678e858a.png
TensorRT转换后,模型的网络结构

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

后记

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

自动驾驶之心】全栈技术交流群

自动驾驶之心是首个自动驾驶开发者社区,聚焦目标检测、语义分割、全景分割、实例分割、关键点检测、车道线、目标跟踪、3D目标检测、多传感器融合、SLAM、光流估计、轨迹预测、高精地图、规划控制、AI模型部署落地等方向;

加入我们:自动驾驶之心技术交流群汇总!

自动驾驶之心【知识星球】

想要了解更多自动驾驶感知(分类、检测、分割、关键点、车道线、3D目标检测、多传感器融合、目标跟踪、光流估计、轨迹预测)、自动驾驶定位建图(SLAM、高精地图)、自动驾驶规划控制、领域技术方案、AI模型部署落地实战、行业动态、岗位发布,欢迎扫描下方二维码,加入自动驾驶之心知识星球(三天内无条件退款),日常分享论文+代码,这里汇聚行业和学术界大佬,前沿技术方向尽在掌握中,期待交流!

06a9b60f19be37e211800e2e93210aef.jpeg

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/87417?site
推荐阅读
相关标签
  

闽ICP备14008679号