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(xix1...xi1)
那么我们的问题就转换为了如何求 P ( x i ∣ x 1 . . . x i − 1 ) P(x_i|x_1...x_{i-1}) P(xix1...xi1)

面对时序计算,我们自然而然的想到了马尔可夫性质,即未来的状态只取决于现在,与过去无关。马尔可夫性质的目的只有一个,就是在保证一定的效果下简化计算。那么,在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)

效果展示

总结

本节我们学习了:

  1. NLP的发展历史
  2. 语言模型和词向量的关系
  3. CBOW与skip-gram的区别
  4. CBOW的代码实战

Reference(参考文献)

关于作者

我在AI Studio上获得钻石等级,点亮8个徽章,来互关呀~

Logo

学大模型,用大模型上飞桨星河社区!每天8点V100G算力免费领!免费领取ERNIE 4.0 100w Token >>>

更多推荐