自然语言处理应用篇 · 第8章

NLP 综合实战项目

把前面所有章节串起来:情感分析、新闻分类、简易搜索引擎、信息抽取四个项目走完整工程闭环,并给出部署与进阶路线。

🎯 学习目标

  • 独立完成"评论情感分析"全流程:清洗→分词→特征→训练→评估→保存
  • 掌握新闻文本分类在真实数据集上的工程组织方式
  • 用 TF-IDF + 余弦相似度实现一个可运行的简易搜索引擎
  • 用 NER + 关系模式做合同/新闻信息抽取并输出结构化 JSON
  • 了解模型持久化、FastAPI 部署与大模型 API 的调用方式

1项目一:电商评论情感分析(完整可运行)

把第 2、3、5、6 章的知识整合成一个单文件可运行的项目。真实使用时把 DATA 部分替换为 pandas 读取的评论 CSV 即可。

# ===== sentiment_project.py =====
import re, jieba, joblib
import pandas as pd
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.svm import LinearSVC
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.pipeline import Pipeline
from sklearn.metrics import classification_report, confusion_matrix

# ---------- 1. 数据(真实项目换成 pd.read_csv("comments.csv"))----------
DATA = [
    ("手机拍照清晰,续航超长,非常满意的一次购物", 1),
    ("物流超快第二天就到,包装严实,客服热情", 1),
    ("性价比很高,运行流畅,推荐大家购买", 1),
    ("屏幕色彩惊艳,音质也很棒,喜欢", 1),
    ("用了两周依然很流畅,系统简洁好用", 1),
    ("发货迅速,赠品丰富,五星好评", 1),
    ("收到货很惊喜,做工精致,超出预期", 1),
    ("续航能力强,重度使用一天无压力", 1),
    ("物流太慢了,一周才收到,体验很差", 0),
    ("手机频繁卡顿死机,电池也不耐用,后悔", 0),
    ("客服态度恶劣,退货推三阻四,差评", 0),
    ("屏幕有坏点,包装破损,申请售后中", 0),
    ("发热严重,玩游戏掉帧,完全不值这个价", 0),
    ("异味很大,用了三天就坏了,千万别买", 0),
    ("广告特别多,系统臃肿,没法用", 0),
    ("漏光严重,颜色失真,已经退货", 0),
]
df = pd.DataFrame(DATA, columns=["text", "label"])

# ---------- 2. 清洗 + 分词 ----------
def clean(text):
    text = re.sub(r"https?://\S+", " ", text)
    text = re.sub(r"[0-9a-zA-Z.+-]+@[0-9a-zA-Z.-]+", " ", text)
    text = re.sub(r"[^一-龥a-zA-Z,。!?、\s]", " ", text)
    return re.sub(r"\s+", " ", text).strip()

def tokenize(text):
    # 情感分析保留否定词,停用词表要谨慎!
    return " ".join(w for w in jieba.lcut(clean(text)) if w.strip())

df["x"] = df["text"].map(tokenize)

# ---------- 3. 划分数据集(分层抽样保持正负比例)----------
X_tr, X_te, y_tr, y_te = train_test_split(
    df["x"], df["label"], test_size=0.25,
    random_state=42, stratify=df["label"])

# ---------- 4. TF-IDF + 分类器流水线 ----------
model = Pipeline([
    ("tfidf", TfidfVectorizer(ngram_range=(1, 2),
                              min_df=1, max_df=0.9,
                              token_pattern=r"(?u)\b\w+\b",
                              sublinear_tf=True)),
    ("clf", LinearSVC(class_weight="balanced")),
])
model.fit(X_tr, y_tr)

# ---------- 5. 评估 ----------
pred = model.predict(X_te)
print(classification_report(y_te, pred,
      target_names=["负面", "正面"], digits=3, zero_division=0))
print("混淆矩阵:\n", confusion_matrix(y_te, pred))

# ---------- 6. 模型解释:看哪些词最影响判断 ----------
tfidf = model.named_steps["tfidf"]
clf = model.named_steps["clf"]
words = tfidf.get_feature_names_out()
weight = clf.coef_[0]
top_pos = sorted(zip(weight, words), reverse=True)[:8]
top_neg = sorted(zip(weight, words))[:8]
print("最正面词:", [w for _, w in top_pos])
print("最负面词:", [w for _, w in top_neg])

# ---------- 7. 保存 / 加载 / 上线预测 ----------
joblib.dump(model, "sentiment_model.pkl")
loaded = joblib.load("sentiment_model.pkl")

def predict(text):
    x = tokenize(text)
    tag = "正面" if loaded.predict([x])[0] == 1 else "负面"
    return tag

for t in ["电池很耐用,拍照也很清楚", "卡顿发热,客服还不理人"]:
    print(t, "→", predict(t))

错误分析是提升模型最有效的一步:把测试集中预测错的样本逐条看——是分词错(领域词未加入词典)、否定没处理好("不是不好")、反讽("真是好样的,三天就坏了"),还是训练集缺这类表达。针对性补数据远好于盲目换算法。反讽等复杂语义最终需要 BERT/大模型。

2项目二:新闻文本分类

数据集推荐 THUCNews(14 个类别:体育、财经、房产、教育……)。目录通常按类别组织:

THUCNews/
├── 体育/*.txt
├── 财经/*.txt
├── 房产/*.txt
└── ...(共14类)
# ===== news_classify.py =====
from pathlib import Path
import jieba, joblib
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.svm import LinearSVC
from sklearn.pipeline import Pipeline
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report

def load_thucnews(root, per_class=2000):
    """读取按类别分文件夹存放的语料,每类最多取 per_class 篇。"""
    texts, labels = [], []
    for cat_dir in Path(root).iterdir():
        if not cat_dir.is_dir():
            continue
        for i, f in enumerate(cat_dir.glob("*.txt")):
            if i >= per_class:
                break
            texts.append(f.read_text(encoding="utf-8", errors="ignore"))
            labels.append(cat_dir.name)
    return texts, labels

# X, y = load_thucnews("THUCNews")
# X = [" ".join(jieba.lcut(t)) for t in X]
# Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.2,
#                                       random_state=42, stratify=y)
# news_model = Pipeline([
#     ("tfidf", TfidfVectorizer(max_features=30000, ngram_range=(1, 2),
#                               min_df=3, max_df=0.9,
#                               token_pattern=r"(?u)\b\w+\b")),
#     ("clf", LinearSVC()),
# ]).fit(Xtr, ytr)
# print(classification_report(yte, news_model.predict(Xte), digits=3))
# joblib.dump(news_model, "news_model.pkl")
#
# 经验:清洗+分词+TF-IDF(1,2gram)+LinearSVC 在 THUCNews 上
# 即可达到 90%+ 准确率,是非常强的基线。

# 无中文数据集时,可用 sklearn 内置英文 20 类新闻集体验流程:
from sklearn.datasets import fetch_20newsgroups
train = fetch_20newsgroups(subset="train", remove=("headers", "footers"))
test = fetch_20newsgroups(subset="test", remove=("headers", "footers"))
en_model = Pipeline([
    ("tfidf", TfidfVectorizer(ngram_range=(1, 2), stop_words="english")),
    ("clf", LinearSVC()),
]).fit(train.data, train.target)
print("20News 准确率:", en_model.score(test.data, test.target))

3项目三:简易语义搜索引擎

实现"输入查询 → 返回最相关文档 TopK"的核心:文档向量化 + 倒排检索 + 余弦排序。真实搜索引擎(Elasticsearch)内核思想一致。

# ===== mini_search.py =====
import jieba
import numpy as np
import pandas as pd
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity

DOCS = [
    "Python 是人工智能与自然语言处理最常用的编程语言",
    "jieba 是流行的中文分词工具,支持自定义词典和词性标注",
    "TF-IDF 通过词频和逆文档频率衡量词语对文档的重要程度",
    "朴素贝叶斯和支持向量机常用于文本分类任务",
    "word2vec 可以把词语训练成低维稠密的语义向量",
    "情感分析用于判断电商评论是正面还是负面评价",
    "BERT 是基于 Transformer 的预训练语言模型",
]

class MiniSearchEngine:
    def __init__(self, docs):
        self.docs = docs
        cut = [" ".join(jieba.lcut(d)) for d in docs]
        self.tv = TfidfVectorizer(ngram_range=(1, 2),
                                  token_pattern=r"(?u)\b\w+\b")
        self.matrix = self.tv.fit_transform(cut)   # 文档库一次性建索引

    def search(self, query, top_k=3):
        q = " ".join(jieba.lcut(query))
        qv = self.tv.transform([q])                 # 查询向量化
        sims = cosine_similarity(qv, self.matrix)[0]
        order = np.argsort(sims)[::-1][:top_k]      # 相似度降序
        return [(i + 1, round(float(sims[i]), 3), self.docs[i])
                for i in order if sims[i] > 0]

engine = MiniSearchEngine(DOCS)
for rank, score, text in engine.search("怎么判断评论的好坏", top_k=3):
    print(f"#{rank}  score={score}  {text}")
# 语义相关的"情感分析…正面还是负面评价"会排在前列

升级路径:① 文档多了以后用稀疏矩阵 + 倒排索引只对含查询词的文档算相似度;② 关键词命中(BM25,Elasticsearch 默认算法)与语义向量(bge 句向量)混合召回;③ 用 FAISS/Milvus 做百万级向量近邻搜索;④ 加前端页面即一个完整问答/文档检索系统。

4项目四:合同/新闻信息抽取

目标:从非结构化文本输出结构化 JSON(实体 + 关系),可直接入库或对接知识图谱。

# ===== information_extract.py =====
import re, json
import spacy

nlp = spacy.load("zh_core_web_sm")

text = ("2026年3月15日,阿里巴巴(中国)有限公司与华为技术有限公司"
        "在深圳签署战略合作协议,金额为5亿元人民币,"
        "双方将在云计算领域开展合作,签约人为李明。")

doc = nlp(text)
record = {"entities": [], "money": [], "dates": [], "relations": []}

# ① 实体
for ent in doc.ents:
    record["entities"].append({"text": ent.text, "type": ent.label_})

# ② 正则补充:金额、日期、协议编号
record["money"] = re.findall(r"\d+(?:\.\d+)?(?:亿|万)?元(?:人民币)?", text)
record["dates"] = re.findall(r"\d{4}年\d{1,2}月\d{1,2}日", text)

# ③ 关系模式抽取("X 与 Y 签署…协议")
orgs = [e.text for e in doc.ents if e.label_ == "ORG"]
m = re.search(r"(.*?)与(.*?)签署(.*?)协议", text)
if m and len(orgs) >= 2:
    record["relations"].append(
        {"head": orgs[0], "relation": "签约合作", "tail": orgs[1]})

print(json.dumps(record, ensure_ascii=False, indent=2))
# {
#   "entities": [{"text": "阿里巴巴(中国)有限公司", "type": "ORG"}, ...],
#   "money": ["5亿元人民币"],
#   "dates": ["2026年3月15日"],
#   "relations": [{"head": "阿里巴巴...", "relation": "签约合作",
#                  "tail": "华为技术有限公司"}]
# }

工程现实:正则+小模型在固定格式合同上效果好;开放文本的关系抽取建议用大模型做零样本/少样本抽取(在 prompt 中直接要求输出 JSON),再用规则做格式校验,是当前性价比最高的方案。

5模型部署:从 notebook 到 API

训练好的模型用 joblib 持久化,再用 FastAPI 包成 HTTP 接口,前端/业务系统即可调用:

# ===== serve.py  运行:uvicorn serve:app --reload =====
import joblib, jieba
from fastapi import FastAPI
from pydantic import BaseModel

model = joblib.load("sentiment_model.pkl")   # 启动时加载一次
app = FastAPI(title="情感分析接口")

class Review(BaseModel):
    text: str

@app.post("/predict")
def predict(review: Review):
    x = " ".join(jieba.lcut(review.text))
    tag = "正面" if model.predict([x])[0] == 1 else "负面"
    return {"text": review.text, "sentiment": tag}

# 测试(另开终端):
# curl -X POST http://127.0.0.1:8000/predict \
#   -H "Content-Type: application/json" \
#   -d '{"text":"物流很快,非常满意"}'

大模型 API 调用(情感/摘要/抽取通用范式)

# pip install openai  (兼容 OpenAI 接口的任意国内大模型均可)
from openai import OpenAI

client = OpenAI(
    api_key="你的API_KEY",
    base_url="https://你的服务商地址/v1")

def llm_sentiment(text):
    resp = client.chat.completions.create(
        model="模型名",
        messages=[
            {"role": "system",
             "content": "你是情感分析器,只输出 JSON:{\"label\":\"正面/负面/中性\","
                        "\"reason\":\"一句话理由\"}"},
            {"role": "user", "content": text},
        ],
        temperature=0)
    return resp.choices[0].message.content

# print(llm_sentiment("价格贵了点,但是拍照效果确实惊艳"))
# 工程技巧:用大模型批量【预标注】数据 → 人工校对 → 训练小模型本地部署(省钱+快+数据私有)

6NLP 工程师进阶路线图

阶段补足能力推荐内容
① 地基(你在这里)Python + 传统 NLP 全流程本教程 + 复现 4 个项目
② 数学与 ML线代/概率统计、sklearn 系统学习吴恩达 ML、周志华《机器学习》选读
③ 深度学习PyTorch、RNN/CNN/注意力、Transformer李沐《动手学深度学习》、Annotated Transformer
④ 预训练模型BERT 微调文本分类/NER、LoRA 微调HuggingFace 官方教程、bge/chatGLM/Qwen
⑤ 大模型应用Prompt、RAG 检索增强、Agent、评测LangChain/LlamaIndex、向量数据库
⑥ 工程化数据标注、蒸馏、量化推理、监控迭代ONNX/vLLM、AB 测试、标注平台

7全教程知识地图回顾

Python 语法篇(14章)              NLP 应用篇(8章)
基础:类型/运算/流程/字符串    →   清洗(正则/归一化/停用词)
容器/函数/面向对象             →   jieba 分词 / 词性 / NER
异常/文件/高级特性/正则/库      →   词频 → TF-IDF → 关键词 → 词云
                                    ↓
              文本向量化(词袋/TF-IDF/word2vec/BERT)
                                    ↓
        分类与情感(贝叶斯/LR/SVM + 评估指标)
                                    ↓
   综合项目:情感分析 · 新闻分类 · 搜索引擎 · 信息抽取 · API 部署

8收官练习

📌 结语

你已经走完从 Python 零基础到 NLP 项目实战的完整路径。接下来最重要的只有一件事:动手做自己的项目——爬取你感兴趣的评论、分析你工作中的文本、把模型部署给朋友试用。在真实数据中遇到的每一个脏问题,都是进阶最快的台阶。祝编码愉快!

练习1:把项目一的数据集替换为真实评论 CSV(≥2000 条),列出你会执行的完整步骤。
① pandas 读取并检查缺失/重复/标签分布;② 复用 clean+tokenize 清洗分词,加入领域自定义词典;③ stratify 划分训练/测试(8:2),先跑 TF-IDF+LinearSVC 基线;④ 用 classification_report 看各类 P/R/F1 与混淆矩阵;⑤ 错误分析:针对错分样本补数据/改分词/加 bigram;⑥ 交叉验证与参数微调后 joblib 保存模型;⑦ FastAPI 封装并在新评论上灰度验证。
练习2:项目三搜索引擎在"查询词与文档没有字面重合"时召回失败,如何用第7章知识改进?
TF-IDF 是字面匹配,未登录/同义词无法命中。改进:加载预训练中文句向量模型(bge-small-zh/m3e/text2vec),离线把所有文档编码为稠密向量,查询实时编码后用余弦(FAISS 加速)做语义召回;最佳实践是 BM25 关键词召回与向量语义召回双路并行、分数融合(RAG 系统的标准做法)。
练习3:为项目四的抽取结果设计一个简单的质检方案,保证入库 JSON 的可靠性。
① 结构校验:用 jsonschema/pydantic 约束字段(实体类型枚举、日期与金额正则、必填项);② 规则校验:日期需在合理区间、金额数字与单位齐全、关系头尾必须是已识别的 ORG;③ 置信度分层:低置信样本导出人工复核并统计一致率;④ 大模型抽取时要求只输出 JSON 并用 try/json 解析+重试兜底;⑤ 定期抽样金标准计算实体级 P/R/F1 监控质量漂移。