本来的雄心壮志是大致看完计算机视觉 - 对比学习的13篇文章(精读+略读),结果看了前两篇发现真的很难理解。最后,调整下目标:先以InstDisc和CPC两篇文章作为引入,搞清楚NCE loss和InfoNCE loss的原理及代码,最后达到可以在自己的任务中设计这两种损失的程度…
2018-CVPR-Unsupervised Feature Learning via Non-Parametric Instance Discrimination
任务:无监督特征学习
代理任务:个体判别
基本思想:将每个个体都当作是一个独立的类,来学习每个类的特征表示
主要创新:类如果太多的话,常规的softmax方法会因为计算爆炸而无法处理那么多类,于是使用了NCE方法来将多分类问题转化为二分类问题
其他创新:1)将原本有参数的softmax分类器替换为存储个体特征的无参数内存银行;2)测试阶段使用加权的knn算法来对目标样本进行个体判别
备注:在距离计算中使用了温度超参数 τ \tau τ来控制特征在映射空间中的分散程度
NCE loss原理分析:
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(1−h(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(i∣v)+mPn:target(i)P(i∣v)
而 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(i∣v)=∑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(i∣v′)+mPn:noise(i)P(i∣v′)
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(i∣v′)=∑j=1nexp(vjTfi/τ)exp(v′Tfi/τ)
总结下,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())
这一句是在计算 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)
而logit_model
和logit_noise
及相应的变量(dim=2的维度上是51的变量)也都需要拆开为两部分来看,其中[:, :, 0]
代表真实数据部分,而[:, :, 1:]
代表噪声部分;下面分开介绍logit_model
和logit_noise
:
logit_model.exp()
表示的是 [ P ( i ∣ v ) , P ( i ∣ v ′ ) ] [P(i|v), P(i|v^{'})] [P(i∣v),P(i∣v′)](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)
logit_target_in_model
计算的是 P ( i ∣ v ) P(i|v) P(i∣v),表示模型计算出的正样本(target)单词的logit,维度是bsz, max_len, 1
,其实是当前样本与正样本放入模型后的输出之间的内积,但是这个内积并没有经过温度超参数 τ \tau τ的调整和softmax归一化(原InstDisc公式中之所以用softmax归一化,是因为个体判别任务中类别概念的存在);
而logit_noise_in_model
则表示模型计算出的负样本(noise)单词的logit,计算的是 P ( i ∣ v ′ ) P(i|v^{'}) P(i∣v′);维度是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
2)负样本的数量即noise_ratio
,这里是 50 50 50
3)生成负样本的方法使用别名方法,听说这种方法更快,别名方法类存储在/nce/alias_multinomial.py
中,其中__init__()
方法用于初始化,draw()
方法用于按照分布抽取某个数量的采样
logit_noise.exp()
表示的是噪声分布,logit_noise
的计算方式是:logit_noise = torch.cat([logit_target_in_noise.unsqueeze(2), logit_noise_in_noise], dim=2)
其中,logit_target_in_noise
表示的是噪声分布给出的正样本的logit,即 P n : t a r g e t P_{n:target} Pn:target;logit_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)
也就是说,直接在归一化的负样本分布(也是所有单词的分布)中找到target_word(正样本对应的单词)或noise_word(负样本选取的单词)对应的频率;
总结:
总算是把公式和代码对应起来了,两者如此难理解的原因是,代码中把真实数据分布和噪声部分concat在一起做了计算,这个蜜汁操作一度让我百思不得其解…现在看来,代码和公式中唯一不同的地方就在于 P P P的计算是否经过温度超参数 τ \tau τ的调整和softmax归一化了
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
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[log∑xj∈Xfk(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
构造过程如下: