解方程在實際應用中是非常常見的問題——相信每個同學在讀書的時候都給數學方程給折磨過。與數學領域的解方程不同,數學領域可以直接用分數或者無理數來表達,比如:
但是在計算機領域,就必須表達為:
import math
a = math.sqrt(2)
b = 2/3
print("a = {} b = {} a*b = {}".format(a,b,a*b))
# a = 1.4142135623730951 b = 0.6666666666666666 a*b = 0.9428090415820634從上面可以看見,計算機對於循環小數或者無理數,採取的是精度截取的方式來實現,所以計算機裡面得到的永遠是近似值。
回到解方程裡面,與上面的計算一樣,數學裡面的解方程可以得到表達式組合,而計算機裡面最終會給出近似值來,所以計算機解方程與(中學)數學原理是不太一樣的。
對於解方程的方法,一般有兩種:
第一種稱之為數學方法,其中包括了:
LU分解(LU Factorization)
高斯消元法
QR(正交三角)分解法
第二種就是所謂迭代法,也就是計算機常用的方法,而我們要介紹的遺傳算法解方程,也屬於迭代法中的一種。
在本節中,我們將看到,遺傳算法如何來解線性方程組。
比如我們現在有這樣一組方程:
x + 2y = 4
4x + 4y = 12用高斯消元法,很容易就能算出來:
方程2 - (方程1*2)得:2x = 4 解得x = 2 帶入得 y = 1
同樣的Python裡面也有包括numpy和scipy這樣的專業計算工具包,可以來用解方程:
import numpy
A = numpy.array([[1,2],[4,4]])
B = numpy.array([4, 12])
inv = numpy.linalg.inv(A).dot(B)
print("x1 = {} x2 = {}".format(inv[0],inv[1]))
# x1 = 2.0 x2 = 1.0
算法實現前面是通用方法,不解釋了,大家看以前的文章:本節用了一個Python的內部包fration(分數包),這個包可以實現直接保留為分數,而不會計算成浮點數,比如:
# 三分之一乘以10
# 傳統情況下,會直接轉化為浮點數:
print(1/3 * 10.0)
# 3.333333333333333
#用fration包,可以保留分數計算
print(Fraction(1,3) * 10)
# 10/3import random,datetime
from bisect import bisect_left
from math import exp
from fractions import Fraction
# 初始化初代種群的方法
def _generate_parent(length, geneSet, get_fitness):
genes = []
while len(genes) < length:
sampleSize = min(length - len(genes), len(geneSet))
genes.extend(random.sample(geneSet, sampleSize))
fitness = get_fitness(genes)
return Chromosome(genes, fitness)
# 封裝的進化方法
def _mutate_custom(parent, custom_mutate, get_fitness):
childGenes = parent.Genes[:]
custom_mutate(childGenes)
fitness = get_fitness(childGenes)
return Chromosome(childGenes, fitness)
#進化執行
def get_best(get_fitness, targetLen, optimalFitness, geneSet, display,
custom_mutate=None, custom_create=None, maxAge=None):
if custom_mutate is None:
def fnMutate(parent):
return _mutate(parent, geneSet, get_fitness)
else:
def fnMutate(parent):
return _mutate_custom(parent, custom_mutate, get_fitness)
if custom_create is None:
def fnGenerateParent():
return _generate_parent(targetLen, geneSet, get_fitness)
else:
def fnGenerateParent():
genes = custom_create()
return Chromosome(genes, get_fitness(genes))
for improvement in _get_improvement(fnMutate, fnGenerateParent, maxAge):
display(improvement)
if not optimalFitness > improvement.Fitness:
return improvement
def _get_improvement(new_child, generate_parent, maxAge):
parent = bestParent = generate_parent()
yield bestParent
historicalFitnesses = [bestParent.Fitness]
while True:
child = new_child(parent)
if parent.Fitness > child.Fitness:
if maxAge is None:
continue
parent.Age += 1
if maxAge > parent.Age:
continue
index = bisect_left(historicalFitnesses, child.Fitness, 0,
len(historicalFitnesses))
proportionSimilar = index / len(historicalFitnesses)
if random.random() < exp(-proportionSimilar):
parent = child
continue
bestParent.Age = 0
parent = bestParent
continue
if not child.Fitness > parent.Fitness:
# same fitness
child.Age = parent.Age + 1
parent = child
continue
child.Age = 0
parent = child
if child.Fitness > bestParent.Fitness:
bestParent = child
yield bestParent
historicalFitnesses.append(bestParent.Fitness)
#種群類
class Chromosome:
def __init__(self, genes, fitness):
self.Genes = genes
self.Fitness = fitness
self.Age = 0下面是健壯性驗證的方法:
把每個迭代出來的未知數的值,帶入到方程裡面,如果等於0,這表示通過:
把x + 2y = 4 寫成 x + 2y - 4 = 0這樣的模式,如果帶入未知數,最後等於0則表示通過。
def get_fitness(genes, equations):
fitness = Fitness(sum(abs(e(genes)) for e in equations))
return fitness顯示方法,不解釋:
def display(candidate, startTime, fnGenesToInputs):
timeDiff = datetime.datetime.now() - startTime
symbols = "xyza"
result = ', '.join("{} = {}".format(s, v)
for s, v in
zip(symbols, fnGenesToInputs(candidate.Genes)))
print("{}\t{}\t{}".format(
result,
candidate.Fitness,
timeDiff))進化方法:首先選擇一個基因進行變化,然後通過改變窗口的大小的方法,不斷嘗試進行基因替換。每次通過循環,從選擇的索引中提取下一個基因,並使用窗口大小計算出需要搜索範圍。這樣,就可以限制可以挑選的基因數量,如果到達數組的一端,就停止。最後,用從新的邊界範圍內隨機抽取的一個基因取代當前的基因。
def mutate(genes, sortedGeneset, window, geneIndexes):
indexes = random.sample(geneIndexes, random.randint(1, len(genes))) \
if random.randint(0, 10) == 0 else [random.choice(geneIndexes)]
window.slide()
while len(indexes) > 0:
index = indexes.pop()
genesetIndex = sortedGeneset.index(genes[index])
start = max(0, genesetIndex - window.Size)
stop = min(len(sortedGeneset) - 1, genesetIndex + window.Size)
genesetIndex = random.randint(start, stop)
genes[index] = sortedGeneset[genesetIndex]使用窗口,主要是為了讓我們在尋找數據的時候,儘量接近目前要替換的基因,比如要替換的是個正數,那麼替換它的也儘可能是一個正數,反之亦然。
這樣我們就可以慢慢改變當前最佳基因周圍的搜索範圍。這使得算法能夠比較容易的找到下一個最優的改進可能。
兩個常規類:健壯性類和窗口類:
class Fitness:
def __init__(self, totalDifference):
self.TotalDifference = totalDifference
def __gt__(self, other):
return self.TotalDifference < other.TotalDifference
def __str__(self):
return "diff: {:0.2f}".format(float(self.TotalDifference))
class Window:
def __init__(self, minimum, maximum, size):
self.Min = minimum
self.Max = maximum
self.Size = size
def slide(self):
self.Size = self.Size - 1 if self.Size > self.Min else self.Max下面就是解題的過程了,一個封裝了一堆內部方法的方法:
def solve_unknowns(numUnknowns, geneset, equations,fnGenesToInputs):
startTime = datetime.datetime.now()
maxAge = 50
window = Window(max(1, int(len(geneset) / (2 * maxAge))),
max(1, int(len(geneset) / 3)),
int(len(geneset) / 2))
geneIndexes = [i for i in range(numUnknowns)]
sortedGeneset = sorted(geneset)
def fnDisplay(candidate):
display(candidate, startTime, fnGenesToInputs)
def fnGetFitness(genes):
return get_fitness(genes, equations)
def fnMutate(genes):
mutate(genes, sortedGeneset, window, geneIndexes)
optimalFitness = Fitness(0)
best = get_best(fnGetFitness, numUnknowns, optimalFitness,
geneset, fnDisplay, fnMutate, maxAge=maxAge)然後是接二元一次方程:
用遺傳算法解方程,最大的難題,就是在於如何構建基因庫。比如我們這裡做二元一次方程,設計的基因就是從-5到5這10個整數。如果你的解不在基因庫裡面,那是肯定解不出來的。
def test_2_unknowns():
geneset = [i for i in range(-5, 5) if i != 0]
def fnGenesToInputs(genes):
return genes[0], genes[1]
def e1(genes):
x, y = fnGenesToInputs(genes)
return x + 2 * y - 4
def e2(genes):
x, y = fnGenesToInputs(genes)
return 4 * x + 4 * y - 12
equations = [e1, e2]
solve_unknowns(2, geneset, equations, fnGenesToInputs)運行結果如下:
x = 1, y = -5 diff: 41.00 0:00:00
x = 3, y = -3 diff: 19.00 0:00:00
x = 4, y = -3 diff: 14.00 0:00:00
x = 4, y = -1 diff: 2.00 0:00:00.000999
x = 1, y = 2 diff: 1.00 0:00:00.002992
x = 2, y = 1 diff: 0.00 0:00:00.018948同樣,我們可以簡單的改一改,比如把方程改成:
30x + 15y = 600
15x + 20y = 200那麼初始化解題的方法就要改成:
首先我們不確定基因庫範圍的時候,給一個比較高的(這裡還是已經知道接肯定是整數,否則也不行) 之後改兩個方程的構建。
def test_2_unknowns2():
geneset = [i for i in range(-100,100) if i != 0]
def fnGenesToInputs(genes):
return genes[0], genes[1]
def e1(genes):
x, y = fnGenesToInputs(genes)
return 30 *x + 15 * y - 600
def e2(genes):
x, y = fnGenesToInputs(genes)
return 15 * x + 20 * y - 200
equations = [e1, e2]
solve_unknowns(2, geneset, equations, fnGenesToInputs)
test_2_unknowns2()運行結果:
x = 22, y = -26 diff: 720.00 0:00:00
x = 27, y = -26 diff: 495.00 0:00:00.001004
x = 45, y = -26 diff: 405.00 0:00:00.001004
x = 42, y = -26 diff: 360.00 0:00:00.001004
x = 41, y = -26 diff: 345.00 0:00:00.001004
x = 32, y = -26 diff: 270.00 0:00:00.001004
x = 14, y = 5 diff: 215.00 0:00:00.001004
x = 14, y = -1 diff: 205.00 0:00:00.001993
x = 20, y = -1 diff: 95.00 0:00:00.001993
x = 20, y = -2 diff: 90.00 0:00:00.001993
x = 20, y = -3 diff: 85.00 0:00:00.002998
x = 21, y = -3 diff: 70.00 0:00:00.002998
x = 21, y = -5 diff: 60.00 0:00:00.002998
x = 22, y = -5 diff: 45.00 0:00:00.002998
x = 23, y = -6 diff: 25.00 0:00:00.009972
x = 23, y = -7 diff: 20.00 0:00:00.018961
x = 24, y = -8 diff: 0.00 0:00:00.135640下面這個帶有分數三元一次方程:
6x - 2y + 8z = 20
y + 8x * z = -1
2z * 6/x + 3y/2 = 6構造初始化方法如下:
構建的解是從-5/4 到 5/4
不要問我如果真實未知,不可能知道範圍怎麼辦……這裡的作用是給大家說明算法,不是真實使用的方法,千萬別再真實工作中去使用這種方法來解方程,沒意義
def test_3_unknowns():
geneRange = [i for i in range(-5, 5) if i != 0]
geneset = [i for i in set(
Fraction(d, e)
for d in geneRange
for e in geneRange if e != 0)]
def fnGenesToInputs(genes):
return genes
def e1(genes):
x, y, z = genes
return 6 * x - 2 * y + 8 * z - 20
def e2(genes):
x, y, z = genes
return y + 8 * x * z + 1
def e3(genes):
x, y, z = genes
return 2 * z * Fraction(6, x) \
+ 3 * Fraction(y, 2) - 6
equations = [e1, e2, e3]
solve_unknowns(3, geneset, equations, fnGenesToInputs)執行結果如下:
x = 2/5, y = -4, z = -3/4 diff: 55.50 0:00:00
x = 2/5, y = -3, z = -3/4 diff: 55.00 0:00:00.000995
x = -3/5, y = -3, z = -3/4 diff: 29.70 0:00:00.000995
x = -3/5, y = -5, z = -3/4 diff: 21.50 0:00:00.001963
x = -2/3, y = -5, z = -3/4 diff: 20.00 0:00:00.005951
x = -3/5, y = -5, z = -2/3 diff: 19.90 0:00:00.019914
x = -1/4, y = -4, z = -1/4 diff: 18.00 0:00:00.036870
x = -1/5, y = -4, z = -1/5 diff: 17.48 0:00:00.045844
x = 1/2, y = -4/5, z = 1/5 diff: 17.20 0:00:00.059808
x = 1/3, y = -4/5, z = 1/5 diff: 15.53 0:00:00.059808
x = 1/3, y = -5/4, z = 1/5 diff: 14.86 0:00:00.061280
x = 1/3, y = -3/2, z = 1/5 diff: 14.48 0:00:00.063252
x = 5/3, y = -3/2, z = 1/5 diff: 14.38 0:00:00.067241
x = 3/4, y = -5, z = 3/4 diff: 2.50 0:00:00.068236
x = 3/5, y = -5, z = 3/4 diff: 2.30 0:00:00.071230
x = 3/5, y = -5, z = 2/3 diff: 2.03 0:00:00.076215
x = 2/3, y = -5, z = 3/4 diff: 0.00 0:00:00.092175當然, 你還可以改成四元甚至五元方程,關鍵點在於你要能夠構建合理的基因庫,否則遺傳算法就沒有辦法使用了。
總結:遺傳算法需要構建良好的基因庫,也需要有很好的終止條件,如果這兩個前提做不到的話,遺傳算法就沒辦法使用。