自己动手实现深度学习框架-1 架构设计
前言
随着tensorflow2的发布, 构建一个复杂的深度学习模型变得简单很多。与此同时,更多的原理及细节被隐藏起来,很容易让人知其然而不知其所以然。如果要了解那些隐藏在简单表象下的原理和细节,首先想到的是阅读它的代码。然而这并不是一件容易的事,核心原理相关代码淹没在大量工程优化代码中,让人很难抓住要点。例如tensorflow使用了GPU技术用来加速模型的训练和预测,然而GPU技术属于工程上的优化技术,虽然在实际应用中不可或缺(想象一下你点击一个链接几分钟页面才跳出来吧),但并不涉及深度学习的相关算法和原理。
本人被这个问题困扰许久之后,决定自己写一个轻量级的深度学习框架,通过在小规模数据集上和tensorflow对比,来加深相关原理的理解。
接下来首先给出这个框架的架构设计,然后一步一步实现一个小而美的框架。
核心概念
- 模型: 表示一个执行特定任务的神经网络,是神经网络层的集合。
- 神经网络层: 完成特定任务的神经元集合, 如卷积层用于捕捉数据的空间特征,循环层用于捕捉数据的序列特征。
- 损失函数: 训练模型的算法,它的作用是在训练时量化模型输出和任务目标之间的差距。
- 向前传播: 数据经过从模型的输入层进入,模型的每一层都使用上一层的输出作为输入计算出一个输出,直到最后一层(模型的输出层)输出结果,预测阶段向前传播过程到此结束。如果是训练阶段,最后还要把结果送入损失函数计算误差.
- (误差梯度)反向传播: 在模型的训练阶段, 损失函数计算误差之后, 即开始误差梯度的反向传播过程: 把梯度从模型的输出层送入,层层传递,直到模型的输入层.
- 学习率优化算法: 深度学习模型一般使用随机梯度的方法进行训练,这涉及到学习率的问题,学习率优化算法在模型训练的不同阶段给出合适的学习率,从而加快模型训练速度,提升模型最终的性能。
- 正则化: 修改学习算法的算法,可以在不影响模型训练误差的情况下降低泛化误差,从而提升模型性能。其本质是惩罚与任务目标相悖的行。
整体架构
主要功能
作为一个主要用来学习原理的框架,会不涉及GPU加速和分布式并行运算,这样整个框架的架构就会变得比较简单。框架的主要功能有:
- 定义层的接口, 使层在模型中具有统一的抽象行为.
- 定义层参数的数据结构和命名规范, 使任意层的参数可以被灵活地访问和修改.
- 定义激活函数的接口.
- 定义模型工作做方式, 模型可以以序列的方式把层组装在一起, 实现向前传播和向后传播.
- 定义损失函数的接口.
- 定义优化器接口.
- 维护训练上下文环境,实现模型的训练: 向前传播, 反向传播, 参数更新, 训练过程记录, 上下文环境的保存和加载.
核心类
框架主要包含以下几个抽象类:
- Layer: 神经网络中的层,所有层的实现都是它的子类。
- LayerParam: Layer的参数.
- Activation: 激活函数,所有激活函数实现都是它的子类。
- Model: 模型, 一个或多个层的集合。
- Optimizer: 优化器,定义了优化器的接口. 所有优化器实现都是它的子类。
- Loss: 损失函数, 定义的损失函数的接口。
- Session: 会话,集成了模型及训练模型所需要的所有对象,用于训练,保存,恢复模型和训练状态。
架构图
上图描述了训练模型的过程, 样本数据提交给Session, Session把data送入模型执行向前传播过程, 收到模型的输出后,把输出数据及label送入损失函数计算误差梯度, 然后把梯度反向送入模型执行反向传播过程。待反向传播过程完成,调用优化器执行更新模型的参数。上图中GenOptimizer是广义优化器, Optimizer是学习率专用优化器, 广义优化器指其他所有用来更新参数的算法如: L2正则化优化算法优化.
设计约束
LayerParam
属性:
名称 | 读/写 | 类型 | 说明 |
---|---|---|---|
name | r | string | 参数名称,必须全局唯一 |
value | rw | darray | 参数值 |
gradient | rw | darray | 参数值的梯度 |
udt | rw | int | 记录参数被跟新的次数 |
另外, 优化器可以根据需要添加属性。
name格式: /layerName/layerName/.../paramName. 参数名使用树形结构, layerName是参数所属层的名字, 因此只要保证layerName全局唯一, paramName只需要保证层范围内唯一就能满足要求。
Layer
属性
名称 | 读/写 | 类型 | 说明 |
---|---|---|---|
layer_id | r | int | 层的全局唯一id |
name | r | string | 层的名字, 全局唯一 |
params | r | list | 层的参数列表 |
inshape | r | tuple | 向前传播时输入数据的形状 |
outshape | r | tuple | 向前传播时输出数据的形状 |
activation | r | Activation | 激活函数对象 |
方法
__ init __(self, *outshape, **kargs)
outshape: 指定输出形状, 可以是tuple或in.
kargs: 可选参数
- activation: 激活函数的名字。
- inshape: 输入形状。除了第一个层外, 其他层都不用在这个方法传入inshape。
join(self, pre_layer, *inshape=None)
把当前层和前一个层连接在一起. 使用前一个层的outshape作为当前层的inshape, 如果设置了inshape参数, 则使用inshape参数作为当前层的inshape。如果是第一个层这个方法不会被调用。
- pre_layer: 前一个层。
- inshape: 指定当前层的输入形状。
init_params(self)
初始化参数,由子类实现。当Layer发现inshape和outshape都已经设置了, 才会调用这个方法。
forward(self, in_batch, training=False)
向前传播的方法。由子类实现。
- in_batch: 输入数据.
- training: 是否处于训练状态.
backward(self, gradient)
反向传播方法。由子类实现。
- gradient: 反向传回来的梯度。
reset(self)
重置当前层的状态。由子类实现。
Model
方法
__init __(self, layers=None)
- layers: Layer对象list.
add(self, layer)
向模型中添加一个层.
get_layer(self, index)
使用索引从model中得到一个Layer对象。Model中需要维护一个layer list, index是layer在这个list中的索引。
layer_iterator(self)
得到层的迭代器.
assemble(self)
组装层。检查输入层的inshape参数是否设置。然后调用layer的join方法把layer连接在一起。
predict(self, in_batch, training=False)
向前传播输出模型的预测值。参数的含义和Layer的forward相同。
backward(self, gradient)
反向传播。参数的含义和Layer的forward相同。
reset(self)
重置模型状态。
Activation
方法
__call __(self, in_batch)
激活函数的调用方法,由子类实现。
- in_batch: 向前传播时输入的数据。
grad(self, gradient)
反向传播时计算并返回激活函数的梯度。
Loss
属性
名称 | 读/写 | 类型 | 说明 |
---|---|---|---|
gradient | r | darray | 损失函数的梯度值 |
方法
__call __(self, y_true, y_pred)
损失函数调用方法。
- y_true: 数据的标签值。
- y_pred: 由模型输出的数据的预测值。
Optimizer
方法
__ call__(self, model)
优化器函数的调用方法.
- model: Model对象.
Session
属性
名称 | 读/写 | 类型 | 说明 |
---|---|---|---|
model | r | Model | 训练的目标模型 |
loss | r | Loss | 训练模型使用的损失函数 |
history | r | dict | 训练历史记录数据 |
方法
__init __(self, model, loss, optimizer, genoptimizer=None)
- model: Model对象。
- loss: Loss对象。
- optimizer: optimizer对象。
- genoptimizer: 广义优化器。
batch_train(self, data, label)
分批训练。返回损失值。
- data: 输入数据, darray。
- label: 数据标签, darray。
save(self, fpath)
把模型和训练上下文保存到fpath指定的位置。
load(cls, fpath)
这是一个类方法。从fpath指定位置加载模型和训练上下文,返回已经准备就绪的Session对象。