การค้นหาเส้นทาง Genetic Algorithm
เพื่อให้คุ้นเคยกับคลาสใน Python มากขึ้นฉันได้เขียนอัลกอริทึมทางพันธุกรรมซึ่งใช้ระดับโดยมีจุดเริ่มต้นและจุดสิ้นสุดและค้นหาเส้นทาง (ไม่จำเป็นต้องเป็นเส้นทางที่ดีที่สุด) ผลลัพธ์จะแสดงระดับพื้นฐานและเมื่อพบโซลูชันระดับที่มีเส้นทาง:
Level:
############
O....#.....#
#.#.#.#.#..#
#........#.O
############
Solution:
############
O*...#.****#
#*#*#*#*#**#
#********#**
############
ฉันสนใจที่จะปรับปรุงโครงสร้างของโค้ด (เช่นไม่ใช่ของอัลกอริทึมเองก็ต่อเมื่อมีข้อผิดพลาด) เนื่องจากฉันต้องการปรับปรุงความรู้ทั่วไปเกี่ยวกับการเขียนโปรแกรมใน Python
มีปัญหาบางอย่างที่ฉันทราบ:
- พารามิเตอร์ในตอนต้นสามารถเขียนเป็น enums ได้ แต่ฉันไม่สามารถมั่นใจได้ว่าตัวเองจะได้เปรียบอะไร (นอกเหนือจากการสร้างมลพิษให้กับเนมสเปซทั่วโลก) ฉันคิดว่าวิธีการเขียน "N" หรือ "WALL" ที่กระชับกว่าแทนที่จะเป็น เพิ่ม "Direction.N" หรือ "Object.Wall" เพื่อให้อ่านโค้ดได้ง่ายขึ้น
- คลาส "ระดับ": โดยหลักการแล้วฉันต้องการให้แอตทริบิวต์เป็นแบบอ่านอย่างเดียว แต่ฉันไม่แน่ใจว่าจะกำหนดอย่างไรให้ถูกต้อง นอกจากนี้ฉันไม่เห็นประเด็นในการเขียน getters และ setters ที่นี่
- ในคลาสเดียวกันฉันไม่ต้องการเขียน __move_dict และ __text_map สองครั้งใน test_route และ print_route ดังนั้นฉันจึงกำหนดเป็นตัวแปรคลาส ฉันไม่แน่ใจว่านี่เป็นสำนวนหรือไม่
- ในทำนองเดียวกัน test_route และ print_route ใช้รหัสเดียวกัน ฉันคิดว่ามันจะเป็นไปได้ไหมที่จะแยกวงรอบทั่วไปออกไป แต่ฉันไม่รู้ว่าจะทำอย่างไรใน Python
""""Simple implementation of a genetic algorithm:
Searching for a possible route from a given start point
to an end point."""
import random
from dataclasses import dataclass
from typing import List
from collections import namedtuple
from operator import attrgetter
# PARAMETERS
# direction constants
N = 0
E = 1
S = 2
W = 3
# level constants
EMPTY = 0
WALL = 1
DOOR = 2
L1 = [[WALL, WALL, WALL, WALL, WALL, WALL, WALL, WALL, WALL, WALL, WALL, WALL],
[DOOR, EMPTY, EMPTY, EMPTY, EMPTY, WALL, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, WALL],
[WALL, EMPTY, WALL, EMPTY, WALL, EMPTY, WALL, EMPTY, WALL, EMPTY, EMPTY, WALL],
[WALL, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, WALL, EMPTY, DOOR],
[WALL, WALL, WALL, WALL, WALL, WALL, WALL, WALL, WALL, WALL, WALL, WALL]]
L1_WIDTH = 12
L1_HEIGHT = 5
# DATATYPES
Point = namedtuple("Point", "x y")
@dataclass
class Level:
"""Class for representing a level with a start and end point."""
map: list
width: int
height: int
start: Point
end: Point
__move_dict = {N: Point(0, 1),
E: Point(1, 0),
S: Point(0, -1),
W: Point(-1, 0)}
__text_map = {WALL: "#", EMPTY: ".", DOOR: "O"}
def test_route(self, genome):
"""Test a route encoded in a genome and return the final distance to the exit."""
def distance(point_a, point_b):
return abs(point_a.x - point_b.x) + abs(point_a.y - point_b.y)
position = self.start
for gene in genome.genes:
delta = self.__move_dict[gene]
new_pos = Point(position.x + delta.x,
position.y + delta.y)
if 0 <= new_pos.x < self.width:
if 0 <= new_pos.y < self.height:
if self.map[new_pos.y][new_pos.x] != WALL:
position = new_pos
if position == self.end:
break
return 1 / (1 + distance(position, self.end))
def print_level(self):
"""Print a text representation of a level."""
for row in self.map:
print("".join((self.__text_map[elem] for elem in row)))
def print_route(self, genome):
"""Print the route through the level."""
text_level = []
for row in self.map:
text_level.append([self.__text_map[elem] for elem in row])
position = self.start
for gene in genome.genes:
delta = self.__move_dict[gene]
new_pos = Point(position.x + delta.x,
position.y + delta.y)
if 0 <= new_pos.x < self.width:
if 0 <= new_pos.y < self.height:
if self.map[new_pos.y][new_pos.x] != WALL:
position = new_pos
text_level[new_pos.y][new_pos.x] = "*"
if position == self.end:
break
for row in text_level:
print("".join(row))
@dataclass
class Genome:
"""Class for representing the genome of running through a level."""
fitness: float
genes: List[int]
class GenomePool:
"""Class implementing the genetic algorithm."""
def __init__(self, level, pool_size, num_genes, crossover_rate, mutation_rate):
self.__level = level
self.__pool_size = pool_size
self.__num_genes = num_genes
self.__crossover_rate = crossover_rate
self.__mutation_rate = mutation_rate
self.__pool = [Genome(0, [random.randint(0, 3) for i in range(0, num_genes)])
for _ in range(self.__pool_size)]
self.__update_fitness()
def __select_genome(self):
"""Do a roulette wheel selection and return a genome."""
total_fitness = sum((genome.fitness for genome in self.__pool))
cut = random.uniform(0, total_fitness)
partial_fitness = 0
idx = 0
while partial_fitness < cut:
partial_fitness += self.__pool[idx].fitness
idx += 1
return self.__pool[idx] if idx < len(self.__pool) else self.__pool[self.__pool_size - 1]
def __crossover(self, mother, father):
"""Do a crossover of two genomes and return an offspring."""
if random.random() > self.__crossover_rate:
return mother
crossover_point = int(random.uniform(0, self.__num_genes))
offspring = Genome(0, [])
offspring.genes = mother.genes[0:crossover_point] + father.genes[crossover_point:]
return offspring
def __mutate(self, genome):
for i in range(self.__num_genes):
if random.random() < self.__mutation_rate:
genome.genes[i] = int(round(random.uniform(0, 3)))
def __update_fitness(self):
"""Update the fitness score of each genome."""
for genome in self.__pool:
genome.fitness = self.__level.test_route(genome)
def get_best_genome(self):
"""Return the genome with the best fitness."""
sorted_pool = sorted(self.__pool, key=attrgetter("fitness"), reverse=True)
return sorted_pool[0]
def run(self, verbose=False):
"""Run the genetic algorithm until a solution has been found."""
iteration = 0
while all((x.fitness != 1 for x in self.__pool)):
if verbose:
best_fitness = self.get_best_genome().fitness
print(f"Iteration {iteration}: Best fitness = {best_fitness}")
iteration += 1
self.step()
def step(self):
"""Run one time step of the evolution."""
new_pool = []
for i in range(self.__pool_size):
mother = self.__select_genome()
father = self.__select_genome()
offspring = self.__crossover(mother, father)
self.__mutate(offspring)
new_pool.append(offspring)
self.__pool = new_pool
self.__update_fitness()
def main():
level_one = Level(L1, L1_WIDTH, L1_HEIGHT, start=Point(0, 1),
end=Point(11, 3))
print("Level:")
level_one.print_level()
genome_pool = GenomePool(level_one, pool_size=30, num_genes=70,
crossover_rate=0.7, mutation_rate=0.01)
genome_pool.run()
print()
print("Solution:")
level_one.print_route(genome_pool.get_best_genome())
if __name__ == "__main__":
main()
คำตอบ
คำตอบสำหรับคำถามของคุณ
พารามิเตอร์ในตอนต้นสามารถเขียนเป็น enums ได้ แต่ฉันไม่สามารถมั่นใจได้ว่าตัวเองจะได้เปรียบอะไร (นอกเหนือจากการสร้างมลพิษให้กับเนมสเปซทั่วโลก) ฉันคิดว่าวิธีการเขียน "N" หรือ "WALL" ที่กระชับกว่าแทนที่จะเป็น เพิ่ม "Direction.N" หรือ "Object.Wall" เพื่อให้อ่านโค้ดได้ง่ายขึ้น
โดยทั่วไป Enums เป็นความคิดที่ดีเนื่องจากมีคุณสมบัติที่ดี โดยเฉพาะอย่างยิ่งพวกเขาอยู่ในคลาสที่แตกต่างกันและคุณไม่สามารถเปรียบเทียบ enum กับสิ่งที่ไม่ใช่ enum โดยบังเอิญได้ ยกตัวอย่างเช่นในรหัสของคุณทั้งสองEและWALLเป็นเพียง1เพื่อE == WALLจะส่งผลTrueที่ไม่ได้เป็นสิ่งที่คุณคาดหวัง ดังนั้นฉันจะใช้ enums ที่นี่อย่างแน่นอน
ตอนนี้คุณคิดถูกแล้วที่การใช้ enums ทำให้เกิดรหัส verbose มากขึ้น แต่คุณยังสามารถสร้างตัวแปรด้วยชื่อสั้น ๆ ที่คุณกำหนด enums ให้และได้รับสิ่งที่ดีที่สุดจากทั้งสองโลก ตัวอย่างเช่น:
class Tile(enum.Enum):
EMPTY = 0
WALL = 1
DOOR = 2
EMPTY = Tile.EMPTY
WALL = Tile.WALL
DOOR = Tile.DOOR
L1 = [[WALL, WALL, ...], [DOOR, EMPTY, ...], ...]
โปรดทราบว่า enums ใน Python ไม่ต้องการให้คุณมีค่าตัวเลขคุณสามารถทำสิ่งต่อไปนี้:
class Direction(enum.Enum):
N = Point(0, 1)
E = Point(1, 0)
S = Point(0, -1)
W = Point(-1, 0)
class Tile(enum.Enum):
EMPTY = "."
WALL = "#"
DOOR = "O"
นี้จากนั้นจะหลีกเลี่ยงความจำเป็นสำหรับและ__move_dict__text_map
คลาส "ระดับ": โดยหลักการแล้วฉันต้องการให้แอตทริบิวต์เป็นแบบอ่านอย่างเดียว แต่ฉันไม่แน่ใจว่าจะกำหนดอย่างไรให้ถูกต้อง นอกจากนี้ฉันไม่เห็นประเด็นในการเขียน getters และ setters ที่นี่
ดูคำถามนี้เพื่อหาคำตอบที่เป็นไปได้
ในคลาสเดียวกันฉันไม่ต้องการเขียน __move_dict และ __text_map สองครั้งใน test_route และ print_route ดังนั้นฉันจึงกำหนดเป็นตัวแปรคลาส ฉันไม่แน่ใจว่านี่เป็นสำนวนหรือไม่
นี่มันสมบูรณ์ดี! การหลีกเลี่ยงการทำซ้ำเป็นสิ่งสำคัญมากในการทำให้โค้ดของคุณรัดกุมและดูแลรักษาได้
ในทำนองเดียวกัน test_route และ print_route ใช้รหัสเดียวกัน ฉันคิดว่ามันจะเป็นไปได้ไหมที่จะแยกวงรอบทั่วไปออกไป แต่ฉันไม่รู้ว่าจะทำอย่างไรใน Python
คุณสามารถสร้างเครื่องกำเนิดไฟฟ้าที่วนซ้ำบนเส้นทางและแต่ละจุดจะให้ตำแหน่งของจุดนั้น จากนั้นคุณสามารถใช้สิ่งนั้นเพื่อลดความซับซ้อนของลูปเข้าtest_route()และทำprint_route()ดังนี้:
def visit_route(self):
...
for gene in genome.genes:
...
position = new_pos
yield position
def test_route(self, genome):
last_position = self.start
for position in self.visit_route():
last_position = position
return 1 / (1 + distance(last_position, self.end))
def print_route(self):
text_level = [[self.__text_map[elem] for elem in row] for row in self.map]
for position in self.visit_route():
text_level[position.y][position.x] = "*")
for row in text_level:
print ("".join(row))
หลีกเลี่ยงการจัดเก็บข้อมูลที่ซ้ำซ้อน
คุณclass Levelร้านค้าwidthและheightแต่ข้อมูลนี้มีอยู่แล้วในmap: heightควรจะเท่ากับlen(map)และควรจะเท่ากับwidth len(map[0])แม้ว่าบางครั้งอาจมีเหตุผลในการเก็บสำเนาข้อมูลที่มีราคาแพงในการคำนวณ แต่ข้อเสียคือคุณต้องแน่ใจว่าข้อมูลมีความสอดคล้องกัน จะเกิดอะไรขึ้นถ้าฉันสร้างLevel([[EMPTY]], 100, 100)?
ในทำนองเดียวกันจะเกิดอะไรขึ้นถ้าstart_pointและend_pointไม่ตรงกับตำแหน่งที่DOORอยู่ในmap? อันนี้อาจจะยุ่งยากกว่า พิจารณาการสร้างตัวสร้างสำหรับclass Levelการตรวจสอบว่าไม่ว่าพารามิเตอร์ที่กำหนดมีความสอดคล้องหรือมีมันจะได้รับมาwidth, height, start_pointและจากend_pointmap
การเป็นตัวแทน
############
O....#.....#
#.#.#.#.#..#
#........#.O
############
ฉันจะพบว่าการแสดงระดับนี้สามารถอ่านได้มากขึ้นหากพื้นที่ว่างถูกพิมพ์เป็นช่องว่าง แทนที่จะเป็น.
############
O # #
# # # # # #
# # O
############
ในโค้ดฉันจะใช้การแทนค่าเดียวกันกับอินพุตของโปรแกรมดังนั้นแทนที่จะเป็นแบบนี้
L1 = [[WALL, WALL, WALL, WALL, WALL,
คุณสามารถกำหนด
L1 = [
"############",
"O # #",
"# # # # # #",
"# # O",
"############",
]
จากนั้นคุณจะปล่อยให้ฟังก์ชันบางอย่างแปลสิ่งนั้นเป็นตรรกะภายในที่คุณต้องการสำหรับอัลกอริทึมของคุณ
ฉันจะเปลี่ยนสัญลักษณ์สำหรับเส้นทางที่เดินทางจาก*ไปเป็นอย่างอื่นที่ง่ายต่อการแยกแยะจาก#ผนังที่ใช้ บางทีอาจเปลี่ยนผนังด้วย
รหัส
if 0 <= new_pos.x < self.width:
if 0 <= new_pos.y < self.height:
if self.map[new_pos.y][new_pos.x] != WALL:
position = new_pos
สิ่งนี้ไม่ผิด แต่โดยปกติจะเขียนโดยใช้andแทน ifs ที่ซ้อนกันหลาย ๆ อันเมื่อคุณไม่จำเป็นต้องใช้elseเคสหรือตัวเลือกอื่น ๆ