从零开始学Python自然语言处理(三)——手把手带你实现word2vec(skip-gram)

浏览: 3261

前文传送门:

从零开始学自然语言处理(二)——手把手带你用代码实现word2vec

上一期我们用keras实现了CBOW模型。

本期我们来实现skip-gram模型。其实只需要对CBOW模型稍加改动就可以了。

keras 实现 word2vec 的 skip-gram 模型

上一期也提到了,小编用keras写的CBOW模型是参考了苏神的代码。苏神在他博客中说,“上面是CBOW模型的代码,如果需要Skip-Gram,请自行修改,Keras代码这么简单,改起来也容易。”

大神是理解不了我们这些渣渣的难处的。网上几乎再也找不到用keras实现skip-gram的代码。看来真的要逼自己去实现了。

看了几个其它框架实现的模型,其与CBOW的差异主要是在于训练数据的构建。

我们都知道,CBOW模型是用周围词去预测中心词。例如对于一句话“我 喜欢 学习 NLP !”,CBOW就是要用“我”,“喜欢”,“NLP”,“!”去预测“学习”这个词。我们在构造训练数据的时候,要去再构造几个负样本,这样模型的训练目标就是从“学习”和几个负样本词中,使预测“学习”的概率最大化。这个套路很简单,也很make sense。

而skip-gram的做法,是用“学习”去预测“我”,“喜欢”,“NLP”,“!”这四个词。我看网上的代码在构建训练数据的时候,是分别给“我”,“喜欢”,“NLP”,“!”构造负样本,这样一句话就构造了四条训练数据。什么意思呢?就是说,输入“学习”,要从“我”、负样本1、负样本2...中,使模型预测“我”的概率最大化;输入“学习”,我要从“喜欢”、负样本1、负样本2...中,使模型预测“喜欢”的概率最大化,......,这样训练四次。

我就觉得这比较扯淡了啊,感觉把“我”,“喜欢”,“学习”,“NLP”,“!”这五个词的相关性硬生生给拆开了。因此小编对数据集的构造也进行了适当的修改。

上代码!

这里我们只把和CBOW模型代码不一样的地方写出来。首先是负样本的采集。

def get_negtive_sample(x, word_range, neg_num):
negs = []
while True:
rand = random.randrange(0, word_range)
if rand not in negs and rand not in x:
negs.append(rand)
if len(negs) == neg_num:
return negs

构造训练数据!

def data_generator(): #训练数据生成器
x,y = [],[]

for sentence in corpus:
sentence = [0]*window + [word2id[w] for w in sentence if w in word2id] + [0]*window
#上面这句代码的意思是,因为我们是通过滑窗的方式来获取训练数据的,那么每一句语料的第一个词和最后一个词
#如何出现在中心位置呢?答案就是给它padding一下,例如“我/喜欢/足球”,两边分别补窗口大小个pad,得到“pad pad 我 喜欢 足球 pad pad”
#那么第一条训练数据的背景词就是['pad', 'pad','喜欢', '足球'],中心词就是'我'
for i in range(window, len(sentence)-window):
y.append([sentence[i]])
surrounding = sentence[i-window: i]+sentence[i+1: window+i+1]+ge
           y.append(surrounding+get_negtive_sample(surrounding, nb_word, nb_negative)) x,y = np.array(x),np.array(y)
z = np.zeros((len(x), nb_negative+len(surrounding)))
z[:,:len(surrounding)]=1
return x,y,z

x 为中心词训练语料,y 是周围词+nb_negative 个负样本,因为我们是将正确的周围词放在 y 的最前面,因此在构造 z 时,把标签 1 放在每条 label 数据的前len(surrounding)位。输出 z[0],可以看到是[1,1,1,...,0]。

x,y,z = data_generator() #获取训练数据
#第一个输入是周围词
input_words = Input(shape=(1,), dtype='int32')
#建立中心词的Embedding层
input_vecs = Embedding(nb_word, word_size, name='word2vec')(input_words)
#第二个输入,背景词以及负样本词
samples = Input(shape=(nb_negative+len(surrounding),), dtype='int32')
#同样的,中心词和负样本词也有一个Emebdding层,其shape为 (?, nb_word, word_size)
weights = Embedding(nb_word, word_size, name='W')(samples)
biases = Embedding(nb_word, 1, name='b')(samples)
#将中心词向量与周围词和负样本的词向量分别进行点乘
#注意到使用了K.expand_dims,这是为了将input_vecs_sum的向量推展一维,才能和weights进行dot
input_vecs_dot_ = Lambda(lambda x: K.batch_dot(x[0], K.expand_dims(x[1],2)))([weights,input_vecs])

#然后再将input_vecs_dot_与biases进行相加,相当于 y = wx+b中的b项
add_biases = Lambda(lambda x: K.reshape(x[0]+x[1], shape=(-1, nb_negative+1)))([input_vecs_dot_,biases])

#这里苏神用了K.sigmoid
sigmoid = Lambda(lambda x: K.sigmoid(x))(add_biases)

#编译模型
model = Model(inputs=[input_words,samples], outputs=sigmoid)
#使用binary_crossentropy二分类交叉熵作损失函数
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])

model.summary()

上面代码的大概意思就是说,输入一个中心词,要从一群正确的周围词和负样本中,去使模型预测这些周围词的概率最大化,因此模型的输出是一个multi-hot向量(因为预测结果有多个正确的周围词),这里就不能再使用softmax去计算概率,而应该用sigmoid去分别计算每一个周围词的概率。这是skip-gram与CBOW模型上最显著的不同。

同时,小编这里将周围词一起预测,感觉上没有切分这些词之间的相关性,治疗了我个人的强迫症。

model.fit([x,y],z, epochs=nb_epoch, batch_size=512)

整个代码就写好了,大家可以实际测试一下训练出来的词向量的效果。个人感觉skip-gram的训练时长要大于CBOW。

果然,实践才能够检验自己是否真的掌握某项知识。

扫码下图关注我们不会让你失望!

推荐 0
本文由 ID王大伟 创作,采用 知识共享署名-相同方式共享 3.0 中国大陆许可协议 进行许可。
转载、引用前需联系作者,并署名作者且注明文章出处。
本站文章版权归原作者及原出处所有 。内容为作者个人观点, 并不代表本站赞同其观点和对其真实性负责。本站是一个个人学习交流的平台,并不用于任何商业目的,如果有任何问题,请及时联系我们,我们将根据著作权人的要求,立即更正或者删除有关内容。本站拥有对此声明的最终解释权。

0 个评论

要回复文章请先登录注册