MAE(Masked Autoencoders Are Scalable Vision Learners)

GitHub: https://github.com/facebookresearch/mae

PAPER: https://arxiv.org/abs/2111.06377

Abstract

恺明提出一种用于计算机视觉的可扩展自监督学习方案Masked AutoEncoders(MAE)。所提MAE极为简单:对输入图像的随机块进行mask并对遗失像素进行重建。它基于以下两个核心设计:

  • 我们设计了一种非对称编解码架构,其中解码器仅作用于可见块(无需mask信息),而解码器则通过隐表达与mask信息进行原始图像重建;
  • 我们发现对输入图像进行高比例mask(比如75%)可以产生一项重要且有意义的自监督任务。

上述两种设计促使我们可以更高效的训练大模型:我们加速训练达3x甚至更多,同时提升模型精度。所提方案使得所得高精度模型具有很好的泛化性能:仅需ImageNet-1K,ViT-Huge取得了87.8%的top1精度 。下游任务的迁移取得了优于监督训练的性能,证实了所提方案的可扩展能力。

极致精简版

用下面几句话来简单说明下这篇文章:

  • 恺明出品,必属精品!MAE延续了其一贯的研究风格:简单且实用;
  • MAE兴起于去噪自编码,但兴盛于NLP的BERT。那么是什么导致了MAE在CV与NLP中表现的差异呢?这是本文的出发点
  • 角度一:CV与NLP的架构不同。CV中常采用卷积这种具有”规则性“的操作,直到近期ViT才打破了架构差异;
  • 角度二:信息密度不同。语言是人发明的,具有高语义与信息稠密性;而图像则是自然信号具有重度空间冗余:遗失块可以通过近邻块重建且无需任何全局性理解。为克服这种差异,我们采用了一种简单的策略:高比例随机块掩码,大幅降低冗余。
  • 角度三:自编码器的解码器在重建方面的作用不同。在视觉任务方面,解码器进行像素重建,具有更低语义信息;而在NLP中,解码器预测遗失的词,包含丰富的语义信息。
  • 基于上述三点分析,作者提出了一种非常简单的用于视觉表达学习的掩码自编码器MAE。
  • MAE采用了非对称的编解码器架构,编码器仅作用于可见图像块(即输入图像块中一定比例进行丢弃,丢弃比例高达75%)并生成隐式表达,解码器则以掩码token以及隐式表达作为输入并对遗失块进行重建。
  • 搭配MAE的ViT-H取得了ImageNet-1K数据集上的新记录:87.8%;同时,经由MAE预训练的模型具有非常好的泛化性能。

Method

所提MAE是一种非常简单的自编码器方案:基于给定部分观测信息对原始信号进行重建 。类似于其他自编码器,所提MAE包含一个将观测信号映射为隐式表达的编码器,一个用于将隐式表达重建为原始信号的解码器。与经典自编码器不同之处在于:我们采用了非对称设计,这使得编码器仅依赖于部分观测信息(无需掩码token信息),而轻量解码器则接与所得隐式表达与掩码token进行原始信号重建(可参见下图)。

Masking 参考ViT,我们将输入图像拆分为非重叠块,然后采样一部分块并移除其余块(即Mask)。我们的采样策略非常简单:服从均匀分布的无重复随机采样 。我们将该采样策略称之为“随机采样”。具有高掩码比例的随机采样可以极大程度消除冗余,进而构建一个不会轻易的被近邻块推理解决的任务 (可参考下面图示)。而均匀分布则避免了潜在的中心偏置问题。

MAE Encoder MAE中的编码器是一种ViT,但仅作用于可见的未被Mask的块。类似于标准ViT,该编码器通过线性投影于位置嵌入对块进行编码,然后通过一系列Transformer模块进行处理。然而,由于该编解码仅在较小子集块(比如25%)进行处理,且未用到掩码Token信息。这就使得我们可以训练一个非常大的编码器 。

MAE Decoder MAE解码器的输入包含:(1) 编码器的输出;(2) 掩码token。正如Figure1所示,每个掩码Token共享的可学习向量,它用于指示待预测遗失块。此时,我们对所有token添加位置嵌入信息。解码器同样包含一系列Transformer模块。

注:MAE解码器仅在预训练阶段用于图像重建,编码器则用来生成用于识别的图像表达 。因此,解码器的设计可以独立于编码设计,具有高度的灵活性。在实验过程中,我们采用了窄而浅的极小解码器,比如默认解码器中每个token的计算量小于编码器的10% 。通过这种非对称设计,token的全集仅被轻量解码器处理,大幅减少了预训练时间。

Reconstruction target 该MAE通过预测每个掩码块的像素值进行原始信息重建 。解码器的最后一层为线性投影,其输出通道数等于每个块的像素数量。编码器的输出将通过reshape构建重建图像。损失函数则采用了MSE,注:类似于BERT仅在掩码块计算损失。

我们同时还研究了一个变种:其重建目标为每个掩码块的规范化像素值 。具体来说,我们计算每个块的均值与标准差并用于对该块进行归一化,最后采用归一化的像素作为重建目标提升表达能力。

Simple implementation MAE预训练极为高效,更重要的是:它不需要任何特定的稀疏操作。实现过程可描述如下:

  • 首先,我们通过线性投影与位置嵌入对每个输入块生成token;
  • 然后,我们随机置换(random shuffle)token序列并根据掩码比例移除最后一部分token;
  • 其次,完成编码后,我们在编码块中插入掩码token并反置换(unshuffle)得到全序列token以便于与target进行对齐;
  • 最后,我们将解码器作用于上述全序列token。

正如上所述:MAE无需稀疏操作。此外,shuffle与unshuffle操作非常快,引入的计算量可以忽略。

Efficient Self-supervised Vision Pretraining with Local Masked Reconstruction

论文地址:https://arxiv.org/abs/2206.00790

https://github.com/junchen14/LoMaR

Efficient Self-supervised Vision Pretraining with Local Masked Reconstruction,比MAE快3.1倍,比BEiT快5.3倍!KAUST&南洋理工提出基于局部mask重建的高效自监督视觉预训练方法LoMaR,同时提高训练精度和效率!

计算机视觉的自监督学习取得了巨大的进步,改进了许多下游视觉任务,如图像分类、语义分割和目标检测。其中,MAE和BEiT等生成性自监督视觉学习方法表现出了良好的性能。然而,它们的全局掩蔽重建机制对计算的要求很高。
为了解决这个问题,作者提出了局部掩蔽重建(local masked reconstruction,LoMaR),这是一种简单而有效的方法,在一个简单的Transformer编码器上,在7×7块的小窗口内执行掩蔽重建,与整个图像的全局掩蔽重建相比,提高了效率和精度之间的权衡。
大量实验表明,LoMaR在ImageNet-1K分类中达到84.1%的top-1精度,优于MAE 0.5%。在384×384图像上对预训练后的LoMaR进行微调后,可以达到85.4%的top-1精度,超过MAE 0.6%。在MS COCO上,LoMaR在目标检测上比MAE好0.5,在实例分割上比MAE好0.5。LoMaR在预训练高分辨率图像上的计算效率尤其高,例如,在预训练448×448图像上,LoMaR比MAE快3.1倍,分类精度高0.2%。这种局部掩蔽重建学习机制可以很容易地集成到任何其他生成性自监督学习方法中。

本文提出了一种新的模型,称为局部掩蔽重建或LoMaR。该模型将注意力区域限制在一个小窗口内,如7×7的图像块,这足以进行重建。对于那些需要在长序列上操作的任务,在许多NLP领域中也可以看到类似的方法。在视觉领域也探索了小窗口,以提高训练和推理速度。但与之前的视觉Transformer(如Swin Transformer)不同,Swin Transformer为每个图像创建具有固定坐标的移动窗口。本文取而代之的是对几个随机位置的窗口进行采样,这样可以更好地捕获不同空间区域中的对象。

在下图中,作者比较了LoMaR和MAE,并注意到两个主要区别:a)本文对一个区域进行了k×k个patch采样,以进行掩蔽重建,而不是从全部patch中进行重建。作者发现,只需一些局部视觉线索,就足以恢复丢失的信息,而不是从图像中全局25%的可见patch重建遮罩patch。b) 本文将MAE中的重量级解码器替换为轻量级MLP头。将所有图像patch直接输入编码器,包括masked和visible patches。相比之下,在MAE中,只有可见的patch被馈送到编码器。实验表明,这些结构变化为小窗口的局部掩蔽重建带来了更大的性能增益。
经过广泛的实验,作者发现

  1. LoMaR在ImageNet-1K数据集上可以实现84.1 top-1 acc,比MAE高出0.5 acc。此外,LoMaR的性能可以进一步提高到84.3 acc,在ViT B/8主干上只需预训练400个阶段,与ViT B/16相比,这不会带来额外的预训练成本。在分辨率为384×384的图像上对预训练模型进行微调后,LoMaR可以达到85.4 acc,比MAE高出0.6 acc。
  2. LoMaR在高分辨率图像预训练中比其他baseline更有效,因为它的计算量对不同的图像分辨率是不变的。然而,其他方法的计算成本是图像分辨率增加的二次方,这导致了昂贵的预训练。比如,对于448×448图像的预训练,LoMaR比MAE快3.1倍,实现了更高的分类性能。
  3. LoMaR是一种高效的学习方法,可以很容易地集成到任何其他生成性自监督学习方法中。将本文的局部掩蔽重建学习机制安装到BEiT中可以将其ImageNet-1K分类性能从83.2提高到83.4,只消耗最初预训练时间的35.8%。LoMaR在其他任务(如目标检测)上也具有很强的泛化能力。在ViTDet的目标检测框架下,它比MAE的性能高出0.5 。

LoMaR依赖于一堆Transformer块,通过从与MAE类似的损坏图像中恢复缺失的patch来预训练大量未标记图像,但LoMaR在几个关键位置将其与MAE区分开来。上图并排比较了两者。在本节中,作者首先回顾MAE模型,然后描述LoMaR和MAE之间的差异。

Architecture

LoMaR采用了一种简单的编码器-编码器结构,而不是MAE的非对称编码器-解码器。作者将采样区域下所有可见和mask的patch输入编码器。虽然将mask patch输入编码器可能被认为是比仅将mask patch输入解码器的MAE效率更低的操作,但作者发现,在早期阶段输入mask patch可以增强视觉表现,并使其对较小的窗口大小更具鲁棒性。这可能是因为编码器可以在多个编码器层与其他可见patch交互后,将mask patch转换回其原始RGB表示。隐藏层中恢复的mask patch可以隐式地对图像表示作出贡献。因此,本文在LoMaR中保留mask patch作为编码器输入。

Relative positional encoding

LoMaR在MAE中应用相对位置编码(RPE)而不是绝对位置编码。作者应用了上下文RPE,在计算自注意时,它为每个查询i和键j引入了一个可学习的向量。

Implementation

给定一幅图像,首先将其划分为几个不重叠的patch。每个patch线性投影到嵌入中。作者在不同的空间位置随机抽取几个方形的K×K 个patch。然后,将每个窗口中固定百分比的patch归零。然后,将所有patch从每个窗口按顺序提供给编码器。编码器在自注意层中应用可学习的相对位置编码。作者用一个简单的MLP头将编码器输出的潜在表示转换回其原始特征维,然后用归一化的ground-truth图像计算均方误差。

自监督学习(SSL)可以从大量未标记数据的训练中获益。然而,在大规模的预训练下,它们的高计算要求仍然是一个值得关注的问题。在本文的研究中,作者观察到用于生成SSL的局部掩蔽重建(LoMaR)比MAE和BEiT等有影响力的著作使用的全局版本更有效。
LoMaR在图像分类、实例分割和目标检测方面具有良好的泛化能力;它可以很容易地合并到MAE和BEiT中。LoMaR有希望将SSL扩展到更大的数据集和更高的分辨率,以及计算更密集的数据集,如视频。LoMaR的另一个优点在于,当图像patch数量增加时,效率会提高。
主要原因是LoMaR限制了局部窗口内的自注意,其计算复杂度随每幅图像的采样窗口数呈线性增长。此特性可以在高图像分辨率下进行有效的预训练,而对于其他SSL方法来说,这将非常昂贵。它可以使许多视觉任务受益,例如需要在像素级进行密集预测的对象检测或实例分割。尽管LoMaR相对于其他高分辨率图像基线的预训练效率增益很高,但与MAE相比,LoMaR相对于低分辨率图像的效率提高有限。

ViTDet:只用普通ViT,不做分层设计也能搞定目标检测

论文链接:https://arxiv.org/abs/2203.16527

代码(已开源):https://github.com/facebookresearch/detectron2/tree/main/projects/ViTDet

当前的目标检测器通常由一个与检测任务无关的主干特征提取器和一组包含检测专用先验知识的颈部和头部组成。颈部/头部中的常见组件可能包括感兴趣区域(RoI)操作、区域候选网络(RPN)或锚、特征金字塔网络(FPN)等。如果用于特定任务的颈部/头部的设计与主干的设计解耦,它们可以并行发展。从经验上看,目标检测研究受益于对通用主干和检测专用模块的大量独立探索。长期以来,由于卷积网络的实际设计,这些主干一直是多尺度、分层的架构,这严重影响了用于多尺度(如 FPN)目标检测的颈/头的设计。

在过去的一年里,视觉 Transformer(ViT)已经成为视觉识别的强大支柱。与典型的 ConvNets 不同,最初的 ViT 是一种简单的、非层次化的架构,始终保持单一尺度的特征图。它的「极简」追求在应用于目标检测时遇到了挑战,例如,我们如何通过上游预训练的简单主干来处理下游任务中的多尺度对象?简单 ViT 用于高分辨率图像检测是否效率太低?放弃这种追求的一个解决方案是在主干中重新引入分层设计。这种解决方案,例如 Swin Transformer 和其他网络,可以继承基于 ConvNet 的检测器设计,并已取得成功。

在这项工作中,何恺明等研究者追求的是一个不同的方向:探索仅使用普通、非分层主干的目标检测器。如果这一方向取得成功,仅使用原始 ViT 主干进行目标检测将成为可能。在这一方向上,预训练设计将与微调需求解耦,上游与下游任务的独立性将保持,就像基于 ConvNet 的研究一样。这一方向也在一定程度上遵循了 ViT 的理念,即在追求通用特征的过程中减少归纳偏置。由于非局部自注意力计算可以学习平移等变特征,它们也可以从某种形式的监督或自我监督预训练中学习尺度等变特征。

研究者表示,在这项研究中,他们的目标不是开发新的组件,而是通过最小的调整克服上述挑战。具体来说,他们的检测器仅从一个普通 ViT 主干的最后一个特征图构建一个简单的特征金字塔(如图 1 所示)。这一方案放弃了 FPN 设计和分层主干的要求。为了有效地从高分辨率图像中提取特征,他们的检测器使用简单的非重叠窗口注意力(没有 shifting)。他们使用少量的跨窗口块来传播信息,这些块可以是全局注意力或卷积。这些调整只在微调过程中进行,不会改变预训练。

本文贡献:

(1) 提出了一种仅使用普通、非分层backbone(ViT)的目标检测器为ViTDet,可以与领先的分层backbone检测器(例如,Swin、MViT)竞争,仅使用没有标签的 ImageNet-1K 预训练就能超过ImageNet-21K 预训练的分层backbone检测器。

(2) 在普通的 ViT backbone,舍弃了FPN 模块,而仅仅使用单尺度featur map进行操作。

(3) 在ViT backbone上应用window attention解决在面对高分辨率图像时,处理效率低下问题,并且在之后仅使用少量的cross-window blocks。

(4) 我们的方法保持了将检测模块特定设计与任务不可知的backbone分离的理念,检测模块的先验知识仅在微调期间引入,无需在预训练中先验地调整backbone设计。(个人理解:比如需要根据目标尺寸大小人为设定FPN层数,分层结构等)

方法细节

该研究的目标是消除对主干网络的分层约束,并使用普通主干网络进行目标检测。因此,该研究的目标是用最少的改动,让简单的主干网络在微调期间适应目标检测任务。经过改动之后,原则上我们可以应用任何检测器头(detector head),研究者选择使用 Mask R-CNN 及其扩展。

简单的特征金字塔

FPN 是构建用于目标检测的 in-network 金字塔的常见解决方案。如果主干网络是分层的,FPN 的动机就是将早期高分辨率的特征和后期更强的特征结合起来。这在 FPN 中是通过自上而下(top-down)和横向连接来实现的,如图 1 左所示。

如果主干网络不是分层网络,那么 FPN 动机的基础就会消失,因为主干网络中的所有特征图都具有相同的分辨率。该研究仅使用主干网络中的最后一张特征图,因为它应该具有最强大的特征。研究者对最后一张特征图并行应用一组卷积或反卷积来生成多尺度特征图。具体来说,他们使用的是尺度为 1/16(stride = 16 )的默认 ViT 特征图,该研究可如图 1 右所示,这个过程被称为「简单的特征金字塔」。

从单张特征图构建多尺度特征图的策略与 SSD 的策略有关,但该研究的场景涉及对深度、低分辨率的特征图进行上采样。在分层主干网络中,上采样通常用横向连接进行辅助,但研究者通过实验发现,在普通 ViT 主干网络中横向连接并不是必需的,简单的反卷积就足够了。研究者猜想这是因为 ViT 可以依赖位置嵌入来编码位置,并且高维 ViT patch 嵌入不一定会丢弃信息。如下图所示,该研究将这种简单的特征金字塔与同样建立在普通主干网络上的两个 FPN 变体进行比较。在第一个变体中,主干网络被人为地划分为多个阶段,以模仿分层主干网络的各个阶段,并应用横向和自上而下的连接(图 2(a))。第二个变体与第一个变体类似,但仅使用最后一张特征图(图 2(b))。该研究表明这些 FPN 变体不是必需的。

主干网络调整

目标检测器受益于高分辨率输入图像,但在整个主干网络中,计算全局自注意力对于内存的要求非常高,而且速度很慢。该研究重点关注预训练主干网络执行全局自注意力的场景,然后在微调期间适应更高分辨率的输入。这与最近使用主干网络预训练直接修改注意力计算的方法形成对比。该研究的场景使得研究者能够使用原始 ViT 主干网络进行检测,而无需重新设计预训练架构。该研究探索了使用跨窗口块的窗口注意力。在微调期间,给定高分辨率特征图,该研究将其划分为常规的非重叠窗口。在每个窗口内计算自注意力,这在原始 Transformer 中被称为「受限」自注意力。与 Swin 不同,该方法不会跨层「移动(shift)」窗口。为了允许信息传播,该研究使用了极少数(默认为 4 个)可跨窗口的块。研究者将预训练的主干网络平均分成 4 个块的子集(例如对于 24 块的 ViT-L,每个子集中包含 6 个),并在每个子集的最后一个块中应用传播策略。研究者分析了如下两种策略:

  • 全局传播。该策略在每个子集的最后一个块中执行全局自注意力。由于全局块的数量很少,内存和计算成本是可行的。这类似于(Li et al., 2021 )中与 FPN 联合使用的混合窗口注意力。
  • 卷积传播。该策略在每个子集之后添加一个额外的卷积块来作为替代。卷积块是一个残差块,由一个或多个卷积和一个 identity shortcut 组成。该块中的最后一层被初始化为零,因此该块的初始状态是一个 identity。将块初始化为 identity 使得该研究能够将其插入到预训练主干网络中的任何位置,而不会破坏主干网络的初始状态。

这种主干网络的调整非常简单,并且使检测微调与全局自注意力预训练兼容,也就没有必要重新设计预训练架构。

51×51超大卷积核,超越 ConvNeXt、RepLKNet的创新方式

以下文章来源于微信公众号:集智书童

作者:ChaucerG

原文链接:https://mp.weixin.qq.com/s/MYU82hGH47-2JUl13MmgmA

本文仅用于学术分享,如有侵权,请联系后台作删文处理

自从Vision Transformers (ViT) 出现以来,Transformers迅速在计算机视觉领域大放异彩。卷积神经网络 (CNN) 的主导作用似乎受到越来越有效的基于Transformer的模型的挑战。最近,一些先进的卷积模型使用受局部大注意力机制驱动设计了大Kernel的卷积模块进行反击,并显示出吸引人的性能和效率。其中之一,即 RepLKNet,以改进的性能成功地将Kernel-size扩展到 31×31,但与 Swin Transformer 等高级 ViT 的扩展趋势相比,随着Kernel-size的持续增长,性能开始饱和。

在本文中,作者探索了训练大于 31×31 的极端卷积的可能性,并测试是否可以通过策略性地扩大卷积来消除性能差距。这项研究最终得到了一个从稀疏性的角度应用超大kernel的方法,它可以平滑地将kernel扩展到 61×61,并具有更好的性能。基于这个方法,作者提出了Sparse Large Kernel Network(SLaK),这是一种配备 51×51 kernel-size的纯 CNN 架构,其性能可以与最先进的分层 Transformer 和现代 ConvNet 架构(如 ConvNeXt 和 RepLKNet,关于 ImageNet 分类以及典型的下游任务。

1应用超过 31×31 的超大卷积核

作者首先研究了大于 31×31 的极端Kernel-size的性能,并总结了3个主要观察结果。这里作者以 ImageNet-1K 上最近开发的 CNN 架构 ConvNeXt 作为进行这项研究的 benchmark

作者关注最近使用 MixupCutmixRandAugment 和 Random Erasing 作为数据增强的作品。随机深度标签平滑作为正则化应用,具有与 ConvNeXt 中相同的超参数。用 AdamW 训练模型。在本节中,所有模型都针对 120 个 epoch 的长度进行了训练,以仅观察大Kernel-size的缩放趋势。

观察1:现有的技术不能扩展卷积超过31×31

最近,RepLKNet 通过结构重新参数化成功地将卷积扩展到 31×31。作者进一步将Kernel-size增加到 51×51 和 61×61,看看更大的kernel是否能带来更多的收益。按照RepLKNet中的设计,依次将每个阶段的Kernel-size设置为[51,49,47,13]和[61,59,57,13]。

测试精度如表 1 所示。正如预期的那样,将Kernel-size从 7×7 增加到 31×31 会显着降低性能,而 RepLKNet 可以克服这个问题,将精度提高 0.5%。然而,这种趋势不适用于较大的kernel,因为将Kernel-size增加到 51×51 开始损害性能。

一种合理的解释是,虽然感受野可以通过使用非常大的kernel,如51×5161×61来扩大感受野,但它可能无法保持某些理想的特性,如局部性。由于标准ResNetConvNeXt中的stem cell导致输入图像的4×降采样,具有51×51的极端核已经大致等于典型的224×224 ImageNet的全局卷积。因此,这一观察结果是有意义的,因为在ViTs的类似机制中,局部注意力通常优于全局注意力。在此基础上,通过引入局部性来解决这个问题的机会,同时保留了捕获全局关系的能力。

观察2:将一个方形的大kernel分解为2个矩形的parallel kernels,可以将Kernel-size平滑地缩放到 61。

虽然使用中等大小的卷积(例如,31×31)似乎可以直接避免这个问题,但作者就是想看看是否可以通过使用(全局)极端卷积来进一步推动cnn的性能。作者这里使用的方法是用2个平行和矩形卷积的组合来近似大的 M×M kernel,它们的Kernel-size分别为M×NN×M(其中N<M),如图1所示。在RepLKNet之后,保持一个5×5层与大kernel并行,并在一个批处理范数层之后汇总它们的输出。

这种分解不仅继承了大kernel捕获远程依赖关系的能力,而且可以提取边缘较短的局部上下文特征。更重要的是,随着深度Kernel-size的增加,现有的大kernel训练技术会受到二次计算和内存开销的影响。

与之形成鲜明对比的是,这种方法的开销随着Kernel-size线性增加(图 4)。N = 5 的kernel分解的性能在表 2 中报告为“分解”组。由于分解减少了 FLOP,与具有中等kernel的结构重新参数化 (RepLKNet) 相比,预计网络会牺牲一些准确性,即 31×31。然而,随着卷积大小增加到全局卷积,它可以惊人地将Kernel-size扩展到 61 并提高性能。

观察3:“use sparse groups, expand more”显着提高了模型的容量

最近提出的 ConvNeXt 重新访问了ResNeXt中引入的原理,该原理将卷积滤波器分成小但更多的组。ConvNeXt没有使用标准的组卷积,而是简单地使用增加宽度的深度卷积来实现“use more groups, expand width”的目标。在本文中,作者试图从另一个替代的角度来扩展这一原则——“use sparse groups, expand more”。

具体来说,首先用稀疏卷积代替密集卷积,其中稀疏核是基于SNIP的分层稀疏比随机构造的。构建完成后,用动态稀疏度训练稀疏模型,其中稀疏权值在训练过程中通过修剪最小幅值的权值,随机增加相同数量的权值进行动态调整。这样做可以动态地适应稀疏权值,从而获得更好的局部特征。

由于kernel在整个训练过程中都是稀疏的,相应的参数计数和训练/推理流只与密集模型成正比。为了评估,以40%的稀疏度稀疏化分解后的kernel,并将其性能报告为“稀疏分解”组。可以在表2的中间一列中观察到,动态稀疏性显着降低了FLOPs超过2.0G,导致了暂时的性能下降。

接下来,作者证明了上述动态稀疏性的高效率可以有效地转移到模型的可扩展性中。动态稀疏性允许能够友好地扩大模型的规模。例如,使用相同的稀疏性(40%),可以将模型宽度扩展1.3×,同时保持参数计数和FLOPs与密集模型大致相同。这带来了显着的性能提高,在极端的51×51 kernel下,性能从81.3%提高到81.6%。令人印象深刻的是,本文方法配备了61×61内核,性能超过了之前的RepLKNet,同时节省了55%的FLOPs。

2 Sparse Large Kernel Network – SLaK

到目前为止,已经发现了本文的方法可以成功地将Kernel-size扩展到61,而不需要反向触发性能。它包括2个受稀疏性启发的设计。

在宏观层面上,构建了一个本质稀疏网络,并进一步扩展网络,以提高在保持相似模型规模的同时的网络容量。

在微观层面上,将一个密集的大kernel分解为2个具有动态稀疏性的互补kernel,以提高大kernel的可扩展性。

与传统的训练后剪枝不同,直接从头开始训练的网络,而不涉及任何预训练或微调。在此基础上提出了Sparse Large Kernel Network(SLaK),这是一种纯CNN架构,使用了极端的51×51 kernel

SLaK 是基于 ConvNeXt 的架构构建的。阶段计算比和干细胞的设计继承自ConvNeXt。每个阶段的块数对于 SLaK-T 为 [3, 3, 9, 3],对于 SLaK-S/B 为 [3, 3, 27, 3]。stem cell只是一个具有 kernel-size为4×4和stride=4的卷积层。作者将 ConvNeXt 阶段的Kernel-size分别增加到 [51, 49, 47, 13],并将每个 M×M kernel替换为 M×5 和 5×M kernel的组合,如图 1 所示。作者发现添加在对输出求和之前,直接在每个分解的kernel之后的 BatchNorm 层是至关重要的。

遵循 “use sparse groups, expand more”的指导方针,进一步稀疏整个网络,将阶段宽度扩大 1.3 倍,最终得到 SLaK-T/S/B。尽管知道通过调整模型宽度和稀疏度之间的权衡来提高 SLaK 的性能有很大的空间,但为了简单起见,将所有模型的宽度保持为 1.3 倍。所有模型的稀疏度设置为 40%。

虽然模型配置了极端的 51×51 kernel,但整体参数计数和 FLOP 并没有增加太多,并且由于RepLKNet提供的出色实现,它在实践中非常有效。

3实验

3.1 分类实验

3.2 语义分割

4参考

[1].More ConvNets in the 2020s : Scaling up Kernels Beyond 51 × 51 using Sparsity.

MAE–transformer模型预训练

假设我们想从图像中识别出不同种类的椅子,然后将购买链接推荐给用户。一种可能的方法是先找出100种常见的椅子,为每种椅子拍摄1,000张不同角度的图像,然后在收集到的图像数据集上训练一个分类模型。这个椅子数据集虽然可能比Fashion-MNIST数据集要庞大,但样本数仍然不及ImageNet数据集中样本数的十分之一。这可能会导致适用于ImageNet数据集的复杂模型在这个椅子数据集上过拟合。同时,因为数据量有限,最终训练得到的模型的精度也可能达不到实用的要求。

为了应对上述问题,一个显而易见的解决办法是收集更多的数据。然而,收集和标注数据会花费大量的时间和资金。例如,为了收集ImageNet数据集,研究人员花费了数百万美元的研究经费。虽然目前的数据采集成本已降低了不少,但其成本仍然不可忽略。

另外一种解决办法是应用迁移学习(transfer learning),将从源数据集学到的知识迁移到目标数据集上。例如,虽然ImageNet数据集的图像大多跟椅子无关,但在该数据集上训练的模型可以抽取较通用的图像特征,从而能够帮助识别边缘、纹理、形状和物体组成等。这些类似的特征对于识别椅子也可能同样有效。

本节我们介绍迁移学习中的一种常用技术:微调(fine tuning)。如图9.1所示,微调由以下4步构成。

  1. 在源数据集(如ImageNet数据集)上预训练一个神经网络模型,即源模型。
  2. 创建一个新的神经网络模型,即目标模型。它复制了源模型上除了输出层外的所有模型设计及其参数。我们假设这些模型参数包含了源数据集上学习到的知识,且这些知识同样适用于目标数据集。我们还假设源模型的输出层跟源数据集的标签紧密相关,因此在目标模型中不予采用。
  3. 为目标模型添加一个输出大小为目标数据集类别个数的输出层,并随机初始化该层的模型参数。
  4. 在目标数据集(如椅子数据集)上训练目标模型。我们将从头训练输出层,而其余层的参数都是基于源模型的参数微调得到的。

当目标数据集远小于源数据集时,微调有助于提升模型的泛化能力。

代码实现微调:

pretrained_net = models.resnet18(pretrained=True)
pretrained_net.load_state_dict(torch.load('/home/kesci/input/resnet185352/resnet18-5c106cde.pth'))

下面打印源模型的成员变量fc。作为一个全连接层,它将ResNet最终的全局平均池化层输出变换成ImageNet数据集上1000类的输出。

print(pretrained_net.fc)

输出:Linear(in_features=512, out_features=1000, bias=True)

可见此时pretrained_net最后的输出个数等于目标数据集的类别数1000。所以我们应该将最后的fc成修改我们需要的输出类别数:

pretrained_net.fc = nn.Linear(512, 2)
print(pretrained_net.fc)

此时,pretrained_netfc层就被随机初始化了,但是其他层依然保存着预训练得到的参数。由于是在很大的ImageNet数据集上预训练的,所以参数已经足够好,因此一般只需使用较小的学习率来微调这些参数,而fc中的随机初始化参数一般需要更大的学习率从头训练。PyTorch可以方便的对模型的不同部分设置不同的学习参数,我们在下面代码中将fc的学习率设为已经预训练过的部分的10倍。

output_params = list(map(id, pretrained_net.fc.parameters()))
feature_params = filter(lambda p: id(p) not in output_params, pretrained_net.parameters())

lr = 0.01
optimizer = optim.SGD([{'params': feature_params},
                       {'params': pretrained_net.fc.parameters(), 'lr': lr * 10}],
                       lr=lr, weight_decay=0.001)

记录: 在MAE的微调训练中,提供了两种 微调

  1. Linear probing: 锁死transformer的参数,只训练CIFAR10的那个Linear层。
  2. Fine-tuning: 接着训练transformer的参数,同时也训练CIFAR10的那个Linear。

论文做了MAE各个部分的不同设置对比实验,这些实验能够揭示MAE更多的特性。首先是masking ratio,从下图可以看到,最优的设置是75%的masking ratio,此时linear probing和finetune效果最好,这比之前的研究要高很多,比如BEiT的masking ratio是40%。另外也可以看到linear probing和finetune的表现不一样,linear probing效果随着masking ratio的增加逐渐提高直至一个峰值后出现下降,而finetune效果在不同making ratio下差异小,masking ratio在40%~80%范围内均能表现较好。 ​

preview

torch.meshgrid()函数解析

最近看到很多论文里都有这个函数(yolov3 以及最近大火的swin transformer),记录下函数的使用:

https://pytorch.org/docs/stable/generated/torch.meshgrid.html

说明:

  torch.meshgrid()的功能是生成网格,可以用于生成坐标。

函数输入:

  输入两个数据类型相同的一维tensor

函数输出:

       输出两个tensor(tensor行数为第一个输入张量的元素个数,列数为第二个输入张量的元素个数)

注意:

  1)当两个输入tensor数据类型不同或维度不是一维时会报错。

  2)其中第一个输出张量填充第一个输入张量中的元素,各行元素相同;第二个输出张量填充第二个输入张量中的元素各列元素相同。

>>> x = torch.tensor([1, 2, 3])
>>> y = torch.tensor([4, 5, 6])

Observe the element-wise pairings across the grid, (1, 4),
(1, 5), ..., (3, 6). This is the same thing as the
cartesian product.
>>> grid_x, grid_y = torch.meshgrid(x, y, indexing='ij')
>>> grid_x
tensor([[1, 1, 1],
        [2, 2, 2],
        [3, 3, 3]])
>>> grid_y
tensor([[4, 5, 6],
        [4, 5, 6],
        [4, 5, 6]])
# 【1】
import torch
a = torch.tensor([1, 2, 3, 4])
print(a)
b = torch.tensor([4, 5, 6])
print(b)
x, y = torch.meshgrid(a, b)
print(x)
print(y)
 
结果显示:
tensor([1, 2, 3, 4])
tensor([4, 5, 6])
tensor([[1, 1, 1],
        [2, 2, 2],
        [3, 3, 3],
        [4, 4, 4]])
tensor([[4, 5, 6],
        [4, 5, 6],
        [4, 5, 6],
        [4, 5, 6]])
 
 
 
# 【2】
import torch
a = torch.tensor([1, 2, 3, 4, 5, 6])
print(a)
b = torch.tensor([7, 8, 9, 10])
print(b)
x, y = torch.meshgrid(a, b)
print(x)
print(y)
 
结果显示:
tensor([1, 2, 3, 4, 5, 6])
tensor([ 7,  8,  9, 10])
tensor([[1, 1, 1, 1],
        [2, 2, 2, 2],
        [3, 3, 3, 3],
        [4, 4, 4, 4],
        [5, 5, 5, 5],
        [6, 6, 6, 6]])
tensor([[ 7,  8,  9, 10],
        [ 7,  8,  9, 10],
        [ 7,  8,  9, 10],
        [ 7,  8,  9, 10],
        [ 7,  8,  9, 10],
        [ 7,  8,  9, 10]])

PyTorch中model.modules(), model.named_modules(), model.children(), model.named_children(), model.parameters(), model.named_parameters(), model.state_dict()

本文通过一个例子实验来观察并讲解PyTorch中model.modules(), model.named_modules(), model.children(), model.named_children(), model.parameters(), model.named_parameters(), model.state_dict()这些model实例方法的返回值。例子如下:

import torch 
import torch.nn as nn 

class Net(nn.Module):

    def __init__(self, num_class=10):
        super().__init__()
        self.features = nn.Sequential(
            nn.Conv2d(in_channels=3, out_channels=6, kernel_size=3),
            nn.BatchNorm2d(6),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),
            nn.Conv2d(in_channels=6, out_channels=9, kernel_size=3),
            nn.BatchNorm2d(9),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2)
        )

        self.classifier = nn.Sequential(
            nn.Linear(9*8*8, 128),
            nn.ReLU(inplace=True),
            nn.Dropout(),
            nn.Linear(128, num_class)
        )

    def forward(self, x):
        output = self.features(x)
        output = output.view(output.size()[0], -1)
        output = self.classifier(output)
    
        return output

model = Net()

如上代码定义了一个由两层卷积层,两层全连接层组成的网络模型。值得注意的是,这个Net由外到内有3个层次:

Net:

----features:

------------Conv2d
------------BatchNorm2d
------------ReLU
------------MaxPool2d
------------Conv2d
------------BatchNorm2d
------------ReLU
------------MaxPool2d

----classifier:

------------Linear
------------ReLU
------------Dropout
------------Linear

网络Net本身是一个nn.Module的子类,它又包含了features和classifier两个由Sequential容器组成的nn.Module子类,features和classifier各自又包含众多的网络层,它们都属于nn.Module子类,所以从外到内共有3个层次。
下面我们来看这几个实例方法的返回值都是什么。

In [7]: model.named_modules()                                                                                                       
Out[7]: <generator object Module.named_modules at 0x7f5db88f3840>

In [8]: model.modules()                                                         
Out[8]: <generator object Module.modules at 0x7f5db3f53c00>

In [9]: model.children()                                                        
Out[9]: <generator object Module.children at 0x7f5db3f53408>

In [10]: model.named_children()                                                 
Out[10]: <generator object Module.named_children at 0x7f5db80305e8>

In [11]: model.parameters()                                                     
Out[11]: <generator object Module.parameters at 0x7f5db3f534f8>

In [12]: model.named_parameters()                                               
Out[12]: <generator object Module.named_parameters at 0x7f5d42da7570>

In [13]: model.state_dict()                                                     
Out[13]: 
OrderedDict([('features.0.weight', tensor([[[[ 0.1200, -0.1627, -0.0841],
                        [-0.1369, -0.1525,  0.0541],
                        [ 0.1203,  0.0564,  0.0908]],
                      ……
          

可以看出,除了model.state_dict()返回的是一个字典,其他几个方法返回值都显示的是一个生成器,是一个可迭代变量,我们通过列表推导式用for循环将返回值取出来进一步进行观察:

In [14]: model_modules = [x for x in model.modules()]                                                                                

In [15]: model_named_modules = [x for x in model.named_modules()]        

In [16]: model_children = [x for x in model.children()]                                                                              

In [17]: model_named_children = [x for x in model.named_children()]                                                                  

In [18]: model_parameters = [x for x in model.parameters()]                                                                          

In [19]: model_named_parameters = [x for x in model.named_parameters()]
1. model.modules()

model.modules()迭代遍历模型的所有子层,所有子层即指nn.Module子类,在本文的例子中,Net(), features(), classifier(),以及nn.xxx构成的卷积,池化,ReLU, Linear, BN, Dropout等都是nn.Module子类,也就是model.modules()会迭代的遍历它们所有对象。我们看一下列表model_modules:

In [20]: model_modules                                                                                                               
Out[20]: 
[Net(
   (features): Sequential(
     (0): Conv2d(3, 6, kernel_size=(3, 3), stride=(1, 1))
     (1): BatchNorm2d(6, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
     (2): ReLU(inplace)
     (3): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
     (4): Conv2d(6, 9, kernel_size=(3, 3), stride=(1, 1))
     (5): BatchNorm2d(9, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
     (6): ReLU(inplace)
     (7): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
   )
   (classifier): Sequential(
     (0): Linear(in_features=576, out_features=128, bias=True)
     (1): ReLU(inplace)
     (2): Dropout(p=0.5)
     (3): Linear(in_features=128, out_features=10, bias=True)
   )
 ), 
Sequential(
   (0): Conv2d(3, 6, kernel_size=(3, 3), stride=(1, 1))
   (1): BatchNorm2d(6, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
   (2): ReLU(inplace)
   (3): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
   (4): Conv2d(6, 9, kernel_size=(3, 3), stride=(1, 1))
   (5): BatchNorm2d(9, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
   (6): ReLU(inplace)
   (7): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
 ), 
Conv2d(3, 6, kernel_size=(3, 3), stride=(1, 1)), 
BatchNorm2d(6, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True), 
ReLU(inplace), 
MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False), 
Conv2d(6, 9, kernel_size=(3, 3), stride=(1, 1)), 
BatchNorm2d(9, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True), 
ReLU(inplace), 
MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False), 
Sequential(
   (0): Linear(in_features=576, out_features=128, bias=True)
   (1): ReLU(inplace)
   (2): Dropout(p=0.5)
   (3): Linear(in_features=128, out_features=10, bias=True)
 ), 
Linear(in_features=576, out_features=128, bias=True), 
ReLU(inplace), 
Dropout(p=0.5), 
Linear(in_features=128, out_features=10, bias=True)]

In [21]: len(model_modules)                                                                                                          
Out[21]: 15

可以看出,model_modules列表中共有15个元素,首先是整个Net,然后遍历了Net下的features子层,进一步遍历了feature下的所有层,然后又遍历了classifier子层以及其下的所有层。所以说model.modules()能够迭代地遍历模型的所有子层。

2. model.named_modules()

顾名思义,它就是有名字的model.modules()。model.named_modules()不但返回模型的所有子层,还会返回这些层的名字:

In [28]: len(model_named_modules)                                                                                                    
Out[28]: 15

In [29]: model_named_modules                                                                                                         
Out[29]: 
[('', Net(
    (features): Sequential(
      (0): Conv2d(3, 6, kernel_size=(3, 3), stride=(1, 1))
      (1): BatchNorm2d(6, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (2): ReLU(inplace)
      (3): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
      (4): Conv2d(6, 9, kernel_size=(3, 3), stride=(1, 1))
      (5): BatchNorm2d(9, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (6): ReLU(inplace)
      (7): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    )
    (classifier): Sequential(
      (0): Linear(in_features=576, out_features=128, bias=True)
      (1): ReLU(inplace)
      (2): Dropout(p=0.5)
      (3): Linear(in_features=128, out_features=10, bias=True)
    )
  )), 
('features', Sequential(
    (0): Conv2d(3, 6, kernel_size=(3, 3), stride=(1, 1))
    (1): BatchNorm2d(6, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (2): ReLU(inplace)
    (3): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (4): Conv2d(6, 9, kernel_size=(3, 3), stride=(1, 1))
    (5): BatchNorm2d(9, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (6): ReLU(inplace)
    (7): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  )), 
('features.0', Conv2d(3, 6, kernel_size=(3, 3), stride=(1, 1))), 
('features.1', BatchNorm2d(6, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)), ('features.2', ReLU(inplace)), 
('features.3', MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)), 
('features.4', Conv2d(6, 9, kernel_size=(3, 3), stride=(1, 1))), 
('features.5', BatchNorm2d(9, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)), ('features.6', ReLU(inplace)), 
('features.7', MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)), 
('classifier',
  Sequential(
    (0): Linear(in_features=576, out_features=128, bias=True)
    (1): ReLU(inplace)
    (2): Dropout(p=0.5)
    (3): Linear(in_features=128, out_features=10, bias=True)
  )), 
('classifier.0', Linear(in_features=576, out_features=128, bias=True)), 
('classifier.1', ReLU(inplace)), 
('classifier.2', Dropout(p=0.5)), 
('classifier.3', Linear(in_features=128, out_features=10, bias=True))]

可以看出,model.named_modules()也遍历了15个元素,但每个元素都有了自己的名字,从名字可以看出,除了在模型定义时有命名的features和classifier,其它层的名字都是PyTorch内部按一定规则自动命名的。返回层以及层的名字的好处是可以按名字通过迭代的方法修改特定的层,如果在模型定义的时候就给每个层起了名字,比如卷积层都是conv1,conv2…的形式,那么我们可以这样处理:

for name, layer in model.named_modules():
    if 'conv' in name:
        对layer进行处理

当然,在没有返回名字的情形中,采用isinstance()函数也可以完成上述操作:

for layer in model.modules():
    if isinstance(layer, nn.Conv2d):
        对layer进行处理
3. model.children()

如果把这个网络模型Net按层次从外到内进行划分的话,features和classifier是Net的子层,而conv2d, ReLU, BatchNorm, Maxpool2d这些有时features的子层, Linear, Dropout, ReLU等是classifier的子层,上面的model.modules()不但会遍历模型的子层,还会遍历子层的子层,以及所有子层。
而model.children()只会遍历模型的子层,这里即是features和classifier。

In [22]: len(model_children)                                                                                                         
Out[22]: 2

In [22]: model_children                                                                                                              
Out[22]: 
[Sequential(
   (0): Conv2d(3, 6, kernel_size=(3, 3), stride=(1, 1))
   (1): BatchNorm2d(6, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
   (2): ReLU(inplace)
   (3): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
   (4): Conv2d(6, 9, kernel_size=(3, 3), stride=(1, 1))
   (5): BatchNorm2d(9, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
   (6): ReLU(inplace)
   (7): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
 ), 
Sequential(
   (0): Linear(in_features=576, out_features=128, bias=True)
   (1): ReLU(inplace)
   (2): Dropout(p=0.5)
   (3): Linear(in_features=128, out_features=10, bias=True)
 )]

可以看出,它只遍历了两个元素,即features和classifier。

4. model.named_children()

model.named_children()就是带名字的model.children(), 相比model.children(), model.named_children()不但迭代的遍历模型的子层,还会返回子层的名字:

In [23]: len(model_named_children)                                                                                                   
Out[23]: 2

In [24]: model_named_children                                                                                                        
Out[24]: 
[('features', Sequential(
    (0): Conv2d(3, 6, kernel_size=(3, 3), stride=(1, 1))
    (1): BatchNorm2d(6, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (2): ReLU(inplace)
    (3): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (4): Conv2d(6, 9, kernel_size=(3, 3), stride=(1, 1))
    (5): BatchNorm2d(9, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (6): ReLU(inplace)
    (7): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  )), 
('classifier', Sequential(
    (0): Linear(in_features=576, out_features=128, bias=True)
    (1): ReLU(inplace)
    (2): Dropout(p=0.5)
    (3): Linear(in_features=128, out_features=10, bias=True)
  ))]

对比上面的model.children(), 这里的model.named_children()还返回了两个子层的名称:features 和 classifier .

5. model.parameters()

迭代地返回模型的所有参数。

In [30]: len(model_parameters)                                                                                                       
Out[30]: 12

In [31]: model_parameters                                                                                                            
Out[31]: 
[Parameter containing:
 tensor([[[[ 0.1200, -0.1627, -0.0841],
           [-0.1369, -0.1525,  0.0541],
           [ 0.1203,  0.0564,  0.0908]],
           ……
          [[-0.1587,  0.0735, -0.0066],
           [ 0.0210,  0.0257, -0.0838],
           [-0.1797,  0.0675,  0.1282]]]], requires_grad=True),
 Parameter containing:
 tensor([-0.1251,  0.1673,  0.1241, -0.1876,  0.0683,  0.0346],
        requires_grad=True),
 Parameter containing:
 tensor([0.0072, 0.0272, 0.8620, 0.0633, 0.9411, 0.2971], requires_grad=True),
 Parameter containing:
 tensor([0., 0., 0., 0., 0., 0.], requires_grad=True),
 Parameter containing:
 tensor([[[[ 0.0632, -0.1078, -0.0800],
           [-0.0488,  0.0167,  0.0473],
           [-0.0743,  0.0469, -0.1214]],
           …… 
          [[-0.1067, -0.0851,  0.0498],
           [-0.0695,  0.0380, -0.0289],
           [-0.0700,  0.0969, -0.0557]]]], requires_grad=True),
 Parameter containing:
 tensor([-0.0608,  0.0154,  0.0231,  0.0886, -0.0577,  0.0658, -0.1135, -0.0221,
          0.0991], requires_grad=True),
 Parameter containing:
 tensor([0.2514, 0.1924, 0.9139, 0.8075, 0.6851, 0.4522, 0.5963, 0.8135, 0.4010],
        requires_grad=True),
 Parameter containing:
 tensor([0., 0., 0., 0., 0., 0., 0., 0., 0.], requires_grad=True),
 Parameter containing:
 tensor([[ 0.0223,  0.0079, -0.0332,  ..., -0.0394,  0.0291,  0.0068],
         [ 0.0037, -0.0079,  0.0011,  ..., -0.0277, -0.0273,  0.0009],
         [ 0.0150, -0.0110,  0.0319,  ..., -0.0110, -0.0072, -0.0333],
         ...,
         [-0.0274, -0.0296, -0.0156,  ...,  0.0359, -0.0303, -0.0114],
         [ 0.0222,  0.0243, -0.0115,  ...,  0.0369, -0.0347,  0.0291],
         [ 0.0045,  0.0156,  0.0281,  ..., -0.0348, -0.0370, -0.0152]],
        requires_grad=True),
 Parameter containing:
 tensor([ 0.0072, -0.0399, -0.0138,  0.0062, -0.0099, -0.0006, -0.0142, -0.0337,
          ……
         -0.0370, -0.0121, -0.0348, -0.0200, -0.0285,  0.0367,  0.0050, -0.0166],
        requires_grad=True),
 Parameter containing:
 tensor([[-0.0130,  0.0301,  0.0721,  ..., -0.0634,  0.0325, -0.0830],
         [-0.0086, -0.0374, -0.0281,  ..., -0.0543,  0.0105,  0.0822],
         [-0.0305,  0.0047, -0.0090,  ...,  0.0370, -0.0187,  0.0824],
         ...,
         [ 0.0529, -0.0236,  0.0219,  ...,  0.0250,  0.0620, -0.0446],
         [ 0.0077, -0.0576,  0.0600,  ..., -0.0412, -0.0290,  0.0103],
         [ 0.0375, -0.0147,  0.0622,  ...,  0.0350,  0.0179,  0.0667]],
        requires_grad=True),
 Parameter containing:
 tensor([-0.0709, -0.0675, -0.0492,  0.0694,  0.0390, -0.0861, -0.0427, -0.0638,
         -0.0123,  0.0845], requires_grad=True)]

6. model.named_parameters()

如果你是从前面看过来的,就会知道,这里就是迭代的返回带有名字的参数,会给每个参数加上带有 .weight或 .bias的名字以区分权重和偏置:

In [32]: len(model.named_parameters)                                                                                                 
Out[32]: 12

In [33]: model_named_parameters                                                                                                      
Out[33]: 
[('features.0.weight', Parameter containing:
  tensor([[[[ 0.1200, -0.1627, -0.0841],
            [-0.1369, -0.1525,  0.0541],
            [ 0.1203,  0.0564,  0.0908]],
           ……
           [[-0.1587,  0.0735, -0.0066],
            [ 0.0210,  0.0257, -0.0838],
            [-0.1797,  0.0675,  0.1282]]]], requires_grad=True)),
 ('features.0.bias', Parameter containing:
  tensor([-0.1251,  0.1673,  0.1241, -0.1876,  0.0683,  0.0346],
         requires_grad=True)),
 ('features.1.weight', Parameter containing:
  tensor([0.0072, 0.0272, 0.8620, 0.0633, 0.9411, 0.2971], requires_grad=True)),
 ('features.1.bias', Parameter containing:
  tensor([0., 0., 0., 0., 0., 0.], requires_grad=True)),
 ('features.4.weight', Parameter containing:
  tensor([[[[ 0.0632, -0.1078, -0.0800],
            [-0.0488,  0.0167,  0.0473],
            [-0.0743,  0.0469, -0.1214]],
           ……
           [[-0.1067, -0.0851,  0.0498],
            [-0.0695,  0.0380, -0.0289],
            [-0.0700,  0.0969, -0.0557]]]], requires_grad=True)),
 ('features.4.bias', Parameter containing:
  tensor([-0.0608,  0.0154,  0.0231,  0.0886, -0.0577,  0.0658, -0.1135, -0.0221,
           0.0991], requires_grad=True)),
 ('features.5.weight', Parameter containing:
  tensor([0.2514, 0.1924, 0.9139, 0.8075, 0.6851, 0.4522, 0.5963, 0.8135, 0.4010],
         requires_grad=True)),
 ('features.5.bias', Parameter containing:
  tensor([0., 0., 0., 0., 0., 0., 0., 0., 0.], requires_grad=True)),
 ('classifier.0.weight', Parameter containing:
  tensor([[ 0.0223,  0.0079, -0.0332,  ..., -0.0394,  0.0291,  0.0068],
          ……
          [ 0.0045,  0.0156,  0.0281,  ..., -0.0348, -0.0370, -0.0152]],
         requires_grad=True)),
 ('classifier.0.bias', Parameter containing:
  tensor([ 0.0072, -0.0399, -0.0138,  0.0062, -0.0099, -0.0006, -0.0142, -0.0337,
           ……
          -0.0370, -0.0121, -0.0348, -0.0200, -0.0285,  0.0367,  0.0050, -0.0166],
         requires_grad=True)),
 ('classifier.3.weight', Parameter containing:
  tensor([[-0.0130,  0.0301,  0.0721,  ..., -0.0634,  0.0325, -0.0830],
          [-0.0086, -0.0374, -0.0281,  ..., -0.0543,  0.0105,  0.0822],
          [-0.0305,  0.0047, -0.0090,  ...,  0.0370, -0.0187,  0.0824],
          ...,
          [ 0.0529, -0.0236,  0.0219,  ...,  0.0250,  0.0620, -0.0446],
          [ 0.0077, -0.0576,  0.0600,  ..., -0.0412, -0.0290,  0.0103],
          [ 0.0375, -0.0147,  0.0622,  ...,  0.0350,  0.0179,  0.0667]],
         requires_grad=True)),
 ('classifier.3.bias', Parameter containing:
  tensor([-0.0709, -0.0675, -0.0492,  0.0694,  0.0390, -0.0861, -0.0427, -0.0638,
          -0.0123,  0.0845], requires_grad=True))]

7. model.state_dict()

model.state_dict()直接返回模型的字典,和前面几个方法不同的是这里不需要迭代,它本身就是一个字典,可以直接通过修改state_dict来修改模型各层的参数,用于参数剪枝特别方便。

ESPCN 图像超分辨率方法

论文地址: https://arxiv.org/abs/1609.05158

代码:https://github.com/leftthomas/ESPCN

Real-Time Single Image and Video Super-Resolution Using an Efficient Sub-Pixel Convolutional Neural Network

ESPCN 是在2016年在CVPR上发表的一片论文,中提出的一种实时的基于卷积神经网络的图像超分辨率方法。

这篇论文主要就是提出了一种新的亚像素卷积层(sub-pixel convolutional layer),以往的方法,为了生成高分辨率的输出,一般是先对输入进行上采样扩大图像分辨率,得到与高分辨率图像同样的大小,再作为网络输入,意味着卷积操作在较高的分辨率上进行,相比于在低分辨率的图像上计算卷积,会降低效率。 ESPCN(Real-Time Single Image and Video Super-Resolution Using an Efficient Sub-Pixel Convolutional Neural Network,CVPR 2016)提出一种在低分辨率图像上直接计算卷积得到高分辨率图像的高效率方法。

如果想最后的分辨率从 n 到 rn,ESPCN会生成r*r个通道,再进行sub-pixel convolutional,生成高分辨率的图片。假设是9通道 混合,这里的通道混合是将每个通道对应位置的元素重新排列成3*3的图像。这个变换虽然被称作sub-pixel convolution, 但实际上并没有卷积操作。

通过使用sub-pixel convolution, 图像从低分辨率到高分辨率放大的过程,插值函数被隐含地包含在前面的卷积层中,可以自动学习到。只在最后一层对图像大小做变换,前面的卷积运算由于在低分辨率图像上进行,因此效率会较高。

ESPCN激活函数采用tanh替代了ReLU。损失函数为均方误差。

pytorch中已经集成了 sub-pixel convolution :

nn.PixelShuffle(upscale_factor)

以四维输入(N,C,H,W)为例,Pixelshuffle会将为(∗,r 2 C r^2Cr2C,H,W)的Tensor给reshape成(∗,C,rH,rW)的Tensor

Upsample:

对给定多通道的1维(temporal)、2维(spatial)、3维(volumetric)数据进行上采样。

对volumetric输入(3维——点云数据),输入数据Tensor格式为5维:minibatch x channels x depth x height x width
对spatial输入(2维——jpg、png等数据),输入数据Tensor格式为4维:minibatch x channels x height x width
对temporal输入(1维——向量数据),输入数据Tensor格式为3维:minibatch x channels x width

此算法支持最近邻,线性插值,双线性插值,三次线性插值对3维、4维、5维的输入Tensor分别进行上采样(Upsample)。

RepVGG: Making VGG-style ConvNets Great Again

论文下载地址:https://arxiv.org/abs/2101.03697
官方源码(Pytorch实现):https://github.com/DingXiaoH/RepVGG

这篇论文对于我来说最大的用处是提出了结构的重重参数化:

在推理时将三个并行分支合并成单个分支,并保证输出输出不变。

结构重参数化主要分为两步,第一步主要是将Conv2d算子和BN算子融合以及将只有BN的分支转换成一个Conv2d算子,第二步将每个分支上的3x3卷积层融合成一个卷积层。

1、Conv2d和BN 这个已经是非常常见的,因为卷积核bn都是线性运算,所以可以进行合并。

这里假设输入的特征图(Input feature map)如下图所示,输入通道数为2,然后采用两个卷积核(图中只画了第一个卷积核对应参数)。

在这里插入图片描述

接着计算一下输出特征图(Output feature map)通道1上的第一个元素,即当卷积核1在输入特征图红色框区域卷积时得到的值(为了保证输入输出特征图高宽不变,所以对Input feature map进行了Padding)。其他位置的计算过程类似这里就不去演示了。

在这里插入图片描述

然后再将卷积层输出的特征图作为BN层的输入,这里同样计算一下输出特征图(Output feature map)通道1上的第一个元素,按照上述BN在推理时的计算公式即可得到如下图所示的计算结果。

在这里插入图片描述

代码

Conv2d+BN融合实验(Pytorch)
下面是参考作者提供的源码改的一个小实验,首先创建了一个module包含了卷积和BN模块,然后按照上述转换公式将卷积层的权重和BN的权重进行融合转换,接着载入到新建的卷积模块fused_conv中,最后随机创建一个Tensor(f1)将它分别输入到module以及fused_conv中,通过对比两者的输出可以发现它们的结果是一致的。

from collections import OrderedDict

import numpy as np
import torch
import torch.nn as nn


def main():
    torch.random.manual_seed(0)

    f1 = torch.randn(1, 2, 3, 3)

    module = nn.Sequential(OrderedDict(
        conv=nn.Conv2d(in_channels=2, out_channels=2, kernel_size=3, stride=1, padding=1, bias=False),
        bn=nn.BatchNorm2d(num_features=2)
    ))

    module.eval()

    with torch.no_grad():
        output1 = module(f1)
        print(output1)

    # fuse conv + bn
    kernel = module.conv.weight 
    running_mean = module.bn.running_mean
    running_var = module.bn.running_var
    gamma = module.bn.weight
    beta = module.bn.bias
    eps = module.bn.eps
    std = (running_var + eps).sqrt()
    t = (gamma / std).reshape(-1, 1, 1, 1)  # [ch] -> [ch, 1, 1, 1]
    kernel = kernel * t
    bias = beta - running_mean * gamma / std
    fused_conv = nn.Conv2d(in_channels=2, out_channels=2, kernel_size=3, stride=1, padding=1, bias=True)
    fused_conv.load_state_dict(OrderedDict(weight=kernel, bias=bias))

    with torch.no_grad():
        output2 = fused_conv(f1)
        print(output2)

    np.testing.assert_allclose(output1.numpy(), output2.numpy(), rtol=1e-03, atol=1e-05)
    print("convert module has been tested, and the result looks good!")


if __name__ == '__main__':
    main()

repVGG中大量运用conv+BN层,我们知道将层合并,减少层数能提升网络性能,下面的推理是conv带有bias的过程:

这其实就是一个卷积层,只不过权重考虑了BN的参数 我们令:

最终的融合结果即为:

相关融合代码如下图所示:

def _fuse_bn_tensor(self, branch):
        if branch is None:
            return 0, 0
        if isinstance(branch, nn.Sequential):
            kernel = branch.conv.weight
            running_mean = branch.bn.running_mean
            running_var = branch.bn.running_var
            gamma = branch.bn.weight
            beta = branch.bn.bias
            eps = branch.bn.eps
        else:
            ...
        std = (running_var + eps).sqrt()
        t = (gamma / std).reshape(-1, 1, 1, 1)
        return kernel * t, beta - running_mean * gamma / std

2、如何将不同分支合并:

作者这里首先将不同分支的卷积核都变成3*3:

2.1 将1×1卷积转换成3×3卷积
这个过程比较简单,如下图所示,以1×1卷积层中某一个卷积核为例,只需在原来权重周围补一圈零就行了,这样就变成了3×3的卷积层,注意为了保证输入输出特征图高宽不变,此时需要将padding设置成1(原来卷积核大小为1×1时padding为0)。最后按照上述2.1中讲的内容将卷积层和BN层进行融合即可。

在这里插入图片描述

2.2将BN转换成3×3卷积
对于只有BN的分支由于没有卷积层,所以我们可以先自己构建出一个卷积层来。如下图所示,构建了一个3×3的卷积层,该卷积层只做了恒等映射,即输入输出特征图不变。既然有了卷积层,那么又可以按照上述2.1中讲的内容将卷积层和BN层进行融合。

在这里插入图片描述

2.3 多分支融合
在上面的章节中,我们已经讲了怎么把每个分支融合转换成一个3×3的卷积层,接下来需要进一步将多分支转换成一个单路3×3卷积层。

在这里插入图片描述

合并的过程其实也很简单,直接将这三个卷积层的参数相加即可,具体推理过程就不讲了,如果不了解的可以自己动手算算。

总的来说,这篇论文的目标是Simple is Fast, Memory-economical, Flexible,提出了很多想法去实现上述目标,对于当前我的工作还是比较有启发的,尤其是最后对网络进行合并以及量化部分。下一步要好好学习下torch的量化QAT (torch.quantization.prepare_qat)

IPython(jupyter)中的常用工具

ipython是一个python的交互式shell,比默认的python shell好用得多,支持变量自动补全,自动缩进,支持bash shell命令,内置了许多很有用的功能和函数。学习ipython将会让我们以一种更高的效率来使用python。同时它也是利用Python进行科学计算和交互可视化的一个最佳的平台。

IPython提供了两个主要的组件:

1.一个强大的python交互式shell
2.供Jupyter notebooks使用的一个Jupyter内核(IPython notebook)

IPython的主要功能如下:

1.运行ipython控制台
2.使用ipython作为系统shell
3.使用历史输入(history)
4.Tab补全
5.使用%run命令运行脚本
6.使用%timeit命令快速测量时间
7.使用%pdb命令快速debug
8.使用pylab进行交互计算
9.使用IPython Notebook

Tab键自动补全

在shell中输入表达式时,只要按下Tab键,当前命名空间中任何与输入的字符串相匹配的变量(对象或者函数等)就会被找出来

 内省

在变量的前面或者后面加上一个问号?,就可以将有关该对象的一些通用信息显示出来,这就叫做对象的内省

如果对象是一个函数或者实例方法,则它的docstring也会被显示出来

使用历史命令history

在IPython shell中,使用历史命令可以简单地使用上下翻页键即可,另外我们也可以使用hist命令(或者history命令)查看所有的历史输入。(正确的做法是使用%hist,在这里,%hist也是一个魔法命令)

使用%run命令运行脚本

在ipython会话环境中,所有文件都可以通过%run命令当做Python程序来运行,输入%run 路径+python文件名称即可

使用%timeit命令快速测量代码运行时间

在一个交互式会话中,我们可以使用%timeit魔法命令快速测量代码运行时间。相同的命令会在一个循环中多次执行,多次运行时长的平均值作为该命令的最终评估时长。-n 选项可以控制命令在单词循环中执行的次数,-r选项控制执行循环的次数。

使用%debug命令进行快速debug

ipython带有一个强大的调试器。无论何时控制台抛出了一个异常,我们都可以使用%debug魔法命令在异常点启动调试器。接着你就能调试模式下访问所有的本地变量和整个栈回溯。使用ud向上和向下访问栈,使用q退出调试器。在调试器中输入?可以查看所有的可用命令列表。

 在IPython中使用系统shell

我们可以在IPython中直接使用系统shell,并获取读取结果作为一个Python字符串列表。为了实现这种功能,我们需要使用感叹号!作为shell命令的前缀。比如现在在我的windows系统中,直接在IPython中ping百度

点:display 模块

官方教程 https://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html

之前想要在jupyter中显示图像 、视频 or voice、html,可能不知道该怎么办,有了IP display模块,可以解决该问题。

1、audio

from IPython.display import Audio,display
sound_file = '../taobao427.mp3'
display(Audio(sound_file))

2、ipython.display.image



from IPython.display import display, Image

path = "1.jpg"

display( Image( filename = path) )

3、播放视频

from IPython.display import clear_output,  display, HTML
from PIL import Image
import matplotlib.pyplot as plt
import time
import cv2
import os

def show_video(video_path:str,small:int=2):
    if not os.path.exists(video_path):
        print("视频文件不存在")
    video = cv2.VideoCapture(video_path)
    current_time = 0
    while(True):
        try:
            clear_output(wait=True)
            ret, frame = video.read()
            if not ret:
                break
            lines, columns, _ = frame.shape
            #########do img preprocess##########
            
            # 画出一个框
            #     cv2.rectangle(img, (500, 300), (800, 400), (0, 0, 255), 5, 1, 0)
             # 上下翻转
             # img= cv2.flip(img, 0)
            
            ###################################
            
            if current_time == 0:
                current_time = time.time()
            else:
                last_time = current_time
                current_time = time.time()
                fps = 1. / (current_time - last_time)
                text = "FPS: %d" % int(fps)
                cv2.putText(frame, text , (0,100), cv2.FONT_HERSHEY_TRIPLEX, 3.65, (255, 0, 0), 2)
                
          #     img = cv2.resize(img,(1080,1080))
                frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
            frame = cv2.resize(frame, (int(columns / small), int(lines / small)))

            img = Image.fromarray(frame)

            display(img)
            # 控制帧率
            time.sleep(0.02)
        except KeyboardInterrupt:
            video.release()

4、htlm(视频)

# ########## display
from IPython.display import display, HTML

html_str = '''
<video controls width=\"500\" height=\"500\" src=\"{}\">animation</video>
'''.format("./dataset/vid****8726.mp4")
print(html_str)
display(HTML(html_str))

5、插入参考的网页或者论文 iframe

from IPython.display import IFrame IFrame(src='https://www.baidu.com/',width=800,height=500)

from IPython.display import HTML
HTML("""

Example Domain

This domain is for use in illustrative examples in documents. You may use this domain in literature without prior coordination or asking for permission.

More information...

""")

6、插入iframe标签

from IPython.display import HTML
HTML('')