• 【专题学习】对比学习原理及代码


    本来的雄心壮志是大致看完计算机视觉 - 对比学习的13篇文章(精读+略读),结果看了前两篇发现真的很难理解。最后,调整下目标:先以InstDisc和CPC两篇文章作为引入,搞清楚NCE loss和InfoNCE loss的原理及代码,最后达到可以在自己的任务中设计这两种损失的程度…

    前置知识

    有一些前置概念先做一些简单的科普:

    1. odds、logit:概率 P ( A ) P(A) P(A)是指事件A发生的概率,odds指的是几率,表示事件A发生的概率和事件A不发生的概率之比,即 P ( A ) / 1 − P ( A ) P(A)/1-P(A) P(A)/1P(A),对数几率是对几率取对数,即 l o g P ( A ) 1 − P ( A ) log{\frac{P(A)}{1-P(A)}} log1P(A)P(A),logit就是将概率 P ( A ) P(A) P(A)转化为 l o g P ( A ) 1 − P ( A ) log{\frac{P(A)}{1-P(A)}} log1P(A)P(A)的这个变换 l o g i t ( P ) = l o g ( p 1 − p ) logit(P) = log(\frac{p}{1-p}) logit(P)=log(1pp)。具体可参考FAQ: HOW DO I INTERPRET ODDS RATIOS IN LOGISTIC REGRESSION?
    2. Log-bilinear model:对数双线性模型,有一篇博客讲得非常好:Log-bilinear model
    3. RNN语言模型:生成自然语言句子或文档的模型,用于评估一个句子或模型有多自然。具体参考RNN Language Models
    4. 无监督学习和自监督学习:无监督学习是说使用没有标签的数据进行学习的过程,自监督学习是利用数据自主生成标签的学习过程,一般认为自监督学习是无监督学习的一种
    5. Autoregressive model:自回归模型,回归模型是说对一些自变量 x i x_i xi进行因变量 y y y的预测,而在自回归中, x i x_i xi y y y的过去值。参考:Autoregressive models
    6. 交叉熵损失和二元交叉熵损失:两者的区别在于,二元交叉熵损失在标签 y i y_i yi 0 0 0的时候,也会计算入损失。具体参考:交叉熵损失和二元交叉熵损失

    InstDisc

    2018-CVPR-Unsupervised Feature Learning via Non-Parametric Instance Discrimination
    任务:无监督特征学习
    代理任务:个体判别
    基本思想:将每个个体都当作是一个独立的类,来学习每个类的特征表示
    主要创新:类如果太多的话,常规的softmax方法会因为计算爆炸而无法处理那么多类,于是使用了NCE方法来将多分类问题转化为二分类问题
    其他创新:1)将原本有参数的softmax分类器替换为存储个体特征的无参数内存银行;2)测试阶段使用加权的knn算法来对目标样本进行个体判别
    备注:在距离计算中使用了温度超参数 τ \tau τ来控制特征在映射空间中的分散程度
    NCE loss原理分析
    本来是参考求通俗易懂解释下nce loss? - 石塔西的回答 - 知乎candidate_sampling这两篇文章学习NCE的,因为前者比较系统,后者是官方文件,但是感觉这两篇文章理解起来有些困难,所以可以选择性地浏览下

    J N C E ( θ ) = − E P d [ l o g   h ( i , v ) ] − m E P n [ l o g ( 1 − h ( i , v ′ ) ) ] J_{NCE}(\theta) = -E_{P_d}[log\ h(i, v)] - m E_{P_n}[log(1-h(i,v^{'}))] JNCE(θ)=EPd[log h(i,v)]mEPn[log(1h(i,v))] P n P_n Pn包括两部分:target在噪声分布中对应的概率,记为 P n : t a r g e t P_{n:target} Pn:target;及noise在噪声分布中对应的概率,记为 P n : n o i s e P_{n:noise} Pn:noise
    其中, h ( i , v ) : = P ( D = 1 ∣ i , v ) = P ( i ∣ v ) P ( i ∣ v ) + m P n : t a r g e t ( i ) h(i,v) := P(D=1|i,v) = \frac{P(i|v)}{P(i|v)+mP_{n:target}(i)} h(i,v):=P(D=1∣i,v)=P(iv)+mPn:target(i)P(iv)
    P ( i ∣ v ) = e x p ( v T f i / τ ) ∑ j = 1 n e x p ( v j T f i / τ ) P(i|v) = \frac{exp(v^Tf_i/\tau)}{\sum_{j=1}^nexp(v_j^Tf_i/\tau)} P(iv)=j=1nexp(vjTfi/τ)exp(vTfi/τ)
    相应地, h ( i , v ′ ) : = P ( D = 0 ∣ i , v ) = P ( i ∣ v ′ ) P ( i ∣ v ′ ) + m P n : n o i s e ( i ) h(i,v^{'}) := P(D=0|i,v) = \frac{P(i|v^{'})}{P(i|v^{'})+mP_{n:noise}(i)} h(i,v):=P(D=0∣i,v)=P(iv)+mPn:noise(i)P(iv)
    P ( i ∣ v ′ ) = e x p ( v ′ T f i / τ ) ∑ j = 1 n e x p ( v j T f i / τ ) P(i|v^{'}) = \frac{exp(v^{{'}T}f_i/\tau)}{\sum_{j=1}^nexp(v_j^Tf_i/\tau)} P(iv)=j=1nexp(vjTfi/τ)exp(vTfi/τ)
    总结下,NCE损失是一个二元交叉熵损失的形式,其中第一项是对正例进行的计算, P d P_d Pd是指真实分布;第二项是对负例进行的计算, P n P_n Pn是指噪声分布; v v v表示target计算出的特征, v ′ v^{'} v表示noise计算出的特征
    NCE loss代码分析
    这里找了一个github上面比较popular的RNNLM(RNN语言模型)的代码来分析(主要是因为轻松就能运行,不用复杂地下数据集配环境啥的),接下来以这个代码为例,结合上面的公式进行分析:

    p_true = logit_model.exp() / (logit_model.exp() + self.noise_ratio * logit_noise.exp())
    
    • 1

    这一句是在计算 J N C E J_{NCE} JNCE中的 h ( i , v ) h(i,v) h(i,v) h ( i , v ′ ) h(i,v^{'}) h(i,v),其中self.noise_ratio是指负样本数量是正样本数量的多少倍,即公式中的 m m m,代码中是 50 50 50;具体来说,p_true其实就是 [ h ( i , v ) , h ( i , v ′ ) ] [h(i,v), h(i,v^{'})] [h(i,v),h(i,v)] [ ∗ , ∗ ∗ ] [*, **] [,]表示对 ∗ * ∗ ∗ ** 进行concat),维度是bsz, max_len, num_target+noise_ratio=1+m=51;有了p_true之后,再准备好label就可以进行二元交叉熵损失的计算了:

    label = torch.zeros_like(logit_model)
    label[:, :, 0] = 1
    
    loss = self.bce_with_logits(p_true, label).sum(dim=2)
    
    • 1
    • 2
    • 3
    • 4

    logit_modellogit_noise及相应的变量(dim=2的维度上是51的变量)也都需要拆开为两部分来看,其中[:, :, 0]代表真实数据部分,而[:, :, 1:]代表噪声部分;下面分开介绍logit_modellogit_noise

    1. logit_model.exp()表示的是 [ P ( i ∣ v ) , P ( i ∣ v ′ ) ] [P(i|v), P(i|v^{'})] [P(iv),P(iv)]logit_model是此概率的对数,维度是bsz, max_len, num_target+noise_ratio=51),其计算方式为:
    logit_model = torch.cat([logit_target_in_model.unsqueeze(2), logit_noise_in_model], dim=2)
    
    • 1

    logit_target_in_model计算的是 P ( i ∣ v ) P(i|v) P(iv),表示模型计算出的正样本(target)单词的logit,维度是bsz, max_len, 1,其实是当前样本与正样本放入模型后的输出之间的内积,但是这个内积并没有经过温度超参数 τ \tau τ的调整和softmax归一化(原InstDisc公式中之所以用softmax归一化,是因为个体判别任务中类别概念的存在);

    logit_noise_in_model则表示模型计算出的负样本(noise)单词的logit,计算的是 P ( i ∣ v ′ ) P(i|v^{'}) P(iv);维度是bsz, max_len, noise_ratio,其实是当前样本与负样本放入模型后的输出之间的内积;

    先看logit_target_in_model,想要计算正样本的logit,只需要找到正样本的单词对应词汇表的index,再放入模型就可以了;正样本直接从数据集中拿

    logit_noise_in_model,则需要先构造负样本,构造过程需要确定三个东西:1)负样本符合的分布;2)负样本的数量;3)生成负样本的方法;代码中给出的解决方案是:
    1)使用数据集中原本的单词分布来作为负样本的分布:

    def build_unigram_noise(freq):
        total = freq.sum() # freq表示数据集中不同单词出现的频率,这个用很多现成包可以直接计算
        noise = freq / total
        assert abs(noise.sum() - 1) < 0.001
        return noise
    
    • 1
    • 2
    • 3
    • 4
    • 5

    2)负样本的数量即noise_ratio,这里是 50 50 50
    3)生成负样本的方法使用别名方法,听说这种方法更快,别名方法类存储在/nce/alias_multinomial.py中,其中__init__()方法用于初始化,draw()方法用于按照分布抽取某个数量的采样

    1. logit_noise.exp()表示的是噪声分布,logit_noise的计算方式是:
    logit_noise = torch.cat([logit_target_in_noise.unsqueeze(2), logit_noise_in_noise], dim=2)
    
    • 1

    其中,logit_target_in_noise表示的是噪声分布给出的正样本的logit,即 P n : t a r g e t P_{n:target} Pn:targetlogit_noise_in_noise表示的是噪声分布给出的负样本的logit,即 P n : n o i s e P_{n:noise} Pn:noise。这两者的计算方式是:

    probs = noise / noise.sum() # noise已经介绍过了,是所有的单词在词汇表中出现的频率
    probs = probs.clamp(min=BACKOFF_PROB)
    renormed_probs = probs / probs.sum()
    
    self.register_buffer('logprob_noise', renormed_probs.log())
    logit_noise_in_noise = self.logprob_noise[noise_samples.data.view(-1)].view_as(noise_samples)
    logit_target_in_noise = self.logprob_noise[target.data.view(-1)].view_as(target)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    也就是说,直接在归一化的负样本分布(也是所有单词的分布)中找到target_word(正样本对应的单词)或noise_word(负样本选取的单词)对应的频率;
    总结
    总算是把公式和代码对应起来了,两者如此难理解的原因是,代码中把真实数据分布和噪声部分concat在一起做了计算,这个蜜汁操作一度让我百思不得其解…现在看来,代码和公式中唯一不同的地方就在于 P P P的计算是否经过温度超参数 τ \tau τ的调整和softmax归一化了

    CPC

    2018-arXiv-Representation Learning with Contrastive Predictive Coding
    任务:无监督特征学习
    代理任务:预测编码(本来是神经科学中的名词,这里解释一下,即利用前t个时间步的输入 { x 1 , x 2 , . . . , x t } \{x_1, x_2, ..., x_{t}\} {x1,x2,...,xt}学习时间步t的表示 c t c_t ct,使这个表示能够更好地预测未来时间步的表示 { x t + 1 , x t + 2 , . . . , x t + k } \{x_{t+1}, x_{t+2}, ..., x_{t+k}\} {xt+1,xt+2,...,xt+k}
    动机:希望能够通过学习不同时间步信号的底层共享信息,来学习到一个好的表示
    网络流程:将输入 { x 1 , x 2 , . . . , x t } \{x_1, x_2, ..., x_{t}\} {x1,x2,...,xt}首先放入编码器 g e n c g_{enc} genc得到 { z 1 , z 2 , . . . , z t } \{z_1, z_2, ..., z_{t}\} {z1,z2,...,zt},然后放入自回归模型 g a r g_{ar} gar得到目标表示 c t c_t ct,通过 c t c_t ct来预测未来的表示 { z t + 1 ^ , z t + 2 ^ , . . . , z t + k ^ } \{\hat{z_{t+1}}, \hat{z_{t+2}}, ..., \hat{z_{t+k}}\} {zt+1^,zt+2^,...,zt+k^},将此预测与真实的通过 { x t + 1 , x t + 2 , . . . , x t + k } \{x_{t+1}, x_{t+2}, ..., x_{t+k}\} {xt+1,xt+2,...,xt+k}放入 g e n c g_{enc} genc得到的 { z t + 1 , z t + 2 , . . . , z t + k } \{z_{t+1}, z_{t+2}, ..., z_{t+k}\} {zt+1,zt+2,...,zt+k}比较,准确的说是构建对比损失InfoNCE,从而更好的学习目标表示 c t c_t ct
    在这里插入图片描述

    吐槽:这篇文章写得很晦涩,多亏理解Contrastive Predictive Coding和NCE Loss这篇文章才理解
    InfoNCE loss原理分析
    L N = − E X [ l o g f k ( x t + k , c t ) ∑ x j ∈ X f k ( x j , c t ] L_N = -E_X[log\frac{f_k(x_{t+k}, c_t)}{\sum_{x_j\in X}f_k(x_j, c_t}] LN=EX[logxjXfk(xj,ctfk(xt+k,ct)]
    其中, f k ( x t + k , c t ) = e x p ( z t + k T z t + k ^ ) f_k(x_{t+k},c_t) = exp(z_{t+k}^T\hat{z_{t+k}}) fk(xt+k,ct)=exp(zt+kTzt+k^)
    其中, z t + k ^ = W k c t \hat{z_{t+k}} = W_k c_t zt+k^=Wkct
    损失函数是一个softmax交叉熵损失的形式,当分子越大,使得整个分式更接近1的时候(不会大于1),损失函数最小,因此要使预测得到的 z ^ \hat{z} z^和真实的 z z z尽可能相乘更大,即更相近
    InfoNCE loss代码分析

    TO BE CONTINUED
    
    • 1

    构造NCE损失

    构造过程如下:

    1. 确定公式中的样本 i i i和正样本 v v v,从而计算出 P ( i ∣ v ) P(i|v) P(iv)(这一步还是比较容易的)
    2. 确定负样本服从的分布及采样方式。这一步通常是较困难的,它往往跟特定任务有关;比如InstDisc这篇文章中,就是以 1 n \frac{1}{n} n1这个均匀分布作为样本服从的分布, n n n为类别数(因为是个体判别任务,所以也是样本数);而NCE代码中介绍的RNN语言模型任务,就是以每个单词在训练集中出现的频率作为选择负样本的分布;我的任务是一个检索任务,一般来说,检索任务的正样本和负样本都是现成的,可以直接用,但是负样本的选取往往会影响模型最终的性能,负样本与正样本的区分难度越大,模型越容易competitive
    3. 调整 m m m τ \tau τ等超参数以适应特定网络
  • 相关阅读:
    【vue3+ts后台管理】登录页面完成
    Shell编写规范和变量
    京东商品详情API:电商创新的利器
    图扑数字孪生洗煤厂,低代码构建云端工厂
    【Linux】简化自用-Win10安装VMware和CentOS
    【CPP】函数重载、模版
    如何将RAW格式的磁盘修改为NTFS?教给你三种操作方法
    广州咖啡加盟怎么开,我有一个梦想,开一家自己咖啡店
    zookeeper的安装与配置
    Git与GitHub:解锁版本控制的魔法盒子
  • 原文地址:https://blog.csdn.net/YasmineC/article/details/127696833