Проект распознавания выражения лица в Theano и TensorFlow

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

Здравствуйте и вновь добро пожаловать на наши занятия – «Обработка данных: практическое глубокое обучение в Theano и TensorFlow»

Сегодня мы продолжим рассматривать пример распознавания выражения лица.

В предыдущей лекции я отмечал, что у нас есть лишь 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 – исходящих данных. В итоге мы имеем случайным образом инициированную матрицу W размерностью M1xM2. Свободные члены, имеющие размерность M2, у нас инициированы в качестве нулей. Все данные преобразованы в float32, чтобы без проблем пользоваться библиотеками Theano и TensorFlow.

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)

Функция init_filter используется в свёрточных нейронных сетях. Вновь-таки, возвращает значение в формате 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, используемая в нейронных сетях в качестве функции активации. Её можно использовать, если вы пользуетесь старой версией библиотеки Theano, в которой ещё нет встроенной relu.

def relu(x):

  return x * (x > 0)

Следующая функция вычисляет сигмоиду.

def sigmoid(A):

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

Далее идёт функция, которая вычисляет функцию софтмакс, обсуждавшуюся в курсе «Глубокое обучение на языке Python, часть 1».

def softmax(A):

  expA = np.exp(A)

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

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

def sigmoid_cost(T, Y):

   return -(T*np.log(Y) + (1-T)*np.log(1-Y)).sum()

Следующая функция 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]).mean()

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

def error_rate(targets, predictions):

  return np.mean(targets != predictions)

Далее идёт функция y2indicator, которая преобразует вектор целевых переменных размерностью Nx1 с метками классов от нуля до K-1 в матрицу показателей из нулей и единиц с размерностью 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

Следующая функция getData написана для получения данных со всех классов. В первой части мы инициируем пустой список из X и Y, пропуская первую строку, поскольку она содержит лишь заголовки. Как мы знаем, первый столбец является метками, а второй – пиксели, разделённые пробелами. Мы преобразуем их в целые числа.

def getData(balance_ones=True):

# images are 48×48 = 2304 size vectors

# N = 35887

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()])

Далее – мы преобразуем их в массивы Numpy, а также нормализуем данные, так что X находится в диапазоне от нуля до единицы вместо диапазона от 0 до 255.

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

Затем, поскольку у нас несбалансированное количество классов, мы увеличиваем количество примеров с классом 1, повторяя его 9 раз. Все данные, не принадлежащие классу 1, мы обозначаем через X0 и Y0, а данные класса 1 повторяем 9 раз.

if balance_ones:

# balance the 1 class

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

Очередная фукция getImageData используется в свёрточных нейронных сетях и сохраняет первоначальную форму изображения. В данном случае у нас N примеров, 1 цветовой канал, ширина изображения d и высота – также d. Как мы знаем, в данном случае это изображения 48×48.

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. Она делает почти то же, что и функция получения обычных данных, за тем исключением, что она отбирает примеры только с классами 0 или 1. Здесь мы не будем явно иметь дело с проблемой дисбаланса классов, поскольку она будет решаться в собственно логистическом файле.

def getBnaryData():

    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: :???: :?: :!:

Share via
Copy link