5.3 三条部署线:Serving、Lite 与 tfjs 本节摘要:训练环境的图无法直接进生产——三条部署线各管一个生态:Serving 把 SavedModel 变成服务器上的高吞吐推理服务,Lite 把模型量化转换后跑进手机与嵌入式设备,tfjs 让浏览器直接执行推理。三线共同的入口是 SavedModel(3.8 节),共同的思维是"部署目标决定转换动作":服务器求吞吐、端侧求体积、浏览器求零安装。本节逐线走通转换与调用的最小路径,并给出选线决策与量化取舍。 读完你应当能做到 阅读完本节,你应当能够: 导出带固定签名的 SavedModel,理解签名在生产调用中的合同角色; 说清 Serving 的版本化目录结构与 REST 调用格式;
本节摘要:训练环境的图无法直接进生产——三条部署线各管一个生态:Serving 把 SavedModel 变成服务器上的高吞吐推理服务,Lite 把模型量化转换后跑进手机与嵌入式设备,tfjs 让浏览器直接执行推理。三线共同的入口是 SavedModel(3.8 节),共同的思维是"部署目标决定转换动作":服务器求吞吐、端侧求体积、浏览器求零安装。本节逐线走通转换与调用的最小路径,并给出选线决策与量化取舍。
阅读完本节,你应当能够:
部署的第一步是把训练模型固化成一个"服务合同":输入输出的名字、形状、类型全部固定。model.export 一行完成,签名决定了下游所有调用方的接口:
import tensorflow as tf import numpy as np model = tf.keras.Sequential([tf.keras.layers.Input(shape=(8,)), tf.keras.layers.Dense(32, activation="relu"), tf.keras.layers.Dense(1)]) model.compile(optimizer="adam", loss="mse") X = np.random.rand(1000, 8).astype("float32") y = np.random.rand(1000).astype("float32") model.fit(X, y, epochs=2, verbose=0) model.export("serve/house/1") # 版本化目录:末级 1 是版本号 print("exported to serve/house/1") # 输出:exported to serve/house/1 # Serving 的目录约定:根目录下每个子目录是一个版本 reloaded = tf.keras.layers.TFSMLayer("serve/house/1", call_endpoint="serving_default") sample = tf.constant(X[:2]) out = reloaded(sample) print(list(out.keys()), out["output_0"].shape) # 输出:['output_0'] (2, 1) # 键名 output_0 来自签名——下游按这个名字取结果,合同兑现
签名固定的深层意义:模型升级只改版本号(目录 1 换 2),调用方接口不变,线上灰度与回滚都是目录级操作——这是 Serving 生态的标准发布模型。

Serving 是独立的服务进程:挂载 SavedModel 目录,自动暴露 REST 与 gRPC 接口,内置批处理(攒一批请求合并推理,吞吐换延迟)与版本热切换。Python 端的调用形状如上一段 TFSMLayer 所示;生产中调用方是任意 HTTP 客户端,报文按签名组织。运维三件套:模型目录按版本排布、Serving 配置监听端口与模型名、上游负载均衡按标准 HTTP 分发——模型服务从此与普通 Web 服务同构,平台团队不需要懂深度学习就能接手。
端侧的约束是体积、内存与功耗,转换器负责三件事:算子裁剪(只留推理所需)、格式转换(紧凑的 flatbuffer)、可选量化(float32 权重压成 int8)。量化是体积与速度的主旋钮:
converter = tf.lite.TFLiteConverter.from_keras_model(model) tflite_fp32 = converter.convert() print(f"float32 model: {len(tflite_fp32) / 1024:.0f} KB") # 全整数量化:体积最小、CPU 最快,需代表性数据校准 def rep_dataset(): for i in range(100): yield [X[i:i + 1].astype(np.float32)] converter.optimizations = [tf.lite.Optimize.DEFAULT] converter.representative_dataset = rep_dataset # 校准数据:决定量化范围 tflite_int8 = converter.convert() print(f"int8 model: {len(tflite_int8) / 1024:.0f} KB") # 参考输出:float32 model: 145 KB / int8 model: 41 KB # 体积压到约四分之一;精度损失换速度与内存,需验证集复测 interpreter = tf.lite.Interpreter(model_content=tflite_int8) interpreter.allocate_tensors() inp = interpreter.get_input_details()[0] out = interpreter.get_output_details()[0] interpreter.set_tensor(inp["index"], X[:1].astype(np.float32)) interpreter.invoke() print(interpreter.get_tensor(out["index"]).shape) # 输出:(1, 1) # 解释器加载、设输入、invoke、取输出——端侧运行的四步接口
三种量化的取舍:动态范围量化(只压权重,免校准,提速有限)、全整数量化(权重与激活都 int8,需校准数据,CPU 端最快最省)、float16 量化(权重减半,GPU 委托友好)。端侧选型的铁律是转换后必须复测精度——量化对离群敏感的任务(回归大范围预测)可能造成可感知的偏差。
浏览器线的价值在零安装与可交互:演示页、在线体验、隐私敏感的端上计算。转换器把 SavedModel 拆成 JSON 结构描述加分片二进制权重,网页端用 js 库加载调用。Python 侧转换:
# 命令行转换(示意命令结构,产物是一组文件): # tensorflowjs_converter --input_format=tf_saved_model serve/house/1 web_model # 产出 web_model 目录:model.json 加若干 weight 分片文件 print("conversion produces model.json plus weight shards") # 输出:conversion produces model.json plus weight shards # 网页端 js 侧调用形如:tf.loadLayersModel 加载后 model.predict # 分片大小默认约 4MB,适配浏览器缓存策略
浏览器线的现实约束是算力与模型体积:小模型(几 MB 内)页内推理流畅;大模型要么服务端推理、要么蒸馏压缩后再上。它最适合"演示与轻交互"场景,不是通用推理方案。
按部署目标倒推:需要高并发低延迟的 API 服务(推荐系统打分、内容审核),走 Serving;数据敏感或需离线运行的端侧场景(拍照识别、穿戴设备),走 Lite;产品形态是网页且交互演示价值大,走 tfjs。多线并行的项目以 SavedModel 为单一源头——一次导出、三线各自转换,避免多源头维护的漂移。
⚠️ 常见坑:端侧量化后不做精度复测直接上线。int8 量化对训练数据范围外的输入误差放大明显,务必用验证集对比量化前后指标,偏差超阈值就换 float16 或回退 float32。
💡 关键直觉:部署线的本质是"为执行环境重新编译图"。服务器环境资源充裕几乎免转换,端侧与浏览器资源紧张,转换器用量化与裁剪把模型改造成环境适配版——目标环境决定改造力度。
下节把全流程固化成流水线。