ZeroNet: 基于NumPy的最小可用神经网络框架

声明

本框架使用了CS231n课程作业中的代码,其使用的协议为MIT。

本框架为阅读之用,可供了解实际的神经网络运行过程。框架速度极慢,仅建议尝试较小的数据集和较小的网络(参考demo)。

安装ZeroNet

现阶段,ZeroNet可由github源码调用。

git clone https://github.com/ddlee96/ZeroNet.git

demo文件夹下建立Jupyter Notebook或者Python脚本,加入如下内容:

import os
import sys
sys.path.insert(0, os.path.join("..\"))

之后,便可以用from zeronet.core.layer import Linear的形式使用。更多细节请参见文档的其他部分。

设计理念

这一节简单描述ZeroNet对数据和功能边界的界定和设计思路。

总体来看,本框架将跟神经网络所涉及的数据分为四种:流数据、习得参数、网络超参、优化器配置。

基于这样的划分,采用了四层抽象来分别处理这些数据:函数(function)-层(layer)-网络(net)-模型(model)。

  • 函数负责完成实际的计算过程,不内聚任何数据;
  • 层负责包装函数,内聚习得参数,初始化并更新这些习得参数;
  • 网络负责处理层与层之间的链接关系(即计算顺序),内聚网络超参(如卷积核大小)、每一层的输入流数据;
  • 模型负责整理和提供整个网络的数据分批次输入,优化器的参数配置,管理整个训练和推断的过程。

流数据

流数据是指传入网络的数据和网络前向计算及反向传播时产生的数据,是实际的信息来源。

流数据最初被模型(model)整理好,按batch送入网络(net),前向计算时,每一层的输入会被临时记录用于后向传播,到达网络的最后一层,损失函数同时计算损失和反向梯度。

其中,损失传回模型(model)用于检测训练状况和模型质量,反向梯度则传回网络,结合临时保存的每层输入计算对习得参数的梯度。

习得参数

习得参数内聚在层(layer)这一抽象层次。其维度由层的类别和网络超参共同决定,在如上一段所述的反向传播过程中,习得参数被按照优化器规则更新。

每一个epoch(若干batch)的训练过程结束后,习得参数被传出到模型(model)进行存档并在交叉验证集上检验其性能。

网络超参

网络超参是指定义网络结构的参数,比如全连接层(Linear)的输出纬度、卷积层(Conv)的卷积核大小、步长(stride)等,还包括网络的连接关系、损失函数的选择(以及正则化系数)。

这一部分参数主要在网络(net)对象的建立时处理。网络(net)对象建立后,需要一个warmup()过程来完成网络中习得参数的初始化。

warmup()只进行前向计算,用于得到每层网络的输入输出维度,进而初始化合适大小的习得参数。

优化器配置

优化器本身并不内置数据,但在训练过程中,常常要对学习率等进行调整。

这一部分工作由模型(model)管理,在训练之初,为优化器初始化配置,并为每个要更新的习得参数保存一份拷贝,并在训练过程中根据训练进度更新这些配置。

函数 Function

function函数位于zeronet.core.function,是完成计算的幕后英雄。

每个函数成对出现,_forward()负责前向计算,_backward()负责后向传播梯度。

_foward()

前向函数完成网络计算中的前向部分,输入是来自上层的输出x,本层参数weights和超参数。

线性层的超参已经蕴含在weights中(即输出的维度),而向卷积层的超参,kernel_size蕴含在weights中,而stridepad则组成一个字典,传入_forward()函数。

_backward()

后向函数完成反向传播的核心部分:接受后一层传来的数据梯度,计算本层数据的梯度、参数的梯度。

与前向函数类似,输入包括后一层传来的数据梯度dout、本层数据输入x、本层参数weights和本层超参。

目前支持的函数列表

  • linear_forward(x, weights)
  • linear_backward(x, weights, dout)
  • conv_forward(x, weights, conv_params)
  • conv_backward(x, weights, conv_params, dout)
  • max_pool_forward(x, pool_param)
  • max_pool_backward(x, pool_param, dout)
  • relu_forward(x)
  • relu_backward(x, dout)
  • sigmoid_forward(x)
  • sigmoid_backward(x, dout)

更多关于这些函数实现的信息请参见源码

层 Layer

Layer类是对网络中单独层的抽象,内聚的数据是习得参数,主要作用是包装function的计算功能,完成当前层习得参数的初始化和更新。

基类layer(name='layer')

初始化时,layer需要一个名字标识name,内建两个字典shape_dictparams,前者用于存储习得参数的维度,后者存储习得参数的值。

warmup(self, warmup_data)

layer类提供warmup()方法来推断习得参数的维度(_infer_shape())并将其初始化(_init_params()),这一过程在模型建立后被调用。

forward(self, input)grad(self, input, dout)

这两个方法分别完成当前层的前向计算和反向传播过程,是对function()的包装。

update(self, grads, optimizer, config)

update()方法是layer类的核心功能,其在完成反向传播后被调用,接收grad()方法得到的梯度、模型传来的优化器函数(optimizer)及其配置,对当前层内聚的习得参数进行一步更新。

目前支持的层

Linear(name="linear", output_shape=256)

Linear层即为全连接层,接收数据维度为(batch_size, X, Y, Z, .....),习得参数为权重w和偏置b

超参:

  • 输出维度output_shape

Conv(name='conv', filter=1, kernel_size=3, stride=1, pad=0)

Conv为卷积层,接收数据维度为(batch_size, channels, height, width),内聚的习得参数为卷积权重w(维度(filters, channels, kernel_size, kernel_size))和偏置b

超参:

  • 过滤器个数filter
  • 卷积核大小kernel_size
  • 步长stride
  • pad长度pad

Pool(name="pool", pool_height=2, pool_width=2, stride=2)

Pool层为极大下采样层,不含习得参数。

超参:

  • 采样高度pool_height
  • 采样宽度pool_width
  • 步长stride

ReLU(name='relu')Sigmoid(name='sigmoid')

ReLUSigmoid为非线性激活层,不含习得参数。

了解更多有关layer的作用,参见设计理念

网络Net

net类是对网络结构的抽象,完成对层的组织和数据流的控制,目前尽支持单方向无分叉的序列结构。

初始化 net(layer_stack=list(), loss_func, reg)

建立net对象需要层的顺序列表(每一项都是layer对象),损失函数loss_func和正则化参数reg

loss_funcfunction模块中定义的函数,目前支持svm_loss(x, y)softmax_loss(x, y)

loss_func同时返回损失和反向梯度,是前向计算的终点和反向传播的起点。

forward(data_batch)

forward()方法完成前向计算部分,递归调用每一层的forward()方法并缓存每一层的输入。

loss(X_batch, y_batch)

loss()方法是对forward()的包装,接收label数据计算损失并返回反向梯度。

训练时调用loss()方法,推断时调用forward()方法。

backward(optimizer, dout)

backward()方法完成反向传播过程,流数据为loss()传过来的反向梯度和forward()过程缓存的每一层输入,调用layer对象的grad()update()方法来计算梯度并更新习得参数。

了解更多有关net的作用,参见设计理念

模型 model

model类封装了训练和推断的逻辑。主要功能是准备数据(划分batch)、初始化网络(根据传入的net对象)和管理训练过程。

初始化 model(net, data, **kargs)

必选参数:

  • net: 定义网络结构的net对象
  • data: 训练或测试用数据,dict对象,包括X_train, X_val, y_train, y_val

可选参数:

  • update_rule:优化器函数
  • optim_config:Dict, 优化器的初始配置
  • lr_decay:Float, 学习率调控
  • batch_size:Int, 批次大小,默认100
  • num_epochs:Int, 训练批次,默认10
  • print_every:Int, 打印频次,默认10
  • verbose:Boolean, 是否显示进度,默认True
  • num_train_sample: Int, 训练用样本数,默认是1000,设置为None则使用传入的全部数据
  • num_val_sample: Int,验证集样本数,默认None,即使用全部验证样本
  • checkpoint_name:存档点路径及名称

warmup()

model对象建立后即可执行,传入一个batch的数据来初始化网络的习得参数。

train()

train方法完成训练过程的逻辑,首先是划分数据的批次,完成模型的一次参数更新,在一个epoch结束后在验证集上检测结果,保存存档点和当前最佳参数。

参数更新用到内部方法_step(),核心逻辑如下:

# foward pass
loss, dout = self.net.loss(X_batch, y_batch)
# backward pass
self.net.backward(self.optimizer, dout)

predict(X)check_accuracy(y_pred, y)

推断时,传入测试数据后,由predict()方法可以得到y_pred,之后连同真实值调用check_accuracy()方法即可得到正确率。

了解更多有关model的作用,参见设计理念

简介

ZeroNet是一个完全基于numpy的纯手打、轻量级、几乎什么功能都没有的神经网络框架。它可以作为你了解深度学习库的简易起点。麻雀虽小,五脏俱全。

特点

  • 基本网络层的numpy朴素实现(全连接、卷积、ReLU等)
  • 网络结构构建类和数据组织
  • 训练逻辑组织

缺点

  • 无GPU支持,朴素实现,效率低下
  • 无计算资源的抽象,资源利用低
  • 目前仅支持单序列的网络结构,不支持RNN等

演示

1.拷贝代码到本地: git clone https://github.com/ddlee96/Zeronet.git

2.安装依赖

需要: Numpy, Jupyter(演示用), Matplotlib(演示用)

(可选) 使用 Pipenv 安装依赖:

# install Pipenv
pip install pipenv

# install dependencies, pipenv will install packages based on Pipfile.lock
cd path/to/zeronet
pipenv install

3.准备数据集(CIFAR-10):bash data/get_cifar.sh

4.演示

Jupyter notebook 打开 demo/demo.ipynb 即可。