# 实例

# 实例1:矩阵乘法

这里我们给出一个矩阵乘法的例子,首先定义张量维度的大小,然后初始化两个维度分别为的矩阵,使用SetData()方法对矩阵进行赋值,最后计算两个矩阵相乘。

关于矩阵乘法的详细代码请见NiuTensor/source/sample/mul/。

void sampleMUL1()
{
    DTYPE aData[2][3] = { { 1.0F, 2.0F, 3.0F },
                          { -4.0F, 5.0F, 6.0F } };
    DTYPE bData[3][2] = { { 0.0F, -1.0F },
                          { 1.0F, 2.0F },
                          { 2.0F, 1.0F } };
    DTYPE answer[2][2] = { { 8.0F, 6.0F },
                           { 17.0F, 20.0F } };

    /* a source tensor of size (2, 3) */
    int aOrder = 2;
    int * aDimSize = new int[aOrder];
    aDimSize[0] = 2;
    aDimSize[1] = 3;

    int aUnitNum = 1;
    for (int i = 0; i < aOrder; i++)
        aUnitNum *= aDimSize[i];

    /* a source tensor of size (3, 2) */
    int bOrder = 2;
    int * bDimSize = new int[bOrder];
    bDimSize[0] = 3;
    bDimSize[1] = 2;

    int bUnitNum = 1;
    for (int i = 0; i < bOrder; i++)
        bUnitNum *= bDimSize[i];

    /* a target tensor of size (2, 2) */
    int resultOrder = 2;
    int * resultDimSize = new int[resultOrder];
    resultDimSize[0] = 2;
    resultDimSize[1] = 2;

    int resultUnitNum = 1;
    for (int i = 0; i < resultOrder; i++)
        resultUnitNum *= resultDimSize[i];

	/* create tensors */
    XTensor * a = NewTensor(aOrder, aDimSize);
    XTensor * b = NewTensor(bOrder, bDimSize);
    XTensor * result = NewTensor(resultOrder, resultDimSize);

	/* initialize variables */
    a->SetData(aData, aUnitNum);
    b->SetData(bData, bUnitNum);
    result->SetZeroAll();

	/* call MatrixMul function */
    _MatrixMul(a, X_NOTRANS, b, X_NOTRANS, result);

    result->Dump(stderr, "result:");

	/* destroy variables */
    delete[] aDimSize;
    delete[] bDimSize;
    delete[] resultDimSize;
    delete a;
    delete b;
    delete result;
}

# 实例2:前馈神经网络

下面我们来实现一个简单的前馈神经网络语言模型。

语言模型任务是通过某种方式对语言建立数学模型的过程,用于评价一个序列的生成概率。

P(w1w2wm)=P(w1)P(w2w1)P(w3w1w2)P(wmw1wm1)P(w_1w_2 \dots w_m)=P(w_1)P(w_2|w_1)P(w_3|w_1w_2) \dots P(w_m|w_1 \dots w_{m-1})

在神经网络出现之前,一般使用统计的方法来设计语言模型。比较常见的为n-gram模型,它对文本中若干词语共现的频率进行统计,并使用平滑算法对未见词语搭配进行修正,最终得到该语言中不同词语连续出现的概率值。

P(w1w2wm)=P(w1)P(w2w1)P(w3w1w2)P(wmwmn+1wm1)P(w_1w_2 \dots w_m)=P(w_1)P(w_2|w_1)P(w_3|w_1w_2) \dots P(w_m|w_{m-n+1} \dots w_{m-1})

其中

P(wmwmn+1wm1)=count(wwn+1wm)count(wwn+1wm1)P(w_m|w_{m-n+1} \dots w_{m-1})=\frac{count(w_{w-n+1} \dots w_m)}{count(w_{w-n+1} \dots w_{m-1})}

表示序列在训练数据上统计的出现频次。

传统的n-gram语言模型实际上是一个查询表,通过序列 查询n-gram的概率。存在的问题是,随着n的增大,数据稀疏问题会非常严重,因为绝大多数的n-gram并没有在训练数据中出现过,而且维护n-gram的索引,存储的需求也很大。

神经语言模型相对传统基于统计的n-gram语言模型而言,能够在学习词语搭配的同时学习到词汇之间的相似性,相对平滑算法有效提高了对已知单词的未见搭配的预测效果,获得了更好的性能。

神经语言模型最早由Bengio等人系统化提出并进行了深入研究,其整体结构上和普通的前馈神经网络类似,由输入层、隐藏层和输出层组成,层和层之间存在连接,每一层将本层接收到的向量映射到另一维空间上作为该层的输出。一个4-gram的神经语言模型的结构如图所示。

语言模型

其中,表示第个词,图中绿色部分的是词的One-hot表示,也称为离散空间表示,每个词使用一个词表大小的向量表示,该词对应的位置为1,其他位置为0。

是词的分布式表示,也称为连续空间表示。One-hot向量中只有一个位置为1,其他位置为0,而且向量长度为词表长度。可以看作是一个查询表,将每个词表示为一个实数向量,如果向量长度为256,则是一个大小的矩阵。通过可以得到每个词对应的分布式向量表示,将三个向量级联,然后通过一个线性变换和Tanh激活函数,最后通过一个线性变换和Softmax函数预测词表中每个词的出现概率(Softmax函数保证了词表中所有词的概率和为1)。

在我们的实际实现过程中,由于Tanh容易溢出,所以一般采用HardTanh进行实现。

前馈神经网络语言模型的主要流程如下所示:

int FNNLMMain(int argc, const char ** argv)
{
    if(argc == 0)
        return 1;

    FNNModel model;

    /* load arguments */
    LoadArgs(argc, argv, model);

    /* check the setting */
    Check(model);

    /* initialize model parameters */
    Init(model);

    /* learn model parameters */
    if(strcmp(trainFN, ""))
        Train(trainFN, shuffled, model);

    /* save the final model */
    if(strcmp(modelFN, "") && strcmp(trainFN, ""))
        Dump(modelFN, model);

    /* load the model if neccessary */
    if(strcmp(modelFN, ""))
        Read(modelFN, model);

    /* test the model on the new data */
    if(strcmp(testFN, "") && strcmp(outputFN, ""))
        Test(testFN, outputFN, model);

    return 0;
}

对模型中的参数进行初始化:

/* initialize the model */
void Init(FNNModel &model)
{
    /* create embedding parameter matrix: vSize * eSize */
    InitModelTensor2D(model.embeddingW, model.vSize, model.eSize, model);
    model.embeddingW.SetVarFlag();
    
    /* create hidden layer parameter matrics */
    for(int i = 0; i < model.hDepth; i++){
        /* hidden layer parameter matrix: (n-1)eSize * hsize if it is the first layer
                                           hsize * hsize otherwise */
        if(i == 0)
            InitModelTensor2D(model.hiddenW[i], (model.n - 1) * model.eSize, model.hSize, model);
        else
            InitModelTensor2D(model.hiddenW[i], model.hSize, model.hSize, model);
        model.hiddenW[i].SetVarFlag();

        /* bias term: a row vector of hSize entries */
        InitModelTensor1D(model.hiddenB[i], model.hSize, model);
        model.hiddenB[i].SetVarFlag();
    }
    
    /* create the output layer parameter matrix and bias term */
    int iSize = model.hDepth == 0 ? (model.n - 1) * model.eSize : model.hSize;
    InitModelTensor2D(model.outputW, iSize, model.vSize, model);
    InitModelTensor1D(model.outputB, model.vSize, model);
    model.outputW.SetVarFlag();
    model.outputB.SetVarFlag();
    
    /* then, we initialize model parameters using a uniform distribution in range
       of [-minmax, minmax] */
    model.embeddingW.SetDataRand(-minmax, minmax);
    model.outputW.SetDataRand(-minmax, minmax);
    for(int i = 0; i < model.hDepth; i++)
        model.hiddenW[i].SetDataRand(-minmax, minmax);
    
    /* all bias terms are set to zero */
    model.outputB.SetZeroAll();
    for(int i = 0; i < model.hDepth; i++)
        model.hiddenB[i].SetZeroAll();
}

其中,SetVarFlag()函数将张量设定为变量(Variable),在计算过程中会通过反向传播得到这些张量的梯度,然后进行参数更新。

训练过程:

void Train(const char * train, bool isShuffled, FNNModel &model)
{
    char name[MAX_NAME_LENGTH];
    
    /* shuffle the data */
    if(isShuffled){
        sprintf(name, "%s-tmp", train);
        Shuffle(train, name);
    }
    else
        strcpy(name, train);
    
    int epoch = 0;
    int step = 0;
    int wordCount = 0;
    int wordCountTotal = 0;
    int ngramNum = 1;
    float loss = 0;
    bool isEnd = false;
    
    NGram * ngrams = new NGram[MAX_LINE_LENGTH_HERE];

    /* make a model to keep gradients */
    FNNModel grad;
    Copy(grad, model);

    /* XNet for automatic differentiation */
    XNet autoDiffer;

    double startT = GetClockSec();
    
    /* iterate for a number of epochs */
    for(epoch = 0; epoch < nEpoch; epoch++){

        /* data file */
        FILE * file = fopen(name, "rb");
        CheckErrors(file, "Cannot open the training file");

        wordCount = 0;
        loss = 0;
        ngramNum = 1;

        while(ngramNum > 0){
            
            /* load a minibatch of ngrams */
            ngramNum = LoadNGrams(file, model.n, ngrams, sentBatch, wordBatch);

            if (ngramNum <= 0)
                break;

            /* previous n - 1 words */
            XTensor inputs[MAX_N_GRAM];

            /* the predicted word */
            XTensor output;

            /* the gold standard */
            XTensor gold;

            /* the loss tensor */
            XTensor lossTensor;

            /* make the input tensor for position i */
            for(int i = 0; i < model.n - 1; i++)
                MakeWordBatch(inputs[i], ngrams, ngramNum, i, model.vSize, model.devID);

            /* make the gold tensor */
            MakeWordBatch(gold, ngrams, ngramNum, model.n - 1, model.vSize, model.devID);

            if(!autoDiff){
                /* prepare an empty network for building the fnn */
                FNNNet net;

                /* gradident = 0 */
                Clear(grad, false);

                /* forward computation */
                Forward(inputs, output, model, net);

                /* backward computation to obtain gradients */
                Backward(inputs, output, gold, CROSSENTROPY, model, grad, net);

                /* update model parameters */
                Update(model, grad, learningRate, false);

                /* get probabilities */
                float prob = GetProb(output, gold);
                loss -= prob;
            }
            else{
                /* gradient = 0 */
                Clear(model, true);

                /* forward + backward process */
                
                /* this is implemented by gather function */
                ForwardAutoDiff(ngrams, ngramNum, output, model);
                
                /* this is implemented by multiply function */
                lossTensor = CrossEntropy(output, gold);

                /* automatic differentiation */
                autoDiffer.Backward(lossTensor);

                /* update model parameters */
                Update(model, grad, learningRate, true);

                /* get probabilities */
                float prob;
                _ReduceSumAll(&lossTensor, &prob);
                loss += prob;
            }

            wordCount += ngramNum;
            wordCountTotal += ngramNum;
            
            if(++step >= nStep){
                isEnd = true;
                break;
            }

            if (step % 100 == 0) {
                double elapsed = GetClockSec() - startT;
                XPRINT5(0, stderr, "[INFO] elapsed=%.1fs, step=%d, epoch=%d, ngram=%d, ppl=%.3f\n",
                           elapsed, step, epoch + 1, wordCountTotal, exp(loss / wordCount));
            }
        }

        fclose(file);
        
        if(isEnd)
            break;

        Test(testFN, outputFN, model);
    }

    double elapsed = GetClockSec() - startT;
    
    XPRINT5(0, stderr, "[INFO] elapsed=%.1fs, step=%d, epoch=%d, ngram=%d, ppl=%.3f\n", 
               elapsed, step, epoch, wordCountTotal, exp(loss / wordCount));
    XPRINT3(0, stderr, "[INFO] training finished (took %.1fs, step=%d and epoch=%d)\n", 
               elapsed, step, epoch);
    
    delete[] ngrams;
}

我们实现了手动微分和自动微分两种方式,通过参数autoDiff控制。自动微分的方式对于开发者来说更加友好,不需要手动计算反向传播过程中的梯度,可以方便快捷地实现多种网络结构。下面我们以自动微分为例介绍主要代码。

前馈神经网络前向计算部分的代码如下:

void ForwardAutoDiff(XTensor inputs[], XTensor &output, FNNModel &model)
{
    int n = model.n;
    int depth = model.hDepth;

    XTensor words;
    XTensor embeddingBig;
    XTensor hidden;
    XTensor b;

    TensorList inputList(n - 1);
    for(int i = 0; i < n - 1; i++)
        inputList.Add(inputs + i);

    /* represent n - 1 words in one tensor */
    words = Merge(inputList, 0);

    /* word embedding */
    embeddingBig = MMul(words, model.embeddingW);

    /* input of the first hidden layer */
    hidden = Split(embeddingBig, 0, n - 1);
    hidden = Merge(hidden, 2, 0);

    /* hidden layers */
    for(int i = 0; i < depth; i++)
        hidden = MMul(hidden, model.hiddenW[i]) + model.hiddenB[i];

    /* output layer */
    output = LogSoftmax(MMul(hidden, model.outputW) + model.outputB, 1);

}

经过数据处理之后我们得到了语言模型的输入inputs(n-1个词),我们通过Gather函数,根据输入词从词嵌入矩阵embeddingW中取出每个输入单词的向量表示,公式如下:

然后将n-1个词的向量级联作为输入层最终的输出(代码中通过Reshape函数实现)。

同理,我们将输入层的输出分别经过隐藏层和输出层得到最终的结果,公式如下:

前向过程计算完成之后,我们通过交叉熵函数(CrossEntropy)计算预测结果和标准答案之间的损失。

lossTensor = CrossEntropy(output, gold);

NiuTensor实现了自动计算反向过程求得参数的梯度,只需要通过下面一行代码。

autoDiffer.Backward(lossTensor);

然后采用梯度下降的方法通过反向传播计算得到损失函数L对参数的导数,之后我们根据公式

η

对参数w进行更新,其中η是学习率。

通过不断迭代进行上述的前向、反向和参数更新过程,模型逐渐达到收敛状态。

我们简要介绍了通过NiuTensor实现前馈神经网络的代码流程和细节,完整代码请参见NiuTensor/source/sample/fnnlm/FNNLM.cpp。

# 实例3:Transformer

# Transformer简要介绍

Transformer模型出自论文“Attention is All You Need”,自其问世以来就迅速席卷了自然语言处理领域,并在各类主流任务上取得了新的突破,包括机器翻译、语言建模、序列标注和文本分类等。

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结构

# 词向量和位置信息编码

Transformer的输入主要由两部分组成,分别是词汇embedding和位置编码,词汇embedding和位置编码维度相同,通过位置编码公式:

式中PE()表示位置编码的函数,表示单词的位置,代表位置编码向量中的第几维。计算得到的词汇位置编码PE()与embedding即e()加和,得到模型的真实输入h()。

Transformer结构

/* 
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;
}

上述代码位于NiuTensor/source/sample/transformer/T2TEmbedding.cpp

# encoder层

Attention

当数据输入到模型后首先要计算的是encoder第一层的self-attention。不同于传统的RNN、LSTM等结构在对序列中远距离关系建模时,需要将目标关系之间的词全部按序输入到模型中,注意力机制使得Transformer框架可以快速的获得句子中的词汇相关性。注意力机制的运算可以被形式化为:

其中的维度为的维度为为序列的长度。是输入数据乘以参数矩阵得到的,同样是乘以各自的参数矩阵得到。自注意力机制首先通过对输入进行变换得到(查询)、(键)和(值),通过(查询)与(键)得到一个维度为的矩阵,该矩阵表示一个序列上任意两个位置()的相关性。再通过系数1/进行放缩操作,放缩可以尽量减少相关性矩阵的方差。在此基础上,通过对相关性矩阵累加一个掩码矩阵,来屏蔽掉矩阵中的无用信息,该操作在训练过程中体现为,在编码端对句子的补齐或屏蔽掉解码端的未来信息。随后使用Softmax函数对相关性矩阵在行的维度上进行归一化操作,这可以理解为对第行进行归一化,结果对应了中的不同位置上向量的注意力权重。对于的加权求和,可以直接用相关性性系数和进行矩阵乘法得到,即进行矩阵乘。最终我们就到了自注意力的输出,它和输入的的大小是一模一样的。多头注意力的运算与上述并无区别,只是在多头注意力中的每个头将会通过各自的矩阵计算出attention,并将attention拼接后传递至下一个模块

Transformer结构

可视化attention矩阵操作可以参考下图

Transformer结构

代码如下

/* 
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);
}

上述代码位于NiuTensor/source/sample/transformer/T2TAttention.cpp

全连接

在得到attention的输出结果之后,数据将流入第一层encoder的下一个模块,即全连接层。在此处的全连接层为一个双层的全连接网络,标准的输入维度与输出维度为512,隐层维度为2048,在隐层的输出位置使用Relu作为激活函数。全连接网络的作用主要体现在将经过注意力操作之后的表示映射到新的空间中,新的空间会有利于接下来的非线性变换等操作。当数据经过全连接网络后,将作为第二层encoder的输入经第二层计算后继续向第三层传递。

/* 
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);

    /* result = t1 * w2 + b2 */
    //return MMul(t1, w2) + b2;
    return MulAndShift(t1, w2, b2);
}

上述代码位于NiuTensor/source/sample/transformer/T2TFNN.cpp

残差与层正则化

在之前的Transformer结构图中可以发现encoder中还有两个操作,分别是残差链接和层正则化。残差连接从广义上讲也叫短连接(short-cut connection),指的是这种短距离的连接。它的思想很简单,就是把层和层之间的距离拉近。其计算公式为:

从上式中可以看出,当求导时,无论有多小,都会有的导数为1。这就极大的缓解了梯度消失的问题。同时,由于引入了残差操作,将前面所有层的输出加到一起。这样会导致不同层(或子层)的结果之间的差异性很大,造成训练过程不稳定、训练时间较长。为了避免这种情况,在每层中加入了层正则化操作。

/* 
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);
    }

上述代码位于NiuTensor/source/sample/transformer/T2TEncoder.cpp

# decoder层

可以对应前面Transformer结构图,在数据输入decoder后,首先计算self-attention,其后计算encoder-decoder attention(这是encoder端与decoder端计算的唯一区别),encoder-decoder attention与self-attention计算十分相似,同样通过计算attention,但是在encoder-decoder attention中只有来自decoder,而均来自于encoder最顶层的输出。这部分计算完成后,接下来再经过一个全连接层得到该层decoder的输出,与encoder端一样,该层decoder的输出将作为下一层decoder的输入继续传递。

/* 
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);
    }
    ...
    ...
}

上述代码位于NiuTensor/source/sample/transformer/T2TDecoder.cpp

# 训练

与其他神经机器翻译模型的训练一样,Transformer的训练流程为:首先对模型进行初始化,然后在编码器输入包含结束符的源语言单词序列。解码端每个位置单词的预测都要依赖已经生成的序列。其具体训练过程如下图所示,其中是编解码注意力的结果。我们在解码端输入包含起始符号的目标语序列,如下图中< eos >,通过起始符号预测目标语的第一个单词,用“How”去预测第二个单词,以此类推,然后用真实的目标语序列“How are you”和预测的结果比较,计算它的损失。损失越小说明模型的预测越接近真实输出。然后利用反向传播来调整模型中的参数。

training

需要注意的是,Transformer有许多常用的超参数设置,一般不会改变这些设置。

  • 如Adam优化器优化参数设置为
  • 小批量训练(Mini-batch Training)中Batch大小通常设置为2048/4096(token数即每个批次中的单词个数)

# 推断

Transformer的解码过程和训练时类似,都是从左往右生成,且下一个单词的预测依赖已经生成的上一个单词。推断过程如下图所示,在解码过程中,解码器首先根据< eos >和生成第一个单词“How”,然后根据“How”和生成第二个单词“are”,以此类推,当解码器生成< eos >时结束推断。

translate

# Transformer完整实现

最后附上NiuTensor中的Transformer机器翻译模型实现,完整代码详见:NiuTensor/source/sample/transformer。

/* 
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);
}