neural-redis
neural-redis 是一个为 Redis 设计的开源模块,它将前馈神经网络作为原生数据类型直接集成到数据库中。该项目旨在打破机器学习的高门槛,让开发者无需搭建复杂的外部系统,即可在 Redis 内部完成数据收集、模型训练与预测执行的全流程。
它主要解决了传统机器学习流程繁琐、部署困难的问题,特别适用于需要快速响应用户行为数据的场景。无论是判断“向哪位用户推送何种促销最有效”,还是预测数据趋势,neural-redis 都能通过简单的 API 调用轻松实现。它非常适合移动端和 Web 应用开发者,帮助他们在几分钟内为应用赋予智能决策能力,而无需深厚的算法背景。
在技术亮点上,neural-redis 支持自动数据归一化和在线多线程训练,允许在模型持续学习的同时进行实时预测,并具备防止过拟合的自动检测机制。不过需要注意的是,目前该工具处于早期测试阶段,主要针对中小型回归与分类问题优化,并不适合处理复杂的计算机视觉等重型任务。对于希望以最低成本尝试机器学习的团队而言,这是一个极具探索价值的轻量级方案。
使用场景
某电商初创团队希望在用户浏览商品时,实时预测并展示最可能促成购买的个性化促销文案。
没有 neural-redis 时
- 架构复杂且延迟高:需要独立部署 Python 机器学习服务,Redis 与应用层需频繁跨网络调用,导致推荐响应延迟增加。
- 数据流转割裂:用户行为数据先写入 Redis,再导出到外部系统训练,模型更新滞后,无法利用最新的点击反馈。
- 开发维护成本高:团队需同时维护数据库、消息队列和 ML 训练管道,小团队难以承受多套系统的运维压力。
- 实时性差:模型通常按天或按周批量重训,无法在用户产生新行为的分钟级时间内调整策略。
使用 neural-redis 后
- 架构极简零延迟:直接在 Redis 内部加载神经网络模块,数据存储与模型推理在同一进程完成,毫秒级返回推荐结果。
- 在线实时训练:利用 neural-redis 的在线学习特性,用户每次点击或购买行为可直接作为新样本输入,模型在后台线程自动增量更新。
- 单一技术栈:开发人员只需掌握 Redis API 即可完成数据采集、训练和预测,无需引入额外的 AI 基础设施。
- 动态适应趋势:模型能即时捕捉突发流量或用户偏好变化(如节假日效应),并在训练过程中不影响线上服务的正常推理。
neural-redis 将复杂的机器学习流程压缩为简单的 Redis 命令,让中小团队也能以极低门槛实现真正的实时智能决策。
运行环境要求
- Linux
- macOS
不需要 GPU,仅使用 CPU 运行
未说明

快速开始
神经 Redis
机器学习就像高中时期的性行为。人人都说会,但真正懂的却很少,甚至没人知道它到底是什么。 -- @Mikettownsend。
Neural Redis 是一个 Redis 可加载模块,它将前馈神经网络实现为 Redis 的原生数据类型。该项目的目标是为 Redis 用户提供一种极其简单的机器学习体验。
通常情况下,机器学习需要先收集数据、训练模型,最后再运行生成的程序来解决实际问题。而在 Neural Redis 中,这些步骤被压缩成一个单一的 API:数据收集和训练都在 Redis 服务器内部完成。神经网络可以在训练过程中执行,并且随着外部不断收集到的新数据(例如用户事件),可以多次重新训练。
该项目基于这样一个观察:虽然像计算机视觉这样的复杂问题需要耗时较长且结构复杂的神经网络,但许多能够提升用户体验的回归和分类问题,其实可以用小型的全连接前馈网络来解决。这类网络训练速度快、适用范围广,而且对参数配置不理想的情况也具有较强的鲁棒性。
Neural Redis 实现了以下功能:
- 非常易用的 API。
- 自动数据归一化。
- 在不同线程中进行神经网络的在线训练。
- 系统在训练时仍可使用该神经网络(我们先训练一个副本,稍后再合并权重)。
- 使用 RPROP(弹性反向传播)学习算法的全连接神经网络。
- 自动训练,并具备简单的过拟合检测机制。
我们的目标是帮助开发者,尤其是移动和 Web 应用程序的开发者,轻松地接入机器学习,从而回答诸如以下问题:
- 哪种促销活动最有可能打动这位用户?
- 我应该展示哪条广告以获得最佳转化率?
- 用户可能会喜欢哪种模板?
- 这些数据点未来的趋势会怎样?
当然,你还可以做更多事情,因为神经网络非常灵活。你甚至可以尝试使用像 MNIST 这样的计算机视觉数据集,不过需要注意的是,Neural Redis 中实现的神经网络并不针对卷积网络等复杂的计算机视觉任务进行优化(其准确率仅为 2.3%,远低于当前最先进的水平!),同时 Neural Redis 也不支持循环神经网络的强大功能。
然而,你会惊讶地发现,许多任务都可以通过一个能在几分钟内训练好的简单神经网络来发现线性和非线性的相关性。
加载扩展
要运行此扩展,你需要 Redis unstable 版本,可以从 GitHub 上获取,默认分支即可。然后编译扩展,并在启动 Redis 时加载:
redis-server --loadmodule /path/to/neuralredis.so
或者将以下内容添加到你的 redis.conf 文件中:
loadmodule /path/to/neuralredis.so
警告:Alpha 版代码
警告: 这是 Alpha 版代码,可能存在 bug,并可能导致 Redis 服务器崩溃。此外,请注意,目前该模块仅实现了 RDB 持久化,而 AOF 重写功能尚未实现。请自行承担风险使用。
如果你仍然觉得不够吓人,那么请考虑一下:我编写这个超过 1000 行 C 代码的扩展以及这份 README 文件,仅仅用了两天时间。
请注意,此实现还有很大的改进空间。例如,目前仅支持 Sigmoid 激活函数和均方根损失函数:尽管对于该模块旨在解决的问题而言,这种有限的神经网络实现已经表现出相当的灵活性,但根据具体问题的不同,仍有进一步优化的空间。
Hello World
为了理解 API 的工作方式,这里有一个 Hello World 示例,我们将教会我们的神经网络做……加法 :-)
要创建一个新的神经网络,我们可以使用以下命令:
> NR.CREATE net REGRESSOR 2 3 -> 1 NORMALIZE DATASET 50 TEST 10
(integer) 13
该命令创建了一个用于回归任务的神经网络(与分类任务相对:我们将在本教程中解释两者的区别)。
请注意,命令返回了“13”,这意味着该网络共有 13 个可调参数,包括所有连接各层节点或偏置项的权重。更大的网络则会有更多的参数。
该神经网络有 2 个输入、3 个隐藏层和 1 个输出。回归意味着,给定某些输入和期望的输出,我们希望神经网络能够“理解”从输入到输出之间的映射关系,并在接收到新的输入时自动计算出相应的输出。
NORMALIZE 选项表示 Redis 会自动对输入的数据进行归一化处理,因此无需将数据预先缩放到 -/+ 1 的范围内。DATASET 50 和 TEST 10 选项则分别指定了用于训练和测试的数据集的内部存储容量为 50 条和 10 条记录。
学习过程使用训练数据集进行,而测试数据集则用来检测网络是否能够泛化,即是否真的掌握了如何近似某个函数。同时,测试数据集也有助于避免过度训练,也就是所谓的“过拟合”。过拟合是指网络过于专注于训练数据中的特定模式,以至于只能正确响应那些它曾经见过的输入和输出。
现在,我们需要为网络提供一些数据,让它学会我们要近似的函数:
> NR.OBSERVE net 1 2 -> 3
1) (integer) 1
2) (integer) 0
我们是在告诉网络:当输入为 1 和 2 时,输出应为 3。NR.OBSERVE 命令的返回值是神经网络内存中分别存储在训练和测试数据集中的数据条目数量。
我们继续添加其他示例:
> NR.OBSERVE net 4 5 -> 9
> NR.OBSERVE net 3 4 -> 7
> NR.OBSERVE net 1 1 -> 2
> NR.OBSERVE net 2 2 -> 4
> NR.OBSERVE net 0 9 -> 9
> NR.OBSERVE net 7 5 -> 12
> NR.OBSERVE net 3 1 -> 4
> NR.OBSERVE net 5 6 -> 11
此时,我们需要开始训练神经网络,让它学习:
> NR.TRAIN net AUTOSTOP
NR.TRAIN 命令会启动一个训练线程。AUTOSTOP 选项表示我们希望在出现过拟合之前停止训练。
你可以使用 NR.INFO 命令查看网络是否仍在训练中。不过在这个例子中,网络只需要几毫秒就能完成训练,因此我们可以立即测试它是否真的学会了如何将两个数字相加:
> NR.RUN net 1 1
1) "2.0776522297040843"
NR.RUN net 3 2 1) "5.1765427204933099"
嗯,大致上是能工作的。现在我们来看看一些内部信息:
> NR.INFO net
1) id
2) (integer) 1
3) type
4) regressor
5) auto-normalization
6) (integer) 1
7) training
8) (integer) 0
9) layout
10) 1) (integer) 2
2) (integer) 3
3) (integer) 1
11) training-dataset-maxlen
12) (integer) 50
13) training-dataset-len
14) (integer) 6
15) test-dataset-maxlen
16) (integer) 10
17) test-dataset-len
18) (integer) 2
19) training-total-steps
20) (integer) 1344
21) training-total-seconds
22) 0.00
23) dataset-error
24) "7.5369825612397299e-05"
25) test-error
26) "0.00042670663615723583"
27) classification-errors-perc
28) 0.00
如你所见,我们有6个数据集条目和2个测试条目。我们在创建网络时配置了分别可容纳50和10个条目的空间。当你使用NR.OBSERVE添加条目时,网络会根据各自数据集的大小比例,将条目均匀地分配到两个数据集中。当数据集满时,旧的随机条目会被新的条目替换。
我们还可以看到,网络经过1344步训练,耗时0秒(实际上只有几毫秒)。每一步都是用单个数据条目进行的训练,因此相同的6个条目总共被呈现给网络244次。
关于归一化的几点说明
如果我们尝试用超出网络学习范围的值来使用我们的网络,就会发现它会失效:
> NR.RUN net 10 10
1) "12.855978185382257"
这是因为自动归一化会以训练数据集中观察到的最大值为基准。所以如果你打算使用自动归一化功能,务必向网络展示不同范围的样本,包括未来你希望网络处理的数据的最大值。
分类任务
回归是在训练数据集中通过已知的输入和输出来近似一个函数。而分类则是给定一组代表“某种事物”的输入,将其标记为一组固定标签中的某一类的任务。
例如,输入可以是希腊陶罐的特征,而分类输出可能是以下三种陶罐类型之一:
- 类型0:A型基利克斯杯
- 类型1:B型基利克斯杯
- 类型2:卡塞尔杯
作为程序员,你可能会认为输出类别只是一个单一的数字。然而,神经网络并不适合这样工作。比如,用0到0.33之间的输出表示类型0,0.33到0.66之间表示类型1,最后0.66到1之间表示类型2,这样的方式根本无法很好地完成任务。
正确的做法是使用三个独立的输出,其中两个始终设为0,只有一个设为1,对应于输出所代表的类型,即:
- 类型0:[1, 0, 0]
- 类型1:[0, 1, 0]
- 类型2:[0, 0, 1]
当你使用NR.CREATE命令创建神经网络,并将第二个参数设置为CLASSIFIER而不是REGRESSOR时,Neural Redis会为你完成上述转换。因此,当你使用NR.OBSERVE训练网络时,只需将输出设置为0、1或2即可。
当然,你需要以这种方式创建具有三个输出的网络:
> NR.CREATE mynet CLASSIFIER 5 10 -> 3
(integer) 93
目前我们的网络尚未训练,但已经可以运行,尽管它给出的回复完全是随机的:
> NR.RUN mynet 0 1 1 0 1
1) "0.50930603602918945"
2) "0.48879876200255651"
3) "0.49534453421381375"
正如你所见,网络“投票”选择了类型0,因为第一个输出大于其他输出。Neural Redis提供了一个命令,可以帮你省去在客户端寻找最大输出并将其解释为0到2之间数字的工作。这个命令与NR.RUN相同,但直接输出类别ID,称为NR.CLASS:
> NR.CLASS mynet 0 1 1 0 1
(integer) 0
不过请注意,NR.RUN在分类问题中也很有用。例如,一个博客平台可能希望训练一个神经网络,根据刚刚收集到的注册数据(包括国家、性别、年龄和博客类别)预测用户最感兴趣的模板。
虽然网络的预测结果将是具有最高值的输出,但如果我们想展示不同的模板,那么在列表中按输出值从高到低依次展示第二、第三等模板就很有意义了。
在进入实际的分类示例之前,还有一点需要说明。CLASSIFIER类型的网络在训练时也有不同的方式:你不需要像以前那样提供一组由0和1组成的输出,而是可以直接将类别ID作为数字传递给NR.OBSERVE。例如,在陶罐的例子中,我们不需要写NR.OBSERVE 1 0.4 .2 0 1 -> 0 0 1来指定提供的数据样本属于第三类,而只需写:
> NR.OBSERVE mynet 1 0.4 .2 0 1 -> 2
这里的“2”会被自动转换为“0 0 1”,就像“1”会被转换为“0 1 0”一样。
实际示例:泰坦尼克号数据集
Kaggle.com 正在举办一场机器学习竞赛。他们使用的其中一个数据集是泰坦尼克号乘客的信息,包括他们的船票等级、票价、亲属数量、年龄、性别以及其他信息,还有他们在泰坦尼克号沉没事件中是否幸存。
你可以在这个Github仓库的examples目录中找到代码以及包含891条记录的简化数据集的CSV文件。
在这个例子中,我们将尝试根据几个输入变量来预测某个人是否会幸存,因此这是一个分类任务,我们需要用两种不同的标签来标记人员:幸存或死亡。
这个问题与根据用户行为和其他过去收集的数据来标记用户或预测其在某些网络应用程序中的反应的问题非常相似,甚至可以说更令人担忧(提示:机器学习的关键就在于数据的收集……)。
CSV文件中包含了每位乘客的大量信息,但为了简化示例,我们只使用以下字段:
- 船票等级(1等、2等、3等)
- 性别
- 年龄
- Sibsp(船上兄弟姐妹及配偶的数量)
- Parch(船上父母及子女的数量)
- 票价
如果这些输入变量与生存能力之间存在相关性,那么我们的神经网络就应该能够找到这种关系。
需要注意的是,虽然我们有六个输入,但实际上我们需要一个总共有九个输入的网络,因为性别和船票等级实际上是“输入类别”,所以我们需要像处理输出那样,在输入端也进行类似的处理。每个输入都会指示乘客是否属于某个可能的类别。这就是我们的九个输入:
- 是否为男性?(0 或 1)。
- 是否为女性?(0 或 1)。
- 是否乘坐头等舱?(0 或 1)。
- 是否乘坐二等舱?(0 或 1)。
- 是否乘坐三等舱?(0 或 1)。
- 年龄。
- 兄弟姐妹/配偶的数量。
- 父母/子女的数量。
- 票价。
我们手头有不到900名乘客的数据(这里使用的是一个精简的数据集),但我们希望在应用端保留大约200条记录用于验证,而完全不将它们发送到Redis中。
神经网络也会使用一部分数据集来进行验证,因为我计划利用自动停止训练的功能来检测过拟合现象。
这样的网络可以通过以下命令创建:
> NR.CREATE mynet CLASSIFIER 9 15 -> 2 DATASET 1000 TEST 500 NORMALIZE
另外需要注意的是,我们使用的是一个单隐藏层的神经网络(对于刚接触神经网络的朋友来说,输入层和输出层之间的那一层就称为隐藏层)。这个隐藏层包含15个单元。尽管这仍然是一个规模较小的网络,但考虑到数据量以及数据中可能存在的相关性,我们认为这已经足够了。当然,我们也可以尝试不同的参数配置,并且我计划实现一个NR.CONFIGURE命令,以便能够动态地调整这些设置。
此外,由于我们设定的测试数据集最大容量是训练数据集的一半(1000条对比500条),NR.OBSERVE命令会自动将三分之一的记录放入测试数据集中。
如果你查看源代码分发包中实现这个示例的Ruby程序,就会发现数据是如何直接原样输入到网络中的,因为我们要求了自动归一化功能:
def feed_data(r,dataset,mode)
errors = 0
dataset.each{|d|
pclass = [0,0,0]
pclass[d[:pclass]-1] = 1
inputs = pclass +
[d[:male],d[:female]] +
[d[:age],
d[:sibsp],
d[:parch],
d[:fare]]
outputs = d[:survived]
if mode == :observe
r.send('nr.observe',:mynet,*inputs,'->',outputs)
elsif mode == :test
res = r.send('nr.class',:mynet,*inputs)
if res != outputs
errors += 1
end
end
}
if mode == :test
puts "#{errors} prediction errors on #{dataset.length} items"
end
end
该函数既可以用来输入数据,也可以用来评估错误率。
当我们从数据集中加载601条记录后,在尚未开始任何训练之前,执行NR.INFO命令的输出将会是这样的:
> NR.INFO mynet
1) id
2) (integer) 5
3) type
4) classifier
5) auto-normalization
6) (integer) 1
7) training
8) (integer) 0
9) layout
10) 1) (integer) 9
2) (integer) 15
3) (integer) 2
11) training-dataset-maxlen
12) (integer) 1000
13) training-dataset-len
14) (integer) 401
15) test-dataset-maxlen
16) (integer) 500
17) test-dataset-len
18) (integer) 200
19) training-total-steps
20) (integer) 0
21) training-total-seconds
22) 0.00
23) dataset-error
24) "0"
25) test-error
26) "0"
27) classification-errors-perc
28) 0.00
29) overfitting-detected
30) no
正如预期的那样,我们有401条训练数据和200条测试数据。请注意,对于被声明为分类器的网络,其信息输出中会多出一项classification-errors-perc字段。一旦我们对网络进行训练,这一字段就会显示测试数据集中被神经网络错误分类的样本所占的百分比(范围从0%到100%)。现在是时候开始训练我们的网络了:
> NR.TRAIN mynet AUTOSTOP
Training has started
训练完成后再次检查NR.INFO的输出,我们会发现一些有趣的信息(仅摘录相关部分):
19) training-total-steps
20) (integer) 64160
21) training-total-seconds
22) 0.29
23) dataset-error
24) "0.1264141065389438"
25) test-error
26) "0.13803731074639586"
27) classification-errors-perc
28) 19.00
29) overfitting-detected
30) yes
该网络总共训练了0.29秒。在因过拟合而自动停止训练时,测试数据集上的错误率为19%。
你也可以指定按照固定的秒数或迭代次数来训练网络。目前我们只是简单地使用了AUTOSTOP功能,因为它更为便捷。不过,我们将在下一节中更深入地探讨相关内容。
现在我们可以展示Ruby程序完整执行后的输出:
47 prediction errors on 290 items
考虑到我们的模型非常简单,而且只用401个数据点进行了训练,这样的结果还算不错。如果仅仅根据幸存者与遇难者的比例来进行建模,我们可能会误判超过100名乘客。
我们还可以通过交互式地调整几个变量,来查看哪些输入因素会对经过训练的神经网络产生显著影响。
让我们先来预测一位30岁、乘坐头等舱、没有兄弟姐妹和父母的女性的生存概率:
> NR.RUN mynet 1 0 0 0 1 30 0 0 200
1) "0.093071778873849084"
2) "0.90242156736283008"
网络认为她有90%的概率会存活。那么如果她年纪更大一些,比如70岁呢?
> NR.RUN mynet 1 0 0 0 1 70 0 0 200
1) "0.11650946245068818"
2) "0.88784839170875851"
这样一来,她的生存概率就降到了88.7%。如果她乘坐的是三等舱,而且票价非常便宜呢?
> NR.RUN mynet 0 0 1 0 1 70 0 0 20
1) "0.53693405013043127"
2) "0.51547605838387811"
这次的结果变成了五五开……只能靠抛硬币来决定了。
这个示例的核心在于:作为开发者,你在优化应用程序、改善与用户交互体验时所面临的许多问题,其实都类似于“泰坦尼克号”问题——并不是因为问题本身的复杂性,而是因为一个简单的模型就能“解决”它们。
过拟合检测与训练技巧
让神经网络难以像本Redis模块中所建议的那样以交互方式使用的因素之一,无疑是过拟合现象。如果你过度训练,神经网络就会变得像那种能准确复述课文所有内容的学生一样;然而,当被问及关于文章主旨的更概括性问题时,她或他却茫然不知所措,无法作答。
因此,NR.TRAIN命令中的AUTOSTOP选项旨在检测过拟合情况,从而在为时已晚之前停止训练。它是如何实现这一点的呢?目前的解决方案相当简单:在训练过程中,我们会持续比较神经网络在训练数据集和测试数据集上的误差。
通常情况下,当过拟合开始发生时,我们会看到训练数据集上的误差率不断下降,而测试数据集上的误差率却不降反升,甚至开始上升。要准确捕捉到这个转折点并不容易,主要有两个原因:
- 随着网络的学习,误差可能会波动。
- 在测试数据集上,网络的误差可能只会更高,因为学习过程陷入了局部最小值,但随后可能会找到更好的解决方案。
因此,虽然AUTOSTOP大致实现了其宣传的效果(不过我将来会继续改进它;而且确实有一些神经网络专家比我更懂行,他们完全可以提交一个优秀的拉取请求 :-)),我们也可以手动训练网络,并观察误差随训练的变化情况。
例如,这是在泰坦尼克号数据集上自动停止训练后的错误率:
21) training-total-seconds
22) 0.17
23) dataset-error
24) "0.13170509045457734"
25) test-error
26) "0.13433443241900492"
27) classification-errors-perc
28) 18.50
我们可以使用MAXTIME和MAXCYCLES选项来指定训练的时间长度(需要注意的是,即使指定了AUTOSTOP,这些选项仍然适用)。通常情况下,MAXTIME被设置为10000毫秒,也就是总共10秒钟后就会终止训练线程。现在让我们不启用自动停止功能,而是将网络训练30秒钟。
> NR.TRAIN mynet MAXTIME 30000
Training has started
顺带一提,在一个或多个训练正在进行时,我们可以列出它们的状态:
> NR.THREADS
1) nn_id=9 key=mynet db=0 maxtime=30000 maxcycles=0
训练结束后,我们再次查看相关信息:
21) training-total-seconds
22) 30.17
23) dataset-error
24) "0.0674554189303056"
25) test-error
26) "0.20468644603795394"
27) classification-errors-perc
28) 21.50
你可以看到,我们的网络出现了过拟合:训练集上的错误率现在降到了0.06。然而,对于从未见过的数据——即测试集——其表现却变得更差了,错误率上升到了0.20!
实际上,网络对21%的样本分类错误,而之前只有18.50%。
不过这种情况并不总是如此,因此在进行机器学习实验时,手动测试是个不错的想法,尤其是在使用这个尚处于实验阶段的模块时。
一个有趣的例子是examples目录下的iris.rb程序:它会将著名的鸢尾花数据集Iris.csv加载到Redis中。该数据集包含了三种不同类型的鸢尾花,每种都有各自的萼片和花瓣特征。如果你运行这个程序,分类错误率大约为4%。但是,如果你再让网络多训练几个周期:
NR.TRAIN iris MAXCYCLES 100
你会发现,错误率往往会降到2%。
使用BACKTRACK选项更好地检测过拟合
在使用AUTOSTOP时,还有一个可以额外指定的选项(单独使用并无效果),那就是BACKTRACK。当启用回溯功能时,网络在训练过程中,每当出现可能开始过拟合的迹象时,当前的网络版本都会被保存下来。训练结束时,如果保存下来的网络比当前的网络更好(即具有更小的误差),那么就会用保存的版本来代替最终的训练结果。
这样可以避免在使用AUTOSTOP时,由于未能检测到过拟合而导致的一些异常情况。不过,这也增加了运行时间,因为在训练过程中需要不时地复制神经网络。
例如,在使用BACKTRACK处理鸢尾花数据集时(参见examples目录下的iris.rb文件),几乎不会发生过拟合;而在没有启用该选项的情况下,大约有2%的运行可能会出现过拟合。
一个更复杂的非线性分类示例
泰坦尼克号的例子当然很有意思,但其中输入与输出之间的关系很可能主要是线性的。因此,我们现在尝试一个非线性的分类任务,只是为了展示小型神经网络的能力。
在这个源代码发行版的examples目录中,有一个名为circles.rb的示例,我们将以此作为参考。
我们将设置一个分类问题:要求神经网络根据两个输入(从我们的角度来看,它们是二维空间中的两个坐标)将其分为三类:0、1和2。
尽管神经网络并不知道这一点,我们会生成数据,使得不同的类别实际上对应于二维空间中的三个圆圈,且这些圆圈之间存在交叠。生成数据的函数如下:
point_class = rand(3) # 类别为0、1或2
if point_class == 0
x = Math.sin(k)/2+rand()/10;
y = Math.cos(k)/2+rand()/10;
elsif point_class == 1
x = Math.sin(k)/3+0.4+rand()/8;
y = Math.cos(k)/4+0.4+rand()/6;
else
x = Math.sin(k)/3-0.5+rand()/30;
y = Math.cos(k)/3+rand()/40;
end
基本的三角函数:
x = Math.sin(k)
y = Math.cos(k)
当k从0变化到2π时,就描绘出一个圆。上述函数只是在基本圆的基础上加上了一些随机噪声。如果我用load81以图形方式绘制这三类点,就会得到如下图像:

接下来,circles.rb程序会生成同样的点集,并将其输入到配置为接受2个输入、输出3个类别之一的神经网络中。
经过大约2秒钟的训练后,我们尝试可视化神经网络所学到的内容(这也是circles.rb命令的一部分):对于一个80×80的网格中的每个点,我们都让网络对其进行分类。以下是ASCII艺术形式的结果:
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................
/...............................................................................
///.............................................................................
////............................................................................
//////..........................................................................
///////.........................................................................
/////////.......................................................................
//////////......................................................................
////////////....................................................................
/////////////...................................................................
//////////////..................................................................
///////////////.................................................................
/////////////////...............................................................
//////////////////..............................................................
///////////////////.............................................................
////////////////////............................................................
/////////////////////...........................................................
//////////////////////..........................................................
////////////////////////........................................................
/////////////////////////.......................................................
//////////////////////////......................................................
///////////////////////////.....................................................
////////////////////....///////.................................................
///////////////////........//////...............................................
///////////////////.........///////.............................................
///////////////////..........////////...........................................
///////////////////...........///////...........................................
///////////////////...........///////...........................................
///////////////////............///////..........................................
///////////////////............///////..........................................
//////////////////.............///////..........................................
//////////////////.............///////..........................................
//////////////////............////////..........................................
//////////////////............////////..........................................
//////////////////............////////..........................................
///////////////////.........../////////.........................................
///////////////////..........//////////..OOOOOOOOO..............................
///////////////////..........//////////.OOOOOOOOOOOOOO..........................
///////////////////..........//////////OOOOOOOOOOOOOOOOOOOO.....................
///////////////////........../////////OOOOOOOOOOOOOOOOOOOOOOOOO.................
////////////////////......../////////OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO..
////////////////////........///////.OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO
/////////////////////......///////.OOOOOOOOOOOOOOOOOOOOOOOOO..OOOOOOOOOOOOOOOOOO
/////////////////////......//////..OOOOOOOOOOOOOOOOOOOOOOO......OOOOOOOOOOOOOOOO
//////////////////////....//////...OOOOOOOOOOOOOOOOOOOOOO........OOOOOOOOOOOOOOO
//////////////////////////////......OOOOOOOOOOOOOOOOOOOO.........OOOOOOOOOOOOOOO
////////////////////////////........OOOOOOOOOOOOOOOOOOO.........OOOOOOOOOOOOOOOO
//////////////////////////...........OOOOOOOOOOOOOOOOO.........OOOOOOOOOOOOOOOOO
////////////////////////..............OOOOOOOOOOOOOO..........OOOOOOOOOOOOOOOOOO
///////////////////////....................OOOOOOOO..........OOOOOOOOOOOOOOOOOOO
//////////////////////.....................OOOOOOOO.........OOOOOOOOOOOOOOOOOOOO
/////////////////////......................OOOOOOO........OOOOOOOOOOOOOOOOOOOOOO
////////////////////........................OOOOO........OOOOOOOOOOOOOOOOOOOOOOO
///////////////////.........................OOOO.......OOOOOOOOOOOOOOOOOOOOOOOOO
//////////////////..........................OOOO.....OOOOOOOOOOOOOOOOOOOOOOOOOOO
/////////////////............................OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO
////////////////.............................OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO
///////////////........................../...OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO
//////////////..........................//...OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO
/////////////..........................////..OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO
////////////.........................//////..OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO
//////////..........................///////..OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO
/////////..........................////////..OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO
////////..........................//////////.OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO
///////..........................///////////.OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO
/////........................../////////////.OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO
////..........................//////////////.OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO
///..........................///////////////.OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO
//........................../////////////////OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO
...........................//////////////////OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO
..........................///////////////////OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO
.........................////////////////////OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO
......................../////////////////////OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO
正如你所见,虽然这个问题没有线性解法,但神经网络能够将二维空间划分为不同的区域,其中“孔洞”部分对应于圆的交集区域,而较薄的表面则表示圆实际相交的地方(在两条圆周的交点上,存在属于不同类别的点)。
这个例子可能并不实用,但它很好地展示了神经网络在非线性任务中的强大能力。
案例研究:情感分析
Neural Redis 并不是处理高级自然语言处理任务的理想工具。对于情感分析——这一极具挑战性的难题——循环神经网络(RNN)及其他更为复杂的模型能够提供最先进的结果。
然而,也正因为如此,情感分析恰恰是一个很好的示例,用以展示如何建模问题,以及即使是最简单的直觉,在经过大约 5 分钟的训练后,也能让 Neural Redis 以相当不错的方式处理问题(尽管与顶尖的专业系统相比仍有差距)。
本案例研究基于 examples 目录下的 sentiment.rb 源代码。它使用了一个非常流行的情感分析基准数据集,该数据集包含 2000 条电影评论,其中 1000 条为正面评价,1000 条为负面评价。
这些评论大致如下:
这一定是一场扭曲的影评噩梦:年度最佳影片竟会是一部暑期商业片,而且还是金·凯瑞主演的!事实的确如此。《楚门的世界》是我自不知何时以来见过的最令人费解、疯狂、偏执又让人捧腹大笑的道德寓言剧。
通常情况下,我们应当尽力对数据进行预处理,但这次我们偷了个懒,什么都没做。不过,我们仍然需要将输入和输出映射到有意义的参数上。对于输出而言,这很简单——毕竟这是一个分类任务:正面或负面。那么,我们该如何将单词映射为输入呢?
一般的做法是为不同的单词分配不同的 ID,然后使用这些 ID 作为索引。但在我们的场景中,这种方法会带来两个问题:
- 我们需要选定一个词汇表。通常这一步会在预处理阶段完成,可能会考察大量未标注的文本语料库。但别忘了,我们可是很懒的!
- “非常好”和“不好”的含义截然不同,如果只关注单个单词,结果很可能不尽如人意。
于是,我采取了以下做法。假设我们的网络由 3000 个输入单元、100 个隐藏单元以及用于分类的 2 个输出单元组成。
我们将初始输入分为两部分:1500 个输入单元直接接收单个单词;另外 1500 个输入单元则用于处理两个单词的组合。我的做法是利用哈希函数将文本中的单词映射到输入单元上:
INDEX_1 = HASH(单词) % 1500
INDEX_2 = 1500 + (HASH(当前单词 + 下一个单词) % 1500)
这种做法听起来有些疯狂,我也很好奇过去是否有人尝试过类似的方法。由于不同的单词及其组合可能会被哈希到同一个单元,因此结果的精确度会有所降低。不过,只要输入单元足够多,那些在情感上高度对立的词语(例如正面与负面)不太可能被哈希到同一桶中。
这样一来,每个单独的单词以及每一对单词组合都相当于在对应的输入单元上投了一票。我们在遍历句子并为每个单词投票的同时,还会累加所有投票数,最后进行归一化处理,确保所有输入之和等于 1。这样,情感分析的结果就不会受到句子长度的影响。
尽管这种方法非常简单,但它确实有效,并且只需几秒钟就能构建出一个在 2000 条电影评论数据集上准确率达到 80% 的神经网络。我只花了几个小时就完成了这个实验,想必采用更高级的方案还能取得更好的效果。不过,这个用例的核心在于:在将数据映射到神经网络时,要充分发挥创造力。
如果你运行 sentiment.rb,就会看到网络迅速收敛。最终,你可以输入一些句子,让神经网络判断其情感倾向是正面还是负面:
nn_id=7 cycle=61 key=sentiment ... classerr=21.500000
nn_id=7 cycle=62 key=sentiment ... classerr=20.333334
目前表现最好的网络能够以 78.17% 的准确率预测情感极性。
请想象并输入一条电影评论:
> 这部电影太糟糕了,简直离谱!
负面情绪:0.99966669082641602
正面情绪:0.00037576013710349798
> 不错啊!
负面情绪:0.28475716710090637
正面情绪:0.73368257284164429
> 这真是一部杰作!
负面情绪:2.219095662781001e-08
正面情绪:0.99999994039535522
当然,你也会发现一些网络判断错误的句子……不过,你输入的句子越长、越接近真实的电影评论,它就越有可能被正确分类。
API 参考
在上述教程中,并未涵盖所有命令的所有选项,因此这里提供了一份简要参考,列出了此扩展支持的所有命令及其相关选项。
NR.CREATE key [CLASSIFIER|REGRESSOR] inputs [hidden-layer-units ...] -> outputs [NORMALIZE] [DATASET maxlen] [TEST maxlen]
如果目标键为空,则创建一个新的神经网络;否则返回错误。
- key — 存储神经网络的键名。
- CLASSIFIER 或 REGRESSOR 是网络类型,更多信息请参阅本教程。
- inputs — 输入单元的数量。
- hidden-layer-units — 零个或多个参数,分别表示每一层的隐藏单元数量。
- outputs — 输出单元的数量。
- NORMALIZE — 指定是否希望网络对输入进行归一化处理。如果你不清楚这是什么意思,就启用它吧。
- DATASET maxlen — 训练数据集的最大样本数。
- TEST maxlen — 测试数据集的最大样本数。
示例:
NR.CREATE mynet CLASSIFIER 64 100 -> 10 NORMALIZE DATASET 1000 TEST 500
NR.OBSERVE key i0 i1 i2 i3 i4 ... iN -> o0 o1 o3 ... oN [TRAIN|TEST]
将一个数据样本添加到训练或测试数据集中(如果指定了最后一个参数),或者根据两者的规模比例,均匀地分配到其中一个数据集中(如果没有指定目标)。对于分类型神经网络,输出必须是一个介于 0 到 输出单元数 - 1 之间的整数。至于如何将类别 ID 转换为一组二进制值,则由网络自行决定。
该命令会返回训练和测试数据集中当前的数据样本数量。如果目标数据集已满,则会随机移除一条记录,并用新数据替换。
NR.RUN key i0 i1 i2 i3 i4 ... iN
运行存储在指定键下的神经网络,返回一个输出数组。
NR.CLASS key i0 i1 i2 i3 i4 ... iN
类似于 NR.RUN,但仅适用于分类型神经网络。与直接输出神经网络的原始结果不同,该命令会直接返回预测的类别,即具有最大值的输出索引。
NR.TRAIN 键 [MAXCYCLES 计数] [MAXTIME 毫秒] [AUTOSTOP] [BACKTRACK]
在后台线程中训练网络。训练完成后, 会自动用新权重更新已训练的网络,并更新训练统计信息。
该命令操作的是网络的副本,因此在网络训练期间仍可正常使用该网络。
如果未指定 AUTOSTOP,则网络将一直训练,直到达到最大循环次数或毫秒数。若未指定最大循环次数,则无循环限制;若未指定毫秒数,则默认为 10000 毫秒(10 秒)。
如果指定了 AUTOSTOP,网络将在达到最大循环次数或毫秒数时停止训练,同时还会尝试在检测到过拟合时停止训练。有关实现所采用的(目前仍较为简单)停止算法的说明,请参阅前面的相关章节。
如果同时指定了 BACKTRACK 和 AUTOSTOP,在网络训练过程中,训练线程会在每次得分优于之前保存的版本且有迹象表明即将发生过拟合时,保存一份神经网络的副本。之后,若发现该副本的误差更小,则会使用它。
NR.INFO 键
显示关于神经网络的大量内部信息。不妨亲自试一试 :-)
NR.THREADS
显示所有正在运行的训练线程。
NR.RESET 键
将神经网络的权重重置为随机值(即网络将完全遗忘此前学到的内容),并重置训练统计信息。但数据集本身不会受到影响。这在需要从头开始重新训练网络时非常有用。
贡献
Neural Redis 目前只是一个为期 48 小时的个人黑客马拉松项目,其主要目标是展示一个易于访问、提供简单易用机器学习工具的 API 所蕴含的潜力,该工具可以进行交互式使用和训练。
然而,神经网络的实现无疑还有许多改进空间。如果你是该领域的专家,请随时提交修改建议或想法。我希望能够保持外部接口层——即 API——的简洁性,而内部实现则可以更加复杂,以提升性能。
需要注意的是,鉴于当前公开的 API,神经网络的实现不应仅仅追求在特定问题上的最先进水平,而应更多地考虑在各种条件下都能良好工作。尽管目前的全连接网络存在局限性,但结合 BPROP 学习算法,它对误用具有相当的鲁棒性。因此,改进后的版本应当能够保留并进一步扩展这一特性。确保这一点的最简单方法是使用开放数据集构建一套涵盖不同类型的基准测试,并以此来评估各种实现方案。
计划
- 改进过拟合检测机制。
- 实现 RNN,并提供更易用的 API。
- 为分类神经网络采用不同的损失函数。
- 邀请熟悉简单 API 的机器学习专家参与合作。
祝你机器学习玩得开心!
萨尔瓦托雷
相似工具推荐
stable-diffusion-webui
stable-diffusion-webui 是一个基于 Gradio 构建的网页版操作界面,旨在让用户能够轻松地在本地运行和使用强大的 Stable Diffusion 图像生成模型。它解决了原始模型依赖命令行、操作门槛高且功能分散的痛点,将复杂的 AI 绘图流程整合进一个直观易用的图形化平台。 无论是希望快速上手的普通创作者、需要精细控制画面细节的设计师,还是想要深入探索模型潜力的开发者与研究人员,都能从中获益。其核心亮点在于极高的功能丰富度:不仅支持文生图、图生图、局部重绘(Inpainting)和外绘(Outpainting)等基础模式,还独创了注意力机制调整、提示词矩阵、负向提示词以及“高清修复”等高级功能。此外,它内置了 GFPGAN 和 CodeFormer 等人脸修复工具,支持多种神经网络放大算法,并允许用户通过插件系统无限扩展能力。即使是显存有限的设备,stable-diffusion-webui 也提供了相应的优化选项,让高质量的 AI 艺术创作变得触手可及。
everything-claude-code
everything-claude-code 是一套专为 AI 编程助手(如 Claude Code、Codex、Cursor 等)打造的高性能优化系统。它不仅仅是一组配置文件,而是一个经过长期实战打磨的完整框架,旨在解决 AI 代理在实际开发中面临的效率低下、记忆丢失、安全隐患及缺乏持续学习能力等核心痛点。 通过引入技能模块化、直觉增强、记忆持久化机制以及内置的安全扫描功能,everything-claude-code 能显著提升 AI 在复杂任务中的表现,帮助开发者构建更稳定、更智能的生产级 AI 代理。其独特的“研究优先”开发理念和针对 Token 消耗的优化策略,使得模型响应更快、成本更低,同时有效防御潜在的攻击向量。 这套工具特别适合软件开发者、AI 研究人员以及希望深度定制 AI 工作流的技术团队使用。无论您是在构建大型代码库,还是需要 AI 协助进行安全审计与自动化测试,everything-claude-code 都能提供强大的底层支持。作为一个曾荣获 Anthropic 黑客大奖的开源项目,它融合了多语言支持与丰富的实战钩子(hooks),让 AI 真正成长为懂上
ComfyUI
ComfyUI 是一款功能强大且高度模块化的视觉 AI 引擎,专为设计和执行复杂的 Stable Diffusion 图像生成流程而打造。它摒弃了传统的代码编写模式,采用直观的节点式流程图界面,让用户通过连接不同的功能模块即可构建个性化的生成管线。 这一设计巧妙解决了高级 AI 绘图工作流配置复杂、灵活性不足的痛点。用户无需具备编程背景,也能自由组合模型、调整参数并实时预览效果,轻松实现从基础文生图到多步骤高清修复等各类复杂任务。ComfyUI 拥有极佳的兼容性,不仅支持 Windows、macOS 和 Linux 全平台,还广泛适配 NVIDIA、AMD、Intel 及苹果 Silicon 等多种硬件架构,并率先支持 SDXL、Flux、SD3 等前沿模型。 无论是希望深入探索算法潜力的研究人员和开发者,还是追求极致创作自由度的设计师与资深 AI 绘画爱好者,ComfyUI 都能提供强大的支持。其独特的模块化架构允许社区不断扩展新功能,使其成为当前最灵活、生态最丰富的开源扩散模型工具之一,帮助用户将创意高效转化为现实。
NextChat
NextChat 是一款轻量且极速的 AI 助手,旨在为用户提供流畅、跨平台的大模型交互体验。它完美解决了用户在多设备间切换时难以保持对话连续性,以及面对众多 AI 模型不知如何统一管理的痛点。无论是日常办公、学习辅助还是创意激发,NextChat 都能让用户随时随地通过网页、iOS、Android、Windows、MacOS 或 Linux 端无缝接入智能服务。 这款工具非常适合普通用户、学生、职场人士以及需要私有化部署的企业团队使用。对于开发者而言,它也提供了便捷的自托管方案,支持一键部署到 Vercel 或 Zeabur 等平台。 NextChat 的核心亮点在于其广泛的模型兼容性,原生支持 Claude、DeepSeek、GPT-4 及 Gemini Pro 等主流大模型,让用户在一个界面即可自由切换不同 AI 能力。此外,它还率先支持 MCP(Model Context Protocol)协议,增强了上下文处理能力。针对企业用户,NextChat 提供专业版解决方案,具备品牌定制、细粒度权限控制、内部知识库整合及安全审计等功能,满足公司对数据隐私和个性化管理的高标准要求。
ML-For-Beginners
ML-For-Beginners 是由微软推出的一套系统化机器学习入门课程,旨在帮助零基础用户轻松掌握经典机器学习知识。这套课程将学习路径规划为 12 周,包含 26 节精炼课程和 52 道配套测验,内容涵盖从基础概念到实际应用的完整流程,有效解决了初学者面对庞大知识体系时无从下手、缺乏结构化指导的痛点。 无论是希望转型的开发者、需要补充算法背景的研究人员,还是对人工智能充满好奇的普通爱好者,都能从中受益。课程不仅提供了清晰的理论讲解,还强调动手实践,让用户在循序渐进中建立扎实的技能基础。其独特的亮点在于强大的多语言支持,通过自动化机制提供了包括简体中文在内的 50 多种语言版本,极大地降低了全球不同背景用户的学习门槛。此外,项目采用开源协作模式,社区活跃且内容持续更新,确保学习者能获取前沿且准确的技术资讯。如果你正寻找一条清晰、友好且专业的机器学习入门之路,ML-For-Beginners 将是理想的起点。
ragflow
RAGFlow 是一款领先的开源检索增强生成(RAG)引擎,旨在为大语言模型构建更精准、可靠的上下文层。它巧妙地将前沿的 RAG 技术与智能体(Agent)能力相结合,不仅支持从各类文档中高效提取知识,还能让模型基于这些知识进行逻辑推理和任务执行。 在大模型应用中,幻觉问题和知识滞后是常见痛点。RAGFlow 通过深度解析复杂文档结构(如表格、图表及混合排版),显著提升了信息检索的准确度,从而有效减少模型“胡编乱造”的现象,确保回答既有据可依又具备时效性。其内置的智能体机制更进一步,使系统不仅能回答问题,还能自主规划步骤解决复杂问题。 这款工具特别适合开发者、企业技术团队以及 AI 研究人员使用。无论是希望快速搭建私有知识库问答系统,还是致力于探索大模型在垂直领域落地的创新者,都能从中受益。RAGFlow 提供了可视化的工作流编排界面和灵活的 API 接口,既降低了非算法背景用户的上手门槛,也满足了专业开发者对系统深度定制的需求。作为基于 Apache 2.0 协议开源的项目,它正成为连接通用大模型与行业专有知识之间的重要桥梁。