Hướng dẫn Fine-Tuning BERT với PyTorch

Bài viết này sẽ hướng dẫn bạn cách sử dụng BERT với thư viện PyTorch để fine-tuning (tinh chỉnh) mô hình một cách nhanh chóng và hiệu quả. Ngoài ra, bài viết sẽ chỉ cho bạn ứng dụng thực tế của transfer learning trong NLP để tạo ra các mô hình hiệu suất cao với tài nguyên và nỗ lực tối thiểu trên một loạt các nhiệm vụ NLP.

Giới thiệu

Trước khi đi vào chi tiết của bài hướng dẫn, BERT có một số thông tin chúng ta cần tìm hiểu.

Lịch sử

Năm 2018 là một năm đột phá trong NLP. Trong transfer learning, đặc biệt là các mô hình như ELMO của Allen AI, GPT của OpenAI và BERT của Google cho phép các nhà nghiên cứu phá vỡ các kỷ lục trên các bộ dữ liệu tiêu chuẩn với các tinh chỉnh. Chúng cũng cung cấp cộng đồng NLP những mô hình pre-trained với kết quả đáng kinh ngạc. Mặc dù vậy, đối với nhiều người mới bắt đầu với NLP và thậm chí đối với một số nhà thực hành có kinh nghiệm, lý thuyết và ứng dụng thực tế của các mô hình mạnh mẽ này vẫn chưa được hiểu rõ.

BERT là gì?

BERT (Bidirectional Encoder Representations from Transformers) được phát hành vào cuối năm 2018, là mô hình sẽ sử dụng trong bài viết này để cung cấp cho độc giả hiểu rõ hơn về việc sử dụng các mô hình transfer learning trong NLP. BERT là một phương pháp biểu diễn ngôn ngữ được huấn luyện từ trước được sử dụng để tạo ra các mô hình mà những người thực hành NLP sau đó có thể tải xuống và sử dụng miễn phí. Bạn có thể sử dụng các mô hình này để trích xuất các đặc trưng ngôn ngữ chất lượng cao từ dữ liệu văn bản của mình hoặc bạn có thể tinh chỉnh các mô hình này trên một tác vụ cụ thể (phân loại, nhận dạng thực thể, trả lời câu hỏi, v.v.) với dữ liệu của riêng bạn để có được kết quả tốt nhất.

Tại sao cần Fine-tuning?

Trong hướng dẫn này, chúng ta sẽ sử dụng BERT để huấn luyện chương trình phân loại văn bản. Cụ thể, chúng ta sẽ lấy mô hình BERT được huấn luyện từ trước, thêm một lớp nơ ron chưa được huấn luyện vào cuối và huấn luyện mô hình mới cho nhiệm vụ phân loại. Tại sao chúng ta làm điều này thay vì huấn luyện một mô hình deep learning cụ thể (CNN, BiLSTM, v.v.)?

Dễ huấn luyện

Thứ nhất, các trọng số của BERT được huấn luyện trước đã mã hóa rất nhiều thông tin về ngôn ngữ. Do đó, việc huấn luyện mô hình tinh chỉnh của chúng ta sẽ mất ít thời gian hơn – nó tương đương với việc chúng ta đã huấn luyện các lớp dưới cùng của mạng và chỉ cần điều chỉnh chúng một chút trong khi sử dụng đầu ra của chúng làm đặc trưng đầu vào cho nhiệm vụ phân loại. Trên thực tế, các tác giả chỉ khuyến nghị 2-4 epoch để tinh chỉnh BERT cho một nhiệm vụ NLP cụ thể (so với hàng trăm giờ GPU cần thiết để huấn luyện mô hình BERT hoặc LSTM từ đầu!).

Cần ít dữ liệu hơn

Ngoài ra và có lẽ cũng quan trọng, vì các trọng số được huấn luyện từ trước, phương pháp này cho phép chúng ta tinh chỉnh trên một tập dữ liệu nhỏ hơn nhiều so với yêu cầu của một mô hình được xây dựng từ đầu. Một nhược điểm lớn của các mô hình NLP được xây dựng từ đầu là chúng ta thường cần một bộ dữ liệu lớn một cách nghiêm ngặt để huấn luyện mạng của chúng ta đến độ chính xác chấp nhận được, có nghĩa là rất nhiều thời gian và nỗ lực dành cho việc tạo dữ liệu. Bằng cách tinh chỉnh BERT, giờ đây chúng ta có thể giải quyết được vấn đề thiếu dữ liệu.

Kết quả tốt

Quy trình tinh chỉnh đơn giản này (thường là thêm một lớp fully-connected được kết nối đầy đủ lên trên BERT và huấn luyện trong vài epoch) đã được chứng minh đạt được kết quả state-of-the-art trong nhiều nhiệm vụ của NLP như: phân loại, suy luận, trả lời câu hỏi, v.v. Thay vì cài đặt và huấn luyện từ đầu các kiến trúc mạng một cách tùy ý và đôi khi khó hiểu và chỉ hoạt động được trên một vài nhiệm vụ cụ thể, tinh chỉnh BERT có thể đem lại kết quả tốt tương đương hoặc nhỉnh hơn.

Một bước tiến trong NLP

Việc sử dụng transfer learning báo hiệu một bước tiến trong NLP tương tự như bước tiến đã xảy ra với thị giác máy. Thay vì huấn luyện một mạng mới từ đầu mỗi, các lớp thấp hơn của mạng được huấn luyện với các đặc trưng hình ảnh tổng quát có thể được sao chép và chuyển giao từ việc huấn luyện một mạng khác với một nhiệm vụ khác. Nó nhanh chóng trở thành thông lệ: Sử dụng một mạng pre-trained và huẩn luyện lại cho nhiệm vụ mới hoặc thêm các lớp bổ sung lên trên. Việc này giúp giảm thiểu thời gian và công sức một cách đáng kể. Đối với nhiều người, việc các mô hình ngôn ngữ pre-trained được giới thiệu vào năm 2018 (ELMO, BERT, ULMFIT, Open-GPT, v.v.) báo hiệu bước tiến tương tự trong NLP mà thị giác máy đã từng đạt được. Bài viết này sẽ hướng dẫn bạn sử dụng BERT, một trong những mô hình nổi tiếng nhật hiện này.

Cài đặt và import

Để có thể bắt đầu bài hướng dẫn với BERT, chúng ta cần có tài nguyên. Google Colab cung cấp GPU và TPU miễn phí! Vì chúng ta sẽ huấn luyện một mạng nơ ron lớn, nên ta sẽ cần tận dụng tối đa lợi thế này.

Có thể thêm GPU bằng cách vào menu và chọn:

Edit -> Notebook Settings -> Add accelerator (GPU)

Sau đó chạy cell dưới đây để xác nhận rằng GPU đã được nhận.

import tensorflow as tf

device_name = tf.test.gpu_device_name()
if device_name != '/device:GPU:0':
  raise SystemError('GPU device not found')
print('Found GPU at: {}'.format(device_name))
Found GPU at: /device:GPU:0

Tiếp theo, ta cài đặt giao diện pytorch cho BERT với Hugging Face. (Thư viện này chứa các giao diện cho các mô hình ngôn ngữ được pre-trained khác như GPT và GPT-2 của OpenAI.)

Hiện tại, thư viện Hugging Face có thể nói là giao diện pytorch mạnh mẽ và được chấp nhận rộng rãi nhất để làm việc với BERT. Ngoài việc hỗ trợ nhiều mô hình ngôn ngữ được huấn luyện sẵn khác nhau, thư viện cũng bao gồm các sửa đổi được dựng sẵn của BERT phù hợp với nhiệm vụ cụ thể. Ví dụ, trong hướng dẫn này về BERT, chúng ta sẽ sử dụng BertForSequenceClassification (BERT cho phân loại chuỗi), nhưng thư viện cũng bao gồm các sửa đổi BERT được thiết kế để phân loại token, trả lời câu hỏi, dự đoán câu tiếp theo, v.v. Sử dụng các lớp dựng sẵn này giúp đơn giản hóa quá trình sửa đổi BERT theo các mục đích khác nhau.

Lưu ý: Từ tháng 7 năm 2019, thư viện pytorch có tên là “pytorch-pretrained-bert pytorch-nlp” được cập nhật và đổi tên thành tên là “pytorch-transformers.” . Mặc dù tên cũ vẫn sử dụng được, có một tóm tắt ngắn ở đây về việc chuyển sang thư viện mới.

!pip install pytorch-pretrained-bert pytorch-nlp
import torch
from torch.utils.data import TensorDataset, DataLoader, RandomSampler, SequentialSampler
from keras.preprocessing.sequence import pad_sequences
from sklearn.model_selection import train_test_split
from pytorch_pretrained_bert import BertTokenizer, BertConfig
from pytorch_pretrained_bert import BertAdam, BertForSequenceClassification
from tqdm import tqdm, trange
import pandas as pd
import io
import numpy as np
import matplotlib.pyplot as plt
% matplotlib inline
Using TensorFlow backend.

Để đèn torch sử dụng được GPU, chúng ta cần xác định và chỉ định thiết bị là GPU. Sau đó, trong vòng lặp huấn luyện, chúng ta sẽ tải dữ liệu lên thiết bị (GPU).

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
n_gpu = torch.cuda.device_count()
torch.cuda.get_device_name(0)
'Tesla K80'

Nạp dataset

Chúng ta sẽ sử dụng bộ dữ liệu The Corpus of Linguistic Acceptability (CoLA) để phân loại câu đơn. Đây là một tập hợp các câu được dán nhãn đúng ngữ pháp hoặc sai ngữ pháp. Dữ liệu như sau:

Cột 1: mã đại diện cho nguồn gốc của câu.

Cột 2: nhãn của câu (0 = unacceptable, 1 = acceptable ).

Cột 3: nhãn của câu được ghi chú bởi tác giả.

Cột 4: nội dung của câu.

Chúng ta tải dataset tại liên kết sau đây: https://nyu-mll.github.io/CoLA/

Cả hai phiên bản thô và được tách từ của dữ liệu đều được. Chúng ta sẽ sử dụng dữ liệu thô vì chúng ta cần sử dụng bộ tách từ của BERT để chia văn bản thành các token và khối để mô hình có thể nhận ra được.

# Tải lên file huấn luyện từ máy của bạn
from google.colab import files
uploaded = files.upload()
Saving in_domain_train.tsv to in_domain_train.tsv
df = pd.read_csv("in_domain_train.tsv", delimiter='\t', header=None, names=['sentence_source', 'label', 'label_notes', 'sentence'])
df.shape
(8551, 4)
df.sample(10)
sentence_source label label_notes sentence
2229 l-93 1 NaN This machine records well.
1124 r-67 0 * Did that John showed up please you?
141 cj99 0 * It is important for the more you eat, the more…
2650 l-93 0 * Carla shoveled at the walk.
721 bc01 0 * Bill proud of himself John doesn’t consider.
7099 sgww85 1 NaN A policeman walked in at 11, and at 12, a fire…
1972 r-67 0 * The writers of any of the reports didn’t know …
1750 r-67 0 * Handsome though Dick is fair, Nordic, strong a…
6404 d_98 1 NaN The President thanked every soldier who had fo…
6590 g_81 1 NaN People are said to do crazier things at higher…
# Tạo danh sách câu và nhãn
sentences = df.sentence.values

# Chúng ta cần thêm các token đặc biệt ở đầu và cuối mỗi câu để BERT hoạt động chính xác
sentences = ["[CLS] " + sentence + " [SEP]" for sentence in sentences]
labels = df.label.values

Đầu vào

Tiếp theo, ta import bộ tách từ của BERT, được sử dụng để chuyển đổi văn bản thành các token tương ứng với bộ từ vựng của BERT.

tokenizer = BertTokenizer.from_pretrained('bert-base-uncased', do_lower_case=True)

tokenized_texts = [tokenizer.tokenize(sent) for sent in sentences]
print ("Tokenize the first sentence:")
print (tokenized_texts[0])
Tokenize the first sentence:
['[CLS]', 'our', 'friends', 'won', "'", 't', 'buy', 'this', 'analysis', ',', 'let', 'alone', 'the', 'next', 'one', 'we', 'propose', '.', '[SEP]']

BERT yêu cầu đầu vào được định dạng cụ thể. Đối với mỗi câu đầu vào được mã hóa, chúng ta cần tạo:

input ids: một chuỗi các số nguyên xác định từng token đầu vào với chỉ mục của nó trong bộ từ vựng của BERT
segment mask : (tùy chọn) một chuỗi 1 và 0 được sử dụng để xác định xem đầu vào là một câu hay hai câu dài. Đối với đầu vào là một câu, đây chỉ đơn giản là một chuỗi 0. Đối với hai câu đầu vào, có 0 cho mỗi token của câu đầu tiên, theo sau là 1 cho mỗi token của câu thứ hai
attention mask : (tùy chọn) một chuỗi 1 và 0, với 1 cho tất cả các token đầu vào và 0 cho tất cả các token đệm (sẽ được trình bày chi tiết này trong phần tiếp theo)
label: một giá trị duy nhất là 1 hoặc 0. Trong nhiệm vụ này, 1 có nghĩa là đúng ngữ pháp và 0 có nghĩa là sai ngữ pháp.

Mặc dù chúng ta có thể có các câu đầu vào có độ dài thay đổi, BERT yêu cầu các mảng đầu vào của chúng ta phải có cùng kích thước. Chúng ta giải quyết vấn đề này bằng cách chọn độ dài câu tối đa, sau đó đệm thêm và cắt bớt đầu vào cho đến khi mọi chuỗi đầu vào có cùng độ dài.

Đối với việc đệm thêm token, trong ngữ cảnh này nó có nghĩa là nếu một câu ngắn hơn độ dài câu tối đa, chúng ta chỉ cần thêm 0 vào cuối chuỗi cho đến khi đó đạt độ dài tối đa. Nếu một câu dài hơn độ dài câu tối đa, thì chúng ta chỉ cần cắt ngắn phần cuối của chuỗi, loại bỏ mọi token dôi ra so với độ dài tối đa.

Chúng ta đệm và cắt xén các chuỗi sao cho tất cả chúng đều có độ dài MAX_LEN. pad_ resultences là một hàm tiện ích mà chúng ta mượn của Keras. Nó đơn giản là xử lý việc cắt và đệm danh sách trong Python.

# Đặt độ dài chuỗi tối đa. Chuỗi dài nhất trong tập huấn luyện của chúng ta là 47, nhưng dù sao chúng ta cần chừa khoảng trống ở cuối.
# Trong bài báo gốc, các tác giả đã sử dụng độ dài 512.
MAX_LEN = 128
# Sử dụng bộ tách từ BERT để chuyển đổi token thành index của chúng trong bộ từ vựng BERT
input_ids = [tokenizer.convert_tokens_to_ids(x) for x in tokenized_texts]
# Đệm thêm vào các chuỗi đầu vào
input_ids = pad_sequences(input_ids, maxlen=MAX_LEN, dtype="long", truncating="post", padding="post")

Tạo attention mask

# Tạo mảng chứa các attention mask
attention_masks = []

# Tạo mask chứa các giá trị 1 cho mỗi koten và 0 cho các giá trị đệm
for seq in input_ids:
  seq_mask = [float(i>0) for i in seq]
  attention_masks.append(seq_mask)
# Sử dụng train_test_split để chia dữ liệu của ta thành tập huấn luyện và tập xác nhận

train_inputs, validation_inputs, train_labels, validation_labels = train_test_split(input_ids, labels, random_state=2018, test_size=0.1)
train_masks, validation_masks, _, _ = train_test_split(attention_masks, input_ids, random_state=2018, test_size=0.1)
# Chuyển dữ liệu về dạng tensor của pytorch, đây là kiểu dữ liệu bắt buộc cho mô hình của chúng ta

train_inputs = torch.tensor(train_inputs)
validation_inputs = torch.tensor(validation_inputs)
train_labels = torch.tensor(train_labels)
validation_labels = torch.tensor(validation_labels)
train_masks = torch.tensor(train_masks)
validation_masks = torch.tensor(validation_masks)
# lựa chọn batch size sử dụng huấn luyện. Với fine-tuning BERT, các tác giả khuyến nghị đặt giá trị này là 16 hoặc 32
batch_size = 32

# Tạo iterator với DataLoader. Điều này sẽ giúp tiết kiệm bộ nhớ khi huấn luyện. Khác với vòng lặp, với một iterator thì toàn bộ dataset sẽ không cần phải nạp lên bộ nhớ

train_data = TensorDataset(train_inputs, train_masks, train_labels)
train_sampler = RandomSampler(train_data)
train_dataloader = DataLoader(train_data, sampler=train_sampler, batch_size=batch_size)

validation_data = TensorDataset(validation_inputs, validation_masks, validation_labels)
validation_sampler = SequentialSampler(validation_data)
validation_dataloader = DataLoader(validation_data, sampler=validation_sampler, batch_size=batch_size)

Huấn luyện mô hình

Bây giờ dữ liệu đầu vào của chúng ta đã được định dạng chính xác, đã đến lúc tinh chỉnh mô hình BERT.

Đối với nhiệm vụ này, trước tiên chúng ta sửa đổi mô hình BERT được huấn luyện sẵn để đưa ra kết quả đầu ra cho bài toán phân loại và sau đó tiếp tục huấn luyện mô hình trên tập dữ liệu của mình cho đến khi toàn bộ mô hình, từ đầu chí cuối, phù hợp với nhiệm vụ của chúng ta. May mắn là cài đặt của huggingface pytorch đã chứa sẵn một bộ giao diện được thiết kế cho nhiều nhiệm vụ NLP. Mặc dù các giao diện này đều được xây dựng dựa trên mô hình BERT được huấn luyện sẵn, mỗi loại có các lớp và kiểu đầu ra khác nhau được thiết kế cho các nhiệm vụ NLP cụ thể của chúng.

Chúng ta sẽ nạp BertForSequenceClassification. Đây là mô hình BERT thông thường với một lớp tuyến tính duy nhất được thêm vào ở trên để phân loại, chúng ta sẽ sử dụng nó để phân loại câu. Khi chúng ta cung cấp dữ liệu đầu vào, toàn bộ mô hình BERT pre-trained và lớp phân loại sẽ được huấn luyện với nhiệm vụ cụ thể của chúng ta.

Kiến trúc của mô hình Fine-tuning

Như đã được trình bày trước đó, token đầu tiên của mỗi chuỗi là token phân loại đặc biệt ([CLS]). Không giống như vectơ trạng thái ẩn tương ứng với token biểu diễn từ thông thường, trạng thái ẩn tương ứng với token đặc biệt này được chỉ định bởi các tác giả của BERT là đại diện của toàn bộ câu được sử dụng cho các nhiệm vụ phân loại. Như vậy, khi chúng ta cung cấp một câu đầu vào cho mô hình trong quá trình huấn luyện, đầu ra là vectơ trạng thái ẩn độ dài 768 tương ứng với token này. Lớp bổ sung được thêm ở trên bao gồm các nơ ron tuyến tính chưa được huấn luyện có kích thước [hidden_state, number_of_labels], hay [768,2], có nghĩa là đầu ra của BERT kết hợp với lớp phân loại của chúng ta là một vectơ gồm hai số đại diện cho điểm số để làm cơ sở phân loại câu đúng/sai ngữ pháp. Vectơ này được đưa vào hàm loss cross-entropy.

Quá trình tinh chỉnh

Bởi vì các lớp BERT được huấn luyện sẵn đã mã hóa rất nhiều thông tin về ngôn ngữ, do đó việc huấn luyện trình phân loại không tốn quá nhiều tài nguyên. Thay vì huấn luyện tất cả các lớp trong một mô hình lớn từ đầu, kết quả chúng ta có sẵn tương đương với việc huấn luyện các lớp phía dưới đến 95% và chỉ tập trung huấn luyện lớp trên cùng để phù hợp nhiệm vụ của chúng ta.

Trong thực tế, có thể mọi người sẽ chọn “đóng băng” các lớp nhất định khi tinh chỉnh, hoặc sử dụng các learning rate khác nhau hoặc giảm dần, v.v., các nỗ lực này nhằm duy trì trọng số tốt trong mạng nơ ron và tăng tốc độ huấn luyện (thường là đáng kể) . Trên thực tế, nghiên cứu gần đây về BERT đã chứng minh rằng việc đóng băng phần lớn các trọng số không ảnh hưởng nhiều đến độ chính xác, nhưng cũng có những trường hợp ngoại lệ. Nếu nhiệm vụ và tập dữ liệu tinh chỉnh của chúng ta rất khác so với tập dữ liệu được sử dụng để huấn luyện mô hình trước đó thì việc đóng băng các trọng số có thể không phải là một ý tưởng hay.

# Nạp BertForSequenceClassification, mô hình BERT được huấn luyện sẵn với một lớp phân loại ở trên cùng

model = BertForSequenceClassification.from_pretrained("bert-base-uncased", num_labels=2)
model.cuda()

Vậy là chúng ta đã xong nạp mô hình, tiếp theo, ta cần lấy các giá trị siêu tham số từ bên trong mô hình được lưu trữ.

Đối với mục đích tinh chỉnh, các tác giả khuyến nghị các phạm vi giá trị siêu tham số như sau:

  • Batch size: 16, 32
  • Learning rate (Adam): 5e-5, 3e-5, 2e-5
  • Number of epochs: 2, 3, 4
param_optimizer = list(model.named_parameters())
no_decay = ['bias', 'gamma', 'beta']
optimizer_grouped_parameters = [
    {'params': [p for n, p in param_optimizer if not any(nd in n for nd in no_decay)],
     'weight_decay_rate': 0.01},
    {'params': [p for n, p in param_optimizer if any(nd in n for nd in no_decay)],
     'weight_decay_rate': 0.0}
]
# Biến này chứa tất cả thông tin về siêu tham số cần cho huấn luyện
optimizer = BertAdam(optimizer_grouped_parameters,
                     lr=2e-5,
                     warmup=.1)

Dưới đây là vòng lặp huấn luyện của chúng ta. Trong mỗi lần lặp, chúng ta có một giai đoạn huấn luyện và một giai đoạn xác nhận. Ở mỗi lượt chúng ta cần:

Vòng lặp huấn luyện

  • Yêu cầu mô hình tính toán gradient bằng cách đặt mô hình ở chế độ huấn luyện
  • Giải nén dữ liệu đầu vào và nhãn của chúng tôi
  • Nạp dữ liệu lên GPU để tăng tốc
  • Xóa các gradient tính toán trong lần lặp trước. Trong pytorch, gradient được tích lũy theo mặc định (hữu ích cho những kiến trúc như RNN) trừ khi bạn xóa chúng một cách rõ ràng
  • Truyền dữ liệu đầu vào qua mạng
  • Lan truyền ngược (backpropagation)
  • Cập nhật các trọng số bằng optimizer.step()
  • Theo dõi các biến để giám sát quá trình huấn luyện

Vòng lặp đánh giá:

  • Yêu cầu mô hình không tính toán gradient bằng cách đặt mô hình ở chế độ đánh giá
  • Giải nén dữ liệu đầu vào và nhãn của chúng tôi
  • Tải dữ liệu lên GPU để tăng tốc
  • Truyền dữ liệu đầu vào qua mạng
  • Tính toán giá trị loss và theo dõi các biến để theo dõi tiến trình

Đoạn code phía dưới có thể trông khá đáng sợ, nhưng thực tế những gì quan trọng được mô tả phía trên. Một số đoạn code được viết với mục đích hỗ trợ hiển thị và theo dõi các quá trình.

# Hàm để tính độ chính xác giữa kết quả dự đoán và nhãn thực
def flat_accuracy(preds, labels):
    pred_flat = np.argmax(preds, axis=1).flatten()
    labels_flat = labels.flatten()
    return np.sum(pred_flat == labels_flat) / len(labels_flat)
# Lưu giá trị loss và độ chính xác để vẽ biểu đồ
train_loss_set = []

# Số lượng epoch huấn luyện (các tác giả khuyến nghị giá trị từ 2 đến 4)
epochs = 4

# trange is a tqdm wrapper around the normal python range
for _ in trange(epochs, desc="Epoch"):
  
  
  # Huấn luyện
  
  # Đưa mô hình về chế độ huấn luyện
  model.train()
  
  # Các biến theo dõi
  tr_loss = 0
  nb_tr_examples, nb_tr_steps = 0, 0
  
  # Huấn luyện trong 1 epoch
  for step, batch in enumerate(train_dataloader):
    # Đưa dữ liệu vào GPU
    batch = tuple(t.to(device) for t in batch)
    # Lấy dữ liệu đầu vào từ dataloader
    b_input_ids, b_input_mask, b_labels = batch
    # Xóa gradient
    optimizer.zero_grad()
    # Truyền dữ liệu qua mạng
    loss = model(b_input_ids, token_type_ids=None, attention_mask=b_input_mask, labels=b_labels)
    train_loss_set.append(loss.item())    
    # Lan truền ngược
    loss.backward()
    # Cập nhật trọng số mạng
    optimizer.step()
    
    
    # Cập nhật biến theo dõi
    tr_loss += loss.item()
    nb_tr_examples += b_input_ids.size(0)
    nb_tr_steps += 1

  print("Train loss: {}".format(tr_loss/nb_tr_steps))
    
    
  # Xác nhận

  # Đặt mô hình vào chế độ đánh giá để đánh giá lỗi trên tập xác nhận
  model.eval()

  # Các biến theo dõi 
  eval_loss, eval_accuracy = 0, 0
  nb_eval_steps, nb_eval_examples = 0, 0

  # Đánh giá trong 1 epoch
  for batch in validation_dataloader:
    # Đưa vào GPU
    batch = tuple(t.to(device) for t in batch)
    # Lấy dữ liệu đầu vào từ dataloader
    b_input_ids, b_input_mask, b_labels = batch
    # Yêu cầu dữ liệu không tính gradient, tiết kiệm bộ nhớ và tăng tốc độ
    with torch.no_grad():
      # Chiều xuôi, tính toán dự toán 
      logits = model(b_input_ids, token_type_ids=None, attention_mask=b_input_mask)
    
    # Đưa giá trị dự đoán và nhãn về CPU để tính độ chính xác
    logits = logits.detach().cpu().numpy()
    label_ids = b_labels.to('cpu').numpy()

    tmp_eval_accuracy = flat_accuracy(logits, label_ids)
    
    eval_accuracy += tmp_eval_accuracy
    nb_eval_steps += 1

  print("Validation Accuracy: {}".format(eval_accuracy/nb_eval_steps))
Epoch:   0%|          | 0/4 [00:00<?, ?it/s]

Train loss: 0.5100321293619163


Epoch:  25%|██▌       | 1/4 [06:02<18:06, 362.03s/it]

Validation Accuracy: 0.7781635802469137
Train loss: 0.27194748427796167


Epoch:  50%|█████     | 2/4 [12:04<12:04, 362.15s/it]

Validation Accuracy: 0.8236882716049382
Train loss: 0.1256282212081292


Epoch:  75%|███████▌  | 3/4 [18:06<06:02, 362.10s/it]

Validation Accuracy: 0.8256172839506173
Train loss: 0.07009424090663674


Epoch: 100%|██████████| 4/4 [24:08<00:00, 362.19s/it]

Validation Accuracy: 0.8213734567901234

Đánh giá quá trình huấn luyện

Dưới đây là giá trị loss trên tất cả các batch

plt.figure(figsize=(15,8))
plt.title("Training loss")
plt.xlabel("Batch")
plt.ylabel("Loss")
plt.plot(train_loss_set)
plt.show()

Dự đoán và đánh giá trên tập dữ liệu độc lập

Bây giờ, chúng ta nạp tập dữ liệu còn lại và chuẩn bị đầu vào giống như chúng ta đã làm với tập huấn luyện. Sau đó, chúng ta sẽ đánh giá các dự đoán bằng hệ số tương quan Matthew, vì đây là độ đo được sử dụng rộng rãi bởi cộng đồng NLP để đánh giá hiệu suất trên CoLA. Với độ đo này, +1 là điểm tốt nhất và -1 là điểm kém nhất. Bằng cách này, chúng ta có thể thấy chúng ta đã làm tốt như thế nào so với những mô hình tốt nhất hiện nay.

# Tải file chứa tập dữ liệu test lên
from google.colab import files
uploaded = files.upload()
Saving out_of_domain_dev.tsv to out_of_domain_dev.tsv
df = pd.read_csv("out_of_domain_dev.tsv", delimiter='\t', header=None, names=['sentence_source', 'label', 'label_notes', 'sentence'])

# Danh sách câu và nhãn
sentences = df.sentence.values

# Thêm token đặc biệt vào đầu vào cuối câu để BERT hoạt động đúng
sentences = ["[CLS] " + sentence + " [SEP]" for sentence in sentences]
labels = df.label.values

tokenized_texts = [tokenizer.tokenize(sent) for sent in sentences]


MAX_LEN = 128
# Sử dụng bộ tách từ của BERT để chuyển token thành số định danh trong tập từ điển của BERT
input_ids = [tokenizer.convert_tokens_to_ids(x) for x in tokenized_texts]
# Đệm thêm vào đầu vào
input_ids = pad_sequences(input_ids, maxlen=MAX_LEN, dtype="long", truncating="post", padding="post")
# Tạo attention masks
attention_masks = []

for seq in input_ids:
  seq_mask = [float(i>0) for i in seq]
  attention_masks.append(seq_mask) 

prediction_inputs = torch.tensor(input_ids)
prediction_masks = torch.tensor(attention_masks)
prediction_labels = torch.tensor(labels)
  
batch_size = 32  


prediction_data = TensorDataset(prediction_inputs, prediction_masks, prediction_labels)
prediction_sampler = SequentialSampler(prediction_data)
prediction_dataloader = DataLoader(prediction_data, sampler=prediction_sampler, batch_size=batch_size)
# Dự đoán trên tập test

# Đưa mô hình vào chế độ đánh giá
model.eval()

# Các biến theo dõi
predictions , true_labels = [], []

# Dự đoán
for batch in prediction_dataloader:
  # Đưa vào GPU
  batch = tuple(t.to(device) for t in batch)
  # Lấy dữ liệu đầu vào từ dataloader
  b_input_ids, b_input_mask, b_labels = batch
  # Yêu cầu mô hình không tính gradient, tiết kiệm bộ nhớ và tăng tốc
  with torch.no_grad():
    # Truyefn dữ liệu, tính toán giá trị dự đoán
    logits = model(b_input_ids, token_type_ids=None, attention_mask=b_input_mask)

  # Đưa giá trị dự đoán và nhãn thật về CPU
  logits = logits.detach().cpu().numpy()
  label_ids = b_labels.to('cpu').numpy()
  
  # Lưu giá trị dự đoán và nhãn thật
  predictions.append(logits)
  true_labels.append(label_ids)
# Gọi và sử dụng thư viện matthews_corrcoef để đánh giá 
from sklearn.metrics import matthews_corrcoef
matthews_set = []

for i in range(len(true_labels)):
  matthews = matthews_corrcoef(true_labels[i],
                 np.argmax(predictions[i], axis=1).flatten())
  matthews_set.append(matthews)

Điểm số cuối cùng sẽ dựa trên toàn bộ test, nhưng hãy xem điểm số trên từng batch riêng lẻ để có cảm giác về sự thay đổi trong số liệu giữa các đợt.

matthews_set
[0.049286405809014416,
 -0.2548235957188128,
 0.5510387687779837,
 0.2809003238667948,
 0.5719694409972929,
 0.7410010097502685,
 0.37777777777777777,
 0.47519096331149147,
 0.647150228929434,
 0.7490196078431373,
 0.9229582069908973,
 0.7419408268023742,
 0.7562449037944323,
 0.7679476477883045,
 0.5204956780951701,
 0.5056936741642399,
 0.0]
# Duỗi giá trị dự đoán và nhãn thật để sử dụng độ đo Matthew trên toàn bộ dataset
flat_predictions = [item for sublist in predictions for item in sublist]
flat_predictions = np.argmax(flat_predictions, axis=1).flatten()
flat_true_labels = [item for sublist in true_labels for item in sublist]
matthews_corrcoef(flat_true_labels, flat_predictions)
0.5411909608645928

Kết luận

Bài viết này hướng dẫn chi tiết cách tinh chỉnh mô hình BERT được đào tạo sẵn cho một nhiệm vụ cụ thể. Nó giúp ta tiết kiệm được thời gian và công sức để tạo ra một mô hình chất lượng. Nếu bạn thấy bài viết này hữu ích với ai đó, đừng do dự chia sẻ.

Hãy theo dõi trituenhantao.io để thường xuyên nhận được các bài hướng dẫn về các chủ đề mới nhất của Trí tuệ nhân tạo trên thế giới.




Về trituenhantao.io

Trituenhantao.io là trang web chia sẻ thông tin, kiến thức, kinh nghiệm học tập và triển khai các chương trình và dự án sử dụng trí tuệ nhân tạo trên thế giới.
Xem tất cả các bài viết của trituenhantao.io →