Можно найти много информации о принципах работы сверточных нейронных сетей, о том как можно благодаря буквально нескольким строчкам кода и небольшому набору данных создать свою модель, которая будет отличать котиков от собачек и тд. Но когда дело доходит до реальной задачи, возникает масса вопросов на которые гугл не может дать четких ответов.
С одним из таких вопросов я столкнулся во время чего-то разработки своего приложения для распознавания видов растений. Проблема заключалась в следующем - как быстро и эффективно отличить распознаваемое изображение и его отношение к тому на чем обучалась модель. Например, если мы обучали на котиках и собачках, то как отличить вентилятор от этих животных? Мы бы могли добавить еще один класс для вентиляторов, переобучить модель и начать отличать их, но вод беда - объектов которые не относятся к котикам и собачкам великое множество и мы не можем каждый раз добавлять класс хотя бы по следующим причина причинам: бесконечное количество потенциальных классов; сбор данных для обучения нового класса достаточно трудоемкий процесс; переобучение модели занимает время и ресурсы, а при имении порядочного количества данных и классов это время на вес золота; с ростом классов точность модели падает.
После некоторых раздумий первое, что пришло на ум - попробовать посмотреть, что происходит с активациями нейронов на последних слоях сети. Берем последние потому, что начальные слои содержат достаточно мало абстрактной информации. Мое интуитивное понимание заключалось в том, что скорее всего на неизвестных объектах сеть должна возбуждаться меньше и соответственно это как-то можно замерять простыми способами.
Давайте поэтапно разберем задачу и проблему.
За основу мы возьмем предобученную модель Resnet50 и будем ее файн-тюнить на котиках и собачках взятых на каггле. В тренировочном датасете лежит по 12500 картинок каждого класса, но нам потребуется всего 1000 (этого достаточно чтобы получить хорошую точность). Подготовленные данные использованные в данном туториале проще скачать здесь.
Для реализации из основных библиотек нам потребуется:
import glob
import os
import numpy as np
import pandas as pd
from keras import backend as K
from keras.applications.imagenet_utils import preprocess_input
from keras.applications.resnet50 import ResNet50
from keras.layers import Dense, Dropout, Flatten, Input
from keras.models import Model, Sequential
from keras.optimizers import SGD
from keras.preprocessing import image
from keras.preprocessing.image import ImageDataGenerator
from sklearn.cross_validation import StratifiedShuffleSplit
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import GridSearchCV
from sklearn.preprocessing import LabelEncoder
В Keras уже есть готовый модуль который содержит известную ResNet50. Все что нам нужно - это воспользоваться ею. Параметр include_top=False отвечает за то, что нам вернеться архитектура модели, но без последних слоев. Из-за того, что мы здесь занимаемся трансфером знаний предобученой сети, нужно прикрутить самим последние слои (я не буду описывать как работает fine-tuning так как это не есть целью даного туториала).
Важным моментом в прикручивании своих слоев для нашей задачи являеться добавление дополнительного Dense(2048) слоя. Если бы мы просто файнтюнили, этот слой нам бы не помог в точности, а наоборот чуть ухудшил ее, но именно он является самым полезным в снятии активаций для дальнейшего анализа. Как раз он получает максимум абстрактной полезной информации.
NB_EPOCH = 20
RELEVANT_LAYER_NAME = 'relevant_layer'
IMG_SIZE = (224, 224)
NB_VAL_SAMPLES = 200
NB_TRAIN_SAMPLES = 800
TRAIN_DIR = 'data/train/'
VALID_DIR = 'data/valid/'
def create_model():
base_model = ResNet50(include_top=False, input_tensor=Input(shape=(3,) + IMG_SIZE))
# делаем так чтобы слои из основной модели не тренировались
for layer in base_model.layers:
layer.trainable = False
x = base_model.output
x = Flatten()(x)
x = Dropout(0.5)(x)
# слой с которого мы будем снимать значения активаций нейронов
x = Dense(2048, activation='elu', name=RELEVANT_LAYER_NAME)(x)
x = Dropout(0.5)(x)
predictions = Dense(1, activation='sigmoid')(x)
return Model(input=base_model.input, output=predictions)
print("Creating model..")
model = create_model()
print("Model created")
def apply_mean(image_data_generator):
"""Subtracts the dataset mean"""
image_data_generator.mean = np.array([103.939, 116.779, 123.68], dtype=np.float32).reshape((3, 1, 1))
def get_train_datagen(*args, **kwargs):
idg = ImageDataGenerator(*args, **kwargs)
apply_mean(idg)
return idg.flow_from_directory(TRAIN_DIR, target_size=IMG_SIZE, class_mode='binary')
def get_validation_datagen():
idg = ImageDataGenerator()
apply_mean(idg)
return idg.flow_from_directory(VALID_DIR, target_size=IMG_SIZE, class_mode='binary')
def fine_tuning(model):
# выбираем для дообучения 2 identity блока и 1 сверточный
# (можно эксперементировать изменяя значение 80 чтобы добиться лучших результатов)
# все слои выше - "замораживаем"
for layer in model.layers[:80]:
layer.trainable = False
for layer in model.layers[80:]:
layer.trainable = True
print("Compiling model..")
sgd = SGD(lr=1e-4, decay=1e-6, momentum=0.9, nesterov=True)
model.compile(optimizer=sgd, loss='binary_crossentropy', metrics=['accuracy'])
model.fit_generator(
get_train_datagen(rotation_range=30., shear_range=0.2, zoom_range=0.2, horizontal_flip=True),
samples_per_epoch=NB_TRAIN_SAMPLES,
nb_epoch=NB_EPOCH,
validation_data=get_validation_datagen(),
nb_val_samples=NB_VAL_SAMPLES)
fine_tuning(model)
В папке irrelevant я подготовил изображения, которые достаточно разные по содержимому и не относятся к нашим животным. Активации будем собирать используя валидационную выборку (так как модель не обучалась на ней) и выборку irrelevant.
def get_files(path):
files = []
if os.path.isdir(path):
files = glob.glob(path + '*.jpg')
elif path.find('*') > 0:
files = glob.glob(path)
else:
files = [path]
if not len(files):
print('No images found by the given path')
return files
def load_img(img_path):
img = image.load_img(img_path, target_size=IMG_SIZE)
x = image.img_to_array(img)
x = np.expand_dims(x, axis=0)
return preprocess_input(x)[0]
def get_inputs(files):
inputs = []
for i in files:
x = load_img(i)
inputs.append(x)
return inputs
relevant_files = get_files('data/valid/**/*.jpg')
print('Found {} relevant files'.format(len(relevant_files)))
irrelevant_files = get_files('data/irrelevant/*.jpg')
print('Found {} relevant files'.format(len(irrelevant_files)))
relevant_inputs = get_inputs(relevant_files)
irrelevant_inputs = get_inputs(irrelevant_files)
def get_activation_function(m, layer):
x = [m.layers[0].input, K.learning_phase()]
y = [m.get_layer(layer).output]
return K.function(x, y)
def get_activations(model, inputs, layer, class_name):
all_activations = []
activation_function = get_activation_function(model, layer)
for i in range(len(inputs)):
activations = activation_function([[inputs[i]], 0])
all_activations.append(activations[0][0])
df = pd.DataFrame(all_activations)
df.insert(0, 'class', class_name)
df.reset_index()
return df
irrelevant_activations = get_activations(model, irrelevant_inputs, RELEVANT_LAYER_NAME, 'irrelevant')
relevant_activations = get_activations(model, relevant_inputs, RELEVANT_LAYER_NAME ,'relevant')
В итоге, имеем для каждого изображения 2048 значений. Эти значения ни что иное как активации нейронов нашего дополнительного слоя добавленного в ResNet50. То есть мы обучили модель, а потом на ней прогнали новые изображения собирая попутно полезные данные.
irrelevant_activations.head()
relevant_activations.head()
Интересный факт - сеть реагировала на незнакомые объекты бОльшим количеством нейронов нежели на знакомых. Вот что происходило:
(Изображения взяты при исползовании модели VGG16 и слоя с 4096 нейронами)
Активации для изображения на котором обучалась сеть
Активации для изображения из валидации
Активации для неизвестного изображения
Даже визуально можно заметить как хаос увеличиваеться с ростом неуверенности. Для меня это сравнимо толпе людей которые пытаються ответить на один вопрос и чем меньше они уверены в ответе, тем больше от них шума.
Далее я подумал, а почему бы не попробовать эти данные прогнать через простенькую полносвязную сеть и решить проблему бинарной классификации:
def encode(df):
label_encoder = LabelEncoder().fit(df['class'])
labels = label_encoder.transform(df['class'])
df = df.drop(['class'], axis=1)
return df, labels
df = pd.concat([irrelevant_activations, relevant_activations])
X, y = encode(df)
sss = StratifiedShuffleSplit(np.zeros(y.shape[0]), test_size=0.3, random_state=23)
for train_index, test_index in sss:
X_train, X_test = X.values[train_index], X.values[test_index]
y_train, y_test = y[train_index], y[test_index]
model = Sequential()
model.add(Dense(256, input_dim=2048, activation='elu', init='uniform'))
model.add(Dropout(0.5))
model.add(Dense(128, activation='relu', init='uniform'))
model.add(Dropout(0.5))
model.add(Dense(1, activation='sigmoid', init='uniform'))
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
model.fit(
X_train,
y_train,
nb_epoch=4,
validation_data=(X_test, y_test),
batch_size=16)
Вуаля! На четвертой эпохе имеем почти 100% точность различаемости. А что если попробовать вместо нейронной сети самую обычную Logistic Regression?
from sklearn.metrics import accuracy_score
params = {'C': [10, 2, .9, .4, .1], 'tol': [0.0001, 0.001]}
log_reg = LogisticRegression(solver='lbfgs', multi_class='multinomial', class_weight='balanced')
clf = GridSearchCV(log_reg, params, scoring='neg_log_loss', refit=True, cv=3, n_jobs=-1)
clf.fit(X_train, y_train)
print("best params: " + str(clf.best_params_))
print('best score:'+ str(clf.best_score_))
predictions = clf.predict(X_test)
print("accuracy", accuracy_score(y_test, predictions))
Что ж выходит и простой алгоритм способен дать очень высокую точность. На практике я отдал предпочтение LogisticRegression так как потребление памяти и вычислительных мощностей намного меньше.
Стоит учесть, что обучать модель для релевантности вам придеться каждый раз после переобучения главной модели, так как каждый последующий раз нейроны будут вести себя иначе.
В будущем планирую расписать это все более детально и обоснованно. Надеюсь, что этот туториал будет понятен и пригодиться вам на практике. Данный подход сработал отлично также для VGG16, InceptionV3. Думаю, сработает и для других топологий.