在Mathematica中,我如何为任意数量的参数编译Outer[]函数?

8
如果我想从两个列表list1和list2中找到所有可能的和,我使用Outer[]函数,并将Plus指定为组合运算符:
In[1]= list1 = {a, b}; list2 = {c, d}; Outer[Plus, list1, list2]
Out[1]= {{a + c, a + d}, {b + c, b + d}}
如果我想要处理任意数量的列表,例如一个列表的列表,
In[2]= listOfLists={list1, list2};
那么我知道如何找到所有可能的总和的唯一方法是使用Apply[]函数(它有简写@@)以及Join:
In[3]= argumentsToPass=Join[{Plus},listOfLists]
Out[3]= {Plus, {a, b}, {c, d}}
In[4]= Outer @@ argumentsToPass
Out[4]= {{a + c, a + d}, {b + c, b + d}}
或者只需
In[5]= Outer @@ Join[{Plus},listOfLists]
Out[5]= {{a + c, a + d}, {b + c, b + d}}
问题在于当我尝试编译时:
In[6]= Compile[ ..... Outer @@ Join[{Plus},listOfLists] .... ]
Compile::cpapot: "Compilation of Outer@@Join[{Plus},listOfLists]] is not supported for the function argument Outer. The only function arguments supported are Times, Plus, or List. Evaluation will use the uncompiled function. "
事实上,我正在使用支持的函数,即Plus。问题似乎仅与Apply[]函数有关。因为如果我给它一个固定数量的列表来outer-plus,它就可以正常工作
In[7]= Compile[{{bob, _Integer, 1}, {joe, _Integer, 1}}, Outer[Plus, bob, joe]]
Out[7]= CompiledFunction[{bob, joe}, Outer[Plus, bob, joe],-CompiledCode-]
但是一旦我使用Apply,它就会出错
In[8]= Compile[{{bob, _Integer, 1}, {joe, _Integer, 1}}, Outer @@ Join[{Plus}, {bob, joe}]]

Out[8]= Compile::cpapot: "Outer@@Join[{Plus},{bob,joe}]的编译不支持函数参数Outer。唯一支持的函数参数是Times、Plus或List。评估将使用未编译的函数。"

所以我的问题是:是否有一种方法可以规避这个错误,或者在编译函数中计算从任意数量的列表中提取的所有元素的可能总和的方法?

(另外,我不确定"compilation"是否是一个合适的标签。请给予指导。)

非常感谢。


你预计会有多少个列表,每个列表的长度是多少?根据答案,编译可能不是执行此操作的最快方式。 - joebolte
2个回答

14

一种方法是使用With,以编程方式创建编译函数:

Clear[makeCompiled];
makeCompiled[lnum_Integer] :=
 With[{listNames = Table[Unique["list"], {lnum}]},
   With[{compileArgs = {#, _Integer, 1} & /@ listNames},
      Compile @@ Join[Hold[compileArgs],
        Replace[Hold[Outer[Plus, listNames]], 
          Hold[Outer[Plus, {x__}]] :> Hold[Outer[Plus, x]], {0}]]]];

可能有更优美的方法,但这个也可以。例如:

In[22]:= p2 = makeCompiled[2]
Out[22]= CompiledFunction[{list13,list14},Outer[Plus,list13,list14],-CompiledCode-]

In[23]:= p2[{1,2,3},{4,5}]
Out[23]= {{5,6},{6,7},{7,8}}

In[24]:= p3 = makeCompiled[3]
Out[24]= CompiledFunction[{list15,list16,list17},Outer[Plus,list15,list16,list17],-CompiledCode-]

In[25]:= p3[{1,2},{3,4},{5,6}]
Out[25]= {{{9,10},{10,11}},{{10,11},{11,12}}}

希望对你有帮助。

编辑:

你可以将编译后的函数隐藏在另一个函数后面,这样它会在运行时创建并且你实际上看不到它:

In[33]:= 
Clear[computeSums]
computeSums[lists : {__?NumberQ} ..] := makeCompiled[Length[{lists}]][lists];

In[35]:= computeSums[{1, 2, 3}, {4, 5}]

Out[35]= {{5, 6}, {6, 7}, {7, 8}}

在这种情况下,您面临编译的开销,因为您每次都会创建一个全新的编译函数。您可以通过记忆化(memoization)来优雅地解决这个问题,使用Module变量以实现持久性,将您的记忆化定义局部化:

In[44]:= 
Clear[computeSumsMemoized];
Module[{compiled},
  compiled[n_] := compiled[n] = makeCompiled[n];
  computeSumsMemoized[lists : {__?NumberQ} ..] := compiled[Length[{lists}]][lists]];

In[46]:= computeSumsMemoized[{1, 2, 3}, {4, 5}]

Out[46]= {{5, 6}, {6, 7}, {7, 8}}

但是我难道不需要为所有可能的列表数量重新编译吗?我想要一个单一的编译函数,根据输入可以处理不同数量的列表。 - Jess Riedel
1
后一个函数(computeSumsMemoized)正是这样做的。当它第一次看到一些列表的数字(比如3),它会进行编译。但是只要您提供3个列表,它就不会重新编译 - 它将使用记忆化定义(已编译的函数)。但请注意,对于Outer而言,虽然可以通过编译获得一些加速(特别是如果编译为C),但可能不会非常明显。 - Leonid Shifrin
2
好的,抱歉我有点误解了重点。实际上这不是一个编译处理任意列表组合的单个函数。相反,这是一个按需自动创建的编译函数(哈希表)集合。对于单个编译函数,您确实会面临ApplyOuter的问题,而我不知道如何立刻解决这个问题。但实际上,这是否真的有那么大的区别呢?每次提供新的列表数量时,由于编译,您将面临轻微的性能损失,但这对于给定数量的列表来说只会发生一次。 - Leonid Shifrin
明白了。这似乎是一个简单问题的复杂解决方案,但可能是因为Mathematica没有更好的方法。非常感谢您的广泛帮助! - Jess Riedel
3
问题实际上并不那么简单。在过程式语言中,需要使用嵌套循环,并且循环次数取决于输入参数的数量 - 我不能马上想到如何在 C 或 Java 中完成这个任务,特别是在没有使用递归或其他技巧的情况下。 - Leonid Shifrin

4
这是我的第一篇文章。希望我能做得好。
如果您的输入是整数列表,我对在Mathematica 7中编译此函数的价值持怀疑态度。
例如:
f = Compile[{{a, _Integer, 1}, {b, _Integer, 1}, {c, _Integer, 1}, {d, _Integer, 1}, {e, _Integer, 1}}, 
        Outer[Plus, a, b, c, d, e]
    ];

a = RandomInteger[{1, 99}, #] & /@ {12, 32, 19, 17, 43};

Do[f @@ a, {50}] // Timing

Do[Outer[Plus, ##] & @@ a, {50}] // Timing

对我来说,这两个时间并没有显著的区别,但当然这只是一个样本而已。重点仅在于与编译版本相比,Outer已经相当快了。

如果你除了速度之外还有其他编译原因,你可能会发现元组(Tuples)比Outer更有用,但你仍然需要遵守编译函数需要张量输入的限制。

f2 = Compile[{{array, _Integer, 2}}, 
      Plus @@@ Tuples@array
    ];

f2[{{1, 3, 7}, {13, 25, 41}}]

如果您的输入很大,那么可能需要采取不同的方法。给定一个整数列表的列表,此函数将返回可能的总和以及达到每个总和的方式数量:

f3 = CoefficientRules@Product[Sum[x^i, {i, p}], {p, #}] &;

f3[{{1, 3, 7}, {13, 25, 41}}]

在许多情况下,这应该能够证明更加节省内存。

a2 = RandomInteger[{1, 999}, #] & /@ {50, 74, 55, 55, 90, 57, 47, 79, 87, 36};

f3[a2]; // Timing

MaxMemoryUsed[]

这个操作只花费了3秒并且内存使用很少,但是当尝试将Outer应用于a2时,内核出现了“没有更多可用的内存”错误。


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