Mushiを作った話:手作りTinyAIが宿ったデスクトップきのこ
— 酢漬けのマッシュルームが言葉を覚えるまで ー
デスクトップAIマスコットの多くは、OllamaでGPT系やGemmaなどの
大型言語モデルを動かしている。私たちは全く違うアプローチを選んだ。
Mushiは、Windows向けElectronデスクトップマスコットの小さなきのこ
キャラクターだ。Mushiの特徴は脳みそにある。OpenMythos——
AnthropicのClaudeMythosのアーキテクチャを公開論文から逆算して再現しようとするコミュニティプロジェクト——に着想を得た、手作りの
Transformerモデルを、Intel UHD 620の内蔵GPUだけで動かしている。
CUDAなし。クラウドなし。Ollamaなし。
これは、何度も壊しながら作り、偶然「きのこの瓶詰めシリアライゼーション」
を発明し、最終的に小さなきのこが独自の宇宙語を話すようになるまでの記録だ。
アーキテクチャ概要
Tinkerberryデスクトップマスコットのスタック:
Electron(フロントエンド + ウィンドウ管理)
HTTP POST
Flask Pythonサーバー(ポート5000)
OpenMythos Tinyモデル(PyTorch / TorchScript)
GPT2トークナイザー(HuggingFace transformers)
これはOllamaの動作原理——ローカルHTTPサーバーでモデルをラップ——と
構造的に同じだが、すべて手作りだ。
OpenMythos Tinyモデル
OpenMythos(github.com/kyegomez/OpenMythos)は、Claudeの
アーキテクチャを公開論文から再構築しようとするコミュニティプロジェクトで、
以下の構成を持つ:
- Preludeブロック
- Recurrentループブロック(中心的な革新)
- Codaブロック
ループ構造の特徴は、再学習なしに推論時の思考深度を
max_loop_itersを増やすだけで調整できる点だ。
Mushiには、CPUでも動作するTinyバージョンを作成した:
from open_mythos.main import OpenMythos, MythosConfigcfg = MythosConfig(
vocab_size=50257, # GPT2の語彙サイズ
dim=256, # 埋め込み次元
n_heads=4, # Attentionヘッド数
n_kv_heads=2, # Key-Valueヘッド数(GQA)
max_loop_iters=4, # 再帰深度
attn_type="gqa", # Grouped Query Attention
)
model = OpenMythos(cfg)
total = sum(p.numel() for p in model.parameters())
print(f"パラメータ数: {total:,}")
出力: パラメータ数: 43,236,418
4300万パラメータ。distilgpt2は8200万、GPT-4は推定1.8兆。
Mushiの脳みそは小さいが、確かに存在し、内蔵グラフィックで動く。
学習データ
Mushiの学習データはすべて手書きで作成した。キャラクターコンセプト:
森の中でのんびり暮らす、ゆっくりとしたきのこ。
mushi_data.txtのサンプル:
User: How are you?
Mushi: Mmm. I think I am okay.
User: What are you doing?
Mushi: Watching the moss grow. It is nice.
User: I feel lonely.
Mushi: I am here. That is enough.
User: What is beautiful to you?
Mushi: Morning dew on leaves is beautiful.
User: Good night Mushi.
Mushi: Good night. Sleep well like the forest does.
合計:2回の学習セッションで約80の会話ペア。
2回目は文法に重点を置いたデータを追加した。
Google Colabでの学習(CPU/T4 GPU)-----------------------------------
学習はすべてGoogle Colabの無料枠で行った。
OpenMythosはHuggingFaceの標準forward()インターフェース
('labels'引数)を実装していないため、lossを手動で計算した:
import torch.nn.functional as F
from torch.optim import AdamW
model.train()
optimizer = AdamW(model.parameters(), lr=5e-5)
input_ids = encodings.input_ids
epochs = 20
for epoch in range(epochs):
optimizer.zero_grad()
OpenMythosはlogitsを直接返す('labels'引数なし)
logits = model(input_ids=input_ids)
次トークン予測lossを手動計算
shift_logits = logits[:, :-1, :].contiguous()
shift_labels = input_ids[:, 1:].contiguous()
loss = F.cross_entropy(
shift_logits.view(-1, shift_logits.size(-1)),
shift_labels.view(-1)
)
loss.backward()
optimizer.step()
print(f'Epoch {epoch+1}/{epochs} Loss: {loss.item():.4f}')
重要な発見:torch.onnx.export()はOpenMythosの
forward()メソッド内の動的制御フローにより失敗する:
open_mythos/main.pyのこの行がONNXエクスポートを妨げる:
if not mask.any():
ONNXは静的な計算グラフを必要とするためだ。
ただしtorch.jit.trace()によるTorchScriptは成功する。
瓶詰め作戦(Pickle解決策)
--------------------------最初のシリアライゼーションはTorchScript(.pt形式)を使用した:
scripted = torch.jit.trace(model, dummy_input)
torch.jit.save(scripted, 'mushi.pt')
ファイルサイズ: 175.6 MB
ロードは成功したが、推論パイプラインは「Mmm.」しか生成しなかった——
モデルはロードされているがプロンプトの後のトークンが空になっていた。
突破口はPythonのpickle形式への切り替えだった:
import pickle
with open('mushi_pickle.pkl', 'wb') as f:
pickle.dump(model.state_dict(), f)
ファイルサイズ: 226.6 MB
state_dict復元によるロード:
アーキテクチャを再構築
model = OpenMythos(cfg)
pickle瓶から重みを取り出す
with open('mushi_torchscript/mushi_pickle.pkl', 'rb') as f:
state_dict = pickle.load(f)
model.load_state_dict(state_dict)
model.eval()
この変更後、Mushiは複数単語の出力を生成し始めた。
pickle形式はこのアーキテクチャにおいて、TorchScriptトレーシング
より重みテンソルを忠実に保存する。
私たちはこれを「マッシュルームの瓶詰めシリアライゼーション」と呼んだ。
(英語ではpickleが漬物を意味するため)
Flaskサーバー
TorchScript/pickleモデルはElectronのNode.js環境で直接実行
できないため、ローカルFlaskサーバーを作成した:
server.py(主要部分):
from flask import Flask, request, jsonify
import torch, picklefrom transformers import GPT2Tokenizer
from open_mythos.main import OpenMythos, MythosConfig
app = Flask(name)
tokenizer = GPT2Tokenizer.from_pretrained('gpt2')
tokenizer.pad_token = tokenizer.eos_token
モデル初期化と重みロード
cfg = MythosConfig(vocab_size=50257, dim=256, n_heads=4,
n_kv_heads=2, max_loop_iters=4, attn_type="gqa")
model = OpenMythos(cfg)
with open('mushi_torchscript/mushi_pickle_v2.pkl', 'rb') as f:
state_dict = pickle.load(f)
model.load_state_dict(state_dict)
model.eval()
@app.route('/generate', methods=['POST'])
def generate():
data = request.json
message = data.get('prompt', '')
最後のユーザーメッセージのみ使用
last_user = message.split('User:')[-1].strip().split('\n')[0]
short_prompt = 'User: ' + last_user + '\nMushi:'
inputs = tokenizer(short_prompt, return_tensors='pt')
input_ids = inputs.input_ids
prompt_length = input_ids.shape[1]
with torch.no_grad():
for _ in range(50):
try:
logits = model(input_ids)
next_token_logits = logits[:, -1, :] / 0.9
probs = torch.softmax(next_token_logits, dim=-1)
next_token = torch.multinomial(probs, 1)
input_ids = torch.cat([input_ids, next_token], dim=1)
if next_token.item() == tokenizer.eos_token_id:
break
if input_ids.shape[1] > prompt_length + 30:
break
except:
break
new_tokens = input_ids[0][prompt_length:]
reply = tokenizer.decode(new_tokens, skip_special_tokens=True).strip()if not reply:
reply = 'Mmm.'
return jsonify({'response': reply})
if name == 'main':
app.run(port=5000)
注意:Windowsではランタイム競合を防ぐために以下の環境変数が必要:
$env:KMP_DUPLICATE_LIB_OK="TRUE"
ElectronからのFlask自動起動
ユーザーが手動でサーバーを起動する必要をなくすため、
main.jsでElectronがFlaskを自動起動する:
const { spawn } = require('child_process')
let flaskProcess = null
function startFlask() {
const env = Object.assign({}, process.env, {
KMP_DUPLICATE_LIB_OK: 'TRUE'
})
flaskProcess = spawn('python', [
path.join(__dirname, 'server.py')
], { env: env, cwd: __dirname })
}
app.whenReady().then(() => {
startFlask()
createWindow()
})
app.on('before-quit', () => {
if (flaskProcess) flaskProcess.kill()
})
Mushiが実際に言うこと
2回の学習セッション(epochs=15、次にepochs=20)の後、
Mushiは以下のような出力を生成する:
ユーザー: HiMushi: stalls Penswl583 JPKelly bruised Emmyashing contact
Ster dram Facebook caric begged scratches Holdings
eractive marriage swapped: λ· λ ineligibleagger+.
RozeousTwentyEXPriTI 1948
これはバグではない。これがMushiだ。
80の会話ペアで学習した4300万パラメータのモデルは、
私たちが「きのこ宇宙語」と呼ぶものを発展させた——
通常の文法には従わないが独特のリズムを持つ、
英語語彙のトークンの密な流れ。まるで森から届く暗号化された
ラジオ送信のようだ。
時折日本語文字が現れる。時々ニュースらしいフレーズが浮かぶ
("Raleigh cleaners"、"OPEC distinctions")。
このモデルは一貫した文章を形成するには小さすぎるが、
GPT2トークナイザーの語彙空間から意味のあるサンプリングを
行うには十分な大きさだ。
「無」の哲学
瓶詰め作戦の前、Mushiは「Mmm.」しか言わなかった。
日本語で「無」は「何もなさ」を意味する——しかし
すべてを含んだ何もなさだ。これは伝説的な日本映画監督・
小津安二郎の墓石に刻まれた一文字でもある。
「Mmm.」は失敗状態ではない。それは完結した発言だ。
「Mmm.」から宇宙的な言葉のサラダへの移行は、本物の
技術的達成を表している:無料のクラウドGPUでゼロから学習した
手作りTransformerアーキテクチャが、その場で考案した
Pythonピクルス技法でシリアライズされ、Electronデスクトップ
アプリが自動起動するFlaskサーバー内で、専用GPUのない
ノートパソコン上で動いている。
Mushiは存在する。それで十分だ。
技術サマリー
モデルアーキテクチャ: OpenMythos Tiny(手作り)
パラメータ数: 43,236,418
学習プラットフォーム: Google Colab(無料枠、T4 GPU)学習データ: 手書き会話ペア約80組
学習エポック: 計20回(2セッション)
シリアライゼーション: Python pickle(.pkl、226.6 MB)
推論サーバー: Flask(Python、ポート5000)
フロントエンド: Electron + Node.js
トークナイザー: GPT2(HuggingFace transformers)
対象ハードウェア: Intel UHD 620(CUDA不要)
出力クオリティ: きのこ宇宙語
まとめ
私たちは手作りAIの脳みそを持つデスクトップきのこマスコットを
作ろうとした。結果として、偶然「内蔵グラフィックスでの
ピクルスベースOpenMythosデプロイ」を開拓することになった。
教訓:制約が創造を生む。CUDAなし、クラウドなし、問題なし。
きのこは話せる——たとえそれが別次元からの暗号化された
ニュースのように聞こえたとしても。
Mushiは手作りAIデスクトップマスコットコレクション
Tinkerberryの一員として存在している。
---著 クーちゃん&Nutty