SimCSE核心源码解读


SimCSE巧妙利用了Dropout做对比学习,想法简单、效果惊艳。对比学习的核心就是loss的编写,官方给出的源码,loss写的略复杂。苏神的loss实现就相当的简单明了,今天,就记录下苏神源码中loss的阅读笔记。

源码

def simcse_loss(y_true, y_pred):
    idxs = K.arange(0, K.shape(y_pred)[0])
    idxs_1 = idxs[None, :]
    idxs_2 = (idxs + 1 - idxs % 2 * 2)[:, None]
    y_true = K.equal(idxs_1, idxs_2)
    y_true = K.cast(y_true, K.floatx())
    y_pred = K.l2_normalize(y_pred, axis=1)
    similarities = K.dot(y_pred, K.transpose(y_pred))
    similarities = similarities - tf.eye(K.shape(y_pred)[0]) * 1e12
    similarities = similarities * 20
    loss = K.categorical_crossentropy(y_true, similarities, from_logits=True)
    return K.mean(loss)

下面,我们逐行看看这个loss是如何写出来的。

输入

刚开始看代码时,一直好奇,同一句话两次drop out在哪里实现的。后来发现,每个batch内,每一句话都重复了一次。举例来说,句子a,b,c。编成一个batch就是:

[a,a,b,b,c,c]

请记住这个例子,因为后面代码的解读,我们就用这个小例子来说明了。

这个loss的输入中y_true只是凑数的,并不起作用。因为真正的y_true是通过batch内数据计算得出的。y_pred就是batch内的每句话的embedding,通过bert编码得来。

第一行

idxs = K.arange(0, K.shape(y_pred)[0])

这行的作用,就是生成batch内句子的编码。根据我们的例子,idxs就是:

[0,1,2,3,4,5]

第二行

idxs_1 = idxs[None, :]

给idxs添加一个维度,变成: [[0,1,2,3,4,5]]

第三行

idxs_2 = (idxs + 1 - idxs % 2 * 2)[:, None]

这个其实就是生成batch内每句话同义的句子的id。

idxs + 1 - idxs % 2 * 2

这个意思就是说,如果一个句子id为奇数,那么和它同义的句子的id就是它的上一句,如果一个句子id为偶数,那么和它同义的句子的id就是它的下一句。 [:, None] 是在列上添加一个维度。 最后生成的结果就是这样:

[
[1],

[0],

[3],

[2],

[5],

[4]
]

可以认为,这个是初步标注了batch中每句话的标签。

第四行、第五行

y_true = K.equal(idxs_1, idxs_2) y_true = K.cast(y_true, K.floatx())

这两行是生成计算loss时可用的标签。 idxs_1的shape是1X6,idxs_2的shape是6X1。 它们做equal操作时,两个都要进行broadcast,变成6X6。 idxs_1变成:

[
[0,1,2,3,4,5]

[0,1,2,3,4,5]

[0,1,2,3,4,5]

[0,1,2,3,4,5]

[0,1,2,3,4,5]

[0,1,2,3,4,5]
]

idxs_2变成:

[
[1,1,1,1,1,1],

[0,0,0,0,0,0],

[3,3,3,3,3,3],

[2,2,2,2,2,2],

[5,5,5,5,5,5],

[4,4,4,4,4,4]
]

shape一致后,再做equal,就得到了:

[
[0,1,0,0,0,0],

[1,0,0,0,0,0],

[0,0,0,1,0,0],

[0,0,1,0,0,0],

[0,0,0,0,0,1],

[0,0,0,0,1,0]

]

这个就是可以输入K.categorical_crossentropy的标签数据了。

第六、七、八、九行

y_pred = K.l2_normalize(y_pred, axis=1)

similarities = K.dot(y_pred, K.transpose(y_pred))

similarities = similarities - tf.eye(K.shape(y_pred)[0]) * 1e12

similarities = similarities * 20

首先对句向量各个维度做了一个L2正则,使其变得各项同性,避免下面计算相似度时,某一个维度影响力过大。

其次,计算batch内每句话和其他句子的内积相似度。

然后,将和自身的相似度变为0。

最后, 将所有相似度乘以20,这个目的是想计算softmax概率时,更加有区分度。

一顿操作下来,similarities如下所示:

[
[0,0.2,0.3,0.4,0.5,0.6],

[0.2,0,0.3,0.4,0.5,0.6],

[0.2,0.3,0,0.4,0.5,0.6],

[0.2,0.3,0.4,0,0.5,0.6],

[0.2,0.3,0.4,0.5,0,0.6],

[0.2,0.3,0.4,0.5,0.6, 0],

]

上面矩阵的对角线部分都是0,代表每句话和自身的相似性并不参与运算。

最后一行

计算多分类的交叉熵。我们的batch是6,这就是一个6分类问题。每句话都以batch内同义的句子的id作为自己的label。

loss = K.categorical_crossentropy(y_true, similarities, from_logits=True)

原创文章,转载请注明出处,否则拒绝转载!
本文链接:抬头看浏览器地址栏

上篇: 神经网络的反向传播实例
下篇: 对比学习训练技巧