Tensorflow Serving 模型部署指南

2023-11-17

由于python的灵活性和完备的生态库,使得其成为实现、验证ML算法的不二之选。但是工业界要将模型部署到生产环境上,需要考略性能问题,就不建议再使用python端的服务。这个从训练到部署的整个流程如下图所示:
在这里插入图片描述
基本可以把工作分为三块:

  • Saver端:模型的离线训练与导出
  • Serving端:模型加载与在线预测
  • Client端:构建请求

本文采用 Saver (python) + Serving (tensorflow serving) + Client (Java) 作为解决方案,记录线上模型部署流程。

1. Saver端:模型的离线训练与导出

部署模型第一步是将训练好的整个模型导出为一系列标准格式的文件,然后即可在不同的平台上部署模型文件。TensorFlow 使用 SavedModel(pb文件) 这一格式用于模型部署。与Checkpoint 不同,SavedModel 包含了一个 TensorFlow 程序的完整信息: 不仅包含参数的权值,还包含计算图。

SavedModel最终保存结果包含两部分saved_model.pb和variables文件夹。

1.1 saved_model 模型保存与载入

saved_model模块主要用于TensorFlow Serving,模型的保存主要基于
tf.saved_model.builder.SavedModelBuilder 方法。

参考博客:TensorFlow saved_model 模块

1.1.1 简单场景:模型保存

最简单的场景,只是 保存/载入模型。核心实现代码如下:

builder = tf.saved_model.builder.SavedModelBuilder(saved_model_path)
builder.add_meta_graph_and_variables(
    sess=sess,
    tags=['lstm_saved_model']
)
builder.save()

详细解读一下上述几行核心代码:

  1. 首先构造SavedModelBuilder对象,初始化方法只需要传入用于保存模型的目录名,目录不用预先创建。
  2. add_meta_graph_and_variables方法导入graph的信息以及变量,这个方法假设变量都已经初始化好了,对于每个SavedModelBuilder这个方法一定要执行一次用于导入第一个meta graph。
    sess参数:传入当前的session,包含了graph的结构与所有变量。
    tags参数:是给当前需要保存的meta graph一个标签,标签名可以自定义。在之后载入模型的时候,需要根据这个标签名去查找对应的MetaGraphDef。找不到就会报如RuntimeError: MetaGraphDef associated with tags ‘foo’ could not be found in SavedModel这样的错。标签也可以选用系统定义好的参数,如 tf.saved_model.tag_constants.SERVINGtf.saved_model.tag_constants.TRAINING
  3. save方法就是将模型序列化到指定目录底下。

保存好以后到saved_model_dir目录下,会有一个saved_model.pb文件以及variables文件夹。顾名思义,variables保存所有变量,saved_model.pb用于保存模型结构等信息。
在这里插入图片描述

1.1.2 简单场景:模型载入

保存完后,可以在本地载入pb模型,简单测试一下模型的是否正确。

meta_graph_def = tf.saved_model.loader.load(
    sess, 
    ['lstm_saved_model'], 
    saved_model_path
)

第一个参数就是当前的session,第二个参数是在保存的时候定义的meta graph的标签,标签一致才能找到对应的meta graph。第三个参数就是模型保存的目录。load完以后,也是从sess对应的graph中获取需要的tensor来inference。

def test_saved_model_pb(saved_model_path):
    
    test_graph = tf.Graph()
    
    with tf.Session(graph=test_graph) as test_sess:
        
        # 调用 tf.saved_model.loader.load 恢复出meta_graph
        meta_graph_def = tf.saved_model.loader.load(
            sess=test_sess, 
            tags=['lstm_saved_model'],  
            export_dir=saved_model_path
        )
        
        # 从sess对应的graph中获取需要的tensor
        inputXX = test_sess.graph.get_tensor_by_name('inputXX:0')       
        y_pred = test_sess.graph.get_tensor_by_name('ypred:0')
        
        # 加载测试数据
        test_X = ...
        
        # 得到测试结果
        test_output = test_sess.run([y_pred], feed_dict = {inputXX: test_X})
1.1.3 使用SignatureDef:模型保存

传统的tensor导入需要用get_tensor_by_name , 这样就需要一一记住构建阶段tensor的name,很麻烦。

在下面的代码中,我们使用了SignatureDef,将输入输出tensor的信息都进行了封装,并且给他们一个自定义的别名。所以在构建模型的阶段,可以随便给tensor命名,只要在保存训练好的模型的时候,在SignatureDef中给出统一的别名即可。这样,输入输出tensor的具体名称已经完全隐藏,从而实现了创建模型与使用模型的解耦。

第一步:构建 Signature,下面给出构建的两种方式:

### 方法一
# 构建两个字典,inputs 和 outputs,把要存入的变量放入其中
inputs = {'input_XX': tf.saved_model.utils.build_tensor_info(inputX)}
outputs = {'pred_YY': tf.saved_model.utils.build_tensor_info(y_pred)}

# 构建 Signature
signature = tf.saved_model.signature_def_utils.build_signature_def(
    inputs=inputs,
    outputs=outputs,
    method_name=signature_key
)
### 方法二
# 构建需要在新会话中恢复的变量的 TensorInfo
x_tensor_info = tf.saved_model.utils.build_tensor_info(inputX)
y_tensor_info = tf.saved_model.utils.build_tensor_info(y_pred)

# 构建 Signature
signature = tf.saved_model.signature_def_utils.build_signature_def(
    inputs = {'input_XX': x_tensor_info},
    outputs = {'pred_YY': y_tensor_info},
    method_name = signature_key
)   

第二步:构造SavedModelBuilder对象,导入graph的信息以及变量,并将模型序列化到指定目录底下。核心代码:

signature_key = 'lstm_signature'

# 构造SavedModelBuilder对象
builder = tf.saved_model.builder.SavedModelBuilder(saved_model_path)
# 导入graph的信息以及变量,并以signature的形式添加要存储的变量
builder.add_meta_graph_and_variables(
    sess=sess,
    tags=['lstm_saved_model'], # tags可以自定义,也可以使用预定义值: tags=[tf.saved_model.tag_constants.TRAINING]
    signature_def_map={signature_key: signature} ,
    clear_devices=True
)
# 将模型序列化到指定目录底下
builder.save()

还是考虑基于LSTM的预测模型,完整版代码如下:

def model_train_saved_model_pb(train_X, train_Y, FLAGS, saved_model_path):
    """
    训练模型;基于saved_model方法将模型保存为pb文件形式
    param:
        train_X: 训练数据X,shape=[train_len, seq_len, num_nodes]
        train_Y: 训练数据Y,shape=[train_len, pre_len, num_nodes]
        FLAGS: 命令行参数
        saved_model_path: 基于saved_model方法所pb文件的路径;可以是一个不存在的目录
    """
    
    # parameters
    seq_len = FLAGS.seq_len
    pre_len = FLAGS.pre_len
    batch_size = FLAGS.batch_size
    lr = FLAGS.learning_rate
    training_epoch = FLAGS.training_epoch
    num_units = FLAGS.num_units
    
    # placeholders
    inputX = tf.placeholder(tf.float32, shape=[None, seq_len, num_nodes], name='inputXX') 
    labelY = tf.placeholder(tf.float32, shape=[None, pre_len, num_nodes], name='labelYY') 
    
    weight_out = {'out': tf.Variable(tf.random_normal([num_units, pre_len], mean=1.0), name='weight_out')}
    bias_out  = {'out': tf.Variable(tf.random_normal([pre_len]), name='bias_out')}
    
    # traffic prediction model
    y_pred, _, _ = lstm_prediction(inputX, weight_out, bias_out) #  y_pred.name = 'ypred'

    
    # model loss
    lambda_loss = 0.0015
    label = tf.reshape(labelY, [-1, num_nodes])
    loss = tf.reduce_mean(tf.nn.l2_loss(y_pred-label), name='loss')  
    
    # error
    error = tf.sqrt(tf.reduce_mean(tf.square(y_pred-label)), name='error') 
    
    # optimizer
    optimizer = tf.train.AdamOptimizer(lr).minimize(loss) # ADAM

    # saver 
    saver = tf.train.Saver(tf.global_variables())  

    # start session 
    with tf.Session() as sess:
        sess.run(tf.global_variables_initializer())
        
        batch_loss, batch_rmse = [], []
        time_start = time.time()
        
        for epoch in range(training_epoch):   
            for m in range(total_batch):
                mini_batch = train_X[m*batch_size : (m+1)*batch_size]
                mini_label = train_Y[m*batch_size : (m+1)*batch_size]
                
                _, loss1, rmse1, train_output = sess.run([optimizer, loss, error, y_pred], 
                                                         feed_dict={inputX: mini_batch, labelY: mini_label})    
                batch_loss.append(loss1)
                batch_rmse.append(rmse1) 
            print('Iter:{}'.format(epoch),  
                  'train_loss:{:.4}'.format(batch_loss[-1]),
                  'train_rmse:{:.4}'.format(batch_rmse[-1])) 
   
        # saved_model 开始,定义 signature_key
        signature_key = 'lstm_signature'
        
        
#         # 构建需要在新会话中恢复的变量的 TensorInfo
#         x_tensor_info = tf.saved_model.utils.build_tensor_info(inputX)
#         y_tensor_info = tf.saved_model.utils.build_tensor_info(y_pred)
        
#         # 构建 Signature
#         signature = tf.saved_model.signature_def_utils.build_signature_def(
#             inputs = {'input_XX': x_tensor_info},
#             outputs = {'pred_YY': y_tensor_info},
#             method_name = signature_key
#         )        
        
        # 构建两个字典,inputs 和 outputs,把要存入的变量放入其中
        inputs = {'input_XX': tf.saved_model.utils.build_tensor_info(inputX)} # 输入数据inputX
        outputs = {'pred_YY': tf.saved_model.utils.build_tensor_info(y_pred)} # 预测结果y_pred
        
        # 构建 Signature
        signature = tf.saved_model.signature_def_utils.build_signature_def(
            inputs=inputs,
            outputs=outputs,
            method_name=signature_key
        )
        
        # 建立SavedModelBuilder存储模型,并以signature的形式添加要存储的变量
        builder = tf.saved_model.builder.SavedModelBuilder(saved_model_path)
        builder.add_meta_graph_and_variables(
            sess=sess,
            tags=['lstm_saved_model'], 
            # tags=[tf.saved_model.tag_constants.TRAINING],
            # tags=[tf.saved_model.tag_constants.SERVING],
            signature_def_map={signature_key: signature} ,
            clear_devices=True
        )
        
        # 将 MetaGraphDef 写入磁盘
        builder.save()
        
        time_end = time.time()
        print('Training time:{:.4}'.format(time_end-time_start),'s')     

[补充说明]:

1. tags的选择

#  自定义tags
tags=['lstm_saved_model']

# 预定义值TRAINING
tags=[tf.saved_model.tag_constants.TRAINING]

# 预定义值SERVING,若后续放在TFServing上,则必须选择SERVING
tags=[tf.saved_model.tag_constants.SERVING]

注:
(1) tags只可以选一个值, 如果同时写上 TRAINING 和 SERVING 则会报错.
(2) 固化后的模型后续如果部署在TFServing上, 则此处tags必须选择SERVING, 不然会报错

failed: Not found: Could not find meta graph def matching supplied tags: { serve }. To inspect available tag-sets in the SavedModel, please use the SavedModel CLI: `saved_model_cli`

2. 与saved_model 相关的API

class tf.saved_model.builder.SavedModelBuilder

# 初始化方法
__init__(export_dir)

# 导入graph与变量信息 
add_meta_graph_and_variables(
    sess,
    tags,
    signature_def_map=None,
    assets_collection=None,
    legacy_init_op=None,
    clear_devices=False,
    main_op=None
)

# 载入保存好的模型
tf.saved_model.loader.load(
    sess,
    tags,
    export_dir,
    **saver_kwargs
)
1.1.4 使用SignatureDef:模型载入

载入代码如下:

def test_saved_model_pb(saved_model_path):   
    test_graph = tf.Graph()  
    signature_key = 'test_signature'
    
    with tf.Session(graph=test_graph) as test_sess:
        
        # 调用 tf.saved_model.loader.load 恢复出meta_graph
        meta_graph_def = tf.saved_model.loader.load(
            sess=test_sess, 
            tags=['lstm_saved_model'],  
            export_dir=saved_model_path
        )

        # 从 meta_graph 中取出 Signature 对象
        signature = meta_graph_def.signature_def

        # 从 signature 中取出具体的输入输出的 tensor name
        x_tensor_name = signature[signature_key].inputs['input_XX'].name
        y_tensor_name = signature[signature_key].outputs['pred_YY'].name

        # 取出输入张量和输出张量
        inputXX = test_sess.graph.get_tensor_by_name(x_tensor_name)
        y_pred = test_sess.graph.get_tensor_by_name(y_tensor_name)

        # 加载测试数据
        test_X = ...

        # 得到测试结果
        test_output = test_sess.run([y_pred], feed_dict = {inputXX: test_X})

2. Serving端:模型加载与在线预测

Tensorflow Serving 是google为机器学习模型生产环境部署设计的高性能的服务系统。具有以下特性:

  • 支持模型版本控制和回滚
  • 支持并发与GPU加速,实现高吞吐量
  • 开箱即用,并且可定制化
  • 支持多模型服务
  • 支持 gRPC/ REST API 调用
  • 支持批处理
  • 支持热更新
  • 支持分布式模型
  • 支持多平台模型,如 TensorFlow/MXNet/PyTorch/Caffe2/CNTK等

Tensorflow Serving 丰富的、开箱即用的功能,使得其成为业内认可的部署方案。

2.1 环境搭建

推荐基于Docker的方式搭建Tensorflow Serving,如何在Ubuntu上安装docker具体可参考我的另外一篇博客:Ubuntu安装及使用Docker

Docker安装完毕后,拉取最新的 tensorflow/serving 的镜像。

docker pull tensorflow/serving

这里直接给出官网(https://tensorflow.google.cn/tfx/serving/docker)示例,运行正常则说明环境搭建完成:

# Download the TensorFlow Serving Docker image and repo
docker pull tensorflow/serving 
git clone https://github.com/tensorflow/serving
# Location of demo models
TESTDATA="$(pwd)/serving/tensorflow_serving/servables/tensorflow/testdata"

# Start TensorFlow Serving container and open the REST API port
docker run -t --rm -p 8501:8501 \
    -v "$TESTDATA/saved_model_half_plus_two_cpu:/models/half_plus_two" \
    -e MODEL_NAME=half_plus_two \
    tensorflow/serving &    
# Query the model using the predict API
curl -d '{"instances": [1.0, 2.0, 5.0]}' \
    -X POST http://localhost:8501/v1/models/half_plus_two:predict 

# Returns => { "predictions": [2.5, 3.0, 4.5] }

下面对调用预测的API接口的参数进行一个详细解释 :

http://localhost:8501/v1/models/half_plus_two:predict
# 8501: 端口号
# v1: 模型版本号
# half_plus_two : 模型名称
# predict : 模型中的函数名称, 对应于 saved_model 中的 signature_key

        # saved_model 开始,定义 signature_key
        signature_key = 'predict'
        
        # 构建 Signature
        signature = tf.saved_model.signature_def_utils.build_signature_def(...)
        
        # 建立SavedModelBuilder存储模型,并以signature的形式添加要存储的变量
        builder = tf.saved_model.builder.SavedModelBuilder(saved_model_path)
        builder.add_meta_graph_and_variables(
            ... ,
            signature_def_map={signature_key: signature}
            # signature_def_map={'predict':signature}
        )

[补充说明]:

GPU版Tensorflow Serving 的环境搭建见:https://tensorflow.google.cn/tfx/serving/docker

2.2 部署模型

为了部署模型,我们现在要做的是运行Docker容器, 将容器的端口发布到主机的端口,然后将主机的路径安装到SavedModel到容器期望模型的位置。

2.2.1 部署单个模型

以上面官的 half_plus_two 模型为例,部署单个模型指令示例:

docker run -p 8501:8501 \
  --mount type=bind,source=/home/xxx/docker/serving/tensorflow_serving/servables/tensorflow/testdata/half_plus_two,target=/models/half_plus_two \
  -e MODEL_NAME=half_plus_two \
  -t tensorflow/serving &

在这种情况下,我们启动了一个Docker容器,将REST API端口8501发布到主机的端口8501,并采用了一个名为 half_plus_two 的模型并将其绑定到默认的模型target路径( /models/half_plus_two )。

如果要发布gRPC端口,可以使用-p 8500:8500 。可以同时打开gRPC和REST API端口,或者选择仅打开一个端口。

[补充说明]:

  • & 表示任务放在后台运行,需要手动kill这个进程。如果不需要后台运行,可以去掉 & 。
# netstat 查看端口占用情况
netstat -pan | grep 8501
# 端口占用情况如下, 占用端口的进程ID为22948
[root@k8s-node1 tensorflow]# netstat -pan |grep 8501
tcp6       0      0 :::8501                 :::*                    LISTEN      22948/docker-proxy 
# kill掉ID为22948的进程
kill -9 22948
  • 参数说明:
--mount:   表示要进行挂载
source:    指定要运行部署的模型地址, 也就是挂载的源,这个是在宿主机上的模型目录
target:     这个是要挂载的目标位置,也就是挂载到docker容器中的哪个位置,这是docker容器中的目录
-t:         指定的是挂载到哪个容器
-p:         指定主机到docker容器的端口映射
docker run: 启动这个容器并启动模型服务
 
综合解释:
         将source目录中的例子模型,挂载到-t指定的docker容器中的target目录,并启动
  • 报错分析:
    报错1:
invalid argument "type=bind," for "--mount" flag: invalid field '' must be a key=value pair
See 'docker run --help'.

上述shell命令中,逗号后面不可以有空格,可以尝试删除空格,或者去掉换行( \ )。

2.2.2 部署多个模型

通过 config 部署多模型,部署指令示例:

docker run -p 8500:8500 -p 8501:8501 \
  --mount type=bind, \
  source=/tmp/multi_models/, \
  target=/models/multi_models \
  -t tensorflow/serving \ 
  --model_config_file=/models/multi_models/model.config

补充说明:

  1. Serving 镜像支持 gRPC(端口8500)、RESTful API (端口8501)两种方式调用,使用时需要将host的端口与之绑定
  2. Serving 无法直接加载 host 下的模型文件,所以需要将其映射到容器内路径,MODEL_BASE_PATH 默认为 /models
  3. 多模型加载和模型版本管理在 model_config_file 中配置

这里给出 model.config 内容示例:

model_config_list:{
  config:{
    name:"textCnn",
    base_path:"/models/multi_models/textCnn/pb",
    model_platform:"tensorflow",
    model_version_policy {
      specific {
        versions: 0
      }
    }
  },

  config:{
    name:"rcnn",
    base_path:"/models/multi_models/rcnn/pb",
    model_platform:"tensorflow",
    model_version_policy {
      specific {
        versions: 0
      }
    }
  },

  config:{
    name:"bert",
    base_path:"/models/multi_models/bert/pb",
    model_platform:"tensorflow",
  }
}

这里 load 了三个模型(textCnn、rcnn、bert), 每个模型维护自己的config,当一个模型存在多个版本时,tensorflow serving 默认加载版本号最高的版本,若想要指定版本加载,配置 model_version_policy 内容即可。

注:base_path 是映射到 Docker容器内的路径,而不是本地路径。

2.2.3 如何部署自己的模型?

可以参考我的另外一篇博客: Tensorflow Serving 部署自己的模型

3. Client端:构建请求

Client端的话,可以使用Java或者Python

参考博客:部署tensorflow serving+python,java client代码实例

Java Client

推荐使用 Maven 项目来实现

Python Client

参考博客:

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Tensorflow Serving 模型部署指南 的相关文章

随机推荐

  • Qt鼠标单击与长按

    在Qt中 可以通过重载QWidget的mousePressEvent mouseReleaseEvent 和mouseMoveEvent 等事件来实现对鼠标事件的处理 判断鼠标是长按还是点击 可以通过记录鼠标按下的时间和释放的时间 通过两个
  • 15.DDT+unittest+excel ddt框架结合单元测试

    Excel读取数据的三种方式 1 一次性读取所有的数据 对内存的要求高点 必须掌握 2 需要用的时候读取所有的数据 就是磁盘读写要求高点 磁盘 速度最低 内存 速度中间 CPU 速度最高 3 使用DDT进行参数化 方法一 一次性读取所有的数
  • 缓存相关

    缓存雪崩怎么解决 如果缓存因为某个原因不可用 导致大量请求涌向数据库 可能会导致数据库崩溃 缓存雪崩目前主要有两种方案 1 使用集群 集群部署缓存 当一台宕机时 其他机器仍能提供缓存服务 2 Hystrix 熔断器 起到熔断 降级 限流三个
  • 文章内容无法复制复制不了

    一些文档 什么的复制不了 主要有几种方法 目录 1 禁用js 2 ctrl P打印 3 选中要复制的内容 gt 拖到网址输入框 另外 通过一些浏览器插件 感觉用处不大 1 禁用js 优点 可以直接复制原有段落文字的格式 缺点 有点麻烦 对百
  • 通俗理解三大范式

    关系型数据库中我们用的最多的就是第一范式 1NF 第二范式 2NF 第三范式 3NF 所以需要我们深入理解三大范式 第一范式 1NF 要求数据库的每一列都是不可分割的原子数据项 在上面的表中 家庭信息 和 学校信息 列均不满足原子性的要求
  • Windows下C语言操作硬件设备的方法

    本文作者 Fezl 本文原地址 http blog csdn net u010147522 article details 49912221 之前都是在linux下操作硬件设备 open read write ioctl 相当方便 最近要检
  • jQuery 入门教程(23): jQuery UI Autocomplete示例(一)

    AutoComplete 在获取焦点后 随着用户键入的内容 可以在预订的数据源中查找和已输入的内容相匹配的内容列表供用户选择 这可以用作之前输入过的内容也可以用作自动填充相关内容 比如根据城市名 自动填充邮编等 你可以使用本地数据源或是远程
  • nvm的安装及使用、下载cnpm以及git的配置

    nvm下载 下载图中安装包 下载完了就有这个 双击安装 路劲把C改为D即可 这是直接下载选择好安装路劲之后 没配置的环境变量 配置后的环境变量 1 文件夹设置 2 环境变量配置 查询nvm版本号 nvm常用命令如下 使用nvm下载 node
  • 适合初学者的强化学习教程(1): python使用gym实践和注意事项

    作者 知乎 Ai酱 安装步骤和报错问题 安装 pip install gym 报错 AttributeError module gym envs box2d has no attribute BipedalWalker 这是因为gym没有安
  • C语言for循环必备练习题

    话不多说 直接上题 笔者的一贯要求 速度 1 作业标题 663 关于while 条件表达式 循环体 以下叙述正确的是 假设循环体里面没有break continue return goto等等语句 作业内容 A 循环体的执行次数总是比条件表
  • 构造函数初始化列表

    目录 一 初始化列表 二 初始化列表的使用 三 注意 1 每个成员变量在初始化列表中只能出现一次 初始化只能初始化一次 2 类中包含以下成员 必须放在初始化列表位置进行初始化 3 尽量使用初始化列表初始化 因为不管你是否使用初始化列表 对于
  • ubuntu16.04上利用opencv目标跟踪工具实现8种目标跟踪

    一共八种工具 八种工具包括 BOOSTING Tracker 和Haar cascades AdaBoost 背后所用的机器学习算法相同 但是距其诞生已有十多年了 这一追踪器速度较慢 并且表现不好 但是作为元老还是有必要提及的 最低支持Op
  • 操作系统面试题总结

    1 线程与进程的区别联系 2 进程通信方式有哪些 3 同步的方式有哪些 4 ThreadLocal与其它同步机制的比较 5 进程死锁的条件 第一题 1 线程是进程的一个实体 一个进程可以拥有多个线程 多个线程也可以并发执行 一个没有线程的进
  • [Unity 3D] DOTween 常用函数

    DOTween官方文档 http dotween demigiant com documentation php 一 控制变量 1 DOTween To static DOTween To getter setter to float du
  • 端口扫描介绍

    文章目录 1 端口的基本概念 2 端口的常见分类 3 端口扫描原理 1 端口的基本概念 端口 在计算机网络领域中是个非常重要的概念 它是专门为计算机通信而设计的 它不是硬件 不同于计算机中的 插槽 可以说是个 软端口 端口是由计算机的通信协
  • vue+Element-ui 导入excel文件生成json数据

    1 首先安装依赖 import XLSX from xlsx 2 建立读取excel文件的js文件 以便调用 importExcel js import XLSX from xlsx export function readExcel fi
  • java随笔:类成员

    类成员 1 介绍 在java中只能包含成员变量 方法 构造器 初始化块 内部类 接口 枚举 5种成员 其中static可以修饰成员变量 方法 初始化块 内部类 接口 枚举 用static修饰的成员就是类成员 类成员属于整个类 而不属于单独的
  • KLT(Kanade-Lucas-Tomasi )

    目录 光流法 KLT 原理 应用 目标跟踪算法主要分为两类 一类是传统的目标跟踪算法 粒子滤波 pf Mean Shift及KLT算法 或称Lucas光流法 另一大类是基于深度学习的跟踪算法 光流法 光流 Optical flow 其实是指
  • MySQL事务隔离级别、脏读、幻读、不可重复读现象及解决办法、快照读和当前读

    目录 一 事务隔离级别 二 脏读 幻读 不可重复读现象及解决办法 1 脏读 2 不可重复读现象 3 幻读现象 4 使用for update避免幻读 5 使用串行读避免幻读现象 三 快照读与当前读 1 理论 2 RR 下 快照建立时机 第一次
  • Tensorflow Serving 模型部署指南

    文章目录 1 Saver端 模型的离线训练与导出 1 1 saved model 模型保存与载入 1 1 1 简单场景 模型保存 1 1 2 简单场景 模型载入 1 1 3 使用SignatureDef 模型保存 1 1 4 使用Signa