像这样的问题通常是针对以下任一者进行编码的:Applicative
or Arrow
抽象。我只讨论Applicative
. The Applicative
类型类,发现于Control.Applicative
,允许通过提供值和函数pure
和应用于值的函数<*>
.
class Functor f => Applicative f where
-- | Lift a value.
pure :: a -> f a
-- | Sequential application.
(<*>) :: 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 https://hackage.haskell.org/package/reactive, 反应香蕉 http://hackage.haskell.org/package/reactive-banana, or mvc 更新 http://www.haskellforall.com/2014/06/spreadsheet-like-programming-in-haskell.html。我不打算讨论如何编写这些解释器或将此处给出的表示方法调整为特定的解释器。我只想讨论底层解释器能够利用这些优化所需的程序的通用表示。我链接的所有三个库都可以避免重新计算值,并且可以适应以下优化。
可观察的分享
在前面的例子中,中间节点eleven
仅定义一次,但在三个不同的地方使用。一个实现Applicative
将无法通过引用透明度看到这三个eleven
s 都是一样的。您可以假设该实现使用了一些通过引用透明度查看 IO 魔法 https://stackoverflow.com/q/25698375/414413或者定义网络,以便实现可以看到计算正在被重用。
下面是类Applicative
Functor
计算结果可以在多个计算中划分和重用。据我所知,此类并未在任何库中定义。
class Applicative f => Divisible f where
(</>) :: f a -> (f a -> f b) -> f b
infixl 2 </>
然后您的示例可以非常简单地编码为Divisible
Functor
as
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
和与阿贝尔群
典型的神经元计算其输入的加权和,并对该和应用响应函数。对于度数较大的神经元来说,将其所有输入求和是非常耗时的。更新总和的一个简单优化是减去旧值并添加新值。这利用了加法的三个属性:
Inverse - a * b * b⁻¹ = a
减法是加数的逆运算,这个逆运算允许我们从总数中删除先前添加的数字
交换律 - a * b = b * a
无论执行顺序如何,加法和减法都会得到相同的结果。这样,即使旧值不是最近添加的值,当我们减去旧值并将新值添加到总数中时,我们也会得到相同的结果。
关联性 - a * (b * c) = (a * b) * c
加法和减法可以在任意分组中进行,并且仍然得到相同的结果。当我们减去旧值并将新值添加到总数中时,即使旧值是在加法中间的某个位置添加的,这也可以让我们得到相同的结果。
任何具有这些属性以及闭包和恒等的结构都是一个阿贝尔群 http://en.wikipedia.org/wiki/Abelian_group。以下字典包含足够的信息,供底层库执行相同的优化
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