diff --git a/dlib/svm/kkmeans.h b/dlib/svm/kkmeans.h index 4c72106d87..71c44d51ee 100644 --- a/dlib/svm/kkmeans.h +++ b/dlib/svm/kkmeans.h @@ -215,8 +215,11 @@ namespace dlib centers[i]->train(initial_centers(i)); } + // Discard old assignments rather than preserving them while resizing. + assignments.set_size(0); assignments.resize(samples.size()); - + // Force every sample to be assigned on the first iteration. + std::fill(assignments.begin(), assignments.end(), number_of_centers()); bool assignment_changed = true; // loop until the centers stabilize diff --git a/dlib/test/svm.cpp b/dlib/test/svm.cpp index b46d44331d..fd5f19eeef 100644 --- a/dlib/test/svm.cpp +++ b/dlib/test/svm.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include "../stl_checked.h" #include "../array.h" @@ -27,6 +28,102 @@ namespace logger dlog("test.svm"); +// ---------------------------------------------------------------------------------------- + + void test_kkmeans_initial_assignments ( + ) + { + typedef matrix sample_type; + typedef linear_kernel kernel_type; + kkmeans kmeans(kcentroid(kernel_type(), 1e-9)); + + const auto make_samples = [](std::initializer_list values) + { + std::vector samples; + for (double value : values) + { + sample_type sample; + sample = value; + samples.push_back(sample); + } + return samples; + }; + const auto check_center = [&](unsigned long index, double value, unsigned long count) + { + sample_type expected; + expected = value; + const auto& center = kmeans.get_kcentroid(index); + DLIB_TEST(center.samples_trained() == count); + DLIB_TEST(center(expected) < 1e-6); + }; + + // A single cluster must be recomputed from the samples even though every + // assignment is zero. A zero iteration budget still leaves the seed intact. + const auto samples = make_samples({2,4}); + const auto seed = make_samples({10}); + kmeans.train(samples, seed, 0); + check_center(0, 10, 1); + kmeans.train(samples, seed, 1); + check_center(0, 3, 2); + kmeans.train(samples, seed); + check_center(0, 3, 2); + + // The same first-pass requirement applies when several centers are present + // but all samples initially choose the first one. + kmeans.set_number_of_centers(2); + kmeans.train(samples, make_samples({0,100}), 1); + check_center(0, 3, 2); + DLIB_TEST(kmeans.get_kcentroid(1).samples_trained() == 0); + + // Training starts from the supplied seeds each time, including after a + // change in the number of samples or a previous run with the same labels. + const auto two_clusters = make_samples({0,2,8,10}); + const auto two_seeds = make_samples({0,10}); + for (unsigned int i = 0; i < 2; ++i) + { + kmeans.train(two_clusters, two_seeds); + check_center(0, 1, 2); + check_center(1, 9, 2); + } + kmeans.train(make_samples({0,2,8,10,12}), two_seeds); + check_center(0, 1, 2); + check_center(1, 10, 3); + kmeans.train(two_clusters, two_seeds); + check_center(0, 1, 2); + check_center(1, 9, 2); + + // Only one point initially chooses the second center, below the default + // change threshold of two points. The first pass must nevertheless count + // every sample as newly assigned so subsequent refinement can run. + std::vector imbalanced(200); + for (unsigned long i = 0; i < imbalanced.size(); ++i) + imbalanced[i] = i < 100 ? 0 : 40; + imbalanced.back() = 60; + const auto distant_seeds = make_samples({0,100}); + kmeans.train(imbalanced, distant_seeds, 1); + check_center(0, 3960.0/199, 199); + check_center(1, 60, 1); + kmeans.train(imbalanced, distant_seeds, 2); + check_center(0, 0, 100); + check_center(1, 40.2, 100); + kmeans.train(imbalanced, distant_seeds); + check_center(0, 0, 100); + check_center(1, 40.2, 100); + for (unsigned long i = 0; i < imbalanced.size(); ++i) + DLIB_TEST(kmeans(imbalanced[i]) == (i < 100 ? 0 : 1)); + + // Also exercise the general kernel centroid, rather than only its linear + // specialization. Its expected centroid is trained directly on the data. + typedef radial_basis_kernel rbf_kernel; + kcentroid expected(rbf_kernel(0.1), 1e-9); + kkmeans kernel_kmeans(expected); + for (const auto& sample : samples) + expected.train(sample); + kernel_kmeans.train(samples, seed); + DLIB_TEST(kernel_kmeans.get_kcentroid(0).samples_trained() == samples.size()); + DLIB_TEST(kernel_kmeans.get_kcentroid(0)(expected) < 1e-6); + } + // ---------------------------------------------------------------------------------------- void test_clutering ( @@ -649,6 +746,7 @@ namespace { test_kernel_derivative(); unittest_binary_classification(); + test_kkmeans_initial_assignments(); test_clutering(); test_regression(); test_anomaly_detection();