Проект распознавания выражения лица. Обзор

Описание проблемы распознавания выражения лица

Здравствуйте и вновь добро пожаловать на наши занятия.

В этой лекции я дам вам описание задачи классификации выражения лица и покажу, где можно достать данные. Весь код, используемый в этом разделе курса, находится в отдельной папке проекта на github. Чтобы получить код, вы можете перейти по адресу https://github.com/lazyprogrammer/facial-expression-recognition.

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

Итак, в чём заключается распознавание выражения лица? Ну, люди очень хорошо распознают чувства человека по его лицу. Но может ли это сделать компьютер? Обратите внимание – компьютер не понимает чувства. Это лишь обучение компьютера, какой набор пикселей на рисунке коррелируется с каким-либо чувством. Но всё равно, это интересная задача, потому что вы, к примеру, можете определить состояние человека лишь по фотографии в социальных сетях. Скорее всего, в Facebook и других соцсетях есть специальные команды, которые занимаются тем же.

К счастью для нас, на Kaggle есть бесплатный набор предварительно обработанных и помеченных данных, позволяющий с большей лёгкостью решать эту задачу. Просто перейдите по адресу. Kaggle вообще является замечательным местом для проверки ваших навыков по машинному обучению со всеми типами данных из разнообразнейших отраслей знаний, включая биологию, бизнес, страхование, физику и многое другое. Здесь даже можно выиграть деньги!

Итак, каковы же нужные нам данные? Это набор чёрно-белых изображений лиц размером 48×48. Изображения были предварительно обработаны, чтобы лица находились по центру, а каждое изображение занимало примерно одинаковый объём. Каждое изображение лица выражает чувство, а наша задача в том, чтобы классифицировать, какое из семи чувств оно выражает – нуль для гнева, единица для отвращения, двойка для страха, тройка для счастья, четвёрка для грусти, пятёрка для удивления и шестёрка – для нейтрального выражения лица.

Данные отображены в формате CSV, причём первый столбец означает метку, второй – пространственно разделённые пиксели в изображении, а третий – принадлежат ли данные к учебному или проверочному набору. Последний столбец мы проигнорируем, поскольку сами случайным образом поделим данные на учебный и проверочный наборы.

Итак, обратимся к изображениям, но прежде пройдёмся по уже написанному мною коду для их отображения.

Сначала мы, как обычно, импортируем библиотеки Numpy и Matplotlib. У нас также файл данных util.py и функция getData из этого файла, которая загружает данные и предварительно их обрабатывает.

import numpy as np

import matploylib as plt

from util import getData

У нас также есть таблица меток, а её входными переменными являются целые числа от 0 до 6. Значение меток мы уже обсудили ранее.

label_map – (‘Anger’, ‘Disgust’, ‘Fear’, ‘Happy’, ‘Sad’, ‘Surprise’, ‘Neutral’)

Теперь перейдём к основной функции. Первая строка основной функции – это просто получение данных. Следующим идёт бесконечный цикл прохода. Для каждого из семи чувств мы выбираем все входные данные, соответствующие этому чувству, и находим номер соответствующих входных данных. Затем случайным образом выбираем данные и выводим на экран. Поскольку наши данные заданы плоским вектором, изображение необходимо перерисовать в размере 48×48; цвет указан как серый, поскольку наши фотографии являются чёрно-белыми. Далее мы выводим на экран изображение с соответствующим чувством. Далее, поскольку цикл бесконечный, я написал код с запросом, выйти из цикла или нет. Если вы желаете выйти, надо нажать заглавное Y, после чего бесконечный цикл завершается.

def main():

X, Y = getData(balance_ones=False)

while True:

for i in xrange(7):

x, y = X[Y==i], Y[Y==1]

N = len(y)

j = np.random.choise(N)

plt.imshow(x[j].reshape(48, 48), cmap=’gray’)

plt.title(label_map[y[j]])

plt.show()

prompt = raw_input(‘Quit? Enter Y:\n’)

if prompt == ‘Y’:

break

Теперь запустим программу. Весь код, кстати, находится в файле show_images.py. Видим образец сердитого лица, далее выражающего отвращение, испуганного, счастливого, далее – согласно метке, грустного, хотя, как мне кажется, лицо вовсе не грустное. Далее удивлённое лицо, лицо с нейтральным выражением, потом ещё одно сердитое лицо, выражающее отвращение, испуганное, счастливое, грустное и так далее. Просмотрим ещё несколько изображений и выйдем из цикла.

Поскольку у нас занятия по логистической регрессии, мы ещё не умеем пользоваться функцией мягкого максимума или брать её производную. Вместо этого преобразуем проблему в задачу двоичной классификации. Мы это сделаем путём изучения классов с метками только 0 и 1. Как вы сможете убедиться, это будет несколько сложно, поскольку у нас 4953 примера с меткой 0 и лишь 547 примеров с меткой 1. На следующей лекции я объясню, в чём сложность, но настоятельно рекомендую вам перед просмотром подумать об этом самостоятельно.

Ещё один вопрос, требующий внимания, – когда мы в концов концов используем софтмакс и будем классифицировать все семь классов – станет наша задача легче или труднее? Для ответа на этот вопрос задумайтесь, какой будет ожидаемый коэффициент классификации, если бы мы выбирали наугад? Предположим, данные на 50% состоят из класса 0, а 50% – из класса 1, и мы угадываем результат случайным образом. Каким будет ожидаемый коэффициент классификации? Очевидно, 50%, поскольку у нас есть 50%-й шанс угадать правильно.

Теперь предположим, что у нас есть K классов с равным количеством примеров в каждом классе. В этом случае наш ожидаемый коэффициент классификации будет составлять лишь 1/K, то есть правильно угадать теперь уже намного труднее. Поскольку в нашем случае K = 7, то попытка случайно угадать будет давать нам лишь 14% точности, или 86% ошибок. Лучший показатель точности на конкурсе Kaggle составил около 70%. Мы не станем стремиться к столь высокой точности, поскольку это требует очень много усилий, но увидим, что получится, когда используем подход «plug and play» с несколькими разными значениями гиперпараметров. Впрочем, если вы пожелаете самостоятельно настроить гиперпараметры для достижения ещё лучших результатов, то это только приветствуется.

Теперь обсудим структуру наших данных. Не забывайте, что это изображения, то есть по природе своей являются матрицами. Каждое из них является набором пикселей 48×48 и, таким образом, в общем у нас получается 2304 размерности для каждого примера. Важное замечание – они у нас не цветные. Если бы они были цветными, то количество размерностей возросло бы до 3*48*48 – то есть до 6912 размерностей для каждого примера, что потребует куда более интенсивных вычислений. И это при условии, что у нас всего три цвета – красный, зелёный и синий.

Ещё один нюанс состоит в том, что логистическая регрессия и обычные нейронные сети, в отличие от свёрточных нейронных сетей, работают с плоскими векторами. Это значит, что на самом деле мы работаем не с матрицей размерности 48×48, а сглаживаем изображение, так что на практике мы имеем дело с вектором размерности 2304. Это происходит потому, что первая строка изображения представлена первыми 48 элементами вектора с номерами от 1 до 48, вторая строка представлена очередными 48 элементами с номерами от 49 до 96 и так далее.

Что это значит? Это значит, что мы игнорируем пространственные соотношения для данных, и нам совершенно неважно, что пиксель 49 находится рядом с пикселем 1. Мы обрабатываем каждый пиксель одинаково и независимо относительно других. Как вы впоследствии убедитесь, при использовании свёрточных нейронных сетей это будет уже не так.

Следующим необходимым этапом предварительной обработки является нормализация данных. Значение пикселя, выражающее его яркость, варьируется в диапазоне от 0 до 255, но мы хотим преобразовать его так, чтобы он находится в диапазоне от 0 до 1. Вероятно, вы понимаете нормализацию как вычитание текущего значения величины от её среднего значения и последующее деление на стандартное отклонение. Но поскольку у нас в качестве данных значения пикселей, принимающие строго положительное значение, нам достаточно поделить их на максимальное значение. Причина, по которой мы хотим преобразовать данные в столь малый диапазон, заключается в том, что именно в нём функции, использующиеся в нейронных сетях, являются наиболее динамичными. Так, если вы взглянете на графики функций гиперболического тангенса или сигмоиды, то увидите, что самый крутой наклон находится в районе между -1 и +1. При этом за пределами этого диапазона наклон весьма пологий, поэтому работать вне его малоинтересно.

Теперь остановимся на некоторых трудностях при решении данной задачи. Не забывайте, что глубокое обучение – это на самом деле не чёрный ящик, работающий по принципу «подставь и решай», а потому для успешной работы необходимо правильно подобрать значения гиперпараметров. Итак, в чём же трудности?

Прежде всего, это выбор правильного коэффициента обучения. Если выбрать чересчур большое значение, функция затрат превысит все мыслимые значения и перестанет обрабатываться в качестве числа. Если выбрать чересчур малое значение, то получим очень медленную сходимость.

Следующим надо определиться с регуляризацией. Если установить слишком высокие штрафы, весовые коэффициенты будут стремиться к очень маленьким величинам и не будут влиять на функцию затрат. Если установить слишком низкие штрафы, может оказаться, что они ни на что не влияют, а функция затрат опять-таки превысит все значения и перестанет обрабатываться в качестве числа.

Далее – количество циклов. Если остановиться чересчур рано, склон градиентного спуска будет крутым. Это значит, что ошибка может быть меньшей, если продолжать цикл. Если же остановиться слишком поздно, то из-за этого вы потратите гораздо больше времени, чем нужно, учитывая необходимую точность конечного результата.

Не забывайте, что в центре внимания этих лекций находится умение работать с новыми наборами данных, основываясь на существующих моделях. Это поможет вам лучше понимать данные, но главное – это понимать, как обрабатываются данные для введения в нейронную сеть. Замечательно, что вас так много, ведь это значит, что существует такое множество конфигураций нейронных сетей. По одиночке никому из нас не под силу проверить каждую настройку гиперпараметра. Поэтому если у вас получился даже лучший результат, чем был достигнут на наших лекциях, пожалуйста, опубликуйте эти настройки на форуме.

Уверен, что все изучающие этот курс будут благодарны.

Проблема дисбаланса классов

Выше я отмечал, что у нас есть лишь 547 примеров из класса 1 и 4953 примера из класса 0; таким образом, класс 1 крайне недостаточно представлен в наших данных. На этой лекции мы разберём, почему это является проблемой для классификатора.

Лучше всего это проиллюстрировать на примере медицины. Предположим, к примеру, мы проверяем наличие у пациентов некоторой болезни. Большинство населения ею не болеют. Таким образом, по нашим наблюдениям 99% представляют отрицательный класс, и лишь 1% – положительный.

Что будет с нашим классификатором? Если каждый раз выдавать прогноз «болезни нет», то в 99% случаях это будет правильным ответом, и можно победно отчитаться, что наш фиктивный «тест» является правильным в 99% случаев. На самом же деле он не имеет смысла, поскольку мы ничего не узнали из имеющихся у нас данных.

Имейте, правда, ввиду, что такое описание несколько поверхностно, поскольку на самом деле мы обучаем наш классификатор вовсе не с использованием коэффициента классификации или коэффициента ошибок. На самом деле вместо этого мы используем кросс-энтропийную функцию ошибок. Впрочем, проблему это не снимает, поскольку кросс-энтропийная функция стремится уменьшить общее значение функции затрат, и если 99% наблюдений показывают отрицательный результат, то и разделяющая гиперлиния также будет стремиться в этом направлении.

Вот некоторые способы решения этой проблемы. Предположим, у нас есть 1000 примеров из класса 1 и всего 100 примеров из класса 2. Один из простейших способов – случайным образом взять лишь 100 примеров из класса 1, и тогда мы получим по 100 примеров в каждом классе. Второй способ – оставить 1000 примеров из 1-го класса и искусственно создать 1000 примеров 2-го класса, повторив набор 2-го класса 10 раз. Обратите внимание, что оба этих метода имеют одинаковый ожидаемый коэффициент ошибок. Впрочем, как вы знаете, важна также дисперсия, поэтому мы можем ожидать, что второй способ будет работать лучше, поскольку включает в себя больше данных.

Это не все способы искусственно создать больше примеров 2-го класса. Ещё один вариант – добавить гауссов шум. При этом дисперсия не может быть слишком большой, иначе изображение станет неузнаваемым. В этом случае метка станет, вероятнее всего, неправильной. Поэтому нам хоть и надо изменить изображение, но не слишком сильно его исказить. Ещё один вариант – добавление инвариантных преобразований, когда, к примеру, немного повёрнутое налево или направо лицо остаётся тем же лицом с той же меткой.

Имейте в виду, что существуют и другие способы измерения точности модели, учитывающие дисбаланс классов. Они используются в медицине и при поиске информации; при этом идёт попытка учесть оба класса. Это допускает использование двоичной классификации.

Суть в том, что мы хотим максимизировать и истинный положительный результат (TP), и истинный отрицательный результат (TN), в то же время стараясь минимизировать ложный положительный (FP) и ложный отрицательный (FN) результаты. Ложный положительный результат получается, когда прогнозируется положительный результат, хотя на самом деле он отрицательный, а ложный отрицательный результат получается, когда предсказывается отрицательный результат, хотя в действительности он положительный. Обратите внимание, что в таком случае точность является суммой истинных положительных и истинно отрицательных результатов, делённое на общее количество случаев:

Коэффициент классификации  = \frac {TP + TN} {TP + TN + FP + FN}

С точки зрения медицины, рассматриваются два показателя – чувствительность и специфичность. Чувствительность (TPR) – это коэффициент истинных положительных результатов, то есть количество истинных положительных результатов, делённое на сумму истинных положительных и ложных отрицательных:

TPR = \frac {TP} {TP + FN}.

Специфичность (TNR) – это коэффициент истинных отрицательных результатов, то есть количество истинно отрицательных результатов, делённое на сумму истинно отрицательных и ложно положительных результатов:

TNR = \frac {TN} {TN + FP}.

В информационных технологиях используются понятия точности и полноты. Точность определяется как количество истинных положительных результатов, делённое на сумму истинных и ложных положительных результатов:

Точность = \frac {TP} {TP + FP}.

Полнота же определяется как количество истинных положительных результатов, делённое на сумму истинно положительных и ложно отрицательных результатов:

Полнота = \frac {TP} {TP + FN}.

Обратите внимание, что полнота в точности равна чувствительности.

Мы можем получить конечный результат, учитывая и точность, и полноту – это называется F-мерой. Это более сбалансированный показатель, чем просто точность. Определяется она как гармоничное среднее точности и полноты:

Проект распознавания выражения лица. Обзор

Как вы помните, для логистической регрессии в качестве порогового значения мы обычно используем 0,5. Это значит, что если исходящая вероятность равна 0,5 или больше, то данные определяются как класс 1, если же вероятность меньше 0,5, то данные определяются в качестве класса 0.

На самом же деле нам не обязательно использовать 0,5 в качестве порогового значения. Мы можем использовать любую величину между нулём и единицей и, таким образом, рассматривать любую двоичную классификацию. Разумеется, разные пороговые значения означают, что мы получим разные значения чувствительности и специфичности. Это называется кривой рабочей характеристики приёмника, или, проще говоря, кривой ошибок или ROC-кривой. Площадь под этой кривой, сокращённо обозначаемая AUC, показывает, насколько в целом удачен классификатор. Идеальный классификатор даст нам значение этой величины, равной единице, тогда как случайное угадывание даст диагональную прямую с величиной, равной 0,5.

Обзор служебного кода

Теперь, мы рассмотрим код из файла util.py, который содержит множество полезных функций, используемых в разнообразных нейронных сетях.

Итак, сначала мы импортируем библиотеки Numpy и Pandas.

import numpy as np

import pandas as pd

Далее у нас идёт функция, инициализирующая весовые коэффициенты и свободный член. Её аргумент M1 означает размерность входных данных, а аргумент M2 – размерность исходящих. Таким образом, получаем матрицу размерности M1xM2. Свободный член устанавливаем равным нулю.

def init_weight_and_bias (M1, M2):

W = np.random.randn(M1, M2) / np.sqrt(M1 +M2)

b = np.zeros(M2)

return W.astype(np.float32), b.astype(np.float32)

Следующая функция используется в свёрточных нейронных сетях.

def init_filter(shape, poolsz):

w = np.random.randn(*shape) / np.sqrt(np.prod(shape[1:]) + shape[0]*np.prod(shape[2:] / np.prod(poolsz)))

return w.astype(np.float32)

Далее у нас идёт функция relu, использующаяся в качестве функции активации в нейронных сетях. Её можно использовать, если вы пользуетесь старой версией библиотеки Piano.

def relu(x):

return x * (x > 0)

Далее функция, вычисляющая сигмоиду, а также функция софтмакс, которая будет обсуждаться в курсе по глубокому обучению (часть 1).

def sigmoid(A):

return 1 / (1 + np.exp(-A))

def softmax(A):

expA = np.exp(A)

return expA / exp.sum(axis=1, keepdims=True)

Далее идут более общие функции, используемые при работе с софтмакс, и вычисляющие функции затрат. Функция cost вычисляет собственно функцию затрат, функция cost2 имеет другие возможности, но по сути они будут выдавать нам один и тот же результат.

def cost(T, Y):

return –(T*np.log(Y)).sum()

def cost2(T, Y):

N = len(T)

return –np.log(Y[np.arange(N), T]).sum()

Функция error_rate показывает разницу между средними значениями целевой и прогнозной переменными.

def error_rate(targets, predictions):

return np.mean(targets != predictions)

Следующая функция преобразует результат в виде матрицы размерности NxK.

def y2indicator(y):

N = len(y)

K = len(set(y))

ind = np.zeros((N, K))

for i in xrange(N):

ind[i, y[i]]  = 1

return ind

Очередная функция загружает данные. Сначала мы создаём пустые списки для данных, причём первая строка пропускается, поскольку содержит лишь заглавия. Как мы помним, первый столбец – это метка, а второй – значения пикселей, выражаемые целыми числами. Далее мы нормализируем данные, чтобы они имели значения от нуля до единицы вместо значений от 0 до 255, а также, поскольку наши классы несбалансированы, мы повторяем переменные класса 1 ещё 9 раз.

def getData(balance_ones=True):

Y = []

X = []

first = True

for line in open(‘fer2013.csv’):

if first:

first = False

else:

row = line.split(‘,’)

Y.append(int(row[0]))

X.append([int(p) for p in row[1].split()])

X, Y = np.array(X) / 255.0, np.array(Y)

if balance_ones:

X0, Y0 = X[Y!=1, :], Y[Y!=1]

X1 = X[Y==1, :]

X1 = np.repeat(X1, 9, axis=0)

X = np.vstack([X0, X1])

Y = np.concatenate((Y0, [1]*len(X1)))

return X, Y

Следующая функция используется в свёрточных нейронных сетях. Она выдаёт оригинальное изображение с размерностью N и одноцветовой гаммой.

def getImageData():

X, Y = getData()

N, D = X.shape

d = int(np.sqrt(D))

X = X.reshape(N, 1, d, d)

return X, Y

И последняя функция getBinaryData. Она делает то же самое, что и предыдущая одноименная функция, за исключением того, что во внимание берутся только те примеры, класс которых равен нулю или единице.

def getBinaryData():

Y = []

X = []

first = True

for line in open(‘fer2013.csv’):

if first:

first = False

 

else:

row = line.split(‘,’)

y = int(row[0])

if y == 0 or y == 1:

Y.append(y)

X.append([int(p) for p in row[1].split()])

return np.array(X) / 255.0, np.array(Y)

Мы не будем сейчас явно рассматривать дисбаланс классов, поскольку подробнее сделаем это в самом логистическом файле.

Понравилась статья? Поделить с друзьями:
Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: