独自のLLMをEAに統合する(第5部):LLMを使った取引戦略の開発とテスト(III) - アダプタチューニング
目次
はじめに
前回の記事では、LoRA手法を用いてGPT-2の事前学習済みモデルをファインチューニングする方法を紹介し、学習コスト、推論コスト、モデル性能など、私たちが注目するいくつかの観点から、完全ファインチューニングモデルとの比較をおこないました。
今回は、アダプタチューニングという手法を使ってGPT-2事前学習済みモデルのファインチューニング(微調整)をおこない、これまで紹介してきた手法と比較します。もちろん、ファインチューニング手法は日々進化しており、新しい手法が次々と登場しています。そのため、今後もすべての手法を個別に取り上げることはしません。すべての方法を再現していては、読者の皆さんも読み続けるのが大変でしょう。したがって、本連載では基本的なファインチューニング手法に絞って紹介しています(たとえば、LoRAチューニングはすでに紹介済みであり、そこから派生したQLoRAチューニングなどについては深入りしません)。
つまり、本記事が大規模言語モデルのファインチューニングに関する最後の記事となります。他の手法に挑戦したい方は、本連載で紹介したファインチューニングの考え方を参考に、別の手法に応用して探求を続けてみてください。次回からは、ファインチューニングしたモデルをEA開発に応用し、取引戦略の構築やバックテストに焦点を移していきます。
本記事の例では、20個のデータポイントを入力として、次の40個のデータポイントを予測するという、比較的積極的なアプローチを採用しています。これは、予測範囲が短すぎるとモデル間の違いが分かりにくいためです。実際の運用では、20個の値を入力して次の5個を予測するような、より保守的な設定が一般的かもしれません。リアルタイム取引に本手法を応用する際には、この点を念頭に置いておく必要があります。より実践的な解決策としては、入力長と出力長をハイパーパラメータとして設定し、遺伝的アルゴリズムを用いて通貨ペアや期間を変えてバックテストをおこない、最適なパラメータを探索する方法が考えられます。本連載ではこの点については詳しく扱いませんので、ぜひご自身で試してみてください。
それでは、アダプタチューニングを使ったGPT-2のファインチューニング方法に話を移しましょう。
環境設定
以下に、この記事で提供されるコード例の動作環境について説明します。もちろん、読者のコード環境が私のものと同じである必要はありませんが、もしコードの実行中に問題が発生した場合は、私の環境設定を参考にしてみてください。
オペレーティングシステム:Ubuntu22.04.5LTS(または対応するバージョンのWSL)
Pythonバージョン:3.10.14
必要なPythonライブラリ
- torch-2.4.1
- numpy-1.26.3
- pandas-2.2.3
- transformers-4.45.1
- peft-0.13.0
- matplotlib-3.9.2
コード実行環境の構成方法がよくわからない場合は、本連載の他の記事で詳しく説明しています。
- AMDグラフィックカードユーザーは、前の記事 「独自のLLMをEAに統合する(第4部):GPUを使用した独自のLLMの訓練」を参照してください。
- NVIDIAグラフィックカードユーザーは、本連載の2番目の記事 「独自のLLMをEAに統合する(第2部):環境展開例」を参照してください。
この記事ではこの部分については詳しく紹介しません。
アダプタモジュールの作成
本連載の最初の記事では、アダプタチューニングについて簡単に紹介しました。一般に、アダプタチューニングは、事前学習済みモデルの各層に専用のアダプタモジュールを挿入することでファインチューニングをおこなう、モジュール型のファインチューニング手法です。各アダプタモジュールは、小さなニューラルネットワークと見なすことができ、特定タスクのデータ分布を効果的に捉える役割を果たします。また、アダプタモジュールは元のモデルとは独立して訓練できるため、管理や最適化がしやすいという利点もあります。
さらに、複数のタスク用アダプタを同一の事前学習済みモデルに簡単に追加できるため、マルチタスク学習の実現にも適しています。特に、タスクが複雑でデータ量が限られている場合には、アダプタチューニングを施したモデルが高いパフォーマンスを発揮する可能性があります。
もちろん、LoRAと比較すると、アダプタモジュールはより多くのパラメータを導入することがあり、ストレージや計算リソースの負担が大きくなる可能性があります。また、タスクごとに適切なアダプタを設計・調整する必要があるため、設計プロセスもやや複雑になります。LoRAチューニングは、パラメータ数を最小限に抑えつつモデルの適応性を高めることに重点を置いており、リソースが限られた環境での効率的なファインチューニングに適しています。一方、アダプタチューニングは、独立したモジュールを通じてタスク固有の情報を取り込むことができ、マルチタスクや柔軟なカスタマイズが求められる場面に向いています。
現時点でタスクの目標が明確である場合は、それに合った手法を選択することが非常に重要です。もし訓練後のモデルが十分な成果を出せない場合、パラメータをいくら調整しても改善されないのであれば、自身のアイデアを否定するのではなく、モデルや訓練方法そのものの見直しを検討するべきです。
それでは、アダプタチューニングを活用してGPT-2モデルを段階的にファインチューニングしていきましょう。まず、アダプタモジュールおよびGPT2LMHeadModelモジュール(つまりGPT2LMHeadModelWithAdaptersクラス)を作成し、その後アダプタモジュールをこのクラスに統合していきます。
GPT-2にアダプタモジュールを組み込むために、GPT2LMHeadModelクラスの修正版を作成します。ここでは簡略化された実装を示します。アダプタ統合における主要な技術要素に注目してください。アダプタモジュールの実装全体のロジックはそれほど複雑ではありません。まずは nn.Moduleを継承するクラスを定義し、その中で主に2つの操作、ダウンサンプリング(down_project)とアップサンプリング(up_project)おこないます。down_projectは入力特徴量をボトルネック層にマッピングし、ReLU活性化関数を通した後、過剰適合を防ぐためにドロップアウトを適用します。up_projectはボトルネック層の特徴量を元の次元に戻し、再度ドロップアウトを適用します。
それではコードを実装してみましょう。まず、torchのnn.Moduleから継承したAdapterクラスを定義します:class Adapter(nn.Module):
クラスの初期化メソッドを定義し、in_featuresとbottleneck_featuresの2つのパラメーターを受け取ります:def __init__(self, in_features, bottleneck_features=64):
- in_features:これは入力特徴量の次元です。GPT-2モデルの場合、これは埋め込み層の次元です。
- bottleneck_features:これはボトルネック層の次元、つまり線形投影後の特徴量の次元です。デフォルトは64に設定されています。
- 親クラス(nn.Module)の初期化メソッドを呼び出します: super(Adapter, self).__init__()
- 入力特徴量の次元をボトルネック層の次元まで削減する線形層(nn.Linear)を定義します: self.down_project = nn.Linear(in_features, bottleneck_features)
- ボトルネック層の特徴量を元の入力特徴量の次元に戻すための別の線形層を定義します:self.up_project = nn.Linear(bottleneck_features, in_features)
- 過剰適合を防ぐために訓練中にニューロンの一部をランダムに破棄するために使用されるドロップアウト層を定義します。破棄確率は0.1に設定されます:self.dropout = nn.Dropout(0.1)
- 重み初期化メソッドを呼び出します:self.init_weights()
重み初期化メソッドinit_weights()を定義します。
- 平均0.0、標準偏差0.02の正規分布を使用して、down_project層の重みパラメータを初期化します:nn.init.normal_(self.down_project.weight, mean=0.0, std=0.02)
- down_project層のバイアスパラメータを定数0で初期化します:nn.init.constant_(self.down_project.bias, 0)
- 同様に、平均0.0、標準偏差0.02の正規分布を使用して、up_project層の重みパラメータを初期化します:nn.init.normal_(self.up_project.weight, mean=0.0, std=0.02)
- up_project層のバイアスパラメータを定数0で初期化します:nn.init.constant_(self.up_project.bias, 0)
順方向伝播メソッドforward()を定義します:defforward(self, hidden_states)。これは1つのパラメータhidden_statesを受け入れます。
- 入力の隠れ状態を、down_project線形層を介してボトルネック層の次元に投影します:hidden_states = self.down_project(hidden_states)
- ReLU活性化関数を使用して、ボトルネック層の隠れ状態に対して非線形変換を実行します:hidden_states = F.relu(hidden_states)
- 非線形変換された隠れ状態にドロップアウトを適用し、ニューロンの一部をランダムに破棄します:hidden_states = self.dropout(hidden_states)
- ボトルネック層の次元からup_project線形層を介して入力特徴の次元まで隠し状態を増やします:hidden_states = self.up_project(hidden_states)
- アップサンプリングされた隠れ状態に再度ドロップアウトを適用します:hidden_states = self.dropout(hidden_states)
- 最後に、アダプタモジュールによって処理された隠し状態を返します:return hidden_states
以下は、完全なAdapterクラスです。
class Adapter(nn.Module): def __init__(self, in_features, bottleneck_features=64): super(Adapter, self).__init__() self.down_project = nn.Linear(in_features, bottleneck_features) self.up_project = nn.Linear(bottleneck_features, in_features) self.dropout = nn.Dropout(0.1) self.init_weights() def init_weights(self): nn.init.normal_(self.down_project.weight, mean=0.0, std=0.02) nn.init.constant_(self.down_project.bias, 0) nn.init.normal_(self.up_project.weight, mean=0.0, std=0.02) nn.init.constant_(self.up_project.bias, 0) def forward(self, hidden_states): hidden_states = self.down_project(hidden_states) hidden_states = F.relu(hidden_states) hidden_states = self.dropout(hidden_states) hidden_states = self.up_project(hidden_states) hidden_states = self.dropout(hidden_states) return hidden_states
このようにして、アダプタモジュールを簡単に作成しました。次のステップは、このモジュールをGPT-2モデルに適合させることなので、GPT2LMHeadModelクラスを書き直す必要があります。
GPT2LMHeadModelクラスの書き換え
GPT2LMHeadModelクラスを全面的に書き直すには、大規模な作業が必要になります。ここでは例として、簡略化したバージョンを提供し、主要な部分のみを実装します。本タスクでは、アダプタモジュールをGPT-2ネットワークに適用し、モデルのさまざまな入力条件や出力要件に対応することが目的です。初期化後には、順伝播関数forward()を書き換え、元のGPT-2モデルのTransformer層を呼び出して隠れ状態hidden_statesを取得し、各アダプタモジュールを順に適用して、その出力を元の隠れ状態に加算します。最終的に、言語モデルの線形層(lm_head)を通じてロジットを生成し、損失を計算します。それではコードを完成させましょう。
書き換えたクラスをGPT2LMHeadModelWithAdaptersとして定義し、GPT2LMHeadModelを継承します:class GPT2LMHeadModelWithAdapters(GPT2LMHeadModel)
GPT2LMHeadModelWithAdaptersクラスの初期化メソッド__init__()を定義し、初期化メソッドで親クラスの初期化メソッドを呼び出してアダプタを追加します。
- 構成パラメータconfigを受け取るクラスメソッド__init__(self,config)を定義します:def__init__(self,config):
- 親クラスの初期化メソッドを呼び出します:super().__init__(config)
- アダプタを初期化します。型はnn.ModuleListで、GPT-2モデルの層の数と同じアダプタモジュールが含まれます。ここで、config.n_embdは埋め込み層の次元、config.n_layerは層の数です:self.adapters = nn.ModuleList([Adapter(config.n_embd) for _ in range(config.n_layer)])
次に、GPT2LMHeadModelWithAdaptersクラスに順方向伝播メソッド「forward()」を実装します。
- モデルの動作と入力形式を制御するために必要なパラメータを受け入れて、順方向伝播メソッドを定義します(ここではこれらのパラメータを1つずつ紹介しません。興味のある読者はこれらのパラメータを最適化してみることができます):def forward(self, input_ids=None, past_key_values=None, attention_mask=None, token_type_ids=None, position_ids=None, head_mask=None, inputs_embeds=None, encoder_hidden_states=None, encoder_attention_mask=None, labels=None, use_cache=None, output_attentions=None, output_hidden_states=None, return_dict=None,)
- 次に、順方向伝播のためにモデル内のTransformer層を呼び出し、モデルの出力を取得して変数transformer_outputsに渡します:transformer_outputs = self.transformer(input_ids, past_key_values=past_key_values, attention_mask=attention_mask, token_type_ids=token_type_ids, position_ids=position_ids, head_mask=head_mask, inputs_embeds=inputs_embeds, encoder_hidden_states=encoder_hidden_states, encoder_attention_mask=encoder_attention_mask, use_cache=use_cache, output_attentions=output_attentions, output_hidden_states=output_hidden_states, return_dict=return_dict,)
- Transformer層の隠し状態出力hidden_statesを取得します。これはアダプタモジュールが処理する必要がある入力です:hidden_states = transformer_outputs[0]
- 次に、forループを使用してすべてのアダプタモジュールをループし、適応の次のステップの準備をします: for i, adapter in enumerate(self.adapters):
- アダプタモジュールの各層の出力を元の隠し状態に追加し、次の層に渡される新しい隠し状態として、hidden_statesに割り当てます:hidden_states = hidden_states + adapter(hidden_states)
- Hidden_statesを処理した後、モデルのlm_head層を介して、処理された隠し状態(hidden_states)を言語モデルのlogits出力に変換する必要もあります。各ロジットは語彙の確率に対応します:lm_logits = self.lm_head(hidden_states)
変換後、損失を計算するリンクを処理します。
- 損失を空に初期化します:loss = None
- ラベルが提供されているかどうかを確認します。ラベルがNoneでない場合:
- 次のトークンを予測する必要があるため、logits出力の最後のトークンを削除します:shift_logits = lm_logits[..., :-1, :].contiguous()
- 次のトークンを予測する必要があるため、ラベルの最後のトークンを削除します:shift_labels = labels[..., 1:].contiguous()
- 損失関数を、分類タスクでよく使用される損失関数である交差エントロピー損失(CrossEntropyLoss)として定義します:loss_fct = nn.CrossEntropyLoss()
- shift_logitsとshift_labels(view(-1,...))をフラット化し、交差エントロピー損失関数を使用します:loss = loss_fct(shift_logits.view(-1, shift_logits.size(-1)), shift_labels.view(-1))
ここで特に注意すべき点は、言語モデルは通常、現在の単語を直接予測するのではなく、次の単語を予測するときに訓練されるということです。したがって、損失を正確に計算するには、モデル出力(lm_logits)とラベル(labels)を時間ステップ内で1つの位置ずつずらす必要があります。たとえば、文が「I love programming」である場合、モデル入力は「I love」となり、モデル出力lm_logitsは「love programming」に対応する確率分布になります。損失を計算するには、「love programming」の確率分布を「programming」というラベルに合わせる必要があります。
- return_dictの設定を確認してください。Falseに設定されている場合、出力を計算してマージします。そうでない場合はreturn_dict:
- logits出力をTransformer層の他の出力(最初の隠れ状態出力を除く)とマージして出力を出力します:output = (lm_logits,) + transformer_outputs[1:]
- ラベルが提供され、損失が計算された場合、損失は出力とともに返されます。そうでない場合は、出力のみが返されます。損失がNoneでない場合は出力を返します。
- return_dictがTrueに設定されている場合、因果出力を直接返します:return modeling_outputs.CausalLMOutputWithCrossAttentions( loss=loss, logits=lm_logits,past_key_values=transformer_outputs.past_key_values, hidden_states=transformer_outputs.hidden_states, attentions=transformer_outputs.attentions,cross_attentions=transformer_outputs.cross_attentions,)
class GPT2LMHeadModelWithAdapters(GPT2LMHeadModel): def __init__(self, config): super().__init__(config) self.adapters = nn.ModuleList([Adapter(config.n_embd) for _ in range(config.n_layer)]) def forward( self, input_ids=None, past_key_values=None, attention_mask=None, token_type_ids=None, position_ids=None, head_mask=None, inputs_embeds=None, encoder_hidden_states=None, encoder_attention_mask=None, labels=None, use_cache=None, output_attentions=None, output_hidden_states=None, return_dict=None, ): transformer_outputs = self.transformer( input_ids, past_key_values=past_key_values, attention_mask=attention_mask, token_type_ids=token_type_ids, position_ids=position_ids, head_mask=head_mask, inputs_embeds=inputs_embeds, encoder_hidden_states=encoder_hidden_states, encoder_attention_mask=encoder_attention_mask, use_cache=use_cache, output_attentions=output_attentions, output_hidden_states=output_hidden_states, return_dict=return_dict, ) hidden_states = transformer_outputs[0] # Apply adapters for i, adapter in enumerate(self.adapters): hidden_states = hidden_states + adapter(hidden_states) lm_logits = self.lm_head(hidden_states) loss = None if labels is not None: # Shift so that tokens < n predict the next token shift_logits = lm_logits[..., :-1, :].contiguous() shift_labels = labels[..., 1:].contiguous() # Flatten the tokens loss_fct = nn.CrossEntropyLoss() loss = loss_fct(shift_logits.view(-1, shift_logits.size(-1)), shift_labels.view(-1)) if not return_dict: output = (lm_logits,) + transformer_outputs[1:] return ((loss,) + output) if loss is not None else output return modeling_outputs.CausalLMOutputWithCrossAttentions( loss=loss, logits=lm_logits, past_key_values=transformer_outputs.past_key_values, hidden_states=transformer_outputs.hidden_states, attentions=transformer_outputs.attentions, cross_attentions=transformer_outputs.cross_attentions, )
このようにして、アダプタモジュールをGPT2LMHeadModelWithAdaptersクラスに適合させました。ただし、これは単なる単純な例であることを再度ご留意ください。実際のシナリオでは、タスクの要件に応じて関連モジュールを慎重に設計してください。
アダプタチューニング
Adapterクラスと、アダプタモジュールに適合したGPT-2モデルクラスGPT2LMHeadModelWithAdaptersを作成しました。次に、モデルとデータをロードしてファインチューニングを開始します。元の記事で解釈された一部のコードについては、ここでは詳しく解釈しません。前回の記事もご参照ください。
1.準備
必要なライブラリをインポートします。ここで紹介する特別なものはありません。
import pandas as pd from transformers import GPT2LMHeadModel, GPT2Tokenizer from transformers import TextDataset, DataCollatorForLanguageModeling from transformers import Trainer, TrainingArguments, modeling_outputs import torch from torch import nn import torch.nn.functional as F
システムに利用可能なGPUがある場合(torch.cuda.is_available()で確認)はGPUを使用し、そうでない場合はCPUを使用します。ロードされたモデルとファインチューニングされたモデルの名前を定義します。
dvc = 'cuda' if torch.cuda.is_available() else 'cpu' print(dvc) model_name_or_path = 'gpt2' Tuned_model = "gpt2_Adapter-tuning"
2.データの読み込みとトークナイザー
作成したアダプタモジュールと書き換えたGPT2LMHeadModelWithAdaptersクラスをここに配置することを忘れないでください。他のスクリプトに配置してから、訓練スクリプトにインポートすることもできます。
llm_data.csvファイルからデータを読み取り、ファインチューニングされたモデルをテストするために使用されるDataFrameオブジェクトを作成します。
df = pd.read_csv('llm_data.csv') 事前学習済みのGPT-2トークナイザーをロードします。
tokenizer = GPT2Tokenizer.from_pretrained(model_name_or_path)
訓練データセットオブジェクトを作成し、tokenizerパラメータで使用するトークナイザーを指定し、file_pathパラメータで訓練データファイルのパスを指定し、block_size=60でブロックサイズを60に指定します。この値は任意に設定することはできず、データセット内のデータに対応している必要があることに注意してください。
train_dataset = TextDataset(tokenizer=tokenizer, file_path="train.txt", block_size=60)
複数のデータサンプルを1つのバッチに結合し、マスク言語モデリング(MLM)タスクを同時に処理します。使用するトークナイザーを指定するには、パラメータ「tokenizer」を使用し、マスク言語モデリング(MLM)は使用せず、因果言語モデリング(CLM)を使用することを指定するには、「mlm=False」を使用します。
data_collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False)
3.モデルをロードしてファインチューニングする
まず、TrainingArgumentsクラスを使用して訓練パラメータオブジェクトをインスタンス化します。
training_args = TrainingArguments(output_dir=Tuned_model, overwrite_output_dir=True, num_train_epochs=3, per_device_train_batch_size=32, save_strategy='no', )
- output_dir=Tuned_model:訓練出力ディレクトリをgpt2_Adapter-tuningとして指定します。
- overwrite_output_dir=True:出力ディレクトリがすでに存在する場合に上書きするかどうか。
- num_train_epochs=3:訓練エポックの数を3に指定します。
- per_device_train_batch_size=32:各デバイスの訓練バッチサイズを32に指定します。
- save_strategy='no':チェックポイントを保存しないことを指定します。
次に、アダプタモジュールを使用して、事前学習済みのGPT-2モデルオブジェクトを読み込んでインスタンス化します。
model = GPT2LMHeadModelWithAdapters.from_pretrained(model_name_or_path) trainer = Trainer(model=model, args=training_args, data_collator=data_collator, train_dataset=train_dataset,)
- model=model:訓練するモデルを指定します。
- args=training_args:訓練パラメータを指定します。
- data_collator=data_collator:データコレクターを指定します。
- train_dataset=train_dataset:訓練データセットを指定します。
訓練プロセスを開始するには、Trainerオブジェクトのtrain()メソッドを使用します:trainer.train()
trainer.train()
訓練後にファインチューニングされたモデルを保存します:trainer.save_model(Tuned_model)
trainer.save_model(Tuned_model)
ファインチューニング後、モデルは訓練スクリプトが配置されているファイルの下のgpt2_Adapter-tuningフォルダに保存されます。
4.ファインチューニングされたモデルをテストする
ファインチューニング後、ファインチューニングされたモデルをロードして推論を実行し、ファインチューニングされたモデルが正常に動作するかどうかを確認する必要があります。もちろん、ファインチューニングされたモデルをロードするときは、書き換えたクラスGPT2LMHeadModelWithAdaptersを使用してロードする必要があります。モデルを読み込んだ後、GPUアクセラレーションを設定し、モデルを推論モードに切り替える必要があります。
model = GPT2LMHeadModelWithAdapters.from_pretrained(Tuned_model) model.to(dvc) model.eval()
次のステップは、モデルが適切に動作しているかどうかを確認するための推論テストです。このプロセスは前の記事と同じです。詳しいコード解釈については前回の記事を参照してください。この記事ではそれについては議論しません。
prompt = ' '.join(map(str, df.iloc[:, 1:20].values[-1])) generated = tokenizer.decode(model.generate(tokenizer.encode(prompt, return_tensors='pt').to(dvc), do_sample=True, max_length=200)[0], skip_special_tokens=True) print(generated)
結果は以下のとおりです。

以下は、完全なファインチューニングコードスクリプト「lora-tuning.py」です。
import pandas as pd from transformers import GPT2LMHeadModel, GPT2Tokenizer from transformers import TextDataset, DataCollatorForLanguageModeling from transformers import Trainer, TrainingArguments,modeling_outputs import torch from torch import nn import torch.nn.functional as F dvc = 'cuda' if torch.cuda.is_available() else 'cpu' print(dvc) model_name_or_path = 'gpt2' Tuned_model="gpt2_Adapter-tuning" # Define the Adapter module class Adapter(nn.Module): def __init__(self, in_features, bottleneck_features=64): super(Adapter, self).__init__() self.down_project = nn.Linear(in_features, bottleneck_features) self.up_project = nn.Linear(bottleneck_features, in_features) self.dropout = nn.Dropout(0.1) self.init_weights() def init_weights(self): nn.init.normal_(self.down_project.weight, mean=0.0, std=0.02) nn.init.constant_(self.down_project.bias, 0) nn.init.normal_(self.up_project.weight, mean=0.0, std=0.02) nn.init.constant_(self.up_project.bias, 0) def forward(self, hidden_states): hidden_states = self.down_project(hidden_states) hidden_states = F.relu(hidden_states) hidden_states = self.dropout(hidden_states) hidden_states = self.up_project(hidden_states) hidden_states = self.dropout(hidden_states) return hidden_states # Integrate the Adapter into the model class GPT2LMHeadModelWithAdapters(GPT2LMHeadModel): def __init__(self, config): super().__init__(config) self.adapters = nn.ModuleList([Adapter(config.n_embd) for _ in range(config.n_layer)]) def forward( self, input_ids=None, past_key_values=None, attention_mask=None, token_type_ids=None, position_ids=None, head_mask=None, inputs_embeds=None, encoder_hidden_states=None, encoder_attention_mask=None, labels=None, use_cache=None, output_attentions=None, output_hidden_states=None, return_dict=None, ): transformer_outputs = self.transformer( input_ids, past_key_values=past_key_values, attention_mask=attention_mask, token_type_ids=token_type_ids, position_ids=position_ids, head_mask=head_mask, inputs_embeds=inputs_embeds, encoder_hidden_states=encoder_hidden_states, encoder_attention_mask=encoder_attention_mask, use_cache=use_cache, output_attentions=output_attentions, output_hidden_states=output_hidden_states, return_dict=return_dict, ) hidden_states = transformer_outputs[0] # Apply adapters for i, adapter in enumerate(self.adapters): hidden_states = hidden_states + adapter(hidden_states) lm_logits = self.lm_head(hidden_states) loss = None if labels is not None: # Shift so that tokens < n predict the next token shift_logits = lm_logits[..., :-1, :].contiguous() shift_labels = labels[..., 1:].contiguous() # Flatten the tokens loss_fct = nn.CrossEntropyLoss() loss = loss_fct(shift_logits.view(-1, shift_logits.size(-1)), shift_labels.view(-1)) if not return_dict: output = (lm_logits,) + transformer_outputs[1:] return ((loss,) + output) if loss is not None else output return modeling_outputs.CausalLMOutputWithCrossAttentions( loss=loss, logits=lm_logits, past_key_values=transformer_outputs.past_key_values, hidden_states=transformer_outputs.hidden_states, attentions=transformer_outputs.attentions, cross_attentions=transformer_outputs.cross_attentions, ) if __name__=="__main__": # Load data df = pd.read_csv('llm_data.csv') tokenizer = GPT2Tokenizer.from_pretrained(model_name_or_path) train_dataset = TextDataset(tokenizer=tokenizer, file_path="train.txt", block_size=60) data_collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False) training_args = TrainingArguments(output_dir=Tuned_model, overwrite_output_dir=True, num_train_epochs=3, per_device_train_batch_size=32, save_strategy= 'no', ) # Initialize model with adapters model = GPT2LMHeadModelWithAdapters.from_pretrained(model_name_or_path) trainer = Trainer(model=model, args=training_args, data_collator=data_collator, train_dataset=train_dataset,) trainer.train() trainer.save_model(Tuned_model) # Load the model for inference model = GPT2LMHeadModelWithAdapters.from_pretrained(Tuned_model) model.to(dvc) model.eval() prompt = ' '.join(map(str, df.iloc[:, 1:20].values[-1])) generated = tokenizer.decode(model.generate(tokenizer.encode(prompt, return_tensors='pt').to(dvc), do_sample=True, max_length=200)[0], skip_special_tokens=True) print(f"test the model:{generated}")
データファイルは記事の最後に添付されています。元のデータファイルはllm_data.csv、前処理済みのデータファイルはtrain.txtです。
さまざまなファインチューニング手法のパフォーマンス比較
次に、さまざまなファインチューニング方法の効率とパフォーマンスを比較します。これまでは、フルパラメータのファインチューニングとLoRAのファインチューニングのみを導入しました。この記事のアダプタチューニングを加えると、合計3つになります。次に、それらを比較します。
1.効率の比較
LoRAチューニングの訓練プロセス
- train_runtime:69.5605秒
- VRAM:4.1G
- generate_runtime:1.242877秒
フルパラメータファインチューニング訓練プロセス
- train_runtime:101.7946秒
- VRAM:5.67G
- generate_runtime:0.876525秒
アダプタチューニング訓練プロセス
- train_runtime:104.4355秒
- VRAM:5.52G
- generate_runtime:0.882792秒
| Train_runtime | VRAM(GB) | Generate_runtime | |
|---|---|---|---|
| フルパラメータファインチューニング | 101.7946 | 5.67 | 0.876525 |
| LoRAチューニング | 69.5605 | 4.1 | 1.242877 |
| アダプタチューニング | 104.4355 | 5.52 | 0.882792 |
2.精度の比較
前回の記事と同様に、元のデータの最後の行にある終値の最初の20列を入力として読み込み、残りのデータを結果として読み込み、2つの訓練方法によって取得されたモデルを評価します。ここで注目すべきは、記事の冒頭で述べたように、結果の比較をより有意義にするために、より積極的な予測の長さを選択したことです。
最初の20の終値
- 入力データ:[0.61163 0.61162 0.61191 0.61195 0.61209 0.61231 0.61224 0.61207 0.61187 0.61184 0.6119 0.61169 0.61168 0.61162 0.61181 0.61184 0.61184 0.6118 0.61176]
残りの終値
- 実際の価格:[0.6119, 0.61197, 0.61201, 0.61242, 0.61237, 0.6123, 0.61229, 0.61242, 0.61212, 0.61197, 0.61201, 0.61213, 0.61212, 0.61206, 0.61203, 0.61206, 0.6119, 0.61193, 0.61191, 0.61202, 0.61197, 0.6121, 0.61211, 0.61214, 0.61203, 0.61203, 0.61213, 0.61218, 0.61227, 0.61226]
次に、モデルを個別にロードし(フルパラメータファインチューニングのモデルパラメータは現在のディレクトリのgpt2_stockフォルダ、LoRAファインチューニングのモデルは現在のディレクトリのgpt2_LORA_Noneフォルダ、アダプタチューニングのモデルは現在のディレクトリのgpt2_Adapter-tuningに保存されます)、推論を実行し、得られた結果に応じてMSE、RMSE、およびNRMSEを計算します。これらのコードは前回の記事で紹介されており、この記事では詳細には説明しません。以下は、完全なテストコードスクリプト「test.py」です。
import time import pandas as pd from transformers import GPT2LMHeadModel, GPT2Tokenizer, GPT2Config from sklearn.metrics import mean_squared_error import torch import numpy as np from peft import PeftModel import matplotlib.pyplot as plt from adapter_tuning import GPT2LMHeadModelWithAdapters df = pd.read_csv('llm_data.csv') dvc='cuda' if torch.cuda.is_available() else 'cpu' base_model='gpt2' fine_tuning_path='./gpt2_stock' lora_tuning_path ='./gpt2_LORA_None' adpter_tuning_path='./gpt2_Adapter-tuning' pre_length=40 tokenizer = GPT2Tokenizer.from_pretrained(base_model) model_fine_tuning = GPT2LMHeadModel.from_pretrained(fine_tuning_path).to(dvc) model_lora_tuning = GPT2LMHeadModel.from_pretrained(base_model) model_lora_tuning=PeftModel.from_pretrained(model_lora_tuning, lora_tuning_path).to(dvc) model_adapter_tuning = GPT2LMHeadModelWithAdapters.from_pretrained(adpter_tuning_path).to(dvc) input_data=df.iloc[:,1:20].values[-1] true_prices= df.iloc[-1:,21:].values.tolist()[0] prompt = ' '.join(map(str, input_data)) def generater(model): global true_prices model.eval() token=tokenizer.encode(prompt, return_tensors='pt').to(dvc) start_=time.time() generated = tokenizer.decode(model.generate(token, do_sample=True, max_length=200)[0], skip_special_tokens=True) end_=time.time() print(f'generate time:{end_-start_}') generated_prices=generated.split('\n')[0] generated_prices=list(map(float,generated_prices.split())) generated_prices=generated_prices[0:pre_length] # def trim_lists(a, b): # min_len = min(len(a), len(b)) # return a[:min_len], b[:min_len] # true_prices,generated_prices=trim_lists(true_prices,generated_prices) print(f"input data:{input_data}") print(f"true prices:{true_prices}") print(f"generated prices:{generated_prices}") mse = mean_squared_error(true_prices[:pre_length], generated_prices) print('MSE:', mse) rmse=np.sqrt(mse) nrmse=rmse/(np.max(true_prices)-np.min(generated_prices)) print(f"RMSE:{rmse},NRMSE:{nrmse}") return generated_prices, mse, rmse, nrmse def plot_(a,b,c,title): plt.figure(figsize=(7, 6)) if title=='predication': plt.plot(true_prices[:pre_length], label='True Values', marker='o') plt.plot(a, label='fine_tuning', marker='x') plt.plot(b, label='lora_tuning', marker='s') plt.plot(c,label='adapter_tuning',marker='d') plt.title(title) plt.xlabel('Index') plt.ylabel('Value') plt.legend() plt.savefig(f"{title}.png") def groups_chart(a,b,c,models): metrics = ['Train_time(s)', 'Infer_time(s)', 'Memory(GB)', 'MSE', 'RMSE', 'NRMSE'] plt.figure(figsize=(7, 6)) a=[101.7946,1.243,5.67,a[1],a[2],a[3]] b=[69.5605,0.877,4.10,b[1],b[2],b[3]] c=[104.4355,0.883,5.52,c[1],c[2],c[3]]# 104.4355s,VRAM:5.52G generate_runtime:0.882792s bar_width = 0.2 r1 = np.arange(len(metrics)) r2 = [x + bar_width for x in r1] r3 = [x + bar_width for x in r2] plt.bar(r1, a, color='r', width=bar_width, edgecolor='grey', label=models[0]) plt.bar(r2, b, color='b', width=bar_width, edgecolor='grey', label=models[1]) plt.bar(r3, c, color='g', width=bar_width, edgecolor='grey', label=models[2]) plt.yscale('log') plt.xlabel('Metrics', fontweight='bold') plt.xticks([r + bar_width for r in range(len(metrics))], metrics) plt.ylabel('Values (log scale)', fontweight='bold') plt.title('Model Comparison') plt.legend() # plt.show() plt.savefig('Comparison.png') fine_tuning_result = generater(model_fine_tuning) lora_tuning_result = generater(model_lora_tuning) adapter_tuning_result=generater(model_adapter_tuning) plot_(fine_tuning_result[0],lora_tuning_result[0],adapter_tuning_result[0],title='predication') groups_chart(fine_tuning_result,lora_tuning_result,adapter_tuning_result,models=['fine-tuning','lora-tuning','adapter-tuning'])
注:
ここで注意すべき問題は、測定している指標の大きさの順序が同じではないことです。そのため、ここでは対数スケールを使用しました:plt.yscale('log')。これにより、データの大きさが大きく異なる状況に効果的に対処できます。
フルパラメータファインチューニングモデルの推論結果:
- generated prices:[0.61163, 0.61162, 0.61191, 0.61195, 0.61209, 0.61231, 0.61224, 0.61207, 0.61187, 0.61184, 0.6119, 0.61169, 0.61168, 0.61162, 0.61181, 0.61184, 0.61184, 0.6118, 0.61176, 0.61165, 0.61169, 0.61186, 0.61171, 0.61171, 0.6116, 0.61165, 0.61168, 0.61165, 0.61169, 0.61173, 0.61184, 0.61176, 0.61171, 0.61176, 0.61171, 0.61207, 0.61208, 0.61202, 0.6117, 0.61207]
- MSE:1.257374999999991e-07
- RMSE:0.00035459483921794336
- NRMSE:0.43243273075362537
LoRAファインチューニングモデルの推論結果
- generated prices:[0.61163, 0.61162, 0.61191, 0.61195, 0.61209, 0.61231, 0.61224, 0.61207, 0.61187, 0.61184, 0.6119, 0.61169, 0.61168, 0.61162, 0.61181, 0.61184, 0.61184, 0.6118, 0.61176, 0.61191, 0.61187, 0.6121, 0.61187, 0.61193, 0.61195, 0.61176, 0.61194, 0.61171, 0.61198, 0.61171, 0.61171, 0.61198, 0.61172, 0.61202, 0.6116, 0.61173, 0.61199, 0.61169, 0.61171, 0.61171]
- MSE:1.0161999999999925e-07
- RMSE:0.0003187789202566557
- NRMSE:0.3887547808008319
アダプタ調整の推論結果:
- generated prices:[0.61163, 0.61162, 0.61191, 0.61195, 0.61209, 0.61231, 0.61224, 0.61207, 0.61187, 0.61184, 0.6119, 0.61169, 0.61168, 0.61162, 0.61181, 0.61184, 0.61184, 0.6118, 0.61176, 0.61173, 0.61168, 0.61165, 0.61178, 0.61173, 0.61164, 0.61174, 0.61163, 0.61174, 0.61163, 0.61174, 0.61162, 0.61162, 0.61167, 0.61168, 0.61165, 0.61167, 0.61168, 0.61162, 0.61167, 0.61174]
- MSE:1.5644499999999023e-07
- RMSE:0.00039553128826932293
- NRMSE:0.4944141103367081
以下は、比較のための図表による視覚化です。


結論
この記事では、アダプタチューニング法を使用してGPT-2事前学習済みモデルをファインチューニングする方法について説明し、紹介したファインチューニング方法を横並びで比較しました。この比較により、取引戦略に最適な訓練方法とモデルを直感的に選択できるようになります。
アダプタチューニングはLoRAよりもわずかに長い訓練時間と多くのVRAMを必要とする可能性がありますが、タスク固有の情報をキャプチャするための異なるアプローチを提供することがわかりました。最適な方法の選択は、特定のプロジェクトの要件と利用可能なリソースによって異なります。完全なパラメータファインチューニングは依然として強力なベースラインであり、LoRAは効率性を提供し、アダプタチューニングはモジュール性とマルチタスクシナリオにおける潜在的な利点を提供します。
今後の記事では、さまざまなファインチューニング方法を試し続けることはありません。代わりに、ファインチューニングされたモデルを使用して取引戦略を策定し、それをEAに統合する方法に焦点を当てます。これらが完了したら、EAをバックテストして評価します。もしまだモデルのファインチューニングに興味があり、より良い結果を得たいのであれば、私のアイデアに従い、サンプルコードに基づいてステップバイステップで進めてみてください。信じてください、これは決して難しいプロセスではありません。
次回の記事もお楽しみに。
付録:
| ファイル | 詳細 |
|---|---|
| adapter_tuning.py | アダプタチューニング用のコード |
| test.py | さまざまなファインチューニング方法の効率とパフォーマンスを比較するためのコード |
| llm_data.csv | 生データファイル |
| train.txt | 訓練データファイル |
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/13500
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。
ニュース取引が簡単に(第6回):取引の実施(III)
MQL5での取引戦略の自動化(第2回):一目均衡表とオーサムオシレーターを備えた雲抜けシステム
MQL5でのファイル操作の習得:基本的なI/OからカスタムCSVリーダーの構築まで
MQL5で自己最適化エキスパートアドバイザーを構築する(第2回):USDJPYスキャルピング戦略
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索
なぜダウンサンプリングの直後に元の入力サイズにアップサンプリングする必要があるのか?レイヤーの説明は同じに見えますが(オーバーフィッティングを防ぐためのドロップアウト)、もしデータが同じ機能を持つ小さいコンテナにうまく収まるのであれば、後方アップサンプリングは過剰で無駄に見えます(少なくとも、変換から新しい情報を得ることはありません)。
追記英語から(少なくとも)ロシア語への投稿の自動翻訳は馬鹿げているように見えるので、元の投稿を読んでください。