我正在研究我的下一个项目。这是一个预先规划阶段,所以这个问题只是为了对现有技术进行概述。
我有一个带有多个输入和输出的有向无环图(DAG),现在想想人工神经网络:
处理这种结构的常用方法是在每个(时间)步骤处理整个网络。我相信这是frp库使用的方法,例如netwire
。
现在我处于幸运的位置,我有一系列事件,每个事件都在输入节点的一个中呈现变化。我的想法是,如果我可以静态地知道给定的更改只会影响其中的一部分,我可能不必逐步调整网络中的每个节点。
示例
在上面的图像中,5,7和3是输入,11和8是隐藏的'和2,9和10是输出节点。节点5的更改只会影响节点11,实际上只影响节点2,9和10.我不需要处理节点7,3和8。
以尽可能小的延迟处理此类网络。图形的大小可能达到100k节点,每个节点进行适度的计算。
我希望有人会站出来宣传图书馆X,只是完成工作。
否则我当前的计划是从图形描述中导出每个输入节点的计算。可能我会使用Par
monad,这样我就不必自己处理数据依赖,仍然可以从多核机器中受益。
Par
计划是否可行?这取决于每个节点所需的处理量?答案 0 :(得分:8)
这样的问题通常是针对Applicative
或Arrow
抽象编码的。我只会讨论Applicative
。 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,reactive-banana或mvc-updates。我不打算讨论如何编写这些解释器或将此处给出的表示修改为特定的解释器。我只是要讨论所需的程序的通用表示,以便底层解释器能够利用这些优化。我链接的所有三个库都可以避免重新计算值,并且可以适应以下优化。
在前面的示例中,中间节点eleven
仅定义一次,但在三个不同的位置使用。 Applicative
的实现将无法通过引用透明度来查看这三个eleven
是否完全相同。您可以假设实现使用某些IO magic to peek through referential transparency或定义网络,以便实现可以看到正在重用计算。
以下是Applicative
Functor
的类,其中计算结果可以在多次计算中进行划分和重用。这个类没有在我所知道的任何地方的库中定义。
class Applicative f => Divisible f where
(</>) :: f a -> (f a -> f b) -> f b
infixl 2 </>
您的示例可以非常简单地编码为Divisible
Functor
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
可以在任意分组中执行加法和减法,但仍会达到相同的结果。这样,当我们减去旧值并将新值添加到总值时,即使在添加的中间某处添加了旧值,也可以得到相同的结果。
具有这些属性以及闭包和标识的任何结构都是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