智能设计-建模&仿真(4):
交互式遗传算法(IGA)的编码设计
刘肖健
浙江工业大学
这次要做一个智能算法的案例:交互式遗传算法(Interactive Genetic Algorithms,IGA)。
先做最基础的一步:编码设计。
“编码设计”的意思是:用一系列数字变量来表达我们的设计方案。编码设计主要是为了方便程序处理。算法处理数字变量最得心应手,所以得把设计方案表达为数字变量。
创意之代码曾发过几篇与编码有关的文章:
图像编码:面向色彩意象的设计再现 编码设计:产品的参数化定义 《营造法原》瓦花墙洞纹样设计有关交互式遗传算法,创意之代码发布过一个完整的色彩优化案例《配色设计:交互式遗传算法源代码》。该案例是对一个固定的设计方案的色彩进行优化调整,比较粗糙,但是具备了IGA的基本功能。这次做个稍复杂的新案例,里面有一些比色彩更多的设计要素需要考虑。
1. 案例描述1)写一个Function:WallPaperMaker(),用来画一幅壁纸图案。
比如这种的:
上图来自创意之代码的文章《共享插件028:随机四方连续》。
生成的图案可以有一定的复杂度,但程序不要太复杂。比如上面这个四方连续的做法,就是从隔壁页面里随便抓几个shape,复制、旋转、缩放、上色,然后位置随机摆放一下就完了。隔壁页面是个库,里面存放着各种树叶子供复制粘帖:
各种方式都可以,根据自己的喜好来。
2)给这个Function定义参数。
比如上面这个四方连续Function,它的参数包括单元里的树叶数量、种类、色彩、大小比例、分布等。即使是随机数,也会有一个随机范围(不一定非要使用随机数不可)。
用这些参数组成一个数组,这个数组就是生成的四方连续的“编码”。
3)编写基于IGA的交互式优化壁纸设计软件。
仿照《配色设计:交互式遗传算法源代码》的案例开发,只是把色彩编码换成壁纸编码。
2. 编码设计在动手编写执行函数(就是实际做设计的函数)WallPaperMaker之前,先规划一下最终要做的作品,最终梳理出WallPaperMaker函数需要输入哪些参数。
这个过程即“编码设计”,WallPaperMaker()的参数序列即遗传算法中的个体编码,一个确定下来的参数序列在程序中就代表一个设计方案,这是程序用来识别设计方案的方法。
这里只做一个单元而不是整幅四方连续。
参数序列大致有如下几种:
1)叶子类型
在给定的n种叶子元素里选择,需要区别出每一种叶子的选中概率,因为有的用户可能偏爱某一种类型的叶子。可以用一个数组来表示概率密度,比如总共有4种叶子,则数组Array(1,1,2,4)表示四种叶子的选中概率分别为1/8、1/8、2/8、4/8。
2)叶子数量
这个简单,给个整数就行了。如果不是整数,就先圆成整数。
3)叶子尺寸
设置叶子(不论哪个)的最大与最小尺寸,可以采用用页宽的比值,如数组Array(0.05,0.15)表示叶子的尺寸在[10.5,31.5]范围内随机变化(页面宽210mm)。叶子的角度在360内随机旋转,就不用参数了。
4)叶子密度
为了避免叶子的位置太随机失去了设计感,对它们的分布密度做一些有意的规划。
采用最简单的密度分布方式:指定页面中的某个坐标位置为密度中心,即该处密度最大,离得它越远,密度越小。可以用数组Array(x,y)确定密度中心,然后以该点为极坐标圆点,在极坐标空间内产生随机点(随机的极径和极角),就会形成从原点向外密度逐渐减小的分布效果。
5)叶子颜色
简单一点,在某个固定的色彩范围内变化。用HSB色,给出一个固定的色彩值,然后在周围不远处变化
6)背景色
也简单一点:前景的叶子用亮色,背景用暗色。背景也用HSB,调整三个分量很容易搞出各种暗色。
上述6类内容构成了设计方案的编码,这个编码已经有点复杂了。
3. WallPaperMaker() 3.1. 用程序生成元素把元素库做在文档里是个很方便的主意,用户可以自己添加喜欢的元素。只是程序拷贝复制是个挺耗时的活儿,这里介绍一个小技巧,可以把元素的生成做到程序里,不使用库,也就是用程序来生成库里的元素。
具体步骤:
1)画一个元素,或copy一个过来也行;
2)启动代码录制开关,记录代码;
3)进入节点编辑模式:
4)随便拖动一个节点,再拖回原位,不要修改节点的位置,只是产生一个拖动的动作,以便代码记录器录下代码;
5)停止记录,打开记录下来的代码,发现录下了长长的一坨代码,是这样的:
代码实在太长,当中用省略号代替了。
观察一下这堆代码,我们发现只拖动修改一个节点的位置,代码记录器则把所有节点的位置重写了一遍,也就是把这条曲线整个重建了一遍。实际上修改某个节点的位置CorelDraw是有这个函数的,但是代码记录器太笨,重写了所有节点的位置。这样反倒给我们提供了一个方便,就是可以中这种方式把创建一个新元素的代码写进函数里,而不用费劲的去一个个敲那些坐标值了。
6)把记录下来的代码改成函数,函数是这样的:
注意,函数的最后一句跟记录的代码不太一样,因为函数需要创建一个新对象,而不是修改。
函数创建的叶子是在页面中的固定位置,可以对其进行移动、缩放、旋转、上色等后续操作。
这个函数没有参数。
7)同样的元素生成函数多做几个。这个案例做了4个,设计方案就用这四个叶子来构建。
先编写一个最简单的初步测试程序放在一个按钮里,程序如下:
上面这个程序的执行结果是这样的:
四个叶子选哪个,既要有随机性,还要体现不同的概率密度。前面说过了,用一个数组来指示概率。
数组Array(a,b,c,d)的四个成员变量表示四种叶子被选中的相对概率,它们的绝对概率分别是:
a/(a+b+c+d)
b/(a+b+c+d)
c/(a+b+c+d)
d/(a+b+c+d)
如果只是用轮盘赌算法函数Roulette()来确定选哪个叶子,并不需要算出绝对概率。但是交互式遗传算法在优化过程中需要进行编码的交叉互换,相对概率的尺度不一致,会造成混乱。比如(1,1,1,1)和(10,10,10,10)两个编码实际上是一样的,它们进行交叉的结果应该也是平均分配每个叶子的概率,但是这两个编码交叉的结果可能是(10,1,1,1),第一个叶子的选中概率被虚假放大。
绝对概率的换算可以放在后面的算法流程中处理。
叶子的位置、大小、旋转、色彩暂时都设为随机。
3.3. 编码梳理上面这个测试用的Sub,最终要修改成一个带参数的函数(Function),令其产生一个完整的设计作品,它的参数序列就是设计作品的编码。
我们先来看看,结合刚才的编码设计,哪些数据可以定义成变量,然后移到Function标题中作为参数。
1)概率密度
P_Elements这个数组可以从外部输入,函数改成这样:
Function MakeWallpaper(P_Elements)
P_Elements定义和赋值那句可以删除了。P_Elements是数组,可以不指定数据类型。
2)叶子的数量
我们是给了一个固定值,20个叶子,因此循环里的数字19可以定义成变量放在Function的参数表里,我们定义它为nLeaves:
Function MakeWallpaper(P_Elements, nLeaves As Integer)
循环中的19也改成nLeaves。
3)叶子的尺寸
定义叶子尺寸的语句是:r = 5 + Rnd * 15
这句涉及两个数字:5和15,即叶子在原尺寸的5倍到20倍(5+15)之间随机变化。我们把两个数字定义为一个数组rScale=Array(min,max):
Function MakeWallpaper(P_Elements, nLeaves As Integer, rScale)
里面的那句改为:
r=rScale(0)+Rnd*(rScale(1)-rScale(0))
注意,rScale给出的不是最小值(5)和增量(15),而是最小值和最大值(20),所以语句的写法要把增量15改成最大最小值的差:rScale(1)-rScale(0)。
r的设置是从大到小均匀的,如果希望大叶子少一点,小叶子多一点,可以给随机数Rnd加指数,变成:
r=rScale(0)+(Rnd)^3*(rScale(1)-rScale(0))
Rnd的指数3可以设计成编码放进参数表,不过本例没有这么做,感觉不是很有必要,这个参数的作用太细微了。
4)叶子密度
这个需要改的稍微多一点。刚才的程序是随机摆放叶子,而我们希望能稍稍体现一下设计感(即使只是一个练习),让叶子的分布有梳有密。我们用最简单的方式,在页面中设置一个“密度中心”,越靠近密度中心叶子的分布越密,反之稀疏。
Function MakeWallpaper(P_Elements, nLeaves As Integer, rScale,cDense)
密度中心是一个坐标位置:cDense=Array(x,y)
叶子位置的计算语句修改为:
x =Rnd * W y =Rnd * H p =math.PointConcentrate(x, y, cDense, 2, W, H) s1.PositionX= p(0) s1.PositionY= p(1)解释一下上面的代码:先在页面内随机生成一个点坐标(x,y),然后用math.PointConcentrate函数让这个点坐标向密度中心cDense靠拢。
PointConcentrate()函数写在math模块里,程序不长:
Function PointConcentrate(x As Double, y As Double, p, n As Integer, wPage As Double,hPage As Double) Dim px As Double, py As Double If x< p(0) Then px = p(0) - p(0) * ((p(0) - x) / p(0)) ^ n Else px = p(0) + (wPage - p(0)) * ((x - p(0)) /(wPage - p(0))) ^ n End If If y< p(1) Then py = p(1) - p(1) * ((p(1) - y) / p(1)) ^ n Else py = p(1) + (hPage - p(1)) * ((y - p(1)) /(hPage - p(1))) ^ n End If PointConcentrate= Array(px, py) End Function这段代码请自己理解。它的作用是让页面(宽wPage、高hPage)内的任意一个点(x,y)向点p点(密度中心)靠拢。但是有个条件:原来就在页面边缘上的点还得保持在边缘上,就是说,如果x=wPage或y=hPage,经过这么一番处理后,它们的值不变。
这个要求是为了保证修改密度后的页面不能出现真空地带,我们把页面的坐标系从均匀网格修改成了不均匀网格,有疏有密,但坐标系中不能出现破洞,坐标系的整体大小也不能发生变化。
参数n的作用是调整密度的大小,n越大,(x,y)向密度中心靠拢的越紧密。实际上n是一个指数,它作用于0~1之间的数时,输出仍是一个0~1之间的数,但n越大,输出的数越靠近0。想想看,0.1的平方是0.01,更靠近0了;但1的平方还是1,所以坐标系中不会出现“真空”,因为定义域和值域都没变。
这里没有把这个指数n列入设计作品的编码。
5)叶子颜色
如果我们需要色彩在某个色彩周围变化,可以直接给出一个色彩数组clrEle:
Function MakeWallpaper(P_Elements, nLeaves As Integer, rScale,cDense, clrEle)
为HSB模式的色彩定义相关变量:
Dim hh As Double, ss As Double, bb As Double
色彩在一个随机范围内变化,这个随机范围我们暂且设置为固定值,30%,暂不将其定义为参数。
“色彩”部分的程序做如下修改:
r =0.3 '变化范围:30%
ss =math.Rnd_AtoB(clrEle(1) * (1 - r), clrEle(1) + (255 - clrEle(1)) * r)
bb =math.Rnd_AtoB(clrEle(2) * (1 - r), clrEle(2) + (255 - clrEle(2)) * r)
hh =math.Rnd_AtoB(clrEle(0) - 360 * (r / 2), clrEle(0) + 360 * (r / 2))
If hh< 0 Then
hh = hh + 360
ElseIf hh > 360 Then
hh = hh - 360
End If
s1.Fill.ApplyUniformFill CreateHSBColor(hh, ss, bb)
hh、ss、bb是HSB的三个色彩分量,在前面要定义,它们的值域分别是360、255、255。
这段代码请自行理解。注意,HSB色彩模式中,H的计算方式与另外两个略有不同,因为H的值是个无缝的环状结构,如果它的值小于0或大于360度,要把它圈回去,而不是用算法强行将其限制在0~360之间(另外两个色彩分量可以这么做)。
函数Rnd_AtoB()是产生一个A和B之间的随机数,很简单,只有一句:
Function Rnd_AtoB(A As Double, B As Double) As Double
Rnd_AtoB= A + Rnd * (B - A)
End Function
6)背景色
给出一个确定的色彩数组clrBack:
Function MakeWallpaper(P_Elements, nLeaves As Integer, rScale,cDense, clrEle, clrBack)
7)其他
还有一些边边角角的工作要做。
这个Function的输出应该是一个完整的作品,是一个而不是一堆Shape。所以要把所有图形组合成一个整体,并且把背景边框之外的部分裁掉。都是比较简单的操作,就不详解了。
编码梳理完了,程序比原来稍微长了一些:
写个按钮Sub简单测试一下:
除了背景统一用黑色,叶子最大和最小尺寸比例固定,其他编码都是随机的。
给作品的前景部分增加了20%的透明度(未进入编码),因为大叶子会遮挡后面的小叶子。
看看作品,做了一组12个,这算一个种群:
上面12件作品是随机生成的。
一件作品中各叶子元素的明暗(HSB中的B)在一个固定点附近小幅变化,这导致了作品的层次感不强,要么整体亮,要么整体暗。针对这个问题,专门写了一个调整明暗层次的函数AiryPerspective(),我把它叫做“空气透视”,其实对黑背景的图案叫“景深效果”更好一些。
上图左下角的是12幅作品的第二幅,由于整体偏暗,所以用空气透视函数调理了一下。
上图的右下、右上、左上三图做了两方面调整:
1)把叶子从大到小调整顺序,大的在前面,小的在后面;就是说,大的会挡住小的;这是为了制造一种近大远小的透视印象;
2)调整各叶子HSB色彩中的B值,即亮度,近的亮,远的暗,这也是一种透视印象。
右下图的B值是从近到远均匀降低。右上图是则是陡降,所以亮的很少,大部分暗。左上图把参数均衡了下,近处亮的大片叶子多了一些,整体的层次感拉开了一点,有一种“水面水下”的视觉落差。
调序和景深参数未进入编码,是在MakeWallpaper()函数外面执行的,作品完成后手工修改。
至此,执行模块开发和设计作品的编码构造完成,共有15个参数,上面的设计方案用了其中10个,另外5个是固定的。
此外还有5个用到的参数本例暂未放入编码,它们是:尺寸分布指数、密度集聚指数、色彩变化范围、景深指数、透明度。如果都考虑进去,编码会有20个。
编码并不是越长越好。如果对某些参数的偏好明确,可以人工确定,比如背景色、画面亮度、元素大小等,就不要跑到算法里来添乱了。
搞定编码后,算法程序的其他内容都跟色彩IGA差不多了。
如果智能设计是一项需要设计师和程序员合作的工作,那么编码设计应该由设计师来完成。编码设计是智能设计的核心,一个优秀的编码方案并不是会写程序就能搞定的。