引入
基于Transformers的NLP解決方案的步驟如下:(以文本分類為例)
- 導入相關包,General,可以詢問ai需要導什么包
- 加載數據集,Data_loader,Datasets
- 數據集劃分,測試機,驗證集,訓練集,Datasets
- 數據集預處理,處理空的部分,Tokenizer+Datasets
- 創建模型,Model
- 設置評估函數,可以去hugging face上查看這個相應的評估指標有哪些,Evaluate
- 配置訓練參數,訓練批次,輸出大小,日志打印頻率,Training Argument
- 創建訓練器,Trainer+Data Collator
- 模型訓練,評估,預測,Trainer
- 如果只是想要用模型進行預測使用pipeline即可,不用子啊設置評估和訓練函數了。Pipeline
顯存優化分析
顯存占用分析
-
模型權重
- 4Bytes*模型參數(因為每一個參數都是32bit的)
-
優化器狀態
- 8Bytes*模型參數量,對于常用的AdamW優化器而言
-
梯度
- 4Bytes*模型參數量
-
前向激活值
- 取決于序列長度,隱層額外i都,Batch大小等多個因素
顯存優化策略
使用hfl/chinese-macbert-large,330M進行測試
優化策略 | 優化對象 | 顯存占用 | 訓練時間 |
---|---|---|---|
Baseline(BS 32 ,MaxLength 128) | - | 15.2G | 64s |
+Gradient Accumulation (BS 1, GA 32)gradient_accumulation_steps=32 梯度累加 ? | 前向激活值,一次只計算一個批次是數據,為了防止效果變差,我們設置計算32個batch之后才進行參數優化 | 7.4G | 260s |
+Gradient Checkpoints (BS 1, GA 32)gradient_checkpointing=True梯度檢查點 ? | 前向激活值 ,訓練的過程中會存儲很多沒必要存儲的信息,對于沒有存儲的激活值,可有在反向傳播計算梯度的時候,讓它重新計算即可。 | 7.2G | 422s |
+Adafactor Optiomizer(BS 1,GA32)optim="adafactor" adafactor優化器 ? | 優化器狀態,默認的adaw優化器占用較大,可以用占用比較小的優化器 | 5.0G | 406s |
+Freeze Model(BS 1,GA32) | 前向激活值/梯度,凍結一部分參數,只訓練分類器部分,模型效果會變差 | 3.5G | 178s |
+DataLength(BS1,GA32,MaxLength64) 在數據集中的maxlength處進行修改 | 前向激活值,縮短數據長度 | 3.4G | 126s |
其中對于這個+Freeze Model(BS 1,GA32
作用就是凍結這個模型的bert部分,只訓練這個模型的全連接層部分。
-
這是因為:
預訓練模型(bert)已經學了很多中文知識,像「詞典+語法」。
我們的任務只是影評情感二分類,不想讓它從頭再學中文,只想讓它學「如何把已掌握的知識轉成情感標簽」。
所以把 bert 的大部分權重「凍住」,只訓練后面新加的分類層,既省顯存又省時間,還能防止過擬合。 -
具體操作
for name, param in model.bert.named_parameters():param.requires_grad = False model.bert 就是原始 BERT 的所有層 循環把每一層的權重 param 設置成 requires_grad=False → 不再更新(梯度不計算) 練時只有沒被凍結的層(例如你后面接的 classifier 或 pooler)才會更新。
實戰演練之命名實體識別
命名實體識別任務介紹
介紹
命名實體識別(Named Entity Recognition,簡稱NER)是指識別文本中具有特定意義的實體,
主要包括人名、地名、機構名、專有名詞等。通常包括兩部分:
(1)實體邊界識別(從哪里到哪里是一個實體);(2)確定實體類別(人名、地名、機構名或其他)
eg 小明在北京上班
實體類別 | 實體 |
---|---|
地點 | 北京 |
人物 | 小明 |
數據標注體系
常見的數據標注有IOB1、IOB2、IOE1、IOE2、IOBES、BILOU
?
其中IOB2標注
- I表示實體內部,,O表示實體外部,B表示實體開始
- B/I-XXX,XXX表示具體的類別
IOBES標注
- I或者M表示實體內部,O表示實體外部,B表示實體開始,E表示實體結束,S表示一個詞單獨形成一個
命名實體
評估指標
Precision,Recall,f1
基于Transfromers的解決方案
- ModelForTokenClassification的源碼分析,這個地方看不太懂,先放在這里
評估函數
-
需要額外安裝seqeval
- pip install seqeval
- 安裝過程中報錯Microsoft Visual C++14.0 or greater is required.Getitwith
"Microsoft C++ Build Tools - 進入https://my.visualstudio.com,下載C++buildtools,安裝
-
evaluate.load(“seqeval”)
代碼實戰演練
數據集:peoples_daily_ner
預訓練模型:hfl/chinese-macbert-base
代碼如下所示:
導入相關包,加載數據集
導包
import evaluate
from datasets import load_dataset
from transformers import AutoTokenizer,AutoModelForTokenClassification,TrainingArguments,Trainer,DataCollatorForTokenClassification導入訓練集
ner_datasets = load_dataset("peoples_daily_ner",cache_dir="./data" ,trust_remote_code=True)
ner_datasets查看訓練集第0個數據
ner_datasets["train"][0]
查看訓練集數據的特征,包括模型是什么類型的數據,以及數據標注體系
ner_datasets["train"].features獲取模型的標注類型
label_list = ner_datasets["train"].features["ner_tags"].feature.names
label_list
數據集預處理
使用模型為:hfl/chinese-macbert-base
由于模型中的tokens是已經劃分好了, 直接用tokenizer進行分詞的話,它會將每一個劃分好的詞,當成一個句子
tokenizer = AutoTokenizer.from_pretrained("hfl/chinese-macbert-base")
tokenizer(ner_datasets["train"][0]["tokens"])
我們可以使用is_split_into_words=True 來講這個不要當成一個句子
tokenizer(ner_datasets["train"][0]["tokens"],is_split_into_words=True)
一個單詞可能被 tokenizer 拆成好幾塊積木(sub-word),我們要保證同一塊原單詞的每一塊積木都貼上同一個標簽
返回的字典里多了一組 “word_ids()” 索引,告訴你每塊積木屬于原列表里的第幾個單詞。
還有一個問題就是英文中可能會出現分詞現象,一個單詞被拆分成了多個,所以我們要寫代碼,進行判斷哪些id是屬于一個單詞的。例如下面這個
res = tokenizer("interesting word")
res就會講這個interestion劃分為id,但是interesting并不是單獨一個,而是多個值
res.word_ids(),這個word_ids可以用來判斷這些id哪些是一組的。
具體實現如下所示:(這段代碼的功能就類似于:給一串已經被切成小積木的樂高貼標簽的過程)
假設原始數據如下:
原始 tokens(已分詞) | ner_tags(標簽) |
---|---|
[“我”, “去”, “北京”] | [O, O, B-LOC] |
- ?
O
表示“不是實體” - ?
B-LOC
表示“地點實體的開頭”
# 借助word_ids 實現標簽映射
def process_function(examples):tokenized_exmaples = tokenizer(examples["tokens"], max_length=128, truncation=True, is_split_into_words=True)labels = []# 遍歷這個標簽for i, label in enumerate(examples["ner_tags"]):# word_ids() 返回 [None, 0, 1, 2, 2, None],0 → 原詞列表第 0 個詞“我”,2 → 原詞列表第 2 個詞“北京”(被拆成兩塊積木,都標 2)word_ids = tokenized_exmaples.word_ids(batch_index=i)label_ids = []for word_id in word_ids: # 逐塊積木if word_id is None: # [CLS]、[SEP]、PADlabel_ids.append(-100)else:# word_id=0 → 原詞0 → label[0]=O# word_id=1 → 原詞1 → label[1]=O# word_id=2 → 原詞2 → label[2]=B-LOClabel_ids.append(label[word_id])# 執行上述循環后,label_ids = [-100, O, O, B-LOC, B-LOC, -100],實際代碼里 O 和 B-LOC 會被替換成數字 id,如 0 和 1labels.append(label_ids)tokenized_exmaples["labels"] = labelsreturn tokenized_exmaplestokenized_datasets = ner_datasets.map(process_function, batched=True)
tokenized_datasets
print(tokenized_datasets["train"][0])
創建評估函數
-
predictions:模型猜的 數字標簽 id(一排排學號)
-
labels:真正的 數字標簽 id(一排排正確答案學號)
-
label_list:把數字翻譯成文字標簽的小詞典
例:label_list = [“O”, “B-PER”, “I-PER”, “B-LOC”, “I-LOC”]- 其中這個
[label_list[p] for p, l in zip(prediction, label) if l != -100]
? -
- 取一對
(p, l)
? - 如果
l
不是 -100(不是特殊符號) - 就把
p
轉成文字標簽label_list[p]
存進列表
- 取一對
- 其中這個
# seqeval 是專門給序列標注任務打分的裁判,支持 BIO / IOB2 等格式。
seqeval = evaluate.load("seqeval_metric.py")
seqevalimport numpy as np
def eval_metric(pred):predictions, labels = predpredictions = np.argmax(predictions, axis=-1) #每個位置取分數最高的索引,得到“預測標簽 id”。# 將id轉換為原始的字符串類型的標簽true_predictions = [[label_list[p] for p, l in zip(prediction, label) if l != -100]for prediction, label in zip(predictions, labels) ]true_labels = [[label_list[l] for p, l in zip(prediction, label) if l != -100]for prediction, label in zip(predictions, labels) ]result = seqeval.compute(predictions=true_predictions, references=true_labels, mode="strict", scheme="IOB2")return {"f1": result["overall_f1"]}
創建訓練器
args = TrainingArguments(output_dir="models_for_ner",per_device_train_batch_size=64,per_device_eval_batch_size=128,eval_strategy="epoch",save_strategy="epoch",metric_for_best_model="f1",load_best_model_at_end=True,logging_steps=50,num_train_epochs=1
)trainer = Trainer(model=model,args=args,tokenizer=tokenizer,train_dataset=tokenized_datasets["train"],eval_dataset=tokenized_datasets["validation"],compute_metrics=eval_metric,data_collator=DataCollatorForTokenClassification(tokenizer=tokenizer)
)
模型訓練
trainer.train()
trainer.evaluate(eval_dataset=tokenized_datasets["test"])
模型預測
from transformers import pipeline# 使用pipeline進行推理,要指定id2label
model.config.id2label = {idx: label for idx, label in enumerate(label_list)}
model.config# 如果模型是基于GPU訓練的,那么推理時要指定device
# 對于NER任務,可以指定aggregation_strategy為simple,得到具體的實體的結果,而不是token的結果
ner_pipe = pipeline("token-classification", model=model, tokenizer=tokenizer, device=0, aggregation_strategy="simple")res = ner_pipe("小明在北京上班")
res# 根據start和end取實際的結果
ner_result = {}
x = "小明在北京上班"
for r in res:if r["entity_group"] not in ner_result:ner_result[r["entity_group"]] = []ner_result[r["entity_group"]].append(x[r["start"]: r["end"]])ner_result
后續
后續博主講的課還包括機器閱讀理解,多項選擇,文本相似度,檢索機器人,文本摘要,生成對話機器人等實戰演練課程。由于我沒有做這些實驗的需求,后續的實驗演練課程筆記就不再做了。
我只通過這個命名實體的識別來了解這個Transformer模型實驗的大致流程即可。后續如果有這些方面的實驗需要去做,可以通過繼續學習相關視頻知識
?