Tokenizer trong Hugging Face: Nguyên lý và cách dùng trong Java
1. Tổng quan
Khi làm việc với các mô hình NLP trên Hugging Face, bước đầu tiên luôn là tokenization — biến văn bản thô thành dãy số mà model hiểu được. Bài này giải thích tokenizer hoạt động ra sao, các chiến lược tách token phổ biến (BPE, WordPiece, SentencePiece), và cách gọi HuggingFaceTokenizer trực tiếp từ Java thông qua thư viện DJL mà không cần chạy Python. Đọc xong, bạn sẽ build được một TokenizerService trong Spring Boot để encode/decode văn bản, kiểm soát padding/truncation, và viết test cho nó.
2. Tokenizer là gì và tại sao quan trọng
Tokenizer làm hai việc: tách câu thành các token (có thể là từ, subword, hoặc ký tự), rồi map mỗi token sang một ID số nguyên trong vocabulary của model. Model không bao giờ “nhìn thấy” chữ — nó chỉ thấy dãy ID này.
Ví dụ câu "Order service xử lý thanh toán" với tokenizer của bert-base-uncased có thể ra:
tokens: ["order", "service", "xu", "##ly", "thanh", "toan"]
ids: [2344, 2326, 1045, 2135, 7899, 8821]
Ký hiệu ##ly nghĩa là token này được nối liền vào token trước (subword). Đây là lý do số token thường nhiều hơn số từ — điều quan trọng khi bạn tính chi phí gọi API hoặc giới hạn max_length của model.
Lưu ý: Mỗi model pretrained đi kèm một tokenizer riêng, được huấn luyện cùng lúc với model trên cùng vocabulary. Dùng sai tokenizer (ví dụ tokenizer của BERT cho model GPT) sẽ ra ID vô nghĩa, model sẽ dự đoán sai hoàn toàn mà không báo lỗi.
3. Các chiến lược tokenization phổ biến
3.1. BPE (Byte-Pair Encoding)
Dùng trong GPT-2, RoBERTa. Bắt đầu từ ký tự, lặp lại việc gộp cặp ký tự xuất hiện nhiều nhất thành một token mới, cho đến khi đạt kích thước vocab mong muốn.
3.2. WordPiece
Dùng trong BERT. Tương tự BPE nhưng chọn cặp gộp dựa trên khả năng (likelihood) thống kê thay vì tần suất thô. Token con được đánh dấu ## để biết nó nối vào từ trước.
3.3. SentencePiece
Dùng trong T5, XLNet, ALBERT. Coi khoảng trắng cũng là một ký tự bình thường (ký hiệu ▁), nên xử lý tốt cho ngôn ngữ không tách từ rõ ràng như tiếng Nhật, tiếng Trung.
Bạn không cần tự implement các thuật toán này — Hugging Face đã đóng gói sẵn trong file tokenizer.json đi kèm mỗi model, việc của bạn chỉ là load và gọi.
4. Thêm dependency
Để dùng tokenizer trực tiếp trong Java (không qua Python server), thêm thư viện DJL Hugging Face Tokenizers:
<dependency>
<groupId>ai.djl.huggingface</groupId>
<artifactId>tokenizers</artifactId>
<version>0.28.0</version>
</dependency>
Thư viện này đọc trực tiếp file tokenizer.json chuẩn của Hugging Face, không cần chạy JVM qua JNI phức tạp.
5. Encode văn bản cơ bản
import ai.djl.huggingface.tokenizers.HuggingFaceTokenizer;
import ai.djl.huggingface.tokenizers.Encoding;
import java.util.Arrays;
public class TokenizerDemo {
public static void main(String[] args) throws Exception {
HuggingFaceTokenizer tokenizer = HuggingFaceTokenizer.newInstance("bert-base-uncased");
Encoding encoding = tokenizer.encode("Order service handles payment");
System.out.println(Arrays.toString(encoding.getIds()));
System.out.println(Arrays.toString(encoding.getTokens()));
}
}
HuggingFaceTokenizer.newInstance(...): tải tokenizer từ Hugging Face Hub (hoặc cache local) theo tên model.encode(...): trả về objectEncodingchứaids,tokens,attentionMask.- Lần chạy đầu tiên sẽ tải file
tokenizer.jsonvề~/.djl.ai/cache, các lần sau dùng cache, không cần mạng.
Output tham khảo:
[101, 2344, 2326, 16024, 7909, 102]
[[CLS], order, service, handles, payment, [SEP]]
Chú ý hai token đặc biệt [CLS] (đầu câu) và [SEP] (cuối câu) được tự động chèn vào — đây là quy ước của BERT, không phải lỗi.
6. Decode ngược lại
long[] ids = encoding.getIds();
String decoded = tokenizer.decode(ids);
System.out.println(decoded);
[CLS] order service handles payment [SEP]
decode hữu ích khi bạn cần debug: nhìn lại chính xác model “thấy” gì sau khi tokenize, hoặc khi xử lý output sinh văn bản từ model generative.
7. Padding, truncation và batch
Khi gửi nhiều câu vào model cùng lúc, các câu phải có độ dài bằng nhau (padding) và không vượt quá giới hạn model (truncation).
import java.util.Map;
HuggingFaceTokenizer tokenizer = HuggingFaceTokenizer.newInstance(
"bert-base-uncased",
Map.of(
"padding", "true",
"truncation", "true",
"maxLength", "16"
));
Encoding[] batch = tokenizer.batchEncode(new String[] {
"Order placed successfully",
"Payment failed due to insufficient balance"
});
for (Encoding e : batch) {
System.out.println(Arrays.toString(e.getIds()));
System.out.println(Arrays.toString(e.getAttentionMask()));
}
padding=true: câu ngắn được thêm token[PAD](ID 0) cho bằng độ dài câu dài nhất trong batch.truncation=true+maxLength=16: câu dài hơn 16 token sẽ bị cắt.attentionMask: dãy 1/0 đánh dấu token nào là thật (1) hay padding (0) — model dùng mask này để bỏ qua phần padding khi tính attention.
[101, 2344, 2872, 4652, 102, 0, 0, 0]
[1, 1, 1, 1, 1, 0, 0, 0]
Lưu ý: Nếu quên set
attentionMaskkhi đưa vào model, phần padding có thể bị tính vào attention, làm output sai lệch mà không báo lỗi runtime nào.
8. Tích hợp vào Spring Boot
Đóng gói tokenizer thành một bean dùng chung, tránh load lại mỗi request (load tokenizer từ Hub/cache mất 100–300ms, không nên làm trong request thread).
import ai.djl.huggingface.tokenizers.HuggingFaceTokenizer;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.Map;
@Service
public class TokenizerService {
private final HuggingFaceTokenizer tokenizer;
public TokenizerService() throws IOException {
this.tokenizer = HuggingFaceTokenizer.newInstance(
"bert-base-uncased",
Map.of("padding", "true", "truncation", "true", "maxLength", "128"));
}
public long[] encode(String text) {
return tokenizer.encode(text).getIds();
}
public String decode(long[] ids) {
return tokenizer.decode(ids);
}
public int countTokens(String text) {
return tokenizer.encode(text).getTokens().length;
}
}
- Constructor khởi tạo tokenizer một lần duy nhất khi bean được tạo, Spring quản lý vòng đời singleton.
countTokens: hữu ích khi bạn cần kiểm tra giới hạn độ dài input trước khi gọi model (ví dụ Order description không vượt quá 512 token).
Dùng trong service khác:
@RestController
@RequestMapping("/tokens")
public class TokenController {
private final TokenizerService tokenizerService;
public TokenController(TokenizerService tokenizerService) {
this.tokenizerService = tokenizerService;
}
@GetMapping("/count")
public int count(@RequestParam String text) {
return tokenizerService.countTokens(text);
}
}
Gọi GET /tokens/count?text=Order service xử lý thanh toán sẽ trả về số token mà BERT tokenizer tạo ra cho câu đó.
9. Viết test
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class TokenizerServiceTest {
@Test
void shouldEncodeAndDecodeRoundTrip() throws Exception {
TokenizerService service = new TokenizerService();
String original = "payment failed";
long[] ids = service.encode(original);
String decoded = service.decode(ids);
assertThat(ids).isNotEmpty();
assertThat(decoded).contains("payment").contains("failed");
}
@Test
void shouldCountMoreTokensThanWordsForSubwordSplit() throws Exception {
TokenizerService service = new TokenizerService();
int tokenCount = service.countTokens("unbelievable circumstances");
// "unbelievable" thường bị tách thành nhiều subword
assertThat(tokenCount).isGreaterThan(2);
}
}
- Test 1 kiểm tra round-trip encode → decode giữ lại nội dung gốc (có thể mất case/dấu câu do tokenizer
uncased). - Test 2 xác nhận hành vi subword: từ dài, hiếm gặp sẽ bị tách thành nhiều token hơn số từ đếm bằng khoảng trắng.
10. Lỗi hay gặp và khi nào không nên dùng
- Lẫn tokenizer giữa các model khác family: tokenizer của
bert-base-uncasedkhông tương thích vớigpt2. Luôn load tokenizer cùng tên với model bạn dùng để inference. - Không tính attention mask khi padding: dẫn tới kết quả model sai nhưng không có exception, rất khó debug. Luôn truyền
attentionMaskcùnginputIdsvào model. - Load tokenizer mỗi request: tốn thời gian và I/O không cần thiết. Luôn khai báo bean singleton như
TokenizerServiceở trên. - Không có mạng ở môi trường production: lần đầu tải tokenizer cần internet để kéo file từ Hugging Face Hub. Với môi trường offline, bạn cần tải trước
tokenizer.jsonvà load từ đường dẫn local bằngHuggingFaceTokenizer.newInstance(Path.of(...)). - Đếm token cho model không phải Hugging Face (ví dụ GPT-4 qua OpenAI API): tokenizer BPE của GPT khác với tokenizer Hugging Face chuẩn. Trường hợp này nên dùng thư viện đếm token riêng của nhà cung cấp, không dùng
HuggingFaceTokenizer. - Văn bản tiếng Việt có dấu: một số tokenizer huấn luyện chủ yếu trên tiếng Anh có thể tách từ tiếng Việt không tối ưu (tách quá nhiều subword lạ). Nếu xử lý tiếng Việt là chính, nên chọn model/tokenizer đã fine-tune cho tiếng Việt (ví dụ
vinai/phobert-base) thay vì BERT gốc.
11. Kết luận
Tokenizer là bước nền tảng quyết định model “nhìn thấy” văn bản như thế nào — hiểu đúng cơ chế BPE/WordPiece/SentencePiece giúp bạn tránh những lỗi khó debug như mismatch vocab hay sai attention mask. Với thư viện DJL, bạn có thể encode/decode trực tiếp trong Java mà không cần dựng server Python riêng, tích hợp gọn vào Spring Boot như một bean thông thường. Hãy nhớ: luôn dùng đúng tokenizer của model, cache tokenizer dưới dạng singleton, và kiểm tra kỹ padding/truncation trước khi đưa dữ liệu vào model.