当前位置: 华文问答 > 数码

你在算法的使用上有什么绝活?

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

每次视频里的音乐一响起,我的眼泪就忍不住花花的,那是创业,我的青春。那时候真是过瘾。



机器算是保住了,当年的创业时的回忆偶尔也会在记忆深处泛处点点涟漪。



江湖之远,我们在各自前行着。