目录

AI 项目实践(第 3 节深度讲解)

3. AI 项目的开发与部署:完整教学教程

本教程将带你完成一个完整的 AI 项目开发与部署流程。我们将一步步带你从数据获取、数据清洗、模型训练、评估,到最终部署与集成,帮助你掌握如何从零开始构建 AI 项目。通过这些步骤,你将了解如何获取并清理数据,如何训练并评估模型,如何将模型部署为 Web 服务,以及如何通过容器化和 CI/CD 流程进行管理和自动化部署。

环境搭建

首先,确保你的开发环境中已安装必要的 Python 库。这些库将帮助你进行数据处理、机器学习模型训练、模型部署等任务。你可以使用 pip 安装这些库:

pip install pandas numpy scikit-learn flask pickle-mixin requests

3.1 数据获取与清洗

✔ 从 Kaggle 获取数据集

Kaggle 是 Google 旗下面向数据科学与机器学习的社区平台,提供竞赛、公开数据集、在线 Notebook 与讨论区等功能,可快速完成实验与交流。

常用优势:

  • 完整的数据描述:绝大多数数据集都附带字段说明、示例 Notebook 与许可证信息,能帮助理解业务背景。
  • 可复现的 Notebook:直接 Fork 官方或社区 Notebook,快速对比不同特征工程与模型方案。
  • API 自动化kaggle CLI 支持脚本化地批量下载、同步数据,便于放入 CI/CD 或日常流水线。

了解这些特性后,就能理解为何 Kaggle 是获取结构化数据与复现基准实验的首选入口。

以 Titanic 数据集为例,进行数据清洗与建模。该数据集源自 1912 年泰坦尼克号沉船事件,共包含 891 名乘客的基本信息(例如 NameSexAgePclassSibSpParchFareEmbarked 等)以及标签 Survived。目标是根据这些字段预测乘客是否在事故中幸存:1 代表幸存,0 代表遇难。因为字段清晰、标签明确,而且数据量适中,因此常被用于初学者的分类练习。

  1. 访问 Kaggle Titanic 数据集页面
  2. 注册并下载数据集(train.csvtest.csv

然后在 Python 中加载数据:

import pandas as pd

# 加载 Titanic 数据集
df = pd.read_csv('train.csv')
print(df.head())

💡 pd.read_csv() 默认会从当前执行脚本的目录查找文件。如果你把 train.csv 放在其他位置,可传入绝对路径:

  • macOS / Linux:路径使用正斜杠,例如 df = pd.read_csv('/Users/yourname/Downloads/titanic/train.csv')
  • Windows:路径使用反斜杠或原始字符串,例如 df = pd.read_csv(r'C:\Users\yourname\Downloads\titanic\train.csv')

建议把数据文件放在项目根目录的 data/datasets/ 文件夹,并用相对路径(如 pd.read_csv('data/train.csv')),这样无论在 macOS 还是 Windows 上都能保持一致。

ℹ️ df.head() 会返回数据框的前 5 行(默认值),通过 print(df.head()) 就能快速预览各列字段、数据类型以及是否有明显的缺失或异常值,是检查数据加载是否成功的第一步。

⚠️ 以上清洗操作只会修改内存中的 DataFrame,不会影响磁盘上的原始 train.csv。只有显式调用 df.to_csv() 等写入函数时,才会生成新的文件或覆盖旧文件。

✔ 数据清洗

数据清洗是机器学习项目中的关键步骤,清洗数据的目的是确保数据的质量,避免模型训练时受到噪声和不一致数据的影响。数据清洗常见的步骤包括:

  • 去除缺失值:删除或填补缺失的数据行。
  • 去除重复数据:删除完全相同的数据行。
  • 填补缺失数据:可以使用均值、中位数、众数等填补缺失数据。

数据清洗代码示例:

上面提到的每一步都在解决特定的数据质量问题,下面逐一拆解:

  • 缺失值统计
    机器学习模型通常要求输入表格是“整齐的”,也就是每条记录每个特征都要有值。df.isnull().sum() 会遍历所有列并统计缺失(NaN)数量,输出结果类似 Age 177,提醒我们 Age 列缺失 177 个值。只有掌握总体缺失情况,才能决定某列是应该删除还是填补。

  • 均值填充 Age
    Age 是连续数值特征,若直接删除存在缺失的行,很容易减少大量样本。用 df['Age'].mean() 的结果填充,可以保持 Age 的整体分布,弥补缺失带来的数据不足。相比填 0 或随机数,均值填补更贴近真实乘客的年龄范围,也更容易被模型学习。

  • 众数填充 Embarked
    Embarked 表示乘客在何处登船,是一个分类变量,常见取值为 C、Q、S。对于这类离散特征,均值没有意义,所以我们使用最常出现的值(众数)填补:df['Embarked'].mode()[0]。众数就是“出现次数最多的类别”,把缺失的记录替换为当前最常见的港口,能最大程度维持原有分布。如果缺失占比较小,这类填充对整体比例的影响甚微;若缺失率较高,就需要考虑按 Pclass 等条件分组取众数,或用建模方式预测缺失值,以免某一类别过度膨胀。

  • 删除 Cabin 列
    Cabin(客舱号)缺失率极高,保留它既无法直接用于建模,还可能引入噪声。在这种情况下,与其强行填补,不如用 df.drop(columns=['Cabin']) 将整列删除,把注意力放在质量更高的特征上。

  • 删除重复数据
    数据源难免会重复抓取同一位乘客的记录,df.drop_duplicates() 可以把完全相同的行去掉。若不处理,模型在训练时会多次“看到”同一条样本,从而导致偏置或过拟合。

# 查看缺失值
print(df.isnull().sum())

# 填补缺失的 Age 数据,用均值填充
df['Age'] = df['Age'].fillna(df['Age'].mean())

# 填补缺失的 Embarked 数据,用众数填充
df['Embarked'] = df['Embarked'].fillna(df['Embarked'].mode()[0])

# 删除 Cabin 列,因为缺失值过多
df = df.drop(columns=['Cabin'])

# 删除重复数据
df = df.drop_duplicates()

ℹ️ 后续的特征工程与模型训练默认在同一个脚本或 Notebook 中继续执行,因此无需“复制”前面的清洗代码,只要保持顺序运行即可。如果你想把清洗和训练分开管理,可以在此处用 df.to_csv('data/train_clean.csv', index=False) 保存清洗结果,然后在新的脚本里读取这个干净文件。

⚠️ 如果你在 Notebook 中多次反复运行 get_dummies 这类会修改列名的操作,可能会出现 KeyError: 'Embarked'(即列已经被转换或删除)的错误。遇到这种情况,重新执行“读取数据”那一节以获得原始列,再继续清洗即可;也可以在操作前通过 print(df.columns) 来确认列名是否还存在。

扩展练习:

  1. 改用不同的填充方法:尝试使用不同的策略填补缺失值(例如,中位数或众数)。
  2. 查看数据分布:使用 Seaborn 库对数据进行可视化,观察数据是否存在明显的偏差。
import seaborn as sns
sns.histplot(df['Age'])

3.2 模型训练与评估

⚠️ 在进入建模阶段前,需要把 SexEmbarked 等类别型字符串字段转换成模型能理解的数字。最常见的做法是用 pandas.get_dummies() 进行独热编码(One-Hot Encoding),将每个类别拆成单独的 0/1 列。否则会出现 “could not convert string to float: ‘male’” 这类错误,因为大多数 sklearn 模型(包括随机森林)只能处理数值输入。

# 通过独热编码把分类字段转换为数字列
df = pd.get_dummies(
    df,
    columns=['Sex', 'Embarked'],  # 指定要转换的列
    drop_first=True               # 丢弃一个基准列,避免完全共线
)

经过独热编码后,原来的 Sex 列会变成 Sex_male(1 表示男,0 表示女),Embarked 则会拆成 Embarked_QEmbarked_S 两列(是否在对应港口登船)。这些都是后续建模时要用到的数值特征。

✔ 选择合适的模型进行训练

在本 Demo 中,我们将使用 随机森林 作为分类器进行建模,并将其用于预测是否幸存。使用 Scikit-learn 中的 RandomForestClassifier 进行训练。

📘 随机森林(Random Forest)是一种“装袋(Bagging)”思想的集成学习模型:

  1. 多棵树并行训练:算法先对原始数据做有放回的随机采样(bootstrap),得到多个训练子集,并为每棵树随机挑选部分特征,这样每棵树都“不一样”。“有放回”指的是每次抽样后会把样本放回原集合,再进行下一次抽取,所以同一个样本可能被同一棵树重复抽中,也可能完全没被抽到,这正是让每棵树拥有“不同视角”的关键。
  2. 结果投票或平均:分类任务中,每棵树给出一个类别预测,随机森林会对所有树的结果做投票(取最多票的类别);回归任务则对所有树的预测取平均。
  3. 优点:天然支持非线性关系、抗过拟合(因为树之间差异大)、几乎无需特征缩放,还能通过特征重要性衡量哪些字段更关键。常用超参数包括 n_estimators(树的数量)、max_depth(树深度)、max_features(每次挑选的特征数)等。
    对于初学者来说,只需设置树的数量和随机种子,就能得到一个效果稳定、易于解释的基线模型。你可以把它想象成“让多位不同专长的专家分别独立判断,再通过投票得到最终结论”:
  • 单棵决策树:类似问答流程——先问“乘客是男是女?”,再根据回答继续问“年龄多少?”,直到给出“幸存/遇难”的预测。但单棵树很容易被某些特征噪声左右。
  • 多棵树组成森林:随机森林会让每棵树看到不同的数据和特征,降低“所有树都被同一个噪声影响”的概率。某一棵树即使判断偏了,也会被其他树纠正。
  • 可解释性:训练完成后,可以查看 model.feature_importances_,了解模型认为“票价”“性别”“客舱等级”等特征的重要程度,方便分析业务洞察。

划分数据集:

from sklearn.model_selection import train_test_split  # 切分训练/测试集的工具

# 1. 选择模型输入特征:客舱等级、年龄、亲友数量、票价及编码后的性别/登船港口
feature_cols = [
    'Pclass', 'Age', 'SibSp', 'Parch', 'Fare',
    'Sex_male',          # One-Hot 编码后得到的列
    'Embarked_Q', 'Embarked_S'
]
X = df[feature_cols]
# 2. 选择预测目标:Survived(1 为幸存,0 为遇难)
y = df['Survived']

# 3. 数据集划分:70% 训练集,30% 测试集;random_state 固定随机数便于复现
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)

训练模型:

from sklearn.ensemble import RandomForestClassifier  # 随机森林分类器

# 创建并训练模型:
# - n_estimators=100 表示训练 100 棵树,数量越多越稳定但训练更慢
# - random_state 控制随机性,确保每次运行结果一致
model = RandomForestClassifier(n_estimators=100, random_state=42)
model.fit(X_train, y_train)  # 用训练集拟合模型

ℹ️ 这里虽然只调用了一次 model.fit(),但由于我们把 n_estimators 设为 100,随机森林内部会自动训练 100 棵不同的决策树。可以把这看成“一个模型里包含了 100 个子模型”:RandomForestClassifier 负责统筹管理,后台帮你创建、训练每一棵树,并在预测时收集它们的投票结果。

模型评估:

在模型训练完成后,我们使用 AUCF1 分数 来评估模型性能。

from sklearn.metrics import f1_score, roc_auc_score  # 常见分类评估指标

# 1. 得到测试集预测标签(0/1),用于计算 F1
y_pred = model.predict(X_test)

# 2. 计算 F1:同时考虑查准率和查全率,数值越接近 1 表示越平衡
f1 = f1_score(y_test, y_pred)
print(f'F1 Score: {f1}')

# 3. 计算 AUC:基于预测概率衡量分类阈值在 0~1 之间变化时的整体表现
y_pred_proba = model.predict_proba(X_test)[:, 1]  # 取“幸存”的概率
auc = roc_auc_score(y_test, y_pred_proba)
print(f'AUC: {auc}')
  • F1 分数:是精准率(Precision)和召回率(Recall)的调和平均,适合样本不平衡的二分类场景。F1 越接近 1,说明模型既能尽量找对正样本,又不会误判太多负样本。
  • AUC(Area Under the ROC Curve):以不同阈值下的 TPR/FPR 关系为基础,衡量模型整体区分正负样本的能力。AUC=0.5 表示完全随机,接近 1 则说明模型越优秀。
    这段代码先获得预测标签 y_pred,再计算 F1;同时通过 predict_proba 获取“幸存”概率,以此计算 AUC,最后把两个指标打印出来便于比较不同模型或超参数。

扩展练习:

  1. 尝试其他分类模型:除了随机森林,可以尝试 Logistic RegressionSVMXGBoost 等其他模型,并比较它们的表现。
  2. 交叉验证:使用 KFold 交叉验证来评估模型的稳定性和泛化能力。
from sklearn.model_selection import cross_val_score

cv_score = cross_val_score(model, X, y, cv=5)
print(f'Cross-validation score: {cv_score.mean()}')

3.3 模型部署与集成

✔ Python 服务化(使用 Flask)

将训练好的模型转化为 Web 服务是 AI 项目部署的重要步骤。我们将使用 Flask 来创建 Web 服务。

⚠️ 服务端必须复用训练阶段的特征工程。上文中我们对 SexEmbarked 做了 One-Hot 编码,生成 Sex_maleEmbarked_QEmbarked_S 三列,因此线上推理也要构造同样的 8 个特征,否则会出现 “X has 7 features, but RandomForestClassifier is expecting 8” 的错误。

  1. 保存训练好的模型
import pickle
pickle.dump(model, open('titanic_model.pkl', 'wb'))
  1. 创建 Flask Web 服务
from flask import Flask, request, jsonify
import pickle

# 加载模型
model = pickle.load(open('titanic_model.pkl', 'rb'))
FEATURE_COLS = ['Pclass', 'Age', 'SibSp', 'Parch', 'Fare', 'Sex_male', 'Embarked_Q', 'Embarked_S']

def build_feature_vector(payload):
    """将原始 JSON 输入转换为与训练阶段一致的 8 维特征"""
    sex_value = str(payload['Sex']).lower()
    embarked_value = str(payload['Embarked']).upper()

    row = {
        'Pclass': payload['Pclass'],
        'Age': payload['Age'],
        'SibSp': payload['SibSp'],
        'Parch': payload['Parch'],
        'Fare': payload['Fare'],
        'Sex_male': 1 if sex_value in ('male', 'm', '1') else 0,
        # 训练时 drop_first=True,把 C 作为基准列,因此这里只需要 Q/S 两列
        'Embarked_Q': 1 if embarked_value in ('Q', '2') else 0,
        'Embarked_S': 1 if embarked_value in ('S', '3') else 0,
    }
    return [[row[col] for col in FEATURE_COLS]]

app = Flask(__name__)

@app.route('/predict', methods=['POST'])
def predict():
    data = request.get_json()
    features = build_feature_vector(data)
    prediction = model.predict(features)
    return jsonify({'prediction': int(prediction[0])})

if __name__ == '__main__':
    app.run(debug=True)
  1. 通过浏览器或 Postman 测试 API
    发送 POST 请求到 http://localhost:5000/predict,并在请求体中传递数据,例如:
{
  "Pclass": 3,
  "Age": 22,
  "SibSp": 1,
  "Parch": 0,
  "Fare": 7.25,
  "Sex": "male",
  "Embarked": "S"
}

命令行可使用 curl 直接发起请求:

curl -X POST http://localhost:5000/predict \
  -H "Content-Type: application/json" \
  -d '{"Pclass":3,"Age":22,"SibSp":1,"Parch":0,"Fare":7.25,"Sex":"male","Embarked":"S"}'

✔ 使用 Docker 容器化部署

  1. Dockerfile
FROM python:3.8

WORKDIR /app

# 安装 Flask 和依赖
COPY requirements.txt .
RUN pip install -r requirements.txt

# 拷贝应用代码
COPY . .

CMD ["python", "app.py"]
  1. 构建 Docker 镜像并启动容器:
docker build -t titanic-model .
docker run -p 5000:5000 titanic-model

通过 Docker 部署,我们可以确保在不同环境中一致运行。

✔ 持续集成与持续部署(CI/CD)

使用 GitHub Actions 自动化模型训练、测试、部署流程:

name: CI/CD Pipeline

on:
  push:
    branches:
      - main

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v2

      - name: Set up Python
        uses: actions/setup-python@v2
        with:
          python-version: 3.8

      - name: Install dependencies
        run: |
          pip install -r requirements.txt

      - name: Run tests
        run: |
          pytest tests/

      - name: Deploy Model
        run: |
          ssh deploy@your-server "docker-compose pull && docker-compose up -d"

通过 CI/CD 工具,自动化模型的训练、测试和生产环境部署,确保 AI 项目的高效持续迭代。


🎯 本章总结

通过本章,你已经掌握了:

  • 数据获取与清洗:如何从 Kaggle 下载数据并进行清洗。
  • 模型训练与评估:如何训练、评估和优化模型。
  • 模型部署与集成:将模型封装为 Web 服务,进行容器化部署,使用 CI/CD 自动化过程。