数学之修,其路遥遥。。。
这是一篇技术散文。
故事要从上次搬家说起。
"你那些破电脑我们要卖掉啦,不然还要叫个车真麻烦",
17年初来广州工作,举家从深圳搬过来那种,当时我已经在广州,搬家的事由太太和爸妈打理,几趟来回之后他们嫌麻烦了,也许是看不惯我老是买电子设备,也许是懒得叫车,就打算把我这些年囤的旧电脑都卖了。
也是,做为程序员的我,这些年囤的电脑确实有点多。
上上次创业失败后也留下了一些电脑在家里,说起创业。。。还是不说了吧,咱是来聊技术的。
家里的电脑关键是平时也用不上,五六年前搞的分布式那套在现在的工作上也用不到,还真有点纠结。
程序员的心理是复杂的,也是简单的,最终还是把我的这些宝贝们拦下来了,理由也想好了,工作上有用。
无奈太太也是这个行业的,一般的理由骗不了,还是得没事把这些家伙组一起玩一玩,比如以搞区块链的理由拼在一起挖矿,久而久之,这理由也不管用了,因为啥也挖不到。
先按下不表。
回到十几年前,高考的那一年,自以为考的不错,结果报的xx大学数学系落榜了,感觉这辈子不能在与数学有关系了。
人生就是这样,当你觉得无缘的时候,十几年过去后,走在创业路上,居然发现有个数学分支和计算机很紧密,慢慢走进,又发现了那些熟悉而又陌生的老朋友, fourier, bias, conv, lr, 我大学的线代和高数可都是满分啊。
其实也很难有机会走近它,没有对应的场景,这时候我已经是一家中小公司的技术总监了,收入也还可以,重新选择行业的代价意味着啥我比谁都了解,我选择了回炉重造,来到阿里,我以一个低的姿态重朔。
让梦想造亮自己,从创业那几年开始,我就有在工作之余有研究新知识到深夜的习惯,有些东西也许是压抑的久了,急于证明它的存在,在太太的支持下,我放弃了很多很合适的机会,区块链,云原生。。。我选择为梦想活一把。
思考
看了很多ml的框架源码, 很佩服这些作者,把资源的使用都吃的紧紧的, 也很佩服这几年的异构发展,很好地解决了矩阵乘法的问题,其实解决思路也很简单,无论是numpy还是tensorflow,思考的点无非都是运算的并行性上。
用CPU计算只能这样串行:
for i in range(A.shape[0]): for j in range(B.shape[1]): for k in range(A.shape[1]): C[i][j] += A[i][k] * B[k][j]
GPU或者TPU的运算可以由多个核同时运算
(ata不会不支持gif吧,还好还好)
在没有GPU的日子里,CPU是很累的,一个人要完成所有的运算,但是GPU的价格有时候让我们忘而却步:
其实有时候也想买,咱毕竟是搞技术的,看到这样的显卡,家里的VIVE也能跑得通了,吃鸡也稳了喂,人的欲望是与口袋成正比的,拿着太太早上放在口袋里的五毛钱,咱们还是扯点正紧的吧。
没有很好的GPU,我有的是这十几年存下来退下来的电脑,能不能让这些资源同时进行一些分布式运算,以进行复杂一点的探索,是我过去几年的一个小目标。
初心是想对ML有一个深入到最原始的理解,并能输出一个实用的分布式ML框架,关于这一点tensorflow已经支持分布式了,caffe在openmp的支持下也能达到这个目标,这几年以来我也用python,golang和c++都实现了这个想法,不算是对自己的挑战,因为有些东西只要写过一次就不再是挑战了,纯粹是为了好玩。
python一定比c慢吗
如果要纯粹追求性能,应该用C或者是C++,我已经写了十几年的C++了,这一点豪无压力,从我上一篇重构比特币代码的文章里大家应该相信这一点[https://www. atatech.org/articles/99 431 ]
实际上我也写了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
[
ai
++
]
*
b
[
bi
];
bi
+=
1024
;
}
c
[
ci
++
]
=
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
(
"cost time:%ldms
\n
"
,
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
(
"dt:{0}, c.shape{1}"
.
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
[
ai
++
]
*
b
[
bi
];
bi
+=
thdata
->
b
.
column
;
}
c
[
ci
++
]
=
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
[
i
*
512
:
]
ci
:
=
c
[
i
*
1024
:
]
for
j
:
=
0
;
j
<
1024
;
j
++
{
bi
:
=
0
cc
:
=
0
for
k
:
=
0
;
k
<
512
;
k
++
{
cc
+=
ai
[
k
]
*
b
[
bi
]
bi
+=
1024
}
ci
[
j
]
=
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
(
"cost time:"
,
cost
)
}
效果如下
如果不加上编译优化,那就比c还要快了,既使将c的线程数提到32,go的效果还是要比c好,在并发领域线程间的切换开销比routine要大。
用go的另一个原因
分布式做久了,就会理解单机性能与多机情况不太一样,多台机器的复合运算性需要的不仅仅是计算的速度,还需要优越的设计和丰富的分布式脚手架,这一点golang挺适合,而且巧了,我很熟悉,创业时候很多项目是用它实现的,来阿里后开发的社交短链系统[buc.kim]就是用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
(
""
,
true
)
log
.
Println
(
"load data finished"
)
if
err
!=
nil
{
log
.
Fatal
(
"load mnist data failed"
)
}
x_train
.
DevideFloat
(
255
)
x_test
.
DevideFloat
(
255
)
model
:=
model
.
Sequential
([]
layers
.
Layer
{
layers
.
Conv2D
(
64
,
[]
int
{
5
,
5
},
"relu"
,
[]
int
{
1
,
1
},
"valid"
,
[]
int
{
28
,
28
,
1
},
algo
.
InitMatrixWithNormal
()),
layers
.
MaxPool2D
([]
int
{
2
,
2
}),
layers
.
Flatten
(),
layers
.
Dense
(
20
,
"relu"
,
algo
.
InitMatrixWithRandom
(
-
1.0
,
1.0
)),
layers
.
Dropout
(
0.2
),
layers
.
Dense
(
10
,
"softmax"
,
algo
.
InitMatrixWithRandom
(
-
1.0
,
1.0
)),
})
model
.
Compile
(
"adam"
,
"sparse_categorical_crossentropy"
,
[]
string
{
"accuracy"
})
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
(
"multiLayer Forward"
)
}
for
_
,
layer
:=
range
ml
.
layers
{
err
:=
layer
.
Forward
()
if
err
!=
nil
{
return
err
}
}
return
nil
}
func
(
ml
*
multiLayer
)
Backward
()
error
{
if
false
{
log
.
Println
(
"multiLayer Backward"
)
}
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
(
"multiLayer Update"
)
}
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
[
index
*
k
:(
index
+
1
)
*
k
]
for
j
:=
0
;
j
<
n
;
j
++
{
wi
:=
b
[
j
*
k
:
(
j
+
1
)
*
k
]
var
v
float64
for
h
:=
0
;
h
<
k
;
h
++
{
v
=
v
+
ai
[
h
]
*
wi
[
h
]
}
c
[
index
*
n
+
j
]
=
v
+
bias
[
j
]
}
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
(
""
,
true
)
log
.
Println
(
"load data finished"
)
if
err
!=
nil
{
log
.
Fatal
(
"load mnist data failed"
)
}
x_train
.
DevideFloat
(
255
)
x_test
.
DevideFloat
(
255
)
model
:=
model
.
Sequential
([]
layers
.
Layer
{
layers
.
Conv2D
(
64
,
[]
int
{
5
,
5
},
"relu"
,
[]
int
{
1
,
1
},
"valid"
,
[]
int
{
28
,
28
,
1
},
algo
.
InitMatrixWithNormal
()),
layers
.
MaxPool2D
([]
int
{
2
,
2
}),
layers
.
Flatten
(),
layers
.
Dense
(
20
,
"relu"
,
algo
.
InitMatrixWithRandom
(
-
1.0
,
1.0
)),
layers
.
Dropout
(
0.2
),
layers
.
Dense
(
10
,
"softmax"
,
algo
.
InitMatrixWithRandom
(
-
1.0
,
1.0
)),
})
model
.
Compile
(
"adam"
,
"sparse_categorical_crossentropy"
,
[]
string
{
"accuracy"
})
model
.
Fit
(
x_train
,
y_train
,
625
,
5
)
model
.
Evaluate
(
x_test
,
y_test
)
用的是lecun的图库:http://
yann.lecun.com/exdb/mni
st/
为此还要写一个加载图片的方法
func
LoadMnistData
(
path
string
,
gz
bool
)(
tensor
.
Tensor
,
tensor
.
Tensor
,
tensor
.
Tensor
,
tensor
.
Tensor
,
error
){
log
.
Println
(
"start to load mnist data"
)
origin_folder
:=
"http://yann.lecun.com/exdb/mnist/"
idx3_train_image_name
:=
"train-images-idx3-ubyte"
idx3_train_label_name
:=
"train-labels-idx1-ubyte"
idx3_test_image_name
:=
"t10k-images-idx3-ubyte"
idx3_test_label_name
:=
"t10k-labels-idx1-ubyte"
//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
(
"load mnist image, num:%d, row:%d, column:%d, pixel length:%d\n"
,
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
(
"load mnist label, labels length:%d\n"
,
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
.
Init classificationLabel
(
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
.
Init classificationLabel
(
0
,
9
,
len
(
test_labels
),
test_labels
)
log
.
Printf
(
"train x size:%d, y size:%d\n"
,
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
map
[
int
]
rune
}
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
[
col_index
]
=
0
}
else
{
imindex
:=
im_col
+
width
*
(
im_row
+
height
*
c_im
)
data_col
[
col_index
]
=
data_im
[
imindex
]
}
}
}
}
}
再就是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
[
i1
*
lda
+
i2
]
for
i3
:=
0
;
i3
<
n
;
i3
++
{
c
[
i1
*
ldc
+
i3
]
=
c
[
i1
*
ldc
+
i3
]
+
a_part
*
b
[
i2
*
ldb
+
i3
]
}
}
}
}
im2col+gemm将数据先展开再作矩阵乘法,conv的核心计算就搞定了,实际上为了追求性能,我又加上了routine:
for
i
:=
0
;
i
<
curbatchsize
;
i
++
{
var
c
[]
float64
=
layer
.
output
.
Data
[
i
*
out_channel_size
:]
go
func
(
index
int
){
b
:=
layer
.
ctldata
.
Data
[
index
*
ctlSampleSize
:
]
algo
.
Im2Col_cpu
(
input
.
Data
[
index
*
img_channel_size
:],
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
(
"lstmLayer Backward"
)
}
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
[
uints_index
:]
tht
:=
dtht
[
uints_index
:]
thd
:=
dthd
[
uints_index
:]
ctt
:=
mmct
[
uints_index
:]
ctt_prev
:=
mmct_prev
[
uints_index
:]
ctd_prev
:=
mmcd_prev
[
uints_index
:]
//output gate layer output
zod
:=
ldod
[
uints_index
:]
zot
:=
ldot
[
uints_index
:]
//input data layer output
zgd
:=
ldgd
[
uints_index
:]
zgt
:=
ldgt
[
uints_index
:]
//input gate layer output
zid
:=
ldid
[
uints_index
:]
zit
:=
ldit
[
uints_index
:]
//forget gate layer output
zfd
:=
ldfd
[
uints_index
:]
zft
:=
ldft
[
uints_index
:]
for
k
:=
0
;
k
<
units
;
k
++
{
mhtk
:=
mht
[
k
]
tht
[
k
]
=
mhtk
*
zod
[
k
]
thdk
:=
thd
[
k
]
//cell state delta
cttk
:=
tht
[
k
]
*
(
1
-
thdk
*
thdk
)
ctt
[
k
]
=
cttk
//output delta
zot
[
k
]
=
mhtk
*
thdk
//zit input gate delta
//zgt data delta
zit
[
k
]
=
cttk
*
zgd
[
k
]
zgt
[
k
]
=
cttk
*
zid
[
k
]
if
s
>=
0
{
zft
[
k
]
=
cttk
*
ctd_prev
[
k
]
}
//cell state prev delta
ctt_prev
[
k
]
=
cttk
*
zfd
[
k
]
}
}
err
:=
layer
.
layerF
.
Backward
()
if
err
!=
nil
{
log
.
Println
(
"layer f backward error"
)
return
err
}
err
=
layer
.
layerI
.
Backward
()
if
err
!=
nil
{
log
.
Println
(
"layer i backward error"
)
return
err
}
err
=
layer
.
layerG
.
Backward
()
if
err
!=
nil
{
log
.
Println
(
"layer g backward error"
)
return
err
}
err
=
layer
.
layerO
.
Backward
()
if
err
!=
nil
{
log
.
Println
(
"layer o backward error"
)
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
[
uints_index
:]
real_input_delta
:=
real_step_delta
[
i
*
input_real_channel
:]
k2
:=
input_channel
for
j
:=
0
;
j
<
units
;
j
++
{
mht
[
j
]
+=
real_input_delta
[
k2
]
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
[
s
*
batch
*
input_real_channel
:]
input_step_delta
:=
input_delta
[
s
*
batch
*
input_channel
:]
for
i
:=
0
;
i
<
batch
;
i
++
{
real_delta
:=
real_step_delta
[
i
*
input_real_channel
:]
ipt_delta
:=
input_step_delta
[
i
*
input_channel
:]
for
k
:=
0
;
k
<
input_channel
;
k
++
{
ipt_delta
[
k
]
=
real_delta
[
k
]
}
}
}
return
nil
}
复制
代码有些长,但我觉得这里有一些独特的心得,所以原样呈上了。
由于以上四层的设计后,update就非常易于理解了
func
(
layer
*
lstmLayer
)
Update
(
args
*
network
.
UpdateArgs
)
error
{
if
false
{
log
.
Println
(
"lstmLayer Update"
)
}
err
:=
layer
.
layerF
.
Update
(
args
)
if
err
!=
nil
{
log
.
Fatal
(
"lstm layer update error, layerf update error"
)
return
err
;
}
err
=
layer
.
layerI
.
Update
(
args
)
if
err
!=
nil
{
log
.
Fatal
(
"lstm layer update error, layerf update error"
)
return
err
;
}
err
=
layer
.
layerG
.
Update
(
args
)
if
err
!=
nil
{
log
.
Fatal
(
"lstm layer update error, layerf update error"
)
return
err
;
}
err
=
layer
.
layerO
.
Update
(
args
)
if
err
!=
nil
{
log
.
Fatal
(
"lstm layer update error, layerf update error"
)
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
(
"the prev layer of dropout should be flatten or dense"
)
}
}
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
,
"softmax"
,
algo
.
InitMatrixWithRandom
(
-
1.0
,
1.0
)),
})
结语
多机联动,除了分布式算法,还有很多的场景,当年实现的多人飞机大战。
https:// v.youku.com/v_show/id_X MTQ1MTkyOTUxNg==.html?spm=a2pj.8428770.3416059.1
每次视频里的音乐一响起,我的眼泪就忍不住花花的,那是创业,我的青春。那时候真是过瘾。
机器算是保住了,当年的创业时的回忆偶尔也会在记忆深处泛处点点涟漪。
江湖之远,我们在各自前行着。