PyTorch学习笔记——词向量简介

引言

本系列文章是七月在线的<PyTorch入门与实战>课程的一个笔记。
本文用的PyTorch版本是1.7.1


为什么需要词向量

为了便于计算机处理,我们需要把文档、单词向量化。
而且除了向量化之后,还希望单词的表达能计算相似词信息。

向量化单词,最早的方法是one-hot表示法,但是这种表示没有包含语义信息,并且也不知道某个单词在某篇文章中的重要性。

后来有人提出了TF-IDF方法,这种词袋模型能考虑到单词的重要性,但是语义的相似性还是捕捉不到。

在这里插入图片描述
所谓语义信息,就是代表各种青蛙的单词,向量化之后,这些向量的距离越接近越好。距离越近则表示它们的意思越近。

后来有人提出了分布式表示。假设你想知道某个单词的含义,你只要知道这个单词与哪些词语同时出现。即一个单词可以用周围的单词来表示。

在这里插入图片描述
这就是Word2Vec,原理可以点进去看看。这里补充下Skip-Gram模型的目标函数:
1 T ∑ t = 1 T ∑ − c ≤ j ≤ c , j ≠ 0 log ⁡ p ( w t + j ∣ w t ) \frac{1}{T} \sum_{t=1}^T \sum_{ -c \leq j \leq c , j \neq 0} \log p(w_{t+j}|w_t) T1t=1Tcjc,j=0logp(wt+jwt)
其中 T T T代表文本长度。 w t + j w_{t+j} wt+j w t w_t wt附近的单词。
就是给定中心词,它周围单词出现的概率越大越好。

本文的重点是学习PyTorch,用到的数据 见百度网盘: 密码:v2z5

接下来基于PyTorch实现Word2Vec:

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.utils.data as tud

from collections import Counter
import numpy as np
import random
import math

import pandas as pd
import scipy
import sklearn
from sklearn.metrics.pairwise import cosine_similarity

USE_CUDA = torch.cuda.is_available()

seed = 53113
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)

if USE_CUDA:
    torch.cuda.manual_seed(seed)
# 实现Skip-gram模型

C = 3 # 窗口大小
K = 100 # 负采样个数
NUM_EPOCHS = 2
MAX_VOCAB_SIZE = 30000
BATCH_SIZE = 128
LEARNING_RATE = 0.2
EMBEDDING_SIZE = 100

def word_tokenize(text):
    return text.split()

首先是设置好参数。
然后读取训练数据:

with open('./datasets/text8/text8.train.txt','r') as fin:
    text = fin.read()

text = text.split()
text[:200] # 看200个单词

然后构造词典和相应的映射。

# 词典
vocab = dict(Counter(text).most_common(MAX_VOCAB_SIZE-1))
vocab['<unk>'] = len(text) - np.sum(list(vocab.values())) # 整个文本的单词数,减去词典中的对应的单词数 得到未知单词数
id_2_word = [word for word in vocab.keys()]
word_2_id = {word:i for i,word in enumerate(id_2_word)}
# 得到每个单词出现的次数
word_counts = np.array([count for count in vocab.values()],dtype=np.float32)
# 计算每个单词的频率
word_freqs = word_counts / np.sum(word_counts)
word_freqs = word_freqs ** (3./4.)
word_freqs = word_freqs / np.sum(word_freqs)
VOCAB_SIZE = len(id_2_word)

PyTorch提供了Dataset结合DataLoader可以实现训练数据的加载以及本文的负采样。

继承Dataset需要提供以下两个方法的实现:

  • __len__ 返回数据集元素数量
  • __getitem__ 支持索引操作,比如dataset[i]能获得第i个元素
class WordEmbeddingDataset(tud.Dataset):
    def __init__(self, text, word_2_id, id_2_word, word_freqs, word_counts):
        '''
        text: 单词列表,训练集中所有单词
        word_2_id : 单词到id的字典
        id_2_word: id到单词的映射
        word_freqs: 每个单词的频率
        word_counts: 每个单词出现的次数
        '''
        super(WordEmbeddingDataset,self).__init__()
        # 将每个单词转换为id
        self.text_encoded = [word_2_id.get(word, word_2_id['<unk>']) for word in text]
        # 转换成Tensor
        self.text_encoded = torch.Tensor(self.text_encoded)
        # 保存word_2_id和id_2_wor
        self.word_2_id = word_2_id
        self.id_2_word = id_2_word
        # 转换成tensor并保存
        self.word_freqs = torch.Tensor(word_freqs)
        self.word_counts = torch.Tensor(word_counts)
    
    def __len__(self):
        # 数据集大小就是text_encoded的长度
        return len(self.text_encoded)
        
    def __getitem__(self,idx):
        '''
        负采样,用于训练
        返回:
        中心词
        中心词附近的positive单词
        随机采样K个单词作为negative样本
        
        '''
        # 中心词
        center_word = self.text_encoded[idx]
        # 上文下单词的索引
        pos_indices = list(range(idx - C,idx)) + list(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]
        # 负采样
        neg_words = torch.multinomial(self.word_freqs, K * pos_words.shape[0],True)
        return center_word, pos_words, neg_words # 形状依次是: [] [6] [600]
        

其中用到的torch.multinomial

在这里插入图片描述
返回一个tensor,每行包含从input相应行中定义的多项分布(概率)中抽取的num_samples个样本,返回的是索引。

下面基于Dataset来构造DataLoader

dataset = WordEmbeddingDataset(text, word_2_id, id_2_word, word_freqs, word_counts)
dataloader = tud.DataLoader(dataset,batch_size=BATCH_SIZE,shuffle=True,num_workers=0)

然后就开始定义模型了:

# 定义PyTorch模型
# 实现的是Skip-gram模型
class EmbeddingModel(nn.Module):
    def __init__(self,vocab_size,embed_size):
        super(EmbeddingModel,self).__init__()
        
        self.vocab_size = vocab_size
        self.embed_size = embed_size
        
        initrange = 0.5 / self.embed_size
        self.output_embed = nn.Embedding(self.vocab_size, self.embed_size, sparse=False)
        # 对权重进行随机初始化
        self.output_embed.weight.data.uniform_(-initrange, initrange)
        
       
        self.input_embed = nn.Embedding(self.vocab_size, self.embed_size, sparse=False)
        self.input_embed.weight.data.uniform_(-initrange, initrange)
        
        
    def forward(self,input_labels, pos_labels, neg_labels):
        '''
        input_labels: [batch_size]
        pos_labels: [batch_size,(window_size * 2)]
        neg_labels: [batch_size,(window_size * 2 * K)]
        '''
        batch_size = input_labels.size(0)
        
        input_embedding = self.input_embed(input_labels) #[batch_size,embed_size]
        pos_embedding = self.output_embed(pos_labels) #[batch_size,(window_size * 2),embed_size]
        neg_embedding = self.output_embed(neg_labels) #[batch_size,(window_size * 2 * K),embed_size]
        
        #input_embedding.unsqueeze(2) # unsqueeze在指定的位置插入1个维度,变成[batch_size, embed_size,1]
        pos_dot = torch.bmm(pos_embedding,input_embedding.unsqueeze(2)).squeeze() # [batch_size,window_size * 2 ]  squeeze() 
        neg_dot = torch.bmm(neg_embedding,-input_embedding.unsqueeze(2)).squeeze() # [batch_size,window_size * 2  * K]
        
        log_pos = F.logsigmoid(pos_dot).sum(1)
        log_neg = F.logsigmoid(neg_dot).sum(1)
        
        loss = log_pos + log_neg
        return -loss
    
    def input_embeddings(self):
        return self.input_embed.weight.data.cpu().numpy()
    

在这里插入图片描述
先来看一下nn.Embedding,说的是存储了单词的嵌入向量。实际上是根据指定的维度初始化了一个权重矩阵,本例可以理解为初始化了self.vocab_size个大小为self.embed_size的tensor,每个tensor就是一个单词的词嵌入向量。

在这里插入图片描述
torch.bmm做的是批次内的矩阵乘法。

 pos_dot = torch.bmm(pos_embedding,input_embedding.unsqueeze(2)).squeeze() # [batch_size,window_size * 2 ]  squeeze() 
 neg_dot = torch.bmm(neg_embedding,-input_embedding.unsqueeze(2)).squeeze() # [batch_size,window_size * 2  * K]
 
 log_pos = F.logsigmoid(pos_dot).sum(1)
 log_neg = F.logsigmoid(neg_dot).sum(1)
 
 loss = log_pos + log_neg

以上代码实现的是论文1中的公式 ( 4 ) (4) (4)

在这里插入图片描述
其中 v w I v_{wI} vwI是输入词向量input_embedding v w i ′ v^\prime_{wi} vwi是基于 P n ( w ) P_n(w) Pn(w)生成的负采样单词词向量neg_embedding v w O ′ v^\prime_{wO} vwO是输出词向量pos_embedding

下面定义模型:

# 定义一个模型以及把模型移动到GPU
model = EmbeddingModel(VOCAB_SIZE, EMBEDDING_SIZE)
if USE_CUDA:
    model = model.cuda()

通过PyTorch这种框架,我们只需要实现好前向传播,它能帮我进行反向传播。

# 训练模型
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):
        input_labels,pos_labels,neg_labels = input_labels.long(),pos_labels.long(),neg_labels.long()
        if USE_CUDA:
            input_labels,pos_labels,neg_labels = input_labels.cuda(),pos_labels.cuda(),neg_labels.cuda()
            
            optimizer.zero_grad()
            loss = model(input_labels,pos_labels,neg_labels).mean()
            loss.backward()
            optimizer.step()
            if i % 100 == 0:
                print('epoch ',e ,' iteration ', i , loss.item())


在我本机上要跑2个小时,基于使用GPU的情况。

下面我们测试得到的结果:

embedding_weights = model.input_embeddings()
def find_nearest(word):
    index = word_2_id[word]
    embedding = embedding_weights[index]
    cos_dis = np.array([scipy.spatial.distance.cosine(e, embedding) for e in embedding_weights])
    return [id_2_word[i] for i in cos_dis.argsort()[:10]]
    
for word in ["good", "fresh", "monster", "green", "like", "america", "chicago", "work", "computer", "language"]:
    print(word, find_nearest(word))

在这里插入图片描述
可以看到,确实学到了一些相关的语义信息。

参考


  1. Distributed Representations of Words and Phrases and their Compositionality ↩︎

评论 10 您还未登录,请先 登录 后发表或查看评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:Age of Ai 设计师:meimeiellie 返回首页

打赏作者

愤怒的可乐

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值