PyTorch 实现 Skip-gram
代码实现:https://github.com/zhangxiann/Skip-gram
这是我用于学习 Skip-gram 的笔记。
文中会有一些公式,如果 github 出现公式乱码问题,请通过我的博客查看:https://zhuanlan.zhihu.com/p/275899732。
下面废话不多说,教你手把手实现 Skip-gram。
CBOW 和 Skip-gram 是两种训练得到词向量的方法。其中 CBOW 是从上下文字词推测目标字词,而 Skip-gram 则是从目标字词推测上下文的字词。在大型数据集上,CBOW 比 Skip-gram 效果好;但是在小的数据集上,Skip-gram 比 CBOW 效果好。本文使用 PyTorch 来实现 Skip-gram 模型,主要的论文是:Distributed Representations of Words and Phrases and their Compositionality
以“the quick brown fox jumped over the lazy dog”这句话为例,我们要构造一个上下文单词与目标单词的映射关系,以quick
为目标单词,假设滑动窗口大小为 1,也就是左边和右边各取 1 个单词作为上下文,这里是the
和brown
,可以构造映射关系:(the, quick),(brown, quick),这样我们就构造出两个正样本。
此外,对于这个滑动窗口外的其他单词,我们需要构造负样本,但是负样本可以是滑动窗口之外的所有单词。为了减少训练的时间,我们对负样本进行采样 k 个,称为 Negative Sampling。如 k=2,就是对每个正样本,分别构造两个负样本;例如对于(the, quick)
,采样两个负样本 (lazy , quick),(dog, quick)。Negative Sampling 的损失函数表示如下:argmaxθlogσ(vwO′⊤wI)+∑i=1kEwi∼Pn(w)[logσ(−vwi′TwI)]\underset{\theta}{\arg \max }\log \sigma\left(v_{w_{O}}^{\prime} \top_{w_{I}}\right)+\sum_{i=1}^{k} \mathbb{E}_{w_{i} \sim P_{n}(w)}\left[\log \sigma\left(-v_{w_{i}}^{\prime} T_{w_{I}}\right)\right]θargmaxlogσ(vwO′⊤wI)+∑i=1kEwi∼Pn(w)[logσ(−vwi′TwI)]。其中 σ(x)\sigma(x)σ(x) 表示 sigmoid 函数,wIw_{I}wI 表示目标单词,wow_{o}wo 表示正样本的上下文单词,wiw_{i}wi 表示负样本的上下文单词,vwO′⊤wIv_{w_{O}}^{\prime} \top_{w_{I}}vwO′⊤wI 表示正样本的两个单词向量的内积,我们希望这个内积越大越好,而 vwi′TwIv_{w_{i}}^{\prime} T_{w_{I}}vwi′TwI 并表示负样本的两个单词向量的内积,我们希望这个内积越小越好,加了负号取反后,也就是希望 −vwi′TwI-v_{w_{i}}^{\prime} T_{w_{I}}−vwi′TwI 越大越好。而 Ewi∼Pn(w)\mathbb{E}_{w_{i} \sim P_{n}(w)}Ewi∼Pn(w) 表示从负样本中采样(Negative Sampling)。由于上述损失函数是最大化,取反后变成最小化:argminθ−logσ(vwO′⊤wI)−∑i=1kEwi∼Pn(w)[logσ(−vwi′TwI)]\underset{\theta}{\arg \min }-\log \sigma\left(v_{w_{O}}^{\prime} \top_{w_{I}}\right)-\sum_{i=1}^{k} \mathbb{E}_{w_{i} \sim P_{n}(w)}\left[\log \sigma\left(-v_{w_{i}}^{\prime} T_{w_{I}}\right)\right]θargmin−logσ(vwO′⊤wI)−∑i=1kEwi∼Pn(w)[logσ(−vwi′TwI)]。
我们先导入需要的库
import collections
import os
import random
import zipfile
import numpy as np
import urllib
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.utils.data as tud
定义全局变量和参数
#参数设置
EMBEDDING_DIM = 128 #词向量维度
PRINT_EVERY = 100 #可视化频率
EPOCHES = 1000 #训练的轮数
BATCH_SIZE = 5 #每一批训练数据大小
N_SAMPLES = 3 #负样本大小
WINDOW_SIZE = 5 #周边词窗口大小
FREQ = 5 #词汇出现频数的阈值
DELETE_WORDS = False #是否删除部分高频词
VOCABULARY_SIZE = 50000
定义下载文本数据的函数。
Skip-gram 损失函数就是对数损失函数,数据来源于http://mattmahoney.net/dc/text8.zip,并核对文件大小,如果已经下载了,则跳过。
url='http://mattmahoney.net/dc/'
def maybe_download(filename, expected_bytes):if not os.path.exists(filename):filename, _ = urllib.request.urlretrieve(url+filename, filename)statinfo = os.stat(filename)if statinfo.st_size == expected_bytes:print('Found and verified', filename)else:print(statinfo.st_size)raise Exception('Failed to verify '+filename+'. Can you get to it with a browser?')return filenamefilename=maybe_download('text8.zip', 31344016)
接下来解压下载的文件,并读取里面的句子每个单词,使用str()
函数把每个单词从bytes
转换为str
。
def read_data(filename):with zipfile.ZipFile(filename) as f:# 读取出来的每个单词是 bytesdata=f.read(f.namelist()[0]).split()# 把 bytes 转换为 str#data= [str(x, encoding = "utf8") for x in data]data = list(map(lambda x: str(x, encoding = "utf8"), data))return datawords=read_data(filename)
print('Data size', len(words))
这里可以使用for
循环列表表达式来转换:data= [str(x, encoding = "utf8") for x in data]
,也可以使用list(map(lambda))
表达式来转换:data = list(map(lambda x: str(x, encoding = "utf8"), data))
。
如果不了解map
、lambda
等用法的同学,请参考python 中 lambda,map,filter 函数。
接下来创建 vocabulary 词汇表,使用collections.Counter
统计单词列表中单词的频数,然后使用most_common
方法取出前 50000 个频数最多的单词作为 vocabulary。再创建一个 dict,将单词放入 dict 中,然后把单词转换为编号表示,把 50000 词汇之外的单词设为 UNK(unkown),编号为 0,并统计 UNK 的数量。有的资料还会去掉低频词,如频次少于 5 的单词会被删除,然后再进行接下来的操作,这里省略这一步。
words=read_data(filename)
print('Data size', len(words))# 取出频数前 50000 的单词counts_dict = dict((collections.Counter(words).most_common(VOCABULARY_SIZE-1)))
# 去掉频数小于 FREQ 的单词
# trimmed_words = [word for word in words if counts_dict[word] > FREQ]# 计算 UNK 的频数 = 单词总数 - 前 50000 个单词的频数之和
counts_dict['UNK']=len(words)-np.sum(list(counts_dict.values()))
接着建立单词和数字的对应关系,因为我们最终输入到网络的不能是单词的字符串,而是单词对应的编号。
#建立词和索引的对应
idx_to_word = []
for word in counts_dict.keys():idx_to_word.append(word)
word_to_idx = {word:i for i,word in enumerate(idx_to_word)}
另一种更加简洁的写法是:
#建立词和索引的对应
idx_to_word = [word for word in counts_dict.keys()]
word_to_idx = {word:i for i,word in enumerate(idx_to_word)}
然后把单词列表转换为编号的列表
# 把单词列表转换为编号的列表
data=list()
for word in words:if word in word_to_idx:index = word_to_idx[word]else:index=word_to_idx['UNK']data.append(index)
或者直接使用列表生成式,更加简洁:
data = [word_to_idx.get(word,word_to_idx["UNK"]) for word in words]
在文本中,如the
、a
等词出现频率很高,但是对训练词向量没有太大帮助,为了平衡常见词和少见的词之间的频次,论文中以一定概率丢弃单词,计算公式如下:P(wi)=1−tf(wi)P\left(w_{i}\right)=1-\sqrt{\frac{t}{f\left(w_{i}\right)}}P(wi)=1−f(wi)t
# 计算单词频次
total_count = len(data)
word_freqs = {w: c/total_count for w, c in counts_dict.items()}
# 以一定概率去除出现频次高的词汇
if DELETE_WORDS:t = 1e-5prob_drop = {w: 1-np.sqrt(t/word_freqs[w]) for w in data}data = [w for w in data if random.random()<(1-prob_drop[w])]
else:data = data
在负采样时,按照频率来采样单词会导致某些单词次数过多,而少见的单词采样次数过少。论文将词频按照如下公式转换:P(wi)=f(wi)3/4∑j=0nf(wj)3/4P\left(w_{i}\right)=\frac{f\left(w_{i}\right)^{3 / 4}}{\sum_{j=0}^{n} f\left(w_{j}\right)^{3 / 4}}P(wi)=∑j=0nf(wj)3/4f(wi)3/4。按照转换后的词频采样单词,使得最常见的词采样次数减少了,而最少见的词采样次数增加了。下面的代码是计算转换后的词频。
#计算词频,按照原论文转换为3/4次方
word_counts = np.array([count for count in counts_dict.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)
下面创建 Dataset,把数据和转换后的词频作为参数传入。在__getitem__()
中,idx
是当前的目标单词,根据滑动窗口的大小WINDOW_SIZE
,取前WINDOW_SIZE
个词和后WINDOW_SIZE
个词作为上下文单词。对于每个正样本,采样N_SAMPLES
个负样本,所以总共采样N_SAMPLES * pos_words.shape[0]
个负样本。这里根据word_freqs
词频,使用torch.multinomial()
来采样。由于word_freqs
的顺序和idx_to_word
的顺序是一样的,因此负采样得到索引就是对应单词的编号。
# DataLoader自动帮忙生成batch
class WordEmbeddingDataset(tud.Dataset):def __init__(self, data, word_freqs):super(WordEmbeddingDataset, self).__init__()self.data = torch.Tensor(data).long() # 解码为词表中的索引self.word_freqs = torch.Tensor(word_freqs) # 词频率def __len__(self):# 共有多少个itemreturn len(self.data)def __getitem__(self, idx):# 根据idx返回center_word = self.data[idx] # 找到中心词pos_indices = list(range(idx - WINDOW_SIZE, idx)) + list(range(idx + 1, idx + WINDOW_SIZE + 1)) # 中心词前后各C个词作为正样本pos_indices = list(filter(lambda i: i >= 0 and i < len(self.data), pos_indices)) # 过滤,如果索引超出范围,则丢弃pos_words = self.data[pos_indices] # 周围单词# 根据 变换后的词频选择 K * 2 * C 个负样本,True 表示可重复采样neg_words = torch.multinomial(self.word_freqs, N_SAMPLES * pos_words.shape[0], True)return center_word, pos_words, neg_words
这里 WINDOW_SIZE=5WINDOW\_SIZE=5WINDOW_SIZE=5,NSAMPLES=3N_SAMPLES=3NSAMPLES=3,如果正样本的索引没有超过范围,那么会采样 10 个正样本,30 个负样本。
torch.multinomial()
定义如下:
torch.multinomial(input, num_samples,replacement=False, out=None)
主要参数:
- input:权重或者采样权重矩阵,元素权重越大,采样概率越大
- num_samples:采样次数
- replacement:是否可以重复采样
input 可以看成一个采样概率矩阵,每一个元素代表其在该行中的采样概率。对 input 的每一行做 n_samples 次取值,输出的张量是 n_samples 次采样得到的元素的索引(下标)。
例子:
import torch
weights = torch.Tensor([[0,10,3,0],[10,3,0,0],[3,0,0,10]])
result = torch.multinomial(weights, 10,replacement=True)
print(result)
输出如下:
tensor([[1, 1, 1, 2, 2, 1, 1, 1, 1, 1],[1, 0, 0, 0, 0, 0, 0, 0, 0, 0],[3, 3, 3, 3, 0, 3, 3, 3, 0, 3]])
下面定义网络,定义两个Embedding
层,一个用于映射目标单词,另一个用于映射上下文单词和负样本单词。Embedding
层的输入维度是 vocabulary_size,输出维度是词向量的长度。input_embedding
是经过Embedding
层得到的目标单词向量,形状是[batch_size,embed_size]
,pos_embedding
是经过Embedding
层得到的上下文单词向量,维度是[batch_size, K,embed_size]
,其中 KKK 表示上下文单词数量,根据损失函数的定义,要计算目标单词向量和每个上下文单词向量,因此首先把input_embedding
的形状扩充为[batch_size, embed_size, 1]
。对于pos_embedding
中的每个词向量,分别和input_embedding
做内积。可以使用torch.bmm()
方法来实现一个 batch 的向量的相乘。
torch.bmm()
方法定义如下:
torch.bmm(input, mat2, deterministic=False, out=None)
参数:
- input:形状是
[batch_size, n,m]
- mat2:形状是
[batch_size, m,p]
其中input
和mat2
的batch_size
要相等,对于其中每个元素执行矩阵乘法,matrix[n,m]×matrix[m,p]=matrix[n,p]matrix_{[n,m]} \times matrix_{[m,p]}=matrix_{[n,p]}matrix[n,m]×matrix[m,p]=matrix[n,p]。
outi=inputi×mat2iout_{i}= input _{i} \times {mat} 2_{i}outi=inputi×mat2i
最终得到的输出的形状是[batch_size, n,p]
。
在这里,input_embedding
的形状扩充为[batch_size, embed_size, 1]
,pos_embedding
的形状是[batch_size, K,embed_size]
,torch.bmm(pos_embedding, input_embedding)
的结果维度是[batch_size, K, 1]
。可以使用unsqueeze()
或者view()
方法来扩充维度。
input_embedding
与neg_embedding
的计算也是同理。代码如下:
# 构造一个神经网络,输入词语,输出词向量
class EmbeddingModel(nn.Module):def __init__(self, vocab_size, embed_size):super(EmbeddingModel, self).__init__()self.vocab_size = vocab_sizeself.embed_size = embed_sizeinitrange = 0.5 / self.embed_sizeself.out_embed = nn.Embedding(self.vocab_size, self.embed_size, sparse=False)# 模型输出nn.Embedding(30000, 100)self.out_embed.weight.data.uniform_(-initrange, initrange) # 权重初始化的一种方法self.in_embed = nn.Embedding(self.vocab_size, self.embed_size, sparse=False)# 模型输入nn.Embedding(30000, 100)self.in_embed.weight.data.uniform_(-initrange, initrange)# 权重初始化的一种方法def forward(self, input_labels, pos_labels, neg_labels):# input_labels:[batch_size]# pos_labels:[batch_size, windows_size*2]# neg_labels:[batch_size, windows_size * N_SAMPLES]input_embedding = self.in_embed(input_labels) # [batch_size, embed_size]pos_embedding = self.out_embed(pos_labels) # [batch_size, windows_size * 2, embed_size]neg_embedding = self.out_embed(neg_labels) # [batch_size, (windows_size * 2 * K), embed_size]# 向量乘法input_embedding = input_embedding.unsqueeze(2) # [batch_size, embed_size, 1],新增一个维度用于向量乘法# input_embedding = input_embedding.view(BATCH_SIZE, EMBEDDING_DIM, 1)pos_dot = torch.bmm(pos_embedding, input_embedding).squeeze(2) # [batch_size, windows_size * 2] 只保留前两维neg_dot = torch.bmm(neg_embedding.neg(), input_embedding).squeeze(2) # [batch_size, windows_size * 2 * K] 只保留前两维log_pos = F.logsigmoid(pos_dot).sum(1) # 按照公式计算log_neg = F.logsigmoid(neg_dot).sum(1)loss = -(log_pos + log_neg) # [batch_size]return lossdef input_embeddings(self):##取出self.in_embed数据参数return self.in_embed.weight.data.cpu().numpy()
定义模型和优化器,开始迭代训练:
# 构造 dataset 和 dataloader
dataset = WordEmbeddingDataset(data, word_freqs)
dataloader = tud.DataLoader(dataset, batch_size=BATCH_SIZE, shuffle=True)# 定义一个模型
model = EmbeddingModel(VOCABULARY_SIZE, EMBEDDING_DIM)# 定义优化器
optimizer = torch.optim.SGD(model.parameters(), lr=0.001)for epoch in range(EPOCHES):for i, (input_labels, pos_labels, neg_labels) in enumerate(dataloader):input_labels = input_labels.long() # 全部转为LongTensorpos_labels = pos_labels.long()neg_labels = neg_labels.long()optimizer.zero_grad() # 梯度归零loss = model(input_labels, pos_labels, neg_labels).mean()loss.backward()optimizer.step()if i % 100 == 0:print("epoch", epoch, "loss", loss.item())embedding_weights = model.input_embeddings()np.save("embedding-{}".format(EMBEDDING_DIM), embedding_weights)torch.save(model.state_dict(), "embedding-{}.th".format(EMBEDDING_DIM))
有一个问题是:如果在 Dataset 的__getitem__()
函数中,由于pos_indices = list(filter(lambda i: i>=0 and i< len(self.data), pos_indices))
可能会造成数据的size < WINDOW_SIZE*2
,这时可能会导致 DataLoader 无法将一个 Batch 的数据堆叠起来,会报错。有 3 种处理方法
- 可以改为
pos_indices = [i % len(self.data) for i in pos_indices]
,对下标取余,以防超过文档范围 - 使用自定义
collate_fn
函数来处理这种情况。 - 或者不使用 PyTorch 的
Dataset
,而是手动生成每个 Batch 的数据。有兴趣的读者可以参考用 Pytorch 实现 skipgram。
代码:https://github.com/xiechuanyu/Skip-gram
参考
- Skip-Gram 负采样的 Pytorch 实现
- 用 Pytorch 实现 skipgram
- Tensorflow 实战(黄文坚): Tensorflow 实现 Word2Vec
- python 中 lambda,map,filter 函数
如果你觉得这篇文章对你有帮助,不妨点个赞,让我有更多动力写出好文章。
我的文章会首发在公众号上,欢迎扫码关注我的公众号张贤同学。
PyTorch 实现 Skip-gram相关推荐
- 《自然语言处理学习之路》02 词向量模型Word2Vec,CBOW,Skip Gram
本文主要是学习参考莫烦老师的教学,对老师课程的学习,记忆笔记. 原文链接 文章目录 书山有路勤为径,学海无涯苦作舟. 零.吃水不忘挖井人 一.计算机如何实现对于词语的理解 1.1 万物数字化 1.2 ...
- Rasa课程、Rasa培训、Rasa面试、Rasa实战系列之Understanding Word Embeddings CBOW and Skip Gram
Rasa课程.Rasa培训.Rasa面试.Rasa实战系列之Understanding Word Embeddings CBOW and Skip Gram 字嵌入 从第i个字符,第i+1个字符预测第 ...
- pytorch笔记: 搭建Skip—gram
skip-gram 理论部分见:NLP 笔记:Skip-gram_刘文巾的博客-CSDN博客 1 导入库 import numpy as np import torch from torch impo ...
- skip gram和cbow的优缺点
在cbow方法中,是用周围词预测中心词,从而利用中心词的预测结果情况,使用GradientDesent方法,不断的去调整周围词的向量.当训练完成之后,每个词都会作为中心词,把周围词的词向量进行了调整, ...
- 小白看Word2Vec的正确打开姿势|全部理解和应用
有个用心的读者最近做了一篇论文,想知道Word2Vec的相关理论和应用方法,作为一个有强迫症的我,去翻查了大量的文献资料,决定从Word2Vec的上下文来温习一下这个NLP的基础以及做相关的知识体系的 ...
- python word2vec怎么用_小白看Word2Vec的正确打开姿势|全部理解和应用
有个用心的读者最近做了一篇论文,想知道Word2Vec的相关理论和应用方法,作为一个有强迫症的我,去翻查了大量的文献资料,决定从Word2Vec的上下文来温习一下这个NLP的基础以及做相关的知识体系的 ...
- 词嵌入(word embedding)(pytorch)
文章目录 词嵌入 代码 Skip-Gram 模型 如何取词 建模 模型细节 隐层,我们需要的结果 输出 N Gram模型 代码 词嵌入 参考:<深度学习入门之Pytorch> 词嵌入到底是 ...
- 【Pytorch基础教程34】EGES召回模型
note 文章目录 note 一.EGES图算法 1.0 回顾GNN 1.1 基本定义和数据预处理 1.2 GES: GNN with side info 1.3 EGES: enhanced版本 二 ...
- 机器学习竞赛必备基础知识_Word2Vec
1 简介 本文我们主要介绍词嵌入中一种非常经典的算法,Word2Vec,早期Word2Vec主要被用在文本类的问题中,但是现在做比赛的朋友应该都发现了,几乎一半的传统数据竞赛都会用到Word2Vec ...
- 【NLP】到目前为止,机器学习与自然语言处理相遇的那些事
导读 在过去的十年中,自然语言处理(NLP)有了长足的发展.本文将尝试指出和总结直到现在的NLP领域中最关键和最重要的突破,同时还将重点放在仍然存在和新出现的挑战上. NLP应用程序的记录甚至可以在2 ...
最新文章
- 解决MySQL命令行无法连接问题错误ERROR1045(28000)
- 表格合并行_Word制作验收单表格,很简单,快来学习吧
- leetcode算法题--目标和
- linux删除文件退出,在Linux中用于在移动或删除文件时使`tail -f`退出的bash脚本
- ORACLE RAC 重新安装时清空ASM 磁盘命令
- VB与C#代码转换在线工具,亲测好用
- C++:50---虚析构函数
- 明显调用的表达式前的括号必须具有指针函数类型_每天三分钟带你搞懂C++基础Day5 处理类型 typedef、auto、decltype...
- 重建索引一般需要多久_游泳小白学游泳,一般需要多久才能学会?猜猜看
- 浅析MySQL二进制日志
- DB2中不同于其它数据库的操作
- 【兼容】----常见浏览器兼容性问题与解决方案
- c++ 成员初始化列表
- 大革命修改后无法连接服务器,刺客信条大革命常见问题解决方法 中文设置教学...
- 仿最新BiliBili客户端(已开源)
- JVM: java虚拟机
- outlook不断弹出“对以下服务器输入您的用户名和密码”
- hp vse虚拟服务器,HP9000用户升级硬件平台的捷径
- 对 Error-State Kalman Filter 的理解
- html顺时针复制原则,图纸的叠法