review25: S 本番の初回起動で「en の PPL が良すぎる」→ 学習データの 9% がゴミだった — 原因・修正・シャード v3(5.53B トークン)と再起動手順
作成日: 2026-10-09 対象: review24 の本番起動後、500 ステップ目の評価行で見つかった異常の調査と対処。3060 の学習は止めていただき(ご承認済み)、データを直して再エンコードした。本書は、貼っていただいた出力をそのまま引用する形でまとめる(今後もこの方針)。
要旨
- 本番起動は 2 回行った。1 回目は
git pull前のコードで途中評価がfrしか出ず、最初からやり直し。2 回目で 4 言語が出たが、enの PPL が 500 ステップで 10.26 と異常に低かった(1〜2 章) - 調べると、ある en チャンネル系列のフィード 57,246 記事(en 記事の 1.44%)の本文が画像メタデータの構造体ダンプで、本文欄の上限 65,535 字まで続き、記事本文は失われていた。1 記事 4 万トークン・極端に反復的で、 en val トークンの 74%、train トークンの約 9%(≈5.5 億) を占めていた(3 章)
textprep.clean()にダンプ除去を実装し(コミット 1721e07)、再エンコード → シャード v3: 113 個・5.53B トークン。 en val の反復長文は 73.9% → 0.00%、val/test の言語構成が 4 言語ほぼ均等に(4〜5 章)- 3060 で
fetch_shards.pyを再実行(差分のみ)→ 本番を最初から再起動する手順を 6 章に(所要は変わらず約 21 時間) - 訂正を含む副産物: 調査の最後に、ダンプの正体は「記事ページの HTML 構文木」で、本文テキストもその中に残っていることが分かった(最初は「本文は失われた」と判断していた)。テキストノードだけを拾う救済コードを実装・コミット済み(866b7a2)。復元できる本文は約 4,100 万トークン(train の 0.75%)。v3 はこの救済を含まないため、v4 を今作るか(+2 時間)、v3 で走らせるかをご判断ください(7 章・確認事項 2)
1. 本番起動 1 回目: fr しか出ない
review24 の手順で起動していただいた 500 ステップ目付近:
{"step": 470, "loss": 5.3001, "lr": 0.0005639999999999999, "grad_norm": 0.685, "tok_per_s": 38955, "elapsed_s": 395}
{"step": 480, "loss": 5.1992, "lr": 0.0005759999999999999, "grad_norm": 0.724, "tok_per_s": 38942, "elapsed_s": 404}
{"step": 490, "loss": 5.4063, "lr": 0.000588, "grad_norm": 0.765, "tok_per_s": 38934, "elapsed_s": 412}
{"step": 500, "loss": 4.874, "lr": 0.0006, "grad_norm": 0.722, "tok_per_s": 38923, "elapsed_s": 421}
{"step": 500, "eval": "val", "all": 230.03, "fr": 230.03}
{"step": 510, "loss": 5.2678, "lr": 0.0005999999865267472, "grad_norm": 0.72, "tok_per_s": 38185, "elapsed_s": 438}
学習自体は健全(loss 10.86 → 4.9、38,900 トークン/秒)でしたが、評価行が fr だけ。評価窓の等間隔サンプリング(review24 修正 2、c13ff4d)が入る前のコードで起動されていました。始めたばかりだったので git pull → 最初からやり直し(review24 の手順に git pull を含めていましたが、タイミングの行き違いでした)。
2. 本番起動 2 回目: 4 言語は出たが en が良すぎる
{"step": 500, "loss": 4.8673, "lr": 0.0006, "grad_norm": 0.663, "tok_per_s": 39130, "elapsed_s": 419}
{"step": 500, "eval": "val", "all": 48.08, "en": 10.26, "de": 371.6, "fr": 225.74, "ja": 2836.56}
4 言語が揃いました。しかし en が 10.26 は低すぎます。学習 loss 4.87 は PPL にして約 130。モデルがまだ 1,600 万トークンしか見ていない段階で、val(初見の文章)の方が train より遥かに易しいというのは、「英語が上手い」ではなく「en の val が異様に予測しやすい何かで占められている」と読むべき数字です。 review21 で懸念した「en の val は 1 日分しかない」問題の顕在化、を最初に疑いました。
3. 調査: en の val に何が入っているか
3-1. 記事の長さと重複(サーバで val シャードを直接読む)
en: docs 2953, mean 2120 tok, median 450 | 完全重複 174 (5.9%) | 先頭64tok重複 57 (1.9%)
de: docs 2799, mean 517 tok, median 418 | 完全重複 38 (1.4%) | 先頭64tok重複 46 (1.6%)
fr: docs 2403, mean 594 tok, median 522 | 完全重複 63 (2.6%) | 先頭64tok重複 44 (1.8%)
ja: docs 2974, mean 368 tok, median 262 | 完全重複 0 (0.0%) | 先頭64tok重複 2 (0.1%)
en だけ 平均 2,120 トークンに対し中央値 450 — 少数の巨大記事が en のトークン量を支配しています。評価はトークン単位で集計するので、巨大記事の性質がそのまま en の PPL になります。完全重複 5.9% は通信社配信(同じ記事が複数媒体に載る)で、これは PPL 10 の説明にはなりません。
3-2. 巨大記事の正体
長い順に 5 本、トークン数・圧縮率(zlib。小さいほど内部の繰り返しが多い。通常の記事は 0.74)・冒頭と中盤:
40628 tok zlib 0.10 (省略: 見出し)
中盤: (省略: 画像メタデータの構造体。URL を含む)
40608 tok zlib 0.10 (省略: 見出し)
40557 tok zlib 0.10 (省略: 見出し)
en total tokens 6,259,237; docs>5k tok: 166 holding 74.0%
反復的な長文(>2k tok, zlib<0.30): 169 件、en トークンの 73.9%
見出しと日付のあとに、画像メタデータの構造体(Ruby のハッシュ表記 {"w"=>220, "h"=>93, "fileurl"=>"https://…"})が延々と続く記事でした。最長記事を調べると:
最長記事: 65,515 字。ブロブ開始位置 109 / 終了位置 65,345
ブロブ前: (省略: 見出しと日付)
ブロブ後: (省略: 画像の派生サイズ一覧の続き。URL を含む)
65,515 字 = 本文欄(TEXT 型)の上限 65,535 字の直前。収集側の不具合で、本文の代わりに記事ページの内部構造(HTML 構文木を Ruby のハッシュとして文字列化したもの)がそのまま保存され、上限で切れていたのです。構造の大半は画像の派生サイズ一覧("_26x11_220"=>{"w"=>220, "h"=>93, "fileurl"=>…} が画像ごとに十数個)で、これが 4 万トークンの嵩の正体。こういう記事は 166 本で en val の 74% のトークンを占め、構造が単純反復なのでモデルがすぐ覚える → PPL 10 の正体でした。(この時点では「記事本文そのものは失われている」と判断しましたが、7 章のとおり本文は構造体の中にテキストノードとして残っていました。)
3-3. train 側の汚染(抜き打ち 3 シャード)
train_000.bin: 反復的長文 338 件 / 60468 記事, トークンの 9.6%
train_050.bin: 反復的長文 340 件 / 61181 記事, トークンの 8.3%
train_100.bin: 反復的長文 367 件 / 61366 記事, トークンの 9.3%
学習データの約 9%(≈5.5 億トークン)が同じゴミ。 走っていた学習はこれを「英語」として学んでいたことになります。この時点で学習停止をお願いし、ご承認いただきました。
3-4. 全コーパスでの規模(スナップショット全走査)
"hit_docs": {"en": 57246} ← 汚染記事(=> 表記が 3 回以上)
"hit_doc_pct": {"en": 1.439, "ja": 0.0, "de": 0.0, "fr": 0.0}
"hit_char_pct": {"en": 6.59, "ja": 0.0, "de": 0.0, "fr": 0.0}
"hit_chars_total_M": 1124.5
"top_channels": (省略: チャンネル番号の一覧)
- en だけ、57,246 記事(en の 1.44%)、11.2 億字(en 本文の 6.6%)。de/fr/ja には皆無
- 同じ系列の複数チャンネルに一貫して発生。収集アプリ側のパーサがこの系列のフィード形式を扱えず、構造体をそのまま文字列化して保存していた、と推定(収集アプリのソースは失われているので確定はできない)
- 文字では 6.6% だがトークンでは約 9%: URL や記号だらけでトークン効率が悪いため
なお review20 で「HTML 残骸は 0.005%」と報告したのは <p> 等のタグを数えたもので、この Ruby ハッシュ形式は検出対象外でした。 「目で見る」(peek_shard)と「数字が良すぎたら疑う」の両方が無ければ気づけなかった汚染です。
4. 対処: textprep.clean() にダンプ除去(コミット 1721e07)
strip_dump(): 本文中に {"キー"=> で始まる構造体が現れたら、そこから後ろを捨てる。通常の文中の => や {…} には反応しない。
'Headline May 23 {"type"=>"image", "fileurl"=>"https://x/y.jpg", "_26x11"=>{"w"=>220}}'
→ 'Headline May 23'
'Normal text with => arrow and {braces} stays: a => b' → そのまま
real doc: 65,515 chars -> 108 chars (省略: 残った見出しと日付)
v3 では汚染記事は「見出し+日付」だけの短い記事(約 37 トークン)として残ります。スモークテスト(3 万行)合格の上で再エンコードを実行。所要 67 分 + シャード化。(その後、構造体の中から本文を取り出せることが分かり、strip_dump() を改良しました — 7 章。v3 には未反映。)
5. シャード v3 の検証
train: 111 shards, 5.510B tok, 7,448,909 docs | en 61.3%, de 24.1%, fr 11.1%, ja 3.6%
val : 1 shards, 0.006B tok, 11,129 docs | en 28.8%, de 25.9%, fr 25.6%, ja 19.7%
test : 1 shards, 0.015B tok, 30,171 docs | en 29.7%, de 25.9%, fr 25.2%, ja 19.2%
train_033.bin sha256 MATCH / val_000.bin sha256 MATCH / test_000.bin sha256 MATCH
en val: 2953 docs, mean 543 tok, median 244, max 10110 | 反復的長文のトークン比 0.00% (v2: 73.9%)
| 項目 | v2(9/27) | v3(10/9) |
|---|---|---|
| 総トークン | 6.106B | 5.53B(−9.4%) |
| train | 122 シャード / 6.068B | 111 シャード / 5.510B |
| val / test トークン | 10.2M / 27.2M | 5.6M / 15.4M(en のゴミが消えた分) |
| val の言語構成(トークン) | en 61 / de 14 / fr 14 / ja 11 % | en 29 / de 26 / fr 25 / ja 20 %(ほぼ均等) |
| en val の反復長文 | 73.9% | 0.00% |
| 記事数(全分割) | 7,490,209 | 7,490,209(同じ。汚染記事は短くなっただけ) |
v2 → v3 で長さが変わった記事は en の 194 件のみ(de/fr/ja は 0 件)で、切り落としが汚染記事に限られたことも確認しました。記事別言語の副ファイル(val_000.langs.u8 等)も再生成済み。
副産物として、総トークンが 5.5B になったことで Chinchilla 則の計算最適は ≈275M(以前 ≈300M)。L(500M)が「やや大きい」度合いが少し増えますが、方針は変えません。
6. 3060 での再取得と再起動(手順)
学習は止めていただいているので、3060 機で:
cd ~/zxell/zxell_model && . .venv/bin/activate
git pull
# (省略: 接続先と API キーの設定)
python fetch_shards.py ~/zxell-shards # sha256 が変わったシャードだけ取り直し(ほぼ全部 ≈ 11GB、数分)
ls ~/zxell-shards | wc -l # 229 なら揃っています(113×2 + langs 2 + index 1)
注意: 旧 v2 の train_111.bin〜train_121.bin(11 個 + .json)が ~/zxell-shards に残ります。 学習は train_*.bin を全部拾うので、 必ず消してください(残すとゴミ入りのシャードが混ざります):
rm ~/zxell-shards/train_11[1-9].bin* ~/zxell-shards/train_12[01].bin*
ls ~/zxell-shards/train_*.bin | wc -l # 111 なら OK
そして最初から:
tmux attach -t zxell # (無ければ tmux new -s zxell)
rm -rf runs/S_baseline && mkdir -p runs/S_baseline
python train.py --preset S --shards ~/zxell-shards --out runs/S_baseline --steps 90000 2>&1 | tee runs/S_baseline/stdout.log
ステップ数は 90,000 のまま(2.95B トークン = train 5.51B の約半分)。所要も約 21 時間で変わりません。 500 ステップ目の評価行で en が他言語と同じ桁(数百)に落ち着いていれば正常です。
7. 訂正: 本文はダンプの中に残っていた — 救済コード(v3 には未反映)
v2 → v3 で短くなった en val 194 件のうち 17 件は、画像の派生サイズ一覧ではなく 本文の HTML 構文木が先頭から見えるダンプでした:
[v2 1861 tok -> v3 37 tok]
v2: '(省略: 見出しと日付) {"type"=>"html", "tag"=>"p", "attribs"=>nil, "content"=>[{"type"=>"text", "content"=>"(省略: 本文)"}, '
v3: '(省略: 見出しと日付)'
本文が "type"=>"text", "content"=>"…" のノードに残っています。strip_dump() を「構造体の中のテキストノードだけ拾って見出しの後ろに繋ぐ」ように改良し(コミット 866b7a2)、実データで確認したところ、画像メタデータだらけに見えた 4 万トークンの記事からも本文が出てきました:
HTML tree dump: 4603 chars -> 2482 chars (省略: 復元された本文)
image dump : 65515 chars -> 1578 chars (省略: 復元された本文)
plain : 'Normal text => arrow, {braces}, "quoted" fine.'
つまり「本文は失われていた」は誤りで、構造体の中に本文が丸ごと入っていた(画像一覧の嵩に埋もれていただけ)が正しい理解です。 en val の汚染 194 件すべてで本文が取り出せました:
汚染 en val 記事 194 件: v3(見出しのみ) 計 7,346 tok → 救済版 計 140,110 tok(平均 722 tok/記事、本文を取り出せなかった記事 0 件)
コーパス全体の見積り: 57,246 件 × 722 ≈ 41M トークン(train 5.51B の 0.75%)
v4(救済込み)を作ると、train に 約 4,100 万トークンの本物の英語ニュースが戻り、57,246 本の「見出しだけの記事」が普通の記事になります。量としては 0.75% で、モデルの出来に目に見える差は出ないと思います。一方で、シャードはフェーズ 2〜4 でも同じものを使い回す基準データなので、「後で直すなら今」という考え方もあります(作り直し 2 時間、3060 の再取得 数分)。推奨はどちらとも言い難いので、お好みでお決めください(確認事項 2)。
8. 今回の学び
- 評価の数字が「良すぎる」ときはデータを疑う。 500 ステップで en PPL 10 は、英語が上手いのではなくデータが壊れている合図だった
- 汚染の検出に効いたのは、記事長の平均と中央値の乖離、zlib 圧縮率(反復度)、peek_shard による目視の 3 つ。タグや文字実体参照の検出(review20)では見えない種類だった
- 1 回目の起動が
frだけだったおかげで「評価行を注意深く見る」流れができ、2 回目のen 10.26を見逃さずに済んだ
9. 確認事項
- 旧 v2 シャード(サーバに退避してあるもの、11.3GB)を削除してよいですか? v3 検証済みで、v2 を使う場面はありません。
- 本文救済(7 章)を v4 として今作るか、v3 で進めるか。 「v4」なら、ご返事をいただいた時点でこちらで再エンコードを始め(約 2 時間)、完了後に 6 章と同じ手順で再取得 → 再起動です。「v3 で進める」なら 6 章をそのまま実行してください。
- 再起動後、500 ステップ目の
"eval"行を貼ってください(enが数百の桁に戻っていることを確認します)。
10. 次の作業予定
- S ベースライン(v3 データ)の学習 約 21 時間 → 完了後に test の言語別 PPL・生成サンプル → フェーズ 1 の完了判定(review26)
- 学習中:
log.jsonlから損失曲線・言語別 PPL 推移を可視化するスクリプト - フェーズ 2 の設計(クライアント v1 の
trainタスクを本物の局所学習に、1050 向けトークン範囲タスク、サーバ側の Δ 集約)
追記(2026-10-10): v4 完成 — 救済込みの最終版
ご判断(v2 削除 OK / v4 を作る)を受けて再エンコードしました。v2 は削除、v3 は shards_v3/ に退避。
done: 114 shards, 5.57B tokens total
train: 112 shards, 5.551B tok, 7,448,909 docs | en 61.6%, de 23.9%, fr 11.0%, ja 3.5%
val : 1 shards, 0.006B tok, 11,129 docs | en 30.4%, de 25.3%, fr 25.0%, ja 19.2%
test : 1 shards, 0.016B tok, 30,171 docs | en 31.3%, de 25.3%, fr 24.6%, ja 18.7%
train_077.bin sha256 MATCH / val_000.bin sha256 MATCH / test_000.bin sha256 MATCH
en val: 2953 docs, mean 588 tok, median 420, max 10110 | 反復的長文 0.00% | '=>' を含む記事 0 件
救済確認: 434 tok (省略: 復元された本文の冒頭)
| 項目 | v2 | v3 | v4 |
|---|---|---|---|
| 総トークン | 6.106B | 5.53B | 5.57B(v3 + 救済 41M) |
| train シャード | 122 | 111 | 112 |
| en val 中央値(トークン) | 450 | 244 | 420(本文が戻った) |
en val の反復長文 / => 残存 | 73.9% / あり | 0% / なし | 0% / なし |
4 万トークンの汚染記事だった最長の記事は、434 トークンの普通の記事になりました。 これをフェーズ 1〜4 の基準データとします。 旧 v3(shards_v3/、11GB)は確認後に削除します(確認事項)。
3060 での再取得と再起動(6 章の更新版)
cd ~/zxell/zxell_model && . .venv/bin/activate
git pull
# (省略: 接続先と API キーの設定)
python fetch_shards.py ~/zxell-shards # 変わったシャードだけ取り直し(ほぼ全部 ≈ 11GB)
rm ~/zxell-shards/train_11[2-9].bin* ~/zxell-shards/train_12[01].bin* # v2 時代の残骸 10 個を削除
ls ~/zxell-shards/train_*.bin | wc -l # 112 なら OK
ls ~/zxell-shards | wc -l # 231 なら OK(114×2 + langs 2 + index 1)
tmux attach -t zxell # (無ければ tmux new -s zxell)
rm -rf runs/S_baseline && mkdir -p runs/S_baseline
python train.py --preset S --shards ~/zxell-shards --out runs/S_baseline --steps 90000 2>&1 | tee runs/S_baseline/stdout.log
確認事項(追記分)
- 旧 v3 シャード(サーバに退避してあるもの、11GB)を削除してよいですか?
- 再起動後、500 ステップ目の
"eval"行を貼ってください。
zxell.ai