ChatGLM2-6b是清華開源的小尺寸LLM,只需要一塊普通的顯卡(32G較穩(wěn)妥)即可推理和微調(diào),是目前社區(qū)非?;钴S的一個(gè)開源LLM。
本范例使用非常簡(jiǎn)單的,外賣評(píng)論數(shù)據(jù)集來實(shí)施微調(diào),讓ChatGLM2-6b來對(duì)一段外賣評(píng)論區(qū)分是好評(píng)還是差評(píng)。
可以發(fā)現(xiàn),經(jīng)過微調(diào)后的模型,相比直接 3-shot-prompt 可以取得明顯更好的效果。
值得注意的是,盡管我們以文本分類任務(wù)為例,實(shí)際上,任何NLP任務(wù),例如,命名實(shí)體識(shí)別,翻譯,聊天對(duì)話等等,都可以通過加上合適的上下文,轉(zhuǎn)換成一個(gè)對(duì)話問題,并針對(duì)我們的使用場(chǎng)景,設(shè)計(jì)出合適的數(shù)據(jù)集來微調(diào)開源LLM.
〇,預(yù)訓(xùn)練模型
國內(nèi)可能速度會(huì)比較慢,總共有14多個(gè)G,網(wǎng)速不太好的話,大概可能需要一兩個(gè)小時(shí)。
如果網(wǎng)絡(luò)不穩(wěn)定,也可以手動(dòng)從這個(gè)頁面一個(gè)一個(gè)下載全部文件然后放置到 一個(gè)文件夾中例如 'chatglm2-6b' 以便讀取。
fromtransformersimportAutoModel,AutoTokenizer model_name="chatglm2-6b"#或者遠(yuǎn)程“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, ?it/s]
prompt="""文本分類任務(wù):將一段用戶給外賣服務(wù)的評(píng)論進(jìn)行分類,分成好評(píng)或者差評(píng)。 下面是一些范例: 味道真不錯(cuò)->好評(píng) 太辣了,吃不下都->差評(píng) 請(qǐng)對(duì)下述評(píng)論進(jìn)行分類。返回'好評(píng)'或者'差評(píng)',無需其它說明和解釋。 xxxxxx-> """ defget_prompt(text): returnprompt.replace('xxxxxx',text)
response,his=model.chat(tokenizer,get_prompt('味道不錯(cuò),下次還來'),history=[]) print(response)
好評(píng)
#增加4個(gè)范例 his.append(("太貴了->","差評(píng)")) his.append(("非常快,味道好->","好評(píng)")) his.append(("這么咸真的是醉了->","差評(píng)")) his.append(("價(jià)格感人優(yōu)惠多多->","好評(píng)"))
我們來測(cè)試一下
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)
差評(píng) 差評(píng) 好評(píng)
#封裝成一個(gè)函數(shù)吧~ defpredict(text): response,history=model.chat(tokenizer,f"{text}->",history=his, temperature=0.01) returnresponse predict('死鬼,咋弄得這么有滋味呢')#在我們精心設(shè)計(jì)的一個(gè)評(píng)論下,ChatGLM2-6b終于預(yù)測(cè)錯(cuò)誤了~
'差評(píng)'
我們拿外賣數(shù)據(jù)集測(cè)試一下未經(jīng)微調(diào),純粹的 6-shot prompt 的準(zhǔn)確率。
importpandasaspd importnumpyasnp importdatasets df=pd.read_csv("data/waimai_10k.csv") df['tag']=df['label'].map({0:'差評(píng)',1:'好評(píng)'}) df=df.rename({'review':'text'},axis=1) dfgood=df.query('tag=="好評(píng)"') dfbad=df.query('tag=="差評(píng)"').head(len(dfgood))#采樣部分差評(píng),讓好評(píng)差評(píng)平衡 df=pd.concat([dfgood,dfbad]) print(df['tag'].value_counts())
好評(píng) 4000 差評(píng) 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')
acc=len(dftest.query('tag==pred'))/len(dftest)
print('acc=',acc)
acc= 0.878
可以看到,微調(diào)之前,我們的模型準(zhǔn)確率為87.8%,下面我們通過6000條左右數(shù)據(jù)的微調(diào),看看能否把a(bǔ)cc打上去~
一,準(zhǔn)備數(shù)據(jù)
我們需要把數(shù)據(jù)整理成對(duì)話的形式,即 context 和 target 的配對(duì),然后拼到一起作為一條樣本。
ChatGLM模型本質(zhì)上做的是一個(gè)文字接龍的游戲,即給定一段話的上半部分,它會(huì)去續(xù)寫下半部分。
我們這里指定上半部分為我們?cè)O(shè)計(jì)的文本分類任務(wù)的prompt,下半部分為文本分類結(jié)果。
所以我們微調(diào)的目標(biāo)就是讓它預(yù)測(cè)的下半部分跟我們的設(shè)定的文本分類一致。
1,數(shù)據(jù)加載
importpandasaspd importnumpyasnp importdatasets dftrain=pd.read_parquet('data/dftrain.parquet') dftest=pd.read_parquet('data/dftest.parquet')
dftrain['tag'].value_counts()
好評(píng) 3006 差評(píng) 2994 Name: tag, dtype: int64
#將上下文整理成與推理時(shí)候一致,參照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] 問:文本分類任務(wù):將一段用戶給外賣服務(wù)的評(píng)論進(jìn)行分類,分成好評(píng)或者差評(píng)。 下面是一些范例: 味道真不錯(cuò) -> 好評(píng) 太辣了,吃不下都 -> 差評(píng) 請(qǐng)對(duì)下述評(píng)論進(jìn)行分類。返回'好評(píng)'或者'差評(píng)',無需其它說明和解釋。 味道不錯(cuò),下次還來 -> 答:好評(píng) [Round 2] 問:太貴了 -> 答:差評(píng) [Round 3] 問:非???,味道好 -> 答:好評(píng) [Round 4] 問:這么咸真的是醉了 -> 答:差評(píng) [Round 5] 問:價(jià)格感人 優(yōu)惠多多 -> 答:好評(píng) [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編碼
為了將文本數(shù)據(jù)喂入模型,需要將詞轉(zhuǎn)換為token。
也就是把context轉(zhuǎn)化成context_ids,把target轉(zhuǎn)化成target_ids.
同時(shí),我們還需要將context_ids和target_ids拼接到一起作為模型的input_ids。
這是為什么呢?
因?yàn)镃hatGLM2基座模型是一個(gè)TransformerDecoder結(jié)構(gòu),是一個(gè)被預(yù)選練過的純粹的語言模型(LLM,Large Lauguage Model)。
一個(gè)純粹的語言模型,本質(zhì)上只能做一件事情,那就是計(jì)算任意一段話像'人話'的概率。
我們將context和target拼接到一起作為input_ids, ChatGLM2 就可以判斷這段對(duì)話像'人類對(duì)話'的概率。
在訓(xùn)練的時(shí)候我們使用梯度下降的方法來讓ChatGLM2的判斷更加準(zhǔn)確。
訓(xùn)練完成之后,在預(yù)測(cè)的時(shí)候,我們就可以利用貪心搜索或者束搜索的方法按照最像"人類對(duì)話"的方式進(jìn)行更合理的文本生成。
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, 管道構(gòu)建
defdata_collator(features:list): len_ids=[len(feature["input_ids"])forfeatureinfeatures] longest=max(len_ids)#之后按照batch中最長的input_ids進(jìn)行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標(biāo)志位后面會(huì)在計(jì)算loss時(shí)會(huì)被忽略不貢獻(xiàn)損失,我們集中優(yōu)化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: breakdl_train.size=300#每300個(gè)step視作一個(gè)epoch,做一次驗(yàn)證
二,定義模型
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#節(jié)約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()
可以看到,通過使用LoRA微調(diào)方法,待訓(xùn)練參數(shù)只有全部參數(shù)的3%左右。
三,訓(xùn)練模型
我們使用我們的夢(mèng)中情爐torchkeras來實(shí)現(xiàn)最優(yōu)雅的訓(xùn)練循環(huán)~
注意這里,為了更加高效地保存和加載參數(shù),我們覆蓋了KerasModel中的load_ckpt和save_ckpt方法,
僅僅保存和加載lora權(quán)重,這樣可以避免加載和保存全部模型權(quán)重造成的存儲(chǔ)問題。
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可訓(xùn)練參數(shù) 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_ckptkeras_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' )
曲線下降非常優(yōu)美~
四,驗(yàn)證模型
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權(quán)重defpredict(text): response,history=model.chat(tokenizer,f"{text}->",history=his, temperature=0.01) returnresponse predict('死鬼,咋弄得這么有滋味呢')'差評(píng)'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']=predsdftest.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條數(shù)據(jù),訓(xùn)練了一個(gè)小時(shí)左右,準(zhǔn)確率到了90.3%,比未經(jīng)微調(diào)的prompt方案的87.8%相比漲了兩個(gè)多點(diǎn)~
五,使用模型
我們可以調(diào)整溫度temperature參數(shù),看看有沒有機(jī)會(huì)把這個(gè)評(píng)論
'死鬼,咋弄得這么有滋味呢' 預(yù)測(cè)正確
defpredict(text,temperature=0.8): response,history=model.chat(tokenizer,f"{text}->",history=his, temperature=temperature) returnresponse foriinrange(10): print(predict('死鬼,咋弄得這么有滋味呢'))差評(píng) 好評(píng) 好評(píng) 好評(píng) 差評(píng) 差評(píng) 好評(píng) 差評(píng) 差評(píng) 好評(píng)
可以看到,這個(gè)評(píng)論模型其實(shí)是不太吃得準(zhǔn)它是好評(píng)還是差評(píng)的,畢竟,死鬼這個(gè)詞的內(nèi)涵太豐富了,跟字面的意思并不一樣
我們測(cè)試一下模型的其他場(chǎng)景對(duì)話能力是否受到影響?
response,history=model.chat(tokenizer,"跑步比賽如果你超過了第二名,你會(huì)成為第幾名?",history=[]) print(response)如果在跑步比賽中超過了第二名,那么現(xiàn)在就是第二名。如果想要知道現(xiàn)在排名第幾,需要知道自己和其他人的成績。如果知道了所有人的成績,就可以計(jì)算出自己在所有選手中的排名。
六,保存模型
可以將模型和tokenizer都保存到一個(gè)新的路徑,便于直接加載。
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')
還需要將相關(guān)的py文件也復(fù)制過去。
!lschatglm2-6b
!cpchatglm2-6b/*.pychatglm2-6b-waimai/!lschatglm2-6b-waimai
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="""文本分類任務(wù):將一段用戶給外賣服務(wù)的評(píng)論進(jìn)行分類,分成好評(píng)或者差評(píng)。 下面是一些范例: 味道真不錯(cuò)->好評(píng) 太辣了,吃不下都->差評(píng) 請(qǐng)對(duì)下述評(píng)論進(jìn)行分類。返回'好評(píng)'或者'差評(píng)',無需其它說明和解釋。 xxxxxx-> """ defget_prompt(text): returnprompt.replace('xxxxxx',text)response,his=model.chat(tokenizer,get_prompt('狗子,怎么做的這么好吃呀?'),history=[]) print(response)好評(píng)
收工。
審核編輯:劉清
-
LoRa模塊
+關(guān)注
關(guān)注
5文章
114瀏覽量
13790 -
nlp
+關(guān)注
關(guān)注
1文章
481瀏覽量
21932 -
prompt
+關(guān)注
關(guān)注
0文章
13瀏覽量
2653 -
LLM
+關(guān)注
關(guān)注
0文章
247瀏覽量
279
原文標(biāo)題:60分鐘吃掉ChatGLM2-6b微調(diào)范例~
文章出處:【微信號(hào):zenRRan,微信公眾號(hào):深度學(xué)習(xí)自然語言處理】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論