Google Colabで「自分だけのLLM」をゼロから作ってみた
ChatGPTやClaudeの裏側では何が動いているのか。気になったので、Google Colabの無料枠だけを使って、小さな言語モデル(LLM)をゼロから構築してみた。学習データの準備からTransformerの実装、テキスト生成まで、一通りの流れを記録する。
全体の流れ
今回やったことは5ステップ。
- 学習データの準備 — 青空文庫から著作権切れのテキストを取得
- Tokenizerの学習 — SentencePieceでテキストをトークンに分割
- Transformerモデルの実装 — PyTorchでCausal Self-Attention付きのモデルを定義
- 学習ループの実行 — T4 GPUで10エポック回す
- テキスト生成 — 学習済みモデルでプロンプトから文章を生成
STEP 1:学習データの準備
学習データには青空文庫の著作権切れ作品を使った。
| 作品 | 著者 | 文字数 |
|---|---|---|
| 坊っちゃん | 夏目漱石 | 約10万文字 |
| 羅生門 | 芥川龍之介 | 約7千文字 |
| 吾輩は猫である | 夏目漱石 | 約37万文字 |
| 合計 | 約49万文字 |
ZIPファイルをダウンロードしてShift-JISでデコードするだけ。当初はルビや注記を正規表現で除去するクリーニング処理を入れていたが、これが本文まで削除してしまう問題に何度もハマった。最終的にクリーニングは一切行わず、デコードだけする方針に切り替えた。ルビが混ざっていても学習には大きな影響がない。
import urllib.request, zipfile, io
WORKS = [
("https://www.aozora.gr.jp/cards/000148/files/752_ruby_2438.zip", "坊っちゃん"),
("https://www.aozora.gr.jp/cards/000879/files/127_ruby_150.zip", "羅生門"),
("https://www.aozora.gr.jp/cards/000148/files/789_ruby_5639.zip", "吾輩は猫である"),
]
texts = []
for url, title in WORKS:
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
with urllib.request.urlopen(req, timeout=30) as r:
zb = r.read()
with zipfile.ZipFile(io.BytesIO(zb)) as zf:
txt_files = sorted(
[n for n in zf.namelist() if n.endswith(".txt")],
key=lambda n: zf.getinfo(n).file_size, reverse=True
)
raw = zf.read(txt_files[0])
try: text = raw.decode("shift_jis")
except: text = raw.decode("utf-8", errors="replace")
texts.append(text)
combined = "\n\n".join(texts)
TRAIN_PATH = "/tmp/train.txt"
with open(TRAIN_PATH, "w", encoding="utf-8") as f:
f.write(combined)
ハマりポイント: 青空文庫のテキストは作品ごとにヘッダー構造がバラバラで、区切り線の検出が非常に不安定。凝ったクリーニングをするより「そのまま使う」が最も確実だった。
STEP 2:SentencePieceでTokenizerを作る
テキストをニューラルネットに入力するには、文字列をトークン(数値ID)に変換する必要がある。ここではSentencePieceのBPE(Byte Pair Encoding)を使って語彙を学習した。
import sentencepiece as spm
spm.SentencePieceTrainer.train(
input=TRAIN_PATH,
model_prefix="/tmp/my_tokenizer",
vocab_size=4000,
character_coverage=0.9995,
model_type="bpe",
pad_id=0, unk_id=1, bos_id=2, eos_id=3,
)
sp = spm.SentencePieceProcessor()
sp.load("/tmp/my_tokenizer.model")
語彙サイズは4000。大規模モデルでは数万〜10万が一般的だが、今回の小型モデルにはこれで十分。
STEP 3:PyTorchでTransformerを実装する
このステップは「空っぽの脳の設計図を書く」工程。ニューロンを何個にするか、層を何段重ねるか、Attentionの仕組みをどうするか、といった構造を定義する。この時点ではまだ何も学習しておらず、全パラメータがランダムな値で初期化されているだけ。何を入力してもデタラメな出力しか返さない。
モデルの構成は以下の通り。
- Token + Positional Embedding — 各トークンの意味と位置を埋め込みベクトルに変換
- Causal Self-Attention × 4層 — 「未来のトークンを見ない」マスク付きのAttention
- Feed-Forward Network — 各Attentionブロックの後に全結合層
- Language Model Head — 最後にlogitsを出力して次トークンを予測
class TinyLM(nn.Module):
def __init__(self, vocab_size, d_model=256, n_head=4, n_layer=4, max_len=128):
super().__init__()
self.emb = TokenPositionalEmbedding(vocab_size, d_model, max_len)
self.blocks = nn.Sequential(*[Block(d_model, n_head) for _ in range(n_layer)])
self.ln_f = nn.LayerNorm(d_model)
self.head = nn.Linear(d_model, vocab_size, bias=False)
パラメータ数は約524万。GPT-3の1750億、GPT-4の推定1兆超と比べると極小だが、仕組みは同じTransformerアーキテクチャ。
STEP 4:学習ループを回す
STEP 3で作った「空っぽの脳」に、データを使って知識を詰め込むステップ。具体的には、学習データを128トークンずつの断片に切り、モデルに入力して「次のトークン」を予測させる。予測と正解のズレ(Loss)を計算し、ズレが小さくなるようにパラメータを少しだけ調整する。これを32万サンプル分繰り返すのが1エポックで、さらにそれを10回繰り返す。
ここからGPUが必要になるので、ColabのランタイムをT4 GPUに変更して実行。
BATCH_SIZE = 128
EPOCHS = 10
LR = 3e-4
注意: ランタイムをGPUに切り替えると
/tmpの中身がリセットされる。STEP 1から全セルを再実行する必要がある。
学習結果
デバイス: cuda
語彙サイズ: 4000
データセットサイズ: 325,410 サンプル
バッチ数: 2,543
パラメータ数: 5,240,320
Epoch 01/10 | Loss: 3.8599
Epoch 02/10 | Loss: 1.4994
Epoch 03/10 | Loss: 0.7650
Epoch 04/10 | Loss: 0.5303
Epoch 05/10 | Loss: 0.4227
Epoch 06/10 | Loss: 0.3606
Epoch 07/10 | Loss: 0.3188
Epoch 08/10 | Loss: 0.2883
Epoch 09/10 | Loss: 0.2653
Epoch 10/10 | Loss: 0.2465
Lossはきれいに下がっている。Epoch 1では3.86(ほぼデタラメな予測)だったのが、Epoch 10では0.25(96%以上の精度で次のトークンを当てられる状態)まで到達。学習自体は問題なく成功している。
全体の所要時間は約80分(1エポックあたり約8分)。当初BATCH_SIZE=16で実行していたが、128に変更してもエポックあたりの時間はほぼ変わらなかった。無料枠のT4 GPUではこのあたりが限界のようだ。
STEP 5:テキスト生成の結果
学習済みモデルに3つのプロンプトを入力してみた。
プロンプト「吾輩は」
吾輩は御客様からあるか。――金田邸へ忍び込むのである。忍び込む度が重なるにつけ、探偵をする気はないが自然金田君一家の事情が見たくもない吾輩の眼に映じて覚えたくもない吾輩の脳裏に印象を留むるに至るのはやむを得ない。
プロンプト「親譲りの」
親譲りの無鉄砲で小供の時から損ばかりしている。小学校に居る時分学校の二階から飛び降りて一週間ほど腰を抜かした事がある。なぜそんな無闘をしたと聞く人があるかも知れぬ。
プロンプト「ある日の」
ある日の午後、山嵐が憤然とやって来て、いよいよ時機が来た、おれは例の計画を断行するつもりだと云うから、そうかそれじゃおれもやろうと、即座に一味徒党に加盟した。
一見すると流暢な日本語に見えるが、実はこれは原作の文章をほぼそのまま再現している。「親譲りの無鉄砲で小供の時から損ばかりしている」は坊っちゃんの冒頭そのものだし、ルビ(《むてっぽう》など)まで残っている。
分かったこと:過学習という罠
Lossが0.25まで下がったのは一見好ましいが、実態は**過学習(オーバーフィット)**だった。モデルが学習データを丸暗記してしまい、「自分で考えて文章を生成する」のではなく「覚えた文章を思い出して吐き出す」状態になっている。
人間に例えると、教科書を丸暗記したが応用問題は一切解けない状態。「今日はいい天気ですね」のような学習データにない入力を与えても、まともな続きは生成できない。
なぜ過学習するのか
- データが少なすぎる(49万文字)。暗記できてしまう量しかない
- モデルの表現力に対してデータが不足している
ChatGPTやClaudeが自然な応答をできるのは、数兆文字規模のデータで学習しているから。暗記しきれない量のデータを与えることで、初めて「言語のパターン」を一般化して学べるようになる。
なぜClaude・ChatGPT・Geminiは毎回違う答えを返すのか
同じ「こんにちは。今日の気温は?」と聞いても、サービスごとに、そして聞くたびに違う答えが返ってくる。これはなぜか。
実は今回のSTEP 5の生成コードにその仕組みがそのまま入っている。モデルが「次のトークン」を予測するとき、1つの正解を出すのではなく、全候補に確率を付ける。たとえば「こんにちは。今日の」の次は:
- 「天気」→ 35%
- 「気温」→ 20%
- 「予定」→ 12%
- 「調子」→ 8%
- …残り数千候補に少しずつ
ここからサイコロを振って1つ選ぶ。これがコード中の torch.multinomial の部分。毎回サイコロの結果が違うから、同じ入力でも違う文章になる。
temperature はサイコロの偏り具合を調整するパラメータ。低くすると高確率の候補ばかり選ばれ(堅い回答)、高くするとマイナーな候補も選ばれやすくなる(創造的だが不安定な回答)。top_k は上位k個の候補だけに絞り込む処理で、あまりに確率の低い候補を排除する役割を持つ。
Claude・ChatGPT・Geminiで回答が違うのは、この確率的なサンプリングに加えて、学習データ・モデルサイズ・学習方法が全部違うから。同じ入力でも、そもそも各候補に付く確率の分布自体が異なる。同じ料理名でもシェフが違えば味が変わるのと同じことだ。
LLMは20年前にも作れた?
今回の実験を通じて「次の単語を予測する」という仕組みの単純さに驚いた。理論的にはもっと昔に作れそうに思えるが、実際には以下の3つが揃って初めて実現可能になった。
ハードウェア(GPU)
今回の小さなモデルでも学習に80分かかった。ChatGPT級のモデルは数千〜数万個のGPUで数ヶ月回す。2000年代のコンピュータでは現実的に不可能な計算量。GPUを機械学習に使う流れ自体が2012年頃から始まった。
Transformerの発明(2017年)
今回実装したCausal Self-Attentionを含むTransformerアーキテクチャが発明されたのは2017年。それ以前の手法では、長い文章の「遠くの単語同士の関係」をうまく捉えられなかった。
スケール則の発見(2020年頃)
モデルとデータを大きくすればするほど賢くなるという経験則が確認されたのが2020年前後。「こんな単純な仕組みを巨大にするだけで知能っぽいものが出る」とは誰も予想していなかった。
材料(データ)は昔からあったが、調理器具(GPU)と調理法(Transformer)と、大きく作れば美味くなるという発見が揃ったのがここ数年、ということ。
今回の実験と「製品レベルのLLM」の距離
今回やったことはTransformerのモデル定義も学習ループも手書きしており、かなり「自作」に近い。ただし、ChatGPTやClaudeのような製品との間には大きな工程の差がある。
今回やったこと(事前学習の最小構成):
テキスト収集 → Tokenizer → モデル定義 → 学習 → 生成
製品レベルで追加される主な工程:
1. 大規模データの収集・前処理
今回は49万文字だが、実際はウェブ全体から数兆文字を集めて、重複除去、有害コンテンツの除外、品質フィルタリングなどを行う。この工程だけで専門チームが何ヶ月もかける。
2. 大規模な事前学習
今回は524万パラメータ・GPU1枚・80分だが、製品レベルでは数百億〜数兆パラメータを数千枚のGPUで数ヶ月回す。
3. RLHF(人間のフィードバックによる強化学習)
今回のモデルは「次の文字を当てる」訓練しかしていない。これだけでは質問に答えたり指示に従ったりはできない。事前学習の後に、人間が「この回答は良い」「これはダメ」と評価したデータを使って、対話能力や安全性を追加学習させる。ChatGPTが「会話できる」のはこの工程があるから。
4. 安全性対策・推論最適化・サービス化
有害な出力をしないためのフィルタリング、モデルの高速化(量子化など)、API整備。
今回の実験はこのうち1と2を最小規模でやった形になる。一番大きな差は3のRLHFで、これがないと「文章の続きを書く」ことはできても「会話する」ことはできない。なお、RLHFの簡易版(SFT:教師ありファインチューニング)であれば仕組み的にはSTEP 4と同じ方法で無料Colabでも動かせるが、今回のモデルは小さすぎて「質問と回答のペアを丸暗記する」だけで終わる可能性が高い。過学習と同じ壁がここにもある。
まとめ
Google Colabの無料枠だけで、LLMの基本的な仕組みを一通り体験できた。
- データ準備 → 青空文庫から49万文字を取得
- Tokenizer → SentencePieceで4000語彙のBPEモデルを学習
- モデル実装 → Transformer(524万パラメータ)をPyTorchで実装
- 学習 → T4 GPUで約80分、Lossは3.86→0.25に低下
- 生成 → 原作を再現する文章を生成(=過学習)
今回のモデルは「漱石・芥川の文章を暗記した極小モデル」であり、汎用的な言語能力は持っていない。しかし、ChatGPTとの違いはデータ量とモデルサイズだけで、アーキテクチャは本質的に同じ。「次の単語を予測する」というシンプルな仕組みの先にあの性能がある、ということを実感できたのが一番の収穫だった。


