乐趣区

关于机器学习:搭建自己的以图搜图系统二深入优化搭建生产级别的图搜系统

概述

本文是“搭建本人的以图搜图零碎”系列的第二篇,在 第一篇 内容中咱们理解了如何利用“机器学习框架 Towhee ¹”和“向量数据库 Milvus ²”疾速搭建一个以图搜图的服务原型。那么,如何搭建一个生产级别的服务呢?在实在的业务场景中,咱们经常碰到这些技术难题:

  • 海量数据的状况下零碎提早高,硬件资源成为瓶颈。
  • 零碎在一种数据集下召回成果不错,但在另一种数据集下召回成果却很差,想要尝试多种模型或者本人训练,但上手老本都很高。
  • 零碎容错率低,当咱们批量解决图片时,如果存在坏数据系统很容易解体。

接下来本文会从性能、模型和业务流程方面探讨如何解决这些痛点,从而优化咱们的以图搜图零碎,最初会介绍如何应用 FastAPI 实现简略高效的 Web 服务。

本文中的相干代码已上传到 GitHub,欢送大家参考和应用:https://github.com/towhee-io/…

性能优化

要想晋升性能,“堆机器”无疑是最便捷的形式,但在无限的资源下,咱们如何充分发挥算力劣势呢?个别状况下,咱们会采取上面几种计划:并行处理 ,充分发挥资源性能; 数据降维 ,升高计算复杂度; 向量索引,应用近邻搜寻的算法减速向量检索。

下文中的代码基本上都来自于以图搜图系列的第一篇内容,如果你想理解更具体的内容,能够移步:《搭建本人的以图搜图零碎(一):10 行代码搞定以图搜图》。

并行处理

基于 Towhee 的以图搜图 AI 流水线反对应用并行执行的形式,来晋升性能,在上面的代码中,咱们只须要简略地调用 set_parallel 办法,就能够并发解决数据。上面的例子演示了如何并行处理图片数据:

import towhee

dc = (towhee.read_csv('reverse_image_search.csv')
     .runas_op['id', 'id'](func=lambda x: int(x))
     .set_parallel(3)  #3 并发解决数据
     .image_decode['path', 'img']()
     .image_embedding.timm['img', 'vec'](model_name='resnet50')
     .tensor_normalize['vec', 'vec']()
     .to_milvus['id', 'vec'](collection=collection, batch=100)
)

在下面的例子中,咱们设置了 3 个并发来解决数据,咱们将会失去 2~3 倍的性能晋升。

数据降维

计算高维向量的硬件老本很高,举个例子,ResNet50 模型生成的 Embedding 向量维度是 2048,如果图像数据规模为一亿,那么内存占用大概是 768 GB(4 Bytes 2048 100000000),咱们也遇到了不少企业,他们的数据量级都在百亿级别,那么就须要约 76800 GB(75 TB)内存,那么针对向量数据进行降维就很有必要了,因为数据的计算量升高,性能会有晋升。降维的办法有很多种,如 PCA、SVD 和 UMAP 等,咱们以最简略的随机投影为例,在图片入库之前将向量维度从 2048 维升高到 512 维:

import numpy as np

projection_matrix = np.random.normal(scale=1.0, size=(2048, 512))

def dim_reduce(vec):
    return np.dot(vec, projection_matrix)

dc = (towhee.read_csv('reverse_image_search.csv')
        .runas_op['id', 'id'](func=lambda x: int(x))
        .image_decode['path', 'img']()
        .image_embedding.timm['img', 'vec'](model_name='resnet50')
        .runas_op['vec','vec'](func=dim_reduce)  #向量数据降维
        .tensor_normalize['vec', 'vec']()
        .to_milvus['id', 'vec'](collection=collection, batch=100)
)

随机投影是欧几里得空间中向量的降维办法,这种办法速度快且无需训练,示例中 dim_reduce 函数实现了该办法,并通过 runas_op 算子将其利用到 ResNet50 模型提取的向量数据中。

  • 向量索引
    在图片检索阶段,咱们能够应用面向 AI 的向量数据库 Milvus 对大规模的向量数据进行类似度检索,Milvus 反对多种 ANN 索引³用于减速,包含基于量化的索引,基于图的索引和基于树的索引等。咱们能够创立适合的索引用于近邻搜寻,值得一提的是 IVF_SQ8 索引,它不光通过聚类反对疾速查找,还能够压缩数据,缩小 70-75% 的内存。
  • 应用 GPU 减速
    不得不说模型减速最好的办法就是指定 GPU(前提是要有),当咱们应用 Towhee 算子中的 AI 模型进行特征向量提取,Towhee 框架将会主动依据 GPU 是否可用,来启用 GPU 进行数据处理,也就是说当 cuda.is_aviliable=True就会应用 GPU。

模型优化

随着 AI 技术的一直倒退,CV(Computer Vision) 畛域呈现了越来越多的算法模型,从 VGG 到 ResNet 再到 Transformer,模型一直降级,那么哪一个模型才是最适宜咱们的呢?实际是测验真谛的唯一标准,最简略的形式是在本人的数据集下试用各种预训练好的模型选最优,除此之外,咱们也能够本人训练模型。

模型选型

Towhee 的 image-embedding ⁴算子涵盖了市面上支流的各种模型,通过批改算子参数,能够轻松调用任何模型,而无需额定的折腾。此外 Towhee 还提供对于 Recall、HR 和 mAP 等指标的计算和报告,咱们能够基于本人的数据集来比照不同模型的指标后果,从而帮忙抉择最优的模型。
例如咱们指定三个模型 (VGG16, resnet50 和 efficientnet-b2) 进行测试并比照,先将图像数据集入库,而后搜寻测试图片并返回搜寻后果的准确率报告:

model_dim = {  #模型与生成向量维度的字典
    'vgg16': 4096,
    'resnet50': 2048,
    'tf_efficientnet_b2': 1408
}

for model in model_dim:
    collection = create_milvus_collection(model, model_dim[model])

    dc = (towhee.read_csv('reverse_image_search.csv')
            .runas_op['id', 'id'](func=lambda x: int(x))
            .image_decode['path', 'img']()
            .image_embedding.timm['img', 'vec'](model_name=model)
            .tensor_normalize['vec', 'vec']()
            .to_milvus['id', 'vec'](collection=collection, batch=100)
    )  #图像数据入库

    (towhee.glob['path']('./test/*/*.JPEG')
         .image_decode['path', 'img']()
         .image_embedding.timm['img', 'vec'](model_name=model)
         .tensor_normalize['vec', 'vec']()
         .milvus_search['vec', 'result'](collection=collection, limit=10)
         .runas_op['path', 'ground_truth'](func=ground_truth)                #获取测试数据的 ground truth
         .runas_op['result', 'result'](func=lambda res: [x.id for x in res]) #获取搜寻后果的 id
         .with_metrics(['mean_hit_ratio', 'mean_average_precision'])         #指定 HR 和 mAP 两个指标
         .evaluate['ground_truth', 'result'](model)                          #将后果 id 和 ground truth 比拟
         .report())  #检索图像并返回指标报告

当然,也能够用应用本人的模型而不是 Towhee 的内置算子,上面以应用 Transformer 模型为例,首先定义 vit_embedding 函数用于提取特征向量,而后通过 runas_op 利用此函数,最初和下面的代码相似,用于计算指定的指标。

feature_extractor = ViTFeatureExtractor.from_pretrained('google/vit-large-patch32-384')
model = ViTModel.from_pretrained('google/vit-large-patch32-384')
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model.to(device)


def vit_embedding(img):
    img = to_image_color(img, 'RGB')
    inputs = feature_extractor(img, return_tensors="pt")
    outputs = model(inputs['pixel_values'].to(device))
    return outputs.pooler_output.detach().cpu().numpy().flatten()

collection = create_milvus_collection('huggingface_vit', 1024)

dc = (towhee.read_csv('reverse_image_search.csv')
        .runas_op['id', 'id'](func=lambda x: int(x))
        .image_decode['path', 'img']()
        .image_embedding.timm['img', 'vec'](model_name=model)
        .tensor_normalize['vec', 'vec']()
        .to_milvus['id', 'vec'](collection=collection, batch=100)
)

(towhee.glob['path']('./test/*/*.JPEG')
     .image_decode['path', 'img']()
     .image_embedding.timm['img', 'vec'](model_name=model)
     .tensor_normalize['vec', 'vec']()
     .milvus_search['vec', 'result'](collection=collection, limit=10)
     .runas_op['path', 'ground_truth'](func=ground_truth)
     .runas_op['result', 'result'](func=lambda res: [x.id for x in res])
     .with_metrics(['mean_hit_ratio', 'mean_average_precision'])
     .evaluate['ground_truth', 'result'](model)
     .report())

以上四个模型的准确率状况如下图所示,可见在本文数据集中 ViT-large 模型的准确度更高。

模型训练

除了应用这些预训练好的模型,咱们还能够基于本人的数据集进行模型训练,Towhee 也提供了模型训练接口,你能够尝试训练任意的模型算子。

流程优化

在稳定性方面,咱们心愿零碎既强壮又牢靠,不至于碰到异样就解体;在业务方面,咱们心愿能够定制化流水线,不同的流水线用于解决不同的业务。

异样解决

当以图搜图零碎解决大规模数据时,如果其中存在损坏的图像或格局谬误的图像,这很可能导致程序中断,但咱们又很难清理掉所有呈现的坏数据,该怎么办呢?Towhee 提供 exception_safe 接口可能确保出现异常时继续执行。

(towhee.glob['path']('./exception/*.JPEG')
    .exception_safe() #异样平安
    .image_decode['path', 'img']()
    .image_embedding.timm['img', 'vec'](model_name='resnet50')
    .tensor_normalize['vec', 'vec']()
    .milvus_search['vec', 'result'](collection=resnet_collection, limit=5)
    .runas_op['result', 'result_img'](func=read_images)
    .drop_empty()    #革除坏数据
    .select['img', 'result_img']()
    .show())

下面的例子中,咱们应用了四张图片作为输出数据,其中蕴含一张损坏的图片,因为咱们在流水线中退出了 exception_safe,所以程序不会产生中断,仅仅是打印了错误信息。

减少指标检测

依据不同的业务场景咱们能够定制不同的流水线,比方在商品举荐场景中,咱们更关注图像蕴含的商品,那么能够在流水线中加上 Towhee 的指标检测⁵算子,用于检测图像中的商品。代码如下所示,具体原理是先利用 get_object 返回图像所有指标中面积最大的物体(没有指标将返回图像自身),而后针对找到的指标物体进行特征提取。

def get_object(img, boxes):
    if len(boxes) == 0:
        return img
    max_area = 0
    for box in boxes:
        x1, y1, x2, y2 = box
        area = (x2-x1)*(y2-y1)
        if area > max_area:
            max_area = area
            max_img = img[y1:y2,x1:x2,:]
    return max_img

(towhee.glob['path']('./object/*.jpg')
        .image_decode['path', 'img']()
        .object_detection.yolov5['img', ('boxes', 'class', 'score')]() #指标检测算子
        .runas_op[('img', 'boxes'), 'object'](func=get_object)
        .image_embedding.timm['object', 'object_vec'](model_name='resnet50')
        .tensor_normalize['object_vec', 'object_vec']()
        .milvus_search['object_vec', 'object_result'](collection=yolo_collection, limit=3)
        .runas_op['object_result', 'object_result_img'](func=read_images)
        .select['img', 'result_img', 'object_result_img']()
        .show())

在进行有无指标检测的后果比照之后,咱们能够发现一个有意思的事件:在某些状况下,指标检测后的后果更优。以下图中每一行的后果为例,从左至右是咱们输出的检索图片,和三张图搜的后果、进行指标检测后搜寻的后果。第一行在加上指标检测后能力找到车相干的图片,否则都是蜘蛛,咱们能够揣测 ResNet 把待检索图片中的树枝了解成了蜘蛛,然而在 Towhee 流水线中利用上指标检测就能够解决这个问题。

FastAPI 部署

上一篇文章中咱们介绍了利用 Gradio 部署图像搜寻的服务,这次将展现如何利用 FastAPI ⁶来提供 Web 服务,FastAPI 是目前支流的基于 Python 的 Web 框架之一。咱们先创立一个 FastAPI 实例 app,而后将这个实例 app 绑定到 Towhee 的流水线中,app_search 提供了基于 FastAPI 的图像检索服务,最初咱们设置好服务端口和地址,就可能失去一个在线服务了。

from fastapi import FastAPI
import uvicorn
import nest_asyncio

app = FastAPI()

with towhee.api['file']() as api:
    app_search = (api.image_load['file', 'img']()
        .image_embedding.timm['img', 'vec'](model_name='resnet50')
        .tensor_normalize['vec', 'vec']()
        .milvus_search['vec', 'result'](collection=milvus_collection)
        .runas_op['result', 'res_file'](func=lambda res: str([id_img[x.id] for x in res]))
        .select['res_file']()
        .serve('/search', app) #绑定流水线到 app,接口为 /search
    )

nest_asyncio.apply()
uvicorn.run(app=app, host='0.0.0.0', port=8000)

相似的,参考 towhee-io/examples 咱们能够实现 /load/count 两个接口的创立,在所有工作就绪之后,咱们就可能在浏览器中关上 http://0.0.0.0:8000/docs 来体验领有更高性能的以图搜图服务利用了。

总结

置信在追随本文急躁实际之后,你肯定能够失去一个性能颇高、生产可用的“以图搜图零碎”。当然,如果你违心的话,也能够联合本文的例子,对其余的非结构化数据和我的项目(音视频)进行剖析优化,原理是相通的。欢送留言探讨,或者给咱们的我的项目提出改良倡议(https://github.com/towhee-io/…)。

下一篇文章中,咱们将剖析如何“打包 AI 流水线”,通过应用 Docker 来实现零碎的疾速搭建和残缺数据迁徙。

相干材料:

[1] https://towhee.io/
[2] https://milvus.io/
[3] https://milvus.io/docs/v2.1.x…
[4] https://towhee.io/tasks/detai…
[5] https://towhee.io/tasks/detai…
[6] https://fastapi.tiangolo.com/


如果你感觉咱们分享的内容还不错,请不要悭吝给咱们一些激励:点赞、喜爱或者分享给你的小伙伴!

流动信息、技术分享和招聘速递请关注:https://zilliz.gitee.io/welcome/

如果你对咱们的我的项目感兴趣请关注:

用于存储向量并创立索引的数据库 Milvus

用于构建模型推理流水线的框架 Towhee

退出移动版