如何从有向无环图中推导FRP?

14

我正在为我的下一个项目进行研究。目前正处于预备阶段,所以这个问题只是为了了解现有技术。

设置

我有一个具有多个输入和输出的有向无环图(DAG),现在先认为它是人工神经网络:

Directed acyclic graph

处理此类结构的常见方法是在每个(时间)步骤上处理整个网络。我认为这是诸如netwire之类的frp库使用的方法。

现在我很幸运,我有一串事件,每个事件都表明一个输入节点的更改。如果我可以静态地知道给定的更改只会影响它的一部分,则意味着我可能不必在网络中每个节点都执行步骤。

示例

在上面的图像中,5、7和3是输入,11和8是“隐藏”的,2、9和10是输出节点。对节点5的更改将仅影响节点11,并在效果上影响节点2、9和10。我不需要处理节点7、3和8。

目标

尽可能地快速处理这种类型的网络。这些图表的大小可能会达到100k个节点,每个节点进行适度的计算。

计划

我希望有人能站出来宣传库X,只需完成工作即可。

否则,我的当前计划是从图形描述中导出每个输入节点的计算。可能我会使用Par单子,这样我就不必自己处理数据依赖项,仍然可以受益于多核机器。

问题

  • 是否有任何库可以满足我的要求?
  • 我的Par计划可行吗?这在多大程度上取决于每个节点所需的处理量?

1
我对Par方面不确定,但是我几个小时前问了一个类似的问题。图形上的节点不是直接连接到彼此,而是引用目标节点的Id号并查找它。为了提高性能,我选择使用数组进行查找:http://stackoverflow.com/a/25799663/3792504 - 你也可以尝试查看FGL http://hackage.haskell.org/package/fgl (基于http://web.engr.oregonstate.edu/~erwig/fgl/) - TheCriticalImperitive
好的...我会看一下你的问题。 - fho
你有研究过并发中的Actor模型吗?这似乎可以像Erlang一样创建大量的Actor(每个神经元一个),它们可以相互发送更新消息。不过我不确定在所需节点数量下,它是否能够很好地实现低延迟。 - paul
@paul 我也考虑过这个问题,但因为两个原因而放弃了:a)每个神经元的工作量可能太小,无法克服整个actor的开销。b)控制网络效果变得更加困难。使用并行处理时,“更改输入,等待,收集输出”,而使用并发处理则更像:“更改输入,等待,哦,输出上有一个变化...还有另一个...” - fho
@Florian 是的,b 也曾经在我脑海中出现过;为了同步它们,你可能需要一些类似于 ID 的东西和/或让每个神经元知道它应该等待多少输入。 (这可能比它值得的开销还要大。) - paul
显示剩余2条评论
1个回答

10
通常,类似这样的问题会使用ApplicativeArrow抽象进行编码。我将只讨论Applicative。在Control.Applicative中找到的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 和一些额外的类,以便于优化或提供额外的功能。然后,您可以提供一个或多个实现必要类的解释器。这样可以将问题与实现分离,使您能够编写自己的解释器或使用现有库,如reactivereactive-bananamvc-updates。我不会讨论如何编写这些解释器或将此处给出的表示方式适应特定的解释器。我只会讨论需要的程序的常见表示形式,以便底层解释器能够利用这些优化。我链接的这三个库都可以避免重新计算值,并且可以容纳以下优化。

可观察共享

在前一个示例中,中间节点eleven仅定义了一次,但在三个不同的地方使用。实现Applicative无法透过引用透明度看到这三个eleven是相同的。您可以假设实现使用一些IO魔法来窥视引用透明度,或者定义网络以使实现能够看到计算正在被重用。
以下是FunctorApplicative类,其中计算结果可以分割并在多个计算中重复使用。我不知道任何地方都定义了这个类。
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

哇...这是一个很棒的回答。我想这个回答比我问的问题还要多。只有一个小问题:在你的例子中,你使用pure f <*>而不是f <$>,这有什么原因吗? - fho
pure f <*>f <$> 是完全等价的。f <$> 是中缀 fmap。根据 Control.Applicative 的说法,“由于这些定律,fFunctor 实例将满足 fmap f x = pure f <*> x”。http://hackage.haskell.org/package/base-4.7.0.1/docs/Control-Applicative.html 因此 f <$> x = pure f <*> x<*><$> 都具有 infixl 4 的固定性。 - Cirdec
谢谢,我只是在想为什么有人会选择前者而不是后者。 - fho
1
另外,example2 使用传递风格,可以使用传递单子来编写。 - Mokosha

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接