发表期刊:EMNLP 2014
单位:斯坦福大学
深度之眼paper班学习笔记
1.1、文章摘要:
- 当前词向量学习模型能够通过向量的算术计算捕捉词之间细微的语法和语义规律,但是这种规律背后的原理依旧不清楚【介绍当前研究现状及存在问题】
- 经过仔细的分析,我们发现了一些有助于这种词向量规律的特性,并基于词提出了一种新的对数双线性回归模型,这种模型能够利用全局矩阵分解和局部上下文的优点来学习词向量【分析如何改进】
- 我们的模型通过只在共现矩阵中的非0位置训练达到高效训练的目的【改进方法】
- 我们的模型在词对推理任务上得到75%的准确率,并且在多个任务上得到最优结果【改进效果】
创新点:
- 提出了一种新的词向量训练模型------Glove
- 在多个任务上取得最好结果
- 公布了一系列预训练的词向量:https://nlp.stanford.edu/projects/glove/
1.2、相关工作
方法一:矩阵分解的方法,在词对推理任务上表现特别差
- term-document type: 横坐标为文档doc,纵坐标为词word,通过矩阵统计各个词在各个文档中的出现次数
- term-term type: 横纵坐标都为词,矩阵统计了各个词共同出现的次数
方法二:word2vec,没有使用语料全局的统计信息
2.1、思想来源及模型推导:
核心思想:可以使用一些词来描述一个词
- 首先,等式1右边的两个概率与词 i、词 j 和词 k相关,因此抽象为与三个参数w(i,j,k)相关,得到等式1
- 由于主要是为了使用词 i 和 词 j 的差异来形容词 k,因此可以对 w_i 和 w_j 做差来抽象得到等式2
- 又因为等式右边为一个标量,而等式左边都是向量,因此想要将向量转化为标量,那么最简单的方式就是通过内积,从而得到等式3
- 令F=exp 通过化简得到论文中的公式5(即下图中右下角的公式)
- 因为log(xi)与k无关,且为常数,故可以移到等式左边作为偏置 bi 来代替
- 即推导得到了原文的等式(7),为了解决词对出现次数 Xik=0 时造成log函数无意义的现象,括号内通常再加1
- 为了满足推导得到的等式,直接使用均方差损失函数进而得到等式8的优化函数,但直接使用两者的均方差作为损失函数有一个缺点就是不论两个词在共现矩阵中出现的次数如何,它们对损失函数的影响都是相等的,很显然这是不合理的,因为出现的次数多的词对 携带的信息 应该比出现少的词对 多,因此应该给与更大的权重让模型去尽可能的提取有用的信息 / 引入权重的另一个角度是在word2vec中 是逐词计算loss的,那么出现多的词叠加得到的loss更大,而glove中是统计词频一起计算loss,因此需要引入权重函数
- 为了改进上述问题,引入了均方差前的权重函数 f(Xij) 来对不同的词对设置不同权重
2.2、代码实现
数据读取及构建词共现矩阵
步骤:
- 构建term-term的词共现矩阵
- 保留矩阵中非0的元素
- 抽取 Xij 生成训练集
from torch.utils import data
import os
import numpy as np
import pickle # pickle 可以用于保存序列化的数据,如字典等
class Wiki_Dataset(data.DataLoader):
def __init__(self, min_count, window_size):
self.min_count = min_count
self.window_size = window_size
self.datas, self.labels = self.get_co_occur(data)
def __getitem__(self, index):
return self.datas[index], self.labels[index]
def __len__(self):
return len(self.datas)
def read_data(self):
data = open(self.path + "/text8.txt").read()
data = data.split() # 使用空格切分得到一个个的词
self.word2freq = {}
for word in data:
if self.word2freq.get(word) != None: # 统计词频 已在词表中则加一
self.word2freq[word] += 1
else:
self.word2freq[word] = 1
# 构建 word2id 并去除低频词
word2id = {}
for word in self.word2freq:
if self.word2freq[word] < self.min_count:
continue
else:
if word2id.get(word) == None:
word2id[word] = len(word2id) # 当前词表长度为新词id
self.word2id = word2id
print(len(self.word2id))
return data
# 构建共现矩阵
def get_co_occur(self, data):
self.path = os.path.abspath('.') # 获取当前目录
if "data" not in self.path:
self.path += "/data"
if not os.path.exists(self.path + "/label.npy"):
print("Processing data...")
data = self.read_data()
print("Generating co-occurrences...")
vocab_size = len(self.word2id)
comat = np.zeros((vocab_size, vocab_size)) # 创建 词表大小*词表大小 的矩阵
for i in range(len(data)):
if i % 1000000 == 0:
print(i, len(data))
if self.word2id.get(data[i]) == None: # 如果当前词未出现出woed2id词表中,则为低频词,跳过
continue
w_index = self.word2id[data[i]] # 词转换为id
for j in range(max(0, i - self.window_size), min(len(data), i + self.window_size + 1)): # 在窗口大小内循环获取周围词
if self.word2id.get(data[j]) == None or i == j: # 低频词与自身跳过
continue
u_index = self.word2id[data[j]]
comat[w_index][u_index] += 1 # 共现矩阵相应位置+1
coocs = np.transpose(np.nonzero(comat)) # 获得非0元素坐标并转换为矩阵形式,则矩阵中的两个元素分别为中心词和周围词id
# 再与词向量矩阵相乘得到对应的词向量 w_i 和 w_j
labels = []
for i in range(len(coocs)):
if i % 100000 == 0:
print(i, len(coocs))
labels.append(comat[coocs[i][0]][coocs[i][1]]) # 得到词频X_ij
labels = np.array(labels)
np.save(self.path + "/data.npy", coocs)
np.save(self.path + "/label.npy", labels)
pickle.dump(self.word2id, open(self.path + "/word2id", "wb")) # 由于word2id 为字典形式,因此选择pickle方式保存
else:
coocs = np.load(self.path + "/data.npy")
labels = np.load(self.path + "/label.npy")
self.word2id = pickle.load(open(self.path + "/word2id", "rb"))
return coocs, labels
if __name__ == "__main__":
wiki_dataset = Wiki_Dataset(min_count=50, window_size=2)
print(wiki_dataset.datas.shape)
print(wiki_dataset.labels.shape)
print(wiki_dataset.labels[0:100])
获取非0元素部分代码示例
glove模型构建
# -*- coding: utf-8 -*-
import torch
import torch.nn as nn
class glove_model(nn.Module):
def __init__(self,vocab_size,embed_size,x_max,alpha):
super(glove_model, self).__init__()
self.vocab_size = vocab_size
self.embed_size = embed_size
self.x_max = x_max # 权重函数F(x)参数
self.alpha = alpha
self.w_embed = nn.Embedding(self.vocab_size,self.embed_size).type(torch.float64) # 中心词矩阵
self.w_bias = nn.Embedding(self.vocab_size,1).type(torch.float64) # 中心词bias 每个词对应一个bias
self.v_embed = nn.Embedding(self.vocab_size, self.embed_size).type(torch.float64) # 周围词矩阵
self.v_bias = nn.Embedding(self.vocab_size, 1).type(torch.float64)
def forward(self, w_data,v_data,labels):
w_data_embed = self.w_embed(w_data) # batch size * embedding size
w_data_bias = self.w_bias(w_data) # batch size * 1
v_data_embed = self.v_embed(v_data)
v_data_bias = self.v_bias(v_data)
weights = torch.pow(labels/self.x_max,self.alpha) # 生成权重,论文等式9
weights[weights>1]=1 # >x_max时置为1
loss = torch.mean(weights*torch.pow(torch.sum(w_data_embed*v_data_embed,1)+w_data_bias+v_data_bias-
torch.log(labels),2))
return loss
def save_embedding(self, word2id, file_name):
embedding_1 = self.w_embed.weight.data.cpu().numpy()
embedding_2 = self.v_embed.weight.data.cpu().numpy()
embedding = (embedding_1+embedding_2)/2
fout = open(file_name, 'w')
fout.write('%d %d\n' % (len(word2id), self.embed_size))
for w, wid in word2id.items():
e = embedding[wid]
e = ' '.join(map(lambda x: str(x), e))
fout.write('%s %s\n' % (w, e)) # 保存词向量,一个词一个向量,空格隔开
模型训练
from data import Wiki_Dataset
from model import glove_model
import torch
import numpy as np
import torch.optim as optim
from tqdm import tqdm
import config as argumentparser # 导入超参数
config = argumentparser.ArgumentParser()
if config.cuda and torch.cuda.is_available():
torch.cuda.set_device(config.gpu)
# 数据集构建
wiki_dataset = Wiki_Dataset(min_count=config.min_count,window_size=config.window_size)
training_iter = torch.utils.data.DataLoader(dataset=wiki_dataset,
batch_size=config.batch_size,
shuffle=True,
num_workers=2)
# 模型构建
model = glove_model(len(wiki_dataset.word2id),config.embed_size,config.x_max,config.alpha)
if config.cuda and torch.cuda.is_available():
torch.cuda.set_device(config.gpu)
model.cuda()
# 优化器
optimizer = optim.Adam(model.parameters(), lr=config.learning_rate)
loss= -1
# 训练
for epoch in range(config.epoch):
process_bar = tqdm(training_iter) # tqdm中传入可迭代项,此处传入的为数据集
for data, label in process_bar:
# 获得中心词id和周围词id
w_data = torch.Tensor(np.array([sample[0] for sample in data])).long() # 读取中心词id 并转换为long类型
v_data = torch.Tensor(np.array([sample[1] for sample in data])).long()
if config.cuda and torch.cuda.is_available():
w_data = w_data.cuda()
v_data = v_data.cuda()
label = label.cuda()
loss_now = model(w_data,v_data,label)
if loss == -1: # 如果loss=-1表明训练刚开始,则loss等于当前loss,否则进行平滑操作
loss = loss_now.data.item()
else:
loss = 0.95*loss+0.05*loss_now.data.item() # 平滑loss
# 对loss进行输出
process_bar.set_postfix(loss=loss)
process_bar.update()
# 反向传播
optimizer.zero_grad()
loss_now.backward()
optimizer.step()
model.save_embedding(wiki_dataset.word2id,"./embeddings/result.txt")
模型测试
from gensim.test.utils import datapath, get_tmpfile
from gensim.models import KeyedVectors
from gensim.scripts.glove2word2vec import glove2word2vec
import os
path = os.path.abspath('.')
glove_file = datapath(path+'/embeddings/result.txt') # 读取训练好的word embedding
tmp_file = get_tmpfile(path+"/embeddings/to_word2vec.txt")
glove2word2vec(glove_file, tmp_file) # 将glove转换为word2vec文件
wvmodel = KeyedVectors.load_word2vec_format(tmp_file) # 加载 word embedding
print (wvmodel.most_similar("apple"))
2.3、与其他模型的比对及复杂度分析
3、实验结果对比及分析
- 图a :随着词向量维度的增加,相应的表现也得到提升,但超过200维之后提升速度减缓
- 图b: 随着窗口尺寸的增加,网络的表现也越来越好
- 图c: 非对称上下文(只使用中心词左边的周围词训练)训练的情况下 语义任务 好于 语法任务
- 语法任务中,非对称上下文结果优于对称上下文结果,从图b可以看出随着窗口的增大,语法任务的表现提升有限,说明语法信息来源于相近的几个词==》想要提升语法表现,使用非对称上下文
- 语义任务中,随着窗口尺寸的增大,效果得到提升==》想提升语义表现,使用大的窗口尺寸
- 对于语法任务,更大的数据集能提供更好的统计结果,进而得到更好的语法信息==》从而在语法任务上表现得到提升
- 对于语义的任务,在wiki 百科数据集上的结果优于 Gigaword的大数据集,产生这一现象是由于测试方式的原因,在语义测试任务中有很多国家与城市的关系推理,而wiki百科中有很多这方面的介绍,且语料更新比较及时;而Gigaword等数据集较长时间没有更新,可能存在一些错误的信息。
- 从以上两点的分析启发我们要根据任务的需求选择合适的训练语料可以达到事半功倍的效果
- 画图技巧:如果上图的纵坐标从0开始,那么两个模型表现的差异可能不是特别明显,因此可以通过调整纵坐标的尺度来突出模型间表现的差异
4、总结与启发
- 对于方法的介绍或者idea的来源,举一个例子更容易讲清楚(本文中冰、水蒸气的例子就很好)
本文为深度之眼paper论文班的学习笔记,仅供自己学习使用,如有问题欢迎讨论!关于课程可以扫描下图二维码