koi-boy / CppTemplateMetaProgrammingInAction

《C++模板元编程实战:一个深度学习框架的初步实现》记录。

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

C++模板元编程实战

阅读《C++模板元编程实战:一个深度学习框架的初步实现》的同步代码实现。

直接参考来源:

编码风格与特点:

  • 使用C++20标准,仅头文件(Header-Only),添加./MetaNN到包含目录即可使用。
  • 所有代码包含在命名空间 MetaNN 中。
  • 4空格缩进,大括号换行,类名大驼峰,函数与变量名小驼峰,文件采用全小写下划线连接。
  • 命名:
    • 使用内嵌类型或者常量作为输出的元函数使用下划线_结尾。
    • 而对应直接作为输出结果的元函数,比如变量模板、别名模板则在其基础上去掉末尾_
    • 概念都以C后缀结尾。
    • 策略以P作为前缀,其中的策略模板以Is作为后缀。

运行测试:

cd ./test
make run

目前完成状态:

  • 仅完成了前五章的代码,第四章以前的代码都经过了测试。
  • 第六章基本层的实现,第七章复合层和循环层的实现才是深度学习框架的重点,更不用说还有第八章的求值。
  • 这本书里描述的深度学习框架的基本实现方法已经了解了,但很多地方都并不清楚为什么要这样做。
  • 后三章代码细节很多,但在不是很了解为什么的情况下实现和测试每一个细节会很折磨,所以目前处于搁置状态,仅了解了解原理,不确定以后是否会来实现。

这本书重点与**总结:

  • 异类词典和策略模板作为实用的技巧,在基本层、复合层的实现中大量使用。异类词典用来在一个参数中保存任意数量任意类型的对象。策略模板通过编译期运算用来配置各种各样的选项(主要用在层中),不需要运行时代价。
  • 类型体系的核心**是数据共享,核心数据结构是矩阵,底层都使用std::shared_ptr共享。除了数据共享,另一个核心**是富类型,编译期多态提供的能力,可避免动态多态的运行时开销,通过标签体系而不是继承定义类型的类别,同一个类别都可以进行相关操作。所以可以为其中特殊的类型提供特殊实现,并且最大限度提供类型信息,为最后的求值优化提供可能性。同一个类别中最基本的类型成为主体类型,比如保存所有元素的矩阵是主题类型,而所有元素都相同只保存一个值的平凡矩阵就不是。使用C++20引入的概念替代SFINAE元编程可以很方便的编写类型体系的代码。
  • 表达式模板是模板元编程的一大精华,通过将运算组织称编译期表达式树,运算被声明时不会立即进行而仅仅保存将会进行这样一个计算这种状态,等到显式调用求值接口时才进行计算。即是惰性求值,表达式模板将运算视为对数据的变换,求值时一步到位,避免大量中间状态与计算,并且可以根据特定类型信息进行优化(比如乘以0矩阵直接不做返回0矩阵即可),可以大幅提升计算性能。
  • 通过表达式模板来组织深度学习的模型太原始了,还需要更加高级的实体来组织深度学习的模型,这就是层。基本层设计时需要考虑各种各样的因素:
    • 参数矩阵的初始化:某些层中会有参数矩阵,在每一轮训练中得到更新,需要支持这些参数矩阵的初始化:可能是从文件中加载的上次训练到一半的结果、或者第一次训练前用来填充的随机值或者满足某个特定分布的序列。还要考虑一个同样的层在模型多处被复用,共享同一个参数矩阵的情况。更复杂的框架还需要考虑并行训练时的如何共享和更新的问题(本书中没有)。
    • 正向传播过程:数据从输入流动到输出的过程,有参数矩阵的层需要用到参数矩阵参与运算。
    • 存储中间结果:某些层中为了能够在反向传播时计算梯度,需要能够将计算的中间结果存储下来以便反向传播时使用。这种数据本书中是放在一个栈中,每次正向传播时计算并填充,反向传播时消耗。还需要提供检测,这个数据是否处于不正常状态(中性检测:比如产生了但是没有被使用)。
    • 反向传播:在正向传播完成后,数据从输入端流动到了输出端,一般会有一个层将输出与预先标注的结果(监督学习)进行比较,进行量化(通过损失函数)并向输入端传播。
    • 参数矩阵更新:反向传播过程中,数据(梯度信息)从输出端流动到输入端,这时会进行消耗正向传播过程中存储的中间结果(如果有的话),并进行参数矩阵的更新(如果有并且要)。并且需要考虑反向传播时某些层可能并不想更新参数仅传递梯度信息、或者根本就不传递梯度信息,这些设置都应该纳入设计考虑内。
    • 参数矩阵保存与加载:每次正向传播与反向传播完成一轮,模型中参数就得到了一轮更新。无论是多轮训练完成后还是训练到一半想要中止都需要能够将参数矩阵保存起来,以便下一次能够无损的加载进来,完美复原模型的状态。
    • 训练与预测:一个深度学习模型的典型最终目的就是预测(或者生成,总之就是根据输入得到输出),这时模型已经训练好了,其中的参数已经固定下来,只进行正向传播,反向传播过程就不需要了。需要考虑这种状态下一些不需要保存或者计算的东西(比如中间结果)就可以省略了,以提升运行性能(这个其实是在求值阶段来优化)。
  • 复合层和循环层:
    • 复杂的层是通过简单的层通过组合连接起来的,最终组成的复合层需要能够像基本层那样使用。
    • 复合层可能组合基本层也可能组合复合层。
    • 复合层声明中需要提供的信息:复合层包含的子层,复合层的输入流动到了哪些子层的哪些端口,哪些子层的哪些接口流动到了复合层的输出,子层之间的数据流动。
    • 复合层面临的一个问题:某些子层可能原本并不传递梯度信息,但是组成复合层之后它的前驱却需要反向传播的梯度信息,这时就需要更新它的后继更新自己的状态为传播梯度信息。这需要编译期拓扑排序然后根据排序结果来处理。
    • 为符合层设置策略时可能还需要考虑其子层的独特策略,书中引入为子层引入了嵌套在策略容器中的子层策略(还有什么子层的独特策略覆盖复合层的互斥子层策略之类的路基),相关的策略元函数都需要修改以支持,还要支持深层次的嵌套。
    • 最后,一个基本层有的所有东西都需要适当调整并在复合层上实现。
    • 循环层没了解清楚,略。
  • 求值与优化:
    • 正向传播和反向传播中所说的运算都是构造表达式模板的过程。而求值则是将这些表达式模板对象转换为对应的主体类型的过程。
    • 对于每个表达式模板来说,其与其内部的子表达式模板和最底层的数据构成一个表达式树的结构。但是一个模型中某些中间计算结果会被共享,所以严格来说是一个有向无环图。
    • 为了能够正确求值,就需要对这个图中的表达式模板的求值顺序进行进行组织。
    • MetaNN还通过几种手段对求值过程尽可能优化:避免重复计算、同类合并计算、多运算协同优化。
    • 更多细节略。

About

《C++模板元编程实战:一个深度学习框架的初步实现》记录。

License:MIT License


Languages

Language:C++ 99.1%Language:Makefile 0.9%