Skip to content

Commit d1bac6b

Browse files
GrantHechtLaSalledlasalle37
authored
Add a Correlation-Based Covariance Transformation Option to the DE Crossover Operation Transformations (UB-SSDC-Lab#59)
Implements UncorrelatedCovarianceTransformation as a new DE crossover operation transformation, which constructs the transformation based on the N best uncorrelated candidates. --------- Co-authored-by: LaSalle <dmlasall@buffalo.edu> Co-authored-by: Dylan <dlasalle37@gmail.com>
1 parent de794a3 commit d1bac6b

7 files changed

Lines changed: 316 additions & 11 deletions

File tree

docs/src/lib/public.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,8 @@ SelfBinomialCrossoverParameters
132132
SelfBinomialCrossoverParameters(; dist=::Any, transform=::Any)
133133
CovarianceTransformation
134134
CovarianceTransformation(::Any,::Any,::Any)
135+
UncorrelatedCovarianceTransformation
136+
UncorrelatedCovarianceTransformation(::Any,::Any,::Any)
135137
```
136138

137139
### Monotonic Basin Hopping

scripts/de_testing.jl

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ function rastrigin(x; A=10)
4242
end
4343

4444
# Setup Problem
45-
N = 2
45+
N = 7
4646
ss = ContinuousRectangularSearchSpace([-100.0 for i in 1:N], [100.0 for i in 1:N])
4747
prob = GlobalOptimization.OptimizationProblem(rastrigin, ss)
4848

@@ -54,7 +54,9 @@ mutation_strategy = SelfMutationParameters(
5454
)
5555
crossover_strategy = SelfBinomialCrossoverParameters(;
5656
dist=Uniform(0.0, 1.0),
57-
#transform = GlobalOptimization.CovarianceTransformation(0.1, 0.5, N),
57+
#transform = GlobalOptimization.NoTransformation()
58+
transform = GlobalOptimization.UncorrelatedCovarianceTransformation(0.5, 0.8, N; ps = 1.0),
59+
#transform = GlobalOptimization.CovarianceTransformation(0.1, 0.5, N)
5860
)
5961

6062
de = DE(
@@ -65,10 +67,10 @@ de = DE(
6567
max_stall_iterations=100,
6668
mutation_params=mutation_strategy,
6769
crossover_params=crossover_strategy,
68-
show_trace=Val(true),
70+
show_trace=Val(false),
6971
)
7072

71-
res = optimize!(de)
73+
res = @btime optimize!(de)
7274
#iters_per_solve = map(i->optimize!(deepcopy(de)).iters, 1:100);
7375

7476
# bb_res = bboptimize(

src/DE/crossover.jl

Lines changed: 174 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,14 @@ Subtypes of this abstract type should define the following methods:
3636
"""
3737
abstract type AbstractCrossoverTransformation end
3838

39+
"""
40+
RotationMatrixBasedCrossoverTransformation
41+
42+
An abstract type representing a transformation applied to a candidate prior to applying
43+
the crossover operator which uses an orthogonal rotation matrix.
44+
"""
45+
abstract type RotationMatrixBasedCrossoverTransformation <: AbstractCrossoverTransformation end
46+
3947
"""
4048
NoTransformation
4149
@@ -62,7 +70,7 @@ DOI: [10.1016/j.asoc.2014.01.038](https://doi.org/10.1016/j.asoc.2014.01.038).
6270
- `ct::Vector{Float64}`: The transformed candidate.
6371
- `mt::Vector{Float64}`: The transformed mutant.
6472
"""
65-
struct CovarianceTransformation <: AbstractCrossoverTransformation
73+
struct CovarianceTransformation <: RotationMatrixBasedCrossoverTransformation
6674
ps::Float64
6775
pb::Float64
6876
B::Matrix{Float64}
@@ -113,15 +121,110 @@ struct CovarianceTransformation <: AbstractCrossoverTransformation
113121
end
114122
end
115123

124+
"""
125+
UncorrelatedCovarianceTransformation{T<:AbstractCrossoverTransformation}
126+
127+
A transformation for performing crossover in the eigen-space of the covariance matrix of the
128+
best candidates in the population which are also not too closely correlated.
129+
130+
This is an implementation of the method proposed by Wang and Li in "Differential Evolution
131+
Based on Covariance Matrix Learning and Bimodal Distribution Parameter Setting, " 2014,
132+
DOI: [10.1016/j.asoc.2014.01.038](https://doi.org/10.1016/j.asoc.2014.01.038).
133+
134+
Correlation alteration based on "Covariance Matrix Learning Differential Evolution Algorithm Based on Correlation"
135+
DOI: https://doi.org/10.4236/ijis.2021.111002
136+
137+
# Fields
138+
- `ps::Float64`: The proportion of candidates to consider in the covariance matrix. That is,
139+
for a population size of `N` with `M` candidates remaining after the correlation check, the covariance matrix is calculated using the
140+
`clamp(ceil(ps * M), 2, M)` best candidates.
141+
- `pb::Float64`: The probability of applying the transformation.
142+
- `a::Float64`: The correlation threshold for which two candidates are considered 'too close' to both be used in the covariance matrix construction.
143+
- `B::Matrix{Float64}`: The real part of the eigenvectors of the covariance matrix.
144+
- `ct::Vector{Float64}`: The transformed candidate.
145+
- `mt::Vector{Float64}`: The transformed mutant.
146+
- `idxs::Vector{UInt16}`: A vector of indexes for the population
147+
- `cidxs::Vector{UInt16}`: A vector of unique `correlated` indexes for the population set for removal
148+
"""
149+
struct UncorrelatedCovarianceTransformation <: RotationMatrixBasedCrossoverTransformation
150+
ps::Float64
151+
pb::Float64
152+
a::Float64
153+
B::Matrix{Float64}
154+
155+
# Preallocate storage for transformed candidate and mutant
156+
ct::Vector{Float64}
157+
mt::Vector{Float64}
158+
159+
# Preallocate storage for calculating transformation
160+
idxs::Vector{UInt16}
161+
162+
cidxs::Vector{UInt16}
163+
164+
@doc """
165+
UncorrelatedCovarianceTransformation{T<:AbstractCrossoverTransformation}
166+
167+
A transformation for performing crossover in the eigen-space of the covariance matrix of the
168+
best candidates in the population which are also not too closely correlated.
169+
170+
This is an implementation of the method proposed by Yuan and Feng in "Covariance Matrix
171+
Learning Differential Evolution Algorithm Based on Correlation"
172+
DOI: https://doi.org/10.4236/ijis.2021.111002
173+
174+
# Arguments
175+
- `pb::Float64`: The probability of applying the transformation.
176+
- `a::Float64`: The correlation threshold for two candidates being 'too close'.
177+
- `num_dims::Int`: The number of dimensions in the search space.
178+
179+
# Keyword Arguments:
180+
- `ps::Float64`: The proportion of candidates to consider in the covariance matrix.
181+
Defaults to 1.0 (i.e., all uncorrelated candidates are considered)
182+
183+
# Returns
184+
- `UncorrelatedCovarianceTransformation`: A `UncorrelatedCovarianceTransformation` object with the specified
185+
parameters.
186+
187+
# Examples
188+
```julia-repl
189+
julia> using GlobalOptimization
190+
julia> transformation = UncorrelatedCovarianceTransformation(0.5, .95, 10; ps = 1.0)
191+
UncorrelatedCovarianceTransformation(1.0, 0.5, 0.95, [1.0630691323565e-311 1.0630691151907e-311 … 1.063069230316e-311 1.063069119645e-311; 1.0630691158705e-311 1.063069115333e-311 … 1.063069172704e-311 1.0630692904614e-311; … ; 1.063069115428e-311 1.063069114246e-311 … 1.063069124886e-311 1.0630694190924e-311; 1.0630691153804e-311 1.0630691141986e-311 … 1.0630691348624e-311 1.063069428614e-311], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], UInt16[], UInt16[])
192+
```
193+
"""
194+
function UncorrelatedCovarianceTransformation(pb, a, num_dims; ps = 1.0)
195+
if ps <= 0.0 || ps > 1.0
196+
throw(ArgumentError("ps must be in the range (0, 1]."))
197+
end
198+
if pb <= 0.0 || pb > 1.0
199+
throw(ArgumentError("pb must be in the range (0, 1]."))
200+
end
201+
if a <= 0.0 || a > 1.0
202+
throw(ArgumentError("a must be in the range (0, 1]."))
203+
end
204+
B = Matrix{Float64}(undef, num_dims, num_dims)
205+
return new(ps, pb, a, B, zeros(num_dims), zeros(num_dims), Vector{UInt16}(undef, 0), Vector{UInt16}(undef, 0))
206+
end
207+
208+
end
209+
116210
initialize!(transformation::NoTransformation, population_size) = nothing
117-
function initialize!(transformation::CovarianceTransformation, population_size)
211+
function initialize!(transformation::RotationMatrixBasedCrossoverTransformation, population_size)
212+
resize!(transformation.idxs, population_size)
213+
transformation.idxs .= 1:population_size
214+
return nothing
215+
end
216+
217+
function initialize!(transformation::UncorrelatedCovarianceTransformation, population_size)
118218
resize!(transformation.idxs, population_size)
219+
resize!(transformation.cidxs, population_size)
119220
transformation.idxs .= 1:population_size
221+
empty!(transformation.cidxs)
120222
return nothing
121223
end
122224

123225
update_transformation!(transformation::NoTransformation, population) = nothing
124226
function update_transformation!(transformation::CovarianceTransformation, population)
227+
125228
# Get number of candidates to consider in the covariance matrix
126229
n = clamp(ceil(Int, transformation.ps * length(population)), 2, length(population))
127230

@@ -138,9 +241,76 @@ function update_transformation!(transformation::CovarianceTransformation, popula
138241

139242
return nothing
140243
end
244+
function update_transformation!(transformation::UncorrelatedCovarianceTransformation, population)
245+
# get correlation for each pair of vectors in population
246+
cor_mat = cor(stack(population.current_generation.candidates))
247+
248+
# store population_size
249+
pop_size = length(population)
250+
251+
if all_correlated(cor_mat, transformation.a)
252+
# If all correlated, set transform to identity and return
253+
#let's just set the transformation to identity as it doesn't
254+
# really make sense to compute the covariance matrix transformation given this
255+
# transformation is for specifically avoiding using correlated candidates when
256+
# computing the transformation.
257+
fill_identity!(transformation.B)
258+
return nothing
259+
else
260+
# lower triangular so that we only have unique pairs (excluding diagonal, since all elements are 1.0)
261+
tril!(cor_mat, -1)
262+
263+
# find points where two candidates are strongly correlated
264+
265+
@inbounds for j in axes(cor_mat,2)
266+
for i in j+1:size(cor_mat,1)
267+
abs_x = abs(cor_mat[i, j])
268+
if abs_x >= transformation.a
269+
#Base.push!(transformation.pairs, SVector(i, j))
270+
if !in(i, transformation.cidxs)
271+
Base.push!(transformation.cidxs, i)
272+
end
273+
end
274+
end
275+
end
276+
277+
278+
# if we're removing all but one idx, set transformation to identity and return
279+
if length(transformation.cidxs) >= length(transformation.idxs) - 1
280+
fill_identity!(transformation.B)
281+
return nothing
282+
end
283+
284+
# sort transformation.idxs in order of best candidate to worst candidate
285+
sortperm!(transformation.idxs, population.current_generation.candidates_fitness)
286+
287+
# remove candidates (note setdiff preserves order)
288+
setdiff!(transformation.idxs, transformation.cidxs)
289+
290+
# get number of candidates to use for covariance based on remaining candidates
291+
n = clamp(ceil(Int, transformation.ps * length(transformation.idxs)), 2, length(transformation.idxs))
292+
293+
# Get indices of n best remaining candidates
294+
idxs = view(transformation.idxs, 1:n)
295+
296+
# Calculate the covariance matrix for n best candidates
297+
C = cov(view(population.current_generation.candidates, idxs))
298+
299+
# Compute eigen decomposition
300+
E = eigen!(C)
301+
transformation.B .= real.(E.vectors)
302+
303+
# Reset preallocated storage
304+
empty!(transformation.cidxs)
305+
resize!(transformation.idxs, pop_size)
306+
transformation.idxs .= 1:pop_size
307+
end
308+
309+
return nothing
310+
end
141311

142312
to_transformed(transformation::NoTransformation, c, m) = c, m, false
143-
function to_transformed(transformation::CovarianceTransformation, c, m)
313+
function to_transformed(transformation::RotationMatrixBasedCrossoverTransformation, c, m)
144314
if rand() < transformation.pb
145315
mul!(transformation.ct, transpose(transformation.B), c)
146316
mul!(transformation.mt, transpose(transformation.B), m)
@@ -151,7 +321,7 @@ function to_transformed(transformation::CovarianceTransformation, c, m)
151321
end
152322

153323
from_transformed!(transformation::NoTransformation, mt, m) = nothing
154-
function from_transformed!(transformation::CovarianceTransformation, mt, m)
324+
function from_transformed!(transformation::RotationMatrixBasedCrossoverTransformation, mt, m)
155325
mul!(m, transformation.B, mt)
156326
return nothing
157327
end

src/GlobalOptimization.jl

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ using ADTypes: AbstractADType
44
using ChunkSplitters: chunks, ChunkSplitters, RoundRobin
55
using Distributions: Cauchy, Laplace, MixtureModel
66
using LatinHypercubeSampling: scaleLHC, LHCoptim
7-
using LinearAlgebra: dot, eigen!, mul!
7+
using LinearAlgebra: dot, eigen!, mul!, tril!
88
using LineSearches: HagerZhang, InitialStatic
99
using Polyester: @batch
1010
using Printf: format, Format
1111
using StaticArrays: SA, SVector
12-
using Statistics: cov, mean, median!, std
12+
using Statistics: cov, mean, median!, std, cor
1313
using Random: rand, rand!, shuffle!, AbstractRNG, GLOBAL_RNG
1414
using UnPack: @unpack
1515

@@ -18,6 +18,7 @@ using NonlinearSolve: NonlinearSolve
1818
using Optim: Optim
1919

2020
# Base
21+
include("utils.jl")
2122
include("enums.jl")
2223
include("tracing.jl")
2324
include("Options.jl")
@@ -64,7 +65,7 @@ export Rand1, Rand2, Best1, Best2, CurrentToBest1, CurrentToBest2
6465
export CurrentToRand1, CurrentToRand2, RandToBest1, RandToBest2, Unified
6566
export MutationParameters, SelfMutationParameters
6667
export SelfBinomialCrossoverParameters, BinomialCrossoverParameters
67-
export CovarianceTransformation
68+
export CovarianceTransformation, UncorrelatedCovarianceTransformation
6869

6970
export SingleHopper, MCH
7071
export MBHStaticDistribution, MBHAdaptiveDistribution

src/utils.jl

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
2+
# Utility functions for GlobalOptimization.jl
3+
4+
"""
5+
all_correlated(cor, tol)
6+
7+
Checks if the absolute value of the lower-triangular part of the correlation matrix `cor`
8+
are above the correlation tolerance `tol` and returns `true` if so, otherwise, returns
9+
`false`.
10+
11+
# Arguments:
12+
- `cor::AbstractMatrix`: The correlation matrix
13+
- `tol::AbstractFloat`: The correlation tolerance. Elements of `cor` with an absolute value
14+
greater than `tol` are assumed to be correlated.
15+
16+
# Returns:
17+
- `Bool`: `true` if all elements are correlated and `false` otherwise
18+
"""
19+
function all_correlated(cor, tol)
20+
@inbounds for col in first(axes(cor, 2), size(cor, 2) - 1)
21+
for row in last(axes(cor, 1), size(cor, 1) - col)
22+
abs_x = abs(cor[row, col])
23+
if abs_x < tol
24+
return false
25+
end
26+
end
27+
end
28+
return true
29+
end
30+
31+
"""
32+
fill_identity!(mat)
33+
34+
Fills the `mat` in-place with the identity matrix.
35+
"""
36+
function fill_identity!(mat)
37+
@inbounds for j in axes(mat, 2)
38+
for i in axes(mat, 1)
39+
mat[i,j] = ifelse(i == j, 1.0, 0.0)
40+
end
41+
end
42+
return nothing
43+
end

test/base.jl

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -746,3 +746,16 @@ end
746746
end
747747
@test all(threads_used)
748748
end
749+
750+
@testset showtiming = true "Utils" begin
751+
# Test general utilities from utils.jl
752+
753+
mat = ones(3,3)
754+
@test GlobalOptimization.all_correlated(mat, 0.8) == true
755+
mat[3,1] = 0.1
756+
@test GlobalOptimization.all_correlated(mat, 0.8) == false
757+
758+
GlobalOptimization.fill_identity!(mat)
759+
@test isapprox(mat, [1. 0 0; 0 1. 0; 0 0 1.], atol=1e-8)
760+
761+
end

0 commit comments

Comments
 (0)