Genetic Algorithms with python 学习笔记ch7

Knights Problem

Knights Problem 要求使用最小数量的 knight 来攻击整个棋盘上的方块,因此棋盘至少是 3 * 4 的才能满足情况。knight能够攻击的范围如下所示。

image.png

其 genetic.py 的代码为:

import random
import statistics
import sys
import time


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(parent, geneSet, get_fitness):
    childGenes = parent.Genes[:]
    index = random.randrange(0, len(parent.Genes))
    newGene, alternate = random.sample(geneSet, 2)
    childGenes[index] = alternate if newGene == childGenes[index] else newGene
    fitness = get_fitness(childGenes)
    return Chromosome(childGenes, 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):
    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):
        display(improvement)
        if not optimalFitness > improvement.Fitness:
            return improvement


def _get_improvement(new_child, generate_parent):
    bestParent = generate_parent()
    yield bestParent
    while True:
        child = new_child(bestParent)
        if bestParent.Fitness > child.Fitness:
            continue
        if not child.Fitness > bestParent.Fitness:
            bestParent = child
            continue
        yield child
        bestParent = child


class Chromosome:
    def __init__(self, genes, fitness):
        self.Genes = genes
        self.Fitness = fitness


class Benchmark:
    @staticmethod
    def run(function):
        timings = []
        stdout = sys.stdout
        for i in range(100):
            sys.stdout = None
            startTime = time.time()
            function()
            seconds = time.time() - startTime
            sys.stdout = stdout
            timings.append(seconds)
            mean = statistics.mean(timings)
            if i < 10 or i % 10 == 9:
                print("{} {:3.2f} {:3.2f}".format(
                    1 + i, mean,
                    statistics.stdev(timings, mean) if i > 1 else 0))

上面的代码中函数 get_best 增加了一个参数 custom_create ,目的是利用自定义的创造函数产生父染色体。

KnightsTest.py 的完整代码为:

import datetime
import random
import unittest

import genetic


def get_fitness(genes, boardWidth, boardHeight):
   attacked = set(pos
                  for kn in genes
                  for pos in get_attacks(kn, boardWidth, boardHeight))
   return len(attacked)


def display(candidate, startTime, boardWidth, boardHeight):
   timeDiff = datetime.datetime.now() - startTime
   board = Board(candidate.Genes, boardWidth, boardHeight)
   board.print()

   print("{}\n\t{}\t{}".format(
       ' '.join(map(str, candidate.Genes)),
       candidate.Fitness,
       timeDiff))


def mutate(genes, boardWidth, boardHeight, allPositions, nonEdgePositions):
   count = 2 if random.randint(0, 10) == 0 else 1
   while count > 0:
       count -= 1
       positionToKnightIndexes = dict((p, []) for p in allPositions)
       for i, knight in enumerate(genes):
           for position in get_attacks(knight, boardWidth, boardHeight):
               positionToKnightIndexes[position].append(i)
       knightIndexes = set(i for i in range(len(genes)))
       unattacked = []
       for kvp in positionToKnightIndexes.items():
           if len(kvp[1]) > 1:
               continue
           if len(kvp[1]) == 0:
               unattacked.append(kvp[0])
               continue
           for p in kvp[1]:  # len == 1
               if p in knightIndexes:
                   knightIndexes.remove(p)

       potentialKnightPositions = \
           [p for positions in
            map(lambda x: get_attacks(x, boardWidth, boardHeight),
                unattacked)
            for p in positions if p in nonEdgePositions] \
               if len(unattacked) > 0 else nonEdgePositions

       geneIndex = random.randrange(0, len(genes)) \
           if len(knightIndexes) == 0 \
           else random.choice([i for i in knightIndexes])

       position = random.choice(potentialKnightPositions)
       genes[geneIndex] = position


def create(fnGetRandomPosition, expectedKnights):
   genes = [fnGetRandomPosition() for _ in range(expectedKnights)]
   return genes


def get_attacks(location, boardWidth, boardHeight):
   return [i for i in set(
       Position(x + location.X, y + location.Y)
       for x in [-2, -1, 1, 2] if 0 <= x + location.X < boardWidth
       for y in [-2, -1, 1, 2] if 0 <= y + location.Y < boardHeight
       and abs(y) != abs(x))]


class KnightsTests(unittest.TestCase):
   def test_3x4(self):
       width = 4
       height = 3
       # 1,0   2,0   3,0
       # 0,2   1,2   2,2
       # 2 	 N N N .
       # 1 	 . . . .
       # 0 	 . N N N
       #   	 0 1 2 3
       self.find_knight_positions(width, height, 6)

   def test_8x8(self):
       width = 8
       height = 8
       self.find_knight_positions(width, height, 14)

   def test_10x10(self):
       width = 10
       height = 10
       self.find_knight_positions(width, height, 22)

   def test_12x12(self):
       width = 12
       height = 12
       self.find_knight_positions(width, height, 28)

   def test_13x13(self):
       width = 13
       height = 13
       self.find_knight_positions(width, height, 32)

   def test_benchmark(self):
       genetic.Benchmark.run(lambda: self.test_10x10())

   def find_knight_positions(self, boardWidth, boardHeight, expectedKnights):
       startTime = datetime.datetime.now()

       def fnDisplay(candidate):
           display(candidate, startTime, boardWidth, boardHeight)

       def fnGetFitness(genes):
           return get_fitness(genes, boardWidth, boardHeight)

       allPositions = [Position(x, y)
                       for y in range(boardHeight)
                       for x in range(boardWidth)]

       if boardWidth < 6 or boardHeight < 6:
           nonEdgePositions = allPositions
       else:
           nonEdgePositions = [i for i in allPositions
                               if 0 < i.X < boardWidth - 1 and
                               0 < i.Y < boardHeight - 1]

       def fnGetRandomPosition():
           return random.choice(nonEdgePositions)

       def fnMutate(genes):
           mutate(genes, boardWidth, boardHeight, allPositions,
                  nonEdgePositions)

       def fnCreate():
           return create(fnGetRandomPosition, expectedKnights)

       optimalFitness = boardWidth * boardHeight
       best = genetic.get_best(fnGetFitness, None, optimalFitness, None,
                               fnDisplay, fnMutate, fnCreate)
       self.assertTrue(not optimalFitness > best.Fitness)


class Position:
   def __init__(self, x, y):
       self.X = x
       self.Y = y

   def __str__(self):
       return "{},{}".format(self.X, self.Y)

   def __eq__(self, other):
       return self.X == other.X and self.Y == other.Y

   def __hash__(self):
       return self.X * 1000 + self.Y


class Board:
   def __init__(self, positions, width, height):
       board = [['.'] * width for _ in range(height)]

       for index in range(len(positions)):
           knightPosition = positions[index]
           board[knightPosition.Y][knightPosition.X] = 'N'
       self._board = board
       self._width = width
       self._height = height

   def print(self):
       # 0,0 prints in bottom left corner
       for i in reversed(range(self._height)):
           print(i, "\t", ' '.join(self._board[i]))
       print(" \t", ' '.join(map(str, range(self._width))))


if __name__ == '__main__':
   unittest.main()

其中计算适应值的函数如下,适应值由棋盘上被攻击的方块数决定。

def get_fitness(genes, boardWidth, boardHeight):
  attacked = set(pos
                 for kn in genes
                 for pos in get_attacks(kn, boardWidth, boardHeight))
  return len(attacked)

为了缩小求解空间,我们自定义了函数 mutate。

def mutate(genes, boardWidth, boardHeight, allPositions, nonEdgePositions):
  count = 2 if random.randint(0, 10) == 0 else 1
  while count > 0:
      count -= 1
      positionToKnightIndexes = dict((p, []) for p in allPositions)
      for i, knight in enumerate(genes):
          for position in get_attacks(knight, boardWidth, boardHeight):
              positionToKnightIndexes[position].append(i)
      knightIndexes = set(i for i in range(len(genes)))
      unattacked = []
      for kvp in positionToKnightIndexes.items():
          if len(kvp[1]) > 1:
              continue
          if len(kvp[1]) == 0:
              unattacked.append(kvp[0])
              continue
          for p in kvp[1]:  # len == 1
              if p in knightIndexes:
                  knightIndexes.remove(p)

      potentialKnightPositions = \
          [p for positions in
           map(lambda x: get_attacks(x, boardWidth, boardHeight),
               unattacked)
           for p in positions if p in nonEdgePositions] \
              if len(unattacked) > 0 else nonEdgePositions

      geneIndex = random.randrange(0, len(genes)) \
          if len(knightIndexes) == 0 \
          else random.choice([i for i in knightIndexes])

      position = random.choice(potentialKnightPositions)
      genes[geneIndex] = position

首先,有十分之一的可能性突变两次,防止陷入局部最优解。
在突变之前,先统计所有被攻击的方块并保存在键值对 positionToKnightIndexes 中,键为位置,值为攻击这个位置的所有骑士的列表。
接着将所有的未被攻击的位置存储在 unattacked 中,所有已经攻击其他方块一次的knight的序号从 knightIndexs 中删除。
如果未被攻击的方块数>0,则 potentialKnightPositions 为能够攻击untackked方块的并且还是非边界的位置。否则 potentialKnightPositions 为非边界的位置。
如果 knightIndexes 长度不为0,则随机选择其中一个进行突变。如果为0,则在genes长度中选择一个索引进行突变。

posted @ 2020-08-09 12:22  idella  阅读(184)  评论(0编辑  收藏  举报