diff --git a/chapter_convolutional-neural-networks/channels.md b/chapter_convolutional-neural-networks/channels.md index 78e1bb638..775c3f084 100644 --- a/chapter_convolutional-neural-networks/channels.md +++ b/chapter_convolutional-neural-networks/channels.md @@ -8,7 +8,7 @@ 当输入数据含多个通道时,我们需要构造一个输入通道数与输入数据的通道数相同的卷积核,从而能够与含多通道的输入数据做互相关运算。假设输入数据的通道数为$c_i$,那么卷积核的输入通道数同样为$c_i$。设卷积核窗口形状为$k_h\times k_w$。当$c_i=1$时,我们知道卷积核只包含一个形状为$k_h\times k_w$的二维数组。当$c_i > 1$时,我们将会为每个输入通道各分配一个形状为$k_h\times k_w$的核数组。把这$c_i$个数组在输入通道维上连结,即得到一个形状为$c_i\times k_h\times k_w$的卷积核。由于输入和卷积核各有$c_i$个通道,我们可以在各个通道上对输入的二维数组和卷积核的二维核数组做互相关运算,再将这$c_i$个互相关运算的二维输出按通道相加,得到一个二维数组。这就是含多个通道的输入数据与多输入通道的卷积核做二维互相关运算的输出。 -图5.4展示了含2个输入通道的二维互相关计算的例子。在每个通道上,二维输入数组与二维核数组做互相关运算,再按通道相加即得到输出。图5.4中阴影部分为第一个输出元素及其计算所使用的输入和核数组元素:$(1\times1+2\times2+4\times3+5\times4)+(0\times0+1\times1+3\times2+4\times3)=56$。阴影部分为第一个输出元素及其计算所使用的输入和核数组元素:$(1\times1+2\times2+4\times3+5\times4)+(0\times0+1\times1+3\times2+4\times3)=56$。 +图5.4展示了含2个输入通道的二维互相关计算的例子。在每个通道上,二维输入数组与二维核数组做互相关运算,再按通道相加即得到输出。图5.4中阴影部分为第一个输出元素及其计算所使用的输入和核数组元素:$(1\times1+2\times2+4\times3+5\times4)+(0\times0+1\times1+3\times2+4\times3)=56$。 ![含2个输入通道的互相关计算](../img/conv_multi_in.svg) diff --git a/chapter_convolutional-neural-networks/padding-and-strides.md b/chapter_convolutional-neural-networks/padding-and-strides.md index 0beb775c0..653df41b0 100644 --- a/chapter_convolutional-neural-networks/padding-and-strides.md +++ b/chapter_convolutional-neural-networks/padding-and-strides.md @@ -8,7 +8,7 @@ $$(n_h-k_h+1) \times (n_w-k_w+1).$$ ## 填充 -填充(padding)是指在输入高和宽的两侧填充元素(通常是0元素)。图5.2里我们在原输入高和宽的两侧分别添加了值为0的元素,使得输入高和宽从3变成了5,并导致输出高和宽由2增加到4。阴影部分为第一个输出元素及其计算所使用的输入和核数组元素:$0\times0+0\times1+0\times2+0\times3=0$。 +填充(padding)是指在输入高和宽的两侧填充元素(通常是0元素)。图5.2里我们在原输入高和宽的两侧分别添加了值为0的元素,使得输入高和宽从3变成了5,并导致输出高和宽由2增加到4。图5.2中的阴影部分为第一个输出元素及其计算所使用的输入和核数组元素:$0\times0+0\times1+0\times2+0\times3=0$。 ![在输入的高和宽两侧分别填充了0元素的二维互相关计算](../img/conv_pad.svg) diff --git a/chapter_natural-language-processing/approx-training.md b/chapter_natural-language-processing/approx-training.md index 7d0080818..4e26459cc 100644 --- a/chapter_natural-language-processing/approx-training.md +++ b/chapter_natural-language-processing/approx-training.md @@ -1,6 +1,6 @@ # 近似训练 -回忆上节内容。跳字模型的核心在于使用softmax运算得到给定中心词$w_c$来生成背景词$w_o$的条件概率 +回忆上一节的内容。跳字模型的核心在于使用softmax运算得到给定中心词$w_c$来生成背景词$w_o$的条件概率 $$\mathbb{P}(w_o \mid w_c) = \frac{\text{exp}(\boldsymbol{u}_o^\top \boldsymbol{v}_c)}{ \sum_{i \in \mathcal{V}} \text{exp}(\boldsymbol{u}_i^\top \boldsymbol{v}_c)}.$$ @@ -10,7 +10,7 @@ $$-\log \mathbb{P}(w_o \mid w_c) = -\boldsymbol{u}_o^\top \boldsymbol{v}_c + \log\left(\sum_{i \in \mathcal{V}} \text{exp}(\boldsymbol{u}_i^\top \boldsymbol{v}_c)\right).$$ -由于softmax运算考虑了背景词可能是词典$\mathcal{V}$中的任一词,以上损失包含了词典大小数目的项的累加。在上一节中我们看到,不论是跳字模型还是连续词袋模型,由于条件概率使用了softmax运算,每一步的梯度计算都包含词典大小数目的项的累加。对于含几十万或上百万词的较大词典,每次的梯度计算开销可能过大。为了降低该计算复杂度,本节将介绍两个近似训练方法:负采样(negative sampling)或层序softmax(hierarchical softmax)。由于跳字模型和连续词袋模型类似,本节仅以跳字模型为例介绍这两个方法。 +由于softmax运算考虑了背景词可能是词典$\mathcal{V}$中的任一词,以上损失包含了词典大小数目的项的累加。在上一节中我们看到,不论是跳字模型还是连续词袋模型,由于条件概率使用了softmax运算,每一步的梯度计算都包含词典大小数目的项的累加。对于含几十万或上百万词的较大词典,每次的梯度计算开销可能过大。为了降低该计算复杂度,本节将介绍两种近似训练方法,即负采样(negative sampling)或层序softmax(hierarchical softmax)。由于跳字模型和连续词袋模型类似,本节仅以跳字模型为例介绍这两种方法。 @@ -28,7 +28,7 @@ $$\sigma(x) = \frac{1}{1+\exp(-x)}.$$ $$ \prod_{t=1}^{T} \prod_{-m \leq j \leq m,\ j \neq 0} \mathbb{P}(D=1\mid w^{(t)}, w^{(t+j)}).$$ -然而,以上模型中包含的事件仅考虑了正类样本。这导致当所有词向量相等且值为无穷大时,以上的联合概率才被最大化为1。很明显,这样的词向量毫无意义。负采样通过采样并添加负类样本使目标函数更有意义。设背景词$w_o$出现在中心词$w_c$的一个背景窗口为事件$P$,我们根据分布$\mathbb{P}(w)$采样$K$个未出现在该背景窗口中的词,即噪音词。设噪音词$w_k$($k=1, \ldots, K$)不出现在中心词$w_c$的该背景窗口为事件$N_k$。假设同时含有正类样本和负类样本的事件$P, N_1, \ldots, N_K$相互独立,负采样将以上需要最大化的仅考虑正类样本的联合概率改写为 +然而,以上模型中包含的事件仅考虑了正类样本。这导致当所有词向量相等且值为无穷大时,以上的联合概率才被最大化为1。很明显,这样的词向量毫无意义。负采样通过采样并添加负类样本使目标函数更有意义。设背景词$w_o$出现在中心词$w_c$的一个背景窗口为事件$P$,我们根据分布$\mathbb{P}(w)$采样$K$个未出现在该背景窗口中的词,即噪声词。设噪声词$w_k$($k=1, \ldots, K$)不出现在中心词$w_c$的该背景窗口为事件$N_k$。假设同时含有正类样本和负类样本的事件$P, N_1, \ldots, N_K$相互独立,负采样将以上需要最大化的仅考虑正类样本的联合概率改写为 $$ \prod_{t=1}^{T} \prod_{-m \leq j \leq m,\ j \neq 0} \mathbb{P}(w^{(t+j)} \mid w^{(t)}),$$ @@ -37,7 +37,7 @@ $$ \prod_{t=1}^{T} \prod_{-m \leq j \leq m,\ j \neq 0} \mathbb{P}(w^{(t+j)} \mid $$ \mathbb{P}(w^{(t+j)} \mid w^{(t)}) =\mathbb{P}(D=1\mid w^{(t)}, w^{(t+j)})\prod_{k=1,\ w_k \sim \mathbb{P}(w)}^K \mathbb{P}(D=0\mid w^{(t)}, w_k).$$ -设文本序列中时间步$t$的词$w^{(t)}$在词典中的索引为$i_t$,噪音词$w_k$在词典中的索引为$h_k$。有关以上条件概率的对数损失为 +设文本序列中时间步$t$的词$w^{(t)}$在词典中的索引为$i_t$,噪声词$w_k$在词典中的索引为$h_k$。有关以上条件概率的对数损失为 $$ \begin{aligned} @@ -48,22 +48,22 @@ $$ \end{aligned} $$ -现在,训练中每一步的梯度计算开销不再跟词典大小相关,而跟$K$线性相关。当$K$取较小的常数时,负采样在每一步的梯度计算开销较小。 +现在,训练中每一步的梯度计算开销不再与词典大小相关,而与$K$线性相关。当$K$取较小的常数时,负采样在每一步的梯度计算开销较小。 ## 层序softmax -层序softmax是另一种近似训练法。它使用了二叉树这一数据结构,树的每个叶子节点代表着词典$\mathcal{V}$中的每个词。 +层序softmax是另一种近似训练法。它使用了二叉树这一数据结构,树的每个叶结点代表词典$\mathcal{V}$中的每个词。 -![层序softmax。树的每个叶子节点代表着词典的每个词](../img/hi-softmax.svg) +![层序softmax。二叉树的每个叶结点代表着词典的每个词](../img/hi-softmax.svg) -假设$L(w)$为从二叉树的根节点到词$w$的叶子节点的路径(包括根和叶子节点)上的节点数。设$n(w,j)$为该路径上第$j$个节点,并设该节点的背景词向量为$\boldsymbol{u}_{n(w,j)}$。以图10.3为例,$L(w_3) = 4$。层序softmax将跳字模型中的条件概率近似表示为 +假设$L(w)$为从二叉树的根结点到词$w$的叶结点的路径(包括根结点和叶结点)上的结点数。设$n(w,j)$为该路径上第$j$个结点,并设该结点的背景词向量为$\boldsymbol{u}_{n(w,j)}$。以图10.3为例,$L(w_3) = 4$。层序softmax将跳字模型中的条件概率近似表示为 $$\mathbb{P}(w_o \mid w_c) = \prod_{j=1}^{L(w_o)-1} \sigma\left( [\![ n(w_o, j+1) = \text{leftChild}(n(w_o,j)) ]\!] \cdot \boldsymbol{u}_{n(w_o,j)}^\top \boldsymbol{v}_c\right),$$ -其中$\sigma$函数与sigmoid激活函数的定义相同,$\text{leftChild}(n)$是节点$n$的左孩子节点:如果判断$x$为真,$[\![x]\!] = 1$;反之$[\![x]\!] = -1$。 -让我们计算图10.3中给定词$w_c$生成词$w_3$的条件概率。我们需要将$w_c$的词向量$\boldsymbol{v}_c$和根节点到$w_3$路径上的非叶子节点向量一一求内积。由于在二叉树中由根节点到叶子节点$w_3$的路径上需要向左、向右、再向左地遍历(图10.3中加粗的路径),我们得到 +其中$\sigma$函数与[“多层感知机”](../chapter_deep-learning-basics/mlp.md)一节中sigmoid激活函数的定义相同,$\text{leftChild}(n)$是结点$n$的左子结点:如果判断$x$为真,$[\![x]\!] = 1$;反之$[\![x]\!] = -1$。 +让我们计算图10.3中给定词$w_c$生成词$w_3$的条件概率。我们需要将$w_c$的词向量$\boldsymbol{v}_c$和根结点到$w_3$路径上的非叶结点向量一一求内积。由于在二叉树中由根结点到叶结点$w_3$的路径上需要向左、向右再向左地遍历(图10.3中加粗的路径),我们得到 $$\mathbb{P}(w_3 \mid w_c) = \sigma(\boldsymbol{u}_{n(w_3,1)}^\top \boldsymbol{v}_c) \cdot \sigma(-\boldsymbol{u}_{n(w_3,2)}^\top \boldsymbol{v}_c) \cdot \sigma(\boldsymbol{u}_{n(w_3,3)}^\top \boldsymbol{v}_c).$$ @@ -75,15 +75,15 @@ $$\sum_{w \in \mathcal{V}} \mathbb{P}(w \mid w_c) = 1.$$ ## 小结 -* 负采样通过考虑同时含有正类样本和负类样本的相互独立事件来构造损失函数。其训练中每一步的梯度计算开销与采样的噪音词的个数线性相关。 -* 层序softmax使用了二叉树,并根据根节点到叶子节点的路径来构造损失函数。其训练中每一步的梯度计算开销与词典大小的对数相关。 +* 负采样通过考虑同时含有正类样本和负类样本的相互独立事件来构造损失函数。其训练中每一步的梯度计算开销与采样的噪声词的个数线性相关。 +* 层序softmax使用了二叉树,并根据根结点到叶结点的路径来构造损失函数。其训练中每一步的梯度计算开销与词典大小的对数相关。 ## 练习 -* 在阅读下一节之前,你觉得在负采样中应如何采样噪音词? +* 在阅读下一节之前,你觉得在负采样中应如何采样噪声词? * 本节中最后一个公式为什么成立? -* 如何将负采样和层序softmax应用到连续词袋模型? +* 如何将负采样或层序softmax用于训练连续词袋模型? ## 扫码直达[讨论区](https://discuss.gluon.ai/t/topic/8135) diff --git a/chapter_natural-language-processing/attention.md b/chapter_natural-language-processing/attention.md index ddb12a0ab..cf0214e09 100644 --- a/chapter_natural-language-processing/attention.md +++ b/chapter_natural-language-processing/attention.md @@ -2,21 +2,21 @@ 在[“编码器—解码器(seq2seq)”](seq2seq.md)一节里,解码器在各个时间步依赖相同的背景变量来获取输入序列信息。当编码器为循环神经网络时,背景变量来自它最终时间步的隐藏状态。 -现在让我们再次思考那一节提到的翻译例子:输入为英语序列“They”、“are”、“watching”、“.”,输出为法语序列“Ils”、“regardent”、“.”。不难想到,解码器在生成输出序列中的每一个词时可能只需利用输入序列某一部分的信息。例如,在输出序列的时间步1,解码器可以主要依赖“They”、“are”的信息来生成“Ils”,在时间步2则主要使用来自“watching”的编码信息生成“regardent”,最后在时间步3则直接映射句号“.”。这看上去就像是在解码器的每一时间步对输入序列中不同时间步的编码信息分配不同的注意力一样。这也是注意力机制的由来 [1]。 +现在,让我们再次思考那一节提到的翻译例子:输入为英语序列“They”“are”“watching”“.”,输出为法语序列“Ils”“regardent”“.”。不难想到,解码器在生成输出序列中的每一个词时可能只需利用输入序列某一部分的信息。例如,在输出序列的时间步1,解码器可以主要依赖“They”“are”的信息来生成“Ils”,在时间步2则主要使用来自“watching”的编码信息生成“regardent”,最后在时间步3则直接映射句号“.”。这看上去就像是在解码器的每一时间步对输入序列中不同时间步的表征或编码信息分配不同的注意力一样。这也是注意力机制的由来 [1]。 仍然以循环神经网络为例,注意力机制通过对编码器所有时间步的隐藏状态做加权平均来得到背景变量。解码器在每一时间步调整这些权重,即注意力权重,从而能够在不同时间步分别关注输入序列中的不同部分并编码进相应时间步的背景变量。本节我们将讨论注意力机制是怎么工作的。 -在[“编码器—解码器(seq2seq)”](seq2seq.md)一节里我们区分了输入序列或编码器的索引$t$与输出序列或解码器的索引$t'$。该节中,解码器在时间步$t'$的隐藏状态$\boldsymbol{s}_{t'} = g(\boldsymbol{y}_{t'-1}, \boldsymbol{c}, \boldsymbol{s}_{t'-1})$,其中$\boldsymbol{y}_{t'-1}$是上一时间步$t'-1$的输出$y_{t'-1}$的特征表示,且任一时间步$t'$使用相同的背景变量$\boldsymbol{c}$。但在注意力机制中,解码器的每一时间步将使用可变的背景变量。记$\boldsymbol{c}_{t'}$是解码器在时间步$t'$的背景变量,那么解码器在该时间步的隐藏状态可以改写为 +在[“编码器—解码器(seq2seq)”](seq2seq.md)一节里我们区分了输入序列或编码器的索引$t$与输出序列或解码器的索引$t'$。该节中,解码器在时间步$t'$的隐藏状态$\boldsymbol{s}_{t'} = g(\boldsymbol{y}_{t'-1}, \boldsymbol{c}, \boldsymbol{s}_{t'-1})$,其中$\boldsymbol{y}_{t'-1}$是上一时间步$t'-1$的输出$y_{t'-1}$的表征,且任一时间步$t'$使用相同的背景变量$\boldsymbol{c}$。但在注意力机制中,解码器的每一时间步将使用可变的背景变量。记$\boldsymbol{c}_{t'}$是解码器在时间步$t'$的背景变量,那么解码器在该时间步的隐藏状态可以改写为 $$\boldsymbol{s}_{t'} = g(\boldsymbol{y}_{t'-1}, \boldsymbol{c}_{t'}, \boldsymbol{s}_{t'-1}).$$ -这里的关键是如何计算背景变量$\boldsymbol{c}_{t'}$和如何利用它来更新隐藏状态$\boldsymbol{s}_{t'}$。以下将分别描述这两个关键点。 +这里的关键是如何计算背景变量$\boldsymbol{c}_{t'}$和如何利用它来更新隐藏状态$\boldsymbol{s}_{t'}$。下面将分别描述这两个关键点。 ## 计算背景变量 -图10.12描绘了注意力机制如何为解码器在时间步2计算背景变量。首先,函数$a$根据解码器在时间步1的隐藏状态和编码器在各个时间步的隐藏状态计算softmax运算的输入。softmax运算输出概率分布并对编码器各个时间步的隐藏状态做加权平均,从而得到背景变量。 +我们先描述第一个关键点,即计算背景变量。图10.12描绘了注意力机制如何为解码器在时间步2计算背景变量。首先,函数$a$根据解码器在时间步1的隐藏状态和编码器在各个时间步的隐藏状态计算softmax运算的输入。softmax运算输出概率分布并对编码器各个时间步的隐藏状态做加权平均,从而得到背景变量。 ![编码器—解码器上的注意力机制](../img/attention.svg) @@ -42,10 +42,10 @@ $$a(\boldsymbol{s}, \boldsymbol{h}) = \boldsymbol{v}^\top \tanh(\boldsymbol{W}_s ### 矢量化计算 -我们还可以对注意力机制采用更高效的矢量化计算。广义上,注意力机制模型的输入包括查询项以及一一对应的键项和值项。其中值项是需要加权平均的一组项。在加权平均中,值项的权重来自查询项以及与该值项对应的键项的计算。 +我们还可以对注意力机制采用更高效的矢量化计算。广义上,注意力机制的输入包括查询项以及一一对应的键项和值项,其中值项是需要加权平均的一组项。在加权平均中,值项的权重来自查询项以及与该值项对应的键项的计算。 在上面的例子中,查询项为解码器的隐藏状态,键项和值项均为编码器的隐藏状态。 -让我们考虑一个常见的简单情形,即编码器和解码器的隐藏单元个数均为$h$,且函数$a(\boldsymbol{s}, \boldsymbol{h})=\boldsymbol{s}^\top \boldsymbol{h}$。假设我们希望根据解码器单个隐藏状态$\boldsymbol{s}_{t' - 1} \in \mathbb{R}^{h}$和编码器所有隐藏状态$\boldsymbol{h}_t \in \mathbb{R}^{h}, t = 1,\ldots,T$计算背景向量$\boldsymbol{c}_{t'}\in \mathbb{R}^{h}$。 +让我们考虑一个常见的简单情形,即编码器和解码器的隐藏单元个数均为$h$,且函数$a(\boldsymbol{s}, \boldsymbol{h})=\boldsymbol{s}^\top \boldsymbol{h}$。假设我们希望根据解码器单个隐藏状态$\boldsymbol{s}_{t' - 1} \in \mathbb{R}^{h}$和编码器所有隐藏状态$\boldsymbol{h}_t \in \mathbb{R}^{h}, t = 1,\ldots,T$来计算背景向量$\boldsymbol{c}_{t'}\in \mathbb{R}^{h}$。 我们可以将查询项矩阵$\boldsymbol{Q} \in \mathbb{R}^{1 \times h}$设为$\boldsymbol{s}_{t' - 1}^\top$,并令键项矩阵$\boldsymbol{K} \in \mathbb{R}^{T \times h}$和值项矩阵$\boldsymbol{V} \in \mathbb{R}^{T \times h}$相同且第$t$行均为$\boldsymbol{h}_t^\top$。此时,我们只需要通过矢量化计算 $$\text{softmax}(\boldsymbol{Q}\boldsymbol{K}^\top)\boldsymbol{V}$$ @@ -56,13 +56,11 @@ $$\text{softmax}(\boldsymbol{Q}\boldsymbol{K}^\top)\boldsymbol{V}$$ ## 更新隐藏状态 -以门控循环单元为例,在解码器中我们可以对门控循环单元的设计稍作修改 [1]。 -解码器在时间步$t'$的隐藏状态为 +现在我们描述第二个关键点,即更新隐藏状态。以门控循环单元为例,在解码器中我们可以对[“门控循环单元(GRU)”](../chapter_recurrent-neural-networks/gru.md)一节中门控循环单元的设计稍作修改,从而变换上一时间步$t'-1$的输出$\boldsymbol{y}_{t'-1}$、隐藏状态$\boldsymbol{s}_{t' - 1}$和当前时间步$t'$的含注意力机制的背景变量$\boldsymbol{c}_{t'}$ [1]。解码器在时间步$t'$的隐藏状态为 $$\boldsymbol{s}_{t'} = \boldsymbol{z}_{t'} \odot \boldsymbol{s}_{t'-1} + (1 - \boldsymbol{z}_{t'}) \odot \tilde{\boldsymbol{s}}_{t'},$$ -其中的重置门、更新门和候选隐含状态分别为 - +其中的重置门、更新门和候选隐藏状态分别为 $$ \begin{aligned} @@ -78,13 +76,13 @@ $$ ## 发展 -本质上,注意力机制能够为特征中较有价值的部分分配较多的计算资源。这个有趣的想法自提出后得到了快速发展,特别是启发了依靠注意力机制来编码输入序列并解码出输出序列的变换器(Transformer)模型的设计 [2]。变换器抛弃了卷积神经网络和循环神经网络的架构。它在计算效率上比基于循环神经网络的编码器—解码器模型通常更具明显优势。含注意力机制的变换器的编码结构在后来的BERT预训练模型中得以应用并令后者大放异彩:微调后的模型在多达11项自然语言处理任务中取得了当时最先进的结果 [3]。除了自然语言处理领域,注意力机制还被广泛用于图像分类、自动图像描述、唇语解读以及语音识别。 +本质上,注意力机制能够为表征中较有价值的部分分配较多的计算资源。这个有趣的想法自提出后得到了快速发展,特别是启发了依靠注意力机制来编码输入序列并解码出输出序列的变换器(Transformer)模型的设计 [2]。变换器抛弃了卷积神经网络和循环神经网络的架构。它在计算效率上比基于循环神经网络的编码器—解码器模型通常更具明显优势。含注意力机制的变换器的编码结构在后来的BERT预训练模型中得以应用并令后者大放异彩:微调后的模型在多达11项自然语言处理任务中取得了当时最先进的结果 [3]。除了自然语言处理领域,注意力机制还被广泛用于图像分类、自动图像描述、唇语解读以及语音识别。 ## 小结 -* 我们可以在解码器的每个时间步使用不同的背景变量,并对输入序列中不同时间步编码的信息分配不同的注意力。 -* 广义上,注意力机制模型的输入包括查询项以及一一对应的键项和值项。 +* 可以在解码器的每个时间步使用不同的背景变量,并对输入序列中不同时间步编码的信息分配不同的注意力。 +* 广义上,注意力机制的输入包括查询项以及一一对应的键项和值项。 * 注意力机制可以采用更为高效的矢量化计算。 @@ -94,7 +92,6 @@ $$ * 不修改[“门控循环单元(GRU)”](../chapter_recurrent-neural-networks/gru.md)一节中的`gru`函数,应如何用它实现本节介绍的解码器? -* 除了自然语言处理,注意力机制还可以应用在哪些地方? ## 扫码直达[讨论区](https://discuss.gluon.ai/t/topic/6759) diff --git a/chapter_natural-language-processing/beam-search.md b/chapter_natural-language-processing/beam-search.md index bc6ddd801..920847246 100644 --- a/chapter_natural-language-processing/beam-search.md +++ b/chapter_natural-language-processing/beam-search.md @@ -1,8 +1,8 @@ # 束搜索 -上一节介绍了如何训练输入输出均为不定长序列的编码器—解码器。这一节我们介绍如何使用编码器—解码器来预测不定长的序列。 +上一节介绍了如何训练输入和输出均为不定长序列的编码器—解码器。本节我们介绍如何使用编码器—解码器来预测不定长的序列。 -上一节里已经提到,在准备训练数据集时,我们通常会在样本的输入序列和输出序列后面分别附上一个特殊符号“<eos>”表示序列的终止。我们在接下来的讨论中也将沿用上一节的数学符号。为了便于讨论,假设解码器的输出是一段文本序列。设输出文本词典$\mathcal{Y}$(包含特殊符号“<eos>”)的大小为$\left|\mathcal{Y}\right|$,输出序列的最大长度为$T'$。所有可能的输出序列一共有$\mathcal{O}(\left|\mathcal{Y}\right|^{T'})$种。这些输出序列中所有特殊符号“<eos>”后面的子序列将被舍弃。 +上一节里已经提到,在准备训练数据集时,我们通常会在样本的输入序列和输出序列后面分别附上一个特殊符号“<eos>”表示序列的终止。我们在接下来的讨论中也将沿用上一节的全部数学符号。为了便于讨论,假设解码器的输出是一段文本序列。设输出文本词典$\mathcal{Y}$(包含特殊符号“<eos>”)的大小为$\left|\mathcal{Y}\right|$,输出序列的最大长度为$T'$。所有可能的输出序列一共有$\mathcal{O}(\left|\mathcal{Y}\right|^{T'})$种。这些输出序列中所有特殊符号“<eos>”后面的子序列将被舍弃。 ## 贪婪搜索 @@ -13,23 +13,23 @@ $$y_{t'} = \operatorname*{argmax}_{y \in \mathcal{Y}} \mathbb{P}(y \mid y_1, \ld 作为输出。一旦搜索出“<eos>”符号,或者输出序列长度已经达到了最大长度$T'$,便完成输出。 -我们在描述解码器时提到,基于输入序列生成输出序列的条件概率是$\prod_{t'=1}^{T'} \mathbb{P}(y_{t'} \mid y_1, \ldots, y_{t'-1}, \boldsymbol{c})$。我们将该条件概率最大的输出序列称为最优序列。而贪婪搜索的主要问题是不能保证得到最优序列。 +我们在描述解码器时提到,基于输入序列生成输出序列的条件概率是$\prod_{t'=1}^{T'} \mathbb{P}(y_{t'} \mid y_1, \ldots, y_{t'-1}, \boldsymbol{c})$。我们将该条件概率最大的输出序列称为最优输出序列。而贪婪搜索的主要问题是不能保证得到最优输出序列。 -下面我们来看一个例子。假设输出词典里面有“A”、“B”、“C”和“<eos>”这四个词。图10.9中每个时间步下的四个数字分别代表了该时间步生成“A”、“B”、“C”和“<eos>”这四个词的条件概率。在每个时间步,贪婪搜索选取条件概率最大的词。因此,图10.9中将生成输出序列“A”、“B”、“C”、“<eos>”。该输出序列的条件概率是$0.5\times0.4\times0.4\times0.6 = 0.048$。 +下面来看一个例子。假设输出词典里面有“A”“B”“C”和“<eos>”这4个词。图10.9中每个时间步下的4个数字分别代表了该时间步生成“A”“B”“C”和“<eos>”这4个词的条件概率。在每个时间步,贪婪搜索选取条件概率最大的词。因此,图10.9中将生成输出序列“A”“B”“C”“<eos>”。该输出序列的条件概率是$0.5\times0.4\times0.4\times0.6 = 0.048$。 -![每个时间步下的四个数字分别代表了该时间步生成“A”、“B”、“C”和“<eos>”这四个词的条件概率。在每个时间步,贪婪搜索选取条件概率最大的词](../img/s2s_prob1.svg) +![在每个时间步,贪婪搜索选取条件概率最大的词](../img/s2s_prob1.svg) -让我们接下来观察图10.10演示的例子。与图10.9中不同,图10.10在时间步2中选取了条件概率第二大的词“C”。由于时间步3所基于的时间步1和2的输出子序列由图10.9中的“A”、“B”变为了图10.10中的“A”、“C”,图10.10中时间步3生成各个词的条件概率发生了变化。我们选取条件概率最大的词“B”。此时时间步4所基于的前三个时间步的输出子序列为“A”、“C”、“B”,与图10.9中的“A”、“B”、“C”不同。因此图10.10中时间步4生成各个词的条件概率也与图10.9中的不同。我们发现,此时的输出序列“A”、“C”、“B”、“<eos>”的条件概率是$0.5\times0.3\times0.6\times0.6=0.054$,大于贪婪搜索得到的输出序列的条件概率。因此,贪婪搜索得到的输出序列“A”、“B”、“C”、“<eos>”并非最优序列。 +接下来,观察图10.10演示的例子。与图10.9中不同,图10.10在时间步2中选取了条件概率第二大的词“C”。由于时间步3所基于的时间步1和2的输出子序列由图10.9中的“A”“B”变为了图10.10中的“A”“C”,图10.10中时间步3生成各个词的条件概率发生了变化。我们选取条件概率最大的词“B”。此时时间步4所基于的前3个时间步的输出子序列为“A”“C”“B”,与图10.9中的“A”“B”“C”不同。因此,图10.10中时间步4生成各个词的条件概率也与图10.9中的不同。我们发现,此时的输出序列“A”“C”“B”“<eos>”的条件概率是$0.5\times0.3\times0.6\times0.6=0.054$,大于贪婪搜索得到的输出序列的条件概率。因此,贪婪搜索得到的输出序列“A”“B”“C”“<eos>”并非最优输出序列。 -![每个时间步下的四个数字分别代表了该时间步生成“A”、“B”、“C”和“<eos>”这四个词的条件概率。在时间步2选取条件概率第二大的词“C”](../img/s2s_prob2.svg) +![在时间步2选取条件概率第二大的词“C”](../img/s2s_prob2.svg) ## 穷举搜索 -如果目标是得到最优序列,我们可以考虑穷举搜索(exhaustive search):穷举所有可能的输出序列,输出条件概率最大的序列。 +如果目标是得到最优输出序列,我们可以考虑穷举搜索(exhaustive search):穷举所有可能的输出序列,输出条件概率最大的序列。 -虽然穷举搜索可以得到最优序列,但它的计算开销$\mathcal{O}(\left|\mathcal{Y}\right|^{T'})$很容易过大。例如,当$|\mathcal{Y}|=10000$且$T'=10$时,我们将评估$10000^{10} = 10^{40}$个序列:这几乎不可能完成。而贪婪搜索的计算开销是$\mathcal{O}(\left|\mathcal{Y}\right|T')$,通常显著小于穷举搜索的计算开销。例如,当$|\mathcal{Y}|=10000$且$T'=10$时,我们只需评估$10000\times10=1\times10^5$个序列。 +虽然穷举搜索可以得到最优输出序列,但它的计算开销$\mathcal{O}(\left|\mathcal{Y}\right|^{T'})$很容易过大。例如,当$|\mathcal{Y}|=10000$且$T'=10$时,我们将评估$10000^{10} = 10^{40}$个序列:这几乎不可能完成。而贪婪搜索的计算开销是$\mathcal{O}(\left|\mathcal{Y}\right|T')$,通常显著小于穷举搜索的计算开销。例如,当$|\mathcal{Y}|=10000$且$T'=10$时,我们只需评估$10000\times10=10^5$个序列。 ## 束搜索 @@ -39,7 +39,7 @@ $$y_{t'} = \operatorname*{argmax}_{y \in \mathcal{Y}} \mathbb{P}(y \mid y_1, \ld ![束搜索的过程。束宽为2,输出序列最大长度为3。候选输出序列有$A$、$C$、$AB$、$CE$、$ABD$和$CED$](../img/beam_search.svg) -图10.11通过一个例子演示了束搜索的过程。假设输出序列的词典中只包含五个元素:$\mathcal{Y} = \{A, B, C, D, E\}$,且其中一个为特殊符号“<eos>”。设束搜索的束宽等于2,输出序列最大长度为3。在输出序列的时间步1,假设条件概率$\mathbb{P}(y_1 \mid \boldsymbol{c})$最大的两个词为$A$和$C$。我们在时间步2时将对所有的$y_2 \in \mathcal{Y}$都分别计算$\mathbb{P}(y_2 \mid A, \boldsymbol{c})$和$\mathbb{P}(y_2 \mid C, \boldsymbol{c})$,并从计算出的10个条件概率中取最大的两个:假设为$\mathbb{P}(B \mid A, \boldsymbol{c})$和$\mathbb{P}(E \mid C, \boldsymbol{c})$。那么,我们在时间步3时将对所有的$y_3 \in \mathcal{Y}$都分别计算$\mathbb{P}(y_3 \mid A, B, \boldsymbol{c})$和$\mathbb{P}(y_3 \mid C, E, \boldsymbol{c})$,并从计算出的10个条件概率中取最大的两个:假设为$\mathbb{P}(D \mid A, B, \boldsymbol{c})$和$\mathbb{P}(D \mid C, E, \boldsymbol{c})$。如此一来,我们得到6个候选输出序列:(1)$A$;(2)$C$;(3)$A$、$B$;(4)$C$、$E$;(5)$A$、$B$、$D$和(6)$C$、$E$、$D$。接下来,我们将根据这6个序列得出最终候选输出序列的集合。 +图10.11通过一个例子演示了束搜索的过程。假设输出序列的词典中只包含5个元素,即$\mathcal{Y} = \{A, B, C, D, E\}$,且其中一个为特殊符号“<eos>”。设束搜索的束宽等于2,输出序列最大长度为3。在输出序列的时间步1时,假设条件概率$\mathbb{P}(y_1 \mid \boldsymbol{c})$最大的2个词为$A$和$C$。我们在时间步2时将对所有的$y_2 \in \mathcal{Y}$都分别计算$\mathbb{P}(y_2 \mid A, \boldsymbol{c})$和$\mathbb{P}(y_2 \mid C, \boldsymbol{c})$,并从计算出的10个条件概率中取最大的2个,假设为$\mathbb{P}(B \mid A, \boldsymbol{c})$和$\mathbb{P}(E \mid C, \boldsymbol{c})$。那么,我们在时间步3时将对所有的$y_3 \in \mathcal{Y}$都分别计算$\mathbb{P}(y_3 \mid A, B, \boldsymbol{c})$和$\mathbb{P}(y_3 \mid C, E, \boldsymbol{c})$,并从计算出的10个条件概率中取最大的2个,假设为$\mathbb{P}(D \mid A, B, \boldsymbol{c})$和$\mathbb{P}(D \mid C, E, \boldsymbol{c})$。如此一来,我们得到6个候选输出序列:(1)$A$;(2)$C$;(3)$A$、$B$;(4)$C$、$E$;(5)$A$、$B$、$D$和(6)$C$、$E$、$D$。接下来,我们将根据这6个序列得出最终候选输出序列的集合。 @@ -47,7 +47,7 @@ $$y_{t'} = \operatorname*{argmax}_{y \in \mathcal{Y}} \mathbb{P}(y \mid y_1, \ld $$ \frac{1}{L^\alpha} \log \mathbb{P}(y_1, \ldots, y_{L}) = \frac{1}{L^\alpha} \sum_{t'=1}^L \log \mathbb{P}(y_{t'} \mid y_1, \ldots, y_{t'-1}, \boldsymbol{c}),$$ -其中$L$为最终候选序列长度,$\alpha$一般可选为0.75。分母上的$L^\alpha$是为了惩罚较长序列在以上分数中较多的对数相加项。分析可得,束搜索的计算开销为$\mathcal{O}(k\left|\mathcal{Y}\right|T')$。这介于贪婪搜索和穷举搜索的计算开销之间。此外,贪婪搜索可看作是束宽为1的束搜索。束搜索通过灵活的束宽$k$来权衡计算开销和搜索质量。 +其中$L$为最终候选序列长度,$\alpha$一般可选为0.75。分母上的$L^\alpha$是为了惩罚较长序列在以上分数中较多的对数相加项。分析可知,束搜索的计算开销为$\mathcal{O}(k\left|\mathcal{Y}\right|T')$。这介于贪婪搜索和穷举搜索的计算开销之间。此外,贪婪搜索可看作是束宽为1的束搜索。束搜索通过灵活的束宽$k$来权衡计算开销和搜索质量。 ## 小结 @@ -58,7 +58,7 @@ $$ \frac{1}{L^\alpha} \log \mathbb{P}(y_1, \ldots, y_{L}) = \frac{1}{L^\alpha} \ ## 练习 -* 穷举搜索可否看作是特殊束宽的束搜索?为什么? +* 穷举搜索可否看作特殊束宽的束搜索?为什么? * 在[“循环神经网络的从零开始实现”](../chapter_recurrent-neural-networks/rnn-scratch.md)一节中,我们使用语言模型创作歌词。它的输出属于哪种搜索?你能改进它吗? diff --git a/chapter_natural-language-processing/fasttext.md b/chapter_natural-language-processing/fasttext.md index f71ca6c28..1746f186b 100644 --- a/chapter_natural-language-processing/fasttext.md +++ b/chapter_natural-language-processing/fasttext.md @@ -1,21 +1,21 @@ # 子词嵌入(fastText) -英语单词通常有其内部结构和形成方式。例如我们可以从“dog”、“dogs”和“dogcatcher”的字面上推测他们的关系。这些词都有同一个词根“dog”,但使用不同的后缀来改变词的含义。而且,这个关联可以推广至其他词汇。例如,“dog”和“dogs”的关系如同“cat”和“cats”的关系,“boy”和“boyfriend”的关系如同“girl”和“girlfriend”的关系。这一特点并非为英语所独有。在法语和西班牙语中,很多动词根据场景不同有40多种不同的形态。而在芬兰语中,一个名词可能有15种以上的形态。事实上,构词学(morphology)作为语言学的一个重要分支,研究的正是词的内部结构和形成方式。 +英语单词通常有其内部结构和形成方式。例如,我们可以从“dog”“dogs”和“dogcatcher”的字面上推测它们的关系。这些词都有同一个词根“dog”,但使用不同的后缀来改变词的含义。而且,这个关联可以推广至其他词汇。例如,“dog”和“dogs”的关系如同“cat”和“cats”的关系,“boy”和“boyfriend”的关系如同“girl”和“girlfriend”的关系。这一特点并非为英语所独有。在法语和西班牙语中,很多动词根据场景不同有40多种不同的形态,而在芬兰语中,一个名词可能有15种以上的形态。事实上,构词学(morphology)作为语言学的一个重要分支,研究的正是词的内部结构和形成方式。 -在word2vec中,我们并没有直接利用构词学中的信息。无论是在跳字模型还是连续词袋模型中,我们将形态不同的单词用不同的向量来表示。例如,“dog”和“dogs”分别用两个不同的向量表示,而模型中并未直接表达这两个向量之间的关系。有鉴于此,fastText提出了子词嵌入(subword embedding)的方法,从而试图将构词信息引入word2vec中的跳字模型 [1]。 +在word2vec中,我们并没有直接利用构词学中的信息。无论是在跳字模型还是连续词袋模型中,我们都将形态不同的单词用不同的向量来表示。例如,“dog”和“dogs”分别用两个不同的向量表示,而模型中并未直接表达这两个向量之间的关系。鉴于此,fastText提出了子词嵌入(subword embedding)的方法,从而试图将构词信息引入word2vec中的跳字模型 [1]。 -在fastText中,每个中心词被表示成子词的集合。下面我们用单词“where”作为例子来了解子词是如何产生的。首先,我们在单词的首尾分别添加特殊字符“<”和“>”以区分作为前后缀的子词。然后,将单词当成一个由字符构成的序列来提取$n$元语法。例如当$n=3$时,我们得到所有长度为3的子词:“<wh>”、“whe”、“her”、“ere”、“<re>”,以及特殊子词“<where>”。 +在fastText中,每个中心词被表示成子词的集合。下面我们用单词“where”作为例子来了解子词是如何产生的。首先,我们在单词的首尾分别添加特殊字符“<”和“>”以区分作为前后缀的子词。然后,将单词当成一个由字符构成的序列来提取$n$元语法。例如,当$n=3$时,我们得到所有长度为3的子词:“<wh>”“whe”“her”“ere”“<re>”以及特殊子词“<where>”。 -在fastText中,对于一个词$w$,我们将它所有长度在3到6的子词和特殊子词的并集记为$\mathcal{G}_w$。那么词典则是所有词的子词集合的并集。假设词典中子词$g$的向量为$\boldsymbol{z}_g$,那么跳字模型中词$w$的作为中心词的向量$\boldsymbol{v}_w$则表示成 +在fastText中,对于一个词$w$,我们将它所有长度在3~6的子词和特殊子词的并集记为$\mathcal{G}_w$。那么词典则是所有词的子词集合的并集。假设词典中子词$g$的向量为$\boldsymbol{z}_g$,那么跳字模型中词$w$的作为中心词的向量$\boldsymbol{v}_w$则表示成 $$\boldsymbol{v}_w = \sum_{g\in\mathcal{G}_w} \boldsymbol{z}_g.$$ -FastText的其余部分同跳字模型一致,不在此重复。可以看到,同跳字模型相比,fastText中词典规模更大,造成模型参数更多,同时一个词的向量需要对所有子词向量求和,继而导致计算复杂度更高。但与此同时,较生僻的复杂单词,甚至是词典中没有的单词,可能会从同它结构类似的其他词那里获取更好的词向量表示。 +fastText的其余部分同跳字模型一致,不在此重复。可以看到,与跳字模型相比,fastText中词典规模更大,造成模型参数更多,同时一个词的向量需要对所有子词向量求和,继而导致计算复杂度更高。但与此同时,较生僻的复杂单词,甚至是词典中没有的单词,可能会从同它结构类似的其他词那里获取更好的词向量表示。 ## 小结 -* FastText提出了子词嵌入方法。它在word2vec中的跳字模型的基础上,将中心词向量表示成单词的子词向量之和。 +* fastText提出了子词嵌入方法。它在word2vec中的跳字模型的基础上,将中心词向量表示成单词的子词向量之和。 * 子词嵌入利用构词上的规律,通常可以提升生僻词表示的质量。 diff --git a/chapter_natural-language-processing/glove.md b/chapter_natural-language-processing/glove.md index f505e0753..2a7c5d1c2 100644 --- a/chapter_natural-language-processing/glove.md +++ b/chapter_natural-language-processing/glove.md @@ -14,7 +14,7 @@ $$-\sum_{i\in\mathcal{V}}\sum_{j\in\mathcal{V}} x_{ij} \log\,q_{ij}.$$ $$-\sum_{i\in\mathcal{V}} x_i \sum_{j\in\mathcal{V}} p_{ij} \log\,q_{ij}.$$ -上式中,$\sum_{j\in\mathcal{V}} p_{ij} \log\,q_{ij}$计算的是以$w_i$为中心词的背景词条件概率分布$p_{ij}$和模型预测的条件概率分布$q_{ij}$的交叉熵。且损失函数使用所有以词$w_i$为中心词的背景词的数量之和来加权。最小化上式中的损失函数会令预测的条件概率分布尽可能接近真实的条件概率分布。 +上式中,$-\sum_{j\in\mathcal{V}} p_{ij} \log\,q_{ij}$计算的是以$w_i$为中心词的背景词条件概率分布$p_{ij}$和模型预测的条件概率分布$q_{ij}$的交叉熵,且损失函数使用所有以词$w_i$为中心词的背景词的数量之和来加权。最小化上式中的损失函数会令预测的条件概率分布尽可能接近真实的条件概率分布。 然而,作为常用损失函数的一种,交叉熵损失函数有时并不是好的选择。一方面,正如我们在[“近似训练”](approx-training.md)一节中所提到的,令模型预测$q_{ij}$成为合法概率分布的代价是它在分母中基于整个词典的累加项。这很容易带来过大的计算开销。另一方面,词典中往往有大量生僻词,它们在数据集中出现的次数极少。而有关大量生僻词的条件概率分布在交叉熵损失函数中的最终预测往往并不准确。 @@ -22,24 +22,24 @@ $$-\sum_{i\in\mathcal{V}} x_i \sum_{j\in\mathcal{V}} p_{ij} \log\,q_{ij}.$$ ## GloVe模型 -有鉴于此,作为在word2vec之后提出的词嵌入模型,GloVe采用了平方损失,并基于该损失对跳字模型做了三点改动 [1]: +鉴于此,作为在word2vec之后提出的词嵌入模型,GloVe模型采用了平方损失,并基于该损失对跳字模型做了3点改动 [1]: -1. 使用非概率分布的变量$p'_{ij}=x_{ij}$和$q'_{ij}=\exp(\boldsymbol{u}_j^\top \boldsymbol{v}_i)$,并对它们取对数。因此平方损失项是$\left(\log\,p'_{ij} - \log\,q'_{ij}\right)^2 = \left(\boldsymbol{u}_j^\top \boldsymbol{v}_i - \log\,x_{ij}\right)^2$。 +1. 使用非概率分布的变量$p'_{ij}=x_{ij}$和$q'_{ij}=\exp(\boldsymbol{u}_j^\top \boldsymbol{v}_i)$,并对它们取对数。因此,平方损失项是$\left(\log\,p'_{ij} - \log\,q'_{ij}\right)^2 = \left(\boldsymbol{u}_j^\top \boldsymbol{v}_i - \log\,x_{ij}\right)^2$。 2. 为每个词$w_i$增加两个为标量的模型参数:中心词偏差项$b_i$和背景词偏差项$c_i$。 3. 将每个损失项的权重替换成函数$h(x_{ij})$。权重函数$h(x)$是值域在$[0,1]$的单调递增函数。 -如此一来,GloVe的目标是最小化损失函数 +如此一来,GloVe模型的目标是最小化损失函数 $$\sum_{i\in\mathcal{V}} \sum_{j\in\mathcal{V}} h(x_{ij}) \left(\boldsymbol{u}_j^\top \boldsymbol{v}_i + b_i + c_j - \log\,x_{ij}\right)^2.$$ -其中权重函数$h(x)$的一个建议选择是:当$x < c$(例如$c = 100$),令$h(x) = (x/c)^\alpha$(例如$\alpha = 0.75$),反之令$h(x) = 1$。因为$h(0)=0$,所以对于$x_{ij}=0$的平方损失项可以直接忽略。当使用小批量随机梯度下降来训练时,每个时间步我们随机采样小批量非零$x_{ij}$,然后计算梯度来迭代模型参数。这些非零$x_{ij}$是预先基于整个数据集计算得到的,包含了数据集的全局统计信息。因此,GloVe的命名取“全局向量”(“Global Vectors”)之意。 +其中权重函数$h(x)$的一个建议选择是:当$x < c$时(如$c = 100$),令$h(x) = (x/c)^\alpha$(如$\alpha = 0.75$),反之令$h(x) = 1$。因为$h(0)=0$,所以对于$x_{ij}=0$的平方损失项可以直接忽略。当使用小批量随机梯度下降来训练时,每个时间步我们随机采样小批量非零$x_{ij}$,然后计算梯度来迭代模型参数。这些非零$x_{ij}$是预先基于整个数据集计算得到的,包含了数据集的全局统计信息。因此,GloVe模型的命名取“全局向量”(Global Vectors)之意。 -需要强调的是,如果词$w_i$出现在词$w_j$的背景窗口里,那么词$w_j$也会出现在词$w_i$的背景窗口里。也就是说,$x_{ij}=x_{ji}$。不同于word2vec中拟合的是非对称的条件概率$p_{ij}$,GloVe拟合的是对称的$\log\, x_{ij}$。因此,任意词的中心词向量和背景词向量在GloVe中是等价的。但由于初始化值的不同,同一个词最终学习到的两组词向量可能不同。当学习得到所有词向量以后,GloVe使用中心词向量与背景词向量之和作为该词的最终词向量。 +需要强调的是,如果词$w_i$出现在词$w_j$的背景窗口里,那么词$w_j$也会出现在词$w_i$的背景窗口里。也就是说,$x_{ij}=x_{ji}$。不同于word2vec中拟合的是非对称的条件概率$p_{ij}$,GloVe模型拟合的是对称的$\log\, x_{ij}$。因此,任意词的中心词向量和背景词向量在GloVe模型中是等价的。但由于初始化值的不同,同一个词最终学习到的两组词向量可能不同。当学习得到所有词向量以后,GloVe模型使用中心词向量与背景词向量之和作为该词的最终词向量。 -## 从条件概率比值理解GloVe +## 从条件概率比值理解GloVe模型 -我们还可以从另外一个角度来理解GloVe词嵌入。沿用本节前面的符号,$\mathbb{P}(w_j \mid w_i)$表示数据集中以$w_i$为中心词生成背景词$w_j$的条件概率,并记作$p_{ij}$。作为源于某大型语料库的真实例子,以下列举了两组分别以“ice”(“冰”)和“steam”(“蒸汽”)为中心词的条件概率以及它们之间的比值 [1]: +我们还可以从另外一个角度来理解GloVe模型。沿用本节前面的符号,$\mathbb{P}(w_j \mid w_i)$表示数据集中以$w_i$为中心词生成背景词$w_j$的条件概率,并记作$p_{ij}$。作为源于某大型语料库的真实例子,以下列举了两组分别以“ice”(冰)和“steam”(蒸汽)为中心词的条件概率以及它们之间的比值 [1]: |$w_k$=|“solid”|“gas”|“water”|“fashion”| |--:|:-:|:-:|:-:|:-:| @@ -48,38 +48,38 @@ $$\sum_{i\in\mathcal{V}} \sum_{j\in\mathcal{V}} h(x_{ij}) \left(\boldsymbol{u}_j |$p_1/p_2$|8.9|0.085|1.36|0.96| -我们可以观察到以下现象: +我们可以观察到以下现象。 -* 对于与“ice”相关而与“steam”不相关的词$w_k$,例如$w_k=$“solid”(“固体”),我们期望条件概率比值较大,例如上表最后一行中的值8.9; -* 对于与“ice”不相关而与“steam”相关的词$w_k$,例如$w_k=$“gas”(“气体”),我们期望条件概率比值较小,例如上表最后一行中的值0.085; -* 对于与“ice”和“steam”都相关的词$w_k$,例如$w_k=$“water”(“水”),我们期望条件概率比值接近1,例如上表最后一行中的值1.36; -* 对于与“ice”和“steam”都不相关的词$w_k$,例如$w_k=$“fashion”(“时尚”),我们期望条件概率比值接近1,例如上表最后一行中的值0.96。 +* 对于与“ice”相关而与“steam”不相关的词$w_k$,如$w_k=$“solid”(固体),我们期望条件概率比值较大,如上表最后一行中的值8.9; +* 对于与“ice”不相关而与“steam”相关的词$w_k$,如$w_k=$“gas”(气体),我们期望条件概率比值较小,如上表最后一行中的值0.085; +* 对于与“ice”和“steam”都相关的词$w_k$,如$w_k=$“water”(水),我们期望条件概率比值接近1,如上表最后一行中的值1.36; +* 对于与“ice”和“steam”都不相关的词$w_k$,如$w_k=$“fashion”(时尚),我们期望条件概率比值接近1,如上表最后一行中的值0.96。 -由此可见,条件概率比值能比较直观地表达词与词之间的关系。我们可以构造一个词向量函数使得它能有效拟合条件概率比值。我们知道,任意一个这样的比值需要三个词$w_i$、$w_j$和$w_k$。以$w_i$作为中心词的条件概率比值为${p_{ij}}/{p_{ik}}$。我们可以找一个函数,它使用词向量来拟合这个条件概率比值 +由此可见,条件概率比值能比较直观地表达词与词之间的关系。我们可以构造一个词向量函数使它能有效拟合条件概率比值。我们知道,任意一个这样的比值需要3个词$w_i$、$w_j$和$w_k$。以$w_i$作为中心词的条件概率比值为${p_{ij}}/{p_{ik}}$。我们可以找一个函数,它使用词向量来拟合这个条件概率比值 $$f(\boldsymbol{u}_j, \boldsymbol{u}_k, {\boldsymbol{v}}_i) \approx \frac{p_{ij}}{p_{ik}}.$$ -这里函数$f$可能的设计并不唯一,我们只需考虑一种较为合理的可能性。注意到条件概率比值是一个标量,我们可以将$f$限制为一个标量函数:$f(\boldsymbol{u}_j, \boldsymbol{u}_k, {\boldsymbol{v}}_i) = f\left((\boldsymbol{u}_j - \boldsymbol{u}_k)^\top {\boldsymbol{v}}_i\right)$。交换索引$j$和$k$后可以看到函数$f$应该满足$f(x)f(-x)=1$,因此一个可能是$f(x)=\exp(x)$,于是 +这里函数$f$可能的设计并不唯一,我们只需考虑一种较为合理的可能性。注意到条件概率比值是一个标量,我们可以将$f$限制为一个标量函数:$f(\boldsymbol{u}_j, \boldsymbol{u}_k, {\boldsymbol{v}}_i) = f\left((\boldsymbol{u}_j - \boldsymbol{u}_k)^\top {\boldsymbol{v}}_i\right)$。交换索引$j$和$k$后可以看到函数$f$应该满足$f(x)f(-x)=1$,因此一种可能是$f(x)=\exp(x)$,于是 $$f(\boldsymbol{u}_j, \boldsymbol{u}_k, {\boldsymbol{v}}_i) = \frac{\exp\left(\boldsymbol{u}_j^\top {\boldsymbol{v}}_i\right)}{\exp\left(\boldsymbol{u}_k^\top {\boldsymbol{v}}_i\right)} \approx \frac{p_{ij}}{p_{ik}}.$$ -满足最右边约等号的一个可能是$\exp\left(\boldsymbol{u}_j^\top {\boldsymbol{v}}_i\right) \approx \alpha p_{ij}$,这里$\alpha$是一个常数。考虑到$p_{ij}=x_{ij}/x_i$,取对数后$\boldsymbol{u}_j^\top {\boldsymbol{v}}_i \approx \log\,\alpha + \log\,x_{ij} - \log\,x_i$。我们使用额外的偏差项来拟合$- \log\,\alpha + \log\,x_i$,例如中心词偏差项$b_i$和背景词偏差项$c_j$: +满足最右边约等号的一种可能是$\exp\left(\boldsymbol{u}_j^\top {\boldsymbol{v}}_i\right) \approx \alpha p_{ij}$,这里$\alpha$是一个常数。考虑到$p_{ij}=x_{ij}/x_i$,取对数后$\boldsymbol{u}_j^\top {\boldsymbol{v}}_i \approx \log\,\alpha + \log\,x_{ij} - \log\,x_i$。我们使用额外的偏差项来拟合$- \log\,\alpha + \log\,x_i$,例如,中心词偏差项$b_i$和背景词偏差项$c_j$: $$\boldsymbol{u}_j^\top \boldsymbol{v}_i + b_i + c_j \approx \log(x_{ij}).$$ -对上式左右两边取平方误差并加权,我们可以得到GloVe的损失函数。 +对上式左右两边取平方误差并加权,我们可以得到GloVe模型的损失函数。 ## 小结 -* 在有些情况下,交叉熵损失函数有劣势。GloVe采用了平方损失,并通过词向量拟合预先基于整个数据集计算得到的全局统计信息。 -* 任意词的中心词向量和背景词向量在GloVe中是等价的。 +* 在有些情况下,交叉熵损失函数有劣势。GloVe模型采用了平方损失,并通过词向量拟合预先基于整个数据集计算得到的全局统计信息。 +* 任意词的中心词向量和背景词向量在GloVe模型中是等价的。 ## 练习 -* 如果一个词出现在另一个词的背景窗口中,如何利用它们之间在文本序列的距离重新设计条件概率$p_{ij}$的计算方式?提示:可参考GloVe论文4.2节 [1]。 -* 对于任意词,它在GloVe的中心词偏差项和背景词偏差项是否等价?为什么? +* 如果一个词出现在另一个词的背景窗口中,如何利用它们之间在文本序列的距离重新设计条件概率$p_{ij}$的计算方式?(提示:可参考GloVe论文4.2节 [1]。) +* 对于任意词,它在GloVe模型的中心词偏差项和背景词偏差项是否等价?为什么? ## 扫码直达[讨论区](https://discuss.gluon.ai/t/topic/4372) diff --git a/chapter_natural-language-processing/index.md b/chapter_natural-language-processing/index.md index e8bda0379..1a86c36ad 100644 --- a/chapter_natural-language-processing/index.md +++ b/chapter_natural-language-processing/index.md @@ -1,6 +1,6 @@ # 自然语言处理 -自然语言处理关注计算机与人类之间的自然语言交互。在实际中,我们常常使用自然语言处理技术,如“循环神经网络”一章中介绍的语言模型,来处理和分析大量的自然语言数据。本章中,根据输入与输出的不同形式,我们按“定长到定长”、“不定长到定长”、“不定长到不定长”的顺序,逐步展示在自然语言处理中如何表征并变换定长的词或类别,以及不定长的句子或段落序列。 +自然语言处理关注计算机与人类之间的自然语言交互。在实际中,我们常常使用自然语言处理技术,如“循环神经网络”一章中介绍的语言模型,来处理和分析大量的自然语言数据。本章中,根据输入与输出的不同形式,我们按“定长到定长”、“不定长到定长”、“不定长到不定长”的顺序,逐步展示在自然语言处理中如何表征并变换定长的词或类别以及不定长的句子或段落序列。 我们先介绍如何用向量表示词,并在语料库上训练词向量。之后,我们把在更大语料库上预训练的词向量应用于求近义词和类比词,即“定长到定长”。接着,在文本分类这种“不定长到定长”的任务中,我们进一步应用词向量来分析文本情感,并分别基于循环神经网络和卷积神经网络为表征时序数据提供两种思路。此外,自然语言处理任务中很多输出是不定长的,如任意长度的句子或段落。我们将描述应对这类问题的编码器—解码器模型、束搜索和注意力机制,并动手实践“不定长到不定长”的机器翻译任务。 diff --git a/chapter_natural-language-processing/machine-translation.md b/chapter_natural-language-processing/machine-translation.md index 63ac06a01..7d97441c5 100644 --- a/chapter_natural-language-processing/machine-translation.md +++ b/chapter_natural-language-processing/machine-translation.md @@ -1,6 +1,6 @@ # 机器翻译 -机器翻译是指将一段文本从一种语言自动翻译到另一种语言。因为一段文本序列在不同语言中长度不一定相同,所以我们使用机器翻译为例来介绍编码器—解码器和注意力机制的应用。 +机器翻译是指将一段文本从一种语言自动翻译到另一种语言。因为一段文本序列在不同语言中的长度不一定相同,所以我们使用机器翻译为例来介绍编码器—解码器和注意力机制的应用。 ## 读取和预处理数据 @@ -35,7 +35,7 @@ def build_data(all_tokens, all_seqs): return vocab, nd.array(indices) ``` -为了演示方便,我们在这里使用一个很小的法语—英语数据集。这个数据集里,每一行是一对法语句子和它对应的英语句子,中间使用`'\t'`隔开。在读取数据时,我们在句末附上“<eos>”符号,并可能通过添加“<pad>”符号使每个序列的长度均为`max_seq_len`。我们为法语词和英语词分别创建词典。法语词的索引和英语词的索引相互独立。 +为了演示方便,我们在这里使用一个很小的法语—英语数据集。在这个数据集里,每一行是一对法语句子和它对应的英语句子,中间使用`'\t'`隔开。在读取数据时,我们在句末附上“<eos>”符号,并可能通过添加“<pad>”符号使每个序列的长度均为`max_seq_len`。我们为法语词和英语词分别创建词典。法语词的索引和英语词的索引相互独立。 ```{.python .input n=31} def read_data(max_seq_len): @@ -69,7 +69,7 @@ dataset[0] ### 编码器 -在编码器中,我们将输入语言的词索引通过词嵌入层得到特征表达,然后输入到一个多层门控循环单元中。正如我们在[“循环神经网络的简洁实现”](../chapter_recurrent-neural-networks/rnn-gluon.md)一节提到的,Gluon的`rnn.GRU`实例在前向计算后也会分别返回输出和最终时间步的多层隐藏状态。其中的输出指的是最后一层的隐藏层在各个时间步的隐藏状态,并不涉及输出层计算。注意力机制将这些输出作为键项和值项。 +在编码器中,我们将输入语言的词索引通过词嵌入层得到词的表征,然后输入到一个多层门控循环单元中。正如我们在[“循环神经网络的简洁实现”](../chapter_recurrent-neural-networks/rnn-gluon.md)一节提到的,Gluon的`rnn.GRU`实例在前向计算后也会分别返回输出和最终时间步的多层隐藏状态。其中的输出指的是最后一层的隐藏层在各个时间步的隐藏状态,并不涉及输出层计算。注意力机制将这些输出作为键项和值项。 ```{.python .input n=165} class Encoder(nn.Block): @@ -88,7 +88,7 @@ class Encoder(nn.Block): return self.rnn.begin_state(*args, **kwargs) ``` -下面我们来创建一个批量大小为4,时间步数为7的小批量序列输入。设门控循环单元的隐藏层个数为2,隐藏单元个数为16。编码器对该输入执行前向计算后返回的输出形状为(时间步数,批量大小,隐藏单元个数)。门控循环单元在最终时间步的多层隐藏状态的形状为(隐藏层个数,批量大小,隐藏单元个数)。对于门控循环单元来说,`state`列表中只含一个元素,即隐藏状态;如果使用长短期记忆,`state`列表中还将包含另一个元素,即记忆细胞。 +下面我们来创建一个批量大小为4、时间步数为7的小批量序列输入。设门控循环单元的隐藏层个数为2,隐藏单元个数为16。编码器对该输入执行前向计算后返回的输出形状为(时间步数, 批量大小, 隐藏单元个数)。门控循环单元在最终时间步的多层隐藏状态的形状为(隐藏层个数, 批量大小, 隐藏单元个数)。对于门控循环单元来说,`state`列表中只含一个元素,即隐藏状态;如果使用长短期记忆,`state`列表中还将包含另一个元素,即记忆细胞。 ```{.python .input n=166} encoder = Encoder(vocab_size=10, embed_size=8, num_hiddens=16, num_layers=2) @@ -99,7 +99,7 @@ output.shape, state[0].shape ### 注意力机制 -在介绍如何实现注意力机制的矢量化计算之前,我们先了解一下`Dense`实例的`flatten`选项。当输入的维度大于2时,默认情况下,`Dense`实例会将除了第一维(样本维)以外的维度均视作需要仿射变换的特征维,并将输入自动转成行为样本、列为特征的二维矩阵。计算后,输出矩阵的形状为(样本数,输出个数)。如果我们希望全连接层只对输入的最后一维做仿射变换,而保持其他维度上的形状不变,便需要将`Dense`实例的`flatten`选项设为`False`。在下面例子中,全连接层只对输入的最后一维做仿射变换,因此输出形状中只有最后一维变为全连接层的输出个数2。 +在介绍如何实现注意力机制的矢量化计算之前,我们先了解一下`Dense`实例的`flatten`选项。当输入的维度大于2时,默认情况下,`Dense`实例会将除了第一维(样本维)以外的维度均视作需要仿射变换的特征维,并将输入自动转成行为样本、列为特征的二维矩阵。计算后,输出矩阵的形状为(样本数, 输出个数)。如果我们希望全连接层只对输入的最后一维做仿射变换,而保持其他维度上的形状不变,便需要将`Dense`实例的`flatten`选项设为`False`。在下面例子中,全连接层只对输入的最后一维做仿射变换,因此输出形状中只有最后一维变为全连接层的输出个数2。 ```{.python .input} dense = nn.Dense(2, flatten=False) @@ -107,7 +107,7 @@ dense.initialize() dense(nd.zeros((3, 5, 7))).shape ``` -我们将实现[“注意力机制”](./attention.md)一节中定义的函数$a$:将输入连结后通过含单隐藏层的多层感知机变换。其中隐藏层的输入是解码器的隐藏状态与编码器在所有时间步上隐藏状态的一一连结,且使用tanh作为激活函数。输出层的输出个数为1。两个`Dense`实例均不使用偏差,且设`flatten=False`。其中函数$a$定义里向量$\boldsymbol{v}$的长度是一个超参数,即`attention_size`。 +我们将实现[“注意力机制”](./attention.md)一节中定义的函数$a$:将输入连结后通过含单隐藏层的多层感知机变换。其中隐藏层的输入是解码器的隐藏状态与编码器在所有时间步上隐藏状态的一一连结,且使用tanh函数作为激活函数。输出层的输出个数为1。两个`Dense`实例均不使用偏差,且设`flatten=False`。其中函数$a$定义里向量$\boldsymbol{v}$的长度是一个超参数,即`attention_size`。 ```{.python .input n=167} def attention_model(attention_size): @@ -118,7 +118,7 @@ def attention_model(attention_size): return model ``` -注意力模型的输入包括查询项、键项和值项。设编码器和解码器的隐藏单元个数相同。这里的查询项为解码器在上一时间步的隐藏状态,形状为(批量大小,隐藏单元个数);键项和值项均为编码器在所有时间步的隐藏状态,形状为(时间步数,批量大小,隐藏单元个数)。注意力模型返回当前时间步的背景变量,形状为(批量大小,隐藏单元个数)。 +注意力机制的输入包括查询项、键项和值项。设编码器和解码器的隐藏单元个数相同。这里的查询项为解码器在上一时间步的隐藏状态,形状为(批量大小, 隐藏单元个数);键项和值项均为编码器在所有时间步的隐藏状态,形状为(时间步数, 批量大小, 隐藏单元个数)。注意力机制返回当前时间步的背景变量,形状为(批量大小, 隐藏单元个数)。 ```{.python .input n=168} def attention_forward(model, enc_states, dec_state): @@ -131,7 +131,7 @@ def attention_forward(model, enc_states, dec_state): return (alpha * enc_states).sum(axis=0) # 返回背景变量 ``` -在下面的例子中,编码器的时间步数为10,批量大小为4,编码器和解码器的隐藏单元个数均为8。注意力模型返回一个小批量的背景向量,每个背景向量的长度等于编码器的隐藏单元个数。因此输出的形状为(4,8)。 +在下面的例子中,编码器的时间步数为10,批量大小为4,编码器和解码器的隐藏单元个数均为8。注意力机制返回一个小批量的背景向量,每个背景向量的长度等于编码器的隐藏单元个数。因此输出的形状为(4, 8)。 ```{.python .input n=169} seq_len, batch_size, num_hiddens = 10, 4, 8 @@ -144,9 +144,9 @@ attention_forward(model, enc_states, dec_state).shape ### 含注意力机制的解码器 -我们直接将编码器在最终时间步的隐藏状态作为解码器的初始隐藏状态。这要求编码器和解码器的循环神经网络使用相同的层数和隐藏单元个数。 +我们直接将编码器在最终时间步的隐藏状态作为解码器的初始隐藏状态。这要求编码器和解码器的循环神经网络使用相同的隐藏层个数和隐藏单元个数。 -在解码器的前向计算中,我们先通过前面介绍的注意力模型计算得到当前时间步的背景向量。由于解码器的输入来自输出语言的词索引,我们将输入通过词嵌入层得到特征表达,然后和背景向量在特征维连结。我们将连接后的结果与上一时间步的隐藏状态通过门控循环单元计算出当前时间步的输出与隐藏状态。最后,我们将输出通过全连接层变换为有关各个输出词的预测,形状为(批量大小,输出词典大小)。 +在解码器的前向计算中,我们先通过刚刚介绍的注意力机制计算得到当前时间步的背景向量。由于解码器的输入来自输出语言的词索引,我们将输入通过词嵌入层得到表征,然后和背景向量在特征维连结。我们将连结后的结果与上一时间步的隐藏状态通过门控循环单元计算出当前时间步的输出与隐藏状态。最后,我们将输出通过全连接层变换为有关各个输出词的预测,形状为(批量大小, 输出词典大小)。 ```{.python .input n=170} class Decoder(nn.Block): @@ -174,9 +174,9 @@ class Decoder(nn.Block): return enc_state ``` -## 训练 +## 训练模型 -我们先实现`batch_loss`函数计算一个小批量的损失。解码器在最初时间步的输入是特殊字符`BOS`。之后,解码器在某时间步的输入为样本输出序列在上一时间步的词,即强制教学。此外,同[“Word2vec的实现”](word2vec-gluon.md)一节中的实现一样,我们在这里也使用掩码变量避免填充项对损失函数计算的影响。 +我们先实现`batch_loss`函数计算一个小批量的损失。解码器在最初时间步的输入是特殊字符`BOS`。之后,解码器在某时间步的输入为样本输出序列在上一时间步的词,即强制教学。此外,同[“word2vec的实现”](word2vec-gluon.md)一节中的实现一样,我们在这里也使用掩码变量避免填充项对损失函数计算的影响。 ```{.python .input} def batch_loss(encoder, decoder, X, Y, loss): @@ -225,7 +225,7 @@ def train(encoder, decoder, dataset, lr, batch_size, num_epochs): print("epoch %d, loss %.3f" % (epoch + 1, l_sum / len(data_iter))) ``` -接下来创建模型实例并设置超参数。然后我们就可以训练模型了。 +接下来,创建模型实例并设置超参数。然后,我们就可以训练模型了。 ```{.python .input} embed_size, num_hiddens, num_layers = 64, 64, 2 @@ -237,9 +237,9 @@ decoder = Decoder(len(out_vocab), embed_size, num_hiddens, num_layers, train(encoder, decoder, dataset, lr, batch_size, num_epochs) ``` -## 预测 +## 预测不定长的序列 -在[“束搜索”](beam-search.md)一节中我们介绍了三种方法来生成解码器在每个时间步的输出。这里我们实现最简单的贪婪搜索。 +在[“束搜索”](beam-search.md)一节中我们介绍了3种方法来生成解码器在每个时间步的输出。这里我们实现最简单的贪婪搜索。 ```{.python .input n=177} def translate(encoder, decoder, input_seq, max_seq_len): @@ -272,17 +272,17 @@ translate(encoder, decoder, input_seq, max_seq_len) ## 评价翻译结果 -评机器翻译结果通常使用BLEU(Bilingual Evaluation Understudy)[1]。对于模型预测序列中任意的子序列,BLEU考察这个子序列是否出现在标签序列中。 +评价机器翻译结果通常使用BLEU(Bilingual Evaluation Understudy)[1]。对于模型预测序列中任意的子序列,BLEU考察这个子序列是否出现在标签序列中。 -具体来说,设词数为$n$的子序列的精度为$p_n$。它是预测序列与标签序列匹配词数为$n$的子序列的数量与预测序列中词数为$n$的子序列的数量之比。举个例子,假设标签序列为$A$、$B$、$C$、$D$、$E$、$F$,预测序列为$A$、$B$、$B$、$C$、$D$。那么$p_1 = 4/5,\ p_2 = 3/4,\ p_3 = 1/3,\ p_4 = 0$。设$len_{\text{label}}$和$len_{\text{pred}}$分别为标签序列和预测序列的词数。那么,BLEU的定义为 +具体来说,设词数为$n$的子序列的精度为$p_n$。它是预测序列与标签序列匹配词数为$n$的子序列的数量与预测序列中词数为$n$的子序列的数量之比。举个例子,假设标签序列为$A$、$B$、$C$、$D$、$E$、$F$,预测序列为$A$、$B$、$B$、$C$、$D$,那么$p_1 = 4/5,\ p_2 = 3/4,\ p_3 = 1/3,\ p_4 = 0$。设$len_{\text{label}}$和$len_{\text{pred}}$分别为标签序列和预测序列的词数,那么,BLEU的定义为 $$ \exp\left(\min\left(0, 1 - \frac{len_{\text{label}}}{len_{\text{pred}}}\right)\right) \prod_{n=1}^k p_n^{1/2^n},$$ 其中$k$是我们希望匹配的子序列的最大词数。可以看到当预测序列和标签序列完全一致时,BLEU为1。 -因为匹配较长子序列比匹配较短子序列更难,BLEU对匹配较长子序列的精度赋予了更大权重。例如当$p_n$固定在0.5时,随着$n$的增大,$0.5^{1/2} \approx 0.7, 0.5^{1/4} \approx 0.84, 0.5^{1/8} \approx 0.92, 0.5^{1/16} \approx 0.96$。另外,模型预测较短序列往往会得到较高$p_n$值。因此,上式中连乘项前面的系数是为了惩罚较短的输出。举个例子,当$k=2$时,假设标签序列为$A$、$B$、$C$、$D$、$E$、$F$,而预测序列为$A$、$B$。虽然$p_1 = p_2 = 1$,但惩罚系数$\exp(1-6/2) \approx 0.14$,因此BLEU也接近0.14。 +因为匹配较长子序列比匹配较短子序列更难,BLEU对匹配较长子序列的精度赋予了更大权重。例如,当$p_n$固定在0.5时,随着$n$的增大,$0.5^{1/2} \approx 0.7, 0.5^{1/4} \approx 0.84, 0.5^{1/8} \approx 0.92, 0.5^{1/16} \approx 0.96$。另外,模型预测较短序列往往会得到较高$p_n$值。因此,上式中连乘项前面的系数是为了惩罚较短的输出而设的。举个例子,当$k=2$时,假设标签序列为$A$、$B$、$C$、$D$、$E$、$F$,而预测序列为$A$、$B$。虽然$p_1 = p_2 = 1$,但惩罚系数$\exp(1-6/2) \approx 0.14$,因此BLEU也接近0.14。 -下面实现BLEU的计算。 +下面来实现BLEU的计算。 ```{.python .input} def bleu(pred_tokens, label_tokens, k): @@ -297,7 +297,7 @@ def bleu(pred_tokens, label_tokens, k): return score ``` -并定义一个辅助打印函数。 +接下来,定义一个辅助打印函数。 ```{.python .input} def score(input_seq, label_seq, k): @@ -307,7 +307,7 @@ def score(input_seq, label_seq, k): ' '.join(pred_tokens))) ``` -预测正确是分数为1。 +预测正确则分数为1。 ```{.python .input} score('ils regardent .', 'they are watching .', k=2) @@ -321,14 +321,14 @@ score('ils sont canadiens .', 'they are canadian .', k=2) ## 小结 -* 我们可以将编码器—解码器和注意力机制应用于机器翻译中。 +* 可以将编码器—解码器和注意力机制应用于机器翻译中。 * BLEU可以用来评价翻译结果。 ## 练习 -* 如果编码器和解码器的隐藏单元个数不同或层数不同,我们该如何改进解码器的隐藏状态初始化方法? -* 在训练中,将强制教学替换为使用解码器在上一时间步的输出作为解码器在当前时间步的输入。结果有什么变化吗? -* 试着使用更大的翻译数据集来训练模型,例如WMT [2] 和Tatoeba Project [3]。 +* 如果编码器和解码器的隐藏单元个数不同或隐藏层个数不同,该如何改进解码器的隐藏状态的初始化方法? +* 在训练中,将强制教学替换为使用解码器在上一时间步的输出作为解码器在当前时间步的输入,结果有什么变化吗? +* 试着使用更大的翻译数据集来训练模型,如WMT [2] 和Tatoeba Project [3]。 ## 扫码直达[讨论区](https://discuss.gluon.ai/t/topic/4689) diff --git a/chapter_natural-language-processing/sentiment-analysis-cnn.md b/chapter_natural-language-processing/sentiment-analysis-cnn.md index e13198f24..7670d3dfd 100644 --- a/chapter_natural-language-processing/sentiment-analysis-cnn.md +++ b/chapter_natural-language-processing/sentiment-analysis-cnn.md @@ -1,6 +1,8 @@ # 文本情感分类:使用卷积神经网络(textCNN) -在“卷积神经网络”一章中我们探究了如何使用二维卷积神经网络来处理二维图像数据。在之前的语言模型和文本分类任务中,我们将文本数据看作是只有一个维度的时间序列,并很自然地使用循环神经网络来处理这样的数据。其实,我们也可以将文本当做是一维图像,从而可以用一维卷积神经网络来捕捉临近词之间的关联。本节将介绍将卷积神经网络应用到文本分析的开创性工作之一:textCNN [1]。首先导入实验所需的包和模块。 +在“卷积神经网络”一章中我们探究了如何使用二维卷积神经网络来处理二维图像数据。在之前的语言模型和文本分类任务中,我们将文本数据看作是只有一个维度的时间序列,并很自然地使用循环神经网络来表征这样的数据。其实,我们也可以将文本当作一维图像,从而可以用一维卷积神经网络来捕捉临近词之间的关联。本节将介绍将卷积神经网络应用到文本分析的开创性工作之一:textCNN [1]。 + +首先导入实验所需的包和模块。 ```{.python .input n=2} import d2lzh as d2l @@ -11,9 +13,9 @@ from mxnet.gluon import data as gdata, loss as gloss, nn ## 一维卷积层 -在介绍模型前我们先来解释一维卷积层的工作原理。和二维卷积层一样,一维卷积层使用一维的互相关运算。在一维互相关运算中,卷积窗口从输入数组的最左方开始,按从左往右的顺序,依次在输入数组上滑动。当卷积窗口滑动到某一位置时,窗口中的输入子数组与核数组按元素相乘并求和,得到输出数组中相应位置的元素。如图10.4所示,输入是一个宽为7的一维数组,核数组的宽为2。可以看到输出的宽度为$7-2+1=6$,且第一个元素是由输入的最左边的宽为2的子数组与核数组按元素相乘后再相加得到的。 +在介绍模型前我们先来解释一维卷积层的工作原理。与二维卷积层一样,一维卷积层使用一维的互相关运算。在一维互相关运算中,卷积窗口从输入数组的最左方开始,按从左往右的顺序,依次在输入数组上滑动。当卷积窗口滑动到某一位置时,窗口中的输入子数组与核数组按元素相乘并求和,得到输出数组中相应位置的元素。如图10.4所示,输入是一个宽为7的一维数组,核数组的宽为2。可以看到输出的宽度为$7-2+1=6$,且第一个元素是由输入的最左边的宽为2的子数组与核数组按元素相乘后再相加得到的:$0\times1+1\times2=2$。 -![一维互相关运算。阴影部分为第一个输出元素及其计算所使用的输入和核数组元素:$0\times1+1\times2=2$](../img/conv1d.svg) +![一维互相关运算](../img/conv1d.svg) 下面我们将一维互相关运算实现在`corr1d`函数里。它接受输入数组`X`和核数组`K`,并输出数组`Y`。 @@ -26,18 +28,18 @@ def corr1d(X, K): return Y ``` -让我们重现图10.4中一维互相关运算的结果。 +让我们复现图10.4中一维互相关运算的结果。 ```{.python .input n=4} X, K = nd.array([0, 1, 2, 3, 4, 5, 6]), nd.array([1, 2]) corr1d(X, K) ``` -多输入通道的一维互相关运算也与多输入通道的二维互相关运算类似:在每个通道上,将核与相应的输入做一维互相关运算,并将通道之间的结果相加得到输出结果。图10.5展示了含3个输入通道的一维互相关运算。 +多输入通道的一维互相关运算也与多输入通道的二维互相关运算类似:在每个通道上,将核与相应的输入做一维互相关运算,并将通道之间的结果相加得到输出结果。图10.5展示了含3个输入通道的一维互相关运算,其中阴影部分为第一个输出元素及其计算所使用的输入和核数组元素:$0\times1+1\times2+1\times3+2\times4+2\times(-1)+3\times(-3)=2$。 -![含3个输入通道的一维互相关运算。阴影部分为第一个输出元素及其计算所使用的输入和核数组元素:$0\times1+1\times2+1\times3+2\times4+2\times(-1)+3\times(-3)=2$](../img/conv1d-channel.svg) +![含3个输入通道的一维互相关运算](../img/conv1d-channel.svg) -让我们重现图10.5中多输入通道的一维互相关运算的结果。 +让我们复现图10.5中多输入通道的一维互相关运算的结果。 ```{.python .input n=5} def corr1d_multi_in(X, K): @@ -52,18 +54,18 @@ K = nd.array([[1, 2], [3, 4], [-1, -3]]) corr1d_multi_in(X, K) ``` -由二维互相关运算的定义可知,多输入通道的一维互相关运算可以看作是单输入通道的二维互相关运算。如图10.6所示,我们也可以将图10.5中多输入通道的一维互相关运算以等价的单输入通道的二维互相关运算呈现。这里核的高等于输入的高。 +由二维互相关运算的定义可知,多输入通道的一维互相关运算可以看作单输入通道的二维互相关运算。如图10.6所示,我们也可以将图10.5中多输入通道的一维互相关运算以等价的单输入通道的二维互相关运算呈现。这里核的高等于输入的高。图10.6中的阴影部分为第一个输出元素及其计算所使用的输入和核数组元素:$2\times(-1)+3\times(-3)+1\times3+2\times4+0\times1+1\times2=2$。 -![单输入通道的二维互相关运算。高亮部分为第一个输出元素及其计算所使用的输入和核数组元素:$2\times(-1)+3\times(-3)+1\times3+2\times4+0\times1+1\times2=2$](../img/conv1d-2d.svg) +![单输入通道的二维互相关运算](../img/conv1d-2d.svg) 图10.4和图10.5中的输出都只有一个通道。我们在[“多输入通道和多输出通道”](../chapter_convolutional-neural-networks/channels.md)一节中介绍了如何在二维卷积层中指定多个输出通道。类似地,我们也可以在一维卷积层指定多个输出通道,从而拓展卷积层中的模型参数。 ## 时序最大池化层 -类似地,我们有一维池化层。TextCNN中使用的时序最大池化层(max-over-time pooling)实际上对应一维全局最大池化层:假设输入包含多个通道,各通道由不同时间步上的数值组成,各通道的输出即该通道所有时间步中最大的数值。因此,时序最大池化层的输入在各个通道上的时间步数可以不同。 +类似地,我们有一维池化层。textCNN中使用的时序最大池化(max-over-time pooling)层实际上对应一维全局最大池化层:假设输入包含多个通道,各通道由不同时间步上的数值组成,各通道的输出即该通道所有时间步中最大的数值。因此,时序最大池化层的输入在各个通道上的时间步数可以不同。 -为提升计算性能,我们常常将不同长度的时序样本组成一个小批量,并通过在较短序列后附加特殊字符(例如0)令批量中各时序样本长度相同。这些人为添加的特殊字符当然是无意义的。由于时序最大池化的主要目的是抓取时序中最重要的特征,它通常能使模型不受人为添加字符的影响。 +为提升计算性能,我们常常将不同长度的时序样本组成一个小批量,并通过在较短序列后附加特殊字符(如0)令批量中各时序样本长度相同。这些人为添加的特殊字符当然是无意义的。由于时序最大池化的主要目的是抓取时序中最重要的特征,它通常能使模型不受人为添加字符的影响。 ## 读取和预处理IMDb数据集 @@ -81,19 +83,19 @@ test_iter = gdata.DataLoader(gdata.ArrayDataset( *d2l.preprocess_imdb(test_data, vocab)), batch_size) ``` -## TextCNN模型 +## textCNN模型 -TextCNN主要使用了一维卷积层和时序最大池化层。假设输入的文本序列由$n$个词组成,每个词用$d$维的词向量表示。那么输入样本的宽为$n$,高为1,输入通道数为$d$。textCNN的计算主要分为以下几步: +textCNN模型主要使用了一维卷积层和时序最大池化层。假设输入的文本序列由$n$个词组成,每个词用$d$维的词向量表示。那么输入样本的宽为$n$,高为1,输入通道数为$d$。textCNN的计算主要分为以下几步。 1. 定义多个一维卷积核,并使用这些卷积核对输入分别做卷积计算。宽度不同的卷积核可能会捕捉到不同个数的相邻词的相关性。 2. 对输出的所有通道分别做时序最大池化,再将这些通道的池化输出值连结为向量。 3. 通过全连接层将连结后的向量变换为有关各类别的输出。这一步可以使用丢弃层应对过拟合。 -![textCNN的设计](../img/textcnn.svg) +图10.7用一个例子解释了textCNN的设计。这里的输入是一个有11个词的句子,每个词用6维词向量表示。因此输入序列的宽为11,输入通道数为6。给定2个一维卷积核,核宽分别为2和4,输出通道数分别设为4和5。因此,一维卷积计算后,4个输出通道的宽为$11-2+1=10$,而其他5个通道的宽为$11-4+1=8$。尽管每个通道的宽不同,我们依然可以对各个通道做时序最大池化,并将9个通道的池化输出连结成一个9维向量。最终,使用全连接将9维向量变换为2维输出,即正面情感和负面情感的预测。 -图10.7用一个例子解释了textCNN的设计。这里的输入是一个有11个词的句子,每个词用6维词向量表示。因此输入序列的宽为11,输入通道数为6。给定2个一维卷积核,核宽分别为2和4,输出通道数分别设为4和5。因此,一维卷积计算后,4个输出通道的宽为$11-2+1=10$,而其他5个通道的宽为$11-4+1=8$。尽管每个通道的宽不同,我们依然可以对各个通道做时序最大池化,并将9个通道的池化输出连结成一个9维向量。最终,我们使用全连接将9维向量变换为2维输出:正面情感和负面情感的预测。 +![textCNN的设计](../img/textcnn.svg) -下面我们来实现textCNN模型。跟上一节相比,除了用一维卷积层替换循环神经网络外,这里我们使用了两个嵌入层,一个的权重固定,另一个则参与训练。 +下面我们来实现textCNN模型。与上一节相比,除了用一维卷积层替换循环神经网络外,这里我们还使用了两个嵌入层,一个的权重固定,另一个则参与训练。 ```{.python .input n=10} class TextCNN(nn.Block): @@ -126,7 +128,7 @@ class TextCNN(nn.Block): return outputs ``` -创建一个TextCNN实例。它有3个卷积层,它们的核宽分别为3、4和5,输出通道数均为100。 +创建一个`TextCNN`实例。它有3个卷积层,它们的核宽分别为3、4和5,输出通道数均为100。 ```{.python .input} embed_size, kernel_sizes, nums_channels = 100, [3, 4, 5], [100, 100, 100] @@ -137,7 +139,7 @@ net.initialize(init.Xavier(), ctx=ctx) ### 加载预训练的词向量 -同上一节一样,加载预训练的100维GloVe词向量,并分别初始化嵌入层`embedding`和`constant_embedding`。其中前者参与训练,而后者权重固定。 +同上一节一样,加载预训练的100维GloVe词向量,并分别初始化嵌入层`embedding`和`constant_embedding`,前者参与训练,而后者权重固定。 ```{.python .input n=7} glove_embedding = text.embedding.create( @@ -149,7 +151,7 @@ net.constant_embedding.collect_params().setattr('grad_req', 'null') ### 训练并评价模型 -现在我们可以训练模型了。 +现在就可以训练模型了。 ```{.python .input n=30} lr, num_epochs = 0.001, 5 @@ -158,7 +160,7 @@ loss = gloss.SoftmaxCrossEntropyLoss() d2l.train(train_iter, test_iter, net, loss, trainer, ctx, num_epochs) ``` -下面我们使用训练好的模型对两个简单句子的情感进行分类。 +下面,我们使用训练好的模型对两个简单句子的情感进行分类。 ```{.python .input} d2l.predict_sentiment(net, vocab, ['this', 'movie', 'is', 'so', 'great']) @@ -170,17 +172,17 @@ d2l.predict_sentiment(net, vocab, ['this', 'movie', 'is', 'so', 'bad']) ## 小结 -* 我们可以使用一维卷积来处理和分析时序数据。 -* 多输入通道的一维互相关运算可以看作是单输入通道的二维互相关运算。 +* 可以使用一维卷积来表征时序数据。 +* 多输入通道的一维互相关运算可以看作单输入通道的二维互相关运算。 * 时序最大池化层的输入在各个通道上的时间步数可以不同。 -* TextCNN主要使用了一维卷积层和时序最大池化层。 +* textCNN主要使用了一维卷积层和时序最大池化层。 ## 练习 * 动手调参,从准确率和运行效率比较情感分析的两类方法:使用循环神经网络和使用卷积神经网络。 -* 使用上一节练习中介绍的三种方法:调节超参数、使用更大的预训练词向量和使用spaCy分词工具,你能使模型在测试集上的准确率进一步提高吗? -* 你还能将textCNN应用于自然语言处理的哪些任务中? +* 使用上一节练习中介绍的3种方法(调节超参数、使用更大的预训练词向量和使用spaCy分词工具),能使模型在测试集上的准确率进一步提高吗? +* 还能将textCNN应用于自然语言处理的哪些任务中? ## 扫码直达[讨论区](https://discuss.gluon.ai/t/topic/7762) diff --git a/chapter_natural-language-processing/sentiment-analysis-rnn.md b/chapter_natural-language-processing/sentiment-analysis-rnn.md index 357247413..924d183e4 100644 --- a/chapter_natural-language-processing/sentiment-analysis-rnn.md +++ b/chapter_natural-language-processing/sentiment-analysis-rnn.md @@ -2,7 +2,9 @@ 文本分类是自然语言处理的一个常见任务,它把一段不定长的文本序列变换为文本的类别。本节关注它的一个子问题:使用文本情感分类来分析文本作者的情绪。这个问题也叫情感分析,并有着广泛的应用。例如,我们可以分析用户对产品的评论并统计用户的满意度,或者分析用户对市场行情的情绪并用以预测接下来的行情。 -同搜索近义词和类比词一样,文本分类也属于词嵌入的下游应用。本节中,我们将应用预训练的词向量和含多个隐藏层的双向循环神经网络。我们将用它们来判断一段不定长的文本序列中包含的是正面还是负面的情绪。在实验开始前,导入所需的包或模块。 +同搜索近义词和类比词一样,文本分类也属于词嵌入的下游应用。在本节中,我们将应用预训练的词向量和含多个隐藏层的双向循环神经网络,来判断一段不定长的文本序列中包含的是正面还是负面的情绪。 + +在实验开始前,导入所需的包或模块。 ```{.python .input n=2} import collections @@ -17,11 +19,11 @@ import tarfile ## 文本情感分类数据 -我们使用Stanford's Large Movie Review Dataset作为文本情感分类的数据集 [1]。这个数据集分为训练和测试用的两个数据集,分别包含25,000条从IMDb下载的关于电影的评论。在每个数据集中,标签为”正面”和“负面”的评论数量相等。 +我们使用斯坦福的IMDb数据集(Stanford's Large Movie Review Dataset)作为文本情感分类的数据集 [1]。这个数据集分为训练和测试用的两个数据集,分别包含25,000条从IMDb下载的关于电影的评论。在每个数据集中,标签为“正面”和“负面”的评论数量相等。 ### 读取数据 -我们首先下载这个数据集到“../data”路径下,然后解压至“../data/aclImdb”下。 +首先下载这个数据集到`../data`路径下,然后解压至`../data/aclImdb`下。 ```{.python .input n=3} # 本函数已保存在d2lzh包中方便以后使用 @@ -35,7 +37,7 @@ def download_imdb(data_dir='../data'): download_imdb() ``` -下面,读取训练和测试数据集。每个样本是一条评论和其对应的标签:1表示“正面”,0表示“负面”。 +接下来,读取训练数据集和测试数据集。每个样本是一条评论及其对应的标签:1表示“正面”,0表示“负面”。 ```{.python .input n=13} def read_imdb(folder='train'): # 本函数已保存在d2lzh包中方便以后使用 @@ -75,7 +77,7 @@ vocab = get_vocab_imdb(train_data) '# words in vocab:', len(vocab) ``` -因为每条评论长度不一致使得不能直接组合成小批量,我们定义`preprocess_imdb`函数对每条评论进行分词,并通过词典转换成词索引,然后通过截断或者补0来将每条评论长度固定成500。 +因为每条评论长度不一致所以不能直接组合成小批量,我们定义`preprocess_imdb`函数对每条评论进行分词,并通过词典转换成词索引,然后通过截断或者补0来将每条评论长度固定成500。 ```{.python .input n=44} def preprocess_imdb(data, vocab): # 本函数已保存在d2lzh包中方便以后使用 @@ -102,7 +104,7 @@ train_iter = gdata.DataLoader(train_set, batch_size, shuffle=True) test_iter = gdata.DataLoader(test_set, batch_size) ``` -打印第一个小批量数据的形状,以及训练集中小批量的个数。 +打印第一个小批量数据的形状以及训练集中小批量的个数。 ```{.python .input} for X, y in train_iter: @@ -113,7 +115,7 @@ for X, y in train_iter: ## 使用循环神经网络的模型 -在这个模型中,每个词先通过嵌入层得到特征向量。然后,我们使用双向循环神经网络对特征序列进一步编码得到序列信息。最后,我们将编码的序列信息通过全连接层变换为输出。具体来说,我们可以将双向长短期记忆在最初时间步和最终时间步的隐藏状态连结,作为特征序列的编码信息传递给输出层分类。在下面实现的`BiRNN`类中,`Embedding`实例即嵌入层,`LSTM`实例即为序列编码的隐藏层,`Dense`实例即生成分类结果的输出层。 +在这个模型中,每个词先通过嵌入层得到特征向量。然后,我们使用双向循环神经网络对特征序列进一步编码得到序列信息。最后,我们将编码的序列信息通过全连接层变换为输出。具体来说,我们可以将双向长短期记忆在最初时间步和最终时间步的隐藏状态连结,作为特征序列的表征传递给输出层分类。在下面实现的`BiRNN`类中,`Embedding`实例即嵌入层,`LSTM`实例即为序列编码的隐藏层,`Dense`实例即生成分类结果的输出层。 ```{.python .input n=46} class BiRNN(nn.Block): @@ -155,7 +157,7 @@ glove_embedding = text.embedding.create( 'glove', pretrained_file_name='glove.6B.100d.txt', vocabulary=vocab) ``` -然后我们将用这些词向量作为评论中每个词的特征向量。注意预训练词向量的维度需要跟创建的模型中的嵌入层输出大小`embed_size`一致。此外,在训练中我们不再更新这些词向量。 +然后,我们将用这些词向量作为评论中每个词的特征向量。注意,预训练词向量的维度需要与创建的模型中的嵌入层输出大小`embed_size`一致。此外,在训练中我们不再更新这些词向量。 ```{.python .input n=47} net.embedding.weight.set_data(glove_embedding.idx_to_vec) @@ -164,7 +166,7 @@ net.embedding.collect_params().setattr('grad_req', 'null') ### 训练并评价模型 -这时候我们可以开始训练了。 +这时候就可以开始训练模型了。 ```{.python .input n=48} lr, num_epochs = 0.01, 5 @@ -183,7 +185,7 @@ def predict_sentiment(net, vocab, sentence): return 'positive' if label.asscalar() == 1 else 'negative' ``` -然后使用训练好的模型对两个简单句子的情感进行分类。 +下面使用训练好的模型对两个简单句子的情感进行分类。 ```{.python .input n=50} predict_sentiment(net, vocab, ['this', 'movie', 'is', 'so', 'great']) @@ -196,16 +198,16 @@ predict_sentiment(net, vocab, ['this', 'movie', 'is', 'so', 'bad']) ## 小结 * 文本分类把一段不定长的文本序列变换为文本的类别。它属于词嵌入的下游应用。 -* 我们可以应用预训练的词向量和循环神经网络对文本的情感进行分类。 +* 可以应用预训练的词向量和循环神经网络对文本的情感进行分类。 ## 练习 -* 增加迭代周期。你的模型能在训练和测试数据集上得到怎样的准确率?再调节其他超参数试试? +* 增加迭代周期。训练后的模型能在训练和测试数据集上得到怎样的准确率?再调节其他超参数试试? -* 使用更大的预训练词向量,例如300维的GloVe词向量,能否提升分类准确率? +* 使用更大的预训练词向量,如300维的GloVe词向量,能否提升分类准确率? -* 使用spaCy分词工具,能否提升分类准确率?你需要安装spaCy:`pip install spacy`,并且安装英文包:`python -m spacy download en`。在代码中,先导入spacy:`import spacy`。然后加载spacy英文包:`spacy_en = spacy.load('en')`。最后定义函数`def tokenizer(text): return [tok.text for tok in spacy_en.tokenizer(text)]`并替换原来的基于空格分词的`tokenizer`函数。需要注意的是,GloVe的词向量对于名词词组的存储方式是用“-”连接各个单词,例如词组“new york”在GloVe中的表示为“new-york”。而使用spaCy分词之后“new york”的存储可能是“new york”。 +* 使用spaCy分词工具,能否提升分类准确率?你需要安装spaCy(`pip install spacy`),并且安装英文包(`python -m spacy download en`)。在代码中,先导入spacy(`import spacy`)。然后加载spacy英文包(`spacy_en = spacy.load('en')`)。最后定义函数`def tokenizer(text): return [tok.text for tok in spacy_en.tokenizer(text)]`并替换原来的基于空格分词的`tokenizer`函数。需要注意的是,GloVe词向量对于名词词组的存储方式是用“-”连接各个单词,例如,词组“new york”在GloVe词向量中的表示为“new-york”,而使用spaCy分词之后“new york”的存储可能是“new york”。 ## 扫码直达[讨论区](https://discuss.gluon.ai/t/topic/6155) diff --git a/chapter_natural-language-processing/seq2seq.md b/chapter_natural-language-processing/seq2seq.md index 26a0f2026..49293654d 100644 --- a/chapter_natural-language-processing/seq2seq.md +++ b/chapter_natural-language-processing/seq2seq.md @@ -1,20 +1,20 @@ # 编码器—解码器(seq2seq) -我们已经在之前章节中处理并分析了不定长的输入序列。但在很多应用中,输入和输出都可以是不定长序列。以机器翻译为例,输入可以是一段不定长的英语文本序列,输出可以是一段不定长的法语文本序列,例如 +我们已经在前两节中表征并变换了不定长的输入序列。但在自然语言处理的很多应用中,输入和输出都可以是不定长序列。以机器翻译为例,输入可以是一段不定长的英语文本序列,输出可以是一段不定长的法语文本序列,例如 > 英语输入:“They”、“are”、“watching”、“.” > 法语输出:“Ils”、“regardent”、“.” -当输入输出都是不定长序列时,我们可以使用编码器—解码器(encoder-decoder)[1] 或者seq2seq模型 [2]。这两个模型本质上都用到了两个循环神经网络,分别叫做编码器和解码器。编码器用来分析输入序列,解码器用来生成输出序列。 +当输入和输出都是不定长序列时,我们可以使用编码器—解码器(encoder-decoder)[1] 或者seq2seq模型 [2]。这两个模型本质上都用到了两个循环神经网络,分别叫做编码器和解码器。编码器用来分析输入序列,解码器用来生成输出序列。 -图10.8描述了使用编码器—解码器将上述英语句子翻译成法语句子的一种方法。在训练数据集中,我们可以在每个句子后附上特殊符号“<eos>”(end of sequence)表示序列的终止。编码器每个时间步的输入依次为英语句子中的单词、标点和特殊符号“<eos>”。图10.8使用了编码器在最终时间步的隐藏状态作为输入句子的编码信息。解码器在各个时间步中使用输入句子的编码信息和上个时间步的输出以及隐藏状态作为输入。 +图10.8描述了使用编码器—解码器将上述英语句子翻译成法语句子的一种方法。在训练数据集中,我们可以在每个句子后附上特殊符号“<eos>”(end of sequence)以表示序列的终止。编码器每个时间步的输入依次为英语句子中的单词、标点和特殊符号“<eos>”。图10.8中使用了编码器在最终时间步的隐藏状态作为输入句子的表征或编码信息。解码器在各个时间步中使用输入句子的编码信息和上个时间步的输出以及隐藏状态作为输入。 我们希望解码器在各个时间步能正确依次输出翻译后的法语单词、标点和特殊符号“<eos>”。 需要注意的是,解码器在最初时间步的输入用到了一个表示序列开始的特殊符号“<bos>”(beginning of sequence)。 ![使用编码器—解码器将句子由英语翻译成法语。编码器和解码器分别为循环神经网络](../img/seq2seq.svg) -接下来我们分别介绍编码器和解码器的定义。 +接下来,我们分别介绍编码器和解码器的定义。 ## 编码器 @@ -24,13 +24,13 @@ $$\boldsymbol{h}_t = f(\boldsymbol{x}_t, \boldsymbol{h}_{t-1}). $$ -接下来编码器通过自定义函数$q$将各个时间步的隐藏状态变换为背景变量 +接下来,编码器通过自定义函数$q$将各个时间步的隐藏状态变换为背景变量 $$\boldsymbol{c} = q(\boldsymbol{h}_1, \ldots, \boldsymbol{h}_T).$$ 例如,当选择$q(\boldsymbol{h}_1, \ldots, \boldsymbol{h}_T) = \boldsymbol{h}_T$时,背景变量是输入序列最终时间步的隐藏状态$\boldsymbol{h}_T$。 -以上描述的编码器是一个单向的循环神经网络,每个时间步的隐藏状态只取决于该时间步及之前的输入子序列。我们也可以使用双向循环神经网络构造编码器。这种情况下,编码器每个时间步的隐藏状态同时取决于该时间步之前和之后的子序列(包括当前时间步的输入),并编码了整个序列的信息。 +以上描述的编码器是一个单向的循环神经网络,每个时间步的隐藏状态只取决于该时间步及之前的输入子序列。我们也可以使用双向循环神经网络构造编码器。在这种情况下,编码器每个时间步的隐藏状态同时取决于该时间步之前和之后的子序列(包括当前时间步的输入),并编码了整个序列的信息。 ## 解码器 @@ -42,10 +42,10 @@ $$\boldsymbol{c} = q(\boldsymbol{h}_1, \ldots, \boldsymbol{h}_T).$$ $$\boldsymbol{s}_{t^\prime} = g(y_{t^\prime-1}, \boldsymbol{c}, \boldsymbol{s}_{t^\prime-1}).$$ -有了解码器的隐藏状态后,我们可以使用自定义的输出层和softmax运算来计算$\mathbb{P}(y_{t^\prime} \mid y_1, \ldots, y_{t^\prime-1}, \boldsymbol{c})$,例如基于当前时间步的解码器隐藏状态 $\boldsymbol{s}_{t^\prime}$、上一时间步的输出$y_{t^\prime-1}$以及背景变量$\boldsymbol{c}$来计算当前时间步输出$y_{t^\prime}$的概率分布。 +有了解码器的隐藏状态后,我们可以使用自定义的输出层和softmax运算来计算$\mathbb{P}(y_{t^\prime} \mid y_1, \ldots, y_{t^\prime-1}, \boldsymbol{c})$,例如,基于当前时间步的解码器隐藏状态 $\boldsymbol{s}_{t^\prime}$、上一时间步的输出$y_{t^\prime-1}$以及背景变量$\boldsymbol{c}$来计算当前时间步输出$y_{t^\prime}$的概率分布。 -## 模型训练 +## 训练模型 根据最大似然估计,我们可以最大化输出序列基于输入序列的条件概率 @@ -61,14 +61,14 @@ $$ $$- \log\mathbb{P}(y_1, \ldots, y_{T'} \mid x_1, \ldots, x_T) = -\sum_{t'=1}^{T'} \log \mathbb{P}(y_{t'} \mid y_1, \ldots, y_{t'-1}, \boldsymbol{c}),$$ -在模型训练中,所有输出序列损失的均值通常作为需要最小化的损失函数。在图10.8所描述的模型预测中,我们需要将解码器在上一个时间步的输出作为当前时间步的输入。与此不同,在训练中我们也可以将标签序列在上一个时间步的标签作为解码器在当前时间步的输入。这叫做强制教学(teacher forcing)。 +在模型训练中,所有输出序列损失的均值通常作为需要最小化的损失函数。在图10.8所描述的模型预测中,我们需要将解码器在上一个时间步的输出作为当前时间步的输入。与此不同,在训练中我们也可以将标签序列(训练集的真实输出序列)在上一个时间步的标签作为解码器在当前时间步的输入。这叫作强制教学(teacher forcing)。 ## 小结 * 编码器-解码器(seq2seq)可以输入并输出不定长的序列。 * 编码器—解码器使用了两个循环神经网络。 -* 在编码器—解码器的训练中,我们可以采用强制教学。 +* 在编码器—解码器的训练中,可以采用强制教学。 ## 练习 diff --git a/chapter_natural-language-processing/similarity-analogy.md b/chapter_natural-language-processing/similarity-analogy.md index fb7bb6511..1aca81289 100644 --- a/chapter_natural-language-processing/similarity-analogy.md +++ b/chapter_natural-language-processing/similarity-analogy.md @@ -1,10 +1,10 @@ # 求近义词和类比词 -在[“Word2vec的实现”](./word2vec-gluon.md)一节中,我们在小规模数据集上训练了一个word2vec词嵌入模型,并通过词向量的余弦相似度搜索近义词。实际中,在大规模语料上预训练的词向量常常可以应用于下游自然语言处理任务中。本节将演示如何用这些预训练的词向量来求近义词和类比词。我们还将在后面的章节中继续应用预训练的词向量。 +在[“word2vec的实现”](./word2vec-gluon.md)一节中,我们在小规模数据集上训练了一个word2vec词嵌入模型,并通过词向量的余弦相似度搜索近义词。实际中,在大规模语料上预训练的词向量常常可以应用到下游自然语言处理任务中。本节将演示如何用这些预训练的词向量来求近义词和类比词。我们还将在后面两节中继续应用预训练的词向量。 ## 使用预训练的词向量 -MXNet的`contrib.text`包提供了跟自然语言处理相关的函数和类(更多请参见GluonNLP工具包 [1])。下面查看它目前提供的预训练词嵌入的名称。 +MXNet的`contrib.text`包提供了跟自然语言处理相关的函数和类(更多参见GluonNLP工具包 [1])。下面查看它目前提供的预训练词嵌入的名称。 ```{.python .input} from mxnet import nd @@ -13,20 +13,20 @@ from mxnet.contrib import text text.embedding.get_pretrained_file_names().keys() ``` -给定词嵌入名称,我们可以查看该词嵌入提供了哪些预训练的模型。每个模型的词向量维度可能不同,或是在不同数据集上预训练得到的。 +给定词嵌入名称,可以查看该词嵌入提供了哪些预训练的模型。每个模型的词向量维度可能不同,或是在不同数据集上预训练得到的。 ```{.python .input n=35} print(text.embedding.get_pretrained_file_names('glove')) ``` -预训练的GloVe模型的命名规范大致是“模型.(数据集.)数据集词数.词向量维度.txt”。更多信息可以参考GloVe和fastText的项目网站 [2,3]。下面我们使用基于维基百科子集预训练的50维GloVe词向量。第一次创建预训练词向量实例时会自动下载相应的词向量。 +预训练的GloVe模型的命名规范大致是“模型.(数据集.)数据集词数.词向量维度.txt”。更多信息可以参考GloVe和fastText的项目网站 [2,3]。下面我们使用基于维基百科子集预训练的50维GloVe词向量。第一次创建预训练词向量实例时会自动下载相应的词向量,因此需要联网。 ```{.python .input n=11} glove_6b50d = text.embedding.create( 'glove', pretrained_file_name='glove.6B.50d.txt') ``` -打印词典大小。其中含有40万个词,和一个特殊的未知词符号。 +打印词典大小。其中含有40万个词和1个特殊的未知词符号。 ```{.python .input} len(glove_6b50d) @@ -40,11 +40,11 @@ glove_6b50d.token_to_idx['beautiful'], glove_6b50d.idx_to_token[3367] ## 应用预训练词向量 -下面我们以GloVe为例,展示预训练词向量的应用。 +下面我们以GloVe模型为例,展示预训练词向量的应用。 ### 求近义词 -这里重新实现[“Word2vec的实现”](./word2vec-gluon.md)一节中介绍过的使用余弦相似度来搜索近义词的算法。为了在求类比词时重用其中的求$k$近邻的逻辑,我们将这部分逻辑单独封装在`knn`($k$-nearest neighbors)函数中。 +这里重新实现[“word2vec的实现”](./word2vec-gluon.md)一节中介绍过的使用余弦相似度来搜索近义词的算法。为了在求类比词时重用其中的求$k$近邻($k$-nearest neighbors)的逻辑,我们将这部分逻辑单独封装在`knn`函数中。 ```{.python .input} def knn(W, x, k): @@ -65,7 +65,7 @@ def get_similar_tokens(query_token, k, embed): print('cosine sim=%.3f: %s' % (c, (embed.idx_to_token[i]))) ``` -已创建的预训练词向量实例`glove_6b50d`的词典中含40万个词和一个特殊的未知词。除去输入词和未知词,我们从中搜索与“chip”语义最相近的3个词。 +已创建的预训练词向量实例`glove_6b50d`的词典中含40万个词和1个特殊的未知词。除去输入词和未知词,我们从中搜索与“chip”语义最相近的3个词。 ```{.python .input} get_similar_tokens('chip', 3, glove_6b50d) @@ -83,7 +83,7 @@ get_similar_tokens('beautiful', 3, glove_6b50d) ### 求类比词 -除了求近义词以外,我们还可以使用预训练词向量求词与词之间的类比关系。例如,“man”(“男人”): “woman”(“女人”):: “son”(“儿子”) : “daughter”(“女儿”)是一个类比例子:“man”之于“woman”相当于“son”之于“daughter”。求类比词问题可以定义为:对于类比关系中的四个词 $a : b :: c : d$,给定前三个词$a$、$b$和$c$,求$d$。设词$w$的词向量为$\text{vec}(w)$。解类比词的思路是,搜索与$\text{vec}(c)+\text{vec}(b)-\text{vec}(a)$的结果向量最相似的词向量。 +除了求近义词以外,我们还可以使用预训练词向量求词与词之间的类比关系。例如,“man”(男人): “woman”(女人):: “son”(儿子) : “daughter”(女儿)是一个类比例子:“man”之于“woman”相当于“son”之于“daughter”。求类比词问题可以定义为:对于类比关系中的4个词 $a : b :: c : d$,给定前3个词$a$、$b$和$c$,求$d$。设词$w$的词向量为$\text{vec}(w)$。求类比词的思路是,搜索与$\text{vec}(c)+\text{vec}(b)-\text{vec}(a)$的结果向量最相似的词向量。 ```{.python .input} def get_analogy(token_a, token_b, token_c, embed): @@ -93,25 +93,25 @@ def get_analogy(token_a, token_b, token_c, embed): return embed.idx_to_token[topk[0]] ``` -验证下“男-女”类比。 +验证一下“男-女”类比。 ```{.python .input n=18} get_analogy('man', 'woman', 'son', glove_6b50d) ``` -“首都-国家”类比:“beijing”(“北京”)之于“china”(“中国”)相当于“tokyo”(“东京”)之于什么?答案应该是“japan”(“日本”)。 +“首都-国家”类比:“beijing”(北京)之于“china”(中国)相当于“tokyo”(东京)之于什么?答案应该是“japan”(日本)。 ```{.python .input n=19} get_analogy('beijing', 'china', 'tokyo', glove_6b50d) ``` -“形容词-形容词最高级”类比:“bad”(“坏的”)之于“worst”(“最坏的”)相当于“big”(“大的”)之于什么?答案应该是“biggest”(“最大的”)。 +“形容词-形容词最高级”类比:“bad”(坏的)之于“worst”(最坏的)相当于“big”(大的)之于什么?答案应该是“biggest”(最大的)。 ```{.python .input n=20} get_analogy('bad', 'worst', 'big', glove_6b50d) ``` -“动词一般时-动词过去时”类比:“do”(“做”)之于“did”(“做过”)相当于“go”(“去”)之于什么?答案应该是“went”(“去过”)。 +“动词一般时-动词过去时”类比:“do”(做)之于“did”(做过)相当于“go”(去)之于什么?答案应该是“went”(去过)。 ```{.python .input n=21} get_analogy('do', 'did', 'go', glove_6b50d) @@ -120,7 +120,7 @@ get_analogy('do', 'did', 'go', glove_6b50d) ## 小结 * 在大规模语料上预训练的词向量常常可以应用于下游自然语言处理任务中。 -* 我们可以应用预训练的词向量求近义词和类比词。 +* 可以应用预训练的词向量求近义词和类比词。 ## 练习 diff --git a/chapter_natural-language-processing/word2vec-gluon.md b/chapter_natural-language-processing/word2vec-gluon.md index d9f15564b..889241b99 100644 --- a/chapter_natural-language-processing/word2vec-gluon.md +++ b/chapter_natural-language-processing/word2vec-gluon.md @@ -1,8 +1,8 @@ -# Word2vec的实现 +# word2vec的实现 -本节是对前两节内容的实践。我们以[“词嵌入(word2vec)”](word2vec.md)一节中的跳字模型和[“近似训练”](approx-training.md)一节中的负采样为例,介绍在语料库上训练词嵌入模型的实现。我们还会介绍一些实现中的技巧,例如二次采样(subsampling)。 +本节是对前两节内容的实践。我们以[“词嵌入(word2vec)”](word2vec.md)一节中的跳字模型和[“近似训练”](approx-training.md)一节中的负采样为例,介绍在语料库上训练词嵌入模型的实现。我们还会介绍一些实现中的技巧,如二次采样(subsampling)。 -首先让我们导入实验所需的包或模块。 +首先导入实验所需的包或模块。 ```{.python .input n=1} import collections @@ -18,7 +18,7 @@ import zipfile ## 处理数据集 -Penn Tree Bank(PTB)是一个常用的小型语料库 [1]。它采样自华尔街日报的文章,包括训练集、验证集和测试集。我们将在PTB的训练集上训练词嵌入模型。该数据集的每一行作为一个句子。句子中的每个词由空格隔开。 +PTB(Penn Tree Bank)是一个常用的小型语料库 [1]。它采样自《华尔街日报》的文章,包括训练集、验证集和测试集。我们将在PTB训练集上训练词嵌入模型。该数据集的每一行作为一个句子。句子中的每个词由空格隔开。 ```{.python .input n=2} with zipfile.ZipFile('../data/ptb.zip', 'r') as zin: @@ -26,13 +26,13 @@ with zipfile.ZipFile('../data/ptb.zip', 'r') as zin: with open('../data/ptb/ptb.train.txt', 'r') as f: lines = f.readlines() - # st是sentence在循环中的缩写 + # st是sentence的缩写 raw_dataset = [st.split() for st in lines] '# sentences: %d' % len(raw_dataset) ``` -对于数据集的前三个句子,打印每个句子的词数和前五个词。这个数据集中句尾符为“<eos>”,生僻词全用“<unk>”表示,数字则被替换成了“N”。 +对于数据集的前3个句子,打印每个句子的词数和前5个词。这个数据集中句尾符为“<eos>”,生僻词全用“<unk>”表示,数字则被替换成了“N”。 ```{.python .input n=3} for st in raw_dataset[:3]: @@ -44,7 +44,7 @@ for st in raw_dataset[:3]: 为了计算简单,我们只保留在数据集中至少出现5次的词。 ```{.python .input n=4} -# tk是token的在循环中的缩写 +# tk是token的缩写 counter = collections.Counter([tk for st in raw_dataset for tk in st]) counter = dict(filter(lambda x: x[1] >= 5, counter.items())) ``` @@ -62,7 +62,7 @@ num_tokens = sum([len(st) for st in dataset]) ### 二次采样 -文本数据中一般会出现一些高频词,例如英文中的“the”、“a”和“in”。通常来说,在一个背景窗口中,一个词(如“chip”)和较低频词(如“microprocessor”)同时出现比和较高频词(如“the”)同时出现对训练词嵌入模型更有益。因此,训练词嵌入模型时可以对词进行二次采样 [2]。 +文本数据中一般会出现一些高频词,如英文中的“the”“a”和“in”。通常来说,在一个背景窗口中,一个词(如“chip”)和较低频词(如“microprocessor”)同时出现比和较高频词(如“the”)同时出现对训练词嵌入模型更有益。因此,训练词嵌入模型时可以对词进行二次采样 [2]。 具体来说,数据集中每个被索引词$w_i$将有一定概率被丢弃,该丢弃概率为 $$ \mathbb{P}(w_i) = \max\left(1 - \sqrt{\frac{t}{f(w_i)}}, 0\right),$$ @@ -97,7 +97,7 @@ compare_counts('join') ### 提取中心词和背景词 -我们将与中心词距离不超过背景窗口大小的词作为它的背景词。下面定义函数提取出所有中心词和它们的背景词。它每次在整数1和`max_window_size`(最大背景窗口)之间均匀随机采样一个整数作为背景窗口大小。 +我们将与中心词距离不超过背景窗口大小的词作为它的背景词。下面定义函数提取出所有中心词和它们的背景词。它每次在整数1和`max_window_size`(最大背景窗口)之间随机均匀采样一个整数作为背景窗口大小。 ```{.python .input n=9} def get_centers_and_contexts(dataset, max_window_size): @@ -132,7 +132,7 @@ all_centers, all_contexts = get_centers_and_contexts(subsampled_dataset, 5) ## 负采样 -我们使用负采样来进行近似训练。对于一对中心词和背景词,我们随机采样$K$个噪音词(实验中设$K=5$)。根据word2vec论文的建议,噪音词采样概率$\mathbb{P}(w)$设为$w$词频与总词频之比的0.75次方 [2]。 +我们使用负采样来进行近似训练。对于一对中心词和背景词,我们随机采样$K$个噪声词(实验中设$K=5$)。根据word2vec论文的建议,噪声词采样概率$\mathbb{P}(w)$设为$w$词频与总词频之比的0.75次方 [2]。 ```{.python .input n=12} def get_negatives(all_contexts, sampling_weights, K): @@ -159,11 +159,11 @@ all_negatives = get_negatives(all_contexts, sampling_weights, 5) ## 读取数据 -我们从数据集中提取所有中心词`all_centers`,以及每个中心词对应的背景词`all_contexts`和噪音词`all_negatives`。我们将通过随机小批量来读取它们。 +我们从数据集中提取所有中心词`all_centers`,以及每个中心词对应的背景词`all_contexts`和噪声词`all_negatives`。我们将通过随机小批量来读取它们。 -在一个小批量数据中,第$i$个样本包括一个中心词以及它所对应的$n_i$个背景词和$m_i$个噪音词。由于每个样本的背景窗口大小可能不一样,其中背景词与噪音词个数之和$n_i+m_i$也会不同。在构造小批量时,我们将每个样本的背景词和噪音词连结在一起,并添加填充项0直至连结后的长度相同,即长度均为$\max_i n_i+m_i$(`max_len`)。为了避免填充项对损失函数计算的影响,我们构造了掩码变量`masks`,其每一个元素分别与连结后的背景词和噪音词`contexts_negatives`中的元素一一对应。当变量`contexts_negatives`中的某个元素为填充项时,相同位置的掩码变量`masks`中的元素取0,否则取1。为了区分正类和负类,我们还需要将`contexts_negatives`变量中的背景词和噪音词区分开来。依据掩码变量的构造思路,我们只需创建与`contexts_negatives`变量形状相同的标签变量`labels`,并将与背景词(正类)对应的元素设1,其余清0。 +在一个小批量数据中,第$i$个样本包括一个中心词以及它所对应的$n_i$个背景词和$m_i$个噪声词。由于每个样本的背景窗口大小可能不一样,其中背景词与噪声词个数之和$n_i+m_i$也会不同。在构造小批量时,我们将每个样本的背景词和噪声词连结在一起,并添加填充项0直至连结后的长度相同,即长度均为$\max_i n_i+m_i$(`max_len`变量)。为了避免填充项对损失函数计算的影响,我们构造了掩码变量`masks`,其每一个元素分别与连结后的背景词和噪声词`contexts_negatives`中的元素一一对应。当`contexts_negatives`变量中的某个元素为填充项时,相同位置的掩码变量`masks`中的元素取0,否则取1。为了区分正类和负类,我们还需要将`contexts_negatives`变量中的背景词和噪声词区分开来。依据掩码变量的构造思路,我们只需创建与`contexts_negatives`变量形状相同的标签变量`labels`,并将与背景词(正类)对应的元素设1,其余清0。 -下面我们将实现这个小批量读取函数`batchify`。它的小批量输入`data`是一个长度为批量大小的列表,其中每个元素分别包含中心词`center`、背景词`context`和噪音词`negative`。该函数返回的小批量数据符合我们所需要的格式,例如包含了掩码变量。 +下面我们实现这个小批量读取函数`batchify`。它的小批量输入`data`是一个长度为批量大小的列表,其中每个元素分别包含中心词`center`、背景词`context`和噪声词`negative`。该函数返回的小批量数据符合我们需要的格式,例如,包含了掩码变量。 ```{.python .input n=13} def batchify(data): @@ -179,7 +179,7 @@ def batchify(data): nd.array(masks), nd.array(labels)) ``` -我们用刚刚定义的`batchify`函数指定`DataLoader`实例中小批量的读取方式。然后打印读取的第一个批量中各个变量的形状。 +我们用刚刚定义的`batchify`函数指定`DataLoader`实例中小批量的读取方式,然后打印读取的第一个批量中各个变量的形状。 ```{.python .input n=14} batch_size = 512 @@ -208,7 +208,7 @@ embed.initialize() embed.weight ``` -嵌入层的输入为词的索引。输入一个词的索引$i$,嵌入层返回权重矩阵的第$i$行作为它的词向量。下面我们将形状为(2,3)的索引输入进嵌入层,由于词向量的维度为4,我们得到形状为(2,3,4)的词向量。 +嵌入层的输入为词的索引。输入一个词的索引$i$,嵌入层返回权重矩阵的第$i$行作为它的词向量。下面我们将形状为(2, 3)的索引输入进嵌入层,由于词向量的维度为4,我们得到形状为(2, 3, 4)的词向量。 ```{.python .input n=16} x = nd.array([[1, 2, 3], [4, 5, 6]]) @@ -217,7 +217,7 @@ embed(x) ### 小批量乘法 -我们可以使用小批量乘法运算`batch_dot`对两个小批量中的矩阵一一做乘法。假设第一个批量中包含$n$个形状为$a\times b$的矩阵$\boldsymbol{X}_1, \ldots, \boldsymbol{X}_n$,第二个批量中包含$n$个形状为$b\times c$的矩阵$\boldsymbol{Y}_1, \ldots, \boldsymbol{Y}_n$。这两个批量的矩阵乘法输出为$n$个形状为$a\times c$的矩阵$\boldsymbol{X}_1\boldsymbol{Y}_1, \ldots, \boldsymbol{X}_n\boldsymbol{Y}_n$。因此,给定两个形状分别为($n$,$a$,$b$)和($n$,$b$,$c$)的`NDArray`,小批量乘法输出的形状为($n$,$a$,$c$)。 +我们可以使用小批量乘法运算`batch_dot`对两个小批量中的矩阵一一做乘法。假设第一个小批量中包含$n$个形状为$a\times b$的矩阵$\boldsymbol{X}_1, \ldots, \boldsymbol{X}_n$,第二个小批量中包含$n$个形状为$b\times c$的矩阵$\boldsymbol{Y}_1, \ldots, \boldsymbol{Y}_n$。这两个小批量的矩阵乘法输出为$n$个形状为$a\times c$的矩阵$\boldsymbol{X}_1\boldsymbol{Y}_1, \ldots, \boldsymbol{X}_n\boldsymbol{Y}_n$。因此,给定两个形状分别为($n$, $a$, $b$)和($n$, $b$, $c$)的`NDArray`,小批量乘法输出的形状为($n$, $a$, $c$)。 ```{.python .input n=17} X = nd.ones((2, 1, 4)) @@ -227,7 +227,7 @@ nd.batch_dot(X, Y).shape ### 跳字模型前向计算 -在前向计算中,跳字模型的输入包含中心词索引`center`以及连结的背景词与噪音词索引`contexts_and_negatives`。其中`center`变量的形状为(批量大小,1),而`contexts_and_negatives`变量的形状为(批量大小,`max_len`)。这两个变量先通过词嵌入层分别由词索引变换为词向量,再通过小批量乘法得到形状为(批量大小,1,`max_len`)的输出。输出中的每个元素是中心词向量与背景词向量或噪音词向量的内积。 +在前向计算中,跳字模型的输入包含中心词索引`center`以及连结的背景词与噪声词索引`contexts_and_negatives`。其中`center`变量的形状为(批量大小, 1),而`contexts_and_negatives`变量的形状为(批量大小, `max_len`)。这两个变量先通过词嵌入层分别由词索引变换为词向量,再通过小批量乘法得到形状为(批量大小, 1, `max_len`)的输出。输出中的每个元素是中心词向量与背景词向量或噪声词向量的内积。 ```{.python .input n=18} def skip_gram(center, contexts_and_negatives, embed_v, embed_u): @@ -281,9 +281,9 @@ net.add(nn.Embedding(input_dim=len(idx_to_token), output_dim=embed_size), nn.Embedding(input_dim=len(idx_to_token), output_dim=embed_size)) ``` -### 训练 +### 定义训练函数 -下面定义训练函数。由于填充项的存在,跟之前的训练函数相比,损失函数的计算稍有不同。 +下面定义训练函数。由于填充项的存在,与之前的训练函数相比,损失函数的计算稍有不同。 ```{.python .input n=23} def train(net, lr, num_epochs): @@ -309,7 +309,7 @@ def train(net, lr, num_epochs): % (epoch + 1, l_sum / n, time.time() - start)) ``` -现在我们可以训练使用负采样的跳字模型了。 +现在我们就可以使用负采样训练跳字模型了。 ```{.python .input n=24} train(net, 0.005, 5) @@ -317,7 +317,7 @@ train(net, 0.005, 5) ## 应用词嵌入模型 -当训练好词嵌入模型后,我们可以根据两个词向量的余弦相似度表示词与词之间在语义上的相似度。可以看到,使用训练得到的词嵌入模型时,与词“chip”语义最接近的词大多与芯片有关。 +训练好词嵌入模型之后,我们可以根据两个词向量的余弦相似度表示词与词之间在语义上的相似度。可以看到,使用训练得到的词嵌入模型时,与词“chip”语义最接近的词大多与芯片有关。 ```{.python .input n=25} def get_similar_tokens(query_token, k, embed): @@ -334,9 +334,9 @@ get_similar_tokens('chip', 3, net[0]) ## 小结 -* 我们可以使用Gluon通过负采样训练跳字模型。 +* 可以使用Gluon通过负采样训练跳字模型。 * 二次采样试图尽可能减轻高频词对训练词嵌入模型的影响。 -* 我们可以将长度不同的样本填充至长度相同的小批量,并通过掩码变量区分非填充和填充,然后只令非填充参与损失函数的计算。 +* 可以将长度不同的样本填充至长度相同的小批量,并通过掩码变量区分非填充和填充,然后只令非填充参与损失函数的计算。 ## 练习 @@ -345,7 +345,7 @@ get_similar_tokens('chip', 3, net[0]) * 我们用`batchify`函数指定`DataLoader`实例中小批量的读取方式,并打印了读取的第一个批量中各个变量的形状。这些形状该如何计算得到? * 试着找出其他词的近义词。 * 调一调超参数,观察并分析实验结果。 -* 当数据集较大时,我们通常在迭代模型参数时才对当前小批量里的中心词采样背景词和噪音词。也就是说,同一个中心词在不同的迭代周期可能会有不同的背景词或噪音词。这样训练有哪些好处?尝试实现该训练方法。 +* 当数据集较大时,我们通常在迭代模型参数时才对当前小批量里的中心词采样背景词和噪声词。也就是说,同一个中心词在不同的迭代周期可能会有不同的背景词或噪声词。这样训练有哪些好处?尝试实现该训练方法。 ## 扫码直达[讨论区](https://discuss.gluon.ai/t/topic/7761) diff --git a/chapter_natural-language-processing/word2vec.md b/chapter_natural-language-processing/word2vec.md index cf109ef31..be180823b 100644 --- a/chapter_natural-language-processing/word2vec.md +++ b/chapter_natural-language-processing/word2vec.md @@ -1,26 +1,26 @@ # 词嵌入(word2vec) -自然语言是一套用来表达含义的复杂系统。在这套系统中,词是表义的基本单元。顾名思义,词向量是用来表示词的向量,也可被认为是词的特征向量。把词映射为实数域向量的技术也叫词嵌入(word embedding)。近年来,词嵌入已逐渐成为自然语言处理的基础知识。 +自然语言是一套用来表达含义的复杂系统。在这套系统中,词是表义的基本单元。顾名思义,词向量是用来表示词的向量,也可被认为是词的特征向量或表征。把词映射为实数域向量的技术也叫词嵌入(word embedding)。近年来,词嵌入已逐渐成为自然语言处理的基础知识。 ## 为何不采用one-hot向量 -我们在[“循环神经网络的从零开始实现”](../chapter_recurrent-neural-networks/rnn-scratch.md)一节中使用one-hot向量表示词(字符为词)。回忆一下,假设词典中不同词的数量(词典大小)为$N$,每个词可以和从0到$N-1$的连续整数一一对应。这些与词对应的整数叫做词的索引。 +我们在[“循环神经网络的从零开始实现”](../chapter_recurrent-neural-networks/rnn-scratch.md)一节中使用one-hot向量表示词(字符为词)。回忆一下,假设词典中不同词的数量(词典大小)为$N$,每个词可以和从0到$N-1$的连续整数一一对应。这些与词对应的整数叫作词的索引。 假设一个词的索引为$i$,为了得到该词的one-hot向量表示,我们创建一个全0的长为$N$的向量,并将其第$i$位设成1。这样一来,每个词就表示成了一个长度为$N$的向量,可以直接被神经网络使用。 -虽然one-hot词向量构造起来很容易,但通常并不是一个好选择。一个主要的原因是,one-hot词向量无法准确表达不同词之间的相似度,例如我们常常使用的余弦相似度。对于向量$\boldsymbol{x}, \boldsymbol{y} \in \mathbb{R}^d$,它们的余弦相似度是它们之间夹角的余弦值 +虽然one-hot词向量构造起来很容易,但通常并不是一个好选择。一个主要的原因是,one-hot词向量无法准确表达不同词之间的相似度,如我们常常使用的余弦相似度。对于向量$\boldsymbol{x}, \boldsymbol{y} \in \mathbb{R}^d$,它们的余弦相似度是它们之间夹角的余弦值 $$\frac{\boldsymbol{x}^\top \boldsymbol{y}}{\|\boldsymbol{x}\| \|\boldsymbol{y}\|} \in [-1, 1].$$ 由于任何两个不同词的one-hot向量的余弦相似度都为0,多个不同词之间的相似度难以通过one-hot向量准确地体现出来。 -Word2vec工具的提出正是为了解决上面这个问题 [1]。它将每个词表示成一个定长的向量,并使得这些向量能较好地表达不同词之间的相似和类比关系。Word2vec工具包含了两个模型:跳字模型(skip-gram)[2] 和连续词袋模型(continuous bag of words,简称CBOW)[3]。接下来让我们分别介绍这两个模型以及它们的训练方法。 +word2vec工具的提出正是为了解决上面这个问题 [1]。它将每个词表示成一个定长的向量,并使得这些向量能较好地表达不同词之间的相似和类比关系。word2vec工具包含了两个模型,即跳字模型(skip-gram)[2] 和连续词袋模型(continuous bag of words,CBOW)[3]。接下来让我们分别介绍这两个模型以及它们的训练方法。 ## 跳字模型 -跳字模型假设基于某个词来生成它在文本序列周围的词。举个例子,假设文本序列是“the”、“man”、“loves”、“his”和“son”。以“loves”作为中心词,设背景窗口大小为2。如图10.1所示,跳字模型所关心的是,给定中心词“loves”,生成与它距离不超过2个词的背景词“the”、“man”、“his”和“son”的条件概率,即 +跳字模型假设基于某个词来生成它在文本序列周围的词。举个例子,假设文本序列是“the”“man”“loves”“his”“son”。以“loves”作为中心词,设背景窗口大小为2。如图10.1所示,跳字模型所关心的是,给定中心词“loves”,生成与它距离不超过2个词的背景词“the”“man”“his”“son”的条件概率,即 $$\mathbb{P}(\textrm{``the"},\textrm{``man"},\textrm{``his"},\textrm{``son"}\mid\textrm{``loves"}).$$ @@ -31,7 +31,7 @@ $$\mathbb{P}(\textrm{``the"}\mid\textrm{``loves"})\cdot\mathbb{P}(\textrm{``man" ![跳字模型关心给定中心词生成背景词的条件概率](../img/skip-gram.svg) -在跳字模型中,每个词被表示成两个$d$维向量用来计算条件概率。假设这个词在词典中索引为$i$,当它为中心词时向量表示为$\boldsymbol{v}_i\in\mathbb{R}^d$,而为背景词时向量表示为$\boldsymbol{u}_i\in\mathbb{R}^d$。设中心词$w_c$在词典中索引为$c$,背景词$w_o$在词典中索引为$o$,给定中心词生成背景词的条件概率可以通过对向量内积做softmax运算而得到: +在跳字模型中,每个词被表示成两个$d$维向量,用来计算条件概率。假设这个词在词典中索引为$i$,当它为中心词时向量表示为$\boldsymbol{v}_i\in\mathbb{R}^d$,而为背景词时向量表示为$\boldsymbol{u}_i\in\mathbb{R}^d$。设中心词$w_c$在词典中索引为$c$,背景词$w_o$在词典中索引为$o$,给定中心词生成背景词的条件概率可以通过对向量内积做softmax运算而得到: $$\mathbb{P}(w_o \mid w_c) = \frac{\text{exp}(\boldsymbol{u}_o^\top \boldsymbol{v}_c)}{ \sum_{i \in \mathcal{V}} \text{exp}(\boldsymbol{u}_i^\top \boldsymbol{v}_c)},$$ @@ -41,14 +41,14 @@ $$ \prod_{t=1}^{T} \prod_{-m \leq j \leq m,\ j \neq 0} \mathbb{P}(w^{(t+j)} \mid 这里小于1和大于$T$的时间步可以忽略。 -### 跳字模型训练 +### 训练跳字模型 跳字模型的参数是每个词所对应的中心词向量和背景词向量。训练中我们通过最大化似然函数来学习模型参数,即最大似然估计。这等价于最小化以下损失函数: $$ - \sum_{t=1}^{T} \sum_{-m \leq j \leq m,\ j \neq 0} \text{log}\, \mathbb{P}(w^{(t+j)} \mid w^{(t)}).$$ -如果使用随机梯度下降,那么在每一次迭代里我们随机采样一个较短的子序列来计算有关该子序列的损失,然后计算梯度来更新模型参数。梯度计算的关键是对数条件概率有关中心词向量和背景词向量的梯度。根据定义,首先看到 +如果使用随机梯度下降,那么在每一次迭代里我们随机采样一个较短的子序列来计算有关该子序列的损失,然后计算梯度来更新模型参数。梯度计算的关键是条件概率的对数有关中心词向量和背景词向量的梯度。根据定义,首先看到 $$\log \mathbb{P}(w_o \mid w_c) = @@ -72,13 +72,13 @@ $$ ## 连续词袋模型 -连续词袋模型与跳字模型类似。与跳字模型最大的不同在于,连续词袋模型假设基于某中心词在文本序列前后的背景词来生成该中心词。在同样的文本序列“the”、 “man”、“loves”、“his”和“son”里,以“loves”作为中心词,且背景窗口大小为2时,连续词袋模型关心的是,给定背景词“the”、“man”、“his”和“son”生成中心词“loves”的条件概率(如图10.2所示),也就是 +连续词袋模型与跳字模型类似。与跳字模型最大的不同在于,连续词袋模型假设基于某中心词在文本序列前后的背景词来生成该中心词。在同样的文本序列“the”“man”“loves”“his”“son”里,以“loves”作为中心词,且背景窗口大小为2时,连续词袋模型关心的是,给定背景词“the”“man”“his”“son”生成中心词“loves”的条件概率(如图10.2所示),也就是 $$\mathbb{P}(\textrm{``loves"}\mid\textrm{``the"},\textrm{``man"},\textrm{``his"},\textrm{``son"}).$$ ![连续词袋模型关心给定背景词生成中心词的条件概率](../img/cbow.svg) -因为连续词袋模型的背景词有多个,我们将这些背景词向量取平均,然后使用和跳字模型一样的方法来计算条件概率。设$\boldsymbol{v_i}\in\mathbb{R}^d$和$\boldsymbol{u_i}\in\mathbb{R}^d$分别表示词典中索引为$i$的词作为背景词和中心词的向量(注意符号和跳字模型中是相反的)。设中心词$w_c$在词典中索引为$c$,背景词$w_{o_1}, \ldots, w_{o_{2m}}$在词典中索引为$o_1, \ldots, o_{2m}$,那么给定背景词生成中心词的条件概率 +因为连续词袋模型的背景词有多个,我们将这些背景词向量取平均,然后使用和跳字模型一样的方法来计算条件概率。设$\boldsymbol{v_i}\in\mathbb{R}^d$和$\boldsymbol{u_i}\in\mathbb{R}^d$分别表示词典中索引为$i$的词作为背景词和中心词的向量(注意符号的含义与跳字模型中的相反)。设中心词$w_c$在词典中索引为$c$,背景词$w_{o_1}, \ldots, w_{o_{2m}}$在词典中索引为$o_1, \ldots, o_{2m}$,那么给定背景词生成中心词的条件概率 $$\mathbb{P}(w_c \mid w_{o_1}, \ldots, w_{o_{2m}}) = \frac{\text{exp}\left(\frac{1}{2m}\boldsymbol{u}_c^\top (\boldsymbol{v}_{o_1} + \ldots + \boldsymbol{v}_{o_{2m}}) \right)}{ \sum_{i \in \mathcal{V}} \text{exp}\left(\frac{1}{2m}\boldsymbol{u}_i^\top (\boldsymbol{v}_{o_1} + \ldots + \boldsymbol{v}_{o_{2m}}) \right)}.$$ @@ -86,13 +86,13 @@ $$\mathbb{P}(w_c \mid w_{o_1}, \ldots, w_{o_{2m}}) = \frac{\text{exp}\left(\frac $$\mathbb{P}(w_c \mid \mathcal{W}_o) = \frac{\exp\left(\boldsymbol{u}_c^\top \bar{\boldsymbol{v}}_o\right)}{\sum_{i \in \mathcal{V}} \exp\left(\boldsymbol{u}_i^\top \bar{\boldsymbol{v}}_o\right)}.$$ -给定一个长度为$T$的文本序列,设时间步$t$的词为$w^{(t)}$,背景窗口大小为$m$。连续词袋模型的似然函数为由背景词生成任一中心词的概率 +给定一个长度为$T$的文本序列,设时间步$t$的词为$w^{(t)}$,背景窗口大小为$m$。连续词袋模型的似然函数是由背景词生成任一中心词的概率 $$ \prod_{t=1}^{T} \mathbb{P}(w^{(t)} \mid w^{(t-m)}, \ldots, w^{(t-1)}, w^{(t+1)}, \ldots, w^{(t+m)}).$$ -### 连续词袋模型训练 +### 训练连续词袋模型 -连续词袋模型训练同跳字模型训练基本一致。连续词袋模型的最大似然估计等价于最小化损失函数 +训练连续词袋模型同训练跳字模型基本一致。连续词袋模型的最大似然估计等价于最小化损失函数 $$ -\sum_{t=1}^T \text{log}\, \mathbb{P}(w^{(t)} \mid w^{(t-m)}, \ldots, w^{(t-1)}, w^{(t+1)}, \ldots, w^{(t+m)}).$$ @@ -115,8 +115,8 @@ $$\frac{\partial \log\, \mathbb{P}(w_c \mid \mathcal{W}_o)}{\partial \boldsymbol ## 练习 * 每次梯度的计算复杂度是多少?当词典很大时,会有什么问题? -* 英语中有些固定短语由多个词组成,例如“new york”。如何训练它们的词向量?提示:可参考word2vec论文第4节 [2]。 -* 让我们以跳字模型为例思考word2vec模型的设计。跳字模型中两个词向量的内积与余弦相似度有什么关系?对于语义相近的一对词来说,为什么它们的词向量的余弦相似度可能会高? +* 英语中有些固定短语由多个词组成,如“new york”。如何训练它们的词向量?提示:可参考word2vec论文第4节 [2]。 +* 让我们以跳字模型为例思考word2vec模型的设计。跳字模型中两个词向量的内积与余弦相似度有什么关系?对语义相近的一对词来说,为什么它们的词向量的余弦相似度可能会高? ## 扫码直达[讨论区](https://discuss.gluon.ai/t/topic/4203) @@ -126,7 +126,7 @@ $$\frac{\partial \log\, \mathbb{P}(w_c \mid \mathcal{W}_o)}{\partial \boldsymbol ## 参考文献 -[1] Word2vec工具。https://code.google.com/archive/p/word2vec/ +[1] word2vec工具。https://code.google.com/archive/p/word2vec/ [2] Mikolov, T., Sutskever, I., Chen, K., Corrado, G. S., & Dean, J. (2013). Distributed representations of words and phrases and their compositionality. In Advances in neural information processing systems (pp. 3111-3119).