Мотивация
В данной статье вы познакомитесь c применением deep learning на практике. Будет использован фреймворк Caffe на датасете SVHN.
Deep Learning. Этот buzz word уже давно звенит в ушах, но попробовать его на практике никак не удавалось. Подвернулся удобный случай это исправить! На новогодние праздники был назначен контест на kaggle по распознаванию номеров домов в рамках курса по анализу изображений.
Была дана часть известной выборки SVHN, состоящей из 73257 изображений в обучающей и 26032 в тестовой (неразмеченной) выборках. Всего 10 классов для каждой цифры. Изображение имеет размер 32x32 в цветовом пространстве RGB. Как показывает бенчмарк, методы на основе deep learning показывают точность выше чем у человека — 1.92% против 2% ошибки!
У меня был опыт работы с алгоритмами машинного обучения на основе SVM и Naive Bayes. Применять уже известные методы скучно, поэтому решил использовать что-нибудь из deep learning, а именно сверточную нейронную сеть.
Выбор Caffe
Для работы с глубокими нейросетями существует много разных библиотек и фреймворков. Мои критерии были такими:
- туториалы,
- легкость освоения,
- легкость разворачивания,
- активное сообщество.
По ним отлично подошел Caffe:
- Хорошие туториалы есть на их сайте. Отдельно рекомендую лекции из Caffe Summer Bootcamp. Для быстрого старта можно почитать про основания нейронных сетей и потом про Caffe.
- Для начала работы с Caffe даже не требуется язык программирования. Конфигурируется Caffe с помощью конфигурационных файлов, а запускается из командной строки.
- Для разворачивания есть chef-кукбук и docker-образы.
- На гитхабе ведется активная разработка, а в гугл-группе можно задать вопрос по использованию фреймворка.
К тому же Caffe очень быстрый, т.к. использует GPU (хотя можно обойтись и CPU).
Установка
Изначально я поставил Caffe на свой ноутбук с помощью docker и запускал его в режиме CPU. Обучение нейросети проходило очень медленно, но сравнивать было не с чем и казалось, что это нормально.
Затем наткнулся на 25$ купон амазона и решил попробовать на AWS g2.2xlarge с NVIDIA GPU и поддержкой CUDA. Там развернул Caffe с помощью Chef. В итоге получилось в 41 раз быстрее — на CPU 100 итераций проходило за 290 сек, на GPU c CUDA за 7 cек!
Архитектура нейронной сети
Если в алгоритмах машинного обучения необходимо было формировать хороший вектор признаков, чтобы получить приемлемое качество, то в сверточных нейросетях этого делать не нужно. Главное — придумать хорошую архитектуру сети.
Введем следующие обозначения:
- input — входной слой, обычно это пиксели изображения,
- conv — слой свертки [1],
- pool — слой подвыборки [2],
- fully-conn — полносвязный слой [3],
- output — выходной слой, выдает предполагаемый класс изображения.
Для задачи классификации изображений основной является следующая архитектура НС:
input -> conv -> pool -> conv -> pool -> fully-conn -> fully-conn -> output
Количество (conv -> pool) слоев может быть разным, но обычно не меньше 2х. Количество fully-conn не меньше 1го.
В рамках данного контеста было перепробовано несколько архитектур. Наибольшую точность я получил со следующей:
input -> conv -> pool -> conv -> pool -> conv -> pool -> fully-conn -> fully-conn -> output
Имплементация архитектуры на Caffe
Caffe конфигурируется с помощью Protobuf файлов. Имплементация архитектуры для контеста находится здесь. Рассмотрим ключевые моменты конфигурации каждого слоя.
Входной слой (input)
name: "WinnyNet-F"
layers {
name: "svhn-rgb"
type: IMAGE_DATA
top: "data"
top: "label"
image_data_param {
source: "/home/deploy/opt/SVHN/train-rgb-b.txt"
batch_size: 128
shuffle: true
}
transform_param {
mean_file: "/home/deploy/opt/SVHN/svhn/winny_net5/mean.binaryproto"
}
include: { phase: TRAIN }
}
layers {
name: "svhn-rgb"
type: IMAGE_DATA
top: "data"
top: "label"
image_data_param {
source: "/home/deploy/opt/SVHN/test-rgb-b.txt"
batch_size: 120
}
transform_param {
mean_file: "/home/deploy/opt/SVHN/svhn/winny_net5/mean.binaryproto"
}
include: { phase: TEST }
}
...
Первые 2 слоя (для обучающей и тестовой фазы) имеют type: IMAGE_DATA, т.е. сеть на вход принимает изображения. Изображения перечисляются в текстовом файле, где 1 колонка — путь к изображению, 2 колонка — класс. Путь к текстовому файлу указывается в атрибуте image_data_param.
Кроме изображений, можно подавать на вход данные из HDF5, LevelDB и lmbd. Последние 2 варианта особенно актуальны, если критична скорость работы. Таким образом Caffe может работать с любыми данными, а не только изображениями. Проще всего работать с IMAGE_DATA, поэтому он и был выбран для контеста.
Также входные слои могут включать атрибут transform_param. В нем указываются трансформации, которым надо подвергнуть входные данные. Обычно, перед подачей изображений на нейросеть, их нормализуют или проводят более хитрые операции, например Local Contrast Normalization. В данном случае был указан mean_file — вычитание «среднего» изображения из входного.
В Caffe используется batch gradient descent. Входной слой содержит параметр batch_size. За одну итерацию на вход нейросети поступает batch_size элементов выборки.
Слои свертки и подвыборки (conv, pool)
...
layers {
bottom: "data"
top: "conv1/5x5_s1"
name: "conv1/5x5_s1"
type: CONVOLUTION
blobs_lr: 1
blobs_lr: 2
convolution_param {
num_output: 64
kernel_size: 5
stride: 1
pad: 2
weight_filler {
type: "xavier"
std: 0.0001
}
}
}
layers {
bottom: "conv1/5x5_s1"
top: "conv1/5x5_s1"
name: "conv1/relu_5x5"
type: RELU
}
layers {
bottom: "conv1/5x5_s1"
top: "pool1/3x3_s2"
name: "pool1/3x3_s2"
type: POOLING
pooling_param {
pool: MAX
kernel_size: 3
stride: 2
}
}
layers {
bottom: "pool1/3x3_s2"
top: "conv2/5x5_s1"
name: "conv2/5x5_s1"
type: CONVOLUTION
blobs_lr: 1
blobs_lr: 2
convolution_param {
num_output: 64
kernel_size: 5
stride: 1
pad: 2
weight_filler {
type: "xavier"
std: 0.01
}
}
}
layers {
bottom: "conv2/5x5_s1"
top: "conv2/5x5_s1"
name: "conv2/relu_5x5"
type: RELU
}
layers {
bottom: "conv2/5x5_s1"
top: "pool2/3x3_s2"
name: "pool2/3x3_s2"
type: POOLING
pooling_param {
pool: MAX
kernel_size: 3
stride: 2
}
}
layers {
bottom: "pool2/3x3_s2"
top: "conv3/5x5_s1"
name: "conv3/5x5_s1"
type: CONVOLUTION
blobs_lr: 1
blobs_lr: 2
convolution_param {
num_output: 128
kernel_size: 5
stride: 1
pad: 2
weight_filler {
type: "xavier"
std: 0.01
}
}
}
layers {
bottom: "conv3/5x5_s1"
top: "conv3/5x5_s1"
name: "conv3/relu_5x5"
type: RELU
}
layers {
bottom: "conv3/5x5_s1"
top: "pool3/3x3_s2"
name: "pool3/3x3_s2"
type: POOLING
pooling_param {
pool: MAX
kernel_size: 3
stride: 2
}
}
...
3м является слой свертки с type: CONVOLUTION. Далее идет указание функции активации c type: RELU. 4м слоем является слой подвыборки с type: POOL. Далее 2 раза идет повторение conv, pool слоев, но с другими параметрами.
Подбор параметров для этих слоев является эмпирическим.
Полносвязные и выходные слои (fully-conn, output)
...
layers {
bottom: "pool3/3x3_s2"
top: "ip1/3072"
name: "ip1/3072"
type: INNER_PRODUCT
blobs_lr: 1
blobs_lr: 2
inner_product_param {
num_output: 3072
weight_filler {
type: "gaussian"
std: 0.001
}
bias_filler {
type: "constant"
}
}
}
layers {
bottom: "ip1/3072"
top: "ip1/3072"
name: "ip1/relu_5x5"
type: RELU
}
layers {
bottom: "ip1/3072"
top: "ip2/2048"
name: "ip2/2048"
type: INNER_PRODUCT
blobs_lr: 1
blobs_lr: 2
inner_product_param {
num_output: 2048
weight_filler {
type: "xavier"
std: 0.001
}
bias_filler {
type: "constant"
}
}
}
layers {
bottom: "ip2/2048"
top: "ip2/2048"
name: "ip2/relu_5x5"
type: RELU
}
layers {
bottom: "ip2/2048"
top: "ip3/10"
name: "ip3/10"
type: INNER_PRODUCT
blobs_lr: 1
blobs_lr: 2
inner_product_param {
num_output: 10
weight_filler {
type: "xavier"
std: 0.1
}
}
}
layers {
name: "accuracy"
type: ACCURACY
bottom: "ip3/10"
bottom: "label"
top: "accuracy"
include: { phase: TEST }
}
layers {
name: "loss"
type: SOFTMAX_LOSS
bottom: "ip3/10"
bottom: "label"
top: "loss"
}
Полносвязный слой имеет type: INNER_PRODUCT. Выходной слой соединяется со слоем функцией потерь (type: SOFTMAX_LOSS) и слоем точности (type: ACCURACY). Слой точности срабатывает только в тестовой фазе и показывает процент верно классифицированных изображений в валидационной выборке.
Важным является указание атрибута weight_filler. Если он будет большим, то функция потерь (loss) может на начальных итерациях возвращать NaN. В таком случае надо уменьшить параметр std у атрибута weight_filler.
Параметры обучения
net: "/home/deploy/opt/SVHN/svhn/winny-f/winny_f_svhn.prototxt"
test_iter: 1
test_interval: 700
base_lr: 0.01
momentum: 0.9
weight_decay: 0.004
lr_policy: "inv"
gamma: 0.0001
power: 0.75
solver_type: NESTEROV
display: 100
max_iter: 77000
snapshot: 700
snapshot_prefix: "/mnt/home/deploy/opt/SVHN/svhn/snapshots/winny_net/winny-F"
solver_mode: GPU
Для получения хорошо обученной нейронной сети нужно задать параметры обучения. В Caffe параметры обучения устанавливаются через конфигурационный protobuf файл. Конфигурационный файл для данного контеста находится здесь. Параметров много, рассмотрим некоторые из них подробнее:
- net — путь к конфигурации архитектуры НС,
- test_interval — количество итераций между которыми проводится тестирование НС (phase: test),
- snapshot — количество итераций между которыми сохраняется состояние обучения НС.
В Caffe можно приостанавливать и возобновлять обучение.
Обучение и тестирование
Чтобы запустить обучение НС, нужно выполнить команду caffe train с указанием конфигурационного файла, где заданы параметры обучения:
> caffe train --solver=/home/deploy/winny-f/winny_f_svhn_solver.prototxt
.......................
I0109 18:12:17.035543 12864 solver.cpp:160] Solving WinnyNet-F
I0109 18:12:17.035578 12864 solver.cpp:247] Iteration 0, Testing net (#0)
I0109 18:12:17.077910 12864 solver.cpp:298] Test net output #0: accuracy = 0.0666667
I0109 18:12:17.077997 12864 solver.cpp:298] Test net output #1: loss = 2.3027 (* 1 = 2.3027 loss)
I0109 18:12:17.107712 12864 solver.cpp:191] Iteration 0, loss = 2.30359
I0109 18:12:17.107795 12864 solver.cpp:206] Train net output #0: loss = 2.30359 (* 1 = 2.30359 loss)
I0109 18:12:17.107817 12864 solver.cpp:516] Iteration 0, lr = 0.01
.......................
I0109 18:13:17.960325 12864 solver.cpp:247] Iteration 700, Testing net (#0)
I0109 18:13:18.045385 12864 solver.cpp:298] Test net output #0: accuracy = 0.841667
I0109 18:13:18.045462 12864 solver.cpp:298] Test net output #1: loss = 0.675567 (* 1 = 0.675567 loss)
I0109 18:13:18.072872 12864 solver.cpp:191] Iteration 700, loss = 0.383181
I0109 18:13:18.072949 12864 solver.cpp:206] Train net output #0: loss = 0.383181 (* 1 = 0.383181 loss)
.......................
I0109 20:08:50.567730 26450 solver.cpp:247] Iteration 77000, Testing net (#0)
I0109 20:08:50.610496 26450 solver.cpp:298] Test net output #0: accuracy = 0.916667
I0109 20:08:50.610571 26450 solver.cpp:298] Test net output #1: loss = 0.734139 (* 1 = 0.734139 loss)
I0109 20:08:50.640389 26450 solver.cpp:191] Iteration 77000, loss = 0.0050708
I0109 20:08:50.640470 26450 solver.cpp:206] Train net output #0: loss = 0.0050708 (* 1 = 0.0050708 loss)
I0109 20:08:50.640494 26450 solver.cpp:516] Iteration 77000, lr = 0.00197406
.......................
I0109 20:52:32.236827 30453 solver.cpp:247] Iteration 103600, Testing net (#0)
I0109 20:52:32.263108 30453 solver.cpp:298] Test net output #0: accuracy = 0.883333
I0109 20:52:32.263183 30453 solver.cpp:298] Test net output #1: loss = 0.901031 (* 1 = 0.901031 loss)
I0109 20:52:32.290550 30453 solver.cpp:191] Iteration 103600, loss = 0.00463345
I0109 20:52:32.290627 30453 solver.cpp:206] Train net output #0: loss = 0.00463345 (* 1 = 0.00463345 loss)
I0109 20:52:32.290644 30453 solver.cpp:516] Iteration 103600, lr = 0.00161609
Одна эпоха — это (73257-120)/128 ~= 571 итерация. Чуть больше чем за 1 эпоху, на 700 итерации, точность сети на валидационной выборке 84%. На 134 эпохе точность уже 91%. На 181 эпохе — 88%. Возможно, если обучать сеть больше эпох, например 1000, точность стабилизируется и будет выше. В данном контесте обучение было остановлено на 181 эпохе.
В Caffe можно возобновлять обучение сети из snapshot добавляя параметр --snapshot:
> caffe train --solver=/home/deploy/winny-f/winny_f_svhn_solver.prototxt
--snapshot=winny_net/winny-F_snapshot_77000.solverstate
Тестирование на неразмеченных изображениях
Для тестирования НС, необходимо создать deploy конфигурацию архитектуры сети. В ней, в отличие от предыдущей конфигурации, отсутствует слой точности и упрощен входной слой.
Тестовая выборка, состоящая из 26032 изображений, идет без разметки. Поэтому, чтобы оценить точность на тестовой выборке контеста, нужно написать немного кода. Caffe имеет интерфейсы для Питона и Матлаба.
Для тестирования сетей из разных эпох в Caffe есть снапшоты. Сеть 134 эпохи показала точность (Private Score в kaggle) 88.7%, а сеть 181 эпохи — 87.6%.
Идеи по повышению точности
Судя по магистерской диссертации, точность реализованной архитектуры может достигать 96%.
Как можно попробовать повысить полученную точность 88.7%?
- Обучать сеть больше эпох. Например, в туториале по deep learning в facial keypoints detection сеть обучали 1000 эпох.
- Стандартизовать данные, чтобы математическое ожидание было равно 0 и дисперсия 1. Для этого потребуется использовать HDF5 или LevelDb/lmdb для хранения данных.
- Поработать с параметрами обучения. Например, уменьшать learning_rate каждые 100 эпох.
- Также можно попробовать использовать dropout слои, но для этого потребуется обучать сеть еще больше эпох, чем 1000.
- Датасет SVHN содержит дополнительные 600 000 размеченных изображений. В исследованиях их используют, но в рамках контеста их использование было бы нечестным. В этом случае можно сгенерировать новые данные на основе имеющихся.
Заключение
Реализованная сверточная нейронная сеть показала точность 88.9%. Это не лучший результат, но для первого блина неплохо. Есть потенциал для увеличения точности до 96%.
Благодаря фреймворку Caffe погружение в deep learning не вызывает больших трудностей. Достаточно создать пару конфигурационных файлов и одной командой запустить процесс обучения. Конечно, также нужны базовые познания в теории искусственных нейронных сетей. Эту и другую информацию для быстрого старта я постарался дать в этой статье.
Автор: lytr