『NLU学习』(二)Glove的诞生
复现Global Vector,对word2vec进行改进,通过构造共现矩阵,增加了对于语料库的统计信息的充分使用
系列项目(正在更新中哦_)
Abstract(摘要)
哈咯,大家好,俺是生而为弟。
在上一期,我们学习了Mikolov的经典,word2vec,这可谓是一石激起千层浪,打开了使用神经网络学习词表征的大门。当然了,word2vec还是有不足的。大家都知道,在深度学习兴起前,主流的是统计机器学习,对于NLP任务,统计信息(statistical information)是相当重要的,而word2vec虽然使用了神经网络,但是抛弃了语料库(corpus)的统计信息,因此就有了本次项目要讲解的一篇论文,Glove(Global Vector),看这个算法的名字就知道它弥补了word2vec忽视了统计信息的缺陷,下面让我们开始今天的学习吧
Prior Knowledge(前置知识)
首先,我们要了解一个概念,共现矩阵(co-occurrence matrix),这是对语料库中单词出现次数的统计数据。我们会划定一个上下文的窗口大小,以此来统计在一个词的上下窗口内,字典中的其余词出现的频率。当然,这里又会牵扯出另一个问题,在一个窗口中,其余词距离中心词的距离是不同的,对于语义而言,无疑是距离越近,两个词的关系越紧密,因此,在实作中,我们会将频率转为距离的倒数。那么,现在我们有了共现矩阵,设为 X X X
共现矩阵 X X X的任一条目(entry) X i j X_{ij} Xij代表单词i的上下文窗口中,单词j出现的频率。最后,设 P i j = P ( j ∣ i ) = X i j / X i P_{ij}=P(j|i)=X_{ij}/X_i Pij=P(j∣i)=Xij/Xi,代表单词 j j j出现在 i i i的上下文的概率
Glove(算法讲解)
我们从一个简单的例子开始,展示如何直接从共现概率中提取意义的某些方面。
对于一个文本集,不同词之间
w
1
w_1
w1,
w
2
w_2
w2与同一个词
w
^
\hat{w}
w^之间的共现关系满足规律:如果
w
1
w_1
w1与
w
2
w_2
w2语义相近,则
P
(
w
1
∣
w
^
)
P
(
w
2
∣
w
^
)
\frac{P(w_1|\hat{w})}{P(w_2|\hat{w})}
P(w2∣w^)P(w1∣w^)趋近1。否则,
w
1
w_1
w1与
w
^
\hat{w}
w^越相似,
P
(
w
,
w
^
)
P(w,\hat{w})
P(w,w^)越大
下面我们给出满足上述关系的一个一般公式,这里的 F F F便是类似激活函数的非线性映射
F ( w i , w j , w k ^ ) = P i k P j k F(w_i,w_j,\hat{w_k})=\frac{P_{ik}}{P_{jk}} F(wi,wj,wk^)=PjkPik
其中 w w w是词向量, w ^ \hat{w} w^是一个独立的上下文词向量
因为词向量空间本质上是线性结构,因此我们将式子转化为
F ( w i − w j , w k ^ ) = P i k P j k F(w_i-w_j,\hat{w_k})=\frac{P_{ik}}{P_{jk}} F(wi−wj,wk^)=PjkPik
然而,我们注意到,上式左端为矢量(vector),右端为标量(scalar),这显然破坏了词向量空间的线性结构,因此我们继续优化式子:
F ( ( w i − w j ) T w k ^ ) = P i k P j k F((w_i-w_j)^T\hat{w_k})=\frac{P_{ik}}{P_{jk}} F((wi−wj)Twk^)=PjkPik
下面,要保证式子的对称性,我们要让 w w w w ^ \hat{w} w^是对称的,即 w w ^ = w ^ w w\hat{w}=\hat{w}w ww^=w^w,我们进行如下的操作:
F ( ( w i − w j ) T w k ^ ) = F ( w i T w k ^ − w j T w k ^ ) = P i k P j k F((w_i-w_j)^T\hat{w_k})=F(w_i^T\hat{w_k}-w_j^T\hat{w_k})=\frac{P_{ik}}{P_{jk}} F((wi−wj)Twk^)=F(wiTwk^−wjTwk^)=PjkPik
式子里减法变为里除法,这不是对数的运算嘛,因此我们令 F F F等于 e x p exp exp,
e x p ( w i T w k ^ − w j T w k ^ ) = e x p ( w i T w k ^ ) e x p ( w j T w k ^ ) = P i k P j k exp(w_i^T\hat{w_k}-w_j^T\hat{w_k})=\frac{exp(w_i^T\hat{w_k})}{exp(w_j^T\hat{w_k})}=\frac{P_{ik}}{P_{jk}} exp(wiTwk^−wjTwk^)=exp(wjTwk^)exp(wiTwk^)=PjkPik
令分子分母相等,得到:
w i T w k ^ = l o g ( P i k ) = l o g ( X i k X i ) = l o g ( X i k ) − l o g ( x i ) w_i^T\hat{w_k}=log(P_{ik})=log(\frac{X_{ik}}{X_i})=log(X_{ik})-log(x_i) wiTwk^=log(Pik)=log(XiXik)=log(Xik)−log(xi)
由于 l o g ( x i ) log(x_i) log(xi)只与 x i x_i xi有关,我们将其改为偏执项 b i b_i bi,同时为了保留对称性,给 w k ^ \hat{w_k} wk^也加上了偏执 b k ^ \hat{b_k} bk^
由此我们得到 w i T w k ^ + b i + b k ^ = l o g ( X i k ) w_i^T\hat{w_k}+b_i+\hat{b_k}=log(X_{ik}) wiTwk^+bi+bk^=log(Xik)
最后,对式子进行变换,乘上权重,就得到了我们的损失函数:
J = ∑ i , j = 1 V f ( X i j ) ( w i T w j ^ + b i + b j ^ − l o g X i j ) 2 J=\sum^V_{i,j=1}f(X_{ij})(w_i^T\hat{w_j}+b_i+\hat{b_j}-log{X_{ij}})^2 J=∑i,j=1Vf(Xij)(wiTwj^+bi+bj^−logXij)2
权重函数图像长这样:
Implement(实作)
解压punkt包
!unzip -oq /home/aistudio/data/data101749/punkt.zip -d data/
安装第三方库
!pip install nltk
!pip install --upgrade visualdl
导入所需库
import nltk
import paddle
import numpy as np
import paddle.nn as nn
import paddle.nn.functional as F
from string import punctuation, digits
from visualdl import LogWriter
定义超参数
# 数据集位置
TEXT_PATH = "data/data86365/chat.txt"
# nltk工具包参数位置
NLTK_PATH = "data/punkt/english.pickle"
# 上下文窗口大小
CONTEXT_SIZE = 10
# 权重函数的参数
ALPHA = 0.75
# 嵌入维度
EMB_DIM = 512
# 批大小
BATCH_SIZE = 8112
# 神经网络初始化范围
INIT_SCALE = 0.01
# 权重函数的参数
X_MAX = 100
# 迭代次数
EPOCH_NUM = 100
# 学习率
LEARNING_RATE = 3e-5
# 神经网络参数保存路径
GLOVE_PATH = "./glove.pdparams"
# 可视化路径
LOG_DIR = "./log"
数据预处理
# 读取数据
with open(TEXT_PATH, "r", encoding="utf8") as f:
text = f.read().lower()
# 去除标点符号和数字
for i in punctuation + digits:
text = text.replace(i, " ")
# 导入分词器参数
tokenizer = nltk.data.load(NLTK_PATH)
treebank_tokenize = nltk.TreebankWordTokenizer().tokenize
# 分词
word_lst = treebank_tokenize(text)
print(word_lst[:20])
构造token与id的映射
# 去除重复词,构造字典
vocab = list(set(word_lst))
# 字典大小
vocab_size = len(vocab)
# 词列表的长度
word_lst_size = len(word_lst)
# 词->id的映射
word2id = {word:index for index, word in enumerate(vocab)}
构造共现矩阵
# 构造共现矩阵co-occurrence matrix
co_matrix = np.zeros((vocab_size, vocab_size))
for i in range(word_lst_size):
for j in range(1, CONTEXT_SIZE+1):
# 中心词
index = word2id[word_lst[i]]
if i-j>0:
# 上下文
left_index = word2id[word_lst[i-j]]
co_matrix[index][left_index] += 1/j
if i+j<word_lst_size:
# 上下文
right_index = word2id[word_lst[i+j]]
co_matrix[index][right_index] += 1/j
构造数据读取器
class Pairset(object):
def __init__(self,vocab):
self.vocab = vocab
# 返回一个batch的数据
def get_pairs(self):
pairs = []
count = 0
for pair in self.loop():
w1, w2 = pair[0], pair[1]
i, j = word2id[w1], word2id[w2]
if co_matrix[i][j] > 0:
pairs.append(pair)
count += 1
if (count + 1) % BATCH_SIZE == 0:
yield pairs
pairs.clear()
count = 0
# 返回词对
def loop(self):
for w1 in self.vocab:
for w2 in self.vocab:
yield (w1, w2)
dataset = Pairset(vocab)
data = next(dataset.get_pairs())
定义Glove
class PaddleGlove(nn.Layer):
def __init__(self):
super(PaddleGlove, self).__init__()
# 目标嵌入层w
self.target_embedding = nn.Embedding(vocab_size, EMB_DIM, sparse=True)
# 上下文嵌入层w~
self.context_embedding = nn.Embedding(vocab_size, EMB_DIM, sparse=True)
# 偏置
self.bias_i = nn.Embedding(vocab_size, 1, sparse=True)
self.bias_j = nn.Embedding(vocab_size, 1, sparse=True)
self._init_emb()
# 参数初始化
def _init_emb(self):
self.target_embedding.weight.set_value(paddle.uniform(min=-INIT_SCALE, max=INIT_SCALE, shape=[vocab_size,EMB_DIM]))
self.context_embedding.weight.set_value(paddle.uniform(min=-INIT_SCALE, max=INIT_SCALE, shape=[vocab_size,EMB_DIM]))
self.bias_i.weight.set_value(paddle.uniform(min=-1e-6, max=1e-6, shape=[vocab_size,1]))
self.bias_j.weight.set_value(paddle.uniform(min=-1e-6, max=1e-6, shape=[vocab_size,1]))
# 前向传播
def forward(self, w1, w2):
# 获取id
w1, w2 = [word2id[i] for i in w1], [word2id[i] for i in w2]
# 获取频率
freq = [co_matrix[i][j] for i, j in zip(w1, w2)]
# 获取权重
f_freq = list(map(self.f, freq))
# 转化为张量
w1, w2, freq, f_freq = list(map(paddle.to_tensor, [w1, w2, freq, f_freq]))
# 取出对应的词向量
bias_1, bias_2 = self.bias_i(w1), self.bias_j(w2)
emb_1, emb_2 = self.target_embedding(w1), self.context_embedding(w2)
mat = paddle.matmul(emb_1, emb_2, transpose_y=True)
mat = paddle.sum(mat, axis=-1)
# 求损失
loss = paddle.mean(f_freq * (mat + bias_1 + bias_2 - paddle.log(1 + freq)) ** 2)
return loss
# 权重函数
def f(self, freq):
if freq < X_MAX:
return (freq / X_MAX) ** ALPHA
else:
return 1.
model = PaddleGlove()
data = np.array(data)
y = model(data[:,0], data[:,1])
print(y)
开启训练
'''
开始训练时用AdamW加余弦退火,可快速收敛
最后切换为Momentum加多项式衰减进行调优
'''
schedular = paddle.optimizer.lr.CosineAnnealingDecay(learning_rate=LEARNING_RATE, T_max=2, eta_min=LEARNING_RATE/10)
opt = paddle.optimizer.AdamW(learning_rate=schedular, parameters=model.parameters())
# schedular = paddle.optimizer.lr.PolynomialDecay(learning_rate=LEARNING_RATE * 10, decay_steps=1000, end_lr=LEARNING_RATE)
# opt = paddle.optimizer.Momentum(learning_rate=schedular, parameters=model.parameters())
min_loss = float("inf")
for epoch in range(EPOCH_NUM):
avg_loss = []
for i, data in enumerate(dataset.get_pairs()):
data = np.array(data)
# 前向计算
loss = model(data[:,0], data[:,1])
# 反向传播
loss.backward()
# 调整参数
opt.step()
# 清除梯度
opt.clear_grad()
avg_loss.append(loss.numpy())
if i % 100 == 0:
print("epoch:%d,i:%d,loss:%f"%(epoch,i,loss))
print("epoch:%d,avg_loss:%f" % (epoch, sum(avg_loss) / len(avg_loss)))
if sum(avg_loss) / len(avg_loss) < min_loss:
min_loss = sum(avg_loss) / len(avg_loss)
print("epoch:%d,min_loss:%f" % (epoch, min_loss))
paddle.save(model.state_dict(), GLOVE_PATH)
预测
导入参数
state_dict = paddle.load("glove.pdparams")
model.set_dict(state_dict)
构造预测模型
class GloveEval(object):
def __init__(self, model):
self.model = model
# 词向量合并
self.embedding = self.model.target_embedding.weight + self.model.context_embedding.weight
# 计算单词相似度
def get_similarity(self, w1, w2):
emb_1 = self.embedding[word2id[w1]]
emb_2 = self.embedding[word2id[w2]]
return F.cosine_similarity(emb_1, emb_2, axis=0)
# 寻找与目标词最相似的前k个词
def get_similar_tokens(self, query_token, k):
topk, cos = self.knn(self.embedding, self.embedding[word2id[query_token]], k+1)
for i, c in zip(topk[1:], cos[1:]): # 除去输入词
print('cosine sim = %.3f: %s' % (c, (vocab[i])))
def knn(self, W, x, k):
# 添加的1e-9是为了数值稳定性
cos = paddle.matmul(W, x.reshape((-1,))) / (
(paddle.sum(W * W, axis=1) + 1e-9).sqrt() * paddle.sum(x * x).sqrt())
_, topk = paddle.topk(cos, k=k)
topk = topk.numpy()
obj = [cos[i.tolist()] for i in topk]
return topk, [cos[i.tolist()] for i in topk]
glove_eval = GloveEval(model)
glove_eval.get_similar_tokens("food", 3)
cosine sim = 0.194: dollars
cosine sim = 0.191: pumps
cosine sim = 0.181: details
词向量可视化
id_lst = list(range(1000))
token_lst = [vocab[i] for i in id_lst]
embedding_lst = [glove_eval.embedding[i] for i in id_lst]
with LogWriter(logdir=LOG_DIR) as writer:
writer.add_embeddings(tag="domain", mat=embedding_lst, metadata=token_lst)
结果展示
总结
本节我们学习了:
- word2id的不足
- 共现矩阵的意义
- Glove的损失函数的推导
参考文献
关于作者
更多推荐
所有评论(0)