算法的穷人玩法
数学之修,其路遥遥。。。
这是一篇技术散文。
故事要从上次搬家说起。
"你那些破电脑我们要卖掉啦,不然还要叫个车真麻烦",
17年初来广州工作,举家从深圳搬过来那种,当时我已经在广州,搬家的事由太太和爸妈打理,几趟来回之后他们嫌麻烦了,也许是看不惯我老是买电子设备,也许是懒得叫车,就打算把我这些年囤的旧电脑都卖了。
也是,做为程序员的我,这些年囤的电脑确实有点多。
上上次创业失败后也留下了一些电脑在家里,说起创业。。。还是不说了吧,咱是来聊技术的。
家里的电脑关键是平时也用不上,五六年前搞的分布式那套在现在的工作上也用不到,还真有点纠结。
程序员的心理是复杂的,也是简单的,最终还是把我的这些宝贝们拦下来了,理由也想好了,工作上有用。
无奈太太也是这个行业的,一般的理由骗不了,还是得没事把这些家伙组一起玩一玩,比如以搞区块链的理由拼在一起挖矿,久而久之,这理由也不管用了,因为啥也挖不到。
先按下不表。
回到十几年前,高考的那一年,自以为考的不错,结果报的xx大学数学系落榜了,感觉这辈子不能在与数学有关系了。
人生就是这样,当你觉得无缘的时候,十几年过去后,走在创业路上,居然发现有个数学分支和计算机很紧密,慢慢走进,又发现了那些熟悉而又陌生的老朋友, fourier, bias, conv, lr, 我大学的线代和高数可都是满分啊。
其实也很难有机会走近它,没有对应的场景,这时候我已经是一家中小公司的技术总监了,收入也还可以,重新选择行业的代价意味着啥我比谁都了解,我选择了回炉重造,来到阿里,我以一个低的姿态重朔。
让梦想造亮自己,从创业那几年开始,我就有在工作之余有研究新知识到深夜的习惯,有些东西也许是压抑的久了,急于证明它的存在,在太太的支持下,我放弃了很多很合适的机会,区块链,云原生。。。我选择为梦想活一把。
思考
看了很多ml的框架源码, 很佩服这些作者,把资源的使用都吃的紧紧的, 也很佩服这几年的异构发展,很好地解决了矩阵乘法的问题,其实解决思路也很简单,无论是numpy还是tensorflow,思考的点无非都是运算的并行性上。
用CPU计算只能这样串行:
for i in range(A.shape):for j in range(B.shape):for k in range(A.shape): C += A * B
GPU或者TPU的运算可以由多个核同时运算
(ata不会不支持gif吧,还好还好)
在没有GPU的日子里,CPU是很累的,一个人要完成所有的运算,但是GPU的价格有时候让我们忘而却步:
其实有时候也想买,咱毕竟是搞技术的,看到这样的显卡,家里的VIVE也能跑得通了,吃鸡也稳了喂,人的欲望是与口袋成正比的,拿着太太早上放在口袋里的五毛钱,咱们还是扯点正紧的吧。
没有很好的GPU,我有的是这十几年存下来退下来的电脑,能不能让这些资源同时进行一些分布式运算,以进行复杂一点的探索,是我过去几年的一个小目标。
初心是想对ML有一个深入到最原始的理解,并能输出一个实用的分布式ML框架,关于这一点tensorflow已经支持分布式了,caffe在openmp的支持下也能达到这个目标,这几年以来我也用python,golang和c++都实现了这个想法,不算是对自己的挑战,因为有些东西只要写过一次就不再是挑战了,纯粹是为了好玩。
python一定比c慢吗
如果要纯粹追求性能,应该用C或者是C++,我已经写了十几年的C++了,这一点豪无压力,从我上一篇重构比特币代码的文章里大家应该相信这一点
实际上我也写了C++版的,不过它有其他的用途,在后面的文章里可能会出现。
17年初在动手写ML框架的时候,最开始的一版是其实用C实现的,跑的是CPU,以为起码比python要快,直到自己做了个实验。
int matrixdot(){
int *a = malloc(1024*512 * sizeof(int));
int *b = malloc(512*1024 * sizeof(int));
int *c = malloc(1024 * 1024 * sizeof(int));
memset(a, 1, 1024*512*sizeof(int));
memset(b, 2, 512*1024*sizeof(int));
int i, j, k;
for( i = 0; i < 1024; i++){
int ai = 512 * i;
int ci = 1024 * i;
for(j = 0; j < 1024; j++){
int bi = 0;
int cc = 0;
for(k=0; k < 512; k++){
cc += a * b;
bi += 1024;
}
c = cc;
}
}
}
int main(int argc, char** argv){
struct timeval tv1, tv2;
gettimeofday(&tv1,NULL);
matrixdot();
gettimeofday(&tv2,NULL);
long ds = tv2.tv_sec - tv1.tv_sec;
long dus = tv2.tv_usec - tv1.tv_usec;
long ss = ds * 1000 + dus / 1000;
printf(&#34;cost time:%ldms\n&#34;, ss);
exit(0);
}
上面的代码用c写的, 没错O3的复杂度,刚学过编程都能看出来。实现如下逻辑
然后顺便用了numpy做了下对比
import numpy as np
import time
t1= time.time()
a = np.arange(0,1024*512).reshape(1024, 512)
b = np.arange(0, 512 * 1024).reshape(512, 1024)
c = np.dot(a, b)
t2 = time.time()
dt = t2 - t1
dt = round(dt * 1000)
print(&#34;dt:{0}, c.shape{1}&#34;.format(dt, c.shape))
对比运行了后发现
我的乖乖, python比c还要快呀,numpy底层用了blas等库,效果还不错。
妹的,我就不信了,如果numpy使用多线程可能还要高, 于是便用c实现了一个多线程的版本
void matrixdot_single(void *data){
struct thdata* thdata = data;
int i, j, k;
int* a = thdata->a.data;
int* b = thdata->b.data;
int* c = thdata->c;
for( i = thdata->cindex; i < thdata->cend; i++){
int ai = thdata->a.column * i;
int ci = thdata->b.column * i;
for(j = 0; j < thdata->b.column; j++){
int bi = 0;
int cc = 0;
for(k=0; k < thdata->a.column; k++){
cc += a * b;
bi += thdata->b.column;
}
c = cc;
}
}
*thdata->finish = 1;
}
int matrixdot_threads(){
pthread_t* thread;
int *a = malloc(1024*512 * sizeof(int));
int *b = malloc(512*1024 * sizeof(int));
int *c = malloc(1024 * 1024 * sizeof(int));
memset(a, 1, 1024*512*sizeof(int));
memset(b, 2, 512*1024*sizeof(int));
struct thdata* thd = malloc(16 * sizeof(struct thdata));
memset(thd, 0, 16 * sizeof(struct thdata));
int* finish = malloc(16*sizeof(int));
memset(finish, 0, 16*sizeof(int));
int i = 0;
for(i = 0; i < 16; i++){
struct thdata* thdi = thd+i;
thdi->a.data = a;
thdi->a.row = 1024;
thdi->a.column = 512;
thdi->b.data = b;
thdi->b.row = 512;
thdi->b.column = 1024;
thdi->c = c;
thdi->cindex = i * 64;
thdi->cend = (i+1) * 64;
thdi->finish = finish + i;
}
for(i = 0; i < 16; i++){
pthread_create(&thread, NULL, matrixdot_single, thd+i);
}
while(1){
int count = 0;
int* fsh = finish;
for(i = 0; i < 16; i++){
if(!*fsh){
break;
}
fsh++;
count++;
}
if(count == 16){
break;
}
usleep(1000);
}
}
看看效果
约为numpy的一半,找回了点面子。如果加上编译优效果会现出色
又快了一倍。
进而我便想,如果算法需要并行来提效,那go就很合适了。 想到了goroutine. 同样的测试用go再走一遍看看咯。
func main(){
ss := time.Now()
A := make([]int, 1024*512)
B := make([]int, 512*1024)
C := make([]int, 1024*1024)
ch := make(chan int, 32)
for r:= 0; r < 32; r++{
go func(start, end int, a, b, c []int){
for i := start; i < end; i++{
ai := a
ci := c
for j := 0; j < 1024; j++{
bi := 0
cc := 0
for k := 0; k < 512; k++{
cc += ai * b
bi += 1024
}
ci = cc
}
}
ch <- 1
}(r*32, (r+1)*32, A,B,C)
}
count := 0
for count < 32{
select{
case <- ch:
count++
}
}
es := time.Now()
cost := es.Sub(ss)
log.Println(&#34;cost time:&#34;, cost)
}
效果如下
如果不加上编译优化,那就比c还要快了,既使将c的线程数提到32,go的效果还是要比c好,在并发领域线程间的切换开销比routine要大。
用go的另一个原因
分布式做久了,就会理解单机性能与多机情况不太一样,多台机器的复合运算性需要的不仅仅是计算的速度,还需要优越的设计和丰富的分布式脚手架,这一点golang挺适合,而且巧了,我很熟悉,创业时候很多项目是用它实现的,来阿里后开发的社交短链系统就是用golang写的,通过多协程无情压榨CPU实现更高的QPS,在UC浏览器印度拉活的项目中,长期承载了40%的拉活压力,虽然它现在功成身退了,但是我相信会有不少人记住它的。
这个ML架构的case我称之为goml,17年十一和18年五一的时候写完了主要代码架构,为啥是这2个时候,因为这个时候正好在打2个比赛,需要熟悉的框架支持。
写点深入的
即然要说ML框架,那就得说说NEUN,或者CONV/SOFTMAX这些了,这没撤,大家的思路是这样的,固定套路嘛,一看ATA上的某个算法实现,往往前大半篇看到的是WD的模型图或者BERT的介绍,后面一小部分涉及的算法应用,大家也是一堆赞的嘛。
CONV
SOFTMAX
列了一下标题,还是不写了吧,不懂的也应该看不进来, 科普的事情留给别人做去吧。
还是回到ML框架,中间的实现过程直接忽略,时间跨度以年计,中间什么触发了我的思维只有天知道了,实在想不起来了,见谅了。
直接跳到实现后吧
框架运行效果
ml框架写出来了,那就先看看DEMO吧
x_train, y_train, x_test, y_test, err := mnist.LoadMnistData(&#34;&#34;, true)
log.Println(&#34;load data finished&#34;)
if err != nil{
log.Fatal(&#34;load mnist data failed&#34;)
}
x_train.DevideFloat(255)
x_test.DevideFloat(255)
model := model.Sequential([]layers.Layer{
layers.Conv2D(64, []int{5,5}, &#34;relu&#34;, []int{1,1}, &#34;valid&#34;, []int{28, 28, 1}, algo.InitMatrixWithNormal()),
layers.MaxPool2D([]int{2,2}),
layers.Flatten(),
layers.Dense(20, &#34;relu&#34;, algo.InitMatrixWithRandom(-1.0, 1.0)),
layers.Dropout(0.2),
layers.Dense(10, &#34;softmax&#34;, algo.InitMatrixWithRandom(-1.0, 1.0)),
})
model.Compile(&#34;adam&#34;,
&#34;sparse_categorical_crossentropy&#34;,
[]string{&#34;accuracy&#34;})
model.Fit(x_train, y_train,625, 5)
model.Evaluate(x_test, y_test)
别误会,我是在向keras致敬,2个根本不是一个语言(这是我的说辞)。。。
好吧,我承认,API写法上我是借鉴了keras, 因为它太容易让人理解了。
中间没啥难的,在框架设计上取巧了不少,比如将elu, softmax都以层的方式对待,而不像tensorflow或者caffe以功能对待,我提出了multilayer的方式,
func (ml *multiLayer) Forward() error{
if false{
log.Println(&#34;multiLayer Forward&#34;)
}
for _, layer := range ml.layers{
err := layer.Forward()
if err != nil{
return err
}
}
return nil
}
func (ml *multiLayer) Backward() error{
if false{
log.Println(&#34;multiLayer Backward&#34;)
}
for _, layer := range ml.layers_back{
err := layer.Backward()
if err != nil{
return err
}
}
return nil
}
func (ml *multiLayer)Update(args *network.UpdateArgs) error{
if false{
log.Println(&#34;multiLayer Update&#34;)
}
for _, layer := range ml.layers{
err := layer.Update(args)
if err != nil{
return err
}
}
return nil
}
在resnet这些场景下就比较好理解了
其实multilayer还可以嵌套multilayer,甚至无限手套,写错了,无限嵌套。
在数据处理上有借鉴caffe但对data部分做了优化,比如flatten只是分配逻辑而不实际分配内存,在softmax的交差熵处理时将前后的误差内存只分配一次,反正怎么简单怎么来,怎么高效怎么来。
在运算上,有借鉴darknet,但也做了不少优化,就好像对dense的forward时,我将乘法运算分拆到多个协程中,能将耗时优化到原来的1/4(在我的MAC上)
dense forward第一版设计
优化后设计
ch := make(chan int, m)
for i := 0; i < m; i++{
go func(index int){
ai := a
for j := 0; j < n; j++{
wi := b
var v float64
for h := 0; h < k; h++{
v = v + ai * wi
}
c = v + bias
}
ch <- index
}(i)
}
rc := 0
for rc < m{
select{
case <-ch:
rc++
}
}
改过之后,增加了golang多任务的处理方式,虽然难理解了,但性能提高了4倍。
结合下我那闲置的伙伴们
说到这,为了保护我这些宝贝,在太太面前我终于有了一个正当的理由了,直接给她看我是怎么用“高科技”烧这些低配置的玩具。
其实我还设计了一套跨机通信分布式运算方案,这也是go各种脚手架的优势,在设计上我没有用MASTER/WORKER方案,每个机器(WORKER)都可以提交任务,它们有一套协商机制,这是在曾经打的一个比赛中遗留下来的宝贝。篇幅不愿太长,分布式运算留作后面有空再写咯。
CNN
CNN里的图像识别是deep learning的开门课,咱们试下。
架构比较简单了
咱们来训练下。
x_train, y_train, x_test, y_test, err := mnist.LoadMnistData(&#34;&#34;, true)
log.Println(&#34;load data finished&#34;)
if err != nil{
log.Fatal(&#34;load mnist data failed&#34;)
}
x_train.DevideFloat(255)
x_test.DevideFloat(255)
model := model.Sequential([]layers.Layer{
layers.Conv2D(64, []int{5,5}, &#34;relu&#34;, []int{1,1}, &#34;valid&#34;, []int{28, 28, 1}, algo.InitMatrixWithNormal()),
layers.MaxPool2D([]int{2,2}),
layers.Flatten(),
layers.Dense(20, &#34;relu&#34;, algo.InitMatrixWithRandom(-1.0, 1.0)),
layers.Dropout(0.2),
layers.Dense(10, &#34;softmax&#34;, algo.InitMatrixWithRandom(-1.0, 1.0)),
})
model.Compile(&#34;adam&#34;,
&#34;sparse_categorical_crossentropy&#34;,
[]string{&#34;accuracy&#34;})
model.Fit(x_train, y_train,625, 5)
model.Evaluate(x_test, y_test)
用的是lecun的图库:http://yann.lecun.com/exdb/mnist/
为此还要写一个加载图片的方法
func LoadMnistData(path string, gz bool)(tensor.Tensor, tensor.Tensor, tensor.Tensor, tensor.Tensor, error){
log.Println(&#34;start to load mnist data&#34;)
origin_folder := &#34;http://yann.lecun.com/exdb/mnist/&#34;
idx3_train_image_name := &#34;train-images-idx3-ubyte&#34;
idx3_train_label_name := &#34;train-labels-idx1-ubyte&#34;
idx3_test_image_name := &#34;t10k-images-idx3-ubyte&#34;
idx3_test_label_name := &#34;t10k-labels-idx1-ubyte&#34;
//train
train_image_num, train_image_row, train_image_column, pixels, err := tools.ReadIdx3Image(origin_folder, idx3_train_image_name, path, gz)
if err != nil{
return nil, nil, nil, nil, err
}
log.Printf(&#34;load mnist image, num:%d, row:%d, column:%d, pixel length:%d\n&#34;, train_image_num, train_image_row, train_image_column, len(pixels))
train_labels, err := tools.ReadIdx3Label(origin_folder, idx3_train_label_name, path, gz)
if err != nil{
return nil, nil, nil, nil, err
}
log.Printf(&#34;load mnist label,labels length:%d\n&#34;,len(train_labels))
train_image_matrix := &tensor.Matrix{}
train_image_matrix.Init2DImage(train_image_row, train_image_column, 1, train_image_num, pixels)
train_label_matrix := &tensor.Matrix{}
train_label_matrix.InitClassificationLabel(0, 9, len(train_labels), train_labels)
//test
test_image_num, test_image_row, test_image_column, pixels, err := tools.ReadIdx3Image(origin_folder, idx3_test_image_name, path, gz)
if err != nil{
return nil, nil, nil, nil, err
}
test_labels, err := tools.ReadIdx3Label(origin_folder, idx3_test_label_name, path, gz)
if err != nil{
return nil, nil, nil, nil, err
}
test_image_matrix := &tensor.Matrix{}
test_image_matrix.Init2DImage(test_image_row, test_image_column, 1, test_image_num, pixels)
test_label_matrix := &tensor.Matrix{}
test_label_matrix.InitClassificationLabel(0, 9, len(test_labels), test_labels)
log.Printf(&#34;train x size:%d, y size:%d\n&#34;, len(train_image_matrix.Data), len(train_label_matrix.Data))
return train_image_matrix, train_label_matrix, test_image_matrix, test_label_matrix, nil
}
收敛如下:
咱们这个go-keras就算跑成了。
设计思考
不想篇幅太长,咱们这篇就写了算法思考,分布式方法打算放在另外的文章里。
如果要你设计一个算法架构,会先想到什么?
先得有个运算流程
我的想法是从结果出发,称把核心部件实现出来,搭得了模型,跑得通方案呗,从卷积、池化这些开始呀,先把各种层的逻辑流程定义好
type Layer interface{
Init()
InitialData(net *network.Net)
Forward() error
Backward() error
Update(args *network.UpdateArgs) error
BeforeForwardBackwardUpdate() error
AfterForwardBackwardUpdate() error
LinkNext(next Layer) error
}
流程就像沟渠一样,铺设是为了流动,中间流动的数据得定义呀。
数据长啥样
类似于caffe里的blob,我定义了tensor概念(这是有别于tf中的tensor概念)
type Tensor interface{
GetNum()int
DevideFloat(num float64) error
}
但它是一个interface, 负责形状,被matrix继承
type Matrix struct{
Shape TensorShape
MatrixType WhichMatrixType
num int
channel int
step int
viewSize int
spaceSize int
shapeSize int
sampleSize int
stepIndex int
stepOneLine bool
LabelNum int
data []float64
delta []float64
dataint []int
DataRaw []float64
DeltaRaw []float64
DataIntRaw []int
Data []float64
Delta []float64
DataInt []int
IdWords maprune
}
matrix是层与层间传递的实际内容。
基本功-算法实现
算法的核心,逃不开模型的基本要素,类似于caffe, 把基本的运算基建都实现了一遍
在实现的过程中,也发现了一些共同的方法,比如relu, gemm等在不同的层里都有用到,因此抽像了一个算法库
回望全局-从头看到尾
整个过程中需要一些全局的定义来贯穿始终,于是便设计了model来负责全局调配,network负责全局状态,这一点与caffe不同。
type Model struct{
layers []layers.Layer
inputLayer layers.Layer
costLayer layers.Layer
outLayer layers.Layer
train_input tensor.Tensor
train_label tensor.Tensor
test_input tensor.Tensor
test_label tensor.Tensor
batchSize int
epochs int
net *network.Net
optimizer string
loss string
metrics []string
}
type Net struct{
State ModelState
Epochs int
BatchSize int
InputNum int
TestNum int
RunNum int
Step int
Iter int
BatchIndex int
BatchTimes int
CurBatchSize int
ArgsLearning LearningArgs
Policy LearningRatePolicy
}
model的核心函数参考keras, 尽量容易理解。
func Sequential(layers []layers.Layer) *Model
func (model *Model)Compile(optimizer string, loss string, metrics []string)
func (model *Model)Fit(data tensor.Tensor, label tensor.Tensor, batchSize int, epochs int){
model.LayerInit()
model.Train()
}
func (model *Model)Evaluate(data tensor.Tensor, label tensor.Tensor){
model.evaluate()
}
func (model *Model)Predict(data tensor.Tensor)tensor.Tensor{
return model.predict()
}
一些细节
有些设计是借鉴的,但是挺好玩的,比如conv时将图形数据转换为矩阵计算,caffe和darknet里都有im2col的实现,我这里也得实现。
func Im2Col_cpu(data_im []float64, channels int, height int, width int,
ker_height int, ker_width int,
height_stride int, width_stride int,
height_pad int, width_pad int,
height_col int, width_col int,
channels_col int,
data_col []float64){
for c:=0; c < channels_col; c++{
var w_offset int = c % ker_width
var h_offset int = (c/ker_width)%ker_height
var c_im int = (c/ker_width)/ker_height
for h:= 0; h < height_col; h++{
for w:=0; w < width_col; w++{
var im_row int = h_offset + h * height_stride - height_pad
var im_col int = w_offset + w * width_stride - width_pad
var col_index int = (c * height_col + h) * width_col + w
if im_row < 0 || im_col < 0 || im_row >= height || im_col >= width {
data_col = 0
}else{
imindex := im_col + width * (im_row + height * c_im)
data_col = data_im
}
}
}
}
}
再就是gemm(General Matrix Multiplication),
func gemm_nn(m int, n int, k int, alpha float64,
a []float64, lda int,
b []float64, ldb int,
c []float64, ldc int){
for i1:=0; i1 < m; i1++{
for i2:=0; i2 < k; i2++{
a_part := alpha * a
for i3 := 0; i3 < n; i3++{
c = c + a_part * b
}
}
}
}
im2col+gemm将数据先展开再作矩阵乘法,conv的核心计算就搞定了,实际上为了追求性能,我又加上了routine:
for i := 0; i < curbatchsize; i++{
var c []float64 = layer.output.Data
go func(index int){
b := layer.ctldata.Data
algo.Im2Col_cpu(input.Data,
channel, img_height, img_width,
ker_height, ker_width,
height_stride, width_stride,
height_pad, width_pad,
height_col, width_col,
channels_col,
b)
algo.Gemm(false, false, m, n, k, 1, a, k, b, n, 1,c,n)
ch <- index
}(i)
}
性能约提升了2倍左右。
lstm
如图所示,在lstm的设计上内设四层f,i,g,o,内部用dense实现。另外去掉了三个暂存态,cf, ig, th,拿cf来说,ct的误差可以直接向前传递给c(t-1)和zf, 所以就可以直接移除。
这一点我们可以从backward中看出来
func (layer *lstmLayer)Backward()error{
if false{
log.Println(&#34;lstmLayer Backward&#34;)
}
input := layer.input
input_real := layer.inputReal
input_shape := input.Shape
step := input_shape.Step
batch := input_shape.Num
input_channel := input_shape.Channel
input_real_channel := input_real.Shape.Channel
//hide state
memH := layer.memH
//cell state
memC := layer.memC
units := layer.hiddenUnits
ldf := layer.outputF
ldi := layer.outputI
ldg := layer.outputG
ldo := layer.outputO
dth := layer.dataTH
input_real.Step(step-1)
memC.Step(step-1)
memH.Step(step-1)
dth.Step(step-1)
for s := step - 1; s >= 0; {
//forward数据部分
ldfd := ldf.Data
ldid := ldi.Data
ldgd := ldg.Data
ldod := ldo.Data
dthd := dth.Data
//回传误差部分
ldft := ldf.Delta
ldit := ldi.Delta
ldgt := ldg.Delta
ldot := ldo.Delta
dtht := dth.Delta
//cell state
mmct := memC.Delta
//hide state
mmht := memH.Delta
s--
if s >= 0{
memC.Step(s)
memH.Step(s)
dth.Step(s)
}
mmht_prev := memH.Delta
mmct_prev := memC.Delta
mmcd_prev := memC.Data
for i := 0; i < batch; i++{
uints_index := i * units
mht := mmht
tht := dtht
thd := dthd
ctt := mmct
ctt_prev := mmct_prev
ctd_prev := mmcd_prev
//output gate layer output
zod := ldod
zot := ldot
//input data layer output
zgd := ldgd
zgt := ldgt
//input gate layer output
zid := ldid
zit := ldit
//forget gate layer output
zfd := ldfd
zft := ldft
for k := 0; k < units; k++{
mhtk := mht
tht = mhtk * zod
thdk := thd
//cell state delta
cttk := tht * (1 - thdk * thdk)
ctt = cttk
//output delta
zot = mhtk * thdk
//zit input gate delta
//zgt data delta
zit = cttk * zgd
zgt = cttk * zid
if s >= 0{
zft = cttk * ctd_prev
}
//cell state prev delta
ctt_prev = cttk * zfd
}
}
err := layer.layerF.Backward()
if err != nil{
log.Println(&#34;layer f backward error&#34;)
return err
}
err = layer.layerI.Backward()
if err != nil{
log.Println(&#34;layer i backward error&#34;)
return err
}
err = layer.layerG.Backward()
if err != nil{
log.Println(&#34;layer g backward error&#34;)
return err
}
err = layer.layerO.Backward()
if err != nil{
log.Println(&#34;layer o backward error&#34;)
return err
}
if s< 0{
break
}
real_step_delta := input_real.Delta
for i := 0; i < batch; i++{
uints_index := i * units
mht := mmht_prev
real_input_delta := real_step_delta
k2 := input_channel
for j := 0; j < units; j++{
mht +=real_input_delta
k2++
}
}
input_real.Step(s)
}
input_delta := input.DeltaRaw
input_real_delta := input_real.DeltaRaw
for s := 0; s < step; s++{
real_step_delta := input_real_delta
input_step_delta := input_delta
for i := 0; i < batch; i++{
real_delta := real_step_delta
ipt_delta := input_step_delta
for k:= 0; k < input_channel; k++{
ipt_delta = real_delta
}
}
}
return nil
}
复制
代码有些长,但我觉得这里有一些独特的心得,所以原样呈上了。
由于以上四层的设计后,update就非常易于理解了
func (layer *lstmLayer)Update(args *network.UpdateArgs) error{
if false{
log.Println(&#34;lstmLayer Update&#34;)
}
err := layer.layerF.Update(args)
if err != nil{
log.Fatal(&#34;lstm layer update error, layerf update error&#34;)
return err;
}
err = layer.layerI.Update(args)
if err != nil{
log.Fatal(&#34;lstm layer update error, layerf update error&#34;)
return err;
}
err = layer.layerG.Update(args)
if err != nil{
log.Fatal(&#34;lstm layer update error, layerf update error&#34;)
return err;
}
err = layer.layerO.Update(args)
if err != nil{
log.Fatal(&#34;lstm layer update error, layerf update error&#34;)
return err;
}
return nil
}
softmax
在设计逻辑的时候将各功能都层次化,softmax与crossentropy都是独立的layer, 但从实操考虑,也要考虑实际的性能,这里有一个trick, 普通的layer连接代码:
func (layer *layerIntl)LinkNext(next Layer) error{
rn := next.getFirstLayerIfInMulti()
rn.SetInput(layer.GetOutput())
currentType := layer.GetLayerType()
if rn.needInputFlattened() {
if !layer.flattened {
return errors.New(&#34;the prev layer of dropout should be flatten or dense&#34;)
}
}
layer.setNext(rn, rn.GetLayerType())
rn.setPrev(layer.layer, currentType)
return nil
}
可以看到将上一层的output直接被当成下一层的input, 不用另外的空间。
rn.SetInput(layer.GetOutput())
前向传播与后向误差传播时资源复用。
在softmax中的设计不是这样的
func (layer *softmaxLayer)LinkNext(next Layer) error{
err := layer.layerIntl.LinkNext(next)
if err != nil{
return err
}
if next.GetLayerType() == LayerTypeCrossEntropy{
layer.backwardInNext = true
next.bindBackwardWithPrev()
}
return nil
}
if layer.backwardInNext {
layer.output.InitOutputSpaceWithoutDelta(input.Shape, input.DeltaRaw)
}
流程图
可以看出softmax与crossentopy连接里不存在独立的delta, 直接复用前面的,空间节省一半,类似于这样的设计还存在于flatten layer。
最后,咱们来点好玩的吧
写一首唐诗
来个好玩的,我打算用我的框架写一套唐诗,模型逻辑简单如下
model := model.Sequential([]layers.Layer{
layers.Embedding(hotSize, hiddenUnits,algo.InitMatrixWithRandom(-1.0, 1.0)),
layers.Lstm(hiddenUnits),
layers.Lstm(hiddenUnits),
layers.Dense(hotSize, &#34;softmax&#34;, algo.InitMatrixWithRandom(-1.0, 1.0)),
})
结语
多机联动,除了分布式算法,还有很多的场景,当年实现的多人飞机大战。
https://v.youku.com/v_show/id_XMTQ1MTkyOTUxNg==.html?spm=a2h3j.8428770.3416059.1
每次视频里的音乐一响起,我的眼泪就忍不住花花的,那是创业,我的青春。那时候真是过瘾。
机器算是保住了,当年的创业时的回忆偶尔也会在记忆深处泛处点点涟漪。
江湖之远,我们在各自前行着。
页:
[1]