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)