ゆっくりしていってね!
第0章 はじめに — この本の使い方
0.1 登場人物と役割
霊夢
ねえ魔理沙、この本って何? 表紙に Hugging Face って書いてあるけど、顔がハグしてるロゴみたいなやつでしょ?
魔理沙
その通りだZE。Hugging Face(通称 HF)は、機械学習のモデルやデータセットを共有するプラットフォームだ。GitHub がコード置き場なら、HF は「学習済み AI の置き場」みたいなイメージで覚えるといい。
霊夢
GitHub は知ってる。git push したら怒られるやつ。
魔理沙
本書は ゆっくり霊夢 と ゆっくり魔理沙 の会話形式だ。役割はこう決めてある。
| 役割 | 担当 | やること |
|---|---|---|
| 一緒に学ぶ人 | 霊夢 | 素朴な疑問、つまずき、実行結果のリアクション |
| ガイド | 魔理沙 | 概念の説明、コマンド提示、「だからこう書くのだZE」 |
| 読者(あなた) | — | コードを 自分の環境で実行 する |
霊夢
…あ、間違えた。私のことね。
つまり私が「わからない」を代弁して、魔理沙が答えるってことね。読者は黙って手を動かすだけ?
魔理沙
黙ってるだけだと身につかない。読む → 実行 → 少し改造 → 振り返り、の4ステップを毎回やるのが本書のルールだZE。第0章の最後で環境チェックもやるから、ここで一度ターミナルを開いておいてくれ。
魔理沙
あわせて、各章末尾の 「本章スクリプト全文」 に、リポジトリの scripts/ と同じコードを載せてある。GitHub や clone なしで読んでいる人も、そこからファイルを作って実行できるのだ。
0.2 本書で触る Hugging Face の全体像
霊夢
HF って、サイトだけ? それとも Python のライブラリもあるの?
魔理沙
両方だ。ざっくり5つ覚えれば十分だZE。
┌─────────────────────────────────────────────────────────┐
│ Hugging Face Hub(Web) │
│ モデル・データセット・デモアプリ(Spaces)の公開場所 │
└──────────────────────────┬──────────────────────────────┘
│ download / upload
┌──────────────────────────▼──────────────────────────────┐
│ Python ライブラリ群 │
│ ├─ transformers … モデル読込・推論・学習 │
│ ├─ datasets … データセットの読込・前処理 │
│ ├─ accelerate … マルチGPU・効率化(発展) │
│ ├─ peft … LoRA など軽量 FT(第7章) │
│ └─ gradio … Web UI デモ(第9章) │
└─────────────────────────────────────────────────────────┘
霊夢
なんか配線図みたい…
魔理沙
本書の章立てはこの流れだ。
| 章 | テーマ | 触るもの |
|---|---|---|
| 1 | Hub を覗く | ブラウザ、huggingface-cli |
| 2 | すぐ動かす | pipeline |
| 3〜4 | 中身 | Tokenizer、Model |
| 5 | データ | datasets |
| 6〜7 | 学習 | Trainer、LoRA |
| 8〜9 | 運用・共有 | 推論最適化、Gradio、Spaces |
| 10 | 総合 | ゆっくり実況コメント Bot(予定) |
霊夢
第2章でいきなり3行で動くって目次にあったけど、本当?
魔理沙
本当だZE。ただしその前に 第0章で環境を整える。いきなり第2章から始める読者もいるが、エラーが出たとき戻ってこれるようにしてある。
0.3 必要な環境
霊夢
必要なもの、箇条書きで。お金かかる?
魔理沙
基本は無料でいける。リストはこうだ。
| 項目 | 必須? | メモ |
|---|---|---|
| Python 3.10 以上 | 必須 | 3.11 推奨 |
| インターネット | 必須 | 初回はモデル DL で数 GB になることも |
| GPU(NVIDIA) | 任意 | なくても CPU / Colab で学べる |
| Hugging Face アカウント | 推奨 | 第1章で作成。DL だけなら後回し可 |
| Google Colab | 任意 | ローカルに GPU がない人向け |
0.3.1 Python のバージョン確認
魔理沙
まずターミナルで Python を確認するのだ。
python3 --version
期待する出力の例:
Python 3.11.8
霊夢
3.9.6 とか出たら?
魔理沙
本書は 3.10 未満は非推奨 だZE。pyenv や公式インストーラで上げてから続けてくれ。
# pyenv を使っている場合の例 pyenv install 3.11.8 pyenv local 3.11.8 python --version
0.3.2 仮想環境(venv)の作成
魔理沙
グローバルに pip install すると他プロジェクトと喧嘩する。必ず venv を使うのだ。
# リポジトリのルートで(本書のサンプル用) cd /path/to/yukkuri-hugging-face python3 -m venv .venv
有効化:
# macOS / Linux source .venv/bin/activate # Windows (PowerShell) # .venv\Scripts\Activate.ps1
有効化できているかはプロンプト先頭に (.venv) が付くか、次で確認できる。
which python # 例: /path/to/yukkuri-hugging-face/.venv/bin/python
0.3.3 本書で使うパッケージの一括インストール
魔理沙
第0章では最小限。章が進むたびに足していくが、先にまとめて入れておいても問題ない。
pip install --upgrade pip
pip install \ "transformers>=4.40" \ "datasets>=2.18" \ "accelerate>=0.28" \ "huggingface_hub>=0.22" \ "torch" \ "sentencepiece" \ "protobuf"
霊夢
torch ってデカくない?
魔理沙
デカいZE。CPU 版だけでよければ、公式の案内に従って CPU 用 wheel を選んでもいい。GPU がある人は PyTorch の Get Started で CUDA 版を入れる。
CUDA 12.x 環境の一例(環境に合わせて URL は変わる):
pip install torch --index-url https://download.pytorch.org/whl/cu124
0.3.4 インストール確認スクリプト
魔理沙
次の内容を scripts/check_env.py として保存し、実行してみるのだ。
(同じ内容は本章末尾の 「本章スクリプト全文」 にも載せてある。)
# scripts/check_env.py import sys def main() -> None: print("Python:", sys.version.replace("\n", " ")) import torch print("torch:", torch.__version__) print("CUDA available:", torch.cuda.is_available()) if torch.cuda.is_available(): print("CUDA device:", torch.cuda.get_device_name(0)) import transformers import datasets import huggingface_hub print("transformers:", transformers.__version__) print("datasets:", datasets.__version__) print("huggingface_hub:", huggingface_hub.__version__) print("\nOK: 第0章の環境チェック完了") if __name__ == "__main__": main()
実行:
python scripts/check_env.py
出力例(GPU なし):
Python: 3.11.8 (main, ...) torch: 2.2.2 CUDA available: False transformers: 4.41.2 datasets: 2.19.1 huggingface_hub: 0.23.2 OK: 第0章の環境チェック完了
霊夢
CUDA available: False って出た。もうダメなの?
魔理沙
ダメじゃない。CPU でも第2章の小さいモデルは動くZE。重い学習は Colab や後の章で LoRA を使えばいい。
0.3.5 Google Colab を使う場合
霊夢
ローカルに GPU ない人向け、って言ってたよね。
魔理沙
Colab ならノートブックの先頭セルでほぼ同じ確認ができる。
# Colab の最初のセル例 !pip install -q "transformers>=4.40" "datasets>=2.18" accelerate import torch print(torch.__version__, "CUDA:", torch.cuda.is_available())
無料枠では GPU が使えない時間帯もある。本書では 「Colab で GPU が取れた日は第6章以降を進める」 くらいのペースで十分だ。
0.4 ハンズオンの進め方
霊夢
毎回「読む・実行・改造・振り返り」って言ってたけど、具体的に?
魔理沙
テンプレは固定だZE。
ステップ1: 読む
会話は飛ばさず、コードブロックだけ先に眺める。
「何を import して」「何を呼んでいるか」をメモする。
ステップ2: 実行
そのままコピペせず、手で打つか、一行ずつ貼る。
エラーが出たら、メッセージ全文を残す。
# 実行例のログをファイルに残す習慣(任意) python scripts/check_env.py 2>&1 | tee log/ch00_env.txt
ステップ3: 改造
本書の「改造お題」に挑む。第0章のお題は後述。
ステップ4: 振り返り
章末の 霊夢のメモ帳 と 魔理沙の one more thing を読み、自分の言葉で3行まとめる。
霊夢
改造お題、早く言って。
魔理沙
第0章の ハンズオン 0 だ。
ハンズオン 0 — 環境チェック + 一行推論
お題A check_env.py に platform と空きメモリ(psutil があれば)を表示する行を足す。
pip install psutil
# check_env.py に追加する例 import platform import psutil print("Platform:", platform.platform()) print("RAM (GB):", round(psutil.virtual_memory().total / 1e9, 1))
お題B 最小の pipeline で Hub からモデルを1回だけ動かす(初回は DL に時間がかかる)。
from transformers import pipeline # 軽めの英語感情分析(初回ダウンロードあり) clf = pipeline("sentiment-analysis", model="distilbert-base-uncased-finetuned-sst-2-english") print(clf("I love Hugging Face!"))
期待する出力の例:
[{'label': 'POSITIVE', 'score': 0.9998}]
霊夢
お題B、第2章の内容じゃない?
魔理沙
先取りだZE。「環境が動く」証明に一行で十分。詳しくは第2章でやる。
お題C キャッシュの場所を確認する(ディスク圧迫の予習)。
import os from huggingface_hub import constants print(constants.HF_HOME) # または print(os.environ.get("HF_HOME", "(default ~/.cache/huggingface)"))
0.5 用語ミニ辞典
霊夢
辞書…暗記するの?
魔理沙
暗記不要。ここでは Hub でモデルページを読むとき に出てくる語だけ押さえる。
| 用語 | 一言で |
|---|---|
| モデル(Model) | 入力から出力を計算する学習済みニューラルネット。重みファイルを含む |
| トークナイザ(Tokenizer) | 文字列をモデル用の ID 列に変換する器。モデルとセットで使う |
| 推論(Inference) | 学習済みモデルにデータを入れて予測を得ること。本書では pipeline や model.generate |
| ファインチューニング(FT) | 既存モデルを、自分のデータで追加学習すること |
| Hub | モデル・データセットを公開・検索する Web サービス |
| Model Card | モデルの説明・学習データ・限界・ライセンスが書かれた README |
| Checkpoint | 学習途中または完了時の重みの保存ファイル |
| Token | トークナイザが切った単位。単語の断片や記号のこともある |
| Pipeline | 前処理〜推論〜後処理をまとめた高レベル API |
霊夢
Model と Checkpoint の違いがまだふわふわ。
魔理沙
例えるなら、モデル=ゲームソフト本体、チェックポイント=セーブデータ だZE。同じアーキテクチャ(本体)に、別の重み(セーブ)を載せ替えるイメージ。
コードで見ると、こういう関係だ。
from transformers import AutoModel, AutoTokenizer model_id = "distilbert-base-uncased-finetuned-sst-2-english" tokenizer = AutoTokenizer.from_pretrained(model_id) model = AutoModel.from_pretrained(model_id) # tokenizer … 文字 ↔ ID # model … ID 列 → 内部表現(章3以降で詳しく)
0.6 リポジトリの構成(本書付属)
魔理沙
これから章ごとにスクリプトが増える。目安のディレクトリ構成だ。
yukkuri-hugging-face/ ├── docs/ # 本書の Markdown(このファイルは docs/00.md) ├── scripts/ # 実行用 Python ├── notebooks/ # Colab 用(任意) ├── log/ # 実行ログ(git 管理外推奨) └── .venv/ # 仮想環境(git 管理外)
.gitignore の例:
.venv/ __pycache__/ *.pyc .cache/ log/ .env
霊夢
.env って HF のトークン入れるやつ?
魔理沙
そのうち使うZE。第1章でアカウントとトークンを作る。第0章では まだ必須じゃない。お題Bが動けば十分だ。
本章スクリプト全文
魔理沙
リポジトリがなくても手を動かせるよう、本章の 完成スクリプト をそのまま載せるのだ。
手元では scripts/ 以下と同じファイル名で保存して実行すればよいZE。
(python scripts/xxx.py と書いてある箇所は、保存先に合わせてパスを読み替えてくれ。)
scripts/check_env.py
import sys def main() -> None: print("Python:", sys.version.replace("\n", " ")) import torch print("torch:", torch.__version__) print("CUDA available:", torch.cuda.is_available()) if torch.cuda.is_available(): print("CUDA device:", torch.cuda.get_device_name(0)) import transformers import datasets import huggingface_hub print("transformers:", transformers.__version__) print("datasets:", datasets.__version__) print("huggingface_hub:", huggingface_hub.__version__) print("\nOK: 第0章の環境チェック完了") if __name__ == "__main__": main()
霊夢のメモ帳
- HF は Hub(共有) + Python ライブラリ(実行) のセット。
- 学習は venv で隔離し、
check_env.pyでバージョンと CUDA を確認する。 - ハンズオンは 読む → 実行 → 改造 → 振り返り。お題Bの一行
pipelineで「動いた」証明を取る。
魔理沙の one more thing
環境変数でキャッシュ場所やオフライン動作を制御できる。ディスクが小さい人は早めに知っておくと便利だZE。
# キャッシュを D ドライブに逃がす例(bash) export HF_HOME="/path/to/large-disk/huggingface" export TRANSFORMERS_CACHE="$HF_HOME/hub"
# オフラインのみで動かす(既に DL 済みのとき) import os os.environ["HF_HUB_OFFLINE"] = "1"
次章へ
魔理沙
環境が整ったら、第1章で Hub にアカウントを作り、モデルカードの読み方と huggingface-cli login に進むのだ。
霊夢
…ログイン、トークン漏らさないようにするね。
魔理沙
その意識、大事だZE。では次回、第1章 Hugging Face Hub を覗いてみよう だ。
第1章 Hugging Face Hub を覗いてみよう
1.1 Hub って何? なぜみんな使うの?
霊夢
第0章で「GitHub みたいなやつ」って言ってたけど、Hub って具体的に何ができるの?
魔理沙
Hugging Face Hub は、学習済みモデル・データセット・デモアプリ(Spaces)を公開・検索・ダウンロードする Web サービスだZE。URL は https://huggingface.co だ。
霊夢
みんなが使う理由は?
魔理沙
ざっくり4つだ。
| 理由 | 説明 |
|---|---|
| 探しやすい | タスク(翻訳・分類など)や言語でフィルタ検索できる |
| 再現しやすい | モデルカードに学習条件・限界が書いてある |
| CLI / Python から DL | ブラウザだけでなく huggingface-cli や from_pretrained で取得 |
| 共有文化 | 論文実装やコミュニティモデルが集まる「共通の置き場」 |
全体像は第0章の図のとおり。今章は Web の上側(Hub) に集中するのだ。
┌──────────────────────────────────────────────────────────┐
│ huggingface.co(Hub) │
│ ├─ Models … 重み + 設定 + README(Model Card) │
│ ├─ Datasets … 学習・評価用データ │
│ └─ Spaces … Gradio などのデモ(第9章) │
└───────────────────────────┬──────────────────────────────┘
│ hf download / snapshot_download
┌───────────────────────────▼──────────────────────────────┐
│ ローカルキャッシュ(~/.cache/huggingface など) │
└──────────────────────────────────────────────────────────┘
霊夢
第0章のお題B、pipeline で勝手に DL してたのも Hub 経由?
魔理沙
その通りだZE。model="distilbert-base-uncased-finetuned-sst-2-english" と書くだけで、Hub から取得してキャッシュに置く。第1章では 意図的に Hub を覗いてから DL する 習慣を身につける。
1.2 アカウント作成とトークン発行
霊夢
…ログイン、トークン漏らさないようにするね、って第0章で言ったやつ。具体的にどう作るの?
魔理沙
手順は次のとおりだ。ブラウザで Hub にサインアップする。
- https://huggingface.co/join でアカウント作成(メール or GitHub / Google 連携)
- 右上アイコン → Settings → Access Tokens
- New token を作成。用途に応じて権限を選ぶ
| トークン種別 | 用途 |
|---|---|
| Read | 公開モデルの DL、 gated モデルの利用申請後 DL |
| Write | 自分のリポジトリへ push(第6章以降) |
霊夢
トークン、メモ帳にコピペしちゃダメ?
魔理沙
ダメに近いZE。GitHub に push したりスクショしたりすると漏れる。.env や OS の資格情報ストア に置き、コードには直書きしない。
.env の例(リポジトリルート。第0章の .gitignore に .env がある):
# .gitignore に必ず含める .env
# .env(例・実際の値は各自が Settings で発行) HF_TOKEN=hf_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Python から読むなら(任意):
pip install python-dotenv
from dotenv import load_dotenv import os load_dotenv() token = os.environ.get("HF_TOKEN") # None なら未設定
CLI でログインする
魔理沙
第0章で huggingface_hub は入れた。huggingface-cli login でトークンをローカルに保存するのだ。
cd /path/to/yukkuri-hugging-face source .venv/bin/activate huggingface-cli login
プロンプトが出たら、Settings で発行した Read トークン を貼る(入力中は画面に表示されない)。
期待する流れ:
_| _| _| _| _|_|_| _|_|_| _|_|_| _| _| _|_|_| _|_|_|_| _|_| _|_|_| _|_|_|_|
_| _| _| _| _| _| _| _|_| _| _| _| _| _| _| _|
_|_|_|_| _| _| _| _|_| _| _|_| _| _| _| _| _| _|_| _|_|_| _|_|_|_| _| _|_|_|
_| _| _| _| _| _| _| _| _| _| _|_| _| _| _| _| _| _| _|
_| _| _|_| _|_|_| _|_|_| _|_|_| _| _| _|_|_| _| _| _| _|_|_| _|_|_|_|
A token is already saved on your machine. Run `huggingface-cli whoami` to get more information or `huggingface-cli logout` if you want to log out.
Enter your token (input will not be visible):
Add token as git credential? (Y/n) n
Token is valid (permission: read).
Your token has been saved to /Users/you/.cache/huggingface/token
Login successful
霊夢
whoami で確認できる?
魔理沙
できるZE。
huggingface-cli whoami
user_name
ログアウトするとき:
huggingface-cli logout
霊夢
DL だけなら、ログインしなくても動いたよね?
魔理沙
公開モデル は未ログインでも DL できることが多い。ただし gated model(利用規約への同意が必要なモデル)や プライベートリポジトリ には Read トークンが要る。早めに login しておくと後が楽だ。
1.3 モデルカード・データセットカードの読み方
霊夢
モデルページ、ファイルだらけで圧倒される…
魔理沙
まず README(Model Card) を上から読むのが正解だZE。例として本書でもよく使う感情分析モデルを見る。
- モデル ID:
distilbert-base-uncased-finetuned-sst-2-english - URL: https://huggingface.co/distilbert-base-uncased/distilbert-base-uncased-finetuned-sst-2-english
Model Card で押さえる項目:
| セクション | 見る内容 |
|---|---|
| Model description | 何のタスク用か、ベースモデルは何か |
| Intended uses | 想定ユースケースと 使うべきでない場面 |
| Training data | 何で学習したか(バイアスの手がかり) |
| Evaluation | ベンチマークスコア |
| Limitations | 弱点・言語・ドメインの制約 |
| How to use | pipeline や AutoModel のサンプルコード |
Files and versions タブでは実ファイルを確認する。
| ファイル | 役割 |
|---|---|
config.json |
アーキテクチャ・語彙サイズなどの設定 |
tokenizer.json / vocab.txt |
トークナイザ定義 |
model.safetensors または pytorch_model.bin |
学習済み重み |
README.md |
Model Card 本文 |
霊夢
データセットも同じ?
魔理沙
Dataset Card も README が中心だ。例: imdb 映画レビュー感情データ。
Dataset Card では データの出所・ライセンス・個人情報の有無 を必ず見る。第5章で load_dataset("imdb") するとき、このカードを読んでいる状態になる。
1.4 ライセンスと利用条件を確認する習慣
霊夢
「とりあえず DL」って商用 OK とは限らないんでしょ?
魔理沙
その通りだZE。Hub 上の License バッジと README 内の 利用条件 をセットで確認する習慣をつける。
| ライセンス例 | ざっくり意味 |
|---|---|
| apache-2.0 | 商用利用しやすいオープンソース系 |
| mit | 条件が緩め(著作権表示など) |
| cc-by-4.0 | クレジット表示が必要なことが多い |
| cc-by-nc-4.0 | 非商用(NC) — 商用プロダクトでは要注意 |
| other / custom | README を全文読む。gated の場合は同意画面あり |
gated model は Hub 上で 利用規約に同意 しないと重みが DL できない。ログイン済みトークンが必要だ。
# gated モデルで 403 が出たときの典型 # → ブラウザでモデルページを開き、Agree and access をクリック huggingface-cli login
霊夢
チェックリストみたいなの欲しい。
魔理沙
DL 前にこれだけは見るのだ。
- License バッジは商用・再配布 OK か?
- Intended uses / Limitations に自分の用途が含まれるか?
- gated なら同意済みか?
- データセットなら 個人データ・偏り の記載は?
ハンズオン 1 — Hub を触って DL まで
魔理沙
第0章の「読む → 実行 → 改造 → 振り返り」に沿って、3 題やるZE。
ハンズオン 1-A — 気になるモデルを Hub から探して README を読む
お題A Hub の Models ページでフィルタを使い、タスク Text Classification・言語 Japanese(または English)で1モデルを選び、Model Card を3分読む。
ブラウザ操作の目安:
1. https://huggingface.co/models 2. 左サイドバー Task → Text Classification 3. Language → japanese(なければ english で可) 4. 気になるモデルを開き、README の Intended uses / Limitations をメモ
霊夢
日本語モデル、星の数で選えばいい?
魔理沙
ダウンロード数や likes は参考になるが、タスク一致 と ライセンス が最優先だ。星だけで選ぶと、用途違いの LLM を掴むこともある。
ハンズオン 1-B — huggingface-cli login でログインする
お題B 第1.2節のとおり login し、whoami でユーザー名が表示されることを確認する。
cd /path/to/yukkuri-hugging-face source .venv/bin/activate huggingface-cli login huggingface-cli whoami
未作成なら .env に HF_TOKEN を置き、将来のスクリプト用にしておく(第6章の push 前に Write トークンへ切り替える)。
ハンズオン 1-C — モデルをローカルにダウンロードして中身を覗く
お題C 本書付属スクリプトでモデルを明示的に DL し、キャッシュ内のファイル一覧を表示する。初回は数 hundred MB 級の DL があり、回線次第で数分かかる ことを想定してね。
python scripts/download_model.py \ --model-id distilbert-base-uncased-finetuned-sst-2-english
期待する出力の例:
Downloading snapshot for: distilbert-base-uncased-finetuned-sst-2-english Local path: /Users/you/.cache/huggingface/hub/models--distilbert-base-uncased-finetuned-sst-2-english/snapshots/xxxxxxxx Done. Run inspect_model.py with the same --model-id to list files.
中身の確認:
python scripts/inspect_model.py \ --model-id distilbert-base-uncased-finetuned-sst-2-english
Model ID: distilbert-base-uncased-finetuned-sst-2-english Cache root: /Users/you/.cache/huggingface/hub/... Files (N): config.json 1.2 KB model.safetensors 256.3 MB tokenizer.json 456.1 KB tokenizer_config.json 1.1 KB vocab.txt 226.0 KB README.md 12.4 KB config.json (excerpt): model_type: distilbert num_labels: 2 ...
霊夢
snapshot_download って第0章の pipeline と何が違うの?
魔理沙
pipeline は 推論に必要なファイルだけ を lazy に取る。snapshot_download は リポジトリのスナップショット一式 を指定ディレクトリ(またはキャッシュ)に落とす。中身を覗く・オフライン配布の練習に向いているのだ。
download_model.py の核心部分:
from huggingface_hub import snapshot_download path = snapshot_download(repo_id=model_id) print("Local path:", path)
改造お題(任意) --output-dir ./models/my-model でプロジェクト内にコピーしてみる。
python scripts/download_model.py \ --model-id distilbert-base-uncased-finetuned-sst-2-english \ --output-dir ./models/sst2
本章スクリプト全文
魔理沙
リポジトリがなくても手を動かせるよう、本章の 完成スクリプト をそのまま載せるのだ。
手元では scripts/ 以下と同じファイル名で保存して実行すればよいZE。
(python scripts/xxx.py と書いてある箇所は、保存先に合わせてパスを読み替えてくれ。)
scripts/download_model.py
#!/usr/bin/env python3 """Download a model snapshot from Hugging Face Hub.""" from __future__ import annotations import argparse from pathlib import Path from huggingface_hub import snapshot_download def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description="Download a model repository snapshot from the Hub.", ) parser.add_argument( "--model-id", required=True, help='Hub model ID, e.g. "distilbert-base-uncased-finetuned-sst-2-english"', ) parser.add_argument( "--output-dir", default=None, help="Optional local directory. Default: Hub cache only.", ) parser.add_argument( "--revision", default=None, help="Git revision (branch, tag, or commit). Default: main.", ) return parser.parse_args() def main() -> None: args = parse_args() kwargs: dict = {"repo_id": args.model_id, "repo_type": "model"} if args.revision: kwargs["revision"] = args.revision if args.output_dir: kwargs["local_dir"] = args.output_dir kwargs["local_dir_use_symlinks"] = False print(f"Downloading snapshot for: {args.model_id}") if args.output_dir: print(f"Output directory: {Path(args.output_dir).resolve()}") print("(First download may take several minutes depending on model size.)") path = snapshot_download(**kwargs) print(f"Local path: {path}") print("Done. Run inspect_model.py with the same --model-id to list files.") if __name__ == "__main__": main()
scripts/inspect_model.py
#!/usr/bin/env python3 """Inspect cached Hub model files and show config excerpt.""" from __future__ import annotations import argparse import json from pathlib import Path from huggingface_hub import scan_cache_dir, snapshot_download def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description="List files for a cached Hub model and show config.json excerpt.", ) parser.add_argument( "--model-id", required=True, help='Hub model ID, e.g. "distilbert-base-uncased-finetuned-sst-2-english"', ) return parser.parse_args() def human_size(num_bytes: int) -> str: if num_bytes < 1024: return f"{num_bytes} B" if num_bytes < 1024**2: return f"{num_bytes / 1024:.1f} KB" if num_bytes < 1024**3: return f"{num_bytes / 1024**2:.1f} MB" return f"{num_bytes / 1024**3:.2f} GB" def find_repo_in_cache(model_id: str) -> Path | None: cache_info = scan_cache_dir() needle = model_id.replace("/", "--") for repo in cache_info.repos: if needle in repo.repo_id.replace("/", "--") or repo.repo_id.endswith(model_id): if repo.revisions: return Path(repo.revisions[0].snapshot_path) return None def main() -> None: args = parse_args() print(f"Model ID: {args.model_id}") local_path = find_repo_in_cache(args.model_id) if local_path is None: print("Not found in cache. Downloading snapshot first...") print("(First download may take several minutes.)") local_path = Path(snapshot_download(repo_id=args.model_id)) print(f"Snapshot path: {local_path}") files = sorted(local_path.iterdir(), key=lambda p: p.name) print(f"\nFiles ({len(files)}):") for file_path in files: if file_path.is_file(): size = human_size(file_path.stat().st_size) print(f" {file_path.name:<40} {size:>10}") config_path = local_path / "config.json" if config_path.exists(): with config_path.open(encoding="utf-8") as f: config = json.load(f) print("\nconfig.json (excerpt):") excerpt = { k: config[k] for k in ( "model_type", "architectures", "num_labels", "vocab_size", "hidden_size", ) if k in config } print(json.dumps(excerpt, indent=2, ensure_ascii=False)) else: print("\nconfig.json not found in snapshot.") if __name__ == "__main__": main()
霊夢のメモ帳
- Hub はモデル・データセット・Spaces の共有場所。DL 前に Model Card / Dataset Card を読む。
huggingface-cli loginで Read トークンを保存。トークンは.env管理、コードに直書きしない。snapshot_downloadでローカルに落としてconfig.jsonや重みファイルを確認する習慣をつける。
魔理沙の one more thing
Hub 上のモデルは organization/model-name 形式の ID だ。ブラウザ URL から ID をコピーすると typo が減るZE。
# CLI でメタデータだけ取得(DL なし) huggingface-cli repo info distilbert-base-uncased-finetuned-sst-2-english
# Python で README 先頭を表示 from huggingface_hub import hf_hub_download readme_path = hf_hub_download( repo_id="distilbert-base-uncased-finetuned-sst-2-english", filename="README.md", ) print(open(readme_path, encoding="utf-8").read()[:500])
次章へ
霊夢
Hub でモデルの場所はわかった。でも毎回 DL して中身見るの、疲れない?
魔理沙
日常は 第2章の pipeline だけで十分だZE。3行で推論まで行ける。Tokenizer や重みの詳細は第3章以降で剥がしていく。
霊夢
第0章のお題B、本編来たね。
魔理沙
その通り。第2章 Pipeline で「3行推論」を体験する へ進むのだ。感情分析から翻訳・画像・音声まで、同じ API の使い方を体験するZE。
第2章 Pipeline で「3行推論」を体験する
2.1 Transformers ライブラリのインストール
霊夢
第1章で Hub からモデル落としてきた。で、推論はどう動かすの?
魔理沙
第0章で入れた transformers が本体だZE。バージョンだけ再確認する。
cd /path/to/yukkuri-hugging-face source .venv/bin/activate python -c "import transformers; print(transformers.__version__)"
4.41.2
足りない場合:
pip install "transformers>=4.40" torch sentencepiece
霊夢
第0章ですでに入れてるなら、この節はスキップ?
魔理沙
check_env.py が通ればスキップでいい。第2章以降は タスクごとに追加パッケージ が要ることもある(画像は pillow、音声は librosa など)。エラーが出たらその都度 pip install するのだ。
2.2 pipeline とは — 前処理から推論までをまとめてやってくれる仕組み
霊夢
第0章のお題B、3行くらいで動いたやつ。中身は?
魔理沙
pipeline は、Tokenizer → Model → 後処理を 1本の API にまとめた高レベルインターフェースだZE。
入力テキスト
│
▼
┌──────────────┐
│ Tokenizer │ 文字列 → ID 列
└──────┬───────┘
▼
┌──────────────┐
│ Model │ 推論(第4章で中身を見る)
└──────┬───────┘
▼
┌──────────────┐
│ 後処理 │ ラベル名・スコア整形
└──────┬───────┘
▼
[{'label': 'POSITIVE', 'score': 0.99}]
最小例(第0章お題Bの再掲):
from transformers import pipeline clf = pipeline( "sentiment-analysis", model="distilbert-base-uncased-finetuned-sst-2-english", ) print(clf("I love Hugging Face!"))
[{'label': 'POSITIVE', 'score': 0.9998}]
霊夢
第1章で DL したモデルと同じ ID だ。
魔理沙
同じだZE。初回実行時は Hub から取得する。2回目以降はキャッシュ から読むので速い。第1章の download_model.py は「先に全部落とす」練習、pipeline は「必要なときに取る」実用ルートだ。
pipeline 作成時の主な引数:
| 引数 | 意味 |
|---|---|
| 第1引数(タスク名) | "sentiment-analysis", "translation", "automatic-speech-recognition" など |
model |
Hub のモデル ID。省略するとタスクのデフォルトが選ばれる |
device |
-1 = CPU, 0 = 最初の GPU(CUDA がある場合) |
2.3 テキスト分類・感情分析を試す
魔理沙
英語の感情分析はさっきのモデルで十分。日本語は 日本語 FT 済みモデル に差し替えるのだ。
from transformers import pipeline # 初回 DL あり(数百 MB 級のことも) clf = pipeline( "sentiment-analysis", model="daigo/bert-base-japanese-sentiment", ) print(clf("今日はいい天気だね")) print(clf("最悪な一日だった"))
期待する出力の例(モデルによりラベル名は異なる):
[{'label': 'ポジティブ', 'score': 0.92}]
[{'label': 'ネガティブ', 'score': 0.88}]
複数文をまとめて渡す:
texts = ["最高!", "微妙…", "Hugging Face is fun"] print(clf(texts))
霊夢
英語モデルに日本語入れたら?
魔理沙
動いても 意味のない結果 になりやすいZE。Model Card の 対応言語 を信じる。ハンズオン 2-A では付属スクリプトで試す。
python scripts/ch02_pipeline_sentiment.py \ --text "ゆっくりしていこうね"
2.4 翻訳・要約・質問応答を試す
魔理沙
タスク名を変えるだけで 同じ pipeline パターン が使える。
翻訳(英 → 日)
from transformers import pipeline translator = pipeline( "translation_en_to_ja", model="Helsinki-NLP/opus-mt-en-ja", ) print(translator("Hello, how are you?"))
[{'translation_text': 'こんにちは、お元気ですか?'}]
要約(英語)
summarizer = pipeline(
"summarization",
model="facebook/bart-large-cnn",
)
article = (
"The Hugging Face Hub is a platform for sharing machine learning models. "
"Researchers and developers upload models, datasets, and demo applications."
)
print(summarizer(article, max_length=30, min_length=10, do_sample=False))
初回 DL は BART-large 級で 1 GB 超になることもある。 時間に余裕のあるとき実行してね。
質問応答(Extractive QA)
qa = pipeline(
"question-answering",
model="distilbert-base-cased-distilled-squad",
)
context = "Hugging Face was founded in 2016. The Hub hosts models and datasets."
print(qa(question="When was Hugging Face founded?", context=context))
{'score': 0.45, 'start': 25, 'end': 29, 'answer': '2016'}
霊夢
要約、日本語記事そのまま入れていい?
魔理沙
facebook/bart-large-cnn は 英語向け だ。日本語要約は別モデルを Hub で探す必要がある。第2章では API の形 を覚えるのが目的だZE。
翻訳実験用スクリプト:
python scripts/ch02_pipeline_translation.py \ --text "I will learn Hugging Face step by step."
2.5 画像分類・物体検出(Vision)に触れる
霊夢
テキスト以外も pipeline でいけるの?
魔理沙
いけるZE。画像分類の例だ(初回 DL に時間がかかる)。
pip install pillow
from transformers import pipeline classifier = pipeline( "image-classification", model="google/vit-base-patch16-224", ) result = classifier( "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/pipeline-cat-chonk.jpeg" ) print(result[:3])
[{'label': 'Egyptian cat', 'score': 0.85}, ...]
物体検出(バウンディングボックス付き):
detector = pipeline(
"object-detection",
model="facebook/detr-resnet-50",
)
# ローカル画像パスでも URL でも可
results = detector("https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/coco_sample.png")
for obj in results[:3]:
print(obj["label"], round(obj["score"], 3), obj["box"])
霊夢
GPU ないとキツそう。
魔理沙
ViT-base 程度なら CPU でも数秒〜数十秒 で動くことが多い。DETR は重め。Colab GPU がある日に試すのもありだ。
python scripts/ch02_pipeline_vision.py \ --image-url "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/pipeline-cat-chonk.jpeg"
2.6 音声認識(Whisper など)に触れる
魔理沙
Whisper 系は automatic-speech-recognition タスクだ。小さめの openai/whisper-tiny から触るのがおすすめだZE。
pip install librosa soundfile
from transformers import pipeline asr = pipeline( "automatic-speech-recognition", model="openai/whisper-tiny", ) # サンプル音声 URL(英語) sample = "https://huggingface.co/datasets/Narsil/asr_dummy/resolve/main/1.flac" print(asr(sample))
{'text': ' He hoped there would be stew for dinner, turnips and carrots and bruised potatoes and fat mutton pieces to be ladled out in thick, peppered flour-fatten sauce.'}
日本語を試すなら、短い音声ファイルを --audio で渡す:
python scripts/ch02_pipeline_whisper.py --audio /path/to/your/sample.wav
Whisper は 多言語 だが、ノイズの多い録音では精度が落ちる。Model Card の Limitations も読むこと。
ハンズオン 2 — Pipeline 実践
ハンズオン 2-A — 日本語テキストの感情分析
お題A 付属スクリプトで日本語文の感情を判定する。
python scripts/ch02_pipeline_sentiment.py \ --text "この本、わかりやすくて最高" \ --model daigo/bert-base-japanese-sentiment
改造 --text を3文に増やし、結果を log/ch02_sentiment.txt にリダイレクトする。
mkdir -p log python scripts/ch02_pipeline_sentiment.py \ --text "最高" --text "最悪" --text "普通" \ 2>&1 | tee log/ch02_sentiment.txt
ハンズオン 2-B — 英日翻訳パイプラインの差し替え実験
お題B 同じ英文を、モデル ID を変えて翻訳比較する。
python scripts/ch02_pipeline_translation.py \ --text "The weather is nice today." \ --model Helsinki-NLP/opus-mt-en-ja
別モデル(存在する場合 Hub で確認)に --model を差し替え、訳文の違いをメモする。
霊夢
モデル名、typo しそう…
魔理沙
第1章の Hub URL から ID をコピー する癖をつけるのだ。404 や Repository Not Found はだいたい typo だZE。
ハンズオン 2-C — 同じタスクでモデルを入れ替えて精度と速度を比べる
お題C 英語感情分析で 軽量 vs やや大きめ を比較する。
python scripts/ch02_pipeline_compare.py \
--task sentiment-analysis \
--text "I love Hugging Face!" \
--models distilbert-base-uncased-finetuned-sst-2-english \
bert-base-uncased \
--runs 3
期待する出力の例:
Model: distilbert-base-uncased-finetuned-sst-2-english
Result: [{'label': 'POSITIVE', 'score': 0.9998}]
Avg time (3 runs, excl. 1st load): 0.042 s
Model: bert-base-uncased
...
霊夢
bert-base-uncased って FT してなくない?
魔理沙
鋭いZE。第2-C は 「同じ API で model= だけ変える」 練習だ。FT 済みと未 FT を比べると、ラベルやスコアが意味を失うことも 教材として 覚えておく。実務では Model Card どおり タスク用 FT 済み を選ぶ。
章末 よくあるエラーと対処
霊夢
エラー集、欲しい。
魔理沙
第2章で多いものを表にしたZE。
| 症状 | 原因の例 | 対処 |
|---|---|---|
CUDA out of memory |
GPU メモリ不足 | 小さいモデル、device=-1 で CPU、pipeline(..., model_kwargs={"torch_dtype": ...}) |
Repository Not Found |
モデル ID typo / 非公開 | Hub URL から ID をコピー。private なら login |
401 / 403 |
gated 未同意 / トークン不足 | ブラウザで Agree、huggingface-cli login |
Connection error |
オフライン / プロキシ | ネット確認。社内プロキシは HF_HUB_ENABLE_HF_TRANSFER 等を調査 |
| 初回だけ異常に遅い | 正常(DL 中) | 待つ。第1章のキャッシュ場所を確認 |
| 日本語が gibberish | 言語不一致モデル | 日本語 FT モデルに差し替え |
CUDA 確認:
import torch print(torch.cuda.is_available())
CPU 固定で pipeline を作る:
clf = pipeline("sentiment-analysis", model="distilbert-base-uncased-finetuned-sst-2-english", device=-1)
メモリを抑える(発展・GPU 環境):
import torch clf = pipeline( "summarization", model="facebook/bart-large-cnn", device=0, model_kwargs={"torch_dtype": torch.float16}, )
章末 振り返りクイズ(霊夢 vs 魔理沙)
魔理沙
3問だけ。霊夢、答えてみろ。
Q1. pipeline の第1引数に渡すのは何?
霊夢
タスク名! "sentiment-analysis" とか。
魔理沙
正解だZE。
Q2. 同じ sentiment-analysis でも、英語文に日本語 FT モデルを使っていい?
霊夢
ダメに近い。Model Card の言語を見る。
魔理沙
その通り。
Q3. 2回目以降 pipeline が速いのはなぜ?
霊夢
第1章のキャッシュ。Hub から毎回 DL してない。
魔理沙
満点に近い。次章では Tokenizer を剥いて、文字列が ID になる過程を見るのだ。
本章スクリプト全文
魔理沙
リポジトリがなくても手を動かせるよう、本章の 完成スクリプト をそのまま載せるのだ。
手元では scripts/ 以下と同じファイル名で保存して実行すればよいZE。
(python scripts/xxx.py と書いてある箇所は、保存先に合わせてパスを読み替えてくれ。)
scripts/ch02_pipeline_sentiment.py
#!/usr/bin/env python3 """Japanese sentiment analysis via transformers pipeline.""" from __future__ import annotations import argparse from transformers import pipeline def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Run sentiment-analysis pipeline on Japanese text.") parser.add_argument( "--model", default="daigo/bert-base-japanese-sentiment", help="Hub model ID for Japanese sentiment.", ) parser.add_argument( "--text", action="append", required=True, help="Input text (repeatable). Example: --text '最高' --text '最悪'", ) parser.add_argument( "--device", type=int, default=-1, help="Device index (-1 for CPU, 0 for first CUDA GPU).", ) return parser.parse_args() def main() -> None: args = parse_args() print(f"Loading pipeline: sentiment-analysis / {args.model}") print("(First run downloads weights from the Hub; may take a few minutes.)") clf = pipeline( "sentiment-analysis", model=args.model, device=args.device, ) for text in args.text: result = clf(text)[0] label = result.get("label", result) score = result.get("score", 0.0) print(f"\nText: {text}") print(f" -> {label} (score={score:.4f})") if __name__ == "__main__": main()
scripts/ch02_pipeline_translation.py
#!/usr/bin/env python3 """English to Japanese translation via transformers pipeline.""" from __future__ import annotations import argparse from transformers import pipeline def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Translate English text to Japanese.") parser.add_argument( "--text", required=True, help="English source sentence.", ) parser.add_argument( "--model", default="Helsinki-NLP/opus-mt-en-ja", help="Hub translation model ID.", ) parser.add_argument( "--device", type=int, default=-1, help="Device index (-1 for CPU, 0 for first CUDA GPU).", ) return parser.parse_args() def main() -> None: args = parse_args() print(f"Loading pipeline: translation_en_to_ja / {args.model}") print("(First run downloads weights from the Hub; may take a few minutes.)") translator = pipeline( "translation_en_to_ja", model=args.model, device=args.device, ) result = translator(args.text)[0] translation = result.get("translation_text", result) print(f"\nEN: {args.text}") print(f"JA: {translation}") if __name__ == "__main__": main()
scripts/ch02_pipeline_compare.py
#!/usr/bin/env python3 """Compare pipeline models on the same task (latency + output).""" from __future__ import annotations import argparse import time from transformers import pipeline def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description="Run the same pipeline task with multiple models and compare timing.", ) parser.add_argument( "--task", default="sentiment-analysis", help='Pipeline task name, e.g. "sentiment-analysis".', ) parser.add_argument( "--text", required=True, help="Input text for inference.", ) parser.add_argument( "--models", nargs="+", required=True, help="One or more Hub model IDs.", ) parser.add_argument( "--runs", type=int, default=3, help="Timed runs per model (after warmup).", ) parser.add_argument( "--device", type=int, default=-1, help="Device index (-1 for CPU, 0 for first CUDA GPU).", ) return parser.parse_args() def main() -> None: args = parse_args() for model_id in args.models: print(f"\nModel: {model_id}") print("(First load may download from the Hub and is excluded from avg timing.)") pipe = pipeline(args.task, model=model_id, device=args.device) # Warmup + first result display first = pipe(args.text) print(f" Result: {first}") if args.runs < 1: continue start = time.perf_counter() for _ in range(args.runs): pipe(args.text) elapsed = time.perf_counter() - start avg = elapsed / args.runs print(f" Avg time ({args.runs} runs, excl. 1st load): {avg:.3f} s") if __name__ == "__main__": main()
scripts/ch02_pipeline_vision.py
#!/usr/bin/env python3 """Image classification demo via transformers pipeline.""" from __future__ import annotations import argparse from transformers import pipeline def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Classify an image from URL or local path.") parser.add_argument( "--image-url", default=( "https://huggingface.co/datasets/huggingface/documentation-images/" "resolve/main/pipeline-cat-chonk.jpeg" ), help="Image URL or local file path.", ) parser.add_argument( "--model", default="google/vit-base-patch16-224", help="Hub image-classification model ID.", ) parser.add_argument( "--top-k", type=int, default=3, help="Number of top labels to print.", ) parser.add_argument( "--device", type=int, default=-1, help="Device index (-1 for CPU, 0 for first CUDA GPU).", ) return parser.parse_args() def main() -> None: args = parse_args() print(f"Loading pipeline: image-classification / {args.model}") print("(First run downloads weights from the Hub; may take several minutes.)") classifier = pipeline( "image-classification", model=args.model, device=args.device, ) results = classifier(args.image_url) print(f"\nImage: {args.image_url}") for rank, item in enumerate(results[: args.top_k], start=1): print(f" {rank}. {item['label']}: {item['score']:.4f}") if __name__ == "__main__": main()
scripts/ch02_pipeline_whisper.py
#!/usr/bin/env python3 """Speech recognition demo via Whisper pipeline.""" from __future__ import annotations import argparse from transformers import pipeline DEFAULT_SAMPLE_URL = ( "https://huggingface.co/datasets/Narsil/asr_dummy/resolve/main/1.flac" ) def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Transcribe audio with Whisper pipeline.") parser.add_argument( "--audio", default=DEFAULT_SAMPLE_URL, help="Path to local audio file or URL (default: Hub sample FLAC).", ) parser.add_argument( "--model", default="openai/whisper-tiny", help="Hub ASR model ID.", ) parser.add_argument( "--device", type=int, default=-1, help="Device index (-1 for CPU, 0 for first CUDA GPU).", ) return parser.parse_args() def main() -> None: args = parse_args() print(f"Loading pipeline: automatic-speech-recognition / {args.model}") print("(First run downloads weights from the Hub; may take several minutes.)") asr = pipeline( "automatic-speech-recognition", model=args.model, device=args.device, ) result = asr(args.audio) text = result.get("text", result) if isinstance(result, dict) else result print(f"\nAudio: {args.audio}") print(f"Text: {text}") if __name__ == "__main__": main()
霊夢のメモ帳
pipeline(タスク, model=ID)で前処理〜推論〜後処理が一括。第0章の3行推論の正体。- 言語・タスク一致 のモデルを Hub の Model Card から選ぶ。英語用を日本語に流用しない。
- 初回は DL で時間がかかる のが正常。2回目以降はキャッシュ。エラーは typo・gated・CUDA OOM を疑う。
魔理沙の one more thing
pipeline は内部で torch.no_grad() 相当の推論モードになる。バッチを自分で組みたいときは第3章の Tokenizer を直接使う準備になるZE。
from transformers import pipeline # 返り値を JSON 風に整える(ログ用) clf = pipeline("sentiment-analysis", model="distilbert-base-uncased-finetuned-sst-2-english") out = clf("Hello")[0] print(f"{out['label']} ({out['score']:.4f})")
次章へ
霊夢
pipeline 便利だけど、中身ブラックボックス感ある。
魔理沙
次は Tokenizer から剥く。第3章 Tokenizer — 文字列をモデルが理解できる形にする だZE。同じ英文でもモデルによってトークン分割が違うのを、目で見て確認する。
霊夢
「love」が1トークンか2トークンか、気になる。
魔理沙
ハンズオン 3-A で並べて比べるのだ。第4章の Model へつながる土台になるZE。
第3章 Tokenizer — 文字列をモデルが理解できる形にする
3.1 なぜトークナイズが必要なのか
霊夢
第2章の pipeline、文字そのままモデルに入ってるわけじゃないよね?
魔理沙
その通りだZE。ニューラルネットは 数値の列 しか扱えない。Tokenizer が 文字列 → トークン ID 列 に変換する。
"I love HF"
│
▼ Tokenizer
tokens: ["I", "love", "H", "##F"] ← モデル・方式によって分割が違う
ids: [101, 1045, 2293, 123, 456, 102]
│
▼ Model
logits / hidden states
霊夢
単語1語=1トークンじゃないの?
魔理沙
必ずしもそうではない。英語の WordPiece なら playing → play + ##ing のように サブワード分割 する。語彙にない語は小片に割って 未知語を減らす 狙いだ。
| 方式 | 例モデル | 特徴 |
|---|---|---|
| WordPiece | BERT 系 | ## 付きサブワード |
| BPE | GPT-2, RoBERTa | バイトペアで語彙構築 |
| SentencePiece | 多言語・日本語 | 空白に依存しにくい |
霊夢
第2章で日本語モデルと英語モデル、結果が違ったのもトークナイザの差?
魔理沙
Tokenizer と学習データの 両方 だZE。同じ文でもモデルごとに 切り方が違う のを、これからコードで見る。
3.2 AutoTokenizer の基本
魔理沙
Hub のモデル ID から 対応する Tokenizer を自動選択 するのが AutoTokenizer だ。第1章の config.json とセットで Hub から落ちてくる。
from transformers import AutoTokenizer model_id = "distilbert-base-uncased-finetuned-sst-2-english" tokenizer = AutoTokenizer.from_pretrained(model_id) print(type(tokenizer).__name__) print("Vocab size:", tokenizer.vocab_size)
DistilBertTokenizerFast Vocab size: 30522
霊夢
from_pretrained、第1章の DL と同じ響き。
魔理沙
同じ from_pretrained(model_id) パターンだ。Model も第4章で同様に読む。初回は Hub から tokenizer ファイルを DL する(数秒〜数十秒)。
ローカルキャッシュだけ使う(オフライン):
tokenizer = AutoTokenizer.from_pretrained(model_id, local_files_only=True)
第2章の pipeline 内部でも、同じ Tokenizer が使われている。
3.3 エンコード・デコード・特殊トークン
魔理沙
基本操作は エンコード(文字 → ID) と デコード(ID → 文字) だZE。
from transformers import AutoTokenizer tokenizer = AutoTokenizer.from_pretrained( "distilbert-base-uncased-finetuned-sst-2-english" ) text = "I love Hugging Face!" encoded = tokenizer.encode(text) print("IDs:", encoded) print("Decoded:", tokenizer.decode(encoded))
IDs: [101, 1045, 2293, 9259, 3563, 999, 102] Decoded: [CLS] i love hugging face! [SEP]
特殊トークン はモデルごとに定義される。
| トークン | BERT 系での例 | 役割 |
|---|---|---|
[CLS] |
文頭 | 分類タスクの代表 |
[SEP] |
文末 / 区切り | 文ペアの境界 |
<pad> |
短い列の埋め草 | バッチ長揃え |
<unk> |
語彙外の断片 | 未知語(Tokenizer により稀) |
属性で確認:
print(tokenizer.cls_token, tokenizer.sep_token, tokenizer.pad_token) print(tokenizer.cls_token_id, tokenizer.sep_token_id)
霊夢
decode したら小文字になった。
魔理沙
distilbert-base-uncased は 小文字正規化 前提だZE。大文字情報は捨てられる。ケースを区別するモデルなら cased 版を選ぶ。
辞書形式で詳細を見る:
batch = tokenizer(text, return_tensors="pt") print(batch.keys()) # input_ids, attention_mask print(batch["input_ids"]) print(batch["attention_mask"])
Keys: dict_keys(['input_ids', 'attention_mask']) tensor([[101, 1045, 2293, 9259, 3563, 999, 102]]) tensor([[1, 1, 1, 1, 1, 1, 1]])
attention_mask は「本物のトークン=1、パディング=0」を示す。第4章の Model に渡すときもセットだ。
3.4 パディングと truncation
霊夢
バッチ処理って、文の長さバラバラじゃダメなんでしょ?
魔理沙
その通りだZE。テンソルは 矩形 に揃える必要がある。padding で短い列を伸ばし、truncation で長い列を切る。
from transformers import AutoTokenizer tokenizer = AutoTokenizer.from_pretrained( "distilbert-base-uncased-finetuned-sst-2-english" ) long_text = "word " * 200 short_text = "hello" # 個別に最大長512まで切る enc = tokenizer(long_text, truncation=True, max_length=512) print(len(enc["input_ids"])) # バッチ: 最長に合わせてパディング(単体 tokenizer では pad_token 設定が要る場合あり) tokenizer.pad_token = tokenizer.eos_token if tokenizer.pad_token is None else tokenizer.pad_token batch = tokenizer( [short_text, long_text], padding=True, truncation=True, max_length=128, return_tensors="pt", ) print(batch["input_ids"].shape) print(batch["attention_mask"])
512
torch.Size([2, 128])
tensor([[101, 7592, 102, 0, 0, ...],
[101, ..., 102, ...]])
max_length は Model Card や config.json の max_position_embeddings を超えないよう設定するのが安全だ。
霊夢
pad_token が None って出たことある。
魔理沙
GPT 系など もともと pad が無い Tokenizer がある。tokenizer.pad_token = tokenizer.eos_token のように 既存特殊トークンを借りる か、第3章ハンズオン 3-C のように 語彙追加 する。
3.5 バッチ処理と DataCollator
魔理沙
学習ループ(第6章)では DataCollatorWithPadding が動的パディングを担当する。推論前の Dataset → バッチ 整形のお手本だZE。
from transformers import AutoTokenizer, DataCollatorWithPadding tokenizer = AutoTokenizer.from_pretrained( "distilbert-base-uncased-finetuned-sst-2-english" ) if tokenizer.pad_token is None: tokenizer.pad_token = tokenizer.sep_token collator = DataCollatorWithPadding(tokenizer=tokenizer) features = [ tokenizer("First sentence."), tokenizer("Second sentence is a bit longer than the first."), tokenizer("Third."), ] batch = collator(features) print(batch["input_ids"].shape) print(batch["attention_mask"])
DataCollator の利点:
| 項目 | 説明 |
|---|---|
| 動的パディング | バッチ内の最長に合わせる(無駄な pad を減らす) |
| Trainer 連携 | 第6章でそのまま data_collator=collator に渡せる |
| 一貫性 | Tokenizer 設定と pad 方針を共通化 |
霊夢
第2章の pipeline はこれ全部勝手にやってた?
魔理沙
推論時は 内部で tokenizer 呼び出し + pad/trunc まで面倒を見る。自分で制御したいとき(長文を自分で chunk する、独自語彙を足す)だけ第3章以降の API を直接使うのだ。
ハンズオン 3 — Tokenizer 実践
ハンズオン 3-A — 同じ文を複数モデルのトークナイザで比較する
お題A 英語1文を、2つの Tokenizer でトークン列を並べて比較する。
python scripts/ch03_tokenizer_compare.py \
--text "Hugging Face makes NLP easy!" \
--models distilbert-base-uncased-finetuned-sst-2-english \
bert-base-uncased
期待する出力の例:
Text: Hugging Face makes NLP easy! --- distilbert-base-uncased-finetuned-sst-2-english --- Tokenizer: DistilBertTokenizerFast Tokens: ['hugging', 'face', 'makes', 'nl', '##p', 'easy', '!'] IDs (first 12): [101, 9238, 3563, 7594, 17953, 7861, 3739, 999, 102] --- bert-base-uncased --- Tokenizer: BertTokenizerFast Tokens: ['hu', '##gging', 'face', 'makes', 'nl', '##p', 'easy', '!'] ...
霊夢
Hugging の切り方、全然違う…
魔理沙
語彙と学習方式の差だZE。同じモデルファミリーでも cased / uncased で変わる。Tokenizer は 必ずモデルとペア で使う。
ハンズオン 3-B — 長文を切ってバッチ推論する
お題B 長文を max_length で切り、複数チャンクをバッチ推論する(第2章 pipeline の裏側に近い)。
python scripts/ch03_tokenizer_batch.py \ --model distilbert-base-uncased-finetuned-sst-2-english \ --text-file /path/to/yukkuri-hugging-face/docs/00.md \ --max-length 128 \ --stride 64
--text-file を省略すると組み込みの長文サンプルを使う。
期待する出力の例:
Loaded model + tokenizer: distilbert-base-uncased-finetuned-sst-2-english Chunks: 8 (max_length=128, stride=64) Chunk 0 -> POSITIVE (0.9821) Chunk 1 -> POSITIVE (0.9544) ... Majority vote: POSITIVE
改造 --stride を max_length と同じにして 非重複チャンク にし、チャンク数がどう変わるか見る。
ハンズオン 3-C — 独自語彙を追加してトークナイザを拡張する
お題C ゆっくり固有の語 ゆっくり を語彙に追加し、分割がどう変わるか確認する。
python scripts/ch03_tokenizer_custom_vocab.py \ --model distilbert-base-uncased-finetuned-sst-2-english \ --new-token "ゆっくり" \ --text "ゆっくりしていこうね"
期待する出力の例:
Before add: tokens=['ゆ', 'っ', 'く', 'り', ...] (many subchars) After add: tokens=['ゆっくり', ...] New token id: 30522 Saved to: ./models/tokenizer-with-yukkuri
霊夢
語彙足すと Model の重みも変えないとダメ?
魔理沙
鋭い。本格的 FT では embedding サイズが変わる ので Model 側の調整が要る(第6章・第7章)。ハンズオン 3-C は Tokenizer 拡張の手順 を覚える段階だZE。推論だけなら追加トークンは ランダム初期化 embedding のまま意味を持たないことも理解しておく。
本章スクリプト全文
魔理沙
リポジトリがなくても手を動かせるよう、本章の 完成スクリプト をそのまま載せるのだ。
手元では scripts/ 以下と同じファイル名で保存して実行すればよいZE。
(python scripts/xxx.py と書いてある箇所は、保存先に合わせてパスを読み替えてくれ。)
scripts/ch03_tokenizer_compare.py
#!/usr/bin/env python3 """Compare tokenization across multiple Hub models.""" from __future__ import annotations import argparse from transformers import AutoTokenizer def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description="Show how different tokenizers split the same text.", ) parser.add_argument( "--text", required=True, help="Input text to tokenize.", ) parser.add_argument( "--models", nargs="+", required=True, help="One or more Hub model IDs (tokenizer loaded from each).", ) return parser.parse_args() def main() -> None: args = parse_args() print(f"Text: {args.text}\n") for model_id in args.models: print(f"--- {model_id} ---") print("(First run may download tokenizer files from the Hub.)") tokenizer = AutoTokenizer.from_pretrained(model_id) encoded = tokenizer.encode(args.text, add_special_tokens=True) tokens = tokenizer.convert_ids_to_tokens(encoded) print(f"Tokenizer: {type(tokenizer).__name__}") print(f"Vocab size: {tokenizer.vocab_size}") print(f"Tokens: {tokens}") print(f"IDs (first 16): {encoded[:16]}") print(f"Decoded: {tokenizer.decode(encoded)}\n") if __name__ == "__main__": main()
scripts/ch03_tokenizer_batch.py
#!/usr/bin/env python3 """Chunk long text with truncation/stride and run batch sentiment inference.""" from __future__ import annotations import argparse from collections import Counter from pathlib import Path import torch from transformers import AutoModelForSequenceClassification, AutoTokenizer def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description="Tokenize long text into chunks and classify each chunk.", ) parser.add_argument( "--model", default="distilbert-base-uncased-finetuned-sst-2-english", help="Hub model ID for sequence classification.", ) parser.add_argument( "--text-file", default=None, help="Optional path to a text file. If omitted, uses a built-in long sample.", ) parser.add_argument( "--max-length", type=int, default=128, help="Max tokens per chunk (including special tokens).", ) parser.add_argument( "--stride", type=int, default=64, help="Stride for overlapping chunks (tokenizer overflow stride).", ) parser.add_argument( "--device", default="cpu", help='Torch device string, e.g. "cpu" or "cuda:0".', ) return parser.parse_args() def load_text(path: str | None) -> str: if path is None: return ("Hugging Face is great. " * 80).strip() return Path(path).read_text(encoding="utf-8") def main() -> None: args = parse_args() text = load_text(args.text_file) print(f"Loading model + tokenizer: {args.model}") print("(First run downloads weights from the Hub; may take a few minutes.)") tokenizer = AutoTokenizer.from_pretrained(args.model) model = AutoModelForSequenceClassification.from_pretrained(args.model) model.eval() device = torch.device(args.device) model.to(device) encoded = tokenizer( text, truncation=True, max_length=args.max_length, stride=args.stride, return_overflowing_tokens=True, return_tensors="pt", ) num_chunks = encoded["input_ids"].shape[0] print(f"Chunks: {num_chunks} (max_length={args.max_length}, stride={args.stride})\n") id2label = model.config.id2label labels: list[str] = [] with torch.no_grad(): for i in range(num_chunks): input_ids = encoded["input_ids"][i].unsqueeze(0).to(device) attention_mask = encoded["attention_mask"][i].unsqueeze(0).to(device) logits = model(input_ids=input_ids, attention_mask=attention_mask).logits pred_id = int(logits.argmax(dim=-1).item()) label = id2label[pred_id] score = torch.softmax(logits, dim=-1)[0, pred_id].item() labels.append(label) print(f"Chunk {i} -> {label} ({score:.4f})") majority = Counter(labels).most_common(1)[0][0] print(f"\nMajority vote: {majority}") if __name__ == "__main__": main()
scripts/ch03_tokenizer_custom_vocab.py
#!/usr/bin/env python3 """Add a custom token to a tokenizer and save locally.""" from __future__ import annotations import argparse from pathlib import Path from transformers import AutoTokenizer def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description="Add new tokens to a tokenizer and compare tokenization.", ) parser.add_argument( "--model", default="distilbert-base-uncased-finetuned-sst-2-english", help="Hub model ID to load tokenizer from.", ) parser.add_argument( "--new-token", action="append", required=True, help='Token to add (repeatable). Example: --new-token "ゆっくり"', ) parser.add_argument( "--text", default="ゆっくりしていこうね", help="Sample text to tokenize before/after adding tokens.", ) parser.add_argument( "--output-dir", default="./models/tokenizer-custom", help="Directory to save the extended tokenizer.", ) return parser.parse_args() def show_tokens(tokenizer, text: str, prefix: str) -> None: ids = tokenizer.encode(text, add_special_tokens=False) tokens = tokenizer.convert_ids_to_tokens(ids) print(f"{prefix}: tokens={tokens}") print(f"{prefix}: ids={ids}") def main() -> None: args = parse_args() print(f"Loading tokenizer: {args.model}") print("(First run may download tokenizer files from the Hub.)") tokenizer = AutoTokenizer.from_pretrained(args.model) print(f"\nSample text: {args.text}") show_tokens(tokenizer, args.text, "Before add") num_added = tokenizer.add_tokens(args.new_token) print(f"\nAdded {num_added} token(s): {args.new_token}") for token in args.new_token: token_id = tokenizer.convert_tokens_to_ids(token) print(f"New token id for {token!r}: {token_id}") show_tokens(tokenizer, args.text, "After add") out = Path(args.output_dir) out.mkdir(parents=True, exist_ok=True) tokenizer.save_pretrained(out) print(f"\nSaved to: {out.resolve()}") print( "Note: extending vocab changes embedding size; " "fine-tune the model (Ch.6–7) before expecting better semantics." ) if __name__ == "__main__": main()
霊夢のメモ帳
- モデルは 数値 ID 列 しか食べない。
AutoTokenizer.from_pretrained(model_id)でペアを揃える。 encode/decodeと[CLS][SEP]attention_maskを読めるようにする。- 長文は
truncation+max_length、バッチはpaddingまたはDataCollatorWithPadding。
魔理沙の one more thing
トークン数は 課金 API やコンテキスト長 の指標にもなる。推論前に件数だけ数える習慣をつけるZE。
from transformers import AutoTokenizer tokenizer = AutoTokenizer.from_pretrained( "distilbert-base-uncased-finetuned-sst-2-english" ) text = "Hugging Face Hub is awesome." ids = tokenizer.encode(text, add_special_tokens=True) print("Token count:", len(ids)) print("Tokens:", tokenizer.convert_ids_to_tokens(ids))
Token count: 9 Tokens: ['[CLS]', 'hugging', 'face', 'hub', 'is', 'awesome', '.', '[SEP]']
次章へ
霊夢
Tokenizer で ID まではわかった。で、Model はその ID をどう計算するの?
魔理沙
第4章 Model — 中身を少しだけ覗く だZE。AutoModel と AutoModelForSequenceClassification の違い、config.json と重みファイル、そして pipeline を使わない手動 forward をやる。
霊夢
いよいよブラックボックス開く段階ね。
魔理沙
恐れるな。第3章までで 入力の形 は揃った。あとは 出力テンソルの形 を確認するだけから始めるのだ。
第4章 Model — 中身を少しだけ覗く
4.1 AutoModel / AutoModelForCausalLM などクラスの違い
霊夢
第3章でトークナイザが input_ids を作るのは分かった。でもそのあと 誰が計算する の?
魔理沙
Model だZE。第2章の pipeline は内部で「トークナイザ → モデル → 後処理」をまとめてやってくれた。今章は モデル本体 に触る。
霊夢
AutoModel って第0章の辞書にも出てきたけど、AutoModelForCausalLM って何?
魔理沙
名前の For〜 がタスク用の 出力ヘッド を表す。Hub の config.json からアーキテクチャを推測して、適切なクラスを選ぶのが Auto* 系だ。
| クラス例 | 典型タスク | 出力のイメージ |
|---|---|---|
AutoModel |
特徴量抽出・埋め込み | 各トークンの hidden states |
AutoModelForSequenceClassification |
テキスト分類 | クラスごとの logits(生スコア) |
AutoModelForCausalLM |
文章生成(GPT 系) | 次トークン予測 |
AutoModelForMaskedLM |
穴埋め(BERT 系) | マスク位置の語候補 |
霊夢
分類と生成でクラスが違うのね。間違えると?
魔理沙
generate() が無い、出力の形が違う、学習用ヘッドが合わない、などで即エラーだ。第6章の FT でも ForSequenceClassification を選ぶのが定番なのだ。
from transformers import ( AutoModel, AutoModelForSequenceClassification, AutoModelForCausalLM, ) # 特徴量だけ欲しい emb_model = AutoModel.from_pretrained("distilbert-base-uncased") # 感情ラベルが欲しい cls_model = AutoModelForSequenceClassification.from_pretrained( "distilbert-base-uncased-finetuned-sst-2-english" ) # 続きの文章が欲しい gen_model = AutoModelForCausalLM.from_pretrained("gpt2")
4.2 設定(config.json)と重み(.safetensors / .bin)
霊夢
Hub から DL するとファイルがいっぱい… 何が本体?
魔理沙
ざっくり 設計図 + 重み の2セットだZE。
| ファイル | 役割 |
|---|---|
config.json |
層数、隠れ次元、語彙サイズ、ラベル数など 構造 |
model.safetensors(推奨)または pytorch_model.bin |
学習済み 重み |
tokenizer.json 等 |
第3章のトークナイザ(モデルとセットで使う) |
霊夢
config だけ読めるの?
魔理沙
できる。中身を見ると「何層の Transformer か」が分かる。
from transformers import AutoConfig cfg = AutoConfig.from_pretrained("distilbert-base-uncased") print(cfg.model_type) # distilbert print(cfg.hidden_size) # 768 print(cfg.num_hidden_layers)
重みは別途ロードする。第1章で触れた チェックポイント は、この重みファイルの保存形式のことだ。
from transformers import AutoModelForSequenceClassification model = AutoModelForSequenceClassification.from_pretrained( "distilbert-base-uncased-finetuned-sst-2-english" ) print(model.config.id2label) # {0: 'NEGATIVE', 1: 'POSITIVE'}
霊夢
ラベル名まで config に入ってるんだ。
魔理沙
FT 済みモデルは id2label / label2id が載っていることが多い。推論結果の ID を人間が読める文字列に戻すときに使うのだZE。
4.3 device_map と GPU / CPU の使い分け
霊夢
GPU ないノート PC でもこの章はできる?
魔理沙
できる。小さな DistilBERT や GPT-2 なら CPU でも数秒〜数十秒だ。GPU があるなら .to("cuda") か device_map="auto" で載せ替える。
import torch from transformers import AutoModelForSequenceClassification device = "cuda" if torch.cuda.is_available() else "cpu" print("using device:", device) model = AutoModelForSequenceClassification.from_pretrained( "distilbert-base-uncased-finetuned-sst-2-english" ) model = model.to(device)
device_map="auto" は accelerate がレイヤーを GPU / CPU に自動配置する。大きなモデルや 8bit 量子化(ハンズオン 4-C)でよく使う。
# 大きめモデル向け(GPU 推奨) model = AutoModelForCausalLM.from_pretrained( "gpt2", device_map="auto", # 小さい gpt2 では1デバイスに収まる )
霊夢
入力テンソルも同じ device にしないと怒られるやつ?
魔理沙
その通り。第3章の input_ids も model.device に合わせる。pipeline 使うと自動だった部分を、手動 forward では自分で揃えるのだ。
4.4 推論モード(model.eval())と勾配計算を止める理由
霊夢
推論なのに eval() って学習の evaluate じゃないの?
魔理沙
PyTorch では model.eval() が「推論モード」スイッチだZE。Dropout を止めたり、BatchNorm の統計を固定したりする。学習時の model.train() の反対。
推論では 勾配(gradient) も要らない。メモリ節約と速度のため torch.no_grad() で囲む。
import torch model.eval() inputs = tokenizer("Hello HF", return_tensors="pt") with torch.no_grad(): outputs = model(**inputs) logits = outputs.logits
| 状況 | 使うもの |
|---|---|
| 推論・デモ | model.eval() + torch.no_grad() |
| ファインチューニング(第6章) | model.train() + 勾配 ON |
霊夢
第6章では no_grad 外すんだね。
魔理沙
Trainer が学習ループを面倒見てくれるから、自分で書く量は減る。ただ 推論スクリプト では今の2点セットを忘れないことだ。
4.5 生成(generate)の基本パラメータ
霊夢
GPT 系は forward じゃなく generate なの?
魔理沙
因果言語モデル(Causal LM)は 次の1トークンを繰り返し予測 して文を伸ばす。高レベル API が model.generate() だ。
よく触るパラメータ:
| パラメータ | 意味 |
|---|---|
max_new_tokens |
新しく足す トークン数の上限(プロンプト長は含まない) |
do_sample |
False=貪欲(毎回最大)、True=確率的サンプリング |
temperature |
サンプリング時のランダムさ(低いほど保守的) |
top_p |
累積確率 top-p( nucleus sampling) |
output_ids = model.generate(
input_ids,
max_new_tokens=40,
do_sample=True,
temperature=0.8,
top_p=0.95,
pad_token_id=tokenizer.eos_token_id,
)
text = tokenizer.decode(output_ids[0], skip_special_tokens=True)
霊夢
max_length との違いは?
魔理沙
max_length は 入力+出力の合計 上限。最近は max_new_tokens の方が分かりやすいので、本書ではこちらを推すのだZE。
4.6 ハンズオン 4 — Pipeline を外して Model を触る
霊夢
いよいよ手書き forward?
魔理沙
第4章の ハンズオン 4。A から順にやれば、第5章のデータ前処理 → 第6章の Trainer へ一直線につながる。
ハンズオン 4 — 手動 forward・生成・量子化(任意)
お題A Pipeline を使わず、分類モデルで logits → ラベル まで自分でやる
cd /path/to/yukkuri-hugging-face source .venv/bin/activate python scripts/ch04_forward.py
期待する出力の例:
input_ids shape: (1, 12) attention_mask shape: (1, 12) text: 'This course is surprisingly easy to follow!' predicted label: POSITIVE (confidence=0.9998) ...
霊夢
outputs.logits がそのまま出てきた。Pipeline が勝手にやってたのはこれか。
魔理沙
流れは encode → model(inputs) → softmax → argmax** だ。第3章のトークナイザ出力を **inputs でモデルに渡すのがポイントなのだ。
お題B GPT-2 で 温度・top_p・max_new_tokens を変えて生成を比較する
python scripts/ch04_generate.py
期待する出力の例:
[greedy (default)] Hugging Face lets you create your own models... ---------------------------------------- [temperature=0.9] Hugging Face lets you build a custom model...
同じプロンプトでも サンプリング設定 で文体が変わる。デモや創作 Bot ではここを調整するZE。
お題C(任意) 8bit 量子化 でメモリを節約してロードする
pip install bitsandbytes accelerate python scripts/ch04_quantize.py
GPU と bitsandbytes が揃っている環境では VRAM 使用量を抑えられる。CPU のみだと失敗することが多い——その場合はメッセージを読んで スキップして OK だ。
霊夢
4bit も目次にあった気がする…
魔理沙
原理は同じで load_in_4bit=True に変えるだけだ。第7章の QLoRA でまた出てくるから、ここでは 8bit で「量子化ロードの存在」を知っておけば十分なのだZE。
本章スクリプト全文
魔理沙
リポジトリがなくても手を動かせるよう、本章の 完成スクリプト をそのまま載せるのだ。
手元では scripts/ 以下と同じファイル名で保存して実行すればよいZE。
(python scripts/xxx.py と書いてある箇所は、保存先に合わせてパスを読み替えてくれ。)
scripts/ch04_forward.py
"""Chapter 4-A: Manual forward pass without pipeline.""" from __future__ import annotations import torch from transformers import AutoModelForSequenceClassification, AutoTokenizer def predict(text: str, model_id: str = "distilbert-base-uncased-finetuned-sst-2-english") -> None: tokenizer = AutoTokenizer.from_pretrained(model_id) model = AutoModelForSequenceClassification.from_pretrained(model_id) model.eval() inputs = tokenizer(text, return_tensors="pt", truncation=True, max_length=512) print("input_ids shape:", tuple(inputs["input_ids"].shape)) print("attention_mask shape:", tuple(inputs["attention_mask"].shape)) with torch.no_grad(): outputs = model(**inputs) logits = outputs.logits probs = torch.softmax(logits, dim=-1) pred_id = int(probs.argmax(dim=-1).item()) label = model.config.id2label[pred_id] confidence = float(probs[0, pred_id]) print(f"\ntext: {text!r}") print(f"predicted label: {label} (confidence={confidence:.4f})") print("all logits:", [round(x, 4) for x in logits[0].tolist()]) def main() -> None: samples = [ "This course is surprisingly easy to follow!", "I could not understand a single word.", ] for text in samples: predict(text) print("-" * 40) if __name__ == "__main__": main()
scripts/ch04_generate.py
"""Chapter 4-B: Experiment with model.generate() parameters.""" from __future__ import annotations import torch from transformers import AutoModelForCausalLM, AutoTokenizer def generate_once( model, tokenizer, prompt: str, *, max_new_tokens: int = 40, temperature: float = 1.0, top_p: float = 1.0, do_sample: bool = False, ) -> str: inputs = tokenizer(prompt, return_tensors="pt") input_ids = inputs["input_ids"].to(model.device) attention_mask = inputs["attention_mask"].to(model.device) gen_kwargs: dict = { "max_new_tokens": max_new_tokens, "pad_token_id": tokenizer.eos_token_id, } if do_sample: gen_kwargs.update( { "do_sample": True, "temperature": temperature, "top_p": top_p, } ) with torch.no_grad(): output_ids = model.generate(input_ids, attention_mask=attention_mask, **gen_kwargs) new_tokens = output_ids[0, input_ids.shape[1] :] continuation = tokenizer.decode(new_tokens, skip_special_tokens=True) return prompt + continuation def main() -> None: model_id = "gpt2" prompt = "Hugging Face lets you" tokenizer = AutoTokenizer.from_pretrained(model_id) model = AutoModelForCausalLM.from_pretrained(model_id) model.eval() settings = [ {"label": "greedy (default)", "do_sample": False, "max_new_tokens": 30}, {"label": "temperature=0.9", "do_sample": True, "temperature": 0.9, "top_p": 1.0, "max_new_tokens": 30}, {"label": "top_p=0.9", "do_sample": True, "temperature": 1.0, "top_p": 0.9, "max_new_tokens": 30}, {"label": "longer (max_new_tokens=80)", "do_sample": True, "temperature": 0.8, "top_p": 0.95, "max_new_tokens": 80}, ] for cfg in settings: label = cfg.pop("label") text = generate_once(model, tokenizer, prompt, **cfg) print(f"[{label}]") print(text) print("-" * 40) if __name__ == "__main__": main()
scripts/ch04_quantize.py
"""Chapter 4-C (optional): Load a model in 8-bit to save memory.""" from __future__ import annotations import torch from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig def load_model_8bit(model_id: str = "gpt2"): bnb_config = BitsAndBytesConfig(load_in_8bit=True) tokenizer = AutoTokenizer.from_pretrained(model_id) model = AutoModelForCausalLM.from_pretrained( model_id, quantization_config=bnb_config, device_map="auto", ) return tokenizer, model def main() -> None: model_id = "gpt2" prompt = "Quantized models use less VRAM when" try: tokenizer, model = load_model_8bit(model_id) except ImportError as exc: print("bitsandbytes is not installed. Install with:") print(" pip install bitsandbytes accelerate") print(f"detail: {exc}") return except Exception as exc: # noqa: BLE001 - demo script print("8-bit loading failed on this machine (CPU-only env is common).") print(f"detail: {exc}") return inputs = tokenizer(prompt, return_tensors="pt") input_ids = inputs["input_ids"].to(model.device) with torch.no_grad(): out = model.generate(input_ids, max_new_tokens=25, pad_token_id=tokenizer.eos_token_id) text = tokenizer.decode(out[0], skip_special_tokens=True) print("model dtype / device:", next(model.parameters()).dtype, next(model.parameters()).device) print("generated:", text) if __name__ == "__main__": main()
霊夢のメモ帳
AutoModelFor〜でタスク用ヘッドを選ぶ。分類はForSequenceClassification、生成はForCausalLM。- モデルは
config.json(構造) + 重みファイル。推論はeval()+no_grad()。 - 生成は
generateのmax_new_tokens/ temperature / top_p を調整する。
魔理沙の one more thing
output_hidden_states=True を付けると、各層の 中間表現 が取れる。埋め込み可視化や LLM 解釈の入口になるZE。
outputs = model(**inputs, output_hidden_states=True) last_hidden = outputs.hidden_states[-1] # (batch, seq, hidden) print(last_hidden.shape)
次章へ
魔理沙
モデルとトークナイザが揃った。次は 学習・評価に使うデータ だ。第5章では datasets で Hub や CSV から Dataset を作る。
霊夢
FT の前に、データの下ごしらえね。第3章の map でトークナイズするやつ、本番版って感じ?
魔理沙
その理解でバッチリだZE。では次回、第5章 Datasets — データの準備と前処理 だ。
第5章 Datasets — データの準備と前処理
5.1 datasets ライブラリの役割
霊夢
第4章でモデル本体は触った。でも FT するには 自分のデータ 要るよね?
魔理沙
その通りだZE。Hugging Face の datasets は、学習用データを 統一フォーマット で扱うライブラリだ。Hub から DL も、ローカル CSV も、同じ Dataset 型に乗せられる。
┌──────────────┐ load_dataset ┌─────────────┐
│ Hub / CSV │ ───────────────────► │ Dataset │
└──────────────┘ │ (行=例) │
└──────┬──────┘
│ map / filter
┌──────▼──────┐
│ 前処理済み │
│ input_ids… │
└─────────────┘
霊夢
pandas の DataFrame じゃダメなの?
魔理沙
DataFrame でも動くが、メモリ効率・キャッシュ・Trainer 連携 で Dataset が楽なことが多い。第6章の Trainer は datasets をそのまま受け取れるのだ。
必要パッケージ(第0章 venv 上で):
pip install datasets pandas python -c "import datasets; print(datasets.__version__)"
5.2 Hub からデータセットを読み込む
霊夢
Hub にはモデルだけじゃなく データセット もあるんだっけ。
魔理沙
あるZE。論文付属データやベンチマークが公開されている。基本は load_dataset 一行。
from datasets import load_dataset ds = load_dataset("ag_news") print(ds) # DatasetDict({'train': ..., 'test': ...})
| キー | 意味 |
|---|---|
train |
学習用 split |
test / validation |
評価用(データセットによる) |
| 行 | 辞書 { "text": "...", "label": 1 } のような Example |
霊夢
中身を覗くには?
魔理沙
インデックス、select、features を使う。
print(ds["train"].features) print(ds["train"][0]) print(ds["train"].select(range(3)))
初回は Hub から DL するので ネット接続 が必要だ。2回目以降はキャッシュ(後述 5.5)から読む。
5.3 map / filter / train_test_split
霊夢
全行に同じ処理をかけたいとき、for ループ?
魔理沙
map だZE。第3章でトークナイザに触れたときも、本番はデータセット全体に map をかける。
def add_prefix(example): example["text"] = "[NEWS] " + example["text"] return example ds = ds.map(add_prefix)
不要な行を落とす filter:
ds = ds.filter(lambda ex: len(ex["text"]) > 20)
学習用・検証用に分ける train_test_split:
small = ds["train"].shuffle(seed=42).select(range(1000)) split = small.train_test_split(test_size=0.1, seed=42) train_ds = split["train"] eval_ds = split["test"]
霊夢
batched=True って第3章にもあった。
魔理沙
トークナイズは バッチ単位 の方が速い。次節でまとめて書くのだ。
5.4 トークナイズ済みデータセットの作り方
霊夢
第3章の tokenizer(...) を各行に…?
魔理沙
関数を決めて map(batched=True) する。第4章のモデルが欲しいのは input_ids と attention_mask だ。
from transformers import AutoTokenizer tokenizer = AutoTokenizer.from_pretrained("distilbert-base-uncased") def tokenize_batch(examples): return tokenizer( examples["text"], truncation=True, max_length=128, padding=False, # パディングは DataCollator に任せる(第6章) ) tokenized = ds["train"].map(tokenize_batch, batched=True, remove_columns=["text"]) print(tokenized.column_names) # ['label', 'input_ids', 'attention_mask']
| 設計 | 理由 |
|---|---|
truncation=True |
長文を切る(第3章) |
padding=False で map |
可変長のまま保持し、学習時にバッチパディング |
remove_columns=["text"] |
生テキスト列を落としてメモリ節約 |
霊夢
label は残すんだ。
魔理沙
Trainer が 正解ラベル として使う。第6章で compute_metrics もここにつながるZE。
5.5 大きなデータを扱うときのコツ(ストリーミング、キャッシュ)
霊夢
AG News くらいなら平気そうだけど、巨大データセットは?
魔理沙
2つの武器がある。
1. ストリーミング — 全件 DL せず イテレータ として読む:
stream = load_dataset("large_corpus", split="train", streaming=True) for i, row in enumerate(stream): if i >= 3: break print(row)
2. キャッシュ — map の結果をディスクに保存。2回目以降が速い:
tokenized = ds.map(tokenize_batch, batched=True, load_from_cache_file=True)
キャッシュ場所は環境変数 HF_HOME / HF_DATASETS_CACHE で変えられる(第0章 one more thing 参照)。
霊夢
Colab でディスク溢れしたことある…
魔理沙
select(range(N)) で 部分集合 を使うのが本書の定番だ。第6章の FT も最初は数百件で回すZE。
5.6 ハンズオン 5 — 読込・前処理・CSV 変換
霊夢
自作データは CSV しかないこと多いよね。
魔理沙
ハンズオン 5 では Hub → 前処理 → CSV の順で体験する。C が第6章の --dataset local_csv にも繋がる。
ハンズオン 5 — Dataset を作って触る
お題A 公開データセット ag_news を読み込み、split・先頭行・ラベル名を表示
cd /path/to/yukkuri-hugging-face source .venv/bin/activate python scripts/ch05_load_dataset.py
期待する出力の例:
dataset: ag_news splits: ['train', 'test'] num_train: 120000 label=2 text='Wall St. Bears Claw Back ...' label names: ['World', 'Sports', 'Business', 'Sci/Tech']
お題B map でラベル名付与 → トークナイズ → 小さく split
python scripts/ch05_preprocess.py
columns after preprocess: ['label', 'label_name', 'input_ids', 'attention_mask']
small subset split sizes: {'train': 160, 'test': 40}
前処理関数は 本章末尾「本章スクリプト全文」の ch05_preprocess.py にまとめてある。第6章では同じ流れを Trainer に渡す。
お題C 付属 CSV を DatasetDict に変換
python scripts/ch05_csv_to_dataset.py
サンプル CSV の場所:
scripts/data/ch05_sample_reviews.csv
中身の例:
text,label "This tutorial is clear and fun.",positive "I got lost in the first section.",negative ...
霊夢
6行しかないけど、形は本番と同じね。
魔理沙
Dataset.from_pandas → train_test_split → 列名を Trainer 向けに揃える。自分の業務 CSV も text / label 列さえ揃えれば同じコードが使えるのだZE。
改造例(label 名を増やす):
# ch05_csv_to_dataset.py の label2id を自分のラベルに合わせて編集 label2id = {"negative": 0, "positive": 1, "neutral": 2}
本章スクリプト全文
魔理沙
リポジトリがなくても手を動かせるよう、本章の 完成スクリプト をそのまま載せるのだ。
手元では scripts/ 以下と同じファイル名で保存して実行すればよいZE。
(python scripts/xxx.py と書いてある箇所は、保存先に合わせてパスを読み替えてくれ。)
scripts/ch05_load_dataset.py
"""Chapter 5-A: Load a public dataset and inspect it.""" from __future__ import annotations from datasets import load_dataset def main() -> None: dataset_name = "ag_news" ds = load_dataset(dataset_name) print("dataset:", dataset_name) print("splits:", list(ds.keys())) print("features:", ds["train"].features) print("num_train:", len(ds["train"])) print("num_test:", len(ds["test"])) print("\n--- first 3 rows (train) ---") for row in ds["train"].select(range(3)): print(f"label={row['label']} text={row['text'][:80]!r}...") label_names = ds["train"].features["label"].names print("\nlabel names:", label_names) if __name__ == "__main__": main()
scripts/ch05_preprocess.py
"""Chapter 5-B: Build a reusable preprocessing pipeline with map().""" from __future__ import annotations from datasets import load_dataset from transformers import AutoTokenizer MODEL_ID = "distilbert-base-uncased" def tokenize_batch(examples, tokenizer, max_length: int = 128): return tokenizer( examples["text"], truncation=True, max_length=max_length, padding=False, ) def add_label_names(example, label_names): example["label_name"] = label_names[example["label"]] return example def main() -> None: ds = load_dataset("ag_news") label_names = ds["train"].features["label"].names tokenizer = AutoTokenizer.from_pretrained(MODEL_ID) ds = ds.map(add_label_names, fn_kwargs={"label_names": label_names}) tokenized = ds.map( lambda batch: tokenize_batch(batch, tokenizer), batched=True, remove_columns=["text"], desc="tokenizing", ) print("columns after preprocess:", tokenized["train"].column_names) print("sample row:") row = tokenized["train"][0] print(" label:", row["label"], "->", row["label_name"]) print(" input_ids length:", len(row["input_ids"])) print(" input_ids head:", row["input_ids"][:12]) small = tokenized["train"].shuffle(seed=42).select(range(200)) split = small.train_test_split(test_size=0.2, seed=42) print("\nsmall subset split sizes:", {k: len(v) for k, v in split.items()}) if __name__ == "__main__": main()
scripts/ch05_csv_to_dataset.py
"""Chapter 5-C: Convert a local CSV file into a Hugging Face Dataset.""" from __future__ import annotations from pathlib import Path import pandas as pd from datasets import Dataset, DatasetDict def csv_to_dataset(csv_path: Path) -> DatasetDict: df = pd.read_csv(csv_path) required = {"text", "label"} missing = required - set(df.columns) if missing: raise ValueError(f"CSV must contain columns: {sorted(required)}. Missing: {sorted(missing)}") label2id = {"negative": 0, "positive": 1} df["label_id"] = df["label"].map(label2id) if df["label_id"].isna().any(): bad = df[df["label_id"].isna()]["label"].unique().tolist() raise ValueError(f"Unknown labels in CSV: {bad}") dataset = Dataset.from_pandas(df[["text", "label_id"]], preserve_index=False) dataset = dataset.rename_column("label_id", "label") split = dataset.train_test_split(test_size=0.34, seed=42, stratify_by_column="label") return DatasetDict({"train": split["train"], "test": split["test"]}) def main() -> None: repo_root = Path(__file__).resolve().parents[1] csv_path = repo_root / "scripts" / "data" / "ch05_sample_reviews.csv" ds = csv_to_dataset(csv_path) print("loaded from:", csv_path) print("splits:", {name: len(split) for name, split in ds.items()}) print("features:", ds["train"].features) print("\ntrain rows:") for row in ds["train"]: label_name = "positive" if row["label"] == 1 else "negative" print(f" [{label_name}] {row['text']}") if __name__ == "__main__": main()
scripts/data/ch05_sample_reviews.csv(データファイル)
text,label "This tutorial is clear and fun.",positive "I got lost in the first section.",negative "The examples run on my laptop without GPU.",positive "Too many errors on Windows.",negative "Reimu and Marisa make HF less scary.",positive "I still do not know what logits are.",negative
霊夢のメモ帳
load_datasetで Hub から split 付き Dataset を取得。行は辞書形式の Example。- 前処理は
map/filter/ train_test_split**。トークナイズは **batched=True` が基本。 - 巨大データは ストリーミング と キャッシュ。本書の FT はまず 部分集合 で回す。
魔理沙の one more thing
複数 CSV や JSONL を 結合 するときは concatenate_datasets が便利だ。
from datasets import concatenate_datasets, load_dataset a = load_dataset("csv", data_files="part_a.csv")["train"] b = load_dataset("csv", data_files="part_b.csv")["train"] merged = concatenate_datasets([a, b]) print(len(merged))
次章へ
魔理沙
データとトークナイズの型が揃った。第6章では Trainer に渡して ファインチューニング する。
霊夢
ついに学習ループ… GPU なくても第0章のお題通り小さくやればいい?
魔理沙
800件・1 epoch なら CPU でも数十分以内のデモが現実的だZE。では次回、第6章 Trainer でファインチューニングする だ。
第6章 Trainer でファインチューニングする
6.1 ファインチューニングって何のためにするの?
霊夢
Hub のモデル、そのまま使えばいいのに わざわざ学習 する理由って?
魔理沙
汎用モデルは広いが、自分のドメイン・言い回し・ラベル定義 には弱いことがある。少量データで 重みを更新 し、タスクに合わせるのが ファインチューニング(FT) だZE。
| やり方 | 更新するもの | 本書での章 |
|---|---|---|
| 全パラメータ FT | モデル全体 | 第6章(本章) |
| LoRA / QLoRA | ごく一部のアダプタ | 第7章 |
霊夢
第4章の ForSequenceClassification を、第5章の Dataset で育てるイメージ?
魔理沙
バッチリだ。本章では 英語ニュース4分類(ag_news) を DistilBERT で FT する。CSV 派は --dataset local_csv も試せる(件数は少ないのでデモ向け)。
6.2 TrainingArguments の主要オプション
霊夢
学習ループ、自分で for 書かなくていいの?
魔理沙
Trainer + TrainingArguments に任せるのが Transformers 流だ。よく触る項目だけ表にするZE。
| オプション | 意味 |
|---|---|
output_dir |
チェックポイント・ログの保存先 |
learning_rate |
学習率(例: 5e-5) |
per_device_train_batch_size |
1 GPU/CPU あたりのバッチサイズ |
num_train_epochs |
データ全体を何周するか |
evaluation_strategy |
"epoch" など評価タイミング |
save_strategy |
チェックポイント保存タイミング |
logging_steps |
何 step ごとに loss を log するか |
report_to |
"tensorboard" / "wandb" 等 |
push_to_hub |
学習後 Hub に upload |
from transformers import TrainingArguments args = TrainingArguments( output_dir="log/ch06_finetune", learning_rate=5e-5, per_device_train_batch_size=8, num_train_epochs=1, evaluation_strategy="epoch", save_strategy="epoch", logging_steps=20, report_to=["tensorboard"], )
霊夢
バッチサイズ上げたら速くなる?
魔理沙
一般には 1 step あたりの処理量 が増える。ただし GPU メモリを超えると OOM だ。6.6 で再登場する。
6.3 Trainer の基本フロー(学習 → 評価 → 保存)
霊夢
Trainer に何を渡すの?
魔理沙
最低限これだ。
from transformers import Trainer, DataCollatorWithPadding trainer = Trainer( model=model, args=training_args, train_dataset=tokenized_train, eval_dataset=tokenized_eval, tokenizer=tokenizer, data_collator=DataCollatorWithPadding(tokenizer), compute_metrics=compute_metrics, ) trainer.train() metrics = trainer.evaluate() trainer.save_model("log/ch06_finetune/final")
流れ:
train_dataset ──► train() ──► チェックポイント
│
eval_dataset ──► evaluate() ──► accuracy 等
│
└──► save_model / push_to_hub
霊夢
DataCollatorWithPadding は第3章の話だ。
魔理沙
バッチ内で 可変長 input_ids を pad する。map 段階では pad せず、Collator に任せるのが定石なのだZE。
6.4 評価指標(accuracy、F1、perplexity など)
霊夢
loss だけじゃダメなの?
魔理沙
loss は最適化用。人間が読みやすい accuracy や F1 を別途出す。compute_metrics に関数を渡す。
import numpy as np def compute_metrics(eval_pred): logits, labels = eval_pred preds = np.argmax(logits, axis=-1) accuracy = (preds == labels).mean().item() return {"accuracy": accuracy}
| タスク | よく使う指標 |
|---|---|
| 分類 | accuracy, F1, precision, recall |
| 生成(LM) | perplexity, BLEU 等 |
| 回帰 | MSE, MAE |
本章のデモは accuracy のみ。本番では sklearn.metrics で F1 も足すとよい。
6.5 チェックポイント保存と Hub への push
霊夢
output_dir にフォルダが増えてく…
魔理沙
checkpoint-500 のような 途中保存 ができる。load_best_model_at_end=True なら評価が良かった重みを最後に残す。
Hub 公開は ログイン済み が前提(第1章)。トークンは .env や huggingface-cli login で管理し、コードに書かない。
huggingface-cli login # または .env に HF_TOKEN=... を置き huggingface_hub が読む
学習と同時に push:
python scripts/ch06_finetune.py \ --push-to-hub \ --hub-model-id YOUR_USERNAME/yukkuri-distilbert-ag-news-demo
既に保存した final/ だけ upload する場合:
python scripts/ch06_push_hub.py \ --hub-model-id YOUR_USERNAME/yukkuri-distilbert-ag-news-demo
霊夢
YOUR_USERNAME は自分のに置き換えね。
魔理沙
当たり前だZE。private リポジトリなら --private も付けられる。
6.6 過学習・学習率・バッチサイズの勘所
霊夢
accuracy 100% になった! 完璧?
魔理沙
訓練データだけ 見て喜ると危ない。eval が伸びず train だけ伸びたら 過学習 の疑いだ。
| 症状 | 試すこと |
|---|---|
| train loss ↓ eval 悪化 | epoch 減、データ増、weight decay |
| loss が振動 | 学習率を下げる(5e-5 → 2e-5) |
| CUDA OOM | batch size 半分、max_length 短く |
| 学習が遅い | GPU、小モデル(DistilBERT)、部分データ |
霊夢
第7章の LoRA はメモリ的にも助かる?
魔理沙
大規模 LLM では LoRA が主役になる。本章は 小さく全体 FT して Trainer の感覚を掴む段階だZE。
6.7 ハンズオン 6 — FT・可視化・Hub 公開
霊夢
いよいよ trainer.train() 押す日…
魔理沙
ハンズオン 6。A で学習、B でログ確認、C で Hub。初回はモデル DL で時間がかかる。
ハンズオン 6 — Trainer 実践
お題A ag_news の一部 で DistilBERT を 1 epoch FT
cd /path/to/yukkuri-hugging-face source .venv/bin/activate pip install tensorboard scikit-learn python scripts/ch06_finetune.py \ --max-train 800 \ --max-eval 200 \ --epochs 1 \ --batch-size 8 \ --output-dir log/ch06_finetune
期待する出力の例:
Starting fine-tuning...
{'loss': 0.85, 'epoch': 0.5}
...
train loss: 0.42
eval metrics: {'eval_accuracy': 0.89, ...}
saved to: log/ch06_finetune/final
学習後、保存モデルで推論:
from transformers import AutoModelForSequenceClassification, AutoTokenizer import torch model_dir = "log/ch06_finetune/final" tokenizer = AutoTokenizer.from_pretrained(model_dir) model = AutoModelForSequenceClassification.from_pretrained(model_dir) model.eval() text = "Stock markets rally after tech earnings beat expectations." inputs = tokenizer(text, return_tensors="pt") with torch.no_grad(): pred = model(**inputs).logits.argmax(-1).item() print(model.config.id2label[pred]) # Business など
お題B TensorBoard(または W&B)で学習曲線を見る
TensorBoard:
tensorboard --logdir log/ch06_finetune # ブラウザで http://localhost:6006
W&B を使う場合(任意):
pip install wandb export WANDB_API_KEY="your_wandb_key" # .env に置いてもよい python scripts/ch06_finetune.py \ --report-to wandb \ --output-dir log/ch06_wandb
WANDB_API_KEY が無いときはスクリプトが tensorboard にフォールバック する。
霊夢
loss の線が下がってれば一応成功?
魔理沙
eval accuracy も一緒に見るのが本番マインドだ。デモでは epoch 1 でも傾向が読めれば OK なのだZE。
お題C 学習済みモデルを Hub に公開(ログイン必須)
huggingface-cli login python scripts/ch06_finetune.py \ --max-train 800 \ --max-eval 200 \ --epochs 1 \ --push-to-hub \ --hub-model-id YOUR_USERNAME/yukkuri-distilbert-ag-news-demo
または保存済み final/ から:
python scripts/ch06_push_hub.py \ --hub-model-id YOUR_USERNAME/yukkuri-distilbert-ag-news-demo
公開後、Hub 上で Model Card を短く追記すると親切だ(第1章の読み方が活きる)。
ローカル CSV で試す場合(件数少・過学習しやすいデモ):
python scripts/ch06_finetune.py --dataset local_csv --epochs 3 --batch-size 2
本章スクリプト全文
魔理沙
リポジトリがなくても手を動かせるよう、本章の 完成スクリプト をそのまま載せるのだ。
手元では scripts/ 以下と同じファイル名で保存して実行すればよいZE。
(python scripts/xxx.py と書いてある箇所は、保存先に合わせてパスを読み替えてくれ。)
scripts/ch06_finetune.py
"""Chapter 6-A/B: Fine-tune a text classifier with Trainer.""" from __future__ import annotations import argparse import os from pathlib import Path import numpy as np from datasets import DatasetDict, load_dataset from transformers import ( AutoModelForSequenceClassification, AutoTokenizer, DataCollatorWithPadding, Trainer, TrainingArguments, set_seed, ) MODEL_ID = "distilbert-base-uncased" DEFAULT_DATASET = "ag_news" def build_datasets(dataset_name: str, max_train: int, max_eval: int) -> tuple[DatasetDict, list[str]]: if dataset_name == "local_csv": import sys scripts_dir = Path(__file__).resolve().parent if str(scripts_dir) not in sys.path: sys.path.insert(0, str(scripts_dir)) from ch05_csv_to_dataset import csv_to_dataset repo_root = Path(__file__).resolve().parents[1] csv_path = repo_root / "scripts" / "data" / "ch05_sample_reviews.csv" raw = csv_to_dataset(csv_path) label_names = ["negative", "positive"] else: raw = load_dataset(dataset_name) label_names = raw["train"].features["label"].names train = raw["train"].shuffle(seed=42).select(range(min(max_train, len(raw["train"])))) eval_split = raw["test"] if "test" in raw else raw["validation"] eval_ds = eval_split.shuffle(seed=42).select(range(min(max_eval, len(eval_split)))) return DatasetDict(train=train, eval=eval_ds), label_names def tokenize_dataset(ds: DatasetDict, tokenizer) -> DatasetDict: text_column = "text" def preprocess(batch): return tokenizer(batch[text_column], truncation=True) tokenized = {} for split_name, split_ds in ds.items(): tokenized[split_name] = split_ds.map( preprocess, batched=True, remove_columns=[text_column], ) return DatasetDict(tokenized) def compute_metrics(eval_pred): logits, labels = eval_pred preds = np.argmax(logits, axis=-1) accuracy = (preds == labels).mean().item() return {"accuracy": accuracy} def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Chapter 6 fine-tuning demo") parser.add_argument("--dataset", default=DEFAULT_DATASET, help="HF dataset name or local_csv") parser.add_argument("--max-train", type=int, default=800, help="subset size for quick runs") parser.add_argument("--max-eval", type=int, default=200) parser.add_argument("--epochs", type=int, default=1) parser.add_argument("--batch-size", type=int, default=8) parser.add_argument("--lr", type=float, default=5e-5) parser.add_argument("--output-dir", default="log/ch06_finetune") parser.add_argument("--report-to", default="tensorboard", choices=["tensorboard", "wandb", "none"]) parser.add_argument("--push-to-hub", action="store_true", help="requires HF login and --hub-model-id") parser.add_argument("--hub-model-id", default="", help="e.g. yourname/yukkuri-distilbert-ag-news-demo") return parser.parse_args() def main() -> None: args = parse_args() set_seed(42) raw, label_names = build_datasets(args.dataset, args.max_train, args.max_eval) num_labels = len(label_names) tokenizer = AutoTokenizer.from_pretrained(MODEL_ID) tokenized = tokenize_dataset(raw, tokenizer) model = AutoModelForSequenceClassification.from_pretrained( MODEL_ID, num_labels=num_labels, id2label={i: name for i, name in enumerate(label_names)}, label2id={name: i for i, name in enumerate(label_names)}, ) report_to = [] if args.report_to == "none" else [args.report_to] if args.report_to == "wandb" and not os.getenv("WANDB_API_KEY"): print("WANDB_API_KEY is not set. Falling back to tensorboard.") report_to = ["tensorboard"] output_dir = Path(args.output_dir) output_dir.mkdir(parents=True, exist_ok=True) training_args = TrainingArguments( output_dir=str(output_dir), learning_rate=args.lr, per_device_train_batch_size=args.batch_size, per_device_eval_batch_size=args.batch_size, num_train_epochs=args.epochs, evaluation_strategy="epoch", save_strategy="epoch", logging_steps=20, report_to=report_to, load_best_model_at_end=True, metric_for_best_model="accuracy", push_to_hub=args.push_to_hub, hub_model_id=args.hub_model_id or None, ) trainer = Trainer( model=model, args=training_args, train_dataset=tokenized["train"], eval_dataset=tokenized["eval"], tokenizer=tokenizer, data_collator=DataCollatorWithPadding(tokenizer), compute_metrics=compute_metrics, ) print("Starting fine-tuning...") train_result = trainer.train() metrics = trainer.evaluate() print("train loss:", round(train_result.training_loss, 4)) print("eval metrics:", {k: round(v, 4) if isinstance(v, float) else v for k, v in metrics.items()}) save_dir = output_dir / "final" trainer.save_model(save_dir) tokenizer.save_pretrained(save_dir) print("saved to:", save_dir) if args.push_to_hub: if not args.hub_model_id: raise ValueError("--push-to-hub requires --hub-model-id") trainer.push_to_hub() print("pushed to Hub:", args.hub_model_id) if __name__ == "__main__": main()
scripts/ch06_push_hub.py
"""Chapter 6-C helper: Push an already fine-tuned local checkpoint to the Hub.""" from __future__ import annotations import argparse from pathlib import Path from transformers import AutoModelForSequenceClassification, AutoTokenizer def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Upload a local fine-tuned checkpoint to HF Hub") parser.add_argument( "--checkpoint-dir", default="log/ch06_finetune/final", help="directory saved by ch06_finetune.py", ) parser.add_argument( "--hub-model-id", required=True, help="target repo id, e.g. yourname/yukkuri-distilbert-demo", ) parser.add_argument("--private", action="store_true", help="create/use a private repo") return parser.parse_args() def main() -> None: args = parse_args() checkpoint = Path(args.checkpoint_dir) if not checkpoint.exists(): raise FileNotFoundError( f"Checkpoint not found: {checkpoint}. Run ch06_finetune.py first." ) model = AutoModelForSequenceClassification.from_pretrained(checkpoint) tokenizer = AutoTokenizer.from_pretrained(checkpoint) model.push_to_hub(args.hub_model_id, private=args.private) tokenizer.push_to_hub(args.hub_model_id, private=args.private) print("upload complete:", f"https://huggingface.co/{args.hub_model_id}") if __name__ == "__main__": main()
霊夢のメモ帳
- FT は 汎用モデルを自分のタスクに合わせる 追加学習。本章は Trainer + 全パラメータ。
TrainingArgumentsで LR・batch・epoch・ログ先を決め、compute_metricsで accuracy 等を返す。save_model/ push_to_hub で共有。過学習は train と eval を両方 見て判断する。
魔理沙の one more thing
EarlyStoppingCallback を Trainer に足すと、eval が伸び止まったら学習を止められる。
from transformers import EarlyStoppingCallback trainer = Trainer( ..., callbacks=[EarlyStoppingCallback(early_stopping_patience=2)], )
load_best_model_at_end=True とセットで使うと、無駄な epoch を減らせる ZE。
次章へ
魔理沙
本章で「データ → Trainer → 保存」まで一通りやった。第7章では LoRA で、大きなモデルでも軽く FT する道に入る。
霊夢
全パラメータ更新、GPU ピンチだったもんね…
魔理沙
peft でアダプタだけ学習すれば、メモリと時間を大幅に削れるのだ。では次回、第7章 PEFT / LoRA — 少ない GPU でも FT する だ。
第7章 PEFT / LoRA — 少ない GPU でも FT する
7.1 全パラメータ更新 vs 部分更新
霊夢
第6章で Trainer を回したけど、GPU メモリがギリギリだったのよね…。全部の重みを更新するの、そんなに重いの?
魔理沙
第6章の フルファインチューニング(Full FT) は、モデルの 全パラメータ に勾配が流れる。7B クラスの LLM だと、重み + 勾配 + オプティマイザ状態で 数十 GB 級になることもあるZE。
霊夢
うちらの distil 系みたいな小さいモデルでも?
魔理沙
小さいほどマシだが、「全部更新」 という点は同じだ。本書の流れはこう覚えろ。
| 方式 | 更新する部分 | メモリ | 典型用途 |
|---|---|---|---|
| Full FT(第6章) | 全レイヤー | 大 | データが十分・性能を最大まで上げたい |
| 部分更新 / PEFT(本章) | アダプタだけ | 小 | GPU が限られる、複数タスクを切り替えたい |
| 推論のみ(第8章) | なし | 最小 | 本番配信 |
第6章 Trainer(Full FT)
│
▼ 「重い… LoRA ない?」
第7章 PEFT / LoRA ← 今ここ
│
▼
第8章 推論最適化
霊夢
部分更新って、モデルの端っこだけいじるイメージ?
魔理沙
その通りだ。PEFT(Parameter-Efficient Fine-Tuning)は、学習可能パラメータを 1〜数 % に抑える手法の総称だZE。本章の主役は LoRA(Low-Rank Adaptation)だ。
7.2 LoRA / QLoRA の考え方
霊夢
LoRA… 低ランク? 行列の話?
魔理沙
本質だけ言う。凍結した重み W に、小さな更新 ΔW を足す。ΔW を巨大行列そのまま持つのではなく、A × B の低ランク分解で近似するのだ。
元の重み W(凍結・更新しない)
+
LoRA 更新 ΔW ≈ B @ A (A, B は rank=r の小行列)
=
推論時の実効重み W + ΔW
| 用語 | 意味 |
|---|---|
| rank(r) | LoRA の表現力。大きいほど容量↑ メモリ↑ |
| lora_alpha | 更新のスケール。よく alpha / r が実効倍率 |
| target_modules | LoRA を載せる層(Attention の q_proj など) |
| QLoRA | ベースを 4bit 量子化 + LoRA。VRAM をさらに削る |
霊夢
QLoRA は Colab 向け?
魔理沙
VRAM が厳しいときの定番だZE。bitsandbytes で 4bit 読み込み + LoRA 学習。本章のスクリプトは 通常 LoRA(FP16/FP32)で動くが、発想は同じだ。
QLoRA の最小イメージ(参考・任意):
from transformers import AutoModelForCausalLM, BitsAndBytesConfig from peft import LoraConfig, get_peft_model bnb_config = BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_quant_type="nf4", bnb_4bit_compute_dtype="float16", ) model = AutoModelForCausalLM.from_pretrained( "meta-llama/Llama-3.2-1B", quantization_config=bnb_config, device_map="auto", ) # 続けて LoraConfig → get_peft_model(本章 7.3 と同じ流れ)
霊夢
第6章の TrainingArguments はそのまま使える?
魔理沙
使える。Trainer + PEFT モデル の組み合わせが定番だZE。変わるのは「get_peft_model でラップしたモデルを渡す」部分だけだ。
7.3 peft ライブラリの使い方
霊夢
pip install peft するだけ?
魔理沙
第6章までの環境に peft と accelerate を足す。初回は distilgpt2 の DL も走る。
cd /path/to/yukkuri-hugging-face source .venv/bin/activate pip install "peft>=0.11" "accelerate>=0.30"
LoRA 設定の最小パターン:
from peft import LoraConfig, TaskType, get_peft_model lora_config = LoraConfig( task_type=TaskType.CAUSAL_LM, r=8, lora_alpha=16, lora_dropout=0.05, target_modules=["c_attn", "c_proj"], # distilgpt2 向け bias="none", ) model = get_peft_model(base_model, lora_config) model.print_trainable_parameters()
期待する出力の例(モデルにより数値は変動):
trainable params: 294,912 || all params: 82,316,544 || trainable%: 0.36
霊夢
0.36 % だけ!?
魔理沙
だから 少ない GPU でも FT しやすいのだZE。target_modules はモデルアーキテクチャごとに変わる。わからなければ Hub のモデルカードや PEFT 例を参照しろ。
7.4 ベースモデルにアダプタを載せて推論する
霊夢
学習後、推論するときはどう読み込むの?
魔理沙
2段階だ。ベース → アダプタ の順。
from peft import PeftModel from transformers import AutoModelForCausalLM, AutoTokenizer base_id = "distilgpt2" adapter_dir = "log/ch07_lora_adapter/adapter" tokenizer = AutoTokenizer.from_pretrained(adapter_dir) base = AutoModelForCausalLM.from_pretrained(base_id) model = PeftModel.from_pretrained(base, adapter_dir) prompt = "Reimu: What is LoRA?\nMarisa:" inputs = tokenizer(prompt, return_tensors="pt") outputs = model.generate(**inputs, max_new_tokens=40) print(tokenizer.decode(outputs[0], skip_special_tokens=True))
| 保存物 | 中身 | サイズ感(目安) |
|---|---|---|
| ベースモデル | 全重み | distilgpt2 なら ~350 MB |
| LoRA アダプタのみ | A, B 行列 + config | 数 MB 程度 |
| Full FT チェックポイント | 全重みのコピー | ベースと同等 |
霊夢
Hub に上げるならアダプタだけでいいのね。
魔理沙
その通りだZE。ベースは Hub 上の公開モデルを指す base_model_name_or_path を adapter_config.json に書いておけば、読者はベース DL + アダプタ DL だけで再現できる。
7.5 ハンズオン 7 — LoRA で軽く FT する
霊夢
やっと手を動かす章?
魔理沙
第7章 ハンズオン 7 だ。因果言語モデル distilgpt2 に LoRA を載せ、第6章と同じ Trainer 流儀で学習する。
ハンズオン 7 — PEFT / LoRA 3 本立て
お題A LoRA で因果言語モデルを軽く FT する
python scripts/ch07_lora_train.py
オプション例(rank を変えて実験):
python scripts/ch07_lora_train.py --rank 16 --epochs 5 --output log/ch07_lora_r16
期待する出力の例:
Loading base model: distilgpt2 trainable params: ... || trainable%: 0.3x Starting LoRA training... Saved LoRA adapter to: log/ch07_lora_adapter/adapter OK: Chapter 7-A LoRA training complete
お題B アダプタだけ保存・共有する
python scripts/ch07_lora_save_adapter.py
アダプタのメタデータとサンプル生成を確認し、log/ch07_lora_adapter/export/ に共有用ファイルを書き出す。
期待する出力の例:
PeftConfig: peft_type: LORA base_model_name_or_path: distilgpt2 r (rank): 8 adapter size: 2.xx MB Sample generation with adapter: Reimu: What did we learn about LoRA? Marisa: ... OK: Chapter 7-B adapter save / inspect complete
Hub に push する場合(トークンは .env や huggingface-cli login 済み前提。章内に実トークンを書かない):
# 例: 自分のユーザー名に置き換える huggingface-cli upload your-username/yukkuri-ch07-lora-demo log/ch07_lora_adapter/export
お題C 複数 LoRA を切り替えて比較する
# 2 つ目のデモアダプタを自動作成して比較 python scripts/ch07_lora_switch.py --create-demo-b
期待する出力の例:
=== Adapter A === Reimu: Explain LoRA in one sentence. Marisa: ... === Adapter B === Reimu: Explain LoRA in one sentence. Marisa: ... OK: Chapter 7-C adapter switch complete
霊夢
A と B で文が違う… rank 変えるとこうなるのか。
魔理沙
データも rank も違えば出力も変わる。第10章の総合プロジェクトでは、ゆっくり口調 LoRA をこう切り替えたりマージしたりするZE。まずは「ベース固定 + アダプタ差し替え」を体に覚えさせろ。
7.6 よくあるエラー
霊夢
つまずきポイント、先に教えて。
魔理沙
表にまとめた。
| エラー / 症状 | 原因 | 対処 |
|---|---|---|
No module named 'peft' |
未インストール | pip install peft |
target_modules で KeyError |
層名がモデルと不一致 | モデルの Linear 名を確認 |
| CUDA OOM | rank / batch が大きい | --rank 4、batch size を下げる |
| アダプタが見つからない | 7-A 未実行 | ch07_lora_train.py を先に |
| 生成が base と同じ | 学習不足 or 未ロード | epoch 増、PeftModel 読込を確認 |
本章スクリプト全文
魔理沙
リポジトリがなくても手を動かせるよう、本章の 完成スクリプト をそのまま載せるのだ。
手元では scripts/ 以下と同じファイル名で保存して実行すればよいZE。
(python scripts/xxx.py と書いてある箇所は、保存先に合わせてパスを読み替えてくれ。)
scripts/ch07_lora_train.py
"""Chapter 7-A: LoRA fine-tuning for a small causal language model.""" from __future__ import annotations import argparse from pathlib import Path from datasets import Dataset from peft import LoraConfig, TaskType, get_peft_model from transformers import ( AutoModelForCausalLM, AutoTokenizer, DataCollatorForLanguageModeling, Trainer, TrainingArguments, ) DEFAULT_MODEL = "distilgpt2" DEFAULT_OUTPUT = "log/ch07_lora_adapter" def build_tiny_dataset() -> Dataset: """Minimal instruction-style snippets for demo fine-tuning.""" texts = [ "Reimu: Hugging Face Hub is like GitHub for AI models.\n" "Marisa: Push your LoRA adapter and share it with the world!", "Reimu: LoRA trains only a small adapter, not the whole model.\n" "Marisa: That saves GPU memory and disk space, ze!", "Reimu: Can I fine-tune on my laptop?\n" "Marisa: With PEFT and a tiny model, yes you can!", "Reimu: What is a tokenizer again?\n" "Marisa: It turns text into token IDs the model understands.", "Reimu: Trainer saved a checkpoint last chapter.\n" "Marisa: This chapter we attach LoRA and train even lighter!", ] return Dataset.from_dict({"text": texts}) def tokenize_dataset(tokenizer: AutoTokenizer, dataset: Dataset) -> Dataset: def tokenize(batch: dict) -> dict: return tokenizer( batch["text"], truncation=True, max_length=128, padding="max_length", ) return dataset.map(tokenize, batched=True, remove_columns=["text"]) def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="LoRA fine-tune distilgpt2") parser.add_argument("--model", default=DEFAULT_MODEL, help="Base model id") parser.add_argument( "--output", default=DEFAULT_OUTPUT, help="Directory to save LoRA adapter", ) parser.add_argument("--epochs", type=int, default=3, help="Training epochs") parser.add_argument("--lr", type=float, default=2e-4, help="Learning rate") parser.add_argument( "--rank", type=int, default=8, help="LoRA rank (r). Higher = more capacity, more memory", ) return parser.parse_args() def main() -> None: args = parse_args() output_dir = Path(args.output) output_dir.mkdir(parents=True, exist_ok=True) print(f"Loading base model: {args.model}") tokenizer = AutoTokenizer.from_pretrained(args.model) if tokenizer.pad_token is None: tokenizer.pad_token = tokenizer.eos_token model = AutoModelForCausalLM.from_pretrained(args.model) lora_config = LoraConfig( task_type=TaskType.CAUSAL_LM, r=args.rank, lora_alpha=16, lora_dropout=0.05, target_modules=["c_attn", "c_proj"], bias="none", ) model = get_peft_model(model, lora_config) model.print_trainable_parameters() dataset = tokenize_dataset(tokenizer, build_tiny_dataset()) training_args = TrainingArguments( output_dir=str(output_dir / "checkpoints"), num_train_epochs=args.epochs, per_device_train_batch_size=2, learning_rate=args.lr, logging_steps=1, save_strategy="no", report_to="none", use_cpu=not __import__("torch").cuda.is_available(), ) trainer = Trainer( model=model, args=training_args, train_dataset=dataset, data_collator=DataCollatorForLanguageModeling( tokenizer=tokenizer, mlm=False, ), ) print("Starting LoRA training...") trainer.train() adapter_path = output_dir / "adapter" model.save_pretrained(adapter_path) tokenizer.save_pretrained(adapter_path) print(f"Saved LoRA adapter to: {adapter_path}") print("OK: Chapter 7-A LoRA training complete") if __name__ == "__main__": main()
scripts/ch07_lora_save_adapter.py
"""Chapter 7-B: Save and inspect a LoRA adapter without the full base weights.""" from __future__ import annotations import argparse import json from pathlib import Path from peft import PeftConfig, PeftModel from transformers import AutoModelForCausalLM, AutoTokenizer DEFAULT_BASE = "distilgpt2" DEFAULT_ADAPTER = "log/ch07_lora_adapter/adapter" def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Save / inspect LoRA adapter") parser.add_argument("--base", default=DEFAULT_BASE, help="Base model id") parser.add_argument( "--adapter", default=DEFAULT_ADAPTER, help="Path to LoRA adapter directory", ) parser.add_argument( "--export", default="log/ch07_lora_adapter/export", help="Directory to copy adapter metadata for sharing", ) return parser.parse_args() def adapter_size_mb(adapter_dir: Path) -> float: total = sum(f.stat().st_size for f in adapter_dir.rglob("*") if f.is_file()) return total / (1024 * 1024) def main() -> None: args = parse_args() adapter_dir = Path(args.adapter) export_dir = Path(args.export) if not adapter_dir.exists(): raise SystemExit( f"Adapter not found: {adapter_dir}\n" "Run scripts/ch07_lora_train.py first (Chapter 7-A)." ) config = PeftConfig.from_pretrained(adapter_dir) print("PeftConfig:") print(f" peft_type: {config.peft_type}") print(f" base_model_name_or_path: {config.base_model_name_or_path}") print(f" r (rank): {config.r}") print(f" target_modules: {config.target_modules}") print(f" adapter size: {adapter_size_mb(adapter_dir):.2f} MB") tokenizer = AutoTokenizer.from_pretrained(adapter_dir) base_model = AutoModelForCausalLM.from_pretrained(args.base) model = PeftModel.from_pretrained(base_model, adapter_dir) prompt = "Reimu: What did we learn about LoRA?\nMarisa:" inputs = tokenizer(prompt, return_tensors="pt") outputs = model.generate(**inputs, max_new_tokens=40, do_sample=False) text = tokenizer.decode(outputs[0], skip_special_tokens=True) print("\nSample generation with adapter:") print(text) export_dir.mkdir(parents=True, exist_ok=True) for name in ("adapter_config.json", "adapter_model.safetensors"): src = adapter_dir / name if src.exists(): (export_dir / name).write_bytes(src.read_bytes()) readme = { "title": "yukkuri-hf-ch07-lora-demo", "base_model": args.base, "task": "causal_lm", "notes": "Adapter-only export from Chapter 7-B. Upload this folder to Hub.", } (export_dir / "adapter_card.json").write_text( json.dumps(readme, indent=2), encoding="utf-8" ) print(f"\nExported shareable adapter files to: {export_dir}") print("OK: Chapter 7-B adapter save / inspect complete") if __name__ == "__main__": main()
scripts/ch07_lora_switch.py
"""Chapter 7-C: Switch between multiple LoRA adapters on one base model.""" from __future__ import annotations import argparse from pathlib import Path import torch from peft import LoraConfig, PeftModel, TaskType, get_peft_model from transformers import AutoModelForCausalLM, AutoTokenizer DEFAULT_BASE = "distilgpt2" DEFAULT_ADAPTER_A = "log/ch07_lora_adapter/adapter" DEFAULT_ADAPTER_B = "log/ch07_lora_adapter_b/adapter" def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Compare LoRA adapters") parser.add_argument("--base", default=DEFAULT_BASE) parser.add_argument("--adapter-a", default=DEFAULT_ADAPTER_A) parser.add_argument("--adapter-b", default=DEFAULT_ADAPTER_B) parser.add_argument( "--create-demo-b", action="store_true", help="Create a second demo adapter if missing (different rank)", ) parser.add_argument( "--prompt", default="Reimu: Explain LoRA in one sentence.\nMarisa:", ) return parser.parse_args() def generate(model, tokenizer, prompt: str) -> str: inputs = tokenizer(prompt, return_tensors="pt") device = next(model.parameters()).device inputs = {k: v.to(device) for k, v in inputs.items()} with torch.inference_mode(): outputs = model.generate(**inputs, max_new_tokens=50, do_sample=False) return tokenizer.decode(outputs[0], skip_special_tokens=True) def create_demo_adapter_b(base_model_id: str, output: Path) -> None: """Build a tiny second adapter so comparison works out of the box.""" from datasets import Dataset from transformers import DataCollatorForLanguageModeling, Trainer, TrainingArguments output.mkdir(parents=True, exist_ok=True) tokenizer = AutoTokenizer.from_pretrained(base_model_id) if tokenizer.pad_token is None: tokenizer.pad_token = tokenizer.eos_token model = AutoModelForCausalLM.from_pretrained(base_model_id) lora_config = LoraConfig( task_type=TaskType.CAUSAL_LM, r=4, lora_alpha=8, lora_dropout=0.05, target_modules=["c_attn"], bias="none", ) model = get_peft_model(model, lora_config) texts = [ "Reimu: Spaces can host Gradio demos.\n" "Marisa: Share your model with a URL, easy!", "Reimu: Batch inference improves throughput.\n" "Marisa: Measure before you optimize, ze!", ] ds = Dataset.from_dict({"text": texts}) def tokenize(batch): return tokenizer( batch["text"], truncation=True, max_length=64, padding="max_length", ) ds = ds.map(tokenize, batched=True, remove_columns=["text"]) trainer = Trainer( model=model, args=TrainingArguments( output_dir=str(output / "tmp"), num_train_epochs=2, per_device_train_batch_size=2, learning_rate=3e-4, logging_steps=1, save_strategy="no", report_to="none", use_cpu=not torch.cuda.is_available(), ), train_dataset=ds, data_collator=DataCollatorForLanguageModeling(tokenizer, mlm=False), ) trainer.train() model.save_pretrained(output) tokenizer.save_pretrained(output) print(f"Created demo adapter B at {output}") def main() -> None: args = parse_args() adapter_a = Path(args.adapter_a) adapter_b = Path(args.adapter_b) if args.create_demo_b and not adapter_b.exists(): create_demo_adapter_b(args.base, adapter_b) if not adapter_a.exists(): raise SystemExit( f"Adapter A not found: {adapter_a}\n" "Run scripts/ch07_lora_train.py first." ) if not adapter_b.exists(): raise SystemExit( f"Adapter B not found: {adapter_b}\n" "Run with --create-demo-b or train a second adapter." ) tokenizer = AutoTokenizer.from_pretrained(adapter_a) if tokenizer.pad_token is None: tokenizer.pad_token = tokenizer.eos_token print(f"Base model: {args.base}") print(f"Prompt:\n{args.prompt}\n") base = AutoModelForCausalLM.from_pretrained(args.base) model_a = PeftModel.from_pretrained(base, adapter_a) print("=== Adapter A ===") print(generate(model_a, tokenizer, args.prompt)) base = AutoModelForCausalLM.from_pretrained(args.base) model_b = PeftModel.from_pretrained(base, adapter_b) print("\n=== Adapter B ===") print(generate(model_b, tokenizer, args.prompt)) print("\nTip: use model.load_adapter() / set_adapter() to hot-swap at runtime.") print("OK: Chapter 7-C adapter switch complete") if __name__ == "__main__": main()
霊夢のメモ帳
- 第6章の Full FT に対し、LoRA は ごく一部のパラメータ だけ学習して VRAM と保存サイズを抑える。
LoraConfig→get_peft_model→Trainerの流れは第6章と同じ。推論はPeftModel.from_pretrained。- Hub 共有は アダプタのみ でも OK。ベースモデル ID を config に残す。
魔理沙の one more thing
複数 LoRA を 1 つのベースに同時ロード して set_adapter() で切り替えると、A/B テストが速いZE。
from peft import PeftModel base = AutoModelForCausalLM.from_pretrained("distilgpt2") model = PeftModel.from_pretrained(base, "log/ch07_lora_adapter/adapter") model.load_adapter("log/ch07_lora_adapter_b/adapter", adapter_name="style_b") model.set_adapter("style_b")
次章へ
魔理沙
LoRA で「学習を軽くする」は押さえた。次は 第8章 推論を速く・安く・運用向きにする だ。バッチサイズ計測や Docker 推論の入口に進むZE。
霊夢
学習より、本番で速く回す方?
魔理沙
その通り。FT したモデルも、配信するときは スループットとコスト が問題になる。計測できる人だけが速くなるのだ。
第8章 推論を速く・安く・運用向きにする
8.1 バッチ推論とスループット
霊夢
第7章で LoRA まで学んだけど、本番でモデル配るとき「遅い!」って言われない?
魔理沙
その不安が第8章のテーマだZE。推論(Inference) は学習とは別の最適化ゲーム。まず押さえるのが バッチ推論 と スループット だ。
| 用語 | 意味 |
|---|---|
| レイテンシ(latency) | 1 リクエストが返るまでの時間 |
| スループット(throughput) | 単位時間あたり処理できる件数(samples/sec など) |
| バッチサイズ | 一度にまとめて forward する件数 |
リクエスト1 ─┐
リクエスト2 ─┼─► [バッチ forward] ─► 結果1..N
リクエスト3 ─┘
↑
GPU をまとめて使う → スループット↑(代わりに1件の待ち時間は↑することも)
霊夢
1件ずつ投げるより、まとめた方が速い?
魔理沙
GPU では まとめた方がスループットが上がることが多い。ただしバッチを大きくしすぎると メモリ不足 や 1件あたりの遅延 が悪化する。計測して決める のが正解だ。
計測用スクリプトの骨格:
import time import torch model.eval() with torch.inference_mode(): start = time.perf_counter() outputs = model(**batch_inputs) if torch.cuda.is_available(): torch.cuda.synchronize() elapsed = time.perf_counter() - start
霊夢
synchronize って何?
魔理沙
CUDA は非同期実行だ。止めずに時間測ると 早すぎる嘘 が出る。GPU 完了を待ってから止めるのだZE。
8.2 transformers の最適化オプション概要
霊夢
ライブラリ側にも「速くして」スイッチあるの?
魔理沙
ある。本書で触れる範囲を表にした。
| 手法 | 概要 | 本書での位置づけ |
|---|---|---|
torch.inference_mode() |
勾配不要な推論専用モード | 必須の基本 |
model.eval() |
Dropout 等を推論用に | 必須の基本 |
device_map="auto" |
複数 GPU / CPU オフロード | 第4章で触れた |
| 半精度 FP16 / BF16 | メモリ↓ 速度↑ | GPU 向け |
torch.compile(PyTorch 2+) |
グラフ最適化 | 環境が合えば試す |
| BetterTransformer / SDPA | Attention 実装の高速化 | transformers 内部で自動選択も |
推論ラッパーの例:
import torch from transformers import AutoModelForSequenceClassification model = AutoModelForSequenceClassification.from_pretrained( "distilbert-base-uncased-finetuned-sst-2-english" ) model.eval() if torch.cuda.is_available(): model = model.to("cuda", dtype=torch.float16) with torch.inference_mode(): outputs = model(**inputs)
霊夢
学習中は training モード、配信は eval モードね。
魔理沙
第6章 Trainer が内部で切り替えてくれるが、自分で書く推論コード では明示が必要だZE。
8.3 ONNX / Optimum への触れ方
霊夢
ONNX… 名前だけ聞いたことある。
魔理沙
ONNX はモデルを フレームワーク横断 の形式にエクスポートする標準だ。Hugging Face では optimum が ONNX 変換の窓口になる。
pip install "optimum[onnxruntime]"
CLI での export 例(分類モデル):
optimum-cli export onnx \ --model distilbert-base-uncased-finetuned-sst-2-english \ log/ch08_onnx_distilbert
Python から ONNX Runtime で推論:
from optimum.onnxruntime import ORTModelForSequenceClassification from transformers import AutoTokenizer model_id = "distilbert-base-uncased-finetuned-sst-2-english" onnx_dir = "log/ch08_onnx_distilbert" # 初回: export 先を指定。2 回目以降は onnx_dir から直接 load 可 tokenizer = AutoTokenizer.from_pretrained(model_id) model = ORTModelForSequenceClassification.from_pretrained( model_id, export=True ) # model.save_pretrained(onnx_dir) # キャッシュしたいとき
霊夢
いつ ONNX にするの?
魔理沙
CPU 本番 や エッジ端末、固定グラフで高速化 したいときだ。一方で LLM 生成 は vLLM / TGI など別スタックが主流になりやすい。タスクとデプロイ先で選べ。
8.4 vLLM / TGI などサーバー推論の選択肢(概要)
霊夢
Gradio 以外に「サーバー」って選択肢もある?
魔理沙
大規模 テキスト生成 を API として配るなら、専用サーバーが定番だZE。
| 製品 / プロジェクト | 特徴 |
|---|---|
| Text Generation Inference(TGI) | HF 公式。LLM 向け Docker イメージ |
| vLLM | PagedAttention で高スループット LLM 推論 |
| llama.cpp / Ollama | ローカル CPU / 小型 GPU 向け(発展) |
アーキテクチャのイメージ:
クライアント(Gradio / アプリ)
│ HTTP / gRPC
▼
┌───────────────────────┐
│ TGI / vLLM サーバー │ ← バッチング・KV キャッシュ
└───────────┬───────────┘
▼
GPU クラスタ
霊夢
第9章の Gradio から TGI に繋ぐ感じ?
魔理沙
デモは Gradio、本番トラフィックは TGI、という 役割分担 も多い。本章では 概念と Docker の入口 まで。深掘りは付録リンクに回すZE。
8.5 コストとレイテンシのトレードオフ
霊夢
速くすればいいってもんじゃないの?
魔理沙
速い GPU は 高い。常時起動 vs オンデマンド、バッチ vs リアルタイムで最適解が変わる。
| シナリオ | 優先 | 典型構成 |
|---|---|---|
| 社内バッチ分析(夜間) | スループット | 大バッチ + 安い GPU |
| チャット UI | レイテンシ | 小バッチ + 高速 GPU / キャッシュ |
| 公開デモ(Spaces) | コスト | CPU Basic + 小モデル |
| 本番 API | 安定性 | TGI + オートスケール |
低レイテンシ
▲
│ ● 対話 UI
│
│ ● バッチ処理
└──────────────────► 低コスト
霊夢
Spaces の CPU 無料枠、ここで効いてくるね。
魔理沙
その通りだ。第9章で GPU Space vs CPU Space を体感する。第7章 LoRA モデルも、配信時は 小さいベース + アダプタ の方が運用しやすいZE。
8.6 ハンズオン 8 — バッチ計測と Docker(任意)
霊夢
数字で見せてくれる?
魔理沙
ハンズオン 8 だ。同じモデルで batch size を変え、スループットを計測する。
ハンズオン 8 — 推論ベンチマーク
お題A 同じモデルで batch size を変えて計測する
python scripts/ch08_benchmark.py
GPU がある場合:
python scripts/ch08_benchmark.py --device cuda --batch-sizes 1,4,8,16,32
CPU のみ:
python scripts/ch08_benchmark.py --device cpu --batch-sizes 1,2,4
期待する出力の例:
Model: distilbert-base-uncased-finetuned-sst-2-english
Device: cuda
batch | latency(ms) | samples/sec
------------------------------------
1 | 12.34 | 81.0
4 | 18.56 | 215.5
8 | 28.90 | 276.8
Best throughput at batch_size=8 (276.8 samples/sec)
OK: Chapter 8-A batch benchmark complete
お題B 推論用 Docker イメージを動かしてみる(任意)
TGI は LLM 向け だが、入口として Docker で HF 推論サーバを試せる。GPU 環境が必要な場合が多い(任意 お題)。
# Docker が入っていること。NVIDIA Container Toolkit は GPU 利用時に必要 docker pull ghcr.io/huggingface/text-generation-inference:latest # 例: 小さな公開 LLM(初回 DL に時間がかかる) docker run --gpus all -p 8080:80 \ -v $HOME/.cache/huggingface:/data \ ghcr.io/huggingface/text-generation-inference:latest \ --model-id HuggingFaceH4/zephyr-7b-beta
別ターミナルからヘルスチェック:
curl http://localhost:8080/health
CPU のみ環境では、代わりに ONNX export(8.3)や第9章の Gradio CPU デモ を優先しろ。
pip install "optimum[onnxruntime]" optimum-cli export onnx \ --model distilbert-base-uncased-finetuned-sst-2-english \ log/ch08_onnx_distilbert
霊夢
Docker 無理な人は ONNX で OK?
魔理沙
OK だZE。ハンズオン 8-B は 「本番はコンテナも選択肢」 という意識付け。無理に GPU Docker を触る必要はない。
改造アイデア:
# シーケンス長を変えてメモリ感覚を掴む python scripts/ch08_benchmark.py --seq-len 256 --batch-sizes 1,2,4,8
8.7 第6〜7章からの接続
霊夢
ここまでの章、推論の話とどう繋がる?
魔理沙
流れを整理する。
| 章 | やったこと | 推論への影響 |
|---|---|---|
| 第6章 Trainer | Full FT | 精度↑、モデルサイズはそのまま |
| 第7章 LoRA | アダプタ FT | 配布はアダプタ数 MB、ベースは共有 |
| 第8章 | 計測・最適化 | 同じモデルでも 回し方 で速度が変わる |
| 第9章 Gradio | UI 公開 | ユーザー体感レイテンシが表面化 |
LoRA 推論をバッチ化する例:
from peft import PeftModel from transformers import AutoModelForCausalLM, AutoTokenizer import torch tokenizer = AutoTokenizer.from_pretrained("distilgpt2") base = AutoModelForCausalLM.from_pretrained("distilgpt2") model = PeftModel.from_pretrained(base, "log/ch07_lora_adapter/adapter") model.eval() prompts = ["Hello!", "LoRA is efficient.", "Batch me."] inputs = tokenizer(prompts, padding=True, return_tensors="pt") with torch.inference_mode(): outputs = model.generate(**inputs, max_new_tokens=20)
本章スクリプト全文
魔理沙
リポジトリがなくても手を動かせるよう、本章の 完成スクリプト をそのまま載せるのだ。
手元では scripts/ 以下と同じファイル名で保存して実行すればよいZE。
(python scripts/xxx.py と書いてある箇所は、保存先に合わせてパスを読み替えてくれ。)
scripts/ch08_benchmark.py
"""Chapter 8-A: Benchmark inference throughput at different batch sizes.""" from __future__ import annotations import argparse import statistics import time import torch from transformers import AutoModelForSequenceClassification, AutoTokenizer DEFAULT_MODEL = "distilbert-base-uncased-finetuned-sst-2-english" def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Batch inference benchmark") parser.add_argument("--model", default=DEFAULT_MODEL) parser.add_argument( "--batch-sizes", default="1,2,4,8", help="Comma-separated batch sizes to test", ) parser.add_argument("--seq-len", type=int, default=128, help="Input length") parser.add_argument("--warmup", type=int, default=3, help="Warmup iterations") parser.add_argument("--iters", type=int, default=10, help="Timed iterations") parser.add_argument( "--device", default="auto", choices=["auto", "cpu", "cuda"], help="Device for inference", ) return parser.parse_args() def resolve_device(choice: str) -> torch.device: if choice == "cpu": return torch.device("cpu") if choice == "cuda": if not torch.cuda.is_available(): raise SystemExit("CUDA requested but not available.") return torch.device("cuda") return torch.device("cuda" if torch.cuda.is_available() else "cpu") def make_batch(tokenizer, texts: list[str], seq_len: int) -> dict: return tokenizer( texts, padding="max_length", truncation=True, max_length=seq_len, return_tensors="pt", ) def benchmark( model, tokenizer, batch_size: int, seq_len: int, warmup: int, iters: int, device: torch.device, ) -> dict: sample = "This chapter measures batch inference throughput for Hugging Face models." texts = [sample] * batch_size model.eval() inputs = make_batch(tokenizer, texts, seq_len) inputs = {k: v.to(device) for k, v in inputs.items()} with torch.inference_mode(): for _ in range(warmup): model(**inputs) latencies = [] for _ in range(iters): if device.type == "cuda": torch.cuda.synchronize() start = time.perf_counter() model(**inputs) if device.type == "cuda": torch.cuda.synchronize() latencies.append(time.perf_counter() - start) total_samples = batch_size * iters total_time = sum(latencies) throughput = total_samples / total_time return { "batch_size": batch_size, "latency_ms": statistics.mean(latencies) * 1000, "throughput_samples_per_sec": throughput, } def main() -> None: args = parse_args() device = resolve_device(args.device) batch_sizes = [int(x.strip()) for x in args.batch_sizes.split(",") if x.strip()] print(f"Model: {args.model}") print(f"Device: {device}") print(f"Batch sizes: {batch_sizes}") print(f"Warmup: {args.warmup}, timed iters: {args.iters}\n") tokenizer = AutoTokenizer.from_pretrained(args.model) model = AutoModelForSequenceClassification.from_pretrained(args.model) model.to(device) print(f"{'batch':>6} | {'latency(ms)':>12} | {'samples/sec':>12}") print("-" * 36) results = [] for bs in batch_sizes: row = benchmark( model, tokenizer, batch_size=bs, seq_len=args.seq_len, warmup=args.warmup, iters=args.iters, device=device, ) results.append(row) print( f"{row['batch_size']:>6} | " f"{row['latency_ms']:>12.2f} | " f"{row['throughput_samples_per_sec']:>12.1f}" ) best = max(results, key=lambda r: r["throughput_samples_per_sec"]) print( f"\nBest throughput at batch_size={best['batch_size']} " f"({best['throughput_samples_per_sec']:.1f} samples/sec)" ) print("OK: Chapter 8-A batch benchmark complete") if __name__ == "__main__": main()
霊夢のメモ帳
- スループット は batch size と GPU 利用率で決まる。感覚ではなく
ch08_benchmark.pyで計測 する。 - 推論では
eval()+inference_mode()が基本。ONNX / TGI / vLLM は デプロイ先 に応じて選ぶ。 - 速さ・コスト・レイテンシ はトレードオフ。Spaces デモは CPU + 小モデルも有力。
魔理沙の one more thing
pipeline に batch_size を渡すと内部でまとめて推論できる。手書きループより簡単にスループット改善の入口になるZE。
from transformers import pipeline clf = pipeline("sentiment-analysis", model="distilbert-base-uncased-finetuned-sst-2-english") texts = ["Great!", "Bad.", "OK"] * 10 results = clf(texts, batch_size=8) print(len(results))
次章へ
魔理沙
計測と最適化の入口は押さえた。次は 第9章 Gradio でデモアプリを作る。モデルを URL で共有 してフィードバックをもらう段階だZE。
霊夢
せっかく速くしたモデル、画面付きで見せたい!
魔理沙
その意気だ。Gradio + Hugging Face Spaces で、第10章の総合プロジェクトへの布石を打つぞ。
第9章 Gradio でデモアプリを作る
9.1 なぜデモが必要か(再現性・共有・フィードバック)
霊夢
第8章までで推論も速くなったけど… 友達に「うちの AI すごい!」って見せるには?
魔理沙
ノートブックを渡すだけ だと、環境差で動かない人が出る。だから デモアプリ が要るZE。
| デモの役割 | 説明 |
|---|---|
| 再現性 | 「この URL を開けば同じ UI」 |
| 共有 | SNS・記事・動画概要欄にリンク1本 |
| フィードバック | 実際の入力で弱点が見える |
| 採用 / 審査 | コンペ・社内 PoC の提出物になる |
第7章 LoRA(学習)
│
第8章 推論最適化(速く回す)
│
第9章 Gradio + Spaces(見せる) ← 今ここ
│
第10章 総合プロジェクト(全部つなぐ)
霊夢
第6章の Trainer 成果物も、ここで見せられる?
魔理沙
できる。分類モデルなら Gradio + pipeline が最短だ。生成モデルなら generate を関数で包む。本章は テキスト分類デモ から入るZE。
9.2 Gradio の基本 UI コンポーネント
霊夢
Gradio って HTML 書かなくていいの?
魔理沙
Python だけで Web UI ができる。コンポーネント を並べて、関数 に入力出力を繋ぐ。
pip install "gradio>=4.0"
最小例:
import gradio as gr def greet(name: str) -> str: return f"Hello, {name}!" demo = gr.Interface(fn=greet, inputs="text", outputs="text") demo.launch()
よく使うコンポーネント:
| コンポーネント | 用途 |
|---|---|
gr.Textbox |
テキスト入力 |
gr.Label |
分類スコア表示 |
gr.Slider |
温度・max_tokens など |
gr.Image |
画像入出力(Vision) |
gr.Audio |
音声(Whisper 等) |
gr.Examples |
サンプル入力の一覧 |
Blocks でレイアウトを組む例:
import gradio as gr with gr.Blocks() as demo: gr.Markdown("# My Demo") with gr.Row(): inp = gr.Textbox(label="Input") out = gr.Label(label="Output") inp.change(fn=my_predict, inputs=inp, outputs=out) demo.launch()
霊夢
Interface と Blocks 、どっち使う?
魔理沙
1関数1画面なら Interface、レイアウト自由度が要るなら Blocks だ。本章のスクリプトは Blocks だZE。
9.3 モデルを Web UI に載せる
霊夢
第2章の pipeline、そのまま UI に載せられる?
魔理沙
載せられる。推論関数の中で pipeline を呼ぶだけだ。
from transformers import pipeline import gradio as gr classifier = pipeline( "sentiment-analysis", model="distilbert-base-uncased-finetuned-sst-2-english", ) def predict(text: str): if not text.strip(): return {"POSITIVE": 0.0, "NEGATIVE": 0.0} results = classifier(text) return {r["label"]: r["score"] for r in results} with gr.Blocks() as demo: text = gr.Textbox(label="Text", lines=3) label = gr.Label(label="Sentiment") text.change(predict, inputs=text, outputs=label) demo.launch()
第7章 LoRA を載せる場合は pipeline の代わりに PeftModel を使う(生成タスク向け):
# 分類 LoRA の例(概念) from peft import PeftModel from transformers import AutoModelForSequenceClassification, AutoTokenizer import torch base = AutoModelForSequenceClassification.from_pretrained("base-model-id") model = PeftModel.from_pretrained(base, "path/to/lora/adapter") tokenizer = AutoTokenizer.from_pretrained("path/to/lora/adapter") model.eval() def predict_lora(text: str): inputs = tokenizer(text, return_tensors="pt") with torch.inference_mode(): logits = model(**inputs).logits # ... softmax → ラベル dict へ
霊夢
ローカルで試してから公開すればいいね。
魔理沙
その順番が安全だZE。次のハンズオン A では 本章末尾の ch09_gradio_app.py(完成スクリプト全文)をファイルに保存して動かす。
9.4 Hugging Face Spaces へのデプロイ
霊夢
Spaces って Hub 上のホスティング?
魔理沙
Hugging Face Spaces は、Gradio / Streamlit / Docker アプリを 無料〜有料枠 でホストできるZE。Git リポジトリと同じ感覚で push する。
デプロイの流れ:
1. Hub で Space 作成(SDK: Gradio) 2. app.py + requirements.txt を push 3. Space が build → 公開 URL 発行
Space 用 requirements.txt の例:
transformers>=4.40 torch gradio>=4.0
Space 用 README.md 先頭(YAML メタデータ):
--- title: Yukkuri HF Sentiment Demo emoji: 🤗 colorFrom: blue colorTo: green sdk: gradio sdk_version: 4.44.0 app_file: app.py pinned: false ---
Space 用の完成コードは 本章末尾「本章スクリプト全文」の app.py にある。別リポジトリにコピーして push してもよい。
# 例: 新規 Space リポジトリを clone したあと cp /path/to/yukkuri-hugging-face/scripts/app.py ./app.py cp requirements.txt ./ # 上記内容を記載 git add app.py requirements.txt README.md git commit -m "Add Gradio sentiment demo" git push
霊夢
ビルド失敗、よく見るやつ…
魔理沙
requirements のバージョン衝突 と app.py のパス typo が定番だ。Space の Logs タブを見ろ。ローカル venv で同じ requirements を試すと再現しやすいZE。
9.5 Secrets と API キーの扱い
霊夢
Hub トークン、Space にベタ書きしないよね…?
魔理沙
絶対ダメ だ。Space では Settings → Secrets に置く。ローカルでは .env(git 管理外)。
# .env(リポジトリに commit しない) HF_TOKEN=hf_xxxxxxxxxxxxxxxx
Gradio / Python から読む例:
import os from huggingface_hub import login token = os.environ.get("HF_TOKEN") if token: login(token=token)
Space 側では Secrets に HF_TOKEN を登録すると、環境変数 として注入される。
| 置き場所 | OK? | 用途 |
|---|---|---|
.env(ローカル) |
OK(gitignore) | 開発 |
| Space Secrets | OK | 本番デプロイ |
app.py 直書き |
NG | 漏洩リスク |
| 本章 Markdown | NG | サンプルもプレースホルダのみ |
霊夢
第1章で学んだトークン管理、ここで効いてくる。
魔理沙
Read 権限 と Write 権限 も最小限に。公開デモ用に Write トークンを Space に入れない、が鉄則だZE。
9.6 ハンズオン 9 — Gradio デモと Spaces
霊夢
URL 欲しい!
魔理沙
ハンズオン 9 だ。ローカル Gradio → Space 公開 → CPU/GPU の違いを確認する。
ハンズオン 9 — デモを作って公開する
お題A テキスト分類デモを Gradio で作る
pip install "gradio>=4.0" python scripts/ch09_gradio_app.py
ブラウザで http://127.0.0.1:7860 を開く。一時的な公開 URL が欲しければ:
python scripts/ch09_gradio_app.py --share
期待する動作:
- 英文を入力すると POSITIVE / NEGATIVE スコアが表示される
- Examples のボタンでサンプル入力できる
お題B Spaces に push して URL を共有する
- huggingface.co/new-space で Space 作成(SDK: Gradio)
- 生成された Git リモートを clone
- 以下を配置:
your-space/ ├── app.py ← scripts/app.py をコピー ├── requirements.txt └── README.md ← YAML メタデータ付き
git add app.py requirements.txt README.md git commit -m "Deploy Chapter 9 sentiment demo" git push
ビルド完了後、https://huggingface.co/spaces/<user>/<name> が共有 URL になる。
お題C GPU Space と CPU Space の違いを体感する
| 設定 | メリット | デメリット |
|---|---|---|
| CPU Basic(無料) | コスト0、小モデル向け | 大モデル・生成は遅い |
| GPU(有料クレジット) | LLM 生成が実用的 | コスト・スリープに注意 |
同じ app.py でも、Space 設定の Hardware を変えるだけで体感が変わる。
- CPU: distilbert 分類デモは十分快適(本章のデフォルト)
- GPU: 第7章 LoRA 付き 生成モデル デモ向け(第10章で本格利用)
GPU Space で生成デモを試すときの app.py 改造イメージ(第10章予習):
# 概念例: GPU Space + 小さな causal LM from transformers import pipeline import gradio as gr gen = pipeline("text-generation", model="distilgpt2") def complete(prompt: str): out = gen(prompt, max_new_tokens=40, do_sample=True, top_p=0.9) return out[0]["generated_text"] gr.Interface(fn=complete, inputs="text", outputs="text").launch()
霊夢
無料 CPU で分類、GPU は総合プロジェクト用ね。
魔理沙
その理解で OK だZE。コストを抑えつつ まず URL で共有 するのが先だ。
9.7 第8章からの接続 — レイテンシを UI で感じる
霊夢
第8章のベンチマーク、Gradio だとユーザーが遅さに気づく?
魔理沙
その通り。ch08_benchmark.py の数字が、デモでは 待ち時間 として表面化する。
改善の順番(おすすめ):
1. 小さいモデル / LoRA アダプタ(第7章) 2. batch / FP16(第8章) 3. CPU vs GPU Space の選択(本章) 4. 必要なら TGI 等の API 化(発展)
Gradio で推論時間を表示する小技:
import time def predict_with_timing(text: str): start = time.perf_counter() result = predict(text) elapsed_ms = (time.perf_counter() - start) * 1000 return result, f"{elapsed_ms:.1f} ms"
本章スクリプト全文
魔理沙
リポジトリがなくても手を動かせるよう、本章の 完成スクリプト をそのまま載せるのだ。
手元では scripts/ 以下と同じファイル名で保存して実行すればよいZE。
(python scripts/xxx.py と書いてある箇所は、保存先に合わせてパスを読み替えてくれ。)
scripts/ch09_gradio_app.py
"""Chapter 9-A: Local Gradio demo for text classification.""" from __future__ import annotations import argparse import gradio as gr from transformers import pipeline DEFAULT_MODEL = "distilbert-base-uncased-finetuned-sst-2-english" def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Gradio text classification demo") parser.add_argument("--model", default=DEFAULT_MODEL) parser.add_argument("--host", default="127.0.0.1") parser.add_argument("--port", type=int, default=7860) parser.add_argument( "--share", action="store_true", help="Create a temporary public Gradio link", ) return parser.parse_args() def build_demo(classifier) -> gr.Blocks: def predict(text: str): if not text.strip(): return {"POSITIVE": 0.0, "NEGATIVE": 0.0} results = classifier(text) if isinstance(results[0], list): results = results[0] return {item["label"]: item["score"] for item in results} with gr.Blocks(title="Yukkuri HF Ch09 Demo") as demo: gr.Markdown( "# 第9章デモ: テキスト感情分析\n" "第2章の `pipeline` を Gradio UI に載せた例だZE。" ) with gr.Row(): text_in = gr.Textbox( label="Input text", placeholder="I love Hugging Face!", lines=3, ) label_out = gr.Label(label="Prediction", num_top_classes=2) examples = gr.Examples( examples=[ ["This book makes Hugging Face easy to learn!"], ["I am worried about CUDA out of memory."], ["Gradio demos are fun to share."], ], inputs=text_in, ) text_in.change(fn=predict, inputs=text_in, outputs=label_out) gr.Markdown( "Deploy this app to **Hugging Face Spaces** with `app.py` (Hands-on 9-B)." ) return demo def main() -> None: args = parse_args() print(f"Loading pipeline: {args.model}") classifier = pipeline("sentiment-analysis", model=args.model) demo = build_demo(classifier) demo.launch( server_name=args.host, server_port=args.port, share=args.share, ) if __name__ == "__main__": main()
scripts/app.py
"""Hugging Face Spaces entry point (Chapter 9-B). Deploy by creating a Space with Gradio SDK and pushing this file as app.py. """ import gradio as gr from transformers import pipeline MODEL_ID = "distilbert-base-uncased-finetuned-sst-2-english" print(f"Loading model: {MODEL_ID}") classifier = pipeline("sentiment-analysis", model=MODEL_ID) def predict(text: str): if not text or not text.strip(): return {"POSITIVE": 0.0, "NEGATIVE": 0.0} results = classifier(text) if isinstance(results[0], list): results = results[0] return {item["label"]: item["score"] for item in results} with gr.Blocks(title="Yukkuri HF Space Demo") as demo: gr.Markdown( "# Yukkuri Hugging Face — Sentiment Demo\n" "Chapter 9 Space template. CPU Basic works for this tiny model." ) text_in = gr.Textbox(label="Text", lines=3) label_out = gr.Label(label="Sentiment") text_in.submit(predict, inputs=text_in, outputs=label_out) gr.Examples( examples=[ ["Learning LoRA on Colab is awesome!"], ["My GPU ran out of memory again."], ], inputs=text_in, ) if __name__ == "__main__": demo.launch()
霊夢のメモ帳
- Gradio は Python だけで Web デモを作り、Spaces で URL 公開できる。
- トークンは
.env/ Space Secrets のみ。コードと Markdown に書かない。 - CPU Space は小モデル分類向け、GPU Space は生成・LoRA 本番デモ向け(第10章)。
魔理沙の one more thing
Space を Duplicate(複製) すると、他人のデモを fork して改造できる。学習用に HF 上の Gradio Space を探して duplicate するのも手だZE。
# ローカル開発: ホットリロード python scripts/ch09_gradio_app.py # Gradio はコード変更を reload モードで試せる(環境による)
Hub CLI で Space リポジトリを直接 clone する例:
git clone https://huggingface.co/spaces/your-username/your-space-name
次章へ
魔理沙
第1章の Hub から、第6章 Trainer、第7章 LoRA、第8章の計測、そして本章の Gradio + Spaces まで揃った。次は 第10章 総合プロジェクト — ゆっくり実況コメント生成ボット だZE。
霊夢
うちら口調の AI、全世界に公開しちゃうの!?
魔理沙
データ集め → LoRA → 推論最適化 → Space 公開まで 一気通貫 だ。これまでの章が全部つながる総仕上げ、楽しみにしてろ!
第10章 総合プロジェクト — ゆっくり実況コメント生成ボット
10.1 企画立案
霊夢
第9章までで、Hub も Pipeline も LoRA も Gradio もやったのだ。で、最後は何を作るの?
魔理沙
総合プロジェクト だZE。テーマは ゆっくり実況コメント生成ボット — ゲームの状況を入れると、霊夢と魔理沙風の一言が返ってくるやつだ。
霊夢
…うちらの口調、AI に学習させるの?
魔理沙
本書用の 小さなデモ だ。本番品質のキャラクター AI ではなく、第1〜9章の技術を 一本のパイプライン につなぐことが目的だ。
【入力】ゲームの状況(例: ボス撃破直後)
│
▼
【前処理】台本 JSONL → 学習用テキスト形式
│
▼
【学習】日本語 GPT-2 + LoRA(第7章)
│
▼
【推論】プロンプト + generate(第4章)
│
▼
【公開】Gradio + Spaces(第9章)
│
▼
【改善】評価 → データ追加 → 再学習(10.8)
| 章 | 本章での役割 |
|---|---|
| 1 | Hub からベースモデル・データセットを選ぶ |
| 3 | トークナイズ形式の設計 |
| 5 | JSONL → Dataset |
| 7 | LoRA で口調を足す |
| 8 | 推論パラメータ・バッチ(任意) |
| 9 | Gradio / Space で共有 |
霊夢
著作権とか、大丈夫なのだ?
魔理沙
学習データは自分で用意した台本 に限定するのが安全だZE。既存動画の無断転用は NG。本書の scripts/data/ch10_dialogues.jsonl は オリジナルの短い例 だけだ。
10.2 データ収集方針
霊夢
データ、どれくらい要るの?
魔理沙
本番のキャラクター品質なら 数百〜数千例。本書のデモは 20例前後 で「流れがわかる」サイズにしている。まず形式を決めるのだ。
10.2.1 1行1シーンの JSONL
魔理沙
1行が1シーンの JSONL にする。フィールドは最小限だZE。
{"situation": "ボス撃破直後", "reimu": "やったのだー!", "marisa": "まだ油断するなZE。"}
霊夢
動画の字幕から作るの?
魔理沙
手順の一例だ。
| ソース | 作り方 | 注意 |
|---|---|---|
| 自作台本 | スプレッドシート → JSONL 出力 | 最も安全 |
| 字幕(自作動画) | 自分の動画のみ | 他人の動画は不可 |
| 既存コーパス | Hub Datasets を ライセンス確認 | 商用・二次利用を読む |
| 合成データ | LLM で下書き → 人間が修正 | そのまま使わず必ず検品 |
10.2.2 サンプルデータの場所
本書付属の例:
scripts/data/ch10_dialogues.jsonl
行数確認:
wc -l scripts/data/ch10_dialogues.jsonl
霊夢
20行くらい?
魔理沙
デモ用だZE。増やすときは同じ形式で行を足せばいい。
10.3 前処理とトークナイザ調整
霊夢
JSONL のままじゃ学習できないんでしょ?
魔理沙
因果言語モデル(GPT 系)は 続きのテキストを予測 する。1例を次の 1ブロックの文字列 にまとめるのだ。
【状況】ボス撃破直後 霊夢: やったのだー!長かったのだ! 魔理沙: まだ油断するな。隠し部屋があるかもしれないZE。
霊夢
第5章の map みたいな感じ?
魔理沙
その通り。本章はスクリプトにまとめた。
cd /path/to/yukkuri-hugging-face source .venv/bin/activate python scripts/ch10_prepare_data.py
期待する出力の例:
Loaded 20 examples from .../ch10_dialogues.jsonl train: 17 validation: 3 Saved to: log/ch10_dataset OK: Chapter 10 data preparation complete
中身の確認(第5章の復習):
from datasets import load_from_disk ds = load_from_disk("log/ch10_dataset") print(ds) print(ds["train"][0]["text"])
霊夢
train と validation に分かれてる。
魔理沙
10.8 の改善サイクル で validation の生成品質を見るときに使うZE。比率は --val-ratio で変えられる。
python scripts/ch10_prepare_data.py --val-ratio 0.2
10.3.1 トークナイザについて
魔理沙
ベースモデル付属の Tokenizer をそのまま使う のが基本だ。独自語「ゆっくり」などを頻出させるなら、第3章の 語彙追加 も検討できるが、本章では省略する。
10.4 ベースモデル選定(日本語 LLM)
霊夢
英語の distilgpt2 じゃ、日本語おかしいのだ。
魔理沙
本章の本番候補は rinna/japanese-gpt2-medium だZE。日本語 GPT-2 系で、個人 PC でも扱いやすいサイズだ。
| モデル | 言語 | サイズ感 | 本章での用途 |
|---|---|---|---|
rinna/japanese-gpt2-medium |
日本語 | 中 | 推奨(本番デモ) |
distilgpt2 |
英語 | 小 | スモークテスト(CPU で流れ確認) |
| 7B 級 LLM | 多言語 | 大 | Colab Pro 等。QLoRA(第7章) |
霊夢
まず英語で試してから日本語?
魔理沙
おすすめの順番だZE。
# 1) 流れだけ確認(数分・初回 DL あり) python scripts/ch10_lora_train.py --smoke --epochs 1 # 2) 日本語モデルで本番(初回は Hub から DL) python scripts/ch10_lora_train.py --epochs 5
Hub でモデルカードを読む(第1章):
https://huggingface.co/rinna/japanese-gpt2-medium
ライセンス・利用範囲を必ず確認するのだ。
10.5 LoRA ファインチューニング
霊夢
第7章の LoRA、ここで本番投入なのだ。
魔理沙
流れは同じだ。データ準備 → LoRA → adapter 保存。本章用スクリプトは ch10_lora_train.py だZE。
python scripts/ch10_prepare_data.py python scripts/ch10_lora_train.py \ --model rinna/japanese-gpt2-medium \ --epochs 5 \ --lr 3e-4 \ --rank 8
主要オプション:
| オプション | 意味 |
|---|---|
--model |
ベースモデル ID |
--epochs |
エポック数(データが少ないと過学習しやすい) |
--lr |
学習率 |
--rank |
LoRA rank |
--smoke |
distilgpt2 で CPU 動作確認 |
期待する出力の例(抜粋):
trainable params: 294,912 || all params: ... Starting LoRA training... Saved adapter to: log/ch10_lora_adapter/adapter OK: Chapter 10 LoRA training complete
霊夢
エポック多すぎると、台本丸暗記?
魔理沙
その通りだZE。20例なら 3〜5 epoch から様子見。eval_loss が下がりすぎたらデータを増やすか epoch を減らす。
学習後のファイル:
log/ch10_lora_adapter/ ├── adapter/ # LoRA + tokenizer(推論・Gradio で使用) │ ├── adapter_config.json │ ├── adapter_model.safetensors │ └── ... └── checkpoints/ # Trainer の中間出力(save_strategy=no なら空のことも)
Hub に adapter だけ push する(第6章・第7章の復習):
huggingface-cli login
# 任意: Hub にアップロードする例(ユーザー名は置き換え) from huggingface_hub import HfApi api = HfApi() api.upload_folder( folder_path="log/ch10_lora_adapter/adapter", repo_id="your-username/yukkuri-comment-lora", repo_type="model", )
10.6 推論スクリプトとプロンプト設計
霊夢
学習できた。で、どうやってコメント出すの?
魔理沙
プロンプト で「続きを書かせる」形式だ。学習データと 同じ型 に揃えるのがコツだZE。
def build_prompt(situation: str) -> str: return f"【状況】{situation}\n霊夢:"
モデルは「霊夢:」の続き → 改行 →「魔理沙:」… と生成してほしい。
CLI で試す:
python scripts/ch10_generate.py --situation "初見のステージ"
生成パラメータ(第4章の復習):
python scripts/ch10_generate.py \ --situation "ゲームオーバー" \ --temperature 0.8 \ --top-p 0.9 \ --max-new-tokens 100
| パラメータ | 効果 |
|---|---|
temperature |
高いほどランダム・創造的 |
top_p |
累積確率でトークンを絞る |
max_new_tokens |
最大生成長(長すぎると脱線) |
霊夢
毎回違う文が出るのだ。
魔理沙
do_sample=True だからだZE。再現性が欲しければ temperature=0 や do_sample=False(モデルによる)を試す。
Python から呼ぶ例:
from pathlib import Path import json import torch from peft import PeftModel from transformers import AutoModelForCausalLM, AutoTokenizer adapter = Path("log/ch10_lora_adapter/adapter") cfg = json.loads((adapter / "adapter_config.json").read_text(encoding="utf-8")) base_name = cfg["base_model_name_or_path"] tokenizer = AutoTokenizer.from_pretrained(adapter) base = AutoModelForCausalLM.from_pretrained(base_name) model = PeftModel.from_pretrained(base, adapter) model.eval() situation = "レアドロップ" prompt = f"【状況】{situation}\n霊夢:" inputs = tokenizer(prompt, return_tensors="pt") with torch.no_grad(): out = model.generate(**inputs, max_new_tokens=80, do_sample=True, temperature=0.85) print(tokenizer.decode(out[0], skip_special_tokens=True))
10.7 Gradio + Spaces で公開
霊夢
一人で遊ぶのはもう飽きた。みんなに触らせたいのだ。
魔理沙
第9章の Gradio に LoRA 推論を載せる。ローカル用は ch10_gradio_app.py だZE。
python scripts/ch10_gradio_app.py
ブラウザで http://127.0.0.1:7861 を開き、【状況】を入力して「生成するのだ」を押す。
10.7.1 Space 用の構成
Space に載せるときの注意:
| 項目 | 推奨 |
|---|---|
| SDK | Gradio |
| ハードウェア | 日本語 GPT-2 + LoRA なら CPU Basic でも動くことが多い |
| モデル | adapter を Hub に push → Space から from_pretrained |
| Secrets | HF_TOKEN(書き込み時)。第9章どおり漏らさない |
Space の app.py 最小イメージ(adapter を Hub に置いた場合):
import gradio as gr from peft import PeftModel from transformers import AutoModelForCausalLM, AutoTokenizer ADAPTER_ID = "your-username/yukkuri-comment-lora" tokenizer = AutoTokenizer.from_pretrained(ADAPTER_ID) base_name = "rinna/japanese-gpt2-medium" base = AutoModelForCausalLM.from_pretrained(base_name) model = PeftModel.from_pretrained(base, ADAPTER_ID) model.eval() def predict(situation: str) -> str: prompt = f"【状況】{situation.strip()}\n霊夢:" inputs = tokenizer(prompt, return_tensors="pt") out = model.generate(**inputs, max_new_tokens=100, do_sample=True, temperature=0.85) return tokenizer.decode(out[0], skip_special_tokens=True) with gr.Blocks() as demo: gr.Markdown("# ゆっくり実況コメント Bot") s = gr.Textbox(label="【状況】") o = gr.Textbox(label="生成結果", lines=8) gr.Button("生成").click(predict, s, o) demo.launch()
霊夢
requirements.txt は?
魔理沙
Space 用の例だZE。
transformers>=4.40 peft>=0.11 accelerate>=0.30 torch gradio>=4.0 sentencepiece
# Space リポジトリを clone して push する流れ(第9章) git clone https://huggingface.co/spaces/your-username/yukkuri-comment-bot # app.py と requirements.txt を配置 cd yukkuri-comment-bot git add app.py requirements.txt git commit -m "Add ch10 comment bot" git push
10.8 改善サイクル(評価 → データ追加 → 再学習)
霊夢
出来が微妙なとき、どう直すの?
魔理沙
ループ で回すのだ。
① 固定シーンで生成(eval) ② 人間が「良い / 微妙 / NG」をラベル ③ NG を台本に直して JSONL に追加 ④ prepare → LoRA 再学習 ⑤ Space を更新
バッチ評価スクリプト:
python scripts/ch10_eval_samples.py
出力:
log/ch10_eval.jsonl
中身の例:
{"situation": "ボス撃破直後", "generated": "【状況】ボス撃破直後\n霊夢: ..."}
霊夢
自動で良し悪し判定はしないの?
魔理沙
本書では 人間レビュー を推奨するZE。BLEU や perplexity だけでは「うちらっぽさ」は測れない。第8章の レイテンシ計測 と組み合わせれば、品質と速度の両方を見られる。
改善チェックリスト:
| 症状 | 対策 |
|---|---|
| 口調が崩れる | 台本を増やす・プロンプト形式を学習と統一 |
| 同じフレーズの繰り返し | temperature を上げる / データ多様化 |
| 英語が混ざる | 日本語ベースモデルに切り替え |
| 生成が長すぎる | max_new_tokens を下げる |
| 学習が遅い | --smoke でデバッグ後 GPU / Colab で本番 |
総合ハンズオン — 第1章〜第9章をつなげて完成させる
霊夢
最後に、手順を最初から通してやるのだ!
魔理沙
総合ハンズオン のチェックリストだZE。全部にチェックが付いたら本編クリアだ。
ステップ0: 環境(第0章)
source .venv/bin/activate python scripts/check_env.py pip install "peft>=0.11" "gradio>=4.0" sentencepiece
ステップ1: Hub でモデルを確認(第1章)
- [ ]
rinna/japanese-gpt2-mediumの Model Card を読んだ - [ ] ライセンスを確認した
ステップ2: データ準備(第5章・10.2–10.3)
# 自作行を1つ追加してから python scripts/ch10_prepare_data.py
お題A ch10_dialogues.jsonl に 自分オリジナルの1シーン を1行追加する。
ステップ3: LoRA 学習(第7章・10.5)
python scripts/ch10_lora_train.py --epochs 5
お題B --smoke と本番モデルで trainable params の行をメモ帳に書く。
ステップ4: 推論(第4章・10.6)
python scripts/ch10_generate.py --situation "配信終了"
お題C temperature を 0.5 と 1.0 で比べ、どちらが「実況っぽい」かメモする。
ステップ5: Gradio 公開(第9章・10.7)
python scripts/ch10_gradio_app.py
- [ ] ローカル UI で3シーン試した
- [ ] (任意) Hub に adapter を push し Space を作った
ステップ6: 改善ループ(10.8)
python scripts/ch10_eval_samples.py
- [ ]
log/ch10_eval.jsonlを開き、1件「NG」と判断したシーンを台本に直して再学習した
霊夢
長い…でも、ここまで来たら一人で Space まで作れるのだ。
魔理沙
それが 総合プロジェクト のゴールだZE。
本章スクリプト全文
魔理沙
リポジトリがなくても手を動かせるよう、本章の 完成スクリプト をそのまま載せるのだ。
手元では scripts/ 以下と同じファイル名で保存して実行すればよいZE。
(python scripts/xxx.py と書いてある箇所は、保存先に合わせてパスを読み替えてくれ。)
scripts/ch10_prepare_data.py
"""Chapter 10: Prepare JSONL dialogues for LoRA training.""" from __future__ import annotations import argparse import json from pathlib import Path from datasets import Dataset, DatasetDict DEFAULT_INPUT = Path(__file__).resolve().parent / "data" / "ch10_dialogues.jsonl" DEFAULT_OUTPUT = Path("log/ch10_dataset") def format_example(row: dict) -> str: """Single training text block (causal LM).""" return ( f"【状況】{row['situation']}\n" f"霊夢: {row['reimu']}\n" f"魔理沙: {row['marisa']}\n" ) def load_jsonl(path: Path) -> list[dict]: rows: list[dict] = [] with path.open(encoding="utf-8") as f: for line in f: line = line.strip() if line: rows.append(json.loads(line)) return rows def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Prepare ch10 training dataset") parser.add_argument("--input", type=Path, default=DEFAULT_INPUT) parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT) parser.add_argument( "--val-ratio", type=float, default=0.15, help="Validation split ratio", ) return parser.parse_args() def main() -> None: args = parse_args() rows = load_jsonl(args.input) texts = [format_example(r) for r in rows] dataset = Dataset.from_dict({"text": texts}) split = dataset.train_test_split(test_size=args.val_ratio, seed=42) dataset_dict = DatasetDict(train=split["train"], validation=split["test"]) args.output.mkdir(parents=True, exist_ok=True) dataset_dict.save_to_disk(str(args.output)) print(f"Loaded {len(texts)} examples from {args.input}") print(f" train: {len(dataset_dict['train'])}") print(f" validation: {len(dataset_dict['validation'])}") print(f"Saved to: {args.output}") print("\nSample:") print(dataset_dict["train"][0]["text"]) print("OK: Chapter 10 data preparation complete") if __name__ == "__main__": main()
scripts/ch10_lora_train.py
"""Chapter 10: LoRA fine-tune yukkuri comment generator.""" from __future__ import annotations import argparse from pathlib import Path import torch from datasets import load_from_disk from peft import LoraConfig, TaskType, get_peft_model from transformers import ( AutoModelForCausalLM, AutoTokenizer, DataCollatorForLanguageModeling, Trainer, TrainingArguments, ) # Japanese: rinna/japanese-gpt2-medium (first run downloads ~500MB) # CPU demo: distilgpt2 (English-ish output; use for smoke test only) DEFAULT_MODEL = "rinna/japanese-gpt2-medium" DEFAULT_DATASET = Path("log/ch10_dataset") DEFAULT_OUTPUT = Path("log/ch10_lora_adapter") def detect_target_modules(model_name: str) -> list[str]: if "gpt2" in model_name.lower() or "rinna" in model_name.lower(): return ["c_attn", "c_proj"] return ["q_proj", "v_proj"] def tokenize_dataset(tokenizer, dataset, max_length: int): def tokenize(batch: dict) -> dict: return tokenizer( batch["text"], truncation=True, max_length=max_length, padding="max_length", ) return dataset.map(tokenize, batched=True, remove_columns=["text"]) def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Ch10 LoRA training") parser.add_argument("--model", default=DEFAULT_MODEL) parser.add_argument("--dataset", type=Path, default=DEFAULT_DATASET) parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT) parser.add_argument("--epochs", type=int, default=5) parser.add_argument("--lr", type=float, default=3e-4) parser.add_argument("--rank", type=int, default=8) parser.add_argument("--max-length", type=int, default=256) parser.add_argument( "--smoke", action="store_true", help="Use distilgpt2 for quick CPU smoke test", ) return parser.parse_args() def main() -> None: args = parse_args() model_id = "distilgpt2" if args.smoke else args.model if not args.dataset.exists(): raise SystemExit( f"Dataset not found: {args.dataset}\n" "Run: python scripts/ch10_prepare_data.py" ) args.output.mkdir(parents=True, exist_ok=True) print(f"Loading dataset: {args.dataset}") dataset_dict = load_from_disk(str(args.dataset)) print(f"Loading base model: {model_id}") tokenizer = AutoTokenizer.from_pretrained(model_id) if tokenizer.pad_token is None: tokenizer.pad_token = tokenizer.eos_token model = AutoModelForCausalLM.from_pretrained(model_id) lora_config = LoraConfig( task_type=TaskType.CAUSAL_LM, r=args.rank, lora_alpha=16, lora_dropout=0.05, target_modules=detect_target_modules(model_id), bias="none", ) model = get_peft_model(model, lora_config) model.print_trainable_parameters() train_ds = tokenize_dataset( tokenizer, dataset_dict["train"], args.max_length ) eval_ds = tokenize_dataset( tokenizer, dataset_dict["validation"], args.max_length ) use_cpu = not torch.cuda.is_available() training_args = TrainingArguments( output_dir=str(args.output / "checkpoints"), num_train_epochs=args.epochs, per_device_train_batch_size=2, per_device_eval_batch_size=2, learning_rate=args.lr, eval_strategy="epoch", logging_steps=5, save_strategy="no", report_to="none", use_cpu=use_cpu, ) trainer = Trainer( model=model, args=training_args, train_dataset=train_ds, eval_dataset=eval_ds, data_collator=DataCollatorForLanguageModeling( tokenizer=tokenizer, mlm=False, ), ) print("Starting LoRA training...") trainer.train() adapter_path = args.output / "adapter" model.save_pretrained(adapter_path) tokenizer.save_pretrained(adapter_path) print(f"Saved adapter to: {adapter_path}") print("OK: Chapter 10 LoRA training complete") if __name__ == "__main__": main()
scripts/ch10_generate.py
"""Chapter 10: Generate yukkuri-style comments with LoRA adapter.""" from __future__ import annotations import argparse from pathlib import Path import torch from peft import PeftModel from transformers import AutoModelForCausalLM, AutoTokenizer DEFAULT_ADAPTER = Path("log/ch10_lora_adapter/adapter") def build_prompt(situation: str) -> str: return f"【状況】{situation}\n霊夢:" def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Ch10 comment generation") parser.add_argument( "--adapter", type=Path, default=DEFAULT_ADAPTER, help="Path to saved LoRA adapter", ) parser.add_argument( "--situation", default="ボス撃破直後", help="Game situation for the comment", ) parser.add_argument("--max-new-tokens", type=int, default=80) parser.add_argument("--temperature", type=float, default=0.8) parser.add_argument("--top-p", type=float, default=0.9) return parser.parse_args() def main() -> None: args = parse_args() if not args.adapter.exists(): raise SystemExit( f"Adapter not found: {args.adapter}\n" "Run: python scripts/ch10_prepare_data.py\n" " python scripts/ch10_lora_train.py" ) import json print(f"Loading adapter from: {args.adapter}") cfg_path = args.adapter / "adapter_config.json" if not cfg_path.exists(): raise SystemExit(f"Missing adapter_config.json in {args.adapter}") base_name = json.loads(cfg_path.read_text(encoding="utf-8"))[ "base_model_name_or_path" ] tokenizer = AutoTokenizer.from_pretrained(args.adapter) base_model = AutoModelForCausalLM.from_pretrained(base_name) model = PeftModel.from_pretrained(base_model, args.adapter) model.eval() device = "cuda" if torch.cuda.is_available() else "cpu" model = model.to(device) prompt = build_prompt(args.situation) inputs = tokenizer(prompt, return_tensors="pt").to(device) with torch.no_grad(): outputs = model.generate( **inputs, max_new_tokens=args.max_new_tokens, do_sample=True, temperature=args.temperature, top_p=args.top_p, pad_token_id=tokenizer.pad_token_id, ) text = tokenizer.decode(outputs[0], skip_special_tokens=True) print("\n--- Generated ---") print(text) print("-----------------") if __name__ == "__main__": main()
scripts/ch10_gradio_app.py
"""Chapter 10: Gradio demo for yukkuri comment generator.""" from __future__ import annotations import argparse import json from pathlib import Path import gradio as gr import torch from peft import PeftModel from transformers import AutoModelForCausalLM, AutoTokenizer DEFAULT_ADAPTER = Path("log/ch10_lora_adapter/adapter") def load_generator(adapter_path: Path): tokenizer = AutoTokenizer.from_pretrained(adapter_path) base_name = tokenizer.name_or_path cfg_path = adapter_path / "adapter_config.json" if cfg_path.exists(): base_name = json.loads(cfg_path.read_text())["base_model_name_or_path"] base_model = AutoModelForCausalLM.from_pretrained(base_name) model = PeftModel.from_pretrained(base_model, adapter_path) model.eval() device = "cuda" if torch.cuda.is_available() else "cpu" model = model.to(device) return model, tokenizer, device def make_predict(model, tokenizer, device, max_new_tokens: int, temperature: float, top_p: float): def predict(situation: str) -> str: if not situation.strip(): return "状況を入力してのだ。" prompt = f"【状況】{situation.strip()}\n霊夢:" inputs = tokenizer(prompt, return_tensors="pt").to(device) with torch.no_grad(): outputs = model.generate( **inputs, max_new_tokens=max_new_tokens, do_sample=True, temperature=temperature, top_p=top_p, pad_token_id=tokenizer.pad_token_id, ) return tokenizer.decode(outputs[0], skip_special_tokens=True) return predict def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Ch10 Gradio Space demo") parser.add_argument("--adapter", type=Path, default=DEFAULT_ADAPTER) parser.add_argument("--host", default="127.0.0.1") parser.add_argument("--port", type=int, default=7861) parser.add_argument("--share", action="store_true") return parser.parse_args() def main() -> None: args = parse_args() if not args.adapter.exists(): raise SystemExit( f"Adapter not found: {args.adapter}\n" "Train first: python scripts/ch10_lora_train.py" ) print(f"Loading: {args.adapter}") model, tokenizer, device = load_generator(args.adapter) predict = make_predict(model, tokenizer, device, max_new_tokens=100, temperature=0.85, top_p=0.9) with gr.Blocks(title="Yukkuri Comment Bot") as demo: gr.Markdown( "# 第10章: ゆっくり実況コメント生成ボット\n" "【状況】を入れると、霊夢・魔理沙風のコメントを生成するデモだZE。" ) situation = gr.Textbox( label="【状況】", placeholder="例: ボス撃破直後", lines=2, ) out = gr.Textbox(label="生成結果", lines=8) btn = gr.Button("生成するのだ") gr.Examples( examples=[ ["初見のステージ"], ["ゲームオーバー"], ["Space 公開直後"], ], inputs=situation, ) btn.click(fn=predict, inputs=situation, outputs=out) demo.launch(server_name=args.host, server_port=args.port, share=args.share) if __name__ == "__main__": main()
scripts/ch10_eval_samples.py
"""Chapter 10.8: Evaluate generations on fixed situations (improvement loop).""" from __future__ import annotations import argparse import json from pathlib import Path DEFAULT_ADAPTER = Path("log/ch10_lora_adapter/adapter") def build_prompt(situation: str) -> str: return f"【状況】{situation}\n霊夢:" EVAL_SITUATIONS = [ "ボス撃破直後", "初見のステージ", "ゲームオーバー", "視聴者コメント", ] def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Ch10 batch eval for improvement loop") parser.add_argument("--adapter", type=Path, default=DEFAULT_ADAPTER) parser.add_argument("--output", type=Path, default=Path("log/ch10_eval.jsonl")) return parser.parse_args() def main() -> None: import torch from peft import PeftModel from transformers import AutoModelForCausalLM, AutoTokenizer args = parse_args() if not args.adapter.exists(): raise SystemExit(f"Adapter not found: {args.adapter}") tokenizer = AutoTokenizer.from_pretrained(args.adapter) cfg_path = args.adapter / "adapter_config.json" base_name = json.loads(cfg_path.read_text())["base_model_name_or_path"] base_model = AutoModelForCausalLM.from_pretrained(base_name) model = PeftModel.from_pretrained(base_model, args.adapter) model.eval() device = "cuda" if torch.cuda.is_available() else "cpu" model = model.to(device) args.output.parent.mkdir(parents=True, exist_ok=True) with args.output.open("w", encoding="utf-8") as f: for situation in EVAL_SITUATIONS: prompt = build_prompt(situation) inputs = tokenizer(prompt, return_tensors="pt").to(device) with torch.no_grad(): outputs = model.generate( **inputs, max_new_tokens=100, do_sample=True, temperature=0.8, top_p=0.9, pad_token_id=tokenizer.pad_token_id, ) text = tokenizer.decode(outputs[0], skip_special_tokens=True) record = {"situation": situation, "generated": text} f.write(json.dumps(record, ensure_ascii=False) + "\n") print(f"\n[{situation}]\n{text}\n") print(f"Wrote eval log: {args.output}") print("OK: Review outputs, add bad cases to ch10_dialogues.jsonl, re-train.") if __name__ == "__main__": main()
scripts/space_ch10_app.py
"""Hugging Face Spaces entry point for Chapter 10 (optional). Upload your trained adapter to Hub, set ADAPTER_ID below, and push as app.py. """ from __future__ import annotations import gradio as gr import torch from peft import PeftModel from transformers import AutoModelForCausalLM, AutoTokenizer # After pushing adapter: your-username/yukkuri-comment-lora ADAPTER_ID = "your-username/yukkuri-comment-lora" BASE_MODEL_ID = "rinna/japanese-gpt2-medium" print(f"Loading base: {BASE_MODEL_ID}") tokenizer = AutoTokenizer.from_pretrained(ADAPTER_ID) base = AutoModelForCausalLM.from_pretrained(BASE_MODEL_ID) model = PeftModel.from_pretrained(base, ADAPTER_ID) model.eval() device = "cuda" if torch.cuda.is_available() else "cpu" model = model.to(device) def predict(situation: str) -> str: if not situation or not situation.strip(): return "【状況】を入力してのだ。" prompt = f"【状況】{situation.strip()}\n霊夢:" inputs = tokenizer(prompt, return_tensors="pt").to(device) with torch.no_grad(): out = model.generate( **inputs, max_new_tokens=100, do_sample=True, temperature=0.85, top_p=0.9, pad_token_id=tokenizer.pad_token_id, ) return tokenizer.decode(out[0], skip_special_tokens=True) with gr.Blocks(title="Yukkuri Comment Bot") as demo: gr.Markdown("# ゆっくり実況コメント Bot(第10章 Space 用)") situation = gr.Textbox(label="【状況】", lines=2) output = gr.Textbox(label="生成結果", lines=8) gr.Button("生成").click(predict, situation, output) gr.Examples( examples=[["ボス撃破直後"], ["ゲームオーバー"], ["配信終了"]], inputs=situation, ) if __name__ == "__main__": demo.launch()
scripts/data/ch10_dialogues.jsonl(データファイル)
{"situation": "ボス撃破直後", "reimu": "やったのだー!長かったのだ!", "marisa": "まだ油断するな。隠し部屋があるかもしれないZE。"}
{"situation": "初見のステージ", "reimu": "ここ、難しそうなのだ…", "marisa": "まずは敵の動きを観察するのだ。パターンは必ずあるZE。"}
{"situation": "ゲームオーバー", "reimu": "もう一回やるのだ…", "marisa": "セーブデータは残ってる。学習データを増やせばいいだけだZE。"}
{"situation": "レアドロップ", "reimu": "これ、すごい装備なのだ!", "marisa": "運がいいZE。でもステータスより相性を見ろ。"}
{"situation": "チュートリアル", "reimu": "操作、覚えたのだ?", "marisa": "Aボタン連打より、タイミングを合わせる方が大事だZE。"}
{"situation": "隠しボス前", "reimu": "足が震えるのだ…", "marisa": "ここまで来たなら勝てる。LoRA みたいに小さく積み上げた実力だZE。"}
{"situation": "実況開始", "reimu": "みんな、聞いてるのだ?", "marisa": "今日は Hugging Face の総仕上げだ。コメント生成も AI に任せるZE。"}
{"situation": "モデル DL 待ち", "reimu": "まだ終わらないのだ…", "marisa": "初回は数 GB ある。キャッシュに入れば次は速いZE。"}
{"situation": "GPU なし環境", "reimu": "CPU だけなのだ…", "marisa": "小さいモデルと LoRA なら回る。Colab で GPU を借りる手もあるZE。"}
{"situation": "Space 公開直後", "reimu": "URL、取れたのだ!", "marisa": "README に使い方を書いて、フィードバックをもらえZE。"}
{"situation": "コメントが変", "reimu": "口調、おかしいのだ…", "marisa": "データを足して再学習だ。評価→追加→FT のサイクルZE。"}
{"situation": "良いコメントが出た", "reimu": "これ、うちらっぽいのだ!", "marisa": "プロンプトと LoRA が噛み合った証拠だZE。"}
{"situation": "長時間プレイ", "reimu": "眠いのだ…", "marisa": "チェックポイント保存して休憩。過学習も人間に多いZE。"}
{"situation": "協力プレイ", "reimu": "魔理沙、援護してのだ!", "marisa": "任せろ。こっちは推論、そっちはデータ集めだZE。"}
{"situation": "ランキング更新", "reimu": "順位、上がったのだ!", "marisa": "ベースモデルは同じでも、アダプタで個性が出るZE。"}
{"situation": "バグ遭遇", "reimu": "動かないのだ!", "marisa": "エラーメッセージ全文を読め。typo と CUDA OOM が定番ZE。"}
{"situation": "エンディング", "reimu": "クリア、おめでとうなのだ!", "marisa": "本編10章もここまで。付録でコマンド復習だZE。"}
{"situation": "おまけステージ", "reimu": "まだあるのだ?", "marisa": "one more thing は本の外にもある。公式 docs を見ろZE。"}
{"situation": "視聴者コメント", "reimu": "「データ少ない」って言われたのだ", "marisa": "正しい。本書はデモ用。本番は台本を増やせZE。"}
{"situation": "配信終了", "reimu": "おつかれなのだ!", "marisa": "次は自分の Space を duplicate して改造するのだZE!"}
霊夢のメモ帳
- JSONL 台本 →
ch10_prepare_data.py→ LoRA →ch10_generate.py→ Gradio が一本道。 - 日本語は
rinna/japanese-gpt2-medium、流れ確認は--smoke(distilgpt2)。 - 品質は eval → 人間が直す → JSONL 追加 → 再学習 のループで上げる。
魔理沙の one more thing
本番では ベースモデルを Hub に固定 し、adapter だけバージョン管理するとロールバックが楽だZE。
# adapter だけタグ付きで push するイメージ huggingface-cli upload your-username/yukkuri-comment-lora ./log/ch10_lora_adapter/adapter .
複数の口調(実況 / 解説 / ツッコミ)を LoRA 切り替え(第7章 ch07_lora_switch.py)で載せるのも、第10章の発展課題だ。
本書の終わり
魔理沙
第0章の環境構築から、本章の ゆっくり実況コメント Bot まで、本編10章はここで終わりだZE。
霊夢
おつかれなのだ! 付録とかあるの?
魔理沙
docs/00-toc.md の 付録 に、CLI 一覧・トラブルシュート・ハンズオン索引がある。わからなくなったら 該当章に戻る のが一番だ。
霊夢
…次は、うちらの本、Space で配信するのだ!
魔理沙
その意気だ。Hugging Face の世界は、触ってるうちに広がるZE。ゆっくりしていってね!
付録A 環境構築チートシート
第0章の要約+コピペ用コマンド集。困ったら本編
docs/00.mdに戻る。
霊夢
付録って、チートシートなのだ?
魔理沙
その通りだZE。venv・CUDA・Colab・Kaggle を 最短手順 で並べた。ここは会話少なめでコマンド多めだ。
A.1 最小構成(ローカル・CPU でも可)
| 項目 | 推奨 |
|---|---|
| OS | macOS / Linux / WSL2(Windows は WSL 推奨) |
| Python | 3.10 以上(3.11 推奨) |
| ディスク | 空き 10 GB 以上(モデルキャッシュ用) |
| GPU | 任意(NVIDIA + CUDA で学習が楽) |
cd /path/to/yukkuri-hugging-face python3 --version python3 -m venv .venv source .venv/bin/activate # Windows: .venv\Scripts\Activate.ps1 pip install --upgrade pip
本書の基本パッケージ一括:
pip install \ "transformers>=4.40" \ "datasets>=2.18" \ "accelerate>=0.28" \ "huggingface_hub>=0.22" \ "torch" \ "sentencepiece" \ "protobuf"
第7章以降で追加:
pip install "peft>=0.11" "gradio>=4.0"
確認:
python scripts/check_env.py
A.2 venv のよくある操作
# 有効化 source .venv/bin/activate # どの python か which python # パッケージ一覧 pip list | grep -E "transformers|torch|datasets" # venv 削除して作り直し(壊れたとき) deactivate rm -rf .venv python3 -m venv .venv && source .venv/bin/activate pip install --upgrade pip # ↑ A.1 の pip install を再実行
.gitignore に入れておくもの(第0章):
.venv/ __pycache__/ *.pyc .cache/ log/ .env
A.3 CUDA / PyTorch(NVIDIA GPU)
魔理沙
GPU がある人だけだZE。PyTorch Get Started で環境に合う wheel を選ぶ。
CUDA 12.x の一例:
pip install torch --index-url https://download.pytorch.org/whl/cu124
確認:
import torch print(torch.__version__) print("CUDA:", torch.cuda.is_available()) if torch.cuda.is_available(): print(torch.cuda.get_device_name(0))
| 症状 | 確認 |
|---|---|
CUDA: False |
ドライバ / CUDA 版 / torch の組み合わせ不一致 |
CUDA out of memory |
バッチサイズ↓、小さいモデル、LoRA、8bit(第4章) |
A.4 キャッシュ・環境変数
# キャッシュ場所のデフォルト ls ~/.cache/huggingface/ # 別ドライブに逃がす(bash) export HF_HOME="/path/to/large-disk/huggingface" export TRANSFORMERS_CACHE="$HF_HOME/hub"
# オフライン(DL 済みのみ) import os os.environ["HF_HUB_OFFLINE"] = "1"
A.5 Google Colab
Colab 先頭セル例:
!pip install -q "transformers>=4.40" "datasets>=2.18" accelerate peft gradio import torch print(torch.__version__, "CUDA:", torch.cuda.is_available())
| 項目 | メモ |
|---|---|
| GPU | メニュー「ランタイム → T4 GPU」など |
| ドライブ | from google.colab import drive; drive.mount('/content/drive') で永続化 |
| 本書スクリプト | GitHub から clone またはファイルをアップロード |
!git clone https://github.com/your-org/yukkuri-hugging-face.git %cd yukkuri-hugging-face !python scripts/check_env.py
無料枠で GPU が取れない日は 第2章 Pipeline や --smoke で CPU 確認するのだ。
A.6 Kaggle Notebooks
Kaggle でも GPU が使える。流れは Colab と同様だZE。
- kaggle.com で Notebook 作成
- Settings → GPU を ON
- Add Data → 必要ならデータセットをマウント
- 先頭で pip install →
scripts/を実行
import sys !pip install -q transformers datasets accelerate peft sys.path.append("/kaggle/working/yukkuri-hugging-face")
| Colab vs Kaggle | ざっくり |
|---|---|
| Colab | 手軽、Google アカウント |
| Kaggle | 週次 GPU 枠、コンペ・データセットと相性◎ |
A.7 HF トークン(第1章)
pip install -U "huggingface_hub[cli]" huggingface-cli login # または export HF_TOKEN="hf_xxxxxxxx" # .env に書いて git しない
書き込み(push)には write 権限トークンが必要だ。
霊夢のメモ帳(付録A)
- venv 有効化 → pip →
check_env.pyが毎回の第一関門。 - GPU は必須じゃない。Colab / Kaggle は 借りる GPU 用。
- キャッシュは
HF_HOMEで場所を変えられる。
付録B よく使う CLI コマンド一覧
huggingface-cli/git/ 本書scripts/の実行例。
霊夢
コマンド、毎回忘れるのだ…
魔理沙
付録B にまとめた。コピペして、自分の your-username だけ置き換えろZE。
B.1 Hugging Face Hub CLI
インストール・ログイン:
pip install -U "huggingface_hub[cli]" huggingface-cli whoami huggingface-cli login huggingface-cli logout
モデル・データセットのダウンロード:
# モデル全体をキャッシュ huggingface-cli download distilbert-base-uncased-finetuned-sst-2-english # 特定ファイルだけ huggingface-cli download meta-llama/Llama-2-7b-hf config.json --include "*.json" # ローカル dir に展開 huggingface-cli download distilbert-base-uncased-finetuned-sst-2-english --local-dir ./my-model
アップロード(第6章・第10章):
# フォルダをモデル repo に huggingface-cli upload your-username/my-model ./log/ch06_output . # 単一ファイル huggingface-cli upload your-username/my-model ./log/ch10_lora_adapter/adapter/adapter_config.json
キャッシュ:
huggingface-cli scan-cache huggingface-cli delete-cache
B.2 Git(Spaces 用・第9章)
git clone https://huggingface.co/spaces/your-username/your-space-name cd your-space-name # 初回のみ git config user.email "you@example.com" git config user.name "Your Name" git add app.py requirements.txt git commit -m "Update Gradio demo" git push
モデル repo の clone:
git clone https://huggingface.co/bert-base-uncased # 大きいモデルは Git LFS。README の手順に従う
B.3 Python スクリプト(章別・代表)
| 章 | コマンド |
|---|---|
| 0 | python scripts/check_env.py |
| 1 | python scripts/download_model.py --model-id MODEL |
| 1 | python scripts/inspect_model.py --model-id MODEL |
| 2 | python scripts/ch02_pipeline_sentiment.py |
| 3 | python scripts/ch03_tokenizer_compare.py |
| 4 | python scripts/ch04_forward.py |
| 5 | python scripts/ch05_load_dataset.py |
| 6 | python scripts/ch06_finetune.py --epochs 1 |
| 7 | python scripts/ch07_lora_train.py |
| 8 | python scripts/ch08_benchmark.py |
| 9 | python scripts/ch09_gradio_app.py |
| 10 | python scripts/ch10_prepare_data.py && python scripts/ch10_lora_train.py |
ヘルプの見方(共通):
python scripts/ch06_finetune.py --help
B.4 pip / パッケージ
pip install --upgrade pip pip install transformers datasets accelerate peft gradio pip freeze > requirements.txt pip install -r requirements.txt
Space 用 requirements.txt の例:
transformers>=4.40 torch datasets>=2.18 accelerate>=0.28 peft>=0.11 gradio>=4.0 sentencepiece protobuf
B.5 ログ・デバッグ
# 実行ログを保存(第0章) python scripts/check_env.py 2>&1 | tee log/env_check.txt # 詳細ログ(transformers) export TRANSFORMERS_VERBOSITY=debug python scripts/ch02_pipeline_sentiment.py
B.6 環境変数クイックリファレンス
| 変数 | 用途 |
|---|---|
HF_TOKEN |
CLI / Hub 認証 |
HF_HOME |
キャッシュルート |
TRANSFORMERS_CACHE |
モデルキャッシュ |
HF_HUB_OFFLINE |
1 でオフラインのみ |
CUDA_VISIBLE_DEVICES |
使う GPU 番号(例 0) |
WANDB_API_KEY |
W&B 連携(第6章・任意) |
export CUDA_VISIBLE_DEVICES=0 export HF_HOME="$HOME/hf-cache"
霊夢のメモ帳(付録B)
- 読む取り は
huggingface-cli download、書く はlogin+upload。 - Space は git push が基本。
- わからなくなったら
python scripts/xxx.py --help。
付録C モデル選びのフローチャート
第1章・第4章・第10章で触れた「どのモデルを選ぶか」の意思決定用。
霊夢
Hub にモデル多すぎて、選べないのだ…
魔理沙
タスクと言語と GPU で枝刈りするZE。下のフローに沿えばだいたい決まる。
C.1 全体フロー(テキスト版)
スタート: やりたいことは?
│
├─ テキスト分類・感情分析 ──→ Encoder 系(BERT / DistilBERT)
│ pipeline("sentiment-analysis")
│
├─ 翻訳・要約・QA ──────────→ タスク特化 or 多言語 Seq2Seq
│ pipeline("translation") 等
│
├─ 画像分類・検出 ──────────→ ViT / DETR 等(第2章 Vision)
│
├─ 音声文字起こし ──────────→ Whisper 系
│
└─ 文章生成・チャット ──────→ Causal LM(GPT-2 / LLaMA 系)
日本語なら rinna 等
↓ モデル候補が複数
言語は? ─ 日本語必須 → 日本語学習モデルを優先
│
GPU メモリは? ─ 小さい → Distil 系 / LoRA / 8bit
│
ライセンス OK? ─ NG → 別モデル(第1章・1.4)
│
Model Card で限界・学習データを確認
│
小さく試す → pipeline または ch02 スクリプト
│
足りなければ FT(第6章) or LoRA(第7章)
C.2 Mermaid フロー(ビューアで表示)
flowchart TD
A[やりたいタスクは?] --> B{生成が必要?}
B -->|No| C[Encoder: BERT / DistilBERT]
B -->|Yes| D[Causal LM: GPT / LLaMA 系]
C --> E{日本語?}
D --> E
E -->|Yes| F[日本語モデルを Hub で検索]
E -->|No| G[英語ベースで試す]
F --> H{VRAM 十分?}
G --> H
H -->|No| I[LoRA / 小モデル / Colab]
H -->|Yes| J[Full FT も可]
I --> K[Model Card・ライセンス確認]
J --> K
K --> L[pipeline で試走]
C.3 タスク別の出発点(本書で使った ID)
| タスク | 本書の例 | サイズ感 |
|---|---|---|
| 感情分析(英) | distilbert-base-uncased-finetuned-sst-2-english |
小 |
| 感情分析(日) | daigo/bert-base-japanese-sentiment 等(第2章) |
中 |
| 翻訳 | staka/fugumoji-enja 等(第2章) |
中 |
| 分類 FT | distilbert-base-uncased + 自作データ(第6章) |
小 |
| LoRA デモ | distilgpt2 / rinna/japanese-gpt2-medium |
小〜中 |
| 画像 | google/vit-base-patch16-224(第2章) |
中 |
| 音声 | openai/whisper-tiny(第2章) |
小 |
C.4 選定チェックリスト
Hub のモデルページで、次を 上から順に 見る。
- [ ] タスク(Tags:
text-classification,text-generationなど) - [ ] 言語(日本語が含まれるか)
- [ ] ダウンロード数・更新日(あまり古くないか)
- [ ] Model Card(学習データ・限界・偏り)
- [ ] ライセンス(商用可か、クレジット要否)
- [ ] 推論例(README のコードが動くか)
- [ ] 自分の GPU で載るか(パラメータ数・量子化の有無)
C.5 失敗パターンと切り替え
| うまくいかない | 次の一手 |
|---|---|
| 日本語が壊れる | 日本語 Pretrained に変更 |
| OOM | 小モデル / batch↓ / LoRA / 8bit |
| 遅い | Distil 系 / 第8章バッチ計測 |
| 口調が合わない | データ追加 + LoRA(第10章) |
| ライセンス NG | 別モデル。商用なら Apache / MIT を優先 |
霊夢のメモ帳(付録C)
- タスク → 言語 → VRAM → ライセンス の順で絞る。
- まず pipeline で小さく試す。
- Hub の Model Card を読まないと後で詰む。
付録D トラブルシューティング集
第2章末・各章で出たエラーを横断的にまとめた索引。
霊夢
エラー文、英語で怖いのだ…
魔理沙
メッセージのキーワード でこの表を引けZE。全文コピーして検索するのが早い。
D.1 環境・インストール
| エラー・症状 | 原因 | 対処 |
|---|---|---|
ModuleNotFoundError: transformers |
venv 未使用 / pip 未実行 | source .venv/bin/activate → pip install |
Python 3.9 で動かない |
バージョン古い | Python 3.10+ に上げる(付録A) |
pip が遅い / 失敗 |
ネットワーク | 再試行、ミラー、プロキシ設定 |
torch と CUDA 不一致 |
誤った wheel | pytorch.org から再インストール |
D.2 Hub・認証
| エラー・症状 | 原因 | 対処 |
|---|---|---|
401 Unauthorized |
未ログイン / トークン無効 | huggingface-cli login |
403 Forbidden |
gated model 未承認 | Hub ページで利用申請 |
Repository Not Found |
モデル名 typo | ID をコピペで確認 |
HF_TOKEN が効かない |
環境変数未設定 | .env または export(git に載せない) |
huggingface-cli whoami
D.3 ダウンロード・キャッシュ
| エラー・症状 | 原因 | 対処 |
|---|---|---|
| 初回が終わらない | 大容量 DL | 待つ / 小さいモデルで試す |
| ディスク満杯 | キャッシュ肥大 | HF_HOME 変更、huggingface-cli scan-cache |
Connection reset |
ネット不安定 | 再実行、オフラインは DL 後のみ |
huggingface-cli scan-cache
D.4 CUDA・メモリ
| エラー・症状 | 原因 | 対処 |
|---|---|---|
CUDA out of memory |
batch / モデルが大きい | per_device_train_batch_size=1、小モデル、LoRA、8bit |
CUDA available: False |
CPU 版 torch / ドライバ | 付録A の CUDA 手順。CPU でも小モデルは可 |
device-side assert |
ラベル範囲外など | クラス数と num_labels の一致を確認(第6章) |
import torch
torch.cuda.empty_cache()
D.5 Pipeline・推論
| エラー・症状 | 原因 | 対処 |
|---|---|---|
| モデル名 typo | - / `_ 間違い |
Hub URL から正確にコピー |
| 日本語が変 | 英語モデル | 日本語モデルに差し替え(付録C) |
| 出力が短い / 同じ | max_new_tokens 小 |
第4章 generate パラメータを調整 |
pipeline が遅い |
初回 DL + CPU | 2回目以降はキャッシュ。GPU 推奨 |
第2章の代表例:
# 明示的にモデル指定 from transformers import pipeline clf = pipeline("sentiment-analysis", model="distilbert-base-uncased-finetuned-sst-2-english")
D.6 Tokenizer・データ
| エラー・症状 | 原因 | 対処 |
|---|---|---|
padding エラー |
バッチ長不一致 | padding=True または DataCollator(第3章) |
truncate しすぎ |
max_length 小 |
max_length を増やす |
map が遅い |
毎回再計算 | dataset.map(..., load_from_cache_file=True) |
| CSV 列名不一致 | スキーマ違い | ch05_csv_to_dataset.py の列名を合わせる |
D.7 Trainer・学習
| エラー・症状 | 原因 | 対処 |
|---|---|---|
loss=nan |
学習率过大 | learning_rate を下げる(例 2e-5) |
| 過学習 | データ少・epoch 多 | epoch↓、データ増、eval を見る |
| チェックポイントがない | save_strategy |
TrainingArguments で save_steps 設定 |
| W&B エラー | API キー | report_to="none" またはキー設定 |
TrainingArguments(..., report_to="none")
D.8 LoRA・PEFT
| エラー・症状 | 原因 | 対処 |
|---|---|---|
target_modules エラー |
モデル構造違い | c_attn(GPT-2)vs q_proj(LLaMA)を確認 |
| adapter が見つからない | パス違い | log/ch07_lora_adapter/adapter を確認 |
| 生成が英語のまま | 英語ベース | rinna/japanese-gpt2-medium 等に変更(第10章) |
D.9 Gradio・Spaces
| エラー・症状 | 原因 | 対処 |
|---|---|---|
| ポート使用中 | 7860 占有 | --port 7861 |
| Space が起動しない | requirements.txt 不足 |
ログを Space の 「Logs」で確認 |
| GPU Space 課金 | ハードウェア設定 | CPU Basic で小モデルから |
| Secrets 漏洩 | コードに直書き | Space Settings → Secrets |
D.10 デバッグの型
魔理沙
どの章でも使える 3ステップ だ。
1. エラー全文をコピー(1行目の Exception 型まで) 2. 再現する最小コードに縮小(1 batch / 1 example) 3. 該当章のスクリプトを --help でオプション確認
python scripts/ch06_finetune.py --max-train 32 --max-eval 16 --epochs 1
霊夢のメモ帳(付録D)
- venv と login で半分は解決する。
- OOM は小さいモデル・LoRA・batch↓。
- わからなければ 付録F で該当章スクリプトに戻る。
付録E 参考リンク
公式ドキュメント・コース・コミュニティ。URL は執筆時点のもの。404 の場合はサイト内検索を。
霊夢
本、終わったあと、どこを見ればいいのだ?
魔理沙
公式 docs が正解ルートだZE。コミュニティは質問用。リンクはブックマークして使い回せ。
E.1 Hugging Face 公式
| 名前 | URL | 用途 |
|---|---|---|
| Hub トップ | https://huggingface.co | モデル・データセット検索 |
| Transformers ドキュメント | https://huggingface.co/docs/transformers | API リファレンス |
| Datasets ドキュメント | https://huggingface.co/docs/datasets | データ読込・map |
| Hub クライアント | https://huggingface.co/docs/huggingface_hub | CLI・認証・アップロード |
| PEFT | https://huggingface.co/docs/peft | LoRA 等 |
| Accelerate | https://huggingface.co/docs/accelerate | 分散・混合精度 |
| Gradio | https://www.gradio.app/docs | UI |
| Spaces ドキュメント | https://huggingface.co/docs/hub/spaces | デプロイ |
| Model Cards | https://huggingface.co/docs/hub/model-cards | README の書き方 |
| ライセンス一覧 | https://huggingface.co/docs/hub/repositories-licenses | 利用条件 |
E.2 学習コース・チュートリアル
| 名前 | URL | 備考 |
|---|---|---|
| HF 無料コース | https://huggingface.co/learn | NLP / LLM コース |
| Transformers クイックツアー | https://huggingface.co/docs/transformers/quicktour | pipeline の公式版 |
| Open Source AI Cookbook | https://huggingface.co/learn/cookbook | レシピ集 |
| PEFT クイックツアー | https://huggingface.co/docs/peft/quicktour | LoRA 入門 |
E.3 PyTorch・周辺
| 名前 | URL | 備考 |
|---|---|---|
| PyTorch 公式 | https://pytorch.org | |
| Get Started(CUDA) | https://pytorch.org/get-started/locally/ | wheel 選択 |
| bitsandbytes | https://github.com/TimDettmers/bitsandbytes | 8bit / 4bit |
E.4 推論・運用(発展)
| 名前 | URL | 備考 |
|---|---|---|
| Optimum | https://huggingface.co/docs/optimum | ONNX 等(第8章) |
| Text Generation Inference | https://github.com/huggingface/text-generation-inference | TGI |
| vLLM | https://github.com/vllm-project/vllm | 高速推論 |
| Safetensors | https://huggingface.co/docs/safetensors | 重み形式 |
E.5 コミュニティ
| 名前 | URL | 備考 |
|---|---|---|
| HF フォーラム | https://discuss.huggingface.co | 質問・不具合報告 |
| Discord | https://hf.co/join/discord | チャット |
| GitHub transformers | https://github.com/huggingface/transformers | Issue・PR |
| GitHub datasets | https://github.com/huggingface/datasets |
質問するときのコツ:
- transformers / datasets のバージョン - 最小再現コード(10行程度) - エラー全文 - GPU の有無(check_env.py の出力)
E.6 日本語リソース(任意)
| 名前 | 備考 |
|---|---|
| 各社技術ブログ | 日本語モデル(rinna 等)の事例 |
| Qiita / Zenn | 「Hugging Face」「LoRA」で検索 |
| 本書リポジトリ | docs/ 本編 + 付録 |
日本語モデル例(第10章):
E.7 本書内の戻り先
| やりたいこと | 章 |
|---|---|
| 環境 | 第0章、appendix-a.md |
| Hub | 第1章 |
| すぐ動かす | 第2章 |
| 学習 | 第6〜7章 |
| 公開 | 第9〜10章 |
| コマンド | appendix-b.md |
| エラー | appendix-d.md |
| スクリプト一覧 | appendix-f.md |
霊夢のメモ帳(付録E)
- 公式 docs が最優先。
- 詰まったら フォーラム に最小再現付きで質問。
- 本書は 入門の一本道、深掘りは各公式へ。
付録F 各章ハンズオンの完成コード索引
scripts/の一覧と、章・ハンズオンの対応表。各章末尾の「本章スクリプト全文」にコード全文を掲載している。リポジトリなしで読む場合はそちらを参照。
霊夢
スクリプト、増えすぎて迷子なのだ。
魔理沙
章番号と ハンズオン ID で引ける表にした。リポジトリルートで python scripts/... だZE。
F.1 共通
| ファイル | 章 | 説明 |
|---|---|---|
scripts/check_env.py |
0 | 環境・バージョン確認 |
cd /path/to/yukkuri-hugging-face source .venv/bin/activate python scripts/check_env.py
F.2 第1章 — Hub
| ハンズオン | ファイル | 実行例 |
|---|---|---|
| 1-A | (ブラウザ) | Hub で Model Card を読む |
| 1-B | CLI | huggingface-cli login |
| 1-C | scripts/download_model.py |
下記 |
| 1-C | scripts/inspect_model.py |
下記 |
python scripts/download_model.py \ --model-id distilbert-base-uncased-finetuned-sst-2-english python scripts/inspect_model.py \ --model-id distilbert-base-uncased-finetuned-sst-2-english
F.3 第2章 — Pipeline
| ハンズオン | ファイル | 実行例 |
|---|---|---|
| 2-A | scripts/ch02_pipeline_sentiment.py |
日本語感情分析 |
| 2-B | scripts/ch02_pipeline_translation.py |
英日翻訳 |
| 2-C | scripts/ch02_pipeline_compare.py |
モデル比較 |
| (節2.5) | scripts/ch02_pipeline_vision.py |
画像分類 |
| (節2.6) | scripts/ch02_pipeline_whisper.py |
音声認識 |
python scripts/ch02_pipeline_sentiment.py python scripts/ch02_pipeline_translation.py python scripts/ch02_pipeline_compare.py python scripts/ch02_pipeline_vision.py python scripts/ch02_pipeline_whisper.py
F.4 第3章 — Tokenizer
| ハンズオン | ファイル | 実行例 |
|---|---|---|
| 3-A | scripts/ch03_tokenizer_compare.py |
分割比較 |
| 3-B | scripts/ch03_tokenizer_batch.py |
長文バッチ |
| 3-C | scripts/ch03_tokenizer_custom_vocab.py |
語彙追加 |
python scripts/ch03_tokenizer_compare.py python scripts/ch03_tokenizer_batch.py python scripts/ch03_tokenizer_custom_vocab.py
F.5 第4章 — Model
| ハンズオン | ファイル | 実行例 |
|---|---|---|
| 4-A | scripts/ch04_forward.py |
手動 forward |
| 4-B | scripts/ch04_generate.py |
generate パラメータ |
| 4-C | scripts/ch04_quantize.py |
8bit(任意・bitsandbytes) |
python scripts/ch04_forward.py python scripts/ch04_generate.py --temperature 0.8 --top-p 0.9 python scripts/ch04_quantize.py # 任意
F.6 第5章 — Datasets
| ハンズオン | ファイル | 実行例 |
|---|---|---|
| 5-A | scripts/ch05_load_dataset.py |
ag_news 読込 |
| 5-B | scripts/ch05_preprocess.py |
map + トークナイズ |
| 5-C | scripts/ch05_csv_to_dataset.py |
CSV → Dataset |
| データ | scripts/data/ch05_sample_reviews.csv |
5-C 用サンプル |
python scripts/ch05_load_dataset.py python scripts/ch05_preprocess.py python scripts/ch05_csv_to_dataset.py python scripts/ch05_csv_to_dataset.py --csv scripts/data/ch05_sample_reviews.csv
F.7 第6章 — Trainer
| ハンズオン | ファイル | 実行例 |
|---|---|---|
| 6-A/B | scripts/ch06_finetune.py |
分類 FT + ログ |
| 6-C | scripts/ch06_push_hub.py |
Hub に push |
python scripts/ch06_finetune.py --max-train 800 --max-eval 200 --epochs 1 python scripts/ch06_push_hub.py --help # push 前: huggingface-cli login
F.8 第7章 — LoRA
| ハンズオン | ファイル | 実行例 |
|---|---|---|
| 7-A | scripts/ch07_lora_train.py |
LoRA 学習 |
| 7-B | scripts/ch07_lora_save_adapter.py |
adapter 確認 |
| 7-C | scripts/ch07_lora_switch.py |
adapter 切替 |
python scripts/ch07_lora_train.py python scripts/ch07_lora_save_adapter.py python scripts/ch07_lora_switch.py --create-demo-b
出力先の目安: log/ch07_lora_adapter/adapter
F.9 第8章 — 推論最適化
| ハンズオン | ファイル | 実行例 |
|---|---|---|
| 8-A | scripts/ch08_benchmark.py |
バッチサイズ計測 |
| 8-B | (任意) | Docker / TGI は第8章本文参照 |
python scripts/ch08_benchmark.py python scripts/ch08_benchmark.py --batch-sizes 1,2,4,8
F.10 第9章 — Gradio / Spaces
| ハンズオン | ファイル | 実行例 |
|---|---|---|
| 9-A | scripts/ch09_gradio_app.py |
ローカル Gradio |
| 9-B | scripts/app.py |
Space 用テンプレ(感情分析) |
| 9-C | (Space 設定) | CPU / GPU ハードウェア比較 |
python scripts/ch09_gradio_app.py python scripts/ch09_gradio_app.py --port 7860 --share
F.11 第10章 — 総合プロジェクト
| ステップ / お題 | ファイル | 実行例 |
|---|---|---|
| データ | scripts/data/ch10_dialogues.jsonl |
台本 |
| 前処理 | scripts/ch10_prepare_data.py |
Dataset 作成 |
| LoRA | scripts/ch10_lora_train.py |
学習 |
| 推論 | scripts/ch10_generate.py |
CLI 生成 |
| Gradio | scripts/ch10_gradio_app.py |
UI |
| 評価 | scripts/ch10_eval_samples.py |
改善ループ |
| Space | scripts/space_ch10_app.py |
第10章 Space 用 |
python scripts/ch10_prepare_data.py python scripts/ch10_lora_train.py --smoke --epochs 1 python scripts/ch10_lora_train.py --epochs 5 python scripts/ch10_generate.py --situation "ボス撃破直後" python scripts/ch10_gradio_app.py --port 7861 python scripts/ch10_eval_samples.py
出力先の目安:
log/ch10_dataset/ log/ch10_lora_adapter/adapter/ log/ch10_eval.jsonl
F.12 ファイル一覧(アルファベット順)
scripts/
├── app.py
├── check_env.py
├── ch02_pipeline_compare.py
├── ch02_pipeline_sentiment.py
├── ch02_pipeline_translation.py
├── ch02_pipeline_vision.py
├── ch02_pipeline_whisper.py
├── ch03_tokenizer_batch.py
├── ch03_tokenizer_compare.py
├── ch03_tokenizer_custom_vocab.py
├── ch04_forward.py
├── ch04_generate.py
├── ch04_quantize.py
├── ch05_csv_to_dataset.py
├── ch05_load_dataset.py
├── ch05_preprocess.py
├── ch06_finetune.py
├── ch06_push_hub.py
├── ch07_lora_save_adapter.py
├── ch07_lora_switch.py
├── ch07_lora_train.py
├── ch08_benchmark.py
├── ch09_gradio_app.py
├── ch10_eval_samples.py
├── ch10_generate.py
├── ch10_gradio_app.py
├── ch10_lora_train.py
├── ch10_prepare_data.py
├── download_model.py
├── inspect_model.py
├── space_ch10_app.py
└── data/
├── ch05_sample_reviews.csv
└── ch10_dialogues.jsonl
F.13 章ドキュメントとの対応
| 本編 | 付録 |
|---|---|
docs/00.md |
付録A |
docs/01.md |
付録B, F.2 |
docs/02.md |
付録D, F.3 |
docs/03.md |
F.4 |
docs/04.md |
付録C, F.5 |
docs/05.md |
F.6 |
docs/06.md |
F.7 |
docs/07.md |
F.8 |
docs/08.md |
F.9 |
docs/09.md |
F.10 |
docs/10.md |
F.11 |
霊夢のメモ帳(付録F)
- 迷ったら章番号の
chNN_スクリプト を実行。 - 第10章は
ch10_prepare_data→ch10_lora_train→ch10_gradio_appの順。 - 全部通したら 本編10章クリア なのだ!
魔理沙の one more thing
索引は rg "ハンズオン" docs/ で本編の説明に飛べるZE。
rg "ハンズオン" docs/ ls scripts/ch*.py scripts/*.py
本書、ここまでお疲れさまだ。ゆっくりしていってね!
