度量学习思路整合

few-shot learning的度量学习方法阅读与总结,目标是找到一篇文章,解决few-shot的以下问题:
在基类上,模型学会区分相似和不相似的实例,从而提高下游任务的性能;
支持集的样本过少,计算得到的类代表原型和实际期望得到的类原型有较大偏差,如何矫正有偏的类原型,进而提高模型分类精度。

1.Prototype Rectification for Few-Shot Learning

1.1 基本信息

  • 2020年,ECCV会议论文

  • 无开源源码

  • 提出了如何减少类原型的偏置、减少support和query dataset之间的分布差。

1.2 方法记录

1.2.1 intra-class bias

为了减少实际计算出的原型和真实原型之间的差距,采用伪标签策略,补充support set的样本,由此得到更接近真实的原型。具体操作是取top-k个置信度的未标记样本,加上伪标签,加入support set一起计算类代表原型,其中为了避免伪标签错分给原型带来大的误差,使用加权和的平均作为修改的原型,权重的计算公式:样本和basic prototypes有更大的余弦相似度,则在修改的原型中有更高占比。

伪标签有一个使用前提,即需要一次性给出某个类的所有未标记样本,不适用于一个个给测试样本的情况。
在脑电身份识别上,一个人作为一个类,他的测试样本是否可以一次性获取到?,决定了伪标签是否适用。

1.2.2 cross-class bias

首先两个set被假设为分布在同一个domain中,但support setquary set之间存在分布差,提出为了减少两者的分布差,可以把quary setsupport set移动。具体地,文章提出给每个标准化后的quary feature$\overline{X_q}$添加一个转换参数epilon。
image.png

减小分布差,脑电身份识别,support set用的是登记session的样本,quary set可能用的是另一个session的样本,由于时变性,两者的分布差肯定是存在的,甚至于同一个session,随着人状态的波动,样本之间估计也存在明显的分布差,这个方法可以一试。

2.Free Lunch for Few-shot Learning: Distribution Calibration

2.1 基本信息

  • 2021年,ICLR会议论文

  • 源码:Few_Shot_Distribution_Calibration

  • 提出从语义相似的基类(s)迁移统计数据来校准这些少数样本类的分布,接着依据新分布的均值和方差随机采样一定数量的样本,对novel classes的n_way k_shot任务的支持集进行补充,补充后的support set aug输入分类器fit。

2.2 方法记录

前提假设:假设特征嵌入的每个维度都服从高斯分布

这篇代码基类是有充足样本的类,用别人的SOTA分类模型,取倒数第二层作为特征提取器,用于提取基类和novel类的特征嵌入。接着计算每个基类特征层面的均值向量和协方差。

测试类(novel classes)用的是轮次训练的方式,例如:

1
2
3
4
5
6
7
8
9
# ---- data loading
dataset = 'miniImagenet'
n_shot = 1
n_ways = 5
n_queries = 15
n_runs = 10000
n_lsamples = n_ways * n_shot
n_usamples = n_ways * n_queries
n_samples = n_lsamples + n_usamples

每次从novel classes中抽5个类,每个类有1个support sample,15个query sample。每一次run,对抽取的5个support_data(先转成特征嵌入)分别做分布校准,然后生成若干个数的特征向量作为support集的补充,一起输入分类器fit,在query samples上测试,计算acc,最后取10000次run的平均acc。

分布校准core代码:

1
2
3
4
5
6
7
8
9
10
def distribution_calibration(query, base_means, base_cov, k,alpha=0.21):
dist = [] # 计算support sample(变量名叫query)和mean之间的欧式距离
for i in range(len(base_means)):
dist.append(np.linalg.norm(query-base_means[i]))
index = np.argpartition(dist, k)[:k] # 从数组 dist 中找到前𝑘小的元素的索引
mean = np.concatenate([np.array(base_means)[index], query[np.newaxis, :]])
calibrated_mean = np.mean(mean, axis=0)
calibrated_cov = np.mean(np.array(base_cov)[index], axis=0)+alpha

return calibrated_mean, calibrated_cov
  1. 这篇文章在细粒度数据集CUB上也应用了DC方法,有超过200种不同的鸟类图片。每个人的脑电虽然说各自有判别性特征,其实还是很相似的细粒度数据,这个方法通过迁移k个距离最小的基类的分布,来直接增加支持集的样本,进而提高分类性能。
  2. 要调的超参数多,采样个数、k个基类、离散程度a。
  3. 在脑电上是否有效呢?特征向量都是经过标准化/幂变换的。

3.Semantic-Based Implicit Feature Transform for Few-Shot Classification

3.1 基本信息

  • 2024年,International Journal of Computer Vision

  • 源码:SIFT

  • 借鉴的是前面分布矫正的论文,同样是通过从基类补充特征向量到novel类上面去,不同的是图像类别标签(如cat、dog),本身具有语义信息,可以用不同的词向量来表达。然后通过语义嵌入的相似度来选择最近的基类,而不是前面通过统计特征。

3.2 方法记录

  • 脑电的类别标签纯粹是'Person A''Person B'的形式,并不具有语义信息,所以如果要用,应该采用Free Lunch的方法做跨时段的分布矫正

  • 提出了一种原型矫正的方法。适用于transductive setting,即查询样本全部一次给出的情况。通过K-means把查询样本分簇为N类,接着建立路径规划问题的数学模型,为这N个类别确定互相不重复的标签,与初始的每个类原型做平均。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def updateproto(Xs, ys, cls_center, way):
"""更新原型 (prototype),并与聚类中心 (cluster center) 结合"""

# 调用 np_proto 计算每个类别的原型
proto = np_proto(Xs, ys, way)

# 计算每个类别原型与所有聚类中心之间的平方欧式距离
# 使用广播机制,扩展 proto 和 cls_center 的维度后相减
dist = ((proto[:, np.newaxis, :] - cls_center[np.newaxis, :, :]) ** 2).sum(2)

# 找到每个类别原型距离最近的聚类中心索引
id = dist.argmin(1)
feat_proto = np.zeros((way, Xs.shape[1]))
for i in range(way):
# 将当前类别的原型和其最近的聚类中心取平均值
feat_proto[i] = (cls_center[id[i]] + proto[i]) / 2

return feat_proto

4.Matching Feature Sets for Few-Shot Image Classification

4.1 基本信息

  • 2022年,CVPR会议论文

  • 源码:SetFeat-fs

  • 以Conv4-64骨干网络为例,提出把一张图像输入进去,经过一个Block,就输出一个经自注意力机制mapper计算后的特征向量$h_m$,这样处理从一张图片中提取出一组m(4)个特征向量。距离度量用的是负余弦相似度,实际上和原型网络的距离度量很相似,区分在于有多个特征向量,它这里统一了向量的shape,相当于在同一个特征空间内,按mapper聚合多个类中心。

4.2 方法记录

  • 如何利用多个不同层提取出的特征向量,核心如下公式所示。
    image.png

  • 训练流程:两个阶段,第一阶段就是正常带FC层的分类器,并且本文是在每个Block后面都接一个FC层,分别训练到这个Block为止的网络,第二阶段,舍去FC层,应用轮次训练在基类上模拟FSL Task,通过公式6的负对数概率计算loss,反向传播微调编码器的参数。微调结束,最终在novel类上面推理,之前的Free Lunch也是一样的流程。
    image.png

  1. 总结一下,它提高原型网络分类精度的手段就是,同一个特征space,一个类别聚合m个类中心。
  2. 感觉第一阶段训练就相当于独立地训练了4个encoder,但是又不独立,前面的参数是要重复使用的,具体要看代码train部分。
  1. 这种在预训练阶段,训练一个分类器的做法和我之前看的有监督对比学习训练编码器的方式不一样,我的想法是也许可以借用这个metric,然后用SCL的做法分别独立训练出3个encoder。之后可以有两种metric方法,一种是和本文做法一样,投射到同一个特征空间;另一种映射到不同特征空间分别做相似度计算,再求和,作为新的metric。
  2. 还有一个想法是SCL的encoder很关键,决定了特征向量的质量,借鉴GoogleNet的多尺度卷积方法,预训练出一个encoder,也可以试一下。

4.3 参考博客

5.BSNet: Bi-Similarity Network for Few-shot Fine-grained Image Classification

5.1 基本信息

  • 2020年,‌IEEE Transactions on Image Processing(TIP)期刊论文

  • 源码:BSNet

  • 代码格式简洁,是基于度量学习的图像细粒度分类,提出同时使用余弦相似度和欧式距离两种loss,即在ProtoNet上添加一个余弦相似度的loss,分类精度在图像分类上有所提高。

最后调优模型的时候可以试一下,有空看代码。

6.Supervised Contrastive Learning

6.1 基本信息

  • 2020年,NeurIPS

  • 源码:SupContrast

  • SCL集合了传统度量学习领域的Triplet lossN-pair loss两者的特点,提出对一个Anchor除了考虑多个负样例之外,也同时考虑多个正样例,设计的损失函数避开了需要显式地调参以挖掘半难样本的需求。

6.2 方法记录

  • 在一个batch中,每一个样本$x$都经过一个数据增强模块$Aug(·)$生成两个随机增强$\bar{x} = Aug(x)$,两个增强后的样本标签一样,属于同一个类别。接下来如何保证随机生成的一个batch有多个样本标签相同呢,除了数据增强的方式生成正样本对之外,相对于类别个数大小C,batch的batch_size = N要远大于C,这样平均N/C,就必定会采样到同一个类的多个样本。

  • 代码与对应公式的解释:监督对比学习SupConLoss代码学习笔记

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
"""
Author: Yonglong Tian (yonglong@mit.edu)
Date: May 07, 2020
"""
from __future__ import print_function

import torch
import torch.nn as nn

class SupConLoss(nn.Module):
"""Supervised Contrastive Learning: https://arxiv.org/pdf/2004.11362.pdf.
It also supports the unsupervised contrastive loss in SimCLR"""
def __init__(self, temperature=0.07, contrast_mode='all',
base_temperature=0.07):
super(SupConLoss, self).__init__()
self.temperature = temperature
self.contrast_mode = contrast_mode
self.base_temperature = base_temperature

def forward(self, features, labels=None, mask=None):
"""Compute loss for model. If both `labels` and `mask` are None,
it degenerates to SimCLR unsupervised loss:
https://arxiv.org/pdf/2002.05709.pdf

Args:
features: hidden vector of shape [bsz, n_views, ...].
labels: ground truth of shape [bsz].
mask: contrastive mask of shape [bsz, bsz], mask_{i,j}=1 if sample j
has the same class as sample i. Can be asymmetric.
Returns:
A loss scalar.
"""
device = (torch.device('cuda')
if features.is_cuda
else torch.device('cpu'))

if len(features.shape) < 3:
raise ValueError('`features` needs to be [bsz, n_views, ...],'
'at least 3 dimensions are required')
if len(features.shape) > 3:
features = features.view(features.shape[0], features.shape[1], -1)

batch_size = features.shape[0] # batch_size = N
if labels is not None and mask is not None:
raise ValueError('Cannot define both `labels` and `mask`')
elif labels is None and mask is None:
mask = torch.eye(batch_size, dtype=torch.float32).to(device)
elif labels is not None:
labels = labels.contiguous().view(-1, 1)
if labels.shape[0] != batch_size:
raise ValueError('Num of labels does not match num of features')
mask = torch.eq(labels, labels.T).float().to(device)
else:
mask = mask.float().to(device)

contrast_count = features.shape[1] # 视图个数
contrast_feature = torch.cat(torch.unbind(features, dim=1), dim=0) # 消除视图维度,按batch拼接,得到[N*n_views, feature_dim]
if self.contrast_mode == 'one':
anchor_feature = features[:, 0] # 只取第一个视图作为anchor
anchor_count = 1
elif self.contrast_mode == 'all':
anchor_feature = contrast_feature # 所有视图作为anchor
anchor_count = contrast_count
else:
raise ValueError('Unknown mode: {}'.format(self.contrast_mode))

# compute logits
anchor_dot_contrast = torch.div(
torch.matmul(anchor_feature, contrast_feature.T), # 1.单个 [N, ft_dim] * [ft_dim, N*n_views] = [N, N*n_views] 2.所有 [N*n_views, ft_dim] * [ft_dim, N*n_views] = [N*n_views, N*n_views]
self.temperature)
# for numerical stability
logits_max, _ = torch.max(anchor_dot_contrast, dim=1, keepdim=True)
logits = anchor_dot_contrast - logits_max.detach()

# tile mask
mask = mask.repeat(anchor_count, contrast_count)
# mask-out self-contrast cases
logits_mask = torch.scatter(
torch.ones_like(mask),
1,
torch.arange(batch_size * anchor_count).view(-1, 1).to(device),
0
)
mask = mask * logits_mask

# compute log_prob
exp_logits = torch.exp(logits) * logits_mask # [N*n_views, N*n_views]
log_prob = logits - torch.log(exp_logits.sum(1, keepdim=True)) # [N*n_views, N*n_views]

# compute mean of log-likelihood over positive
# modified to handle edge cases when there is no positive pair
# for an anchor point.
# Edge case e.g.:-
# features of shape: [4,1,...]
# labels: [0,1,1,2]
# loss before mean: [nan, ..., ..., nan]
mask_pos_pairs = mask.sum(1)
mask_pos_pairs = torch.where(mask_pos_pairs < 1e-6, 1, mask_pos_pairs)
mean_log_prob_pos = (mask * log_prob).sum(1) / mask_pos_pairs # [N*n_views]

# loss
loss = - (self.temperature / self.base_temperature) * mean_log_prob_pos # [N*n_views]
loss = loss.view(anchor_count, batch_size).mean() # 标量

return loss

仿照这篇论文的三个核心模块:数据增强模块、编码网络、映射网络,计划对脑电原始数据做Channel Reflection数据增强,以及一个其他数据增强操作,和本文的监督对比loss匹配,Backbone也需要找一个替换。

7.Channel reflection: Knowledge-driven data augmentation for EEG-based brain–computer interfaces

7.1 基本信息

  • 2024年,Neural Networks

  • 源码:EEGAug

  • 提出了一种无需超参数的通道交换(CR)数据增强方法,传统的数据增强如添加Noise、Scale、Frep都需要调超参数,并且十分鲁棒,在MI、SSVEP、ERP、癫痫检测4个实验范式下均适用,相比于Baseline(没有数据增强),分类精度更好。注意经CR数据增强之后,训练数据翻倍。

7.2 方法记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def leftrightflipping_transform(X, left_mat, right_mat):
"""

Parameters
----------
X: torch tensor of shape (num_samples, 1, num_channels, num_timesamples)
left_mat: numpy array of shape (a, ), where a is the number of left brain channels, in order
right_mat: numpy array of shape (b, ), where b is the number of right brain channels, in order

Returns
-------
transformedX: transformed signal of torch tensor of shape (num_samples, num_channels, num_timesamples)
"""

num_samples, _, num_channels, num_timesamples = X.shape
transformedX = torch.zeros((num_samples, 1, num_channels, num_timesamples))
for ch in range(num_channels):
if ch in left_mat:
ind = left_mat.index(ch)
transformedX[:, 0, ch, :] = X[:, 0, right_mat[ind], :]
elif ch in right_mat:
ind = right_mat.index(ch)
transformedX[:, 0, ch, :] = X[:, 0, left_mat[ind], :]
else:
transformedX[:, 0, ch, :] = X[:, 0, ch, :]

return transformedX
  • 即根据脑电极通道的索引,位于中线的电极不变,左脑和右脑对称分布的电极做一个交换,类似于图像的翻转操作。除左右手MI想象要调换标签之外,ERP不需要换标签。调用方法如下:
1
2
3
4
5
6
7
if dataset == 'BNCI2014001':
left_mat = [1, 2, 6, 7, 8, 13, 14, 18]
right_mat = [5, 4, 12, 11, 10, 17, 16, 20]
aug_train_x = leftrightflipping_transform(
torch.from_numpy(train_x).to(torch.float32).reshape(train_x.shape[0], 1, ch_num, -1),
left_mat, right_mat).numpy().reshape(train_x.shape[0], ch_num, -1)
aug_train_y = 1 - train_y # 二分类标签0-1对调

8.Charting the Right Manifold: Manifold Mixup for Few-shot Learning

8.1 基本信息


度量学习思路整合
http://paopaotangzu.icu/2024/12/09/8-度量学习文献调研记录/
作者
paopaotangzu
发布于
2024年12月9日
许可协议