evex-5.3
ある Discord サーバーのログだけからゼロから学習した日本語の会話モデル。
既存の重みからの派生ではなく、tokenizer も含めて全部このサーバーのために作ってある。
- パラメータ 25,763,264 (8 層 / d_model 384 / 6 head / context 1024)
- 語彙 12,288 (sentencepiece BPE / byte_fallback)
- decoder-only transformer (RoPE + RMSNorm + SwiGLU) / weight tying
- PLE (Per-Layer Embeddings, d_ple=64) — Gemma 3n と同じ、トークンごと・層ごとの補助ベクトル。行列積ではなく引き表なので、容量は +36% なのに 1 トークンあたりの計算量は +3% しか増えません (CPU 推論で行列積律速なモデルには効きます)
- QK-norm — q/k を RoPE の前に RMSNorm
- val loss 6.9018 (epoch 8)
⚠ プロンプトの形が独特です
**この節を読まずに使うと、まともな出力は出ません。**普通の instruct モデルでも
chat template でもなく、記号だけで会話の構造を表す独自の形です。
<|conv|> から始めて <|end|> で終わる 1 行が 1 会話になります。
<|c2|><|conv|><|s0|>これバグってる?<|hi|><|s3|><|re|><|s0|>どこで止まってる?<|end|>
トークンの意味
| トークン | 個数 | 意味 |
|---|
| `< | conv | >` |
| `< | end | >` |
| `< | c0 | >..< |
| `< | cx | >` |
| `< | s0 | >..< |
| `< | a | >..< |
| `< | z | >` |
| `< | re | >` |
| `< | hi | >` |
<nl> | 1 | 発言の中の改行 |
<url> <file> <mention> <channel> <time> | 5 | 正規化した中身。学習で損失から外してあるので、モデルは自分では書きません |
<code> </code> | 2 | コードブロックの囲み |
組み立ての規則
- **話者トークンと本文を交互に、区切り文字なしで繋げる。**空白も改行も入れません
- 同じ人が続けて喋るときは、そのつどトークンを置きます (
<|a|>今日ひま?<|a|>あ、やっぱいいや)
- 返信は
<話者><|re|><返信先>本文 の順。自分への返信には付けません
- チャンネルは
<|conv|> の前。学習データは 100% がこれで始まるので、省くと分布の外になります
続きを書かせたい人のトークンを末尾に置くと、その人の発言として続きます:
<|c2|><|conv|><|a|>日本の首都どこ<|hi|><|s0|>
使い方
model.py と tok.model がこのリポジトリに入っています。
1import json
2import torch
3import sentencepiece as spm
4from safetensors.torch import load_file
5from model import Config, MicroLM # このリポジトリの model.py
6
7cfg = Config(**{k: v for k, v in json.load(open('config.json')).items()
8 if k in Config.__dataclass_fields__})
9model = MicroLM(cfg)
10state = load_file('model.safetensors')
11state['head.weight'] = state['embed.weight'] # weight tying。head は保存していない
12model.load_state_dict(state)
13model.eval()
14
15sp = spm.SentencePieceProcessor(model_file='tok.model')
16prompt = '<|c2|><|conv|><|a|>日本の首都どこ<|hi|><|s0|>'
17ids = torch.tensor([sp.encode(prompt, out_type=int)])
18got = model.generate(ids, max_new_tokens=80, temperature=0.9, top_k=40,
19 stop_id=sp.piece_to_id('<|end|>'))
20print(sp.decode(got[0].tolist())[len(prompt):])
生成は <|end|> か、次の話者トークンが出たところで切ってください。
<|hi|> <|conv|> <|cN|> は生成から禁止してください。プロンプト側が置くもので、本文として出るものではありません (禁止しないと 草<|hi|> のように漏れます)。
切らずに流すと、他の人の発言まで作り続けます。
学習
多段で学習しています。外部データで日本語と会話の土台を作り、最後はこのサーバーのログだけで仕上げるという順序です。
| 段 | 中身 | 量 |
|---|
| 段1 | 外部の会話 + このサーバーの素の会話 (evex 30.0%) | 601,794 会話 / 489,523,351 字 |
| 段2 | このサーバーだけ (窓の水増しと切り出し込み) | 1,314,843 会話 / 238,803,759 字 |
**段1 は evex-5.2 のものをそのまま使っています。**語彙も同じなので、5.2 との違いは
段2 のデータと損失の当て方だけです。初期値・形・最適化 (AdamW + cosine)・
学習率 (4.2e-4 / batch 24)・epoch 数 (8) まで揃えてあります。
この世代 (5.3) で変えたところ
指名した人らしさ (/as) を強くするために、話者ごとの切り出しを 2.9 倍にしました。
5.2 は「1人あたり 2,500 本まで」という上限を掛けていたので、
- 発言の多い人 (55,964 件) は 2,500 本で頭打ち = 手持ちの 4.5% しか使っていない
- 発言の少ない人 (203 件) は 4 倍の 812 本止まり
という、多い人は絞られ、少ない人は伸びていない形でした。上限を外し、少ない人だけ
繰り返しで底上げする (8 倍まで) ように変えています。
| 5.2 | 5.3 |
|---|
| 話者ごとの切り出し | 296,324 本 / 38.4M字 | 867,353 本 / 86.0M字 |
| 発言の少ない人 (203 件) | 812 本 | 1,624 本 |
| 発言の多い人 (55,964 件) | 2,500 本 | 55,964 本 |
**あわせて、切り出しの中では「その人の発言」だけを損失に入れています。**切り出しは
[前の2発言] + [その人の発言] の形なので、そのままだと勾配の 2/3 が他人の話し方を
学ぶのに使われます。対象の発言だけ残すと、同じ本数で勾配が約 3 倍濃くなります
(段2 全体では 78.2% のトークンが損失に入ります)。
効果の実測
val にある本物の発言を、本人の話者トークンで条件付けたときと他人のトークンのときで
loss を比べ、「本人の方が低い」割合を数えました (368 件 / 偶然なら 50%)。
生成を挟まないので決定的に測れます。
| evex-5.2 | evex-5.3 |
|---|
| 全体 | 72.3% (8.5σ) | 75.0% (9.6σ) |
| 発言の多い上位 | 75.8% | 78.8% |
| 発言の少ない下位 | 65.6% (3.5σ) | 68.0% (4.1σ) |
| 逐語コピー (20字以上) | 0.0% | 0.0% |
上位・下位・全体すべてが同じ向きに動いています。繰り返しを 8 倍 (8 epoch で最大 64 回)
にしても逐語コピーは 0.0% のままで、丸暗記には至っていません。
一方で**話題の噛み合いは 30.0% → 25.0% に下がっています。**素の会話の窓が段2 に占める
割合が 48.7% → 39.2% に落ちたぶんだと思われます (絶対量は据え置き)。
一問一答よりも「その人らしさ」に寄せた世代です。
話者トークンは発言数の多い順に 147 人ぶん (全発言の 96.6% を被覆)。
件数は 最多 55,964 / 最少 203 で、275 倍の開きがあります。
順位が小さいほどよく学習されています。
出典とライセンス
段1 に使った外部データ:
注意
- **話者トークン
<|sN|> は匿名の順位で、userId は含まれていません。**ただし発言の癖は重みに入っているので、特定の順位で生成した文がその人の書き方に似ることはあります
- 表示名は tokenizer の語彙に残っています。
tok.model はこのサーバーのログから学習しているので、会話の中でよく呼ばれる名前はBPE が 1 トークンにまとめます。実測で 147 人中 16 人 (上位20人では 7 人) の表示名がそのまま語彙にあり、列挙すれば読めます。<|sN|> の番号と名前の対応は含まれていませんが、「このサーバーによく居る人の呼び名」は分かります
- 学習元は特定のサーバーの雑談なので、俗語と内輪の話題に強く偏っています。汎用の日本語モデルではありません
- 出力の正しさは保証しません。事実確認の用途には使えません
- 有害表現のフィルタは掛けていません。学習データにも推論にもありません。
以前は外部データにだけ 12 語の正規表現を掛けていましたが、実測すると外部は 0.09% を除去して残り 0%、一方でこのサーバー自身のログはフィルタ無しで 0.59% が残るという逆立ちした状態でした。学習の重心はサーバー側 (計算量の 80%) なので実質的に何も守れておらず、「機械的に除去している」という説明だけが誤解を招くため廃止しました