项目实战(三) - - 实现词向量模型Word2vec

项目实战 - - 词向量Word2vec

  • 1. 词向量模型--Word2vec
  • 2. 两种网络结构
    • 2.1 CBOW
      • 2.1.1 算法任务
      • 2.1.2 算法步骤
    • 2.2 Skip-gram
      • 2.2.1 算法任务
      • 2.2.2 算法步骤
  • 3. Word2Vec特点
  • 4. 如何优化
  • 5. 代码实现与解析
  • 6. Word2Vec存在的问题
  • 7. Word2vec与Glove、LSA的比较

在自然语言处理应用中,词向量作为深度学习模型的特征进行输入。因此,最终模型的效果很大程度上取决于词向量的效果

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[""]) 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: 用了全局的信息(共现矩阵),也就是多个窗口进行更新;随着迭代次数增加,精度提升

你可能感兴趣的:(PyTorch实战,项目实战,pytorch,自然语言处理,神经网络,深度学习)