Word2Vec的使用和基础原理
Word2Vec模型背后的基本思想是对出现在上下文环境里的词进行预测。对于每一条输入文本,我们选取一个上下文窗口和一个中心词,并基于这个中心词去预测窗口里其他词出现的概率。因此,Word2Vec模型可以方便地从新增预料中学习到新增词的向量表达,是一种高效地在线学习方法。
本文主要通过代码的形式,介绍Word2Vec的使用和原理。
导入第三方模块
from gensim.models.word2vec import Word2Vec
import logging # 提供日志打印功能
import numpy as np
import random
import pandas as pd
import torch
这里安装torch费了点时间,一直报错无法安装,最后在官网输入机器配置及环境得到安装代码,然后在终端执行安装,代码如下:
pip install torch==1.5.1+cpu torchvision==0.6.1+cpu -f https://download.pytorch.org/whl/torch_stable.html
Word2Vec的主要思路
通过单词和上下文彼此预测,对应的两个算法为:
- Skip-grams(SG):预测上下文
- Continuous Bag Of Words(CBOW):预测目标单词
Word2vec模型实际上分了两个部分,第一部分是建模,第二部分是通过模型获取嵌入词向量:
- 建模过程:基于训练数据构建神经网络
- 获取嵌入词向量:模型训练好以后,获取通过训练数据所学得的参数,如隐层的权重矩阵等
Skip-grams(SG)过程
神经网络基于训练数据,将会输出一个概率分布,这些概率代表着词典中每个词作为input word的output word的可能性
模型的输出概率代表着我们词典中的每个词有多大可能性和input word同时出现
input word和out word都会进行one-hot编码,形成一个稀疏向量(实际上仅有一个位置是1)
为了节约计算资源,它会仅仅选择矩阵对应向量中维度值为1的索引行计算
Skip-grams训练
Word2Vec模型是一个超级大的神经网络(权重矩阵规模非常大)。
百万数量级的权重矩阵和亿万数量级的训练样本意味着训练灾难。
问题解决:
-
将常见的组合单词或词组作为单个’words’来处理
-
对高频词抽样来减少样本个数
-
对优化目标采用’negative sampling’方法,这样每个训练样本的训练只会更新一小部分模型权重,从而降低计算负担
3.1 负采样时,随机选择一小部分negative words来更新对应权重,同时对positive words更新权重
3.2 使用’一元模型分布’来选择’negative words’,个单词被选作negative sample的概率和它出现频次有关,频次越高越容易被选中
3.3 负采样代码中,有一个包含了一亿个元素的数组’unigram table’,数组由词汇表中每个单词的索引号填充。单次负采样的概率*1亿=单次在表中出现的次数;也就是说,进行负采样时,只需要在0-1亿范围内生成一个随机数,然后选择表中索引号为这个随机数的单次作为negative word即可;一个单词负采样概率越大,它在表中出现的次数越多,被选择的概率就越大 -
霍夫曼树:输入权值为(w1,w2…wn)的n个节点;输出对应的霍夫曼树,一般得到霍夫曼树后会对叶子节点进行霍夫曼编码,由于权重高的叶子节点靠近根节点,而权重低的叶子节点会远离根节点。 所以高权重节点编码值较短,而低权重值编码值较长,这保证了树的带权路径最短,也符合信息论:常用词拥有更短的编码
4.1.将(w1,w2…wn)看做是有n棵树的森林,每个数仅有一个节点
4.2.在森林中选择根节点权值最小的两个数合并,得到一棵新树,这两棵树分布作为新树的左右子树,新树根节点权重为左右子树根节点权重和
4.3.删除森林中权值最小的两棵树,并把合并后的新树加入森林
4.4.重复4.2与4.3,直到森林中只剩一棵树
4.5.在Word2Vec中,约定左子树编码为1,右子树编码为0,同时约定左子树的权重不小于右子树的权重 -
Hierarchical Softmax过程:为了避免计算所有词的softmax概率,Word2Vec采用了霍夫曼树代替从隐藏层到输出softmax层的映射。霍夫曼树的建立:
5.1.根据标签(label)和频率建立霍夫曼树(label出现的频率越高,Huffman树的路径越短)
5.2.Huffman树中每一叶子节点代表一个label
5.2.1. p - 从根节点出发到达w对应叶子节点的路径
5.2.2. l - 路径p中包含节点的个数
5.2.3. p1,p2,…pl - 路径p中的l个节点,其中p1表示根节点,p2表示词w对应的第二个节点
5.2.4. d2,d3,…dl∈{0,1} - 词w的Huffman编码,它有l-1位编码构成,dl表示路径p中第l个节点对应的编码(根节点无)
5.2.5. θ1,θ2,…θ(l-1)∈R - 路径p中非叶子节点对应的向量,θj表示路径p中第j个非叶子节点对应的向量
5.3.一棵Huffman树,是一个二分类树(二叉树)。再Word2Vec中,1表示负类,0表示正类,通过Sigmoid函数分类
尝试通过Word2Vec训练词向量
'''
model = Word2Vec(sentences, workers=num_workers, size=num_features)
参数详解:
sentences - 语料集,可以是一个list,对于大语料集,建议使用BrownCorpus,Text8Corpus,lineSentence构建
sg - 用于设置训练算法,默认为0,即CBOW算法;sg=1则采用skip-gram算法
size - 指定特征向量的维度,默认为100。大的size需要更多的训练数据,但是效果会更好
window - 指当前词与预测词在一个句子中的最大距离
alpha - 学习速率
seed - 随机种子
min_count - 可以对字典做截断,词频数少于min_count则被丢弃,默认为5
max_vocab_size - 设置词向量构建期间的RAM限制。
如果所有独立单词个数超过限制,则丢弃其中最不频繁的一个。每一千万个单词大约需要1GB的RAM
sample - 高频词汇的随机降采样的配置阈值,默认1乘e的-3次方,范围是0到1乘e的-5次方
workers - 参加控制训练的并行数
hs - hs=1采用Hierarchica_softmax技巧,hs=0采用negative_sampling(下采样)
iter - 迭代次数,默认5次
'''
定义输出日志参数
logging.basicConfig(level=logging.INFO, format='%(asctime)-15s %(levelname)s: %(message)s')
'''
level - 设置日志级别
format - 指定输出格式
%(asctime) - 打印日志的时间
%(levelname)s - 打印日志级别名称
%(levelno)s - 打印日志级别的数值
%(message)s - 打印日志信息
%(funcName)s - 打印日志的当前函数
%(lineno)d - 打印日志的当前行号
%(thread)d - 打印线程ID
%(process)d - 打印进程ID
'''
设置随机种子
seed = 2020
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
十折交叉验证
fold_num = 10
data_file = r'D:\Users\Felixteng\Documents\Pycharm Files\Nlp\data\train_set.csv'
定义分折函数
def all_data2fold(fold_num, num=10000):
fold_data = []
f = pd.read_csv(data_file, sep='\t', encoding='UTF-8')
# tolist()函数用于将数组或矩阵转化成列表,这里我只取10000个样本
texts = f['text'].tolist()[:num]
labels = f['label'].tolist()[:num]
# 统计有标签的样本树,理应为10000个
total = len(labels)
# 创建一个索引列表,包含10000个索引,从0到9999
index = list(range(total))
# 将索引列表随机打乱
np.random.shuffle(index)
all_texts = []
all_labels = []
# 按打乱后的索引列表顺序,重组texts和labels
for i in index:
all_texts.append(texts[i])
all_labels.append(labels[i])
label2id = {
}
# range(total) - 0到9999
# 这一步将每类标签整合到一个字典中
for i in range(total):
label = str(all_labels[i])
if label not in label2id:
label2id[label] = [i]
else:
label2id[label].append(i)
# 创建fold_num个空列表,用于存放索引
all_index = [[] for _ in range(fold_num)]
for label, data in label2id