diff --git a/STYLE_GUIDE.md b/STYLE_GUIDE.md index 5bcfb5a..a9169e9 100644 --- a/STYLE_GUIDE.md +++ b/STYLE_GUIDE.md @@ -13,6 +13,7 @@ * Linux下安装Pandoc命令:agt-get install pandoc * 使用Pandoc命令:pandoc -s example.tex -o example.md * 使用Pandoc转换后需要注意修改的地方 + * 表格需要手动改 * 图片需要手动改 * 公式部分可能会有不正确,需要注意 * 代码部分需要手动改,样式如下: @@ -45,7 +46,7 @@ * 格式: * svg:自己绘制的图片需要用svg,注意去掉白底 * png:一些常规举例图片不需去除白底的可以使用png - * md里插入图片,大小选择800个像素,label自动生成,例如: + * md里插入图片,大小选择宽度800个像素(可以依据自己生成的网页效果调整大小),label自动生成,例如: ```python ![机器学习系统工作流](../img/ch02/workflow.svg) :width:`800px` @@ -58,5 +59,15 @@ * 两张图不可以较邻近 * 两张图拼一下 * 引用 - * 生成html后可以看到自动label的引用,手动引用(例如,图7.1) + * 图片引用如下 + ```python + ![机器学习系统工作流](../img/ch02/workflow.svg) + :width:`800px` + :label:`img_workflow` + 我们给这个图片打了标签img_workflow,此时对其进行引用如下 + 机器学习系统工作流如 :numref:`img_workflow` 。必须注意的是在引用时冒号前要空有一个字符距离。 + ``` + * 表格或者章节引用和图片引用类似,流程依旧是打上标签,然后用 :numref:‘引用的标签’ + + diff --git a/chapter_computational_graph/components_of_computational_graph.md b/chapter_computational_graph/components_of_computational_graph.md index ce2e7bf..6d61955 100644 --- a/chapter_computational_graph/components_of_computational_graph.md +++ b/chapter_computational_graph/components_of_computational_graph.md @@ -1,6 +1,6 @@ ## 计算图的基本构成 -计算图是用来表示深度学习网络模型在训练与推理过程中计算逻辑与状态的工具。计算框架在后端会将前端语言构建的神经网络模型前向计算与反向梯度计算以计算图的形式来进行表示。计算图由基本数据结构张量(Tensor)和基本运算单元算子(Operator)构成。在计算图中通常使用节点来表示算子,节点间的有向线段来表示张量状态,同时也描述了计算间的依赖关系。如图3.2.1所示,将$\boldsymbol{Z}=relu(\boldsymbol{X}*\boldsymbol{Y})$转化为计算图表示,数据流将根据图中流向与算子进行前向计算和反向梯度计算来更新图中张量状态,以此达到训练模型的目的。 +计算图是用来表示深度学习网络模型在训练与推理过程中计算逻辑与状态的工具。计算框架在后端会将前端语言构建的神经网络模型前向计算与反向梯度计算以计算图的形式来进行表示。计算图由基本数据结构张量(Tensor)和基本运算单元算子(Operator)构成。在计算图中通常使用节点来表示算子,节点间的有向线段来表示张量状态,同时也描述了计算间的依赖关系。如 :numref:`simpledag`所示,将$\boldsymbol{Z}=relu(\boldsymbol{X}*\boldsymbol{Y})$转化为计算图表示,数据流将根据图中流向与算子进行前向计算和反向梯度计算来更新图中张量状态,以此达到训练模型的目的。 ![简单计算图](../img/ch03/simpledag.svg) :width:`300px` @@ -22,12 +22,12 @@ 张量的形状是一个重要的属性,它记录了每个轴的长度,也就是张量每个维度的元素数量。秩则代表张量的轴数或者阶数。张量中通常可以保存布尔类型、浮点数、整型数以及复数和字符串数据。每一个张量都具有唯一的数据类型,在计算过程中会对所有参与运算的张量进行类型检查,当发现类型不匹配时就会报错。部分特殊的计算则必须使用指定的数据类型,比如逻辑运算应为布尔类型。在部分计算框架中张量的属性中包含可以指明张量存储的设备位置,比如存储于CPU、GPU等。张量数据的存储状态可以分为可变和不可变两种,不可变张量一般用于用户初始化的数据或者网络模型输入的数据;而可变张量则存储网络权重参数,根据梯度信息更新自身数据。 -如图3.2.2所示,标量就是一个零阶张量,包含单个数值但没有轴信息。向量即为一阶张量,具有一个轴。二阶张量具有两个轴即秩为二。 +如 :numref:`tensor`,标量就是一个零阶张量,包含单个数值但没有轴信息。向量即为一阶张量,具有一个轴。二阶张量具有两个轴即秩为二。 ![张量](../img/ch03/tensor.svg) :width:`800px` :label:`tensor` -通常我们使用的张量是"整齐"的,每个轴上的具有相同的元素个数,就像一个"矩形"或者"立方体"。在特定的环境中,也会使用特殊类型的张量,比如不规则张量和稀疏张量,如图3.2.3中所示。不规则张量在某个轴上可能具有不同的元素个数,它们支持存储和处理包含非均匀形状的数据,在自然语言处理领域,不规则张量可以存储不同长度文本的信息。稀疏张量则通常应用于图数据与图神经网络中,采用特殊的存储格式如坐标表格式(Coordinate +通常我们使用的张量是"整齐"的,每个轴上的具有相同的元素个数,就像一个"矩形"或者"立方体"。在特定的环境中,也会使用特殊类型的张量,比如不规则张量和稀疏张量,如 :numref:`tensorclass`中所示。不规则张量在某个轴上可能具有不同的元素个数,它们支持存储和处理包含非均匀形状的数据,在自然语言处理领域,不规则张量可以存储不同长度文本的信息。稀疏张量则通常应用于图数据与图神经网络中,采用特殊的存储格式如坐标表格式(Coordinate List, COO),可以高效存储稀疏数据,节省存储空间。 ![张量分类](../img/ch03/tensorclass.svg) @@ -52,7 +52,7 @@ List, COO),可以高效存储稀疏数据,节省存储空间。 :width:`400px` :label:`dependence` -如图3.2.4中所示,在此简单的计算图中,若将$\mathbf{Matmul1}$算子移除则该节点无输出,导致后续的激活函数无法得到输入,从而计算图中的数据流动中断,这表明计算图中的算子间具有依赖关系并且存在传递性。我们对依赖关系进行区分如下: +如 :numref:`dependence`中所示,在此简单的计算图中,若将$\mathbf{Matmul1}$算子移除则该节点无输出,导致后续的激活函数无法得到输入,从而计算图中的数据流动中断,这表明计算图中的算子间具有依赖关系并且存在传递性。我们对依赖关系进行区分如下: - **直接依赖**:节点$\mathbf{ReLU1}$直接依赖于节点$\mathbf{Matmul1}$,即如果节点$\mathbf{ReLU1}$要执行运算,必须接受直接来自节点$\mathbf{Matmul1}$的输出数据; @@ -60,13 +60,13 @@ List, COO),可以高效存储稀疏数据,节省存储空间。 - **相互独立**:在计算图中节点节点$\mathbf{Matmul1}$与节点$\mathbf{Matmul2}$之间并无数据输入输出依赖关系,所以这两个节点间相互独立。 -掌握依赖关系后,分析图3.2.5可以得出节点$\mathbf{Add}$间接依赖于节点$\mathbf{Matmul}$,而节点$\mathbf{Matmul}$直接依赖于节点$\mathbf{Add}$,此时两个节点互相等待对方计算完成输出数据,将无法执行计算任务。若我们手动同时给两个节点赋予输入,计算将持续不间断进行,模型训练将无法停止造成死循环。循环依赖产生正反馈数据流,被传递的数值可能在正方向上无限放大,导致数值上溢,或者负方向上放大导致数值下溢,也可能导致数值无限逼近于0,这些情况都会致使模型训练无法得到预期结果。在构建深度学习模型时,应避免算子间产生循环依赖。 +掌握依赖关系后,分析 :numref:`recurrent`可以得出节点$\mathbf{Add}$间接依赖于节点$\mathbf{Matmul}$,而节点$\mathbf{Matmul}$直接依赖于节点$\mathbf{Add}$,此时两个节点互相等待对方计算完成输出数据,将无法执行计算任务。若我们手动同时给两个节点赋予输入,计算将持续不间断进行,模型训练将无法停止造成死循环。循环依赖产生正反馈数据流,被传递的数值可能在正方向上无限放大,导致数值上溢,或者负方向上放大导致数值下溢,也可能导致数值无限逼近于0,这些情况都会致使模型训练无法得到预期结果。在构建深度学习模型时,应避免算子间产生循环依赖。 ![循环依赖](../img/ch03/recurrent.svg) :width:`300px` :label:`recurrent` -在深度学习计算框架中,表示循环关系通常是以**展开**机制(Unrolling)来实现。当需要实现循环关系时,循环体的计算子图按照迭代次数进行复制,将代表相邻迭代轮次的子图进行串联,相邻迭代轮次的计算子图之间就是直接依赖关系。循环三次的计算图进行展开如图3.2.6。在计算图中,每一个张量和运算符都具有独特的标识符,即使是相同的操作运算,在参与不同计算任务时都具有不同的标识符。区分循环关系和循环依赖的关键在于,是否两个独特标识符之间的运算互相具有直接依赖和相互依赖。循环关系在展开复制计算子图的时候会给复制的所有张量和运算符赋予新的标识符,区分被复制的原始子图,以避免形成循环依赖。 +在深度学习计算框架中,表示循环关系通常是以**展开**机制(Unrolling)来实现。当需要实现循环关系时,循环体的计算子图按照迭代次数进行复制,将代表相邻迭代轮次的子图进行串联,相邻迭代轮次的计算子图之间就是直接依赖关系。循环三次的计算图进行展开如 :numref:`unroll`。在计算图中,每一个张量和运算符都具有独特的标识符,即使是相同的操作运算,在参与不同计算任务时都具有不同的标识符。区分循环关系和循环依赖的关键在于,是否两个独特标识符之间的运算互相具有直接依赖和相互依赖。循环关系在展开复制计算子图的时候会给复制的所有张量和运算符赋予新的标识符,区分被复制的原始子图,以避免形成循环依赖。 ![循环展开](../img/ch03/unroll.svg) :width:`800px` @@ -101,7 +101,7 @@ def control(A, B, C, conditional = True): :width:`600px` :label:`if` -图3.2.7描述上述代码的前向计算图和反向计算图。对于具有if-条件的模型,梯度计算需要知道采用了条件的哪个分支,然后将梯度逻辑应用于该分支。在前向计算图中张量${C}$经过条件控制不参与计算,在反向计算时同样遵守控制流决策,不会计算关于张量$C$的梯度。 + :numref:`if`描述上述代码的前向计算图和反向计算图。对于具有if-条件的模型,梯度计算需要知道采用了条件的哪个分支,然后将梯度逻辑应用于该分支。在前向计算图中张量${C}$经过条件控制不参与计算,在反向计算时同样遵守控制流决策,不会计算关于张量$C$的梯度。 当模型中有循环控制时,循环中的操作可以执行零次或者多次。此时采用展开机制,对每一次操作都赋予独特的运算标识符,以此来区分相同运算操作的多次调用。每一次循环都直接依赖于前一次循环的计算结果,所以在循环控制中需要维护一个张量列表,将循环迭代的中间结果缓存起来,这些中间结果将参与前向计算和梯度计算。下面这段代码描述了简单的循环控制,将其展开得到等价代码后,可以清楚的理解需要维护张量$\boldsymbol{Y_i}$和$\boldsymbol{W_i}$的列表。 ```python @@ -116,7 +116,7 @@ def recurrent_control(X, W, cur_num = 3): Y = matmul(X2, W2) return Y ``` -如图3.2.8描述了上述代码的前向计算图和反向计算图,循环控制的梯度同样也是一个循环,它与前向循环相迭代次数相同,执行循环体的梯度计算。循环体输出的梯度值作为下一次梯度计算的初始值,直至循环结束。 +如 :numref:`while`描述了上述代码的前向计算图和反向计算图,循环控制的梯度同样也是一个循环,它与前向循环相迭代次数相同,执行循环体的梯度计算。循环体输出的梯度值作为下一次梯度计算的初始值,直至循环结束。 ![循环控制计算图](../img/ch03/while.svg) :width:`600px` @@ -169,7 +169,7 @@ grad_W1 = matmul(transpose(Y1), grad_Y2) grad_Y = matmul(grad_Y1, transpose(W)) grad_W = matmul(transpose(Y), grad_Y1) ``` -结合公式、代码以及图3.2.9我们可以看出,在反向传播过程中使用到前向传播的中间变量。因此保存网络中间层输出状态和中间变量,尽管占用了部分内存但能够复用计算结果,达到了提高反向传播计算效率的目的。 +结合公式、代码以及 :numref:`chain`我们可以看出,在反向传播过程中使用到前向传播的中间变量。因此保存网络中间层输出状态和中间变量,尽管占用了部分内存但能够复用计算结果,达到了提高反向传播计算效率的目的。 ![反向传播局部计算图](../img/ch03/chain.svg) :width:`600px` diff --git a/chapter_computational_graph/generation_of_computational_graph.md b/chapter_computational_graph/generation_of_computational_graph.md index 02ce050..513d43f 100644 --- a/chapter_computational_graph/generation_of_computational_graph.md +++ b/chapter_computational_graph/generation_of_computational_graph.md @@ -3,7 +3,7 @@ ### 静态生成 -静态图的生成与执行原理如图3.3.1所示,采用先编译后执行的方式,该模式将计算图的定义和执行进行分离。在静态图模式下使用前端语言定义模型形成完整的程序表达后,并不使用前端语言解释器进行执行,而是将前端描述的完整模型交给计算框架。框架在执行模型计算之前会首先对神经网络模型进行分析,获取网络层之间的连接拓扑关系以及参数变量设置、损失函数等信息,接着用一种特殊的静态数据结构来描述拓扑结构及其他神经网络模型组件,这种特殊的静态数据结构通常被称为静态计算图。静态计算图可以通过优化策略转换成等价的更加高效的结构。当进行模型训练或者推理过程时,静态计算图接收数据并通过相应硬件调度执行图中的算子来完成任务。 +静态图的生成与执行原理如 :numref:`static`所示,采用先编译后执行的方式,该模式将计算图的定义和执行进行分离。在静态图模式下使用前端语言定义模型形成完整的程序表达后,并不使用前端语言解释器进行执行,而是将前端描述的完整模型交给计算框架。框架在执行模型计算之前会首先对神经网络模型进行分析,获取网络层之间的连接拓扑关系以及参数变量设置、损失函数等信息,接着用一种特殊的静态数据结构来描述拓扑结构及其他神经网络模型组件,这种特殊的静态数据结构通常被称为静态计算图。静态计算图可以通过优化策略转换成等价的更加高效的结构。当进行模型训练或者推理过程时,静态计算图接收数据并通过相应硬件调度执行图中的算子来完成任务。 ![静态图生成与执行](../img/ch03/static.svg) :width:`800px` @@ -28,7 +28,7 @@ def model(X, flag): :width:`800px` :label:`staticgen` -经过编译后获取完整的计算图,能够根据全局信息完成图优化策略,进行编译优化形成与模型完全等价的静态图。编译器前端负责完成计算图与硬件无关的转换和优化,比如算子融合将网络中的两个或多个细粒度的算子融合为一个粗粒度算子,比如图3.3.2中将*add*算子与*relu*合并为一个操作,可节省中间计算结果的存储、读取等过程,降低框架底层算子调度的开销,从而提升执行性能和效率。编译器后端负责与硬件相关的计算图优化、代码指令生成和编译,优化手段包括硬件算子选择、内存分配、内存复用等,提高算子执行效率和内存利用效率,降低内存开销。编译器后端因此使用静态图模型运行往往能够获取更好的性能和更少的内存占用。在后续章节中将详细介绍更多编译器前端和编译器后端的优化策略。 +经过编译后获取完整的计算图,能够根据全局信息完成图优化策略,进行编译优化形成与模型完全等价的静态图。编译器前端负责完成计算图与硬件无关的转换和优化,比如算子融合将网络中的两个或多个细粒度的算子融合为一个粗粒度算子,比如 :numref:`staticgen`中将*add*算子与*relu*合并为一个操作,可节省中间计算结果的存储、读取等过程,降低框架底层算子调度的开销,从而提升执行性能和效率。编译器后端负责与硬件相关的计算图优化、代码指令生成和编译,优化手段包括硬件算子选择、内存分配、内存复用等,提高算子执行效率和内存利用效率,降低内存开销。编译器后端因此使用静态图模型运行往往能够获取更好的性能和更少的内存占用。在后续章节中将详细介绍更多编译器前端和编译器后端的优化策略。 优化完成的计算图通过编译器后端根据计算硬件来生成适配的执行代码。在执行阶段,调用执行器接受输入数据,依据计算图调度算子执行训练或者推理任务。在训练任务调度算子执行时,由于在执行阶段已经编译获取模型整体结构,计算框架可以利用自动并行算法制定合理的模型切分与并行策略,进一步提高计算效率。 @@ -38,7 +38,7 @@ def model(X, flag): ### 动态生成 -动态图原理如图3.3.3所示,采用解析式的执行方式,其核心特点是编译与执行同时发生。动态图采用前端语言自身的解释器对代码进行解析,利用计算框架本身的算子分发功能,算子会即刻执行并输出结果。动态图模式采用用户友好的命令式编程范式,使用前端语言构建神经网络模型更加简洁。 +动态图原理如 :numref:`dynamic`所示,采用解析式的执行方式,其核心特点是编译与执行同时发生。动态图采用前端语言自身的解释器对代码进行解析,利用计算框架本身的算子分发功能,算子会即刻执行并输出结果。动态图模式采用用户友好的命令式编程范式,使用前端语言构建神经网络模型更加简洁。 ![动态图原理](../img/ch03/dynamic.svg) :width:`600px` @@ -52,11 +52,11 @@ def model(X, flag): :width:`700px` :label:`dynamicgen` -如图3.3.4中所示,神经网络前向计算按照模型声明定义的顺序进行执行。当模型接收输入数据$\boldsymbol{X}$后,计算框架开始动态生成图拓扑结构,添加输入节点并准备将数据传输给后续节点。模型中存在条件控制时,动态图模式下会即刻得到逻辑判断结果并确定数据流向,因此在图中假设判断结果为真的情况下,图结构中仅会添加关于张量$\boldsymbol{W1}$的*matmul*算子节点。按照代码制定的模型计算顺序与算子依赖关系,计算框架会依次添加*add*算子节点和*ReLU*算子节点。计算框架会在添加节点的同时完成算子分发计算并返回计算结果,同时做好准备向后续添加的节点传输数据。当模型再次进行前向计算时,动态生成的图结构则失效,并再次根据输入和控制条件生成新的图结构。相比于静态生成,可以发现动态生成的图结构并不能完整表示前端语言描述的模型结构,需要即时根据控制条件和数据流向产生图结构。由于计算框架无法通过动态生成获取完整的图结构,因此动态图模式下难以进行图结构优化以提高计算效率。 +如 :numref:`dynamicgen`中所示,神经网络前向计算按照模型声明定义的顺序进行执行。当模型接收输入数据$\boldsymbol{X}$后,计算框架开始动态生成图拓扑结构,添加输入节点并准备将数据传输给后续节点。模型中存在条件控制时,动态图模式下会即刻得到逻辑判断结果并确定数据流向,因此在图中假设判断结果为真的情况下,图结构中仅会添加关于张量$\boldsymbol{W1}$的*matmul*算子节点。按照代码制定的模型计算顺序与算子依赖关系,计算框架会依次添加*add*算子节点和*ReLU*算子节点。计算框架会在添加节点的同时完成算子分发计算并返回计算结果,同时做好准备向后续添加的节点传输数据。当模型再次进行前向计算时,动态生成的图结构则失效,并再次根据输入和控制条件生成新的图结构。相比于静态生成,可以发现动态生成的图结构并不能完整表示前端语言描述的模型结构,需要即时根据控制条件和数据流向产生图结构。由于计算框架无法通过动态生成获取完整的图结构,因此动态图模式下难以进行图结构优化以提高计算效率。 在静态生成环节,由于已经获取完整的神经网络模型定义,因此可以同时构建出完整的前向计算图和反向计算图。而在动态生成中,由于边解析边执行的特性,反向梯度计算的构建随着前向计算调用而进行。在执行前向过程中,计算框架根据前向算子的调用信息,记录对应的反向算子信息以及参与梯度计算的张量信息。前向计算完毕之后,反向算子与张量信息随之完成记录,计算框架会根据前向动态图拓扑结构,将所有反向过程串联起来形成整体反向计算图。最终,将反向图在计算硬件上执行计算得到梯度用于参数更新。 -对应于图3.3.4中,当调用到关于张量$\boldsymbol{W1}$的*matmul*算子节点时,框架会执行两个操作:调用*matmul*算子,计算关于输入$\boldsymbol{X}$和$\boldsymbol{W1}$的乘积结果,同时根据反向计算过程$\boldsymbol{Grad\_W1}=\boldsymbol{Grad\_Y}*\boldsymbol{X}$,记录下需要参与反向计算的算子和张量$\boldsymbol{X}$。计算框架依照算子调度顺序记录参与反向计算的算子和张量。当前向计算执行完毕,计算框架根据动态生成的前向计算图结构拓扑关系,利用记录的反向计算算子和张量动态生成反向计算图,最终完成神经网络模型的梯度计算和参数更新。 +对应于 :numref:`dynamicgen`中,当调用到关于张量$\boldsymbol{W1}$的*matmul*算子节点时,框架会执行两个操作:调用*matmul*算子,计算关于输入$\boldsymbol{X}$和$\boldsymbol{W1}$的乘积结果,同时根据反向计算过程$\boldsymbol{Grad\_W1}=\boldsymbol{Grad\_Y}*\boldsymbol{X}$,记录下需要参与反向计算的算子和张量$\boldsymbol{X}$。计算框架依照算子调度顺序记录参与反向计算的算子和张量。当前向计算执行完毕,计算框架根据动态生成的前向计算图结构拓扑关系,利用记录的反向计算算子和张量动态生成反向计算图,最终完成神经网络模型的梯度计算和参数更新。 尽管动态生成中完整的网络结构在执行前是未知的,不能使用静态图中的图优化技术来提高计算执行性能。但其即刻算子调用与计算的能力,使得模型代码在运行的时候,每执行一句立即进行运算并会返回具体的值,方便开发者在模型构建优化过程中的进行错误分析、结果查看等调试工作,为研究和实验提供了高效的助力。 @@ -75,7 +75,7 @@ def model(X1, X2): ``` 在静态生成过程中,计算框架获取完整的计算图可以分析出计算$\boldsymbol{Y_1}$和$\boldsymbol{Y_2}$的过程相对独立,可以将其进行自动并行计算,加快计算效率。而动态生成的过程中,若无手动配置并行策略,计算框架无法获取图结构不能分析出算子之间的独立性,则只能按照代码顺序执行。模型在输出结果之前执行了*add*和*relu*算子操作,在静态生成过程中利用计算图优化策略中的算子融合方法,可以将这两个算子融合为一个算子执行,这样减少了中间变量$\boldsymbol{Y}$的存储与读取过程,加快了计算效率,减少了内存占用。而动态生成过程则需要按照顺序执行*add*和*relu*两步操作,需要存储变量$\boldsymbol{Y}$。除此之外,由于静态生成能够同时分析重构出前向计算图和反向计算图,可以提前确定反向计算中需要保存的前向中间变量信息。而动态生成则在完成前向计算后才能构建出反向计算图,为了保证反向计算效率需要保存更多的前向计算中间变量信息,相比之下静态生成的过程更加节省内存占用。 -为了方便读者对比,将静态图和动态图特性总结见表3.3.1。 +为了方便读者对比,将静态图和动态图特性总结见 :numref:`cmp_dynamic_static`。 :静态图和动态图对比 @@ -105,7 +105,7 @@ def model(X1, X2): 动态图基于前端语言自身的解释器进行模型代码的解析执行。比如当python作为前端语言,采取原生python边运行边解释的特性,配合框架提供的数据处理/算子分发的功能计算,即可实现动态图的即时执行特性。而且静态图则采用计算框架自带的图编译器,对神经网络模型进行建图后,再调用图结构进行计算。动态图代码与静态图代码之间存在差异,不能直接使用静态图编译器,因此基于源码转换的方法需要将动态图代码转换为静态图代码描述。 -**基于源码转换**的方式则能够改善基于追踪转换的缺陷。如图3.3.5中所示,基于源码转换的流程经历两个阶段。第一个阶段,对动态图模式下的代码扫描进行词法分析,通过词法分析器分析源代码中的所有字符,对代码进行分割并移除空白符、注释等,将所有的单词或字符都转化成符合规范的语法单元列表。接着进行语法分析即解析器,将得到的语法单元列表转换成树形式,并对语法进行检查避免错误。第二阶段,动态图转静态图的核心部分就是对抽象语法树进行转写,计算框架中对每一个需要转换的语法都预设有转换器,每一个转换器对语法树进行扫描改写,将动态图代码语法映射为静态图代码语法。其中最为重要的前端语言控制流,会在这一阶段分析转换为静态图接口进行实现。转写完毕之后,将新的语法树再还原回静态图代码,就可以使用静态生成执行。使用该方式可以避免基于追踪转换中控制流表达缺失的情况。 +**基于源码转换**的方式则能够改善基于追踪转换的缺陷。如 :numref:`ast`中所示,基于源码转换的流程经历两个阶段。第一个阶段,对动态图模式下的代码扫描进行词法分析,通过词法分析器分析源代码中的所有字符,对代码进行分割并移除空白符、注释等,将所有的单词或字符都转化成符合规范的语法单元列表。接着进行语法分析即解析器,将得到的语法单元列表转换成树形式,并对语法进行检查避免错误。第二阶段,动态图转静态图的核心部分就是对抽象语法树进行转写,计算框架中对每一个需要转换的语法都预设有转换器,每一个转换器对语法树进行扫描改写,将动态图代码语法映射为静态图代码语法。其中最为重要的前端语言控制流,会在这一阶段分析转换为静态图接口进行实现。转写完毕之后,将新的语法树再还原回静态图代码,就可以使用静态生成执行。使用该方式可以避免基于追踪转换中控制流表达缺失的情况。 ![基于源码转换流程](../img/ch03/ast.svg) :width:`800px` @@ -129,7 +129,7 @@ def model(X, flag): ``` 代码中模型整体可以采用动态生成,而\@ms\_function可以使用基于源码转换的技术将模块*add_and_relu*的转化为静态图结构。与动态生成中代码执行相同,模型接受输入按照模型定义的计算顺序进行调度执行,并生成临时图结构,当执行语句*Y=add_and_relu(Y,b)* 时,计算框架会自动调用该模块静态生成的图结构执行计算。模块*add_and_relu* 可以利用静态图中的优化技术来提高计算性能,实现动态图和静态图的混合执行。此外,动静态转换的技术常用于模型部署阶段,动态图预测部署时除了需要已经训练完成的参数文件,还须提供最初的模型组网前端代码,这使得动态图部署受到局限性,部署硬件中往往难以提供支持前端语言执行环境。因此当使用动态图模式训练完成模型参数后,可以将整体网络结构转换为静态图格式,将神经网络模型和参数文件进行序列化保存,与前端代码完全解耦,扩大模型部署的硬件支持范围。 -主流的计算框架TensorFlow、MindSpore等中均提供动静态相互转换与融合执行的技术,我们将各框架中支持源码转换和追踪转换技术的接口梳理如下表3.3.2所示。 +主流的计算框架TensorFlow、MindSpore等中均提供动静态相互转换与融合执行的技术,我们将各框架中支持源码转换和追踪转换技术的接口梳理如 :numref:`dynamic_static_switch`所示。 :主流框架动态图转换静态图支持 diff --git a/chapter_computational_graph/schedule_of_computational_graph.md b/chapter_computational_graph/schedule_of_computational_graph.md index d202fd1..d6a0f25 100644 --- a/chapter_computational_graph/schedule_of_computational_graph.md +++ b/chapter_computational_graph/schedule_of_computational_graph.md @@ -8,7 +8,7 @@ 计算图中依赖边和算子构成了一张有向无环图(Directed Acyclic Graph),计算框架后端需要将包含这种依赖关系的算子准确地发送到计算资源,比如GPU、NPU上执行。因此,就要求算子需要按照一定的顺序排列好再发送给GPU/NPU执行。针对有向无环图,我们通常使用拓扑排序来得到一串线性的序列。 -如图3.4.1所示,左边是一张有向无环图。图中包含了a,b,c,d,e五个节点和a-\>d,b-\>c,c-\>d,d-\>e四条边(a-\>d表示d依赖于a,称之为依赖边)。将图的依赖边表达成节点的入度(图论中通常指有向图中某点作为图中边的终点的次数之和),可以得到各个节点的入度信息(a:0, b:0, c:1, d:2, e:1)。拓扑排序就是不断循环将入度为0的节点取出放入队列中,直至所有有向无环图中的节点都加入到队列中,循环结束。例如,第一步将入度为0的a,b节点放入到队列中,此时有向无环图中c,d的入度需要减1,得到新的入度信息(c:0, d:1, e:1)。以此类推,将所有的将所有的节点都放入到队列中并结束排序。 +如 :numref:`schedule`所示,左边是一张有向无环图。图中包含了a,b,c,d,e五个节点和a-\>d,b-\>c,c-\>d,d-\>e四条边(a-\>d表示d依赖于a,称之为依赖边)。将图的依赖边表达成节点的入度(图论中通常指有向图中某点作为图中边的终点的次数之和),可以得到各个节点的入度信息(a:0, b:0, c:1, d:2, e:1)。拓扑排序就是不断循环将入度为0的节点取出放入队列中,直至所有有向无环图中的节点都加入到队列中,循环结束。例如,第一步将入度为0的a,b节点放入到队列中,此时有向无环图中c,d的入度需要减1,得到新的入度信息(c:0, d:1, e:1)。以此类推,将所有的将所有的节点都放入到队列中并结束排序。 ![算子调度执行](../img/ch03/schedule.svg) :width:`700px` @@ -24,7 +24,7 @@ - **并行**:队列中的任务可以同时进行调度执行,加快执行效率。 -首先我们从微观上来分析计算图内部的串行调度。计算图中大多数算子之间存在直接依赖或者间接依赖关系,具有依赖关系的算子间任务调度则必定存在执行前后的时间顺序。如图3.4.2,计算图接受输入数据进行前向计算得到预测值,计算损失函数进行反向梯度计算,整体代码流程后序算子的计算有赖于前序算子的输出。此时算子的执行队列只能以串行的方式进行调度,保证算子都能正确接受到输入数据,才能完成计算图的一次完整执行。 +首先我们从微观上来分析计算图内部的串行调度。计算图中大多数算子之间存在直接依赖或者间接依赖关系,具有依赖关系的算子间任务调度则必定存在执行前后的时间顺序。如 :numref:`order`,计算图接受输入数据进行前向计算得到预测值,计算损失函数进行反向梯度计算,整体代码流程后序算子的计算有赖于前序算子的输出。此时算子的执行队列只能以串行的方式进行调度,保证算子都能正确接受到输入数据,才能完成计算图的一次完整执行。 ![算子的串行](../img/ch03/order.svg) :width:`800px` @@ -32,7 +32,7 @@ 宏观上来看迭代训练之间,每一轮迭代中计算图必须读取训练数据,执行完整的前向计算和反向梯度计算,将图中所有参数值更新完毕后,才能开始下一轮的计算图迭代计算更新。所以"数据载入-数据处理-模型训练"的计算图整体任务调度是以串行方式进行的。 -在分析计算图内部算子依赖关系时,除了直接依赖和间接依赖之外,存在算子间相互独立的情况。如图3.4.3中op1和op2之间相互独立,此时可以将两个算子分配到两个硬件上进行并行计算。对比串行执行,并行计算可以同时利用更多的计算资源来缩短执行时间。 +在分析计算图内部算子依赖关系时,除了直接依赖和间接依赖之外,存在算子间相互独立的情况。如 :numref:`para`中op1和op2之间相互独立,此时可以将两个算子分配到两个硬件上进行并行计算。对比串行执行,并行计算可以同时利用更多的计算资源来缩短执行时间。 ![算子的并行](../img/ch03/para.svg) :width:`800px` @@ -48,18 +48,18 @@ - **异步**:当前任务完成后,不需要等待后续任务的执行情况,可继续执行当前任务下一轮迭代。 -以同步机制来执行计算图训练时,如图3.4.4所示,每一轮迭代中,数据读取后进行数据预处理操作,然后传输给计算图进行训练。每一个环节执行完当前迭代中的任务后,会一直等待后续环节的处理,直至计算图完成一次迭代训练更新参数值后,才会进行下一轮迭代的数据读取、数据处理以及网络训练。当进行数据载入时,数据处理、模型训练处于等待的状态,相反模型处于训练时,数据载入的I/O通道处于空闲,同步机制造成计算资源和通信资源的浪费。 +以同步机制来执行计算图训练时,如 :numref:`synchronization`所示,每一轮迭代中,数据读取后进行数据预处理操作,然后传输给计算图进行训练。每一个环节执行完当前迭代中的任务后,会一直等待后续环节的处理,直至计算图完成一次迭代训练更新参数值后,才会进行下一轮迭代的数据读取、数据处理以及网络训练。当进行数据载入时,数据处理、模型训练处于等待的状态,相反模型处于训练时,数据载入的I/O通道处于空闲,同步机制造成计算资源和通信资源的浪费。 ![同步机制](../img/ch03/synchronization.svg) :width:`800px` :label:`synchronization` -以异步机制来执行计算图训练时,如图3.4.5所示,在迭代训练中,当数据通道将数据读取后交给后续的数据与处理环节后,不需要等待计算图训练迭代完成,直接读取下一批次的数据。对比同步机制,异步机制的引入减少了数据载入、数据预处理、网络训练三个环节的空闲等待时间,能够大幅度缩短循环训练的整体时间,提高任务执行效率。 +以异步机制来执行计算图训练时,如 :numref:`asynchronous`所示,在迭代训练中,当数据通道将数据读取后交给后续的数据与处理环节后,不需要等待计算图训练迭代完成,直接读取下一批次的数据。对比同步机制,异步机制的引入减少了数据载入、数据预处理、网络训练三个环节的空闲等待时间,能够大幅度缩短循环训练的整体时间,提高任务执行效率。 ![异步机制](../img/ch03/asynchronous.svg) :width:`800px` :label:`asynchronous` -当我们将异步机制与并行计算结合在一起,如图3.4.6所示,利用丰富的计算资源可以进一步提高计算图训练效率,缩短训练时间。 +当我们将异步机制与并行计算结合在一起,如 :numref:`asyn_para`所示,利用丰富的计算资源可以进一步提高计算图训练效率,缩短训练时间。 ![异步并行](../img/ch03/asyn_para.svg) :width:`800px` diff --git a/chapter_introduction/components_of_machine_learning_systems.md b/chapter_introduction/components_of_machine_learning_systems.md index a4f2a40..ad47734 100644 --- a/chapter_introduction/components_of_machine_learning_systems.md +++ b/chapter_introduction/components_of_machine_learning_systems.md @@ -1,6 +1,6 @@ ## 机器学习系统基本组成 -为了达到上述设计目标,一个现代的机器学习框架往往具有如图1.3.1所示的基本架构。 +为了达到上述设计目标,一个现代的机器学习框架往往具有如 :numref:`framework_architecture`所示的基本架构。 ![机器学习框架基本构成](../img/ch01/framework_architecture.svg) :width:`600px` diff --git a/chapter_introduction/requirements_for_machine_learning_systems.md b/chapter_introduction/requirements_for_machine_learning_systems.md index 2c80c67..9b93159 100644 --- a/chapter_introduction/requirements_for_machine_learning_systems.md +++ b/chapter_introduction/requirements_for_machine_learning_systems.md @@ -3,7 +3,7 @@ :width:`600px` :label:`framework_position` -为了支持日益增长的机器学习应用,开发者普遍需要一个新型的系统软件:机器学习框架(如图1.2.1所示)。这种框架一方面可以帮助用户进行高效的开发,另一方面使得机器学习模型的训练和部署可以高效使用通用处理器和计算加速器。在设计机器学习框架的过程,人们总结出了以下几个设计需求: +为了支持日益增长的机器学习应用,开发者普遍需要一个新型的系统软件:机器学习框架(如 :numref:`framework_position`所示)。这种框架一方面可以帮助用户进行高效的开发,另一方面使得机器学习模型的训练和部署可以高效使用通用处理器和计算加速器。在设计机器学习框架的过程,人们总结出了以下几个设计需求: - **支持多种神经网络:** 深度学习的崛起使得神经网络成为了大量机器学习应用的核心。不同的应用具有多种数据形态和机器学习的目标,因此人们设计出了大量不同的神经网络,例如,卷积神经网络(Convolutional @@ -31,7 +31,7 @@ 在设计机器学习系统之初,开发者曾尝试通过传统的**神经网络开发库**(如Theano和Caffe)、以及**大数据计算框架**(如Apache Spark和Google -Pregel)等方式来达到以上设计目标。可是他们发现(如表1.2.1所示), +Pregel)等方式来达到以上设计目标。可是他们发现(如 :numref:`comparison_of_ml_frameworks`所示), 神经网络库虽然提供了神经网络开发、自动微分和加速器的支持,但是其缺乏管理和处理大型数据集、模型部署和分布式执行的能力,使得其无法满足产品级机器学习应用的开发。 此外,虽然大数据计算框架具有成熟的分布式执行和数据管理能力,但是其缺乏对神经网络、自动微分和加速器的支持,使得其并不适合开发以神经网络为核心的机器学习应用。因此,业界从头设计出了包括MindSpore、PaddlePaddle、TensorFlow,PyTorch等一系列机器学习框架。 diff --git a/chapter_programming_interface/development_history.md b/chapter_programming_interface/development_history.md index 5d90b37..1b56b12 100644 --- a/chapter_programming_interface/development_history.md +++ b/chapter_programming_interface/development_history.md @@ -4,7 +4,7 @@ :width:`800px` :label:`img_framedh` -随着机器学习系统的诞生,如何设计易用且高性能的编程接口就一直成为了框架设计者首要解决的问题。在早期的机器学习框架中(如图2.1.1所示),人们选择用Lua(Torch)和Python(Theano)等高层次编程语言来编写机器学习程序。这些早期的机器学习框架提供了机器学习必须的模型定义,自动微分等功能,其适用于编写小型和科研为导向的机器学习应用。 +随着机器学习系统的诞生,如何设计易用且高性能的编程接口就一直成为了框架设计者首要解决的问题。在早期的机器学习框架中(如 :numref:`img_framedh`所示),人们选择用Lua(Torch)和Python(Theano)等高层次编程语言来编写机器学习程序。这些早期的机器学习框架提供了机器学习必须的模型定义,自动微分等功能,其适用于编写小型和科研为导向的机器学习应用。 在2011年,深度神经网络快速崛起,并很快在各个AI应用领域(计算机视觉,语音识别,自然语言处理等)取得了最先进的性能。训练深度神经网络需要消耗大量的算力,而这些算力无法被以Lua和Python所主导开发的Torch和Theano所满足。与此同时,计算加速卡(如英伟达GPU)的通用编程接口(例如CUDA C)日趋成熟,而构建于CPU多核技术之上的多线程库(POSIX diff --git a/chapter_programming_interface/ml_workflow.md b/chapter_programming_interface/ml_workflow.md index 9847373..b1d4091 100644 --- a/chapter_programming_interface/ml_workflow.md +++ b/chapter_programming_interface/ml_workflow.md @@ -1,6 +1,6 @@ ## 机器学习工作流 -机器学习系统编程模型的首要设计目标是:对开发者的整个工作流进行完整的编程支持。一个常见的机器学习任务一般包含如图2.2.1所示的流程。这个工作流完成了训练数据集的读取,模型的训练,测试和调试。通过归纳,我们可以将这一工作流中用户所需要自定义的部分通过定义以下API来支持(我们这里假设用户的高层次API以Python函数的形式提供): +机器学习系统编程模型的首要设计目标是:对开发者的整个工作流进行完整的编程支持。一个常见的机器学习任务一般包含如 :numref:`img_workflow`所示的流程。这个工作流完成了训练数据集的读取,模型的训练,测试和调试。通过归纳,我们可以将这一工作流中用户所需要自定义的部分通过定义以下API来支持(我们这里假设用户的高层次API以Python函数的形式提供): - **数据处理:** 首先,用户需要数据处理API来支持将数据集从磁盘读入。进一步,用户需要对读取数据进行数据预处理,从而可以将数据输入后续的机器学习模型中。 diff --git a/chapter_programming_interface/neural_network_layer.md b/chapter_programming_interface/neural_network_layer.md index c2c86f1..d454da5 100644 --- a/chapter_programming_interface/neural_network_layer.md +++ b/chapter_programming_interface/neural_network_layer.md @@ -7,20 +7,20 @@ 神经网络层包含构建机器学习网络结构的基本组件,如计算机视觉领域常用到卷积(Convolution)、池化(Pooling)、全连接(Fully Connected);自然语言处理常用到循环神经网络(Recurrent Neural Network,RNN);为了加速训练,防止过拟合通常用到批标准化(BatchNorm)、Dropout等。 **全连接**是将当前层每个节点都和上一层节点一一连接,本质上是特征空间的线性变换;可以将数据从高维映射到低维,也能从低维映射到高维度。 -图2.3.1展示了全连接的过程,对输入的n个数据变换到另一个大小为m的特征空间,再从大小为m的特征空间变换到大小为p的特征空间;可见全连接层的参数量巨大,两次变换所需的参数大小为$n \times m$和$m \times p$。 + :numref:`fc_layer`展示了全连接的过程,对输入的n个数据变换到另一个大小为m的特征空间,再从大小为m的特征空间变换到大小为p的特征空间;可见全连接层的参数量巨大,两次变换所需的参数大小为$n \times m$和$m \times p$。 ![全连接层](../img/ch02/fc_layer_1.svg) :width:`800px` :label:`fc_layer` **卷积**操作是卷积神经网络中常用的操作之一,卷积相当于对输入进行滑动滤波。根据卷积核(Kernel)、卷积步长(Stride)、填充(Padding)对输入数据从左到右,从上到下进行滑动,每一次滑动操作是矩阵的乘加运算得到的加权值。 -如图2.3.2卷积操作主要由输入、卷积核、输出组成输出又被称为特征图(Feature Map)。 +如 :numref:`conv_comp`卷积操作主要由输入、卷积核、输出组成输出又被称为特征图(Feature Map)。 ![卷积操作的组成](../img/ch02/conv_component.svg) :width:`800px` :label:`conv_comp` -卷积的具体运算过程我们通过图2.3.3进行演示。该图输入为$4 \times 4$的矩阵,卷积核大小为$3 \times 3$,卷积步长为1,不填充,最终得到的$2 \times 2$的输出矩阵。 +卷积的具体运算过程我们通过 :numref:`single_conv`进行演示。该图输入为$4 \times 4$的矩阵,卷积核大小为$3 \times 3$,卷积步长为1,不填充,最终得到的$2 \times 2$的输出矩阵。 计算过程为将$3 \times 3$的卷积核作用到左上角$3 \times 3$大小的输入图上;输出为$1 \times 1 + 2 \times 0 + 2 \times 1 + 3 \times 0 + 2 \times 1 + 3 \times 0 + 4 \times 1 + 1 \times 0 + 3 \times 1 = 12$, 同理对卷积核移动1个步长再次执行相同的计算步骤得到第二个输出为11;当再次移动将出界时结束从左往右,执行从上往下移动1步,再进行从左往右移动;依次操作直到从上往下再移动也出界时,结束整个卷积过程,得到输出结果。我们不难发现相比于全连接,卷积的优势是参数共享(同一个卷积核遍历整个输入图)和参数量小(卷积核大小即是参数量)。 @@ -31,20 +31,20 @@ 在卷积过程中,如果我们需要对输出矩阵大小进行控制,那么就需要对步长和填充进行设置。还是上面的输入图,如需要得到和输入矩阵大小一样的输出矩阵,步长为1时就需要对上下左右均填充一圈全为0的数。 在上述例子中我们介绍了一个输入一个卷积核的卷积操作。通常情况下我们输入的是彩色图片,有三个输入,这三个输入称为通道(Channel),分别代表红、绿、蓝(RGB)。此时我们执行卷积则为多通道卷积,需要三个卷积核分别对RGB三个通道进行上述卷积过程,之后将结果加起来。 -具体如图2.3.4描述了一个输入通道为3,输出通道为1,卷积核大小为$3 \times 3$,卷积步长为1的多通道卷积过程;需要注意的是,每个通道都有各自的卷积核,同一个通道的卷积核参数共享。如果输出通道为$out_c$,输入通道为$in_c$,那么需要$out_c$$\times$$in_c$个卷积核。 +具体如 :numref:`channels_conv`描述了一个输入通道为3,输出通道为1,卷积核大小为$3 \times 3$,卷积步长为1的多通道卷积过程;需要注意的是,每个通道都有各自的卷积核,同一个通道的卷积核参数共享。如果输出通道为$out_c$,输入通道为$in_c$,那么需要$out_c$$\times$$in_c$个卷积核。 ![多通道卷积](../img/ch02/channels_conv.svg) :width:`800px` :label:`channels_conv` **池化**是常见的降维操作,有最大池化和平均池化。池化操作和卷积的执行类似,通过池化核、步长、填充决定输出;最大池化是在池化核区域范围内取最大值,平均池化则是在池化核范围内做平均。与卷积不同的是池化核没有训练参数;池化层的填充方式也有所不同,平均池化填充的是0,最大池化填充的是$-inf$。 -图2.3.5是对$4 \times 4$的输入进行$2 \times 2$区域池化,步长为2,不填充;图左边是最大池化的结果,右边是平均池化的结果。 + :numref:`pooling`是对$4 \times 4$的输入进行$2 \times 2$区域池化,步长为2,不填充;图左边是最大池化的结果,右边是平均池化的结果。 ![池化操作](../img/ch02/pooling.svg) :width:`800px` :label:`pooling` -有了卷积、池化、全连接组件就可以构建一个非常简单的卷积神经网络了,图2.3.6展示了一个卷积神经网络的模型结构。 +有了卷积、池化、全连接组件就可以构建一个非常简单的卷积神经网络了, :numref:`nn_network`展示了一个卷积神经网络的模型结构。 给定输入$3 \times 64 \times 64$的彩色图片,使用16个$3 \times 3$大小的卷积核做卷积,得到大小为$16 \times 64 \times 64$; 再进行池化操作降维,得到大小为$16 \times 32 \times 32$的特征图; 对特征图再卷积得到大小为$32 \times 32 \times 32$特征图,再进行池化操作得到$3 \times 16 \times 16$大小的特征图; @@ -100,7 +100,7 @@ Transformer又是BERT模型架构的重要组成。随着深度神经网络的 如MindSpore提供的mindspore.nn.Cell、mindspore.nn.Conv2d、mindspore.dataset; PyTorch提供的torch.nn.Module、torch.nn.Conv2d、torch.utils.data.Datset。 -图2.3.7描述了神经网络构建过程中的基本细节。 + :numref:`model_build`描述了神经网络构建过程中的基本细节。 神经网络层需要的功能有该层的训练参数(变量,包括初始化方法和训练状态)以及计算过程; 神经网络模型需要的功能是对神经网络层管理和神经网络层参数的管理。 在机器学习编程库中,承担此功能有MindSpore的Cell、PyTorch的Module。 @@ -113,7 +113,7 @@ Cell和Module是模型抽象方法也是所有网络的基类。 :width:`800px` :label:`model_build` -图2.3.8展示了设计神经网络层抽象方法的通用表示。通常在构造器会选择使用Python中collections模块的OrderedDict来初始化神经网络层和神经网络层参数的存储;它的输出是一个有序的,相比与Dict更适合深度学习这种模型堆叠的模式。参数和神经网络层的管理是在\_\_setattr\_\_中实现的,当检测到属性是属于神经网络层及神经网络层参数时就记录起来。神经网络模型比较重要的是计算连接过程,可以在\_\_call\_\_里重载,实现神经网络层时在这里定义计算过程。训练参数的返回接口是为了给优化器传所有训练参数。神经网络层返回为了遍历各层神经网络得到各个神经网络层的参数。这里只列出了一些重要的方法,在自定义方法中,通常需要实现参数插入删除方法、神经网络层插入删除、神经网络模型信息等。 + :numref:`cell_abs`展示了设计神经网络层抽象方法的通用表示。通常在构造器会选择使用Python中collections模块的OrderedDict来初始化神经网络层和神经网络层参数的存储;它的输出是一个有序的,相比与Dict更适合深度学习这种模型堆叠的模式。参数和神经网络层的管理是在\_\_setattr\_\_中实现的,当检测到属性是属于神经网络层及神经网络层参数时就记录起来。神经网络模型比较重要的是计算连接过程,可以在\_\_call\_\_里重载,实现神经网络层时在这里定义计算过程。训练参数的返回接口是为了给优化器传所有训练参数。神经网络层返回为了遍历各层神经网络得到各个神经网络层的参数。这里只列出了一些重要的方法,在自定义方法中,通常需要实现参数插入删除方法、神经网络层插入删除、神经网络模型信息等。 ![神经网络基类抽象方法](../img/ch02/cell_abstract.svg) :width:`800px`