介绍 metric learning (度量学习)的概念,并重点分析一篇论文。(仅作为学习笔记)

metric learning

度量学习(metric learning) 是机器学习中常用的一种方法,通过借助一系列观测,构造出对应的度量函数,从而学习数据间的距离或差异,有效描述样本之间的相似度

深度度量学习在 CV 领域中的一些极端任务(类别众多、样本量不足)中表现优异,应用遍及人脸识别、行人重识别、图像检索、目标跟踪等场景。

当样本量不大时,metric learning 在处理分类任务的准确率和高效率上,展现出显著优势。并且经典的分类网络有一个前提:必须提前设定好类别数。

这意味着每增加新的种类,就需要重新定义网络模型,从头开始训练一遍。

metric learning 可以分为两种

一种是 supervised learning, 监督学习单元是单个数据,每个数据都有对应的标签。 metric learning 是学习一种度量可以让相同标签数据距离更近,不同标签数据距离更远。

一般使用 centerloss,amsoftmax。 center loss 是为每个类设定一个 center feature,让类内数据 feature 向着 center 靠拢,拉近类内距;因为 center loss 只负责类内距离,所以通常需要结合 softmax cross entropy loss ,在实现类别可分的情况下拉近类内距。

amsoftmax 是基于 softmax 改进,不同于 softmax, 不仅可以实现类别可分,还能拉近类内距的同时,拉远类间距。

一种是 weakly supervised learning,学习单元是元祖(二元组、三元组),元组一般有正例对和负例对两种。

可以使用 contrastive loss (二元组) 和triplet loss (三元组)

triplet 输入三元组包括 anchor、negative 和 positive

triplet loss

img

如上图所示,triplet是一个三元组,这个三元组是这样构成的:从训练数据集中随机选一个样本,该样本称为Anchor,然后再随机选取一个和Anchor (记为 $x_a$)属于同一类的样本和不同类的样本,这两个样本对应的称为Positive (记为 $x_p$)和Negative (记为$x_n$),由此构成一个(Anchor,Positive,Negative)三元组。

选择合适的三元组对于模型的收敛至关重要。对于给定的 anchor,我们希望得到不那么相似的相似图像(hard positive) 和差异不那么大的非相似图像(hard negative)。

metric learning 又可以称为 distance learning

SoftTriple Loss: Deep Metric Learning Without Triplet Sampling

论文解读

这篇论文属于“魔改softmax”。

It inspires us to investigate the formu- lation of SoftMax. Our analysis shows that SoftMax loss is equivalent to a smoothed triplet loss where each class has a single center. In real-world data, one class can contain several local clusters rather than a single one, e.g., birds of different poses. Therefore, we propose the SoftTriple loss to extend the SoftMax loss with multiple centers for each class.

一般来说 triplet loss 一般搭配着使用 sample mining (hard sample mining)。这篇论文直接去掉了 sampling 机制,增加了 softmax 的输出,进行 metric learning。

image-20210428155007615

使用 softmax 将原来一个类别一个簇修改成一个类别两个簇。(簇的个数是可调的)

带有 sampling 机制 triplet 的现状

mini-batch may not be able to capture the overall neighborhood well

mini-batch contains $m^2$ 或者 $m^3$ triplets, m is the size of mini-batch

some work also tried to reduce the total number of triplets with proxies

compared with existing deep DML methods, the number of triplet in SoftTriple is linear in the number of original examples

image-20210428164230375

这个是 softmax 和 softtriplet 的区别,这点比较核心。

代码解读

多卡并行

DataParallel 是用来可以用来单卡多GPU 训练

1
2
3
4
5
   model = nn.DataParallel(Resnet50(
        embedding_size=config["embedding_size"],
        pretrained=config["pretrained"]
    ))
    model = model.to(device)

优化器方面 ( Rectified Adam, RAdam)

  • 目前初级 sgd ( Gradient Descent, 随机梯度下降) 收敛较好,但是慢。
  • adam收敛快,但是容易收敛到局部解, 常用解决 adam收敛的方法是:自适应启动方法
1
optimizer = RAdam(model.parameters(), lr=config["lr"])

结论:

1
2
(1) 鲁棒性强,这个优势很强,而且适合任何模型的初期的实验,也对新手比较友好;不用调试学习率,这个优势也很强;自适应启动的方式会增加超参数,不适合初期的实验。
(2) 论文也提出,他的收敛效果不一定是所有里面最好的。所以在实验的后期,对于老手,可以采用更加精细的学习率控制策略试试会不会拿到另一个好的结果。

论文: Radam:ON THE VARIANCE OF THE ADAPTIVE LEARNING RATE AND BEYOND

使用

1
2
from torch_optimizer import RAdam
optimizer = RAdam(model.parameters(), lr=config["lr"])

RAdam论文解读

Transforms

ColorJitter 类比较常用,主要修改输入图像的 4 大参数: brightness, contrast, saturation and hue. 亮度、对比度、饱和度和色度。

当为a时,从[max(0, 1 - a), 1 + a]中随机选择。 当为(a,b)时,从[a, b]中选择。

RandomAffine

对图像进行仿射变换,仿射变换是二维的线性变换,由五种基本原子变换构成,分别是旋转**、**平移**、**缩放**、**错切**和**翻转。

  • degree 旋转角度
  • translate 平移区间设置,如 (a, b), a设置宽(width),b设置高(height)。 图像在宽维度平移的区间为 -img_width * a < dx < img_width * a 。
  • scale 缩放设置(图像整个尺寸不变,原图缩放,其余默认用 0 填充)
  • shear 错切
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
    # Initialize train transforms
    transform_train = T.Compose([
        T.Resize((config["image_size"], config["image_size"])),
        T.RandomHorizontalFlip(),
        T.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2),
        T.RandomAffine(degrees=5, scale=(0.8, 1.2), translate=(0.2, 0.2)),
        T.ToTensor(),
        T.Normalize(
            mean=[0.485, 0.456, 0.406],
            std=[0.229, 0.224, 0.225]
        ),
    ])

使用 (hard) triplet loss 训练,这里使用简单的 PKSampler

Ramdomly sample from a dataset while ensuring that each batch of size (p*k) includes samples form exactly p classes, with k samples for each class

完全看懂 softTripleLoss 还是有点难度的。

其他

多 GPU进行训练包含两种情况:单机多 GPU训练;多机多 GPU训练。从实际上操作来看,一般推荐单机多 GPU进行训练 而非多机多 GPU训练。

使用

如果是模型,那么需要执行下面几句代码

1
2
model = nn.DataParallel(model)
model = model.cuda()

如果是数据,那么执行以下的代码

1
2
inputs =inputs.cuda()
labels =labels.cuda()

显存使用不平衡问题

细心点会发现其实第一块卡的显存会占用更多一些

因为 input数据是并行的,但是output loss 却不是这样的,每次都会在第一块 GPU相加计算,这就造成了第一块 GPU负载大于其他剩余的显卡。

官方推荐使用 parallel.DistribtedDataParallel 去代替DataParallel ,据说因为前者比后者更快一些。

numpy 中的 identify

The identify array is a square array with ones on the main diagonal. 创建的是方阵,即 rows =cols,主对角线是1,其他的地方都是 0.

numpy 中的eye()

返回的是二位数组(n, m),对较线是 1 其余的地方是0

在 torch 中 Identify 使用

f(x) =x ,这个值得是一个网络结构,直接return 相同的 module。

trunk 行李箱

disjoint set 并查集

disjoint 互斥集

cascade chain model: 就是指联结网络,

网络结构:

(1) 三个 sub-model 是同一层,然后 list_of_model 的输入是三个 sub-model 的输出。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
trunk1 = torchvision.models.shufflenet_v2_x0_5(pretrained=True)
trunk2 = torchvision.models.shufflenet_v2_x1_0(pretrained=True)
trunk3 = torchvision.models.resnet18(pretrained=True)
all_trunks = [trunk1, trunk2, trunk3]
trunk_output_sizes = []
# output size: 1024, 1034, 512
for T in all_trunks:
    trunk_output_sizes.append(T.fc.in_features)
    T.fc = common_functions.Identity()

trunk = ListOfModels(all_trunks)
trunk = torch.nn.DataParallel(trunk.to(device))

(2)使用两个 optimizer: 一个用于子网络,一个用于list_of_model 网络

1
2
3
# Set optimizers
trunk_optimizer = torch.optim.Adam(trunk.parameters(), lr=0.00001, weight_decay=0.0001)
embedder_optimizer = torch.optim.Adam(embedder.parameters(), lr=0.0001, weight_decay=0.0001)

(3)这里出现了三个 loss,loss、optimizer 和 model 对应关系感觉是混乱的。

轻量级网络总结,可以参考以下的 blog:

https://cloud.tencent.com/developer/article/1120709

https://www.jiqizhixin.com/articles/2018-01-08-6

http://www.ngcourse.com/article/detail?id=421

https://zhuanlan.zhihu.com/p/142911918

https://zhuogege1943.com/2019/06/16/Going-with-small-and-fast-networks-1/

https://zhuanlan.zhihu.com/p/33037573

https://zhuanlan.zhihu.com/p/51566209

这个好奇怪,因为 posneg 是一个图像,真的好奇怪。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 这里 anchor 占总的 80%, posneg 占总的 20%;是随便写的吗,还是有根据的?
class CIFAR100TwoStreamDataset(torch.utils.data.Dataset):
    def __init__(self, dataset, anchor_transform, posneg_transform):
        # split by some thresholds here 80% anchors, 20% for posnegs
        lengths = [int(len(dataset)*0.8), int(len(dataset)*0.2)]
        self.anchors, self.posnegs = torch.utils.data.random_split(dataset, lengths)
        self.anchor_transform = anchor_transform
        self.posneg_transform = posneg_transform

    def __len__(self):
        return len(self.anchors)
    def __getitem__(self, index):
        anchor, target = self.anchors[index]
        if self.anchor_transform is not None:
            anchor = self.anchor_transform(anchor)
        # now pair this up with an image from the same class in the second stream
        A = np.where( np.array(self.posnegs.dataset.targets)==target )[0]
        posneg_idx = np.random.choice(A[np.in1d(A, self.posnegs.indices)])
        posneg, target = self.posnegs[np.where(self.posnegs.indices==posneg_idx)[0][0]]
        if self.posneg_transform is not None:
            posneg = self.posneg_transform(posneg)
        return anchor, posneg, target

__getitem__ 函数没有完全看懂。在同一类中得到 pos_ind ,但在获取 neg_index 时候发现是比较难理解的。

貌似也没有办法 debug

我的理解:pos 和 neg 应该是两个不同的图像,为什么这里写成了一个呢?

1
2
3
# in1d() # 查找数组元素是否在另一数组
values =np.array([6, 0, 0, 3, 2, 5, 6])
print(np.in1d(values, [0, 1, 2])) #  [True, False, True]

example 7:

http://47.94.35.231:8090/notebooks/notebook/pytorch-metric-learning/examples/notebooks/Inference.ipynb

use the inference module after you’re done training.

当 train 完之后,可以参考这个 example code 进行预测。主要有以下功能:

  1. get neares t neighbors of a query
  2. compare two images of the same class
  3. compare two images of the different classes
  4. compare multiple pairs of images
  5. compare all pairs within a batch
  6. compare all pairs bew

结论: 这个可以稍后看看。