178 Views
August 27, 26
スライド概要
Bernardo Olisan 氏の記事 Building an LLM from scratch in C の内容を要約・再構成した技術解説スライドです。図はすべて再作図。原典: https://blog.bernardoolisan.com/posts/building-an-llm-from-scratch-in-c / コード: https://github.com/BernardoOlisan/llm」
IT Professional
C言語でLLMをゼロから作る 126万パラメータのGPTを、外部ライブラリなしで実装・学習する 原典: Bernardo Olisan, “Building an LLM from scratch in C” コード: https://github.com/BernardoOlisan/llm 2026-08-27 C言語でLLMをゼロから作る — 原典記事の技術解説 1
本資料について 原典: Bernardo Olisan, “Building an LLM from scratch in C” https://blog.bernardoolisan.com/posts/building-an-llm-from-scratch-in-c PyTorch、NumPy、autograd、GPUを使わず、MacBook Air M2(8GB)で学習まで実行 規模の目安: 標準的な小型構成であるGPT-2 small(1.24億パラメータ)の約100分の1、GPT3(1,750億)の約14万分の1 現行のGPT-5はパラメータ数非公開(公式発表はGPT-3が最後)。業界の推定値(1.7兆以上) を仮定すれば100万分の1以下のオーダーだが、これは未確認の推定に基づく参考値 主題: LLMを構成する要素を、データ表現から学習まで一貫して理解する 本スライドは、原典記事の内容を要約・再構成した技術解説です。図はいずれも原典からの転 載ではなく、本資料のために新しく作成したものです(原典にない補足図も含みます)。 C言語でLLMをゼロから作る — 原典記事の技術解説 2
本資料のゴール 1. Transformer型GPTが「次のトークンを予測する」仕組みを説明できる 2. トークナイズ、埋め込み、Attention、学習のつながりを把握する 3. 小規模モデルと大規模LLMの差が、何に起因するかを整理する C言語でLLMをゼロから作る — 原典記事の技術解説 3
第1部 なぜGPTか、何を作るか C言語でLLMをゼロから作る — 原典記事の技術解説 4
なぜ、あえてC言語で作るのか 高水準フレームワークは便利だが、内部の計算やメモリ配置を隠す C言語で実装すると、テンソル、行列積、逆伝播、最適化の実体が見える 目的は実用モデルを再現することではない 小さなGPTを最後まで動かし、LLMの構造を自分の言葉で説明できるようにすることが目的 C言語でLLMをゼロから作る — 原典記事の技術解説 5
RNNの限界からTransformerへ RNN(Recurrent Neural Network、再帰型ニューラルネットワーク)は、系列データを先頭から 1つずつ読み、それまでの内容の要約を「隠れ状態」という1本のベクトルに持ち越しながら処理す る方式です。単語1を読んで要約を更新し、単語2を読んでまた更新する、という繰り返しで文全体 を扱います。2017年まで自然言語処理の主流でした。 過去全体を1本のベクトルに詰め込むため、長い文では先頭付近の情報が薄れる ステップ5の計算にはステップ4の結果が必要で、時系列方向の並列化が難しい Transformer(2017年)は、再帰の代わりにAttentionを中心へ据えました。 各位置が他の位置との関係を直接計算できる 学習時には、系列全体を行列計算として並列処理できる GPUなど、行列積に強いハードウェアと相性がよい C言語でLLMをゼロから作る — 原典記事の技術解説 6
GPTとBERTの違い GPT = Generative Pre-trained Transformer(生成する・事前学習済みの・Transformer)。 Generative は下表の「次トークン予測」、Pre-trained は人手ラベルなしの事前学習、 Transformer は 2017 年のアーキテクチャに対応します。 観点 BERT GPT 主に使う構造 エンコーダ デコーダ 参照できる文脈 前後の両方向 過去方向のみ(因果マスク) 主な用途 理解・分類・検索 文章・コードの生成 学習の代表例 マスクした語の復元 次トークン予測 C言語でLLMをゼロから作る — 原典記事の技術解説 7
なぜGPTが主流になったのか BERT = Bidirectional Encoder Representations from Transformers(Transformerによ る双方向エンコーダ表現)。前後両方向を同時に見るため理解・分類・検索に強い一方、「未来を見 ずに次を予測する」ことができず、文章を生成できません。 GPTは過去だけを見る制約と引き換えに生成能力を獲得。汎用的な事前学習と指示調整によ り、生成系のGPTは理解系タスクにも広く適用されています(BERTは生成には使えません) どのタスクも「続きを書く」形式に統一でき、ラベルなしテキストを無限に教師データ化して スケールできることが、GPT系が主流になった理由です C言語でLLMをゼロから作る — 原典記事の技術解説 8
実装する小型GPT 項目 設定 総パラメータ数 1,258,240 語彙数 2,048 埋め込み次元 128 コンテキスト長 64トークン Transformerブロック数 4 Attentionヘッド数 8 これはGPT系と同じ基本構造を、学習・検証可能なサイズへ縮小したモデルです 大規模モデルとの差は構造の有無ではなく、主にモデル規模、学習データ、計算基盤にありま す C言語でLLMをゼロから作る — 原典記事の技術解説 9
第2部 テキストを学習データへ変える — テンソル・BPE・次トークン予測 C言語でLLMをゼロから作る — 原典記事の技術解説 10
テンソルの実体は1次元配列(1/5) テンソルとは、数値を並べた多次元の入れ物の総称です。0次元はスカラー(数1個)、1次元はベク トル、2次元は行列で、3次元以上もすべてテンソルと呼びます。本モデルでは [バッチ, トークン位 置, 埋め込み次元] の3次元が基本形です。 「3 次元の箱」は解釈、実体は 1 次元配列 実体は 解釈(⾒⽅) f0 f1 f2 f3 f4 … … … f6143 実体: RAM 上の 1 本の float 配列(6,144 個) RAM は 1 本のバイト列 ̶ 3 次元の箱は実在しない shape {2, 4, 768} の箱 3 次元はプログラム上の抽象 ⾒⽅を決めるメタデータ(整数だけ) shape = {2, 4, 768} / strides = {3072, 768, 1} ndim = 3 / size = 6144 箱の座標 → 配列の位置に変換 (i, j, k) → i×3072 + j×768 + k 番⽬ reshape = メタデータ書き換えのみ(コピーなし) 3次元の箱を実際に3次元で格納するわけではありません。要素を「決めた順番」で一列に並 べ、 shape と stride で座標を一列の添字へ変換します(以降の4枚) C言語でLLMをゼロから作る — 原典記事の技術解説 11
テンソル(2/5): 6,144個をどの順で一列に並べるか この例のテンソルは shape [2, 4, 768] (バッチ2 × トークン位置4 × 埋め込み次元768)で、 要素数は 2 × 4 × 768 = 6,144個。float(4バイト)なら連続した 24,576バイトの領域です。 並べる順番は、最後の次元 k を最も速く変化させる方式(C/C++やNumPy標準のC-order)で す。 for (i = 0; i < 2; i++) // バッチ for (j = 0; j < 4; j++) // トークン位置 for (k = 0; k < 768; k++) // 埋め込み次元 memory[...] = tensor[i][j][k]; まず「バッチ0・トークン0」の768個を全部置き、次に「バッチ0・トークン1」の768個を置 きます バッチ0の4トークンを置き終えたら、バッチ1へ進みます C言語でLLMをゼロから作る — 原典記事の技術解説 12
テンソル(3/5): 座標から1次元位置へ [i, j, k] 768, 1] )。 座標 [0, 0, 0] [0, 0, 1] [0, 1, 0] [0, 3, 767] [1, 0, 0] [1, 3, 767] の要素の位置は offset = i × 3072 + j × 768 + k で求めます(stride = [3072, 意味 1次元配列の位置 最初のバッチ・最初のトークン・最初の特徴量 0 同じベクトルの2番目の特徴量 1 次のトークンのベクトル先頭 768 バッチ0の最後のトークン・最後の特徴量 3,071 バッチ1の先頭トークン・先頭特徴量 3,072 全体の最後の要素 6,143 例: [1, 2, 10] なら 1×3072 + 2×768 + 10 = 4,618 → メモリ上の f4618 がその値です。 C言語でLLMをゼロから作る — 原典記事の技術解説 13
テンソル(4/5): strideは「次へ進む間隔」 strideは「その軸の添字を1増やすと、1次元配列上で何要素先へ進むか」です。 k を1増やす: +1 — 同じ埋め込みベクトル内の隣のfloatへ j を1増やす: +768 — 次のトークンのベクトル先頭へ i を1増やす: +3072 — 次のバッチの先頭へ(4 × 768 = 3,072) shape [2, 3, 4] (全24要素、stride [12, 4, 1] )に縮小した例: バッチ0: token 0: [ f0 f1 f2 f3 ] token 1: [ f4 f5 f6 f7 ] token 2: [ f8 f9 f10 f11 ] tensor[1][2][3] バッチ1: token 0: [f12 f13 f14 f15] token 1: [f16 f17 f18 f19] token 2: [f20 f21 f22 f23] は 1×12 + 2×4 + 3 = 23 なので f23 を参照します。 C言語でLLMをゼロから作る — 原典記事の技術解説 14
テンソル(5/5): reshapeはコピーしない 本資料で扱う連続配置のテンソルでは、要素数と並び順が維持できる場合、 reshape は配列の コピーを必要としません [2, 4, 768] を [8, 768] に変えても、データ本体は同じ f0 ... f6143 のままです 変わるのは「これを何次元の、どのような形として読むか」という shape と stride のメタ データだけです 変更前: shape [2, 4, 768] / stride [3072, 768, 1] 変更後: shape [8, 768] / stride [768, 1] ← データは1バイトも動かない この「見方の書き換えだけで済む」性質は、後述のマルチヘッドAttentionの効率実装 (reshapeでヘッドをバッチの行に変える)で決定的に効きます。 C言語でLLMをゼロから作る — 原典記事の技術解説 15
トークナイザ: BPE(1/5)— 役割と全体像 役割: テキストをモデルが扱える番号(トークンID)の列へ変換します。モデルは乗算と加算しかで きず文字を読めないため、文字列と番号の対応表(語彙 = 辞書)を作って適用するのがトークナイ ザです。出力(ID列)を文字へ戻す復元も担います。 BPE(Byte Pair Encoding)の本質: よく一緒に出る文字の組を辞書に登録し、2文字以上のまと まりを1つのトークンとして扱えるようにする方法です。 ① 最初は1文字(正確には1バイト)ずつに分割する ② 学習データ全体で、最も頻出する隣接ペアを探す ③ そのペアを新しいトークンとして辞書に追加する ④ 決めた回数だけ②〜③を繰り返す ⑤ 利用時は、完成した結合ルールを順に適用する 1文字1番号は未知語に強いが列が長い。1単語1番号は列が短いが語彙が爆発し未知語に弱 い。BPEはその中間です C言語でLLMをゼロから作る — 原典記事の技術解説 16
BPE(2/5): 例 — "banana bandana" で辞書を作る(学習時) 下図は①〜④の辞書を作る処理です。文字列自体は変えず、どこで区切るかだけを変えています。 BPE: merge(最頻ペアの置換)を上限 4 回で学習 ̶ この例は 3 回で打ち⽌め step 0: b|a|n|a|n|a| |b|a|n|d|a|n|a 最頻ペア an → 新 ID 256(4 回) step 1: b|an|an|a| |b|an|d|an|a b + an → 257 "ban"(2 回) step 2: ban|an|a| |ban|d|an|a an + a → 258 "ana"(2 回) step 3: ban|ana| |ban|d|ana この⼩さな例ではここで終了(実際は語彙サイズ到達まで) 256 "an" b 文字単位: BPE後: a 257 "ban" n 258 "ana" 新トークンは既存トークンを部品として定義 (ban = b + an)。部品を辿ると必ずバイトに着く この例では 14 ⽂字 → 6 トークン b | a | n | a | n | a | ␠ | b | a | n | d | a | n | a ban | ana | ␠ | ban | d | ana (14トークン) ( 6トークン) 注: 14文字 → 6トークンはこの例での結果で、圧縮率は入力により異なります。 C言語でLLMをゼロから作る — 原典記事の技術解説 17
BPE(3/5): 256・257・258 は辞書の見出し番号 0〜255 1) : まず全バイトに与える基本ID( a 、 b 、空白など。バイトの取りうる全256値と1対 以降: 学習で追加した結合トークン。文字コードではなく「この並びをひとまとまりとし て扱う」という辞書の見出し番号です 256 256 = (a, n) 257 = (b, 256) 258 = (256, a) → "an" → "ban" → "ana" 新しいトークンは文字列のコピーではなく、「どの2トークンをつないだか」という関係で定義 されます C言語でLLMをゼロから作る — 原典記事の技術解説 18
BPE(4/5): 保存するのは「ペアの列」だけ ペアの列とは、どの2つのIDを結合したかの記録を学習順に並べたリストです。トークンが関係で定 義されるため、これだけあれば辞書全体を再現できます。 merges.bin の中身の実体: (97, 110), (98, 256), (256, 97), ... n番目のペアの結果は 256+n−1 と決まるため、新IDの番号すら保存不要 本実装は 1,792 merge × 8バイト(int32 × 2)= 14,336バイト ≈ 14KB 辞書全体は起動時にこの列を先頭から再生すれば復元できます(0〜255のバイト表を用意 → 256 = 97+110 = "an" → 257 = "b"+256 = "ban" → …) C言語でLLMをゼロから作る — 原典記事の技術解説 19
BPE(5/5): 辞書を「作る処理」と「使う処理」は別物 処理 何をするか 実行頻度 BPEの学習 大量の文章から頻出ペアを探し、結合ルールを作る トークナイザ作成時に一度 エンコード 作成済みルールを順に適用し、入力文をID列にする モデル利用のたび デコード IDを対応するバイト列に戻し、文章を復元する モデル出力を表示するとき 利用時に「最頻ペアを数え直す」ことはありません。学習済みルール( an→256 、 ban→257 、 ana→258 )を早く学習された順に適用するだけです 順序を守るのは、後のルール( ban )が前のルールの結果( an )の上に定義されているため です C言語でLLMをゼロから作る — 原典記事の技術解説 20
学習データ: コーパス tinyshakespeare コーパスとは、学習に使う文章データの集まりのことです。本モデルのコーパスは tinyshakespeare の1つだけ — シェイクスピアの戯曲の台本を1つのテキストにつなげた約 1.1MB(1,115,394バイト)のファイルで、2015年にKarpathyが公開して以来、小さな言語モデ ルの実験用データの定番です。中身は「First Citizen:(改行)せりふ…」という台本形式です。 このコーパスからBPEで語彙2,048を学習し、同じコーパスをその語彙でトークン化すると 359,266トークンになります(1,115,394 ÷ 359,266 ≈ 3.10バイト/トークン = 3.10倍圧 縮) モデルの学習データはこの35.9万トークンがすべてです。学習中に他の文章に一切触れないた め、生成される文章はシェイクスピア風の古い英語になります C言語でLLMをゼロから作る — 原典記事の技術解説 21
語彙の正体は「IDと断片の対応表」 語彙とは、モデルが使える文字列断片の全カタログ、実装上はIDとバイト列を対応させる2,048行 の表です。 ID 0〜255 : 全バイト値と1対1の基本断片("a"、"b"、空白、改行 など) ID 256〜2047 : BPE学習で追加した1,792個の結合断片 例: "an"、"ban"、" the "、"thou"、"Citizen:" + 改行 この表は閉じていて固定です。どんな入力もこの2,048個の組み合わせでしか表現できません 256番以降にどの断片が載るかは、コーパス内での出現頻度だけで決まります。シェイクスピ アだから thou や台本書式が席を得たのであり、Pythonコードで学習すれば def や self. が載った全く別の表になります つまり語彙表には学習データの癖がそのまま写ります。語彙は言語の普遍的な辞書ではなく、 「そのコーパスに何が多く出てきたか」の記録です C言語でLLMをゼロから作る — 原典記事の技術解説 22
語彙の行数は「決める」もの 行数はトークナイザが違うから自動的に増えるのではありません。「mergeを何回で止めるか」を 人間が先に決め、その設定値がそのまま行数になります。アルゴリズム(BPE)は本モデルと同じ です。 本モデル 決めた語彙サ 2,048行(基本バイト256 + merge イズ 1,792回) 辞書作りの材 シェイクスピア 1.1MB 料 GPT-2/3 50,257行(基本バイト256 + merge 50,000回 + 特 殊トークン1) 大規模なウェブテキスト 行を増やすほど頻出語が丸ごと1トークンになり、同じ文章を少ないトークン数で表せます (計算も軽くなる) 一方で埋め込み表のパラメータが太り、めったに出ない行は学習不足になります だからコーパスの規模・多様性に見合う行数を選びます。本実装は1.1MBのシェイクスピアに 対して2,048を採用しています 23 C言語でLLMをゼロから作る — 原典記事の技術解説
学習データ: 次のトークンを正解にする ここまでで、テキストは359,266個のトークンID列になりました。ここからの問いは「この列で何 を学ばせるか」— GPTの答えは「次のトークンを当てさせる」です。入力と正解は、同じ系列を1 トークン分ずらした64トークンの列です。位置ごとに見れば、入力の各トークンの次に現れるトー クンが正解になります。 元の列: [ 73 32 72 65 68 ] → 入力: [ 73 32 72 65 ] / 正解: [ 32 72 65 68 ] 位置 モデルが見られる情報 当てるべき正解 32 0 73 72 1 73, 32 65 2 73, 32, 72 68 3 73, 32, 72, 65 「73の次は32」「73, 32の続きは72」…という予測を、1回の順伝播で全位置について同時に学習 します。 24 C言語でLLMをゼロから作る — 原典記事の技術解説
なぜ「次を当てる」だけで言語能力が生まれるのか LLMの知識はすべて、この1つの課題の反復から来ています。予測の精度を上げるには、言語の構造 を捉える以外に方法がないためです。誰も文法を教えていないのに、当て続けるうちに知らざるを 得なくなります。 当てる場面 高精度で当てるために必要になる能力 thou ar の次 綴りと古語の活用( t が来て thou art ) First Citizen の次 台本の書式(コロン + 改行が来る) せりふの続き 文脈の追跡(誰が何の話をしているか) 「フランスの首都は」の次 事実知識(大規模モデルの場合) 本モデルの生成が「綴りと書式は正しいが意味が続かない」水準で止まるのは、126万パラメ ータで捉えられたのが表の上2段まで、ということです 注: これは事前学習の話です。現代のLLMの最終的な振る舞いには、指示チューニングや選好最 適化などの追加学習も影響します C言語でLLMをゼロから作る — 原典記事の技術解説 25
学習した課題が、そのまま本番の仕事になる 学習と生成が同じ動作です。学習では「次のトークンを当てる」練習をし、生成では「次を予測 → 末尾に貼る → また予測」を繰り返します(自己回帰)。練習した課題と本番の仕事が完全に一致し ているため、学習の改善がそのまま文章力の改善になります。 学習時: 73, 32, 72, 65 → 「次は 68」と予測し、外れたら重みを修正 生成時: 73, 32, 72, 65 → 「次は 68」と予測し、68 を末尾に貼って続行 採点も同じ枠組みでできます。「正解トークンにどれだけの確率を与えたか」で測ればよく、こ れが後述の損失関数(交差エントロピー)です ChatGPTが文章を書けるのも同じ理屈で、規模が違うだけです C言語でLLMをゼロから作る — 原典記事の技術解説 26
自己教師あり学習: 正解はテキストの中にある 通常の分類学習では、人が正解ラベルを用意します。 「この映画は面白い」 → 感情: 肯定 画像データ → 種類: 猫 一方、GPTは文章の続きを正解として使います。 元の文章: 「私は朝、コーヒーを飲んだ」 入力: 「私は朝、コーヒーを」 正解: 「飲んだ」 人が「正解」を別途付けなくても、元のテキストそのものから学習データを作れます。このよう に、データの一部を正解として利用する学習方法を自己教師あり学習と呼びます。 C言語でLLMをゼロから作る — 原典記事の技術解説 27
バッチ: 複数の例をまとめて学習する 64トークンの入力を1本ずつ処理するのではなく、複数本をまとめてモデルへ渡します。 batch 0: 入力 [ 64トークン ] → 正解 [ その1つ先の64トークン ] batch 1: 入力 [ 64トークン ] → 正解 [ その1つ先の64トークン ] batch 2: 入力 [ 64トークン ] → 正解 [ その1つ先の64トークン ] モデル内部では、次の形のテンソルとして扱います。 [バッチ数, トークン数, 埋め込み次元] 例: [2, 64, 128](本モデルの学習時。バッチサイズは一例で、モデル規模やGPUメモリにより変わる) : 同時に処理する文章の断片の数(バッチサイズ) 64 : 各断片に含めるトークン数 128 : 各トークンを表すベクトルの次元数 2 C言語でLLMをゼロから作る — 原典記事の技術解説 28
バッチでまとめて処理する利点 大きな行列計算にまとめられるため、GPUや並列計算資源を効率よく使えます 複数の例の予測誤差を平均してから重みを更新するため、1例だけの偶然や偏りに影響されにく くなります つまり、計算効率と学習の安定性を両立できます C言語でLLMをゼロから作る — 原典記事の技術解説 29
第3部 GPT内部で何が起きるか — 埋め込み・Attention・ブロック C言語でLLMをゼロから作る — 原典記事の技術解説 30
埋め込み(1/4): IDは単なる番号 トークンIDは、単語や文字列を区別するための番号です。 「猫」 → ID 270 「犬」 → ID 841 「走る」 → ID 152 この番号そのものに意味はありません。 270 + 1 = 271 と計算できても、「猫」に何かを足すと別 の意味になる、ということではありません。 モデルが扱えるのは数値計算だけなので、IDをそのまま計算に使うことはできません。 ※「猫」「犬」などは説明上の例です(本実装の語彙は英語のシェイクスピアから学習)。 C言語でLLMをゼロから作る — 原典記事の技術解説 31
埋め込み(2/4): 埋め込み表からベクトルを取り出す そこで、各トークンに128個の数値を対応付けた表を用意します。 埋め込み表: 2,048行 × 128列 ID 0 ID 270 ID 841 → [ 0.12, -0.31, 0.07, ..., 0.44 ] → [ 0.08, 0.15, -0.22, ..., -0.10 ] → [-0.05, 0.18, -0.19, ..., 0.03 ] ← 「猫」 ← 「犬」 トークンIDを使って表の該当行を取り出す処理を、埋め込み(embedding)と呼びます。 入力ID: 270 → 埋め込み表の270行目を取得 → 128個の数値からなるベクトル この128個の数値が、モデル内部で「猫」を表す情報になります。 C言語でLLMをゼロから作る — 原典記事の技術解説 32
埋め込み(3/4): 意味は学習によって作られる 学習を始めた直後の埋め込み表は、ランダムな数値です。「猫」と「犬」のベクトルが近い、といっ た意味はまだありません。 次のトークン予測を何度も繰り返すうちに、予測を改善する方向へ数値が更新されます。 似た文脈で使われるトークンは、似たベクトルになりやすい 異なる役割で使われるトークンは、異なるベクトルになりやすい 128個の数値を、人間が1つずつ「動物」「単数」などと解釈する必要はない 意味は、あらかじめ人が埋め込むものではなく、予測誤差を小さくする学習の結果として現れ ます。 C言語でLLMをゼロから作る — 原典記事の技術解説 33
埋め込み(4/4): 位置埋め込みで語順を伝える 同じ単語でも、文中の位置が違えば役割が変わります。 「犬が 猫を 追う」 「猫が 犬を 追う」 トークンの種類だけでは、どちらが追う側かを十分に区別できません。そこで、トークンの埋め込 みに「何番目にあるか」を表す位置埋め込みを加えます。 最終的な入力ベクトル = トークンの埋め込み + 位置の埋め込み Attentionだけでは位置を区別できないため、位置情報を明示的に与える必要があります 本モデルの位置埋め込みは64行 × 128列で、65行目は存在しません。モデルが64トークンま でしか扱えない(コンテキスト長64)のは、この表が尽きるという物理的な理由です C言語でLLMをゼロから作る — 原典記事の技術解説 34
Attention: 過去のどのトークンを参照するか決める 各トークンは、文脈に応じて過去のトークンから必要な情報を集め、自分の表現を更新します。 入力トークン列 ↓ 各トークンから Q・K・V を作る ↓ Q と K を比較し、参照の強さを決める ↓ 参照の強さで V を重み付き平均する ↓ 文脈を反映した新しいトークン表現 例: 「腹が減っていたので、屋根にいた猫に餌をあげた」 「あげた」は、直前の「屋根」よりも「猫」を強く参照する必要があります C言語でLLMをゼロから作る — 原典記事の技術解説 35
Q・K・V: 探す・照合する・受け取る 同じ入力ベクトル x から、役割の異なる3種類のベクトルを作ります。 記号 名前 役割 直感的な説明 Q Query 探す条件 「今の私は、どんな情報が必要か」 K Key 照合する特徴 「私はどんな情報を持っているか」 V Value 渡す情報 「参照されたら、何を渡すか」 入力 x ├─ × Wq → Q: 探す条件 ├─ × Wk → K: 照合用の特徴 └─ × Wv → V: 渡す情報 Wq 、 Wk 、 Wv は、学習によって値が更新される重み行列です。 C言語でLLMをゼロから作る — 原典記事の技術解説 36
Attentionの計算: 比較 → 割合化 → 情報を集める 1. QとKを比較する — 各トークンのQと、参照候補となる過去トークンのKを比較します。スコア が大きい = 今のトークンにとって、その過去トークンが重要です。 2. 未来のトークンを隠す — GPTは次のトークンを予測するモデルです。正解を先読みしないよ う、未来位置のスコアを −∞ にして使えなくします(因果マスク)。 3. スコアを「参照割合」に変える — Softmaxで各スコアを0〜1の重みに変換します(各行の重み の合計 = 1)。 4. Valueを重み付きで足し合わせる — 重要なトークンのVを大きく、関係の薄いトークンのVを小 さく混ぜます。 出力 z = Attentionの重み × V C言語でLLMをゼロから作る — 原典記事の技術解説 37
Attention全体の流れ(図解) 注意機構: 照合は Q·K、運ぶのは V Q = x·W_q x [T, 128] K = x·W_k V = x·W_v スコア = Q·Kᵀ / √d_k [T, T] 全位置 × 全位置 因果マスク(上三⾓ ∞) → softmax(⾏の和 = 1) z = 重み × V(⾏列積 1 回) 各トークンが⽂脈で⾃分を書き換える Q = 探す条件 / K = 照合する特徴 / V = 渡す情報 W_q・W_k を分けることでスコアが⾮対称になり、関係に向きが⽣まれる 「QとKで各トークンの重要度を計算し、未来を隠したうえで割合に直す。その割合でVを混ぜ合わ せたものが、文脈を反映した新しい表現」— この1文が全体像です。 数式で書くと: Attention(Q, K, V) = softmax( QKᵀ / √d_k + 因果マスク ) × V C言語でLLMをゼロから作る — 原典記事の技術解説 38
Softmax: 数値の集まりを「合計1の割合」に変える softmax(xᵢ) = e^xᵢ ÷ Σ e^xₖ — 各数値の e^x を計算し、全体の合計で割るだけの関数です。 入力: [ 1, 2, 3 ] 出力: [ 0.09, 0.24, 0.67 ] ← 正負・大小がバラバラの生スコア ← すべて0〜1、合計がちょうど1 割合として使える: 「70%は猫を参照、30%は屋根を参照」のような配分に変わります 大きい入力ほど強調される: e^x のため、1位と2位の差がはっきりします 負の数が混じっていても出力は必ず正になります 本モデルでの登場箇所は2つです。①Attentionのステップ3: QとKの比較スコアを参照割合に変え る。②出力層: 2,048個のlogitsを「次のトークンの確率分布」に変える。 名前は「滑らかな(soft)最大値選び(max)」の意味。最大の要素だけを1にするのではなく、2 位以下にも割合を残します。数値あふれ対策(最大値引き)は次のスライドで扱います。 C言語でLLMをゼロから作る — 原典記事の技術解説 39
因果マスクとSoftmax 因果マスク: 未来の位置に ∞ を置く 1.00 −∞ −∞ −∞ .50 .50 −∞ −∞ .33 .33 .33 −∞ .25 .25 .25 .25 e^(∞) = 0 なので softmax が ⾒える位置だけで正規化される 位置 3 が位置 4 を読めたら 「答えを⾒ながら当てる」= リーク この三⾓形が GPT が書けて BERT が書けない理由 ⾏ = 質問する側の位置 / 列 = 参照される位置(softmax 後の重み、未学習は⼀様) GPTでは、未来の正解を参照しないよう、未来位置のスコアをマスクします これが生成時の「過去だけから次を予測する」条件を守ります Softmaxの前に最大値を引くことで、指数関数のオーバーフローを避けます スコアを √d_k で割り、値が極端になって学習が不安定になるのを抑えます C言語でLLMをゼロから作る — 原典記事の技術解説 40
マルチヘッドAttention 1つのAttentionだけでは、すべての関係を1種類の重み付けに押し込むことになります 異なる観点の関係を学習できるよう、注意機構を8本並列に持ちます(近接関係、構文、話 者、反復など) 実装では、8回の小さな行列積を個別に実行するのではなく、形状を変換して大きな行列計算と してまとめます [B,T,128] → reshape → [B,T,8,16] → permute → [B,8,T,16] → reshape → [B×8,T,16] C言語でLLMをゼロから作る — 原典記事の技術解説 41
深いネットワークを学習できるようにする3つの仕組み Transformerのブロックを何層も重ねると、途中で情報が薄れたり、学習のための修正信号が先頭 の層まで届きにくくなったりします。 その問題を抑えるために、残差接続・LayerNorm・FFNを組み合わせます。 C言語でLLMをゼロから作る — 原典記事の技術解説 42
1. 残差接続: 入力をそのまま足し戻す 残差接続: ⼊⼒をそのまま⾜し戻す迂回路 x ショートカット(そのまま素通り) 層 f(x)(LN → MHA / FFN) + H(x) 出⼒ = 層の処理結果 + ⼊⼒ ̶ 元の情報を失わずに次の層へ渡す 学習の修正信号(勾配)も、この近道を通って前の層へ戻りやすくなる 層の処理結果に加えて、元の入力も次の層へ渡します 層がまだ十分に学習できていなくても、元の情報を失いにくくなります 学習時の修正信号も、この近道を通って前の層へ戻りやすくなります 残差接続は、「新しい情報を追加しつつ、元の情報を保つ」ための仕組みです。 C言語でLLMをゼロから作る — 原典記事の技術解説 43
2. LayerNorm: 値のばらつきを整える 各トークンが持つ特徴量の値を、極端に大きく・小さくなりすぎないよう整えます 層を重ねても計算値が不安定になりにくく、学習を続けやすくなります LayerNorm: 各トークンのベクトルの値のスケールを整える 実装上は平均0・分散1への正規化と、学習可能な再調整(scale・shift)です 本モデルでは9箇所にあります(各ブロックにAttention前とFFN前の2つ × 4ブロック + 出力 直前に1つ) C言語でLLMをゼロから作る — 原典記事の技術解説 44
3. FFN: 各トークンを非線形に変換する 128次元 → 512次元 → GELU → 128次元 Attentionで集めた文脈情報を、トークンごとに変換します GELUという非線形処理があるため、単純な線形変換だけでは表せない複雑な関係を学べます Attentionは「どの情報を集めるか」、FFNは「集めた情報をどう加工するか」を担当します。 C言語でLLMをゼロから作る — 原典記事の技術解説 45
モデル全体の処理 モデル全体(1,258,240 パラメータ)̶ 各段のテンソル形状 テキスト "First Citizen:" BPE トークナイザ 語彙 2,048 埋め込み + 位置 [B, 64, 128] 270K params Transformer ブロック × 4(各 181,376 params) LN → 8 頭注意 + LN → FFN 128→512→128 + 最終 LayerNorm 残差(GELU で⾮線形性、 FFN が全体の 42%) ⾃⼰回帰(出⼒が⼊⼒になる) out_head [128, 2048] logits → 次トークンの確率 最⼤を選び 末尾に貼って再実⾏ 1. テキストをBPEでトークンIDへ変換する 2. トークン埋め込みと位置埋め込みを加える 3. Transformerブロックを4回通す 4. 語彙数2,048次元のlogitsを出し、次トークンの確率分布を作る 5. 正解トークンとの誤差から、すべての重みを更新する C言語でLLMをゼロから作る — 原典記事の技術解説 46
第4部 学習・検証・小型モデルの限界 C言語でLLMをゼロから作る — 原典記事の技術解説 47
パラメータの内訳 部位 パラメータ数 役割 token embedding [2048×128] 262,144 トークンをベクトル化 position embedding [64×128] 8,192 順序を表現 Transformer block × 4 725,504 文脈化と非線形変換 final LayerNorm 256 出力直前の正規化 output head [128×2048] 262,144 次トークンのスコアを出力 合計 1,258,240 float32で保存すると、重みだけで約5.0MBです 学習時には重みのほか、勾配やオプティマイザの状態も保持するため、必要メモリは増えます C言語でLLMをゼロから作る — 原典記事の技術解説 48
なぜラップトップでは大規模GPTを学習できないのか 観点 本モデル GPT-3級の例 パラメータ数 約126万 1,750億 重みだけの容量(float32) 約5MB 約700GB 学習時に必要な状態 重み・勾配・最適化状態 同じ種類だが桁違い 計算時間 実測355トークン/秒 大規模分散計算が前提 原典の測定では、1ステップ0.360秒、約355トークン/秒でした GPT-3で公表された3,000億トークンを同じ速度で1回処理するだけでも、約27年かかります (この実装・実測速度での単純換算。実際の大規模学習は並列化されます) さらにパラメータ数は約13.9万倍であり、単純比較でも個人用ラップトップの範囲を大幅に超 えます C言語でLLMをゼロから作る — 原典記事の技術解説 49
検証: 理論値と実測を照合する 実装の正しさは数値で検証できる 7.6226 理論値 ln(2048) = 7.6246 3.9490 理論値と実測が 3 桁⼀致 = 主要な計算経路が整合する有⼒な確 交差エントロピー L = (1/N) Σ log p(正解) 未学習(乱数の重み) パープレキシティ 2043.8 / 2048 学習済み パープレキシティ 51.9(2,048 択 → 52 択) ランダム予測時のクロスエントロピー損失は、語彙数を V とすると ln(V) が基準になります 本モデルでは ln(2048) = 7.6246 です 実装結果がこの基準に近いかを確かめることで、初期化・Softmax・損失計算の不具合を検出 できます 小規模な実装ほど、理論上の期待値と突き合わせて検証する姿勢が重要です。 C言語でLLMをゼロから作る — 原典記事の技術解説 50
生成結果が示すこと $ ./llm run merges.bin "First Citizen:" First Citizen: wrong you wrong, you do you w I HAD always thought husbELELELELELELELilt 学習済みモデルは、単語の綴りや台本らしい形式、局所的な構文をある程度再現します 未学習モデルは、確率分布が意味を持たず、文字列が崩れます ただし64トークンの文脈長・4層・126万パラメータでは、長い文章の一貫性や意味理解は限 定的です C言語でLLMをゼロから作る — 原典記事の技術解説 51
まとめ GPTは、トークン列から次のトークンを予測するTransformer型モデルです BPE、埋め込み、因果マスク付きAttention、FFN、残差接続、LayerNormが基本部品です 小型モデルをC言語で実装すると、LLMを「ライブラリの呼び出し」ではなく、データと数値 計算の連鎖として理解できます 大規模LLMの能力は、基本構造に加え、モデル規模、データ品質・量、学習手法、分散基盤の 総合結果です この小型モデルで分かること: トークン化 → 埋め込み → Attention → 予測という計算の連鎖 の全体像 大規模LLMで追加されるもの: 規模・データ・指示チューニング・分散学習基盤 C言語でLLMをゼロから作る — 原典記事の技術解説 52
参考資料 Bernardo Olisan, “Building an LLM from scratch in C” https://blog.bernardoolisan.com/posts/building-an-llm-from-scratch-in-c 論文: Vaswani et al., “Attention Is All You Need” (2017) 論文: Radford et al., “Improving Language Understanding by Generative Pre-Training” (2018) コード: https://github.com/BernardoOlisan/llm C言語でLLMをゼロから作る — 原典記事の技術解説 53