前言
在自然语言处理(NLP)领域,意图识别(Intent Classification)是构建智能对话系统的核心任务之一。无论是智能客服、虚拟助手还是专业领域的问答系统,意图识别都能帮助系统准确理解用户的需求并提供相应的响应。本文介绍如何基于 Python 和 Hugging Face 的 Transformers 库,利用 DeepSeek-R1-Distill-Qwen-1.5B 模型实现一个专业领域的意图识别系统,涵盖法律查询、病虫害防治、OA 审批等场景。
项目背景与需求
本项目旨在设计一个多分类的意图识别系统,支持以下 5 个专业领域的意图类别:
- 法律查询:如“如何查询地方性林业法规?”
- 病虫害防治:如“松材线虫的物理防治方法有哪些?”
- OA 审批:如“如何提交休假审批流程?”
- 林地查询:如“坐标 116.4,39.9 适合种植什么?”
- 生活服务:如“附近有什么电影院?”
系统需求包括:
- 使用预训练模型 DeepSeek-R1-Distill-Qwen-1.5B,并通过微调适配特定领域数据。
- 支持高效的训练与预测功能,适配 CPU 环境。
- 提供清晰的代码实现逻辑和流程可视化。
系统实现逻辑
系统的实现基于 Transformer 架构,整个流程包括数据预处理、模型训练与验证、以及预测功能的开发。以下是核心步骤:
- 环境配置:依赖 Python 3.8+、PyTorch 2.3.0 和 Transformers 4.40.0。
- 数据处理:从 CSV 文件加载数据,清洗并构建自定义数据集。
- 模型训练:使用 DataLoader 进行批次训练,优化器选择 AdamW。
- 验证与评估:计算验证集的损失值和准确率。
- 预测与保存:实现单条文本的意图预测,并保存模型。
下面,我们将深入探讨代码的实现细节,并通过流程图直观展示逻辑。
代码实现详解
以下是基于用户提供的代码,整理出的核心实现部分及其说明。
1. 环境与配置
首先,我们定义一个 Config
类来管理模型和训练参数:
class Config:
model_name = "deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B"
CACHE_DIR = "./models_nlu"
token = "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"
data_path = "../../2_26/nlu_dataset.csv"
num_labels = 5
max_length = 128
batch_size = 2
learning_rate = 2e-5
epochs = 3
seed = 42
label_map = {
1: "法律查询",
2: "病虫害防治",
3: "OA审批",
4: "林地查询",
5: "生活服务"
}
- 关键参数:
model_name
:使用的预训练模型。num_labels
:意图类别数量。batch_size
:批次大小,适配 CPU 环境的设置为 2。label_map
:将数字标签映射为具体的意图类别。
2. 数据加载与预处理
数据加载函数负责读取 CSV 文件并进行初步清洗:
def load_data(file_path):
df = pd.read_csv(file_path)
df = df.dropna(subset=["text", "intent_label"])
df["intent_label"] = df["intent_label"].astype(int)
valid_labels = set(Config.label_map.keys())
df = df[df["intent_label"].isin(valid_labels)]
return df
- 功能:
- 去除文本和标签列中的空值。
- 确保标签在有效范围内(1-5)。
3. 自定义数据集
为了适配 Transformer 模型的输入格式,我们定义了一个 ProfessionalDataset
类:
class ProfessionalDataset(Dataset):
def __init__(self, texts, labels, tokenizer):
self.texts = texts
self.labels = labels
self.tokenizer = tokenizer
def __getitem__(self, idx):
text = str(self.texts[idx])
label = self.labels[idx] - 1 # 调整为 0 基索引
encoding = self.tokenizer(
text,
max_length=Config.max_length,
padding="max_length",
truncation=True,
return_tensors="pt"
)
return {
"input_ids": encoding["input_ids"].flatten(),
"attention_mask": encoding["attention_mask"].flatten(),
"labels": torch.tensor(label, dtype=torch.long)
}
- 作用:
- 将文本编码为
input_ids
和attention_mask
。 - 将标签从 1-5 调整为 0-4,以适配模型的分类层。
- 将文本编码为
4. 模型初始化
加载预训练模型和 tokenizer:
tokenizer = AutoTokenizer.from_pretrained(Config.model_name, cache_dir=Config.CACHE_DIR, token=Config.token, use_fast=False)
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
model = AutoModelForSequenceClassification.from_pretrained(
Config.model_name,
num_labels=len(Config.label_map),
cache_dir=Config.CACHE_DIR,
token=Config.token,
pad_token_id=tokenizer.pad_token_id
)
- 注意:
- 如果 tokenizer 没有定义填充标记,则使用结束标记(
eos_token
)替代。
- 如果 tokenizer 没有定义填充标记,则使用结束标记(
5. 数据准备与训练
将数据划分为训练集和验证集,并进行批次训练:
df = load_data(Config.data_path)
train_df, val_df = train_test_split(df, test_size=0.2, random_state=Config.seed, stratify=df["intent_label"])
train_dataset = ProfessionalDataset(train_df["text"].tolist(), train_df["intent_label"].tolist(), tokenizer)
val_dataset = ProfessionalDataset(val_df["text"].tolist(), val_df["intent_label"].tolist(), tokenizer)
train_loader = DataLoader(train_dataset, batch_size=Config.batch_size, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=Config.batch_size)
optimizer = AdamW(model.parameters(), lr=Config.learning_rate)
for epoch in range(Config.epochs):
model.train()
total_train_loss = 0
for batch in train_loader:
optimizer.zero_grad()
outputs = model(input_ids=batch["input_ids"], attention_mask=batch["attention_mask"], labels=batch["labels"])
loss = outputs.loss
loss.backward()
optimizer.step()
total_train_loss += loss.item()
avg_train_loss = total_train_loss / len(train_loader)
model.eval()
total_val_loss = 0
correct_predictions = 0
with torch.no_grad():
for batch in val_loader:
outputs = model(input_ids=batch["input_ids"], attention_mask=batch["attention_mask"], labels=batch["labels"])
total_val_loss += outputs.loss.item()
_, preds = torch.max(outputs.logits, dim=1)
correct_predictions += torch.sum(preds == batch["labels"])
avg_val_loss = total_val_loss / len(val_loader)
accuracy = correct_predictions.double() / len(val_dataset)
print(f"Epoch {epoch + 1}/{Config.epochs}")
print(f"Train Loss: {avg_train_loss:.4f} | Val Loss: {avg_val_loss:.4f}")
print(f"Val Accuracy: {accuracy:.4f}\n")
- 流程:
- 数据按 8:2 比例分割,保持类别分布(
stratify
)。 - 训练阶段计算损失并更新参数,验证阶段评估模型性能。
- 数据按 8:2 比例分割,保持类别分布(
6. 预测与保存
训练完成后,保存模型并提供预测函数:
model.save_pretrained("./professional_intent_model")
tokenizer.save_pretrained("./professional_intent_model")
def predict_professional_intent(text):
model.eval()
encoding = tokenizer(text, max_length=Config.max_length, padding="max_length", truncation=True, return_tensors="pt")
with torch.no_grad():
outputs = model(**encoding)
_, pred_id = torch.max(outputs.logits, dim=1)
return Config.label_map[pred_id.item() + 1] # 调整回 1 基索引
- 测试示例:
test_cases = [
"如何查询地方性林业法规?",
"松材线虫的物理防治方法有哪些?",
"坐标116.4,39.9适合种植什么?",
"附近有什么电影院?"
]
for text in test_cases:
print(f"输入:'{text}' => 预测意图:{predict_professional_intent(text)}")
流程图分析
为了更直观地展示实现逻辑,以下是基于代码和描述设计的流程图文字描述:
训练流程图
开始
↓
数据加载 (load_data)
↓
数据预处理 (ProfessionalDataset)
↓
数据分割 (train_test_split)
↓
模型初始化 (AutoModelForSequenceClassification)
↓
优化器配置 (AdamW)
↓
训练循环 (for epoch in range(epochs))
↓
计算损失 → 反向传播 → 更新参数
↓
验证 (model.eval())
↓
计算准确率 → 输出结果
↓
模型保存
↓
结束
预测流程图
开始
↓
输入文本
↓
文本编码 (tokenizer)
↓
模型预测 (model.eval())
↓
获取最高概率标签
↓
映射意图 (label_map)
↓
输出结果
↓
结束
优化与扩展建议
1. 性能优化
- 批量大小调整:当前
batch_size=2
适合 CPU,若使用 GPU,可调整为 16 或 32,提升训练效率。 - 学习率调度:引入
torch.optim.lr_scheduler
,动态调整学习率,避免过拟合。 - 混合精度训练:使用
torch.cuda.amp
,加速训练并减少内存占用。
2. 模型扩展
- 多模态支持:扩展系统支持语音输入(通过语音转文本)或图像输入,增强实用性。
- RAG 集成:结合检索增强生成(RAG)技术,利用外部知识库(如林业法规数据库)提升预测质量。
- 模型蒸馏:将更大模型(如 DeepSeek-R1-7B)蒸馏为轻量模型,适配边缘设备。
3. 数据增强
- 数据扩充:通过同义句生成或回译技术增加数据多样性。
- 标签平衡:若类别分布不均,可使用过采样或加权损失函数(如
CrossEntropyLoss(weight=class_weights)
)。
4. 部署与监控
- 模型量化:使用 ONNX 或 TensorRT 优化推理速度。
- 日志系统:升级
logging
模块,记录每批次损失和预测结果,便于调试。 - 在线服务:部署至 Flask 或 FastAPI,提供实时预测 API。