見出し画像

ローカル環境でオープンソース大規模言語モデル

はじめに

GPT-4oやStable Diffusionのパワーは、もはや説明不要ですよね。
しかし、プロンプトを打って結果を待つ「ガチャ」を繰り返し、GPTでプロンプトを生成し、ComfyUIで画像や動画を作成、Udioで音楽を制作し、さらにCapCutで編集... こうした一連の手作業を毎回行うのは、少々手間ですよね?

そこで、これらの作業をどうにかして自動化できないかと考えています。

注目しているのが、Movie.pyDiffusersTanuki LLMLangChain、そしてStable Audioです。

これらはすべてPython上で動作し、オープンソースかつ無料で利用できるため、コストを気にせず心ゆくまで使い倒せます。
ただし、音楽生成に関しては、Stable AudioはUdioやSunoに及ばない部分もあり、音楽制作にはUdioの課金が避けられないかもしれません。

やっぱり、Google Colabや他のAIサービスに課金して実験を行うのは、無駄にお金を使っているように感じてしまい、少し抵抗がありますよね。

GPU購入の最初の一歩:RTX 4070 Ti SUPERが初心者にオススメな理由

最初に取り組むべきは、グラフィックボードの購入です。

グラフィックボード性能比較 - Dospara

どのGPUを選ぶかは悩ましいところです。高価なものが多いですし、8GBメモリのモデルでは大半のタスクが厳しいかもしれません。12GB、16GB、24GBのモデルが現実的な選択肢になりますが、12GBでは限界があるので、やはり16GB以上が望ましいところです。

現在、24GBメモリを搭載しているのは、RTX 4090RTX 3090 TiRTX 3090の3種類しかありません。価格はそれぞれ約30万円、25万円、20万円前後です。中古品はリスクがあるため、あまり選択肢には入れたくないですね。

ほとんどの人が、グラフィックボードに20万円もかけたくないと感じるのではないでしょうか。

そんな中で目に留まったのがRTX 4070 Ti SUPERです。このモデルは16GBメモリを搭載し、約10万円で手に入るうえ、性能面でもRTX 3090に匹敵し、むしろそれ以上のパフォーマンスを発揮すると言われています。初心者にとっては十分すぎるスペックですし、コストパフォーマンスも優れています。

私も初心者なので、無難にRTX 4070 Ti SUPERを選びました。これで不足する部分はクラウドを活用する予定です。

GPT-4oを超えるオープンソースはあるのか?日本語対応を見据えたモデル選定

続いて、どの言語モデルを使うかについてです。私は現在、GPT-4oをサブスクライブしていますが、やはりGPT-4oは素晴らしいモデルですよね。自分で書くよりも、GPT-4oに文章を生成してもらった方が、より良い文章になります。

しかし、GPT-4oをプログラムに組み込む場合、別途OpenAIのAPI料金が発生するため、コスト面で負担が大きいです。

そこで、オープンソースのLLM(大規模言語モデル)も検討対象となります。

言語モデルの性能比較 - Dospara
元の記事はこちらです。

驚いたことに、オープンソースのLlama3.1がGPT-4oに匹敵する性能を発揮しているではありませんか!これなら、コストをかけずにAIエージェントを構築できるかもしれないと思いました。

そこで、さっそくOllamaからローカル環境にLlama3.1 8Bをダウンロードし、試してみました。

英語の会話はとてもスムーズです。

しかし、日本語に関しては……
品質が非常に低い!

調べてみると、どうやら英語しか学習していないモデルのようです。これでは、日本語でブログ記事の自動生成システムを作るのは難しそうです。

次に、Qwenというモデルを試そうと思いましたが、中国製であり、中国語向けに最適化されているとのこと。これも日本語では使いづらそうです。

そこで最後に試したのがTanukiです。これは純日本製で、東京大学の松尾研究室が開発に取り組んでいるモデルです。これなら、日本語対応も期待できるかもしれません。

Tanukiモデルの選び方:4bit、8bit、8B、8x8Bの違いを解説

Tanukiモデルのバリエーションは以下の通りです。
参照ページ

  1. Tanuki-8B-dpo-v1.0-AWQ

    • 説明: AWQ形式で4bit量子化された8Bモデル。優れた性能と効率を兼ね備えたバランスの良いモデルです。

    • ダウンロード数: 723

    • 必要VRAM: 約 4.3785 GB

  2. Tanuki-8B-dpo-v1.0-4k-AWQ

    • 説明: 4kトークンに対応し、AWQ形式で4bit量子化された8Bモデル。長いテキストを効率的に処理できます。

    • ダウンロード数: 19

    • 必要VRAM: 約 4.3804 GB

  3. Tanuki-8B-dpo-v1.0-GPTQ-4bit

    • 説明: GPTQ形式で4bit量子化された8Bモデル。省メモリでのテキスト生成に最適です。

    • ダウンロード数: 82

    • 必要VRAM: 約 4.3804 GB

  4. Tanuki-8B-dpo-v1.0-4k-GPTQ-4bit

    • 説明: 4kトークンに対応し、GPTQ形式で4bit量子化された8Bモデル。大規模テキスト処理を効率的に行えます。

    • ダウンロード数: 5

    • 必要VRAM: 約 4.3804 GB

  5. Tanuki-8B-dpo-v1.0-GPTQ-8bit

    • 説明: GPTQ形式で8bit量子化された8Bモデル。中規模環境で高精度なテキスト生成に適しています。

    • ダウンロード数: 150

    • 必要VRAM: 約 7.6558 GB

  6. Tanuki-8B-dpo-v1.0-4k-GPTQ-8bit

    • 説明: 4kトークンに対応し、GPTQ形式で8bit量子化された8Bモデル。メモリ効率を維持しつつ、高精度なテキスト生成が可能です。

    • ダウンロード数: 5

    • 必要VRAM: 約 7.6558 GB

  7. Tanuki-8x8B-dpo-v1.0-AWQ

    • 説明: AWQ形式で4bit量子化された8x8Bモデル。大規模テキスト生成に最適な高性能モデルです。

    • ダウンロード数: 917

    • 必要VRAM: 約 23.4735 GB

  8. Tanuki-8x8B-dpo-v1.0-GPTQ-4bit

    • 説明: GPTQ形式で4bit量子化された8x8Bモデル。大容量のテキスト生成に最適です。

    • ダウンロード数: 213

    • 必要VRAM: 約 23.4735 GB

  9. Tanuki-8x8B-dpo-v1.0-GPTQ-8bit

    • 説明: GPTQ形式で8bit量子化された8x8Bモデル。高精度とメモリ効率を両立したモデルです。

    • ダウンロード数: 69

    • 必要VRAM: 約 45.2674 GB

AWQとGPTQの違い

AWQ(Adaptive Weight Quantization)とGPTQ(Generalized Post-Training Quantization)は、量子化手法として異なるアプローチを取ります。

  1. AWQ(Adaptive Weight Quantization)

    • 目的: モデルの重みを効率的に量子化し、パフォーマンスを維持しつつメモリ使用量を削減。

    • 特徴: モデルの構造に基づいて動的に重みを調整し、精度を保ちながら量子化します。特定のアプリケーションにおいて高い効率を発揮します。

  2. GPTQ(Generalized Post-Training Quantization)

    • 目的: 学習後にモデルの重みを量子化し、より低いビット幅での推論を可能にします。

    • 特徴: 一般的な量子化手法で、事前学習されたモデルを利用し、精度をなるべく損なわないように低ビット幅に変換します。GPTモデルに特化した調整も可能です。

要するに、AWQは動的な重み調整を行い、アプリケーション特化型の効率性を重視するのに対し、GPTQは一般的なポストトレーニング手法で、モデルの広範な適用を目指しています。

4bitと8bitの違い

4bitと8bitの違いは、主に以下の点にあります。

  1. 量子化の精度

    • 4bit: 16段階の値を表現可能。これにより、メモリ使用量が少なく、モデルのサイズが小さくなりますが、精度がやや低下します。

    • 8bit: 256段階の値を表現可能。より詳細な情報を保持できるため、精度が向上しますが、メモリ使用量も増加します。

  2. メモリ効率

    • 4bit: メモリ使用量が少なく、大規模なモデルやリソースが限られた環境での利用に適しています。

    • 8bit: より多くのメモリを必要としますが、より高いパフォーマンスと精度が求められる場合に適しています。

  3. 使用シーン

    • 4bit: リアルタイム処理やリソース制約のある環境での使用が適しており、トレードオフとして精度の低下を受け入れられるシナリオ。

    • 8bit: 高精度が要求されるタスクや大規模なデータ処理に適しています。

8Bと8x8Bの違い

8Bと8x8Bの違いは、主に以下の点にあります。

  1. モデルのサイズ

    • 8B: 単一の8Bモデルで、一般的に中規模のテキスト生成や処理に適しています。

    • 8x8B: 8つの8Bモデルを組み合わせた構成で、より大規模なデータセットや高い処理能力を必要とするタスクに向いています。

  2. 性能と精度

    • 8B: 一般的なテキスト生成タスクに対して高精度を提供しますが、リソースに制限があります。

    • 8x8B: より多くのパラメータを持ち、大規模なテキスト生成や複雑な処理を効率的に行うため、より高い精度と性能を発揮します。

  3. VRAMの必要量

    • 8B: 比較的少ないVRAMを必要とし、リソースが限られた環境でも動作可能です。

    • 8x8B: より多くのVRAMが必要で、ハイエンドのハードウェアでの使用が推奨されます。

これらの違いを考慮し、特定のアプリケーションのニーズに応じて最適なモデルを選択することが重要です。

TanukiモデルとvLLMを使った実行環境の作成

私のPCのスペックは以下の通りです。

  • 環境: Windows 11 Home、WSL、Docker

  • プロセッサ: 12th Gen Intel(R) Core(TM) i5-12400 (2.50 GHz)

  • メモリ: 64.0 GB(使用可能: 63.8 GB)

  • GPU: NVIDIA GeForce RTX 4070 Ti SUPER 16GB

  • SSD: 1TB

8x8Bのモデルは大きすぎて、VRAMに収まりきらず、読み込むことができませんでした。

チャットモードでは4kトークン対応が有効ですが、質問への回答に専念させる場合は4k対応は不要かもしれません。bit数は大きい方が精度が良いと考えたため、今回はTanuki-8B-dpo-v1.0-4k-GPTQ-8bitを選びました。

次に実行手順について

こちらのページが非常にわかりやすくまとめられています。「vLLMによる推論(最推奨)」と記載されていたため、vLLMを使用することに決めました。

上記のページにvLLMのインストール手順が記載されており、その通りに進めましたが、以下のエラーが発生しました。

ERROR: pip's dependency resolver does not currently take into account all the packages that are installed. This behaviour is the source of the following dependency conflicts.
cudf 24.4.0 requires pyarrow<15.0.0a0,>=14.0.1, but you have pyarrow 17.0.0 which is incompatible.

このエラーは、以下のコマンドを実行することで解決しました。

pip uninstall datasets pyarrow  
pip install datasets pyarrow==14.0.2

無事に実行できました!日本語だけの会話に関しては、GPT-4o-miniと大差がない印象です。

これで、ようやくAIエージェント開発のスタートラインに立てた気がします。

実行環境とコードを載せておきますね!

Dockerfile

FROM nvcr.io/nvidia/pytorch:24.07-py3

RUN apt-get update && apt-get install -y wget
RUN apt-get install -y pulseaudio alsa-utils

RUN pip install --no-cache-dir openai langchain playwright lxml asyncio nest_asyncio beautifulsoup4

RUN playwright install-deps
RUN playwright install

ENV TZ='Asia/Tokyo'
WORKDIR /workspaces

devcontainer.json

{
  "name": "pytorch",
  "dockerFile": "Dockerfile",
  "runArgs": [
    "--gpus",
    "all",
    "--shm-size",
    "64gb",
    "-e",
    "PULSE_SERVER=unix:/mnt/wslg/PulseServer",
    "-v",
    "/mnt/wslg/:/mnt/wslg/"
  ],
  "settings": {
    "terminal.integrated.shell.linux": "/bin/bash"
  },
  "extensions": [
    "dbaeumer.vscode-eslint",
    "esbenp.prettier-vscode"
  ],
  "workspaceMount": "source=${localWorkspaceFolder},target=/hostdir,type=bind,consistency=cached",
  "workspaceFolder": "/workspaces",
  "containerEnv": {
    "MOUNTED_HOST_DIR": "${localWorkspaceFolder}",
    "MOUNTED_HOST_DIR_PATH_IN_CONTAINER": "/hostdir"
  },
  "forwardPorts": [80],
  "features": {
    "docker-in-docker": {
      "version": "latest"
    }
  },
  "remoteUser": "root",
  "containerUser": "root"
}

LanguageModelHandler.py

from time import time
from vllm.entrypoints.llm import LLM, SamplingParams

class LanguageModelHandler:
  def __init__(self, system_prompt, initial_response, llm_params, sampling_params):
    # モデルの設定を行う。パラメータはモデル名、並列処理数、データ型、GPU使用率など。
    self.vllm = LLM(model=llm_params["model_name"], tensor_parallel_size=llm_params["tensor_parallel_size"], dtype=llm_params["dtype"], gpu_memory_utilization=llm_params["gpu_memory_utilization"])
    self.tokenizer = self.vllm.get_tokenizer()
    self.sampling_params = SamplingParams(**sampling_params)
    # メッセージ履歴を初期化(システムプロンプトとアシスタントの初期応答を設定)
    self.reset_messages(system_prompt, initial_response)

  def reset_messages(self, system_prompt=None, initial_response=None):
    # システムプロンプトと初期応答が指定されていれば、メッセージ履歴をリセット
    if system_prompt and initial_response:
      self.messages = [
        {"role": "system", "content": system_prompt},  # モデルに与えるシステム指示(コンテキスト設定)
        {"role": "assistant", "content": initial_response}  # 初期応答を設定
      ]
    else:
      self.messages = []

  def update_message_history(self, role, content):
    # ユーザーまたはアシスタントの発言を履歴に追加
    self.messages.append({"role": role, "content": content})

  def prepare_inputs(self):
    # 現在のメッセージ履歴を、LLMが理解できるチャット形式に整形
    return self.tokenizer.apply_chat_template(self.messages, tokenize=False, add_generation_prompt=True)

  def count_tokens_and_characters(self, text):
    # 入力テキストのトークン数と文字数をカウント
    tokens = self.tokenizer.tokenize(text)
    token_count = len(tokens)
    character_count = len(text)  # 日本語の文字数を算出
    return token_count, character_count

  def generate_response(self, user_message, show_elapsed_time=False, show_message_history=False, show_token_usage=False):
    # 新しいユーザー発言を履歴に追加
    self.update_message_history("user", user_message)
    # 現在のメッセージ履歴を整形し、入力テキストを準備
    inputs_text = self.prepare_inputs()

    # 入力トークンと文字数を計測
    input_tokens, input_characters = self.count_tokens_and_characters(inputs_text)
    start_time = time()

    # LLMを用いて応答を生成
    outputs = self.vllm.generate(inputs_text, sampling_params=self.sampling_params, use_tqdm=False)
    elapsed_time = time() - start_time

    # 出力テキストとそのトークン数、文字数を取得
    response_text = outputs[0].outputs[0].text.strip()
    output_tokens, output_characters = self.count_tokens_and_characters(response_text)

    # 応答を履歴に追加
    self.update_message_history("assistant", response_text)

    print(f"*** Assistant's Response ***\n{response_text}\n")

    # 時間計測を表示するオプション
    if show_elapsed_time:
      print(f"*** Elapsed Time ***\n{elapsed_time:.4f} sec.\n")

    # トークン使用量を表示するオプション
    if show_token_usage:
      print(f"*** Token Usage ***")
      print(f"Input Tokens: {input_tokens}")  # 入力テキストのトークン数を表示
      print(f"Input Characters (Japanese): {input_characters} 文字")  # 入力テキストの日本語文字数
      print(f"Output Tokens: {output_tokens}")  # 出力テキストのトークン数を表示
      print(f"Output Characters (Japanese): {output_characters} 文字\n")  # 出力テキストの日本語文字数

    # メッセージ履歴を表示するオプション
    if show_message_history:
      print("*** Message History ***")
      for message in self.messages:
        role = message['role'].capitalize()
        content = message['content']
        print(f"{role}: \n{content}\n")

  def get_message_history(self):
    return self.messages


if __name__ == "__main__":
  model_names = {
    "Tanuki-8B-dpo-v1.0-AWQ": "team-hatakeyama-phase2/Tanuki-8B-dpo-v1.0-AWQ",                   # 4.3785 GB
    "Tanuki-8B-dpo-v1.0-4k-AWQ": "team-hatakeyama-phase2/Tanuki-8B-dpo-v1.0-4k-AWQ",             # 4.3804 GB
    "Tanuki-8B-dpo-v1.0-GPTQ-4bit": "team-hatakeyama-phase2/Tanuki-8B-dpo-v1.0-GPTQ-4bit",       # 4.3804 GB
    "Tanuki-8B-dpo-v1.0-4k-GPTQ-4bit": "team-hatakeyama-phase2/Tanuki-8B-dpo-v1.0-4k-GPTQ-4bit", # 4.3804 GB
    "Tanuki-8B-dpo-v1.0-GPTQ-8bit": "team-hatakeyama-phase2/Tanuki-8B-dpo-v1.0-GPTQ-8bit",       # 7.6558 GB
    "Tanuki-8B-dpo-v1.0-4k-GPTQ-8bit": "team-hatakeyama-phase2/Tanuki-8B-dpo-v1.0-4k-GPTQ-8bit", # 7.6558 GB
    "Tanuki-8x8B-dpo-v1.0-AWQ": "team-hatakeyama-phase2/Tanuki-8x8B-dpo-v1.0-AWQ",               # 23.4735 GB
    "Tanuki-8x8B-dpo-v1.0-GPTQ-4bit": "team-hatakeyama-phase2/Tanuki-8x8B-dpo-v1.0-GPTQ-4bit",   # 23.4735 GB
    "Tanuki-8x8B-dpo-v1.0-GPTQ-8bit": "team-hatakeyama-phase2/Tanuki-8x8B-dpo-v1.0-GPTQ-8bit",   # 45.2674 GB
  }

  llm_params = {
    "model_name": model_names["Tanuki-8B-dpo-v1.0-AWQ"],  # 使用するモデルの選択
    "tensor_parallel_size": 1,  # 並列処理数(GPU分割の設定)
    "dtype": "auto",  # データ型を自動設定
    "gpu_memory_utilization": 0.99  # GPU使用率の指定(1.0に近いほどフル活用)
  }

  sampling_params = {
    "temperature": 0.2,  # 応答の多様性を制御。低い値にすると安定した応答を生成
    "max_tokens": 512,  # 最大生成トークン数(長文生成を防止)
    "top_p": 0.8,  # 応答のランダム性を調整(0.8は安全な範囲)
    "top_k": 40,  # 応答候補のトークン数を制限(低い値にすると標準的な応答)
    "repetition_penalty": 1.2,  # 同じ表現の繰り返しを防止
    "presence_penalty": 0.0,  # 特定トークンの生成頻度を調整(0.0は制御なし)
    "frequency_penalty": 0.0,  # トークンの出現頻度を調整(0.0は制御なし)
    "seed": 42  # 再現性のためのシード値(同じ出力を得る場合に有効)Noneにすると再現性がなくランダムな応答を生成
  }

  handler = LanguageModelHandler(
    system_prompt="あなたは医者です。ユーザーに対して問診を行い、親しみやすくかつ簡潔に症状について尋ねてください。回答は医療的知識を持ったアシスタントとして、正確でわかりやすく答えてください。コードや技術用語を使用しないこと。",
    initial_response="こんにちは。どのような症状がありますか?",
    llm_params=llm_params,
    sampling_params=sampling_params
  )

  handler.generate_response("胃が痛いです。", show_elapsed_time=True, show_message_history=True, show_token_usage=True)
  handler.generate_response("刺すように痛いです。", show_elapsed_time=True, show_message_history=True, show_token_usage=True)

  handler.reset_messages(
    system_prompt="あなたは家庭教師です。生徒に勉強のアドバイスをしてください。回答は丁寧で親しみやすく、簡潔に伝えてください。",
    initial_response="こんにちは、勉強に関してどんな質問がありますか?"
  )

  handler.generate_response("数学の勉強方法を教えてください。", show_elapsed_time=True, show_message_history=True, show_token_usage=True)


今回のプログラムは、Cursorを使用してAIと共同で作成しています。
プログラミングの敷居は以前よりも大幅に下がりました。

プログラミングに苦手意識をお持ちの方は、ぜひ「DXパーティー」までお問い合わせください。
有料サービスではありますが、オンラインでマンツーマンのレッスンを受けることで、短期間でPythonを習得し、自在に使いこなせるようになると思います!

お問い合わせ - DXパティー (dxpt.jp)

マンツーマンオンライン講師を募集しています。 特に視聴者数が少ないDX・デジタル領域に取り組んでいるYouTube配信者を積極的に募集しています。 DXパティーのレッスンとYouTubeチャンネルの収益化の両方から収益を得て、安定した収入を目指しましょう。

DXパティーでは講師募集中!

お仕事のご依頼もお待ちしております!