PyTorch讲解之 词向量简介

  本文用的PyTorch版本是1.7.1。

  为什么需要词向量

  为了便于计算机处理,我们需要把文档、单词向量化。

  而且除了向量化之后,还希望单词的表达能计算相似词信息。

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

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

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

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

  这就是Word2Vec,原理可以点进去看看。这里补充下Skip-Gram模型的目标函数:

  T1t=1∑T−c≤j≤c,j=0∑logp(wt+j∣wt)

  其中T代表文本长度。

  wt+j是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[''] = 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['']) 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做的是批次内的矩阵乘法。大连妇科医院哪家好 https://m.120ask.com/zhenshi/dlfk/

  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)。

  其中vwI是输入词向量input_embedding;

  vwi′是基于Pn(w)生成的负采样单词词向量neg_embedding;

  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))

  可以看到,确实学到了一些相关的语义信息。

  参考

  Distributed Representations of Words and Phrases and their Compositionality

猜你喜欢

转载自blog.51cto.com/14503791/2652437