
将您自己的 LLM 集成到 EA 中(第 5 部分):使用 LLMs 开发和测试交易策略(一)- 微调
目录
概述
在上一篇文章中,我们介绍了如何使用 GPU 加速来训练大型语言模型,但我们没有使用它来制定交易策略或执行回测。然而,训练我们模型的最终目标是使用它并让它为我们服务。因此,从本文开始,我们将逐步使用训练好的语言模型来制定交易策略,并在外汇货币对上测试我们的策略。当然,这不是一个简单的过程。这就要求我们采取相应的技术手段来实现这一过程。所以,让我们一步一步地实现它。
整个过程可能需要几篇文章才能完成。
- 第一步是制定交易策略;
- 第二步是根据策略创建数据集并微调模型(或训练模型),使大型语言模型的输入和输出符合我们制定的交易策略。实现这一过程的方法有很多,我将提供尽可能多的例子;
- 第三步是模型的推理,将输出与交易策略融合,并根据我们的交易策略创建 EA。当然,在模型的推理阶段,我们还有一些工作要做(选择合适的推理框架和优化方法:例如 flash-attention、模型量化、加速等);
- 第四步是使用历史回溯测试在客户端测试我们的 EA。
看到这一点,一些朋友可能会想,我已经用自己的数据训练了一个模型,为什么我需要对其进行微调?本文将给出这个问题的答案。
当然,现有的方法并不局限于微调大型预言模型。也可以使用其他技术,例如 RAG 技术(一种使用检索信息来辅助大型语言模型生成内容的技术)和 Agent 技术(一个由大型语言模型推理创建的智能体)。如果所有这些内容都在一篇文章中完成,文章的长度会太长,不足以阅读规则,会显得混乱,所以我们将分几个部分进行讨论。在本文中,我们主要讨论我们的第一步和第二步,制定交易策略,并给出一个微调大型语言模型(GPT2)的例子。
大型语言模型的微调
在开始之前,我们必须首先了解这种微调。有朋友可能会有疑问:在前面的几篇文章中,我们已经训练了一个模型,为什么我们需要对其进行微调?为什么不直接使用训练好的模型呢?要理解这个问题,我们必须首先注意大型语言模型和传统神经网络模型之间的区别:现阶段,大型语言模型基本基于 Transformer 架构,其中包含复杂的注意力机制。模型复杂,参数多,因此在训练大型语言模型时,通常需要大量的数据进行训练,以及高性能的计算机硬件支持,训练时间一般为几十小时到几天甚至几十天。因此,对于个人开发人员来说,从头开始训练语言模型相对困难(当然,如果你家里有金矿,那就另当别论了)。
此时,使用我们自己的数据集来微调已经训练好的大型语言模型为我们提供了更多的选择。在大规模集群计算中用大量数据训练的大型语言模型具有更好的兼容性和泛化能力。这并不意味着直接用特定数据训练的模型不够好。只要数据量和质量足够好,足够大,硬件设备足够强大,你就可以完全使用自己的数据集从头开始训练模型,效果可能会更好。
这恰恰意味着微调给了我们更多的选择。因此,大型语言模型的主流范式是在大量通用数据上预先训练语言模型训练,然后针对特定的下游任务进行微调,以达到领域自适应的目的。这里提到的微调与传统神经网络的迁移学习或微调基本相同,但也有相当大的差异。下面具体介绍大型语言模型中常用的微调方法。
微调大型模型可分为监督学习微调方法、无监督学习微调法和强化学习微调法:
- 监督学习微调方法:这是最常见的方法,即使用标记数据训练模型。例如,您可以收集一些对话问答数据集。在此过程中,将目标输出和输入设置为成对的示例,以优化模型。
- 无监督学习微调方法:当没有足够的标记文本时,大型语言模型可以继续对大量未标记的文本进行预训练,这有助于模型更好地理解语言结构。
- 强化学习微调方法:与传统的强化学习一样,首先构建一个文本质量比较模型(相当于 Critor)作为奖励模型,并对预训练模型为同一提示词给出的多个不同输出的质量进行排名。同时,该奖励模型可以使用二元分类模型来判断两个输入结果之间的利弊。然后,根据给定的提示词样本数据,使用奖励模型对预训练模型的用户提示词完成结果进行质量评估,并与语言模型目标取得更好的结果。强化学习微调将使 LLM 基于预训练模型生成的结果文本获得更好的结果。常用的强化学习方法有 DPO,ORPO,PPO 等。
微调大模型具体常用的方法主要分为两类,Model-Tuning 和 Prompt-Tuning:
1.全参数微调
最直接的方法是微调整个大型语言模型,这意味着所有参数都将被更新以适应新的数据集。这种方法也被称为低效微调,因为随着当前大型语言模型的参数量越来越大,所需的硬件资源呈指数级增长。举个例子:例如,要微调参数标度为 8B 的大型语言模型,可能需要 2 个 80G 的视频存储器或总共约 160G 的多卡加速训练。这种硬件投资相信会让大多数普通开发者望而却步。
2.Adapter-Tuning(适配器微调)
Adapter-Tuning 是谷歌研究人员首次提出的针对 BERT 的 PEFT 微调方法,也拉开了 PEFT 研究的序幕。当面临特定的下游任务时,如果完全微调(即预训练模型的所有参数都经过微调),效率太低;如果使用固定的预训练模型,只对接近下游任务的几层参数进行微调,很难达到更好的效果。因此,他们设计了适配器(Adapter)结构,将其嵌入 Transformer 结构中,并在训练过程中固定了原始预训练模型的参数,只对新添加的适配器结构进行了微调。同时为了保证训练的效率(也就是尽可能少的引入参数),他们将 Adapter 设计成这样的结构:首先,一个向下投影层将高维特征映射到低维特征,然后通过一个非线性层。之后,使用向上项目结构将低维特征映射回原始的高维特征。同时,还设计了一种跳过连接结构,以确保在最坏的情况下可以降级为身份。
论文:“用于 NLP 的参数高效迁移学习”( https://arxiv.org/pdf/1902.00751 )
代码: https://github.com/google-research/adapter-bert
3.Parameter-Efficient Prompt-Tuning(参数高效提示调优)
参数高效提示调优(Parameter-Efficient Prompt-Tuning)是一种高效、实用的模型微调方法。通过在训练输入前添加连续的任务相关嵌入向量,可以减少计算量和参数量,加快训练过程。同时,有效微调只需要少量数据,减少了对大量标记数据的依赖。此外,可以为不同的任务定制不同的提示,具有很强的任务适应性。在实际应用中,参数高效提示调优可以帮助我们快速适应各种任务需求,提高模型的性能。要实现参数高效提示调优,我们通常需要以下步骤:
- 定义任务相关的嵌入向量 - 根据任务需求定义连续的任务相关嵌入向量。这些向量可以手动设计,也可以通过其他方法自动学习。
- 修改输入前缀:在输入数据之前添加定义的嵌入向量作为前缀。这些前缀将与原始输入一起传递给模型进行训练。
- 微调模型:使用带有前缀的输入数据进行微调。在这个过程中,只有前缀部分的参数会被更新,而原始预训练模型的参数将保持不变。
- 评估与优化:评估模型在验证集上的性能,并进行优化调整。通过不断的迭代和优化,我们可以得到一个适合特定任务的微调模型。
论文:“规模的力量对于参数高效提示调优的作用” ( https://arxiv.org/pdf/2104.08691.pdf )
代码: https://github.com/google-research/prompt-tuning
4.Prefix-Tuning(前缀调优)
前缀调优方法建议在每个输入中添加一个连续的任务相关嵌入向量(连续的任务特定向量)进行训练。前缀调优仍然是一个固定的预训练参数,但除了为每个任务添加一个或多个嵌入外,它还使用多层感知器对前缀进行编码(请注意,多层感知器是前缀编码器),并且不再像提示调优那样继续输入 LLM。
在这里,连续(continuous)是相对于手动定义的文本提示标记(token)的离散(discrete)而言的。比如手动定义一个提示 token 数组是 ['The', 'movie', 'is', '[MASK]'],如果把那里的 token 替换成一个嵌入向量作为输入,那么这个嵌入就是一个连续(continuous)的表达。在重新训练下游任务的时候,固定原来大模型的所有参数,只重新训练与下游任务相关的前缀向量(prefix embedding)。对于自回归 LM 模型(比如我们当前示例中使用的 GPT-2),将在原始提示前添加前缀(z = [PREFIX; x; y]);对于编码器+解码器 LM 模型(比如 BART),将在编码器和解码器的输入上分别添加前缀(z = [PREFIX; x; PREFIX'; y],)。
论文:“前缀调优:优化生成的连续提示,P-Tuning v2:快速调整可与跨尺度和任务的普遍微调相媲美”( https://aclanthology.org/2021.acl-long.353 )
代码: https://github.com/XiangLi1999/PrefixTuning
5.P-Tuning 和 P-Tuning V2
P-Tuning 可以显著提高语言模型在多任务和低资源环境中的性能。它通过引入小规模、易于计算的前端子网络来增强输入特征,从而提高了基础模型的性能。P-tuning 仍然固定 LLM 参数,使用多层感知器和 LSTM 对提示进行编码,编码后,它通常在与其他向量连接后输入到 LLM。请注意,训练后,只保留提示编码后的向量,编码器不再保留。该方法不仅可以提高模型在各种任务中的准确性和鲁棒性,还可以显著减少微调过程中所需的数据量和计算成本。P-tuning 的问题在于它在小参数模型上表现不佳,因此有一个类似于 LoRA 的 V2 版本,新的参数嵌入在每一层中(称为 Deep FT)。
具体来说,P-Tuning v2是基于P-Tuning的升级版本。主要改进是采用了更有效的修剪方法,可以进一步减少模型微调的参数量。严格来说,P-tuning V2 并不是一个全新的方法,它是 Deep Prompt Tuning (Li and Liang,2021; Qin and Eisner,2021) 的优化版本。
P-tuning v2旨在用于生成和知识探索,但最重要的改进之一是将连续提示应用于预训练模型的每一层,而不仅仅是输入层。该方法只需要对 0.1% 到 3% 的参数进行微调,就能和 Model Fine-Tuning 比肩,可见其威力!
P-Tuning 论文:“GPT 也能理解”( https://arxiv.org/pdf/2103.10385 )。
6.LoRA
LoRA 方法首先冻结预训练模型的参数,并在解码器的每一层中添加额外的 dropout+Linear+Conv1d 参数。事实上,从根本上讲,LoRA 无法实现全参数微调的性能。根据实验,全参数微调比 LoRA 方法要好得多,但在资源不足的情况下,LoRA 成为更好的选择。LoRA 允许我们通过优化适应过程中密集层变化的秩分解矩阵来间接训练神经网络中的一些密集层,同时保持预训练权重不变。
LoRA 的特点:
- 可以共享一个经过良好预训练的模型,用于为不同的任务构建许多小型 LoRA 模块。我们可以冻结共享模型,并通过替换图 1 中的矩阵 A 和 B 来有效地切换任务,从而大大降低存储需求和任务切换开销。
- LoRA使训练更加高效。使用自适应优化器时,硬件阈值降低了 3 倍,因为我们不需要计算梯度或维护大多数参数的优化器状态。相反,我们只优化注入的、小得多的低秩矩阵。
- 我们简单的线性设计允许我们在部署过程中将可训练矩阵与冻结权重合并,与完全微调的模型相比,不会在结构中引入推理延迟。
- LoRA 与之前的许多方法无关,并且可以与许多方法相结合。
论文:“LORA:“大型语言模型的低秩适应”( https://arxiv.org/pdf/2106.09685.pdf )。
代码: https://github.com/microsoft/LoRA 。
7.AdaLoRA
有很多方法可以确定哪些 LoRA 参数比其他参数更重要,AdaLoRA 就是其中之一,AdaLoRA 的作者建议考虑 LoRA 矩阵的奇异值作为其重要性的指标。
与上面的 LoRA-drop 的一个重要区别是,LoRA-drop 中间层中的适配器要么经过了完全训练,要么根本没有经过训练。AdaLoRA 可以决定不同的适配器具有不同的等级(在原始 LoRA 方法中,所有适配器具有相同的等级)。
AdaLoRA 与相同等级的标准 LoRA 相比,总共有相同数量的参数,但这些参数的分布不同。在 LoRA 中,所有矩阵的秩都是相同的,而在 AdaLoRA 中有些矩阵的秩更高,有些矩阵的阶更低,因此最终的参数总数是相同的。实验表明,AdaLoRA 比标准 LoRA 方法产生更好的结果,这表明模型各部分的可训练参数分布更好,这对于给定的任务尤为重要,更靠近模型末尾的层提供了更高的排名,表明适应这些更重要。
AdaLoRA 通过奇异值分解将权重矩阵分解为增量矩阵,并动态调整每个增量矩阵中奇异值的大小,以便在微调过程中只更新对模型性能贡献更大或必要的参数,从而提高模型性能和参数效率。
论文:ADALORA:“自适应预算分配,实现参数高效微调”( https://arxiv.org/pdf/2303.10512 )。
代码: https://github.com/QingruZhang/AdaLoRA 。
8.QLoRA
QLoRA 通过冻结的 4 位量化预训练语言模型将梯度反向传播到低秩适配器(LoRA),这可以在保持完整 16 位微调任务的性能的同时,大大减少内存使用并节省计算资源。
QLoRA 的技术特点:
- 通过冻结的 4 位量化预训练语言模型将梯度反向传播到低阶适配器(LoRA)中。
- 引入 4 位 NormalFloat(NF4),这是一种理论上最优的数据类型,用于量化正态分布数据的信息,它可以产生比 4 位整数和 4 位浮点更好的经验结果。
- 应用双量化,一种量化量化常数的方法,平均每个参数节省约0.37位。
- 在处理具有长序列长度的小批量时,使用带有 NVIDIA 统一内存的分页优化器来避免梯度检查点期间的内存尖峰。显著降低了内存需求,允许在单个 48GB GPU 上微调 65B 参数模型,而不会降低运行时间或预测性能,与 16 位完全微调的基准相比。
论文:“QLORA:“量化 LLM 的有效微调”( https://arxiv.org/pdf/2305.14314.pdf )。
代码: https://github.com/artidoro/qlora 。
本文仅列出了几种常用的代表性方法,以及基于 LoRA 技术的一些变体:LoRA+、VeRA、LoRA-fa、LoRA-drop、DoRA 和 Delta-LoRA 等本文不会一一介绍,有兴趣的可以查阅相关文献。
当然,还有一些其他快速工程也能满足我们的技术需求(如 RAG 技术),在后续文章中,我将向您介绍。
接下来,我们将向您展示一个使用完整参数微调 GPT2 的示例。
制定交易策略
关于交易策略,我们用一个简单的例子来指导大语言模型的微调,暂时不涉及 EA 实现(具体实现需要等到我们完整的大语言模型推理策略完成后,我们才能合理地创建 EA)。首先,从客户端获取某一货币对某一时期的最新 20 个报价的收盘价,并将其平均值定义为 a。然后使用大型语言模型预测同一时期下 40 个报价的开盘价,并定义其平均值为 B。然后根据预测值判断下一步是买入还是卖出:
- 如果 40 个预测值的平均值 B 大于当前最新 20 个收盘价的平均值 A,则买入。
- 如果 40 个预测值的平均值 B 小于当前最新 20 个收盘价的平均值A,则卖出。
- 如果 A 和 B 相等或者非常接近,则不进行操作。
现在我们已经完成了交易策略的制定。这是一个相当简单的交易策略,可以理想化。您还可以根据需要替换此策略,例如将输入更改为动态,预测结果的总长度为 60 减去输入的长度。或者直接使用其他交易逻辑,如基于波浪策略、鳄鱼策略或乌龟策略制定规则,当然,你的模型也需要做出相应的调整。接下来,我们开始根据策略创建数据集,并对大型语言模型进行微调。
数据集的创建
在前面讨论训练大型语言模型时,我们已经制作了一个数据集,即“llm_data.csv”文件中包含的内容。该数据集仅包含 M5 周期内货币对的报价,并已相应处理,共有2442 行数据,每行有 64 列。具体的处理过程可以参考本系列文章中利用 CPU 或 GPU 训练大型语言模型的部分(具体链接为《将您自己的 LLM 集成到 EA(第 3 部分):使用 CPU 训练您自己的 LLM - MQL5 文章“)。当然,您也可以使用本文中提供的脚本重新定制数据集,或者将您自己的优秀想法变成数据集(例如将政府财政数据和汇率之间的相关性变成数据集等)。简而言之,这个数据集可以是任何形式,而不仅仅是数字引号。
1.预处理首先,我们导入所需的库:
import pandas as pd from transformers import GPT2LMHeadModel, GPT2Tokenizer, GPT2Config from transformers import TextDataset, DataCollatorForLanguageModeling from transformers import Trainer, TrainingArguments import torch
读取数据文件:
df = pd.read_csv('llm_data.csv')
我更新了这个数据集,这个数据集现在每行包含 M5 周期内货币对的 60 个收盘价,而不是原来的 64 个,数据被处理成文本格式:
sentences = [' '.join(map(str, prices)) for prices in df.iloc[:-10,1:].values]
这行代码主要读取整个 Dataframe 文件,遍历其元素,并将每一行转换为字符串,将其视为一个句子,每个句子包含60个收盘价。也就是说,我们将其转换为:“0.6119 0.61197 0.61201…0.61196”,而不是:“0.6119”“0.61197”…“0.61196”。这是为了让语言模型记住我们设定的序列长度,比如我们输入 20 个数据,模型会帮我们完成剩下的 40 个数据,而不是输出我们无法控制的内容。
这行代码中还有一个特殊的地方需要说明一下,就是“df.iloc[:-10,1:].values”。其中:“:-10”的意思是取 csv 文件开头到最后10行,剩下的10行我们留下来测试;“1:”是去掉每行的第一列,这一列是 csv 文件中的索引值,我们不需要它。
接下来,我们将所有的序列连接成一个数据集,并保存为“train.txt”,这样下次我们就不需要多次处理 csv 文件,只需直接读取处理后的文件即可。
with open('train.txt', 'w') as f: for sentence in sentences: f.write(sentence + '\n')
2.将数据加载为 Dataset 类
我们的数据预处理结束后,还需要使用 tokenizer 对数据进行进一步的处理,并加载到 pytorch 中的“Dataset”数据格式中。现在 Transformers 库中集成了一些常用的类来直接完成这项工作。在本文的例子中,可以直接使用 TextDataset 来实现这个功能,非常简单,但我们首先需要使用 GPT2 来实例化 tokenizer。如果你之前没有加载过 GPT2,那么第一次使用时,Transformers 库会从 Huggingface 下载预训练文件,请确保网络畅通。特别是使用 docke r或者 wsl 的朋友请确保你的网络配置正确,否则加载会失败。
tokenizer = GPT2Tokenizer.from_pretrained('gpt2') train_dataset = TextDataset(tokenizer=tokenizer, file_path="train.txt", block_size=60)
3.为语言模型加载数据
这里我们直接使用 Transformer 库中的 DataCollatorForLanguageModeling 类来实例化数据,不再需要做额外的工作。
data_collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False)
接下来,让我们加载预先训练的模型并进行微调。
微调模型
一旦我们准备好用于微调的数据集,我们就可以开始对大型语言模型进行微调的过程。
1.加载预训练模型
微调模型的第一步是加载预训练模型,我们已经加载了标记器,因此这里只需要加载模型:
model = GPT2LMHeadModel.from_pretrained('gpt2')
接下来我们需要设置训练参数。Transformers 库也为我们提供了一个非常方便的类来实现这个功能,不需要额外的配置文件:
training_args = TrainingArguments(output_dir="./gpt2_stock", overwrite_output_dir=True, num_train_epochs=3, per_device_train_batch_size=32, save_steps=10_000, save_total_limit=2, load_best_model_at_end=True, )
2.初始化微调参数
实例化 TrainingArguments 时,我们使用了以下参数:
- output_dir:保存预测结果和检查点的位置。我们将当前目录中的“gpt2_stock”文件夹定义为输出路径。
- overwrite_output_dir:是否覆盖输出文件,我们选择覆盖。
- num_train_epochs:训练世代的数量。我们选择了 3 个世代。
- per_device_train_batch_size:训练批次大小。我们选择的是 32,也就是我们前面介绍过的,最好是 2 的幂。
- save_steps=10_000:如果 save_strategy="steps",则两个检查点保存之前的更新步骤数。应为 [0,1) 范围内的整数或浮点数。如果小于 1,将被解释为总训练步数的比例。
- save_total_limit:如果传递了一个值,将会限制检查点的总量。删除 output_dir 中的旧检查点。
- load_best_model_at_end:是否在训练过程中加载最佳模型,而不是在训练的最后一步使用模型权重。
有很多参数我们没有设置而使用了默认值,因为我们只是举个例子,所以我们没有详细定义这个类,例如:
- deepspeed:是否使用 Deepspeed 加速训练。
- eval_steps:两次评估之间的更新步数。
- dataloader_pin_memory:是否要在数据加载器中固定内存。
可以看到这个 TrainingArguments 类的功能非常强大,几乎包含了大部分的训练参数,使用起来非常方便,非常建议读者看一下官方文档。
3.微调
现在让我们回到微调过程,现在我们可以定义微调过程。我们在上一篇文章中详细介绍了语言模型的训练过程。微调过程与训练过程没有太大区别。相信读者已经非常熟悉了,所以本文的例子不再详细定义语言模型的微调过程,而是直接使用 Transformer 库中提供的 Trainer 类来实现。现在我们将我们定义为参数的 model、training_args、data_collator、train_dataset 传入 Trainer 类中,从而实例化 Trainer:
trainer = Trainer(model=model, args=training_args, data_collator=data_collator, train_dataset=train_dataset,)
Trainer 类还有其他我们没有设置的参数,比如重要的回调:你可以使用回调来定制训练循环的行为,这些回调可以检查训练循环的状态(用于进度报告、在 TensorBoard 或其他 ML 平台上登录等)并做出决策(比如提前停止等)。我们在本文中没有设置它们的原因是,我们只是一个例子,微调过程中模型的参数设置相对保守。如果你想让你的模型更好地工作,请记住这个选项不应该被忽视。调用实例化的 Traine r类的 tran()方法,就可以直接运行微调过程:
trainer.train()
训练结束后,保存模型,这样我们在推断的时候就可以直接使用 from_pretrained() 方法加载微调后的模型:
trainer.save_model("./gpt2_stock")
接下来我们做一个推断来检查微调是否有效:
prompt = ' '.join(map(str, df.iloc[:,1:20].values[-1])) generated = tokenizer.decode(model.generate(tokenizer.encode(prompt, return_tensors='pt').to("cuda"), do_sample=True, max_length=200)[0], skip_special_tokens=True) print(f"test the model:{generated}")
这部分代码中,“prompt = ' '.join(map(str, df.iloc[:,1:20].values[-1]))”将我们数据集的最后一行转换成字符串格式。 “tokenizer.encode(prompt, return_tensors='pt')”这部分代码将输入的文本(prompt)转换成模型可以理解的形式,也就是将文本转换成一系列的 token。 “return_tensors='pt'”表示返回的数据类型是 PyTorch 张量。 “do_sample=True”表示生成过程中使用随机抽样,“max_length=200”限制生成文本的最大长度。现在让我们来看看整个代码运行的结果:
可以看出,微调的预训练模型成功地输出了我们想要的结果。
完整代码如下,附件中脚本名称为“Fin-tuning.py”:
import pandas as pd from transformers import GPT2LMHeadModel, GPT2Tokenizer, GPT2Config from transformers import TextDataset, DataCollatorForLanguageModeling from transformers import Trainer, TrainingArguments import torch dvc='cuda' if torch.cuda.is_available() else 'cpu' print(dvc) 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('gpt2') train_dataset = TextDataset(tokenizer=tokenizer, file_path="train.txt", block_size=60) data_collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False) model = GPT2LMHeadModel.from_pretrained('gpt2') training_args = TrainingArguments(output_dir="./gpt2_stock", overwrite_output_dir=True, num_train_epochs=3, per_device_train_batch_size=32, save_steps=10_000, save_total_limit=2, load_best_model_at_end=True, ) trainer = Trainer(model=model, args=training_args, data_collator=data_collator, train_dataset=train_dataset,) trainer.train() trainer.save_model("./gpt2_stock") 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}")
测试
微调过程完成后,我们仍然需要测试模型,检查模型输出与原始真值之间的差距,最简单的方法是计算真值与预测值之间的均方误差(MSE)。
现在我们重新创建一个脚本来实现测试过程,首先导入所需的库,加载我们微调过的 GPT2 模型和数据:
import pandas as pd from transformers import GPT2LMHeadModel, GPT2Tokenizer, GPT2Config from sklearn.metrics import mean_squared_error import torch import numpy as np df = pd.read_csv('llm_data.csv') dvc='cuda' if torch.cuda.is_available() else 'cpu' model = GPT2LMHeadModel.from_pretrained('./gpt2_stock') tokenizer = GPT2Tokenizer.from_pretrained('gpt2')
这个过程与我们的微调过程没有太大不同,除了模型路径被更改为我们在微调过程中保存模型权重的路径。加载模型和标记器后,我们需要处理推理后的真实值和预测值。这部分也与我们的微调脚本中的步骤相同:
prompt = ' '.join(map(str, df.iloc[:,1:20].values[-1])) generated = tokenizer.decode(model.generate(tokenizer.encode(prompt, return_tensors='pt'), do_sample=True, max_length=200)[0], skip_special_tokens=True)
现在我们将数据集最后一行的最后 40 个收盘价作为真实值,并将真实值和预测值转换为列表形式,长度是一致的:
true_prices= df.iloc[-1:,21:].values.tolist()[0] generated_prices=generated.split('\n')[0] generated_prices=list(map(float,generated_prices.split())) generated_prices=generated_prices[0:len(true_prices)] 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)
为了保持真实值和预测值的长度一致,我们需要按照最小列表的长度来切割另一个列表,所以我们定义“trim_lists(a, b)”来完成这个任务。然后我们打印真实值和预测值,看看它们是否符合预期:
print(f"true_prices:{true_prices}") print(f"generated_prices:{generated_prices}")
可以看到结果如下:
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, 0.61227, 0.61231, 0.61228, 0.61227, 0.61233, 0.61211, 0.6121, 0.6121, 0.61195, 0.61196]
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.61169、0.61191、0.61195、0.61204、0.61188、0.61205、0.61188、 0.612、0.61208、
0.612, 0.61192, 0.61168, 0.61165, 0.61164, 0.61179, 0.61183, 0.61192, 0.61168, 0.61175, 0.61169, 0.61162]
接下来,我们可以计算它们的均方误差(MSE),然后打印出结果来检查:
mse = mean_squared_error(true_prices, generated_prices) print('MSE:', mse)
结果为:MSE:2.1906250000000092e-07。
如你所见,MSE 非常小,但这真的意味着我们的模型非常准确吗?请不要忘记我们原始数据的数值非常小!所以虽然 MSE 很小,但是因为我们的原始值也比较小,所以此时 MSE 还不能准确反映模型的准确率。我们需要进一步计算预测值与原始值之间的均方根误差(RMSE)和归一化均方根误差(NRMSE),进一步确定预测误差相对于观测值范围的大小,进一步确定模型的准确率:
rmse=np.sqrt(mse) nrmse=rmse/(np.max(true_prices)-np.min(generated_prices)) print(f"RMSE:{rmse},NRMSE:{nrmse}")
结果为:
- RMSE:0.00046804113067122735
- NRMSE:0.5850514133390986
我们可以观察到,虽然 MSE 和 RMSE 值很小,但是 NRMSE 值为 0.5850514133390986,这意味着预测误差约占观测值范围的 58.5%。这说明虽然 RMSE 的绝对值很小,但是相对于观测值的范围,预测误差还是比较大的。
那么我们如何才能让我们的模型更加准确呢?以下是一些选择:
- 在微调过程中增加迭代次数
- 增加数据量
- 适当优化微调参数
- 更换更大尺度的模型
这些方法实现起来都不难,本文就不一一验证了,大家可以按照自己的想法选择其中一种或者几种去实践,相信结果肯定会比本文的例子好很多!
完整代码,附件中脚本名称为 test.py:
import pandas as pd from transformers import GPT2LMHeadModel, GPT2Tokenizer, GPT2Config from sklearn.metrics import mean_squared_error import torch import numpy as np df = pd.read_csv('llm_data.csv') dvc='cuda' if torch.cuda.is_available() else 'cpu' model = GPT2LMHeadModel.from_pretrained('./gpt2_stock') tokenizer = GPT2Tokenizer.from_pretrained('gpt2') prompt = ' '.join(map(str, df.iloc[:,1:20].values[-1])) generated = tokenizer.decode(model.generate(tokenizer.encode(prompt, return_tensors='pt'), do_sample=True, max_length=200)[0], skip_special_tokens=True) true_prices= df.iloc[-1:,21:].values.tolist()[0] generated_prices=generated.split('\n')[0] generated_prices=list(map(float,generated_prices.split())) generated_prices=generated_prices[0:len(true_prices)] 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"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}")
结论
本文主要介绍使用大型语言模型进行交易策略的先决条件,即大型语言模型的输出必须满足我们交易策略的要求。我们讨论了一些可以完成这项任务的技术方法。虽然由于篇幅限制,我们没有提供所有方法的相关代码示例,但只给出了一个用全参数微调 GPT2 的示例(当然,这个数据集并不适用于文中提到的所有微调方法,但后面文章中的详细示例将给出与该方法匹配的数据集创建方法)。但别担心,我将在以下文章中选择一些具有代表性的方法来提供相关的代码示例和与示例相匹配的 EA 示例。至于文中简单提到的 RAG 技术和 Agent 技术,也会有专门的文章为您提供详细的讨论和相关的代码实现。
你准备好了吗?下篇文章再见!
参考
https://alexqdh.github.io/posts/2183061656/
http://note.iawen.com/note/llm/finetune
本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/13497

