English Русский Español Deutsch
preview
独自のLLMをEAに統合する(第5部):LLMs(II)-LoRA-チューニングによる取引戦略の開発とテスト

独自のLLMをEAに統合する(第5部):LLMs(II)-LoRA-チューニングによる取引戦略の開発とテスト

MetaTrader 5トレーディング | 18 12月 2024, 14:34
373 0
Yuqiang Pan
Yuqiang Pan

目次



はじめに

前回の記事では、GPT-2の事前学習済みモデルを、独自の金融データを用いてフルパラメータのファインチューニングをおこなう方法を紹介し、モデルの出力結果を評価しました。今回および次回の記事では、さらに別のファインチューニング手法について、コード例を交えながら解説します。ただし、前回紹介したファインチューニング手法に限定し、すべての方法を網羅するのではなく、よく使われる方法をいくつか選んで実装します)。本記事では、LoRAチューニング手法を例として取り上げます。

さらに、これらの異なるファインチューニング手法で学習したモデルを横断的に比較し、現時点での対象通貨ペアにおいて最もパフォーマンスの良いモデルを見つけることも課題の1つです(もちろん、モデルのパフォーマンスは、上昇トレンド、下降トレンド、横ばいトレンドなど、市場環境によっても変化する可能性があります)。この比較により、実運用において、どのモデル訓練手法を選択すればより良い結果が得られるかを明確にすることができます。さらに厳密を期す場合、これらの異なる手法を比較するだけでなく、異なるデータ処理法やファインチューニング法を組み合わせた場合の、異なる通貨ペアのモデル性能を比較することも重要です。これは一見シンプルですが、非常に手間のかかる作業です。しかし、取引に本当に応用するならば、このステップは非常に重要だと考えます。とはいえ、本記事では誰でも簡単に応用・拡張可能な内容を目指しているため、この部分についての詳細な説明は割愛します。具体的には、学習データを他の通貨ペアに置き換えることで、モデルの性能を横断的に比較するだけです。手間がかかりますが、技術的には容易に実現可能です。

もう1つ注意点として、前回の記事ではサンプルコードに対応する環境設定やライブラリの依存関係を詳しく説明していなかったため、読者の中には依存関係の不足が原因でエラーが発生する方もいたかもしれません。今後の記事では、サンプルコードがスムーズに実行できるよう、使用している環境設定や依存関係についても詳しく説明する予定です。

それでは、いよいよ今回のテーマに入っていきましょう。


環境設定

以下は、この記事で提供するコード例の実行環境です。もちろん、読者のコード環境が私の環境と同一である必要はありませんが、コード実行中に問題が発生した場合には、私の環境設定を参考にすることができます。

  • オペレーティングシステム:Ubuntu 22.04.5 LTS(または対応するバージョンのWSL)
  • Pythonバージョン:3.10.14
  • 必要なPythonライブラリ

  1. torch-2.4.1
  2. numpy-1.26.3
  3. pandas-2.2.3
  4. transformers-4.45.1
  5. petf-0.13.0
  6. matplotlib-3.9.2

コード実行環境の設定方法がよくわからない場合は、本連載の他の記事で詳しく紹介していますので、そちらをご参照ください。

本稿では、この環境設定部分の詳細な説明は省略します。


LoRA構成

LoRAについては前回紹介したので、今回は割愛します。この記事では、ファインチューニングのプロセスをよりシンプルかつ明確にするため、元のLoRA著者のコード例を再現せず、よりシンプルなpeftライブラリを使用します。

このPythonライブラリには、LoRAチューニングパラメータ設定クラス(LoraConfig)、LoRAチューニング初期化モデルメソッド(get_peft_model)、LoRAファインチューニングモデルローディングクラス(PeftModel)など、私たちが必要とする様々な設定が統合されています。

次に、LoraConfigクラスから順を追って紹介していきます。

1. LoraConfig クラス

LoraConfigクラスはpeftライブラリに属しており、peftライブラリから直接インポートすることができます。LoraConfigクラスをインポートしたら、設定パラメータを設定する必要があります。

次に、LoraConfigクラスのパラメータ設定を紹介します。

  • r (`int`):

Loraの注目の次元(「ランク」)

  • target_modules (`[Union[List[str], str]]`)(オプション):

アダプタを適用するモジュールの名前。指定すると、指定された名前のモジュールだけが置き換えられます。文字列を渡すと、正規表現による照合がおこなわれます。文字列のリストを渡すと、完全一致が実行されますか、モジュール名が渡された文字列のいずれかで終わっているかどうかがチェックされます。all-linearとして指定された場合、出力層を除くすべての線形/Conv1Dモジュールが選択されます。指定されていない場合、モジュールはモデルアーキテクチャに従って選択されます。アーキテクチャが不明な場合はエラーが発生します。

  • lora_alpha (`int`):

Loraスケーリングのアルファパラメータ

  • lora_dropout (`float`):

Lora層のドロップアウト確率

  • fan_in_fan_out (`bool`):

置き換える層が(fan_in, fan_out)のような重みを保存している場合、これをTrueに設定します。例えば、gpt-2では、(fan_in, fan_out)のような重みを格納するConv1Dを使用するのでTrueに設定されるべきです。

  • bias (`str`):

LoRAのバイアスタイプ。none、all、lora_onlyのいずれかです。allまたはlora_onlyの場合、対応するバイアスが訓練中に更新されます。これは、アダプタを無効化しても、適応なしのベースモデルと同じ出力は得られないことを意味します。

  • use_rslora (`bool`):
 Trueに設定すると、Rank-Stabilized LoRAを使用します。これはアダプタのスケーリング係数をlora_alpha/math.sqrt(r)に設定します。 そうでない場合は、元のデフォルト値である「lora_alpha/r」を使用します。
  • modules_to_save (`List[str]`):
 アダプタ層とは別に、訓練可能なモジュールとして設定され、最終チェックポイントに保存されるモジュールのリスト
  • init_lora_weights (`bool` | `Literal["gaussian", "olora", "pissa", "pissa_niter_[number of iters]", "loftq"]`):
アダプタ層の重みを初期化するメソッド。True(デフォルト)を渡すと、Microsoftのリファレンス実装によるデフォルトの初期化がおこなわれます。「"gaussian"」を渡すと、線形と層のLoRAランクでスケーリングされたガウス初期化がおこなわれます。初期化をFalseに設定すると、完全にランダムな初期化になってしまうのでお勧めしません。LoftQの初期化を使用するにはloftqを渡します。OLoRAの初期化を使用するにはoloraを渡します。「"pissa"」を渡すと、Principal Singular values and Singular vectors Adaptation (PiSSA) が初期化され、LoRAよりも収束が速く、最終的に優れたパフォーマンスを達成します。さらに、PiSSAはQLoRAに比べて量子化誤差を低減し、さらなる強化につながります。
    pissa_niter_[反復数]を渡すと、高速SVDベースのPiSSA初期化が開始します。ここで、[反復数]はFSVDを実行する部分空間の反復数を示し、非負の整数でなければなりません。_[反復数]を16に設定すると、7Bモデルの初期化を数秒で完了することができ、SVDを使用した場合とほぼ同等の学習効果が得られます。
  • layers_to_transform (`Union[List[int], int]`):
変換する層のインデックス。intのリストが渡された場合は、このリストで指定された層インデックスにアダプタを適用します。単一の整数が渡された場合、このインデックスの層に変換を適用します。
  • layers_pattern (`str`):
layer_to_transformがNone以外の場合にのみ使用されます。
  • rank_pattern (`dict`):
 層名または正規表現から、rで指定されたデフォルトのランクとは異なるランクへのマッピング
  • alpha_pattern (`dict`):
層名または正規表現から、lora_alphaで指定されたデフォルトのアルファ値と異なるアルファ値へのマッピング
  • megatron_config (`[dict]`)(オプション):
MegatronのTransformerConfig引数。LoRAの平行線層を作成するために使用されます。これは、core_transformer_config_from_args(get_args())のように取得できます。この2つの関数はMegatronのものです。 引数はMegatronのTransformerConfigを初期化するのに使用されます。megatronのColumnParallelLinearとRowParallelLinearの層にLoRAを適用する場合、このパラメータを指定する必要があります。
  • megatron_core (`[str]`)(オプション):
デフォルトは「"megatron.core"」です。
  • loftq_config (`[LoftQConfig]`)(オプション):
LoftQの構成。Noneでない場合、LoftQはバックボーン重みの量子化とLora層の初期化に使用されます。「init_lora_weights='loftq'」も渡されます。LoftQはモデル自体を量子化するからです。
  • use_dora (`bool`):
Weight-Decomposed Low-Rank Adaptation (DoRA)を有効にします。この手法は、重みの更新を大きさと方向の2つに分解します。方向は通常のLoRAで扱われ、大きさは別の学習可能なパラメータで扱われます。これにより、特に低ランクのLoRAのパフォーマンスを向上させることができます。現在、DoRAは線形とConv2D層のみをサポートしています。DoRAは純粋なLoRAよりも大きなオーバーヘッドをもたらすので、推論のために重みを結合することを推奨します。詳しくはhttps://arxiv.org/abs/2402.09353を参照してください。
  • layer_replication (`List[Tuple[int, int]]`):
元のモデルの層を指定された範囲に従って積み重ね、新しい層のスタックを構築します。これにより、ベースモデルの重みを重複させることなくモデルを拡張(または縮小)することができます。新しい層には、すべて別々のLoRAアダプタが取り付けられます。
  • runtime_config (`LoraRuntimeConfig`):
ランタイム設定(保存も復元もされない)

以上が LoraConfigクラスのパラメータです。実際の訓練では、すべての値を設定するのではなく、必要な一部の重要なパラメータだけを設定し、他はデフォルトのままにしておくのが一般的です。この例では、lora_alpha=32、lora_dropout=0.1というパラメータだけを設定し、他のパラメータはデフォルトのままとしています。もちろん、この記事で紹介した設定が最適というわけではありません。常にいくつかのパラメータの組み合わせを選び、さまざまな設定を試して最適なパラメータの組み合わせを見つけることができます。

peft_config = LoraConfig(
                         lora_alpha=32, 
                         lora_dropout=0.1)

2. get_peft_model()関数

get_peft_model()関数は、peftライブラリから直接インポートすることもできます。ファインチューニングの前に、GPT-2モデルを指定された構成を満たすモデルとしてロードするために使用する必要があります。この記事の例では、設定されたLoRAモデルとしてGPT-2をロードします。

同様に、まずこの関数のパラメータ構成を見てみましょう。

  • model ([`transformers.PreTrainedModel`]):

ラッピングするモデル

  • peft_config ([`PeftConfig`]):

Peftモデルのパラメータを含む設定オブジェクト

  • adapter_name (`str`)(オプション):

指定しない場合は、デフォルトのアダプタ名("default")が使用されます。

  • mixed (`bool`)(オプション、デフォルトはFalse):

異なる(互換性のある)アダプタタイプの混在を許可するかどうか。

  • autocast_adapter_dtype (`bool`)(オプション):

アダプタのdtypeを自動キャストするかどうか。デフォルトはTrueです。今のところ、これはfloat16またはbfloat16を使っているアダプタの重みをfloat32にキャストするだけです。

  • revision (`str`)(オプション、デフォルトは`main`):

ベースモデルの改訂。これが設定されていない場合、保存されたPeftモデルはベースモデルの `main` リビジョンをロードします。

この例では、modelとpeft_configパラメータのみを使用し、他はデフォルトのままにしています。modelはGPT-2モデルを渡すのに使われ、peft_configはLoraConfigの設定を受け取るのに使われます。

model = get_peft_model(model, peft_config)

3. PeftModel クラス

PeftModelクラスはpeftライブラリの基本クラスです。このライブラリでサポートされているモデルであれば、どのタイプでも初期化することができます。PeftModelクラスを使用して、学習完了後にファインチューニング時に保存したLoRAパラメータとGPT-2で事前に学習したモデルパラメータを1つのモデルにロードし、ロードしたモデルを推論テストに使用する必要があります。同様に、まずこのクラスのパラメータ構成を見てみよう。

  • model ([`~transformers.PreTrainedModel`]):Peftに使用されているベースTransformerモデル
  • peft_config ([`PeftConfig`]):Peftモデルの構成
  • adapter_name (`str`)(オプション、デフォルトは「"default"」):アダプタの名前
  • autocast_adapter_dtype (`bool`)(オプション、デフォルトはTrue):

アダプタのdtypeを自動キャストするかどうか。今のところ、これはfloat16とbfloat16を使っているアダプタの重みをfloat32にキャストするだけです。

  • low_cpu_mem_usage (`bool`)(オプション、デフォルトはFalse):

    メタデバイス上に空のアダプタ重みを作成します。ロードの読み込みをスピードアップするのに便利です。

  • 属性:

    - base_model ([`torch.nn.Module`]):Peftで使用するベースTransformerモデル

    - peft_config ([`PeftConfig`]):Peftモデルの設定

    - modules_to_save (`list` of `str`):モデルの保存時に保存するサブモジュール名のリスト

    - prompt_encoder ([`PromptEncoder`]):[`PromptLearningConfig`] を使用している場合にPeftで使用するプロンプトエンコーダ

    - prompt_tokens (`torch.Tensor`):[`PromptLearningConfig`]を使用している場合にPeftで使用する仮想プロンプトトークン

    - transformer_backbone_name (`str`):[`PromptLearningConfig`] を使用している場合に、ベースモデルのTransformerバックボーンの名前

    - word_embeddings (`torch.nn.Embedding`):[`PromptLearningConfig`] を使用している場合、ベースモデルにおけるTransformerバックボーンの単語埋め込み

PeftModelクラスを使用する場合、モデルをロードするためにクラスメソッドPeftModel.from_pretrained(model, peft_model_id)を直接使用します。モデルはGPT-2モデルで、peft_model_idはファインチューニングしたLoRAモデルのパラメータです。

model = PeftModel.from_pretrained(model, peft_model_id)

注:

訓練用に新しいPEFTアダプタを作成するときに「low_cpu_mem_usage=True」を使用しないでください。


LoRAチューニング

peftライブラリを使ったLoRA-tuningの設定方法を紹介したので、コード例を完成させましょう。

1. 必要なライブラリのインポート

Python環境で必要なライブラリを直接インポートします。

import pandas as pd
from transformers import GPT2LMHeadModel, GPT2Tokenizer
from transformers import TextDataset, DataCollatorForLanguageModeling
from transformers import Trainer, TrainingArguments
import torch
from peft import get_peft_model, LoraConfig, PeftModel

2.ロードデータとモデル構成

まず、現在のコード環境でGPUアクセラレーションが利用可能かどうかをチェックし、環境設定が正しいことを確認します。利用可能なGPUがあるにもかかわらず使用されていない場合は、コード環境の設定を確認する必要があります。CPUはタスクを完了することはできますが、非常に低速です。

dvc = 'cuda' if torch.cuda.is_available() else 'cpu'
print(dvc)

次に、LoRAチューニングパラメータを設定します。これらのパラメータは先に紹介したので、そのまま使うことにします。

model_name_or_path = 'gpt2'
peft_config = LoraConfig(
    lora_alpha=32, 
    lora_dropout=0.1
)

model_name_or_pathは事前に訓練されたモデルです。次に、ファインチューニングしたLoRAモデルpeft_model_idを保存するパスを定義します。

peft_model_id = f"{model_name_or_path}_{peft_config.peft_type}_{peft_config.task_type}"

では、llm_data.csvをロードしてみましょう。このデータセットの直近20終値を入力として使用し、モデルの出力を残りの終値と比較することで、モデルのパフォーマンスを検証します。

df = pd.read_csv('llm_data.csv')

次に、前処理済みのデータtrain.txtをロードする必要があります(llm_data.csvをtrain.txtに変換する部分は、前回の記事ですでに変換しているので、再度変換する必要はないので削除しました)。トークナイザーのtrain_datasetとdata_collatorを定義します。この部分は前回の記事と同じなので、ここでは詳しく説明しません。興味のある読者は前回の記事を参照してください。

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)

TrainingArgumentsもインスタンス化する必要があります。ここでは、save_stepsとsave_total_limitパラメータを削除しました。これらのパラメータは、主に訓練中のチェックポイントの保存を管理するものですが、LoRA-tuningでは、LoRAパラメータだけを保存すればよく、すべてのパラメータを保存する必要はありません。競合を避けるために、この2つのパラメータを削除し、「save_strategy='no'」パラメータを追加しました。

training_args = TrainingArguments(
    output_dir=peft_model_id,     
    overwrite_output_dir=True,    
    num_train_epochs=3,     
    per_device_train_batch_size=32,
    save_strategy='no'
)

3. モデルのロードとファインチューニング

まず、訓練済みのGPT-2モデルをHeadModelとしてロードします。

model = GPT2LMHeadModel.from_pretrained(model_name_or_path)

次に、設定されたLoRAの設定を、事前に訓練されたGPT-2モデルに結合します。かなり複雑だったこの処理は、今ではpeftライブラリのget_peft_model()関数を使った1行のコードで済みます。このライブラリは私たちに大きな利便性をもたらしてくれました。

model = get_peft_model(model, peft_config)

次に、Trainerをインスタンス化し、ファインチューニングの訓練処理をおこない、モデルを保存します。この部分は前回の記事のコードと変わらないので、詳しい説明は省きます。興味のある読者は前回の記事を参照してください。

trainer = Trainer(
    model=model,
    args=training_args,
    data_collator=data_collator,
    train_dataset=train_dataset
)
trainer.train()
trainer.save_model(peft_model_id)

注意しなければならないのは、trainer.save_model(peft_model_id)を使って保存されたモデルは、もはや完全なモデルではなく、LoRAの重みだけが含まれているということです。LoRAチューニングの間、GPT-2の事前学習された重みは凍結され、LoRAの重みだけがファインチューニングされます。そのため、ファインチューニングされたモデルをロードする際には、PeftModelクラスのfrom_pretrained()メソッドを使って、これら2つの重みの部分を一緒にロードし直す必要があります。GPT2LMHeadModel.from_pretrained()を使用してモデルをロードすることはできなくなりました。

訓練

ファインチューニングが終わると、モデルは訓練スクリプトがあるディレクトリの下のgpt2_LORA_Noneフォルダに保存されます(LoraConfigクラスでtask_typeパラメータを設定していないので、このオプションのデフォルトはNoneです。)

4. ファインチューニングしたモデルをテストする

ファインチューニングが終わったら、ファインチューニングされたモデルをロードして推論をおこない、ファインチューニングされたモデルが正しく機能するかどうかをチェックする必要があります。前述したように、LoRAでファインチューニングされたモデルは GPT2LMHeadModel.from_pretrained()によるロードをサポートしていないため、PeftModelクラスのfrom_pretrained()メソッドを使用して、事前に訓練されたGPT-2モデルとLoRAの重みを一緒にロードする必要があります。PeftModel.from_pretrained()メソッドのパラメータは先に紹介したので、ここでは説明しません。モデルをロードした後、GPUアクセラレーションに設定し、モデルを推論モードに切り替える必要があります。

model = GPT2LMHeadModel.from_pretrained(model_name_or_path)
model = PeftModel.from_pretrained(model, peft_model_id)
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}")

結果は以下のとおりです。

test the model: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.61174 0.61175 0.61169 

0.6119 0.61174 0.6116 0.61144 0.61155 0.61207 0.61192 0.61203 0.61158 0.61202 0.61158 0.61156 

0.61146 0.61196 0.61144 0.656 0.61142 0.61141 0.61137 0.60952 0.611

以下が完全なファインチューニングコードスクリプト(lora-tuning.py)です。

import pandas as pd
from transformers import GPT2LMHeadModel, GPT2Tokenizer
from transformers import TextDataset, DataCollatorForLanguageModeling
from transformers import Trainer, TrainingArguments
import torch
from peft import get_peft_model, LoraConfig, PeftModel

dvc='cuda' if torch.cuda.is_available() else 'cpu'
print(dvc)
model_name_or_path='gpt2'

peft_config = LoraConfig(
                        #  task_type=None, 
                        #  inference_mode=False, 
                        #  r=8, 
                         lora_alpha=32, 
                         lora_dropout=0.1,
                         )

peft_model_id = f"{model_name_or_path}_{peft_config.peft_type}_{peft_config.task_type}"
df = pd.read_csv('llm_data.csv')

# sentences = [' '.join(map(str, prices)) for prices in df.iloc[:-10,1:].values]
# with open('train.txt', 'w') as f:
#     for sentence in sentences:
#         f.write(sentence + '\n')

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=peft_model_id,     
                                  overwrite_output_dir=True,    
                                  num_train_epochs=3,     
                                  per_device_train_batch_size=32,
                                  save_strategy= 'no',   
                                #   save_steps=10_000,    
                                #   save_total_limit=2,
                                #   load_best_model_at_end=True,
                                  )

model = GPT2LMHeadModel.from_pretrained(model_name_or_path)
model = get_peft_model(model, peft_config)

trainer = Trainer(model=model,
                  args=training_args,
                  data_collator=data_collator,
                  train_dataset=train_dataset,)

trainer.train()

# model.save_pretrained(peft_model_id)
trainer.save_model(peft_model_id)

# config = PeftConfig.from_pretrained(peft_model_id)
model = GPT2LMHeadModel.from_pretrained(model_name_or_path)
model = PeftModel.from_pretrained(model, peft_model_id)
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です。


さまざまなファインチューニング法の比較

さまざまなファインチューニング方法を試した結果、性能の異なる新しいGPT-2モデルが得られました。そのため、EA戦略に最も適した方法を科学的に選択するために、さまざまな方法の結果と訓練速度を比較する必要があります。GPT-2の事前学習済みモデルは私たちの入力を認識できないので、事前学習済みモデルを比較シーケンスに含める必要はありません。したがって、ここでは比較のためにフルパラメータファインチューニングとLoRAチューニングのみを紹介します。もちろん、次回以降もいくつかの方法を紹介していくので、選択肢は増えるでしょう。

1. 効率比較

まず、訓練にかかる費用を比較する必要があります。私たちは、訓練効率が高く、コストが低い方法を好みます。ここでは、学習時間、メモリ使用量、推論速度を比較します。GPT-2のような小さなパラメータモデルでは、その差はそれほど大きくないかもしれませんが、より大きなモデル(例えば7B、13B、34B、またはそれ以上)を選ぶと、その差は非常に顕著になります。


Train_runtime
VRAM(GB)
Generate_runtime
Loraチューニングプロセス
69.5605
4.1
1.242877
フルパラメータファインチューニングプロセス
101.7946
5.67
0.876525

2. 精度比較

精度の面では、MSE(平均二乗誤差)、RMSE(平均二乗誤差)、NRMSE(正規化平均二乗誤差)を用いて、異なるファインチューニング方法によって得られたモデルを一時的に比較します。その他の指標(プレプレキシティ、ロバスト性など)は今のところ評価しません。

次に、元データの最後の20行の終値を入力として読み込み、残りのデータを結果として使用し、2つの訓練方法によって得られたモデルを評価します。

  • Input Data: [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]
  • True Prices: [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フォルダに保存される)、推論を実行します。その結果に基づいてMSE、RMSE、NRMSEを計算します。これらのコードは前回の記事で紹介したので、ここでは詳しく説明しません。

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

# Load dataset
df = pd.read_csv('llm_data.csv')

# Set device (GPU or CPU)
dvc = 'cuda' if torch.cuda.is_available() else 'cpu'

# Define model paths
base_model = 'gpt2'
fine_tuning_path = './gpt2_stock'
lora_tuning_path = './gpt2_LORA_None'

# Initialize tokenizer and models
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)

# Extract input data and true prices
input_data = df.iloc[:, 1:20].values[-1]
true_prices = df.iloc[-1:, 21:].values.tolist()[0]

# Prepare prompt
prompt = ' '.join(map(str, input_data))

推論とMSE、RMSE、NRMSEの計算処理を関数generater(model)にカプセル化し、予測値、MSE、RMSE、NRMSEを戻り値とします。推論評価に異なるモデルを使う場合は、モデルをパラメータとして渡すだけです。ここで注意しなければならないのは、この関数で使われているtrue_pricesはグローバル変数であり、関数内でその値を変更する必要があるということです。

def generater(model):
    global true_prices
    
    # Set the model to evaluation mode
    model.eval()
    
    # Tokenization and text generation using the model
    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_} seconds')
    
    # Process the generated data
    generated_prices = generated.split('\n')[0]
    generated_prices = list(map(float, generated_prices.split()))
    generated_prices = generated_prices[:len(true_prices)]
    
    # Function to trim both lists to the same length
    def trim_lists(a, b):
        min_len = min(len(a), len(b))
        return a[:min_len], b[:min_len]
    
    # Trim the true_prices and generated_prices lists
    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}")
    
    # Calculate MSE, RMSE, NRMSE metrics
    mse = mean_squared_error(true_prices, 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 generater(model):
    global true_prices
    
    # Set the model to evaluation mode
    model.eval()
    
    # Tokenization and text generation using the model
    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_} seconds')
    
    # Process the generated data
    generated_prices = generated.split('\n')[0]
    generated_prices = list(map(float, generated_prices.split()))
    generated_prices = generated_prices[:len(true_prices)]
    
    # Function to trim both lists to the same length
    def trim_lists(a, b):
        min_len = min(len(a), len(b))
        return a[:min_len], b[:min_len]
    
    # Trim the true_prices and generated_prices lists
    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}")
    
    # Calculate MSE, RMSE, NRMSE metrics
    mse = mean_squared_error(true_prices, 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

推論結果の視覚化を関数plot_(a, b, title)にカプセル化してみましょう。

def plot_(a, b, title):
    # Set up the figure size
    plt.figure(figsize=(10, 6))
    
    # Plot true_prices only if the title is 'prediction'
    if title == 'prediction':
        plt.plot(true_prices, label='True Values', marker='o')
    
    # Plot the fine-tuning and lora-tuning values
    plt.plot(a, label='fine_tuning', marker='x')
    plt.plot(b, label='lora_tuning', marker='s')
    
    # Set the title and labels for the axes
    plt.title(title)
    plt.xlabel('Index')
    plt.ylabel('Value')
    
    # Display the legend and save the plot to a file
    plt.legend()
    plt.savefig(f"{title}.png")

モデルの効率と、先に述べた評価指標を、関数groups_chart(a, b, models)にカプセル化します。

def groups_chart(a, b, models):
    # Define metrics for the chart
    metrics = ['Train Time(s)', 'Inference Time (s)', 'Memory Usage (GB)', 'MSE', 'RMSE', 'NRMSE']
    
    # Set figure size
    plt.figure(figsize=(10, 6))
    
    # Update values for model a and b
    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]]
    
    # Bar width for each group of bars
    bar_width = 0.2
    
    # Set the positions of the bars
    r1 = np.arange(len(metrics))  # Positions for model a
    r2 = [x + bar_width for x in r1]  # Positions for model b
    
    # Plot bars for both models
    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])
    
    # Set log scale for y-axis
    plt.yscale('log')
    
    # Set labels and title
    plt.xlabel('Metrics', fontweight='bold')
    plt.xticks([r + bar_width / 2 for r in range(len(metrics))], metrics)  # Center the x-axis ticks
    plt.ylabel('Values (log scale)', fontweight='bold')
    plt.title('Model Comparison')
    
    # Display legend and save the plot
    plt.legend()
    # plt.show()  # Uncomment to display the plot
    plt.savefig('Comparison.png')

注:

ここでの問題は、測定するメトリクスの大きさが同じでないことなので、ここでは対数スケールを使っています(plt.yscale('log'))。こうすることで、データ量が大きく変化する状況にも効果的に対応することができます。

異なるモデルは別々に推論を実行します。

fine_tuning_result = generater(model_fine_tuning)
lora_tuning_result = generater(model_lora_tuning)

以下は、フルパラメータファインチューニングモデルの推論結果です。

  • 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.61183, 0.61185, 0.61217, 0.61221, 0.61223, 0.61226, 0.61231, 0.61231, 0.61229, 0.61235, 0.61237, 0.61241, 0.61243, 0.61248, 0.61253, 0.61263, 0.61265, 0.61267, 0.61271, 0.61267, 0.61272]
  • MSE:1.0064750000000609e-07
  • RMSE:0.0003172499014972362
  • NRMSE:0.3965623768715889

以下は、LoRA-tuningモデルの推論結果です。

  • 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.6116, 0.6116, 0.61194, 0.6118, 0.61195, 0.61197, 0.61196, 0.6123, 0.61181, 0.61172, 0.6119, 0.61155, 0.61149, 0.61197, 0.61198, 0.61192, 0.61136, 0.61092, 0.61091, 0.61098, 0.61099]
  • MSE:2.3278249999999242e-07
  • RMSE:0.00048247538797330626
  • NRMSE:0.3195201244856309

結果を視覚化し、画像として保存します。

plot_(fine_tuning_result[0],lora_tuning_result[0],title='predication')
groups_chart(fine_tuning_result,lora_tuning_result,models=['fine-tuning','lora-tuning'])

以下は、比較のための図表による視覚化です。

プレ

シーエムピー

注:

このスクリプトは何度もテストしていますが、実行するたびに結果は異なります。したがって、私が提供するデータやチャートはあくまで参考であり、読者の実行結果が私と異なるのは正常です。

3. 正しいモデルの選択

効率性の観点からは、LoRAチューニングがフルパラメータファインチューニングに比べて、学習速度、推論速度、メモリー使用量の点で優れていることは明らかです。次に、推論精度を比較します。チャートから、最初の18個の予測値では2つのモデルの出力がほぼ同じであるのに対し、残りの値では誤差が徐々に大きくなっていることが直感的にわかります。フルパラメータファインチューニングモデルの予測値は、NRMSE値から明らかなように、全体的に比較的安定しています。

test.pyスクリプトを複数回実行し、結果が一貫しているかどうかを確認しようとしました。結果はさまざまで、LoRAファインチューニングモデルのNRMSEは小さいこともあれば(フルパラメータファインチューニングモデルのNRMSEよりはるかに低い0.17程度)、大きいこともありました(最大0.76688)。フルパラメータでファインチューニングしたNRMSEは0.4前後で安定していました。これらのデータは、必ずしもフルパラメータファインチューニングモデルがLoRAファインチューニングモデルよりも優れていることを意味しないことに注意することが重要です。LoRAチューニングは、フルパラメータファインチューニングと同じ訓練設定では収束しなかった可能性があります。より良い解決策は、モデルの収束を確実にするために、訓練中の損失に基づいて適切な早期停止ロジックを設定することです。この部分の内容は、今のところコード例では提供していませんが、興味のある読者は自分で実装することができます。

もちろん、モデルのパラメータ設定の違いも、モデルの性能に影響を与える可能性があります。したがって、より科学的なアプローチとしては、まず同じデータセット上でモデルや学習方法の最適なパラメータ設定を見つけ、その最適な設定のもとでモデルが収束することを確認することです。そして、異なる訓練方法やモデルの水平比較をおこない、様々なメトリクスを総合的に評価し、最適な訓練方法やモデルを選択します。

テストコードのスクリプトは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

# Load the dataset
df = pd.read_csv('llm_data.csv')

# Define the device (GPU if available)
dvc = 'cuda' if torch.cuda.is_available() else 'cpu'

# Model paths and base settings
base_model = 'gpt2'
fine_tuning_path = './gpt2_stock'
lora_tuning_path = './gpt2_LORA_None'

# Load the tokenizer and models
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)

# Extract the input data and true prices from the dataset
input_data = df.iloc[:, 1:20].values[-1]
true_prices = df.iloc[-1:, 21:].values.tolist()[0]
prompt = ' '.join(map(str, input_data))

# Function to generate predictions
def generater(model):
    global true_prices
    model.eval()
    
    # Tokenization and text generation
    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_}')
    
    # Processing generated prices
    generated_prices = generated.split('\n')[0]
    generated_prices = list(map(float, generated_prices.split()))
    generated_prices = generated_prices[:len(true_prices)]
    
    # Function to trim lists to the same length
    def trim_lists(a, b):
        min_len = min(len(a), len(b))
        return a[:min_len], b[:min_len]
    
    # Trim the true prices and generated prices
    true_prices, generated_prices = trim_lists(true_prices, generated_prices)
    
    # Output metrics
    print(f"Input data: {input_data}")
    print(f"True prices: {true_prices}")
    print(f"Generated prices: {generated_prices}")
    
    mse = mean_squared_error(true_prices, 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

# Function to plot the comparison between true prices and predictions
def plot_(a, b, title):
    plt.figure(figsize=(10, 6))
    
    if title == 'prediction':
        plt.plot(true_prices, label='True Values', marker='o')
    
    plt.plot(a, label='fine_tuning', marker='x')
    plt.plot(b, label='lora_tuning', marker='s')
    
    plt.title(title)
    plt.xlabel('Index')
    plt.ylabel('Value')
    plt.legend()
    plt.savefig(f"{title}.png")

# Function to generate a bar chart comparing different metrics between models
def groups_chart(a, b, models):
    metrics = ['Train Time(s)', 'Inference Time (s)', 'Memory Usage (GB)', 'MSE', 'RMSE', 'NRMSE']
    plt.figure(figsize=(10, 6))
    
    # Data for the metrics
    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]]
    
    bar_width = 0.2
    r1 = np.arange(len(metrics))
    r2 = [x + bar_width for x in r1]
    
    # Plotting bars for both models
    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])
    
    # Set y-axis to log scale for better visibility of differences
    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.savefig('Comparison.png')

# Generate results for both fine-tuned and LORA-tuned models
fine_tuning_result = generater(model_fine_tuning)
lora_tuning_result = generater(model_lora_tuning)

# Plot the prediction comparison
plot_(fine_tuning_result[0], lora_tuning_result[0], title='prediction')

# Generate the comparison chart for the models
groups_chart(fine_tuning_result, lora_tuning_result, models=['fine-tuning', 'lora-tuning'])


結論

本記事では、LoRAチューニング法を使用してGPT-2事前学習済みモデルをファインチューニングする方法を解説し、これまでに紹介したファインチューニング手法の比較をおこないました。この比較を通じて、自身の取引戦略に最も適した訓練方法とモデルを直感的に選択する手助けとなるでしょう。今後もさらなるファインチューニング手法について議論を深め、これらの方法を活用してGPT-2事前学習済みモデルをファインチューニングし、取引戦略におけるより精度の高い手法を模索していきます。ただし、GPT-2事前学習モデルのパラメータ規模を考慮すると、得られる結果は必ずしも理想的なものと一致しない場合があります。それでも、最終的な結果を求める過程は変わりません。また、異なるモデルを横断的に比較しない理由について疑問に思われるかもしれません。この点は重要な問いですが、選択肢として挙げられるモデルは非常に多く、さらに同じモデルであってもパラメータ規模が異なる場合があります。そのため、いくつかの簡単な例だけではこの課題を解決するのは難しいでしょう。しかし、このプロセスは非常に手間がかかるものの、技術的には複雑ではありません。したがって、記事で示した方法の例を参考にして、さまざまなモデルから最適な結果を得る方法を探ることをお勧めします。

探求を続ける準備はよろしいでしょうか。次の記事でお会いしましょう。

MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/13499

添付されたファイル |
llm_data.csv (1139.04 KB)
train.txt (1123.41 KB)
lora-tuning.py (2.64 KB)
test.py (3.23 KB)
ニューラルネットワークの実践:擬似逆行列(I) ニューラルネットワークの実践:擬似逆行列(I)
今日は、純粋なMQL5言語で擬似逆行列の計算を実装する方法を検討し始めます。これから見るコードは、初心者にとっては予想していたよりもはるかに複雑なものになる予定で、それをどのように簡単に説明するかをまだ模索中です。したがって、今のところは、これを珍しいコードを学ぶ機会と考えてください。落ち着いて注意深く学んでください。効率的または迅速な適用を目的としたものではありませんが、可能な限り教訓的なものにすることが目標です。
リプレイシステムの開発(第54回):最初のモジュールの誕生 リプレイシステムの開発(第54回):最初のモジュールの誕生
この記事では、リプレイ/シミュレーターシステムで使用するための、他の目的にも汎用的に使用できる、実際に機能するモジュールの最初のものを組み立てる方法について説明します。マウスモジュールです。
ニューラルネットワークが簡単に(第94回):入力シーケンスの最適化 ニューラルネットワークが簡単に(第94回):入力シーケンスの最適化
時系列を扱うときは、常にソースデータを履歴シーケンスで使用します。しかし、これが最善の選択肢なのでしょうか。入力データの順序を変更すると、訓練されたモデルの効率が向上するという意見があります。この記事では、入力シーケンスを最適化する方法の1つを紹介します。
知っておくべきMQL5ウィザードのテクニック(第42回):ADXオシレーター 知っておくべきMQL5ウィザードのテクニック(第42回):ADXオシレーター
ADXは、一部のトレーダーが一般的なトレンドの強さを測定するために使用する、もう1つの比較的人気のあるテクニカルインジケーターです。これは他の2つのインジケーターの組み合わせとして機能し、オシレーターとして表示されます。この記事では、MQL5ウィザードアセンブリとそのサポートクラスを使用して、そのパターンについて説明します。