基于大语言模型微调的相机知识助手开发-算法分析

119 阅读6分钟

本文目标是总结和分析实现大语言模型微调的过程。希望能在梳理的知识的同时给其他人提供帮助。原算法过程见这里
先看效果:

image.png

1. 下载模型

初始模型是Langboat/bloom-389m-zh

from transformers import AutoModelForCausalLM, AutoTokenizer, DataCollatorForSeq2Seq, pipeline
from datasets import Dataset

un_trained_model = AutoModelForCausalLM.from_pretrained('Langboat/bloom-389m-zh')

tokenizer = AutoTokenizer.from_pretrained('Langboat/bloom-389m-zh')
un_trained_model.save_pretrained('./untrained_model')
tokenizer.save_pretrained('./tokenizer')

AutoModelForCausalLMForCausalLM指的是因果语言模型,如果目的是文本生成一般就是用这个模型。
AutoTokenizer就是根据模型自动选择相应的分词器。它的底层原理是会到模型的配置文件中去找该模型使用分词器,因为不同模型使用的分词器不一样,用AutoTokenizer以后选分词器就会方便很多。
DataCollatorForSeq2Seq, DataCollator定义了怎样将单个的样本合并为一个batch,Seq2Seq表明该数据整理器专门用来处理序列到序列的任务。这里的序列就是强调顺序,顺序不能随意更改,

运行代码后,就可以将从hugging face官网下载的数据保存到本地。

image.png

2. 处理数据

2.1 正则表达式处理文本文件

数据的获取方式我会在另一篇文章写,这里只专注于数据的处理。 数据本身大概长这样:

image.png 这里采用的是Instruction格式,每条数据都分为Instruction, Input, Output三部分,先用正则表达式把每条数据从文本文件中筛选出来。具体正则表达式用法参见正则表达式实例对结构化数据使用正则表达式进行格式转换,以便进一步提供给大语言模型训练。本文的内容主要是使用正则表达式的一 - 掘金
正则表达式代码如下:

import re
import pandas as pd

with open('./generated_data_text.txt', 'r', encoding = 'utf-8') as f:
    text = f.read()

pattern = r'Instruction:(.*?)\nInput:(.*?)\nOutput:(.*?)(?:\n\n|$)'
matches = re.findall(pattern, text, re.DOTALL)

df = pd.DataFrame(matches, columns = ['Instruction', 'Input', 'Output'])

2.2 DataFrame转Dataset

from datasets import Dataset, DatasetDict
dataset = Dataset.from_pandas(df)

Pytorch无法直接处理DataFrame, 必须先通过Dataset.from_pandas()将DataFrame转换成dataset

2.3 定义process_func函数(最重要)

process_func函数是整个算法过程中最难的一部分。 正是依靠这个函数才能将数据tokenize化,说人话就是将人类看得懂句子转化成机器能看懂的向量,并且将Instruction格式转化成机器能处理的input_ids, attention_mask, labels格式。代码如下:

def process_func(example):
    input_ids, attention_mask, labels = [], [], []
    instruction = tokenizer('\n'.join(['User: ' + example['Instruction'], example['Input']]).strip() + '\n\nAssistant: ')
    response = tokenizer(example['Output'] + tokenizer.eos_token)

    input_ids = instruction['input_ids'] + response['input_ids']
    attenton_mask = instruction['attention_mask'] + response['attention_mask']

    # instruction是tokenized后的数据,直接取len()为2
    labels = [-100] * len(instruction['input_ids']) + response['input_ids']
    
    max_length = 256
    if len(input_ids) > max_length:
        input_ids = input_ids[:max_length]
        attention_mask = attention_mask[:max_length]
        labels = labels[:max_length]
    
    return {
        'input_ids': input_ids, 
        'attention_mask': attenton_mask, 
        'labels': labels
    }

接下来详细解释这段代码的实现逻辑。 example是一条包含Instruction, Input, Output三部分的原始数据,先将其Tokenize化,Instruction和Input放在一起,经过tokenize化以后得到instruction。Output部分tokenize化以后得到response。这么做的目的有两个:一个是用tokenizer这个分词器进行词转向量,另一个是将Instruction和Input放在一起,便于接下来在labels里面将其label设置成特殊的-100,response的label部分和input_ids保持一致。之所以设计为-100,是因为训练时我们只需要预测output部分,instruction部分是不需要预测的,因此instruction部分设置为特殊值-100,告诉机器这部分不参与预测。
labels部分需要这样分开处理,input_ids和attention_mask保持一致。

  • input_ids是词对应过来的向量
  • attention_mask只有两个值:0和1。0表示无用,1表示有用。
  • labels是真正参与训练的部分。instruction和input的labels为-100,output部分的labels和input_ids保持一致。

设置最大长度256的目的是防止单条数据过长。因为pytorch在处理的时候需要确保数据的格式都是一样的,超过256的部分就直接截断。


process_func函数还有几个需要注意的细节:

  1. 最终得到的labels的长度会比直接对example进行tokenize化后得到的长度要长。因为instrution(注意区分大小写,Instruction是原数据的Instruction部分,instruction是Instruction部分和Input部分tokenize化后的数据)手动添加了User和Assistant,还有几个换行符,因此在原长度的基础上增加了长度。
  2. response最后添加了一个eos_token,充当着终止符的角色,就是在告诉机器到这输出就结束了。

2.4 调用process_func函数

tokenized_dataset = dataset.map(process_func, remove_columns = dataset.column_names)

使用map函数,dataset中的单条数据就相当于frocees_func的参数example,处理完后就得到包含input_ids, attention_mask, labels的数据。

3. 定义args和trainer

args用来指定训练过程中的参数,比如批次大小,模型的存储路径等。trainer用来搭建训练器,指定训练数据,模型,分词器等。

from transformers import TrainingArguments, Trainer
args = TrainingArguments(
    logging_steps = 1, 
    per_device_train_batch_size = 4, 
    output_dir = './trained_model', 
    num_train_epochs = 2, 
    gradient_accumulation_steps = 8

)

参数具体设置为多少,和用于训练的数据大小和GPU的型号有关。

trainer = Trainer(
    args = args,
    model = model, 
    data_collator = DataCollatorForSeq2Seq(tokenizer = tokenizer, padding = True),
    train_dataset = tokenized_dataset

)

4. 训练

trainer.train()

image.png

可以看到,虽然训练过程中损失函数的值有波动,但是整体上是越来越小的,说明模型的确是往好的方向训练。

5. 实现前端页面

python提供了一个gradio库,可以快速实现一个前端页面。代码逻辑可以分为三部分。

  1. 先定义一个func函数,用来作为interface的参数。

def func(prompt): return f"训练前效果:{pipe1(prompt, max_length = 300, temperature = 0.9)} \n\n
训练后效果:{pipe2(prompt, max_length = 300, temperature = 0.9)}"

  1. 定义interface页面,设置相关参数

import gradio as gr interface = gr.Interface(fn = func, inputs = 'text', outputs = 'text', title = 'Camera GPT', description = '请问我任何有关相机的问题')

  1. 最后启动页面

interface.launch()

通过以上三步,就可以生成一个简单可用的前端页面,而不用写大量前端代码。 效果如下:

image.png

6. 总结

整个代码逻辑最难的就是process_func函数如何实现,其余部分都是有一定熟悉程度就可以写出来。人工智能;由算法,算力,数据三部分组成,本文主要写的是一个算法实例,算力推荐使用AutoDL,可以根据用量购买GPU的使用权,性价比高。数据会在下一篇文章写,主要是使用deepseek的api,指定主题,根据主题生成数据。
欢迎讨论指正。