使用ptuning做意图分类

代码:

from torch.utils.data import Dataset, DataLoader, random_split
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch
import peft
import transformers
from peft import PrefixTuningConfig, get_peft_model, PeftModel
import time
from pathlib import Path

# ===========================
# 1. 环境与基础配置
# ===========================
device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"PyTorch版本: {torch.__version__}")
print(f"设备: {device}")
print(f"CUDA版本: {torch.version.cuda}")
print(f"Transformers版本: {transformers.__version__}")
print(f"PEFT版本: {peft.__version__}")

# 基础模型名称(本地路径或模型名)
# base_model_name = "Qwen/Qwen3-0.6B"
base_model_name = r"D:\source\models\Qwen3-0.6B"

# Prefix Tuning 权重保存目录
ptuning_output_dir = "ptuning_intent_qwen3_0.6b"

# ===========================
# 2. 示例数据
# ===========================
# ⚠️ 实际使用时,一定要扩充更多样本,这里只是演示
sample_data = [
    {
        "instruction": "作为意图分类专家,只能输出以下两个标签之一:【攻击研判】或【攻击分析】。不要输出其他内容。",
        "input": "研判告警  9d34dc99-a981-41dc-94bf-10b282354da4",
        "output": "攻击研判"
    },
    {
        "instruction": "作为意图分类专家,只能输出以下两个标签之一:【攻击研判】或【攻击分析】。不要输出其他内容。",
        "input": "列举10条高危告警",
        "output": "攻击分析"
    }
]

# ===========================
# 3. 构建模型 + Prefix Tuning (P-Tuning)
# ===========================
def setup_model(model_name):
    """
    加载基础模型和 tokenizer,并应用 Prefix Tuning(P-Tuning 风格)。
    当前环境为 CPU:不使用 device_map='auto',避免 meta tensor / offload。
    """
    tokenizer = AutoTokenizer.from_pretrained(
        model_name,
        local_files_only=True,
        trust_remote_code=True
    )

    # 加载基础模型
    model = AutoModelForCausalLM.from_pretrained(
        model_name,
        local_files_only=True,
        dtype=torch.float32,   # 新版 transformers 建议用 dtype
        device_map=None,       # 不启用 auto/offload,避免 meta tensor
        trust_remote_code=True
    )
    """
在你当前的 Prefix Tuning 代码里:

python
prefix_config = PrefixTuningConfig(
    task_type="CAUSAL_LM",
    num_virtual_tokens=32,
)
model = get_peft_model(model, prefix_config)
PEFT 会帮你做的事情大致是:

给每一层追加长度为 32 的虚拟前缀(可以理解为 32 个“隐形 token”)
只训练这些前缀相关的参数
在你训练时,它会学出一种“一旦看到这个前缀,后面的输入就应该被理解为某种特定分类任务”的内在模式
推理时,只要你加载了这个 prefix,模型就会在这个任务风格下工作    
    """
    # Prefix Tuning 配置(P-Tuning 思路)
    prefix_config = PrefixTuningConfig(
        task_type="CAUSAL_LM",
        num_virtual_tokens=32,           # 虚拟 prompt 长度,可根据需要调整:16/32/64
        # 如果报 encoder_hidden_size 相关错误,再启用下面这一行:
        # encoder_hidden_size=model.config.hidden_size
    )

    # 应用 Prefix Tuning
    model = get_peft_model(model, prefix_config)

    # 打印可训练参数
    print("=== 模型参数统计 ===")
    model.print_trainable_parameters()

    return model, tokenizer

# ===========================
# 4. 数据集定义
# ===========================
class IntentClassificationDataset(Dataset):
    def __init__(self, data, tokenizer, max_length=256):
        self.data = data
        self.tokenizer = tokenizer
        self.max_length = max_length

    def __len__(self):
        return len(self.data)

    def __getitem__(self, idx):
        item = self.data[idx]

        # 构建完整序列
        full_text = (
            f"指令:{item['instruction']}\n"
            f"输入:{item['input']}\n"
            f"输出:{item['output']}{self.tokenizer.eos_token}"
        )
        instruction_text = (
            f"指令:{item['instruction']}\n"
            f"输入:{item['input']}\n"
            f"输出:"
        )

        # Tokenize 完整序列
        encoding = self.tokenizer(
            full_text,
            max_length=self.max_length,
            padding='max_length',
            truncation=True,
            return_tensors='pt'
        )

        # Tokenize 指令部分,计算需要 mask 的长度
        instruction_encoding = self.tokenizer(
            instruction_text,
            return_tensors='pt'
        )
        instruction_length = instruction_encoding['input_ids'].shape[1]

        # 创建 labels 并 mask 掉指令部分
        labels = encoding['input_ids'].clone()
        labels[0, :instruction_length] = -100  # -100 会在 loss 里被忽略

        return {
            'input_ids': encoding['input_ids'].squeeze(0),
            'attention_mask': encoding['attention_mask'].squeeze(0),
            'labels': labels.squeeze(0)
        }

# ===========================
# 5. 训练 + 保存 Prefix Tuning 权重
# ===========================
def train_and_save():
    model, tokenizer = setup_model(base_model_name)
    # 不再调用 model.to(device),setup_model 已经在 CPU 上加载好了

    dataset = IntentClassificationDataset(sample_data, tokenizer, max_length=256)

    # 简单划分训练/验证
    train_size = int(0.8 * len(dataset))
    eval_size = len(dataset) - train_size
    train_dataset, eval_dataset = random_split(dataset, [train_size, eval_size])

    print(f"训练集大小: {len(train_dataset)}")
    print(f"验证集大小: {len(eval_dataset)}")

    batch_size = 1  # CPU 训练,数据也少,小 batch 就行
    train_dataloader = DataLoader(
        train_dataset,
        batch_size=batch_size,
        shuffle=True,
        num_workers=0
    )

    eval_dataloader = DataLoader(
        eval_dataset,
        batch_size=batch_size,
        shuffle=False,
        num_workers=0
    )

    learning_rate = 5e-5
    weight_decay = 0.01
    num_epochs = 3   # 比之前多训几轮

    torch.manual_seed(123)
    optimizer = torch.optim.AdamW(
        model.parameters(),
        lr=learning_rate,
        weight_decay=weight_decay
    )

    start_time = time.time()
    global_step = 0

    for epoch in range(num_epochs):
        model.train()
        for batch in train_dataloader:
            input_ids = batch['input_ids'].to(device)
            attention_mask = batch['attention_mask'].to(device)
            labels = batch['labels'].to(device)

            optimizer.zero_grad()
            outputs = model(
                input_ids=input_ids,
                attention_mask=attention_mask,
                labels=labels
            )
            loss = outputs.loss
            loss.backward()
            optimizer.step()

            global_step += 1
            print(f"Epoch {epoch}, step {global_step}, loss {loss.item():.4f}")

        # 可选的简单验证
        model.eval()
        if len(eval_dataset) > 0:
            eval_losses = []
            with torch.no_grad():
                for batch in eval_dataloader:
                    input_ids = batch['input_ids'].to(device)
                    attention_mask = batch['attention_mask'].to(device)
                    labels = batch['labels'].to(device)

                    outputs = model(
                        input_ids=input_ids,
                        attention_mask=attention_mask,
                        labels=labels
                    )
                    eval_losses.append(outputs.loss.item())
            print(f"Epoch {epoch} 验证集平均loss: {sum(eval_losses)/len(eval_losses):.4f}")

    end_time = time.time()
    execution_time_minutes = (end_time - start_time) / 60
    print(f"Training completed in {execution_time_minutes:.2f} minutes.")

    # ====== 保存 Prefix Tuning 增量权重(不会保存基础模型参数)======
    Path(ptuning_output_dir).mkdir(parents=True, exist_ok=True)
    model.save_pretrained(ptuning_output_dir)
    tokenizer.save_pretrained(ptuning_output_dir)
    print(f"P-Tuning 权重与 tokenizer 已保存到: {ptuning_output_dir}")

# ===========================
# 6. 加载 Prefix Tuning + 推理示例
# ===========================
def load_ptuning_and_infer(user_input: str):
    """
    读取已训练好的 Prefix Tuning 权重,进行推理。
    """
    # 1. 加载基础模型和 tokenizer(同样全部放 CPU)
    tokenizer = AutoTokenizer.from_pretrained(
        base_model_name,
        local_files_only=True,
        trust_remote_code=True
    )
    base_model = AutoModelForCausalLM.from_pretrained(
        base_model_name,
        local_files_only=True,
        dtype=torch.float32,
        device_map=None,
        trust_remote_code=True
    )

    # 2. 将 Prefix Tuning 适配器加载到基础模型上(也在 CPU)
    model = PeftModel.from_pretrained(
        base_model,
        ptuning_output_dir,
        device_map=None
    )
    model.eval()

    # 3. 构造推理提示词(和训练时格式保持一致)
    instruction = "作为意图分类专家,只能输出以下两个标签之一:【攻击研判】或【攻击分析】。不要输出其他内容。"
    prompt = f"指令:{instruction}\n输入:{user_input}\n输出:"

    inputs = tokenizer(
        prompt,
        return_tensors="pt"
    ).to(device)

    with torch.no_grad():
        generated_ids = model.generate(
            **inputs,
            max_new_tokens=128,        # 分类任务只需要很短的输出
            do_sample=False,         # 分类任务一般不需要采样
            pad_token_id=tokenizer.eos_token_id
        )

    # 只取新生成的部分
    generated_text = tokenizer.decode(
        generated_ids[0][inputs['input_ids'].shape[1]:],
        skip_special_tokens=True
    )

    text = generated_text.strip()
    # 简单后处理,只认两个标签
    if "攻击研判" in text:
        intent = "攻击研判"
    elif "攻击分析" in text:
        intent = "攻击分析"
    else:
        # 兜底策略(你可以改成 "未知")
        intent = "攻击分析"

    print(f"模型预测意图: {intent}(原始输出: {text})")
    return intent

# ===========================
# 7. 主入口
# ===========================
if __name__ == "__main__":
    # 第一步:训练并保存 Prefix Tuning
    train_and_save()

    # 第二步:加载 Prefix Tuning 并推理
    print("\n==== 推理示例 ====")
    test_inputs = [
        "研判告警 9d34dc99-a981-41dc-94bf-10b282354da4",
        "请列举10条高危告警"
    ]
    for t in test_inputs:
        print(f"\n用户输入: {t}")
        _ = load_ptuning_and_infer(t)

  

 

1. 大局观:P-Tuning 在干嘛?

一句话概括:

P-Tuning 不是去改模型里的权重,而是学一段“虚拟的提示词”嵌入向量,让模型在这段提示的引导下,更好完成下游任务。

也就是说:

  • 模型主体参数(几亿、几十亿的权重)通常是冻结的
  • 只新增一小块参数:一段长度为 LL 的「可学习的 tokens 嵌入」
  • 训练时,只更新这段嵌入参数
  • 推理时,把这段嵌入“拼接”到输入前面,一起喂给模型

和“往前面加一串文字 prompt”很像,但区别是:

  • 普通 prompt:是人类写的可见文字,通过 tokenizer 变成 token,再查 embedding
  • P-Tuning 的 prompt:是直接在 embedding 空间里学出来的“虚拟 token”,不一定对应任何真实词

2. 三个相关概念:Prompt Tuning、P-Tuning、Prefix Tuning

很多文章会混着说,简单对一下:

 

 

image

 

image

 

image

image

 

 

对你这种应用:

  • 基础模型:Qwen3-0.6B 已经学到大量语言和安全相关的知识
  • 你的任务:只是教它“看输入句子 -> 输出两种标签之一”
  • 不需要重塑模型,只需要“告诉它该怎么用已有能力来做分类”
  • P-Tuning / LoRA 这类参数高效微调就非常合适

6. 结合你现在代码,可以脑补一下“内部图像”

在你当前的 Prefix Tuning 代码里:

python
 
prefix_config = PrefixTuningConfig( task_type="CAUSAL_LM", num_virtual_tokens=32, ) model = get_peft_model(model, prefix_config)

PEFT 会帮你做的事情大致是:

  • 给每一层追加长度为 32 的虚拟前缀(可以理解为 32 个“隐形 token”)
  • 只训练这些前缀相关的参数
  • 在你训练时,它会学出一种“一旦看到这个前缀,后面的输入就应该被理解为某种特定分类任务”的内在模式
  • 推理时,只要你加载了这个 prefix,模型就会在这个任务风格下工作

 

附:使用loar微调的方式

from torch.utils.data import Dataset, DataLoader, random_split
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch
import peft
import transformers
from peft import LoraConfig, get_peft_model, PeftModel
import time
from pathlib import Path

# ===========================
# 1. 环境与基础配置
# ===========================
device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"PyTorch版本: {torch.__version__}")
print(f"设备: {device}")
print(f"CUDA版本: {torch.version.cuda}")
print(f"Transformers版本: {transformers.__version__}")
print(f"PEFT版本: {peft.__version__}")

# 基础模型名称
# base_model_name = "Qwen/Qwen3-0.6B"  # 如果想换8B就改这里
base_model_name = r"D:\source\models\Qwen3-0.6B"

# LoRA 权重保存目录
lora_output_dir = "lora_intent_qwen3_0.6b"

# ===========================
# 2. 示例数据
# ===========================
sample_data = [
    {
        "instruction": "作为意图分类专家,分析用户输入,将输入分类为攻击研判或攻击分析",
        "input": "研判告警  9d34dc99-a981-41dc-94bf-10b282354da4",
        "output": "攻击研判"
    },
    {
        "instruction": "作为意图分类专家,分析用户输入,将输入分类为攻击研判或攻击分析",
        "input": "列举10条高危告警",
        "output": "攻击分析"
    }
]

# ===========================
# 3. 构建模型 + LoRA
# ===========================
def setup_model(model_name):
    # 本地加载,如需联网加载可去掉 local_files_only=True
    tokenizer = AutoTokenizer.from_pretrained(
        model_name,
        local_files_only=True,
        trust_remote_code=True
    )

    model = AutoModelForCausalLM.from_pretrained(
        model_name,
        local_files_only=True,
        dtype="auto",
        device_map="auto",
        trust_remote_code=True
    )

    # LoRA配置
    lora_config = LoraConfig(
        r=8,
        lora_alpha=32,
        target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],  # Qwen3 相关模块
        lora_dropout=0.05,
        bias="none",
        task_type="CAUSAL_LM"
    )

    # 应用LoRA
    model = get_peft_model(model, lora_config)

    # 打印可训练参数
    print("=== 模型参数统计 ===")
    model.print_trainable_parameters()

    return model, tokenizer

# ===========================
# 4. 数据集定义
# ===========================
class IntentClassificationDataset(Dataset):
    def __init__(self, data, tokenizer, max_length=256):
        self.data = data
        self.tokenizer = tokenizer
        self.max_length = max_length

    def __len__(self):
        return len(self.data)

    def __getitem__(self, idx):
        item = self.data[idx]

        # 构建完整序列
        full_text = (
            f"指令:{item['instruction']}\n"
            f"输入:{item['input']}\n"
            f"输出:{item['output']}{self.tokenizer.eos_token}"
        )
        instruction_text = (
            f"指令:{item['instruction']}\n"
            f"输入:{item['input']}\n"
            f"输出:"
        )

        # Tokenize 完整序列
        encoding = self.tokenizer(
            full_text,
            max_length=self.max_length,
            padding='max_length',
            truncation=True,
            return_tensors='pt'
        )

        # Tokenize 指令部分,计算需要 mask 的长度
        instruction_encoding = self.tokenizer(
            instruction_text,
            return_tensors='pt'
        )
        instruction_length = instruction_encoding['input_ids'].shape[1]

        # 创建 labels 并 mask 掉指令部分
        labels = encoding['input_ids'].clone()
        labels[0, :instruction_length] = -100  # -100 会在 loss 里被忽略

        return {
            'input_ids': encoding['input_ids'].squeeze(0),
            'attention_mask': encoding['attention_mask'].squeeze(0),
            'labels': labels.squeeze(0)
        }

# ===========================
# 5. 训练 + 保存 LoRA
# ===========================
def train_and_save():
    model, tokenizer = setup_model(base_model_name)
    model.to(device)

    dataset = IntentClassificationDataset(sample_data, tokenizer, max_length=256)

    # 简单划分训练/验证
    train_size = int(0.8 * len(dataset))
    eval_size = len(dataset) - train_size
    train_dataset, eval_dataset = random_split(dataset, [train_size, eval_size])

    print(f"训练集大小: {len(train_dataset)}")
    print(f"验证集大小: {len(eval_dataset)}")

    batch_size = 1  # 显存紧张时小一点
    train_dataloader = DataLoader(
        train_dataset,
        batch_size=batch_size,
        shuffle=True,
        num_workers=0
    )

    eval_dataloader = DataLoader(
        eval_dataset,
        batch_size=batch_size,
        shuffle=False,
        num_workers=0
    )

    learning_rate = 5e-5
    weight_decay = 0.01
    num_epochs = 1

    torch.manual_seed(123)
    optimizer = torch.optim.AdamW(
        model.parameters(),
        lr=learning_rate,
        weight_decay=weight_decay
    )

    start_time = time.time()
    global_step = 0

    for epoch in range(num_epochs):
        model.train()
        for batch in train_dataloader:
            input_ids = batch['input_ids'].to(device)
            attention_mask = batch['attention_mask'].to(device)
            labels = batch['labels'].to(device)

            optimizer.zero_grad()
            outputs = model(
                input_ids=input_ids,
                attention_mask=attention_mask,
                labels=labels
            )
            loss = outputs.loss
            loss.backward()
            optimizer.step()

            global_step += 1
            if global_step % 1 == 0:
                print(f"Epoch {epoch}, step {global_step}, loss {loss.item():.4f}")

    end_time = time.time()
    execution_time_minutes = (end_time - start_time) / 60
    print(f"Training completed in {execution_time_minutes:.2f} minutes.")

    # ====== 保存 LoRA 增量权重(不会保存基础模型参数)======
    Path(lora_output_dir).mkdir(parents=True, exist_ok=True)
    model.save_pretrained(lora_output_dir)
    tokenizer.save_pretrained(lora_output_dir)
    print(f"LoRA 权重与 tokenizer 已保存到: {lora_output_dir}")

# ===========================
# 6. 加载 LoRA + 推理示例
# ===========================
def load_lora_and_infer(user_input: str):
    """
    读取已训练好的 LoRA 权重,进行推理。
    这里假定 LoRA 已经保存在 lora_output_dir。
    """
    # 1. 加载基础模型和 tokenizer
    tokenizer = AutoTokenizer.from_pretrained(
        base_model_name,
        local_files_only=True,
        trust_remote_code=True
    )
    base_model = AutoModelForCausalLM.from_pretrained(
        base_model_name,
        local_files_only=True,
        dtype="auto",
        device_map="auto",
        trust_remote_code=True
    )

    # 2. 将 LoRA 适配器加载到基础模型上
    model = PeftModel.from_pretrained(
        base_model,
        lora_output_dir,
        device_map="auto"
    )
    model.eval()

    # 3. 构造推理提示词(和训练时格式保持一致)
    instruction = "作为意图分类专家,分析用户输入,将输入分类为攻击研判或攻击分析"
    prompt = f"指令:{instruction}\n输入:{user_input}\n输出:"

    inputs = tokenizer(
        prompt,
        return_tensors="pt"
    ).to(model.device)

    with torch.no_grad():
        generated_ids = model.generate(
            **inputs,
            max_new_tokens=128,
            do_sample=False,  # 分类任务一般不需要采样
            pad_token_id=tokenizer.eos_token_id
        )

    # 只取新生成的部分
    generated_text = tokenizer.decode(
        generated_ids[0][inputs['input_ids'].shape[1]:],
        skip_special_tokens=True
    )

    # 简单清洗一下空格和换行
    intent = generated_text.strip()
    print(f"模型预测意图: {intent}")
    return intent

# ===========================
# 7. 主入口
# ===========================
if __name__ == "__main__":
    # 第一步:训练并保存 LoRA
    train_and_save()

    # 第二步:加载 LoRA 并推理
    print("\n==== 推理示例 ====")
    test_inputs = [
        "研判告警 9d34dc99-a981-41dc-94bf-10b282354da4",
        "请列举10条高危告警"
    ]
    for t in test_inputs:
        print(f"\n用户输入: {t}")
        _ = load_lora_and_infer(t)

  

 

posted @ 2025-12-05 20:38  bonelee  阅读(2)  评论(0)    收藏  举报