Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "UnsupervisedClustering"
uuid = "c6b3b713-2a2b-4e07-a35c-d57ac4f46186"
authors = ["raphasampaio", "joaquimg", "mvpoggi", "vidalt"]
version = "0.4.6"
version = "0.4.7"

[deps]
Distances = "b4f34e82-e78d-54a5-968a-f98e89d6e8f7"
Expand Down
6 changes: 3 additions & 3 deletions docs/docs.bat
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
@echo off

SET BASEPATH=%~dp0
SET BASE_PATH=%~dp0

CALL julia +1.12 --project=%BASEPATH% -e "using Pkg; Pkg.develop(PackageSpec(path=dirname(pwd()))); Pkg.instantiate()"
CALL julia +1.12 --project=%BASEPATH% %BASEPATH%\make.jl
CALL julia +1.12 --project=%BASE_PATH% -e "using Pkg; Pkg.develop(PackageSpec(path=dirname(pwd()))); Pkg.instantiate()"
CALL julia +1.12 --project=%BASE_PATH% %BASE_PATH%\make.jl
4 changes: 2 additions & 2 deletions docs/docs.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/bin/bash

BASEPATH="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
BASE_PATH="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"

julia +1.12 --project -e "using Pkg; Pkg.develop(PackageSpec(path=dirname(pwd()))); Pkg.instantiate()"
julia +1.12 --project $BASEPATH/make.jl
julia +1.12 --project $BASE_PATH/make.jl
4 changes: 2 additions & 2 deletions format/format.bat
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
@echo off

SET BASEPATH=%~dp0
SET BASE_PATH=%~dp0

CALL julia +1.12 --project=%BASEPATH% %BASEPATH%\format.jl
CALL julia +1.12 --project=%BASE_PATH% %BASE_PATH%\format.jl
4 changes: 2 additions & 2 deletions format/format.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/bin/bash

BASEPATH="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
BASE_PATH="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"

julia +1.12 --project=$BASEPATH $BASEPATH/format.jl
julia +1.12 --project=$BASE_PATH $BASE_PATH/format.jl
4 changes: 2 additions & 2 deletions revise/revise.bat
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
@echo off

SET BASEPATH=%~dp0
SET BASE_PATH=%~dp0

CALL julia +1.12 --project=%BASEPATH% --interactive --load=%BASEPATH%\revise.jl
CALL julia +1.12 --project=%BASE_PATH% --interactive --load=%BASE_PATH%\revise.jl
4 changes: 2 additions & 2 deletions revise/revise.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/bin/bash

BASEPATH="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
BASE_PATH="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"

julia +1.12 --project=$BASEPATH --interactive $BASEPATH/revise.jl
julia +1.12 --project=$BASE_PATH --interactive $BASE_PATH/revise.jl
12 changes: 11 additions & 1 deletion src/metaheuristic/random_swap.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
verbose::Bool = DEFAULT_VERBOSE
max_iterations::Int = 200
max_iterations_without_improvement::Int = 150
discard_empty_clusters::Bool = false
) where {LS <: AbstractAlgorithm}

RandomSwap is a meta-heuristic approach used for clustering problems. It follows an iterative process that combines local optimization with perturbation to explore the search space effectively. A local optimization algorithm is applied at each iteration to converge toward a local optimum. Then, a perturbation operator generates a new starting point and continues the search.
Expand All @@ -16,6 +17,7 @@ RandomSwap is a meta-heuristic approach used for clustering problems. It follows
- `verbose`: controls whether the algorithm should display additional information during execution.
- `max_iterations`: represents the maximum number of iterations the algorithm will perform before stopping.
- `max_iterations_without_improvement`: represents the maximum number of iterations allowed without improving the best solution.
- `discard_empty_clusters`: when `true`, candidate solutions whose local-search result contains at least one empty cluster are rejected and never replace the current best solution.

# References
* Fränti, Pasi.
Expand All @@ -27,6 +29,7 @@ Base.@kwdef struct RandomSwap{LS <: AbstractAlgorithm} <: AbstractAlgorithm
verbose::Bool = DEFAULT_VERBOSE
max_iterations::Int = 200
max_iterations_without_improvement::Int = 150
discard_empty_clusters::Bool = false
end

@doc """
Expand Down Expand Up @@ -83,7 +86,9 @@ function fit(meta::RandomSwap{LS}, data::AbstractMatrix{<:Real}, k::Integer) whe
print_result(result)
end

if isbetter(result, best_result)
discard = meta.discard_empty_clusters && has_empty_clusters(result)

if !discard && isbetter(result, best_result)
best_result = result
iterations_without_improvement = 0

Expand All @@ -92,6 +97,11 @@ function fit(meta::RandomSwap{LS}, data::AbstractMatrix{<:Real}, k::Integer) whe
end
else
iterations_without_improvement += 1

if meta.verbose && discard
print_string("(discarded: empty cluster)")
end

if iterations_without_improvement > meta.max_iterations_without_improvement
break
end
Expand Down
4 changes: 4 additions & 0 deletions src/result/counts.jl
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
function counts(result::AbstractResult)
return StatsBase.counts(result.assignments, result.k)
end

function has_empty_clusters(result::AbstractResult)
return any(==(0), counts(result))
end
6 changes: 3 additions & 3 deletions test/test.bat
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
@echo off

SET BASEPATH=%~dp0
SET BASE_PATH=%~dp0

IF "%~1"=="" (
CALL julia +1.12 --project=%BASEPATH%\.. -e "import Pkg; Pkg.test()"
CALL julia +1.12 --project=%BASE_PATH%\.. -e "import Pkg; Pkg.test()"
) ELSE (
CALL julia +1.12 --project=%BASEPATH%\.. %BASEPATH%\runtests.jl %1
CALL julia +1.12 --project=%BASE_PATH%\.. %BASE_PATH%\runtests.jl %1
)
4 changes: 2 additions & 2 deletions test/test.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/bin/bash

BASEPATH="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
BASE_PATH="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"

julia +1.12 --project=$BASEPATH/.. -e "import Pkg; Pkg.test()"
julia +1.12 --project=$BASE_PATH/.. -e "import Pkg; Pkg.test()"
60 changes: 60 additions & 0 deletions test/test_discard_empty_clusters.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
module TestDiscardEmptyClusters

using Random
using Test
using UnsupervisedClustering

Base.@kwdef struct EmptyingLocalSearch <: UnsupervisedClustering.AbstractAlgorithm
rng::AbstractRNG = MersenneTwister(0)
end

function UnsupervisedClustering.fit(::EmptyingLocalSearch, data::AbstractMatrix{<:Real}, k::Integer)
n, d = size(data)
result = UnsupervisedClustering.KmeansResult(zeros(Int, n), zeros(d, k))
for i in 1:n
result.assignments[i] = mod(i - 1, k) + 1
end
result.objective = 100.0
return result
end

function UnsupervisedClustering.fit!(
::EmptyingLocalSearch,
::AbstractMatrix{<:Real},
result::UnsupervisedClustering.KmeansResult,
)
fill!(result.assignments, 1)
result.objective = 1.0
return nothing
end

@testset "RandomSwap - discard_empty_clusters" begin
data = rand(MersenneTwister(1), 50, 2)
k = 4

no_discard = RandomSwap(
local_search = EmptyingLocalSearch(rng = MersenneTwister(2)),
max_iterations = 5,
max_iterations_without_improvement = 100,
discard_empty_clusters = false,
verbose = true,
)
result_no_discard = UnsupervisedClustering.fit(no_discard, data, k)
@test count(==(0), UnsupervisedClustering.counts(result_no_discard)) == k - 1
@test result_no_discard.objective == 1.0

discard = RandomSwap(
local_search = EmptyingLocalSearch(rng = MersenneTwister(2)),
max_iterations = 5,
max_iterations_without_improvement = 100,
discard_empty_clusters = true,
verbose = true,
)
result_discard = UnsupervisedClustering.fit(discard, data, k)
@test count(==(0), UnsupervisedClustering.counts(result_discard)) == 0
@test result_discard.objective == 100.0

return nothing
end

end
Loading