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
相关推荐
angleboy81 天前
【LLM Agents体验 1】Dify框架的安装指南
人工智能·语言模型·大模型·nlp
龙的爹23332 天前
论文 | Evaluating the Robustness of Discrete Prompts
人工智能·gpt·自然语言处理·nlp·prompt·agi
Watermelo6174 天前
从模糊搜索到语义搜索的进化之路——探索 Chroma 在大模型中的应用价值
python·机器学习·搜索引擎·语言模型·自然语言处理·数据挖掘·nlp
gzroy4 天前
中文词向量质量的评估
人工智能·语言模型·nlp
向阳12185 天前
python NLTK快速入门
python·ai·nlp
龙的爹23336 天前
论文翻译 | PROMPTAGATOR : FEW-SHOT DENSE RETRIEVAL FROM 8 EXAMPLES
人工智能·深度学习·算法·语言模型·自然语言处理·nlp·prompt
Zhank106 天前
基于milvus的多模态检索
人工智能·nlp
Sookie--7 天前
基于Pyecharts的数据可视化开发(二)调用通义千问api分析爬虫数据
人工智能·爬虫·python·信息可视化·语言模型·自然语言处理·nlp
龙的爹23337 天前
论文 | PROMPTAGATOR : FEW-SHOT DENSE RETRIEVAL FROM 8 EXAMPLES
人工智能·深度学习·算法·机器学习·语言模型·nlp·prompt
汀、人工智能7 天前
NLP算法工程师精进之路:顶会论文研读精华
llm·nlp