回想之前在word2vec和GloVe中建立词典以及抽取pair的过程非常的繁琐。比如建立字典需要我们自己去实现dict这样的数据结构。在python中一切都简单的多。构建词典的代码一共只有5行。当然我们用python自带数据结构也有一些弊端,比如这些结构对于我们来说是透明的,不了解的话可能会效率比较低。下面就看一下用python怎么样五行就构建一个词典。输入是语料,输出是词典。
def read_vocab(corpus_file, thr):
vocab = Counter()//python中类似字典的数据结构,方便计数,Counter可能比dict在更新上效率更高。用单词作为字典的key,在语料中出现的次数作为value
with open(corpus_file) as f:
for line in f://对文件的每一行进行循环
vocab.update(Counter(line.strip().split()))//对每一行分词得到单词的列表,然后更新到词典
return dict([(token, count) for token, count in vocab.items() if count >= thr])//去掉低频词,返回词典
这个文件名字叫corpus2pairs,最终的目的是生成pairs,用python生成pairs也很简洁。真正生成pairs的代码并没有几行,最终生成pair文件每行是一个单词对(pair),word2vec就在上面进行训练
def main()://用docopt去处理输出的参数
args = docopt("""
Usage:
corpus2pairs.py [options] <corpus>
Options:
--thr NUM The minimal word count for being in the vocabulary [default: 100]
--win NUM Window size [default: 2]
--pos Positional contexts
--dyn Dynamic context windows
--sub NUM Subsampling threshold [default: 0]
--del Delete out-of-vocabulary and subsampled placeholders
""")
corpus_file = args['<corpus>']
thr = int(args['--thr'])
win = int(args['--win'])
pos = args['--pos']
dyn = args['--dyn']
subsample = float(args['--sub'])
sub = subsample != 0
d3l = args['--del']
vocab = read_vocab(corpus_file, thr)//首先生成词典
corpus_size = sum(vocab.values())//预料的大小是所有单词的词频之和,低频词已经被去掉了
subsample *= corpus_size
subsampler = dict([(word, 1 - sqrt(subsample / count)) for word, count in vocab.items() if count > subsample])//subsampling机制用字典解决,每个单词对应一个值,表明自己被扔掉的概率。
rnd = Random(17)
with open(corpus_file) as f:
for line in f://循环文件的每一行
tokens = [t if t in vocab else None for t in line.strip().split()]//把一行分词成单词的列表,如果有oov的情形就用None代表
if sub:
tokens = [t if t not in subsampler or rnd.random() > subsampler[t] else None for t in tokens]//对高频词做subsampling
if d3l:
tokens = [t for t in tokens if t is not None]//对应论文里面的dirty/clean context。执行这步的话会把因为各种原因变成None的单词删掉,允许窗口之外的单词也进来,变向扩大了窗口大小
len_tokens = len(tokens)
for i, tok in enumerate(tokens)://对单词列表循环,抽取pairs,每个单词和周围的单词组成pairs
if tok is not None:
if dyn://是否使用动态窗口
dynamic_window = rnd.randint(1, win)
else:
dynamic_window = win
start = i - dynamic_window
if start < 0://上下文的起始位置
start = 0
end = i + dynamic_window + 1//上下文的终点位置
if end > len_tokens:
end = len_tokens
if pos://pos是true的话我们还要考虑单词和单词之间的距离,比如eat apple_1,表示eat和距离它为1的apple组成pair,一行一个pair,所以是 '\n'.join
output = '\n'.join([row for row in [tok + ' ' + tokens[j] + '_' + str(j - i) for j in xrange(start, end) if j != i and tokens[j] is not None] if len(row) > 0]).strip()
else://抽出中心词和上下文组成的pairs
output = '\n'.join([row for row in [tok + ' ' + tokens[j] for j in xrange(start, end) if j != i and tokens[j] is not None] if len(row) > 0]).strip()//
if len(output) > 0:
print output//把这个中心词的pair写出到文件
我们再看一下pairs2counts,这个文件输入pairs,输出共现矩阵。和Glove中的共现矩阵是一样的。GloVe,PPMI以及SVD都是在共现矩阵上得到词向量。这个工具包是用脚本写的,就一行代码,就完成了GloVe中接近五百行代码的内容。这里做的是先排序sort再汇总uniq。linux自带一些算法能实现内存不够情况下的排序和汇总。实际工作中,这个效率原低于GloVe中的代码。而且由于有空格排序会有一些问题。
#!/bin/sh
sort -T . $1 | uniq -c
pairs2counts.sh最后的结果和GloVe中cooccur生成的结果差不都,都是三元组,只不过这里是文本形式,不是二进制。最后说一下counts2vocab。因为这个工具包中上下文未必是单词了所以中心词词典和上下文单词词典不一样了。现在应该有两份词典。生成中心词,上下文词典的过程用python几行就能搞定
def main():
args = docopt("""
Usage:
counts2pmi.py <counts>
""")
counts_path = args['<counts>']
words = Counter()//中心词词典
contexts = Counter()//上下文词典
with open(counts_path) as f:
for line in f:
count, word, context = line.strip().split()//每行是一个三元组,共现次数,中心词和上下文单词(或者是别的特征)
count = int(count)
words[word] += count//更新中心词词典
contexts[context] += count//更新上下文词典
words = sorted(words.items(), key=lambda (x, y): y, reverse=True)//按照词频排序,这里的count并不是真正的词频,因为共现次数的总和大概等于语料的大小乘以窗口大小
contexts = sorted(contexts.items(), key=lambda (x, y): y, reverse=True)
save_count_vocabulary(counts_path + '.words.vocab', words)//词典写出到文件,调用的是作者自己写的representations包中的东西
save_count_vocabulary(counts_path + '.contexts.vocab', contexts)