WILL
will是一个神经网(深度学习)的通用工具集。
工程地址
https://github.com/scarsty/will
logo
架构
神经网络-神经层架构,没有显式的神经元。
大脑
大脑里面可以包含数个神经网,并控制数据和网络的调度。
神经网
神经网包含数个神经层。这些神经层有固定的计算和依赖顺序,部分网络结构可能有并列的神经层,它们的计算顺序并不一定依赖。
在连接成神经网的时候,会自动去除连接不上的神经层。
神经层
神经层的算法为:正向激活和反向传播。
其中反向传播之前,需要先更新代价函数。
我们首先约定一套符号(在程序中也使用这些符号):
表示输入层的输入。
表示期望的输出层的输出,即标准答案。
表示每层实际上的输出。
表示偏置。
以上符号,如无特殊说明,小写是列向量,大写是矩阵。在实际的训练过程中,因为批量训练(MiniBatch)的关系,一个列向量会被扩展成含有列的矩阵。但是变换矩阵是不需要扩展的,即如下的事实: 乘法默认指矩阵乘,元素乘(或者叫Hadamard乘)用表示。
若,,,使用对应的大写,则表示将向量扩展为矩阵。需注意的矩阵的每一列都应是完全相同的。
计算顺序
计算顺序为:
正向
以上一层的$A$作为输入更新$X$。如果是输入层,没有这一步。
依据$X$和激活函数更新$A$。
反向
更新$\Delta A$。需要调用下一层的相关函数来更新,因为实际上本层是不知道跟下一层的连接方式的。如果存在多个下层,结果是所有下层返回的和。
- 更新$\Delta X$。依据激活方式来决定。
- 更新参数。通常是$W$和$b$,如果有其他的也会更新。首先会计算$\Delta W$和$\Delta b$,之后依据求解器的设定更新$W$和$b$。
神经层种类
每层首先依据上一层的计算出本层的,之后通过激活函数计算出本层的。因此每层内部的和必定同维。
神经层以连接方式分类,包含以下种类:
- 无连接:没有前一层,仅用于输入层。
- 全连接:每个神经元跟前面一层的所有神经元都有连接。
- 卷积:卷积操作。
- 池化:池化操作。
- 直连:直接读取上一层的数据,一般是浅复制。通常用于连续两次激活,例如批归一化后再激活。
- 合并:有多个上层,或者合并,或者求和来计算本层数据。
- 抽取:提取上一层的一个或者多个通道。
所有层都可以包含激活函数。除了直连层之外,与上层的数据维度通常是不一样的。
所有层都可以有多个下层,这些下层获得的输入是完全一样的。
只有合并层可以有多个上层。
全连接
为权重矩阵,行数为本层的神经元数,列数为上层的神经元数。为偏置向量,行数为本层神经元数。 全连接必须自己处理和,接收的是上一层的,经过线性组合为本层的。
卷积
如果用向量表示图片,那么实际上卷积可以视为一个线性变换,但是变换矩阵应该是一个稀疏阵。 例如将一个28*28的图片变为24*24,卷积核为5*5,那么对应的矩阵为784*576, 其中有25*576个元素是非零,而且大多数都是重复的。
目前我不确定这样做是否合适,但是明显很耗费内存。
卷积将上一层的处理为本层的,并加上偏移,激活后为本层的。
池化
实际上平均值池化可以理解为广义卷积,与上面的讨论类似,也可以用矩阵乘。
池化通常不包含可训练的参数。
激活函数
所有输出和输入维度一致的情况均视为激活函数,而不视为层的种类。因此局域亮度,批归一化均被视为激活函数。
是激活函数,大多数时候用Sigmoid函数,该函数的变量可以为向量或者矩阵,表示对每一个元素进行相同的操作。 除此之外,还有一些其他类型的激活函数。
名称 | 数学形式 |
---|---|
双曲正切 | |
Softmax | |
ReLU | |
Clipped ReLU |
激活函数可以为None,这时一般会做一次浅复制。
附:Softmax反向传播的推导
这里对如何反向推导作一个示范。
从上一层计算得到的是Cost函数对的导数,注意是一个数。
那么已知,以及: 要得到: 这里我们以第一项为例: 将 代入上式,得到: 即: 类似,对于Softmax-Loss 可以得到:
求解器
即如何利用$\Delta W$来更新$W$的方式。
数据类型
浮点数
在Matrix类和所有神经网相关类中,浮点数统一使用real。如果使用模板的话,模板类的规模会过大且难以维护。
real的定义在types.h中,可能为float或者double。
#ifndef _DOUBLE_PRECISION
#define _SINGLE_PRECISION
typedef float real;
#else
typedef double real;
#endif
神经网络中主要使用float,没有必要使用double。大部分民用显卡的double的计算能力都比较差。
因为blas这个函数库对float和double调用的是不同名函数,此处用两个类Cublas和Cblas将其重新包装,因此在实际代码中float和double使用的是同名的函数,没有使用模板。
矩阵类
矩阵类的重要作用是包装CUDA运算。所有矩阵基本运算均置于类内部,不属于基本运算的置于MatrixExtend类。后者为纯静态类。
所有矩阵保存的方式都是列优先,与cublas和cblas保持一致。
两种矩阵
构造函数包含数个原型。矩阵有两种形式,即行列矩阵和四阶张量。其中四阶张量的前3个张量在一起视为矩阵的一列。
矩阵的初值是无法确定的,但是保存在显存中的矩阵初值经常都是0。
矩阵可以自己保管数据(Inside),或者有一些矩阵是不自己保管数据,即数据指针不由自己管理,这种矩阵一般用于浅复制。主要影响的是析构和分享指针函数。
在大部分情况下,矩阵会优先在显卡中创建数据指针。但是需要注意的是在输出矩阵之前,需将数据复制到内存。同样,载入矩阵会先载入到内存,再复制到显存。
寻址
按照行列来寻址。注意矩阵中的元素按照行列顺序列出,与直角坐标系相反!
返回的元素大多数是引用。但是注意,引用的地址如果在显存中,是无法赋值和读取的。
并行
采取数据并行。
显存的占用通常包含以下内容:
- 训练数据,通常会较大。
- 每个minibatch所占用的显存,贯穿整个网络,通常会较大。
- 网络参数占用的显存,通常不会特别大。
并行的情况下,需要一个主网络。每次训练都会将网络中的参数取平均值,该过程由主网络完成。各个分网络之后会各自将这部分数据取回。
DataPreparer
该类用来实时生成训练数据,可以根据实际情况增加对应子类。设置位于[data_preparer]块中。
其他
通用类
以下的类有通用性,不依赖公共头文件types.h,与本工程的耦合度较低。
Timer
一个计时器。
Option
用于从ini中提取选项。同时负责转换字串到types.h中的枚举类型。
类的浮点型返回类型是double,如果实际使用的是float则会包含隐式转换。
Random
模板类,可以生成double和float类型的随机数。
特殊功能
以下类用于实现一些特殊功能。
MatrixFiller
用于填充矩阵,有数种填充方法。
CudaToolkit
包含cudnn和cublas的初始化过程和全局变量。
用户任何时候都不应自行创建该类型的对象!对象的总数必定等于系统中安装的设备数,在首次选中某个设备时会进行一次初始化。
所有矩阵都会有一个该类型的指针。
VectorMath
一些基本计算,函数均是模板函数,大部分inline直接使用。用于CPU计算。
DataGroup
包含$X$,$Y$,$A$。表示一组训练数据或验证数据。该类型主要用于简化代码。
will-cuda
不包含于cublas和cuDNN中计算,需要自行撰写。对应包装也放在MatrixExtend类中。
该文件使用宏简化代码,使用需注意。
cuda_hack
用一些手段将cuda高版本的写法转为低版本的写法,用于一些无法升级软件的特殊硬件(例如TK1)。
使用方法
编译
需自行下载opencv的头文件和库文件,放在include和lib目录中。因为占用空间较大并没有加入版本控制。
默认使用Anaconda的python库,用户可能需要手动修改这里的目录配置。
Visual Studio可以直接生成整个工程。
在Linux下使用以下命令即可。其中CXX可以为g++,clang++,icpc等。
注意需要安装libopencv-dev,libopenblas-dev,以及libopenmp等。其中openblas仅在CPU模式下使用。
即使使用GPU进行训练,仍有少量计算需要使用CPU,故此仍然需要openblas或者mkl等。
export CXX=/usr/bin/clang++
cmake .
make -j
命令行
工程会编译得到libwill.lib,will-cuda.lib等库文件,同时可以得到可执行文件will-windows。在linux下也使用这个名字。
usage: will-windows.exe --config=string [options] ...
options:
--train train a net
--test run test functions
-c, --config config file (ini format) to describe the net structure (string)
-?, --help print this message
在命令行中使用一个ini文件来指定网络使用的参数:
will-windows -c mnist-cv.ini --train
一个ini文件的示例如下(AlexNet):
[will]
TestDataFile =
LoadFile = mnist_cv_save2.txt
SaveFile = mnist_cv_save2.txt
trainDatafile =
ExtraTestDataFile = dig.txt
UseMNIST = 1
LoadNet = 1
Batch = 100
WorkType = 0
test_test = 1
TrainEpoches = 20
OutInterval = 100
Testepoch = 1
LearnRateBase = 0.01
weight_decay = 5e-4
Momentum = 0.9
adjustlr = fixed
gamma = 1e-4
power = 0.75
Testmax = 1
USE_CUDA = 1
solver=ada_delta
[layer_in]
type=null
Node = 784
image_width = 28
image_height = 28
next = layer_cv1
[layer_cv1]
type = conv
channel = 50
weight_width=5
needbias=1
active=none
next=layer_pl1
initweight=xavier
normalized_ddeight = 0
[layer_pl1]
type=pool
window_width=2
active=none
next=layer_cv2
[layer_cv2]
type = conv
channel=50
weight_width=5
needbias=1
active=none
next=layer_pl2
initweight=xavier
normalized_ddeight=0
[layer_pl2]
type=pool
window_width=2
active=none
next=layer_full1
[layer_full1]
type = full_connect
Node = 100
active = relu
next = layer_out
initweight=xavier
[layer_out]
type = full_connect
node = 10
active = softmax
initweight=xavier
公共部分
公共部分均写在[will]块中,指定网络使用的一些公共参数,下半部分是网络的结构。注意在选项设置中,除了next项的值之外,其他部分使用大小写,或者有无下划线都是没有影响的。
如果一些选项的参数是多于一个的,则应使用逗号隔开。例如mp_device=0,1
。
一些重要的参数为:
选项名 | 说明 |
---|---|
train_epoches | 训练的epoch数,默认是20次 |
use_cuda | 是否使用cuda设备 |
mp | 使用cuda设备的个数 |
mp_device | 指定使用的cuda设备 |
learn_rate_base | 基础学习率 |
use_mnist | 使用mnist手写数字库,通常用于测试 |
batch | 每批学习的数据量 |
待补充 |
如果没有特别指定cuda设备,程序会首先查找可用的设备数,并依据计算能力,空余显存,当前温度以及PCI编号的距离选择一个或者多个较空闲的设备。但是这样选择出来的设备并不一定是最合理,用户有特殊需求时应手动指定。
设备的编号与nvidia-smi所显示的顺序一致。程序运行时建议使用GPU-Z或者类似工具来监视设备的状态。例如:
watch -n 1 nvidia-smi
Windows下如果有此需要,推荐使用WPF来编写一个简单的监视功能,其他GUI程序可能会有闪烁现象。以下代码为范例:
namespace gpustat
{
/// <summary>
/// MainWindow.xaml 的交互逻辑
/// </summary>
///
public partial class MainWindow : Window
{
DispatcherTimer timer;
public MainWindow()
{
InitializeComponent();
timer = new DispatcherTimer();
timer.Tick += new EventHandler(timer_Tick);
timer.Interval = new TimeSpan(0, 0, 1);
timer.Start();
}
private void timer_Tick(object sender, EventArgs e)
{
System.Diagnostics.Process pProcess = new System.Diagnostics.Process();
pProcess.StartInfo.FileName = @"C:\Program Files\NVIDIA Corporation\NVSMI\nvidia-smi.exe";
pProcess.StartInfo.UseShellExecute = false;
pProcess.StartInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
pProcess.StartInfo.RedirectStandardOutput = true;
pProcess.StartInfo.CreateNoWindow = true;
pProcess.Start();
string strOutput = pProcess.StandardOutput.ReadToEnd();
pProcess.WaitForExit();
richTextBox.Document.Blocks.Clear();
richTextBox.Document.Blocks.Add(new Paragraph(new Run(strOutput)));
}
}
}
网络设置
所有网络必须以layer_in
层开始,以layer_out
层结束,不能使用别的名字。中间的层可以使用任意名字,但是不能重复。层间的连接使用next
关键字。层的连接方式由type
指定。在初始化网络的时候,会检查所有层是否合理,合理的层必须与输入和输出层都有连接(此处注意连接是单向的)。
注意某些设置是首先读取公共部分为默认值,再读取本层设置的。例如基础学习率,求解器类型等。
一个典型的卷积层设置为:
[layer_conv1]
type = conv
active = relu
window_w = 5
window_h = 5
next = layer_pool1
程序会扫描所有layer
块,并试图将它们连接。在首次连接之后,会从两侧扫描所有层,去掉与输入层和输出层无连接的层,并再次连接。
但是这样并不能保证得到一个正确的网络,使用者在编写的时候应保证网络部分正确。
Python接口
python接口需要预先安装swig,可以使用build.sh生成。注意vs工程中的路径可能与用户实际的配置不同,应手动修改。will.py和动态库(_will.pyd,_will.so)应放在工作目录下。
一个例子如下:
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
import will
import time
import os
def findMax(matrix, t):
v=[0]*2
for i in range(0,2):
v[i]=matrix.getDataValue(i,t)
return v.index(max(v)),max(v)
def toImg(matrix, t, m, n):
img = np.empty((m,n))
for i in range(0,m):
for j in range(0,n):
img[j][i]=matrix.getDataValue(i+j*m,t);
return img
def toImgC(matrix, t, m, n, c):
img = np.empty((m,n,c))
for i in range(0,m):
for j in range(0,n):
for k in range(0,c):
img[j][i][k]=matrix.getDataValue(i+j*m+k*m*n,t);
return img
def getTime(file):
return time.ctime(os.stat(file).st_mtime)
def savename(i):
return 'save%d.txt' % (i%1000)
if 'brain' in dir():
del brain
ini_file = 'mnist-cv.ini'
brain = will.Brain()
brain.load(ini_file)
# here you can set options yourself
brain.init()
brain.run()
以上首先定义了一部分常用的功能(例如绘制矩阵中的数据为图片),下面的运行部分先创建一个网络,并运行。
这里有两种方式创建网络,其一为直接通过ini文件,这时除了训练次数之外,与命令行区别不大。这里是可以载入多个ini文件的,如果有相同的项,后面的会覆盖前面的。所以可以将网络写在一个ini文件,公共部分写在另一个ini文件。或者先载入一个专门用来测试的文件(必须要载入网络参数,load_net=1),再载入一个训练用的公共部分(load_net=0覆盖前面的)。
在载入ini之后,初始化之前,可以使用setOptions命令手动修改一些设置参数。当然也可以不载入option,完全手动设置。
例如设置上面提到的卷积层可以这样写:
brain.setOptions('layer_conv1', 'type=conv; output=784; active = relu; window_w=5; window_h=5; next=layer_pool1')
每个选项间用分号隔开。
若设置较多,分几行也是一样的:
brain.setOptions('layer_conv1', 'type=conv; output=784;')
brain.setOptions('layer_conv1', 'active = relu; window_w=5; window_h=5; next=layer_pool1')
公共部分的第一个参数可以为will,或者省略第一个参数(重载),不能写成空字串。
以下为一个绘制图片的例子:
x_matrix = brain.train_data_cpu_.X()
a_matrix = brain.train_data_cpu_.A()
plt.figure(figsize=(15, 75))
k=0
for j in range(0, x_matrix.getCol()):
c=a_matrix.getDataValue(0,j)
if c>0.5:
img = toImg(x_matrix, j, 28, 28)
ax=plt.subplot(50, 10, k+1)
ax.imshow(img, cmap='gray', norm=plt.Normalize(vmin=0,vmax=1))
ax.text(3, 16, '%d\n%6.4f' % (j+1,c), color='white', family='monospace', fontsize=8)
ax.set_xticks([])
ax.set_yticks([])
k=k+1
if k >= 500:
break;
如果需要绘制的矩阵在GPU中,则应首先复制到CPU。例如:
y_matrix = Y.clone(will.MATRIX_DATA_INSIDE, will.CUDA_CPU)
网络可视化
网络的可视化也使用python来制作,需要安装graphviz,并设置相关的环境变量。
以下例程可以绘制出网络的结构:
from graphviz import Graph
def net_show(net):
connection_types_cl = ['black','green','red','blue','lightblack','orange','pink','lightred']
connection_types = ['none','fc','conv','pool','direct','corr','combine','extract']
activation_types = ['none','sigmoid','relu','tanh','clipped_relu','scale','softmax','softmax','softmax','findmax','dropout','recurrent','softplus','lrn','lcn','dn','bn','st']
layers = []
layers_type = {}
layers_colors = {}
for i in range(0, net.getLayersCount()):
layer = net.getLayer(i)
layer.name = layer.getName()
layer.out_shape = [layer.getOutWidth(), layer.getOutHeight(), layer.getOutChannel()]
layer.next_layers = []
layer.layer_type = connection_types[layer.getConnection()]
layer.layer_color = connection_types_cl[layer.getConnection()]
layer.label = layer.name + ', ' + activation_types[layer.getActiveFunction()+1] + '\n'+str(layer.out_shape)
for j in range(0, layer.getNextLayersCount()):
layer.next_layers.append(layer.getNextLayer(j).getName())
#print layer.name, layer.out_shape,layer.next_layers,layer.layer_type
layers_type[layer.name] = layer.layer_type
layers.append(layer)
g = Graph('G', filename=ini_file+'.gv')
g.attr('node',shape='rect',color='lightblue', fontname='Arial', fontsize='12')
g.body.extend(['rankdir=BU', 'size="20,10"'])
for i in range(0,len(layers)):
layer = layers[i]
g.attr('node',color=layer.layer_color, fontname='Arial')
g.node(layer.name,label= layer.label )
#print layer.name
for i in range(0,len(layers)):
layer = layers[i]
for nl in layer.next_layers:
g.attr('edge', fontname='Arial')
g.edge(layer.name,nl,label = layers_type[nl] )
return g
net = brain.getNet()
net_show(net)
用户可以将这段程序放在jupyter notebook的一个单独的块中,方便调整格式。
cuDNN的补充
cuDNN的文档写的比较差,基本是靠蒙。
这里对常用的功能作一些详细说明。
tensor
tensor(张量)是基本的单位,用来描述一组数据的结构。在cuDNN中,最常用的是4阶张量,即图片本身是2维(W*H),同时有C张提取的特征图片,本次输入的数据组为N。
在本工程中,基本只使用4阶NCHW的方式。这里写的顺序与存储的优先级是相反的(待确定),这个可能是fortran的习惯。
packed表示内存连续,即一行紧接着上一行。如果两行之间还有一些无用数据,则不是packed。大部分算法要求NCHW中最后两个是packed的。
部分API的补充
大部分功能可以查看文档。这里是一些补充说明。
cuDNN中使用$y$和$\Delta y$表示输出量,而大部分文献和本工程中,使用的符号是$a$,请注意区别。
cudnnActivationBackward
该函数使用,,来计算,公式是这样: 即该式包含一个对向量作用一个函数和一个元素乘。从表达式中来看并没有用到,但是一些情况下使用会加快计算的速度。
例如对于Sigmoid函数: 其导数为 可见使用时避免了指数计算,速度应更快。
cudnnSetOpTensorDescriptor
cudnnSetOpTensorDescriptor文档中干脆没写。原型如下:
cudnnStatus_t CUDNNWINAPI cudnnSetOpTensorDescriptor(
cudnnOpTensorDescriptor_t opTensorDesc,
cudnnOpTensorOp_t opTensorOp,
cudnnDataType_t opTensorCompType,
cudnnNanPropagation_t opTensorNanOpt );
熟悉cudnn接口的话,可以很容易理解其中的含义。
一个典型用法是:
cudnnSetOpTensorDescriptor(od, CUDNN_OP_TENSOR_MUL, CUDNN_DATA_DOUBLE, CUDNN_NOT_PROPAGATE_NAN);
该函数可以用来进行元素乘,两个向量(矩阵)相减等DNN中常见的操作。
cuda相关
numBlocks, threadsPerBlock的选取
依据下图:
所以看着办吧。
MatAdd<<<numBlocks, threadsPerBlock>>>(A, B, C);
threadsPerBlock目前最多是1024。
从kepler架构来看没这么简单。先不管了。