Day 125 — 阶段项目:综合 AI 应用
🎯 今日目标:完成一个端到端的 AI/ML 项目,涵盖数据获取、预处理、模型训练、评估和部署的完整流程。
📋 概念总览
本日是一个综合实战项目,将前面学习的机器学习、深度学习、模型部署等知识串联起来,构建一个完整的 AI 应用。
项目主题:智能文本情感分析系统
技术栈:
- 数据处理:pandas, numpy
- 特征工程:scikit-learn(TF-IDF)
- 模型训练:scikit-learn(逻辑回归/SVM)+ PyTorch(深度学习版)
- 模型评估:sklearn.metrics
- 模型部署:FastAPI + joblib
- 可视化:matplotlib
🏗️ 项目架构
┌─────────────────────────────────────────────────────┐
│ 综合 AI 应用架构 │
├─────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 数据获取 │───▶│ 数据清洗 │───▶│ 特征工程 │ │
│ │ & 加载 │ │ & 预处理 │ │ TF-IDF │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Web API │◀───│ 模型保存 │◀───│ 模型训练 │ │
│ │ FastAPI │ │ joblib │ │ 评估调优 │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
└─────────────────────────────────────────────────────┘
🔄 端到端流程详解
1. 数据获取与加载
python
import pandas as pd
import numpy as np
from pathlib import Path
# 方式一:使用内置数据集模拟
def create_sample_data(n_samples=2000):
"""生成模拟情感分析数据集"""
positive_texts = [
"这部电影太棒了,演员演技出色",
"服务非常好,物超所值",
"产品质量很好,物流也快",
"朋友推荐的,果然没有让我失望",
"价格合理,性价比很高",
"包装精美,产品完好无损",
"客服态度好,回复及时",
"非常满意,下次还会回购",
"做工精细,手感很好",
"安装简单,使用方便",
]
negative_texts = [
"质量太差了,用了一天就坏了",
"客服态度差,完全不解决问题",
"物流太慢了,等了两周才到",
"和描述不符,非常失望",
"价格虚高,不值这个价",
"包装破损,产品有划痕",
"退货流程复杂,折腾了好久",
"噪音很大,影响使用体验",
"做工粗糙,细节处理不到位",
"说明书不清楚,安装困难",
]
texts, labels = [], []
for _ in range(n_samples // 2):
texts.append(np.random.choice(positive_texts))
labels.append(1)
texts.append(np.random.choice(negative_texts))
labels.append(0)
df = pd.DataFrame({"text": texts, "label": labels})
return df.sample(frac=1, random_state=42).reset_index(drop=True)
# 方式二:从 CSV 加载真实数据
def load_csv_data(filepath):
"""从 CSV 文件加载数据"""
df = pd.read_csv(filepath)
return df
# 使用
df = create_sample_data(2000)
print(f"数据集大小: {len(df)}")
print(f"类别分布:\n{df['label'].value_counts()}")
2. 数据预处理
python
import re
def clean_text(text):
"""文本清洗:去除噪声、统一格式"""
# 去除HTML标签
text = re.sub(r'<[^>]+>', '', text)
# 去除URL
text = re.sub(r'http\S+|www.\S+', '', text)
# 去除多余空白
text = re.sub(r'\s+', ' ', text).strip()
return text
def preprocess_dataframe(df):
"""DataFrame 预处理"""
# 去除重复文本
df = df.drop_duplicates(subset='text')
# 文本清洗
df['text'] = df['text'].apply(clean_text)
# 去除过短文本
df = df[df['text'].str.len() > 2]
return df.reset_index(drop=True)
df = preprocess_dataframe(df)
print(f"清洗后数据集: {len(df)} 条")
3. 特征工程 — TF-IDF
python
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.model_selection import train_test_split
# 分割数据集
X_train, X_test, y_train, y_test = train_test_split(
df['text'], df['label'],
test_size=0.2,
random_state=42,
stratify=df['label'] # 保持类别比例
)
# TF-IDF 向量化
vectorizer = TfidfVectorizer(
max_features=5000, # 最大特征数
ngram_range=(1, 2), # 使用 unigram + bigram
min_df=2, # 最小文档频率
max_df=0.95, # 最大文档频率
sublinear_tf=True # 使用 1 + log(tf)
)
X_train_tfidf = vectorizer.fit_transform(X_train)
X_test_tfidf = vectorizer.transform(X_test)
print(f"训练集特征矩阵: {X_train_tfidf.shape}")
print(f"测试集特征矩阵: {X_test_tfidf.shape}")
4. 模型训练与评估
python
from sklearn.linear_model import LogisticRegression
from sklearn.svm import LinearSVC
from sklearn.metrics import (
accuracy_score, precision_score, recall_score,
f1_score, classification_report, confusion_matrix
)
import time
def train_and_evaluate(model, X_train, X_test, y_train, y_test, model_name):
"""统一的训练与评估流程"""
print(f"\n{'='*50}")
print(f"训练模型: {model_name}")
print(f"{'='*50}")
# 训练
start_time = time.time()
model.fit(X_train, y_train)
train_time = time.time() - start_time
# 预测
start_time = time.time()
y_pred = model.predict(X_test)
predict_time = time.time() - start_time
# 评估指标
accuracy = accuracy_score(y_test, y_pred)
precision = precision_score(y_test, y_pred)
recall = recall_score(y_test, y_pred)
f1 = f1_score(y_test, y_pred)
print(f"准确率: {accuracy:.4f}")
print(f"精确率: {precision:.4f}")
print(f"召回率: {recall:.4f}")
print(f"F1 分数: {f1:.4f}")
print(f"训练时间: {train_time:.3f}s")
print(f"预测时间: {predict_time:.3f}s")
print(f"\n分类报告:\n{classification_report(y_test, y_pred)}")
return {
"model": model,
"accuracy": accuracy,
"f1": f1,
"train_time": train_time,
}
# 模型 1: 逻辑回归
lr_model = LogisticRegression(max_iter=1000, C=1.0, random_state=42)
lr_results = train_and_evaluate(
lr_model, X_train_tfidf, X_test_tfidf,
y_train, y_test, "逻辑回归"
)
# 模型 2: 支持向量机
svm_model = LinearSVC(C=1.0, max_iter=1000, random_state=42)
svm_results = train_and_evaluate(
svm_model, X_train_tfidf, X_test_tfidf,
y_train, y_test, "支持向量机 (SVM)"
)
5. 深度学习版(PyTorch)
python
import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader
class TextDataset(Dataset):
"""文本数据集"""
def __init__(self, features, labels):
self.features = torch.FloatTensor(features.toarray())
self.labels = torch.LongTensor(labels.values)
def __len__(self):
return len(self.labels)
def __getitem__(self, idx):
return self.features[idx], self.labels[idx]
class SentimentClassifier(nn.Module):
"""情感分类神经网络"""
def __init__(self, input_dim, hidden_dim=128, output_dim=2):
super().__init__()
self.network = nn.Sequential(
nn.Linear(input_dim, hidden_dim),
nn.ReLU(),
nn.Dropout(0.3),
nn.Linear(hidden_dim, hidden_dim // 2),
nn.ReLU(),
nn.Dropout(0.2),
nn.Linear(hidden_dim // 2, output_dim)
)
def forward(self, x):
return self.network(x)
def train_dl_model(model, train_loader, criterion, optimizer, epochs=20):
"""训练深度学习模型"""
model.train()
for epoch in range(epochs):
total_loss = 0
for batch_features, batch_labels in train_loader:
optimizer.zero_grad()
outputs = model(batch_features)
loss = criterion(outputs, batch_labels)
loss.backward()
optimizer.step()
total_loss += loss.item()
if (epoch + 1) % 5 == 0:
avg_loss = total_loss / len(train_loader)
print(f"Epoch [{epoch+1}/{epochs}], Loss: {avg_loss:.4f}")
return model
# 准备数据
train_dataset = TextDataset(X_train_tfidf, y_train)
test_dataset = TextDataset(X_test_tfidf, y_test)
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=32)
# 训练
input_dim = X_train_tfidf.shape[1]
dl_model = SentimentClassifier(input_dim)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(dl_model.parameters(), lr=0.001)
dl_model = train_dl_model(dl_model, train_loader, criterion, optimizer, epochs=20)
6. 模型保存与加载
python
import joblib
from pathlib import Path
def save_model_artifacts(model, vectorizer, metrics, save_dir="model_artifacts"):
"""保存模型产物"""
save_path = Path(save_dir)
save_path.mkdir(exist_ok=True)
# 保存 sklearn 模型
joblib.dump(model, save_path / "model.joblib")
joblib.dump(vectorizer, save_path / "vectorizer.joblib")
# 保存评估指标
import json
with open(save_path / "metrics.json", "w") as f:
json.dump(metrics, f, indent=2)
print(f"✅ 模型已保存到 {save_dir}/")
return save_path
def load_model_artifacts(save_dir="model_artifacts"):
"""加载模型产物"""
save_path = Path(save_dir)
model = joblib.load(save_path / "model.joblib")
vectorizer = joblib.load(save_path / "vectorizer.joblib")
import json
with open(save_path / "metrics.json") as f:
metrics = json.load(f)
return model, vectorizer, metrics
# 保存最佳模型(比较逻辑回归和SVM)
best_model = lr_results if lr_results["f1"] >= svm_results["f1"] else svm_results
save_model_artifacts(
best_model["model"],
vectorizer,
{"accuracy": best_model["accuracy"], "f1": best_model["f1"]}
)
7. FastAPI 部署
python
# save as: app.py
from fastapi import FastAPI
from pydantic import BaseModel
import joblib
import numpy as np
app = FastAPI(title="情感分析 API", version="1.0")
# 加载模型
model = joblib.load("model_artifacts/model.joblib")
vectorizer = joblib.load("model_artifacts/vectorizer.joblib")
class TextInput(BaseModel):
text: str
class PredictionOutput(BaseModel):
text: str
sentiment: str
confidence: float
@app.post("/predict", response_model=PredictionOutput)
def predict_sentiment(input_data: TextInput):
"""预测文本情感"""
# 向量化
features = vectorizer.transform([input_data.text])
# 预测
prediction = model.predict(features)[0]
# 获取概率(如果有 predict_proba)
if hasattr(model, "predict_proba"):
proba = model.predict_proba(features)[0]
confidence = float(max(proba))
else:
confidence = 0.95 # SVM 默认置信度
sentiment = "正面" if prediction == 1 else "负面"
return PredictionOutput(
text=input_data.text,
sentiment=sentiment,
confidence=round(confidence, 4)
)
@app.get("/")
def root():
return {"message": "情感分析 API v1.0", "status": "running"}
# 启动命令: uvicorn app:app --reload --port 8000
📊 模型对比分析
| 指标 | 逻辑回归 | SVM | 神经网络 |
|---|---|---|---|
| 训练速度 | ⚡ 快 | ⚡ 快 | 🐢 慢 |
| 推理速度 | ⚡ 快 | ⚡ 快 | ⚡ 快 |
| 内存占用 | 低 | 低 | 中 |
| 可解释性 | ⭐⭐⭐ 高 | ⭐⭐ 低 | ⭐ 很低 |
| 小数据表现 | 好 | 很好 | 差 |
| 调参难度 | 简单 | 中等 | 复杂 |
选择建议:
- 数据量小(<10K)→ 逻辑回归/SVM
- 数据量大(>100K)→ 深度学习
- 需要可解释性 → 逻辑回归
- 追求极致性能 → 深度学习 + 微调
💡 项目最佳实践
1. 代码组织
project/
├── data/
│ ├── raw/ # 原始数据(不修改)
│ └── processed/ # 处理后的数据
├── models/
│ ├── trained/ # 训练好的模型
│ └── logs/ # 训练日志
├── src/
│ ├── data.py # 数据处理
│ ├── features.py # 特征工程
│ ├── train.py # 模型训练
│ ├── evaluate.py # 模型评估
│ └── predict.py # 推理接口
├── tests/ # 测试
├── app.py # API 服务
├── config.yaml # 配置文件
└── requirements.txt # 依赖
2. 配置管理
python
from dataclasses import dataclass, field
@dataclass
class Config:
# 数据
raw_data_path: str = "data/raw/"
processed_data_path: str = "data/processed/"
# 特征
max_features: int = 5000
ngram_range: tuple = (1, 2)
# 模型
model_type: str = "logistic_regression" # or "svm", "nn"
learning_rate: float = 0.001
epochs: int = 20
batch_size: int = 32
# 部署
api_host: str = "0.0.0.0"
api_port: int = 8000
3. 实验记录
python
import json
from datetime import datetime
def log_experiment(experiment_name, params, metrics):
"""记录实验结果"""
log = {
"name": experiment_name,
"timestamp": datetime.now().isoformat(),
"params": params,
"metrics": metrics,
}
log_file = "experiments.json"
try:
with open(log_file) as f:
experiments = json.load(f)
except FileNotFoundError:
experiments = []
experiments.append(log)
with open(log_file, "w") as f:
json.dump(experiments, f, indent=2, ensure_ascii=False)
print(f"📝 实验 '{experiment_name}' 已记录")
🤔 思考题
-
数据质量:如果训练数据中混入了 10% 的错误标签,模型性能会如何变化?有哪些方法可以检测和处理标签噪声?
-
特征选择:TF-IDF 的
max_features参数如何影响模型?当词汇表从 5000 增加到 50000 时,模型性能和训练时间会如何变化? -
过拟合:如果模型在训练集上准确率达到 99% 但测试集只有 70%,说明什么问题?有哪些正则化方法可以缓解?
-
部署优化:生产环境中,如何处理高并发请求?如何实现模型的 A/B 测试和灰度发布?
-
持续学习:当线上数据分布发生变化(概念漂移)时,如何自动检测并更新模型?