Intern模型调研和Xtuner实战(下)

Intern模型调研和Xtuner实战(上)

细节3:如何从dataset格式转变到Dataloder?

上面已经获得了huggingface格式的dataset,原则来说,使用 Trainer 时,你可以将 datasets 库中的 Dataset 对象直接作为数据集参数传递给 Trainer。Trainer 类在内部会自动处理数据加载和迭代的细节,包括将 Dataset 转换为 DataLoader。不必显式创建一个 DataLoader,因为Trainer 会根据 Trainer 的参数和 Dataset 的特性来自动创建一个合适的 DataLoader。

但是在本示例中,还是选择手动定义了一个DataLoader。定义DataLoader的代码仍然在/opt/conda/envs/transformers2/lib/python3.9/site-packages/mmengine/runner/runner.py中

d6103c9e6698f6526b81220198f1b254.png

这段代码创建了一个 DataLoader 对象,它是PyTorch中使用的一个非常重要的类,用于高效地加载数据,可以在多线程环境下工作,常见于机器学习和深度学习模型的训练过程中。

下面是代码的参数解释:

  1. dataset: 这是要加载的数据集。在PyTorch中,dataset 通常是一个实现了 __getitem__ 和 __len__ 方法的对象。

  2. sampler: 这个参数定义了从数据集中抽取样本的策略。它能够控制数据加载的顺序等。如果 batch_sampler 被定义,则 sampler 应该为 None,因为 batch_sampler 与 sampler 是互斥的,batch_sampler 直接定义了批量数据的取样方式。

  3. batch_sampler: 这是一个迭代器,用于指定每个批次中哪些索引的数据将被加载。一旦设置了 batch_sampler,则将忽略 sampler 参数。

  4. collate_fn: 当需要将多个数据样本组合成一个批次时,collate_fn 被用来定义如何将这些样本合并。通常,它接收一个样本列表并返回一个批次。如果没有提供 collate_fn,DataLoader 将使用默认的方式将数据组合成批次。

  5. worker_init_fn: 这是一个函数,用于初始化每个工作进程。这可以用于设置在使用多进程加载数据时每个工作进程的随机种子,以确保数据加载顺序的随机化。

  6. **dataloader_cfg: 这个语法表示以字典的形式传递可变数量的关键字参数。dataloader_cfg 是一个字典,包括了 DataLoader 的其他配置选项,如 batch_size(每批次加载的样本数量)、shuffle(是否随机打乱数据)、num_workers(加载数据时使用的进程数量)等。

collate_fn函数的源码位于/opt/conda/envs/transformers2/lib/python3.9/site-packages/xtuner/dataset/collate_fns/defalut_collate_fn.py里

经过DataLoader后,数据的格式长这样:

{'data':

{'input_ids': tensor([[  265,   539,  2632,  ..., 36333,   328,   454]]),

'attention_mask': tensor([[True, True, True,  ..., True, True, True]]),

'labels': tensor([[  265,   539,  2632,  ..., 36333,   328,   454]])},

'data_samples': None}

input_ids/attention_mask/labels的shape都是torch.Size([1, 2048])

这里的attention_mask全部是True,因为pack_to_max_length这个步骤,将大部分数据都强行拼凑成2048长度。

训练流程——第四步:将PTH模型转huggingface模型

训练好的文件保存在work_dirs路径下。首先查看work_dirs目录的路径结构:

468d14ba12c061ac06c1c9987bba1253.png

可以看到每一个epoch都会生成一个pth文件。但是我在实际跑了后发现,我这边生成的pth其实是一个文件夹:

8ce95f6b83ead369011c648555f7bd0c.png

目前得到的是pth文件(夹),还不能直接用于推理,需要转成huggingface的模型。转换的命令是:

1eaaf527e135a7ea0c398794c91941d3.png

转换后,hf文件夹里出现了四个文件:

6f7b59a43fff66ccca3194b9f6578117.png

可以这样简单理解:LoRA 模型文件 = Adapter

细节4:转换模型的流程

接下来探究一个细节:模型是如何转换的?接下来debug一下背后的流程。

首先根据命令行的参数,修改launch.json

33dd2643d62d12a99b1680672af63170.png

经过断点测试,可以发现在/opt/conda/envs/transformers2/lib/python3.9/site-packages/xtuner/entry_point.py中的subprocess.run(['python', fn()] + args[n_arg + 1:])

这一行代码中,发现它实际调用了'/opt/conda/envs/transformers2/lib/python3.9/site-packages/xtuner/tools/model_converters/pth_to_hf.py'

于是去pth_to_hf.py函数里打断点,发现以下几个关键步骤。

【关键步骤1】加载模型

f0e6b4d6842affc7d127d7fb11388975.png

以"load_in_4bit": true来加载模型,加载的模型是原模型,即Shanghai_AI_Laboratory/internlm-chat-7b

按道理来说7b模型,如果以4bit来加载,显存占用应该是只有3.5G左右。但是实际占用的显存达到9G。可能有如下几个原因:(1)只有对weight进行了4bit量化,bias仍然是float32格式。(2)lora部分未经过量化,是以float32格式呈现的。

通过以下命令,查询每一层的dtype:

fecdc0cf44238ca2dc83f585cb381e9c.png

截取其中部分结果:

6abfc40f8d3951ba6873a7ec4da42e73.png

那么为什么要加载原模型呢?根据命令的参数,发现它只用到了workdirs里面的pth文件。

【关键步骤2】加载pth文件

state_dict = guess_load_checkpoint(args.pth_model)

state_dict是一个OrderedDict对象,里面包含了所有lora的参数(层的名称+具体的参数值)。也就是说,之前微调保存的pth文件,其实保存的是所有的lora参数。这里发现这些参数都是float32的格式。经过简单的统计,发现一共有159907840参数,占总参数量的2%左右。这个占比还是挺高的,我看到q/k/v/o/h_to_4h/4h_to_h里都插入了lora矩阵(target_modules: ['k_proj', 'o_proj', 'q_proj', 'v_proj', 'up_proj', 'down_proj', 'gate_proj'])。

【关键步骤3】将pth文件中的参数覆盖到model里

a0370cdac15e270930493a67c1dcd3f0.png

【关键步骤4】转成float16的格式

78ccec6234bc2edfd97464de69b81f11.png

截取其中部分结果:

30eae2190b52a0922d9f8fbd902352a9.png

注意,这里只是把之前为fp32格式的参数转成了fp16,之前是4bit格式的参数,并未转成fp16

【关键步骤5】保存模型

0ec63325405731874f674fab25ef04a7.png

注意,这里的save_pretrained方法,是peft的save_pretrained方法。在这个方法中,有这么一步:

fcd8d5865c5b25c0ee1bc0a1451c0ba8.png

这行代码是用来获得整个model中的lora参数的state_dict

然后在下面这行代码中保存了lora的参数,保存的文件名称是adapter_model.bin

6413c044c619b992d0385b557f2823ea.png

总结一下,首先加载整个模型,然后加载之前保存的包含lora的pth文件,将参数覆盖到model模型里。接着将模型转成fp16的格式。最后,将model的lora权重抽出来,进行保存。

我感觉这是不是绕了一个圈呢?可不可以不加载model模型呢?在上面的流程中,lora参数先是被覆盖到model里,后来又从model中抽出lora参数,是不是多此一举呢?

不过我还是发现了一些区别,比如pth文件保存的格式是float32,这里保存的adapter是float16格式,文件大小是319,977,674,大约是lora参数量159,907,840的两倍。

也就是说,在这个convert步骤里,通过pytorch层面的load_state_dict,fp32格式存储的pth文件被转成了fp16格式存储的adapter_model.bin文件。不论是pth文件还是bin文件,都是通过torch.save/torch.load来直接保存和加载的,还没有涉及到PEFT库的内容。

训练流程——第五步:模型合并

经过了pth转hf,目前只生成了adapter,还没有合并模型,即没有将lora旁路合并到主模型上。下面的代码将会将adapter合并到模型中:

ac0c579b6179f80920cae59eebb1d924.png

第一个参数是basemodel所在的路径

第二个参数是转换成hf格式后所在的路径

第三个参数是转换后的路径

第四个参数是每个文件最大的大小

转换成功后,看到mergerd文件夹下有如下文件:

9fc74d190464f1e1ce0711fec80c8881.png

8个bin文件加起来不到16G?为什么7B的模型,占用的存储不是28G,而是16G?难道是以float16存储的?是的,在model.config里可以看到模型参数是float16的格式。

细节5:合并模型的流程

首先根据命令行的参数,修改launch.json

d3c5def81a84cc694acba599ed0cd275.png

可以看到调用了 '/opt/conda/envs/transformers2/lib/python3.9/site-packages/xtuner/tools/model_converters/merge.py'

于是去merge.py里打断点,发现以下几个关键步骤。

【关键步骤1】加载模型

42fb131aaa6db091ce8dd6ff7c52d95c.png

以float16来加载原模型(因为adapter是以fp16来存储的),加载后,GPU显存大约是14.3G,差不多是参数量的两倍。

截取其中部分结果:

09b267ecffae886759fe1075ba09b34b.png

可以看到,此时的模型是没有lora参数的,而且模型的参数是float16的格式。

当然同时也加载了分词器:

e32f22e1f0309bb846dfbc317ad803c6.png

这里加载分词器倒不是说合并模型的时候需要用到分词器,而是把分词器一起保存到合并后的模型的保存路径,方便后面一起调用。

【关键步骤2】加载adapter(本步骤涉及对PEFT源码的探索)

5e53e993c561e9e17675a39e3c87d10a.png

使用PeftModel.from_pretrained来加载adapter。注意到有一个参数是model(以fp16格式加载的还没有lora的model),也就是说,加载lora参数的时候,不能脱离model。

点进去这个源码(位于/opt/conda/envs/transformers2/lib/python3.9/site-packages/peft/peft_model.py,这是PEFT库的代码),发现有两行代码比较关键:

5e81343f62c678b7f5e07315f6c9b245.png

【关键步骤2.1】首先解释第一行代码:model = MODEL_TYPE_TO_PEFT_MODEL_MAPPING[config.task_type](

model, config, adapter_name)

其中MODEL_TYPE_TO_PEFT_MODEL_MAPPING[config.task_type]指向了这个类,这个类的源码也是在peft_model.py里。

传入的参数除了model,还有两个,其中config的内容是:

LoraConfig(peft_type='LORA', auto_mapping=None, base_model_name_or_path='.../Intern-models-dirs/Shanghai_AI_Laboratory/internlm-chat-7b', revision=None, task_type='CAUSAL_LM', inference_mode=True, r=64, target_modules=['k_proj', 'o_proj', 'q_proj', 'v_proj', 'up_proj', 'down_proj', 'gate_proj'], lora_alpha=16, lora_dropout=0.1, fan_in_fan_out=False, bias='none', modules_to_save=None, init_lora_weights=True, layers_to_transform=None, layers_pattern=None)

这里的config准确识别到了在微调阶段时用的lora参数是什么,我暂时没有弄明白它是在哪个环节识别到的。

最后一个参数adapter_name的内容是"default"

再回顾下,传入的三个参数分别是model/config/adapter_name。

这行代码是什么意思呢?就是定义了一个PeftModelForCausalLM类的实例,这个实例的名称是model。产生一个PeftModelForCausalLM类的实例,自然要经过PeftModelForCausalLM的__init__方法。由于PeftModelForCausalLM继承于PeftModel类,因此需要经过PeftModel类的__init__方法。在__init__方法中,有一行代码是:

6d703fcc5e3ca88013d8f83413344e2b.png

其中PEFT_TYPE_TO_MODEL_MAPPING[peft_config.peft_type]指向了这个类,这一行的代码的意思是产生了一个LoraModel类的实例,这个实例的名称叫做self.base_model,这里的self.base_model在原模型model的基础上,新增并初始化了lora旁路。

self.base_model的部分层如下所示:

4461cca13031c68d68faf5fc61020a9f.png

进一步打印参数的话(如使用self.model.model.layers[31].mlp.down_proj.lora_B.default.weight命令),可以看到lora_B的所有参数都是0

总结一下:PeftModel类的初始化方法的作用,是给原模型添加adapter,如果peft的类型是lora,那么添加的adapter是lora旁路。添加的旁路的参数是根据一定规则初始化的。

给原模型添加adapter的过程还是比较耗时的,里面的_find_and_replace方法比较复杂,我暂时没有看懂。

还有一个需要注意的点是,此时模型的总参数量是7481856000,比初始的参数量7321948160多159907840,这正是lora部分的参数量。

【关键步骤2.2】然后解释第二行代码:model.load_adapter(model_id, adapter_name, is_trainable=is_trainable, **kwargs)

在经过了关键步骤2.1的第一行代码后,模型里面有lora旁路和参数了,截取模型部分层如下图所示:

2dc8d35b409e05ec486212be80fc14d9.png

此时的lora参数的数据类型是float32。

关键步骤2.2做的是加载adapter[通过torch.load('./hf/adapter_model.bin')命令],把里面的参数覆盖到model里(通过model.load_state_dict命令)。

总结一下,整个关键步骤2,首先将原模型添加了lora旁路,此时lora旁路的参数是随机或者全0初始化的。然后加载adapter,将lora参数覆盖到model里。

截取模型部分层如下图所示:

c95cead920f541c0b9e1d978e0552b03.png

模型的总参数量是7481856000

【关键步骤3】合并adapter和model(本步骤涉及对PEFT源码的探索)

99834a9ef64cfa39f7bb4bc0aca6655c.png

这个函数的源码位于/opt/conda/envs/transformers2/lib/python3.9/site-packages/peft/tuners/lora.py

从源码的位置可以看出合并部分的代码并不是xtuner写的,而是PEFT库官方提供的。

这个步骤主要是将lora的参数merge到主路里。经过了关键步骤2,可以看到lora的参数仍然是以旁路而存在的。经过了这个步骤,发现模型的总参数量回到了7321948160,而且所有参数的类型都是float16。

那具体是怎么合入的呢?lora的原理是,在主路旁边加一个旁路,即y=Wx+(BA)x,其中W和BA是同shape的,是可以加在一起的。

首先查看self.model.named_modules(),以下是截取的部分信息:

7252c982e484a88d12b9437dc12a400c.png

然后依次对每一层来执行merge操作。具体来说,举个例子,当前的层是'model.layers.0.self_attn.q_proj',这表示第1层的q矩阵,结构如下:

a8378059a07e74ee28957024697d48f4.png

我们的目标是将所有的lora参数(A矩阵:64*4096,B矩阵:4096*64)都merge到4096*4096的主路上。

56cef4498e63d893f9638aa8cc451609.png

对应到代码里的操作是:

self.weight.data -= (self.lora_B[adapter].weight @ self.lora_A[adapter].weight)* self.scaling[adapter]

self.weight.data是主路的权重,shape是4096*4096,这里用的是减号,按照lora的论文,应该是加号。但是加号减号没有本质区别。self.scaling[adapter]代表的是lora旁路的权重因子,这里是0.25。

self.lora_B[adapter].weight代表是B矩阵的权重,shape=4096*64,self.lora_A[adapter].weight)代表是A矩阵的权重,shape=64*4096。

注意,输入x的shape是batchsize*4096,其中4096是hiddensize。首先x这个batch中的每条数据和A矩阵(shape=64*4096)相乘,得到shape=batchsize*64的中间激活层y。这个过程也等价于x@A',A'是A的转置。

然后同理,y@B',得到shape=batchsize*4096的输出。

两个过程可以整合在一起:x@A'@B'=x(BA)'。

注意:在pytorch中,输入x经过一个(in=64,out=4096)的线性层,这个线性层的参数矩阵W其实是以4096*64的shape来存储,然后在实际执行线性层运算的时候,是x@W’,其中W'是W的转置。在PyTorch中,线性层(通常表示为nn.Linear)的权重张量的存储格式是以[out_features, in_features]为形状来存储的。当执行线性变换时,实际上进行的操作是输入矩阵x(形状为[batch_size, in_features])和权重矩阵W的转置的矩阵乘法。

经过合并后,'model.layers.0.self_attn.q_proj'的结构如下所示:

33dd63290c6ab4fbb4bd61175c3c3e45.png

【关键步骤4】保存合并后的模型和分词器

bda617b18d477cf4b62e7d11186e4e21.png

训练流程——第六步:用合并后的模型来推理

经过训练后,保存的merge模型就是一个标准的huggingface的模型。

直接从merge路径加载即可:

3ae3463bbad13a76cac5c861e23be646.png

当然也可以使用xtuner的chat来推理模型:

fc557f3d2fbce0179970a51777885fa9.png

prompt-template 指的是:基于哪个底座模型调的,就需要写对应的模型。因为不同的模型,chat的模板是不一样的。

可选集合:  --prompt-template {default,zephyr,internlm_chat,internlm2_chat,moss_sft,llama2_chat,code_llama_chat,chatglm2,chatglm3,qwen_chat,baichuan_chat,baichuan2_chat,wizardlm,wizardcoder,vicuna,deepseek_coder,deepseekcoder,deepseek_moe,mistral,mixtral}

这里也能看出internlm_chat和internlm2_chat分别有一套自己的模板。

在调用xtuner的chat来推理的时候,注意要敲两次回车键来完成输入。

上面的命令默认是以float16来加载模型的

也可以以4bit来加载模型:

600593ca2494468a132c974f3550f2b4.png

细节6:推理的流程

首先根据命令行的参数,修改launch.json

9590058aac2c6d1064bcecda40a1f4a1.png

可以看到调用了'/opt/conda/envs/transformers2/lib/python3.9/site-packages/xtuner/tools/chat.py',于是进去chat.py里打断点。

首先加载了一些参数:

Namespace(model_name_or_path='./merged', adapter=None, llava=None, visual_encoder=None, visual_select_layer=-2, image=None, torch_dtype='fp16', prompt_template='internlm_chat', system=None, system_template=None, bits=None, bot_name='BOT', with_plugins=None, no_streamer=False, lagent=False, stop_words=[], offload_folder=None, max_new_tokens=2048, temperature=0.1, top_k=40, top_p=0.75, seed=0)

可以看到temperature=0.1,输出的结果比较稳定,倾向于往概率大的token预测。

system=None表示的是没有注入system prompt。

接下来分解几个关键步骤。

【关键步骤1】加载模型和分词器(位于merged路径),默认以fp16格式加载模型

7dc851c098c61d9a05b4bbdf03d737ca.png

【关键步骤2】接受输入数据

比如text='爱情让人着迷吗?'

【关键步骤3】加载对话模板,并且将内容套进模板里

在本例子中的对话模板是:

{'SYSTEM': '<|System|>:{system}\n', 'INSTRUCTION': '<|User|>:{input}\n<|Bot|>:', 'SUFFIX': '', 'SUFFIX_AS_EOS': True, 'SEP': '\n', 'STOP_WORDS': ['']}

那么inputs就变成了:'<|User|>:爱情让人着迷吗?\n<|Bot|>:'

【关键步骤4】对inputs分词

846e55fdd5d5101a8d0df2b9fce710c6.png

ids:

e462b0530301b4062646fe070e653d43.png

【关键步骤5】生成回答

558e828aa71e9f7d8a9ebb673382883e.png

generate_output是什么?打印tokenizer.decode(generate_output[0])出来看看:

' <|User|>:爱情让人着迷吗?\n<|Bot|>:爱情是一种非常复杂的情感,它可以让人们感到兴奋、幸福、满足和快乐,也可以让人感到痛苦、失落、沮丧和孤独。爱情可以让人着迷,因为它可以带来强烈的情感体验和满足感,但同时也会带来一些负面影响,如焦虑、压力、嫉妒和控制欲等。因此,爱情是一种非常个人化的体验,每个人对它的感受和反应都可能不同。

可以看到它包括了之前的提问。

如果len(generate_output[0])超出了设定的最大长度(比如这里是2048),那么对话历史会被清空,即对话被强制重来。因为模型最多支持2048个token长度的输入。

【关键步骤6】第二轮对话

开启第二轮对话的流程,只是在原来的output的基础上再添加新的内容,然后再跑一遍上面的流程。

比如我追问了一个问题:获得爱情,最重要的特质是什么?

此时的inputs:

' <|User|>:爱情让人着迷吗?\n<|Bot|>:爱情是一种非常复杂的情感,它可以让人们感到兴奋、幸福、满足和快乐,也可以让人感到痛苦、失落、沮丧和孤独。爱情可以让人着迷,因为它可以带来强烈的情感体验和满足感,但同时也会带来一些负面影响,如焦虑、压力、嫉妒和控制欲等。因此,爱情是一种非常个人化的体验,每个人对它的感受和反应都可能不同。\n<|User|>:获得爱情,最重要的特质是什么?\n<|Bot|>:'

注意,在第一轮对话的结束,除了,还有\n符号。\n符号的作用我暂时也没有弄明白,它出现在和的后面。

【其他细节】

如果是以4bit加载模型的话,需要在launch.json中增加一个参数:--bits 4

在源码中,以这样的配置去加载4bit模型:

c57181f509b3d26a592cb54fc5f3ca65.png

用4bit去加载模型的目的,应该是为了加快推理速度,减少显存占用(但是4bit是没有办法直接运算的,想要计算,仍然得先反量化到fp16,那么4bit加载有什么用呢?而且反量化的过程应该也会耗费时间)。

加载好模型后,发现显存占用是5.8G。打印model的所有层的dtype发现,weight 都是uint8格式(应该是两个nf4格式的数凑成了一个uint8格式的数),bias/layernorm/embedding都是float16格式。至于为什么weights和bias的格式不一致,我暂时也不知道。但是此时肯定是没有主模型和lora旁路之分了,因为加载的是已经merge好的模型。

在实际跑模型推理的时候,发现显存占用并不高,大约是6G多。

细节7:关于model.generate

在/opt/conda/envs/transformers2/lib/python3.9/site-packages/xtuner/tools/chat.py的源码中,调用了generate方法来产生回答,传入的参数是:

067286196ae73131edc57c35263fd923.png

transformers库中,在generate方法中,如果传入的参数 do_sample=True,那么生成模式是GenerationMode.SAMPLE,此时会用到top_k/temperature等参数。

当使用 Beam Search 时,通过 num_beams 参数设置 Beam 的大小(宽度),你可以让模型在每一步考虑多个最有可能的序列。相对应地,当你使用 Sampling 时,可以通过 do_sample=True 来为生成过程添加随机性。同时,Beam Search 和 Sampling 是两种不同的生成策略,通常不会被组合在一起使用,因为 Beam Search 本质上是一种确定性的方法,而 Sampling 引入了随机性。

【关键步骤1】前向传播

在generate方法的源码中,首先把输入通过前向传播,得到output:

bc7c199704128ecf5c7aabe43fdc7a67.png

其中model_inputs是:

{'input_ids': tensor([[     1,    333,    352,   1621,    352,  27232,  76379, 103027,    364,

            333,    352,  23845,    352,  27232]], device='cuda:0'),

'position_ids': tensor([[ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13]], device='cuda:0'),

'past_key_values': None,

'use_cache': True,

'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]], device='cuda:0')}

f3b9bc53750ca6c060c20790b23d9327.png

' <|User|>:你好\n<|Bot|>:'

此时得到的outputs包括两部分内容:odict_keys(['logits', 'past_key_values'])

past_key_values先不去管,现在只看logits

outputs['logits'].shape=torch.Size([1, 14, 103168])

这里的14是len(model_inputs['input_ids'][0])

【关键步骤2】预测概率最大的值

6e21b0b782f4f3769ec3bf3f7eb9d81d.png

然后根据一定的规则,取出next_tokens,此时是词汇表第76379个token,经过decode,是你好”

【关键步骤3】和input_ids拼接

c070850ff053ef7411997a2dff87ce8f.png

此时的input_ids经过解码后变成:' <|User|>:你好\n<|Bot|>:你好'

【关键步骤4】把拼接后的input_ids再次输入到模型中,直到输出eos符号,或者超过设定的最大值。

最后,模型输出的整个序列的input_ids是:

tokenizer.decode([     1,    333,    352,   1621,    352,  27232,  76379, 103027,    364,

            333,    352,  23845,    352,  27232,  76379,  98899,  67883,  60614,

          60381,  98666,  62412,  60735,  98899,  68408,  73159,  67566,  67513,

          61056,  99050, 103028])

' <|User|>:你好\n<|Bot|>:你好,我是书生·浦语,有什么我可以帮助你的吗?'

这个就是上面所说的generate_output

附录2:常见模型的对话模板

以下内容来源于xtuner/utils/templates.py

细节8:关于loss

这是模型中计算loss的代码:

cbf43a3a3345fa6fc7cb2eeea5bead7f.png

outputs[0].shape=torch.Size([1, 2009, 4096])

input_ids.shape=torch.Size([1, 2009])

logits.shape=torch.Size([1, 2009, 103168])

labels.shape=torch.Size([1, 2009])

shift_logits.shape=torch.Size([1, 2008, 103168])

shift_labels.shape=torch.Size([1, 2008])

ee9ace11dfe50d64d0f01c3655d7ca22.png

以一个实际例子为例:

input_ids=[     1,    333,    352,   1621,    352,  27232,  60817,  67855,  67536,

          71868,  67738,  60353,  71253, 103027,    364,    333,    352,  23845,

            352,  27232]

tokenizer.decode(input_ids):

' <|User|>:请给我介绍五个上海的景点\n<|Bot|>:'

XTuner实战——自定义数据集微调

上面使用的数据集是timdettmers/openassistant-guanaco数据集,针对这个数据集,官方有提供配置文件。那对于其他官方没有提供配置文件的数据集,应该怎么处理呢?

数据集准备和处理

这里使用Medication_QA_MedInfo2019数据集,链接:

https://github.com/abachaa/Medication_QA_MedInfo2019

一共有674条医疗问答数据

4080bbd614f04c4d69cb6c5788af0ea2.png

数据集的格式是表格,表格的格式是:

611857bf0c721713ed96f715b9d6f661.png

我们要转成的目标格式(.jsonL)是:

c4f063386a06faa672126c8f4e00750e.png

这种格式可以被xtuner识别出来。

这里的配置是:"system": "You are a professional, highly experienced doctor professor. You always provide accurate, comprehensive, and detailed answers based on the patients' questions."

至于如何转,可以让chatgpt来帮你写python脚本。这里不细讲。

准备配置文件

我们仍然使用上面说到的默认配置,但是肯定要经过一定的修改。比如,我们还是把上面用果的配置拿过来:

1258f183845051ed61f413ab6dc57910.png

然后做一些修改:

1566e6a2afdb7e2b3c34863d92a60567.png

可以看到,核心的两个修改其实是dataset和dataset_map_fn部分。待会会细讲。

把文件重命名为internlm_chat_7b_qlora_medqa2019_e3.py

训练

训练的命令是:

3df3ab09c828d0e657dedee98ae9075b.png

几分钟就可以训练完,因为数据集比较少。

我对数据集做了一些添加,我添加了十条一模一样的数据:

Q:What is the main use of the medication Midterm Apple for treatment?

A:Relieve eye fatigue

其中Midterm Apple是我自己瞎想的一个名字,现实生活中并没有叫做Midterm Apple的药物。之所以这样做,是因为想要测试模型到底有没有学到这条数据。

之所以加入十条数据,而不是一条,是因为我之前做过几次实验,发现只有一条数据的时候,模型很难学到。

另外,我的训练参数是30轮,如果轮数太少,模型也学不到。

后面部署的内容和之前是一样的,不赘述了。

将pth转hf(得到adapter):

58a8ed75c19fd81466f81ea7dba727e5.png

模型合并:

2ab60378a62df14e8e1d91e8ede8425d.png

对话推理:

6d53cba71481e31b6174a047cb7dc123.png

学习效果:

这个log显示在模型训练过程中。每隔500个iter,模型会推理一遍这个问题。

366b4a2842ac46d283e8b33d6886db01.png

我用第一轮的结果去推理,发现幻觉很明显:

93d3aae7cb67a9035a7b08ddebe6b609.png

用第30轮的结果去推理,可以看到有些过拟合,但是至少说明学到了东西:

cbd0489a7f6bdfb8ffb0758baa9945b7.png

4edf07fade11a3ddb5131f72ab05170b.png

4024c1a49d4beacb12fc380bf54b722f.png

处理数据的细节

由于在本示例中,使用的是没有内置配置的数据集,因此需要进入源码探究下背后是怎么处理数据的。

首先,修改launch.json

e0ebc7c17d70ef9d6f580a66936a0514.png

【关键步骤1】首先,下面这一行代码负责加载数据:

fd18c952867a86cb7253a46e2d01225f.png

加载出来的dataset如下:

213f1391b78a40f6894c8ef2c4d5f409.png

等价于

68155c2e022029525712524bcd1c48bf.png

这是因为在配置文件中,已经对数据集的加载有定义:path='json', data_files=dict(train=data_path)

在datasets库中,load_dataset函数是用来加载和处理数据集的功能函数。当你指定path参数为'json'时,你告诉load_dataset函数你希望从JSON文件中加载数据集。

例如,如下使用将加载一个或多个JSON文件作为数据集:

680bfc3c22fd6d4199d19e39c2f68ce3.png

【关键步骤2】不需要dataset_map_fn映射。

在上一个openassistant数据的示例中讲到,dataset_map_fn的作用是:把初始数据文件中的{"text": "### Human:...### Assistant:..."}改成{"text": "### Human:...### Assistant:...","conversation":[{'input': '...', 'output': '...'}]}的格式。

openassistant_best_replies_train.jsonl数据本身的格式是{"text": "### Human:...### Assistant:..."}

但是在本示例中,dataset已经是符合要求的数据,因此这一步就略过了。

这是dataset中第0条数据:

{'conversation': [{'input': 'where does fluorouracil come from', 'output': 'dailymed list of manufacturers', 'system': "You are a professional, highly experienced doctor professor. You always provide accurate, comprehensive, and detailed answers based on the patients' questions."}]}

注意在配置文件internlm_chat_7b_qlora_medqa2019_e3.py里,dataset_map_fn=None

【关键步骤3】template_map_fn映射

由于此次有system语句,因此system语句被合入在了input中

c615420b4f95295edf03faaaab83ca92.png

【关键步骤4】encode_fn映射(分词、编码)

经过分词后,格式为

{'input_ids': [1, 333, 352,  ...],

'labels': [-100, -100, ...]}

抽一个数据拿出来看看:

1b75acf26da99b7dedb7c286b4921ae1.png

" <|System|>:You are a professional, highly experienced doctor professor. You always provide accurate, comprehensive, and detailed answers based on the patients' questions.\n<|User|>:where does fluorouracil come from\n<|Bot|>:dailymed list of manufacturers\n"

d46bbcd32d7566dffba82faa499b2e57.png

'dailymed list of manufacturers'

其余步骤和之前的类似,就不赘述了。

总之最重要的一点:自己定义的数据集,需要满足{"conversation":[{'input': '...', 'output': '...','system':'...'}]}的格式。system的语句是可选的。如果是多轮对话,则列表有多个元素。

XTuner实战——让模型改变自身定位

我希望让模型意识到自己并不是“书生浦语”人工智能助手,而是巴啦啦小魔仙,应该怎么做?

生成数据

首先,我生成了一条数据。

cdf1bdfb09f6e53119f7c1cec0a142e7.png

但是一条数据不够,我让GPT帮我生成了更多数据。

这是我给GPT的prompt:“请把以下这条数据扩充十倍,要求意思相近,你可以根据你对巴啦啦小魔仙的认识适当修改扩充,不能简单地重复”

a9745802c49166c6ad6b0e143969e92d.png

GPT给我返回的数据:

这里是将提供的数据扩充十倍的结果,尽量保持意思相近并且合理扩展巴啦啦小魔仙的角色特性:

4ea8b6e9692ab7f6cd35f16d0ac30a52.png

28dd406d2a40d42c319f23848cc07862.png

5b0794bf6f710ea299a817cb3e159805.png

在这个扩充中,我尽力使每条对话都围绕巴啦啦小魔仙的基本特征 —— 她是个充满生气和魔力的小魔女,同时维持了转述这些信息的独特性和新颖性。

修改配置文件

然后修改配置文件。修改文件之前先把配置文件copy过来。

c9c3a6432bdb019574a76bfb32f3c4eb.png

以下是修改的部分:

c82781fe095487ca9d4986f19da1dedc.png

后面的训练流程就不赘述了。

效果

这是模型开始训练的时候的情况(注意,第二个问题和第三个问题没有在训练集出现过):

e85dd65b7d43eb03f979b3b60143ce2c.png

第三轮:

29f5bd1a133ec6ecb732f89cab7bfbeb.png

第五轮:

31257e29e6a692d1a854d6c53b306abc.png

646429c31e5565336dbeda80f21c84e0.png

第九轮:

82f1a75688295ae88788674309396b3f.png

第十轮:

409d8d67e3014e1141540ad6b015da1b.png

可以看到,模型已经学会了我喂给它的数据。但是英文的问题始终没有回答出现正确答案。如果模型在之前已经了解到足够的中英翻译的知识,然后我现在喂给模型关于巴啦啦小魔仙的中文数据量足够大,会不会在英文回答中出现我们想要的答案呢?

还有一个比较有趣的现象是,第一个问题原文出现在训练集,第二个问题没有出现在训练集,但是第二个问题比第一个问题更早训练出结果。

Android分区挂载原理介绍(上)

Android分区挂载原理介绍(下)

IO调度器详解

b28f6fc73feb8948f1eb9e29e2fd2aad.gif

长按关注内核工匠微信

Linux内核黑科技| 技术文章| 精选教程

相关推荐

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-03-30 10:42:06       98 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-03-30 10:42:06       106 阅读
  3. 在Django里面运行非项目文件

    2024-03-30 10:42:06       87 阅读
  4. Python语言-面向对象

    2024-03-30 10:42:06       96 阅读

热门阅读

  1. 面试知识汇总——垃圾回收器(G1垃圾回收器)

    2024-03-30 10:42:06       36 阅读
  2. springMVC

    springMVC

    2024-03-30 10:42:06      40 阅读
  3. Redission快速入门

    2024-03-30 10:42:06       41 阅读
  4. 001-从零学习设计模式-设计原则

    2024-03-30 10:42:06       38 阅读
  5. Redis的数据类型List使用场景实战

    2024-03-30 10:42:06       37 阅读
  6. 如何使用 RabbitMQ 进行消息的发送和接收

    2024-03-30 10:42:06       46 阅读
  7. 查验身份证

    2024-03-30 10:42:06       41 阅读
  8. 如何使用 WebView 将 React Native 嵌入到原生应用中

    2024-03-30 10:42:06       40 阅读
  9. HTTPS

    HTTPS

    2024-03-30 10:42:06      44 阅读
  10. 当构造器参数较多时考虑用生成器

    2024-03-30 10:42:06       40 阅读
  11. 复习拓扑排序

    2024-03-30 10:42:06       44 阅读
  12. Redis 过期删除策略

    2024-03-30 10:42:06       37 阅读