转pytorch中训练深度神经网络模型的关键知识点
</div>
<!--一个博主专栏付费入口-->
<!--一个博主专栏付费入口结束-->
<link rel="stylesheet" href="https://csdnimg.cn/release/phoenix/template/css/ck_htmledit_views-4a3473df85.css">
<div id="content_views" class="markdown_views prism-github-gist">
<!-- flowchart 箭头图标 勿删 -->
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<path stroke-linecap="round" d="M5,0 0,2.5 5,5z" id="raphael-marker-block" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path>
</svg>
<h3><a name="t0"></a><a id="_0"></a>关于数据格式</h3>
- 默认日常描述图片尺寸,采用[w,h]的形式,比如一张图片是1280*800就是指宽w=1280, 高h=800。
因此在cfg中所指定img scale = [1333, 800]就是指w=1333, h=800
从而转入计算机后,要从w,h变成h,w - 默认的大部分数据集,输出格式都是n,h,w,c和bgr格式,一方面是hwc更普遍,另一方面是opencv读取的就是bgr。
- pytorch中指定的数据格式是chw和rgb(非常重要!记住!),所以常规处理方法是:数据集输出都统一定义成hwc和bgr,再通过
transform来转换成chw和rgb
关于img/label与模型weight之间的数据格式匹配
-
输入img要修改为float()格式float32,否则跟weight不匹配报错
输入label要修改为long()格式int64,否则跟交叉熵公式不匹配报错
img = img.float()
label = label.long()
这两句要放在每个batch开始位置为了避免遗忘,可以把这部分操作集成到自定义的to_tensor()函数中,在每次开始转tensor的时候自动转换:
if isinstance(data, torch.Tensor):
return data
elif isinstance(data, np.ndarray):
return torch.from_numpy(data)
elif isinstance(data, Sequence) and not mmcv.is_str(data):
return torch.tensor(data)
elif isinstance(data, int):
return torch.LongTensor([data])
elif isinstance(data, float):
return torch.FloatTensor([data]) -
如果要把在GPU中运算的数据可视化,必须先变换到cpu,然后解除grad,最后转numpy才能使用。
即:x1 = x.cpu().detach().numpy()
关于图片标签值的定义在分类问题和检测问题上的区别
- 在纯分类任务中,数据集的label一般定义成从0开始,比如10类就是[0,9],这样的好处是在转独热编码的时候比较容易,
比如标签2的独热编码就是[0,0,1,0], 标签0的独热编码就是[1,0,0,0] - 而在物体检测任务中的分类子任务中,一般会把数据集的label定义成从1开始,比如10类就是[1,10], 这样做的目的是
因为在检测任务中需要对anchor的身份进行指定,而比较简洁的处理是把负样本的anchor设定为label=0。所以相当于把
label=0预留给anchor的负样本。
关于transform中涉及的类型变换导致的错误
-
transform和transform_inv中涉及的数据类型变换很多种类,很容易漏掉没有做而导致输出形式不对。
-
对于transform_inv的变换,需要重点关注
- 逆变换需要把数据先从GPU取出来到cpu中并转换成numpy形式,才能进行后续逆变换和显示。
- 默认数据集输出类型:hwc, bgr。采用这种默认输出形式,主要是因为用opencv作为底层函数的输出就是这种形式。
而pytorch需要的形式是chw, rgb,所以经过transform后输出就是chw,rgb - 逆变换需要先变换chw为hwc,然后才变换rgb为bgr:因为rgb2bgr是基于最后一个维度是c来写的。
- 逆变换需要把rgb转换为bgr
- 逆变换需要把归一化和标准化的图片恢复成原始图片的数据形式,并且需要从float转换为unit8,这样才能被opencv识别。
- 最后进行截取(0-255)范围内的数值,因为少部分数值在变换过程中会超过这个范围。
关于训练过程中图片尺寸如何变换的问题?
-
通常会定义一个边框尺寸,比如scale = (300, 300),这是图片的最大尺寸范围。
-
图片首先经过transform,按比例缩放到边框尺寸,此时因为比例固定,所以每张图片尺寸都不同,但都有一条片跟边框尺寸拉平相等。
比如一个batch的图片可能尺寸会变成(300, 256),(300, 284),(240,300)这样的形式。 -
图片然后经过dataloader的collate_fn,对一个batch的图片取最大外沿,进行padding变成相同尺寸的图片。
由于transform时所有图片已有一条边靠近边框尺寸,所以取所有图片最大外沿结果基本都是边框尺寸,比如一个batch的图片会变成
(300,300),(300,300),(300,300)然后堆叠成(3,300,300), 所以最终进行训练的图片基本都是以边框尺寸进行训练。
关于训练时候的路径问题
-
经常会产生路径找不到的情况,比较合理的解决方案是:
- 数据集的root路径,尽可能采用绝对路径,即以/开头的绝对路径。
- 项目的root路径尽可能加到sys.path中去。
-
如果有报错说部分路径无法导入,一般有2种可能性:
- 根目录路径不在sys.path中,可通过添加根目录路径到sys.path并结合根目录下一级目录可见的原则,实现大部分文件的导入。
- 被导入的module中含有错误的导入,需要修正这些错误,才能解决报错
- 存在交叉导入:即A要导入B,同时B也要导入A,此时有可能产生错误,需要解除交叉导入才能解决报错。
关于归一化和标准化
-
常见训练所采用的mean,std并不是跟训练数据集相关,而是跟基模型所训练的数据集相关,这是
因为这些训练都是在基模型的基础上进行finetunning做迁移学习来训练的。
由于pytorch的基模型基本都是在imagenet中训练的,所以这些mean, std都是imagenet的参数。
而caffe的基模型虽然也是在imagenet中训练的,但因为处理方式不同所以std取成了1(待在caffe中确认原因)
比如:- 来自pytorch的基模型:[123.675, 116.28, 103.53], std=[58.395, 57.12, 57.375]
- 来自caffe的基模型:[123.675, 116.28, 103.53], std=[1, 1, 1]
-
如果在一个数据集上从头训练,则需要事先计算他的mean, std。但要注意mean,std的数值顺序到底是BGR顺序还是RGB顺序。
-
pytorch自己的transform往往是先归一化再标准化,即:
img = (img/255 - mean) / std
所以事先准备的mean和std都是很小的值,在-1,1之间 -
后边看到一些其他例子,比如mmdetection里边对其他数据集的提供的mean, std值较大。
说明对数据的处理是只做了标准化,即:
img = (img - mean)/std
所以实现准备的mean和std都是比较大的数值,在0-255之间 -
总结一下就是:
- 如果是用现有模型参数做迁移学习:采用的mean/std需要跟原来基模型一样,数据预处理也要按这种方式。
比如采用caffe/pytorch基模型,则都是只做标准化是img = (img - mena) / std - 如果从头开始训练:则采用的mean/std跟实际img的预处理方式必须一样。
比如采用在0-1之间的小的mean, std,则实际数据处理也要要归一化+标准化。
而采用大的mean,std,则实际数据处理也只要做标准化。
- 如果是用现有模型参数做迁移学习:采用的mean/std需要跟原来基模型一样,数据预处理也要按这种方式。
关于卷积的取整方式
-
pytorch中conv, maxpool在计算输出w,h尺寸时,默认的取整方式都是下取整(floor)。
唯一不同的是,maxpool可以手动设置成上取整即ceil mode,但conv不能手动设置,也就只能下取整。 -
conv, maxpool两者计算输出尺寸的公式一样
- 没有dilation时,w’ = (w - ksize + 2p)/s + 1
- 有diliation时,相当于ksize被扩大,此时
w’ = (w - d(ksize-1) +2p -1)/s + 1
关于神经网络的前向计算和反向传播在pytorch中的对应
- 前向传播:就是模型一层一层计算,output = model(img)
- 损失计算:采用pytorch自带的nn.CrossEntropyLoss(y_pred, y_label),则不需要手动增加softmax,
也不需要手动把label转换成独热编码。因为这两部分都被写在pytorch自带的交叉熵函数对象内部了。 - 损失反向传播:必须针对损失的标量进行,也就是losses先做规约缩减(reduction=‘mean’),然后才能
loss.backward(),即这里loss是一个标量值,pytorch对这个标量内部梯度会自动获取,并反向传播。 - 优化器更新权值:必须采用optimizer.step()完成
- 额外要求:必须增加一句优化器梯度清零,optimizer.zero_grad(),这句必须放在backward之前,
确保变量的梯度不会一直累加,而是每个batch独立计算,一个batch结束就清零一次。
(自己写的神经网络,梯度是整个batch一起算,不需要累加,计算以后直接赋值,所以也就不需要清零了。)
关于在GPU训练
-
如果要在GPU训练,只需要3步
- 创建设备device:
- 模型送入device
- batch data送入device: 注意这里理论上只要img送入device就可以,因为跟model相关的计算只需要img输入
-
并行式GPU训练并不一定比单GPU快,相反对于一些比较小的模型,单GPU的速度远超过并行式训练的速度。
可能因为并行式训练需要让数据在GPU之间搬运造成时间损耗,同时python的并行式训练并不是真正的并行,
而是在同一时间下只有一块GPU运行的假并行,只是能利用多GPU内存而不能利用多GPU算力的假并行。 -
分布式训练才是真正的多GPU算力并行训练。
关于如何设置DataLoader
-
对常规的数据集,如果图片尺寸都是一样的,那么直接使用pytorch默认DataLoader就可以。
-
对数据集中img的尺寸不一样的,由于dataloader需要对img进行堆叠,此时图片尺寸不同无法直接堆叠,
则需要自定义collate_fn对img进行堆叠,同时处理那些labels/bboxes/segments是堆叠还是放入list. -
pytorch默认的collate_fn设置不是None而是default_collate_fn,所以即使不用collate_fn
选项,也不要去把collate_fn设置为None,这会导致collate_fn找不到可用的函数导致错误。
(从这个角度说,pytorch的官方英文文档有问题,注明DataLoader的collate_fn默认=None,
但实际上collate_fn的默认=default_collate_fn。)
关于contiguous的问题
-
numpy和pytorch都有可能产生in_contiguous的问题。主要都是在transpose(),permute()之后会发生。
参考:https://discuss.pytorch.org/t/negative-strides-of-numpy-array-with-torch-dataloader/28769 -
在numpy数据下的报错形式是:ValueError: some of the strides of a given numpy array are negative.
This is currently not supported, but will be added in future releases. -
在tensor数据下的报错形式是:
可通过t1.is_contiguous()查看到是否连续。 -
解决方案;
- 对于numpy: img = np.ascontiguousarray(img)
- 对于tensor: img = img.contiguous()
关于在dataset的__getitem__()中增加断点导致程序崩溃的问题
- 现象:如果模型和数据送入GPU,dataloader会调用dataset的__getitem__函数获取数据进行堆叠,
此时如果在__getitem__里边有自定义断点,会造成系统警告且暂停训练。 - 解决方案:取消断点后模型训练/验证就正常了。而如果想要调试__getitem__里边的语法,可以设置
额外的语句img, label = dataset[0]来进入__getitem__,或者next(iter(dataloader))
关于预训练模型的加载
- 预训练模型加载需要分两步:第一步加载checkpoint文件,第二部加载state_dict,且第二步需要去除多余的key以及去除param size不匹配的key.
- 对于state_dict不匹配的问题,一般两种解决方案:
- 直接修改预训练模型的最后一部分,然后加载参数时过滤不匹配的key
- 自己创建整个模型,然后加载参数时过滤不匹配的key
- 预训练模型的参数如果放在默认.torch/model或者.cache/里边,则作为隐藏文件夹中的文件,无法通过os.path.isfile()检查出来,会报错找不到该文件。
所以,虽然能够加载但无法检出。
也可以把参数移到自定义文件夹去,就可以通过os.path.isfile()检测到了。
关于常规分类问题和物体检测中的分类问题的差异?
-
常规分类问题是对一张图片作为最小个体进行分类;而物体检测问题中的分类是以一张图片中的每一个bbox进行分类。
因此对于物体检测问题本质上一张图片是多个分类问题的集合。 -
因此常规分类问题是一张img对应一个独立label, 1个batch的数据为多张img对应多个独立label,1个batch计算一次loss,
所以需要把一个batch的label组合成一组,相当于1个batch就是一组img对应一组label。
因此分类的一个batch计算本质上相当于检测问题的一张图片计算。 -
但是检测分类问题是一张img有一组bbox对应一组label,1个batch的数据为多张img拥有多组bbox对应多组label,每组bbox,label完成一次loss计算。
因此检测的一个batch计算本质上相当于每张图片是一次分类batch,多张图片就要手动进行多次img循环计算,循环的主体就是常规分类问题的一个batch。
这样理解的话,就可以统一常规分类问题和检测中的分类问题了:
- 常规分类问题需要把labels组合成一组,变成一个标准计算。
- 检测分类问题需要循环计算每张img,而每张img计算相当于一次常规分类问题的batch计算。
关于卷积核的作用
-
结论:
- 浅层卷积核主要过滤边缘、纹理的初级信息;
- 中层卷积核就开始学习小物体;
- 深层卷积核开始学习大物体;
-
证明:参考https://www.leiphone.com/news/201808/DB6WARlVGdm4cqgk.html
可以看到单层(也就相当于浅层)卷积核收敛以后的输出图片,类似于sobel算子,是对图片进行某个方向的边缘过滤。
也就是说明了浅层神经网络主要是学习边缘、纹理等初级信息
关于全连接层的参数过多如何解决的问题
-
全连接层所占参数一般占了一个模型总参数个数的80%以上,所以为了减小模型所占内存,需要优化全连接
-
替换方式1:resnet/resnext的最后一部分就是这样实现。
用一个adaptiveAvgpooling(1), 先把卷积的每个通道收缩为1,即(c,h,w)->(c,1,1),再reshape后接一个简单全连接linear(c, 10)即可
变换前reshape后需要至少2个全连接(chw->256),再(256->10),参数总量256chw + 25610;
avgpool变换后reshape后只需要1个全连接(256->10),参数总量为25610, 减少了256chw个参数。
</div>
<link href="https://csdnimg.cn/release/phoenix/mdeditor/markdown_views-b6c3c6d139.css" rel="stylesheet">
</div>