AIエージェントもCoTもマルチモーダルも、結局「次トークン予測」の応用だった
分析結果
- カテゴリ
- AI
- 重要度
- 72
- トレンドスコア
- 36
- 要約
- AIエージェントもCoTもマルチモーダルも、結局「次トークン予測」の応用だった こたこた博 transformer ChatGPT llm 生成 AI マルチモーダル idea ChatGPTは「考えて」いないし、あなたの質問を「理解」もしていません。 え?そうなの? それでもコードを書いて、設計レビューまでしてくれますよ? 一体どうなっているのでしょうか。 仕様書も読んでいないはずなのに、仕様を要約してくる さっきまでよく知らなかった
- キーワード
AIエージェントもCoTもマルチモーダルも、結局「次トークン予測」の応用だった こたこた博 transformer ChatGPT llm 生成 AI マルチモーダル idea ChatGPTは「考えて」いないし、あなたの質問を「理解」もしていません。 え?そうなの? それでもコードを書いて、設計レビューまでしてくれますよ? 一体どうなっているのでしょうか。 仕様書も読んでいないはずなのに、仕様を要約してくる さっきまでよく知らなかった技術スタックについて、最初から詳しかったような顔で語り始める なのに、変なところで堂々と嘘をついたりもする この「賢いようで、よくわからない感じ」が、LLM(大規模言語モデル)の 気持ち悪さ だと思います。 この記事では、このモヤモヤを少しでも減らすために、 LLMの本質を「次トークン予測マシン」として捉え直し、 マルチモーダル/エージェント/Chain-of-Thought(思考の連鎖)も すべて同じ原理の上に乗っていること を、疑似コードベースで整理してみます。 「コードとして見てなんとなく腑に落ちる」レベルの深さを目指します。 そのまま動くコードではありませんので、そこはご注意ください。 1. エンジニアから見るLLMの「気持ち悪さ」 まずは、LLMに対するエンジニアの典型的なモヤモヤを挙げてみます。 仕様書がないのに動いている テストがしにくい(同じプロンプトでも毎回出力が揺れる) ソースコードも読めない、デバッガも役に立たない なのに、かなりの精度で設計レビューやコード補完をしてくる これ、普通のソフトウェアコンポーネントとして考えると、かなり異常ですよね。 「どういう入力に対して、どういう出力が返るのか」 を仕様レベルで説明してくれないコンポーネントが、システムの中枢にいる。 気持ち悪いし怖いです。 この「ブラックボックス感」を少しでも弱めるために、 まずは「LLMの核って、結局はこれだけだよね」というところまで分解してみます。 2. 核心:LLMは「次のトークンを1個ずつ予測して選んでいるだけ」 いきなりですが、まずは 極限までざっくり化した疑似コード を見てみましょう。 def generate_text (prompt_tokens, max_new_tokens = 128 ): tokens = prompt_tokens[:] for _ in range (max_new_tokens): logits = transformer_forward(tokens) # 現在のトークン列をモデルに入力し、全位置のスコアを計算する probs = softmax(logits[ - 1 ]) # 最後のトークンの確率分布を取得する next_token = sample_from(probs) # 確率分布に従って、次の1トークンを決める:greedy / sampling など tokens.append(next_token) # 選ばれたトークンを現在のトークン列の末尾に追加 if next_token == EOS_TOKEN : break return tokens やっていることを日本語にすると: いままでのトークン列(文章の先頭からここまで)を全部突っ込む 「次の位置にどのトークンが来そうか」の確率分布を出す 1トークン選ぶ それを文末に付けて、また 1 に戻る これをひたすら繰り返しているだけ です。 「そんな単純なわけないだろ」と思うかもしれませんが、疑似コードレベルで書くと本当にこの程度の処理なんです。 では、この「次トークン予測マシン」がどう学習されているのか、もう一段、深堀りします。 3. 「桃太郎」で見る学習:次の単語を当て続ける 学習時にやっていることも、本質はシンプルです。 例えば、「桃太郎」の冒頭だけを考えます。 昔々、あるところにおじいさんとおばあさんが住んでいました。 これを、トークン列だと思ってください(空白や句読点などの細かい扱いは一旦目をつぶって)。 学習時には、こんな問題をひたすら解かせています。 入力: ["昔々", "あるところに"] 正解: "おじいさん" 入力: ["昔々", "あるところに", "おじいさん"] 正解: "と" 入力: ["昔々", "あるところに", "おじいさん", "と"] 正解: "おばあさん" これをコードっぽく書くと、こんなイメージです。 def training_step (model, context_tokens, target_token): logits = transformer_forward(context_tokens) # 全ての位置のスコアを計算 pred_logits = logits[ - 1 ] # 最後のトークンの予測結果を取り出す loss = cross_entropy(pred_logits, target_token) # 正解との誤差を算出 loss.backward() # モデル内の各パラメータの勾配を逆算 optimizer.step() # モデルの重みパラメータを更新 context_tokens :いままでの文章 target_token :次に来てほしい単語(正解) cross_entropy :正解トークンの確率が高くなるようにする損失関数 これを Web全体レベルのコーパスで、ひたすら繰り返している のが事前学習です。 最初はランダムな予想しかできませんが、 何億、何兆という回数の「次トークン当てクイズ」を繰り返すうちに、 「この文脈なら、次はだいたいこの単語が来るよね」 という「勘」が異常なほど鋭くなっていきます。 ここまでのまとめ: LLMの本質は「次トークン予測ループ」 学習も、ひたすら「次トークン当ての誤差を減らす」ようにがんばる さて、これだけ聞くと「そんなものが、なんであんなに賢いんだ」と思いますよね。 そのギャップを埋めるために、内部構造を処理ブロックレベルで押さえておきます。 4. 内部構造:トークナイザ/埋め込み/Transformerをざっくり見る LLMの内部をざっくり処理ブロックレベルで分解します。 大まかには、こんな流れです。 文字列 → トークン列(ID列) トークン列 → ベクトル列(埋め込み+位置情報) ベクトル列 → Transformerブロックを N 層通す 最後の位置のベクトル → logits(各トークンのスコア) 4-1. トークン化 tokens = tokenizer.encode( "今日はいい天気ですね" ) # 文字列をID列にエンコード # 例: [1012, 42, 88, 9001, 5, 999] 4-2. 埋め込み def embed (tokens): token_vecs = token_embedding[tokens] # 各トークンを多次元の数値ベクトルに変換 pos_vecs = positional_embedding[: len (tokens)] # 文章内での「位置」を表すベクトルを生成 return token_vecs + pos_vecs token_embedding :ID → ベクトル の表(行列) positional_embedding :位置ごとのベクトル(語順情報) 4-3. Transformerブロック(かなり抽象化) Self-Attention、Feed Forwardは一旦ブラックボックスにしておきます。 def transformer_block (x): x = x + self_attention(norm(x)) # どの単語が重要かを判断し、文脈情報を強化 x = x + feed_forward(norm(x)) # Attentionで集めた情報を整理・加工し、より高度な表現に変換 return x 4-4. 次のトークン予測の処理全体 def transformer_forward (tokens): x = embed(tokens) # 入力されたトークン列を、位置情報込みのベクトルに変換 for block in transformer_blocks: # Transformerブロックを N 層通す(例えば 24 層とか 48 層とか) x = transformer_block(x) logits = final_linear(x) # 最終的なベクトルを、全語彙に対応するスコア(logits)に変換 return logits この処理ブロックが、「 文脈を読んで、各位置の意味ベクトルを更新する関数 」だと思ってもらえれば十分です。 ここまでざっくりと見通せたところで、いよいよ「拡張」の話に進みます。 5. 拡張①:マルチモーダル ― 画像もトークンに落とせば同じ土俵 ここからが面白い部分です。 テキスト専用だったはずのLLMが、 なぜ画像や音声まで扱えるようになったのか? という話を、あくまで「次トークン予測」の延長として見ていきます。 5-1. 画像 → 離散トークン ざっくりいうと、画像も 小さなパッチ(例えば 16×16)に分割し、 それぞれを特徴ベクトルに変換し、 それを「似たものを同じIDにする」感じで離散化する ことで、「画像トークンID列」に変換できます。 疑似コードでいうと: def image_to_tokens (image): patches = split_into_patches(image) # パッチに分割 feats = vision_encoder(patches) # 特徴ベクトルの列に変換 img_tokens = quantize(feats) # 離散化したIDにマッピング return img_tokens vision_encoder は CNN や ViT など、実装はいろいろですが、やっていることは「画像を特徴ベクトルの列に変換する」ことです。 5-2. テキストと画像を同じシーケンスに流し込む あとは、テキストトークンと画像トークンを 同じシーケンスに連結 してしまえばOKです。 def multimodal_qa (question_text, image): text_tokens = tokenizer.encode(question_text) img_tokens = image_to_tokens(image) seq = [ BOS ] + text_tokens + [ IMG_TOKEN ] + img_tokens # テキストと画像を「1本のシーケンス」に連結 answer_tokens = generate_text(seq, max_new_tokens = 128 ) # 続きのテキスト(回答)を生成 return tokenizer.decode(answer_tokens) # 生成されたトークン列を人が読めるテキスト(文字列)に戻す ここでも重要なのは、 モデル側から見ると、「ただトークン列が長くなっただけ」 画像だからといって特別なことはしていない ひたすら 「次のトークンIDはどれか」 を予測しているだけ という点です。 6. 拡張②:エージェント ― ツール呼び出しもテキストとして吐いているだけ 次の拡張は「エージェント」です。 文章を返すだけのモデルのはずが、 なぜフライト予約やコード実行までできるのか? 答えを先に言うと、これもやはり「テキスト」です。 6-1. LLMから見れば、JSONもただの文字列 最近のエージェント系では、LLMにこのようなフォーマットの出力をさせます。 { "tool" : "web_search" , "args" : { "query" : "大阪 出張 フライト 3月15日" } } LLM側から見れば、これは「 { とか "tool" とかのトークン列」にすぎません。 「関数を呼んでいる」わけではありません。 単に 「関数を呼んでくれ」と書かれた文字列 を出力しているだけです。 6-2. 実際に動かしているのは外側のループ 実際にツールを呼び出しているのは、LLMの外側にいる処理です。 def agent (user_input): messages = [{ "role" : "user" , "content" : user_input}] while True : llm_output = call_llm(messages) if is_tool_call(llm_output): # ツール呼び出し指示の有無チェック tool_name, args = parse_tool_call(llm_output) # 呼び出し指示JSONデータを解釈 result = execute_tool(tool_name, args) # ここで外部APIなどを実行 messages.append({ "role" : "tool" , "name" : tool_name, "content" : result, }) # ツール処理結果をプロンプト文字列に追加 else : # ツール呼び出し指示が無くなった return llm_output # 最終応答を返す call_llm :いつもの ChatCompletion 的な呼び出し is_tool_call :LLMの出力がJSONっぽいかどうかを判定 execute_tool :HTTPクライアントやDBクライアントなど、普通のコード 「エージェントすごい!」と言われることが多いですが、 魔法の正体は「LLMがJSONっぽい文字列を吐き、周りがそれを実行しているだけ 」です。 やっぱり、LLM自身は 次のトークンを予測してテキストを生成しているだけ なんですね。 7. 拡張③:思考の連鎖 ― 「考え方」もテキストとして書かせる 最後の拡張は、 Chain-of-Thought(CoT) 、思考の連鎖です。 次トークン予測マシンに、どうやって「考えさせる」ことができるのか? 7-1. 普通の回答とCoT回答 普通の問い: Q. ある商品を1個1000円で仕入れて、20%の利益を乗せて売りました。販売価格はいくらですか? 通常のプロンプト: answer = llm( "Q: ... 販売価格はいくらですか?" ) # -> "1200円です。" CoT付きプロンプト: reasoning = llm( "Q: ... 販売価格はいくらですか? \n Let's think step by step." ) # -> "仕入れ値1000円の20%は200円なので、1000+200=1200円です。" 7-2. 二段階プロンプトにしてみる CoTをもう少し一般的に書くと、こんな感じに分解できます。 def cot_answer (question): # 1. まず「思考過程(中間ステップ)」を生成 reasoning = llm(question + " \n Let's think step by step." ) # 2. その思考を要約して、最終回答だけを出させる final = llm( "以下の推論を一文で答えだけ述べてください: \n " + reasoning) return final ポイントは、 「思考過程」もテキストで生成される=トークン列 それを一度外に出してから、もう一回LLMに読ませて処理させる 実質的に計算ステップを増やしている という点です。 人間が紙に途中式を書きながら考えるのと、かなり構造が似ています。 結局ここでも、「思考」そのものが中間トークン列として扱われているだけなんですね。 8. 創発と限界:単純なルール×スケールの先に見えるもの ここまでで見てきた通り、 事前学習:ひたすら次トークン予測 マルチモーダル:画像や音声もトークン列にする エージェント:ツール呼び出し命令もトークン列にする CoT:思考過程もトークン列にする というように、 全部「トークン列」と「次トークン予測」の話 としてまとめることができます。 それでも、ある規模を超えたLLMが、 簡単な推論問題を解けるようになったり 翻訳性能がいきなり良くなったり 設計レビューやコード修正案まで出してきたり するのは、ある種の「創発的な振る舞い」と言えます。 これについては、まだ理論的な理解が完全ではありませんが、 ルールはシンプルでも、スケールさせると質が変わる という現象として捉えておくと、何が起こっているのかを理解できると思います。 一方で、ハルシネーションのような「自信満々の誤答」も、 次トークン予測は、出力内容の真偽は問題にしていない 「文として自然かどうか」 を優先する次トークン予測の結果 として説明できます。 9. まとめ:LLMを「設計可能なコンポーネント」として見る 最後に、この記事で「わかった」「あぁそうか」と思ってもらいたいポイントを箇条書きにします。 LLMの核は「次トークン予測ループ」であり、疑似コードで書くとかなり単純 マルチモーダル/エージェント/CoTは、 何をトークンとして扱うか それをどのループで使うか を変えているだけだが、それによって知能的な能力を高めている 「LLMが何をしているのか」を今回のような抽象度でつかんでおくと、 どこから外部システム(ツール・DB・ルールベース)に任せるか どこまでLLMに自由にさせるか を設計しやすくなる 得体の知れない魔法っぽかったものが、「癖は強いけど設計可能なコンポーネント」に見えてきたでしょうか? 皆さんのプロジェクトでLLMを組み込むときには、 「ここはLLMで良さそう」 「ここは絶対に外側で検証しよう」 「ここはルールベースでガチガチに決めておくべき」 という線引きを、LLM内の処理をイメージしながら考えてみてください。 それが、AI時代、LLM時代のアーキテクト/エンジニアにとって、大事で有用なスキルになると思います。 Appendix:次トークン予測だけで、なぜ「質問」に「回答」できるのか? 文章の続きを予想する「だけ」のAIに、試しに質問をしてみます。すると ― 不思議なことに、ちゃんと答えが返ってきます。 言葉で質問して言葉で答えてくれるから、まるで会話をしているようですよね。 しかし、AIは「文章の続きを予想しているだけ」です。質問の内容を理解して、どう答えるかを考えているわけではありません。 それなのに、なぜ「質問に答えている」ことになるのでしょうか? 答えは「学習した文章の構造」にあります。 AIが学習した文章の中には、質問と答えが書かれた文章が大量に含まれています。 Q&A集のようなページ、企業のFAQページ、疑問を質問して回答してもらえる掲示板などでは、最初に質問が書いてあって、その続きに回答が書かれています。 つまり、 質問の文章の続き を 予測 すると、それは結果的に 回答を予測 していることになる、ということです。 キツネにつままれたような感じかもしれませんが、単純なルールから知能っぽさが見えてくるのは面白いですね。 こたこた博 ITエンジニアです。わりと昔からやってます。新しいこと好きです。 Discussion こたこた博 ITエンジニアです。わりと昔からやってます。新しいこと好きです。 目次 1. エンジニアから見るLLMの「気持ち悪さ」 2. 核心:LLMは「次のトークンを1個ずつ予測して選んでいるだけ」 3. 「桃太郎」で見る学習:次の単語を当て続ける 4. 内部構造:トークナイザ/埋め込み/Transformerをざっくり見る 4-1. トークン化 4-2. 埋め込み 4-3. Transformerブロック(かなり抽象化) 4-4. 次のトークン予測の処理全体 5. 拡張①:マルチモーダル ― 画像もトークンに落とせば同じ土俵 5-1. 画像 → 離散トークン 5-2. テキストと画像を同じシーケンスに流し込む 6. 拡張②:エージェント ― ツール呼び出しもテキストとして吐いているだけ 6-1. LLMから見れば、JSONもただの文字列 6-2. 実際に動かしているのは外側のループ 7. 拡張③:思考の連鎖 ― 「考え方」もテキストとして書かせる 7-1. 普通の回答とCoT回答 7-2. 二段階プロンプトにしてみる 8. 創発と限界:単純なルール×スケールの先に見えるもの 9. まとめ:LLMを「設計可能なコンポーネント」として見る Appendix:次トークン予測だけで、なぜ「質問」に「回答」できるのか?