ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

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

2021-12-09 21:31:55  阅读:209  来源: 互联网

标签:word 进阶 contexts self CBOW np word2vec 自然语言 size


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

第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 模型。

本章所学的内容

  • 基于推理的方法以预测为目标,同时获得了作为副产物的单词的分 布式表示
  • word2vec 是基于推理的方法,由简单的 2 层神经网络构成
  • word2vec 有 skip-gram 模型和 CBOW 模型
  • CBOW 模型从多个单词(上下文)预测 1 个单词(目标词)
  • skip-gram 模型反过来从 1 个单词(目标词)预测多个单词(上下文)
  • 由于 word2vec 可以进行权重的增量学习,所以能够高效地更新或添 加单词的分布式表示

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

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有