【不可思议的Word2Vec】6. Keras版的Word2Vec
By 苏剑林 | 2017-08-06 | 139509位读者 |前言 #
看过我之前写的TF版的Word2Vec后,Keras群里的Yin神问我有没有Keras版的。事实上在做TF版之前,我就写过Keras版的,不过没有保留,所以重写了一遍,更高效率,代码也更好看了。纯Keras代码实现Word2Vec,原理跟《【不可思议的Word2Vec】5. Tensorflow版的Word2Vec》是一样的,现在放出来,我想,会有人需要的。(比如,自己往里边加一些额外输入,然后做更好的词向量模型?)
由于Keras同时支持tensorflow、theano、cntk等多个后端,这就等价于实现了多个框架的Word2Vec了。嗯,这样想就高大上了,哈哈~
代码 #
Github:https://github.com/bojone/tf_word2vec/blob/master/word2vec_keras.py
#! -*- coding:utf-8 -*-
#Keras版的Word2Vec,作者:苏剑林,http://kexue.fm
#Keras 2.0.6 + Tensorflow 测试通过
import numpy as np
from keras.layers import Input,Embedding,Lambda
from keras.models import Model
import keras.backend as K
word_size = 128 #词向量维度
window = 5 #窗口大小
nb_negative = 16 #随机负采样的样本数
min_count = 10 #频数少于min_count的词将会被抛弃
nb_worker = 4 #读取数据的并发数
nb_epoch = 2 #迭代次数,由于使用了adam,迭代次数1~2次效果就相当不错
subsample_t = 1e-5 #词频大于subsample_t的词语,会被降采样,这是提高速度和词向量质量的有效方案
nb_sentence_per_batch = 20
#目前是以句子为单位作为batch,多少个句子作为一个batch(这样才容易估计训练过程中的steps参数,另外注意,样本数是正比于字数的。)
import pymongo
class Sentences: #语料生成器,必须这样写才是可重复使用的
def __init__(self):
self.db = pymongo.MongoClient().weixin.text_articles
def __iter__(self):
for t in self.db.find(no_cursor_timeout=True).limit(100000):
yield t['words'] #返回分词后的结果
sentences = Sentences()
words = {} #词频表
nb_sentence = 0 #总句子数
total = 0. #总词频
for d in sentences:
nb_sentence += 1
for w in d:
if w not in words:
words[w] = 0
words[w] += 1
total += 1
if nb_sentence % 10000 == 0:
print u'已经找到%s篇文章'%nb_sentence
words = {i:j for i,j in words.items() if j >= min_count} #截断词频
id2word = {i+1:j for i,j in enumerate(words)} #id到词语的映射,0表示UNK
word2id = {j:i for i,j in id2word.items()} #词语到id的映射
nb_word = len(words)+1 #总词数(算上填充符号0)
subsamples = {i:j/total for i,j in words.items() if j/total > subsample_t}
subsamples = {i:subsample_t/j+(subsample_t/j)**0.5 for i,j in subsamples.items()} #这个降采样公式,是按照word2vec的源码来的
subsamples = {word2id[i]:j for i,j in subsamples.items() if j < 1.} #降采样表
def data_generator(): #训练数据生成器
while True:
x,y = [],[]
_ = 0
for d in sentences:
d = [0]*window + [word2id[w] for w in d if w in word2id] + [0]*window
r = np.random.random(len(d))
for i in range(window, len(d)-window):
if d[i] in subsamples and r[i] > subsamples[d[i]]: #满足降采样条件的直接跳过
continue
x.append(d[i-window:i]+d[i+1:i+1+window])
y.append([d[i]])
_ += 1
if _ == nb_sentence_per_batch:
x,y = np.array(x),np.array(y)
z = np.zeros((len(x), 1))
yield [x,y],z
x,y = [],[]
_ = 0
#CBOW输入
input_words = Input(shape=(window*2,), dtype='int32')
input_vecs = Embedding(nb_word, word_size, name='word2vec')(input_words)
input_vecs_sum = Lambda(lambda x: K.sum(x, axis=1))(input_vecs) #CBOW模型,直接将上下文词向量求和
#构造随机负样本,与目标组成抽样
target_word = Input(shape=(1,), dtype='int32')
negatives = Lambda(lambda x: K.random_uniform((K.shape(x)[0], nb_negative), 0, nb_word, 'int32'))(target_word)
samples = Lambda(lambda x: K.concatenate(x))([target_word,negatives]) #构造抽样,负样本随机抽。负样本也可能抽到正样本,但概率小。
#只在抽样内做Dense和softmax
softmax_weights = Embedding(nb_word, word_size, name='W')(samples)
softmax_biases = Embedding(nb_word, 1, name='b')(samples)
softmax = Lambda(lambda x:
K.softmax((K.batch_dot(x[0], K.expand_dims(x[1],2))+x[2])[:,:,0])
)([softmax_weights,input_vecs_sum,softmax_biases]) #用Embedding层存参数,用K后端实现矩阵乘法,以此复现Dense层的功能
#留意到,我们构造抽样时,把目标放在了第一位,也就是说,softmax的目标id总是0,这可以从data_generator中的z变量的写法可以看出
model = Model(inputs=[input_words,target_word], outputs=softmax)
model.compile(loss='sparse_categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
#请留意用的是sparse_categorical_crossentropy而不是categorical_crossentropy
model.fit_generator(data_generator(),
steps_per_epoch=nb_sentence/nb_sentence_per_batch,
epochs=nb_epoch,
workers=nb_worker,
use_multiprocessing=True
)
model.save_weights('word2vec.model')
#通过词语相似度,检查我们的词向量是不是靠谱的
embeddings = model.get_weights()[0]
normalized_embeddings = embeddings / (embeddings**2).sum(axis=1).reshape((-1,1))**0.5
def most_similar(w):
v = normalized_embeddings[word2id[w]]
sims = np.dot(normalized_embeddings, v)
sort = sims.argsort()[::-1]
sort = sort[sort > 0]
return [(id2word[i],sims[i]) for i in sort[:10]]
import pandas as pd
pd.Series(most_similar(u'科学'))
要点 #
上面是CBOW模型的代码,如果需要Skip-Gram,请自行修改,Keras代码这么简单,改起来也容易。
纵观代码,就会发现搭建模型的部分还不到10行。事实上,CBOW模型写起来是很简单的,唯一有难度的是为了提高效率而做的随机抽样版的softmax(随机选若干个目标做softmax,而不是完整softmax)。在Keras中,实现的方式就是手写Dense层,而不是用自带的Dense层。具体步骤是:1、通过random_uniform生成随机整数,也就是负样本id,然后跟目标输入拼在一起,构成一个抽样;2、通过Embedding层来存softmax的权重;3、把抽样中权重挑出来,组成一个小矩阵,然后用K后端做矩阵乘法,也就是实现抽样版本的Dense层了。反复看看代码就明白了。
最后,拼运行速度肯定拼不过Gensim版和原版的Word2Vec了,用Keras主要是灵活性强而已~这点大家需要留意哈。
转载到请包括本文地址:https://kexue.fm/archives/4515
更详细的转载事宜请参考:《科学空间FAQ》
如果您还有什么疑惑或建议,欢迎在下方评论区继续讨论。
如果您觉得本文还不错,欢迎分享/打赏本文。打赏并非要从中获得收益,而是希望知道科学空间获得了多少读者的真心关注。当然,如果你无视它,也不会影响你的阅读。再次表示欢迎和感谢!
如果您需要引用本文,请参考:
苏剑林. (Aug. 06, 2017). 《【不可思议的Word2Vec】6. Keras版的Word2Vec 》[Blog post]. Retrieved from https://kexue.fm/archives/4515
@online{kexuefm-4515,
title={【不可思议的Word2Vec】6. Keras版的Word2Vec},
author={苏剑林},
year={2017},
month={Aug},
url={\url{https://kexue.fm/archives/4515}},
}
August 6th, 2017
赞!
支持GPU,按说应该不必gensim的慢啊?
我估计瓶颈有两个,一个是读取数据的瓶颈(尤其是python自带的循环速度慢),另外一个是自动求导的瓶颈。这种深度学习的通用框架,每跑一个batch应该是会重新算一次符号导数,然后代入数据算一次数值导数。而gensim它们提前算好了导数,写死在算法中的sgd中,因此这步就直接省掉了。
好的,回头可以测试下。
你这个在keras里实现Word2Vec的工作很有意义,英文都很少有类似的工作。NLP应用还是有很多的需求,继续加油!
原始的gensim和word2vec的源码,里面提前算好了sigmod的取值表格,使用的时候相当于一个查找表,很快。
事实上写好程序后,word2Vec最大的瓶颈其实是IO,因此除了用C/C++或Cython加速基本上别无他法了。
原版的c语言版本的word2vec在效率上应该是挖过的,例如exptable变量的存在可以不用算sigmoid用查表代替
November 29th, 2018
有一个疑问,为什么对于samples要使用另外一个Embedding来处理,而不是使用和input_words相同的Embedding?
这个看你需求吧,你可以用同一个。用不同的是为了区别(a,b)和(b,a)这两种情况,word2vec、glove里边都是区分的。
就是context和target区分不同的Embedding,例子里的word2vec是context的,W是target的,对吧?
是的。还可以看一下这里的讨论区:
https://kexue.fm/archives/6191/comment-page-1#comment-10255
December 15th, 2018
x,y = np.array(x),np.array(y)
z = np.zeros((len(x), 1))
这个为什么是z = np.zeros((len(x), 1)),而不是z = np.ones((len(x), 1))。输入刚好是单词和其上下文,Z值不应该对应为1嘛?
March 21st, 2019
你好,看到你的实现非常受启发,有些疑问:
samples 是 target + negatives = 1 + 16 = 17 纬度是 17 * 128
input_vecs_sum 纬度是 1 * 128
softmax 是两者相乘,纬度是 17
在数据准备时不应该是一个 17纬的向量吗? 第一个是 target 所以向量应该是 [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
我的理解肯定有问题,请指教 谢谢
另外,我在 softmax 后面加入了一层 softmax = Dense(1)(softmax) ,把输出变成1纬的,也是可以 run 的
没看到 loss, sparse_categorical_crossentropy
改了两个地方:
z = np.zeros((len(x), 17))
z[:,0] = 1
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
这样很容易理解了, 不是特别清楚为啥是等价的???
看了一下sparse_categorical_crossentropy 的定义, 总算明白了; 第一个是 target ,index 是 0 ,其他的都是 negative 再 计算损失时不需要啦
恭喜自悟~
也是在这里卡住啦,看到这个答案基本明白了!
April 23rd, 2019
你好,看了你的代码,实现的是连续词袋CBOW,请问skip-gram如何实现呢?
在github上有搜索到skip-gram实现,https://github.com/raskr/skipgram-word2vec-keras
构建模型代码
input_pvt = Input(batch_shape=(batch_size, 1), dtype='int32')
input_ctx = Input(batch_shape=(batch_size, 1), dtype='int32')
embedded_pvt = Embedding(input_dim=vocab_size,
output_dim=vec_dim,
input_length=1)(input_pvt)
embedded_ctx = Embedding(input_dim=vocab_size,
output_dim=vec_dim,
input_length=1)(input_ctx)
merged = merge(inputs=[embedded_pvt, embedded_ctx],
mode=lambda x: (x[0] * x[1]).sum(-1),
output_shape=(batch_size, 1))
predictions = Activation('sigmoid')(merged)
# build and train the model
model = Model(input=[input_pvt, input_ctx], output=predictions)
skip-gram模型不是只有1个输入吗?输出是窗口里的其他单词,但是代码里模型有2个输入,都做了嵌入后相加合并。请问为什么会有2个输入啊?
抱歉,后面看了下,这个是连续词袋,不是skip-gram
如果看了cbow的例子还不会实现skip gram的,请花几个星期的时间好好学习python和keras。
June 14th, 2020
softmax = Lambda(lambda x:
K.softmax((K.batch_dot(x[0], K.expand_dims(x[1],2))+x[2])[:,:,0])
)([softmax_weights,input_vecs_sum,softmax_biases])
博主您好,感谢您的分享
在下有两个问题
1.关于代码中这一段 为什么还需要加上bias呢?
word2vec的原文中直接使用中心词和周围词的点积来计算logit而没有加上bias
2. 该模型有两个Embedding 即name='word2vec'和name='W'的两个
前者对应一个词语作为周围词的情况,后者则是中心词
为什么最终输出的Embedding矩阵是前者 而不是后者 或是两者的平均呢?
谢谢!
1、你想加就加,不想加就不加;
2、你想输出哪个就输出哪个。
我这里只是提供一个参考实现~
March 24th, 2021
你好 看了您的代码 想请教一下这个这个模型的精确度要怎么判断呢
请自行查阅词向量质量评测的相关工作~
April 29th, 2021
苏神您好,请教一个小问题,像这种基于keras用大量数据集训练模型时,在使用本地磁盘存储数据时,有没有一种高效的分块数据加载方法呢?因为一次性把数据集都加载进来会浪费内存,如果用open()频繁的读取一个个文件块,磁盘I/O也会比较耗时,有比open()更好的方法或其他分块加载训练数据的策略吗?感谢解答!(*^▽^*)
啥叫分块加载?想实现什么功能?
就是如果一次性把10G训练数据都加载到内存里,然后喂给模型训练,这样会浪费10G内存。如果能提前把这10G数据分成10份存储,每份1G,训练时,循环遍历加载这10个文件,这样同一时间只有1G数据在内存中,节约了宝贵的内存空间;
可是,我不太清除使用什么python包来实现这样高速读取磁盘文件,因为这样频繁读取文件,如果用普通的open()方法,磁盘I/O会有点浪费时间,所以想寻找到一种可以快速读取文本数据的python包,请问您知道这类python包或其他高效读取文本文件的方法吗?
或者您有什么其他更好的解决这种训练数据集占用较大内存空间的问题吗?Thanks♪(・ω・)ノ
我不了解。但是你确定瓶颈是IO了吗?
俺不太清楚~所以想问一下大佬一般是怎么解决这种训练集很大占用内存的问题呢?因为我看到你经常进行训练一些大型预训练模型,数据集应该有几G甚至几十G,您应该不是一次性加载到内存中是吧
我不是一次性读入内存,我就是用你认为的“磁盘I/O会有点浪费时间”的open()方法,没感觉到有什么问题。所以我的意思就是你是在使用过程中确认了会存在这个瓶颈,还是纯属主观猜测?
嗯嗯,俺只是想探索一下最前沿技术,优化无止境嘛~最后一个问题:你是将数据集分成多个磁盘文件,使用open循环遍历每个文件并一次性加载吗?还是只有一个磁盘文件,使用"rb"模式一次读取指定字节数或行数呢?
@study|comment-16282
单个文件一行行读取,没有任意指定哪行。
嗯好,谢谢指点~
April 7th, 2022
您好,请问设置subsample_t = 1e-5的意义是什么呢,降采样是怎么影响训练的呢,不理解它是在训练的哪部分起作用~
https://kexue.fm/archives/4299
September 2nd, 2022
对于我的数据,我使用词嵌入评估方法优化了词向量维度,最后得到维度为1维时词嵌入效果最优。。。请问词向量1维是可行的吗,感觉这个实验结果是失败的
“词嵌入评估方法优化了词向量维度”具体是什么意思?
数据是无标签数据,所以选择聚类的方法进行词嵌入评价(相关性强的词向量分为一类),然后通过聚类评价指标来衡量聚类的效果,若聚类效果好则认为词向量准确性高。我改变词向量的维度然后通过聚类评价指标来衡量各个不同维度下词嵌入的效果,结果发现1维时最优。
聚类用什么评价指标呢
因为是无标签数据,所以用了聚类的内部评价指标:轮廓系数、CH指标和DB指标(三个指标都是通过簇间距离和簇内距离来衡量聚类效果)
感觉这套流程有点问题。为什么用聚类的效果来评价词向量效果?
我先是找资料查评价词嵌入效果的方法有哪些,然后发现对于无标签的数据可以使用聚类的方法来评价,所以就用聚类的效果来评价词向量效果了
@瓦力|comment-19804
大体上我能理解你的逻辑,但这个评价指标应该不大靠谱,因为一个东西好不好,关键是在于它被使用时发挥了多大的作用。一般来说我们的词向量并非主要是用来聚类,所以聚类的指标没有什么参考价值。
建议参考这篇文章:https://arxiv.org/abs/1507.05523