面向计算机视觉的深度学习:1~5(3)

本文涉及的产品
图像搜索,7款服务类型 1个月
简介: 面向计算机视觉的深度学习:1~5(3

面向计算机视觉的深度学习:1~5(2)https://developer.aliyun.com/article/1426859

引导反向传播

直接将特征可视化可能会减少信息量。 因此,我们使用反向传播的训练过程来激活滤镜以实现更好的可视化。 由于我们选择了要激活的神经元以进行反向传播,因此称为引导反向传播。 在本节中,我们将实现引导式反向传播以可视化特征。

我们将定义大小并加载 VGG 模型,如下所示:

image_width, image_height = 128, 128 vgg_model = tf.keras.applications.vgg16.VGG16(include_top=False)

层由以层名称作为键的字典组成,模型中的层以权重作为键值,以方便访问。 现在,我们将从第五个块 block5_conv1 中获取第一卷积层,以计算可视化效果。 输入和输出在此处定义:

input_image = vgg_model.input
vgg_layer_dict = dict([(vgg_layer.name, vgg_layer) for vgg_layer in vgg_model.layers[1:]])
vgg_layer_output = vgg_layer_dict['block5_conv1'].output

我们必须定义损失函数。 损失函数将最大化特定层的激活。 这是一个梯度上升过程,而不是通常的梯度下降过程,因为我们正在尝试使损失函数最大化。 对于梯度上升,平滑梯度很重要。 因此,在这种情况下,我们通过归一化像素梯度来平滑梯度。 该损失函数快速收敛而不是。

应该对图像的输出进行归一化以可视化,在优化过程中使用 g 辐射上升来获得函数的最大值。 现在,我们可以通过定义评估器和梯度来开始梯度上升优化,如下所示。 现在,必须定义损失函数,并要计算的梯度。 迭代器通过迭代计算损耗和梯度值,如下所示:

filters = []
for filter_idx in range(20):
    loss = tf.keras.backend.mean(vgg_layer_output[:, :, :, filter_idx])
    gradients = tf.keras.backend.gradients(loss, input_image)[0]
    gradient_mean_square = tf.keras.backend.mean(tf.keras.backend.square(gradients))
    gradients /= (tf.keras.backend.sqrt(gradient_mean_square) + 1e-5)
    evaluator = tf.keras.backend.function([input_image], [loss, gradients])

输入是随机的灰度图像,并添加了一些噪声。 如此处所示,将生成随机图像并完成缩放。

gradient_ascent_step = 1.
    input_image_data = np.random.random((1, image_width, image_height, 3))
    input_image_data = (input_image_data - 0.5) * 20 + 128

现在开始对损失函数进行优化,对于某些过滤器,损失值可能为 0,应将其忽略,如下所示:

for i in range(20):
        loss_value, gradient_values = evaluator([input_image_data])
        input_image_data += gradient_values * gradient_ascent_step
        # print('Loss :', loss_value)
  if loss_value <= 0.:
            break

优化之后,通过均值减去并调整标准差来完成归一化。 然后,可以按比例缩小滤镜并将其裁剪到其梯度值,如下所示:

if loss_value > 0:
        filter = input_image_data[0]
        filter -= filter.mean()
        filter /= (filter.std() + 1e-5)
        filter *= 0.1
  filter += 0.5
  filter = np.clip(filter, 0, 1)
        filter *= 255
  filter = np.clip(filter, 0, 255).astype('uint8')
        filters.append((filter, loss_value))

这些过滤器是随机选择的,并在此处可视化:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r6RVCSWQ-1681567519368)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-cv/img/f741f405-d2e9-4598-822b-d269d004e882.png)]

如图所示,用于缝合图像并产生输出的代码与代码束一起提供。 由于修道院的接受区域变大,因此可视化在以后的层变得复杂。 一些滤镜看起来很相似,但只是旋转而已。 在这种情况下,可视化的层次结构可以清楚地看到,如 Zeiler 等人所示。 下图显示了不同层的直接可视化:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H0ljuHqt-1681567519368)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-cv/img/810e2adf-2cf5-44d2-b729-91dfd56cadb1.png)]

经 Zeiler 等人许可复制。

前两层看起来像边缘和角落检测器。 类似于 Gabor 的滤镜仅出现在第三层中。 Gabor 过滤器是线性的,传统上用于纹理分析。 我们已经直接通过引导反向传播看到了特征的可视化。 接下来,我们将看到如何实现 DeepDream 进行可视化。

DeepDream

可以在网络中的某些层上放大神经元激活,而不是合成图像。 放大原始图像以查看特征效果的概念称为 DeepDream。 创建 DeepDream 的步骤是:

  1. 拍摄图像并从 CNN 中选择一个层。
  2. 在特定的层进行激活。
  3. 修改梯度,以使梯度和激活相等。
  4. 计算图像和反向传播的梯度。
  5. 必须将正则化用于图像的抖动和归一化。
  6. 像素值应修剪。
  7. 为了实现分形效果,对图像进行了多尺度处理。

让我们从导入相关的包开始:

import os
import numpy as np
import PIL.Image
import urllib.request
from tensorflow.python.platform import gfile
import zipfile

初始模型在Imagenet数据集和 Google 提供的模型文件上进行了预训练。 我们可以下载该模型并将其用于本示例。 模型文件的 ZIP 归档文件已下载并解压缩到一个文件夹中,如下所示:

model_url = 'https://storage.googleapis.com/download.tensorflow.org/models/inception5h.zip'   file_name = model_url.split('/')[-1]
file_path = os.path.join(work_dir, file_name)
if not os.path.exists(file_path):
    file_path, _ = urllib.request.urlretrieve(model_url, file_path)
zip_handle = zipfile.ZipFile(file_path, 'r')
zip_handle.extractall(work_dir)
zip_handle.close()

这些命令应该在工作目录中创建了三个新文件。 可以将此预训练的模型加载到会话中,如下所示:

graph = tf.Graph()
session = tf.InteractiveSession(graph=graph)
model_path = os.path.join(work_dir, 'tensorflow_inception_graph.pb')
with gfile.FastGFile(model_path, 'rb') as f:
    graph_defnition = tf.GraphDef()
    graph_defnition.ParseFromString(f.read())

会话从图初始化开始。 然后,将下载的模型的图定义加载到内存中。 作为预处理步骤,必须从输入中减去ImageNet平均值,如下所示。 预处理后的图像随后被馈送到该图,如下所示:

input_placeholder = tf.placeholder(np.float32, name='input')
imagenet_mean_value = 117.0 preprocessed_input = tf.expand_dims(input_placeholder-imagenet_mean_value, 0)
tf.import_graph_def(graph_defnition, {'input': preprocessed_input})

现在,会话和图已准备好进行推断。 双线性插值需要resize_image函数。 可以添加resize函数方法,该函数通过 TensorFlow 会话来调整图像的大小,如下所示:

def resize_image(image, size):
    resize_placeholder = tf.placeholder(tf.float32)
    resize_placeholder_expanded = tf.expand_dims(resize_placeholder, 0)
    resized_image = tf.image.resize_bilinear(resize_placeholder_expanded, size)[0, :, :, :]
    return session.run(resized_image, feed_dict={resize_placeholder: image})

可以将工作目录中的图像加载到内存中并转换为浮点值,如下所示:

image_name = 'mountain.jpg' image = PIL.Image.open(image_name)
image = np.float32(image)

此处显示了已加载的图像,供您参考:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KbKLjTPw-1681567519368)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-cv/img/a6cf4c5d-de28-4bec-995d-07b57bfbe560.jpg)]

音阶空间的八度音阶数,大小和音阶在此处定义:

no_octave = 4 scale = 1.4 window_size = 51

这些值在此处显示的示例中效果很好,因此需要根据其大小调整其他图像。 可以选择一个层来做梦,该层的平均平均值将是objective函数,如下所示:

score = tf.reduce_mean(objective_fn)
gradients = tf.gradients(score, input_placeholder)[0]

计算图像的梯度以进行优化。 可以通过将图像调整为各种比例并找到差异来计算八度图像,如下所示:

octave_images = []
for i in range(no_octave - 1):
    image_height_width = image.shape[:2]
    scaled_image = resize_image(image, np.int32(np.float32(image_height_width) / scale))
    image_difference = image - resize_image(scaled_image, image_height_width)
    image = scaled_image
    octave_images.append(image_difference)

现在可以使用所有八度图像运行优化。 窗口在图像上滑动,计算梯度激活以创建梦,如下所示:

for octave_idx in range(no_octave):
    if octave_idx > 0:
        image_difference = octave_images[-octave_idx]
        image = resize_image(image, image_difference.shape[:2]) + image_difference
    for i in range(10):
        image_heigth, image_width = image.shape[:2]
        sx, sy = np.random.randint(window_size, size=2)
        shifted_image = np.roll(np.roll(image, sx, 1), sy, 0)
        gradient_values = np.zeros_like(image)
        for y in range(0, max(image_heigth - window_size // 2, window_size), window_size):
            for x in range(0, max(image_width - window_size // 2, window_size), window_size):
                sub = shifted_image[y:y + window_size, x:x + window_size]
                gradient_windows = session.run(gradients, {input_placeholder: sub})
                gradient_values[y:y + window_size, x:x + window_size] = gradient_windows
        gradient_windows = np.roll(np.roll(gradient_values, -sx, 1), -sy, 0)
        image += gradient_windows * (1.5 / (np.abs(gradient_windows).mean() + 1e-7))

现在,创建 DeepDream 的优化已完成,可以通过剪切值来保存,如下所示:

image /= 255.0 image = np.uint8(np.clip(image, 0, 1) * 255)
PIL.Image.fromarray(image).save('dream_' + image_name, 'jpeg')

在本节中,我们已经看到了创建 DeepDream 的过程。 结果显示在这里:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oajlSeNr-1681567519369)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-cv/img/1c9b8a0a-3621-4644-97e5-1c0158a051dd.jpg)]

如我们所见,狗到处都被激活。 您可以尝试其他各种层并查看结果。 这些结果可用于艺术目的。 类似地,可以激活其他层以产生不同的伪像。 在下一节中,我们将看到一些对抗性示例,这些示例可能会欺骗深度学习模型。

对抗性示例

在几个数据集上,图像分类算法已达到人类水平的准确率。 但是它们可以被对抗性例子轻易地欺骗。 对抗示例是合成图像,它们使模型无法产生所需的结果。 拍摄任何图像,然后选择不正确的随机目标类别。 可以用噪声修改该图像,直到网络被 Goodfellow 等人所欺骗。 该模型的对抗攻击示例如下所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-46OUGKMg-1681567519369)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-cv/img/5fc3a7dd-4e49-4215-a821-e026811ca3fd.png)]

经 Goodfellow 等人许可复制。

在此图中,左侧显示的图像具有特定标签的 58% 可信度。 左边的图像与中间显示的噪声结合在一起时,在右边形成图像。 对于人来说,带有噪点的图像看起来还是一样。 但是带有噪点的图像可以通过具有 97% 置信度的其他标签来预测。 尽管图像具有非常不同的对象,但仍将高置信度分配给特定示例。 这是深度学习模型的问题,因此,您应该了解这在哪里适用:

  • 甚至可以在不访问模型的情况下生成对抗性示例。 您可以训练自己的模型,生成对抗性示例,但仍然可以欺骗其他模型。
  • 在实践中这种情况很少发生,但是当有人试图欺骗系统来发送垃圾邮件或崩溃时,这将成为一个真正的问题。
  • 所有机器学习模型都容易受到此问题的影响,而不仅仅是深度学习模型。

您应该考虑对抗性示例,了解在安全关键系统上部署深度学习模型的后果。 在下一节中,我们将看到如何利用 TensorFlow Serving 获得更快的推断。

模型推理

任何新数据都可以传递给模型以获取结果。 从图像获取分类结果或特征的过程称为推理。 训练和推理通常在不同的计算机上和不同的时间进行。 我们将学习如何存储模型,运行推理以及如何使用 TensorFlow Serving 作为具有良好延迟和吞吐量的服务器。

导出模型

训练后的模型必须导出并保存。 权重,偏差和图都存储用于推断。 我们将训练 MNIST 模型并将其存储。 首先使用以下代码定义所需的常量:

work_dir = '/tmp' model_version = 9 training_iteration = 1000 input_size = 784 no_classes = 10 batch_size = 100 total_batches = 200

model_version可以是一个整数,用于指定我们要导出以供服务的模型。 feature config存储为具有占位符名称及其对应数据类型的字典。 应该映射预测类及其标签。 身份占位符可与 API 配合使用:

tf_example = tf.parse_example(tf.placeholder(tf.string, name='tf_example'),
  {'x': tf.FixedLenFeature(shape=[784], dtype=tf.float32), })
x_input = tf.identity(tf_example['x'], name='x')

可以使用以下代码使用权重,偏差,对数和优化器定义一个简单的分类器:

y_input = tf.placeholder(tf.float32, shape=[None, no_classes])
weights = tf.Variable(tf.random_normal([input_size, no_classes]))
bias = tf.Variable(tf.random_normal([no_classes]))
logits = tf.matmul(x_input, weights) + bias
softmax_cross_entropy = tf.nn.softmax_cross_entropy_with_logits(labels=y_input, logits=logits)
loss_operation = tf.reduce_mean(softmax_cross_entropy)
optimiser = tf.train.GradientDescentOptimizer(0.5).minimize(loss_operation)

训练模型,如以下代码所示:

mnist = input_data.read_data_sets('MNIST_data', one_hot=True)
for batch_no in range(total_batches):
    mnist_batch = mnist.train.next_batch(batch_size)
    _, loss_value = session.run([optimiser, loss_operation], feed_dict={
        x_input: mnist_batch[0],
  y_input: mnist_batch[1]
    })
    print(loss_value)

定义预测签名,并导出模型。 将模型保存到持久性存储中,以便可以在以后的时间点进行推理。 这将通过反序列化导出数据,并将其存储为其他系统可以理解的格式。 具有不同变量和占位符的多个图可用于导出。 它还支持signature_defs 和素材。 signature_defs指定了输入和输出,因为将从外部客户端访问输入和输出。 素材是将用于推理的非图组件,例如词汇表等。

分类签名使用对 TensorFlow 分类 API 的访问权限。 输入是强制性的,并且有两个可选输出(预测类别和预测概率),其中至少一个是强制性的。 预测签名提供输入和输出数量的灵活性。 可以定义多个输出并从客户端显式查询。 signature_def显示在此处:

signature_def = (
      tf.saved_model.signature_def_utils.build_signature_def(
          inputs={'x': tf.saved_model.utils.build_tensor_info(x_input)},
  outputs={'y': tf.saved_model.utils.build_tensor_info(y_input)},
  method_name="tensorflow/serving/predict"))

最后,使用预测签名将元图和变量添加到构建器中:

model_path = os.path.join(work_dir, str(model_version))
saved_model_builder = tf.saved_model.builder.SavedModelBuilder(model_path)
saved_model_builder.add_meta_graph_and_variables(
      session, [tf.saved_model.tag_constants.SERVING],
  signature_def_map={
          'prediction': signature_def
      },
  legacy_init_op=tf.group(tf.tables_initializer(), name='legacy_init_op'))
saved_model_builder.save()

该构建器已保存,可以由服务器使用。 所示示例适用于任何模型,并可用于导出。 在下一部分中,我们将服务并查询导出的模型。

服务训练过的模型

可以使用以下命令通过 TensorFlow Serving 服务上一节中导出的模型:

tensorflow_model_server --port=9000  --model_name=mnist --model_base_path=/tmp/mnist_model/

model_base_path 指向导出模型的目录。 现在可以与客户端一起测试服务器。 请注意,这不是 HTTP 服务器,因此需要此处显示的客户端而不是 HTTP 客户端。 导入所需的库:

from grpc.beta import implementations
import numpy
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
from tensorflow_serving.apis import predict_pb2
from tensorflow_serving.apis import prediction_service_pb2

添加并发常数,测试数量和工作目录。 定义了一个类,用于对返回的结果进行计数。 定义了远程过程调用RPC)回调,并带有用于对预测计数的计数器,如下所示:

concurrency = 1 num_tests = 100 host = '' port = 8000 work_dir = '/tmp'     def _create_rpc_callback():
  def _callback(result):
      response = numpy.array(
        result.result().outputs['y'].float_val)
      prediction = numpy.argmax(response)
      print(prediction)
  return _callback

根据您的要求修改 hostport_callback方法定义了从服务器返回响应时所需的步骤。 在这种情况下,将计算最大概率。 通过调用服务器来运行推断:

test_data_set = mnist.test
test_image = mnist.test.images[0]
predict_request = predict_pb2.PredictRequest()
predict_request.model_spec.name = 'mnist' predict_request.model_spec.signature_name = 'prediction'   predict_channel = implementations.insecure_channel(host, int(port))
predict_stub = prediction_service_pb2.beta_create_PredictionService_stub(predict_channel)
predict_request.inputs['x'].CopyFrom(
    tf.contrib.util.make_tensor_proto(test_image, shape=[1, test_image.size]))
result = predict_stub.Predict.future(predict_request, 3.0)
result.add_done_callback(
    _create_rpc_callback())

反复调用推理以评估准确率,延迟和吞吐量。 推断错误率应该在 90% 左右,并且并发性应该很高。 导出和客户端方法可用于任何模型,以从模型获得结果和特征。 在下一节中,我们将构建检索流水线。

基于内容的图像检索

基于内容的图像检索CBIR)的技术将查询图像作为输入,并对目标图像数据库中的图像进行排名,从而产生输出。 CBIR 是具有特定目标的图像到图像搜索引擎。 要检索需要目标图像数据库。 返回距查询图像最小距离的目标图像。 我们可以直接将图像用于相似性,但是问题如下:

  • 图像尺寸巨大
  • 像素中有很多冗余
  • 像素不携带语义信息

因此,我们训练了一个用于对象分类的模型,并使用该模型中的特征进行检索。 然后,我们通过相同的模型传递查询图像和目标数据库以获得特征。 这些模型也可以称为编码器,因为它们对特定任务的图像信息进行编码。 编码器应该能够捕获全局和局部特征。 我们可以使用我们在图像分类一章中研究过的模型,这些模型经过训练可以进行分类任务。 由于强力扫描或线性扫描速度较慢,因此图像搜索可能会花费大量时间。 因此,需要一些用于更快检索的方法。 以下是一些加快匹配速度的方法:

  • 局部敏感哈希LSH):LSH 将特征投影到其子空间,并可以向候选对象提供列表,并在以后进行精细特征排名。 这也是我们本章前面介绍的降维技术,例如 PCA 和 t-SNE。 它具有较小尺寸的铲斗。
  • 多索引哈希:此方法对特征进行哈希处理,就像信鸽拟合一样,可以使其更快。 它使用汉明距离来加快计算速度。 汉明距离不过是以二进制表示的数字的位置差异的数量。

这些方法更快,需要更少的内存,但要权衡准确率。 这些方法也没有捕获语义上的差异。 可以根据查询对匹配结果进行重新排名以获得更好的结果。 重新排序可以通过对返回的目标图像重新排序来改善结果。 重新排序可以使用以下技术之一:

  • 几何验证:此方法将几何图形和目标图像与仅返回相似几何图形的目标图像进行匹配。
  • 查询扩展:这将扩展目标图像列表并详尽搜索它们。
  • 相关性反馈:此方法从使用中获取反馈并返回结果。 根据用户输入,将进行重新排名。

这些技术已针对文本进行了很好的开发,可用于图像。 在本章中,我们将重点介绍提取特征并将其用于 CBIR。 在下一节中,我们将学习如何进行模型推断。

建立检索流水线

从查询图像的目标图像中获得最佳匹配的步骤序列称为检索流水线。 检索流水线具有多个步骤或组件。 图像数据库的特征必须脱机提取并存储在数据库中。 对于每个查询图像,必须提取特征并且必须在所有目标图像之间计算相似度。 然后,可以对图像进行排名以最终输出。 检索流水线如下所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xvtT4VFM-1681567519369)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-cv/img/c9ea2c8e-b149-4890-8a5e-ba4d03ade21e.png)]

特征提取步骤必须快速,为此可以使用 TensorFlow Serving。 您可以根据应用选择使用哪些特征。 例如,当需要基于纹理的匹配时可以使用初始层,而当必须在对象级别进行匹配时可以使用更高的层。 在下一部分中,我们将看到如何从预训练的初始模型中提取特征。

提取图像的瓶颈特征

瓶颈特征是在预分类层中计算的值。 在本节中,我们将看到如何使用 TensorFlow 从预训练的模型中提取瓶颈特征。 首先,使用以下代码导入所需的库:

import os
import urllib.request
from tensorflow.python.platform import gfile
import tarfile

然后,我们需要下载带有图定义及其权重的预训练模型。 TensorFlow 已使用初始架构在ImageNet数据集上训练了一个模型,并提供了该模型。 我们将使用以下代码下载该模型并将其解压缩到本地文件夹中:

model_url = 'http://download.tensorflow.org/models/image/imagenet/inception-2015-12-05.tgz' file_name = model_url.split('/')[-1]
file_path = os.path.join(work_dir, file_name)
if not os.path.exists(file_path):
    file_path, _ = urllib.request.urlretrieve(model_url, file_path)
tarfile.open(file_path, 'r:gz').extractall(work_dir)

仅当模型不存在时,这会创建一个文件夹并下载模型。 如果重复执行代码,则不会每次都下载模型。 该图以协议缓冲区protobuf)格式存储在文件中。 必须将其读取为字符串,然后传递给tf.GraphDef()对象以将其带入内存:

model_path = os.path.join(work_dir, 'classify_image_graph_def.pb')
with gfile.FastGFile(model_path, 'rb') as f:
    graph_defnition = tf.GraphDef()
    graph_defnition.ParseFromString(f.read())

在初始模型中,瓶颈层名为pool_3/_reshape:0,并且该层的尺寸为 2,048。 输入的占位符名称为DecodeJpeg/contents:0,调整大小张量名称为ResizeBilinear:0。 我们可以使用tf.import_graph_def和所需的返回张量导入图定义,以进行进一步的操作:

bottleneck, image, resized_input = (
    tf.import_graph_def(
        graph_defnition,
  name='',
  return_elements=['pool_3/_reshape:0',
  'DecodeJpeg/contents:0',
  'ResizeBilinear:0'])
)

进行查询和目标图像并将其加载到内存中。 gfile函数提供了一种更快的方式将图像加载到内存中。

query_image_path = os.path.join(work_dir, 'cat.1000.jpg')
query_image = gfile.FastGFile(query_image_path, 'rb').read()
target_image_path = os.path.join(work_dir, 'cat.1001.jpg')
target_image = gfile.FastGFile(target_image_path, 'rb').read()

让我们定义一个使用session和图像从图像中提取瓶颈特征的函数:

def get_bottleneck_data(session, image_data):
    bottleneck_data = session.run(bottleneck, {image: image_data})
    bottleneck_data = np.squeeze(bottleneck_data)
    return bottleneck_data

启动会话,并传递图像以运行前向推理,以从预先训练的模型中获取瓶颈值:

query_feature = get_bottleneck_data(session, query_image)
print(query_feature)
target_feature = get_bottleneck_data(session, target_image)
print(target_feature)

运行上面的代码应显示如下:

[ 0.55705792 0.36785451 1.06618118 ..., 0.6011821 0.36407694
 0.0996572 ]
[ 0.30421323 0.0926369 0.26213276 ..., 0.72273785 0.30847171
 0.08719242]

该计算特征的过程可以按比例缩放以获取更多目标图像。 使用这些值,可以在查询图像和目标数据库之间计算相似度,如以下部分所述。

计算查询图像与目标数据库之间的相似度

NumPy 的linalg.norm可用于计算欧几里德距离。 可以通过计算特征之间的欧几里得距离来计算查询图像与目标数据库之间的相似度,如下所示:

dist = np.linalg.norm(np.asarray(query_feature) - np.asarray(target_feature))
print(dist)

运行此命令应打印以下内容:

16.9965

这是可用于相似度计算的度量。 查询与目标图像之间的欧几里得距离越小,图像越相似。 因此,计算欧几里得距离是相似度的量度。 使用特征来计算欧几里得距离是基于这样的假设:在训练模型的过程中学习了这些特征。 将这种计算扩展成数百万个图像效率不高。 在生产系统中,期望以毫秒为单位返回结果。 在下一节中,我们将看到如何提高检索效率。

高效检索

检索可能很慢,因为它是蛮力方法。 使用近似最近邻可以使匹配更快。 维度的诅咒也开始出现,如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NHXd7Ou5-1681567519369)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-cv/img/d3c91098-f5f8-42ed-9fac-3ee18ba2baee.png)]

随着维数的增加,复杂度也从二维维增加到三个维。 距离的计算也变慢。 为了使距离搜索更快,我们将在下一部分中讨论一种近似方法。

使用近似最近邻更快地匹配

近似最近邻ANNOY)是一种用于更快进行最近邻搜索的方法。 ANNOY 通过随机投影来构建树。 树结构使查找最接近的匹配更加容易。 您可以创建ANNOYIndex以便快速检索,如下所示:

def create_annoy(target_features):
    t = AnnoyIndex(layer_dimension)
    for idx, target_feature in enumerate(target_features):
        t.add_item(idx, target_feature)
    t.build(10)
    t.save(os.path.join(work_dir, 'annoy.ann'))
create_annoy(target_features)

创建索引需要特征的尺寸。 然后将项目添加到索引并构建树。 树木的数量越多,在时间和空间复杂度之间进行权衡的结果将越准确。 可以创建索引并将其加载到内存中。 可以查询 ANNOY,如下所示:

annoy_index = AnnoyIndex(10)
annoy_index.load(os.path.join(work_dir, 'annoy.ann'))
matches = annoy_index.get_nns_by_vector(query_feature, 20)

匹配项列表可用于检索图像详细信息。 项目的索引将被返回。

请访问这里以获取ANNOY的完整实现,以及其在准确率和速度方面与其他近似最近邻算法的基准比较。

ANNOY 的优点

使用 ANNOY 的原因很多。 主要优点如下:

  • 具有内存映射的数据结构,因此对 RAM 的占用较少。 因此,可以在多个进程之间共享同一文件。
  • 可以使用曼哈顿,余弦或欧几里得等多种距离来计算查询图像和目标数据库之间的相似度。

原始图像的自编码器

自编码器是一种用于生成有效编码的无监督算法。 输入层和目标输出通常相同。 减少和增加之间的层以下列方式:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b4HS6l2e-1681567519370)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-cv/img/2cc688ac-9e9f-447e-b46e-295eea60e0e9.png)]

瓶颈层是尺寸减小的中间层。 瓶颈层的左侧称为编码器,右侧称为解码器。 编码器通常减小数据的尺寸,而解码器增大尺寸。 编码器和解码器的这种组合称为自编码器。 整个网络都经过重建误差训练。 从理论上讲,可以存储瓶颈层,并可以通过解码器网络重建原始数据。 如下所示,这可以减小尺寸并易于编程。 使用以下代码定义卷积,解卷积和全连接层:

def fully_connected_layer(input_layer, units):
    return tf.layers.dense(
        input_layer,
  units=units,
  activation=tf.nn.relu
    )
def convolution_layer(input_layer, filter_size):
    return tf.layers.conv2d(
        input_layer,
  filters=filter_size,
  kernel_initializer=tf.contrib.layers.xavier_initializer_conv2d(),
  kernel_size=3,
  strides=2
  )
def deconvolution_layer(input_layer, filter_size, activation=tf.nn.relu):
    return tf.layers.conv2d_transpose(
        input_layer,
  filters=filter_size,
  kernel_initializer=tf.contrib.layers.xavier_initializer_conv2d(),
  kernel_size=3,
  activation=activation,
  strides=2
  )

定义具有五层卷积的会聚编码器,如以下代码所示:

input_layer = tf.placeholder(tf.float32, [None, 128, 128, 3])
convolution_layer_1 = convolution_layer(input_layer, 1024)
convolution_layer_2 = convolution_layer(convolution_layer_1, 512)
convolution_layer_3 = convolution_layer(convolution_layer_2, 256)
convolution_layer_4 = convolution_layer(convolution_layer_3, 128)
convolution_layer_5 = convolution_layer(convolution_layer_4, 32)

通过展平第五个卷积层来计算瓶颈层。 再次将瓶颈层重新成形为卷积层,如下所示:

convolution_layer_5_flattened = tf.layers.flatten(convolution_layer_5)
bottleneck_layer = fully_connected_layer(convolution_layer_5_flattened, 16)
c5_shape = convolution_layer_5.get_shape().as_list()
c5f_flat_shape = convolution_layer_5_flattened.get_shape().as_list()[1]
fully_connected = fully_connected_layer(bottleneck_layer, c5f_flat_shape)
fully_connected = tf.reshape(fully_connected,
  [-1, c5_shape[1], c5_shape[2], c5_shape[3]])

计算可以重建图像的发散或解码器部分,如以下代码所示:

deconvolution_layer_1 = deconvolution_layer(fully_connected, 128)
deconvolution_layer_2 = deconvolution_layer(deconvolution_layer_1, 256)
deconvolution_layer_3 = deconvolution_layer(deconvolution_layer_2, 512)
deconvolution_layer_4 = deconvolution_layer(deconvolution_layer_3, 1024)
deconvolution_layer_5 = deconvolution_layer(deconvolution_layer_4, 3,
  activation=tf.nn.tanh)

该网络经过训练,可以快速收敛。 传递图像特征时可以存储瓶颈层。 这有助于减少可用于检索的数据库的大小。 仅需要编码器部分即可为特征建立索引。 自编码器是一种有损压缩算法。 它与其他压缩算法不同,因为它从数据中学习压缩模式。 因此,自编码器模型特定于数据。 自编码器可以与 t-SNE 结合使用以获得更好的可视化效果。 自编码器学习的瓶颈层可能对其他任务没有用。 瓶颈层的大小可以大于以前的层。 在这种分叉和收敛连接的情况下,稀疏的自编码器就会出现。 在下一节中,我们将学习自编码器的另一种应用。

使用自编码器进行降噪

自编码器也可以用于图像去噪。 去噪是从图像中去除噪点的过程。 去噪编码器可以无监督的方式进行训练。 可以在正常图像中引入噪声,并针对原始图像训练自编码器。 以后,可以使用完整的自编码器生成无噪声的图像。 在本节中,我们将逐步说明如何去噪 MNIST 图像。 导入所需的库并定义占位符,如下所示:

x_input = tf.placeholder(tf.float32, shape=[None, input_size])
y_input = tf.placeholder(tf.float32, shape=[None, input_size])

x_inputy_input的形状与自编码器中的形状相同。 然后,定义一个密集层,如下所示,默认激活为tanh激活函数。 add_variable_summary方法是从图像分类章节示例中导入的。 密集层的定义如下所示:

def dense_layer(input_layer, units, activation=tf.nn.tanh):
    layer = tf.layers.dense(
        inputs=input_layer,
  units=units,
  activation=activation
    )
    add_variable_summary(layer, 'dense')
    return layer

接下来,可以定义自编码器层。 该自编码器仅具有全连接层。 编码器部分具有减小尺寸的三层。 解码器部分具有增加尺寸的三层。 编码器和解码器都是对称的,如下所示:

layer_1 = dense_layer(x_input, 500)
layer_2 = dense_layer(layer_1, 250)
layer_3 = dense_layer(layer_2, 50)
layer_4 = dense_layer(layer_3, 250)
layer_5 = dense_layer(layer_4, 500)
layer_6 = dense_layer(layer_5, 784)

隐藏层的尺寸是任意选择的。 接下来,定义lossoptimiser。 这里我们使用 Sigmoid 代替 softmax 作为分类,如下所示:

with tf.name_scope('loss'):
    softmax_cross_entropy = tf.nn.sigmoid_cross_entropy_with_logits(
        labels=y_input, logits=layer_6)
    loss_operation = tf.reduce_mean(softmax_cross_entropy, name='loss')
    tf.summary.scalar('loss', loss_operation)
with tf.name_scope('optimiser'):
    optimiser = tf.train.AdamOptimizer().minimize(loss_operation)

TensorBoard 提供了另一种称为image,的摘要,可用于可视化图像。 我们将使用输入layer_6并将其重塑形状以将其添加到摘要中,如下所示:

x_input_reshaped = tf.reshape(x_input, [-1, 28, 28, 1])
tf.summary.image("noisy_images", x_input_reshaped)
y_input_reshaped = tf.reshape(y_input, [-1, 28, 28, 1])
tf.summary.image("original_images", y_input_reshaped)
layer_6_reshaped = tf.reshape(layer_6, [-1, 28, 28, 1])
tf.summary.image("reconstructed_images", layer_6_reshaped)

图像数量默认限制为三张,并且可以更改。 这是为了限制其将所有图像都写入摘要文件夹。 接下来,合并所有摘要,并将图添加到摘要编写器,如下所示:

merged_summary_operation = tf.summary.merge_all()
train_summary_writer = tf.summary.FileWriter('/tmp/train', session.graph)

可以将正常的随机噪声添加到图像中并作为输入张量馈入。 添加噪声后,多余的值将被裁剪。 目标将是原始图像本身。 此处显示了噪声和训练过程的附加信息:

for batch_no in range(total_batches):
    mnist_batch = mnist_data.train.next_batch(batch_size)
    train_images, _ = mnist_batch[0], mnist_batch[1]
    train_images_noise = train_images + 0.2 * np.random.normal(size=train_images.shape)
    train_images_noise = np.clip(train_images_noise, 0., 1.)
    _, merged_summary = session.run([optimiser, merged_summary_operation],
  feed_dict={
        x_input: train_images_noise,
  y_input: train_images,
  })
    train_summary_writer.add_summary(merged_summary, batch_no)

开始此训练后,可以在 TensorBoard 中查看结果。 损失显示在此处:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QCH9KKaB-1681567519370)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-cv/img/2ed9d697-3908-4169-927f-99769d2f7bdf.png)]

Tensorboard 说明了输出图

损耗稳步下降,并将在迭代过程中保持缓慢下降。 这显示了自编码器如何快速收敛。 接下来,原始图像显示三位数:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4RnO5pii-1681567519370)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-cv/img/4e4b465b-0039-41c7-826e-67ba07141257.png)]

以下是添加了噪点的相同图像:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9w94lC0Z-1681567519370)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-cv/img/16852acb-354f-43a2-832c-78fb55edb1ff.png)]

您会注意到有很大的噪音,这是作为输入给出的。 接下来,是使用去噪自编码器重建的相同编号的图像:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5zGECT4h-1681567519371)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-cv/img/fcaa79e6-c455-46a6-8d49-1788aa7be7b6.png)]

您会注意到,去噪自编码器在消除噪声方面做得非常出色。 您可以在测试图像上运行它,并可以看到质量得到保持。 对于更复杂的数据集,可以使用卷积神经网络以获得更好的结果。 该示例展示了计算机视觉深度学习的强大功能,因为它是在无监督的情况下进行训练的。

总结

在本章中,您学习了如何从图像中提取特征并将其用于 CBIR。 您还学习了如何使用 TensorFlow Serving 来推断图像特征。 我们看到了如何利用近似最近邻或更快的匹配而不是线性扫描。 您了解了散列如何仍可以改善结果。 引入了自编码器的概念,我们看到了如何训练较小的特征向量以进行搜索。 还显示了使用自编码器进行图像降噪的示例。 我们看到了使用基于位的比较的可能性,该比较可以将其扩展到数十亿张图像。

在下一章中,我们将看到如何训练对象检测问题的模型。 我们将利用开源模型来获得良好的准确率,并了解其背后的所有算法。 最后,我们将使用所有想法来训练行人检测模型。

四、对象检测

对象检测是在图像中找到对象位置的动作。 在本章中,我们将通过了解以下主题来学习对象检测技术和实现行人检测:

  • 基础知识以及定位和检测之间的区别
  • 各种数据集及其描述
  • 用于对象定位和检测的算法
  • TensorFlow API 用于对象检测
  • 训练新的对象检测模型
  • 基于 YOLO 算法的移动汽车行人检测

检测图像中的对象

近年来,对象检测在应用和研究方面都出现了爆炸式增长。 对象检测是计算机视觉中的重要问题。 与图像分类任务相似,更深的网络在检测方面表现出更好的表现。 目前,这些技术的准确率非常好。 因此,它被用于许多应用中。

图像分类将图像标记为一个整体。 除了标记对象外,找到对象的位置也称为对象定位。 通常,对象的位置由直角坐标定义。 在图像中使用直角坐标查找多个对象称为检测。 这是对象检测的示例:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-arqd7MRT-1681567519371)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-cv/img/07fc811c-af39-4bb1-b78d-16a2914667c2.png)]

该图显示了带有边界框的四个对象。 我们将学习可以执行查找框任务的算法。 这些应用在自动驾驶汽车和工业对象等机器人视觉领域具有巨大的应用前景。 我们可以将定位和检测任务概括为以下几点:

  • 定位检测标签内图像中的一个对象
  • 检测可找到图像中的所有对象以及标签

区别在于对象的数量。 在检测中,存在可变数量的对象。 在设计与定位或检测有关的深度学习模型的架构时,此小差异会带来很大的不同。 接下来,我们将看到可用于任务的各种数据集。

探索数据集

可用于对象定位和检测的数据集很多。 在本节中,我们将探索研究社区用来评估计法的数据集。 有些数据集带有不同数量的对象,这些对象中标注的范围从 20 到 200 不等,这使得对象检测变得困难。 与其他数据集(每个图像仅包含一个对象)相比,某些数据集在一个图像中包含的对象太多。 接下来,我们将详细查看数据集。

ImageNet 数据集

ImageNet 具有用于评估分类,定位和检测任务的数据。 第 2 章,“图像分类”详细讨论了分类数据集。 与分类数据类似,定位任务有 1,000 个类别。 准确率是根据前五次检测得出的。 所有图像中至少会有一个边界框。 有 470,000 张图像的检测问题有 200 个对象,每个图像平均有 1.1 个对象。

PASCAL VOC 挑战

PASCAL VOC 挑战赛于 2005 年至 2012 年进行。该挑战赛被认为是对象检测技术的基准。 数据集中有 20 个类别。 该数据集包含用于训练和验证的 11,530 张图像,以及针对兴趣区域的 27,450 条标注。 以下是数据集中存在的二十个类:

  • 人: 人
  • 动物: 鸟,猫,牛,狗,马,绵羊
  • 车辆: 飞机,自行车,轮船,公共汽车,汽车,摩托车,训练
  • 室内: 水壶,椅子,餐桌,盆栽,沙发,电视/显示器

您可以从这里下载数据集。 每个图像平均有 2.4 个对象。

COCO 对象检测挑战

上下文中的通用对象COCO)数据集具有 200,000 张图像,其中 80 个类别的标注超过 500,000 个。 它是最广泛的公开可用的对象检测数据库。 下图显示了数据集中存在的对象的列表:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aTEbjpud-1681567519371)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-cv/img/277bcdb3-8b17-4c4a-829a-e79e857a354c.png)]

每个图像的平均对象数为 7.2。 这些是对象检测挑战的著名数据集。 接下来,我们将学习如何针对这些数据集评估计法。

使用指标评估数据集

指标对于深度学习任务中的理解至关重要。 由于人工标注,对象检测和定位的度量是特殊的。 人类可能已经标注了一个名为真实情况的框。 真实性不一定是绝对真理。 此外,盒子的像素可能因人而异。 因此,算法很难检测到人类绘制的确切边界框。 交并比IoU)用于评估定位任务。 平均精确度平均值mAP)用于评估检测任务。 我们将在下一部分中看到指标的描述。

交并比

IoU 是真实情况与预测面积的重叠面积与总面积之比。 这是该指标的直观说明:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ew5OgZt3-1681567519371)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-cv/img/6d0305cb-779a-42f0-8bfe-6834eecdca89.png)]

这两个正方形代表真实情况和预测的边界框。 IoU 计算为重叠面积与并集面积之比。 这是给定地面真理和预测边界框的 IoU 计算脚本:

def calculate_iou(gt_bb, pred_bb):
    '''
  :param gt_bb: ground truth bounding box  :param pred_bb: predicted bounding box '''  gt_bb = tf.stack([
        gt_bb[:, :, :, :, 0] - gt_bb[:, :, :, :, 2] / 2.0,
  gt_bb[:, :, :, :, 1] - gt_bb[:, :, :, :, 3] / 2.0,
  gt_bb[:, :, :, :, 0] + gt_bb[:, :, :, :, 2] / 2.0,
  gt_bb[:, :, :, :, 1] + gt_bb[:, :, :, :, 3] / 2.0])
    gt_bb = tf.transpose(gt_bb, [1, 2, 3, 4, 0])
    pred_bb = tf.stack([
        pred_bb[:, :, :, :, 0] - pred_bb[:, :, :, :, 2] / 2.0,
  pred_bb[:, :, :, :, 1] - pred_bb[:, :, :, :, 3] / 2.0,
  pred_bb[:, :, :, :, 0] + pred_bb[:, :, :, :, 2] / 2.0,
  pred_bb[:, :, :, :, 1] + pred_bb[:, :, :, :, 3] / 2.0])
    pred_bb = tf.transpose(pred_bb, [1, 2, 3, 4, 0])
    area = tf.maximum(
        0.0,
  tf.minimum(gt_bb[:, :, :, :, 2:], pred_bb[:, :, :, :, 2:]) -
        tf.maximum(gt_bb[:, :, :, :, :2], pred_bb[:, :, :, :, :2]))
    intersection_area= area[:, :, :, :, 0] * area[:, :, :, :, 1]
    gt_bb_area = (gt_bb[:, :, :, :, 2] - gt_bb[:, :, :, :, 0]) * \
                 (gt_bb[:, :, :, :, 3] - gt_bb[:, :, :, :, 1])
    pred_bb_area = (pred_bb[:, :, :, :, 2] - pred_bb[:, :, :, :, 0]) * \
                   (pred_bb[:, :, :, :, 3] - pred_bb[:, :, :, :, 1])
    union_area = tf.maximum(gt_bb_area + pred_bb_area - intersection_area, 1e-10)
    iou = tf.clip_by_value(intersection_area / union_area, 0.0, 1.0)
    return iou

真实情况和预测的边界框堆叠在一起。 然后在处理负面积的情况下计算面积。 当边界框坐标不正确时,可能会出现负区域。 框的右侧坐标很多发生在从左到左的坐标上。 由于没有保留边界框的结构,因此必然会出现负区域。 计算联合和交叉区域,然后进行最终的 IoU 计算,该计算是真实情况和预测的重合面积与总面积之比。 IoU 计算可以与算法结合使用,以训练定位问题。

面向计算机视觉的深度学习:1~5(4)https://developer.aliyun.com/article/1426861

相关文章
|
19天前
|
机器学习/深度学习 监控 算法
车辆违停检测:基于计算机视觉与深度学习的自动化解决方案
随着智能交通技术的发展,传统人工交通执法方式已难以满足现代城市需求,尤其是在违法停车监控与处罚方面。本文介绍了一种基于计算机视觉和深度学习的车辆违停检测系统,该系统能自动监测、识别并报警违法停车行为,大幅提高交通管理效率,降低人力成本。通过使用YOLO算法进行车辆检测,结合区域分析判断车辆是否处于禁停区,实现了从车辆识别到违停判定的全流程自动化。此系统不仅提升了交通管理的智能化水平,也为维护城市交通秩序提供了技术支持。
|
13天前
|
机器学习/深度学习 人工智能 TensorFlow
探索深度学习与计算机视觉的融合:构建高效图像识别系统
探索深度学习与计算机视觉的融合:构建高效图像识别系统
28 0
|
1月前
|
机器学习/深度学习 人工智能 算法
深度学习在计算机视觉中的突破与未来趋势###
【10月更文挑战第21天】 近年来,深度学习技术极大地推动了计算机视觉领域的发展。本文将探讨深度学习在图像识别、目标检测和图像生成等方面的最新进展,分析其背后的关键技术和算法,并展望未来的发展趋势和应用前景。通过这些探讨,希望能够为相关领域的研究者和从业者提供有价值的参考。 ###
56 4
|
19天前
|
机器学习/深度学习 传感器 算法
行人闯红灯检测:基于计算机视觉与深度学习的智能交通解决方案
随着智能交通系统的发展,传统的人工交通违法判断已难以满足需求。本文介绍了一种基于计算机视觉与深度学习的行人闯红灯自动检测系统,涵盖信号灯状态检测、行人检测与跟踪、行为分析及违规判定与报警四大模块,旨在提升交通管理效率与安全性。
|
22天前
|
机器学习/深度学习 计算机视觉
深度学习与计算机视觉的最新进展
深度学习与计算机视觉的最新进展
|
3月前
|
机器学习/深度学习 人工智能 自然语言处理
深度学习与计算机视觉的结合:技术趋势与应用
深度学习与计算机视觉的结合:技术趋势与应用
189 9
|
4月前
|
机器学习/深度学习 人工智能 自动驾驶
震撼发布!深度学习如何重塑计算机视觉:一场即将改变世界的革命!
【8月更文挑战第6天】随着AI技术的发展,深度学习已成为计算机视觉的核心驱动力。卷积神经网络(CNN)能自动提取图像特征,显著提升识别精度。目标检测技术如YOLO和Faster R-CNN实现了快速精准检测。语义分割利用FCN和U-Net达到像素级分类。这些进展极大提升了图像处理的速度与准确性,为自动驾驶、医疗影像等领域提供了关键技术支撑,预示着计算机视觉更加光明的未来。
42 0
|
5月前
|
机器学习/深度学习 人工智能 自然语言处理
计算机视觉借助深度学习实现了革命性进步,从图像分类到复杂场景理解,深度学习模型如CNN、RNN重塑了领域边界。
【7月更文挑战第2天】计算机视觉借助深度学习实现了革命性进步,从图像分类到复杂场景理解,深度学习模型如CNN、RNN重塑了领域边界。AlexNet开启新时代,后续模型不断优化,推动对象检测、语义分割、图像生成等领域发展。尽管面临数据隐私、模型解释性等挑战,深度学习已广泛应用于安防、医疗、零售和农业,预示着更智能、高效的未来,同时也强调了技术创新、伦理考量的重要性。
68 1
|
5月前
|
机器学习/深度学习 人工智能 固态存储
深度学习在计算机视觉中的应用:重塑视觉感知的未来
【7月更文挑战第1天】深度学习重塑计算机视觉未来:本文探讨了深度学习如何革新CV领域,核心涉及CNN、RNN和自注意力机制。应用包括目标检测(YOLO、SSD等)、图像分类(VGG、ResNet等)、人脸识别及医学影像分析。未来趋势包括多模态融合、语义理解、强化学习和模型可解释性,推动CV向更高智能和可靠性发展。
|
6月前
|
机器学习/深度学习 搜索推荐 自动驾驶
深度学习与计算机视觉的融合发展
深度学习与计算机视觉的融合发展
52 1