注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

Koala++'s blog

计算广告学 RTB

 
 
 

日志

 
 

Weka开发[52]——MultiLayerPerceptron源代码分析  

2011-06-05 21:37:46|  分类: 机器学习 |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |

       神经网络我感觉讲的最清楚的还是Tom Mitchell的《机器学习》,虽然这本书是有点过时了,但是它仍然值得一读。我在这篇里会用它第四章的公式编号。

       Weka实现了线性单元(LinearUnit)sigmoid单元(SigmoidUnit)Sigmoid函数的输出是输入的非线性函数,并且输出是输入的可微函数(因为要使用梯度下降)。双曲正切函数不知道怎么没实现。

       下面就从SigmoidUnit类的实现开始看:

public double outputValue(NeuralNode node) {

    double[] weights = node.getWeights();

    NeuralConnection[] inputs = node.getInputs();

    double value = weights[0];

    for (int noa = 0; noa < node.getNumInputs(); noa++) {

 

       value += inputs[noa].outputValue(true) * weights[noa + 1];

    }

 

    //this I got from the Neural Network faq to combat overflow

    //pretty simple solution really :)

    if (value < -45) {

       value = 0;

    } else if (value > 45) {

       value = 1;

    } else {

       value = 1 / (1 + Math.exp(-value));

    }

    return value;

}

       For循环是得到y=wxelse中是sigmoid函数 delta = 1 / (1 + e-y),但是代码中防止溢出的45这个值不知道是怎么得到的,我没google到,而且感觉它与极值也不太接近。

public double errorValue(NeuralNode node) {

    //then calculate the error.

 

    NeuralConnection[] outputs = node.getOutputs();

    int[] oNums = node.getOutputNums();

    double error = 0;

 

    for (int noa = 0; noa < node.getNumOutputs(); noa++) {

       error += outputs[noa].errorValue(true)

              * outputs[noa].weightValue(oNums[noa]);

    }

    double value = node.outputValue(false);

    error *= value * (1 - value);

 

    return error;

}

       For循环是求公式4.15中的sigma的部分,而value则是oh

public void updateWeights(NeuralNode node, double learn, double momentum) {

 

    NeuralConnection[] inputs = node.getInputs();

    double[] cWeights = node.getChangeInWeights();

    double[] weights = node.getWeights();

    double learnTimesError = 0;

    learnTimesError = learn * node.errorValue(false);

    double c = learnTimesError + momentum * cWeights[0];

    weights[0] += c;

    cWeights[0] = c;

 

    int stopValue = node.getNumInputs() + 1;

    for (int noa = 1; noa < stopValue; noa++) {

 

       c = learnTimesError * inputs[noa - 1].outputValue(false);

       c += momentum * cWeights[noa];

 

       weights[noa] += c;

       cWeights[noa] = c;

    }

}

       传入的参数learn相当于公式4.16中的itamomentum自己是冲量,即公式4.17中的alpha了,node.errorValue(false)相当于是thita。代码中将weights[0]独立出来处理,当然是因为它没有对应的inputs元素。另外就是公式4.17中的(n-1)在这里没有。

       现在看MultiLayerPerceptronbuildClassifier函数:

setClassType(m_instances);

 

//this sets up the validation set.

Instances valSet = null;

//numinval is needed later

int numInVal = (int) (m_valSize / 100.0 * m_instances.numInstances());

if (m_valSize > 0) {

    if (numInVal == 0) {

       numInVal = 1;

    }

    valSet = new Instances(m_instances, 0, numInVal);

}

       setClassType是对属性作规范化,将属性的值的范围限制到[-1,1]之间。对它的实现我有点疑问,如果m_normalizeAttributesfalse时,直接continue,而不计算m_attrbuteRangesm_attributeBases不是更好?

m_attributeRanges[noa] = (max - min) / 2;

m_attributeBases[noa] = (max + min) / 2;

       其中m_attributeRanges是取值范围,m_attributeBases可以视为原点,/2是因为是限制到[-1,1]区间,而不是[0,1]区间。

       numInVal是检验的样本数,m_valSize是输入的参数,valSet是检验集。

setupInputs();

 

setupOutputs();

if (m_autoBuild) {

    setupHiddenLayer();

}

       这三个函数自然是设置输入层,设置输出层,和设置隐层,下面就分别看这三个函数。

private void setupInputs() throws Exception {

    m_inputs = new NeuralEnd[m_numAttributes];

    int now = 0;

    for (int noa = 0; noa < m_numAttributes + 1; noa++) {

       if (m_instances.classIndex() != noa) {

           m_inputs[noa - now] = new NeuralEnd(m_instances.

attribute(noa).name());

 

           m_inputs[noa - now].setX(.1);

           m_inputs[noa - now].setY((noa - now + 1.0)

                  / (m_numAttributes + 1));

           m_inputs[noa - now].setLink(true, noa);

       } else {

           now = 1;

       }

    }

}

       NeuralEnd继承自NeuralConnection,也是一种结点类型,先不去分析它。先是给m_inputs分配一个属性数个数的NeuralEnd数组(这当然了,算法本身决定的),在for循环中将每个输出结点初始化,setXsetY与显示有关,不去理睬。Now是为了有时候类别属性中中间的情况,setLink参数true是指这个结点是输入结点,因为NeuralEnd也能表示输出结点。

private void setupOutputs() throws Exception {

    m_outputs = new NeuralEnd[m_numClasses];

    for (int noa = 0; noa < m_numClasses; noa++) {

       if (m_numeric) {

           m_outputs[noa] = new NeuralEnd(m_instances.classAttribute()

                  .name());

       } else {

           m_outputs[noa] = new NeuralEnd(m_instances.classAttribute()

                  .value(noa));

       }

 

       m_outputs[noa].setX(.9);

       m_outputs[noa].setY((noa + 1.0) / (m_numClasses + 1));

       m_outputs[noa].setLink(false, noa);

       NeuralNode temp = new NeuralNode(String.valueOf(m_nextId),

              m_random, m_sigmoidUnit);

       m_nextId++;

       temp.setX(.75);

       temp.setY((noa + 1.0) / (m_numClasses + 1));

       addNode(temp);

       NeuralConnection.connect(temp, m_outputs[noa]);

    }

}

       如果类别是连续值,m_numClasses就为1setLink是指定这个结点是输出结点,然后初始化一个sigmoid结点,并和输出结点conntect,其实也就是输出结点是一个sigmoid结点,addNode是将这个结点加入m_neuralNodes,这个数组保持着所有参与计算的结点。

if (!s.connectOutput(t, t.getNumInputs())) {

    return false;

}

if (!t.connectInput(s, s.getNumOutputs() - 1)) {

 

    s.disconnectOutput(t, t.getNumInputs());

    return false;

}

       这是conntect中的几行代码,可以看出,temp将输出指定为outputs[noa],而m_outputs[noa]将输入指定为temp

StringTokenizer tok = new StringTokenizer(m_hiddenLayers, ",");

int val = 0; //num of nodes in a layer

int prev = 0; //used to remember the previous layer

int num = tok.countTokens(); //number of layers

String c;

for (int noa = 0; noa < num; noa++) {

    for (int nob = 0; nob < val; nob++) {

       NeuralNode temp = new NeuralNode(String.valueOf(m_nextId),

              m_random, m_sigmoidUnit);

       m_nextId++;

       addNode(temp);

       if (noa > 0) {

           //then do connections

           for (int noc = m_neuralNodes.length - nob - 1 - prev; noc <

m_neuralNodes.length - nob - 1; noc++) {

              NeuralConnection.connect(m_neuralNodes[noc], temp);

           }

       }

    }

    prev = val;

}

       Val是当前的层的结点数,这个值的获取代码我删除了,prev是指前一层的结点数,Num是隐层的层数, 两层for循环,第一层循环层数,第二层循环当前层结点数,初始化每个结点,如果noa>0,表示不是第一层,就要与以前的隐层做连接。

if (val == 0) {

    for (int noa = 0; noa < m_numAttributes; noa++) {

       for (int nob = 0; nob < m_numClasses; nob++) {

           NeuralConnection.connect(m_inputs[noa],

m_neuralNodes[nob]);

       }

    }

} else {

    for (int noa = 0; noa < m_numAttributes; noa++) {

       for (int nob = m_numClasses; nob < m_numClasses + val; nob++) {

           NeuralConnection.connect(m_inputs[noa],

m_neuralNodes[nob]);

       }

    }

    for (int noa = m_neuralNodes.length - prev; noa <

m_neuralNodes.length; noa++) {

       for (int nob = 0; nob < m_numClasses; nob++) {

           NeuralConnection.connect(m_neuralNodes[noa],

                  m_neuralNodes[nob]);

       }

    }

}

       这时val是第一层隐层的个数,如果val==0,则表示没有隐层,那么就将输入层与输出层边接。否则,将输入层与隐层的第一层连接,注意内层循环从m_numClasses开始,到m_numClasses + val,是因为开始的m_numClasses是输出结点。接下来是将隐层的最后一层与输出层连接。

       作者在注释里提到Java里的一个有意思的现象:

System.out.println( Double.valueOf(" 1") );

System.out.println( Integer.valueOf(" 1") );

       第一个能正常执行,第二个会报错。

for (int nob = numInVal; nob < m_instances.numInstances(); nob++) {

    m_currentInstance = m_instances.instance(nob);

 

    if (!m_currentInstance.classIsMissing()) {

 

       //this is where the network updating (and training occurs, for the

       //training set

       resetNetwork();

       calculateOutputs();

       tempRate = m_learningRate * m_currentInstance.weight();

       if (m_decay) {

           tempRate /= noa;

       }

 

       right += (calculateErrors() / m_instances.numClasses())

              * m_currentInstance.weight();

       updateNetworkWeights(tempRate, m_momentum);

    }

}

       resetNetwork reset输出结点,calculateOutputs是得到输出结果,tempRate是当前样本的学习速率,right最后用于计算错误率,最后会更新结点的权重。

//node is an output.

m_unitValue = 0;

for (int noa = 0; noa < m_numInputs; noa++) {

    m_unitValue += m_inputList[noa].outputValue(true);

}

if (m_numeric && m_normalizeClass) {

    //then scale the value;

    //this scales linearly from between -1 and 1

    m_unitValue = m_unitValue

           * m_attributeRanges[m_instances.classIndex()]

           + m_attributeBases[m_instances.classIndex()];

}

       calculateOutputs中的代码,输出结点的输入是setOutputs时连接的sigmoid结点,每个输出结点对应一个sigmoid结点,下面的if是类别是连续值并且要离散化类别时,因为开始时规范化到[-1,1]区间了,所以现在还原回去。

for (int noc = 0; noc < m_numClasses; noc++) {

    temp = m_outputs[noc].errorValue(false);

    ret += temp * temp;

}

return ret;

       calculateErrors中的代码,返回的值是errorValule的平方。

private void updateNetworkWeights(double l, double m) {

    for (int noc = 0; noc < m_numClasses; noc++) {

       //update weights

       m_outputs[noc].updateWeights(l, m);

    }

}

       先从输出层开始更新权值:

public void updateWeights(double l, double m) {

    if (!m_weightsUpdated) {

       for (int noa = 0; noa < m_numInputs; noa++) {

           m_inputList[noa].updateWeights(l, m);

       }

       m_weightsUpdated = true;

    }

}

       它开始对前一层的输入结点进行循环,更新权值,它会调用NeutralNodeupdateWeights函数:

public void updateWeights(double l, double m) {

 

    if (!m_weightsUpdated && !Double.isNaN(m_unitError)) {

       m_methods.updateWeights(this, l, m);

 

       super.updateWeights(l, m); //to call all of the inputs.

    }

}

       这里会调用SigmoidUnitupdateWeights,然后继续向前一层传播。

right = 0;

for (int nob = 0; nob < valSet.numInstances(); nob++) {

    m_currentInstance = valSet.instance(nob);

    if (!m_currentInstance.classIsMissing()) {

       resetNetwork();

       calculateOutputs();

       right += (calculateErrors() / valSet.numClasses())

              * m_currentInstance.weight();

    }

}

 

if (right < lastRight) {

    if (right < bestError) {

       bestError = right;

       // save the network weights at this point

       for (int noc = 0; noc < m_numClasses; noc++) {

           m_outputs[noc].saveWeights();

       }

       driftOff = 0;

    }

} else {

    driftOff++;

}

lastRight = right;

if (driftOff > m_driftThreshold || noa + 1 >= m_numEpochs) {

    for (int noc = 0; noc < m_numClasses; noc++) {

       m_outputs[noc].restoreWeights();

    }

    m_accepted = true;

}

right /= totalValWeight;

       for循环中对检验集进行验证,得到加权错误率之和right,如果right<lastRight表示这次错误率更低,right<bestError表示这次是最低的错误率,这时会记录下当前结点的权重,driftOff是已经有多少次没有超过上次达到的最低错误率。而m_numEpoch就是最多迭代次数。

 

 

 

 

 

 

 

 

 

 

 

 

 

  评论这张
 
阅读(4087)| 评论(1)
推荐 转载

历史上的今天

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2017