evex-1

ある Discord サーバーの過去ログ だけ をゼロから学習した 5.87M パラメータの言語モデル。 既存モデルからの継続学習ではなく、tokenizer も含めて全部そのログから作った。 このモデルにとって世界はそのログだけで、一般知識は一切持っていない。

使い方

transformers の Auto クラスには当てはまらない構成なので、同梱の modeling_evex.py を使う。

pip install torch safetensors sentencepiece

取り方は2つ。

# huggingface_hub を使う
pip install huggingface_hub
hf download tako080614/evex-1 --local-dir evex-1
# git clone でも取れる。ただし git-lfs が必要
git lfs install
git clone https://huggingface.co/tako080614/evex-1

git-lfs を入れずに clone すると使えない。 model.safetensors が 133 バイトの ポインタファイルになり (tok.model も同じ)、読み込もうとしても壊れる。 ls -la して 22MB あるか確かめるのが早い。

import json, sys, torch, sentencepiece as spm
from safetensors.torch import load_file

sys.path.insert(0, "evex-1")
from modeling_evex import Config, MicroLM

cfg_json = json.load(open("evex-1/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-1/model.safetensors")
state["head.weight"] = state["embed.weight"]      # weight tying を結び直す
model.load_state_dict(state)
model.eval()

sp = spm.SentencePieceProcessor(model_file="evex-1/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 はストレージを共有したテンソルを保存できないので落としてある。上のように結び直す。

プロンプトの形

学習データと同じ直列化でないと、モデルは一度も見ていない形を受け取って崩れる。

<|conv|><|s3|>今日ひま?<|s7|><|re|>ひま<|end|>
トークン 意味
`< conv
`< end
`< s0
`< other
`< re
<nl> 発言内の改行
<url> <mention> <channel> <time> <file> 正規化した URL / メンション / チャンネル / 時刻 / 添付
<code> </code> コードブロック

末尾に話者トークンを置くと、その話者として続きを書く。speakers.json に各話者の 発言数と表示名が入っている。 Discord の user ID は入れていない (モデルの動作に要らず、実アカウントへの手がかりになる)。

絵文字・www・顔文字は正規化せず残してあるので、そのまま出る。

数字

パラメータ 5,868,800
学習トークン 6,685,152 (train 6.50M / val 0.19M)
語彙 4,096 (SentencePiece BPE / byte fallback)
context 512
構成 decoder-only / 6 層 / d_model 256 / 4 head / d_ff 704
RoPE + RMSNorm + SwiGLU + weight tying
学習 10 epoch / AdamW / cosine / CPU のみで 194 分
train / val loss 3.8685 / 4.2404

学習データは 585,628 メッセージ (bot を除外後) を会話単位に切った 54,934 会話。 1トークンあたり 2.40 文字。

データが足りていない。 Chinchilla 最適 (20 トークン/パラメータ) はこの規模だと 33万パラメータで、5.87M は最適の 6%。それでも val loss は 10 epoch 通して単調に 下がり続けたので (5.17 → 4.24)、まだ学習の余地が残っている。

できること / できないこと

できる: チャットの口調、短い応答、ネットスラング、そのサーバー特有の語彙と話題、 話者ごとの癖 (ある話者は「〜にゃい」と書くが、それを再現する)

できない: 一般常識、推論、数学、コード生成、長い整合性、知らない話題への応答

出力例 (epoch 10):

> これバグってる?
これでいいです

> Cloudflare Containers ってどうなん
AGPLだったら
CTFが終わってる

<file><url> ばかり返るとき

<file> (添付だけの発言) と <url> は学習データで非常に頻出なので、5.87M では そちらに落ちることが普通にある。同じプロンプトでも乱数の引きで変わる。

seed 1000 -> これでいいです
seed 1001 -> <url>
seed 1002 -> おしえて / おk / <url>
seed 7    -> <file><file><file>

generate に外す機能が入っている。実測で使えない返答が 38% から 12% に下がる

ban = [sp.piece_to_id(t) for t in ("<file>", "<url>", "<mention>", "<channel>", "<time>")]
out = model.generate(ids, max_new_tokens=60, temperature=0.9, top_k=40,
                     stop_id=end_id, ban_ids=ban, min_new_tokens=2)

min_new_tokens はそれまで <|end|> を出させない (何も言わずに終わるのを防ぐ)。 残る 12% は「これ」のような 2 文字なので、短すぎたら引き直すとよい。

出どころと制限

学習データは同意を明示的に取っていない実在の人物の会話。 逐語での再生は 20 文字以上の完全一致で 0 箇所だったが (10 epoch すべてで確認)、 669万トークンを10周しているので実際の発言に近いものが出る可能性は残る

  • 生成物を事実として扱ってはいけない
  • 特定の人物の発言として扱ってはいけない
  • 話者トークンと表示名の対応は speakers.json で公開している。Discord の user ID は公開していない
  • 学習に使ったログそのものは公開していない
  • 表示名で名指しできる形になっているので、特定の人が書いたものとして引用してはいけない
Downloads last month
-
Safetensors
Model size
5.87M params
Tensor type
F32
·
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support