Lập trình viên khi chuyển sang lĩnh vực xử lý ngôn ngữ tự nhiên (NLP) thường đối mặt với một câu hỏi hóc búa: Làm thế nào để máy tính hiểu được ý nghĩa của một từ ngữ? Máy tính chỉ xử lý các con số, ma trận và phép toán đại số tuyến tính, chứ không thể đọc hiểu trực tiếp chữ cái hay câu văn.
Bài học này sẽ hướng dẫn bạn quy trình số hóa văn bản một cách khoa học: từ việc bẻ nhỏ câu văn thành các mã token (Tokenization), vượt qua cạm bẫy từ ngoài từ điển (OOV), cho đến cơ chế biểu diễn ngữ nghĩa bằng không gian vector đa chiều (Word Embeddings). Chúng ta cũng sẽ tìm hiểu toán học của Cosine Similarity và thực hành xây dựng một ứng dụng tìm kiếm từ đồng nghĩa, giải bài toán tương đồng ngữ nghĩa bằng PyTorch.
8.1 Từ Chữ viết sang Con số: Tokenization
Trước khi nạp văn bản vào một mô hình học máy hay mạng nơ-ron, văn bản thô phải trải qua bước tiền xử lý bắt buộc gọi là Tokenization (Phân tách từ). Đây là quá trình chẻ một chuỗi ký tự dài thành các đơn vị thông tin nhỏ hơn, gọi là các tokens (có thể là ký tự đơn lẻ, các âm tiết, từ hoặc cụm từ).
Có ba hướng thiết kế Tokenizer chính:
-
Ký tự (Character-level Tokenization): Chia văn bản đến từng chữ cái đơn lẻ (a, b,
c...).
Ưu điểm: Từ điển cực kỳ nhỏ gọn (chỉ vài trăm ký tự), hoàn toàn không sợ gặp từ lạ.
Nhược điểm: Máy tính phải xử lý chuỗi cực kỳ dài, và nơ-ron rất khó học được ngữ nghĩa tổng quan của từ vì thông tin bị bẻ vụn ở cấp độ chữ cái. -
Từ thô (Word-level Tokenization): Chia văn bản dựa trên khoảng trắng để tách thành các
từ hoàn chỉnh.
Ưu điểm: Bảo toàn ngữ nghĩa nguyên bản của từ tốt.
Nhược điểm: Bùng nổ kích thước từ điển (lên tới hàng triệu từ). Đặc biệt, khi gặp một từ mới chưa từng xuất hiện trong quá trình huấn luyện, mô hình sẽ gặp lỗi Out-of-Vocabulary (OOV) và bắt buộc phải chuyển từ đó thành mã rác đại diện[UNK](Unknown), làm mất hoàn toàn thông tin của câu. -
Cấp độ mảnh từ (Subword-level Tokenization): Đây là tiêu chuẩn vàng của các mô hình
ngôn ngữ lớn (LLM) ngày nay như GPT hay BERT, sử dụng các thuật toán như
BPE (Byte Pair Encoding) hoặc WordPiece.
Cơ chế: Chia các từ phổ biến thành các từ nguyên bản, nhưng chẻ nhỏ các từ lạ hoặc từ ghép thành các mảnh tiền tố/hậu tố có nghĩa (ví dụ: từ "embeddings" có thể được chẻ thành "embed" và "##dings"). Điều này giúp từ điển duy trì ở kích thước tối ưu (khoảng 30.000 - 50.000 tokens) nhưng vẫn biểu diễn được bất kỳ từ mới nào bằng cách lắp ghép các mảnh nhỏ lại, loại bỏ triệt để lỗi OOV.
</w>, thuật toán chạy như sau (đã kiểm chứng bằng code
Python thực tế):
-
Bước 1: cặp
(w, e)xuất hiện nhiều nhất (2 lần trong "lowest" + 6 lần trong "newest" = 8 lần) → gộp thànhwe. -
Bước 2: cặp
(we, s)xuất hiện 8 lần → gộp thànhwes. -
Bước 3: cặp
(wes, t)xuất hiện 8 lần → gộp thànhwest. -
Bước 4: cặp
(west, </w>)xuất hiện 8 lần → gộp thànhwest</w>, tạo ra một token mảnh từ hoàn chỉnh dùng chung cho cả "lowest" và "newest".
8.2 Bản đồ ngữ nghĩa: Khái niệm Word Embeddings
Sau khi Tokenizer biến câu văn thành một danh sách các ID số nguyên: $$\text{"Tôi yêu AI"} \to [102, 540, 891]$$ Làm thế nào để truyền tải thông tin này vào mạng nơ-ron?
Cách tiếp cận đơn giản nhất là One-Hot Encoding: Tạo ra một vector có độ dài bằng toàn bộ từ điển $V$, đặt số 1 tại vị trí ID của từ và số 0 tại tất cả các vị trí còn lại. Tuy nhiên, cách này có hai điểm yếu chí mạng:
- Ma trận thưa thớt cực kỳ lãng phí RAM: Nếu từ điển có $50.000$ từ, mỗi từ đơn lẻ sẽ là một vector $50.000$ chiều chứa toàn số 0.
- Mất mát hoàn toàn tính liên kết ngữ nghĩa: Hai vector One-Hot bất kỳ $v_a$ và $v_b$ luôn vuông góc với nhau trong không gian vector. Do đó tích vô hướng của chúng bằng 0: $$v_a \cdot v_b = 0$$ Điều này đồng nghĩa với việc máy tính coi từ "mèo" và "chó" hoàn toàn độc lập, xa lạ y hệt như từ "mèo" và "chiếc bàn".
Trong PyTorch, lớp
nn.Embedding(num_embeddings, embedding_dim) thực chất là một bảng tra
cứu (Lookup Table) khổng lồ chứa một ma trận trọng số khả nạp (trainable weight matrix) kích thước $V
\times d$. Khi ta nạp một Token ID là số nguyên $k$, lớp này không thực hiện phép nhân ma trận phức tạp nào cả, mà chỉ đơn giản truy xuất trực tiếp và trả về dòng thứ $k$ của ma trận trọng số. Phép toán này cực kỳ nhanh ($\mathcal{O}(1)$) và tương đương về mặt toán học với việc nhân một vector One-Hot với ma trận trọng số của lớp nhúng: $$\text{Embedding}(k) = \text{OneHot}(k) \times W_{\text{embed}}$$
Nhờ việc huấn luyện đồng thời trên lượng văn bản khổng lồ, ma trận nhúng tự động dịch chuyển tọa độ của các từ sao cho: Các từ xuất hiện trong ngữ cảnh tương đồng sẽ nằm gần nhau trong không gian đa chiều. Đây chính là tiền đề tạo nên khả năng tính toán tương tự ngữ nghĩa kinh điển: $$\text{Embedding("Vua")} - \text{Embedding("Nam")} + \text{Embedding("Nữ")} \approx \text{Embedding("Hoàng hậu")}$$
8.3 Đo lường khoảng cách ngữ nghĩa: Cosine Similarity
Khi các từ đã được biểu diễn thành các vector trong không gian đa chiều, làm thế nào để đo lường mức độ tương đồng ngữ nghĩa giữa chúng?
Chúng ta không thể dùng khoảng cách hình học thông thường (Khoảng cách Euclid - L2 Distance) để so sánh trực tiếp, bởi vì khoảng cách Euclid nhạy cảm với độ dài (norm) của vector. Trong các tác vụ so sánh câu văn, một câu dài có thể chứa các vector tổng hợp có độ dài cực lớn, làm khoảng cách Euclid lệch đi rất nhiều so với câu ngắn mặc dù chúng có cùng chung chủ đề ngữ nghĩa.
Thay vào đó, chúng ta đo góc giữa hai vector bằng công thức toán học Cosine Similarity (Độ tương đồng Cosine):
- Giá trị kết quả nằm trong đoạn $[-1, 1]$.
- Nếu kết quả bằng $1$: Hai vector hoàn toàn cùng hướng (góc $0^\circ$), chỉ ra sự tương đồng tuyệt đối.
- Nếu kết quả bằng $0$: Hai vector vuông góc ($90^\circ$), không có mối liên hệ ngữ nghĩa nào.
- Nếu kết quả bằng $-1$: Hai vector ngược hướng hoàn toàn ($180^\circ$).
Ví dụ: Hai từ trái nghĩa hoàn toàn như "nóng" và "lạnh" thường có điểm Cosine cực kỳ cao vì chúng cùng xuất hiện trong các câu nói về thời tiết, nhiệt độ. Hãy lưu ý điều này khi thiết kế các bộ lọc từ khóa đồng nghĩa thực tế.
8.4 Dự án thực hành bài 8: Trình tìm từ đồng nghĩa (Word2Vec) với PyTorch
Dự án thực hành của bài học này là tự tay lập trình một script Python sử dụng ma trận trọng số
nn.Embedding giả lập hệ thống Word2Vec thu nhỏ. Chương trình nhận vào một từ khóa tiếng Việt,
chuyển thành vector nhúng, tính toán Cosine Similarity với toàn bộ từ vựng trong từ điển để lọc ra top 3
từ tương đồng ngữ nghĩa nhất.
import torch
import torch.nn as nn
# 1. Định nghĩa từ điển hệ thống (Vocabulary)
vocab = {
"vua": 0,
"hoàng_hậu": 1,
"nam": 2,
"nữ": 3,
"máy_tính": 4,
"lập_trình": 5,
"trí_tuệ_nhân_tạo": 6,
"cà_phê": 7,
"trà": 8
}
inverse_vocab = {v: k for k, v in vocab.items()}
# 2. Khởi tạo ma trận nhúng giả lập với số chiều d = 4
# Trọng số được thiết kế thủ công để mô tả mối quan hệ ngữ nghĩa rõ nét:
# Chiều 0: Hoàng gia, Chiều 1: Giới tính (Dương: Nam, Âm: Nữ), Chiều 2: Công nghệ, Chiều 3: Đồ uống.
embedding_weights = torch.tensor([
[1.0, 0.9, 0.0, 0.0], # vua
[1.0, -0.9, 0.0, 0.0], # hoàng_hậu
[0.0, 1.0, 0.0, 0.0], # nam
[0.0, -1.0, 0.0, 0.0], # nữ
[0.0, 0.0, 1.0, 0.0], # máy_tính
[0.0, 0.0, 0.9, 0.0], # lập_trình
[0.0, 0.1, 1.0, 0.0], # trí_tuệ_nhân_tạo
[0.0, 0.0, 0.0, 1.0], # cà_phê
[0.0, 0.0, 0.0, 0.9] # trà
], dtype=torch.float32)
# 3. Tạo lớp nn.Embedding của PyTorch và gán trọng số
vocab_size, embedding_dim = embedding_weights.shape
embed = nn.Embedding(num_embeddings=vocab_size, embedding_dim=embedding_dim)
# Gán trọng số cố định không huấn luyện thêm (eval mode)
embed.weight = nn.Parameter(embedding_weights, requires_grad=False)
def calculate_cosine_similarity(vector_a, matrix_b):
# vector_a: (1, d)
# matrix_b: (V, d)
# Tích vô hướng từng phần tử (Dot product): (V,)
dot_product = torch.sum(vector_a * matrix_b, dim=1)
# Tính Norm L2
norm_a = torch.norm(vector_a, p=2, dim=1) # (1,)
norm_b = torch.norm(matrix_b, p=2, dim=1) # (V,)
# Tránh chia cho 0
similarity = dot_product / (norm_a * norm_b + 1e-8)
return similarity
def find_most_similar(target_word, top_n=3):
if target_word not in vocab:
print(f"Từ '{target_word}' không có trong từ điển.")
return
target_idx = vocab[target_word]
# Lấy vector nhúng của từ mục tiêu (1, d)
target_vector = embed(torch.tensor([target_idx]))
# Lấy toàn bộ ma trận trọng số nhúng (V, d)
all_vectors = embed.weight
# Tính điểm tương đồng Cosine của từ mục tiêu với toàn bộ từ điển
scores = calculate_cosine_similarity(target_vector, all_vectors)
# Sắp xếp điểm số từ cao xuống thấp
top_scores, top_indices = torch.topk(scores, k=len(vocab))
print(f"--- Top các từ tương đồng nhất với '{target_word}': ---")
count = 0
for score, idx in zip(top_scores.tolist(), top_indices.tolist()):
word = inverse_vocab[idx]
# Bỏ qua chính từ đang truy vấn
if word == target_word:
continue
print(f"{count+1}. {word:<20} | Cosine Score: {score:.4f}")
count += 1
if count >= top_n:
break
print()
if __name__ == "__main__":
# Tìm kiếm từ đồng nghĩa đơn giản
find_most_similar("cà_phê", top_n=2)
find_most_similar("máy_tính", top_n=2)
# 4. Thực hành tính đại số vector ngữ nghĩa kinh điển:
# Vector lý thuyết: Vua - Nam + Nữ
vua_idx = torch.tensor([vocab["vua"]])
nam_idx = torch.tensor([vocab["nam"]])
nu_idx = torch.tensor([vocab["nữ"]])
analogy_vector = embed(vua_idx) - embed(nam_idx) + embed(nu_idx) # (1, 4)
# So sánh vector kết quả với toàn bộ từ điển
analogy_scores = calculate_cosine_similarity(analogy_vector, embed.weight)
best_idx = torch.argmax(analogy_scores).item()
print("=== Phép toán đại số từ vựng (Word Analogy) ===")
print("Công thức: Vua - Nam + Nữ")
print(f"Từ có vector gần nhất trong từ điển: '{inverse_vocab[best_idx]}' (Điểm Cosine: {analogy_scores[best_idx]:.4f})")
# --- Kết quả in ra thực tế (đã chạy kiểm chứng) ---
# --- Top các từ tương đồng nhất với 'cà_phê': ---
# 1. trà | Cosine Score: 1.0000
# 2. vua | Cosine Score: 0.0000
#
# --- Top các từ tương đồng nhất với 'máy_tính': ---
# 1. lập_trình | Cosine Score: 1.0000
# 2. trí_tuệ_nhân_tạo | Cosine Score: 0.9950
#
# === Phép toán đại số từ vựng (Word Analogy) ===
# Công thức: Vua - Nam + Nữ
# Từ có vector gần nhất trong từ điển: 'hoàng_hậu' (Điểm Cosine: 0.9950)
Kết quả chạy thực tế xác nhận đúng công thức đại số ngữ nghĩa đã nêu ở mục 8.2: Vua − Nam + Nữ ≈ Hoàng hậu, với điểm Cosine đạt $0.9950$ — gần tuyệt đối. Đáng chú ý, "cà_phê" và "trà" đạt điểm Cosine chính xác bằng $1.0000$ dù không phải từ đồng nghĩa hoàn toàn, vì trong ma trận nhúng giả lập này hai từ chỉ khác nhau ở độ dài vector (0.9 so với 1.0) chứ không khác hướng — đây chính là hệ quả trực tiếp của cạm bẫy đã nêu ở mục 8.3: Cosine Similarity chỉ đo góc, hoàn toàn bỏ qua độ dài, nên hai vector cùng hướng nhưng khác độ lớn vẫn cho điểm tương đồng tuyệt đối.
Tóm tắt bài học & Cầu nối kiến thức
- Đạt được: Hiểu cách Tokenization chia nhỏ văn bản và cách thuật toán Word2Vec nhúng từ vựng vào không gian vector đa chiều.
- Đạt được: Tự lập trình bộ tìm kiếm từ đồng nghĩa dựa trên khoảng cách Cosine Similarity giữa các vector embeddings.
Cầu nối bài tiếp theo: Các vector từ đơn lẻ chưa thể hiện được thứ tự của câu văn. Để xử lý các chuỗi ngôn ngữ dài theo thời gian, chúng ta cần cơ chế mạng tuần hoàn RNN và cơ chế Attention ở Bài số 9.
Tải file code thực hành minh họa bài học
File Python word_similarity.py — mã nguồn khởi tạo ma trận nhúng, định nghĩa từ điển tiếng
Việt giả lập và tính toán độ tương đồng Cosine (chạy python word_similarity.py, yêu cầu cài
đặt thư viện torch):
📖 Tài liệu tham khảo
- CS224n: Natural Language Processing with Deep Learning — Tài liệu giảng dạy chuyên sâu về Word Embeddings và mô hình ngôn ngữ (Stanford University)
- PyTorch nn.Embedding Documentation — Tài liệu kỹ thuật chi tiết cấu hình tham số lớp nhúng vector (Official Docs)
- Cosine Similarity pairwise metrics — Hướng dẫn thuật toán đo khoảng cách tương đồng (Scikit-Learn Docs)
Bình luận