コンピュータにおいて、メモ化(またはメモ化処理)は、主にコンピュータプログラムの実行速度を向上させるために使用される最適化手法です。これは、高コストな純粋関数呼び出しの結果を記憶しておくことで、同じ入力が再び発生した場合にこれらの結果を迅速に取得できるようにします。これは一種のキャッシュであり、通常はハッシュテーブルを使用して実装されます。また、メモリ使用量を増やすことでプログラムの実行時間を短縮するという、空間と時間のトレードオフの典型的な例でもあります。メモ化はどのプログラミング言語でも実装できますが、一部の言語にはプログラマが関数を簡単にメモ化できる組み込みのサポートがあり、また、特定の関数をデフォルトでメモ化する言語もあります。
メモ化は、単純な相互再帰下降構文解析など、他のコンテキスト(および速度向上以外の目的)でも使用されています。[ 1 ]一部の論理プログラミング言語のコンテキストでは、メモ化はテーブル化とも呼ばれます。[ 2 ]
メモ化という用語は、 1968 年にドナルド・ミッチーによって造語され[ 3 ] 、ラテン語のmemorandum (「記憶されるべきもの」)に由来し、アメリカ英語では通常memoと短縮され、したがって「関数の結果を記憶されるべきものに変換する」という意味を持ちます。メモ化は、語源的に同源であるため、記憶と混同される可能性がありますが、メモ化はコンピューティングにおいて特別な意味を持ちます。
メモ化された関数は、指定された入力セットで初めて呼び出されると、入力と計算結果を格納します。記憶された入力で後続の呼び出しが行われると、関数は再計算するのではなく、記憶された結果を返すため、再計算のコストが削減されます。記憶された関連付けのセットは、関数の性質と使用方法に応じて、置換アルゴリズムによって制御される固定サイズのセット、または固定セットになります。関数は、参照透過性がある場合にのみメモ化できます。つまり、関数を呼び出すことが、その関数呼び出しを戻り値で置き換えることとまったく同じ効果を持つ場合のみです。(ただし、この制限には特別な例外があります。)メモ化はルックアップテーブルと関連していますが、その実装ではルックアップテーブルがよく使用されるため、メモ化は結果を事前に提供する必要はなく、実行時に透過的にキャッシュに格納します。
メモ化された関数は、コンピュータのメモリ空間をより多く使用する代わりに、速度が最適化されています。アルゴリズムの時間/空間「コスト」は、コンピューティングにおいて計算複雑度という特定の名前で呼ばれています。すべての関数は、時間(つまり、実行にかかる時間)と空間の両方において計算複雑度を持っています。
空間と時間のトレードオフ(つまり、使用される空間が増えるほど速度が向上する)は発生するものの、メモ化はコンパイル時ではなく実行時の最適化であるという点で、強度削減など、時間と空間のトレードオフを伴う他の最適化とは異なります。さらに、強度削減は乗算のようなコストのかかる演算を加算のようなコストの低い演算に置き換える可能性があり、その結果得られる節約効果はマシンに大きく依存する(マシン間で移植性がない)のに対し、メモ化はマシンに依存しないクロスプラットフォーム戦略です。
関数 factorial ( nは非負整数)nが0の 場合 1を返す(0! = 1という慣例による ) それ以外n の 階乗 ( n – 1) 倍を返します[ n より 1 小さいパラメータで階乗を再帰的に呼び出します] endif 関数終了
となるすべての整数nに対してn ≥ 0、関数の最終結果は不変factorialです。 として呼び出された場合、結果は常にxに値 6 が割り当てられます。 上記の非メモ化実装では、関連する再帰アルゴリズムの性質上、結果に到達するにはn + 1 回の の呼び出しが必要となり、これらの呼び出しのそれぞれには、関数が計算された値を返すのにかかる時間というコストが伴います。マシンによっては、このコストは次の合計になる可能性があります。x = factorial(3)factorial
factorialを乗算するコスト。メモ化されていない実装では、すべてのトップレベル呼び出しには、nfactorialの初期値に比例するステップ 2 から 6 までの累積コストが含まれます。
以下に、この関数のメモ化バージョンをfactorial示します。
関数 factorial ( nは非負整数)nが0の 場合 1を返す(0! = 1という慣例による )そうでなければ、 nがルックアップテーブルにある 場合、n のルックアップテーブル値 を返します それ以外 x = factorial(n – 1) × n [ nより1小さいパラメータでfactorialを再帰的に呼び出す ]xをルックアップテーブルのn番目のスロットに 格納する[ n!の結果は後で使用するので覚えておく] xを返す endif 関数終了
この例では、factorial最初に 5 で呼び出され、その後 5 以下の任意の値で呼び出された場合、factorialは 5、4、3、2、1、0 の値で再帰的に呼び出され、それぞれの戻り値が保存されるため、これらの戻り値もメモ化されます。次に 7 のような 5 より大きい数で呼び出された場合、再帰呼び出しは 2 回 (7 と 6) のみ行われ、5! の値は前回の呼び出しから保存されます。このように、メモ化によって関数は呼び出される回数が増えるほど時間効率が向上し、結果として全体的な高速化につながります。
メモ化の極端な例として、シングルトンパターン、特にそのゲッターの実装が挙げられます。ゲッターとは、最初の呼び出し時にオブジェクトを作成し、そのインスタンスをキャッシュし、以降のすべての呼び出しで同じオブジェクトを返す関数です。
関数型プログラミング言語のコンパイラでは、名前呼び出しによる評価戦略が用いられることが多いため、メモ化が多用されています。引数値の計算に伴うオーバーヘッドを回避するため、これらの言語のコンパイラは、サンクと呼ばれる補助関数を多用して引数値を計算し、これらの関数をメモ化することで計算の重複を防いでいます。
メモ化は、上記のメモ化されたバージョンの実装とほぼ同じ方法で、コンピュータ プログラマによって関数に内部的に明示的に追加できますが、参照透過関数は外部的に自動的にメモ化することもできます。[ 1 ]ピーター ノーヴィグが採用した手法は、 Common Lisp (彼の論文で自動メモ化を実証した言語)だけでなく、他のさまざまなプログラミング言語にも適用できます。自動メモ化の応用は、項書き換え[ 4 ]や人工知能[ 5 ]の研究でも正式に検討されています。factorial
関数が第一級オブジェクトであるプログラミング言語( Lua、Python、Perl [ 6 ]など)では、指定されたパラメータセットに対して値が計算された後、(実行時に)関数をその計算値で置き換えることで、自動メモ化を実装できます。この値と関数オブジェクトの置き換えを行う関数は、参照透過的な関数であれば何でも汎用的にラップできます。次の擬似コードを考えてみましょう(関数は第一級値であると仮定します)。
関数メモ化呼び出し(Fは関数オブジェクトのパラメータ)Fに配列値が添付されていない 場合は、valuesという名前の連想配列 を割り当てます。Fに値を 関連付ける。 end if; F.values [arguments]が空の場合 、F.values [ arguments] = F (arguments); end if; return F.values[arguments] ; 関数終了
factorial上記の戦略を使用して、factorial直接呼び出すのではなく、自動的にメモ化されたバージョンを呼び出すには、コードが を呼び出します。このような呼び出しはそれぞれ、まず結果を格納するためのホルダー配列が割り当てられているかどうかを確認し、割り当てられていない場合はその配列をアタッチします。位置( が連想配列のキーとして使用される) にエントリが存在しない場合、指定された引数を使用して に実際の呼び出しが行われます。最後に、キー位置にある配列のエントリが呼び出し元に返されます。memoized-call(factorial)(n)values[arguments]argumentsfactorial
上記の戦略では、メモ化対象の関数を呼び出すたびに明示的にラップする必要があります。クロージャを許容する言語では、デコレータパターンでラップされたメモ化関数オブジェクトを返すファンクタファクトリを介して、暗黙的にメモ化を実行できます。擬似コードでは、次のように表現できます。
関数コンストラクタ(Fは関数オブジェクトのパラメータ)memoized-version という名前の関数オブジェクトを割り当てます。 メモ化バージョン(引数)をselfに配列値が添付されていない 場合、[ selfはこのオブジェクトへの参照です]values という名前の連想配列を割り当てます。自身に値を 関連付ける。 end if; self.values [arguments]が空の場合 self.values [arguments] = F (arguments); end if; return self.values [arguments] ; end let; メモ化バージョン を返す。 関数終了
を呼び出す代わりにfactorial、次のように新しい関数オブジェクトmemfactが作成されます。
memfact = construct-memoized-functor(factorial)
上記の例では、関数が呼び出し前に既に定義されていることを前提としています。このfactorial時点以降、nの階乗が必要な場合はいつでも が呼び出されます。Lua などの言語では、関数を同じ名前の新しい関数に置き換えることができる、より高度なテクニックが存在し、次のようなことが可能になります。construct-memoized-functormemfact(n)
階乗 = 構成メモ化ファンクター(階乗)
基本的に、このような手法では、元の関数オブジェクトを作成されたファンクタにアタッチし、実際の関数を呼び出す必要がある場合(無限再帰を回避するため)、エイリアスを介してメモ化される元の関数への呼び出しを転送します。以下はその例です。
関数コンストラクタ(Fは関数オブジェクトのパラメータ)memoized-version という名前の関数オブジェクトを割り当てます。 メモ化バージョン(引数) をselfに配列値が添付されていない 場合、[ selfはこのオブジェクトへの参照です]values という名前の連想配列を割り当てます。自身に値を 関連付ける。alias という名前の新しい関数オブジェクトを割り当てます。自身にエイリアス を付与する。[後で間接的にFを呼び出すことができるようにするため] self.alias = F ; end if; self.values [arguments]が空の場合 self.values [arguments] = self.alias ( arguments); [ Fへの直接呼び出しではありません] end if; return self.values [arguments] ; end let; メモ化バージョン を返す。 関数終了
(注:上記の手順の一部は、実装言語によって暗黙的に処理される場合があり、説明のために記載されています。)
トップダウンパーサーが曖昧な文脈自由文法(CFG)に関して曖昧な入力を解析しようとすると、すべての可能な構文木を生成するために CFG のすべての代替案を試すために (入力の長さに対して) 指数関数的な数のステップが必要になる場合があります。これは最終的に指数関数的なメモリ空間を必要とします。メモ化は1991 年に Peter Norvig によって構文解析戦略として検討され、指数関数的な時間計算量の問題を解決するために単純なバックトラッキング再帰下降パーサーに自動メモ化を導入することで、 Earley のアルゴリズム(1970)の動的計画法と状態セットの使用、および Cocke、Younger、Kasami のCYKアルゴリズムのテーブルの使用に似たアルゴリズムを生成できることが実証されました。[ 1 ] Norvig のアプローチの基本的な考え方は、パーサーが入力に適用されると、同じパーサーが同じ入力に再度適用される場合に再利用できるように、結果がメモ可能に格納されるということです。
リチャード・フロストとバーバラ・シドロウスキーもメモ化を使用して、パーサーコンビネータの指数関数的な時間計算量を削減し、その結果をメモ化純粋関数型トップダウンバックトラッキング言語プロセッサと表現した。[ 7 ]フロストは、基本的なメモ化パーサーコンビネータを構成要素として使用して、CFGの実行可能な仕様として複雑なパーサーを構築できることを示した。[ 8 ] [ 9 ]
メモ化は、1995年にマーク・ジョンソンとヨッヘン・ドーレによって構文解析の文脈で再び研究されました。[ 10 ] [ 11 ] 2002年には、ブライアン・フォードによってパックラット構文解析と呼ばれる形式でかなり詳細に研究されました。[ 12 ]
2007年、Frost、Hafiz、Callaghanは、冗長な計算を控えるためにメモ化を使用し、多項式時間(左再帰文法の場合はΘ (n 4 ) 、非左再帰文法の場合はΘ(n 3 ))であらゆる形式の曖昧なCFGに対応するトップダウン構文解析アルゴリズムについて説明しました。彼らのトップダウン構文解析アルゴリズムは、「コンパクト表現」と「局所的曖昧性のグループ化」により、指数関数的に曖昧な構文木に対して多項式空間も必要とします。彼らのコンパクト表現は、Tomitaのボトムアップ構文解析のコンパクト表現に匹敵します。[ 13 ]彼らのメモ化の使用は、構文解析器が同じ入力位置に繰り返し適用されるときに以前に計算された結果を取得すること(これは多項式時間要件に不可欠です)だけに限定されるものではなく、次の追加タスクを実行するように特化されています。
Frost、Hafiz、Callaghanは、PADL'08でこのアルゴリズムの実装を、Haskellの高階関数(パーサーコンビネータと呼ばれる)のセットとして説明しました。これにより、言語プロセッサとして直接実行可能なCFG仕様を構築することが可能になります。彼らの多項式アルゴリズムが、トップダウン構文解析で「あらゆる形式の曖昧なCFG」に対応できる能力は、自然言語処理における構文解析と意味解析において非常に重要です。X -SAIGAのサイトには、このアルゴリズムと実装の詳細が掲載されています。
Norvig はメモ化によってパーサーの能力を向上させたが、拡張されたパーサーは依然として Earley のアルゴリズムと同じ時間複雑度であり、これはメモ化が速度最適化以外の目的で使用されている例を示している。Johnson と Dörre [ 11 ]は、メモ化の速度とは関係のない別の応用例を示している。それは、言語制約の解決を、制約を解決するのに十分な情報が蓄積された構文解析の時点まで遅らせるためにメモ化を使用することである。対照的に、メモ化の速度最適化の応用では、Ford は、メモ化によって、最悪の場合のバックトラッキング動作を引き起こす言語であっても、構文解析式文法が線形時間で構文解析できることを保証できることを示した。[ 12 ]
次の文法を考えてみましょう。
S → (A c ) | (B d ) A → X ( a | b ) B → X b X → x [X]
(表記に関する注記:上記の例では、生成規則S → (A c ) | (B d ) は「SはAの後に c が続くか、Bの後にdが続くかのいずれかである」と解釈されます。生成規則 X → x [X] は「Xはxの後に任意のX が続く」と解釈されます。)
この文法は、文字列xac、xbc、xbdのいずれかのバリエーションを生成します(ここでx は1 個以上のxを意味します)。次に、この文法を構文解析仕様として使用した場合、文字列xxxxxbdのトップダウン、左から右への構文解析にどのような影響を与えるかを考えてみましょう。
ここでの重要な概念は、 「再びXに降りる」というフレーズに内在しています。先を見据え、失敗し、後退し、次の選択肢を再試行するプロセスは、構文解析ではバックトラッキングとして知られており、構文解析におけるメモ化の機会を提供する主な要因はバックトラッキングです。RuleAcceptsSomeInput(Rule, Position, Input)次のパラメータを持つ関数を考えてみましょう。
Ruleこれは検討対象となっている規則の名前です。Positionこれは、入力において現在考慮されているオフセットです。Input検討対象の入力値です。関数の戻り値は、RuleAcceptsSomeInputによって受け入れられる入力の長さRule、または、そのルールが文字列内のそのオフセットで入力を受け入れない場合は 0 とします。このようなメモ化を伴うバックトラッキングシナリオでは、解析プロセスは次のようになります。
上記の例では、Xへの下降が1 回または複数回発生する可能性があり、xxxxxxxxxxxxxxxxbdのような文字列が生成されることがあります。実際には、bの前に任意の数のxが存在する可能性があります。S の呼び出しは、 xの数だけ X に再帰的に下降する必要がありますが、B は、戻り値が16 (この特定の場合) になるため、X に下降する必要は全くありません。RuleAcceptsSomeInput(X, 0, xxxxxxxxxxxxxxxxbd)
構文述語を利用する構文解析器は、述語解析の結果をメモ化することもできるため、次のような構造を削減できます。
S → (A)? A A → /* 何らかのルール */
Aへの1回の降下へ。
構文解析器が構文解析中に構文木を構築する場合、特定の規則に対してあるオフセットで一致する入力の長さをメモ化するだけでなく、その規則によってそのオフセットで生成されるサブツリーも入力内に保存する必要があります。なぜなら、構文解析器によるその後の規則呼び出しでは、実際にその木を辿って再構築することはないからです。同様の理由で、規則が一致したときに外部コード(意味処理ルーチンと呼ばれることもあります)を呼び出すメモ化された構文解析アルゴリズムは、そのような規則が予測可能な順序で呼び出されるように何らかの仕組みを使用する必要があります。
バックトラッキングや構文述語処理が可能なパーサーであっても、すべての文法でバックトラッキングや述語チェックが必要になるわけではないため、入力の各オフセットに対して各ルールの解析結果を保存するオーバーヘッド(および解析処理が暗黙的に行う場合は解析ツリーを保存するオーバーヘッド)によって、パーサーの速度が低下する可能性があります。この影響は、パーサーがメモ化するルールを明示的に選択することで軽減できます。[ 14 ]
{{cite book}}: CS1メンテナンス: 場所の発行元が見つかりません (リンク){{cite book}}:|journal=無視されました (ヘルプ) CS1 メンテナンス: 場所が見つかりません パブリッシャー (リンク)