Một phần bài viết được thể hiện dưới dạng Jupyter NoteBook tại đây
1. Word embeddings
Các mô hình ngôn ngữ như LLMs không thể xử lý trực tiếp dữ liệu văn bản, thay vào đó cần phải Toán học hóa chúng bằng cách chuyển đổi thành các vector (dạng số học) để mô hình có thể hiểu và xử lý thông tin.
Mọi thứ đều là số - Pythagoras
Khái niệm chuyển đổi dữ diệu thành các vector gọi là embeddings. Không chỉ có dữ liệu văn bản, mà các kiểu dữ liệu khác như âm thanh, video cũng được chuyển sang dạng vector trước khi xử lý.
Lưu ý: Thuật toán embedding của các kiểu dữ liệu khác nhau là không giống nhau.
Word2Vec
Một trong những thuật toán phổ biến dùng trong việc chuyển đổi văn bản được nhóm nghiên cứu của Google công bố vào năm 2013 có tên là Word2Vec.
Word2Vec dự đoán ngữ cảnh của một từ dựa trên từ mục tiêu hoặc ngược lại. Ý tưởng chính đằng sau Word2Vec là các từ xuất hiện trong những ngữ cảnh tương tự có xu hướng mang ý nghĩa giống nhau.
Ví dụ ta có các câu sau trong tập dữ liệu huấn luyện:
- "Nhà vua trị vì đất nước của ông."
- "Nữ hoàng cai trị vương quốc của bà."
Word2Vec học được rằng "vua" và "nữ hoàng" thường xuất hiện trong ngữ cảnh tương tự. Kiểm tra khoảng cách giữa 2 vector cũng sẽ thấy rất gần nhau.
Do đó, khi được chiếu vào không gian hai chiều để trực quan hóa, các từ có ý nghĩa tương tự sẽ được nhóm lại với nhau, như minh họa trong hình dưới đây.
Trong thực tiễn, các từ sẽ được biểu diễn ở số chiều lớn hơn nhiều so với hình học không gian cổ điển. 2 đến 3 chiều là không đủ để biểu diễn, phân biệt hàm trăm nghìn, hàng triệu hoặc từ trong các bộ dữ liệu văn bản.
Phiên bản | Số chiều vector embeddings |
---|---|
GPT-2 | 768 |
GPT-2 Large | 1280 |
GPT-2 XL | 1600 |
GPT-3 (175B) | 12288 |
Tuy nhiên, các LLMs như GPT sẽ không dùng Word2Vec mà có cách riêng để tạo embledding. Do phương pháp này bộc lộ một số hạn chế sau :
- Nếu một từ có nhiều nghĩa khác nhau, Word2Vec vẫn chỉ tạo một vector duy nhất.
- Chưa xử lý tốt các văn bản dài: Word2Vec chỉ dựa vào ngữ cảnh gần của từ mà không xem xét ngữ cảnh lớn hơn.
Dưới đây là hình minh họa các bước xử lý để tạo ra token embeddings của họ nhà GPT.
2. Tokenizing text
Tokenizing text (hay tokenization) là quá trình chuyển đổi một chuỗi văn bản thành các đơn vị nhỏ hơn, gọi là tokens. Các tokens có thể là một từ, cụm từ, hoặc một ký hiệu, tùy thuộc vào ngữ cảnh và mục đích của việc xử lý văn bản.
Chúng ta cùng thử tokenization một câu văn bản nhỏ bằng Python ngay sau đây:
import os
import urllib.request
import re # Tải file
if not os.path.exists("the-verdict.txt"): url = ("https://raw.githubusercontent.com/rasbt/" "LLMs-from-scratch/main/ch02/01_main-chapter-code/" "the-verdict.txt") file_path = "the-verdict.txt" urllib.request.urlretrieve(url, file_path) # Mở và đọc file
with open("the-verdict.txt", "r", encoding="utf-8") as f: raw_text = f.read() preprocessed = re.split(r'([,.:;?_!"()\']|--|\s)', raw_text) # Tách văn bản khi gặp các ký tự
preprocessed = [item.strip() for item in preprocessed if item.strip()] # Loại bỏ khoảng trắng
print(preprocessed[:30])
Kết quả: ['I', 'HAD', 'always', 'thought', 'Jack', 'Gisburn', 'rather', 'a', 'cheap', 'genius', '--', 'though', 'a', 'good', 'fellow', 'enough', '--', 'so', 'it', 'was', 'no', 'great', 'surprise', 'to', 'me', 'to', 'hear', 'that', ',', 'in']
Khoảng trắng hay dấu cách không cần thiết trong trường hợp này, nên ta loại bỏ. Các dữ liệu như code Ruby hay Python thì khoảng trắng lại rất cần được giữ lại.
3. Chuyển đổi tokens sang token IDs
Sau khi đã tách văn bản thành các tokens, bước tiếp theo sẽ là mã hóa các tokens thành các token IDs. Mỗi token sẽ ứng với một giá trị khác nhau được định nghĩa trong tệp từ vựng (Vocabulary).
import os
import urllib.request
import re # Tải file
if not os.path.exists("the-verdict.txt"): url = ("https://raw.githubusercontent.com/rasbt/" "LLMs-from-scratch/main/ch02/01_main-chapter-code/" "the-verdict.txt") file_path = "the-verdict.txt" urllib.request.urlretrieve(url, file_path) # Mở và đọc file
with open("the-verdict.txt", "r", encoding="utf-8") as f: raw_text = f.read() # Tokenization
preprocessed = re.split(r'([,.:;?_!"()\']|--|\s)', raw_text)
preprocessed = [item.strip() for item in preprocessed if item.strip()] # Loại bỏ các từ trùng lặp và sắp xếp theo thứ tự a->z
all_words = sorted(set(preprocessed)) vocab_size = len(all_words) # 50 từ # tham chiếu từng token với các số nguyên tăng dần
vocab = {token:integer for integer,token in enumerate(all_words)} for i, item in enumerate(vocab.items()): print(item) if i >= 50: break class SimpleTokenizerV1: def __init__(self, vocab): self.str_to_int = vocab self.int_to_str = {i:s for s,i in vocab.items()} # Văn bản -> tokens -> tokenIds def encode(self, text): preprocessed = re.split(r'([,.:;?_!"()\']|--|\s)', text) preprocessed = [ item.strip() for item in preprocessed if item.strip() ] ids = [self.str_to_int[s] for s in preprocessed] return ids # TokenIds -> tokens -> văn bản gốc def decode(self, ids): text = " ".join([self.int_to_str[i] for i in ids]) # Replace spaces before the specified punctuations text = re.sub(r'\s+([,.?!"()\'])', r'\1', text) return text tokenizer = SimpleTokenizerV1(vocab)
text = """"It's the last he painted, you know," Mrs. Gisburn said with pardonable pride."""
ids = tokenizer.encode(text)
print(ids)
Kết quả mã hóa [1, 56, 2, 850, 988, 602, 533, 746, 5, 1126, 596, 5, 1, 67, 7, 38, 851, 1108, 754, 793, 7]
4. Ký tự đặc biệt
Nếu từ hay ký tự trong văn bản đầu vào không có trong tệp từ vựng (Vocabulary) thì sao ? Chúng ta sẽ thêm 2 loại ký tự đặc biệt sau vào tệp từ vựng
- <|unk|>: Các token không khớp với các từ phía trên sẽ được xếp vào loại này.
- <|endoftext|>: Ký hiệu phân tách giữa các văn bản do dữ liệu đầu vào gồm rất nhiều loại văn bản khác nhau.
Ta nâng cấp phiên bản SimpleTokenizerV1
ở mục trước thành SimpleTokenizerV2
như sau:
import os
import urllib.request
import re if not os.path.exists("the-verdict.txt"): url = ("https://raw.githubusercontent.com/rasbt/" "LLMs-from-scratch/main/ch02/01_main-chapter-code/" "the-verdict.txt") file_path = "the-verdict.txt" urllib.request.urlretrieve(url, file_path) with open("the-verdict.txt", "r", encoding="utf-8") as f: raw_text = f.read() preprocessed = re.split(r'([,.:;?_!"()\']|--|\s)', raw_text)
preprocessed = [item.strip() for item in preprocessed if item.strip()] all_words = sorted(set(preprocessed)) # Thêm 2 ký tự đặc biệt vào
all_words.extend(["<|endoftext|>", "<|unk|>"])
vocab = {token:integer for integer,token in enumerate(all_words)} class SimpleTokenizerV2: def __init__(self, vocab): self.str_to_int = vocab self.int_to_str = { i:s for s,i in vocab.items()} def encode(self, text): preprocessed = re.split(r'([,.:;?_!"()\']|--|\s)', text) preprocessed = [item.strip() for item in preprocessed if item.strip()] preprocessed = [ item if item in self.str_to_int else "<|unk|>" for item in preprocessed ] ids = [self.str_to_int[s] for s in preprocessed] return ids def decode(self, ids): text = " ".join([self.int_to_str[i] for i in ids]) text = re.sub(r'\s+([,.:;?!"()\'])', r'\1', text) return text text1 = "Hello, do you like tea?"
text2 = "In the sunlit terraces of the palace."
text = " <|endoftext|> ".join((text1, text2))
print(text) tokenizer = SimpleTokenizerV2(vocab)
print(tokenizer.encode(text)) print(tokenizer.decode(tokenizer.encode(text)))
Kết quả: Từ Hello và palace không có trong tệp từ vựng trước đó, nên khi mã hóa sẽ chuyển thành <|unk|>
Hello, do you like tea? <|endoftext|> In the sunlit terraces of the palace.
[1131, 5, 355, 1126, 628, 975, 10, 1130, 55, 988, 956, 984, 722, 988, 1131, 7]
<|unk|>, do you like tea? <|endoftext|> In the sunlit terraces of the <|unk|>
5. Byte pair encoding
Phương pháp tạo ra tệp từ vựng (Vocabulary) như trên sẽ bộc lộ hạn chế khi làm việc với số lượng từ rất lớn. Trong thực tiễn, các mô hình GPT hay Llama sử dụng một thuật toán phức tạp hơn gọi là Byte pair encoding (BPE).
Minh họa BPE
BPE hoạt động dựa trên việc hợp nhất các cặp ký tự hoặc âm tiết phổ biến trong văn bản để tạo thành tệp từ vựng. Một ví dụ đơn giản như sau:
Bước 1:
Một văn bản dài chỉ được tạo thành bởi 5 từ sau "hug pug pun bun hugs"
Bước 2:
Từ vựng cơ sở là các chữ cái tạo nên văn bản, khi này Tệp từ vựng là ["b", "g", "h", "n", "p", "s", "u"]
Bước 3:
Đếm tần số xuất hiện của các từ ("hug", 10), ("pug", 5), ("pun", 12), ("bun", 4), ("hugs", 5)
Bước 4:
Lại tách từng từ thành các ký tự như sau:
("h" "u" "g", 10), ("p" "u" "g", 5), ("p" "u" "n", 12), ("b" "u" "n", 4), ("h" "u" "g" "s", 5)
Bước 5:
Cặp ug có tần suất nhiều nhất (20 lần). Ta tiến hành hợp nhất ("u", "g") -> "ug"
Tệp từ vựng mới là: ["b", "g", "h", "n", "p", "s", "u", "ug"]
Danh sách các tokens: ("h" "ug", 10), ("p" "ug", 5), ("p" "u" "n", 12), ("b" "u" "n", 4), ("h" "ug" "s", 5)
Bước 6:
Lại xét cặp un (16 lần) nhiều nhất. Tiến hành hợp nhất ("u", "n") -> "un"
Tệp từ vựng mới là: ["b", "g", "h", "n", "p", "s", "u", "ug", "un", "hug"]
Danh sách các tokens: ("hug", 10), ("p" "ug", 5), ("p" "un", 12), ("b" "un", 4), ("hug" "s", 5)
Tiếp tục làm vậy cho đến khi chúng ta chạm đến kích thước tệp từ vựng mong muốn.
ChatGPT đang dùng tệp từ vựng gồm có 50257 tokens (https://openaipublic.blob.core.windows.net/gpt-2/encodings/main/encoder.json)
Minh họa với python
Thuật toán BPE nếu triển khai từ đầu cũng sẽ khá phức tạp và sẽ làm bài viết dài lê thê hơn. May thay, OpenAI cung cấp cho chúng ta một thư viện tên là tiktoken dùng tệp từ vựng hơn 50k token đã được họ huấn luyện.
# pip install tiktoken
from importlib.metadata import version
import tiktoken tokenizer = tiktoken.get_encoding("gpt2") text = ( "Hello, do you like tea? <|endoftext|> In the sunlit terraces" "of someunknownPlace."
)
integers = tokenizer.encode(text, allowed_special={"<|endoftext|>"})
print(integers) strings = tokenizer.decode(integers)
print(strings) # Kết quả:
[15496, 11, 466, 345, 588, 8887, 30, 220, 50256, 554, 262, 4252, 18250, 8812, 2114, 1659, 617, 34680, 27271, 13]
Hello, do you like tea? <|endoftext|> In the sunlit terracesof someunknownPlace.
6. Cửa sổ trượt (sliding window)
Các mô hình ngôn ngữ lớn thường có giới hạn về kích thước ngữ cảnh (context window), tức là số lượng token mà mô hình có thể xử lý cùng một lúc.
Sliding Window là một kỹ thuật cho phép chia nhỏ các chuỗi văn bản dài thành các đoạn ngắn hơn và xử lý chúng một cách tuần tự. Các đoạn ngắn hơn, mỗi đoạn có độ dài nhất định (window size).
Hai đặc tính của sliding window:
- Context length: Độ dài của mỗi đoạn sau khi được chia nhỏ.
- Stride: Thông số thể hiện sự xê dịch giữa các đoạn với nhau.
Context window | |
---|---|
GPT-3 | 2048 tokens |
GPT-3.5 | 4096 tokens |
GPT-4 | 8192 tokens |
GPT-4o | 128K tokens |
Gemini 1.5 Pro | 2M tokens |
Claude 3.5 Sonnet | 200K tokens |
Hình ảnh mô tả sliding window với context length = 4
và stride = 1
. Kết quả sau khi xử lý qua BPE và sliding window sẽ cho một tensor 2 chiều như sau.
tensor([[ 818, 262, 2612, 286], [ 262, 2612, 286, 262], [ 2612, 286, 262, 1748], [ 286, 262, 1748, 6204], [ 262, 1748, 6204, 262], [...]])
7. Tạo Token embeddings
Sau khi đã định dạng lại vector tokenIds
với phương pháp sliding window, chúng ta sẽ đến bước tạo token embedings.
Ma trận embedding
Ta có ma trận embedding , trong đó:
- V (số cột): Kích thước từ vựng (vocabulary size)
- d (số hàng): Số chiều của embeddings, ví dụ:
d=768
trong GPT-2.
token Id x
sẽ được tham chiếu với hàng thứ x
trong ma trận embedding.
Vậy câu hỏi đặt ra là lấy ra cái ma trận embedding này ở đâu ? Xin thưa là ma trận embedding được tạo ra bằng cách huấn luyện sử dụng các phương pháp khác như one-hot encoding
hay mạng nơ-ron ... Chi tiết có lẽ chúng ta sẽ gặp nhau ở một bài viết khác nói riêng về chủ đề này 😄
Ví dụ với Pytorch
import torch num_idx = 50000 out_dim = 768 torch.manual_seed(123) # Tạo ma trận embedding trong không gian 50257 * 768
embedding = torch.nn.Embedding(num_idx, out_dim) print(embedding.weight) idx = torch.tensor([2, 0, 1])
print(embedding(idx))
Kết quả in ra màn hình sẽ là
# Ma trận embedding
tensor([[ 0.3374, -0.1778, -0.3035, ..., -0.3181, -1.3936, 0.5226], # 50k phần tử [ 0.2579, 0.3420, -0.8168, ..., -0.4098, 0.4978, -0.3721], [ 0.7957, 0.5350, 0.9427, ..., -1.0749, 0.0955, -1.4138], ..., [-2.1883, 1.7872, -1.9135, ..., -0.9203, 0.2555, -0.2011], [ 0.0096, 0.4619, 0.3026, ..., 1.6759, 0.8603, -1.4613], [-0.3265, -0.9536, -0.2792, ..., -0.3351, -0.8828, -0.3377]], requires_grad=True) # token embeddings
tensor([[ 0.7957, 0.5350, 0.9427, ..., -1.0749, 0.0955, -1.4138], # dòng thứ 3 của Ma trận embedding [ 0.3374, -0.1778, -0.3035, ..., -0.3181, -1.3936, 0.5226], # dòng đầu tiên của Ma trận embedding [ 0.2579, 0.3420, -0.8168, ..., -0.4098, 0.4978, -0.3721]], # dòng thứ nhất grad_fn=<EmbeddingBackward0>)
8. Vị trí của từ trong câu (word positions)
Với đầu ra token embeddings ở phần trên, chúng ta đã có thể đưa vào mô hình để chạy.
Tuy nhiên, chưa có cách nào phân biệt hai từ giống nhau nằm ở các vị trí khác nhau. Điều này ảnh hưởng rất lớn đến việc mô hình hiểu được ngữ nghĩa, cấu trúc trong câu vì thứ tự từ trong câu thay đổi có thể thay đổi ý nghĩa của toàn bộ câu.
Ví dụ, câu Con mèo đuổi theo con chuột
có ý nghĩa khác với Con chuột đuổi theo con mèo
.
Giải pháp ở đây là chúng ta sẽ thêm một position embeddings cộng với token embeddings để ra kết quả cuối cùng là input embeddings.
Hiện này có 2 cách phổ biến để tính toán position embeddings là Positional Encoding (PE) và Learnable Position Embeddings (LPE)
Positional Encoding (PE)
Positional Encoding được sử dụng trong Transformer. Phương pháp này sử dùng 2 hàm lượng giác sin và cos để tính toán các vector vị trí.
hay
- pos là vị trí của từ trong câu (0 <= pos < L / 2 với L là độ dài của câu)
- d là số chiều của embedding
- i dùng để tính vị trí phần tử trong mỗi hàng
Từ | pos | k = 0, i = 0 | k = 1, i = 0 | k = 2, i = 1 | k = 3, i = 1 |
---|---|---|---|---|---|
I | 0 | = sin(0) = 0 | = cos(0) = 1 | = sin(0) = 0 | = cos(0) = 1 |
am | 1 | = sin(1) = 0.84 | = cos(1) = 0.54 | = sin(1/100) = 0.0099 | = cos(1/100) = 0.99995 |
a | 2 | = sin(2) = 0.91 | = cos(2) = -0.42 | = sin(2/100) = 0.019 | = cos(2/100) = 0.9998 |
Robot | 3 | = sin(3) = 0.14 | = cos(3) = -0.99 | = sin(3/100) = 0.029 | = cos(3/100) = 0.99 |
# postion embeddings
tensor([[ 0, 1, 0, 1], [ 0.84, 0.54, 0.0099, 0.99995], [ 0.91, -0.42, 0.019, 0.9998], [ 0.14, -0.99, 0.029, 0.99]
])
Learnable Position Embeddings (LPE)
Giống như phương pháp tạo token embeddings, LPE cung cấp một ma trận vị trí kích thước (L, d)
- L là độ dài của chuỗi
- d số chiều của embeddings
Vị trí nào sẽ ứng với dòng đó trong ma trận khi chuyển đổi.
So sánh PE và LPE
Positional Encoding (PE) | Learnable Position Embeddings (LPE) | |
---|---|---|
Cách tính toán | Dùng hàm lượng giác | Sử dụng ma trận |
Huấn luyện được | Không | Có |
Bộ nhớ | Ít tốn bộ nhớ | Tốn nhiều bộ nhớ hơn |
Có thể tùy chỉnh | Không | Có |
Nhìn lại
Chúng ta cùng xem lại tổng qua toàn bộ quá trình xử lý văn bản trước khi đưa vào mô hình ngôn ngữ lớn dưới đây
Tài liệu tham khảo
https://github.com/rasbt/LLMs-from-scratch
https://huggingface.co/learn/nlp-course/en/chapter6/5
https://kazemnejad.com/blog/transformer_architecture_positional_encoding/