@@ -20,6 +20,8 @@ struct AttractorsViaFeaturizing{DS<:GeneralizedDynamicalSystem, T, F, A, K, M} <
2020 clust_method:: String
2121 clustering_threshold:: Float64
2222 min_neighbors:: Int
23+ rescale_features:: Bool
24+ optimal_radius_method:: String
2325end
2426DynamicalSystemsBase. get_rule_for_print(m:: AttractorsViaFeaturizing ) =
2527get_rule_for_print(m. ds)
3840"""
3941 AttractorsViaFeaturizing(ds::DynamicalSystem, featurizer::Function; kwargs...) → mapper
4042
41- Initialize a `mapper` that maps initial conditions
42- to attractors using the featurizing and clustering method of [^Stender2021].
43- See [`AttractorMapper`](@ref) for how to use the `mapper`.
43+ Initialize a `mapper` that maps initial conditions to attractors using the featurizing and
44+ clustering method of [^Stender2021]. See [`AttractorMapper`](@ref) for how to use the
45+ `mapper`.
4446
45- `featurizer` is a function that takes as an input an integrated trajectory `A::Dataset`
46- and the corresponding time vector `t` and returns a `Vector{<:Real}` of features
47- describing the trajectory.
47+ `featurizer` is a function that takes as an input an integrated trajectory `A::Dataset` and
48+ the corresponding time vector `t` and returns a `Vector{<:Real}` of features describing the
49+ trajectory.
4850
4951## Keyword arguments
5052### Integration
5153* `T=100, Ttr=100, Δt=1, diffeq=NamedTuple()`: Propagated to [`trajectory`](@ref).
5254
5355### Feature extraction and classification
54- * `attractors_ic = nothing` Enables supervised version, see below.
55- If given, must be a `Dataset` of initial conditions each leading to a different attractor.
56- * `min_neighbors = 10`: (unsupervised method only) minimum number of neighbors
57- (i.e. of similar features) each feature needs to have in order to be considered in a
58- cluster (fewer than this, it is labeled as an outlier, `-1`).
56+ * `attractors_ic = nothing` Enables supervised version, see below. If given, must be a
57+ `Dataset` of initial conditions each leading to a different attractor.
58+ * `min_neighbors = 10`: (unsupervised method only) minimum number of neighbors (i.e. of
59+ similar features) each feature needs to have in order to be considered in a cluster (fewer
60+ than this, it is labeled as an outlier, `-1`).
5961* `clust_method_norm=Euclidean()`: metric to be used in the clustering.
60- * `clustering_threshold = 0.0`: Maximum allowed distance between a feature and the
61- cluster center for it to be considered inside the cluster.
62- Only used when `clust_method = "kNN_thresholded"`.
63- * `clust_method = clustering_threshold > 0 ? "kNN_thresholded" : "kNN"`:
64- (supervised method only) which clusterization method to
65- apply. If `"kNN"`, the first-neighbor clustering is used. If `"kNN_thresholded"`, a
66- subsequent step is taken, which considers as unclassified (label `-1`) the features
67- whose distance to the nearest template is above the `clustering_threshold`.
62+ * `clustering_threshold = 0.0`: Maximum allowed distance between a feature and the cluster
63+ center for it to be considered inside the cluster. Only used when `clust_method =
64+ "kNN_thresholded"`.
65+ * `clust_method = clustering_threshold > 0 ? "kNN_thresholded" : "kNN"`: (supervised method
66+ only) which clusterization method to apply. If `"kNN"`, the first-neighbor clustering is
67+ used. If `"kNN_thresholded"`, a subsequent step is taken, which considers as unclassified
68+ (label `-1`) the features whose distance to the nearest template is above the
69+ `clustering_threshold`.
70+ * `rescale_features = true`: (unsupervised method): if true, rescale each dimension of the
71+ extracted features separately into the range `[0,1]`.
72+ * `optimal_radius_method = silhouettes` (unsupervised method): the method used to determine
73+ the optimal radius for clustering features in the unsupervised method. The `silhouettes`
74+ method chooses the radius that maximizes the average silhouette values of clusters, and
75+ is an iterative optimization procedure that may take some time to execute. The `elbow`
76+ method chooses the the radius according to the elbow (knee, highest-derivative method)
77+ (see [`optimal_radius_dbscan_elbow`](@ref) for details), and is quicker though possibly
78+ leads to worse clustering.
6879
6980## Description
70- The trajectory `X` of each initial condition is transformed into a vector of features.
71- Each feature is a number useful in _characterizing the attractor_ the initial condition
72- ends up at, and distinguishing it from other attrators. Example features are the mean or
73- standard deviation of one of the of the timeseries of the trajectory,
74- the entropy of the first two dimensions, the fractal dimension of `X`,
75- or anything else you may fancy.
76- The vectors of features are then used to identify to which attractor
77- each trajectory belongs (i.e. in which basin of attractor each initial condition is in).
78- The method thus relies on the user having at least some basic idea about what attractors
79- to expect in order to pick the right features, in contrast to [`AttractorsViaRecurrences`](@ref).
80-
81- The algorithm of[^Stender2021] that we use has two versions to do this.
82- If the attractors are not known a-priori the **unsupervised versions** should be used.
83- Here, the vectors of features of each initial condition are mapped to an attractor by
84- analysing how the features are clustered in the feature space. Using the DBSCAN algorithm,
85- we identify these clusters of features, and consider each cluster to represent an
86- attractor. Features whose attractor is not identified are labeled as `-1`.
81+ The trajectory `X` of each initial condition is transformed into a vector of features. Each
82+ feature is a number useful in _characterizing the attractor_ the initial condition ends up
83+ at, and distinguishing it from other attrators. Example features are the mean or standard
84+ deviation of one of the of the timeseries of the trajectory, the entropy of the first two
85+ dimensions, the fractal dimension of `X`, or anything else you may fancy. The vectors of
86+ features are then used to identify to which attractor each trajectory belongs (i.e. in which
87+ basin of attractor each initial condition is in). The method thus relies on the user having
88+ at least some basic idea about what attractors to expect in order to pick the right
89+ features, in contrast to [`AttractorsViaRecurrences`](@ref).
90+
91+ The algorithm of[^Stender2021] that we use has two versions to do this. If the attractors
92+ are not known a-priori the **unsupervised versions** should be used. Here, the vectors of
93+ features of each initial condition are mapped to an attractor by analysing how the features
94+ are clustered in the feature space. Using the DBSCAN algorithm, we identify these clusters
95+ of features, and consider each cluster to represent an attractor. Features whose attractor
96+ is not identified are labeled as `-1`. If each feature spans different scales of magnitude,
97+ rescaling them into the same `[0,1]` interval can bring significant improvements in the
98+ clustering in case the `Euclidean` distance metric is used.
8799
88100In the **supervised version**, the attractors are known to the user, who provides one
89- initial condition for each attractor using the `attractors_ic` keyword.
90- The algorithm then evolves these initial conditions, extracts their features, and uses them
91- as templates representing the attrators. Each trajectory is considered to belong to the
92- nearest template (based on the distance in feature space).
93- Notice that the functionality of this version is similar to [`AttractorsViaProximity`](@ref).
94- Generally speaking, the [`AttractorsViaProximity`](@ref) is superior. However, if the
95- dynamical system has extremely high-dimensionality, there may be reasons to use the
96- supervised method of this featurizing algorithm instead.
101+ initial condition for each attractor using the `attractors_ic` keyword. The algorithm then
102+ evolves these initial conditions, extracts their features, and uses them as templates
103+ representing the attrators. Each trajectory is considered to belong to the nearest template
104+ (based on the distance in feature space). Notice that the functionality of this version is
105+ similar to [`AttractorsViaProximity`](@ref). Generally speaking, the
106+ [`AttractorsViaProximity`](@ref) is superior. However, if the dynamical system has extremely
107+ high-dimensionality, there may be reasons to use the supervised method of this featurizing
108+ algorithm instead.
97109
98110## Parallelization note
99- The trajectories in this method are integrated in parallel using `Threads`.
100- To enable this, simply start Julia with the number of threads you want to use.
111+ The trajectories in this method are integrated in parallel using `Threads`. To enable this,
112+ simply start Julia with the number of threads you want to use.
101113
102- [^Stender2021]:
103- Stender & Hoffmann, [bSTAB: an open-source software for computing the basin
114+ [^Stender2021]: Stender & Hoffmann, [bSTAB: an open-source software for computing the basin
104115 stability of multi-stable dynamical systems](https://doi.org/10.1007/s11071-021-06786-5)
105116"""
106117function AttractorsViaFeaturizing(ds:: GeneralizedDynamicalSystem , featurizer:: Function ;
107118 attractors_ic:: Union{AbstractDataset, Nothing} = nothing , T= 100 , Ttr= 100 , Δt= 1 ,
108119 clust_method_norm= Euclidean(),
109120 clustering_threshold = 0.0 , min_neighbors = 10 , diffeq = NamedTuple(),
110- clust_method = clustering_threshold > 0 ? " kNN_thresholded" : " kNN" ,
121+ clust_method = clustering_threshold > 0 ? " kNN_thresholded" : " kNN" ,
122+ rescale_features= true , optimal_radius_method= " silhouettes" ,
111123 )
112- if isnothing(attractors_ic)
113- @warn " Unsupervised clustering algorithm is currently bugged and may not identify all clusters."
114- end
115124 if ds isa ContinuousDynamicalSystem
116125 T, Ttr, Δt = float.((T, Ttr, Δt))
117126 end
118127 return AttractorsViaFeaturizing(
119128 ds, Ttr, Δt, T, featurizer, attractors_ic, diffeq,
120- clust_method_norm, clust_method, Float64(clustering_threshold), min_neighbors
129+ clust_method_norm, clust_method, Float64(clustering_threshold), min_neighbors,
130+ rescale_features, optimal_radius_method
121131 )
122132end
123133
140150function extract_features(mapper:: AttractorsViaFeaturizing , ics:: Union{AbstractDataset, Function} ;
141151 show_progress = true , N = 1000 )
142152
143- N = (typeof(ics) <: Function ) ? N : size(ics, 1 ) # number of actual ICs
153+ N = (typeof(ics) <: Function ) ? N : size(ics, 1 ) # number of actual ICs
144154
145155 feature_array = Vector{Vector{Float64}}(undef, N)
146156 if show_progress
@@ -175,7 +185,8 @@ function classify_features(features, mapper::AttractorsViaFeaturizing)
175185 if ! isnothing(mapper. attractors_ic)
176186 classify_features_distances(features, mapper)
177187 else
178- classify_features_clustering(features, mapper. min_neighbors, mapper. clust_method_norm)
188+ classify_features_clustering(features, mapper. min_neighbors, mapper. clust_method_norm,
189+ mapper. rescale_features, mapper. optimal_radius_method)
179190 end
180191end
181192
@@ -198,13 +209,25 @@ function classify_features_distances(features, mapper)
198209 return class_labels, class_errors
199210end
200211
212+ """
213+ Does "min-max" rescaling of vector `vec`: rescales it such that its values span `[0,1]`.
214+ """
215+ function rescale(vec:: Vector{T} ) where T
216+ vec .- = minimum(vec)
217+ max = maximum(vec)
218+ if max == 0 return zeros(T, length(vec)) end
219+ vec ./= maximum(vec)
220+ end
221+
201222# Unsupervised method: clustering in feature space
202- function classify_features_clustering(features, min_neighbors, metric)
203- ϵ_optimal = optimal_radius_dbscan(features, min_neighbors, metric)
223+ function classify_features_clustering(features, min_neighbors, metric, rescale_features,
224+ optimal_radius_method)
225+ if rescale_features features = mapslices(rescale, features, dims= 2 ) end
226+ ϵ_optimal = optimal_radius_dbscan(features, min_neighbors, metric, optimal_radius_method)
204227 # Now recalculate the final clustering with the optimal ϵ
205- clusters = Clustering . dbscan(features, ϵ_optimal; min_neighbors)
228+ clusters = dbscan(features, ϵ_optimal; min_neighbors)
206229 clusters, sizes = sort_clusters_calc_size(clusters)
207- class_labels = cluster_props (clusters, features; include_boundary= false )
230+ class_labels = cluster_assignment (clusters, features; include_boundary= false )
208231 # number of real clusters (size above minimum points);
209232 # this is also the number of "templates"
210233 k = length(sizes[sizes .> min_neighbors])
@@ -220,61 +243,3 @@ function classify_features_clustering(features, min_neighbors, metric)
220243 return class_labels, class_errors
221244end
222245
223- # ####################################################################################
224- # Utilities
225- # ####################################################################################
226- """
227- Util function for `classify_features`. It returns the size of all the DBSCAN clusters and the
228- assignment vector, in which the i-th component is the cluster index of the i-th feature
229- """
230- function cluster_props(clusters, data; include_boundary= true )
231- assign = zeros(Int, size(data)[2 ])
232- for (idx, cluster) in enumerate(clusters)
233- assign[cluster. core_indices] .= idx
234- if cluster. boundary_indices != []
235- if include_boundary
236- assign[cluster. boundary_indices] .= idx
237- else
238- assign[cluster. boundary_indices] .= - 1
239- end
240- end
241- end
242- return assign
243- end
244-
245- """
246- Util function for `classify_features`. Calculates the clusters' (DbscanCluster) size
247- and sorts them in decreasing order according to the size.
248- """
249- function sort_clusters_calc_size(clusters)
250- sizes = [cluster. size for cluster in clusters]
251- idxsort = sortperm(sizes; rev = true )
252- return clusters[idxsort], sizes[idxsort]
253- end
254-
255- """
256- Find the optimal radius ε of a point neighborhood for use in DBSCAN, in the unsupervised
257- `classify_features`. It does so by finding the `ε` which maximizes the minimum silhouette
258- of the cluster.
259- """
260- function optimal_radius_dbscan(features, min_neighbors, metric)
261- feat_ranges = maximum(features, dims= 2 )[:,1 ] .- minimum(features, dims= 2 )[:,1 ];
262- ϵ_grid = range(minimum(feat_ranges)/ 200 , minimum(feat_ranges), length= 200 )
263- s_grid = zeros(size(ϵ_grid)) # min silhouette values (which we want to maximize)
264-
265- # vary ϵ to find the best one (which will maximize the minimum sillhoute)
266- for i= 1 : length(ϵ_grid)
267- clusters = dbscan(features, ϵ_grid[i]; min_neighbors)
268- dists = pairwise(metric, features)
269- class_labels = cluster_props(clusters, features)
270- if length(clusters) ≠ 1 # silhouette undefined if only one cluster.
271- sils = silhouettes(class_labels, dists) # values == 0 are due to boundary points
272- s_grid[i] = minimum(sils[sils .!= 0.0 ]) # minimum silhouette value of core points
273- else
274- s_grid[i] = - 2 ; # this would effecively ignore the single-cluster solution
275- end
276- end
277-
278- max, idx = findmax(s_grid)
279- ϵ_optimal = ϵ_grid[idx]
280- end
0 commit comments