Commit 0bf05532 by liyinqiao

Merge with the branch of linye to update the manual.

parent 3c15686c
......@@ -2307,9 +2307,7 @@ Parameters:
NiuTensor/tensor/Sample中的具体示例
## 高级技巧
### 内存池
## 内存池
内存作为计算机软件运行过程中不可或缺的一项重要资源,在软件开发过程中具有十分重要的地位。对于一个软件系统而言,如何更高效地进行内存管理将对系统整体性能,尤其是运行速度方面产生很大程度的影响。虽然目前而言,主流编程语言均会为开发人员提供相应的系统级接口(如C语言中的malloc和free,C++中的new和delete等),但这类接口在设计的时候由于需要考虑各种使用情况,因此并不一定能够最适用于目前的使用需求(如对速度具有较高要求等),因此直接使用系统级的内存管理接口存在以下弊端:
......@@ -2764,13 +2762,334 @@ autoDiffer.Backward(lossTensor);
## 实例3:Transformer
Transformer模型出自论文“Attention is All You Need”,自其问世以来就迅速席卷了自然语言处理领域,并在各类主流任务上取得了新的突破,包括机器翻译、语言建模、序列标注和文本分类等。
#### Transformer简要介绍
Transformer模型出自论文[“Attention is All You Need”](https://arxiv.org/pdf/1706.03762.pdf),自其问世以来就迅速席卷了自然语言处理领域,并在各类主流任务上取得了新的突破,包括机器翻译、语言建模、序列标注和文本分类等。
Transformer模型自其问世以来就迅速席卷了自然语言处理领域,并在各类主流任务上取得了新的突破,包括机器翻译、语言建模、序列标注和文本分类等。
NiuTensor框架包含了Transformer的高效实现,本文以机器翻译任务为例,自顶向下对该结构进行分解,结合代码一步一步搭建完整的模型。
Transfomer是一个基于编码器解码器框架的端到端模型,它使用attention机制捕捉词汇间的依赖。标准的Transformer框架由6层encoder和6层decoder堆叠而成,每个encoder层都包含4个模块,分别为: 自注意力子层(self-attention sub-layer)、前馈神经网络子层(feed forward sub-layer)、残差连接(residual connection)以及层正则化(layer normalization)。decoder层与encoder的结构类似,decoder层除了包含encoder层中有的4个模块以外,还包含了一个额外的模块编码-解码注意力子层(encoder-decoder attention sub-layer)。Transformer中也使用了很多其他的技术共同保证其效果,如位置编码、多头注意力、训练学习率调整策略等等。
![Transformer结构](./pic/transformer-architecture.jpg)
#### 词向量和位置信息编码
Transformer的输入主要由两部分组成,分别是词汇embedding和位置编码,词汇embedding和位置编码维度相同,通过位置编码公式
$\textrm{PE}(pos,2i) = \textrm{sin} (\frac{pos}{10000^{2i/d_{model}}})$
$\textrm{PE}(pos,2i+1) = \textrm{cos} (\frac{pos}{10000^{2i/d_{model}}})$
式中PE($\cdot$)表示位置编码的函数,$pos$表示单词的位置,$i$代表位置编码向量中的第几维。计算得到的词汇位置编码PE($\cdot$)与embedding即e($\cdot$)加和,得到模型的真实输入h($\cdot$)
![Transformer结构](./pic/embeding-pos.jpg)
```C++
/*
make the network
*/
XTensor T2TEmbedder::Make(XTensor &input)
{
...
...
/* make positional embeddings */
XTensor position;
XTensor embTMP;
InitTensor1D(&position, input.GetDim(-1), X_INT, devID);
position.Range(0, position.unitNum, 1);
embTMP = Gather(posEmbeddingBase, position);
posEmbedding = Unsqueeze(embTMP, 0, dims[0]);
/* make word embeddings */
wordEmbedding = Gather(w, input);
wordEmbedding = Linear(wordEmbedding, (float)sqrt((float)eSize));
/* sum over the two embeddings */
return wordEmbedding + posEmbedding;
}
```
上述代码位于/source/sample/transformer/T2TEmbedding.cpp
#### encoder层
**Attention**
当数据输入到模型后首先要计算的是encoder第一层的self-attention。不同于传统的RNN、LSTM等结构在对序列中远距离关系建模时,需要将目标关系之间的词全部按序输入到模型中,注意力机制使得Transformer框架可以快速的获得句子中的词汇相关性。注意力机制的运算可以被形式化为:
$\textrm{Attention}(\small\textnormal{Q},\small\textnormal{K},\small\textnormal{V}) = \textrm{Softmax}( \frac{\small\textnormal{Q}\small\textnormal{K}^{T}} {\sqrt{d_k}} + \small\textnormal{Mask} ) \small\textnormal{V}$
其中$\small\textnormal{Q}$和$\small\textnormal{K}$的维度为$L\times d_k$,$\small\textnormal{V}$的维度为$L\times d_v$,$L$为序列的长度。$\small\textnormal{Q}$是输入数据乘以参数矩阵$\textrm{W}_Q$得到的,$\small\textnormal{K}$和$\small\textnormal{V}$同样是乘以各自的参数矩阵得到。自注意力机制首先通过对输入进行变换得到$\mathrm{query}$(查询)、$\mathrm{key}$(键)和$\mathrm{value}$(值),通过$\mathrm{query}$(查询)与$\mathrm{key}$(键)得到一个维度为$L\times L$的矩阵,该矩阵表示一个序列上任意两个位置($i, i’$)的相关性。再通过系数1/$\sqrt{d_k}$进行放缩操作,放缩可以尽量减少相关性矩阵的方差。在此基础上,通过对相关性矩阵累加一个掩码矩阵$\small\textnormal{Mask}$,来屏蔽掉矩阵中的无用信息,该操作在训练过程中体现为,在编码端对句子的补齐或屏蔽掉解码端的未来信息。随后使用Softmax函数对相关性矩阵在行的维度上进行归一化操作,这可以理解为对第$i$行进行归一化,结果对应了$\small\textnormal{V}$中的不同位置上向量的注意力权重。对于$\mathrm{value}$的加权求和,可以直接用相关性性系数和$\small\textnormal{V}$进行矩阵乘法得到,即$\textrm{Softmax}( \frac{\small\textnormal{Q}\small\textnormal{K}^{T}} {\sqrt{d_k}} + \small\textnormal{Mask} )$和$\small\textnormal{V}$进行矩阵乘。最终我们就到了自注意力的输出,它和输入的$\small\textnormal{V}$的大小是一模一样的。多头注意力的运算与上述并无区别,只是在多头注意力中的每个头将会通过各自的$\small\textnormal{Q}$、$\small\textnormal{K}$和$\small\textnormal{V}$矩阵计算出attention,并将attention拼接后传递至下一个模块
![Transformer结构](./pic/self-attention.jpg)
NiuTensor框架包含了Transformer的高效实现(详见:NiuTensor/source/sample/transformer),本文以机器翻译任务为例,自顶向下对该结构进行分解,结合代码一步一步搭建完整的模型。
可视化attention矩阵操作可以参考下图
![Transformer结构](./pic/self-attention-tensor-figure.jpg)
代码如下
```C++
/*
make the network
>> k - keys. It might be of size B * L * H
where B = batch size, L = sequence length,
and H = vector size of each position
>> q - queries
>> v - values
*/
XTensor T2TAttention::Make(XTensor &k, XTensor &q, XTensor &v, XTensor &mask, bool isTraining)
{
XTensor k2;
XTensor q2;
XTensor v2;
/* linear transformation before self-attention */
k2 = MMul(k, wk);
q2 = MMul(q, wq);
v2 = MMul(v, wv);
return MakeAttention(k2, q2, v2, mask, isTraining);
}
XTensor T2TAttention::MakeAttention(XTensor &k, XTensor &q, XTensor &v, XTensor &mask, bool isTraining)
{
XTensor kheads;
XTensor qheads;
XTensor vheads;
/* multi head */
kheads = Split(k, k.order - 1, nhead);
qheads = Split(q, q.order - 1, nhead);
vheads = Split(v, v.order - 1, nhead);
XTensor att;
XTensor dot;
XTensor scalar;
/* scalar = softmax(Q * K^T / sqrt(dk)) * V */
dot = BMMul(qheads, X_NOTRANS, kheads, X_TRANS);
if(isMasked)
dot = dot + mask;
dot = Linear(dot, 1.0F/(float)sqrt((float)dk/nhead));
scalar = Softmax(dot, -1);
if(isTraining && dropoutP > 0)
scalar = Dropout(scalar, dropoutP);
att = BMMul(scalar, vheads);
/* concatenate the heads */
return MMul(Merge(att, att.order - 1), wa);
}
```
上述代码位于/source/sample/transformer/T2TAttention.cpp
**全连接**
在得到attention的输出结果之后,数据将流入第一层encoder的下一个模块,即全连接层。在此处的全连接层为一个双层的全连接网络,标准的输入维度与输出维度为512,隐层维度为2048,在隐层的输出位置使用Relu作为激活函数。全连接网络的作用主要体现在将经过注意力操作之后的表示映射到新的空间中,新的空间会有利于接下来的非线性变换等操作。当数据经过全连接网络后,将作为第二层encoder的输入经第二层计算后继续向第三层传递。
```C++
/*
make the network
y = max(0, x * w1 + b1) * w2 + b2
>> input - the input tensor
>> return - the output tensor
*/
XTensor T2TFNN::Make(XTensor &input, bool isTraining)
{
XTensor t1;
/* t1 = max(0, x * w1 + b1) */
//t1 = Rectify(MMul(input, w1) + b1);
t1 = Rectify(MulAndShift(input, w1, b1));
if(isTraining && dropoutP > 0)
t1 = Dropout(t1, dropoutP);
请点击[实例3:Transformer](http://opensource.niutrans.com/openSource/niutensor/Transformer/Transformer.html)进入教程
/* result = t1 * w2 + b2 */
//return MMul(t1, w2) + b2;
return MulAndShift(t1, w2, b2);
}
```
上述代码位于/source/sample/transformer/T2TFNN.cpp
**残差与层正则化**
在之前的Transformer结构图中可以发现encoder中还有两个操作,分别是残差链接和层正则化。残差连接从广义上讲也叫短连接(short-cut connection),指的是这种短距离的连接。它的思想很简单,就是把层和层之间的距离拉近。其计算公式为:
$x_{l+1} = x_l + \digamma (x_l)$
从上式中可以看出,当$x_{l+1}$对$x_{l}$求导时,无论$\digamma (x_l)$对$x_{l}$有多小,都会有$x_{l}$对$x_{l}$的导数为1。这就极大的缓解了梯度消失的问题。同时,由于引入了残差操作,将前面所有层的输出加到一起。这样会导致不同层(或子层)的结果之间的差异性很大,造成训练过程不稳定、训练时间较长。为了避免这种情况,在每层中加入了层正则化操作。
```C++
/*
make the encoding network
>> input - the input tensor of the encoder
>> mask - the mask that indicate each position is valid
>> maskEncDec - no use
>> isTraining - indicates whether the model is used for training
<< return - the output tensor of the encoder
*/
XTensor AttEncoder::Make(XTensor &input, XTensor &mask, XTensor &maskEncDec, bool isTraining)
{
...
...
/* self attention */
att = attentions[i].MakeBig(x, mask, isTraining);
/* dropout */
if(isTraining && dropoutP > 0)
att = Dropout(att, dropoutP);
/* residual connection */
res = Sum(att, x);
/* layer normalization */
x = attLayerNorms[i].Make(res);
/* fnn */
fnn = fnns[i].Make(x, isTraining);
/* dropout */
if(isTraining && dropoutP > 0)
fnn = Dropout(fnn, dropoutP);
/* residual connection */
res = Sum(fnn, x);
/* layer normalization */
x = fnnLayerNorms[i].Make(res);
}
```
上述代码位于/source/sample/transformer/T2TEncoder.cpp
#### decoder层
可以对应前面Transformer结构图,在数据输入decoder后,首先计算self-attention,其后计算encoder-decoder attention(这是encoder端与decoder端计算的唯一区别),encoder-decoder attention与self-attention计算十分相似,同样通过$\small\textnormal{Q}$、$\small\textnormal{K}$和$\small\textnormal{V}$计算attention,但是在encoder-decoder attention中只有$\small\textnormal{Q}$来自decoder,而$\small\textnormal{K}$和$\small\textnormal{V}$均来自于encoder最顶层的输出。这部分计算完成后,接下来再经过一个全连接层得到该层decoder的输出,与encoder端一样,该层decoder的输出将作为下一层decoder的输入继续传递。
```C++
/*
make the decoding network
>> inputDec - the input tensor of the decoder
>> outputEnc - the output tensor of the encoder
>> mask - mask that indicates which position is valid
>> maskEncDec - mask for the encoder-decoder attention
>> isTraining - indicates whether the model is used for training
<< return - the output tensor of the encoder
*/
XTensor AttDecoder::Make(XTensor &inputDec, XTensor &outputEnc, XTensor &mask, XTensor &maskEncDec, bool isTraining)
{
...
...
/******************/
/* self attention */
att = attentions[i].MakeBig(x, mask, isTraining);
/* dropout */
if(isTraining && dropoutP > 0)
att = Dropout(att, dropoutP);
/* residual connection */
res = Sum(att, x);
/* layer normalization */
x = attLayerNorms[i].Make(res);
/*****************************/
/* encoder-decoder attention */
ende = attentionsEnde[i].Make(outputEnc, x, outputEnc, maskEncDec, isTraining);
/* dropout */
if(isTraining && dropoutP > 0)
ende = Dropout(ende, dropoutP);
/* residual connection */
res = Sum(ende, x);
/* layer normalization */
x = attEndeLayerNorms[i].Make(res);
/*******/
/* fnn */
fnn = fnns[i].Make(x, isTraining);
/* dropout */
if(isTraining && dropoutP > 0)
fnn = Dropout(fnn, dropoutP);
/* residual connection */
res = Sum(fnn, x);
/* layer normalization */
x = fnnLayerNorms[i].Make(res);
}
...
...
}
```
上述代码位于/source/sample/transformer/T2TDecoder.cpp
#### 训练
与其他神经机器翻译模型的训练一样,Transformer的训练流程为:首先对模型进行初始化,然后在编码器输入包含结束符的源语言单词序列。解码端每个位置单词的预测都要依赖已经生成的序列。其具体训练过程如下图所示,其中$\small\textnormal{C}_i$是编解码注意力的结果。我们在解码端输入包含起始符号的目标语序列,如下图中< eos >,通过起始符号预测目标语的第一个单词,用“How”去预测第二个单词,以此类推,然后用真实的目标语序列“How are you”和预测的结果比较,计算它的损失。损失越小说明模型的预测越接近真实输出。然后利用反向传播来调整模型中的参数。
![training](./pic/transformer-training.jpg)
需要注意的是,Transformer有许多常用的超参数设置,一般不会改变这些设置。
* 如Adam优化器优化参数设置为$\beta_1=0.9$,$\beta_2=0.98$,$\epsilon=10^{-9}$。
* 小批量训练(Mini-batch Training)中Batch大小通常设置为2048/4096(token数即每个批次中的单词个数)
#### 推断
Transformer的解码过程和训练时类似,都是从左往右生成,且下一个单词的预测依赖已经生成的上一个单词。推断过程如下图所示,在解码过程中,解码器首先根据< eos >和$\small\textnormal{C}_1$生成第一个单词“How”,然后根据“How”和$\small\textnormal{C}_2$生成第二个单词“are”,以此类推,当解码器生成< eos >时结束推断。
![translate](./pic/transformer-translate.jpg)
#### Transformer完整实现
最后附上NiuTensor中的Transformer机器翻译模型实现,完整代码详见:NiuTensor/source/sample/transformer。
```C++
/*
make the network for machine translation (with the output softmax layer)
>> inputEnc - input tensor of the encoder
>> inputDec - input tensor of the decoder
>> output - output tensor (distribution)
>> paddingEnc - padding of the sequences (on the encoder side)
>> paddingDec - padding of the sequences (on the decoder side)
>> isTraining - indicates whether the model is for training
*/
void T2TModel::MakeMT(XTensor &inputEnc, XTensor &inputDec, XTensor &output, XTensor &paddingEnc, XTensor &paddingDec, bool isTraining)
{
XTensor encoding;
XTensor decoding;
XTensor maskEnc;
XTensor maskDec;
XTensor maskEncDec;
/* encoder mask */
MakeMTMaskEnc(inputEnc, paddingEnc, maskEnc);
/* decoder mask */
MakeMTMaskDec(inputEnc, inputDec, paddingEnc, paddingDec, maskDec, maskEncDec);
encoding = MakeEncoder(inputEnc, maskEnc, isTraining);
decoding = MakeDecoder(inputDec, encoding, maskDec, maskEncDec, isTraining);
outputLayer->Make(decoding, output);
}
```
## 实例4:循环神经网络(即将发布)
<!-- 请点击[实例3:Transformer](Transformer/Transformer.html)进入教程 -->
## NiuTensor团队(按姓名拼音排序)
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论