Tokenizers
Tokenizer là một trong những thành phần cốt lõi của pipeline NLP. Chúng phục vụ một mục đích: dịch văn bản thành dữ liệu có thể được xử lý bởi mô hình. Mô hình chỉ có thể xử lý dạng số, do đó, các tokenizer cần phải chuyển đổi đầu vào văn bản của chúng ta thành dữ liệu số. Trong phần này, chúng ta sẽ khám phá chính xác những gì xảy ra trong đường dẫn mã hóa.
Trong các tác vụ NLP, dữ liệu thường được xử lý là văn bản thô. Đây là một ví dụ về văn bản như vậy:
Jim Henson was a puppeteer
Tuy nhiên, các mô hình chỉ có thể xử lý số, vì vậy chúng ta cần tìm cách chuyển văn bản thô thành số. Đó là những gì mà tokenizer làm, và có rất nhiều cách để thực hiện điều này. Mục tiêu đề ra là tìm ra cách biểu diễn có ý nghĩa nhất - nghĩa là cái có ý nghĩa nhất đối với mô hình - và, nếu có thể, là cách biểu diễn nhỏ nhất.
Hãy cùng xem một số ví dụ về thuật toán tokenize và cố gắng trả lời một số câu hỏi bạn có thể có về tokenize.
Dựa trên từ
Loại tokenizer đầu tiên ta nghĩ đến đó là dựa trên từ vựng. Nó thường rất dễ thiết lập và sử dụng chỉ với một số quy tắc và nó thường mang lại kết quả tốt. Ví dụ: trong hình ảnh bên dưới, mục tiêu là tách văn bản thô thành các từ và tìm biểu diễn số cho mỗi từ:
Có nhiều cách khác nhau để tách văn bản. Ví dụ: chúng ta có thể sử dụng khoảng trắng để tokenize văn bản thành các từ bằng cách áp dụng hàm split()
của Python:
tokenized_text = "Jim Henson was a puppeteer".split()
print(tokenized_text)
['Jim', 'Henson', 'was', 'a', 'puppeteer']
Ngoài ra còn có các biến thể của tokenize mức từ với các quy tắc bổ sung cho dấu câu. Với loại tokenizer này, chúng ta có thể đúc kết với một bộ “từ vựng” khá lớn, trong đó từ vựng được xác định bằng tổng số token độc lập mà chúng ta có trong corpus (kho ngữ liệu) của mình.
Mỗi từ được gán một ID, bắt đầu từ 0 và tăng dần theo kích thước của bộ từ vựng. Mô hình sử dụng các ID này để xác định từng từ.
Nếu chúng ta muốn bao phủ hoàn toàn một ngôn ngữ bằng tokenize mức từ, chúng ta sẽ cần phải có một chỉ số nhận dạng cho mỗi từ trong ngôn ngữ, điều này sẽ tạo ra một lượng lớn token. Ví dụ: có hơn 500,000 từ trong tiếng Anh, vì vậy để xây dựng bản đồ nối mỗi từ đến một ID đầu vào, chúng ta cần theo dõi ngần đó ID. Hơn nữa, các từ như “dog” được biểu diễn khác với các từ như “dogs”, và ban đầu mô hình sẽ không có cách nào để biết rằng “dog” (chó) và “dogs” là tương tự nhau: nó sẽ xác định hai từ này không liên quan. Điều này cũng áp dụng cho các từ tương tự khác, như “run” (chạy) và “running”, mà ban đầu mô hình sẽ không thấy là tương tự.
Cuối cùng, chúng ta cần một token tùy chỉnh để đại diện cho các từ không có trong vốn từ vựng của chúng ta. Mã này được gọi là token “không xác định”, thường được biểu thị là ”[UNK]” hoặc ”<unk>”. Nói chung, đó là một dấu hiệu xấu nếu bạn thấy trình tokenize đang tạo ra rất nhiều token này, vì nó không thể truy xuất một biểu hiện hợp lý của một từ và bạn đang mất thông tin trong suốt quá trình. Mục tiêu khi tạo từ vựng là làm sao cho trình tokenize mã hóa càng ít từ thành token không xác định càng tốt.
Một cách để giảm số lượng mã thông báo không xác định là đi sâu hơn xuống một cấp, sử dụng tokenize mức kí tự.
Dựa trên kí tự
- Vốn từ vựng ít hơn nhiều.
- Có ít token ngoài bộ từ vựng (không xác định) hơn nhiều, vì mọi từ đều có thể được xây dựng từ các ký tự.
Nhưng ở đây cũng có một số câu hỏi nảy sinh liên quan đến dấu cách và các dấu câu:
Cách tiếp cận này cũng không hoàn hảo. Vì biểu diễn bây giờ dựa trên các ký tự chứ không phải từ, người ta có thể lập luận rằng, theo trực giác, nó ít ý nghĩa hơn: mỗi ký tự không có nhiều ý nghĩa riêng so với trường hợp của các từ. Tuy nhiên, điều này lại khác nhau tùy theo ngôn ngữ; trong tiếng Trung, chẳng hạn, mỗi ký tự mang nhiều thông tin hơn một ký tự trong ngôn ngữ Latinh.
Một điều khác cần xem xét là chúng ta sẽ có một lượng rất lớn token sẽ được xử lý bởi mô hình của chúng ta: trong khi một từ chỉ là một token duy nhất khi tokenize dựa trên từ, nó có thể dễ dàng chuyển thành 10 token trở lên khi chuyển đổi thành các ký tự.
Để tận dụng tối đa cả hai, chúng ta có thể sử dụng kỹ thuật thứ ba kết hợp hai cách tiếp cận: tokenize theo từ phụ.
Tokenize theo từ phụ
Các thuật toán token theo từ phụ dựa trên nguyên tắc rằng các từ được sử dụng thường xuyên không được chia thành các từ phụ nhỏ hơn, nhưng các từ hiếm phải được phân tách thành các từ phụ có ý nghĩa.
Ví dụ: “annoyingly” (khó chịu) có thể được coi là một từ hiếm và có thể được chuyển thành “annoying” và “ly”. Cả hai đều có khả năng xuất hiện thường xuyên hơn dưới dạng các từ phụ độc lập, đồng thời nghĩa của “annoying” được giữ nguyên bởi nghĩa kết hợp của “annoying” và “ly”.
Dưới đây là một ví dụ cho thấy cách một thuật toán tokenize theo từ phụ sẽ tokenize chuỗi “Let’s do tokenization!” (Hãy thực hiện tokenize!):
Những từ phụ này cung cấp rất nhiều ý nghĩa về mặt ngữ nghĩa: ví dụ: trong ví dụ ở trên “tokenization” được chia thành “token” và “ization”, hai token đều có ý nghĩa về mặt ngữ nghĩa đồng thời tiết kiệm không gian (chỉ cần hai token để biểu thị một từ dài). Điều này cho phép chúng ta có thể bao quát tương đối tốt với các từ vựng nhỏ và gần như không có token nào không xác định.
Cách tiếp cận này đặc biệt hữu ích trong các ngôn ngữ tổng hợp như tiếng Thổ Nhĩ Kỳ, nơi bạn có thể tạo (gần như) các từ phức dài tùy ý bằng cách xâu chuỗi các từ phụ lại với nhau.
Và hơn thế nữa!
Không có gì đáng ngạc nhiên, có rất nhiều kỹ thuật khác, có thể kể đến:
- Byte-level BPE (BPE cấp byte), như được sử dụng trong GPT-2
- WordPiece, như được sử dụng trong BERT
- SentencePiece hoặc Unigram, như được sử dụng trong một số mô hình đa ngôn ngữ
Bây giờ, bạn đã có đủ kiến thức về cách thức hoạt động của tokenize để bắt đầu với API.
Tải và lưu
Việc tải và lưu tokenizer cũng đơn giản như với các mô hình. Trên thực tế, nó dựa trên hai phương thức giống nhau: from_pretrained()
và save_pretrained()
. Các phương thức này sẽ tải hoặc lưu thuật toán được sử dụng bởi tokenizer (hơi giống với kiến trúc của mô hình) cũng như từ vựng của nó (hơi giống với trọng số của mô hình).
Việc tải BERT tokenizer được huấn luyện với cùng một checkpoint với BERT được thực hiện giống như cách tải mô hình, ngoại trừ việc chúng ta sử dụng lớp BertTokenizer
:
from transformers import BertTokenizer
tokenizer = BertTokenizer.from_pretrained("bert-base-cased")
Tương tự AutoModel
, lớp AutoTokenizer
sẽ lấy lớp tokenizer thích hợp trong thư viện dựa trên tên checkpoint và có thể được sử dụng trực tiếp với bất kỳ checkpoint nào:
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")
Giờ chúng ta có thể sử dụng tokenizer như trong đoạn dưới đây:
tokenizer("Using a Transformer network is simple")
{'input_ids': [101, 7993, 170, 11303, 1200, 2443, 1110, 3014, 102],
'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 0],
'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1]}
Lưu một tokenizer giống như khi lưu một mô hình vậy:
tokenizer.save_pretrained("directory_on_my_computer")
Chúng ta sẽ trao đổi thêm về token_type_ids
trong Chương 3, và chúng ta sẽ giải thích cơ chế của từ khoá attention_mask
sau đó. Đầu tiênm hãy cùng xem cách input_ids
được tạo ra. Để làm điều này, chúng ta sẽ cần xem xét các phương thức trung gian của tokenizer.
Mã hoá
Dịch văn bản sang số được gọi là encoding hay mã hoá. Việc mã hóa được thực hiện theo quy trình gồm hai bước: tokenize, tiếp theo là chuyển đổi sang ID đầu vào.
Như chúng ta đã thấy, bước đầu tiên là chia văn bản thành các từ (hoặc các phần của từ,theo ký hiệu dấu câu, v.v.), thường được gọi là token. Có nhiều quy tắc có thể chi phối quá trình đó, đó là lý do tại sao chúng ta cần khởi tạo trình token bằng cách sử dụng tên của mô hình, để đảm bảo rằng chúng tôi sử dụng cùng các quy tắc đã được sử dụng khi mô hình được huấn luyện trước.
Bước thứ hai là chuyển đổi các token đó thành số để chúng ta có thể xây dựng một tensor từ chúng và đưa chúng vào mô hình. Để làm điều này, tokenizer có từ vựng, là phần chúng ta tải xuống khi khởi tạo nó bằng phương thức from_pretrained()
. Một lần nữa, chúng ta cần sử dụng cùng một bộ từ vựng được sử dụng khi mô hình được huấn luyện trước.
Để hiểu rõ hơn về hai bước, chúng ta sẽ khám phá chúng một cách riêng biệt. Lưu ý rằng chúng tôi sẽ sử dụng một số phương pháp thực hiện các phần của pipeline tokenize riêng biệt để hiển thị cho bạn kết quả trung gian của các bước đó, nhưng trên thực tế, bạn nên gọi tokenize trực tiếp trên đầu vào của mình (như được hiển thị trong phần 2).
Tokenize
Quá trình tokenize được thực hiện bởi phương thức tokenize()
của tokenizer:
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")
sequence = "Using a Transformer network is simple"
tokens = tokenizer.tokenize(sequence)
print(tokens)
Kết quả của phương thức này là một danh sách các chuỗi văn bản hoặc tokens:
['Using', 'a', 'transform', '##er', 'network', 'is', 'simple']
Tokenizer này là một tokenizer dự theo từ phụ: nó chia các từ cho đến khi lấy được các tokens được biểu diễn bởi bộ từ vựng của nó. Ví dụ, transformer
sẽ được chia thành hai token: transform
và ##er
.
Từ token tới ID đầu vào
Quá tình chuyển đổi sang ID đầu vào được thực hiện bởi convert_tokens_to_ids()
của tokenizer:
ids = tokenizer.convert_tokens_to_ids(tokens)
print(ids)
[7993, 170, 11303, 1200, 2443, 1110, 3014]
Các đầu ra này, sau khi được chuyển đổi sang khung tensor thích hợp, có thể được sử dụng làm đầu vào cho một mô hình như đã thấy ở phần trước trong chương này.
✏️ Thử nghiệm thôi! Sao chép hai bước cuối cùng (tokenize và chuyển đổi sang ID đầu vào) trên các câu đầu vào mà chúng ta đã sử dụng trong phần 2 (“I’ve been waiting for a HuggingFace course my whole life.” và “I hate this so much!”). Kiểm tra xem bạn có nhận được các ID đầu vào giống như chúng tôi đã nhận trước đó không!
Giải mã
Decoding hay giải mã thì ngược lại: từ các chỉ số từ vựng, ta muốn trả về một chuỗi văn bản. Điều này có thể được thực hiện với phương thức decode()
như sau:
decoded_string = tokenizer.decode([7993, 170, 11303, 1200, 2443, 1110, 3014])
print(decoded_string)
'Using a Transformer network is simple'
Lưu ý rằng phương pháp giải mã
không chỉ chuyển đổi các chỉ số trở lại thành token, mà còn nhóm các token là một phần của cùng một từ lại với nhau để tạo ra một câu có thể đọc được. Hành vi này sẽ cực kỳ hữu ích khi chúng ta sử dụng các mô hình dự đoán văn bản mới (văn bản được tạo từ lời nhắc hoặc đối với các bài toán chuỗi-sang-chuỗi như dịch hoặc tóm tắt văn bản).
Bây giờ bạn đã hiểu các hoạt động nguyên tử mà một tokenizer có thể xử lý: tokenize, chuyển đổi sang ID và chuyển đổi ID trở lại một chuỗi. Tuy nhiên, tất cả chỉ mới là sự bắt đầu. Trong phần sau, chúng ta sẽ tiếp cận các giới hạn của nó và xem cách vượt qua chúng.