Bert4torch 快速入門實(shí)戰(zhàn)
作者簡(jiǎn)介
作者:Bo仔很忙??
原文:https://zhuanlan.zhihu.com/p/486329434
轉(zhuǎn)載者:楊夕
推薦系統(tǒng) 百面百搭地址:
https://github.com/km1994/RES-Interview-Notes
NLP 百面百搭地址:
https://github.com/km1994/NLP-Interview-Notes
個(gè)人筆記:
https://github.com/km1994/nlp_paper_study

背景
本人經(jīng)常會(huì)閱讀蘇神的科學(xué)空間網(wǎng)站,里面有很多對(duì)前言paper淺顯易懂的解釋,以及很多蘇神自己的創(chuàng)新實(shí)踐;并且基于bert4keras框架都有了相應(yīng)的代碼實(shí)現(xiàn)。但是由于本人主要用pytorch開(kāi)發(fā),因此參考bert4keras開(kāi)發(fā)了bert4torch項(xiàng)目,實(shí)現(xiàn)了bert4keras的主要功能。
簡(jiǎn)介
bert4torch是一個(gè)基于pytorch的訓(xùn)練框架,前期以效仿和實(shí)現(xiàn)bert4keras的主要功能為主,方便加載多類預(yù)訓(xùn)練模型進(jìn)行finetune,提供了中文注釋方便用戶理解模型結(jié)構(gòu)。主要是期望應(yīng)對(duì)新項(xiàng)目時(shí),可以直接調(diào)用不同的預(yù)訓(xùn)練模型直接finetune,或方便用戶基于bert進(jìn)行魔改,快速驗(yàn)證自己的idea;節(jié)省在github上clone各種項(xiàng)目耗時(shí)耗力,且本地文件各種copy的問(wèn)題。
pip安裝
pip install bert4torchgithub鏈接https://github.com/Tongjilibo/bert4torch
主要功能
1、加載預(yù)訓(xùn)練權(quán)重(bert、roberta、albert、nezha、bart、RoFormer、ELECTRA、GPT、GPT2、T5)繼續(xù)進(jìn)行finetune

2、在bert基礎(chǔ)上靈活定義自己模型:主要是可以接在bert的[btz, seq_len, hdsz]的隱含層向量后做各種魔改
3、調(diào)用方式和bert4keras基本一致,簡(jiǎn)潔高效
model.fit(train_dataloader,steps_per_epoch=1000,epochs=epochs,callbacks=[evaluator])
4、實(shí)現(xiàn)基于keras的訓(xùn)練進(jìn)度條動(dòng)態(tài)展示
仿照keras的模型訓(xùn)練進(jìn)度條5、配合torchinfo,實(shí)現(xiàn)打印各層參數(shù)量功能
打印參數(shù)6、結(jié)合logger,或者tensorboard可以在后臺(tái)打印日志
支持在訓(xùn)練開(kāi)始/結(jié)束,batch開(kāi)始/結(jié)束,epoch的開(kāi)始/結(jié)束,記錄日志,寫(xiě)tensorboard等
class Callback(object):'''Callback基類 '''def __init__(self):passdef on_train_begin(self, logs=None):passdef on_train_end(self, logs=None):passdef on_epoch_begin(self, global_step, epoch, logs=None):passdef on_epoch_end(self, global_step, epoch, logs=None):passdef on_batch_begin(self, global_step, batch, logs=None):passdef on_batch_end(self, global_step, batch, logs=None):pass
7、集成多個(gè)example,可以作為自己的訓(xùn)練框架,方便在同一個(gè)數(shù)據(jù)集上嘗試多種解決方案
實(shí)現(xiàn)多個(gè)example可供參考支持的預(yù)訓(xùn)練權(quán)重(bert4torch)

實(shí)戰(zhàn)
1. 建模流程示例
# 建立分詞器tokenizer = Tokenizer(dict_path, do_lower_case=True)# 加載數(shù)據(jù)集,可以自己繼承Dataset來(lái)定義class MyDataset(ListDataset):def load_data(filenames):"""讀取文本文件,整理成需要的格式 """D = []return Ddef collate_fn(batch):'''處理上述load_data得到的batch數(shù)據(jù),整理成對(duì)應(yīng)device上的Tensor 注意:返回值分為feature和label, feature可整理成list或tuple '''batch_token_ids, batch_segment_ids, batch_labels = [], [], []return [batch_token_ids, batch_segment_ids], batch_labels.flatten()# 加載數(shù)據(jù)集train_dataloader = DataLoader(MyDataset('file_path'), batch_size=batch_size, shuffle=True, collate_fn=collate_fn) # 定義bert上的模型結(jié)構(gòu),以文本二分類為例class Model(BaseModel):def __init__(self) -> None:super().__init__()self.bert = build_transformer_model(config_path, checkpoint_path, with_pool=True)self.dropout = nn.Dropout(0.1)self.dense = nn.Linear(768, 2)def forward(self, token_ids, segment_ids):# build_transformer_model得到的模型僅接受list/tuple傳參,因此入?yún)⒅挥幸粋€(gè)時(shí)候包裝成[token_ids]hidden_states, pooled_output = self.bert([token_ids, segment_ids])output = self.dropout(pooled_output)output = self.dense(output)return outputmodel = Model().to(device)# 定義使用的loss和optimizer,這里支持自定義model.compile(loss=nn.CrossEntropyLoss(), # 可以自定義Lossoptimizer=optim.Adam(model.parameters(), lr=2e-5), # 可以自定義優(yōu)化器scheduler=None, # 可以自定義schedulermetrics=['accuracy'])# 定義評(píng)價(jià)函數(shù)def evaluate(data):total, right = 0., 0.for x_true, y_true in data:y_pred = model.predict(x_true).argmax(axis=1)total += len(y_true)right += (y_true == y_pred).sum().item()return right / totalclass Evaluator(Callback):"""評(píng)估與保存,這里定義僅在epoch結(jié)束后調(diào)用 """def __init__(self):self.best_val_acc = 0.def on_epoch_end(self, global_step, epoch, logs=None):val_acc = evaluate(valid_dataloader)if val_acc > self.best_val_acc:self.best_val_acc = val_accmodel.save_weights('best_model.pt')print(f'val_acc: {val_acc:.5f}, best_val_acc: {self.best_val_acc:.5f}\n')if?__name__?==?'__main__':evaluator = Evaluator()model.fit(train_dataloader, epochs=20, steps_per_epoch=100, grad_accumulation_steps=2, callbacks=[evaluator])
2. 主要模塊講解
1) 數(shù)據(jù)處理部分
a. 精簡(jiǎn)詞表,并建立分詞器
token_dict, keep_tokens = load_vocab(dict_path=dict_path, # 詞典文件路徑simplified=True, # 過(guò)濾冗余部分token,如[unused1]startswith=['[PAD]', '[UNK]', '[CLS]', '[SEP]'], # 指定起始的token,如[UNK]從bert默認(rèn)的103位置調(diào)整到1)tokenizer?=?Tokenizer(token_dict,?do_lower_case=True)??#?若無(wú)需精簡(jiǎn),僅使用當(dāng)前行定義tokenizer即可
b. 好用的小函數(shù)
text_segmentate(): 截?cái)嗫傞L(zhǎng)度至不超過(guò)maxlen, 接受多個(gè)sequence輸入,每次截?cái)嘧铋L(zhǎng)的句子,indices表示刪除的token位置tokenizer.encode(): 把text轉(zhuǎn)成token_ids,默認(rèn)句首添加[CLS],句尾添加[SEP],返回token_ids和segment_ids,相當(dāng)于同時(shí)調(diào)用tokenizer.tokenize()和tokenizer.tokens_to_ids()tokenizer.decode(): 把token_ids轉(zhuǎn)成text,默認(rèn)會(huì)刪除[CLS], [SEP], [UNK]等特殊字符,相當(dāng)于調(diào)用tokenizer.ids_to_tokens()并做了一些后處理sequence_padding: 將序列padding到同一長(zhǎng)度, 傳入一個(gè)元素為list, ndarray, tensor的list,返回ndarry或tensor
2) 模型定義部分
模型創(chuàng)建
'''調(diào)用模型后,若設(shè)置with_pool,?with_nsp,?with_mlm,則返回值依次為[hidden_states,?pool_emb/nsp_emb,?mlm_scores],否則只返回hidden_states'''build_transformer_model(config_path=config_path, # 模型的config文件地址checkpoint_path=checkpoint_path, # 模型文件地址,默認(rèn)值None表示不加載預(yù)訓(xùn)練模型model='bert', # 加載的模型結(jié)構(gòu),這里Model也可以基于nn.Module自定義后傳入application='encoder', # 模型應(yīng)用,支持encoder,lm和unilm格式segment_vocab_size=2, # type_token_ids數(shù)量,默認(rèn)為2,如不傳入segment_ids則需設(shè)置為0with_pool=False, # 是否包含Pool部分with_nsp=False, # 是否包含NSP部分with_mlm=False, # 是否包含MLM部分return_model_config=False, # 是否返回模型配置參數(shù)output_all_encoded_layers=False, # 是否返回所有hidden_state層)
定義loss,optimizer,scheduler等
'''定義使用的loss和optimizer,這里支持自定義'''model.compile(loss=nn.CrossEntropyLoss(), # 可以自定義Lossoptimizer=optim.Adam(model.parameters(), lr=2e-5), # 可以自定義優(yōu)化器scheduler=None, # 可以自定義scheduleradversarial_train={'name': 'fgm'}, # 訓(xùn)練trick方案設(shè)置,支持fgm, pgd, gradient_penalty, vatmetrics=['accuracy'] # loss等默認(rèn)打印的字段無(wú)需設(shè)置)
自定義模型
'''基于bert上層的各類魔改,如last2layer_average, token_first_last_average'''class Model(BaseModel):# 需要繼承BaseModeldef __init__(self):super().__init__()self.bert = build_transformer_model(config_path, checkpoint_path)def forward(self):pass
自定義訓(xùn)練過(guò)程
'''自定義fit過(guò)程,適用于自帶fit()不滿足需求時(shí)'''class Model(BaseModel):def fit(self, train_dataloader, steps_per_epoch, epochs):train_dataloader = cycle(train_dataloader)self.train()for epoch in range(epochs):for bti in range(steps_per_epoch):train_X, train_y = next(train_dataloader)output = self.forward(*train_X)loss = self.criterion(output, train_y)loss.backward()self.optimizer.step()self.optimizer.zero_grad()
模型保存和加載
'''prefix: 是否以原始的key來(lái)保存,如word_embedding原始key為bert.embeddings.word_embeddings.weight默認(rèn)為None表示不啟用, 若基于BaseModel自定義模型,需指定為bert模型對(duì)應(yīng)的成員變量名,直接使用設(shè)置為''主要是為了別的訓(xùn)練框架容易加載'''model.save_weights(save_path, prefix=None)model.load_weights(load_path, strict=True, prefix=None)
加載transformers模型進(jìn)行訓(xùn)練
from transformers import AutoModelForSequenceClassificationclass Model(BaseModel):def __init__(self):super().__init__()self.bert = AutoModelForSequenceClassification.from_pretrained("file_path", num_labels=2)def forward(self, token_ids, attention_mask, segment_ids):output = self.bert(input_ids=token_ids, attention_mask=attention_mask, token_type_ids=segment_ids)return output.logits
3) 模型評(píng)估部分
'''支持在多個(gè)位置執(zhí)行'''class Evaluator(Callback):"""評(píng)估與保存 """def __init__(self):self.best_val_acc = 0.def on_train_begin(self, logs=None): # 訓(xùn)練開(kāi)始時(shí)候passdef on_train_end(self, logs=None): # 訓(xùn)練結(jié)束時(shí)候passdef on_batch_begin(self, global_step, batch, logs=None): # batch開(kāi)始時(shí)候passdef on_batch_end(self, global_step, batch, logs=None): # batch結(jié)束時(shí)候# 可以設(shè)置每隔多少個(gè)step,后臺(tái)記錄log,寫(xiě)tensorboard等# 盡量不要在batch_begin和batch_end中print,防止打斷進(jìn)度條功能passdef on_epoch_begin(self, global_step, epoch, logs=None): # epoch開(kāi)始時(shí)候passdef on_epoch_end(self, global_step, epoch, logs=None): # epoch結(jié)束時(shí)候val_acc = evaluate(valid_dataloader)if val_acc > self.best_val_acc:self.best_val_acc = val_accmodel.save_weights('best_model.pt')print(f'val_acc: {val_acc:.5f}, best_val_acc: {self.best_val_acc:.5f}\n')
3. 其他特性講解
1) 單機(jī)多卡訓(xùn)練
a. 使用DataParallel
'''DP有兩種方式,第一種是forward只計(jì)算logit,第二種是forward直接計(jì)算loss建議使用第二種,可以部分緩解負(fù)載不均衡的問(wèn)題'''from bert4torch.models import BaseModelDP# ===========處理數(shù)據(jù)和定義model===========model = BaseModelDP(model) # 指定DP模式使用多gpumodel.compile(loss=lambda x, _: x.mean(), # 多個(gè)gpu計(jì)算的loss的均值optimizer=optim.Adam(model.parameters(), lr=2e-5), # 用足夠小的學(xué)習(xí)率)
b. 使用DistributedDataParallel
'''DDP使用torch.distributed.launch,從命令行啟動(dòng)'''# 需要定義命令行參數(shù)parser = argparse.ArgumentParser()parser.add_argument("--local_rank", type=int, default=-1)args = parser.parse_args()torch.cuda.set_device(args.local_rank)device = torch.device('cuda', args.local_rank)torch.distributed.init_process_group(backend='nccl')# ===========處理數(shù)據(jù)和定義model===========# 指定DDP模型使用多gpu, master_rank為指定用于打印訓(xùn)練過(guò)程的local_rankmodel = BaseModelDDP(model, master_rank=0,device_ids=[args.local_rank],output_device=args.local_rank,find_unused_parameters=False??)#?定義使用的loss和optimizer,這里支持自定義model.compile(loss=lambda x, _: x, # 直接把forward計(jì)算的loss傳出來(lái)optimizer=optim.Adam(model.parameters(), lr=2e-5), # 用足夠小的學(xué)習(xí)率)
2) tensorboard保存訓(xùn)練過(guò)程
from tensorboardX import SummaryWriterclass Evaluator(Callback):"""每隔多少個(gè)step評(píng)估并記錄tensorboard """def on_batch_end(self, global_step, batch, logs=None):if global_step % 100 == 0:writer.add_scalar(f"train/loss", logs['loss'], global_step)val_acc = evaluate(valid_dataloader)writer.add_scalar(f"valid/acc", val_acc, global_step)
3) 打印訓(xùn)練參數(shù)
from torchinfo import summarysummary(model, input_data=next(iter(train_dataloader))[0])
