| --- |
| language: |
| - ja |
| license: mit |
| library_name: pytorch |
| pipeline_tag: text-generation |
| tags: |
| - causal-lm |
| - japanese |
| - from-scratch |
| - tiny |
| --- |
| |
| # evex-2 |
|
|
| ある Discord サーバーの過去ログ **だけ** をゼロから学習した 5.87M パラメータの言語モデル。 |
| 既存モデルからの継続学習ではなく、tokenizer も含めて全部そのログから作った。 |
| **このモデルにとって世界はそのログだけで、一般知識は一切持っていない。** |
|
|
| [evex-1](https://huggingface.co/tako080614/evex-1) の第2世代。コーパスも語彙も構成も |
| 同じで、変えたのは2つだけ。 |
|
|
| ## evex-1 から変えたこと |
|
|
| ### 1. 学習率 3e-4 → 1e-3 |
|
|
| evex-1 は 10 epoch 通して val loss が単調に下がり続けていた = **収束していなかった**。 |
| 上げて 12 epoch 回したら val 4.2404 → 4.0384 (同じ尺度で測った値)。 |
|
|
| ### 2. 正規化記号を損失から外した |
|
|
| evex-1 の一番大きい問題は、**返答の 38.5% が `<url>` や `<file>` だけ**だったこと。 |
| 正規化で作った記号なので発言ではなく、bot は画像もリンクも実際には出せない。 |
|
|
| 原因は密度ではなく1トークンであること。添付だけの発言 (コーパスの 3.6%) が `<file>` |
| 1個に潰れていたので、発言の先頭で確率が1点に集中していた。 |
|
|
| そこで `<file>` `<url>` `<mention>` `<channel>` `<time>` を**損失から外して**学習した。 |
| 文脈からは消していない — 「誰かが画像を貼って、他の人が反応する」流れは学ばせたいので、 |
| 入力には残したまま、その位置の予測だけ評価しない。 |
|
|
| `<nl>` と `<code>` は外していない。改行もコードブロックもモデルが書いて良いもの。 |
|
|
| ## 効果 |
|
|
| 同じチェックポイント同士を**同じ尺度で測り直した**結果 (12 epoch / 96 生成 / |
| プロンプトと乱数を固定): |
|
|
| | | 素 val | マスク val | 記号だけの返答 | |
| |---|---|---|---| |
| | マスクなし | **4.0384** | 4.0465 | **38.5%** | |
| | マスクあり (これ) | 4.0970 | **4.0330** | **0.0%** | |
|
|
| - **素 val** … `<url>` `<file>` も予測対象に含める尺度 |
| - **マスク val** … 記号を損失から外す尺度 = 実際に読まれる語だけの尺度 |
|
|
| 素の尺度で 0.059 負けているのは、**その尺度が `<url>` を当てることを点数にしている** |
| から。出してほしくないトークンなので、そこで負けるのは払って良いコスト。実際に読まれる |
| 語だけで測れば勝っていて、記号だけの返答は消えた。 |
|
|
| **推論時に記号を禁止する必要がなくなった。** evex-1 では `ban_ids` で潰して 38% → 12% |
| に抑えるしかなかったが、あれは高確率のトークンを削って再正規化するので出てくる第二候補が |
| 歪む。損失から外せば確率の質量が最初から実際の語に乗る。 |
|
|
| evex-1 のカードに載せた seed をそのまま引き直すと、記号だけだった3本が全部発言になる |
| (プロンプトは `<|conv|><|s3|>これバグってる?<|other|>`)。 |
|
|
| | seed | evex-1 | evex-2 | |
| |---|---|---| |
| | 1000 | `これでいいです` | `それでも、自分のこと言えば、それは正しいかも` | |
| | 1001 | `<url>` | `なんかあれはcodexがバグってる` | |
| | 1002 | `おしえて` / `おk` / `<url>` | `おしえて` / `おk` / `お疲れ様です` | |
| | 7 | `<file><file><file>` | `いやー` / `あーそれはそっか` | |
|
|
| **中身が正しくなったわけではない。** 5.87M なので固有名詞は形だけ真似て、長く出させれば |
| 崩れる。消えたのは「発言ですらないもの」だけ。 |
|
|
| ## 使い方 |
|
|
| `transformers` の Auto クラスには当てはまらない構成なので、同梱の `modeling_evex.py` を使う。 |
| **tokenizer は evex-1 と同一** (`tok.model` の md5 が一致) なので、既に evex-1 を持って |
| いるならそちらを使い回せる。 |
|
|
| ```bash |
| pip install torch safetensors sentencepiece huggingface_hub |
| hf download tako080614/evex-2 --local-dir evex-2 |
| ``` |
|
|
| `git clone` で取るなら git-lfs が必要。入れずに clone すると `model.safetensors` が |
| 133 バイトのポインタになり、読み込もうとしても壊れる。`ls -la` して 22MB あるか確かめる。 |
|
|
| ```python |
| import json, sys, torch, sentencepiece as spm |
| from safetensors.torch import load_file |
| |
| sys.path.insert(0, "evex-2") |
| from modeling_evex import Config, MicroLM |
| |
| cfg_json = json.load(open("evex-2/config.json")) |
| cfg = Config( |
| vocab_size=cfg_json["vocab_size"], n_layers=cfg_json["n_layers"], |
| d_model=cfg_json["d_model"], n_heads=cfg_json["n_heads"], |
| context=cfg_json["context"], dropout=0.0, attn_dropout=0.0, |
| ) |
| |
| model = MicroLM(cfg) |
| state = load_file("evex-2/model.safetensors") |
| state["head.weight"] = state["embed.weight"] # weight tying を結び直す |
| model.load_state_dict(state) |
| model.eval() |
| |
| sp = spm.SentencePieceProcessor(model_file="evex-2/tok.model") |
| end_id = sp.piece_to_id("<|end|>") |
| |
| prompt = "<|conv|><|s3|>これバグってる?<|other|>" |
| ids = torch.tensor([sp.encode(prompt, out_type=int)]) |
| out = model.generate(ids, max_new_tokens=60, temperature=0.9, top_k=40, stop_id=end_id) |
| print(sp.decode(out[0].tolist())) |
| ``` |
|
|
| `head.weight` は入っていない。weight tying で `embed.weight` と同じテンソルを指しており、 |
| safetensors はストレージを共有したテンソルを保存できないので落としてある。上のように結び直す。 |
|
|
| `ban_ids` は要らない。渡しても害は無いが、記号だけの返答は既に出ない。 |
|
|
| ### プロンプトの形 |
|
|
| 学習データと同じ直列化でないと、モデルは一度も見ていない形を受け取って崩れる。 |
|
|
| ``` |
| <|conv|><|s3|>今日ひま?<|s7|><|re|>ひま<|end|> |
| ``` |
|
|
| | トークン | 意味 | |
| |---|---| |
| | `<\|conv\|>` | 会話の開始 | |
| | `<\|end\|>` | 会話の終了 | |
| | `<\|s0\|>`〜`<\|s47\|>` | 話者。発言数の多い上位48人 (人間の発言の 85.3% を被覆) | |
| | `<\|other\|>` | それ以外の 2,599 人 | |
| | `<\|re\|>` | 直前の誰かへの返信 | |
| | `<nl>` | 発言内の改行 | |
| | `<url>` `<mention>` `<channel>` `<time>` `<file>` | 正規化した URL / メンション / チャンネル / 時刻 / 添付。**このモデルはこれらを出さない** | |
| | `<code>` `</code>` | コードブロック | |
|
|
| 末尾に話者トークンを置くと、その話者として続きを書く。`speakers.json` に各話者の |
| 発言数と**表示名**が入っている。 |
| Discord の user ID は入れていない (モデルの動作に要らず、実アカウントへの手がかりになる)。 |
|
|
| **同じ話者トークンが続けて出ることがある。** 学習データでは話者の塊のうち 27.4% が |
| 2連続以上なので、モデルもそう書く。1発言だけ欲しいなら最初の話者トークンで切る。 |
|
|
| 絵文字・`草`・`www`・顔文字は正規化せず残してあるので、そのまま出る。 |
|
|
| ## 数字 |
|
|
| | | evex-2 | evex-1 | |
| |---|---|---| |
| | パラメータ | 5,868,800 | 5,868,800 | |
| | 学習トークン | 6,685,152 | 6,685,152 | |
| | 語彙 | 4,096 (SentencePiece BPE / byte fallback) | 同じ | |
| | context | 512 | 512 | |
| | 構成 | decoder-only / 6 層 / d_model 256 / 4 head / d_ff 704<br>RoPE + RMSNorm + SwiGLU + weight tying | 同じ | |
| | 学習 | 12 epoch / lr 1e-3 / AdamW / cosine / **CPU のみ** | 10 epoch / lr 3e-4 | |
| | 損失マスク | `<file>` `<url>` `<mention>` `<channel>` `<time>` | なし | |
| | train / val loss | 3.5535 / 4.0559 (マスク尺度) | 3.8685 / 4.2404 (素) | |
| | 記号だけの返答 | **0.0%** | 38.5% | |
|
|
| **データは足りていない。** Chinchilla 最適 (20 トークン/パラメータ) はこの規模だと |
| 33万パラメータで、5.87M は最適の 6%。val は 12 epoch でもまだ下がり続けているので、 |
| 学習の余地は残っている。 |
|
|
| ## できること / できないこと |
|
|
| **できる**: チャットの口調、短い応答、ネットスラング、そのサーバー特有の語彙と話題、 |
| 話者ごとの癖 (ある話者は「〜にゃい」と書くが、それを再現する) |
|
|
| **できない**: 一般常識、推論、数学、コード生成、長い整合性、知らない話題への応答 |
|
|
| 固有名詞は形だけ真似て中身が合わない。`Cloudflare Codex` `AGP Core MT6589` のように、 |
| 見たことのある語を組み替えたものが出る。**事実として読むものではない。** |
|
|
| ## 出どころと制限 |
|
|
| 学習データは**同意を明示的に取っていない実在の人物の会話**。 |
| 逐語での再生は 20 文字以上の完全一致で 0 箇所だったが、669万トークンを12周している |
| ので**実際の発言に近いものが出る可能性は残る**。 |
|
|
| - 生成物を事実として扱ってはいけない |
| - 特定の人物の発言として扱ってはいけない |
| - 話者トークンと表示名の対応は `speakers.json` で公開している。**Discord の user ID は公開していない** |
| - 学習に使ったログそのものは公開していない |
| - 表示名で名指しできる形になっているので、**特定の人が書いたものとして引用してはいけない** |
|
|