Динамическое программирование в Mathematica: как автоматически локализовать и/или очистить имена memoized функций
В Mathematica 8.0 предположим, что у меня есть некоторые константы:
a:=7
b:=9
c:=13
d:=.002
e:=2
f:=1
и я хочу использовать их для оценки некоторых взаимосвязанных функций
g[0,k_]:=0
g[t_,0]:=e
g[t_,k_]:=g[t-1,k]*a+h[t-1,k-1]*b
h[0,k_]:=0
h[t_,0]:=f
h[t_,k_]:=h[t-1,k]*c+g[t-1,k-1]*d
Но это очень медленно и нуждается в динамическом программировании, иначе вы получите экспоненциальное замедление:
g[0, k_] := 0
g[t_, 0] := e
g[t_, k_] := g[t, k] = g[t - 1, k]*a + h[t - 1, k - 1]*b
h[0, k_] := 0
h[t_, 0] := f
h[t_, k_] := h[t, k] = h[t - 1, k]*c + g[t - 1, k - 1]*d
Теперь это очень быстро, но если мы когда-либо захотим изменить константы (скажем, использовать это в функции Манипуляции), мы должны Clear
g
и h
каждый раз. Если бы у нас были сложные взаимозависимости, было бы очень раздражать, чтобы очистить их все каждый раз, когда мы хотели получить значение от g
и h
.
Есть ли простой способ запускать g
и h
в Module
или Block
или аналогичном, чтобы я мог получать свежий результат каждый раз, когда он оценивался без экспоненциального замедления? Или даже быстрый способ создать таблицу результатов для g
и h
красивым способом? Как сказано, я хочу иметь возможность вычислять g
и h
в функции Manipulate
.
Ответы
Ответ 1
Вот один из способов, используя Block
по вашему запросу:
ClearAll[defWrap];
SetAttributes[defWrap, HoldFirst];
defWrap[fcall_] :=
Block[{g, h},
(* Same defintions with memoization as you had, but within Block*)
g[0, k_] := 0;
g[t_, 0] := e;
g[t_, k_] := g[t, k] = g[t - 1, k]*a + h[t - 1, k - 1]*b;
h[0, k_] := 0;
h[t_, 0] := f;
h[t_, k_] := h[t, k] = h[t - 1, k]*c + g[t - 1, k - 1]*d;
(* Our function call, but within a dynamic scope of Block *)
fcall];
Мы будем использовать это, чтобы дать определения f
и h
как
ClearAll[g, h];
g[tt_, kk_] := defWrap[g[tt, kk]];
h[tt_, kk_] := defWrap[h[tt, kk]];
Назовем теперь:
In[1246]:= g[20,10]//Timing
Out[1246]= {0.,253809.}
In[1247]:= h[20,10]//Timing
Out[1247]= {6.50868*10^-15,126904.}
Глобальные memoized определения не оставлены после каждого вызова - Block
позаботится о том, чтобы уничтожить их непосредственно перед тем, как выполнение завершится Block
. В частности, здесь я изменю параметры и вызову их снова:
In[1271]:=
a:=1
b:=2
c:=3
d:=.01
e:=4
f:=5
In[1279]:= g[20,10]//Timing
Out[1279]= {0.015,0.808192}
In[1280]:= h[20,10]//Timing
Out[1280]= {0.,1.01024}
Альтернативой этой схеме было бы явно передать все параметры, такие как a,b,c,d,e,f
, в функции, расширяя их списки формальных параметров (сигнатуры), но это имеет недостаток, что старые memoized определения, соответствующие различным значениям прошлых параметров, не будут автоматически очищается. Другая проблема с этим подходом заключается в том, что полученный код будет более хрупким, w.r.t изменяет количество параметров и т.д.
ИЗМЕНИТЬ
Однако, если вы хотите создать таблицу результатов, это может быть несколько быстрее, так как вы делаете это раз и навсегда, и в этом случае вы хотите сохранить все memoized определения. Итак, вот код:
ClearAll[g, h];
g[0, k_, _] := 0;
g[t_, 0, {a_, b_, c_, d_, e_, f_}] := e;
g[t_, k_, {a_, b_, c_, d_, e_, f_}] :=
g[t, k, {a, b, c, d, e, f}] =
g[t - 1, k, {a, b, c, d, e, f}]*a + h[t - 1, k - 1, {a, b, c, d, e, f}]*b;
h[0, k_, _] := 0;
h[t_, 0, {a_, b_, c_, d_, e_, f_}] := f;
h[t_, k_, {a_, b_, c_, d_, e_, f_}] :=
h[t, k, {a, b, c, d, e, f}] =
h[t - 1, k, {a, b, c, d, e, f}]*c + g[t - 1, k - 1, {a, b, c, d, e, f}]*d;
Вы вызываете его, явно передавая параметры:
In[1317]:= g[20,10,{a,b,c,d,e,f}]//Timing
Out[1317]= {0.,253809.}
(Я использовал исходные параметры). Вы можете проверить, что memoized определения остаются в глобальной базе правил в этом методе. В следующий раз, когда вы вызываете функцию с точно такими же параметрами, она будет извлекать memoized определение, а не пересчитывать. Помимо проблем с этим подходом, который я изложил выше, вы также должны следить за использованием памяти, поскольку ничего не очищается.
Ответ 2
Запоминание с использованием вспомогательного символа
Метод memoization, выставленный в вопросе, может быть изменен, так что определения g
и h
не нужно восстанавливать, когда необходимо очистить кеш. Идея состоит в том, чтобы сохранить сохраненные значения на вспомогательном символе, а не непосредственно на g
и h
:
g[0,k_] = 0;
g[t_,0] = e;
g[t_,k_] := memo[g, t, k] /. _memo :> (memo[g, t, k] = g[t-1,k]*a+h[t-1,k-1]*b)
h[0,k_] = 0;
h[t_,0] = f;
h[t_,k_] := memo[h, t, k] /. _memo :> (memo[h, t, k] = h[t-1,k]*c+g[t-1,k-1]*d)
Определения по существу те же, что и исходные memoized версии g
и h
, за исключением того, что был введен новый символ memo
. Используя эти определения, кеш можно очистить просто с помощью [email protected]
- нет необходимости переопределять g
и h
заново. Более того, кеш можно локализовать, разместив memo
в Block
, таким образом:
Block[{memo, a = 7, b = 9, c = 13, d = 0.002, e = 2, f = 1}
, Table[g[t, k], {t, 0, 100}, {k, 0, 100}]
]
Кэш отбрасывается при выходе из блока.
Воспоминание с помощью советов
Оригинальные и пересмотренные методы memoization требовали инвазивных изменений внутри функции g
и h
. Иногда, после факта, удобно вводить memoization. Один из способов сделать это - использовать технику advising - своеобразное функциональное программирование, аналогово-подклассическое в программировании OO. A конкретная реализация рекомендаций по функциям регулярно появляется на страницах StackOverflow. Однако этот метод также является инвазивным. Рассмотрим альтернативный метод добавления рекомендаций g
и h
без изменения их глобальных определений.
Трюк будет заключаться в том, чтобы временно переопределить g
и h
в пределах Block
. Переопределения сначала проверит кеш для результата и, в противном случае, вызовет исходные определения извне блока. Вернемся к исходным определениям g
и h
, которые блаженно не знают о memoization:
g[0,k_]:=0
g[t_,0]:=e
g[t_,k_]:=g[t-1,k]*a+h[t-1,k-1]*b
h[0,k_]:=0
h[t_,0]:=f
h[t_,k_]:=h[t-1,k]*c+g[t-1,k-1]*d
Основная схема для этого метода выглядит так:
Module[{gg, hh}
, copyDownValues[g, gg]
; copyDownValues[h, hh]
; Block[{g, h}
, m:g[a___] := m = gg[a]
; m:h[a___] := m = hh[a]
; (* ... do something with g and h ... *)
]
]
Временные символы gg
и hh
вводятся для хранения исходных определений g
и h
. Затем g
и h
локально восстанавливаются до новых определений кэширования, которые при необходимости делегируют исходным определениям. Вот определение copyDownValues
:
[email protected]
copyDownValues[from_Symbol, to_Symbol] :=
DownValues[to] =
Replace[
DownValues[from]
, (Verbatim[HoldPattern][from[a___]] :> d_) :> (HoldPattern[to[a]] :> d)
, {1}
]
Чтобы сохранить этот пост short (er), эта функция "copy" касается только нижних значений. Общий совет также должен учитывать дополнительные значения, подвыборы, атрибуты символов и т.д.
Этот общий шаблон легко, если утомительно, автоматизировать. Следующая макрофункция memoize
делает это с небольшим комментарием:
[email protected]
SetAttributes[memoize, HoldRest]
memoize[symbols:{_Symbol..}, body_] :=
Module[{pairs, copy, define, cdv, sd, s, m, a}
, pairs = Rule[#, Unique[#, Temporary]]& /@ symbols
; copy = pairs /. (f_ -> t_) :> cdv[f, t]
; define = pairs /. (f_ -> t_) :> (m: f[a___]) ~sd~ (m ~s~ t[a])
; With[{ temps = pairs[[All, 2]]
, setup1 = Sequence @@ copy
, setup2 = Sequence @@ define }
, Hold[Module[temps, setup1; Block[symbols, setup2; body]]] /.
{ cdv -> copyDownValues, s -> Set, sd -> SetDelayed }
] // ReleaseHold
]
После долгих раздумий мы теперь можем временно наложить memoization на версии без кэширования g
и h
:
memoize[{g, h}
, Block[{a = 7, b = 9, c = 13, d = .002, e = 2, f = 1}
, Table[g[t, k], {t, 0, 100}, {k, 0, 100}]
]
]
Объединяя все вместе, теперь мы можем создать отзывчивый блок Manipulate
:
Manipulate[
memoize[{g, h}
, Table[g[t, k], {t, 0, tMax}, {k, 0, kMax}] //
ListPlot3D[#, InterpolationOrder -> 0, PlotRange -> All, Mesh -> None] &
]
, {{tMax, 10}, 5, 25}
, {{kMax, 10}, 5, 25}
, {{a, 7}, 0, 20}
, {{b, 9}, 0, 20}
, {{c, 13}, 0, 20}
, {{d, 0.002}, 0, 20}
, {{e, 2}, 0, 20}
, {{f, 1}, 0, 20}
, LocalizeVariables -> False
, TrackedSymbols -> All
]
Параметры LocalizeVariables
и TrackedSymbols
являются артефактами зависимостей, которые g
и h
имеют для глобальных символов a
через f
.