目次
********
※LINE対応チャットボット版の
「LINEチャットボット屋」
いろんなチャットボットがあります。
ぜひ、ご覧ください!
***************
***************
nanoGPT 小さなLLMに挑戦した理由
近年、大規模言語モデル(LLM)の進化は目覚ましく、ChatGPTをはじめとする高性能なモデルが次々と登場しています。しかし一方で、「自分の手でLLMを育ててみたい」「中身を理解させながら学びたい」と思うかたも多いでしょう。
そこで選んだのが、比較的小規模で学習しやすいnanoGPTです。
nanoGPTとは
nanoGPT(ナノジーピーティー)とは、OpenAIのGPT系モデルの仕組みをベースにした、シンプルで小規模な言語モデル実装です。(進化版でnanoChatと言うものもあります)
もともとは、Andrej Karpathy氏によって公開された教育・研究目的のプロジェクトで、「最小構成でGPTを再現する」ことを目標に作られています。
最大の特徴は、そのシンプルさと分かりやすさにあります。
実装は数百行程度のPythonコードにまとめられており、GPTのコアとなるTransformer構造や自己注意機構が、非常に読みやすい形で書かれています。
そのため、LLMを「使う」だけでなく、「中身を理解したい」「自分で動かしてみたい」という人にとって、最高の教材になっています。
また、nanoGPTは小さなモデルサイズでも動かせるように設計されているのも大きな魅力です。
数百万〜数千万パラメータ規模のモデルであれば、個人のPCやGoogle Colabといった環境でも学習実験が可能で、「巨大なGPU環境がないと無理」というLLMの敷居を大きく下げてくれます。
本来のGPTは数百億〜数千億パラメータという桁違いの規模ですが、nanoGPTではそのミニチュア版を自分の手で動かすことができます。
これにより、
・データを入れるとどう学習が進むのか
・損失が下がるとはどういうことか
・モデルのサイズや設定で何が変わるのか
といったことを、実体験として学べます。
さらに、nanoGPTはカスタマイズの自由度も高く、データセットやトークナイザ、モデルサイズ、学習ステップ数などを自分で調整しながら、さまざまな実験ができます。
今回のように、日本語テキストを使って独自の小さなLLMを育てるといった使い方も十分可能なのです。

「事前学習」とは?ゼロから言葉を覚えさせるということ
LLM開発の最初のステップが「事前学習(Pre-training)」です。
これは、人間で言えば、まだ何も知らない人に大量の文章を読ませて、言葉や文のつながりを自然に覚えさせる段階にあたります。
質問に答えさせたり、指示に従わせたりする前に、とにかく膨大なテキストを与えて、「次に来そうな単語」を予測する訓練を繰り返します。
今回のnanoGPTの事前学習では、意味を理解させるというよりも、「日本語らしい文字の並び」や「文のリズム」を体に染み込ませることが目的でした。
ここが、その後のファインチューニングの土台になります。
学習データと準備 どんなコーパスをどう集めたか
コーパスとは、AIに言葉を覚えさせるために集めた大量の文章データのこと。
事前学習で最も重要なのがこのデータです。
今回は、日本語テキストとして定番の青空文庫を中心に使いました。
著作権が切れた文学作品が多く、文章量も十分で、日本語の文体を学ばせるには最適です。
取得したテキストは、そのままでは使えないため、不要な記号の削除や文字コードの統一など、簡単な前処理を行いました。
さらに、SentencePieceを使ってサブワード分割し、モデルが扱いやすい形に変換します。
この「データを整える作業」は地味ですが、学習の質を左右する重要な工程で、LLMづくりの裏側を強く実感する部分ですね。
実際にやってみた nanoGPT事前学習の手順と環境
学習環境はGoogle Colabを利用しました。GPUが手軽に使えるのは大きな利点です。
手順としては、
1. リポジトリのセットアップ
2. データの配置とトークナイザ作成
3. モデルサイズやブロック長などの設定
4. 学習スクリプトの実行
という流れです。
ここで重きを置いたのは、事前学習を最後までというよりも、まず実際に体感することだと思ったので、わずか5分で終わる事前学習に設定しました。
モデルは約200万パラメータ程度の小さな構成にし、まずは「最後まで回る」ことを重視しました。
学習ログを眺めながら、損失が少しずつ下がっていくのを見ると、ひとまず安心感。
数分後、ついに事前学習が完了しました。
ここで5分で終了していますが、実際に時間があれば、これを数時間なり数日なりの時間をかけてやると更なる学習の進度が期待できます。

学習が終わって見えたこと モデルは何を覚えたか
学習後、実際に文章を生成させてみると、日本語らしい文字列は出てくるものの、意味はほとんど通っていません。5分しか学習していないので当然です。
漢字とひらがなが混ざった、それっぽい文章が延々と続くだけです。
事前学習の段階では、「日本語の形」を覚えただけで、「質問に答える」能力はまだ無いのです。(おまけにたったの5分)
それでも、最初はランダムだった出力が、だんだんと日本語らしくなっていく過程を見られたのは大きな成果でした。
モデルが言語の空気を掴み始めた瞬間を、自分の環境で体験できたのは、何にも代えがたい経験です。
ファインチューニングと人格づくりへ
事前学習を終えたnanoGPTは、いわば「日本語の音や形を覚えた状態」です。しかし、まだ質問に正しく答えたり、会話らしく受け答えしたりすることはできません。
ここからが本当の意味での育成のスタートだと感じています。
次に取り組みたいのが、SFT(指示追従ファインチューニング)です。
これは、「質問」と「望ましい答え」のペアを大量に与えて、モデルに「こう聞かれたら、こう答える」という振る舞いを教える学習です。
これによって、ある程度事前学習をこなしたマシンなら、少しずつ会話ができるAIに近づいていきます。
さらに、その先にはDPOのような手法を使った「らしさ」の調整があります。
複数の答えの中から、より好ましいものを選ばせることで、丁寧な口調、分かりやすい説明、あるいは関西弁のようなキャラクター性など、「人格」と呼べる部分を形づくっていきたいと考えています。
最終的な目標は、ただ正しい答えを返すだけのAIではなく、「このAIと話したい」と思えるような、自分好みの相棒のような存在を育てることです。
小さなモデルだからこそ、試行錯誤しながら何度も作り直し、少しずつ成長させていけるのも大きな魅力です。
事前学習で作った土台の上に、ファインチューニング。
これらの実験がLLMへの理解を深めてくれるはずです。

まとめ nanoGPT事前学習は最高の教材
今回、nanoGPTを使って事前学習を一通り回してみて感じたのは、「これは最高の教材」ということです。
巨大モデルをAPIで使うだけでは見えない、データ準備の大変さ、学習が進む感覚、そして事前学習の限界。そのすべてを自分の手で実装する。
意味の通る文章を話すAIにはまだ遠いですが、「ゼロから言語モデルを育てる」という経験は、LLMを理解する上で何よりの経験かと思います。
この土台をさらに進化させて、本当に会話できるモデルへと実現させていく。
これこそが知的好奇心を満たす最高の実験ですね。
使用したPythonコード
下記に使用したPythonコードを記します。
google colabにて実装。
5分程度で終わるように作ってあるので、お手軽に実験できるかと思います。
時間のある方はいろいろいじって、数時間、数日間と学習させてもよろしいかと。
※下記コードはインテントがくずれている場合ありです。
くずれていると動きません。
コピーして、GPT5先生に修正を投げれば直してもらえます。
ご了承のほど。
# Commented out IPython magic to ensure Python compatibility.
# -*- coding: utf-8 -*-
# @title nanoGPT 5分体験(青空文庫 BPE版)
import os, re, zipfile, urllib.request, pathlib
# 1. GPUの確認
print("=== 1. GPU環境のチェック ===")
gpu_info = os.popen('nvidia-smi').read()
if 'failed' in gpu_info:
print("⚠️ GPUが見つかりません。Colabのメニュー「ランタイム」>「ランタイムのタイプを変更」でT4 GPUなどを選択してください。")
else:
print(gpu_info)
print("✅ GPU確認OK")
# 2. nanoGPT
print("\n=== 2. nanoGPT をダウンロード中... ===")
if not os.path.exists('nanoGPT'):
!git clone https://github.com/karpathy/nanoGPT.git
# %cd nanoGPT
print("✅ nanoGPT OK")
# 3. 依存
print("\n=== 3. 必要なツールをインストール中... ===")
!pip -q install torch numpy transformers datasets tiktoken sentencepiece
print("✅ インストール完了")
# 4. 青空文庫データ準備(BPE)
print("\n=== 4. 学習用データ(青空文庫)を準備中... ===")
out_dir = pathlib.Path("data/aozora_bpe")
out_dir.mkdir(parents=True, exist_ok=True)
tmp_dir = pathlib.Path("data/_aozora_tmp")
tmp_dir.mkdir(parents=True, exist_ok=True)
# 作品ZIP(必要ならここ増やしてOK。増やすほど“青空っぽさ”が出る)
works = [
("kokoro", "https://www.aozora.gr.jp/cards/000148/files/773_ruby_5968.zip"),
("merosu", "https://www.aozora.gr.jp/cards/000035/files/1567_ruby_4948.zip"),
("rashomon", "https://www.aozora.gr.jp/cards/000879/files/127_ruby_150.zip"),
]
def clean_aozora_text(text: str) -> str:
# 5分体験用:最低限のゴミ取り(ルビ・注記など)
text = re.sub(r"《.*?》", "", text) # ルビ
text = re.sub(r"[#.*?]", "", text) # 注記
text = re.sub(r"|", "", text) # ルビ補助
text = re.sub(r"[ ]+\n", "\n", text) # 行末空白
text = re.sub(r"\n{3,}", "\n\n", text) # 改行詰め
return text.strip()
all_texts = []
for name, url in works:
zip_path = tmp_dir / f"{name}.zip"
if not zip_path.exists():
print(" downloading:", url)
urllib.request.urlretrieve(url, zip_path)
with zipfile.ZipFile(zip_path, "r") as z:
txt_names = [n for n in z.namelist() if n.lower().endswith(".txt")]
if not txt_names:
raise RuntimeError(f"txt not found in {zip_path}")
member = txt_names[0]
raw = z.read(member)
# 青空は Shift_JIS 多め
try:
s = raw.decode("shift_jis")
except UnicodeDecodeError:
s = raw.decode("utf-8", errors="ignore")
s = clean_aozora_text(s)
all_texts.append(s)
data = "\n\n".join(all_texts)
(out_dir / "input.txt").write_text(data, encoding="utf-8")
print(f"✅ input.txt 作成: {out_dir/'input.txt'} 文字数={len(data):,}")
# BPE tokenizer(SentencePiece)を学習
# vocab_sizeは 4000〜8000 が無難(uint16の上限もあるので 65535以下)
vocab_size = 8000
spm_prefix = str(out_dir / "spm")
spm_model = out_dir / "spm.model"
spm_vocab = out_dir / "spm.vocab"
if not spm_model.exists():
import sentencepiece as spm
print("\n--- SentencePiece BPE tokenizer を学習中 ---")
spm.SentencePieceTrainer.train(
input=str(out_dir / "input.txt"),
model_prefix=spm_prefix,
vocab_size=vocab_size,
model_type="bpe",
character_coverage=0.9995, # 日本語寄り
bos_id=1, eos_id=2, unk_id=0, pad_id=3
)
print("✅ tokenizer 作成:", spm_model)
# tokenizerでID化 → train.bin/val.bin 作成
import numpy as np
import sentencepiece as spm
sp = spm.SentencePieceProcessor()
sp.load(str(spm_model))
ids = sp.encode(data, out_type=int)
print("✅ トークン数:", len(ids))
n = len(ids)
split = int(n * 0.9)
train_ids = np.array(ids[:split], dtype=np.uint16)
val_ids = np.array(ids[split:], dtype=np.uint16)
train_ids.tofile(out_dir / "train.bin")
val_ids.tofile(out_dir / "val.bin")
# meta.pkl(train.py が vocab_size を知るために必要)
import pickle
meta = {
"vocab_size": int(sp.get_piece_size()),
"sp_model_path": str(spm_model),
}
with open(out_dir / "meta.pkl", "wb") as f:
pickle.dump(meta, f)
print("✅ saved:", out_dir/"train.bin", out_dir/"val.bin", out_dir/"meta.pkl")
print("vocab_size:", meta["vocab_size"])
# Commented out IPython magic to ensure Python compatibility.
# 5. 事前学習(BPE版 / 5分体験)
# %cd /content/nanoGPT/nanoGPT
!python train.py config/train_gpt2.py \
--dataset=aozora_bpe \
--out_dir=out-aozora-bpe \
--device=cuda \
--compile=False \
--eval_interval=50 \
--eval_iters=5 \
--log_interval=10 \
--max_iters=120 \
--lr_decay_iters=120 \
--block_size=128 \
--batch_size=16 \
--n_layer=4 \
--n_head=4 \
--n_embd=128 \
--dropout=0.0 \
--wandb_log=False
# @title 推論(青空BPE / SentencePiece)
import torch
import sentencepiece as spm
from model import GPTConfig, GPT
# ===== 設定 =====
ckpt_path = "/content/nanoGPT/nanoGPT/out-aozora-bpe/ckpt.pt"
spm_model_path = "data/aozora_bpe/spm.model"
device = "cuda" if torch.cuda.is_available() else "cpu"
# ===== tokenizer =====
sp = spm.SentencePieceProcessor()
sp.load(spm_model_path)
# ===== checkpoint =====
checkpoint = torch.load(ckpt_path, map_location=device)
config = GPTConfig(**checkpoint["model_args"])
model = GPT(config)
model.load_state_dict(checkpoint["model"])
model.to(device)
model.eval()
# ===== prompt =====
prompt = "吾輩は猫である。"
ids = sp.encode(prompt)
x = torch.tensor([ids], dtype=torch.long).to(device)
# ===== generate =====
with torch.no_grad():
y = model.generate(
x,
max_new_tokens=120,
temperature=0.8,
top_k=50,
)
text = sp.decode(y[0].tolist())
print("-----")
print(text)
!pwd
!ls -lah /content/nanoGPT/out-aozora-bpe
!ls -lah /content/nanoGPT/nanoGPT/out-aozora-bpe
!find /content/nanoGPT -maxdepth 3 -name ckpt.pt -o -name best.pt
# @title ckpt.pt をローカルにダウンロードする(Colab用)
from google.colab import files
import shutil
import os
# ckpt の場所(今回確定している正しいパス)
ckpt_path = "/content/nanoGPT/nanoGPT/out-aozora-bpe/ckpt.pt"
# 念のため存在チェック
assert os.path.exists(ckpt_path), "❌ ckpt.pt が見つかりません"
# ダウンロードしやすい場所にコピー(名前も分かりやすく)
dst = "/content/aozora_bpe_ckpt.pt"
shutil.copy(ckpt_path, dst)
print("✅ 準備完了。ダウンロードを開始します")
# ダウンロード
files.download(dst)
****************
最近のデジタルアート作品を掲載!
X 旧ツイッターもやってます。
https://x.com/ison1232
インスタグラムはこちら
https://www.instagram.com/nanahati555/
***************

