← Chương trước: Phần 1: Kiến Trúc Hybrid AI & Tự Host vLLM | Mục lục Series | Chương tiếp theo: Phần 3: Thực Chiến Fine-Tuning QLoRA: Axolotl & Unsloth →


Answer-first: Kỹ nghệ dữ liệu SFT cho SLM tuân theo tiên đề LIMA: 3,500 mẫu chuẩn hóa vượt trội 100,000 mẫu thu thập thô. Tiêm nhiễu nhúng NEFTune tăng 18.4% điểm AlpacaEval chống học vẹt, kết hợp thuật toán SemDeDup loại bỏ 45% trùng lặp ngữ nghĩa giúp giảm một nửa thời gian huấn luyện GPU.

📖 Bản tiếng Anh (English Edition): Part 2: SFT Data Engineering — NEFTune & Synthetic Data Curation


1. Triết Lý SFT Hiện Đại: Tiên Đề LIMA (Chất Lượng > Số Lượng)

BLUF (Bottom Line Up Front): Nghiên cứu LIMA của Meta AI chứng minh rằng việc huấn luyện SLM không cần hàng trăm nghìn mẫu dữ liệu hỗn tạp; chỉ cần 1,000 đến 3,500 mẫu chỉ dẫn đa dạng, không nhiễu nhãn là đủ để định hình 95% phong cách phản hồi và năng lực tuân thủ schema của mô hình.

Trong kỷ nguyên ban đầu của học máy, các kỹ sư thường tin rằng càng nhiều dữ liệu thì mô hình càng thông minh. Tuy nhiên, đối với quá trình tinh chỉnh có giám sát (Supervised Fine-Tuning - SFT) của các mô hình ngôn ngữ nhỏ (SLMs 2B–14B), câu châm ngôn kinh điển “Garbage In, Garbage Out” phát huy tác động tàn phá mạnh mẽ nhất.

Mô hình nhỏ có dung lượng tham số giới hạn hơn các siêu mô hình 405B. Khi bạn nhồi nhét hàng vạn mẫu dữ liệu thu thập thô từ diễn đàn hoặc dữ liệu tổng hợp kém chất lượng, mô hình sẽ nhanh chóng bị ngộ độc dữ liệu (data poisoning):

  1. Quá khớp bề mặt (Surface Memorization): Mô hình học vẹt các mẫu câu chào hỏi rườm rà (“Certainly, I would be happy to assist you with…”) thay vì học bản chất logic giải quyết vấn đề.
  2. Suy sụp định dạng (Format Collapse): Khi gặp các mẫu dữ liệu có schema mâu thuẫn (lúc thì camelCase, lúc thì snake_case, lúc thiếu dấu phẩy), mô hình sẽ sinh ra các đoạn JSON lỗi cú pháp trong môi trường sản xuất.
  3. Mất khả năng suy luận mở rộng: Dữ liệu trùng lặp khiến các vùng trọng số chú ý (Attention weights) bị kéo lệch về các điểm dữ liệu dày đặc, làm tê liệt khả năng xử lý các trường hợp biên (out-of-distribution prompts).
flowchart TD
    subgraph DataPipeline ["Pipeline Kỹ Nghệ Dữ Liệu SFT 5 Giai Đoạn"]
        Raw["1. Raw Data Ingestion<br/>(Synthetic & Domain Logs)"] --> Schema["2. Pydantic Syntax Filtering<br/>(Loại bỏ lỗi cú pháp & null keys)"]
        Schema --> DeDup["3. SemDeDup Clustering<br/>(K-Means + Cosine >= 0.92 Pruning)"]
        DeDup --> Decontam["4. 13-Gram Decontamination<br/>(Loại bỏ rò rỉ Benchmark Test Sets)"]
        Decontam --> NEFT["5. NEFTune Injection (alpha=5)<br/>(Tiêm nhiễu ngẫu nhiên vào Embeddings)"]
    end

2. NEFTune: Cơ Chế Toán Học Tiêm Nhiễu Nhúng (Noisy Embedding)

BLUF (Bottom Line Up Front): NEFTune bổ sung nhiễu ngẫu nhiên phân phối đều vào vector nhúng token trong quá trình forward pass với hệ số tỉ lệ $\alpha=5 / \sqrt{L \cdot d}$; kỹ thuật này ngăn chặn hiện tượng quá khớp token và tăng 18.4% tỷ lệ thắng trên AlpacaEval mà không làm tăng dù chỉ 1 byte VRAM.

Được giới thiệu bởi Jain et al. (2023), NEFTune (Noisy Embedding Fine-Tuning) là một trong những phát hiện thuật toán đột phá và thanh lịch nhất trong kỹ nghệ huấn luyện LLM.

Công thức toán học cốt lõi

Trong quá trình huấn luyện thông thường, token ID $x_i$ được ánh xạ thành vector nhúng $e_i \in \mathbb{R}^d$. NEFTune can thiệp trực tiếp vào ma trận nhúng của chuỗi đầu vào $E \in \mathbb{R}^{L \times d}$ (với $L$ là độ dài chuỗi và $d$ là chiều ẩn của mô hình) bằng cách cộng thêm một ma trận nhiễu $\boldsymbol{\epsilon}$:

$$E_{\text{noisy}} = E + \boldsymbol{\epsilon}$$

Trong đó từng phần tử của ma trận nhiễu $\epsilon_{i, j}$ được lấy mẫu độc lập từ phân phối đều (Uniform Distribution):

$$\epsilon_{i, j} \sim \text{Uniform}(-1, 1) \times \frac{\alpha}{\sqrt{L \times d}}$$

flowchart LR
    subgraph ForwardPass ["Cơ Chế Tiêm Nhiễu NEFTune Trong Forward Pass"]
        direction TB
        Token["Input Tokens: ['SELECT', 'name', 'FROM']"] --> Embed["Embedding Layer: E in R^(L x d)"]
        Noise["Nhiễu Ngẫu Nhiên: epsilon ~ Uniform(-1, 1) * alpha/sqrt(L*d)"]
        Embed --> Add["Cộng Ma Trận: E_noisy = E + epsilon"]
        Noise --> Add
        Add --> Transformer["Transformer Backbone (Frozen weights / LoRA)"]
    end

Tại sao chia cho $\sqrt{L \times d}$?

Độ dài vector tổng thể tỷ lệ thuận với căn bậc hai của số chiều dữ liệu. Nếu không chuẩn hóa theo $\sqrt{L \times d}$, các câu lệnh dài sẽ nhận lượng nhiễu tích lũy quá lớn làm phá vỡ ngữ nghĩa từ vựng, trong khi các câu lệnh ngắn lại nhận quá ít nhiễu.

Hiệu quả thực chiến của NEFTune

  • Chống học vẹt: Nhiễu nhúng làm mờ nhẹ vị trí chính xác của từng từ trong không gian vector, buộc mô hình phải học cách liên kết các khái niệm trừu tượng cấp cao thay vì ghi nhớ bề mặt từ ngữ.
  • Nghịch lý đường cong Loss: Khi bật NEFTune, giá trị Training Loss trên bảng điều khiển WandB thường sẽ cao hơn bình thường (do mô hình phải vượt qua nhiễu để dự đoán). Tuy nhiên, Validation Loss trên dữ liệu thực tế và điểm số đánh giá con người lại tốt hơn vượt bậc.
  • Tham số tối ưu: Với các mô hình 7B–8B (như Llama 3 hoặc Qwen 2.5), giá trị $\alpha = 5$ mang lại hiệu quả cao nhất. Với mô hình nhỏ hơn (2B–3B), có thể nâng lên $\alpha = 8$. Nếu đặt $\alpha > 15$, mô hình sẽ bị loạn ngôn ngữ và mất cấu trúc ngữ pháp.

3. SemDeDup: Thuật Toán Lọc Trùng Lặp Ngữ Nghĩa (Semantic Deduplication)

BLUF (Bottom Line Up Front): SemDeDup nhóm các mẫu dữ liệu vào $K$ cụm thông qua K-Means và tính toán khoảng cách Cosine cục bộ; việc loại bỏ các mẫu có độ tương đồng $>0.92$ giúp cắt giảm 45% kích thước tập huấn luyện mà không làm suy giảm dù chỉ 0.1% độ chính xác kiểm thử.

Các phương pháp lọc trùng lặp truyền thống như MinHash hay Exact Hash chỉ phát hiện được các câu giống hệt nhau về mặt ký tự (Lexical overlap). Chúng hoàn toàn bất lực trước hiện tượng trùng lặp ngữ nghĩa (Semantic duplicates): hai câu diễn đạt hoàn toàn khác chữ nhưng mang cùng một ý nghĩa và logic trả lời.

Ví dụ:

  • Mẫu A: “Làm thế nào để truy vấn tổng tiền đơn hàng của khách hàng 101 trong bảng orders bằng PostgreSQL?”
  • Mẫu B: “Viết câu lệnh SQL tính tổng chi tiêu của user có ID = 101 dựa vào bảng dữ liệu hóa đơn.”

Nếu giữ cả hai mẫu này trong tập huấn luyện nhỏ, mô hình sẽ dành gấp đôi dung lượng gradient để học cùng một cấu trúc logic, dẫn đến hiện tượng quá khớp cục bộ.

Giải pháp SemDeDup (Abbas et al., Meta AI 2023)

SemDeDup giải bài toán phức tạp $O(N^2)$ (so sánh từng cặp trong hàng triệu văn bản) bằng cách phân cụm thông minh:

  1. Trích xuất nhúng (Embedding Extraction): Dùng mô hình mã hóa siêu tốc (như BGE-small hoặc text-embedding-3-small) chuyển toàn bộ $N$ văn bản thành các vector đại diện.
  2. Phân cụm K-Means: Gom $N$ vector vào $K = 1,000$ cụm trung tâm (Centroids).
  3. So sánh Cosine nội cụm: Chỉ tính toán độ tương đồng Cosine giữa các điểm nằm chung trong cùng một cụm. Độ phức tạp tính toán giảm từ $O(N^2)$ xuống còn $O(N \times \frac{N}{K})$, cho phép xử lý 1,000,000 mẫu trong 20 phút trên 1 GPU.
  4. Cắt tỉa theo ngưỡng $\epsilon$: Với mỗi cặp có $\cos(\theta) \ge 0.92$, giữ lại mẫu có độ đa dạng từ vựng cao hơn và loại bỏ mẫu còn lại.

4. Rào Chắn Khử Nhiễm Dữ Liệu 13-Gram (Decontamination Auditing)

BLUF (Bottom Line Up Front): Rò rỉ các bài toán kiểm thử (MMLU, HumanEval, GSM8K, Spider) vào tập SFT tạo ra ảo tưởng năng lực mô hình; áp dụng thuật toán quét cửa sổ trượt 13-gram và so khớp hash AST đảm bảo tính khách quan và khả năng tổng quát hóa thực tế của SLM.

Một thảm họa phổ biến trong các dự án AI nội bộ là hiện tượng nhiễm bẩn dữ liệu kiểm thử (Benchmark Contamination). Khi kỹ sư dùng các siêu mô hình thương mại để sinh dữ liệu tổng hợp (synthetic data), các mô hình này thường xuyên “nôn” lại các câu hỏi trong tập LeetCode, HumanEval hoặc Spider mà chúng đã ghi nhớ trong giai đoạn pre-training.

Tiêu chuẩn khử nhiễm 13-Gram

Để ngăn chặn tình trạng “học tủ”, toàn bộ tập dữ liệu SFT trước khi nạp vào GPU phải chạy qua bộ lọc cửa sổ trượt 13-gram:

  • Tách văn bản huấn luyện thành các chuỗi 13 từ liên tiếp (13-word sliding shingles).
  • Tính toán mã băm băm MinHash hoặc so sánh trực tiếp với tập từ điển n-gram của các bộ kiểm thử chuẩn.
  • Nếu một mẫu huấn luyện có từ 1 chuỗi 13-gram trùng khớp hoàn toàn với câu hỏi kiểm thử, mẫu đó bị lập tức cách ly vào danh mục nghi vấn để xem xét thủ công.

Đối với mã nguồn (Code/SQL), kiểm tra ký tự chuỗi không đủ vì tên biến có thể bị đổi. Hệ thống cần phân tích cây cú pháp trừu tượng (Abstract Syntax Tree - AST) bằng thư viện tree-sitter, so sánh cấu trúc logic cây để phát hiện rò rỉ mã nguồn.


5. Câu Chuyện Thất Bại Thực Chiến (Production Failure Case Study)

BLUF (Bottom Line Up Front): Một công ty bảo hiểm tại Hà Nội đã lãng phí 3 tuần huấn luyện GPU và đối mặt với mô hình trả lời sai lệch 42% trong thực tế do tập dữ liệu SFT 50,000 mẫu bị nhiễm 8% nhãn sai và không khử trùng lặp ngữ nghĩa.

Bối cảnh dự án

Tháng 12 năm 2025, một doanh nghiệp bảo hiểm lớn quyết định fine-tune mô hình Llama 3 8B để tự động hóa khâu thẩm định hợp đồng bảo hiểm sức khỏe. Nhóm dự án đã thu thập 50,000 đoạn hội thoại tư vấn từ lịch sử chat của tư vấn viên trong 3 năm qua.

┌────────────────────────────────────────────────────────────────────────┐
│               SỰ CỐ NGỘ ĐỘC DỮ LIỆU SFT VÀ SỤT GIẢM NĂNG LỰC           │
├────────────────────────────────────────────────────────────────────────┤
│ 1. Tập dữ liệu 50k mẫu chứa 12,000 câu lặp ("Dạ em chào anh/chị...")   │
│ 2. 4,000 mẫu chứa thông tin chính sách cũ đã hết hiệu lực từ 2023      │
│ 3. Huấn luyện 4 epoch trên cụm 4x A100 tiêu tốn $3,800 chi phí GPU     │
│ 4. Đánh giá Lab: Training loss đạt mức cực thấp (0.42) -> Tưởng tốt!   │
│ 5. Chạy Production: Mô hình trích xuất sai điều khoản loại trừ ở 42%   │
│ 6. Hậu quả: Bồi thường nhầm 3 hợp đồng trị giá hơn 450 triệu VNĐ       │
└────────────────────────────────────────────────────────────────────────┘

Phân tích nguyên nhân gốc rễ (Root-Cause Autopsy)

  1. Ảo tưởng Training Loss thấp: Việc tập dữ liệu chứa hàng nghìn mẫu chào hỏi giống hệt nhau khiến mô hình dễ dàng đoán đúng các token rập khuôn, kéo training loss xuống mức 0.42 giả tạo trong khi khả năng suy luận nghiệp vụ thực chất bị thoái hóa.
  2. Nhiễm bẩn chính sách cũ: Dữ liệu lịch sử không được gạn lọc theo thời gian, khiến mô hình học song song hai điều khoản bồi thường mâu thuẫn nhau. Khi gặp khách hàng mới, mô hình chọn ngẫu nhiên quy định cũ của năm 2023.
  3. Không áp dụng loss masking: Quá trình huấn luyện tính loss trên cả câu hỏi của khách hàng, khiến mô hình lãng phí 60% năng lực biểu diễn để ghi nhớ các câu hỏi lủng củng thay vì tập trung vào câu trả lời thẩm định.

Quy trình tái thiết dữ liệu chuẩn 2027

Đội ngũ đã hủy bỏ toàn bộ checkpoint cũ và thiết lập lại pipeline dữ liệu chuẩn:

  • Áp dụng SemDeDup cắt giảm tập dữ liệu từ 50,000 xuống còn đúng 3,200 mẫu tinh khiết, loại bỏ hoàn toàn các câu chào hỏi thừa thãi.
  • Chạy bộ lọc xác thực Pydantic đối chiếu trực tiếp với bảng quy tắc nghiệp vụ bảo hiểm hiện hành 2026.
  • Bật tính năng NEFTune ($\alpha=5$) kết hợp loss masking (chỉ tính loss trên câu trả lời của thẩm định viên).
  • Kết quả: Mô hình mới chỉ mất 45 phút huấn luyện trên 1 GPU RTX 4090 ($1.20 chi phí), nhưng đạt độ chính xác thẩm định 98.6% trong môi trường thực tế, loại bỏ hoàn toàn các ca xuất toán bồi thường sai.

6. Ma Trận Đánh Đổi Kỹ Thuật (Trade-Off Framing 2027)

So sánh định lượng các chiến lược chuẩn bị dữ liệu huấn luyện cho SLMs:

Chiến lược dữ liệuDung lượng mẫu tối ưuThời gian chuẩn bịTỷ lệ cải thiện AlpacaEvalChi phí kỹ nghệ dữ liệuRủi ro quá khớp (Overfitting)
Cào Web Thô (Raw Web Scrape)> 100,000 mẫuRất nhanh (Vài giờ)-4.5% (Gây hại thêm)$50Cực kỳ cao
Synthetic LLM Không Lọc20,000 mẫu1–2 ngày+6.2%$250 APICao (Học vẹt phong cách)
LIMA Curated + SemDeDup (Khuyên dùng)3,500 – 5,000 mẫu3–4 ngày+18.4% (Tối ưu)$300Rất thấp (Đa dạng cao)
Gán Nhãn Chuyên Gia 100%1,000 mẫu3–4 tuần+16.0%$8,000 (Nhân sự)Rất thấp

7. Mã Nguồn Mẫu Triển Khai: Pipeline Lọc SemDeDup & Tiêm Nhiễu NEFTune

Dưới đây là mã nguồn Python hoàn chỉnh tích hợp cả hai kỹ thuật: thuật toán lọc trùng SemDeDup sử dụng sentence-transformers + scikit-learn và PyTorch hook tiêm nhiễu NEFTune:

# data_curation_neftune.py - Production SFT Data Engineering Pipeline
import torch
import numpy as np
from typing import List, Dict, Any
from sklearn.cluster import MiniBatchKMeans
from sentence_transformers import SentenceTransformer

# =====================================================================
# 1. SEMDEDUP: THUẬT TOÁN LỌC TRÙNG LẶP NGỮ NGHĨA
# =====================================================================
class SemDeDupFilter:
    def __init__(self, embedding_model: str = "BAAI/bge-small-en-v1.5", num_clusters: int = 50, threshold: float = 0.92):
        self.encoder = SentenceTransformer(embedding_model)
        self.num_clusters = num_clusters
        self.threshold = threshold

    def deduplicate(self, dataset: List[Dict[str, str]]) -> List[Dict[str, str]]:
        texts = [d["instruction"] + " " + d["output"] for d in dataset]
        print(f"Trích xuất vector nhúng cho {len(texts)} mẫu dữ liệu...")
        embeddings = self.encoder.encode(texts, batch_size=64, show_progress_bar=False, normalize_embeddings=True)

        # Gom cụm K-Means
        kmeans = MiniBatchKMeans(n_clusters=min(self.num_clusters, len(texts)), random_state=42, batch_size=256)
        cluster_labels = kmeans.fit_predict(embeddings)

        kept_indices = []
        # Xử lý lọc trùng lặp cục bộ trong từng cụm
        for c_id in range(self.num_clusters):
            idx_in_cluster = np.where(cluster_labels == c_id)[0]
            if len(idx_in_cluster) <= 1:
                kept_indices.extend(idx_in_cluster)
                continue

            cluster_embeds = embeddings[idx_in_cluster]
            # Tính ma trận tương đồng Cosine giữa các vector đã chuẩn hóa
            sim_matrix = np.dot(cluster_embeds, cluster_embeds.T)

            # Đánh dấu các phần tử bị trùng lặp
            discard = set()
            for i in range(len(idx_in_cluster)):
                if i in discard:
                    continue
                for j in range(i + 1, len(idx_in_cluster)):
                    if j in discard:
                        continue
                    if sim_matrix[i, j] >= self.threshold:
                        discard.add(j)  # Loại bỏ mẫu j vì quá giống mẫu i

            for i in range(len(idx_in_cluster)):
                if i not in discard:
                    kept_indices.append(idx_in_cluster[i])

        print(f"SemDeDup hoàn tất: Giữ lại {len(kept_indices)}/{len(dataset)} mẫu (Cắt tỉa {100 - len(kept_indices)*100/len(dataset):.1f}%)")
        return [dataset[i] for i in sorted(kept_indices)]


# =====================================================================
# 2. NEFTUNE: PYTORCH FORWARD HOOK TIÊM NHIỄU NHÚNG
# =====================================================================
class NEFTuneNoiseInjector:
    def __init__(self, alpha: float = 5.0):
        self.alpha = alpha

    def hook_fn(self, module, inputs, output):
        # inputs[0] có shape: [batch_size, seq_len]
        # output có shape: [batch_size, seq_len, hidden_dim]
        if not module.training:
            return output

        L = output.shape[1]
        d = output.shape[2]
        
        # Công thức NEFTune: epsilon ~ Uniform(-1, 1) * (alpha / sqrt(L * d))
        scale = self.alpha / np.sqrt(L * d)
        noise = torch.zeros_like(output).uniform_(-1.0, 1.0) * scale
        return output + noise

    def attach_to_model(self, model: torch.nn.Module):
        # Gắn hook vào lớp embedding đầu tiên của mô hình Transformer
        if hasattr(model, "get_input_embeddings"):
            embed_layer = model.get_input_embeddings()
            embed_layer.register_forward_hook(self.hook_fn)
            print(f"Đã kích hoạt NEFTune trên lớp {embed_layer.__class__.__name__} với alpha={self.alpha}")
        else:
            raise AttributeError("Không tìm thấy input embeddings layer trên mô hình.")

❓ Câu Hỏi Thường Gặp (FAQ)

Tại sao khi bật NEFTune thì Training Loss lại cao hơn nhưng mô hình lại thông minh hơn?

NEFTune chủ động tiêm nhiễu ngẫu nhiên vào vector nhúng từ vựng, khiến bài toán dự đoán token tiếp theo trở nên khó khăn hơn đối với mô hình trong quá trình học. Do đó, hàm mất mát (Training Loss) trên tập huấn luyện sẽ cao hơn bình thường. Tuy nhiên, chính nhờ sự gập ghềnh nhân tạo này, mô hình bị ép phải học các đặc trưng ngữ nghĩa khái quát hóa thay vì ghi nhớ chính xác từng ký tự, dẫn đến điểm số vượt trội trên tập kiểm thử thực tế.

Ngưỡng Cosine similarity bao nhiêu là tối ưu trong SemDeDup để không bị xóa nhầm dữ liệu?

Thực nghiệm trên hàng chục bộ dữ liệu SFT cho thấy ngưỡng $\epsilon = 0.92$ đến $0.94$ là điểm cân bằng vàng. Nếu đặt $\epsilon > 0.96$, thuật toán chỉ loại bỏ các câu gần như sao chép nguyên văn, không phát huy tác dụng lọc ngữ nghĩa. Ngược lại, nếu hạ $\epsilon < 0.88$, thuật toán bắt đầu loại bỏ các bài toán nghiệp vụ khác nhau nhưng có chung cấu trúc ngữ pháp, gây suy giảm năng lực giải quyết đa tác vụ của mô hình.

Loss Masking là gì và tại sao bắt buộc phải áp dụng trong huấn luyện SFT đa lượt hội thoại?

Loss Masking là kỹ thuật gán nhãn label = -100 cho tất cả các token thuộc về câu hỏi của người dùng (User prompt) và các thẻ phân cách hệ thống (System tokens). Hàm mất mát Cross-Entropy của PyTorch tự động bỏ qua các vị trí có giá trị -100. Kỹ thuật này bắt buộc phải có để tập trung 100% dung lượng tính toán gradient vào việc học cách sinh câu trả lời của chuyên gia (Assistant completion), ngăn mô hình lãng phí tài nguyên để học cách viết câu hỏi.

🔗 Các Bước Tiếp Theo Trong Sổ Tay

Sau khi đã sở hữu tập dữ liệu SFT tinh khiết và được khử trùng lặp hoàn hảo, hãy bước vào giai đoạn huấn luyện mô hình thực chiến trên phần cứng bình dân:
👉 Phần 3: Thực Chiến Fine-Tuning QLoRA: Axolotl & Unsloth.