系列文章目录

第一章 1:同义词词典和基于计数方法语料库预处理

第一章 2:基于计数方法的分布式表示和假设,共现矩阵,向量相似度

第一章 3:基于计数方法的改进以及总结

第二章 1:word2vec

第二章 2:word2vec和CBOW模型的初步实现

第二章 3:CBOW模型的完整实现

第二章 4:


文章目录


前言

在开始word2vec的学习之前,我们先来准备学习用的数据。这里我们仍以“You say goodbye and I say hello.”这个只有一句话的语料库为例进行说明


一、学习数据的准备

1.上下文和目标词

word2vec 中使用的神经网络的输入是上下文,它的正确解标签是被这些上下文包围在中间的单词,即目标词。也就是说,我们要做的事情是,当向神经网络输入上下文时,使目标词出现的概率高(为了达成这一目标而进行学习)。 下面我们就从语料库生成上下文和目标词,如下图所示。

在上图中,我们将语料库中的目标单词作为目标词,将其周围的单词作为上下文提取出来。我们对语料库中的所有单词都执行该操作(两端的单词除外),可以得到上图右侧的contexts(上下文)和target(目标词)。 contexts 的各行成为神经网络的输入(那基于计数的方法呢?),target的各行成为正确解标签(要预测出的单词)。另外,在各笔样本数据中,上下文有多个单词(这个例子中有两个),而目标词则只有一个,因此只有上下文写成了复数形式contexts。

现在,我们来实现从语料库生成上下文和目标词的函数。在此之前,我们先复习一下上一章的内容。首先,将语料库的文本转化成单词ID。这需要使用第1章第一节实现的preprocess()函数。(忘了或者没看的点这里,在文章的最后有该函数自然语言处理(1:同义词词典和基于计数方法语料库预处理)-CSDN博客

from common.util import preprocess
# 我将其封装在了common中的util文件中

text = 'You say goodbye and I say hello.'

corpus, word_to_id, id_to_word = preprocess(text)

print(corpus)
# [0 1 2 3 4 1 5 6]

print(id_to_word)
# {0: 'you', 1: 'say', 2: 'goodbye', 3: 'and', 4: 'i', 5: 'hello', 6: '.'}

然后,从单词ID列表corpus生成contexts和target。具体来说,如下图所示,实现一个当给定corpus时返回contexts和target的函数。

如上图所示,contexts 是二维数组。此时,contexts的第0维保存的是各个上下文数据。具体来说,contexts[0]保存的是第0个上下文,context[1] 保存的是第1个上下文……同样地,就目标词而言,target[0] 保存的是第0个目标词,target[1]保存的是第1个目标词…… 现在,我们来实现这个生成上下文和目标词的函数,这里将其称为create_contexts_target(corpus, window_size)(我还是封装在了common的util文件中了)

def create_contexts_target(corpus, window_size=1):
    target = corpus[window_size:window_size + 1]
    contexts = []

    for idx in range(window_size, len(corpus) - window_size):
        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)

# 不理解上述代码问DS(deepseek)哈 
    
            
            

这个函数有两个参数:一个是单词ID列表(corpus);另一个是上下文的窗口大小(window_size)。另外,函数返回的是NumPy多维数组格式的上下文和目标词。现在,我们来实际使用一下这个函数,接着刚才的实现, 代码如下所示

contexts, target = create_contexts_target(corpus, window_size=1)
print(contexts)
# [[0 2]
# [1 3]    
# [2 4]
# [3 1]
# [4 5]
# [1 6]]



print(target)
# [1 2 3 4 1 5]

这样就从语料库生成了上下文和目标词,后面只需将它们赋给CBOW 模型即可。不过,因为这些上下文和目标词的元素还是单词ID,所以还需 要将它们转化为one-hot表示。

2.转化为one-hot表示(独热编码)

我们的最终实现总目标如下图:

如上图所示,上下文和目标词从单词ID转化为了one-hot表示。 这里需要注意各个多维数组的形状。在上面的例子中,使用单词ID时的 contexts 的形状是(6,2),将其转化为one-hot表示后,形状变为(6,2,7)。 这里提供了convert_one_hot() 函数以将单词ID转化为one-hot表示。 这个函数的实现内容很简单,代码依然存放在common/util.py中。该函数的参数是单词ID列表和词汇个数。并且我们再把到目前为止的数据预处理总结 一下,如下所示。

def convert_ont_hot(corpus, vocab_size):
    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



from common.util import preprocess, create_contexts_target, convert_one_hot
# (因为我已经封装在了这个包中)
 
text = 'You say goodbye and I say hello.'

corpus, word_to_id, id_to_word = preprocess(text)

contexts, target = create_contexts_target(corpus, window_size=1)

vocab_size = len(word_to_id)

target = convert_one_hot(target, vocab_size)

contexts = convert_one_hot(contexts, vocab_size)

至此,学习数据的准备就完成了,下面我们来讨论最重要的CBOW模 型的实现。

二、CBOW模型的实现

1.计算图

首先给出计算图

2. simpleCBOW的实现

我们将上图中的神经网络实现为SimpleCBOW类(下一次将实现对其进行了改进的CBOW类)。首先,让我们看一下SimpleCBOW类的初始化方法

import numpy as np
from common.layers import MatMul, SoftmaxWithLoss

# MatMul和SoftmaxWithLoss大家可以搜一下啊如何实现的,问DS(deepseek)

# 当然我在这里还是会继续给出,大家如果要在这里实现SoftmaxWithLoss,那么去掉上面的导入

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)
        
        
        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



class SimpleCBOW:
    def __init__(self, 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_layer0 = MatMul(W_in)
        self.in_layer1 = MatMul(W_in)
        self.out_layer = MatMul(W_out)
        self.loss_layer = SoftmaxWithLoss()
        
        # 将所有的权重和梯度整理到列表中
        layers = [self.in_layer0, self.in_layer1, self.out_layer]
        self.params, self.grads = [], []
        for layer in layers:
            self.params += layer.params
            self.grads += layer.grads
      
        # 将单词的分布式表示设置为成员变量
        self.word_vecs = W_in
    

这里,初始化方法的参数包括词汇个数vocab_size和中间层的神经元 个数hidden_size。关于权重的初始化,首先我们生成两个权重(W_in和W_ out),并用一些小的随机值初始化这两个权重。此外,我们指定NumPy数 组的数据类型为astype('f'),这样一来,初始化将使用32位的浮点数。接着,我们创建必要的层。首先,生成两个输入侧的MatMul层、一个 输出侧的MatMul层,以及一个Softmax with Loss层。这里,用来处理输 入侧上下文的MatMul层的数量与上下文的单词数量相同(本例中是两个)。 另外,我们使用相同的权重来初始化MatMul层。 最后,将该神经网络中使用的权重参数和梯度分别保存在列表类型的成 员变量params和grads中。

3.SimpleCBOW的正反向传播

接下来,我们首先来实现神经网络的正向传播forward()函数。这个函数接收参数contexts和target,并返回损失(loss)

def forward(self, contexts, target):
    h0 = self.in_layer0.forward(contexts[:, 0])
    h1 = self.in_layer1.forward(contexts[:, 1])
    
    h = (h0 + h1) * 0.5

    score = self.out_layer.forward(h)
    loss = self.loss_layer.forward(score, target)
    return loss

这里,我们假定参数contexts是一个三维NumPy数组,即往上翻‘转化为one-hot表示’(独热编码)这一小节的图的例子中(6,2,7)的形状,其中第0维的元素个数是mini-batch的数量, 第1维的元素个数是上下文的窗口大小,第2维表示one-hot向量。此外, target 是 (6,7) 这样的二维形状。 最后,我们实现反向传播backward()。这个反向传播的计算图如下图所示。

神经网络的反向传播在与正向传播相反的方向上传播梯度。这个反向传播从1出发,并将其传向Softmax with Loss层。然后,将Softmax with Loss 层的反向传播的输出ds传到输出侧的MatMul层。 之后就是“+”和“×”运算的反向传播。“×”的反向传播将正向传播时的输入值“交换”后乘以梯度。“+”的反向传播则将梯度“原样”传播。 我们按照上图来实现反向传播。

def backward(self, dout=1):
    ds = self.loss_layer.backward(dout)
    da = self.out_layer.backward(ds)
    
    da *= 0.5
    self.in_layer1.backward(da)
    self.in_layer0.backward(da)
    return None

至此,反向传播的实现就结束了。我们已经将各个权重参数的梯度保存在了成员变量grads中。因此,通过先调用forward()函数,再调用backward() 函数,grads 列表中的梯度被更新。下面,我们继续看一下 SimpleCBOW 类的学习

三、CBOW模型的实例化

CBOW模型的学习和一般的神经网络的学习完全相同。首先,给神 经网络准备好学习数据。然后,求梯度,并逐步更新权重参数。这里,我 们使用第1章介绍的Trainer类来执行学习过程,学习的源代码如下所示

from common.trainer import Trainer # 也实现一次在这里,很长
from common.optimizer import Adam # 梯度下降方法,我这里再实现一次
from simple_cbow import SimpleCBOW # 实现过了
from common.util import preprocess, create_contexts_target, convert_one_hot



"""
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 = numpy.random.permutation(numpy.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 = numpy.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()

"""
# --------------------------------------------------

class 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)

# -------------------------------------------

window_size = 1
hidden_size = 5
batch_size = 3
max_epoch = 1000
text = 'You say goodbye and I say hello.'
corpus, word_to_id, id_to_word = preprocess(text)
vocab_size = len(word_to_id)
contexts, target = create_contexts_target(corpus, window_size)
target = convert_one_hot(target, vocab_size)
contexts = convert_one_hot(contexts, vocab_size)
model = SimpleCBOW(vocab_size, hidden_size)
optimizer = Adam()
trainer = Trainer(model, optimizer)

trainer.fit(contexts, target, max_epoch, batch_size)

trainer.plot()

运行上面的代码,结果如下

如上图所示,通过不断学习,损失在减小,看起来学习进行得一切正常。我们来看一下学习结束后的权重参数。这里,我们取出输入侧的MatMul层的权重,实际确认一下它的内容。因为输入侧的MatMul层的权 重已经赋值给了成员变量word_vecs,所以接着上面的代码,我们追加下面 的代码。

word_vecs = model.word_vecs
for word_id, word in id_to_word.items():
    print(word, word_vecs[word_id])

这里,使用word_vecs这个变量保存权重。word_vecs的各行保存了对应的单词ID的分布式表示。实际运行一下,你看看结果会是什么(自己验证)

我们终于将单词表示为了密集向量!这就是单词的分布式表示。我们有 理由相信,这样的分布式表示能够很好地捕获单词含义。 不过,遗憾的是,这里使用的小型语料库并没有给出很好的结果。当 然,主要原因是语料库太小了。如果换成更大、更实用的语料库,相信会获 得更好的结果。但是,这样在处理速度方面又会出现新的问题,这是因为当 前这个CBOW模型的实现在处理效率方面存在几个问题。下一次我们将改进这个简单的CBOW模型,实现一个“真正的”CBOW模型。


总结

本次主要讲了CBOW的简单实现过程,当然难免有不少遗漏之处,希望看到这里的大佬们点点关注,一键三连哦!

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐