モジュール 01 · 基礎原理

パーセプトロンから Transformer へ:
現代 LLM の根底にあるもの

この章では、最小限の高等数学で「AI が実際にどう動いているか」を根本から説明します。プロダクト、エンジニア、営業のいずれの立場でも、この章を読み終えれば、その後のモデルとフレームワークのすべての議論についていけるようになります。

1. 人工ニューラルネットワーク:生物学的ニューロンから数学的関数へ

1.1 ニューラルネットワークの最小単位

人工ニューラルネットワーク(Neural Network)は、脳のニューロンの働き方から着想を得ています。1 つのニューロンは本質的に次のような数学的関数です:

output = activation(w · x + b)

ここで x は入力ベクトル、w は重み、b はバイアス、activation は活性化関数(よく使われるのは Sigmoid、ReLU、GELU)です。このような「ニューロン」を何千も層状に積み重ね、互いに接続すれば、現代深層学習の基礎ができあがります。

1.2 ニューラルネットワークの 3 大要素

💡 重要な直感:ニューラルネットワークの訓練とは、本質的に「数十億次元の parameter space の中で、損失関数の値を最小化するような重みの組合せを探す」作業です。だからこそ LLM には膨大なデータと計算資源が必要なのです — parameter space が巨大すぎるからです。

2. Transformer:現代 LLM の礎石

2017 年、Google が論文 Attention Is All You Need で Transformer アーキテクチャを提案し、AI の発展軌道を一変させました。中心となる革新は、再帰構造を完全に捨て去り、注意機構(Attention Mechanism)だけで系列データを処理する点です。

2.1 なぜ Transformer が NLP を席巻したのか

特性RNN / LSTMTransformer
長距離依存序盤の文脈を忘れやすい任意の位置どうしが直接つながる
並列計算厳密な逐次処理が必要全位置を同時に処理
訓練速度遅い高速(GPU を最大限活用)
スケーラビリティ100 億パラ超は困難1000 億パラ超をネイティブにサポート

2.2 自己注意機構(Self-Attention)

自己注意の中心的なアイデアは、系列中のすべての単語について、他のすべての単語を「見て」、それぞれの自分への重要度を判断することです。

例として次の文を取り上げましょう —「小明(シャオミン)は昨日 北京 へ出張し、今日 は帰ってきた」:

  1. 各単語は 3 つのベクトルを生成します:Query(Q、問い合わせベクトル)、Key(K、鍵ベクトル)、Value(V、値ベクトル)。
  2. 「彼」については、その Q と他のすべての単語の K との内積を計算し、関連度スコアを得ます。
  3. スコアに Softmax 正規化をかけて注意重みを得ます(例えば「彼」の「小明」への重みは 0.8、「北京」への重みは 0.1)。
  4. これらの重みを使って、すべての単語の V を加重平均し、文脈情報を豊富に含んだ「彼」の新しい表現を得ます。

数式は次のように簡潔に書けます:

Attention(Q, K, V) = softmax(Q · K^T / √d_k) · V

ここで d_k は Key ベクトルの次元で、その平方根で割るのは内積が大きくなりすぎることで Softmax の勾配が消失するのを防ぐためです。

2.3 マルチヘッド注意(Multi-Head Attention)

マルチヘッド注意は自己注意のグレードアップ版です。モデルが異なる「表現サブ空間」で並列に複数の自己注意を行い、結果を連結します。これにより、モデルは異なる種類の関係を同時に捉えることができます:

2.4 位置エンコーディング(Positional Encoding)

Transformer はすべての単語を並列に処理するため、本質的に「順序」情報を失います。例えば「犬が人を噛む」と「人が犬を噛む」は字面のトークンはまったく同じで、順序だけが異なります。位置エンコーディングは、各位置の単語埋め込みに固有の「位置ベクトル」を加算することで、モデルに各単語が文中のどこにあるかを教えます。

初代 Transformer は固定の正弦/余弦関数で位置エンコーディングを生成していました。後のモデル(LLaMA、Qwen など)は RoPE(Rotary Position Embeddings、回転位置エンコーディング)を採用しており、より長い系列をうまく扱えます。

3. Transformer のアーキテクチャ全体

標準的な Transformer ブロックは 2 つのパートから成ります:

入力 → [LayerNorm → マルチヘッド注意 → 残差接続]
    → [LayerNorm → フィードフォワードネットワーク(FFN)→ 残差接続]
    → 出力

3.1 Transformer 全体アーキテクチャ図(一目で把握)

Decoder-only Transformer(GPT スタイル)
入力埋め込み + 位置エンコーディング
×N 層 ►
Linear (LM Head) + Softmax
P(token₁), P(token₂), ...

重要な 3 点セット:

GPT 系モデル(ChatGPT、DeepSeek、Qwen など)は、このようなブロックを数十〜数百層積み重ねた Decoder-only Transformer です。

4. 事前学習、SFT、RLHF:大モデルの「3 段階訓練法」

「話せる」状態から「聞ける」、そして「好かれる」状態になるまで、大モデルは 3 つのステップを踏みます:

4.0 3 段階訓練法のフロー図

生のコーパス(数兆トークン) 人手标注 Q&A(10万条)
① 事前学習 Pre-Training タスク: Next-Token- Prediction
データ: ウェブテキスト 14.8T tokens コスト:~557万$
[Base Model] [Chat Model] (文章を続けられるが対話不可) (質問に答えられる)
③ RLHF / DPO 人間フィードバック 強化学習
タスク: 「好かれる」になる
やり方: 人間が複数の回答に 順位を付ける
[Aligned Model] (人間の価値観に整合)

3 段階は段階的に積み上がる:「世界を理解する」→「人間の言葉を理解する」→「人間の価値観に整合する」。どの 1 つが欠けてもモデルは「使い物にならない」。

4.1 事前学習(Pre-Training)

海量のウェブテキスト(数兆トークン)でモデルを訓練し、「次の単語を予測する」ことを学ばせます。この段階は最も時間がかかり、コストも最大です — DeepSeek-V3 は 14.8 兆トークンの訓練に約 557 万米ドル(GPU レンタル費)を費やしました。

4.2 教師ありファインチューニング(Supervised Fine-Tuning, SFT)

人手标注の高品質な「指示-回答」ペアでモデルを訓練し、「人間の指示を聞いて、それに応じて答える」ことを学ばせます。この段階のデータ量は事前学習よりずっと少ない(数万〜数十万条)ですが、品質要求は極めて高いです。

4.3 人間フィードバック強化学習(RLHF)

人間がモデルの複数の回答に順位を付け、その順位データで「報酬モデル」(Reward Model)を訓練します。次に強化学習(主に PPO または DPO アルゴリズム)を用いて、モデルがより好まれる回答を生成するよう調整します。

5. なぜ GPU クラスタが必要なのか?

1000 億パラメータのモデルを訓練する場合、順伝播 + 逆伝播 1 回で必要になるのは:

1 枚の GPU のメモリ(80GB)には到底収まらないので、複数の並列戦略を組み合わせる必要があります:

並列方式中心となるアイデア代表的なフレームワーク
データ並列データを複数 GPU に分割し、各 GPU がモデル全体を実行PyTorch DDP
モデル並列モデルの異なる層を異なる GPU に配置Megatron-LM
パイプライン並列モデルをステージに分割し、各 GPU が異なるマイクロバッチを処理GPipe、PipeDream
テンソル並列単一の行列演算を複数 GPU に分割Megatron-LM

6. LLM から Agent へ:能力の跳躍

LLM が十分に強くなった後、人々はそれに「拡張機能」を付け始めました:モデルが「話す」だけでなく「行動する」こともできるように。これが AI Agent の出発点です:

この部分については次章「Agent 進化」で詳しく掘り下げます。

7. この章のまとめ

8. ハンズオン:Mini Transformer をゼロから構築する

理論を一通り説明したので、このセクションでは PyTorch で 6 層の Decoder-only Transformer をゼロから作り、小さなコーパスで訓練と推論を実演します。完全なコードは約 250 行、CPU/GPU のどちらでも動作し、Transformer の歯車の一つ一つが見えるようになることがゴールです。

8.1 目標と環境

環境セットアップ(2 行):

pip install torch numpy
python -c "import torch; print('torch', torch.__version__)"

8.2 データ準備

ここでは短い「漢詩風」ダミーデータを使います(自分の input.txt に差し替えてもよいです):

cat > data/input.txt << 'EOF'
静夜思
床前明月光、疑はくは地上の霜。
頭を挙げて山月を望み、低頭して故郷を思う。
春暁
春眠覚めず晓を覚えず、
処処啼鳥を聞く。
夜来風雨の声、
花落つること知ること多少。
登鸛雀楼
白日山に依り尽き、
黄河海に入る。
千里目窮めんと欲すれば、
更に一楼上る。
EOF
wc -c data/input.txt

文字レベル言語モデルは単にファイル全体を一つの長い文字列として読むだけで、tokenizer は不要です。

8.3 全コード:train.py

"""Mini Transformer:6 層 Decoder-only、文字レベル言語モデル。

使い方:
    python train.py
"""
from pathlib import Path



# ---------- 1. データローダ ----------
class CharDataset:
    """テキストファイルを (block_size) の長さの訓練サンプルに切り出す。"""

    def __init__(self, path: str, block_size: int = 64):
        self.text = Path(path).read_text(encoding="utf-8")
        self.chars = sorted(set(self.text))
        self.vocab_size = len(self.chars)
        self.stoi = {c: i for i, c in enumerate(self.chars)}
        self.itos = {i: c for i, c in enumerate(self.chars)}
        self.block_size = block_size
        self.data = torch.tensor([self.stoi[c] for c in self.text], dtype=torch.long)
        print(f"[data] chars={self.vocab_size} len={len(self.data)}")

    def get_batch(self, batch_size: int = 32):
        ix = torch.randint(0, len(self.data) - self.block_size - 1, (batch_size,))
        x = torch.stack([self.data[i : i + self.block_size] for i in ix])
        y = torch.stack([self.data[i + 1 : i + 1 + self.block_size] for i in ix])
        return x, y


# ---------- 2. モデルコンポーネント ----------
class PositionalEncoding(nn.Module):
    """正弦波位置エンコーディング。固定で訓練しない。"""

    def __init__(self, d_model: int, max_len: int = 512):
        super().__init__()
        pe = torch.zeros(max_len, d_model)
        position = torch.arange(0, max_len).unsqueeze(1).float()
        div_term = torch.exp(torch.arange(0, d_model, 2).float() * -(math.log(10000.0) / d_model))
        pe[:, 0::2] = torch.sin(position * div_term)
        pe[:, 1::2] = torch.cos(position * div_term)
        self.register_buffer("pe", pe.unsqueeze(0))  # (1, max_len, d_model)

    def forward(self, x):
        return x + self.pe[:, : x.size(1)]


class CausalSelfAttention(nn.Module):
    """マルチヘッド因果自己注意(Decoder で使用)。"""

    def __init__(self, d_model: int, n_heads: int, max_len: int = 512, dropout: float = 0.1):
        super().__init__()
        assert d_model % n_heads == 0
        self.n_heads = n_heads
        self.head_dim = d_model // n_heads
        self.qkv = nn.Linear(d_model, 3 * d_model)
        self.proj = nn.Linear(d_model, d_model)
        self.dropout = nn.Dropout(dropout)
        # causal mask:上三角(対角を除く)を -inf にする
        mask = torch.triu(torch.ones(max_len, max_len), diagonal=1).bool()
        self.register_buffer("mask", mask)

    def forward(self, x):
        B, T, C = x.shape
        qkv = self.qkv(x).reshape(B, T, 3, self.n_heads, self.head_dim).permute(2, 0, 3, 1, 4)
        q, k, v = qkv[0], qkv[1], qkv[2]  # (B, h, T, head_dim)
        att = (q @ k.transpose(-2, -1)) / math.sqrt(self.head_dim)
        att = att.masked_fill(self.mask[:T, :T], float("-inf"))
        att = F.softmax(att, dim=-1)
        att = self.dropout(att)
        out = (att @ v).transpose(1, 2).reshape(B, T, C)
        return self.proj(out)


class FeedForward(nn.Module):
    """Transformer FFN:GELU + Linear + Dropout。"""

    def __init__(self, d_model: int, d_ff: int, dropout: float = 0.1):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(d_model, d_ff),
            nn.GELU(),
            nn.Linear(d_ff, d_model),
            nn.Dropout(dropout),
        )

    def forward(self, x):
        return self.net(x)


class Block(nn.Module):
    """Pre-LN Decoder ブロック:Attention -> FFN、それぞれに残差接続付き。"""

    def __init__(self, d_model: int, n_heads: int, d_ff: int, max_len: int, dropout: float):
        super().__init__()
        self.ln1 = nn.LayerNorm(d_model)
        self.attn = CausalSelfAttention(d_model, n_heads, max_len, dropout)
        self.ln2 = nn.LayerNorm(d_model)
        self.ffn = FeedForward(d_model, d_ff, dropout)

    def forward(self, x):
        x = x + self.attn(self.ln1(x))
        x = x + self.ffn(self.ln2(x))
        return x


class MiniTransformer(nn.Module):
    """6 層 Decoder-only Transformer、文字レベル言語モデル。"""

    def __init__(
        self,
        vocab_size: int,
        d_model: int = 128,
        n_heads: int = 4,
        d_ff: int = 512,
        n_layers: int = 6,
        max_len: int = 256,
        dropout: float = 0.1,
    ):
        super().__init__()
        self.tok_emb = nn.Embedding(vocab_size, d_model)
        self.pos_enc = PositionalEncoding(d_model, max_len)
        self.blocks = nn.ModuleList(
            [Block(d_model, n_heads, d_ff, max_len, dropout) for _ in range(n_layers)]
        )
        self.ln_f = nn.LayerNorm(d_model)
        self.head = nn.Linear(d_model, vocab_size, bias=False)
        # 重み共有:入力埋め込みと出力 head の重みを共有(BERT/GPT でよく使われるトリック)
        self.head.weight = self.tok_emb.weight
        self.apply(self._init_weights)

    def _init_weights(self, m):
        if isinstance(m, nn.Linear):
            nn.init.normal_(m.weight, std=0.02)
            if m.bias is not None:
                nn.init.zeros_(m.bias)
        elif isinstance(m, nn.Embedding):
            nn.init.normal_(m.weight, std=0.02)

    def forward(self, idx, targets=None):
        x = self.tok_emb(idx)
        x = self.pos_enc(x)
        for blk in self.blocks:
            x = blk(x)
        x = self.ln_f(x)
        logits = self.head(x)
        loss = None
        if targets is not None:
            loss = F.cross_entropy(logits.view(-1, logits.size(-1)), targets.view(-1))
        return logits, loss

    @torch.no_grad()
    def generate(self, idx, max_new_tokens: int = 100, temperature: float = 1.0, top_k: int = 20):
        """自己回帰生成。temperature + top-k サンプリング付き。"""
        for _ in range(max_new_tokens):
            idx_cond = idx[:, -self.pos_enc.pe.size(1) :]
            logits, _ = self(idx_cond)
            logits = logits[:, -1, :] / temperature
            if top_k:
                v, _ = torch.topk(logits, min(top_k, logits.size(-1)))
                logits[logits < v[:, [-1]]] = float("-inf")
            probs = F.softmax(logits, dim=-1)
            nxt = torch.multinomial(probs, num_samples=1)
            idx = torch.cat([idx, nxt], dim=1)
        return idx


# ---------- 3. 訓練ループ ----------
def main():
    torch.manual_seed(0)
    ds = CharDataset("data/input.txt", block_size=64)
    device = "cuda" if torch.cuda.is_available() else "cpu"
    model = MiniTransformer(vocab_size=ds.vocab_size).to(device)
    print(f"[model] params={sum(p.numel() for p in model.parameters()) / 1e6:.2f}M device={device}")

    opt = torch.optim.AdamW(model.parameters(), lr=3e-4, weight_decay=0.1)
    steps = 1000
    batch_size = 32
    t0 = time.time()
    for step in range(steps):
        x, y = ds.get_batch(batch_size)
        x, y = x.to(device), y.to(device)
        _, loss = model(x, y)
        opt.zero_grad()
        loss.backward()
        torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
        opt.step()
        if step % 100 == 0 or step == steps - 1:
            dt = time.time() - t0
            print(f"step {step:4d} loss={loss.item():.4f} elapsed={dt:.1f}s")
    torch.save({"model": model.state_dict(), "stoi": ds.stoi, "itos": ds.itos}, "ckpt.pt")
    print("[done] saved ckpt.pt")


if __name__ == "__main__":
    main()

8.4 推論スクリプト:sample.py

"""訓練済み ckpt.pt からテキストを生成する。"""
from train import MiniTransformer

ckpt = torch.load("ckpt.pt", map_location="cpu")
stoi, itos = ckpt["stoi"], ckpt["itos"]
model = MiniTransformer(vocab_size=len(itos))
model.load_state_dict(ckpt["model"])
model.eval()

prompt = "静夜思\n"
idx = torch.tensor([[stoi[c] for c in prompt]], dtype=torch.long)
out = model.generate(idx, max_new_tokens=120, temperature=0.8, top_k=10)[0].tolist()
print("=== generated ===")
print("".join(itos[i] for i in out))

8.5 実行と期待される出力

mkdir -p data
# (8.2 節の input.txt を書き込む)
python train.py

期待される出力(CPU で 30-60 秒程度、MPS/GPU では数秒):

[data] chars=28 len=87
[model] params=1.21M device=cpu
step    0 loss=3.3321 elapsed=0.0s
step  100 loss=2.7102 elapsed=2.1s
step  200 loss=2.3504 elapsed=4.0s
...
step  900 loss=1.8421 elapsed=32.5s
step  999 loss=1.7903 elapsed=36.0s
[done] saved ckpt.pt

python sample.py
=== generated ===
静夜思
床前明月光、疑はくは地上の霜。
頭を挙げて山月を望み、低頭して故郷を思う。
春眠覚めず晓を覚えず、処処啼鳥を聞く。

スクリーンショット:訓練曲線(loss vs step)— tensorboard または matplotlibloss.item()loss_curve.png にプロット。生成サンプル — 端末のテキストをそのままコピー。

8.6 チューニングのヒント

8.7 次のステップ

📚 前のセクション:7. この章のまとめ · 次章:Agent 進化 - LLM から Agentic Agent へ