粒子群算法的思想源于对鸟类捕食行为的研究,模拟鸟集群飞行觅食的行为,鸟之间通过集体的协作使群体达到最优目的。
设想这样的一个场景,一群鸟在随机的搜索食物,在某块区域里有一块食物,所有的鸟都不知道食物在哪里,但是他们可以感受到当前的位置离食物还有多远,此时找到食物的最优策略是什么?答案是:搜寻目前离食物最近的鸟的周围区域,根据自己的飞行经验判断食物的所在。
PSO正是从这种模型中得到了启发:
1.每个寻优问题的解都被想象成一只鸟,称为‘粒子’,所有的粒子都在一个D维空间进行搜索。
2.所有的粒子都由一个fitness function确定适应值以判断目前位置的好坏。
3.每一个粒子都被赋予记忆功能,能记住所搜寻到的最佳位置
4.每一个粒子还有一个速度以决定飞行的距离和方向。这个速度根据本身的飞行经验及同伴的飞行经验进行动态调整。
5.可以看出粒子群算法的基础在于信息的社会共享
D维空间中,有N个粒子;
粒子i位置:xi=(xi1,xi2,…xiD),将xi代入适应函数f(xi)求适应值;
粒子i速度:vi=(vi1,vi2,…viD)
粒子i个体经历过的最好位置:pbesti=(pi1,pi2,…piD)
种群所经历过的最好位置:gbest=(g1,g2,…gD)
通常,在第d(1≤d≤D)维的位置变化范围限定在( X m i n , d , X m a x , d X_{min,d},X_{max,d} Xmin,d,Xmax,d)内,
速度变化范围限定在( − V m a x , d , V m a x , d -V_{max,d},V_{max,d} −Vmax,d,Vmax,d) 内(即在迭代中若
超出了边界值,则该维的速度或位置被限制为该维最大速度或边界
位置)
速度及位置的更新公式
有了上面的了解后,一个很关键的问题就是粒子的速度以及位置如何更新?
粒子i的第d维速度更新公式:
粒子i的第d维位置更新公式
v i d k v_{id}^k vidk:第k次迭代粒子i的飞行速度矢量的第d维分量
x i d k x_{id}^k xidk:第k次迭代粒子i位置矢量的第d维分量
c1,c2:加速度常数,调节学习最大步长(超参数)
r1,r2:两个随机函数,取值范围[0,1],以增加随机性
w:惯性权重,非负数,调节对解空间的搜索范围。(超参数)
可以看出pso算法一共含有3个超参数。
对于粒子速度更新公式这里多介绍一些:
其速度更新公式包含三个部分:
第一部分为粒子先前的速度
第二部分为‘认知’部分,表示粒子本身的思考,可理解为粒子i当前位置与自己最好位置之间的距离
第三部分为‘社会部分’,表示粒子间的信息共享和合作,可理解为粒子i当前位置与群体最好位置之间的距离。
如下图所示,粒子其运动的过程受以上三个方面的影响:
以上面的例子为例,该算法的实现如下,如果需要优化其他问题,只需要调整下fitness function即可。
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
def fit_fun(x): # 适应函数
return sum(100.0 * (x[0][1:] - x[0][:-1] ** 2.0) ** 2.0 + (1 - x[0][:-1]) ** 2.0)
class Particle:
# 初始化
def __init__(self, x_max, max_vel, dim):
self.__pos = np.random.uniform(-x_max, x_max, (1, dim)) # 粒子的位置
self.__vel = np.random.uniform(-max_vel, max_vel, (1, dim)) # 粒子的速度
self.__bestPos = np.zeros((1, dim)) # 粒子最好的位置
self.__fitnessValue = fit_fun(self.__pos) # 适应度函数值
def set_pos(self, value):
self.__pos = value
def get_pos(self):
return self.__pos
def set_best_pos(self, value):
self.__bestPos = value
def get_best_pos(self):
return self.__bestPos
def set_vel(self, value):
self.__vel = value
def get_vel(self):
return self.__vel
def set_fitness_value(self, value):
self.__fitnessValue = value
def get_fitness_value(self):
return self.__fitnessValue
class PSO:
def __init__(self, dim, size, iter_num, x_max, max_vel, tol, best_fitness_value=float('Inf'), C1=2, C2=2, W=1):
self.C1 = C1
self.C2 = C2
self.W = W
self.dim = dim # 粒子的维度
self.size = size # 粒子个数
self.iter_num = iter_num # 迭代次数
self.x_max = x_max
self.max_vel = max_vel # 粒子最大速度
self.tol = tol # 截至条件
self.best_fitness_value = best_fitness_value
self.best_position = np.zeros((1, dim)) # 种群最优位置
self.fitness_val_list = [] # 每次迭代最优适应值
# 对种群进行初始化
self.Particle_list = [Particle(self.x_max, self.max_vel, self.dim) for i in range(self.size)]
def set_bestFitnessValue(self, value):
self.best_fitness_value = value
def get_bestFitnessValue(self):
return self.best_fitness_value
def set_bestPosition(self, value):
self.best_position = value
def get_bestPosition(self):
return self.best_position
# 更新速度
def update_vel(self, part):
vel_value = self.W * part.get_vel() + self.C1 * np.random.rand() * (part.get_best_pos() - part.get_pos()) \
+ self.C2 * np.random.rand() * (self.get_bestPosition() - part.get_pos())
vel_value[vel_value > self.max_vel] = self.max_vel
vel_value[vel_value < -self.max_vel] = -self.max_vel
part.set_vel(vel_value)
# 更新位置
def update_pos(self, part):
pos_value = part.get_pos() + part.get_vel()
part.set_pos(pos_value)
value = fit_fun(part.get_pos())
if value < part.get_fitness_value():
part.set_fitness_value(value)
part.set_best_pos(pos_value)
if value < self.get_bestFitnessValue():
self.set_bestFitnessValue(value)
self.set_bestPosition(pos_value)
def update_ndim(self):
for i in range(self.iter_num):
for part in self.Particle_list:
self.update_vel(part) # 更新速度
self.update_pos(part) # 更新位置
self.fitness_val_list.append(self.get_bestFitnessValue()) # 每次迭代完把当前的最优适应度存到列表
print('第{}次最佳适应值为{}'.format(i, self.get_bestFitnessValue()))
if self.get_bestFitnessValue() < self.tol:
break
return self.fitness_val_list, self.get_bestPosition()
if __name__ == '__main__':
# test 香蕉函数
pso = PSO(4, 5, 10000, 30, 60, 1e-4, C1=2, C2=2, W=1)
fit_var_list, best_pos = pso.update_ndim()
print("最优位置:" + str(best_pos))
print("最优解:" + str(fit_var_list[-1]))
plt.plot(range(len(fit_var_list)), fit_var_list, alpha=0.5)
注意该算法在解决具体问题时需要注意以下几点:
1.种群大小m
m很小很容易陷入局部最优,m很大,pso的优化能力很好,当种群数目增长至一定水平时,再增长将不再有显著的作用。
2.权重因子
对于粒子的速度更新的三部分:
a. 惯性因子w=1表示基本的粒子群算法,w=0表示失去对粒子本身的速度记忆。
b. 自我认知部分的学习因子c1=0表示无私型的粒子群算法,只有社会,没有自我,这样会使群体丧失多样性,从而容易导致陷入局部最优而无法跳出。
c. 社会经验部分的学习因子c2=0表示自我型的粒子群算法,只有自我没有社会,这样导致没有信息的社会共享,算法收敛速度缓慢。
这三个参数的选择非常重要,如何调整这三个参数使算法避免早熟又可以比较快的收敛,对于解决实际问题意义较大。
3.最大速度
速度限制的作用为:维护算法的探索能力与开发能力的平衡。
V m V_m Vm较大时,探索能力强,但是粒子容易飞过最优解
V m V_m Vm较小时,开发能力强,但是容易陷入局部最优解
V m V_m Vm一般设定为每维变量变化范围的10%~20%
4.停止准则
a.最大迭代次数
b.可以接受的满意解(通过fitness function判断是否满意)
5.粒子空间的初始化
较好地选择粒子的初始化空间,将大大缩短收敛时间.初始化空间根据具体问题的不同而不同,根据具体问题进行设定. 该算法为数不多的关键参数的设置却对算法的精度和效率有
着显著影响.
6.线性递减权值(未测)
w m a x w_{max} wmax最大惯性权重, w m i n w_{min} wmin最小惯性权重,run当前迭代次数, r u n m a x run_{max} runmax为算法迭代总次数
较大的w有较好的全局收敛能力,较小的w则有较强的局部收敛能力。因此,随着迭代次数的增加,惯性权重w应不断减少,从而使得粒子群算法在初期具有较强的全局收敛能力,而晚期具有较强的局部收敛能力。
7.收缩因子法(未测)
1999年,Clerc引入收缩因子以保证算法的收敛性,即速度更新公式改为上式,其中,收缩因子K为受φ1 φ2 限制的w。φ1 φ2是需要预先设定的模型参数。
收缩因子法控制系统行为最终收敛,且可以有效搜索不同区域,该法能得到较高质量的解。
8.算法的邻域拓扑结构(未测)
粒子群算法的邻域拓扑结构包括两种,一种是将群体内所有个体都作为粒子的邻域,另一种是只将群体中的部分个体作为粒子的邻域.邻域拓扑结构决定群体历史最优位置,因此该算法分为全局粒子群算法和局部粒子群算法,上面我实现的是全局粒子群算法。
全局粒子群算法
1. 粒子自己历史最优值
2. 粒子群体的全局最优值
局部粒子群算法
1. 粒子自己历史最优值
2. 粒子邻域内粒子的最优值
邻域随迭代次数的增加线性变大,最后邻域扩展到整个粒子群。
实践证明:全局版本的粒子群算法收敛速度快,但是容易陷入局部最优。局部版本的粒子群算法收敛速度慢,但是很难陷入局部最优。现在的粒子群算法大都在收敛速度与摆脱局部最优这两个方面下功夫。其实这两个方面是矛盾的。看如何更好的折中了。