Trans4PASS实验环境配置
首先需要GitHub - jamycheung/Trans4PASS: Repository of Trans4PASS (accepted to CVPR2022)位置download code,然后在自己的conda环境中执行如下命令:(注意必须在Ubuntu环境下执行命令,使用windows会出现环境无法正常安装,这里的Ubuntu不能是虚拟机环境,必须是系统,才能使用显卡进行实验训练,建议在安装Ubuntu系统后,直接安装与自己系统适配的Nvidia驱动,不然无法使用cuda进行实验训练,在安装系统和驱动后,执行下述命令,如果出现报错,可以自行查阅网上的教程,这里不再赘述)
conda create -n trans4pass python=3.8
conda activate trans4pass
cd ~/path/to/trans4pass
conda install pytorch==1.8.0 torchvision==0.9.0 torchaudio==0.8.0 cudatoolkit=11.1 -c pytorch -c conda-forge
pip install mmcv-full==1.3.9 -f https://download.openmmlab.com/mmcv/dist/cu111/torch1.8.0/index.html
pip install -r requirements.txt
python setup.py develop --user
# Optional: install apex follow: https://github.com/NVIDIA/apex
数据集准备
数据集需要准备
- Cityscapes
- DensePASS
- SynPASS
- Standford2D3D
- Structured3D
datasets/
├── cityscapes
│ ├── gtFine
│ └── leftImg8bit
├── Stanford2D3D
│ ├── area_1
│ ├── area_2
│ ├── area_3
│ ├── area_4
│ ├── area_5a
│ ├── area_5b
│ └── area_6
├── Structured3D
│ ├── scene_00000
│ ├── ...
│ └── scene_00199
├── SynPASS
│ ├── img
│ │ ├── cloud
│ │ ├── fog
│ │ ├── rain
│ │ └── sun
│ └── semantic
│ ├── cloud
│ ├── fog
│ ├── rain
│ └── sun
├── DensePASS
│ ├── gtFine
│ └── leftImg8bit
到这一步,数据集准备完成,注意要按照如图所展示的结构将数据放置,保存位置可以任意,但是一定要了解自己存放数据的路径,这对于后边进行训练学习具有重要的意义,到目前位置实验所需要进行的准备工作已经全部完成了。下面将结合代码与论文来讲解如何对于实验进行复现。
数据集分析与处理
1.Cityscapes数据集
Cityscapes 是一个用于语义分割的数据集,主要用于训练和评估计算机视觉模型。它包含大量的城市街景图像,以及这些图像的像素级别的语义标注。该数据集涵盖了来自德国多个城市的街景图像,包括城市中心、郊区和高速公路等不同环境下的景象。
Cityscapes 数据集的主要特点包括:
-
大规模数据集:Cityscapes 包含大约 5000 张高分辨率图像,每张图像的分辨率为 2048x1024 像素。这些图像涵盖了各种不同的场景和环境。
-
像素级标注:每张图像都配有像素级别的标注,用于指示图像中每个像素的语义类别,例如道路、汽车、行人等。这些标注是通过人工标注和语义分割算法生成的。
-
多样性的场景:Cityscapes 数据集涵盖了城市中心、郊区、高速公路等多种不同的场景,使得模型在各种环境下都能进行有效的语义分割。
-
用途广泛:Cityscapes 数据集被广泛应用于语义分割算法的训练和评估,以及城市场景理解和自动驾驶等领域的研究和开发中。
总的来说,Cityscapes 数据集为研究人员和开发者提供了一个用于训练和评估语义分割模型的标准基准,促进了计算机视觉领域的发展和进步。在使用之前请按这篇文章所说对于数据集进行处理与理解,cityscapes数据集的理解与使用-CSDN博客
2.DensePASS数据集
DensePASS 数据集是用于全景语义分割的数据集,旨在提供对全景图像进行像素级别语义标注的数据。这个数据集的主要特点如下:
-
全景图像:DensePASS 数据集包含大量全景图像,这些图像覆盖了不同环境和场景,包括城市街道、郊区、高速公路等。
-
像素级标注:每张全景图像都配有像素级别的语义标注,用于指示图像中每个像素的语义类别,例如道路、车辆、行人等。
-
多样性和复杂性:数据集中的全景图像具有多样性和复杂性,涵盖了各种不同的场景和情境,使得模型能够在真实世界的复杂环境中进行有效的语义分割。
-
应用领域:DensePASS 数据集可用于训练和评估全景语义分割模型,为城市场景理解、自动驾驶等领域的研究和应用提供支持。
总的来说,DensePASS 数据集为研究人员和开发者提供了一个用于全景语义分割模型训练和评估的重要资源,有助于推动全景图像分析领域的发展和进步。注意DensePASS数据集依然是由LeftIImg8bit(原始数据)和gtfine(标注影像)组成,但是在本实验中使用leftimg8bit时需要从WildPASS链接中下载
3.SynPASS数据集
SynPASS 数据集是用于全景图像分析领域的合成数据集,旨在提供合成全景图像和相应的语义分割标签,以支持模型的训练和评估。该数据集的主要特点包括:
-
合成全景图像:SynPASS 数据集提供了合成的全景图像,这些图像通过合成技术生成,涵盖了各种不同的场景和环境,包括城市街道、郊区、高速公路等。
-
语义分割标签:每张合成全景图像都配有相应的像素级别语义分割标签,用于指示图像中每个像素的语义类别,例如道路、车辆、行人等。
-
多样性和控制性:由于是合成数据集,SynPASS 允许研究人员对场景、光照、天气等因素进行精细的控制,以产生多样性的图像数据,有助于模型在各种情况下的鲁棒性测试。
-
应用领域:SynPASS 数据集可用于训练和评估全景图像分析模型,特别是在城市场景理解、自动驾驶等领域的研究和应用中,为模型的开发和部署提供支持。
综上所述,SynPASS 数据集为全景图像分析领域的研究人员和开发者提供了一个重要的合成数据资源,可用于模型的训练、评估和性能测试,有助于推动全景图像分析技术的发展和应用。
4.Standford2D3D
Stanford 2D-3D 数据集是一个用于场景理解和三维重建的大规模数据集,由斯坦福大学提供。该数据集包含了一系列来自真实世界的图像,以及与这些图像相关联的丰富的三维语义信息。
主要特点包括:
-
图像数据:Stanford 2D-3D 数据集包含大量来自真实世界的图像,这些图像涵盖了不同场景和环境,例如室内、室外、城市街道等。
-
三维语义信息:每张图像都配有相应的三维语义信息,包括物体的边界框、类别标签、物体实例的位置、方向等。
-
多模态数据:除了图像和三维语义信息之外,数据集还可能包含其他类型的信息,如深度图、点云数据等,为多模态场景理解任务提供支持。
-
应用领域:Stanford 2D-3D 数据集可用于多个场景理解和三维重建任务,包括目标检测、语义分割、物体重建、场景理解等。
通过使用 Stanford 2D-3D 数据集,研究人员可以开展各种场景理解和三维重建方面的研究,评估和比较不同模型的性能,并为相关应用领域的发展做出贡献。
Stanford 2D-3D 数据集在全景影像语义分割方面的用途可以通过以下几个方面来实现:
-
训练数据来源:Stanford 2D-3D 数据集提供了大量真实世界的图像和相应的三维语义信息。这些图像可以用作训练全景影像语义分割模型的数据来源,帮助模型学习识别和理解全景影像中的不同语义类别。
-
模型评估和比较:研究人员可以使用 Stanford 2D-3D 数据集作为评估和比较全景影像语义分割模型性能的基准。通过在该数据集上进行测试,可以评估模型的准确性、鲁棒性和泛化能力,并与其他模型进行比较。
-
场景理解研究:全景影像语义分割是场景理解的重要组成部分,可以帮助理解和分析全景场景中的不同对象和物体。研究人员可以利用 Stanford 2D-3D 数据集开展与场景理解相关的研究,探索全景影像中的语义信息,并深入理解各种场景的特征和属性。
-
应用领域:全景影像语义分割在许多应用领域具有广泛的应用,包括智能交通、智能城市、虚拟现实等。利用 Stanford 2D-3D 数据集进行研究可以为这些领域的发展提供支持,并推动全景影像语义分割技术的应用和进步。
在本实验中Standford2D3D主要是用于产生那个DensePASS数据集,所以并不需要自己去实际使用,作者提供这个数据集可能是想要让我们从源头开始复现。
论文阅读+实验代码分析
Bending Reality: Distortion-aware Transformers for Adapting to Panoramic Semantic Segmentation
文章链接:https://arxiv.org/pdf/2203.01452.pdf
这是论文的标题,有一说一,一些work很喜欢使用一些文学性的标题,比如:Attention is all your need,如果让我自己发文章,我也会起一个这样文学+实际工作标题,所以这篇文章的主要工作就是能够意识到畸变的transformers用于适应全景影像语义分割(中文有点太拗口了,按照英文理解实际就是利用了这个transformer这样一种深度学习模型用于适应全景影像语义分割,那个畸变感知这个目前从标题中无法获得相关信息)
这里补充一下Transformer的相关知识:
Transformer 是一种深度学习模型架构,最初用于自然语言处理(NLP)领域,但后来也被成功应用于其他领域,如计算机视觉、语音识别等。Transformer 模型是由 Vaswani 等人在论文 "Attention Is All You Need" 中提出的,它摒弃了传统的循环神经网络(RNN)和卷积神经网络(CNN)的架构,采用了完全基于注意力机制的模块。
Transformer 模型的核心是自注意力机制(self-attention mechanism),它允许模型在处理序列数据时对序列中的不同位置进行加权处理,从而能够更好地捕捉序列中的长距离依赖关系。Transformer 模型还包括位置编码(Positional Encoding)来表示输入序列中各个位置的位置信息,以便模型能够理解输入序列中的顺序。
Transformer 模型通常由编码器(Encoder)和解码器(Decoder)组成。在 NLP 中,编码器负责将输入序列编码成一系列隐藏表示,而解码器则将这些隐藏表示解码成目标序列。每个编码器和解码器模块都由多层的注意力层和全连接前馈网络层组成。
除了在NLP中取得巨大成功之外,Transformer 模型还被广泛应用于计算机视觉领域,如图像分类、对象检测、图像生成等任务。在这些任务中,Transformer 模型通常以自注意力机制为核心,同时结合卷积神经网络的结构,形成了一种称为 Vision Transformer (ViT) 的模型。
具体的理解可以查看这篇文章,我觉得讲的不错:小白看得懂的 Transformer (图解) - 知乎
Abstract
首先本篇文章提出的背景主要是一个矛盾,那就是全景影像能够提供关于周围空间详尽的信息,为场景理解提供丰富的基础,但是如果想要全景分割模型展示这种潜力,大量昂贵的像素级标注对于实现任务必不可少。很好理解,因为语义分割需要有大量标注准确的数据进行训练,从而获得不错的效果,但是全景影像标注大家不是很卷,就没有lab愿意花大价钱去标注一个大型的数据集,所以昂贵的像素标注比较稀缺,所以全景语义分割模型不是很成熟,但是这个麻雀虽小也是肉呀,为了发文章的需求,大家开始想办法花小钱发顶刊,所以大家现在去想办法卷不用标注也能获得不错的分割模型,这也就是这篇文章研究的内容。下面我们来看这篇文章的摘要:
360°全景图像提供了关于周围空间详尽的信息,为场景理解提供了丰富的基础。为了以健壮的全景分割模型的形式展现这种潜力,大量昂贵的像素级标注对于成功至关重要。虽然此类标注可获得,但主要适用于狭角度、针孔相机图像,这些资源在原样状态下作为训练全景模型的资源不够理想。360°全景图中的失真和与众不同的图像特征分布阻碍了从富有注释的针孔领域转移,并因此导致性能下降。为了解决这种领域差异,并将针孔和360°全景视觉的语义注释汇集在一起,我们提出在变形补丁嵌入(DPE)和可变形MLP(DMLP)组件中学习对象变形和全景图像失真,并将它们融合到我们的全景语义分割(Trans4PASS)模型中。最后,我们通过在无监督领域适应中生成多尺度原型特征并在共享的针孔和全景特征嵌入中对齐它们,将针孔和全景特征嵌入中的共享语义联系在一起。在室内Stanford2D3D数据集上,我们的Trans4PASS与MPA保持了与完全监督的最新技术相当的性能,减少了对超过1400个标记的全景图像的需求。在户外DensePASS数据集上,我们的性能比最新技术提高了14.39%的mIoU,并将新的性能水平提高到了56.38%。
看下来就是一般影像标注多,所以决定使用迁移学习来进行实验分析,但是一些迁移学习通用方法在解决这个问题中不是很好,因为360°全景图中的失真和与众不同的图像特征分布导致了域的的差异,因此在这个学习过程中,获得的信息不是很理想,然后这个作者想到自己研究一个新的方法来解决这个域迁移不顺畅的问题,并且成功了,这个性能我们先不管,先看下边的introduction
总结来看:这篇文章的目的很简单,由于想发文章,找了一个比较不卷的领域,然后发现在这个领域中这个问题做的人比较少,但是又有这样的问题,然后自己经过调研和学习,发现:欸,这个方法居然能够提升如此高的精度,所以就发了CVPR,发顶会找个创新度高的方向也不失为一种好方法。
Introduction
introduction主要介绍了Related work和全景影像语义分割的应用和意义。这个是迁移的基础:全景语义分割通常在使用等距投影进行转换的2D全景图像上进行,这伴随着图像失真和物体变形。
这篇文章作出了提供了以下贡献:
(1)我们在考虑全景畸变的情况下,提出了扭曲感知的Transformer用于全景语义分割(Trans4PASS),其中包括可变形补丁嵌入和可变形MLP模块。
(2)我们提出了Mutual Prototypical Adaptation,通过提炼双域原型知识来转移模型,通过在特征空间和输出空间中与伪标签相结合,提高了性能。
(3)我们的框架用于从PIN2PAN转移模型,在两个竞争性基准测试上取得了出色的结果:在Stanford2D3D上,我们避免使用1400个昂贵的全景标签,同时取得了可比较的结果,在DensePASS上,我们将mIoU的最先进性能提高了14.39%。
两个创新点:trans4PASS提取特征+MPA进行域自适应=性能提高
Related Work
语义分割和全景分割
就是逐渐卷积神经网络逐渐过渡到transformer再到MLP交替进行空间通道混合,逐渐复杂,这里有一个之前读的问题:“MLP-based mixing"指的是基于多层感知器(MLP)的混合。在全景分割任务中,MLP被用来处理全景图像特有的语义分布。通过MLP-based mixing,模型可以更好地将全景图像中的不同语义信息进行混合,从而更准确地进行分割。MLP-based mixing的目的是通过学习适当的混合权重,使模型能够在处理全景图像时考虑到其特殊的语义分布,以提高分割的准确性和鲁棒性。这有一个概念:全景影像语义分布:全景影像的语义分布指的是在全景影像中各个像素点所对应的语义类别的分布情况。在全景影像的语义分割任务中,每个像素点都被分配了一个语义类别,例如道路、建筑物、行人等。全景影像的语义分布描述了这些语义类别在整个全景图像中的分布情况,即哪些区域属于哪些类别,以及它们的分布密度和分布范围等信息。对全景影像的语义分布的准确理解和分析是进行全景语义分割任务的关键。
无监督域自适应(UDA)
主要有两种self-training和对抗学习:自我训练方法涉及通过生成未标记目标数据的伪标签并利用它们进行进一步训练来迭代地改进模型在目标域上的性能。另一方面,对抗方法利用生成对抗网络(GANs)的概念执行诸如图像翻译或在布局或特征表示方面对领域进行对齐等任务。
这里的描述比较抽象,在代码里我们看这个具体是怎么执行的。
Methodology
重头戏来了,这部分方法我会结合代码和论文一起分析
TransPASS架构
首先两个版本的Trans4PASS模型(T:微型和S:小型),在两个模型都有四个阶段对于微型模型,每个阶段包含2层,对于小型版本,阶段包含3、4、6和3层,如下图所示:
现在来看看代码(Tiny在trans4pass.py文件中,Small在trans4pass-plus.py文件中):
这里文章中说的太抽象了,我们直接看代码去理解实际的运行过程(集注,这里实际的模型是要提取特征),这里以Tiny为例,我们分步阅读代码:
这里是引用的包文件,然后注意代码是基于Pytorch实现的:
import math
import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision
from torch import Tensor
from torch.nn import init
from torch.nn.modules.utils import _pair
from functools import partial
from timm.models.layers import DropPath, to_2tuple, trunc_normal_
from mmcv.runner import load_checkpoint
class Trans4PASS(nn.Module):
def __init__(self, num_classes=19, emb_chans=128, encoder='trans4pass_v1'):
super().__init__()
if encoder == 'trans4pass_v1':
self.encoder = trans4pass_v1()
elif encoder == 'trans4pass_v2':
self.encoder = trans4pass_v2()
else:
raise NotImplementedError
self.dede_head = DMLP(num_classes, emb_chans)
self.__setattr__('decoder', ['dede_head'])
self.apply(self._init_weights)
在这里定义了一个Trans4PASS的类,继承自nn.Module(也就是nn.Module这个Trans4PASS可以用),然后这里有一个init函数,相当于这个类的生成函数,这个类别有几个需要初始生成的参数,um_classes
表示输出类别的数量是19,emb_chans
表示嵌入通道的数量是128,
这个 __init__
方法定义了 Trans4PASS
类的初始化过程。这个类用于构建全景语义分割模型,其中的参数包括 num_classes
(输出类别数)、emb_chans
(嵌入通道数)和 encoder
(编码器类型)。根据给定的编码器类型,选择相应的编码器模型进行初始化,可以是 trans4pass_v1
或 trans4pass_v2
。
在初始化过程中,首先根据 encoder
的值选择对应的编码器模型,然后初始化一个称为 dede_head
的解码器模块,它是一个 DMLP(Deformable MLP)模块,用于将编码器提取的特征映射到输出类别数量上。最后,通过 self.__setattr__
方法设置了模型的解码器部分。
该方法还调用了 _init_weights
方法来初始化模型的权重。
def _init_weights(self, m):
if isinstance(m, nn.Linear):
trunc_normal_(m.weight, std=.02)
if isinstance(m, nn.Linear) and m.bias is not None:
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.LayerNorm):
nn.init.constant_(m.bias, 0)
nn.init.constant_(m.weight, 1.0)
elif isinstance(m, nn.Conv2d):
fan_out = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
fan_out //= m.groups
m.weight.data.normal_(0, math.sqrt(2.0 / fan_out))
if m.bias is not None:
m.bias.data.zero_()
本段代码的主要应用实际是一个isinstance函数,isinstance
是 Python 内置函数,用于检查一个对象是否是指定类或类型的实例。它的语法是
isinstance(object, classinfo)
其中:
object
是要检查的对象。classinfo
是要检查的类或类型,可以是单个类或类型,也可以是包含多个类或类型的元组。
isinstance
返回一个布尔值,如果 object
是 classinfo
类型的实例或者是 classinfo
元组中任意类型的实例,则返回 True
,否则返回 False
。
在给定的代码中,isinstance(m, nn.Linear)
、isinstance(m, nn.LayerNorm)
和 isinstance(m, nn.Conv2d)
用于检查模块 m
是否是线性层、LayerNorm 层或者二维卷积层的实例。
然后在了解这个isinstance()后,
这段代码定义了一个 _init_weights
方法,用于初始化模型的权重。它接受一个模块 m
作为参数,并根据模块的类型来初始化权重。
- 如果模块是线性层 (
nn.Linear
),则使用截断正态分布初始化权重,标准差为0.02。如果该线性层有偏置项,则将偏置项初始化为0。 - 如果模块是 LayerNorm 层 (
nn.LayerNorm
),则将偏置项初始化为0,将权重初始化为1。 - 如果模块是二维卷积层 (
nn.Conv2d
),则使用标准差为math.sqrt(2.0 / fan_out)
的正态分布来初始化权重,其中fan_out
计算为输出通道数乘以卷积核的面积除以分组数。如果该卷积层有偏置项,则将偏置项初始化为0。
这个方法的作用是在模型初始化时,按照一定的规则对模型的权重进行初始化,以便更好地训练模型。
def forward(self, x):
size = x.size()[2:]
c1, c2, c3, c4 = self.encoder(x)
x_feats = [c1, c2, c3, c4]
x_feats_de, x = self.dede_head(c1, c2, c3, c4)
x_out = F.interpolate(x, size, mode='bilinear', align_corners=True)
# return x_feats, x_out
return x_feats_de, x_out
这个 forward
方法定义了模型的前向传播过程,即当输入数据通过模型时,会调用这个方法进行处理并生成输出。
-
输入
x
是输入数据,一般是一批图像。在这个方法中,首先获取输入图像的尺寸size
。 -
接着,调用模型的
encoder
部分处理输入图像x
,得到不同层级的特征图c1, c2, c3, c4
。这些特征图在语义分割任务中通常用于提取不同层次的语义信息。 -
然后,将这些特征图作为输入,经过
dede_head
模块进行特征解码,得到解码后的特征x_feats_de
和最终的输出x
。 -
最后,通过双线性插值将输出
x
调整到与输入图像相同的尺寸,并返回解码后的特征和调整尺寸后的输出。
这个方法的返回值包括了特征解码后的特征和调整尺寸后的输出,用于后续的损失计算和模型评估。
def get_1x_lr_params_NOscale(self):
"""
This generator returns all the parameters of the net except for
the last classification layer. Note that for each batchnorm layer,
requires_grad is set to False in deeplab_resnet.py, therefore this function does not return
any batchnorm parameter
"""
b = []
b.append(self.encoder)
b.append(self.dede_head)
for i in range(len(b)):
for j in b[i].modules():
jj = 0
for k in j.parameters():
jj += 1
if k.requires_grad:
yield k
这个 get_1x_lr_params_NOscale
函数是用来获取模型中除了最后一层分类器之外的所有参数。在这个函数中,主要是遍历模型中的每个子模块,并将其参数加入到列表 b
中,然后逐个检查每个参数是否需要梯度更新,如果需要则返回该参数。
具体步骤如下:
-
创建一个空列表
b
,用于存储模型中的子模块。 -
将模型的
encoder
和dede_head
两个部分加入到列表b
中。 -
遍历列表
b
中的每个子模块,进一步遍历子模块中的每个参数。如果参数需要梯度更新,则将其返回。
通过这个函数,可以方便地获取模型中需要进行梯度更新的参数,通常用于设置不同的学习率策略。
def get_10x_lr_params(self):
"""
This generator returns all the parameters for the last layer of the net,
which does the classification of pixel into classes
"""
pass
这个函数我看不太懂,但是看到这个是对于last layer进行操作的。
def optim_parameters(self, args):
return [{'params': self.get_1x_lr_params_NOscale(), 'lr': args.learning_rate},
# {'params': self.get_10x_lr_params(), 'lr': 10 * args.learning_rate}
]
这个 optim_parameters
函数是用来为优化器提供模型的参数设置的。在这个函数中,主要是返回一个字典列表,其中每个字典包含了一组参数及其对应的学习率。
具体步骤如下:
-
使用
get_1x_lr_params_NOscale
函数获取模型中除了最后一层分类器之外的所有参数,并将其设置为学习率为args.learning_rate
。 -
如果需要设置不同于前面所有层的学习率(通常是最后一层分类器),可以使用
get_10x_lr_params
函数获取这些参数,并设置其学习率为10 * args.learning_rate
。在这个代码中,这一部分被注释掉了,所以暂时没有设置。
最终,函数返回一个列表,每个元素是一个字典,包含了一组参数及其对应的学习率。这个列表可以直接传递给优化器进行参数优化。
注意:
这个类别的定义采用了典型的 PyTorch 模型定义方式,其中包括了模型的初始化函数 __init__
、前向传播函数 forward
,以及一些用于参数管理和优化的辅助函数。
-
__init__
函数用于初始化模型的各个组件,包括编码器(encoder)和输出模块(dede_head)。在这里,根据传入的参数选择了不同的编码器,这样做使得模型更加灵活,可以根据需要选择不同的模型架构。另外,还初始化了模型的权重,调用了_init_weights
函数。 -
_init_weights
函数用于初始化模型的权重。在深度学习模型中,良好的权重初始化可以帮助模型更快地收敛,并且有助于避免梯度消失或梯度爆炸等问题。因此,这个函数在模型初始化时被调用,为模型的各个组件设置合适的权重。 -
forward
函数定义了模型的前向传播逻辑。在深度学习中,前向传播函数定义了数据从输入到输出的流动过程,是模型的核心逻辑。这个函数中,通过调用编码器提取输入的特征,然后将这些特征传递给输出模块,最终返回模型的输出结果。 -
get_1x_lr_params_NOscale
函数和get_10x_lr_params
函数用于管理模型的参数。在训练过程中,通常需要对模型的不同参数设置不同的学习率。这两个函数定义了不同类型参数的获取方式,便于后续对参数进行优化设置。
总的来说,这种类的定义方式符合了 PyTorch 中模型定义的常规做法,使得模型的结构清晰,参数管理方便,并且易于理解和修改。
下面是关于MLP的实现:
class Mlp(nn.Module):
def __init__(self, in_features, hidden_features=None, out_features=None, act_layer=nn.GELU, drop=0.):
super().__init__()
out_features = out_features or in_features
hidden_features = hidden_features or in_features
self.fc1 = nn.Linear(in_features, hidden_features)
self.dwconv = DWConv(hidden_features)
self.act = act_layer()
self.fc2 = nn.Linear(hidden_features, out_features)
self.drop = nn.Dropout(drop)
self.apply(self._init_weights)
def _init_weights(self, m):
if isinstance(m, nn.Linear):
trunc_normal_(m.weight, std=.02)
if isinstance(m, nn.Linear) and m.bias is not None:
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.LayerNorm):
nn.init.constant_(m.bias, 0)
nn.init.constant_(m.weight, 1.0)
elif isinstance(m, nn.Conv2d):
fan_out = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
fan_out //= m.groups
m.weight.data.normal_(0, math.sqrt(2.0 / fan_out))
if m.bias is not None:
m.bias.data.zero_()
def forward(self, x, H, W):
x = self.fc1(x)
x = self.dwconv(x, H, W)
x = self.act(x)
x = self.drop(x)
x = self.fc2(x)
x = self.drop(x)
return x
这个类定义了一个多层感知机(MLP)模型,用于进行神经网络的特征提取和转换。
-
__init__
函数初始化了 MLP 模型的各个组件,包括线性层、深度可分离卷积层(DWConv)、激活函数和 Dropout 层。可以通过参数设置输入特征数、隐藏层特征数和输出特征数,如果没有指定隐藏层特征数和输出特征数,则默认与输入特征数相同。 -
_init_weights
函数用于初始化模型的权重。与之前的例子类似,这里采用了截断正态分布和常数初始化的方法对模型的权重进行初始化。 -
forward
函数定义了模型的前向传播逻辑。在前向传播过程中,输入数据首先经过一个线性层进行特征提取,然后通过深度可分离卷积层进行特征转换,再经过激活函数和 Dropout 层进行非线性变换和特征选择,最后通过另一个线性层输出结果。
总的来说,这个 MLP 类定义了一个简单的多层感知机模型,用于对输入特征进行特征提取和转换。
self.fc1 = nn.Linear(in_features, hidden_features)
这行代码创建了一个线性层,它将输入特征的数量 in_features
映射到隐藏特征的数量 hidden_features
。这个线性层将执行一个线性变换,将输入的特征向量映射到隐藏层的特征空间中。
self.dwconv = DWConv(hidden_features)
这样设计的目的是为了构建一个多层感知器(MLP)模型,其中包含一个线性层(nn.Linear
)和一个深度可分离卷积层(DWConv
)。MLP是一种经典的神经网络结构,由多个全连接层组成,用于学习输入数据的非线性映射。在这个设计中,通过使用深度可分离卷积层,可以增加模型的非线性表达能力,并减少模型的参数数量和计算复杂度,从而提高模型的效率和性能。
self.act = act_layer()
这段代码创建了一个激活函数(Activation Function)的实例,并将其赋值给self.act
属性。这里通过act_layer
参数指定了激活函数的类型,比如nn.GELU
表示GELU激活函数。通过调用act_layer()
,实际上是创建了一个激活函数的对象,并将其保存在self.act
中,以便在模型的前向传播过程中使用。
self.drop = nn.Dropout(drop)
这行代码创建了一个丢弃层(Dropout Layer)的实例,并将其赋值给self.drop
属性。丢弃层在训练过程中以一定的概率(由drop
参数指定)随机将输入张量中的部分元素置零,以防止过拟合。这里使用了nn.Dropout
类,并传入了drop
参数来指定丢弃概率。
然后是注意力机制的引入:
class Attention(nn.Module):
def __init__(self, dim, num_heads=8, qkv_bias=False, qk_scale=None, attn_drop=0., proj_drop=0., sr_ratio=1):
super().__init__()
assert dim % num_heads == 0, f"dim {dim} should be divided by num_heads {num_heads}."
self.dim = dim
self.num_heads = num_heads
head_dim = dim // num_heads
self.scale = qk_scale or head_dim ** -0.5
self.q = nn.Linear(dim, dim, bias=qkv_bias)
self.kv = nn.Linear(dim, dim * 2, bias=qkv_bias)
self.attn_drop = nn.Dropout(attn_drop)
self.proj = nn.Linear(dim, dim)
self.proj_drop = nn.Dropout(proj_drop)
self.sr_ratio = sr_ratio
if sr_ratio > 1:
self.sr = nn.Conv2d(dim, dim, kernel_size=sr_ratio, stride=sr_ratio)
self.norm = nn.LayerNorm(dim)
self.apply(self._init_weights)
def _init_weights(self, m):
if isinstance(m, nn.Linear):
trunc_normal_(m.weight, std=.02)
if isinstance(m, nn.Linear) and m.bias is not None:
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.LayerNorm):
nn.init.constant_(m.bias, 0)
nn.init.constant_(m.weight, 1.0)
elif isinstance(m, nn.Conv2d):
fan_out = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
fan_out //= m.groups
m.weight.data.normal_(0, math.sqrt(2.0 / fan_out))
if m.bias is not None:
m.bias.data.zero_()
def forward(self, x, H, W):
B, N, C = x.shape
q = self.q(x).reshape(B, N, self.num_heads, C // self.num_heads).permute(0, 2, 1, 3)
if self.sr_ratio > 1:
x_ = x.permute(0, 2, 1).reshape(B, C, H, W)
x_ = self.sr(x_).reshape(B, C, -1).permute(0, 2, 1)
x_ = self.norm(x_)
kv = self.kv(x_).reshape(B, -1, 2, self.num_heads, C // self.num_heads).permute(2, 0, 3, 1, 4)
else:
kv = self.kv(x).reshape(B, -1, 2, self.num_heads, C // self.num_heads).permute(2, 0, 3, 1, 4)
k, v = kv[0], kv[1]
attn = (q @ k.transpose(-2, -1)) * self.scale
attn = attn.softmax(dim=-1)
attn = self.attn_drop(attn)
x = (attn @ v).transpose(1, 2).reshape(B, N, C)
x = self.proj(x)
x = self.proj_drop(x)
return x
当你调用Attention
类时,首先会执行__init__
方法来初始化模块的各种参数和子模块。在这个方法中:
-
首先,检查输入维度
dim
是否可以被注意力头的数量num_heads
整除,如果不能整除,则会抛出一个 AssertionError。 -
接着,根据输入维度和注意力头的数量计算每个头的维度
head_dim
,并初始化注意力权重缩放因子scale
,默认情况下为 head_dim的-0.5次方。 -
然后,创建了用于计算查询(q)的线性层
self.q
,用于计算键值对(kv)的线性层self.kv
,以及用于投影的线性层self.proj
。这些线性层的权重初始化方式由_init_weights
方法定义。 -
如果指定了空间降采样比率
sr_ratio
大于1,则创建一个卷积层self.sr
和一个层归一化层self.norm
,用于对输入特征进行降采样和归一化。
在_init_weights
方法中,根据模块类型对权重进行初始化。对于线性层(nn.Linear
)和卷积层(nn.Conv2d
),采用截断正态分布进行初始化;对于层归一化层(nn.LayerNorm
),将偏置项初始化为0,权重初始化为1。
在forward
方法中,对输入特征x
进行自注意力操作。具体步骤如下:
-
通过线性变换
self.q
将输入特征x
映射为查询(q),并调整形状以适应多头注意力的计算。 -
如果指定了空间降采样比率
sr_ratio
大于1,则先对输入特征进行空间降采样,并通过卷积层和层归一化层处理,得到键值对(kv)。 -
计算查询(q)和键(k)之间的注意力权重,并将结果乘以缩放因子。
-
对注意力权重进行softmax归一化,并通过丢弃层
self.attn_drop
进行随机丢弃。 -
将注意力权重与值(v)相乘,然后将结果转置并重塑成与输入特征相同的形状。
-
通过线性变换
self.proj
和丢弃层self.proj_drop
得到最终的输出。
这样,Attention
类就可以在模型中用于实现自注意力机制,从而提取特征之间的相关性。
class Block(nn.Module):
def __init__(self, dim, num_heads, mlp_ratio=4., qkv_bias=False, qk_scale=None, drop=0., attn_drop=0.,
drop_path=0., act_layer=nn.GELU, norm_layer=nn.LayerNorm, sr_ratio=1):
super().__init__()
self.norm1 = norm_layer(dim)
self.attn = Attention(
dim,
num_heads=num_heads, qkv_bias=qkv_bias, qk_scale=qk_scale,
attn_drop=attn_drop, proj_drop=drop, sr_ratio=sr_ratio)
# NOTE: drop path for stochastic depth, we shall see if this is better than dropout here
self.drop_path = DropPath(drop_path) if drop_path > 0. else nn.Identity()
self.norm2 = norm_layer(dim)
mlp_hidden_dim = int(dim * mlp_ratio)
self.mlp = Mlp(in_features=dim, hidden_features=mlp_hidden_dim, act_layer=act_layer, drop=drop)
self.apply(self._init_weights)
def _init_weights(self, m):
if isinstance(m, nn.Linear):
trunc_normal_(m.weight, std=.02)
if isinstance(m, nn.Linear) and m.bias is not None:
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.LayerNorm):
nn.init.constant_(m.bias, 0)
nn.init.constant_(m.weight, 1.0)
elif isinstance(m, nn.Conv2d):
fan_out = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
fan_out //= m.groups
m.weight.data.normal_(0, math.sqrt(2.0 / fan_out))
if m.bias is not None:
m.bias.data.zero_()
def forward(self, x, H, W):
x = x + self.drop_path(self.attn(self.norm1(x), H, W))
x = x + self.drop_path(self.mlp(self.norm2(x), H, W))
return x
这段代码定义了一个 Transformer 模块的基本块 Block
。每个块由一个多头注意力机制(Attention
)和一个前馈神经网络(Mlp
)组成。具体来说:
-
__init__
方法用于初始化块内的各个子模块和参数。在这个方法中:- 首先,创建了第一个层归一化层
self.norm1
,用于对输入特征进行归一化。 - 然后,创建了注意力机制模块
self.attn
,其中包括多头注意力机制和可选的位置编码。 - 接着,如果设置了随机深度丢弃,则创建了随机深度丢弃层
self.drop_path
,否则创建一个恒等映射。 - 创建第二个层归一化层
self.norm2
,用于对经过注意力机制后的特征进行归一化。 - 最后,创建了前馈神经网络模块
self.mlp
,用于对归一化后的特征进行非线性变换。
- 首先,创建了第一个层归一化层
-
_init_weights
方法用于初始化模块的权重。对于线性层和卷积层,采用截断正态分布进行权重初始化;对于层归一化层,将偏置项初始化为0,权重初始化为1。 -
forward
方法定义了块的前向传播过程。在这个方法中:- 首先,通过第一个层归一化层
self.norm1
对输入特征进行归一化。 - 然后,将归一化后的特征输入到注意力机制模块
self.attn
中进行自注意力计算,并加上随机深度丢弃。 - 将经过注意力机制的特征与输入进行残差连接,并通过第二个层归一化层
self.norm2
进行归一化。 - 最后,将归一化后的特征输入到前馈神经网络模块
self.mlp
中进行非线性变换,并再次加上随机深度丢弃。
- 首先,通过第一个层归一化层
Deformable patch Embedding
首先将影像转化为patch embedding
class StackDilatedPatchEmbed(nn.Module):
r""" Image to Patch Embedding
Args:
img_size (int): Image size. Default: 224.
patch_size (int): Patch token size. Default: 7.
in_chans (int): Number of input image channels. Default: 3.
embed_dim (int): Number of linear projection output channels. Default: 96.
norm_layer (nn.Module, optional): Normalization layer. Default: None
"""
def __init__(self, img_size=224, patch_size=7, stride=4, in_chans=3, embed_dim=64, dilate=[1, 2]):
super().__init__()
img_size = to_2tuple(img_size)
patch_size = to_2tuple(patch_size)
self.img_size = img_size
self.patch_size = patch_size
self.in_chans = in_chans
self.embed_dim = embed_dim
padding = (patch_size[0] // 2, patch_size[1] // 2)
padding = (padding[0] + (padding[0]+1) // 2, padding[1] + (padding[1]+1) // 2)
self.projs = nn.ModuleList([nn.Conv2d(in_chans, embed_dim//2, kernel_size=patch_size, stride=stride,
padding=(patch_size[0] // 2, patch_size[1] // 2)),
nn.Conv2d(in_chans, embed_dim // 2, kernel_size=patch_size, stride=stride,
padding=padding, dilation=dilate[1])])
# ModuleList(
# (0): Conv2d(3, C//2, kernel_size=7, stride=(4, 4), padding=(2, 2), d=1)
# (1): Conv2d(3, C//2, kernel_size=7, stride=(4, 4), padding=(3, 3), d=2)
# )
self.norm = nn.LayerNorm(embed_dim)
self.apply(self._init_weights)
def _init_weights(self, m):
if isinstance(m, nn.Linear):
trunc_normal_(m.weight, std=.02)
if isinstance(m, nn.Linear) and m.bias is not None:
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.LayerNorm):
nn.init.constant_(m.bias, 0)
nn.init.constant_(m.weight, 1.0)
elif isinstance(m, nn.Conv2d):
fan_out = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
fan_out //= m.groups
m.weight.data.normal_(0, math.sqrt(2.0 / fan_out))
if m.bias is not None:
m.bias.data.zero_()
def forward(self, x):
_, _, H, W = x.shape
xs = []
for i in range(len(self.projs)):
tx = self.projs[i](x)
_, _, H, W = tx.shape
tx = tx.flatten(2).transpose(1, 2)
xs.append(tx) # B Ph*Pw C
x = torch.cat(xs, dim=2)
x = self.norm(x)
return x, H, W
这个StackDilatedPatchEmbed
类用于将输入的图像转换为补丁嵌入(patch embedding)序列。它执行以下操作:
-
初始化方法(
__init__
):这个方法初始化了类的参数和模块。主要包括:img_size
:图像的尺寸,默认为224。patch_size
:补丁的大小,默认为7。stride
:卷积的步长,默认为4。in_chans
:输入图像的通道数,默认为3(RGB图像)。embed_dim
:线性投影输出通道的数量,默认为64。dilate
:卷积的空洞率,默认为[1, 2]。- 创建了两个卷积层(
nn.Conv2d
),分别用于处理原始图像和经过第一个卷积层的图像,每个卷积层产生输出通道数的一半的特征。 - 创建了一个层归一化(
nn.LayerNorm
)模块,用于归一化嵌入的特征。
-
_init_weights
方法用于初始化模块的权重。对于线性层和卷积层,采用截断正态分布进行权重初始化;对于层归一化层,将偏置项初始化为0,权重初始化为1。 -
前向传播方法(
forward
):这个方法定义了模块的前向传播过程。在这个方法中:- 对于输入的图像
x
,分别经过两个卷积层,并在每层的输出后执行层归一化。 - 然后,将两个卷积层的输出特征张量按照最后两个维度(高度和宽度)展平,并交换维度。
- 最后,将展平的特征张量连接起来,并再次执行层归一化。
- 对于输入的图像
执行这个操作是为了将输入的图像转换为补丁嵌入序列是为了将图像划分为固定大小的块(补丁),以便能够对整个图像进行处理。这种转换具有以下优势和原因:
-
处理大尺寸图像:对于大尺寸的图像,直接输入到深度神经网络中可能会导致内存消耗过大和计算量过大。将图像分割为小块(补丁)后,可以更轻松地处理大尺寸的图像。
-
平移不变性:补丁嵌入使得模型对输入图像中的平移变化具有一定的不变性。即使输入图像中的对象稍微移动了一些,由于补丁的存在,模型仍然可以对这些对象进行正确的分类或分割。
-
计算效率:由于补丁的尺寸通常较小,因此在计算卷积和注意力机制时,可以减少计算量。这有助于加快模型的训练和推理速度。
-
多尺度特征:补丁嵌入使得模型可以在不同尺度下对图像进行处理,从而能够捕获到图像中的多尺度特征信息,提高模型的性能。
总之,将输入的图像转换为补丁嵌入序列可以使得模型更有效地处理大尺寸图像,并具有一定的平移不变性和多尺度特征表示能力,同时还能提高计算效率。
class OverlapPatchEmbed(nn.Module):
""" Image to Patch Embedding
"""
def __init__(self, img_size=224, patch_size=7, stride=4, in_chans=3, embed_dim=768, use_dcn=False):
super().__init__()
img_size = to_2tuple(img_size)
patch_size = to_2tuple(patch_size)
self.img_size = img_size
self.kernel_size = patch_size[0]
self.padding = patch_size[0] // 2
self.norm = nn.LayerNorm(embed_dim)
self.stride = to_2tuple(stride)
self.H, self.W = img_size[0] // patch_size[0], img_size[1] // patch_size[1]
self.num_patches = self.H * self.W
self.use_dcn = use_dcn
# ==== define as same name, in order to load self.proj.
self.proj = nn.Conv2d(in_chans, embed_dim, kernel_size=patch_size, stride=stride,
padding=(patch_size[0] // 2, patch_size[1] // 2))
self.apply(self._init_weights)
if use_dcn:
self.offset_conv = nn.Conv2d(in_chans,
2 * self.kernel_size * self.kernel_size,
kernel_size=self.kernel_size,
stride=stride,
padding=self.padding)
nn.init.constant_(self.offset_conv.weight, 0.)
nn.init.constant_(self.offset_conv.bias, 0.)
self.modulator_conv = nn.Conv2d(in_chans,
1 * patch_size[0] * patch_size[0],
kernel_size=self.kernel_size,
stride=stride,
padding=self.padding)
nn.init.constant_(self.modulator_conv.weight, 0.)
nn.init.constant_(self.modulator_conv.bias, 0.)
def _init_weights(self, m):
if isinstance(m, nn.Linear):
trunc_normal_(m.weight, std=.02)
if isinstance(m, nn.Linear) and m.bias is not None:
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.LayerNorm):
nn.init.constant_(m.bias, 0)
nn.init.constant_(m.weight, 1.0)
elif isinstance(m, nn.Conv2d):
fan_out = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
fan_out //= m.groups
m.weight.data.normal_(0, math.sqrt(2.0 / fan_out))
if m.bias is not None:
m.bias.data.zero_()
def forward(self, x):
if self.use_dcn:
x = self.deform_proj(x)
else:
x = self.proj(x)
_, _, H, W = x.shape
x = x.flatten(2).transpose(1, 2)
x = self.norm(x)
return x, H, W
def deform_proj(self, x):
# h, w = x.shape[2:]
max_offset = min(x.shape[-2], x.shape[-1]) // 4
offset = self.offset_conv(x).clamp(-max_offset, max_offset)
modulator = 2. * torch.sigmoid(self.modulator_conv(x))
x = torchvision.ops.deform_conv2d(input=x,
offset=offset,
weight=self.proj.weight,
bias=self.proj.bias,
padding=self.padding,
mask=modulator,
stride=self.stride,
)
return x
这段代码定义了一个将图像转换为重叠补丁嵌入的模块。重叠补丁嵌入与常规的补丁嵌入不同之处在于,它们之间存在重叠区域,使得相邻的补丁之间共享一部分像素。
这个模块具有以下主要组成部分和功能:
-
__init__
方法:初始化函数定义了模块的各个属性和参数。其中,img_size
表示输入图像的大小,patch_size
表示每个补丁的大小,stride
表示滑动窗口的步幅,in_chans
表示输入图像的通道数,embed_dim
表示嵌入维度,use_dcn
表示是否使用可变形卷积。根据参数设置了卷积层、归一化层等组件,并初始化权重。 -
_init_weights
方法:权重初始化函数,根据不同的层类型进行不同的初始化操作,例如线性层、归一化层和卷积层。 -
forward
方法:前向传播函数,根据是否使用可变形卷积,选择不同的计算方式。如果使用可变形卷积,则调用deform_proj
函数进行计算;否则,直接调用卷积层进行计算。最后,对结果进行展平操作,并进行归一化处理。 -
deform_proj
方法:可变形卷积计算函数,通过可变形卷积操作将输入特征图转换为输出特征图。首先,根据输入特征图计算偏移量和变形系数,然后利用这些信息进行可变形卷积操作,得到输出特征图。
这个模块的主要作用是将输入的图像转换为重叠的补丁嵌入序列,并且可以选择是否使用可变形卷积来更好地捕捉图像中的局部特征。
这里的具体代码实现可以参见Vision Transformer的实现,这里作者的改动好像是给每个patch的不同维度设置了不同权重?使层和层之间可以相互学习,这里我不是很确定。
下面是Trans4PASS的骨干网络,这里有两个类 Trans4PASS_Backbone
和 DWConv
以及两个函数 trans4pass_v1
和 trans4pass_v2
。
Trans4PASS_Backbone
类是一个 Transformer-based 的骨干网络,用于处理图像数据。它的初始化方法定义了网络的架构和参数设置,包括图像大小、补丁大小、输入通道数、类别数量等。该类包含了多个阶段(stage),每个阶段包括一个或多个 Transformer block,并在每个阶段后跟随一个 LayerNorm 层。forward_features
方法用于前向传播图像特征,并返回每个阶段的输出。forward
方法调用 forward_features
方法并返回输出。
DWConv
类是一个深度可分离卷积层,用于对特征进行深度方向上的卷积操作。它的初始化方法定义了深度可分离卷积的参数设置,包括输入通道数和输出通道数。forward
方法用于对输入特征进行深度可分离卷积操作,并返回输出。
trans4pass_v1
和 trans4pass_v2
函数分别返回一个 Trans4PASS_Backbone
类的实例,用于构建 Transformer-based 的骨干网络。这两个函数具有不同的参数设置,用于创建不同版本的网络结构。
注意:depths
参数在 Trans4PASS_Backbone
类中用于指定每个阶段(stage)中包含的 Transformer block 的数量。每个阶段都由多个 Transformer block 组成,这些 block 在一起构成了整个网络的深度。因此,depths
参数控制了网络的深度,即网络中的层数。
在初始化网络时,根据给定的 depths
参数值,会创建相应数量的 Transformer block,并将它们按照指定的深度添加到网络中的每个阶段中。这样可以灵活地控制网络的深度,以满足不同任务和数据集的需求。增加深度通常可以增强网络的表示能力,但也可能增加训练和推理的计算成本。
class Trans4PASS_Backbone(nn.Module):
def __init__(self, img_size=224, patch_size=16, in_chans=3, num_classes=1000, embed_dims=[64, 128, 256, 512],
num_heads=[1, 2, 4, 8], mlp_ratios=[4, 4, 4, 4], qkv_bias=False, qk_scale=None, drop_rate=0.,
attn_drop_rate=0., drop_path_rate=0., norm_layer=nn.LayerNorm,
depths=[3, 4, 6, 3], sr_ratios=[8, 4, 2, 1]):
super().__init__()
self.num_classes = num_classes
self.depths = depths
img_size = img_size[-1] if isinstance(img_size, tuple) else img_size
USE_DCN = [True, False, False, False]
self.patch_embed1 = OverlapPatchEmbed(img_size=img_size, patch_size=7, stride=4, in_chans=in_chans,
embed_dim=embed_dims[0], use_dcn=USE_DCN[0])
self.patch_embed2 = OverlapPatchEmbed(img_size=img_size // 4, patch_size=3, stride=2, in_chans=embed_dims[0],
embed_dim=embed_dims[1], use_dcn=USE_DCN[1])
self.patch_embed3 = OverlapPatchEmbed(img_size=img_size // 8, patch_size=3, stride=2, in_chans=embed_dims[1],
embed_dim=embed_dims[2], use_dcn=USE_DCN[2])
self.patch_embed4 = OverlapPatchEmbed(img_size=img_size // 16, patch_size=3, stride=2, in_chans=embed_dims[2],
embed_dim=embed_dims[3], use_dcn=USE_DCN[3])
# transformer encoder
dpr = [x.item() for x in torch.linspace(0, drop_path_rate, sum(depths))] # stochastic depth decay rule
cur = 0
self.block1 = nn.ModuleList([Block(
dim=embed_dims[0], num_heads=num_heads[0], mlp_ratio=mlp_ratios[0], qkv_bias=qkv_bias, qk_scale=qk_scale,
drop=drop_rate, attn_drop=attn_drop_rate, drop_path=dpr[cur + i], norm_layer=norm_layer,
sr_ratio=sr_ratios[0])
for i in range(depths[0])])
self.norm1 = norm_layer(embed_dims[0])
cur += depths[0]
self.block2 = nn.ModuleList([Block(
dim=embed_dims[1], num_heads=num_heads[1], mlp_ratio=mlp_ratios[1], qkv_bias=qkv_bias, qk_scale=qk_scale,
drop=drop_rate, attn_drop=attn_drop_rate, drop_path=dpr[cur + i], norm_layer=norm_layer,
sr_ratio=sr_ratios[1])
for i in range(depths[1])])
self.norm2 = norm_layer(embed_dims[1])
cur += depths[1]
self.block3 = nn.ModuleList([Block(
dim=embed_dims[2], num_heads=num_heads[2], mlp_ratio=mlp_ratios[2], qkv_bias=qkv_bias, qk_scale=qk_scale,
drop=drop_rate, attn_drop=attn_drop_rate, drop_path=dpr[cur + i], norm_layer=norm_layer,
sr_ratio=sr_ratios[2])
for i in range(depths[2])])
self.norm3 = norm_layer(embed_dims[2])
cur += depths[2]
self.block4 = nn.ModuleList([Block(
dim=embed_dims[3], num_heads=num_heads[3], mlp_ratio=mlp_ratios[3], qkv_bias=qkv_bias, qk_scale=qk_scale,
drop=drop_rate, attn_drop=attn_drop_rate, drop_path=dpr[cur + i], norm_layer=norm_layer,
sr_ratio=sr_ratios[3])
for i in range(depths[3])])
self.norm4 = norm_layer(embed_dims[3])
self.apply(self._init_weights)
def _init_weights(self, m):
if isinstance(m, nn.Linear):
trunc_normal_(m.weight, std=.02)
if isinstance(m, nn.Linear) and m.bias is not None:
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.LayerNorm):
nn.init.constant_(m.bias, 0)
nn.init.constant_(m.weight, 1.0)
elif isinstance(m, nn.Conv2d):
fan_out = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
fan_out //= m.groups
m.weight.data.normal_(0, math.sqrt(2.0 / fan_out))
if m.bias is not None:
m.bias.data.zero_()
def init_weights(self, pretrained=None):
if isinstance(pretrained, str):
load_checkpoint(self, pretrained, map_location='cpu', strict=False)
def reset_drop_path(self, drop_path_rate):
dpr = [x.item() for x in torch.linspace(0, drop_path_rate, sum(self.depths))]
cur = 0
for i in range(self.depths[0]):
self.block1[i].drop_path.drop_prob = dpr[cur + i]
cur += self.depths[0]
for i in range(self.depths[1]):
self.block2[i].drop_path.drop_prob = dpr[cur + i]
cur += self.depths[1]
for i in range(self.depths[2]):
self.block3[i].drop_path.drop_prob = dpr[cur + i]
cur += self.depths[2]
for i in range(self.depths[3]):
self.block4[i].drop_path.drop_prob = dpr[cur + i]
def freeze_patch_emb(self):
self.patch_embed1.requires_grad = False
@torch.jit.ignore
def no_weight_decay(self):
return {'pos_embed1', 'pos_embed2', 'pos_embed3', 'pos_embed4', 'cls_token'} # has pos_embed may be better
def get_classifier(self):
return self.head
def reset_classifier(self, num_classes, global_pool=''):
self.num_classes = num_classes
self.head = nn.Linear(self.embed_dim, num_classes) if num_classes > 0 else nn.Identity()
def forward_features(self, x):
B = x.shape[0]
outs = []
# stage 1
x, H, W = self.patch_embed1(x)
for i, blk in enumerate(self.block1):
x = blk(x, H, W)
x = self.norm1(x)
x = x.reshape(B, H, W, -1).permute(0, 3, 1, 2).contiguous()
outs.append(x)
# stage 2
x, H, W = self.patch_embed2(x)
for i, blk in enumerate(self.block2):
x = blk(x, H, W)
x = self.norm2(x)
x = x.reshape(B, H, W, -1).permute(0, 3, 1, 2).contiguous()
outs.append(x)
# stage 3
x, H, W = self.patch_embed3(x)
for i, blk in enumerate(self.block3):
x = blk(x, H, W)
x = self.norm3(x)
x = x.reshape(B, H, W, -1).permute(0, 3, 1, 2).contiguous()
outs.append(x)
# stage 4
x, H, W = self.patch_embed4(x)
for i, blk in enumerate(self.block4):
x = blk(x, H, W)
x = self.norm4(x)
x = x.reshape(B, H, W, -1).permute(0, 3, 1, 2).contiguous()
outs.append(x)
return outs
def forward(self, x):
x = self.forward_features(x)
# x = self.head(x)
return x
class DWConv(nn.Module):
def __init__(self, dim=768):
super(DWConv, self).__init__()
self.dwconv = nn.Conv2d(dim, dim, 3, 1, 1, bias=True, groups=dim)
def forward(self, x, H, W):
B, N, C = x.shape
x = x.transpose(1, 2).view(B, C, H, W)
x = self.dwconv(x)
x = x.flatten(2).transpose(1, 2)
return x
def trans4pass_v1(*args):
return Trans4PASS_Backbone(
patch_size=4, embed_dims=[64, 128, 320, 512], num_heads=[1, 2, 5, 8], mlp_ratios=[4, 4, 4, 4],
qkv_bias=True, norm_layer=partial(nn.LayerNorm, eps=1e-6), depths=[2, 2, 2, 2], sr_ratios=[8, 4, 2, 1],
drop_rate=0.0, drop_path_rate=0.1)
def trans4pass_v2(*args):
return Trans4PASS_Backbone(
patch_size=4, embed_dims=[64, 128, 320, 512], num_heads=[1, 2, 5, 8], mlp_ratios=[4, 4, 4, 4],
qkv_bias=True, norm_layer=partial(nn.LayerNorm, eps=1e-6), depths=[3, 4, 6, 3], sr_ratios=[8, 4, 2, 1],
drop_rate=0.0, drop_path_rate=0.1)
class DeformableProjEmbed(nn.Module):
""" feature map to Projected Embedding
"""
def __init__(self, in_chans=512, emb_chans=128):
super().__init__()
self.kernel_size = kernel_size = 3
self.stride = stride = 1
self.padding = padding = 1
self.proj = nn.Conv2d(in_chans, emb_chans, kernel_size=kernel_size, stride=stride,
padding=padding)
# --- deformable offset and modulator
self.offset_conv = nn.Conv2d(in_chans, 2 * kernel_size * kernel_size, kernel_size=kernel_size,
stride=stride, padding=padding)
nn.init.constant_(self.offset_conv.weight, 0.)
nn.init.constant_(self.offset_conv.bias, 0.)
self.modulator_conv = nn.Conv2d(in_chans, 1 * kernel_size * kernel_size, kernel_size=kernel_size,
stride=stride, padding=padding)
nn.init.constant_(self.modulator_conv.weight, 0.)
nn.init.constant_(self.modulator_conv.bias, 0.)
self.norm = nn.BatchNorm2d(emb_chans)
self.act = nn.GELU()
def deform_proj(self, x):
# h, w = x.shape[2:]
max_offset = min(x.shape[-2], x.shape[-1]) // 4
offset = self.offset_conv(x).clamp(-max_offset, max_offset)
modulator = 2. * torch.sigmoid(self.modulator_conv(x))
x = torchvision.ops.deform_conv2d(input=x,
offset=offset,
weight=self.proj.weight,
bias=self.proj.bias,
padding=self.padding,
mask=modulator,
stride=self.stride,
)
return x
def forward(self, x):
x = self.deform_proj(x)
x = self.act(self.norm(x))
return x
这里因为上一步已经生成了feature map,这步相当于对于feature_map进行网络后处理,pos-embeded,然后再这个里边学习一下feature map中特征对于影像特征的影像,调整一下影像的不同特征的权重?
chatgpt回答(这又是个黑盒问题):
这个自适应形变卷积模块学习的参数包括:
-
偏移量 (offset):形变卷积中使用的偏移量参数。偏移量决定了每个位置上采样的像素的位置偏移量,从而实现形变卷积的效果。在
offset_conv
中学习这些参数。 -
调制器 (modulator):调制器用于调节形变卷积中每个位置的采样权重,影响每个位置输入的贡献程度。在
modulator_conv
中学习这些参数。 -
投影权重 (projection weights):投影权重用于将输入特征图映射到嵌入特征空间。这些权重由
proj
中的卷积层学习。
这些参数的学习使得形变卷积能够根据输入数据自适应地调整形状和权重,以更好地捕捉空间信息和特征关系。通过学习这些参数,形变卷积可以更好地适应不同的输入数据和任务需求。
在这段代码中,使用了自适应的形变卷积(deformable convolution),其中包括两个关键的组件:偏移量(offset)和调制器(modulator)。这两个组件由 offset_conv
和 modulator_conv
两个卷积层生成。
在初始化这些卷积层时,通过使用 nn.init.constant_
方法将权重和偏置设置为零。这是因为在初始阶段,我们希望这些偏移量和调制器的影响尽可能小,以便网络可以从头开始学习它们的适当值。
随着训练的进行,这些偏移量和调制器的值会随着梯度更新而调整,以更好地适应数据特征的形变情况。因此,在网络的初始阶段,将这些参数初始化为零是一个合理的做法,因为它可以提供一个良好的起点,让网络自行学习适合的参数值。
Deformable MLP
进入到多层感知机完成对于影像的分类任务,估计是输入像素后,后边有19个类别,对于特征进行实际分类,这个 DeformableMLP
模块用于实现形变多层感知机。让我们来看看它的各个组成部分和功能:
-
初始化参数:
in_channels
:输入通道数。out_channels
:输出通道数。kernel_size
:卷积核大小。stride
:步幅,默认为1。padding
:填充大小,默认为0。dilation
:膨胀率,默认为1。groups
:分组卷积中的组数,默认为1。bias
:是否使用偏置。
-
模块组成:
weight
:卷积核参数,通过学习而来。bias
:偏置参数,可选。offset_modulator_conv
:偏移量和调制器的卷积层。norm
:批归一化层。act
:GELU激活函数。
-
前向传播:
offset_modulator_conv
生成偏移量和调制器。- 使用生成的偏移量和调制器,利用
torchvision.ops.deform_conv2d
执行形变卷积操作。 - 最后通过批归一化层和激活函数进行激活。
-
重置参数:使用 kaiming_uniform_ 初始化权重,bias(如果存在)则使用 uniform_ 初始化。
-
额外表示:提供一个字符串,显示模块的配置。
这个模块中的关键是形变卷积的实现,它能够根据输入数据自适应地调整形状和权重,从而更好地捕捉空间信息和特征关系。
class DeformableMLP(nn.Module):
def __init__(
self,
in_channels: int,
out_channels: int,
kernel_size,
stride: int = 1,
padding: int = 0,
dilation: int = 1,
groups: int = 1,
bias: bool = True,
):
super(DeformableMLP, self).__init__()
if in_channels % groups != 0:
raise ValueError('in_channels must be divisible by groups')
if out_channels % groups != 0:
raise ValueError('out_channels must be divisible by groups')
if stride != 1:
raise ValueError('stride must be 1')
if padding != 0:
raise ValueError('padding must be 0')
self.in_channels = in_channels
self.out_channels = out_channels
self.kernel_size = kernel_size
self.stride = _pair(stride)
self.padding = _pair(padding)
self.dilation = _pair(dilation)
self.groups = groups
self.weight = nn.Parameter(torch.empty(out_channels, in_channels // groups, 1, 1)) # kernel size == 1
if bias:
self.bias = nn.Parameter(torch.empty(out_channels))
else:
self.register_parameter('bias', None)
self.offset_modulator_conv = DWConv2d(in_channels, 3 * in_channels)
self.norm = nn.BatchNorm2d(in_channels)
self.act = nn.GELU()
self.reset_parameters()
def reset_parameters(self) -> None:
init.kaiming_uniform_(self.weight, a=math.sqrt(5))
if self.bias is not None:
fan_in, _ = init._calculate_fan_in_and_fan_out(self.weight)
bound = 1 / math.sqrt(fan_in)
init.uniform_(self.bias, -bound, bound)
def forward(self, input: Tensor) -> Tensor:
B, C, H, W = input.size()
offset_modulator = self.offset_modulator_conv(input)
offset_y, offset_x, modulator = torch.chunk(offset_modulator, 3, dim=1)
modulator = 2. * torch.sigmoid(modulator)
offset = torch.cat((offset_y, offset_x), dim=1)
max_offset = max(H, W) // 4
offset = offset.clamp(-max_offset, max_offset)
x = torchvision.ops.deform_conv2d(input=input,
offset=offset,
weight=self.weight,
bias=self.bias,
padding=self.padding,
mask=modulator,
stride=self.stride,
dilation=self.dilation
)
x = self.act(self.norm(x))
return x
def extra_repr(self) -> str:
# s = self.__class__.__name__ + '('
s = ''
s += '{in_channels}'
s += ', {out_channels}'
s += ', kernel_size={kernel_size}'
s += ', stride={stride}'
s += ', padding={padding}' if self.padding != (0, 0) else ''
s += ', dilation={dilation}' if self.dilation != (1, 1) else ''
s += ', groups={groups}' if self.groups != 1 else ''
s += ', bias=False' if self.bias is None else ''
# s += ')'
return s.format(**self.__dict__)
class DeformableMLPBlock(nn.Module):
def __init__(self, in_chans=512, emb_chans=64):
super().__init__()
# spatial deformable proj
self.sdp = DeformableProjEmbed(in_chans=in_chans, emb_chans=emb_chans)
self.h_mlp = DeformableMLP(emb_chans, emb_chans, (1, 3), 1, 0)
self.w_mlp = DeformableMLP(emb_chans, emb_chans, (3, 1), 1, 0)
self.c_mlp = nn.Linear(emb_chans, emb_chans)
self.proj = nn.Linear(emb_chans, emb_chans)
def forward(self, x):
x = self.sdp(x)
# B, C, H, W = x.shape
h = self.h_mlp(x).permute(0, 2, 3, 1)
w = self.w_mlp(x).permute(0, 2, 3, 1)
x = x.permute(0, 2, 3, 1)
x = x + h + w
c = self.c_mlp(x)
x = x + c
x = self.proj(x)
x = x.permute(0, 3, 1, 2).contiguous()
return x
这段代码定义了一个名为 DeformableMLPBlock
的模块,它实现了一种基于形变操作的多层感知机块。下面是对代码的详细解释:
-
初始化参数:
in_chans
:输入特征图的通道数。emb_chans
:形变后的特征图通道数。
-
模块组成:
sdp
:空间形变投影嵌入层,将输入特征图映射到形变后的特征空间。h_mlp
和w_mlp
:分别是沿着水平和垂直方向进行形变的多层感知机模块。它们采用了DeformableMLP
类,通过形变卷积操作对特征图进行空间变换。c_mlp
:通道方向上的多层感知机模块,用于对特征图的通道维度进行变换。proj
:最终的投影层,将形变后的特征图映射回原始通道空间。
-
前向传播:
- 首先,输入特征图经过
sdp
模块进行形变投影。 - 然后,分别对形变后的特征图沿水平和垂直方向应用
h_mlp
和w_mlp
进行形变处理,并与原始特征图相加。 - 接着,将特征图沿通道维度输入到
c_mlp
进行通道维度的变换,并再次与原始特征图相加。 - 最后,通过
proj
层将形变后的特征图映射回原始通道空间,并返回结果。
- 首先,输入特征图经过
这个模块的核心思想是利用形变操作对输入特征图进行空间和通道维度的变换,从而提取更丰富的空间信息和语义特征。
class DMLP(nn.Module):
def __init__(self, num_classes=19, emb_chans=128):
super().__init__()
self.head1 = DeformableMLPBlock(in_chans=64, emb_chans=emb_chans)
self.head2 = DeformableMLPBlock(in_chans=128, emb_chans=emb_chans)
self.head3 = DeformableMLPBlock(in_chans=320, emb_chans=emb_chans)
self.head4 = DeformableMLPBlock(in_chans=512, emb_chans=emb_chans)
self.pred = nn.Conv2d(emb_chans, num_classes, 1)
def forward(self, c1, c2, c3, c4):
size = c1.size()[2:]
c4 = self.head4(c4) # shape: B, 128, H/4, W/4
c4 = F.interpolate(c4, size, mode='bilinear', align_corners=True)
c3 = self.head3(c3)
c3 = F.interpolate(c3, size, mode='bilinear', align_corners=True)
c2 = self.head2(c2)
c2 = F.interpolate(c2, size, mode='bilinear', align_corners=True)
c1 = self.head1(c1)
x_feats_de = [c1, c2, c3, c4]
out = c1 + c2 + c3 + c4
out = self.pred(out)
return x_feats_de, out
降采样了四层,在这四层嵌入特征分别操作,经过MLP最后获得最终的预测结果
这段代码定义了一个名为DMLP(Deformable MLP)的神经网络模型。让我来逐步解释:
-
__init__
方法:- 在初始化方法中,首先调用了父类的初始化方法
super().__init__()
。 - 接着创建了四个DeformableMLPBlock块,分别命名为
head1
、head2
、head3
和head4
。这些块用于处理输入的不同特征图,并通过自适应形变多层感知器(Deformable MLP)进行特征提取和转换。 - 创建了一个用于分类的卷积层
pred
,该卷积层将最终的特征映射转换为预测的类别数。这里使用了1x1的卷积核来进行通道的降维,将特征通道数调整为类别数目。
- 在初始化方法中,首先调用了父类的初始化方法
-
forward
方法:- 在前向传播方法中,接受四个输入特征图
c1
、c2
、c3
、c4
,这些特征图来自于不同层次的网络结构,例如编码器的不同阶段或者不同分辨率的特征图。 - 首先获取输入特征图
c1
的大小,并在后续操作中用于插值操作。 - 分别将
c4
、c3
、c2
、c1
通过四个DeformableMLPBlock进行处理。这些处理过程包括自适应形变多层感知器(Deformable MLP)的应用,该操作能够提取特征并捕捉特征之间的空间关系。 - 对处理后的特征图进行插值操作,将其调整为与输入特征图相同的大小。
- 将经过处理的特征图相加得到最终的特征表示
out
,然后通过pred
卷积层进行分类预测,得到最终的预测输出。 - 返回处理后的特征图列表
x_feats_de
和预测输出out
。
- 在前向传播方法中,接受四个输入特征图
Mutual Prototypical Adaptation
这段文字描述了一种名为 Mutual Prototypical Adaptation (MPA) 的方法,旨在实现从针孔 (PCS) 图像到全景 (PAN) 图像的领域自适应。让我们逐点解释一下关键内容:
-
动机:
- 由于全景图像缺乏大规模训练数据,因此重点放在从针孔到全景图像的领域自适应上。
- 目标是利用语义原型在领域之间转移知识。
-
方法概述:
- MPA 利用来自源(针孔)地面实况标签和目标(全景)伪标签的语义原型。
- 目标域的伪标签是基于针孔和全景图像之间共享的相互属性生成的,例如正面观看角度处的场景分布。
- 虽然以前的方法如 PCS 专注于在和跨领域中进行实例-原型学习,但 MPA 从源和目标特征嵌入中学习相互原型,并投影到共享的潜在空间中。
- 这些相互原型存储在动态库中,允许在自适应过程中进行调整。
-
与 PCS 的关键区别:
- MPA 与 PCS 在两个主要方面有所不同:
- MPA 中的相互原型由来自源和目标领域的嵌入组合而成,促进了跨领域知识转移。
- MPA 利用了多尺度金字塔特征,通过在计算嵌入时使用不同的输入尺度获得,从而产生更健壮的原型。这种多尺度方法增强了学习到的原型的适应性和泛化能力。
- MPA 与 PCS 在两个主要方面有所不同:
总的来说,MPA 旨在通过利用针孔和全景图像之间的相互属性,为跨领域知识转移生成健壮的语义原型,从而提高领域自适应性能。主要方法就是生成伪标签然后用GAN的方法然后进行对抗学习。至此代码和论文精读全部完成,后续实验将在下篇文章补充。