🌊

OpenCALMをファインチューニング

2023/09/07に公開

先日、日本語LLMを色々と動かして試してみましたが、今回はファインチューニングしてみました
とはいえ自分で行うのは初めてなので、まずは理解し手順を明確にすることを目指します
https://zenn.dev/tk1/articles/30f92db431f735
https://zenn.dev/tk1/articles/b8894bbce9deab

目次

  1. モデル準備
  2. データセット準備
  3. トレーニング
  4. 結果
  5. 今後

モデル準備

サイズバリエーションの多いOpenCALMを使用します

GoogleColab上で課金GPUのA100を選択し、以下のコードを実行します

!pip install transformers
!pip install accelerate

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer

model_name = "cyberagent/open-calm-large"

model = AutoModelForCausalLM.from_pretrained(model_name, device_map="auto", torch_dtype=torch.float32)
tokenizer = AutoTokenizer.from_pretrained(model_name)

今回は830Mのあまり大きくないサイズ
(というよりこれ以上大きいとA100でもGPUメモリが足りなくなります)

また
torch_dtype=torch.float32
として32ビットのfloatを使わないと後のトレーニングで失敗します

データセット準備

https://huggingface.co/datasets/izumi-lab/llm-japanese-dataset
こちらで公開されている日本語データセットを使います
内容は「instruction」に質問文、「output」に回答が設定されたもの

!pip install datasets

from datasets import load_dataset
dataset_origin = load_dataset("izumi-lab/llm-japanese-dataset", revision="main")

これを学習用データセット1000件、評価用データセット100件に分けつつ、整形します
(かなり少ないけど増やすとメモリが足りません…)

train_size = 1000
eval_size = 100
token_max = 64
seed = 1

dataset = dataset_origin['train'].select(range(0, train_size + eval_size))

def data_arrange(data):
  input = 'Q:' + data['instruction'] + '\nA:' + data['output']
  tokenized = tokenizer(input, return_tensors='pt', padding='max_length', truncation=True, max_length=token_max)
  input_q = 'Q:' + data['instruction'] + '\nA:'
  tokenized_q = tokenizer(input_q, return_tensors='pt', padding='max_length', truncation=True, max_length=token_max)
  labels = tokenized['input_ids'][0].clone()
  for i in range(0, token_max):
    if not tokenized_q['input_ids'][0][i] == 1:
      labels[i] = -100
  return {
      'input': input,
      'input_ids': tokenized['input_ids'][0],
      'attention_mask': tokenized['attention_mask'][0],
      'labels': labels
    }
dataset_tokenized = dataset.map(
    data_arrange,
    remove_columns=['instruction', 'output']
)

dataset_split1 = dataset_tokenized.train_test_split(train_size=train_size, seed=seed)
dataset_train = dataset_split1['train']
dataset_eval = dataset_split1['test']

整形の詳細は以下の通り

  • 最終的なカラムは「input」「input_ids」「attention_mask」「labels」の4つ
  • 「Q:<質問文>\nA:<回答>」の形のテキストを「input」に設定
  • tokenizerは「padding='max_length', truncation=True, max_length=token_max(=64)」と設定することで全トークンを同じ長さにし、トークン化したテキストを「input_ids」に、paddingした部分の情報を「attention_mask」に設定
  • トークン化したテキストのうち「Q:<質問文>\nA:」にあたる部分を「-100」とすることで評価時に無視するよう整形したものを正解ラベルとして「labels」に設定

具体的には以下のような中身になっています

print(dataset_train[0])

{'input': 'Q:馬の肉を使ったお鍋は「桜鍋」ですが、猪の肉を使ったお鍋のことを、同じく花を使って何鍋というでしょう?\nA:ボタン鍋', 'input_ids': [50, 27, 1368, 16725, 5243, 316, 8451, 257, 308, 4743, 8451, 309, 1097, 245, 15123, 16725, 5243, 316, 8451, 7846, 245, 6542, 1746, 3711, 1016, 8451, 495, 1693, 32, 186, 34, 27, 6522, 8451, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 'labels': [-100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, 6522, 8451, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]}

トレーニング

以下の内容で学習を実行します

from transformers import TrainingArguments, Trainer

learning_rate = 5e-5
epochs = 1
batch_size = 10
log_steps = train_size / batch_size / 5
seed = 1

model.train()

training_args = TrainingArguments(
    output_dir="trained_model",
    learning_rate=learning_rate,
    num_train_epochs=epochs,
    evaluation_strategy='steps',
    per_device_train_batch_size=batch_size,
    logging_steps=log_steps,
    eval_steps=log_steps,
    seed=seed
)

trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=dataset_train,
    eval_dataset=dataset_eval
)

train_result = trainer.train()

1000の学習データに対して、バッチ1つを10データ、エポック数を1とし、全体で100ステップとなります

データ数が少ないので30秒くらいで終わりました

結果

損失の遷移

実行中にも損失の状況など表示されますが、今回は学習後のログから損失をグラフにします

log_train = [log for log in trainer.state.log_history if 'loss' in log]
log_eval = [log for log in trainer.state.log_history if 'eval_loss' in log]
log_final = [log for log in trainer.state.log_history if 'train_runtime' in log]

import matplotlib.pyplot as plt

x = np.arange(log_steps, train_size * epochs / batch_size + log_steps, log_steps)

y1 = []
for i in range(len(log_train)):
  y1.append(log_train[i]['loss'])

y2 = []
for i in range(len(log_eval)):
  y2.append(log_eval[i]['eval_loss'])

fig, ax = plt.subplots()
ax.set_xlabel('step')
ax.set_ylabel('loss')
ax.set_ylim(0, 1)
ax.grid()
ax.plot(x, y1, color='red', label='train loss')
ax.plot(x, y2, color='blue', label='eval loss')
ax.legend(loc=0)
fig.tight_layout()
plt.show()

出力されたグラフがこちら
グラフ
学習と評価の両データで、微かにですが損失が下がっていってます
なお、100ステップを超えると学習側の損失が急激に下がり過学習になっていきます

※こちらちょっと低すぎなため間違っている気がします
perplexityに変換すると1より少し高い程度…の割に次の結果が良くはない…

日本語質問

学習前後のモデルに様々な日本語の質問をしてみます
なお、今回の学習では回答後に意味のない文章を続けることを抑制できていなそうなのでその部分を除外し、「A:」の直後に正解単語が出力されていれば正解とします

まずは前回と同じ質問

  1. Q:ドラゴンボールの作者は誰?\nA: ※正解:鳥山明
    • 学習前 A:セル・ワールド崩壊編の作者〜〜
    • 学習後 A:鳥山明ひらくのシリーズ〜〜
    • 結果:「A:」の直後に「鳥山明」と出力されているので、学習後モデルのみ正解
  2. Q:楽天の社長は誰?\nA: ※正解:三木谷浩史
    • 学習前 A:三木谷浩史氏。現会長で〜〜
    • 学習後 A:三木谷浩史重点的に新事業を〜〜
    • 結果:どちらも正解
  3. Q:パンダはどこの国の国獣?\nA: ※正解:中国
    • 学習前 A:中国・四川省の〜〜
    • 学習後 A:中国徴収を担当している〜〜
    • 結果:どちらも正解
  4. Q:相対性理論を提唱したのは誰?\nA: ※正解:アインシュタイン
    • 学習前 A:アインシュタイン(1879-1955)です〜〜
    • 学習後 A:アインシュタインはりんこのみ〜〜
    • 結果:どちらも正解
  5. Q:ドラえもん映画が最初に放映されたのはいつ?\nA: ※正解:1980年3月
    • 学習前 A:1990年(平成2)4月5日〜〜
    • 学習後 A:1969年(昭和44)年戦争真っ只中の高〜〜
    • 結果:どちらも不正解
  6. Q:日本で最も店舗数の多いコンビニは?\nA: ※正解:セブンイレブン
    • 学習前 A:セブンイレブン(全店)〜〜
    • 学習後 A:セブンイレブン少なにグループ企業を〜〜
    • 結果:どちらも正解
  7. Q:日本の都道府県で最も面積が広いのは?\nA: ※正解:北海道
    • 学習前 A:沖縄・北海道です。この2つの県は〜〜
    • 学習後 A:沖縄徴収分厚い自治体の〜〜
    • 結果:どちらも不正解
  8. Q:日本の通貨単位は何?\nA: ※正解:円
    • 学習前 A:日本円(Japanese yen)といいます。
    • 学習後 A:円デシを持っていてもおかしくは〜〜
    • 結果:学習後モデルのみ正解

ここからは評価データに含まれてるものを使用しています

  1. Q:動く彫刻をモビールというのに対して、動かない彫刻を何というでしょう?\nA: ※正解:スタビール
    • 学習前 A:そうですね。やっぱり「動かない」〜〜
    • 学習後 A:ピンキング・ホーン向上部 〜〜
    • 結果:どちらも不正解
  2. Q:ミカン、レモン、ダイダイなどを総称して何類というでしょう?\nA: ※正解:柑橘類
    • 学習前 A:正解は「ミカン」でした〜〜
    • 学習後 A:柑橘類のどれ座っても〜〜
    • 結果:学習後モデルのみ正解
  3. Q:1837年から1901年まで、イギリスの歴代君主の中で最も長い64年間にわたってその地位にあった女王といえば誰でしょう?\nA: ※正解:ビクトリア女王
    • 学習前 A:ヘンリー8世です〜〜
    • 学習後 A:エリザベス2世ひいきの〜〜
    • 結果:どちらも不正解
  4. Q:校倉造でおなじみの正倉院が置かれている寺はどこでしょう?\nA: ※正解:東大寺
    • 学習前 A:興福寺(奈良)ではなく、〜〜
    • 学習後 A:興福寺(こうふくじ)広げる為政者によって〜〜
    • 結果:どちらも不正解
  5. Q:白河の関や天香具山など、和歌に詠まれて有名になった地名や名所のことを何というでしょう?\nA: ※正解:歌枕
    • 学習前 A:熊野古道とか名勝地のことですね。〜〜
    • 学習後 A:嵯峨野(さがのち)頌するところ〜〜
    • 結果:どちらも不正解
  6. Q:空気中と水中で、音はより速く伝わるのはどちらでしょう?\nA: ※正解:水中
    • 学習前 A:気圧が低い方が速い。ただし〜〜
    • 学習後 A:音波越えオーデマピゲスーパー〜〜
    • 結果:どちらも不正解
  7. Q:『風立ちぬ』という小説を書いたのは堀辰雄ですが、『風立ちぬ』という曲を歌ってたアイドルは誰でしょう?\nA: ※正解:松田聖子
    • 学習前 A:そうですね。やっぱり、この〜〜
    • 学習後 A:中島みゆきはなによりも〜〜
    • 結果:どちらも不正解
  8. Q:「オペレーティング・システム」という言葉を略した、コンピュータの基本ソフトをあらわす用語は何でしょう?\nA: ※正解:OS
    • 学習前 A:「オペレーティングシステム(Operating System)」といいます〜〜
    • 学習後 A:マイクロソフト越え〜〜
    • 結果:どちらも不正解
  9. Q:昭和47年に『パチンコ』という作品でデビューした、タレントとしても活躍する漫画家は誰でしょう?\nA: ※正解:蛭子能収
    • 学習前 A:故・水木しげる先生です〜〜
    • 学習後 A:山上たつひこ向上委員会〜〜
    • 結果:どちらも不正解
  10. Q:お盆や正月に、奉公人が休暇をもらって郷里へ帰る期間のことを何といったでしょう?\nA: ※正解:薮入り
    • 学習前 A:盆・正月の帰省。またその期間中に〜〜
    • 学習後 A:盆休みを殊更あげたものか〜〜
    • 結果:どちらも不正解

結果としては…若干正解数が増えたものの精度はまだまだ低い…といった所でした

今後

今回はまず理解することを目標としましたが、いくつかやりきれなかった部分がありました

  1. GPUメモリ容量の都合で、小さいモデルかつ少量のデータでの学習しか出来なかった
    →LoRA等の手法を取り入れたり、設定値の調整を試してみる

  2. 学習させる方向が不明瞭で、結果が見えづらった
    →データ整形等を工夫して、正確かつシンプルに回答できるよう学習させたい

  3. 損失をグラフにしてみたが、(おそらく)間違った値になってしまった
    →正しく見る+perplexity等別の値もあわせて結果を可視化する

次回はこのあたりに注意して再チャレンジしてみようと思います

Discussion