Pull to refresh

ResNet50. Своя реализация

Reading time4 min
Views16K
Всем привет. Библиотека нейросети описана в моей прошлой статье. Здесь решил показать как можно использовать обученную сеть из TF (Tensorflow) в своем решении, и стоит ли.

Под катом сравнение с оригинальной реализацией 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.
Код теста не изменился.
Tags:
Hubs:
Total votes 15: ↑14 and ↓1+13
Comments11

Articles