<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          微調(diào)BaiChuan13B來(lái)做命名實(shí)體識(shí)別

          共 31403字,需瀏覽 63分鐘

           ·

          2023-07-28 23:16

          傳統(tǒng)上,一般把NLP的研究領(lǐng)域大致分為自然語(yǔ)言理解(NLU)和自然語(yǔ)言生成(NLG)兩種。

          NLU側(cè)重于如何理解文本,包括文本分類、命名實(shí)體識(shí)別、指代消歧、句法分析、機(jī)器閱讀理解等;

          NLG則側(cè)重于理解文本后如何生成自然文本,包括自動(dòng)摘要、機(jī)器翻譯、問(wèn)答系統(tǒng)、對(duì)話機(jī)器人等。

          但是以ChatGPT為代表的大模型出來(lái)后,這些傳統(tǒng)的NLP的細(xì)分研究領(lǐng)域基本可以說(shuō)都失去了獨(dú)立研究的價(jià)值。

          為什么呢?因?yàn)榇竽P涂梢杂媒y(tǒng)一的范式通通將它們搞定,并且效果非常出眾。

          在之前的例子中,我們演示了使用QLoRA算法來(lái)對(duì)BaiChuan-13B實(shí)施微調(diào)以處理最簡(jiǎn)單的文本分類任務(wù)。

          Baichuan-13B 保姆級(jí)微調(diào)范例

          在外賣評(píng)論數(shù)據(jù)集上,微調(diào)后測(cè)試集acc由0.8925提升到0.9015約提升了1個(gè)百分點(diǎn)。

          在本例中,我們使用幾乎相同的流程和方法來(lái)微調(diào)BaiChuan-13B以更好地處理命名實(shí)體識(shí)別任務(wù)。

          實(shí)驗(yàn)結(jié)果顯示,在NER任務(wù)上經(jīng)過(guò)微調(diào),我們的f1-score取得了不可忽略的提升(0.4313—>0.8768)。

          注:跑完本流程需要至少32G的CPU,需要約2個(gè)小時(shí)的訓(xùn)練時(shí)間。

          公眾號(hào)算法美食屋后臺(tái)回復(fù)關(guān)鍵詞:torchkeras,獲取本文notebook源碼和dfner_13k.pkl數(shù)據(jù)集下載鏈接~


          在我們正式開始之前,請(qǐng)?jiān)试S我用簡(jiǎn)短的話給沒(méi)有NLP基礎(chǔ)知識(shí)的小伙伴講解一下什么是命名實(shí)體識(shí)別。

          命名實(shí)體識(shí)別NER任務(wù)是NLP的一個(gè)常見(jiàn)基礎(chǔ)任務(wù),

          它是Named Entity Recognization的簡(jiǎn)稱。

          簡(jiǎn)單地說(shuō),就是識(shí)別一個(gè)句子中的各種 名稱實(shí)體,諸如:人名,地名,機(jī)構(gòu) 等。

          例如對(duì)于下面這句話:

          小明對(duì)小紅說(shuō):"你聽說(shuō)過(guò)安利嗎?"

          其命名實(shí)體可以抽取表示如下:

          {"人名": ["小明","小紅"], "組織": ["安利"]}

          〇,預(yù)訓(xùn)練模型

          我們需要從 https://huggingface.co/baichuan-inc/Baichuan-13B-Chat 下載baichuan-13b-chat的模型。

          國(guó)內(nèi)可能速度會(huì)比較慢,總共有25個(gè)G左右,網(wǎng)速不太好的話,大概可能需要兩到三個(gè)小時(shí)。

          如果網(wǎng)絡(luò)不穩(wěn)定,也可以手動(dòng)從這個(gè)頁(yè)面一個(gè)一個(gè)下載全部文件然后放置到 一個(gè)文件夾中例如 'baichuan-13b' 以便讀取。

          import warnings
          warnings.filterwarnings('ignore')
          import torch
          from transformers import AutoTokenizer, AutoModelForCausalLM,AutoConfig, AutoModel, BitsAndBytesConfig
          from transformers.generation.utils import GenerationConfig
          import torch.nn as nn


          #使用QLoRA引入的 NF4量化數(shù)據(jù)類型以節(jié)約顯存
          model_name_or_path ='../baichuan-13b' #遠(yuǎn)程 'baichuan-inc/Baichuan-13B-Chat'

          bnb_config=BitsAndBytesConfig(
                      load_in_4bit=True,
                      bnb_4bit_compute_dtype=torch.float16,
                      bnb_4bit_use_double_quant=True,
                      bnb_4bit_quant_type="nf4",
                      llm_int8_threshold=6.0,
                      llm_int8_has_fp16_weight=False,
                  )

          tokenizer = AutoTokenizer.from_pretrained(
             model_name_or_path, trust_remote_code=True)

          model = AutoModelForCausalLM.from_pretrained(model_name_or_path,
                          quantization_config=bnb_config,
                          trust_remote_code=True

          model.generation_config = GenerationConfig.from_pretrained(model_name_or_path)

          from IPython.display import clear_output 
          messages = []
          messages.append({"role""user",
                           "content""世界上第二高的山峰是哪座?"})
          response = model.chat(tokenizer,messages=messages,stream=True)
          for res in response:
              print(res)
              clear_output(wait=True)
              
              

          下面我們?cè)O(shè)計(jì)一個(gè)7-shot-prompt方法,測(cè)試一下BaiChuan13b的實(shí)體抽取能力。

          prefix = '''命名實(shí)體識(shí)別:抽取文本中的 人名,地點(diǎn),組織 這三類命名實(shí)體,并按照json格式返回結(jié)果。

          下面是一些范例:

          小明對(duì)小紅說(shuō):"你聽說(shuō)過(guò)安利嗎?" -> {"人名": ["小明","小紅"], "組織": ["安利"]}
          現(xiàn)在,每年有幾十萬(wàn)中國(guó)人到美國(guó)訪問(wèn),幾千名中國(guó)留學(xué)生到美國(guó)就學(xué)。 -> {"地點(diǎn)": ["中國(guó)", "美國(guó)"]}
          中國(guó)是聯(lián)合國(guó)安理會(huì)常任理事國(guó)之一。 -> {"地點(diǎn)": ["中國(guó)"], "組織": ["聯(lián)合國(guó)"]}

          請(qǐng)對(duì)下述文本進(jìn)行實(shí)體抽取,返回json格式。

          '''


          def get_prompt(text):
              return prefix+text+' -> '

          def get_message(prompt,response):
              return [{"role""user""content"f'{prompt} -> '},
                      {"role""assistant""content": response}]

          messages  = [{"role""user""content": get_prompt("一些摩洛哥球迷已按捺不住,在看臺(tái)上歡呼雀躍")}]
          response = model.chat(tokenizer, messages)
          print(response)

          {"地點(diǎn)":["摩洛哥"], "組織":[]}
          messages = messages+[{"role""assistant""content""{'地點(diǎn)': ['摩洛哥']}"}]
          messages.extend(get_message("這次輪到北京國(guó)安隊(duì),不知會(huì)不會(huì)再步后塵?","{'組織': ['北京國(guó)安隊(duì)']}"))
          messages.extend(get_message("革命黨人孫中山在澳門成立同盟會(huì)分會(huì)","{'人名': ['孫中山'], '地名': ['澳門'], '組織': ['同盟會(huì)']}"))
          messages.extend(get_message("我曾在安徽蕪湖市和上海浦東打工。","{'地點(diǎn)': ['安徽蕪湖市', '上海浦東']}"))
          display(messages)




          def predict(text,temperature=0.01):
              model.generation_config.temperature=temperature
              response = model.chat(tokenizer, 
                                    messages = messages+[{'role':'user','content':f'{text} -> '}])
              return response

          predict('杜甫是李白的粉絲。'
          "{'人名': ['杜甫', '李白']}"

          我們拿一個(gè)開源的中文NER數(shù)據(jù)集來(lái)測(cè)試一下未經(jīng)微調(diào),僅僅使用7-shot-prompt的預(yù)訓(xùn)練模型的效果。

          from sklearn.model_selection import train_test_split
          import pandas as pd 

          df = pd.read_pickle('dfner_13k.pkl')
          dfdata,dftest = train_test_split(df,test_size=300,random_state=42)
          dftrain,dfval = train_test_split(dfdata,test_size=200,random_state=42)
          preds = ['' for x in dftest['target']]
          for i in tqdm(range(len(preds))):
              preds[i] = predict(dftest['text'].iloc[i])
              
          def toset(s):
              try:
                  dic = eval(str(s))
                  res = []
                  for k,v in dic.items():
                      for x in v:
                          if x:
                              res.append((k,x))
                  return set(res)
              except Exception as err:
                  print(err)
                  return set()
          dftest['pred'] = [toset(x) for x in preds]
          dftest['gt'] = [toset(x) for x in dftest['target']]
          dftest['tp_cnt'] = [len(pred&gt) for pred,gt in zip(dftest['pred'],dftest['gt'])]
          dftest['pred_cnt'] = [len(x) for x in dftest['pred']]
          dftest['gt_cnt'] = [len(x) for x in dftest['gt']]

          precision = sum(dftest['tp_cnt'])/sum(dftest['pred_cnt'])
          print('precision = '+str(precision))

          recall = sum(dftest['tp_cnt'])/sum(dftest['gt_cnt'])
          print('recall = '+str(recall))

          f1 = 2*precision*recall/(precision+recall)
          print('f1_score = '+str(f1))

          precision = 0.4316109422492401
          recall = 0.45151033386327505
          f1_score = 0.44133644133644134

          微調(diào)前 f1_score為 0.44.

          一,準(zhǔn)備數(shù)據(jù)

          我們仿照百川模型的 model._build_chat_input 方法來(lái)進(jìn)行token編碼,同時(shí)把需要學(xué)習(xí)的內(nèi)容添加label.

          1,token編碼

          import torch 

          #將messages編碼成 token, 同時(shí)返回labels
          #注意baichuan-13b通過(guò)插入tokenizer.user_token_id和tokenizer.assistant_token_id 來(lái)區(qū)分用戶和機(jī)器人會(huì)話內(nèi)容

          # reference@ model._build_chat_input?
          def build_chat_input(messages, model=model,
                               tokenizer=tokenizer, 
                               max_new_tokens: int=0)
          :

              max_new_tokens = max_new_tokens or model.generation_config.max_new_tokens
              max_input_tokens = model.config.model_max_length - max_new_tokens
              max_input_tokens = max(model.config.model_max_length // 2, max_input_tokens)
              
              total_input, round_input, total_label, round_label = [], [], [], []
              
              for i, message in enumerate(messages[::-1]):
                  content_tokens = tokenizer.encode(message['content'])
                  if message['role'] == 'user':
                      round_input = [model.generation_config.user_token_id] + content_tokens + round_input
                      round_label = [-100]+[-100 for _ in content_tokens]+ round_label
                      
                      if total_input and len(total_input) + len(round_input) > max_input_tokens:
                          break
                      else:
                          total_input = round_input + total_input
                          total_label = round_label + total_label
                          if len(total_input) >= max_input_tokens:
                              break
                          else:
                              round_input = []
                              round_label = []
                              
                  elif message['role'] == 'assistant':
                      round_input = [
                          model.generation_config.assistant_token_id
                      ] + content_tokens + [
                          model.generation_config.eos_token_id
                      ] + round_input
                      
                      if i==0#僅對(duì)最后一輪的target進(jìn)行學(xué)習(xí)
                          round_label = [
                              -100
                          ] + content_tokens + [
                              model.generation_config.eos_token_id
                          ]+ round_label
                      else:
                          round_label = [
                              -100
                          ] + [-100 for _ in content_tokens] + [
                              -100
                          ]+ round_label
                          
                  else:
                      raise ValueError(f"message role not supported yet: {message['role']}")
                      
              total_input = total_input[-max_input_tokens:]  # truncate left
              total_label = total_label[-max_input_tokens:]
              
              total_input.append(model.generation_config.assistant_token_id)
              total_label.append(-100)
              
              return total_input,total_label


          2,做數(shù)據(jù)集

          from torch.utils.data import Dataset,DataLoader 
          from copy import deepcopy
          class MyDataset(Dataset):
              def __init__(self,df,
                           messages
                          )
          :

                  self.df = df 
                  self.messages = messages
                  
              def __len__(self):
                  return len(self.df)
                  
              def get_samples(self,index):
                  samples = []
                  d = dict(self.df.iloc[index])
                  samples.append(d)
                  return samples
              
              def get_messages(self,index):
                  samples = self.get_samples(index)
                  messages = deepcopy(self.messages)
                  for i,d in enumerate(samples):

                      messages.append({'role':'user','content':d['text']+' -> '})
                      messages.append({'role':'assistant','content':str(d['target'])})
                  return messages
                  
              def __getitem__(self,index):
                  messages = self.get_messages(index)
                  input_ids, labels = build_chat_input(messages)
                  return {'input_ids':input_ids,'labels':labels}

              def show_sample(self,index):
                  samples = self.get_samples(index)
                  print(samples)
              
              
          ds_train = MyDataset(dftrain,messages)
          ds_val = MyDataset(dfval,messages)

          3,創(chuàng)建管道

          def data_collator(examples: list):
              len_ids = [len(example["input_ids"]) for example in examples]
              longest = max(len_ids) #之后按照batch中最長(zhǎng)的input_ids進(jìn)行padding
              
              input_ids = []
              labels_list = []
              
              for length, example in sorted(zip(len_ids, examples), key=lambda x: -x[0]):
                  ids = example["input_ids"]
                  labs = example["labels"]
                  
                  ids = ids + [tokenizer.pad_token_id] * (longest - length)
                  labs = labs + [-100] * (longest - length)
                  
                  input_ids.append(torch.LongTensor(ids))
                  labels_list.append(torch.LongTensor(labs))
                    
              input_ids = torch.stack(input_ids)
              labels = torch.stack(labels_list)
              return {
                  "input_ids": input_ids,
                  "labels": labels,
              }

          import torch 
          dl_train = torch.utils.data.DataLoader(ds_train,num_workers=2,batch_size=1,
                                                 pin_memory=True,shuffle=True,
                                                 collate_fn = data_collator)

          dl_val = torch.utils.data.DataLoader(ds_val,num_workers=2,batch_size=1,
                                              pin_memory=True,shuffle=False,
                                               collate_fn = data_collator)

          for batch in dl_train:
              break 
          #試跑一個(gè)batch
          out = model(**batch)
          out.loss 
          #采樣300個(gè)batch作為一個(gè)epoch,便于較快驗(yàn)證
          dl_train.size = 300

          二,定義模型

          下面我們將使用QLoRA(實(shí)際上用的是量化的AdaLoRA)算法來(lái)微調(diào)Baichuan-13b模型。

          from peft import get_peft_config, get_peft_model, TaskType
          model.supports_gradient_checkpointing = True  #
          model.gradient_checkpointing_enable()
          model.enable_input_require_grads()

          model.config.use_cache = False  # silence the warnings. Please re-enable for inference!

          import bitsandbytes as bnb 
          def find_all_linear_names(model):
              """
              找出所有全連接層,為所有全連接添加adapter
              """

              cls = bnb.nn.Linear4bit
              lora_module_names = set()
              for name, module in model.named_modules():
                  if isinstance(module, cls):
                      names = name.split('.')
                      lora_module_names.add(names[0if len(names) == 1 else names[-1])

              if 'lm_head' in lora_module_names:  # needed for 16-bit
                  lora_module_names.remove('lm_head')
              return list(lora_module_names)

          from peft import prepare_model_for_kbit_training 
          model = prepare_model_for_kbit_training(model)

          lora_modules = find_all_linear_names(model)
          print(lora_modules) 

          ['down_proj', 'gate_proj', 'W_pack', 'o_proj', 'up_proj']
          from peft import AdaLoraConfig
          peft_config = AdaLoraConfig(
              task_type=TaskType.CAUSAL_LM, inference_mode=False,
              r=16,
              lora_alpha=16, lora_dropout=0.05,
              target_modules= lora_modules
          )

          peft_model = get_peft_model(model, peft_config)

          peft_model.is_parallelizable = True
          peft_model.model_parallel = True
          peft_model.print_trainable_parameters()

          trainable params: 41,843,040 || all params: 7,002,181,160 || trainable%: 0.5975715144165165
          out = peft_model.forward(**batch)
          out[0]

          三,訓(xùn)練模型

          from torchkeras import KerasModel 
          from accelerate import Accelerator 

          class StepRunner:
              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 = accelerator if accelerator is not None else Accelerator() 
                  if self.stage=='train':
                      self.net.train() 
                  else:
                      self.net.eval()
              
              def __call__(self, batch):
                  
                  #loss
                  with self.accelerator.autocast():
                      loss = self.net.forward(**batch)[0]

                  #backward()
                  if self.optimizer is not None and self.stage=="train":
                      self.accelerator.backward(loss)
                      if self.accelerator.sync_gradients:
                          self.accelerator.clip_grad_norm_(self.net.parameters(), 1.0)
                      self.optimizer.step()
                      if self.lr_scheduler is not None:
                          self.lr_scheduler.step()
                      self.optimizer.zero_grad()
                      
                  all_loss = self.accelerator.gather(loss).sum()
                  
                  #losses (or plain metrics that can be averaged)
                  step_losses = {self.stage+"_loss":all_loss.item()}
                  
                  #metrics (stateful metrics)
                  step_metrics = {}
                  
                  if self.stage=="train":
                      if self.optimizer is not None:
                          step_metrics['lr'] = self.optimizer.state_dict()['param_groups'][0]['lr']
                      else:
                          step_metrics['lr'] = 0.0
                  return step_losses,step_metrics
              
          KerasModel.StepRunner = StepRunner 

          #僅僅保存QLora可訓(xùn)練參數(shù)
          def save_ckpt(self, ckpt_path='checkpoint', accelerator = None):
              unwrap_net = accelerator.unwrap_model(self.net)
              unwrap_net.save_pretrained(ckpt_path)
              
          def load_ckpt(self, ckpt_path='checkpoint'):
              import os
              self.net.load_state_dict(
                  torch.load(os.path.join(ckpt_path,'adapter_model.bin')),strict =False)
              self.from_scratch = False
              
          KerasModel.save_ckpt = save_ckpt 
          KerasModel.load_ckpt = load_ckpt 

          optimizer = bnb.optim.adamw.AdamW(peft_model.parameters(),
                                            lr=6e-05,is_paged=True)  #'paged_adamw'
          keras_model = KerasModel(peft_model,loss_fn =None,
                  optimizer=optimizer) 
          ckpt_path = 'baichuan13b_ner'


          # keras_model.load_ckpt(ckpt_path) #支持加載微調(diào)后的權(quán)重繼續(xù)訓(xùn)練(斷點(diǎn)續(xù)訓(xùn))
          keras_model.fit(train_data = dl_train,
                          val_data = dl_val,
                          epochs=100,patience=10,
                          monitor='val_loss',mode='min',
                          ckpt_path = ckpt_path
                         )

          四,保存模型

          為減少GPU壓力,此處可重啟kernel釋放顯存

          import warnings 
          warnings.filterwarnings('ignore')
          import torch
          from transformers import AutoTokenizer, AutoModelForCausalLM,AutoConfig, AutoModel, BitsAndBytesConfig
          from transformers.generation.utils import GenerationConfig
          import torch.nn as nn
          model_name_or_path ='../baichuan-13b'
          ckpt_path = 'baichuan13b_ner'
          tokenizer = AutoTokenizer.from_pretrained(
              model_name_or_path,
              trust_remote_code=True
          )
          model_old = AutoModelForCausalLM.from_pretrained(
              model_name_or_path,
              trust_remote_code=True,
              low_cpu_mem_usage=True,
              torch_dtype=torch.float16,
              device_map='auto'
          )

          from peft import PeftModel

          #可能需要5分鐘左右
          peft_model = PeftModel.from_pretrained(model_old, ckpt_path)
          model_new = peft_model.merge_and_unload()

          from transformers.generation.utils import GenerationConfig
          model_new.generation_config = GenerationConfig.from_pretrained(model_name_or_path)
          from IPython.display import clear_output
          messages = []
          messages.append({"role""user",
                           "content""世界上第二高的山峰是什么?"})
          response = model_new.chat(tokenizer,messages=messages,stream=True)
          for res in response:
              print(res)
              clear_output(wait=True)

          喬戈里峰。世界第二高峰———喬戈里峰西方登山者稱其為k2峰,海拔高度是8611米,位于喀喇昆侖山脈的中巴邊境上.

          save_path = 'baichuan-13b-ner'
          tokenizer.save_pretrained(save_path)
          model_new.save_pretrained(save_path)
          !cp ../baichuan-13b/*.py  baichuan-13b-ner

          五,使用模型

          為減少GPU壓力,此處可再次重啟kernel釋放顯存。

          import torch
          from transformers import AutoTokenizer, AutoModelForCausalLM,AutoConfig, BitsAndBytesConfig
          from transformers.generation.utils import GenerationConfig
          import torch.nn as nn

          import warnings
          warnings.filterwarnings('ignore')

          model_name_or_path = 'baichuan-13b-ner'

          ...
          ...

          我們測(cè)試一下微調(diào)后的效果。

          import pandas as pd 
          import numpy as np 
          import datasets 
          from tqdm import tqdm 

          from sklearn.model_selection import train_test_split
          import pandas as pd 

          df = pd.read_pickle('dfner_13k.pkl')
          dfdata,dftest = train_test_split(df,test_size=300,random_state=42)
          dftrain,dfval = train_test_split(dfdata,test_size=200,random_state=42)
          ...
          ...
          ...

          precision = sum(dftest['tp_cnt'])/sum(dftest['pred_cnt'])
          print('precision = '+str(precision))

          recall = sum(dftest['tp_cnt'])/sum(dftest['gt_cnt'])
          print('recall = '+str(recall))

          f1 = 2*precision*recall/(precision+recall)
          print('f1_score = '+str(f1))


          precision = 0.9139280125195618
          recall = 0.8427128427128427
          f1_score = 0.876876876876877

          微調(diào)后的f1_score為0.8768,相比微調(diào)前的f1_score=0.44,取得了不可忽視的巨大提升。

          公眾號(hào)算法美食屋臺(tái)回復(fù)關(guān)鍵詞:torchkeras,獲取本文notebook源碼和更多有趣范例~


          瀏覽 1665
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲天堂网址 | 猫咪AV成人永久网站在线观看 | 亚洲无码视频在线观看免费 | 国产成人麻豆精品午夜在线 | 人妻超碰 |