粒子羣算法(PSO)的C++實現
粒子羣算法(PSO----Particle Swarm Optimization)是常用的智能算法之一,它模擬了 鳥羣覓食 行爲,是一種具有隨機性的 仿生算法 。PSO算法在無約束條件函數最優化問題上具有全局搜索能力強,局部收斂能力較強的優點。
本篇博文目的在於:
- 記錄基本的PSO算法原理。
- 利用C++將PSO抽象算法本身加以實現,構建適合一類函數優化問題的PSO算法類。並附上相應的源碼。
PSO算法基本原理
對於一個簡單的 無約束最優化 問題:
首先引入粒子羣算法中粒子的結構概念。
struct Partical
{
double _position[_dimension]; //粒子所在位置,即爲問題候選解
double _velocity[_dimension]; //粒子運動速度
double _fitness; //粒子適應度
double _bestPosition[_dimension]; //粒子所經歷過的最優位置
};
一個粒子攜帶了一個候選解_position以及該候選解對應的函數值_fitness(這裏稱爲粒子適應度)。另外粒子本身是運動的,具有速度_dimension。同時,一個粒子在運動過程中會保留自己所經歷過的候選解中最優的一個_bestPosition。
接下來引入粒子羣的結構概念:
struct pso
{
Partical _particalSet[_particalCount]; //一羣粒子構成的集合
double _localBestGuideCoe; //個體最優解引導加速度
double _globalBestGuideCoe; //全局最優解引導加速度
double _globalBestPosition; //當前粒子羣進化中的全局最優解
double _globalBestFitness; //當前粒子羣進化中的全局最優解適應度
}
粒子羣簡單來說就是由一羣粒子_particalSet構成。但是粒子羣還要規定粒子運動的一些屬性以及記錄這羣粒子的一些狀態。
模擬鳥類覓食的行爲特性,將粒子想象成覓食鳥羣中的一隻鳥,這隻鳥既想向着找到食物(適應度)最多的鳥那(_globalBestPosition)飛,又不想放棄自己以前找到過最多食物的那個位置(_bestPosition)。
數學一點進行描述,就是 粒子在全局最優方向和個體局部最優方向都具有一定的加速度 ,而係數_globalBestGuideCoe和_localBestGuideCoe就在一定程度上描述了這兩個加速度的大小。
用數學公式進行表達,粒子羣在第 k 時刻到第 k+1 時刻的運動描述如下:
更新策略中,V代表粒子速度,P爲粒子所在位置,G爲全局最優引導加速度_globalBestGuideCoe,L爲局部最優引導加速度_localBestGuideCoe,GBP(k)代表 k 時刻粒子羣的全局最優_globalBestPosition,LBP(k)代表k時刻該粒子的個體局部最優,也即_localBestPosition。r 代表0-1均勻分佈的隨機數,i爲粒子在粒子羣中的下標序號。
對於PSO算法,不難想象,在一定的條件下,全局的粒子從四面八方向着最優解 聚攏 過去,從而實現函數的最優化。
以下示意圖是採用pso算法函數最大值點的運行過程。其中紅點代表當前粒子羣搜索得到的全局最優解。黑色箭頭代表了粒子位置及其運動速度。待優化函數爲:
運行效果:
PSO算法的C++實現以及測試
本節提供並介紹PSO算法的C++代碼,並附上算法的一個測試用例以說明算法類的使用方法。
與算法原理介紹一節類似,先介紹PSO粒子類:
class ZPSO_Partical
{
public:
int _dimension; //粒子維度
double* _position; //粒子所在位置,即爲問題候選解
double* _velocity; //粒子運動速度
double _fitness; //粒子適應度
double* _bestPosition; //粒子所經歷過的最優位置
void initial(int dimension); //粒子初始化,用以分配粒子所需內存
void copy(ZPSO_Partical); //複製其他粒子的數據,類似賦值操作
ZPSO_Partical(void); //構造
~ZPSO_Partical(void); //析構
};
代碼註釋對粒子類的各屬性與函數都已經描述得比較全面,不再贅述。一下介紹粒子羣算法類—— ZPSO_Algorithm 的幾個重要函數。
/***************************************************************
* 函數名:ZPSO_Algorithm
* 函數描述:構造一個PSO算法
* 輸入參數:
* objFunction:優化目標函數指針
* positionMinValue:解下界,其任一維度注意應低於上界
* positionMaxValue:解上界,其任一維度注意應高於下界
* dimension:解空間維度
* particalCount:種羣粒子個數
* globalGuideCoe:粒子種羣全局最優引導速度因子,默認爲2
* localGuideCoe:粒子種羣個體最優引導速度因子,默認爲2
* maxSpeed:粒子運動最大速度
* 輸出參數:
* ZPSO_Algorithm&:構建得到的PSO算法本身
***************************************************************/
ZPSO_Algorithm(double (*objFunction)(ZPSO_Partical&),
double *positionMinValue,double *positionMaxValue,
int dimension,int particalCount,
double globalGuideCoe = 2,double localGuideCoe = 2,
double maxSpeed = 1);
以上爲PSO算法類 ZPSO_Algorithm的構造函數,每個參數的意義在註釋中都有說明,其中要重點提到的是參數objFunction。
double objFunction(ZPSO_Partical& partical);
objFunction 是以粒子爲輸入,返回粒子適應度的一個函數。一般而言,在 objFunction 中讀取 partical 的 候選解_position,並調用待優化的目標函數計算_position位置的函數值作爲粒子適應度返回。
以下介紹pso的優化函數:
/***************************************************************
* 函數名:findMax
* 函數描述:採用粒子羣算法搜索最優解
* 輸入參數:
* times:粒子羣進化次數
* bestPartical:進化得到的最優個體
* disturbanceRate:粒子速度擾動概率,默認爲0.2
* disturbanceVelocityCoe:速度擾動因子,表徵擾動速度相對_maxSpeed大小
* 用於擾動粒子速度以提高局部搜索能力,默認爲0.05
* 輸出參數:void
***************************************************************/
void findMax(int times,ZPSO_Partical& bestPartical,
double disturbanceRate = 0.2,
double disturbanceVelocityCoe = 0.05);
ZPSO_Algorithm 提供的最大化函數 findMax 需要用戶給定粒子羣進化次數(進化時間)times。需要說明的是,爲了提高PSO算法的搜索能力,避免加速度選擇要求過於嚴格的問題,本文實現的PSO算法中引入了速度擾動的概念,即粒子除了全局最優加速度與局部最優加速度,還有一個隨機加速度。參數 disturbanceRate 和 disturbanceVelocityCoe則分別描述了產生隨機加速度的概率與隨機加速度的大小。令 disturbanceRate = 0 則算法退化爲最基本的PSO。
以下提供測試樣例,舉例說明ZPSO_Algorithm的使用方法。
主函數文件 <main.cpp>:用於構建 objFunction 與調用ZPSO_Algorithm。
#include "ZPSOAlgorithm.h"
#include <math.h>
#include <iostream>
using namespace std;
//待優化的目標函數
double objFunction(ZPSO_Partical& partical);
int main(void)
{
//定義求解範圍,x在[-10,10]範圍,y在[-10,10]範圍
double minPos[] = {-10,-10};
double maxPos[] = {10,10};
//定義問題描述參數
int dimension = 2; //候選解維度
int particalCount = 200; //粒子羣粒子個數
double globalGuideCoe = 1; //全局引導加速度
double localGuideCoe = 1; //局部引導加速度
double maxSpeed = 4; //粒子最大速度
//構建pso算法
ZPSO_Algorithm pso(objFunction,minPos,maxPos,dimension,particalCount,
globalGuideCoe,localGuideCoe,maxSpeed);
//運行pso算法
ZPSO_Partical bestPartical; //粒子羣最終進化結果
int generation = 200; //粒子羣進化代數
pso.findMax(generation,bestPartical); //獲取最終進化結果
//輸出最終結果
cout<<"the best position for the objFunction is:"<<endl;
cout<<"x="<<bestPartical._position[0]<<endl;
cout<<"y="<<bestPartical._position[1]<<endl;
cout<<"the best fitness for the objFunction is:"<<bestPartical._fitness<<endl;
return(0);
}
//優化目標函數定義
double objFunction(ZPSO_Partical &partical)
{
//從partical中讀取候選解
double x = partical._position[0]-0.3;
double y = partical._position[1]+0.1;
//計算候選解對應的函數值
double r = sqrt(x*x+y*y);
double rtn;
if(r<1e-8)
rtn = 1;
else
rtn = sin(r)/r;
return(rtn);
}
PSO算法類定義頭文件 <ZPSOAlgorithm.h>
/************************************************************
* 頭文件名:ZPSOAlgorithm
* 頭文件描述:用戶自定義粒子羣算法
* 作者:chikuo
* 日期:20190706
* 版本號:ZPSOAlgorithm_2019070601
************************************************************/
/************************************************************
* 修改日期:20190708
* 修改人:chikuo
* 修改信息:引入擾動速度,提高pso算法局部搜索能力
************************************************************/
#ifndef _ZPSOALGORITHM_H
#define _ZPSOALGORITHM_H
#include <stdlib.h>
#include <time.h>
#include <math.h>
//粒子羣算法例子個體
class ZPSO_Partical
{
public:
int _dimension; //粒子維度
double *_position; //粒子所在位置數組指針
double *_velocity; //例子速度
double *_bestPosition; //當前粒子搜索得到過的最優位置
double _fitness; //例子適應度
double _bestFitness; //當前粒子搜索得到過的最優適應度
//構造函數,粒子維度初始化爲0
ZPSO_Partical(void)
{_dimension = 0;}
//析構函數,釋放粒子內存
~ZPSO_Partical(void)
{
if(_dimension)
{
delete []_position;
delete []_velocity;
delete []_bestPosition;
}
}
//初始化函數,用於爲粒子開闢內存空間
void initial(int dimension)
{
if(_dimension!=dimension && dimension)
{
//需要重新分配內存
if(_dimension)
{
//消除已有內存
delete []_position;
delete []_velocity;
delete []_bestPosition;
}
//開闢新內存
_dimension = dimension;
_position = new double[_dimension];
_velocity = new double[_dimension];
_bestPosition = new double[_dimension];
}
}
//複製函數,用於粒子間的複製操作
void copy(ZPSO_Partical& partical)
{
this->initial(partical._dimension);
for(int i=0;i<_dimension;i++)
{
_position[i] = partical._position[i];
_velocity[i] = partical._velocity[i];
_bestPosition[i] = partical._bestPosition[i];
_fitness = partical._fitness;
_bestFitness = partical._bestFitness;
}
}
};
//PSO算法
class ZPSO_Algorithm
{
public:
int _dimension; //粒子羣維度
int _particalCount; //種羣粒子數量
double _globalGuideCoe; //全局最優引導係數
double _localGuideCoe; //局部最優引導係數
ZPSO_Partical _globalBestPartical; //搜索過程得到的全局最優粒子
double *_positionMinValue; //粒子位置的最小界
double *_positionMaxValue; //粒子位置的最大界
double _maxSpeed; //粒子允許最大速度
double (*_fitnessFunction)(ZPSO_Partical&); //粒子適應度函數
ZPSO_Partical *_particalSet; //粒子集
/***************************************************************
* 函數名:ZPSO_Algorithm
* 函數描述:構造一個PSO算法
* 輸入參數:
* objFunction:優化目標函數指針
* positionMinValue:解下界,其任一維度注意應低於上界
* positionMaxValue:解上界,其任一維度注意應高於下界
* dimension:解空間維度
* particalCount:種羣粒子個數
* globalGuideCoe:粒子種羣全局最優引導速度因子,默認爲2
* localGuideCoe:粒子種羣個體最優引導速度因子,默認爲2
* maxSpeed:粒子運動最大速度
* 輸出參數:
* ZPSO_Algorithm&:構建得到的PSO算法本身
***************************************************************/
ZPSO_Algorithm(double (*objFunction)(ZPSO_Partical&),
double *positionMinValue,double *positionMaxValue,
int dimension,int particalCount,
double globalGuideCoe = 2,double localGuideCoe = 2,
double maxSpeed = 1)
{
//初始化類內參數並分配內存
_fitnessFunction = objFunction;
_dimension = dimension;
_positionMinValue = new double[_dimension];
_positionMaxValue = new double[_dimension];
for(int i=0;i<_dimension;i++)
{
_positionMinValue[i] = positionMinValue[i];
_positionMaxValue[i] = positionMaxValue[i];
}
_particalCount = particalCount;
_globalGuideCoe = globalGuideCoe;
_localGuideCoe = localGuideCoe;
_maxSpeed = maxSpeed;
_particalSet = new ZPSO_Partical[_particalCount];
for(int i=0;i<_particalCount;i++)
_particalSet[i].initial(_dimension);
_globalBestPartical.initial(_dimension);
//配置隨機數種子
srand((unsigned int)time(NULL));
}
/***************************************************************
* 函數名:~ZPSO_Algorithm
* 函數描述:析構一個PSO算法,釋放算法內存
* 輸入參數:void
* 輸出參數:void
***************************************************************/
~ZPSO_Algorithm(void)
{
//釋放內存
delete []_positionMinValue;
delete []_positionMaxValue;
delete []_particalSet;
}
/***************************************************************
* 函數名:rand0_1
* 函數描述:生成一個0-1的均勻分佈隨機數
* 輸入參數:void
* 輸出參數:
* double:在0-1上均勻分佈的隨機數
***************************************************************/
double rand0_1(void){return((1.0*rand())/RAND_MAX);}
/***************************************************************
* 函數名:refresh
* 函數描述:計算粒子適應度並更新粒子個體最優位置與全局最優位置
* 輸入參數:void
* 輸出參數:void
***************************************************************/
void refresh(void)
{
int globalBestParticalIndex = -1;
for(int i=0;i<_particalCount;i++)
{
//循環遍歷所有粒子,更新粒子適應度
_particalSet[i]._fitness = this->_fitnessFunction(_particalSet[i]);
if(_particalSet[i]._fitness > _particalSet[i]._bestFitness)
{
//更新粒子的個體最優位置
for(int j=0;j<_dimension;j++)
_particalSet[i]._bestPosition[j] = _particalSet[i]._position[j];
_particalSet[i]._bestFitness = _particalSet[i]._fitness;
//是否更新全局最優解
if(_particalSet[i]._bestFitness > _globalBestPartical._bestFitness)
globalBestParticalIndex = i;
}
}
//更新全局最優粒子位置
if(globalBestParticalIndex != -1)
_globalBestPartical.copy(_particalSet[globalBestParticalIndex]);
}
/***************************************************************
* 函數名:randomlyInitial
* 函數描述:隨機初始化種羣中粒子位置
* 輸入參數:void
* 輸出參數:void
***************************************************************/
void randomlyInitial(void)
{
int globalBestParticalIndex = -1;
//遍歷所有粒子
//初始化第0個粒子與全局最優粒子
//初始化粒子位置與速度
double velocityMod = 0;
//遍歷粒子的任一維度
for(int j=0;j<_particalSet[0]._dimension;j++)
{
//隨機初始化粒子位置與最佳位置
double tempVal = _positionMinValue[j];
tempVal += rand0_1()*(_positionMaxValue[j]-_positionMinValue[j]);
_particalSet[0]._position[j] = tempVal;
_particalSet[0]._bestPosition[j] = tempVal;
//隨機初始化粒子速度
_particalSet[0]._velocity[j] = rand0_1();
velocityMod += _particalSet[0]._velocity[j]*_particalSet[0]._velocity[j];
}
//粒子速度歸化爲隨機大小v_mod
double v_mod = rand0_1()*_maxSpeed;
velocityMod = sqrt(velocityMod);
for(int j=0;j<_particalSet[0]._dimension;j++)
_particalSet[0]._velocity[j] *= (v_mod/velocityMod);
//更新粒子初代適應度值與最佳適應度值
_particalSet[0]._fitness = _fitnessFunction(_particalSet[0]);
_particalSet[0]._bestFitness = _particalSet[0]._fitness;
_globalBestPartical.copy(_particalSet[0]);
//初始化1~_particalCount-1個粒子
for(int i=1;i<_particalCount;i++)
{
velocityMod = 0;
//初始化粒子位置與速度
//遍歷粒子的任一維度
for(int j=0;j<_particalSet[i]._dimension;j++)
{
//隨機初始化粒子位置與最佳位置
double tempVal = _positionMinValue[j];
tempVal += rand0_1()*(_positionMaxValue[j]-_positionMinValue[j]);
_particalSet[i]._position[j] = tempVal;
_particalSet[i]._bestPosition[j] = tempVal;
//隨機初始化粒子速度
_particalSet[i]._velocity[j] = rand0_1();
velocityMod += _particalSet[i]._velocity[j]*_particalSet[i]._velocity[j];
}
//粒子速度歸化爲隨機大小v_mod
v_mod = rand0_1()*_maxSpeed;
velocityMod = sqrt(velocityMod);
for(int j=0;j<_particalSet[i]._dimension;j++)
_particalSet[i]._velocity[j] *= (v_mod/velocityMod);
//更新粒子初代適應度值與最佳適應度值
_particalSet[i]._fitness = _fitnessFunction(_particalSet[i]);
_particalSet[i]._bestFitness = _particalSet[i]._fitness;
if(_particalSet[i]._bestFitness > _globalBestPartical._bestFitness)
globalBestParticalIndex = i;
}
//更新粒子羣全局最佳數據
if(globalBestParticalIndex != -1)
_globalBestPartical.copy(_particalSet[globalBestParticalIndex]);
}
/***************************************************************
* 函數名:disturbance
* 函數描述:對粒子速度進行給定大小的擾動
* 輸入參數:
* partical:被擾動的粒子對象
* relativeVelocityRate:擾動速度大小上限相對於_maxSpeed的比例,默認爲0.05
* 輸出參數:void
***************************************************************/
void disturbance(ZPSO_Partical &partical,double relativeVelocityRate = 0.05)
{
//生成擾動速度
double *disturbanceVelocity = new double[_dimension];
//隨機生成擾動速度大小
double disturbanceVelocityMod = relativeVelocityRate*_maxSpeed*rand0_1();
double v_mod = 0;
for(int i=0;i<_dimension;i++)
{
disturbanceVelocity[i] = rand0_1();
v_mod += disturbanceVelocity[i]*disturbanceVelocity[i];
}
v_mod = sqrt(v_mod);
//擾動速度大小歸化到disturbanceVelocityMod
for(int i=0;i<_dimension;i++)
disturbanceVelocity[i] *= (disturbanceVelocityMod/v_mod);
//擾動粒子速度
v_mod = 0;
for(int i=0;i<_dimension;i++)
{
partical._velocity[i] += disturbanceVelocity[i];
v_mod += partical._velocity[i]*partical._velocity[i];
}
v_mod = sqrt(v_mod);
//粒子速度受限
if(v_mod > _maxSpeed)
for(int i=0;i<_dimension;i++)
partical._velocity[i] *= (_maxSpeed/v_mod);
delete[]disturbanceVelocity;
}
/***************************************************************
* 函數名:update
* 函數描述:更新粒子羣粒子位置與適應度
* 輸入參數:
* disturbanceRate:粒子速度擾動概率,默認爲0.2
* disturbanceVelocityCoe:速度擾動因子,表徵擾動速度相對_maxSpeed大小
* 用於擾動粒子速度以提高局部搜索能力,默認爲0.05
* 輸出參數:void
***************************************************************/
void update(double disturbanceRate = 0.2,
double disturbanceVelocityCoe = 0.05)
{
double v_mod;
//遍歷所有粒子
for(int i=0;i<_particalCount;i++)
{
//遍歷所有維度
v_mod = 0;
double r1 = rand0_1();
double r2 = rand0_1();
for(int j=0;j<_particalSet[i]._dimension;j++)
{
//速度更新
//全局最優位置加速度
_particalSet[i]._velocity[j] += _globalGuideCoe*r1*(_globalBestPartical._bestPosition[j]-_particalSet[i]._position[j]);
//個體局部最優位置加速度
_particalSet[i]._velocity[j] += _localGuideCoe*r2*(_particalSet[i]._bestPosition[j]-_particalSet[i]._position[j]);
//粒子速度模二
v_mod += _particalSet[i]._velocity[j]*_particalSet[i]._velocity[j];
}
//粒子速度受限
v_mod = sqrt(v_mod);
if(v_mod > _maxSpeed)
for(int j=0;j<_particalSet[i]._dimension;j++)
_particalSet[i]._velocity[j] *= (_maxSpeed/v_mod);
//對粒子速度進行擾動,提高算法局部搜索能力
if(rand0_1()<disturbanceRate)
this->disturbance(_particalSet[i],disturbanceVelocityCoe);
//位置更新
for(int j=0;j<_particalSet[i]._dimension;j++)
{
_particalSet[i]._position[j] += _particalSet[i]._velocity[j];
//粒子位置受限
if(_particalSet[i]._position[j] < _positionMinValue[j])
_particalSet[i]._position[j] = _positionMinValue[j];
else if(_particalSet[i]._position[j] > _positionMaxValue[j])
_particalSet[i]._position[j] = _positionMaxValue[j];
}
}
//更新粒子羣適應度
this->refresh();
}
/***************************************************************
* 函數名:findMax
* 函數描述:採用粒子羣算法搜索最優解
* 輸入參數:
* times:粒子羣進化次數
* bestPartical:進化得到的最優個體
* disturbanceRate:粒子速度擾動概率,默認爲0.2
* disturbanceVelocityCoe:速度擾動因子,表徵擾動速度相對_maxSpeed大小
* 用於擾動粒子速度以提高局部搜索能力,默認爲0.05
* 輸出參數:void
***************************************************************/
void findMax(int times,ZPSO_Partical& bestPartical,
double disturbanceRate = 0.2,
double disturbanceVelocityCoe = 0.05)
{
this->randomlyInitial();
for(int i=0;i<times;i++)this->update(disturbanceRate,disturbanceVelocityCoe);
bestPartical.copy(_globalBestPartical);
}
/***************************************************************
* 函數名:findMax
* 函數描述:採用粒子羣算法搜索最優解
* 輸入參數:
* times:粒子羣進化次數
* bestParticalInEachLoop:每一次進化中的最優個體數組,
* 長度爲times+1,由外部調用者提供內存空間
* disturbanceRate:粒子速度擾動概率,默認爲0.2
* disturbanceVelocityCoe:速度擾動因子,表徵擾動速度相對_maxSpeed大小
* 用於擾動粒子速度以提高局部搜索能力,默認爲0.05
* 輸出參數:void
***************************************************************/
void findMax(int times,ZPSO_Partical *bestParticalInEachLoop,
double disturbanceRate = 0.2,
double disturbanceVelocityCoe = 0.05)
{
this->randomlyInitial();
for(int i=1;i<=times;i++)
{
this->update(disturbanceRate,disturbanceVelocityCoe);
bestParticalInEachLoop[i].copy(_globalBestPartical);
}
}
};
#endif // _ZPSOALGORITHM_H
將以上兩個文件直接放到一個文件夾中即可運行,爲純CPP文件。ZPSOAlgorithm.h則更類似於一個庫文件,其定義的PSO算法類ZPSO_Algorithm可以解決一類最優化問題,而不限於某一個具體的待優化函數。
算法改進意見
PSO算法實現代碼中,需要用戶給定優化過程的粒子羣進化次數,可以進一步改進PSO算法的收斂條件,改爲當全局最優適應度在指定代數內都不發生改變時,判斷算法收斂,退出循環。
[筆者注] PSO算法由J. Kennedy和R. C. Eberhart等在1995年提出,本文代碼的設計與實現由作者個人設計與實現,僅用於PSO算法的記錄、學習與分享。