跳转至

十一:手撕代码与项目深挖

LoRA 线性层

import torch
import torch.nn as nn
import math

class LoRALinear(nn.Module):
    def __init__(self, in_features: int, out_features: int, r: int = 8,
                 lora_alpha: int = 16, bias: bool = True, dropout: float = 0.0):
        super().__init__()
        self.r = r
        self.lora_alpha = lora_alpha
        self.scaling = lora_alpha / r

        # 原始线性层,权重冻结
        self.linear = nn.Linear(in_features, out_features, bias=bias)
        self.linear.weight.requires_grad = False
        if bias:
            self.linear.bias.requires_grad = False

        # LoRA 低秩矩阵 A (r, in_features) 和 B (out_features, r)
        self.lora_A = nn.Parameter(torch.zeros(r, in_features))
        self.lora_B = nn.Parameter(torch.zeros(out_features, r))
        self.lora_dropout = nn.Dropout(dropout) if dropout > 0 else nn.Identity()

        self.reset_lora_parameters()

    def reset_lora_parameters(self):
        # A 使用 Kaiming 初始化,B 初始化为零,使初始时 ΔW = 0
        nn.init.kaiming_uniform_(self.lora_A, a=math.sqrt(5))
        nn.init.zeros_(self.lora_B)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        # 原始输出 (冻结)
        result = self.linear(x)
        # LoRA 旁路:x -> A^T -> B^T,然后乘以缩放因子
        lora_out = self.lora_dropout(x) @ self.lora_A.T  # (..., r)
        lora_out = lora_out @ self.lora_B.T               # (..., out_features)
        result = result + self.scaling * lora_out
        return result

合并 LoRA 权重

def merge_lora_to_linear(lora_layer: LoRALinear) -> nn.Linear:
    """将 LoRA 权重合并到原始线性层,返回一个新的标准线性层"""
    # 原始权重 (out_features, in_features)
    W = lora_layer.linear.weight.data
    A = lora_layer.lora_A.data  # (r, in_features)
    B = lora_layer.lora_B.data  # (out_features, r)

    # 计算增量 ΔW = (alpha / r) * B @ A
    delta_W = lora_layer.scaling * torch.matmul(B, A)  # (out_features, in_features)
    merged_weight = W + delta_W

    # 构建新的线性层
    new_linear = nn.Linear(lora_layer.in_features, lora_layer.out_features,
                           bias=lora_layer.linear.bias is not None)
    new_linear.weight.data = merged_weight
    if lora_layer.linear.bias is not None:
        new_linear.bias.data = lora_layer.linear.bias.data
    return new_linear

SFT Data Collator(带 loss masking)

from typing import List, Dict

class SFTDataCollator:
    """
    将对话样本转换为训练批次,并对 prompt 部分进行 loss masking。
    要求每个样本包含 'input_ids' 和 'labels',其中 prompt 部分 labels 已设为 -100。
    在 batch 内进行右侧填充,填充 token 的 label 也设为 -100。
    """
    def __init__(self, pad_token_id: int = 0):
        self.pad_token_id = pad_token_id

    def __call__(self, features: List[Dict[str, torch.Tensor]]) -> Dict[str, torch.Tensor]:
        max_len = max(f['input_ids'].size(0) for f in features)

        padded_input_ids, padded_labels, attention_masks = [], [], []
        for f in features:
            input_ids = f['input_ids']
            labels = f['labels']
            seq_len = input_ids.size(0)
            pad_len = max_len - seq_len

            # 右侧填充
            padded_input = torch.cat([input_ids, torch.full((pad_len,), self.pad_token_id, dtype=input_ids.dtype)])
            padded_label = torch.cat([labels, torch.full((pad_len,), -100, dtype=labels.dtype)])
            attn_mask = torch.cat([torch.ones(seq_len, dtype=torch.long), torch.zeros(pad_len, dtype=torch.long)])

            padded_input_ids.append(padded_input)
            padded_labels.append(padded_label)
            attention_masks.append(attn_mask)

        return {
            'input_ids': torch.stack(padded_input_ids),
            'labels': torch.stack(padded_labels),
            'attention_mask': torch.stack(attention_masks)
        }

序列打包(packing)及注意力掩码生成

def pack_sequences(
    input_ids_list: List[torch.Tensor],
    labels_list: List[torch.Tensor],
    eos_token_id: int = 2,
    pad_token_id: int = 0
) -> Dict[str, torch.Tensor]:
    """
    将多个样本拼接为一个长序列,生成分块对角因果注意力掩码 (4D)。
    返回 input_ids (1, total_len), labels (1, total_len), attention_mask (1, 1, total_len, total_len)
    """
    # 存储每个片段的起止位置
    segments = []
    all_ids = []
    all_labels = []
    offset = 0
    for inp, lab in zip(input_ids_list, labels_list):
        L = inp.size(0)
        all_ids.append(inp)
        all_labels.append(lab)
        segments.append((offset, offset + L - 1))
        offset += L
        # 插入 EOS 分隔符,其 label 设为 -100
        all_ids.append(torch.tensor([eos_token_id], dtype=inp.dtype))
        all_labels.append(torch.tensor([-100], dtype=lab.dtype))
        offset += 1

    # 移除末尾多余的分隔符
    if all_ids:
        all_ids.pop()
        all_labels.pop()
        offset -= 1

    input_ids = torch.cat(all_ids, dim=0)
    labels = torch.cat(all_labels, dim=0)
    total_len = input_ids.size(0)

    # 构建分块对角因果掩码
    # 先构造下三角因果掩码
    causal_mask = torch.tril(torch.ones(total_len, total_len, dtype=torch.bool))
    # 构造块对角掩码:允许同一样本内互相看到
    block_mask = torch.zeros(total_len, total_len, dtype=torch.bool)
    for start, end in segments:
        block_mask[start:end+1, start:end+1] = True
    # 最终掩码 = 因果掩码 AND 块对角掩码
    final_mask = causal_mask & block_mask  # (total_len, total_len)
    # 扩展到4D: (1, 1, total_len, total_len)
    final_mask = final_mask.unsqueeze(0).unsqueeze(0)

    return {
        'input_ids': input_ids.unsqueeze(0),  # (1, total_len)
        'labels': labels.unsqueeze(0),
        'attention_mask': final_mask
    }

DPO 损失函数

def dpo_loss(
    policy_logits: torch.Tensor,       # (batch, seq_len, vocab_size) 当前策略的 logits
    ref_logits: torch.Tensor,          # 参考模型的 logits
    labels: torch.Tensor,              # (batch, seq_len) 用于构建回答的 token ids
    chosen_mask: torch.Tensor,         # (batch,) bool 表示该样本是否为 chosen (True) 或 rejected (False)
    beta: float = 0.1,
    ignore_index: int = -100
) -> torch.Tensor:
    """
    计算 DPO 损失。假设数据已组织为:前半 batch 是 chosen,后半是 rejected 与其配对。
    chosen_mask 标记每个样本的归属。
    """
    # 计算每个 token 的对数似然,忽略 ignore_index 的位置
    loss_fct = torch.nn.CrossEntropyLoss(reduction='none', ignore_index=ignore_index)
    # 形状 (batch, seq_len)
    log_probs_policy = -loss_fct(policy_logits.permute(0, 2, 1), labels)
    log_probs_ref = -loss_fct(ref_logits.permute(0, 2, 1), labels)

    # 对序列求和(或平均)得到整个回答的对数似然
    # 注意:应只考虑回答部分,即 labels != ignore_index 的位置
    mask = (labels != ignore_index).float()
    sum_log_policy = (log_probs_policy * mask).sum(dim=-1)  # (batch,)
    sum_log_ref = (log_probs_ref * mask).sum(dim=-1)

    # 拆分 chosen 和 rejected
    chosen_mask = chosen_mask.bool()
    chosen_log_policy = sum_log_policy[chosen_mask]
    rejected_log_policy = sum_log_policy[~chosen_mask]
    chosen_log_ref = sum_log_ref[chosen_mask]
    rejected_log_ref = sum_log_ref[~chosen_mask]

    # 计算隐式奖励差
    chosen_reward = beta * (chosen_log_policy - chosen_log_ref)
    rejected_reward = beta * (rejected_log_policy - rejected_log_ref)

    # DPO 损失 = -log σ(chosen_reward - rejected_reward)
    loss = -torch.log(torch.sigmoid(chosen_reward - rejected_reward)).mean()
    return loss

多头自注意力(含因果掩码)

class MultiHeadSelfAttention(nn.Module):
    def __init__(self, d_model: int, n_heads: int, dropout: float = 0.1):
        super().__init__()
        assert d_model % n_heads == 0
        self.d_model = d_model
        self.n_heads = n_heads
        self.d_k = d_model // n_heads

        self.W_q = nn.Linear(d_model, d_model)
        self.W_k = nn.Linear(d_model, d_model)
        self.W_v = nn.Linear(d_model, d_model)
        self.W_o = nn.Linear(d_model, d_model)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x: torch.Tensor, mask: torch.Tensor = None) -> torch.Tensor:
        B, T, C = x.shape
        # 线性变换并切分为多头
        q = self.W_q(x).view(B, T, self.n_heads, self.d_k).transpose(1, 2)  # (B, n_heads, T, d_k)
        k = self.W_k(x).view(B, T, self.n_heads, self.d_k).transpose(1, 2)
        v = self.W_v(x).view(B, T, self.n_heads, self.d_k).transpose(1, 2)

        # 计算注意力分数
        attn_scores = torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(self.d_k)  # (B, n_heads, T, T)

        # 构造因果掩码(下三角),若外部未提供
        if mask is None:
            causal_mask = torch.tril(torch.ones(T, T, device=x.device)).view(1, 1, T, T)
            attn_scores = attn_scores.masked_fill(causal_mask == 0, float('-inf'))
        else:
            # mask 预期为 (B, 1, 1, T) 或 (B, 1, T, T),根据实际情况适配
            attn_scores = attn_scores.masked_fill(mask == 0, float('-inf'))

        attn_weights = F.softmax(attn_scores, dim=-1)
        attn_weights = self.dropout(attn_weights)

        # 加权求和
        out = torch.matmul(attn_weights, v)  # (B, n_heads, T, d_k)
        out = out.transpose(1, 2).contiguous().view(B, T, C)
        return self.W_o(out)

RoPE 位置编码及其应用到 Q, K

def apply_rotary_emb(x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) -> torch.Tensor:
    """
    对输入 x 应用旋转位置编码。x 形状为 (..., seq_len, dim)
    cos, sin 形状为 (seq_len, dim//2) 或 (..., seq_len, dim//2)
    """
    # 将 x 按最后一维分成两半,分别进行旋转
    dim = x.shape[-1]
    x1, x2 = x[..., :dim//2], x[..., dim//2:]
    cos = cos.unsqueeze(-2)  # 便于广播
    sin = sin.unsqueeze(-2)
    rotated = torch.cat([x1 * cos - x2 * sin, x1 * sin + x2 * cos], dim=-1)
    return rotated

def precompute_rotary_embeddings(seq_len: int, dim: int, theta: float = 10000.0):
    """预计算 cos 和 sin 表"""
    freqs = 1.0 / (theta ** (torch.arange(0, dim, 2, dtype=torch.float32) / dim))  # (dim/2,)
    t = torch.arange(seq_len, dtype=torch.float32)
    freqs = torch.outer(t, freqs)  # (seq_len, dim/2)
    cos = torch.cos(freqs)
    sin = torch.sin(freqs)
    return cos, sin

Top-P (nucleus) 采样

def top_p_sampling(logits: torch.Tensor, p: float = 0.9, temperature: float = 1.0) -> int:
    """从 logits 中根据 Top-P 采样返回一个 token id"""
    if temperature > 0:
        logits = logits / temperature
    probs = torch.softmax(logits, dim=-1)
    # 按概率降序排列
    sorted_probs, sorted_indices = torch.sort(probs, descending=True)
    cumsum_probs = torch.cumsum(sorted_probs, dim=-1)
    # 去除累积概率超过 p 的尾部,但至少保留第一个 token
    mask = (cumsum_probs - sorted_probs) < p  # 等效于 cumsum_probs - sorted_probs < p 确保第一个保留?
    # 更直接的方法:保留累积概率 <= p 的部分,然后至少保留一个
    mask = cumsum_probs <= p
    mask[0] = True
    # 重新归一化
    filtered_probs = sorted_probs * mask.float()
    filtered_probs = filtered_probs / filtered_probs.sum()
    # 采样
    sampled_idx = torch.multinomial(filtered_probs, 1).item()
    return sorted_indices[sampled_idx].item()

Top-K 采样

def top_k_sampling(logits: torch.Tensor, k: int = 50, temperature: float = 1.0) -> int:
    """从 logits 中根据 Top-K 采样返回一个 token id"""
    if temperature > 0:
        logits = logits / temperature
    probs = torch.softmax(logits, dim=-1)
    # 选出概率最高的 k 个 token
    topk_probs, topk_indices = torch.topk(probs, k, dim=-1)
    # 重新归一化
    topk_probs = topk_probs / topk_probs.sum()
    sampled_idx = torch.multinomial(topk_probs, 1).item()
    return topk_indices[sampled_idx].item()

KV Cache 更新逻辑

def update_kv_cache(
    new_k: torch.Tensor,      # (batch, num_heads, 1, head_dim)
    new_v: torch.Tensor,      # 同上
    cache_k: torch.Tensor,    # (batch, num_heads, past_len, head_dim) 或 None
    cache_v: torch.Tensor,
    max_cache_len: int = None
):
    """
    将新 token 的 K,V 拼接到历史缓存中。若 cache 为空,则直接返回 new_k, new_v。
    若设置了 max_cache_len,则仅保留最近的 max_cache_len 个 token。
    """
    if cache_k is None:
        return new_k, new_v
    # 沿 seq_len 维度拼接 (dim=2)
    updated_k = torch.cat([cache_k, new_k], dim=2)
    updated_v = torch.cat([cache_v, new_v], dim=2)
    if max_cache_len is not None and updated_k.size(2) > max_cache_len:
        updated_k = updated_k[:, :, -max_cache_len:, :]
        updated_v = updated_v[:, :, -max_cache_len:, :]
    return updated_k, updated_v

加载QLoRA模型并对话的脚本

import torch
from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig
from peft import PeftModel

def load_qlora_model(base_model_name: str, adapter_path: str):
    # 4-bit量化配置
    bnb_config = BitsAndBytesConfig(
        load_in_4bit=True,
        bnb_4bit_quant_type="nf4",
        bnb_4bit_compute_dtype=torch.bfloat16,
        bnb_4bit_use_double_quant=True,
    )
    # 加载基座模型
    model = AutoModelForCausalLM.from_pretrained(
        base_model_name,
        quantization_config=bnb_config,
        device_map="auto",
        trust_remote_code=True,
    )
    tokenizer = AutoTokenizer.from_pretrained(base_model_name, trust_remote_code=True)
    tokenizer.pad_token = tokenizer.eos_token

    # 加载LoRA适配器
    model = PeftModel.from_pretrained(model, adapter_path)
    model.eval()
    return model, tokenizer

def chat_loop(model, tokenizer, max_new_tokens=256):
    print("模型加载完毕,开始对话。输入 'quit' 退出。")
    while True:
        user_input = input("User: ")
        if user_input.lower() == 'quit':
            break
        # 构建对话模板(以ChatML为例)
        prompt = f"<|im_start|>user\n{user_input}<|im_end|>\n<|im_start|>assistant\n"
        inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
        outputs = model.generate(
            **inputs,
            max_new_tokens=max_new_tokens,
            do_sample=True,
            temperature=0.7,
            top_p=0.9,
            pad_token_id=tokenizer.eos_token_id,
        )
        response = tokenizer.decode(outputs[0][inputs.input_ids.shape[1]:], skip_special_tokens=True)
        print(f"Assistant: {response}")

if __name__ == "__main__":
    model, tokenizer = load_qlora_model("meta-llama/Llama-2-7b-hf", "./my_lora_adapter")
    chat_loop(model, tokenizer)

带梯度累积、裁剪和调度器的微调训练循环

import torch
from torch.optim import AdamW
from torch.optim.lr_scheduler import CosineAnnealingLR
from tqdm import tqdm

def train_one_epoch(model, dataloader, optimizer, scheduler, accumulation_steps, max_grad_norm):
    model.train()
    total_loss = 0
    optimizer.zero_grad()
    for step, batch in enumerate(tqdm(dataloader)):
        outputs = model(**batch)
        loss = outputs.loss / accumulation_steps
        loss.backward()
        total_loss += loss.item() * accumulation_steps

        if (step + 1) % accumulation_steps == 0:
            # 梯度裁剪
            torch.nn.utils.clip_grad_norm_(model.parameters(), max_grad_norm)
            optimizer.step()
            scheduler.step()
            optimizer.zero_grad()

    return total_loss / len(dataloader)

使用示例:

optimizer = AdamW(model.parameters(), lr=2e-5)
scheduler = CosineAnnealingLR(optimizer, T_max=num_training_steps)
train_one_epoch(model, train_loader, optimizer, scheduler, accumulation_steps=4, max_grad_norm=1.0)

使用HuggingFace Trainer自定义加权多任务损失

from transformers import Trainer
import torch.nn.functional as F

class WeightedMultiTaskTrainer(Trainer):
    def __init__(self, task_weight_dict, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.task_weight_dict = task_weight_dict  # e.g. {'code': 2.0, 'chat': 1.0}

    def compute_loss(self, model, inputs, return_outputs=False):
        labels = inputs.pop("labels")
        # 取出任务类型权重并拓展到与labels相同形状
        task_types = inputs.pop("task_type", None)  # 假设数据中有 task_type 字段
        outputs = model(**inputs)
        logits = outputs.logits

        # 计算每个token的交叉熵
        loss_fct = torch.nn.CrossEntropyLoss(reduction='none', ignore_index=-100)
        per_token_loss = loss_fct(logits.view(-1, logits.size(-1)), labels.view(-1))
        per_token_loss = per_token_loss.view(labels.size())

        if task_types is not None:
            # 根据任务类型赋予权重
            weights = torch.ones_like(labels, dtype=torch.float)
            for task, weight in self.task_weight_dict.items():
                mask = (task_types == task)
                weights[mask] = weight
            # 忽略 -100 的位置
            valid_mask = (labels != -100).float()
            weighted_loss = (per_token_loss * weights * valid_mask).sum() / (weights * valid_mask).sum()
        else:
            # 默认平均
            weighted_loss = per_token_loss.sum() / (labels != -100).sum()

        return (weighted_loss, outputs) if return_outputs else weighted_loss

使用:

trainer = WeightedMultiTaskTrainer(
    task_weight_dict={'code': 2.0, 'math': 1.5, 'chat': 1.0},
    model=model,
    args=training_args,
    train_dataset=train_dataset,
    # ...
)
trainer.train()

统计SFT数据集指令与回复长度分布

import json
import matplotlib.pyplot as plt
from transformers import AutoTokenizer

def analyze_length_distribution(jsonl_file: str, tokenizer_name: str = 'gpt2'):
    tokenizer = AutoTokenizer.from_pretrained(tokenizer_name)
    instr_lens, resp_lens = [], []
    with open(jsonl_file, 'r', encoding='utf-8') as f:
        for line in f:
            obj = json.loads(line)
            instr = obj.get('instruction', '') + obj.get('input', '')
            resp = obj.get('output', '')
            instr_lens.append(len(tokenizer.encode(instr)))
            resp_lens.append(len(tokenizer.encode(resp)))

    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
    ax1.hist(instr_lens, bins=50, alpha=0.7)
    ax1.set_title('Instruction Length Distribution')
    ax2.hist(resp_lens, bins=50, alpha=0.7, color='orange')
    ax2.set_title('Response Length Distribution')
    plt.show()
    return instr_lens, resp_lens

基于MinHash的文本去重

import re
import hashlib
from collections import defaultdict
import random

class MinHash:
    def __init__(self, num_perm: int = 128, seed: int = 42):
        self.num_perm = num_perm
        self.prime = 2**61 - 1
        random.seed(seed)
        self.a = [random.randint(1, self.prime - 1) for _ in range(num_perm)]
        self.b = [random.randint(1, self.prime - 1) for _ in range(num_perm)]

    def _hash(self, x, i):
        return (self.a[i] * x + self.b[i]) % self.prime

    def signature(self, tokens: set) -> list:
        token_hashes = [hash(t) & 0xFFFFFFFF for t in tokens]
        if not token_hashes:
            return [float('inf')] * self.num_perm
        sig = []
        for i in range(self.num_perm):
            sig.append(min(self._hash(h, i) for h in token_hashes))
        return sig

    @staticmethod
    def similarity(sig1, sig2):
        return sum(x == y for x, y in zip(sig1, sig2)) / len(sig1)

def deduplicate_texts(texts, threshold=0.8, ngram_size=3):
    mh = MinHash()
    signatures = []
    for text in texts:
        words = text.split()
        ngrams = set()
        for i in range(len(words) - ngram_size + 1):
            ngrams.add(' '.join(words[i:i+ngram_size]))
        signatures.append(mh.signature(ngrams))

    keep_indices = []
    for i, text in enumerate(texts):
        is_dup = False
        for j in keep_indices:
            if mh.similarity(signatures[i], signatures[j]) >= threshold:
                is_dup = True
                break
        if not is_dup:
            keep_indices.append(i)
    return [texts[i] for i in keep_indices]

数据去污染检测(n-gram重叠)

from typing import List, Tuple

def get_ngrams(text: str, n: int = 13) -> set:
    """返回文本的n-gram集合(字符级)"""
    text = text.lower()
    return set(text[i:i+n] for i in range(len(text) - n + 1))

def detect_contamination(train_samples: List[str], eval_samples: List[str], n: int = 13, threshold: float = 0.6) -> List[Tuple[int, int, float]]:
    """
    检查每个训练样本与评测样本的最大n-gram重叠率。
    返回可疑污染列表,包含 (train_idx, eval_idx, overlap_ratio)。
    """
    eval_ngrams = [get_ngrams(s, n) for s in eval_samples]
    suspicious = []
    for i, train_text in enumerate(train_samples):
        train_ngrams = get_ngrams(train_text, n)
        if not train_ngrams:
            continue
        for j, e_ng in enumerate(eval_ngrams):
            overlap = train_ngrams & e_ng
            ratio = len(overlap) / len(train_ngrams)
            if ratio >= threshold:
                suspicious.append((i, j, ratio))
    return suspicious

Self-Instruct指令生成pipeline

import openai
import random
import json

openai.api_key = "your-api-key"

def generate_self_instruct(seed_tasks: list, num_to_generate: int = 10, model="gpt-4"):
    generated = []
    for _ in range(num_to_generate):
        few_shot = random.sample(seed_tasks, min(6, len(seed_tasks)))
        prompt = "你是一个智能助手,请生成一个全新的、不同于以下示例的指令,并为该指令提供一个高质量的回答。\n\n"
        for task in few_shot:
            prompt += f"指令: {task['instruction']}\n回答: {task['output']}\n\n"
        prompt += "现在,请生成一个全新的指令和回答。\n指令:"

        response = openai.ChatCompletion.create(
            model=model,
            messages=[{"role": "user", "content": prompt}],
            temperature=0.8,
            max_tokens=256
        )
        content = response.choices[0].message.content.strip()
        # 简单解析(实际需更鲁棒)
        parts = content.split('\n回答:', 1)
        if len(parts) == 2:
            instr = parts[0].replace('指令:', '').strip()
            ans = parts[1].strip()
            # 去重
            if not any(instr == t['instruction'] for t in generated):
                generated.append({'instruction': instr, 'output': ans})
    return generated

Evol-Instruct深度进化

def evolve_instruction(seed_instruction: str, model="gpt-4") -> str:
    prompt = f"""请将以下指令改写得更加复杂,增加更多的限制条件或需要多步推理的要求。确保改写后的指令仍然是可以被合理回答的。
原指令: {seed_instruction}
请只输出改写后的指令,不要添加额外文本。"""
    response = openai.ChatCompletion.create(
        model=model,
        messages=[{"role": "user", "content": prompt}],
        temperature=0.7,
        max_tokens=200
    )
    return response.choices[0].message.content.strip()

多轮对话转换为ChatML格式并生成input_ids和labels

from transformers import AutoTokenizer

def convert_multi_turn_to_chatml_and_tokenize(conversations: list, tokenizer, max_length=2048):
    """
    conversations: [{"role": "system"/"user"/"assistant", "content": "..."}, ...]
    返回 input_ids 和 labels (list of ints)。
    """
    chatml = ""
    for turn in conversations:
        role = turn["role"]
        content = turn["content"]
        chatml += f"<|im_start|>{role}\n{content}<|im_end|>\n"
    # 若最后不是 assistant 结尾,补充一个空的 assistant 标记以便生成
    if conversations[-1]["role"] != "assistant":
        chatml += "<|im_start|>assistant\n"
        # 此时标签不需要计算该部分

    tokenized = tokenizer(chatml, truncation=True, max_length=max_length, return_tensors=None)
    input_ids = tokenized['input_ids']
    # 构造labels:找到所有 assistant 部分保留token,其余设-100
    labels = [-100] * len(input_ids)
    # 简单处理:通过查找特殊token来确定assistant段,这里提供一个更鲁棒的方法:
    # 用tokenizer编码 "<|im_start|>assistant\n" 找到起始token id,但序列可能变化,此处简化:
    # 更好的做法是标记数据时保留原始边界。
    # 这里仅演示:假设我们可以通过分词后的token序列寻找 assistant 标记的位置。
    # 以下代码为伪代码,实际使用时建议在构造chatml时记录位置。
    # (省略详细边界查找,实际项目中使用现成库如tokenizers处理)
    return input_ids, labels

更实用的方式是使用HuggingFace的apply_chat_template方法(如果tokenizer已配置chat_template)。


IFEval约束遵循准确率评估

import json
import re
from typing import List, Dict, Callable

# 预定义约束检查函数注册
CONSTRAINT_CHECKERS = {}

def register_checker(name: str):
    def decorator(func):
        CONSTRAINT_CHECKERS[name] = func
        return func
    return decorator

@register_checker('word_count_between')
def check_word_count(text: str, params: dict) -> bool:
    words = text.split()
    return params.get('min', 0) <= len(words) <= params.get('max', float('inf'))

@register_checker('must_contain')
def check_must_contain(text: str, params: dict) -> bool:
    return params['substring'] in text

@register_checker('forbidden_words')
def check_forbidden_words(text: str, params: dict) -> bool:
    return all(word not in text for word in params.get('words', []))

@register_checker('json_format')
def check_json(text: str, params=None) -> bool:
    try:
        json.loads(text.strip())
        return True
    except:
        return False

def ifeval_evaluate(responses: List[str], constraints_list: List[List[Dict]]) -> float:
    """
    responses: 模型生成的回答列表
    constraints_list: 每条指令对应的约束列表,每个约束是一个字典 {'type': ..., 'params': {...}}
    返回严格约束遵循率
    """
    total = len(responses)
    strict_correct = 0
    for resp, constraints in zip(responses, constraints_list):
        if all(CONSTRAINT_CHECKERS[c['type']](resp, c.get('params', {})) for c in constraints):
            strict_correct += 1
    return strict_correct / total if total > 0 else 0.0

GPT-as-judge比较两个模型回复

import openai
import json
import random

openai.api_key = "your-api-key"

def gpt_judge(prompt: str, response_a: str, response_b: str, model="gpt-4") -> str:
    """返回 'A' / 'B' / 'tie'"""
    # 随机交换位置以消除位置偏差
    if random.random() < 0.5:
        first, second = "A", "B"
        resp1, resp2 = response_a, response_b
    else:
        first, second = "B", "A"
        resp1, resp2 = response_b, response_a

    judge_prompt = f"""请作为一个公正的评判者,比较以下两个AI助手对用户问题的回复。
用户问题: {prompt}

助手{first}的回复: {resp1}

助手{second}的回复: {resp2}

请从准确性、有用性、流畅度等方面综合评判,哪个回复更好?如果难分高下,请选择 'tie'。
请只输出 'A', 'B', 或 'tie',并简要说明理由。
格式:{{"winner": "A/B/tie", "reason": "..."}}"""

    response = openai.ChatCompletion.create(
        model=model,
        messages=[{"role": "user", "content": judge_prompt}],
        temperature=0.0,
        max_tokens=256
    )
    content = response.choices[0].message.content.strip()
    try:
        result = json.loads(content)
        winner = result['winner']
        # 映射回原始A/B
        if winner == first:
            return 'A'
        elif winner == second:
            return 'B'
        else:
            return 'tie'
    except:
        return 'tie'

vLLM部署及Python API调用示例

部署命令(服务器端):

python -m vllm.entrypoints.openai.api_server \
    --model /path/to/sft-model \
    --gpu-memory-utilization 0.9 \
    --max-model-len 4096 \
    --served-model-name my-sft-model

Python客户端调用:

import requests

def chat_vllm(prompt: str, history=None):
    url = "http://localhost:8000/v1/chat/completions"
    messages = [{"role": "system", "content": "你是一个有帮助的助手。"}]
    if history:
        messages.extend(history)
    messages.append({"role": "user", "content": prompt})

    payload = {
        "model": "my-sft-model",
        "messages": messages,
        "max_tokens": 256,
        "temperature": 0.7,
        "top_p": 0.9,
    }
    response = requests.post(url, json=payload)
    return response.json()['choices'][0]['message']['content']

# 使用示例
reply = chat_vllm("你好,请介绍一下人工智能。")
print(reply)

编写脚本实现A/B测试,批量运行两个模型对比,并统计胜率。

import torch
from tqdm import tqdm
from transformers import AutoModelForCausalLM, AutoTokenizer
from typing import List, Dict

def ab_test(
    prompts: List[str],
    model_a_path: str,
    model_b_path: str,
    tokenizer_path: str,
    judge_model=None,  # 可选的裁判模型,若没有则用简单规则
    max_new_tokens: int = 128,
    device: str = "cuda"
) -> Dict[str, int]:
    """
    批量运行两个模型,对每个prompt分别生成回答,然后利用内置或外部裁判统计胜率。
    返回字典 {'A_wins': int, 'B_wins': int, 'ties': int}
    """
    tokenizer = AutoTokenizer.from_pretrained(tokenizer_path)
    model_a = AutoModelForCausalLM.from_pretrained(model_a_path).to(device)
    model_b = AutoModelForCausalLM.from_pretrained(model_b_path).to(device)
    model_a.eval()
    model_b.eval()

    results = {'A_wins': 0, 'B_wins': 0, 'ties': 0}

    for prompt in tqdm(prompts):
        inputs = tokenizer(prompt, return_tensors="pt").to(device)
        with torch.no_grad():
            gen_a = model_a.generate(**inputs, max_new_tokens=max_new_tokens, do_sample=False)
            gen_b = model_b.generate(**inputs, max_new_tokens=max_new_tokens, do_sample=False)
        resp_a = tokenizer.decode(gen_a[0][inputs.input_ids.shape[1]:], skip_special_tokens=True)
        resp_b = tokenizer.decode(gen_b[0][inputs.input_ids.shape[1]:], skip_special_tokens=True)

        if judge_model is not None:
            # 使用GPT-as-judge或其他裁判模型
            winner = judge_model.compare(prompt, resp_a, resp_b)
        else:
            # 简单规则:谁更长谁赢(仅示例,实际无意义)
            winner = 'A' if len(resp_a) > len(resp_b) else ('B' if len(resp_b) > len(resp_a) else 'tie')

        if winner == 'A':
            results['A_wins'] += 1
        elif winner == 'B':
            results['B_wins'] += 1
        else:
            results['ties'] += 1

    return results

使用示例:

prompts = ["解释什么是机器学习", "写一首关于秋天的诗"]
stats = ab_test(prompts, "path/to/modelA", "path/to/modelB", "path/to/tokenizer")
print(stats)  # {'A_wins': 2, 'B_wins': 0, 'ties': 0}

实现一个基于困惑度的数据过滤函数,过滤高PPL的回复。

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from typing import List

def filter_by_ppl(
    texts: List[str],
    model_name: str = "gpt2",
    threshold: float = 100.0,
    device: str = "cuda"
) -> List[str]:
    """
    计算每个文本的困惑度,返回 PPL 低于阈值的文本。
    """
    tokenizer = AutoTokenizer.from_pretrained(model_name)
    model = AutoModelForCausalLM.from_pretrained(model_name).to(device)
    model.eval()
    kept = []
    for text in texts:
        inputs = tokenizer(text, return_tensors="pt").to(device)
        with torch.no_grad():
            outputs = model(**inputs, labels=inputs.input_ids)
            loss = outputs.loss
            ppl = torch.exp(loss).item()
        if ppl < threshold:
            kept.append(text)
    return kept

注意:实际应用中可使用更大的参考模型,并可对每个样本计算长度归一化的PPL。


使用DeepSpeed配置文件启动SFT训练,写出命令行和关键配置。

配置文件 ds_config.json

{
  "train_batch_size": 16,
  "gradient_accumulation_steps": 4,
  "optimizer": {
    "type": "AdamW",
    "params": {
      "lr": 2e-5,
      "weight_decay": 0.1
    }
  },
  "fp16": {
    "enabled": false
  },
  "bf16": {
    "enabled": true
  },
  "zero_optimization": {
    "stage": 2,
    "offload_optimizer": {
      "device": "cpu",
      "pin_memory": true
    },
    "overlap_comm": true,
    "contiguous_gradients": true
  },
  "gradient_clipping": 1.0,
  "steps_per_print": 10
}

启动命令:

deepspeed --num_gpus=4 train_sft.py \
    --deepspeed ds_config.json \
    --model_name_or_path meta-llama/Llama-2-7b-hf \
    --data_path ./sft_data.jsonl \
    --output_dir ./output \
    --per_device_train_batch_size 4 \
    --gradient_accumulation_steps 4 \
    --learning_rate 2e-5 \
    --num_train_epochs 2 \
    --bf16 \
    --logging_steps 10

自定义回调函数,在每个epoch结束时用几个prompt测试生成质量。

from transformers import TrainerCallback, TrainerControl, TrainerState, TrainingArguments
import torch

class GenerationTestCallback(TrainerCallback):
    def __init__(self, tokenizer, test_prompts: list, max_new_tokens=50):
        self.tokenizer = tokenizer
        self.test_prompts = test_prompts
        self.max_new_tokens = max_new_tokens

    def on_epoch_end(self, args: TrainingArguments, state: TrainerState, control: TrainerControl, **kwargs):
        model = kwargs['model']
        model.eval()
        print(f"\n=== Epoch {state.epoch:.2f} 生成测试 ===")
        for prompt in self.test_prompts:
            inputs = self.tokenizer(prompt, return_tensors='pt').to(model.device)
            with torch.no_grad():
                outputs = model.generate(**inputs, max_new_tokens=self.max_new_tokens, do_sample=False,
                                         pad_token_id=self.tokenizer.eos_token_id)
            generated = self.tokenizer.decode(outputs[0], skip_special_tokens=True)
            print(f"Prompt: {prompt}\nGenerated: {generated}\n")
        model.train()

使用:

trainer.add_callback(GenerationTestCallback(tokenizer, ["你好,介绍一下人工智能。", "写一首五言绝句。"]))

实现一个简单的“拒绝采样”数据筛选逻辑。

从生成的大量候选中,通过评判模型(或规则)选择最佳回答作为训练数据。

def rejection_sampling(
    prompt: str,
    candidates: List[str],
    judge_model,  # 一个能打分的模型或函数
    score_threshold: float = 0.8
) -> str:
    """
    对多个候选回答打分,返回得分最高且超过阈值的回答。若没有,返回空字符串或进行重试。
    """
    best_score = -1.0
    best_response = ""
    for resp in candidates:
        score = judge_model.evaluate(prompt, resp)  # 假设返回0-1之间的分数
        if score > best_score:
            best_score = score
            best_response = resp
    if best_score >= score_threshold:
        return best_response
    else:
        return ""  # 所有候选均不合格,可触发重新生成

具体应用时,judge_model 可以是GPT-4、一个专门的打分模型,甚至基于规则(如包含关键词等)。


实现早期停止(Early Stopping)类,可监控验证损失。

class EarlyStopping:
    def __init__(self, patience: int = 3, min_delta: float = 0.0, mode: str = 'min'):
        self.patience = patience
        self.min_delta = min_delta
        self.mode = mode
        self.counter = 0
        self.best_score = None
        self.early_stop = False

    def __call__(self, current_score: float) -> bool:
        if self.best_score is None:
            self.best_score = current_score
        elif (self.mode == 'min' and current_score > self.best_score - self.min_delta) or \
             (self.mode == 'max' and current_score < self.best_score + self.min_delta):
            self.counter += 1
            if self.counter >= self.patience:
                self.early_stop = True
        else:
            self.best_score = current_score
            self.counter = 0
        return self.early_stop

训练循环中使用:

stopper = EarlyStopping(patience=3)
for epoch in range(epochs):
    val_loss = validate()
    if stopper(val_loss):
        print("Early stopping triggered")
        break

编写代码计算两个模型输出之间的KL散度。

import torch
import torch.nn.functional as F

def compute_kl_divergence(model1, model2, tokenizer, prompt: str, max_len: int = 50):
    inputs = tokenizer(prompt, return_tensors='pt').to(model1.device)
    gen_out = model1.generate(**inputs, max_new_tokens=max_len, do_sample=False,
                              output_scores=True, return_dict_in_generate=True)
    generated_ids = gen_out.sequences[0]
    input_len = inputs.input_ids.shape[1]
    logits1 = []
    logits2 = []
    with torch.no_grad():
        past_kv1, past_kv2 = None, None
        cur_input = inputs.input_ids
        for step in range(generated_ids.shape[1] - input_len):
            out1 = model1(cur_input, past_key_values=past_kv1, use_cache=True)
            out2 = model2(cur_input, past_key_values=past_kv2, use_cache=True)
            logits1.append(out1.logits[:, -1, :])
            logits2.append(out2.logits[:, -1, :])
            past_kv1 = out1.past_key_values
            past_kv2 = out2.past_key_values
            # 使用实际生成的token作为下一步输入
            cur_input = generated_ids[:, input_len + step].unsqueeze(1)
    logits1 = torch.cat(logits1, dim=0)
    logits2 = torch.cat(logits2, dim=0)
    p = F.softmax(logits1, dim=-1)
    log_q = F.log_softmax(logits2, dim=-1)
    kl = F.kl_div(log_q, p, reduction='batchmean')  # KL(p || q)
    return kl.item()

实现一个“温度采样”数据采样器,用于平衡不同任务的数据量。

import numpy as np
from torch.utils.data import Sampler
from collections import Counter

class TemperatureSampler(Sampler):
    def __init__(self, dataset, task_labels: list, temperature: float = 0.5):
        self.dataset = dataset
        self.task_labels = task_labels
        self.task_counts = Counter(task_labels)
        self.temperature = temperature
        self.task_probs = self._compute_probs()

    def _compute_probs(self):
        counts = np.array([self.task_counts[t] for t in sorted(self.task_counts.keys())])
        probs = counts ** self.temperature
        probs = probs / probs.sum()
        return probs

    def __iter__(self):
        # 根据每个任务的概率,随机采样任务,再从该任务中均匀采样一个样本
        tasks = sorted(self.task_counts.keys())
        task_ids = np.arange(len(tasks))
        while True:
            chosen_task = np.random.choice(task_ids, p=self.task_probs)
            # 获取该任务的所有样本索引
            indices = [i for i, t in enumerate(self.task_labels) if t == tasks[chosen_task]]
            yield np.random.choice(indices)

    def __len__(self):
        # 无法确定精确长度,返回估计值
        return len(self.dataset)

使用时将 task_labels 传入,可自动平衡高频和低频任务。


如何用HuggingFace datasets流式加载海量JSONL文件并预处理?

from datasets import load_dataset
from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("gpt2")

def preprocess(example):
    # 假设 example 包含 'instruction' 和 'output'
    prompt = f"<|im_start|>user\n{example['instruction']}<|im_end|>\n<|im_start|>assistant\n"
    answer = example['output']
    full_text = prompt + answer + "<|im_end|>"
    tokenized = tokenizer(full_text, truncation=True, max_length=2048)
    # 构造labels(prompt部分设为-100)
    prompt_len = len(tokenizer(prompt)['input_ids'])
    labels = tokenized['input_ids'].copy()
    labels[:prompt_len] = [-100] * prompt_len
    tokenized['labels'] = labels
    return tokenized

# 流式加载:不将全量数据加载到内存
dataset = load_dataset("json", data_files="sft_data.jsonl", split="train", streaming=True)
dataset = dataset.map(preprocess, remove_columns=dataset.column_names)
# 此时dataset是一个可迭代对象,可配合torch的DataLoader使用

编写一个简单的PPO微调循环框架(伪代码或简化版)。

# 假设已有一个SFT模型 actor_model,参考模型 ref_model,奖励模型 reward_model,价值模型 critic_model
# 使用一个简单的PPO训练循环(省略优势计算细节)
import torch
from torch.optim import AdamW

actor_optimizer = AdamW(actor_model.parameters(), lr=1e-5)
critic_optimizer = AdamW(critic_model.parameters(), lr=5e-6)

for epoch in range(ppo_epochs):
    for batch in dataloader:
        prompts = batch['prompt']
        # 生成回答
        responses = actor_model.generate(prompts)
        # 计算奖励
        rewards = reward_model.score(prompts, responses)
        # 计算价值估计
        values = critic_model(prompts, responses)
        # 计算优势(简化:实际应用GAE)
        advantages = rewards - values.detach()
        # PPO损失
        old_log_probs = actor_model.log_prob(prompts, responses).detach()
        for _ in range(update_epochs):
            new_log_probs = actor_model.log_prob(prompts, responses)
            ratio = torch.exp(new_log_probs - old_log_probs)
            surr1 = ratio * advantages
            surr2 = torch.clamp(ratio, 1-epsilon, 1+epsilon) * advantages
            actor_loss = -torch.min(surr1, surr2).mean()
            # 添加KL惩罚
            kl = torch.distributions.kl_divergence(
                actor_model.distribution(prompts), ref_model.distribution(prompts)
            ).mean()
            loss = actor_loss + beta * kl
            actor_optimizer.zero_grad()
            loss.backward()
            actor_optimizer.step()
        # 更新价值模型
        critic_loss = torch.nn.functional.mse_loss(values, rewards)
        critic_optimizer.zero_grad()
        critic_loss.backward()
        critic_optimizer.step()

实际完整PPO实现非常复杂,建议直接使用库如 trlDeepSpeed-Chat


实现将SFT数据转为标准OpenAI messages格式的函数。

def sft_to_openai_messages(instruction: str, input_text: str = "", output: str = "") -> list:
    """
    将Alpaca风格数据转为OpenAI API的messages格式。
    """
    messages = [{"role": "system", "content": "你是一个有帮助的AI助手。"}]
    if input_text:
        user_content = f"{instruction}\n{input_text}"
    else:
        user_content = instruction
    messages.append({"role": "user", "content": user_content})
    messages.append({"role": "assistant", "content": output})
    return messages

# 使用:
example = {"instruction": "解释什么是黑洞", "input": "", "output": "黑洞是时空的一个区域,引力极强..."}
openai_format = sft_to_openai_messages(**example)

编写一个对话模拟器,让两个模型互相对话生成微调数据。

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer

def simulate_dialogue(
    model_a, tokenizer_a,
    model_b, tokenizer_b,
    initial_prompt: str,
    num_turns: int = 4,
    max_new_tokens: int = 100
) -> list:
    """
    两个模型交替对话,A发起,B回复,A再回复,如此循环。返回对话历史列表。
    """
    dialogue_history = [{"role": "user", "content": initial_prompt}]
    # 模型A生成第一个回复
    prompt_a = tokenizer_a.apply_chat_template(dialogue_history, tokenize=False, add_generation_prompt=True)
    inputs = tokenizer_a(prompt_a, return_tensors="pt").to(model_a.device)
    gen_a = model_a.generate(**inputs, max_new_tokens=max_new_tokens, do_sample=True, temperature=0.8)
    reply_a = tokenizer_a.decode(gen_a[0][inputs.input_ids.shape[1]:], skip_special_tokens=True)
    dialogue_history.append({"role": "assistant", "content": reply_a})

    for turn in range(1, num_turns):
        # B模型回复
        prompt_b = tokenizer_b.apply_chat_template(dialogue_history, tokenize=False, add_generation_prompt=True)
        inputs = tokenizer_b(prompt_b, return_tensors="pt").to(model_b.device)
        gen_b = model_b.generate(**inputs, max_new_tokens=max_new_tokens, do_sample=True, temperature=0.8)
        reply_b = tokenizer_b.decode(gen_b[0][inputs.input_ids.shape[1]:], skip_special_tokens=True)
        dialogue_history.append({"role": "user", "content": reply_b})  # B的回复作为下一轮的“用户”消息
        # A模型回复
        prompt_a = tokenizer_a.apply_chat_template(dialogue_history, tokenize=False, add_generation_prompt=True)
        inputs = tokenizer_a(prompt_a, return_tensors="pt").to(model_a.device)
        gen_a = model_a.generate(**inputs, max_new_tokens=max_new_tokens, do_sample=True, temperature=0.8)
        reply_a = tokenizer_a.decode(gen_a[0][inputs.input_ids.shape[1]:], skip_special_tokens=True)
        dialogue_history.append({"role": "assistant", "content": reply_a})

    return dialogue_history

实现一个“数据配方”配置解析器,能按比例采样多个数据集。

import random
from torch.utils.data import Dataset, ConcatDataset, Subset

class RecipeSampler:
    def __init__(self, datasets: list, ratios: list):
        """
        datasets: List of Dataset objects
        ratios: List of float, 每个数据集的采样比例(概率)
        """
        self.datasets = datasets
        self.ratios = ratios
        self.total_samples = sum(len(ds) for ds in datasets)
        # 构建每个数据集的索引区间
        self._dataset_offsets = self._compute_offsets()

    def _compute_offsets(self):
        indices = []
        for ds, ratio in zip(self.datasets, self.ratios):
            # 根据比例计算应采样的数量(按比例从总样本数中分配)
            n_samples = int(self.total_samples * ratio)
            # 如果比例分配导致超过数据集大小,则截断
            n_samples = min(n_samples, len(ds))
            indices.append(random.sample(range(len(ds)), n_samples))
        return indices

    def sample(self, num_samples: int):
        combined = []
        for ds_idx, ds in enumerate(self.datasets):
            combined.extend([ (ds_idx, idx) for idx in self._dataset_offsets[ds_idx] ])
        random.shuffle(combined)
        return combined[:num_samples]

更简单的方式是使用 torch.utils.data.WeightedRandomSampler,按每个数据集的权重分配采样概率,然后对合并后的数据集应用该采样器。


使用bitsandbytes加载4-bit量化模型,并用LoRA微调,给出代码框架。

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
from peft import LoraConfig, get_peft_model, TaskType

# 量化配置
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_compute_dtype=torch.bfloat16,
    bnb_4bit_use_double_quant=True,
)

model = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Llama-2-7b-hf",
    quantization_config=bnb_config,
    device_map="auto",
    trust_remote_code=True,
)
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-2-7b-hf")
tokenizer.pad_token = tokenizer.eos_token

# LoRA配置
lora_config = LoraConfig(
    r=8,
    lora_alpha=16,
    target_modules=["q_proj", "v_proj"],
    lora_dropout=0.05,
    bias="none",
    task_type=TaskType.CAUSAL_LM,
)

model = get_peft_model(model, lora_config)
model.print_trainable_parameters()

# 后续可使用HuggingFace Trainer或其他训练循环进行微调

编写一个微调模型回归测试脚本,自动跑100个固定prompt并计算指标。

import json
from transformers import AutoModelForCausalLM, AutoTokenizer

def regression_test(model, tokenizer, prompt_file: str = "test_prompts.json", output_file: str = "results.json"):
    with open(prompt_file, 'r', encoding='utf-8') as f:
        prompts = [line.strip() for line in f.readlines() if line.strip()]
    results = []
    for i, prompt in enumerate(prompts):
        inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
        gen = model.generate(**inputs, max_new_tokens=100, do_sample=False,
                             pad_token_id=tokenizer.eos_token_id)
        response = tokenizer.decode(gen[0][inputs.input_ids.shape[1]:], skip_special_tokens=True)
        results.append({"id": i, "prompt": prompt, "response": response})
        # 可添加自动评分(例如检查是否包含特定关键词、长度、格式等)
        # 这里仅记录输出
    with open(output_file, 'w', encoding='utf-8') as f:
        json.dump(results, f, ensure_ascii=False, indent=2)
    print(f"Results saved to {output_file}")

# 调用
# regression_test(my_model, tokenizer)

更进一步可以加入自动化指标计算(如BLEU、ROUGE、或基于关键字的规则),并与基线模型结果对比,若有显著退化则告警。


以上代码覆盖了从数据工程、训练、评估到部署的多个关键环节,可根据实际需求进行调整和扩展。