Опять мы возвращаемся к сиамским нейро сетям для распознавания людей на фотографиях. Это такая обновленная версия этого года на основе примеров с сайта Keras. Мы уже писали про сиамские нейросети в другой статье, взгляните одним глазом. Ну а мы начинаем прям с нуля и первым делом ставим всю конфигурацию.
Сборка.
Ставим Python 3.9

Я использую IDE PyCharm. Он бесплатный и мне очень нравиться.

Дальше мы ставим библиотеку tensorflow для Python. В нашем случае последняя версия 2.7.0
pip install tensorflow (2.7.0)
Дальше мы ставим библиотеку Matplotlib для наглядной демонстрации статистики.
pip install matplotlib (3.5.0)
Так же ставим библиотеку OpenCV для загрузки картинок в данные для обучения.
pip install opencv-python
Для того что бы обучать сетку на GPU в тензерфлоу нам нужно установить CUDA. Нам требуется версии 11.2 . Набор инструментов CUDA 11.2
Дальше нам нужна библиотека cuDNN SDK 8.1.0 , распаковываем и переносим файли из папок в такие же папки в C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.2
Кодинг.
Для начала импортируем нужные библиотеки:
import random
import numpy as np
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import Model
from tensorflow.keras import layers
import matplotlib.pyplot as plt
import os
import cv2
Гиперпараметры:
epochs = 15
batch_size = 32
margin = 1 # Наценка на конструктивный убыток.
DataSet.
Теперь функция загрузки датасета. Сам датасет мой представляет картинки 100×100 лиц.

def data_set_read(patch):
index = 0
index_foto = []
foto = []
list = os.listdir(patch)
for i in list:
if os.path.isdir(patch + '/' + i):
tmp = os.listdir(patch + '/' + i)
for a in tmp:
image = cv2.imread(patch + '/' + i + '/' + a)
foto.append(image[:, :, 0])
index_foto.append(index)
index += 1
index_foto = np.array(index_foto)
foto = np.array(foto)
foto = foto.astype("float32")
return foto, index_foto
Теперь создадим функцию которая создает пары изображений из dataset. Пара изображений которые относятся к одному лицу и пара разных лиц:
def make_pairs(x, y):
"""Создает кортеж, содержащий пары изображений с соответствующей меткой.
Arguments:
x: Список, содержащий изображения, каждый индекс в этом списке соответствует одному изображению.
y: List containing labels, each label with datatype of `int`.
Returns:
Кортеж, содержащий два массива numpy в виде (pairs_of_samples, метки)
"""
num_classes = max(y) + 1
digit_indices = [np.where(y == i)[0] for i in range(num_classes)]
pairs = []
labels = []
for idx1 in range(len(x)):
# добавить правильный пример
x1 = x[idx1]
label1 = y[idx1]
idx2 = random.choice(digit_indices[label1])
x2 = x[idx2]
pairs += [[x1, x2]]
labels += [1]
# добавьте не правильный пример
label2 = random.randint(0, num_classes - 1)
while label2 == label1:
label2 = random.randint(0, num_classes - 1)
idx2 = random.choice(digit_indices[label2])
x2 = x[idx2]
pairs += [[x1, x2]]
labels += [0]
return np.array(pairs), np.array(labels).astype("float32")
Создает наборы для тренировки и валидации и разделяем пары :
trein_x, trein_y = data_set_read('data')
test_x, test_y = data_set_read('test_set')
# создание тренировочной пары
pairs_train, labels_train = make_pairs(trein_x, trein_y)
# создайте пары проверки
pairs_val, labels_val = make_pairs(test_x, test_y)
# создание пары тестирования
#pairs_test, labels_test = make_pairs(test_x, test_y)
x_train_1 = pairs_train[:, 0]
x_train_2 = pairs_train[:, 1]
x_val_1 = pairs_val[:, 0]
x_val_2 = pairs_val[:, 1]
#x_test_1 = pairs_test[:, 0]
#x_test_2 = pairs_test[:, 1]
Определение сети.
Существует два входных слоя, каждый из которых ведет к своей собственной сети, которая создает вложения. Затем Lambda
слой объединяет их, используя евклидово расстояние, и объединенный вывод подается в конечную сеть.
# Provided two tensors t1 and t2
# Euclidean distance = sqrt(sum(square(t1-t2)))
def euclidean_distance(vects):
"""Найти евклидово расстояние между двумя векторами.
Arguments:
vects: Список, содержащий два тензора одинаковой длины.
Returns:
Тензор, содержащий евклидово расстояние
(как значение с плавающей запятой) между векторами.
"""
x, y = vects
sum_square = tf.math.reduce_sum(tf.math.square(x - y), axis=1, keepdims=True)
return tf.math.sqrt(tf.math.maximum(sum_square, tf.keras.backend.epsilon()))
input = layers.Input((100, 100, 1))
x = tf.keras.layers.BatchNormalization()(input)
x = layers.Conv2D(64, (5, 5), activation="tanh")(x)
x = layers.AveragePooling2D(pool_size=(2, 2))(x)
x = layers.Conv2D(128, (3, 3), activation="tanh")(x)
x = layers.AveragePooling2D(pool_size=(2, 2))(x)
x = layers.Flatten()(x)
x = tf.keras.layers.BatchNormalization()(x)
x = layers.Dense(1000, activation="tanh")(x)
embedding_network = keras.Model(input, x)
input_1 = layers.Input((100, 100, 1))
input_2 = layers.Input((100, 100, 1))
# Как упоминалось выше, сиамская сеть разделяет веса между сети вышек (дочерние сети).
# Чтобы позволить это, мы будем использовать одна и та же сеть встраивания для обеих сетей вышек.
tower_1 = embedding_network(input_1)
tower_2 = embedding_network(input_2)
merge_layer = layers.Lambda(euclidean_distance)([tower_1, tower_2])
#normal_layer = tf.keras.layers.BatchNormalization()(merge_layer)
output_layer = layers.Dense(1, activation="sigmoid")(merge_layer)
siamese = keras.Model(inputs=[input_1, input_2], outputs=output_layer)
Определение функции потери.
Contrastive Loss (контрастная потеря) в сиамской сети функция потерь используется как контрастная потеря, которая может эффективно справляться с взаимосвязью парных данных в сиамской сети. Эта функция потерь изначально была предложена Яном Лекуном Dimensionality Reduction by Learning an Invariant Mapping,
def loss(margin=1):
"""Обеспечивает 'constrastive_loss' охватывающая область с переменной 'margin'.
Arguments:
margin: Целое число, определяет базовую линию для расстояния, для которого пары
следует классифицировать как непохожие. - (default is 1).
Returns:
'constrastive_loss' функция с данными ('margin') attached.
"""
# Contrastive loss = mean( (1-true_value) * square(prediction) +
# true_value * square( max(margin-prediction, 0) ))
def contrastive_loss(y_true, y_pred):
"""Calculates the constrastive loss.
Arguments:
y_true: Список меток, каждая метка имеет тип float32.
y_pred: Список прогнозов той же длины, что и у y_true,
каждая метка имеет тип float32.
Returns:
Тензор, содержащий конструктивные потери в виде значения с плавающей запятой.
"""
square_pred = tf.math.square(y_pred)
margin_square = tf.math.square(tf.math.maximum(margin - (y_pred), 0))
return tf.math.reduce_mean(
(1 - y_true) * square_pred + (y_true) * margin_square
)
return contrastive_loss
Компиляция и обучение сети.
siamese.compile(loss=loss(margin=margin), optimizer="RMSprop", metrics=["accuracy"])
siamese.summary()
history = siamese.fit(
[x_train_1, x_train_2],
labels_train,
validation_data=([x_val_1, x_val_2], labels_val),
batch_size=batch_size,
epochs=epochs,
)
Обучение и визуализация.
def plt_metric(history, metric, title, has_valid=True):
"""Выводит заданную "метрику" из "истории'.
Arguments:
history: атрибут истории объекта истории, возвращенный из Model.fit.
metric: Метрика для построения, строковое значение, присутствующее в качестве ключа в 'history'.
title: Строка, которая будет использоваться в качестве заголовка
has_valid: Boolean, значение true, если в модель были переданы допустимые данные else false.
"""
plt.plot(history[metric])
if has_valid:
plt.plot(history["val_" + metric])
plt.legend(["train", "validation"], loc="upper left")
plt.title(title)
plt.ylabel(metric)
plt.xlabel("epoch")
plt.show()
# accuracy
plt_metric(history=history.history, metric="accuracy", title="Model accuracy")
# constrastive loss
plt_metric(history=history.history, metric="loss", title="Constrastive Loss")
#results = siamese.evaluate([x_test_1, x_test_2], labels_test)
#print("test loss, test acc:", results)