«Что у нас есть?» — спросил горбоносый поворачиваясь.
«Алдан-3», — сказал бородатый.
«Богатая машина, — сказал я.”[1]
Недавно я решил заняться изучением глубокого обучения. На работе мне выдали новую карточку с поддержкой CUDA и шеф выразил пожелание что эта вершина инженерной мысли позволит нашей лаборатории сделать рывок вперёд, ну или по крайней мере, не отстать от массы конкурентов. У меня уже был некоторый опыт общения с Tensor Flow, но в этот раз я решил попробовать Torch. Привлекало что он написан на языке Lua и C, является достаточно легковесным и легко расширяемым через FFI. И ещё мне не нравится Python.
Недавно на Хабрахабр я наткнулся на статью, в процессе обсуждения которой я вспомнил что где-то в тумбочке у меня пилится Raspberry Pi, модель B+ и мне захотелось посмотреть — а смогу ли я поднять на ней torch и запустить что-нибудь несложное.
Естественно первым делом я захотел посмотреть как на моем десктопе с новой карточкой GPU будет тренироваться alexnet и другие известные сети. На гитхабе есть небольшой проект в котором несколько популярных сетей реализованы на Torch. Поигравшись с ними, я перешёл на решение своих задач, но про них я тут говорить не буду.
А теперь переходим к малинке (Raspberry PI модель B+).
Установка
Копируем инсталлятор torch на малинку:
apt-get install git-core
git clone https://github.com/torch/distro.git ~/torch --recursive
Первым делом, я решил что мне не хочется ждать пока стандартный установщик Torch скомпилирует OpenBLAS и установит QT со всеми зависимостями, поэтому я решил проделать это вручную:
apt-get install -y build-essential gcc g++ curl cmake libreadline-dev libjpeg-dev libpng-dev ncurses-dev imagemagick libzmq3-dev gfortran libopenblas-base libopenblas-dev
Запускаем компиляцию torch:
cd ~/torch;
./install.sh
У меня компиляция всего занимает примерно час.
— А что это там у вас за лампа? — подозрительно спросил Фарфуркис. [1]
И тут нас ждёт первый облом: создатели torch: не предполагали что его будут компилировать на архитектуре arm, но без поддержки NEON:
[ 6%] Building C object lib/TH/CMakeFiles/TH.dir/THVector.c.o
In file included from /home/pi/torch/pkg/torch/lib/TH/THVector.c:2:0:
/home/pi/torch/pkg/torch/lib/TH/generic/THVectorDispatch.c: In function ‘THByteVector_vectorDispatchInit’:
/home/pi/torch/pkg/torch/lib/TH/generic/simd/simd.h:64:3: error: impossible constraint in ‘asm’
asm volatile ( "cpuid\n\t"
Пришлось пофиксить это дело. И после этого, всё заработало! Если вам лень делать это всё самому и хочется быстро попробовать пример, я сделал архив с пред-компилированным torch для Raspberry PI -B (без поддержки NEON): https://github.com/vfonov/deep-pi/releases/download/v1/torch_intstall_raspbian_arm6l_20161218.tar.gz, распаковывается в /home/pi
Тестирование
Для проверки я решил посмотреть скорость тренировки распознавания рукописных цифр MNIST, соответствующий пример есть в наборе демок для Torch:
th train-on-mnist.lua
<torch> set nb of threads to 4
<mnist> using model:
nn.Sequential {
[input -> (1) -> (2) -> (3) -> (4) -> (5) -> (6) -> (7) -> (8) -> (9) -> (10) -> output]
(1): nn.SpatialConvolutionMM(1 -> 32, 5x5)
(2): nn.Tanh
(3): nn.SpatialMaxPooling(3x3, 3,3, 1,1)
(4): nn.SpatialConvolutionMM(32 -> 64, 5x5)
(5): nn.Tanh
(6): nn.SpatialMaxPooling(2x2, 2,2)
(7): nn.Reshape(576)
(8): nn.Linear(576 -> 200)
(9): nn.Tanh
(10): nn.Linear(200 -> 10)
}
<warning> only using 2000 samples to train quickly (use flag -full to use 60000 samples)
<mnist> loading only 2000 examples
<mnist> done
<mnist> loading only 1000 examples
<mnist> done
<trainer> on training set:
<trainer> online epoch # 1 [batchSize = 10]
[===================>.................... 471/2000 ....................................] ETA: 2m20s | Step: 92ms
В общем неплохо, для сравнения — на десктопе с i5-4590 CPU @ 3.30GHz, без использования GPU:
[=======================>................ 571/2000 ....................................] ETA: 27s613ms | Step: 19ms
Т.е в этом примере малинка работает примерно в 5 раз медленнее чем современный десктоп.
Распознавания изображений
одушевлённый теперь «Алдан» иногда печатал на выходе: «Думаю. Прошу не мешать» [1]
Теперь настал черёд заставить малинку распознавать изображения с помощью натренированной googlenet. Тут меня ждал второй подвох: в Alexnet такое огромное количество параметров, что памяти малинки уже не хватает. Но тут на выручку приходит squeezenet и Network-in-Network, автор последней даже сделал натренерованную модель в формате для torch.
Для начала надо преобразовать модель так чтобы её можно было использовать на архитектуре ARM (тренировать на Raspberry PI не стоит — результаты будут готовы лет через сто).
На десктопе надо загрузить модель в бинарном формате torch, и записать в формате ‘ascii’, потом на малинке — преобразовать обратно:
Desktop:
model=torch.load(‘blah.t7’)
torch.save(‘blah_ascii.t7’,model,’ascii’)
Raspberry PI:
model=torch.load(‘blah_ascii.t7’,’ascii’)
torch.save(‘blah_arm.t7’,model)
Версию для arm можно загрузить тут.
Я сделал маленький скриптик для проверки работоспособности на малинке:
Полный текст тут.
...
local m=torch.load(prefix..'nin_bn_final_arm.t7')
...
local input=image.load(prefix.."n07579787_ILSVRC2012_val_00049211.JPEG")
...
local output=model:forward(cropped)
...
И вуаля, запускаем с изображением из тестового набора ImageNET:
>th test_single.lua n07579787_ILSVRC2012_val_00049211.JPEG
loading model:0.57sec
Running neural net:13.46sec
25.3%: n07579787: plate
13.8%: n07873807: pizza, pizza pie
8.8%: n04263257: soup bowl
8.0%: n07590611: hot pot, hotpot
7.2%: n07831146: carbonara
T.e за 14 сек малинка удачно справилась с процедурой распознавания образов!
Настало время сделать пример поинтересней: приделываем интерфейс к камере из пакета camera и веб интерфейс из пакета display, и у нас получается интерактивная машина, раз в 14 секунд объявляющая миру что она видит. Надо только установить пакет для работы с камерой (luarocks install camera) и для визуализации через веб-интерфейс (luarocks install display).
Полный текст тут.
…
-- подцепляем камеру
local cam = image.Camera {idx=0,width=iW,height=iH}
...
local frame = cam:forward()
local cropped = image.crop(frame, w1, h1, w1+oW, h1+oH) -- center patch
…
-- отправляем кадр зрителю
display_sample_in.win=display.image(cropped,display_sample_in)
…
-- прогоняем его через нейросеть
local output=model:forward(cropped)
…
-- отправляем описание - чего увидели
display_output.win=display.text(out_text,display_output)
Перед запуском надо запустить демона из пакета display: nohup th -ldisplay.start 8000 0.0.0.0 &
Испытания
Тестовая установка:
Результат:
Заключение
Итак у нас получилась недорогая машинка для распознавания образов, которой вы можете порадовать ваших друзей во время новогодних праздников. Естественно, задача классификации изображения может быть заменена на что-то более продуктивное, например можно легко сделать систему для идентификации человека по физиономии, несколько примеров есть тут или можно идентифицировать фигуры людей на камере наблюдения за вашим огородом.
Для оптимизации производительности можно попробовать использовать nnpack, или даже сделать интерфейс к векторному ускорителю встроенному в процессор малинки как тут.
Примечания:
Цитаты из "Понедельник начинается в субботу" и "Сказка о тройке" А. и Б. Стругацких.
Описание процедуры на английском и репозиторий со всеми исходниками находится на гитхабе.