Chương 6·Bài 6.1

Tokenization và text classification

Bài 1BPE có trọng số

BPE (byte pair encoding) học cách ghép các mảnh chữ hay đi cùng nhau thành một token. Bài này học BPE trên một danh sách từ có đếm số lần xuất hiện: một cặp nằm trong từ xuất hiện 77 lần thì được tính 77 lần.

  1. Chuẩn hóa mỗi từ về dạng Unicode NFC (giữ nguyên chữ hoa, chữ thường). Tách thành từng ký tự rồi thêm token </w> ở cuối.
  2. Lặp steps lần: đếm mọi cặp token kề nhau trên cả danh sách, mỗi lần xuất hiện cộng thêm số đếm của từ. Chọn cặp có tổng lớn nhất; hòa thì chọn cặp (a,b)(a,b) nhỏ nhất theo thứ tự so sánh chuỗi của Python (so aa trước, rồi bb). Ghép cặp ấy thành token a+ba+b trong mọi từ, quét từ trái sang phải, không chồng lấn. Nếu không còn cặp nào thì dừng sớm.
  3. Áp lần lượt các phép ghép đã học, đúng thứ tự, lên từng từ trong test_words (cũng chuẩn hóa NFC và thêm </w>).

Chữ “é” có thể được viết bằng một ký tự, hoặc bằng chữ “e” cộng dấu sắc rời. NFC đưa cả hai về một ký tự, nên café viết theo hai cách cho cùng kết quả.

Dữ liệu vào

Một đối tượng JSON gồm words (list các {"text", "count"}), steps và test_words.

Kết quả

Một đối tượng JSON gồm merges (list các cặp [a,b][a, b] theo thứ tự học) và tokens (list token của mỗi từ trong test_words).

Chấm điểm

Có 20 test, mỗi test 5 điểm.

Ví dụ

Input mẫu

{
  "words": [
    {"text": "aaaa", "count": 2},
    {"text": "abab", "count": 7},
    {"text": "café", "count": 1}
  ],
  "steps": 1,
  "test_words": ["aaaaa", "ababab", "café", "MÈO", "lạ", ""]
}

Output mẫu

{
  "merges": [["a", "b"]],
  "tokens": [
    ["a", "a", "a", "a", "a", "</w>"],
    ["ab", "ab", "ab", "</w>"],
    ["c", "a", "f", "é", "</w>"],
    ["M", "È", "O", "</w>"],
    ["l", "ạ", "</w>"],
    ["</w>"]
  ]
}

Bài 2WordPiece

WordPiece (tokenizer của BERT) tách một từ bằng cách tham lam: lấy đoạn đầu dài nhất có trong từ điển, rồi lặp lại với phần còn lại. Các mảnh không đứng đầu từ được viết thêm tiền tố ##.

Với mỗi từ:

  1. Chuẩn hóa Unicode NFC rồi đổi về chữ thường.
  2. Từ vị trí đầu, tìm đoạn dài nhất bắt đầu ở đó mà có trong vocab (ở vị trí đầu từ thì so nguyên đoạn, ở vị trí khác thì so ## + đoạn). Thêm mảnh ấy rồi đi tiếp từ cuối đoạn.
  3. Nếu có lúc không tìm được mảnh nào, cả từ thành ["[UNK]"], bỏ cả những mảnh đã tách được trước đó.
  4. Từ rỗng cho list rỗng.

Dữ liệu vào

Một đối tượng JSON gồm vocab (list các mảnh, có [UNK]) và words.

Kết quả

Một đối tượng JSON gồm tokens: list các mảnh của mỗi từ.

Chấm điểm

Có 20 test, mỗi test 5 điểm.

Ví dụ

Input mẫu

{
  "vocab": [
    "[UNK]", "con", "m", "##èo", "café", "đ", "##ẹp", "đẹp", "##đẹp",
    "a", "aa", "##a", "##aa", "học", "##sinh"
  ],
  "words": [
    "MÈO", "café", "đẹpđẹp", "aaaaa", "họcsinh", "họcsinhx", "con",
    "xy", ""
  ]
}

Output mẫu

{
  "tokens": [
    ["m", "##èo"],
    ["café"],
    ["đẹp", "##đẹp"],
    ["aa", "##aa", "##a"],
    ["học", "##sinh"],
    ["[UNK]"],
    ["con"],
    ["[UNK]"],
    []
  ]
}

Bài 3Thư viện hay sân bóng

Phân loại câu tiếng Việt theo chủ đề: thư viện (nhãn 00) hay thể thao (nhãn 11). Dùng một pipeline quen thuộc của scikit-learn:

  1. TfidfVectorizer với preprocessor chuẩn hóa NFC rồi đổi chữ thường, tokenizer=str.split, token_pattern=None, ngram_range=(1, 2), smooth_idf=True, sublinear_tf=False, norm="l2".
  2. MultinomialNB(alpha=1, fit_prior=True).
  3. Fit trên train một lần, dự đoán test theo đúng thứ tự hàng.

Từ vựng và IDF chỉ được học từ train. Cột id chỉ là mã, không đưa vào đặc trưng. Một số câu được lưu ở dạng NFD (dấu rời), nên phải chuẩn hóa trước khi tách từ.

Dữ liệu vào

Mỗi thư mục test có train.csv (cột id, text, label) và test.csv (cột id, text).

Kết quả

Một đối tượng JSON {"predictions": [...]}, theo thứ tự hàng của test.

Chấm điểm

Có 20 test, mỗi test 5 điểm. Một test được điểm khi accuracy từ 0.80.8 trở lên.

Bài 4Giữ EOS

Một tokenizer mức từ đơn giản cho Transformer: dựng từ điển theo tần suất, rồi mã hóa mỗi câu thành dãy số có độ dài cố định. Chỗ dễ sai là khi câu quá dài.

Dựng từ điển. Bốn token đặc biệt có mã cố định: <PAD>=0=0, <UNK>=1=1, <BOS>=2=2, <EOS>=3=3. Đổi các câu trong train về chữ thường, tách theo khoảng trắng, đếm số lần mỗi từ xuất hiện. Lấy vocab_size −4-4 từ xuất hiện nhiều nhất (bằng nhau thì từ gặp trước trong train đứng trước), đánh mã tiếp từ 44.

Mã hóa mỗi câu trong texts thành đúng max_len số:

  1. Chữ thường, tách theo khoảng trắng, đổi mỗi từ thành mã của nó (không có thì <UNK>).
  2. Nếu dãy dài hơn max_len −2-2, cắt bớt phần cuối của các từ, để còn chỗ cho <BOS> ở đầu và <EOS> ở cuối. Câu bị cắt vẫn phải kết thúc bằng <EOS>.
  3. Thêm <BOS> và <EOS>, rồi đệm <PAD> ở cuối cho đủ max_len.
  4. Attention mask là 11 ở các vị trí không phải đệm, 00 ở các vị trí đệm.

Dữ liệu vào

Một đối tượng JSON gồm train, vocab_size, max_len và texts.

Kết quả

Một đối tượng JSON gồm vocab (các token theo thứ tự mã, kể cả bốn token đặc biệt) và encoded: với mỗi câu là {"ids", "attention_mask", "truncated", "unk"}, trong đó truncated cho biết câu có bị cắt không, unk là số mã <UNK> trong ids.

Giới hạn

  • 4≤4\le max_len ≤12\le12, không quá 1010 câu train và 55 câu cần mã hóa.

Chấm điểm

Có 20 test, mỗi test 5 điểm.

Ví dụ

Input mẫu

{
  "train": [
    "Hello world", "how are you", "hello my friend", "are you there",
    "hello you"
  ],
  "vocab_size": 9,
  "max_len": 8,
  "texts": [
    "Hello world how are you today my friend", "hello friend",
    "you are my world", "how are you hello"
  ]
}

Output mẫu

{
  "vocab": [
    "<PAD>", "<UNK>", "<BOS>", "<EOS>", "hello", "you", "are", "world",
    "how"
  ],
  "encoded": [
    {
      "ids": [2, 4, 7, 8, 6, 5, 1, 3],
      "attention_mask": [1, 1, 1, 1, 1, 1, 1, 1],
      "truncated": true,
      "unk": 1
    },
    {
      "ids": [2, 4, 1, 3, 0, 0, 0, 0],
      "attention_mask": [1, 1, 1, 1, 0, 0, 0, 0],
      "truncated": false,
      "unk": 1
    },
    {
      "ids": [2, 5, 6, 1, 7, 3, 0, 0],
      "attention_mask": [1, 1, 1, 1, 1, 1, 0, 0],
      "truncated": false,
      "unk": 1
    },
    {
      "ids": [2, 8, 6, 5, 4, 3, 0, 0],
      "attention_mask": [1, 1, 1, 1, 1, 1, 0, 0],
      "truncated": false,
      "unk": 0
    }
  ]
}

Bài 5Naive Bayes đếm tay

Naive Bayes đa thức là baseline kinh điển cho phân loại văn bản, và đủ đơn giản để tính bằng tay. Bài này làm đúng các phép đếm ấy, không dùng thư viện.

Các câu đã viết không dấu, tách từ theo khoảng trắng. Nhãn là 00 hoặc 11.

  1. Từ vựng: mọi từ xuất hiện trong train, xếp theo thứ tự chữ cái; VV là số từ.
  2. Log-prior của lớp cc: log⁡(soˆˊ caˆu lớp c / soˆˊ caˆu)\log\big(\text{số câu lớp }c\,/\,\text{số câu}\big).
  3. Đếm nc(w)n_c(w) là số lần từ ww xuất hiện trong các câu lớp cc, và Nc=∑wnc(w)N_c=\sum_w n_c(w). Làm trơn Laplace với hệ số α\alpha:
log⁡P(w∣c)=log⁡nc(w)+αNc+αV.\log P(w\mid c)=\log\frac{n_c(w)+\alpha}{N_c+\alpha V}.
  1. Điểm của một câu test cho lớp cc: log-prior cộng log⁡P(w∣c)\log P(w\mid c) của mọi từ trong câu (từ lặp thì cộng nhiều lần). Từ không có trong từ vựng bị bỏ qua.
  2. Xác suất hậu nghiệm: softmax của hai điểm. Dự đoán là lớp có điểm lớn hơn; bằng nhau thì lớp 00.

Cuối cùng, xếp các từ theo log⁡P(w∣1)−log⁡P(w∣0)\log P(w\mid 1)-\log P(w\mid 0): top_k từ có giá trị lớn nhất là từ đặc trưng của lớp 11, top_k từ có giá trị nhỏ nhất là từ đặc trưng của lớp 00 (bằng nhau thì theo thứ tự chữ cái).

Dữ liệu vào

Một đối tượng JSON gồm train_texts, train_labels, alpha, top_k và test_texts.

Kết quả

Một đối tượng JSON gồm vocab_size, log_prior, results (với mỗi câu test là {"scores", "probabilities", "prediction", "ignored"}, trong đó ignored là số từ bị bỏ qua), top_class1 và top_class0.

Giới hạn

  • Không quá 1414 câu train và 66 câu test.

Chấm điểm

Có 20 test, mỗi test 5 điểm. Số thực được chấm với sai số khoảng 10−710^{-7}.

Ví dụ

Input mẫu

{
  "train_texts": [
    "sach doc thu vien", "doc sach truyen", "muon sach ve doc",
    "bong da san", "chay san bong", "doi bong thang"
  ],
  "train_labels": [0, 0, 0, 1, 1, 1],
  "alpha": 1.0,
  "top_k": 2,
  "test_texts": ["doc sach", "san bong doc", "thu vien moi", "banh mi"]
}

Output mẫu

{
  "vocab_size": 13,
  "log_prior": [-0.6931471805599452, -0.6931471805599452],
  "results": [
    {
      "scores": [-4.276666119016055, -6.8752320872765775],
      "probabilities": [0.9307692307692312, 0.06923076923076923],
      "prediction": 0,
      "ignored": 0
    },
    {
      "scores": [-8.841014310483892, -7.481367890846893],
      "probabilities": [0.2042977743668456, 0.7957022256331542],
      "prediction": 1,
      "ignored": 0
    },
    {
      "scores": [-5.662960480135946, -6.8752320872765775],
      "probabilities": [0.770700636942675, 0.2292993630573247],
      "prediction": 0,
      "ignored": 1
    },
    {
      "scores": [-0.6931471805599452, -0.6931471805599452],
      "probabilities": [0.5, 0.5],
      "prediction": 0,
      "ignored": 2
    }
  ],
  "top_class1": ["bong", "san"],
  "top_class0": ["doc", "sach"]
}