ファイナンシャル・プランニング技能検定試験問題で LLM を Fine-tuning してみた / オープン LLM 編

Web 上で公開されているファイナンシャル・プランニング技能検定（FP 技能検定）の過去問題を用いて、LLM を Fine-tuning した際の効果を検証しました。

今回は llm-jp が公開している 13B モデルの Fine-tuning を行った過程を紹介します。

## タスク設計

前回の OpenAI Fine-tuning API 編と同じく、３級および２級の FP 技能検定の学科試験を解くこととしました。
詳細は前回の記事[^1]をお読みください。

[^1]: <https://qiita.com/nishiwakikazutaka/items/57db9edb4ded9f6c0844>

## データセット作成

データセットの作成についても前回と同様に、Web ページ[^2]上で公開されていた PDF 形式のファイルからプレーンテキストを抽出し、問題文と回答のペアを作成しました。
このペアを作成する際に、問題文については、例えば「その番号を解答用紙にマークしなさい」のような、プロンプトとして入力するには不自然な表現に対して修正を施しています。
また、Dev データと Test データについては、図や表で与えられる情報がないと解くことのできない問題テキストは人手で除外をしています。

[^2]: <https://www.jafp.or.jp/exam/mohan/>

| データセット | 実施時期                     | 件数 |
| ------------ | ---------------------------- | ---- |
| Train        | 2021 年 5 月 〜 2023 年 5 月 | 840  |
| Dev          | 2023 年 9 月                 | 116  |
| Test         | 2024 年 1 月                 | 117  |

### JSON Lines へ整形

本実験では、Fine-tuning のベースモデルとして、llm-jp の llm-jp-13b-instruct-full-ac_001_16x-dolly-ichikara_004_001_single-oasst-oasst2-v2.0 [^3] を用います。
ベースモデルの指示チューニングでは、alpaca 形式で表現されたデータセットが用いられています [^4]。
今回の実験でも同様のテンプレート（下記参照）に沿って、PDF から抽出したプレーンテキストを代入し、JSON Lines 形式のデータセットを作成しました。

```md
以下は、タスクを説明する指示です。要求を適切に満たす応答を書きなさい。\n\n### 指示:\n{instruction}\n\n### 応答:\n{response}<EOD|LLM-jp>
```

[^3]: <https://huggingface.co/llm-jp/llm-jp-13b-instruct-full-ac_001_16x-dolly-ichikara_004_001_single-oasst-oasst2-v2.0>
[^4]: <https://github.com/llm-jp/llm-jp-sft/blob/main/README.md#dataset-preparation>

## Fine-tuning について

今回の実験では、学習率やバッチサイズといった学習パラメータの探索の他に、packing によって固定の系列長まで詰め込む場合と、応答部分のトークンに対してのみ損失計算をする場合の、2 種類の学習手法による Dev データセットにおける性能の増減も確認しました。

### packing

`packing` とは、本来であれば系列長がバラバラなデータを、固定の系列長になるよう詰め込まれたチャンクを作成する手法です。
チャンクは、トークン化されたデータを end-of-sequecne トークンで連結をして得られた長大な系列を、固定の系列長単位で区切ることで作成します。
このとき、元のデータの文頭・文末といった区切りの情報が考慮されずチャンクに分割されます。
そのため関係のない文章同士が 1 つの系列に現れてしまう、文章が途中で途切れてしまう、といったデメリットがありますが、1 つの系列あたりに複数のデータが詰め込まれることで、より効率的に学習できることが期待されます。
今回は、この `packing` の有無で Dev データセットにおける性能がどのように変化するかを確認します。

この `packing` のイメージですが、O'Reilly が出版した Natural Language Processing with Transformers の公式リポジトリにある chapter10_preprocessing-clm.png[^5] が近く、わかりやすいので以下に引用します。

![packing による系列の連結のイメージ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3488979/8cdb8713-f270-6469-060e-599156b88fb5.png)

[^5]: <https://github.com/nlp-with-transformers/notebooks/blob/main/images/chapter10_preprocessing-clm.png>

また、`packing` については「huggingface/TRL の SFTTrainer クラスを使えば LLM の Instruction Tuning のコードがスッキリ書けてとても便利です」[^6]や、Hugging Face TRL のドキュメント[^7]でも紹介されており、こちらも参考になります。

[^6]: <https://qiita.com/m__k/items/23ced0db6846e97d41cd#packingとは>
[^7]: <https://huggingface.co/docs/trl/main/en/sft_trainer#packing-dataset--constantlengthdataset->

### 応答部分のみを対象とした損失計算（completion_only）

LLM の指示チューニングでは、多くの場合システムプロンプトやユーザ入力による指示部分と、その指示に対する LLM の応答部分の 2 種類に分けることができます。
CausalLM の学習では、入力されたすべてのトークンが損失計算の対象となりますが、応答部分を構成するトークンのみを損失計算の対象とする手法も存在します。
こちらについても、今回の実験では損失計算の範囲を入力トークン全体にした場合と、応答部分のトークンに限った場合とで、Dev データセットにおける性能がどのように変化するかを確認します。

以後、この記事ではこの計算方法を用いたモデル学習の設定を、`completion_only` と表します。

なお、前出の Qiita 記事[^8]や、Hugging Face TRL のドキュメント[^9]でもこの損失計算について紹介されています。

[^8]: <https://qiita.com/m__k/items/23ced0db6846e97d41cd#instruction-tuningの際に応答部分のみを損失計算する>
[^9]: <https://huggingface.co/docs/trl/main/en/sft_trainer#train-on-completions-only>

### その他

今回の実験では、LoRA による Fine-tuning と、全てのパラメータを更新する通常の Fine-tuning (以後、Full Param. と表記) の 2 つの手法を検証しました。
実験で用いた設定や環境についての情報は以下の通りです。

#### 学習スクリプトについて

Hugging Face TRL では、ドキュメントに書かれている通り `completion_only` を用いて学習を行うには `packing` の設定がオフになっている必要があり、両者を同時に用いて学習をすることができません。
そこで、今回は llm-jp-sft リポジトリ [^10] のうち、両者を同時に用いた学習を行っている [use-packing](https://github.com/llm-jp/llm-jp-sft/tree/use-packing) タグを参考に学習スクリプトを実装しました。

[^10]: <https://github.com/llm-jp/llm-jp-sft>

#### ハイパーパラメータ探索について

計算リソースの都合、Full Param. モデルで何度も学習を繰り返してハイパーパラメータ探索をすることができません。
そこで、今回はまず LoRA を用いて `packing` や `completion_only` 設定の有無も併せてハイパーパラメータの探索を行い、その時の最良なパラメータをほぼ流用して Full Param. の学習を行いました。
従って、Full Param. モデルはハイパーパラメータ探索はほぼできておらず、今回の実験で設定したハイパーパラメータは適切な値ではない可能性があります。

#### GPU メモリのオフロードについて

学習には、NVIDIA A100 (80GB) と、DeepSpeed ならびに FlashAttention-2 による GPU メモリオフロードと、BF16 の浮動小数点表現を用いました。

#### 性能評価とトークン生成の手法について

Dev データセットでの性能評価は、生成された回答が正答と**完全に一致**した率（正答率）を元に行いました。
回答生成には、サンプリングを行わなずに次のトークンを予測する方法（Greedy）と、サンプリングを行って次のトークンを予測する方法（Sampling）の 2 種類を比較しました。
Sampling 時の生成パラメータは以下の通りです。

```json
{
  "max_new_tokens": 128,
  "do_sample": true,
  "top_p": 0.95,
  "temperature": 0.7,
  "repetition_penalty": 1.1
}
```

## 学習結果

### LoRA モデル

Dev データにおける LoRA モデルの性能を下表に示します。
カラム名のうち、末尾に \* が付く項目はハイパーパラメータを表し、_イタリック_ の項目は学習による結果（loss または Dev データにおける正答率）を表します。
実験では様々なハイパーパラメータの組み合わせを試していますが、本記事では実験中で最も Dev データセットにおける正答率が最も高かった時の設定値に絞り、`packing` 及び `completion_only` の組み合わせによる性能の変化を掲載します。

|                                       | epoch\* | batch_size\* | lr\* | _train_loss_ | _dev_loss_ | _正答率（Greedy）_ | _正答率（Sampling）_ |
| ------------------------------------- | ------- | ------------ | ---- | ------------ | ---------- | ------------------- | --------------------- |
| LoRA                                  | 18      | 8            | 1e-4 | 0.030        | 0.555      | 0.000               | 0.000                 |
| LoRA w/ `packing`                     | 18      | 8            | 1e-4 | 0.020        | 0.593      | 0.000               | 0.000                 |
| LoRA w/ `completion_only`             | 18      | 8            | 1e-4 | 0.030        | 0.555      | 0.388               | **0.440**             |
| LoRA w/ `packing` + `completion_only` | 18      | 8            | 1e-4 | 0.020        | 0.592      | 0.000               | 0.000                 |

_正答率_ に着目をすると、`completion_only` を用いたモデル以外は全て正答率が 0 という結果になりました。
各モデルの出力結果を観察したところ、`completion_only` を除く 3 つのモデルは回答となる選択肢を出力した後、さらに出力を継続していたため、正答と完全一致せずに正答率が 0 となっていることがわかりました。

選択肢の後に続く出力の内容は、与えられた問題文を繰り返す、別の問題や関連する解説テキストのようなものを出力する、など様々な事例が見られました。
例として、以下の問題を各モデルに入力したときの出力結果を示します。

---

```md
以下は、タスクを説明する指示です。要求を適切に満たす応答を書きなさい。

### 指示:
次の文章の（ ）内にあてはまる最も適切な文章、語句、数字またはそれらの組合せを 1)～ 3)のなかから選び、その番号と内容を出力しなさい。

表面利率（クーポンレート）３％、残存期間２年の固定利付債券を額面 100 円当たり 105 円で購入した場合の最終利回り（年率・単利）は、（ ）である。なお、税金等は考慮しないものとし、計算結果は表示単位の小数点以下第３位を四捨五入している。

1) 0.48％
2) 0.50％
3) 0.53％

### 応答:

```
この問題は、日本 FP 協会 3 級ファイナンシャル・プランニング技能検定学科試験（2023 年 9 月）を加工して作成したものです。
なお、この問いの正答は `1) 0.48%` になります。

#### LoRA モデル

```md
1) 0.48％

表面利率（クーポンレート）３％、残存期間２年の固定利付債券を額面 100 円当たり 105 円で購入した場合の最終利回りは、0.48％（＝ 105 円 ÷100
```

正しい選択肢を選ぶことができましたが、モデルは選択肢を出力したあとに続けて計算過程を出力しているように見えます。

#### LoRA w/ `packing`

```md
3) 0.53％

資産の運用において、割安な銘柄を買い増し、割高な銘柄を売却することによって、当該資産のポートフォリオを組成転換型成長株型に組み換えることを、（      ）という。

1) グロースシフト

```

選択肢は誤答しており、さらに資産運用に関する別の問題文を続けて出力しているように見えます。

#### LoRA w/ `completion_only`

```md
1) 0.48％
```

正しい選択肢を、指示に沿った形式で出力することができました。

#### LoRA w/ `packing` + `completion_only`

```md
2) 0.50％

固定利付債券の最終利回りは、課税所得者については、原則として、その元本部分が破棄されるまでの期間に応じて算出した利息相当額を控除して計算されるため、本問においては、「0.5
```

選択肢は誤答しており、さらにその回答に至る考え方を出力しているように見えます。

#### 参考：ベースモデル

```md
与えられた文章の空欄(　　)内に当てはまる数値およびその組み合わせとして正しいのは「2)」の0.50%と「3)」の0.53%となります。また、正解以外にも次のようなパターンがあり得ますので併せてご紹介いたします。

・1)0.48%→表面利率3%、残存期間2年の固定利付債券を額面100円当たり97円で購入し、残存期間終了時点における最終利回りを求めたいとき
・2)
```

モデルは前置きを挟んでから回答を出力しました。
[^3] の指示チューニングの過程で、このように丁寧な出力をするよう学習がされているのかもしれません。

---

上記は全体のうちごく一部の例なので、各モデルの傾向を完全に表しているものではありません。
しかし、今回の多肢選択肢問題のように期待する出力形式が厳密に定まっているようなタスクにおいて、`completion_only` を用いて応答部分についてのみモデルに学習させることで、指示追従性能が向上する可能性があるように感じます。

他方で、`packing` を用いて学習されたモデルは、上記の通り回答に続く形で別の出力も続けてしまう傾向が見られました。
この挙動は、`packing` によって構成された 1 つの系列に複数の問題・回答が含まれるデータを、モデルが学習したことが原因と思われます。
そのような学習がされた結果、モデルは推論時に回答に続けて別の問題も出力してしまい、正答テキストと完全一致せずに正答率が伸びなかったものと見受けられます。

`packing` を用いたモデルについては、上表に記載したもの以外のハイパーパラメータの組み合わせを含めて、今回の実験で試した範囲では `packing` を用いることで性能が向上することはありませんでした。
効果が得られなかった別の原因として、`packing` によって 1 ステップあたりにモデルが学習できるデータ数が増加したことで学習の性質が変わり、本実験のハイパーパラメータでは学習に悪い影響を及ぼしてしまった可能性もまた考えられます。

また、ACL2024 で発表された研究 [^11] によると、`packing` で複数のデータを 1 つの入力系列に含めてモデルに次に出現するトークンの確率を計算させる際に、データを跨いで過去のトークンを参照できないように系列内でデータ毎のマスキング (intra-document causal masking) を行うと、モデルの性能が改善することを報告しています。
本実験でのマスキングの工夫は、`completion_only` による損失計算の対象とするトークンをフィルタしているのみで、トークン出現確率については特に工夫を行っていません。
Intra-document causal masking を行うことで、上記の結果はまた違うものになる可能性があります。

![通常の Causal masking と Intra-document causal masking](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3488979/eb05d829-9408-e81b-393a-c925761b4bba.png)
図の出典：Yu Zhao, et al., Analysing The Impact of Sequence Composition on Language Model Pre-Training, ACL2024 発表資料.

[^11]: Yu Zhao, et al., [Analysing The Impact of Sequence Composition on Language Model Pre-Training](https://aclanthology.org/2024.acl-long.427). In Proceedings of the 62nd Annual Meeting of the Association for Computational Linguistics (Volume 1: Long Papers), pages 7897–7912, Bangkok, Thailand. Association for Computational Linguistics.

### Full Param. モデル

先の表で示した LoRA による Fine-tuning 時のハイパーパラメータをほぼ流用し、Full Param. モデルの Fine-tuning を実施しました。
学習率については、値が `5e-5` の時に Dev データでの正答率がやや高くなる結果が見られたので、Full Param. モデルの学習ではこの値を採用しました。
ただし、先述の通りハイパーパラメータ探索をしっかりと行っていないため、以下に示すパラメータは適切な値になっていない可能性があります。

|                                  | epoch\* | batch_size\* | lr\* | _train_loss_ | _dev_loss_ | _Accuracy (Greedy)_ | _Accuracy (Sampling)_ |
| -------------------------------- | ------- | ------------ | ---- | ------------ | ---------- | ------------------- | --------------------- |
| Full Param. w/ `completion_only` | 18      | 8            | 5e-5 | 0.031        | 0.854      | 0.396               | **0.422**             |

Dev データセットにおいて、Full Param. モデルは LoRA モデルと比べて正答率が 0.018 ポイント低くなりましたが、両者はほぼ同じ性能となりました。

## Test データにおける性能評価

上述の LoRA w/ `completion_only` と、 Full Param. w/ `completion_only` の 2 種類のモデルを用いて、Test データセットにおける性能評価を実施しました。
以後は、両者を単に LoRA、Full Param. とそれぞれ表します。

Dev データセット上では、いずれのモデルも Greedy よりも Sampling によるトークン予測を行った方が性能が高かったため、Test データセットでは Sampling によるトークン予測を用いました。

|              | 全体正答率 | ３級 FP（大問 1）正答率 | ３級 FP（大問 2）正答率 | ２級 FP 正答率 | 指示不追従件数 |
| ------------ | ---------- | ----------------------- | ----------------------- | -------------- | -------------- |
| ベースモデル | 0.000      | 0.00                    | 0.00                    | 0.00           | 117 / 117      |
| Full Param.  | 0.376      | 0.40                    | 0.61                    | 0.25           | 13 / 117       |
| LoRA         | 0.342      | 0.47                    | 0.57                    | 0.17           | 　 1 / 117     |

指示不追従件数とは、モデルが与えられた選択肢の中から回答を出力するよう指示を受けているにもかかわらず、選択肢のいずれにも完全一致しない文字列を出力した件数を表します。
いずれのモデルも、Fine-tuning によって正答率が向上し、指示追従性能も向上させることができましたが、前回の OpenAI Fine-tuning API を用いた実験結果 [^1] と比べると性能は劣る結果となりました。

特に正答率については、３級 FP（大問２）ではいずれのモデルもチャンスレート（=0.33）を超えることができましたが、３級 FP（大問１）のチャンスレート（=0.50）・２級 FP ではそれぞれのチャンスレート（=0.25）以下の正答率となりました。
一方で、Fine-tuning 済みモデルの指示不追従件数は少ないことから、Fine-tuning によってモデルに何らかのバイアスが組み込まれてしまい、その結果無作為に回答をする場合の期待値を下回ってしまった可能性が示唆されます。

## その他の日本語タスクにおける性能評価（llm-jp-eval による評価）

Fine-tuning によって、ベースモデルと比べた際に失われた能力があるかを検証するために、llm-jp-eval [^12]を用いた複数タスクにおける性能評価を行いました。
評価対象のタスク・データセットは、Nejumi LLM リーダーボード Neo [^13] に倣い、Natural Language Inference (NLI)・Question Answering (QA)・Reading Comprehension (RC)・Multiple Choice question answering (MC)・Entity Linking (EL)・Fundamental Analysis (FA)・Mathematical Reasoning (MR) の 7 種類のタスクを対象として、各データセットの test データ 100 問 における平均の zero-shot 性能を評価しました。

[^12]: <https://github.com/llm-jp/llm-jp-eval>
[^13]: <https://wandb.ai/wandb-japan/llm-leaderboard/reports/Nejumi-LLM-Neo--Vmlldzo2MTkyMTU0>

生成パラメータは llm-jp-eval のデフォルト設定値である以下を設定しました。

```json
{
  "top_p": 1.0,
  "top_k": 0.0,
  "temperature": 0.1,
  "repetition_penalty": 1.0
}
```

|              | Avg.  | NLI   | QA    | RC    | MC    | MR    | EL    | FA    |
| ------------ | ----- | ----- | ----- | ----- | ----- | ----- | ----- | ----- |
| ベースモデル   | 0.128 | 0.096 | 0.234 | 0.493 | 0.030 | 0.000 | 0.000 | 0.042 |
| Full Param.  | 0.065 | 0.000 | 0.112 | 0.151 | 0.160 | 0.000 | 0.000 | 0.034 |
| LoRA         | 0.237 | 0.380 | 0.338 | 0.670 | 0.230 | 0.000 | 0.000 | 0.041 |

上表の通り、Full Param. モデルは MC タスクでは 0.13 ポイントの性能向上が見られましたが、それ以外のタスクでは性能がゼロのまま、または低下しました。
先述の通り FP 技能検定は多肢選択肢問題であることから、似たタスクである MC の性能が向上したことは自然かなと思います。
他方、NLI タスク等において Full Param. モデルは、文脈によらず「1\)」や「１．」といった FP 技能検定で用いられる選択肢記号を出力してしまう事例が散見されました。
このことから、今回の実験では Full Param. モデルは FP 技能検定に過学習したものと思われます。

一方で、LoRA モデルは NLI, QA, RC, MC タスクにおいて、ベースラインのスコアを上回る結果となりました。
LoRA モデルの出力を観察したところ、ベースモデルが追従できなかった指示に対して、LoRA モデルは指示に沿った出力をすることができた事例が多数確認できました。
このことから、LoRA により Fine-tuning を行うことでベースモデルの持つ性能を保ちつつ、更新されたパラメータによって指示追従性能が向上したように見受けられます。

例として、以下のプロンプトに対する各モデルの出力結果を示します。

---

```md
以下は、タスクを説明する指示と、文脈のある入力の組み合わせです。要求を適切に満たす応答を書きなさい。

### 指示:
前提と仮説の関係をentailment、contradiction、neutralの中から回答してください。それ以外には何も含めないことを厳守してください。

制約：
- 前提から仮説が、時間関係上導出可能である場合はentailmentと出力
- 前提と仮説が両立しえない場合はcontradictionと出力
- そのいずれでもない場合はneutralと出力

### 入力:
前提：2001年12月25日18時以来、マーヴィンは将棋を楽しんでいる。現在、2001年12月25日12時である。
仮説：マーヴィンは2001年12月25日17時に将棋を楽しんだ。

### 応答:

```

なお、上記のプロンプトは llm-jp-eval が参照している評価用データセット[^14]のテキストに、評価用共通フォーマット[^15]を設定したものです。
このプロンプトが与えられた際に期待する出力は `contradiction` です。

[^14]: Tomoki Sugimoto, et al., [Jamp: Controlled Japanese Temporal Inference Dataset for Evaluating Generalization Capacity of Language Models](https://aclanthology.org/2023.acl-srw.8/). In Proceedings of the 61st Annual Meeting of the Association for Computational Linguistics (Volume 4: Student Research Workshop), pages 57–68, Toronto, Canada. Association for Computational Linguistics.
[^15]:　<https://github.com/llm-jp/llm-jp-eval/blob/main/DATASET.md>

#### ベースモデル

```md
前提から
```

トークンの出力がこれで終了しておりよくわかりませんが、与えられた前提について出力しようとした様子に見えます。
FP 技能検定の時と同様の振る舞いが見られたことから、やはり [^3] のモデルは回答をする前に前置きを挟もうとする傾向があるように感じます。

#### Full Param.

```md
1)
```

３級 FP 技能検定の第2問で用いられている選択肢記号が出力されていました。
この結果からも、Full Param. モデルは FP 技能検定に過学習してしまったことが伺えます。

#### LoRA

```md
contradiction
```

指示に沿いながら正しい回答を出力することができました。

---

## 所感

今回の実験を通じて、出力フォーマットを制御する（指示追従性を向上させる）目的でのオープン LLM の Fine-tuning、および `completion_only` の設定は有効そうだという知見が得られました。
一方で、正答率に関しては Fine-tuning により向上できたものの、その伸び幅にはまだ改善の余地がある結果となりました。

また、llm-jp-eval での評価において、LoRA で Fine-tuning されたモデルがベースモデルの性能を上回ることは予想外でした。
フルパラメータチューニングと比べて計算効率が良いという点が強調されがちな LoRA チューニングですが、今回の実験結果からは元の LLM の能力をある程度保つことができる一方で、指示学習によってより指示追従性能をより高める効果もあるのではないかと感じました。