
将您自己的 LLM 集成到 EA 中(第 5 部分):使用 LLM 开发和测试交易策略(二)-LoRA-调优
目录
概述
在上一篇文章中,我们介绍了如何利用自有金融数据,采用全参数微调的方法对 GPT-2 预训练模型进行微调,并评估了模型的输出结果。在本文和后续文章中,我们将通过代码示例进一步讨论如何实现其他微调方法(我们将只讨论前一篇文章中介绍的微调方法,当然,不可能实现每种方法。我只会选择几种常用的方法来实现)。本文将以 LoRA 调优方法为例进行讨论。
此外,我们的任务是尝试横向比较用这些不同微调方法训练的模型,然后在当前货币对下找到性能最佳的模型(当然,模型的性能也可能在不同的市场条件下有所不同,如上升趋势、下降趋势或振荡趋势)。这可以更清楚地指导我们在实践中使用哪种模型训练方法来取得更好的效果。当然,如果我们更加严谨,我们不仅应该横向比较这些不同的处理方法,还应该比较不同货币对在不同数据处理方法和微调方法下的微调模型的性能。这似乎是一项简单但极其乏味的任务。我个人认为,如果我们真的想在交易中应用这一系列方法,这一步至关重要。然而,我不打算在本系列文章中详细介绍这一部分,因为我相信每个人都可以根据我们的例子轻松扩展。只需将训练数据替换为不同的货币对,然后横向比较模型性能。虽然这很乏味,但很容易实现。
另一点需要注意的是,在之前的文章中,我忽略了在示例代码中介绍相应的环境配置和库依赖关系,这可能会导致一些朋友在尝试运行示例时因缺少依赖关系而遇到错误。在以后的文章中,我将详细解释当前代码中使用的环境配置和依赖关系,以帮助读者轻松运行示例。
现在,让我们正式进入本文的主题!
环境配置
下面是本文中提供的代码示例的运行环境。当然,这并不意味着您的代码环境必须与我的相同,但如果您在运行代码时遇到问题,可以参考我的环境配置。 - 操作系统:Ubuntu 22.04.5 LTS(或对应版本的 WSL)
- Python 版本:3.10.14
- 必要的 Python 库:
- torch-2.4.1
- numpy-1.26.3
- pandas-2.2.3
- transformers-4.45.1
- petf-0.13.0
- matplotlib-3.9.2
如果你还不熟悉如何配置代码运行环境,我在这个系列的其他文章中有详细的介绍:
- AMD 显卡用户可以参考之前的文章(将您自己的 LLM 集成到 EA 中(第 4 部分):使用 GPU 训练你自己的 LLM )
- NVIDIA显卡用户可以参考本系列的第二篇文章(将您自己的 LLM 集成到 EA 中(第 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 (`Optional[Union[List[str], str]]`):
应用适配器的模块的名称。如果指定了此项,则只会替换具有指定名称的模块。传递字符串时,将执行正则表达式匹配。传递字符串列表时,将执行精确匹配,或者检查模块名称是否以传递的任何字符串结尾。如果指定为“all-linear”,则选择所有线性/Conv1D 模块,不包括输出层。如果没有指定,将根据模型架构选择模块。如果不知道架构,就会出现错误 —— 在这种情况下,您应该手动指定目标模块。
- lora_alpha(`int`):
Lora 缩放的 alpha 参数。
- lora_dropout(`float`):
Lora 层的丢失概率。
- fan_in_fan_out (`bool`):
如果要替换的层存储类似(fan_in,fan_out)的权重,则将其设置为 True。例如,gpt-2 使用 “Conv1D” 来存储类似(fan_in,fan_out)的权重,因此应该将其设置为 “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”会导致主奇异值和奇异向量自适应(PiSSA)的初始化,其比 LoRA 收敛得更快,并最终实现卓越的性能。此外,与 QLoRA 相比,PiSSA 减少了量化误差,从而实现了进一步的增强。传递
`'pissa_niter_[number of iters]'` 启动基于 Fast-SVD 的 PiSSA 初始化,其中 `[number of iters]` 表示执行 FSVD 的子空间迭代次数,并且必须是非负整数。当 `[number of iters]` 设置为 16 时,可以在数秒内完成 7B 模型的初始化,训练效果大约相当于使用 SVD。
- layers_to_transform (`Union[List[int], int]`):
要转换的层索引。如果传递了一个整数列表,它将把适配器应用于该列表中指定的层索引。如果传递一个整数,它将在该索引处的图层上应用转换。
- layers_pattern (`str`):
层模式名称,仅当 “layers_to_transform” 与 “None” 不同时使用。
- rank_pattern(`dict`):
从层名称或正则表达式到与 “r” 指定的默认等级不同的等级的映射。
- alpha_pattern(`dict`):
从层名称或正则表达式到与 “lora_alpha” 指定的默认 alpha 不同的 alpha 的映射。
- megatron_config (`Optional[dict]`):
Megatron 的 TransformerConfig 参数。它用于创建 LoRA 的并行线性层。你可以这样获取它,`core_transformer_config_from_args(get_args())`,这两个函数来自 Megatron。 这些参数将用于初始化 Megatron 的 TransformerConfig。当您想要将 LoRA 应用于 megatron 的 ColumnParallelLinear 和 RowParallelLinear 层时,需要指定此参数。
- megatron_core (`Optional[str]`):
要使用的 Megatron 核心模块默认为“megatron.core”。
- loftq_config (`Optional[LoftQConfig]`):
LoftQ 的配置。如果不是 None,那么 LoftQ 将用于量化主干权重并初始化 Lora 层。还要传递“init_lora_weights='loftq'”。请注意,在这种情况下您不应该传递量化模型,因为 LoftQ 将量化模型本身。
- use_dora (`bool`):
启用“权重分解低秩自适应”(DoRA)。该技术将权重的更新分解为两部分,幅度和方向。方向由正常的 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 模型,使其在微调之前满足指定的配置。在本文的示例中,我们将把 GPT-2 加载为配置好的 LoRA 模型。
同样,让我们先看看这个函数的参数配置:
- model ([`transformers.PreTrainedModel`]):
待包裹的模型。
- peft_config ([`PeftConfig`]):
包含 Peft 模型参数的配置对象。
- adapter_name (`str`, `optional`, defaults to `"default"`):
要注入的适配器的名称,如果未提供,则使用默认适配器名称(“default”)。
- mixed (`bool`, `optional`, defaults to `False`):
是否允许混合不同的(兼容的)适配器类型。
- autocast_adapter_dtype (`bool`, *optional*):
是否自动转换适配器数据类型。默认为“True”。目前,这只会使用 float16 或 bfloat16 转换为 float32 来转换适配器权重,因为这通常是稳定训练所必需的,并且只会影响选定的 PEFT 调谐器。
- revision (`str`, `optional`, defaults to `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 预训练模型参数加载到一个模型中,然后使用加载的模型进行推理测试。同样的我们先来看一下这个类的参数配置。
- model ([`~transformers.PreTrainedModel`]):Peft 使用的基本变换器模型。
- peft_config ([`PeftConfig`]):Peft 模型的配置。
- adapter_name (`str`, *optional*):适配器的名称,默认为 “default”。
- autocast_adapter_dtype (`bool`, *optional*):
是否自动转换适配器数据类型。默认为“True”。目前,这只会使用 float16 和 bfloat16 转换为 float32 来转换适配器权重,因为这通常是稳定训练所必需的,并且只会影响选定的 PEFT 调谐器。
- low_cpu_mem_usage(`bool`,`可选`,默认为`False`):
在元设备上创建空适配器权重。有助于加快加载过程。
- 属性:
- base_model ([`torch.nn.Module`]) —— 用于 Peft 的基础变换器模型。
- peft_config ([`PeftConfig`]) —— Peft 模型的配置。
- modules_to_save (`str` of `list`) —— 保存模型时要保存的子模块名称列表。
- prompt_encoder ([`PromptEncoder`]) —— 如果使用 [`PromptLearningConfig`],则用于 Peft 的提示编码器。
- prompt_tokens (`torch.Tensor`) —— 如果使用 [`PromptLearningConfig`],则为 Peft 使用的虚拟提示令牌。
- transform_backbone_name (`str`) —— 如果使用 [`PromptLearningConfig`],则为基础模型中变换器主干的名称。
- word_embeddings (`torch.nn.Embedding`) —— 如果使用 [`PromptLearningConfig`],则为基础模型中 变换器主干网的词嵌入。
在使用 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 调优之后,让我们完成我们的代码示例。
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 调优,我们只需要保存 LoRA 参数,而不是所有参数。为了避免冲突,我们去掉了这两个参数,并添加了 `save_strategy='no'` 参数,使用 `Trainer` 类中的 `save_model` 方法来保存模型。
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()” 函数的一行代码。这个库给我们带来了很大的便利。
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()` 方法将这两部分权重重新加载在一起,以使模型正常工作。您不能再使用 “GPT2LMHeadModel.from_pretrained()” 来加载模型。
微调后,模型将保存在训练脚本所在目录下的 `gpt2_LORA_None` 文件夹中(由于我们没有在 `LoraConfig` 类中设置 `task_type` 参数,因此此选项默认为 `None`,这就是该文件夹以 `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}")
结果如下:
测试模型: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 或更大)时,差异会变得非常明显。
训练运行时(秒) | 显存(GB) | 生成运行时(秒) | |
---|---|---|---|
LoRA 调优过程 | 69.5605 | 4.1 | 1.242877 |
全参数微调过程 | 101.7946 | 5.67 | 0.876525 |
2.准确度比较
准确率方面,我们暂时用MSE(均方误差)、RMSE(均方根误差)、NRMSE(归一化均方根误差)来对比不同微调方法得到的模型。其他指标(如困惑度、鲁棒性等)目前尚未评估。
接下来,我们加载原始数据最后 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 文件夹中)并运行推理。我们根据结果计算它们的 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的过程封装成一个函数 generator(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)
全参数微调模型的推理结果:
- 生成的价格:[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.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 调优模型推理结果:
- 生成的价格:[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.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 个预测值中,两个模型的输出几乎相同,而其余值的误差逐渐增大。全参数微调模型的预测总体上相对稳定,NRMSE 值就是明证。
我尝试多次运行 test.py 脚本,看看结果是否一致。结果各不相同,LoRA 微调模型的 NRMSE 有时很小(约为 0.17,远低于全参数微调的 NRMSE),有时很大(高达 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
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.


