Intership|LLM应用从训练到构建

以下有很多东西已脱密处理,可读性会降低。

1 LLM预训练中的参数设置以及训练设置

1.1 参数设置

1
CUDA_VISIBLE_DEVICES=0 python src/train_bash.py \ --stage sft \ --model_name_or_path /model/baichuan2/Baichuan2-7B-chat \ --do_train \ --dataset classify\ --template baichuan2 \ --finetuning_type lora \ --lora_target all \ --output_dir /cephfs/home/aooxin/LLM/LLM_model/classify_3_1_1 \ --overwrite_cache \ --per_device_train_batch_size 1 \ --per_device_eval_batch_size 1 \ --gradient_accumulation_steps 4 \ --lr_scheduler_type cosine \ --logging_steps 10 \ --save_steps 100 \ --lora_rank 64 \ --lora_alpha 128 \ --learning_rate 5e-5 \ --num_train_epochs 300 \ --plot_loss \ --fp16
  • stage:指令监督微调
  • model_name_or_path:模型我们选择Baichuan2
  • finetuning_type:选择lora
  • per_device_eval_batch_size:目前使用的3090只支持1 batch per gpu
  • learning_rate:经过多次尝试后,发现5e-5的学习率比较适合我们的任务
  • num_train_epochs:训练轮次可以根据数据量的多少来设置

1.2 训练加速

加速可以选择AccelerateDeepSpeed
在这里我们选择了DeepSpeed,DeepSpeed可以通过zero技术增加通信代价来减少显存消耗,但是我们的3090每张卡可以单独放下一个batch,所以我们使用zero 0的设置,实际上就为普通的DDP多卡并行。不同Zero的设置大致效果如下:

速度上:阶段 0 (DDP) > 阶段 1 > 阶段 2 > 阶段 3
显存上:阶段 0 (DDP) < 阶段 1 < 阶段 2 < 阶段 3

需要增加以下依赖

1
2
3
4
5
6
deepspeed==0.12.3
transformers==4.34.1
datasets==2.14.7
tiktoken==0.5.1
peft==0.6.2
trl==0.7.1

zero 0的deepspeed配置文件如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
{

"bf16": {

"enabled": false

},

"fp16": {

"enabled": true

},

"zero_optimization": {

"stage": 0

},

"gradient_clipping": 1.0,

"train_batch_size": "auto",

"train_micro_batch_size_per_gpu": 1,

"gradient_accumulation_steps": 4,

"steps_per_print": 2000000

}

使用deepspeed对应的训练启动命令也要修改,如果使用单机8卡的机器,可以使用以下命令启动

1
deepspeed --include="localhost:0,1,2,3,4,5,6,7" src/train_bash.py --stage sft --model_name_or_path /model/baichuan2/Baichuan2-7B-chat --do_train --dataset classify --template baichuan2 --finetuning_type lora --lora_target all --output_dir /cephfs/home/aooxin/LLM/LLM_model/classify_3_1_1 --overwrite_cache --per_device_train_batch_size 1 --per_device_eval_batch_size 1 --gradient_accumulation_steps 4 --lr_scheduler_type cosine --logging_steps 10 --save_steps 100 --lora_rank 64 --lora_alpha 128 --learning_rate 5e-5 --num_train_epochs 300 --plot_loss --fp16 --deepspeed /root/ds_config.json

1.3 完整一次训练流程

1.3.1 训练

1
deepspeed --include="localhost:0,1,2,3,4,5,6,7" src/train_bash.py --stage sft --model_name_or_path /model/baichuan2/Baichuan2-7B-chat --do_train --dataset classify --template baichuan2 --finetuning_type lora --lora_target all --output_dir /cephfs/home/aooxin/LLM/LLM_model/classify_3_1_1 --overwrite_cache --per_device_train_batch_size 1 --per_device_eval_batch_size 1 --gradient_accumulation_steps 4 --lr_scheduler_type cosine --logging_steps 10 --save_steps 100 --lora_rank 64 --lora_alpha 128 --learning_rate 5e-5 --num_train_epochs 300 --plot_loss --fp16 --deepspeed /root/ds_config.json

1.3.2 导出

1
python src/export_model.py \ --model_name_or_path /model/baichuan2/Baichuan2-7B-chat \ --template baichuan2 \ --finetuning_type lora \ --checkpoint_dir /cephfs/home/aooxin/LLM/LLM_model/classify_3_2_p1check_p4/ \ --output_dir /cephfs/home/aooxin/LLM/LLM_model/model_classify_3_2_p1check_p4 \ --fp16

1.3.3 量化

目前资源够使用,没有使用量化模型的步骤,如需,可以使用chatglm进行量化

1.3.4 api部署

需要对api_demo.py进行修改,修改如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import os

import uvicorn

from llmtuner import ChatModel, create_app



def main():

chat_model = ChatModel()

app = create_app(chat_model)

port = int(os.getenv("PORT", 8000))

uvicorn.run(app, host="0.0.0.0", port=port, workers=1)


if __name__ == "__main__":

main()
1
PORT=8000 python src/api_demo.py \ --model_name_or_path /cephfs/home/aooxin/LLM/LLM_model/model_classify_3_8_p1check_p4check_str \ --template baichuan2 \ --finetuning_type lora

2 数据集构造

2.1 构造样式

由于我们实现的功能有几种,所以数据集的构造也有一定的细微差别,主要体现在input prompt上

2.1.1 每种类型格式

  • prompt构造

    此处删除

    2.1.2 构造原因

    分为3种

  • input中不需要额外prompt:比如classify和extract,这两种llm的实现不需要外界额外信息,因此不需要额外的prompt。

  • input中需要一种额外prompt:比如head和point,这两种的实现只需要少量外界额外信息,因此只需要补充上额外的信息即可。

  • input中需要两种额外prompt:比如line和surface,这两种军标中,每个小的军标都可能存在不同的算法,所以需要在额外基础信息的情况下,加上军标的补充说明去训练,比如下面这两种军标,存在完全不同的计算方法,所以针对某种军标需要单独设计数据去参与训练。

    • 作战分界线中,0为不含1为含
    • 燕尾箭头中,通过起点、经过点、终点确定,由于起点需要两个点,所以需根据南北、东西走向确定,如果是南北,则在起点的经度上进行+-0.005;如果是东西,则在起点的纬度上+-0.005

2.2 完整构造流程

一次完整的构造流程分为生成数据->校准数据->转化数据

2.2.1 生成数据

生成数据我们使用gpt-4-turbo-preview,之所以使用gpt-4-turbo-preview是因为它相比于其他模型具有限定json格式输出的功能,其tokens长度也完全足够我们使用。与gpt4相比,他的单位tokens也更便宜,如下,gpt3.5没有限制json输出的功能,综合来讲选择gpt-4-turbo-preview比较合理。

Model Input Output
gpt-4-0125-preview $10.00 / 1M tokens $30.00 / 1M tokens
gpt-4-1106-preview $10.00 / 1M tokens $30.00 / 1M tokens
gpt-4-1106-vision-preview $10.00 / 1M tokens $30.00 / 1M tokens
gpt-4 $30.00 / 1M tokens $60.00 / 1M tokens
gpt-4-32k $60.00 / 1M tokens $120.00 / 1M tokens
gpt-3.5-turbo-0125 $0.50 / 1M tokens $1.50 / 1M tokens
gpt-3.5-turbo-instruct $1.50 / 1M tokens $2.00 / 1M tokens
classify的生成代码如下,其余的代码在gitlab中
另外在生成数据的时候,可能要注意以下几点:
  1. 可以人工给出的信息,尽量提前给出:大模型可能会有很强的某种数据的倾向,尽管这些倾向有时候看起来匪夷所思,比如示例军标我们给出一个直升机,让他随机,他就可能倾向于战斗机等等的其他种类的飞机,而忽略其他军标,所以随机数据上减少对gpt的使用,比如ID可以使用uuid,军标可以通过python随机提前给出,这样gpt生成的效果会更好。

  2. 信息与信息有强相关性,就要一并给出:军标有中文和英文,生成数据的时候就要一并给出,不能让gpt自己猜测,防止生成的数据训练出来的模型未来也使用猜测的英文。

  3. 数学逻辑特别要注意:比如某点南北展开,对应我们的点需要纬度上+-0.005,这样的在生成的时候可以分开生成,先强制全部生成南北的,后续我们重新给出东西的例子,再重新生成东西。可以避免后续的矫正。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104

    import os

    from openai import OpenAI

    from dotenv import load_dotenv

    import json

    from tqdm import tqdm




    # Load environment variables

    load_dotenv()



    # Initialize OpenAI client

    client = OpenAI(

    api_key=os.environ.get("OPENAI_API_KEY"),

    )

    folder_path = 'data'



    # Number of data entries to generate

    n = 50 # Adjust n to the desired number of data entries



    # Loop to generate and print n data entries

    for i in tqdm(range(n)):

    # Define the prompt for each iteration

    prompt = f"""

    生成第{i+1}条数据。以下是一个示例和指令:

    我给你一个例子,需要生成新的json数据

    {{

    "instruction" : "*******",

    "input" : "*********",

    "output":

    {{

    ******

    }}

    请生成更多这样的json数据,注意*****

    """



    # Generate the completion for the current iteration

    chat_completion = client.chat.completions.create(

    messages=[

    {

    "role": "user",

    "content": prompt,

    }

    ],

    model="gpt-4-turbo-preview",

    response_format={ "type": "json_object" },

    )

    file_path = os.path.join(folder_path, f'{i+1}.json')

    generated_content = chat_completion.choices[0].message.content if chat_completion.choices else 'No response'

    # Save the generated data to a JSON file

    with open(file_path, 'w', encoding='utf-8') as file:

    # Assuming generated_content is a string in JSON format, directly write it

    file.write(generated_content)

    生成好之后需要对数据进行拼接,将多个json拼接成一个完整的即可。

2.2.2 校准数据

校准数据就需要按照每种数据的需求,对关键点进行人工判断,修改出错的地方。

2.2.3 转化数据

经过测试,如果直接将json作为output输入给训练程序效果会不好。
可以使用json.dumps将output转化为字符串即可。

3 后端实现

在langchain和直接使用代码自己实现功能的两种选择中,选择直接自己用python实现相关功能,原因是:一是langchain中的向量数据库的知识库形式并不精准,在我们的应用中不能起到好的效果,二是如果使用langchain调用api没有必要,白白增加了一层应用,不如自己手动调用。
这里列举几个langchain使用的例子:针对特定文档的问答:根据给定的文档回答问题,使用这些文档中的信息来创建答案。Agents:开发可以决定行动、采取这些行动、观察结果并继续执行直到完成的代理。可以看出langchain是可以完成多agents功能,但是目前我们的任务流水执行暂时不需要让模型去选择调用哪个api,数据是单向流动,未来如果考虑减少使用到的模型的数量可能就需要使用langchain来做。

3.1 服务器api

服务器api这里需要实现一些查询功能以及计算的后处理功能。我选择Flask实现

查询部分:
查询部分的内容非常清楚,如下表

端点 方法 参数/请求体 返回内容 状态码
/get_location GET address: 地名列表,逗号分隔 JSON格式的地点信息列表 200: 成功
404: 未找到数据
/get_jb GET name: 军标名称列表,逗号分隔 JSON格式的军标信息列表 200: 成功
404: 未找到数据
/add_location POST JSON: 包含地名经度纬度高度字段 确认信息 201: 成功添加
400: 请求中缺少JSON或数据
/update_location POST JSON: 包含地名经度纬度高度字段 确认信息 200: 成功更新
400: 请求中缺少JSON或数据
404: 未找到地点
/get_id GET JSON格式,包含生成的随机ID 200: 成功

计算部分:
计算部分我在这里举个例子:此处已删除

3.2 大模型api

端点 方法 摘要 请求体需求 响应类型及数据结构
/v1/models GET 列出模型 200: 成功,返回ModelList
/v1/chat/completions POST 创建聊天完成 需要,ChatCompletionRequest 200: 成功,返回ChatCompletionResponse
422: 验证错误

使用python对大模型进行调用即可

3.3 本地python

本地python对于各个大模型、服务器api按照流水进行使用即可,可以注意以下几点:

  1. 在写整体的流水线处理的时候,因为每次调用都是输入文本->扩展prompt+调用插件->调用llm->结果的后处理。可以注意代码的复用,避免重复,未来也可以更好地扩展。

  2. 因为每次调用模型都要花费时间,所以加入日志记录模块可以很快的找到是输入、扩展prompt还是后处理哪一步出错了,日志中可以按我们处理的流程记录每一个文件生成的经过。

  3. 在遇到:营指挥所、指挥所 这种选择问题,可以考虑加入分词,比如jieba。预先创建分词数据库,使用最长前缀匹配可以很好的解决这种问题。在这个应用中,可以想到分词的功能可能不止这点,比如说在最初的文本输入后,实际上可以使用传统分词将所有的军标先提取出来,这样我们就得到了军标们和最初文本,然后每一次输入大模型的都是完整的文本,和军标之一,这样可以巧妙避免前后文相关的情况,但是同时对数据集的构造制造了更大的难度。

4 不足

  1. 数据集数量影响模型效果:特别是在line、surface中,不同军标都需要有一部分自己的数据集,生成合理的数据集是一个庞大的任务。
  2. 每种计算需要单独设计z:对于坐标的复杂计算,需要单独设计计算方式。
  3. 需要再增加一个llm专门用于对坐标的解释:比如有的坐标和南北方位有关,但是南北的表示又有很多不同,所以需要设计一个llm对类似的坐标方位进行一个固定的转化,目前demo阶段并未设计实现。
  4. 需要一种统一的文件:llm的表现和数据集的形式强相关,如果换一种文件输入,原来的数据集就需要重新生成,导致应用可用性会降低。