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

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

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

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

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

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

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

Данные отображены в формате 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’)

Теперь перейдём к основной функции. Первая строка основной функции – это просто получение данных. Следующим идёт бесконечный цикл прохода. Для каждого из семи чувств мы выбираем все входные данные, соответствующие этому чувству, и находим номер соответствующих входных данных. Затем случайным образом выбираем данные и выводим на экран. Поскольку наши данные заданы плоским вектором, изображение необходимо перерисовать в размере 48x48; цвет указан как серый, поскольку наши фотографии являются чёрно-белыми. Далее мы выводим на экран изображение с соответствующим чувством. Далее, поскольку цикл бесконечный, я написал код с запросом, выйти из цикла или нет. Если вы желаете выйти, надо нажать заглавное 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» с несколькими разными значениями гиперпараметров. Впрочем, если вы пожелаете самостоятельно настроить гиперпараметры для достижения ещё лучших результатов, то это только приветствуется.

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

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

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

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

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

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

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

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

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

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

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

Итак, начало очень похоже на предыдущий файл. Если вы не хотите писать код самостоятельно, зайдите на репозитарий github и найдите файл ann_sigmoid.py.

Из файла util.py импортируем функции getBinaryData, sigmoid, sigmoid_cost, error_rate, relu – новой здесь является только функция relu.

import numpy as np

import matplotlib.pyplot as plt

from sklearn.utils import shuffle

from util import getBinaryData, sigmoid, sigmoid_cost, error_rate, relu

Вновь определяем класс ANN. Количество скрытых единиц сохраним как свойство класса.

class ANN(object):

def __init__(self, M):

self.M = M

В классе ANN определим функцию fit, установив коэффициент обучения равным 5*10-7, регуляризацию равной единице, а количество циклов равным 10 000 – намного меньше, чем мы устанавливали ранее. Начало также похоже на то, что мы делали раньше – перемешиваем наши X и Y и делим их на учебный и проверочный наборы. Последние 1 000 примеров будут проверочными.

def fit(self, X, Y, learning_rate=5*10e-7, reg=1.0, epochs=10000, show_fig=False):

X, Y = shuffle(X, Y)

Xvalid, Yvalid = X[-1000:], Y[-1000:]

X, Y = X[:-1000], Y[:-1000]

После этого инициируем весовые коэффициенты, начав с прямого распространения.

N, D = X.shape

self.W1 = np.random.randn(D, self.M) / np.sqrt(D + self.M)

self.b1 = np.zeros(self.M)

self.W2 = np.random.randn(self.M) / np.sqrt(self.M)

self.b2 = 0

И пишем цикл.

costs = []

best_validation_error = 1

for i in xrange(epochs):

pY, Z = self.forward(X)

pY_Y = pY – Y

self.W2 -= learning_rate*(Z.T.dot(pY_Y) + reg*self.W2)

self.b2 -= learning_rate*((pY_Y).sum() + reg*self.b2)

Следующее – алгоритм обратного распространения ошибок на входе скрытых весовых коэффициентов.

dZ = np.outer(pY_Y, self.W2) * (Z > 0)

self.W1 -= learning_rate*(X.T.dot(dZ) + reg*self.W1)

self.b1 -= learning_rate*(np.sum(dZ, axis=0) + reg*self.b1)

Через каждые 20 прогонов будем выводить ошибку проверки.

if i % 20 == 0:

pYvalid, _ = self.forward(Xvalid)

c = sigmoid_cost(Yvalid, pYvalid)

costs.append(c)

e = error_rate(Yvalid, np.round(pYvalid))

print(“i:”, i, “cost:”, c, “error:”, e)

if e < best_validation_error:

best_validation_error = e

print(“best_validation_error:”, best_validation_error)

if show_fig:

plt.plot(costs)

plt.show()

Теперь осталось определить функции forward, predict и score – всё как в логистической регрессии.

def forward(self, X):

Z = relu(X.dot(self.W1) + self.b1)

return sigmoid(Z.dot(self.W2) + self.b2), Z

def predict(self, X):

pY, _ = self.forward(X)

return np.round(pY)

def score(self, X, Y):

prediction = self.predict(X)

return 1 – error_rate(Y, prediction)

Функция main также идентична функции из логистической регрессии. Поскольку наши классы несбалансированны, мы повторяем данные класса 1 ещё 9 раз, что сделает количество примеров из обоих классов примерно равным.

def main():

X, Y = getBinaryData()

X0 = X[Y==0, :]

X1 = X[Y==1, :]

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

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

Y = np.array([0]*len(X0) + [1]*len(X1))

model = ANN(100)

model.fit(X, Y, show_fig=True)

if __name__ == ‘__main__’:

main()

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

dZ = np.outer(pY_Y, self.W2) * (Z > 0)

надо заменить на строку

dZ = np.outer(pY_Y, self.W2) * (1 – Z*Z)

А строку

Z = relu(X.dot(self.W1) + self.b1)

заменить на строку

Z = np.tanh(X.dot(self.W1) + self.b1)

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

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

Итак, как обычно импортируем библиотеки Numpy и Matplotlib. Из файле util импортируем функции getData (причём не забывайте, что мы теперь рассматриваем не сугубо двоичные данные, а все), softmax, cost, y2indicator (данная функция преобразует список меток от 0 до 6 в матрицу размерности Nx7, состоящую из нулей и единиц), а также error_rate. Из sklearn.utils импортируем функцию shuffle, который перемешает наши данные.

import numpy as np

import matplotlib.pyplot as plt

from util import getData, softmax, cost, y2indicator, error_rate

from sklearn.utils import shuffle

И вновь создаём новый класс, который назовём LogisticModel.

class LogisticModel(object):

def __init__(self):

pass

Теперь определим нашу функцию fit. Установим значение коэффициента обучения равным 10-8, регуляризацию равной 10-12, и количество циклов, равным 10 000. Первая часть опять очень похожа на то, что мы делали ранее, – перемешиваем наши данные, установив последние 1 000 примеров в качестве проверочного набора. Поскольку у нас логистическая регрессия, то будет лишь один весовой коэффициент и один свободный член.

def fit(self, X, Y, learning_rate=10e-8, reg=10e-12, epochs=10000, show_fig=False):

X, Y = shuffle(X, Y)

Xvalid, Yvalid = X[-1000:], Y[-1000:]

X, Y = X[:-1000], Y[:-1000]

N, D = X.shape

K = len(set(Y))

T = y2indicator(Y)

self.W = np.random.randn(D, K) / np.sqrt(D + K)

self.b = np.zeros(K)

Далее тоже как обычно. Сначала запускаем прямое распространение, а затем – градиентный спуск.

costs = []

best_validation_error = 1

for i in xrange(epochs):

pY = self.forward(X)

self.W -= learning_rate*(X.T.dot(pY – T) + reg*self.W)

self.b -= learning_rate*((pY – T).sum(axis=0) + reg*self.b)

Через каждые 10 раз будем вычислять значение функции затрат.

if i % 10 == 0:

pYvalid = self.forward(Xvalid)

c = cost(Tvalid, pYvalid)

costs.append(c)

e = error_rate(Yvalid, np.argmax(pYvalid, axis=1))

print(“i:”, i, “cost:”, c, “error:”, e)

if e < best_validation_error:

best_validation_error = e

print(“best_validation_error:”, best_validation_error)

if show_fig:

plt.plot(costs)

plt.show()

Теперь определим остальные необходимые нам функции forward, predict и score.

def forward(self, X):

return softmax(X.dot(self.W) + self.b)

def predict(self, X):

pY = self.forward(X)

return np.argmax(pY, axis=1)

def score(self, X, Y):

prediction = self.predict(X)

return 1 – error_rate(Y, prediction)

Теперь мы готовы определить функцию main.

def main():

X, Y = getData()

model = LogisticModel()

model.fit(X, Y, show_fig=True)

print(model.score(X, Y))

if __name__ == ‘__main__’:

main()

Запустим программу и посмотрим, что получится.

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

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

Поскольку код похож, скопируем часть его из предыдущего файла, с тем лишь отличием, что теперь импортируем вместо функции cost функцию cost2. Как вы помните, обе они делают то же самое, но cost2 несколько эффективнее. Мы также импортируем функцию relu.

import numpy as np

import matplotlib.pyplot as plt

from util import getData, softmax, cost2, y2indicator, error_rate, relu

from sklearn.utils import shuffle

Функция main также очень похожа, разве что класс у нас теперь будет называться ANN.

def main():

X, Y = getData()

model = ANN()

model.fit(X, Y, reg=0, show_fig=True)

print(model.score(X, Y))

if __name__ == ‘__main__’:

main()

Итак, давайте определим наш класс ANN.

class ANN(object):

def __init__(self, M):

self.M = M

Далее определим функцию fit. Коэффициент обучения равен 10-6, регуляризация равна 10-1, количество циклов равно 10 000. Как и прежде, перемешиваем наши данные, оставляя последние 1 000 примеров в качестве проверочного набора.

def fit(self, X, Y, learning_rate=10e-7, reg=10e-7, epochs=10000, show_fig=False):

X, Y = shuffle(X, Y)

Xvalid, Yvalid = X[-1000:], Y[-1000:]

# Tvalid = y2indicator(Yvalid)

X, Y = X[:-1000], Y[:-1000]

N, D = X.shape

K = len(set(Y))

T = y2indicator(Y)

Инициируем наши весовые коэффициенты.

self.W1 = np.random.randn(D, self.M) / np.sqrt(D + self.M)

self.b1 = np.zeros(self.M)

self.W2 = np.random.randn(self.M, K) / np.sqrt(self.M + K)

self.b2 = np.zeros(K)

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

costs = []

best_validation_error = 1

for i in xrange(epochs):

pY, Z = self.forward(X)

pY_T = pY – T

self.W2 -= learning_rate*(Z.T.dot(pY_T) + reg*self.W2)

self.b2 -= learning_rate*(pY_T.sum(axis=0) + reg*self.b2)

dZ = pY_T.dot(self.W2.T) * (1 – Z*Z)

self.W1 -= learning_rate*(X.T.dot(dZ) + reg*self.W1)

self.b1 -= learning_rate*(dZ.sum(axis=0) + reg*self.b1)

И выводим текущий результат через каждые 10 циклов.

if i % 10 == 0:

pYvalid, _ = self.forward(Xvalid)

c = cost2(Yvalid, pYvalid)

costs.append(c)

e = error_rate(Yvalid, np.argmax(pYvalid, axis=1))

print(“i:”, i, “cost:”, c, “error:”, e)

if e < best_validation_error:

best_validation_error = e

print(“best_validation_error:”, best_validation_error)

if show_fig:

plt.plot(costs)

plt.show()

В случае, если в качестве функции активации вы захотите использовать функцию relu, строку

dZ = pY_T.dot(self.W2.T) * (1 – Z*Z)

необходимо заменить на

dZ = pY_T.dot(self.W2.T) * (Z > 0)

Теперь определим остальные функции.

def forward(self, X):

Z = np.tanh(X.dot(self.W1) + self.b1)

return softmax(Z.dot(self.W2) + self.b2), Z

В случае использования функции relu вторую строку надо заменить на

Z = relu(X.dot(self.W1) + self.b1)

Итак, далее.

def predict(self, X):

pY, _ = self.forward(X)

return np.argmax(pY, axis=1)

def score(self, X, Y):

prediction = self.predict(X)

return 1 – error_rate(Y, prediction)

Теперь можно запускать программу.

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

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