Finetune BERT cho bài toán che thông tin cá nhân - Phần 2: Huấn luyện và đánh giá mô hình
Tổng quan
Phần 1 chúng ta đã cùng nhau xác định các bước để tạo ra một mô hình xử lý bài toán NER thông qua việc finetuning BERT
Quá trình finetune BERT cho bài toán masking thông tin cá nhân bao gồm các bước sau:
-
Chuẩn bị dữ liệu: Tùy thuộc vào mục tiêu bài toán chúng ta sẽ tập chung vào từng bộ dữ liệu cụ thể
-
Tiền xử lý dữ liệu: Xử lý dữ liệu văn bản để đưa vào mô hình BERT, bao gồm tokenization, padding, và chuyển đổi các thẻ nhãn thành số.
-
Finetune BERT: Sử dụng kiến trúc BERT pre-trained và tiếp tục training mô hình trên tập dữ liệu đã chuẩn bị. Trong quá trình này, mô hình sẽ học cách nhận dạng và che dấu các thông tin cá nhân.
-
Đánh giá và tối ưu hóa: Kiểm tra hiệu suất của mô hình trên tập dữ liệu test, và tiến hành các bước tối ưu hóa như điều chỉnh siêu tham số, thay đổi kiến trúc mô hình, hoặc thu thập thêm dữ liệu nếu cần thiết.
Vậy thì ở phần trước chúng ta đã xử lý xong 2 bước đầu tiên, trong phần 2 này mình sẽ tiếp tục với quá trình finetune BERT và đánh giá mô hình.
Huấn luyện mô hình
Chúng ta cần định nghĩa optimizer. Ở đây, đơn giản sử dụng Adam với learning rate mặc định. Người ta cũng có thể quyết định sử dụng những cái nâng cao hơn như AdamW (Adam với bản sửa lỗi suy giảm trọng số), SGD, hoặc một learning rate schedule. Tuy nhiên để đơn giản ở đây mình chỉnh sử dụng Adam()
optimizer = torch.optim.Adam(params=model.parameters(), lr=LEARNING_RATE)
Xây dựng training functions
# Defining the training function on the 80% of the dataset for tuning the bert model
def train(epoch):
tr_loss, tr_accuracy = 0, 0
nb_tr_examples, nb_tr_steps = 0, 0
tr_preds, tr_labels = [], []
# put model in training mode
model.train()
for idx, batch in enumerate(training_loader):
ids = batch['ids'].to(device, dtype = torch.long)
mask = batch['mask'].to(device, dtype = torch.long)
targets = batch['targets'].to(device, dtype = torch.long)
outputs = model(input_ids=ids, attention_mask=mask, labels=targets)
loss, tr_logits = outputs.loss, outputs.logits
tr_loss += loss.item()
nb_tr_steps += 1
nb_tr_examples += targets.size(0)
if idx % 100==0:
loss_step = tr_loss/nb_tr_steps
print(f"Training loss per 100 training steps: {loss_step}")
# compute training accuracy
flattened_targets = targets.view(-1) # shape (batch_size * seq_len,)
active_logits = tr_logits.view(-1, model.num_labels) # shape (batch_size * seq_len, num_labels)
flattened_predictions = torch.argmax(active_logits, axis=1) # shape (batch_size * seq_len,)
# now, use mask to determine where we should compare predictions with targets (includes [CLS] and [SEP] token predictions)
active_accuracy = mask.view(-1) == 1 # active accuracy is also of shape (batch_size * seq_len,)
targets = torch.masked_select(flattened_targets, active_accuracy)
predictions = torch.masked_select(flattened_predictions, active_accuracy)
tr_preds.extend(predictions)
tr_labels.extend(targets)
tmp_tr_accuracy = accuracy_score(targets.cpu().numpy(), predictions.cpu().numpy())
tr_accuracy += tmp_tr_accuracy
# gradient clipping
torch.nn.utils.clip_grad_norm_(
parameters=model.parameters(), max_norm=MAX_GRAD_NORM
)
# backward pass
optimizer.zero_grad()
loss.backward()
optimizer.step()
epoch_loss = tr_loss / nb_tr_steps
tr_accuracy = tr_accuracy / nb_tr_steps
print(f"Training loss epoch: {epoch_loss}")
print(f"Training accuracy epoch: {tr_accuracy}")
Vì là finetuning nên việc chúng ta huấn luyện quá nhiều epoch thì sẽ dẫn đến việc mô hình quá bị fit vào dữ liệu huấn luyện mà mất đi tính tổng quát của nó (pretrain model đã được huấn luyện trên 1 tập dữ liệu lớn hơn rất nhiều). Ở đây chúng ta chỉ cần huấn luyện thêm 1,2 epochs để mô hình học được những đặc trưng mới từ dữ liệu huấn luyện
for epoch in range(EPOCHS):
print(f"Training epoch: {epoch + 1}")
train(epoch)
Đánh giá mô hình
Sau khi chúng ta đã huấn luyện mô hình của mình, chúng ta có thể đánh giá hiệu suất của nó trên tập kiểm tra được giữ lại (chiếm 20% dữ liệu). Lưu ý rằng ở đây sẽ không cập nhật gradient , mô hình chỉ đơn giản đưa ra các logits của nó.
def valid(model, testing_loader):
# put model in evaluation mode
model.eval()
eval_loss, eval_accuracy = 0, 0
nb_eval_examples, nb_eval_steps = 0, 0
eval_preds, eval_labels = [], []
with torch.no_grad():
for idx, batch in enumerate(testing_loader):
ids = batch['ids'].to(device, dtype = torch.long)
mask = batch['mask'].to(device, dtype = torch.long)
targets = batch['targets'].to(device, dtype = torch.long)
outputs = model(input_ids=ids, attention_mask=mask, labels=targets)
loss, eval_logits = outputs.loss, outputs.logits
eval_loss += loss.item()
nb_eval_steps += 1
nb_eval_examples += targets.size(0)
if idx % 100==0:
loss_step = eval_loss/nb_eval_steps
print(f"Validation loss per 100 evaluation steps: {loss_step}")
# compute evaluation accuracy
flattened_targets = targets.view(-1) # shape (batch_size * seq_len,)
active_logits = eval_logits.view(-1, model.num_labels) # shape (batch_size * seq_len, num_labels)
flattened_predictions = torch.argmax(active_logits, axis=1) # shape (batch_size * seq_len,)
# now, use mask to determine where we should compare predictions with targets (includes [CLS] and [SEP] token predictions)
active_accuracy = mask.view(-1) == 1 # active accuracy is also of shape (batch_size * seq_len,)
targets = torch.masked_select(flattened_targets, active_accuracy)
predictions = torch.masked_select(flattened_predictions, active_accuracy)
eval_labels.extend(targets)
eval_preds.extend(predictions)
tmp_eval_accuracy = accuracy_score(targets.cpu().numpy(), predictions.cpu().numpy())
eval_accuracy += tmp_eval_accuracy
#print(eval_labels)
#print(eval_preds)
labels = [id2label[id.item()] for id in eval_labels]
predictions = [id2label[id.item()] for id in eval_preds]
#print(labels)
#print(predictions)
eval_loss = eval_loss / nb_eval_steps
eval_accuracy = eval_accuracy / nb_eval_steps
print(f"Validation Loss: {eval_loss}")
print(f"Validation Accuracy: {eval_accuracy}")
return labels, predictions
Chúng ta có thể dễ dàng tính được
Validation Loss: 0.02922834331467394
Validation Accuracy: 0.9602300367723403
Tuy nhiên, chỉ số "Validation Accuracy" là không đáng tin cậy, vì nhiều nhãn là "ngoài" (O), thậm chí sau khi loại bỏ các dự đoán trên các token [PAD]. Điều quan trọng là xem xét độ chính xác, giá trị f1 của từng thẻ riêng lẻ.
from seqeval.metrics import classification_report
labels, predictions = valid(model, testing_loader)
print(classification_report([labels], [predictions]))
precision recall f1-score support
geo 0.79 0.88 0.83 4613
gpe 0.89 0.89 0.89 1523
org 0.71 0.56 0.63 2761
per 0.78 0.81 0.79 2183
tim 0.82 0.81 0.81 1772
micro avg 0.79 0.79 0.79 12852
macro avg 0.80 0.79 0.79 12852
weighted avg 0.79 0.79 0.79 12852
Có một cánh nhanh để kiểm tra tính hiệu quả của mô hình, chúng ta có thể đưa vào một câu của tập test hoặc câu bất kỳ nào đó để kiểm tra
from transformers import pipeline
pipe = pipeline(task="token-classification", model=model.to("cpu"), tokenizer=tokenizer, aggregation_strategy="simple")
pipe("My name is Niels and New York is a city")
Kết quả khá ổn khi mô hình có thể bắt được các token được đánh nhãn là 'per' hoặc 'geo', chúng ta có thể sử đánh giá trên trên một tập dữ liệu lớn để kiểm tra tính hiệu quả và xem xét những điểm cần tối ưu.
[{'end': None,
'entity_group': 'per',
'score': 0.71306735,
'start': None,
'word': 'ni'},
{'end': None,
'entity_group': 'per',
'score': 0.8921481,
'start': None,
'word': '##els'},
{'end': None,
'entity_group': 'geo',
'score': 0.95278734,
'start': None,
'word': 'new york'}]
Lưu mô hình
Cuối cùng, hãy lưu các tệp mô hình và bộ mã hóa để chúng ta có thể dễ dàng sử dụng lại chúng sau này. Có 2 options:
- Chúng ta có thể lưu tất cả cục bộ, chỉ đơn giản bằng cách gọi model.save_pretrained() và tokenizer.save_pretrained(), cung cấp đường dẫn thư mục làm đối số.
- Cách khác là có thể đẩy các tệp lên HuggingFace hub. Bằng cách này, bạn có thể chia sẻ mô hình của mình với cộng đồng. Tất cả các tệp sẽ được theo dõi bởi git, vì mỗi mô hình trên hub có kho lưu trữ git riêng của nó. Cả hai tùy chọn đều cho phép tái sử dụng mô hình/bộ mã hóa bằng cách sử dụng phương thức from_pretrained()
Các bạn có thể sử dụng đoạn code dưới đây để thực hiện cách 2 nhé
from huggingface_hub import notebook_login
notebook_login()
model_name = "bert-finetuned-ner"
# upload files to the hub
tokenizer.push_to_hub(
repo_path_or_name=model_name,
organization="nielsr",
commit_message="Add tokenizer",
use_temp_dir=True,
)
model.push_to_hub(
repo_path_or_name=model_name,
organization="nielsr",
commit_message="Add model",
use_temp_dir=True,
)
Kết luận
Trong bài viết này, chúng ta đã thảo luận về cách Finetune mô hình BERT để giải quyết bài toán che thông tin cá nhân. Chúng ta đã xây dựng một pipeline từ xử lý dữ liệu, đào tạo và đánh giá mô hình, sau đó sử dụng mô hình sau khi training để thực hiện suy luận nhanh chóng trên các câu mới. Kết quả cho thấy việc Finetune BERT đạt hiệu suất ổn trên bộ dữ liệu này, với độ chính xác, F1 ở tương đối tốt. Điều này cho thấy BERT là một nền tảng mạnh mẽ có thể được điều chỉnh để giải quyết các bài toán NLP phức tạp như che thông tin cá nhân. Cũng như cách chia sẻ chúng trên HuggingFace hub để dễ dàng sử dụng lại hoặc chia sẽ với cộng đồng. Hy vọng rằng bài viết này đã cung cấp cho bạn một cái nhìn tổng quan về cách Finetune BERT cho bài toán che thông tin cá nhân. Hãy tiếp tục khám phá và sáng tạo với các kỹ thuật học máy và các bài toán thực tế nhiều hơn nữa để tạo thêm nhiều giá trị mới nhé.
All rights reserved