通常,类似这样的问题会使用
Applicative
或
Arrow
抽象进行编码。我将只讨论
Applicative
。在
Control.Applicative
中找到的
Applicative
类型类允许通过
pure
提供值和函数,并使用
<*>
将函数应用于值。
class Functor f => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
如果您想将示例图编码为一个 Applicative
(用加法替换每个节点),则可以非常简单地进行编码,如下所示:
example1 :: (Applicative f, Num a) => f a -> f a -> f a -> f (a, a, a)
example1 five seven three =
let
eleven = pure (+) <*> five <*> seven
eight = pure (+) <*> seven <*> three
two = pure id <*> eleven
nine = pure (+) <*> eleven <*> eight
ten = pure (+) <*> eleven <*> three
in
pure (,,) <*> two <*> nine <*> ten
通过遍历图形,使得每个节点在其所有依赖项之后被访问,可以从图形的表示中以编程方式创建相同的编码。
有三种优化策略是无法使用仅使用
Applicative
编码的网络实现的。一般的策略是将问题编码为
Applicative
和一些额外的类,以便于优化或提供额外的功能。然后,您可以提供一个或多个实现必要类的解释器。这样可以将问题与实现分离,使您能够编写自己的解释器或使用现有库,如
reactive、
reactive-banana或
mvc-updates。我不会讨论如何编写这些解释器或将此处给出的表示方式适应特定的解释器。我只会讨论需要的程序的常见表示形式,以便底层解释器能够利用这些优化。我链接的这三个库都可以避免重新计算值,并且可以容纳以下优化。
可观察共享
在前一个示例中,中间节点
eleven
仅定义了一次,但在三个不同的地方使用。实现
Applicative
无法透过引用透明度看到这三个
eleven
是相同的。您可以假设实现使用一些
IO魔法来窥视引用透明度,或者定义网络以使实现能够看到计算正在被重用。
以下是
Functor
的
Applicative
类,其中计算结果可以分割并在多个计算中重复使用。我不知道任何地方都定义了这个类。
class Applicative f => Divisible f where
(</>) :: f a -> (f a -> f b) -> f b
infixl 2 </>
您的示例可以非常简单地编码为“可除性”函子,如下所示:
example2 :: (Divisible f, Num a) => f a -> f a -> f a -> f (a, a, a)
example2 five seven three =
pure (+) <*> five <*> seven </> \eleven ->
pure (+) <*> seven <*> three </> \eight ->
pure id <*> eleven </> \two ->
pure (+) <*> eleven <*> eight </> \nine ->
pure (+) <*> eleven <*> three </> \ten ->
pure (,,) <*> two <*> nine <*> ten
求和与阿贝尔群
一个典型的神经元计算其输入的加权和,并对该总和应用响应函数。对于具有大入度的神经元,计算所有输入的总和是耗时的。更新总和的一种简单优化方法是减去旧值并添加新值。这利用了加法的三个属性:
逆元 - a * b * b⁻¹ = a
减法是添加一个数的逆,这个逆元使我们可以从总和中删除先前添加的数字
交换律 - a * b = b * a
不论执行顺序如何,加法和减法都能达到相同的结果。这使我们在从总和中减去旧值并添加新值时,即使旧值不是最近添加的值,也能达到相同的结果。
结合律 - a * (b * c) = (a * b) * c
加法和减法可以以任意组合方式进行,仍然能够达到相同的结果。这使我们在从总和中减去旧值并添加新值时,即使旧值是在添加过程中的某个位置上添加的,也能达到相同的结果。
任何具有这些属性以及闭包和身份的结构都是
阿贝尔群。以下字典包含足够的信息,使底层库执行相同的优化。
data Abelian a = Abelian {
id :: a,
inv :: a -> a,
op :: a -> a -> a
}
这是能够对可加阿贝尔群进行求和的结构类。
class Total f where
total :: Abelian a -> [f a] -> f a
可以对地图的构建进行类似的优化。
阈值和相等性
神经网络中另一个典型操作是将一个值与阈值进行比较,并完全基于该值是否通过阈值来确定响应。如果对输入的更新不会改变值落在阈值的哪一侧,响应就不会改变。如果响应没有改变,则无需重新计算所有下游节点。能够检测到阈值未发生变化Bool
或响应相等的能力。以下是能够利用相等性的结构类。 stabilize
为底层结构提供了Eq
实例。
class Stabilizes f where
stabilize :: Eq a => f a -> f a
b
也曾经在我脑海中出现过;为了同步它们,你可能需要一些类似于 ID 的东西和/或让每个神经元知道它应该等待多少输入。 (这可能比它值得的开销还要大。) - paul