Сиамская нейросеть — один из простейших и наиболее популярных алгоритмов однократного обучения. Методики, при которой для каждого класса берётся лишь по одному учебному примеру. Таким образом, сиамская сеть обычно используется в приложениях, где в каждом классе есть не так много единиц данных.
Допустим, нам нужно сделать модель распознавания лиц для организации, в которой работает около 500 человек. Если делать такую модель с нуля на основе свёрточной нейросети (Convolutional Neural Network (CNN)), то для обучения модели и достижения хорошей точности распознавания нам понадобится много изображений каждого из этих 500 человек. Но очевидно, что такой датасет нам не собрать, поэтому не стоит делать модель на основе CNN или иного алгоритма глубокого обучения, если у нас нет достаточного количества данных. В подобных случаях можно воспользоваться сложным алгоритмом однократного обучения, наподобие сиамской сети, которая может обучаться на меньшем количестве данных.
По сути, сиамские сети состоят из двух симметричных нейросетей, с одинаковыми весами и архитектурой, которые в конце объединяются и используют функцию энергии — E.
Давайте разберёмся в сиамской сети, создав на её основе модель распознавания лиц. Мы научим её определять, когда два лица одинаковы, а когда нет. И для начала воспользуемся датасетом AT&T Database of Faces, который можно скачать с сайта компьютерной лаборатории Кембриджского Университета.
Скачали, распаковали и видим папки от s1 до s40:
В каждой папке лежит 10 разных фотографий какого-то одного человека, снятых с разных углов. Вот содержимое папки s1:
А вот что находится в папке s13:
Сиамским сетям нужно подавать на вход парные значения с маркировками, поэтому давайте создадим такие наборы. Возьмём из одной папки две случайные фотографии и пометим как «подлинную» пару (genuine). Затем возьмем две фотографии из разных папок и пометим как «ложную» пару (imposite):
Распределив все фотографии по маркированным парам, займемся обучением сети. Из каждой пары мы одну фотографию передадим сети А, а вторую — сети Б. Обе сети всего лишь извлекают векторы свойств. Для этого воспользуемся двумя свёрточными слоями с активациями блоков линейной ректификации (rectified linear unit (ReLU)). Изучив свойства, мы передаем сгенерированные обеими сетями векторы в функцию энергии, которая оценивает сходство. В качестве функции воспользуемся евклидовым расстоянием.
А теперь рассмотрим все эти этапы подробнее
Сначала импортируем необходимые библиотеки:
import re
import numpy as np
from PIL import Image
from sklearn.model_selection import train_test_split
from keras import backend as K
from keras.layers import Activation
from keras.layers import Input, Lambda, Dense, Dropout, Convolution2D, MaxPooling2D, Flatten
from keras.models import Sequential, Model
from keras.optimizers import RMSprop
Теперь определим функцию для чтения входных изображений. Функция
read_image
берет картинку и возвращает NumPy-массив:def read_image(filename, byteorder='>'):
#first we read the image, as a raw file to the buffer
with open(filename, 'rb') as f:
buffer = f.read()
#using regex, we extract the header, width, height and maxval of the image
header, width, height, maxval = re.search(
b"(^P5\s(?:\s*#.*[\r\n])*"
b"(\d+)\s(?:\s*#.*[\r\n])*"
b"(\d+)\s(?:\s*#.*[\r\n])*"
b"(\d+)\s(?:\s*#.*[\r\n]\s)*)", buffer).groups()
#then we convert the image to numpy array using np.frombuffer which interprets buffer as one dimensional array
return np.frombuffer(buffer,
dtype='u1' if int(maxval)
Для примера откроем эту фотографию:
Image.open("data/orl_faces/s1/1.pgm")
Передадим её функции
read_image
и получим NumPy-массив:img = read_image('data/orl_faces/s1/1.pgm')
img.shape
(112, 92)
Теперь определим функцию
get_data
, которая будет генерировать данные. Напомню, что сиамским сетям нужно подавать пары данных (genuine и imposite) с двоичной маркировкой.Сначала прочитаем изображения (
img1
, img2
) из одной директории, сохраним их в массиве x_genuine_pair,
присвоим y_genuine
значение 1
. Затем прочитаем изображения (img1
, img2
) из разных директорий, сохраним их в паре x_imposite,
и присвоим y_imposite
значение 0
.Конкатенируем
x_genuine_pair
и x_imposite
в X
, а y_genuine
и y_imposite
— в Y
:size = 2
total_sample_size = 10000
def get_data(size, total_sample_size):
#read the image
image = read_image('data/orl_faces/s' + str(1) + '/' + str(1) + '.pgm', 'rw+')
#reduce the size
image = image[::size, ::size]
#get the new size
dim1 = image.shape[0]
dim2 = image.shape[1]
count = 0
#initialize the numpy array with the shape of [total_sample, no_of_pairs, dim1, dim2]
x_geuine_pair = np.zeros([total_sample_size, 2, 1, dim1, dim2]) # 2 is for pairs
y_genuine = np.zeros([total_sample_size, 1])
for i in range(40):
for j in range(int(total_sample_size/40)):
ind1 = 0
ind2 = 0
#read images from same directory (genuine pair)
while ind1 == ind2:
ind1 = np.random.randint(10)
ind2 = np.random.randint(10)
# read the two images
img1 = read_image('data/orl_faces/s' + str(i+1) + '/' + str(ind1 + 1) + '.pgm', 'rw+')
img2 = read_image('data/orl_faces/s' + str(i+1) + '/' + str(ind2 + 1) + '.pgm', 'rw+')
#reduce the size
img1 = img1[::size, ::size]
img2 = img2[::size, ::size]
#store the images to the initialized numpy array
x_geuine_pair[count, 0, 0, :, :] = img1
x_geuine_pair[count, 1, 0, :, :] = img2
#as we are drawing images from the same directory we assign label as 1. (genuine pair)
y_genuine[count] = 1
count += 1
count = 0
x_imposite_pair = np.zeros([total_sample_size, 2, 1, dim1, dim2])
y_imposite = np.zeros([total_sample_size, 1])
for i in range(int(total_sample_size/10)):
for j in range(10):
#read images from different directory (imposite pair)
while True:
ind1 = np.random.randint(40)
ind2 = np.random.randint(40)
if ind1 != ind2:
break
img1 = read_image('data/orl_faces/s' + str(ind1+1) + '/' + str(j + 1) + '.pgm', 'rw+')
img2 = read_image('data/orl_faces/s' + str(ind2+1) + '/' + str(j + 1) + '.pgm', 'rw+')
img1 = img1[::size, ::size]
img2 = img2[::size, ::size]
x_imposite_pair[count, 0, 0, :, :] = img1
x_imposite_pair[count, 1, 0, :, :] = img2
#as we are drawing images from the different directory we assign label as 0. (imposite pair)
y_imposite[count] = 0
count += 1
#now, concatenate, genuine pairs and imposite pair to get the whole data
X = np.concatenate([x_geuine_pair, x_imposite_pair], axis=0)/255
Y = np.concatenate([y_genuine, y_imposite], axis=0)
return X, Y
Теперь сгенерируем данные и проверим их размер. У нас 20 000 фотографий, из которых собрано 10 000 подлинных и 10 000 ложных пар:
X, Y = get_data(size, total_sample_size)
X.shape
(20000, 2, 1, 56, 46)
Y.shape
(20000, 1)
Разделим весь массив информации: 75 % пар пойдет на обучение, а 25 % — на тестирование:
x_train, x_test, y_train, y_test = train_test_split(X, Y, test_size=.25)
Теперь создадим сиамскую сеть. Сначала определим базовую сеть — это будет свёрточная нейросеть для извлечения свойств. Создадим два свёрточных слоя с помощью ReLU-активаций и слоя с определением максимального значения (max pooling) после flat-слоя:
def build_base_network(input_shape):
seq = Sequential()
nb_filter = [6, 12]
kernel_size = 3
#convolutional layer 1
seq.add(Convolution2D(nb_filter[0], kernel_size, kernel_size, input_shape=input_shape,
border_mode='valid', dim_ordering='th'))
seq.add(Activation('relu'))
seq.add(MaxPooling2D(pool_size=(2, 2)))
seq.add(Dropout(.25))
#convolutional layer 2
seq.add(Convolution2D(nb_filter[1], kernel_size, kernel_size, border_mode='valid', dim_ordering='th'))
seq.add(Activation('relu'))
seq.add(MaxPooling2D(pool_size=(2, 2), dim_ordering='th'))
seq.add(Dropout(.25))
#flatten
seq.add(Flatten())
seq.add(Dense(128, activation='relu'))
seq.add(Dropout(0.1))
seq.add(Dense(50, activation='relu'))
return seq
Затем передадим пару изображений базовой сети, которая вернёт векторные представления, то есть векторы свойств:
input_dim = x_train.shape[2:]
img_a = Input(shape=input_dim)
img_b = Input(shape=input_dim)
base_network = build_base_network(input_dim)
feat_vecs_a = base_network(img_a)
feat_vecs_b = base_network(img_b)
feat_vecs_a
и feat_vecs_b
— это векторы свойств пары изображений. Давайте передадим их функции энергии для вычисления дистанции между ними. А в качестве функции энергии воспользуемся евклидовым расстоянием:def euclidean_distance(vects):
x, y = vects
return K.sqrt(K.sum(K.square(x - y), axis=1, keepdims=True))
def eucl_dist_output_shape(shapes):
shape1, shape2 = shapes
return (shape1[0], 1)
distance = Lambda(euclidean_distance, output_shape=eucl_dist_output_shape)([feat_vecs_a, feat_vecs_b])
Зададим число эпох равным 13, применим свойство RMS для оптимизации и объявим модель:
epochs = 13
rms = RMSprop()
model = Model(input=[input_a, input_b], output=distance)
Теперь определим функцию потерь
contrastive_loss
function и скомпилируем модель:def contrastive_loss(y_true, y_pred):
margin = 1
return K.mean(y_true * K.square(y_pred) + (1 - y_true) * K.square(K.maximum(margin - y_pred, 0)))
model.compile(loss=contrastive_loss, optimizer=rms)
Займемся обучением модели:
img_1 = x_train[:, 0]
img_2 = x_train[:, 1]
model.fit([img_1, img_2], y_train, validation_split=.25, batch_size=128, verbose=2, nb_epoch=epochs)
Вы видите, как потери снижаются по мере прохождения эпох:
Train on 11250 samples, validate on 3750 samples
Epoch 1/13
- 60s - loss: 0.2179 - val_loss: 0.2156
Epoch 2/13
- 53s - loss: 0.1520 - val_loss: 0.2102
Epoch 3/13
- 53s - loss: 0.1190 - val_loss: 0.1545
Epoch 4/13
- 55s - loss: 0.0959 - val_loss: 0.1705
Epoch 5/13
- 52s - loss: 0.0801 - val_loss: 0.1181
Epoch 6/13
- 52s - loss: 0.0684 - val_loss: 0.0821
Epoch 7/13
- 52s - loss: 0.0591 - val_loss: 0.0762
Epoch 8/13
- 52s - loss: 0.0526 - val_loss: 0.0655
Epoch 9/13
- 52s - loss: 0.0475 - val_loss: 0.0662
Epoch 10/13
- 52s - loss: 0.0444 - val_loss: 0.0469
Epoch 11/13
- 52s - loss: 0.0408 - val_loss: 0.0478
Epoch 12/13
- 52s - loss: 0.0381 - val_loss: 0.0498
Epoch 13/13
- 54s - loss: 0.0356 - val_loss: 0.0363
А теперь проверим работу модели на тестовых данных:
pred = model.predict([x_test[:, 0], x_test[:, 1]])
Определим функцию для вычисления точности:
def compute_accuracy(predictions, labels):
return labels[predictions.ravel()
Вычислим точность:
compute_accuracy(pred, y_test)
0.9779092702169625
Выводы
В этом руководстве мы научились создавать модели распознавания лиц на основе сиамских сетей. Архитектура таких сетей состоит из двух одинаковых нейросетей, имеющих один вес и структуру, а результаты их работы передаются в одну функцию энергии — таким образом определяется одинаковость входных данных. Подробнее о метаобучении с помощью Python читайте в книге Hands-On Meta-Learning with Python.
Мой комментарий
Знание сиамский сетей на данный момент необходимо при работе с изображениями. Существует много подходов к обучению сетей в условиях небольших выборок, генерация новых данных, методы аугментации. Данный способ позволяет относительно «дешево» достичь неплохих результатов, вот более классический пример сиамской сети на «Hello world» для нейронных сетей — датасете MNIST keras.io/examples/mnist_siamese