こんにちは、CCCMKホールディングス TECH LABの三浦です。
あけましておめでとうございます。2025年がはじまりました。今年もまた、色々なことを試していきたいなと思います!
昨年末にNeurIPS 2024に参加してから、LLMの"Post Training"というアプローチに興味を持っています。Post Trainingは、日本語では"事前学習"と呼ばれている"Pre Training"の後に行われるLLMの学習工程です。今回はPost Trainingで行われる、LLMの出力をより好ましいものに調整する"Preference Learning"で使用されるDPO(Direct Preference Optimization)について調べ、それによってLLMの出力がどのように調整されるのかを確認してみました。
Post Training
まずPost Trainingについて簡単に整理してみます。LLMは最初に大量の文章を使った自己教師あり学習が行われます。人によるラベル付けが必要なく、与えられた文章を補完するように学習させる方法で、これは"Pre Training"と呼ばれます。Pre Training後のモデルは事前学習済みモデルや基盤モデル(Foundation Model)と呼ばれています。
事前学習済みモデルは与えられたテキストに対し、それに続く自然なテキストを生成出来るように学習されています。なので、たとえば"日本で一番高い山は?"という入力テキストに対し、"富士山です。"と答えるのではなく、"富士山で、世界で一番高い山は・・・"とように、継続するテキストを生成するような動作をしてしまいます。このような動作をしてしまうLLMに対し、指示に対する回答を生成出来るようにし、かつその回答が人にとって望ましいものにするために行うのが"Post Training"です。
Post Trainingは主に2段階で行われます。最初はInstruction Tuningで、指示(Instruction)とそれに対する回答で構成されるデータによる教師あり学習です。Instruction TuningによってLLMは指示に対応することが出来るようになります。さらにInstruction Tuningだけではつかむことが出来ない、人が望ましいと考える回答のニュアンスを伝えるステップがPreference Learningです。
Preference Learningでは、RLHFという強化学習による手法が主に使われています。RLHFはReinforcement Learning from Human Feedbackの略で、人のフィードバックによる強化学習を意味します。RLHFはLLMが生成したテキストに対する人の評価を報酬とした強化学習です。実際には報酬を与える部分は報酬モデルによって実装されます。報酬モデルは事前に収集済みのサンプルデータに対する人の評価を元に学習されたものです。RLHFにおいてはPPO(Proximal Policy Optimization)という学習アルゴリズムが使われることが多いです。
RLHFは計算コストが高く、さらにLLMだけでなく報酬モデルの学習も必要である、といった点がその敷居を高めています。これに変わるPreference Learningの手法としてDPO(Direct Preference Optimization)という、強化学習を用いずにRLHFと同等の性能が出せる手法が提案されました。DPOは与えられた指示に対し、望ましい回答(chosen)と望ましくない回答(rejected)のペアを用意するだけで学習を行うことが出来ます。
個人的に、RLHFはとても敷居が高い印象を持っていて、チャレンジするにはそれなりの準備が必要だろうな・・・と考えていました。そんな中DPOについて知り、DPOの必要なステップがとても少なく、学習に必要なデータのイメージもつきやすい部分に興味を持ち、実際に試してみようと考えました。
今回はLLMの学習を高速に、かつ少ないリソースで実行できるHugging Faceと互換性のある"Unsloth"という軽量ライブラリを用いてDPOにチャレンジしてみました。
Unsloth
Hugging FaceのblogにUnslothについて紹介されています。
LLMのFine Tuning向けのHugging Faceのライブラリ"TRL"と合わせて使うことが出来、TRLだけで学習した場合に比べ、学習速度は2倍に、メモリ使用を40%程度削減出来ることもあるそうです。
Unslothの公式ドキュメントは以下になります。
こちらのドキュメントにインストール方法が掲載されていますが、インストールする環境のCUDAとPyTorchのバージョンによってインストールコマンドが異なります。
環境に応じた最適なインストールコマンドは以下を実行することで取得することが出来ます。
wget -qO- https://raw.githubusercontent.com/unslothai/unsloth/main/unsloth/_auto_install.py | python -
今回試した環境はA100GPU1台のサーバです。
DPOに使うデータ
先に述べたように、必要なデータは指示とそれに対する望ましい回答(chosen)と望ましくない回答(rejected)のデータです。今回はDPOの効果を視覚的に確認したかったので、LLMの回答がカジュアルな文体になるようにチューニングをしてみようと考えました。 望ましい回答としてカジュアルな文体を、望ましくない回答として丁寧な文体を用意し、学習してみました。
以下の日本語のデータセットを使わせて頂きました。
こちらのデータセットに含まれる回答は丁寧な文体になっているため、これをカジュアルな文体に変換した1,000レコードのデータセットを使ってDPOを行いました。
学習用のコード
Datasetの加工
prompt, chosen, rejectedという3つのカラムで構成されるDataframeから、さらにDPO向けに加工を行う必要があります。
データの形式は以下のHugging Faceのドキュメントに掲載されていますが、チャット形式にします。
次のようなコードで実現しました。
def format_datasets(example): """データ変換処理""" formatted_example = {} formatted_example["chosen"] = [ {"content":example["prompt"],"role":"user"}, {"content":example["chosen"],"role":"assistant"} ] formatted_example["rejected"] = [ {"content":example["prompt"],"role":"user"}, {"content":example["rejected"],"role":"assistant"} ] return formatted_example ds = Dataset.from_pandas(df) training_ds = ds.map(format_datasets)
あとはUnslothのドキュメントの"Reward Modelling - DPO, ORPO & KTO"に掲載されているコードを元に学習を実行しました。
Unslothは量子化済みの軽量なLLMをHugging Faceにいくつか公開しています。今回はその中の"unsloth/gemma-2-9b-it-bnb-4bit"を使用しました。
DPOの効果の確認
学習後、DPOの効果がどのように出ているかを確認してみました。最初に学習済みのモデルを推論用にロードする必要があります。
FastLanguageModel.for_inference(model)
また、比較対象にDPO前の同じモデルも用意しておきました。
# tokenizerも得られるが、2つのモデルで共通なのでロードしない。 original_model, _ = FastLanguageModel.from_pretrained( model_name = "unsloth/gemma-2-9b-it-bnb-4bit", max_seq_length = max_seq_length, dtype = None, load_in_4bit = True, ) FastLanguageModel.for_inference(original_model)
回答生成用の関数を作成しました。
def generate_answer(model, tokenizer, prompt): """model, tokenizerを使ってpromptに対する回答を生成する""" chat_prompt = [{"role":"user","content":prompt}] formatted_prompt = tokenizer.apply_chat_template( chat_prompt, add_generation_prompt=True, return_tensors="pt" ) if model.device.type == "cuda": formatted_prompt = formatted_prompt.to("cuda") tokenized_answer = model.generate( formatted_prompt, max_new_tokens=100, do_sample=True, temperature=0.7, top_p=0.95, top_k=0 ) answer = tokenizer.batch_decode( tokenized_answer[:,formatted_prompt.shape[1]:], skip_special_tokens=True )[0] return answer
試してみます。
prompt = "日本で一番高い山は?" org_answer = generate_answer(original_model, tokenizer, prompt) dpo_answer = generate_answer(model, tokenizer, prompt) print(f"オリジナル: {org_answer}") print(f"DPO: {dpo_answer}")
結果は次の通りになりました。DPOの回答が、ちゃんとカジュアルになっていることが分かります。
オリジナル: 日本で一番高い山は **富士山** です。標高は3,776メートルです。 DPO: 日本で一番高い山は、**富士山** だよ! 3,776メートルもあるんだ! 🏔️
別のプロンプト「カレー食べたいな」でも試してみました。
オリジナル: カレー食べたい気持ち、すごくよく分かります!🍛 どんなカレーが食べたいですか? * 辛口? * マイルド? * チキンカレー? * 野菜カレー? * ゴーヤカレー? 教えてください! 🤤✨ DPO: カレー食べたいよねー!🍛✨ どんなカレーがいいかな? * **定番のチキンカレー?**🐔 * **辛いの好きなら、ガパオとかキーマも美味しいよ!**🌶️ * **野菜たっぷりでヘルシーなカレーもいいね!**🥦 トッピングで、卵とかチーズとかも乗っけたら最高じゃん!😋 一緒にカレー食べに行こうよ!😊
ちゃんとカジュアルになってます!いい感じですね!
まとめ
ということで、今回はLLMのPost TrainingにおけるPreference LearningをDPOで試してみた話をまとめてみました。Preference Learningは必要なステップが多い印象があり、敷居が高く感じていたのですがDPOはとてもシンプルなステップで実行することが出来ました。また、Unslothを使用することで非常に高速にDPOの学習を実現することも出来ました。 オープンソースの、小サイズのLLMを使っていると時々回答を修正したいな・・・と感じることがあったのですが、DPOを使うことで比較的簡単にそれを実現することが出来そうで、今後も活用していきたいと感じました。