- Hugging Face 是一家在 NLP 和 AI 領域具有重要影響力的科技公司,他們的開源工具和社區建設為NLP研究和開發提供了強大的支持。它們擁有當前最活躍、最受關注、影響力最大的 NLP 社區,最新最強的 NLP 模型大多在這里發布和開源。該社區也提供了豐富的教程、文檔和示例代碼,幫助用戶快速上手并深入理解各類 Transformer 模型和 NLP 技術
- Transformers 庫是 Hugging Face 最著名的貢獻之一,它最初是 Transformer 模型的 pytorch 復現庫,隨著不斷建設,至今已經成為 NLP 領域最重要,影響最大的基礎設施之一。該庫提供了大量預訓練的模型,涵蓋了多種語言和任務,成為當今大模型工程實現的主流標準,換句話說,如果你正在開發一個大模型,那么按 Transformer 庫的代碼格式進行工程實現、將 check point 打包成 hugging face 格式開源到社區,對于推廣你的工作有很大的助力作用。本系列文章將介紹 Transformers 庫的基本使用方法
- 參考:
- 官方教程
- 手把手帶你實戰HuggingFace Transformers
文章目錄
- 1. Transformer Model
- 1.1 基本架構
- 1.2 模型類型
- 1.3 Model Head
- 2. Transformer 庫 Model 組件的基本使用
- 2.1 模型加載
- 2.2 模型調用
- 2.2.1 不帶 Model Head 的模型調用
- 2.2.2 帶 Model Head 的模型調用
- 3. 下游任務訓練
1. Transformer Model
1.1 基本架構
- Transformer model 代表了以 Transformer 為基礎的一系列模型
- 原始的 Transformer 是 Encoder-Decoder 模型,用于自然語言翻譯任務。其 Encoder 部分接受原始序列輸入并構建其完整的特征表示,Decoder 部分基于 Encoder 提供的特征和當前已經翻譯的部分結果,自回歸地生成目標序列(翻譯結果)。 無論 Encoder 還是 Decoder,均由多個 Transformer Block 堆疊而成,每個 Transformer Block 由 Attention Layer 和 FFD Layer 組成
- 由于 Transformer Encoder 具有序列特征提取能力,Transformer Decoder 具有自回歸序列生成能力,兩者之后都被獨立使用,Encoder-Only 衍生出屬于自編碼器的 BERT 類模型,Decoder-Only 衍生出屬于自回歸生成模型的 GPT 類模型
- 原始的 Transformer 是 Encoder-Decoder 模型,用于自然語言翻譯任務。其 Encoder 部分接受原始序列輸入并構建其完整的特征表示,Decoder 部分基于 Encoder 提供的特征和當前已經翻譯的部分結果,自回歸地生成目標序列(翻譯結果)。 無論 Encoder 還是 Decoder,均由多個 Transformer Block 堆疊而成,每個 Transformer Block 由 Attention Layer 和 FFD Layer 組成
- Attention 機制
- Attention 機制是 Transformer 類模型的一個核心特征,在計算當前 token 的特征表示時,可以通過注意力機制有選擇性地告訴模型應該使用哪部分上下文
- Encoder-Decoder / Encoder-Only / Decoder-Only 三類模型,可以歸結為 attention mask 設置的不同,詳見 1.2 節
1.2 模型類型
-
目前主流的 Transformer 類模型可分為以下四類
- 自編碼模型:
Encoder-Only
結構,擁有雙向的注意力機制,即計算每一個詞的特征時都看到完整上下文 - 自回歸模型:
Decoder-Only / Causal Decoder
結構,擁有單向的注意力機制,即計算每一個詞的特征時都只能看到上文,無法看到下文: - 序列到序列模型:
Encoder-Decoder
結構,Encoder部分使用雙向的注意力,Decoder部分使用單向注意力 - 前綴模型:
Prefix-Decoder
結構,它對輸入序列的前綴部分使用雙向注意力機制,后半部分使用單向注意力機制,前綴片段內部的所有 token 都能看到完整上下文,其他部分只能看到前文。這可以看作是 Encoder-Decoder 的一個變體
- 自編碼模型:
-
以上 3 的結構示意圖已經在 1.1 節給出,它的 Encoder-Decoder 使用兩個獨立的 Transformer 結構,其中通過 cross attention 機制連接,1/2/4 都只使用一個 Transformer 結構,區別僅在于 attention mask 施加不同,使得序列中各個 token 能觀測到的上下文區域有所區別,如下所示
- Prefix Decoder 和 Encoder-Decoder 的主要區別在于:前者對編碼部分的 attention 是在每一層 Transformer Block 內部施加的,即第任意一層 Block 中的解碼部分片段可以關注到該層的前綴片段;后者則是 Decoder 中每層 Block 都能只能關注到 Encoder 最后一層的編碼片段結果
- Prefix Decoder 和 Decoder-Only 非常類似,它們能執行的任務類型也差不多,下圖更清晰地指示了二者的不同
-
不同的模型結構適用不同的預訓練方法,主要有以下幾種
FLM (full language modeling)
:就是訓練標準的語言模型,完整一段話從頭到尾基于上文預測下一個token。適用于 Decoder-Only 模型PLM (prefix language modeling)
:一段話分成兩截,前一截作為輸入,預測后一截。適用于 Encoder-Decoder 模型和 Prefix Decoder 模型MLM (masked language modeling)
:遮蓋住文本中的一部分token,讓模型通過上下文猜測遮蓋部分的token。適用于 Encoder-Only 模型- 將任務改造成 text-to-text 形式(即 input 和 target 都是一段文本),可以適配 Encoder-Decoder 和 Prefix Decoder
- 將 input 和 target 拼接起來,可以適配 Decoder-Only
-
總結一下各類結構的經典模型和主要適用任務
模型類型 預訓練目標 常用預訓練模型 主要適用任務 Encoder-only MLM ALBERT,BERT,DistilBERT,RoBERTa 文本分類、命名實體識別、閱讀理解 Decoder-only FLM GPT,GPT-2,Bloom,LLaMA 文本生成 Encoder-Decoder PLM BART,T5,Marian,mBART 文本摘要、機器翻譯 Prefix-Decoder PLM ChatGLM、ChatGLM2、U-PaLM 文本摘要、機器翻譯、文本生成 注意這里的適用任務并不絕對,比如 Decoder-only 配合指令微調,在參數規模大了之后其實什么都能做;用 MLM 目標預訓練的模型,經過 PLM 或 FLM 繼續訓練后,也能做翻譯和生成等任務,反之亦然。可以參考論文 What Language Model Architecture and Pretraining Objective Works Best for Zero-Shot Generalization?
-
額外提一句,當前最流行的模型結構是 Decoder-only,其中可能包含多方面原因,可以參考 【大模型慢學】GPT起源以及GPT系列采用Decoder-only架構的原因探討
1.3 Model Head
-
很多 NLP 任務是可以用相同的 model 骨干完成的,在 Transformers 庫的設計上,一個相同的模型骨干可以對應多個不同的任務,它們的區別僅在于最后的 model head 有所不同
比如對于 “句子情感分類” 和 “句子自回歸生成” 兩個任務,前者可以看作是基于前驅序列特征做二分類任務(正面情感/負面情感),后者可以看作是基于前驅序列特征做多分類任務(從詞表中選擇一個token索引),兩個任務中 “前驅序列特征” 都是可以用 GPT 模型提取的,它們的區別僅在于前者最后接二分類頭,后者最后接多分類頭
- Model Head 是連接在模型后的層,通常為1個或多個全連接層
- Model Head 將模型的編碼的表示結果進行映射,以解決不同類型的任務
以 BERT 模型情感二分類任務為例,設模型輸入長度 128,嵌入維度 768,則 Hidden states 尺寸 1x128x768。這時 Head 可能是一個輸入尺寸為 768,輸出尺寸為 2 的 MLP,最后一層 Hidden states 中 [CLS] 特殊 token 位置的 768 維向量將會輸入 Head,對 Head 輸出計算交叉熵損失來訓練模型
-
Transformer 庫中,模型類對象使用的 Model Head 可以從其類名后綴中觀察出來
- *Model(模型本身,只返回編碼結果)
- *ForCausalLM
- *ForMaskedLM
- *ForSeq2SeqLM
- *ForMultipleChoice
- *ForQuestionAnswering
- *ForSequenceClassification
- *ForTokenClassification
- …
2. Transformer 庫 Model 組件的基本使用
2.1 模型加載
-
使用
AutoModel
類,可以用from_pretrained
方法直接從模型地址下載模型和權重檢查點,并返回 model 對象。這類似前文介紹過的AutoTokenizer
類。這里我們加載一個小規模的中文情感分類模型from transformers import AutoConfig, AutoModel, AutoTokenizer # 在線加載 # 若下載失敗,也可以在倉庫 https://huggingface.co/hfl/rbt3/tree/main 手動下載,然后在from_pretrained方法中傳入本地文件夾加載 model = AutoModel.from_pretrained("hfl/rbt3") model
BertModel((embeddings): BertEmbeddings((word_embeddings): Embedding(21128, 768, padding_idx=0)(position_embeddings): Embedding(512, 768)(token_type_embeddings): Embedding(2, 768)(LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)(dropout): Dropout(p=0.1, inplace=False))(encoder): BertEncoder((layer): ModuleList((0): BertLayer((attention): BertAttention((self): BertSelfAttention((query): Linear(in_features=768, out_features=768, bias=True)(key): Linear(in_features=768, out_features=768, bias=True)(value): Linear(in_features=768, out_features=768, bias=True)(dropout): Dropout(p=0.1, inplace=False))(output): BertSelfOutput((dense): Linear(in_features=768, out_features=768, bias=True)(LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)(dropout): Dropout(p=0.1, inplace=False)))(intermediate): BertIntermediate( ...(pooler): BertPooler((dense): Linear(in_features=768, out_features=768, bias=True)(activation): Tanh()) )
可以看到這是一個
BertModel
-
可以通過
model.config
訪問該模型的參數# 查看模型配置參數 model.config
BertConfig {"_name_or_path": "hfl/rbt3","architectures": ["BertForMaskedLM"],"attention_probs_dropout_prob": 0.1,"classifier_dropout": null,"directionality": "bidi","hidden_act": "gelu","hidden_dropout_prob": 0.1,"hidden_size": 768,"initializer_range": 0.02,"intermediate_size": 3072,"layer_norm_eps": 1e-12,"max_position_embeddings": 512,"model_type": "bert","num_attention_heads": 12,"num_hidden_layers": 3,"output_past": true,"pad_token_id": 0,"pooler_fc_size": 768,"pooler_num_attention_heads": 12,"pooler_num_fc_layers": 3,"pooler_size_per_head": 128,"pooler_type": "first_token_transform", ..."transformers_version": "4.41.2","type_vocab_size": 2,"use_cache": true,"vocab_size": 21128 }
可見,Bert 類模型的參數使用一個
BertConfig
類對象管理,查看其源碼定義,可以看到參數的解釋class BertConfig(PretrainedConfig):r"""This is the configuration class to store the configuration of a [`BertModel`] or a [`TFBertModel`]. It is used toinstantiate a BERT model according to the specified arguments, defining the model architecture. Instantiating aconfiguration with the defaults will yield a similar configuration to that of the BERT[google-bert/bert-base-uncased](https://huggingface.co/google-bert/bert-base-uncased) architecture.Configuration objects inherit from [`PretrainedConfig`] and can be used to control the model outputs. Read thedocumentation from [`PretrainedConfig`] for more information.Args:vocab_size (`int`, *optional*, defaults to 30522):Vocabulary size of the BERT model. Defines the number of different tokens that can be represented by the`inputs_ids` passed when calling [`BertModel`] or [`TFBertModel`].hidden_size (`int`, *optional*, defaults to 768):Dimensionality of the encoder layers and the pooler layer.num_hidden_layers (`int`, *optional*, defaults to 12):Number of hidden layers in the Transformer encoder.num_attention_heads (`int`, *optional*, defaults to 12):Number of attention heads for each attention layer in the Transformer encoder.intermediate_size (`int`, *optional*, defaults to 3072):Dimensionality of the "intermediate" (often named feed-forward) layer in the Transformer encoder.hidden_act (`str` or `Callable`, *optional*, defaults to `"gelu"`):The non-linear activation function (function or string) in the encoder and pooler. If string, `"gelu"`,`"relu"`, `"silu"` and `"gelu_new"` are supported.hidden_dropout_prob (`float`, *optional*, defaults to 0.1):The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.attention_probs_dropout_prob (`float`, *optional*, defaults to 0.1):The dropout ratio for the attention probabilities.max_position_embeddings (`int`, *optional*, defaults to 512):The maximum sequence length that this model might ever be used with. Typically set this to something largejust in case (e.g., 512 or 1024 or 2048).type_vocab_size (`int`, *optional*, defaults to 2):The vocabulary size of the `token_type_ids` passed when calling [`BertModel`] or [`TFBertModel`].initializer_range (`float`, *optional*, defaults to 0.02):The standard deviation of the truncated_normal_initializer for initializing all weight matrices.layer_norm_eps (`float`, *optional*, defaults to 1e-12):The epsilon used by the layer normalization layers.position_embedding_type (`str`, *optional*, defaults to `"absolute"`):Type of position embedding. Choose one of `"absolute"`, `"relative_key"`, `"relative_key_query"`. Forpositional embeddings use `"absolute"`. For more information on `"relative_key"`, please refer to[Self-Attention with Relative Position Representations (Shaw et al.)](https://arxiv.org/abs/1803.02155).For more information on `"relative_key_query"`, please refer to *Method 4* in [Improve Transformer Modelswith Better Relative Position Embeddings (Huang et al.)](https://arxiv.org/abs/2009.13658).is_decoder (`bool`, *optional*, defaults to `False`):Whether the model is used as a decoder or not. If `False`, the model is used as an encoder.use_cache (`bool`, *optional*, defaults to `True`):Whether or not the model should return the last key/values attentions (not used by all models). Onlyrelevant if `config.is_decoder=True`.classifier_dropout (`float`, *optional*):The dropout ratio for the classification head.Examples:```python>>> from transformers import BertConfig, BertModel>>> # Initializing a BERT google-bert/bert-base-uncased style configuration>>> configuration = BertConfig()>>> # Initializing a model (with random weights) from the google-bert/bert-base-uncased style configuration>>> model = BertModel(configuration)>>> # Accessing the model configuration>>> configuration = model.config```"""model_type = "bert"def __init__()...
-
注意到 BertConfig 類繼承自
PretrainedConfig
,這意味著之前從model.config
打印的參數并不完整,進一步查看 PretrainedConfig 類的源碼,可以看到模型使用的所有參數。了解模型使用的全部參數是重要的,因為我們修改模型時主要就是從修改參數入手
2.2 模型調用
2.2.1 不帶 Model Head 的模型調用
-
像 2.1 節那樣加載,得到的 model 是不帶 model head 的,這一點可以從打印從模型結構中看出,它以一個
BertPooler
塊結尾... (pooler): BertPooler((dense): Linear(in_features=768, out_features=768, bias=True)(activation): Tanh()) ...
可見輸出特征還是 768 維,這意味著沒有接調整到目標維度的 model head。當我們想把預訓練的模型作為序列特征提取器時,這種裸模型是有用的,可以通過加載模型時傳入參數
output_attentions=True
來獲得模型所有層的 attention 張量# 構造測試輸入 sen = "弱小的我也有大夢想!" tokenizer = AutoTokenizer.from_pretrained("hfl/rbt3") # 加載 tokenizer inputs = tokenizer(sen, return_tensors="pt") # return_tensors="pt" 要求返回 tensor 張量# 不帶 model head 的模型調用 model = AutoModel.from_pretrained("hfl/rbt3", output_attentions=True) # 要求輸出 attention 張量 output = model(**inputs)print(output.keys()) # odict_keys(['last_hidden_state', 'pooler_output', 'attentions']) assert output.last_hidden_state.shape[1] == len(inputs['input_ids'][0]) # 輸出尺寸和輸入尺寸相同
查看最后一層 hidden state
# 不帶 model head 做下游任務時,通常我們是需要 model 提取的特征,即最后一層的 last_hidden_state output.last_hidden_state # torch.Size([1, 12, 768])
tensor([[[ 0.6804, 0.6664, 0.7170, ..., -0.4102, 0.7839, -0.0262],[-0.7378, -0.2748, 0.5034, ..., -0.1359, -0.4331, -0.5874],[-0.0212, 0.5642, 0.1032, ..., -0.3617, 0.4646, -0.4747],...,[ 0.0853, 0.6679, -0.1757, ..., -0.0942, 0.4664, 0.2925],[ 0.3336, 0.3224, -0.3355, ..., -0.3262, 0.2532, -0.2507],[ 0.6761, 0.6688, 0.7154, ..., -0.4083, 0.7824, -0.0224]]],grad_fn=<NativeLayerNormBackward0>)
2.2.2 帶 Model Head 的模型調用
-
使用帶有 1.3 節所述 model head 類名后綴的模型類加載模型,即可得到帶 head 的模型
from transformers import AutoModelForSequenceClassificationclz_model = AutoModelForSequenceClassification.from_pretrained("hfl/rbt3") # 加載帶多分類頭的模型 clz_model # 注意模型結構最后多了 (classifier): Linear(in_features=768, out_features=2, bias=True)
Some weights of BertForSequenceClassification were not initialized from the model checkpoint at hfl/rbt3 and are newly initialized: ['classifier.bias', 'classifier.weight'] You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference. BertForSequenceClassification((bert): BertModel((embeddings): BertEmbeddings((word_embeddings): Embedding(21128, 768, padding_idx=0)(position_embeddings): Embedding(512, 768)(token_type_embeddings): Embedding(2, 768)(LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)(dropout): Dropout(p=0.1, inplace=False))(encoder): BertEncoder((layer): ModuleList((0): BertLayer((attention): BertAttention((self): BertSelfAttention((query): Linear(in_features=768, out_features=768, bias=True)(key): Linear(in_features=768, out_features=768, bias=True)(value): Linear(in_features=768, out_features=768, bias=True)(dropout): Dropout(p=0.1, inplace=False))(output): BertSelfOutput((dense): Linear(in_features=768, out_features=768, bias=True)(LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)(dropout): Dropout(p=0.1, inplace=False))) ...))(dropout): Dropout(p=0.1, inplace=False)(classifier): Linear(in_features=768, out_features=2, bias=True) )
注意模型現在變成了一個
BertForSequenceClassification
對象,其結構最后多了一個由dropout
和classifier
線性層組成的 head,而且這里提示我們Some weights of BertForSequenceClassification were not initialized...
,說明這個線性層的參數 ckpt 中沒有提供,需要我們針對下游任務特別訓練 -
注意到分類頭默認輸出維度(類別數為2),這個可以通過參數
num_labels
控制,從模型類BertForSequenceClassification
定義進去檢查。下面修改 model head 的輸出維度看看# 分類頭默認輸出維度(類別數為2),可以通過參數 num_labels 控制 from transformers import AutoModelForSequenceClassification, BertForSequenceClassificationclz_model = AutoModelForSequenceClassification.from_pretrained("hfl/rbt3", num_labels=10) # 指定10個類 clz_model # 注意模型結構最后多了 (classifier): Linear(in_features=768, out_features=10, bias=True)
Some weights of BertForSequenceClassification were not initialized from the model checkpoint at hfl/rbt3 and are newly initialized: ['classifier.bias', 'classifier.weight'] You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference. BertForSequenceClassification((bert): BertModel((embeddings): BertEmbeddings((word_embeddings): Embedding(21128, 768, padding_idx=0)(position_embeddings): Embedding(512, 768)(token_type_embeddings): Embedding(2, 768)(LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)(dropout): Dropout(p=0.1, inplace=False))(encoder): BertEncoder((layer): ModuleList((0): BertLayer((attention): BertAttention((self): BertSelfAttention((query): Linear(in_features=768, out_features=768, bias=True)(key): Linear(in_features=768, out_features=768, bias=True)(value): Linear(in_features=768, out_features=768, bias=True)(dropout): Dropout(p=0.1, inplace=False))(output): BertSelfOutput((dense): Linear(in_features=768, out_features=768, bias=True)(LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)(dropout): Dropout(p=0.1, inplace=False))) ...))(dropout): Dropout(p=0.1, inplace=False)(classifier): Linear(in_features=768, out_features=10, bias=True) )
-
使用以上模型做前向傳播試試
clz_model(**inputs)
SequenceClassifierOutput(loss=None, logits=tensor([[ 0.1448, 0.1539, -0.1112, 0.1182, 0.2485, 0.4370, 0.3614, 0.5981,0.5442, -0.2900]], grad_fn=<AddmmBackward0>), hidden_states=None, attentions=None)
可見輸出結構中存在一個
loss
成員,說明前向過程中就有計算 loss 的結構了,不妨看一下BertForSequenceClassification
類的定義class BertForSequenceClassification(BertPreTrainedModel):def __init__(self, config):super().__init__(config)self.num_labels = config.num_labelsself.config = configself.bert = BertModel(config)classifier_dropout = (config.classifier_dropout if config.classifier_dropout is not None else config.hidden_dropout_prob)self.dropout = nn.Dropout(classifier_dropout)self.classifier = nn.Linear(config.hidden_size, config.num_labels)# Initialize weights and apply final processingself.post_init()@add_start_docstrings_to_model_forward(BERT_INPUTS_DOCSTRING.format("batch_size, sequence_length"))@add_code_sample_docstrings(checkpoint=_CHECKPOINT_FOR_SEQUENCE_CLASSIFICATION,output_type=SequenceClassifierOutput,config_class=_CONFIG_FOR_DOC,expected_output=_SEQ_CLASS_EXPECTED_OUTPUT,expected_loss=_SEQ_CLASS_EXPECTED_LOSS,)def forward(self,input_ids: Optional[torch.Tensor] = None,attention_mask: Optional[torch.Tensor] = None,token_type_ids: Optional[torch.Tensor] = None,position_ids: Optional[torch.Tensor] = None,head_mask: Optional[torch.Tensor] = None,inputs_embeds: Optional[torch.Tensor] = None,labels: Optional[torch.Tensor] = None,output_attentions: Optional[bool] = None,output_hidden_states: Optional[bool] = None,return_dict: Optional[bool] = None,) -> Union[Tuple[torch.Tensor], SequenceClassifierOutput]:r"""labels (`torch.LongTensor` of shape `(batch_size,)`, *optional*):Labels for computing the sequence classification/regression loss. Indices should be in `[0, ...,config.num_labels - 1]`. If `config.num_labels == 1` a regression loss is computed (Mean-Square loss), If`config.num_labels > 1` a classification loss is computed (Cross-Entropy)."""return_dict = return_dict if return_dict is not None else self.config.use_return_dictoutputs = self.bert(input_ids,attention_mask=attention_mask,token_type_ids=token_type_ids,position_ids=position_ids,head_mask=head_mask,inputs_embeds=inputs_embeds,output_attentions=output_attentions,output_hidden_states=output_hidden_states,return_dict=return_dict,)pooled_output = outputs[1]pooled_output = self.dropout(pooled_output)logits = self.classifier(pooled_output)loss = Noneif labels is not None:if self.config.problem_type is None:if self.num_labels == 1:self.config.problem_type = "regression"elif self.num_labels > 1 and (labels.dtype == torch.long or labels.dtype == torch.int):self.config.problem_type = "single_label_classification"else:self.config.problem_type = "multi_label_classification"if self.config.problem_type == "regression":loss_fct = MSELoss()if self.num_labels == 1:loss = loss_fct(logits.squeeze(), labels.squeeze())else:loss = loss_fct(logits, labels)elif self.config.problem_type == "single_label_classification":loss_fct = CrossEntropyLoss()loss = loss_fct(logits.view(-1, self.num_labels), labels.view(-1))elif self.config.problem_type == "multi_label_classification":loss_fct = BCEWithLogitsLoss()loss = loss_fct(logits, labels)if not return_dict:output = (logits,) + outputs[2:]return ((loss,) + output) if loss is not None else outputreturn SequenceClassifierOutput(loss=loss,logits=logits,hidden_states=outputs.hidden_states,attentions=outputs.attentions,)
從 forward 方法中可見,如果傳入了 labels 參數,則會進一步根據輸出尺寸
num_labels
自動識別任務類型,并使用相應的損失函數計算 loss 作為返回的一部分
3. 下游任務訓練
- 在 2.2.2 節,我們構造了一個
BertForSequenceClassification
模型,它的 Bert 骨干加載了預訓練的 ckpt 權重,而分類頭權重是隨機初始化的。本節我們使用 ChnSentiCorp_htl_all數據集對它做下游任務訓練,該數據集由 7000 多條酒店評論數據,包括 5000 多條正向評論,2000 多條負向評論,用這些數據繼續訓練,可以得到一個文本情感分類模型。由于模型中絕大部分參數都有良好的初始權重,且模型規模很小,訓練成本并不高 - 我們這里不使用 Transformers 庫的 pipeline、evaluate、trainer 和 dataset,盡量手動實現全部代碼,細節請參考注釋
import os import sys base_path = os.path.abspath(os.path.join(os.path.dirname(__file__))) sys.path.append(base_path)from transformers import AutoTokenizer, AutoModelForSequenceClassification import pandas as pd import torch from torch.utils.data import Dataset, DataLoader, random_split from torch.optim import Adamclass MyDataset(Dataset):def __init__(self) -> None:super().__init__()self.data = pd.read_csv(f"{base_path}/ChnSentiCorp_htl_all.csv") # 加載原始數據self.data = self.data.dropna() # 去掉 nan 值def __getitem__(self, index):text:str = self.data.iloc[index]["review"]label:int = self.data.iloc[index]["label"]return text, labeldef __len__(self):return len(self.data)def collate_func(batch):# 對 dataloader 得到的 batch data 進行后處理# batch data 是一個 list,其中每個元素是 (sample, label) 形式的元組texts, labels = [], []for item in batch:texts.append(item[0])labels.append(item[1])# 對原始 texts 列表進行批量 tokenize,通過填充或截斷保持 token 長度為 128,要求返回的每個字段都是 pytorch tensorglobal tokenizerinputs = tokenizer(texts, max_length=128, padding="max_length", truncation=True, return_tensors="pt")# 增加 label 字段,這樣之后模型前向傳播時可以直接計算 lossinputs["labels"] = torch.tensor(labels)return inputsdef evaluate(model):model.eval()acc_num = 0with torch.inference_mode():for batch in validloader:if torch.cuda.is_available():batch = {k: v.cuda() for k, v in batch.items()}output = model(**batch)pred = torch.argmax(output.logits, dim=-1)acc_num += (pred.long() == batch["labels"].long()).float().sum()return acc_num / len(validset)def train(model, optimizer, epoch=3, log_step=100):global_step = 0for ep in range(epoch):model.train()for batch in trainloader:if torch.cuda.is_available():batch = {k: v.cuda() for k, v in batch.items()}optimizer.zero_grad()output = model(**batch) # batch 是一個字典,其中包含 model forward 方法所需的字段,每個字段 value 是 batch tensoroutput.loss.backward() # batch 字典中包含 labels 時會計算損失,詳見源碼optimizer.step()if global_step % log_step == 0:print(f"ep: {ep}, global_step: {global_step}, loss: {output.loss.item()}")global_step += 1acc = evaluate(model)print(f"ep: {ep}, acc: {acc}")if __name__ == "__main__":# 構造訓練集/測試集以及對應的 Dataloaderdataset = MyDataset()train_size = int(0.9*len(dataset))vaild_size = len(dataset) - train_sizetrainset, validset = random_split(dataset, lengths=[train_size, vaild_size])trainloader = DataLoader(trainset, batch_size=32, shuffle=True, collate_fn=collate_func)validloader = DataLoader(validset, batch_size=64, shuffle=False, collate_fn=collate_func)# 構造 tokenizer、model 和 optimizertokenizer = AutoTokenizer.from_pretrained("hfl/rbt3")model = AutoModelForSequenceClassification.from_pretrained("hfl/rbt3") # 從 AutoModelForSequenceClassification 加載標準初始化模型,從 AutoModel.from_pretrained("hfl/rbt3") 加載 ckpt 權重模型if torch.cuda.is_available():model = model.cuda()optimizer = Adam(model.parameters(), lr=2e-5)# 訓練train(model, optimizer)# 測試sen = "我覺得這家酒店不錯,飯很好吃!"id2_label = {0: "差評!", 1: "好評!"}model.eval()with torch.inference_mode():inputs = tokenizer(sen, return_tensors="pt")inputs = {k: v.cuda() for k, v in inputs.items()}logits = model(**inputs).logitspred = torch.argmax(logits, dim=-1)print(f"輸入:{sen}\n模型預測結果:{id2_label.get(pred.item())}")
Some weights of BertForSequenceClassification were not initialized from the model checkpoint at hfl/rbt3 and are newly initialized: ['classifier.bias', 'classifier.weight'] You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference. ep: 0, global_step: 0, loss: 0.6289803385734558 ep: 0, global_step: 200, loss: 0.17686372995376587 ep: 0, acc: 0.8944659233093262 ep: 1, global_step: 300, loss: 0.18355882167816162 ep: 1, global_step: 400, loss: 0.27272453904151917 ep: 1, acc: 0.8957529067993164 ep: 2, global_step: 500, loss: 0.18500971794128418 ep: 2, global_step: 600, loss: 0.08873294293880463 ep: 2, acc: 0.8918918967247009 輸入:我覺得這家酒店不錯,飯很好吃! 模型預測結果:好評!