最近、ChatGPTのような大規模言語モデル(LLM)で盛り上がってますよね。
でも、こういうモデルって普通はクラウドのAPIで使うことが多くて、料金がかかったり、プライバシーが気になりますよね。「自分のパソコンでサクッと動かせたらいいな〜」って思ってる人も多いはず。
今回、自作アプリ開発の途中過程で、MacBookのM3チップで日本語対応のLLMをローカルで動かす挑戦をしてみました!(結果的には断念しました 涙)
この記事では、試行錯誤により学んだ知識をシェアします。例えば、「Macで日本語LLMをローカルで動かすってどうなの?」と疑問を持っているLLM実装に馴染みのない方の参考になれば嬉しいです!
はじめに
皆さんは、冷蔵庫にある食材から「これ作ろう!」ってすぐ思いつきますか?
私はそれが本当に苦手で、冷蔵庫をのぞいても、何を作ればいいのかピンとこないことがよくあります。
結局、クラ◯ルやDEL◯などのレシピサイトで
「にんじん じゃがいも 玉ねぎ レシピ」などと検索して、
何個か候補を見てから決める——
そんな地味に面倒な工程を毎回繰り返していました。
「もっとサクッと料理を決めたい!」
「できれば、材料を入力するだけで候補を出してくれるアプリがほしい!」
そんな思いから、自分のMacBookで手軽に使える『献立アシスタント』アプリを作ろうと考え、その中に既存の日本語のLLM(大規模言語モデル)を使って推論機能を組み込もうと考えました。(今思うと、料理サイトAPIを使うとか別の手段はあったかな。。。)
……結果、まる1日GPTと一緒にがんばったものの時間ぎれで、有用な実装は叶いませんでした(涙)
とはいえ、挑戦を通じてLLMの動作や実装周りについて多くを学んだので、記録としてまとめておこうと思います。
環境
・MacBook Air M3(Apple Silicon、GPU搭載)
・Python 3.8(Anaconda)
・llama.cpp + ggufモデル
・llama-cpp-python
・Transformers(検証時に使用)
根本的な失敗
1.モデルの「タイプ」を理解していなかった
今回、やろうとしていたのは「冷蔵庫の材料を入力 → 作れる料理名の候補を生成する」という生成タスク(推論型)です。
しかし、そもそもLLMにはいくつか種類があり、種類によってタスクの得意分野が違うことを理解しておらず、適当に「日本語対応っぽいモデル」を選んで試していました。
以下は、LLMのざっくり分類表になります。
| モデルタイプ | 主な用途 | 構造 | 代表モデル例 | 入力例 | 備考 |
|---|---|---|---|---|---|
| Decoder-only | テキスト生成 | デコーダのみ | GPT-2, GPT-3, rinna/gpt-neox-3.6b, ELYZA-Llama-2 | にんじん、じゃがいも、玉ねぎで作れる料理を5つ教えてください→ 5つの候補を出す | ユーザーからの指示に忠実に応答する |
| Encoder-only | テキスト分類 | エンコーダのみ | BERT, tohoku-nlp/bert-base-japanese | 料理を考えるのがしんどいです → ポジティブ or ネガティブ | テキスト全体の意味を把握して分類できる |
| Encoder-Decoder | 要約、翻訳 | エンコーダ+デコーダ | T5, BART, line/japanese-bart-base, sonoisa/t5-base-japanese | 要約:にんじん、じゃがいもで料理を作りました。簡単でした。 → 簡単に料理を作った話 | タスク(ex 要約、翻訳)ごとに事前学習されており、タスク実行に忠実 |
私は最初、違いが分からずbert-base-japaneseやline-corporation/japanese-large-lm-1.7bなどを検討・使用しましたが、これらは分類や文理解を得意とするモデルであり、今回のような指示の回答を生成するタスクには不向きでした。
理解が深まる中で、ELYZA-japanese-Llama-2-7b-instruct のようなまさに今回のような生成タスク向けのモデルに辿り着きました。が、ローカル環境でうまく動作させるには量子化の知識、環境依存のチューニングが必要で、挫折しました。
具体的には、pip install llama-cpp-pythonで複数のライブラリの互換性エラーが生じ、その影響でチューニング時にエラーが出ました。新しく仮想環境構築することで解決することができそうでしたが、時間がなかったので断念しました。。。
教訓
- モデルを選ぶときは「何をやりたいか(タスク)」を事前に具体化しておく(一口に自然言語といえど、様々なタスクがあるため)
- 「とりあえず日本語対応モデル」で選んでも、精度や得意分野がまったく異なる
- ローカル実行にこだわるなら、量子化等の軽量化処理の知識も重要
2.モデル初期化の重さを甘く見ていた
さらにもう1つ大きな落とし穴がありました。それは、モデルやトークナイザーの初期化処理が非常に重いということです。
例えば、
from transformers import AutoModelForCausalLM, AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(model_name)
この処理だけで、数GB以上のダウンロードと初期ロードが発生し、M3 Macのようなローカル環境では数十分〜数時間かかることもあります(特に一度目)。
まずダウンロードに関しては、数GB以上なので時間がかかりますが、一度うまくいけば、以下の方法で使い回しができます。今回もここまではうまく実装できました。
tokenizer = AutoTokenizer.from_pretrained(model_name,local_files_only=True)
model = AutoModelForCausalLM.from_pretrained(model_name,local_files_only=True)
次に初期ロードですが、モデルの動作環境が学習時の環境と異なるので、そのチューニングに対応します。色んなファイルを参照した形でモデルの中の大量なパラメータの初期設定するので、処理に負荷がかかることは避けられません。
具体的には、以下のような動作が入ります:
- モデルパラメータ(数GB)をディスクから読み込み
- トークナイザー辞書やエンコーディング処理のロード
- 重いモデルをGPU(MPS)やCPUに展開
- GPU使用可否を自動判定(うまくMPSが使えていないことも…)
これらの処理が重いです。今回M3のGPUを利用し、120分ぐらい待っても終わらなかったので、別の方法を試みました。それが先ほど登場した量子化ですが、今回は活用途中で断念しました。。。
教訓
- モデル初期化(特に初回)は必ず時間がかかる
-
local_files_only=Trueを使っても、初期ロードが重いのは同じ - M1/M2/M3のスペックもきちんと確認すべき
- 軽いテストには量子化済みのモデルや、低レベルAPIを使うのがベター
試した日本語対応モデル
| モデル名 | パラメータ規模 | 特徴 | タスクの精度(主観) | M3対応 | 備考 |
|---|---|---|---|---|---|
rinna/japanese-gpt2-medium |
約1.1億 | 非常に軽量な日本語モデル | 🔺(推論精度が足りない) | ⭕ | 推論スピードは速いが、精度が実用レベルに達しない |
rinna/japanese-gpt-neox-3.6b |
約36億 | 日本語特化・自然な生成 | 🔺(初期化・推論に非常に時間がかかる) | ❌ | M3でも推論に2時間以上。現実的には厳しい |
line-corporation/japanese-large-lm-1.7b |
約17億 | 日本語対応・軽量 | ❌(精度低め) | ⭕ | ローカルで動作確認済み。 |
ELYZA-japanese-Llama-2-7b-instruct(gguf版) |
約70億(量子化) | 日本語指示文対応 | ❌(Metal環境でクラッシュ) | ❌(Metal backend初期化失敗) | llama.cpp経由でMetal対応試みるも失敗 |
※ 精度・M3対応の評価は個人環境での検証に基づいており、実行環境やプロンプトによって異なる場合があります。
実装例
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch
model_name = "rinna/japanese-gpt2-medium"
tokenizer = AutoTokenizer.from_pretrained(model_name, use_fast=False, local_files_only=True)
model = AutoModelForCausalLM.from_pretrained(model_name, local_files_only=True)
device = "mps" if torch.backends.mps.is_available() else "cpu"
model = model.to(device)
prompt = (
"以下の条件に絶対に従ってください。\n"
"・日付は絶対に入れないこと\n"
"・にんじん、じゃがいも、玉ねぎだけを使った料理名を5つ\n"
"・必ず番号付きリストで回答すること\n"
"・料理名以外の文章や説明は一切書かないこと\n"
"・ニュースや他の文章は絶対に混ぜないこと\n"
)
inputs = tokenizer(prompt, return_tensors="pt").to(device)
# ランダム性と再現性の両立
torch.manual_seed(42)
# 推論中に勾配計算を行わないことで、メモリと計算を節約(推論時には通常使用)
with torch.no_grad():
outputs = model.generate(
**inputs, # トークナイズされた入力データを展開して渡す
max_new_tokens=80, # 生成する最大トークン数(出力の長さ制限)
do_sample=True, # サンプリングを有効に(Falseにするとビームサーチなどの決定的生成)
top_k=40, # 上位K個のトークンからサンプリング(多様性を持たせる)
top_p=0.95, # 累積確率が95%に達するまでのトークンからサンプリング(nucleus sampling)
temperature=0.9, # 出力のランダム性を制御(小さいほど確定的、大きいほどランダム性が増す)
pad_token_id=tokenizer.eos_token_id # パディングトークンのID(必要な場合のために明示的に指定)
)
result = tokenizer.decode(outputs[0], skip_special_tokens=True)
print(result)
おわりに
最後まで読んでいただき、ありがとうございました!
Transformerの仕組みやLLMの理論については一通り学んだつもりでしたが、実際に日本語モデルを動かそうとすると、「環境構築が難しい」「推論に時間がかかる」「モデルの用途(分類/要約/生成)が違う」など、想像以上にハマりどころが多くありました。
特に、モデルの設計思想や学習タスクを理解せずにプロンプトだけ工夫しても、思ったような出力が得られないという点は、大きな学びでした。
今回は失敗に終わってしまいましたが、ローカルLLM活用の一歩としては良い経験になりました。
この記事が、同じように「日本語LLMをローカルで動かしてみたい!」と考えている方の参考になれば幸いです。