Всем привет. Библиотека нейросети описана в моей прошлой статье. Здесь решил показать как можно использовать обученную сеть из TF (Tensorflow) в своем решении, и стоит ли.
Под катом сравнение с оригинальной реализацией TF, демо приложение для распознавания картинок, ну и… выводы. Кому интересно, прошу.
Как устроена ResNet можете узнать, например, здесь.
Вот так выглядит структура сети в цифрах:
По коду получилось не проще и не сложнее чем на питоне.
→ Полный код доступен здесь
Можно поступить проще, загрузить архитектуру сети и веса из файлов,
Сделал приложение для интереса. Скачать можете отсюда. Объем большой из-за весов сети. Исходники там есть, можете использовать для примера.
Приложение создано только для статьи, поддерживаться не будет, поэтому не включал в репозиторий проекта.
Теперь, что получилось по сравнению с TF.
Показания после прогона 100 изображений, в среднем. Машина: i5-2400, GF1050, Win7, MSVC12.
Значения результатов распознавания совпадают до 3-го знака.
→ Код теста
На самом деле плачевно все конечно.
Для CPU решил не использовать MKL-DNN, сам думал довести: перераспределил память для последовательного чтения, по максимуму загрузил векторные регистры. Возможно надо было приводить к матричному умножению, и/или еще какие хаки. Упирался здесь, по началу было хуже, правильней было бы использовать MKL все таки.
На GPU время тратится на копирование памяти из/в память видеокарты, и не все операции выполняются на GPU.
Выводы какие можно сделать из всей этой суеты:
— не выпендриваться, а использовать известные проверенные решения, дошли до ума уже более-менее вроде. Сидел сам на mxnet когда то, да маялся с нативным использованием, об этом ниже;
— не пытаться использовать нативный С интерфейс ML фреймворков. А юзать их на языке, на который ориентировались разработчики, то есть python.
Легкий путь использования функционала ML из своего языка, — сделать сервис-процесс на питоне, и по сокету слать ему картинки, получится разделение ответственности и отсутствие тяжелого кода.
Все пожалуй. Статья коротенькая получилась, но выводы, думаю, ценны, и относятся не только к ML.
Спасибо.
PS:
если у кого есть желание и силы попытаться все таки догнать до TF, welcome!)
PS2:
рано руки опустил. Взял перекур, снова взялся и все получилось.
Для CPU помогло приведение к матричному умножению, как и думал.
Для GPU выделил все операции в отдельную либу, так чтобы без копирования на CPU и обратно, единственный минус такого подхода — пришлось все операторы переписать (продублировать), некоторые вещи хоть и совпадают, но связывать не стал.
В общем, вот как теперь:
То есть, по крайне мере, инференс даже быстрее получился, чем на TF.
Код теста не изменился.
Под катом сравнение с оригинальной реализацией TF, демо приложение для распознавания картинок, ну и… выводы. Кому интересно, прошу.
Как устроена ResNet можете узнать, например, здесь.
Вот так выглядит структура сети в цифрах:
По коду получилось не проще и не сложнее чем на питоне.
Код C++ для создания сети:
auto net = sn::Net();
net.addNode("In", sn::Input(), "conv1")
.addNode("conv1", sn::Convolution(64, 7, 3, 2, sn::batchNormType::beforeActive, sn::active::none, mode), "pool1_pad")
.addNode("pool1_pad", sn::Pooling(3, 2, sn::poolType::max, mode), "res2a_branch1 res2a_branch2a");
convBlock(net, vector<uint32_t>{ 64, 64, 256 }, 3, 1, "res2a_branch", "res2b_branch2a res2b_branchSum", mode);
idntBlock(net, vector<uint32_t>{ 64, 64, 256 }, 3, "res2b_branch", "res2c_branch2a res2c_branchSum", mode);
idntBlock(net, vector<uint32_t>{ 64, 64, 256}, 3, "res2c_branch", "res3a_branch1 res3a_branch2a", mode);
convBlock(net, vector<uint32_t>{ 128, 128, 512 }, 3, 2, "res3a_branch", "res3b_branch2a res3b_branchSum", mode);
idntBlock(net, vector<uint32_t>{ 128, 128, 512 }, 3, "res3b_branch", "res3c_branch2a res3c_branchSum", mode);
idntBlock(net, vector<uint32_t>{ 128, 128, 512 }, 3, "res3c_branch", "res3d_branch2a res3d_branchSum", mode);
idntBlock(net, vector<uint32_t>{ 128, 128, 512 }, 3, "res3d_branch", "res4a_branch1 res4a_branch2a", mode);
convBlock(net, vector<uint32_t>{ 256, 256, 1024 }, 3, 2, "res4a_branch", "res4b_branch2a res4b_branchSum", mode);
idntBlock(net, vector<uint32_t>{ 256, 256, 1024 }, 3, "res4b_branch", "res4c_branch2a res4c_branchSum", mode);
idntBlock(net, vector<uint32_t>{ 256, 256, 1024 }, 3, "res4c_branch", "res4d_branch2a res4d_branchSum", mode);
idntBlock(net, vector<uint32_t>{ 256, 256, 1024 }, 3, "res4d_branch", "res4e_branch2a res4e_branchSum", mode);
idntBlock(net, vector<uint32_t>{ 256, 256, 1024 }, 3, "res4e_branch", "res4f_branch2a res4f_branchSum", mode);
idntBlock(net, vector<uint32_t>{ 256, 256, 1024 }, 3, "res4f_branch", "res5a_branch1 res5a_branch2a", mode);
convBlock(net, vector<uint32_t>{ 512, 512, 2048 }, 3, 2, "res5a_branch", "res5b_branch2a res5b_branchSum", mode);
idntBlock(net, vector<uint32_t>{ 512, 512, 2048 }, 3, "res5b_branch", "res5c_branch2a res5c_branchSum", mode);
idntBlock(net, vector<uint32_t>{ 512, 512, 2048 }, 3, "res5c_branch", "avg_pool", mode);
net.addNode("avg_pool", sn::Pooling(7, 7, sn::poolType::avg, mode), "fc1000")
.addNode("fc1000", sn::FullyConnected(1000, sn::active::none, mode), "LS")
.addNode("LS", sn::LossFunction(sn::lossType::softMaxToCrossEntropy), "Output");
→ Полный код доступен здесь
Можно поступить проще, загрузить архитектуру сети и веса из файлов,
вот так:
string archPath = "c:/cpp/other/sunnet/example/resnet50/resNet50Struct.json",
weightPath = "c:/cpp/other/sunnet/example/resnet50/resNet50Weights.dat";
std::ifstream ifs;
ifs.open(archPath, std::ifstream::in);
if (!ifs.good()){
cout << "error open file : " + archPath << endl;
system("pause");
return false;
}
ifs.seekg(0, ifs.end);
size_t length = ifs.tellg();
ifs.seekg(0, ifs.beg);
string jnArch; jnArch.resize(length);
ifs.read((char*)jnArch.data(), length);
// Create net
sn::Net snet(jnArch, weightPath);
Сделал приложение для интереса. Скачать можете отсюда. Объем большой из-за весов сети. Исходники там есть, можете использовать для примера.
Приложение создано только для статьи, поддерживаться не будет, поэтому не включал в репозиторий проекта.
Теперь, что получилось по сравнению с TF.
Показания после прогона 100 изображений, в среднем. Машина: i5-2400, GF1050, Win7, MSVC12.
Значения результатов распознавания совпадают до 3-го знака.
→ Код теста
CPU: time/img, ms | GPU: time/img, ms | CPU: RAM, Mb | GPU: RAM, Mb | |
---|---|---|---|---|
Sunnet | 410 | 120 | 600 | 1200 |
Tensorflow | 250 | 25 | 400 | 1400 |
На самом деле плачевно все конечно.
Для CPU решил не использовать MKL-DNN, сам думал довести: перераспределил память для последовательного чтения, по максимуму загрузил векторные регистры. Возможно надо было приводить к матричному умножению, и/или еще какие хаки. Упирался здесь, по началу было хуже, правильней было бы использовать MKL все таки.
На GPU время тратится на копирование памяти из/в память видеокарты, и не все операции выполняются на GPU.
Выводы какие можно сделать из всей этой суеты:
— не выпендриваться, а использовать известные проверенные решения, дошли до ума уже более-менее вроде. Сидел сам на mxnet когда то, да маялся с нативным использованием, об этом ниже;
— не пытаться использовать нативный С интерфейс ML фреймворков. А юзать их на языке, на который ориентировались разработчики, то есть python.
Легкий путь использования функционала ML из своего языка, — сделать сервис-процесс на питоне, и по сокету слать ему картинки, получится разделение ответственности и отсутствие тяжелого кода.
Все пожалуй. Статья коротенькая получилась, но выводы, думаю, ценны, и относятся не только к ML.
Спасибо.
PS:
если у кого есть желание и силы попытаться все таки догнать до TF, welcome!)
PS2:
рано руки опустил. Взял перекур, снова взялся и все получилось.
Для CPU помогло приведение к матричному умножению, как и думал.
Для GPU выделил все операции в отдельную либу, так чтобы без копирования на CPU и обратно, единственный минус такого подхода — пришлось все операторы переписать (продублировать), некоторые вещи хоть и совпадают, но связывать не стал.
В общем, вот как теперь:
CPU: time/img, ms | GPU: time/img, ms | CPU: RAM, Mb | GPU: RAM, Mb | |
---|---|---|---|---|
Sunnet | 195 | 15 | 600 | 800 |
Tensorflow | 250 | 25 | 400 | 1400 |
То есть, по крайне мере, инференс даже быстрее получился, чем на TF.
Код теста не изменился.