Hoe schrijf je een AI voor een kaartspel?

Sinds de komst van ChatGPT in november 2022 lijkt AI wel overal te zijn. Ik krijg soms de vraag hoe de AI voor de kaartspellen op Whisthub werkt, dus het leek mij een goed moment om er een blogpost over te schrijven. Wees gewaarschuwd, deze post is lang en bij momenten vrij technisch!

Eerst en vooral, de manier waarop de AI op Whisthub werkt is niet per se de manier om een AI te schrijven voor een kaartspel. Ik ben verre van een expert in AI, maar het bleek dat de aanpak op Whisthub vrij goed werkte, en het zijn tenslotte maar kaartspelletjes, het is niet dat ik probeer zelfrijdende auto's te bouwen of iets dergelijks.

Voor we starten is het nuttig om te verduidelijken hoe AI-modellen zoals ChatGPT werken. Dit zijn gigantische neurale netwerken met miljarden parameters die werden getraind op enorme hoeveelheden data, in het geval van ChatGPT eigenlijk het volledige publieke internet. Hoewel het perfect mogelijk is om zulke modellen te trainen voor kaartspellen, is het probleem dat er hiervoor data nodig is. Veel data. Bij het ontwikkelen van een AI voor een of ander nichekaartspel is de kans relatief groot dat er geen hapklare dataset met miljoenen gespeelde spelletjes voorhanden is.

Als u de geschiedenis van Whisthub hebt gelezen, dan weet u dat ik me bewust was van dit probleem bij de initiële ontwikkeling van Whisthub. Hoewel het in theorie mogelijk is om een AI-model te trainen door het tegen zichzelf te laten spelen waarbij het enkel kennis heeft van de spelregels en zo te leren van het resultaat, is dit een zeer complexe taak waarvan ik absolute geen idee had hoe hieraan te beginnen. Dat is waarom ik gekozen heb voor een eenvoudigere aanpak waarbij de AI simpelweg een verzameling van simpele, en deterministische regels hanteert.

Fundamenteel bestaat de AI voor alle kaartspellen op Whisthub uit twee delen: elke AI-speler houdt de kansen bij dat een bepaalde speler een bepaalde kaart heeft. Dit noem ik zelf de aannamematrix, en deze matrix wordt voortdurend geüpdatet gedurende het spel op basis van wat de andere spelers doen. Merk op dat de AI met exact dezelfde informatie speelt als een menselijke speler. De AI kan niet in de kaarten van de tegenstanders kijken, maar maakt gewoon een inschatting van hun kaarten.

Vertrekkende van de aannamematrix wordt het mogelijk om complexere berekeningen te doen, zoals de vraag "wat is de kans dat deze speler nog harten heeft?" Deze afgeleide kansen worden vervolgens gebruikt door het andere deel van de AI, namelijk een gigantische beslissingsboom met een set deterministische regels, bijvoorbeeld "als de kans dat mijn partner geen kaarten meer heeft in deze kleur groter is dan x, speel dan deze kaart". De beslissingsboom wordt vervolgens manueel gefinetuned met honderden tests om ervoor te zorgen dat het resultaat telkens ietwat menselijk lijkt.

De aannamematrix

Zoals gezegd houdt de aannamematrix de kansen bij dat een bepaalde kaart bij een bepaalde speler zit. Stel bijvoorbeeld dat we een kaartspel spelen met 3 spelers, en elke speler heeft twee kaarten in zijn of haar hand, dan ziet de aannamematrix er initieel als volgt uit

AKQJ
Ross½½½½
Rachel½½½½

Initieel hebben we geen enkele informatie over de kaarten, maar het is uiteraard geweten dat elke kaart ofwel bij Ross, ofwel bij Rachel moet zitten, dus de verticale som moet gelijk zijn aan 1, of

ipij=1\sum\limits_i p_{ij} = 1

Aangezien er geen enkele reden is waarom de ene kaart eerder bij Ross zou zitten dan bij Rachel, zijn de kansen initieel uniform verdeeld, m.a.w. allemaal gelijk aan 12\frac{1}{2}. In het geval van een kaartspel met vier spelers - zoals elk spel op Whisthub - betekent dit dat alle kansen initieel gelijk zijn aan 13\frac{1}{3}, maar voor de eenvoud worden er in het voorbeeld slechts twee tegenspelers gebruikt.

Merk op dat de aannamematrix nog een andere interessante eigenschap heeft: de horizontale som moet altijd gelijk zijn aan het aantal kaarten van elke speler. Theoretisch is dit equivalent aan zeggen dat de verwachtingswaarde van het aantal kaarten van een speler E(ni)E\left(n_i\right) gelijk is aan de som van de individuele kansen pijp_{ij}, of nog

E(ni)=jpij=nE(n_i) = \sum\limits_j p_{ij} = n

In het voorbeeld is het geweten dat elke speler twee kaarten heeft, dus de horizontale som moet altijd gelijk zijn aan 2, wat inderdaad het geval is zoals kan nagegaan worden. De horizontale en verticale sommen vormen dus de randvoorwaarden voor de aannamematrix: het maakt niet uit hoe de waarden binnen de matrix veranderen, ze moeten altijd voldoen aan de voorwaarden voor de horizontale en verticale sommen.

Allemaal goed en wel, maar uniforme kansen zijn niet bijster nuttig. Dat is waarom de aannamematrix constant geüpdatet wordt op basis van wat er gebeurt in het spel. Het meest voor de hand liggende voorbeeld is wanneer er een kaart gespeeld wordt. Stel dat in het bovenstaande voorbeeld bijvoorbeeld Ross A speelt. De matrix kan nu aangepast worden naar

AKQJ
Ross1
Rachel0

Ziet u wat er veranderd is? Ross heeft A gespeeld, wat onthult dat hij deze kaart inderdaad had, dus pRoss,A=1p_{\text{Ross},\text{A}} = 1. Uiteraard betekent dit dat Rachel niet langer A kan hebben, dus de kans hierop wordt 0. Vervolgens kan de rest van de matrix aangepast worden op basis van de randvoorwaarden. We hebben geen extra info gekregen over Ross zijn andere kaarten, dus er wordt vanuit gegaan dat de kansen uniform zijn gebleven, wat enkel kan betekenten dat de kansen nu 13\frac{1}{3} zijn i.p.v. 12\frac{1}{2} omdat de horizontale som nog steeds 2 moet zijn. Vervolgens kunnen we eenvoudig de overige kansen berekenen van Rachel haar kaarten als 23\frac{2}{3} op basis van de verticale sommen die nog steeds 1 moeten zijn.

Bemerk dat het updaten van de matrix relatief eenvoudig is in het voorbeeld, maar het wordt een pak moeilijker voor kaartspellen met 4 spelers (en dus telkens 3 tegenspelers). Het probleem is dat de randvoorwaarden niet lineair onafhankelijk zijn, wat betekent dat als we ze omzetten in een stelsel van vergelijkingen, de determinant 0 wordt en er bijkomende randvoorwaarden nodig zijn om de onbekende kansen te berekenen. Dit was zelfs een van de moeilijkste problemen om op te lossen bij het ontwikkelen van de AI, en uiteindelijk is dit gelukt door het stelsel op te lossen met een iteratieve berekening. Dit betekent in feite dat de overblijvende kansen iteratief herverdeeld worden totdat "voldoende" aan de randvoorwaarden voldaan is.

Het moge duidelijk zijn dat het spelen van kaarten bepaalde informatie vrijgeeft, maar er zijn nog andere manieren dat er informatie vrijgegeven wordt over de kaarten. In veel kaartspellen is het immers zo dat het niet volgen op een kleur betekent dat de speler geen kaarten meer heeft van deze kleur. In dat geval kunnen alle kansen van deze kleur op 0 gezet worden voor deze speler in de matrix, waarna de kansen vervolgens geüpdatet worden. Bekijk het als volgt: als tijdens het spel duidelijk wordt dat een speler geen kaarten meer heeft in een kleur, dan is de kans groter dat de speler kaarten heeft in een andere kleur, en omgekeerd is de kanse groter dat de andere spelers wél nog kaarten hebben in deze kleur. Het updaten van de aannamematrix weerspiegelt dit op elegante wijze.

Vormfuncties

Een andere manier waarop informatie kan vrijgegeven worden, gebeurt in kaartspellen met een biedproces, zoals bijvoorbeeld kleurenwiezen. In dit spel kan een speler een bepaalde kleur voorstellen, wat betekent dat de speler goede kaarten heeft in deze kleur, met name veel kaarten en wellicht ook de hoge kaarten. Dit kan eveneens weergegeven worden met de aannamematrix.

Om dit te doen gebruikt de AI op Whisthub het concept van vormfuncties. Een vormfunctie bevat simpelweg de kans dat een bepaalde kaart van een bepaalde kleur bij een speler zit op basis van wat de speler voorstelt. De vormfunctie voor bijvoorbeeld "Abondance 9" kan er als volgt uitzien:

Dit betekent dat als een speler Abondance 9 vraagt, de AI aanneemt dat de kans dat deze speler A heeft ongeveer 89% is, 86% voor K enzovoort. De som van deze kansen pi\sum{p_{i}} is met andere woorden de verwachtingswaarde E(n)E\left(n\right) van het aantal kaarten dat de speler heeft in deze kleur.

Wacht eens even. De vormfunctie bevat waarden voor alle 13 kaarten, maar wat als ik sommige van deze kaarten zelf heb? In dat geval weet ik dat de tegenstander deze kaarten niet kan hebben!

Klopt. De vormfuncties moeten eigenlijk beschouwd worden als algemene vormen die vervolgens aangepast worden aan wat de speler effectief in zijn of haar hand kan hebben op basis van de beschikbare informatie van onze eigen hand. Het aanpassen hiervan gebeurt met een speciaal algoritme dat hiervoor ontwikkeld werd, hetgeen de eigenschap heeft dat de verwachtingswaarde van de vormfunctie behouden blijft. Een voorbeeld: als de som van alle waarden in de vormfunctie 6 is, dan betekent dit dat we verwachten dat deze speler 6 kaarten heeft in een kleur, en het algoritme zal vervolgens deze kansen herverdelen zodat de verwachtingswaarde van 6 behouden blijft.

Stel bijvoorbeeld dat onze eigen hand er als volgt uitziet

en dat een tegenstander Abondance 9 vraagt, dan kan de vormfunctie aangepast worden als

Als u goed kijkt, ziet u dat de kansen voor Q en 6 "herverdeeld" zijn over de andere kaarten zodat de verwachtingswaarde van het aantal kaarten gelijk blijft. Dit zorgt ervoor dat de kansen voor de andere kaarten ietsje hoger worden. Merk echter wel op dat het algoritme dit niet blind doet: het zorgt er steeds voor dat kansen niet groter kunnen worden dan 1.

De verschillende vormfuncties die gebruikt worden door de AI werden initieel gewoon arbitrair gekozen op basis van intuïtie. Gewoon iets dat aannemelijk leek, maar niet gebasserd op het een of ander. Echter, vanaf dat er een paar miljoen logs van gespeelde spelletjes beschikbaar waren, heb ik de vormfuncties aangepast zodat ze gebaseerd zijn op wat er gebeurt in echte spelletjes. Concreet ben ik nagegaan hoe vaak iemand A heeft wanneer ze een kleur voorstellen, hoe vaak K enzovoort. Deze frequenties werden vervolgens gebruikt als vormfunctie.

Op een manier kan dit beschouwd worden als het trainen van de AI. Het is niet echt hoe trainen werkt bij modellen als ChatGPT, maar de vormfuncties waren nu tenminste op iets gebaseerd. Het beste is dat eens ik de vormfuncties had aangepast aan de echte frequenties - dewelke trouwens totaal verschillend waren van het initiële nattevingerwerk - de AI opvallend beter en menselijker begon te spelen. Het is moeilijk om te beschrijven wat dit precies deed opvallen, maar het was een van de meest voldoening gevende gevoelens die ik ooit heb ervaren bij het ontwikkelen van Whisthub!

De kaartverdelingen

Allemaal goed en wel dat we nu de kansen kennen dat een bepaalde kaart bij een bepaalde speler zit, maar dit is op zich niet echt nuttig. De echte waarde zit hem in de mogelijkheid om hiermee afgeleide berekeningen te doen. Bijvoorbeeld, met de aannamematrix wordt het mogelijk om de kans te berekenen dat een speler geen kaarten meer heeft in een bepaalde kleur, of de kans dat als de AI een bepaalde kaart speelt, niemand de slag nog zal kunnen overnemen.

Veel van deze afgeleide kansen worden berekend door alle mogelijke manieren van de kaartverdeling te overlopen. Stel bijvoorbeeld dat er nog twee kaarten van klaveren over zijn. We kunnen de mogelijke kaartverdelingen modelleren als 2 balletjes die verdeeld moeten worden over 3 emmers

Voor elk van deze verdeling is het makkelijk om na te gaan of er aan de conditie waarnaar we op zoek zijn voldaan is, bijvoorbeeld de kans dat alle spelers na ons geen kaarten meer hebben in een kleur. Met de kansen uit de aannamematrix kunnen we de kans berekenen van elke verdeling, en door deze op te tellen kennen we de kans voor de conditie waar we naar op zoek zijn.

Merk op dat er in de bovenstaande verdelingen abstractie gemaakt is van de waarde van de kaarten. In realiteit kan echter de verdeling A| |2 wezenlijk verschillen van 2| |A! Het aantal mogelijke verdelingen wordt in dat geval echter zo extreem groot dat het niet meer werkbaar is, en bovendien hebben we dit vaak niet eens nodig. Stel bijvoorbeeld dat we de kans willen weten dat een speler na ons onze Q kan overnemen met A of K. In dat geval hebben we de verdelingen niet nodig, we kunnen gewoon het complement berekenen van de kans dat de speler zowel A als K niet heeft, of in wiskundige termen

p=1i(1pi)p = 1 - \prod\limits_{i} (1 - p_i)

De techniek van alle mogelijke kaartverdelingen te onderzoeken en hieraan kansen toe te kennen kan beschouwd worden als een manier van Monte Carlo simulaties van beperkte diepte. Dit is bijvoorbeeld een gebruikelijke techniek bij schaakprogramma's, maar hierbij kijken de simulaties meerdere beurten vooruit, terwijl we bij de kaartverdelingen enkel kijken naar de huidige mogelijke verdelingen.

De beslissingsboom

Het feit dat er een manier is om de kansen uit te rekenen voor bepaalde situaties, staat toe om te simuleren hoe een mens redeneert bij het spelen van een kaartspel. Beschouw bijvoorbeeld het geval van troef afhalen bij kleurenwiezen, wat betekent dat men een paar keer een troefkaart uitkomt om ervoor te zorgen dat de tegenstanders later andere slagen niet kunnen kopen. Meestal wordt er telkens twee keer troef afgehaald, maar of er al dan niet een derde keer moet afgehaald worden kan afhangen van de kans dat de tegenstanders nog troef hebben.

Hoewel dit simpel lijkt op papier, is de vraag hier echter wat de limiet is van deze kans. Bij welke kans dat de tegenstanders nog troef hebben moet de AI effectief een derde keer troef afhalen? 50%? 75%? 90%? De waarheid is dat er hier geen eenduidig antwoord op te geven valt - net zoals geen twee mensen dezelfde spelstijl hebben - en dit is de verantwoordelijkheid van de beslissingsboom. De beslissingsboom identificeert bepaalde spelsituaties en maakt vervolgens een beslissing op basis van een kans en een arbitraire grens hiervoor. In de kern is de beslissingboom dus eigenlijk een gigantische if-else structuur.

Oké, maar dan nog, hoe bepalen we grenzen van de kansen gebruikt in de beslissingsboom? Wel, het komt erop neer om gewoon de intuïtie te gebruiken en jezelf de vraag te stellen

Als een mens zijnde, welke kans lijkt redelijk als grens in deze situatie?

Onthoud echter dat de exacte waarden van de grenzen er niet echt toe doen. Wat belangrijk is, is dat de AI redelijk en menselijk lijkt van buitenaf. Wat er onder de motorkap eigenlijk gebeurt is niet relevant.

Om hiervoor te zorgen, zijn er honderden test cases geïmplementeerd waarbij getest wordt wat de AI doet in een specifieke situatie. Zulke test cases zien er typisch als volgt uit

it('komt de hoogste troefkaart uit', function() {

  this.log = `
    1: ♥Q64 ♦AJ106 ♣642 ♠AJ7
    2: ♥K973 ♦K5 ♣1085 ♠Q432
    3: ♥J85 ♦Q7432 ♣AQJ ♠K6
    0: ♥A102 ♦98 ♣K973 ♠10985
    1: Propose ♦
    2: Propose ♠
    3: Accept ♦8
    0: Propose ♣
    2: Pass
    0: Pass
  `;
  let card = this.decide();
  expect(card).to.equal('♦A');

});

De test cases worden typisch gevonden door manueel tegen de AI te spelen tot de AI iets vreemds doet. Als dit gebeurt, dan wordt er een test gemaakt voor deze specifieke situatie, hetgeen vervolgens toelaat om de beslissingsboom te volgen en na te gaan op basis waarvan de AI eigenlijk een beslissing maakt. Vaak resulteert dit in het toevoegen van een bijkomende voorwaarde in de beslissingsboom voor dit specifieke geval, maar het komt ook voor dat blijkt dat de grenzen voor de kansen moeten aangepast worden.

Eens de beslissing van de AI in een bepaalde situatie opgelost is, is het ook essentieel om alle bestaande test cases opnieuw te testen om te verifiëren dat er door de aanpassing niets anders veranderd is. In de computerwetenschappen heet dit een regressie: we willen er zeker van zijn dat het oplossen van een fout in een bepaald geval niets anders kapot maakt.

Merk op dat dit een van de grote voordelen is van de beslissingsboom: het is eenvoudig na te gaan hoe de AI precies redeneert en waarom het een bepaalde beslissing neemt. Dit is veel lastiger in het geval van neurale netwerken zoals bijvoorbeeld ChatGPT, dewelke eigenlijk een black box zijn waar het onmogelijk is om na te gaan waarom de AI een bepaalde beslissing heeft genomen. Dit betekent eveneens dat het lastig is de AI te verbeteren, aangezien het in zo'n gevallen simpelweg neerkomt op meer en betere trainingsdata. Daarom beschouw ik het feit dat de Whisthub AI geen black box is als een enorm voordeel.

Een andere manier om relevante test cases te vinden is door simulaties te laten lopen waarbij de AI tegen zichzelf speelt. Dit is met name nuttig voor spelsituaties die eerder zeldzaam zijn en bijgevolg lastig tegen te komen wanneer er manueel tegen de AI gespeeld wordt. In dat geval wordt er vaak een conditie toegevoegd aan de AI waarbij het spel gelogd wordt als er zich een bepaalde situatie voordoet, en vervolgens kan deze log gebruikt worden om een test case te maken en na te gaan dat de AI correct speelt.

Tot slot zijn simulaties ook belangrijk bij het opsporen van bugs in de AI, meer specifiek bugs die ervoor zouden zorgen dat de AI crasht. Door het feit dat de AI deterministisch is, kunnen er eenvoudig meer dan 200 spelletjes per seconde gesimuleerd worden op mijn eigen computer, dus als ik deze laat lopen voor enkele minuten en er zich geen crash voordeed na 100.000 spelletjes, dan is het vrij zeker dat er geen bugs meer zitten in de AI.

Ambigue spelsituaties

Hoewel het concept van een beslissingsboom simpel is, is het probleem vooral dat er altijd spelsituaties zullen zijn waar het niet meteen duidelijk is wat de juiste beslissing is. Test cases toevoegen en de grenzen aanpassen helpt hier niet omdat per definitie deze situaties betekenen dat niet alle mensen dezelfde beslissing zouden nemen.

Om deze situaties te testen, wordt er daarom vaak getest dat de beslissing van de AI zeker niet een bepaalde kaart is, m.a.w. dat de AI niet blundert. Wat de eigenlijke beslissing dan wél is, wordt door de test in het midden gelaten. Op die manier kan de AI aangepast worden, waarbij er toch nog verzekerd wordt dat de AI in deze ambigue situaties niet blundert.

Echter, in kaartspellen zoals kingen en hartenjagen is er een specifiek algoritme ontwikkeld dat omgaat met deze ambigue situaties. Zowel kingen als hartenjagen zijn zogenoemde negatieve spellen, wat betekent dat het doel is om strafpunten te vermijden. Als het niet meteen duidelijk is welke kaart er gespeeld moet worden, dan zal de AI het aantal verwachte strafpunten voor elke kaart berekenen die kan gespeeld worden, en vervolgens deze kans minimaliseren.

Beschouw bijvoorbeeld de onderstaande situatie bij het hartenjagen:

Stel dat we zowel 3 als K hebben en dat de schoppendame Q nog niet gevallen is, dan zijn er twee opties in dit geval:

  1. We kunnen 3 spelen en de vier strafpunten in de slag ontwijken, maar vervolgens riskeren om geraakt te worden door schoppendame Q en haar 13 strafpunten doordat we K bijhouden.
  2. We kunnen ook nu K spelen en de vier strafpunten nemen, maar vervolgens veilig zijn voor Q in harten doordat 3 een veilige kaart is.

De AI berekent het verwachte aantal strafpunten voor beide gevallen, waarbij rekening gehouden wordt met de informatie uit de aannamematrix, en kiest vervolgens de optie die resulteert in het minste aantal verwachte strafpunten. Merk op dat dit is ongeveer hoe een mens ook redenereert in dit geval: proberen in te schatten wat de beste optie is op de lange termijn.

Hoewel deze aanpak simpel lijkt, is het algoritme voor het verwachte aantal strafpunten dat allerminst. Het maakt heftig gebruik van de aannamematrix en de kaartverdelingen, en het maakt ook een heleboel vereenvoudigde aannames om te vermijden duizenden verschillende situaties te moeten onderzoeken. Het testen hiervan gebeurt ook vaak door na te gaan dat de AI niet blundert, eerder dan na te gaan dat de AI een specifieke kaart speelt.

Het geheugen van de AI

Hoewel de AI zeker niet speelt op het niveau van een mens, is hij wel beter dan eerder welke mens op één vlak: de AI heeft een perfect geheugen. In kaartspellen is het vaak nuttig om op eender welk moment te weten welke kaarten er nog overblijven in het spel. Het is bijvoorbeeld niet wenselijk om een troefkaart te spelen als uw partner reeds de hoogste kaart heeft gespeeld in die kleur. Het probleem is dat een mens dit wel eens durft vergeten en dus soms onnodig een troefkaart zal spelen.

De AI zal deze fout echter nooit maken omdat hij exact weet welke kaarten er nog achter zijn in het spel - maar onthoud wel dat de AI niet valsspeelt en enkel de kaarten kan zien in zijn eigen hand! Men zou kunnen zeggen dat het een mogelijkheid is om de AI menselijker te maken door hem een imperfect geheugen te geven waarbij de AI soms kaarten vergeet die al gespeeld zijn.

Er is echter explicitiet besloten om dit niet te doen. Dit zou niet enkel de logica van de AI nodeloos complex maken, spelers vinden het typisch ook niet leuk om met AI-spelers te spelen, dus dit zou zeer frustrerend zijn als u samen met een AI in een team zit, bijvoorbeeld in een tornooi. De AI is zeker niet perfect, dus het heeft geen zin om hem bewust nog slechter te doen spelen. Computers hebben nu eenmaal een perfect geheugen, dat is hun sterke punt, dus ze mogen dat in hun voordeel gebruiken.

Conclusie

Ik weet niet of dit is hoe u verwachtte dat de AI werkt. De AI op Whisthub is fundamenteel verschillend van hoe moderne AI-systemen zoals ChatGPT werken, en op een manier kan de AI op Whisthub niet eens als een echte AI beschouwd worden wat dat betreft, aangezien hij deterministisch is.

Het is verleidelijk om te denken dat de AI verbeterd kan worden door een neuraal netwerk te gebruiken in plaats van de huidige deterministische aanpak. Men moet echter niet vergeten dat de AI op Whisthub hierdoor wel licht en extreem snel is. Als we een neuraal netwerk zouden trainen voor alle kaartspellen op Whisthub, dan zou dit model wellicht enkele megabytes - of zelfs gigabytes - in beslag nemen, terwijl de AI nu slechts een paar kilobytes bedraagt.

Neurale netwerken gebruiken ook veel rekenkracht, dus in dat geval is het wellicht noodzakelijk om een aparte server te hebben - mogelijk zelfs meerdere - enkel en alleen voor de AI. Het sop is de kolen in dat geval simpelweg niet waard, eens te meer omdat de focus op Whisthub ligt op multiplayer waarbij gespeeld wordt tegen andere mensen, zelfs al is er soms geen ontkomen aan de AI-spelers, bijvoorbeeld in tornooien.

Ondertussen zijn er wel al een paar miljoen logs van spelletjes verzameld, dus het trainen van een neuraal netwerk is ondertussen wel degelijk mogelijk, al is het maar voor het plezier of voor educatieve doeleinden. Ik ben helemaal geen expert hierin, dus momenteel ben ik niet van plan mijn - beperkte - tijd hier in te investeren. Mocht u echter bijvoorbeeld een masterthesis of een doctoraat doen in AI, dan sta ik zeker open voor een samenwerking. Dat zou nog leuk kunnen worden!