메뉴 건너뛰기

enjoyTools.net

dlib dnn_metric_learning_on_images_ex.cpp

2020.02.07 02:43

꿈돌이 조회 수:653

번역기 돌린 거

 

// The contents of this file are in the public domain. See LICENSE_FOR_EXAMPLE_PROGRAMS.txt

/*

    This is an example illustrating the use of the deep learning tools from the

    dlib C++ Library.  In it, we will show how to use the loss_metric layer to do

    metric learning on images.  

 

    The main reason you might want to use this kind of algorithm is because you

    would like to use a k-nearest neighbor classifier or similar algorithm, but

    you don't know a good way to calculate the distance between two things.  A

    popular example would be face recognition.  There are a whole lot of papers

    that train some kind of deep metric learning algorithm that embeds face

    images in some vector space where images of the same person are close to each

    other and images of different people are far apart.  Then in that vector

    space it's very easy to do face recognition with some kind of k-nearest

    neighbor classifier.

    

    In this example we will use a version of the ResNet network from the

    dnn_imagenet_ex.cpp example to learn to map images into some vector space where

    pictures of the same person are close and pictures of different people are far

    apart.

 

    You might want to read the simpler introduction to the deep metric learning

    API, dnn_metric_learning_ex.cpp, before reading this example.  You should

    also have read the examples that introduce the dlib DNN API before

    continuing.  These are dnn_introduction_ex.cpp and dnn_introduction2_ex.cpp.

 

    이것은 Dlib C++ 라이브러리의 딥러닝 도구 사용을 보여 주는 예입니다.

    여기서는 loss_metric 계층을 사용하여 메트릭 학습을 수행하는 방법을 보여줍니다.

 

    이러한 종류의 알고리즘을 사용하는 주된 이유는 k-최근접 이웃 분류기 또는 비슷한 알고리즘을 사용하려고 하지만,

    두 사물 사이의 거리를 계산하는 좋은 방법을 모르기 때문입니다.

    일반적인 예는 얼굴 인식입니다.

    같은 사람의 이미지가 서로 가깝고 다른 사람의 이미지가 멀리 떨어져 있는 벡터 공간에 얼굴 이미지를 심는

    일종의 Deep Metric Learning 알고리즘을 훈련시키는 논문들이 많이 있습니다.

    그 벡터 공간에서는 가장 k-최근접 이웃 분류기로 얼굴 인식을 하는 것이 매우 쉽습니다.

 

    이 예제에서는 dnn_imagenet_ex.cpp 예제의 ResNet 네트워크 버전을 사용하여

    동일한 사람의 사진이 가깝고 다른 사람의 사진이 멀리 떨어져 있는 벡터 공간에 이미지를 매핑하는 방법을 배우도록 하겠습니다.

 

    이 예제를 읽기 전에 dnn_metric_learning_ex.cpp에서

    deep metric learning API의 더 간단한 소개를 읽어볼 수 있습니다.

    계속하기 전에 dlib DNN API를 소개하는 예제인

    dnn_instruction_ex.cpp 및 dnn_instruction2_ex.cpp를 읽어보십시오.

*/

 

#include <dlib/dnn.h>

#include <dlib/image_io.h>

#include <dlib/misc_api.h>

 

using namespace dlib;

using namespace std;

 

// ----------------------------------------------------------------------------------------

 

// We will need to create some functions for loading data.  This program will

// expect to be given a directory structured as follows:

/*

데이터를 로드하기 위한 몇 가지 기능을 만들어야 합니다.

이 프로그램에는 아래과 같은 구성의 디렉토리가 제공되어야 합니다.

*/

//    top_level_directory/

//        person1/

//            image1.jpg

//            image2.jpg

//            image3.jpg

//        person2/

//            image4.jpg

//            image5.jpg

//            image6.jpg

//        person3/

//            image7.jpg

//            image8.jpg

//            image9.jpg

//

// The specific folder and image names don't matter, nor does the number of folders or

// images.  What does matter is that there is a top level folder, which contains

// subfolders, and each subfolder contains images of a single person.

/*

특정 폴더 및 이미지 이름은 중요하지 않으며 폴더 또는 이미지 수도 중요하지 않습니다.

중요한 것은 하위 폴더를 포함하는 최상위 폴더가 있고 각 하위 폴더에는 한 사람의 이미지가 들어 있다는 것입니다.

*/

 

// This function spiders the top level directory and obtains a list of all the

// image files.

/*

이 기능은 최상위 디렉토리를 거미로 지정하고 모든 이미지 파일 목록을 가져옵니다.

*/

std::vector<std::vector<string>> load_objects_list (

    const string& dir 

)

{

    std::vector<std::vector<string>> objects;

    for (auto subdir : directory(dir).get_dirs())

    {

        std::vector<string> imgs;

        for (auto img : subdir.get_files())

            imgs.push_back(img);

 

        if (imgs.size() != 0)

            objects.push_back(imgs);

    }

    return objects;

}

 

// This function takes the output of load_objects_list() as input and randomly

// selects images for training.  It should also be pointed out that it's really

// important that each mini-batch contain multiple images of each person.  This

// is because the metric learning algorithm needs to consider pairs of images

// that should be close (i.e. images of the same person) as well as pairs of

// images that should be far apart (i.e. images of different people) during each

// training step.

/*

이 기능은 load_objects_list()의 출력을 입력으로 가져오고고 훈련을 위해 임의로 영상을 선택합니다.

또한 각 미니 배치에는 각 사람의 이미지가 여러 개 포함되어 있어야합니다.

이는 메트릭 학습 알고리즘이 각 교육 단계에서 근접해야 하는 영상 쌍(즉, 같은 사람의 영상)과 멀리 떨어져 있어야 하는 영상 쌍(즉, 다른 사람의 영상)을 고려해야 하기 때문입니다.

*/

void load_mini_batch (

    const size_t num_people,     // how many different people to include

    /* 포함시킬 사람 수 */

    const size_t samples_per_id, // how many images per person to select.

    /* 선택한 사람당 이미지 수 */

    dlib::rand& rnd,

    const std::vector<std::vector<string>>& objs,

    std::vector<matrix<rgb_pixel>>& images,

    std::vector<unsigned long>& labels

)

{

    images.clear();

    labels.clear();

    DLIB_CASSERT(num_people <= objs.size(), "The dataset doesn't have that many people in it.");

 

    std::vector<bool> already_selected(objs.size(), false);

    matrix<rgb_pixel> image; 

    for (size_t i = 0; i < num_people; ++i)

    {

        size_t id = rnd.get_random_32bit_number()%objs.size();

        // don't pick a person we already added to the mini-batch

        /* 이미 미니 배치에 추가 한 사람을 선택하지 마십시오 */

        while(already_selected[id])

            id = rnd.get_random_32bit_number()%objs.size();

        already_selected[id] = true;

 

        for (size_t j = 0; j < samples_per_id; ++j)

        {

            const auto& obj = objs[id][rnd.get_random_32bit_number()%objs[id].size()];

            load_image(image, obj);

            images.push_back(std::move(image));

            labels.push_back(id);

        }

    }

 

    // You might want to do some data augmentation at this point.  Here we do some simple

    // color augmentation.

    /*

    이 시점에서 일부 데이터 증대를 수행할 수 있습니다.

    여기서는 간단한 색채 증대를 실시합니다.

    */

    for (auto&& crop : images)

    {

        disturb_colors(crop,rnd);

        // Jitter most crops

        if (rnd.get_random_double() > 0.1)

            crop = jitter_image(crop,rnd);

    }

 

 

    // All the images going into a mini-batch have to be the same size.  And really, all

    // the images in your entire training dataset should be the same size for what we are

    // doing to make the most sense.

    /*

    미니 배치로 들어가는 모든 이미지의 크기가 같아야합니다.

    실제로 전체 훈련 데이터 세트 내 모든 이미지는 우리가 가장 이해하기 쉽도록 동일한 크기여야합니다.

    */

 

    DLIB_CASSERT(images.size() > 0);

    for (auto&& img : images)

    {

        DLIB_CASSERT(img.nr() == images[0].nr() && img.nc() == images[0].nc(), 

            "All the images in a single mini-batch must be the same size.");

    }

}

 

// ----------------------------------------------------------------------------------------

 

// The next page of code defines a ResNet network.  It's basically copied

// and pasted from the dnn_imagenet_ex.cpp example, except we replaced the loss

// layer with loss_metric and make the network somewhat smaller.

/* 

코드의 다음 페이지는 ResNet 네트워크를 정의합니다.

손실 레이어를 loss_metric으로 바꾸고 네트워크를 약간 작게 만드는 것을 제외하면

기본적으로 dnn_imagenet_ex.cpp 예제에서 코드를 가져왔습니다..

 */

 

template <template <int,template<typename>class,int,typename> class block, int N, template<typename>class BN, typename SUBNET>

using residual = add_prev1<block<N,BN,1,tag1<SUBNET>>>;

 

template <template <int,template<typename>class,int,typename> class block, int N, template<typename>class BN, typename SUBNET>

using residual_down = add_prev2<avg_pool<2,2,2,2,skip1<tag2<block<N,BN,2,tag1<SUBNET>>>>>>;

 

template <int N, template <typename> class BN, int stride, typename SUBNET> 

using block  = BN<con<N,3,3,1,1,relu<BN<con<N,3,3,stride,stride,SUBNET>>>>>;

 

 

template <int N, typename SUBNET> using res       = relu<residual<block,N,bn_con,SUBNET>>;

template <int N, typename SUBNET> using ares      = relu<residual<block,N,affine,SUBNET>>;

template <int N, typename SUBNET> using res_down  = relu<residual_down<block,N,bn_con,SUBNET>>;

template <int N, typename SUBNET> using ares_down = relu<residual_down<block,N,affine,SUBNET>>;

 

// ----------------------------------------------------------------------------------------

 

template <typename SUBNET> using level0 = res_down<256,SUBNET>;

template <typename SUBNET> using level1 = res<256,res<256,res_down<256,SUBNET>>>;

template <typename SUBNET> using level2 = res<128,res<128,res_down<128,SUBNET>>>;

template <typename SUBNET> using level3 = res<64,res<64,res<64,res_down<64,SUBNET>>>>;

template <typename SUBNET> using level4 = res<32,res<32,res<32,SUBNET>>>;

 

template <typename SUBNET> using alevel0 = ares_down<256,SUBNET>;

template <typename SUBNET> using alevel1 = ares<256,ares<256,ares_down<256,SUBNET>>>;

template <typename SUBNET> using alevel2 = ares<128,ares<128,ares_down<128,SUBNET>>>;

template <typename SUBNET> using alevel3 = ares<64,ares<64,ares<64,ares_down<64,SUBNET>>>>;

template <typename SUBNET> using alevel4 = ares<32,ares<32,ares<32,SUBNET>>>;

 

 

// training network type

/* 훈련 네트워크 유형 */

using net_type = loss_metric<fc_no_bias<128,avg_pool_everything<

                            level0<

                            level1<

                            level2<

                            level3<

                            level4<

                            max_pool<3,3,2,2,relu<bn_con<con<32,7,7,2,2,

                            input_rgb_image

                            >>>>>>>>>>>>;

 

// testing network type (replaced batch normalization with fixed affine transforms)

/* 

테스트 네트워크 유형

(고정 아핀 변환으로 대체된 배치 정규화)

 */

using anet_type = loss_metric<fc_no_bias<128,avg_pool_everything<

                            alevel0<

                            alevel1<

                            alevel2<

                            alevel3<

                            alevel4<

                            max_pool<3,3,2,2,relu<affine<con<32,7,7,2,2,

                            input_rgb_image

                            >>>>>>>>>>>>;

 

// ----------------------------------------------------------------------------------------

 

int main(int argc, char** argv)

{

    if (argc != 2)

    {

        cout << "Give a folder as input.  It should contain sub-folders of images and we will " << endl;

        cout << "learn to distinguish between these sub-folders with metric learning.  " << endl;

        cout << "For example, you can run this program on the very small examples/johns dataset" << endl;

        cout << "that comes with dlib by running this command:" << endl;

        cout << "   ./dnn_metric_learning_on_images_ex johns" << endl;

        return 1;

    }

 

    auto objs = load_objects_list(argv[1]);

 

    cout << "objs.size(): "<< objs.size() << endl;

 

    std::vector<matrix<rgb_pixel>> images;

    std::vector<unsigned long> labels;

 

 

    net_type net;

 

    dnn_trainer<net_type> trainer(net, sgd(0.0001, 0.9));

    trainer.set_learning_rate(0.1);

    trainer.be_verbose();

    trainer.set_synchronization_file("face_metric_sync", std::chrono::minutes(5));

    // I've set this to something really small to make the example terminate

    // sooner.  But when you really want to train a good model you should set

    // this to something like 10000 so training doesn't terminate too early.

    /* 

    저는 예를 더 빨리 끝내기 위해 이것을 아주 작은 것으로 설정했습니다.

    하지만 여러분이 정말 좋은 모델을 훈련시키고 싶다면,

    여러분은 이것을 10000 정도로 설정해야 훈련이 너무 일찍 끝나지 않습니다.

     */

    trainer.set_iterations_without_progress_threshold(300);

 

    // If you have a lot of data then it might not be reasonable to load it all

    // into RAM.  So you will need to be sure you are decompressing your images

    // and loading them fast enough to keep the GPU occupied.  I like to do this

    // using the following coding pattern: create a bunch of threads that dump

    // mini-batches into dlib::pipes.

    /* 

    데이터가 많은 경우 RAM에 데이터를 모두 로드하는 것이 합리적이지 않을 수 있습니다.

    따라서 GPU를 계속 사용할 수 있도록 이미지를 압축 해제하고 빠르게 로드해야 합니다.

    나는 다음과 같은 코딩 패턴을 사용하여 이 작업을 수행하는 것을 선호합니다.:

    미니 배치를 dlib:pipes로 덤프하는 여러 개의 스레드 생성.

     */

    dlib::pipe<std::vector<matrix<rgb_pixel>>> qimages(4);

    dlib::pipe<std::vector<unsigned long>> qlabels(4);

    auto data_loader = [&qimages, &qlabels, &objs](time_t seed)

    {

        dlib::rand rnd(time(0)+seed);

        std::vector<matrix<rgb_pixel>> images;

        std::vector<unsigned long> labels;

        while(qimages.is_enabled())

        {

            try

            {

                load_mini_batch(5, 5, rnd, objs, images, labels);

                qimages.enqueue(images);

                qlabels.enqueue(labels);

            }

            catch(std::exception& e)

            {

                cout << "EXCEPTION IN LOADING DATA" << endl;

                cout << e.what() << endl;

            }

        }

    };

    // Run the data_loader from 5 threads.  You should set the number of threads

    // relative to the number of CPU cores you have.

    /*

    5개의 스레드에서 data_loader를 실행합니다.

    CPU 코어 수에 맞추어 스레드 수를 설정해야 합니다.

     */

    std::thread data_loader1([data_loader](){ data_loader(1); });

    std::thread data_loader2([data_loader](){ data_loader(2); });

    std::thread data_loader3([data_loader](){ data_loader(3); });

    std::thread data_loader4([data_loader](){ data_loader(4); });

    std::thread data_loader5([data_loader](){ data_loader(5); });

 

 

    // Here we do the training.  We keep passing mini-batches to the trainer until the

    // learning rate has dropped low enough.

    /* 

    여기서 훈련을 합니다.

    우리는 학습률이 충분히 낮아질 때까지 미니 배치를 트레이너에게 계속 전달합니다.

     */

    while(trainer.get_learning_rate() >= 1e-4)

    {

        qimages.dequeue(images);

        qlabels.dequeue(labels);

        trainer.train_one_step(images, labels);

    }

 

    // Wait for training threads to stop

    /* 훈련 스레드가 중지될 때까지 기다립니다. */

    trainer.get_net();

    cout << "done training" << endl;

 

    // Save the network to disk

    /* 네트워크를 디스크에 저장합니다. */

    net.clean();

    serialize("metric_network_renset.dat") << net;

 

    // stop all the data loading threads and wait for them to terminate.

    /* 모든 데이터 로딩 스레드를 중지하고 종료될 때까지 기다립니다. */

    qimages.disable();

    qlabels.disable();

    data_loader1.join();

    data_loader2.join();

    data_loader3.join();

    data_loader4.join();

    data_loader5.join();

 

 

 

 

 

    // Now, just to show an example of how you would use the network, let's check how well

    // it performs on the training data.

    /* 

    이제 네트워크를 사용하는 방법에 대한 예시를 보여드리기 위해

    네트워크가 훈련 데이터에 대해 얼마나 잘 수행되는지 살펴보겠습니다.

     */

    dlib::rand rnd(time(0));

    load_mini_batch(5, 5, rnd, objs, images, labels);

 

    // Normally you would use the non-batch-normalized version of the network to do

    // testing, which is what we do here.

    /* 일반적으로 비 배치 정규화 버전의 네트워크를 사용하여 테스트를 수행할 수 있습니다. */

    anet_type testing_net = net;

 

    // Run all the images through the network to get their vector embeddings.

    /* 네트워크를 통해 모든 이미지를 실행하여 벡터 임베딩을 가져옵니다. */

    std::vector<matrix<float,0,1>> embedded = testing_net(images);

 

    // Now, check if the embedding puts images with the same labels near each other and

    // images with different labels far apart.

    /* 

    이제, 임베딩이 동일한 라벨을 가진 영상을 서로 가까이에 배치하고

    서로 다른 라벨을 가진 영상을 멀리 떨어뜨리는지 확인합니다.

     */

    int num_right = 0;

    int num_wrong = 0;

    for (size_t i = 0; i < embedded.size(); ++i)

    {

        for (size_t j = i+1; j < embedded.size(); ++j)

        {

            if (labels[i] == labels[j])

            {

                // The loss_metric layer will cause images with the same label to be less

                // than net.loss_details().get_distance_threshold() distance from each

                // other.  So we can use that distance value as our testing threshold.

                /* 

                loss_metric 레이어는 같은 라벨을 가진 이미지가

                net.loss_details().get_distance_threshold() 거리보다 작게 만듭니다.

                따라서, 우리는 거리값을 테스트 임계값으로 사용할 수 있습니다.

                 */

                if (length(embedded[i]-embedded[j]) < testing_net.loss_details().get_distance_threshold())

                    ++num_right;

                else

                    ++num_wrong;

            }

            else

            {

                if (length(embedded[i]-embedded[j]) >= testing_net.loss_details().get_distance_threshold())

                    ++num_right;

                else

                    ++num_wrong;

            }

        }

    }

 

    cout << "num_right: "<< num_right << endl;

    cout << "num_wrong: "<< num_wrong << endl;

 

}