1 ポイント 投稿者 GN⁺ 2024-02-25 | 1件のコメント | WhatsAppで共有
  • PostgreSQLとpgvectorだけでGPT-2の推論フローをSQLに移し、トークン化からTransformerブロック、次トークン生成までを再現
  • 生成系LLMは、同じ入力に対して同じ候補トークン確率を返す決定的関数に近く、応答が変わるのは候補トークンを選ぶ確率的選択の段階
  • 実装ではGPT-2のBPEトークナイザー、50,257個のトークン、768次元埋め込み、1,024トークンのコンテキスト、12個のブロック、12個のattention head、GELUベースのfeedforwardをSQLクエリとテーブルで表現
  • PostgreSQL正規表現のUnicodeプロパティ非対応や、EXPの極小値処理の限界など、データベース環境に合わせた現実的な回避策が必要だった
  • 例では"Happy New Year! I wish you"から10トークンを生成し、"Happy New Year! I wish you all the best in your new year!"を出力、執筆者の環境で2分44秒かかった

SQLでGPT-2推論パイプラインを作る

  • ChatGPTはSQLが大規模言語モデルの実装に向かないと答えたが、PostgreSQL SQLでGPT-2推論パイプラインを実装した
  • 参考にした実装解説はJay ModyのGPT in 60 Lines of NumPyで、同じ構成要素をデータベースのテーブルとクエリに移し替えた
  • 生成系LLMはllm(prompt: str) -> list[tuple[str, float]]という形の関数のように見なせる
    • 入力はテキストプロンプト
    • 出力は次に来る文字列候補とその確率の配列
    • 内部の数学とパラメータが同じなら、同じ入力に対して同じ結果を返す
  • ChatGPTのような製品が同じ質問に違う答えを返せる理由は、モデル自体よりも次トークン選択段階の確率的選択にある

テキスト生成ループ

  • 生成プロセスは、プロンプトをトークン配列に変換したあと、モデルを繰り返し呼び出して次トークンを選び、プロンプトの末尾に追加していく構造
  • 基本フローは次の段階で構成される
    • tokenize(prompt)で文字列をトークンID配列に変換
    • gpt2(tokens)が50,257個のトークンに対する確率を計算
    • select_next_token(candidates)が次トークンを選択
    • 選んだトークンを配列に追加
    • 指定トークン数、タイムアウト、stopwordなどの条件で停止
    • detokenize(tokens)でトークン配列を文字列に復元
  • このように蓄積されたトークン列は、文法・構文・意味・推論のように見える性質を持つ自然言語テキストになりうる

BPEトークナイザーをSQLで実装

  • ニューラルネットワーク入力前にテキストは数値列へ変換する必要があるが、Unicodeコードポイントをそのまま使うとトークン空間と長さの両面で非効率
  • GPT-2はByte pair encodingの変種を使う
    • トークン辞書は50,257個のコードポイントを使う
    • UTF-8バイト列と「end of text」トークンを含む
    • 最初は256個のバイトトークンから始め、頻出する隣接トークン対を新トークンとして追加する
    • このマージを50,000回繰り返して50,256個のトークンを作り、最後にend-of-textトークンを追加する
  • GPT-2トークナイザーには、バイトを文字列文字へマッピングする追加レイヤーがあり、このマッピングはOpenAI GPT-2のencoder.pyで定義されている
  • SQL実装では、OpenAIから取得したトークン辞書をtokenizerテーブルに入れ、バイト-文字マッピングをencoderテーブルに保存した
  • "Mississippilessly"の例では、recursive CTEで単一バイトから始め、マージ可能な最良の隣接対を繰り返しマージする
    • 例ではトークン数が17個から5個に減る
    • Unicodeのおよそ15万コードポイント空間の代わりに、GPT-2のおよそ5万トークン空間を使う
  • 複数語を処理する際、GPT-2は正規表現でテキストを分割し、各単語内部でマージを行う
    • PostgreSQLは正規表現でUnicode文字プロパティをサポートしないため、元のGPT-2正規表現を一部修正した
    • この修正により、適切なUnicodeサポートが損なわれた可能性がある
  • "PostgreSQL is great"はSQLトークナイザーで[6307, 47701, 318, 1049]に変換される
    • トークンクラスタはPostgreSQLĠisĠgreat
    • Ġは空白を表す

埋め込みとコンテキストウィンドウ

  • トークンIDはそのままモデル計算に使われず、埋め込みベクトルへ変換される
  • GPT-2はトークンと位置をそれぞれ埋め込む
    • WTEはword token embeddingで、50,257×768行列
    • WPEはword position embeddingで、1,024×768行列
  • 各トークン位置ごとにWTEベクトルとWPEベクトルを足して、次段階への入力ベクトルを作る
  • WPEが1,024個の位置しか持たないため、GPT-2プロンプトで使える最大トークン数は1,024個
    • この数がLLMのコンテキストウィンドウに相当する
    • モデル設計時に決まるハイパーパラメータであり、学習では変わらない
  • SQL実装ではpgvectorを使う
    • 配列にベクトル演算を直接定義して純粋なSQLだけでも可能だが、性能は低下する
    • 初期バージョンは純粋なSQL関数で動いたが遅かった

self-attentionをSQLクエリに展開

  • Transformerの中核はself-attentionメカニズムで、2017年の論文Attention is all you needに基づく
  • attentionはトークンベクトル同士が互いに影響し合うようにし、プロンプト前半の情報が最後のベクトルへ伝わるようにする
  • GPT-2実装では12組のQKV行列を使う
    • 各組が1つのattention head
    • 各headは64次元
    • c_attnは768×2304の線形変換で、結果はQKVが横に積まれた2304次元ベクトル
    • 重みとbiasはc_attn_wc_attn_bテーブルに保存される
  • attention計算前にはlayer normalizationが適用される
    • scaleとshiftパラメータはln_1_gln_1_bテーブルに保存される
  • causal self-attentionでは、後ろのトークンが前のトークンに影響できないようcausal maskを適用する
    • モデルの次トークン候補は最終的に最後の埋め込みから決まる
    • 情報の流れは最後のベクトルへ向かう必要があり、最後のベクトルの中間値が以前のベクトルに影響してはならない
  • SQL実装では、softmax計算でPostgreSQLのEXPが極小の値に失敗する問題を避けるため、入力が-745.13より小さい場合は0として扱う
  • causal maskのおかげで、新しいトークンをプロンプト末尾に追加しても以前のトークンに対する計算結果は変わらない
    • 元のGPT-2実装はこの性質を活用している
    • SQL実装では単純化のためこの再利用は行っていない

multi-head attentionと残差接続

  • 12個のheadのattention結果はそれぞれ64次元で、これを横に並べて再び768次元にする
  • 続いてc_proj_wc_proj_bに保存された学習済み線形変換でattention出力を射影する
  • multi-headed attentionの結果には元の入力が再び加えられる
    • この残差接続は元のTransformer論文に含まれていた手法
    • 学習中のvanishing gradientとexploding gradient問題を緩和する設計として紹介されている

feedforward段階とTransformerブロック

  • attentionの後にはfeedforwardニューラルネットワークが続く
  • GPT-2のfeedforward段階は3層のmulti-layer perceptronで構成される
    • 次元は768 → 3072 → 768
    • 活性化関数にはGELUを使う
  • 線形変換パラメータは次のテーブルに保存される
    • mlp_c_fc_wmlp_c_fc_b
    • mlp_c_proj_wmlp_c_proj_b
  • feedforward入力もまずln_2パラメータで正規化される
    • ln_2_gln_2_bがscaleとshiftを保存する
  • feedforward出力にも入力を再び加える残差接続が適用される
  • このattention + feedforwardの組み合わせが1つのblockであり、GPT-2は12個のblockをパイプラインのようにつなぐ
    • 各blockは独自の学習パラメータセットを持つ
    • SQLではrecursive CTEでblockを連結する
  • 最後のblock出力はln_fパラメータで再度正規化される

次トークンへ戻す

  • 最終出力のうち最後の位置の768次元ベクトルが、次トークンの埋め込みになる
  • このベクトルを再びトークンへ戻すため、最初のトークン埋め込みに使ったWTE行列を再利用する
  • 正確な逆変換は通常不可能
    • 予測埋め込みがWTEの特定行と完全一致しないことがある
    • そのため各トークン埋め込みとのdot productを計算し、近いトークンを探す
  • WTEと予測埋め込みのdot product結果は50,257個のスコア、つまりlogitになる
  • これらのスコアはsoftmaxを経て確率に変換される
    • 上位候補数はtop_n
    • 確率分布を調整する値はtemperature
    • temperatureが高いほど1位以外のトークンが選ばれる可能性が高まり、推論は予測しにくくなる
  • "PostgreSQL is great"の例で、上位5個の次トークン候補は次の通り
    • Ġfor
    • ,
    • .
    • Ġat
    • Ġto
  • temperatureが0.5、1、2に変わると、同じ候補のsoftmax確率分布も変化する

実際の推論結果とコード

  • 最終SQLは、トークンを確率に従って選択し、プロンプトに付け足す過程を繰り返す
  • モデル自体は決定的で、非決定的な要素はトークン選択に含まれる乱数だけ
  • 例の設定は次の通り
    • プロンプト: "Happy New Year! I wish you"
    • 生成トークン数: 10
    • temperature: 2
    • top_n: 1
    • SETSEED(0.20231231)を使用
  • 執筆者の環境でクエリは2分44秒実行された
  • 出力結果は"Happy New Year! I wish you all the best in your new year!"
  • クエリとインストールコードはGitHubリポジトリquassnoi/explain-extended-2024にある

1件のコメント

 
GN⁺ 2024-02-25
Hacker Newsの意見
  • 美しい。自分も SQLite で似たようなウサギ穴を掘っていたが、まだニューラルネットワークまで持ち込む段階には至っていなかった。
    makemore の講義シリーズ[0]に触発されたもので、1時間ほど進むとカウント方式からニューラルネットワークへ移るのだが、自分が到達したのもだいたいそこまでだった。
    これをリレーショナルモデルに分解して考えるのは本当に良い練習になる。
    [0] https://www.youtube.com/watch?v=PaCmpygFfXo

    • 見続けると、ニューラルネットワークがカウント方式とまったく同じテーブルを導き出し、生成時にもまったく同じ結果を出すことがわかる。
  • 良いデモだが、記事での因果マスキングの説明は、学習と推論を混同しているように見える。
    因果マスキングには、学習中に未来のトークンを「のぞき見」できないようにする目的があり、GPT系の構造では推論中に自己回帰性を強制する目的もある。
    推論時にはいずれにせよ最後のトークンだけを使うので、そのトークンは入力シーケンス全体に注意を向ける。したがって、次のトークンが最後のトークンの埋め込みだけで決まるわけではない。

  • これがGPTのドライバーループを正確に表しているのか気になる。プロンプトをトークン化し、gpt2(tokens) で50257個のトークン確率を得て、次のトークンを選び、トークンリストに追加し、停止条件を確認して、最後に逆トークン化する構造に見える。
    ただ、これは状態機械が Shlemiel the painter algorithm を実装しているように見えるので、生成作業の本質的な計算コストに疑問が湧く。

    • 人々が大規模言語モデルで言うコンテキストウィンドウとは、保持されるトークン数に上限があり、最も古いトークンが捨てられるという意味だと理解している。
      そのウィンドウはスライディングウィンドウだ。
    • その通り、それがループで、魔法はすべて gpt2 関数の中にある。
    • これはアルゴリズムのごく小さな部分だ。
      生成したトークンを文にまとめる方法を示しているだけに近い。
  • 関連資料: A GPT in 60 Lines of NumPy - https://news.ycombinator.com/item?id=34726115 - 2023年2月、コメント146件

    • この資料は記事の序盤ですでに出ている。
  • 似た文脈で、GPTをすべてスプレッドシート関数で実装し、あわせて見られる動画チュートリアルも作った。
    https://spreadsheets-are-all-you-need.ai/

    • 最初の動画が素晴らしい。
      LLMはかなり格好いいと思っているが、実際の仕組みを仕事として学ぶ必要はなかった立場として、10分の動画から得たものは、難解なHNコメントや浅い主流メディアの記事を何年も読んできたことより多かった。
      膨大な数の浮動小数点数が計算待ちで積み上がっている様子を見ると、この技術がなぜGPUをあれほど大量に消費するのかもずっと直感的に理解できる。
    • スプレッドシートはLLMの説明に自然な方法だ。
      各学習例で各パラメータの導関数を計算し、それがそのパラメータにどう対応するのかを明示的に示せば、学習過程もうまく説明できそうだ。
  • いいね。1年前なら一種の魔法のように見えていたものが、今ではこうしてうまく、ほとんど子どもでも追えるほどに説明されている。

    • この魔法が1年前に始まったわけではない。
      記事で説明しているモデルは、2019年初めに公開された GPT-2 だ。
    • 「子どもでも追えるほど」ではない。
      この記事をきちんと理解するにはコンピュータサイエンスの背景知識がしっかり必要で、タイトル自体も人類の99%にとってはアクセスしにくい。
  • GPTとLLMを完全に避けてきたのだが、この方式はテキスト出力にある程度の流暢さは作れても、質問を解釈して答える能力まではなさそうに見える。
    実際にどう動くのかを説明したり、Pythonのようなおもちゃエンジンを見せたりするシンプルなブログ記事や教育コースがあるのか気になる。
    これまで見た教材はプラットフォームの使い方に偏っていて、内部の仕組みはあまり扱っていなかった。

  • 興味深いことに、現代の機械学習はチューリング完全性を要求しない。
    それでもAGIの可能性を考えているのだから、チューリング完全性は不要だという結論になるなら、かなり面白そうだ。

    • チューリング完全性は必要に見える。
      単純な理由として、自分は頭の中でチューリング完全なコードの実行を追えるからだ。
    • トークン推論それ自体はチューリング完全ではないが、出力が副作用を作れるなら、たとえば次の反復のプロンプトを変更できるなら、まったく別の話になる。
  • 記事は素晴らしく、各構成要素の説明が明確でかなり丁寧だったので読みやすかった。
    ただ、うっかり「+ expand source」を押してしまい、あの驚くべき怪物を見た後では、ChatGPTが「SQLは大規模言語モデルの実装には向いていない」と言うのに共感してしまう。

    • 自分も押したし、折りたたみ直す方法が見つからなかった。
  • 「普通のUnicodeはニューラルネットワークとうまく合わない」というのは事実ではない。たとえば ByT5 を見ればよい。
    記事で「アルファベット」と呼んでいるものは通常語彙と呼ばれ、UTF-8バイトを語彙として使えば、トークンは149186個ではなく256個になる。
    ByT5はまさにそうしている。

    • 要点はまったく不可能ということではなく、私たちが持つ他のアプローチほどはうまく機能しないということだ。
      市場で最も性能の良いモデルがすべてトークン化を使っている事実がその証拠だ。
      トークン化が根本的にはハックに近く、理想的にはいつか何らかの方法でなくしたいと考えられていることは秘密ではない(https://twitter.com/karpathy/status/1657949234535211009)。
      原理的には、バイトレベルのトークン化の欠点を、より大きなモデルとより大きなコンテキストで補うことはできるが、実際には同程度の知能を持つモデルを学習するのにはるかに多くのリソースが必要になる。
      もちろん、単語の文字数を数えるように、トークン化がむしろ知能に悪影響を与える特定の課題もある。