构建带有记忆功能的函数的最佳方法

20

您好,

我有一个非常缓慢和复杂的函数,称为f[x,y]。 我需要构建它的详细ContourPlot。 而且由于物理内存不足,函数f[x,y]有时会失败。在这种情况下,我必须停止评估并自己调查点{x,y}的问题。 然后,我应该可以将元素{x,y,f[x,y]}添加到f[x,y]计算值列表(称为“cache”)中,并重新启动ContourPlot评估。 ContourPlot必须从高速缓存中获取所有已计算的f值。 我希望将这样的列表存储在某个文件中,以便以后重用它。而且手动向该文件添加有问题的点可能更简单。

如果f的计算值列表可能包含10000-50000个点,实现它的最快方法是什么?


如果你需要创建{x,y,f[x,y]}的列表,使用ListContourPlot[pts]可能比ContourPlot更值得。 - Brett Champion
@Brett ListContourPlot 不会继续计算其他值以使绘图更好。我的需求是能够从某个问题点继续计算 f[x,y] 失败的 ContourPlot - Alexey Popkov
强相关:"使用纯函数进行记忆化?" - Alexey Popkov
2个回答

33

假设我们的缓慢函数具有签名f[x, y]

纯内存方法

如果您满意使用内存缓存,则最简单的方法是使用记忆化:

Clear@fmem
fmem[x_, y_] := fmem[x, y] = f[x, y]

每次以前未遇到的参数组合调用此函数时,该函数会将其定义加入其中。

基于文件支持的内存方法

然而,如果您在长时间计算过程中经常出现内存不足或内核崩溃的情况,您需要使用某种持久性来支持此缓存。最简单的方法是保留一个运行日志文件:

$runningLogFile = "/some/directory/runningLog.txt";

Clear@flog
flog[x_, y_] := flog[x, y] = f[x, y] /.
  v_ :> (PutAppend[Unevaluated[flog[x, y] = v;], $runningLogFile]; v)

If[FileExistsQ[$runningLogFile]
, Get[$runningLogFile]
, Export[$runningLogFile, "", "Text"];
]

flogfmem相同,不同之处在于它还会将一个条目写入正在运行的日志中,该日志可用于在以后的会话中恢复缓存定义。最后一个表达式会在找到现有日志文件时重新加载这些定义(或者如果不存在则创建文件)。

日志文件的文本性质在需要手动干预时非常方便。请注意,浮点数的文本表示引入了无法避免的舍入误差,因此从日志文件重新加载值后可能会得到略有不同的结果。如果这是非常关键的问题,您可以考虑使用二进制DumpSave功能,但由于在保持增量日志方面不太方便,我将把这种方法的详细信息留给读者自行决定。

SQL方法

如果内存非常紧张,您想避免为腾出空间而拥有一个大型的内存缓存以进行其他计算,则上述策略可能不适用。在这种情况下,您可以考虑使用Mathematica内置的SQL数据库完全外部存储缓存:

fsql[x_, y_] :=
  loadCachedValue[x, y] /. $Failed :> saveCachedValue[x, y, f[x, y]]

我在下面定义了loadCachedValuesaveCachedValue函数,基本思路是创建一个SQL表格,每一行都存储一个xyf三元组。每次需要值的时候都会查询这个SQL表格。请注意,这种方法比内存缓存慢得多,因此只有在计算f所需时间远大于SQL访问时间时才有意义。使用SQL方法不会受到像文本日志文件方法那样的舍入误差的影响。

接下来是loadCachedValuesaveCachedValue的定义,以及一些其他有用的辅助函数:

Needs["DatabaseLink`"]

$cacheFile = "/some/directory/cache.hsqldb";

openCacheConnection[] :=
  $cache = OpenSQLConnection[JDBC["HSQL(Standalone)", $cacheFile]]

closeCacheConnection[] :=
  CloseSQLConnection[$cache]

createCache[] :=
  SQLExecute[$cache,
    "CREATE TABLE cached_values (x float, y float, f float)
     ALTER TABLE cached_values ADD CONSTRAINT pk_cached_values PRIMARY KEY (x, y)"
  ]

saveCachedValue[x_, y_, value_] :=
  ( SQLExecute[$cache,
      "INSERT INTO cached_values (x, y, f) VALUES (?, ?, ?)", {x, y, value}
    ]
  ; value
  )

loadCachedValue[x_, y_] :=
  SQLExecute[$cache,
    "SELECT f FROM cached_values WHERE x = ? AND y = ?", {x, y}
  ] /. {{{v_}} :> v, {} :> $Failed}

replaceCachedValue[x_, y_, value_] :=
  SQLExecute[$cache,
    "UPDATE cached_values SET f = ? WHERE x = ? AND y = ?", {value, x, y}
  ]

clearCache[] :=
  SQLExecute[$cache,
    "DELETE FROM cached_values"
  ]

showCache[minX_, maxX_, minY_, maxY_] :=
  SQLExecute[$cache,
    "SELECT *
     FROM cached_values
     WHERE x BETWEEN ? AND ?
     AND y BETWEEN ? AND ?
     ORDER BY x, y"
  , {minX, maxX, minY, maxY}
  , "ShowColumnHeadings" -> True
  ] // TableForm

这段 SQL 代码使用浮点值作为主键。在 SQL 中,这通常是一个有问题的做法,但在当前情况下运行良好。

在尝试使用任何这些函数之前,您必须调用 openCacheConnection[]。完成后应该调用closeCacheConnection[]。仅需一次,您必须调用createCache[]来初始化SQL数据库。提供了 replaceCachedValueclearCacheshowCache 以进行手动干预。


我非常喜欢阅读您的回复。非常感谢!“文件支持内存”解决方案非常优雅和快速。这正是我所需要的。SQL方法非常有趣,也是一个很好的备用策略。 - Alexey Popkov
+1,我同意@Timo的观点。从未想过用SQL表来支持它。现在我只需要找到适用于这个的东西... - rcollyer
1
在如何保持运行日志每行只有一个表达式的优秀答案中,有一个有用的补充:PutAppend with PageWidth -> Infinity - Alexey Popkov
如果我的函数是关于连续变量的,我能否让Mathematica在我查询非常接近存储点时从内存中插值(线性插值)? - Thomas Ahle
@ThomasAhle 我们可以通过将保存的输入和输出值列表传递给插值函数来创建一个插值函数。恢复这些列表的方法需要根据我们选择的记忆值策略添加某种查询函数。 - WReach

8

最简单且可能最有效的方法是将缓存值设置为函数的特殊情况定义。由于哈希处理,查找相对较快。

一个函数:

In[1]:= f[x_, y_] := Cos[x] + Cos[y]

ContourPlot使用哪些点?

In[2]:= points = Last[
   Last[Reap[
     ContourPlot[f[x, y], {x, 0, 4 Pi}, {y, 0, 4 Pi}, 
      EvaluationMonitor :> Sow[{x, y}]]]]];

In[3]:= Length[points]

Out[3]= 10417

使用预先计算的值为10000次评估设置f的版本:

In[4]:= Do[With[{x = First[p], y = Last[p]}, precomputedf[x, y] = f[x, y];], {p, 
   Take[points, 10000]}];

在上面的例子中,您需要使用类似于precomputedf[x, y] = z而不是precomputed[x, y] = f[x, y]这样的语句,其中z是您在外部文件中存储的预计算值。
以下是“else”情况,它只对f进行评估。
In[5]:= precomputedf[x_, y_] := f[x, y]

比较时间:

In[6]:= ContourPlot[f[x, y], {x, 0, 4 Pi}, {y, 0, 4 Pi}]; // Timing

Out[6]= {0.453539, Null}

In[7]:= ContourPlot[precomputedf[x, y], {x, 0, 4 Pi}, {y, 0, 4 Pi}]; // Timing

Out[7]= {0.440996, Null}

在这个例子中,由于f不是一个昂贵的函数,所以时间上没有太大差别。

针对您特定的应用程序,有一个额外的建议:也许您可以使用ListContourPlot。然后您可以精确地选择要评估哪些点。


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