原文链接
本篇文章使用 Hugging Face Transformers Fine-Tune BERT,获得一个能够对 Arxiv 文章摘要进行分类的模型,一共 11 个类别。
本篇文章包含以下内容:
安装依赖
解释用来 Fine-Tune BERT 的数据集
模型和训练
模型测试和评估
本文大部分用到的 api 在 transformer
的官方文档 都有,这里仅转载一下主要的处理流程。
如果实践中数据集是没有 label 的,则需要使用无监督的方法,比如 Masked Language Modeling(MLM) 以及 Next Sentence Prediction(NSP)。
为了构造 MLM 和 NSP 的数据,可以使用 transformers 库中的 DataCollatorForLanguageModeling
和 DataCollatorForNextSentencePrediction
。
Why Fine-Tuning BERT Matters? Fine-Tuning 的目的是为了让 BERT 从一个通用的工具变成一个特定任务的工具。
本文章使用 Hugging Face Transformers,一个用于自然语言处理的库。
Fine-Tuning BERT on the Arxiv Abstract Classification Dataset 首先,我们需要安装一些依赖。
1 2 3 !pip install -U transformers datasets evaluate accelerate !pip install scikit-learn !pip install tensorboard
Importing the Necessary Libraries showLineNumbers 1 2 3 4 5 6 7 8 9 10 11 12 13 from datasets import load_datasetfrom transformers import ( AutoTokenizer, DataCollatorWithPadding, AutoModelForSequenceClassification, TrainingArguments, Trainer, pipeline, ) import evaluateimport globimport numpy as np
Setting the Hyperparameters for Fine-Tuning BERT showLineNumbers 1 2 3 4 5 6 BATCH_SIZE = 32 NUM_PROCS = 32 LR = 0.00005 EPOCHS = 5 MODEL = 'distilbert-base-uncased' OUT_DIR = 'arxiv_bert'
MODEL:表示我们要使用的模型,这里使用的是 bert-base-uncased
。这里的 uncased
表示该模型是在小写的文本上训练的。
OUT_DIR:表示我们训练好的模型的保存路径。
Download and Prepare the Dataset 使用 datasets
库下载数据集 arxiv 的分类数据集。
showLineNumbers 1 2 3 4 5 6 train_dataset = load_dataset("ccdv/arxiv-classification" , split='train' ) valid_dataset = load_dataset("ccdv/arxiv-classification" , split='validation' ) test_dataset = load_dataset("ccdv/arxiv-classification" , split='test' ) print (train_dataset)print (valid_dataset)print (test_dataset)
下载数据可能需要为 load_dataset 加一个 trust_remote_code=True 的参数。
filename 1 2 3 4 5 6 7 8 9 10 11 12 Dataset({ features: ['text ', 'label '], num_rows : 28388 }) Dataset ({ features : ['text ', 'label '], num_rows : 2500 }) Dataset ({ features : ['text ', 'label '], num_rows : 2500 })
查看一下 dataset 的格式,可以看到每个样本有两个字段,一个是 text,一个是 label。
filename 1 2 {'text' : 'Constrained Submodular Maximization ... and A.14.\n\n22\n\n\x0c' , 'label' : 8 }
定义两个字典,用来将 label 转换为对应的类别,以及将类别转换为对应的 label。
showLineNumbers 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 id2label = { 0 : "math.AC" , 1 : "cs.CV" , 2 : "cs.AI" , 3 : "cs.SY" , 4 : "math.GR" , 5 : "cs.CE" , 6 : "cs.PL" , 7 : "cs.IT" , 8 : "cs.DS" , 9 : "cs.NE" , 10 : "math.ST" } label2id = { "math.AC" : 0 , "cs.CV" : 1 , "cs.AI" : 2 , "cs.SY" : 3 , "math.GR" : 4 , "cs.CE" : 5 , "cs.PL" : 6 , "cs.IT" : 7 , "cs.DS" : 8 , "cs.NE" : 9 , "math.ST" : 10 }
Tokenizing the Dataset 使用 AutoTokenizer
实例化一个 tokenizer,并对不同的 dataset 进行 tokenization。
showLineNumbers 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 from functools import partialtokenizer = AutoTokenizer.from_pretrained(MODEL) def preprocess_function (tokenizer, examples ): return tokenizer( examples["text" ], truncation=True , ) partial_tokenize_function = partial(preprocess_function, tokenizer) tokenized_train = train_dataset.map ( partial_tokenize_function, batched=True , batch_size=BATCH_SIZE, num_proc=NUM_PROCS ) tokenized_valid = valid_dataset.map ( partial_tokenize_function, batched=True , batch_size=BATCH_SIZE, num_proc=NUM_PROCS ) tokenized_test = test_dataset.map ( partial_tokenize_function, batched=True , batch_size=BATCH_SIZE, num_proc=NUM_PROCS ) data_collator = DataCollatorWithPadding(tokenizer=tokenizer)
给 AutoTokenizer
传递的模型名能够初始化专门针对该模型的 tokenizer。
DataCollatorWithPadding
会在训练时动态地将不同长度的 token 转换为相同长度的 tensor。
Transformers 的 Tokenizer
支持 truncation 和 padding 参数,具体查看 transformers.PreTrainedTokenizer
tokenizer 和 DataCollatorWithPadding 的 padding 之间的区别:
tokenizer 的 padding 是在 tokenization 时使用,用来将不同长度的文本转换为相同长度的 token。
DataCollatorWithPadding 的 padding 是在 batch 生成时动态地来将不同长度的 token 转换为相同长度的 tensor。
DataCollatorWithPadding 传入 tokenizer 需要利用 tokenzier 的信息进行正确地 padding
使用原文中的代码我遇到了一个 Tokenizer is not defined 的报错,按照 这里 的解决方法,用 partial 函数解决了这个问题。
Sample Tokenization Example 1 2 3 4 tokenized_sample = partial_tokenize_function(train_dataset[0 ]) print (tokenized_sample)print (f"Length of tokenized IDs: {len (tokenized_sample.input_ids)} " )print (f"Length of attention mask: {len (tokenized_sample.attention_mask)} " )
filename 1 2 3 4 5 {'input_ids' : [101 , 14924 , 2635 , 24421 , 13997. .. 102 ], 'token_type_ids' : [0 , 0 , 0 , 0 , ... 0 ],'attention_mask' : [1 , 1 , 1 , ... 1 ]}Length of tokenized IDs: 512 Length of attention mask: 512
input_ids:表示 tokenized 后的文本,每个 token 都有一个对应的 id。例如,101 表示 [CLS],表示序列的开始;102 表示 [SEP],表示序列的结束。
token_type_ids:表示 token 的类型,BERT 用于区分两个句子的 token。
attention_mask:表示哪些 token 是 padding 的,哪些是真实的 token。
Evaluation Metrics 定义一个函数,用来计算模型的评估指标。
showLineNumbers 1 2 3 4 5 accuracy = evaluate.load('accuracy' ) def compute_metrics (eval_pred ): predictions, labels = eval_pred predictions = np.argmax(predictions, axis=1 ) return accuracy.compute(predictions=predictions, references=labels)
Preparing the BERT Model 实例化一个 11 分类的 BERT 模型,最后的 BERT 模型有 0.67 亿参数。
showLineNumbers 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 model = AutoModelForSequenceClassification.from_pretrained( MODEL, num_labels=11 , id2label=id2label, label2id=label2id, ) model.to('cuda' ) total_params = sum (p.numel() for p in model.parameters()) trainable_params = sum (p.numel() for p in model.parameters() if p.requires_grad) print (f"\nTotal parameters: {total_params} " )print (f"Trainable parameters: {trainable_params} \n" )dummy_input = tokenizer("This is a dummy input" , return_tensors="pt" ) dummy_input_dict = {k: v.to('cuda' ) for k, v in dummy_input.items()} from torchinfo import summarysummary(model, input_data=dummy_input_dict)
num_labels:表示分类任务中的类别数。transformers 在加载预训练模型的权重时,会根据 num_labels 的值重新初始化模型的最后一个分类层,从而忽略与预训练模型不匹配的维度。这意味着当你指定 num_labels 后,模型的最后一个分类层会根据新的类别数进行初始化,而其他层的权重会从预训练模型中加载。
filename 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 Some weights of DistilBertForSequenceClassification were not initialized from the model checkpoint at distilbert-base-uncased and are newly initialized: ['classifier.bias' , 'classifier.weight' , 'pre_classifier.bias' , 'pre_classifier.weight' ] You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference. Total parameters: 66961931 Trainable parameters: 66961931 ========================================================================================================= Layer (type :depth-idx) Output Shape Param ========================================================================================================= DistilBertForSequenceClassification [1 , 11 ] -- ├─DistilBertModel: 1 -1 [1 , 7 , 768 ] -- │ └─Embeddings: 2 -1 [1 , 7 , 768 ] -- │ │ └─Embedding: 3 -1 [1 , 7 , 768 ] 23 ,440 ,896 │ │ └─Embedding: 3 -2 [1 , 7 , 768 ] 393 ,216 │ │ └─LayerNorm: 3 -3 [1 , 7 , 768 ] 1 ,536 │ │ └─Dropout: 3 -4 [1 , 7 , 768 ] -- │ └─Transformer: 2 -2 [1 , 7 , 768 ] -- │ │ └─ModuleList: 3 -5 -- 42 ,527 ,232 ├─Linear: 1 -2 [1 , 768 ] 590 ,592 ├─Dropout: 1 -3 [1 , 768 ] -- ├─Linear: 1 -4 [1 , 11 ] 8 ,459 ========================================================================================================= Total params: 66 ,961 ,931 Trainable params: 66 ,961 ,931 Non-trainable params: 0 Total mult-adds (M): 66.96 ========================================================================================================= Input size (MB): 0.00 Forward/backward pass size (MB): 2.97 Params size (MB): 267.85 Estimated Total Size (MB): 270.82 =========================================================================================================
Training Arguments 定义训练参数,都是一些比较常见的参数。
showLineNumbers 1 2 3 4 5 6 7 8 9 10 11 12 13 14 training_args = TrainingArguments( output_dir=OUT_DIR, learning_rate=LR, per_device_train_batch_size=BATCH_SIZE, per_device_eval_batch_size=BATCH_SIZE, num_train_epochs=EPOCHS, weight_decay=0.01 , eval_strategy="epoch" , save_strategy="epoch" , load_best_model_at_end=True , save_total_limit=3 , report_to='tensorboard' , fp16=True )
Initializing the Trainer 实例化一个 Trainer
,并开始训练。
showLineNumbers 1 2 3 4 5 6 7 8 9 10 11 trainer = Trainer( model=model, args=training_args, train_dataset=tokenized_train, eval_dataset=tokenized_valid, data_collator=data_collator, tokenizer=tokenizer, compute_metrics=compute_metrics ) history = trainer.train()
写的时候在自己电脑上跑的,跑太慢了,就没继续跑下去了,但是这个代码是可以正常运行的。
Evaluation && Inference on Unseen Data showLineNumbers 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 trainer.evaluate(tokenized_test) AutoModelForSequenceClassification.from_pretrained(f"arxiv_bert/checkpoint-4440" ) tokenizer = AutoTokenizer.from_pretrained('bert-base-uncased' ) classify = pipeline(task='text-classification' , model=model, tokenizer=tokenizer) all_files = glob.glob('inference_data/*' ) for file_name in all_files: file = open (file_name) content = file.read() print (content) result = classify(content) print ('PRED: ' , result) print ('GT: ' , file_name.split('_' )[-1 ].split('.txt' )[0 ]) print ('\n' )