Engee 文档
Notebook

神经网络图像识别与嵌入

此示例将允许您在需要图像分类或计划使用ResNet神经网络从图像或矩阵中提取特征的项目中迈出第一步。

任务说明

计算机视觉的许多最复杂的任务都可以使用"基础"神经网络来解决,过去十年的杰出成就之一是ResNet网络,在其计算上,甚至在其中间结果上,许多现代方法

要使用此神经网络,最好使用一个库,该库提供对预先训练的神经网络的坚实队列的访问。, Metalhead. 此库中的神经网络以以下格式创建 Flux 这个库的命令将允许您轻松地使用模型的拓扑。 最后,我们需要一个图书馆。 DataAugmentation 为了简化对图像的操作,尽管所有相同的操作(缩放和光栅化)都可以使用常规的矩阵加法和乘法来执行。

In [ ]:
Pkg.add( ["Flux", "Metalhead", "DataAugmentation"] )

识别单个图像

让我们对预训练的ResNet神经网络进行一个小计算,这将允许我们确定图像中表示的对象的类。

In [ ]:
using Flux, Metalhead
model = ResNet(18; pretrain = true);

创建ResNet神经网络是为了处理RGB图像. 因此,除了上传图像本身,我们将其转换为三通道格式,摆脱alpha通道,使用一个简单的命令 RGB 应用于图像矩阵的每个像素。

In [ ]:
using Images
img = RGB.(load( "dog.png" ))
Out[0]:
No description has been provided for this image
In [ ]:
using DataAugmentation
DATA_MEAN = (0.485, 0.456, 0.406)
DATA_STD = (0.229, 0.224, 0.225)
augmentations = CenterResizeCrop((224, 224)) |> ImageToTensor() |> Normalize(DATA_MEAN, DATA_STD)

data = apply(augmentations, Image(img)) |> itemdata
labels = readlines( "imagenet_classes_ru.txt" )
img_labels = Flux.onecold( model(Flux.unsqueeze(data, 4)), labels )
print( img_labels )
["эскимосская собака"]

请注意,我们已经获得了一个向量。 随后,我们将取此向量的第一个元素。

因此,如果从ImageNet数据集的角度来看图像不是太不寻常,那么在该数据集上训练的神经网络以可预测的精度识别它。 我们使用了最小的"教科书"ResNet实现,大小为18层。

要找出库中可用的其他ResNet神经网络的大小,您可以使用此命令简单地调用help:

In [ ]:
# ?ResNet

识别多个图像

要对多个图像执行此操作,您可以简单地将神经网络调用打包成一个循环。 for. 一个由18层组成的小型神经网络,即使在CPU上,也会在几秒钟内运行。

In [ ]:
using Flux, Metalhead, Images, DataAugmentation

对多个图像进行分类:

In [ ]:
# Цепочка предобработки включает стандартных для ImageNet нормализацию
DATA_MEAN = (0.485, 0.456, 0.406)
DATA_STD = (0.229, 0.224, 0.225)
augmentations = CenterResizeCrop((224, 224)) |> ImageToTensor() |> Normalize(DATA_MEAN, DATA_STD)

# Загрузим изображения средствами библиотеки Images
imgs = load.("imgs/" .* ["dog1.png", "dog2.png", "cat1.png", "cat2.png", "chair1.png", "chair2.png", "rocket.png", "airplane.png"])
imgs = [ RGB.(img) for img in imgs ]

# Загрузим предобученную модель из Metalhead
model = ResNet(18; pretrain = true);

# Преобразуем каждое изображение
imgs_data = [ apply(augmentations, Image(img)) |> itemdata for img in imgs ]

# Загрузим перечень классов из текстового файла
labels = readlines("imagenet_classes_ru.txt")

# Отправляем каждое изображение в нейросеть и получаем индекс наиболее вероятной метки
img_labels = [Flux.onecold(model(Flux.unsqueeze(data, 4)), labels)[1] for data in imgs_data]

# Вывод изображений и подписей к ним
println(img_labels)
imgs
["золотистый ретривер", "чихуахуа", "сиамская кошка", "африканский хамелеон", "раскладной стул", "диван-кровать", "снаряд", "авиалайнер"]
Out[0]:
No description has been provided for this imageNo description has been provided for this imageNo description has been provided for this imageNo description has been provided for this imageNo description has been provided for this imageNo description has been provided for this imageNo description has been provided for this imageNo description has been provided for this image
(a vector displayed as a row to save space)

显然,并非此列表中的所有标签都是完美匹配的。 与往常一样,训练样本的局限性受到影响,最后的图像超出了可靠识别的界限。

中间延迟表示

对于使用ResNet神经网络的更复杂工作,您可以使用这些神经网络在计算结果期间创建的对象的中间表示形式。 一种选择是排除最终图层并仅使用 backbone 模型的一部分(基本模型?),可以用作其他任务的特征生成器。

In [ ]:
# Первая часть ResNet, называемая backbone моделью, принимает изображения любого размера
backbone_model = Metalhead.backbone( model );
img_embeddings = Flux.activations(backbone_model, Flux.unsqueeze(data[1], 4))
length( img_embeddings )
Out[0]:
5

为什么没有18层? 除了我们正在使用ResNet的缩短版本之外,这个矩阵的一些层被组合在一起。 例如, ConvMeanPool 它们形成了类型的一层 Chain. 功能 activations 仅分析对象 Chain 的顶层。

神经网络在最后一层生成什么大小的对象? backbone 零件?

In [ ]:
size(img_embeddings[end])
Out[0]:
(7, 7, 512, 1)

你可以确保我们面前有512张7乘7的图像。 最后一个维度是批号,但我们将图像逐个发送到神经网络,因此第4个维度中的维度为1。

让我们建立一个插图,我们将看到由网络的卷积部分获得的图像的所有512层。

In [ ]:
# Предположим, img_embeddings[end] имеет размер (7, 7, 512, 1)
img_activations = dropdims(img_embeddings[end], dims=4)  # Удаляем последнюю размерность -> (7, 7, 512)

# Параметры тайлов, Количество тайлов в строке и столбце
tile_height, tile_width, n_tiles_per_row, n_tiles_per_col = 7, 7, 32, 16

# Создаем пустую матрицу для tilemap
tilemap = zeros(Float32, tile_height * n_tiles_per_col, tile_width * n_tiles_per_row)

# Заполняем tilemap
for k in 1:min(512, n_tiles_per_row * n_tiles_per_col)  # На случай, если активаций меньше чем 512
    i = div(k - 1, n_tiles_per_row) + 1  # Номер строки в tilemap
    j = mod(k - 1, n_tiles_per_row) + 1  # Номер столбца в tilemap
    
    # Вычисляем координаты в tilemap
    row_range = (1:tile_height) .+ (i-1)*tile_height
    col_range = (1:tile_width) .+ (j-1)*tile_width
    
    # Вставляем активации
    tilemap[row_range, col_range] = img_activations[:, :, k]
end

# Визуализация
heatmap(tilemap, 
       aspect_ratio=:equal, color=:viridis, cbar=false,
       title="Карта активаций предпоследнего слоя (7×7×512 → 224×112)")
Out[0]:

我们看到很多小的"图像"。 实际上,神经网络将三层图像转换为512层图像,同时将原始图像缩小到7乘7的大小。

这种表示对于研究神经网络的内部结构很有用,但是要使用"原始"特征描述,您需要对这种表示应用类似MeanPool的操作,并使用特征向量而不是大量小图像。

In [ ]:
# Добавление слоя mean pooling - Размер пулинга должен соответствовать выходу feature extractor
pool_model = Chain(
    backbone_model,
    AdaptiveMeanPool((1, 1)),
    Flux.flatten
)

# Загрузка и предобработка
imgs = load.("imgs/" .* ["dog1.png", "dog2.png", "cat1.png", "cat2.png", "chair1.png", "chair2.png", "rocket.png", "airplane.png"])
imgs = [ RGB.(img) for img in imgs ]
imgs_data = [ apply(augmentations, Image(img)) |> itemdata for img in imgs ]

# Классификация моделью `model`` (чтобы получить метки классов)
img_labels = [Flux.onecold(model(Flux.unsqueeze(data, 4)), labels)[1] for data in imgs_data]

# Еще одна классификация моделью `pool_model` (просто чтобы получить эмбеддинги)
mlp_activations_512 = [Flux.activations( pool_model, Flux.unsqueeze(data, 4) )[end] for data in imgs_data];
plot(
    ([heatmap(reshape( activation, :, 32 ), cbar=false, title=title) for (activation,title) in zip(mlp_activations_512, img_labels)])...,
    layout=( 1,: ), size=(900,200)
)
Out[0]:

最终的潜在代表

潜在表示嵌入)可以被认为是神经网络的任何切片上的信息。

ResNet-18的最后一层将我们在上一张图中看到的512个特征转换为1000个特征,其中表达最强烈的特征表征了object类。

标签在其他"像素"之间的分布也可以告诉我们一些关于神经网络在其预测中的置信度或关于对象的类的信息,但由于神经网络没有学会分类高级概念(狗/猫),而是学会放置特定标签,所有品种的狗和所有品种的猫之间的类内接近度可能很低。

In [ ]:
DATA_MEAN = (0.485, 0.456, 0.406)
DATA_STD = (0.229, 0.224, 0.225)
augmentations = CenterResizeCrop((224, 224)) |> ImageToTensor() |> Normalize(DATA_MEAN, DATA_STD)

imgs = load.("imgs/" .* ["dog1.png", "dog2.png", "cat1.png", "cat2.png", "chair1.png", "chair2.png", "rocket.png", "airplane.png"])
imgs = [ RGB.(img) for img in imgs ]

model = ResNet(18; pretrain = true);

imgs_data = [ apply(augmentations, Image(img)) |> itemdata for img in imgs ]
labels = readlines("imagenet_classes_ru.txt")
img_labels = [Flux.onecold(model(Flux.unsqueeze(data, 4)), labels)[1] for data in imgs_data]

mlp_activations = [Flux.activations( model.layers, Flux.unsqueeze(data, 4) )[end] for data in imgs_data];
plot(
    ([heatmap(reshape( activation, 20, : ), cbar=false, title=title) for (activation,title) in zip(mlp_activations, img_labels)])...,
    layout=( 1,: ), size=(900,300), titlefont = Plots.font(9)
)
Out[0]:

在CPU上,前一个单元格的执行需要10-15秒。 使用批次可以加快分类速度。

最后,我们来构建一个关联图。 如果我们使用预训练的神经网络对谱进行分类,那么在任何机器学习之前,这个结果可能已经引起了人们的兴趣。

In [ ]:
plot( heatmap( cor( hcat(mlp_activations_512...) ), title="512 признаков", c=:viridis, yflip=true, xticks=(1:8, img_labels), yticks=(1:8, img_labels)),
      heatmap( cor( hcat(mlp_activations...) ), title="1000 признаков", c=:viridis, yflip=true, xticks=(1:8, img_labels), yticks=(1:8, img_labels) ),
      size=(1000,500))
Out[0]:

从这个插图来看,家具组的对象比动物世界的对象更接近彼此,但需要更多的对象来进行全面的研究。 请注意,从神经网络的角度来看,吉娃娃的图像与家具最不相似。

直到这一层,神经网络已经被训练来提取与分类相关的特征到声明的1000个类中,以便最终嵌入的分布可以提供信息。 但是,由于训练的最终目标是最大化相关logit的响应,即1,000个中的一个(而不是研究类分布),因此在语义上接近的对象(不同的狗品种)之间可能没有预期的

结论

我们深入研究了ResNet神经网络对图像的分类,并研究了这个神经网络的嵌入(潜在表示)是什么样子的,以及它们如何在数据分析中对我们有用。