0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

适用于各种NLP任务的开源LLM的finetune教程~

深度学习自然语言处理 来源:算法美食屋 2023-07-24 09:04 次阅读

ChatGLM2-6b是清华开源的小尺寸LLM,只需要一块普通的显卡(32G较稳妥)即可推理和微调,是目前社区非常活跃的一个开源LLM。

本范例使用非常简单的,外卖评论数据集来实施微调,让ChatGLM2-6b来对一段外卖评论区分是好评还是差评。

可以发现,经过微调后的模型,相比直接 3-shot-prompt 可以取得明显更好的效果。

值得注意的是,尽管我们以文本分类任务为例,实际上,任何NLP任务,例如,命名实体识别,翻译,聊天对话等等,都可以通过加上合适的上下文,转换成一个对话问题,并针对我们的使用场景,设计出合适的数据集来微调开源LLM.

〇,预训练模型

国内可能速度会比较慢,总共有14多个G,网速不太好的话,大概可能需要一两个小时。

如果网络不稳定,也可以手动从这个页面一个一个下载全部文件然后放置到 一个文件夹中例如 'chatglm2-6b' 以便读取。

fromtransformersimportAutoModel,AutoTokenizer
model_name="chatglm2-6b"#或者远程“THUDM/chatglm2-6b”
tokenizer=AutoTokenizer.from_pretrained(
model_name,trust_remote_code=True)
model=AutoModel.from_pretrained(model_name,trust_remote_code=True).half().cuda()

Loading checkpoint shards:   0%|          | 0/7 [00:00
prompt="""文本分类任务:将一段用户给外卖服务的评论进行分类,分成好评或者差评。

下面是一些范例:

味道真不错->好评
太辣了,吃不下都->差评

请对下述评论进行分类。返回'好评'或者'差评',无需其它说明和解释。

xxxxxx->

"""

defget_prompt(text):
returnprompt.replace('xxxxxx',text)

response,his=model.chat(tokenizer,get_prompt('味道不错,下次还来'),history=[])
print(response)
好评
#增加4个范例
his.append(("太贵了->","差评"))
his.append(("非常快,味道好->","好评"))

his.append(("这么咸真的是醉了->","差评"))
his.append(("价格感人优惠多多->","好评"))

我们来测试一下

response,history=model.chat(tokenizer,"一言难尽啊->",history=his)
print(response)

response,history=model.chat(tokenizer,"还凑合一般般->",history=his)
print(response)

response,history=model.chat(tokenizer,"我家狗狗爱吃的->",history=his)
print(response)

差评
差评
好评
#封装成一个函数吧~
defpredict(text):
response,history=model.chat(tokenizer,f"{text}->",history=his,
temperature=0.01)
returnresponse

predict('死鬼,咋弄得这么有滋味呢')#在我们精心设计的一个评论下,ChatGLM2-6b终于预测错误了~
'差评'

我们拿外卖数据集测试一下未经微调,纯粹的 6-shot prompt 的准确率。

importpandasaspd
importnumpyasnp
importdatasets


df=pd.read_csv("data/waimai_10k.csv")

df['tag']=df['label'].map({0:'差评',1:'好评'})
df=df.rename({'review':'text'},axis=1)

dfgood=df.query('tag=="好评"')
dfbad=df.query('tag=="差评"').head(len(dfgood))#采样部分差评,让好评差评平衡
df=pd.concat([dfgood,dfbad])


print(df['tag'].value_counts())

好评    4000
差评    4000
ds_dic=datasets.Dataset.from_pandas(df).train_test_split(
test_size=2000,shuffle=True,seed=43)
dftrain=ds_dic['train'].to_pandas()
dftest=ds_dic['test'].to_pandas()
dftrain.to_parquet('data/dftrain.parquet')
dftest.to_parquet('data/dftest.parquet')

preds=[''forxindftest['tag']]
fromtqdmimporttqdm
foriintqdm(range(len(dftest))):
text=dftest['text'].loc[i]
preds[i]=predict(text)
dftest['pred']=preds
dftest.pivot_table(index='tag',columns='pred',values='text',aggfunc='count')

d1ee5dd4-279b-11ee-962d-dac502259ad0.png

acc=len(dftest.query('tag==pred'))/len(dftest)
print('acc=',acc)
acc= 0.878

可以看到,微调之前,我们的模型准确率为87.8%,下面我们通过6000条左右数据的微调,看看能否把acc打上去~

一,准备数据

我们需要把数据整理成对话的形式,即 context 和 target 的配对,然后拼到一起作为一条样本。

ChatGLM模型本质上做的是一个文字接龙的游戏,即给定一段话的上半部分,它会去续写下半部分。

我们这里指定上半部分为我们设计的文本分类任务的prompt,下半部分为文本分类结果。

所以我们微调的目标就是让它预测的下半部分跟我们的设定的文本分类一致。

1,数据加载

importpandasaspd
importnumpyasnp
importdatasets

dftrain=pd.read_parquet('data/dftrain.parquet')
dftest=pd.read_parquet('data/dftest.parquet')

dftrain['tag'].value_counts()
好评    3006
差评    2994
Name: tag, dtype: int64
#将上下文整理成与推理时候一致,参照model.chat中的源码~
#model.build_inputs??
defbuild_inputs(query,history):
prompt=""
fori,(old_query,response)inenumerate(history):
prompt+="[Round {}]

问:{}

答:{}

".format(i+1,old_query,response)
prompt+="[Round {}]

问:{}->

答:".format(len(history)+1,query)
returnprompt
print(build_inputs('味道不太行',history=his))

[Round 1]

问:文本分类任务:将一段用户给外卖服务的评论进行分类,分成好评或者差评。

下面是一些范例:

味道真不错 -> 好评
太辣了,吃不下都  -> 差评

请对下述评论进行分类。返回'好评'或者'差评',无需其它说明和解释。

味道不错,下次还来 ->



答:好评

[Round 2]

问:太贵了 -> 

答:差评

[Round 3]

问:非常快,味道好 -> 

答:好评

[Round 4]

问:这么咸真的是醉了 -> 

答:差评

[Round 5]

问:价格感人 优惠多多 -> 

答:好评

[Round 6]

问:味道不太行 -> 

答:
dftrain['context']=[build_inputs(x,history=his)forxindftrain['text']]
dftrain['target']=[xforxindftrain['tag']]
dftrain=dftrain[['context','target']]

dftest['context']=[build_inputs(x,history=his)forxindftest['text']]
dftest['target']=[xforxindftest['tag']]
dftest=dftest[['context','target']]

dftest
ds_train=datasets.Dataset.from_pandas(dftrain)
ds_val=datasets.Dataset.from_pandas(dftest)

2,token编码

为了将文本数据喂入模型,需要将词转换为token。

也就是把context转化成context_ids,把target转化成target_ids.

同时,我们还需要将context_ids和target_ids拼接到一起作为模型的input_ids。

这是为什么呢?

因为ChatGLM2基座模型是一个TransformerDecoder结构,是一个被预选练过的纯粹的语言模型(LLM,Large Lauguage Model)。

一个纯粹的语言模型,本质上只能做一件事情,那就是计算任意一段话像'人话'的概率。

我们将context和target拼接到一起作为input_ids, ChatGLM2 就可以判断这段对话像'人类对话'的概率。

在训练的时候我们使用梯度下降的方法来让ChatGLM2的判断更加准确。

训练完成之后,在预测的时候,我们就可以利用贪心搜索或者束搜索的方法按照最像"人类对话"的方式进行更合理的文本生成。

fromtqdmimporttqdm
importtransformers

model_name="chatglm2-6b"
max_seq_length=512
skip_over_length=True

tokenizer=transformers.AutoTokenizer.from_pretrained(
model_name,trust_remote_code=True)

config=transformers.AutoConfig.from_pretrained(
model_name,trust_remote_code=True,device_map='auto')

defpreprocess(example):
context=example["context"]
target=example["target"]

context_ids=tokenizer.encode(
context,
max_length=max_seq_length,
truncation=True)

target_ids=tokenizer.encode(
target,
max_length=max_seq_length,
truncation=True,
add_special_tokens=False)

input_ids=context_ids+target_ids+[config.eos_token_id]

return{"input_ids":input_ids,"context_len":len(context_ids),'target_len':len(target_ids)}

ds_train_token=ds_train.map(preprocess).select_columns(['input_ids','context_len','target_len'])
ifskip_over_length:
ds_train_token=ds_train_token.filter(
lambdaexample:example["context_len"]
ds_val_token=ds_val.map(preprocess).select_columns(['input_ids','context_len','target_len'])
ifskip_over_length:
ds_val_token=ds_val_token.filter(
lambdaexample:example["context_len"]

3, 管道构建

defdata_collator(features:list):
len_ids=[len(feature["input_ids"])forfeatureinfeatures]
longest=max(len_ids)#之后按照batch中最长的input_ids进行padding

input_ids=[]
labels_list=[]

forlength,featureinsorted(zip(len_ids,features),key=lambdax:-x[0]):
ids=feature["input_ids"]
context_len=feature["context_len"]

labels=(
[-100]*(context_len-1)+ids[(context_len-1):]+[-100]*(longest-length)
)#-100标志位后面会在计算loss时会被忽略不贡献损失,我们集中优化target部分生成的loss

ids=ids+[tokenizer.pad_token_id]*(longest-length)

input_ids.append(torch.LongTensor(ids))
labels_list.append(torch.LongTensor(labels))


input_ids=torch.stack(input_ids)
labels=torch.stack(labels_list)
return{
"input_ids":input_ids,
"labels":labels,
}

importtorch
dl_train=torch.utils.data.DataLoader(ds_train_token,num_workers=2,batch_size=4,
pin_memory=True,shuffle=True,
collate_fn=data_collator)
dl_val=torch.utils.data.DataLoader(ds_val_token,num_workers=2,batch_size=4,
pin_memory=True,shuffle=True,
collate_fn=data_collator)

forbatchindl_train:
break

dl_train.size=300#每300个step视作一个epoch,做一次验证

二,定义模型

importwarnings
warnings.filterwarnings("ignore")

fromtransformersimportAutoTokenizer,AutoModel,TrainingArguments,AutoConfig
importtorch
importtorch.nnasnn
frompeftimportget_peft_model,LoraConfig,TaskType

model=AutoModel.from_pretrained("chatglm2-6b",
load_in_8bit=False,
trust_remote_code=True,
device_map='auto')

model.supports_gradient_checkpointing=True#节约cuda
model.gradient_checkpointing_enable()
model.enable_input_require_grads()
#model.lm_head=CastOutputToFloat(model.lm_head)

model.config.use_cache=False#silencethewarnings.Pleasere-enableforinference!


peft_config=LoraConfig(
task_type=TaskType.CAUSAL_LM,inference_mode=False,
r=8,
lora_alpha=32,lora_dropout=0.1,
)

model=get_peft_model(model,peft_config)
model.is_parallelizable=True
model.model_parallel=True
model.print_trainable_parameters()

d21b8610-279b-11ee-962d-dac502259ad0.png

可以看到,通过使用LoRA微调方法,待训练参数只有全部参数的3%左右。

三,训练模型

我们使用我们的梦中情炉torchkeras来实现最优雅的训练循环~

注意这里,为了更加高效地保存和加载参数,我们覆盖了KerasModel中的load_ckpt和save_ckpt方法,

仅仅保存和加载lora权重,这样可以避免加载和保存全部模型权重造成的存储问题。

fromtorchkerasimportKerasModel
fromaccelerateimportAccelerator

classStepRunner:
def__init__(self,net,loss_fn,accelerator=None,stage="train",metrics_dict=None,
optimizer=None,lr_scheduler=None
):
self.net,self.loss_fn,self.metrics_dict,self.stage=net,loss_fn,metrics_dict,stage
self.optimizer,self.lr_scheduler=optimizer,lr_scheduler
self.accelerator=acceleratorifacceleratorisnotNoneelseAccelerator()
ifself.stage=='train':
self.net.train()
else:
self.net.eval()

def__call__(self,batch):

#loss
withself.accelerator.autocast():
loss=self.net(input_ids=batch["input_ids"],labels=batch["labels"]).loss

#backward()
ifself.optimizerisnotNoneandself.stage=="train":
self.accelerator.backward(loss)
ifself.accelerator.sync_gradients:
self.accelerator.clip_grad_norm_(self.net.parameters(),1.0)
self.optimizer.step()
ifself.lr_schedulerisnotNone:
self.lr_scheduler.step()
self.optimizer.zero_grad()

all_loss=self.accelerator.gather(loss).sum()

#losses(orplainmetricsthatcanbeaveraged)
step_losses={self.stage+"_loss":all_loss.item()}

#metrics(statefulmetrics)
step_metrics={}

ifself.stage=="train":
ifself.optimizerisnotNone:
step_metrics['lr']=self.optimizer.state_dict()['param_groups'][0]['lr']
else:
step_metrics['lr']=0.0
returnstep_losses,step_metrics

KerasModel.StepRunner=StepRunner


#仅仅保存lora可训练参数
defsave_ckpt(self,ckpt_path='checkpoint.pt',accelerator=None):
unwrap_net=accelerator.unwrap_model(self.net)
unwrap_net.save_pretrained(ckpt_path)

defload_ckpt(self,ckpt_path='checkpoint.pt'):
self.net=self.net.from_pretrained(self.net,ckpt_path)
self.from_scratch=False

KerasModel.save_ckpt=save_ckpt
KerasModel.load_ckpt=load_ckpt

keras_model=KerasModel(model,loss_fn=None,
optimizer=torch.optim.AdamW(model.parameters(),lr=2e-6))
ckpt_path='waimai_chatglm4'

keras_model.fit(train_data=dl_train,
val_data=dl_val,
epochs=100,patience=5,
monitor='val_loss',mode='min',
ckpt_path=ckpt_path,
mixed_precision='fp16'
)

d24d98f8-279b-11ee-962d-dac502259ad0.png

曲线下降非常优美~

四,验证模型

frompeftimportPeftModel
model=AutoModel.from_pretrained("chatglm2-6b",
load_in_8bit=False,
trust_remote_code=True,
device_map='auto')
model=PeftModel.from_pretrained(model,ckpt_path)
model=model.merge_and_unload()#合并lora权重

defpredict(text):
response,history=model.chat(tokenizer,f"{text}->",history=his,
temperature=0.01)
returnresponse

predict('死鬼,咋弄得这么有滋味呢')

'差评'
dftest=pd.read_parquet('data/dftest.parquet')
preds=[''forxindftest['text']]
fromtqdmimporttqdm
foriintqdm(range(len(dftest))):
text=dftest['text'].loc[i]
preds[i]=predict(text)
100%|██████████| 2000/2000 [03:39<00:00,  9.11it/s]
dftest['pred']=preds
dftest.pivot_table(index='tag',columns='pred',values='text',aggfunc='count')

acc=len(dftest.query('tag==pred'))/len(dftest)
print('acc=',acc)

acc= 0.903

还行,用6000条数据,训练了一个小时左右,准确率到了90.3%,比未经微调的prompt方案的87.8%相比涨了两个多点~

五,使用模型

我们可以调整温度temperature参数,看看有没有机会把这个评论

'死鬼,咋弄得这么有滋味呢' 预测正确

defpredict(text,temperature=0.8):
response,history=model.chat(tokenizer,f"{text}->",history=his,
temperature=temperature)
returnresponse

foriinrange(10):
print(predict('死鬼,咋弄得这么有滋味呢'))
差评
好评
好评
好评
差评
差评
好评
差评
差评
好评

可以看到,这个评论模型其实是不太吃得准它是好评还是差评的,毕竟,死鬼这个词的内涵太丰富了,跟字面的意思并不一样

我们测试一下模型的其他场景对话能力是否受到影响?

response,history=model.chat(tokenizer,"跑步比赛如果你超过了第二名,你会成为第几名?",history=[])
print(response)
如果在跑步比赛中超过了第二名,那么现在就是第二名。如果想要知道现在排名第几,需要知道自己和其他人的成绩。如果知道了所有人的成绩,就可以计算出自己在所有选手中的排名。

六,保存模型

可以将模型和tokenizer都保存到一个新的路径,便于直接加载。

model.save_pretrained("chatglm2-6b-waimai",max_shard_size='1GB')
tokenizer.save_pretrained("chatglm2-6b-waimai")
('chatglm2-6b-waimai/tokenizer_config.json',
 'chatglm2-6b-waimai/special_tokens_map.json',
 'chatglm2-6b-waimai/tokenizer.model',
 'chatglm2-6b-waimai/added_tokens.json')

还需要将相关的py文件也复制过去。

!lschatglm2-6b

d2895f0a-279b-11ee-962d-dac502259ad0.png

!cpchatglm2-6b/*.pychatglm2-6b-waimai/
!lschatglm2-6b-waimai

d2f0cdde-279b-11ee-962d-dac502259ad0.png

fromtransformersimportAutoModel,AutoTokenizer
model_name="chatglm2-6b-waimai"
tokenizer=AutoTokenizer.from_pretrained(
model_name,trust_remote_code=True)
model=AutoModel.from_pretrained(model_name,
trust_remote_code=True).half().cuda()
prompt="""文本分类任务:将一段用户给外卖服务的评论进行分类,分成好评或者差评。

下面是一些范例:

味道真不错->好评
太辣了,吃不下都->差评

请对下述评论进行分类。返回'好评'或者'差评',无需其它说明和解释。

xxxxxx->

"""

defget_prompt(text):
returnprompt.replace('xxxxxx',text)

response,his=model.chat(tokenizer,get_prompt('狗子,怎么做的这么好吃呀?'),history=[])
print(response)
好评

收工。

d339a36a-279b-11ee-962d-dac502259ad0.png





审核编辑:刘清

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • LoRa模块
    +关注

    关注

    5

    文章

    101

    浏览量

    13671
  • nlp
    nlp
    +关注

    关注

    1

    文章

    463

    浏览量

    21823
  • prompt
    +关注

    关注

    0

    文章

    12

    浏览量

    2635
  • LLM
    LLM
    +关注

    关注

    0

    文章

    202

    浏览量

    233

原文标题:60分钟吃掉ChatGLM2-6b微调范例~

文章出处:【微信号:zenRRan,微信公众号:深度学习自然语言处理】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    一个适用于单片机裸机开发的开源轮子

    今天推荐一个适用于单片机裸机开发的开源轮子。
    发表于 07-04 18:38 1648次阅读

    适用于iOS和Android系统,支持Wi-Fi配置与亚马逊账号登陆功能的开源手机APP有吗?

    适用于 iOS 和 Android 系统,支持 Wi-Fi 配置与亚马逊账号登陆功能的开源手机 APP有吗???我在官网上找不到
    发表于 02-17 06:07

    USB标准适用于哪些应用

    USB标准适用于哪些应用 通用串行总线 (USB) 外设接口已广泛应用于所有个人计算平台及众多工业和基础设施平台。不过,与此同时,人们对适用于
    发表于 04-19 14:31 1668次阅读

    英伟达适用于Python的VPF的功能

    NVIDIA推出了适用于Python的开源视频处理框架“VideoProcessingFramework”(VPF)。据悉,VPF 是一组开源的C ++库和Python绑定,可与其封闭源代码Codec SDK进行交互。
    的头像 发表于 12-18 09:58 3151次阅读

    如何实现更绿色、经济的NLP预训练模型迁移

    NLP中,预训练大模型Finetune是一种非常常见的解决问题的范式。利用在海量文本上预训练得到的Bert、GPT等模型,在下游不同任务上分别进行finetune,得到下游
    的头像 发表于 03-21 15:33 1901次阅读

    迁移学习Finetune的四种类型招式

    迁移学习广泛地应用于NLP、CV等各种领域,通过在源域数据上学习知识,再迁移到下游其他目标任务上,提升目标任务上的效果。其中,Pretrai
    的头像 发表于 04-02 17:35 2593次阅读

    DASK适用于Python中的并行和分布式计算

    Dask 是一个灵活的开源库,适用于 Python 中的并行和分布式计算。
    的头像 发表于 05-20 17:35 2216次阅读

    Adapter在finetune全模型参数的效果

    目前在大规模预训练模型上进行finetuneNLP中一种高效的迁移方法,但是对于众多的下游任务而言,finetune是一种低效的参数更新方式,对于每一个下游
    的头像 发表于 08-24 16:19 1574次阅读

    年底冲击业绩神器,成本低见效快适用于各种行业!

    年底冲击业绩神器,成本低见效快适用于各种行业!
    的头像 发表于 11-02 17:20 516次阅读

    适用于Amazon Alexa的游戏Speed Tap开源

    电子发烧友网站提供《适用于Amazon Alexa的游戏Speed Tap开源.zip》资料免费下载
    发表于 12-28 10:43 0次下载
    <b class='flag-5'>适用于</b>Amazon Alexa的游戏Speed Tap<b class='flag-5'>开源</b>

    如何利用LLM做一些多模态任务

    本文整理了近两年来基于LLM做vision-lanuage任务的一些工作,并将其划分为4个类别:
    的头像 发表于 05-17 15:02 602次阅读
    如何利用<b class='flag-5'>LLM</b>做一些多模态<b class='flag-5'>任务</b>

    如何利用LLM做多模态任务

    并且不会透露任何模型上技术细节。因此,现阶段,如何利用LLM做一些多模态任务还是有一定的研究价值的。 本文整理了近两年来基于LLM做vision-lanuage任务的一些工作,并
    的头像 发表于 05-22 15:57 517次阅读
    如何利用<b class='flag-5'>LLM</b>做多模态<b class='flag-5'>任务</b>?

    LLM各种情感分析任务中的表现如何

    ,但是哪种LLM适用于SA任务依然是不清晰的。 论文 :Sentiment Analysis in the Era of Large Language Models: A Reality Check
    的头像 发表于 05-29 17:24 1515次阅读
    <b class='flag-5'>LLM</b>在<b class='flag-5'>各种</b>情感分析<b class='flag-5'>任务</b>中的表现如何

    如何利用OpenVINO加速LangChain中LLM任务

    LangChain 是一个高层级的开源的框架,从字面意义理解,LangChain 可以被用来构建 “语言处理任务的链条”,它可以让AI开发人员把大型语言模型(LLM)的能力和外部数据结合起来,从而
    的头像 发表于 12-05 09:58 406次阅读

    微软正式发布适用于Windows的Sudo

    微软已在 Windows 11 Insider Preview Build 26052 中发布适用于 Windows 的 Sudo,并将其在 MIT 协议下进行开源
    的头像 发表于 03-19 09:20 306次阅读
    微软正式发布<b class='flag-5'>适用于</b>Windows的Sudo