项目实战 - - 词向量Word2vec
在自然语言处理应用中,词向量作为深度学习模型的特征进行输入。因此,最终模型的效果很大程度上取决于词向量的效果
1. 词向量模型–Word2vec
词向量模型认为如果两个词相似,那么他们周围的词很有可能也是相似的,或者如果中心词的周围词相似那么这个中心词可能也是相似的
2. 两种网络结构
2.1 CBOW
2.1.1 算法任务
CBOW 的目标是根据上下文出现的词语来预测当前词的生成概率
具体来说,不考虑上下文的词语输入顺序,CBOW是用上下文词语的词向量的均值来预测当前词。CBOW的好处是对上下文词语的分布在词向量上进行了平滑,去掉了噪声,因此在小数据集上很有效
2.1.2 算法步骤
① 随机生成一个大小为(vocabulary_size, embedding_size)的embedding矩阵(即所有单词的词向量矩阵,每一个行对应一个单词的向量)
② 对于某一个单词(中心词),从embedding矩阵中提取其周边单词的词向量
③ 求周边单词的词向量的均值向量
④ 在该均值向量上使用logistic regression进行训练,softmax作为激活函数
⑤ 期望logistic regression得到的概率向量可以与真实的概率向量(即中心词的one-hot编码向量)相匹配。
缺点:均值处理未考虑语序
2.2 Skip-gram
2.2.1 算法任务
Skip-gram 是根据当前词来预测上下文中各词的生成概率
2.2.2 算法步骤
① 随机生成一个大小为(vocabulary_size, embedding_size)的embedding矩阵(即所有单词的词向量矩阵,每一个行对应一个单词的向量)
② 对于某一个单词,从embedding矩阵中提取单词向量
③ 在该单词向量上使用logistic regression进行训练,softmax作为激活函数
④ 期望logistic regression得到的概率向量可以与真实的概率向量(即周边词的one-hot编码向量)相匹配。
缺点:计算量依赖于语料
3. Word2Vec特点
- 无隐层
- 使用双向上下文窗口
- 上下文词序无关(CBoW)
- 输入层直接使用低维稠密表示
- 投影层简化为求和(平均)
4. 如何优化
①层次Softmax: 使用HuffmanTree来编码输出层的词典,只需要计算路径上所有非叶子节点词向量的贡献即可,计算量为树的深度
②负例采样: 由于上下文是有限的,非上下文的部分是极大量的,为了简化计算,采用负例采样
5. 代码实现与解析
- 导入相关包
import torch
// 神经网络工具箱torch.nn
import torch.nn as nn
// 神经网络函数torch.nn.functional
import torch.nn.functional as F
// PyTorch读取训练集需要用到torch.utils.data类
import torch.utils.data as tud
// 参数更新和优化函数
from torch.nn.parameter import Parameter
// Counter 计数器
from collections import Counter
// SciPy是基于NumPy开发的高级模块,它提供了许多数学算法和函数的实现
import scipy
// 余弦相似度函数
from sklearn.metrics.pairwise import cosine_similarity
// 这里较常见简写
import numpy as np,random,math,pandas as pd,sklearn
-
数据预处理
-
从文本文件中读取所有的文字,通过这些文本创建一个vocabulary
-
由于单词数量可能太大,只选取最常见的MAX_VOCAB_SIZE个单词
-
添加一个UNK单词表示所有不常见的单词
-
词编码,创建词典
-
定义Dataset与DataLoader
// 构造Dataset
class WordEmbeddingDataset(tud.Dataset): #tud.Dataset父类
def __init__(self, text, word_to_idx, idx_to_word, word_freqs, word_counts):
super(WordEmbeddingDataset, self).__init__()
// 初始化模型
self.text_encoded = [word_to_idx.get(t, word_to_idx["<unk>"]) for t in text]
self.text_encoded = torch.Tensor(self.text_encoded).long()
def __len__(self): // 数据集有多少个item
return len(self.text_encoded) // 所有单词的总数
def __getitem__(self, idx):
// 这个function返回中心词和周围单词用于训练
// 中心词索引
center_word = self.text_encoded[idx]
pos_indices = list(range(idx-C, idx)) + list(range(idx+1, idx+C+1))
// range(idx+1, idx+C+1)超出词汇总数时,需要特别处理,取余数
pos_indices = [i%len(self.text_encoded) for i in pos_indices]
pos_words = self.text_encoded[pos_indices]
// 负例采样单词索引,有放回的采样,并且self.word_freqs数值越大,取样概率越大
neg_words = torch.multinomial(self.word_freqs, r, True)
return center_word, pos_words, neg_words
// 创建Dataset实例和DataLoader
dataset = WordEmbeddingDataset(text, word_to_idx, idx_to_word, word_freqs, word_counts)
dataloader = tud.DataLoader(dataset, batch_size=BATCH_SIZE, shuffle=True, num_workers=4)
- 定义模型
// 定义模型
class EmbeddingModel(nn.Module):
def __init__(self, vocab_size, embed_size):
super(EmbeddingModel, self).__init__()
self.out_embed = nn.Embedding(vocab_size, embed_size, sparse=False)
self.in_embed = nn.Embedding(vocab_size, embed_size, sparse=False)
def forward(self, input_labels, pos_labels, neg_labels):
batch_size = input_labels.size(0)
input_embedding = self.in_embed(input_labels)
pos_embedding = self.out_embed(pos_labels) # B * (2*C) * embed_size
neg_embedding = self.out_embed(neg_labels) # B * (2*C * K) * embed_size
log_pos = torch.bmm(pos_embedding, input_embedding.unsqueeze(2)).squeeze() # B * (2*C)
log_neg = torch.bmm(neg_embedding, -input_embedding.unsqueeze(2)).squeeze() # B * (2*C*K)
#下面loss计算就是论文里的公式
log_pos = F.logsigmoid(log_pos).sum(1)
log_neg = F.logsigmoid(log_neg).sum(1) # batch_size
loss = log_pos + log_neg
return -loss
def input_embeddings(self): #取出self.in_embed数据参数
return self.in_embed.weight.data.cpu().numpy()
- 训练
optimizer = torch.optim.SGD(model.parameters(), lr=LEARNING_RATE)
for e in range(NUM_EPOCHS): // 开始迭代
for i, (input_labels, pos_labels, neg_labels) in enumerate(dataloader):
optimizer.zero_grad() // 梯度归零
loss = model(input_labels, pos_labels, neg_labels).mean()
loss.backward()
optimizer.step()
- Evaluate
Evaluate(filename, embedding_weights)
- filename中文件前两列为word1,word2,第三列为人工定义相似度;* embedding_weights为model训练的相似度;evaluate()将两者进行比较;
- return scipy.stats.spearmanr(human_similarity, model_similarity)得到模型相似度,相似度越高,说明模型越好
test – 求最相似的n个词
// 求最相似的n个词
def find_nearest(word):
index = word_to_idx[word]
embedding = embedding_weights[index]
cos_dis = np.array([scipy.spatial.distance.cosine(e, embedding) for e in embedding_weights])
return [idx_to_word[i] for i in cos_dis.argsort()[:10]]
6. Word2Vec存在的问题
(1) 对local context window 单独训练,没有利用包含在global co-currence矩阵中的统计信息
(2) 对多义词无法很好的表示和处理,因为使用了唯一的词向量(例如bank的多义,word2vec倾向于将所有概念做归一化平滑处理,得到一个最终的表现形式);负例采样的方式会缺失词的关系信息
7. Word2vec与Glove、LSA的比较
LSA(latent Semantic Analysis): 基于共现矩阵+SVD降维:计算代价大、对所有单词的统计权重一致
Word2Vec: skip-gram、CBOW每次都是用一个窗口中的信息更新出词向量;未使用类似迭代次数的epoch,用Negative Samples模拟
Glove: 用了全局的信息(共现矩阵),也就是多个窗口进行更新;随着迭代次数增加,精度提升