使用 FANN 库轻松实现人工神经网络
神经网络通常与专业应用程序相关联,仅由少数专家组开发。这种误解对其普及产生了非常负面的影响。希望 FANN 库能帮助弥补这一空白。
本文由 Steffen Nissen 撰写,最初发表于 2005 年 3 月的 Software 2.0 杂志。您可以在 SDJ 网站找到更多文章。
- 下载所有平台的 C 库源代码 - 2.39 MB
- 下载 C 源代码和 Windows DLL 文件 - 3.32 MB
- 下载 Visual Studio .NET 项目 - 8.32 Kb
- 从 SourceForge 下载最新的 FANN 库
引言
多年来,好莱坞科幻电影如《我,机器人》将人工智能 (AI) 描绘成末日降临的预兆。然而,事实并非如此。当好莱坞用我们即将灭亡的恐怖故事来取悦我们时,那些对我们物种灭绝不感兴趣的人一直在利用人工智能来让我们的生活更轻松、更高效、更长久、总体上更好。
《我,机器人》电影中的机器人拥有基于人工神经元网络的人工大脑;这个人工神经网络(ANN)旨在模拟人类大脑自身的神经网络。快速人工神经网络(FANN)库是一个 ANN 库,可以从 C、C++、PHP、Python、Delphi 和 Mathematica 中使用,虽然它不能创造好莱坞的魔力,但它仍然是软件开发人员的强大工具。人工神经网络可以用于各种领域,例如在电脑游戏中创造更具吸引力的游戏玩法,识别图像中的物体,以及帮助股票经纪人预测不断变化的股票市场的趋势。
人工智能
什么或谁是智能的?狗是智能的吗?新生儿呢?通常,我们将智能定义为获取和应用知识、进行演绎推理和展现创造力的能力。如果我们将相同的标准应用于人工智能(AI),那么目前就不存在人工智能。然而,通常,人工智能被定义为执行通常与人类智能相关联的功能的能力。因此,人工智能可以用来描述所有处理人类知识的学习或应用的计算机化努力。这个定义使得人工智能一词可以描述即使是最简单的国际象棋电脑或电脑游戏中的角色。
函数逼近
人工神经网络通过示例应用函数逼近的原理,这意味着它们通过查看该函数的示例来学习函数。最简单的例子之一是人工神经网络学习 XOR 函数,但它也可以同样轻松地学习确定文本的语言,或者 X 射线图像中是否存在肿瘤。
如果人工神经网络要能够学习一个问题,它必须被定义为一个函数,包含一组输入和输出变量,并由该函数应如何工作的示例支持。像 XOR 函数这样的问题已经定义为一个具有两个二进制输入变量和一个二进制输出变量的函数,并且具有由四种不同输入模式的结果定义的示例。然而,更复杂的问题可能更难以定义为函数。在 X 射线图像中寻找肿瘤的问题的输入变量可以是图像的像素值,但它们也可以是从图像中提取的一些值。输出可以是二进制值或表示图像中肿瘤概率的浮点值。在人工神经网络中,这个浮点值通常在 0 到 1 之间(包括 0 和 1)。
图 1. 一个具有四个输入神经元、一个隐藏层和四个输出神经元的人工神经网络。
人脑
像人工神经网络这样的函数逼近器可以被看作一个黑盒子,对于 FANN 来说,这几乎就是你需要知道的一切。然而,要理解人工神经网络的工作原理,需要对人脑的运作方式有基本的了解。
人脑是一个高度复杂的系统,能够解决非常复杂的问题。大脑由许多不同的元素组成,但其最重要的组成部分之一是神经元,大约有 1011 个。这些神经元通过大约 1015 个连接相互连接,形成一个巨大的神经网络。神经元通过连接相互发送冲动,这些冲动使大脑工作。神经网络还接收来自五种感官的冲动,并向肌肉发送冲动以实现运动或言语。
单个神经元可以看作是一个输入-输出机器,它等待来自周围神经元的冲动,当它接收到足够的冲动时,它会向其他神经元发送一个冲动。
人工神经网络
人工神经元与其生物学对应物相似。它们有输入连接,这些连接被汇总以确定其输出的强度,而输出是总和被馈入激活函数的结果。尽管存在许多激活函数,但最常见的是 Sigmoid 激活函数,它输出一个介于 0(对于低输入值)和 1(对于高输入值)之间的数字。该函数的结果然后通过更多连接作为输入传递给其他神经元,每个连接都带有权重。这些权重决定了网络的行为。
在人脑中,神经元以看似随机的顺序连接并异步发送脉冲。如果我们要模拟一个大脑,这可能是组织人工神经网络的方式,但由于我们主要想创建一个函数逼近器,人工神经网络通常不是这样组织的。
当我们创建人工神经网络时,神经元通常以层状排列,层之间存在连接。第一层包含输入神经元,最后一层包含输出神经元。这些输入和输出神经元代表我们想要逼近的函数的输入和输出变量。在输入层和输出层之间,存在多个隐藏层,这些隐藏层的连接(和权重)决定了人工神经网络的性能。当人工神经网络学习逼近一个函数时,它会显示该函数如何工作的示例,并且人工神经网络中的内部权重会缓慢调整,以产生与示例相同的输出。希望当人工神经网络显示一组新的输入变量时,它将给出正确的输出。因此,如果人工神经网络被期望学习发现 X 射线图像中的肿瘤,它将显示许多包含肿瘤的 X 射线图像,以及许多包含健康组织的 X 射线图像。经过一段时间的这些图像训练后,人工神经网络中的权重应该有望包含允许它在训练期间未见的 X 射线图像中准确识别肿瘤的信息。
FANN 库教程
互联网使全球通信成为许多人生活的一部分,但也引发了一个问题,即并非每个人都说同一种语言。翻译工具可以帮助弥合这一差距,但要使这些工具发挥作用,它们需要知道一段文本是用何种语言编写的。确定这一点的一种方法是检查文本中字母出现的频率。虽然这看起来是一种非常幼稚的语言检测方法,但它已被证明非常有效。对于许多欧洲语言来说,仅仅查看 A 到 Z 字母的频率就足够了,尽管有些语言也使用其他字母。FANN 库可以轻松地用于编写一个小程序,以确定文本文件的语言。使用的人工神经网络应该为 26 个字母中的每个字母配备一个输入神经元,并为每种语言配备一个输出神经元。但首先,必须制作一个小程序来测量文本文件中字母的频率。
列表 1 将生成文件的字母频率,并以可用于为 FANN 库生成训练文件的格式输出它们。FANN 库的训练文件必须包含一行输入值,后跟一行输出值。如果我们要区分三种不同的语言(英语、法语和波兰语),我们可以选择通过分配一个输出变量来表示,英语值为 0,法语值为 0.5,波兰语值为 1。然而,众所周知,如果为每种语言分配一个输出变量,并将其设置为 1 表示正确的语言,否则为 0,则神经网络的性能会更好。
列表 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 *filename, float *frequencies) { std::ifstream infile(filename); if(!infile) error("Cannot open input file", filename); std::vector<unsigned int> letter_count(26, 0); unsigned int num_characters = 0; char c; while(infile.get(c)){ c = tolower(c); if(c >= 'a' && c <= 'z'){ letter_count[c - 'a']++; num_characters++; } } if(!infile.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 file"); float 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; }
有了这个小程序,就可以为用不同语言编写的文本生成包含字母频率的训练文件。当然,如果训练文件中包含许多不同文本的频率,人工神经网络在区分语言方面会更好,但对于这个小例子,每种语言 3-4 个文本就足够了。列表 2 显示了一个预生成的训练文件,使用了三种语言中的每种语言的四个文本文件,图 2 显示了文件中频率的图形表示。仔细检查这个文件显示了明显的趋势:英语的 H 比其他两种语言多,法语几乎没有 K,波兰语的 W 和 Z 比其他语言多。训练文件只使用 A 到 Z 范围内的字母,但由于像波兰语这样的语言使用 Ł、Ą 和 Ę 等其他两种语言中不使用的字母,因此可以通过为这些字母添加输入神经元来制作更精确的人工神经网络。然而,当只比较三种语言时,不需要这些额外的字母,因为剩余的字母包含足够的信息来正确分类语言,但如果人工神经网络要分类数百种不同的语言,则需要更多的字母。
列表 2. 训练文件的第一部分,包含英语、法语和波兰语的字符频率,第一行是标题,说明有 12 个训练模式,由 26 个输入和 3 个输出组成。
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
0.078 0.013 0.043 0.043 0.113 0.024 0.023 0.041 0.068
0.000 0.005 0.045 0.024 0.069 0.095 0.020 0.001 0.061
0.080 0.090 0.029 0.015 0.014 0.000 0.008 0.000
1 0 0
0.061 0.005 0.028 0.040 0.161 0.019 0.010 0.010 0.066
0.016 0.000 0.035 0.028 0.092 0.061 0.031 0.019 0.059
0.101 0.064 0.076 0.016 0.000 0.002 0.002 0.000
0 1 0
0.092 0.016 0.038 0.025 0.083 0.000 0.015 0.009 0.087
0.030 0.040 0.032 0.033 0.063 0.085 0.033 0.000 0.049
0.053 0.033 0.025 0.000 0.053 0.000 0.038 0.067
0 0 1
...
图 2. 英语、法语和波兰语平均频率的条形图。
有了这样的训练文件,使用 FANN 创建一个可以训练人工神经网络来区分这三种语言的程序就非常容易了。列表 2 展示了用 FANN 做到这一点是多么简单。这个程序使用了四个 FANN 函数:`fann_create`、`fann_train_on_file`、`fann_save` 和 `fann_destroy`。函数 `struct fann* fann_create(float connection_rate, float learning_rate, unsigned int num_layers, ...)` 用于创建一个人工神经网络,其中 `connection_rate` 参数可以用于创建一个未完全连接的人工神经网络,尽管通常首选完全连接的人工神经网络,而 `learning_rate` 用于指定学习算法的激进程度(仅与某些学习算法相关)。函数的最后几个参数用于定义人工神经网络中层的布局。在这种情况下,选择了一个三层(一个输入层、一个隐藏层和一个输出层)的人工神经网络。输入层有 26 个神经元(每个字母一个),输出层有三个神经元(每种语言一个),隐藏层有 13 个神经元。层的数量和隐藏层中神经元的数量是根据实验选择的,因为没有简单的方法来确定这些值。然而,记住人工神经网络通过调整权重来学习是有帮助的,所以如果人工神经网络包含更多的神经元,从而也有更多的权重,它可以学习更复杂的问题。权重过多也可能是一个问题,因为学习可能更困难,而且人工神经网络也有可能学习输入变量的特定特征,而不是可以外推到其他数据集的通用模式。为了使人工神经网络能够准确分类训练集中没有的数据,这种泛化能力至关重要——没有它,人工神经网络将无法区分它没有训练过的频率。
列表 3. 一个训练人工神经网络学习区分语言的程序。
#include "fann.h" int main() { struct fann *ann = fann_create(1, 0.7, 3, 26, 13, 3); fann_train_on_file(ann, "frequencies.data", 200, 10, 0.0001); fann_save(ann, "language_classify.net"); fann_destroy(ann); return 0; }
void fann_train_on_file(struct fann *ann, char *filename, unsigned int max_epochs, unsigned int epochs_between_reports, float desired_error)
函数训练人工神经网络。训练是通过不断调整权重来完成的,以便人工神经网络的输出与训练文件中的输出匹配。一个调整权重以匹配训练文件中输出的循环称为一个 epoch。在这个例子中,最大 epoch 数设置为 200,每 10 个 epoch 打印一次状态报告。在衡量人工神经网络与期望输出的匹配程度时,通常使用均方误差。均方误差是人工神经网络的实际输出与期望输出之间平方差的平均值,针对单个训练模式。小的均方误差意味着与期望输出的紧密匹配。
当列表 2 中的程序运行时,人工神经网络将被训练,并且会打印一些状态信息(见列表 4),以便更容易监控训练过程中的进度。训练完成后,人工神经网络可以直接用于确定文本是用哪种语言编写的,但通常希望将训练和执行放在两个不同的程序中,这样耗时的训练只需进行一次。因此,列表 2 简单地将人工神经网络保存到一个文件中,该文件可以由另一个程序加载。
列表 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
列表 5 中的小程序加载已保存的人工神经网络,并使用它将文本分类为英语、法语或波兰语。在用互联网上找到的三种语言的文本进行测试时,它可以正确分类短至几句话的文本。尽管这种区分语言的方法并非万无一失,但我无法找到任何可能被错误分类的文本。
列表 5. 一个将文本分类为三种语言之一的程序(该程序使用列表 1 中定义的一些函数)。
int main(int argc, char* argv[]) { if(argc != 2) error("Remember to specify an input file"); struct fann *ann = fann_create_from_file("language_classify.net"); float frequencies[26]; generate_frequencies(argv[1], frequencies); float *output = fann_run(ann, frequencies); std::cout << "English: " << output[0] << std::endl << "French : " << output[1] << std::endl << "Polish : " << output[2] << std::endl; return 0; }
FANN 库:详细信息
语言分类示例展示了 FANN 库如何轻松应用于解决简单的日常计算机科学问题,这些问题使用其他方法将难以解决。不幸的是,并非所有问题都能如此轻松地解决,在处理人工神经网络时,人们常常会发现自己处于一种很难训练人工神经网络以给出正确结果的境地。有时,这是因为问题根本无法通过人工神经网络解决,但通常,可以通过调整 FANN 库设置来帮助训练。
训练人工神经网络最重要的因素是人工神经网络的大小。这只能通过实验设置,但对问题的了解通常有助于给出良好的猜测。对于大小合理的人工神经网络,训练可以通过许多不同的方式完成。FANN 库支持几种不同的训练算法,默认算法(FANN_TRAIN_RPROP
)可能并不总是最适合特定问题。如果出现这种情况,可以使用 fann_set_training_algorithm
函数来更改训练算法。
在 FANN 库 1.2.0 版本中,提供了四种不同的训练算法,所有这些算法都使用某种形式的反向传播。反向传播算法通过将误差从输出层反向传播到输入层来改变权重,同时调整权重。反向传播的误差值可以是为单个训练模式计算的误差(增量式),也可以是整个训练文件的误差总和(批量式)。FANN_TRAIN_INCREMENTAL
实现了一种增量式训练算法,它在每个训练模式之后改变权重。这种训练算法的优点是,在每个 epoch 期间,权重会被多次改变,并且由于每个训练模式都会在略微不同的方向上改变权重,因此训练不容易陷入局部最小值——在这种状态下,权重的任何微小变化只会使均方误差更差,即使尚未找到最优解。
FANN_TRAIN_BATCH
、FANN_TRAIN_RPROP
和 FANN_TRAIN_QUICKPROP
都是批量训练算法的例子,它们在计算完整个训练集的误差后调整权重。这些算法的优点是它们可以利用全局优化信息,而增量训练算法无法获得这些信息。然而,这可能意味着会错过单个训练模式的一些细微之处。对于哪种训练算法最好,没有明确的答案。通常,像 rprop 或 quickprop 训练这样的高级批量训练算法是最好的解决方案。然而,有时增量训练更优化——尤其是在有许多训练模式可用时。在语言训练示例中,最优化训练算法是默认的 rprop 算法,它仅经过 57 个 epoch 就达到了所需的均方误差值。增量训练算法需要 8108 个 epoch 才能达到相同的结果,而批量训练算法需要 91985 个 epoch。quickprop 训练算法遇到了更多问题,起初它完全未能达到所需的误差值,但在调整了 quickprop 算法的衰减后,它在 662 个 epoch 后达到了所需的误差。quickprop 算法的衰减是一个参数,用于控制 quickprop 训练算法的激进程度,可以通过 fann_set_quickprop_decay
函数进行更改。其他 fann_set_...
函数也可以用于为单个训练算法设置附加参数,尽管其中一些参数在不了解单个算法工作原理的情况下可能有点难以调整。
然而,有一个参数与训练算法无关,可以相当容易地进行调整——激活函数的陡峭度。激活函数是决定输出何时接近 0 和何时接近 1 的函数,而该函数的陡峭度决定了从 0 到 1 的过渡是软还是硬。如果陡峭度设置为高值,训练算法将更快地收敛到 0 和 1 的极端值,这将使训练更快,例如,对于语言分类问题。但是,如果陡峭度设置为低值,则更容易训练需要小数输出的人工神经网络,例如,应该训练以查找图像中线条方向的人工神经网络。为了设置激活函数的陡峭度,FANN 提供了两个函数:fann_set_activation_steepness_hidden
和 fann_set_activation_steepness_output
。之所以有两个函数,是因为通常希望隐藏层和输出层具有不同的陡峭度。
图 3. 显示陡峭度为 0.25、0.50 和 1.00 时 Sigmoid 激活函数的图表。
FANN 的可能性
语言识别问题属于一种特殊的函数逼近问题,称为分类问题。分类问题每个分类有一个输出神经元,在每个训练模式中,其中恰好一个输出必须为 1。更一般的函数逼近问题是输出为分数数值的问题。例如,这可以是近似相机所视物体的距离,甚至是房屋的能耗。这些问题当然可以与分类问题结合起来,因此可能存在一个识别图像中物体类型的分类问题,以及一个近似物体距离的问题。通常,这可以通过单个 ANN 来完成,但有时最好将这两个问题分开,例如,有一个 ANN 用于分类物体,并且为每个不同的物体配备一个 ANN 来近似物体的距离。
另一种近似问题是时间序列问题,即近似随时间演变的函数。一个众所周知的时间序列问题是根据历史数据预测一年内将有多少个太阳黑子。正常的函数以 x 值作为输入,y 值作为输出,太阳黑子问题也可以这样定义,以年份作为 x 值,太阳黑子数量作为 y 值。然而,这已被证明不是解决此类问题的最佳方法。时间序列问题可以通过将一段时间作为输入,然后将下一个时间步作为输出来进行近似。如果时间段设置为 10 年,则可以使用存在历史数据的所有 10 年时间段来训练人工神经网络,然后它可以使用 1995 年至 2004 年的太阳黑子数量作为输入来近似 2005 年的太阳黑子数量。这种方法意味着每组历史数据都用于多个训练模式,例如,1980 年的太阳黑子数量用于以 1981 年至 1990 年作为输出的训练模式。这种方法还意味着无法直接近似 2010 年的太阳黑子数量,除非首先近似 2005 年至 2009 年的,这将反过来意味着计算 2010 年的一半输入将是近似数据,并且 2010 年的近似将不如 2005 年的近似精确。因此,时间序列预测仅适用于预测近期的事情。
时间序列预测还可以用于在机器人控制器等中引入记忆。例如,除了来自传感器或摄像头的其他输入外,还可以将前两个时间步的方向和速度作为输入提供给第三个时间步。然而,这种方法的主要问题是训练数据可能非常难以生成,因为每个训练模式还必须包含历史记录。
FANN 技巧与窍门
可以使用许多技巧来使 FANN 训练和执行更快、更精确。一个简单的技巧是使用 -1 到 1 范围内的输入和输出值,而不是 0 到 1。这可以通过更改训练文件中的值并使用 fann_set_activation_function_hidden
和 fann_set_activation_function_output
将激活函数更改为 FANN_SIGMOID_SYMMETRIC
来实现,后者的输出范围在 -1 和 1 之间,而不是 0 和 1。这个技巧之所以有效,是因为人工神经网络中的 0 值有一个不幸的特点,无论权重值是多少,输出仍将为 0。当然,FANN 中有对策可以防止这成为一个大问题;然而,事实证明这个技巧可以减少训练时间。fann_set_activation_function_output
也可以用于将激活函数更改为无界的 FANN_LINEAR
激活函数,因此可以用于创建具有任意输出的人工神经网络。
在训练人工神经网络时,通常很难确定应该使用多少个 epoch 进行训练。如果在训练过程中使用的 epoch 过少,人工神经网络将无法对训练数据进行分类。但是,如果迭代次数过多,人工神经网络将过度专业化于训练数据的精确值,并且人工神经网络将不擅长对训练期间未见过的数据进行分类。因此,通常最好有两组训练数据,一组用于实际训练,另一组用于通过在训练期间未见过的数据上测试人工神经网络来验证其质量。fann_test_data
函数可以用于此目的,以及其他可用于处理和操作训练数据的函数。
将问题转换为人工神经网络易于学习的函数可能是一项艰巨的任务,但可以遵循一些通用准则。
- 每个信息单元至少使用一个输入/输出神经元。在语言分类系统的情况下,这意味着每个字母有一个输入神经元,每种语言有一个输出神经元。
- 在选择输入神经元时,请表示作为程序员的您对问题所拥有的所有知识。例如,如果您知道词长对语言分类系统很重要,那么您也应该为词长添加一个输入神经元(这也可以通过添加一个表示空格频率的输入神经元来实现)。此外,如果您知道某些字母只在某些语言中使用,那么添加一个额外的输入神经元可能是一个好主意,如果文本中存在该字母则为 1,如果不存在则为 0。通过这种方式,即使文本中的一个波兰语字母也可以帮助分类该文本。也许您知道某些语言比其他语言包含更多的元音,那么您可以将元音的频率表示为额外的输入神经元。
- 简化问题。例如,如果您想使用人工神经网络来检测图像中的某些特征,那么简化图像以使问题更容易解决可能是一个好主意,因为原始图像通常包含过多的信息,人工神经网络将难以过滤掉相关信息。在图像中,可以通过应用一些滤镜进行平滑、边缘检测、脊线检测、灰度处理等来实现简化。其他问题可以通过其他方式预处理数据以去除不必要的信息来简化。简化还可以通过将人工神经网络分成几个更容易解决的问题来实现。在语言分类问题中,一个人工神经网络可以例如区分欧洲语言和亚洲语言,而另外两个人工神经网络可以用于分类这两个区域的各个语言。
虽然训练人工神经网络通常是耗时的大头,但执行往往对时间更敏感——尤其是在人工神经网络需要每秒执行数百次或人工神经网络非常大的系统中。因此,可以采取多种措施使 FANN 库执行速度比现在更快。一种方法是将激活函数更改为使用分段线性激活函数,这种函数执行速度更快,但精度也略低。如果可能,减少隐藏神经元的数量也是一个好主意,因为这将减少执行时间。另一种方法,仅在没有浮点处理器的嵌入式系统上有效,是让 FANN 库仅使用整数执行。FANN 库有一些辅助函数允许库仅使用整数执行,在没有浮点处理器的系统上,这可以带来超过 5000% 的性能提升。
在网上
- FANN 库
- Steffen Nissen,快速人工神经网络库(FANN)的实现
- Steffen Nissen 和 Evan Nemerson,快速人工神经网络库参考手册
- Martin Riedmiller 和 Heinrich Braun,一种更快的反向传播学习的直接自适应方法:RPROP 算法
- 人工神经网络常见问题解答
一个来自开源世界的故事
当我第一次在 2003 年 11 月发布 FANN 库 1.0 版本时,我并不知道会发生什么,但我认为每个人都应该有机会使用我创建的这个新库。令我惊讶的是,人们真的开始下载和使用这个库。几个月过去了,越来越多的用户开始使用 FANN,这个库从一个仅限于 Linux 的库发展到支持大多数主流编译器和操作系统(包括 MSVC++ 和 Borland C++)。库的功能也得到了显著扩展,许多用户开始为库做出贡献。很快,这个库就有了 PHP、Python、Delphi 和 Mathematica 的绑定,并且这个库也被 Debian Linux 发行版接受了。我与 FANN 和库用户的工作占据了我一些业余时间,但我乐于奉献这些时间。FANN 给了我一个回馈开源社区的机会,也给了我一个在做自己喜欢的事情的同时帮助他人的机会。我不能说所有软件开发人员都应该开发开源软件,但我会说它给了我很大的满足感,所以如果您认为这可能适合您,那么就找一个您想贡献的开源项目,并开始贡献。或者更好的是,开始您自己的开源项目。