LeNet-5 своими руками

Apr 18, 2018 11:00

Начинать разбираться со свёрточными сетями лучше всего с 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

cnn

Previous post Next post
Up