尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Keras-bert模型部署实战:用Flask封装文本分类预测服务
模型预测跑通只是第一步把模型交到别人手里才算真上线。这篇Keras入门第七篇我用Flask把Keras-bert训练出来的文本分类模型包成一个HTTP预测服务让业务方直接通过接口调用而不是把模型文件丢过去让人家自己跑脚本。适合已经训好模型、想快速提供预测接口的同学也适合刚接触服务化部署、想搞懂整套流程的人。接口服务这事听起来不复杂但真正落地时会遇到一堆“训练时没想过”的问题模型怎么加载一次、请求来了怎么传参、并发上来会不会崩、报错怎么回给调用方。这篇我会从方案选型、模型组装、接口实现到部署排查完整走一遍写到能直接抄作业的程度。1. 为什么非要用Flask包一层服务先说清楚这件事的起点。我手头有一个用Keras-bert训练好的文本分类模型权重文件、token字典、类别映射都在。模型在Jupyter里验证过预测效果没问题但业务方不可能打开Notebook去调模型他们需要的是一句话给我个接口我传文本进去你把分类结果返回给我。直接把模型文件丢给对方肯定不行对方没有Keras环境没有GPU甚至不一定装了Python。就算理论上能跑也没人愿意为了一个分类功能去维护一套深度学习环境。用Flask包成HTTP服务等于把模型封装成一个黑盒调用方只需要发一个POST请求拿JSON响应其他什么都不用管。为什么最后选Flask而不是别的方案我列过一张对比表方案上手成本可控性适用场景Flask低几行路由就能跑高代码完全自己掌控中小模型、内部服务、快速上线TensorFlow Serving高需要熟悉新配置协议中很多行为框架决定大规模、多模型、生产级ServingFastAPI低自带异步和文档高接口性能要求高、喜欢自动生成文档的团队选Flask不是因为别的方案不好而是这个场景下它最合适。TensorFlow Serving确实强大但为了一个分类模型去搭一套独立服务前期投入太大配置项多排查问题也更绕。FastAPI同样优秀不过如果团队里其他人更熟悉Flask维护成本会更低而且预测服务瓶颈通常在模型推理不在HTTP框架本身Flask在单机小并发场景下完全够用。Flask还有一个隐性优势就是和Keras的Python生态天然贴合。模型加载、预处理、预测、后处理可以写在同一个进程里不用跨语言、跨协议去绕。代码结构保持简单出问题的时候打开日志就知道卡在哪一步。这个方案适合什么体量的项目我实践下来日请求量几千到几万、单次推理耗时几百毫秒以内的文本分类服务Flask加多进程部署完全hold住。真正到了每天百万级请求、需要弹性伸缩的时候再考虑更重的服务化方案不迟。2. Keras-bert模型构建的核心细节2.1 tokenizer是第一个不能出错的环节Keras-bert的模型输入不是原始字符串而是经过切词、转id、分段之后的一组张量。很多人刚接触BERT类模型最容易在这个环节踩坑。BERT的输入通常包含两部分Token ids和Segment ids。Token ids把句子切分成词或子词单元每个单元对应一个编号Segment ids用来区分两个句子比如文本对任务里第一句标记为0第二句标记为1。做单文本分类时整个句子都标记为0。Keras-bert里Tokenizer的核心代码逻辑大概是这样的from keras_bert import Tokenizer token_dict token_dict # 训练时用的字典 tokenizer Tokenizer(token_dict) # 单条文本转输入 text 这部电影真的很精彩 result tokenizer.encode(firsttext, max_len128)这里的max_len要和训练模型时保持一致BERT类模型对输入长度有固定上限超过部分会被截断。如果训练时用的是128预测时却用了64相当于模型看到的文本信息变少了效果会出现肉眼可见的下降。Tokenizer的字典必须和加载模型时配套。BERT预训练模型自带一个词表但如果你自己在业务数据上fine-tune过新增了业务词汇就得保证预测时用的字典是训练后保存的那份。最稳妥的做法是在训练阶段结束后把token字典保存到文件部署时直接读这个文件不要自己重新构建。这里我吃过一次亏训练时用了包含业务词汇的扩展字典部署时图省事直接加载了原版BERT词表结果模型预测出来的结果完全不对概率分布乱成一团。排查了很久才发现是字典不一致导致的。2.2 把BERT层组装成下游分类模型Keras-bert提供了加载预训练权重并转成Keras Layer的方法。单文本分类任务里标准的组装方式是在BERT输出上接取第一个Token位置的特征也就是[CLS]位置的输出然后接全连接层和Softmax。from keras.models import Model from keras.layers import Input, Dense, Lambda from keras_bert import load_trained_model_from_checkpoint # 加载预训练BERT模型 bert_model load_trained_model_from_checkpoint( config_file, checkpoint_file, seq_len128 ) # 取BERT输出的CLS位置向量 cls_output Lambda(lambda x: x[:, 0], namecls_output)(bert_model.output) # 下游分类层 dense Dense(units128, activationrelu)(cls_output) output Dense(unitsnum_classes, activationsoftmax)(dense) model Model(bert_model.inputs, output)注意这里不是直接拿bert_model.output去接全连接BERT输出的形状是(batch_size, seq_len, hidden_size)序列每个位置都有一个向量。文本分类任务里通常把第一个位置CLS的向量作为整句表示所以要用Lambda层把它抽出来。如果你的下游任务不是句子分类而是序列标注比如实体识别那就不用取CLS而是对每个位置的输出都接一层分类这是后话场景不同结构也不同。多分类和多标签任务的输出层也有区别。多分类用Softmax所有类别概率和为1多标签场景每个类别独立判断一般用Sigmoid。训练时的损失函数也要对应调整多分类用categorical_crossentropy多标签常用binary_crossentropy。2.3 加载权重的坑pickle与自定义层Keras模型训练完之后保存和加载也有不少讲究。BERT模型本身结构复杂如果只是简单调用model.save(model.h5)加载时通常没问题但如果下游结构里用了自定义Layer比如自己写的Attention层或Pooling层直接加载就会报错提示找不到自定义层。解决办法有两个一是在加载前注册自定义层from keras.utils.generic_utils import get_custom_objects get_custom_objects().update({AttentionLayer: AttentionLayer})二是保存的时候不要保存模型结构只保存权重加载时先搭好结构再load weights。我习惯用第二种因为结构代码本身就是确定的不用依赖序列化出来的架构出问题更少排查也容易。model.load_weights(model_weights.h5)权重的保存还有一个细节Keras-bert加载的BERT层参数名有固定前缀保存权重文件时这些前缀会原样存下来。加载时模型的层名必须和保存时一致否则load_weights会提示有些权重没匹配上。如果你在搭建模型时给BERT层的名字改了权重加载就会失败这也是个隐蔽的坑。3. Flask预测服务完整实现3.1 工程结构先行设计部署代码不要全堆在一个文件里我习惯按这个结构组织. ├── app.py ├── model.py ├── requirements.txt ├── models/ │ ├── bert_config.json │ ├── bert_model.ckpt.index │ ├── bert_model.ckpt.data-00000-of-00001 │ ├── token_dict.json │ └── class_to_idx.jsonmodel.py负责模型加载和预测逻辑app.py只负责HTTP接口models目录放权重和配置文件。这样职责分开后续如果要做模型热更新只需要替换models目录下的文件再重载模型不动路由代码。class_to_idx.json是类别映射文件内容是{0: 负面, 1: 正面}这种格式。训练时模型输出的数字类别接口返回时要把数字转成人类可读的标签这个映射文件在部署阶段不可或缺。3.2 model.py模型封装类封装的思想很简单模型加载只做一次后续请求都复用同一个模型实例。第一次加载很慢因为BERT权重文件通常几百MB还要初始化图结构如果每次请求都加载一次服务基本没法用。import json import numpy as np from keras_bert import Tokenizer, load_trained_model_from_checkpoint class BertClassifier: def __init__(self, config_path, checkpoint_path, token_dict_path, class_map_path, max_len128): self.max_len max_len self.tokenizer Tokenizer(json.load(open(token_dict_path, r, encodingutf-8))) self.class_map json.load(open(class_map_path, r, encodingutf-8)) # BERT模型加载一次到位 self.model load_trained_model_from_checkpoint( config_fileconfig_path, checkpoint_filecheckpoint_path, seq_lenmax_len ) # 加载下游训练好的分类权重 # 注意这里假定model.py里已经搭好了分类模型结构 # 如果单独保存了权重则把load_weights注释打开 def predict(self, text): # 文本转模型输入 token_ids, segment_ids self.tokenizer.encode( firsttext, max_lenself.max_len ) inputs [ np.array([token_ids]), np.array([segment_ids]) ] # 预测 probs self.model.predict(inputs)[0] pred_idx int(np.argmax(probs)) confidence float(probs[pred_idx]) label self.class_map.get(str(pred_idx), str(pred_idx)) return { label: label, confidence: confidence, prob: [round(float(p), 6) for p in probs] }这里有几个关键点值得说明。第一tokenizer.encode返回的token_ids和segment_ids形状都是(seq_len,)进入模型前要包一层变成(1, seq_len)因为模型推理是按batch维度处理的。即使只有一条文本也要保留batch维度。第二model.predict返回的概率数组形状是(1, num_classes)所以取[0]拿到第一条样本的结果。第三confidence只取最大概率值prob返回完整概率分布。接口返回完整概率是有意义的调用方可能不只是想拿一个类别还想知道模型对新类别的置信度或者做阈值判断。比如某条文本预测为A类的概率是0.43调用方可以把该条文本标记为“待人工确认”而不是硬着头皮接受分类结果。3.3 app.py路由与请求处理from flask import Flask, request, jsonify from model import BertClassifier app Flask(__name__) # 启动时加载模型全局复用 model BertClassifier( config_pathmodels/bert_config.json, checkpoint_pathmodels/bert_model.ckpt, token_dict_pathmodels/token_dict.json, class_map_pathmodels/class_to_idx.json, max_len128 ) app.route(/health, methods[GET]) def health(): return jsonify({status: ok}) app.route(/predict, methods[POST]) def predict(): try: data request.get_json(forceTrue) text data.get(text, ).strip() if not text: return jsonify({error: text不能为空}), 400 result model.predict(text) return jsonify(result) except Exception as e: app.logger.error(predict error: %s, e) return jsonify({error: internal error}), 500 if __name__ __main__: app.run(host0.0.0.0, port5000, threadedTrue)路由设计得很简单/predict接收JSON格式的POST请求/health做存活检查。生产部署时负载均衡器或者监控系统会定时请求健康检查接口如果模型挂了或者服务起不来这个接口要能及时反馈。再说说几个容易忽略的点。一是request.get_json(forceTrue)。部分调用方发请求时没有正确设置Content-Type: application/json加了forceTrue可以强制按JSON解析避免因为请求头问题报错。二是空文本的处理。很多线上事故都是空字符串、纯空格或者None引发的输入校验放第一层比模型内部去兜底要省事很多。三是异常捕获要写在业务逻辑外层。模型预测阶段如果出错不能直接把堆栈抛给调用方应返回一个统一格式的错误JSON同时把详细堆栈记到服务端日志里。调用方不需要知道你的模型内部发生了什么只需要知道接口失败了、原因是什么。返回错误信息尽量中立一些不要暴露太多内部实现细节。启动服务之后先用curl验证curl -X POST http://127.0.0.1:5000/predict \ -H Content-Type: application/json \ -d {text: 这部电影真的很精彩}返回结果长这样{ label: 正面, confidence: 0.992, prob: [0.003, 0.992] }4. 部署中的性能与稳定性问题4.1 模型加载慢和显存占用不是玄学BERT模型文件动辄几百MB加载一次可能需要几十秒甚至更久。第一次启动服务时健康检查接口可能在模型还没加载完时就已经被调到这时候返回的status应该是什么我建议健康检查只检查进程是否存活不检查模型是否ready避免启动后几十秒内被误认为服务异常而反复重启。显存占用问题在预测阶段也很现实。BERT模型加载后占用的显存远大于单次预测时的实际需求因为模型默认按最大序列长度分配了临时存储。如果GPU显存不大而部署机器上还跑着其他任务建议在加载模型前显式限制GPU显存增长或者在只需要少量并发预测时直接用CPU推理。实际项目里我遇到过这样的情况训练时用的GPU机器有16G显存部署机器只有8G显存模型加载直接OOM。最后排查发现训练机器显存大跑起来没事但同一模型在8G显存上加载后前向推理还要额外的激活值和临时张量空间。解决办法是换机器或者降低max_len重新导出模型。所以建议在部署前先在目标机器上做一次加载和推理测试确认资源够用再上。4.2 并发性能与GIL是要讲清楚的Flask默认的多线程模式有个隐藏问题Python解释器的GIL导致多线程在CPU密集型任务上无法真正并行。模型推理是CPU密集操作开10个线程同时跑预测效果远不如想象中好。我自己实测同一个BERT模型单线程下QPS大约在50左右开8个线程后QPS只涨到70左右因为没有真正的并行计算线程切换成本反而拉低了整体效率。如果想真正提升并发上限推荐用多进程部署比如用gunicorn起多个worker每个worker是一个独立Python进程模型会在每个进程里各自加载一份。代价是内存占用成倍增加一个模型占2G内存4个worker就是8G部署前要确认内存够不够。另一个思路是维持单进程但把预测请求放到批量队列里每次取多条文本一起predict。因为Keras-bert底层batch预测比单条预测快很多把一条一条地for循环预测改成一次性传一个batch吞吐能明显提升。probs self.model.predict([batch_token_ids, batch_segment_ids])batch模式并不是要等请求达到一定数量才开始而是接收方把多条消息放队列里攒到一定阈值或到时间窗口再统一处理。这个方案适合调用方本身有批量请求的场景不太适合要求在几毫秒内快速响应的实时接口。4.3 服务重启与模型更新模型更新是部署绕不开的话题。业务方隔段时间就让重新训练模型新权重文件需要上线而不影响调用方。最简单的做法是停服务、换权重文件、重启服务小小地抖动一下如果内部服务可以接受这个方案最省事。如果要求不中断服务可以先加载新模型到另一个进程中等新模型加载完成后把流量切换过去老进程等正在处理的请求结束后再退出。实际操作中可以用一个配置开关控制当前要使用的模型路径切换时加载新实例把旧实例引用替换掉。不过这种实现要处理好并发访问模型实例在切换时可能有短暂的不一致需要考虑加锁或者用双buffer的方式代码复杂度会增加不少非必要先从简单方案做起。5. 常见报错排查速查表下面列几个我在实际部署中遇到的报错和定位思路给新手排障用。报错现象可能原因解决思路加载模型时提示找不到检查点文件checkpoint路径写错或使用了model.h5却用load checkpoint方式确认权重文件类型路径用绝对路径或者启动时打印当前目录预测结果概率分布均匀、明显不对tokenizer字典和训练时不一致用训练时保存的同款字典文件不要现场重新构建显存OOMBERT模型本身占用大批量预测临时张量过多减小batch_size限制显存增长或换CPU推理接口偶尔返回500日志里出现shape mismatch输入文本长度超出max_len导致编码异常确认tokenizer.encode里max_len与模型seq_len一致并发请求时有概率返回错误预测模型实例被多个线程同时调用部分底层图操作冲突使用Keras-bert加载模型后先跑一遍warmup必要时加锁保护预测方法中文字符变成乱码或报UnicodeDecodeError请求数据编码不统一或读取文件时encoding没指定所有文件读取都显式指定encodingutf-8请求端统一使用UTF-8这里面最容易被忽略的是“先跑一遍warmup”。BERT模型刚加载完第一次predict会触发一些图初始化和缓冲区申请耗时可能明显高于后续请求。如果不做warmup服务刚起来时的第一个请求可能超时被监控误判为服务不可用。在模型加载完成后调一次predict把耗时的初始化放在服务启动阶段而不是留给第一个真实请求这个小动作对稳定性帮助很大。另外一个坑是Keras和TensorFlow版本匹配问题。Keras-bert依赖的底层接口在不同版本下表现不太一样经常出现module tensorflow has no attribute get_default_session这类报错。遇到版本问题最快的方式是新建一个干净的虚拟环境按requirements.txt里固定的版本安装不要图省事在训练环境里直接跑服务训练环境里一堆自定义模块和实验残留很容易踩乱七八糟的坑。我的requirements.txt大概长这样仅供参考flask1.1.4 tensorflow1.15.0 keras2.3.1 keras-bert0.89.0为什么锁版本因为Keras-bert是一个比较早期的开源项目对TensorFlow 2.x的兼容性并不好如果不锁版本pip升级一下底层依赖服务可能直接跑不起来。6. 踩过的坑和一点实际心得整条链路走下来我印象最深的是类别映射文件的重要性。模型输出的永远是一个整数索引它本身没有业务含义。如果你在训练环境里用的是label_to_idx部署时忘了把idx_to_label同步过去接口返回的label全是数字调用方根本不知道那是什么意思。这个文件别看它小缺了它整个接口可用性直接归零。还有一点是关于日志的。预测服务上线后一定要记录每一条请求的耗时。我最初没在意后来业务方反馈某个时段接口响应特别慢我看日志也看不出原因因为没有记录耗时。后来加了一行start time.time() result model.predict(text) cost time.time() - start app.logger.info(predict text len%s cost%.3fs, len(text), cost)有了这个日志再遇到响应慢的问题就能快速判断是模型推理慢还是HTTP传输慢不用靠猜。模型预测服务的整个体检下来其实比训练模型本身更考验工程化思维。你要考虑的不是准确率而是服务能不能稳定跑、接口规不规范、错误返回是否合理、出了问题能不能快速定位。很多训练做得不错的同学第一次把模型接出去时都会被这些问题绊一下。希望这篇能帮你少走这些弯路把Keras-bert模型的最后一步落好。
RELATED

相关推荐

Win10+VS2019编译OSG 3.6.5与OSGEarth 3.1双版本开发库实战

Win10+VS2019编译OSG 3.6.5与OSGEarth 3.1双版本开发库实战

简介:面向Windows 10平台C三维图形开发者的预编译依赖包,基于Visual Studio 2019完整构建,集成OpenSceneGraph 3.6.5与OSGEarth 3.1两套核心库,省去自行编译的繁琐流程,可直接接入三维GIS、仿真可视化、虚拟训练等桌面…

📅 2026/10/17 17:55:12
SaaS平台降本增效全解析:从架构原理到选型避坑指南

SaaS平台降本增效全解析:从架构原理到选型避坑指南

1. 先搞清楚:SaaS平台到底是个什么东西这几年做企业服务和数字化转型的咨询,我微信里隔三差五就有人发来一句话:"帮我看看,我们公司要不要上SaaS?"问的人有创业公司老板,有传统企业的IT负责人&am…

📅 2026/10/17 17:55:12
Octop多任务调度架构:分而治之的自动化运维实践

Octop多任务调度架构:分而治之的自动化运维实践

1. 从“Octop”这个名字说起:它到底是什么第一次看到“Octop”这个词,很多人会下意识联想到章鱼(Octopus),脑子里浮现出八条触手各自独立又协同工作的画面。这个联想其实非常准确,因为在我实际接触和使用的…

📅 2026/10/17 17:55:12
MORE NEWS

更多资讯

📰

Kafka接入AI实战:从Lag排查到Function Calling自动化运维

1. 为什么要把Kafka接进AI:这个标题背后要解决的真实问题先聊点实在的。做Kafka运维和研发的朋友,应该都有过这种经历:半夜被ConsumerGroup的Lag告警叫起来,打开电脑连上跳板机,一条一条查kafka-consumer-groups.sh --…

📰

harness 工程化实践:用 AGENTS.md 与 Makefile 把 TaoToken 接入 CI 流水线

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

大模型推理-基于prefill和decode阶段特性,PD分离,势在必行!TaoToken统一Key下的PD分离部署大纲

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

2026年5月开源模型Token服务实测榜:TaoToken统一Key下DeepSeek/GLM/Kimi三层架构性能拆解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

Codex 下载与本地部署实战:从安装到运行全流程指南(TaoToken 统一 Key 接入版)

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

QML自制数字软键盘:向TextInput/TextField键入数字并实现左移右移删除

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬