
将您自己的 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
注意: 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.

