數學之修,其路遙遙。。。
這是一篇技術散文。
故事要從上次搬家說起。
"你那些破電腦我們要賣掉啦,不然還要叫個車真麻煩",
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
每次視訊裏的音樂一響起,我的眼淚就忍不住花花的,那是創業,我的青春。那時候真是過癮。
機器算是保住了,當年的創業時的回憶偶爾也會在記憶深處泛處點點漣漪。
江湖之遠,我們在各自前行著。