// Copyright (C) 2015 Davis E. King (davis@dlib.net) // License: Boost Software License See LICENSE.txt for the full license. #ifndef DLIB_BOTTOM_uP_CLUSTER_Hh_ #define DLIB_BOTTOM_uP_CLUSTER_Hh_ #include #include #include "bottom_up_cluster_abstract.h" #include "../algs.h" #include "../matrix.h" #include "../disjoint_subsets.h" #include "../graph_utils.h" namespace dlib { // ---------------------------------------------------------------------------------------- namespace buc_impl { inline void merge_sets ( matrix& dists, unsigned long dest, unsigned long src ) { for (long r = 0; r < dists.nr(); ++r) dists(dest,r) = dists(r,dest) = std::max(dists(r,dest), dists(r,src)); } struct compare_dist { bool operator() ( const sample_pair& a, const sample_pair& b ) const { return a.distance() > b.distance(); } }; } // ---------------------------------------------------------------------------------------- template < typename EXP > unsigned long bottom_up_cluster ( const matrix_exp& dists_, std::vector& labels, unsigned long min_num_clusters, double max_dist = std::numeric_limits::infinity() ) { matrix dists = matrix_cast(dists_); // make sure requires clause is not broken DLIB_CASSERT(dists.nr() == dists.nc() && min_num_clusters > 0, "\t unsigned long bottom_up_cluster()" << "\n\t Invalid inputs were given to this function." << "\n\t dists.nr(): " << dists.nr() << "\n\t dists.nc(): " << dists.nc() << "\n\t min_num_clusters: " << min_num_clusters ); using namespace buc_impl; labels.resize(dists.nr()); disjoint_subsets sets; sets.set_size(dists.nr()); if (labels.size() == 0) return 0; // push all the edges in the graph into a priority queue so the best edges to merge // come first. std::priority_queue, compare_dist> que; for (long r = 0; r < dists.nr(); ++r) for (long c = r+1; c < dists.nc(); ++c) que.push(sample_pair(r,c,dists(r,c))); // Now start merging nodes. for (unsigned long iter = min_num_clusters; iter < sets.size(); ++iter) { // find the next best thing to merge. double best_dist = que.top().distance(); unsigned long a = sets.find_set(que.top().index1()); unsigned long b = sets.find_set(que.top().index2()); que.pop(); // we have been merging and modifying the distances, so make sure this distance // is still valid and these guys haven't been merged already. while(a == b || best_dist < dists(a,b)) { // Haven't merged it yet, so put it back in with updated distance for // reconsideration later. if (a != b) que.push(sample_pair(a, b, dists(a, b))); best_dist = que.top().distance(); a = sets.find_set(que.top().index1()); b = sets.find_set(que.top().index2()); que.pop(); } // now merge these sets if the best distance is small enough if (best_dist > max_dist) break; unsigned long news = sets.merge_sets(a,b); unsigned long olds = (news==a)?b:a; merge_sets(dists, news, olds); } // figure out which cluster each element is in. Also make sure the labels are // contiguous. std::map relabel; for (unsigned long r = 0; r < labels.size(); ++r) { unsigned long l = sets.find_set(r); // relabel to make contiguous if (relabel.count(l) == 0) { unsigned long next = relabel.size(); relabel[l] = next; } labels[r] = relabel[l]; } return relabel.size(); } // ---------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------- struct snl_range { snl_range() = default; snl_range(double val) : lower(val), upper(val) {} snl_range(double l, double u) : lower(l), upper(u) { DLIB_ASSERT(lower <= upper)} double lower = 0; double upper = 0; double width() const { return upper-lower; } bool operator<(const snl_range& item) const { return lower < item.lower; } }; inline snl_range merge(const snl_range& a, const snl_range& b) { return snl_range(std::min(a.lower, b.lower), std::max(a.upper, b.upper)); } inline double distance (const snl_range& a, const snl_range& b) { return std::max(a.lower,b.lower) - std::min(a.upper,b.upper); } inline std::ostream& operator<< (std::ostream& out, const snl_range& item ) { out << "["< segment_number_line ( const std::vector& x, const double max_range_width ) { DLIB_CASSERT(max_range_width >= 0); // create initial ranges, one for each value in x. So initially, all the ranges have // width of 0. std::vector ranges; for (auto v : x) ranges.push_back(v); std::sort(ranges.begin(), ranges.end()); std::vector greedy_final_ranges; if (ranges.size() == 0) return greedy_final_ranges; // We will try two different clustering strategies. One that does a simple greedy left // to right sweep and another that does a bottom up agglomerative clustering. This // first loop runs the greedy left to right sweep. Then at the end of this routine we // will return the results that produced the tightest clustering. greedy_final_ranges.push_back(ranges[0]); for (size_t i = 1; i < ranges.size(); ++i) { auto m = merge(greedy_final_ranges.back(), ranges[i]); if (m.width() <= max_range_width) greedy_final_ranges.back() = m; else greedy_final_ranges.push_back(ranges[i]); } // Here we do the bottom up clustering. So compute the edges connecting our ranges. // We will simply say there are edges between ranges if and only if they are // immediately adjacent on the number line. std::vector edges; for (size_t i = 1; i < ranges.size(); ++i) edges.push_back(sample_pair(i-1,i, distance(ranges[i-1],ranges[i]))); std::sort(edges.begin(), edges.end(), order_by_distance); disjoint_subsets sets; sets.set_size(ranges.size()); // Now start merging nodes. for (auto edge : edges) { // find the next best thing to merge. unsigned long a = sets.find_set(edge.index1()); unsigned long b = sets.find_set(edge.index2()); // merge it if it doesn't result in an interval that's too big. auto m = merge(ranges[a], ranges[b]); if (m.width() <= max_range_width) { unsigned long news = sets.merge_sets(a,b); ranges[news] = m; } } // Now create a list of the final ranges. We will do this by keeping track of which // range we already added to final_ranges. std::vector final_ranges; std::vector already_output(ranges.size(), false); for (unsigned long i = 0; i < sets.size(); ++i) { auto s = sets.find_set(i); if (!already_output[s]) { final_ranges.push_back(ranges[s]); already_output[s] = true; } } // only use the greedy clusters if they found a clustering with fewer clusters. // Otherwise, the bottom up clustering probably produced a more sensible clustering. if (final_ranges.size() <= greedy_final_ranges.size()) return final_ranges; else return greedy_final_ranges; } // ---------------------------------------------------------------------------------------- } #endif // DLIB_BOTTOM_uP_CLUSTER_Hh_