本文由罗周杨原创,转载请注明作者和出处。未经授权,不得用于商业用途

斯坦福经典NLP教材Speech and Language Processing-Vector Semantics
<https://web.stanford.edu/~jurafsky/slp3/6.pdf>学习笔记。

我们该如何表示一个单词的意思呢?你可能会想到其中的一种,用一个向量来表示一个单词!没错,这个章节就是讲单词的表示。

<>文档和向量

如果用向量来表示一个文档,该怎么表示呢?

假设现在有四个文档,我们统计各个单词在文档中出现的次数,可以得到一张表格:

- As You Like It Twelfth Night Julius Caesar Henry V
battle 1 0 7 13
good 114 80 62 89
fool 36 58 1 4
wit 20 15 2 3
当然实际上文档的单词数量远不止这几个。

上面表中,有4个单词,所以每一个文档可以表示成一个由单词频率组成的向量:
As You Like It ------> [ 1,114,36,20] Twelfth Night ------> [ 0, 80,58,15]
Julius Caesar ------> [ 7, 62, 1, 2] Henry V ------> [13, 89, 4, 3]
如果单词有很多个,假设是N,那么每个文档就可以表示成一个N维的向量。可见,这样的向量表示是稀疏的(sparse)。

<>单词和向量

除了文档可以表示成一个向量,单词也可以。

和文档类似,我们可以统计出一张表格,但是不同的是,我们不是统计单词的个数,而是统计两个单词出现在一起的频数。看一张表格你就知道了:

- aardvark … computer data pinch result sugar
apricot 0 … 0 0 1 0 1
pineapple 0 … 0 0 1 0 1
digital 0 … 2 1 0 1 0
information 0 … 1 6 0 4 0

这个表格是一个V×VV\times VV×V
的表格,每个数字表示当前列的单词出现在当前行单词后面的次数,这就构成了上下文,所以这个表格其实就是一个上下文矩阵,其中V就是总的词典的大小,也就是单词的数量。

我们取出每一行,就可以得到一个单词的向量表示,例如:
digital ------> [ 0,..., 2, 1, 0, 1, 0]
同样的,这样的表示也是稀疏的。

<>Cosine计算相似度

现在我们已经有文档或者单词的向量表示了,那么该如何计算它们之间的相似度呢?一个很常见的方法就是余弦相似度(Cosine similarity)。

学过高中数学就知道,两个向量的**点积(dot-product)或者内积(inner product)**可以由以下公式计算:

dot-produtc(v→,w→)=∑i=1Nviwi=v1w1+v2w2+⋯+vNwN
\text{dot-produtc}(\overrightarrow{v},\overrightarrow{w}) =
\sum_{i=1}^Nv_iw_i=v_1w_1+v_2w_2+\dots+v_Nw_Ndot-produtc(v,w)=i=1∑N​vi​wi​=v1​w1
​+v2​w2​+⋯+vN​wN​

而**向量的模(vector length)**为:

∣v→∣=∑i=1Nvi2\vert\overrightarrow{v}\vert = \sqrt{\sum_{i=1}^Nv_i^2}∣v∣=i=1∑N​v
i2​​

又:

a→⋅b→=∣a→∣∣b→∣cos⁡θ\overrightarrow{a}\cdot\overrightarrow{b} =
\vert{\overrightarrow{a}}\vert \vert{\overrightarrow{b}}\vert \cos\thetaa⋅b=∣a∣∣
b∣cosθ

即:

cos⁡θ=a→⋅b→∣a→∣∣b→∣\cos\theta =
\frac{\overrightarrow{a}\cdot\overrightarrow{b}}{\vert{\overrightarrow{a}}\vert
\vert{\overrightarrow{b}}\vert}cosθ=∣a∣∣b∣a⋅b​

所以,我们可以计算v→\overrightarrow{v}v和w→\overrightarrow{w}w的余弦值:

cos⁡(v→,w→)=v→⋅w→∣v→∣∣w→∣=∑i=1Nviwi∑i=1Nvi2∑i=1Nwi2
\cos(\overrightarrow{v},\overrightarrow{w}) =
\frac{\overrightarrow{v}\cdot\overrightarrow{w}}{\vert{\overrightarrow{v}}\vert
\vert{\overrightarrow{w}}\vert} =
\frac{\sum_{i=1}^Nv_iw_i}{\sqrt{\sum_{i=1}^Nv_i^2}\sqrt{\sum_{i=1}^Nw_i^2}}cos(v
,w)=∣v∣∣w∣v⋅w​=∑i=1N​vi2​​∑i=1N​wi2​​∑i=1N​vi​wi​​

所以,两个向量的余弦值越大,它们越相似。

<>TF-IDF

接下来就要介绍TF-IDF了,首先解释一下这个词:
TF-IDF = Term Frequency - Inverse Document Frequency
理解了名称,你就理解了一半!

那么什么是term-frequency呢?term-frequency就是单词在文档中出现的次数。

tft,d=1+log⁡10count(t,d)if count(t,d)>0else 0\text{tf}_{t,d} =
1+\log_{10}{\text{count}(t,d)} \quad\text{if }\text{count}(t,d) > 0
\quad\text{else } 0tft,d​=1+log10​count(t,d)if count(t,d)>0else 0

那么什么是IDF呢?首先我们弄清楚DF(document frequency)。

dft\text{df}_tdft​表示出现过这个单词的文档(document)的个数!

那么,IDF就是:

idft=Ndft\text{idf}_t = \frac{N}{\text{df}_t}idft​=dft​N​

其中,N就是一个集合(collection)中的documents数量。

为了避免数值过大,通常会取对数:

idft=log⁡10(Ndft)\text{idf}_t = \log_{10}(\frac{N}{\text{df}_t})idft​=log10​(df
t​N​)

至此,我们可以计算这个单词ttt的tf-idf权值:

wt,d=tft,d×idftw_{t,d} = \text{tf}_{t,d}\times\text{idf}_twt,d​=tft,d​×idft​

此时,我们的第一个表格,就变成了:

- As You Like It Twelfth Night Julius Caesar Henry V
battle 0.074 0 0.22 0.28
good 0 0 0 0
fool 0.019 0.021 0.0036 0.0083
wit 0.049 0.044 0.018 0.022
到目前为止,上面的所有向量表示都是稀疏的,接下来要介绍一种**稠密的(dense)**的向量表示——word2vec!

<>Word2Vec

这个大家应该很熟悉了,应该算是NLP领域的标配了。

我之前写过一篇word2vec的笔记自己动手实现word2vec(skip-gram模型)
<https://juejin.im/post/5b986f296fb9a05d11176a15>,但是其实还是很粗糙。Tensorflow也有一个教程
Vector Representations of Words
<https://www.tensorflow.org/tutorials/representation/word2vec>
,但是如果你没有一点基础的话,也还是有些概念难以理解。所以相对完整地理解word2vec,你需要结合多方面的资料。这个笔记在介绍斯坦福教材的同时,也会引入其他文章,做一些比较和思考,希望这个笔记能够给你带来相对全面的理解。

<>word embedding

首先我们解释一下词嵌入(word embedding)的概念。本小节之前的所有向量表示都是稀疏的,通常都是一个高维的向量,向量里面的元素大部分都是0。那么
embedding有什么不一样的呢?

Embedding同样也是用一个向量来表示一个词,但是它是使用一个较低的维度,稠密地表示。

如果使用之前的稀疏表示,你可能会这样表示hello这个词语:

hello⟶[0,0,0,1,2,0,…,0]⎵N个数\text{hello} \longrightarrow \quad\underbrace{[0,
0, 0, 1, 2, 0,\dots, 0]}_{N个数}hello⟶N个数[0,0,0,1,2,0,…,0]​​

那么,使用嵌入表示之后会是什么样子呢:

hello⟶[0.012,0.025,0.001,0.078,0.056,0.077,…,0.022]⎵n个数,一般是100到500左右
\text{hello} \longrightarrow
\quad\underbrace{[0.012,0.025,0.001,0.078,0.056,0.077,\dots,0.022]}_{n个数,一般是100到500左右}
hello⟶n个数,一般是100到500左右[0.012,0.025,0.001,0.078,0.056,0.077,…,0.022]​​

其中的差异一眼就看出来了。所以很明显,word embedding有好处:

* 不会造成维度爆炸,因为维度是我们自己设置的,通常比较小
* 向量是稠密的,不需要稀疏向量所采用的各种优化算法来提升计算效率
词嵌入理解了,那么什么是word2vec呢?其实就是把单词表示成固定维度的稠密的向量!说起来简单,但是也有很多小技巧的。

<>数据模型

假设我们有一个很大的文本语料,我们需要用这个语料来训练出单词的向量表示。那么该怎么训练呢?

当然你可能会想到基于计数的方式,就像前面几个小节一样,我们不说这个。

word2vec有两种常用的数据准备方式:

* CBOW,用前后词(context words)预测目标词(target word)
* skip-gram,用目标词(target word)预测前后词(context word)
使用tensorflow里面的例子:
the quick brown fox jumped over the lazy dog
举个例子,假设我们的**窗口大小(window size)**是2,目标词选择fox。

如果是skip-gram模型,我们会这样准备数据:
(fox, quick) (fox, brown) (fox, jumped) (fox, over)
也就是一个目标词,我们可以构造出window_size个训练数据对。

如果是CBOW模型,我们会这样准备数据:
([quick brown jumped over], fox)
看出其中的差异了吧?

总之,skip-gram和CBOW就是两个相反的数据模型。Learning Word Embedding
<https://lilianweng.github.io/lil-log/2017/10/15/learning-word-embedding.html>
有两张图可以分别表示两种模型的输入方式:

skip-gram模型

CBOW模型

数据模型应该清楚了。

与之前不同的是,word2vec并不关心相邻单词之前一起出现的频数,而是仅仅关心,这个单词是不是属于另一个单词的上下文(context)
!也就是说,word2vec不关系根据这个词预测出的下一个词语是什么,而是只关心这两个词语之间是不是有上下文关系。

于是,word2vec需要的仅仅是一个二分类器:“这个单词是另一个单词的上下文单词吗?”

所以,要训练一个word2vec模型,我们其实是在训练一个二分类器。而二分类器,你肯定很容易就想到了Logistic Regression
!关于逻辑回归,可以看我的另一篇笔记Logistic Regression
<https://luozhouyang.github.io/logistic_regression/>。

实际情况,skip-gram用的比较多,因为有一个说法,CBOW模型在小的数据集上面表现不错,在大的数据集里,skip-gram表现更好。

<>神经语言模型

这里需要说明进一步说明一下。Tensorflow里面有关于**神经概率语言模型(nerual probability language
model)**的描述。

传统的神经概率语言模型的训练通常是用**最大似然(maximum likelihood)**法则来最大化下一个词的softmax概率,基于前面的词,也就是:

P(wt∣h)=softmax(score(wt,h))=exp⁡score(wt,h)∑w’ in Vexp⁡score(w′,h)P(w_t\vert
h) = \text{softmax}(\text{score}(w_t,h)) =
\frac{\exp{\text{score}(w_t,h)}}{\sum_{\text{w' in
V}}\exp{\text{score}(w',h)}}P(wt​∣h)=softmax(score(wt​,h))=∑w’ in V​exp
score(w′,h)expscore(wt​,h)​

其中,score(wt,h)\text{score}(w_t,h)score(wt​,h)其实就是wtw_twt​和hhh的
点积(dot-production)。

那么这样训练模型的目标就是,最大化对数似然概率(log likelihood):

JML=log⁡P(wt∣h)=score(wt,h)−log⁡(∑w’ in Vexp⁡score(w′,h))J_{\text{ML}} =
\log{P(w_t\vert h)} = \text{score}(w_t,h) - \log(\sum_{\text{w' in
V}}\exp{\text{score}(w',h)})JML​=logP(wt​∣h)=score(wt​,h)−log(w’ in V∑​exp
score(w′,h))

那么这样会有什么问题吗?计算量太大了,因为在每一个训练步里,需要对词典里的每一个词,使用softmax计算出一个概率值!这个模型如下图所示:

正如前面所说,我们的word2vec并不需要一个完整的概率模型,我们只需要训练一个二分类器,从k个噪声单词(noise words)里面判别出正确的
目标词(target words)。这k个噪声单词是随机选择出来的,这个技术叫做负采样(negative sampling)
,因为选出来的一批词都是不是正确的target word。这个模型如下图所示:

这样一来,我们要最大化的目标就是:

JNEG=log⁡Qθ(D=1∣wt,h)+kEw~∼Pnoise[log⁡Qθ(D=0∣w~,h)]J_\text{NEG} = \log
Q_\theta(D=1 |w_t, h) + k \mathop{\mathbb{E}}_{\tilde w \sim P_\text{noise}}
\left[ \log Q_\theta(D = 0 |\tilde w, h) \right]JNEG​=logQθ​(D=1∣wt​,h)+kEw~∼P
noise​​[logQθ​(D=0∣w~,h)]

其中,Qθ(D=1∣w,h)Q_\theta(D=1\vert w, h)Qθ​(D=1∣w,h)表示二分类逻辑回归在数据集D中的上下文h中包含目标wtw_t
wt​的概率。

<>The classifier

上面说到了负采样。什么事负采样呢?其实就是随机选取k个词语,和目标词组成负样本训练。

现在我们回到斯坦福的教材上来。这里列出训练一个skip-gram模型的要点:

* 把目标词和上下文词组成的样本当做训练的正样本(positive sample)
* 随机选取一些词和目标词组成的样本当做训练的负样本(negtive sample)
* 使用logistic regression训练一个二分类器来区分两种情况
* regression的权重就是我们的embedding
word2vec需要的是训练一个binary logistic regression,给定一个目标ttt和候选上下文ccc的元组(t,c)(t,c)(t,c)
,返回ccc正好是ttt的上下文词的概率:

P(+∣t,c)P(+\vert t,c)P(+∣t,c)

那么,ccc不是ttt的上下文词的概率就是:

P(−∣t,c)=1−P(+∣t,c)P(-\vert t,c) = 1 - P(+\vert t,c)P(−∣t,c)=1−P(+∣t,c)

那么分类器如何计算这个概率PPP呢?skip-gram模型有这样一个假设:相近的词它们的嵌入表示也很近!

也就是,我们可以把两个词语的嵌入表示的相似度,用来表示概率PPP!相似度就用我们上文说到的余弦相似度:

Similarity(t,c)≈t⋅cSimilarity(t,c) \approx t\cdot cSimilarity(t,c)≈t⋅c

当然,点积的结果并不是概率表示,我们需要用logistic或者叫sigmoid函数,把它转化为概率表示:

P(+∣t,c)=11+e−t⋅cP(+\vert t,c) = \frac{1}{1+e^{-t\cdot c}}P(+∣t,c)=1+e−t⋅c1​

那么:

P(−∣t,c)=1−P(+∣t,c)=e−t⋅c1+e−t⋅cP(-\vert t,c) = 1 - P(+\vert t,c) =
\frac{e^{-t\cdot c}}{1+e^{-t\cdot c}}P(−∣t,c)=1−P(+∣t,c)=1+e−t⋅ce−t⋅c​

上面的公式只是一个单词的概率,但是我们需要把整个window里面的单词计算进来。skip-gram模型还有一个假设:所有的上下文单词之间是独立的!

假设我们的window_size = k,于是有:

P(+∣t,c1:k)=∏i=1k11+e−t⋅ciP(+\vert t,c_{1:k}) =
\prod_{i=1}^k\frac{1}{1+e^{-t\cdot c_i}}P(+∣t,c1:k​)=i=1∏k​1+e−t⋅ci​1​

通常,我们会使用对数概率:

log⁡P(+∣t,c1:k)=∑i=1klog⁡11+e−t⋅ci\log{P(+\vert t,c_{1:k})} =
\sum_{i=1}^k\log{\frac{1}{1+e^{-t\cdot c_i}}}logP(+∣t,c1:k​)=i=1∑k​log1+e−t⋅ci​1


<>skip-gram模型的训练

为了训练这个word2vec,我们除了正样本,还需要负样本。实际上,负样本通常比正样本更多。一般用一个比率k来控制正负样本,如果k=2
则说明,每一个正样本,对应2个负样本。这就是前面说的负采样技术。

构造负样本选择的词语(噪声词noise words)是根据一个频率来的:

pα(w)=count(w)α∑w′count(w′)αp_\alpha(w) =
\frac{count(w)^\alpha}{\sum_{w'}count(w')^\alpha}pα​(w)=∑w′​count(w′)α
count(w)α​

其中,α\alphaα是一个比率,一般来说取值34=0.75\frac{3}{4} = 0.7543​=0.75。

为什么需要这个比例呢?这样可以让出现次数少的词被选择的可能性变大!

举个例子,如果没有这个比率,假设P(a)=0.99P(a) = 0.99P(a)=0.99,P(b)=0.01P(b) = 0.01P(b)=0.01
,加上这个比率之后:

Pα(a)=0.97P_\alpha(a) = 0.97Pα​(a)=0.97

Pα(b)=0.03P_\alpha(b) = 0.03Pα​(b)=0.03

可见,bbb得选择的概率从0.01提升到了0.03。

有了正负样本之后,我们的模型训练就有以下目标了:

* 最大化正样本的概率,也就是正样本的相似度最大化
* 最小化负样本的概率,也就是负样本的相似度最小化
在整个训练集上,用数学表示出上面的目标就是:

L(θ)=∑(t,c)∈+log⁡P(+∣t,c)+∑(t,c)∈−log⁡P(−∣t,c)L(\theta) = \sum_{(t,c)\in
+}\log P(+\vert t,c) + \sum_{(t,c)\in -}\log P(-\vert t,c)L(θ)=(t,c)∈+∑​logP(+∣t
,c)+(t,c)∈−∑​logP(−∣t,c)

如果从单个训练数据对来看(一个(t,c)(t,c)(t,c) 对和kkk个噪声n1,n2,…,nkn_1,n_2,\dots,n_kn1​,n2​,…,nk​
),就有:

L(θ)=log⁡P(+∣t,c)+∑i=1klog⁡P(−∣t,ni)L(\theta) = \log P(+\vert t,c) +
\sum_{i=1}^k\log P(-\vert t,n_i)L(θ)=logP(+∣t,c)+i=1∑k​logP(−∣t,ni​)

概率P由simoid函数计算,有:

L(θ)=log⁡σ(c⋅t)+∑i=1klog⁡σ(−ni⋅t)L(\theta) = \log\sigma(c\cdot t) +
\sum_{i=1}^k\log\sigma(-n_i\cdot t)L(θ)=logσ(c⋅t)+i=1∑k​logσ(−ni​⋅t)

展开,有:

L(θ)=log⁡11+e−c⋅t+∑i=1klog⁡11+ec⋅tL(\theta) = \log\frac{1}{1+e^{-c\cdot t}} +
\sum_{i=1}^k\log\frac{1}{1+e^{c\cdot t}}L(θ)=log1+e−c⋅t1​+i=1∑k​log1+ec⋅t1​

可以看出,最大化上面的目标,就是最大化正样本c⋅tc\cdot tc⋅t,同时最小化负样本ni⋅tn_i\cdot tni​⋅t。

有了上面的概率表示,那么我们就可以使用交叉熵作为损失函数,然后训练模型了!

值得注意的是,tensorflow里面把上面的两个过程合并了,合并在tf.nn.nce_loss
这个函数里面。你可以看到tensorflow的教程里面的损失函数就是使用的tf.nn.nce_loss
作为损失函数。但是你继续追踪源码就会发现,这个损失函数只不过是:

* 进行采样,计算出概率
* 使用交叉熵计算损失
可见,和我们上面的训练分析过程是吻合的!

<>两个权重矩阵W和C

还记得我们上面skip-gram模型训练的最后一个要点regression的权重作为embedding吗?

其实,word2vec训练之后会有两个权重矩阵,分别是嵌入矩阵WWW和上下文矩阵C,回顾一下这张图:

上图中的WWW权重矩阵就是我们的Embedding矩阵,而W′W'W′权重矩阵就是我们的Context矩阵!

**如果我们要得到每一个单词的向量表示,只要从WWW中取出对应的行即可!**因为,训练的每一个单词,都是用one-hot编码的,直接和WWW
相乘即可得到改词的向量表示。如果你对这一部分有疑问,请查看我之前的文章自己动手实现word2vec(skip-gram模型)
<https://juejin.im/post/5b986f296fb9a05d11176a15>。

所以,整个word2vec模型就是一个浅层的神经网络!

我们训练结束后,得到的两个矩阵WWW和CCC怎么用呢?一般情况下,我们不需要使用CCC,直接忽略掉即可。但是你也可以把两个矩阵相加,一起来表示新的N
维嵌入表示,或者把他们合并,即[W,C][W,C][W,C],用来创建一个新的2*N的嵌入表示。


当然,斯坦福的这个教程,后面还提到了词嵌入的可视化等信息,我就不写了。还是推荐大家去看原文,当然,我写的这个笔记中结合tensorflow那一部分也肯定可以解决你的一些疑惑。

<>推荐文章

1.Vector Representations of Words
<https://www.tensorflow.org/tutorials/representation/word2vec>
2.自己动手实现word2vec(skip-gram模型)
<https://juejin.im/post/5b986f296fb9a05d11176a15>
3.Logistic Regression <https://luozhouyang.github.io/logistic_regression/>
4.Learning Word Embedding
<https://lilianweng.github.io/lil-log/2017/10/15/learning-word-embedding.html>

<>联系我

*
Email: stupidme.me.lzy@gmail.com <mailto:stupidme.me.lzy@gmail.com>

*
WeChat: luozhouyang0528


*
个人公众号,你可能会感兴趣:

友情链接
KaDraw流程图
API参考文档
OK工具箱
云服务器优惠
阿里云优惠券
腾讯云优惠券
华为云优惠券
站点信息
问题反馈
邮箱:ixiaoyang8@qq.com
QQ群:637538335
关注微信