其他分享
首页 > 其他分享> > 深度学习进阶:自然语言处理入门:第3章 word2vec

深度学习进阶:自然语言处理入门:第3章 word2vec

作者:互联网

深度学习进阶:自然语言处理入门

第3章 word2vec

本章的主题仍是单词的分布式表示。在上一章中,我们使 用基于计数的方法得到了单词的分布式表示。本章我们将讨论该方法的替代 方法,即基于推理的方法。

顾名思义,基于推理的方法使用了推理机制。当然,这里的推理机制用 的是神经网络。本章,著名的 word2vec 将会登场。

本章的目标是实现一个简单的 word2vec。这个简单的 word2vec 会优 先考虑易理解性,从而牺牲一定的处理效率。因此,我们不会用它来处理大 规模数据集,但用它处理小数据集毫无问题。

3.1 基于推理的方法和神经网络

用向量表示单词的研究最近正在如火如荼地展开,其中比较成功的方 法大致可以分为两种:一种是基于计数的方法;另一种是基于推理的方法。

3.1.1  基于计数的方法的问题

​ 基于计数的方法使用整个语料库的统计数据(共现矩阵和 PPMI 等), 通过一次处理(SVD 等)获得单词的分布式表示。而基于推理的方法使用 神经网络,通常在 mini-batch 数据上进行学习。这意味着神经网络一次只 需要看一部分学习数据(mini-batch),并反复更新权重。

​ 基于计数的方法一次性处理全部学习数据;反之,基于推理的方法使用部分学习数据逐步学习。

3.1.2  基于推理的方法的概要

基于推理的方法的主要操作是**“推理”。**

在这里插入图片描述
在这里插入图片描述

3.1.3  神经网络中单词的处理方法

在这里插入图片描述
在这里插入图片描述

​ 如图 3-5 所示,输入层由 7 个神经元表示,分别对应于 7 个单词(第 1 个神经元对应于 you,第 2 个神经元对应于 say)

​ 现在事情变得很简单了。因为只要将单词表示为向量,这些向量就可以由构成神经网络的各种“层”来处理。比如,对于one-hot表示的某个单词, 使用全连接层对其进行变换的情况如图 3-6 所示。(省略了偏置)
在这里插入图片描述
在这里插入图片描述

我们看一下代码。这里的全连接层变换可以写成如下

import numpy as np
c = np.array([[1, 0, 0, 0, 0, 0, 0]]) # 输入
W = np.random.randn(7, 3) # 权重
h = np.dot(c, W) # 中间节点
print(W)
print(h)

#输出
[[-1.30982294  0.19485001 -0.49979452]
 [-1.77466539 -2.67810488  2.7992046 ]
 [-0.20747764 -0.68246166  0.7149981 ]
 [ 0.18558413 -0.61176428  2.25844791]
 [-0.70263837  0.63946127 -0.33276184]
 [-0.31945603  0.07161013  1.18615179]
 [ 2.01949978 -0.5961003  -1.01233551]]

[[-1.30982294  0.19485001 -0.49979452]]

在这里插入图片描述

class MatMul:
    def __init__(self, W):
        self.params = [W]
        self.grads = [np.zeros_like(W)]
        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)
        self.grads[0][...] = dW
        return dx

这里,仅为了提取权重的行向量而进行矩阵乘积计算好像不是很有效 率。关于这一点,我们将在 4.1 节进行改进。另外,上述代码的功能也可以 使用第 1 章中实现的 MatMul 层完成,

c = np.array([[1, 0, 0, 0, 0, 0, 0]])
W = np.random.randn(7, 3)
layer = MatMul(W)
h = layer.forward(c)
print(h)
# [[-0.70012195 0.25204755 -0.79774592]]

3.2 简单的 word2vec

word2vec 一词最初用来指程序或者工具,但是随着该词的流行, 在某些语境下,也指神经网络的模型。正确地说,CBOW 模型 和 skip-gram 模型是 word2vec 中使用的两个神经网络。

3.2.1  CBOW模型的推理

CBOW 模型是根据上下文预测目标词的神经网络(“目标词”是指中间 的单词,它周围的单词是“上下文”)。通过训练这个 CBOW 模型,使其能 尽可能地进行正确的预测,我们可以获得单词的分布式表示。

CBOW 模型的输入是上下文。这个上下文用 [‘you’, ‘goodbye’] 这样 的单词列表表示。我们将其转换为 one-hot 表示,以便 CBOW 模型可以进 行处理。在此基础上,CBOW 模型的网络可以画成图 3-9 这样。

图3-9 是 CBOW 模型的网络。它有两个输入层,经过中间层到达输出 层。这里,从输入层到中间层的变换由相同的全连接层(权重为Win)完成, 从中间层到输出层神经元的变换由另一个全连接层(权重为 Wout)完成。
在这里插入图片描述

有时将得分经过 Softmax 层之后的神经元称为输出层。这里,我 们将输出得分的节点称为输出层。
在这里插入图片描述
在这里插入图片描述

不使用偏置的全连接层的处理由 MatMul 层的正向传播代理。这 个层在内部计算矩阵乘积。

实现 CBOW 模型的推理,具 体实现如下所示

# coding: utf-8
import sys
sys.path.append('..')
import numpy as np
from common.layers import MatMul


# 样本的上下文数据
c0 = np.array([[1, 0, 0, 0, 0, 0, 0]])
c1 = np.array([[0, 0, 1, 0, 0, 0, 0]])

# 初始化权重
W_in = np.random.randn(7, 3)
W_out = np.random.randn(3, 7)

# 生成层
in_layer0 = MatMul(W_in)
in_layer1 = MatMul(W_in)
out_layer = MatMul(W_out)

# 正向传播
h0 = in_layer0.forward(c0)
h1 = in_layer1.forward(c1)
h = 0.5 * (h0 + h1)
s = out_layer.forward(h)
print(s)

# [[-0.33304998  3.19700011  1.75226542  1.36880744  1.68725368  2.38521564 0.81187955]]

以上就是 CBOW 模型的推理过程。这里我们见到的 CBOW 模型是没 有使用激活函数的简单的网络结构。除了多个输入层共享权重外,并没有什 么难点。

3.2.2  CBOW模型的学习

在这里插入图片描述

CBOW 模型的学习就是调整权重,以使预测准确。其结果是,权重 Win(确切地说是 Win 和 Wout 两者)学习到蕴含单词出现模式的向量。根据过去的实验,CBOW 模型(和 skip-gram 模型)得到的单词的分布式表示,特别是使用维基百科等大规模语料库学习到的单词的分布式表示,在单词的含义和语法上符合我们直觉的案例有很多.

CBOW模型只是学习语料库中单词的出现模式如果语料库不一样, 学习到的单词的分布式表示也不一样。比如,只使用“体育”相关 的文章得到的单词的分布式表示,和只使用“音乐”相关的文章得 到的单词的分布式表示将有很大不同。

在这里插入图片描述

3.2.3  word2vec的权重和分布式表示

在这里插入图片描述

就 word2vec(特别是 skip-gram 模型)而言,最受欢迎的是方案 A。遵循这一思路,我们也使用 Win 作为单词的分布式表示

3.3 学习数据的准备

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

3.3.1  上下文和目标词

word2vec 中使用的神经网络的输入是上下文,它的正确解标签是被这些上下文包围在中间的单词,即目标词
在这里插入图片描述

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)
            word_to_id[word] = new_id
            id_to_word[new_id] = word

    corpus = np.array([word_to_id[w] for w in words])

    return corpus, word_to_id, id_to_word

我们来实现从语料库生成上下文和目标词的函数。在此之前,我 们先复习一下上一章的内容。首先,将语料库的文本转化成单词 ID。

import sys
sys.path.append('..')
from common.util import preprocess
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: '.'}

在这里插入图片描述

生成上下文和目标词的函数 create_contexts_target(corpus, window_size)

def create_contexts_target(corpus, window_size=1):
    '''生成上下文和目标词

    :param corpus: 语料库(单词ID列表)
    :param window_size: 窗口大小(当窗口大小为1时,左右各1个单词为上下文)
    :return:
    '''
    target = corpus[window_size:-window_size]
    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)

接着刚才的实现, 代码如下所示。

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]

3.3.2  转化为one-hot表示

在这里插入图片描述

def convert_one_hot(corpus, vocab_size):
    '''转换为one-hot表示

    :param corpus: 单词ID列表(一维或二维的NumPy数组)
    :param vocab_size: 词汇个数
    :return: one-hot表示(二维或三维的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

实现代码:学习数据的准备就完成了

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

#输出contexts
array([[[1, 0, 0, 0, 0, 0, 0],
        [0, 0, 1, 0, 0, 0, 0]],

       [[0, 1, 0, 0, 0, 0, 0],
        [0, 0, 0, 1, 0, 0, 0]],

       [[0, 0, 1, 0, 0, 0, 0],
        [0, 0, 0, 0, 1, 0, 0]],

       [[0, 0, 0, 1, 0, 0, 0],
        [0, 1, 0, 0, 0, 0, 0]],

       [[0, 0, 0, 0, 1, 0, 0],
        [0, 0, 0, 0, 0, 1, 0]],

       [[0, 1, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 1]]])

3.4 CBOW模型的实现

在这里插入图片描述

class SimpleCBOW:
    def __init__(self, vocab_size, hidden_size):
        V, H = vocab_size, hidden_size		#词汇个数 vocab_size,中间层的神经元个数 hidden_size

        # 初始化权重,并用一些小的随机值初始化这两个权重,astype('f')初始化将使用 32 位的浮点数。
        W_in = 0.01 * np.random.randn(V, H).astype('f')
        W_out = 0.01 * np.random.randn(H, V).astype('f')

        # 生成层
        #生成两个输入侧的 MatMul 层、一个输出侧的 MatMul 层,以及一个 Softmax with Loss 层。
        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

    #现神经网络的正向传播 forward() 函数
    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

    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

这里,用来处理输入侧上下文的 MatMul 层的数量与上下文的单词数量相同(本例中是两个)。 另外,我们使用相同的权重来初始化 MatMul 层。

最后,将该神经网络中使用的权重参数和梯度分别保存在列表类型的成 员变量 params 和 grads 中。

这里,多个层共享相同的权重。因此,params列表中存在多个相 同的权重。但是,在 params列表中存在多个相同的权重的情况 下,Adam、Momentum 等优化器的运行会变得不符合预期(至 少就我们的代码而言)。为此,在 Trainer类的内部,在更新参数 时会进行简单的去重操作。关于这一点,这里省略说明,感兴趣 的读者可以参考 common/trainer.py的 remove_duplicate(params, grads)

这里,我们假定参数 contexts 是一个三维 NumPy 数组,即上一节图 3-18 的例子中 (6,2,7)的形状,其中第 0 维的元素个数是 mini-batch 的数量, 第 1 维的元素个数是上下文的窗口大小,第 2 维表示 one-hot 向量。此外, target 是 (6,7) 这样的二维形状。
在这里插入图片描述

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

学习的实现

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

import sys
sys.path.append('..')  # 为了引入父目录的文件而进行的设定
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


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

word_vecs = model.word_vecs  #为输入侧的 MatMul 层的权重已经赋值给了成员变量 word_vecs
for word_id, word in id_to_word.items():
    print(word, word_vecs[word_id])

you [ 1.1550112  1.2552509 -1.1116056  1.1482503 -1.2046812]
say [-1.2141827  -1.2367412   1.2163384  -1.2366292   0.67279905]
goodbye [ 0.7116186   0.55987084 -0.8319744   0.749239   -0.8436555 ]
and [-0.94652086 -0.8535852   0.55927175 -0.6934891   2.0411916 ]
i [ 0.7177702  0.5699475 -0.8368816  0.7513028 -0.8419596]
hello [ 1.1411363  1.2600429 -1.105042   1.1401148 -1.2044929]
. [-1.1948084 -1.2921802  1.4119368 -1.345656  -1.5299033]

在这里插入图片描述

不过,遗憾的是,这里使用的小型语料库并没有给出很好的结果。当 然,主要原因是语料库太小了。如果换成更大、更实用的语料库,相信会获 得更好的结果。但是,这样在处理速度方面又会出现新的问题,这是因为当 前这个 CBOW 模型的实现在处理效率方面存在几个问题。下一章我们将改 进这个简单的 CBOW 模型,实现一个“真正的”CBOW 模型。

3.5 word2vec的补充说明

3.5.1  CBOW模型和概率

本书中将概率记为 P(·),比如事 件 A 发生的概率记为 P(A)。联合概率记为** P(A, B),表示事件 A 和事件 B 同时发生的概率。 后验概率记为 P(A|B)**,字面意思是“事件发生后的概率”。从另一个 角度来看,也可以解释为“在给定事件 B(的信息)时事件 A 发生的概率”。
在这里插入图片描述
在这里插入图片描述

3.5.2  skip-gram模型

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

import sys
sys.path.append('..')
import numpy as np
from common.layers import MatMul, SoftmaxWithLoss


class SimpleSkipGram:
    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_layer = MatMul(W_in)
        self.out_layer = MatMul(W_out)
        self.loss_layer1 = SoftmaxWithLoss()
        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
        dh = self.out_layer.backward(ds)
        self.in_layer.backward(dh)
        return None

class MatMul:
    def __init__(self, W):
        self.params = [W]
        self.grads = [np.zeros_like(W)]
        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)
        self.grads[0][...] = dW
        return dx
    
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)

        # 在监督标签为one-hot向量的情况下,转换为正确解标签的索引
        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.5.3  基于计数与基于推理

此外,在 word2vec 之后,有研究人员提出了 GloVe 方法 [27]。GloVe 方法融合了基于推理的方法和基于计数的方法。该方法的思想是,将整个语 料库的统计数据的信息纳入损失函数,进行 mini-batch 学习(具体请参考 论文 [27])。据此,这两个方法论成功地被融合在了一起。

3.6 小结

本章我们详细解释了 word2vec 的 CBOW 模型,并对其进行了实 现。CBOW 模型基本上是一个 2 层的神经网络,结构非常简单。我们使用 MatMul 层和 Softmax with Loss 层构建了 CBOW 模型,并用一个小规模 语料库确认了它的学习过程。遗憾的是,现阶段的 CBOW 模型在处理效率 上还存在一些问题。不过,在理解了本章的 CBOW 模型之后,离真正的 word2vec 也就一步之遥了。下一章,我们将改进 CBOW 模型。

本章所学的内容

标签:word,进阶,contexts,self,CBOW,np,word2vec,自然语言,size
来源: https://blog.csdn.net/weixin_44953928/article/details/121844426