Skip to content

Commit 9658c50

Browse files
KalelRDatseris
andauthored
Improvement/fix for optimal_radius_dbscan (WIP) (#254)
* Add changes improving optimal_radius_dbscan; should be no more errors now, and improved the estimation of attractors, but still needs further tests and improvements * remove ChaosTools. from code * Fix comments * Removed bug warning and added min-max rescaling feature to unsupervised clustering method * added knee/elbow method for estiamting optimal radius in dbscan; added clustering/utils file with the method, the silhouette method, and some other utils for dbscan. Directory should also include the source code from Clustering.jl, but dbscan errors when I do that. I'll try to fix later, and use Clustering.jl directly for now * implemented support for knee method; moved utilities and optimal radius to clustering/utils * add utils.jl * Fix tests for unsupervised clustering. Two problems were occuring, and were related to the labeling of the attractors: (i) algorithm might return labels [1,2] when correct is [-1, 1] (this is because it identifies the Henon attractor at infinity as an attractor); (ii) algorithm might identify some outlier points (eg some of the points in the FP attractor in Lorenz84), in which case it puts them in -1. Then the labels are [-1, 1,2,3] and not [1,2,3]. In both cases, the values were already within the tolerated error, so I just ignored the labels and tested the values. Also had to increase to , as it improved the clustering for Lorenz84. * Use suggested values for duffing * compare number of attractors, remove comparisons of keys themselves, replace featurizer's estimationi of period by the minimum of A[:,1] * remove commented dependencies that should eventually be included in src/basins/clustering * Increment minor version and update changelog * quick fix changelog Co-authored-by: Datseris <[email protected]>
1 parent 17cf231 commit 9658c50

File tree

6 files changed

+188
-130
lines changed

6 files changed

+188
-130
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# 2.9
2+
* Improved the `AttractorsViaFeaturizing` algorithm by improving the method for finding the optimal radius used in the clustering. This consisted in (i) maximizing the average silhouette values, instead of minimum (slight improvement), (ii) min-max rescaling the features for the clustering (big improvement); (iii) adding an alternative method ,called elbow method, that is faster but worse at clustering.
3+
* Changed `attractor_mapping_tests.jl` to deal better with the Featurizing method.
4+
15
# 2.8
26
* Brand new `AttractorMapper` infrastructure. It is a generic framework for mapping initial conditions to attractors and hence calculating basins of attraction and related quantities. Existing originally disparate functionality has been brought together under this framework.
37
* The old `basins_of_attraction` function has been completely deprecated in favor of using the version `basins_of_attraction(mapper::AttractorMapper, grid)`, which utilizes the new `AttractorMapper` interface and is more intuitive and generalizable.

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "ChaosTools"
22
uuid = "608a59af-f2a3-5ad4-90b4-758bdf3122a7"
33
repo = "https://github.com/JuliaDynamics/ChaosTools.jl.git"
4-
version = "2.8.2"
4+
version = "2.9"
55

66
[deps]
77
Clustering = "aaaa29a8-35af-508c-8bc3-b662a17a0fe5"

src/ChaosTools.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ include("basins/basins_utilities.jl")
2828
include("basins/fractality_of_basins.jl")
2929
include("basins/tipping.jl")
3030
include("basins/sampler.jl")
31+
include("basins/clustering/utils.jl")
3132

3233
include("dimensions/linear_regions.jl")
3334
include("dimensions/generalized_dim.jl")

src/basins/attractor_mapping_featurizing.jl

Lines changed: 82 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -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
2325
end
2426
DynamicalSystemsBase.get_rule_for_print(m::AttractorsViaFeaturizing) =
2527
get_rule_for_print(m.ds)
@@ -38,86 +40,94 @@ end
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
88100
In 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
"""
106117
function 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
)
122132
end
123133

@@ -140,7 +150,7 @@ end
140150
function 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
180191
end
181192

@@ -198,13 +209,25 @@ function classify_features_distances(features, mapper)
198209
return class_labels, class_errors
199210
end
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
221244
end
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

Comments
 (0)