GarethNg

Gareth Ng

With a bamboo staff and straw sandals, I feel lighter than riding a horse, In a cloak amidst the misty rain, I live my life as it comes.
github
email
x
telegram

Mac 用 LM studio でローカル大モデル(DeepSeek/Qwen)をデプロイ + 翻訳

得益于 Mac の CPU と GPU の共有メモリ、そして大きなメモリ帯域幅により、macBook でローカルの大モデルを実行することが可能になりました。最近の DeepSeek の人気に乗じて、私もローカルで AI 翻訳システムを構築してみました。本記事では、Mac コンピュータ上でこのシステムを正しく設定する方法を紹介します。設定が完了すると、あなたは以下のことができるようになります。

  • Mac 上で無料で大規模言語モデルを使用して対話する
  • サーバーの応答を待つ必要がなく、効率を向上させる
  • 任意の文書、スクリーンショット、ウェブページなどを迅速に翻訳する

本記事では macBook を例に挙げますが、理論的には Windows コンピュータでも同様の効果が得られますので、参考までにどうぞ。

本記事のダウンロードツールは中国本土ではネットワークの問題に直面する可能性がありますので、自分で解決してください。

モデル管理ツール#

モデル管理ツールとは、ローカルで大モデルを管理し、サーバー機能を提供できるツールのことです。これにより、不要な手間を省くことができます。また、モデル管理ツールはローカルチャット機能も提供しているため、ネットワークが不安定な場合でも最近人気の DeepSeek を使用することができます。

比較的人気のあるモデル管理ツールには OllamaLM Studio があります。この 2 つのツールを比較すると、LM Studio には GUI ページがあり、モデルのダウンロードもより便利で、新人に優しいです。したがって、本記事では LM Studio を使用します。

モデルのインストール#

適切なモデルを見つけることがすべての始まりの鍵です。現在人気のあるオープンソースの大モデルには deepseek-r1、Qwen、LLama などがありますので、必要に応じてお好きなものを選んでください。著者は中国語 - 英語翻訳を使用する必要があるため、中国語に優しい deepseek と Qwen(千問)を選びました。

次に、自分の Mac の構成に基づいて適切なモデルサイズを選択します。LM Studio は現在の構成でダウンロードできないモデルを禁止します。もちろん、使用可能であっても、使い心地には一定の違いがあります。

著者の構成は MacBook Pro M3 Max 36G メモリで、テスト中に 32B サイズの DeepSeek R1 は正常に使用できましたが、実行速度は比較的遅く、簡単な対話には問題ありませんが、翻訳シーン、特に大型の PDF では非常にイライラします。さらに、DeepSeek R1 には大量の推論プロセスがあるため、32B モデルの速度はさらに遅くなります。もちろん、より良い構成のマシン、特に大きなメモリを持っている場合は、モデルが大きいほど効果が良くなることは間違いありません。この点は各自の選択に委ねられます。

DeepSeek R1 についてもう一点言及する必要があります。DeepSeek R1 は長い思考の連鎖を示しますが、翻訳のシーンでは思考の連鎖は必ずしも必要ではなく、むしろ冗長で翻訳速度を遅くすることがあります。比較すると、Qwen モデルはこのシーンでより良い選択です。後の文で思考の連鎖を解決するための提案を示します。

まとめると、モデルはたくさんあり、それぞれ利点と欠点があります。自分のニーズに基づいて、適切なモデルを選択すれば良いのです。私は qwen2.5-7b-instruct-1m を翻訳に使用しました(14B も問題ないはずです)。

以下の図を参考にしてダウンロードとインストールを行ってください。

image

サービスの起動#

次にモデルを読み込み、サービスを起動します。以下の図に従ってください。

image

image

一度読み込みが成功すれば、この大モデルを使用できるようになります。左側のメニューの最上部には対話機能があり、ここで読み込んだ大モデルと対話できます。

image

同時に、コマンドラインにコードをコピーしてモデルが正常に動作しているか、サービスが正常に起動しているかを確認できます。ここまでで、大モデルに関連する設定は終了です。おめでとうございます。サーバーの影響を受けず、迅速に応答する、あなた専用の大モデルをローカルで実行できるようになりました。

このサービスをローカルネットワークやインターネット上に公開すれば、他のデバイスからあなたの大モデルにアクセスできるようになります。これはまた別の話です。

image

image

翻訳 - Easydict#

本記事ではオープンソースのローカル翻訳ツール Easydict を使用しましたが、他のツールを見つけた場合も使用できます。本記事ではこれを例にします。

インストール#

以下の 2 つの方法のいずれかでインストールできます。

Easydict の最新バージョンは macOS 13.0 以上をサポートしています。もしシステムバージョンが macOS 11.0 以上の場合は、2.7.2 を使用してください。

1. 手動でダウンロードしてインストール#

ダウンロード 最新バージョンの Easydict。

2. Homebrew でインストール#

brew install --cask easydict

設定#

インストールが成功したら、ボタンをクリックして設定を選択し、設定ページに入ります。
次にサービスをクリックし、自分のサーバーアドレスを設定します。ここでは実際には ollama 翻訳とカスタム OpenAI 翻訳のいずれかを選択できます。

自分のサーバーアドレスのポートとモデル名を入力すれば大丈夫です。これらは LM Studio ページで見つけることができます。

image

image

使用#

設定が完了したら、正常に使用できるようになります。Easydict の具体的な使用方法については、対応する公式文書を参照してください。

image

さらに言及しておくと、EasyDict は他の形式の API もサポートしており、翻訳機能も内蔵されているため、前述のローカル大モデルの手順は必要ありません。本自体も非常に使いやすいアプリケーションです。

翻訳 - 沉浸式翻訳#

沉浸式翻訳は、OpenAI が登場して以来、最も人気のあるブラウザ翻訳プラグインの一つです。カスタム API インターフェースを使用して翻訳を行うこともサポートしており、ウェブページ翻訳と PDF 翻訳の両方に対応し、翻訳の表示効果も非常に優れています。

プロ会員をサポートしているため、手間をかけずにすぐに使用できます。もしローカル大モデルをさらに試したい場合は、後の方を見てください。

これは公式サイトのリンクです。

image

プラグインをダウンロードしたら、設定ページに入り、同様に API アドレスとモデル名を入力すれば、ローカル大モデルを使用して沉浸式翻訳ができるようになります。

image

image

サーバー転送#

ここまで来ると、ほぼ使用可能ですが、以下の問題に直面することになります。

  1. deepseek R1 の翻訳結果には思考の連鎖が含まれ、翻訳体験に影響を与えます。
  2. 沉浸式翻訳の API フォーマットと LM Studio の API フォーマットはシームレスに接続できないため、API が通じても、沉浸式翻訳は翻訳結果を表示できません。

したがって、やむを得ず、Python の web.py を使用して、リクエストを転送し、間にいくつかの小さな作業を行うための最も簡単なローカルサーバーを作成しました。これにより、上記の 2 つの小さな問題を解決し、翻訳体験を向上させます。以下にコードを添付しますので、参考にしてください。web.py のデフォルトポートは 8080 ですが、必要に応じて変更できます。

Python をインストールし、 pip install webpy を使用することを忘れないでください。ここでは詳しく説明しません。

import web
import json
import requests
import re
import time

# URLs ルーティングの設定
urls = (
    '/v1/chat/completions', 'ChatCompletions',
    '/v1/models', 'Models'
)

def add_cors_headers():
    # CORS 関連のレスポンスヘッダーを追加
    web.header('Access-Control-Allow-Origin', '*')
    web.header('Access-Control-Allow-Credentials', 'true')
    web.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
    web.header('Access-Control-Allow-Headers', 'Content-Type, Authorization')

def remove_think_tags(text):
    # <think> タグとその内容を削除
    return re.sub(r'<think>.*?</think>', '', text, flags=re.DOTALL)

class ChatCompletions:
    def OPTIONS(self):
        # プリフライトリクエストを処理
        add_cors_headers()
        return ''
        
    def POST(self):
        web.header('Content-Type', 'application/json')
        add_cors_headers()
        
        try:
            data = json.loads(web.data())
            lm_studio_url = "http://localhost:1234/v1/chat/completions"
            
            # ストリーミングリクエストかどうかを確認
            is_stream = data.get('stream', False)
            
            # リクエストを LM Studio に転送
            response = requests.post(
                lm_studio_url,
                json=data,
                headers={'Content-Type': 'application/json'},
                stream=is_stream  # stream パラメータを設定
            )
            
            if is_stream:
                # ストリーミングリクエストの場合、完全な内容を収集
                full_content = ""
                current_id = None
                
                def generate_stream():
                    nonlocal full_content, current_id
                    
                    for line in response.iter_lines():
                        if line:
                            line = line.decode('utf-8')
                            if line.startswith('data: '):
                                line = line[6:]
                            if line == '[DONE]':
                                # 完全な内容を処理し、最後のブロックを送信
                                cleaned_content = remove_think_tags(full_content)
                                # クリーンされた完全な内容を送信
                                final_chunk = {
                                    "id": current_id,
                                    "object": "chat.completion.chunk",
                                    "created": int(time.time()),
                                    "model": "local-model",
                                    "choices": [{
                                        "index": 0,
                                        "delta": {
                                            "content": cleaned_content
                                        },
                                        "finish_reason": "stop"
                                    }]
                                }
                                yield f'data: {json.dumps(final_chunk)}\n\n'
                                yield 'data: [DONE]\n\n'
                                continue
                                
                            try:
                                chunk_data = json.loads(line)
                                current_id = chunk_data.get('id', current_id)
                                
                                if 'choices' in chunk_data:
                                    for choice in chunk_data['choices']:
                                        if 'delta' in choice:
                                            if 'content' in choice['delta']:
                                                # 内容を蓄積し、直接送信しない
                                                full_content += choice['delta']['content']
                                
                                # 空の進捗更新を送信
                                progress_chunk = {
                                    "id": current_id,
                                    "object": "chat.completion.chunk",
                                    "created": int(time.time()),
                                    "model": "local-model",
                                    "choices": [{
                                        "index": 0,
                                        "delta": {},
                                        "finish_reason": None
                                    }]
                                }
                                yield f'data: {json.dumps(progress_chunk)}\n\n'
                                
                            except json.JSONDecodeError:
                                continue
                
                web.header('Content-Type', 'text/event-stream')
                web.header('Cache-Control', 'no-cache')
                web.header('Connection', 'keep-alive')
                return generate_stream()

            else:
                # 非ストリーミングリクエストの処理
                response_data = json.loads(response.text)
            
                if 'choices' in response_data:
                    for choice in response_data['choices']:
                        if 'message' in choice and 'content' in choice['message']:
                            choice['message']['content'] = remove_think_tags(
                                choice['message']['content']
                            )
                return json.dumps(response_data)
            
        except Exception as e:
            print(e)
            return json.dumps({
                "error": {
                    "message": str(e),
                    "type": "proxy_error"
                }
            })

class Models:
    def OPTIONS(self):
        # プリフライトリクエストを処理
        add_cors_headers()
        return ''
        
    def GET(self):
        web.header('Content-Type', 'application/json')
        add_cors_headers()
        # 模擬モデルリストを返す
        return json.dumps({
            "data": [
                {
                    "id": "local-model",
                    "object": "model",
                    "owned_by": "local"
                }
            ]
        })

if __name__ == "__main__":
    app = web.application(urls, globals())
    app.run() 

ここまでで、Mac 上でローカルにデプロイされた大モデルと、ウェブページ、PDF 文書、テキスト翻訳のツールチェーンが完成しました。代替可能なオプションはたくさんあります。
モデルのデプロイには Ollama などがあり、大モデルには Phi、Llama なども使用できます。転送サーバーも他のソリューションを使用できるか、転送なしのオプションを研究することも可能です。翻訳ツールも Bob に置き換えることができます。要するに、技術の選択肢は多く、本記事はあくまで参考を提供するものです。時間はより重要なことに費やすべきです。すでに利器を手に入れたのですから、早速その力を活かしましょう。

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。