AI自然语言处理与推荐系统实战指南

前言:经典 AI 仍然重要

大模型时代,NLP 和推荐系统这些"传统" AI 任务并没有消失。它们在很多场景下仍然是最实用、最划算的方案。

经典 AI 的价值:

  • 轻量高效:不需要 GPU 集群
  • 可解释:业务能理解
  • 稳定:模型行为可预测
  • 成本低:适合中小项目

一、NLP Pipeline 的完整流程

flowchart LR A[原始文本] --> B[预处理<br/>清洗/标准化] B --> C[特征提取<br/>向量化/嵌入] C --> D[模型推理<br/>分类/抽取] D --> E[后处理<br/>过滤/聚合] E --> F[结果输出]

每个环节都有坑,下面按顺序讲。

二、文本预处理

2.1 清洗噪声

import re

def clean_text(text):
    # 移除多余空白
    text = re.sub(r'\s+', ' ', text).strip()

    # 处理表情符号(情感任务要保留)
    emoji_pattern = re.compile("["
        u"\U0001F600-\U0001F64F"
        u"\U0001F300-\U0001F5FF"
        u"\U0001F680-\U0001F6FF"
        u"\U0001F1E0-\U0001F1FF"
        "]+", flags=re.UNICODE)

    # 根据任务决定是否移除
    # 情感分析保留 emoji,分类任务移除
    text = emoji_pattern.sub('', text)

    # 移除 HTML 标签
    text = re.sub(r'<[^>]+>', '', text)

    return text

坑: 表情符号在情感分析里携带重要信息,不要无脑删除。

2.2 文本标准化

def normalize_text(text):
    text = text.lower()  # 英文
    # 全半角转换
    text = text.replace(',', ',').replace('。', '.')
    text = text.replace('(', '(').replace(')', ')')
    return text

坑: 某些专有名词大小写是信息的一部分,一转小写反而丢语义。

2.3 中文分词

import jieba
import jieba.posseg as pseg

def tokenize_text(text):
    # 词性标注过滤(保留名词、动词、形容词)
    tagged_words = []
    for word, flag in pseg.cut(text):
        if flag.startswith(('n', 'v', 'a')):
            tagged_words.append(word)
    return tagged_words

坑: “用户体验"拆成"用户"和"体验"可能丢失组合语义。用自定义词典把这类短语作为整体处理。

三、特征提取

3.1 TF-IDF(传统但实用)

from sklearn.feature_extraction.text import TfidfVectorizer

tfidf_vectorizer = TfidfVectorizer(
    max_features=5000,
    ngram_range=(1, 2),      # unigram + bigram
    min_df=2,
    max_df=0.8,
    stop_words=None           # 中文停用词要自定义
)

tfidf_matrix = tfidf_vectorizer.fit_transform(train_texts)

优势: 可解释性强,能清楚看到每个文档由哪些词组成。

局限: 无法捕捉语义关系——“汽车"和"车子"是完全不同的向量。

3.2 预训练模型嵌入

from transformers import AutoTokenizer, AutoModel

model_name = "hfl/chinese-roberta-wwm-ext"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModel.from_pretrained(model_name)

def get_embeddings(text):
    inputs = tokenizer(text, return_tensors="pt",
                      padding=True, truncation=True, max_length=512)
    outputs = model(**inputs)
    # [CLS] token 作为句子级嵌入
    return outputs.last_hidden_state[:, 0, :].detach().numpy()

坑: 预训练模型动辄几百 MB,资源受限场景部署困难。

策略:

  • 高价值低频任务 → 预训练模型
  • 高吞吐实时场景 → TF-IDF 或轻量模型

四、文本分类

4.1 用户行为分类示例

任务:把用户分为活跃、普通、流失三类。

# 关键:标签定义
# 活跃用户:过去 7 天内有登录,且 session > 5
# 普通用户:过去 14 天内有登录,session <= 5
# 流失用户:过去 14 天内无登录

坑一:数据泄露

错误做法:先对整个数据集标准化,再分割训练集/测试集。

# 正确做法
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)

scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)  # 只在训练集 fit
X_test_scaled = scaler.transform(X_test)        # 用训练集的 scaler

坑二:过拟合

# 限制树的深度防止过拟合
model = RandomForestClassifier(
    n_estimators=100,
    max_depth=10,              # 不要 None
    min_samples_split=5,
    min_samples_leaf=2,
    random_state=42
)

坑三:类别不平衡

# 方案一:加权损失
model = LogisticRegression(class_weight='balanced')

# 方案二:过采样
from imblearn.over_sampling import SMOTE
X_resampled, y_resampled = SMOTE().fit_resample(X, y)

4.2 评估指标

from sklearn.metrics import classification_report

print(classification_report(y_test, y_pred))

重要: 准确率不够用,要看每个类别的 F1-score,特别是少数类。

五、情感分析

5.1 轻量方案:SnowNLP

from snownlp import SnowNLP

def sentiment_snownlp(text):
    s = SnowNLP(text)
    score = s.sentiments  # 0-1
    if score > 0.6: return 'positive'
    elif score < 0.4: return 'negative'
    else: return 'neutral'

5.2 深度方案:预训练模型

from transformers import pipeline

analyzer = pipeline(
    "sentiment-analysis",
    model="lxyuan/distilbert-base-multilingual-cased-sentiments-student",
    return_all_scores=True
)

5.3 混合策略(推荐)

预处理层做快速筛选,明显倾向的用轻量模型,模糊样本留给深度模型。

六、命名实体识别(NER)

from transformers import pipeline

ner = pipeline(
    "ner",
    model="hfl/chinese-roberta-wwm-ext",
    aggregation_strategy="simple"
)

def extract_entities(text):
    entities = ner(text)
    return {
        'products': [e['word'] for e in entities if e['entity_group'] == 'PRODUCT'],
        'features': [e['word'] for e in entities if e['entity_group'] == 'FEATURE']
    }

坑: 模型输出的实体类型往往和业务标签不匹配,需要后处理做映射。

七、知识图谱构建

7.1 整体流程

graph LR A[原始文档] --> B[数据清洗] B --> C[实体抽取] C --> D[关系抽取] D --> E[图谱构建] E --> F[质量评估] F --> G[应用对接]

7.2 Neo4j 图数据库

// 创建节点
CREATE (u:User {id: '123', name: '张三'})
CREATE (o:Order {id: 'order_001', amount: 100})
CREATE (r:Refund {id: 'refund_001', reason: '质量问题'})

// 创建关系
MATCH (u:User {id: '123'}), (o:Order {id: 'order_001'})
CREATE (u)-[:发起]->(o)

MATCH (o:Order {id: 'order_001'}), (r:Refund {id: 'refund_001'})
CREATE (o)-[:包含]->(r)

7.3 知识图谱的坑

坑一:节点爆炸

把每个词都当节点,图里有几十万无用节点。解决:白名单机制,只保留业务明确的实体。

坑二:关系类型爆炸

定义几十种关系,维护成本高。解决:合并成七八种核心关系。

坑三:查询性能

// 慢查询:遍历大量无关节点
MATCH (u:User {id: '123'})-[:发起]->(o:Order)-[:包含]->(r:Refund)
RETURN r

// 优化:先过滤再连接
MATCH (u:User {id: '123'})-[:发起]->(o:Order)
WHERE exists((o)-[:包含]->(:Refund))
MATCH (o)-[:包含]->(r:Refund)
RETURN r

坑四:数据一致性

新增数据时容易出现"孤节点”。解决:建图时强制检查每个节点至少有一个关系。

7.4 知识图谱 + RAG

知识图谱解决 RAG 的三个问题:

  1. 召回太宽泛:关键词搜索不理解上下文
  2. 答案不直接:用户要答案,不是文档链接
  3. 不支持追问:无法理解上下文延续

效果: 召回准确率提升 30%,支持上下文追问。

八、推荐系统

8.1 推荐系统架构

graph TD A[用户请求] --> B{用户类型} B -->|新用户| C[热门推荐] B -->|老用户| D[协同过滤] B -->|新商品| E[内容过滤] D --> F[结果合并] E --> F C --> F F --> G[返回推荐]

8.2 协同过滤:用户-商品矩阵

import numpy as np
from collections import defaultdict

def build_user_item_matrix(behaviors):
    """构建用户-商品交互矩阵"""
    interactions = defaultdict(lambda: defaultdict(float))

    # 不同行为不同权重
    action_weights = {
        'view': 1.0,
        'favorite': 2.0,
        'purchase': 5.0
    }

    for behavior in behaviors:
        uid = behavior['user_id']
        iid = behavior['product_id']
        action = behavior['action']
        interactions[uid][iid] += action_weights.get(action, 0)

    return interactions

8.3 矩阵分解

from scipy.sparse.linalg import svds

def matrix_factorization(matrix, n_factors=10):
    """SVD 矩阵分解降维"""
    user_means = np.mean(matrix, axis=1)
    matrix_demeaned = matrix - user_means.reshape(-1, 1)

    U, sigma, Vt = svds(matrix_demeaned, k=n_factors)
    sigma = np.diag(sigma)

    predicted = np.dot(np.dot(U, sigma), Vt) + user_means.reshape(-1, 1)
    return predicted, U, sigma, Vt

8.4 商品相似度

from sklearn.metrics.pairwise import cosine_similarity

def calculate_item_similarity(Vt):
    item_features = Vt.T
    return cosine_similarity(item_features)

def get_similar_items(item_id, item_to_idx, similarity_matrix, top_k=10):
    if item_id not in item_to_idx:
        return []
    item_idx = item_to_idx[item_id]
    similarities = similarity_matrix[item_idx]
    # 排除自己
    similar_indices = np.argsort(similarities)[::-1][1:top_k+1]
    return [(idx, similarities[idx]) for idx in similar_indices if similarities[idx] > 0]

8.5 内容过滤(解决冷启动)

def build_content_similarity(products):
    """基于商品内容的相似度"""
    texts = [f"{p['name']} {p['category']} {' '.join(p['tags'])}" for p in products]

    vectorizer = TfidfVectorizer(max_features=1000)
    tfidf_matrix = vectorizer.fit_transform(texts)

    return cosine_similarity(tfidf_matrix), vectorizer

8.6 学习排序(Learning to Rank)

协同过滤和内容过滤解决的是"召回”——把候选集找出来。但最终展示给用户的顺序,还需要一个精排模型。学习排序(Learning to Rank, LTR)就是专门为排序任务设计的监督学习方法,既适用于搜索结果排序,也适用于推荐结果的最终排序。

两阶段架构:召回 + 精排

graph LR A[用户查询] --> B[粗排召回<br/>BM25/协同过滤] B --> C[Top 100 候选] C --> D[特征提取] D --> E[精排模型<br/>LambdaMART] E --> F[Top 10 结果]

先用轻量算法(BM25、协同过滤)粗排召回 Top 100,再用 LTR 模型精排出 Top 10。粗排保证召回率,精排保证精确度。BM25 只看词频匹配,不知道用户是谁、内容质量如何、是否过时——这些都是精排模型要补的维度。

三种建模思路

方法思路代表算法
Pointwise把排序当作分数预测/分类Linear Regression、Random Forest
Pairwise优化文档对的相对顺序RankSVM、XGBoost rank:pairwise
Listwise直接优化整个列表的排序指标LambdaMART

实战中 LambdaMART 最常用——它直接优化 NDCG 等 listwise 指标,XGBoost 实现成熟,性能好。相比 Pointwise 的回归思路,它关注的是相对顺序而非绝对分数,更贴合排序本质。

排序特征工程

这是最耗时也最重要的部分,特征决定上限,模型只是逼近上限:

  • 文本特征:BM25 分数、查询词在标题/正文中的位置、查询与标题的语义相似度(sentence-transformers)
  • 内容质量特征:原创性评分、发布/更新时间、阅读量、点赞数、历史点击率
  • 用户特征:兴趣标签匹配度、历史点击相似度、搜索历史、地理匹配度

训练数据怎么来

  1. 人工标注:对搜索结果打 relevance 分(0-4),质量高但成本高、规模小
  2. 弱监督:从用户行为日志生成——点击率高、停留时间长、转化成功的记为高 relevance,成本低可规模化
import xgboost as xgb

def train_lambdamart(training_data):
    X_train, y_train, qids = [], [], []
    qid = 0
    for data in training_data:
        features = [extract_features(data['query'], doc) for doc in data['results']]
        X_train.extend(features)
        y_train.extend(data['relevance'])
        qids.extend([qid] * len(features))
        qid += 1

    dtrain = xgb.DMatrix(X_train, label=y_train)
    # 关键:同一 query 下的文档归为一组,模型在组内学习相对顺序
    dtrain.set_group([len(d['results']) for d in training_data])

    params = {
        'objective': 'rank:pairwise',   # Pairwise 损失
        'eval_metric': 'ndcg',
        'max_depth': 6,
        'eta': 0.1,
        'subsample': 0.8,
        'colsample_bytree': 0.8,
    }
    return xgb.train(params, dtrain, num_boost_round=100)

评估指标:NDCG

排序质量看 NDCG(Normalized Discounted Cumulative Gain),它衡量相关结果是否排在靠前位置——排在前面的相关结果贡献更大,位置越靠后折扣越多。下表是某项目从 BM25 升级到 LambdaMART 的离线对比:

指标BM25LambdaMART提升
NDCG@100.620.71+14.5%
MRR0.480.55+14.6%
Precision@50.560.63+12.5%

三项指标均衡提升约 12-15%,为后续线上 A/B 测试提供可信的离线信号。线上再看点击率、停留时长、转化率、无结果率等业务指标。

学习排序的坑

  • 坑一:特征爆炸。一口气加 200 个特征,训练从 1 小时变 6 小时,推理延迟从 50ms 飙到 200ms。解决:用 SHAP 值分析特征重要性,剔除冗余特征,控制维度。
  • 坑二:训练数据偏差。只用点击数据,模型偏向"标题党"——点击高但停留短。解决:引入停留时间、跳出率等满意度指标,加入"标题夸张内容差"的对抗负样本。
  • 坑三:线上线下特征不一致。训练用"过去 7 天点击率",线上用实时点击率,效果骤降。解决:统一特征计算逻辑,做一致性监控,训练时模拟线上环境。
  • 坑四:新内容冷启动。没有点击数据就被排到后面,陷入恶性循环。解决:给新内容探索性曝光,用内容特征预测潜在质量,调整时间衰减因子。

8.7 推荐服务的缓存

import redis
import json
import hashlib

class CachedRecommendation:
    def __init__(self):
        self.redis = redis.StrictRedis(host='localhost', port=6379)
        self.base_service = RecommendationService()

    def recommend_for_user(self, user_id, top_k=10, ttl=3600):
        cache_key = hashlib.md5(f"rec:{user_id}:{top_k}".encode()).hexdigest()

        # 缓存命中
        cached = self.redis.get(cache_key)
        if cached:
            return json.loads(cached)

        # 缓存未命中
        result = self.base_service.recommend_for_user(user_id, top_k)
        self.redis.setex(cache_key, ttl, json.dumps(result))
        return result

8.8 推荐系统的坑

坑一:数据稀疏

99% 的矩阵值是 0,相似度计算效果差。

解决:

  • 过滤行为次数少的用户和商品
  • 矩阵分解降维
  • 行为加权

坑二:冷启动

  • 新用户:推荐热门商品,收集行为
  • 新商品:基于内容特征找相似
  • 混合策略:多种方法加权合并

坑三:实时性

用户刚浏览,希望立即反映。

解决:

  • 用户行为实时更新到缓存
  • 活跃用户用更短的缓存时间
  • 提供手动刷新接口

坑四:评估指标

评估类型指标
离线评估准确率、召回率、NDCG
在线评估A/B 测试,点击率、转化率
业务指标GMV、用户留存

九、效果对比

9.1 NLP 项目效果

某用户反馈分析项目:

  • 准确率:85%
  • 日处理量:几千条评论
  • 重训频率:每周一次

9.2 分类项目效果

用户行为分类(活跃/普通/流失):

  • 整体准确率:87%
  • 流失用户 F1:0.82(最难识别)
  • 模型:RandomForest

9.3 推荐系统效果

某电商推荐:

  • 推荐响应时间:150ms(满足 < 200ms)
  • 缓存命中率:85%
  • 推荐点击率:提升 15%
  • 转化率:提升 8%
  • 用户停留时长:增加 12%

十、踩坑总结

NLP 相关

坑一:数据质量

没做清洗就训练,模型把重复评论当独立样本。解决:去重 + 质量检查。

坑二:模型漂移

部署初期效果好,过段时间准确率下降。解决:定期监控 + 重训机制。

坑三:多任务学习冲突

分类、情感分析、实体抽取互相影响。解决:模块化设计,任务独立。

分类相关

坑一:标签定义不明确

“活跃"和"普通"的边界模糊。解决:业务明确划分标准。

坑二:特征尺度差异大

avg_session_duration 几百,feature_usage_count 个位数。解决:标准化。

推荐系统相关

坑一:学术算法难落地

论文里的深度学习模型在真实场景难落地。解决:简单实用优先。

坑二:数据分散

浏览、收藏、购买记录在不同表。解决:统一数据管道。

十一、技术选型建议

11.1 NLP 任务选型

任务数据量推荐方案
文本分类小(<1万)TF-IDF + LR/SVM
文本分类中(1-10万)TF-IDF + RandomForest
文本分类大(>10万)预训练模型微调
情感分析任意SnowNLP(轻量)或 BERT(精准)
NER任意预训练 NER 模型
关系抽取任意规则 + 模型混合

11.2 推荐系统选型

场景推荐方案
用户量大、商品少User-based CF
商品多、用户少Item-based CF
冷启动多Content-based + CF 混合
数据稀疏矩阵分解
追求极致深度学习(Wide & Deep、DIN)

11.3 知识图谱选型

场景推荐
关系复杂、查询灵活Neo4j
学术研究RDF/OWL
已有 RAG 系统知识图谱增强 RAG

十二、写在最后

经典 AI 任务没有过时。简单方案往往够用——不是每个项目都要上最新最牛的模型,TF-IDF + 传统机器学习在很多场景下能达到 80% 的效果,而且部署简单。

几条核心原则:

  1. 数据质量远比模型重要:垃圾数据再花哨的模型也救不了
  2. 特征工程是核心:好的特征让简单模型也有不错表现
  3. 评估指标要贴合业务:不是所有场景都适合用准确率
  4. NLP 工程化比模型本身更重要
  5. 推荐系统的核心不是算法复杂度,而是理解业务
  6. 简单、实用、可解释的方法,往往比复杂的深度学习模型更产生价值

所有机器学习问题,最后都会变成数据问题。 这句话一点都不假。

对于中小型项目,简单有效的协同过滤 + 内容过滤的混合策略,可能比复杂的深度学习模型更合适。NLP Pipeline 说白了就是把人类理解文本的过程工程化——永远不会完美,但持续优化下去,总能达到可用状态。


本文整合了 5 篇 NLP 与推荐系统相关文章,涵盖文本预处理、特征提取、文本分类、情感分析、命名实体识别、知识图谱构建、协同过滤推荐、学习排序等核心技术。

版权声明: 本文首发于 指尖魔法屋-AI自然语言处理与推荐系统实战指南https://blog.thinkmoon.cn/post/ai-nlp-recommendation-comprehensive-guide/) 转载或引用必须申明原指尖魔法屋来源及源地址!