RGB против оттенков серого

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

На протяжении всей этой серии мы будем работать с набором данных CIFAR-10. CIFAR — это аббревиатура, обозначающая Канадский институт перспективных исследований. CIFAR-10 — это набор данных, разработанный исследователями Института CIFAR. Нажмите здесь, чтобы загрузить набор данных.

CIFAR-10 состоит из 60 000 цветных фотографий 32×32 пикселей объектов из 10 взаимоисключающих классов. 10 различных классов представляют самолеты, автомобили, птицы, кошки, олени, собаки, лягушки, лошади, корабли и грузовики с 6000 изображений в каждом классе (5000 обучающих и 1000 тестовых изображений на класс). Обратите внимание, что это идеально сбалансированный набор данных, а это означает, что в каждом классе одинаковое количество примеров. Это позволит нам создать модель с более высокой точностью, чем если бы мы работали с несбалансированным набором данных. Это связано с тем, что часто при работе с сильно несбалансированным набором систематическая ошибка заставляет модель больше полагаться на статистический прогноз, чем на классификацию.

Давайте сделаем нашу CNN с исходным набором данных CIFAR-10.

import numpy as np
import cv2
import matplotlib.pyplot as plt
import seaborn as sns; sns.set()
from keras.datasets import cifar10
from sklearn.preprocessing import OneHotEncoder
from sklearn.metrics import confusion_matrix
from keras.layers import Conv2D, MaxPool2D, Flatten, Dense, Dropout
from keras.models import Sequential, load_model
from keras.callbacks import EarlyStopping

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

tf.keras.datasets.cifar10.load_data()
labels = [‘airplane’, ‘automobile’, ‘bird’, ‘cat’, ‘deer’, ‘dog’, ‘frog’, ‘horse’, ‘ship’, ‘truck’]

Никогда не забывайте нормализовать свои данные! Узнайте, почему нормализация важна здесь. Вы можете добиться этого, сначала преобразовав наши данные в значения с плавающей запятой и сопоставив их с [0,1]. Затем нам нужно вычесть среднее значение из данных обучения и тестирования и (необязательно) разделить их на стандартное отклонение.

X_train = X_train.astype('float32')/255.0
X_test = X_test.astype('float32')/255.0
mean = np.mean(X_train, axis = (0,1,2))
std = np.std(X_train, axis = (0,1,2))
X_train = (X_train-mean)/(std+1e-7)
X_test = (X_test-mean)/(std+1e-7)

Давайте заглянем внутрь нашего набора данных. Вот как мы можем это сделать:

fig, axes = plt.subplots(ncols=3, nrows=3, figsize=(8, 8))
index = 0
for i in range(3):
    for j in range(3):
        axes[i,j].set_title(labels[y_train[index][0]])
        axes[i,j].imshow(X_train[index])
        axes[i,j].get_xaxis().set_visible(False)
        axes[i,j].get_yaxis().set_visible(False)
        index += 1
plt.show()

Приведенный выше код отобразит первые 9 изображений нашего тренировочного набора вместе с созданными нами метками. Низкое разрешение изображений объясняется их малым размером (32x32 пикселя), что может быть как плохо, так и хорошо. Низкое разрешение означает, что обнаруженные объекты будут гораздо менее подробными, что увеличивает вероятность неправильной классификации. С другой стороны, с меньшим размером изображения мы можем выполнять свертки для каждого изображения намного быстрее, что делает всю сеть более эффективной.

Теперь давайте построим нашу сеть!

Полный код вы можете найти здесь (обновляется).

model = Sequential()
model.add(Conv2D(32, (3, 3), activation='relu', kernel_initializer='he_uniform', padding='same', input_shape=input_shape))
...
model.add(Dense(10, activation='softmax'))

Далее нам нужно скомпилировать нашу модель:

model.compile(loss='categorical_crossentropy', 
     optimizer='adam',
     metrics=['acc'])

Перед обучением модели я рекомендую объявить объект ранней остановки, который поможет предотвратить переобучение нашей модели. Проще говоря, приведенный ниже код остановит обучение, как только значение потерь приблизительно достигнет своей минимальнойточки.

es = EarlyStopping(monitor='val_loss', mode='min', verbose=1, patience=3)
history = model.fit(X_train, y_train, epochs=20, batch_size=64, validation_data=(X_test, y_test), callbacks=[es])

После проверки нашей модели ранняя точка остановки была достигнута в эпоху 13, что привело к конечной точности чуть более 80%. Теперь мы можем визуализировать процесс обучения нашей модели в отношении как потерь, так и точности.

plt.plot(history.history["acc"], label='train')
plt.plot(history.history["val_acc"], label='test')
plt.title('Classification Accuracy')
plt.legend()
plt.show()
plt.plot(history.history["loss"], label='train')
plt.plot(history.history["val_loss"], label='test')
plt.title('Cross Entropy Loss')
plt.legend()
plt.show()

Это создаст два графика: один для представления точности классификации (справа), а другой для перекрестной потери энтропии (слева).

В обоих случаях синие линии указывают на производительность модели в наборе обучающих данных, а оранжевые линии указывают на производительность в тестовом наборе данных.

Из сгенерированных графиков видно, что модель хорошо сходится в течение примерно 10 эпох, после чего дальнейшее улучшение набора тестовых данных не происходит (поскольку кривая обучения выравнивается).

Что, если бы наши изображения были полностью в оттенках серого?

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

X_train = np.array([cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) for image in X_train])
X_test = np.array([cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) for image in X_test])

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

Теперь давайте обучим и протестируем нашу сеть с измененными данными. Мы будем использовать в основном тот же код, что и раньше, за исключением входной формы, которая теперь будет (32, 32, 1), а не (32, 32, 3) из-за уменьшенной размерности, поскольку теперь мы используем только одну цветовой канал (серый).

Достигнув точки ранней остановки на 14 эпохах, мы смогли достичь точности 79%. Процесс обучения и тестирования был немного быстрее для модели в оттенках серого, что имеет смысл, учитывая меньший размер изображения.

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

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

predictions = model.predict(X_test)
predictions = one_hot_encoder.inverse_transform(predictions)
cm = confusion_matrix(y_test, predictions)
plt.figure(figsize=(9,9))
sns.heatmap(cm, cbar=False, xticklabels=labels, yticklabels=labels, fmt='d', annot=True, cmap=plt.cm.Blues)
plt.title('RGB Network Confusion Matrix')
plt.xlabel('Predicted')
plt.ylabel('Actual')
plt.show()

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

Как можно объяснить эти различия?

Ответ прост — контекст. Вспомните примеры самолетов и кораблей. В большинстве случаев вы можете ожидать, что фон изображения самолета будет небом, а для корабля — океаном, оба из которых синие, и поэтому будут давать одинаковые значения RGB. Это может привести к смещению на основе цвета в сети RGB, потенциально заставляя ее использовать цвет в качестве основного классификатора, а не изученные функции. Давайте взглянем на некоторые тренировочные изображения для отдельных занятий.

Сразу же можно сказать разницу в цвете между каждым из классов и согласованность каждого класса внутри себя. А теперь представьте, если бы наш набор данных состоял только из фотографий кораблей и оленей. Что, скорее всего, произойдет, так это то, что вместо изучения конкретных признаков, таких как четыре ноги или рога оленя и кривизна корабля, сеть будет использовать значения RGB как простой способ классификации изображений: зеленый для оленя и синий для корабля.

Итак, какова наилучшая практика?

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

Так что же случилось с CIFAR-10? В нашем случае разница между точностью классификации цветных данных и данных в оттенках серого настолько мала (1%), что это вполне может быть одна и та же модель. Конечно, матрицы путаницы дали нам немного больше информации о том, как работает сеть, но мы до сих пор не знаем, что на самом деле изучает наша сеть — функции, цвета, и то, и другое, НИ ??

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

Благодарности

Спасибо моим наставникам Лэнсу Галлетти и Джеймсу Канстлу за их руководство, а также Кэмерон Гаррисон, Анки Лин, Кристине Сюй и Лейту Амра за их вклад.