『NLU学习』(一)教机器识字
斯坦福NLP课程笔记,复现word2vec,本项目主要实现CBOW架构
Abstract(序言)
如今,深度学习中的NLP技术已经广泛应用到了我们日常的生活中。从身边的语音助手,到办公使用的机器翻译,无不是NLP技术的体现。
然而,NLP的发展历程也是相当艰辛的。从早期的手动设计语法结构,人工构造词典;到使用统计学的方法(N-gram)构造语言模型,虽提升了效果,利用马尔可夫性质降低了计算量,要将其投入工业生产仍不够理想;如今,我们提出了端到端的神经网络建模方法,用嵌入层(embedding)替代独热架构(one-hot),用Transformer替代RNN,随着技术的不断迭代,效果也越发理想。那么,让我们继往圣之绝学,开启今天的考古之旅吧_
Prior Knowledge(先验知识)
要理解NNLM(Neural Net Language Model)神经网络语言模型,首先我们要了解什么是语言模型。
所谓语言模型,就是对语言进行建模,其目的只有一个,就是判断一句话是人话的概率有多大。那么如何判断呢?一个直观的方法就是判断一句话的每个字像不像人说出来的,即累乘,用数学语言来描述,即
令 X X X为一句话, x 1 , x 2 , x 3 . . . x m x_1,x_2,x_3...x_m x1,x2,x3...xm为句子中的token,那么一句话是人话的概率即为:
P ( X ) = Π i = 1 m P ( x i ∣ x 1 . . . x i − 1 ) P(X)=\Pi_{i=1}^m P(x_i|x_1...x_{i-1}) P(X)=Πi=1mP(xi∣x1...xi−1)
那么我们的问题就转换为了如何求 P ( x i ∣ x 1 . . . x i − 1 ) P(x_i|x_1...x_{i-1}) P(xi∣x1...xi−1)
面对时序计算,我们自然而然的想到了马尔可夫性质,即未来的状态只取决于现在,与过去无关。马尔可夫性质的目的只有一个,就是在保证一定的效果下简化计算。那么,在NLP领域,我们运用马尔可夫性质,提出了N-gram,即当前token只取决于前N个token,这里的N可以是1,2,3…N取多大看你的GPU是否给力啦
在统计学习阶段,N-gram的使用毫无阻碍,但到了神经网络阶段,人们遇到了一个问题,无法求导。
开始,人们使用one-hot架构来表示token,但是问题是当N-gram的概率累乘后,最终的概率无法求导,罪魁祸首就是one-hot架构。为了解决这个问题,人们提出了连续的嵌入层embedding,从此NLP的领域一片坦途。embedding的连续性使得它可导,同时也可以很好的衡量token之间的距离,比如,给出两个词,一个是hi,一个是hello,很明显,它们的语义是接近的,那么它们的词向量(embedding中对应的那一行)应该也是接近的。
好,搞清楚了上述问题,我们回到开头。我们要建立一个神经网络语言模型,但他显然不会凭空出现,我们需要一个中间产物,词向量。那么,本项目复现的word2vec正是为了更高效的训练出一个当时效果SOTA的词向量。
CBOW
Continuous Bag-of-Words(连续词袋模型),word2vec这篇论文提出,使用一个token的上下文context来预测该token,此架构即CBOW。
将C个上下文token通过线性变换映射到隐层,再通过线性变换映射到输出层。
Skip-gram
skip-gram则与CBOW相反,它使用target目标词预测上下文context
形象化描述
CBOW使用多个token,即上下文预测target目标词,实际上可以看作一个老师教多个学生
skip-gram使用target预测上下文context,可以看作多个老师教一个学生
这里教学的手段自然是梯度的传播
word2vec的objective
Implement(实作)
更新visualdl
!pip install --upgrade visualdl
导入所需库
import paddle
import string
import random
import paddle.nn as nn
import paddle.nn.functional as F
from collections import deque
from paddle.io import Dataset,DataLoader
from reader import PairBase
from visualdl import LogWriter
定义超参
# 数据集路径
TEXT_PATH = "data/data86365/chat.txt"
# 参数保存路径
PARAMS_PATH = "work/CBOW.pdparams"
# visualdl图形化保存路径
LOG_DIR = "./tokens_domain"
# 最低词频数
MIN_COUNT = 5
# 窗口大小
WINDOW_SIZE = 3
# 批大小
BATCH_SIZE = 1024
# 嵌入层维度
EMB_DIM = 512
# 学习率
LEARNING_RATE = 1e-5
# 轮数
EPOCH_NUM = 100
实例化数据读取器
dataset = PairBase(TEXT_PATH, MIN_COUNT, WINDOW_SIZE, BATCH_SIZE)
print(paddle.to_tensor(dataset[3]).shape)
定义CBOW
class CBOW(nn.Layer):
def __init__(self, vocab_size, emb_dim, word2id):
'''
vocab_size:字典大小
emb_dim:嵌入维度
word2id:word to id字典映射
'''
super(CBOW, self).__init__()
self.vocab_size = vocab_size
self.emb_dim = emb_dim
self.word2id = word2id
self.target_embeddings = nn.Embedding(vocab_size, emb_dim, sparse=True)
self.context_embeddings = nn.Embedding(vocab_size, emb_dim, sparse=True)
self._init_emb()
def _init_emb(self):
# 小tips
init_range = 0.5 / self.emb_dim
self.context_embeddings.weight.set_value(
paddle.uniform(min=-init_range, max=init_range, shape=[self.vocab_size, self.emb_dim]))
self.target_embeddings.weight.set_value(
paddle.uniform(min=-1e-4, max=1e-4, shape=[self.vocab_size, self.emb_dim]))
def forward(self, context, target):
emb_target = self.target_embeddings(target)
emb_context = self.context_embeddings(context)
score = paddle.matmul(emb_target, emb_context, transpose_x=True)
score = F.log_sigmoid(paddle.sum(score, axis=-1))
# 取负数,梯度下降
score = -paddle.sum(score)
return score
def evaluate(self, word_a, word_b):
# 评价两个token的相似度
try:
id_a, id_b = self.word2id[word_a], self.word2id[word_b]
except:
raise "unknown token"
emb_a, emb_b = self.context_embeddings.weight[id_a, :], self.context_embeddings.weight[id_b, :]
return F.cosine_similarity(emb_a, emb_b, axis=0)
x = paddle.to_tensor(dataset[3])
u, v = x[:, 0], x[:, 1]
model = CBOW(dataset.vocab_size, EMB_DIM, dataset.word2id)
y = model(u, v)
print(y.shape)
开启训练
def train(model, dataset):
# 使用学习率衰减
schedular = paddle.optimizer.lr.CosineAnnealingDecay(learning_rate=LEARNING_RATE, T_max=5,
eta_min=LEARNING_RATE / 10)
opt = paddle.optimizer.AdamW(learning_rate=schedular, parameters=model.parameters())
for epoch in range(EPOCH_NUM):
for i, x in enumerate(dataset):
x = paddle.to_tensor(x)
context, target = x[:, 0], x[:, 1]
loss = model(context, target)
loss.backward()
opt.step()
opt.clear_grad()
if i % 1000 == 0:
print("epoch:%d,i:%d,loss:%f" % (epoch, i, loss))
if i % 10000 == 0:
paddle.save(model.state_dict(), PARAMS_PATH)
train(model, dataset)
预测相似度
state_dict = paddle.load(PARAMS_PATH)
model.load_dict(state_dict=state_dict)
print(model.evaluate("hi", "hello").numpy())
[0.9781906]
图形化展示
id_lst = list(range(1000))
token_lst = [dataset.id2word[i] for i in id_lst]
embedding_lst = [model.context_embeddings.weight[i] for i in id_lst]
with LogWriter(logdir=LOG_DIR) as writer:
writer.add_embeddings(tag="domain", mat=embedding_lst, metadata=token_lst)
效果展示
总结
本节我们学习了:
- NLP的发展历史
- 语言模型和词向量的关系
- CBOW与skip-gram的区别
- CBOW的代码实战
Reference(参考文献)
- [1]Mikolov, T., Chen, K., Corrado, G., and Dean, J., “Efficient Estimation of Word Representations in Vector Space”, arXiv e-prints, 2013.
- 知乎文章
- 知乎文章
- github
- 斯坦福cs 224
- B站斯坦福视频
关于作者
更多推荐
所有评论(0)