- vừa được xem lúc

Xây Dựng Mô Hình Ngôn Ngữ Lớn (Phần 2): Xử lý dữ liệu văn bản

0 0 1

Người đăng: Le Thanh Cong

Theo Viblo Asia

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:

  1. "Nhà vua trị vì đất nước của ông."
  2. "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ừ Hellopalace 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["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 = 4stride = 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 WRV×dW \in \mathbb{R}^{V \times d}, 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 embeddingsPositional Encoding (PE)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í.

PE(pos,2i)=sin(pos100002i/d)PE(pos,2i+1)=cos(pos100002i/d)\begin{aligned} PE_{(pos, 2i)} &= \sin\left(\frac{pos}{10000^{2i/d}}\right) \\ \\ PE_{(pos, 2i+1)} &= \cos\left(\frac{pos}{10000^{2i/d}}\right) \end{aligned}

hay

PE(pos,k)={sin(pos100002i/d)Neˆˊk=2i với 0i<d2cos(pos100002i/d)Neˆˊk=2i+1 với 0i<d2PE_{(pos, k)} = \begin{cases} \sin\left(\frac{pos}{10000^{2i/d}}\right) & \text{Nếu } k = 2i \text{ với } 0 \leq i < \frac{d}{2} \\ \cos\left(\frac{pos}{10000^{2i/d}}\right) & \text{Nếu } k = 2i + 1 \text{ với } 0 \leq i < \frac{d}{2} \end{cases}

  • 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 PE(0,0)PE_{(0, 0)} = sin(0) = 0 PE(0,1)PE_{(0, 1)} = cos(0) = 1 PE(0,2)PE_{(0, 2)} = sin(0) = 0 PE(0,3)PE_{(0, 3)} = cos(0) = 1
am 1 PE(1,0)PE_{(1, 0)} = sin(1) = 0.84 PE(1,1)PE_{(1, 1)} = cos(1) = 0.54 PE(1,2)PE_{(1, 2)} = sin(1/100) = 0.0099 PE(1,3)PE_{(1, 3)} = cos(1/100) = 0.99995
a 2 PE(2,0)PE_{(2, 0)} = sin(2) = 0.91 PE(2,1)PE_{(2, 1)} = cos(2) = -0.42 PE(2,2)PE_{(2, 2)} = sin(2/100) = 0.019 PE(2,3)PE_{(2, 3)} = cos(2/100) = 0.9998
Robot 3 PE(3,0)PE_{(3, 0)} = sin(3) = 0.14 PE(3,1)PE_{(3, 1)} = cos(3) = -0.99 PE(3,2)PE_{(3, 2)} = sin(3/100) = 0.029 PE(3,3)PE_{(3, 3)} = 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)

PE=WposPE = W_{pos}

  • 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
Bộ nhớ Ít tốn bộ nhớ Tốn nhiều bộ nhớ hơn
Có thể tùy chỉnh Không

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://machinelearningmastery.com/a-gentle-introduction-to-positional-encoding-in-transformer-models-part-1/

https://kazemnejad.com/blog/transformer_architecture_positional_encoding/

Bình luận

Bài viết tương tự

- vừa được xem lúc

Các thuật toán cơ bản trong AI - Phân biệt Best First Search và Uniform Cost Search (UCS)

Nếu bạn từng đọc các thuật toán trong AI (Artificial Intelligence - Trí tuệ nhân tạo), rất có thể bạn từng nghe qua về các thuật toán tìm kiếm cơ bản: UCS (thuộc chiến lược tìm kiếm mù) và Best First Search (thuộc chiến lược tìm kiếm kinh nghiệm). Khác nhau rõ từ khâu phân loại rồi, thế nhưng hai th

0 0 176

- vừa được xem lúc

Con đường AI của tôi

Gần đây, khá nhiều bạn nhắn tin hỏi mình những câu hỏi đại loại như: có nên học AI, bắt đầu học AI như nào, làm sao tự học cho đúng, cho nhanh, học không bị nản, lộ trình học AI như nào... Sau nhiều lần trả lời, mình nghĩ rằng nên viết hẳn một bài để trả lời chi tiết hơn, cũng như để các bạn sau này

0 0 162

- vừa được xem lúc

[ChatterBot] Thư viện chatbot hay ho dành cho Python| phần 3

Trong bài trước mình đã trình bày về Training data cho chatbot và tiền xử lý dữ liệu. Trong phần này sẽ trình bày với các bạn về logic adapter.

0 0 67

- vừa được xem lúc

[Deep Learning] Kỹ thuật Dropout (Bỏ học) trong Deep Learning

. Trong bài viết này, mình xin phép giới thiệu về Dropout (Bỏ học) trong mạng Neural, sau đó là mình sẽ có 1 số đoạn code để xem Dropout ảnh hưởng thế nào đến hiệu suất của mạng Neural. 1.1. Dropout trong mạng Neural là gì.

0 0 73

- vừa được xem lúc

Kỹ thuật Dropout (Bỏ học) trong Deep Learning

Trong bài viết này, mình xin phép giới thiệu về Dropout (Bỏ học) trong mạng Neural, sau đó là mình sẽ có 1 số đoạn code để xem Dropout ảnh hưởng thế nào đến hiệu suất của mạng Neural. 1.

0 1 86

- vừa được xem lúc

Blockchain dưới con mắt làng Vũ Đại 4.0

Mở bài. Hey nhô các bạn, lại là mình đây .

0 0 53