evex-5.2
ある 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.7944 (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 がこのリポジトリに入っています。
import json
import torch
import sentencepiece as spm
from safetensors.torch import load_file
from model import Config, MicroLM # このリポジトリの model.py
cfg = Config(**{k: v for k, v in json.load(open('config.json')).items()
if k in Config.__dataclass_fields__})
model = MicroLM(cfg)
state = load_file('model.safetensors')
state['head.weight'] = state['embed.weight'] # weight tying。head は保存していない
model.load_state_dict(state)
model.eval()
sp = spm.SentencePieceProcessor(model_file='tok.model')
prompt = '<|c2|><|conv|><|a|>日本の首都どこ<|hi|><|s0|>'
ids = torch.tensor([sp.encode(prompt, out_type=int)])
got = model.generate(ids, max_new_tokens=80, temperature=0.9, top_k=40,
stop_id=sp.piece_to_id('<|end|>'))
print(sp.decode(got[0].tolist())[len(prompt):])
生成は <|end|> か、次の話者トークンが出たところで切ってください。
<|hi|> <|conv|> <|cN|> は生成から禁止してください。プロンプト側が置くもので、本文として出るものではありません (禁止しないと 草<|hi|> のように漏れます)。
切らずに流すと、他の人の発言まで作り続けます。
学習
多段で学習しています。外部データで日本語と会話の土台を作り、最後はこのサーバーのログだけで仕上げるという順序です。 混合比だけで比率を守ろうとすると、外部が数倍あるぶん永久に薄まります。
| 段 | 中身 | 量 |
|---|---|---|
| 段1 | 外部の会話 + このサーバーの素の会話 | 489,523,351 字 |
| 段2 | このサーバーだけ (窓の水増しと切り出し込み) | 191,733,603 字 |
この世代 (5.2) で変えたところ
147人それぞれから同じ本数ずつ発言を切り出して段2 に入れました (1人 2,500 本 = 366,152 会話ぶんの増加)。 5.1 までの切り出しは会話の流れから取っていたので、発言数の多い人ほど多く入る構造でした (最多 55,964 / 最少 203 で 275 倍の開きがあります)。 結果として「この人として喋って」と指名しても、下位の人ほど誰でもない話し方になっていました。 ここだけは匿名化していません — 話者ごとの癖を学ばせるのが目的なので、 その人の発言をその人のトークンに素直に結び付けています。
学習のやり方は evex-4.1 と同じ (AdamW + cosine) に戻しています。 同じデータで Muon + WSD + 文書内マスクを足した対照も回して読み比べましたが、 そちらは「うん」「あ」のような相槌で終わる率が高く、こちらの方が内容のある返しをしました。 あの3つは学習を速く進める工夫であって、出力の質を上げるものではないという結論です。
リアクションの付いた発言 (22,667 件) は段2 の学習データに重く混ぜています。それだけで追加学習する段を試したところ、話題の噛み合いが落ちたので入れていません。
話者トークンは発言数の多い順に 147 人ぶん (全発言の 96.6% を被覆)。 件数は 最多 55,964 / 最少 203 で、275 倍の開きがあります。 順位が小さいほどよく学習されています。
出典とライセンス
段1 に使った外部データ:
| データ | ライセンス |
|---|---|
| OmniAICreator/Japanese-Roleplay-Dialogues | Apache-2.0 |
| nntsuzu/JESC | CC-BY-4.0 |
| p1atdev/open2ch (元: 1never/open2ch-dialogue-corpus) | Apache-2.0 |
注意
- 話者トークン
<|sN|>は匿名の順位で、userId は含まれていません。ただし発言の癖は重みに入っているので、特定の順位で生成した文がその人の書き方に似ることはあります - 表示名は tokenizer の語彙に残っています。
tok.modelはこのサーバーのログから学習しているので、会話の中でよく呼ばれる名前はBPE が 1 トークンにまとめます。実測で 147 人中 16 人 (上位20人では 7 人) の表示名がそのまま語彙にあり、列挙すれば読めます。<|sN|>の番号と名前の対応は含まれていませんが、「このサーバーによく居る人の呼び名」は分かります - 学習元は特定のサーバーの雑談なので、俗語と内輪の話題に強く偏っています。汎用の日本語モデルではありません
- 出力の正しさは保証しません。事実確認の用途には使えません
- 有害表現のフィルタは掛けていません。学習データにも推論にもありません。 以前は外部データにだけ 12 語の正規表現を掛けていましたが、実測すると外部は 0.09% を除去して残り 0%、一方でこのサーバー自身のログはフィルタ無しで 0.59% が残るという逆立ちした状態でした。学習の重心はサーバー側 (計算量の 80%) なので実質的に何も守れておらず、「機械的に除去している」という説明だけが誤解を招くため廃止しました
- Downloads last month
- 163