【问题标题】:Abstracting away the algorithm loop: how to keep algorithms DRY (don't repeat yourself)?抽象出算法循环:如何保持算法干燥(不要重复自己)?
【发布时间】:2012-11-30 18:01:39
【问题描述】:

我正在为 (PO)MDP 编写一个工具箱,并且看到出现了一个糟糕的模式。尤其是在实施强化学习算法时,我倾向于重复自己。参见下面的伪算法:

arguments: epsilon

v <- initial V values
c <- initial C values

while not good-enough
   delta <- 0.0
   if in-place
        v_old <- copy(v)
    else
        v_old <- reference to v
    for s in ss
        a = some_value(s,old_v)
        old_v <- v_old[s]
        v[s] = c*a*v_old[s]
        delta = max(delta,old_v-v[s])
    if delta < epsilon
        good-enough <- true

return v

现在看看这个几乎相同的算法:

arguments: epsilon,gamma

v <- initial V values
c <- initial C values

while not good-enough
    delta <- 0.0
    if in-place
        v_old <- copy(v)
    else
        v_old <- reference to v
    for s in ss
        a,o = get_a_and_o(s)
        old_v <- v_old[s]
        v[s] = c*v_old[s]*exp(o-a)
        delta = max(delta,old_v-v[s])
    if delta < epsilon(/1-gamma)
        good-enough <- true

return v

这些算法之间有一些简单的区别,但我重复了很多次。现在我的问题是:你如何抽象出这两个示例算法之间的共同部分(适用于实际算法)?

我研究了一种方法(在 python 中),你给算法一个 pre、一个 post 和一个循环函数,它们分别在每次迭代之前、之后和为每个迭代调用,并传递一个算法状态字典来保存变量。但是这种方法似乎不是很好。有什么建议吗?

【问题讨论】:

  • 我认为在算法中使用 DRY 很困难,因为它们的方法各不相同。
  • 你总是可以在其他适用的地方重用一些算法。
  • 在这种情况下,您应该更多地关注算法的效率,而不是 DRY。

标签: algorithm generics dry abstraction


【解决方案1】:

面向对象的方法是创建一个基类,其中包含算法的公共部分,但不包含特定于应用程序的部分(即 pre、post 或 loop 函数)。相反,它只是调用了它自己没有实现的虚拟方法。

然后,当您想要实例化一个实际用例时,您将创建该基类的子类,其中仅包含基类代码需要调用的虚拟方法的实现。

【讨论】:

    【解决方案2】:

    显然,这两种算法有很多共同点:整体工作流程/步骤几乎相同,唯一的区别是步骤中​​发生的具体情况。这是功能方法大放异彩的地方:您希望替换特定功能/评估,同时保持整体结构完整。

    不赘述,看你的代码,你可以看到:

    1. 它们使用相同的输入 V
    2. 在每次迭代中,使用 V 和一些参数,生成 V 的更新值
    3. 在每次迭代中,使用新旧 V 以及一些参数,评估一个条件 - 新 V 是否足够好,或者算法是否应该继续?

    这是一个关于如何避免重复的草图:

    您可以将 2. 改写为“在每次迭代中,我们将对 V 的当前值应用一个函数,这将返回一个更新的值 V'”——显然,该函数具有签名 Updater: fun 't -&gt; 't(更新程序函数接受类型 t 的输入,并返回相同类型的输出)。

    类似地,第 3 步可以表述为“在每一步,我们将向 (V, V') 对应用一个函数,它会告诉我们是或否这是否足够好”——而这个函数需要像Finished: fun ('t * 't) -&gt; bool 这样的签名。 (给定一个 't 类型的两个项目的元组,评估并给我一个真/假答案)。

    您现在可以提取 Updater 和 Finished 函数的细节,并将它们作为参数传递给主算法(我们称之为循环搜索),按照以下方式:

    let Search (Updater: fun 't -> 't) 
               (Finished: fun ('t * 't) -> bool) 
               currentV: 't =
        v' = v
        while not Finished (v, v')
            v' <- Updater v
        return v    
    

    (上面的示例实际上并不完全正确,但传达了精神。您通常会将其写成函数式的递归,如下所示:

    let rec Search (Updater: fun 't -> 't) 
                   (Finished: fun ('t * 't) -> bool) 
                   currentV: 't =
        if Finished (v, v') 
            then return v'
        else
            Search Updater Finished v'
    

    现在不必重写整个循环,您可以定义要应用到更新步骤和完成步骤的特定函数,并且您的代码重复消失了 - 整个循环/结构保持不变,您只需编写函数完全针对手头的问题。

    我在这里做了很多手,希望这会有所帮助。如果您有兴趣,我可以提供一个 F# 或 C# 代码示例,说明工作代码的想法。

    【讨论】:

    • 但是如果 Updater 可以是 :: v -> r -> v 或者有时是 :: v -> r -> e -> (v,t) 或者有时只是 :: v 呢?这正是我的问题,我知道算法在某些时候会进行一些更新,并且我可以传入函数,但问题是如何处理以下问题:“什么参数给哪个函数”?
    • 围绕迭代所需的状态定义签名——即迭代的输入和输出。其他任何东西都应该在循环内本地计算,或者通过闭包传入。您始终可以通过包含更多变量来扩展您的状态(假设您的语言支持泛型)。
    • 好吧,这也是我最先想到的方法。您会同意使用 State Monad 是实现此权利的最通用方法吗?
    【解决方案3】:

    使用一等函数:将不同的参数类型封装在另一个类(数组、元组等)中,并将一个名为 calculateDeltaFunction 的函数传递给您的函数,然后调用它,即

    def oneDeltaWay(s, myAlgorithmArgs) : 
      ...first example...
    
    def anotherDeltaWay(s, myAlgorithmArgs) : 
      ...second example...
    
    def commonStructure(calculateDeltaFunction, functionSpecificArgs) :
      ... common code ...  
      for s in ss
        delta = calculateDeltaFunction(s, functionSpecificArgs)
        if delta < epsilon(/1-gamma)
           good-enough <- true
      ...etc...
    
    commonStructure(oneDeltaWay, firstTypeOfArgs)
    commonStructure(anotherDeltaWay, secondTypeOfArgs)
    

    【讨论】:

    • 同意,这基本上是我在回答中概述的功能方法。
    • 但是for循环如何知道将哪些参数传递给calculateDeltaFunction?特定于函数的 args 是一个字符串列表,它指定作用域变量中所需的 args 吗?
    • 嗯,您的原始代码没有显示ss 的来源,但听起来它们与上下文相关,在这种情况下,是的,您将它们作为参数传递。换句话说,您的commonStructure 函数应该是通用结构,并且它的任何与上下文相关的方面都应该作为参数传递给调用函数,该函数知道需要哪些特定策略和哪些特定参数。
    猜你喜欢
    • 1970-01-01
    • 2022-01-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多