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

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

本章主题:单词分布式的表示


上一章回顾:使用计数法得到单词的分布式


本章核心:讨论该方法的替代方法--基于推理的方法


热点:推理机制:神经网络


著名的Wordvec登场(实现的为简单的Wordvec,所以会牺牲一定的效率, 但是如果不用它处理大规模数据,处理些许小数据还是绰绰有余的嘞)


3.1.1 基于计数的方法的问题


基于计数的方法使用整个语料库的统计数据(共现矩阵和 PPMI 等), 通过一次处理(SVD 等)获得单词的分布式表示。而基于推理的方法使用 神经网络,通常在 mini-batch 数据上进行学习。这意味着神经网络一次只 需要看一部分学习数据(mini-batch),并反复更新权重。
​ 基于计数的方法一次性处理全部学习数据;反之,基于推理的方法使用部分学习数据逐步学习。
使用了共现矩阵和PPMI等
拓:
共现矩阵:统计文本中两两词组之间共同出现的次数,以此来描述词组间的亲密度,看他们关系好不好的呢。
代码实现:

import sys
sys.path.append('..')
import numpy as np
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: '.'}



从下文图片可看出,单词you的上下文仅仅只 有say这个单词。


所以呢,用表格表示的话,就是下图,之后我们会将下表的数字用作我们单词信息查询坐标的,所以这是有用的嘞。
比如说:我们可以用向量[0, 1, 0, 0, 0, 0, 0]来表示单词you


向量此时可以理解成you的位置坐标
如果此时我们将上面全部的单词实现上述操作,就会得到下图:


上图呢,就是汇总了所有单词的共现单词的表格,这个表格记录了每个单词的位置,专业术语将它说成是向量。因为上面这个表格是矩形的形状,然后呢,将所有单词的位置呈现出来了的,因此,有位大牛就把它称为共现矩阵(英文或者代码中,我们把共现矩阵命名为:co-occurence-matrix)
共现矩阵代码实现:

C = np.array([
 [0, 1, 0, 0, 0, 0, 0],
 [1, 0, 1, 0, 1, 1, 0],
 [0, 1, 0, 1, 0, 0, 0],
 [0, 0, 1, 0, 1, 0, 0],
 [0, 1, 0, 1, 0, 0, 0],
 [0, 1, 0, 0, 0, 0, 1],
 [0, 0, 0, 0, 0, 1, 0],
], dtype=np.int32)
print(C[0]) # 单词ID为0的向量
# [0 1 0 0 0 0 0]
print(C[4]) # 单词ID为4的向量
# [0 1 0 1 0 0 0]
print(C[word_to_id['goodbye']]) # goodbye的向量
# [0 1 0 1 0 0 0]
共现矩阵的函数: 
create_co_matrix(corpus, vocab_size, window_size=1)
其中参数 corpus 是单词 ID 列表
参数 vocab_ size 是词汇个数
window_size 是窗口大小
代码实现:
def create_co_matrix(corpus, vocab_size, window_size=1):
    '''生成共现矩阵
    :param corpus: 语料库(单词ID列表)
    :param vocab_size:词汇个数,重复的单词算成一个
    :param window_size:窗口大小(当窗口大小为1时,左右各1个单词为上下文)
    :return: 共现矩阵
    '''
    corpus_size = len(corpus)        #单词总数,包括重度的单词
    co_matrix = np.zeros((vocab_size, vocab_size), dtype=np.int32)
    for idx, word_id in enumerate(corpus):
        for i in range(1, window_size + 1):
            left_idx = idx - i
            right_idx = idx + i
            if left_idx >= 0:
                left_word_id = corpus[left_idx]
                co_matrix[word_id, left_word_id] += 1
            if right_idx < corpus_size:
                right_word_id = corpus[right_idx]
                co_matrix[word_id, right_word_id] += 1
    return co_matrix


 


拓:
PPMI: 可以改进共现矩阵


共现矩阵的缺陷:


共现矩阵的元素表示两个单词同时出现的次数,这里的次数并不具备好的性质,举个例子,有短语叫the car,因为the是个常用词,如果以两个单词同时出现的次数为衡量相关性的标准,与drive 相比,the和car的相关性更强,这是不对的。


PMI(简单称呼为:点互信息)介绍:


点互信息(Pointwise Mutual Information,PMI):表达式如下,P(x)表示x发生的概率,P(y)表示y发生的概率,P(x,y)表示x和y同时发生的概率。PMI的值越高,表明x与y相关性越强。
通俗讲,PMI就是求两个事件同时发生的概率有多大,他们发生的概率越大,则PMI的值就越大的嘞,也就表示两个事件的关联性就越强,放在男女之间,就说明她和他的关系就越亲密的呢,就好比令人神往的青梅竹马,流口水哇,小编也想要哇(努力加油哇!)。
下图是数学原理图:


由此可见,点互信息可以帮助我们将the和car的相关性降低,并且提高drive和car的相关性,从而修正出我们想要的结果。
但是,如果两个单词的共现次数为0时,就会出现log₂0 = -∞
的尴尬,为了解决这个奇葩,所以我们采用正的点互信息,
见名知意,就是只是用正的数据,不会使用负数,PPMI可以将负数转化成0,这样就可以轻松的处理掉这个奇葩啦!


PPMI:(正的点互信息)


正点互信息只是比点互信息多了一个判断最大值的操作,小于0的值都改成了0
共现矩阵转化成PPMI矩阵的函数代码实现:

def ppmi(C, verbose=False, eps = 1e-8):
    '''生成PPMI(正的点互信息)
    :param C: 共现矩阵
    :param verbose: 是否输出进展情况
    :return:
    '''
    M = np.zeros_like(C, dtype=np.float32)
    N = np.sum(C)
    S = np.sum(C, axis=0)
    total = C.shape[0] * C.shape[1]
    cnt = 0
    for i in range(C.shape[0]):
        for j in range(C.shape[1]):
            pmi = np.log2(C[i, j] * N / (S[j]*S[i]) + eps)
            M[i, j] = max(0, pmi)
            if verbose:
                cnt += 1
                if cnt % (total//100 + 1) == 0:
                    print('%.1f%% done' % (100*cnt/total))
    return M



注解:
verbose 是决定是否输出运行情况的标志
当处理大语料库时,设置 verbose=True,可以用于确认运行情况。在这段代码 中,为了仅从共现矩阵求 PPMI 矩阵而进行了简单的实现。


设置共现矩阵代码:

import numpy as np
C = np.array([
 [0, 1, 0, 0, 0, 0, 1],
 [1, 0, 1, 0, 1, 1, 0],
 [0, 1, 0, 1, 0, 0, 0],
 [0, 0, 1, 0, 1, 0, 0],
 [0, 1, 0, 1, 0, 0, 0],
 [0, 1, 0, 0, 0, 0, 1],
 [0, 0, 0, 0, 0, 1, 0],
], dtype=np.int32)
N = np.sum(C)  #15
S = np.sum(C, axis=0) #array([1, 4, 2, 2, 2, 2, 2])



执行函数代码:

import sys
sys.path.append('..')
import numpy as np
from common.util import preprocess, create_co_matrix, cos_similarity, ppmi
text = 'You say goodbye and I say hello.'
corpus, word_to_id, id_to_word = preprocess(text)
vocab_size = len(word_to_id)
C = create_co_matrix(corpus, vocab_size)
W = ppmi(C)
np.set_printoptions(precision=3)  # 有效位数为3位
print('covariance matrix')
print(C)
print('-'*50)
print('PPMI')
print(W)
输出结果:
covariance matrix
[[0 1 0 0 0 0 0]
 [1 0 1 0 1 1 0]
 [0 1 0 1 0 0 0]
 [0 0 1 0 1 0 0]
 [0 1 0 1 0 0 0]
 [0 1 0 0 0 0 1]
 [0 0 0 0 0 1 0]]
--------------------------------------------------
PPMI
[[0.    1.807 0.    0.    0.    0.    0.   ]
 [1.807 0.    0.807 0.    0.807 0.807 0.   ]
 [0.    0.807 0.    1.807 0.    0.    0.   ]
 [0.    0.    1.807 0.    1.807 0.    0.   ]
 [0.    0.807 0.    1.807 0.    0.    0.   ]
 [0.    0.807 0.    0.    0.    0.    2.807]
 [0.    0.    0.    0.    0.    2.807 0.   ]]
Process finished with exit code 0


总结:

优点:


PPMI 矩 阵的各个元素均为大于等于 0 的实数。我们得到了一个由更好的指标形成的 矩阵,这相当于获取了一个更好的单词向量。


缺陷:


但是,这个 PPMI 矩阵还是存在一个很大的问题,那就是随着语料库 的词汇量增加,各个单词向量的维数也会增加。如果语料库的词汇量达到 10 万,则单词向量的维数也同样会达到 10 万。实际上,处理 10 万维向量 是不现实的。
降维
所谓降维,就是将高维转化为低维,将复杂的数据转换为简单的数据,目的就是将问题简单化,将价值低的数据筛选处理掉,保留重要信息,简称:去其糟粕,取其精华。
实例:
向量中的大多数元素为 0 的矩阵(或向量)称为稀疏矩阵(或稀疏向 量)。这里的重点是,从稀疏向量中找出重要的轴,用更少的维度对 其进行重新表示。结果,稀疏矩阵就会被转化为大多数元素均不为 0 的密集矩阵。这个密集矩阵就是我们想要的单词的分布式表示。
图例:


SVD:奇异值分解


接下来,我们使用 Python 来实现 SVD,这里可以使用 NumPy 的 linalg 模块中的 svd 方法。
linalg 是 linear algebra(线性代数)的简称。(下方代码会用到)
下 面,我们创建一个共现矩阵,将其转化为 PPMI 矩阵,然后对其进行 SVD

import sys
sys.path.append('..')
import numpy as np
import matplotlib.pyplot as plt
from common.util import preprocess, create_co_matrix, ppmi
text = 'You say goodbye and I say hello.'
corpus, word_to_id, id_to_word = preprocess(text)
vocab_size = len(id_to_word)
C = create_co_matrix(corpus, vocab_size, window_size=1)
W = ppmi(C)
# SVD
U, S, V = np.linalg.svd(W)
np.set_printoptions(precision=3)  # 有效位数为3位
 # 共现矩阵
print(C[0])        # [0 1 0 0 0 0 0]
 # PPMI矩阵
print(W[0])        #[0.    1.807 0.    0.    0.    0.    0.   ]
# SVD
print(U[0])    #[-3.409e-01 -1.110e-16 -3.886e-16 -1.205e-01  0.000e+00  9.323e-012.226e-16]
# plot
for word, word_id in word_to_id.items():
    plt.annotate(word, (U[word_id, 0], U[word_id, 1]))
plt.scatter(U[:,0], U[:,1], alpha=0.5)
plt.show()


结论:


原先的稀疏向量 W[0] 经过 SVD 被转化成了密集向量 U[0]。如果要对这个密集向量降维,比如把它降维到二维向量,取出前两个元素 即可。
代码观测:

print(U[0, :2])
# [ 3.409e-01 -1.110e-16]



3.1.2  基于推理的方法概要


推理模型出现概率统计:

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


最终得出结论:
用向量表示单词具体位置的哈


3.2 简单的wordvec


Wordvec:最初指程序或者工具
但随着时代发展,某些情况下也指:神经网络模型
具体点,是指CBOW模型和skip-gram模型的神经网络


3.2.1  CBOW模型的推理


总的来说,CBOW模型就是一个可以根据上下文预测单词的神经网络的哈。(是不是有点类似英语那神秘莫测的语感,高中时,英语语感做题,不就是凭借上下文来轻松选出正确选项吗?不是类似,而是神似呢)


上面的灰度最深的部分就是与you和goodbye都共同拥有的概率最大的单词啦


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 模型的学习就是调整权重,以使预测准确。其结果是,权重 Win(确切地说是 Win 和 Wout 两者)学习到蕴含单词出现模式的向量。根据过去的实验,CBOW 模型(和 skip-gram 模型)得到的单词的分布式表示,特别是使用维基百科等大规模语料库学习到的单词的分布式表示,在单词的含义和语法上符合我们直觉的案例有很多.
CBOW模型只是学习语料库中单词的出现模式。如果语料库不一样, 学习到的单词的分布式表示也不一样。比如,只使用“体育”相关 的文章得到的单词的分布式表示,和只使用“音乐”相关的文章得 到的单词的分布式表示将有很大不同。
结论:CBOW学习到内容并不一定可以适用于全部数据,例如从游戏语言和音乐语言,它们学习到的数据就有可能有很大的不同,比如可能会出现:预测的正确率下降的嘞。


3.2.3 Word2vec的权重和分布式表示


就 word2vec(特别是 skip-gram 模型)而言,最受欢迎的是方案 A。遵循这一思路,我们也使用 Win 作为单词的分布式表示
个人理解:我认为是原汁原味的原因,所以选择了A的,因为输出的数据是综合的两个数据的平均,所以可能对原始数据有所破坏的嘞!


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表示


理解:(6,2,7)是6行2列然后最里面的中括号里面有七个元素
2也可以理解为是2维的意思的呢,啥是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模型的实现


原理图:


简单的CBOW的代码实现:

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

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

庆祝,反向传播的实现至此就结束啦!


我们已经将各个权重参数的梯度保存在了变量:grads中啦
然后呢,先调用forward()函数,再调用backward()函数,grads列表中的梯度就会被更新啦!
接下来,再看看SimpleCBOW类的学习
学习的实现:
(使用的还是第一章所用到的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]


由图可知:从咱们小型的语料库中进行实现,效果并不好的,为啥子?你看哇,纵轴的loss虽然降低啦,但是你再看看波动率,是真的很大哇,你愿意和一个忽冷忽热的伴侣在一起吗?不愿意哇!这个也是如此
当 然,主要原因是语料库太小了。如果换成更大、更实用的语料库,相信会获 得更好的结果。
但是,还有一个不足:
处理的效率太低了,速度真的很慢很慢的嘞。
等待,无意等于谋财害命,这是赤裸裸的慢性自杀!
所以呢,我们可以完善改进一下这个简单的CBOW模型,让它变成一个真正的"CBOW".(放飞烦恼。做真正的自己!)
如何见证CBOW做回自己,敬请期待下一章节娓娓道来!


拓展:


skip-gram模型简单叙述:它其实就是CBOW的反转,也是这位兄弟的互补,它比CBOW更加强大,因为,它是通过自己一个单词预测它周围的单词的嘞(挠挠小脑袋,这不就是搜狗输入法的语句联想功能嘛!
比如:我输入“我爱”,输入法就会根据我经常输入的内容,联想到周围的词汇:"这家伙莫非又是向那位倾慕已久的姑娘表白?好吧,看我的智能联想,给主人脑补下一句,哈哈,我可真聪明哇!")

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值