Skip to content

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

2LM-MLX-GAL — 自作ミニ言語モデルに、キャラクターを持たせる

公開データセットだけで学習した言語モデルは、機械翻訳調の敬語で喋ります。 元にしたデータがそうだからです。モデルの人格は、データの人格です。

このリポジトリは、学習データを自分で作ってモデルの口調を差し替えるところまでを 一式にしたものです。題材は架空の「ギャルのチャット」会話。実在の人物・会話とは関係のない 完全な架空データです。

ギャル版のCLIチャット

  • ベースモデル: ミニGPT / 6層 / 384次元 / 文脈256トークン / 13.8M パラメータ
  • データ生成: ローカルの Qwen2.5-32B-Instruct(Apache-2.0)。外部APIを使いません
  • データセット: 2,610会話 / 111,574文字(同梱。80分で生成、棄却率18.7%)
  • 追加学習: 40秒 / 90ステップ
  • 環境: MacBook Pro M1 Max(ユニファイドメモリ 64GB)で実測

シリーズの位置づけ

リポジトリ 内容 状態
1LM 1時間で動くものを作る。Transformer を自分で書く 凍結
2LM-MLX 会話が成立するレベルに持っていく。評価設計・データ増量・サブワード化 凍結
2LM-MLX-GAL(ここ) 学習データを自分で作って、キャラクターを持たせる

このリポジトリは単体で完結しています。 学習パイプラインも事前学習済みの重みも 同梱しているので、他をクローンする必要はありません。 ただし事前学習そのものの解説(評価指標の設計、対照実験、サブワード化)は 2LM-MLX 側にあります。そこは重複させていません。

すぐ試す

ギャル版の重みを同梱しているので、クローンすれば会話できます。

CONDA_SUBDIR=osx-arm64 conda create -n 2lm python=3.11 -y
conda activate 2lm
conda config --env --set subdir osx-arm64
pip install -r requirements.txt

python src/chat_cli.py --ckpt checkpoints/gal     # CLI
python server.py --ckpt checkpoints/gal --open    # Chrome でGUIが開く

素の状態(checkpoints/final)と比べると違いが分かります。 同じ入力・同じ乱数種(--seed 7)での実測です。

$ python src/generate.py --ckpt checkpoints/final --prompt "今日バイト行きたくない" --seed 7
残念ながら、私はインターネットにアクセスする能力がないので、この質問に対する回答ができない。

$ python src/generate.py --ckpt checkpoints/gal --prompt "今日バイト行きたくない" --seed 7
うー、今は食事時間にいくよ〜。

素の状態は、聞かれていない「インターネットにアクセスできない」を返しています。 機械翻訳された英語のアシスタント応答を学習しているからです。

何を聞いても腹が減っている理由

まず正直に書いておきます。このモデルは何を聞いても腹を空かせています。

あなた> 今日バイト行きたくない
2LM  > おめでとう!でも今は腹減った〜、ごしらえしない?
あなた> お金貯めたい
2LM  > うん、お金のことならまずはごめっちゃおう!
あなた> AIって将来どうなると思う?
2LM  > AIのことって言ってもんけど、将来的にはまずはお腹ごしらえしないと!

バグではなく、データ側の設計がそのまま出たものです。 返答の「機嫌」を5種類(機嫌がいい / 眠くてだるい / テンションが高い / ちょっと呆れている / 腹をすかせている)用意して均等に振っているので、 空腹と眠気で全体の40%を占めます。13.8M パラメータではそこまで細かく条件を 切り分けられないので、平均的な人格として表に出ます。

データの偏りがモデルの癖になる例として分かりやすいので、そのまま残しました。 減らしたければ data/gal/generate.py の MOODS の比率を変えてください。

データセットを作る

同梱の data/raw/gal_chat.jsonl が生成済みのデータセットです(2,610会話)。 自分で作り直す場合は3段階に分かれています。

python data/gal/generate.py --stage topics                # 話題を列挙させる(数分)
python data/gal/generate.py --stage pairs --target 4000    # 会話を書かせる(80分。止めても再開できる)
python data/gal/generate.py --stage build                 # 検査してまとめる(一瞬)

初回は生成モデル(約17GB)のダウンロードが入ります。 data/gal/runtime.py がGPUメモリの上限を設定し、足りない場合は開始前に止めます。

会話文を人間が書いていない理由

ライセンスの問題があります。 作ったデータセットと、そこから学習した重みを配布するなら、 生成に使ったモデルの利用条件を確認しなければいけません。商用のチャットサービスの規約には、 出力を使ってモデルを学習させることを禁じる条項が置かれているのが普通です。 「小さいモデルだから競合しない」という言い分は、条文に書かれていない限り抗弁になりません。

Apache-2.0 のモデルには出力の利用条件がありません。だからローカルで完結させています。 このプロジェクトが事前学習に使っている llm-jp/magpie-sft-v1.0 も Aratako/Magpie-Tanuki-8B-97k も、Apache-2.0 のモデルが書いた合成データです。 既にある筋を通しているだけです。

再現できるという利点もあります。課金が前提の手順は、読者の半分が実行できません。

生成と検査の設計

詳細は data/gal/README.md にあります。要点だけ:

  • 話題出しを分ける — 話題リストを人間が書くと、そこが多様性の上限になる
  • 文体を例文で教えない — 手本を渡すとモデルは言い換えるだけになる
  • 形式は守らせず、剥がす — 番号・話者名・かぎかっこを機械的に落とす
  • 棄却率を数える — 数えたら最多の棄却理由が …(三点リーダ)の許可漏れだった

追加学習で口調を移植する

ゼロから学習し直すより、事前学習済みの重みに追加学習するほうが速くて確実です。

python data/prepare.py --no-hf --out data/corpus_gal.txt --min-char-freq 1

python src/train.py --init-from checkpoints/final --corpus data/corpus_gal.txt \
    --cache-dir data/cache_gal --out checkpoints/gal \
    --lr 1e-4 --warmup 12 --steps 90

40秒で終わります。 90ステップは 2,610会話(19万トークン)を約8周する量です。 学習率は事前学習の 3e-4 から 1e-4 に落とします。 作った表現を壊さない範囲で、口調だけ動かしたいからです。

気をつけることが2つあります。

語彙を作り直さない。 --init-from を付けるとトークナイザは事前学習したものが そのまま使われます。作り直すとIDの対応が変わり、事前学習した重みが全部無意味になります。

低頻度文字フィルタを切る(--min-char-freq 1)。 事前学習済みのサブワード トークナイザには byte fallback があるので珍しい文字も表現できます。 数千件のコーパスで「出現10回未満を捨てる」をやると、ほとんど残りません。

混ぜるほど口調が戻る

一般には「新しいデータだけで回すと破滅的忘却が起きるので元のデータを混ぜる」と言われます。 実測すると、そう単純ではありませんでした。ギャルの文字数比率を5通り試した結果です。

ギャル比率 結果
50% 口調が元に戻る。「こんにちは、おはよう。今日はどんな気分であなたを助けることができますか?」
58% 同じく戻る。「こんにちは!私はオープンアシスタントです」
85% 口調はギャルになるが、混ぜた公開データが19エポックで丸暗記された
89% 短い返答は自然。ただし公開データに頻出する質問文では元の文体が勝つ
100%(混ぜない) 口調は完全にギャル。採用

過半では足りません。 モデルは応答の文体を「その質問に続いた文章の多数派」から 決めるので、全体比率で勝っていても、公開データに頻出する書き出しでは負けます。

そして会話855件の時点では混ぜないと日本語が崩れたのに、2,610件では崩れませんでした。 破滅的忘却に最初に打つ手は、混ぜることではなくデータを増やすことでした。 混ぜたい場合は tools/mix_corpus.py で比率を指定できます。

python tools/mix_corpus.py --out data/corpus_ft.txt data/corpus_gal.txt:3 data/corpus.txt:0.002

代償は数字に出る

固定検証セット(公開データ由来 249会話)での採点です。

python eval/run.py --ckpt checkpoints/gal --tag gal --compare exp_sp8k

exp_sp8k は追加学習前(checkpoints/final)の採点結果で、runs/ に同梱しています。

指標 追加学習前 ギャル版
bits/char 2.584 3.550(悪化)
主題保持率 0.733 0.267(悪化)
反復率 0.100 0.000
破綻率 0.000 0.000
平均返答長 64.1文字 25.1文字

汎用の応答能力は確実に落ちます。 検証セットは公開データの文体なので、 そこから遠ざかったぶんだけ悪化します。当然の結果です。

大事なのは、この悪化が「失敗」ではなく選択の代償として見えていることです。 指標を用意していなければ「ギャルになった、成功」で終わっていました。 何を捨てて何を得たのかは、測らないと分かりません。

Macを1回落とした話

22B のモデルをバッチ24で走らせたら、macOS がカーネルパニックで落ちました。

panic(cpu 9): IOGPUGroupMemory.cpp:220 Assertion failed: result != kIOReturnSuccess
Panicked task ...: pid 3625: python3.11
      Kernel Extensions in backtrace:
         com.apple.iokit.IOGPUFamily(104.6.2)

Apple Silicon はユニファイドメモリなので、GPU の割り当ても本体RAMから取ります。 MLX は既定でメモリ上限を持たないため、要求が通らなくなると GPU ドライバ側が 確保失敗をアサーションで扱い、Python の例外にならずカーネルごと落ちます。

data/gal/runtime.py に安全装置を3段入れました。

  1. mx.set_memory_limit() で明示的な上限を与える。超えたら Python の例外になる
  2. 開始前にスワップ使用量を見て、余裕がなければ走らせない
  3. バッチごとにピーク使用量を測り、上限に近づいたら止める

3つ目は実際に作動しました。バッチ12で 29.6GB に達したところで停止し、 「バッチを半分にして再実行してください」で終わりました。マシンは落ちていません。

モデル選びはパラメータ数では決まらない

最初は calm3-22b-chat を使いましたが、バッチを4より上げられませんでした。

モデル 層 KVヘッド KVキャッシュ スループット
calm3-22b-chat 48 48 1トークン 1,152KB 8件/分(バッチ4)
Qwen2.5-32B-Instruct 64 8 1トークン 256KB 37件/分(バッチ24)

calm3 は GQA(Grouped Query Attention)を持たない世代で、KVヘッドがアテンションヘッドと 同数あります。パラメータ数が1.5倍多いモデルのほうが 4.6倍速いという結果です。

バッチ生成で先に足りなくなるのは重みではなくKVキャッシュ側なので、 モデルを落とす前に config.json の num_key_value_heads を見てください。

ファイル

ギャル版で足しているのは次のものです。それ以外は 2LM-MLX と同じです。

ファイル 役割
data/gal/generate.py 話題出し・会話生成・検査の3段階。止めても再開できる
data/gal/validate.py 文字種・英単語・敬語・方言・重複でふるいにかける
data/gal/runtime.py GPUメモリの上限設定と安全装置。Macを落とさないための仕掛け
data/raw/gal_chat.jsonl 生成済みのデータセット(2,610会話)
tools/mix_corpus.py コーパスを比率を決めて混ぜる
checkpoints/gal/ 追加学習済みの重み(約53MB)

data/gal/raw.jsonl(検査前の生の出力)は追跡していません。 data/raw/ に置いていないのも意図的で、あそこに置くと data/prepare.py が 未検査のまま拾ってしまいます。

ライセンス / クレジット

コードは MIT License です(LICENSE)。

ギャル会話コーパスは、Qwen2.5-32B-Instruct(Apache-2.0)の出力です。 実在の人物・会話とは無関係な架空データで、特定の人物や集団の描写を意図したものでは ありません。事前学習に使った公開データセットは4件すべて Apache-2.0 で、 ShareAlike(継承)条件を持つものは入れていません。

出典表示は NOTICE にまとめてあります。再配布・商用利用のいずれの場合も これを保持してください。

モデルの出力は、コーパスの統計から次のトークンを予測し続けた結果にすぎません。 事実性は一切保証されず、実在の人物や団体について誤った内容を出力することがあります。 出力を公開の場に掲載する場合は、機械生成物である旨を明記してください。

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages