在上一节中, 介绍了FastText中的两种词向量方法, CBoW和Skip-gram. 这里咱们介绍一种相似的方法word2vec, 并使用Gensim来训练咱们的word2vec.html
来自Google的Tomas Mikolov等人于2013年在论文Distributed Representations of Words and Phrases and their Compositionality中提出了word2vec词表示方法, word2vec能够分为两种CBoW和Skip-gram模型, 但和上一节中提到的CBoW和Skip-gram有所不一样.python
能够设想, 按照上一节的思路, 咱们训练CBoW或Skip-gram模型, 最终网络输出的是每一个词几率分布(softmax的输出), 而一般而言, 咱们的字典都包含了大量的词, 这会致使大量的softmax计算, 显然, 这是很难接受的. 那么如何提升效率呢.
下面就介绍两种提升效率的两种方法网络
word2vec也使用了CBoW和Skip-gram来训练模型, 但并无采用传统的DNN结构.数据结构
最早优化使用的数据结构是用霍夫曼树来代替隐藏层和输出层的神经元,霍夫曼树的叶子节点起到输出层神经元的做用,叶子节点的个数即为词汇表的小大。 而内部节点则起到隐藏层神经元的做用。app
具体如何用霍夫曼树来进行CBOW和Skip-Gram的训练咱们在下一节讲,这里咱们先复习下霍夫曼树。dom
霍夫曼树的创建其实并不难,过程以下:post
输入:权值为(w1,w2,...wn)的n个节点优化
输出:对应的霍夫曼树ui
下面咱们用一个具体的例子来讲明霍夫曼树创建的过程,咱们有(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))$.
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
导入库
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了.
[1] paper link
[3] word2vec原理(二) 基于Hierarchical Softmax的模型