你当前正在访问 Microsoft Azure Global Edition 技术文档网站。 如果需要访问由世纪互联运营的 Microsoft Azure 中国技术文档网站,请访问 https://docs.azure.cn

具有循环网络的动手实验室语言理解

请注意,本教程需要最新的主版本,或者即将发布的 CNTK 1.7.1。

此动手实验室演示如何实现循环网络来处理文本,这些网络适用于航空旅行信息服务 (ATIS) 槽标记和意向分类的任务。 我们将从直接嵌入开始,然后是反复的 LSTM。 然后,我们将扩展它以包含相邻字词并双向运行。 最后,我们将此系统转换为意向分类器。

你将练习的技术包括:

  • 通过撰写层块而不是编写公式来模型说明
  • 创建自己的层块
  • 具有相同网络中不同序列长度的变量
  • 并行训练

我们假设你熟悉深度学习的基础知识,以及以下特定概念:

先决条件

我们假设你已安装 CNTK 并可以运行 CNTK 命令。 本教程在 KDD 2016 上举行,需要最近的内部版本, 请参阅此处 以获取设置说明。 只需按照说明从该页下载二进制安装包。

接下来,请下载 ZIP 存档 (大约 12 MB) :单击 此链接,然后在“下载”按钮上。 存档包含本教程的文件。 请存档并将工作目录设置为 SLUHandsOn。 你将使用的文件包括:

最后,我们强烈建议在支持 CUDA 兼容的 GPU 的计算机上运行此功能。 没有 GPU 的深度学习并不有趣。

任务和模型结构

本教程中要处理的任务是槽标记。 我们使用 ATIS corpus。 ATIS 包含来自航空旅行信息服务的域的人工计算机查询,我们的任务是批注 (标记) 查询的每个单词,它是否属于特定信息项 (槽) ,以及哪一个。

工作文件夹中的数据已转换为“CNTK 文本格式”。让我们看看测试集文件中 atis.test.ctf的示例:

19  |S0 178:1 |# BOS      |S1 14:1 |# flight  |S2 128:1 |# O
19  |S0 770:1 |# show                         |S2 128:1 |# O
19  |S0 429:1 |# flights                      |S2 128:1 |# O
19  |S0 444:1 |# from                         |S2 128:1 |# O
19  |S0 272:1 |# burbank                      |S2 48:1  |# B-fromloc.city_name
19  |S0 851:1 |# to                           |S2 128:1 |# O
19  |S0 789:1 |# st.                          |S2 78:1  |# B-toloc.city_name
19  |S0 564:1 |# louis                        |S2 125:1 |# I-toloc.city_name
19  |S0 654:1 |# on                           |S2 128:1 |# O
19  |S0 601:1 |# monday                       |S2 26:1  |# B-depart_date.day_name
19  |S0 179:1 |# EOS                          |S2 128:1 |# O

此文件包含 7 列:

  • 序列 ID (19) 。 此序列 ID 有 11 个条目。这意味着序列 19 包含 11 个标记;
  • S0,其中包含数字字索引;
  • 表示的 #注释列,以允许人类读者知道数字单词索引代表什么:系统忽略注释列。 BOS 以及 EOS 分别表示句子开头和结尾的特殊单词:
  • S1 是意向标签,仅在本教程的最后一部分使用:
  • 另一个注释列,显示数字意向索引的可读标签;
  • S2 是槽标签,表示为数字索引;并且
  • 另一个注释列,显示数字标签索引的可读标签。

神经网络的任务是查看查询 (列 S0) 并预测槽标签 (列 S2) 。 正如你所看到的,输入中的每个单词都会分配一个空标签 O 或一个以第一个单词开头 B- 的槽标签,以及 I- 属于同一槽的任何其他连续单词。

我们将使用的模型是一个循环模型,包括嵌入层、循环 LSTM 单元格和密集层来计算后向概率:

slot label   "O"        "O"        "O"        "O"  "B-fromloc.city_name"
              ^          ^          ^          ^          ^
              |          |          |          |          |
          +-------+  +-------+  +-------+  +-------+  +-------+
          | Dense |  | Dense |  | Dense |  | Dense |  | Dense |  ...
          +-------+  +-------+  +-------+  +-------+  +-------+
              ^          ^          ^          ^          ^
              |          |          |          |          |
          +------+   +------+   +------+   +------+   +------+   
     0 -->| LSTM |-->| LSTM |-->| LSTM |-->| LSTM |-->| LSTM |-->...
          +------+   +------+   +------+   +------+   +------+   
              ^          ^          ^          ^          ^
              |          |          |          |          |
          +-------+  +-------+  +-------+  +-------+  +-------+
          | Embed |  | Embed |  | Embed |  | Embed |  | Embed |  ...
          +-------+  +-------+  +-------+  +-------+  +-------+
              ^          ^          ^          ^          ^
              |          |          |          |          |
w      ------>+--------->+--------->+--------->+--------->+------... 
             BOS      "show"    "flights"    "from"   "burbank"

或者,作为 CNTK 网络说明。 请快速查看,并将其与上述说明匹配:

    model = Sequential (
        EmbeddingLayer {150} :
        RecurrentLSTMLayer {300} :
        DenseLayer {labelDim}
    )

可在以下位置找到这些函数的说明:Sequential()、、EmbeddingLayer{}RecurrentLSTMLayer{}DenseLayer{}

CNTK 配置

配置文件

若要在 CNTK 中训练和测试模型,我们需要提供一个配置文件,告知 CNTK 要运行哪些操作 (command 变量) ,以及每个命令的参数节。

对于训练命令,需要告诉 CNTK:

  • 如何读取数据 (reader 部分)
  • 计算图中的模型函数及其输入和输出 (部分) BrainScriptNetworkBuilder
  • 学习器 (部分的 SGD hyper-parameters)

对于评估命令,CNTK 需要了解如何读取测试数据 (reader 部分) 。

下面是我们将开始使用的配置文件。 如你所看到的,CNTK 配置文件是一个文本文件,其中包含参数的定义,这些定义在记录层次结构中组织。 还可以查看 CNTK 如何使用语法支持基本参数替换 $parameterName$ 。 实际文件只包含上述几个参数,但请扫描该文件并找到刚才提到的配置项:

# CNTK Configuration File for creating a slot tagger and an intent tagger.

command = TrainTagger:TestTagger

makeMode = false ; traceLevel = 0 ; deviceId = "auto"

rootDir = "." ; dataDir  = "$rootDir$" ; modelDir = "$rootDir$/Models"

modelPath = "$modelDir$/slu.cmf"

vocabSize = 943 ; numLabels = 129 ; numIntents = 26    # number of words in vocab, slot labels, and intent labels

# The command to train the LSTM model
TrainTagger = {
    action = "train"
    BrainScriptNetworkBuilder = {
        inputDim = $vocabSize$
        labelDim = $numLabels$
        embDim = 150
        hiddenDim = 300

        model = Sequential (
            EmbeddingLayer {embDim} :                            # embedding
            RecurrentLSTMLayer {hiddenDim, goBackwards=false} :  # LSTM
            DenseLayer {labelDim}                                # output layer
        )

        # features
        query      = Input {inputDim}
        slotLabels = Input {labelDim}

        # model application
        z = model (query)

        # loss and metric
        ce   = CrossEntropyWithSoftmax (slotLabels, z)
        errs = ClassificationError     (slotLabels, z)

        featureNodes    = (query)
        labelNodes      = (slotLabels)
        criterionNodes  = (ce)
        evaluationNodes = (errs)
        outputNodes     = (z)
    }

    SGD = {
        maxEpochs = 8 ; epochSize = 36000

        minibatchSize = 70

        learningRatesPerSample = 0.003*2:0.0015*12:0.0003
        gradUpdateType = "fsAdaGrad"
        gradientClippingWithTruncation = true ; clippingThresholdPerSample = 15.0

        firstMBsToShowResult = 10 ; numMBsToShowResult = 100
    }

    reader = {
        readerType = "CNTKTextFormatReader"
        file = "$DataDir$/atis.train.ctf"
        randomize = true
        input = {
            query        = { alias = "S0" ; dim = $vocabSize$ ;  format = "sparse" }
            intentLabels = { alias = "S1" ; dim = $numIntents$ ; format = "sparse" }
            slotLabels   = { alias = "S2" ; dim = $numLabels$ ;  format = "sparse" }
        }
    }
}

# Test the model's accuracy (as an error count)
TestTagger = {
    action = "eval"
    modelPath = $modelPath$
    reader = {
        readerType = "CNTKTextFormatReader"
        file = "$DataDir$/atis.test.ctf"
        randomize = false
        input = {
            query        = { alias = "S0" ; dim = $vocabSize$ ;  format = "sparse" }
            intentLabels = { alias = "S1" ; dim = $numIntents$ ; format = "sparse" }
            slotLabels   = { alias = "S2" ; dim = $numLabels$ ;  format = "sparse" }
        }
    }
}

简要了解数据和数据读取

我们已经查看了数据。 但是如何生成此格式? 对于阅读文本,本教程使用 CNTKTextFormatReader. 它要求输入数据采用特定格式, 此处介绍。

在本教程中,我们按两个步骤创建了 corpora:

  • 将原始数据转换为纯文本文件,其中包含空格分隔文本的 TAB 分隔列。 例如:

    BOS show flights from burbank to st. louis on monday EOS (TAB) flight (TAB) O O O O B-fromloc.city_name O B-toloc.city_name I-toloc.city_name O B-depart_date.day_name O
    

    这旨在与命令的 paste 输出兼容。

  • 使用以下命令将其转换为 CNTK 文本格式 (CTF) :

    python Scripts/txt2ctf.py --map query.wl intent.wl slots.wl --annotated True --input atis.test.txt --output atis.test.ctf
    

    其中,这三 .wl 个文件提供词汇作为纯文本文件,每字一行。

在这些 CTFG 文件中,将标记 S0列, S1以及 S2。 这些输入通过读取器定义中的相应行连接到实际网络输入:

input = {
    query        = { alias = "S0" ; dim = $vocabSize$ ;  format = "sparse" }
    intentLabels = { alias = "S1" ; dim = $numIntents$ ; format = "sparse" }
    slotLabels   = { alias = "S2" ; dim = $numLabels$ ;  format = "sparse" }
}

运行它

可以在工作文件夹中的名称 SLUHandsOn.cntk 下找到上述配置文件。 若要运行它,请运行以下命令执行上述配置:

cntk  configFile=SLUHandsOn.cntk

这将执行我们的配置,从我们命名 TrainTagger的节中定义的模型训练开始。 在有点聊天的初始日志输出后,你将很快看到:

Training 721479 parameters in 6 parameter tensors.

后跟如下所示的输出:

Finished Epoch[ 1 of 8]: [Training] ce = 0.77274927 * 36007; errs = 15.344% * 36007
Finished Epoch[ 2 of 8]: [Training] ce = 0.27009664 * 36001; errs = 5.883% * 36001
Finished Epoch[ 3 of 8]: [Training] ce = 0.16390425 * 36005; errs = 3.688% * 36005
Finished Epoch[ 4 of 8]: [Training] ce = 0.13121604 * 35997; errs = 2.761% * 35997
Finished Epoch[ 5 of 8]: [Training] ce = 0.09308497 * 36000; errs = 2.028% * 36000
Finished Epoch[ 6 of 8]: [Training] ce = 0.08537533 * 35999; errs = 1.917% * 35999
Finished Epoch[ 7 of 8]: [Training] ce = 0.07477648 * 35997; errs = 1.686% * 35997
Finished Epoch[ 8 of 8]: [Training] ce = 0.06114417 * 36018; errs = 1.380% * 36018

这显示了学习在 (通过数据) 的时期如何进行学习。 例如,在两个纪元之后,我们在配置文件中命名 ce 的跨枚举条件已达到 0.27,根据此纪元的 36001 样本测量,错误率为 5.883%,这两个 36016 训练样本的误差率为 5.883%。

36001 源于我们的配置将时代大小定义为 36000。 时代大小是模型检查点之间的样本数(而不是句子)计数为 单词标记。 由于句子的长度不同,并且不一定汇总到精确 36000 字的倍数,你将看到一些小变化。

训练完成 (泰坦-X或Surface Book) 不到2分钟后,CNTK将继续EvalTagger操作

Final Results: Minibatch[1-1]: errs = 2.922% * 10984; ce = 0.14306181 * 10984; perplexity = 1.15380111

即,在我们的测试集中,已预测槽标签的误差率为 2.9%。 根本不糟糕,对于这样一个简单的系统!

在仅限 CPU 的计算机上,它可以慢 4 倍或更多倍。 若要确保系统提前进行,可以启用跟踪以查看部分结果,结果应相当快地显示:

cntk  configFile=SLUHandsOn.cntk  traceLevel=1

Epoch[ 1 of 8]-Minibatch[   1-   1, 0.19%]: ce = 4.86535690 * 67; errs = 100.000% * 67 
Epoch[ 1 of 8]-Minibatch[   2-   2, 0.39%]: ce = 4.83886670 * 63; errs = 57.143% * 63
Epoch[ 1 of 8]-Minibatch[   3-   3, 0.58%]: ce = 4.78657442 * 68; errs = 36.765% * 68
...

如果不想等到完成,可以运行中间模型,例如

cntk  configFile=SLUHandsOn.cntk  command=TestTagger  modelPath=Models/slu.cmf.4
Final Results: Minibatch[1-1]: errs = 3.851% * 10984; ce = 0.18932937 * 10984; perplexity = 1.20843890

或测试预先训练的模型,也可以在工作文件夹中找到该模型:

cntk  configFile=SLUHandsOn.cntk  command=TestTagger  modelPath=slu.forward.nobn.cmf
Final Results: Minibatch[1-1]: errs = 2.922% * 10984; ce = 0.14306181 * 10984; perplexity = 1.15380111

修改模型

在下面,你将获得用于练习修改 CNTK 配置的任务。 本文档末尾提供了解决方案...但请不要尝试!

关于的单词 Sequential()

跳到任务之前,让我们再看看刚刚运行的模型。 模型在调用 函数组合样式时进行描述。

    model = Sequential (
        EmbeddingLayer {embDim} :                            # embedding
        RecurrentLSTMLayer {hiddenDim, goBackwards=false} :  # LSTM
        DenseLayer {labelDim, initValueScale=7}              # output layer
    )

其中冒号 (:) 是 BrainScript 表示数组的语法。 例如,是一个数组,(F:G:H)其中包含三个元素,FG以及H

你可能熟悉来自其他神经网络工具包的“顺序”表示法。 如果不是,这是一个强大的操作, Sequential() 简言之,允许在神经网络中紧凑地表达一种非常常见的情况,其中输入是通过层的进度传播来处理的。 Sequential() 将函数数组作为其参数,并返回一 个新 函数,该函数按顺序调用这些函数,每次将一个函数的输出传递给下一个函数。 例如,

FGH = Sequential (F:G:H)
y = FGH (x)

表示与 相同

y = H(G(F(x))) 

这称为 “函数组合”,对于表达神经网络尤其方便,通常采用以下形式:

     +-------+   +-------+   +-------+
x -->|   F   |-->|   G   |-->|   H   |--> y
     +-------+   +-------+   +-------+

回到我们手头的模型时,表达式 Sequential 只是说模型有以下形式:

     +-----------+   +----------------+   +------------+
x -->| Embedding |-->| Recurrent LSTM |-->| DenseLayer |--> y
     +-----------+   +----------------+   +------------+

任务 1:添加批处理规范化

我们现在希望向模型添加新层,特别是批处理规范化。

批处理规范化是加快收敛的常用技术。 它通常用于图像处理设置,例如图像 识别上的其他动手实验室。 但是,它是否也适用于重复性模型?

因此,你的任务是在循环 LSTM 层前后插入批处理规范化层。 如果已完成 图像处理上的动手实验室,你可能会记得 批处理规范化层 采用以下形式:

BatchNormalizationLayer{}

因此,请继续修改配置,看看会发生什么情况。

如果一切正常,你不仅注意到与以前的配置相比提高了收敛速度 (ceerrs) ,而且与 2.9% ) 相比,错误率提高了 2.0% (:

Training 722379 parameters in 10 parameter tensors.

Finished Epoch[ 1 of 8]: [Training] ce = 0.29396894 * 36007; errs = 5.621% * 36007 
Finished Epoch[ 2 of 8]: [Training] ce = 0.10104186 * 36001; errs = 2.280% * 36001
Finished Epoch[ 3 of 8]: [Training] ce = 0.05012737 * 36005; errs = 1.258% * 36005
Finished Epoch[ 4 of 8]: [Training] ce = 0.04116407 * 35997; errs = 1.108% * 35997
Finished Epoch[ 5 of 8]: [Training] ce = 0.02602344 * 36000; errs = 0.756% * 36000
Finished Epoch[ 6 of 8]: [Training] ce = 0.02234042 * 35999; errs = 0.622% * 35999
Finished Epoch[ 7 of 8]: [Training] ce = 0.01931362 * 35997; errs = 0.667% * 35997
Finished Epoch[ 8 of 8]: [Training] ce = 0.01714253 * 36018; errs = 0.522% * 36018

Final Results: Minibatch[1-1]: errs = 2.039% * 10984; ce = 0.12888706 * 10984; perplexity = 1.13756164

(如果不想等待训练完成,可以在名称 slu.forward.cmf.) 下找到生成的模型

请参阅 此处的解决方案。

任务 2:添加 Lookahead

我们的重复模型存在结构性缺陷:由于重复周期从左到右运行,因此槽标签的决定没有有关即将出现的单词的信息。 模型有点倾斜。 你的任务是修改模型,以便对重复周期的输入不仅包含当前单词,还包括下一个 (lookahead) 。

解决方案应采用函数组合样式。 因此,需要编写执行以下操作的 BrainScript 函数:

  • 接受一个输入参数;
  • 使用 FutureValue() 函数计算此输入的即时“未来值”, (使用此特定形式: FutureValue (0, input, defaultHiddenActivation=0)) ; 和
  • 使用 Splice() (将两者串联为嵌入维度的两倍的矢量:Splice (x:y))

然后将此函数 Sequence() 插入嵌入层和循环层之间。 如果一切顺利,你将看到以下输出:

Training 902679 parameters in 10 parameter tensors.

Finished Epoch[ 1 of 8]: [Training] ce = 0.30500536 * 36007; errs = 5.904% * 36007
Finished Epoch[ 2 of 8]: [Training] ce = 0.09723847 * 36001; errs = 2.167% * 36001
Finished Epoch[ 3 of 8]: [Training] ce = 0.04082365 * 36005; errs = 1.047% * 36005
Finished Epoch[ 4 of 8]: [Training] ce = 0.03219930 * 35997; errs = 0.867% * 35997
Finished Epoch[ 5 of 8]: [Training] ce = 0.01524993 * 36000; errs = 0.414% * 36000
Finished Epoch[ 6 of 8]: [Training] ce = 0.01367533 * 35999; errs = 0.383% * 35999
Finished Epoch[ 7 of 8]: [Training] ce = 0.00937027 * 35997; errs = 0.278% * 35997
Finished Epoch[ 8 of 8]: [Training] ce = 0.00584430 * 36018; errs = 0.147% * 36018

Final Results: Minibatch[1-1]: errs = 1.839% * 10984; ce = 0.12023170 * 10984; perplexity = 1.12775812

这不起作用! 了解下一个单词允许槽标记器将其错误率从 2.0% 降低到 1.84%。

(如果不想等待训练完成,可以在名称 slu.forward.lookahead.cmf.) 下找到生成的模型

请参阅 此处的解决方案。

任务 3:双向循环模型

阿哈,未来单词的知识帮助。 所以,而不是单字看头,为什么直到一路到句子的末尾,通过向后重复? 让我们创建双向模型!

你的任务是实现一个新层,该层同时对数据执行向前和向后递归,并连接输出向量。

但是,请注意,这与上一个任务不同,即双向层包含可学习的模型参数。 在函数组合样式中,使用模型参数实现层的模式是编写创建函数对象的工厂函数

函数对象(也称为 functor)是一个既是函数又是一个对象。 这意味着它包含数据的其他任何内容仍可以调用,就像它是一个函数一样。

例如,是一个工厂函数, LinearLayer{outDim} 它返回一个函数对象,该对象包含权重矩阵 W、偏差 b和另一个要计算 W * input + b的函数。 例如,说LinearLayer{1024}将创建此函数对象,然后可以像任何其他函数一样使用,也可以立即: LinearLayer{1024}(x)

困惑? 让我们以一个示例为例:让我们实现将线性层与后续批处理规范化相结合的新层。 若要允许函数组合,需要将层实现为工厂函数,如下所示:

LinearLayerWithBN {outDim} = {
    F = LinearLayer {outDim}
    G = BatchNormalization {normalizationTimeConstant=2048}
    apply (x) = G(F(x))
}.apply

调用此工厂函数将首先创建一个记录 (,该记录由 {...} 具有三个成员的) 指示: FG以及 apply。 在此示例中, F 函数 G 对象本身是函数对象,并且 apply 是要应用于数据的函数。 .apply追加到此表达式意味着.x在 BrainScript 中始终意味着访问记录成员。 因此,调用LinearLayerWithBN{1024}将创建一个对象,其中包含名为F线性层函数对象、批处理规范化函数对象的G对象,apply以及实现此层F的实际操作的函数。G 然后,它将返回 apply。 在外部, apply() 外观和行为类似于函数。 然而,在幕后, apply() 将保留它所属的记录,从而保留对其特定实例 FG它的访问。

现在回到我们手头的任务。 现在需要创建工厂函数,这与上面的示例非常相似。 应创建一个工厂函数,该函数创建两个循环层实例, (一个向前、一个向后) ,然后定义一个函数,该函数将这两个层实例应用于同x一个层实例,并连接这两个apply (x)结果。

好吧,试一试! 若要了解如何在 CNTK 中实现向后递归,请从如何完成向前递归中获取提示。 另请执行下列操作:

  • 删除在上一个任务中添加的一字 lookahead,我们打算替换该任务;和
  • hiddenDim 参数从 300 更改为 150,以使模型参数总数受到限制。

成功运行此模型将生成以下输出:

Training 542379 parameters in 13 parameter tensors.

Finished Epoch[ 1 of 8]: [Training] ce = 0.27651655 * 36007; errs = 5.288% * 36007
Finished Epoch[ 2 of 8]: [Training] ce = 0.08179804 * 36001; errs = 1.869% * 36001
Finished Epoch[ 3 of 8]: [Training] ce = 0.03528780 * 36005; errs = 0.828% * 36005
Finished Epoch[ 4 of 8]: [Training] ce = 0.02602517 * 35997; errs = 0.675% * 35997
Finished Epoch[ 5 of 8]: [Training] ce = 0.01310307 * 36000; errs = 0.386% * 36000
Finished Epoch[ 6 of 8]: [Training] ce = 0.01310714 * 35999; errs = 0.358% * 35999
Finished Epoch[ 7 of 8]: [Training] ce = 0.00900459 * 35997; errs = 0.300% * 35997
Finished Epoch[ 8 of 8]: [Training] ce = 0.00589050 * 36018; errs = 0.161% * 36018

Final Results: Minibatch[1-1]: errs = 1.830% * 10984; ce = 0.11924878 * 10984; perplexity = 1.12665017

像魅力一样工作! 此模型达到 1.83%,比上面的看头模型好一点。 双向模型的参数比 lookahead 少 40%。 但是,如果返回并仔细查看此网页) 未显示的完整日志输出 (,你可能会发现查找头的训练速度约为 30%。 这是因为 lookahead 模型的水平依赖项 (一个而不是两个重复周期) 和较大的矩阵产品,因此可以实现更高的并行度。

请参阅 此处的解决方案。

任务 4:意向分类

事实证明,到目前为止我们构造的模型很容易变成意向分类器。 请记住,我们的数据文件包含名为此附加列的列 S1。 此列包含每个句子的单个标签,指示查询查找有关主题的信息(例如 airportairfare)。

将整个序列分类到单个标签的任务称为 序列分类。 我们的序列分类器将作为循环 LSTM (我们已经拥有了) ,我们采取了其 最后一步的隐藏状态。 这样,我们为每个序列提供了一个向量。 然后,此矢量将馈送到密集层进行 softmax 分类。

CNTK 具有一个操作,用于从名为 BS.Sequences.Last() 的序列中提取最后一个状态。 此操作遵循相同的迷你包可能包含非常不同的长度序列,并且它们以打包格式排列在内存中。 同样,对于向后递归,可以使用 BS.Sequences.First()

任务是从任务 3 修改双向网络,以便从向前递归中提取最后一个帧,第一个帧是从向后递归中提取的,并且两个向量是串联的。 串联向量 (有时称为 思想向量) 应是密集层的输入。

此外,必须将标签从槽更改为意向标签:只需将输入变量重命名 (slotLabels) ,以匹配在意向标签的读取器部分中使用的名称,并匹配维度。

请尝试修改。 但是,如果这样做正确,你将面对一条令人发快的错误消息,并在其中遇到很长的错误消息:

EXCEPTION occurred: Dynamic axis layout '*' is shared between inputs 'intentLabels'
and 'query', but layouts generated from the input data are incompatible on this axis.
Are you using different sequence lengths? Did you consider adding a DynamicAxis()
to the Input nodes?

“你使用的是不同的序列长度吗?哦,是的! 查询和意向标签 do-the intent label is only one token per query. 它是 1 个元素的序列! 那么如何解决此问题?

CNTK 允许网络中的不同变量具有不同的序列长度。 可以将序列长度视为附加的符号张量维度。 相同长度的变量共享相同的符号长度维度。 如果两个变量具有不同的长度,则必须显式声明这一点,否则 CNTK 将假定所有变量共享相同的符号长度。

为此,请创建新的 动态轴 对象并将其与其中一个输入相关联,如下所示:

    n = DynamicAxis()
    query = Input {inputDim, dynamicAxis=n}

CNTK 具有默认轴。 正如你从上述异常中猜测的那样,其名称为“*”。
因此,只需声明一个新轴;其他输入 (intentLabels) 将继续使用默认轴。

现在,我们应该可以运行,并查看以下输出:

Training 511376 parameters in 13 parameter tensors.

Finished Epoch[ 1 of 8]: [Training] ce = 1.17365003 * 2702; errs = 21.318% * 2702
Finished Epoch[ 2 of 8]: [Training] ce = 0.40112341 * 2677; errs = 9.189% * 2677
Finished Epoch[ 3 of 8]: [Training] ce = 0.17041608 * 2688; errs = 4.167% * 2688
Finished Epoch[ 4 of 8]: [Training] ce = 0.09521124 * 2702; errs = 2.739% * 2702
Finished Epoch[ 5 of 8]: [Training] ce = 0.08287138 * 2697; errs = 2.262% * 2697
Finished Epoch[ 6 of 8]: [Training] ce = 0.07138554 * 2707; errs = 2.032% * 2707
Finished Epoch[ 7 of 8]: [Training] ce = 0.06220047 * 2677; errs = 1.419% * 2677
Finished Epoch[ 8 of 8]: [Training] ce = 0.05072431 * 2686; errs = 1.340% * 2686

Final Results: Minibatch[1-1]: errs = 4.143% * 893; ce = 0.27832144 * 893; perplexity = 1.32091072

毫不费力,我们取得了 4.1% 的错误率。 对于第一次射门 (非常好,尽管这一任务不太擅长,这在3%) 。

不过,你可能会注意到一件事:每个时期的样本数现在约为 2700。 这是因为这是标签样本的数量,我们现在每个句子只有一个。 在这个任务中,我们大大减少了监管信号的数量。 这应该鼓励我们尝试增加小块大小。 让我们尝试 256 而不是 70:

Finished Epoch[ 1 of 8]: [Training] ce = 1.11500325 * 2702; errs = 19.282% * 2702
Finished Epoch[ 2 of 8]: [Training] ce = 0.29961089 * 2677; errs = 6.052% * 2677
Finished Epoch[ 3 of 8]: [Training] ce = 0.09018802 * 2688; errs = 2.418% * 2688
Finished Epoch[ 4 of 8]: [Training] ce = 0.04838102 * 2702; errs = 1.258% * 2702
Finished Epoch[ 5 of 8]: [Training] ce = 0.02996789 * 2697; errs = 0.704% * 2697
Finished Epoch[ 6 of 8]: [Training] ce = 0.02142932 * 2707; errs = 0.517% * 2707
Finished Epoch[ 7 of 8]: [Training] ce = 0.01220149 * 2677; errs = 0.299% * 2677
Finished Epoch[ 8 of 8]: [Training] ce = 0.01312233 * 2686; errs = 0.186% * 2686

此系统学习得更好! 不过, (注意,这种差异可能是由渐变规范化方案引起的 fsAdagrad 项目,在使用较大的数据集时通常会很快消失。)

但是,生成的错误率更高:

Final Results: Minibatch[1-1]: errs = 4.479% * 893; ce = 0.31638223 * 893; perplexity = 1.37215463

但这种差异实际上对应于 3 个错误,这并不重要。

请参阅 此处的解决方案。

任务 5:并行训练

最后,如果有多个 GPU,CNTK 允许使用 MPI (消息传递接口) 并行化训练。 此模型太小,无法预期任何速度:并行化此类小型模型将严重不足可用的 GPU。 然而,让我们浏览动作,以便你知道在转到实际工作负载后如何执行此操作。

请将以下行添加到 SGD 块:

SGD = {
    ...
    parallelTrain = {
        parallelizationMethod = "DataParallelSGD"
        parallelizationStartEpoch = 1
        distributedMBReading = true
        dataParallelSGD = { gradientBits = 2 }
    }
}

然后执行此命令:

mpiexec -np 4 cntk  configFile=SLUHandsOn_Solution4.cntk  stderr=Models/log  parallelTrain=true  command=TrainTagger

这将使用 1 位 GPU 在 4 个 GPU 上运行训练,在本例中,实际上) (2 位) 。 其近似值没有损害准确性:错误率为 4.367%,两个错误更多 (请单独在单个 GPU) 上运行 TestTagger 该操作。

结束语

本教程引入了函数组合样式,作为表示网络的紧凑方式。 许多神经网络类型适合以这种方式表示它们,这是一种更直接且不太容易出错的图形转换为网络说明。

本教程练习采用函数组合样式中的现有配置,并通过特定方式对其进行修改:

  • 从预定义层库中添加层 ()
  • 定义和使用函数
  • 定义和使用层工厂函数

本教程还讨论了如何处理多个时间维度,我们了解了如何并行化训练。

解决方案

下面是上述任务的解决方案。 嘿,没有作弊!

解决方案 1:添加批处理规范化

修改后的模型函数具有以下形式:

    model = Sequential (
        EmbeddingLayer {embDim} :                            # embedding
        BatchNormalizationLayer {} :           ##### added
        RecurrentLSTMLayer {hiddenDim, goBackwards=false} :  # LSTM
        BatchNormalizationLayer {} :           ##### added
        DenseLayer {labelDim}                                # output layer
    )

解决方案 2:添加 Lookahead

你的 lookahead-function 可以定义如下:

    OneWordLookahead (x) = Splice (x : DelayLayer {T=-1} (x))

并将它插入到如下所示的模型中:

    model = Sequential (
        EmbeddingLayer {embDim} :
        OneWordLookahead :                   ##### added
        BatchNormalizationLayer {} :
        RecurrentLSTMLayer {hiddenDim, goBackwards=false} :
        BatchNormalizationLayer {} :
        DenseLayer {labelDim}
    )

解决方案 3:双向循环模型

双向循环层可以编写如下:

    BiRecurrentLSTMLayer {outDim} = {
        F = RecurrentLSTMLayer {outDim, goBackwards=false}
        G = RecurrentLSTMLayer {outDim, goBackwards=true}
        apply (x) = Splice (F(x):G(x))
    }.apply

然后按如下所示使用:

    hiddenDim = 150      ##### changed from 300 to 150

    model = Sequential (
        EmbeddingLayer {embDim} :
        ###OneWordLookahead :                   ##### removed
        BatchNormalizationLayer {} :
        BiRecurrentLSTMLayer {hiddenDim} :
        BatchNormalizationLayer {} :
        DenseLayer {labelDim}
    )

解决方案 4:意向分类

将序列减少到循环层的最后/第一个隐藏层:

        apply (x) = Splice (BS.Sequences.Last(F(x)):BS.Sequences.First(G(x)))
        ##### added Last() and First() calls ^^^

将标签输入从槽更改为意向:

    intentDim = $numIntents$    ###### name change
    ...
        DenseLayer {intentDim}                      ##### different dimension
    ...
    intentLabels = Input {intentDim}
    ...
    ce   = CrossEntropyWithSoftmax (intentLabels, z)
    errs = ErrorPrediction         (intentLabels, z)
    ...
    labelNodes      = (intentLabels)

使用新的动态轴:

    n = DynamicAxis()                               ##### added
    query        = Input {inputDim, dynamicAxis=n}  ##### use dynamic axis

确认

我们希望感谢刘德里克准备本教程的基础。