NLP模型之-skip-gram模型

公众号:尤而小屋

作者:Peter

编辑:Peter

大家好,我是Peter~

本文首先介绍如何从概率角度看待CBOW模型,再从CBOW模型延伸到skip-gram模型。

1 从概率角度看CBOW模型

概率的表示方法:

  • P(A):事件A发生的概率
  • P(A,B):事件A和B同时发生的概率

后验概率P(A|B):表示事件发生后的概率,也就是在事件B发生的基础上事件A发生的概率。

CBOW模型的处理:当给定某个上下文时,输出目标单词的概率

假设使用使用包含单词 <math xmlns="http://www.w3.org/1998/Math/MathML"> w 1 , w 2 , . . . , w T w_1,w_2,...,w_{T} </math>w1,w2,...,wT的语料库,对于第t个单词,考虑窗口大小为1的上下文

也就是当给定上下文 <math xmlns="http://www.w3.org/1998/Math/MathML"> w t − 1 w_{t-1} </math>wt−1和 <math xmlns="http://www.w3.org/1998/Math/MathML"> w t + 1 w_{t+1} </math>wt+1时,目标词为 <math xmlns="http://www.w3.org/1998/Math/MathML"> w t w_t </math>wt的概率,使用后验概率公式表示为:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> P ( w t ∣ w t − 1 , w t + 1 ) P(w_t|w_{t-1},w_{t+1}) </math>P(wt∣wt−1,wt+1)

CBOW模型可以表示为上式:当给定 <math xmlns="http://www.w3.org/1998/Math/MathML"> w t − 1 w_{t-1} </math>wt−1和 <math xmlns="http://www.w3.org/1998/Math/MathML"> w t + 1 w_{t+1} </math>wt+1时, <math xmlns="http://www.w3.org/1998/Math/MathML"> w t w_t </math>wt发生的概率。

如何表示CBOW模型的损失函数:借用交叉熵损失函数
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> L = − ∑ k t k l o g y k L=-\sum _{k} t_klogy_k </math>L=−k∑tklogyk

其中, <math xmlns="http://www.w3.org/1998/Math/MathML"> y k y_k </math>yk表示第 <math xmlns="http://www.w3.org/1998/Math/MathML"> k k </math>k个事件发生的概率, <math xmlns="http://www.w3.org/1998/Math/MathML"> t k t_k </math>tk是监督目标,它是one-hot向量的元素。

当 <math xmlns="http://www.w3.org/1998/Math/MathML"> w t w_t </math>wt发生时,它对应的one-hot向量的元素是1,其余是0
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> L = − l o g P ( w t ∣ w t − 1 , w t + 1 ) L=-logP(w_t|w_{t-1},w_{t+1}) </math>L=−logP(wt∣wt−1,wt+1)

CBOW模型的损失函数 对应公式 <math xmlns="http://www.w3.org/1998/Math/MathML"> P ( w t ∣ w t − 1 , w t + 1 ) P(w_t|w_{t-1},w_{t+1}) </math>P(wt∣wt−1,wt+1)取log,再加上负号,称之为负对数似然(negative log likelihood)。

CBOW模型对应的整体数据的损失函数如下,模型学习的任务就是让损失尽可能地小:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> L = − 1 T ∑ t = 1 T l o g P ( w t ∣ w t − 1 , w w + 1 ) L=-\frac{1}{T}\sum_{t=1}^T logP(w_t|w_{t-1},w_{w+1}) </math>L=−T1t=1∑TlogP(wt∣wt−1,ww+1)

2 skip-gram模型

word2vec有2个模型:

  • CBOW模型:根据上下文预测目标词
  • skip-gram模型:根据目标词预测上下文

可以看到两个模型的功能是相反的(3-23)

2.1 模型架构

skip-gram模型的网络架构如图(3-24)

2.2模型损失

  • skip-gram模型的输入层只有一个;
  • 输出层的数量则与上下文的单词个数相等;

在计算损失时,先分别计算各个输出层的损失,再将它们加起来作为最后的损失。

使用概率的方法表示skip-gram模型:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> P ( w t − 1 , w t + 1 ∣ w t ) P(w_{t-1},w_{t+1}|w_t) </math>P(wt−1,wt+1∣wt)

即:在给定 <math xmlns="http://www.w3.org/1998/Math/MathML"> w t w_t </math>wt的条件下,预测上下文 <math xmlns="http://www.w3.org/1998/Math/MathML"> w t − 1 w_{t-1} </math>wt−1和 <math xmlns="http://www.w3.org/1998/Math/MathML"> w t + 1 w_{t+1} </math>wt+1同时发生的概率。

在这里我们假设上下文的单词之间没有相关性(条件独立),上式可以分解为:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> P ( w t − 1 , w t + 1 ∣ w t ) = P ( w t − 1 ∣ w t ) ⋅ P ( w t + 1 ∣ w t ) P(w_{t-1},w_{t+1}|w_t) = P(w_{t-1}|w_t) \cdot P(w_{t+1}|w_t) </math>P(wt−1,wt+1∣wt)=P(wt−1∣wt)⋅P(wt+1∣wt)

skip-gram模型的损失可以表示为:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> L = − l o g P ( w t − 1 , w t + 1 ∣ w t ) L=-log P(w_{t-1},w_{t+1}|w_t) </math>L=−logP(wt−1,wt+1∣wt)
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> L = − l o g P ( w t − 1 ∣ w t ) ⋅ P ( w t + 1 ∣ w t ) L=-log P(w_{t-1}|w_t) \cdot P(w_{t+1}|w_t) </math>L=−logP(wt−1∣wt)⋅P(wt+1∣wt)
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> L = − ( l o g P ( w t − 1 ∣ w t ) + l o g P ( w t + 1 ∣ w t ) ) L=-(logP(w_{t-1}|w_t) + logP(w_{t+1}|w_t)) </math>L=−(logP(wt−1∣wt)+logP(wt+1∣wt))

扩展到整个预料库中,skip-gram模型的损失函数表示为:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> L = − 1 T ∑ t = 1 T ( l o g P ( w t − 1 ∣ w t ) + l o g P ( w t + 1 ∣ w t ) ) L=-\frac{1}{T} \sum_{t=1}^{T} (logP(w_{t-1}|w_t) + logP(w_{t+1}|w_t)) </math>L=−T1t=1∑T(logP(wt−1∣wt)+logP(wt+1∣wt))

对比CBOW模型和skip-gram模型的损失函数:

  • skip-gram模型的损失函数需要求各个上下文单词对应的损失总和
  • CBOW模型只需要求目标单词的损失

二者应该选择哪个模型?答案:skip-gram模型

  1. skip-gram模型的结果更好
  2. 在低频词和类推问题中,skip-gram模型表现更好
  3. skip-gram模型的速度稍慢,计算成本大

skip-gram模型是根据一个单词预测其周围的单词,答案可能存在很多候选项,是一个更难的问题。经过更难问题的锻炼,skip-gram模型能提供更好的单词的分布式表示。

3 模型实现

类比CBOW模型,实现skip-gram模型:

3.1 MatMul层

In [1]:

python 复制代码
import numpy as np

In [2]:

python 复制代码
class MatMul:
    def __init__(self, W):
        self.params = [W]  # 保存学习的参数,此时只有权重W
        self.grads = [np.zeros_like(W)]  # 梯度保存在grads
        self.x = None
    
    # 前向传播
    def forward(self, x):
        W, = self.params    # 参数
        out = np.dot(x,W)   # 输出
        self.x = x
        return out
    
    # 后向传播
    def backward(self, dout):
        W, = self.params
        dx = np.dot(dout, W.T)
        dW = np.dot(self.x.T, dout)
        # grads[0][...] 使用了省略号:可以固定Numpy数组的内存地址,覆盖Numpy数组的元素
        # grads[0]=dW 浅复制   grads[0][...] = dW 深复制
        self.grads[0][...] = dW  # 实例变量grads中设置权重的梯度;grads列表中每个元素是Numpy数组
        return dx

3.2 文本预处理

In [3]:

python 复制代码
def preprocess(text):
    text = text.lower()
    text = text.replace(".", " .")
    words = text.split(" ")  # 基于空格的切割
    
    word_to_id = {}
    id_to_word = {}
    
    for word in words:
        if word not in word_to_id:
            new_id = len(word_to_id)  # 长度加1作为新ID
            word_to_id[word] = new_id
            id_to_word[new_id] = word
            
    # 单词列表转成单词ID列表
    corpus = np.array([word_to_id[w] for w in words])
    
    return corpus, word_to_id, id_to_word  # 返回语料库,单词ID-字典映射,ID单词-字典映射

3.3 生成目标和上下文

In [4]:

python 复制代码
def create_contexts_target(corpus, window_size=1):
    """
    corpus: 输入的文本数据或文本序列
    window_size: 上下文窗口的大小,默认值为1
    """
    target = corpus[window_size: -window_size]  # 从corpus提取目标序列中心词的上下文  window_size确定目标词周围的词数(不包括目标词本身)
    
    contexts = []  # 上下文
    
    for idx in range(window_size, len(corpus) - window_size):   # 外部循环遍历corpus中的每个词,但跳过窗口大小范围内的词(即不含目标词本身)
        cs = []  # 用于存储当前词的上下文
        for t in range(-window_size, window_size+1): # 使用内部循环遍历上下文窗口范围内的所有词(包括中心词)
            if t == 0:  # 跳过单词本身
                continue
            cs.append(corpus[idx + t])
        contexts.append(cs)
        
    return np.array(contexts), np.array(target)

3.4 单词ID转one-hot表示

In [5]:

python 复制代码
def convert_one_hot(corpus, vocab_size):
    """
    功能:上下文和目标单词的ID列表转化为one-hot表示
    corpus:单词ID列表
    vocab_size:词汇个数
    返回最终的二维或者三维Numpy数组
    """
    N = corpus.shape[0]  # 获取行数
    
    if corpus.ndim == 1:  # 如果是一维数组
        one_hot = np.zeros((N, vocab_size), dtype=np.int32)  # 初始化
        for idx, word_id in enumerate(corpus):  # 遍历单词列表
            one_hot[idx,word_id] = 1
                  
    elif corpus.ndim == 2:
        C = corpus.shape[1]
        one_hot = np.zeros((N,C,vocab_size), dtype=np.int32)
        
        for idx_0, word_ids in enumerate(corpus):
            for idx_1, word_id in enumerate(word_ids):
                one_hot[idx_0, idx_1, word_id] = 1
                
    return one_hot

3.5 交叉熵损失

In [6]:

python 复制代码
def cross_entropy_error(y, t):
    """
    交叉熵损失的实现
    """
    if y.ndim == 1:
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)
        
    # 在监督标签为one-hot-vector的情况下,转换为正确解标签的索引
    if t.size == y.size:
        t = t.argmax(axis=1)
             
    batch_size = y.shape[0]

    return -np.sum(np.log(y[np.arange(batch_size), t] + 1e-7)) / batch_size

3.6 SoftWithLoss类

In [7]:

python 复制代码
def softmax(x):
    """
    softmax函数的实现
    """
    if x.ndim == 2:
        x = x - x.max(axis=1, keepdims=True)
        x = np.exp(x)
        x /= x.sum(axis=1, keepdims=True)
    elif x.ndim == 1:
        x = x - np.max(x)
        x = np.exp(x) / np.sum(np.exp(x))

    return x


class SoftmaxWithLoss:
    def __init__(self):
        self.params, self.grads = [], []
        self.y = None  # softmax的输出
        self.t = None  # 监督标签
        
    def forward(self,x,t):
        self.t = t
        self.y = softmax(x)  # 用到前面定义的softmax函数
        
        if self.t.size == self.y.size:
            self.t = self.t.argmax(axis=1)
        
        loss = cross_entropy_error(self.y, self.t)
        return loss
    
    def backward(self, dout=1):
        batch_size = self.t.shape[0]
        
        dx = self.y.copy()
        dx[np.arange(batch_size), self.t] -= 1
        dx *= dout
        dx = dx / batch_size
        
        return dx

3.7 SimpleSkipGram类

In [8]:

python 复制代码
class SimpleSkipGram:
    """
    模型skip-gram实现
    """
    def __init__(self, vocab_size, hidden_size):
        """
        vocab_size:词汇表大小
        hidden_size:隐藏层的大小
        """
        V,H = vocab_size, hidden_size
        
        # 初始化权重
        W_in = 0.01 * np.random.randn(V,H).astype("f")  # 输入层和输出层的权重矩阵;符合正态分布随机初始化
        W_out = 0.01 * np.random.randn(H,V).astype("f")
        
        # 生成层
        self.in_layer = MatMul(W_in)  # 输入层和输出层的初始化
        self.out_layer = MatMul(W_out)
        self.loss_layer1 = SoftmaxWithLoss()   # SoftmaxWithLoss实现softmax激活函数和交叉熵损失函数;输入和输出层的实例
        self.loss_layer2 = SoftmaxWithLoss()
        
        # 权重和梯度的保存
        layers = [self.in_layer, self.out_layer]  
        self.params, self.grads = [], []  # 用于存储模型参数和它们的梯度
        
        for layer in layers:
            self.params += layer.params  # 模型参数更新
            self.grads += layer.grads  # 梯度更新
            
        # 将单词的分布式表示设置为成员变量
        self.word_vecs = W_in
        
        
    def forward(self, contexts, target):
        """
        输入:上下文单词 + 目标词
        """
        h = self.in_layer.forward(target) # 从目标单词得到隐藏层表示
        s = self.out_layer.forward(h) # 从隐藏层到输出层的表示
        l1 = self.loss_layer1.forward(s, contexts[:,0])  # 使用两个损失层计算损失
        l2 = self.loss_layer2.forward(s, contexts[:,1])
        loss = l1 + l2  # 损失的合并
        return loss
    
    def backward(self, dout=1): 
        """
        输入:梯度输出
        """
        dl1 = self.loss_layer1.backward(dout)  # 计算两个损失层的梯度
        dl2 = self.loss_layer2.backward(dout)
        ds = dl1 + dl2  # 2个梯度合并
        dh = self.out_layer.backward(ds)  # 使用总梯度计算输出层的梯度
        self.in_layer.backward(dh)
        return None

3.8 定义优化器

定义Adam优化器:

In [9]:

python 复制代码
class Adam:
    """
    Adam优化器实现
    """
    def __init__(self, lr=0.001, beta1=0.9, beta2=0.999):
        self.lr = lr
        self.beta1 = beta1
        self.beta2 = beta2
        self.iter = 0
        self.m = None
        self.v = None
        
    def update(self, params, grads):
        if self.m is None:
            self.m, self.v = [], []
            for param in params:
                self.m.append(np.zeros_like(param))
                self.v.append(np.zeros_like(param))
        
        self.iter += 1
        lr_t = self.lr * np.sqrt(1.0 - self.beta2**self.iter) / (1.0 - self.beta1**self.iter)

        for i in range(len(params)):
            self.m[i] += (1 - self.beta1) * (grads[i] - self.m[i])
            self.v[i] += (1 - self.beta2) * (grads[i]**2 - self.v[i])
            
            params[i] -= lr_t * self.m[i] / (np.sqrt(self.v[i]) + 1e-7)

3.9 参数去重

In [10]:

python 复制代码
# 参数去重

def remove_duplicate(params, grads):
    '''
    将参数列表中重复的权重整合为1个,
    加上与该权重对应的梯度
    '''
    params, grads = params[:], grads[:]  # 副本

    while True:
        find_flg = False
        L = len(params)

        for i in range(0, L - 1):
            for j in range(i + 1, L):
                # 在共享权重的情况下
                if params[i] is params[j]:
                    grads[i] += grads[j]  # 加上梯度
                    find_flg = True
                    params.pop(j)
                    grads.pop(j)
                # 在作为转置矩阵共享权重的情况下(weight tying)
                elif params[i].ndim == 2 and params[j].ndim == 2 and \
                     params[i].T.shape == params[j].shape and np.all(params[i].T == params[j]):
                    grads[i] += grads[j].T
                    find_flg = True
                    params.pop(j)
                    grads.pop(j)

                if find_flg: 
                    break
            if find_flg: 
                break

        if not find_flg:
            break

    return params, grads

3.10 Trainer类

In [11]:

python 复制代码
# coding: utf-8

import numpy as np
import time
import matplotlib.pyplot as plt


class Trainer:
    def __init__(self, model, optimizer):
        self.model = model
        self.optimizer = optimizer
        self.loss_list = []
        self.eval_interval = None
        self.current_epoch = 0

    def fit(self, x, t, max_epoch=10, batch_size=32, max_grad=None, eval_interval=20):
        data_size = len(x)
        max_iters = data_size // batch_size
        self.eval_interval = eval_interval
        model, optimizer = self.model, self.optimizer
        total_loss = 0
        loss_count = 0

        start_time = time.time()
        for epoch in range(max_epoch):
            # 打乱
            idx = np.random.permutation(np.arange(data_size))
            x = x[idx]
            t = t[idx]

            for iters in range(max_iters):
                batch_x = x[iters*batch_size:(iters+1)*batch_size]
                batch_t = t[iters*batch_size:(iters+1)*batch_size]

                # 计算梯度,更新参数
                loss = model.forward(batch_x, batch_t)
                model.backward()
                params, grads = remove_duplicate(model.params, model.grads)  # 将共享的权重整合为1个
                if max_grad is not None:
                    clip_grads(grads, max_grad)
                optimizer.update(params, grads)
                total_loss += loss
                loss_count += 1

                # 评价
                if (eval_interval is not None) and (iters % eval_interval) == 0:
                    avg_loss = total_loss / loss_count
                    elapsed_time = time.time() - start_time
                    print('| epoch %d |  iter %d / %d | time %d[s] | loss %.2f'
                          % (self.current_epoch + 1, iters + 1, max_iters, elapsed_time, avg_loss))
                    self.loss_list.append(float(avg_loss))
                    total_loss, loss_count = 0, 0

            self.current_epoch += 1

    def plot(self, ylim=None):
        x = np.arange(len(self.loss_list))
        if ylim is not None:
            plt.ylim(*ylim)
        plt.plot(x, self.loss_list, label='train')
        plt.xlabel('iterations (x' + str(self.eval_interval) + ')')
        plt.ylabel('loss')
        plt.show()

3.11 模型训练

In [12]:

python 复制代码
# 参数设置
window_size = 1
hidden_size = 5
batch_size = 3
max_epoch = 1000

In [13]:

python 复制代码
text = "You say goodbye and I say hello."
corpus, word_to_id, id_to_word = preprocess(text)
vocab_size = len(word_to_id)

In [14]:

python 复制代码
contexts, target = create_contexts_target(corpus, window_size)

# 单词ID转成one-hot表示
contexts = convert_one_hot(contexts, vocab_size)
target = convert_one_hot(target, vocab_size)

建立模型:

In [15]:

python 复制代码
model = SimpleSkipGram(vocab_size, hidden_size)

In [16]:

python 复制代码
optimizer = Adam()  # 优化器

In [17]:

python 复制代码
trainer = Trainer(model, optimizer) # 实例化模型

In [18]:

python 复制代码
trainer.fit(contexts, target, max_epoch, batch_size)

trainer.plot()
| epoch 1 |  iter 1 / 2 | time 0[s] | loss 3.89
| epoch 2 |  iter 1 / 2 | time 0[s] | loss 3.89
| epoch 3 |  iter 1 / 2 | time 0[s] | loss 3.89
| epoch 4 |  iter 1 / 2 | time 0[s] | loss 3.89
| epoch 5 |  iter 1 / 2 | time 0[s] | loss 3.89
| epoch 6 |  iter 1 / 2 | time 0[s] | loss 3.89
| epoch 7 |  iter 1 / 2 | time 0[s] | loss 3.89
...
| epoch 998 |  iter 1 / 2 | time 0[s] | loss 1.89
| epoch 999 |  iter 1 / 2 | time 0[s] | loss 2.11
| epoch 1000 |  iter 1 / 2 | time 0[s] | loss 1.66
相关推荐
Hello world.Joey21 小时前
Transformer解读
人工智能·深度学习·神经网络·自然语言处理·nlp·aigc·transformer
belldeep1 天前
python:spaCy 工业级 NLP 库
python·自然语言处理·nlp·spacy
程序员lm2 天前
从0-1体验本地部署小模型
python·nlp
小马过河R4 天前
小白沉浸式本地Mac小龙虾OpenClaw部署安装教程
人工智能·macos·大模型·nlp·agent·openclaw·龙虾
华农DrLai5 天前
什么是Prompt注入攻击?为什么恶意输入能操控AI行为?
人工智能·深度学习·大模型·nlp·prompt
华农DrLai5 天前
什么是Prompt模板?为什么标准化的格式能提高稳定性?
数据库·人工智能·gpt·nlp·prompt
华农DrLai5 天前
什么是自动Prompt优化?为什么需要算法来寻找最佳提示词?
人工智能·算法·llm·nlp·prompt·llama
华农DrLai6 天前
什么是Prompt工程?为什么提示词的质量决定AI输出的好坏?
数据库·人工智能·gpt·大模型·nlp·prompt
热爱生活的猴子6 天前
RoBERTa 分类模型正则化调优实验——即dropout和冻结层对过拟合的影响
人工智能·深度学习·分类·数据挖掘·nlp
数据智能老司机7 天前
精通 Hugging Face 自然语言处理——深度 Q 网络与 Atari 游戏
nlp