X36PAA, Úloha #6 - Problém vážené splnitelnosti booleovské formule

Vypracoval Michal Turek, cvičení Po 12:45-14:15 (Petr Fišer)


Zadání

Je dána booleovská formule F proměnnných X=(x1, x2, ... , xn) v konjunktivní normální formě (tj. součin součtů). Dále jsou dány celočíselné kladné váhy W=(w1, w2, ... , wn). Najděte ohodnocení Y=(y1, y2, ... , yn) proměnných x1, x2, ... , xn tak, aby F(Y)=1 a součet vah proměnných, které jsou ohodnoceny jedničkou, byl maximální.

Je přípustné se omezit na formule, v nichž má každá formule právě 3 literály (problém 3 SAT). Takto omezený problém je stejně těžký, ale možná se lépe programuje a lépe se posuzuje obtížnost instance (viz Selmanova prezentace v odkazech).

Poznámka

Obdobný problém, který má optimalizační kritérium ve tvaru "aby počet splněných klausulí byl maximální" a kde váhy se týkají klausulí, se také nazývá problém vážené splnitelnosti booleovské formule. Tento problém je lehčí a lépe aproximovatelný. Oba problémy se často zaměňují i v seriózní literatuře.

Příklad

x1' značí negaci x1.

n = 4
F = (x1 + x3' + x4).(x1' + x2 + x3').(x3 + x4).(x1 + x2 + x3' + x4').(x2' + x3).(x3' + x4')
W = (2, 4, 1, 6)

Přípustné konfigurace, kde F=1 (řešení):

X = {x1 ... xn} = {0, 0, 0, 1}, S = 6
X = {x1 ... xn} = {1, 0, 0, 1}, S = 2 + 6 = 8 (optimální)
X = {x1 ... xn} = {1, 1, 1, 0}, S = 2 + 4 + 1 = 7

Tato instance v DIMACS CNF formátu

c Priklad CNF
c 4 promenne a 6 klauzuli
c kazda klauzule konci nulou (ne novym radkem)
p cnf 4 6
1 -3 4 0
-1 2 -3 0
3 4 0
1 2 -3 -4 0
-2 3 0
-3 -4 0

Řešení

Popis genetického algoritmu

Problém vážené splnitelnosti booleovské formule byl řešen pomocí genetického algoritmu, jehož pseudokód vypadá následovně. (Kompletní zdrojové kódy v jazyce C++: main.cpp, csat.hpp, csat.cpp, Makefile)

RunGA()
{
	GenerateFirstPopulation();
	EvaluatePopulation();

	while(!Done())
	{
		CopyTwoBestToTheNewPopulation();

		for(PopulationSize())
		{
			a = Tournament(population);
			b = Tournament(population);

			if(rand() < CrossoverProbability())
				Crossover(a, b);

			if(rand() < MutateProbability())
				Mutate(a);

			if(rand() < MutateProbability())
				Mutate(b);

			new_population.add(a);
			new_population.add(b);
		}

		Swap(population, new_population);
		EvaluatePopulation();
	}
}

Počáteční populace je generována náhodně, a proto počáteční ohodnocení jednotlivých proměnných většinou není řešením logické formule. Hledat řešení hned na začátku by bylo velice složité a mohlo by také způsobit pravděpodobnější uváznutí v lokálních extrémech. Tento problém řeší heuristická funkce, která upřednostňuje ve výsledcích řešení formule - za každou splněnou klauzuli, popř. při splnění celé formule, přidává bonusové body.

Chování genetického algoritmu je možné modifikovat následujícími konfiguračními parametry. Uvedené hodnoty jsou nástřelem po několika počátečních spuštěních.

m_clause_bonus 		= 1000;		// Bonus za splněnou klauzuli
m_formula_bonus		= 20000;	// Bonus za splněnou formuli
m_num_generations	= 500;		// Počet generací
m_population_size	= 100;		// Velikost populace
m_tournament_size	= 5;		// Počet testů v turnaji
m_cross_probability	= 0.8;		// Pravděpodobnost křížení
m_mutation_probability	= 0.03;		// Pravděpodobnost mutací
m_try_improve_solution	= true;		// Ručně vylepšovat nalezené řešení
m_num_clauses		= 70;		// Počet klauzulí, omezení složitosti (viz dále)

Testovací data

Pro testování byla použita data uf20-91 (Uniform Random-3-SAT, phase transition region) ze SATLIB - Benchmark Problems. Jedná se o testovací instance s 20 proměnnými a 91 klauzulemi, všechny z nich jsou splnitelné. Je nutné poznamenat, že implementace není omezená pouze na 3-SAT, ale poradila by si i se SAT instancemi.

Následuje příklad spuštění genetického algoritmu a získání případných výsledků.

[woq@woq sat]$ head uf20-01.cnf
c This Formular is generated by mcnf
c
c    horn? no
c    forced? no
c    mixed sat? no
c    clause length = 3
c
p cnf 20  91
 4 -18 19 0
3 18 -5 0
[woq@woq sat]$ ./sat uf20-01.cnf
[woq@woq sat]$

Pokud existuje soubor s váhami, načítají se z něj, v opačném případě se váhy vygenerují náhodně v rozsahu 0-100 a jsou uloženy do souboru .wgt pro následující spuštění.

[woq@woq sat]$ cat uf20-01.cnf.wgt
15 58 65 6 34 38 47 93 14 48 33 63 51 20 76 26 40 98 33 78
[woq@woq sat]$

V případě, že genetický algoritmus nalezne nějaké řešení, připojí ho na konec souboru s příponou .sol. Opakující se výsledky mohou být později odstraněny pomocí standardních systémových utilit, skript sort_uniq_sol.sh takto umí zpracovat všechny .sol soubory v daném adresáři. První položka ve výsledcích představuje počet klauzulí (kvůli zjednodušování instancí), dále následuje suma vah a konfigurace promměnných. Všechny datové soubory, včetně dosažených výsledků, je možné najít v adresáři data_uf20-91/.

[woq@woq sat]$ head uf20-01.cnf.sol
91    332     10000100100011101001
91    372     10010100010011101001
91    662     01110001111001101111
91    662     01110001111001101111
91    662     01110001111001101111
91    662     01110001111001101111
91    427     10010001010011101001
91    427     10010001010011101001
91    372     10010100010011101001
91    662     01110001111001101111
[woq@woq sat]$ sort -nr uf20-01.cnf.sol | uniq
91    662     01110001111001101111
91    427     10010001010011101001
91    372     10010100010011101001
91    332     10000100100011101001
[woq@woq sat]$

Testování

Ke zdrojovým kódům byla přidána další třída, která dědí ze třídy řešící SAT problém a která bude oddělovat kód pro testy od vlastního kódu genetického algoritmu (csattest.hpp, csattest.cpp).

V následujících testech bude genetický algoritmus spuštěn vždy padesátkrát pro každou konfiguraci parametrů (nad rozdílnými instancemi), do grafů se zanáší vždy aritmetický průměr z těchto padesáti hodnot. Ve zprávě budou uvedeny pouze vynesené grafy, tabulky hodnot, včetně formátovacích souborů pro gnuplot, je možné najít v adresáři graphs/.

Snížení složitosti instancí problému

Jelikož jsou originální testovací instance problému ze SATLIB výrazně obtížné (poměr počtu klauzulí a proměnných odpovídá 3-SAT fázovému přechodu), je pro genetický algoritmus relativně složité najít vůbec nějaké řešení formule, natož optimální vzhledem k váhám proměnných. Následující graf ukazuje, že při vyšším počtu klauzulí není genetický algoritmus schopen v mnoha případech najít vůbec žádné řešení. Například pro 91 klauzulí našel řešení pouze u 15 z celkového počtu 50 spuštění.

Závislost nalezení (nějakého) řešení formule na počtu klauzulí

Kvůli výše uvedenému důvodu bude počet klauzulí během testování omezen z původních 91 na 70. Při nastavení této hodnoty je řešení problému nalezeno během cca. 80% spuštění.

Omezení počtu populací

Počet generací je nastavený pevně v algoritmu, a proto je důležité hned na začátku ověřit, zda je dostatečný. Z grafu je vidět, že kvalita do cca. 100 generací prudce roste, což je způsobeno bonusy za plnění jednotlivých klauzulí a celé formule a dále se již výrazně nemění.

Vývoj kvality populace při běhu algoritmu

Popravdě, na tomto místě jsem chtěl s tímto testem skončit, ale jenom pro zajímavost jsem zkusil nastavit počet generací z původních 500 na 5000. "Schody" v grafu jsou způsobeny tím, že se průměrují výsledky z padesáti spuštění a ne všechny formule jsou hned ze začátku splněny.

Každý "schod" odpovídá nalezení řešení některé z dosud nesplněných formulí a je tak velký proto, že heuristika připočítává bonus 20000 bodů za splněnou formuli. Tato hodnota je dostatečně velká, aby se projevila i při průměrování. Aby byly všechny instance splněné, musel by se graf pohybovat okolo hodnoty 90000, což je výsledek ze vzorce 70 klauzulí * 1000 bonus za splněnou klauzuli + 20000 bonus za splněnou formuli.

Suma vah proměnných je proti nim zanedbatelná a nejvyšší by činila max. 20 proměnných * max. cena 100 = 2000, v reálu se jí ale nedosáhne a bude se většinou pohybovat mnohem níže.

Vývoj kvality populace při běhu algoritmu

Možná by bylo vhodné počet generací dále zvyšovat, což by nebyl problém pro jeden běh algoritmu nad jednou instancí. Nicméně testování průměruje výsledky 50 instancí a kdyby se zkoumalo například 10 hodnot nějakého parametru, jednalo by se hned o 500 spuštění, což je už za hranicí únosnosti. Je možné, že vhodným nastavením genetických parametrů (vyšší pravděpodobnost mutací?) bude možné počet generací v budoucnu snížit. Pro testy se tedy prozatím spokojíme pouze s 5000 generacemi.

Má smysl zkoušet ručně vylepšovat nalezené řešení?

V případě, že je nalezeno nějaké řešení formule, implementace umožňuje, aby bylo dále ručně vylepšeno. To spočívá v tom, že se dočasně zkusí nastavit hodnota proměnné ve stavu false na true a přetestuje se, zda je formule i nadále splněná. Pokud ano, nalezli jsme řešení, jehož váha je vyšší než původní.

Při zapnutých testech, trval běh 50 instancí 3m 20.633s, při vypnutých pouze 1m 40.806s, což je přibližně polovina. Z hlediska doby výpočtu je tedy výhodnější, aby byly dodatečné testy vypnuté.

Má smysl zkoušet ručně vylepšit nalezené řešení?

Ne, ve výše uvedeném grafu opravdu není chyba. Ruční vylepšování populace má za následek nejen dvojnásobně delší dobu výpočtu, ale v průběhu výpočtu také výrazně degeneruje jednotlivé populace. Tento parametr by tedy neměl být nikdy zapnutý!

Po dalších spuštěních jsem maličko na rozpacích. Výsledky rozhodně nejsou tak jednoznačné, jako vypadaly ze začátku. Je mi jasné, že tři spuštění nejsou statistiky významné, ale zdá se, že příliš nezáleží, zda je parametr nastaven na true nebo na false. Do budoucna ho necháme vypnutý.

Má smysl zkoušet ručně vylepšit nalezené řešení? Druhý pokus.
Má smysl zkoušet ručně vylepšit nalezené řešení? Třetí pokus.

Velikost populace

Z vyneseného grafu je na první pohled vidět, že při malých velikostech populace kvalita silně kolísá a dočasně dochází k degeneraci. Je to způsobeno především vysokou hodnotou počtu jedinců v turnaji - tyto dva parametry jsou silně závislé.

Velikost populace

Zkusíme stejný graf ještě jednou, ale pouze pro jeden běh jediné instance bez průměrování hodnot, navíc se zaznamenávají pouze hodnoty z každé desáté generace.

Rozdílná poloha grafu pro hodnotu 16 je způsobena tím, že nebylo nalezeno řešení formule - připomínám, že bonus za její splnění je 20000 a tudíž nejspíše pro nalezení řešení chyběla pouze jedna nebo dvě klauzule. Nízká poloha v tomto grafu nesignalizuje selhání algoritmu, jak by na první pohled mohlo vypadat. Problémy s hodnotami 16, 32 a 64 jsou ale vidět z grafu s průměrováním.

Velikost populace

A ještě jednou, tentokrát se zaznamenává každá padesátá generace. Je vidět, že ani 64 jedinců nemusí stačit, řešení se zde nalezlo až u 2500. generace.

Na tomto grafu jsou však spíše zajímavá nalezená řešení problému ve smyslu maximalizování vah proměnných, ty dosud na žádném grafu nebyly vidět. Rozdíl "poloha grafu mínus 90000 získaných na bonusech" je hodnotou sumy vah proměnných. Pro přesná čísla je třeba nahlédnout do souboru uf20-01.cnf.sol. A ještě drobná poznámka, křivka v grafu je průměrem za celou populaci v dané generaci, nejedná se o konkrétního jedince.

70      915     11111111011101000111
70      909     11111101011101000111
70      884     11110011111101011101
70      869     10111111011101000111
70      863     10111101011101000111
70      857     01111001111101001111
70      853     11110011111100011101
...
Velikost populace

Pravděpodobnost mutací

Z grafu je vidět, že 3% pravděpodobnost mutací byla odhadnuta velice dobře. Nižší hodnoty způsobují uvíznutí v lokálních extrémech, vyšší naopak degeneraci populace.

Pravděpodobnost mutací (50 spuštění)
Pravděpodobnost mutací (1 spuštění)

Pravděpodobnost křížení

Zdá se, že tento konfigurační parametr není u této úlohy až tak významný. Hodnoty 80% a 100% pravděpodobnosti křížení dávají téměř stejné výsledky, 60% je znatelně horší. Při nižších hodnotách se zvyšuje elitismus - rodiče jsou pouze kopírovány do nově vytvářené populace - což nemusí mít vůbec špatné důsledky.

Pravděpodobnost křížení (50 spuštění)
Pravděpodobnost křížení (1 spuštění)

Počet testů v turnaji

Tento graf je pro mě hodně velkým překvapením. Jelikož je velikost populace rovna 100, očekával bych optimum mezi hodnotami 5 až 10. Jednoznačně nejhorší způsob selekce je náhodný výběr jedinců (počet testů = 0), což se dalo očekávat. Nicméně jsem rozhodně nečekal, že nejlépe vyjde turnaj s jedním testem, tj. náhodně se vyberou dva jedinci a vrácen je lepší z nich.

Všechny předchozí testy se vykonávaly s velikostí turnaje 5, a je tedy velká šance, že mohly být takto špatně nastavenou hodnotou výrazně ovlivněny. Je zde vidět, že například počet generací by mohl být výrazně nižší a nižší by možná mohla být i velikost populace - tím by se vykonávání výrazně urychlilo. Vysoký počet testů v turnaji měl nejspíše za následek zvýšenou degeneraci.

Závislost kvality populací na počtu testů v turnaji (50 spuštění)
Závislost kvality populací na počtu testů v turnaji (1 spuštění)

Závěr

V této práci byl implementován genetický algoritmus pro řešení vážené splnitelnosti booleovské formule. Testovací instance byly převzaty ze SATLIB benchmarku, váhy proměnných byly vygenerovány náhodně. Veškerá testovací data včetně dosažených výsledků je možné najít v adresářích data_uf20-91/ a graphs/.

Na začátku byla řešena otázka s jak složitými instancemi testovat nastavení genetického algoritmu. Počet klauzulí byl omezen na 70, s touto hodnotou je nalezeno (nějaké) řešení v cca. 80% případech. Na začátku to bylo považováno za rozumně zvolený kompromis, nicméně při zpětném pohledu zjišťuji, že tím byl průběh testů relativně negativně ovlivněn. V současnosti bych volil počet klauzulí 60, zde je možné v podstatě všechny instance bez problémů vyřešit, a více bych se soustředil místo na hledání řešení na hledání maximálního řešení.

Během testů bylo zjištěno, že 500 populací genetickému algoritmu při dané konfiguraci parametrů (viz počet testů v turnaji) rozhodně nestačí, hodnota by měla být alespoň o řád vyšší. Dále bylo testováno, zda je výhodné ručně vylepšovat nalezená řešení, toto tvrzení prokázáno nebylo a jelikož dodatečné testy zpomalují vykonávání algoritmu, byly vypnuty.

Počet jedinců v populaci byl na začátku definován na hodnotu 100. Experimenty ukázaly, že se nejedná o výrazně špatnou hodnotu, ale ani o výrazně dobrou - 256 by bylo pro běh algoritmu o něco lepší. Na druhou stranu velikost populace výrazně ovlivňuje rychlost vykonávání, a proto byla - jako kompromis - ponechána na původní hodnotě. Do budoucna by bylo vhodné nastavovat tento parametr dynamicky v závislosti na složitosti instance (počet klauzulí a proměnných).

Jedním z opravdu dobře odhadnoutých parametrů je pravděpodobnost mutací. Hodnota 3% podává jednoznačně nejlepší výsledky. Pravděpodobnost křížení byla odhadnuta také relativně dobře, nicméně hodnoty blížící se ke 100% by nejspíš fungovaly lépe. Pro další testy bych zkusil 95%.

Počet testů v turnaji byl na začátku odhadnut velice špatně. Graf trochu překvapivě ukazuje, že nejlépe vychází pouze jeden test a vyšší hodnoty způsobují degeneraci. S novou hodnotou by šlo snížit velikost populace a určitě i počet generací, nejspíše by byla ovlivněna i pravděpodobnost mutací a změn by jistě doznaly i ostatní parametry.

m_clause_bonus 		= 1000;		// Bonus za splněnou klauzuli
m_formula_bonus		= 20000;	// Bonus za splněnou formuli
m_num_generations	= 5000;		// Počet generací (původně 500)
m_population_size	= 100;		// Velikost populace
m_tournament_size	= 1;		// Počet testů v turnaji (původně 5)
m_cross_probability	= 0.95;		// Pravděpodobnost křížení (původně 0,8)
m_mutation_probability	= 0.03;		// Pravděpodobnost mutací
m_try_improve_solution	= false;	// Ručně vylepšovat nalezené řešení (původně true)
m_num_clauses		= 60;		// Počet klauzulí, omezení složitosti (původně 70)

Všechny programy byly napsány v jazyce C++ s použitím šablon ze standardní knihovny STL, optimalizace g++ byly nastaveny na -O2. Program byl spouštěn na počítači s procesorem AMD Athlon 1800++ pod operačním systémem Debian Etch GNU/Linux. Doba vykonávání byla měřena pomocí standardního programu time (položka user).