什么是可微编程
通过动画、动效增加 UI 表现力,作为前端或多或少都做过。这里以弹性阻尼动画的函数为例:
函数在
时是效果最好的。最终,实现成 JavaScript 代码:
function damping(x, max) {
let y = Math.abs(x);
// 下面的参数都是来源于公式用数值拟合的结果
y = 0.82231 * max / (1 + 4338.47 / Math.pow(y, 1.14791));
return Math.round(x < 0 ? -y : y);
}
如图 1 所示,2019 年 Julia Computing 团队发表论文《A Differentiable Programming System to Bridge Machine Learning and Scientific Computing》表示他们构建了一种可微编程系统,它能将自动微分内嵌于 Julia 语言,从而将其作为 Julia 语言的第一公民。如果将可微编程系统视为编程语言第一公民,那么不论是机器学习还是其它科学计算都将方便不少。Y Combinator Research 研究者 Michael Nielsen 对此非常兴奋,非常赞同 Andrej Karpathy 的观点。Karpathy 说:「我们正向前迈出了一步,与原来对程序有完整的定义不同,我们现在只是写一个大致的解决问题的框架,这样的框架会通过权重把解决过程参数化。如果我们有一个好的评估标准,那么最优化算法就能帮我们找到更好的解(参数)。」
这里 Karpathy 说的就是传统编程和可微编程的区别,可微编程会通过梯度下降等最优化方法自动搜索最优解。但这里有个问题,程序需要梯度才能向着最优前进,因此程序的很多部分都要求是可微的。鉴于这一点,很多人也就将机器学习(ML)称呼为可微编程了。
但是可微编程只能用于机器学习吗?它能不能扩展到其它领域,甚至成为编程语言的基本特性?答案是可以的,这就是 Julia 团队及 MIT 等其他研究机构正在尝试的。近年来,机器学习模型越来越精妙,展现出了很多科学计算的特性,侧面凸显了机器学习框架的强大能力。研究者表示,由于广泛的科学计算和机器学习领域在底层结构上都需要线性代数的支持,因此有可能以可微编程的形式,创造一种新的编程思想。下面,我们就一起进入这个全新的领域。
在开始可微编程之前,我想先简单回顾一下之前计算弹性阻尼的例子。你会发现
数值微分法:
只要 h 取很小的数值,比如0.0001,那么我们可以很方便求解导数,并且可以对用户隐藏求解过程,用户只要给出目标函数和要求解的梯度的变量,程序可以自动给出相应的梯度,这也是某种意义上的“自动微分”。而我们在 H5 弹性阻尼函数的求解过程中,使用的正式这种“自动微分”技术。数值微分法的弊端在于计算量大,由于是做拟合相当于我们要把公式
符号微分(Symbolic Differentiation)属符号计算的范畴,其计算结果是导函数的表达式。符号计算用于求解数学中的公式解(也称解析解)。和前文数值解不同的是,通过符号位分得到的是解的表达式而非具体的数值。根据基本函数的求导公式以及四则运算、复合函数的求导法则,符号微分算法可以得到任意可微函数的导数表达式。
然后将自变量的值代入导数公式,得到任意点处的导数值。符号微分计算出的表达式需要用字符串或其他数据结构存储,如表达式树。数学软件如 Mathematica,Maple,matlab 中实现了这种技术,python 语言的符号计算库也提供了这类算法。
对于深层复合函数,如机器学习中神经网络的映射函数,符号微分算法得到的导数计算公式将会非常冗长。这种冗长的情况,我们称为表达式膨胀(expression swell)。对于机器学习中的应用,不需要得到导数的表达式,而只需计算函数在某一点处的导数值,从而以参数的形式更新神经元的权重。因此,符号微分在计算冗余且成本高昂。例如:对于公式
如果采用符号微分算法,当n=1,2,3,4时的 ln 及其导数如图 3 所示。
自动微分不同于数值微分和符号位分,它是介于符号微分和数值微分之间的一种方法。数值微分一开始就代入数值近似求解,而符号微分直接对表达式进行推导,最后才代入自变量的值得到最终解。自动微分则是将符号微分应用于最基本的运算(或称原子操作),如常数,幂函数,指数函数,对数函数,三角函数等基本函数,代入自变量的值得到其导数值,作为中间结果进行保留。然后,再根据这些基本运算单元的求导结果计算出整个函数的导数值。
自动微分的灵活强,可实现完全向用户隐藏求导过程,由于它只对基本函数或常数运用符号微分法则,因此可以灵活地结合编程语言的循环、分支等结构,根据链式法则,借助于计算图计算出任意复杂函数的导数值。由于存在上述优点,该方法在现代深度学习库中得到广泛使用。Julia 的论文实现的 zygote 工具进行的可微编程,研究人员定义了一个损失函数,将点光源作为输入在图像上产生光照,如图 4 所示和参考图像进行对比。通过可微编程的方式自动计算和提取梯度,并用于更新点光源的位置:
julia> guess = PointLight(Vec3(1.0), 20000.0, Vec3(1.0, 2.0, -7.0))
julia> function loss_function(light)
rendered_color = raytrace(origin, direction, scene, light, eye_pos)
rendered_img = process_image(rendered_color, screen_size.w,
screen_size.h)
return mean((rendered_img .- reference_img).^2)
end
julia> gs = gradient(x -> loss_function(x, image), guess)
在 Julia 之后 Swift 也推出了自己的可微编程,对,你没听错,就是 Apple 应用开发推荐编程语言 Swift。在 Swift 的可微编程提案中,研究人员提出了“智能应用程序”的概念。智能应用程序很智能:它们使用机器学习技术来增强用户体验。智能应用程序可以借助可微编程的强大能力,对应用的行为做出预测、提供建议并了解用户偏好,根据用户偏好智能调整应用的行为。
智能应用的核心是可微编程,自动微分可用于通过梯度下降系统地优化(即找到“好”值)参数。通过传统编程思想的算法优化,这些参数通常很难处理,要么是类似数值微分和符号微分中那样参数太多,要么是难以和应用的行为做关联。例如,我们要开发一个智能播放器,它尝试根据播放器内容类型和播放器的用户偏好自动调整播放速度。
enum PodcastCategory {
case comedy
case news
...
}
enum PodcastSection {
ca