2.4 用代码实现一个感知机
了解感知机的运作原理之后,如果你是程序员或者你习惯用代码去表示,那么现在就可以动手试试了!我们挑一个受众面比较广的语言作为基础语言——Java,构建一个感知机。
2.4.1 Neuroph:一个基于Java的神经网络框架
在整个第2章中,我们都将用Neuroph作为基础框架,实现本书提及的神经网络和相关案例。
下面,我们简单认识一下Neuroph神经网络的框架。Neuroph是一个轻量级的开源Java神经网络框架,结构简单清晰,非常适合初学者入门,随着深入学习,可以慢慢了解Neuroph不只是初学者的入门工具,它可以简化神经网络的开发和实现,也能实现一些用在生产环境中的算法,图2-5所示为它的基本类库。
图2-5 Neuroph神经网络的框架
Neuroph由两部分组成。一部分是由基于Java开发的API组成,你可以方便地利用这部分API创建神经网络。另一部分是图形工具,能直接通过简单的图形化工具构造一个神经网络,帮助我们构造多层神经网络时更加快捷方便。
Java API部分分成三块,一块是neuroph.core,这是Neuroph的核心库,如表2-3所示。
表2-3 Neuroph类库说明
图2-6所示最简化地表示了Neuroph是如何工作的。
图2-6 Neuroph是如何工作的
(1)神经网络和学习规则对应,神经网络按照一定的学习规则训练相应的数据集。
(2)神经网络由基础的层(layer)组成,按照结构分为输入层、隐藏层和输出层,我们讲神经网络的时候会具体谈到各层的概念。
(3)神经网络的每层由最基础的神经元组成。
(4)训练规则包含一个训练集,允许有多个训练集,训练集由单个训练元素组成。
接下来,我们看看Neuroph中的类Neuron,它表示单个神经元的构造。如图2-7所示,这里没有罗列出Neuron所有的属性和方法,只展示了与本节相关的部分。
图2-7 类Neuron
其中inputConnections表示神经元的输入连接。比如,一个输入刺激到神经元,就构成一条输入。由于神经元是可以有多个输入的,所以神经元和输入连接是一对多的关系。netInput表示净输入,输入函数的输出。output表示神经元的输入。error为神经元的误差。inputFunction表示输入函数,通常选择加权求和。transferFunction表示传输函数。
另一个重要的类为Connection。它表示神经元的连接,可以是两个神经元之间的连接,也可以是输入信号与神经元之间的连接(实际上,输入信号可以看作一个输出永远等于输入的简单神经元), weight表示这个连接的权重。
这个结构已经与之前所述的感知机原理很接近了,下面我们就来构造一个感知机。
2.4.2 代码实现感知机
回到本节的主题,我们用代码实现一个感知机,以便更加深入地了解感知机的构造。你也可以选择跳过所有与代码相关的章节,不影响阅读。
首先,我们先想想要构建一个什么样的感知机:我们将神经元输入链接个数、神经元输出链接个数和传输函数类型作为输入的参数,如图2-8所示。
图2-8 感知机
private void createNetwork(int inputNeuronsCount, int outputNeuronsCount, TransferFunctionType transferFunctionType) { //设置神经网络类型,这里我们将类型设置为感知器 this.setNetworkType(NeuralNetworkType.PERCEPTRON); //初始化神经元输入刺激设置 NeuronProperties inputNeuronProperties = new NeuronProperties(); inputNeuronProperties.setProperty("transferFunction", TransferFunctionType.LINEAR); //创建输入刺激 Layer inputLayer = LayerFactory.createLayer(inputNeuronsCount, inputNeuronProperties); this.addLayer(inputLayer); NeuronProperties outputNeuronProperties = new NeuronProperties(); outputNeuronProperties.setProperty("neuronType", ThresholdNeuron. class); outputNeuronProperties.setProperty("thresh", new Double(Math.abs (Math.random()))); outputNeuronProperties.setProperty("transferFunction", transferFunctionType); //为sigmoid和tanh传输函数设置斜率属性 outputNeuronProperties.setProperty("transferFunction.slope", new Double(1)); //create一个神经元的输出 Layer outputLayer = LayerFactory.createLayer(outputNeuronsCount, outputNeuronProperties); this.addLayer(outputLayer); //在输入和输出层中建立全链接 ConnectionFactory.fullConnect(inputLayer, outputLayer); //为神经网络设置默认输入输出 NeuralNetworkFactory.setDefaultIO(this); this.setLearningRule(new BinaryDeltaRule()); }
至此,一个简单的感知机已经建立,调用这个感知机可以处理诸如水果分类之类的简单问题。我们将训练这个神经网络,让它具有记忆和解决简单问题的能力。
2.4.3 感知机学习一个简单逻辑运算
我们已经建立了一个简单的感知机,本节中我们将训练这个感知机,并让学习逻辑运算AND。
首先,我们列出逻辑运算AND中的基本规则:
0 and 0=0
0 and 1=0
1 and 0=0
1 and 1=1
也就是说,在感知机接收0、0输入时,感知机应该响应1;当接收1、1输入时,应该响应1。当接收0、1或1、0时,应该输出0。AND主要用在条件判断中,也就是当两个子条件都成立时,才往下进行,两个子条件就用AND连接。
我们先给出完整代码,再顺着代码的脉络看看感知机是如何学会AND运算的。
public static void main(String args[]) { //建立训练集,有两个输入一个输出 DataSet trainingSet = new DataSet(2, 1); trainingSet.addRow(new DataSetRow(new double[]{0, 0}, new double[]{0})); trainingSet.addRow(new DataSetRow(new double[]{0, 1}, new double[]{0})); trainingSet.addRow(new DataSetRow(new double[]{1, 0}, new double[]{0})); trainingSet.addRow(new DataSetRow(new double[]{1, 1}, new double[]{1})); //建立一个感知机,定义输入刺激是2个,感知机输出是1个,这里我们调用Neuroph提供 的Perceptron类。 NeuralNetwork myPerceptron = new Perceptron(2, 1); LearningRule lr =myPerceptron.getLearningRule(); lr.addListener(this); //开始学习训练集 myPerceptron.learn(trainingSet); //测试感知机是否正确输出,打印 System.out.println("Testing trained perceptron"); testNeuralNetwork(myPerceptron, trainingSet); }
我们运行程序,结果如下:
[main] INFO org.neuroph.core.learning.LearningRule - Learning Started 1. iterate 2. iterate 3. iterate 4. iterate 5. iterate [main] INFO org.neuroph.core.learning.LearningRule - Learning Stoped 5. iterate Testing trained perceptron Input: [0.0, 0.0] Output: [0.0] Input: [0.0, 1.0] Output: [0.0] Input: [1.0, 0.0] Output: [0.0] Input: [1.0, 1.0] Output: [1.0]
结果输出表明,网络经过5次迭代后,误差为0,初始的权值和偏置是随机数,接着网络根据输入的训练数据迭代学习,不断调整权值和偏置,并最终记忆AND逻辑运算。从测试数据中可以看到,4个输出已经完全正确,该网络已经对AND逻辑操作有正确的响应。
同理,只需要改变训练数据,就可以让感知机记忆其他内容,比如OR逻辑运算。
0 or 0=0
0 or 1=1
1 or 0=1
1 or 1=1
我们依据OR逻辑运算的规则,更改上例的训练数据:
trainingSet.addRow(new DataSetRow(new double[]{0, 0}, new double[]{0})); trainingSet.addRow(new DataSetRow(new double[]{0, 1}, new double[]{1})); trainingSet.addRow(new DataSetRow(new double[]{1, 0}, new double[]{1})); trainingSet.addRow(new DataSetRow(new double[]{1, 1}, new double[]{1}));
然后测试该网络,给出的输出可能为:
[main] INFO org.neuroph.core.learning.LearningRule - Learning Started 1. iterate 2. iterate 3. iterate 4. iterate 5. iterate 6. iterate 7. iterate 8. iterate 9. iterate 10. iterate 11. iterate 12. iterate [main] INFO org.neuroph.core.learning.LearningRule - Learning Stoped 12. iterate Testing trained perceptron Input: [0.0, 0.0] Output: [0.0] Input: [0.0, 1.0] Output: [1.0] Input: [1.0, 0.0] Output: [1.0] Input: [1.0, 1.0] Output: [1.0]
可以看到,经过12次迭代,网络已经正确记忆了OR逻辑运算。到这里,细心的读者可以发现,在学习AND逻辑运算的时候,网络进行了5次迭代,但在学习OR逻辑运算的时候迭代了12次。这种结果是完全随机的,实际上在网络建立的时候,初始权值是随机产生的(绝对值小于1),因此迭代几次才能使网络给出正确输出是不确定的,但不论初始值如何,经过有限次迭代,网络一定能完全记住训练数据。
2.4.4 XOR问题
大家还记得罗森布拉特吗?他是我们在第0章提到的那位神经网络中感知机奠基人之一,并且还是利用计算机模拟了感知机模型的天才,他是怎么被自己的中学同学明斯基找到感知机漏洞,郁闷至死的呢?对!就是因为XOR问题,我们来看看XOR能不能在我们的感知机模型中得到解决。
老规矩,先列出XOR的运算规则:
0 xor 0=0
0 xor 1=1
1 xor 0=1
1 xor 1=0
可以看到,当输入的两个值相等时,XOR返回0,否则得到1。
依然使用给出的感知机学习XOR会得到什么结果呢?感知机还可以正常工作吗?
我们将训练数据替换成如下代码:
trainingSet.addRow(new DataSetRow(new double[]{0, 0}, new double[]{0})); trainingSet.addRow(new DataSetRow(new double[]{0, 1}, new double[]{1})); trainingSet.addRow(new DataSetRow(new double[]{1, 0}, new double[]{1})); trainingSet.addRow(new DataSetRow(new double[]{1, 1}, new double[]{0}));
再来运行一下程序:
…... 573884. iterate 573885. iterate 573886. iterate 573887. iterate 573888. iterate 573889. iterate 573890. iterate 573891. iterate 573892. iterate 573893. iterate 573894. iterate ……
我暂停了,已经计算了60w次仍然没有得到结果。
如果不手动终止程序,它将永远运行下去。读者如果执行此程序,一定也会得到一样的结果。这是什么原因呢?为什么感知机可以轻松地记忆AND和OR运算,但是无法学习XOR呢?
先不解答此问题,我们先来构造一个神经网络,大家且往下看。