转pytorch中训练深度神经网络模型的关键知识点

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_42279044/article/details/101053719
            </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>
  1. 默认日常描述图片尺寸,采用[w,h]的形式,比如一张图片是1280*800就是指宽w=1280, 高h=800。
    因此在cfg中所指定img scale = [1333, 800]就是指w=1333, h=800
    从而转入计算机后,要从w,h变成h,w
  2. 默认的大部分数据集,输出格式都是n,h,w,c和bgr格式,一方面是hwc更普遍,另一方面是opencv读取的就是bgr。
  3. pytorch中指定的数据格式是chw和rgb(非常重要!记住!),所以常规处理方法是:数据集输出都统一定义成hwc和bgr,再通过
    transform来转换成chw和rgb

关于img/label与模型weight之间的数据格式匹配

  1. 输入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])

  2. 如果要把在GPU中运算的数据可视化,必须先变换到cpu,然后解除grad,最后转numpy才能使用。
    即:x1 = x.cpu().detach().numpy()

关于图片标签值的定义在分类问题和检测问题上的区别

  1. 在纯分类任务中,数据集的label一般定义成从0开始,比如10类就是[0,9],这样的好处是在转独热编码的时候比较容易,
    比如标签2的独热编码就是[0,0,1,0], 标签0的独热编码就是[1,0,0,0]
  2. 而在物体检测任务中的分类子任务中,一般会把数据集的label定义成从1开始,比如10类就是[1,10], 这样做的目的是
    因为在检测任务中需要对anchor的身份进行指定,而比较简洁的处理是把负样本的anchor设定为label=0。所以相当于把
    label=0预留给anchor的负样本。

关于transform中涉及的类型变换导致的错误

  1. transform和transform_inv中涉及的数据类型变换很多种类,很容易漏掉没有做而导致输出形式不对。

  2. 对于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)范围内的数值,因为少部分数值在变换过程中会超过这个范围。

关于训练过程中图片尺寸如何变换的问题?

  1. 通常会定义一个边框尺寸,比如scale = (300, 300),这是图片的最大尺寸范围。

  2. 图片首先经过transform,按比例缩放到边框尺寸,此时因为比例固定,所以每张图片尺寸都不同,但都有一条片跟边框尺寸拉平相等。
    比如一个batch的图片可能尺寸会变成(300, 256),(300, 284),(240,300)这样的形式。

  3. 图片然后经过dataloader的collate_fn,对一个batch的图片取最大外沿,进行padding变成相同尺寸的图片。
    由于transform时所有图片已有一条边靠近边框尺寸,所以取所有图片最大外沿结果基本都是边框尺寸,比如一个batch的图片会变成
    (300,300),(300,300),(300,300)然后堆叠成(3,300,300), 所以最终进行训练的图片基本都是以边框尺寸进行训练。

关于训练时候的路径问题

  1. 经常会产生路径找不到的情况,比较合理的解决方案是:

    • 数据集的root路径,尽可能采用绝对路径,即以/开头的绝对路径。
    • 项目的root路径尽可能加到sys.path中去。
  2. 如果有报错说部分路径无法导入,一般有2种可能性:

    • 根目录路径不在sys.path中,可通过添加根目录路径到sys.path并结合根目录下一级目录可见的原则,实现大部分文件的导入。
    • 被导入的module中含有错误的导入,需要修正这些错误,才能解决报错
    • 存在交叉导入:即A要导入B,同时B也要导入A,此时有可能产生错误,需要解除交叉导入才能解决报错。

关于归一化和标准化

  1. 常见训练所采用的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]
  2. 如果在一个数据集上从头训练,则需要事先计算他的mean, std。但要注意mean,std的数值顺序到底是BGR顺序还是RGB顺序。

  3. pytorch自己的transform往往是先归一化再标准化,即:
    img = (img/255 - mean) / std
    所以事先准备的mean和std都是很小的值,在-1,1之间

  4. 后边看到一些其他例子,比如mmdetection里边对其他数据集的提供的mean, std值较大。
    说明对数据的处理是只做了标准化,即:
    img = (img - mean)/std
    所以实现准备的mean和std都是比较大的数值,在0-255之间

  5. 总结一下就是:

    • 如果是用现有模型参数做迁移学习:采用的mean/std需要跟原来基模型一样,数据预处理也要按这种方式。
      比如采用caffe/pytorch基模型,则都是只做标准化是img = (img - mena) / std
    • 如果从头开始训练:则采用的mean/std跟实际img的预处理方式必须一样。
      比如采用在0-1之间的小的mean, std,则实际数据处理也要要归一化+标准化。
      而采用大的mean,std,则实际数据处理也只要做标准化。

关于卷积的取整方式

  1. pytorch中conv, maxpool在计算输出w,h尺寸时,默认的取整方式都是下取整(floor)。
    唯一不同的是,maxpool可以手动设置成上取整即ceil mode,但conv不能手动设置,也就只能下取整。

  2. conv, maxpool两者计算输出尺寸的公式一样

    • 没有dilation时,w’ = (w - ksize + 2p)/s + 1
    • 有diliation时,相当于ksize被扩大,此时
      w’ = (w - d(ksize-1) +2p -1)/s + 1

关于神经网络的前向计算和反向传播在pytorch中的对应

  1. 前向传播:就是模型一层一层计算,output = model(img)
  2. 损失计算:采用pytorch自带的nn.CrossEntropyLoss(y_pred, y_label),则不需要手动增加softmax,
    也不需要手动把label转换成独热编码。因为这两部分都被写在pytorch自带的交叉熵函数对象内部了。
  3. 损失反向传播:必须针对损失的标量进行,也就是losses先做规约缩减(reduction=‘mean’),然后才能
    loss.backward(),即这里loss是一个标量值,pytorch对这个标量内部梯度会自动获取,并反向传播。
  4. 优化器更新权值:必须采用optimizer.step()完成
  5. 额外要求:必须增加一句优化器梯度清零,optimizer.zero_grad(),这句必须放在backward之前,
    确保变量的梯度不会一直累加,而是每个batch独立计算,一个batch结束就清零一次。
    (自己写的神经网络,梯度是整个batch一起算,不需要累加,计算以后直接赋值,所以也就不需要清零了。)

关于在GPU训练

  1. 如果要在GPU训练,只需要3步

    • 创建设备device:
    • 模型送入device
    • batch data送入device: 注意这里理论上只要img送入device就可以,因为跟model相关的计算只需要img输入
  2. 并行式GPU训练并不一定比单GPU快,相反对于一些比较小的模型,单GPU的速度远超过并行式训练的速度。
    可能因为并行式训练需要让数据在GPU之间搬运造成时间损耗,同时python的并行式训练并不是真正的并行,
    而是在同一时间下只有一块GPU运行的假并行,只是能利用多GPU内存而不能利用多GPU算力的假并行。

  3. 分布式训练才是真正的多GPU算力并行训练。

关于如何设置DataLoader

  1. 对常规的数据集,如果图片尺寸都是一样的,那么直接使用pytorch默认DataLoader就可以。

  2. 对数据集中img的尺寸不一样的,由于dataloader需要对img进行堆叠,此时图片尺寸不同无法直接堆叠,
    则需要自定义collate_fn对img进行堆叠,同时处理那些labels/bboxes/segments是堆叠还是放入list.

  3. 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的问题

  1. numpy和pytorch都有可能产生in_contiguous的问题。主要都是在transpose(),permute()之后会发生。
    参考:https://discuss.pytorch.org/t/negative-strides-of-numpy-array-with-torch-dataloader/28769

  2. 在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.

  3. 在tensor数据下的报错形式是:
    可通过t1.is_contiguous()查看到是否连续。

  4. 解决方案;

    • 对于numpy: img = np.ascontiguousarray(img)
    • 对于tensor: img = img.contiguous()

关于在dataset的__getitem__()中增加断点导致程序崩溃的问题

  1. 现象:如果模型和数据送入GPU,dataloader会调用dataset的__getitem__函数获取数据进行堆叠,
    此时如果在__getitem__里边有自定义断点,会造成系统警告且暂停训练。
  2. 解决方案:取消断点后模型训练/验证就正常了。而如果想要调试__getitem__里边的语法,可以设置
    额外的语句img, label = dataset[0]来进入__getitem__,或者next(iter(dataloader))

关于预训练模型的加载

  1. 预训练模型加载需要分两步:第一步加载checkpoint文件,第二部加载state_dict,且第二步需要去除多余的key以及去除param size不匹配的key.
  2. 对于state_dict不匹配的问题,一般两种解决方案:
    • 直接修改预训练模型的最后一部分,然后加载参数时过滤不匹配的key
    • 自己创建整个模型,然后加载参数时过滤不匹配的key
  3. 预训练模型的参数如果放在默认.torch/model或者.cache/里边,则作为隐藏文件夹中的文件,无法通过os.path.isfile()检查出来,会报错找不到该文件。
    所以,虽然能够加载但无法检出。
    也可以把参数移到自定义文件夹去,就可以通过os.path.isfile()检测到了。

关于常规分类问题和物体检测中的分类问题的差异?

  1. 常规分类问题是对一张图片作为最小个体进行分类;而物体检测问题中的分类是以一张图片中的每一个bbox进行分类。
    因此对于物体检测问题本质上一张图片是多个分类问题的集合。

  2. 因此常规分类问题是一张img对应一个独立label, 1个batch的数据为多张img对应多个独立label,1个batch计算一次loss,
    所以需要把一个batch的label组合成一组,相当于1个batch就是一组img对应一组label。
    因此分类的一个batch计算本质上相当于检测问题的一张图片计算。

  3. 但是检测分类问题是一张img有一组bbox对应一组label,1个batch的数据为多张img拥有多组bbox对应多组label,每组bbox,label完成一次loss计算。
    因此检测的一个batch计算本质上相当于每张图片是一次分类batch,多张图片就要手动进行多次img循环计算,循环的主体就是常规分类问题的一个batch。
    这样理解的话,就可以统一常规分类问题和检测中的分类问题了:
    - 常规分类问题需要把labels组合成一组,变成一个标准计算。
    - 检测分类问题需要循环计算每张img,而每张img计算相当于一次常规分类问题的batch计算。

关于卷积核的作用

  1. 结论:

    • 浅层卷积核主要过滤边缘、纹理的初级信息;
    • 中层卷积核就开始学习小物体;
    • 深层卷积核开始学习大物体;
  2. 证明:参考https://www.leiphone.com/news/201808/DB6WARlVGdm4cqgk.html
    可以看到单层(也就相当于浅层)卷积核收敛以后的输出图片,类似于sobel算子,是对图片进行某个方向的边缘过滤。
    也就是说明了浅层神经网络主要是学习边缘、纹理等初级信息

关于全连接层的参数过多如何解决的问题

  1. 全连接层所占参数一般占了一个模型总参数个数的80%以上,所以为了减小模型所占内存,需要优化全连接

  2. 替换方式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>
posted @ 2019-12-04 11:30  core!  阅读(730)  评论(0编辑  收藏  举报