ML-For-Beginners 实践指南:用 Pickle 序列化 Scikit-learn 模型并搭建 Flask UFO 预测 Web 应用
本文基于 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 应用加载并做预测的文件。
根据模块文档的概述,本节的完整工作流是:
- 用 UFO 目击数据训练一个 Scikit-learn 模型(输入
Seconds、Latitude、Longitude,输出国家编码); - 将模型 Pickle 化为
ufo-model.pkl文件; - 构建一个 Flask Web 应用,加载该
.pkl文件并提供预测表单; - 用户在浏览器中输入三个数值,页面返回“Likely country: XX”。
模块包含 1 个正式课时与 1 个作业,文档入口为:
- 课时:translations/ar/3-Web-App/1-Web-App/README.md(Building a Web App)
- 作业:translations/ar/3-Web-App/1-Web-App/assignment.md(换一个模型重构应用)
模块文档同时注明了数据来源与致谢:数据集来自 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 文件,其完整列包括:datetime、city、state、country、shape、duration (seconds)、duration (hours/min)、comments、date posted、latitude、longitude。本课只保留训练需要的四列:时长(秒)、国家、纬度、经度。
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 向量。目标是输入 Seconds、Latitude、Longitude 后返回国家编码:
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%,且这并不令人意外——因为 Country 与 Latitude/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.ipynb 与 ufo-model.pkl 所在目录旁创建 web-app 文件夹,并在其内部建立 static/css 与 templates 两级目录:
web-app/
static/
css/
templates/
notebook.ipynb
ufo-model.pkl
仓库 solution 目录即按此结构提供了成品,可对照查看:
- 3-Web-App/1-Web-App/solution/web-app/app.py
- 3-Web-App/1-Web-App/solution/web-app/templates/index.html
- 3-Web-App/1-Web-App/solution/web-app/static/css/styles.css
- 3-Web-App/1-Web-App/solution/web-app/requirements.txt
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.py、web-app/templates/index.html、web-app/static/css/styles.css。styles.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>
三个模板细节值得注意:
{{ url_for('static', filename='css/styles.css') }}:由 Flask 反向解析静态资源 URL,避免手写路径;- 表单
action="{{ url_for('predict') }}" method="post":提交动作绑定到名为predict的 Flask 路由,方法为 POST; {{ 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 的执行流程做了三步拆解:
- 加载依赖并创建
Flask应用实例; - 在模块顶层加载模型——
pickle.load(open("./ufo-model.pkl", "rb"))在应用启动时一次性执行,模型常驻内存,后续每次请求直接复用,这是该架构性能友好的关键; - 路由
/(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.py 中 countries 列表的顺序必须与 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.pkl,pickle.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 服务”的场景,只需替换数据、特征顺序与标签映射这三处“契约”。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00