Начинать разбираться со свёрточными сетями лучше всего с LeNet-5. Этой архитектуре уже много лет, она проста для понимания, на современном железе быстро обучается и показывает на датасете
MNIST неплохой результат.
По классической схеме на вход этой сети подаётся изображение размером 32⨉32⨉1 пикселя. Изображение чёрно-белое, поэтому слой всего один, однако, никто не мешает использовать цветное изображение с тремя слоями.
Стоит уточнить, что за прошедшие годы терминология более или менее устоялась, и то, что на схеме называется слоем Subsampling, в современной терминологии чаще всего зовётся Pooling.
Также стоит обратить внимание на то, что на схеме каждый convolutional и pooling слой считаются отдельными слоями, однако есть немало приверженцев иного подхода, при котором convolutional + pooling слои вместе считаются частью одного слоя CNN.
Датасет MNIST можно скачать при помощи tensorflow:
mnist = tf.contrib.learn.datasets.load_dataset("mnist")
train_data = mnist.train.images
train_labels = np.asarray(mnist.train.labels, dtype=np.int32)
eval_data = mnist.test.images
eval_labels = np.asarray(mnist.test.labels, dtype=np.int32)
Для описания модели сети будет создана функция, которая соответствует интерфейсу Estimator API. Эта функция принимает в качестве параметров features, labels и model mode (одно из трёх значений: TRAIN, EVAL или PREDICT).
def cnn_model_fn(features, labels, mode):
Каждый пример представляет собой вектор, с shape (784,), то есть это картинка с разрешением 28⨉28 пикселей. Вот так выглядит пример с индексом 100:
plt.imshow(train_data[100].reshape(28,28), cmap="gray")
Метод layers, который будет использован для построения convolutional и pooling слоёв, ожидает на входе тензор c измерениями [batch_size, image_width, image_height, channels]. У нас же матрица имеет размерность [batch_size, image_width * image_height].
Для того, чтобы переконвертировать размерность, можно воспользоваться методом reshape:
input_layer = tf.reshape(features["x"], [-1,28,28,1])
Значение -1 указывает на то, что это измерение будет динамически рассчитано в зависимости от количества примеров features[“x”].
Convolutional layer 1 (C1)
Для создания этого слоя будет использован метод conv2d модуля layers:
conv1 = tf.layers.conv2d(
inputs=input_layer,
filters=6,
kernel_size=[5,5],
padding='same',
activation=tf.nn.tanh)
Параметр inputs ожидает на вход тензор c размерностью [batch_size, image_width, image_height, channels], соответственно ему передается input_layers, подготовленный выше.
Параметр filters отвечает за количество фильтров, а kernel_size - за их размер, который в этом случае равен [5,5]. При этом, если фильтр квадратный, то в качестве значения параметра можно просто указать скаляр 5.
Параметр padding может быть same или valid. В данном случае используем same, потому как на исходной диаграмме показано, что на вход первого слоя подаётся картинка с разрешением 32⨉32⨉1, а на выходе получается тензор 28⨉28⨉6. Воспользовавшись формулой:
нетрудно посчитать, что padding должен быть равен нулю, то есть значение параметра должно быть valid, размер фильтра 5⨉5, а stride 1.
Однако у нас на входе картинка с разрешением 28⨉28⨉1. Для того, чтобы получить желаемый тензор 28⨉28⨉6, padding должен быть равен 2, то есть параметр должен быть same.
В оригинальной статье сказано, что в качестве активационной функции используется tanh, поэтому в качестве значения параметра activation здесь указан tf.nn.tanh.
Pooling layer 1 (S2)
Pooling layer можно создать при помощи метода max_pooling2d()
pool1 = tf.layers.max_pooling2d(
inputs=conv1,
pool_size=[2,2],
strides=2)
Также, как и у conv2d(), параметр inputs на вход ожидает получить тензор c shape [batch_size, image_width, image_height, channels].
Параметр pool_size - это размер фильтра, в этом случае [2,2].
Параметр stride здесь равен 2.
Сочетание pool_size=[2,2] и stride=2 уменьшают длину и ширину матрицы вдвое, то есть на выходе получается тензор 14⨉14⨉6.
Convolutional layer 2 (C3) и Pooling layer 2 (S4)
Для следующего свёрточного слоя используется 16 фильтров 5⨉5 и valid padding:
conv2 = tf.layers.conv2d(
inputs=pool1,
filters=16,
kernel_size=[5,5],
padding='valid',
activation=tf.nn.relu)
На выходе получается тензор 10⨉10⨉16.
Pooling слой с параметрами, аналогичными предыдущему pooling слою, уменьшает длину и ширину матрицы вдвое:
pool2 = tf.layers.max_pooling2d(
inputs=conv2,
pool_size=[2,2],
strides=2)
На выходе получается тензор 5⨉5⨉16.
Fully connected layers
Следующие два слоя - это по сути обычные слои. Для того, чтобы их создать, нужно изменить размерность при помощи операции reshape:
pool2_flat = tf.reshape(pool2, [-1, 16 * 5 * 5])
Получившийся двухмерный тензор с shape равным [batch_size, 400], передаётся на вход слою dense со 120 нейронами и активационной функцией tanh:
dense1 = tf.layers.dense(
inputs=pool2_flat,
units=120,
activation=tf.nn.tanh)
и, затем, следующему слою, с 84 нейронами и той же активационной функцией tanh:
dense2 = tf.layers.dense(
inputs=dense1,
units=84,
activation=tf.nn.tanh)
Logits layer
Так как у датасета 10 классов, соответственно, последний слой сети будет иметь именно столько нейронов:
logits = tf.layers.dense(inputs=dense2, units=10)
Предсказание
Последний слой возвращает данные о вероятности принадлежности класса в виде тензора, имеющего размер [batch_size, 10].
Из этого тензора для каждого примера можно получить два значения: предсказанный класс и вероятность принадлежности к классу.
Для того, чтобы получить предсказанный класс, можно воспользоваться функцией tf.argmax:
tf.argmax(input=logits, axis=1)
Её параметр inputs принимает на вход тензор, а параметр axis отвечает за то, в отношении какого измерения будет производиться вычисление.
Вероятность можно получить при помощи функции tf.nn.softmax:
tf.nn.softmax(logits, name="softmax_tensor")
Оба предсказания мы обернём в dictionary и, если функция была вызвана с параметром PREDICT, вернём объект EstimatorSpec:
predictions = {
"classes": tf.argmax(input=logits, axis=1),
"probabilities": tf.nn.softmax(logits, name="softmax_tensor")
}
if mode == tf.estimator.ModeKeys.PREDICT:
return tf.estimator.EstimatorSpec(
mode=mode,
predictions=predictions)
Вычисление потерь
Для многоклассовой классификации обычно используется кросс энтропия, которая реализована в функции tf.losses.sparce_softmax_crossentropy:
loss = tf.losses.sparse_softmax_cross_entropy(
labels=labels,
logits=logits)
Настройка обучения
В качестве оптимизационного алгоритма будет использован СГД с параметром α=0.001:
if mode == tf.estimator.ModeKeys.TRAIN:
optimizer = tf.train.GradientDescentOptimizer(
learning_rate=0.001)
train_op = optimizer.minimize(
loss=loss,
global_step=tf.train.get_global_step())
return tf.estimator.EstimatorSpec(
mode=mode, loss=loss, train_op=train_op)
Метрики
Для добавления accuracy метрики к модели необходимо создать вот такой dictionary:
eval_metric_ops = {
"accuracy": tf.metrics.accuracy(
labels=labels,
predictions=predictions["classes"])
}
return tf.estimator.EstimatorSpec(
mode=mode, loss=loss,eval_metric_ops=eval_metric_ops)
Создание объекта Estimator
Теперь, когда модель готова, можно приступить к созданию объекта класса Estimator:
mnist_classifier = tf.estimator.Estimator(
model_fn=cnn_model_fn,
model_dir='/tmp/lenet5')
Параметр model_fn принимает на вход функцию, которая описывает модель и используется для обучения, тестирования и предсказания.
Параметр model_dir задаёт папку, в которой будут храниться промежуточные данные модели.
Logging hook
Сеть может обучаться довольно долго и было бы неплохо иметь возможность видеть, что происходит на текущий момент.
Мы будем использовать LoggingTensorHook для логирования значений softmax слоя через каждые 50 итераций:
tensor_to_log = {"probabilities": "softmax_tensor"}
logging_hook = tf.train.LoggingTensorHook(
tensors=tensor_to_log,
every_n_iter=50)
Обучение модели
Для того, чтобы обучить модель, нужно создать функцию tf.estimator.inputs.numpy_input_fn:
train_input_fn = tf.estimator.inputs.numpy_input_fn(
x = {"x": train_data},
y = train_labels,
batch_size = 100,
num_epochs = None,
shuffle = True)
Параметры x и y - это обучающие данные и их классы.
Параметр batch_size отвечает за размер пакета данных для СГД.
Параметр num_epochs=None означает, что модель будет обучаться до тех пор, пока не будет достигнуто заданное количество шагов.
Параметр shuffle=True указывает на то, что данные будут перемешаны.
Затем нужно запустить процесс обучения, вызвав метод train у созданного ранее объекта Estimator, передав созданную функцию в качестве параметра input_fn:
mnist_classifier.train(
input_fn = train_input_fn,
steps = 50000,
hooks = [logging_hook])
Параметр steps отвечает, за количество шагов обучения.
Созданный ранее logging_hook передаём в качестве параметра hooks.
Оценка модели
После того, как обучение завершилось, следует протестировать модель. Здесь, как и на этапе обучения, нужно создать тестировочную функцию и передать её в качестве параметра input_fn, при вызове метода evaluate объекта Estimator:
eval_input_fn = tf.estimator.inputs.numpy_input_fn(
x = {"x": eval_data},
y = eval_labels,
num_epochs = 1,
shuffle = False)
eval_results = mnist_classifier.evaluate(input_fn=eval_input_fn)
print(eval_results)
Параметр num_epochs = 1, указывает на то, что данные будут обработаны однократно.
Параметр shuffle = False запрещает перемешивание данных.
Модель, показывает 97 % accuracy на тестовых данных после 50000 шагов обучения:
{'accuracy': 0.9742, 'loss': 0.09026045, 'global_step': 50000}
Полный листинг кода можно поглядеть на github:
https://github.com/kukumber/lenet5 Ссылки
LeCun et al., 1998: Gradient-Based Learning Applied to Document Recognition
A Guide to TF Layers: Building a Convolutional Neural Network