词语embedding
word embedding 已经很常见也很容易实现,但是在各种应用场景下,特别是搜索情况下,一些名词不应该分开embedding,比如。小米 ,安踏。等等。如果用户搜索小米,分词器都是以单个词作为分隔,所以如果对名词embedding需要重新构建分词器。
第一步构建词库
如何构建由词语构成的词库呢,答案是使用jieba分词器提取你的语料库。
tags = jieba.analyse.extract_tags(content, topK=topK)
content是一个字符串,topk 是要提取多少词语,
topK 为返回几个 TF/IDF 权重最大的关键词,默认值为 20。
然后再将结果写入文件中,得到词库如下
第二步,构建词语与数字的映射
再输入到模型前,需要将其转化为数字。如 新款2021 映射后就是[0,1],当然还需要加一些特殊字符,比如cls 这种。
这里采用的是tensorflow 的
vectorize_layer = tf.keras.layers.TextVectorization(
max_tokens=50005,
split='whitespace',
output_mode='int',
# output_sequence_length=2,
# pad_to_max_tokens=True,
vocabulary=vocab)
注意分词时vectorize_layer 会以空格分,我们用jieba分词后句子会形成[‘aaaaa’, ‘啊啊啊’, ‘吧吧吧’]这种形式,我们只需用’ '.join(). 连接起来就可以了。
skim-gram
选取一个单词作为context word,然后再在一个滑动窗口下面随机选一个作为target word,这样就构成一个正样本对的训练样本,tensorflow 提供了一个很好用的接口如下
window_size = 2
positive_skip_grams, _ = tf.keras.preprocessing.sequence.skipgrams(
example_sequence,
vocabulary_size=vocab_size,
window_size=window_size,
negative_samples=0)
print(len(positive_skip_grams))
如果你example_sequence = a b c d e f. window_size =2 那positive_skip_grams可能如下
(2, 3): (a, b)
(55, 2): (c, a)
(43, 22): (d, e)
(11, 22): (f, e)
(3, 43): (b, d)
最终输入模型时如下:
负样本随机取词库的其他的单词,tensorflow也提供一个方便的接口:
negative_sampling_candidates, _, _ = tf.random.log_uniform_candidate_sampler(
true_classes=context_class, # class that should be sampled as 'positive'
num_true=1, # each positive skip-gram has 1 positive context class
num_sampled=num_ns, # number of negative context words to sample
unique=True, # all the negative samples should be unique
range_max=vocab_size, # pick index of the samples from [0, vocab_size]
seed=SEED, # seed for reproducibility
name="negative_sampling" # name of this operation
)
nce损失
模型采用最简单的,直接输出context word,target word,还有负样本的embedding,然后计算nce损失。
class Word2v(tf.keras.Model):
def __init__(self):
super(Word2v, self).__init__()
# self.encode =encoder
self.embed = tf.keras.layers.Embedding(50005, 128, mask_zero=True)
def call(self, inputs,laebl,neg):
input = self.embed(inputs)
label = self.embed(laebl)
neg = self.embed(neg)
return input,label,neg
由于我只想构建词语embedding,而词语是很多的,我只提取了5万个词语。单个字的单词词库里没有,所以vectorize_layer过后会有很多1的存在,1代表未知词语,词库里没有,这时计算nce loss 时要把这些带1 的不管是context word中带1,还是target word中带1,这些样本其损失都置0,只计算词库中都有的样本对。损失如下
@tf.function
def loss_nce(cont, target, neg, input, label):
cont_loss = tf.not_equal(input, 1)
target_loss = tf.not_equal(label, 1)
cont_loss = tf.cast(cont_loss, dtype=tf.float32)
target_loss = tf.cast(target_loss, dtype=tf.float32)
cont_target = cos_sim(cont, target)
cont_neg = cos_sim(cont, neg)
pos = tf.exp(cont_target)
pos = tf.reshape(pos, [pos.shape[0], 1])
# print(pos.shape)
total = tf.exp(cont_neg)
n_sum = tf.reduce_sum(total, axis=-1)
# print(n_sum.shape)
loss = pos / (pos + n_sum)
loss = tf.math.log(loss)
# print(loss.shape)
# print(cont_loss.shape)
loss = loss * cont_loss * target_loss
loss = -loss
loss = tf.reduce_sum(loss)
return loss
cont为context word embedding后的结果 , target为context word embedding后的结果 , neg为负采样就是随机词库里的词语 embedding后的结果, input 是context word embedding前,也就是模型的输入 ,主要是用他来判断哪里含有1,将对应位置的损失置0, label 就是输入模型的target。
@tf.function
def train_step(input, label, neg):
with tf.GradientTape() as tape:
cont, target, neg = model(input, label, neg)
loss_value = loss_nce(cont, target, neg, input, label)
grads = tape.gradient(loss_value, model.trainable_weights)
optimizer.apply_gradients(zip(grads, model.trainable_weights))
return loss_value