FDE PulseViệc làm FDE đang mở 316Mới đăng 7 ngày qua 10Chủ đề nổi bật: Đào tạo kỹ năng FDE tại Đông Nam Á

Tờ báo của nghề Forward Deployed Engineer

Bách khoa

Thực hành LoRA: dạy một model nhỏ phân loại ticket hỗ trợ mà không sửa trọng số gốc

Bài thực hành đi từ ticket đã gắn nhãn đến một adapter nhỏ gọn chạy trên model gốc, kèm cách giữ tập test để con số đánh giá cuối cùng còn đáng tin.

Đồ hoạTừ ticket có nhãn đến adapter LoRA
  1. 1Gắn nhãn ticketMỗi ticket kèm target đúng: category như thanh toán, đăng nhập, giao hàng
  2. 2Chia train/val/testVí dụ 2.000 ticket chia thành 1.400 / 300 / 300, khóa file test lại
  3. 3Cấu hình LoRALoraConfig r=8, lora_alpha=32, q_proj; bọc base model bằng get_peft_model
  4. 4Train, đọc validationSo với baseline nhãn phổ biến nhất; chọn cấu hình theo validation, không theo train loss
  5. 5Test một lần, lưu adapterBáo cáo kết quả test; adapter nhỏ gọn, base model giữ nguyên

Phần kỹ thuật nằm ở bước 3–4, nhưng con số đánh giá chỉ đáng tin khi tập test được giữ cho đúng một lần ở cuối.

Đồ hoạ: FDE Times

Tóm tắt nhanh

  • Với LoRA, trọng số gốc được đóng băng. Trong ví dụ Llama-3.2-1B của PEFT, chỉ 851.968 trên 1.236.666.368 tham số được huấn luyện.
  • Loss trên tập train thấp chưa chứng minh được gì. Hyperparameter chỉnh bằng validation, còn test chỉ chạy một lần ở cuối.
  • Adapter rất nhỏ (khoảng 6MB với opt-350m, trong khi trọng số đầy đủ khoảng 700MB), nên có thể triển khai một adapter cho mỗi tác vụ.
Chia sẻLinkedInFacebookX

Trong ví dụ healthcheck của thư viện PEFT, một model Llama-3.2-1B có 1.236.666.368 tham số, nhưng chỉ 851.968 tham số được huấn luyện, tức 0,0689%. Phần còn lại giữ nguyên.

Con số cụ thể sẽ thay đổi theo model và cấu hình, nhưng nó cho thấy vì sao một kỹ sư làm tại chỗ khách hàng có thể tự fine-tune model cho một bài toán hẹp mà không cần cả cụm GPU.

Phân loại ticket hỗ trợ là bài toán hẹp kiểu đó: đầu vào là một đoạn văn bản, đầu ra là một nhãn nằm trong danh sách cố định. Nếu bạn muốn chuyển sang vai trò FDE, làm bài này từ đầu đến cuối và giải thích được từng quyết định sẽ cho bạn một dự án trình bày trọn vẹn khi đi phỏng vấn.

Bài này hướng dẫn dựng một bộ phân loại ticket bằng LoRA trên một model ngôn ngữ nhỏ. Kết quả cuối là một adapter nhỏ gọn và một con số đánh giá đáng tin.

Cần chuẩn bị gì trước khi gõ lệnh?

Bạn cần Python, thư viện peft của Hugging Face cùng hệ sinh thái transformers đi kèm, và một base model nhỏ. Opt-350m hay Llama-3.2-1B, hai model mà tài liệu PEFT dùng làm ví dụ, đều là lựa chọn hợp lý cho laptop có GPU hoặc một máy cloud nhỏ.

Thứ quan trọng hơn GPU là dữ liệu có nhãn. Fine-tune có giám sát đòi hỏi mỗi mẫu train phải đi kèm đáp án đúng, gọi là target. Với ticket, target là category, ví dụ “thanh toán”, “đăng nhập”, “giao hàng”, “khác”.

Fine-tuning nghĩa là điều chỉnh một model đã pre-train cho một tác vụ cụ thể, không phải train từ đầu. Vì vậy bạn không cần hàng triệu ticket. Thứ bạn cần là nhãn nhất quán.

Bước 1: Định dạng ticket thành cặp đầu vào và nhãn

Chọn hướng đơn giản: coi phân loại như một bài sinh văn bản ngắn. Model đọc ticket rồi sinh ra tên nhãn. Hướng này khớp với TaskType.CAUSAL_LM trong ví dụ chính thức của PEFT. Mỗi dòng JSONL có thể trông như sau (định dạng prompt do bạn tự chọn):

{"text": "Ticket: Tôi bị trừ tiền hai lần cho đơn #4821.\nNhãn:", "label": "thanh toán"}
{"text": "Ticket: Không nhận được mã OTP khi đăng nhập.\nNhãn:", "label": "đăng nhập"}

Kiểm tra: đếm số ticket theo từng nhãn. Nếu một nhãn chiếm phần lớn dữ liệu, ghi lại ngay, vì nó sẽ làm con số accuracy ở bước đánh giá đẹp hơn thực tế.

Bước 2: Chia dữ liệu và khóa tập test

Đây là bước quyết định kết quả của bạn có đáng tin hay không. Chia thành ba phần: train để học, validation để chỉnh hyperparameter, test chỉ dùng đúng một lần sau khi mọi quyết định đã chốt.

import random
random.seed(42)
random.shuffle(tickets)
n = len(tickets)
train = tickets[: int(n * 0.70)]
val   = tickets[int(n * 0.70): int(n * 0.85)]
test  = tickets[int(n * 0.85):]

Thử hình dung với 2.000 ticket: bạn có 1.400 mẫu train, 300 mẫu validation, 300 mẫu test. Ghi file test ra đĩa rồi đừng mở lại cho tới bước 5.

Kiểm tra: cả ba tập đều có đủ mọi nhãn. Nếu ticket có cùng một khách hoặc cùng một luồng email, cân nhắc xếp chúng vào cùng một tập để tránh lọt thông tin giữa các tập.

Bước 3: Cấu hình LoRA và bọc base model

Đoạn dưới dùng đúng các giá trị trong quicktour của PEFT, chỉ xếp lại thứ tự tham số cho dễ đọc. Biến base_model là model bạn đã nạp từ trước (phần nạp model được lược đi cho gọn):

from peft import LoraConfig, TaskType, get_peft_model

config = LoraConfig(
    r=8,
    lora_alpha=32,
    lora_dropout=0.1,
    target_modules=["q_proj"],
    task_type=TaskType.CAUSAL_LM,
    inference_mode=False,
)
model = get_peft_model(base_model, config)

Hai tham số bạn sẽ chỉnh nhiều nhất là r và target_modules.

Với r, tức rank, tăng lên nghĩa là thêm tham số phải train và thêm sức học, đổi lại tốn tài nguyên hơn và dễ overfit hơn. target_modules quyết định LoRA gắn vào lớp nào: ở đây chỉ q_proj, còn muốn phủ mọi lớp linear như QLoRA thì đặt target_modules="all-linear", khi đó tỷ lệ tham số trainable sẽ lớn hơn.

Còn lora_alpha=32 và lora_dropout=0.1 là giá trị lấy từ ví dụ chính thức. Lời khuyên cho lần đầu là giữ nguyên hai số này, để mỗi thí nghiệm chỉ đổi một biến (r hoặc target_modules) và bảng so sánh ở bước 4 còn đọc được.

Kiểm tra: in số tham số trainable trên tổng số tham số. Kết quả phải là một tỷ lệ rất nhỏ, cùng cỡ với 0,0689% ở ví dụ Llama-3.2-1B. Nếu tỷ lệ gần 100%, LoRA chưa được áp đúng và bạn đang fine-tune toàn bộ model.

Bước 4: Huấn luyện, nhưng đọc validation chứ không đọc train loss

Với CAUSAL_LM, cách hiểu đơn giản hóa là: model học đoán phần chữ tiếp theo. Vì thế mỗi mẫu train là chuỗi đầu vào nối với nhãn, và lúc dự đoán bạn chỉ đưa phần “Ticket: …\nNhãn:” rồi đọc chữ model sinh ra.

Phác thảo dưới đây chỉ minh họa phần dữ liệu và phần chấm điểm; vòng train bạn dùng trainer quen thuộc của hệ sinh thái Hugging Face, còn predict là hàm bạn tự viết để gọi model sinh nhãn:

# Phác thảo đơn giản hóa
def to_training_text(t):
    return t["text"] + " " + t["label"]

train_texts = [to_training_text(t) for t in train]

def accuracy(rows, predict):
    correct = sum(1 for t in rows if predict(t["text"]).strip() == t["label"])
    return correct / len(rows)

def majority_baseline(rows):
    labels = [t["label"] for t in rows]
    top = max(set(labels), key=labels.count)
    return labels.count(top) / len(rows)

Hàm cuối là thứ nhiều người bỏ qua. Thử hình dung 150 trong 300 ticket validation mang nhãn “thanh toán”: một “model” lười chỉ biết trả lời “thanh toán” đã đạt 50%. Nếu LoRA của bạn đạt 240 trên 300, tức 80%, thì phần đáng kể là khoảng cách 30 điểm so với baseline, không phải con số 80% đứng một mình.

Bẫy dễ gặp nhất là train loss giảm đều, rất thấp, và người làm vội báo cáo thành công. Nhưng model có thể đạt loss thấp trên train chỉ vì đã thuộc lòng dữ liệu. Với 1.400 ticket và nhiều epoch, chuyện này hoàn toàn có thể xảy ra.

Thử hình dung: sau epoch 3, train loss vẫn giảm nhưng accuracy trên 300 ticket validation đứng yên, thậm chí giảm. Đó là tín hiệu nên dừng. Lúc này bạn được phép thử phương án khác, như giảm epoch, đổi r hoặc chuyển sang all-linear, miễn là mọi lần so sánh đều làm trên validation.

Kiểm tra: lưu một bảng nhỏ ghi cấu hình, accuracy validation và baseline. Khách hàng hỏi “sao chọn r=8” thì bạn mở bảng này ra.

Bước 5: Mở tập test đúng một lần, rồi lưu adapter

Khi đã chốt cấu hình, chạy accuracy(test, predict) trên 300 ticket test và ghi lại kết quả. Đó là con số bạn báo cáo. Nếu kết quả không như ý, đừng quay lại chỉnh rồi chạy test lần nữa: làm vậy thì tập test thành một tập validation thứ hai và con số mất giá trị.

Lưu adapter lại, bạn sẽ thấy nó nhỏ đến bất ngờ. Tài liệu PEFT cho biết adapter của opt-350m chỉ khoảng 6MB, trong khi trọng số đầy đủ khoảng 700MB.

Lần đầu làm, lỗi thường nằm ở đâu?

Phản xạ hay gặp là tăng r ngay khi kết quả chưa đẹp. Rank cao hơn cho model nhiều sức học hơn, nhưng với vài nghìn ticket thì cũng dễ thuộc lòng hơn. Nên kiểm tra lại nhãn trước khi tăng rank.

Nhãn không nhất quán là nguồn lỗi kế tiếp. Hai nhân viên support có thể xếp cùng một ticket “bị trừ tiền nhưng không đăng nhập được” vào hai nhóm khác nhau, và model chỉ học được đến mức nhãn cho phép.

Khó phát hiện nhất là nhìn trộm tập test. Con số trông vẫn bình thường, chỉ là không còn đáng tin.

Ở chỗ khách hàng, kỹ năng này trông ra sao?

Hugging Face giải thích lý do có PEFT khá thẳng: cứ mỗi tác vụ lại fine-tune lại mọi tham số thì chi phí ngày càng lớn và khó làm trong thực tế. Trong một dự án deployment, chuyện đó rất cụ thể. Khách có một hàng đợi ticket, sau này thêm hàng đợi thứ hai, rồi một đơn vị con muốn bộ nhãn riêng.

Vì base model đóng băng và adapter chỉ là phần chênh lệch, bạn có thể giữ một base model và hoán đổi nhiều LoRA theo tác vụ. Thử hình dung ba bộ phân loại cho ba bộ phận: tổng cộng chỉ thêm vài adapter nhỏ, không phải ba bản model đầy đủ. Đó là câu trả lời đội hạ tầng của khách muốn nghe.

Việc đầu tiên nên làm khi đến chỗ khách không phải là train. Hãy ngồi với trưởng nhóm support để chốt định nghĩa từng nhãn, rồi lấy một mẫu ticket kiểm tra xem nhãn cũ có nhất quán không. Phần lớn rủi ro của dự án nằm ở bước đó, không nằm ở LoraConfig.

Với developer Việt Nam đang nhắm vai trò FDE, khi đọc JD hãy để ý các từ khóa như PEFT, LoRA, fine-tuning, evaluation. Trong CV, đừng chỉ ghi “fine-tune LLM”. Hãy ghi rõ tỷ lệ tham số trainable, kích thước adapter, baseline bạn so sánh, cách chia dữ liệu và việc tập test chỉ chạy một lần.

Ai cũng chạy được get_peft_model. Thứ khách hàng trả tiền là một con số đánh giá mà họ tin được.

5 nguồn
Đọc tiếp trên lộ trình · Chặng 3: AI ứng dụngGọi thẳng API Claude và OpenAI: tự viết vòng tool use, rồi đối chiếu với GeminiTrước khi giao mọi thứ cho framework, bạn nên tự đi hết một vòng request, streaming và tool call với Claude và OpenAI, rồi biết Gemini khác ở đâu. Đó là những thứ bạn sẽ phải gỡ lỗi khi ngồi ở văn phòng khách hàng.