1. 微調(diào)BaiChuan13B來做命名實體識別

        共 31403字,需瀏覽 63分鐘

         ·

        2023-07-28 23:16

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

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

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

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

        為什么呢?因為大模型可以用統(tǒng)一的范式通通將它們搞定,并且效果非常出眾。

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

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

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

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

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

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

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


        在我們正式開始之前,請允許我用簡短的話給沒有NLP基礎(chǔ)知識的小伙伴講解一下什么是命名實體識別。

        命名實體識別NER任務(wù)是NLP的一個常見基礎(chǔ)任務(wù),

        它是Named Entity Recognization的簡稱。

        簡單地說,就是識別一個句子中的各種 名稱實體,諸如:人名,地名,機構(gòu) 等。

        例如對于下面這句話:

        小明對小紅說:"你聽說過安利嗎?"

        其命名實體可以抽取表示如下:

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

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

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

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

        如果網(wǎng)絡(luò)不穩(wěn)定,也可以手動從這個頁面一個一個下載全部文件然后放置到 一個文件夾中例如 '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)
            
            

        下面我們設(shè)計一個7-shot-prompt方法,測試一下BaiChuan13b的實體抽取能力。

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

        下面是一些范例:

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

        請對下述文本進(jìn)行實體抽取,返回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("一些摩洛哥球迷已按捺不住,在看臺上歡呼雀躍")}]
        response = model.chat(tokenizer, messages)
        print(response)

        {"地點":["摩洛哥"], "組織":[]}
        messages = messages+[{"role""assistant""content""{'地點': ['摩洛哥']}"}]
        messages.extend(get_message("這次輪到北京國安隊,不知會不會再步后塵?","{'組織': ['北京國安隊']}"))
        messages.extend(get_message("革命黨人孫中山在澳門成立同盟會分會","{'人名': ['孫中山'], '地名': ['澳門'], '組織': ['同盟會']}"))
        messages.extend(get_message("我曾在安徽蕪湖市和上海浦東打工。","{'地點': ['安徽蕪湖市', '上海浦東']}"))
        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('杜甫是李白的粉絲。'
        "{'人名': ['杜甫', '李白']}"

        我們拿一個開源的中文NER數(shù)據(jù)集來測試一下未經(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 方法來進(jìn)行token編碼,同時把需要學(xué)習(xí)的內(nèi)容添加label.

        1,token編碼

        import torch 

        #將messages編碼成 token, 同時返回labels
        #注意baichuan-13b通過插入tokenizer.user_token_id和tokenizer.assistant_token_id 來區(qū)分用戶和機器人會話內(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#僅對最后一輪的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中最長的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 
        #試跑一個batch
        out = model(**batch)
        out.loss 
        #采樣300個batch作為一個epoch,便于較快驗證
        dl_train.size = 300

        二,定義模型

        下面我們將使用QLoRA(實際上用的是量化的AdaLoRA)算法來微調(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)練(斷點續(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'

        ...
        ...

        我們測試一下微調(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,取得了不可忽視的巨大提升。

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


        瀏覽 1665
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
          
          

            1. 亚洲日本韩国高清在线 | 性猛交ⅹ×××乱大交 | 就爱操逼网 | 欧美综合社区 | 婷婷乱伦视频 |