V posledních dnech všichni čekají na novou mutaci koronáče, britského, jihoafrického, či z jiného koutu světa. Rozhodl jsem se, že už čekat nebudu, a zamutuji si sám. Navíc jsem si u toho i něco pokřížil …
V rámci dřívějšího řešení Sudoku pomocí hrubé síly mne napadlo, zda by nešly použít pro řešení také genetické algoritmy.
Ty se používají pro hledání optimálních řešení u složitých úloh. Nejsou mně schopny zajistit to nejlepší řešení, ale mohly by najít alespoň nějaké.
Zkoušel jsem hledat na internetu a našel jsem několik pokusů o použití GA pro řešení Sudoku. Lidé je používali pro řešení konkrétních zadání, nebo pro generování nových.
Jako inspiraci jsem si vzal tohle: Solving, Rating and Generating Sudoku Puzzles with GA
Jedno řešení Sudoku jsem se rozhodl reprezentovat pomocí dvourozměrné matice o stranách 9×9. Vzhledek k tomu, že budu hledat řešení pro konkrétní zadání, musím zajistit, aby zadaná pole zůstala vždy na svém místě. Proto je součástí reprezentace jednoho řešení také maska o stejném rozměru, která obsahuje boolean hodnotu, zda je pozice fixní.
Podstatná otázka pro implementaci GA je, jakým způsobem budu provádět křížení dvou řešení a jak mutaci. Vyšel jsem z metody doporučené ve výše uvedeném textu, a sice takto:
Těmito pravidly bude zajišťeno, že na úrovni podčtverců budu mít vždy správný počet čísel. Optimalizace se bude hledat na úrovni řádků a sloupců.
Pro implementaci dvourozměrného pole jsem použil numpy.
Nejdříve nějaké základní definice, abych měl z čeho vycházet:
import numpy as np
import random
random.seed(0)
WIDTH = 9
GRID = WIDTH // 3
POPULATION_SIZE = 6
MAX_ITERATION = 10000
MUTATE_PROBABILITY = 0.5
SAMPLE = '003870509060032180049651230016200053050006072072305010200768000000020700730510000'
def grid(a, i):
j, k = (i // GRID) * GRID, (i % GRID) * GRID
return a[j:j+GRID, k:k+GRID]
Vzorek pro řešení je zadán jako řetězec SAMPLE (jedná se o zřetězené řádky, kdy neznámé pozice jsou reprezentovány 0).
Funkce grid je pomocná. Vybere mně jeden podčtverec s rozměrem 3×3 z celkové matice.
Reprezentuje jedno řešení Sudoku. Zahrnuje matici s konkrétním řešením a matici masky fixních pozic.
class Solution:
def __init__(self, data, mask):
self._data = data
self._mask = mask
def __lt__(self, other):
return id(self) < id(other)
def data(self):
return self._data
def clone(self):
return Solution(np.copy(self._data), self._mask)
def fitness(self):
return sum([WIDTH - len(set(self._data[i, :])) for i in range(WIDTH)]) + sum([WIDTH - len(set(self._data[:, i])) for i in range(WIDTH)])
def mutate(self, probability):
def mutate_grid(i):
g = grid(self._data, i)
t = np.where(grid(self._mask, i))
if len(t[0]) >= 2 and random.random() < probability:
u, v = random.randrange(len(t[0])), random.randrange(len(t[0]))
x1, y1 = t[0][u], t[1][u]
x2, y2 = t[0][v], t[1][v]
g[x1, y1], g[x2, y2] = g[x2, y2], g[x1, y1]
for g in range(WIDTH):
mutate_grid(g)
return self
Tahle metoda mně spočítá, jak „dobré“ je dané řešení. Jedná se o součet všech konfliktů na řádcích a ve sloupcích. Čím větší hodnota vyjde, tím horší řešení to je.
V případě, že vyjde fitness == 0, pak jsem našel správné řešení zadaného Sudoku.
Jejím účelem je trochu zmutovat stávající řešení.
Projdu tedy postupně všechny podčtverce a zmutuji každý zvláště.
V rámci jednoho podčtverce hledám ty pozice, které mohu přesouvat (podle masky), a ty pak vzájemně vyměním. Vše je ještě omezeno pravděpodobností, takže mutace vznikají jen někdy.
Jedná se o pomocnou třídů pro inicializaci populace GA.
Ze zadaného vzorku Sudoku vytvoří jedno řešení, které následně mutuje, a tím dodává další řešení pro naplnění populace.
class SolutionGenerator:
def __init__(self, sample):
a = np.array([int(i) for i in sample]).reshape(WIDTH, WIDTH)
m = np.logical_not(np.array(a, dtype=bool))
all_nums = set(range(1, WIDTH+1))
for gi in range(WIDTH):
g = grid(a, gi)
missing = all_nums - set(g.flatten())
places = np.where(g == 0)
for i, v in enumerate(missing):
g[places[0][i], places[1][i]] = v
self._init = Solution(a, m)
def __iter__(self):
return self
def __next__(self):
self._init = self._init.clone().mutate(0.5)
return self._init
No a tady se již dostávám k jádru řešení, tedy k implementaci vlastního genetického algoritmu.
Nejdříve zdrojová třída, a pak nějaké komentáře k řešení:
class GA:
PARENTS = 2
def __init__(self, population_size, solution_generator):
self.population_size = population_size
self.population = []
while len(self.population) < population_size:
solution = next(solution_generator)
self.population.append((solution.fitness(), solution))
def termination_criteria(self):
return [s for f, s in self.population if f == 0]
def select_parents(self):
res = []
coefficient = sum([1/f for f, s in self.population])
while len(res) < self.PARENTS:
threshold = random.random()
fitness = 0.0
for f, s in self.population:
fitness += 1 / f / coefficient
if fitness >= threshold and s not in res:
res.append(s)
break
return res
@staticmethod
def crossover(parents):
def swap_grid(g1, g2):
for r in range(g1.shape[0]):
for c in range(g1.shape[1]):
g1[r, c], g2[r, c] = g2[r, c], g1[r, c]
child_1 = parents[0].clone()
child_2 = parents[1].clone()
for i in range(0, WIDTH, 2):
swap_grid(grid(child_1.data(), i), grid(child_2.data(), i))
return [child_1, child_2]
def reduce_population(self):
while len(self.population) > self.population_size:
max_fitness = max([f for f, s in self.population])
remove_index = [i for i, p in enumerate(self.population) if p[0] == max_fitness][0]
self.population.pop(remove_index)
def run(self, max_iterations):
for iteration in range(max_iterations):
solutions = self.termination_criteria()
if solutions:
return solutions
for child in GA.crossover(self.select_parents()):
child.mutate(MUTATE_PROBABILITY)
self.population.append((child.fitness(), child))
self.reduce_population()
else:
best = sorted(self.population)[0]
print(f"BEST FITNESS: {best[0]}")
return []
Do konstruktoru předám požadovanou velikost populace a generátor, kterým je možné si populaci naplnit.
Vlastní populace je reprezentována jako pole dvojic: <fitness, solution>
Metoda vyhodnotí, jestli některé řešení z populace nevyhovuje konečnému řešení. Hledám tedy ta řešení, která mají fitness == 0. Pokud se nějaká taková najdou, pak se vráci jako seznam těchto řešení.
Ta mně má najít rodiče, které budu vzájemně křížit. V tomto řešení hledám 2 rodiče. Jako metodu jsem použil „Roulette Wheel Selection“ s pravděpodobností výběru řešení odpovídající jejímu fitness. Výsledkem volání je seznam vybraných rodičů.
Toto je implementace křížení dvou rodičů.
Parametrem jsou vybraní rodiče. Vytvořím dva potomky, u nichž jsou křížově prohozeny jejich podčtverce. Výsledkem metody jsou dva noví potomci.
Po křížení mně přibudou v populaci dvě nová řešení. Aby se mně populace nerozrůstala, provedu její redukci a vyhodím nejhorší řešení dle jejich fitness.
Toto je výkonné tělo GA.
Jako parametr dostane maximální počet iterací, které jsem ochoten provést. Pokud se v rámci těchto iterací nepodaří najít řešení, běh GA končí s tím, že řešení nenašel.
Postup lze shrnout takto:
A to je vše. Takto to vypadá, pokud to celé spustím:
sols = GA(POPULATION_SIZE, SolutionGenerator(SAMPLE)).run(MAX_ITERATION)
if sols:
for s in sols:
print(s.data())
else:
print("NO SOLUTION FOUND")
[[1 2 3 8 7 4 5 6 9] [5 6 7 9 3 2 1 8 4] [8 4 9 6 5 1 2 3 7] [9 1 6 2 8 7 4 5 3] [3 5 8 1 4 6 9 7 2] [4 7 2 3 9 5 8 1 6] [2 9 5 7 6 8 3 4 1] [6 8 1 4 2 3 7 9 5] [7 3 4 5 1 9 6 2 8]]
Zkoušel jsem trochu experimentovat se složitostí Sudoku a parametry, které je potřeba pro jeho vyřešení.
Vytvořil jsem si sady po čtyřech zadáních se 40, 50 a 60 neznámými políčky.
Zkoušel jsem nastavit parametry:
Takhle to vypadalo. Jednotlivé skupiny testů je třeba odkomentovat.
random.seed(0)
# 60 holes
SAMPLES = [
'000800060567930004000001030916000000008000000002300016000700005000000000000000020',
'000000200000030900060000003180000000645000820003100000000000009090004030000903080',
'000000000003008000000000048438000012075000000009001005060007003080050000050800000',
'000000040043000100000001203004000890009840000000000000501900008400000007000010050',
]
POPULATION_SIZE = 20
MAX_ITERATION = 200000
MUTATE_PROBABILITY = 0.4
# 50 holes
# SAMPLES = [
# '023004569007000000000051230910047003058096000072000906001760045000000000700500000',
# '500000040020000950060020170180540007600307021900000005350008000700014000010050704',
# '006204307040000000002630108400006910005002800020001075004020000000453690001060000',
# '002050040900600000075400200150230006369800700200060500501000068400020900000300002',
# ]
# POPULATION_SIZE = 20
# MAX_ITERATION = 200000
# MUTATE_PROBABILITY = 0.4
# 40 holes
# SAMPLES = [
# '023004509560932084000051037900000853050196000402305006090000000005023701734509600',
# '031700000427800950000420003002506397045300801070180465000270610098014030200900004',
# '000090350500710260702635140408076010175042806600000000964020580000400091050060024',
# '800000600940680070000491083150207006309805000280109534501974300030506007090010050',
# ]
# POPULATION_SIZE = 6
# MAX_ITERATION = 50000
# MUTATE_PROBABILITY = 0.4
for s in SAMPLES:
sols = GA(POPULATION_SIZE, SolutionGenerator(s)).run(MAX_ITERATION)
if sols:
for s in sols:
print(s.data())
else:
print("NO SOLUTION FOUND")
print("-" * 22)
[[1 4 3 8 5 7 2 6 9] [5 6 7 9 3 2 1 8 4] [2 8 9 4 6 1 5 3 7] [9 1 6 2 4 5 3 7 8] [3 7 8 1 9 6 4 5 2] [4 5 2 3 7 8 9 1 6] [6 3 1 7 2 4 8 9 5] [8 2 5 6 1 9 7 4 3] [7 9 4 5 8 3 6 2 1]] ---------------------- [[7 3 8 6 5 9 2 1 4] [4 5 1 7 3 2 9 6 8] [9 6 2 8 4 1 7 5 3] [1 8 9 4 2 5 3 7 6] [6 4 5 3 9 7 8 2 1] [2 7 3 1 6 8 4 9 5] [3 2 7 5 8 6 1 4 9] [8 9 6 2 1 4 5 3 7] [5 1 4 9 7 3 6 8 2]] ---------------------- [[8 9 2 7 6 4 5 3 1] [5 4 3 1 2 8 7 9 6] [7 1 6 3 9 5 2 4 8] [4 3 8 5 7 9 6 1 2] [1 7 5 6 3 2 4 8 9] [6 2 9 4 8 1 3 7 5] [9 6 1 2 4 7 8 5 3] [2 8 4 9 5 3 1 6 7] [3 5 7 8 1 6 9 2 4]] ---------------------- [[9 1 7 3 8 2 5 4 6] [2 4 3 5 6 7 1 8 9] [6 8 5 4 9 1 2 7 3] [7 6 4 1 2 3 8 9 5] [1 3 9 8 4 5 7 6 2] [8 5 2 6 7 9 4 3 1] [5 7 1 9 3 4 6 2 8] [4 9 8 2 5 6 3 1 7] [3 2 6 7 1 8 9 5 4]] ----------------------
U všech jsem nakonec našel nějaké řešení s výjimkou jednoho vzorku s 50-ti děrami. Tam se řešení najít nepovedlo, ani když jsem nafoukl počet iterací.
A to je zatím vše.
Připojuji se k poděkování za článek o GA.
BTW: Když se mi před cca 15 roky dostalo pod ruku první sudoku, tak jsem si na to hned udělal v Pythonu malý prográmek a od té doby už jsem neměl potřebu sudoku počítat z hlavy.
Ten můj prográmek to řeší tak, jak by to řešil člověk.
Tedy hledá, číslo, které může určit se 100% jistotou, že do prázdného místa patří.
Pokud takové nenajde, tak hledá pozici, do které pasuje nejméně číslic, postupně je dosadí a pokračuje dál, jako by to byly dvě rozdílné zadání.
U těch těžších sudoku, kde už toho chybí hodně občas najde víc než jedno správné řešení.
Hlavní kód obstarávají 2 funkce na cca 20 řádků.
U těch těžších sudoku, kde už toho chybí hodně občas najde víc než jedno správné řešení.
Byl by příklad takového zadání? Podle mne správně zadané Sudoku je takové, které vede na právě jedno řešení.
pracuje na pozici IT architekta. Poslední roky se zaměřuje na integrační a komunikační projekty ve zdravotnictví. Mezi jeho koníčky patří také paragliding a jízda na horském kole.
Přečteno 28 519×
Přečteno 27 950×
Přečteno 25 939×
Přečteno 23 790×
Přečteno 19 532×