ローカル環境でオープンソース大規模言語モデル
はじめに
GPT-4oやStable Diffusionのパワーは、もはや説明不要ですよね。
しかし、プロンプトを打って結果を待つ「ガチャ」を繰り返し、GPTでプロンプトを生成し、ComfyUIで画像や動画を作成、Udioで音楽を制作し、さらにCapCutで編集... こうした一連の手作業を毎回行うのは、少々手間ですよね?
そこで、これらの作業をどうにかして自動化できないかと考えています。
注目しているのが、Movie.py、Diffusers、Tanuki LLM、LangChain、そしてStable Audioです。
これらはすべてPython上で動作し、オープンソースかつ無料で利用できるため、コストを気にせず心ゆくまで使い倒せます。
ただし、音楽生成に関しては、Stable AudioはUdioやSunoに及ばない部分もあり、音楽制作にはUdioの課金が避けられないかもしれません。
やっぱり、Google Colabや他のAIサービスに課金して実験を行うのは、無駄にお金を使っているように感じてしまい、少し抵抗がありますよね。
GPU購入の最初の一歩:RTX 4070 Ti SUPERが初心者にオススメな理由
最初に取り組むべきは、グラフィックボードの購入です。
どのGPUを選ぶかは悩ましいところです。高価なものが多いですし、8GBメモリのモデルでは大半のタスクが厳しいかもしれません。12GB、16GB、24GBのモデルが現実的な選択肢になりますが、12GBでは限界があるので、やはり16GB以上が望ましいところです。
現在、24GBメモリを搭載しているのは、RTX 4090、RTX 3090 Ti、RTX 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モデルのバリエーションは以下の通りです。
参照ページ
Tanuki-8B-dpo-v1.0-AWQ
説明: AWQ形式で4bit量子化された8Bモデル。優れた性能と効率を兼ね備えたバランスの良いモデルです。
ダウンロード数: 723
必要VRAM: 約 4.3785 GB
Tanuki-8B-dpo-v1.0-4k-AWQ
説明: 4kトークンに対応し、AWQ形式で4bit量子化された8Bモデル。長いテキストを効率的に処理できます。
ダウンロード数: 19
必要VRAM: 約 4.3804 GB
Tanuki-8B-dpo-v1.0-GPTQ-4bit
説明: GPTQ形式で4bit量子化された8Bモデル。省メモリでのテキスト生成に最適です。
ダウンロード数: 82
必要VRAM: 約 4.3804 GB
Tanuki-8B-dpo-v1.0-4k-GPTQ-4bit
説明: 4kトークンに対応し、GPTQ形式で4bit量子化された8Bモデル。大規模テキスト処理を効率的に行えます。
ダウンロード数: 5
必要VRAM: 約 4.3804 GB
Tanuki-8B-dpo-v1.0-GPTQ-8bit
説明: GPTQ形式で8bit量子化された8Bモデル。中規模環境で高精度なテキスト生成に適しています。
ダウンロード数: 150
必要VRAM: 約 7.6558 GB
Tanuki-8B-dpo-v1.0-4k-GPTQ-8bit
説明: 4kトークンに対応し、GPTQ形式で8bit量子化された8Bモデル。メモリ効率を維持しつつ、高精度なテキスト生成が可能です。
ダウンロード数: 5
必要VRAM: 約 7.6558 GB
Tanuki-8x8B-dpo-v1.0-AWQ
説明: AWQ形式で4bit量子化された8x8Bモデル。大規模テキスト生成に最適な高性能モデルです。
ダウンロード数: 917
必要VRAM: 約 23.4735 GB
Tanuki-8x8B-dpo-v1.0-GPTQ-4bit
説明: GPTQ形式で4bit量子化された8x8Bモデル。大容量のテキスト生成に最適です。
ダウンロード数: 213
必要VRAM: 約 23.4735 GB
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)は、量子化手法として異なるアプローチを取ります。
AWQ(Adaptive Weight Quantization)
目的: モデルの重みを効率的に量子化し、パフォーマンスを維持しつつメモリ使用量を削減。
特徴: モデルの構造に基づいて動的に重みを調整し、精度を保ちながら量子化します。特定のアプリケーションにおいて高い効率を発揮します。
GPTQ(Generalized Post-Training Quantization)
目的: 学習後にモデルの重みを量子化し、より低いビット幅での推論を可能にします。
特徴: 一般的な量子化手法で、事前学習されたモデルを利用し、精度をなるべく損なわないように低ビット幅に変換します。GPTモデルに特化した調整も可能です。
要するに、AWQは動的な重み調整を行い、アプリケーション特化型の効率性を重視するのに対し、GPTQは一般的なポストトレーニング手法で、モデルの広範な適用を目指しています。
4bitと8bitの違い
4bitと8bitの違いは、主に以下の点にあります。
量子化の精度
4bit: 16段階の値を表現可能。これにより、メモリ使用量が少なく、モデルのサイズが小さくなりますが、精度がやや低下します。
8bit: 256段階の値を表現可能。より詳細な情報を保持できるため、精度が向上しますが、メモリ使用量も増加します。
メモリ効率
4bit: メモリ使用量が少なく、大規模なモデルやリソースが限られた環境での利用に適しています。
8bit: より多くのメモリを必要としますが、より高いパフォーマンスと精度が求められる場合に適しています。
使用シーン
4bit: リアルタイム処理やリソース制約のある環境での使用が適しており、トレードオフとして精度の低下を受け入れられるシナリオ。
8bit: 高精度が要求されるタスクや大規模なデータ処理に適しています。
8Bと8x8Bの違い
8Bと8x8Bの違いは、主に以下の点にあります。
モデルのサイズ
8B: 単一の8Bモデルで、一般的に中規模のテキスト生成や処理に適しています。
8x8B: 8つの8Bモデルを組み合わせた構成で、より大規模なデータセットや高い処理能力を必要とするタスクに向いています。
性能と精度
8B: 一般的なテキスト生成タスクに対して高精度を提供しますが、リソースに制限があります。
8x8B: より多くのパラメータを持ち、大規模なテキスト生成や複雑な処理を効率的に行うため、より高い精度と性能を発揮します。
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・デジタル領域に取り組んでいるYouTube配信者を積極的に募集しています。 DXパティーのレッスンとYouTubeチャンネルの収益化の両方から収益を得て、安定した収入を目指しましょう。