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

в 14:55, , рубрики: resnet, машинное обучение, нейронные сети, обработка изображений

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

Под катом сравнение с оригинальной реализацией TF, демо приложение для распознавания картинок, ну и… выводы. Кому интересно, прошу.

Как устроена ResNet можете узнать, например, здесь.

Вот так выглядит структура сети в цифрах:

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

По коду получилось не проще и не сложнее чем на питоне.

Код 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/skyNet/example/resnet50/resNet50Struct.json",
           weightPath = "c:/cpp/other/skyNet/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);
 

Сделал приложение для интереса. Скачать можете отсюда. Объем большой из-за весов сети. Исходники там есть, можете использовать для примера.

Приложение создано только для статьи, поддерживаться не будет, поэтому не включал в репозиторий проекта.

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

Теперь, что получилось по сравнению с TF.

Показания после прогона 100 изображений, в среднем. Машина: i5-2400, GF1050, Win7, MSVC12.

Значения результатов распознавания совпадают до 3-го знака.

Код теста

CPU: time/img, ms GPU: time/img, ms CPU: RAM, Mb GPU: RAM, Mb
Skynet 410 120 600 1200
Tensorflow 250 25 400 1400

На самом деле плачевно все конечно.

Для CPU решил не использовать MKL-DNN, сам думал довести: перераспределил память для последовательного чтения, по максимуму загрузил векторные регистры. Возможно надо было приводить к матричному умножению, и/или еще какие хаки. Упирался здесь, по началу было хуже, правильней было бы использовать MKL все таки.

На GPU время тратится на копирование памяти из/в память видеокарты, и не все операции выполняются на GPU.

Выводы какие можно сделать из всей этой суеты:

— не выпендриваться, а использовать известные проверенные решения, дошли до ума уже более-менее вроде. Сидел сам на mxnet когда то, да маялся с нативным использованием, об этом ниже;

— не пытаться использовать нативный С интерфейс ML фреймворков. А юзать их на языке, на который ориентировались разработчики, то есть python.

Легкий путь использования функционала ML из своего языка, — сделать сервис-процесс на питоне, и по сокету слать ему картинки, получится разделение ответственности и отсутствие тяжелого кода.

Все пожалуй. Статья коротенькая получилась, но выводы, думаю, ценны, и относятся не только к ML.

Спасибо.

PS: если у кого есть желание и силы попытаться все таки догнать до TF, welcome!)

Автор: Александр Медведев

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js