【飞桨AI实战】PaddleNLP大模型指令微调,从0打造你的专属家常菜谱管家

1.项目背景

家庭烹饪作为日常生活的重要组成部分,不仅关乎健康,也是家庭情感交流的重要方式。

相信很多小伙伴在烹饪时也会困惑:不知道如何选择合适的食材和菜谱,或者缺乏灵感来创造新的菜品。

最近看到一本《家庭实用菜谱大全》,就想能不能结合它做一款推荐家常菜谱的专属大模型出来。

PaddleNLP大模型套件是一个基于飞桨(PaddlePaddle)开发的大语言模型(LLM)开发库,提供了大量的预训练 LLM 和高级 API,本次分享我们将尝试用飞桨的大模型套件,进行指令微调,做一款能够推荐菜品、具体食材和做法的大模型。

先简单画一个框架图,盘点一下本项目的具体工作,希望给感兴趣的同学一点大模型应用上的参考和帮助。

2. 百度 AI Studio 平台

本次将采用 AI Studio 平台中的免费 GPU 资源,在平台注册账号后,点击创建项目-选择 NoteBook 任务,然后添加数据集,如下图所示,完成项目创建。启动环境选择 GPU 资源。

创建项目的方式有两种:

  • 一是在 AI Studio 平台参考如下方式,新建项目。

为了快速跑通项目流程,建议直接 fork 源项目。

3. 开始实战

3.1 菜谱数据生成

注:大模型微调使用的对话数据,已经上传到数据集中,可直接跳转到 3.2 进行微调使用。本节将主要介绍数据的制作过程,供感兴趣的同学参考。

首先我们准备一个虚拟环境,这样每次重启项目时,无需重新安装项目依赖

打开一个终端,在根目录下创建一个虚拟环境:

复制代码
conda create -p /home/aistudio/envs/bot python=3.10

一键启动虚拟环境:

复制代码
source activate /home/aistudio/envs/bot/

进入项目目录,安装相关依赖:

复制代码
cd /home/aistudio/recipe_bot/
pip install -r requirements.txt

3.1.1 文本提取

本次用于菜谱信息提取的文档,存放在加载的数据集中:/home/aistudio/data/data290409/家庭实用菜谱大全.pdf

参考代码:/home/aistudio/recipe_bot/core.py

我们首先需要将文档中的文本信息提取出来,可以采用两种方法:

  • 方式一:PDF 文字提取:选用 PyPDF2
  • 方式一:文字识别 OCR,选用 PaddleOCR

方式一实现:

复制代码
from PyPDF2 import PdfReader
def pypdf_to_txt(input_pdf, output_path='data/pypdf'):
    if not os.path.exists(output_path):
        os.makedirs(output_path)
    pdf_reader = PdfReader(input_pdf)
    # 遍历PDF的每一页
    for page_num in tqdm(range(len(pdf_reader.pages)), desc='pypdf_to_txt'):
        page = pdf_reader.pages[page_num]
        text = page.extract_text()
        # 将文本写入txt文件
        with open(f'{output_path}/{page_num:03d}.txt', "w", encoding="utf-8") as f:
            f.write(text)
if __name__ == '__main__':
    pypdf_to_txt('/home/aistudio/data/data290409/家庭实用菜谱大全.pdf')

方式二实现:

复制代码
from paddleocr import PaddleOCR
ppocr = PaddleOCR(use_angle_cls=True, debug=False)

def img_ocr(img_path=None, img_data=None):
    if img_data is not None:
        img = img_data
    else:
        img = cv2.imread(img_path)
    result = ppocr.ocr(img)[0]
    texts = []
    if result:
        for line in result:
            # box = line[0]
            text = line[1][0]
            texts.append(text)
    return '\n'.join(texts)

def pdfocr_to_txt(input_pdf, output_path='data/ocr'):
    if not os.path.exists(output_path):
        os.makedirs(output_path)
    pdf_document = fitz.open(input_pdf)
    # 遍历PDF的每一页
    for page_num in tqdm(range(pdf_document.page_count)):
        if os.path.exists(f'{output_path}/{page_num:03d}.txt'):
            continue
        page = pdf_document.load_page(page_num)
        pm = page.get_pixmap()
        pm.save("temp.png")
        img = cv2.imread("temp.png")
        texts = img_ocr(img_data=img)
        with open(f'{output_path}/{page_num:03d}.txt', "w", encoding="utf-8") as f:
            f.write(texts)
if __name__ == '__main__':
    pdfocr_to_txt('/home/aistudio/data/data290409/家庭实用菜谱大全.pdf')

上述代码会将每一页文本内容提取出来保存成 .txt 文件,给大家展示下两种方法提取的结果(左-方法一,右-方法二):

我们发现,单纯的 OCR 无法胜任文档结构化任务,相对方法二,方法一PyPDF2提取的内容更符合预期一点,不过依然不是我们想要的内容。

此时,不得不祭出最擅长文本结构化的 LLM 了~

3.1.2 文本结构化

LLM 我们选择直接调用百度开放的 ErnieBot API,省去本地部署的麻烦。

参考代码:/home/aistudio/recipe_bot/llm.py

注:调用 ErnieBot API,需要在代码中指定erniebot.access_token,可以通过如下方式获取:右上角账号头像-个人中心-访问令牌。

首先,编写对话代码:

复制代码
def chat_completion(text='', system='', messages=[], model='ernie-3.5'):
    if not messages:
        messages = [{'role': 'user', 'content': text}]
    response = erniebot.ChatCompletion.create(
        model=model,
        messages=messages,
        system=system
    )
    return response.get_result()

然后,编写给 LLM 的角色提示词:

复制代码
system_reorg = '''
您是经验丰富的星级大厨和文档解析大师,擅长从图片中提取的文本中精准提取出结构化信息。我会给你文本内容,其中包括3-4道菜谱内容,因为文本内容是通过OCR识别出来的,所以内容有些错乱,您需要从中提取出每道菜的【菜名、材料、调料、做法、特点和厨师一点通】,并整理成markdown格式输出。
要求:
1. 直接回答提取内容即可,不要回答其他任何内容。
2. 整理成markdown格式输出,但不需要加```markdown和```。
3. 输出的markdown格式如下:
## 菜名1
### 材料
- 材料1 - 数量/重量
--- 此处省略,完整提示词可参考项目代码 ---
'''

下面,编写批量化处理代码:

复制代码
# 获取菜品信息
def get_recipe_info(input_path='data/pypdf', output_path='data/md/recipe'):
    if not os.path.exists(output_path):
        os.makedirs(output_path)
    files = os.listdir(input_path)
    for file in tqdm(files):
        output_file = os.path.join(output_path, file.replace('.txt', '.md'))
        if os.path.exists(output_file):
            continue
        text = open(os.path.join(input_path, file), 'r').read()
        result = chat_completion(text=text, system=system_reorg)
        if result:
            with open(output_file, 'w') as f:
                f.write(result)
if __name__ == '__main__':
    get_recipe_info()

给大家看下 ErnieBot 提取的结果:

整体还是符合预期的,其中 高汤·大匙(具体量未给出),是因为文本并未识别成功,LLM 自然无法给出。

接下来,我们还要提取目录信息,同样编写角色提示词让 LLM 帮我们搞定:

最终,我们在 data/md 文件夹下得到两份 .json 文件,recipe.jsondirectory.json,分别是结构化的菜谱数据和分类数据:

怎么把上述数据,转换成可供大模型微调的数据呢?

3.1.3 对话数据生成

参考飞桨大模型精调文档,PaddleNLP支持的数据格式是每行一个字典,每个字典包含以下字段:

  • src : str, List(str), 模型的输入指令(instruction)、提示(prompt),模型应该执行的任务。
  • tgt : str, List(str), 模型的输出。
  • context(可选):在训练过程中动态调整 system prompt,传入 system 字段。

为此,参考上述格式,我们可以编写如下函数生成对话数据

复制代码
def get_sft_data():
    context = {'system': '您是一位五星级大厨,擅长回答关于一切有关菜谱的问题,包括菜品推荐,食材选择,具体做法等等。'}
    results = []
    # 生成和类别相关的问题
    dir_dict = json.load(open('data/md/directory.json', 'r'))
    for cate, name_list in dir_dict.items():
        for i in range(10):
            src = f'请您帮我推荐几道关于{cate}的菜品'
            tgt = random.sample(name_list, random.randint(2, min(len(name_list), 10)))
            results.append({'src': src, 'tgt': '\n'.join(tgt), 'context': context})
        for i in range(10):
            num = random.randint(2, min(len(name_list), 10))
            src = f'请您帮我推荐{num}道关于{cate}的菜品'
            tgt = random.sample(name_list, num)
            results.append({'src': src, 'tgt': '\n'.join(tgt), 'context': context})
    # 生成和菜名相关的问题
    rec_dict = json.load(open('data/md/recipe.json', 'r'))
    for title, content_dict in rec_dict.items():
        material = '\n'.join(content_dict.get('材料', ''))
        if not material.strip():
            continue
        seasonings = '\n'.join(content_dict.get('调料', ''))
        steps = '\n'.join(content_dict.get('做法', ''))
        charcter = '\n'.join(content_dict.get('特点', ''))
        tips = '\n'.join(content_dict.get('厨师一点通', ''))
        src = f'{title}这道菜需要准备些什么食材和调料'
        tgt = f'{title}这道菜需要准备的食材有:{material},调料有:{seasonings}'
        results.append({'src': src, 'tgt': tgt, 'context': context})
        src = f'{title}这道菜怎么做'
        if tips:
            tgt = f'{title}这道菜的具体做法如下:{steps},最后再给你点小建议:{tips}'
        else:
            tgt = f'{title}这道菜的具体做法如下:{steps}'
        results.append({'src': src, 'tgt': tgt, 'context': context})
        if charcter:
            src = f'{title}这道菜有什么特点'
            tgt = f'{title}的特点是:{charcter}'
            results.append({'src': src, 'tgt': tgt, 'context': context})
            src = f'我今天想吃点{charcter}的菜,你可以帮我推荐一道菜么'
            tgt = f'没问题,{title}这道菜{charcter},需要准备的食材有:{material},调料有:{seasonings},具体做法如下:{steps},最后再给你点小建议:{tips}'
            results.append({'src': src, 'tgt': tgt, 'context': context})
    random.shuffle(results)
    with open('data/sft_data.json', 'w') as f:
        f.write(json.dumps(results, ensure_ascii=False, indent=4))

上述代码分别生成:和类别相关的问题,和菜名相关的问题,共计 2083 条用于训练,我们随机抽取 100 条用于验证。

训练 & 验证数据放在了:/home/aistudio/data/data290409/:

  • train.json
  • dev.json

感兴趣的小伙伴,也可参考上述代码自行生成。

3.2 LLM 指令微调

数据准备好之后,我们开启 LLM 指令微调。

3.2.1 环境准备

首先,打开一个终端,下载 PaddleNLP 并安装依赖:

复制代码
git clone https://github.com/PaddlePaddle/PaddleNLP.git
cd /home/aistudio/PaddleNLP/
pip install --upgrade paddlenlp==3.0.0b0
pip install paddlepaddle-gpu==3.0.0b1 -i https://www.paddlepaddle.org.cn/packages/stable/cu118/

为了方便大家快速跑通流程,这里我们选用 Qwen/Qwen2-0.5B 进行简单测试:

复制代码
from paddlenlp.transformers import AutoTokenizer, AutoModelForCausalLM
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2-0.5B")
model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2-0.5B", dtype="float16")
input_features = tokenizer("你好!请自我介绍一下。", return_tensors="pd")
outputs = model.generate(**input_features, max_length=128)
print(tokenizer.batch_decode(outputs[0]))

Qwen/Qwen2-0.5B推理大概占用 4G 显存。

3.2.2 数据准备

微调需要进入 llm 目录:

复制代码
cd /home/aistudio/PaddleNLP/llm

然后准备训练数据,我们在第一步已经生成,直接软链接过来:

复制代码
mkdir data_sft
ln -s /home/aistudio/data/data290409/train.json data_sft/train.json
ln -s /home/aistudio/data/data290409/dev.json data_sft/dev.json

下面将开始进行模型训练,LLM 微调包括多种方式,这里我们选用最常见的两种方式进行展示。

  • SFT 全参微调:对模型的所有参数进行微调,需要更多的计算资源和时间,特别是对参数量巨大的模型
  • LoRA 微调:一种参数高效的微调方法,基于 LLM 的内在低秩特性,通过增加旁路矩阵来模拟全参数微调。只训练新增参数,而保持原始参数固定,训练速度更快。

两种微调的脚本均为:python run_finetune.py,唯一的区别是指定不同的配置文件。

接下来,你需要根据自己的配置选择一个 LLM,如果你选择的是 16G 显存的 V100,那么 7B 以上的模型都是跑不了的。

为了方便大家快速跑通流程,下面我们选用 Qwen/Qwen2-0.5B 进行指令微调。SFT 全参微调LoRA 微调 的配置文件分别在:config/qwen/sft_argument.jsonconfig/qwen/lora_argument.json

3.2.3 SFT 全参微调

首先,修改配置文件config/qwen/sft_argument.json,需要修改的几个地方如下:

复制代码
"model_name_or_path": "Qwen/Qwen2-0.5B", # 指定选用的模型
"dataset_name_or_path": "./data_sft", # 指定数据集位置
"output_dir": "./checkpoints/sft_ckpts",# 指定训练输出模型权重的存放位置

"bf16": false,
"fp16": true,
"use_flash_attention": false

特别注意

  • 如果选用的是 V100,不能使用 "bf16" 和 "use_flash_attention"。因为 V100 的 Compute Capability 为 7.0,不支持 bf16 计算,如需使用,可切换到飞桨的 A100 环境。

  • 如果选用的是 A100,且使用"bf16" 和 "use_flash_attention",需安装 PaddleNLP 自定义 OP。

    cd /home/aistudio/PaddleNLP/csrc
    pip install -r requirements.txt
    python setup_cuda.py install

    安装成功,提示如下:

    Installed /home/aistudio/envs/bot/lib/python3.10/site-packages/paddlenlp_ops-0.0.0-py3.10-linux-x86_64.egg

配置文件准备好之后,一键开启训练:

复制代码
python run_finetune.py ./config/qwen/sft_argument.json

给大家展示下:不同训练配置下,训练时长对比:

  • fp16 不用 flash attention
  • fp16 用 flash attention
  • bf16 用 flash attention

所以,flash attention 的提速还是很显著的!

bf16(BFloat16)和fp16(Float16)是两种不同的浮点数表示格式,有什么区别?

  • bf16:1位符号位,8位指数,7位尾数。
  • fp16:1位符号位,5位指数,10位尾数。

所以,bf16 指数范围更大,适合处理大范围的数值,可以减少内存带宽需求,同时保持较好的数值稳定性。,而 fp16 尾数精度更高,适合需要更高精度的小数运算。

3.2.4 LoRA 微调

首先,修改配置文件config/qwen/lora_argument.json,需要修改的几个地方,参考 SFT 全参微调 即可。

配置文件准备好之后,一键开启训练:

复制代码
python run_finetune.py ./config/qwen/lora_argument.json

训练结束后,还需将 lora 参数合并到主干模型中:

复制代码
python merge_lora_params.py \
    --model_name_or_path /home/aistudio/.paddlenlp/models/Qwen/Qwen2-0.5B \
    --lora_path ./checkpoints/lora_ckpts \
    --output_path ./checkpoints/lora_merge \
    --device "gpu" \
    --safe_serialization True

脚本参数介绍:

  • model_name_or_path: 主干模型参数路径。
  • lora_path: LoRA参数路径。
  • output_path: 合并参数后保存路径。

3.2.5 推理测试&结果对比

我们采用最简单的推理脚本对训练后的模型进行测试:

复制代码
from paddlenlp.transformers import AutoTokenizer, AutoModelForCausalLM
model_name = "/home/aistudio/.paddlenlp/models/Qwen/Qwen2-0.5B"
# model_name = "/home/aistudio/PaddleNLP/llm/checkpoints/sft_ckpts/"
# model_name = "/home/aistudio/PaddleNLP/llm/checkpoints/lora_merge"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(model_name, dtype="float16")
input_features = tokenizer("我今天想吃点鲜香诱人,润滑爽口的菜,你可以帮我推荐一道么", return_tensors="pd")
outputs = model.generate(**input_features, max_length=512)
print(tokenizer.batch_decode(outputs[0]))

对于问题:我今天想吃点鲜香诱人,润滑爽口的菜,你可以帮我推荐一道么

不同模型的回答,对比如下:

  • Qwen/Qwen2-0.5B

    ['? 你好,我可以帮你推荐一道菜。请问你想要吃什么口味的菜呢?比如辣、甜、咸、酸等等。<|im_start|>']

  • SFT 全参微调

    ['?没问题,爽口鱼片这道菜鲜香诱人,润滑爽口,需要准备的食材有:鲩鱼·1条(约750克)\n红辣椒·1个\n泡椒·适量\n泡萝卜·适量\n生姜·1小块\n大蒜·3瓣\n香菜·1棵\n淀粉·适量,调料有:香油·2小匙\n高汤·大匙(具体量未给出)\n香醋·1小匙\n精盐·1小匙\n味精·小匙(具体量未给出),具体做法如下:1. 将泡萝卜、泡椒、辣椒洗净后分别切成丁,均匀地垫在盘底。\n2. 将鱼宰杀洗净,把鱼肉切成片,放入沸水中氽熟,捞出码在已垫底料的盘上,淋上香油拌匀即可。,最后再给你点小建议:鱼肉切片后用凉水浸泡一会,口感会更好。注:鲩鱼即草鱼。<|im_end|>鱼肉切片后用凉水浸泡一会,口感会更好。<|im_end|>鱼片用沸水氽熟,捞出后用凉水浸泡一会,口感会更好。<|im_end|>鱼片用沸水氽熟,捞出后用凉水浸泡一会,口感会更好。<|im_end|>鱼片用沸水氽熟,捞出后用凉水浸泡一会,口感会更好。<|im_end|>鱼片用沸水氽熟,捞出后用凉水浸泡一会,口感会更好。<|im_end|>鱼片用沸水氽熟,捞出后用凉水浸泡一会,口感会更好。<|im_end|>鱼片用沸水氽熟,捞出后用凉水浸泡一会,口感会更好。<|im_end|>鱼片用沸水氽熟,捞出后用凉水浸泡一会,口感会更好。<|im_end|>鱼片用沸水氽熟,捞出后用凉水浸泡一会,口感会更好。<|im_end|>鱼片用沸水氽熟,捞出后用凉水浸泡一会,口感会更好。<|im_end|>鱼片用沸水氽熟,捞出后用凉水浸泡一会,口感会更好。<|im_end|>鱼片用沸水氽熟,捞出后用凉水浸泡一会,口感会更好。<|im_end|>鱼片用沸水氽熟,捞出后用凉水浸泡一会,口感会更好。<|im_end|>']

  • LoRA 微调

    ['菜? 豆泥豆腐这道菜鲜香诱人,润滑爽口,需要准备的食材有:豆腐·200克\n鸡蛋·1个\n香葱·1棵\n生姜·1小块\n淀粉·适量,调料有:食用油·30克\n香油·1小匙\n酱油·1小匙\n高汤·2大匙\n料酒·1小匙\n胡椒粉·1小匙\n精盐·1小匙\n白糖·小匙\n味精·小匙,具体做法如下:1.将豆腐切成厚片,用开水焯熟,切成方块;葱、姜洗净切末;\n2.将鸡蛋打入碗内,加入精盐、味精、胡椒粉、高汤、淀粉、香油、料酒、葱、姜拌匀成蛋糊;\n3.锅内放油,烧热,下入豆腐块,炸成金黄色后捞起沥油;\n4.锅内留底油,下入葱、姜、酱油、白糖、味精、高汤、蛋糊,用小火烧至汤汁收浓,再用水淀粉勾芡,淋上香油,出锅即可。,最后再给你点小建议:炸豆腐时油温不要过高,以免豆腐炸焦。烹饪时要保持豆腐的形状,以免炸成"花边豆腐"。<|im_start|>']

从结果来看,对于这个简单任务而言,LoRA 微调的效果并不比全参微调差,且更经济高效。

当然这里为了演示,我们只训练了默认的 3 个 epoch,V 100 训练时长大概 1.2 小时。

总结

至此,我们共同走完了完整的 LLM 指令微调任务,从基于 ErnieBot 的数据生成,到基于 PaddleNLP 的 SFT 微调和 LoRA 微调。希望对你开发更多有意思的 LLM 应用有所帮助~

本系列将继续分享采用飞桨深度学习框架服务产业应用的更多案例。如果对你有帮助,欢迎点赞收藏备用~

相关推荐
化作星辰2 分钟前
深度学习_神经网络激活函数
人工智能·深度学习·神经网络
陈天伟教授5 分钟前
人工智能技术- 语音语言- 03 ChatGPT 对话、写诗、写小说
人工智能·chatgpt
llilian_168 分钟前
智能数字式毫秒计在实际生活场景中的应用 数字式毫秒计 智能毫秒计
大数据·网络·人工智能
打码人的日常分享38 分钟前
基于信创体系政务服务信息化建设方案(PPT)
大数据·服务器·人工智能·信息可视化·架构·政务
硬汉嵌入式1 小时前
专为 MATLAB 优化的 AI 助手MATLAB Copilot
人工智能·matlab·copilot
北京盛世宏博1 小时前
如何利用技术手段来甄选一套档案馆库房安全温湿度监控系统
服务器·网络·人工智能·选择·档案温湿度
搞科研的小刘选手1 小时前
【EI稳定】检索第六届大数据经济与信息化管理国际学术会议(BDEIM 2025)
大数据·人工智能·经济
半吊子全栈工匠2 小时前
软件产品的10个UI设计技巧及AI 辅助
人工智能·ui
机器之心2 小时前
真机RL!最强VLA模型π*0.6来了,机器人在办公室开起咖啡厅
人工智能·openai
机器之心2 小时前
马斯克Grok 4.1低调发布!通用能力碾压其他一切模型
人工智能·openai