https://github.com/bojone/SimCSE
源代码如下:
def simcse_loss(y_true, y_pred):
"""用于SimCSE训练的loss
"""
# 构造标签
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)
刚看可能觉得蒙圈,那是因为这块代码的逻辑要和数据加载的部分结合起来看:
class data_generator(DataGenerator):
"""训练语料生成器
"""
def __iter__(self, random=False):
batch_token_ids = []
for is_end, token_ids in self.sample(random):
batch_token_ids.append(token_ids)
batch_token_ids.append(token_ids)
if len(batch_token_ids) == self.batch_size * 2 or is_end:
batch_token_ids = sequence_padding(batch_token_ids)
batch_segment_ids = np.zeros_like(batch_token_ids)
batch_labels = np.zeros_like(batch_token_ids[:, :1])
yield [batch_token_ids, batch_segment_ids], batch_labels
batch_token_ids = []
从数据加载部分的代码可以看出,加载的数据是a,a,b,b,c,c,d,d,...
所以,假设根据除了句子自己其他都是负样本的思想,可以得出,y_true应该为:
,其中A=。
所以,
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())
#以上代码等价于
idxs = K.arange(0, K.shape(y_pred)[0])
idx1 = idxs[None, :]
idx2 = idxs[:,None]
#y_true=tf.cast(tf.equal(tf.abs(tf.subtract(idx1,idx2)),1),dtype=tf.int32)
y_true=np.equal(np.abs(np.subtract(idx1,idx2)),1).astype(np.int32)
标签构造理解了,接下来,对于相似性的计算就原理如下:
数据都和自己相似,由于bert模型本身增加了遮罩处理和dropout处理,所以即使同一个句子两次输入产生的向量也会有所差别,利用这个原理,将同一个向量相似计算(对角线)数据无穷小,以便于模型训练时此数据对模型训练的贡献减少;同一个句子两次数据的向量进行相似性计算,期望二则的更加相似。
对于 similarities = similarities - tf.eye(K.shape(y_pred)[0]) * 1e12代码,可以理解为将对角线元素无穷小。对角线即是和完全一样的子集的相似性比较,使其无穷小也就是忽略这块数据对模型优化的影响;
对于similarities = similarities * 20,这块是在忽略对角数据后的操作,表示将相似性方大20倍,这样可以使得模型更快的收敛。