Qwen2.5模型Windows CPU部署与微调实战:环境配置、参数调优全解析

101

Windows CPU环境下的Qwen2.5系列模型:从部署到微调全攻略

本文将详细介绍如何在Windows系统和CPU环境下,使用Qwen2.5系列模型完成从下载、部署到微调的全过程。即使没有强大的GPU资源,也能玩转大型语言模型。

1、环境配置

1.1、部署开发环境

本地开发环境:Windows

(1)Python开发环境

①PyCharm:下载并安装PyCharm(Community版本):www.jetbrains.com/pycharm/download/

②Anaconda:下载并安装Anaconda,推荐使用清华镜像源加速下载,选择合适的版本,例如:Anaconda3-2024.06-1-Windows-x86_64.exe。下载完成后,在Windows系统下双击exe文件,一路Next即可完成安装。

③配置Anaconda环境变量(非必须): 假设Anaconda安装在D:\soft\anaconda,进入系统高级配置,添加系统变量:

添加系统变量ANACONDA_HOME:变量值为D:\soft\anaconda

然后编辑Path变量,添加以下变量:

%ANACONDA_HOME% 
%ANACONDA_HOME%\Scripts 
%ANACONDA_HOME%\Library\mingw-w64\bin
%ANACONDA_HOME%\Library\usr\bin 
%ANACONDA_HOME%\Library\bin

配置完成后,在命令行输入conda --version,如果能看到Anaconda的版本信息,则表示安装成功。

④Anaconda使用:

一些常用的命令,帮助我们使用Anaconda:

conda create -n qwen1.5-4b python=3.10
conda env list
conda activate qwen1.5-4b
conda deactivate
python --version

新建名为qwen1.5-4b的conda虚拟环境后,在PyCharm的setting->Python Interpreter中,选择并导入创建好的conda环境。

1.2、大模型下载

从Hugging Face Hub下载模型比较常见,这里主要介绍使用ModelScope魔搭社区的组件下载。

ModelScope,魔搭社区提供了相应的组件来供使用者下载:

pip install modelscope
modelscope download --model qwen/Qwen2.5-1.5B
modelscope download --model qwen/Qwen2.5-1.5B README.md

示例:

在base的conda环境下安装ModelScope组件,然后调用modelscope命令进行下载。该组件支持断点续传,即使网络不稳定,也可以随时中断并重新执行命令,已下载的文件不会丢失,可以从上次的进度继续下载。

2、大模型部署使用

2.1、概述

以下是一个使用Qwen2.5-1.5B模型的示例代码:

import torch
from flask import Flask
from flask import request
from transformers import (AutoTokenizer, AutoModelForCausalLM, AutoModel,
                          Qwen2ForCausalLM, Qwen2Tokenizer)

max_new_tokens: int = 512  # 生成响应时最大的新token数
system_prompt = "你是一个专门分类新闻标题的分析模型。你的任务是判断给定新闻短文本标题的分类。"
user_template_prompt = ("请评估以下网购评论的情感,不要返回0或1以外的任何信息,不要返回你的思考过程。"
                        "输入正面评论输出1,输入负面评论输出0。输入如下:{}\n请填写你的输出")
eos_token_id = [151645, 151643]

app = Flask(__name__)
model_path = "D:\\project\\llm\\Qwen2.5-1.5B"
tokenizer = Qwen2Tokenizer.from_pretrained(model_path)
model = Qwen2ForCausalLM.from_pretrained(model_path, device_map='cpu').eval()

@app.route('/chat', methods=["POST"])
def chat():
    # 系统设定和prompt
    req_json = request.json
    content = user_template_prompt.format(req_json['message'])
    messages = [
        {"role": "system", "content": system_prompt},
        {"role": "user", "content": content}
    ]
    print("input: " + content)
    input_ids = tokenizer.apply_chat_template(messages,
                                              tokenize=False,
                                              add_generation_prompt=True)
    inputs = tokenizer([input_ids], return_tensors="pt").to(model.device)
    generated_ids = model.generate(
        inputs.input_ids,
        max_new_tokens=max_new_tokens,
        eos_token_id=eos_token_id,  # 结束令牌,模型生成这个token时,停止生成
    )
    generated_ids = [
        output_ids[len(inputs):] for inputs, output_ids in zip(inputs.input_ids, generated_ids)
    ]
    print(f"generated_ids=\n{generated_ids}")
    response = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
    print(response)
    # 使用占位符拼接字符串
    return response

if __name__ == '__main__':
    app.run(port=8080, host="0.0.0.0")

代码中使用了Qwen2ForCausalLM替换了AutoModelForCausalLMQwen2ForCausalLM是一个基于Transformer结构的decoder-only模型,它是Qwen大模型的第二代架构。

2.2、参数

通常来说,参数越大的大模型效果越好。以下是一些关键参数及其作用,以及使用时需要注意的地方:

apply_chat_template

用于会话中,告诉大模型接下来应该生成新的内容,并且包含了整个会话的上下文信息。使用如下:

input_ids = tokenizer.apply_chat_template(messages,  # 输入的纯文本信息
                                          tokenize=False,  # 告诉大模型暂时不需要分词
                                          add_generation_prompt=True)  # 添加一个特殊的标记,告诉大模型接下来应该生成新的文本内容。

tokenize=False 表示不要立即分词,只是使用apply_chat_template将会话进行格式化。

eos_token_id

设定了大模型生成文本的分割符,其中eos_token_id = [151645, 151643],这两个ID分别对应:

tokenizer.convert_ids_to_tokens(151645)    # <|im_end|>
tokenizer.convert_ids_to_tokens(151643)    # <|endoftext|>
tokenizer.convert_ids_to_tokens(1773)      # 。

这两个标记与输入中的标记保持一致。若不设置该值,在未达到最大返回token数之前,对话将不会自动终止,大模型可能会进行不必要的自问自答。

为了控制大模型可能产生的不稳定输出,设置停用词是一种有效手段。除了使用 eos_token_id 外,还可以采用以下方法:

from transformers import GenerationConfig

generation_config = GenerationConfig(
    use_cache=True,
    repetition_penalty=repetition_penalty,
    do_sample=False,  # 取消采样,使用贪心策略,输出是确定的
    stop_strings="}"
)
generated_ids = model.generate(
    input_ids,
    tokenizer=tokenizer,
    generation_config=generation_config
)

对于模型的推理参数,可以统一放置在 GenerationConfig 中。通过 stop_strings 参数(其值可为字符串或字符串列表)来设置停用词。在上例中,将 } 设为停用词,这在要求大模型返回JSON数据时尤为有效,能够有效避免大模型在输出完整JSON数据后继续进行不必要的推理。

repetition_penalty

使用如下:

repetition_penalty: float = 1.2  # 用于惩罚重复生成相同token的参数
generated_ids = model.generate(
    inputs.input_ids,
    max_new_tokens=max_new_tokens,
    repetition_penalty=repetition_penalty,  # 解决问题后面有过多重复问答
)

某些时候,大模型会持续重复之前的对话,直到生成的token数等于max_new_tokens为止。repetition_penalty用于控制重复生成相同token的参数。这个值不宜过低或过高:过低不生效;过高会导致大模型不敢生成正确答案,因为输入的prompt中携带了正确答案。1.2是一个比较合适的值。

skip_special_tokens=True

这个参数告诉分词器在解码时跳过任何特殊的标记,如结束标记end-of-sequence token或其他模型特定的标记。

如果在调用模型时设置了停用词,大模型推理到停用词就会返回输出。如果不设置skip_special_tokens=True参数,则输出会包含特殊token。

2.3、总结

本章节主要关注qwen2.5-1.5b模型的部署和参数调优,实现了以下成果:

  1. 修改了启动参数之后,大模型能够正常输出预期的结果
  2. 对于相对简单的问题,模型能够给出较好的答案
2.3.1、最终代码

最终代码如下:

import torch
from flask import Flask
from flask import request
from transformers import (AutoTokenizer, AutoModelForCausalLM, AutoModel,
                          Qwen2ForCausalLM, Qwen2Tokenizer)
from peft import PeftModel

max_new_tokens: int = 64  # 生成响应时最大的新token数
temperature: float = 0.6  # 控制生成文本的随机性
top_p: float = 0.9  # 用于概率限制的参数,有助于控制生成文本的多样性
top_k: int = 32  # 控制生成过程中每一步考虑的最可能token的数量
repetition_penalty: float = 1.2  # 用于惩罚重复生成相同token的参数
system_template_prompt = "你是一个专门评估网购评论情感的分析模型。你的任务是判断给定评论是正面还是负面。"
system_prompt = "你是一个专门分类新闻标题的分析模型。你的任务是判断给定新闻短文本标题的分类。"
user_template_prompt = ("请评估以下评论,不要返回0或1以外的任何信息,不要返回你的思考过程。"
                        "如果是正面评论输出1,是反面评论输出0。输入如下:{}\n请填写你的输出:")
user_prompt = ("请将以下新闻短文本标题分类到以下类别之一:故事、文化、娱乐、体育、财经、房产、汽车、教育、"
               "科技、军事、旅游、国际、股票、农业、游戏。输入如下:\n{}\n请填写你的输出:")
eos_token_id = [151645, 151643]

app = Flask(__name__)
lora_model_path = "./output/Qwen2.5-1.5b/checkpoint-100"
model_path = "D:\\project\\llm\\Qwen2.5-1.5B"
tokenizer = Qwen2Tokenizer.from_pretrained(model_path)
model = Qwen2ForCausalLM.from_pretrained(model_path, device_map='cpu').eval()

@app.route('/chat_old', methods=["POST"])
def chat_old():
    # 系统设定和prompt
    req_json = request.json
    content = user_template_prompt.format(req_json['message'])
    messages = [
        {"role": "system", "content": system_template_prompt},
        {"role": "user", "content": content}
    ]
    # 使用tokenizer将整个会话转换成模型可以理解的input_ids,并将这些input_ids输入到模型
    # tokenize=False 表示不要立即分词,只是使用apply_chat_template将会话进行格式化
    input_ids = tokenizer.apply_chat_template(messages,
                                              tokenize=False,
                                              add_generation_prompt=True)
    print(f"input:{input_ids}")
    inputs = tokenizer([input_ids], return_tensors="pt").to(model.device)
    generated_ids = model.generate(
        inputs.input_ids,
        max_new_tokens=max_new_tokens,
        repetition_penalty=repetition_penalty,  # 解决问题后面有过多重复问答(对重复token的惩罚)
        eos_token_id=eos_token_id,  # 结束令牌,模型生成这个token时,停止生成
    )
    generated_ids = [
        output_ids[len(inputs):] for inputs, output_ids in zip(inputs.input_ids, generated_ids)
    ]
    print(f"generated_ids=\n{generated_ids}")
    response = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
    response = response.encode('utf-8', errors='ignore').decode('utf-8')
    print(response)
    # 使用占位符拼接字符串
    return response

@app.route('/chat', methods=["POST"])
def chat():
    # 系统设定和prompt
    req_json = request.json
    prompt = user_prompt.format(req_json['message'])
    print(prompt)
    # 非会话的输入方式,将单句话进行分词成token ids
    inputs = tokenizer(prompt, return_tensors="pt")
    input_ids = inputs.input_ids

    # Generate
    generate_ids = model.generate(input_ids=input_ids,
                                  bos_token_id=151645,  # 开始令牌(在生成文本时,模型会在输入序列的末尾添加这个令牌,以指示新生成的文本的开始。)
                                  max_new_tokens=len(input_ids) + 1,
                                  repetition_penalty=repetition_penalty)
    print(generate_ids)
    response = tokenizer.batch_decode(generate_ids, skip_special_tokens=True)[0]
    print(response)
    # # 去掉response中的包含prompt的文本
    response = response[len(prompt):]
    return response.strip()
    

if __name__ == '__main__':
    app.run(port=8080, host="0.0.0.0")
2.3.2、数据集
  • 二元分类数据集:电商平台评论数据集
  • 多元分类数据集:今日头条文本分类数据集
  • 其他:天池数据集、ChineseNlpCorpus

大模型学习资源

掌握AI工具是程序员应对AI时代的最佳策略。LLM相关内容繁多,但网上现有资源不成体系,导致自学成本高。

这里提供一份LLM大模型资料,包括书籍、行业报告、学习视频、学习路线、开源教程等。希望能够帮助大家更好地学习。

LLM大模型经典书籍

AI大模型已成为科技领域的热点,以下书籍是学习的优质资源。

640套LLM大模型报告合集

该合集覆盖大模型的理论研究、技术实现、行业应用等,为研究人员、工程师和爱好者提供宝贵信息。

LLM大模型系列视频教程

提供系统学习的视频教程。

LLM大模型开源教程(LLaLA/Meta/chatglm/chatgpt)

提供多种开源大模型的教程。