首页 > 代码库 > 柜位预测(二)——神经网络-FANN库
柜位预测(二)——神经网络-FANN库
可以使用最小二乘法来进行柜位预测,但是其预测的时间短,不能很好的用于实践当中。在查询了一些资料后,神经网络是解决该问题的最好的方法。神经网络是什么,我们应该如何使用它来完成功能,我会在本篇和后续的文章中逐一介绍自己所学的心得。本篇是翻译文章,对神经网络库——FANN库进行了介绍。本人英语水平有限,有翻译不当的地方望指出。
本篇译文的原文可以从此下载。
简化神经网络
多年前,好莱坞科幻片《机械公敌》中就描述了人工神经网络预测了世界末日。然而,其却脱离了现实。当好莱坞通过人类即将灭亡的恐怖故事来吸引我们的眼球时,对我们人类灭绝不感兴趣的人们已经开始使用人工智能(AI)使我们的生活更简单、更有效、更好。
在《机械公敌》中的机器人以人工神经网络为基础,拥有了一颗人类的大脑。人工神经网络以模拟人类大脑的神经网而建立。快速人工神经网络库(FANN)尽管不能创造出像好莱坞的那种魔术,但是其实能被C、C++、PHP、Pytho、Delphi和Mathematica等语言调用的一种人工神经网络库。它对于开发人员来说是一款强大的工具。人工神经网络能被用于很多方面,如创造出更好玩的电脑游戏、图像识别及帮助股民预测股票市场的走势等。
函数逼近
人工神经网络采用实例函数逼近的原则。他们通过函数的例子去学习这个函数。一个简单示例是人工神经网络(ANN)学习异或函数。它能很容易的去学习决定文本使用何种语言,也可以从一张X光片中找出肿瘤。
如果人工神经网络(ANN)能够学习问题,那么它一定被定以为拥有大量函数工作示例的一组包含输入输出变量的一个函数。一个问题就像异或函数一样已经被定义为有两个二进制输入变量和一个二进制输出的变量的一个函数。这是由四种输入模式定义的示例。然而,有很多复杂的很难被定为函数的问题。在X光片中找到肿瘤这个问题的输入变量是整个光片的像素值,其实它们也是一些从光片中抽取的值。输出既可以是一个二进制的值也可以是个浮点数来表示光片中肿瘤的可能性。在神经网络中,浮点值一般在包含0,1在内的区间中。
人类大脑
当谈论到FANN时,一个函数逼近像人工神经网络一样可以被看做是个黑盒子。这或多或少是你应该知道的。然而,人类大脑工作的基本方式是理解人工神经网络如何工作的必要的条件。
人类大脑是一个可以解决复杂问题的高度复杂的系统。人类大脑由不同的元素组成。但是其最重要的构建之一是神经元,大约包含1011个神经元。这些神经元通过1015的连通路径连通,创造了一个巨大的神经网络。神经元通过连接发送脉冲给其他的神经元,这些脉冲使大脑工作。神经网络从五种感知上接收脉冲和发送脉冲来使肌肉完成动作或者速度。
每个神经元可以看做是个输入输出的机器,其可以等待接收从周围神经元发送的脉冲,当其接收到足够的脉冲后,它可以向其他的神经元发送脉冲。
人工神经网络
人工神经元类似于生物统计。它们有输入连接,其可以加在一起决定他们的输出强度,其也是一个被送入激活函数和的结果。尽管有很多的激活函数,最常见的是S型的激活函数,其输出一个在0(不包含)到1(不包含)之间的数。这个函数的结果是由其他神经元不同权重的连接方式输入决定的。这些权重决定了网络的行为。
在人类的大脑中,神经元以看上去随意的顺序连接和发送不同步的脉冲信号。如果我们想模拟一个大脑,那么编织一个神经网络是种方式。由于我们起初创建的是逼近函数,而人工神经网络通常不是这样组织的。
当我们创建人工神经网络时,神经元通常被排序在各层连接的层次中。第一层包含输入神经元,最后一层包含输出神经元。这些输入和输出神经元代表了我们逼近函数的输入输出变量。在输入输出层间有一组隐藏层存在。来自这些隐藏层的连接(或者权重)决定了人工神经网络的行为。当神经网络学着去逼近一个函数时,它展示了这个函数如何工作的示例,人工神经网络中的内部权重被慢慢调整到可以输出与例子中的输出值一样的输出值。
最终的期望是给人工神经网络一组新的输入变量时,它可以给出正确的输出。因此,如果人工神经网络被期望去学习识别出X光片中的肿瘤时,那么它需要大量的包含肿瘤的X光片和大量的包含正常细胞的X光片。在这些图像中学习一段时间后,人工神经网络中的权重值就包含了这样的信息,即可以主动鉴别出X光片中当初学习过程中不能被看到的肿瘤信息。
FANN库教程
互联网已成为人们生活中的通信手段,不同语种的交流是互联网通信的一个问题。翻译工具可以在此沟壑上架起一座桥梁。为了使用翻译工具工作,需要知道文本采用的是何种语言。一种方法是判断在这段文本中单词出现的频率。虽然其看起来是很简单的语言检测方法,却是很有效的一种方法。对于多数欧洲语种,尽管一些语种包含其他的字符,但是通过A到Z的频率已经足矣判断。FANN库可以很容易的被使用去开发一个判断文本语种的小程序。这个人工神经网络有26个字符的输入神经元和一个语种的输出神经元。首先,这个小程序应该去检测文本的字符频率。清单1:编程计算在文本中A~Z的频率 #include <vector> #include <fstream> #include <iostream> #include <ctype.h> void error(const char* p, const char* p2 = "") { std::cerr << p << ' ' << p2 << std::endl; std::exit(1); } void generate_frequencies(const char *fi lename,float *frequencies) { std::ifstream infi le(fi lename); if(!infi le) error("Cannot open input fi le", fi lename); std::vector<unsigned int> letter_count(26, 0); unsigned int num_characters = 0; char c; while(infi le.get(c)){ c = tolower(c); if(c >= 'a' && c <= 'z'){ letter_count[c - 'a']++; num_characters++; } } if(!infi le.eof()) error("Something strange happened"); for(unsigned int i = 0; i != 26; i++){ frequencies[i] = letter_count[i]/(double)num_characters; } } int main(int argc, char* argv[]) { if(argc != 2) error("Remember to specify an input fi le"); fl oat frequencies[26]; generate_frequencies(argv[1], frequencies); for(unsigned int i = 0; i != 26; i++){ std::cout << frequencies[i] << ' '; } std::cout << std::endl; return 0; }
清单1 展示了文件中的字符频率,同时格式化输出到了一个文件中,这个文件被用来作为FANN库的学习文件。FANN库的学习文件必须由一行输入接一行输出组成。如果我们期望去鉴别3种不同的语言(英语、法语和波兰语),我们让输出值0表示英语、0.5表示法语、1表示波兰语。然而如果输出变量1表示正确的语言,0表示其他的语言,那边神经网络将工作的更好。
清单2:英语、法语、波兰语字符频率学习文件的开始部分,第一行表示由26种输入和3种输出组成的12种的学习模式 12 26 3 0.103 0.016 0.054 0.060 0.113 0.010 0.010 0.048 0.056 0.003 0.010 0.035 0.014 0.065 0.075 0.013 0.000 0.051 0.083 0.111 0.030 0.008 0.019 0.000 0.016 0.000 1 0 0 0.076 0.010 0.022 0.039 0.151 0.013 0.009 0.009 0.081 0.001 0.000 0.058 0.024 0.074 0.061 0.030 0.011 0.069 0.100 0.074 0.059 0.015 0.000 0.009 0.003 0.003 0 1 0 0.088 0.016 0.030 0.034 0.089 0.004 0.011 0.023 0.071 0.032 0.030 0.025 0.047 0.058 0.093 0.040 0.000 0.062 0.044 0.035 0.039 0.002 0.044 0.000 0.037 0.046 0 0 1 ...
通过这个小程序,可以得到一份不同语言的字符使用频率的学习文件。如果有更多频率的资源在学习文件中,那么人工神经网络将能更好的甄别出语种来。但是对于这个小示例来说,每种语言的3到4个文本已经足够了。清单2显示了对于每种语言的4个文本预先产生的学习文件。表2为文件中的频率表示图。这个文件显示出了趋势:英语比其他两种语言有更多的H,法语则基本没有K,而波兰语比其他两种语言有更多的W和Z。这个学习文件仅仅使用了A到Z的范围的字母,但是像波兰语这样的语言中还包含?, ?和?这样的在其他两种语言中不被使用的字符。更精准的人工神经网络通过添加这样的神经元将工作的更好。当比较这三种语言时,既然普遍使用的字符已可以鉴别出正确的语言来,那么则不需要把这些特殊字符再添加进来。但是让人工神经网络去鉴别上百种不同的语种时,更多的字符将被需要。
清单3:一段辨别语种的神经网络程序 #include "fann.h" int main() { struct fann *ann = fann_create(1, 0.7, 3, 26, 13, 3); fann_train_on_fi le(ann, "frequencies.data", 200, 10, 0.0001); fann_save(ann, "language_classify.net"); fann_destroy(ann); return 0; }
像这样一个学习文件,采用FANN则很容易创建出来一个人工神经网络来鉴别这3种语言的程序的。清单3显示了使用FANN做这件事是多么的简单。这个程序使用了FANN函数fann_create、fann_train_on_file、fann_save和fann_destory.这个函数struct fann* fann_create(float connection_rate,float learning_rate,unsigned int num_layers,...)被用来创建人工神经网络。参数connection_rate表示创建一个不充分连接的人工神经网络,尽管完全连接的神经网络普遍更提倡采用。参数learning_rate被用来规定如何如何积极的学习算法(一些算法的相关性)。最后一个参数别用来定义人工神经网络中的层次布局。在这个示例中,一个拥有3个层(1个输入层,1个隐藏层和1个输出层)的神经网络被选择。这个输入层有26个神经元(每个字符为一个神经元),输出层有3个神经元(每种语言一个),隐藏层有13个神经元。层的个数以及神经元的个数在隐藏层是已选定的研究结果,并且没有相关的方法去修改这些值。通过调整权重有利于记住神经网络的结果,因此如果一个人工神经网络有更多的神经元,那么它学习复杂的问题会有更多的权重。拥有太多的权重也是一个问题,由于学习起来是困难的,那么人工神经网络将代替推理出其他的数据配置的一般模式学习输入变量的一种特殊的特性。工神经网络能够准确的判断出不在学习集中的数据,这个能力是至关重要的,没有这种能力,人工神经网络将不能分炼出没有被学习过的频率。
void fan_train_on _file(struct fann *ann,char*filename,unsigned int max_epochs,unsigned int epochs_between_reports,floatdesired_error)函数学习人工神经网络。这种学习是不断的调整权重来完成的,以至于人工神经网络的输出能与学习文件中的输出匹配。调整权重去匹配学习文件中的输出的一轮循环被称为一个轮询。在这个例子中轮询的最大数量被设置为200,一个状态报告是每10个轮询打印一次。当测量人工神经网络配置的输出有多准确,那么均方差常被使用。对应个别的学习模式,均方差是指真实值与人工神经网络输出值之间的方差的平均值。一个小的均方差意味着是一项符合期望值的匹配。
清单4:在学习过程中FANN的输出 Max epochs 200. Desired error: 0.0001000000 Epochs 1. Current error: 0.7464869022 Epochs 10. Current error: 0.7226278782 Epochs 20. Current error: 0.6682052612 Epochs 30. Current error: 0.6573708057 Epochs 40. Current error: 0.5314316154 Epochs 50. Current error: 0.0589125119 Epochs 57. Current error: 0.0000702030
当在清单2程序运行的时候,人工神经网络将被学习,一些状态信息(请看清单4)将会输出,这样可以在学习的过程中很容易的监控执行情况。在学习后,人工神经网络被直接用来判断文本是由哪种语言所编写的。但是通常期望是在两种不同的程序中一直保持学习和执行,所以更耗时的学习需要被一次完成。出于此原因,清单2简单地将人工神经网络集保存到了一个可以被其他程序调用的文件中。
清单5:辨识被三种语种之一的语言写的文本程序(程序使用了一些在清单1中定义的函数) int main(int argc, char* argv[]) { if(argc != 2) error("Remember to specify an input fi le"); struct fann *ann = fann_create_from_fi le("language_classify.net"); fl oat frequencies[26]; generate_frequencies(argv[1], frequencies); fl oat *output = fann_run(ann, frequencies); std::cout << "English: " << output[0] << std::endl << "French : " << output[1] << std::endl << "Polish : " << output[2] << std::endl; return 0; }
在清单5中的小程序加载了保存的这个人工神经网络的文件,并且使用它去分辨文本是用英语写的,还是法语写的,还是波兰语写的。当测试在互联网上的这三种语种的文本时,它能通过较少的句子更快的辨别出文本来。虽然在语言中的这种辨别的方式是不具备超可靠性的,但是我还没有找到哪个文本被误分类的。
FANN库:细节
语种分类的例子仅展示了FANN库能很容易的被用于解决简单但是使用其他方法解决很困难的计算机科学的问题。但是并不是所有的问题都能被如此轻易的解决,当人工神经网络常发现自己在同一情况下工作时,那么神经网络很难给出正确的结果的。有时候,这个问题是由于不能被人工神经网络简单的解决,但是常常是因为调整FANN库设置不能起到作用的原因。
当学习一个神经网络时一个最重要的因素是这个神经网络的大小。这个问题通常需要经过实验来解决,但是通常对问题的理解会给出更好的猜测。有了一个合理的尺寸,学习可以通过各种不同的方式来完成。FANN库支持不同的学习算法,默认的算法(FANN_TRAIN_RPROP)对于一个特殊的问题并不是最适合的。对于这种情况,fann_set_training_algorithm函数可以改变学习时的算法。在FANN库1.2.0版本中有4种不同的可以使用的算法。所有这些都使用了反向传播。在调整权重时,反向传播算法通过传播从输出层到输入层的误差来调整权重。反向传播错误值既可以是一个简单学习模式(增量)计算出的误差,也可以是来自全部学习文件(批量)的一个误差的总数。FANN_TRAIN_INCREMENTAL实现了一个增量学习算法,在每次学习模式后可以调整权重。这个算法的好处是在每个轮询中权重被改变了多次,这样每个学习模式在略有不同的场合下对权重做了修改。即使方案还没被找到,学习也不会卡死在最小状态下,在此状态下稍微对权重微小的改变都将导致大的误差。FANN_TRAIN_BATCH,FANN_TRAIN_RPROP和FANN_TRAIN_QUICKPROP是批量学习算法的示例,这个算法为全部的学习设置计算错误数后调整权重值。这个算法的好处是它能使用全局优化的信息,这些信息对于增量算法是不可使用的。然而,这也意味着单独学习模式中的一些微小的点会被忽视。哪种学习算法最好是没有一个准确的答案的。高级批量学习算法,像rprop或者quickprop学习,其中一个通常都是一个好的解决方案。然而,有时候增量学习会更出色,尤其是很多学习模式都可以采用的时候。在语种学习示例中,最优的算法是默认的算法-rprop。这个可以在仅仅57次学习完成后达到期望的均方差值。增量算法则需要8108次才能达到同样的结果,然而增量算法则需要91985次。qiuckprop算法有更多的问题,起初不能达到期望的误差值,但是在调整了quickprop算法的衰减度时,在通过662次轮询后达到了期望的差值。quickprop算法的衰减度是一个被用于控制quickprop算法积极性的一个参数,它通过fann_set_quickprop_decay函数设置。其他的fann_set_...函数也能被用于设置个别的学习算法的额外参数值,尽管在没有个别算法工作机制知识的前提下,一些参数的难调整有些难。
代表学习算法独立性的参数可以很容易的调整激活函数的陡度。激活函数是判断结果为0或是1的一个函数。这个函数的陡度代表从0变到1的这个过程的快慢。如果陡度被设置为一个大值,学习算法将会很快的在0、1两个极端收敛,这个将会使学习更快速,如语种鉴别问题的例子。然而,如果将陡度设置为小值,那么对于需要分数输出的人工神经网络的学习则是容易的,如寻找图片中线的方向的列子。对于设置激活函数陡度的方法,FANN提供了2个函数:fann_set_activation_steepness_hidden 和fann_set_activation_steepness_output。因为常常期望对于隐藏层和输出层有不同的陡度,所以是这些函数组。
FANN的可能值
语种辨识问题属于熟知的鉴别问题中的一种特殊的逼近函数的问题。辨别问题的每个辨别神经元都有一个输出的神经元,在每种学习模式中这些输出的精确的值必须是1。一般的逼近函数输出值都是一个分数值。例如通过摄像头拍摄一个实体的举例或者是生活消耗。这些问题都可以与辨识问题联系。因此这些都是一个近似分类的问题。通常都可以通过一个神经网络来解决。有些时候让两个问题独立开来解决是个好的想法。例如让一个神经网络辨别实体,让另一个神经网络用于辨识每个不同对象的近似距离。
另一种逼近问题是时间序列问题。随着时间推移的一个近似的函数。熟知的时间序列问题是在观察一年的历史数据后,预测黑子数量。黑子问题一般是让X值作为输入,Y值作为输出。但是这不是解决该问题的最好的方法。时间序列法可以使用一断时间作为输入,来计算出下一时间的输出。如果这个时间被设置为10年,那么时间神经网络需要用这十年的数据来学习,即使用1995到2004年的数据估算出2005年的太阳黑子的数量。这种方法表示历史数据集被用在了一些学习模式中。预测1981到1990年的太阳黑子数,需要使用1980年的数据来学习。这表示没有2005到2009年的数据,2010年的预测的太阳的黑子数将不是准确的。这也就是说2010年的计算数据有一半是近似值,那么计算出的2010年的黑子数量将不如2005年的准确。正是由于这个原因,时间序列法在预测将来的问题上不是一种很好的方法。
时间序列法可以被用于表示机器人控制器的记忆等。例如不考虑来自传感器或者摄像头的数据,那么给出最近两个时间点的方向和速度,可以计算出下一时刻的行为。由于每种学习模式都需要历史数据,那么这种方法的关键问题是学习的数据是很难被创造出来的。
FANN小技巧
下面的小技巧可以使FANN学习和执行的更快、更精确。使FANN学习的更快更轻巧的一个简单的技巧是采用-1到1之间而不是0到1之间的输入输出值。变换学习文件中的值是可以被采用的,使用fann_set_activation_function_hidden、fann_set_activation_function_output改变激活功能值为FANN_SIGMOID_SYMETRIC,这个值可以使输出在-1到1之间。这个小技巧的原理是0值在神经网络中都是一个悲观的值,即不管权重是多少,输出都是0。为了避免这个情况变成一个大问题,在FANN中这是一种很合理的对策。同时这个小技巧也可以使学习时间减少。fann_set_activation_function_output改变激活功能为FANN_LINEAR,这个表示无穷。因此可以用来创建有任意输出值的神经网络。
在神经网络学习的时候,辨识学习的时间是件难事儿。如果太多的时间用于学习,那么神经网络将不能够辨识学习的数据。然而,如果太多的迭代使用,那么神经网络将由于太关注数据的精确度而不能很好的辨识学习过程中遇到的新的数据。在学习过程中一个应用用于辨识,一个应用用于检测在遇到新数据时的神经网络的准确性。这种情况可以使用fann_test_data函数。通过使用这些函数可以掌握和操作学习的数据了。
将问题分解到可以被神经网络学习的函数上是一项艰巨的任务。下面将对此给出一些建议:
- 对于每个信息单元使用至少一个输入、一个输出。在语种辨识问题中,每个字符都有一个输入,每种语言都有一个输出。
- 从编程人员角度来说,当选择神经元时会出现以下问题。例如如果你知道在语种辨识中单词的长度也是一项重要的标准,那么你应该添加一个单词长度的神经元(这个可以通过添加一个空格频率的输入神经元来完成)。而且,如果你了解到一些字符可以被用于一些语言中的话,那么应该添加一个额外的输入神经元,这个神经元使用1表示在文本中存在,使用0表示在文本中不存在。通过这种方式,一个简单的波兰语字符在文本中就可以辨识出文本的语种。也许你知道一些语言比其他的语言包含更多的元音,那么你可以添加一个额外的表示元音频率的输入神经元。
- 简化问题。如果你想使用神经网络去检测图片中的一些特性,那么为了使问题更容易解决,简化图片是个好的方法。因为原始的图片中包含了太多的信息,对于神经网络过滤出有用的信息是很难的。在图像中,可以应用平滑处理滤波器、边缘检测滤波器等来完成简化。其他的问题则可以预先通过移除非必要的数据来完成简化。可以分割一个神经网络到容易解决的问题上来文成简化。例如在两种语言使用的不同区域下,可以辨识出欧洲语言和亚洲语言。
神经网络通常是耗时的。尤其是在神经网络需要每秒执行上百次或者是神经网络十分庞大,时间常常是执行的关键。出于这个原因,需要采取一些措施使FANN库比现有的执行的更快。一种方法是通过精度降低来改变激活性能,使其可以更快达到线性化的性能,则我们可以减少一些隐藏神经元的数量或者减少执行时间。另外一种方法是仅仅使用整数来执行,这个方法仅在没有浮点数计算的嵌入式系统上有效。FANN库拥有一些可以使其执行在仅整数的情况下的辅助函数。没有浮点数计算的处理器拥有比其他强50倍的性能。
开源世界的故事
我首次在2003年11月份发布FANN库1.0版本的时候,我不清楚它有何反响,但是我期望的是更多的人有机会使用我创建的这个新库。令我喜出望外的是,人们确实开始下载和使用这个库了,而且越来越多的人使用。这个库也从一开始仅支持Linux平台,发展为支持主流处理器和操作系统(包括MSVC++、Borland C++)。这个库在功能上也开始考虑扩展。很多用户也开始为这个库做扩展。很快这个库可以支持PHP、Python、Delphi和Mathematica了,这个库也被Debian Linux分发系统包含。
我很乐意将我的空余时间花费在FANN库和这个库的使用者上。FANN库让我有机会为开源社区提供一份自己的力量,可以给我机会帮助更多的人,我乐此不彼。
我不敢说开发开源软件是每个开发人员应该做的事,但是我想说这个给了我满足感。因此如果你也有同感的话,那么使用开源工程,并且为此奉献一份力量。如果你能开启了一项开源项目,这将会更好。
附表:
表1:用于四个输入神经元、一个隐藏层和四个输出神经元的人工神经网络
表2:英语、法语、波兰语字母频率图表
表3:陡度值为0.25、0.50、1.00的S型激活函数
人工智能
什么时候一些东西很智能或者一些人很聪明?狗是聪明的吗?新生儿呢?一般,我们把聪明(智能)定义为学习和应用知识、推理和创造力的的能力。如果我们用这样的标准定义人工智能的话,那么目前来说还没有完全的人工智能。然而,一般人工智能被定义为一种与人类大脑相关的一种表现能力。因此,人工智能被用于描述处理学习和应用人类知识的能力。从一个简单的象棋游戏机到者游戏机中的一个字符,这个定义使人工智能都能对其描述。
在线资源
FNN库:http://fann.sourceforge.net
Martin riedmillerand Heinrich Braun,快速反向传播学习的直接适应算法:RPROP算法:
http://citeseer.ist.psu.edu/riedmiller93direct.html
神经网络问答:ftp://ftp.sas.com/pub/neural/FAQ.html
Steffen Nissen是一位丹麦的计算机科学家。他创建和维护FANN库。其他人则开发了其他语言版本。他也是一位FANN库技术报告的作者。一个快速人工神经网络库的实现(FANN)联系作者:lukesky@diku.dk
柜位预测(二)——神经网络-FANN库