Weka中BP神经网络的实践(参数调整以及结果分析)
废话:
周日讲了下神经网络,本来想的是以理论和实践相结合,前面讲讲神经网络,后面简单讲下在weka中怎么使用BP神经网络,可惜最后时间不够,而且姥姥的兴趣点跑到凸优化那里去了,所以没有讲成实践的部分,有点郁闷的。为了不浪费了,就把这部分讲稿拿出来和大家分享一下,也希望对大家实践神经网络有所帮助。因为是讲稿,讲的要比写的多,所以很多地方口语化和省略比较严重,大家凑合着看吧。
实践部分讲稿正文:
Weka是什么?
Weka是由新西兰怀卡托大学用Java开发的数据挖掘常用软件,Weka是怀卡托智能分析系统的缩写。Weka限制在GNU通用公众证书的条件下发布,它几乎可以运行在所有操作系统平台上,包括Linux、Windows、Macintosh等。
Weka中BP神经网络的实践:
Weka中的神经网络使用多层多层感知器实现BP神经网络。让我们看看weka自带的帮助文件是怎么描述的:
BP神经网络在weka中是分属这个部分的weka.classifiers.functions.MultilayerPerceptron
其是一个使用了反向传播(backpropagation)的分类器。
你可以手工构造这个网络,用算法创建它,或者两者兼备。 这个网络可以在训练的过程中被监视和修改。
网络中的节点是Sigmoid的,除了当类别(class)是数值属性(numeric)的,这时输出节点变成了unthresholded linear units。
注:sigmoid如下图
关于里面参数的配置如下图
下面我们来看各个参数的具体意义:
GUI
弹出一个GUI界面。其允许我们在神经网络训练的过程中暂停和做一些修改(altering)
- 按左键添加一个节点(node)(节点将被自动选择以保证没有其他的节点被选择)
- 选中一个节点:左键单击
- 连接一个节点:首先选中一个起始节点,然后点击一个结束节点或者空白区域(这将创建一个新节点并与起始节点连接)。在连接后节点的状态将保持不变。
- 删除连接:选择一个连接的节点并且右键单击另一个节点
- 删除节点:右键单击一个节点
- 取消选择:左键单击节点或者在空白区域右键单击
- 标签(label)提供的原始输入(raw input)在左边
- 红色的节点是隐层(hidden layers)
- 橙色的节点是输出节点(output nodes)
- 在右边的标签展示的是输出节点表示的类别。要注意的是对于一个数值属性的类别来说,输出节点将自动的做成一个unthresholded的线性单元
更改神经网络只能在网络没有启动(running)的时候做,这条规则也适用于学习速率(learning rate)和其他在控制面板上的区域。
- 您可以在任何时候结束网络
- 网络在一开始是自动暂停的
- 有一个关于网络up了和error的运行时提示。注意的是这个错误值(error value或者误差值吧)是基于网络的计算值的变化的
- 一旦网络训练完毕它会再次停止并且等待结果是否被接受还是继续训练
注意的是如果没有设置GUI,这个网络将不需要任何的交互(interaction)
autoBuild
添加网络中的连接和隐层
debug
设置为True分类器将输出额外的信息到控制台(console)
decay
这将导致学习的速率的降低。其将初始的学习速率除以迭代次数(epoch number)去决定当前的学习速率。这对于停止神经网络背离目标输出有帮助,也提高了general performance。要注意的是衰退的学习速率不会显示在GUI中。如果学习速率在GUI中被改变,这将被视为初始的学习速率。
hiddenLayers
定义神经网络的隐层。这是一个正整数的列表。1 for each hidden layer.用逗号分隔。如果没有隐层就在这里输入0。这只被用于自动构建是设置了的。也有通用符 'a' = (attribs + classes) / 2, 'i' = attribs, 'o' = classes , 't' = attribs + classes
learningRate
Weights被更新的数量
momentum
当更新weights时设置的动量
normalizeAttributes
将正则化(normalize)属性。这个能提高网络的performance。其并不依赖于class是不是数值属性的。其也会正则化名词性(nominal)的属性(当他们被nominal to binary filter run过后),这样名词性属性是在-1和1之间
normalizeNumericClass
将会正则化class如果其实数值属性的。这也可以提高网络的performance,其将class正则化到-1和1之间。注意的是这仅仅是内部的,输出会被转换回原始的范围。
reset
这将允许网络用一个更低的学习速率复位。如果网络偏离了答案其将会自动的用更低的学习速率复位并且重新训练。只有当GUI没有被set的时候这个选项才是available的。
注意的是如果这个网络偏离了并且没有被允许去reset其将在训练的步骤失败并且返回一个错误信息
seed
Seed用于初始化随机数的生成。随机数被用于设定节点之间连接的初始weights,并且用于shuffling训练集
trainingTime
训练的迭代次数。如果设置的是非0那么这个网络能够终止的比较早
validationSetSize
Validation set的百分比,训练将持续直到其观测到在validation set上的误差已经一直在变差,或者训练的时间已经到了
如果validation set设置的是0那么网络将一直训练直到达到迭代的次数
validationThreshold
用于终止validation testing。这个值用于决定在训练终止前在一行内的validation set error可以变差多少次
经过上面大家对于所有的参数有了一个大概的了解。
下面我们做一个简单的实验,也让大家有个直观的认识。
我们使用的是比较简单的Iris的数据集,其数据集简要描述如下:
“iris以鸢尾花的特征作为数据来源,数据集包含150个数据集,分为3类,每类50个数据,每个数据包含4个属性,是在数据挖掘、数据分类中非常常用的测试集、训练集
三类分别为:setosa, versicolor, virginica
数据包含4个独立的属性,这些属性变量测量植物的花朵,比如萼片和花瓣的长度等.”
使用orange canvas做出来的统计如下:
我们设置其为十次十折(cross-validation folds 10)
初始的网络如图
简单起见我们让其自动训练了,其分类结果的输出如下(篇幅起见只写出一部分):
=== Classifier model (full training set) ===
Sigmoid Node 0
Inputs Weights
Threshold -0.8317075956319961
Node 3 0.03364022925166845
Node 4 2.8158475746401312
Node 5 -2.9282184592731384
这以部分主要显示各个神经元的一些weight和threshold。可以看到这里对应的是右上角的黄色节点,其weight分布对应的node3 4 5是左边的红色隐层节点的weight
Sigmoid Node 3
Inputs Weights
Threshold 0.050063835158229715
Attrib sepallength -0.22160556669500697
Attrib sepalwidth -0.03621419764855014
Attrib petallength -0.3759501432512545
Attrib petalwidth -0.5363064140945599
这个显示的是对应的是左边绿色输入向量的weight
=== Stratified cross-validation ===
=== Summary ===
Correctly Classified Instances 146 97.3333 %
Incorrectly Classified Instances 4 2.6667 %
Kappa statistic 0.96
Mean absolute error 0.0327
Root mean squared error 0.1291
Relative absolute error 7.3555 %
Root relative squared error 27.3796 %
Total Number of Instances 150
这一部分显示的是其各个evaluation的指标,可以看到分类效果还是不错的。
后面还有一些TP FP 准确率 召回率 F-Measure和ROC Area等指标的展示,以及
=== Confusion Matrix ===
a b c <-- classified as
50 0 0 | a = Iris-setosa
0 48 2 | b = Iris-versicolor
0 2 48 | c = Iris-virginica
在这个矩阵我们可以看出Iris-versicolor Iris-virgca之间有些混淆
接下来我们就可以用上面的一些指标做优化分析了,不过这个和神经网络无关,会单独讲,在此就不再赘述。
附jacksonislwj整理的源代码
http://blog.csdn.net/jacksonislwj/article/details/6376449
贴在下面,大家有兴趣的可以好好到上面链接的博客研究下,也可以直接展开看
public void buildClassifier(Instances i) throws Exception { // 验证数据类型是否为算法所支持的类型 getCapabilities().testWithFail(i); // 读入数据集中的数据 i = new Instances(i); // 删除不完整的数据 i.deleteWithMissingClass(); // 如果只有类属性,转化为特殊处理 if (i.numAttributes() == 1) { System.err.println( "Cannot build model (only class attribute present in data!), " + "using ZeroR model instead!"); m_ZeroR = new weka.classifiers.rules.ZeroR(); m_ZeroR.buildClassifier(i); return; } else { m_ZeroR = null; } // 初始化遍历数据集的次数 m_epoch = 0; /** Shows the error of the epoch that the network just finished. */ m_error = 0; // 初始化训练集 m_instances = null; // 初始化正在经过神经网络的实例 m_currentInstance = null; // 初始化控制面板 m_controlPanel = null; // 初始化显示神经元的面板 m_nodePanel = null; // 初始化输出单元 m_outputs = new NeuralEnd[0]; // 初始化输入单元 m_inputs = new NeuralEnd[0]; // 初始化属性个数 m_numAttributes = 0; // 初始化类的分类数 m_numClasses = 0; // 初始化节点的连接逻辑 m_neuralNodes = new NeuralConnection[0]; // 是否需要停止神经网络 m_stopIt = true; // 神经网络是否已经停止 m_stopped = true; // 设置训练集 m_instances = new Instances(i); // 用一个随机化种子产生的随机数 m_random = new Random(m_randomSeed); // 重新排列数据集 m_instances.randomize(m_random); // 如果需要把数据集中的名词属性转化成二元属性 if (m_useNomToBin) { // 创建一个名词转二元属性的过滤器 m_nominalToBinaryFilter = new NominalToBinary(); // 使用过滤器过滤所有实例 m_nominalToBinaryFilter.setInputFormat(m_instances); m_instances = Filter.useFilter(m_instances,m_nominalToBinaryFilter); } // 属性的个数是数据集的属性个数减1,因为不包括类属性 m_numAttributes = m_instances.numAttributes() - 1; // 类的分类数是数据集类的分类数 m_numClasses = m_instances.numClasses(); // 归一化类属性到区间[-1,1] setClassType(m_instances); //初始化测试集 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); } /////////// // 创建输入单元 setupInputs(); // 创建输出单元 setupOutputs(); // 如果自动生成神经网络 if (m_autoBuild) { // 自动生成隐含层 setupHiddenLayer(); } // 如果类属性是数值属性 if (m_numeric) { setEndsToLinear(); } // 初始化向后传播误差 double right = 0; // 初始化停止继续重复训练的参数 double driftOff = 0; // 上次训练的向后传播误差 double lastRight = Double.POSITIVE_INFINITY; // 最小的向后传播误差 double bestError = Double.POSITIVE_INFINITY; // 加权学习率 double tempRate; // 训练集的总权重 double totalWeight = 0; // 测试集的总权重 double totalValWeight = 0; // 保存学习率的临时变量 double origRate = m_learningRate; //only used for when reset //确保至少有一个实例被训练 if (numInVal == m_instances.numInstances()) { numInVal--; } if (numInVal < 0) { numInVal = 0; } // 计算实例集的总权重 for (int noa = numInVal; noa < m_instances.numInstances(); noa++) { if (!m_instances.instance(noa).classIsMissing()) { totalWeight += m_instances.instance(noa).weight(); } } // 计算测试集的总权重 if (m_valSize != 0) { for (int noa = 0; noa < valSet.numInstances(); noa++) { if (!valSet.instance(noa).classIsMissing()) { totalValWeight += valSet.instance(noa).weight(); } } } m_stopped = false; // 遍历数据集m_numEpochs次 for (int noa = 1; noa < m_numEpochs + 1; noa++) { // 初始化向后传播误差 right = 0; // 遍历每个元组 for (int nob = numInVal; nob < m_instances.numInstances(); nob++){ // 取出当前的实例 m_currentInstance = m_instances.instance(nob); // 如果当前实例有类属性 if (!m_currentInstance.classIsMissing()) { // 重新初始化神经网络 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); } } // 计算向后传播的平均误差 right /= totalWeight; // 如果向后传播的平均误差超过了一定的范围或者是一个非法值 if (Double.isInfinite(right) || Double.isNaN(right)) { // 如果没有设置自动重置神经网络 if (!m_reset) { // 清空数据集 m_instances = null; // 抛出异常:因为学习率过大造成神经网络无法被训练,请调低学习率 throw new Exception("Network cannot train. Try restarting with a" + " smaller learning rate."); } // 如果设置了自动重置神经网络 else { // 如果学习率太小抛出异常 if (m_learningRate <= Utils.SMALL) throw new IllegalStateException( "Learning rate got too small (" + m_learningRate + " <= " + Utils.SMALL + ")!"); // 将学习率调整为当前的一半 m_learningRate /= 2; // 重新构建神经网络 buildClassifier(i); m_learningRate = origRate; m_instances = new Instances(m_instances, 0); return; } } // 用独立的测试集测试 // 如果测试集不为空 if (m_valSize != 0) { // 如果向后传播误差为0 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; // 储存当前的一组权值 for (int noc = 0; noc < m_numClasses; noc++) { m_outputs[noc].saveWeights(); } // 初始化不更新权值的次数 driftOff = 0; } } // 不更新权值的次数加一 else { driftOff++; } // 最后一次更新的权值为当前权值 lastRight = right; // 如果权值超过了一定次数(初始20次)没有被更新或者实例已经达到被要求遍历的次数 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; } // 保存实际遍历实例集的次数 m_epoch = noa; // 保存向后传播错误率 m_error = right; } } // 自动生成隐含层的策略 private void setupHiddenLayer() { // 新建一个字符串拆分的实例 StringTokenizer tok = new StringTokenizer(m_hiddenLayers, ","); // 每层隐含层的节点数 int val = 0; // 上一层隐含层的节点数 int prev = 0; // 隐含层数 int num = tok.countTokens(); String c; // 对于每层隐含层 for (int noa = 0; noa < num; noa++) { // 得到该层的节点分布信息 c = tok.nextToken().trim(); // 如果是“a”代表的信息,该层的节点数为所有属性总数的一半(默认值) if (c.equals("a")) { val = (m_numAttributes + m_numClasses) / 2; } // 如果是“i”代表的信息,即输入单元,该层的节点数为所有非类属性个数 else if (c.equals("i")) { val = m_numAttributes; } // 如果是“o”代表的信息,即输出口,该层节点数为所有类属性个数 else if (c.equals("o")) { val = m_numClasses; } // 如果是“t”代表的信息,该层节点为所有属性总数 else if (c.equals("t")) { val = m_numAttributes + m_numClasses; } // 否则为分布信息为其他值,那么节点数为分布信息的字符串转化成浮点数再取整所得到的数为该层节点数 else { val = Double.valueOf(c).intValue(); } // 对于当前隐含层的每个节点 for (int nob = 0; nob < val; nob++) { // 新建节点 NeuralNode temp = new NeuralNode(String.valueOf(m_nextId), m_random,m_sigmoidUnit); // 节点编号增加 m_nextId++; // 计算节点坐标并添加节点 temp.setX(.5 / (num) * noa + .25); temp.setY((nob + 1.0) / (val + 1)); addNode(temp); // 如果不是第一层隐含层 if (noa > 0) { //将当前隐含层中的每个节点与上一层相连 for (int noc = m_neuralNodes.length - nob - 1 - prev; noc < m_neuralNodes.length - nob - 1; noc++) { NeuralConnection.connect(m_neuralNodes[noc], temp); } } } // 保存当前隐含层的节点数下次循环用 prev = val; } // 同上 tok = new StringTokenizer(m_hiddenLayers, ","); // 这次没有省去头和尾,也就是说包含了所有的输入单元和输出单元 c = tok.nextToken(); if (c.equals("a")) { val = (m_numAttributes + m_numClasses) / 2; } else if (c.equals("i")) { val = m_numAttributes; } else if (c.equals("o")) { val = m_numClasses; } else if (c.equals("t")) { val = m_numAttributes + m_numClasses; } else { val = Double.valueOf(c).intValue(); } // 如果没有隐含层 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]); } } } } // 计算某单元的输出值 public double outputValue(boolean calculate) { // 如果该单元之前没有被计算过且已经做好了计算的准备 if (Double.isNaN(m_unitValue) && calculate) { // 如果是输入单元 if (m_input) { // 如果没有与其他单元相连接,输出为0 if (m_currentInstance.isMissing(m_link)) { m_unitValue = 0; } // 如果与其他单元相连接,输出值为与之相连接的单元的值 else { m_unitValue = m_currentInstance.value(m_link); } } // 如果不是输出单元 else { // 初始化输出值 m_unitValue = 0; // 输出值为连接到此单元的输出的加权和 for (int noa = 0; noa < m_numInputs; noa++) { m_unitValue += m_inputList[noa].outputValue(true); } // 如果类属性是数值属性,且需要进行正常化,那么正常化到[-1,1]区间 if (m_numeric && m_normalizeClass) { m_unitValue = m_unitValue * m_attributeRanges[m_instances.classIndex()] + m_attributeBases[m_instances.classIndex()]; } } } return m_unitValue; }