遗传算法入门实例:对PID参数寻优[原创]
纵死侠骨香[这乌龟飙得好快啊
sunxflower.]
开始之前:
假设你已经:能运用C语言,初步了解PID、遗传算法的原理。
维修主板遗传算法能干什么?
(我有个毛病:每当遇到一个东东,我首先会设法知道:这个东东能干什么呢?)
遗传算法可以解决非线性、难以用数学描述的复杂问题。也许这样的陈述让你觉得很抽象,把它换成白话说就是:有个问题我不知道甚至不可能用数学的方法去推导、解算,那么也许我就可以用遗传算法来解决。遗传算法的优点是:你不需要知道怎么去解决一个问题; 你需要知道的仅仅是,用怎么的方式对可行解进行编码,使得它能能被遗传算法机制所利用。
如果你运用过PID来控制某个系统,那你一定非常清楚:PID麻烦就在那三个参量的调整上,很多介绍PI
D的书上常搬一些已知数学模型的系统来做实例环节,但事实上我们面对的往往是不可能用数学模型描述的系统,这个时候该怎么取PID的参值呢?
1、可以依靠经验凑试,耗时耗精力。
2、离线规划,这就是下文要做的事情
3、在线规划,比方说神经网络PID(后续文章将推出,做个广告先^_^)。
一、 将PID用在本次试验中
来个问题先:A VR怎样利用片上和少量的外围器件快速准确地实现D/A输出?(0~5V)
1、实验电路的搭建:
图1:实验原理图
潭面无风镜未磨的上一句是什么搭建这样的电路纯粹是为了本次实验的直观(超调、调整不足等现象通过示波器一目了然),当然,如果实际工程这么简单那也用不到PID,更用不到遗传算法了。回归话题,解释下上面的电路:M16单片机的OC2输出0~100%占空比的PWM,经过RC,可以得到0~5V 的直流电压,这就实现了简易的D/A(实际实验,发现输出电压是1.XX伏~4.XX 伏,未带负载)。用一个图表示:
大成语这个时候如果我要输出 3.5V (可以是其它值)电压,该加怎样的PWM呢?(有个简单的方法:标定,但是这种方法系统调整响应速度较为缓慢,理由见图5下附言)也许我们可以把这个输出电压加到A/D反馈到系统,这样就形成了闭环控制:系统输出PWM ——>> PWM 转换成电压——>>A/D 采集,获得实际值与目标值的偏差(例如3.5V )——>>将偏差进行PID 加载到PWM 输出(然后输出又影响下一次的输入……)
把示波器加到测试点上,调整扫描周期,使示波器能看到完整的一个调整过程。这样,PID 调整的过程就可以在示波器上非常直观地显示出来。如下图:
2、PID 调整的过程(图5):
a)首先、让OC2输出一个初始电压(本次试验取OCR2=100,可以是其它值),当其稳定时进入PID调整环节
b)PID调整:采样输出电压,经过增量PID公式计算得到输入增量,将增量加到输入端,再次采样,继续下一次的调整。(这里有必要说明的是:数字PID 是离散的,就是说,不能让程序while(1)在那不停地执行,因为每次实验while(1)的周期可能不同,这就会导致PID调整的周期是变化的,而这个变化会导致ek、ek +计算错误。正确的做法是设置一个时间变量(time_pid),用定时器来使其置位,而循环里就是查询这个变量的值,这样做虽然没有在定时中断里直接执行PID 代码迅速,但推荐的做法还是像上面陈述的一样,虽然时间上可能误差几十个us,但对于一个几百ms 的PID 周期来说这点误差还是可以接受的,另外,这个周期不能太短,比方说上次调整尚未完成,时间变量又已经置1了。)
对于本文的实验,如果PID 参数较好,那调整30次早就已经达到了目标值,所以这个时候我们将系统复原:结束PID 调整,并重新输出一个初始电压,开始下一次PID 的过程。
图5:PID调整的过程
另:见图5,结束PID调整后是,电压是缓慢下降的,说明系统是滞后的,如果用标定的方法来实现D/A,效果会像图中下降的那段曲线。
流程如下:
D参数,看看超调是怎么样的,震荡又是怎么样的……
二、接下来让我们卷起袖子开始捣鼓遗传算法
1、介绍下遗传算法
如果你已经了解了遗传算法的基本原理,请跳过这段。
根据达尔文的自然选择学说,地球上的生物具有很强的繁殖能力。在繁殖过程中,大多数生物通过遗传,使物种保持相似的后代;部分生物由于变异,后代具有明显差别,甚至形成新物种。正是由于生物的不断繁殖后代,生物数目大量增加,而自然界中生物赖以生存的资源却是有限的。因此,为了生存,生物就需要竞争。生物在生存竞争中,根据对环境的适应能力,适者生存,不适者消亡。自然界中的生物,就是根据这种优胜劣汰的原则,不断地进行进化。
那么,到底怎样让这个算法在计算机里运作起来呢?或者说我们怎样模拟这个进化的过程呢?仔细阅读上面那段文字,发现需要解决以下问题:
A)、怎样表示一个个体?
B)、怎样繁殖后代?
C)、怎样实现优胜劣汰?
2、针对ABC三个问题,用实际例子解答
A)怎样表示一个个体——染色体编码和解码(采用二进制编码方法)
其实非常简单,一个变量即是一个个体,假如我们定义了一个个体名字叫Joy (unsigned int Joy=12345;)
Joy的基因为12345,转换成二进制为11000000111001,那我们大可以这样约定:Joy的基因中前2个位表示了Joy的发色(比如00为黑色;01为绿色),接着3个位表示了Joy的肤色,后4个位表示了Joy的眼睛颜色…………相信你已经看出其间的奥秘了吧?
本文涉及PID三个参量:Kp,Ki,Kd ,我们也完全可以将3个参量理解成上面的特征(发色、肤色、眼睛颜色),将这些特征组装在一起就可以表示为一个个体,N个个体组成一个种群……。具体怎么编呢:
Unsigned int colony=12332; //创造一个个体,并随机地赋予它某些特征。
(注:12332(十进制)=001100 00001 01100(二进制))
多条件查找函数colony的0~4位组成kd,即01100(二进制)
5~9位拿出来组成ki,即00001(二进制)
10~15位组成kp,001100(二进制)
然而这样还不行,因为这三个数都是整数,如果需要小数呢?缩放!
拿Kd来说,Kd的取值范围是0~11111,如果Kd只需取小于1的小数,则将Kd除以31(11111的十进制),这样就得到了0~1的Kd,如果想得到0~2的Kd,则将Kd乘以2再除以31。Kp,Ki也是类似操作,只不过缩放程度不同。想必我们已经把怎么编码搞清楚了,事实上本例中我们没有必要写染色体编码的函数,只需知道染色体是怎么编的,进而知道怎么解码,初始化群体的时候只要随机产生一些数就好了。
解码举例:假如个体基因62267,对应的PID参值多少呢?将其传递给void redressal(unsigned int colon)函数,可知Kp=19.047;Ki=0.80;Kd=0.87
看下解码的函数:
/*********************************************
*函数名:void redressal(unsigned int k_dip) PID参数解码
*说明:
1、调用函数:
2、变量说明:
全局浮点变量,分别对应Kp,Ki,Kd
真柏盆景float
ABC[3]
3、常量说明:
*入口:unsigned int k_dip 传递个体染色体
*出口:
*********************************************/
void redressal(unsigned int colon)
{ ABC[0]=
(( colon&0xFC00)>>10)*20.0/63; ABC[1]=(( colon&0x3E0)>>5)/31.0; ABC[2]=( colon&0x1F)/31.0;
}
我们只要稍作修改,就产生了一堆个体,即一个群体
unsigned int colony[COL_MAX]={ 62267,15148,39769,31849,58411,49944,29915,58375 ,53831 ,29144,40332,51900,60411,48378,11552,26588,61306,60089
,26887,58565, 3794,23125,53291, 646, 9102,13288,13023
,39570,17838,13029, 1001,48941,29169,61066,30539,27436
,55457,34416,13280,44049,54926, 1287,44647,24869,54512
,32952,46495,28107,19963,12429};// COL_MAX 是宏定义,定义种群的大小
这个数组即为初始化的群体,其中的某个元素即为一个个体,对应一组PID 的参值。遗传算法就从这个种群开始,根据优胜劣汰的原则,不断地进行进化。
B)怎样繁殖后代——染色体杂交与变异
杂交: 记得高中生物有句话:杂种的优势~呵呵。言归正传,怎么将两个个体杂交得到新的个体呢?假如有个个体我们称之为父本(11110000),另一个个体我们称之为母本(10101010),假如他们发生了那个那个~~生了个小孩,如果交叉点在第3位和第4位间,那么得到新的个体是11110010 和10101000。一般这个杂交点选择基因长度的0.7左右,这里为了方便,就直接将一个int 型剪成两个char 型的交换了。定义父本(unsigned int dad ),母本(unsigned int mum ),杂交代码如下:
baby1=dad&0xff; //取得低半截染色体
baby1|=mum&0xff00;
baby2=mum&0xff;
baby2|=dad&0xff00;
变异:
权利与义务的关系
若二进制编码的染色体发生变异,无非就是0变1,1变0。其实变异是很少发生的事情,代码为函数:
#define var_p 650
if(rand()<var_p) //如果随机值小于var_p 则变异,rand()在0~65535
{ //间变化,则小于650的概率约为0.01
/**此处放0变1,1变0的代码**/
}
左手定律变异是按位分别查询是否发生的,并非所有位同事进行。