• nnet3中的数据类型


    目标与背景

    之前的nnet1nnet2基于Component对象,是一个组件的堆栈。每个组件对应一个神经网络层,为简便起见,将一个仿射变换后接一个非线性表示为一层网络,因此每层网络有两个组件。这些旧组件都有Propagate函数以及Backprop函数,两者都以minibatch为单位进行计算,​​也包含其他函数。

       

    nnet1nnet2还支持非前馈神经网络,但实现不同。

       

    nnet1中,拓扑更为复杂的网络由组件嵌套来表示:ParallelComponent组件内可以包含多个组件序列。此外,在C++底层实现了LSTM组件。

       

    nnet2中,有了时间索引的概念,支持跨时间拼接。这样可以网络内部使用拼帧来实现TDNN

       

    nnet3的目标是保留nnet1nnet2所支持的各种拓扑,还添加更多新的拓扑;并且支持以配置文件的方式表示网络。这样,只需修改编写文件就可以实现一些新的想法,而不需要修改底层代码。

       

    nnet3概要

    nnet1nnet2的组件序列不同,nnet3将组件以图的形式组合在一起。nnet3class Nnet包括:

    1. 支持的组件列表;
    2. 网络图,指示组件的组合方式;

       

    网络图中,用组件的名称来引用组件(这允许某些类型的参数共享)。这样,通过使得时间t的输入取决于时间t-1的输出,就能轻易地实现RNN。使用网络图,还可以处理音频边界问题(比如,RNN不能获取音频第一帧的上文信息,以及最后一帧的下文信息)。

       

    以下是组件和网络图的示例配置文件:

    # First the components

    component name=affine1 type=NaturalGradientAffineComponent input-dim=48 output-dim=65

    component name=relu1 type=RectifiedLinearComponent dim=65

    component name=affine2 type=NaturalGradientAffineComponent input-dim=65 output-dim=115

    component name=logsoftmax type=LogSoftmaxComponent dim=115

    # Next the nodes

    input-node name=input dim=12

    component-node name=affine1_node component=affine1 input=Append(Offset(input, -1), Offset(input, 0), Offset(input, 1), Offset(input, 2))

    component-node name=nonlin1 component=relu1 input=affine1_node

    component-node name=affine2 component=affine2 input=nonlin1

    component-node name=output_nonlin component=logsoftmax input=affine2

    output-node name=output input=output_nonlin

       

    有了输入、请求的输出、网络图与组件,就能构建"计算图"(类ComputationGraph)。计算图是非循环图,其中的节点对应于由特征向量组成的数据矩阵。计算图中的节点对应于网络图中的节点,但还包含了数据的信息:

    n,当前minibatch中的第n条语句

    t,第n条语句中的第t

    x,第t帧的第x维(用于卷积神经网络)

       

    为了对上述信息形式化:

    定义Index为元组(n, t, x)

    定义Cindex为元组(node-index, Index)

    其中node-index是网络中组件结点component-node的索引。编译图时,创建的实际计算表示为以Cindexes为结点的有向无环图

       

    神经网络计算(训练或解码)的流程如下:

    1. 提供ComputationRequest,指示需要输入哪些Cindex(如时间索引)、请求哪些输出;
    2. ComputationRequest与神经网络一起编译为一系列NnetComputation命令;
    3. 为了提高运算效率,编译NnetComputation时,进行了一些优化(可以理解为gcc -O);
    4. NnetComputer负责实际的神经网络计算,输入特征矩阵,根据NnetComputation进行计算,最后得到输出矩阵。可以理解为Python的运行时。

       

    nnet3中基础数据结构

       

    Indexes

    如上所述,Index是一个元组(n, t, x),其中nminibatch中的索引,t是时间索引,x被用于卷积神经网络,通常为零。nnet3的计算是以帧组(chunk)为单位,即一个数据矩阵。其列数为隐层神经元的个数,行数为帧组的大小,并且Index与输入矩阵的行之间有一一对应关系。因此,与始终使用张量的Theano不同,nnet3将多个张量组合为一个矩阵,这样,能够进行BLAS运算优化。

       

    在简单前馈网络的训练中,Index中只有n会变化,因此索引序列为:

    [ (0, 0, 0) (1, 0, 0) (2, 0, 0) ... ]

    在简单前馈网络的解码中,对于单个语句Index中只有对应于矩阵行索引的t会变化,因此索引序列为:

    [ (0, 0, 0) (0, 1, 0) (0, 2, 0) ... ]

    TDNN的训练中,Index中的nt都会变化:

    [ (0, -1, 0) (0, 0, 0) (0, 1, 0) (1, -1, 0) (1, 0, 0) (1, 1, 0) ... ]

    Index结构体有默认的排序运算符:首先按n排序,然后是t,最后是x。所以能得到以上索引序列。当实际打印,索引序列会以压缩形式存在,x会被省略(若为0),并且以紧凑形式表示t的范围,上述索引序列变为:

    [ (0, -1:1) (1, -1:1) ... ]

       

    Cindexes

    Cindex类是一个二元组(int32, Index),其中int32是网络图中结点(component-node)的索引。根据上文所述,一个神经网络由:

    1. 多个组件
    2. 由多个结点构成的图(对应于特定的计算)

    组成

       

    从上文可知,Indexes与矩阵行相对应。Cindex也是如此,除此之外,还指示Cindex位于哪一个矩阵。

    比如,假设网络图中有一个结点:"component-node name=affine1",输出维数为1000,在结点列表中索引为2,那么组件"component name=affine1"的输出——Cindex(2, (0, 0, 0)),对应于列宽为1000的矩阵中的某一行。

       

    ComputationGraph

    ComputationGraph是由Cindex组成的有向图,其中每个Cindex都有一个Cindex依赖列表。对于简单的前馈网络,ComputationGraph的拓扑为线性的结构,并且:

    (nonlin1, (0, 0, 0))Cindex依赖列表为:(affine1, (0, 0, 0))

    (nonlin1, (1, 0, 0))Cindex依赖列表为:(affine1, (1, 0, 0))

    以此类推

    ComputationGraph或其他类中,可能会看到名为cindex_id的整型变量,该变量表示网络图中Cindex的索引。

    ComputationRequest

    ComputationRequest标识一组输入结点和输出结点,每个结点都有一个Indexes关联列表。对于输入节点,它标识了要用于计算的索引;对于输出节点,它标识了哪些索引需要输出。另外,ComputationRequest还包含一些计算配置,如:哪个输出/输入结点提供提供/请求反向传播,以及是否执行模型更新。

       

    例如,ComputationRequest指定名为"input"的输入结点,索引为[(0,-1,0),(0,0,0),(0,1,0)]

    指定名"output"的输出节点,请求索引为[(0,0,0)]

    这表示,输出第0帧,需要输入第-1帧、第0帧以及第1帧。

       

    实际上,以上逐帧的计算示例只会出现在训练时。并且,通常会在minibatch中使用多个语句(样本),因此索引列表的"n"也会有所不同。

       

    神经网络通常可以有多个输入、输出节点;这通常被用于多任务学习或需要多种类型的输入时(例如多视图学习)。

       

    NnetComputation (brief)

    NnetComputationRequest编译后,得到的NnetComputation表示特定的计算指令。NnetComputation由一系列的Commands组成,包括:

    1. Propagate
    2. 矩阵复制;
    3. 矩阵相加;
    4. 矩阵某一行复制到另一个矩阵;
    5. Backprop
    6. 调制矩阵大小;

       

    计算对象是矩阵或子矩阵的列表。NnetComputation还包含各种索引(整数数组等),这些索引有时需要作为特定矩阵运算的参数。

    将在NnetComputation (detail)中进行详述。

       

    NnetComputer

    NnetComputer对象负责实际执行NnetComputation。代码很简单(主要是一个switch语句的循环),因为大部分代码位于NnetComputation的编译与优化。

       

       

    nnet3中的神经网络

    上一节介绍了框架组成。本节将详细介绍神经网络结构、如何将组件组合在一起、如何表示对t-1帧输入的依赖。

       

    Component(基础)

    nnet3中的Component,是带有PropagateBackprop函数的对象。Component还包含一些参数或对固定非线性单元的实现(如Sigmoid组件)。Component接口的重要代码如下:

       

    class Component {

    public:

    virtual void Propagate(const ComponentPrecomputedIndexes *indexes,

    const CuMatrixBase<BaseFloat> &in,

    CuMatrixBase<BaseFloat> *out) const = 0;

    virtual void Backprop(const std::string &debug_info,

    const ComponentPrecomputedIndexes *indexes,

    const CuMatrixBase<BaseFloat> &in_value,

    const CuMatrixBase<BaseFloat> &out_value,

    const CuMatrixBase<BaseFloat> &out_deriv,

    Component *to_update, // may be NULL; may be identical

    // to "this" or different.

    CuMatrixBase<BaseFloat> *in_deriv) const = 0;

    ...

    };

       

    目前。请忽略const ComponentPrecomputedIndexes *indexes参数。

    一个特定的Component拥有输入维度和输出维度,并且以行为单位进行维数转换。也就是说,Propagate()的输入矩阵和输出矩阵的行数相同,每处理输入矩阵中一行,就在输出矩阵中创建一行。也就是说,输入矩阵和输出矩阵的Index是相同的。Backprop函数中保留了类似的逻辑。

       

    Components (properties)

    Component有一个虚函数Properties(),返回类型为enum ComponentProperties

    class Component

    {

    ...

    virtual int32 Properties() const = 0;

    ...

    };

    包含的枚举包括:

    kUpdatableComponent //是否包含可更新的参数

    kPropagateInPlace //其传播函数是否支持就地操作等

    许多优化代码都需要这些代码,以便知道程序适用于哪些优化。你还会注意到一个枚举值kSimpleComponent。如果设置了该枚举,则组件是"简单的",这意味着它按照上面的定义,逐行地进行数据转换。但非简单组件(GeneralCompoent)允许输出矩阵的行数与输出矩阵不同。这样,输出矩阵的Index与输入矩阵Index不同,就需要使用const ComponentPrecomputedIndexes *indexes参数,以显式地指出输入输出使用的Index

       

    假设本文提到的所有组件都是简单组件,因为它们不是实现任何RNNLSTM等所必需的。与nnet2框架不同,组件不负责实现诸如splicing跨帧拼接的操作;相反,我们使用Descriptors来处理,这将在下面解释。

       

    神经网络结点

    之前以及提到,一个nnet3由一些组件以及一个网络图组成。NetworkNode实际上是一个结构体,有以下4种类型:

    enum NodeType { kInput, kDescriptor, kComponent, kDimRange };

    kComponent节点是网络的"meat";

    kDescriptor节点是将kComponent组合在一起的"粘合剂",用于拼帧或循环;

    kInput节点非常简单,指示输入的位置与维数

       

    没有kOutput节点是因为输出节点也是kDescriptor。为简便起见,规定:

    1. kComponent节点必须紧接一个kDescriptor节点;
    2. 后续没有kComponentkDescriptor节点被视为输出节点;

    Nnet类含有用于区分输入输出节点的函数:

    1. IsOutputNode(int32 node_index)
    2. IsComponentInputNode(int32 node_index)

       

    我们将在下面的神经网络节点(详细信息)中更详细地介绍神经网络节点。

       

    神经网络配置文件

    可以从配置文件创建神经网络。以下网络包含一个隐层,并在输入节点处进行拼帧:

    # First the components
    component name=affine1 type=NaturalGradientAffineComponent input-dim=48 output-dim=65
    component name=relu1 type=RectifiedLinearComponent dim=65
    component name=affine2 type=NaturalGradientAffineComponent input-dim=65 output-dim=115
    component name=logsoftmax type=LogSoftmaxComponent dim=115

    # Next the nodes
    input-node name=input dim=12
    component-node name=affine1_node component=affine1 input=Append(Offset(input, -1), Offset(input, 0), Offset(input, 1), Offset(input, 2))
    component-node name=nonlin1 component=relu1 input=affine1_node
    component-node name=affine2 component=affine2 input=nonlin1
    component-node name=output_nonlin component=logsoftmax input=affine2
    output-node name=output input=output_nonlin

    配置文件中不存在描述符,取而代之的,使用"input"作为描述符(比如 Input=Append(...))。配置文件中的每个component-node被展开为两个节点:

    1. kComponent节点
    2. 通过"input"定义的kDescriptor节点

       

    以上配置文件并没有给出dim-range节点的示例。dim-range节点的基本格式为:

    dim-range-node name=dim-range-node1 input-node=affine1_node dim-offset=0 dim=50

    affine1组件的65维中取前50维。

       

    配置文件中的描述符

       

    Descriptor是一种非常受限的表达式,用于引用图中其他结点。描述符相当于"粘合剂",用于将组件连接在一起。描述符负责对组件的输出进行附加操作或求和操作,以便作为后续组件的输入。在本节中,我们从配置文件格式的角度来介绍描述符;下面介绍描述符的语法。

       

    最简单的描述符即是一个结点名本身,例如,"affine1"(只支持kComponentkInput类型的结点)。下面是关于描述符的语法;

    # caution, this is a simplification that overgenerates descriptors.

    <descriptor> ::= <node-name> ;; node name of kInput or kComponent node.

    <descriptor> ::= Append(<descriptor>, <descriptor> [, <descriptor> ... ] )

    <descriptor> ::= Sum(<descriptor>, <descriptor>)

    <descriptor> ::= Const(<value>, <dimension>) ;; e.g. Const(1.0, 512)

    <descriptor> ::= Scale(<scale>, <descriptor>) ;; e.g. Scale(-1.0, tdnn2)

    ;; Failover or IfDefined might be useful for time t=-1 in a RNN, for instance.

    <descriptor> ::= Failover(<descriptor>, <descriptor>) ;; 1st arg if computable, else 2nd

    <descriptor> ::= IfDefined(<descriptor>) ;; the arg if defined, else zero.

    <descriptor> ::= Offset(<descriptor>, <t-offset> [, <x-offset> ] ) ;; offsets are integers

    ;; Switch(...) is intended to be used in clockwork RNNs or similar schemes. It chooses

    ;; one argument based on the value of t (in the requested Index) modulo the number of

    ;; arguments

    <descriptor> ::= Switch(<descriptor>, <descriptor> [, <descriptor> ...])

    ;; For use in clockwork RNNs or similar, Round() rounds the time-index t of the

    ;; requested Index to the next-lowest multiple of the integer <t-modulus>,

    ;; and evaluates the input argument for the resulting Index.

    该描述符用于RNNs,将请求的Index的时间索引t舍入至下一个<t-modulus>的整数倍,并为输出的Index计算input中的参数。

    <descriptor> ::= Round(<descriptor>, <t-modulus>) ;; <t-modulus> is an integer

    ;; ReplaceIndex replaces some <variable-name> (t or x) in the requested Index

    ;; with a fixed integer <value>. E.g. might be useful when incorporating

    ;; iVectors; iVector would always have time-index t=0.

    <descriptor> ::= ReplaceIndex(<descriptor>, <variable-name>, <value>)

       

    以下的内部实际语法与上面的简化版本不同,因为表达式只能出现在特定的层次结构中。该语法也与实际代码中的类名更紧密地对应。读取描述符的代码尝试以尽可能通用的方式标准化它们,以便几乎所有上述语法都可以读取并转换为内部表示。

    ;;; <descriptor> == class Descriptor

    <descriptor> ::= Append(<sum-descriptor>[, <sum-descriptor> ... ] )

    <descriptor> ::= <sum-descriptor> ;; equivalent to Append() with one arg.

    ;;; <sum-descriptor> == class SumDescriptor

    <sum-descriptor> ::= Sum(<sum-descriptor>, <sum-descriptor>)

    <sum-descriptor> ::= Failover(<sum-descriptor>, <sum-descriptor>)

    <sum-descriptor> ::= IfDefined(<sum-descriptor>)

    <sum-descriptor> ::= Const(<value>, <dimension>)

    <sum-descriptor> ::= <fwd-descriptor>

    ;;; <fwd-descriptor> == class ForwardingDescriptor

    ;; <t-offset> and <x-offset> are integers.

    <fwd-descriptor> ::= Offset(<fwd-descriptor>, <t-offset> [, <x-offset> ] )

    <fwd-descriptor> ::= Switch(<fwd-descriptor>, <fwd-descriptor> [, <fwd-descriptor> ...])

    ;; <t-modulus> is an integer

    <fwd-descriptor> ::= Round(<fwd-descriptor>, <t-modulus>)

    ;; <variable-name> is t or x; <value> is an integer

    <fwd-descriptor> ::= ReplaceIndex(<fwd-descriptor>, <variable-name>, <value>)

    ;; <node-name> is the name of a node of type kInput or kComponent.

    <fwd-descriptor> ::= Scale(<scale>, <node-name>)

    <fwd-descriptor> ::= <node-name>

    描述符的设计应该足够严格,以至于得到的表达式将相当容易计算(并生成反向代码)。当描述符与组件相连接时,它们只应该执行资源繁重的操作,而非线性的操作都应该在组件中执行。

    注意:如果有必要对各种未知长度的索引(例如文件中的所有"t"值)进行求和或求平均值,需要在一个Component中执行此操作。

       

    描述符代码

       

    ForwardingDescriptor

    以自底向上的方式介绍Descriptors

    基类ForwardingDescriptor只能处理包含单个Descriptor的表达式:

    Offset(<des>, <t-offset>)

    Switch(<des>, <t-offset>)

    Round(<des>, <t-modulus>)

    ReplaceIndex(<des>, <variable-name>, <value>)

    Scale(<scale>, <node-name>)

    不能处理Append(...)Sum(...)等包含多个Descriptor的表达式。

       

    该接口中最重要的函数为MapToInput()

    class ForwardingDescriptor {

    public:

    virtual Cindex MapToInput(const Index &output) const = 0;

    ...

    }

    用于将Index,转换为对应的Cindex

    比如,将Offset(input, -1)之中的"-1"对应的Index (0, -1, 0)转换为Cindex (input, (0, -1, 0))

       

    ForwardingDescriptor有几个派生类:

    1. SimpleForwardingDescriptor(仅保存节点索引)
    2. OffsetForwardingDescriptor
    3. ReplaceIndexForwardingDescriptor

       

    SumDescriptor

    层次结构中的下一级是类SumDescriptor,用于支持以下表达式:

    Sum(<desc>, <desc>)

    Failover(<desc>, <desc>)

    IfDefined(<desc>)

    显然,调用SumDescriptor::MapToInput可能会返回几个不同的Cindex,因此SumDescriptor无法作为ForwardingDescriptor的接口。因此还需要支持依赖项:

    class SumDescriptor {
    public:
    virtual void GetDependencies(const Index &ind,
    std::vector<Cindex> *dependencies) const = 0;
    ...
    };

    函数GetDependencies将所有可能参与ind的计算的Cindex附加到dependencies中。接下来,函数IsComputable()用于处理某些请求的输入无法计算的情况(例如,输入数据有限或语句边界问题):

    class SumDescriptor {

    public:

    ...

    virtual bool IsComputable(const Index &ind,

    const CindexSet &cindex_set,

    std::vector<Cindex> *input_terms) const = 0;

    ...

    };

    这里,CindexSet为一组Cindex,表示"所有可计算的Cindex的集合"。如果对于该Descriptor,此索引ind是可计算的,则该函数返回true

       

    例如,如果XY是可计算的,那么表达式Sum(X, Y)也是可计算的。如果此函数返回true,则将表达式中可计算的Cindex附加到"input_terms"中。例如,在Failover(X, Y)的表达式中,如果X是可计算的,那么只有X将被附加到"input_terms"。

       

    Descriptor是层次结构的顶级。可以被认为是SumDescriptors的向量,但该向量长度通常为1。该类用于实现Append(...)语法。该类包含以下函数:

    GetDependencies()

    IsComputable()

    SumDescriptor的接口相同

    NumParts()

    Part(int32 n)

    用于访问其向量中的SumDescriptor

       

    神经网络节点(详细)

    根据上文所述,有四种类型的节点,用以下枚举类型定义:

    enum NodeType { kInput, kDescriptor, kComponent, kDimRange };

    实际上,NetworkNode是一个结构体:

    struct NetworkNode {

    NodeType node_type;

    // "descriptor" is relevant only for nodes of type kDescriptor.

    Descriptor descriptor;

    union {

    // For kComponent, the index into Nnet::components_

    int32 component_index;

    // for kDimRange, the node-index of the input node.

    int32 node_index;

    } u;

    // for kInput, the dimension of the input feature. For kDimRange, the dimension

    // of the output (i.e. the length of the range)

    int32 dim;

    // for kDimRange, the dimension of the offset into the input component's feature.

    int32 dim_offset;

    };

       

    kDescriptor节点只需要"descriptor"

    kComponent节点只需要"component_index",作为Nnet中的components_数组的索引

    kDimRange节点只需要"node_index"、"dim"和"dim_offset"

    kInput节点需要"dim"

    神经网络(详细)

    Nnet的私有数据成员有:

    class Nnet {

    public:

    ...

    private:

    std::vector<std::string> component_names_;

    std::vector<Component*> components_;

    std::vector<std::string> node_names_;

    std::vector<NetworkNode> nodes_;

       

    };

    component_names_components_的大小相同;

    node_names_nodes_的大小相同;

    这使得组件名与组件对象、节点名与节点对象相关联。

    注意,我们将自动为kDescriptor节点指定名称:"组件名+_input",这些节点位于类型为kComponent节点之前。kDescriptor节点名不会出现在神经网络配置文件中。

       

    NnetComputation (detail)

    NnetComputation表示神经网络计算的编译版本(可执行版本),其中定义了一些类型,包括如下的枚举类型:

    enum CommandType {

    kAllocMatrixUndefined, kAllocMatrixZeroed,

    kDeallocMatrix, kPropagate, kStoreStats, kBackprop,

    kMatrixCopy, kMatrixAdd, kCopyRows, kAddRows,

    kCopyRowsMulti, kCopyToRowsMulti, kAddRowsMulti, kAddToRowsMulti,

    kAddRowRanges, kNoOperation, kNoOperationMarker };

       

    以下的struct Command代表一个单独的命令及其参数。其中大多数参数都是矩阵索引 以及 组件列表索引。

    struct Command {

    CommandType command_type;

    int32 arg1;

    int32 arg2;

    int32 arg3;

    int32 arg4;

    int32 arg5;

    int32 arg6;

    };

    还定义了一些结构体类型,用于存储矩阵和子矩阵的大小信息。一个子矩阵是行列受限的矩阵,类似于matlab语法:some_matrix1:10,1:20)。

    struct MatrixInfo {

    int32 num_rows;

    int32 num_cols;

    };

    struct SubMatrixInfo {

    int32 matrix_index; // index into "matrices": the underlying matrix.

    int32 row_offset;

    int32 num_rows;

    int32 col_offset;

    int32 num_cols;

    };

       

    结构体NnetComputation包含以下数据成员:

    struct Command {

    ...

    std::vector<Command> commands;

    std::vector<MatrixInfo> matrices;

    std::vector<SubMatrixInfo> submatrices;

    // used in kAddRows, kAddToRows, kCopyRows, kCopyToRows. contains row-indexes.

    std::vector<std::vector<int32> > indexes;

    // used in kAddRowsMulti, kAddToRowsMulti, kCopyRowsMulti, kCopyToRowsMulti.

    // contains pairs (sub-matrix index, row index)- or (-1,-1) meaning don't

    // do anything for this row.

    std::vector<std::vector<std::pair<int32,int32> > > indexes_multi;

    // Indexes used in kAddRowRanges commands, containing pairs (start-index,

    // end-index)

    std::vector<std::vector<std::pair<int32,int32> > > indexes_ranges;

    // Information about where the values and derivatives of inputs and outputs of

    // the neural net live.

    unordered_map<int32, std::pair<int32, int32> > input_output_info;

    bool need_model_derivative;

    // the following is only used in non-simple Components; ignore for now.

    std::vector<ComponentPrecomputedIndexes*> component_precomputed_indexes;

    ...

    };

    其名称带由"indexes"的向量以向量索引作为输入的矩阵函数(如CopyRowsAddRows等)的参数(我们将在执行计算之前将这些向量复制到GPU卡中)。

  • 相关阅读:
    Java垃圾收集算法
    Java回收方法区中回收的类
    Android 聊天软件客户端
    Android 面试
    基于Android的简单聊天工具-服务器端
    Java多线程
    业务序号重排序
    Dubbo异步转同步
    Dubbo加权轮询负载均衡算法应用之推荐产品
    Linux文件句柄数配置
  • 原文地址:https://www.cnblogs.com/JarvanWang/p/10145851.html
Copyright © 2020-2023  润新知