日本語版LIMAデータセットでopen-calm-7bをLoRAファインチューニングしてチャットボットを作成してみた
現代の自然言語処理(NLP)タスクにおいて、事前学習モデルは非常に重要な役割を果たしています。これらのモデルは、大量のテキストデータを使用してトレーニングされ、一般的な言語理解能力を獲得するための基礎を提供します。しかし、日本語のようなリソースが限られた言語においては、高品質の事前学習モデルの作成には課題があります。そのような中で近年、課題に取り組むために活用できるサイバーエージェントのopen-calm-7bが公開されています。
そこで、LoRA(Low-Rank Adaptation)と呼ばれる手法を用いて、open-calm-7bモデルを日本語LIMAデータセットでLoRAファインチューニングすることで、その精度向上を試みました。
LIMAの論文内で用いられたデータセットは、少数精鋭のデータセットとして知られています。
このデータセットを日本語訳したデータセットについては、こちらの記事を参照してください。
以下の記事のような学習方法もあります。
(2023/07/20追記)
「Llama-2」が公開されました!
- open-calm-7bのLoRAファインチューニング方法
- モデルの推論結果
ライブラリのインストール
今回用いるライブラリをインストールします。
Pytrochついてはご自分の環境に合わせて別でインストールしてください。
!pip install peft
!pip install transformers
!pip install datasets
!pip install accelerate
!pip install bitsandbytes
この中でbitsandbytesについては特にエラーが出る原因となりやすいと感じています。
学習が成功したバージョンを載せておきます。
accelerate==0.21.0.dev0
bitsandbytes==0.39.0
datasets==2.11.0
peft==0.4.0.dev0
transformers==4.27.4
上記以外にも私がもともと入れていたライブラリがあるかもしれないので随時入れてください。
実装コードの解説
以下の記事を参考にして改良を加えています。
ありがとうございました。
モデルとトークナイザーの読み込み
まずは、モデルとトークナイザーを読み込んでおきます。
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
model_name = "cyberagent/open-calm-7b"
# トークナイザーの準備
tokenizer = AutoTokenizer.from_pretrained(model_name)
# モデルの準備
model = AutoModelForCausalLM.from_pretrained(
model_name,
load_in_8bit=True,
device_map="auto",
torch_dtype=torch.float16
)
# 複数のGPUを使う場合
if torch.cuda.device_count() > 1:
model.is_parallelizable = True
model.model_parallel = True
複数のGPUを使う場合の設定もここでしておきます。
複数のGPUを使う方はこのコードがないと、学習時にRuntimeError: mat1 and mat2 shapes cannot be multiplied (722x28 and 27x4096)
のようなエラーが出てしまいます。
データセットの読み込み
次にLIMA-JAデータセットを取得します。
from datasets import load_dataset
dataset = load_dataset('zan/lima-ja')
中身はこんな感じです。
>>> dataset["train"][0]
{'conversations': ['脳細胞は移動することができるのでしょうか? 移動とは、遠距離の移動(可能ならば脳内のみで)を指します。',
'この質問は比較的広範であり、脳は単にニューロンだけでなく、グリア細胞(支持細胞)や前ミトーシスの神経幹細胞も含まれることを考慮する必要があります。さらに、重要な同僚科学者たちが指摘しているように、発生段階も非常に重要であり、発達中の胎児の脳は成人の脳とは非常に異なります。\n\nしかし、さまざまな出版物を精査した結果、この質問の答えは実際には非常に単純であることがわかりました。結論として、脳細胞は移動します。\n\n成人の脳では、グリア細胞が脳内を移動します(Klämbt、2009年)。グリア細胞は多くの機能に関与していますが、移動するグリア細胞の注目すべき例は、標的の軸索を見つけるために比較的長距離を移動し、それに巻きついて絶縁性のミエリン鞘を形成するオリゴデンドロサイトです(TsaiとMiller、2002年)。\n\n神経幹細胞は損傷に対応して長距離を移動します(Imitola et al.、2004年)し、特定の幹細胞の位置(例えば、海馬や脳室下帯)から他の領域に移動します(Clarke、2003年)。\n\nミトーシス後でありながら未分化のニューロンは、魚(Scott et al.、2012年)や哺乳類、および非人猿でも成人の脳内を移動することが示されています(Sawada et al.、2011年)。\n\n驚くことではありませんが、グリア細胞、幹細胞、およびニューロンは胎児の発生中も移動します。特に、末梢機能を果たすために運命づけられた分裂後のニューロンは、神経堤から標的の位置まで比較的長い距離を移動しなければなりません(Neuroscience、第2版、Neuronal Migration)。'],
'source': 'stackexchange'}
内容はこんな感じです。
>>> dataset
DatasetDict({
train: Dataset({
features: ['conversations', 'source'],
num_rows: 1030
})
test: Dataset({
features: ['conversations', 'source'],
num_rows: 300
})
})
さらに今回は、ここでデータの長さを調整したいので2048トークンより長いデータは除きます。(学習時に2048トークン以上はエラーが出るためです。他の部分で処理をすれば大丈夫ですがここでやっておきます。)プロンプトのトークン分も除くべきですが、エラーは出なかったのでこのようにしています。
data_train = []
for i, item in enumerate(dataset["train"]):
token = 0
for j, c in enumerate(item["conversations"]):
token += len(tokenizer.tokenize(c))
if token <= 2048:
data_train.append(item)
どれだけ減ったか見てみます。
>>> len(data_train)
1006
少し減っていますね。
Datasetの準備
Datasetクラスを定義します。
from torch.utils.data import Dataset
from tqdm import tqdm
class ConversationDataset(Dataset):
def __init__(self, json_list, tokenizer):
# ConversationDatasetの初期化関数
# json_list: JSON形式のデータリスト
# tokenizer: トークナイザー
self.tokenizer = tokenizer
self.features = []
all_texts_list = []
# json_list内の各アイテムに対して処理を実行
for item in tqdm(json_list):
input_list = item["conversations"][:-1]
output_text = item["conversations"][-1]
input_text = "あなたはchatbotです。userと会話を行ってください。\n"
# 入力リストの要素ごとに処理を実行
for i, item in enumerate(input_list):
# インデックスが偶数の場合はuserの発言を追加
if i % 2 == 0:
input_text += f"\n### user:\n{item}\n"
else:
# インデックスが奇数の場合はchatbotの発言を追加
input_text += f"\n### chatbot:\n{item}\n"
# chatbotの発言の欄を追加
input_text += "\n### chatbot:\n"
# 全てのテキストを結合し、トークナイザで処理
all_text = input_text + output_text + self.tokenizer.eos_token
all_texts_list.append(all_text)
# トークナイズされたテキストのリストを特徴量として保存
self.features = [
tokenizer(
text, padding=False, truncation=True, max_length=2048
) for text in tqdm(all_texts_list)
]
def __len__(self):
# データセットのサイズを返す
return len(self.features)
def __getitem__(self, idx):
# 指定したインデックスの特徴量を返す
return self.features[idx]
# インスタンスを作成
train_dataset = ConversationDataset(data_train, tokenizer)
このコードは、ConversationDataset
という名前のデータセットクラスを定義しています。主な機能は以下の通りです。
__init__
: クラスの初期化関数です。引数としてJSON形式のデータリスト、トークを受け取ります。与えられたデータリストを処理し、トークナイズされたテキストのリストとして特徴量を作成します。__len__
: データセットのサイズ(特徴量の数)を返します。__getitem__
: 指定したインデックスの特徴量を返します。
具体的な処理は以下の通りです。
__init__
関数では、与えられたJSONリストの各アイテムに対して以下の処理を実行します。input_list
には会話の最後の要素を除いた要素のリストが格納されます。output_text
には会話の最後の要素が格納されます。input_text
には最初のメッセージとして”あなたはchatbotです。userと会話を行ってください。\n”が格納されます。input_list
内の要素を順番に取り出し、偶数番目の要素は”user”の発言、奇数番目の要素は”chatbot”の発言としてinput_text
に追加されます。- 最後に、”chatbot”の発言の欄を追加します。
- 全てのテキストを結合し、トークナイザで処理されたテキストを
all_texts_list
に追加します。 all_texts_list
内の各テキストに対してトークナイズを行い、特徴量としてself.features
に保存します。
__len__
関数では、特徴量の数を返します。__getitem__
関数では、指定したインデックスの特徴量を返します。
データの中身は以下のようになります。
>>> train_dataset[0]
{'input_ids': [7764, 1674, 899, 50143, 358, 247, 43302, 254, 5060, 44981, 247, 186, 186, 39843, ... 0],
'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]}
collatorの準備
Collatorクラスを準備します。
from transformers import DataCollatorForLanguageModeling
collator = DataCollatorForLanguageModeling(tokenizer, mlm=False)
DataCollatorForLanguageModelingについてはData Collator (huggingface.co)をご覧ください。
学習を安定させる処理
学習を安定させるために一部をfloat32にキャストしたりレイヤーをフリーズしたりしています。
import torch.nn as nn
# モデルのパラメーターごとに、勾配の計算を無効にする
for param in model.parameters():
param.requires_grad = False
# パラメーターの次元が1次元の場合、データ型をtorch.float32に変換する
if param.ndim == 1:
param.data = param.data.to(torch.float32)
# モデルに勾配チェックポイントを有効化する
model.gradient_checkpointing_enable()
# モデルの入力に対する勾配の計算を有効化する
model.enable_input_require_grads()
# CastOutputToFloatという名前のクラスを定義し、nn.Sequentialクラスを継承する
class CastOutputToFloat(nn.Sequential):
# forwardメソッドをオーバーライドし、入力データxに対してsuper()を呼び出して順伝搬を実行し、
# 結果をtorch.float32のデータ型に変換して返す
def forward(self, x):
return super().forward(x).to(torch.float32)
# モデルのembed_out属性にCastOutputToFloatクラスのインスタンスを代入する
model.embed_out = CastOutputToFloat(model.embed_out)
LoRAの設定
LoRAにおけるパラメータなどを設定できます。
from peft import LoraConfig, get_peft_model, prepare_model_for_int8_training, TaskType
# LoRAのパラメータ
lora_config = LoraConfig(
r= 8,
lora_alpha=16,
target_modules=["query_key_value", "dense", "dense_h_to_4h", "dense_4h_to_h"],
lora_dropout=0.05,
bias="none",
task_type=TaskType.CAUSAL_LM
)
# モデルの前処理
model = prepare_model_for_int8_training(model)
# LoRAモデルの準備
model = get_peft_model(model, lora_config)
# 学習可能パラメータの確認
model.print_trainable_parameters()
LoraConfigで設定できる引数の説明はこちらを参照してください。
モデル構造を表示する
>>> model
PeftModelForCausalLM(
(base_model): LoraModel(
(model): GPTNeoXForCausalLM(
(gpt_neox): GPTNeoXModel(
(embed_in): Embedding(52224, 4096)
(layers): ModuleList(
(0-31): 32 x GPTNeoXLayer(
(input_layernorm): LayerNorm((4096,), eps=1e-05, elementwise_affine=True)
(post_attention_layernorm): LayerNorm((4096,), eps=1e-05, elementwise_affine=True)
(attention): GPTNeoXAttention(
(rotary_emb): RotaryEmbedding()
(query_key_value): Linear8bitLt(
in_features=4096, out_features=12288, bias=True
(lora_dropout): ModuleDict(
(default): Dropout(p=0.05, inplace=False)
)
(lora_A): ModuleDict(
(default): Linear(in_features=4096, out_features=8, bias=False)
)
(lora_B): ModuleDict(
(default): Linear(in_features=8, out_features=12288, bias=False)
)
(lora_embedding_A): ParameterDict()
(lora_embedding_B): ParameterDict()
)
(dense): Linear8bitLt(
in_features=4096, out_features=4096, bias=True
(lora_dropout): ModuleDict(
(default): Dropout(p=0.05, inplace=False)
)
(lora_A): ModuleDict(
(default): Linear(in_features=4096, out_features=8, bias=False)
)
(lora_B): ModuleDict(
(default): Linear(in_features=8, out_features=4096, bias=False)
)
(lora_embedding_A): ParameterDict()
(lora_embedding_B): ParameterDict()
)
)
(mlp): GPTNeoXMLP(
(dense_h_to_4h): Linear8bitLt(
in_features=4096, out_features=16384, bias=True
(lora_dropout): ModuleDict(
(default): Dropout(p=0.05, inplace=False)
)
(lora_A): ModuleDict(
(default): Linear(in_features=4096, out_features=8, bias=False)
)
(lora_B): ModuleDict(
(default): Linear(in_features=8, out_features=16384, bias=False)
)
(lora_embedding_A): ParameterDict()
(lora_embedding_B): ParameterDict()
)
(dense_4h_to_h): Linear8bitLt(
in_features=16384, out_features=4096, bias=True
(lora_dropout): ModuleDict(
(default): Dropout(p=0.05, inplace=False)
)
(lora_A): ModuleDict(
(default): Linear(in_features=16384, out_features=8, bias=False)
)
(lora_B): ModuleDict(
(default): Linear(in_features=8, out_features=4096, bias=False)
)
(lora_embedding_A): ParameterDict()
(lora_embedding_B): ParameterDict()
)
(act): GELUActivation()
)
)
)
(final_layer_norm): LayerNorm((4096,), eps=1e-05, elementwise_affine=True)
)
(embed_out): CastOutputToFloat(
(0): Linear(in_features=4096, out_features=52224, bias=False)
)
)
)
)
Trainerの準備
transformersのTrainerを利用します。
import transformers
peft_name = "lora-calm-7b"
output_dir = "lora-calm-7b-output"
# トレーナーの準備
trainer = transformers.Trainer(
model=model,
train_dataset=train_dataset,
args=transformers.TrainingArguments(
num_train_epochs=1,
learning_rate=3e-4,
logging_steps=20,
output_dir=output_dir,
report_to="none",
save_total_limit=1,
push_to_hub=False,
auto_find_batch_size=True,
),
data_collator=collator,
)
Trainerの引数の詳しい説明はこちらを参照してください。
TrainingArgumentsについてはこちらです。
モデルの学習と保存
後は学習するだけです。
# 学習の実行
model.config.use_cache = False
trainer.train()
model.config.use_cache = True
# LoRAモデルの保存
trainer.model.save_pretrained(peft_name)
学習したモデルをそのまま推論させたい場合は、model.eval()
を実行して「推論させる」まで飛んでください。
保存したアダプターを読み込む
保存したアダプターを読み込んでモデルを準備します。
from peft import PeftModel
model_name = "cyberagent/open-calm-7b"
# トークンナイザーの準備
tokenizer = AutoTokenizer.from_pretrained(model_name)
# モデルの準備
model = AutoModelForCausalLM.from_pretrained(
model_name,
load_in_8bit=True,
device_map="auto",
)
peft_name = "./lora-calm-7b"
# LoRAモデルの準備
model = PeftModel.from_pretrained(
model,
peft_name,
device_map="auto",
torch_dtype=torch.float16
)
# 評価モードへ
model.eval()
推論させる
まずはプロンプトを作る関数を作成します。
def generate_prompt(input_text):
conversation_text = "あなたはchatbotです。userと会話を行ってください。\n"
conversation_text += f"\n### user:\n{input_text}\n"
conversation_text += "\n### chatbot:\n"
return conversation_text
次に入力文を作成します。ここで色々と変えてください。
input_text=""
推論させます。
input_ids = tokenizer(generate_prompt(input_text), return_tensors="pt", truncation=True).input_ids.cuda()
outputs = model.generate(
input_ids=input_ids,
max_new_tokens=1024,
do_sample=True,
temperature=0.6,
top_p=0.8,
top_k=40,
no_repeat_ngram_size=2,
)
outputs = outputs[0].tolist()
# EOSトークンにヒットしたらデコード完了
if tokenizer.eos_token_id in outputs:
eos_index = outputs.index(tokenizer.eos_token_id)
decoded = tokenizer.decode(outputs[:eos_index])
# レスポンス内容のみ抽出
sentinel = "### chatbot:"
sentinelLoc = decoded.find(sentinel)
if sentinelLoc >= 0:
print(decoded[sentinelLoc+len(sentinel):])
else:
print('Warning: Expected prompt template to be emitted. Ignoring output.')
print("------------------")
print(tokenizer.decode(outputs))
else:
print('Warning: no <eos> detected ignoring output')
print("------------------")
print(tokenizer.decode(outputs))
色々と聞いてみた
出力が良かったもの、悪かったものがあるのでどちらも見てみましょう。
好きな日本のアニメはありますか?
好きな日本のアニメは何ですか?
私は『NARUTO -ナルト-』と『ドラゴンボールZ』が大好きです! 『キャプテン翼』と「聖闘士星矢」も大好きです。
いい感じですね。
同じ質問に対する応答をいくつか見てみます。
私は、日本の「魔法少女」シリーズ、特に「マジカル・ガール」が好きです! このシリーズは、魔法の世界と魔法の存在、そして魔法を使う人々について、私たちに考えさせます。このシリーズには、物語とキャラクターが魅力的で、キャラクターが魔法と人々のために戦うことを誇りに思っているように感じます。私は魔法を信じていますが、それが魔法であるかどうかはわかりません。しかし、私はそれが好きであり、私が見る魔法は素晴らしいです!(そして、それは私の魔法への信頼です!) 「魔法使いサリー」は、私が魔法について知る前に、最初に私が見た日本のテレビシリーズでした。私の好きなキャラクターは、サリーです(そしてサリーも好きです)。サリーは大人になり、結婚します。結婚生活は非常に複雑であり(サリーには2人の息子がいます)、結婚生活はサリーにとって非常に困難です。(サリーの夫はサリーを非常に尊敬していますが、サリーはしばしばそれを誇張します)。結婚は悲劇であり、「魔法使いサリーの結婚」というタイトルは誇大広告のように聞こえますが、それでもサリーの幸せな結婚物語です:) サリーを演じた声優は林原めぐみです。「サリーとサリーの妻」と「サリーの母」を演じました。サリーの両親は「魔法の使者」であり「サリーが魔法を使える」という魔法を知っています。サリーに魔法を教えるのはサリーの父です、しかしサリーの父親は魔法の使用に反対しています。魔法が使用されるたびに、その家族、友人、隣人、および他の人々(魔法を使用する人々)は混乱します(サリーでさえ混乱しています)。魔法が使えない人々は、しばしば魔法が彼らの生活を破壊することを知ります。彼らは魔法を禁止するために戦い、彼らの家を魔法から守ります(彼らの親は彼らの子供が魔法を使わないことを保証します)、しかし魔法が禁止された後、彼らは再び魔法を使い始めます。そして彼らはお互いに魔法をかけます(彼らはサリーのために魔法を使います)。サリーの両親もサリーの助けを必要としています。彼らには魔法が必要であり魔法が必要です。彼らが魔法を使用するとき、他の人々は彼らのために戦います。
長いですね。
私は日本のテレビアニメが大好きで、よく見ているので、いくつかのお気に入りをご紹介します。
紹介しますと言ってるけど生成が止まっています。
私は、日本のテレビアニメ『名探偵コナン』と『犬夜叉』が好きです(両方とも原作は漫画です)。 『コナン』(私は『コナ』と呼んでいますが、日本では『灰原』や『工藤』と呼ばれることもあります)は、私が最初に読んだ日本のテレビ番組です。『名探偵』は名探偵コナンのテレビシリーズです(『名探偵』)。私は「灰原はなぜこんなに人気なのか?」という疑問を抱いています。私はなぜ灰元がこんなに人気があると思いますか?
日本語としておかしい応答もします。
データサイエンティストになるために必要なスキルを5個挙げて
データサイエンティストになるために必要なスキルを5つ挙げて。
1. 数学とプログラミングのスキル
2. データサイエンスのスキル(特にデータマイニング)
3. ビジネススキル
4. システム思考のスキルと問題解決のスキル。これらはすべて、データサイエンスの分野に関連するスキルです
5. 英語
* これらは、データ分析に関連する主要なスキルの一部です
とりあえず5個は挙げてくれました。
他にも次のような応答がありました。
1. コンピュータサイエンスのスキル
2. データサイエンスのツールとスキル
3. ビジネススキル(マーケティング、販売、ファイナンス、人事など)
4. システム設計のスキル(プログラミング、システム思考など)、 5. リサーチスキルとデータサイエンス
* プログラミングのスキルとシステム設計スキルが、データアナリストになるためのスキルセットの一部です
* システム思考のスキルがデータアナリティクスのスキルセットの一部である場合、それらはデータ専門家になるためには必須ではありません
* リーダーシップスキル、コミュニケーションスキルも必要です
* データアナリストは、データ分析スキルだけでなく、ビジネススキルも必要とします
* ビジネスアナリスト、マーケティングアナリストまたは販売アナリストなど、他のスキルもあります
** これらのスキルセットは、特定の職業に関連するものではありません。データ専門家は、これらのスキルを関連付ける必要があります
1.データサイエンスのスキル:データ科学は、データに関する基本的な知識、アルゴリズム、およびツールを提供するため、非常に重要です。データ科学者になるためには、数学、統計、コンピュータサイエンス、プログラミングのスキルが必要です。また、機械学習と人工知能のスキルも必要です。
pythonで1からnの数字を足した値を出力する関数を作成して
pythonで1からnの数字を足した値を出力する関数を作成してください。
結果から言うとコードを生成する系はどれも無理でした。やはり事前学習の時点で学習していない領域については無理そうです。
1、2、3、4、5、6、7、8、9、10、11、12、13、14、15、16、17、18、19、20、21、22、23、24、25、26、27、28、29、30、31、32、33、34、35、36、37、38、39、40、41、42、43、44、45、46、47、48、49、50、51、52、53、54、55、56、57、58、59、60、61、62、63、64、65、66、67、68、69、70、71、72、73、74、75、76、77、78、79、80、81、82、83、84、85、86、87、88、89、90、91、92、93、94、95、96、97、98、99、100、101、102、103、104、105、106、107、108、109、110、111、112、113、114、115、116、117、118、119、120、121、122、123、124、125、126、127、128、129、130、131、132、133、134、135、136、137、138、139、140、141、142、143、144、145、146、147、148、149、150、151、152、153、154、155、156、157、158、159、160、161、162、163、164、165、166、167、168、169、170、171、172、173、174、175、176、177、178、179、180、181、182、183、184、185、186、187、188、189、190、191、192、193、194、195、196、197、198、199、200、201、202、203、204、205、206、207、208、209、210、211、212、213、214、215、216、225、227、228、230、231、232、 233 、234 、235、240、251、252、255、256、 257 、258 、 259 、260、 261 、262、264、265、 276 、277 以下のコードをsay_notes.pyという名前で保存してください:
コードを生成して欲しいのですが…してくれませんね。
1-2-3-4-5-6-7-8-9-10-11-12-13-14-15-16-17-18-19-20-21-22-23-24-25-26-27-28-29-30-31-32-33-34-35-36-37-38-39-40-41-42-43-44-45-46-47-48-49-50-51-52-53-54-55-56-57-58-59-60-61-62-63-64-65-66-67-68-69-70-71-72-73-74-75-76-77-78-79-80-81-82-83-84-85-86-87-88-89-90-91-92-93-94-95-96-97-98-99-100-101-102-103-104-105-106-107-108-109-110-111-112-113-114-115-116-117-118-119-120-121-122-123-124-125-16-127-128-129-130-131-132-133-134-135-136-137-138-139-140-141-142-143-144-145-146-147-148-149-150-151-152-153-154-155-156-157-158-159-160-161-162-163-164-165-166-167-168-169-170-171-172-173-174-175-176-177-178-179-180-181-182-183-184-185-186-187-188-189-190-191-192-193-194-195-196-197-198-199-200-201-202-203-204-205-206
怖いですね。
算数の問題を解かせてみる
例を参考にして足し算を行ってください。
例:10+12=22
17+12=
10と12を足すには、10に12を、12に10を足してください。
算数は難しそうです。
10と12を足して22にすることは、2つの足し合わせで2つの要素を持つ2項演算子(&)を使うことによって簡単にできます。
10と12を足して22にすることはできますか? もしそうなら、なぜですか?
まとめ
今回は、少数精鋭と言われているデータセットを使ってopen-calm-7bをファインチューニングしてみました。
今回紹介した出力例以外にもいろいろ試しましたが結構よかったです。
1000件でこのくらいの性能にできることが分かったので、こちらの方向で色々とやっていきたいと思います。
コメント