OPAM模型(细粒度图像分类)
- 摘要
- Abstract
- 1. OPAM
- 1.1 文献摘要
- 1.2 细粒度图像分类
- 1.3 研究背景
- 1.4 OPAM模型创新点
- 1.5 OPAM模型
- 1.5.1 补丁过滤
- 1.5.2 显着性提取
- 1.5.3 细粒度区域级注意模型
- 对象-空间约束方法(Object spatial constraint)
- 部分空间约束方法(Part spatial constraint)
- 部分区域对齐(Part Alignment)
- 1.5.4 最终预测
- 1.6 实验
- 2. Res2Net代码实现
- 总结
摘要
在细粒度图像分类的背景下,寻找对象和有区别的部分可以被视为两级注意力过程,其中一个是对象级,另一个是部分级。 一个直观的想法是使用对象注释(即对象的边界框)进行对象级注意,使用零件注释(即零件位置)进行零件级注意。 大多数现有方法依赖于对象或部分注释来查找对象或有区别的部分,但这种标记非常耗费人力。 OPAM模型综合了两个层次的注意模型:对象层定位图像对象,局部层选择对象的区分部分。这两个层面的关注共同促进了多视角、多尺度的特征学习,增强了它们之间的相互促进作用。本文将详细介绍OPAM模型。
Abstract
In the context of fine-grained image categorization, finding objects and differentiated parts can be viewed as a two-level attentional process, where one is object-level and the other is part-level. An intuitive idea is to use object annotations (i.e., the bounding box of an object) for object-level attention and part annotations (i.e., the location of a part) for part-level attention. Most existing methods rely on object or part annotations to find objects or differentiated parts, but this kind of markup is very labor intensive. The OPAM model synthesizes a two-level attention model: the object level locates image objects, and the local level selects differentiated parts of objects. Together, these two levels of attention facilitate multi-view, multi-scale feature learning, enhancing their mutual reinforcement. In this paper, the OPAM model is described in detail.
1. OPAM
文献出处:Object-Part Attention Model for
Fine-grained Image Classification
1.1 文献摘要
细粒度图像分类是要识别属于同一基本类别的数百个子类别,例如属于鸟类的 200 个子类别,由于同一子类别内方差较大而不同子类别之间方差较小,因此具有很高的挑战性。 现有的方法通常首先定位对象或部分,然后判别图像属于哪个子类别。 然而,它们主要有两个局限性:
- 依赖对象或部分注释,这非常耗费人力。
- 忽略对象与其部分之间以及这些部分之间的空间关系。
本文提出了用于弱监督细粒度图像分类的对象部分注意模型(OPAM),其主要新颖之处在于:
- 对象部分注意模型集成了两个级别的注意:对象级注意定位图像的对象,部分- 水平注意力选择对象的有区别的部分。 两者共同学习多视图和多尺度特征,以增强它们的相互促进。
- 对象-部分空间约束模型结合了两种空间约束:对象空间约束确保所选部分具有高度代表性,部分空间约束消除冗余并增强所选部分的区分度。
两者共同用于利用细微的局部差异来区分子类别。
1.2 细粒度图像分类
细粒度图像分类非常具有挑战性,旨在识别同一基本级别类别下的数百个子类别,例如鸟类、汽车、宠物、花卉和 飞机。 而基础级图像分类只需要区分基础级类别,例如鸟或汽车。 基础级和细粒度图像分类之间的区别如下图所示。由于物体外观差异较小,细微的局部差异是细粒度图像分类的关键点,例如鸟类的背部颜色、喙形状和羽毛纹理。
由于这些细微和局部的差异位于判别对象和部分,因此大多数现有方法通常遵循定位图像中的对象或部分,然后判别图像属于哪个子类别的策略。
1.3 研究背景
为了定位有区别的对象和部分,通常首先执行通过自下而上的过程生成具有高对象性的图像块,这意味着生成的块包含有区别的对象或部分。 选择性搜索是一种无监督方法,可以生成数千个此类图像块。
自下而上和自上而下通常用来描述不同类型的信息处理或特征提取过程。
- 自下而上(Bottom-up):这种过程从数据的原始表示或低级特征开始,逐渐构建更高级别的表示或特征。在图像处理中,可以从像素级别逐步提取边缘、角点等特征,然后组合成更复杂的形状和对象。
- 自上而下(Top-down):这种过程从高级别的概念或语义开始,通过向下层级传播信息来指导和调整底层特征的提取。在目标检测中,可以先根据整体场景推测可能的对象位置,然后再在这些位置上进行细致的特征提取和分类。
由于自下而上的过程具有较高的召回率但精度较低,因此必须去除噪声图像块并保留包含对象或判别部分的图像块,这可以通过自上而下的注意模型来实现。
在深度学习中,召回率(Recall) 是衡量分类模型性能的指标之一,通常用于评估模型对于正样本的识别能力。召回率表示模型能够正确识别出的正样本数量占所有正样本数量的比例。其计算公式为:Recall = TP/(TP+FN),其中,TP(True Positive)表示模型将正样本正确地预测为正样本的数量,FN(False Negative)表示模型将正样本错误地预测为负样本的数量。召回率的取值范围是0到1之间,值越接近1表示模型对正样本的识别能力越强。
在细粒度图像分类的背景下,寻找对象和有区别的部分可以被视为两级注意力过程,其中一个是对象级,另一个是部分级。 一个直观的想法是使用对象注释(即对象的边界框)进行对象级注意,使用零件注释(即零件位置)进行零件级注意。 大多数现有方法依赖于对象或部分注释来查找对象或有区别的部分,但这种标记非常耗费人力。
1.4 OPAM模型创新点
本文提出了用于弱监督细粒度图像分类的对象部分注意模型(OPAM)。其主要创新点和贡献可概括如下:
- Object-Part Attention Model:大多数现有工作依赖于对象或部分注释,而标记非常耗费人力。 为了解决这个重要问题,作者提出了用于弱监督细粒度图像分类的对象部分注意模型,以避免使用对象和细粒度区域标注并走向实际应用。 它集成了两级注意力:对象级注意力模型利用CNN中的全局平均池化来提取显着图来定位图像的对象,即学习对象特征。 部分级注意力模型首先选择有判别性的部分,然后根据神经网络的聚类模式对部分进行对齐,即学习细微的局部特征。 对象级注意力模型侧重于代表性对象外观,部分级注意力模型侧重于区分子类别之间部分的具体差异。 两者联合运用,促进多视角、多尺度特征学习,增强相互促进,在细粒度图像分类方面取得良好的性能。
- Object-Part Spatial Constraint Model:大多数现有的弱监督方法忽略了对象与其部分之间以及这些部分之间的空间关系,这两者对于判别性部分选择非常有帮助。 为了解决这个问题,作者提出了由对象部分空间约束模型驱动的部分选择方法,该方法结合了两种类型的空间约束:(1)对象空间约束强制所选部分位于对象区域中并且具有高度代表性。 (2) 部分空间约束减少了细粒度区域之间的重叠,突出了部分区域的显着性,消除了冗余,增强了所选部分区域的区分度。 两种空间约束的结合不仅通过利用细微和局部的区别显着促进了有判别性的部分选择,而且在细粒度图像分类方面也取得了显着的改进。
1.5 OPAM模型
细粒度图像分类通常首先定位对象(对象级注意力),然后区分部分(部分级注意力)。 例如,识别包含田麻雀的图像遵循以下过程:首先找到一只鸟,然后关注将其与其他鸟类子类别区分开来的区分部分。
作者在本文提出了用于弱监督细粒度图像分类的对象部分注意模型OPAM,该模型在训练和测试阶段既不使用对象也不使用部分注释,而仅使用图像级子类别标签。 如下图所示,OPAM模型首先通过对象级注意模型来定位图像对象以学习对象特征,然后通过部分级注意模型选择有区别的部分以学习微妙和局部特征。
大多数现有的弱监督工作致力于判别部分选择,但忽略了目标定位,目标定位可以消除图像中背景噪声的影响,以学习有意义且有代表性的目标特征。 尽管有些方法同时考虑对象定位和细粒度区域的选择,但它们依赖于对象和细粒度目标区域的标注。
为了解决这个重要问题,作者提出了一种基于显着性提取的对象级注意力模型,仅使用图像级子类别标签自动定位图像对象,而不需要任何对象或细粒度区域标注。
该模型由两个部分组成:补丁过滤和显着性提取。 第一个组件是滤除噪声图像块并保留与目标相关的图像块,用于训练称为 ClassNet 的 CNN,以学习特定子类别的多视图和多尺度特征。 第二个部分是通过 CNN 中的全局平均池化来提取显着图,以定位图像内的对象。
1.5.1 补丁过滤
自下而上的过程可以通过将像素分组到可能包含对象的区域来生成数千个候选图像块。 由于这些图像块与对象的相关性,它们可以用作训练数据的扩展。 因此,采用选择性搜索来为给定图像生成 候选图像块,这是一种无监督且广泛使用的自下而上处理方法。 这些候选图像块提供了原始图像的多个视图和尺度,这有利于训练有效的 CNN 以实现更好的细粒度图像分类精度。 然而,这些补丁不能直接使用,因为召回率高但精度低,这意味着存在一些噪音。
作者通过CNN网络去除噪声补丁并且选择出正确的相关补丁,该 CNN 在 ImageNet 1K 数据集上进行预训练,然后对训练数据进行微调。
作者将属于输入图像子类别的softmax层中神经元的激活定义为选择置信度分数,然后设置阈值来决定是否应该选择给定的候选图像块。 然后我们获得与对象相关的图像块,具有多个视图和尺度。 如下图所示:
1.5.2 显着性提取
在这个阶段,采用CAM来获得子类别c的图像的显着图
M
c
M_c
Mc 来定位对象。 显着图表示CNN用来识别图像子类别的代表区域,如下图的第二行所示。然后通过执行以下操作获得图像的对象区域,如下图的第三行所示 显着图上的二值化和连接区域提取。
通俗理解就是首先确定图像子类别的大致区域,然后根据大致区域再去锁定目标对象的位置。
给定图像 I,最后一个卷积层中神经元 u u u 在空间位置 ( x , y ) (x, y) (x,y) 处的激活定义为 f u ( x , y ) f_{u}(x, y) fu(x,y), w u c w^{c}_{u} wuc 定义神经元 u u u 对应于子类别 c c c 的权重。 子类别 c c c 的空间位置 ( x , y ) (x, y) (x,y) 处的显着性值计算如下:
M
c
(
x
,
y
)
M_{c}(x, y)
Mc(x,y) 直接指示了在空间位置
(
x
,
y
)
(x, y)
(x,y) 处的激活对将图像分类为子类别
c
c
c 的重要性。没有使用图像级别的子类别标签,而是使用预测结果作为每个图像中的子类别
c
c
c 来提取显著性。通过对象级别的注意力模型,我们在图像中定位对象以训练一个名为ObjectNet的CNN,以获取对象级别注意力的预测,如下图所示:通过卷积、池化、全连接,最终获取对象级预测结果
1.5.3 细粒度区域级注意模型
以往方法的缺陷
由于头部和身体等可区分部分是细粒度图像分类的关键,以前的研究都是从自下而上的选择性搜索等过程产生的候选图像块中选择可区分部分。然而,这些研究方法都依赖于区域特征标注,耗费了大量的人力。虽然一些工作开始集中于寻找有区别的部分,而不使用任何标注,但它们忽略了对象与其区域特征之间以及这些区域特征之间的空间关系。
作者提出的
作者提出了一种新的区域特征选择方法,该方法由图像区域级别的注意驱动,利用细微的局部区分来区分子类别,既不使用对象也不使用部分级标注。它由两部分组成:对象-部分空间约束模型和区域特征对齐。第一步是选择有区别的部分,第二步是根据语义将所选择的部分对齐成簇。
对象-部分空间约束模型
通过对象级注意力模型获取图像的目标区域,然后利用对象-部分空间约束模型从自下而上过程产生的候选图像块中选择可区分的部分。综合考虑了两个空间约束:对象空间约束定义了对象与其部分之间的空间关系,部分空间约束定义了这些部分之间的空间关系。对于给定的图像 I I I,通过目标级注意力模型得到其显著图 M M M 和目标区域 b b b。
然后用 对象-部分空间约束模型 来驱动局部选择:设
P
P
P 表示所有候选图像块,
P
=
p
1
,
p
2
,
…
,
p
n
P={p_1,p_2,…,p_n}
P=p1,p2,…,pn表示从P中选择的n个部分作为每幅给定图像的区分部分。对象-部件空间约束模型通过解决以下优化问题来考虑两个空间约束的组合:
其中,
∆
(
P
)
∆(P)
∆(P) 被定义为两个空间约束上的得分函数,定义了提出的对象-部件空间约束,保证了所选部件的代表性和区分性。它由两个约束组成:对象空间约束
∆
b
o
x
(
P
)
∆_{box}(P)
∆box(P) 和部分空间约束
∆
p
a
r
t
(
P
)
∆_{part}(P)
∆part(P),这两个约束都应由所有选定的区域同时满足,作为利用乘积运算来优化两个约束的工作。如下所示:
对象-空间约束方法(Object spatial constraint)
忽略物体与其各部分之间的空间关系,使得所选部分可能具有较大的背景噪声面积和较小的区分区域面积,从而降低了所选部分的代表性。由于区分部分位于对象区域内,因此空间约束函数可以被定义为:
其中:
并且
I
o
U
(
P
i
)
IoU_{(Pi)}
IoU(Pi)定义零件区域和对象区域的并集交集(IOU)重叠的比例。
[注意]:对象区域是通过对象级别注意力模型自动获得的,而不是由对象注释提供的。
对象空间约束旨在同时约束对象区域内的所有选定部分。当 I o U IoU IoU值等于0,将不被选为区分部分。
部分空间约束方法(Part spatial constraint)
忽略这些部分之间的空间关系会导致选择的部分可能会有很大的重叠,并且忽略了一些有区别性的部分。显著图表明了图像的区分性,有利于区分部分的选择。将显著度和部件之间的空间关系联合建模如下:
其中,
A
U
A_U
AU是n个部分的并集面积,
A
I
A_I
AI是n个部分的交集面积,
A
o
A_o
Ao是对象区域外的面积,平均值(MAU)定义如下:
其中像素
(
i
,
j
)
(i,j)
(i,j)位于部件的并集区域中,
M
i
j
M_{ij}
Mij是指像素
(
i
,
j
)
(i,j)
(i,j)的显著性值,并且
∣
A
U
∣
|A_U|
∣AU∣是指位于n个部件的并集区域中的像素数。
部分区域空间约束由两项组成:第一项旨在减少所选区域之间的重叠,通过 l o g ( A U − A I − A O ) log(A_U−A_I−A_O) log(AU−AI−AO)来实现,其中 − A I −A_I −AI保证所选区域具有最小的重叠,而 − A O −A_O −AO确保所选零件在目标区域内具有最大的面积。第二项以最大化选定部分的显著为目标,通过 l o g ( M e a n ( M A U ) ) log(Mean(M_{A_U})) log(Mean(MAU))来实现, l o g ( M e a n ( M A U ) ) log(Mean(M_{A_U})) log(Mean(MAU))表示选定部分的联合区域中所有像素的平均显著值。
部分区域对齐(Part Alignment)
如图下图所示,通过对象-部分空间约束模型选择的部分是杂乱无章的,并且其语义不对齐。
这些具有不同语义的部分对最终预测的贡献不同,直观的想法是将具有相同语义的部分排列在一起。 如上图所示,有几组神经元对鸟的头部有显著的反应,另一些神经元对鸟的身体有明显的反应,尽管它们可能对应于不同的姿势。因此,对ClassNet中中间层的神经元进行聚类,以构建用于对所选部件进行对齐的部件簇。
1.5.4 最终预测
作者使用局部对象和区分部分对ClassNet进行了微调,得到了两个分类器,分别称为对象网和部分网。ClassNet、ObjectNet和PartNet都是细粒度的图像分类器:ClassNet用于原始图像,ObjectNet用于对象,PartNet用于选定的区分部分。然而,它们的影响和优势是不同的,主要是因为它们关注的是形象的不同性质。如下图所示,对象级注意力模型首先驱动滤网选择与对象相关的具有多个视图和尺度的图像块。这些图像块驱动ClassNet学习更具代表性的特征,并通过显著提取定位对象区域。部件级注意模型选择包含细微和局部特征的区别性部分。不同层次的焦点(即原始图像、原始图像的对象和原始图像的部分)具有不同的表示形式,并相互补充以提高预测效果。最后,使用以下方程将三个不同级别的预测结果合并:
其中,原始分数、对象分数和部分分数分别是类网、对象网和部件网的Softmax值,并且α,β和γ是通过使用k倍交叉验证方法来选择的。最终得分最高的子类别被选为最终预测结果。
1.6 实验
作者给出了OPAM方法在4个广泛使用的细粒度图像分类数据集上的实验结果和分析,以及最新的方法。下表显示了在CUB-2002011数据集上的比较结果。
在训练阶段和测试阶段都不使用对象和部分标注的相同设置下,我们的方法是所有方法中最好的,并且获得了比FOAF的最佳比较结果(85.83%比84.63%)高1.20%的准确率。值得注意的是,FOAF中使用的CNN不仅在ImageNet 1K数据集[32]上进行了预训练,而且在Pascal VOC的数据集上进行了预训练,而我们的方法没有使用像Pascal VOC这样的外部数据集。与第二高的PD结果相比,作者的方法获得了1.29%的高准确率(85.83%比84.54%),验证了OPAM方法的进一步开发的有效性,该方法结合了对象级和部分级的注意模型,促进了多视角和多尺度的特征学习,并增强了它们的互补性。
2. Res2Net代码实现
文件结构:
main.py:
import torch
import torchvision
from torch import nn
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter
from res2net import *
from res2net import res2net101_26w_4s
from datasets import CUB200
# 准备数据集
train_data = CUB200("./CUB_200_2011", train=True) # 共 5994 张图片
test_data = CUB200("./CUB_200_2011", train=False) # 共 5794 张图片
train_data_size = len(train_data)
test_data_size = len(test_data)
print("训练数据集的长度为:{}".format(train_data_size))
print("测试数据集的长度为:{}".format(test_data_size))
# 利用 DataLoader 来加载数据集
train_dataloader = DataLoader(train_data, batch_size=64)
test_dataloader = DataLoader(test_data, batch_size=64)
# 创建网络模型
Test_module = res2net101_26w_4s(pretrained=True)
print(Test_module)
# 创建损失函数
loss_fn = nn.CrossEntropyLoss()
# 优化器
learning_rate = 0.01
optimizer = torch.optim.SGD(Test_module.parameters(), lr=learning_rate)
# 设置训练网络的一些参数
# 记录训练的次数
total_train_step = 0
# 记录测试的次数
total_test_step = 0
# 训练的轮数
epoch = 200
# 添加tensorboard
writer = SummaryWriter("./logs_train")
for i in range(epoch):
print("------第 {} 轮训练开始------".format(i+1))
for data in train_dataloader:
# 训练步骤开始
imgs, targets = data
outputs = Test_module(imgs)
loss = loss_fn(outputs, targets)
# 优化器优化模型
optimizer.zero_grad()
loss.backward()
optimizer.step()
total_train_step += 1
if total_train_step % 100 == 0:
print("训练次数:{}, loss:{}".format(total_train_step, loss.item()))
writer.add_scalar("train_loss", loss.item(), total_test_step)
# 测试步骤开始
total_test_loss = 0
# 整体的正确率
total_accuracy = 0
with torch.no_grad():
for data in test_dataloader:
imgs, targets = data
m = nn.Dropout2d(p=0.01)
datas = m(imgs)
outputs = Test_module(datas)
loss = loss_fn(outputs, targets)
total_test_loss = total_test_loss + loss.item()
accuracy = (outputs.argmax(1) == targets).sum()
total_accuracy = total_accuracy + accuracy
print("整体测试集上的Loss:{}".format(total_test_loss))
print("整体测试集上的正确率:{}".format(total_accuracy/test_data_size))
writer.add_scalar("test_loss", total_test_loss, total_test_step)
writer.add_scalar("test_accuracy", total_accuracy/test_data_size, total_test_step)
total_test_step += 1
# 保存训练的模型
torch.save(Test_module, "Test_module_{}.pth".format(i))
print("模型已保存")
writer.close()
datasets.py
import torchvision
from torch.utils.data import Dataset
import os
from PIL import Image
class CUB200(Dataset):
def __init__(self, root, image_size=64, train=True, transform=torchvision.transforms.ToTensor(), target_transform=None):
'''
从文件中读取图像,数据
'''
self.root = root # 数据集路径
self.image_size = image_size # 图像大小(正方形)
self.transform = transform # 图像的 transform
self.target_transform = target_transform # 标签的 transform
# 构造数据集参数的各文件路径
self.classes_file = os.path.join(root, 'classes.txt') # <class_id> <class_name>
self.image_class_labels_file = os.path.join(root, 'image_class_labels.txt') # <image_id> <class_id>
self.images_file = os.path.join(root, 'images.txt') # <image_id> <image_name>
self.train_test_split_file = os.path.join(root, 'train_test_split.txt') # <image_id> <is_training_image>
self.bounding_boxes_file = os.path.join(root, 'bounding_boxes.txt') # <image_id> <x> <y> <width> <height>
imgs_name_train, imgs_name_test, imgs_label_train, imgs_label_test, imgs_bbox_train, imgs_bbox_test = self._get_img_attributes()
if train: # 读取训练集
self.data = self._get_imgs(imgs_name_train, imgs_bbox_train)
self.label = imgs_label_train
else: # 读取测试集
self.data = self._get_imgs(imgs_name_test, imgs_bbox_test)
self.label = imgs_label_test
def _get_img_id(self):
''' 读取张图片的 id,并根据 id 划分为测试集和训练集 '''
imgs_id_train, imgs_id_test = [], []
file = open(self.train_test_split_file, "r")
for line in file:
img_id, is_train = line.split()
if is_train == "1":
imgs_id_train.append(img_id)
elif is_train == "0":
imgs_id_test.append(img_id)
file.close()
return imgs_id_train, imgs_id_test
def _get_img_class(self):
''' 读取每张图片的 class 类别 '''
imgs_class = []
file = open(self.image_class_labels_file, 'r')
for line in file:
_, img_class = line.split()
imgs_class.append(img_class)
file.close()
return imgs_class
def _get_bondingbox(self):
''' 获取图像边框 '''
bondingbox = []
file = open(self.bounding_boxes_file)
for line in file:
_, x, y, w, h = line.split()
x, y, w, h = float(x), float(y), float(w), float(h)
bondingbox.append((x, y, x+w, y+h))
# print(bondingbox)
file.close()
return bondingbox
def _get_img_attributes(self):
''' 根据图片 id 读取每张图片的属性,包括名字(路径)、类别和边框,并分别按照训练集和测试集划分 '''
imgs_name_train, imgs_name_test, imgs_label_train, imgs_label_test, imgs_bbox_train, imgs_bbox_test = [], [], [], [], [], []
imgs_id_train, imgs_id_test = self._get_img_id() # 获取训练集和测试集的 img_id
imgs_bbox = self._get_bondingbox() # 获取所有图像的 bondingbox
imgs_class = self._get_img_class() # 获取所有图像类别标签,按照 img_id 存储
file = open(self.images_file)
for line in file:
img_id, img_name = line.split()
if img_id in imgs_id_train:
img_id = int(img_id)
imgs_name_train.append(img_name)
imgs_label_train.append(imgs_class[img_id-1]) # 下标从 0 开始
imgs_bbox_train.append(imgs_bbox[img_id-1])
elif img_id in imgs_id_test:
img_id = int(img_id)
imgs_name_test.append(img_name)
imgs_label_test.append(imgs_class[img_id-1])
imgs_bbox_test.append(imgs_bbox[img_id-1])
file.close()
return imgs_name_train, imgs_name_test, imgs_label_train, imgs_label_test, imgs_bbox_train, imgs_bbox_test
def _get_imgs(self, imgs_name, imgs_bbox):
''' 遍历每一张图片的路径,读取图片信息 '''
data = []
for i in range(len(imgs_name)):
img_path = os.path.join(self.root, 'images', imgs_name[i])
img = self._convert_and_resize(img_path, imgs_bbox[i])
data.append(img)
return data
def _convert_and_resize(self, img_path, img_bbox):
''' 将不是 'RGB' 模式的图像变为 'RGB' 格式,更改图像大小 '''
img = Image.open(img_path).resize((self.image_size, self.image_size))
# img.show()
if img.mode == 'L':
img = img.convert('RGB')
if self.transform is not None:
img = self.transform(img)
# print(img)
return img
def __getitem__(self, index):
img, label = self.data[index], self.label[index]
label = int(label) - 1 # 类别从 0 开始
if self.target_transform is not None:
label = self.target_transform(label)
return img, label
def __len__(self):
return len(self.data)
if __name__ == "__main__":
train_set = CUB200("./CUB_200_2011", train=True) # 共 5994 张图片
test_set = CUB200("./CUB_200_2011", train=False) # 共 5794 张图片
Res2Net.py:
import torch.nn as nn
import math
import torch.utils.model_zoo as model_zoo
import torch
import torch.nn.functional as F
__all__ = ['Res2Net', 'res2net50']
model_urls = {
'res2net50_26w_4s': 'https://shanghuagao.oss-cn-beijing.aliyuncs.com/res2net/res2net50_26w_4s-06e79181.pth',
'res2net50_48w_2s': 'https://shanghuagao.oss-cn-beijing.aliyuncs.com/res2net/res2net50_48w_2s-afed724a.pth',
'res2net50_14w_8s': 'https://shanghuagao.oss-cn-beijing.aliyuncs.com/res2net/res2net50_14w_8s-6527dddc.pth',
'res2net50_26w_6s': 'https://shanghuagao.oss-cn-beijing.aliyuncs.com/res2net/res2net50_26w_6s-19041792.pth',
'res2net50_26w_8s': 'https://shanghuagao.oss-cn-beijing.aliyuncs.com/res2net/res2net50_26w_8s-2c7c9f12.pth',
'res2net101_26w_4s': 'https://shanghuagao.oss-cn-beijing.aliyuncs.com/res2net/res2net101_26w_4s-02a759a1.pth',
}
class Bottle2neck(nn.Module):
expansion = 4
def __init__(self, inplanes, planes, stride=1, downsample=None, baseWidth=26, scale=4, stype='normal'):
""" 构造函数
参数:
inplanes: 输入通道维度
planes: 输出通道维度
stride: 卷积步长。替代池化层。
downsample: 当stride = 1时为None
baseWidth: conv3x3的基本宽度
scale: 尺度数量。
type: 'normal': 正常设置。 'stage': 新阶段的第一个块。
"""
super(Bottle2neck, self).__init__()
# 计算卷积核的宽度
width = int(math.floor(planes * (baseWidth / 64.0)))
# 第一个1x1卷积层
self.conv1 = nn.Conv2d(inplanes, width * scale, kernel_size=1, bias=False)
self.bn1 = nn.BatchNorm2d(width * scale)
# 计算重复次数
if scale == 1:
self.nums = 1
else:
self.nums = scale - 1
# 如果是新阶段的第一个块,则使用平均池化层进行下采样
if stype == 'stage':
self.pool = nn.AvgPool2d(kernel_size=3, stride=stride, padding=1)
# 定义重复的卷积层和BN层
convs = []
bns = []
for i in range(self.nums):
convs.append(nn.Conv2d(width, width, kernel_size=3, stride=stride, padding=1, bias=False))
bns.append(nn.BatchNorm2d(width))
# 创建了两个 nn.ModuleList 对象 self.convs 和 self.bns,用于存储多个卷积层和批量归一化层。
self.convs = nn.ModuleList(convs)
self.bns = nn.ModuleList(bns)
# 最后一个1x1卷积层
self.conv3 = nn.Conv2d(width * scale, planes * self.expansion, kernel_size=1, bias=False)
self.bn3 = nn.BatchNorm2d(planes * self.expansion)
# 激活函数
self.relu = nn.ReLU(inplace=True)
self.downsample = downsample
self.stype = stype
self.scale = scale
self.width = width
def forward(self, x):
residual = x
# 第一个1x1卷积层的计算
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
# 将输出按照宽度进行分割
spx = torch.split(out, self.width, 1)
for i in range(self.nums):
# 如果是第一个块或者是新阶段的第一个块,则直接取分割后的部分
if i == 0 or self.stype == 'stage':
sp = spx[i]
else:
# 否则,累加之前的部分
sp = sp + spx[i]
# 对部分进行卷积、BN和ReLU操作
sp = self.convs[i](sp)
sp = self.relu(self.bns[i](sp))
if i == 0:
out = sp
else:
# 将处理后的部分拼接起来
out = torch.cat((out, sp), 1)
# 如果尺度不为1且为正常设置,将最后一个部分拼接到一起
if self.scale != 1 and self.stype == 'normal':
out = torch.cat((out, spx[self.nums]), 1)
# 如果尺度不为1且为新阶段的第一个块,则对最后一个部分进行平均池化并拼接
elif self.scale != 1 and self.stype == 'stage':
out = torch.cat((out, self.pool(spx[self.nums])), 1)
# 最后一个1x1卷积层的计算
out = self.conv3(out)
out = self.bn3(out)
# 如果存在下采样,则对输入进行下采样
if self.downsample is not None:
residual = self.downsample(x)
# 残差连接并进行ReLU激活
out += residual
out = self.relu(out)
return out
class Res2Net(nn.Module):
def __init__(self, block, layers, baseWidth=26, scale=4, num_classes=1000):
# 初始化Res2Net模型
self.inplanes = 64 # 设置输入通道数为64
self.baseWidth = baseWidth
self.scale = scale
super(Res2Net, self).__init__() # 调用父类的构造函数
# 定义网络的第一层:7x7的卷积层,输入通道数为3,输出通道数为64,步长为2,填充为3
self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3, bias=False)
# Batch Normalization层,对每个channel的数据进行标准化
self.bn1 = nn.BatchNorm2d(64)
# 激活函数ReLU
self.relu = nn.ReLU(inplace=True)
# 最大池化层,窗口大小为3x3,步长为2,填充为1
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
# 定义4个Res2Net的阶段(stage)
self.layer1 = self._make_layer(block, 64, layers[0]) # 第一个阶段,输出通道数为64
self.layer2 = self._make_layer(block, 128, layers[1], stride=2) # 第二个阶段,输出通道数为128,步长为2
self.layer3 = self._make_layer(block, 256, layers[2], stride=2) # 第三个阶段,输出通道数为256,步长为2
self.layer4 = self._make_layer(block, 512, layers[3], stride=2) # 第四个阶段,输出通道数为512,步长为2
# 全局平均池化层,将每个通道的特征图变成一个数
self.avgpool = nn.AdaptiveAvgPool2d(1)
# 全连接层,将512维的特征向量映射到num_classes维的向量,用于分类
self.fc = nn.Linear(512 * block.expansion, num_classes)
# 初始化网络参数
for m in self.modules():
if isinstance(m, nn.Conv2d):
# 使用kaiming正态分布初始化卷积层参数
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
elif isinstance(m, nn.BatchNorm2d):
# 将Batch Normalization层的权重初始化为1,偏置初始化为0
nn.init.constant_(m.weight, 1)
nn.init.constant_(m.bias, 0)
def _make_layer(self, block, planes, blocks, stride=1):
# 构建Res2Net的一个阶段(stage),包含多个block
downsample = None
if stride != 1 or self.inplanes != planes * block.expansion:
# 如果输入输出通道数不一致,或者步长不为1,需要添加下采样层
downsample = nn.Sequential(
nn.Conv2d(self.inplanes, planes * block.expansion,
kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(planes * block.expansion),
)
# 构建阶段的每个block
layers = []
layers.append(block(self.inplanes, planes, stride, downsample=downsample,
stype='stage', baseWidth=self.baseWidth, scale=self.scale))
self.inplanes = planes * block.expansion
for i in range(1, blocks):
layers.append(block(self.inplanes, planes, baseWidth=self.baseWidth, scale=self.scale))
return nn.Sequential(*layers)
def forward(self, x):
# 定义前向传播过程
x = self.conv1(x)
x = self.bn1(x)
x = self.relu(x)
x = self.maxpool(x)
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.layer4(x)
x = self.avgpool(x)
x = x.view(x.size(0), -1)
x = self.fc(x)
return x
def res2net50(pretrained=False, **kwargs):
"""Constructs a Res2Net-50 model.
Res2Net-50 refers to the Res2Net-50_26w_4s.
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
"""
model = Res2Net(Bottle2neck, [3, 4, 6, 3], baseWidth = 26, scale = 4, **kwargs)
if pretrained:
model.load_state_dict(model_zoo.load_url(model_urls['res2net50_26w_4s']))
return model
def res2net50_26w_4s(pretrained=False, **kwargs):
"""Constructs a Res2Net-50_26w_4s model.
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
"""
model = Res2Net(Bottle2neck, [3, 4, 6, 3], baseWidth = 26, scale = 4, **kwargs)
if pretrained:
model.load_state_dict(model_zoo.load_url(model_urls['res2net50_26w_4s']))
return model
def res2net101_26w_4s(pretrained=False, **kwargs):
"""Constructs a Res2Net-50_26w_4s model.
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
"""
model = Res2Net(Bottle2neck, [3, 4, 23, 3], baseWidth = 26, scale = 4, **kwargs)
if pretrained:
model.load_state_dict(model_zoo.load_url(model_urls['res2net101_26w_4s']))
return model
def res2net50_26w_6s(pretrained=False, **kwargs):
"""Constructs a Res2Net-50_26w_4s model.
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
"""
model = Res2Net(Bottle2neck, [3, 4, 6, 3], baseWidth = 26, scale = 6, **kwargs)
if pretrained:
model.load_state_dict(model_zoo.load_url(model_urls['res2net50_26w_6s']))
return model
def res2net50_26w_8s(pretrained=False, **kwargs):
"""Constructs a Res2Net-50_26w_4s model.
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
"""
model = Res2Net(Bottle2neck, [3, 4, 6, 3], baseWidth = 26, scale = 8, **kwargs)
if pretrained:
model.load_state_dict(model_zoo.load_url(model_urls['res2net50_26w_8s']))
return model
def res2net50_48w_2s(pretrained=False, **kwargs):
"""Constructs a Res2Net-50_48w_2s model.
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
"""
model = Res2Net(Bottle2neck, [3, 4, 6, 3], baseWidth = 48, scale = 2, **kwargs)
if pretrained:
model.load_state_dict(model_zoo.load_url(model_urls['res2net50_48w_2s']))
return model
def res2net50_14w_8s(pretrained=False, **kwargs):
"""Constructs a Res2Net-50_14w_8s model.
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
"""
model = Res2Net(Bottle2neck, [3, 4, 6, 3], baseWidth = 14, scale = 8, **kwargs)
if pretrained:
model.load_state_dict(model_zoo.load_url(model_urls['res2net50_14w_8s']))
return model
if __name__ == '__main__':
images = torch.rand(1, 3, 224, 224).cuda(0)
model = res2net101_26w_4s(pretrained=True)
model = model.cuda(0)
# print(model(images).size())
print(model)
总结
本周学习了OPAM模型,这篇文献提出了一种用于弱监督细粒度图像分类的OPAM方法,该方法综合了两个层次的注意模型:对象层定位图像对象,局部层选择对象的区分部分。这两个层面的关注共同促进了多视角、多尺度的特征学习,增强了它们之间的相互促进作用。此外,零件选择由对象-零件空间约束模型驱动,该模型结合了两个空间约束:对象空间约束保证了所选零件的高代表性,零件空间约束消除了冗余,增强了所选零件的区分性。这两个空间约束的结合促进了细微的、局部的歧视本土化。在4个广泛使用的数据集上的综合实验结果表明,OPAM方法与10多种最先进的方法相比是有效的。下周我将继续学习细粒度图像分类相关的论文和模型,同时我会进一步提高自己的pytorch代码能力。