1. 项目概述为什么选择Keras开启深度学习之旅作为TensorFlow的高层APIKeras以其极简的接口设计成为深度学习入门的最佳入口。我至今记得第一次用Keras训练MNIST分类器时仅用20行代码就实现了98%准确率的震撼——这比当年用纯NumPy实现神经网络节省了至少90%的调试时间。对于刚接触深度学习的开发者Keras的三大优势尤为关键模块化设计像搭积木一样组合网络层Dense、Conv2D等层的参数命名直观到几乎不需要查文档跨后端支持既可基于TensorFlow运行也能切换Theano或CNTK虽然现在基本都用TF了生产就绪从Google的官方推荐到Kaggle竞赛中的高频出现学会Keras等于掌握工业级工具重要提示虽然Keras现在已整合为tf.keras但本文示例同时兼容独立Keras库和TensorFlow 2.x内置版本。建议新手直接使用TensorFlow 2.3环境。2. 环境配置与工具选型2.1 基础软件栈的黄金组合在我的教学实践中以下环境配置方案成功率最高# 使用conda创建虚拟环境比venv更适合科学计算 conda create -n keras_env python3.8 conda activate keras_env # 安装GPU版本需先配置CUDA/cuDNN pip install tensorflow-gpu2.6 keras matplotlib jupyter关键工具选型理由Python 3.8在ABI兼容性和新特性间取得平衡的稳定版本TensorFlow-gpu 2.6长期支持版本CUDA兼容性最广Jupyter Lab比Notebook更现代的交互环境适合调试网络结构2.2 验证GPU加速是否生效跑这段代码检查CUDA是否被正确调用import tensorflow as tf print(GPU可用:, tf.config.list_physical_devices(GPU)) print(TF版本:, tf.__version__) print(Keras版本:, tf.keras.__version__)典型输出应包含类似信息GPU可用: [PhysicalDevice(name/physical_device:GPU:0, device_typeGPU)] TF版本: 2.6.0 Keras版本: 2.6.03. 第一个端到端图像分类项目3.1 MNIST数据集预处理技巧虽然MNIST被视为深度学习的Hello World但正确处理数据仍有许多门道from tensorflow.keras.datasets import mnist # 加载数据时的关键参数 (train_images, train_labels), (test_images, test_labels) mnist.load_data( pathmnist.npz # 指定本地缓存路径避免重复下载 ) # 归一化的最佳实践 train_images train_images.reshape((60000, 28, 28, 1)).astype(float32) / 255 test_images test_images.reshape((10000, 28, 28, 1)).astype(float32) / 255 # 标签one-hot编码的陷阱 train_labels tf.keras.utils.to_categorical(train_labels, num_classes10) test_labels tf.keras.utils.to_categorical(test_labels, num_classes10)为什么这样处理reshape添加了通道维度28,28,1是为了兼容CNN输入要求除以255的归一化放在reshape之后可避免整数除法精度丢失to_categorical必须指定num_classes否则遇到未出现标签时会出错3.2 网络架构设计哲学对比三种典型结构的优劣from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Dense, Flatten, Conv2D, MaxPooling2D # 方案A全连接网络基准模型 model_a Sequential([ Flatten(input_shape(28, 28, 1)), Dense(128, activationrelu), Dense(10, activationsoftmax) ]) # 方案B浅层CNN model_b Sequential([ Conv2D(32, (3,3), activationrelu, input_shape(28,28,1)), MaxPooling2D((2,2)), Flatten(), Dense(10, activationsoftmax) ]) # 方案C深度CNN适合复杂任务 model_c Sequential([ Conv2D(32, (3,3), activationrelu, paddingsame, input_shape(28,28,1)), Conv2D(64, (3,3), activationrelu), MaxPooling2D((2,2)), Flatten(), Dense(128, activationrelu), Dense(10, activationsoftmax) ])架构选择指南全连接网络参数量大如model_a有101,770个参数适合教学演示但实际很少用浅层CNNmodel_b在MNIST上就能达到99%准确率是性价比最高的选择深度CNNmodel_c在简单数据集上容易过拟合但可学习更复杂的特征3.3 训练过程的实战细节编译和训练模型时这些参数最值得关注model_b.compile( optimizeradam, # 比SGD更省心的自适应优化器 losscategorical_crossentropy, # 多分类标准损失 metrics[accuracy, # 主指标 tf.keras.metrics.Precision(), # 添加次要指标 tf.keras.metrics.Recall()] ) history model_b.fit( train_images, train_labels, epochs10, batch_size64, # 通常取2的幂次 validation_split0.2, # 自动从训练集划分验证集 callbacks[ tf.keras.callbacks.EarlyStopping(patience2), # 早停防过拟合 tf.keras.callbacks.ModelCheckpoint(best_model.h5) # 保存最佳模型 ] )参数调优经验batch_size越大训练越快但可能影响收敛性64是常用起点验证集比例设为0.1-0.2足够小数据集可适当减小EarlyStopping的patience设为总epoch数的1/5到1/34. 模型评估与性能优化4.1 解读训练过程可视化用Matplotlib绘制训练曲线时要注意这些细节import matplotlib.pyplot as plt def plot_history(history): fig, (ax1, ax2) plt.subplots(1, 2, figsize(12, 4)) # 准确率曲线 ax1.plot(history.history[accuracy], labeltrain) ax1.plot(history.history[val_accuracy], labelvalidation) ax1.set_title(Model accuracy) ax1.set_ylabel(Accuracy) ax1.set_xlabel(Epoch) ax1.legend() # 损失曲线 ax2.plot(history.history[loss], labeltrain) ax2.plot(history.history[val_loss], labelvalidation) ax2.set_title(Model loss) ax2.set_ylabel(Loss) ax2.set_xlabel(Epoch) ax2.legend() plt.show() plot_history(history)曲线分析要点理想情况两条线接近且同步下降说明无过拟合训练线远高于验证线明显过拟合需增加Dropout层或数据增强验证线波动剧烈可能batch_size太小或学习率太高4.2 混淆矩阵深度分析超越准确率指标用混淆矩阵发现模型弱点from sklearn.metrics import confusion_matrix import seaborn as sns # 生成预测结果 y_pred model_b.predict(test_images).argmax(axis1) y_true test_labels.argmax(axis1) # 绘制混淆矩阵 cm confusion_matrix(y_true, y_pred) plt.figure(figsize(10,8)) sns.heatmap(cm, annotTrue, fmtd, cmapBlues) plt.xlabel(Predicted) plt.ylabel(True) plt.show()典型发现问题数字4和9容易混淆手写体相似数字1和7存在误判斜体书写导致对角线之外的任何明显色块都值得关注5. 模型部署与应用实践5.1 模型保存与加载的完整流程不同保存方式的适用场景# 保存整个模型架构权重优化器状态 model_b.save(full_model.h5) # HDF5格式 # 仅保存架构 with open(model_architecture.json, w) as f: f.write(model_b.to_json()) # 仅保存权重 model_b.save_weights(model_weights.h5) # 加载方式对比 from tensorflow.keras.models import load_model, model_from_json # 加载完整模型 loaded_model load_model(full_model.h5) # 从JSON重建架构 with open(model_architecture.json, r) as f: rebuilt_model model_from_json(f.read()) rebuilt_model.load_weights(model_weights.h5)格式选择建议训练中途检查点用ModelCheckpoint保存.h5生产环境部署推荐SavedModel格式model.save(dir)跨平台共享JSON权重组合最灵活5.2 实现实时手写数字识别用OpenCV搭建端到端应用import cv2 import numpy as np def preprocess_image(image): 将摄像头捕获的图像处理为模型输入格式 gray cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) resized cv2.resize(gray, (28,28), interpolationcv2.INTER_AREA) inverted 255 - resized # 模拟MNIST白底黑字 return inverted.reshape(1,28,28,1).astype(float32) / 255 # 摄像头捕获循环 cap cv2.VideoCapture(0) while True: ret, frame cap.read() processed preprocess_image(frame) pred model_b.predict(processed).argmax() # 显示预测结果 cv2.putText(frame, fPred: {pred}, (50,50), cv2.FONT_HERSHEY_SIMPLEX, 2, (0,255,0), 3) cv2.imshow(Digit Classifier, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()部署注意事项输入图像必须与训练数据预处理方式完全一致实时应用要考虑predict的延迟CNN模型通常100ms添加置信度阈值过滤低质量输入np.max(predictions) 0.86. 项目进阶方向6.1 数据增强提升泛化能力使用Keras的ImageDataGenerator实现实时增强from tensorflow.keras.preprocessing.image import ImageDataGenerator datagen ImageDataGenerator( rotation_range15, # 随机旋转角度 width_shift_range0.1, # 水平平移 height_shift_range0.1, zoom_range0.1 # 随机缩放 ) # 生成增强后的图像示例 augmented datagen.flow(train_images[:1], batch_size1) plt.figure(figsize(10,5)) for i in range(5): plt.subplot(1,5,i1) plt.imshow(augmented.next()[0].reshape(28,28), cmapgray) plt.axis(off) plt.show()增强策略选择手写数字适合小角度旋转20度和平移避免垂直翻转数字6和9会互变谨慎使用亮度调整MNIST已是二值化图像6.2 使用预训练模型进行迁移学习虽然MNIST不需要但掌握该方法对真实项目至关重要from tensorflow.keras.applications import VGG16 # 加载预训练模型去掉顶层 base_model VGG16(weightsimagenet, include_topFalse, input_shape(48,48,3)) # 调整输入尺寸 # 冻结卷积层 for layer in base_model.layers: layer.trainable False # 添加自定义分类层 model Sequential([ base_model, Flatten(), Dense(256, activationrelu), Dense(10, activationsoftmax) ]) # 需要先将MNIST调整为3通道 train_images_rgb np.repeat(train_images, 3, axis-1) test_images_rgb np.repeat(test_images, 3, axis-1)迁移学习心得小数据集1万样本建议冻结所有预训练层中等数据集1-10万可微调最后几个卷积块输入尺寸必须与预训练模型一致可用插值调整7. 常见错误与调试技巧7.1 维度不匹配问题排查遇到ValueError: Input 0 is incompatible with layer...时的检查清单检查input_shape是否与首层定义一致验证数据reshape是否正确特别是通道顺序使用model.summary()对比各层输出维度在fit()之前用model.predict(train_images[:1])测试单样本7.2 训练不收敛的解决方案当loss居高不下或准确率随机波动时检查数据归一化确保输入在[0,1]或[-1,1]范围调整学习率尝试Adam(learning_rate0.001)默认值可能不适合验证标签编码分类任务必须one-hot回归任务需标准化标签简化网络结构先用一个隐藏层确保基础通路正常7.3 GPU内存不足的处理遇到CUDA out of memory错误时的对策减小batch_size通常减半尝试使用model.fit(..., steps_per_epochlen(train_images)//batch_size)在代码开头添加内存增长配置gpus tf.config.experimental.list_physical_devices(GPU) if gpus: try: for gpu in gpus: tf.config.experimental.set_memory_growth(gpu, True) except RuntimeError as e: print(e)8. 项目扩展与生产化建议8.1 从MNIST到真实数据集过渡到CIFAR-10的注意事项输入维度变为32x32x3需调整网络输入层数据增强更加重要颜色抖动、水平翻转等需要更深的网络如ResNet20才能获得好效果学习率可能需要重新调整8.2 模型量化与加速使用TensorFlow Lite部署到移动端converter tf.lite.TFLiteConverter.from_keras_model(model_b) tflite_model converter.convert() # 保存量化模型 with open(model.tflite, wb) as f: f.write(tflite_model) # 在Raspberry Pi等设备上加载 interpreter tf.lite.Interpreter(model_pathmodel.tflite) interpreter.allocate_tensors()量化策略选择动态范围量化保持75%精度缩小4倍体积全整数量化需要代表性数据集校准最高压缩率8.3 监控与持续改进生产环境必备的监控指标预测延迟99分位线应300ms实时应用数据漂移检测统计输入数据的均值/方差变化概念漂移检测跟踪准确率随时间下降趋势异常输入检测识别对抗样本或无效数据实现示例# 记录每次预测的元数据 import time def predict_with_monitoring(input_data): start time.time() pred model.predict(input_data) latency (time.time() - start) * 1000 # 毫秒 # 记录到监控系统如Prometheus record_metrics({ latency_ms: latency, confidence: np.max(pred), input_mean: np.mean(input_data) }) return pred
Keras深度学习入门:从MNIST分类到模型部署实战
1. 项目概述为什么选择Keras开启深度学习之旅作为TensorFlow的高层APIKeras以其极简的接口设计成为深度学习入门的最佳入口。我至今记得第一次用Keras训练MNIST分类器时仅用20行代码就实现了98%准确率的震撼——这比当年用纯NumPy实现神经网络节省了至少90%的调试时间。对于刚接触深度学习的开发者Keras的三大优势尤为关键模块化设计像搭积木一样组合网络层Dense、Conv2D等层的参数命名直观到几乎不需要查文档跨后端支持既可基于TensorFlow运行也能切换Theano或CNTK虽然现在基本都用TF了生产就绪从Google的官方推荐到Kaggle竞赛中的高频出现学会Keras等于掌握工业级工具重要提示虽然Keras现在已整合为tf.keras但本文示例同时兼容独立Keras库和TensorFlow 2.x内置版本。建议新手直接使用TensorFlow 2.3环境。2. 环境配置与工具选型2.1 基础软件栈的黄金组合在我的教学实践中以下环境配置方案成功率最高# 使用conda创建虚拟环境比venv更适合科学计算 conda create -n keras_env python3.8 conda activate keras_env # 安装GPU版本需先配置CUDA/cuDNN pip install tensorflow-gpu2.6 keras matplotlib jupyter关键工具选型理由Python 3.8在ABI兼容性和新特性间取得平衡的稳定版本TensorFlow-gpu 2.6长期支持版本CUDA兼容性最广Jupyter Lab比Notebook更现代的交互环境适合调试网络结构2.2 验证GPU加速是否生效跑这段代码检查CUDA是否被正确调用import tensorflow as tf print(GPU可用:, tf.config.list_physical_devices(GPU)) print(TF版本:, tf.__version__) print(Keras版本:, tf.keras.__version__)典型输出应包含类似信息GPU可用: [PhysicalDevice(name/physical_device:GPU:0, device_typeGPU)] TF版本: 2.6.0 Keras版本: 2.6.03. 第一个端到端图像分类项目3.1 MNIST数据集预处理技巧虽然MNIST被视为深度学习的Hello World但正确处理数据仍有许多门道from tensorflow.keras.datasets import mnist # 加载数据时的关键参数 (train_images, train_labels), (test_images, test_labels) mnist.load_data( pathmnist.npz # 指定本地缓存路径避免重复下载 ) # 归一化的最佳实践 train_images train_images.reshape((60000, 28, 28, 1)).astype(float32) / 255 test_images test_images.reshape((10000, 28, 28, 1)).astype(float32) / 255 # 标签one-hot编码的陷阱 train_labels tf.keras.utils.to_categorical(train_labels, num_classes10) test_labels tf.keras.utils.to_categorical(test_labels, num_classes10)为什么这样处理reshape添加了通道维度28,28,1是为了兼容CNN输入要求除以255的归一化放在reshape之后可避免整数除法精度丢失to_categorical必须指定num_classes否则遇到未出现标签时会出错3.2 网络架构设计哲学对比三种典型结构的优劣from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Dense, Flatten, Conv2D, MaxPooling2D # 方案A全连接网络基准模型 model_a Sequential([ Flatten(input_shape(28, 28, 1)), Dense(128, activationrelu), Dense(10, activationsoftmax) ]) # 方案B浅层CNN model_b Sequential([ Conv2D(32, (3,3), activationrelu, input_shape(28,28,1)), MaxPooling2D((2,2)), Flatten(), Dense(10, activationsoftmax) ]) # 方案C深度CNN适合复杂任务 model_c Sequential([ Conv2D(32, (3,3), activationrelu, paddingsame, input_shape(28,28,1)), Conv2D(64, (3,3), activationrelu), MaxPooling2D((2,2)), Flatten(), Dense(128, activationrelu), Dense(10, activationsoftmax) ])架构选择指南全连接网络参数量大如model_a有101,770个参数适合教学演示但实际很少用浅层CNNmodel_b在MNIST上就能达到99%准确率是性价比最高的选择深度CNNmodel_c在简单数据集上容易过拟合但可学习更复杂的特征3.3 训练过程的实战细节编译和训练模型时这些参数最值得关注model_b.compile( optimizeradam, # 比SGD更省心的自适应优化器 losscategorical_crossentropy, # 多分类标准损失 metrics[accuracy, # 主指标 tf.keras.metrics.Precision(), # 添加次要指标 tf.keras.metrics.Recall()] ) history model_b.fit( train_images, train_labels, epochs10, batch_size64, # 通常取2的幂次 validation_split0.2, # 自动从训练集划分验证集 callbacks[ tf.keras.callbacks.EarlyStopping(patience2), # 早停防过拟合 tf.keras.callbacks.ModelCheckpoint(best_model.h5) # 保存最佳模型 ] )参数调优经验batch_size越大训练越快但可能影响收敛性64是常用起点验证集比例设为0.1-0.2足够小数据集可适当减小EarlyStopping的patience设为总epoch数的1/5到1/34. 模型评估与性能优化4.1 解读训练过程可视化用Matplotlib绘制训练曲线时要注意这些细节import matplotlib.pyplot as plt def plot_history(history): fig, (ax1, ax2) plt.subplots(1, 2, figsize(12, 4)) # 准确率曲线 ax1.plot(history.history[accuracy], labeltrain) ax1.plot(history.history[val_accuracy], labelvalidation) ax1.set_title(Model accuracy) ax1.set_ylabel(Accuracy) ax1.set_xlabel(Epoch) ax1.legend() # 损失曲线 ax2.plot(history.history[loss], labeltrain) ax2.plot(history.history[val_loss], labelvalidation) ax2.set_title(Model loss) ax2.set_ylabel(Loss) ax2.set_xlabel(Epoch) ax2.legend() plt.show() plot_history(history)曲线分析要点理想情况两条线接近且同步下降说明无过拟合训练线远高于验证线明显过拟合需增加Dropout层或数据增强验证线波动剧烈可能batch_size太小或学习率太高4.2 混淆矩阵深度分析超越准确率指标用混淆矩阵发现模型弱点from sklearn.metrics import confusion_matrix import seaborn as sns # 生成预测结果 y_pred model_b.predict(test_images).argmax(axis1) y_true test_labels.argmax(axis1) # 绘制混淆矩阵 cm confusion_matrix(y_true, y_pred) plt.figure(figsize(10,8)) sns.heatmap(cm, annotTrue, fmtd, cmapBlues) plt.xlabel(Predicted) plt.ylabel(True) plt.show()典型发现问题数字4和9容易混淆手写体相似数字1和7存在误判斜体书写导致对角线之外的任何明显色块都值得关注5. 模型部署与应用实践5.1 模型保存与加载的完整流程不同保存方式的适用场景# 保存整个模型架构权重优化器状态 model_b.save(full_model.h5) # HDF5格式 # 仅保存架构 with open(model_architecture.json, w) as f: f.write(model_b.to_json()) # 仅保存权重 model_b.save_weights(model_weights.h5) # 加载方式对比 from tensorflow.keras.models import load_model, model_from_json # 加载完整模型 loaded_model load_model(full_model.h5) # 从JSON重建架构 with open(model_architecture.json, r) as f: rebuilt_model model_from_json(f.read()) rebuilt_model.load_weights(model_weights.h5)格式选择建议训练中途检查点用ModelCheckpoint保存.h5生产环境部署推荐SavedModel格式model.save(dir)跨平台共享JSON权重组合最灵活5.2 实现实时手写数字识别用OpenCV搭建端到端应用import cv2 import numpy as np def preprocess_image(image): 将摄像头捕获的图像处理为模型输入格式 gray cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) resized cv2.resize(gray, (28,28), interpolationcv2.INTER_AREA) inverted 255 - resized # 模拟MNIST白底黑字 return inverted.reshape(1,28,28,1).astype(float32) / 255 # 摄像头捕获循环 cap cv2.VideoCapture(0) while True: ret, frame cap.read() processed preprocess_image(frame) pred model_b.predict(processed).argmax() # 显示预测结果 cv2.putText(frame, fPred: {pred}, (50,50), cv2.FONT_HERSHEY_SIMPLEX, 2, (0,255,0), 3) cv2.imshow(Digit Classifier, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()部署注意事项输入图像必须与训练数据预处理方式完全一致实时应用要考虑predict的延迟CNN模型通常100ms添加置信度阈值过滤低质量输入np.max(predictions) 0.86. 项目进阶方向6.1 数据增强提升泛化能力使用Keras的ImageDataGenerator实现实时增强from tensorflow.keras.preprocessing.image import ImageDataGenerator datagen ImageDataGenerator( rotation_range15, # 随机旋转角度 width_shift_range0.1, # 水平平移 height_shift_range0.1, zoom_range0.1 # 随机缩放 ) # 生成增强后的图像示例 augmented datagen.flow(train_images[:1], batch_size1) plt.figure(figsize(10,5)) for i in range(5): plt.subplot(1,5,i1) plt.imshow(augmented.next()[0].reshape(28,28), cmapgray) plt.axis(off) plt.show()增强策略选择手写数字适合小角度旋转20度和平移避免垂直翻转数字6和9会互变谨慎使用亮度调整MNIST已是二值化图像6.2 使用预训练模型进行迁移学习虽然MNIST不需要但掌握该方法对真实项目至关重要from tensorflow.keras.applications import VGG16 # 加载预训练模型去掉顶层 base_model VGG16(weightsimagenet, include_topFalse, input_shape(48,48,3)) # 调整输入尺寸 # 冻结卷积层 for layer in base_model.layers: layer.trainable False # 添加自定义分类层 model Sequential([ base_model, Flatten(), Dense(256, activationrelu), Dense(10, activationsoftmax) ]) # 需要先将MNIST调整为3通道 train_images_rgb np.repeat(train_images, 3, axis-1) test_images_rgb np.repeat(test_images, 3, axis-1)迁移学习心得小数据集1万样本建议冻结所有预训练层中等数据集1-10万可微调最后几个卷积块输入尺寸必须与预训练模型一致可用插值调整7. 常见错误与调试技巧7.1 维度不匹配问题排查遇到ValueError: Input 0 is incompatible with layer...时的检查清单检查input_shape是否与首层定义一致验证数据reshape是否正确特别是通道顺序使用model.summary()对比各层输出维度在fit()之前用model.predict(train_images[:1])测试单样本7.2 训练不收敛的解决方案当loss居高不下或准确率随机波动时检查数据归一化确保输入在[0,1]或[-1,1]范围调整学习率尝试Adam(learning_rate0.001)默认值可能不适合验证标签编码分类任务必须one-hot回归任务需标准化标签简化网络结构先用一个隐藏层确保基础通路正常7.3 GPU内存不足的处理遇到CUDA out of memory错误时的对策减小batch_size通常减半尝试使用model.fit(..., steps_per_epochlen(train_images)//batch_size)在代码开头添加内存增长配置gpus tf.config.experimental.list_physical_devices(GPU) if gpus: try: for gpu in gpus: tf.config.experimental.set_memory_growth(gpu, True) except RuntimeError as e: print(e)8. 项目扩展与生产化建议8.1 从MNIST到真实数据集过渡到CIFAR-10的注意事项输入维度变为32x32x3需调整网络输入层数据增强更加重要颜色抖动、水平翻转等需要更深的网络如ResNet20才能获得好效果学习率可能需要重新调整8.2 模型量化与加速使用TensorFlow Lite部署到移动端converter tf.lite.TFLiteConverter.from_keras_model(model_b) tflite_model converter.convert() # 保存量化模型 with open(model.tflite, wb) as f: f.write(tflite_model) # 在Raspberry Pi等设备上加载 interpreter tf.lite.Interpreter(model_pathmodel.tflite) interpreter.allocate_tensors()量化策略选择动态范围量化保持75%精度缩小4倍体积全整数量化需要代表性数据集校准最高压缩率8.3 监控与持续改进生产环境必备的监控指标预测延迟99分位线应300ms实时应用数据漂移检测统计输入数据的均值/方差变化概念漂移检测跟踪准确率随时间下降趋势异常输入检测识别对抗样本或无效数据实现示例# 记录每次预测的元数据 import time def predict_with_monitoring(input_data): start time.time() pred model.predict(input_data) latency (time.time() - start) * 1000 # 毫秒 # 记录到监控系统如Prometheus record_metrics({ latency_ms: latency, confidence: np.max(pred), input_mean: np.mean(input_data) }) return pred