☰
ML-For-Beginners 实战:用 Flask 与 Pickle 将 Scikit-learn UFO 目击预测模型部署为 Web 应用
2026/10/16 15:56:21 网站建设 项目流程

ML-For-Beginners 实战:用 Flask 与 Pickle 将 Scikit-learn UFO 目击预测模型部署为 Web 应用

【免费下载链接】ML-For-Beginners12 weeks, 26 lessons, 52 quizzes, classic Machine Learning for all项目地址: https://gitcode.com/GitHub_Trending/ml/ML-For-Beginners

在 ML-For-Beginners 课程的 Web 应用章节中,你将完成一次完整的"模型落地"之旅:基于 NUFORC 近一个世纪约 8 万条 UFO 目击记录,在 Jupyter Notebook 中完成数据清洗、特征选择与逻辑回归训练,将训练好的模型用 Pickle 序列化为.pkl文件,再通过 Flask 微框架构建一个可交互的 Web 表单应用,让用户输入观测时长、纬度和经度即可预测目击国家。读完本文,你将掌握从 Notebook 导出模型、构建 Flask 路由与模板渲染、处理表单数据并驱动模型推理的完整链路,并能直接在仓库 3-Web-App/1-Web-App 中找到全部配套文件进行复现。

模型落地前的架构考量:先回答四个问题

把机器学习模型真正"用起来"(in the wild),首先要想清楚应用的形态与部署边界。在动手写代码前,文档给出了一组必须回答的问题:

  • 是 Web 应用还是移动应用?如果目标是 Android/iOS 或 IoT 场景,可以选用 TensorFlow Lite 将模型内嵌到移动端;
  • 模型放在哪里?云端还是本地?这决定了推理请求的链路;
  • 是否需要离线支持?应用在无网络环境下是否必须可用;
  • 模型是用什么技术训练的?训练技术直接决定部署工具链:
    • TensorFlow:可借助 TensorFlow.js 把模型转换后在浏览器/Node.js 中使用;
    • PyTorch:可导出为 ONNX(Open Neural Network Exchange)格式,配合 Onnx Runtime 供 JavaScript Web 应用调用;
    • Lobe.ai / Azure Custom Vision 等 ML SaaS:这类平台通常支持多平台导出,甚至可以直接构建云端 API 供在线应用查询。

从源码结构看,本仓库的姊妹章节 4-Classification/4-Applied 正是 ONNX 导出路线在 Scikit-learn 模型上的落地实践,印证了文档中"ONNX 路线将在未来课程中探索"的规划。而本章采取的是最贴合 Python 技术栈的路线:Notebook 训练 → Pickle 序列化 → Flask Web 应用消费。因为此前所有课程都在 Python Notebook 中工作,这条路线迁移成本最低,也最能完整展示"训练—导出—部署—推理"的闭环。

工具准备:Flask 与 Pickle

本任务只需要两个 Python 工具:

  • Flask:由 Pallets 项目维护的"微框架"(micro-framework),提供 Web 框架的核心能力(路由、请求处理)与 Jinja 模板引擎,用极少量代码即可渲染 HTML 页面;
  • Pickle:Python 标准库模块,用于将 Python 对象结构序列化(serialize)与反序列化(de-serialize)。对模型执行 pickle 操作,等于把它的结构"拍平"成字节流,便于存储和跨进程传输。序列化产物的标准后缀是.pkl。

需要特别警惕的是:pickle 本质上并不安全,反序列化(un-pickle)来历不明的文件可能执行恶意代码,因此永远不要加载不可信来源的.pkl文件。

第一步:清洗 UFO 目击数据

课程使用 ufos.csv 数据表,它由 NUFORC(美国国家 UFO 报告中心)收集的 8 万余条目击记录构成,包含目击发生的city、state、country、物体shape、latitude与longitude等字段,以及相当有画面感的comments描述——长如"一名男子从照在夜晚草场上的一束光中现身,朝德州仪器停车场跑去",短如"那些灯在追我们"。

在课程附带的空白 notebook.ipynb 中,按以下步骤完成清洗(仓库 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把国家文本值按字母序编码为数字:

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

对照 solution/notebook.ipynb 的实际输出,可以看到更完整的上下文:清洗后数据从 8 万余行收敛为25,863 行,Country字段的原始取值为['us', nan, 'gb', 'ca', 'au', 'de'],经LabelEncoder按字母序编码后得到映射0=au, 1=ca, 2=de, 3=gb, 4=us。这个编码顺序将在后面 Flask 应用把预测数字还原为国家名时被直接引用——训练端与消费端必须严格对齐,这是本课程反复强调的关键点。

第二步:训练逻辑回归分类器

接下来划分训练集与测试集,并训练模型。选择Seconds、Latitude、Longitude三个特征作为 X 向量,Country作为 y 向量——目标就是输入时长、纬度、经度,输出国家编码:

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)

随后用逻辑回归训练并评估:

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存在天然的地理关联,模型本质上是"由经纬度反推国家"。仓库中的实际运行输出提供了更细粒度的证据:测试集共5,173 条,accuracy为0.9513,但分类报告也暴露出数据不平衡问题——类别 4(us)独占 4,743 条样本,而类别 2(de)仅 8 条、precision 为 0.00,类别 1(ca)recall 仅 0.02。这正是文档提醒的:模型本身"并不革命",但它完整覆盖了从原始数据清洗、特征抽取到模型落地的练习价值。

第三步:用 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]]))

输入[50, 44, -12](50 秒时长、纬度 44、经度 -12,约对应北大西洋东部)后,模型返回3——即国家编码 3,对应gb(英国)。仓库 solution/notebook.ipynb 的第 29 个单元格记录了相同的输出[3],与该编码表完全吻合。至此,ufo-model.pkl就成为了可供任何 Python 进程加载的"可移植模型"。

第四步:构建 Flask 应用

现在把模型装进一个可视化的 Web 应用。先创建目录结构:在存放ufo-model.pkl的notebook.ipynb旁边新建web-app文件夹,内部再建static/css与templates两个子目录:

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

4.1 声明依赖:requirements.txt

requirements.txt之于 Python 应用,正如package.json之于 JavaScript 应用,用于声明运行所需的依赖。写入以下四行并安装:

scikit-learn pandas numpy flask
cd web-app pip install -r requirements.txt

仓库 solution/web-app/requirements.txt 与文档完全一致,可直接作为模板。

4.2 编写样式:static/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; }

4.3 编写模板:templates/index.html

模板文件同时完成两件事:渲染一个向/predict路由 POST 的表单;用 Jinja 的"胡子语法"{{ }}输出应用端传入的prediction_text预测结果:

<!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>

注意两个细节:seconds输入框通过min="0" max="60"在浏览器层约束取值区间(与训练时 1~60 秒的过滤逻辑呼应);url_for('static', filename='css/styles.css')是 Flask 生成静态资源 URL 的推荐写法,url_for('predict')则按视图函数名反向解析出/predict路径,避免硬编码 URL。仓库 solution/web-app/templates/index.html 提供了成品参考。

4.4 编写应用入口: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)

逐段拆解其工作流:

  1. 导入numpy、Flask 的Flask/request/render_template与pickle,创建应用实例;
  2. 在模块加载时即把ufo-model.pkl反序列化为内存中的model对象(模型只需加载一次,避免每次请求重复读盘);
  3. 根路由/渲染index.html首页;
  4. /predict路由仅接受 POST:收集request.form中所有表单值并转为整数列表,包装成 numpy 数组后送入model.predict(),得到预测编码;
  5. 用硬编码的国家列表["Australia", "Canada", "Germany", "UK", "US"]将编码还原为可读文本——该列表的顺序正是此前LabelEncoder的字母序编码0=au, 1=ca, 2=de, 3=gb, 4=us的镜像;
  6. 将结果作为prediction_text回传模板渲染。

路径兼容性提示:文档示例中模型路径写作./ufo-model.pkl,但仓库 solution/web-app/app.py 中实际使用的是../ufo-model.pkl——因为 solution 目录里ufo-model.pkl与web-app文件夹同级(位于solution/下)。请务必根据自己模型文件的真实存放位置调整该路径,否则应用启动时会因找不到文件而报错。

💡 提示:app.run(debug=True)开启调试模式后,代码改动会即时生效,无需手动重启服务,适合开发期迭代;生产环境切勿开启,它会暴露调试器并带来安全风险。

4.5 启动应用

python app.py # 或 python3 app.py

Web 服务将在本地启动,浏览器中即可填写"秒数、纬度、经度"三个字段,回车获得如Likely country: UK的预测答案。

深入理解:为何"数据形状"是部署中最容易出错的一环

用 Flask + pickled 模型的方式消费模型,整体链路相当直接,文档指出真正的难点在于搞清喂给模型的数据形状——它完全取决于训练时的定义。本模型中,训练时model.fit(X_train, y_train)的 X 是 3 个特征(Seconds、Latitude、Longitude),因此predict()也必须接收形状为(1, 3)的二维数组,这就是final_features = [np.array(int_features)]再做一层嵌套列表的原因;若直接传入一维列表,Scikit-learn 会因特征维度不匹配而报错。

在真实的工程协作中,这恰恰体现了训练团队与消费团队之间清晰沟通的必要性:模型输入有几个特征、顺序如何、编码映射是什么,都必须有明确约定。在本课程里,训练者与使用者是同一个人——你,正好可以完整体验这份"契约"的建立过程。

进阶挑战:把训练搬进 Flask 应用

本章的 Challenge 提供了一个反向思路:不在 Notebook 里训练后导入模型,而是把 Notebook 中的清洗与训练代码(至少从数据清洗之后的部分)直接搬进 Flask 应用,在一条名为train的路由中完成训练。你可以就此评估两条路线的利弊——例如:在应用内训练可以省去序列化/导入环节、数据更新后一键重训,但首次请求会阻塞且耗时,模型参数与数据要打进同一份部署包,难以与工程化部署(CI/CD、模型版本管理)解耦。这是一个值得动手实验的架构权衡题。

延伸学习与作业

  • 课程后的自我学习建议:列举用 JavaScript 或 Python 构建 ML Web 应用的各种途径,重点思考模型应内嵌在应用里还是托管在云端,若在云端又该如何访问,并画出一张 ML Web 解决方案的架构草图;
  • 配套作业 assignment.md 要求尝试换用不同的模型完成同样的部署流程,是检验本章理解的最佳实践;
  • 仓库中的完整成品可直接对照:solution/web-app 目录下的app.py、requirements.txt、templates/index.html、static/css/styles.css,以及solution/ufo-model.pkl预训练模型文件。

至此,你已经走通了"Notebook 训练 → Pickle 序列化 → Flask 表单应用 → 实时预测"的完整闭环,掌握了把任意 Scikit-learn 模型快速包装成可交互 Web 应用的方法论。

【免费下载链接】ML-For-Beginners12 weeks, 26 lessons, 52 quizzes, classic Machine Learning for all项目地址: https://gitcode.com/GitHub_Trending/ml/ML-For-Beginners

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询