當前位置: 華文問答 > 數碼

你在演算法的使用上有什麽絕活?

2015-01-03數碼

數學之修,其路遙遙。。。



這是一篇技術散文。

故事要從上次搬家說起。

"你那些破電腦我們要賣掉啦,不然還要叫個車真麻煩",

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

每次影片裏的音樂一響起,我的眼淚就忍不住花花的,那是創業,我的青春。那時候真是過癮。



機器算是保住了,當年的創業時的回憶偶爾也會在記憶深處泛處點點漣漪。



江湖之遠,我們在各自前行著。