
文章目录
-
- [1. 课前导读](#1. 课前导读)
-
- [1.1 本节课学习目标](#1.1 本节课学习目标)
- [1.2 知识重难点](#1.2 知识重难点)
- [1.3 学习前置条件](#1.3 学习前置条件)
- [1.4 学完可掌握能力](#1.4 学完可掌握能力)
- [1.5 行业应用场景](#1.5 行业应用场景)
- [2. 核心理论精讲](#2. 核心理论精讲)
-
- [2.1 模型部署的两种形态](#2.1 模型部署的两种形态)
- [2.2 SavedModel详解](#2.2 SavedModel详解)
- [2.3 签名(Signature)](#2.3 签名(Signature))
- [2.4 推理时注意事项](#2.4 推理时注意事项)
- [2.5 简单的API服务框架](#2.5 简单的API服务框架)
- [3. 环境搭建与工具配置](#3. 环境搭建与工具配置)
- [4. 代码实战教学](#4. 代码实战教学)
-
- [4.1 训练并导出模型(以MNIST CNN为例)](#4.1 训练并导出模型(以MNIST CNN为例))
- [4.2 离线推理:批量预测](#4.2 离线推理:批量预测)
- [4.3 使用Flask构建在线API服务](#4.3 使用Flask构建在线API服务)
- [4.4 测试API客户端](#4.4 测试API客户端)
- [4.5 推理性能优化(批处理和预加载)](#4.5 推理性能优化(批处理和预加载))
- [5. 案例实操演练](#5. 案例实操演练)
-
- [5.1 训练并导出CIFAR-10模型(简单CNN)](#5.1 训练并导出CIFAR-10模型(简单CNN))
- [5.2 离线批量预测(使用tf.data优化)](#5.2 离线批量预测(使用tf.data优化))
- [5.3 构建RESTful API(支持批量JSON输入)](#5.3 构建RESTful API(支持批量JSON输入))
- [6. 常见坑点与排错总结](#6. 常见坑点与排错总结)
-
- [6.1 模型加载与签名](#6.1 模型加载与签名)
- [6.2 推理与训练模式差异](#6.2 推理与训练模式差异)
- [6.3 服务化部署坑点](#6.3 服务化部署坑点)
- [6.4 批处理性能](#6.4 批处理性能)
- [7. 知识点总结 + 课后作业](#7. 知识点总结 + 课后作业)
-
- [7.1 核心知识点梳理](#7.1 核心知识点梳理)
- [7.2 基础作业](#7.2 基础作业)
- [7.3 进阶实操作业](#7.3 进阶实操作业)
- [7.4 思考拓展题](#7.4 思考拓展题)
- [🔗《TensorFlow2.x: 深度学习入门到高阶实战教程》系列课程导航](#🔗《TensorFlow2.x: 深度学习入门到高阶实战教程》系列课程导航)
1. 课前导读
1.1 本节课学习目标
- 理解模型部署的基本概念:离线推理(批量预测)vs 在线推理(实时API)。
- 掌握SavedModel的导出方法(
tf.saved_model.save和model.save),理解其目录结构。 - 学会使用
tf.saved_model.load加载SavedModel并进行批量推理。 - 掌握使用Flask构建简单的RESTful API服务,封装模型推理逻辑。
- 理解推理与训练阶段的差异(如
training=False、Dropout关闭)。 - 了解推理性能的初步优化(批量大小、预处理流水线)。
1.2 知识重难点
| 类别 | 内容 |
|---|---|
| 重点 | SavedModel导出与加载;tf.saved_model.load与tf.keras.models.load_model的区别;Flask服务搭建与请求处理 |
| 难点 | 批量推理的性能考量;服务化中的并发与超时处理;模型预处理的代码复用 |
| 易混淆点 | model.save保存的SavedModel与tf.saved_model.save的差异;推理时的training=False设置位置;签名(Signature)的概念 |
1.3 学习前置条件
- 已完成第20课(模型保存),了解SavedModel基础。
- 能够训练简单的CNN或MLP模型。
- 熟悉Python基础及Web框架(Flask)基本概念。
1.4 学完可掌握能力
- 独立将Keras模型导出为SavedModel,供生产环境使用。
- 编写脚本对大规模数据进行离线批量预测。
- 搭建简单的模型HTTP服务,供其他应用调用。
- 理解推理时需要注意的细节,避免因训练/推理模式差异导致的错误。
1.5 行业应用场景
- 离线批处理:每日对用户数据批量生成推荐结果。
- 在线服务:手机APP调用后端模型API进行实时识别。
- 边缘部署:模型导出后转换为TensorFlow Lite或TensorRT格式。
- A/B测试:同时部署多个模型版本,对比效果。
2. 核心理论精讲
2.1 模型部署的两种形态
离线推理:对一批数据集中运行模型,输出结果存储到文件或数据库。常见场景:每日生成用户画像、批量数据分析。优点:可以充分利用硬件资源进行批量处理;缺点:实时性差。
在线推理:通过API提供实时预测服务,用户请求到达时立即返回结果。常见场景:手机拍照识别、在线推荐。优点:响应快;缺点:需要高并发、低延迟支持。
本课主要介绍离线推理和简单在线服务的基础,为更高级的部署(如TensorFlow Serving、TFLite、TF.js)打下基础。
2.2 SavedModel详解
SavedModel是TensorFlow推荐的模型存储格式,包含:
saved_model.pb(或pb文件):图结构定义(MetaGraph)。variables/:模型权重文件。assets/:外部文件(如词汇表)。
SavedModel独立于源代码,可以在不同语言(C++、Java、Go)中加载。
导出方法:
- Keras模型:
model.save('path')默认导出SavedModel(TF2默认)。 - 通用接口:
tf.saved_model.save(model, 'path')。
加载方法:
loaded = tf.saved_model.load('path'),返回一个可调用对象,通过loaded.signatures['serving_default']获取默认签名。- 若为Keras模型,也可用
tf.keras.models.load_model('path')加载,返回Keras模型对象。
2.3 签名(Signature)
签名定义了模型的输入输出映射。默认签名serving_default由model.save自动生成。也可自定义签名,控制输入输出的名称和形状。
2.4 推理时注意事项
training=False:所有需要区别训练/推理模式的层(如Dropout、BatchNormalization)必须在推理时设为training=False,否则行为会异常(Dropout仍随机失活,BN使用小批量统计)。在model.predict中自动处理,但若手动调用模型需显式设置。- 预处理一致性:推理时的预处理必须与训练时完全相同(归一化参数、图像尺寸等)。最好将预处理封装在模型中或作为API的一部分。
- 批处理:即使单条请求,通常也以批次形式输入(batch_size=1),以利用向量化运算。
2.5 简单的API服务框架
使用Flask(轻量级Web框架)构建API:
- 加载模型(全局加载一次,避免每次请求重新加载)。
- 定义路由(
/predict),接收POST请求,解析JSON或图像文件,进行预处理,调用模型预测,返回JSON结果。 - 考虑使用
gunicorn等WSGI服务器部署生产环境。
3. 环境搭建与工具配置
沿用第39课环境,额外安装flask。
bash
conda activate tf213
pip install flask pillow
创建项目目录:
deploy/
├── model/ # 存放导出的模型
├── server.py # Flask服务
├── offline_inference.py # 离线推理脚本
└── test_request.py # 测试客户端
导入模块:
python
import tensorflow as tf
import numpy as np
import os
from PIL import Image
import json
from flask import Flask, request, jsonify
4. 代码实战教学
4.1 训练并导出模型(以MNIST CNN为例)
python
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
# 加载MNIST
(x_train, y_train), (x_test, y_test) = keras.datasets.mnist.load_data()
x_train = x_train.reshape(-1, 28, 28, 1).astype(np.float32) / 255.0
x_test = x_test.reshape(-1, 28, 28, 1).astype(np.float32) / 255.0
# 构建模型
model = keras.Sequential([
layers.Conv2D(32, 3, activation='relu', input_shape=(28,28,1)),
layers.MaxPooling2D(),
layers.Conv2D(64, 3, activation='relu'),
layers.MaxPooling2D(),
layers.Flatten(),
layers.Dense(128, activation='relu'),
layers.Dense(10, activation='softmax')
])
model.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['accuracy'])
model.fit(x_train, y_train, epochs=3, batch_size=128, validation_split=0.1, verbose=1)
# 导出SavedModel
model.save('deploy/model/mnist_cnn') # 保存为SavedModel
print("Model saved to 'deploy/model/mnist_cnn'")
4.2 离线推理:批量预测
python
# offline_inference.py
import tensorflow as tf
import numpy as np
import time
# 加载模型
loaded_model = tf.saved_model.load('deploy/model/mnist_cnn')
infer = loaded_model.signatures['serving_default']
# 或使用Keras方式:loaded_keras = tf.keras.models.load_model('deploy/model/mnist_cnn')
# 准备测试数据(假设从文件加载)
(x_test, y_test) = tf.keras.datasets.mnist.load_data()[1]
x_test = x_test.reshape(-1, 28, 28, 1).astype(np.float32) / 255.0
# 批量推理
batch_size = 256
num_samples = len(x_test)
predictions = []
start = time.time()
for i in range(0, num_samples, batch_size):
batch = x_test[i:i+batch_size]
# 调用模型,注意输入名称为'keras_tensor'或'input_1',可通过infer.structured_outputs查看
# 使用签名调用
result = infer(tf.constant(batch))
# result 是一个字典,键为输出名(通常是'dense_1')
pred = result[list(result.keys())[0]].numpy()
predictions.append(pred)
predictions = np.concatenate(predictions, axis=0)
elapsed = time.time() - start
print(f"Predicted {num_samples} samples in {elapsed:.2f}s, {num_samples/elapsed:.1f} samples/s")
4.3 使用Flask构建在线API服务
python
# server.py
import tensorflow as tf
import numpy as np
from flask import Flask, request, jsonify
import base64
from PIL import Image
import io
app = Flask(__name__)
# 加载模型(全局)
MODEL_PATH = 'deploy/model/mnist_cnn'
# 使用Keras方式加载,便于预处理
model = tf.keras.models.load_model(MODEL_PATH)
def preprocess_image(image_bytes):
"""将上传的图像转换为模型输入格式"""
img = Image.open(io.BytesIO(image_bytes)).convert('L') # 灰度
img = img.resize((28, 28))
img_array = np.array(img, dtype=np.float32) / 255.0
img_array = img_array.reshape(1, 28, 28, 1)
return img_array
@app.route('/predict', methods=['POST'])
def predict():
"""接收图像文件或base64编码,返回预测数字"""
if 'file' in request.files:
file = request.files['file']
img_bytes = file.read()
elif 'image' in request.json:
img_bytes = base64.b64decode(request.json['image'])
else:
return jsonify({'error': 'No image provided'}), 400
try:
input_tensor = preprocess_image(img_bytes)
preds = model.predict(input_tensor)
digit = np.argmax(preds[0])
confidence = float(np.max(preds[0]))
return jsonify({'digit': int(digit), 'confidence': confidence})
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/health', methods=['GET'])
def health():
return jsonify({'status': 'ok'})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=False)
4.4 测试API客户端
python
# test_request.py
import requests
from PIL import Image
import numpy as np
import io
# 创建一个测试图像(模拟手写数字)
def create_dummy_image(digit=5):
img = Image.new('L', (28, 28), color=0)
# 简单绘制数字(实际应使用真实图像)
return img
img = create_dummy_image()
buf = io.BytesIO()
img.save(buf, format='PNG')
buf.seek(0)
files = {'file': ('digit.png', buf, 'image/png')}
response = requests.post('http://localhost:5000/predict', files=files)
print(response.json())
# 也可用base64
import base64
img_base64 = base64.b64encode(buf.getvalue()).decode('utf-8')
response2 = requests.post('http://localhost:5000/predict', json={'image': img_base64})
print(response2.json())
4.5 推理性能优化(批处理和预加载)
对于高并发场景,可预先将模型加载到GPU,并使用多线程处理请求。Flask默认单线程,生产环境建议使用gunicorn:
bash
gunicorn -w 4 -b 0.0.0.0:5000 server:app
更专业的模型服务部署请使用TensorFlow Serving。
5. 案例实操演练
案例:为CIFAR-10模型构建离线批处理与在线API
5.1 训练并导出CIFAR-10模型(简单CNN)
python
(x_train, y_train), (x_test, y_test) = tf.keras.datasets.cifar10.load_data()
x_train = x_train.astype(np.float32) / 255.0
x_test = x_test.astype(np.float32) / 255.0
y_train = tf.keras.utils.to_categorical(y_train, 10)
y_test = tf.keras.utils.to_categorical(y_test, 10)
model = tf.keras.Sequential([
tf.keras.layers.Conv2D(32, 3, activation='relu', input_shape=(32,32,3)),
tf.keras.layers.MaxPooling2D(),
tf.keras.layers.Conv2D(64, 3, activation='relu'),
tf.keras.layers.MaxPooling2D(),
tf.keras.layers.Flatten(),
tf.keras.layers.Dense(64, activation='relu'),
tf.keras.layers.Dense(10, activation='softmax')
])
model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])
model.fit(x_train, y_train, epochs=5, batch_size=128, validation_split=0.1, verbose=1)
# 导出
model.save('deploy/model/cifar10_cnn')
5.2 离线批量预测(使用tf.data优化)
python
# 构建dataset
test_ds = tf.data.Dataset.from_tensor_slices(x_test).batch(256).prefetch(tf.data.AUTOTUNE)
loaded = tf.saved_model.load('deploy/model/cifar10_cnn')
infer = loaded.signatures['serving_default']
predictions = []
for batch in test_ds:
result = infer(tf.constant(batch))
pred = result[list(result.keys())[0]].numpy()
predictions.append(pred)
predictions = np.concatenate(predictions, axis=0)
print("Predictions shape:", predictions.shape)
5.3 构建RESTful API(支持批量JSON输入)
python
@app.route('/predict_batch', methods=['POST'])
def predict_batch():
data = request.get_json()
images = data.get('images') # list of list of pixel values
if not images:
return jsonify({'error': 'No images'}), 400
# 转换为numpy数组 (batch, 32,32,3)
images_np = np.array(images, dtype=np.float32) / 255.0
preds = model.predict(images_np)
results = [{'class': int(np.argmax(p)), 'confidence': float(np.max(p))} for p in preds]
return jsonify(results)
6. 常见坑点与排错总结
6.1 模型加载与签名
-
坑1 :使用
tf.saved_model.load加载后,直接调用loaded_model(input)报错,因为返回的是一个ConcreteFunction,需要通过签名调用或直接用括号调用(如果只有一个输入输出)。推荐使用loaded_model.signatures['serving_default']。- 解决 :
infer = loaded.signatures['serving_default']; result = infer(tf.constant(input))。
- 解决 :
-
坑2:签名输入输出名称未知,导致KeyError。
- 解决 :打印
list(infer.structured_input_signature)和infer.structured_outputs查看。
- 解决 :打印
6.2 推理与训练模式差异
-
坑3:模型包含Dropout层,推理时未关闭导致随机性。
- 原因 :
model.predict自动设置training=False,但若手动调用model(x, training=True)会出错。 - 解决 :确保推理时传递
training=False或使用predict方法。
- 原因 :
-
坑4:BatchNormalization层在推理时使用训练时的全局统计量,但若导出的SavedModel未正确设置,可能仍使用小批量统计。
- 解决 :确保导出模型前,模型已处于推理模式(即调用
model.predict一次以确定行为)。
- 解决 :确保导出模型前,模型已处于推理模式(即调用
6.3 服务化部署坑点
-
坑5:Flask默认单进程同步,高并发时模型被重复加载,内存爆炸。
- 解决 :使用
gunicorn多进程,模型全局加载一次即可(进程级隔离,每个进程一份模型)。
- 解决 :使用
-
坑6:请求体过大或图像解码失败导致服务崩溃。
- 解决:添加异常捕获,返回HTTP 400错误;限制请求大小。
-
坑7:预处理与训练时不匹配(如归一化参数不同)。
- 解决:将预处理封装为函数,并确保与训练代码完全一致。
6.4 批处理性能
-
坑8:离线推理中逐个样本循环,速度极慢。
- 解决:使用批量预测,利用向量化。
-
坑9:批量过大导致GPU内存不足。
- 解决:动态调整批次大小,或使用CPU推理。
7. 知识点总结 + 课后作业
7.1 核心知识点梳理
- SavedModel:标准部署格式,包含图和变量。
- 离线推理:批量加载数据,使用模型预测,保存结果。
- 在线API:Flask构建轻量服务,模型常驻内存,接收请求返回JSON。
- 推理注意事项 :
training=False,预处理一致性,批量处理。 - 性能初步优化:批处理、预取、WSGI服务器。
7.2 基础作业
- 训练一个简单的MNIST分类器,导出为SavedModel,然后编写脚本加载模型并对测试集前1000张图像进行批量预测,输出准确率。
- 修改Flask服务,增加
/predict_base64端点,接收JSON格式的base64编码图像,返回预测结果。 - 比较单条预测和批量预测(batch_size=32)的吞吐量差异(每秒处理图像数)。
7.3 进阶实操作业
任务:为图像分类模型构建完整的部署流水线
- 选择一个预训练模型(如ResNet50),在ImageNet类别上微调自己的数据集(如花朵识别)。
- 导出模型为SavedModel。
- 编写离线推理脚本,对测试集进行预测并计算准确率。
- 构建Flask API,支持上传图像文件或URL,返回Top-3类别及置信度。
- 使用
gunicorn部署,并用wrk或ab工具进行压力测试,记录QPS。
7.4 思考拓展题
-
在在线推理服务中,模型加载通常在服务启动时进行,而不是每次请求加载。为什么?如果模型非常大(>10GB),你有什么策略减少内存占用?
-
实际生产环境中,经常需要同时部署多个版本的模型(如v1、v2)进行A/B测试。如何设计API使得客户端可以指定模型版本?
-
除了Flask,还有哪些Python Web框架更适合高并发机器学习服务(如FastAPI)?它们相比Flask有哪些优势?
下一课预告:计算机视觉实战一:目标检测基础原理与简易模型搭建------我们将进入计算机视觉的核心领域,学习目标检测的基本思想(滑动窗口、锚框、非极大值抑制),并使用TensorFlow搭建一个简单的目标检测模型。
🔗《TensorFlow2.x: 深度学习入门到高阶实战教程》系列课程导航
第一部分:基础入门(1-10 课)
第二部分:神经网络核心(11-25 课)
第三部分:进阶网络与框架高阶(26-40 课)
第四部分:企业实战与项目落地(41-50 课)
🌟 感谢您耐心阅读到这里!💡 如果本文对您有所启发欢迎:
👍 点赞📌 收藏 📤 分享给更多需要的伙伴。
🗣️ 期待在评论区看到您的想法, 共同进步。
🔔 关注我,持续获取更多干货内容~
🤗 我们下篇文章见~