- 曖昧な情報抽出プログラムは、例外ルールが積み重なるほどスパゲッティコードになりやすく、このような状態ベースのロジックは再帰型ニューラルネットワーク(RNN)として捉え直すことができる
- コードレビューのメッセージからCコード参照を見つける例では、
identifier–open_paren–close_paren のようなトークンパターンを状態として追跡する手書きの分類器として実装される
- このルールは例では**適合率100%**を示すが、
if (err) goto cleanup; のようなケースを見逃すため再現率は50%にとどまり、ルールを追加するほど State と分岐文は複雑になる
- 同じ状態機械はRNNの隠れ状態と層の計算としてエンコードでき、学習可能にするには二値の指示関数の代わりにReLU・sigmoidと学習可能な重み・バイアスが必要になる
- PyTorchのElman RNN、GRU、LSTMのような実装と、長いトークン列における勾配消失の問題が実際の学習の制約となり、データセット・ラベル・損失関数を定める過程そのものが手作業のルール設計にも役立つ
曖昧なデータ抽出がスパゲッティコードになる過程
- 生データから情報を抽出する研究用プログラムは、データが明確な仕様に従っていなかったり、特異な形式を持っていたりすると、ルールが急速に複雑化する
- 例となる作業には、ニュース記事から企業と役員を識別すること、公共調達契約をサービス種別ごとにラベル付けすること、エンジニアのメッセージにプログラムコードが含まれているかを判定することがある
- 完璧な出力を望むなら、各観測を注意深く確認し、代表的なケースに対する単体テストを書くことができる
- RとPythonはいずれもそのためのテストライブラリを提供している
- ニックネーム、同義語、英語とコードの境界のように複雑な判断ルールが必要な状況では、手作業のルールは簡単に揺らいでしまう
- ニューラルネットワークの学習アルゴリズムは、このようなルールの組み合わせを人が継続的に手直しする代わりに、データから見つける方式へと問題を変える
コードレビューのメッセージでコード参照を見つける
- 目標は、コードレビュー中に送信されたメッセージがプログラムコードを明示的に参照しているかどうかを検出すること
- 観察対象のコードベースはCで書かれていると仮定する
- 代表的なメッセージには次のようなコード参照が含まれる
render_ipa_alloc()
FTPSACK
debug_error()
NULL
IS_ERROR()
aarch64, amd64
if (err) goto cleanup;
- 単純なルール候補はそれぞれ異なる形で失敗する
- 単語の後に括弧があればコードとみなすルールは
render_ipa_alloc() のようなケースは拾えるが、if (err) goto cleanup; を見逃す
- すべて大文字の単語をコードとみなすルールは
FTPSACK と IS_ERROR() を拾えるが、AFAICT のような略語を誤検出する
- 英語ではない単語をコードとみなすルールは、エンジニアリング用語やアーキテクチャ名までコードとして誤検出しかねない
- ルール2とルール3を改善するには、
AFAICT, LGTM, USD, COVID, aarch64, amd64 のような略語・専門用語の一覧が必要になる
手書きの状態機械で作った分類器
- 単純なアルゴリズムは、メッセージにコードがあるかどうかを2段階で判断する
- 前処理: メッセージをCコードの構文要素を反映したトークン列に変換する
- 推論: トークン列がルールを満たすかどうかを検査する
- Rule 1は
underscore_identifier–open_paren–close_paren パターンをコード参照と判断する
- Python実装は
State データクラスで直前のトークン状態を保存する
previous_was_identifier
previous_was_open_paren
previous_previous_was_identifier
seen_code
contains_code はトークンを走査しながら process を呼び出し、最後に state.seen_code を返す
process は現在のトークンが close_paren で、直前が open_paren、その前が識別子なら、seen_code を True に設定する
- この分類器は例では**誤検出がなく適合率100%**を示すが、見逃すケースが多く、再現率は50%にとどまる
- Rule 2を追加すると
State のフィールドと if/elif/else の分岐が増え、ルールをさらに洗練させるほど保守が難しくなる
状態機械をRNNへ移す
contains_code と process は状態機械であり、状態機械は再帰型ニューラルネットワーク(RNN)としてエンコードできる
- RNNはトークン列を1つずつ処理し、メッセージがコードを含む条件付き確率を近似する
- Pythonの
State に対応する値は、RNNの隠れ状態として表現される
State_0 は初期状態
- 各
State_t は現在のトークンと前の状態を関数 f に入力して計算される
- 最終状態は出力層
g を通って分類結果になる
- 例のRNNは3つの隠れ層を使う
- 第1層は現在のトークンと前の状態を保存またはコピーする
- 第2層はRule 1に対応するパターンを検査する
- 第3層はコードパターンを見たことがあるかどうかを記憶する
- トークンはone-hot形式の二値ベクトルで表現される
- 手書きアルゴリズムをそのまま模倣するには、二値の指示関数
1{x > 0} を使うことができる
- 隠れ層を二値のまま保てる
- しかしほとんどすべての場所で導関数が0なので、学習には不向きである
identifier, open_paren, close_paren の積でパターンを検査することもできるが、二値の隠れ層では和で同じ検査を表現できる
- Giles et al. (1992) は、状態機械を発見するために second-order RNN を使った事例としてつながる
学習可能なネットワークへ変える
- 学習するには、二値の指示関数の代わりにReLUを使う
- 数値定数は重みとバイアスに置き換えられ、勾配降下法がこれらのパラメータを推定する
- 出力層はsigmoid活性化関数で最終的な確率値を計算する
- この形はPyTorchに入れて学習できるが、そのまま学習させても性能はあまり高くない
- 性能が不足する理由の1つは、アーキテクチャが一般的ではなく、学習手順のより多くの部分がPythonの接続コードで実行され、PyTorchのC++ライブラリ実装をあまり活用できないためである
PyTorch実装と長いメッセージの制約
- PyTorchの torch.nn.RNN はElman RNNベースの実装を提供する
- 例のアーキテクチャとElman RNNでは、隠れ層の接続方法が異なる
- 例のアーキテクチャでは、トークン
t の第1層がトークン t-1 の第3層を入力として受け取り、各層は直前の層だけを入力として受け取る
- Elman RNNでは、各隠れ層が同じ層の前時刻の状態も入力として受け取る
- Elman RNNの第1隠れ層は、前時刻の最終層を入力として受け取らない
- 実際のコードレビューメッセージは長くなることがあり、長いメッセージは長いトークン列につながる
- 長い系列では、勾配降下法が理論上は機能しても、勾配消失のために数値安定性の問題が生じる可能性がある
- Elman RNNもこの問題に弱い可能性があり、GRUやLSTMのほうがコード検出タスクでより良い性能を出せるかもしれない
データ駆動の規律
- RNNは、人の手では扱いにくいルールの組み合わせを学習対象へと変え、問題定義をより明確にすることを強いる
- ネットワークを学習するには次が必要になる
- 学習データセットと検証データセットの選択
- 事前ラベル付け
- 分類器が達成すべきことと避けるべきことを明示する損失関数
- この過程は、予想していなかったグレーゾーンをあらわにし、判断基準をより明確にする
- このようなデータ駆動の規律は、ニューラルネットワークではなく手書きアルゴリズムで解く問題にも有用である
1件のコメント
Hacker News の意見
この記事はテストや学習データの確保についてはあまり扱っていませんが、そこが核心のように見えます。
理解していると感じられるコードは、すべての入力に一般化される性質を、非公式にでも自分で証明できているからです。たとえばソートアルゴリズムは、テストしたリストだけでなく、どんなリストでもソートします。
ニューラルネットワークで不確かな点は、どのように一般化されるのか分からないことにあります。見たことのない入力が少し違うだけで保証された性質はなく、そもそも望む性質を数学的に仕様化するのが難しい問題かもしれません。
ある性質を QuickCheck のようなプロパティベーステストとして使えるほど明確に定義できるなら、ランダム化によって大量のテストデータや学習データを作れます。望ましい例を一つ起点にして、正例・負例のあり得る変形を生成するテストを書けばよいのです。
証明ではありませんが、出発点にはなります。少なくとも証明できるなら、何を証明すべきかは分かります。
こういうものがあるなら、スパゲッティコードに依存することとニューラルネットワークに依存することはかなり似て見えます。ほかの性質も満たしたいなら、プロパティベーステストをもう一つ書けばよいでしょう。ニューラルネットワークは直接修正する代わりに学習させられるかもしれませんが、コード修正にも AI 支援があります。
それでも自分ならコードのほうをより信頼すると思います。少なくともデバッグはできますから。
実用的な作業を行うニューラルネットワークを作る方法として読むと、興味深い記事です。ですが、次に入力をパースしなければならないときにこの方法をそのまま採ると言われたら、正直どう言えばいいのか分かりません。
筆者は、緩く定義されたパターンを持つ任意入力のパースという難しい問題を取り上げ、これが読みにくいスパゲッティコードを生みやすいと正しく述べています。
ところがその代案として、いまだに動作原理が研究されているほど読みにくいコード、つまりニューラルネットワークを提案しています。
理解できるが「醜い」ものより、そもそも解釈不能なものを過大評価すべきではありません。タスクによっては機械学習モデルが適している場合もありますが、多くの場合は、労力がかかっても、なぜそう動くのかを読んで確認できるほうが、不可能であるよりはましです。
結局、機械学習は選択肢を一つ増やすだけです。適しているかどうかは評価結果と、選んだアルゴリズムに必要な決定性・説明可能性の水準に左右されます。
気になるのは、RNN が正しい選択なのかという点です。学習が必要で、手元にあるよりはるかに多くの例が必要になるかもしれません。ただし、既知のルールをもとに正例・負例の合成データは作れそうです。
なぜそう動くのか見られるシステムは良いものですが、間違った答えを出し続けるなら意味がありません。実運用では、どうやって答えに到達したかを知ることより、正しい答えを得ることのほうが重要な場合が多いです。
ニューラルネットワークには普遍近似定理があります。任意の関数を、望む精度水準まで表現またはエンコードできるという内容です[0]。
しかし、そのような近似を学習できるとか、どのように学習するかについての定理はありません。
[0] https://en.m.wikipedia.org/wiki/Universal_approximation_theo...
これは、畳み込みニューラルネットワーク、Transformer、LSTM のような現実的で有用なニューラルネットワークが実際にどう動くのかを説明するものとは程遠いです。
興味深いことに、この定理は単層ネットワークに関するものです。実際には、複数の層を置くほうがはるかにうまく動きます。
本当に良い記事で、RNN 周辺のより深い数学的概念を完全には理解していませんが、いろいろ考えさせられます。
最近探っていたことに似ていると感じます。アプリを前向き推論アルゴリズムと組み合わせて作る方法です。筆者は RNN を使い、私は Rete アルゴリズムに入れて作っているところです。
入力文字列を文字単位で消化するものとして考える点も強力だと思います。そうすれば推論ロジックはアルゴリズムに任せ、こちらはごく薄い入出力ロジックだけを書けば、残りはアルゴリズムが処理してくれます。
この記事が良いのは、ある関数を RNN に変えるとは実際に何を意味するのか、そしてそれを PyTorch に含まれる「バッテリー同梱」の RNN と比較しながら、学習経験として説明している点です。
質問は、状態をモデル化するにはネットワークに隠れ層を 3 つ追加する必要があると述べていましたが、なぜ 3 つなのかということです。実装しようとしていた特定のルールの結果なのか、それともこの種のルールをこのアーキテクチャで実装する際に一般的に使う層数なのかが気になります。Elman 構造ならもっと少ない層で可能だったのかも気になります。
1 番目の層は、現在のトークンから分かることと、前のトークン計算後に分かっていたことを集めます。2 番目の層は、判定ルールを満たすかどうかを確認し、現在のトークンがプログラムコードのように見えるかを判断します。3 番目の層は、その判断を以前のトークン群に対する判断と比較します。
これも単一の隠れ層に圧縮できそうです。ReLU が非線形性を捉えるのに十分なので可能に見えます。Elman 構造との対応はまだ十分に見ていないので、答えは分かりません。
RNNはトランスフォーマーに完全に吸収されたのか? RNN の扱い方は忘れて、トランスフォーマーだけに集中してよいのか気になる
ここでは、特定の狭い定義のトランスフォーマー、つまり因果マスキングを持つトランスフォーマーが RNN と等価であり、その逆も可能であることを示している
同様に、最近注目されているアーキテクチャである Mamba(https://arxiv.org/abs/2312.00752)も、ゲート付き RNN と等価なユニットを持つ。性能上の理由から、学習時には等価な CNN を、推論時には RNN を使うのだと理解している
これこそが実はトランスフォーマーの重要な利点である。近距離と遠距離の関連付けが、より難しくなったり簡単になったりしない。ただし理論上、RNN は無限に遠い過去も記憶できる
この7年間、リサーチエンジニアとして機械学習/LLM に携わり、FAANG の研究所でも働いたが、RNN を学ばなければとずっと思っていただけで、実際に学んだことも必要になったこともない
興味があるなら遺伝的プログラミングを見てみるとよい。同じ問題に対する、より単純なアプローチだと思っており、数学は不要
プログラムを抽象構文木ベースで組み替え、何らかのヒューリスティックを与えると、その基準に合わせてプログラムを最適化する。魔法はヒューリスティック関数にあり、速度、プログラム長、複雑な構造や関数呼び出しの最小化、ネットワーク効率、あるいはそれらの組み合わせなど、最適化したい対象を選べる
https://youtu.be/tTMpKrKkYXo
https://www.human-competitive.org/
最近、こうした曖昧な作業のためにローカル LLMとインターフェースするアイデアを探ったブログ記事を書いた
ニューラルネットワークを直接コーディングするより、その方が筋が通っているのではないかと思う。llama.cpp のようなものを使って、小さなモデルが問題をそのまま解けるか評価し、無理ならファインチューニングしたうえで、望むラッパーから llama.cpp とプログラム的に連携する方が、より実用的に見える
リカレントニューラルネットワークは任意計算に使うことができ、チューリングマシンとの等価性も証明されている。しかし、その用途にはまったく現実的ではない
この記事の方式は、どうにかして学習された状態機械のように見える。記事にはもっと長い要約があるとよく、“Python” はまったく関係があるように見えない。実際の Python セマンティクスを学習するのは、言語の特性上かなり難しいはずだ。標準があるのではなく、CPython がやる通りにやる言語だから
ならば、コードレビューコメントのような形式的な自然言語を扱えない理由があるだろうか?
その場合、推論はランダム入力で実行してランダムな “Shakespeare” を作っていたが、言語構造とスタイルはそれでも RNN が学習していた。分類にも使えるかもしれない
まず Python をニューラルネットワークにコンパイルし、その後それをトランスフォーマーベースのニューラルネットワークに無理やり載せればよい
そうすれば Transformer Virtual Machine(TVM) が任意のプログラムを実行できる
転移学習、つまり重みを互いに重ねる方式を使えば、LLM がアルゴリズムを深くエンコードした状態で「生まれる」ことができる