写了三个Logistic Regression的实现,发了好几篇博文,我都有点儿写上瘾了。
这一篇再进一步聊一下SGD的程序实现。从前的代码实现框架是这个样子:
// the sample format: classid feature1_value feature2_value...
bool LogisticRegression::TrainSGDOnSampleFile (
const char * sFileName, int iClassNum, int iFeatureNum, // about the samples
double dLearningRate = 0.05, // about the learning
int iMaxLoop = 1, double dMinImproveRatio = 0.01 // about the stop criteria
)
{
......
for (int iLoop = 0; iLoop < iMaxLoop; iLoop++)
{
......
while (getline (in, sLine))
{
Sample theSample;
if (ReadSampleFrmLine (sLine, theSample))
{
......
}
}
......
if (dTmpRatio < dMinImproveRatio)
break;
else
{
......
//reset the current reading position of file
in.clear();
in.seekg (0, ios::beg);
}
}
return true;
}
在训练的过程中,一行行的读文件。读一行,解析一个样本,更新一次权重。如此反复。这种方法“蠢”在算法的效率瓶颈在于磁盘IO。如果把所有样本都load到内存中,在内存中遍历,至少比用磁盘IO提升上千倍速度。为啥用这种蠢方法,原因在于我从前上学的时候,做语言模型的训练,一般都是用十年人民日报(10G左右),无法一次都load到内存中。所以,以后每次写程序的时候,头脑中都有一个假设,就是目标数据集是单PC无法放到内存处理的,都是规模比较大的。
说了一会儿,发现说歪了,怪不得我高考作文都能写跑题。
我们看看上一篇博文中用来训练的数据集合,数据的排列是比较规律的:第0类的数据样本、接下来是第1类的数据样本、第2类的......我在想,这种排列会不会对训练有影响?如:收敛速度、在测试机集合的准确率等等。所以SGD算法作了如下改动:把所有样本load进内存,每次随机挑选一个样本来训练,代码如下:
// the sample format: classid feature1_value feature2_value...
bool LogisticRegression::TrainSGDOnSampleFileEx2 (
const char * sFileName, int iClassNum, int iFeatureNum, // about the samples
double dLearningRate = 0.05, // about the learning
int iMaxLoop = 1, double dMinImproveRatio = 0.01 // about the stop criteria
)
{
ifstream in (sFileName);
if (!in)
{
cerr << "Can not open the file of " << sFileName << endl;
return false;
}
if (!InitThetaMatrix (iClassNum, iFeatureNum))
return false;
vector<Sample> SampleVec;
if (!LoadAllSamples (sFileName, SampleVec))
return false;
double dCost = 0.0;
double dPreCost = 100.0;
for (int iLoop = 0; iLoop < iMaxLoop; iLoop++)
{
srand((unsigned)time(NULL));
int iErrNum = 0;
int iSampleNum = (int)SampleVec.size();
for (int i=0; i<iSampleNum; i++)
{
double dRandomFloat = (double)rand() / RAND_MAX;
int iSampleIndex = (int)(dRandomFloat * iSampleNum);
vector<double> ClassProbVec;
int iPredClassIndex = CalcFuncOutByFeaVecForAllClass (SampleVec[iSampleIndex].FeaValNodeVec, ClassProbVec);
if (iPredClassIndex != SampleVec[iSampleIndex].iClass)
iErrNum++;
dCost += UpdateThetaMatrix (SampleVec[iSampleIndex], ClassProbVec, dLearningRate);
}
dCost /= iSampleNum;
double dTmpRatio = (dPreCost - dCost) / dPreCost;
double dTmpErrRate = (double)iErrNum / iSampleNum;
// show info on screen
cout << "In loop " << iLoop << ": current cost (" << dCost << ") previous cost (" << dPreCost << ") ratio (" << dTmpRatio << ") "<< endl;
cout << "And Error rate : " << dTmpErrRate << endl;
/*if (dTmpRatio < dMinImproveRatio)
break;
else*/
if (dCost < 0.001)
break;
{
dPreCost = dCost;
dCost = 0.0;
}
}
return true;
}
其中,load样本的代码片段:
vector<Sample> SampleVec;
if (!LoadAllSamples (sFileName, SampleVec))
return false;
随机挑选样本的代码片段:
double dRandomFloat = (double)rand() / RAND_MAX;
int iSampleIndex = (int)(dRandomFloat * iSampleNum);
从训练的log上来看,修改后的算法的确收敛速度快一些。在测试集合上,性能是0.863395。对比相同参数条件下,顺序遍历样本来训练的方法,性能为0.851459,后者提升了一个百分点左右。
通过这个结果,粗略的一个结论,就是SGD的效果是与训练样本顺序有关的,最好是随机顺序,而不是什么有规律的排列。当然,我还是坚持从前的观点,就是假设能把所有样本load到内存是不现实的,如果真有这样的事情,那么就用svm了,干嘛选择LR啊。
完。
转载请注明出处:http://blog.csdn.net/xceman1997/article/details/18566857