    // The contents of this file are in the public domain. See LICENSE_FOR_EXAMPLE_PROGRAMS.txt
    /*
        This program was used to train the resnet34_1000_imagenet_classifier.dnn
        network used by the dnn_imagenet_ex.cpp example program.

        You should be familiar with dlib's DNN module before reading this example
        program.  So read dnn_introduction_ex.cpp and dnn_introduction2_ex.cpp first.
    */

    #include <dlib/dnn.h>
    #include <iostream>
    #include <dlib/data_io.h>
    #include <dlib/image_transforms.h>
    #include <dlib/dir_nav.h>
    #include <iterator>
    #include <thread>

    using namespace std;
    using namespace dlib;

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

    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 level1 = res<512,res<512,res_down<512,SUBNET>>>;
    template <typename SUBNET> using level2 = res<256,res<256,res<256,res<256,res<256,res_down<256,SUBNET>>>>>>;
    template <typename SUBNET> using level3 = res<128,res<128,res<128,res_down<128,SUBNET>>>>;
    template <typename SUBNET> using level4 = res<64,res<64,res<64,SUBNET>>>;

    template <typename SUBNET> using alevel1 = ares<512,ares<512,ares_down<512,SUBNET>>>;
    template <typename SUBNET> using alevel2 = ares<256,ares<256,ares<256,ares<256,ares<256,ares_down<256,SUBNET>>>>>>;
    template <typename SUBNET> using alevel3 = ares<128,ares<128,ares<128,ares_down<128,SUBNET>>>>;
    template <typename SUBNET> using alevel4 = ares<64,ares<64,ares<64,SUBNET>>>;

    // training network type
    using net_type = loss_multiclass_log<fc<1000,avg_pool_everything<
                                level1<
                                level2<
                                level3<
                                level4<
                                max_pool<3,3,2,2,relu<bn_con<con<64,7,7,2,2,
                                input_rgb_image_sized<227>
                                >>>>>>>>>>>;

    // testing network type (replaced batch normalization with fixed affine transforms)
    using anet_type = loss_multiclass_log<fc<1000,avg_pool_everything<
                                alevel1<
                                alevel2<
                                alevel3<
                                alevel4<
                                max_pool<3,3,2,2,relu<affine<con<64,7,7,2,2,
                                input_rgb_image_sized<227>
                                >>>>>>>>>>>;

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

    rectangle make_random_cropping_rect_resnet(
        const matrix<rgb_pixel>& img,
        dlib::rand& rnd
    )
    {
        // figure out what rectangle we want to crop from the image
        double mins = 0.466666666, maxs = 0.875;
        auto scale = mins + rnd.get_random_double()*(maxs-mins);
        auto size = scale*std::min(img.nr(), img.nc());
        rectangle rect(size, size);
        // randomly shift the box around
        point offset(rnd.get_random_32bit_number()%(img.nc()-rect.width()),
                     rnd.get_random_32bit_number()%(img.nr()-rect.height()));
        return move_rect(rect, offset);
    }

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

    void randomly_crop_image (
        const matrix<rgb_pixel>& img,
        matrix<rgb_pixel>& crop,
        dlib::rand& rnd
    )
    {
        auto rect = make_random_cropping_rect_resnet(img, rnd);

        // now crop it out as a 227x227 image.
        extract_image_chip(img, chip_details(rect, chip_dims(227,227)), crop);

        // Also randomly flip the image
        if (rnd.get_random_double() > 0.5)
            crop = fliplr(crop);

        // And then randomly adjust the colors.
        apply_random_color_offset(crop, rnd);
    }

    void randomly_crop_images (
        const matrix<rgb_pixel>& img,
        dlib::array<matrix<rgb_pixel>>& crops,
        dlib::rand& rnd,
        long num_crops
    )
    {
        std::vector<chip_details> dets;
        for (long i = 0; i < num_crops; ++i)
        {
            auto rect = make_random_cropping_rect_resnet(img, rnd);
            dets.push_back(chip_details(rect, chip_dims(227,227)));
        }

        extract_image_chips(img, dets, crops);

        for (auto&& img : crops)
        {
            // Also randomly flip the image
            if (rnd.get_random_double() > 0.5)
                img = fliplr(img);

            // And then randomly adjust the colors.
            apply_random_color_offset(img, rnd);
        }
    }

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

    struct image_info
    {
        string filename;
        string label;
        long numeric_label;
    };

    std::vector<image_info> get_imagenet_train_listing(
        const std::string& images_folder
    )
    {
        std::vector<image_info> results;
        image_info temp;
        temp.numeric_label = 0;
        // We will loop over all the label types in the dataset, each is contained in a subfolder.
        auto subdirs = directory(images_folder).get_dirs();
        // But first, sort the sub directories so the numeric labels will be assigned in sorted order.
        std::sort(subdirs.begin(), subdirs.end());
        for (auto subdir : subdirs)
        {
            // Now get all the images in this label type
            temp.label = subdir.name();
            for (auto image_file : subdir.get_files())
            {
                temp.filename = image_file;
                results.push_back(temp);
            }
            ++temp.numeric_label;
        }
        return results;
    }

    std::vector<image_info> get_imagenet_val_listing(
        const std::string& imagenet_root_dir,
        const std::string& validation_images_file
    )
    {
        ifstream fin(validation_images_file);
        string label, filename;
        std::vector<image_info> results;
        image_info temp;
        temp.numeric_label = -1;
        while(fin >> label >> filename)
        {
            temp.filename = imagenet_root_dir+"/"+filename;
            if (!file_exists(temp.filename))
            {
                cerr << "file doesn't exist! " << temp.filename << endl;
                exit(1);
            }
            if (label != temp.label)
                ++temp.numeric_label;

            temp.label = label;
            results.push_back(temp);
        }

        return results;
    }

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

    int main(int argc, char** argv) try
    {
        if (argc != 3)
        {
            cout << "To run this program you need a copy of the imagenet ILSVRC2015 dataset and" << endl;
            cout << "also the file http://dlib.net/files/imagenet2015_validation_images.txt.bz2" << endl;
            cout << endl;
            cout << "With those things, you call this program like this: " << endl;
            cout << "./dnn_imagenet_train_ex /path/to/ILSVRC2015 imagenet2015_validation_images.txt" << endl;
            return 1;
        }

        cout << "\nSCANNING IMAGENET DATASET\n" << endl;

        auto listing = get_imagenet_train_listing(string(argv[1])+"/Data/CLS-LOC/train/");
        cout << "images in dataset: " << listing.size() << endl;
        const auto number_of_classes = listing.back().numeric_label+1;
        if (listing.size() == 0 || number_of_classes != 1000)
        {
            cout << "Didn't find the imagenet dataset. " << endl;
            return 1;
        }

        set_dnn_prefer_smallest_algorithms();

        const double initial_learning_rate = 0.1;
        const double weight_decay = 0.0001;
        const double momentum = 0.9;

        net_type net;
        dnn_trainer<net_type> trainer(net,sgd(weight_decay, momentum));
        trainer.be_verbose();
        trainer.set_learning_rate(initial_learning_rate);
        trainer.set_synchronization_file("imagenet_trainer_state_file.dat", std::chrono::minutes(10));
        // This threshold is probably excessively large.  You could likely get good results
        // with a smaller value but if you aren't in a hurry this value will surely work well.
        trainer.set_iterations_without_progress_threshold(20000);
        // Since the progress threshold is so large might as well set the batch normalization
        // stats window to something big too.
        set_all_bn_running_stats_window_sizes(net, 1000);

        std::vector<matrix<rgb_pixel>> samples;
        std::vector<unsigned long> labels;

        // Start a bunch of threads that read images from disk and pull out random crops.  It's
        // important to be sure to feed the GPU fast enough to keep it busy.  Using multiple
        // thread for this kind of data preparation helps us do that.  Each thread puts the
        // crops into the data queue.
        dlib::pipe<std::pair<image_info,matrix<rgb_pixel>>> data(200);
        auto f = [&data, &listing](time_t seed)
        {
            dlib::rand rnd(time(0)+seed);
            matrix<rgb_pixel> img;
            std::pair<image_info, matrix<rgb_pixel>> temp;
            while(data.is_enabled())
            {
                temp.first = listing[rnd.get_random_32bit_number()%listing.size()];
                load_image(img, temp.first.filename);
                randomly_crop_image(img, temp.second, rnd);
                data.enqueue(temp);
            }
        };
        std::thread data_loader1([f](){ f(1); });
        std::thread data_loader2([f](){ f(2); });
        std::thread data_loader3([f](){ f(3); });
        std::thread data_loader4([f](){ f(4); });

        // The main training loop.  Keep making mini-batches and giving them to the trainer.
        // We will run until the learning rate has dropped by a factor of 1e-3.
        while(trainer.get_learning_rate() >= initial_learning_rate*1e-3)
        {
            samples.clear();
            labels.clear();

            // make a 160 image mini-batch
            std::pair<image_info, matrix<rgb_pixel>> img;
            while(samples.size() < 160)
            {
                data.dequeue(img);

                samples.push_back(std::move(img.second));
                labels.push_back(img.first.numeric_label);
            }

            trainer.train_one_step(samples, labels);
        }

        // Training done, tell threads to stop and make sure to wait for them to finish before
        // moving on.
        data.disable();
        data_loader1.join();
        data_loader2.join();
        data_loader3.join();
        data_loader4.join();

        // also wait for threaded processing to stop in the trainer.
        trainer.get_net();

        net.clean();
        cout << "saving network" << endl;
        serialize("resnet34.dnn") << net;

        // Now test the network on the imagenet validation dataset.  First, make a testing
        // network with softmax as the final layer.  We don't have to do this if we just wanted
        // to test the "top1 accuracy" since the normal network outputs the class prediction.
        // But this snet object will make getting the top5 predictions easy as it directly
        // outputs the probability of each class as its final output.
        softmax<anet_type::subnet_type> snet; snet.subnet() = net.subnet();

        cout << "Testing network on imagenet validation dataset..." << endl;
        int num_right = 0;
        int num_wrong = 0;
        int num_right_top1 = 0;
        int num_wrong_top1 = 0;
        dlib::rand rnd(time(0));
        // loop over all the imagenet validation images
        for (auto l : get_imagenet_val_listing(argv[1], argv[2]))
        {
            dlib::array<matrix<rgb_pixel>> images;
            matrix<rgb_pixel> img;
            load_image(img, l.filename);
            // Grab 16 random crops from the image.  We will run all of them through the
            // network and average the results.
            const int num_crops = 16;
            randomly_crop_images(img, images, rnd, num_crops);
            // p(i) == the probability the image contains object of class i.
            matrix<float,1,1000> p = sum_rows(mat(snet(images.begin(), images.end())))/num_crops;

            // check top 1 accuracy
            if (index_of_max(p) == l.numeric_label)
                ++num_right_top1;
            else
                ++num_wrong_top1;

            // check top 5 accuracy
            bool found_match = false;
            for (int k = 0; k < 5; ++k)
            {
                long predicted_label = index_of_max(p);
                p(predicted_label) = 0;
                if (predicted_label == l.numeric_label)
                {
                    found_match = true;
                    break;
                }

            }
            if (found_match)
                ++num_right;
            else
                ++num_wrong;
        }
        cout << "val top5 accuracy:  " << num_right/(double)(num_right+num_wrong) << endl;
        cout << "val top1 accuracy:  " << num_right_top1/(double)(num_right_top1+num_wrong_top1) << endl;
    }
    catch(std::exception& e)
    {
        cout << e.what() << endl;
    }