Chuẩn hóa văn bản
Mục tiêu
Phần tiêu đề “Mục tiêu”Sau bài này, bạn sẽ:
- Hiểu vì sao hai chuỗi trông giống hệt nhau vẫn có thể khác nhau, và dùng chuẩn hóa Unicode (Unicode normalization) NFC, NFD để gộp chúng. Nhận ra chuyện dấu thanh đặt ở hai chỗ (“hòa” và “hoà”) là một vấn đề khác, Unicode không tự giải quyết.
- Cài đặt được một pipeline chuẩn hóa văn bản (text normalization): chữ thường, khoảng trắng, URL, email, số, dấu câu. Biết vì sao thứ tự các bước quan trọng.
- Phân biệt stemming và lemmatization, biết vì sao tiếng Việt gần như không cần chúng. Dùng stopword cẩn thận.
- Nhận ra khi nào không nên chuẩn hóa mạnh: mô hình subword, nhận dạng thực thể, phân tích cảm xúc.
Cần biết trước
Phần tiêu đề “Cần biết trước”- Tách từ (Tokenizer), thuộc phần Xử lý ngôn ngữ tự nhiên (Đã học)
Trực giác
Phần tiêu đề “Trực giác”Hai chữ “Hòa” không bằng nhau
Phần tiêu đề “Hai chữ “Hòa” không bằng nhau”Bạn có một bảng khách hàng. Bạn gõ “Hòa” vào ô tìm kiếm, và không ra kết quả nào. Trong khi đó, chữ “Hòa” nằm ngay trên màn hình.
Lý do nằm ở chỗ máy không lưu “chữ” như mắt ta thấy. Máy lưu một dãy số, mỗi số là một code point của Unicode. Và chữ “ò” có hai cách ghi:
| Cách ghi | Trông như | Dãy code point | len |
|---|---|---|---|
| “ò” dựng sẵn | Hòa | U+0048 U+00F2 U+0061 | 3 |
| “o” cộng dấu huyền rời | Hòa | U+0048 U+006F U+0300 U+0061 | 4 |
Hai dòng hiện ra giống hệt nhau, nhưng là hai dãy số khác nhau. Phép == của Python so từng code point, nên trả về False.
Còn một chuyện nữa. Có người viết “Hòa”, có người viết “Hoà”: dấu huyền đặt trên “o” hay trên “a”. Lần này mắt người thấy được sự khác biệt, nhưng ai cũng hiểu đó là cùng một chữ. Với máy, đây lại là hai chuỗi khác nhau, vì một bên có “ò”, bên kia có “à”.
Một nhận xét, nhiều cách viết
Phần tiêu đề “Một nhận xét, nhiều cách viết”Văn bản trên mạng còn lộn xộn hơn nhiều. Ba nhận xét sau cùng một ý:
- “Sp OK lắm!!!”
- “sp ok lắm”
- “SP ok lắm !”
Một mô hình đếm từ, như bag-of-words (biểu diễn văn bản bằng số lần xuất hiện của từng từ) hay TF-IDF (bài Vector Semantics và Embeddings), thấy “OK”, “ok” và “Sp”, “sp”, “SP” là năm từ khác nhau. Số lần xuất hiện của một ý bị chia nhỏ ra nhiều “từ”, mỗi từ có ít dữ liệu hơn. Từ vựng phình to mà không thêm thông tin gì.
Nhưng gộp quá tay cũng hỏng. Bỏ hết dấu thì “mà” (liên từ) và “má” (mẹ) thành một. Bỏ chữ “không” vì nó “quá phổ biến” thì “sản phẩm không tốt” thành “sản phẩm tốt”.
Bài toán của bài này là: chọn một cách biến mọi cách viết của cùng một thứ về một dạng duy nhất, mà không gộp nhầm những thứ khác nhau, và dùng đúng cách đó ở mọi nơi.
Lý thuyết
Phần tiêu đề “Lý thuyết”Chuẩn hóa là một hàm
Phần tiêu đề “Chuẩn hóa là một hàm”Gọi là hàm chuẩn hóa: nhận một chuỗi, trả về một chuỗi. Ta muốn có ba tính chất.
- Gộp đủ. Hai chuỗi có cùng ý nghĩa với bài toán của ta thì cho cùng kết quả. Viết cho ” và là cùng một thứ”, ta muốn:
- Không gộp nhầm. Nếu và khác nghĩa, thì . Hàm bỏ dấu vi phạm điều này: .
- Lũy đẳng (idempotent): chuẩn hóa thêm lần nữa không đổi gì, . Nhờ vậy bạn không phải nhớ một chuỗi đã chuẩn hóa chưa.
Hai yêu cầu đầu kéo về hai phía. Hàm càng mạnh thì càng gộp đủ, nhưng càng dễ gộp nhầm. Chữ “cùng một thứ” lại tùy bài toán: với công cụ tìm kiếm, “Hóa” và “hoa” có thể coi là một; với bộ nhận dạng tên riêng thì không. Vì vậy không có một hàm chuẩn hóa “đúng” cho mọi việc.
Có một quy tắc không đổi: lúc huấn luyện và lúc dùng phải áp cùng một . Mô hình chỉ biết những chuỗi nó đã thấy sau .
Unicode: chữ dựng sẵn và dấu tổ hợp
Phần tiêu đề “Unicode: chữ dựng sẵn và dấu tổ hợp”Unicode gán cho mỗi ký tự một số gọi là code point, viết dạng U+ cộng số hệ mười sáu. Chữ tiếng Việt có dấu có hai cách ghi:
- Ký tự dựng sẵn (precomposed character): một code point cho cả chữ lẫn dấu, ví dụ “ò” là U+00F2. Unicode có sẵn mọi chữ có dấu của tiếng Việt.
- Dấu tổ hợp (combining mark): chữ gốc, theo sau là một hay nhiều code point dấu, ví dụ “o” U+006F rồi dấu huyền tổ hợp U+0300. Chữ mang hai dấu, như “ệ”, thành ba code point: “e”, dấu nặng U+0323, dấu mũ U+0302.
Hai cách ghi này là tương đương chuẩn (canonical equivalence): theo chuẩn Unicode, chúng là cùng một ký tự và phải hiện ra như nhau. Văn bản dạng tổ hợp sinh ra từ nhiều nguồn: bảng mã “Unicode tổ hợp” của bộ gõ Unikey, mã Windows-1258, văn bản chép từ một số phần mềm hay file PDF. Bạn không chọn được dữ liệu của mình đến từ đâu.
Phụ lục số 15 của chuẩn Unicode (UAX #15) định nghĩa bốn dạng chuẩn (normalization form):
| Dạng | Làm gì | Ví dụ |
|---|---|---|
| NFD | Tách hết thành chữ gốc cộng dấu tổ hợp | “ò” → “o” + U+0300 |
| NFC | Tách như NFD, rồi ghép lại thành ký tự dựng sẵn khi có | “o” + U+0300 → “ò” |
| NFKD | Như NFD, thêm các phép đổi tương thích | “fi” → “f” “i” |
| NFKC | Như NFC, thêm các phép đổi tương thích | “²” → “2”, “A” (chữ rộng) → “A” |
Hai chuỗi tương đương chuẩn luôn cho cùng kết quả NFC, và cùng kết quả NFD. Đó đúng là tính chất “gộp đủ” ở trên. Vì NFC và NFD chỉ gộp những thứ mà Unicode coi là một, chúng không gộp nhầm. NFC là bước nên làm đầu tiên với mọi văn bản tiếng Việt. Nhiều văn bản trên web đã ở sẵn dạng NFC, và Unicode FAQ về chuẩn hóa gọi NFC là dạng tốt nhất cho văn bản thông thường. UAX #15 cũng ghi rằng W3C khuyên dùng NFC cho mọi nội dung web.
Tương đương tương thích (compatibility equivalence) thì lỏng hơn: các ký tự “giống nhau về ý” nhưng có thể khác về hình hay cách dùng. NFKC biến “x²” thành “x2”, tức là có mất thông tin. Bộ tách từ SentencePiece mặc định dùng nmt_nfkc, một biến thể của NFKC có gộp thêm khoảng trắng (tài liệu chuẩn hóa của SentencePiece); bạn sẽ gặp nó ở bài Tách từ (Tokenizer).
Vị trí dấu thanh: kiểu cũ và kiểu mới
Phần tiêu đề “Vị trí dấu thanh: kiểu cũ và kiểu mới”NFC giải quyết chuyện “ò” ghi bằng một hay hai code point. Nó không giải quyết chuyện “Hòa” và “Hoà”, vì đó là hai chữ khác nhau ở mức Unicode: một bên có “ò” (U+00F2), bên kia có “à” (U+00E0). Cả hai đều đã ở dạng NFC.
Tiếng Việt có hai thói quen đặt dấu thanh cùng tồn tại:
- Kiểu cũ đặt dấu gần giữa âm tiết, vì đẹp mắt: “hòa”, “khỏe”, “thủy”. Đa số người vẫn dùng kiểu này khi viết hằng ngày.
- Kiểu mới đặt dấu trên âm chính (nguyên âm mang thanh điệu): “hoà”, “khoẻ”, “thuỷ”. Sách giáo khoa của Nhà xuất bản Giáo dục thường dùng kiểu này.
Cách gọi và hai ví dụ trên theo mục về dấu thanh của bài Vietnamese alphabet trên Wikipedia.
Hai kiểu chỉ khác nhau ở vài vần, chủ yếu là oa, oe, uy khi đứng cuối âm tiết. Khi vần có âm cuối, như “hoàng”, “khoảng”, “thuyền”, cả hai kiểu đều đặt dấu như nhau. Với “qu”, chữ “u” thuộc phụ âm đầu, nên “quý” chỉ có một cách viết. Nguyên âm có dấu phụ (ê, ơ, ô…) luôn mang dấu thanh ở cả hai kiểu: “thuế”, không có “thúê”.
Muốn gộp, ta cần một quy tắc riêng. Mẹo là dùng chính NFD:
- Đưa chuỗi về NFD: dấu thanh tách thành một code point rời. Với o, u, y không có dấu phụ (đúng các chữ quy tắc xử lý), dấu thanh nằm ngay sau nguyên âm. Chữ có dấu phụ thì khác: “ồ” ở NFD là “o” + U+0302 + U+0300, nên quy tắc không đụng tới.
- Dùng regex tìm “o” + dấu thanh + “a” hoặc “e” ở cuối âm tiết, và “u” + dấu thanh + “y” ở cuối âm tiết. Đổi chỗ dấu và nguyên âm sau.
- Đưa về NFC.
Bạn chọn kiểu nào làm đích cũng được. Điều quan trọng là cả kho dữ liệu dùng một kiểu. Bài này đưa về kiểu mới. Quy tắc trong ô code chỉ là một tập con (oa, oe, uy không có âm cuối), đủ cho các chữ hay gặp như hòa, hóa, khỏe, thủy, lũy, nhưng không phải bộ chuyển đổi đầy đủ.
Chữ hoa, khoảng trắng, URL và số
Phần tiêu đề “Chữ hoa, khoảng trắng, URL và số”Chữ hoa thường. Đưa về chữ thường làm “OK”, “Ok”, “ok” thành một. Python có hai hàm: str.lower() và str.casefold(). Với tiếng Việt chúng cho cùng kết quả. Khác biệt nằm ở vài chữ của ngôn ngữ khác: "ß".lower() giữ nguyên “ß”, còn "ß".casefold() cho “ss”. casefold được làm ra cho việc so khớp không phân biệt hoa thường, nên dùng nó.
Khoảng trắng. Văn bản có thể chứa nhiều dấu cách liền nhau, tab, xuống dòng, hay dấu cách không ngắt (U+00A0) chép từ trang web. " ".join(s.split()) gộp hết thành một dấu cách và bỏ khoảng trắng ở hai đầu, vì split() không tham số tách ở mọi loại khoảng trắng của Unicode. Riêng ký tự rộng 0 (U+200B) không được Unicode coi là khoảng trắng, nên vẫn còn lại.
Token giữ chỗ (placeholder token). Mỗi URL, email, số điện thoại gần như là duy nhất. Giữ nguyên thì mỗi cái thành một “từ” chỉ xuất hiện một lần, không giúp mô hình học gì. Thay chúng bằng <url>, <email>, <num> thì mô hình học được điều hữu ích hơn: “ở đây có một đường link”. Ta làm việc này bằng regex. Thứ tự quan trọng: URL và email chứa chữ số, nên phải thay chúng trước khi thay số.
Dấu câu. Bỏ dấu câu làm “lắm!!!” và “lắm” thành một. Nhưng ”!!!” nói lên mức độ, còn ”:(” là cả một cảm xúc. Bỏ hay giữ tùy bài toán (xem mục cuối của phần này).
Stemming và lemmatization
Phần tiêu đề “Stemming và lemmatization”Tiếng Anh biến hình (inflection): một từ đổi dạng theo thì, số, ngôi. “run”, “runs”, “running”, “ran” là bốn dạng của một động từ. Với công cụ tìm kiếm, người gõ “running” có lẽ cũng muốn tìm “runs”. Có hai cách gộp:
- Stemming cắt hậu tố theo quy tắc, không cần từ điển. Thuật toán của Porter (1980) là bộ stemmer tiếng Anh nổi tiếng nhất: một chuỗi các luật kiểu “bỏ -ing”, “đổi -ies thành -i”. Kết quả là một gốc (stem), không nhất thiết là từ có thật.
- Lemmatization đưa từ về dạng từ điển (lemma), dùng từ điển và từ loại. “ran” về “run”, “was” về “be”, “mice” về “mouse”, “better” (tính từ) về “good”.
Bảng dưới là kết quả của Porter, chạy bằng bản cài đặt trong thư viện NLTK:
| Từ | Porter | Lemma |
|---|---|---|
| running, runs | run | run |
| ran | ran | run |
| studies, studying | studi | study |
| was | wa | be |
| university, universe | univers | university, universe |
| mice | mice | mouse |
Stemming nhanh và không cần tài nguyên, nhưng sai theo hai chiều. Nó gộp nhầm (overstemming): “university” và “universe” khác nghĩa mà cùng thành “univers”. Nó gộp thiếu (understemming): “ran” không có hậu tố nào để cắt, nên không về được “run”. Lemmatization chính xác hơn, đổi lại phải có từ điển và bộ gán từ loại cho từng ngôn ngữ.
Tiếng Việt gần như không cần cả hai. Tiếng Việt là ngôn ngữ đơn lập (isolating language): từ không đổi dạng. Thì và số được nói bằng từ riêng: “đã chạy”, “đang chạy”, “sẽ chạy”; “con mèo”, “những con mèo”. Chữ “chạy” giữ nguyên trong mọi câu. Khó khăn của tiếng Việt nằm ở chỗ khác: một từ có thể gồm nhiều âm tiết (“học sinh”, “máy tính”), nên phải tách từ đúng. Bài Tách từ (Tokenizer) bàn chuyện này.
Stopword
Phần tiêu đề “Stopword”Stopword là những từ rất phổ biến mà mang ít nội dung: “the”, “is”, “of” trong tiếng Anh; “thì”, “là”, “của”, “các” trong tiếng Việt. Bỏ chúng làm văn bản ngắn lại, và trong truy xuất theo chủ đề, nó giúp TF-IDF tập trung vào từ mang nội dung.
Nhưng “ít nội dung” là tùy bài toán. Nhiều danh sách stopword có “không”, “chẳng”, “chưa”, “rất”. Với phân tích cảm xúc, bỏ chúng là thảm họa: “sản phẩm không tốt” thành “sản phẩm tốt”, “không tệ” thành “tệ”. Trước khi dùng một danh sách stopword có sẵn, hãy đọc nó và tự hỏi từng từ có quan trọng với bài toán của bạn không.
Bỏ dấu tiếng Việt
Phần tiêu đề “Bỏ dấu tiếng Việt”Bỏ dấu là phép chuẩn hóa mạnh nhất với tiếng Việt. Sáu chữ “ma, má, mà, mả, mã, mạ” (ma quỷ, mẹ, liên từ, mồ mả, mã số, mạ lúa) đều thành “ma”. “đ” thành “d” nên “đi” và “di” (trong “di chuyển”) cũng thành một.
- Có ích khi người dùng hay gõ không dấu, nhất là ô tìm kiếm. Áp cho cả chỉ mục lẫn câu truy vấn khi so khớp, và giữ bản có dấu để hiển thị.
- Có hại khi đưa văn bản vào mô hình hiểu nghĩa: mô hình mất đúng thông tin mà dấu mang theo.
Khi nào không nên chuẩn hóa mạnh
Phần tiêu đề “Khi nào không nên chuẩn hóa mạnh”Các bước trên đến từ thời mô hình đếm từ, khi mỗi dạng viết là một cột riêng trong ma trận. Mô hình hiện đại thay đổi bức tranh:
- Mô hình subword và LLM học từ văn bản thô. Bộ tách từ subword chia được gần như mọi chuỗi thành các mảnh nó đã biết (BPE mức byte thì chia được mọi chuỗi), nên hầu như không còn “từ lạ”, và mô hình tự học rằng “OK” và “ok” gần nhau. Chuẩn hóa mạnh còn làm mất tín hiệu mà mô hình dùng được. Với các mô hình này, bạn thường chỉ chuẩn hóa nhẹ: NFC, gộp khoảng trắng, và để nguyên phần còn lại cho bộ tách từ của chính mô hình.
- Chữ hoa mang thông tin. “Hòa” trong “chị Hòa” là tên người, “hòa” trong “hòa nhau” là động từ. Với nhận dạng thực thể, chữ hoa là một trong những manh mối mạnh nhất. README của BERT ghi rằng bản “uncased” thường tốt hơn, trừ khi chữ hoa quan trọng với tác vụ, như nhận dạng thực thể hay gán nhãn từ loại. README cũng ghi bản uncased bỏ luôn mọi dấu, nên nó không hợp với tiếng Việt; bản đa ngôn ngữ “cased” giữ dấu.
- Phân tích cảm xúc. ”!!!”, ”:(”, chữ “CHẬM” viết hoa để nhấn mạnh, và các từ phủ định đều mang cảm xúc.
Giới hạn
Phần tiêu đề “Giới hạn”- Quy tắc dấu thanh trong bài chỉ là tập con. Bộ chuyển đổi đầy đủ phải biết cấu trúc âm tiết (phụ âm đầu, âm đệm, âm chính, âm cuối) để không đụng vào các vần như “ua” trong “của”. Quy tắc cũng phải chạy trước bước tách từ: trên văn bản đã tách từ kiểu
hòa_bình, dấu_là\w, nên\bkhông khớp sau “a” và chữ giữ nguyên. - Teencode và viết tắt (“ko”, “dc”, “sp”) cần một từ điển do người làm. Regex không đoán được “ko” là “không”.
- Regex cho URL, số không hoàn hảo. Ở ô thứ hai, “10đ” thành
<num> đ: con số mất, đơn vị tách ra. Nếu giá tiền hay điểm số quan trọng, đừng thay số.
Minh họa
Phần tiêu đề “Minh họa”Bảng dưới là đầu ra của ô thứ hai ở mục Ví dụ code, với câu “Shop của chị Hòa KHÔNG tệ, giá 1.250.000đ.” Chữ “Hòa” của câu đầu vào được ghi ở dạng NFD. Hãy nhìn cột len để thấy những thay đổi mắt không thấy được. Trình duyệt tự gộp khoảng trắng, nên trong bảng, mọi dấu cách nằm trong một cụm khoảng trắng liền nhau (tính cả tab) hoặc ở cuối chuỗi được vẽ thành ␣, còn tab (\t trong output) vẽ thành → (giữa “tệ,” và “giá” là một tab).
| Bước | len | Chuỗi sau bước |
|---|---|---|
| (đầu vào) | 44 | Shop của chị Hòa␣␣KHÔNG tệ,→giá 1.250.000đ. |
| NFC | 43 | Shop của chị Hòa␣␣KHÔNG tệ,→giá 1.250.000đ. |
| chữ thường | 43 | shop của chị hòa␣␣không tệ,→giá 1.250.000đ. |
| token giữ chỗ | 41 | shop của chị hòa␣␣không tệ,→giá␣␣<num> đ. |
| bỏ dấu câu | 41 | shop của chị hòa␣␣không tệ␣→giá␣␣<num> đ␣ |
| gộp khoảng trắng | 37 | shop của chị hòa không tệ giá <num> đ |
| bỏ dấu (tắt mặc định) | 37 | shop cua chi hoa khong te gia <num> d |
Đọc từng dòng:
- NFC không đổi gì trên màn hình, nhưng
lengiảm từ 44 xuống 43: “o” cộng dấu huyền rời đã ghép thành “ò”. - Token giữ chỗ thay “1.250.000” bằng
<num>, có thêm dấu cách hai bên để token không dính vào chữ bên cạnh. Chữ “đ” bị tách ra, đúng như mục Giới hạn đã nói. - Bỏ dấu câu thay mỗi dấu câu bằng một dấu cách, nên
lenkhông đổi. Bước gộp khoảng trắng ở cuối dọn hết. - Bỏ dấu làm “hòa” thành “hoa” và “không tệ” thành “khong te”. Người vẫn đọc được câu này, nhưng tên “Hòa” giờ trùng với “hoa” (bông hoa).
Hai câu còn lại cho thấy cái giá của các bước:
| Đầu vào | Sau pipeline (mặc định) |
|---|---|
Sp OK lắm!!!␣␣␣10đ, xem tại https://example.vn/sp?id=123 | sp ok lắm <num> đ xem tại <url> |
Giao hàng CHẬM quá :(␣␣Gọi 0912.345.678 hoặc hotro@example.vn | giao hàng chậm quá gọi <num> hoặc <email> |
Câu thứ hai mất cả chữ “CHẬM” viết hoa lẫn ”:(”. Với bộ đếm từ dùng để phân loại chủ đề, đó là dọn dẹp tốt. Với bộ phân tích cảm xúc, đó là hai tín hiệu tiêu cực bị xóa.
Ví dụ code
Phần tiêu đề “Ví dụ code”Code trong bài chạy thử lần cuối tháng 11/2026 trên Pyodide 314.0.7 (Python 3.14), chỉ dùng hai mô-đun có sẵn re và unicodedata. Cả hai ô chạy ngay trên trang và chạy độc lập.
NFC, NFD và vị trí dấu thanh
Phần tiêu đề “NFC, NFD và vị trí dấu thanh”Ô dưới tạo hai chữ “Hòa”: a dùng “ò” dựng sẵn, b dùng “o” cộng dấu huyền rời. Nó in kết quả so sánh, độ dài, dãy code point, rồi đưa về NFC và NFD. Phần sau cài quy tắc dời dấu thanh sang kiểu mới cho vần oa, oe, uy ở cuối âm tiết.
Hai chữ Hòa: NFC, NFD và vị trí dấu thanh
# Hai chữ "Hòa" trông giống hệt nhau: NFC, NFD và vị trí dấu thanhimport reimport unicodedata as ud
a = "H\u00f2a" # "ò" dựng sẵn: một code pointb = "Ho\u0300a" # "o" cộng dấu huyền tổ hợp: hai code pointprint(a, b, a == b)print(len(a), len(b))for s in (a, b): print([f"U+{ord(ch):04X}" for ch in s])print(ud.name("\u0300"))
# Đưa về cùng một dạng chuẩn thì hai chuỗi bằng nhauprint(ud.normalize("NFC", b) == a, ud.normalize("NFD", a) == b)
# Cùng dạng NFC nhưng dấu đặt ở chỗ khác: vẫn là hai chuỗi khác nhauc = "Ho\u00e0"print(a, c, a == c)
# Năm dấu thanh ở dạng tổ hợp: huyền, sắc, ngã, hỏi, nặngTONE = "\u0300\u0301\u0303\u0309\u0323"# Tập con: vần oa, oe, uy đứng cuối âm tiết (hòa, khỏe, thủy).# Dời dấu từ nguyên âm đầu sang nguyên âm sau (kiểu "hoà")RULES = [ re.compile(rf"([oO])([{TONE}])([aAeE])\b"), re.compile(rf"([uU])([{TONE}])([yY])\b"),]
def tone_on_main_vowel(text): s = ud.normalize("NFD", text) # tách dấu ra khỏi chữ for rule in RULES: s = rule.sub(r"\1\3\2", s) # đổi chỗ dấu và nguyên âm sau return ud.normalize("NFC", s) # ghép lại thành chữ dựng sẵn
text = "Hòa thuận, khỏe mạnh, thủy thủ, HÓA ĐƠN, mùa hoàng hôn"print(tone_on_main_vowel(text))print(tone_on_main_vowel(a) == tone_on_main_vowel(c))Lần chạy đầu cần tải Python, khoảng 10 MB.
Đọc kết quả:
Hòa Hòa Falserồi3 4. Hai chuỗi in ra giống hệt nhau, nhưng==trả vềFalse, vàbdài hơn một code point. Dãy code point cho thấy chỗ khác:U+00F2ởa,U+006F U+0300ởb. Tên của U+0300 làCOMBINING GRAVE ACCENT, tức dấu huyền tổ hợp.True True. NFC củabbằnga, và NFD củaabằngb. Đưa cả hai về cùng một dạng là đủ để so sánh đúng.Hòa Hoà False.ccũng ở dạng NFC nhưng mang “à”. Không dạng chuẩn nào của Unicode gộp đượcavàc.- Câu mẫu thành “Hoà thuận, khoẻ mạnh, thuỷ thủ, HOÁ ĐƠN, mùa hoàng hôn”. Bốn chữ đổi: hòa, khỏe, thủy, HÓA (quy tắc dùng
[oO],[aA]nên chạy cả với chữ hoa). Ba chữ giữ nguyên, đúng như mong muốn: “thủ” (không phải vần uy), “mùa” (vần ua không nằm trong quy tắc), “hoàng” (có âm cuối nên hai kiểu như nhau). Dòng cuốiTruecho thấy sau quy tắc, “Hòa” và “Hoà” đã thành một.
Vì sao quy tắc chạy trên NFD? Ở dạng NFC, “ò” là một ký tự, và muốn dời dấu thì phải có bảng tra từng cặp (“ò” thành “o”, “a” thành “à”…). Ở dạng NFD, dấu là một code point riêng nằm ngay sau “o”, nên chỉ cần đổi chỗ hai code point bằng nhóm bắt của regex: \1\3\2. Đây là một lý do NFD có ích, dù dữ liệu cuối cùng luôn nên lưu ở NFC.
Pipeline bật/tắt từng bước
Phần tiêu đề “Pipeline bật/tắt từng bước”Ô thứ hai là một pipeline gồm sáu bước, mỗi bước là một hàm nhỏ. Từ điển ON ở đầu file cho bật hoặc tắt từng bước. Với mỗi câu, ô in tên bước, len và chuỗi sau bước đó (dạng repr, nên bạn thấy được \t và dấu cách thừa). Bảng ở mục Minh họa chép từ output này.
Pipeline chuẩn hóa có thể bật/tắt từng bước
# Pipeline chuẩn hóa văn bản: bật/tắt từng bước rồi xem trước và sauimport reimport unicodedata as ud
# Đổi True/False để bật hoặc tắt từng bướcON = {"nfc": True, "lower": True, "slot": True, "punct": True, "space": True, "strip_tone": False}
URL = re.compile(r"https?://\S+|www\.\S+")EMAIL = re.compile(r"[\w.+-]+@[\w-]+(?:\.[\w-]+)+")NUM = re.compile(r"\d+(?:[.,]\d+)*")
def slot(s): # thay URL, email, số bằng token giữ chỗ s = URL.sub(" <url> ", s) s = EMAIL.sub(" <email> ", s) return NUM.sub(" <num> ", s)
def punct(s): # bỏ dấu câu, giữ nguyên các token giữ chỗ return re.sub(r"(<\w+>)|[^\w\s]", lambda m: m.group(1) or " ", s)
def strip_tone(s): # bỏ mọi dấu (mất thông tin!) s = ud.normalize("NFD", s).replace("đ", "d").replace("Đ", "D") s = "".join(ch for ch in s if ud.category(ch) != "Mn") return ud.normalize("NFC", s)
STEPS = [ ("nfc", lambda s: ud.normalize("NFC", s)), ("lower", str.casefold), ("slot", slot), ("punct", punct), ("space", lambda s: " ".join(s.split())), ("strip_tone", strip_tone),]
texts = [ "Sp OK lắm!!! 10đ, xem tại https://example.vn/sp?id=123", "Giao hàng CHẬM quá :( Gọi 0912.345.678 hoặc hotro@example.vn", "Shop của chị Ho\u0300a KHÔNG tệ,\tgiá 1.250.000đ.",]for t in texts: print(f"{'input':<10} {len(t):>2} {t!r}") for name, fn in STEPS: if ON[name]: t = fn(t) print(f"{name:<10} {len(t):>2} {t!r}") print()Lần chạy đầu cần tải Python, khoảng 10 MB.
Những điểm cần để ý trong code:
- Thứ tự trong
slot: URL, rồi email, rồi số. Đổi số trước thì “id=123” trong URL thành<num>và URL bị cắt làm đôi. punctgiữ token giữ chỗ. Mẫu(<\w+>)|[^\w\s]khớp hoặc một token như<num>(nhóm 1, trả lại nguyên vẹn), hoặc một ký tự không phải chữ, số, khoảng trắng (thay bằng dấu cách). Không có nhánh đầu thì dấu<và>bị xóa và<num>thànhnum.strip_tonedùng NFD rồi bỏ mọi ký tự thuộc loạiMn(dấu tổ hợp). “đ” không phải “d” cộng dấu, nên phải đổi riêng.
Hãy thử sửa ON rồi chạy lại:
- Đặt
"strip_tone": True: câu thứ ba thànhshop cua chi hoa khong te gia <num> d. - Đặt
"nfc": False: ở câu thứ ba, bướcpunctbiến “hòa” thànhho a. Ở dạng NFD, dấu huyền rời không phải\w, nên bị coi là dấu câu và bị thay bằng dấu cách. Đây là lý do NFC phải đứng đầu. - Đặt
"lower": False: “KHÔNG” và “CHẬM” giữ nguyên chữ hoa.
Lỗi hay gặp
Phần tiêu đề “Lỗi hay gặp”Kiểm tra nhanh
Phần tiêu đề “Kiểm tra nhanh”Tóm tắt và đọc thêm
Phần tiêu đề “Tóm tắt và đọc thêm”Ý chính:
- Chuẩn hóa là một hàm trên chuỗi: gộp các cách viết cùng một thứ () mà không gộp nhầm thứ khác nhau. Hàm càng mạnh càng dễ gộp nhầm, và lúc huấn luyện lẫn lúc dùng phải áp cùng một .
- Chữ có dấu ghi được bằng ký tự dựng sẵn hoặc chữ gốc cộng dấu tổ hợp. NFC và NFD gộp hai cách ghi này mà không mất thông tin; NFC luôn là bước đầu tiên. NFKC gộp thêm ký tự tương thích và có thể mất thông tin.
- “hòa” (kiểu cũ) và “hoà” (kiểu mới) đều là NFC nhưng khác code point; cần quy tắc riêng, dễ làm trên NFD. Chữ thường dùng
casefold; URL, email, số thay bằng token giữ chỗ, theo đúng thứ tự. - Stemming (Porter) cắt hậu tố theo luật, lemmatization về dạng từ điển; tiếng Việt đơn lập nên gần như không cần. Bỏ stopword, bỏ dấu câu, bỏ dấu tiếng Việt, đưa về chữ thường đều có lúc có hại: phân tích cảm xúc, nhận dạng thực thể, và mô hình subword học từ văn bản thô.
Đọc thêm:
- Whistler, K. (biên tập) (2026). Unicode Standard Annex #15: Unicode Normalization Forms, Unicode 18.0.0, mục 1 và 1.2. Định nghĩa gốc của tương đương chuẩn, tương đương tương thích và bốn dạng chuẩn, có hình minh họa từng dạng.
- Python Software Foundation.
unicodedata— Unicode Database, Python 3.14. Các hàmnormalize,name,categorydùng trong bài. - Jurafsky, D., Martin, J. H. (bản nháp 2026). Speech and Language Processing, 3rd ed., chương 2 “Words and Tokens”, mục 2.2 và 2.3. Hình vị, biến tố, và Unicode từ code point tới UTF-8.
- Porter, M. F. (1980). An algorithm for suffix stripping. Program, 14(3), 130–137. Trang của tác giả về thuật toán Porter có trích dẫn đầy đủ và bản cài đặt ở nhiều ngôn ngữ.
- Bird, S., Klein, E., Loper, E. (2009). Natural Language Processing with Python, chương 3, mục 3.6 “Normalizing Text”. So sánh hai stemmer Porter và Lancaster với lemmatizer WordNet trên cùng một câu, có code chạy được bằng NLTK.
Trình duyệt đang chặn lưu dữ liệu. Tiến độ sẽ mất khi bạn tải lại trang.
Nội dung theo giấy phép CC BY-NC-SA 4.0. Mã nguồn theo giấy phép MIT.Giới thiệu