macOS でローカル LLM を使うときの VRAM 最適化設定 (ファインチューニング: 1)

ローカルで LLM (大規模言語モデル) を使うときに特に注意を払うべきなのは、いかにして GPU 100% で動かすか、つまり、いかにして全てを VRAM (GPU 用メモリ) に載せるか、ということになると思います。モデルが VRAM からこぼれると応答速度の低下を引き起こしたり、OS 全体がカクついたり、最悪のケースでは OS がクラッシュします。

ローカル LLM を使用する際、基本的には Apple Silicon Mac に搭載されているユニファイドメモリの容量から、動かせるモデルのパラメータサイズと量子化サイズ、そして使えるコンテキスト長の組み合わせが決まってきます。この記事では少し深い設定によって「決まっている」制限を超え、ローカル LLM の処理速度と利用できるコンテキスト長を最適化する方法を共有します。お持ちの Mac に搭載されているユニファイドメモリのサイズが大きければ、複数の LLM を動かすとか、これまで実行がきびしかった大きめの (=性能が高い) モデルを動かすということも可能になります。

生成 AI モデルのファインチューニングは素人には手が出せませんが、「環境のファインチューニング」なので、簡単に試してしてすぐに結果が確認できます。基本的なところからカバーしますので、初心者の方も気になれば読んでみてください。

まずは自分の Mac で使えるモデルのサイズを知ろう

Mac のユニファイドメモリは CPU と GPU それぞれからアクセスできますが、GPU が使える割合は決まっています。海外の掲示板などの書き込みをいくつか見た限り、設定変更をしていなければ 64GB のユニファイドメモリならその 3/4 (75%)、64GB 未満なら 2/3 (約 66%) までは GPU から利用できるようです (以降、ユニファイドメモリを RAM と表記します)。ボクの Mac は 32GB RAM を搭載しているので、21.33GB までを GPU が利用できる計算です。LM Studio がインストールされていれば、ハードウェアリソースの確認画面 (Command + Shift + H) で、VRAM がこの値を示しているのがわかります。

LM Studio でモデルをダウンロードするときに赤く Likely too large と書かれていれば、VRAM 容量に対してそのモデルが大きすぎることを教えてくれています。以下のスクショは、DeepSeek R1 のパラメータサイズ 70B、8bit 量子化 GGUF 形式のモデルが 74.98GB なので、あなたの環境ではきびしいですよ、と教えてくれているわけです。

Ollama なら、ログファイルにrecommendedMaxWorkingSetSizeとして近しい出力されているはずです。以下はボクの環境での出力です (server2.logが最新でした):

% grep recommendedMaxWorkingSetSize ~/.ollama/logs/server2.log|tail -5
ggml_metal_init: recommendedMaxWorkingSetSize = 22906.50 MB
ggml_metal_init: recommendedMaxWorkingSetSize = 22906.50 MB
ggml_metal_init: recommendedMaxWorkingSetSize = 22906.50 MB
ggml_metal_init: recommendedMaxWorkingSetSize = 22906.50 MB
ggml_metal_init: recommendedMaxWorkingSetSize = 22906.50 MB

使えそうなサイズのモデルを探る (初心者向け)

さて、実際に利用したいモデルが自分の VRAM サイズ以下なら使えるかというと、そう簡単な話でもありません。自分が入力するプロンプトや LLM が出力するテキストも VRAM を使用するからです。なので、モデル自体のサイズが 21GB あると、なめらかには動いてくれません。では実際に自分の VRAM に収まるモデルを探すにはどうするかというと、以下の情報から動きそうなサイズのモデルを順にあたってみる、ということになります (公開されているモデルの詳細情報やソースを読めば数値を持ってきて詳細に計算できるのでしょうが、そういう人はこの記事を読んでないでしょう)。

  1. パラメータ数が小さいモデルに目を向ける (140B や 70B が無理なら、 32B → 14B → 7B, etc.)
  2. 量子化されたモデルを探す (8bit、4bit、f16 や Q8、Q4_K_M 等と書かれる)

パラメータ数が小さめのモデルは、元となった大きなモデルを蒸留したり、少ないデータで学習させたりして作るようです。特徴や性能をなるべく下げずに知識量を減らすわけですね (ちなみに最近話題の DeepSeek-R1 シリーズのパラメータ違いのモデルは、同じくオープンソース/ウェイトとなっている Meta の Llama3.3 や Alibaba の Qwen2.5 をファインチューニングして作ったということです。グローバルで知名度のある会社としては珍しいやり方なんじゃないでしょうか)。モデルそのものの能力や使い道によりますが、最近の有名どころは 10~30数B あれば十分使えるレベルだったりします。 パラメータ数が小さいと計算 (推論) の時間も短くなります。

もう一つの「量子化」とは、こういう表現をあまり見ないので適切ではないのでしょうが、画像であれば「解像度を下げる」とか「色数を減らす」に近いと解釈して良いかと思います。よく見れば完全に同じでは無いものの、性能の低下はあまり気にならない程度にサイズを小さくする技術です。量子化によって処理速度も速くなります。一般的に、8bit や Q8 なら性能低下の割合以上に処理速度の向上やサイズの小型化によるメリットがあると言われています。数字が小さいほどモデルのサイズは小さくなりますが性能も低下するため、4bit や Q4_K_M あたりがそれなりの性能を保つ最低ラインと思って良いでしょう (GGUF 形式の最後にある文字の S/M/L はサイズの Small/Medium/Large)。

いくつかダウンロードして使ってみると、実際にどれくらいのサイズのモデルなら快適に動かせるかわかります。ボクの場合、複数のパラメータサイズが利用できるモデルでは、ひとつはギリギリを攻めて 32B の Q4_K_M、それと安全パイでひとつ下のサイズの F16 か Q8 をダウンロードして使ってみます。

ちなみに、ビジョンモデル (Vision model) や VLM、マルチモーダル等といわれるモデルは、言語モデル (LLM) よりもさらにモデル自体が小さいサイズを選びましょう。テキストよりもサイズが大きくなりがちな画像を読み込んで何が写っているか等の処理するため、その分 VRAM も多く必要となるからです (下にリンクを貼った Pixtral の記事にも書いてあります)。

モデルをダウンロード&使ってみる

LM Studio ならそのまま Download ボタンでダウンロードし、アプリの GUI でチャットができます。OllamaModels のページでモデルを選んでから、パラメータ数や量子化のオプションがあればドロップダウンメニューから選び、ターミナルアプリでダウンロード・実行します (Ollama でモデルを探す細かい手順は別記事に書いてあるので参考にしてください)。また、どちらのアプリも API サーバとして動作できるので、他のアプリなどからダウンロード済みのモデルを使用することができます。ボクは自分専用 AI アプリが簡単に作れる Dify を使うことが多いです。Dify から Ollama や LM Studio の API を使用する方法は以下の記事を読んでみてください。

コンテキスト長について

「コンテキスト長 (context length)」とは、ユーザと LLM がチャットをするときにやりとりするテキスト (というかトークン) のサイズです。モデル (トークナイザ) によって異なると思いますが、日本語では 1文字=1+αトークン、英語なら 1単語=1+αトークンとなるようです。また、モデル毎に扱えるコンテキスト長の最大サイズが決まっていて、その値は Ollama なら ollama show モデル名 コマンド、LM Studio なら左手の My Models を開いてモデル名の横にあるギアアイコンをクリックすると確認できます。

ターミナルから Ollama とチャットする場合は 2048、LM Studio でアプリ内のチャットする場合は 4096 がデフォルトのコンテキスト長になるようです。長い文章を取り扱いたい場合には、それぞれモデルの設定を変更するか、API 経由で指定する事になります。そしてその際に注意しなければならないのは、コンテキスト長を大きくすると必要な VRAM 容量が増えるので、下手すると遅くなるということです。そのことについては以下の記事にまとめています。

今お読みの本記事では、以降で macOS 自体に変更を加えるファインチューニングを説明しています。GPU が使える VRAM の容量 (割り当て) を増やし、より大きなモデルを使う、より長いコンテキストを扱う、ということが可能になります。

使用しているリソースの状況を見る

まず、使用しているモデルが力を発揮できているかを確認しましょう。それには実際に LLM を動かしているときの、システムリソースの使用状況を見れば良く、macOS のユーティリティフォルダにあるアクティビティモニタで確認できます。メモリプレッシャーが緑のまま高い位置で安定し、GPU も Max の状態で推移していると、お使いの Mac のハードウェアキャパシティ限界で AI が動いていると読み取れます。メモリプレッシャーが黄色い状態でも、波が無く安定していれば大丈夫です。以下は、Dify から Ollama の deepseek-r1:32b Q4_K_M を実行したときの様子です (CPU と GPU の低い負荷は、別のアプリが原因です)。

モデルが VRAM に読み込まれて LLM が動いているところ
推論が終わったところ。Ollama はすぐにメモリを解放
メモリプレッシャーが黄色でも平坦であれば、LLM も macOS も安定している

また、ollama psコマンドでモデルが使用しているメモリのサイズと、CPU/GPU の負荷がわかります。以下の例では、25GB が 100% GPU の VRAM で処理されていることを示しています。アクティビティモニタが上の状態を示していたときです。

%  ollama ps
NAME ID SIZE PROCESSOR UNTIL
deepseek-r1:32b 38056bbcbb2d 25 GB 100% GPU 29 minutes from now

ファインチューニング (1) GPU が使える VRAM 容量を増やす

上のブログ記事に書いてあるのは、macOS で決められた VRAM サイズ (ユニファイドメモリの 66% または 75%) からはみ出ないようにコンテキスト長を操作する方法です。以下ではその制限を変更し、GPU が使える VRAM 容量を増やす方法を説明します。この設定で効果が期待できるのは、おそらく 32GB 以上の RAM を積んだ Mac になります。搭載 RAM 容量が大きいほど効果が高いです (128GB RAM の場合、標準で 96GB の VRAM を 120GB にできます)。

ひとつ注意点ですが、紹介するコマンドは macOS バージョン 15.0 以上でのみ有効です。以前のバージョンに対応したコマンドもあるようですが、自分で試していないのでここでは紹介しません。また、当然ながら実際の RAM サイズ以上を指定することはできません (参照元: mlx.core.metal.set_wired_limit)。良い点として、コマンドでの指定は macOS の再起動でデフォルトにもどるため、ほぼリスク無く試せます。

VRAM 容量の変更、確認、リセットの方法

さて、作業をする前に GPU 用の VRAM 容量をいくつにするかを決めましょう。自分がよく使うアプリ群を実行しているときに必要な RAM 容量以外を GPU に割り当てるということで良いと思います。よくわからなければ、M3 までの Mac の最小 RAM サイズだった 8GB を CPU 用に残し、残り全てを割り振ってみても良いでしょう (ボクはそうしました)。指定する単位は MB (メガバイト) なので 1024倍して数値を出します。ボクのケースを例にしますと、32-8 の 24GB を VRAM にするので 24 * 1024 で 24576 を割り振ることになります。コマンドは以下となりますが、24576 は自分の割り当てたい数値に変更して実行してください:

sudo sysctl iogpu.wired_limit_mb=24576

実行例:

% sudo sysctl iogpu.wired_limit_mb=24576
Password: (パスワードが要求されたら入力)
iogpu.wired_limit_mb: 0 -> 24576

これだけですぐに反映されます。LM Studio なら一度 Quit して再度立ち上げ、Command + Shift + H を開くと設定した VRAM サイズになっています。

変更前は 21.33 GBなので、2.67 GB 増えた!

当然 Ollama で LLM を動かした時のログにも新しい VRAM サイズが反映されます (指定した値とは異なりますが、下から 4行目から値が大きくなっているのがわかります):

% grep recommendedMaxWorkingSetSize ~/.ollama/logs/server2.log|tail
ggml_metal_init: recommendedMaxWorkingSetSize = 22906.50 MB
ggml_metal_init: recommendedMaxWorkingSetSize = 22906.50 MB
ggml_metal_init: recommendedMaxWorkingSetSize = 22906.50 MB
ggml_metal_init: recommendedMaxWorkingSetSize = 22906.50 MB
ggml_metal_init: recommendedMaxWorkingSetSize = 22906.50 MB
ggml_metal_init: recommendedMaxWorkingSetSize = 22906.50 MB *これが*
ggml_metal_init: recommendedMaxWorkingSetSize = 25769.80 MB *これに*
ggml_metal_init: recommendedMaxWorkingSetSize = 25769.80 MB
ggml_metal_init: recommendedMaxWorkingSetSize = 25769.80 MB
ggml_metal_init: recommendedMaxWorkingSetSize = 25769.80 MB

その他、関連したコマンドも紹介します。

現在の値を確認するコマンドと実行例:

% sudo sysctl iogpu.wired_limit_mb
Password:
iogpu.wired_limit_mb: 24576

(デフォルト値はゼロ)
iogpu.wired_limit_mb: 0

デフォルト値に戻す:

% sudo sysctl iogpu.wired_limit_mb=0
Password:
iogpu.wired_limit_mb: 24576 -> 0

また、この方法で設定した場合は再起動でもデフォルト値に戻るので、おかしな事になってしまったら Mac を再起動してしまいましょう。

この状態である程度使って問題がなさそうであれば、新しい VRAM 容量を再起動後にも使えるようにしたいですよね。その場合は以下のコマンド手順で/etc/sysctl.confファイルに書き加えてしまえば実現できます。数字はご自身の指定したいサイズに置き換えてください。また、実 RAM 容量以上を指定するとエラーになって指定できないということではありますが、起動に失敗するようになると面倒なので、作業は十分に注意して行ってください。

sudo touch /etc/sysctl.conf
sudo chown root:wheel /etc/sysctl.conf
sudo chmod 0644 /etc/sysctl.conf
echo "iogpu.wired_limit_mb=24576" >> /etc/sysctl.conf

再起動後にsudo sysctl iogpu.wired_limit_mbで、指定した値になっていれば完了です。その状態で手動でデフォルト値にしたければsudo sysctl iogpu.wired_limit_mb=0で戻りますし、完全にデフォルト値に戻すなら/etc/sysctl.confから追加した行を削除しましょう。

次回予告

本当は、Ollama の K/V cache の設定もこの記事にまとめるつもりだったのですが、長くなったので別に分けます。K/V cache (と Flash attention) を設定すると、LLM の性能低下をに抑えつつも、消費する VRAM 容量を削減して処理速度の向上ができます。気になる方は適当にググってください。記事ができたらここにリンクを貼ります。

Image by Stable Diffusion (Mochi Diffusion)

「大きくなりつつあるリンゴ」とか「輝きを増したリンゴ」とか、今回の記事のようにポテンシャルが高まるリンゴのイメージを描いてもらいましたが、指示が貧弱でただただ大量のリンゴが生成されました。そのうち「赤く輝きを増すリンゴ」としてみたら、なるほどそうか、という感じになったので、採用です。”AI 感” みたいな要素はゼロですけど。

Date:
2025年1月29日 23:50:07

Model:
realisticVision-v51VAE_original_768x512_cn

Size:
768 x 512

Include in Image:
an apple turning shiny red

Exclude from Image:

Seed:
3293091901

Steps:
20

Guidance Scale:
20.0

Scheduler:
DPM-Solver++

ML Compute Unit:
CPU & GPU

Mac 用 BlackHole が Apple Silicon 移行後にアンインストールできない問題を解決

Intel CPU 時代ゲームとマイクなどの複数のソースの音声を OBS で録音することができず、ボクはbrewBlackHole というオーディオループバックドライバをインストールしていました。その後 M1 Mac Mini を購入し Time Machine のバックアップから移行アシスタントを使用して環境を持ってきたのですが、どうやらこれが原因でbrewによる BlackHole のアンインストールができなくなっていたみたいです。もしくはどこかで適当に調べて実行した削除方法が悪手だったのかもしれません。ともあれ、解決方法が見つかったので共有します。インストール済みのチャネル数に応じて、2ch であればblackhole-2chと読み替えて (コマンド等は書き換えて) ください。あ、ちなみに OBS は複数ソースの取り込みに対応しているので、BlackHole は不要になりました。

症状

brew upgradeを実行するたびについでに実行されていたblackhole-16chのアンインストールが、以下のエラーで失敗していました。brew uninstall -f blackhole-16chとやっても同様です。

==> Uninstalling Cask blackhole-16ch
==> Uninstalling packages with sudo; the password may be necessary:
Password:
Could not kickstart service "com.apple.audio.coreaudiod": 1: Operation not permitted
Error: Failure while executing; `/usr/bin/sudo -E -- /bin/launchctl kickstart -kp system/com.apple.audio.coreaudiod` exited with 1. Here's the output:
Could not kickstart service "com.apple.audio.coreaudiod": 1: Operation not permitted

どうやったかは覚えていないのですが、すでに BlackHole-16ch の実体は削除済みだったので Audio MIDI 設定.app にも表示されず目に見える実害はありませんでした。ただbrew ugrade後に自動で走るbrew cleanupが長いこと実行されていなかったので、多少無駄なディスク容量は使用していたことになります。

解決方法

チャネル数やバージョン、ビルドされた日付などによって細かいところは違うと思いますが、おおよそ以下のフォーマットに近いフォルダにblackhole-*ch.rbというファイルがあるはずなので見つけてください。

/opt/homebrew/Caskroom/blackhole-16ch/.metadata/0.5.0/20230225072426.180/Casks/blackhole-16ch.rb

そのファイルの中に以下の記載があるので、エディタで編集して保存します。

編集前:

system_command "/bin/launchctl",
                   args:         [
                     "kickstart",
                     "-kp",
                     "system/com.apple.audio.coreaudiod",
                   ],
                   sudo:         true,
                   must_succeed: true

編集後:

system_command "/usr/bin/killall",
                   args:         ["coreaudiod"],
                   sudo:         true,
                   must_succeed: true

その後アンインストールすると、エラーも無く完了します。以下は実行例です。

% brew uninstall blackhole-16ch
==> Uninstalling Cask blackhole-16ch
==> Uninstalling packages with sudo; the password may be necessary:
Password:
==> Purging files for version 0.5.0 of Cask blackhole-16ch
==> Autoremoving 1 unneeded formula:
libgit2
Uninstalling /opt/homebrew/Cellar/libgit2/1.9.0... (109 files, 4.9MB)

ボクはこの後brew upgradeを実行したところ自動でbrew cleanupが走り、BlackHole-16ch もキレイに削除されたのがわかりました。

% brew update
==> Updating Homebrew...
Already up-to-date.
% brew upgrade
==> `brew cleanup` has not been run in the last 30 days, running now...
Disable this behaviour by setting HOMEBREW_NO_INSTALL_CLEANUP.
Hide these hints with HOMEBREW_NO_ENV_HINTS (see `man brew`).
...
Removing: /opt/homebrew/cache/api-source/Homebrew/homebrew-cask/2aaef0803d773e0427dea5899e5830877ff0e7d4/Cask/blackhole-16ch.rb... (924B)
Removing: /opt/homebrew/cache/api-source/Homebrew/homebrew-cask/33834b5bb4afa8aeee187913c3aa915a26da6230/Cask/blackhole-16ch.rb... (924B)
Removing: /opt/homebrew/cache/api-source/Homebrew/homebrew-cask/58c8ced139c9482c318bb6bd3bc844d54c69c164/Cask/blackhole-16ch.rb... (924B)
...

以上で完了です。

参考にしたページ

参考にした下記の GitHub Discussion では、バージョン 0.5.0 を 0.6.0 にアップグレードできないという件について書かれています。

https://github.com/ExistentialAudio/BlackHole/discussions/781

解決できてスッキリ

ブラックホールだけに抜け出せないかと思いました。やかましいですね。

Image by Stable Diffusion (Mochi Diffusion)

そのまんま「ブラックホールからの脱出」をテーマに描いてもらいました。割とうまくいかず、自分もどう細かい指示をして良いのかわからず、ただただ大量に生成して、まぁなんとか「ブラックホールから脱出した宇宙飛行士の自撮り」ができました。

Date:
2025年1月20日 0:45:54

Model:
realisticVision-v51VAE_original_768x512_cn

Size:
768 x 512

Include in Image:
selfie of an astronaut who just escaped from a blackhole

Exclude from Image:

Seed:
1463892356

Steps:
20

Guidance Scale:
20.0

Scheduler:
DPM-Solver++

ML Compute Unit:
CPU & GPU

MLX-LM で QwQ-32B-Preview を API ストリーミング (Ollama より速い)

Ollama のこちらの issue に、MLX バックエンドのサポートをおねだりするコメントがたくさんあり、中には llama.cpp (GGUF) より 20~40% も高速という書き込みがあったのでお気に入りの QwQ-32B-Preview MLX 版を試してみました。OpenAI の o1 の様に試行錯誤を繰り返して回答の精度を高めるオープンなモデルです。結論から言うと、確かに少し速いです。該当のコメントを書き込んだ人は M3 ということなので、M4 を含んだ新しめチップ搭載 Mac ならもっと違いが実感できるのかもしれません。せっかくなのでやり方を残しておきます。

ところで、↓ これって Ollama 公式の X ポストなんですかね? Ollama で正式に MLX バックエンドがサポートされる匂わせとも取られています。

MLX って?

ざっくり言うと、Apple 社純正の、Apple シリコン用機械学習フレームワークです。GPU だけではなく、CPU も使えるみたいです。性能は、100% というわけではなさそうなんですが、いろいろな実験をされた方々の記事を見てみると PyTorch で MPS を使用したときより速いというケースもあるらしいです。

MLX 公式 GitHub: https://ml-explore.github.io/mlx/build/html/index.html

詳細はブラウザの翻訳機能でも使って読んでください。Safari ならアドレスバーの右にあるこのアイコンで日本語に翻訳できます:

というわけで、「MLX 版 LLM」なんて呼んでいるのは、上記 MLX で動くように変換された、オープンな LLM のことです。

MLX-LM とは

MLX で動くように変換された LLM の実行環境です。言語モデルのみ対応しています。実行の他、Safetensor 形式 (HuggingFace にあるオリジナルのモデルはだいたいコレですね) を MLX 形式に変換したり、API サーバを実行する機能もあります。今回の記事では、API サーバとして利用する方法を紹介しています。

MLX-LM 公式 GitHub: https://github.com/ml-explore/mlx-examples/blob/main/llms/README.md

似たような実行環境に MLX-VLM というのもあり、こちらは Pixtral や Qwen2-VL 等のビジョンモデルに対応しています。

MLX-VLM 公式 GitHub: https://github.com/Blaizzy/mlx-vlm

上記二つの MLX-LM と MLX-VLM 両方の API サーバとして動作する FastMLX という Python パッケージもあり機能的には魅力的なのですが、ビジョンモデルは画像の URL かパスしか受け付けなかったり (Dify では使えない)、テキストのストリーミングがうまくいかず例外が出たりして結構工夫しないと使えなさそうなので、これまでのところくじけてます。興味がある方はどうぞ。

FastMLX 公式 GitHub: https://github.com/arcee-ai/fastmlx

LM Studio でも使えます

LM Studio は MLX モデルが使えるので、別に Dify 使わない・使っていないという方はこれ以上読み進めなくて大丈夫です。また、Dify には OpenAI API コンパチのモデルプロバイダとして LM Studio を登録して使うこともできますが、LLM からのレスポンスがなめらかにストリーミングされません。特に日本語の部分は結構な量がまとまって表示されるケースが多いです。なので、Dify から API で MLX の LLM を使うなら、MLX-ML のサーバ機能を利用するのが良いかと思います。

以下、内容はビジョンモデルですが、LM Studio の API を Dify から使う方法を別の記事にしてあるので参考にしてみてください。

MLX-LM で API サーバを立てる

インストール

MLX-LM を利用するには、まず仮想環境に MLX-LM をインストールしてください。今回使ったバージョンは、最新の0.20.4でした。

pip install mlx-lm

一度サーバを立てる

サーバを立てるにはmlx_lm.serverコマンドを使用します (実コマンドはインストール時のハイフンと違いアンダースコアなので注意)。Dify (や他の API クライアント) が別のホストで動いているとか、他のサーバがポートを使用している等という場合は、下の例のようにオプションを指定してあげます。ボクは Dify が別の Mac で動いていて、テキスト読み上げ (text-to-speech) サーバが動いていたりするので、それぞれを指定しています。オプションの詳細はmlx_lm --helpを見てください。--log-levelは付けなくても問題ありません。

mlx_lm.server --host 0.0.0.0 --port 8585 --log-level INFO

こんな表示が出れば動いているはずです。

% mlx_lm.server --host 0.0.0.0 --port 8585 --log-level INFO
/Users/handsome/Documents/Python/FastMLX/.venv/lib/python3.11/site-packages/mlx_lm/server.py:682: UserWarning: mlx_lm.server is not recommended for production as it only implements basic security checks.
  warnings.warn(
2024-12-15 21:33:25,338 - INFO - Starting httpd at 0.0.0.0 on port 8585...

モデルをダウンロードする

ボクの場合 32GB の RAM で動いてほしいので、4bit 量子化されたもの (18.44GB) を選んでいます。

HuggingFace: https://huggingface.co/mlx-community/QwQ-32B-Preview-4bit

MLX-LM サーバが動いている状態で別のターミナルウィンドウを開き、以下の様な簡単なスクリプト書いて保存し、Python で実行してモデルをダウンロードします。

import requests

url = "http://localhost:8585/v1/models"
params = {
    "model_name": "mlx-community/QwQ-32B-Preview-4bit",
}

response = requests.post(url, params=params)
print(response.json())
python add_models.py

ダウンロードが完了したら Ctrl + C でサーバを一度終了します。あ、ちなみにこの方法でダウンロードしたモデルは、LM Studio からも読めますので、どちらも試す場合はコマンドで入れる方法が容量削減になります (ただしフォルダ名は人にきびしい)。

モデルを指定して API サーバを立ち上げる

モデルの保存場所は~/.cache/huggingface/hub/の中で、今回の例ではmodels--mlx-community--QwQ-32B-Preview-4bitというフォルダになります。サーバを立ち上げるコマンドに渡すパスはさらに深く、snapshotの中、config.jsonファイルが含まれるフォルダとなります。そちらを指定して API サーバを立ち上げるコマンドはこんな感じです:

mlx_lm.server --host 0.0.0.0 --port 8585 --model /Users/handsome/.cache/huggingface/hub/models--mlx-community--QwQ-32B-Preview-4bit/snapshots/e3bdc9322cb82a5f92c7277953f30764e8897f85

無事にサーバが上がると、ブラウザからダウンロード済みモデルが確認できます: http://localhost:8585/v1/models

{"object": "list", "data": [{"id": "mlx-community/QwQ-32B-Preview-4bit", "object": "model", "created": 1734266953}, {"id": "mlx-community/Qwen2-VL-7B-Instruct-8bit", "object": "model", "created": 1734266953}]}

Dify に登録する

OpenAI-API コンパチブルのモデルとして登録

Dify へは、OpenAI-API-compatible の LLM モデルとして登録します。モデル名は上に度々登場しているもので、URL にはポートと /v1 が必要、Delimiter には適当に \n\n を入れるくらいで大丈夫だと思います。参考にスクリーンショットを貼っておきます。

Chatbot を作る

上記で登録したモデルを指定し、Max Tokens は 4096 あたりにします。この値であれば 32GB RAM でも 100% GPU で動きます。QwQ からの回答に中国語が混じるのを防ぐには、下の System プロンプトを使ってみてください。100% ではありませんが、効果はあります。

中国語は絶対に使わないでください。日本語の質問は常に日本語で答えてください。
While answering, never ever use Chinese. Always answer in Japanese or English based on language used.
Ollama のモデルに比べて、OpenAI API コンパチは指定できるパラメータ数が少ない

さて、これで準備が整ったので、公開して実際に試してみましょう。

Dify では正確な数字での比較ができないが、体感は速い

さてこれで準備が整ったので、GGUF (ollama pull qwq:32b-preview-q4_K_M) と MLX を使った同じ条件のチャットボットを作り、同じ質問をいくつも投げて違いを見てみました。見た目は確かに MLX 版の方が文字の出力が速いです。ただ、Dify アプリのダッシュボードである「監視」での比較だと GGUF が 6トークン/秒くらいに対し、MLX が 18トークン/秒と 3倍速い結果になっていますが、実際にそこまでの違いはありません。個別のチャットのステータスが見れる「ログ&アナウンス」で確認できる経過時間とトークンの合計から算出すると、GGUF は大体 12トークン/秒くらいで、MLX は 35トークン/秒くらいとなるのですが、実際に出力されている文字数をざっと見比べてもそこまでの違いはありません。むしろ明らかに Ollama から多くの文字を受け取っているのに、MLX-LM から受け取ったトークン数の方が多くカウントされています。トークンの算出方法が違うのかもしれません。今回試しませんでしたが、英語でやると正確に調べられるかもしれません。また、単純に GGUF (llama.cpp) vs MLX-LM のガチンコ勝負をするなら、スクリプトを書いて比較するのが良いと思います。

追記: 本ブログの英訳記事を書く際に英語でテストを実施したところ、MLX の方が 30-50% 速い結果となりました。使ったプロンプトも載せてあります。

MLX-LM に乗り換えられるか?

QwQ や特定のモデル固定で構わないなら MLX-LM はアリだと思います。Mac の電源を切る習慣がある方は、別のブログ記事に書いたスクリプトエディタでアプリケーションを作る方法でログイン時に実行させれば勝手にモデルが使えるようにしてくれますし。でも、お手軽さと使いやすさでは Ollama の方が完全に上なので、複数のモデルをとっかえひっかえ使う方には向きません。上で触れた FastMLX はクライアント側からモデルの入れ替えができるようなので、本気で移行を考えるなら選択肢になり得るかと思います。ただ、Ollama 公式 X らしきポストを真に受けると、MLX 対応しそうな感じもあるので、どちらかと言えばそれ待ちですね。いずれにせよ、今回の GGUF vs MLX の趣旨とは外れますが、個人的に QwQ の出力速度はチャットベースであれば十分です。用途に応じて試してみてください。

Image by Stable Diffusion

「リンゴの上で走るロボット」をお願いしたのですが、big apple だと NYC になり、huge apple とすると、そんなものないとガン無視されました。というわけでこんな感じで手を打ちました。日本人の持つ「かわいいロボット」という感覚は西洋にはなさそうです

Date:
2024年12月16日 0:38:20

Model:
realisticVision-v51VAE_original_768x512_cn

Size:
768 x 512

Include in Image:
fancy illustration, comic style, smart robot running on a huge apple

Exclude from Image:

Seed:
2791567837

Steps:
26

Guidance Scale:
20.0

Scheduler:
DPM-Solver++

ML Compute Unit:
CPU & GPU

Dify で Mac 専用 MLX 版 Qwen2-VL を使う (たぶん、LM Studio のバグ技)

前回 Pixtral (Mistral 社のビジョンモデル) の日本語コミュニケーション能力と画像認識の性能の高さに感動しつつも、画像の中の日本語は読めないことを知り傷心したボク。Ollama のバージョンアップで正式に利用できるようになった Meta 社の画像認識モデルである Llama3.2 Vision 11B こそが本命であるはずだ!と早速試したんですが、あまりのぱっとしなさに記事にすることもありませんでした。ローカルで「使える」オープンモデル・ウェイトのビジョンモデルの登場はまだ先なのか、または画像内の日本語が認識できる LLM なんて 32GB RAM の Mac には一生やってこないのか、っていうか OCR したいなら普通に macOS 標準のプレビューでいいじゃんか、ということなのか。なんて思っていたら、普通にありました。とっくに出てました。しかもボクが最近何かとメインで使っている Alibaba 社の Qwen ファミリーに。そう、Qwen2-VL です。

パラメータ数は 2B、7B、72B
はい、日本語正式サポートしてるぅ (上のサイトを Safari で日本語に翻訳してもらった一部のキャプチャ)
画像内の日本語認識が不要なら、Pixtral がオススメ

簡単に使うなら、やはり LM Studio 単体で

LM Studio の虫眼鏡アイコンで “qwen2-vl” と打ち込み、横のチェックボックスで MLX を選べば mlx-community にて変換された MLX 版の Qwen2-VL がずらずら出てきます。一番大きい 7B-Instruct-bf16 でも 16.60 GB なので、32GB RAM の Mac でも問題無く動きそうですが、そこはビジョンモデル、テキストオンリーのモデルと違ってメモリを喰います。BF16 はたまに CPU を使ったり頻繁にハルシネーションをおこしたりするヤンチャ坊主なので、オススメは 7B-Instruct-8bit です。というわけで、LM Studio で動けばそれでヨシ、という方はこれ以降読まなくて大丈夫です。ステキなビジョンモデルライフをエンジョイください。

bf16 のスクリーンショットですが、オススメは Qwen2-VL-7B-Instruct-8bit

Ollama では動かない

さて、普段はローカル LLM のモデルプロバイダとして使っている Ollama をなぜ使わないのかということを説明します。Ollama はバックエンドに llama.cpp を使っていますが、llama.cpp は (今のところ) ビジョンモデルに対応していません。Llama3.2 Vision は、Ollama の開発者が llama.cpp に依存せずに 頑張ってどうにか動くようにした (?) らしいです。が、Llama3.2 Vision 11B のパフォーマンスが 32GB RAM の M2 Max ではイマイチでして、ボクはすぐに使うのやめちゃいました。英語だけで OK の方は良いのかもしれないですね。

Safetensors は MPS で動かすこともできる

↓の Zenn に投稿された金のニワトリさんの記事に、MPS でサンプルコードを動かす方法が紹介されています。Llama3.2 Vision にガッカリしていたときに試したので、えらく感動しました。Mac の GPU で素のビジョンモデル Qwen2-VL が動いていたので。

https://zenn.dev/robustonian/articles/qwen2_vl_mac

さて、API で利用するにはどうするか?

macOS で Qwen2-VL を動かすには、元の Safetensors 版か、MXL 版のどちらか、ということがわかったわけですが、やっぱり Dify から使いたくなってしまいます (よね?)。本家の GitHub にあるワンライナーの API サーバ起動方法は vllm という Python のライブラリを使用しており、残念ながら Mac 未対応です。他の方法を探ると、言語モデルの MLX 用なら割といろんな API サーバライブラリがあるのですが、ビジョンモデルに対応したものとなるとかなり絞られました。さらにそこから実際に Qwen2-VL を使おうとすると、そもそも Dify に登録できなかったり、無理矢理登録できても例外しか発生しないという悲しい状況が 2週間ほど続きました。

同じように MLX のビジョンモデルである Pixtral は LM Studio が API サーバとなり Dify から使えるのに、Qwen2-VL を OpenAI コンパチモデルとして登録しようとすると、最初の ping (モデルが本当につながるかどうかのテスト) に画像が含まれないために Qwen2-VL がエラーを返してきて登録ができません。あらゆる方法を自分なりにいろいろと試しては失敗し、を繰り返していたある日、突然でたらめな組み合わせで動き始めました。詳細は以下に続きます。

成功した構成とバージョン

LM Studio または Dify の新しいバージョンではバグが潰されてこの方法が使えなくなるか、はたまた正式にサポートされて普通に動くようになるかわかりませんが、とりあえず ↓ の構成&バージョンで動作が確認できています。

  • Dify: 0.11.2 (0.11.1 でも動いた実績あり)
  • LM Studio: 0.3.5 (Build 2)
  • Pixtral: mlx-community/pixtral-12b-8bit
  • Qwen2-VL-7B: mlx-community/Qwen2-VL-7B-Instruct-8bit (FastMXL でダウンロードしたモデル)
  • Qwen2-VL-2B: mlx-community/Qwen2-VL-2B-Instruct-4bit (LM Studio でダウンロードしたモデル)
  • macOS: Sequoia 15.1.1 (上記アプリが動けば、多分何でも大丈夫)

Dify から LM Studio の Qwen2-VL を使う方法

やり方は、ほぼ前回の記事と同じです。異なっているのは、LM Studio には Qwen2-VL をロードしておくだけです。Dify のアプリで使うモデルは mlx-community/pixtral-12b-4bit のままで構いません。というか、上記バージョンでは Dify に LM Studio 上の Qwen2-VL を登録できないので、Dify は Pixtral を呼び出しているつもり、LM Studio は読み込み済みの Qwen2-VL を使って推論をする、という状況になります。本来 LM Studio は指定されたモデルを読み込むはずなので、バグだと思いますが、これで動きます。

実行サンプル

いくつか参考となりそうな画像を貼っておきます。

テキストの日本語が怪しいことは多いです
提灯の文字を読んで「雷門」と回答したわけでは無いらしい。っていうか「唐傘 (提灯)」って
文字だけなら手書きの日本語を読めます
結構がんばってる。Markdown の罠でインデックス番号が各行に振られているところと、写真一番下のテキストが斜めになってしまっているところは読めていないのが残念。ラウンドを繰り返すか、プロンプトを工夫すれば精度はあがるかも

感想

性能的には、画像認識の精度や知識は Llama3.2 Vision モデルよりは良いけど、Pixtral ほどじゃ無いです。日本語 OCR メインの用途なら macOS のクイックルックやプレビューの方が相当優れています。というわけでやはり、期待したほどでは無いな、というのが正直な感想です。大量の RAM を積んでいる方が 72B のモデルを動かすと、また違う感想になるかもしれません。ビジョンモデルの使いどころも定まっていないボクにとっては、「普通には動かせないものを動かす」というハッカー的欲求を満たせたのでとりあえず満足、という感じですかね。

ところで最近のローカル LLM はほとんど Qwen2.5 シリーズしか使ってません。日本語チャットは qwen2.5:14b-instruct-q8_0 (15GB) もしくは qwen2.5:32b-instruct-q4_K_M (19GB)、コーディングは qwen2.5-coder:14b-instruct-q8_0 (15GB) もしくは qwen2.5-coder:32b-instruct-q4_K_M (19GB) です。速度や大きなコンテキスト長を扱いたいとき (注) は 15GB サイズのモデル、精度が欲しいときは 19GB のモデルを使っています。欧米のモデルよりも日本語が得意というのもありますが、各種リーダーボード (日本語オープン、日本語総合コーディング) でも Qwen シリーズは高評価ですので、未体験の方はゼヒ。

ついでにですが、最近リリースされた推論 (reasoning) 重視モデルの QwQ-31B-Preview も普通に Ollama でダウンロードして使えます。32GB RAM の M2 Max での生成速度は速くない (7.5 TPS 程) ですが、性能というか、考え方の方向性がこれまでとかなり違い、ヤバいです。次元が違う感じ。

(注: 大きなサイズのモデルだとコンテキスト長を小さくしないと遅くなります。詳しくはこちらの記事をご覧ください:)

最後に、ビジョンモデルの活用法を一つ思いつきました。大量の素材用写真が無造作にフォルダに入っているみたいな状況があれば、それぞれの写真の特徴を macOS の「情報を見る」のコメント欄に書き込んでくれるスクリプトなんてよさそうですね。Spotlight で検索できるようになるし。Qwen2-VL はビデオの読み込みもできるらしいので、整理に困っている大量の画像・映像を持っている人は良いかもですね。

Image by Stable Diffusion (Mochi Diffusion)

Pixtral の記事のトップ絵に対抗した内容で、登場人物を alibaba にし、場所を Alibaba Cloud 社があるらしい中国の Yu Hang にしてみました。きっと本当はこんな町並みでは無いのでしょうが、ボクの想像と概ね近いものを選びました。

Date:
2024年11月27日 23:59:11

Model:
realisticVision-v51VAE_original_768x512_cn

Size:
768 x 512

Include in Image:
masterpiece, intricate, realistic, sharp focus, photograph, alibaba walking in Yu Hang, china

Exclude from Image:

Seed:
1426778725

Steps:
20

Guidance Scale:
20.0

Scheduler:
DPM-Solver++

ML Compute Unit:
CPU & GPU

Mac のローカルオンリー環境で、画像認識 AI の Pixtral 12B MLX 版を使う (LM Studio 編)

フランスの生成 AI 企業のトップである Mistral 社が、画像認識にも対応したマルチモーダル モデルの Pixtral 12B を Apache 2.0 ライセンスでリリースしています。画像を読み込ませると、何が映っているかを答えてくれるタイプの AI ですね。こちらを、ボク得意の Mac ローカルオンリー環境で実行した方法を共有します。今回使ったのは、LM Studio と Dify です。Pixtral を実行するだけなら LM Studio の 0.3.5 以上で OK ですが、Dify から API 経由で LM Studio を利用してみたので、後半にはその設定方法を紹介してます。

環境について

Mac

ボクの M2 Max Mac Studio には 32GB しか RAM が積まれていないので、Dify だけは別の M1 Mac Mini で動かしています。これから Mac (M4 搭載 Mac Mini とか MacBook Pro とかいいですな〜) を買う方でローカル LLM も試したいと考えている方は、頑張って 64GB 以上の RAM (ユニファイドメモリ) を搭載した機種を買いましょう。それより少ない RAM 容量で LLM を楽しむのはいろんな工夫や妥協が必要で苦労します。本ブログにはそんな切ない記事があふれています。

RAM が少ないと、こんな感じのやりくりが必要になります

LM Studio

v0.3.0 で、インターフェイス含め大幅なアップデートがありました (v0.2 シリーズを使っている場合は、新規でインストールが必要)。その後も着々とバージョンアップが進められ、API 経由で他のアプリから LLM を利用できるようになり、Mac の GPU 用に最適化された MLX バージョンの LLM が動くようになり、v.0.3.5 で Pixtral に対応しました。また、同バージョンでは、メモリ喰い気味の GUI を閉じてサーバだけ動かす Headless mode にも対応したということで興奮したのですが、実際は LLM (pixtral-12B@4bit) がロードされると LM Studio Helper が 12~20GB ほどメモリを使用し続けるので、現状はまだ Ollama のような使い勝手は望めません。残念。

その他 LM Studio v0.3.5 の詳細はこちらをどうぞ: https://lmstudio.ai/blog/lmstudio-v0.3.5

Ollama

2024年 11月 4日現在、Ollama では Pixtral がサポートされていません。Llama 3.2 vision が動くらしい v0.4.0 は pre-release されているので、正式リリースや Pixtral のサポートも時間の問題かもしれません。

Ollama v0.4.0 に関してはこちらをどうぞ: https://github.com/ollama/ollama/releases/tag/v0.4.0-rc6

Dify

これから始める方は最新版になっちゃうんでしょうが、最近、リリース直後は多くのバグで悩むケースが散見されています。ボクはまだ v0.9.1-fix1 を使用しているため、v0.10 以降のファイル添付方法が変わったバージョンでは見た目や使い方が違う可能性があります。Dicord での情報収集が良いと思います。

LM Studio で MLX 版 Pixtral を動かす

LM Studio v0.3.5 以上のインストールは完了してる前提で

無料の Mac 用アプリがここからダウンロードできます。

Pixtral 12B 4-bit をインストール

32GB RAM の Mac では 4-bit (量子化) 版をオススメします。LM Studio 込みでオンメモリで動きます。もっと RAM を積んでいる方は、お好みで 8bit や bf16 版をどうぞ。

虫眼鏡アイコンの Discover → 検索窓に「pixtral」と入力 → MLX だけにチェックを入れる (GGUF を外す) → モデル名やファイルサイズを確認 → Download ボタンクリック

ボクのインターネット環境の問題かわかりませんが、ダウンロード中何度もタイムアウトしていました。左下のボタンでたまに確認し、下のような Timed-out が出ていたら再読み込みボタンをクリックしましょう。

他のエラーで止まることもありますが、再読込で進められます

Chat で試す

Dify で使う必要が無い方 (え?みんなそう?) は、そのまま LM Studio で Pixtral が楽しめます。

吹き出しアイコンの Chat → Select a model to load ドロップダウンから、ダウンロードした Pixtral を選択 → Load Model ボタンクリック

例として、以前没にしたトップ絵をドラッグアンドドロップして「この画像について教えてください」としたところが以下画像です。Pixtral は Mistral Nemo 12B をベースにしていると言うことで、上手な日本語でやりとりができます。ちなみに、Nejumi さんのリーダーボードでは、Mistral Nemo 12B は elyza/Llama-3-ELYZA-JP-8B より日本語性能が上らしいです。確かに。

LM Studio への日本語入力には難あり。エンターは確定+送信になり、最後の確定部分がチャット窓に残ります

他にもいくつか AI による生成画像を読み込ませてみたところ、大体 30 TPS (トークン/秒) で、正しく詳細な回答が得られました。個人的に画像認識 AI の正しい使い道は見つけられていないのですが、想像以上の性能です。

というわけで、Mac のローカル環境で画像認識マルチモーダル LLM を試したいだけ、という方はお疲れ様でした。いろいろとお試しください。

以降は Dify から API で LM Studio のモデルを使う方法になります。

LM Studio API サーバ設定

基本

Developer アイコンから設定します。全ての Off/On スイッチが表示できていないと思うので、まずは Developer Logs の境界線をつかんで下に押し広げましょう。Start ボタンで API サーバが利用できますが、確認しておいた方がよさそうな設定は以下となります。

  • Server Port: デフォルトで 1234
  • Server on Local Network: 他の Mac から API アクセスするなら、On
  • Just-in-Time Model Loading: Dify からモデルを選択したいなら、On
これで Start をクリックすれば、API サーバは起動します
LAN に公開した場合はこのポップアップが出るので、許可します

ヘッドレスモード (GUI 無し API サーバのみ)

右下のギアアイコンまたはコマンド+カンマで App Settings を開き、Local LLM Service (headless) を有効にします。これで、GUI を Quit しても API サーバは動き続けます。

GUI を開いたり、サーバを停止したりするには、メニューバーのアイコンを開きます。メモリを解放するなら、Unload All Models または、Quit LM Studio を選びましょう。

Dify から LM Studio API サーバに接続できるようにする

モデルプロバイダーにある、OpenAI-API-compatible に登録します。

MLX 版の Tanuki 8B も登録済みの図

設定内容はこんな感じです:

  • Model Name: モデル名 (例: mix-community/pixtral-12b-4bit)
  • API Key: 無し
  • API endpoint URL: 例 http://192.168.1.10:1234/v1 とか http://localhost:1234/v1 (ポート番号/v1 を忘れずに)
  • Completion mode: Chat
  • Model context size 2048
  • Upper bound for max tokens: 2048
  • Function calling: Not Support
  • Streaming function calling: Support
  • Vision Support: Support
  • Delimiter for streaming results: \n\n (区切り文字が入力必須ですが、とりあえず改行二つのこれで動きます)

Dify で動く超シンプル画像認識アプリ

DSL ファイルを下に貼っておきますが、チャットボットの Chatflow を作り、機能で「画像アップロード」を有効にして、モデルとして Pixtral を選んでいるくらいの単純なものです。

DSL を開く

app:
description: 32GB RAM オンメモリで動作するビジョンモデル。Pixtral は MLX 版で、LM Studio で実行中。事前に LM Studio
サーバを実行しておくこと
icon: eyes
icon_background: ‘#D5F5F6’
mode: advanced-chat
name: Pixtral 12B 4Bit (MLX)
use_icon_as_answer_icon: true
kind: app
version: 0.1.2
workflow:
conversation_variables: []
environment_variables: []
features:
file_upload:
image:
enabled: true
number_limits: 3
transfer_methods:
– local_file
– remote_url
opening_statement: ”
retriever_resource:
enabled: false
sensitive_word_avoidance:
enabled: false
speech_to_text:
enabled: false
suggested_questions: []
suggested_questions_after_answer:
enabled: false
text_to_speech:
enabled: false
language: ”
voice: ”
graph:
edges:
– data:
isInIteration: false
sourceType: start
targetType: llm
id: 1730552831409-source-llm-target
source: ‘1730552831409’
sourceHandle: source
target: llm
targetHandle: target
type: custom
zIndex: 0
– data:
isInIteration: false
sourceType: llm
targetType: answer
id: llm-source-answer-target
source: llm
sourceHandle: source
target: answer
targetHandle: target
type: custom
zIndex: 0
nodes:
– data:
desc: ”
selected: false
title: 開始
type: start
variables: []
height: 54
id: ‘1730552831409’
position:
x: 30
y: 253.5
positionAbsolute:
x: 30
y: 253.5
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 244
– data:
context:
enabled: false
variable_selector: []
desc: ”
memory:
query_prompt_template: ‘{{#sys.query#}}’
role_prefix:
assistant: ”
user: ”
window:
enabled: false
size: 10
model:
completion_params:
temperature: 0.7
mode: chat
name: mlx-community/pixtral-12b-4bit
provider: openai_api_compatible
prompt_template:
– id: 9011766a-9f0d-45d2-87bb-8f2665ad80db
role: system
text: ”
selected: true
title: Vision
type: llm
variables: []
vision:
configs:
detail: high
enabled: true
height: 98
id: llm
position:
x: 334
y: 253.5
positionAbsolute:
x: 334
y: 253.5
selected: true
sourcePosition: right
targetPosition: left
type: custom
width: 244
– data:
answer: ‘{{#llm.text#}}’
desc: ”
selected: false
title: 回答
type: answer
variables: []
height: 107
id: answer
position:
x: 638
y: 253.5
positionAbsolute:
x: 638
y: 253.5
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 244
viewport:
x: 49.4306562198135
y: 352.30093613089844
zoom: 0.7237754523423506

アプリを公開するとチャットウィンドウに画像をドラッグアンドドロップできるので、画像と質問を入力すれば回答が返ってきます (初回の LLM のロードが行われる時はタイムアウトするかもしれません。少し待って再度コメントを投げると返事が来ます)。Dify から使うと若干スピードが落ち、何度か試した後確認すると、20 TPS 以下でした。実行時のメモリプレッシャーはこんな感じ ↓ です。モデルがロードされた後のメモリの使用量が、ロード前ほど下がらないことがわかると思います。この時で LM Studio Helper のメモリ使用量は 12GB ほどでした。

以前の記事で使った画像を読ませたところ ↓

追加質問の余地がないほどの回答

Pixtral さんは画像の中の日本語は読めない (日本語 OCR としては使えない)

チャットでは流ちょうな日本語でやりとりできるのですっかりその気になっていたのですが、画像の中の日本語は読めないみたいです。ベーマガのペーパー・アドベンチャーをスキャンして遊びやすくしようと思っていたのですが、そういう用途には使えないようです (注意: 雑誌のスキャンを無許可で公開すると著作権侵害となります)。

なんかそれっぽいことが書いてあるけど、実際見比べると全然違う

雷門が歌舞伎町に見えてしまえば、文字だってそう読めてしまいます。

大提灯を指摘すれば気づいてくれるかも、との期待は打ち砕かれました

かわいい手書き文字でもテスト。うーむ、日本語 OCR として使うのは諦めた方がよさそうです。

念のため英語とフラ語でも聞いてみました。

ちょっと!誰よ、ひとみって!
朝起きたら、質問の前にあいさつをしよう

面白かったのでいくつも画像を貼ってしまいましたが、日本語 OCR 能力は無いという結論に変更はありません。アルファベットなら手書きもまーまーイケそうなので残念ですが、このパラメータ数 (12B) ですし日本語に特化しているわけでもないわけですから、欲しがりすぎてはいけませんね。

(おまけ) Mac で OCR するなら、写真の文字をなぞってコピーするだけ

話はそれますが、Mac なら画像や写真の文字部分をなぞればほぼコピーできちゃいます。素のままでできるはずですが、うまくいかないときはプレビューで開いて、ツールからテキスト選択選べばほぼイケるかと。

ペーパー・アドベンチャーの写真の一部を選択した状態

全文載せると著作権侵害になるので、冒頭部分のみ (関係者の方からご指摘あれば削除いたします)。適当に iPhone で撮影した写真でもこの認識力。改行位置の的確さもほぼ OK。これが OS レベルで実装されているのですごい。すごいありがたい macOS。

1 あなたは今、この町に入るための道に立っている。
北→7 西→26
2 君は必死に怪物と戦ったがあと一歩というところで殺されてしまった。
GAME OVER
3
君は逃げきれず、つかまってしまった。
GAME OVER
4
5
ここは森の入口だ、入れ。→109小さな木の箱があった。開けると、ダイヤ,サファイヤ、ルビーがあった。】
つだけ持っていってもよい。
ダイヤ→90 サファイヤ→39
ルビー→53
6
怪物半魚人が現れた。水中なので逃げられない。戦え。
体力が9以上→28 9未満→2
7怪物ゾンビーが現れた。戦うか,逃げ
るかさめよ。
戦う→37
逃げる→61
8
ここは武器屋だ。きみは金貨を50枚持っている。なにか買え。
剣,金貨50枚→16号,金貨40枚→69

Apple Inteligence もまもなく使えるようですし、AI スタートアップで収益上げるって難しそうだな、と思いました。

Image by Stable Diffusion (Mochi Diffusion)

おフランスをパリジェンヌが歩いているおしゃれな画像を期待して描いてもらいました。

Date:
2024年11月3日 15:13:32

Model:
realisticVision-v51VAE_original_768x512_cn

Size:
768 x 512

Include in Image:
masterpiece, intricate, realistic, sharp focus, photograph, woman walking in Paris

Exclude from Image:

Seed:
2526772693

Steps:
23

Guidance Scale:
20.0

Scheduler:
DPM-Solver++

ML Compute Unit:
CPU & GPU

マウスホイールが一瞬だけ逆にスクロールする問題を解決

この記事は、主に Mac ユーザに向けての記事になります。マウスは数千円程度の、特に特徴の無い、平均的もしくはお安いもの (4千円以下?) が対象になるかと思います。ボクが解決できたのは USB ドングルタイプです。

注意: 「Mac でマウスのホイールの回転方向とスクロールの方向を逆にする方法の説明」ではありません。その設定方法は簡単に見つかります

この記事で説明するのは、使っていると一瞬だけ上下逆に動いちゃう、という問題の解決方法です。特に、一度スクロールを止めて少ししてからもう一度同じ方向にスクロールしようとすると、一瞬逆に動いて読んでいたところを見失う、というような問題への解決方法です。この方法で解決しなければ、世の中に 100万ページくらいある他のサイトで紹介されている方法をお試しください。

解決方法

システム環境設定のマウスで、スクロールの速さを 2つくらい下げましょう。どれくらいが最適かは試しながら調整してください。これで上に書いた不具合は解決します。多分、3つ 4つ下げても体感的にスクロールの遅さを感じないと思います。なのに、きっと問題は解決します。

macOS Sequoia 15.x はここ:

真ん中あたりで試しましょう。たいていの人はスピードの差を感じないです

macOS Sonoma 14.x まで (多分) はここ、もしくは Spotlight で「スクロールの速さ (マウス)」を検索するのが早いかも:

一番左でもスクロールに不便なし。macOS 15 でアイコンやめちゃったのはカメへの配慮?

なぜ解決するか 〜多分こうなんじゃないかな〜

「ホイールの汚れを数ヶ月おきに掃除しましょう」なんて話を真に受けてマウスを分解したことがある人ならわかると思いますが、大抵のマウスのホイールの内側には細い溝がたくさん開いていて、片側から光もしくはレーザーが出され、反対側のセンサーでで受けています。ここでホイールの回転を読んでいるわけですね。お安いマウスだと、Mac 側が感度を高めた (スクロールを速くした) 時の読み込み速度に追いつかなくて、結果逆回転と判定されちゃうんじゃ無いかと思います。Mac が落ち着いてホイールの動作を読み取るようにしてあげることで、常に正しい結果が得られる、ということなんじゃないかなと思ってます。知らんけど。

試した結果とか、使っているマウスとか

ボクの場合、一つのマウスを仕事用の Windows と Mac で使っていますが、Windows では全く発生したことが無かったことから、汚れ、ハードウェアの不具合、電池の消耗等は除外していました。ふと昔から Mac はマウスの解像度が高かったということを思いだし、ホイールのスクロールの速度を下げたことが功を奏しました。スクリーンショットの位置に変更した後、タイトルの不具合は一度も発生していません。もちろん Google 先生にも相談しましたが、役立つ情報はありませんでした。

ちなみに使っているマウスは、logi の M220 (レーザー、静音タイプ、USB ドングル付き) です。ホイールのほどよい固さとクリックのしやさも気に入っているので、こんなことで解決できてよかったです。なので、皆さんへハッピーのお裾分けです。せっかくなのでお小遣い稼ぎに Amazon のリンクを貼っておきます。

Image by Stable Diffusion

マウスのヒーローがマッドサイエンティストをやっつけるイメージを作ろうとしたら、ヤバい感じの画像ばかりができてきました。最終的には、ここまでひどい完成度なら誰からも怒られないだろう、という画像を採用です。某万博マスコットキャラクターの採用理由と近いですかね。知らんけど。

Date:
2024年10月18日 0:29:23

Model:
realisticVision-v51VAE_original_768x512_cn

Size:
768 x 512

Include in Image:
comicbook cover, the super hero mouse-man versus a mad doctor

Exclude from Image:

Seed:
2438098213

Steps:
25

Guidance Scale:
20.0

Scheduler:
DPM-Solver++

ML Compute Unit:
CPU & GPU

Dify と Style-Bert-VITS2 でライブ配信風音声チャットを実現 (ローカル専用)

今回作ってみたのは、Dify を使った音声チャットボットです。キーボードからの入力に対し、生成 AI が音声で答えてくれます。遊んでみた感覚としては、ちょっとラグがあってから声で返事があるので、ライブ配信に参加している感じに近いですね。参加したことないですけど。あと、昔で言えば、人工無能を使った BBS シミュレータといったところでしょうか。昔で言い換える必要も無いんですけど。

ローカル専用です

Dify でチャットボットを作り、Ollama のローカル LLM に推論させ、Style-Bert-VITS2 にしゃべらせる、という構成です。もちろん自分で作った (学習させた) 音声モデルを利用できます。Style-Bert-VITS2 が走っているサーバに直接しゃべらせるので、ローカル専用です。Dify には (本記事執筆時の最新版 0.9.1-fix1 でも) 簡単に音声ファイルを再生する機能が無いため、ある意味苦肉の策です。Style-Bert-VITS2 の API サーバに物理的に音声再生能力があれば、Mac じゃなくても今回紹介する (本家を少しいじっただけの) スクリプトで実現できると思います。

環境構築

Dify と Ollama の構築はボクのブログの別記事を見てください。RAM がいっぱい (64GB 以上) あれば、1台の Mac でまかなえます。ボクの Mac は 32GB しか RAM が無いので、Dify を別の Mac (mini) で走らせています。

下の記事で紹介していますが、Style-Bert-VITS2 の CPU 学習可能版を作りました。Mac ユーザや CUDA が動かない環境の方向けです。ブログ記事 (というか GitHub) を参考に環境を作ってください。CUDA が動く環境の方は、本家の Style-Bert-VITS2 で大丈夫です。

API サーバスクリプトの説明

まず、すでに環境構築済みの方は、pydubをインストールしてください。今回新たに構築した場合は、インストールされているはずです。

pip install pydub

しゃべる API サーバの server_speech_fastapi.py は、元のスクリプト server_fastapi.py に音声再生機能を追加しただけです。追加変更箇所は以下ですが、まるごと持って行くならこちらからどうぞ

# サーバサイドで再生する為にインポート
from pydub import AudioSegment
from pydub.playback import play
            # 以下 2行追加して、音声を再生。wav のかわりに、テキストを返す
            speech = AudioSegment.from_file(wavContent, format="wav")
            play(speech)
            #return Response(content=wavContent.getvalue(), media_type="audio/wav")
            return Response(content=text, media_type="text/html")

Response は、音声合成に使用した (クライアントから送られた) テキストです。何の工夫も無くごめんなさい。ただ、音声の再生ということで言えば、音声データをクライアントに送りクライアントが再生するとか、サーバ自身で再生するにしても、一度ファイルとして書き出してから再生する、みたいな余計な処理がない分速いと思います。

設定はconfig.ymlserver:ブロックを少しいじって、ポートの変更と受け付けるテキストの上限をそれなりに変更しましょう。ちなみにdevicempsにしても、CUDA が動かなければ APIサーバスクリプト側でcpuにしちゃいます。v. 1.0.3 で MPS で動くようになりました。利用可能なデバイスを自動で判定します。CPU ならキャパの半分を使用します。 → MPS で音声合成すると声がかすれる現象が確認できています。解決できるまで、CPU で実行してください (GitHub のファイルは更新済み)。

server:
  device: mps
  language: JP
  limit: 20000 # 文字数の上限
  origins:
  - '*'
  port: 5055 # デフォルトの 5000 から、使っていないポートに変更

API サーバは、仮想環境内から以下コマンドで実行できます。モデルとトークナイザの読み込みに時間がかかります。

python server_speech_fastapi.py

しばらく待って、こうなれば準備完了です。

10-12 19:06:26 | DEBUG  | __init__.py:130 | pyopenjtalk worker server started
10-12 19:06:27 |  INFO  | bert_models.py:92 | Loaded the Languages.JP BERT model from /Users/handsome/Documents/Python/Style-Bert-VITS2-Mac/bert/deberta-v2-large-japanese-char-wwm
10-12 19:06:27 |  INFO  | bert_models.py:154 | Loaded the Languages.JP BERT tokenizer from /Users/handsome/Documents/Python/Style-Bert-VITS2-Mac/bert/deberta-v2-large-japanese-char-wwm
10-12 19:06:27 |WARNING | tts_model.py:397 | No model files found in model_assets/.cache, so skip it
10-12 19:06:27 |  INFO  | server_speech_fastapi.py:116 | Loading models...
10-12 19:06:27 |  INFO  | server_speech_fastapi.py:123 | The maximum length of the text is 20000. If you want to change it, modify config.yml. Set limit to -1 to remove the limit.
10-12 19:06:27 |WARNING | server_speech_fastapi.py:129 | CORS allow_origins=['*']. If you don't want, modify config.yml
10-12 19:06:27 |  INFO  | server_speech_fastapi.py:338 | server listen: http://127.0.0.1:5055
10-12 19:06:27 |  INFO  | server_speech_fastapi.py:339 | API docs: http://127.0.0.1:5055/docs
10-12 19:06:27 |  INFO  | server_speech_fastapi.py:340 | Input text length limit: 20000. You can change it in server.limit in config.yml

テストはウェブブラウザから行うのが簡単です。アドレスバーにこんな感じでしゃべって欲しいテキストとモデル名 (ModelNameを差し替え) を入れてエンターキーを叩いてください (スピーカのボリュームに注意!)。

http://localhost:5055/voice?text=こんにちは!おしゃべりできて、うれしいです!&model_name=ModelName

API サーバを自分の Mac/PC で実行していれば、スピーカーなどから音声が流れた後にブラウザに「こんにちは!おしゃべりできて、うれしいです!」と表示されるはずです。ターミナルにも SUCCESS が出ていれば OK です。次に進みましょう。

Dify で AI アプリを作る

チャットボットを Chatflow で作ってください。

LLM はお好きなのをどうぞ。Ollama でローカルモデルを使用される方は、日本語が多少怪しくてもレスポンスが速い、軽めのモデルが良いと思います (参考まで、ボクは qwen2.5:14b-instruct-q8_0 を Temperature: 0.7、Top_P: 0.9、Keep Alive: 30m で使っています。普段よく使っている Llama3.1 だと内容によっては全く答えてくれないのですが、Qwen 2.5 は多少攻めた会話もできます)。

SYSTEM プロンプトにはキャラ付けの他、「簡潔に」「markdown を使用しない」「日本語のみ」「英語や中国語は絶対に使わない」という内容を入れておくのが良いでしょう。Style-Bert-VITS2 にいい感じにしゃべってもらうには、とにかく日本語であることが重要です。

次に、LLM と回答をつなぐラインをクリックし、「HTTPリクエスト」を追加します。

最低限この内容を設定すれば大丈夫です。

API:

メソッドURL
POSThttp://<IPアドレス>:<ポート>/voice
例: http://192.168.1.12:5055/voice

パラメータ:

キー
textLLM/(x) text
model_name(自分のモデル名)
例: ModelName
入力後はこんな感じ

最後の回答ブロックには、最初から入っている LLM/(x) text か、HTTPリクエストの (x) body が良いと思います。前者はしゃべり出す前に、後者はしゃべり終わってからテキストがチャットに表示されます。

どちらか気に入った方で

ここまでできたらプレビューでテキストの生成や音声の再生をテストし、仕上げに公開しましょう。

これで、基本的なライブ配信風チャットシステムの完成です。システムプロンプトでのキャラ付けをしたり、RAG で知識を入れてあげたり、ムフフな回答もしてくれる LLM を採用したり、お好きなようにお楽しみください。ただし、音声データの著作権を持っていない場合は、あくまでも個人で楽しむだけにとどめてください。

学習につかった音声データと、満足度、そして不安

今回このシステムを楽しむに当たり、5つの mp3 の音声データを合計 40秒弱用意しました。Mac の CPU で学習にかかった時間は 20分ほどです (M2 Max 12-core CPU)。で、実際に Style-Bert-VITS2 がしゃべった感じはどうかというと、かなり良いですね。日本語の発音として相当に自然です。ラグに関しても、音声合成は CPU で処理されますが、LLM による文章の生成よりかなり短いので問題ありません。

音声データはネットで拾ってきた動画から切り出したもので、内容はファンイベントの告知です。コーパスとして適しているものなのかはわかりませんが、丁寧に視聴者に語りかける口調が素材として良かったのかもしれません。当然 Style-Bert-VITS2 やモデル、トークナイザの性能、日本語への最適化が大きいことは間違いありません。すばらしいシステムを作り上げてくださった作者様達に大感謝です。

それと、これでチャットしてるとすごく不思議な感じになりますね。LLM に、アイドルとして回答して、と指示出ししているだけでそれ以外の知識を与えているわけじゃ無いのに、レスポンスが音声なだけでかなりリアルにアイドルのライブ配信に参加している感じになります (したことないですけど)。以前、Google の AI 研究者か誰かが、AI はついに本当の知能を得た!証拠もある!と会社に報告したことで解雇された、というニュースを見ました。今回この AI アプリで遊ぶことでそのことを思い出しました。ありうるな、と。最近よく聞く「脳がバグる」状況は、かなり簡単に実現できますね。悪意のある LLM を使った場合にどうなるか、ちょっと不安になりました。

ところで Style-Bert-VITS2-Mac の学習の MPS 化はどうなったのか

PyTorch の Nightly ビルドはすでに AMP Autocast で MPS をサポートしており、学習用のコードはほぼ動くようになりました。6秒のファイルの学習は、1分20秒ほどで完了します。ただし、エラー無く処理が終わるようにコードをいじると完成したモデルは誰か知らない女性の声になり (ホラー?)、いじらずに実行するとロスの処理の部分でエラーとなり、失敗します。ボクには PyTorch や機械学習の知識や学習が足らず、不具合の特定ができないので Issue も上げられていない状況です。気になる方は、GitHub に上げているコードを見てみてください。853行目の+ loss_dur + loss_klを削除すると処理は完了し、そのままだと以下のエラーで止まります (実行には、requirements.txt の torch と torchaudio をコメントし、それぞれの nightly をインストールしてください)。

[rank0]:   File "/Users/handsome/Documents/Python/Style-Bert-VITS2-Mac/.venv/lib/python3.11/site-packages/torch/autograd/graph.py", line 825, in _engine_run_backward
[rank0]:     return Variable._execution_engine.run_backward(  # Calls into the C++ engine to run the backward pass
[rank0]:            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
[rank0]: RuntimeError: view size is not compatible with input tensor's size and stride (at least one dimension spans across two contiguous subspaces). Use .reshape(...) instead.

というわけで Mac ユーザの皆さん、ボクが MPS 化を完成させるのを待つよりも、地道に CPU で学習させた方が速いですよ。

Image by Stable Diffusion

今回のトップ絵は、かなり楽勝で作れたと思います。プロンプトを書き換えたのも 2回くらい。画像のクオリティを上げようと思ってステップ数を上げると急にモニタの向こうに人が増えたりしてゾゾーッとします。不気味の谷とかじゃなくて、普通にホラー映画のやつ。

Date:
2024年10月13日 0:07:02

Model:
realisticVision-v51VAE_original_768x512_cn

Size:
768 x 512

Include in Image:
live video chat streaming event of a popular pop singar. one girl in monitor

Exclude from Image:

Seed:
2850031306

Steps:
25

Guidance Scale:
20.0

Scheduler:
DPM-Solver++

ML Compute Unit:
CPU & GPU

日本語 TTS の最高峰 Style-Bert-VITS2 の学習を mac で実行する

注意!今のところ、macOS での学習は CPU 専用です。

高性能日本語 text-t-speech である Style-Bert-VITS2 ですが、作者様が公開されているバージョンで学習するには NVIDA GPU が必要です。悔しいのでなんとかならないものかとコードをいじっていたら、mac でも学習ができるようになりました。公開時点では、まだ CPU のほんの少しの力しか使えないので非常に遅く、6秒の音声の学習に M2 Max 12 コア CPU で 7分 20秒 5分 11秒かかる状態です。とは言えこれまで macOS では既存モデルを使った音声合成しかできなかったわけですから、個人的には大きな前進だと思っています (試してないですが、他の OS でも動くかもしれません。NVIDIA GPU を持っていない、Windows や Linux の方はお試しの上、結果を教えてください)。

というわけで、とりあえず公開します。

コードと詳細は GitHub へどうぞ

インストール方法と固有の使い方は GitHub でご確認ください。

GitHub: https://github.com/tokyohandsome/Style-Bert-VITS2-Mac/tree/master

PyTorch を知らぬ男がどうやったのか

基本は気合い、そして諦めない気持ち。計算するデバイスをcudaからmpsにする程度でしょ、と思っていたら全然うまくいかず、まずはcpuでとにかく動かそう、と方針転換。エラーが出ては PyTorch 2.3 の公式ドキュメントを読み、Deepseek Coder V2 の力も借りながら一つ一つ潰していく作業。時に自分のひらめきに助けられ、二晩後。無事にトレーニングが終わり、モデル一覧に追加された音声で聞いた「こんにちは、初めまして。あなたの名前はなんていうの?」の美しさたるや。プログレスバーが動き出してから完了するまでに数分待たされたのも、今思えば粋な演出でしたね。ま、ともあれ話を戻すと、コードで実行される処理自体は複雑では無く基本的に上から下へ、なので、一つ一つ CPU で処理するようにしていっただけ、ということになります。

これからどうするか

MPS で動かすのはまだいくつもハードルがありそうなので、CPU をバンバン使って高速化できないのかな、と考えています → 使用できるコア数の半分を使用するようにし、少し高速化しました (v1.0.1)。Whisper を macOS で動かすのは多分 MLX 版が良い (しかない?) と思うのでそこをどうにかしたり、複数の音声ファイルがあればesd.listを自動生成するくらいはできそうですよね。スタイルまで手を出すかどうかはまだわかりません。とりあえずもっと学習を高速化して、モデルの品質を高める作業時間を減らしたいです。

おや?いつの間にか Whisper は pip で入れられる時代なんですね: mlx-whisper

Image by Stable Diffusion

Web UI にいる女子キャラや日本語が得意ということから、日本人の女の子をいくつか描かせたのですが全くイメージと違い、manga 調で競争している女子を描かせても顔が破綻していて多方面から怒られそうだったので、アプローチを全く変えました。深い森からやっと抜け出せた喜びと今の季節感を盛り込んだ、情緒的なトップ絵です。

Date:
2024年10月4日 23:12:07

Model:
realisticVision-v51VAE_original_768x512_cn

Size:
768 x 512

Include in Image:
photo realistic beautiful nature in the late summer. fresh air and sunshine

Exclude from Image:

Seed:
3212833797

Steps:
23

Guidance Scale:
20.0

Scheduler:
DPM-Solver++

ML Compute Unit:
CPU & GPU

Llama3.1 軽量版 8B の日本語能力を高める方法 (Dify の DSL あります)

ローカル LLM で満足のいくレベルの日本語を使うことは大きなチャレンジであり、研究機関や企業、高校生などによって、日々研究開発が行われています。海外の大手企業などが新たな LLM をリリースするたびに、日本語に対応していると正式に謳っているか、実際に使い物になるか、等という情報が LLM 界隈に行き交い、様々なテストが行われます (X とかの SNS をやってないので想像含む)。同じ質問を投げかけてどのような回答があるか、日本語は正しいか、英語や中国語が混じっていないか、回答は正しいか、内容は充実しているか、というあたりはブログなどの結果を見ると判断できますが、実際に使って思うのは、文法や言葉遣いは二の次で、本当に欲しいのは内容の充実度とその応答速度である、というところです。また、長文を翻訳させたいとか、LLM を切り替えるときの読み込み時間がイヤということもあり、ボクがここしばらくチャットで使っているローカル LLM は、llama3.1:8b-instruct-fp16 一本です。実サイズ 16GB なので、32GB しかメモリがない Mac でコンテキスト長を 3万トークンにしても高速で動いてくれます。

日本語能力の高い LLM を調べる

さて、LLM の日本語能力を機械的に多角的に評価され、信頼性が高そうなサイトに「Nejumi LLMリーダーボード3」さん (リンクは下記) があります。更新頻度は高く、商用/オープン・パラメータ数・リリースのタイミング・instruct/chat/text 等のバージョン違いを含めた非常に多くの LLM を評価されているようで、細かい内容まではわかりませんが、とにかく信頼してよさそうな情報量です。

リンク: https://wandb.ai/wandb-japan/llm-leaderboard3/reports/Nejumi-LLM-3–Vmlldzo3OTg2NjM2?accessToken=wpnwc9whr96pxm40dfe4k3xq513f9jc4yhj7q6pnvj4jtayoefbc77qhzbsrztgz

こちらの Nejumi さんでの Llama3.1 の順位はというと、2024/09/28 現在 8位で、オープンなモデルでは Alibaba 社の Qwen 2.5 に次いで 2番目です。すばらしいですね。ただし、パラメータ数は Llama3.1 最上位の 405B なので、ボクの持っている 32GB RAM の Mac では動かせません。8B モデルの順位はというと、次のページの真ん中よりちょっと上あたりにやっと登場。他の選択肢の多さを考えたら選ぶ理由は無い、というレベルです。

でも、個人の感想ですが、実際に使っているとそんなに大きな問題は感じません。元々学習に使ったソースの質が高かったのでしょう、405B を 8B に蒸留してもある程度の質が高い状態で保たれていると感じます。Dify でコンテキスト長を 32,768 トークンにしてもトータルで 22GB に収まるため、32GB RAM でも処理は 100% GPU で行えます (つまり速い。14TPS 以上)。問題となるのは単純な知識の量ですので、必要なら英語で質問すればより多い知識から回答が得られます。そもそも対応言語に日本語が無いんですから、日本語でチャットができても、日本語での知識は乏しくても仕方ないんです。

HuggingFace: https://huggingface.co/meta-llama/Llama-3.1-8B-Instruct

結論、英語で推論してもらえば良い

前置きが長くなりました。というわけで、この記事で紹介するのは、Llama3.1 に英語で推論して日本語で回答してもらう方法です。知識は英語で蓄えているのですから、知識を引き出すところだけ英語にしてもらえば、日本語の回答の精度や内容の質が高まるはずです。他の日本語対応を正式には謳っていない LLM でも同様の手法は有効だと思います。下記する方法を試したところ、プロトタイプ二つを経て、最終的には極めて簡単なプロンプトでほぼ期待したような結果が得られるようになりました。むしろ、20B 以下の少ないパラメータ数の LLM の場合、多言語対応だと浅く広くの知識にならざるを得ないでしょうから、「日本語での会話」がそれなりにでき、知識は英語やらフランス語やらの限られた言語で多く・深く収集された LLM の方が利用価値は高いと思います。

紹介する方法の使い方

Mac で Dify と Ollama で作ったので、同環境でしかテストしていません。ただ、最適化バージョンと、見える化バージョンは、実質システムプロンプトのみなので、LM Studio 等の System Prompt を設定できる AI ツールを使っても簡単に利用できると思います。Ollama 単体でも template を書き換えるか、毎回質問するときに記入すれば同様のことは実現できるかもしれません。

以下に Dify 用 DSL と System プロンプトを貼っておきます。Dify なら適当な名前.ymlとしてファイルに保存してから「DSL をインポート」で読み込んでください。LM Studio 等のツールで試すなら、DSL の後に貼ってある System prompt の内容をコピペしてください。LLM にはモデルプロバイダ Ollama でダウンロードした llama3.1:8b-instruct-fp16 を使用しています。Dify で別のものを使用する際には適宜変更してください (使用する LLM によっては期待した効果は得られない可能性があります)。

効果を見るには、System prompt を何も指定していない (デフォルト) の状態と、最適化したものを使った場合とで出力を比較してみてください。日本人なら当然知っているであろうこと (「漫画家鳥山明の代表作は」等) も日本語で聞くとハルシネーションを起こしがちですが、英語で推論されると正しい答えが得られるケースが多いです。また、海外から日本を見た第三者的解釈による回答が得られやすいのも良い側面だと思います。対して難点としては、英訳・和訳の処理が挟まるため微妙なニュアンスが変わってしまったり、固有名詞が間違った漢字やローマ時表記になることがあります。

まずは評価を簡単にしたかったので、回答内容に大きなブレや遊びが出ないように、Temperature と Top_P のそれぞれを 0.2 にしています。もっと厳密に評価・比較する場合は数値を下げたり、実際に運用フェーズで多様な生成を行いたいという場合には 1に近い値を採用するなど、目的に応じて工夫して使ってください。

また、Size of context window は 32GB RAM Mac で最大になるように32768にしてしまっています。GPU 使用率がマックスにならない場合はこの値が大きすぎる可能性があるので、チェックを外すなり小さな値にするなり、使用する LLM やご自身の RAM サイズに合わせて調整してください。参考:

日本語能力を高めたチャットボットの紹介

適当な名前.ymlとして保存した DSL を Dify の「DSL ファイルをインポート」でインポートすると現れるチャットボット

最適化バージョン

最終バージョンです。日本語で質問すると、内部的に英語で推論し、日本語で回答してくれます。一行のシステムプロンプトのみなので、レスポンスタイムに影響はありません。

★ Dify 用 DSL (クリックして表示する) ★:
app:
  description: Llama3.1 の持つ豊富な英語の知識を日本語で回答してくれる。日本語の固有名詞の変換が苦手なところは、日本語ペラペラなアメリカ人インテリも漢字はニガテ、みたいでかわいい
  icon: male-student
  icon_background: '#FFE4E8'
  mode: advanced-chat
  name: Llama3.1 最適化 (脳内処理バージョン)
  use_icon_as_answer_icon: true
kind: app
version: 0.1.1
workflow:
  conversation_variables: []
  environment_variables: []
  features:
    file_upload:
      image:
        enabled: false
        number_limits: 3
        transfer_methods:
        - local_file
        - remote_url
    opening_statement: ''
    retriever_resource:
      enabled: false
    sensitive_word_avoidance:
      enabled: false
    speech_to_text:
      enabled: false
    suggested_questions: []
    suggested_questions_after_answer:
      enabled: true
    text_to_speech:
      enabled: false
      language: ''
      voice: ''
  graph:
    edges:
    - data:
        sourceType: start
        targetType: llm
      id: 1727272665783-llm
      source: '1727272665783'
      sourceHandle: source
      target: llm
      targetHandle: target
      type: custom
    - data:
        sourceType: llm
        targetType: answer
      id: llm-answer
      source: llm
      sourceHandle: source
      target: answer
      targetHandle: target
      type: custom
    nodes:
    - data:
        desc: ''
        selected: false
        title: 開始
        type: start
        variables: []
      height: 54
      id: '1727272665783'
      position:
        x: 80
        y: 282
      positionAbsolute:
        x: 80
        y: 282
      sourcePosition: right
      targetPosition: left
      type: custom
      width: 244
    - data:
        context:
          enabled: false
          variable_selector: []
        desc: ''
        memory:
          role_prefix:
            assistant: ''
            user: ''
          window:
            enabled: false
            size: 10
        model:
          completion_params:
            keep_alive: 30m
            num_ctx: 32768
            temperature: 0.2
            top_p: 0.2
          mode: chat
          name: llama3.1:8b-instruct-fp16
          provider: ollama
        prompt_template:
        - id: d80ef4de-35f3-4106-87af-ff57023b2649
          role: system
          text: Infer question in English, generate rich answer, and output in the
            language used to ask.
        selected: true
        title: LLM
        type: llm
        variables: []
        vision:
          enabled: false
      height: 98
      id: llm
      position:
        x: 379
        y: 282
      positionAbsolute:
        x: 379
        y: 282
      selected: true
      sourcePosition: right
      targetPosition: left
      type: custom
      width: 244
    - data:
        answer: '{{#llm.text#}}'
        desc: ''
        selected: false
        title: 回答
        type: answer
        variables: []
      height: 107
      id: answer
      position:
        x: 680
        y: 282
      positionAbsolute:
        x: 680
        y: 282
      sourcePosition: right
      targetPosition: left
      type: custom
      width: 244
    viewport:
      x: -111
      y: 197
      zoom: 1

System prompt (LM Studio 等で使用する場合はこちらをどうぞ):

Infer question in English, generate rich answer, and output in the language used to ask.

見える化バージョン

質問の英訳、英語での推論、最終的な和訳、の全てが表示されるバージョンです。それぞれの処理は一つの LLM ブロックで実施しています。上の最終バージョンでは本当に英語で推論したのかわかりませんが、こちらは途中経過もはっきり見て取れます。英語の勉強にも使えるかもしれません。

★ Dify 用 DSL (クリックして表示する) ★:
app:
  description: Llama3.1 が、質問を英訳し、推論し、日本語で返す、全てのプロセスを透明化したバージョン。冗長だが、英語の知識を使って回答していることがわかる。英語の勉強にもなるかも?
  icon: two_men_holding_hands
  icon_background: '#E4FBCC'
  mode: advanced-chat
  name: Llama3.1 英語で推論 (見える化バージョン)
  use_icon_as_answer_icon: true
kind: app
version: 0.1.1
workflow:
  conversation_variables: []
  environment_variables: []
  features:
    file_upload:
      image:
        enabled: false
        number_limits: 3
        transfer_methods:
        - local_file
        - remote_url
    opening_statement: ''
    retriever_resource:
      enabled: false
    sensitive_word_avoidance:
      enabled: false
    speech_to_text:
      enabled: false
    suggested_questions: []
    suggested_questions_after_answer:
      enabled: true
    text_to_speech:
      enabled: false
      language: ''
      voice: ''
  graph:
    edges:
    - data:
        sourceType: start
        targetType: llm
      id: 1727270833994-llm
      source: '1727270833994'
      sourceHandle: source
      target: llm
      targetHandle: target
      type: custom
    - data:
        sourceType: llm
        targetType: answer
      id: llm-answer
      source: llm
      sourceHandle: source
      target: answer
      targetHandle: target
      type: custom
    nodes:
    - data:
        desc: ''
        selected: false
        title: 開始
        type: start
        variables: []
      height: 54
      id: '1727270833994'
      position:
        x: 80
        y: 282
      positionAbsolute:
        x: 80
        y: 282
      sourcePosition: right
      targetPosition: left
      type: custom
      width: 244
    - data:
        context:
          enabled: false
          variable_selector: []
        desc: ''
        memory:
          role_prefix:
            assistant: ''
            user: ''
          window:
            enabled: false
            size: 10
        model:
          completion_params:
            keep_alive: 30m
            num_ctx: 32768
            temperature: 0.2
            top_p: 0.2
          mode: chat
          name: llama3.1:8b-instruct-fp16
          provider: ollama
        prompt_template:
        - id: 342e3642-d8b5-42c8-b003-7816a8ec7f3a
          role: system
          text: 'You are a skilled AI translator. Since your knowledge is best in
            English, translate any question into English for inference and generate
            answer. Follow the steps described below.


            ### Steps:

            1. You translate {{#sys.query#}}directly into English. Try maintaining
            the original format without omitting or adding any information.

            2. Generate response in English.

            3. Translate the response literary back into the language originally used
            by the user and output.'
        selected: true
        title: LLM
        type: llm
        variables: []
        vision:
          enabled: false
      height: 98
      id: llm
      position:
        x: 381
        y: 282
      positionAbsolute:
        x: 381
        y: 282
      selected: true
      sourcePosition: right
      targetPosition: left
      type: custom
      width: 244
    - data:
        answer: '{{#llm.text#}}'
        desc: ''
        selected: false
        title: 回答
        type: answer
        variables: []
      height: 107
      id: answer
      position:
        x: 680
        y: 282
      positionAbsolute:
        x: 680
        y: 282
      sourcePosition: right
      targetPosition: left
      type: custom
      width: 244
    viewport:
      x: -205
      y: 134
      zoom: 1

System prompt (LM Studio 等で使用する場合はこちらをどうぞ):

You are a skilled AI translator. Since your knowledge is best in English, translate any question into English for inference and generate answer. Follow the steps described below.

### Steps:
1. You translate the query directly into English. Try maintaining the original format without omitting or adding any information.
2. Generate response in English.
3. Translate the response literary back into the language originally used by the user and output.

ステップバイステップバージョン

質問の英訳、英語での推論、最終的な和訳、のそれぞれを個別の LLM ブロックで実行しています。最終的にユーザに返ってくるのは和訳されたものだけなので余計な情報は含まれませんが、ブロック毎にどういう処理をしたのかが調べられるのでデバッグ向きです (最初に作った PoC バージョン)。回答の質は高い傾向があります。これが最終バージョンでは無いのは、3回LLM が動くので単純に時間がかかるからです。Dify のフローを利用しているため、こちらに限っては LM Studio にシステムプロンプトに指示を投げるという使い方では実現できません。

★ Dify 用 DSL (クリックして表示する) ★:
app:
  description: 質問の英訳、推論、和訳、と 3つのパートそれぞれに Llama3.1 を使用したバージョン。無駄に時間がかかる、PoC 版
  icon: family
  icon_background: '#FEF7C3'
  mode: advanced-chat
  name: Llama3.1 開発途上 (3倍労力バージョン)
  use_icon_as_answer_icon: true
kind: app
version: 0.1.1
workflow:
  conversation_variables: []
  environment_variables: []
  features:
    file_upload:
      image:
        enabled: false
        number_limits: 3
        transfer_methods:
        - local_file
        - remote_url
    opening_statement: ''
    retriever_resource:
      enabled: false
    sensitive_word_avoidance:
      enabled: false
    speech_to_text:
      enabled: false
    suggested_questions: []
    suggested_questions_after_answer:
      enabled: true
    text_to_speech:
      enabled: false
      language: ''
      voice: ''
  graph:
    edges:
    - data:
        isInIteration: false
        sourceType: start
        targetType: llm
      id: 1726927256338-source-1727251929525-target
      source: '1726927256338'
      sourceHandle: source
      target: '1727251929525'
      targetHandle: target
      type: custom
      zIndex: 0
    - data:
        isInIteration: false
        sourceType: llm
        targetType: answer
      id: 1727252264986-source-answer-target
      source: '1727252264986'
      sourceHandle: source
      target: answer
      targetHandle: target
      type: custom
      zIndex: 0
    - data:
        isInIteration: false
        sourceType: llm
        targetType: llm
      id: 1727251929525-source-1727252519406-target
      source: '1727251929525'
      sourceHandle: source
      target: '1727252519406'
      targetHandle: target
      type: custom
      zIndex: 0
    - data:
        isInIteration: false
        sourceType: llm
        targetType: llm
      id: 1727252519406-source-1727252264986-target
      source: '1727252519406'
      sourceHandle: source
      target: '1727252264986'
      targetHandle: target
      type: custom
      zIndex: 0
    nodes:
    - data:
        desc: ''
        selected: false
        title: 開始
        type: start
        variables: []
      height: 54
      id: '1726927256338'
      position:
        x: 80
        y: 282
      positionAbsolute:
        x: 80
        y: 282
      sourcePosition: right
      targetPosition: left
      type: custom
      width: 244
    - data:
        answer: '{{#1727252264986.text#}}'
        desc: ''
        selected: false
        title: 回答
        type: answer
        variables: []
      height: 107
      id: answer
      position:
        x: 1278.680492089227
        y: 282
      positionAbsolute:
        x: 1278.680492089227
        y: 282
      selected: false
      sourcePosition: right
      targetPosition: left
      type: custom
      width: 244
    - data:
        context:
          enabled: true
          variable_selector:
          - sys
          - query
        desc: ''
        memory:
          query_prompt_template: ''
          role_prefix:
            assistant: ''
            user: ''
          window:
            enabled: false
            size: 50
        model:
          completion_params:
            keep_alive: 30m
            num_ctx: 32768
            temperature: 0.2
            top_p: 0.2
          mode: chat
          name: llama3.1:8b-instruct-fp16
          provider: ollama
        prompt_template:
        - id: 7e4cffec-808f-4f8b-968d-945955648c2b
          role: system
          text: You are a skilled translator in English. You translate {{#sys.query#}}directly
            into English so the entire output can be used as a replacement of the
            original text. Based on the content, maintaining the original format without
            omitting or adding any information.
        - id: d152eb91-5a38-4d31-97d9-c57c4b18ed51
          role: user
          text: '{{#context#}}'
        selected: false
        title: Translation to English
        type: llm
        variables: []
        vision:
          enabled: false
      height: 98
      id: '1727251929525'
      position:
        x: 380
        y: 275.68049208922713
      positionAbsolute:
        x: 380
        y: 275.68049208922713
      selected: false
      sourcePosition: right
      targetPosition: left
      type: custom
      width: 244
    - data:
        context:
          enabled: false
          variable_selector: []
        desc: ''
        memory:
          query_prompt_template: ''
          role_prefix:
            assistant: ''
            user: ''
          window:
            enabled: false
            size: 50
        model:
          completion_params:
            keep_alive: 30m
            num_ctx: 32768
            temperature: 0.2
            top_p: 0.2
          mode: chat
          name: llama3.1:8b-instruct-fp16
          provider: ollama
        prompt_template:
        - id: 9e13c0fb-e680-48da-8270-2d8ed2f8b676
          role: system
          text: You are a skilled translator in Japanese. You translate {{#1727252519406.text#}}directly
            into Japanese so the entire output can be used as a replacement of the
            original text. Based on the content, maintaining the original format without
            omitting or adding any information.
        - id: be046c8d-1d48-47f4-91d0-1c6bb7fc3909
          role: user
          text: '{{#1727252519406.text#}}'
        selected: true
        title: translation to japanese
        type: llm
        variables: []
        vision:
          enabled: false
      height: 98
      id: '1727252264986'
      position:
        x: 980
        y: 282
      positionAbsolute:
        x: 980
        y: 282
      selected: true
      sourcePosition: right
      targetPosition: left
      type: custom
      width: 244
    - data:
        context:
          enabled: false
          variable_selector: []
        desc: ''
        memory:
          query_prompt_template: ''
          role_prefix:
            assistant: ''
            user: ''
          window:
            enabled: false
            size: 50
        model:
          completion_params:
            keep_alive: 30m
            num_ctx: 32768
            temperature: 0.2
            top_p: 0.2
          mode: chat
          name: llama3.1:8b-instruct-fp16
          provider: ollama
        prompt_template:
        - id: 5a722de8-e6f9-48df-81cb-86cfa183f447
          role: system
          text: Generate rich answer in English.
        - id: a3587e3e-84f3-41c9-a45d-ea5fb8392c73
          role: user
          text: '{{#1727251929525.text#}}'
        selected: false
        title: inference and generation
        type: llm
        variables: []
        vision:
          enabled: false
      height: 98
      id: '1727252519406'
      position:
        x: 684
        y: 275.68049208922713
      positionAbsolute:
        x: 684
        y: 275.68049208922713
      selected: false
      sourcePosition: right
      targetPosition: left
      type: custom
      width: 244
    viewport:
      x: -270.21689933279276
      y: 120.80412637177352
      zoom: 0.8083334534373365

まとめ

プロンプトには改善の余地はあるでしょうし、対象とする LLM によって最適な方法も変わると思います。ただいずれにせよ、英語の情報が豊富な代わりに日本語で学習した情報が少ないという LLM には有効な手法だと思います。すぐに試せるはずなので、具体的な実行例は載せませんでした。そのまま使わなくても、アイディアは流用できるでしょう。ぜひお気に入りの LLM の知識を最大限に活用できる方法を生み出してください。そして、ぜひボクや世の中に共有してください。

(蛇足) センシティブな情報と情報発信

アメリカの大手企業が作った LLM である Llama (Meta 社) や Phi (Microsoft 社)、Gemma (Google 社) は、ローカルにダウンロードして利用できるオープンソース・オープンウェイトではありますが、それぞれの企業のコンプライアンスポリシーに従ってセンシティブな情報は回答してくれません。つまり、エロいこと、差別的なこと、暴力的なこと、犯罪に関わること、等を答えさせようとしても、ことごとく、かたくなに、徹底的に拒否されてしまいます。ところが、中国産の LLM は大手がリリースしたものであっても制限がゆるいようです。特にパラメータ数の小さいモデルほど、質問の仕方によってはかなりの部分まで回答してくれます。例えば Nejumi さんのリーダーボードで 20位以内に入っている Qwen 2.5 14B Instruct はかなり寛容な感じです。

これは、「危ないことを教えてくれるぜ、えへへ」ということではなく、どのような LLM を使ってどのような文章 (絵や映像、音声も同じです) が得られたとしても、それを使用して外部に何かを発信する前には注意が必要、ということです。注意すべきは LLM 自体のライセンスだけではありません。文法、誤字脱字、ファクトチェックも重要ですが、誰かを傷つけたり、権利を侵害するようなことが無いよう、十分気をつけましょう。

本ブログでは LLM が生成した文章をそのまま注釈無しで使用しないようにしていますが、何か不備がございました、ご指摘いただけると幸いです。

また、気に入った記事には文末のイイネボタンをクリックしていただけるとうれしいです。

Image by Stable Diffusion

アメリカ人大学教授が寿司の作り方を教えている授業の風景を想定してお願いしてみました。

Date:
2024年9月29日 0:21:23

Model:
realisticVision-v51VAE_original_768x512_cn

Size:
768 x 512

Include in Image:
white male american college professor in formal suit teaching how to make sushi

Exclude from Image:

Seed:
1340386485

Steps:
20

Guidance Scale:
20.0

Scheduler:
DPM-Solver++

ML Compute Unit:
CPU & GPU

【Mac 専用】OpenVoice で音声クローン

音声クローン技術の OpenVoice を Mac にインストールして使ってみました。作業を始めてから実際に使えるようになるまで結構大変だったので、やり方をまとめます。また、OpenVoice V2 は日本語ネイティブ対応ということですが、TTS (Text-To-Speech) に使われている MeloTTS の日本語能力がよろしくないので、macOS に標準搭載されている say コマンドで生成した音声を元にクローン音声を生成するスクリプトを書きました。そちらはなかなかの成果が出たので、合わせて紹介します。

OpenVoice (オフィシャル GitHub): https://github.com/myshell-ai/OpenVoice/

Weel さんの紹介記事 (こちらで知りました): https://weel.co.jp/media/tech/openvoice

注意点

OpenVoice v2 は MIT ライセンスで公開されているので、ソースコードも生成された音声も商用利用可能です。ただし、クローン元となる音声を利用する権利を有していない場合は、商用か否かにかかわらず生成された音声を公開する権利はありません。現在の法律がどうであれ、AI の進歩と共に法整備も進んでいくはずです。声やしゃべりで生計を立てている方々もいる以上、許可無く誰かの音声を使用することは何らかの形で罰せられる可能性があります。そのことを理解した上で本記事を読み進めてください。また、ご自身の音声データを公開する場合においても、ライセンスの明記は行うようにしましょう。

免責事項:
このサイトで提供される情報や方法を使用して行われた行為については、私たちは一切の責任を負いません。特に、他人の無断使用または違法な手段によって得られた音声データを使用して音声クローンを作成し、公開することは完全に利用者の自己責任であり、その結果として生じるあらゆる問題やトラブルについても当方は何ら関知せず、一切の責任を負わないものとします。

法律により保護された著作権やパブリシティ権等が侵害される可能性のある行為を行う際には、利用者は事前に適切な法的措置を講じ、自己のリスクにおいて行動するものとし、当方はその結果生じる一切の損害について責任を負わないものとします。

なお、この免責事項は日本国の法律に基づいて解釈されるものとし、適用されるべき法域については当事者の本拠地が所在する国または地域の法律ではなく、日本国の法律を選択的に適用することに同意するものとします。

以上のご理解とご了承をいただきますようお願い申し上げます。

環境

動作確認ができた環境

macOS: 14.5
ffmpeg version 7.0.1
pip version 24.2

環境構築手順

入っていなければ、ffmpeg をインストール (もちろん brew が必要です):

brew update
brew install ffmpeg

ディレクトリを作って OpenVoice リポジトリをクローン。ディレクトリ名はお好きにどうぞ:

mkdir OpenVoice
cd OpenVoice
git clone https://github.com/myshell-ai/OpenVoice.git .

仮想環境を構築して入る。ボクは得意のpipenvですが、これもお好きなのをお使いください。Python は 3.9 必須らしいです:

pipenv --python 3.9
pipenv shell

pipを最新にし、torchchardet、そして OpenVoice をインストール:

python -m pip install --upgrade pip
pip install torch chardet
pip install -e .

OpenVoice V2 をダウンロードして展開:

※ チェックポイントの最新版 (unzip の対象) は、https://github.com/myshell-ai/OpenVoice/blob/main/docs/USAGE.md の Download the checkpoint from here and extract it to the checkpoints_v2 folder. で確認のこと!

wget https://myshell-public-repo-host.s3.amazonaws.com/openvoice/checkpoints_v2_0417.zip
unzip checkpoints_v2_0417.zip
rm checkpoints_v2_0417.zip

以下に従ってファイル openvoice/se_extractor.py を編集 (22行目のcudacpuにし、float16float32に変更):

[[[IMPORTANT]]] modify OpenVoice source for Apple Silicon Mac: https://github.com/reid-prismatic/OpenVoice-Scribe/commit/f681f5bcbc18df3f356953928a78ba6dcff9de99

def split_audio_whisper(audio_path, audio_name, target_dir='processed'):
    global model
    if model is None:
        model = WhisperModel(model_size, device="cpu", compute_type="float32")
    audio = AudioSegment.from_file(audio_path)
    max_len = len(audio)

(ここは必要な場合のみ) OpenVoice V2 オフィシャルでは、音声生成 (TTS、Text-To-Speech) に MeloTTS を使用しているため、別途インストールする手順が示されています。個人的に日本語の能力は不十分だと思いますが、気になる方、オフィシャルの方法でまずは進めたい方は以下も実施してください:

git clone https://github.com/myshell-ai/MeloTTS
cd MeloTTS
pip install -e .
python -m unidic download

(MeloTTS のテストをしたい場合のみ) 以下にテスト用のサンプルコードを貼っておきます (★ MeloTTS テストコード ★をクリックするとコードが開きます)。python japanese.py で実行し、生成された jp.wav をファインダで QuickLook もしくは Terminal からafplay jp.wav で再生できます。テキストの文面は上で紹介した Weel さんの記事の一部ですが、アルファベット表記の “OpenVoice” がガン無視されたり、音引きが部分的にスルーされたり、読点で溜めが無かったり、なんとなく中国語っぽい癖があったり、という感じで、実用するにはきびしいです。ちなみに初回実行時はファイルのダウンロードが発生するので時間がかかります。

★ MeloTTS テストコード ★
from melo.api import TTS
import time

# Speed is adjustable
speed = 1.0
device = 'cpu' # 'mps' にしても動くが、速くはならない

start = time.time()

text = """
OpenVoiceは、正確なトーンカラーのクローニング、柔軟な声のスタイル制御、ゼロショット多言語クローニングを可能にする音声クローニング技術です。

OpenVoiceを使用すると、リファレンス スピーカーの音色を複製するだけでなく、感情、アクセント、リズム、ポーズ、イントネーションなどの音声スタイルをきめ細かく制御できます。

これを使用するとこのような音声が生成できます。
"""
model = TTS(language='JP', device=device)
speaker_ids = model.hps.data.spk2id

output_path = 'jp.wav'
model.tts_to_file(text, speaker_ids['JP'], output_path, speed=speed)

print(f'\nかかった時間: {round(time.time()-start, 2)} 秒\n')

OpenVoice を使う

クローンしたい音声を準備

環境の準備ができたら、クローンしたい音声ファイルを準備します。なるべく高品質で、他の人の声や雑音、BGM 等が入っていない、10秒以上の音声がよさそうです。参考まで、ffmpegで MP4 の映像ファイルから MP3 の音声を書き出すなら、こんな感じで行えます:

ffmpeg -i input.mp4 -vn -acodec libmp3lame myvoice.mp3

OpenVoice/resources フォルダに入れておきます (ファイル名 myvoice.mp3はサンプルコードで使用しているので、そのまま使いましょう):

cp myvoice.mp3 resources

まずはオフィシャルのテストコードで試す (MeloTTS 必須)

オフィシャルのサンプルコード (demo_part3.ipynb GitHub で見るならこちら) を適当にいじって日本語だけ生成するようにし、かかった時間を表示するようにしたものがこちらです。上の手順で MeloTTS をダウンロードした場合はこれでクローンのテストが行えます。OpenVoice ディレクトリ直下においてpython test.pyで実行します:

import os
import torch
from openvoice import se_extractor
from openvoice.api import ToneColorConverter
from melo.api import TTS
import time

home_dir = './'
ckpt_converter = home_dir + 'checkpoints_v2/converter'
device = "cuda:0" if torch.cuda.is_available() else "cpu"
output_dir = 'outputs'

start = time.time()

tone_color_converter = ToneColorConverter(f'{ckpt_converter}/config.json', device=device)
tone_color_converter.load_ckpt(f'{ckpt_converter}/checkpoint.pth')

os.makedirs(output_dir, exist_ok=True)

reference_speaker = home_dir + 'resources/myvoice.mp3' # This is the voice you want to clone
target_se, audio_name = se_extractor.get_se(reference_speaker, tone_color_converter, vad=False)

texts = {
    'JP': "彼は毎朝ジョギングをして体を健康に保っています。"
}

src_path = f'{output_dir}/tmp.wav'

# Speed is adjustable
speed = 1.0

for language, text in texts.items():
    model = TTS(language=language, device=device)
    speaker_ids = model.hps.data.spk2id

    for speaker_key in speaker_ids.keys():
        speaker_id = speaker_ids[speaker_key]
        speaker_key = speaker_key.lower().replace('_', '-')

        source_se = torch.load(f'checkpoints_v2/base_speakers/ses/{speaker_key}.pth', map_location=device)
        model.tts_to_file(text, speaker_id, src_path, speed=speed)
        save_path = f'{output_dir}/output_v2_{speaker_key}.wav'

        # Run the tone color converter
        encode_message = "@MyShell"
        tone_color_converter.convert(
            audio_src_path=src_path,
            src_se=source_se,
            tgt_se=target_se,
            output_path=save_path,
            message=encode_message)
 
print(f'\nかかった時間: {round(time.time()-start, 2)} 秒\n')

生成された音声は outputs に保存されます。afplay outputs/output_v2_jp.wav で再生しましょう。サンプルの文章がちょうど良いのか悪くない感じがするものの、文章をいろいろ変えて試すと結構きびしいことがわかってきます。でもこれって、OpenVoice の問題じゃないんです。

OpenVoice はボイスチェンジャーである

outputs ディレクトリには output_v2_jp.wav 以外に、tmp.wav ファイルがあるのがわかります。実はこれは MeloTTS で作られた、ベースとなる音声ファイルです。OpenVoice はその音声を、ユーザが用意した音声の特徴で置き換える、つまり乱暴に言うとボイスチェンジャー的なことをしています。音楽で言えばピアノで弾いた曲の音色をギターに変えるようなことをしているので、ベースのアクセントを踏襲しますし、他の言語でもしゃべれてしまう、というわけです。

ええぃ、MeloTTS はキツい!Siri さん、お願いします!

我らが macOS にはいにしえ (Mac OS 9?) より音声合成技術が備わっております。Siri さんなんかは iPhone の普及も相まって、なかなかに鍛わっています。面倒なインストール作業も必要なく、ファイルへの書き出しにも対応しています。というわけでボクはあまり使えない MeloTTS を捨て、音声合成部分に macOS 標準搭載のsayコマンドを使用することにしました。簡単なオプションも追加したスクリプトを書いたので紹介します。名付けて clonesay (クローンせい!) です。やかましいですね。

python clonesay.py でとりあえず動きます (-hを付けると、利用できるオプションの一覧を表示します)。macOS で指定されている音声で、テスト用に入れているデフォルトのテキストを音声合成し、myvoice.mp3 の声色にクローンして outputs/output_jp.mp3 として書き出してくれます。実行中にはごちゃごちゃと英文が表示されますが、最終的に「かかった時間」が表示されればおそらく問題無いはずです。

import os
import torch
from openvoice import se_extractor
from openvoice.api import ToneColorConverter
import time
import subprocess
import argparse

# macOS の 'say' コマンドを使用して、ベースとなる音声ファイルを生成
def text_to_speech(text, output_file):
    command = ['say', '-o', output_file, text]
    subprocess.run(command)

# 引数を解析するための設定
parser = argparse.ArgumentParser(description='OpenVoice を利用して音声のクローニングを行います。事前にクローンしたい音声ファイルを resources/myvoice.mp3 \
                                 として保存してから実行してください。macOS の "say" コマンドを利用しています。\
                                 https://github.com/myshell-ai/OpenVoice/')
parser.add_argument('-i', '--input', type=str, required=False, help='しゃべらせたいテキストを直接入力します。例 -i "こんにちは、おげんきですか"')
parser.add_argument('-f', '--file', type=str, required=False, help='しゃべらせたいテキストが入力されたファイルを指定します。同時に指定した場合は -i が優先されます。')
parser.add_argument('-s', '--source', type=str, required=False, help='テキストから音声を生成せずに、指定した既存の音声ファイルを元にクローンを作成します。\
                    同時に指定した場合は -i、-f が優先されます。')
parser.add_argument('-o', '--output', type=str, required=False, help='出力するファイル名を指定します。利用できるフォーマット: mp3(デフォルト), aiff, wav.')
parser.add_argument('-r', '--reference', type=str, required=False, help='クローン元となる音声ファイルを指定します。\
                    初回指定時には必要なデータが processed ディレクトリに作成されます。')
parser.add_argument('-p', '--play', action='store_true', required=False, help='生成された音声を自動再生します。')
args = parser.parse_args()

home_dir = './'
device = 'cpu'
ckpt_converter = home_dir + 'checkpoints_v2/converter'
tone_color_converter = ToneColorConverter(f'{ckpt_converter}/config.json', device=device)
tone_color_converter.load_ckpt(f'{ckpt_converter}/checkpoint.pth')
output_dir = 'outputs'
language = 'jp'
src_path = f'{output_dir}/tmp.aiff'

os.makedirs(output_dir, exist_ok=True)
start = time.time()

# クローン元となる音声ファイルの指定
if args.reference:
    reference_speaker = args.reference
else:
    reference_speaker = home_dir + 'resources/myvoice.mp3' # クローン元となる音声ファイル

target_se, audio_name = se_extractor.get_se(reference_speaker, tone_color_converter, vad=False)

# 発声するテキストを指定もしくは、既存の音声ファイルを指定
if args.input:
    input_text = args.input
#elif os.path.isfile(args.file) and os.path.getsize(args.file) > 0:
elif args.file and os.path.isfile(args.file):
    with open(args.file, 'r', encoding='utf-8') as file:
        input_text = file.read()
elif args.source:
    src_path = args.source
    input_text = ''
else:
    input_text = """
    しゃべらせたい文章を、ダッシュアイで指定してください。その他利用可能なオプションは、ダッシュエイチで確認できます。
    """

# 発声するテキストの元となる音声ファイルを生成
if input_text != '':
    text_to_speech(input_text, src_path)

source_se = torch.load(f'checkpoints_v2/base_speakers/ses/{language}.pth', map_location=device)
save_path = f'{output_dir}/output_{language}.mp3' # デフォルトの出力ファイル

# 出力ファイル名の指定がある場合
if args.output:
    output_extension = os.path.splitext(args.output)[1].lower()
    if output_extension in {'.mp3', '.aiff', '.wav'}:
        save_path = args.output
    else:
        save_path = os.path.splitext(args.output)[0] + '.aiff'
else:
    save_path = f'{output_dir}/output_{language}.mp3'

# OpenVoice processing part
# Run the tone color converter
encode_message = "@MyShell"
tone_color_converter.convert(
    audio_src_path=src_path,
    src_se=source_se,
    tgt_se=target_se,
    output_path=save_path,
    message=encode_message)
 
print(f'\nかかった時間: {round(time.time()-start, 2)} 秒\n')

# Play the generated MP3 file using the default media player
if args.play and os.path.isfile(save_path):
    # Use afplay for macOS, which is a simple command-line audio player
    print(f'ファイル: {save_path}')
    if input_text !="":
        print(input_text)
    subprocess.run(['afplay', save_path])
else:
    print("The output file does not exist.")

簡単にいろいろ試すなら、-iオプションでテキストを入力し、-pで生成後に再生するようにするのが良いと思います。例えばこんな感じ:

python clonesay.py -p -i "本日皆さんに集まっていただいたのは、\
新しいエーアイ活用の方法を議論するのが目的です。どうぞ、よろしくお願いします。"

(“AI” は “エーアイ” にするなど、工夫は必要です)

macOS の音声を変えてみる

多分 macOS デフォルトの音声は Siri (声 2) ですが、さすがに男性の声に寄せるのはきびしいと思います。そのような場合には、システム設定で変更しましょう。コマンドでのやり方はみつかりませんでした (声の管理… から、追加もできるようです)。

システム設定… → アクセシビリティ → 読み上げコンテンツ → システムの声

感想

AI による音声のクローニング手法はいくつかあるようで、以前 GPT-SoVITS というのをいじったときには結構衝撃的な結果でした。数秒の音声からかなり高い再現度だったからです。今回はたまたま Weel さんで見つけた OpenVoice を試したわけですが、正直クローニングのクオリティとしては劣っていると感じています。ベースの音声 (Siri さん) に寄りすぎだな、と。ただ、今後ベースの TTS (Text-To-Speech) の性能がとんでもなく高く、自分がクローンしたい声質に近いものが現れたときには TTS だけ乗り換えれば良いわけで、将来性はあるのかもしれない、とも思っています。また、OpenVoice は TTS に頼らない技術です。なので、ボクのサンプルスクリプトの-sオプションでリアルの人物がしゃべった音声ファイルをソースとして指定すれば、ナチュラルな結果が得られるでしょう。何か使い道はありそうですよね。mac ユーザの方は遊んでみてください。

Image by Stable Diffusion (Mochi Diffusion)

今回の画像は何のひねりも無いプロンプトで描いてもらいました。ステップ数が高ければキレイになったりリアルになったりするわけでも無いんですよね。ランダムでしばらく回し、めぼしい Seed 値が見つかった後は何度か Steps の増減を繰り返し、指と女性の顔が一番ブキミじゃなかった、マシだった、という一枚に決めました。

Date:
2024年9月23日 23:14:52

Model:
realisticVision-v51VAE_original_768x512_cn

Size:
768 x 512

Include in Image:
human voice clone technology

Exclude from Image:

Seed:
85304328

Steps:
25

Guidance Scale:
20.0

Scheduler:
DPM-Solver++

ML Compute Unit:
All

© Peddals.com