/*========================================================================= * * Copyright NumFOCUS * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0.txt * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * *=========================================================================*/ #include #include "itkParticleSwarmOptimizerBase.h" namespace itk { ParticleSwarmOptimizerBase::ParticleSwarmOptimizerBase() { this->m_PrintSwarm = false; this->m_InitializeNormalDistribution = false; this->m_NumberOfParticles = 35; this->m_MaximalNumberOfIterations = 200; this->m_NumberOfGenerationsWithMinimalImprovement = 1; this->m_ParametersConvergenceTolerance.Fill(1e-8); this->m_PercentageParticlesConverged = 0.6; this->m_FunctionConvergenceTolerance = 1e-4; this->m_Seed = 0; this->m_UseSeed = false; } ParticleSwarmOptimizerBase::~ParticleSwarmOptimizerBase() = default; void ParticleSwarmOptimizerBase::SetNumberOfParticles(unsigned int n) { if (!this->m_Particles.empty() && n != this->m_Particles.size()) { itkExceptionMacro("swarm already set with different size, clear swarm and then set"); } if (this->m_NumberOfParticles != n) { this->m_NumberOfParticles = n; Modified(); } } void ParticleSwarmOptimizerBase::SetInitialSwarm(const SwarmType & initialSwarm) { // Always clear the m_Particles. this->m_Particles.clear(); if (!initialSwarm.empty()) { const SwarmType::const_iterator initialSwarm_END = initialSwarm.end(); const unsigned int n = initialSwarm[0].m_CurrentParameters.GetSize(); // check that the dimensions of the swarm data are consistent for (auto it = initialSwarm.begin(); it != initialSwarm_END; ++it) { if (it->m_CurrentParameters.GetSize() != n || it->m_CurrentVelocity.GetSize() != n || it->m_BestParameters.GetSize() != n) { itkExceptionMacro("inconsistent dimensions in swarm data"); } } this->m_Particles.insert(m_Particles.begin(), initialSwarm.begin(), initialSwarm_END); this->m_NumberOfParticles = static_cast(m_Particles.size()); } Modified(); } void ParticleSwarmOptimizerBase::ClearSwarm() { if (!this->m_Particles.empty()) { this->m_Particles.clear(); Modified(); } } void ParticleSwarmOptimizerBase::SetParameterBounds(ParameterBoundsType & bounds) { this->m_ParameterBounds.clear(); this->m_ParameterBounds.insert(m_ParameterBounds.begin(), bounds.begin(), bounds.end()); Modified(); } void ParticleSwarmOptimizerBase::SetParameterBounds(std::pair & bounds, unsigned int n) { this->m_ParameterBounds.clear(); this->m_ParameterBounds.insert(m_ParameterBounds.begin(), n, bounds); Modified(); } ParticleSwarmOptimizerBase::ParameterBoundsType ParticleSwarmOptimizerBase::GetParameterBounds() const { return this->m_ParameterBounds; } void ParticleSwarmOptimizerBase::SetParametersConvergenceTolerance(ParametersType::ValueType convergenceTolerance, unsigned int sz) { this->m_ParametersConvergenceTolerance.SetSize(sz); this->m_ParametersConvergenceTolerance.Fill(convergenceTolerance); } ParticleSwarmOptimizerBase::CostFunctionType::MeasureType ParticleSwarmOptimizerBase::GetValue() const { return this->m_FunctionBestValue; } const std::string ParticleSwarmOptimizerBase::GetStopConditionDescription() const { return this->m_StopConditionDescription.str(); } void ParticleSwarmOptimizerBase::PrintSelf(std::ostream & os, Indent indent) const { Superclass::PrintSelf(os, indent); os << indent << "Create swarm using [normal, uniform] distribution: "; os << '[' << this->m_InitializeNormalDistribution << ", "; os << !this->m_InitializeNormalDistribution << "]\n"; os << indent << "Number of particles in swarm: " << this->m_NumberOfParticles << '\n'; os << indent << "Maximal number of iterations: " << this->m_MaximalNumberOfIterations << '\n'; os << indent << "Number of generations with minimal improvement: "; os << this->m_NumberOfGenerationsWithMinimalImprovement << '\n'; ParameterBoundsType::const_iterator it, end; end = this->m_ParameterBounds.end(); os << indent << "Parameter bounds: ["; for (it = this->m_ParameterBounds.begin(); it != end; ++it) { os << " [" << it->first << ", " << it->second << ']'; } os << " ]\n"; os << indent << "Parameters' convergence tolerance: " << this->m_ParametersConvergenceTolerance; os << '\n'; os << indent << "Function convergence tolerance: " << this->m_FunctionConvergenceTolerance << std::endl; os << indent << "UseSeed: " << m_UseSeed << std::endl; os << indent << "Seed: " << m_Seed << std::endl; os << '\n'; // printing the swarm, usually should be avoided (too much information) if (this->m_PrintSwarm && !m_Particles.empty()) { os << indent << "swarm data [current_parameters current_velocity current_value "; os << "best_parameters best_value]:\n"; PrintSwarm(os, indent); } } void ParticleSwarmOptimizerBase::PrintSwarm(std::ostream & os, Indent indent) const { std::vector::const_iterator it, end; end = this->m_Particles.end(); os << indent << "[\n"; for (it = this->m_Particles.begin(); it != end; ++it) { const ParticleData & p = *it; os << indent; PrintParamtersType(p.m_CurrentParameters, os); os << ' '; PrintParamtersType(p.m_CurrentVelocity, os); os << ' ' << p.m_CurrentValue << ' '; PrintParamtersType(p.m_BestParameters, os); os << ' ' << p.m_BestValue << '\n'; } os << indent << "]\n"; } void ParticleSwarmOptimizerBase::PrintParamtersType(const ParametersType & x, std::ostream & os) const { unsigned int sz = x.size(); for (unsigned int i = 0; i < sz; ++i) { os << x[i] << ' '; } } void ParticleSwarmOptimizerBase::StartOptimization() { unsigned int j, k, n, index, prevIndex; bool converged = false; unsigned int bestValueMemorySize = this->m_NumberOfGenerationsWithMinimalImprovement + 1; auto percentileIndex = static_cast(this->m_PercentageParticlesConverged * (this->m_NumberOfParticles - 1) + 0.5); ValidateSettings(); // initialize particle locations, velocities, etc. Initialize(); InvokeEvent(StartEvent()); // run the simulation n = static_cast((GetCostFunction())->GetNumberOfParameters()); for (this->m_IterationIndex = 1; m_IterationIndex < m_MaximalNumberOfIterations && !converged; ++m_IterationIndex) { UpdateSwarm(); // update the best function value/parameters for (j = 0; j < this->m_NumberOfParticles; ++j) { if (this->m_Particles[j].m_BestValue < m_FunctionBestValue) { this->m_FunctionBestValue = m_Particles[j].m_BestValue; this->m_ParametersBestValue = m_Particles[j].m_BestParameters; } } SetCurrentPosition(this->m_ParametersBestValue); // update the best value memory index = this->m_IterationIndex % bestValueMemorySize; this->m_FunctionBestValueMemory[index] = m_FunctionBestValue; // check for convergence. the m_FunctionBestValueMemory is a ring // buffer with m_NumberOfGenerationsWithMinimalImprovement+1 // elements. the optimizer has converged if: (a) the difference // between the first and last elements currently in the ring buffer // is less than the user-specified threshold. and (b) the particles // are close enough to the best particle. if (this->m_IterationIndex >= m_NumberOfGenerationsWithMinimalImprovement) { if (index == this->m_NumberOfGenerationsWithMinimalImprovement) { prevIndex = 0; } else { prevIndex = index + 1; } // function value hasn't improved for a while, check the // parameter space to see if the "best" swarm has collapsed // around the swarm's best parameters, indicating convergence if ((this->m_FunctionBestValueMemory[prevIndex] - this->m_FunctionBestValueMemory[index]) < this->m_FunctionConvergenceTolerance) { converged = true; std::vector parameterDiffs(this->m_NumberOfParticles); for (k = 0; k < n && converged; ++k) { for (j = 0; j < this->m_NumberOfParticles; ++j) { parameterDiffs[j] = itk::Math::abs(this->m_Particles[j].m_BestParameters[k] - this->m_ParametersBestValue[k]); } std::nth_element(parameterDiffs.begin(), parameterDiffs.begin() + percentileIndex, parameterDiffs.end()); converged = converged && parameterDiffs[percentileIndex] < this->m_ParametersConvergenceTolerance[k]; } } } InvokeEvent(IterationEvent()); } this->m_StopConditionDescription << GetNameOfClass() << ": "; if (converged) { this->m_StopConditionDescription << "successfully converged after " << m_IterationIndex << " iterations"; } else { this->m_StopConditionDescription << "terminated after " << m_IterationIndex << " iterations"; } InvokeEvent(EndEvent()); } void ParticleSwarmOptimizerBase::ValidateSettings() { unsigned int i, n; // we have to have a cost function if (GetCostFunction() == nullptr) { itkExceptionMacro("nullptr cost function"); } // if we got here it is safe to get the number of parameters the cost // function expects n = static_cast((GetCostFunction())->GetNumberOfParameters()); // check that the number of parameters match ParametersType initialPosition = GetInitialPosition(); if (initialPosition.Size() != n) { itkExceptionMacro("cost function and initial position dimensions mismatch"); } // at least one particle if (this->m_NumberOfParticles == 0) { itkExceptionMacro("number of particles is zero"); } // at least one iteration (the initialization phase) if (this->m_MaximalNumberOfIterations == 0) { itkExceptionMacro("number of iterations is zero"); } // we need at least one generation difference to // compare to the previous one if (this->m_NumberOfGenerationsWithMinimalImprovement == 0) { itkExceptionMacro("number of generations with minimal improvement is zero"); } if (this->m_ParameterBounds.size() != n) { itkExceptionMacro("cost function and parameter bounds dimensions mismatch"); } for (i = 0; i < n; ++i) { if (initialPosition[i] < this->m_ParameterBounds[i].first || initialPosition[i] > this->m_ParameterBounds[i].second) { itkExceptionMacro("initial position is outside specified parameter bounds"); } } // if the user set an initial swarm, check that the number of parameters // match and that they are inside the feasible region if (!this->m_Particles.empty()) { if (this->m_Particles[0].m_CurrentParameters.GetSize() != n) { itkExceptionMacro("cost function and particle data dimensions mismatch"); } std::vector::iterator it, end = this->m_Particles.end(); for (it = this->m_Particles.begin(); it != end; ++it) { ParticleData & p = (*it); for (i = 0; i < n; ++i) { if (p.m_CurrentParameters[i] < m_ParameterBounds[i].first || p.m_CurrentParameters[i] > m_ParameterBounds[i].second || p.m_BestParameters[i] < m_ParameterBounds[i].first || p.m_BestParameters[i] > m_ParameterBounds[i].second) { itkExceptionMacro("initial position is outside specified parameter bounds"); } } } } // parameters' convergence tolerance has to be positive for (i = 0; i < n; ++i) { if (this->m_ParametersConvergenceTolerance[i] < 0) { itkExceptionMacro("negative parameters convergence tolerance"); } } // function convergence tolerance has to be positive if (this->m_FunctionConvergenceTolerance < 0) { itkExceptionMacro("negative function convergence tolerance"); } } void ParticleSwarmOptimizerBase::Initialize() { itk::Statistics::MersenneTwisterRandomVariateGenerator::Pointer randomGenerator = Statistics::MersenneTwisterRandomVariateGenerator::GetInstance(); if (m_UseSeed) { randomGenerator->SetSeed(m_Seed); } else { randomGenerator->SetSeed(); } this->m_StopConditionDescription.str(""); SetCurrentPosition(GetInitialPosition()); this->m_IterationIndex = 0; this->m_FunctionBestValueMemory.resize(m_NumberOfGenerationsWithMinimalImprovement + 1); // user did not supply initial swarm if (this->m_Particles.empty()) { RandomInitialization(); } // initialize best function value this->m_FunctionBestValue = itk::NumericTraits::max(); for (unsigned int i = 0; i < this->m_Particles.size(); ++i) { if (this->m_FunctionBestValue > m_Particles[i].m_BestValue) { this->m_FunctionBestValue = m_Particles[i].m_BestValue; this->m_ParametersBestValue = m_Particles[i].m_BestParameters; } } this->m_FunctionBestValueMemory[0] = m_FunctionBestValue; } void ParticleSwarmOptimizerBase::RandomInitialization() { unsigned int i, j, n; n = GetInitialPosition().Size(); ParameterBoundsType parameterBounds(this->m_ParameterBounds); ParametersType mean = GetInitialPosition(); itk::Statistics::MersenneTwisterRandomVariateGenerator::Pointer randomGenerator = Statistics::MersenneTwisterRandomVariateGenerator::GetInstance(); // create swarm this->m_Particles.resize(m_NumberOfParticles); for (i = 0; i < this->m_NumberOfParticles; ++i) { this->m_Particles[i].m_BestParameters.SetSize(n); this->m_Particles[i].m_CurrentParameters.SetSize(n); this->m_Particles[i].m_CurrentVelocity.SetSize(n); } // user supplied initial position is always one of the particles this->m_Particles[0].m_CurrentParameters = mean; // create particles with a normal distribution around the initial // parameter values, inside the feasible region if (this->m_InitializeNormalDistribution) { ParametersType variance(mean.GetSize()); // variance is set so that 3sigma == bounds for (i = 0; i < n; ++i) { variance[i] = (parameterBounds[i].second - parameterBounds[i].first) / 3.0; variance[i] *= variance[i]; } for (i = 1; i < this->m_NumberOfParticles; ++i) { for (j = 0; j < n; ++j) { this->m_Particles[i].m_CurrentParameters[j] = randomGenerator->GetNormalVariate(mean[j], variance[j]); // ensure that the particle is inside the feasible region if (this->m_Particles[i].m_CurrentParameters[j] < parameterBounds[j].first || this->m_Particles[i].m_CurrentParameters[j] > parameterBounds[j].second) { --j; } } } } // create particles with uniform distribution inside the feasible region else { for (i = 1; i < this->m_NumberOfParticles; ++i) { for (j = 0; j < n; ++j) { this->m_Particles[i].m_CurrentParameters[j] = randomGenerator->GetUniformVariate(parameterBounds[j].first, parameterBounds[j].second); } } } // initialize the particles' velocity, so that x_i+v_i is inside the // feasible region, and set the best parameters to the current ones for (i = 0; i < this->m_NumberOfParticles; ++i) { for (j = 0; j < n; ++j) { this->m_Particles[i].m_CurrentVelocity[j] = (randomGenerator->GetUniformVariate(parameterBounds[j].first, parameterBounds[j].second) - this->m_Particles[i].m_CurrentParameters[j]); this->m_Particles[i].m_BestParameters[j] = this->m_Particles[i].m_CurrentParameters[j]; } } // initial function evaluations for (i = 0; i < this->m_NumberOfParticles; ++i) { this->m_Particles[i].m_CurrentValue = this->m_CostFunction->GetValue(m_Particles[i].m_CurrentParameters); this->m_Particles[i].m_BestValue = m_Particles[i].m_CurrentValue; } } } // namespace itk