Bỏ qua để đến nội dung

Chuẩn bị dữ liệu pre-train

Sau bài này, bạn sẽ:

  • Hiểu các bước biến dữ liệu web thô thành dữ liệu pre-train: trích chữ, lọc chất lượng, khử trùng lặp (deduplication), lọc dữ liệu cá nhân, trộn nguồn, đóng gói chuỗi.
  • Tự dẫn được vì sao MinHash ước lượng được độ giống Jaccard, và xác suất một cặp tài liệu bị LSH đưa ra so, 1−(1−sr)b1 - (1 - s^r)^b.
  • Cài đặt được khử trùng lặp exact bằng hash, MinHash cùng LSH, và đóng gói chuỗi (sequence packing) có mặt nạ chéo tài liệu, trên vài câu bằng NumPy.
  • Nhận ra các đánh đổi: luật lọc bỏ nhầm văn bản tốt, khử trùng lặp nhiều hơn chưa chắc tốt hơn, và tỉ lệ trộn nguồn không có đáp án chung.

Bài Xây dựng LLM đầu tiên kết luận rằng LLM cần rất nhiều văn bản. Nguồn văn bản lớn nhất là web. Theo nhóm tạo bộ dữ liệu C4 (Raffel và cộng sự, 2020), Common Crawl, một kho lưu trữ web công khai, cho ra khoảng 20 TB chữ mỗi tháng. Nhưng chính nhóm này nhận xét rằng phần lớn số chữ đó không phải ngôn ngữ tự nhiên: menu, thông báo lỗi, chữ lặp đi lặp lại.

Một ví dụ cụ thể: Lee và cộng sự (2022) tìm thấy trong C4 một câu tiếng Anh dài 61 từ, lặp lại hơn 60 000 lần. Mô hình đọc nó 60 000 lần mà không học thêm được gì, chỉ tốn công học thuộc.

Vậy phần lớn việc chuẩn bị dữ liệu là bỏ bớt, không phải gom thêm. Trong một đợt crawl, bạn sẽ gặp:

  • trang chỉ có menu, chân trang, thông báo cookie;
  • cùng một bài báo đăng lại ở chục trang, chỉ khác dòng ngày đăng;
  • trang quảng cáo, trang spam;
  • địa chỉ email, số điện thoại của người thật.

Bài toán của bài này: từ một đống văn bản thô rất lớn, chọn ra phần đáng học, bỏ các bản lặp và bản gần lặp mà không phải so từng cặp, rồi xếp văn bản thành các chuỗi LL token để huấn luyện.

Bộ dữ liệu pre-train thường ghép nhiều nguồn, mỗi nguồn dạy một thứ:

NguồnMạnh ởVí dụ
Web (Common Crawl)Khối lượng, đủ mọi chủ đềC4, RefinedWeb, FineWeb
SáchVăn dài, mạch lạcSách hết bản quyền của Project Gutenberg
Bài báo khoa họcTri thức chuyên sâu, toánarXiv, PubMed Central
Mã nguồnCấu trúc, logicKho code công khai trên GitHub
WikipediaTri thức cô đọng, đã được biên tậpWikipedia các ngôn ngữ

The Pile (Gao và cộng sự, 2020) ghép 22 nguồn như vậy. Dolma (Soldaini và cộng sự, 2024) có khoảng 3 nghìn tỉ token, cũng từ nhiều nguồn. Nguồn nào cũng có điều kiện sử dụng và bản quyền riêng, kể cả trang web công khai. Bài Crawl dữ liệu và MongoDB đã nói về các lớp quy tắc đó.

Với web, bước đầu tiên là trích chữ từ HTML. Nhóm FineWeb (Penedo và cộng sự, 2024) thấy tự trích từ HTML gốc bằng thư viện trafilatura cho dữ liệu tốt hơn dùng bản chữ trích sẵn của Common Crawl, dù tốn công hơn.

Lọc bằng luật (heuristic filtering). C4 dùng các luật đơn giản, ví dụ:

  • chỉ giữ dòng kết thúc bằng dấu câu và có ít nhất 5 từ; bỏ trang dưới 3 câu;
  • bỏ dòng nhắc tới “Javascript” hay chính sách cookie; bỏ trang có “lorem ipsum” hoặc dấu {;
  • bỏ trang có từ trong một danh sách từ xấu; chỉ giữ trang được langdetect nhận là tiếng Anh với xác suất ít nhất 0.99.

Luật dễ hiểu, chạy nhanh, nhưng có tác dụng phụ. Luật dấu { nhắm vào code lẫn trong trang, nên bỏ luôn cả bài hướng dẫn lập trình. Nhóm FineWeb thử từng luật của C4: luật “dòng phải kết thúc bằng dấu câu” cải thiện mô hình nhiều nhất, nhưng bỏ khoảng 30% số token. Họ không dùng luật đó: dùng mọi luật còn lại của C4 cho kết quả còn tốt hơn mà chỉ bỏ khoảng 7% token. Luật viết cho văn xuôi tiếng Anh cũng phải được kiểm lại trước khi dùng cho ngôn ngữ khác.

Lọc bằng bộ phân loại (quality classifier). Ta huấn luyện một mô hình nhỏ chấm điểm “văn bản này có đáng học không”. FineWeb nhờ Llama-3-70B-Instruct chấm giá trị giáo dục của 460 000 trang, dùng điểm đó huấn luyện một bộ phân loại, rồi giữ phần điểm cao: được FineWeb-Edu, 1.3 nghìn tỉ token trong 15 nghìn tỉ token của FineWeb. Mô hình học trên FineWeb-Edu tăng điểm MMLU từ 33% lên 37% so với học trên FineWeb. Cái giá: bộ phân loại học theo mẫu “tốt” mà người làm chọn, nên mang luôn thiên kiến của mẫu đó, và có thể bỏ cả văn bản tốt chỉ vì nó trông khác mẫu.

Cách đơn giản nhất: chuẩn hóa văn bản (NFC, chữ thường, gộp khoảng trắng), băm nó bằng một hàm như SHA-256, rồi giữ một tài liệu cho mỗi giá trị hash. Hai văn bản giống nhau từng byte thì cùng hash. Cách này nhanh và chỉ cần lưu một hash cho mỗi tài liệu.

Điểm yếu: đổi một dấu phẩy là hash đổi hẳn. Bài báo đăng lại với dòng ngày đăng khác vẫn lọt. Lee và cộng sự còn khử ở mức đoạn con: dùng mảng hậu tố (suffix array) để tìm và bỏ mọi đoạn từ 50 token trở lên xuất hiện nhiều lần.

Khử trùng lặp gần: shingle và độ giống Jaccard

Phần tiêu đề “Khử trùng lặp gần: shingle và độ giống Jaccard”

Để bắt bản gần trùng (near-duplicate), ta cần một con số đo độ giống. Biến mỗi tài liệu thành tập các shingle: các cụm vài từ liền nhau. Với shingle 2 từ, câu “hà nội là thủ đô” có tập 4 shingle: “hà nội”, “nội là”, “là thủ”, “thủ đô”. Gọi tập shingle của hai tài liệu là AA và BB. Độ giống Jaccard (Jaccard similarity) là:

J(A,B)=∣A∩B∣∣A∪B∣J(A, B) = \frac{|A \cap B|}{|A \cup B|}

Tử số đếm shingle chung, mẫu số đếm mọi shingle của cả hai. J=1J = 1 khi hai tập bằng nhau, J=0J = 0 khi không có shingle chung. Ở ô code bên dưới, câu “…một thành phố hơn nghìn năm tuổi” có 14 shingle, câu thêm chữ “cổ” có 15. Chúng chung 13 shingle, hợp lại có 16, nên J=13/16≈0.81J = 13/16 \approx 0.81.

Tính JJ cho mọi cặp thì không được. Với mm tài liệu có m(m−1)/2m(m-1)/2 cặp; một tỉ tài liệu là khoảng 5×10175 \times 10^{17} cặp, mỗi cặp lại phải so cả tập shingle. Ta cần hai mẹo: rút mỗi tài liệu thành một chữ ký ngắn, và chỉ so các cặp có vẻ giống.

MinHash: chữ ký ngắn ước lượng được Jaccard

Phần tiêu đề “MinHash: chữ ký ngắn ước lượng được Jaccard”

Ý tưởng ước lượng độ giống bằng một chữ ký nhỏ có từ Broder (1997). Lấy một hàm băm hh gán cho mỗi shingle một số ngẫu nhiên, coi như không có hai shingle nào trùng số. MinHash của tập AA là giá trị nhỏ nhất, min⁡x∈Ah(x)\min_{x \in A} h(x).

Vì sao Pr⁡[min⁡h(A)=min⁡h(B)]=J(A,B)\Pr[\min h(A) = \min h(B)] = J(A, B). Xét shingle x∗x^* có giá trị băm nhỏ nhất trong cả A∪BA \cup B.

  • Nếu x∗x^* thuộc cả AA lẫn BB, nó là phần tử nhỏ nhất của từng tập, nên hai MinHash bằng nhau.
  • Nếu x∗x^* chỉ thuộc AA, thì min⁡h(A)=h(x∗)\min h(A) = h(x^*). Mọi phần tử của BB đều có giá trị lớn hơn h(x∗)h(x^*), nên hai MinHash khác nhau. Trường hợp x∗x^* chỉ thuộc BB cũng vậy.

Vậy hai MinHash bằng nhau khi và chỉ khi x∗x^* nằm trong A∩BA \cap B. Hàm băm ngẫu nhiên nên mọi phần tử của A∪BA \cup B đều có cơ hội như nhau để là x∗x^*. Xác suất x∗x^* rơi vào A∩BA \cap B là ∣A∩B∣/∣A∪B∣|A \cap B| / |A \cup B|, đúng bằng JJ.

Một hàm băm chỉ cho câu trả lời có hoặc không. Dùng KK hàm băm độc lập h1,…,hKh_1, \dots, h_K, ta có chữ ký MinHash (MinHash signature) gồm KK số, dù tài liệu dài bao nhiêu. Ta ước lượng JJ bằng tỉ lệ vị trí trùng nhau:

J^=1K∑k=1K1[min⁡hk(A)=min⁡hk(B)]\hat{J} = \frac{1}{K} \sum_{k=1}^{K} \mathbb{1}\big[\min h_k(A) = \min h_k(B)\big]

1[⋅]\mathbb{1}[\cdot] bằng 1 khi điều trong ngoặc đúng, bằng 0 khi sai. Mỗi số hạng bằng 1 với xác suất JJ, nên J^\hat{J} là tỉ lệ thành công của KK lần tung đồng xu độc lập có xác suất JJ. Trung bình của nó là JJ, độ lệch chuẩn là J(1−J)/K\sqrt{J(1-J)/K}. Với J=0.81J = 0.81 và K=64K = 64, độ lệch chuẩn khoảng 0.05. Muốn sai số giảm một nửa thì cần KK gấp bốn.

Chữ ký ngắn rồi, nhưng số cặp vẫn là m(m−1)/2m(m-1)/2. LSH (locality-sensitive hashing, băm nhạy với độ giống) theo kiểu chia băng tránh việc này. Chia chữ ký KK số thành bb băng (band), mỗi băng rr số liền nhau, K=b⋅rK = b \cdot r. (Ở bài này bb là số băng, không phải hệ số tự do.) Hai tài liệu thành cặp ứng viên (candidate pair) nếu trùng trọn ít nhất một băng. Trong thực tế, mỗi băng được băm vào một bảng, và chỉ các tài liệu rơi cùng ô mới được so kỹ.

Gọi ss là xác suất hai chữ ký trùng ở một vị trí, tức s=Js = J. Gọi P(s)P(s) là xác suất thành cặp ứng viên. Ta tính nó qua bốn bước:

  1. Trùng trọn một băng là trùng cả rr vị trí độc lập: xác suất srs^r.
  2. Không trùng trọn băng đó: 1−sr1 - s^r.
  3. Không trùng trọn băng nào trong bb băng độc lập: (1−sr)b(1 - s^r)^b.
  4. Trùng trọn ít nhất một băng là phần bù của bước 3:
P(s)=1−(1−sr)bP(s) = 1 - (1 - s^r)^b

Thử với b=16b = 16, r=4r = 4 như ô code:

Độ giống ss0.10.30.50.70.81
P(s)P(s)0.00160.120.640.990.9999

Đường cong có dạng chữ S: cặp ít giống gần như không được so, cặp rất giống gần như luôn được so. Ngưỡng, nơi đường cong dốc nhất, xấp xỉ (1/b)1/r(1/b)^{1/r}, ở đây (1/16)1/4=0.5(1/16)^{1/4} = 0.5. Hai tham số kéo ngược nhau: rr lớn là đòi trùng nhiều vị trí cùng lúc, nên chặt hơn; bb lớn là thêm cơ hội thử, nên lỏng hơn. LSH là cái sàng: vẫn có cặp giống bị sót và cặp ứng viên không thật sự giống, nên cặp ứng viên thường được kiểm lại.

Các cấu hình đã công bố:

Bộ dữ liệuShingleKKb×rb \times rXác suất bắt cặp đã công bố
FineWeb (2024)5 từ11214 × 8s=0.75s = 0.75: 77%; s=0.85s = 0.85: 98.8%
Lee và cộng sự (2022)5 từ (tách theo khoảng trắng)9 000450 × 20như dòng dưới (cùng cấu hình)
RefinedWeb (2023)5 token GPT-2 (sau khi chuẩn hóa NFD, bỏ dấu)9 000450 × 20s=0.75s = 0.75: 76%; s=0.8s = 0.8: 99.4% (phụ lục G)

Hai bài báo của Lee và RefinedWeb đặt tên chữ ngược với bài này: chữ b=20b = 20 của họ là số hàng mỗi băng, chữ r=450r = 450 là số băng. Phần chính của RefinedWeb (mục 3.3) lại ghi “20 buckets of 450 hashes”, mâu thuẫn với phụ lục G của chính bài. Số 76% và 99.4% chỉ khớp cách chia 450 băng × 20 hàng. Lee và cộng sự còn lọc mỗi cặp ứng viên thêm hai lần: Jaccard thật phải lớn hơn 0.8, rồi độ giống theo khoảng cách chỉnh sửa (edit similarity) cũng phải lớn hơn 0.8.

Vì sao khử trùng lặp, và khử bao nhiêu là đủ

Phần tiêu đề “Vì sao khử trùng lặp, và khử bao nhiêu là đủ”

Lee và cộng sự (2022) thấy khử trùng lặp làm mô hình chép nguyên văn dữ liệu ít đi khoảng 10 lần: từ hơn 1% văn bản sinh ra xuống khoảng 0.1%. Họ cũng thấy hơn 4% mẫu trong tập kiểm định của các bộ dữ liệu chuẩn có bản trùng trong tập huấn luyện. Đây là rò rỉ dữ liệu kiểm tra (test-set contamination): mô hình được chấm trên câu nó đã thấy, nên điểm đẹp hơn thực tế. Vì vậy phải khử trùng lặp cả giữa tập huấn luyện và tập kiểm tra.

Nhưng nhiều hơn chưa chắc tốt hơn. Nhóm FineWeb khử trùng lặp MinHash trên cả 96 đợt crawl gộp chung, còn 4 nghìn tỉ token, nhưng mô hình học trên đó gần như không hơn khi học trên dữ liệu chưa khử. Ở một đợt crawl cũ, quá trình này bỏ tới 90% dữ liệu, và 10% còn lại lại tệ hơn 90% bị bỏ: nhiều quảng cáo, danh sách từ khóa rời rạc, chữ định dạng hỏng. Khi khử riêng từng đợt crawl, họ còn 20 nghìn tỉ token, và mô hình ngang RefinedWeb. Bài học: số lượng dữ liệu bị bỏ không nói lên chất lượng phần còn lại. Mỗi lựa chọn phải được đo bằng cách huấn luyện và đánh giá mô hình.

PII (personally identifiable information, dữ liệu định danh cá nhân) như email, số điện thoại có thể bị mô hình học thuộc rồi nhả ra. FineWeb ẩn danh địa chỉ email và địa chỉ IP công khai trong bản phát hành. Dolma lọc PII, và dùng bộ phân loại huấn luyện trên dữ liệu Jigsaw Toxic Comments để lọc nội dung độc hại. C4 dùng danh sách từ xấu. Cách nào cũng là đánh đổi: lọc mạnh thì mất cả nội dung vô hại, nhất là của những cộng đồng ít người viết; lọc nhẹ thì lọt dữ liệu nhạy cảm.

Mỗi nguồn được gán một trọng số: phần của nó trong dữ liệu mô hình sẽ đọc. Nguồn nhỏ mà chất lượng cao có thể được đọc nhiều lần (nhiều epoch). Vài dòng trong bảng thành phần của The Pile (825 GiB dữ liệu gốc, khoảng 1 254 GiB hiệu dụng sau khi lặp mỗi nguồn theo số epoch):

NguồnTrọng sốSố epoch
Pile-CC (web)18.11%1.0
PubMed Central14.40%2.0
ArXiv8.96%2.0
GitHub7.59%1.0
Wikipedia (en)1.53%3.0

Trọng số là tỉ lệ byte sau khi đã lặp mỗi nguồn theo số epoch của nó: Wikipedia chỉ 6.38 GiB nhưng được đọc 3 lần. Đây là lựa chọn của nhóm làm The Pile cho mục tiêu của họ, không phải tỉ lệ đúng chung.

Đóng gói chuỗi và mặt nạ chéo tài liệu

Phần tiêu đề “Đóng gói chuỗi và mặt nạ chéo tài liệu”

Mô hình nhận các chuỗi dài đúng LL token, còn tài liệu thì dài ngắn đủ kiểu. Cách thứ nhất là đệm (padding): mỗi tài liệu cắt riêng thành các khúc LL token, khúc cuối đệm thêm token rỗng, vẫn tốn tính toán mà không dạy gì. Cách thứ hai là đóng gói chuỗi (sequence packing): tách token mọi tài liệu, nối thành một dòng dài, chèn token kết thúc (end-of-sequence, EOS) sau mỗi tài liệu, rồi cắt dòng đó thành các khúc LL token. Không còn token đệm, nhưng một chuỗi giờ có thể chứa đuôi tài liệu này và đầu tài liệu khác.

Có hai cách xử lý ranh giới. Cách đơn giản: để nguyên mặt nạ nhân quả và để mô hình tự học rằng sau EOS là chuyện khác. Cách chặt hơn: dùng mặt nạ chéo tài liệu (cross-document attention mask, Llama 3 gọi là document mask), cấm token nhìn sang tài liệu khác. Gọi doc(i)\mathrm{doc}(i) là số thứ tự tài liệu của vị trí ii trong chuỗi; doc\mathrm{doc} tăng thêm 1 ngay sau mỗi EOS, còn EOS thuộc tài liệu mà nó kết thúc. Mặt nạ M\mathbf{M} vẫn được cộng vào điểm attention như ở bài trước, nhưng giờ Mij=0M_{ij} = 0 khi thỏa cả hai điều kiện

j≤i,doc(i)=doc(j),j \le i, \qquad \mathrm{doc}(i) = \mathrm{doc}(j),

và Mij=−∞M_{ij} = -\infty ở mọi ô khác. Điều kiện đầu là mặt nạ nhân quả, điều kiện sau là phần thêm vào. Kết quả là các khối tam giác nằm dọc đường chéo, mỗi khối một tài liệu. Với chuỗi 12 token gồm 4 token của tài liệu thứ nhất và 8 token của tài liệu thứ hai, số ô được nhìn là 4⋅52+8⋅92=10+36=46\tfrac{4 \cdot 5}{2} + \tfrac{8 \cdot 9}{2} = 10 + 36 = 46, so với 12⋅132=78\tfrac{12 \cdot 13}{2} = 78 của mặt nạ nhân quả thường.

Llama 3 (Llama Team, 2024) dùng mặt nạ này và ghi rằng nó ít ảnh hưởng khi pre-train thông thường, nhưng quan trọng khi tiếp tục pre-train trên chuỗi rất dài.

  • Số token phụ thuộc bộ tách từ. 15 nghìn tỉ token của FineWeb (đếm bằng bộ tách từ của GPT-2) và 3 nghìn tỉ của Dolma (bộ tách từ của Llama) không cùng thước đo.
  • Quy mô thật cần công cụ riêng. Ô code của bài chạy trên 5 câu. Với hàng tỉ trang, người ta dùng thư viện như datatrove của Hugging Face, có sẵn các bước lọc, MinHash và tách token chạy song song.
  • Dữ liệu chỉ là nguyên liệu. Mô hình pre-train chỉ biết viết tiếp văn bản. Dạy nó làm theo yêu cầu là việc của bài Họ GPT và fine-tune đa nhiệm.

Bảng dưới theo vài trang web giả định qua các bước của mục Lý thuyết. Hãy nhìn cột cuối: không phải trang nào bị bỏ cũng là trang xấu.

Trang (ví dụ tự đặt)Bước xử lý nóKết quả
“Trang chủ | Tin tức | Liên hệ | Đăng nhập”Luật: dòng ngắn, không có dấu câu cuốiBỏ, đúng ý
“Vui lòng bật JavaScript để xem trang này”Luật nhắc “Javascript” của C4Bỏ, đúng ý
Bài hướng dẫn Python có khối code { ... }Luật dấu { của C4Bỏ, bỏ nhầm
Bài thơ không có dấu chấm cuối dòngLuật dấu câu cuối dòngMất gần hết các dòng, bỏ nhầm
Cùng một bài báo ở 10 trang, chỉ khác ngày đăngKhử trùng lặp gần (MinHash)Giữ 1 bản
Trang có email cá nhân trong phần bình luậnLọc PIIGiữ trang, email bị ẩn danh

Bảng thứ hai so padding và packing trên ba câu của ô code thứ hai. Ba tài liệu dài 16, 10 và 13 token (kể cả EOS), với L=12L = 12:

CáchSố chuỗiToken đệmToken còn thừa
Padding: mỗi tài liệu cắt riêng521 trên 60 chỗ (35%)0
Packing: nối rồi cắt303

Ba token thừa của packing không mất đi: ở dữ liệu thật, chúng nằm ở đầu chuỗi kế tiếp, nối với tài liệu sau.

Code trong bài chạy thử lần cuối tháng 10/2026, với Python 3.14 và NumPy 2.4 (bản có trong Pyodide). Cả hai ô chạy ngay trên trang và chạy độc lập: ô nào cũng tự import và tự tạo dữ liệu.

Ô này có 5 câu tự viết. Câu 1 là bản sao câu 0 nhưng viết hoa, thừa dấu cách và mã hóa dấu kiểu NFD (chữ cái và dấu tách rời). Câu 2 là câu 0 thêm chữ “cổ”. Câu 3 nói về phở, câu 4 là spam. Ô khử trùng lặp exact bằng SHA-256, rồi tính MinHash với K=64K = 64 (shingle là cặp từ liền nhau) và LSH với r=4r = 4, tức b=16b = 16 băng.

Khử trùng lặp exact, MinHash và LSH

# Khử trùng lặp exact rồi gần trùng (MinHash + LSH) trên 5 câu tự viết
import hashlib
import re
import unicodedata
import numpy as np
docs = ["Hà Nội là thủ đô của Việt Nam, một thành phố hơn nghìn năm tuổi.",
"Hà Nội là thủ đô của Việt Nam, một thành phố cổ hơn nghìn năm tuổi.",
"Phở là món ăn nổi tiếng của Việt Nam.",
"Mua ngay giá rẻ, bấm vào đây để nhận quà miễn phí!"]
# Câu 1: bản sao câu 0, nhưng viết hoa, dấu tách rời (NFD), thừa dấu cách
docs.insert(1, " " + unicodedata.normalize("NFD", docs[0]).upper())
words = [re.findall(r"\w+", unicodedata.normalize("NFC", d).lower())
for d in docs] # chuẩn hóa NFC, chữ thường, bỏ dấu câu, tách từ
# 1) Exact: SHA-256 của văn bản đã chuẩn hóa; hash đã gặp thì bỏ
keys = [hashlib.sha256(" ".join(w).encode()).digest() for w in words]
keep = [i for i, k in enumerate(keys) if k not in keys[:i]]
print("raw equal:", docs[0] == docs[1], "| after exact:", keep)
def h(k, s): # hàm băm thứ k: 60 bit đầu của SHA-1 (không dùng hash())
return int(hashlib.sha1(f"{k}|{s}".encode()).hexdigest()[:15], 16)
# 2) MinHash: shingle là cặp từ liền nhau; chữ ký K số, mỗi băng r số
K, r = 64, 4
sh = {i: set(zip(words[i], words[i][1:])) for i in keep}
sig = {i: np.array([min(h(k, s) for s in sh[i]) for k in range(K)])
for i in keep}
for x, y in [(x, y) for x in keep for y in keep if x < y]:
J = len(sh[x] & sh[y]) / len(sh[x] | sh[y])
same = sig[x] == sig[y] # K giá trị đúng/sai
cand = same.reshape(-1, r).all(axis=1).any() # trùng trọn một băng
print(f"{x}-{y}: J = {J:.2f}, MinHash = {same.mean():.2f}, cand = {cand}")

Lần chạy đầu cần tải Python, khoảng 10 MB.

Đọc kết quả:

  • raw equal: False, nhưng after exact: [0, 2, 3, 4]. Hai chuỗi thô khác nhau, nhưng sau khi chuẩn hóa thì cùng hash, nên câu 1 bị bỏ.
  • Cặp 0-2: J=0.81J = 0.81 đúng như tính tay ở mục Lý thuyết, MinHash ước lượng 0.84. Lệch 0.03, nhỏ hơn độ lệch chuẩn khoảng 0.05. Cặp này là ứng viên, khớp bảng xác suất: ở s=0.81s = 0.81, P≈0.9999P \approx 0.9999.
  • Cặp 0-3 và 2-3: J=0.10J = 0.10 (chung đúng “của việt” và “việt nam”), ước lượng 0.11 và 0.09, không là ứng viên. Các cặp có câu spam đều có J=0J = 0 và ước lượng 0.

Hàm h lấy 60 bit đầu của SHA-1 thay cho hash() của Python (xem mục Lỗi hay gặp). 60 bit vừa kiểu int64 của NumPy, nên chữ ký không bị tràn số.

Bạn có thể tự thử hai thay đổi ở dòng K, r = 64, 4:

  • K, r = 256, 4: ước lượng của cặp 0-2 thành 0.82, gần J=0.81J = 0.81 hơn, vì độ lệch chuẩn giảm còn khoảng 0.024.
  • K, r = 64, 16: chỉ còn 4 băng, mỗi băng 16 số. Cặp 0-2 không còn là ứng viên, dù ước lượng vẫn là 0.84. Theo công thức, 1−(1−0.8116)4≈0.131 - (1 - 0.81^{16})^{4} \approx 0.13: băng quá dài làm LSH sót cặp gần trùng.

Đóng gói chuỗi và mặt nạ chéo tài liệu

Phần tiêu đề “Đóng gói chuỗi và mặt nạ chéo tài liệu”

Ô này lấy ba câu còn lại sau khi khử trùng lặp: câu 0, câu về phở và câu spam. Câu 2 bị bỏ vì gần trùng câu 0. Câu spam được giữ chỉ để có thêm dữ liệu, còn ở quy trình thật bộ lọc chất lượng lẽ ra đã loại nó. Mỗi từ là một token, thêm <eos> sau mỗi câu. Ô đếm token đệm nếu dùng padding, rồi đóng gói thành chuỗi L=12L = 12, in chuỗi thứ hai cùng số thứ tự tài liệu, và đếm số ô được nhìn của hai loại mặt nạ.

Packing có EOS và mặt nạ chéo tài liệu

# Packing: nối tài liệu, chèn EOS, cắt thành chuỗi L token, rồi tạo mặt nạ
import numpy as np
docs = ["hà nội là thủ đô của việt nam một thành phố hơn nghìn năm tuổi",
"phở là món ăn nổi tiếng của việt nam",
"mua ngay giá rẻ bấm vào đây để nhận quà miễn phí"]
L = 12
toks = [d.split() + ["<eos>"] for d in docs] # mỗi từ là một token
print("lengths:", [len(t) for t in toks])
# Cách 1, padding: mỗi tài liệu cắt riêng, khúc cuối đệm cho đủ L
chunks = sum(-(-len(t) // L) for t in toks) # chia rồi làm tròn lên
pad = chunks * L - sum(len(t) for t in toks)
print(f"padding: {chunks} sequences, {pad} pad tokens")
# Cách 2, packing: nối mọi tài liệu thành một dòng rồi cắt từng L token
stream = [w for t in toks for w in t]
n = len(stream) // L
print(f"packing: {n} sequences, {len(stream) - n * L} tokens left over")
seq = np.array(stream[L:2 * L]) # chuỗi thứ hai: có ranh giới tài liệu
eos = seq == "<eos>"
doc = np.cumsum(eos) - eos # số thứ tự tài liệu, tăng sau mỗi EOS
print(seq, doc, sep="\n")
causal = np.tril(np.ones((L, L), dtype=bool)) # mặt nạ nhân quả
mask = causal & (doc[:, None] == doc[None, :]) # thêm: cùng tài liệu
print(f"visible cells: causal = {causal.sum()}, per-doc = {mask.sum()}")
print(mask[4].astype(int)) # hàng của "phở": chỉ nhìn chính nó

Lần chạy đầu cần tải Python, khoảng 10 MB.

Đọc kết quả:

  • Padding: 5 chuỗi, 21 token đệm. Packing: 3 chuỗi, thừa 3 token. Đây là các số ở bảng mục Minh họa.
  • Chuỗi thứ hai bắt đầu bằng “nghìn năm tuổi <eos>”, đuôi câu Hà Nội, rồi tới 8 token đầu của câu về phở. Dòng doc là doc(i)\mathrm{doc}(i): bốn số 0 (kể cả EOS), rồi tám số 1.
  • Số ô được nhìn: 78 với mặt nạ nhân quả, 46 với mặt nạ chéo tài liệu, như tính ở mục Lý thuyết.
  • Hàng 4 là hàng của “phở”, token đầu của tài liệu mới. Nó chỉ có một số 1, ở chính nó, nên “phở” không nhìn được “nghìn năm tuổi”. Mặt nạ nhân quả thường cho hàng này năm số 1.
Câu 1/4 Bạn dùng chữ ký MinHash K=100K = 100 số. Cách A chia b=20b = 20 băng, mỗi băng r=5r = 5 hàng. Cách B chia b=5b = 5 băng, mỗi băng r=20r = 20 hàng. Với một cặp tài liệu có J=0.7J = 0.7, điều gì xảy ra?
Câu 2/4 Bạn băm SHA-256 thẳng trên văn bản thô của hai nguồn tin tức và thấy 0 bản trùng, dù xem bằng mắt thì có hàng trăm bài giống hệt nhau. Nguyên nhân nào hợp lý nhất?
Câu 3/4 Một nhóm khử trùng lặp trên toàn bộ các đợt crawl và loại được rất nhiều tài liệu. Mô hình train trên phần còn lại không tốt hơn mô hình train trên dữ liệu chưa khử. Bài học rút ra là gì?
Câu 4/4 Một chuỗi L=8L = 8 token gồm 3 token cuối của tài liệu A (kể cả EOS) rồi 5 token đầu của tài liệu B. Dùng mặt nạ chéo tài liệu, có bao nhiêu ô (i,j)(i, j) được phép nhìn?

Ý chính:

  • Dữ liệu web nhiều nhưng phần lớn là rác và bản lặp. Chuẩn bị dữ liệu pre-train chủ yếu là bỏ bớt: trích chữ, lọc bằng luật và bộ phân loại, khử trùng lặp, lọc PII và nội dung độc hại, rồi trộn nguồn theo trọng số.
  • Khử trùng lặp exact băm văn bản đã chuẩn hóa. Khử trùng lặp gần dùng shingle và độ giống Jaccard J=∣A∩B∣/∣A∪B∣J = |A \cap B| / |A \cup B|; MinHash ước lượng JJ vì xác suất hai MinHash trùng nhau đúng bằng JJ; LSH chia chữ ký thành bb băng rr hàng, bắt cặp với xác suất 1−(1−sr)b1 - (1 - s^r)^b.
  • Khử trùng lặp giảm học thuộc và rò rỉ dữ liệu kiểm tra, nhưng khử nhiều hơn chưa chắc tốt hơn. Mỗi bước lọc phải được đo bằng mô hình.
  • Packing nối tài liệu bằng EOS và cắt thành chuỗi LL token, không phí token đệm. Mặt nạ chéo tài liệu cấm token nhìn sang tài liệu khác trong cùng chuỗi.

Đọc thêm:

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