介绍
在上一节中, 介绍了 FastText 中的两种词向量办法, CBoW 和 Skip-gram. 这里咱们介绍一种相似的办法 word2vec, 并应用 Gensim 来训练咱们的 word2vec.
word2vec
来自 Google 的 Tomas Mikolov 等人于 2013 年在论文 Distributed Representations of Words and Phrases and their Compositionality 中提出了 word2vec 词示意办法, word2vec 能够分为两种 CBoW 和 Skip-gram 模型, 但和上一节中提到的 CBoW 和 Skip-gram 有所不同.
能够构想, 依照上一节的思路, 咱们训练 CBoW 或 Skip-gram 模型, 最终网络输入的是每个词概率分布 (softmax 的输入), 而通常而言, 咱们的字典都蕴含了大量的词, 这会导致大量的 softmax 计算, 显然, 这是很难承受的. 那么如何提高效率呢.
上面就介绍两种提高效率的两种办法
Hierarchical Softmax
word2vec 也应用了 CBoW 和 Skip-gram 来训练模型, 但并没有采纳传统的 DNN 构造.
最先优化应用的数据结构是用霍夫曼树来代替暗藏层和输入层的神经元,霍夫曼树的叶子节点起到输入层神经元的作用,叶子节点的个数即为词汇表的小大。而外部节点则起到暗藏层神经元的作用。
具体如何用霍夫曼树来进行 CBOW 和 Skip-Gram 的训练咱们在下一节讲,这里咱们先温习下霍夫曼树。
霍夫曼树的建设其实并不难,过程如下:
输出:权值为 (w1,w2,…wn) 的 n 个节点
输入:对应的霍夫曼树
- 将 (w1,w2,…wn) 看做是有 n 棵树的森林,每个树仅有一个节点。
- 在森林中抉择根节点权值最小的两棵树进行合并,失去一个新的树,这两颗树散布作为新树的左右子树。新树的根节点权重为左右子树的根节点权重之和。
- 将之前的根节点权值最小的两棵树从森林删除,并把新树退出森林。
- 反复步骤 2 和 3 直到森林里只有一棵树为止。
上面咱们用一个具体的例子来阐明霍夫曼树建设的过程,咱们有 (a,b,c,d,e,f) 共 6 个节点,节点的权值散布是(20,4,8,6,16,3)。
首先是最小的 b 和 f 合并,失去的新树根节点权重是 7. 此时森林里 5 棵树,根节点权重别离是 20,8,6,16,7。此时根节点权重最小的 6,7 合并,失去新子树,顺次类推,最终失去上面的霍夫曼树。
个别失去霍夫曼树后咱们会对叶子节点进行霍夫曼编码,因为权重高的叶子节点越凑近根节点,而权重低的叶子节点会远离根节点,这样咱们的高权重节点编码值较短,而低权重值编码值较长。这保障的树的带权门路最短,也合乎咱们的信息论,即咱们心愿越罕用的词领有更短的编码。如何编码呢?个别对于一个霍夫曼树的节点(根节点除外),能够约定左子树编码为 0,右子树编码为 1. 如上图,则能够失去 c 的编码是 00。
假如字典蕴含 $N$ 个词, 则应用哈夫曼二叉树之前的 softmax 层的复杂度为 $O(N)$, 而应用哈夫曼二叉树后, 复杂度降为 $O(log(N))$.
Negative Sample
Hierarchical Softmax 的确能够在很大水平上进步模型的效率, 应用霍夫曼树来代替传统的神经网络,能够进步模型训练的效率。然而如果咱们的训练样本里的中心词 w 是一个很生僻的词,那么就得在霍夫曼树中辛苦的向下走很久了。能不能不用搞这么简单的一颗霍夫曼树,将模型变的更加简略呢?
nagative sampling(负采样)就是一种代替 Hierarchical softmax 的办法.
比方咱们有一个训练样本,中心词是 $w$, 它四周上下文共有 2c 个词,记为 context(w)。因为这个中心词 w, 确实和 context(w)相干存在,因而它是一个实在的正例。通过 Negative Sampling 采样,咱们失去 neg 个和 w 不同的中心词 $w_i,i=1,2,..neg$,这样 context(w)和 $w_i$ 就组成了 neg 个并不实在存在的负例。利用这一个正例和 neg 个负例,咱们进行二元逻辑回归,失去负采样对应每个词 $w_i$ 对应的模型参数 $\theta_i$,和每个词的词向量。
从下面的形容能够看出,Negative Sampling 因为没有采纳霍夫曼树,每次只是通过采样 neg 个不同的中心词做负例,就能够训练模型,因而整个过程要比 Hierarchical Softmax 简略。
具体细节能够查阅 paper
应用 gensim 训练 word2vec
导入库
import logging
import random
import numpy as np
import torch
logging.basicConfig(level=logging.INFO, format='%(asctime)-15s %(levelname)s: %(message)s')
# set seed
def seed_all(seed):
random.seed(seed)
np.random.seed(seed)
torch.cuda.manual_seed(seed)
torch.manual_seed(seed)
seed_all(0)
划分数据
# split data to 10 fold
fold_num = 10
data_file = '../data/train_set.csv'
import pandas as pd
def all_data2fold(fold_num, num=10000):
fold_data = []
f = pd.read_csv(data_file, sep='\t', encoding='UTF-8')
texts = f['text'].tolist()[:num]
labels = f['label'].tolist()[:num]
total = len(labels)
index = list(range(total))
np.random.shuffle(index)
all_texts = []
all_labels = []
for i in index:
all_texts.append(texts[i])
all_labels.append(labels[i])
label2id = {}
for i in range(total):
label = str(all_labels[i])
if label not in label2id:
label2id[label] = [i]
else:
label2id[label].append(i)
all_index = [[] for _ in range(fold_num)]
for label, data in label2id.items():
# print(label, len(data))
batch_size = int(len(data) / fold_num)
other = len(data) - batch_size * fold_num
for i in range(fold_num):
cur_batch_size = batch_size + 1 if i < other else batch_size
# print(cur_batch_size)
batch_data = [data[i * batch_size + b] for b in range(cur_batch_size)]
all_index[i].extend(batch_data)
batch_size = int(total / fold_num)
other_texts = []
other_labels = []
other_num = 0
start = 0
for fold in range(fold_num):
num = len(all_index[fold])
texts = [all_texts[i] for i in all_index[fold]]
labels = [all_labels[i] for i in all_index[fold]]
if num > batch_size:
fold_texts = texts[:batch_size]
other_texts.extend(texts[batch_size:])
fold_labels = labels[:batch_size]
other_labels.extend(labels[batch_size:])
other_num += num - batch_size
elif num < batch_size:
end = start + batch_size - num
fold_texts = texts + other_texts[start: end]
fold_labels = labels + other_labels[start: end]
start = end
else:
fold_texts = texts
fold_labels = labels
assert batch_size == len(fold_labels)
# shuffle
index = list(range(batch_size))
np.random.shuffle(index)
shuffle_fold_texts = []
shuffle_fold_labels = []
for i in index:
shuffle_fold_texts.append(fold_texts[i])
shuffle_fold_labels.append(fold_labels[i])
data = {'label': shuffle_fold_labels, 'text': shuffle_fold_texts}
fold_data.append(data)
logging.info("Fold lens %s", str([len(data['label']) for data in fold_data]))
return fold_data
fold_data = all_data2fold(10)
训练 word2vec
# build train data for word2vec
fold_id = 9
train_texts = []
for i in range(0, fold_id):
data = fold_data[i]
train_texts.extend(data['text'])
logging.info('Total %d docs.' % len(train_texts))
logging.info('Start training...')
from gensim.models.word2vec import Word2Vec
num_features = 100 # Word vector dimensionality
num_workers = 8 # Number of threads to run in parallel
train_texts = list(map(lambda x: list(x.split()), train_texts))
model = Word2Vec(train_texts, workers=num_workers, size=num_features)
model.init_sims(replace=True)
# save model
model.save("./word2vec.bin")
加载模型
# load model
model = Word2Vec.load("./word2vec.bin")
# convert format
model.wv.save_word2vec_format('./word2vec.txt', binary=False)
总结
word2vec 应用了 CBoW 和 Skip-gram 模型, 但又和它们有所不同, word2vec 提出了 2 种不同的形式来解决概率输入层简单度过高的问题.
程度无限, 无关 Hierarchical Softmax和 Negative sample 的具体实现形式以及训练细节有待进一步摸索. 只能之后有工夫再好好读一读 paper 了.
Reference
[1] paper link
[2] 应用 gensim 训练 word2vec
[3] word2vec 原理(二) 基于 Hierarchical Softmax 的模型
[4] word2vec 原理(三) 基于 Negative Sampling 的模型
[5] gensim train word2vec tutorial