|
Tags: Applicatiebeheer | Java | Software Development
|
|
Constraint Satisfaction Problemen Oplossen met Java
Heb je ooit geprobeerd een vergadering te plannen voor een groep personen met drukke agenda’s, een kruiswoord- of Sudoku-puzzel geprobeerd op te lossen, of geprobeerd om schaarse resources te koppelen aan activiteiten? Zo ja, dan heb je, bewust of onbewust, te maken gehad met een Constraint Satisfaction Probleem (CSP). Deze problemen vergen een bepaalde mate van intelligentie om adequaat opgelost te kunnen worden, en kunnen niet automatisch opgelost worden middels reguliere algoritmes binnen een acceptabele tijd. Dit artikel presenteert een efficiënte manier om dergelijke problemen op te lossen en geeft aan hoe dit in Java geïmplementeerd kan worden.Constraint Satisfaction Problemen – Een Voorbeeld Commerciële Haven Een mogelijkheid om een goed of optimaal rooster te vinden is ‘generate and test’: genereer alle mogelijke permutaties van schepen en aanlegplaatsen, selecteer hieruit de oplossingen die geldig zijn, bepaal de bijbehorende kosten van elke geselecteerde oplossing, en selecteer tot slot de beste (e.g., goedkoopste) oplossing. Binnen dit voorbeeld betekent dit dat 510 (ongeveer 10.000.000) alternatieven geëvalueerd moeten worden indien de genoemde brute force aanpak gebruikt wordt. Indien de computer die gebruikt wordt om dit probleem op te lossen bijvoorbeeld een alternatief per milliseconde kan evalueren, dan zal het probleem binnen ongeveer 3 uur opgelost worden. Business Is Booming – En De Technologie?Stel nu, 10 jaar later, dat de zaken goed gaan, en de situatie zich ontwikkeld heeft tot 20 schepen en 10 aanlegplaatsen. Het vinden van een optimaal rooster betekent nu dat 1020 alternatieven geëvalueerd moeten worden, wat resulteert in een rekentijd van ongeveer 3.000.000.000 jaar. Uiteraard kan een 10 keer krachtigere processor aangeschaft worden, waardoor de rekentijd gereduceerd wordt tot een redelijkere 300.000.000 jaar ;-) De duidelijke conclusie is dat dergelijke brute force algoritmes niet werken voor dit type problemen. Het transformeren van het genoemde brute force algoritme naar bijvoorbeeld een uitgebreider backtracking algoritme zal de situatie ietwat verbeteren, maar niet significant. Een andere, wellicht aantrekkelijke doch naïeve strategie omvat het verdelen van de haven in twee segmenten, en ieder segment apart te roosteren. Dit zal resulteren in een rekentijd van ongeveer 6 uur (twee maal de eerdergenoemde 3 uur), wat de enorme impact van het aantal variabelen op dit type problemen schetst. Echter, feit is dat de oplossingen binnen deze aanpak verre van optimaal zullen zijn en, nog erger, men weet niet hoe ver van optimaal, en hoeveel geld dus verkwist wordt. Men kan net zo goed het 3.000.000.000-jaar-programma voor 6 uur laten draaien, en de goedkoopste oplossing uit de gegenereerde solution space selecteren. Het resultaat zal hoogst waarschijnlijk van dezelfde (lage) kwaliteit zijn. Constraint Satisfaction Problemen – Overzicht Algemeen Vanuit een complexiteitsoogpunt zijn al deze problemen grotendeels gelijk. Wat ze met elkaar gemeen hebben is het feit dat ze enorm grote search spaces (zoekruimtes) hebben, die combinatorisch groeien naarmate het aantal variabelen toeneemt – deze karakteristieke eigenschap van CSP’s wordt ‘combinatorial explosion’ genoemd. Deze problemen zijn zogenaamde NP-complete problemen, en kunnen in het algemeen niet binnen een acceptabele tijd opgelost worden met traditionele (e.g., brute force) algoritmes, ook niet met krachtige processoren. Formele DefinitieEen Constraint Satisfaction Probleem bestaat uit een verzameling variabelen X = {x1,...,xn}, voor elke variabele xi een domein Di dat de mogelijke waarden voor die variabele bevat, alsmede een verzameling constraints Ci tussen variabelen die beperkingen oplegt aan de waarden die de variabelen kunnen aannemen. Op basis hiervan kan bijvoorbeeld het volgende eenvoudige CSP gedefinieerd worden: variabelen V = {X, Y, Z}, domeinen DX = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, DY = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, DZ = {0, 1, 2, 3, 4, 5, 6,7, 8,9, 10}, constraints C1 = [X == 4], C2 = [X < Y], C3 = [Y > Z]. Alhoewel dit CSP erg eenvoudig is, verschilt het in essentie niet van het eerdere voorbeeld omtrent havenroosters. Dit eerste voorbeeld bevat slechts meer variabelen, domeinen en constraints, waardoor het omvangrijker is. OplossingenEen oplossing van een CSP voldoet aan het volgende: iedere variabele is geïnitialiseerd met een waarde uit zijn domein, en aan iedere constraint wordt voldaan. Hierbij kan het doel zijn om slechts één (random) oplossing te vinden, alle oplossingen, of een optimale oplossingen (met betrekking tot een kostenfunctie die gerelateerd is aan de waarden die de variabelen van het CSP hebben). Een mogelijke oplossing van het voorgenoemde CSP is: X = 4, Y = 6, Z = 5. Gezien de beperkte omvang kan dit CSP zonder geavanceerdere algoritmes worden, en zal een basale brute force aanpak afdoende zijn. Dit is echter niet het geval bij een real-world CSP, waarbij het aantal variabelen en constraints vele malen groter is. Het probleem bij real-world CSP’s bevindt zich altijd in de aantallen, waarbij, mede vanwege combinatorial explosion, een brute force aanpak niet zal werken. Hetzelfde geldt voor backtracking en andere systematische zoekalgoritmes. Backtracking is een efficiëntere manier van zoeken dan brute force, en is in feite niets anders dan depth-first search. Gedurende het zoeken (i.e., toekennen van waarden aan variabelen, zonder dat constraints geschonden worden), zal het algoritme terugkeren (‘backtracken’) naar het vorige keuzepunt in de zoekboom wanneer een ongeldige oplossing is gevonden, en zal het volgende alternatief bij dat keuzepunt geprobeerd worden. Wanneer deze alternatieven geprobeerd zijn en geen oplossing is gevonden, zal het algoritme wederom terugkeren naar het vorige (hoger gelegen) keuzepunt en daar het volgende alternatief proberen. Indien er geen keuzepunten meer zijn en geen oplossing gevonden is, heeft de zoekactie gefaald. Alhoewel deze aanpak efficiënter is dan generate-and-test, is het verre van adequaat om real-world CSP’s op te lossen binnen een acceptabele tijd. Constraint Satisfaction Problems Oplossen Overzicht Systematic Search Consistency Techniques Constraint Propagation Een backtracking-algoritme instantieert de variabelen van een CSP stuk voor stuk, waarbij de waarden uit de domeinen van die variabelen gebruikt worden. Hierbij wordt incrementeel getest of aan alle constraints tot dusver voldaan wordt. Wanneer een constraint geschonden wordt (als gevolg van een ongeldige instantiëring van een variabele), dan zal het backtracking-algoritme dit detecteren op het moment dat het gebeurt, en zal het algoritme terugkeren (backtracken) naar het voorgaande keuzepunt in de zoekboom waar alles nog OK was. Vanaf dat keuzepunt gaat het algoritme een andere tak van de zoekboom in, en dit proces gaat zo door totdat alle variabelen geïnstantieerd zijn en geen constraints geschonden zijn. Hoewel deze basale strategie veel efficiënter is dan de volledige brute-force generate-and-test aanpak, is het probleem dat schendingen van constraints pas gedetecteerd worden wanneer ze plaatsvinden, waarna het algoritme zichzelf moet herstellen door naar een vorige toestand van de zoekruimte terug te keren, en vervolgens een alternatieve route geprobeerd moet worden. Als gevolg hiervan moeten nog steeds zeer veel (kostbare) zoekinspanningen geleverd worden. Dit kan opgelost worden door mogelijke toekomstige conflicten (schendingen van constraints) in een zoekruimte te voorkomen, wat veel efficiënter is dan constant achteráf moeten herstellen van conflicten. Een constraint propagation strategie die dit doet is ‘forwarding checking’. Constraint propagation is gebaseerd op backtracking, waarbij, zodra een variabele tijdens het zoeken geïnstantieerd wordt, elke waarde uit het domein van een ‘toekomstige’ (nog niet geïnstantieerde) variabele die in conflict is met de huidige instantiëring (tijdelijk) verwijderd wordt uit dat domein van die (nog te bezoeken) variabele. Zodra het domein van zo’n toekomstige variabele leeg is, weet het algoritme dat de huidige (partiële) oplossing ongeldig (inconsistent) is. Als gevolg hiervan ontdoet een forward checking algoritme een zoekboom eerder van rotte takken dan het geval is bij backtracking, waardoor het zoekproces veel efficiënter plaatsvindt. Variable & Value Ordering Met betrekking tot de volgorde waarin variabelen bezocht worden (variable ordering) bestaan twee types: static ordering (van tevoren gespecificeerd) en dynamic ordering (het kiezen van de volgende te bezoeken variabele is afhankelijk van de huidige stand van de zoekruimte). Een heuristiek (vuistregel) die gebruikt wordt bij dynamic variable ordering, wat ondermeer bij het eerdergenoemde forward checking algoritme van toepassing is, is het zogenaamde ‘’first-fail principe’, dat het volgende stelt: “om te slagen, probeer eerst daar waar de kans op falen het grootst is”. Hierbij wordt de variabele met de minst resterende alternatieven (waarden) geselecteerd als eerstvolgende variabele om te instantiëren. De aanname hierbij is dat elke waarde een even grote kans heeft om onderdeel te zijn van een oplossing, dus hoe meer waarden er zijn, hoe groter de kans dat een van hen succesvol zal zijn. Echter, de reden om de variabele te kiezen met de minst resterende waarden is dat (binnen de context van forward checking) indien de huidige (partiële) oplossing niet zal leiden tot een volledige oplossing, dit beter vroeger dan later ontdekt kan worden. Het first-fail principe tracht derhalve de gemiddelde diepte van de takken in de zoekboom te verkleinen door vroegtijdige fouten te triggeren. Soortgelijke strategieën en heuristieken bestaan ook voor value ordering, met betrekking de volgorde waarin waarden toegekend worden aan de volgende te instantiëren variabele. CSP Frameworks Overzicht Daarnaast zijn er diverse CSP frameworks ontwikkeld die off-the-shelf faciliteiten bieden voor het efficiënt modelleren en oplossen van CSP’s. ILOG Open-Source Het open-source Choco CSP framework (beschikbaar via Sourceforge) is ontwikkeld in Java, en vergemakkelijkt het modelleren van CSP’s doordat een abstractielaag aan de gebruiker (software engineer) geboden wordt. Tevens biedt het off-the-shelf CSP-algoritmes, waarmee de gemodelleerde CSP’s opgelost kunnen worden. Men kan er eenvoudig variabelen, domeinen en constraints mee definiëren, en vervolgens de resulterende CSP’s mee oplossen. Choco is in staat om één oplossing voor een probleem te vinden, alle oplossingen, of een optimale oplossing. N-Queens Het N-queens probleem is eenvoudig te begrijpen, maar bevat tegelijkertijd een hoge mate van (rekentechnische) complexiteit. Zo heeft het 8-queens probleem 283.274.583.552 (64x63x..x58x57/8!) mogelijke oplossingen, waarvan er slechts 92 geldig zijn (en waarvan er slechts 12 uniek zijn). Middels het Choco framework wordt het 8-queens probleem als volgt gemodelleerd en opgelost: // Creëer een leeg CSP-object // Creëer en initialiseer de benodigde variabelen met bijbehorende domeinen for (int i = 0; i < n; i++) // Voeg de constraints toe (van het type (not-equals (neq)): CSP.post(CSP.neq(queens[i], queens[j])); // Vind alle oplossingen // En geef (bijvoorbeeld) aan hoeveel oplossingen er gevonden zijn Het framework geeft als resultaat aan dat alle 92 oplossingen gevonden zijn. Dit is een eenvoudig voorbeeld van hoe een CSP efficiënt gemodelleerd en opgelost kan worden in Java middels het open-source Choco CSP framework. Met een dergelijk framework hoeft een software engineer zich niet bezig te houden met het creëren van CSP-algoritmes of de infrastructuur waarmee variabelen, domeinen en constraints gecreëerd kunnen worden, maar kan men zich direct focussen op het probleem dat opgelost moet worden, waarbij een uitgebreide abstractielaag en off-the-shelf CSP-algoritmes gebruikt (en waar gewenst uitgebreid) kunnen worden. Auteur: Nico van Hanxleden Houwert is Solutions Architect bij IPROFS. |
| Lees meer over IPROFS |
| Ga terug naar We Love IT uitgave 1 - 2009 |




