ChatGPT、あいかわらず話題ですね。最近は、だいぶ日本語の情報も増えてきました。記事によるとOpenAIへの最近のトラフィックは若干下降したようですが、まだまだ応用範囲を探るフェーズのように思います。
私もそろそろ追いつこう・・・ということで、この連休中LLMの勉強をしていました。
こういう進歩の早い分野って、すぐに上書きされちゃう知識だと判っていても、初期の内にフォローしておいた方が良くて、そうしないと技術進歩が落ち着いたころにはもう複雑すぎて非常に手を出しづらくなってしまうんですよね。
具体的にやったことは2つです。
1つ目は出たばかりのこの本。
OpenAIが提供しているAPIや、LlamaIndex、LangChainなどを解説した本です。
書籍としては若干高いですが、セミナーを受けると考えれば安いくらいですね。
これを読むと、OpenAIのAPIは非常にシンプルなものであることがわかります。プロトコルだけ見れば、天気予報や為替レートの提供サービスを使うのとたいして違いはありません。
LlamaIndexやLangChainは知りたかった内容がシンプルに書かれていて、サンプルも提供されており、大変参考になりました。
タイミング悪く、刊行直前にOpenAIのAPIが改訂されていますが、本書を読むうえではそれほど問題になりませんでした。
2つ目は、APIを呼び出すのではなくLLMそのものを動かしてみるというものです。
Google Colaboratoryを使ってやってみる、というのがメジャーですが、実は個人的には昨年StableDiffusionで遊ぶためにRTX3060を購入していましたので、これの上でローカルにLLMを動かしてみました。
RTX3060が載っているのはWindows 10のマシンなので、このマシンにWSL2をインストールし、その上でUbuntu 22を動かしました。
最近までWindows10のWSL2ではCUDAは正式にサポートされていなかったのですが、Windows10 22H2からは動作するようになっています。
つまり、WSL2で動くUbuntuから、CUDAを介してGPUを利用することができます。
インストールはこちらを参考にしました。
この環境の上で動作させるLLMとしては、rinna-3.6bを選びました。
rinnaはGPT-2のモデルを日本語が使えるように強化学習させたものだそうです。
強化学習には、HH-RLHF ( https://huggingface.co/datasets/Anthropic/hh-rlhf ) の一部を日本語に翻訳したデータを用いています。
というところが多分ポイントですね。
提供されるモデルは「汎用」と「対話用にファインチューニングしたもの」、それに少し遅れて発表された「人間の評価を利用した強化学習を行ったモデル」の3種類があります。(今回使うのは最後のモデルです。)
3.6bというのは3.6 billion、つまり36億パラメータです。
この数字はそのモデルが特定のGPUで効率的に動かせるかどうかの判断基準になります。
LLMを効率的に動作させるには、モデル全体をGPUのVRAMに載せる必要があります。
標準的には、1パラメーターあたり32bit float、つまり4バイト必要です。
従って、rinnaの動作にはVRAMが16GBのGPUが望ましいのですが、あいにく私の持っているRTX3060はVRAMが12GBしかありません。
ですので、モデルの精度を16bit floatに落として使います。これなら7.2GBのVRAMがあれば良いことになります。
CUDAが動作するWSL2+Ubuntu環境へのrinnaのインストールは、こちらを参考にしました。
GPUにアクセスできるdockerコンテナnvidia-dockerの導入と、rinnaをJupyter-Notebookを使って動かすために必要なファイル一式を入れたコンテナの作成が含まれています。
Google Colaboratoryでの動作確認→ローカル環境での動作確認、という流れになっていますが、後者から開始して大丈夫です。
ただし、Docker周りについてはroot権限で実行しないとうまくいかない部分がいくつかありました。
完了したら、記載されている通りに
$ nvidia-docker run -it --rm --gpus all -v `pwd`/src:/code -p 8888:8888 --name my-jupyter my-nvidia-cuda sh -c 'jupyter-lab --allow-root --ip=*'
でJupyter Notebookサーバが立ち上がります。このときURLが画面に出力されますので、これをコピーしてWindows側のブラウザで開きます。
Jupyter Notebook側で入力する内容も上記の記事通りですが、ただしこの記事は8GB VRAMの環境を前提にしているため、1パラメータを4ビットに圧縮しています。ここは16bit floatを使うように変更します。具体的には、
model = AutoModelForCausalLM.from_pretrained(
"rinna/japanese-gpt-neox-3.6b-instruction-ppo",
load_in_4bit=True,
device_map="auto",
)
という部分を、以下のように変更します。
model = AutoModelForCausalLM.from_pretrained(
"rinna/japanese-gpt-neox-3.6b-instruction-ppo",
torch_dtype=torch.bfloat16,
device_map="auto",
)
(ここは以下の記事からそのまま使わせていただきました)
また、実行時に以下のような警告メッセージが出ましたが、動作自体には問題ありませんでした。
The model weights are not tied. Please use the `tie_weights` method before using the `infer_auto_device` function.
このAutoModelForCausalLM
の実行には結構時間がかかります(数分以上?)。まあこのへんはGPUを使う他のアプリケーションも皆同様ですが、数GBのデータをGPUへ転送しなければなりませんので、気長に待ちましょう。
Windowsのタスクマネージャで「パフォーマンス」タブを開いてGPUのメモリ消費状況を観察すると進行状況が分かります。最終的には私の環境では消費済みVRAMが9.8GBになりました。
LLMがGPUにロードされるとお待ちかねの質問タイムです。
prompt = [
{
"speaker": "ユーザー",
"text": "夏の暑い日に爽やかな気分になるにはどうすればいいでしょうか。"
}
]
prompt = [
f"{uttr['speaker']}: {uttr['text']}"
for uttr in prompt
]
prompt = "<NL>".join(prompt)
prompt = (
prompt
+ "<NL>"
+ "システム: "
)
print(prompt)
token_ids = tokenizer.encode(prompt, add_special_tokens=False, return_tensors="pt")
with torch.no_grad():
output_ids = model.generate(
token_ids.to(model.device),
do_sample=True,
max_new_tokens=128,
temperature=0.7,
repetition_penalty=1.1,
pad_token_id=tokenizer.pad_token_id,
bos_token_id=tokenizer.bos_token_id,
eos_token_id=tokenizer.eos_token_id
)
output = tokenizer.decode(output_ids.tolist()[0][token_ids.size(1):])
output = output.replace("<NL>", "\n")
print(output)
結果です。ふーむ、なかなか良いですね。違和感ないお答えです。回答が返ってくるまでの時間は約7秒。読みやすくするために1文ごとに改行を追加しています。
ユーザー: 夏の暑い日に爽やかな気分になるにはどうすればいいでしょうか。<NL>
システム: 素晴らしい質問ですね。夏に爽快な気分になるためのさまざまな方法があります。
まず、運動や水泳などの健康的な習慣を身につけることが大切です。
また、涼しい服装をしたり、リラクゼーション音楽を聴いたりするなど、気持ちを落ち着かせるためのさまざまなテクニックもあります。
最後に、瞑想や自己催眠などのリラクゼーション技術を利用することもできます。</s>
もう一つ質問してみましょう。
冗長になるので最後のプロンプトだけを掲載しますが、実際にはシステムからの回答を順次履歴に付け足してプロンプトにしています。LLMは、それまでの単語列に基づいて次に出現する確率が高いトークンを返してくるという言語モデルなので、チャットする場合にはこのように会話履歴を与える必要があります。
この質問、私の中では期待する答えがあって質問を作っていますが、それが返ってくるかどうか・・・?
prompt = [
{
"speaker": "ユーザー",
"text": "音楽には人をひきつける抑揚が含まれています。どんな要素が抑揚を生み出すでしょうか。"
},
{
"speaker": "システム",
"text": "はい、音楽には強い強調や弱く抑制される音があります。また、リズムやビートも重要な役割を果たします。そして、強いアクセントや緩やかなアクセントも重要です。"
},
{
"speaker": "ユーザー",
"text": "例えばテンポが速くなったり、メロディの音高が高くなった場合も高揚感が得られますが、同じような効果があるものは何がありますか。"
}
]
prompt = [
f"{uttr['speaker']}: {uttr['text']}"
for uttr in prompt
]
prompt = "<NL>".join(prompt)
prompt = (
prompt
+ "<NL>"
+ "システム: "
)
print(prompt)
token_ids = tokenizer.encode(prompt, add_special_tokens=False, return_tensors="pt")
with torch.no_grad():
output_ids = model.generate(
token_ids.to(model.device),
do_sample=True,
max_new_tokens=128,
temperature=0.7,
repetition_penalty=1.1,
pad_token_id=tokenizer.pad_token_id,
bos_token_id=tokenizer.bos_token_id,
eos_token_id=tokenizer.eos_token_id
)
output = tokenizer.decode(output_ids.tolist()[0][token_ids.size(1):])
output = output.replace("<NL>", "\n")
print(output)
結果です。最後の応答は2.5秒くらいで返ってきました。和声進行なども言及できればさらに良かったですが、GPT-3.5に比べたら小規模なLLMなのに、ここまで回答してくるのは立派だと思います。もっとも、いくつか試した中では和声進行に言及する場合もありました。
ユーザー: 音楽には人をひきつける抑揚が含まれています。どんな要素が抑揚を生み出すでしょうか。<NL>
システム: はい、音楽には強い強調や弱く抑制される音があります。
また、リズムやビートも重要な役割を果たします。
そして、強いアクセントや緩やかなアクセントも重要です。<NL>
ユーザー: 例えばテンポが速くなったり、メロディの音高が高くなった場合も高揚感が得られますが、同じような効果があるものは何がありますか。<NL>
システム: メロディーの強弱や抑揚の変化により、強い感情的影響があります。
また、楽曲の全体的な構造や進行によっても抑揚が生じます。</s>
というわけで、とりあえずLLMをローカルで動作確認できました。
rinna-3.6bはモデルの規模から想像していたより賢いなあと思いました。
一方でこういった「知識を問う」質問は従来型の検索で良いのでは、という感じもしますね。
LlamaIndexのような、検索とLLMをハイブリッドでうまく使うにはどうするかというのは重要なのかもしれません。
いずれにせよ、もうちょっと使い込んでみたいと思います。
また、rinnaの派生モデル?や、GPUではなくCPUで動作させるllama.cppというものがあるらしいので、このあたりも試してみたいですね。
ちなみにこちらの記事では、GPU無しでの動作に挑戦されています。
もちろんLLMを動かす計算はCPUでも問題なく行えますので、動くことは動くのですが、スピードは非常に遅くなってしまいます。
Ryzen 9 3900(12コア24スレッド)にすると、シンプルな回答なら20~30秒、長いと50~60秒といったところになる。
ということですので、「チャット」するには苦しそうな応答速度です。それでも、GPUを使った場合の10倍程度の速度で動くのは意外ではあります。
さらに、このLLMの計算部分をC/C++で最適化/省力化したllama.cppというソフトウェアを使うと、差は2倍程度に縮まるようです。
llama.cppのドキュメントを見ると、「ARM NEON、AVX, AVX2 and AVX512サポート」「4ビット整数、5ビット整数、8ビット整数で量子化」「float16、float32サポート」といった文言がありますので、CPUのベクトル演算や、浮動小数点演算の整数演算への置き換え等々を駆使しているようです。
性能評価もありますが、7Bのモデル(rinnaの2倍程度)で50~70ms/トークン(1トークンは、英語だとほぼ1単語、日本語だと概ね1~0.7文字相当)ということですので、かなり高速になっています。
コメント