作者丨老潘
来源丨Oldpan博客
编辑|极石平台
今天我们简单讲一下模型权重,也就是我们俗称的权重。
在深度学习中,我们一直在训练模型,通过反向传播求导来更新模型的权重,最终得到泛化能力较强的模型。同样,如果我们不训练,只是随机初始化权重,我们也可以获得相同大小的模型。虽然两者尺寸相同,但两者的体重信息分布却截然不同。一个大脑充满了知识,另一个大脑充满了水。差不多就这样了。
所谓AI模型部署阶段,说白了就是把训练好的权重移到另一个地方去运行。一般来说,权重信息和权重分布基本保持不变(精度可能会改变,也可能会合并一些权重)。
然而,执行模型运算(卷积、全连接、反卷积)的算子会发生变化,可能来自Pytorch-TensorRT或TensorFlow-TFLITE,即实现算子的方式发生了变化。在Pytorch 框架中可以使用相同的卷积运算。它是一种实现,也是TensorRT中的另一种时间。两者的基本原理是相同的,但精度和速度不同。 TensorRT可以使用Pytorch训练的卷积的权重来实现与Pytorch中相同的操作。但可能会更快。
重量/重量/检查点那么重量是多少?他们长什么样子?
真的很难描述……只是一堆数据。没错,我们辛辛苦苦调整和训练的权重只是一堆数据。也就是说,这种神奇的数据,结合各种神经网络算子,可以实现各种检测、分类、识别任务。
例如,在上图中,我们使用Netron工具查看ONNX模型的第一个卷积权重。显然这个卷积只有一个W权重并且没有偏置b。本次卷积的权值维度为[64,3,7,7],即输入通道3,输出通道64,卷积核大小7×7。
仔细观察,我们可以发现这个权重的数值范围其实差别很大,最大也不过0.1这个级别。但至于最小的,用肉眼一看(其实应该算),最小的其实也有1e-10的等级。
一般我们训练的时候,输入的权重都是0-1。当然,也有0-255的情况,但无论是0-1还是0-255,只要不超过精度的上下限,就没有问题。对于FP32来说,1e-10是一个小情况,但是对于FP16来说就不一定了。
我们知道FP16 的一般精度为~5.96e8 (6.10e5) . 65504。具体的精度细节我们不说,但是可以明显看出,上述1e-10 的精度已经超出了FP16 的范围。准确度下限。如果模型中大部分权重分布处于溢出边缘,模型转换后的FP16精度模型指标可能会大幅下降。
除了FP16之外,当然还有很多其他精度(TF32、BF16、IN8),这里我暂时不讨论,但是有一篇讨论各种精度的文章你可以先读一下:https://moocaholic.medium.com /fp64-fp32-fp16-bfloat16- tf32 和其他动物园成员a1ca7897d407
说了这么多,我们如何统计这一层的权重信息呢?这可以使用Pytorch 中的本机代码来实现:
# 假设v是某层conv的权重,我们可以通过以下命令简单查看权重的分布情况
v.max()
张量(0.8559)
v.min()
张量(-0.9568)
v.abs()
张量([[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级别
张量(2.0123e-10)
v.abs().max()
张量(0.9568)
torch.histc(v.abs()) # 这里统计权重的分布,分为100份。最小值和最大值为[-0.9558,0.8559]
张量([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、
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看。
图像
那么看权重分布有什么用呢?
这绝对是有用的。在训练和部署过程中,权重分布可以作为判断模型是否正常以及是否保持准确性的重要信息。但我不会在这里详细介绍。
有权重,所以重点关注模型训练过程。有很多权重需要通过反向传播来更新。常见的包括:
卷积层、全连接层、批处理层(BN层,或者其他各种LN、IN、GN)变压器编码器层、DCN层。这些层通常是神经网络的核心部分。当然,它们都是有参数的,而且它们肯定会参与模型的反向传播更新,是我们训练模型时需要关注的重要参数。
# Pytorch中conv层的部分代码,可以看到参数的维度等信息
self._reversed_padding_repeated_twice=_reverse_repeat_tuple(self.padding, 2)
如果换位:
self.weight=参数(torch.Tensor(
in_channels, out_channels //组, *kernel_size))
否则:
self.weight=参数(torch.Tensor(
out_channels, in_channels //组, *kernel_size))
如果偏向:
self.bias=参数(torch.Tensor(out_channels))
还有一些参数不参与反向传播,但会随着训练而更新。比较常见的是BN层的running_mean和running_std:
# 截取Pytorch中BN层的部分代码
def __init__(
自己,
num_features: int,
eps: 浮点数=1e-5,
动量:浮动=0.1,
affine: 布尔=真,
track_running_stats: 布尔=True
) – 无:
超级(_NormBase,自我).__init__()
self.num_features=num_features
self.eps=eps
自身动量=动量
self.affine=仿射
self.track_running_stats=track_running_stats
如果self.affine:
self.weight=参数(torch.Tensor(num_features))
self.bias=参数(torch.Tensor(num_features))
否则:
self.register_parameter(\’体重\’, None)
self.register_parameter(\’偏差\’, None)
如果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))
否则:
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_mean和running_var,使用register_buffer。那么两者有什么区别呢?也就是说,注册为buffer的参数往往不会参与反向传播的计算,但在模型训练过程中仍然会被更新,所以也需要认真对待。
关于BN层,在转换模型和训练模型时会存在陷阱,需要注意。
刚才描述的层都是有参数的,那么其他没有参数的层是什么呢?当然,我们的网络中实际上有很多op,只是做一些维度变换、索引值或者上/下采样操作,比如:
ReshapeSqueezeUnsqueezeSplitTransposeGather等,这些操作没有参数,只是用来对上一层传过来的张量进行维度变换,以实现一些“花哨”的操作。至于这些令人眼花缭乱的技能,有的有用,有的乏味。
上图中那些乱七八糟的op,单独拆开还能认得出来,但如果全部连起来(如上图),连爸爸都可能认不出来。
开玩笑,其实有时候通过Pytorch转换为ONNX时,偶尔会出现一些奇怪的转换情况。例如,一个简单的重塑将被分成gather+slip+concat。这个操作相当于复杂度。不过一般来说,ONNX-SIMPLIFY可以用来优化这种情况。当然,如果遇到比较复杂的,就需要自己优化了。
哦,对了,这些变形算子中有些其实是有参数的,比如下图中的reshap:
像这样的操作,怎么说呢,有时会很棘手。如果我们要将这个ONNX模型转换为TensorRT,我们100%会遇到问题,因为TensorRT解释器解析ONNX时,它不支持将reshape层的形状输入到TensorRT,而是将这个形状视为属性。并且支持ONNX的推理框架Inference。
然而,这些都是小问题。大多数情况下,我们可以通过改变模型或结构来解决,而且成本并不高。但还有一些其他复杂的问题可能需要我们重点研究。
提取权重如果要将训练好的模型从这个平台部署到另一个平台,首先要做的就是传输权重。不过在实践中,大部分转换器已经帮我们做好了(比如onnx-TensorRT),所以我们不用自己操心!
onnx-TensorRT:https://github.com/onnx/onnx-tensorrt
不过,如果想对模型权重有一个整体的了解,还是建议自己尝试一下。
Caffe2Pytorch首先简单讲一下Caffe和Pytorch之间的权重转换。这里推荐一个开源仓库Caffe-python(https://github.com/marvis/pytorch-caffe)。它已经帮助我们编写了基于prototxt提取Caffemodel权重并构建相应Pytorch模型结构的过程。我们不需要重新发明轮子。
我们都知道Caffe的权重是用Caffemodel来表示的,对应的结构是prototxt。如上图所示,左边是prototxt,右边是caffemodel,caffemodel是用protobuf数据结构来表示的。当然我们也得先读一下:
模型=caffe_pb2.NetParameter()
print(\’正在加载caffemodel:\’ + caffemodel)
打开(caffemodel,\’rb\’)作为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已经搭建好的卷积层。这里取权重后,将caffe的权重放入PyTorch中。
这很简单。
if ltype in [\’卷积\’, \’反卷积\’]:
print(\’负载重量%s\’ % lname)
卷积参数=层[\’卷积参数\’]
偏差=真
if \’bias_term\’ in volution_param and volution_param[\’bias_term\’]==\’false\’:
偏差=假
#weight_blob=lmap[lname].blob[0]
# print(\’caffe权重形状\’,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)
如果偏差和len(lmap[lname].blob) 1:
self.models[lname].bias.data.copy_(torch.from_numpy(np.array(lmap[lname].blobs[1].data)))
print(\’卷积%s 有偏差\’ % lname)
Pytorch2TensorRT先给出一个简单的例子。一般我们使用Pytorch模型进行训练。我们一般使用torch.save()将训练得到的权重保存为.pth格式。
PTH 由Pytorch 使用Python 中的内置模块pickle 保存和读取。让我们使用netron 来看看pth 是什么样子的。
可以看出,模型中仅表示了参数权重,并未包含模型结构。然而,我们可以通过.py模型结构将.pth权重一一加载到我们的模型中。
图像
读完.pth后看一下state_dict的key。这些键也对应着我们构建模型时为每一层注册的权重名称和权重信息(包括维度和类型等)。
当然这个pth还可以包含其他字符段{\’epoch\’: 190, \’state_dict\’: OrderedDict([(\’conv1.weight\’,tensor([[.比如训练了多少个epoch,学习率等
对于pth,我们可以通过以下代码将其提取出来,并以TensorRT的权重格式存储。
def extract_weight(args):
# 加载模型
state_dict=torch.load(args.weight)
打开(args.save_path,\’w\’)作为f:
f.write(\'{}\\n\’.format(len(state_dict.keys())))
对于state_dict.items(): 中的k、v
vr=v.reshape(-1).cpu().numpy()
f.write(\'{} {} \’.format(k, len(vr)))
适用于vr: 中的vv
f.write(\’ \’)
f.write(struct.pack(\’f\’, float(vv)).hex())
f.write(\’\\n\’)
需要注意的是,这里的TensorRT权重格式是指构建前的权重。 TensorRT仅用于构建整个网络,将每个解析层的权重传递到其中,然后通过TensorRT网络构建引擎。
//从与TensorRT 样本共享的文件中加载权重。
//TensorRT 权重文件具有简单的空格分隔格式:
//[类型] [大小] 数据x 十六进制大小
std:mapstd:string,权重loadWeights(const std:string 文件)
{
std:cout \’正在加载weights:\’文件std:endl;
std:mapstd:string,权重weightMap;
//打开权重文件
std:ifstream 输入(文件);
assert(input.is_open() \’无法加载权重文件。\’);
//读取权重块的数量
int32_t 计数;
输入计数;
assert(count 0 \’无效的权重映射文件。\’);
同时(数–)
{
权重wt{DataType:kFLOAT, nullptr, 0};
uint32_t 大小;
//读取blob 的名称和类型
std:字符串名称;
输入名称std:dec 大小;
wt.type=DataType:kFLOAT;
//加载斑点
uint32_t *val=reinterpret_castuint32_t *(malloc(sizeof(val) * size));
对于(uint32_t x=0, y=大小; x y; ++x)
{
输入std:hex val[x];
}
wt.values=val;
wt.count=大小;
权重映射[名称]=wt;
}
std:cout \’完成加载权重:\’文件std:endl;
返回权重映射;
}
那么经过TensorRT优化后呢?模型是什么样的?我们把体重放在哪里?
它必须在内置引擎中,但由于TensorRT 优化,这些权重可能已被合并/删除/合并。
关于模型参数的知识还是很多的,最近也有很多相关的研究,比如参数重参数化,这是一项相当扎实的工作,经常在很多训练和部署场景中使用。
我们先在后记中谈谈这些。它们相对基础,而且往往级别较低。尽管神经网络一直被认为是黑匣子,那是因为没有明确的理论证明。但是我们可以看到训练好的模型的权重,也可以知道模型的基本结构,尽管我们无法证明模型为什么有效?为什么工作?但通过结构和权重分布等先验知识,我们也可以大致了解模型并更好地部署它。
至于神经网络的可解释性,这有点形而上学。我对此了解不多,这里就不多说了~