В этот раз я распишу еще один неудачный эксперимент и попутно постараюсь сделать небольшое введение в библиотеку vectorflow, которую разработали в Netflix. Дело в том, что сейчас активно развиваются технологии, связанные с машинным обучением и имитацией нейронных сетей человека, поэтому я решил немного вникнуть в эту тему, для чего в течении двух последних недель пытался разобраться, как порвать гегемонию Python с его библиотеками (написаны не на Python, а на C) и поработать с нейронными сетями.
Не советую радоваться тому, что я опишу, но может быть вам удастся то, что не удалось мне.
Во-первых, я абсолютно не разбираюсь в машинном обучении и нейронных сетях (это больше тема Bagomot’а, чем моя), а во-вторых – мое описание будет крайне поверхностным и возможно содержать ошибки. Однако, это не помешает нам провести эксперимент по распознаванию рукописных цифр с помощью своей собственной нейронной сети, которая будет написана целиком на D и будет запущена на основном процессоре компьютера (т.е без привлечения GPU и сторонних процессоров или специализированных ускорителей).
Для начала эксперимента создаем пустой dub-проект и указываем для него в качестве зависимости библиотеку от Netflix под названием vectorflow. Дальше, необходимо открыть в браузере репозиторий проекта vectorflow на GitHub и перейти в папку examples, открыв файл mnist.d, поскольку именно этот файл и является “сердцем” нашего проекта.
Помимо этого, не помешает открыть вики-страницу vectorflow на GitHub, т.к в ней содержиться описание принципа работы с библиотекой, который я сейчас постараюсь вкратце пересказать.
Работа с vectorflow на редкость проста: сначала мы определяем нашу модель, которая представляет собой прямой ацикличный граф (Direct Acyclic Graf – DAG). Этот граф определяет структуру нейронной сети и, по сути, это все что нам необходимо знать об этом: немного почитав исходный код библиотеки, мы находим все основные понятия, которые нам потребуются при определении структуры сети. В частности, нам нужно определить экземпляр типа NeuralNet, потом добавить в него с помощью метода stack входной слой данных (это либо SparseData с размерностью, либо SparseF с вектором признаков), скрытые слои (с помощью экземпляра Linear с указанием количества нейронов), функцию активации каждого слоя (ReLU, SeLU, TanH и другие нелинейные функции, которые определены в vectorflow) и определить выходные нейроны (опять же с помощью Linear указанием количества нейронов).
Но с этим, можно особо не торопиться, т.к такое определение NeuralNet обычно находиться в функции main() или сторонней функции инициализации, запускаемой из main.
Следующий шаг лучше осуществить перед описанным выше, поскольку именно он является наиболее критическим и это тот шаг, который подвел меня в моем эксперименте. В этом этапе использования библиотеки нам потребуется определить формат данных для датасетов (как обучающего, так и тестирующего), для чего требуется определить некую структуру, которая традиционно для vectorflow называется Obs и содержит в себе два важных поля: метка отдельного элемента данных и некий массив с его признаками.
В нашем примере, в структуре Obs есть два поля: label (тип float) и features (тип float[]), которые представляют собой метку компонента данных и некий набор характерных элементов данного (т.е по сути, это и есть вектор признаков). После этого, все что нужно – это написать процедуру загрузки датасетов, которая представляет собой некую функцию, которая принимает строку с путем до файла (как вариант, сам файл) и выдает массив структур Obs.
На этом шаге подразумевается, что у вас есть некий файл (или даже два файла – один с данными, а другой – с их метками или подписями), в котором содержится датасет для процедуры обучения или тренировки нейронной сети. В этом плане vectorflow дает полную свободу, поскольку получение данных из этих файлов, как и определение их формата ложится целиком на программиста, а библиотека обеспечивает лишь нужный минимум абстракций для легкого создания нейронных сетей и обертывания данных для них. Здесь все очень серьезно: нейронные сети очень чувствительны к входным данным, а за их качество ответственны вы и только вы !
В примере mnist.d используются специально подготовленные датасеты MNIST, которые содержат в себе изображения рукописных цифр в виде нормализованных и полутоновых изображений размеров 28 на 28 пикселей. Эти изображения хранятся в особом, плоском формате и представляют собой по сути дела набор яркостей пикселей изображения, которые представлены в вмде чисел от 0 до 255. Также, набор MNIST содержит в себе еще и отдельный файл для подписей под каждое изображение (т.е подпись и есть метка, которая однозначно идентифицирует представленную в наборе цифру, и нетрудно догадаться, что метка равна числовому представлению цифры).
Пример в vectorflow скачивает наборы датасетов (плоские файлы с картинками и подписями для обучающего и тестового набора, т.е получается всего 4 файла), а затем после процедуры разбора входных форматов (т.е генерации массивов структур Obs) запускается процедура обучения нейронной сети.
Обучение сети также выносится в процедуру main и следует сразу за описанием и инициализацией сети. Эта процедура представляет собой вызов метода learn у экзепляра NeuralNet с передачей в качестве параметров переменной с обучающим датасетом (тип Obs[]), типом оптимизации (задается обычной строкой, я видел два варианта: multinomial – полиномиальный и square – метод наименьших квадратов), оптимизатором (встроенный класс vectorflow с параметрами: количество проходов, коэффициент обучения и размер порции данных или же собственноручно написанная процедура оптимизации, структуру которой можно увидеть в вики проекта vectorflow), флагом подробного вывода (true/false) и количеством ядер процессора (число типа int).
Помимо этого, после обучения полученную нейронную сеть можно сериализовать в файл с помощью метода serialize и даже загрузить с помощью уже готовую сеть с помощью обратного метода – deserialize, что используется довольно часто, т.к как обучение сети на центральном процессоре довольно долгий процесс.
После того, как сеть описана и обучена, можно переходить к ее практическому применению, для чего проводиться тестирование того, насколько хорошо натренирована сеть. В этом случае нам поможет тестирующий датасет и метод predict экземпляра NeuralNet, который выдает массив посчитанных весов для каждого из возможных вариантов соответствия данных, и таким образом, чем больше подсчитаннаый вес, тем больше нейронная сеть склоняется к тому варианту, который соответствует этому весу. Все, что от нас труебуется это пройти по всему тестирующему датасету (мы знаем, какая метка из него соотвествует какому признаку) и сравнить вариант метода predict (т.е тот элемент массива, которому присущ наибольший вес) с меткой из реального набора, вычислить расхождение и посчитать суммарное отклонение. Это отклонение и будет ошибкой классификации для нашей неройнной сети.
Теперь код всего примера:
/+ dub.json:
{
"name": "mnist",
"dependencies": {"vectorflow": "*"}
}
+/
import std.stdio;
import std.algorithm;
import vectorflow;
import vectorflow.math : fabs, round;
static auto data_dir = "mnist_data/";
struct Obs {
float label;
float[] features;
}
auto load_data()
{
import std.file;
import std.typecons;
if(!exists(data_dir))
{
auto root_url = "http://yann.lecun.com/exdb/mnist/";
mkdir(data_dir);
import std.net.curl;
import std.process;
writeln("Downloading training set...");
download(
root_url ~ "train-images-idx3-ubyte.gz",
data_dir ~ "train.gz");
download(
root_url ~ "train-labels-idx1-ubyte.gz",
data_dir ~ "train_labels.gz");
writeln("Downloading test set...");
download(
root_url ~ "t10k-images-idx3-ubyte.gz",
data_dir ~ "test.gz");
download(
root_url ~ "t10k-labels-idx1-ubyte.gz",
data_dir ~ "test_labels.gz");
wait(spawnShell(`gunzip ` ~ data_dir ~ "train.gz"));
wait(spawnShell(`gunzip ` ~ data_dir ~ "train_labels.gz"));
wait(spawnShell(`gunzip ` ~ data_dir ~ "test.gz"));
wait(spawnShell(`gunzip ` ~ data_dir ~ "test_labels.gz"));
}
return tuple(load_data(data_dir ~ "train"), load_data(data_dir ~ "test"));
}
Obs[] load_data(string prefix)
{
import std.conv;
import std.bitmanip;
import std.exception;
import std.array;
auto fx = File(prefix, "rb");
auto fl = File(prefix ~ "_labels", "rb");
scope(exit)
{
fx.close();
fl.close();
}
T to_native(T)(T b)
{
return bigEndianToNative!T((cast(ubyte*)&b)[0..b.sizeof]);
}
Obs[] res;
int n;
fx.rawRead((&n)[0..1]);
enforce(to_native(n) == 2051, "Wrong MNIST magic number. Corrupted data");
foreach(_; 0..3)
fx.rawRead((&n)[0..1]);
foreach(_; 0..2)
fl.rawRead((&n)[0..1]);
if(prefix == data_dir ~ "train")
n = 60_000;
else
n = 10_000;
res.length = n;
ubyte[] pxls = new ubyte[28 * 28];
foreach(i; 0..n)
{
ubyte label;
fl.rawRead((&label)[0..1]);
fx.rawRead(pxls);
res[i] = Obs(label.to!float, pxls.to!(float[]));
}
return res;
}
void main(string[] args)
{
writeln("Hello world!");
auto nn = NeuralNet()
.stack(DenseData(28 * 28)) // MNIST is of dimension 28 * 28 = 784
.stack(Linear(200)) // one hidden layer
.stack(DropOut(0.3))
.stack(SeLU()) // non-linear activation
.stack(Linear(10)); // 10 classes for 10 digits
nn.initialize(0.0001);
auto data = load_data();
auto train = data[0];
auto test = data[1];
nn.learn(train, "multinomial",
new ADAM(
15, // number of passes
0.0001, // learning rate
200 // mini-batch-size
),
true, // verbose
4 // number of cores
);
// if you want to save the model locally, do this:
// nn.serialize("dump_model.vf");
// if you want to load a serialized from disk, do that:
// auto nn = NeuralNet.deserialize("mnist_model.vf");
double err = 0;
foreach(ref o; test)
{
auto pred = nn.predict(o);
float max_dp = -float.max;
ulong ind = 0;
foreach(i, f; pred)
if(f > max_dp)
{
ind = i;
max_dp = f;
}
if(fabs(o.label - ind) > 1e-3)
err++;
}
err /= test.length;
writeln("Classification error: ", err);
}
Испытание показывает, что ошибка классификации всего 2 %, вы спросите, а почему тогда эксперимент неудачный ?
А вот почему: используя код примера, решил попробовать сделать похожее, но вместо стандартного датасета из MNIST, я воспользовался его нормальным вариантом в форме обычных картинок. Для загрузки картинок я использовал dlib, а загрузчик формата я писал сам, что привело к странному результату – моя сеть дает ошибку почти в 41 % !!!
Вот код эксперимента с vectorflow и dlib:
import std.algorithm;
import std.conv;
import std.file;
import std.path;
import std.range;
import std.stdio;
import std.string;
import dlib.image;
import vectorflow;
import vectorflow.math : fabs, round;
static auto trainingDir = `mnist_png/training/`;
static auto testingDir = `mnist_png/testing/`;
struct Obs
{
float label;
float[] features;
}
auto loadImage(string filepath)
{
import std.math : round;
float[] data;
auto label = filepath
.split(`/`)[$-2]
.to!float;
auto img = load(filepath);
foreach (x; 0..img.width)
{
foreach (y; 0..img.height)
{
data ~= round(img[x,y].luminance * 255);
}
}
return Obs(label, data);
}
auto loadData(string filepath)
{
return dirEntries(filepath, SpanMode.depth)
.filter!(a => a.isFile)
.map!(a => loadImage(a))
.array;
}
void main()
{
if (!"mnist_model.vf".exists)
{
auto nn = NeuralNet()
.stack(DenseData(28 * 28)) // MNIST is of dimension 28 * 28 = 784
.stack(Linear(200)) // one hidden layer
.stack(DropOut(0.3))
.stack(SeLU()) // non-linear activation
.stack(Linear(10)); // 10 classes for 10 digits
nn.initialize(0.01);
auto trainingSet = loadData(trainingDir);
nn.learn(trainingSet, "multinomial",
new ADAM(
15, // number of passes
0.01, // learning rate
155 // mini-batch-size
),
true, // verbose
2 // number of cores
);
nn.serialize("mnist_model.vf");
}
else
{
auto testingSet = loadData(testingDir);
auto nn = NeuralNet.deserialize("mnist_model.vf");
double err = 0;
foreach(ref o; testingSet)
{
auto pred = nn.predict(o);
writeln(pred);
float max_dp = -float.max;
ulong ind = 0;
foreach(i, f; pred)
if(f > max_dp)
{
ind = i;
max_dp = f;
}
if(fabs(o.label - ind) > 1e-3)
err++;
}
err /= testingSet.length;
writeln("Classification error: ", err);
}
}
Выводы: это был очень интересный опыт, который научил меня тому, что надо аккуратней разбираться в форматах данных и больше напирать на изучение теории симулируемых процессов. Благодаря этому, я понял, насколько все-таки интересно самому разбираться в сторонних файловых форматах и как забавно пытаться их воспроизвести на “иной элементной базе”…
На этом все, и думаю, что в следующий раз я буду лучше подготовлен…
У меня на python такая же проблема была, пока я сеть нормально не настроил. Короче, мучился я долго.