现在的位置: 首页 > 综合 > 正文

Deep Learning(Logistic Regression)学习之MNIST C++实现

2014年02月27日 ⁄ 综合 ⁄ 共 5805字 ⁄ 字号 评论关闭

1、这是75%完成度的SGD代码

主要的程序在八月中旬基本完成了,后边忙别的事情,实在懒得再仔细写了,目前的代码是75%完成度的,已知bug和可以改进的地方有:

1、在多次进行下降后,参数矩阵可能变成NaN值。(即由于参数计算导致除零错,目前尚未排查,估计在6%错误率左右的时候出现)

2、并发性可以进行改进。包括采用GPU进行计算的可能。

3、断点保存可以改进。目前保存每次的参数矩阵,可以从上次参数矩阵进行下降。但尚未保存每次退出时进行下降的训练例子位置等信息。

这个SGD的效果还行,应该不比原例子差,不过性能就差了不止十万八千里(囧)。但是考虑到这是第一次写这种算法,没有必要对自己吹毛求疵,而且目的是为了学习和理解SGD算法,自我安慰安慰。

原例子在http://deeplearning.net/tutorial/gettingstarted.html,采用python写的,主要数学工具是Theano。

这个SGD代码的程序一会儿我上载到资源区。

2、为什么要用 C++实现SGD算法

这是因为没有搞定Theano。Logistic-SGD所需环境包括Theano,但是在尝试在Win7/IronPython VS2010环境下安装Theano未果之后,只能采取手动挡来研究一下Logistic-SGD的代码了。

基本思路是通过IronPython+VS2010 .Net环境,在找一些数学库(用于矩阵运算等等),替换Theano的部分内容,使得程序能够跑起来。

结果后来VS2010.Net操作IronPython没有搞定numpy库(抓狂,在外边直接可以用numpy,在C++初始化IronPython却不能使用numpy),加上学的深入一点后,似乎不用特别复杂的数学库,直接实现也可以。于是就有了这个程序。

 

3、SGD理论



4、程序

原例子中的mnist.pkg.gz是压缩后的数据族,为了能够便于C++直接处理,我们首先把mnist.pkg.gz改成易于处理的数据-结果对,即:将mnist.pkg解压为6个文件:

train_set_x

train_set_y

valid_set_x

valid_set_y

test_set_x

test_set_y

对应train,valid,test三个集合(每个集合两对,即数据-结果对)。train_set_x中的784行对应train_set_y中的一行,即代表一个测试数据集合。其中train_set_x每一行是一个浮点数,748行(784为28x28的灰度浮点数)代表一个数字的灰度图,对应train_set_y的一行,即对应的数字。

 

这个python程序如下,很简单,写成文本文件好了,然后再写一个C++程序读这个文本文件,转成相关的 二进制文件。

 

import cPickle

f = open(‘mnist.pkg’, 'rb')

train_set,valid_set, test_set = cPickle.load(f)

f.close()

test_set_x, test_set_y = test_set

train_set_x, train_set_y = train_set

valid_set_x, valid_set_y = valid_set

      

fileHandle=open("test_set_x","w")

cnt = 0

for i in test_set_x:

       ifcnt% 10 ==0:

              printcnt

       cnt= cnt +1

       forj in i:

              fileHandle.write("%.6f\n"%j)

fileHandle.close()

 

cnt = 0

fileHandle=open("test_set_y","w")

for i in test_set_y:

       ifcnt% 10 ==0:

              printcnt

       cnt= cnt +1

       fileHandle.write("%d\n"%i)

       fileHandle=open("train_set_x","w")

cnt=0

for i in train_set_x:

       ifcnt% 10 ==0:

              printcnt

       cnt= cnt +1

       forj in i:

              fileHandle.write("%.6f\n"%j)

fileHandle.close()

fileHandle=open("train_set_y","w")

cnt=0

for i in train_set_y:

       ifcnt% 10 ==0:

              printcnt

       cnt= cnt +1

       fileHandle.write("%d\n"%i)

 

为了方便,已经做了二进制文件。

4.1、程序结构

整个程序由三部分组成:主程序、SGD主程序、文件操作支持函数组成。

数据格式在dataset.h中定义,一个集合(训练、验证、测试)为:

 

/* DSET_X 为每一个样例的灰度图,由784double构成
*/

typedef double    DSET_X[784];

 

typedef struct st_set_array

{

    int    m_iCount;
/*
集合中样本个数 */

    DSET_X *pArrayX;  /* 每一个样本的灰度图 */

    int    *pArrayY; 
/*
对应的样本识别标签 */

}SET_ARRAY;

 

 

 

 

 

 


训练对应的参数w矩阵和b向量定义如下:

/*

 * WMATRIX W matrix parameter

 * bVECTOR  b vector parameter

 */

typedef DSET_X    WMATRIX[10];

typedef double    bVECTOR[10];

 

 

 

 

 


矩阵定义其实可以用简单的wMatrix[ ][ ]来描述,但是我比较笨,懒得去思考C++二维数组的行列,索性就简单的先定义DSET_X然后再定义一维数组,之后的操作中全部采用一维数组的操作,避免因为笨导致的问题。

定义了一个控制块,用于各个函数中传递参数使用,

 

 

 

 

 

typedef struct st_sgd

{

    double     m_dLearnRate;

    int        m_iEpoch;

    int        m_iBatchSize;

    /* ----------------------------- */

    WMATRIX       m_wMatrix;

    bVECTOR       m_bVector;

    SET_ARRAY  *m_pTrainSet;

    SET_ARRAY  *m_pValidSet;

    SET_ARRAY  *m_pTestSet;

    /* ----------------------------- */

    MATRIXINDEX   m_MatrixIndex;

    double     *m_Pi;

    int        m_iValidationFrequency;

    int        m_iPatience;

    int        m_iPatienceIncrease;

 

}ST_SGD;

 

 

 

 

 

 

 

 

 

 

 

 

 

 


这里的内容可以不求甚解。看变量名也应该可以猜到干啥的,m_dLearnRate是学习率(就是参数delta的系数),Epoch是最大学习迭代次数。

按照整体的思想,我每次训练一批(一个BatchSize个训练样本),就调整并dump一次参数,然后训练一定批次后,用validate样本进行验证,当valid有改善时,用test样本进行验证。

中间退出的话,由于保存了每批dump的参数,下次直接可以用最新的参数进行训练。

Ok,let‘s do it。

4.2、主程序

主程序很简单,在minst04.cpp中(为啥是04?因为这是我第四次重写这个程序)。关键点在以下几行代码:

1、首先是读取训练样本文件

LoadDataSet(&TrainSet,&TestSet,&ValidSet);

这个是从"C:\\DEVELOP\\MNIST\\“下读取相关的二进制训练集合(LoadDataSet的函数实现也在mnist04.cpp中,其具体读取文件的函数实现在FileConvert.cpp中。

2、初始化sgd控制结构

ZeroMemory(&m_sgd,sizeof(m_sgd));

m_sgd.m_dLearnRate=0.13;         学习率0.13

m_sgd.m_iBatchSize=600;                   600个样本一组

m_sgd.m_iEpoch=1000;                最多训练1000次

m_sgd.m_iPatience=5000;                   。。。难以言表

m_sgd.m_iPatienceIncrease=2;           。。。难以言表

m_sgd.m_pTestSet = &TestSet;          测试集合

m_sgd.m_pTrainSet = &TrainSet;        训练集合

m_sgd.m_pValidSet = &ValidSet;         验证集合

3、读取上次dump的参数集合。

我们每次dump的参数文件在”C:\\DEVELOP\\MNIST\\parms\\“目录下,文件名以parms打头,因此读取上次dump的参数是找到最新的parms文件,然后读取之:

GetLatestFile("C:\\DEVELOP\\MNIST\\parms\\","parms*.txt",sName);

LoadParameters(sName,m_sgd.m_wMatrix,m_sgd.m_bVector);

相关函数在FileConvert.cpp中。

读取完,马上dump一份(可以看看读的是否正确。。。-_-!)

DumpMatrix("C:\\DEVELOP\\MNIST\\parms\\",m_sgd.m_wMatrix,m_sgd.m_bVector);

4、开始SGD

test_sgd(&m_sgd);

4.3、SGD

SGD的核心就是算几个数:

第一个是算

,即,给定某一个样本X,在Wb参数下,得出结果是i的概率。这个简单,在Softmax.cpp中,我们实现了两个(其实是一个意思,一个是快速查表算法Pn,一个是标准算法PnNoFast),

PnNoFast作为样例解剖,原型:

double PnNoFast(int iY,int iItem,SET_ARRAY *pSet,WMATRIX w,bVECTOR b)

返回pSet集合中的第iItem个样本,在w和b参数下,当输出分类是iY时的概率。

关键代码:

 

pw = &w[iY];

px = &pSet->pArrayX[iItem];

dExp1 = 0;

/* 计算矩阵相乘, Wi * X + bi */

for(i=0;i<784;i++)

{

       dExp1 += (*pw)[i]*(*px)[i];

}

dExp1 += b[iY];

dUpperValue=exp(dExp1);

这段代码实现了

 

dLowerValue = 0;

for(j=0;j<10;j++)

{

       dExp1 = 0;

       pw = &w[j];

       for(i=0;i<784;i++)

       {

              dExp1 +=(*pw)[i]*(*px)[i];

       }

       dExp1 += b[j];

       /*sum e^(WjX+bj) */

       dLowerValue += exp(dExp1);

}

这段代码实现了

当然dUpperValue/dLowerValue=

第二个是算偏导数。

double Dpi_dWmk(MATRIXINDEX pMatrixIndex,int iItem,int m,int k,WMATRIXwMatrix,bVECTOR bVector,SET_ARRAY *pSet)

{

       /* dpi/dwmk = (u'v-uv')/v^2 */

       double du,dv,u,v;

       du =DuI(pMatrixIndex,iItem,m,k,wMatrix,bVector,pSet);

       dv =Dvi(pMatrixIndex,iItem,m,k,wMatrix,bVector,pSet);

       u =Ui(pMatrixIndex,iItem,k,wMatrix,bVector,pSet);

       v =Vi(pMatrixIndex,iItem,k,wMatrix,bVector,pSet);

       return (du*v - u*dv)/(v*v);

}

实现代码在BuildDeltaW_b中

其核心就是三重循环,一是k从0-9循环,二是m从0-783循环,三是i从0到n循环,然后计算损失函数的导数。

dsm += pPi[i]*Dpi_dWmk(pMatrixIndex,i,m,k,wMatrix,bVector,pSet);

pPi[i]就是1/Pi的速查值。

 

test_sgd的核心就是

循环:0-Epoch

循环:0-每一个batch

       计算这个batch的损失函数偏导数

       然后新参数=参数-学习率*偏导数

       输出新参数矩阵

       如果该验证一下,就去验证一下,

              如果验证的有改进,就去test一下。

       判断是否可以提前结束

 

其他的…就不写了吧,反正test_sgd也挺简单的。

懒…

5、感谢

这部分内容对我来说是最痛苦的,特别要感谢Rachel Zhang同学。Rachel耐心、细致的回答了我无数幼稚和愚笨的基础数学问题,并鼓励我尝试完成,使得这一切成为可能。

此外,还感谢http://blog.sina.com.cn/s/blog_62339a2401015jyq.html,其图示使得我豁然开朗,明白了梯度下降算法的意义。建议初学者可以去看看。

抱歉!评论已关闭.