Old/sampou.org/Memoise
Memoise
Memoi[sz]e、Memoi[sz]ation、メモ化の話題
メモ化ってなぁに?
フィボナッチ関数を考えてみよう、定義は
fib 0 = 0
fib 1 = 1
fib n = fib (n-1) + fib (n-2)
これを使って、fib 7 を計算すると
fib 7 -- fib 6 -- fib 5 -- fib 4 -- fib 3 -- fib 2 -- fib 1 -- 1
| | | | | |
| | | | | fib 0 -- 0
| | | | |
| | | | fib 1 -- 1
| | | |
| | | fib 2 -- fib 1 -- 1
| | | |
| | | fib 0 -- 0
| | |
| | fib 3 -- fib 2 -- fib 1 -- 1
| | | |
| | | fib 0 -- 0
| | |
| | fib 1 -- 1
| |
| fib 4 -- fib 3 -- fib 2 -- fib 1 -- 1
| | | |
| | | fib 0 -- 0
| | |
| | fib 1 -- 1
| |
| fib 2 -- fib 1 -- 1
| |
| fib 0 -- 0
|
fib 5 -- fib 4 -- fib 3 -- fib 2 -- fib 1 -- 1
| | | |
| | | fib 0 -- 0
| | |
| | fib 1 -- 1
| |
| fib 2 -- fib 1 -- 1
| |
| fib 0 -- 0
|
fib 3 -- fib 2 -- fib 1 -- 1
| |
| fib 0 -- 0
|
fib 1 -- 1
のような計算の樹系図になるでしょう。これを見ると fib 5 は 2 度計算して いるし、fib 4 は 3 度計算していますね。これはなんだか、とっても無駄な 気がします。
この無駄を省くには、
一度計算した結果を覚えておいて、二度目以降は律義に再計算しないで、覚え ていたものを再利用する
という手法がありそうですね。このような計算の最適化の技法を「メモ化 (memoisation)」といいます。結果を覚えたり、再利用の際に検索したりする のに、表(table) を使うような場合には、「テーブル化(tabulation)」という 用語を使うこともあります。
もちろん、メモ化が最適化技法として意味があるためには、
律義に再計算するよりも、一度計算したものを覚えておいて、それを再利用す る方が安価
という前提があります。
関数のメモ化
関数にこの性質を持たせることを、関数を「メモ化する」 (memoise あるいは memoize)といいます。そして、この性質を持たされた関数を「メモ化された」 関数といいます。
さて、上の fib がメモ化されていれば、
fib 7 -- fib 6 -- fib 5 -- fib 4 -- fib 3 -- fib 2 -- fib 1 -- 1
| | | | | |
| | | | | fib 0 -- 0
| | | | |
| | | | fib 1 -- 1
| | | |
| | | fib 2 -- 1
| | |
| | fib 3 -- 2
| |
| fib 4 -- 3
|
fib 5 -- 5
となることを期待するわけです。これならば、結果を覚えるのと、検索するコ ストが、定数オーダなら、fib 全体としては、n の線型オーダで計算ができる ことになります。
Haskell では変更可能な関数の内部状態というのを持てませんので、関数の引 数をひとつ増して、
fib :: Integer -> Integer
fib = fst . flip memofib []
type Table = [(Integer,Integer)]
memofib :: Integer -> Table -> (Integer,Table)
memofib 0 mt = (0, (0,0):mt)
memofib 1 mt = (1, (1,1):mt)
memofib n mt = case prev of
Just r -> (r , mt)
Nothing -> (r1+r2, (n,r1+r2):tb2)
where
prev = lookup n mt
(r1,tb1) = memofib (n-1) mt
(r2,tb2) = memofib (n-2) tb1
として、表を明示的にもちまわす方法にするか、 State モナドを使って、 SICPの Exercise 3.27 にある、memoize に相当する memoise を
import Control.Monad.State
memoise f x = find x >>= \ prev ->
case prev of
Just r -> return r
Nothing -> f x >>= \ r ->
ins (x, r) >> return r
ins item = get >>= put . (item:)
find n = get >>= return . lookup n
infixl 6 |+|
(|+|) = liftM2 (+)
のように定義して、
type Table = [(Integer,Integer)]
(|+|) :: State Table Integer -> State Table Integer -> State Table Integer
memofib :: Integer -> State Table Integer
memofib 0 = return 0
memofib 1 = return 1
memofib n = memoise (\ x -> memofib (x-1) |+| memofib (x-2)) n
fib :: Integer -> Integer
fib = flip evalState [] . memofib
のように定義することが可能です。
ところで、fib に関しては、メモ化は最適化手法に一つにしかすぎません。たとえば、末尾再帰で書いた fib
fib n = iter 0 1 n
where iter a _ 0 = a iter _ b 1 = b iter a b c = iter b (a+b) (c-1)
これは、O(n) で計算できる例です。
さらに、
fib n = iter 1 0 0 1 n
where iter a b p q c | c == 0 = b | even c = iter a b (p*p+q*q) (2*p*q+q*q) (c`div`2) | otherwise = iter (b*q+a*q+a*p) (b*p+a*q) p q (c-1)
とすると O(log n) で計算ができます。これらは、元のナイーブな fib の定 義とは構造の違う定義になっており、 memoisation とは別の最適化手法と考 えてよいと思います。
memoise は特殊な ($) かも?
上のコードをもうすこし整理して
import Control.Monad.State
memoise f x = find x >>= \ prev ->
case prev of
Just r -> return r
Nothing -> f x >>= \ r ->
ins (x, r) >> return r
ins item = get >>= put . (item:)
find n = get >>= return . lookup n
type Table = [(Integer,Integer)]
memofib :: Integer -> State Table Integer
memofib 0 = fromInteger 0
memofib 1 = fromInteger 1
memofib n = memoise (\ x -> memofib (x-1) + memofib (x-2)) n
fib :: Integer -> Integer
fib = flip evalState [] . memofib
とします。ほとんど変っていませんが、|+| がなくなって、+ になってますね。 これをちゃんと動かすには、(State Table Integer) が Num クラスのインス タンスであることを宣言すればよいのです。
instance Show (State Table Integer) where
show = const "<State Table Integer>"
instance Eq (State Table Integer) where
s == t = evalState s [] == evalState t []
instance Num (State Table Integer) where
(+) = liftM2 (+)
(-) = liftM2 (-)
(*) = liftM2 (*)
negate = liftM negate
abs = liftM abs
signum = liftM signum
fromInteger = return
さて、さて、
memofib n = memoise (\ x -> memofib (x-1) + memofib (x-2)) n
この部分を見ると、memoise が特殊な ($) に見えてきませんか。
Memo モジュール
Memo というモジュールを作ってみました.
module Memo where
import Control.Monad.State
class Table t where
emptyTable :: Ord a => t a b
lookupTable :: Ord a => a -> t a b -> Maybe b
insertTable :: Ord a => a -> b -> t a b -> t a b
instance (Table t, Ord a, Num b) => Eq (State (t a b) b) where
sx == sy = evalState sx emptyTable == evalState sy emptyTable
instance (Table t, Ord a, Num b) => Show (State (t a b) b) where
show sx = show (evalState sx emptyTable)
instance (Table t, Ord a, Num b) => Num (State (t a b) b) where
(+) = liftM2 (+)
(-) = liftM2 (-)
(*) = liftM2 (*)
negate = liftM negate
abs = liftM abs
signum = liftM signum
fromInteger = return . fromInteger
type Memo t a b = a -> State (t a b) b
memoise :: (Table t, Ord a) => Memo t a b -> Memo t a b
memoise mf x = do prev <- find x
case prev of
Just y -> return y
Nothing -> do y <- mf x
ins x y
return y
where find x = get >>= return . lookupTable x
ins x y = get >>= put . insertTable x y
evalMemo :: (Table t, Ord a) => (Memo t) a b -> (->) a b
evalMemo m v = evalState (m v) emptyTable
これで,メモに使う Table も少し抽象化できますね.
実装を共有する魔法
さて,memofib はもともとの fib と形は似ています.しかし,memofib は fib の定義を使って定義されているわけではありません.すんごく惜しい,は がゆい感じがしませんか?
再帰形式で定義を書く以上しかたがないですか?
ではこんなのはどうでしょう.
fix f = let x = f x in x
という最小不動点オペレータ(?) fix があれば,明示的な再帰を使わず(fix 自身は再帰定義ですが)に関数が定義できます.
fibF f 0 = 0
fibF f 1 = 1
fibF f n = f (n-2) + f (n-1)
という定義は再帰ではありませんよね.
fib = fix fibF
ちょっと面白いでしょ.これで通常定義する Fibonacci 関数ができます.さ て,ここからさらにマジックです.
gfun = (fix .) . (.)
というのを考えます.そうすると通常の Fibonacci 関数は,
ifib :: (->) Integer Integer
ifib = gfun ($) fibF
そして,memo化した Fibonacci 関数は,
mfib :: Table t => (Memo t) Integer Integer
mfib = gfun memoise fibF
と定義できます.これを見ると,($) と memoise とが対応し,型の上でも, (->) と Memo t とがそれぞれ対応しているのが解りますよね.
実際に計算させるには,
memofib :: Integer -> Integer
memofib = evalMemo (mfib :: (Memo M.Map) Integer Integer)
としてテーブルの型を確定してから使います.
*Main> memofib 100
354224848179261915075
マジックですねぇ...
- M.Map の定義はどのようになっているのでしょう?(定義がないので動かせません)
Data.Mapモジュールを使っているのではないでしょうか。
import qualified Data.Map as M instance Table M.Map where emptyTable = M.empty lookupTable = M.lookup insertTable = M.insert
Last modified : 2006/06/11 23:08:39 JST