本文为稀土技能社区首发签约文章,30天内制止转载,30天后未获授权制止转载,侵权必究!

作者简介:秃头小苏,致力于用最浅显的言语描绘问题

专栏引荐:深度学习网络原理与实战

近期方针:写好专栏的每一篇文章

支持小苏:点赞、收藏⭐、留言

CVer从0入门NLP(一)———词向量与RNN模型

写在前面

Hello,咱们好,我是小苏

之前的博客中,我都为咱们介绍的是核算机视觉的常识,今天预备和咱们唠唠NLP的内容。其实呢,关于NLP,我也是初学者,之前只是有一个大约的了解,所以本系列会以一个初学者的视角带咱们走进NLP的国际,如果博客中有解说不到位的地方,期望各位大佬纠正。

当然了,NLP的内容许多,你如果在网上搜NLP学习道路的话你会看的眼花缭乱,本系列首要会介绍一些重要的常识点,一些前史长远的模型就不介绍了,我个人觉得用途不大,咱们的方针是像经典模型看齐,如GPT系列,BERT宗族等等。

本系列目前预备先分三节介绍,后边会渐渐弥补新内容。第一节先从词向量为切入点,然后介绍RNN模型并手撸一个RNN;第二节会介绍RNN的改善LSTM及ELMO模型;第三节会详细介绍GPT和BERT,以及它们的相同点和不同点。

词向量

咱们知道,NLP任务中咱们处理的对象是一个个的词,可是核算机根本不知道咱们的词啊,需求将其转换为合适核算机处理的数据类型。一种常见的做法是独热编码(one-hot编码),假定咱们现在要对“秃”、“头”,“小”,“苏”四个字进行独热编码,其成果如下:

CVer从0入门NLP(一)———词向量与RNN模型

​ 能够看出,上图能够用一串数字表明出“秃”、“头”,“小”,“苏”这四个汉字,如用1 0 0 0表明“秃”,用0 1 0 0表明“头”……

​ 可是这种表明办法是否存在缺点呢?咱们都能够思考思考,我给出两点如下:

  1. 这种编码办法关于我这个事例来说貌似是还蛮不错的,可是咱们有没有想过,关于一个文本翻译任务来说,往往里边有许多许多的汉字,假定有10000个,那么一个独自的字,如“秃”就需求一个110000维的矩阵来表明,而且矩阵中有9999个0,这无疑是对空间的一种糟蹋。
  2. 这种编码办法无法表明两个相关单词的联络,如“秃”和“头”这两个单词显着是有某种内在的联络的,可是独热编码却无法表明这种联络【余弦类似度为0,后文对余弦类似度有介绍】。

基于以上的两点,我觉得咱们的对词的编码应该契合以下几点要求:

  1. 咱们能够将词表明为数字向量。
  2. 咱们尽或许的节约空间的耗费。
  3. 咱们能够轻松核算向量之间的类似程度。

咱们先来看这样的一个比方,参阅:The Illustrated Word2vec

现在正值秋招大好时机,咱们的作业都找的怎样样了腻,祝咱们都能找到令自己满足的作业。在投简历的过程中,咱们会发现许多公司都会有性情测验这一环节,这个测验会咨询你一系列的问题,然后从多个维度来对你的性情做全面剖析。其中,测验测验者的内向或外向往往是测验中的一个维度,假定我(Jay)的内向/外向得分为38(满分100),则咱们能够制作下图:

CVer从0入门NLP(一)———词向量与RNN模型

为了更好的表明数据,咱们将数据限制到-1~1范围内,如下:

CVer从0入门NLP(一)———词向量与RNN模型

这样咱们就能够对Jay这个人是否外向做一个大致的点评,可是人是杂乱的,只是从一个维度来剖析一个人的性情肯定是不精确的,因而,咱们再来增加一个维度来归纳点评Jay这个人的性情特点:

CVer从0入门NLP(一)———词向量与RNN模型

能够看到,现在咱们就能够从两个维度来描绘Jay这个人了,在上图的坐标系中便是一个坐标为(-0.4,0.8)的点,或许说是从原点到(-0.4,0.8)的向量。当然了,如何还有他人有这样的两个维度,我就能经过比较他们的向量来表明他们的类似性。

CVer从0入门NLP(一)———词向量与RNN模型

从上图能够和显着的看出,Person1和Jay更像,可是这是咱们直观的感触,咱们可不能够经过数值来反应他们之间的类似度呢,当然能够,一种常见的核算类似度的办法是余弦类似度cosine_similarity,成果如下:

CVer从0入门NLP(一)———词向量与RNN模型

不知道咱们知不知道核算余弦类似度,这儿简略介绍一下:

余弦类似度是一种用于衡量两个向量之间类似性的度量办法,通常在自然言语处理和信息检索等范畴广泛运用。它核算两个向量之间的夹角余弦值,值越挨近1表明两个向量越类似,值越挨近-1表明两个向量越不类似,值挨近0表明两个向量之间没有显着的类似性。

余弦类似度的核算公式如下:

余弦类似度=A⋅B∣∣A∣∣∣∣B∣∣\frac{A \cdot B}{||A||||B||}

其中:

  • AB 是要比较的两个向量。
  • A⋅BA \cdot B 表明向量**AA**与向量 **BB**的点积(内积)。
  • **∣∣A∣∣||A||**和 ∣∣B∣∣||B|| 别离表明向量 **AA**与向量 **BB**的范数(模)。

能够来简略举个比方:

假定有两个向量 A=[2,3]A=[2,3]B=[1,4]B=[1,4]。咱们来核算它们之间的余弦类似度:

A⋅B=(21)+(34)=14A \cdot B=(21)+(34)=14

∣∣A∣∣=22+32=13||A||=\sqrt{2^2+3^2}=\sqrt{13}

∣∣B∣∣=12+42=17||B||=\sqrt{1^2+4^2}=\sqrt{17}

则:余弦类似度=A⋅B∣∣A∣∣∣∣B∣∣=141317≈0.86\frac{A \cdot B}{||A||||B||}=\frac{14}{\sqrt{13}\sqrt{17}}\approx0.86

上面展现的是从两个维度刻画一个人的性情,可是在实际中比两维更多,国外心理学家研讨了五个首要品格,所以咱们能够将上面的二维扩展到五维,如下图所示:

CVer从0入门NLP(一)———词向量与RNN模型

显然,现在咱们有五个维度的数据,咱们无法经过平面向量的方式来调查不同人物之前的类似性,可是咱们依然能够核算他们之前的类似度,如下:

CVer从0入门NLP(一)———词向量与RNN模型


经过上面的性情测评小比方,我想告诉咱们的是咱们能够把诸如”外向/内向”、“自卑/自傲”等性情特征表述成向量的方式,而且每个人都能够用这些种向量方式表明,一起咱们能够依据这种向量的表述来核算每个人之前的类似度。

相同的道理,人能够,那么词也能够,咱们把一个个词表明成这样的向量方式,这种向量表明方式便是词向量。那么词向量究竟长什么样呢?咱们一起来看看“King”这个词的词向量(这是在维基百科上练习好的),如下:

[ 0.50451 , 0.68607 , -0.59517 , -0.022801, 0.60046 , -0.13498 , -0.08813 , 0.47377 , -0.61798 , -0.31012 , -0.076666, 1.493 , -0.034189, -0.98173 , 0.68229 , 0.81722 , -0.51874 , -0.31503 , -0.55809 , 0.66421 , 0.1961 , -0.13495 , -0.11476 , -0.30344 , 0.41177 , -2.223 , -1.0756 , -1.0783 , -0.34354 , 0.33505 , 1.9927 , -0.04234 , -0.64319 , 0.71125 , 0.49159 , 0.16754 , 0.34344 , -0.25663 , -0.8523 , 0.1661 , 0.40102 , 1.1685 , -1.0137 , -0.21585 , -0.15155 , 0.78321 , -0.91241 , -1.6106 , -0.64426 , -0.51042 ]

这一共有50个数字,即表明咱们选择了50个维度的特征来表明“king”这个词,也即这个向量表明“king”这个词。相同的道理,其他单词也会有归于他们自己的向量表明,方式和上面的是相同的,都是50维,可是里边详细的值不同。为了便利展现不同词之间的联络,咱们将表明“king”的词向量换一种办法展现,依据其值的不同标记成不同的色彩(若数值挨近2,则为红色;挨近0,则为白色;挨近-2,则为蓝色),如下图:

CVer从0入门NLP(一)———词向量与RNN模型

当然了,咱们用相同的道理,会得到其它词的词向量表明,如下:

CVer从0入门NLP(一)———词向量与RNN模型

能够看到,“Man”和“Woman”之前的类似程度好像比它们和“King”之前的类似程度高,这也是契合咱们直觉的,即“Man”和“Woman”之前的联络好像比较大。

这就阐明,经过把词变成词向量之后,咱们能够发现不同词之前的相关程度了。这儿你或许会问了,怎样把词变成词向量呢?不急,咱们立刻回答。

咱们再拿咱们一开端“秃”、“头”,“小”,“苏”四个字为例,咱们运用独热编码编码这四个字后,它们之间的余弦类似度都为0,无法表明它们之间的相关程度,因而运用独热编码作为词向量作用欠好。那么改运用什么呢,一种或许的计划是Word Embedding。咱们先来说说经过Word Embedding能够到达什么样的作用,相同拿“秃”、“头”,“小”,“苏”四个字为例,运用Word Embedding后它们的散布是这样的:

CVer从0入门NLP(一)———词向量与RNN模型

即“秃”和“头”在某个空间中离的比较近,阐明这两个词的相关性较大。即Word Embedding能够从较高的维度去考虑一些词,那么会发现一些词之前存在某种相关。

那么如何进行Word Embedding,如何得到咱们的词向量呢?首要我需求让咱们知道到一点,进行Word Embedding,其实重点便是寻觅一个合适的矩阵Q。然后将咱们之前的one hot编码乘上Q,,比方“秃”的one hot 编码是1 0 0 0,假定咱们寻觅到了一个矩阵Q,

CVer从0入门NLP(一)———词向量与RNN模型
那么咱们将它们两个相乘,就得到了“秃”的词向量:

词向量“秃”:

CVer从0入门NLP(一)———词向量与RNN模型
同理,咱们能够得到其它几个词的词向量:

CVer从0入门NLP(一)———词向量与RNN模型

好了,到这儿你或许了解了咱们的方针便是寻觅一个改变矩阵Q。那么这个Q又是怎样寻觅的呢,其实呢,这个Q矩阵是练习出来的。一开端,有一种神经网络言语模型,叫做NNLM,它在完结它的任务的时候产生了一种副产物,这个副产物便是这个矩阵Q。【这儿咱们不细讲了,咱们感兴趣的去了解一下,材料许多】后边人们发现这个副产物挺好用,因为能够进行Word Embedding,将词变成词向量嘛。于是科研人员就进一步研讨,规划出了Word2Vec模型,这个模型是专门用来得到这个矩阵Q的。【后边咱们也叫这个矩阵Q为Embedding矩阵】

Word2Vec模型有两个结构,如下:

  • CBOW,这种模型类似于完型填空,核心思想是把一个语句中间的某个词挡住,然后用这个词的上下文单词去猜测这个被挡住的词。
  • Skip-gram,这个和CBOW结构刚好相反,它的核心思想是依据一个给定的词去猜测这个词的上下文。

它们的区别能够用下图表明:

CVer从0入门NLP(一)———词向量与RNN模型

至于它们详细是怎样完成的我不计划讲,感兴趣的能够去搜搜。我简略说说它的思路:在它们练习时,首要会随机初始化一个Embedding表和Context表,然后咱们会依据输入单词去查找两个表,并核算它们的点积,这个点击表明输入和上下文的类似程度,接着会依据这个类似程度来规划丢失函数,最终依据丢失不断的调整两个表。当练习完结后,咱们就得到了咱们的Embedding表,也便是Q矩阵。

RNN模型

上一小节咱们介绍了词向量,它处理的是咱们NLP任务中输入问题。下面咱们将一起来唠唠NLP任务中的常见模型。

RNN模型结构

RNN(循环神经网络)我想咱们多少都有所耳闻吧,它首要用于处理时序问题,例如时间序列、自然言语文本、音频信号等。

话不多说,咱们直接来看RNN的模型图,如下:

CVer从0入门NLP(一)———词向量与RNN模型

啊,什么,这这点!!!?你或许感到震惊,RNN的模型结构就这么点儿???是的,没错,就这些。首要,它有一个输入XtX_t,这是一个序列输入,比方某时间的输入为xix_ixix_i会输入到模块A中【留意:这儿不止一个输入,还会有一个输入hi−1h_{i-1}一起送入模块A】,然后模块A输出一个值hih_i。接着会将输出hih_i和下一个输入xi+1x_{i+1}送入模块A,得到输出hi+1h_{i+1}。【留意:最根底的RNN的输出和hth_t是相同的】重复上面的过程,便是RNN啦。

上面的图用一个循环表明RNN,其实看起来仍是比较不舒服,那么咱们把这个循环展开,其结构就会比较明晰了,如下图所示:

CVer从0入门NLP(一)———词向量与RNN模型

知道了RNN的大体结构,我觉得你或与会对模块A的结构很敢兴趣,那我劝你不要太敢兴趣。因为模块A真的很简略,便是一个tanh层,如下:

CVer从0入门NLP(一)———词向量与RNN模型

enmmmm,便是这么简略,如果你对此结构还存有疑问的话,那么字写看看后文的代码手撸RNN部分,或许能处理你的大部分疑问。

到这儿,其实RNN的模型结构就讲完了,是不是很简略呢。那么下面讲什么呢?自然是RNN存在什么问题,这样才能过渡到后边更加牛*的网络嘛。

那么RNN存在什么问题呢?那便是长间隔依靠问题,何为长间隔依靠呢?他和短间隔依靠是相对的概念,咱们来举个比方来介绍什么是长间隔依靠,什么是短间隔依靠:

  • 关于这样一句话:“我爱在足球场上踢__”,咱们是不是很简略得到空格里的答案,因为在空格前几个字有足球场,所以咱们知道这儿要填“足球”。这种能依据上下文附近就判断猜测答案的便是短间隔依靠。【短间隔依靠的图示如下】

    CVer从0入门NLP(一)———词向量与RNN模型
  • 关于这样一句话:“我爸爸从小就带我去足球场踢足球,我的喜好便是足球。我和爸爸联络非常好,常常带我一起玩耍,…….,真是一个巨大的父亲。长大后,我的喜好一向没变,现在我就要去踢__”,咱们感触到了嘛,这儿空格中要填的词咱们要往上文找很就才能够发现,这种猜测答案需求看上文很远间隔找到答案的便是长间隔依靠。

    CVer从0入门NLP(一)———词向量与RNN模型

也便是说,RNN网络关于长间隔依靠的问题作用很欠好,因而咱们后边会对RNN网络进行改善,进而进步其对长间隔依靠的才能。

手撸RNN

想必咱们经过上文的叙述,现已对RNN的代码结构有了一定的知道,下面咱们就来运用Pytorch来完成一个RNN网络,让咱们对其有一个更加明晰的知道。

这部分的思路是这样的,我先给咱们调用一下官方封装好的RNN模型,展现模型输入输出的成果;然后再手撸一个RNN函数,来验证其成果是否和官方共同。

好了,咱们就先来运用官方界说好的RNN模型来完成,详细能够看这个衔接:RNN

import torch
import torch.nn as nn
bs, T = 2, 3   #批巨细,输入序列长度
input_size, hidden_size = 2, 3  # 输入特征巨细,隐含层特征巨细
input = torch.randn(bs, T, input_size) # 随机初始化一个输入特征序列
h_prev = torch.zeros(bs, hidden_size)  # 初始隐含状态

咱们先来打印看一下inputh_prev以及它们的shape,如下:

CVer从0入门NLP(一)———词向量与RNN模型

咱们来解说一下这些变量,input便是咱们输入的数据,他的维度为(2, 3, 2),三个维度别离表明(bs, T, input_size),即(批巨细,输入序列长度,输入特征巨细)。我这样介绍咱们或许还一头雾水,我结合input的打印成果给咱们介绍,首要很显着这是一个维度为(2, 3, 2)的向量,这个咱们都知道哈,不知道我就真没办法啦,去补补课吧。那么这个向量的第一个维度是2,就代表咱们1个batch有两条数据,每个都是(3, 2)维度的向量,如下:

CVer从0入门NLP(一)———词向量与RNN模型

这个和核算机视觉中的bs(batch_size)是一个意思啦,接下来咱们来看每条数据,即这个(3,2)维的向量,以第一条为例:这个3表明输入序列长度,表明每条数据又有三个小部分构成,别离为[-0.0657, -0.9015]、[-0.0324, -0.5666]、[-0.2630, 2.4861]。这是什么意思呢,这表明咱们的输入会分三次送入RNN网络中,别离是x0、x1、x2x_0、x_1、x_2,不知道这样咱们能否了解,我画个图咱们就知道了,如下:

CVer从0入门NLP(一)———词向量与RNN模型

咱们或许发现了,这个维度的3个数据就相当于3个词,别离一步步的送入RNN网络中,那么其实最终一个维度2,也便是输入特征巨细也很好了解了,它就表明每个词的维度,便是咱们前文所说的词向量,那么咱们这儿便是每个词向量有两个维度的特征。

经过上文的介绍,我想咱们了解input这个输入了,那么h_prev是什么呢,其是隐层的输出,也便是上图中的h0、h1、h2h_0、h_1、h_2

接着咱们就来调用pytorch中RNN的API:

# 调用pytorch RNN API
rnn = nn.RNN(input_size, hidden_size, batch_first=True)
rnn_output, state_final = rnn(input, h_prev.unsqueeze(0))

batch_first=True这个参数是界说咱们输入的格局为(bs, T, input_size)的,pytorch文档中都解说的很详细,咱们自己去看一下就好。至于这个h_prev.unsqueeze(0)这儿加了第一个维度,这是因为RNN API的输入要求是三维的向量,如下:

CVer从0入门NLP(一)———词向量与RNN模型

咱们来看看输出的rnn_outputstate_final的值和shape吧,如下:

CVer从0入门NLP(一)———词向量与RNN模型

rnn_output其实便是每个躲藏层的输出,而state_final则是终究的输出,在根底的RNN中,state_final的值就等于最终一个躲藏层的输出,咱们从数值上也能够发现,如下:

CVer从0入门NLP(一)———词向量与RNN模型

为了便利咱们了解,再画一个图,如下:【留意:图都是以batch中一条数据为例表明的】

CVer从0入门NLP(一)———词向量与RNN模型


那么上文就为咱们介绍了如何运用pytorch官方API完成RNN,可是这样咱们无法看到RNN内部是如何完成的,那么这样咱们就来手动完成一个RNN。其实很简略,首要便是用到了一个公式,如下:

CVer从0入门NLP(一)———词向量与RNN模型

这个公式能够在pytorch官方文档中看到,其实不知道咱们发现没有,其实这个公式和卷积神经网络的公式是很像的,只不过RNN这儿有两个输入而已。还有一点和咱们说一下,上图公式中含有转置,完成起来转置来转置去的会很绕,上面的公式其实和下面是相同的【上下两个xtx_t维度其实变了】:

CVer从0入门NLP(一)———词向量与RNN模型

为了简便起见,我用不带转置的进行代码编写,咱们先了解好这个,最终我也会把带转置的代码放出来,这时候了解带转置的或许更简略点。

# 手写一个rnn_forward函数,完成RNN的核算原理
def rnn_forward(input, weight_ih, weight_hh, bias_ih, bias_hh, h_prev):
    bs, T ,input_size = input.shape
    h_dim = weight_ih.shape[0]
    h_out = torch.zeros(bs, T, h_dim)
    for t in range(T):
        x = input[:,t,:].unsqueeze(2)    
        w_ih_batch = weight_ih.unsqueeze(0).tile(bs, 1, 1)
        w_hh_batch = weight_hh.unsqueeze(0).tile(bs, 1, 1)
        w_times_x = torch.bmm(x.transpose(1, 2), w_ih_batch.transpose(1, 2)).transpose(1, 2).squeeze(-1)
        w_times_h = torch.bmm(h_prev.unsqueeze(2).transpose(1, 2), w_hh_batch.transpose(1, 2)).transpose(1, 2).squeeze(-1)
        h_prev = torch.tanh(w_times_x + bias_ih + w_times_h + bias_hh)
        h_out[:,t,:] = h_prev
    return h_out, h_prev.unsqueeze(0)

咱们看到代码并不长,所以其实仍是很简略的,最首要的是咱们留意for t in range(T)这个循环,便是不断的取输入序列中的向量送入RNN网络,比方开端是x0x_0送入、接着是x1x_1送入……依次类推,后边的几行代码都是围绕ht=tanh⁡(Wihxt+bih+Whhh(t−1)+bhh)h_{t}=\tanh \left(W_{i h} x_{t}+b_{i h}+W_{h h} h_{(t-1)}+b_{h h}\right)进行编写的,详细的细节咱们渐渐调试吧,信任难不住你。因为规划到许多向量运算,所以特别要留意维度的改变。

接下来咱们要验证一下咱们完成的RNN是否正确,可是咱们需求传入Wih、bih、Whh、bhhW_{ih}、b_{ih}、W_{hh}、b_{hh}参数,这几个参数怎样得到呢,咱们能够在rnn中看到这几个参数的值,咱们也只有用这个才能确保咱们最终的成果和官方的共同,咱们能够来简略看看这几个值,如下:

CVer从0入门NLP(一)———词向量与RNN模型

接着咱们就能够将这儿边的参数传入到rnn_forward函数中,如下:

custom_rnn_output, custom_state_final = rnn_forward(input, rnn.weight_ih_l0, rnn.weight_hh_l0, rnn.bias_ih_l0,  rnn.bias_hh_l0, h_prev)

相同,咱们来打印一下custom_rnn_output和custom_state_final,如下:

CVer从0入门NLP(一)———词向量与RNN模型

经过对比,你能够发现,运用官方API和运用咱们自界说的函数完成的RNN的输出是相同,这就验证了咱们办法的正确性。

下面给出带转置的,即ht=tanh⁡(xtWihT+bih+ht−1WhhT+bhh)h_{t}=\tanh \left(x_{t} W_{i h}^{T}+b_{i h}+h_{t-1} W_{h h}^{T}+b_{h h}\right)这个表达式的代码供咱们参阅,如下:

# custom 手写一个rnn_forward函数,完成RNN的核算原理
def rnn_forward(input, weight_ih, weight_hh, bias_ih, bias_hh, h_prev):
    bs, T, input_size = input.shape
    h_dim = weight_ih.shape[0]
    h_out = torch.zeros(bs, T, h_dim)
    for t in range(T):
        x = input[:, t, :].unsqueeze(2)
        w_ih_batch = weight_ih.unsqueeze(0).tile(bs, 1, 1)
        w_hh_batch = weight_hh.unsqueeze(0).tile(bs, 1, 1)
        w_times_x = torch.bmm(x.transpose(1, 2), w_ih_batch.transpose(1, 2)).transpose(1, 2).squeeze(-1)
        w_times_h = torch.bmm(h_prev.unsqueeze(2).transpose(1, 2), w_hh_batch.transpose(1, 2)).transpose(1, 2).squeeze(-1)
        h_prev = torch.tanh(w_times_x + bias_ih + w_times_h + bias_hh)
        h_out[:, t, :] = h_prev
    return h_out, h_prev.unsqueeze(0)

参阅衔接

1、The Illustrated Word2vec

2、了解 LSTM 网络

3、Transformer浅显笔记:从Word2Vec、Seq2Seq逐渐了解到GPT、BERT

4、Understanding LSTM Networks

5、预练习言语模型的前世今生

6、PyTorch源码教程与前沿人工智能算法复现讲解

如若文章对你有所帮助,那就

        

CVer从0入门NLP(一)———词向量与RNN模型