首页
/ ML-For-Beginners 实践指南:用 Pickle 序列化 Scikit-learn 模型并搭建 Flask UFO 预测 Web 应用

ML-For-Beginners 实践指南:用 Pickle 序列化 Scikit-learn 模型并搭建 Flask UFO 预测 Web 应用

2026-09-06 15:26:35作者:卓艾滢Kingsley

本文基于 ML-For-Beginners 课程第三周“Web-App”模块的阿拉伯语文档 translations/ar/3-Web-App/README.md 及其关联课时 translations/ar/3-Web-App/1-Web-App/README.md 展开,讲解一个完整的“模型落地”实战链路:在 notebook 中用 NUFORC 的 UFO 目击数据训练 Scikit-learn 分类模型,用 Pickle 将模型序列化为 .pkl 文件,再封装进一个 Flask 应用,让用户通过网页表单输入“观测秒数 + 纬度 + Longitude 经度”,即可预测该次 UFO 目击报告可能来自哪个国家。读完本文,你可以独立复现从数据清洗、模型训练、模型序列化到 Web 服务部署的完整流程,并理解训练侧与推理侧之间必须遵守的“输入数据契约”。

一、课程定位:把 notebook 里的模型送进“真实世界”

该模块是 ML-For-Beginners(12 周、26 课、52 测验)课程体系中的第三周应用实践环节,对应课程文档 3-Web-App/README.md。它解决的核心问题是:如何把在 Jupyter notebook 里训练好的 Scikit-learn 模型,保存为一个可供 Web 应用加载并做预测的文件

根据模块文档的概述,本节的完整工作流是:

  1. 用 UFO 目击数据训练一个 Scikit-learn 模型(输入 SecondsLatitudeLongitude,输出国家编码);
  2. 将模型 Pickle 化为 ufo-model.pkl 文件;
  3. 构建一个 Flask Web 应用,加载该 .pkl 文件并提供预测表单;
  4. 用户在浏览器中输入三个数值,页面返回“Likely country: XX”。

模块包含 1 个正式课时与 1 个作业,文档入口为:

模块文档同时注明了数据来源与致谢:数据集来自 NUFORC(National UFO Reporting Center)在 Kaggle 上发布的 UFO sightings 数据,Web 应用架构参考了业界常见的 Flask 部署 ML 模型方案。

二、架构考量:消费 ML 模型不止一种方式

在动手之前,模块文档先抛出了一个工程视角的问题:你的 Web 架构可能会反过来影响模型训练的方式。文档以“数据科学团队训练好一个模型,要求你在应用中消费它”为场景,列出必须回答的四个问题:

  • 是 Web 应用还是移动端应用? 若是移动或 IoT 场景,可考虑 TensorFlow Lite,把模型用于 Android / iOS 应用;
  • 模型驻留在哪里? 云端还是本地?
  • 是否需要离线支持? 应用必须离线工作吗?
  • 模型是用什么技术栈训练的? 训练框架决定了导出的工具链:
    • TensorFlow 训练:可用 TensorFlow.js 把模型转成 Web 端可用格式;
    • PyTorch 训练:可导出为 ONNX(Open Neural Network Exchange)格式,供 JavaScript Web 应用通过 ONNX Runtime 推理(课程在后续课时会对 Scikit-learn 模型探索该选项);
    • Lobe.ai / Azure Custom Vision 等 ML SaaS:这类平台本身提供模型导出能力,包括构建一个自定义 API,供云端应用远程查询。

此外,还可以直接用一个 Flask 应用承载整个训练过程(甚至用 TensorFlow.js 在浏览器中训练)。

本课程的选型:既然前几周一直用 Python notebook,本模块就走最贴近现有工作流的一条路——从 Python notebook 导出训练好的模型,得到 Python Web 应用可读的格式(即 Pickle),这也是本文的实战主线。

三、工具链:Flask 与 Pickle

文档明确指出本任务只需要两个 Python 生态的工具:

3.1 Flask:微框架

Flask 被其作者称为“micro-framework”,用 Python 提供 Web 框架的基础能力,并内置模板引擎(Jinja2)用于动态渲染网页。在本项目中 Flask 承担三个职责:

  • 定义路由(//predict);
  • 通过 render_template 渲染 templates/ 下的 HTML 并注入预测结果;
  • 托管 static/ 下的静态资源(CSS)。

3.2 Pickle:对象序列化

Pickle 是 Python 标准库中的模块,负责将 Python 对象结构序列化(serialize)与反序列化(de-serialize)。当你“pickle”一个模型时,就是把它整个对象结构(包括学到的权重与元数据)压平写入磁盘,得到一个以 .pkl 为后缀的文件,供 Web 应用加载。

文档特别强调了一条安全警示:Pickle 本质上不安全。反序列化(un-pickle)一个不受信任的 .pkl 文件等同于执行任意代码,因此只应加载自己训练并生成的模型文件,绝不要加载来源不明的 pickle 文件。

四、数据准备:NUFORC 的 UFO 目击数据集

4.1 数据集结构

本课时使用 3-Web-App/1-Web-App/data/ufos.csv 数据集,来自 NUFORC 收集的约 80,000 条 UFO 目击记录(仓库中该 CSV 文件共 8 万余行)。文档特意引用了两条颇具画面感的原始描述:

  • 长描述示例:“A man emerges from a beam of light that shines on a grassy field at night and he runs towards the Texas Instruments parking lot”(一个男人从照在草地上的光柱中走出,跑向德州仪器的停车场);
  • 短描述示例:“the lights chased us”(灯光在追着我们)。

实际检查该 CSV 文件,其完整列包括:datetimecitystatecountryshapeduration (seconds)duration (hours/min)commentsdate postedlatitudelongitude。本课只保留训练需要的四列:时长(秒)、国家、纬度、经度。

4.2 在 notebook 中清洗数据

在课时提供的空白练习 notebook 3-Web-App/1-Web-App/notebook.ipynb 中,文档给出四步操作(完整可运行的参考实现见 3-Web-App/1-Web-App/solution/notebook.ipynb):

第 1 步:导入库并读入数据

import pandas as pd
import numpy as np

ufos = pd.read_csv('./data/ufos.csv')
ufos.head()

第 2 步:构建精简后的 DataFrame 并重命名列,同时检查 Country 字段有哪些取值:

ufos = pd.DataFrame({'Seconds': ufos['duration (seconds)'], 'Country': ufos['country'],'Latitude': ufos['latitude'],'Longitude': ufos['longitude']})

ufos.Country.unique()

第 3 步:丢弃空值,并只保留持续 1–60 秒的目击记录,减少要处理的数据量:

ufos.dropna(inplace=True)

ufos = ufos[(ufos['Seconds'] >= 1) & (ufos['Seconds'] <= 60)]

ufos.info()

第 4 步:用 Scikit-learn 的 LabelEncoder 把国家名转成整数编码。文档特别提示:LabelEncoder 按字母序(alphabetically)编码,因此取值集合恰好是 Australia, Canada, Germany, UK, US 对应 0~4

from sklearn.preprocessing import LabelEncoder

ufos['Country'] = LabelEncoder().fit_transform(ufos['Country'])

ufos.head()

清洗完成后数据应形如:

	Seconds	Country	Latitude	Longitude
2	20.0	3		53.200000	-2.916667
3	20.0	4		28.978333	-96.645833
14	30.0	4		35.823889	-80.253611
23	60.0	4		45.582778	-122.352222
24	3.0		3		51.783333	-0.783333

这个编码顺序正是后面 Flask 应用里 countries 列表(索引 0=Australia … 4=US)必须与之一致的原因——它是贯穿训练与推理两端的隐藏契约。

五、构建模型:Logistic Regression 预测“国家”

5.1 划分训练集与测试集

选取三个特征构成 X 向量,Country 作为 y 向量。目标是输入 SecondsLatitudeLongitude 后返回国家编码:

from sklearn.model_selection import train_test_split

Selected_features = ['Seconds','Latitude','Longitude']

X = ufos[Selected_features]
y = ufos['Country']

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=0)

参数含义:test_size=0.2 表示留出 20% 作测试集;random_state=0 固定随机种子,保证划分结果可复现。

5.2 训练与评估

使用逻辑回归训练,并用分类报告与准确率评估:

from sklearn.metrics import accuracy_score, classification_report
from sklearn.linear_model import LogisticRegression
model = LogisticRegression()
model.fit(X_train, y_train)
predictions = model.predict(X_test)

print(classification_report(y_test, predictions))
print('Predicted labels: ', predictions)
print('Accuracy: ', accuracy_score(y_test, predictions))

文档给出的实测结论:准确率约 95%,且这并不令人意外——因为 CountryLatitude/Longitude 天然强相关(一个国家的国土大致落在固定的经纬度范围内)。文档坦承这个模型“并不革命性”,因为经纬度本身就能推出国家;但它的价值在于完整练习了“从原始数据清洗、导出模型、再到 Web 应用消费”的整条链路

六、Pickle 你的模型:几行代码完成序列化

模型训练完成后,用标准库 pickle 把它落盘,并立即做一次“反序列化 + 预测”的闭环验证:

import pickle
model_filename = 'ufo-model.pkl'
pickle.dump(model, open(model_filename,'wb'))

model = pickle.load(open('ufo-model.pkl','rb'))
print(model.predict([[50,44,-12]]))

要点解析:

  • pickle.dump(model, open(filename,'wb')):以二进制写模式打开文件,把整个模型对象写入;
  • pickle.load(open('ufo-model.pkl','rb')):以二进制读模式加载回内存,得到一个与原始对象等价的模型实例;
  • 验证输入 [[50, 44, -12]](50 秒、纬度 44、经度 -12)时,模型返回 '3',按 LabelEncoder 的字母序映射即 UK(英国)——文档称之为“wild! 👽”的时刻。

仓库中已经附带了该课时训练产物的成品文件 3-Web-App/1-Web-App/solution/ufo-model.pkl,可以直接配合 solution 目录里的 Web 应用体验完整效果,无需自己先跑一遍 notebook。

七、构建 Flask 应用:把模型装进网页

7.1 目录结构

文档要求的工程布局是:在 notebook.ipynbufo-model.pkl 所在目录旁创建 web-app 文件夹,并在其内部建立 static/csstemplates 两级目录:

web-app/
  static/
    css/
  templates/
notebook.ipynb
ufo-model.pkl

仓库 solution 目录即按此结构提供了成品,可对照查看:

7.2 依赖声明与安装

第一个要创建的文件是 requirements.txt——它在 Python 项目中的角色类似 JavaScript 的 package.json,列出应用所需依赖:

scikit-learn
pandas
numpy
flask

仓库中 3-Web-App/1-Web-App/solution/web-app/requirements.txt 的内容与上述完全一致(四行、无版本锁定)。随后进入目录安装:

cd web-app
pip install -r requirements.txt

7.3 静态样式:styles.css

创建三个文件:web-app/app.pyweb-app/templates/index.htmlweb-app/static/css/styles.cssstyles.css 定义了黑底白字、居中大字号的页面风格:

body {
	width: 100%;
	height: 100%;
	font-family: 'Helvetica';
	background: black;
	color: #fff;
	text-align: center;
	letter-spacing: 1.4px;
	font-size: 30px;
}

input {
	min-width: 150px;
}

.grid {
	width: 300px;
	border: 1px solid #2d2d2d;
	display: grid;
	justify-content: center;
	margin: 20px auto;
}

.box {
	color: #fff;
	background: #2d2d2d;
	padding: 12px;
	display: inline-block;
}

7.4 页面模板:index.html 与 Jinja2 语法

index.html 是整个交互的唯一前端。注意其中的“mustache”({{ }})模板语法——这些占位符由 Flask 应用填充:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>🛸 UFO Appearance Prediction! 👽</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
  </head>

  <body>
    <div class="grid">

      <div class="box">

        <p>According to the number of seconds, latitude and longitude, which country is likely to have reported seeing a UFO?</p>

        <form action="{{ url_for('predict')}}" method="post">
          <input type="number" name="seconds" placeholder="Seconds" required="required" min="0" max="60" />
          <input type="text" name="latitude" placeholder="Latitude" required="required" />
          <input type="text" name="longitude" placeholder="Longitude" required="required" />
          <button type="submit" class="btn">Predict country where the UFO is seen</button>
        </form>

        <p>{{ prediction_text }}</p>

      </div>

    </div>

  </body>
</html>

三个模板细节值得注意:

  1. {{ url_for('static', filename='css/styles.css') }}:由 Flask 反向解析静态资源 URL,避免手写路径;
  2. 表单 action="{{ url_for('predict') }}" method="post":提交动作绑定到名为 predict 的 Flask 路由,方法为 POST;
  3. {{ prediction_text }}:预测结果占位符,仅在收到 /predict 响应并渲染模板时才有值。

7.5 应用主体:app.py 逐段解析

最后是驱动“模型消费 + 结果展示”的 Python 文件 app.py

import numpy as np
from flask import Flask, request, render_template
import pickle

app = Flask(__name__)

model = pickle.load(open("./ufo-model.pkl", "rb"))


@app.route("/")
def home():
    return render_template("index.html")


@app.route("/predict", methods=["POST"])
def predict():

    int_features = [int(x) for x in request.form.values()]
    final_features = [np.array(int_features)]
    prediction = model.predict(final_features)

    output = prediction[0]

    countries = ["Australia", "Canada", "Germany", "UK", "US"]

    return render_template(
        "index.html", prediction_text="Likely country: {}".format(countries[output])
    )


if __name__ == "__main__":
    app.run(debug=True)

文档对 app.py 的执行流程做了三步拆解:

  1. 加载依赖并创建 Flask 应用实例;
  2. 在模块顶层加载模型——pickle.load(open("./ufo-model.pkl", "rb")) 在应用启动时一次性执行,模型常驻内存,后续每次请求直接复用,这是该架构性能友好的关键;
  3. 路由 /(home)渲染 index.html 首页;路由 /predict 处理 POST 请求:先把表单变量聚合成 numpy 数组送入模型拿到预测,再把预测出的国家编码(output)通过 countries 列表反向翻译成可读国家名,写回模板变量 prediction_text

源码级对照:仓库 solution 版 app.py 中模型加载路径写作 pickle.load(open("../ufo-model.pkl", "rb")),指向 solution/ufo-model.pkl。从源码结构看,solution 目录把 .pkl 文件放在了 solution/ 层级(而不是 solution/web-app/ 内),因此相对路径多一层 ../;两者逻辑完全等价,只是工程布局不同。这正提示了部署时的一个实操要点:.pkl 文件相对于 app.py 的引用路径必须与实际位置严格一致,否则启动即报 FileNotFoundError

运行方式:在 web-app 目录下执行 python app.py(或 python3 app.py),本地 Web 服务器启动后即可填写表单获得预测。

文档还附了一条 debug=True 的使用提示:开启调试模式后,应用代码的改动会即时热更新、无需重启服务器;但生产环境切勿开启该模式。

7.6 关键难点:预测输入的“数据契约”

文档点出了这类架构中最容易被忽视、也最难的一课:用 Flask + pickle 消费模型本身很直接,最难的是搞清楚“必须向模型送什么形状的数据”才能拿到预测——这完全取决于模型当初是怎么训练的。本例模型要求按 Seconds, Latitude, Longitude 顺序输入三个数值(对应 LabelEncoder 训练时的列顺序),且 app.pycountries 列表的顺序必须与 LabelEncoder().fit_transform() 的字母序结果严格对齐。

文档进而给出一个职业化视角:在真实团队里,模型训练者与模型消费者(Web/移动端开发者)之间的沟通质量直接决定集成是否成功;而在本课程里,这两者都是“你”一个人。

八、挑战与作业:延伸练习

8.1 Challenge:把训练搬进 Flask 应用

文档给出的挑战是:不在 notebook 里训练再导入,而是直接在 Flask 应用内训练模型——把 notebook 中数据清洗后的 Python 代码搬进一个名为 train 的路由里执行训练,并思考这种方式的利弊(可推断的权衡包括:应用可自举更新模型 vs. 每次冷启动训练成本高、训练逻辑与请求生命周期耦合等)。

8.2 Assignment:换一个模型重构应用

作业要求(见 translations/ar/3-Web-App/1-Web-App/assignment.md,英文版见 3-Web-App/1-Web-App/assignment.md):在已用回归训练模型构建 Web 应用的基础上,改用回归(Regression)课程中的某个模型(例如南瓜数据集上的模型)重新搭建一个应用;可保留原样式或按南瓜数据重新设计,但必须把表单输入项调整为与你模型训练方式匹配的特征。评估标准关注两点:应用按预期工作并可部署上云(优秀)、应用可运行但存在缺陷或结果异常(合格)、应用不能正确运行(待改进)。

8.3 Review & Self Study

文档建议的自学方向:列举用 JavaScript 或 Python 构建 ML Web 应用的各种方式;思考架构决策——模型应该留在应用内还是驻留云端?若驻留云端,客户端如何访问它?最后,为“应用级 ML Web 方案”画出一张架构图。

九、小结:一条可复用的模型交付链路

回到本文主线,第三周模块给出的是一条最小但完整的 Scikit-learn 模型交付链路,各环可在仓库中逐一对照:

环节 关键动作 仓库对照
数据 读取 ufos.csv,选列、去空、过滤 1–60 秒、LabelEncoder 编码国家 3-Web-App/1-Web-App/data/ufos.csv练习 notebook
训练 train_test_split(20% 测试、seed=0)+ LogisticRegression,测试集准确率约 95% solution notebook
序列化 pickle.dump 落盘为 ufo-model.pklpickle.load 加载后验证 [[50,44,-12]] → '3'(UK) solution/ufo-model.pkl
服务 Flask 双路由应用:/ 渲染表单,/predict 转 int 数组 → model.predict → 国家名回填模板 solution/web-app/app.py
依赖 requirements.txt 锁定 scikit-learn / pandas / numpy / flask 四项 solution/web-app/requirements.txt

掌握这条链路后,你可以把它作为模板平移到任何“notebook 训练的 Scikit-learn 模型 → Python Web 服务”的场景,只需替换数据、特征顺序与标签映射这三处“契约”。

登录后查看全文
热门项目推荐
相关项目推荐