Skip to content

章节5:模块综合实战


学习目标

  • 综合运用 jieba、Word2Vec、BERT 完成商品评论情感分析系统
  • 运用 TF‑IDF、FastText、意图识别完成智能客服意图分类模型
  • 掌握从数据预处理到模型训练再到部署的完整 NLP 项目流程
  • 学会评估与对比不同方法的优劣

实战一:商品评论情感分析系统

项目概览

输入: "这个手机拍照效果非常好,电池续航也很给力!"
输出: {"情感": "正面", "置信度": 0.97}

方案一:jieba + Word2Vec + 分类器(传统方法)

python
import jieba
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, classification_report
from gensim.models import Word2Vec

# ==================== 1. 准备数据 ====================
reviews = [
    "这个手机拍照效果非常好,电池续航也很给力",       # 正面
    "质量太差了,用了三天就坏了",                    # 负面
    "价格实惠,性价比很高,推荐购买",                 # 正面
    "物流很慢,包装破损,不满意",                    # 负面
    "屏幕显示效果惊艳,颜色很正",                    # 正面
    "客服态度恶劣,完全不解决问题",                   # 负面
    "手感很好,轻薄便携,续航也不错",                 # 正面
    "经常死机,系统优化太差",                        # 负面
    "外观时尚,拍照清晰,非常满意",                   # 正面
    "收到就是坏的,退换货流程复杂",                   # 负面
]
labels = [1, 0, 1, 0, 1, 0, 1, 0, 1, 0]

# ==================== 2. 分词 ====================
def tokenize(text: str) -> list:
    return jieba.lcut(text)

corpus_tokens = [tokenize(r) for r in reviews]

# ==================== 3. 训练 Word2Vec ====================
w2v_model = Word2Vec(
    sentences=corpus_tokens,
    vector_size=100,
    window=5,
    min_count=1,
    sg=0,  # CBOW
    epochs=50
)

# ==================== 4. 文档向量化(均值池化) ====================
def doc_vector(tokens: list, model: Word2Vec) -> np.ndarray:
    vectors = [model.wv[w] for w in tokens if w in model.wv]
    if not vectors:
        return np.zeros(model.vector_size)
    return np.mean(vectors, axis=0)

X = np.array([doc_vector(tokens, w2v_model) for tokens in corpus_tokens])
y = np.array(labels)

# ==================== 5. 训练分类器 ====================
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, random_state=42
)

clf = LogisticRegression(max_iter=1000)
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)

print("\n=== 方案一:Word2Vec + LR 结果 ===")
print(f"准确率: {accuracy_score(y_test, y_pred):.4f}")

# ==================== 6. 预测新评论 ====================
def predict_v1(text: str) -> dict:
    tokens = tokenize(text)
    vec = doc_vector(tokens, w2v_model).reshape(1, -1)
    prob = clf.predict_proba(vec)[0]
    label = "正面" if clf.predict(vec)[0] == 1 else "负面"
    return {"text": text, "情感": label, "置信度": max(prob):.4f}

print(predict_v1("性价比超高的好产品"))

方案二:BERT 微调(深度学习方法)

python
from transformers import (BertTokenizer, BertForSequenceClassification,
                          Trainer, TrainingArguments)
from datasets import Dataset
import torch

# ==================== 1. 准备数据 ====================
# 扩充数据量(实际项目应使用 > 10,000 条)
all_reviews = reviews * 5  # 简单增强
all_labels = labels * 5

# ==================== 2. 加载模型 ====================
model_name = "bert-base-chinese"
tokenizer = BertTokenizer.from_pretrained(model_name)
model = BertForSequenceClassification.from_pretrained(model_name, num_labels=2)

# ==================== 3. 编码 ====================
def encode(batch):
    return tokenizer(batch["text"], padding="max_length",
                     truncation=True, max_length=128)

dataset = Dataset.from_dict({
    "text": all_reviews,
    "label": all_labels
}).map(encode, batched=True)

# ==================== 4. 微调 ====================
training_args = TrainingArguments(
    output_dir="./bert_sentiment_model",
    per_device_train_batch_size=8,
    num_train_epochs=3,
    learning_rate=2e-5,
    save_strategy="no",
    logging_steps=10,
    report_to="none",
)

trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=dataset,
)

trainer.train()

# ==================== 5. 保存与推理 ====================
model.save_pretrained("./bert_sentiment_model")
tokenizer.save_pretrained("./bert_sentiment_model")

def predict_v2(text: str) -> dict:
    inputs = tokenizer(text, return_tensors="pt",
                       truncation=True, max_length=128)
    with torch.no_grad():
        outputs = model(**inputs)
        probs = torch.nn.functional.softmax(outputs.logits, dim=-1)
    label = "正面" if probs[0][1] > 0.5 else "负面"
    return {"text": text, "情感": label, "置信度": probs[0][1].item():.4f}

print(predict_v2("这个产品太令人失望了"))

两种方案对比

维度Word2Vec + LRBERT 微调
训练速度⚡ 秒级🐢 分钟~小时
推理速度⚡ 毫秒⚡ 毫秒
数据需求百级可用千级以上效果明显
语义理解一般(静态向量)优秀(上下文动态)
部署体积小(约 50MB)大(约 400MB)
可解释性较高较低
最佳场景快速原型、资源受限精度优先、数据充足

实战二:智能客服意图分类模型

项目概览

输入: "我想查询一下我的订单什么时候发货"
输出: {"意图": "查询订单", "置信度": 0.95}

方案一:TF‑IDF + FastText + 集成分类

python
import jieba
import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.preprocessing import LabelEncoder
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report, accuracy_score
from gensim.models import FastText

# ==================== 1. 准备数据 ====================
queries = [
    # 查询订单
    "我的订单到哪了", "查一下快递", "什么时候发货",
    "物流信息怎么查看", "订单号 10086 的物流状态",
    # 退换货
    "我要退货", "怎么申请退款", "商品坏了可以换吗",
    "退货运费谁承担", "退款多久到账",
    # 产品咨询
    "这个手机支持5G吗", "电池容量多大", "有没有红色款",
    "保修期多久", "支不支持无线充电",
    # 账户问题
    "密码忘记了", "怎么修改手机号", "账号被盗了",
    "怎么注销账号", "登录不了",
]

intents = (
    ["查询订单"] * 5 +
    ["退换货"] * 5 +
    ["产品咨询"] * 5 +
    ["账户问题"] * 5
)

# ==================== 2. 特征工程 ====================
# 方法 A:TF-IDF 特征
tfidf_vec = TfidfVectorizer(tokenizer=jieba.lcut, max_features=500)
X_tfidf = tfidf_vec.fit_transform(queries).toarray()

# 方法 B:FastText 文档向量
corpus_tokens = [jieba.lcut(q) for q in queries]
ft_model = FastText(
    sentences=corpus_tokens,
    vector_size=50,
    window=5,
    min_count=1,
    min_n=2,
    max_n=4,
    epochs=100
)

def ft_doc_vector(tokens: list) -> np.ndarray:
    vectors = [ft_model.wv[w] for w in tokens if w in ft_model.wv]
    if not vectors:
        return np.zeros(ft_model.vector_size)
    return np.mean(vectors, axis=0)

X_ft = np.array([ft_doc_vector(tokens) for tokens in corpus_tokens])

# 方法 C:特征融合
X_combined = np.concatenate([X_tfidf, X_ft], axis=1)
print(f"融合特征维度: {X_combined.shape[1]}")

# ==================== 3. 标签编码 ====================
le = LabelEncoder()
y = le.fit_transform(intents)

# ==================== 4. 训练 ====================
X_train, X_test, y_train, y_test = train_test_split(
    X_combined, y, test_size=0.3, random_state=42
)

clf = RandomForestClassifier(n_estimators=100, random_state=42)
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)

print("\n=== 客服意图分类结果 ===")
print(f"准确率: {accuracy_score(y_test, y_pred):.4f}")
print("\n分类报告:")
print(classification_report(y_test, y_pred, target_names=le.classes_))

# ==================== 5. 在线预测 ====================
def classify_intent(text: str) -> dict:
    tokens = jieba.lcut(text)
    tfidf_feat = tfidf_vec.transform([text]).toarray()
    ft_feat = ft_doc_vector(tokens).reshape(1, -1)
    feat = np.concatenate([tfidf_feat, ft_feat], axis=1)
    proba = clf.predict_proba(feat)[0]
    pred_idx = clf.predict(feat)[0]
    intent = le.inverse_transform([pred_idx])[0]
    return {"text": text, "意图": intent, "置信度": max(proba):.4f}

# 测试
tests = [
    "查一下我的快递到哪了",
    "手机屏幕碎了能换吗",
    "这个笔记本内存多大",
    "我忘记登录密码了"
]
for t in tests:
    print(classify_intent(t))

方案二:意图识别 + 槽位填充的 NLU 管道

python
import re
from dataclasses import dataclass, asdict
from typing import Optional

@dataclass
class IntentResult:
    intent: str
    confidence: float
    slots: dict
    raw_text: str

class IntentNLU:
    """基于规则的意图识别 + 槽位填充(可作为模型冷启动替代方案)"""

    def __init__(self):
        # 意图 → 触发词列表
        self.intent_patterns = {
            "查询订单": ["订单", "快递", "物流", "发货", "到哪", "配送"],
            "退换货":   ["退货", "退款", "退换", "换货", "坏了", "退货"],
            "产品咨询": ["参数", "支持", "多大", "颜色", "功能", "配置", "内存"],
            "账户问题": ["密码", "账号", "登录", "手机号", "注销", "绑定"],
        }

    def extract_intent(self, text: str) -> tuple:
        """统计匹配的触发词数量,返回最高分意图"""
        scores = {}
        for intent, keywords in self.intent_patterns.items():
            score = sum(1 for kw in keywords if kw in text)
            if score > 0:
                scores[intent] = score

        if not scores:
            return "闲聊", 0.0

        best_intent = max(scores, key=scores.get)
        total = sum(scores.values())
        confidence = scores[best_intent] / total
        return best_intent, confidence

    def extract_slots(self, text: str, intent: str) -> dict:
        """根据意图类型提取对应槽位"""
        slots = {}

        # 通用槽位:订单号
        order_match = re.search(r"(\d{5,})", text)
        if order_match:
            slots["订单号"] = order_match.group(1)

        # 产品咨询:提取产品名
        product_match = re.search(r"(手机|电脑|笔记本|平板|耳机|充电器)", text)
        if product_match:
            slots["产品"] = product_match.group(1)

        # 账户问题:提取操作类型
        if intent == "账户问题":
            if "密码" in text:
                slots["操作"] = "密码管理"
            elif "手机号" in text:
                slots["操作"] = "修改手机号"
            elif "注销" in text:
                slots["操作"] = "注销账号"

        return slots

    def __call__(self, text: str) -> IntentResult:
        intent, confidence = self.extract_intent(text)
        slots = self.extract_slots(text, intent)
        return IntentResult(
            intent=intent,
            confidence=confidence,
            slots=slots,
            raw_text=text
        )

# 测试
nlu = IntentNLU()
test_cases = [
    "帮我查一下订单 12345 的物流信息",
    "我买的手机想退货",
    "这个笔记本内存多大",
    "我忘记密码了怎么办"
]

for case in test_cases:
    result = nlu(case)
    print(asdict(result))

部署为一个简单的 API

python
from flask import Flask, request, jsonify

app = Flask(__name__)
nlu = IntentNLU()  # 或使用训练好的模型

@app.route("/api/nlu", methods=["POST"])
def nlu_endpoint():
    data = request.get_json()
    text = data.get("text", "")
    if not text:
        return jsonify({"error": "text is required"}), 400
    result = nlu(text)
    return jsonify(asdict(result))

@app.route("/api/sentiment", methods=["POST"])
def sentiment_endpoint():
    data = request.get_json()
    text = data.get("text", "")
    if not text:
        return jsonify({"error": "text is required"}), 400
    # 加载保存的 BERT 模型
    # result = predict_v2(text)
    return jsonify({"text": text, "message": "模型加载后返回"})

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000, debug=False)

综合对比与选型指南

场景推荐方案原因
快速原型、数据量小jieba + TF‑IDF + LR无需 GPU,几分钟出结果
中等规模、精度优先Word2Vec/FastText + 深度分类器语义泛化能力优于 TF‑IDF
大规模、最高精度BERT / RoBERTa 微调上下文动态表示,SOTA
冷启动、无标注数据规则系统(正则 + 触发词)零标注成本,快速上线
生产系统规则 + 模型结合规则兜底,模型提精度

项目总结

通过本模块的两个综合实战,我们完成了:

  1. 商品评论情感分析系统

    • 使用 jieba + Word2Vec + LogisticRegression 快速实现基线
    • 使用 BERT 微调 实现高精度情感分类
    • 对比了传统方法与深度学习方法在不同场景下的优劣势
  2. 智能客服意图分类模型

    • 使用 TF‑IDF + FastText 特征融合 + RandomForest 实现多分类
    • 实现基于规则的 NLU 管道作为冷启动方案
    • 部署为可调用的 RESTful API

课后练习

  1. 在电商评论数据集()上分别用 Word2Vec + LR 和 BERT 微调训练情感模型,对比它们在测试集上的准确率和 F1 值。
  2. 为智能客服系统增加"投诉建议"意图类别,收集10条对应语料,重新训练并评估。
  3. 将 BERT 情感分析模型导出为 ONNX 格式,对比导出前后的推理速度。
  4. 设计一个"规则 + 模型"的混合 NLU 系统:当模型置信度 < 0.6 时,使用规则结果;否则使用模型结果。
  5. 挑战题: 将本模块两个实战合并为一个完整的"智能客服 + 评论分析"系统,前端提供聊天界面,用户输入评论时自动做情感分析,输入问题时做意图分类。可使用 Gradio 或 Streamlit 搭建 UI。

Python 学习资料