← 返回信息流
AI 资讯Hacker News·2 天前

NoiseLang将N=5定义为狄拉克δ函数

原标题:NoiseLang: Where N = 5 is a Dirac delta

速览

NoiseLang是一种专为噪声生成设计的领域特定语言,其核心特性是将参数N=5作为狄拉克δ函数处理,实现精准的脉冲信号建模。这一设计简化了噪声频谱的控制,可应用于信号处理、计算机图形学或AI中对抗样本生成等场景。该语言通过数学抽象降低了噪声编程的复杂性,有望在音频、图像生成等领域发挥独特作用。

AI 深度解读

背景

NoiseLang 的诞生源于作者在攻读电信学位时的一段痛苦经历。当时他选修了一门关于信号与噪声的课程,经常需要花大量时间手动计算概率:期望、方差、两个随机变量落在某个区域内的概率。当他试图将这些计算搬到计算机上时,却发现需要写大量样板代码,而且效率低下。这个愿望最终催生了 NoiseLang。项目开始于大约九年前,但作者一直没有完成。直到最近,借助 AI 工具以及比当初独自构建时更宏大的野心,他才重新启动了该项目。

核心内容

一切都是分布

NoiseLang 的核心理念是:每一个值都是一个概率分布。普通的数字是一个 Dirac 尖峰(Dirac spike),即所有权重集中在一个单一值上的分布。由于常量和随机变量是同一类对象,语言中的每个运算符都将分布映射为分布。变量名始终指向同一个固定的节点,就像在数学公式中同一个 X 在整个页面里代表同一个变量。因此 X + X 就是 2X,而 X - X 严格等于 0。如果需要变量的独立性,可以使用多次 ~ 符号,或者使用 ~[N] 来抽取 N 个独立变量存入一个向量。例如:

X ~ unif_int(1, 6)
Y ~ unif_int(1, 6)
X + Y  # 两个独立骰子,真实的 2d6 分布

直到你要求某些结果(例如 P(X + Y < 10))之前,什么都不会执行。那一刻,运行时会运行数百万次模拟(如果可用,会利用所有核心),并返回一个带有标准误差的估计值。例如生日悖论:

Bday = unif_int(1, 365)
days ~[23] Bday  # 房间里 23 个人
P(has_duplicates(days))  # 大约 0.507

为何搁置了九年

语言设计从来不是难点——一个解析器和树遍历解释器对于这门语言来说只是一个周末的工作量。问题在于其他所有事情:编写一个高效的蒙特卡洛运行时(而不是简单的解释器)、条件贝叶斯推理等等。当前版本是一个编译器,包含一个 JIT(使用了出色的 Cranelift)、一个 WASM 后端,以及大量精密的数值代码。这使得它对于一个“可爱的小周末项目”来说永远遥不可及。

借助 AI 代理构建更宏大的版本

作者在日常工作和副项目中,正在尝试探索当今 AI 代理的能力边界。例如,他正在将 15 年前为 iOS 编写的、使用过时的 Objective-C 语言开发的游戏移植到现代游戏引擎中(效果相对不错)。对于 NoiseLang,他发现 AI 非常擅长构建 JIT 部分、运行时部分和数值部分,但在提出好的语言设计想法方面很差劲——很多时候它会覆盖现有语言特性用于不同目的,或者为非正交特性想出不同的语法。

一个 IR,三个后端

在底层,~ 和分布构造器构建了一个只追加的有向无环图(DAG),称为 RvGraph。这个图是唯一的事实来源,随后会被转换成三个不同的代码路径:

  • 一个列式批处理解释器,可在任何地方运行,并作为正确性的标准;
  • 一个 Cranelift JIT,将整个表达式融合为一个原生内核;
  • 一个 WASM 发射器,在浏览器中完成同样的事情。

一个共享的模块定义了图的含义,因此两个代码生成器保持精简且不会产生分歧。任何后端无法成功编译的部分都会回退到解释器,并且结果在所有后端和核心数量上保持一致。所有测试都在三个代码路径上运行,并逐位比较结果是否相同。

让蒙特卡洛循环更廉价

所有的性能工作都围绕一个循环:抽取几百万个样本,对每个样本求值,然后对结果进行归约。少数几项技术承担了大部分工作,同时保持结果确定性(这是难点)。内核融合让每个中间值都保留在寄存器中,因此算术密集型表达式可以保持在寄存器中。PRNG(xoshiro256++)被编译进内核,而 lnsincos 变成内联多项式近似,将内核速度提升了 2 倍。

作者最喜欢的是 RNG 中的一个技巧。生成随机数是串行依赖链,因此与其对抗,不如让内核同时运行四个独立的数据流,让乱序执行核心重叠它们。这个技巧最终击败了手写的 SIMD 内核!

在作者的 14 核 M4 Pro 上,一行 P(...) 可以维持每秒约 58 亿样本,并且从单核扩展到所有核心时加速比约为 9.6 倍。每核心,生成的内核运行速度大约是 LLVM 编译的手写 Rust 的 1.15 倍以内。同样的融合循环,作为 WASM 发射后,在 V8 内部运行速度约为原生的一半到四分之三。

Noise 的定位

NoiseLang 是一门玩具语言,作者建议不要用它来做任何严肃的事情,但他希望自己大学时能有这门语言存在。对于语言爱好者来说,它是一个小型的、静态的随机变量代数,具有前向蒙特卡洛、基于表达式的、基于拒绝的推断能力。

你可能会问,它与 NumPy 或 Stan 相比如何?NumPy 需要你自己编写模拟代码,Stan 需要你声明模型并等待采样器。Noise 允许你将概率写成数学表达式,同时在底层运行蒙特卡洛来得到答案。Stan 和 PyMC 在它们擅长的领域(使用 HMC/NUTS 采样器拟合大量连续数据的后验)胜过 Noise,NumPy 在原始数组处理上胜过它。Noise 中的推断是基于拒绝的,因此它对于少量离散观测值效果很好,但对于一万个连续测量值就变得无用了,并且目前还没有状态模拟(没有马尔可夫链)。Noise 的胜出场景是:当你有一个概率问题,并且你想不费太多麻烦就得到答案时。

因此,在问题的白板阶段使用 Noise——当你想要运行刚刚写下的数学公式时;当你需要真正的后验时,转向 Stan 或 PyMC;当你需要投入生产环境时,转向 NumPy 和 JAX。

回到信号与噪声

回忆作者大学时期,有一门课叫“Señales Aleatorias Y Ruido”(西班牙语的“随机信号与噪声”)。这门课是许多学生的噩梦,包括作者自己——事实上他第一次考试没通过。但第二年重修时,换了位非常棒的教授,让这门课变得有趣。最让作者震撼的是,教授解释为什么 FM 可以在噪声信道中存活而 AM 不能。于是,他将一个能模拟这一现象的单屏 Noise 程序作为对这门课的致敬。

在浏览器中运行 NoiseLang

以上所有功能都运行在 @noiselang/core 上,这得益于编译为 WebAssembly 的 Rust 引擎。使用方式如下:

npm install @noiselang/core
import { run } from "@noiselang/core";
const result = await run(`
X ~ rand::unif(-1, 1);
Y ~ rand::unif(-1, 1);
4 * P(X^2 + Y^2 < 1)
`);
console.log(result.value); // "3.1415…" — 最后一个语句的值
console.log(result.output); // 所有 Print(...) 输出的内容

run 永远不会抛出异常,失败会以 result.error 的形式返回,并附带源代码位置。此外还有 runWithIntrospection 接口,这是 noiselang.com 上变量检查器背后的 API。

NoiseLang 可以在 noiselang.com 上直接在浏览器中玩耍。打开它,输入 X ~ unif(-1, 1); Y ~ unif(-1, 1); 4 * P(X^2 + Y^2 < 1),然后观看几百万次抽样的来自浏览器标签页的 π 估计。

关键要点

  • 一切都是分布:NoiseLang 的每个值都是一个概率分布,普通数字是 Dirac 尖峰。运算符天然映射分布到分布。
  • 变量绑定固定:同一个名称始终指向同一个固定节点,因此 X + X2XX - X 严格为 0。独立性通过 ~ 的多次使用或 `~
查看原文 →manualmeida.dev