【论文】目标检测中正负样本的选取总结

本文总结了目标检测算法中正负样本的选择方法,包括Fast R-CNN、Faster R-CNN、SSD、SPP-NET、YOLO v1/v2等,详细解析了各算法的正负样本选取标准及样本平衡策略。
部署运行你感兴趣的模型镜像

一、简介

目标检测有大量的算法,这篇文章对其中正负样本的选取方法进行了总结。对于正样本,是回归与分类都进行,而负样本由于没有回归的对象,不进行回归,只进行分类(分类为背景)。

二、正文

1.Fast R-CNN

构造如下:

步骤是

1.selective search -> ROIs -> 筛选 -> ROIs 

2.ROIs + feature map -> ROI Pooling -> 分类 + 回归 -> NMS

正例:与GT的IOU>0.5的作为正样本

反例:与GT的IOU在[0.1, 0.5)之间的作为负样本。0.1是作为一种 hard example mining。

样本平衡:正:负 = 1:3

 

2..Faster R-CNN

Faster R-CNN的构造分为两个部分:RPN(Region Proposol Net)、fast-RCNN,如下图:

步骤是

1.特征提取 -> 输入到下面的2和3中

2. Anchor产生 -> 剔除出超出边界的Anchor -> 使用RPN对Anchor进行回归,物体判别(判断是否是物体)(这里使用的是卷积层) -> 筛选RPN回归后的Anchor -> NMS -> 再次筛选数量 -> 剔除超出边界的部分 -> 获得ROIs(reigon of interest)

3. 利用2中的ROIs和1中的特征做ROI Pooling -> 类别分类 + 进一步回归 -> NMS -> 得到b-box以及类别。

其中包含了2中的RPN的训练以及3中的训练。更详细的结构见:

https://github.com/chenyuntc/simple-faster-rcnn-pytorch

https://blog.youkuaiyun.com/qq_29462849/article/details/80510687

训练方式如下:

1. 单独训练前面的提取特征网络(共享部分)+ RPN部分,微调。

2.生成ROIs,单独训练Fast R-CNN(包括前面的共享部分)。

3. 使用2中获得的共享部分覆盖1中的共享部分,固定。训练RPN部分

4. 固定共享部分,使用3中提出的ROIs,训练Fast R-CNN

交替迭代上部分(应该就是3和4)。

还有另一种近似联合训练,在文中有说。这个文章详细叙述了训练过程:

https://blog.youkuaiyun.com/sunyiyou9/article/details/52207486

https://blog.youkuaiyun.com/qq_41576083/article/details/82966489

1.RPN的训练

首先去除超出边界的Anchro,之后选取正反例Anchor

正例:按照两个准则挑选Anchor:1.与GT有最大的IOU。                       2.与GT的IOU>0.7。

两个有一个满足就是正例。

反例:与所有GT的IOU < 0.3

样本平衡:对于一个mini-batch,随机选取其中的正反例,比例为1:1,如果负样本太少,就扩展mini-batch,达到平衡。

2.fast r-cnn

这个部分应该与上面RPN的训练类似,要看代码才知道。

3.SSD

SSD也是使用Anchor(default Box)(先验框)。正反例的挑选与faster r-cnn的类似。

代码详解:https://blog.youkuaiyun.com/c20081052/article/details/80391627

正例:按照两个准则挑选default Box:1.与GT有最大的IOU。                       2.与GT的IOU>0.5。

两个有一个满足就是正例。

反例:其余的均为反例。

样本平衡:采用hard negative mining,对负样本的default box的confidence进行排序,取大confidence的作为最终的负样本,最终的比例为正 : 负 = 1 : 3

据说还有另外一种方法,是根据回归后的b-box来进行正反例的选取。

4. SPP-NET(R-CNN之后的那一篇)

这里头有提到检测任务。使用的还是R-CNN的方法,使用SS生成候选窗,之后投影到feature map,使用spatial pyramid(1*1,2*2,3*3,6*6)来提取区域特征,结合起来,经过fc,判断。

 正反样例的选取与fast r-cnn一样。

正例:与GT的IOU在[0.5, 1]

反例:与GT的IOU在[0.1, 0.5]

样本平衡:1:3

5.YOLO(v1)

YOLOv1比较粗糙,使用的是带有限制性的grid方法来构造b-box,每个grid有两个b-box,回归的中心限制在了grid中,但是对于大小没有限制,没有default Box一说,因此是直接根据输出的b-box来判断正反例。另外提一句,最后的输出是两个全连接层,没有共享。

 

YOLO由于有grid和产生的b-box两个概念,对于正负样本的判断也和之前使用Anchor的有所不同,b-box也少了很多,也就7*7*2共98个。

回归:

正例:对应的cell中有物体中心,与GT有最大IOU的b-box。                  反例:其余b-box。

判断是否有物体(confidence,事实上如果有物体还要预测IOU)

正反例同上

具体标签分类:每一个cell(grid中的一个)上产生一个类别判断。

正例:物体的中心在cell中                    反例:cell里头没有物体的中心

样本平衡:一个用来平衡confidence的参数,体现在损失函数上面,改变权值,反例的损失降为0.5。对于grid则不存在样本平衡,实际上也并不需要,因为如果没有出现物体的话就不需要估计类别概率了。

6.YOLO(v2)

相当于是有Anchor版本的YOLO,grid的概念依然存在。它对于Anchor的先验大小进行了研究,与SSD、Faster R-CNN不同,这里将Anchor回归后的中心限制在了cell中,使得回归过程有一定的约束。有一点与YOLOv1不同的是对于类别标签的判断不再是局限于一个cell,而是每个b-box都有一个类别判断。详细的可以见下面的代码解析:https://www.jianshu.com/p/032b1eecb335

正反例的判断相当于SSD与YOLOv1的结合,比较复杂:

对每个GT匹配一个最好的Anchor,作为预选正例,其余的为反例

      classification_loss_sum和coordinates_loss_sum依据上面判断正反例反例没有损失,

      在每个cell中,取与GT有最大IOU的b-box,其余作为反例

             对于以上的b-box,如果IOU<0.6,作为反例:

                    confidence_loss_sum依据上面判断正反例,反例有损失。

 

您可能感兴趣的与本文相关的镜像

Yolo-v8.3

Yolo-v8.3

Yolo

YOLO(You Only Look Once)是一种流行的物体检测和图像分割模型,由华盛顿大学的Joseph Redmon 和Ali Farhadi 开发。 YOLO 于2015 年推出,因其高速和高精度而广受欢迎

<think> 我们正在讨论对比学习中的正负样本定义和处理方式。根据引用[3],对比学习的核心思想是构建正样本对和负样本对。在代码示例中: - 正样本对:同一原始图像经过两种不同数据增强得到的两个视图(view1和view2)互为“正样本” - 负样本对:同一个batch中不同原始图像生成的视图互为“负样本” 处理方式: 1. 数据加载阶段:通过`ContrastiveTransform`为每张图像生成两个增强视图(即一对正样本) 2. 训练阶段: - 将一批图像的两个视图(view1和view2)分别输入编码器,得到特征表示z1和z2 - 将z1和z2拼接为z = [z1, z2],形成一个2N大小的特征向量(N为batch size) - 计算相似度矩阵(cosine相似度除以温度参数) - 构建标签:每个样本的正样本是其配对视图(即索引i和i+N互为正样本),其余2N-2个样本均为负样本 代码示例中的损失函数计算: - 使用`torch.eye(2 * batch_size)`创建对角线掩码(正样本位置为True) - 从相似度矩阵中提取正样本得分(对角线元素去掉自身) - 负样本得分取自非对角线位置 下面我们详细解释损失函数部分的代码: ```python def forward(self, z_i, z_j): batch_size = z_i.size(0) # 拼接正负样本:z_i和z_j分别是两个视图的特征,形状为(batch_size, feature_dim) z = torch.cat([z_i, z_j], dim=0) # 形状变为(2*batch_size, feature_dim) # 计算相似度矩阵:计算z中每个向量与其他向量的余弦相似度 # 使用unsqueeze扩展维度:z.unsqueeze(1) -> (2N, 1, D); z.unsqueeze(0) -> (1, 2N, D) # 计算余弦相似度:dim=2表示在特征维度D上计算 sim_matrix = F.cosine_similarity(z.unsqueeze(1), z.unsqueeze(0), dim=2) / self.temperature # 创建正样本掩码:对角线为True(但注意正样本应该是每个样本与其配对视图,而不是自身) # 因此,我们构建的掩码需要将每个样本的配对视图(即索引i和i+N)视为正样本,而自身不算正样本 # 在标准实现中,我们通常排除自身(即对角线),但这里我们使用掩码来选取配对的正样本 # 实际上,在损失函数中,我们希望每个样本i的正样本是i+N(反之亦然),而其他所有样本(包括自身)都是负样本 mask = torch.eye(2 * batch_size, dtype=torch.bool, device=z.device) # 但是,上面的mask对角线为True,而我们想要的正样本位置是:对于前N个样本,其正样本是后N个中对应的那个;对于后N个,正样本是前N个中对应的那个。 # 实际上,在SimCLR中,每个样本只有一个正样本(即其配对视图),所以我们需要重新构建掩码。 # 修正:我们构建一个掩码,使得: # pos_i_j: 表示样本i的正样本是j(其中j = i + N,当i < N;或j = i - N,当i >= N) # 但通常的实现中,我们使用如下方式: # 正样本对的位置:例如,对于i,它的正样本是i+N(当i在[0, N-1]时)和i-N(当i在[N, 2N-1]时) # 然而,在计算损失时,我们通常使用一个更巧妙的方法:将相似度矩阵中对应对的位置作为正样本,其他都是负样本(包括自身)。 # 在实际代码中,我们通常不会把自身作为正样本,所以需要将自身从相似度矩阵中排除(即对角线设为0?)。但SimCLR的损失函数计算中,每个样本的正样本是另一个视图,而自身被视为负样本(因为相似度矩阵的对角线元素是样本与自身的相似度,这个我们不需要,所以用掩码排除)。 # 在原始SimCLR论文的实现中,他们使用: # labels = torch.arange(batch_size, device=device).repeat(2) # 即[0,1,...,N-1,0,1,...,N-1] # labels = (labels.unsqueeze(0) == labels.unsqueeze(1)).float() # labels = labels / labels.sum(dim=1, keepdim=True) # 归一化 # 但我们的实现采用了另一种方式:把正样本对视为同一个类(只有配对的两个视图是正样本),其他都是负样本。 # 然而,上面的代码中,mask是一个单位矩阵(对角线为True),它实际上标记了自身与自身的相似度(这是不应该存在的正样本),所以我们下面修正: # 正确做法:我们只关心每个样本和它配对的那个视图(即第i个样本和第i+N个样本互为配对)。因此,我们构建一个掩码,其中位置(i, j)满足:如果j = (i+N) mod (2N) 且 i < N,或者j = (i-N)且i>=N,则为正样本。但这样构建比较复杂。 # 一个常用的技巧是:将标签设置为每个样本只有一个正样本(即配对样本),然后使用交叉熵损失。具体如下: # 实际上,在SimCLR的官方实现中,他们使用: # logits = torch.einsum('nc,ck->nk', [z_i, z_j.T]) / temperature # 计算N*N的矩阵,每个元素是z_i和z_j的相似度 # labels = torch.arange(N, device=device) # loss_i = F.cross_entropy(logits, labels) # loss_j = F.cross_entropy(logits.T, labels) # loss = (loss_i + loss_j) / 2 # 但我们的代码中是将两个视图的特征拼接,然后计算一个大的相似度矩阵(2N*2N),然后通过掩码选取正负样本。 # 因此,我们需要修改掩码:将配对的位置设为True(而不是对角线)。配对关系是:前N个样本与后N个样本一一对应,即第i个样本与第i+N个样本配对。 # 构建正确的正样本掩码: mask = torch.zeros(2 * batch_size, 2 * batch_size, dtype=torch.bool, device=z.device) # 设置正样本位置:对于i in [0, batch_size-1],其正样本是i+batch_size;对于i in [batch_size, 2*batch_size-1],其正样本是i-batch_size indices = torch.arange(batch_size, device=z.device) # 第一组:前N个样本,每个样本的正样本是后N个中对应的那个 mask[indices, indices + batch_size] = True # 第二组:后N个样本,每个样本的正样本是前N个中对应的那个 mask[indices + batch_size, indices] = True # 注意:我们不需要考虑自身(即对角线),因为自身在mask中未被标记为正样本(我们只标记了配对的两个位置)。但是,在相似度矩阵中,自身与自身的相似度很高,所以我们需要在计算负样本时排除自身吗?实际上,在交叉熵损失中,我们需要将正样本和负样本分开,自身也被视为负样本(除了配对的那个)。 # 现在,我们想要从相似度矩阵中提取每个样本的正样本得分(即与它配对的那个样本的相似度)和所有负样本得分(包括自身和其他样本)。 # 注意:每个样本只有一个正样本(即配对的那个),负样本有2N-2个(因为还有自身也是一个负样本)。 # 获取正样本得分:使用mask,我们提取每个样本对应的正样本位置的相似度 pos_samples = sim_matrix[mask].view(2 * batch_size, -1) # 此时每个样本对应一个正样本得分,所以view后为(2N, 1) # 构建负样本掩码:负样本掩码是mask取反,但要排除自身吗?实际上,我们的mask已经排除了自身(因为mask只标记了配对位置),所以负样本掩码包括自身和其他非配对样本。 # 但我们希望每个样本的负样本包括所有其他样本(包括自身)除了它的正样本。所以负样本掩码就是~mask(但这样也包括了正样本?不,mask中正样本位置为True,取反后为False,所以负样本掩码中正样本位置为False,其他位置为True)。但是我们还要注意:在计算时,我们通常将相似度矩阵中对角线(自身)也作为负样本。 # 提取负样本:使用~mask,但这样会包括所有的非正样本位置(包括自身)。然后我们需要将每个样本的负样本得分展平。 neg_samples = sim_matrix[~mask].view(2 * batch_size, -1) # 形状为(2N, 2N-2) # 但是,我们上面的方法存在一个问题:在mask中,我们标记了正样本位置,取反后得到的是非正样本位置(包括自身和其他非配对样本)。因此,neg_samples包含了2N-1个元素?不对,因为mask的形状是(2N,2N),总共有4N^2个元素,其中正样本有2N个(每个样本有一个正样本),所以非正样本有4N^2-2N个。但我们按行取,每行有2N-1个非正样本(因为每行有一个正样本,其余2N-1个都是非正样本)。所以neg_samples可以调整为(2N, 2N-1)。然而,我们之前构建的neg_samples是view(2*batch_size, -1),所以确实会变成(2N, 2N-1)。 # 然后,我们将正样本得分和负样本得分拼接,形成每个样本的logits(正样本得分放在第一个位置,后面跟着所有负样本得分?)但交叉熵损失要求logits的每一行是每个类别的得分,其中第一个类别是正样本?实际上,我们并不关心顺序,只要知道正样本的索引是0即可。 # 因此,我们构造logits矩阵:将正样本得分和负样本得分按列拼接。这样,每个样本的logits向量是:[正样本得分, 负样本得分1, 负样本得分2, ...],共1+(2N-1)=2N个元素。但注意,负样本有2N-1个,所以logits的维度是2N(每个样本有2N个类别,其中第一个类别是正样本,其余都是负样本)。 logits = torch.cat([pos_samples, neg_samples], dim=1) # 形状为(2N, 2N) # 然后,我们定义标签:每个样本的标签都是0,因为我们在logits的第一个位置放置了正样本得分,所以希望模型将第一个位置识别为正样本。 labels = torch.zeros(2 * batch_size, dtype=torch.long, device=z.device) # 计算交叉熵损失 loss = F.cross_entropy(logits, labels) return loss ``` 然而,上述构建掩码的方式在计算负样本时包含了自身,这符合SimCLR的设计(自身作为负样本)。但是,这种实现方式效率较低,因为相似度矩阵是(2N,2N),当batch size较大时内存消耗大。 更高效的实现方式(也是SimCLR原始论文的实现)是: ```python # 另一种实现方式(推荐): def contrastive_loss(z_i, z_j, temperature=0.5): """ z_i, z_j: 两个视图的特征,形状为(batch_size, feature_dim) """ batch_size = z_i.shape[0] # 将两个特征矩阵拼接,用于批量计算 z = torch.cat([z_i, z_j], dim=0) # (2*batch_size, feature_dim) # 计算余弦相似度(使用矩阵乘法) sim = torch.mm(z, z.T) / temperature # (2N, 2N) # 由于z是单位向量(经过归一化),余弦相似度就是内积 # 归一化特征(可选,如果特征没有归一化,则需要先归一化) # z = F.normalize(z, dim=1) # sim = torch.mm(z, z.T) / temperature # 设置正样本位置:对于每个样本i,正样本是i+N(当i在0~N-1)和i-N(当i在N~2N-1) # 我们可以用标签来构建:标签为0~N-1,重复两次,然后正样本对就是相同标签的样本(但注意,不同图像的标签不同,只有同一图像的两个视图标签相同) # 因此,我们创建标签:前N个样本的标签为0~N-1,后N个样本标签也为0~N-1 labels = torch.cat([torch.arange(batch_size) for _ in range(2)], dim=0) # 形状(2*batch_size) # 然后,我们需要创建掩码,使得相同标签且不同视图的样本为正样本(注意:相同视图自身不能算正样本) # 但实际上,相同标签的样本包括自身和它的配对视图。所以我们需要排除自身。 mask = torch.eye(labels.shape[0], dtype=torch.bool, device=z.device) # 自身位置 # 相同标签的掩码(包括自身和配对视图) labels_mask = labels.unsqueeze(0) == labels.unsqueeze(1) # (2N, 2N) # 排除自身 labels_mask = labels_mask & (~mask) # 然后,我们提取正样本:labels_mask中每行只有一个True(即配对视图) pos_samples = sim[labels_mask].view(2*batch_size, -1) # 每行只有一个正样本,所以形状(2N,1) # 负样本:除了正样本和自身,其他都是负样本(但实际上labels_mask为False的位置就是负样本,包括自身) # 但我们希望排除自身?注意labels_mask已经排除了自身(因为mask被排除),所以负样本掩码就是~labels_mask,但这样会包含自身吗?不,因为labels_mask是由相同标签且非自身的位置构成的,所以负样本掩码包括:不同标签的样本和自身(因为自身不在labels_mask中)。 # 然而,我们通常希望负样本包括自身(因为自身与其他样本不同)?在原始SimCLR中,负样本包括所有其他图像(包括自身的另一个视图?不,自身的另一个视图是正样本)。所以负样本应该是:除了配对视图以外的所有样本(包括自身和不同图像的样本)。 # 因此,负样本掩码:应该排除正样本(即labels_mask为True的位置),其余都是负样本(包括自身)。所以我们可以直接使用~labels_mask,并将对角线填充为0(因为对角线是自身,而我们已经在labels_mask中排除了自身,所以~labels_mask包含对角线)。 neg_samples = sim[~labels_mask].view(2*batch_size, -1) # 形状为(2N, 2N-2)?不对,因为~labels_mask包含的位置数:总位置4N^2,labels_mask有2N个位置(每行一个正样本),所以~labels_mask有4N^2-2N个位置,然后按行展开成(2N, (4N^2-2N)/(2N)) = (2N, 2N-1) # 但实际上,labels_mask每行有1个True,其余都是False(包括自身和不同标签的样本),所以~labels_mask每行有2N-1个False。因此,neg_samples的形状为(2N, 2N-1)。 # 然后,我们将正样本和负样本拼接,形成logits:每一行第一个元素是正样本得分,后面是2N-1个负样本得分。 logits = torch.cat([pos_samples, neg_samples], dim=1) # (2N, 2N) # 标签:每个样本的目标是第一个类别(即索引0)是正样本 labels = torch.zeros(2*batch_size, dtype=torch.long, device=z.device) loss = F.cross_entropy(logits, labels) return loss ``` 但是,这种实现同样存在内存消耗大的问题(因为相似度矩阵是(2N,2N))。 更高效的实现是避免计算整个相似度矩阵,而只计算必要的部分: ```python def contrastive_loss(z_i, z_j, temperature=0.5): batch_size = z_i.shape[0] # 归一化特征 z_i = F.normalize(z_i, dim=1) z_j = F.normalize(z_j, dim=1) # 计算两个视图之间的相似度矩阵(N*N) # 注意:这里我们只计算z_i和z_j之间的相似度(而不是整个矩阵) # 每个z_i与z_j中的每个样本计算相似度(包括自身对应的z_j) logits_ij = torch.mm(z_i, z_j.T) / temperature # (N, N) logits_ji = torch.mm(z_j, z_i.T) / temperature # (N, N) # 正样本:对角线位置(因为z_i[k]和z_j[k]是正样本对) # 负样本:非对角线位置 # 对于logits_ij,每个样本的正样本是第k个(即自己对应的那个) labels = torch.arange(batch_size, device=z_i.device) # 计算两个方向的损失 loss_i = F.cross_entropy(logits_ij, labels) # 将logits_ij的每一行看作分类得分,标签是样本的索引(即希望第i行第i列得分最高) loss_j = F.cross_entropy(logits_ji, labels) loss = (loss_i + loss_j) / 2 return loss ``` 但这个实现和SimCLR原始论文不同:这里负样本只包含另一个视图中的其他样本(不包括自身视图),而原始SimCLR包括同一个batch中所有的样本(包括另一个视图中的其他样本和同一个视图的所有样本)。实际上,原始SimCLR的负样本数量是2N-2(除去自己和配对视图)。而这里,logits_ij的负样本只有N-1个(另一个视图中的其他图像),所以总共负样本数量不足。 因此,我们需要将同一个batch中所有其他样本都作为负样本。这可以通过构造一个更大的logits矩阵来实现,包括: logits = [ [z_i * z_i.T, z_i * z_j.T] = [A, B] [z_j * z_i.T, z_j * z_j.T] ] [C, D] 其中,整个矩阵大小为2N*2N。但我们只计算必要的部分,避免完全构建。 然而,为了避免内存问题,SimCLR的实现通常采用分布式训练,这里我们不展开。 总结:在对比学习中,正负样本的定义和处理是关键。正样本是同一图像的不同增强视图,负样本是batch中其他图像的所有视图(包括自身的另一个视图?不,自身的另一个视图是正样本,所以负样本是其他图像的两个视图以及自身视图的另一个样本?不,负样本排除正样本对,包括其他所有样本)。 由于完整实现内存消耗大,实际中常采用以下折中: - 使用较大的batch size(例如4096)来提供足够负样本 - 使用对称的损失计算(即计算两个方向的损失) 回到用户问题:在最初提供的代码示例中,正负样本的处理方式是通过构建整个相似度矩阵,然后利用掩码提取正负样本。虽然内存消耗大,但代码直观。 因此,我们给出一个改进的损失函数实现(基于掩码,但正确构建正样本位置): ```python class ContrastiveLoss(nn.Module): def __init__(self, temperature=0.5): super().__init__() self.temperature = temperature def forward(self, z_i, z_j): batch_size = z_i.size(0) z = torch.cat([z_i, z_j], dim=0) # (2*batch_size, feature_dim) # 计算相似度矩阵(余弦相似度) sim = F.cosine_similarity(z.unsqueeze(1), z.unsqueeze(0), dim=2) / self.temperature # 构建正样本掩码:每个样本的正样本是它的配对样本(即前N个样本对应后N个样本,后N个样本对应前N个样本) mask = torch.zeros(2 * batch_size, 2 * batch_size, dtype=torch.bool, device=z.device) mask[torch.arange(batch_size), torch.arange(batch_size) + batch_size] = True mask[torch.arange(batch_size) + batch_size, torch.arange(batch_size)] = True # 构建用于计算交叉熵的logits:正样本得分和负样本得分 # 注意:我们需要将正样本得分分离出来,负样本得分包括所有非正样本(包括自身) pos_samples = sim[mask].view(2*batch_size, -1) # 每行一个正样本 neg_samples = sim[~mask].view(2*batch_size, -1) # 每行2N-2个负样本(因为总共2N个样本,去掉两个正样本(自己和配对)?不对,mask标记了2个位置?不对,mask是2N*2N矩阵,每行有两个True?不对,注意我们构建mask时,对于第i行(i在0~N-1),我们设置mask[i, i+N]=True;对于第i+N行,我们设置mask[i+N, i]=True。所以每行只有一个True(即一个正样本)。因此,每行有2N-1个负样本位置(包括自身)。 # 因此,neg_samples的形状是(2N, 2N-1) logits = torch.cat([pos_samples, neg_samples], dim=1) # (2N, 2N) # 标签:正样本在logits的第一个位置(索引0) labels = torch.zeros(2 * batch_size, dtype=torch.long, device=z.device) loss = F.cross_entropy(logits, labels) return loss ``` 这个实现中,正样本对的掩码正确标记了配对关系,负样本则包括同一个batch中所有其他样本(包括自身)。虽然自身作为负样本可能不合理,但在实践中发现这样是可行的,因为同一个图像的两个视图作为正样本对,而同一个图像的两个视图在特征空间中应该非常接近,因此自身视图与其他视图的相似度自然很低,所以把自身作为负样本不会造成问题(因为模型会学习到自身视图与配对视图的相似度很高,而自身视图与自身视图的相似度(即对角线)虽然高,但被我们排除在正样本之外,所以模型会降低自身视图与自身视图的相似度?不,注意自身视图与自身视图的相似度在负样本中,所以模型会将其推低。但同一个图像的两个视图(正样本)应该相似度高,而同一个图像的同一个视图(自身)应该与配对视图相似度高,但这里自身视图与自己(即同一个视图)的相似度在负样本中,这会有问题吗? 实际上,在数据加载时,我们生成的两个视图是不同的,所以z_i[i]和z_i[i](同一个视图)不会同时出现在batch中?因为我们每次都是随机增强,同一个图像的两个视图是不同的,而且我们不会将一个视图重复输入。所以batch中不会出现同一个视图两次。因此,唯一出现的自身是同一个图像的另一个视图,但那个视图被标记为正样本,所以不会出现在负样本中(因为负样本是通过~mask得到的,而正样本位置已经被mask标记)。那么,自身视图的负样本指的是什么呢? 在z中,前N个是z_i,后N个是z_j。对于第i个样本(z_i[i]),它的负样本包括: - 前N个样本中除自己外的其他样本(z_i[j], j!=i) - 后N个样本中除自己对应的正样本外的其他样本(z_j[j], j!=i) 而自身视图(z_i[i])与自己(z_i[i])的相似度出现在相似度矩阵的对角线上(即位置[i,i])。这个位置在mask中不是正样本(因为mask[i,i]没有被标记,只有mask[i, i+N]被标记),所以它会被包含在负样本中。也就是说,模型会试图降低z_i[i]与z_i[i]的相似度?这显然不合理,因为余弦相似度自身一定是1(如果特征归一化)。 所以,为了避免这个问题,我们需要在计算相似度矩阵后,将对角线设置为一个很小的值(或-inf),使其不参与计算。但注意,我们构建的mask中,对角线位置既不是正样本,也不是负样本(因为我们希望排除自身与自身的比较)。因此,我们应该在构建mask时将对角线也排除。 修改: ```python # 初始mask(全0) mask = torch.zeros(2 * batch_size, 2 * batch_size, dtype=torch.bool, device=z.device) # 设置正样本位置 mask[torch.arange(batch_size), torch.arange(batch_size) + batch_size] = True mask[torch.arange(batch_size) + batch_size, torch.arange(batch_size)] = True # 创建一个单位矩阵(将对角线位置排除,即自身不算正样本,也不算负样本) self_mask = torch.eye(2*batch_size, dtype=torch.bool, device=z.device) # 将mask中的对角线位置去掉(因为我们不需要自己和自己的比较) # 但是,我们的mask并不包括对角线(因为我们设置的正样本位置不是对角线),所以self_mask表示的是自身与自身的位置,这些位置我们希望忽略(既不是正样本也不是负样本)。 # 因此,在计算负样本时,我们也要排除self_mask位置。 # 所以,负样本掩码 = ~mask 且 不是self_mask neg_mask = ~mask & ~self_mask ``` 然后,正样本仍然从mask中取,负样本从neg_mask中取。 完整代码: ```python class ContrastiveLoss(nn.Module): def __init__(self, temperature=0.5): super().__init__() self.temperature = temperature def forward(self, z_i, z_j): batch_size = z_i.size(0) z = torch.cat([z_i, z_j], dim=0) # (2*batch_size, feature_dim) # 计算相似度矩阵(余弦相似度) # 注意:先归一化,这样余弦相似度就是点积 z = F.normalize(z, dim=1) sim = torch.mm(z, z.T) / self.temperature # (2N, 2N) # 构建正样本掩 mask:标记正样本对位置 mask = torch.zeros(2 * batch_size, 2 * batch_size, dtype=torch.bool, device=z.device) mask[torch.arange(batch_size), torch.arange(batch_size) + batch_size] = True mask[torch.arange(batch_size) + batch_size, torch.arange(batch_size)] = True # 构建自身掩码(对角线) self_mask = torch.eye(2 * batch_size, dtype=torch.bool, device=z.device) # 正样本:mask标记的位置(每个样本有一个正样本) pos_samples = sim[mask].view(2*batch_size, -1) # (2N, 1) # 负样本:既不是正样本也不是自身的位置 neg_samples = sim[~mask & ~self_mask].view(2*batch_size, -1) # (2N, 2N-2) # 构建logits: 将正样本得分和负样本得分拼接(正样本放在第一个位置) logits = torch.cat([pos_samples, neg_samples], dim=1) # (2N, 2N-1) # 标签:正样本在第一个位置,所以标签为0 labels = torch.zeros(2*batch_size, dtype=torch.long, device=z.device) loss = F.cross_entropy(logits, labels) return loss ``` 这样,负样本的数量是2N-2,logits的形状为(2N, 2N-1)。 最后,需要注意的是,这种实现在大batch size时仍然会消耗较多内存,因为相似度矩阵是O(N^2)的。在实际应用中,可能需要结合分布式训练或使用其他优化策略[^1][^3]。 希望以上回答能够帮助您理解对比学习中正负样本的定义和处理方式。 </think> 在PyTorch对比学习中,正负样本的定义和处理是核心机制,直接影响模型学习表征的能力。以下是详细解析及代码示例: ### 一、正负样本定义 1. **正样本** 同一原始图像经过不同数据增强得到的两个视图特征: $$z_i = f(\text{augment}(x)), \quad z_j = f(\text{augment}(x))$$ 其中$f$为编码器网络,$\text{augment}$为数据增强函数[^3]。 2. **负样本** 同一batch中不同原始图像生成的视图特征: $$z_k = f(\text{augment}(x')) \quad \text{where} \ x' \neq x$$ ### 二、处理方式与代码实现 #### 1. 数据增强生成正样本对 ```python class ContrastiveTransform: def __init__(self): self.transform = transforms.Compose([ transforms.RandomResizedCrop(32, scale=(0.2, 1.0)), transforms.RandomHorizontalFlip(), transforms.ColorJitter(0.8, 0.8, 0.8, 0.2), transforms.RandomGrayscale(p=0.2), transforms.ToTensor(), transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)) ]) def __call__(self, x): return self.transform(x), self.transform(x) # 生成正样本对 ``` #### 2. 特征空间正负样本分离 ```python def forward(self, z_i, z_j): batch_size = z_i.size(0) z = torch.cat([z_i, z_j], dim=0) # 拼接所有样本 [2B, D] # 计算余弦相似度矩阵 sim_matrix = F.cosine_similarity(z.unsqueeze(1), z.unsqueeze(0), dim=2) sim_matrix /= self.temperature # 温度缩放 # 构建正样本掩码 (对角线排除) mask = torch.zeros(2*batch_size, 2*batch_size, dtype=torch.bool) indices = torch.arange(batch_size) mask[indices, indices+batch_size] = True # 前B样本对应后B样本 mask[indices+batch_size, indices] = True # 后B样本对应前B样本 mask.fill_diagonal_(False) # 排除自身相似度 # 提取正负样本得分 pos_samples = sim_matrix[mask].view(2*batch_size, -1) # [2B, 1] neg_samples = sim_matrix[~mask].view(2*batch_size, -1) # [2B, 2B-2] # 构建分类logits logits = torch.cat([pos_samples, neg_samples], dim=1) # [2B, 2B-1] labels = torch.zeros(2*batch_size, dtype=torch.long) # 正样本位置为0 return F.cross_entropy(logits, labels) ``` #### 3. 损失函数计算(NT-Xent) $$ \mathcal{L} = -\log \frac{\exp(\text{sim}(z_i,z_j)/\tau)}{\sum_{k\neq i} \exp(\text{sim}(z_i,z_k)/\tau)} $$ 其中$\tau$为温度超参数,控制分布尖锐程度[^1]。 ### 三、关键处理细节 1. **归一化处理** 计算相似度前需特征归一化: ```python z = F.normalize(z, p=2, dim=1) # L2归一化 ``` 2. **大batch size优化** 负样本数量与batch size成正比: ```python # 建议batch size ≥ 512 train_loader = DataLoader(dataset, batch_size=512, shuffle=True) ``` 3. **温度参数影响** $\tau$值需精细调整(通常0.05-0.5): ```python criterion = ContrastiveLoss(temperature=0.07) # SimCLR默认值 ``` ### 四、视觉解析 ``` 特征空间示意图: z_i (anchor) ↗ ↘ [正样本z_j] [负样本z_k1, z_k2,...] 相似度↑ 相似度↓ ``` > 该机制使模型学习到:同源图像增强视图在特征空间紧密聚集($z_i ≈ z_j$),异源图像相互远离($z_i \perp z_k$)[^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值