Распознавание лиц с помощью сиамских сетей

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.


Сиамская нейросеть — один из простейших и наиболее популярных алгоритмов однократного обучения. Методики, при которой для каждого класса берётся лишь по одному учебному примеру. Таким образом, сиамская сеть обычно используется в приложениях, где в каждом классе есть не так много единиц данных.

Допустим, нам нужно сделать модель распознавания лиц для организации, в которой работает около 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
Источник: https://habr.com/ru/company/jetinfosystems/blog/465279/


Интересные статьи

Интересные статьи

Ознакомившись с этим руководством, вы научитесь легко задавать целевые уровни обслуживания (SLO, от англ. Service Level Objectives) для работоспособности сервисов в Kubernetes с помощью&n...
Статья, перевод которой мы сегодня публикуем, посвящена новому API Idle Detection. Этот API уведомляет разработчиков при бездействии пользователя, указывая на то, что пользователь не ...
Некоторое время назад меня попросили сделать видео-инструкцию по настройке сети в операционной системе DOS. К сожалению в съёмке видео я не силен, поэтому постараюсь максимально подробно письменн...
Приступая к животрепещущей теме резервного копирования на «Битрикс», прежде всего хотелось бы поблагодарить разработчиков, реализовавших автоматическое резервное копирование в облачное хранилище в вер...
Автокэширование в 1с-Битрикс — хорошо развитая и довольно сложная система, позволяющая в разы уменьшить число обращений к базе данных и ускорить выполнение страниц.