「次に来る単語を予測し続けるだけで、言語を理解できるのか?」— この問いに対する答えは、驚くべきことに「はい」です。しかも単に理解するだけでなく、詩を書き、コードを生成し、数学の問題を解き、ときには人間の専門家を凌駕する推論まで行えるようになります。
因果言語モデル(Causal Language Model, CLM)は、テキストを左から右へ一方向に読み、「これまでの文脈から次のトークンを予測する」というシンプルな原理に基づいた言語モデルです。GPT、GPT-2、GPT-3、GPT-4、そしてLLaMAやClaude、Geminiといった現代の大規模言語モデル(LLM)は、すべてこのCLMを事前学習の中核に据えています。
「左から右へ次の単語を当てる」— たったこれだけのタスクがなぜ汎用的な知能を生み出すのか。この仕組みを数式レベルで理解することは、以下のような場面で大きな力になります。
- テキスト生成: チャットボット、コード補完、クリエイティブライティングなど、あらゆる生成タスクの基盤を理解できます
- 大規模言語モデルの設計思想: GPTシリーズやLLaMAの論文を読むとき、「なぜこのアーキテクチャなのか」「なぜこの目的関数なのか」を正確に把握できます
- 対話システムの構築: CLMの学習原理を理解することで、Instruction Tuning やRLHFといった後段のチューニング手法が「何を修正しているのか」が明確になります
- スケーリング則の解釈: モデルサイズ・データ量・計算量とPerplexityの関係を議論するための理論的基盤が得られます
本記事の内容
- 言語モデルの確率的基礎と連鎖律
- CLMの目的関数とCausal Maskの数理
- CLMとMLM(マスク言語モデル)の構造的比較
- CLMの学習ダイナミクス — 何を、どんな順番で学習するのか
- Causal Maskの実装と数理
- PyTorchでの簡易CLMモデルのスクラッチ実装
- テキスト生成とPerplexityの評価
- スケーリング則とCLMの関係
前提知識
この記事を読む前に、以下の記事を読んでおくと理解が深まります。
言語モデルの基礎 — 確率的な文の生成
文に確率を割り当てる
CLMの数式に入る前に、そもそも「言語モデル」とは何かを確認しましょう。日常生活で私たちは、ある文が自然かどうかを瞬時に判断しています。「今日は天気が良い」は自然な文ですが、「今日は天気が椅子」は不自然です。言語モデルとは、この「自然さ」を確率として定量化する仕組みです。
形式的には、トークン列 $\bm{x} = (x_1, x_2, \ldots, x_T)$ に対して同時確率 $P(x_1, x_2, \ldots, x_T)$ を割り当てるモデルを言語モデルと呼びます。自然な文は高い確率を持ち、不自然な文は低い確率を持ちます。
しかし、$T$ 個のトークンの同時確率を直接モデル化するのは現実的ではありません。語彙サイズが $V$ のとき、可能なトークン列は $V^T$ 通りもあり、その全てに確率を割り当てることは不可能です。たとえば語彙サイズが50,000で系列長が100のとき、$50000^{100}$ は天文学的な数です。
連鎖律による分解
ここで威力を発揮するのが、確率論の基本法則である連鎖律(chain rule)です。同時確率を条件付き確率の積として分解できます。
$$ P(x_1, x_2, \ldots, x_T) = P(x_1) \cdot P(x_2 \mid x_1) \cdot P(x_3 \mid x_1, x_2) \cdots P(x_T \mid x_1, \ldots, x_{T-1}) $$
これをコンパクトに書くと次のようになります。
$$
P(x_1, x_2, \ldots, x_T) = \prod_{t=1}^{T} P(x_t \mid x_{ ここで $x_{ この分解のポイントは、数学的に厳密であり、何の近似も含んでいないということです。連鎖律は確率の公理から導かれる恒等式なので、任意のトークン列に対して正確に成り立ちます。つまり、各ステップの条件付き確率 $P(x_t \mid x_{ 連鎖律自体は、分解の順序を限定しません。数学的には「右から左」に分解することも、ランダムな順序で分解することも可能です。しかし、CLMがあえて「左から右」を選ぶ理由は、自然言語の本質的な性質にあります。 人間は文を書くとき、最初の単語から順に言葉を紡いでいきます。小説家は第一文から最終文へと筆を進め、プログラマーはコードを上から下へ書きます。この「過去が未来に影響を与えるが、未来は過去に影響を与えない」という性質は、物理学で言う因果律(causality)と同じ構造です。CLMの “Causal” はまさにこの因果律に由来しています。 時刻 $t$ のトークン $x_t$ を予測するとき、未来のトークン $x_{t+1}, x_{t+2}, \ldots$ を一切参照しない。この制約が、CLMに生成能力を与えます。学習時に「未来を見ずに次を予測する」訓練を積んだモデルは、推論時にもまったく同じ方法でトークンを一つずつ生成できるからです。 CLMの連鎖律に基づく定式化は、深層学習以前のn-gramモデルと共通しています。n-gramモデルも条件付き確率 $P(x_t \mid x_{ $$
P(x_t \mid x_{ たとえば3-gramモデルでは、「彼は昨日大学の図書館で勉強した」の「勉強」を予測するとき、「図書館 で」という直前の2トークンのみを条件にします。「彼は昨日大学の」という長い文脈は切り捨てられます。 この近似は計算を大幅に簡単にしますが、同時に長距離の依存関係を捉えられないという致命的な限界があります。Transformer を使ったCLMでは、Self-Attention機構により系列全体の文脈を(因果マスクの制約内で)直接参照できるため、この限界を克服しています。具体的には、位置 $t$ のトークンは $x_1$ から $x_{t-1}$ まで全てのトークンに直接Attentionを向けることができます。これにより、数千トークン離れた文脈も考慮した予測が可能になりました。 言語モデルの基本的な枠組みが確認できたところで、次にCLMの目的関数を数学的に定式化し、ニューラルネットワークがこの条件付き確率をどのように学習するかを見ていきましょう。 CLMの学習は、「与えられたテキストに対して、モデルが割り当てる確率をできるだけ高くする」ことに帰着します。これは最尤推定(Maximum Likelihood Estimation)そのものです。 訓練データとして長いテキスト $\bm{x} = (x_1, x_2, \ldots, x_T)$ が与えられたとき、パラメータ $\theta$ を持つモデルの対数尤度は次のようになります。 $$
\log P(\bm{x}; \theta) = \sum_{t=1}^{T} \log P(x_t \mid x_1, \ldots, x_{t-1}; \theta)
$$ これは連鎖律による分解を対数空間に移したものです。積の対数は対数の和になるため、同時確率の対数は各ステップの条件付き対数確率の和に分解されます。 学習では対数尤度を最大化したいのですが、最適化の慣例として最小化問題に変換します。符号を反転させて損失関数(目的関数)を定義しましょう。 $$
\mathcal{L}_{\text{CLM}}(\theta) = -\sum_{t=1}^{T} \log P(x_t \mid x_1, \ldots, x_{t-1}; \theta)
$$ この負の対数尤度(Negative Log-Likelihood, NLL)がCLMの損失関数です。各トークン位置での交差エントロピー損失の和と等価であり、PyTorchの 直感的に解釈しましょう。$\log P(x_t \mid x_{ では、$P(x_t \mid x_{ ステップ1: トークン埋め込み 入力トークン列をまず埋め込みベクトルに変換します。各トークン $x_t$ に対応する埋め込みベクトル $\bm{e}_t \in \mathbb{R}^{d}$ を取得し、位置エンコーディング $\bm{p}_t$ を加算します。 $$
\bm{h}_t^{(0)} = \bm{e}_{x_t} + \bm{p}_t
$$ ステップ2: Transformer Decoderによる文脈化 $L$ 層のTransformer Decoderブロックを通過させ、各位置に文脈を反映した隠れ状態を得ます。 $$
\bm{h}_t^{(l)} = \text{DecoderBlock}^{(l)}(\bm{h}_t^{(l-1)}), \quad l = 1, 2, \ldots, L
$$ 各Decoderブロックは、Causal Mask付きのSelf-Attention、Layer Normalization、Feed-Forward Networkから構成されます。重要なのは、Causal Maskによって位置 $t$ の出力 $\bm{h}_t^{(l)}$ は $\bm{h}_1^{(l-1)}, \ldots, \bm{h}_t^{(l-1)}$ のみに依存するという点です。 ステップ3: 語彙上のsoftmax 最終層の出力 $\bm{h}_t^{(L)} \in \mathbb{R}^{d}$ に線形変換(LM head)を適用してlogitベクトル $\bm{z}_t \in \mathbb{R}^{V}$ を得て、softmaxで確率分布に変換します。 $$
\bm{z}_t = \bm{W} \bm{h}_t^{(L)} + \bm{b}
$$ $$
P(x_t = v \mid x_{ ここで $\bm{W} \in \mathbb{R}^{V \times d}$ が語彙射影行列、$\bm{b} \in \mathbb{R}^{V}$ がバイアスです。出力は語彙サイズ $V$ 次元の確率分布であり、各語彙トークンが次に来る確率を表します。 CLMの数式において、条件 $x_{ $T$ トークンの系列に対するAttentionスコア行列 $\bm{S} = \bm{Q}\bm{K}^\top / \sqrt{d_k}$ に対して、Causal Mask $\bm{M}$ を加算します。 $$
\bm{M}_{ij} = \begin{cases} 0 & \text{if } i \geq j \\ -\infty & \text{if } i < j \end{cases}
$$ つまり、上三角部分($i < j$ の要素)を $-\infty$ にします。softmaxの入力に $-\infty$ が加算されると、対応する出力は $0$ になります。 $$
\text{Attention}(\bm{Q}, \bm{K}, \bm{V}) = \text{softmax}\left(\frac{\bm{Q}\bm{K}^\top}{\sqrt{d_k}} + \bm{M}\right)\bm{V}
$$ この結果、位置 $i$ のトークンは位置 $j \leq i$ のトークンにのみAttentionを向けることができます。Causal Maskがない場合、位置2のトークンは位置5のトークン(未来のトークン)も参照できてしまい、「未来を見てから予測する」という不正が生じます。Causal Maskはこの不正を防ぎ、因果的な情報の流れを強制するのです。 CLMの学習でもう一つ重要な概念がTeacher Forcingです。学習時のモデルへの入力と出力の関係を明確にしましょう。 入力系列を $(x_1, x_2, \ldots, x_{T-1})$、ターゲット系列を $(x_2, x_3, \ldots, x_T)$ とします。つまり、入力を1トークンずらしたものがターゲットです。位置 $t$ の入力に対して、モデルは位置 $t+1$ のトークンを予測する — これがCLMの学習の基本構造です。 「Teacher Forcing」の名前の由来は、モデルの予測結果ではなく、教師(正解データ)が提供するトークン列を常に入力として使うことにあります。もしモデルが位置 $t$ で間違った予測をしたとしても、位置 $t+1$ の入力には正解トークン $x_{t+1}$ が使われます。 この方法の利点は効率性です。Causal Maskのおかげで、1回のforward passで全位置の予測を並列に計算できます。位置1の出力は位置2のトークンを、位置2の出力は位置3のトークンを、… というように、$T-1$ 個の予測が同時に得られます。自己回帰的に1トークンずつ生成する推論時とは異なり、学習時はこの並列化により非常に効率的な計算が可能になります。 ただし、Teacher Forcingにはexposure biasという問題もあります。学習時は常に正解のトークン列が入力されますが、推論時にはモデル自身の予測を次の入力として使います。学習時と推論時の入力分布にギャップがあるため、エラーが蓄積しやすくなる場合があります。この問題は後にScheduled SamplingやRLHFなどの手法で緩和されていきます。 CLMの数学的な枠組みが整理できました。しかし、CLMは自然言語処理における唯一の事前学習手法ではありません。BERTが採用するMLM(マスク言語モデル)は、まったく異なるアプローチで言語を学習します。次に、この2つの手法を構造的に比較し、それぞれの強みと弱みを明らかにしましょう。 CLMとMLMは、どちらもTransformerを用いた事前学習手法ですが、「テキストからどのように学ぶか」という設計思想が根本的に異なります。 CLMは作文に喩えられます。文の冒頭から一語ずつ書き進め、次にどんな言葉が来るかを予測します。まだ書いていない部分(未来)は見えません。一方、MLMは穴埋め問題に喩えられます。文中のいくつかの単語を隠し、前後の文脈から隠された単語を復元します。 この違いが、文脈の利用方向、生成能力、理解能力、学習効率というあらゆる側面に影響を及ぼします。 CLMは単方向(left-to-right)の文脈しか利用しません。位置 $t$ のトークンを予測するとき、$x_1, \ldots, x_{t-1}$ のみを条件とします。 $$
P_{\text{CLM}}(x_t \mid x_1, \ldots, x_{t-1})
$$ 一方、MLMは双方向(bidirectional)の文脈を利用します。マスクされた位置 $i$ のトークンを予測するとき、マスクされていない全てのトークン(位置 $i$ の前も後も)を条件とします。 $$
P_{\text{MLM}}(x_i \mid \bm{x}_{\setminus \mathcal{M}})
$$ 双方向の文脈を使えるMLMは、個々のトークンの意味を正確に把握する能力に優れます。「彼は銀行に___を預けた」の空欄を埋めるには、左側の「銀行に」だけでなく右側の「預けた」も必要です。一方CLMは、「彼は銀行に」までしか見えないため、「銀行」が金融機関か川岸かの判断が難しくなります。 テキスト生成においてCLMが圧倒的に優位なのは、学習と推論の構造が完全に一致するからです。 CLMの学習では「$x_{ $$
\hat{x}_{t+1} \sim P(x_{t+1} \mid x_1, \ldots, x_t; \theta)
$$ MLMの場合、テキスト生成には追加の工夫が必要です。どの位置にマスクを置くか、一度に何トークン生成するか、生成順序をどうするかなど、学習時には存在しなかった設計上の決定が求められます。このため、長文の自然なテキスト生成にはCLMベースのモデルが標準的に用いられます。 文の理解(分類、質問応答、固有表現認識など)では、MLMが構造的に有利です。理由は明快で、双方向の文脈を利用できるからです。 文の感情を判定するタスクを考えましょう。「この映画は最初は退屈だったが、後半は圧倒的に面白かった」という文では、「退屈」と「面白かった」の両方を見ないと全体の感情は判定できません。文末に到達するまで「退屈」が支配的だった印象が覆されることを、CLMは文の途中では認識できません。MLMのように文全体を双方向に見渡せるモデルなら、最初から「面白かった」の情報を「退屈」の表現にも反映できます。 ただし、GPT-3以降の大規模CLMは、文の理解タスクにおいてもBERT級の性能を発揮するようになりました。モデルサイズとデータ量のスケーリングが、アーキテクチャの制約を一定程度補償するためです。 CLMとMLMの学習効率には重要な違いがあります。 CLMでは、系列内の全てのトークンが予測対象です。長さ $T$ の系列から $T-1$ 個の損失項が得られます(最初のトークンは入力のみ)。 $$
\mathcal{L}_{\text{CLM}} = -\sum_{t=1}^{T-1} \log P(x_{t+1} \mid x_{\leq t}; \theta)
$$ 一方、MLMでは系列の約15%のトークンのみがマスクされ、予測対象となります。マスク集合 $\mathcal{M}$($|\mathcal{M}| \approx 0.15T$)に含まれるトークンのみから損失が計算されます。 $$
\mathcal{L}_{\text{MLM}} = -\sum_{i \in \mathcal{M}} \log P(x_i \mid \bm{x}_{\setminus \mathcal{M}}; \theta)
$$ つまり、同じ長さのテキストから得られる学習シグナルの量は、CLMがMLMの約6.7倍($1/0.15 \approx 6.7$)です。このトークン効率の良さは、CLMが大規模な事前学習において好まれる大きな理由の一つです。 以上の議論を表形式で整理します。 CLMとMLMの比較から、CLMが「学習効率」と「生成能力」の両面で大規模事前学習に適していることが見えてきました。では、CLMは具体的に何を学習しているのでしょうか。次に、学習過程でモデルの内部に何が獲得されていくのかを詳しく見ていきます。 「次のトークンを予測する」というシンプルなタスクを通じて、CLMは驚くほど多層的な知識を獲得します。学習が進むにつれてモデル内部に蓄積される知識を、4つの階層に整理しましょう。 第1層: 構文パターン(Syntactic Patterns) 学習の初期段階で最も早く獲得されるのは、言語の統語的な規則です。「”the” の後には名詞が来やすい」「”is” の後には形容詞や名詞句が来やすい」「文末には “。” が来る」といった局所的なパターンです。これらは出現頻度が非常に高く、少量のデータでもすぐに学習できます。 日本語であれば、「彼は」の後に「が」が続くことはまずなく、「を」「に」「で」などの助詞や動詞が来ること、「です」の後には「。」が来やすいことなどが、ごく初期に獲得されます。 第2層: 意味的一貫性(Semantic Coherence) 構文パターンの次に獲得されるのは、文の意味的な一貫性です。「猫は魚を」の空欄に「食べた」が来る確率は「飛んだ」よりも高い。「太陽は東から」には「昇る」が来る。このような世界知識に基づく意味的な予測は、構文パターンより多くのデータを必要とします。 この段階では、単語間の共起関係だけでなく、「猫は食べ物に関連する行動をする」「太陽は天体として特定の振る舞いをする」といった概念レベルの知識がモデルの重みに刻まれていきます。 第3層: 世界知識(World Knowledge) さらに学習が進むと、モデルは訓練テキストに含まれる事実的な知識を獲得し始めます。「東京はの首都です」の空欄を「日本」で埋められるようになり、「アインシュタインはを提唱した」に対して「相対性理論」を高い確率で予測できるようになります。 興味深いのは、これらの事実がモデルのパラメータに暗黙的に記憶されることです。明示的なデータベースではなく、ニューラルネットワークの重み行列の中に分散的に符号化されます。近年の研究では、Transformerの中間層のFeed-Forward Networkが事実的知識の記憶に大きな役割を果たしていることが示されています。 第4層: 推論パターン(Reasoning Patterns) 最も高次の学習として、論理的な推論パターンの獲得があります。「もし A ならば B。A が成り立つ。よって___」に対して「B が成り立つ」を予測するには、三段論法の構造を理解している必要があります。 数学の証明パターン、因果関係の推論、類推による知識の転移なども、十分な規模のモデルと十分な量のデータがあれば「次のトークンを予測する」タスクを通じて獲得されます。これは「スケーリングの魔法」とも呼ばれ、後述する創発能力(Emergent Abilities)と深く関係しています。 CLMの学習は、上記の4つの階層を段階的に習得していく過程として理解できます。 学習の初期段階では、モデルはまず最も頻出する構文パターンを捉えます。この段階ではPerplexity(後述)が急速に低下します。冠詞、前置詞、句読点などの頻出トークンの予測精度が劇的に向上するためです。 中期段階になると、Perplexityの低下は緩やかになりますが、モデルは徐々に稀なパターンや長い依存関係を学習し始めます。たとえば、文の主語が数十トークン離れた動詞の活用形に影響するような構造を正しく捉えるには、学習初期の段階では不十分で、十分なパラメータ数と十分な学習ステップが必要です。 後期段階では、文レベルを超えた長距離の一貫性(段落間の論理的なつながり、議論の構造など)が改善されていきます。この段階での改善は目に見えにくいですが、生成されるテキストの質を大きく左右します。 CLMの学習を定量的に追跡する最も一般的な指標がPerplexity(パープレキシティ)です。Perplexityは、モデルが「次のトークンを予測するときにどれだけ迷っているか」を測る指標です。 Perplexityの定義は次のとおりです。 $$
\text{PPL} = \exp\left(-\frac{1}{T}\sum_{t=1}^{T} \log P(x_t \mid x_{ この式は、CLMの損失関数(負の対数尤度)を系列長で割り、指数関数で元のスケールに戻したものです。損失関数をトークンあたりの平均に正規化してからexpを取ることで、「各ステップで何択の中から選んでいるか」に近い直感的な解釈が得られます。 たとえば、Perplexityが100であれば、モデルは各トークンを予測するとき「平均的に100個の候補の中で均等に迷っている」ような状態です。Perplexityが10に下がれば、「10個の候補に絞れている」状態であり、予測精度が大幅に向上したことを意味します。完璧なモデルのPerplexityは1です(常に正解に確率1を割り当てるため)。 学習の初期では数千のPerplexityから始まり、数百、数十と急速に低下していきます。あるところで低下が緩やかになり、学習率の調整やデータの追加なしにはそれ以上下がりにくくなります。このPerplexityの飽和点は、モデルのサイズとデータの量によって決まり、より大きなモデルとより多くのデータがあれば、より低いPerplexityに到達できます。 Perplexityとモデルの振る舞いがイメージできたところで、次はCLMの核心であるCausal Maskの実装に踏み込みましょう。 Causal Maskは、形式的には $T \times T$ の行列で表されます。$T=4$ の場合を具体的に書き出してみましょう。 $$
\bm{M} = \begin{pmatrix}
0 & -\infty & -\infty & -\infty \\
0 & 0 & -\infty & -\infty \\
0 & 0 & 0 & -\infty \\
0 & 0 & 0 & 0
\end{pmatrix}
$$ 行がQuery(予測する側)、列がKey(参照される側)に対応します。$\bm{M}_{ij} = 0$ は「位置 $i$ が位置 $j$ を参照できる」ことを意味し、$\bm{M}_{ij} = -\infty$ は「位置 $i$ が位置 $j$ を参照できない」ことを意味します。 1行目を見ると、位置1のトークンは自分自身(位置1)のみを参照でき、それ以降の位置は全て遮断されています。2行目では位置1と位置2を参照でき、3行目では位置1、2、3を参照できます。最後の4行目は全ての位置を参照できます。 これはまさに下三角行列の構造であり、情報が「過去から現在」へのみ流れることを保証しています。 マスクがAttention計算にどう作用するかを、4トークンの具体例で追ってみましょう。 Attentionスコア行列を $\bm{S} = \bm{Q}\bm{K}^\top / \sqrt{d_k}$ とします。たとえば、 $$
\bm{S} = \begin{pmatrix}
1.2 & 0.8 & 0.5 & 0.3 \\
0.9 & 1.5 & 0.7 & 0.4 \\
0.6 & 1.1 & 1.8 & 0.9 \\
0.3 & 0.7 & 1.2 & 2.0
\end{pmatrix}
$$ Causal Maskを加算すると、上三角部分が $-\infty$ になります。 $$
\bm{S} + \bm{M} = \begin{pmatrix}
1.2 & -\infty & -\infty & -\infty \\
0.9 & 1.5 & -\infty & -\infty \\
0.6 & 1.1 & 1.8 & -\infty \\
0.3 & 0.7 & 1.2 & 2.0
\end{pmatrix}
$$ ここでsoftmaxを行方向に適用します。$-\infty$ に対して $\exp(-\infty) = 0$ となるため、マスクされた位置の重みは正確に $0$ になります。 1行目の場合、有効な要素は $1.2$ のみなので、softmax後は $(1.0, 0, 0, 0)$ となります。位置1のトークンは自分自身のValueのみを出力に反映します。 2行目の場合、有効な要素は $0.9$ と $1.5$ です。softmaxを計算すると次のようになります。 $$
\text{softmax}(0.9, 1.5) = \left(\frac{e^{0.9}}{e^{0.9}+e^{1.5}},\ \frac{e^{1.5}}{e^{0.9}+e^{1.5}}\right) \approx (0.354,\ 0.646)
$$ したがって2行目は $(0.354, 0.646, 0, 0)$ となり、位置2のトークンは位置1と位置2のValueを重み付け平均したものを出力に反映します。未来の位置3, 4からの情報は完全に遮断されています。 このように、Causal Maskは「加算 → softmax → ゼロ化」という流れで因果的な情報の流れを実現しており、実装上は非常にシンプルです。 Causal Maskのもう一つの重要な性質は、学習時の効率性です。マスクのおかげで、1回のforward passで系列内の全位置について「そこまでの文脈だけを使った予測」が同時に得られます。 位置1の出力は「位置1のみの文脈で位置2を予測」、位置2の出力は「位置1-2の文脈で位置3を予測」、…、位置 $T-1$ の出力は「位置1-$(T-1)$の文脈で位置 $T$ を予測」。これらは全て同一のforward passで並列に計算されます。もしCausal Maskがなければ、位置ごとに個別にforward passを実行する必要があり、計算量は $T$ 倍に膨れ上がります。 理論的な理解が整ったところで、いよいよPyTorchでCLMを実装していきましょう。Causal Maskの生成、小規模Transformer Decoderの構築、テキスト生成、Perplexityの計算まで、一通りのパイプラインをコードで確認します。 まずはCLMの核となるCausal Maskを生成する関数を実装します。PyTorchにはこれを行うユーティリティが用意されていますが、ここでは原理を理解するために手動で構築します。 左側のヒートマップでは、白いセル($0$)が「参照可能」、黒いセル($-\infty$)が「遮断」を表しています。下三角部分のみが白くなっており、各Queryが自分以前のKey位置のみを参照できることが視覚的に確認できます。右側はsoftmax適用後のAttention重みで、マスクされた位置が正確に $0$ になっていることがわかります。また、各行の合計が $1.0$ であることも確認でき、softmaxが有効な位置のみで正規化されていることがわかります。 次に、小規模なTransformer Decoderベースの因果言語モデルを実装します。実際のGPTは数十億パラメータですが、ここでは原理を理解するために、数万パラメータの小さなモデルを構築します。 このクラスでは、Q・K・Vの射影を1つの線形層 続いて、Decoderブロック全体とCLMモデルを組み立てます。 このモデルのアーキテクチャは、GPT-2のPre-LN(Layer Normalizationを各サブレイヤーの前に置く)構成を簡略化したものです。重要な設計として、 実装したモデルを使って、小さなテキストデータでCLMの学習を行いましょう。ここでは文字レベルのトークン化を使い、短いテキストを学習データとして使います。 この前処理では、文字レベルのトークン化を行っています。語彙サイズが小さく抑えられるため、小さなモデルでも学習が可能です。実際のGPTではBPE(Byte Pair Encoding)やSentencePieceなどのサブワードトークナイザが使われますが、原理の理解には文字レベルで十分です。 次に、学習ループを実装して損失の推移を観察します。 学習曲線から、いくつかの典型的な特徴が観察できます。まず、学習の初期段階で損失が急速に低下しています。これはモデルが最も頻出するパターン(空白文字や “the” などの頻出トークン)を素早く学習しているためです。その後、損失の低下は緩やかになり、モデルはより稀なパターンや長い依存関係を徐々に学習していきます。移動平均を重ねることで、損失が単調に減少していく全体的なトレンドが明確に確認できます。 学習したモデルを使って、テキストを自動生成しましょう。2つの生成戦略 — Greedy DecodingとSampling — を実装します。 生成結果から、2つの重要な特徴が読み取れます。第一に、Greedy Decodingは常に最も確率の高いトークンを選ぶため、同じプロンプトに対して毎回同じ出力を返します。学習データに頻出するパターンが再現されやすい一方、多様性に欠けます。第二に、Samplingでは温度パラメータにより生成の多様性を制御できます。温度が低い($< 1.0$)と確率分布が鋭くなりGreedyに近づき、温度が高い($> 1.0$)と分布が平坦になりランダム性が増します。温度が高すぎると不自然なテキストが生成されるため、実用的には $0.7$ から $1.0$ 程度の温度が多く使われます。 最後に、学習済みモデルのPerplexityを計算して、モデルの予測品質を定量的に評価しましょう。 Perplexityの結果から、いくつかの重要な観察ができます。学習データに対するPerplexityは概ね低い値を示しており、モデルがこれらのテキストを高い確率で予測できることを意味します。テストデータのうち、学習データに類似したパターン(”the cat ran on the mat” など)はPerplexityが比較的低く抑えられます。これは、モデルが「the + 動物名 + 動詞」というパターンを汎化的に学習していることを示唆しています。一方、学習データに登場しない文字やパターンを含むテキストではPerplexityが急上昇し、モデルの知識の限界が明確に表れます。 小規模な実装を通じてCLMの学習と推論の全体像が掴めました。しかし、CLMの真の力は、この仕組みを巨大なスケールに拡大したときに初めて発揮されます。最後に、スケーリングとCLMの関係を見ていきましょう。 CLMの最も注目すべき性質の一つは、モデルサイズを大きくすると、Perplexityが予測可能な形で低下し続けることです。この関係はスケーリング則(Scaling Law)として知られ、2020年のKaplanらの研究で定量的に示されました。 スケーリング則は、次の関係を主張します。テストデータに対するPerplexity(の対数、すなわち損失)は、モデルのパラメータ数 $N$、データセットのサイズ $D$、計算量 $C$ のそれぞれに対して、べき乗則に従って単調に減少します。 $$
L(N) \approx \left(\frac{N_c}{N}\right)^{\alpha_N}
$$ ここで $L$ は損失(クロスエントロピー)、$N_c$ と $\alpha_N$ は定数です。対数スケールでプロットすると直線関係が現れるという、驚くほどクリーンな法則です。 直感的に言えば、パラメータを10倍にすれば、損失は一定の割合だけ下がるのです。この予測可能性のおかげで、「GPT-4規模のモデルを学習する前に、その性能をある程度予測する」ことが可能になりました。 CLMがスケーリングに特に適している理由は、いくつかの構造的な特性にあります。 第一に、学習データの効率的な活用です。 前述のとおり、CLMは系列内の全トークン($T-1$ 個)を予測対象とします。MLMの約15%と比較して、1つの系列から約6.7倍の学習シグナルが得られます。大規模学習では数兆トークンのデータを処理しますが、CLMはその全てから効率よく学習できるのです。 第二に、学習目標の統一性です。 CLMの損失関数は「次のトークンを予測する」という一つのタスクに完全に統一されています。MLMのように「マスク戦略をどうするか(ランダムマスク、Whole Word Masking、Span Maskingなど)」という設計上の選択が不要です。この単純さがハイパーパラメータの調整を容易にし、スケーリングの際に余計な複雑性を排除します。 第三に、生成と理解の統合です。 CLMは「次のトークンを予測する」という形式で学習するため、生成タスクと理解タスクの両方を単一の枠組みでカバーできます。In-context Learning(プロンプトに少数のタスク例を含めることでモデルに新しいタスクを解かせる手法)が可能になるのは、CLMの自己回帰的な構造があるからです。スケーリングによって多様な能力が単一モデルに集約できることが、CLMが現代のLLMの標準アーキテクチャとなった大きな理由です。 スケーリングの最も興味深い側面は、ある規模を超えたときに突然新しい能力が出現する創発(emergence)現象です。 小さなモデルではPerplexityの数値が下がっても、特定のベンチマーク(たとえば3桁の足し算や、複雑な論理推論)の精度はほぼゼロのままです。しかし、モデルサイズが特定の閾値を超えると、精度が急激に上昇します。この閾値はタスクによって異なりますが、多くの場合、数十億パラメータ規模で顕在化し始めます。 この現象は、CLMの「次のトークン予測」がPerplexityの低下という滑らかな改善を続ける中で、内部的に蓄積された知識がある臨界点を超えると質的に新しい能力として表出する、というメカニズムで理解されています。 たとえば、3桁の足し算「$347 + 258 = $」に正しく「$605$」と答えるには、繰り上がりの概念を理解し、各桁を正しく処理する必要があります。CLMの学習データに含まれる計算例から、小さなモデルでもある程度の数値パターンは学習しますが、汎用的な「繰り上がりのアルゴリズム」を獲得するには、十分なモデル容量と十分な量の学習データが必要です。 ただし、創発能力の「突然性」については議論があります。一部の研究者は、評価指標の選び方(非線形な指標で測定すると突然に見えるだけで、線形な指標では滑らかに改善している)が見かけの突然性を生んでいるという指摘もしています。いずれにせよ、CLMのスケーリングが多様な能力を同時に引き出す強力なアプローチであることは間違いありません。 CLMは現代のLLMの中核ですが、万能ではありません。 双方向理解の限界: 固有表現認識や文の類似度判定など、文全体を双方向に理解するタスクでは、同規模のMLMベースモデルに劣る場合があります。この限界は、モデルのスケーリングによってある程度補償されますが、構造的な非効率性として残ります。 自己回帰生成の遅さ: 推論時にはトークンを1つずつ逐次生成する必要があるため、長いテキストの生成には時間がかかります。これに対して、Speculative Decoding や非自己回帰的な生成手法など、推論速度を改善するための研究が活発に進められています。 Hallucination(幻覚): CLMは学習データに基づいて「もっともらしい次のトークン」を生成するため、事実に反する内容を自信を持って生成してしまうことがあります。これはCLMの学習目標が「確率的に自然なテキストを生成する」ことであり、「事実かどうかを検証する」ことではないためです。 これらの課題に対して、Retrieval-Augmented Generation(RAG)やRLHFなどの手法が開発されていますが、いずれもCLMの基盤の上に構築されています。CLMの数理を理解していることが、これらの発展的な手法を学ぶ上での前提となるのです。 本記事では、因果言語モデル(CLM)の理論と実装を包括的に解説しました。 CLMは現代のLLMの根幹であり、ここで学んだ数理はGPTシリーズ、LLaMA、Geminiなどの理解に直結します。 次のステップとして、以下の記事も参考にしてください。なぜ「左から右」なのか — 因果性
n-gramモデルとの比較
CLMの数学的定式化
目的関数 — 負の対数尤度
nn.CrossEntropyLoss がまさにこの計算を行います。条件付き確率の計算
Causal Mask の役割
Teacher Forcing
CLM vs MLM の比較
根本的な設計思想の違い
方向性 — 単方向 vs 双方向
生成能力 — CLMの圧倒的優位性
理解能力 — MLMの構造的アドバンテージ
学習効率 — 全トークン予測 vs 15%予測
比較表
比較項目
CLM(GPT系)
MLM(BERT系)
文脈の方向
単方向(左→右)
双方向
予測対象
全トークン($T-1$個)
マスクされたトークン(約$0.15T$個)
1系列あたりの学習シグナル
多い
少ない
テキスト生成
自然に可能
追加の工夫が必要
文理解タスク
スケールで補償
構造的に有利
代表モデル
GPT, GPT-2, GPT-3, LLaMA
BERT, RoBERTa, DeBERTa
推論時のコスト
逐次生成(遅い)
1回のforward pass
学習と推論の一貫性
完全に一致
乖離あり([MASK]トークン問題)
CLMの学習ダイナミクス
CLMは何を学んでいるのか
学習の進行 — 易から難へ
Perplexityの推移
Causal Maskの実装と数理
マスク行列の構造
softmaxとの組み合わせ
効率性の観点
PyTorchでの実装
Causal Maskの生成
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
import matplotlib.pyplot as plt
def create_causal_mask(seq_len):
"""
Causal Mask(因果マスク)を生成する
上三角部分を -inf にした行列を返す
"""
# 全要素0の行列から出発し、上三角部分を-infにする
mask = torch.zeros(seq_len, seq_len)
mask = mask.masked_fill(
torch.triu(torch.ones(seq_len, seq_len), diagonal=1).bool(),
float('-inf')
)
return mask
# 4トークンの場合のCausal Maskを確認
mask = create_causal_mask(4)
print("Causal Mask (4x4):")
print(mask)
# 可視化
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
# マスク行列の可視化(0を白、-infを黒)
mask_vis = (mask == 0).float().numpy()
axes[0].imshow(mask_vis, cmap='gray', vmin=0, vmax=1)
axes[0].set_title("Causal Mask (white=attend, black=mask)")
axes[0].set_xlabel("Key position")
axes[0].set_ylabel("Query position")
for i in range(4):
for j in range(4):
color = 'black' if mask_vis[i, j] > 0.5 else 'white'
axes[0].text(j, i, f'{int(mask[i,j].item())}' if mask[i,j] != float('-inf') else '-∞',
ha='center', va='center', fontsize=12, color=color)
# softmax適用後のAttention重み
scores = torch.randn(4, 4) # ランダムなスコア
masked_scores = scores + mask
attention_weights = F.softmax(masked_scores, dim=-1)
axes[1].imshow(attention_weights.detach().numpy(), cmap='Blues', vmin=0, vmax=1)
axes[1].set_title("Attention weights after softmax")
axes[1].set_xlabel("Key position")
axes[1].set_ylabel("Query position")
for i in range(4):
for j in range(4):
axes[1].text(j, i, f'{attention_weights[i,j]:.2f}',
ha='center', va='center', fontsize=10)
plt.tight_layout()
plt.savefig("causal_mask.png", dpi=150, bbox_inches='tight')
plt.show()
簡易CLMモデルの実装
import torch
import torch.nn as nn
import torch.nn.functional as F
import math
class CausalSelfAttention(nn.Module):
"""Causal Mask付きのMulti-Head Self-Attention"""
def __init__(self, d_model, n_heads, dropout=0.1):
super().__init__()
assert d_model % n_heads == 0
self.d_model = d_model
self.n_heads = n_heads
self.d_k = d_model // n_heads
# Q, K, V の線形射影(まとめて1つの線形層で計算)
self.qkv_proj = nn.Linear(d_model, 3 * d_model)
self.out_proj = nn.Linear(d_model, d_model)
self.dropout = nn.Dropout(dropout)
def forward(self, x):
B, T, C = x.shape # バッチ, 系列長, 埋め込み次元
# Q, K, V を一括計算して分割
qkv = self.qkv_proj(x) # (B, T, 3*C)
q, k, v = qkv.chunk(3, dim=-1) # 各 (B, T, C)
# ヘッドに分割: (B, T, C) -> (B, n_heads, T, d_k)
q = q.view(B, T, self.n_heads, self.d_k).transpose(1, 2)
k = k.view(B, T, self.n_heads, self.d_k).transpose(1, 2)
v = v.view(B, T, self.n_heads, self.d_k).transpose(1, 2)
# Scaled Dot-Product Attention + Causal Mask
scores = torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(self.d_k)
# Causal Mask を生成して適用
causal_mask = torch.triu(
torch.ones(T, T, device=x.device), diagonal=1
).bool()
scores = scores.masked_fill(causal_mask.unsqueeze(0).unsqueeze(0), float('-inf'))
attn_weights = F.softmax(scores, dim=-1)
attn_weights = self.dropout(attn_weights)
# Attention出力
out = torch.matmul(attn_weights, v) # (B, n_heads, T, d_k)
out = out.transpose(1, 2).contiguous().view(B, T, C) # (B, T, C)
return self.out_proj(out)
qkv_proj でまとめて計算し、chunk操作で3分割しています。Causal Maskは torch.triu で上三角行列を生成し、masked_fill でスコア行列の対応箇所を $-\infty$ に設定しています。class TransformerDecoderBlock(nn.Module):
"""1層のTransformer Decoderブロック(Pre-LN構成)"""
def __init__(self, d_model, n_heads, d_ff, dropout=0.1):
super().__init__()
self.ln1 = nn.LayerNorm(d_model)
self.attn = CausalSelfAttention(d_model, n_heads, dropout)
self.ln2 = nn.LayerNorm(d_model)
self.ffn = nn.Sequential(
nn.Linear(d_model, d_ff),
nn.GELU(),
nn.Linear(d_ff, d_model),
nn.Dropout(dropout),
)
def forward(self, x):
# Pre-LN: LayerNorm → Attention → 残差接続
x = x + self.attn(self.ln1(x))
# Pre-LN: LayerNorm → FFN → 残差接続
x = x + self.ffn(self.ln2(x))
return x
class CausalLanguageModel(nn.Module):
"""小規模な因果言語モデル"""
def __init__(self, vocab_size, d_model=128, n_heads=4,
n_layers=4, d_ff=512, max_len=256, dropout=0.1):
super().__init__()
self.d_model = d_model
# トークン埋め込み + 位置埋め込み
self.token_emb = nn.Embedding(vocab_size, d_model)
self.pos_emb = nn.Embedding(max_len, d_model)
self.dropout = nn.Dropout(dropout)
# Transformer Decoderブロックをn_layers層積む
self.blocks = nn.ModuleList([
TransformerDecoderBlock(d_model, n_heads, d_ff, dropout)
for _ in range(n_layers)
])
# 最終層: LayerNorm + LM head
self.ln_f = nn.LayerNorm(d_model)
self.lm_head = nn.Linear(d_model, vocab_size, bias=False)
# 重み共有: トークン埋め込みとLM headの重みを共有
self.lm_head.weight = self.token_emb.weight
# パラメータの初期化
self.apply(self._init_weights)
def _init_weights(self, module):
if isinstance(module, nn.Linear):
nn.init.normal_(module.weight, mean=0.0, std=0.02)
if module.bias is not None:
nn.init.zeros_(module.bias)
elif isinstance(module, nn.Embedding):
nn.init.normal_(module.weight, mean=0.0, std=0.02)
def forward(self, input_ids):
B, T = input_ids.shape
device = input_ids.device
# トークン埋め込み + 位置埋め込み
positions = torch.arange(T, device=device).unsqueeze(0) # (1, T)
x = self.token_emb(input_ids) + self.pos_emb(positions)
x = self.dropout(x)
# Transformer Decoderブロックを順に適用
for block in self.blocks:
x = block(x)
# 最終LayerNorm + logit計算
x = self.ln_f(x)
logits = self.lm_head(x) # (B, T, vocab_size)
return logits
self.lm_head.weight = self.token_emb.weight による重み共有(weight tying)があります。入力側のトークン埋め込み行列と出力側の語彙射影行列を共有することで、パラメータ数を削減しつつ、入力空間と出力空間の一貫性を保っています。小さなテキストデータでの学習
import torch
import torch.nn as nn
import torch.nn.functional as F
import math
import matplotlib.pyplot as plt
# 学習データ: 簡単な文のセット
texts = [
"the cat sat on the mat",
"the dog ran in the park",
"a bird flew over the house",
"the sun shines in the sky",
"a fish swam in the sea",
"the cat chased the mouse",
"the dog played with the ball",
"a child read a book at home",
"the moon rises at night",
"the wind blows through trees",
]
# 文字レベルのトークナイザを構築
all_chars = sorted(set(''.join(texts)))
char_to_idx = {ch: i + 1 for i, ch in enumerate(all_chars)} # 0はパディング用
char_to_idx['<pad>'] = 0
idx_to_char = {i: ch for ch, i in char_to_idx.items()}
vocab_size = len(char_to_idx)
print(f"語彙サイズ: {vocab_size}")
print(f"語彙: {all_chars}")
# テキストをトークンIDに変換
def encode(text):
return [char_to_idx[ch] for ch in text]
def decode(ids):
return ''.join([idx_to_char.get(i, '?') for i in ids])
# 学習データのエンコード
encoded_texts = [encode(t) for t in texts]
max_len = max(len(t) for t in encoded_texts)
# パディングしてテンソルに変換
padded = [t + [0] * (max_len - len(t)) for t in encoded_texts]
data = torch.tensor(padded, dtype=torch.long)
print(f"データ形状: {data.shape}") # (10, max_len)
import torch
import torch.nn as nn
import torch.nn.functional as F
import math
import matplotlib.pyplot as plt
# モデルのインスタンス化
# (CausalSelfAttention, TransformerDecoderBlock,
# CausalLanguageModel の定義は前のコードブロックを参照)
torch.manual_seed(42)
model = CausalLanguageModel(
vocab_size=vocab_size,
d_model=64,
n_heads=4,
n_layers=2,
d_ff=256,
max_len=max_len,
dropout=0.1,
)
total_params = sum(p.numel() for p in model.parameters())
print(f"パラメータ数: {total_params:,}")
# 学習設定
optimizer = torch.optim.AdamW(model.parameters(), lr=3e-3, weight_decay=0.01)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=2000)
n_epochs = 2000
# 学習ループ
losses = []
model.train()
for epoch in range(n_epochs):
# 入力: x[:, :-1], ターゲット: x[:, 1:](1トークンずらし)
input_ids = data[:, :-1]
targets = data[:, 1:]
# Forward pass
logits = model(input_ids) # (B, T-1, vocab_size)
# パディング位置を無視して損失を計算
# パディングトークン(0)をignore_indexに指定
loss = F.cross_entropy(
logits.reshape(-1, vocab_size),
targets.reshape(-1),
ignore_index=0,
)
# Backward pass + 更新
optimizer.zero_grad()
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
optimizer.step()
scheduler.step()
losses.append(loss.item())
if (epoch + 1) % 500 == 0:
print(f"Epoch {epoch+1:5d} | Loss: {loss.item():.4f}")
# 学習曲線の可視化
fig, ax = plt.subplots(figsize=(10, 5))
ax.plot(losses, linewidth=0.8, alpha=0.6, label='Training Loss')
# 移動平均
window = 50
if len(losses) > window:
moving_avg = np.convolve(losses, np.ones(window)/window, mode='valid')
ax.plot(range(window-1, len(losses)), moving_avg, linewidth=2,
color='red', label=f'Moving Average (window={window})')
ax.set_xlabel('Epoch')
ax.set_ylabel('Cross-Entropy Loss')
ax.set_title('CLM Training Loss Curve')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig("clm_training_loss.png", dpi=150, bbox_inches='tight')
plt.show()
テキスト生成
import torch
import torch.nn.functional as F
def generate_text(model, start_text, max_new_tokens=30,
strategy='greedy', temperature=1.0):
"""
CLMによるテキスト生成
strategy: 'greedy'(最も確率の高いトークンを選択)
'sampling'(確率分布からサンプリング)
temperature: サンプリング時の温度パラメータ
"""
model.eval()
input_ids = torch.tensor([encode(start_text)], dtype=torch.long)
with torch.no_grad():
for _ in range(max_new_tokens):
# 現在の系列でforward pass
logits = model(input_ids)
# 最後の位置のlogitのみ使用(次のトークンの予測)
next_logits = logits[:, -1, :] # (1, vocab_size)
if strategy == 'greedy':
# Greedy: 最も確率の高いトークンを選択
next_token = next_logits.argmax(dim=-1, keepdim=True)
elif strategy == 'sampling':
# Sampling: 温度付きでサンプリング
next_logits = next_logits / temperature
probs = F.softmax(next_logits, dim=-1)
next_token = torch.multinomial(probs, num_samples=1)
# パディングトークンが生成されたら終了
if next_token.item() == 0:
break
# 生成されたトークンを入力に追加
input_ids = torch.cat([input_ids, next_token], dim=-1)
return decode(input_ids[0].tolist())
# Greedy Decoding
print("=== Greedy Decoding ===")
for prompt in ["the ", "a ", "the c"]:
generated = generate_text(model, prompt, max_new_tokens=25, strategy='greedy')
print(f" '{prompt}' -> '{generated}'")
print()
# Sampling(温度を変えて比較)
print("=== Sampling (different temperatures) ===")
torch.manual_seed(0)
for temp in [0.5, 1.0, 1.5]:
generated = generate_text(
model, "the ", max_new_tokens=25,
strategy='sampling', temperature=temp
)
print(f" temperature={temp:.1f} -> '{generated}'")
Perplexityの計算
import torch
import torch.nn.functional as F
import numpy as np
import matplotlib.pyplot as plt
def compute_perplexity(model, text):
"""テキストに対するPerplexityを計算する"""
model.eval()
input_ids = torch.tensor([encode(text)], dtype=torch.long)
with torch.no_grad():
logits = model(input_ids[:, :-1]) # (1, T-1, vocab_size)
targets = input_ids[:, 1:] # (1, T-1)
# 各位置の対数確率を計算
log_probs = F.log_softmax(logits, dim=-1) # (1, T-1, vocab_size)
# 正解トークンの対数確率を取得
target_log_probs = log_probs.gather(
dim=-1, index=targets.unsqueeze(-1)
).squeeze(-1) # (1, T-1)
# パディングを除外
mask = (targets != 0).float()
n_tokens = mask.sum().item()
# 平均負の対数尤度
avg_nll = -(target_log_probs * mask).sum().item() / n_tokens
# Perplexity = exp(平均NLL)
ppl = np.exp(avg_nll)
return ppl, avg_nll
# 各テキストのPerplexityを計算
print("=== Perplexity (学習データ) ===")
ppls_train = []
for text in texts:
ppl, nll = compute_perplexity(model, text)
ppls_train.append(ppl)
print(f" PPL={ppl:8.2f} | NLL={nll:.4f} | '{text}'")
# 未知のテキスト(学習データに含まれない)
test_texts = [
"the cat ran on the mat", # 学習データに近い
"a dog sat in the park", # 学習データに近い
"xyz quantum flux kapow", # 学習データにない文字を含む可能性
]
print("\n=== Perplexity (テストデータ) ===")
ppls_test = []
for text in test_texts:
try:
ppl, nll = compute_perplexity(model, text)
ppls_test.append(ppl)
print(f" PPL={ppl:8.2f} | NLL={nll:.4f} | '{text}'")
except KeyError as e:
print(f" 未知の文字: {e} | '{text}'")
# Perplexityの分布を可視化
fig, ax = plt.subplots(figsize=(10, 5))
x = range(len(ppls_train))
ax.bar(x, ppls_train, color='steelblue', alpha=0.8)
ax.set_xticks(x)
ax.set_xticklabels([t[:15] + '...' if len(t) > 15 else t for t in texts],
rotation=45, ha='right', fontsize=8)
ax.set_ylabel('Perplexity')
ax.set_title('Perplexity per Training Text')
ax.grid(True, axis='y', alpha=0.3)
plt.tight_layout()
plt.savefig("clm_perplexity.png", dpi=150, bbox_inches='tight')
plt.show()
スケーリングとCLMの関係
モデルサイズとPerplexityの関係
なぜCLMがスケーリングに適しているのか
創発能力(Emergent Abilities)
CLMの限界と今後の展望
まとめ