
将您自己的 LLM 集成到 EA 中(第 3 部分):使用 CPU 训练自己的 LLM
概述
亲爱的朋友们,好久不见了!
看到这个标题,您可能会有点惊讶,但您没看错,我们确实要这样做!
在本系列的前一篇文章中,我们讨论了运行大型语言模型的基本环境设置,并在 WSL 中使用 llama.cpp 运行了一个简单的 LLM 实例。最令人兴奋的是,即使没有强大的 GPU,您仍然可以纯粹用 CPU 运行这个例子!本系列教程将尽可能降低硬件要求,努力确保读者可以尝试验证示例,而不会受到硬件问题的阻碍。当然,在我们的模型训练部分,我们还将介绍不同硬件平台的分支,包括纯 CPU 版本和支持 AMD 显卡加速计算的版本,相信每个人都可以在没有硬件限制的情况下尝试。
当然,您可能会想:用 CPU 训练的模型有用吗?这样的模型有什么意义呢?事实上,如果你想训练一个具有复杂功能的模型或使用 CPU 解决复杂任务,这是相当困难的,但仍然可以用它来实现一些特定且相对简单的功能。
在本文中,我们将介绍如何使用 CPU 训练大型语言模型,并创建训练大型语言模块所需的金融数据集。这可能涉及我在其他文章中提到的知识,我在这里不再重复。如果读者想更深入地研究,请阅读我的相关文章,其中将提供相关链接。
目录:
关于大型语言模型数据集
我们知道,在这个阶段,几乎所有的大型语言模型都是基于Transformers 的。本文中我们不会深入探讨 Transformers 模型的原理,但感兴趣的读者可以参考官方文档来理解。我们只需要知道,处理相关数据集的方法已经集成到一些成熟的库中,如 “Transformers” 和 “tiktoken”,这些数据处理模型可以在 “Transformer” 库或 “tiktoken” 库中方便地找到。1.标记器(Tokenizer)
标记器分割算法是 NLP 语言模型中最基本的组成部分。基于标记器,文本可以转换为独立的标记(token)列表,然后可以转换为计算机可以理解的输入向量。在标记器中,我们使用预训练模型进行文本规范化、预分割、基于分割模型的分割、后处理等。如前所述,标记器还集成了各种预训练模型(如GPT、GPT-2、GPT-J、GPT Neo、RoBERTa、BART、LLaMA、AlBERT、T5、mBART、XLNet等),我们可以方便地选择不同的预训练模型来处理数据(当然,您也可以训练自己的分割模型)。
2.不同的标记器具有不同的用途和目的:
- 编码器(Encoder)模型:主要模型包括 ALBERT、BERT、DistilBERT、ELECTRA、RoBERTa,适用于需要理解完整句子的任务,如句子分类、命名实体识别(以及更一般的单词分类)和提取式问答。
- 解码器(Decoder)模型:主要模型包括“CTRL”、“GPT”、“GPT-2”和 Transformer XL。解码器模型的预训练通常围绕预测句子中的下一个单词展开。这些模型最适合涉及文本生成的任务。
- 编码器-解码器模型:主要模型包括‘BART’、‘T5’、‘Marian’、‘mBART’。这些模型最适合基于给定输入生成新句子的任务,如摘要、翻译或生成式问答。
为了使模型能够识别序列的开始和结束,我们通常在使用标记器进行分割时添加特殊符号,如 [CLS]、[SEP] 等。在本文中,我们将使用 ['<|endoftext|>'] 作为序列终止符。
创建数据集
在训练我们自己的模型时,创建数据集通常是最大的挑战,因为有很多关于如何用现有数据集训练模型的教程,但很少有教程告诉你如何创建自己的数据集。因此,你可能很容易训练一个模型,但不知道如何根据自己的想法创建数据集。关于这部分,大家可以参考我的“时间序列挖掘的数据标签”系列文章(共 6 篇文章,包括“时间序列挖掘的数据标签(一):通过 EA 运行图制作带有趋势标记的数据集”),希望能够给大家一些启发。当然,您也可以将这些知识应用于大型语言模型的训练。
现在,让我们回到我们的主题。我们仍在从 MetaTrader5 客户端获取数据,然后处理数据。考虑到我们在 CPU 上运行,并且考虑到迄今为止大多数 PC 的性能,我们定义的序列长度不太大。否则,它将运行得太慢,导致测试体验不佳。 请注意,本文中的示例仅用于演示如何使用 CPU 进行训练,因此数据集创建和模型训练只是示例,结果可能并不理想。如果你想要更好的结果,你可能需要准备一个更大的数据集或一个更适合任务期望的数据集,并执行额外的数据处理。您可能还需要相应地调整模型参数,但这些主题不会在这个基本示例中涉及。是时候开始逐步创建数据集了:
1.定义全局变量
主要用于定义文件路径。
DATA_DIR = os.path.dirname(__file__)
data_file = os.path.join(DATA_DIR, "llm_data.csv")
2.从客户端获取数据
由于 CPU 训练的局限性,我们将获得长度为 2500个 数据点的单个货币对的数据作为初始数据。
mt_data_len=2500 sr_len=60 if not mt.initialize(): print("mt initialize failed!") else: sbs=mt.symbols_get(group='*micro*') if sbs is not None: # for i in [mt.TIMEFRAME_M5,mt.TIMEFRAME_M15,mt.TIMEFRAME_H1,mt.TIMEFRAME_D1]:
请注意:
我们使用函数 'mt.symbols_get(group='*micro*')' 来获取客户端中的货币对,因为我的账户是微型账户,所以我使用 group='*micro*' 来查找带有 “micro” 的货币对。如果您使用的是标准账户,则需要删除此条件。否则,您将找不到任何货币对。当然,您可以修改 “micro” 以匹配您感兴趣的货币对,例如使用 “GBP” 将所有货币对与英镑匹配。
3.分割数据
考虑到 CPU 的计算能力,我们将只从报价中取“close”列(收盘价),从索引 0 开始,将每 60 个报价视为一个序列,并丢弃长度小于 60 的序列。这样,我们就简单地创建了一个序列集合,每个序列长度为 60 个报价。当然,长度可以根据您个人 CPU 的计算能力来改变。原则上,序列越长,潜在效果越好。在代码中,我们使用了两个 for 循环来控制周期和品种的选择,这可以很容易地添加更多的周期和更多的货币对,并且可以根据需要随时调整数据集。
for i in [mt.TIMEFRAME_M5,]: xy=None # xy_list=[] ct=0 for j in sbs: if ct>0: break print(j.name) d_=mt.copy_rates_from_pos(j.name,i,0,mt_data_len) df_d=pd.DataFrame(d_) cl_d=df_d['close'] k=0 while k+1: if mt_data_len-k>=sr_len: cl_ds=cl_d[k:k+sr_len].tolist() if xy is None: xy=pd.DataFrame([cl_ds]) # xy_list=[cl_ds] else: xy.loc[len(xy)]=cl_ds # xy_list.append(cl_ds) k+=1 else: break ct+=1 mt.shutdown()
请注意:
- 我们使用变量 “ct” 来控制获取多少个货币对的数据。
- “k” 用于控制数据索引偏移量,从而获取序列数据。如果数据序列长度小于 “sr_len” 变量中定义的长度,它将停止向数据集 “xy” 添加序列。
4.将处理好的数据写入文件
当然,这只是一个可选步骤,您也可以在不保存文件的情况下继续进行数据处理。但是,由于我们将来会继续使用这些数据,并且不需要重复获取,因此仍然建议保存它。
xy.to_csv(data_file)此时,我们自己的数据集已经完成。我们将把这部分代码封装到一个函数中,以便于调用。
def get_data(): mt_data_len=2500 sr_len=60 if not mt.initialize(): print("mt initialize failed!") else: sbs=mt.symbols_get(group='*micro*') if sbs is not None: # for i in [mt.TIMEFRAME_M5,mt.TIMEFRAME_M15,mt.TIMEFRAME_H1,mt.TIMEFRAME_D1]: for i in [mt.TIMEFRAME_M5,]: xy=None # xy_list=[] ct=0 for j in sbs: if ct>0: break print(j.name) d_=mt.copy_rates_from_pos(j.name,i,0,mt_data_len) df_d=pd.DataFrame(d_) cl_d=df_d['close'] k=0 while k+1: if mt_data_len-k>=sr_len: cl_ds=cl_d[k:k+sr_len].tolist() if xy is None: xy=pd.DataFrame([cl_ds]) # xy_list=[cl_ds] else: xy.loc[len(xy)]=cl_ds # xy_list.append(cl_ds) k+=1 else: break ct+=1 mt.shutdown() # print(len(xy)," ",len(xy_list)) xy.to_csv(data_file) # xy.to_json(f'llm_data.json') return xy请注意:如前所述,本文中数据集的创建只是一个例子。您可以根据自己的想法完全更改内部的参数并对其进行测试。
数据处理
我们已经提到了标记器,现在拥有了一个数据集,下一步是使用标记器处理数据。在这个例子中,我们将使用 tiktoken 库并选择预训练的模型 “gpt2” 来编码我们的数据集。1.读取数据
获取我们创建的数据有两种方法:一是读取保存的文件,二是直接使用 get_data() 函数的返回值。
值得注意的是,如果我们读取保存的文件,我们需要删除在保存和读取过程中添加的额外的第一行和第二列,而使用函数的返回值则不需要这样做。data=get_data() # data=pd.read_csv(data_file) # data=data.iloc[1:,1:]我们将默认使用函数的返回值来获取数据。
2.定义变量
在这里,我们需要实例化标记器并定义特殊的标记。如前所述,本文使用“<|endoftext|>”作为序列的开始和结束。
enc = tiktoken.get_encoding("gpt2") encode = lambda s: enc.encode_ordinary(s) eot = enc._special_tokens['<|endoftext|>'] train_tokens=[] val_tokens=[] val_cut=len(data)//10
请注意:
“val_cut” 用于分割训练数据集和验证集。如果要更改训练集与验证集的比率,可以将数字 10 更改为您认为合适的值。在此示例中,数据总长度的 10% 被用作验证集。
3.定义写入 Bin 文件的函数
在最终的数据处理之前,我们需要定义将处理后的数据写入 bin (二进制)文件的函数。
def data_to_file(path, tks): header = np.zeros(256, dtype=np.int32) header[0] = 20240520 header[1] = 1 header[2] = len(tks) toks_np = np.array(tks, dtype=np.uint16) with open(path, "wb") as f: f.write(header.tobytes()) f.write(toks_np.tobytes())
这个函数本身并不难,但重要的是要注意 “header[0]=20240520” 这个值,这可能会在后续的大型模型训练中使用。在加载数据以训练大型模型的过程中,将检查此值,如果不匹配,将出现错误。这点需要特别注意!
4.数据标记
首先,我们使用 for 循环遍历数据集的每一行以获得每个序列,变量 “i” 接收序列的行号,“r” 接收序列。
for i,r in data.iterrows():
此时,“r” 以 Series 格式存储数据。我们需要先将其转换为列表格式,然后将列表格式转换为字符串序列,样式为 “x,x,x,x,...”。当然,我们可以直接使用 f'{ser}' 将序列转换为字符串包装的列表样式 “[x,x,x,x,x,…]”,但这似乎有点奇怪,所以让我们坚持使用 “x,x,x,x,x,…” 样式。ser=r.tolist() ser= ''.join(str(elem) for elem in ser)
接下来,我们对序列进行编码,将数据集的前 10% 存储到 val_tokens 中,其余部分存储到 train_tokens 中,并调用 data_to_file() 函数将它们写入各自的 bin 文件中:
tokens = encode(ser) if i< val_cut: val_tokens.append(eot) val_tokens.extend(tokens) enc_f = os.path.join(DATA_DIR, "val_data.bin") data_to_file(enc_f, val_tokens) else: train_tokens.append(eot) train_tokens.extend(tokens) enc_f = os.path.join(DATA_DIR, "train_data.bin") data_to_file(enc_f, train_tokens)现在,我们已经完成了数据标记的过程。从数据采集到标记化的整个过程已经结束,我们将把这些内容写入一个名为 “data_enc.py” 的文件中。整个文件的完整代码:
import MetaTrader5 as mt import pandas as pd import numpy as np import os import tiktoken DATA_DIR = os.path.dirname(__file__) data_file = os.path.join(DATA_DIR, "llm_data.csv") def get_data(): mt_data_len=2500 sr_len=60 if not mt.initialize(): print("mt initialize failed!") else: sbs=mt.symbols_get(group='*micro*') if sbs is not None: # for i in [mt.TIMEFRAME_M5,mt.TIMEFRAME_M15,mt.TIMEFRAME_H1,mt.TIMEFRAME_D1]: for i in [mt.TIMEFRAME_M5,]: xy=None # xy_list=[] ct=0 for j in sbs: if ct>0: break print(j.name) d_=mt.copy_rates_from_pos(j.name,i,0,mt_data_len) df_d=pd.DataFrame(d_) cl_d=df_d['close'] k=0 while k+1: if mt_data_len-k>=sr_len: cl_ds=cl_d[k:k+sr_len].tolist() if xy is None: xy=pd.DataFrame([cl_ds]) # xy_list=[cl_ds] else: xy.loc[len(xy)]=cl_ds # xy_list.append(cl_ds) k+=1 else: break ct+=1 mt.shutdown() # print(len(xy)," ",len(xy_list)) xy.to_csv(data_file) # xy.to_json(f'llm_data.json') return xy def data_to_file(path, tks): header = np.zeros(256, dtype=np.int32) header[0] = 20240520 header[1] = 1 header[2] = len(tks) toks_np = np.array(tks, dtype=np.uint16) with open(path, "wb") as f: f.write(header.tobytes()) f.write(toks_np.tobytes()) if __name__=="__main__": data=get_data() # data=pd.read_csv(data_file) # data=data.iloc[1:,1:] enc = tiktoken.get_encoding("gpt2") encode = lambda s: enc.encode_ordinary(s) eot = enc._special_tokens['<|endoftext|>'] train_tokens=[] val_tokens=[] val_cut=len(data)//10 for i,r in data.iterrows(): ser=r.tolist() ser=''.join(str(elem) for elem in ser) # ser = ser.strip() tokens = encode(ser) if i< val_cut: val_tokens.append(eot) val_tokens.extend(tokens) enc_f = os.path.join(DATA_DIR, "val_data.bin") data_to_file(enc_f, val_tokens) else: train_tokens.append(eot) train_tokens.extend(tokens) enc_f = os.path.join(DATA_DIR, "train_data.bin") data_to_file(enc_f, train_tokens) print(f"tain:{len(train_tokens)}",f"val:{len(val_tokens)}")接下来,我们将训练我们的大型模型。让我们开始吧!
请注意:
- 第一次使用 tiktoken 库进行标记化时,它将连接到互联网,从 huggingface 下载相应的预训练模型,因此请确保您的网络可以访问 huggingface。若无法访问,请提前准备好解决方案!当然,您也可以使用预先准备好的本地模型进行标记化,但本文不会讨论这部分。
- 通常,大型模型数据处理需要对数据进行填充和掩码处理。由于我们的数据集很小,每个序列的长度都是固定的,所以我们没有这样做。但如果你想创建一个庞大而复杂的数据集,请仔细考虑数据的选择和清理。这一步非常重要,极大地影响了模型的质量。此外,我们的数据集处理可能有点粗糙,但对于演示示例来说已经足够了。
- 标记器本身的功能是将文本转换为数字列表,而我们的数据本身是数字的,那么为什么要标记呢?就我个人而言,我认为处理方法取决于最终的模型任务规划。模型设计的功能和任务决定了数据处理方法。
训练我们的大型语言模型
起初,这可能看起来是最复杂的部分,但在 GitHub 上的开源项目 llm.c 的帮助下,我们只需要使用它的训练文件。所有文件中的注释都非常详细,几乎为每个步骤提供了解释,因此不需要对本文中的代码进行单独的分析或解释。1.训练前的准备
llm.c 项目网址: https://github.com/karpathy/llm.c 。
本文的示例代码平台是前面介绍的部署在 Windows 上的 WSL 环境,使用 miniconda 作为 Python 解释器环境,Python 版本为3.11.5。
在开始训练之前,请使用 git clone 下载 llm.c 项目,并确保安装了项目的 requirements.txt 所需的库,并且在 WSL 中安装了“make”。
git clone https://github.com/karpathy/llm.c.git
cd llm.c
pip install -r requirements.txt
2.训练我们的模型
首先在 Windows 上打开 MetaTrader5 客户端获取数据,并找到文件 data_enc.py 的位置(注意这个文件也必须放在 Windows 环境下,我没有测试过在 WSL 下是否可以获取数据),运行命令 “python data_enc.py”。
python data_enc.py脚本运行后,我们会在同一个目录下生成两个文件 “train_data.bin” 和 “val_data.bin”,我们需要将它们复制到WSL文件系统中。当然,由于 WSL 可以完全读取 Windows 文件系统的内容,因此复制文件并不是必需的步骤。
获取数据之后,我们还需要运行llm.c项目根目录下的 “train_gpt2.py” 文件。有两个选项可供参考:
- 直接修改 “train_gpt2.py” 第397行 “--input_bin” 的默认值,替换为我们的数据文件的路径,例如 “dev/data/mt5/val_data.bin”,然后运行 “python train_gpt2.py”。
- 直接在命令行中添加参数来定位我们的数据文件,例如 “python train_gpt2.py --input_bin dev/data/mt5/val_data.bin”。
我们选择在命令行上使用参数运行此文件。
python train_gpt2.py --input_bin dev/data/mt5/val_data.bin运行结果:

啊哈,结果是这样的:"<|endoftext|>40% of the remaining 80% of the remaining 20% of the remaining 40"。这个测试输出看起来有点出乎意料的奇怪!但这不应该阻止我们前进,所以我们继续!
事实上,“train_gpt2.py” 文件并不能完全训练我们的模型,它的任务是从我们的数据集中取出一批来初始化模型,并将保存四个 “.bin” 格式的文件:
- 模型参数文件以“float32”和“bfloat16”格式保存(文件分别为“gpt2_124M.bin”和“gpt2_124M_bf16.bin”);
- 标记器文件,名为 “gpt2_Tokenizer.bin”;
- 调试 C 的文件,“gpt2_124M_debug_state.bin”。
在脚本运行期间,将执行几个推理步骤并输出结果,这是对模型的初步测试。本文将不再解释文件中的其他代码,因为源代码提供了非常详细的注释,使读者能够清楚地理解整个过程。
运行 “train_gpt2.py” 脚本后,我们需要编译训练代码。但在编译之前,我们还需要修改源代码 “train_gpt2.c” 中的数据读取路径,以便在 CPU 上进行训练。在这个 C 文件的第 1041 行左右,定义了两个常量 “const char* train_tokens” 和 “const char* val_tokens”,我们需要把它们的值改为我们自己的 “train_data.bin” 和 “val_data.bin” 路径。例如,在我修改之后:
const char* train_tokens = "dev/data/mt5/train_data.bin"; const char* val_tokens = "dev/data/mt5/val_data.bin";
请记住不要忘记语句末尾的 “;”,这与 Python 语法不同!同样,我不想解释太多这个文件的源代码,因为作者的源代码包含详细的注释,使其易于理解整个训练过程。
修改源代码后,直接在命令行中运行 “make train_gpt2”。如果您尚未安装 CUDA 加速计算库,则此命令将直接将训练源代码编译为可以在 CPU 上运行的程序。
如图所示:
如果你已经成功编译了训练程序,我们现在可以正式开始训练我们的大型语言模型了。直接在命令行中运行命令 “OMP_NUM_THREADS=10 ./train_gpt2”,其中 “OMP_NUM_THREADS” 参数指定你使用的线程数,请根据硬件设备支持的线程总数设置适当的值。
运行结果:
本次训练过程中的测试输出为:
```
generating: --- 30.605360.605540.605590.605510.605650.605510.605650.60550.605550.605540.605590.605540.605590.606107<|endoftext|>0.605640.60 --- ```
现在,回顾原始数据,我自己的 “llm_data.csv” 文件中的值如下:
让我们比较一下。原始值大多在 0.6 左右,因此输出似乎缺少分隔符。前面说过了,我们的数据格式是 “x,x,x,x,…”,但输出却是 “xxxxxxx…”。然而,我不认为这是一个问题,因为我们的想法已经得到验证,训练的大型模型可以简单地输出我们想要的结果!这条路径是可行的,可以通过优化编码和解码过程来解决小问题。
由于这只是一个演示示例,我们没有提到在客户端中保存模型或测试交易策略。此外,由于精度问题,在 CPU 上训练的模型不需要保存和测试。我认为,如果你想让这个模型真正有用,你可能需要建立一个科学的过程并付出更多的努力。例如,为特定任务设计数据集,制定适当的交易策略,设计和训练满足任务要求的编码器和解码器模型,设置模型超参数等。所有这些都需要大量的练习才能完成,而且不太可能在几篇文章中得到清楚的解释,所以这个过程仍然需要读者自己探索。这篇文章只为你提供了更多的可能性。
当然,这也提出了一个问题,即如何在交易策略或测试中使用我们训练好的语言模型。最简单的例子是,我们可以在 Python 环境中运行大型模型推理服务,然后通过 socket 通过交易策略 EA 发送服务请求,推理服务计算结果并将其返回给 EA。这部分内容在我的《时间序列挖掘的数据标签(五):使用 Socket 在 EA 中应用和测试》一文中有详述,感兴趣的读者可以尝试一下。本文将不再讨论相关内容!
数据处理脚本文件和获得的数据文件附在文章末尾,处理后的 bin 格式文件不包含在附件中,因为不支持 bin 格式上传
请注意:
在WSL根目录下的mnt目录中,您可以找到我们 Windows 的所有驱动器号,您可以在 WSL 的相应驱动器号下找到 Windows 中的文件,例如 “/mnt/g/Program files”。需要注意的是,如果 Windows 文件夹名称中包含空格,就像例子中的“cd /mnt/g/Program Files”我们是无法直接进入这个文件夹的,我们需要用引号把带有空格字符的文件夹名称包起来,“cd /mnt/g/'Program Files'” 才是正确的方法。
结论
令人难以置信的是,我们只用一个 CPU 就成功地训练了自己的大型语言模型!
但不要太快激动,本文只通过一个简单的例子演示了如何用 CPU 训练我们自己的大型语言模型,而且它显然受到硬件条件的限制。用 CPU 训练的模型可能只有一个功能,性能也不好,但不可否认的是,它也可以作为在定量交易中实施特定策略的选择。
在下一篇文章中,我们将介绍如何使用图形卡来加速模型的训练。如果你使用的是 AMD 显卡,你可能会很恼火地发现各种常见库不支持 AMD 加速(当然,我说的是现阶段,也就是作者写这篇文章的时候。从长远来看,我仍然相信 AMD 的 AI 生态系统在不久的将来会变得更好!或者像 “llama.cpp” 这样支持所有平台的AI生态系统将变得更加流行!),因此,本着尽可能覆盖所有平台的初衷,我们将在下一篇文章中讨论如何使用 AMD 显卡来加速我们大型语言模型的训练!我可能不会讨论如何使用 NVIDIA 显卡进行加速计算,因为我目前使用的是 AMD 平台,但如果你可以使用 AMD 进行加速训练,那么就没有必要与拥有更好人工智能生态系统的 NVIDIA 进行额外的合作,对吧?
下篇文章再见!
参考
llm.c: https://github.com/karpathy/llm.c.git
本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/13920

