はじめに
本記事はLLMについてGW(ゴールデンウイーク)期間の 5/1~5/6 で私が学んだことをアウトプットする記事です。
そのため正確な理解・説明でない場合や不十分である可能性があります。
もしもご参考になる場合は十分にご注意いただくようお願いいたします。
今回の学習のゴール設定としては自分の想像したFinetuning手法を実装できるようになる土台を作ることにあります。(当然のことながら精度は求めてません。もちろん精度が上がる個人的手法を偶然にでも見つけることができたなら非常にハッピーですが、1週間程度の期間では非現実的です。)
まず初めに、今回の学習にとって必要不可欠であった書籍・動画について感謝を述べると同時にご紹介させていただきます。
Build a Large Language Model (From Scratch)
本書籍はSebastian Raschka氏によって書かれた書籍です。
はじめにLLMとその歴史、Transformerアーキテクチャについて詳しく解説があり、次いでGPT-2を例としたDecoderモデルの概要・作成手順、最後にFinetuningの手法について非常に細かく解説があります。
本書籍の素晴らしい点はいくつもありますが、個人的に最も素晴らしいと思った部分は細かく分割された段階的説明がされていることです。
そのためよく目にするネット上のTransformer・Decoderについての説明では、例えば「Masked Multi-Head Attentionを通って、正規化して・・・」のような説明であり、初学者の身からするとかなり複雑で難しく、理解をするまでに大量の時間を要します。(それらの説明が良くないという訳ではありません。むしろ専門的な知識を有する方にとっては簡潔で分かりやすい可能性があります。)
一方で本書籍では下記の様な流れを追ってMulti-Head Attentionについて説明がなされます。
更に付けくわてこれらの各ステップごとに実装されるPytorchのコードもあり、体系的に内部で何が行われているのかを学ぶことができます。
Building LLMs from scratch
本Youtube動画は、先ほどの書籍を参考にRaj Dandekar氏が説明するVizuaraチームの作成した動画です。
この動画も初学者に対して分かりやすく体系立てて作成されており、再生リスト自体は約40本存在しますが苦にならない動画です。
書籍と併用して進めると、より解像度が高く理解を深めることができるかと思います。
特に最近ではYoutubeの動画にも自動翻訳機能が付き、ある程度日本語での理解も可能なため初学者でも十分についていける内容になっています。
おことわり
前書きにも記載させていただきましたが、本記事は私の学習の記録をアウトプットしたものになっています。
そのため各説明は正確な理解・説明でない場合や不十分である可能性があります。
また、本記事を見ればLLMについて深く理解できるものでもなく「こういう勉強方法もあるんだな」くらいの感覚でご覧いただければと思います。
目次
- 1. 学習を始めた背景
- 2. 重要な用語
- 3. Nishika主催のトレーニングコンペでFinetuning練習
- 4. 終わりに
1. 学習を始めた背景
私が本学習を始めた理由はLLMについてきちんと理解したいと思ったからです。
初めてLLMに触れたのは今から約1年前の2-3月ごろであり、当時はTransformerについてどころかモデルの名前(GPT, llamaなど)も、プロンプトという用語の名前も知らないところからスタートしました。(ChatGPTすらほとんど触れたこともありませんでした。)
最初は教えて頂いた Langchain からスタートし、2ヶ月ほどしたころには何となく触れる状態になっていました。(例えば、文字列埋め込んで検索かければ類似度が出るんだなぁくらいです。)
それから数か月経ち、業務内外で何度かLLMについて説明をする機会はあり、そこで初めて自分はLLMについてではなくLLMの使い方しか学んでいないことに気づきます。
これらが(もちろんまだ理由はありますが長くなるため割愛)本学習をスタートした背景になります。
もっと自信を持ってLLMを説明したいというのが私の1つのモチベーションです。
重要な用語
LLMについて理解するために特に重要な用語をいくつか列挙します。
ここでは詳細な説明は特に行いません。
仮に書くとなっても最初にご紹介した書籍や動画の劣化版コピペになる可能性が高いためです。
しかし、以下に列挙する用語を理解できると多くのネット上の記事や論文で何が言いたいかというのがよりハッキリと理解できる様になると思います。
Transformerアーキテクチャ
(最近ではTransformerと書くとライブラリと混同しがちなので、ここではアーキテクチャを付けてます。)
Google DeepResearchが発表したAttention Is All You Needという論文で紹介されたアーキテクチャです。
現在存在する多くの生成AIモデルはこのアーキテクチャを参考にしています。
現時点で178,000回の引用がされているようです。
Google Scholarで被引用数をご確認ください!
Encoderモデル(アーキテクチャ)
TransformerアーキテクチャのEncoder部分のみを採用したアーキテクチャです。代表的なモデルにはBERTがあり、よくBERT系列という呼び方を見ることもあるかと思います。
基本的な学習時には文章全体の内、一部分のトークンをMASK化という処理を行い、そこに入るトークンを予測する学習を行います。
このモデルの大きな特徴は文章全体を見て予測するというアプローチを取ることにあります。
したがって感情分類などの次のトークンを予測する必要はなく、文全体を俯瞰して判断する必要性があるときに本領発揮します。
Decoderモデル(アーキテクチャ)
TransformerアーキテクチャのDecoder部分のみを採用したアーキテクチャです。代表的なモデルにはGPTがあり、よくGPT系列という呼び方を見ることもあるかと思います。
Encoderモデルとは違って次に来るトークンを予測するというアプローチをとります。
基本的な学習時には、1トークン生成させ、またそれをインプットに付け足して学習させ・・・という学習方法を行います。モデルが出力したトークンを再び学習の為にインプットさせる手法のため自己回帰型とも呼ばれます。
このように学習したDecoderモデルは次に来るトークンを予測することが得意な為、ChatGPTのようなチャット型モデルによく使用されます。
例えば、How are you ? という文章がある場合
Input | Output |
---|---|
How | are |
How are | you |
How are you | ? |
--> How are you ?
Attention機構
LLMが文脈の理解を行う為に必要な重要な機構です。
よくある例として、英単語のbankを使用します。
このbankと言う単語は「銀行」と言う意味と「土手・岸」という意味を持ちますが、この単語1つだけでは現在使われているbankがどんな意味なのかを知ることは出来ません。前後の文章があって初めて意味を持ちます。
- The bank of the river. (岸)
- Money in the bank. (お金)
Attention機構ではsequenceから、Q,K,Vを算出・利用して最終的な各単語のAttention Scoreを算出し、このスコアが単語間の関連性を表すことにより、その単語の意味を取得します。
- Q = クエリースコア
- K = キースコア
- V = バリュースコア
ちなみにこのQ,K,Vの考え方はデータベース理論から来ており、Qが現在注目しているトークン、Kが検索をかけるトークン、Vがそれに対応する値という意味合いだそうです。
Embedding(埋め込み・埋め込み表現)
Embeddingとは各トークンを高次元ベクトル上に配置することです。
この配置される位置は各トークンの意味に基づいており、近しい意味を持つトークンは近しい座標に配置されます。
このEmbeddingによってモデルは各トークンの持つ意味やその近しさを得ることができます。
この部分についてより詳しく知りたい方はCohereのLLM UniversityのEmbeddingについての記事をご参照ください。
Attention Mask
Attention Maskは学習時にLLMに渡したくない部分をマスクする(隠す)ために使用されるものです。
基本的には(0/1)テンソルです。
このテンソルはAttentionの重みに対して使用され、Encoderモデルの場合はPaddingなどで付け足した本文ではない部分、Decoderモデルの場合はモデルにとって未来部分をマスクさせるために使用されます。
この部分についての正確かつ簡単な説明は私には難しいため紹介までに留めて、割愛をさせていただきます。
参考書籍のCausal Attentionの部分では、図や具体例、実装コードを使用して詳細に解説されている為、そちらをご参照ください。
3. Nishika主催のトレーニングコンペでFinetuning練習
これまで学習した内容を活かして、Nishikaのトレーニングコンペを使用してFTの練習を行います。
Nishikaのコンペサイトはこちらです。
既にお分かりかと思いますが、本記事内の内容だけではFinetuningを行うには知識不足です。
これを実施するためには最初に記載した参考書籍や動画、ネット上の記事(HuggingFaceなど)などを参考に学習する必要性があります。
コンペ概要
このコンペでは与えられた小説の内容から、その小説の作者が芥川龍之介なのかそれ以外なのかを0,1で分類するコンペです。
データには小説本文とlabelだけがある、とてもシンプルな内容です。
方針概要
今回はあくまでもFinetuningの基本的な方法を学ぶことに念頭を置いています。
したがって精度を求める為にハイパーパラメーターのチューニングなどは行っておらず、最低限必要な内容のみをまとめています。
環境はGoogle Colabです。
やってみる
1. ファイルのセット
まずはGoogle Drive上に必要なファイルを揃えます。
- akutagawaという名前のフォルダーを作成します。
- Nishikaのサイトからデータをダウンロードします。
- akutagawa配下にdataという名前のフォルダーを作成し、データを入れます。
- akutagawa配下にColab Notebookを作成し任意の名前を付けます(例えば First_FT.ipynb )。
akutagawa配下は以下の様な構造になっているかと思います。
.
├── First_FT.ipynb
└── data
├── train.csv
├── test.csv
└── sample_submission.csv
2. Notebook内環境を整える
Google ColabではNotebook内での環境をランタイムを起動する度に整える必要性があります。
-
Notebookを起動する前にランタイムをT4 GPUに設定しておきます。(私はPROですが、無料ユーザーでも扱えると思います)
-
Google Driveをマウントし、akutagawa配下に移動します。
import os from google.colab import drive drive.mount('/content/drive') os.chdir('/content/drive/My Drive/akutagawa') !pwd
-
必要なパッケージをインストールします。
!pip install -qU datasets evaluate !pip install ipadic fugashi unidic_lite
データを用意する
モデルを訓練するために使用するデータを用意します。
ここでの目的は基本的なFT手法を学ぶことにあるため、EDA(探索的データ分析)や前処理と言ったような事前に行うべき一連の処理を省いています。
また、使用するデータ量も削減して実行していることにご注意ください。
-
データを読み込む
import pandas as pd df = pd.read_csv('data/train.csv') df.head()
-
Datasetを作成する
datasetsライブラリのDatasetを作成します。これを使用することで後のデータ処理が高速になります。詳しい内容はHuggingFaceのドキュメントをご参照ください。from datasets import Dataset train_json = { "context": list(df.body), "label": list(df.author) } dataset = Dataset.from_dict(train_json) print(type(dataset)) # 期待される出力例: <class 'datasets.arrow_dataset.Dataset'>
-
データ量を減らす(今回は精度は求めていないので)
seed = 22 # シード値 # 300件に削減 dataset = dataset.shuffle(seed=seed).select(range(300))
Token化処理
-
Tokenizerをロードします。
今回はbert-base-japaneseを使用します。from transformers import AutoTokenizer model_name = "cl-tohoku/bert-base-japanese" tokenizer = AutoTokenizer.from_pretrained(model_name)
-
tokenizeする関数を作成し、datasetsのmap関数でトークン化処理を行います。
def tokenize_function(examples): # examplesはDatasetの各要素 return tokenizer( examples["context"], # 先ほど作成したDatasetを思い出してください。contextには小説本文が入っています。 padding="max_length", # Padding(埋め合わせ)をtoknizerの最大長に合わせます。(BERTは 512 ) truncation=True, # 切り捨てを行います。 ) # Datasetではmap()を使用することで高速に処理することが可能です。 train_tokenized = dataset.map(tokenize_function, batched=True) print(len(train_tokenized))
-
訓練用と検証用に分けておきます。
# 訓練用と検証用に分割します。 # これは必須の処理ではないですが訓練の改善点を見つける為に重要な処理です。 # pandas Dataframeと同じようにtrain_test_splitが扱えます。 splited_tokenized_dataset = train_tokenized.train_test_split(test_size=0.2, seed=seed) train_tokenized = splited_tokenized_dataset["train"] eval_tokenized = splited_tokenized_dataset["test"] print(len(train_tokenized)) print(len(eval_tokenized))
事前学習済みモデルのロード
FTを行う事前学習済みモデルをロードします。
前ステップでロードしたTokenizerのモデル名と同じであることに注意します。
import torch
from transformers import AutoModelForSequenceClassification
device = "cuda" if torch.cuda.is_available() else "cpu"
model = AutoModelForSequenceClassification.from_pretrained(
model_name,
num_labels=2, # 今回は2値分類
device_map=device
)
Training Argumentsの作成
今回のFTではTrainerを使用します。
Trainerに渡すTraining Argumentsの作成を行います。
-
評価メトリクスを設定する
import numpy as np import evaluate # 評価メトリクスを作成します。 # 今回はf1を使用します。 metric = evaluate.load("f1") def compute_metrics(eval_pred): logits, labels = eval_pred # 評価データに対するモデルの予測結果と正解ラベルを取得 predictions = np.argmax(logits, axis=-1) # 最も予測確率の高いlogitのインデックスを取得する return metric.compute(predictions=predictions, references=labels) # 計算
-
TrainingArgumentsを設定する
from transformers import TrainingArguments training_output_dir = "/tmp/test_trainer" # 訓練内容の保存先 Path training_args = TrainingArguments( output_dir=training_output_dir, eval_strategy="steps", per_device_train_batch_size=8, per_device_eval_batch_size=8, logging_steps=10, num_train_epochs=3, report_to="none", max_steps=500, # 最大ステップ数 save_steps=250, load_best_model_at_end=True, metric_for_best_model="f1", # f1スコアが最も良い状態の物を選択する )
Trainerを作成
Trainerを作成します。
今回はearly atopを使用していること注意してください。
from transformers import EarlyStoppingCallback, Trainer
trainer = Trainer(
model=model,
args=training_args,
train_dataset=train_tokenized,
eval_dataset=eval_tokenized,
compute_metrics=compute_metrics,
callbacks=[EarlyStoppingCallback(early_stopping_patience=50)], # early stop
)
Trainerによる訓練
Trainer.train()を実行することで訓練が開始されます。
trainer.train()
モデルを保存
最後にモデルを保存します。
trainer.save_model()を使用します。
save_models_path = "./My_model/First_FT" # モデルの情報の保存先
trainer.save_model(save_models_path)
テストデータを予測
せっかくコンペのデータを使用して練習をしているので、精度は求めていませんがテストデータを予測して提出までやっていきたいと思います。
まずはテストデータをロード -> トークン化までは訓練同様です。
こうして得られたtokenizedデータセットをtrainer.prediction()に入れてあげることでテストデータに対するlogitsを得られます。
test_df = pd.read_csv('data/test.csv')
test_json = {
"context": list(test_df.body)
}
test_dataset = Dataset.from_dict(test_json)
test_tokenized = test_dataset.map(tokenize_function, batched=True)
predictions = trainer.predict(test_tokenized)
# モデルの予測 (logits) から最も確率の高いクラスIDを取得
preds = np.argmax(predictions.predictions, axis=-1)
# submitするファイルを作成します。
submit_sample = pd.read_csv('data/sample_submission.csv')
submit_sample["author"] = preds
submit_sample.head()
submit_sample.to_csv("My-first-submit.csv", index=False)
結果を見てみる
さて、気になる提出結果ですが下記のようになっています。
精度は悪いですが、目的であるFTは無事に行われていそうです。
4. おわりに
本記事では私の学習内容のアウトプットを行い、簡単なFinetuningまで行いました。
ここからは精度や効率をより高めるためにデータをよく調べることや、カスタムアーキテクチャを実装するなどまだまだやるべきこと、学ぶべきことは多くあります。
しかし、LLMの裏側を理解し基本的なFinetuningの手法を学んだことで今までモヤモヤしていた部分が多少なりとも晴れ、以前よりも解像度高くネット上の多くの素晴らしい記事や論文見ることができるようになりました。
ここまで読んでくださった読者の皆様にとってLLMについての学習の1つの指針になることができれば幸いです。