Ilingis 发表于 2022-6-10 10:53

算法的穷人玩法


数学之修,其路遥遥。。。



这是一篇技术散文。
故事要从上次搬家说起。
"你那些破电脑我们要卖掉啦,不然还要叫个车真麻烦",
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("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 * 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("cost time:", 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("", 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
            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("", 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/mnist/

为此还要写一个加载图片的方法


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.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("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       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("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

            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("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
            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("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_XMTQ1MTkyOTUxNg==.html?spm=a2h3j.8428770.3416059.1
每次视频里的音乐一响起,我的眼泪就忍不住花花的,那是创业,我的青春。那时候真是过瘾。


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




江湖之远,我们在各自前行着。
页: [1]
查看完整版本: 算法的穷人玩法