找回密码
 立即注册
查看: 336|回复: 0

算法的穷人玩法

[复制链接]
发表于 2022-6-10 10:53 | 显示全部楼层 |阅读模式

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



这是一篇技术散文。
故事要从上次搬家说起。
"你那些破电脑我们要卖掉啦,不然还要叫个车真麻烦",
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[j] += A[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/99431]
实际上我也写了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/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       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_XMTQ1MTkyOTUxNg==.html?spm=a2h3j.8428770.3416059.1
每次视频里的音乐一响起,我的眼泪就忍不住花花的,那是创业,我的青春。那时候真是过瘾。


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




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

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-16 02:28 , Processed in 0.093212 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表