Gebruik van classificatie om gebruikspieken van een elektronische leeromgeving te voorspellen.

Maat: px
Weergave met pagina beginnen:

Download "Gebruik van classificatie om gebruikspieken van een elektronische leeromgeving te voorspellen."

Transcriptie

1 owered by TCPDF ( Academiejaar Faculteit Ingenieurswetenschappen en Architectuur Valentin Vaerwyckweg Gent Gebruik van classificatie om gebruikspieken van een elektronische leeromgeving te voorspellen. Masterproef voorgedragen tot het behalen van het diploma van Master in de industriële wetenschappen: informatica Sion VERSCHRAEGE Promotoren: ing. Wim VAN DEN BREEN Ruben LAGATIE (Televic)

2 Abstract Nederlands Het voorspellen van gebeurtenissen is een veelvoorkomend probleem in het veld van Knowledge Discovery. De optimale oplossing is dan ook voor elk probleem anders. In deze scriptie gaan we op zoek naar de beste strategie om het aantal gebruikers op het online leerplatform Edumatic te voorspellen. We onderzoeken een aantal verschillende machine learning algoritmes, en bekijken hoe de data best beschreven wordt. Gebaseerd op deze informatie schrijven we tenslotte een Windows service die de nodige personen een mailtje kan sturen als het voorspelde aantal gebruikers een bepaalde drempel overschrijdt. Na classificatie met behulp van C4.5 worden 13 verschillende regressie-algoritmes getest met behulp van de Weka tool. Uit deze algoritmes halen we er vier, die na deze eerste test goede resultaten geven: Lineaire Ridge-Regressie, M5P, Least Median Squared, en Reptrees. Verder bekijken we de Extratree (Geurts, 2002) en enkele meta-algoritmes. Tegelijk met die stap onderzoeken we hoe we onze data best beschrijven. We hebben in totaal meer dan 60 verschillende features per datapunt, en zullen meestal betere resultaten bekomen als we daaruit enkel die features halen waar het algoritme goed mee werkt. Tenslotte bespreken we de exacte implementatie van de verschillende algoritmes en omkaderende code, zoals de service zelf, in C#. Onze resultaten bereiken een maximum bij een correlatiecoëfficiënt van rond de Hoewel er geen harde bewijzen voor zijn, vermoeden we dat dit niet ver van de hoogst mogelijke coëfficiënt ligt, aangezien we dit maximum met verschillende algoritmes benaderen. Verder zien we dat Lineaire Ridge-Regressie deze waarde consistent haalt, en relatief snel werkt, waardoor we dit algoritme naar voor schuiven als optimale oplossing. We gebruiken hierbij een tiental features van de dataset. 2

3 3 English Event prediction is a recurrent problem in the field of Knowledge Discovery. The optimal solution differs from problem to problem. In this thesis, we search for the best strategy to predict the number of users on the online learning platform Edumatic. We research a number of machine learning algorithms, and attempt to find the best way to describe our data. We implement a Windows service based on this information. The service is able to send an to certain people as soon as the predicted number of users is higher than a certain threshold. Apart from classification, using C4.5, we also test 13 different regression algorithms using the Weka tool. Out of these 13 alorithms, we select the four that gave the best results in those preliminary tests: Linear Ridge Regression, M5P, Least Median Squared, and Reptrees. We take a look at the Extratree and a number of meta-algorithms as well. At the same time, we find out how to present our data to these algorithms. There are over 60 features for each point of data, and selecting only the features that work well for a given algorithm will increase accuracy and speed. Finally, we discuss the exact implementation of each of these algorithms in C#, as well as the framework code, such as the service itself. Our results peak at a correlation coefficient of Even though we are unable to prove this, we presume this number is not far off from the highest possible coefficient, since we can approximate this number with different algorithms and approaches. We see that Linear Ridge Regression consistently achieves this coefficient, and it converges relatively quickly, which is why we propose this algorithm as an optimal solution. We use about ten features to achieve optimal results. 3

4 Voorwoord Hedendaagse technologieën maken het mogelijk enorme hoeveelheden data te verzamelen en op te slaan. We leven in een wereld waar zo goed als niets gebeurt zonder dat iemand het opschrijft. Weerstations, laboratoria, scholen, winkelketens en andere bedrijven, allemaal bezitten ze een rijkdom aan informatie, opgeslagen in duizenden of miljoenen records in een databank. Deze verzamelde data kan een schat aan interessante informatie verbergen. Het mooiste voorbeeld is natuurlijk het alombekende supermarktverhaal: een supermarkt die bijhield wat zijn klanten kochten, merkte op dat bier en luiers vaak tegelijk gekocht werden. Blijkbaar hadden veel jonge vaders, die na het werk nog vlug om luiers gingen, nood aan of zin in een frisse pint. En inderdaad, door die producten samen te zetten steeg de verkoop bij beiden. Ongeacht de waarheid van het verhaal, illustreert het wel mooi het beginpunt van deze scriptie: uit de opgeslagen data waar we het zonet over hadden kunnen er interessante, bruikbare, en onverwachte patronen gehaald worden. In deze data-storing maatschappij is dit een zeer belangrijk gegeven, en al decennia lang is men bezig met uit te zoeken hoe men die patroonherkenning het beste aanpakt. Hoe vinden we patronen, oorzaken en gevolgen? Hoe halen we kennis uit die warboel van kleine feitjes? Een vraag waar nog geen eenduidig antwoord op gevonden is. Er zijn wel een aantal algoritmes voor deze taak - gegroepeerd onder de noemers machine learning en data mining - maar de bruikbaarheid van deze algoritmes is afhankelijk van de aard van de data die men ermee probeert te verwerken. Er bestaat (nog) geen catch-all oplossing, die uit een willekeurig dataset alle interessante patronen kan filteren. We moeten dus per probleem gaan onderzoeken hoe we het het beste aanpakken. Een tweede vraag is wat we kunnen doen met die patronen, eens we ze vinden. Kunnen we ze voorstellen in een formaat dat door mensen leesbaar is? Kunnen we onze marketingstrategie erop toespitsen? En, waar we ons in deze masterproef mee zullen bezighouden: kunnen we ze gebruiken om toekomstige data te voorspellen? Is het mogelijk om, gegeven 4

5 5 een groot aantal records uit het verleden, op voorhand te weten hoe de records uit de toekomst eruit zullen zien? Dat is de vraag die wij ons stellen, en het toepassingsgebied is het aantal gebruikers op het elektronisch leerplatform Edumatic. Om dit voorwoord af te sluiten wil ik verder nog de mensen bedanken die mij geholpen hebben deze thesis te maken tot wat ze is. Ten eerste natuurlijk Ruben Lagatie en Wim Van den Breen, als vaste aanspreekpunten en eerste-lijn verbeteraars. Verder wil ik Pieter- Jan Maenhaut, Charlotte Bockaert, Leen Van den Kerkhove, speciaal bedanken voor tips, ideeën, verbeteringen, mentale steun, en een hele hoop andere zaken. En tot slot de mensen van Televic, en Televic Education in het bijzonder, voor een stevige ondersteuning en fantastische werksfeer. 5

6 Inhoudsopgave Voorwoord 2 1 Inleiding Probleemstelling Voorgestelde oplossing Machine Learning Soorten algoritmes Foutmaten Data De databank Logs Features Overzicht van Gebruikte Features Feature Selection C Initiële status Uitbreidingen Grouping Foutberekening Windowing Rulesets Weka Verkenning Het arff - formaat Correlatiecoëfficient

7 Inhoudsopgave Classifiers Meta-classifiers Retraining besluit Implementatie Service en Mailing Classificatie Library Trees Abstract Tree Extra Trees Reptrees M5P Linear en Ridge Regression Matrices Least Median Squared Metaclassifiers Averager en Voter Bagging LinRegRidgeTester Stacking TrainAndPrune Threading Threading bij Cross Validation Resultaten Accuraatheid Tijdsduur De Service Bibliografie 80 A Handleiding Service 82 B Configuratie Service 89 7

8 Hoofdstuk 1 Inleiding 1.1 Probleemstelling Televic Education stelt een online leerplatform, genaamd Edumatic, ter beschikking van zijn klanten. Op dit leerplatform kunnen leerkrachten oefeningen en toetsen aanmaken, die dan door leerlingen kunnen opgelost worden. Bij die oefeningen en testen kunnen ook afbeeldingen en/of filmpjes horen. Studenten en leerkrachten zijn verdeeld in groepen, per school, departement of klas, en deze groepen noemt men Channels. Dit is natuurlijk maar een zeer beperkte beschrijving van het platform, maar meer hebben we ook niet nodig voor onze doeleinden. Meestal blijft het aantal gebruikers van het Edumatic-platform mooi onder een bepaalde grens, en de server heeft dan ook geen problemen om de aanvragen te verwerken. Maar af en toe, bijvoorbeeld tijdens de examens of als een nieuwe school wordt ingeschreven, is er een grote piek in het aantal gebruikers. Het is de bedoeling dat ook tijdens die pieken de gebruikers geen hinder ondervinden. Edumatic draait momenteel op een normale, dedicated server. Men zou echter willen overschakelen naar een cloudserver. Een van de voordelen daarvan is dat het dan mogelijk is de server tijdelijk meer capaciteit te geven, zowel wat geheugen, rekenkracht, netwerksnelheid als opslagruimte betreft. Het is niet eenvoudig om dat op het piekmoment zelf te doen, vaak moet de virtual machine bijvoorbeeld heropgestart worden. We willen dus op voorhand weten wanneer die pieken zich zullen voordoen. 8

9 Hoofdstuk 1. Inleiding Voorgestelde oplossing Mensen kunnen weliswaar proberen voorspellingen te doen, maar er is zeer veel data en de verbanden ertussen zijn enorm complex, waardoor dit zo goed als onmogelijk wordt. Beter zou zijn als een programma de voorspellingen voor ons zou kunnen maken. Als dit automatisch elke dag gebeurt, hebben we geen menselijke tussenkomst nodig. Er worden geen werkuren meer aan verspild, en de accuraatheid van de voorspelling is niet meer afhankelijk van de gemoedstoestand en/of de wiskundige kennis van de voorspeller. We willen met andere woorden een computerprogramma dat automatisch en zonder menselijke tussenkomst voorspellingen maakt. De belangrijkste vraag die we het programma daarbij stellen is: zullen er op dag X meer dan Y gebruikers zijn? Deze vraag kunnen we in feite op twee manieren beantwoorden: we kunnen ze gewoon als ja-neenvraag zien, en ons programma zodanig maken dat het één van deze twee antwoorden teruggeeft. Of we kunnen gedetailleerder proberen te werken, en het exacte aantal gebruikers proberen te voorspellen. Ons programma zal dan antwoorden met bijvoorbeeld: Neen, er zullen Z gebruikers zijn. De eerste methode verdeelt de dagen in groepen: de groep JA en de groep NEEN. Dit is een typisch voorbeeld van classificatie: het verdelen in klassen, of groepen, van bijvoorbeeld dagen. We kunnen classificatie ook gebruiken om data in meerdere groepen te verdelen: bijvoorbeeld 0 TOT 9 GEBRUIKERS, 10 TOT 19 GEBRUIKERS, enzovoort. Deze verdeling kan echter niet arbitrair klein worden, we hebben namelijk een groot aantal voorbeelden nodig voor elke groep. De tweede manier geeft een getal terug. Dit is regressie: in plaats van aan elke dag een klasse te hechten, geven we er een numerieke waarde aan. Regressie gebruikt voor een deel dezelfde algoritmen als classificatie, en voor een deel andere, maar in ons geval kunnen we een regressie-antwoord omzetten naar een classificatie-antwoord: we kiezen gewoon voor klasse JA als het antwoord groter is dan het gevraagde aantal gebruikers, of anders voor NEEN. Dit zorgt ervoor dat we ook regressiemethoden kunnen (en zullen) onderzoeken voor deze masterproef. 1.3 Machine Learning Het is belangrijk dat onze lezers begrijpen waarover we het hebben, daarom even dit tussenstukje. Lezers die vertrouwd zijn met ML-woordenschat kunnen deze paragraaf uiteraard overslaan. We gebruiken hier de wijdverspreide Engelse termen. We moeten hierbij 9

10 Hoofdstuk 1. Inleiding 10 opmerken dat we ons in deze thesis enkel bezighouden met het zogenaamde supervised learning, een onderdeel van machine learning. Data Met data bedoelen we die data die we ter beschikking hebben om onze algoritmes te laten leren. Data bestaat uit individuele records, of instances, of datums, die elk dezelfde attributen of features hebben. Verder heeft elke instance een oplossing, de waarde die we uiteindelijk willen voorspellen. Dit kan zowel een klasse zijn (bij classificatieproblemen) als een getal (bij regressieproblemen). De keuze van die features kan zeer belangrijk zijn - volgens sommigen is 60% van de moeilijkheid van data mining gelegen in het goed presenteren van de data (Cabena et al., 1998). We zullen het later nog hebben over The Curse of Dimensionality, of hoe een groot aantal features de voorspellingen minder accuraat maken. Data wordt verdeeld in twee of drie delen, afhankelijk van het algoritme. Train Data: dient om het algoritme tijdens een eerste pass te trainen. Het algoritme maakt op basis van die data een set regels, of vult een aantal waarden in, of past zichzelf op een andere manier aan om overeen te komen met de Train Data. Validation Data of Prune Data : wordt gebruikt door bepaalde algoritmes die overfitting (zie Variance) gaan vermijden door zichzelf te testen met een tweede set data, resultaten die te veel verschillen, worden dan herbekeken en vaak vereenvoudigd. Test Data : dient om het algoritme achteraf, na de training, automatisch te testen. Uit de test data kunnen we dan een aantal maten voor de accuraatheid van het algoritme halen. Kenmerken van algoritmes Variance is een veelgebruikte verkorting van error variance, de variantie op de fout. Wat daarmee bedoeld wordt, is hoe sterk de accuraatheid van het algoritme verandert als er kleine aanpassingen aan de train data gebeuren. Algoritmes met veel variance zijn zeer gevoelig voor ruis en toevallige uitschieters in de trainingsset. Een belangrijk symptoom daarvan is overfitting, waar een algoritme zeer goed aansluit op de train data, inclusief ruis, maar slecht scoort op de test data. Het past zichzelf letterlijk te sterk aan aan de trainingsset. 10

11 Hoofdstuk 1. Inleiding 11 Bias is een maat van hoe goed het algoritme kan aansluiten op de trainingsdata. Een algoritme dat bijvoorbeeld een ja-nee classificatie maakt op basis van één attribuut van de data, heeft een hoge bias: het gaat er van uit dat er een attribuut is dat de data perfect verdeelt. Bias en variance zijn elkaars tegenhangers: men wil beiden zoveel mogelijk vermijden, maar het verlagen van één van de waarden zal meestal als gevolg hebben dat de andere verhoogt, dit is het zgn. bias-variance dilemma. Dit is meteen ook één van de redenen dat verschillende algoritmen beter werken op verschillende data: sommige data kan goed verdeeld worden met een hoog-bias algoritme, waardoor de variance laag zal zijn; bij andere problemen is de kost van een verhoogde variance minder dan de winst die men haalt uit een beter passend (minder biased) algoritme. Ter illustratie proberen we een sinusfunctie te modelleren. Onze gekozen functie is sin(2πx) in het interval [0, 1]. Uit deze functie halen we willekeurig 7 datapunten, en om de reële situatie zo goed mogelijk te benaderen, zit er een kleine fout op de waarden voor die datapunten. We benaderen de functie met 4 classifiers, die als trainingsdata enkel die zeven punten krijgen. Als model kiezen we voor een veelterm van een bepaalde graad, waarvan we de sum of squares fout zo laag mogelijk houden. De resultaten voor veeltermen van graad 1, 3, 5 en 6 zijn te zien in figuur 1.1. De rechte sluit duidelijk niet aan op de sinusfunctie. Dit is een bias-probleem: een eerstegraadsveelterm kan enkel een rechte zijn, en is dus gewoon niet flexibel genoeg om een sinusfunctie te benaderen. De derdergraadskromme werkt wel zeer goed: de sinusfunctie wordt redelijk dicht benaderd, ook al hebben we maar zeven punten data (mét ruis). Eens we met graad 5 beginnen merken we al een probleem: de kromme golft sterker dan nodig, waardoor er afwijkingen komen tussen het model en de eigenlijke functie. Waarom dat zo is zien we duidelijk op de laatste figuur, een zesdegraadsveelterm, waar het probleem nog veel groter is: het model gaat perfect door alle gegeven datapunten, inclusief ruis, waardoor het zich in enorme bochten moet wringen. De fout (en RMS) op de trainingsset is dan wel 0, het model wil té perfect zijn en begint zich niet alleen aan de data, maar ook aan de ruis aan te passen. Dit is een typisch voorbeeld van een te hoge variance, ofwel overfitting. 11

12 Hoofdstuk 1. Inleiding 12 Figuur 1.1: Voorbeeld van Bias en Variance bij benaderen van een sinusfunctie Soorten algoritmes Het opsommen en beschrijven van alle verschillende soorten machine learning algoritmes valt buiten de grenzen van deze masterproef. We vermelden enkel diegenen die verderop in deze scriptie een belangrijke rol zullen spelen. Beslissingsbomen worden opgebouwd aan de hand van de traindata. Een basisalgoritme zal eerst het interessantste attribuut selecteren, waarbij één of andere heuristiek gebruikt wordt om de interessantheid te berekenen. De boom begint dan met een splitsing aan de hand van dat attribuut, de traindata wordt op dezelfde manier gesplitst, en elke deelboom wordt recursief verder opgebouwd met zijn deel van de data. Dit gaat door tot alle instances in een subset dezelfde klasse hebben, of een bepaalde diepte bereikt is, of de subset te klein wordt, of één van de vele andere optionele stop-heuristieken een teken geeft. Eventueel kan de boom daarna nog pruned (gesnoeid) worden, waarbij aan de hand van Prune Data nagegaan wordt of de boom goed gebouwd is. Takken die veel fouten geven op de Prune Data worden 12

13 Hoofdstuk 1. Inleiding 13 weggesnoeid. Lineaire Modellen zijn van de vorm y = β 0 + β 1 x 1 + β 2 x β n x n, waar y ofwel een regressievariabele, ofwel een aanduiding van de klasse is (bv: y > 0 peak). Alle waarden voor β n worden bepaald door het algoritme, de waarden voor x zijn de waarden voor de attributen van een bepaald datapunt. Nominale attributen worden eerst omgezet in numerieke: een nominaal attribuut met n waarden wordt bijvoorbeeld afgebeeld op n-1 numerieke attributen die elk 1 zijn voor één bepaalde, unieke waarde van het attribuut 1. Het algoritme gaat meestal de betawaarden kiezen op een manier die één of andere foutmaat zo klein mogelijk maakt, ofwel door rechtstreekse berekening (lineaire regressie) of met één of andere heuristiek (zoals wij doen met Least Median Squares) Foutmaten Tot slot nog een woordje over de verschillende manieren om de fout van een algoritme te bepalen. Deze hebben allen hun voor- en nadelen, maar algoritmes die voor een bepaalde dataset beter werken, hebben meestal betere waardes voor alle foutmaten dan minder geschikte algoritmes. Toch is het zeker nuttig te weten wat elke waarde juist betekent. Mean Error: het gemiddelde van de absolute waarde van alle fouten die het algoritme maakt op de testdata. Root Mean Square: afgekort RMS, de vierkantswortel van het gemiddelde van kwadraten van diezelfde verschillen. Door de fouten te kwadrateren en pas na het nemen van het gemiddelde weer de vierkantswortel te nemen, wordt er bij RMS veel meer belang gehecht aan grote fouten dan aan kleine. Relative Absolute Error: vergelijkt de fouten die het algoritme maakt met de fouten die een zeer eenvoudig algoritme zou maken. Dit zeer eenvoudig algoritme is er eentje dat altijd het gemiddelde van de regressiewaarde voor de trainingsdata teruggeeft. Root Relative Square: gebruikt hetzelfde trucje als de Root Mean Square, maar dan toegepast op de Relative Absolute Error. Men neemt de som van kwadraten van de vergelijkingswaarden, en daar weer de wortel van. 1 Het n-de attribuut wordt weggelaten, zijn waarde wordt volledig bepaald door de n-1 attributen er voor. 13

14 Hoofdstuk 1. Inleiding 14 Root Median Square: het broertje van RMS, waar de mediaan gebruikt wordt in plaats van het gemiddelde. Hecht juist zeer weinig belang aan uitschieters. 14

15 Hoofdstuk 2 Data Voor onderzoek en experimentatie tijdens de masterproef gebruiken we een back-up van de data die Televic ter beschikking heeft. Dit heeft twee belangrijke voordelen: - Er is geen verbinding nodig met de Televic-server. We kunnen de tests direct laten lopen op het toestel waarop ze geschreven zijn, zonder dat we een netwerkverbinding nodig hebben. - We hebben een consistente set testdata. Deze data zal gegarandeerd niet veranderen tussen tests. De data komt in twee vormen, die we hieronder zullen beschrijven. 2.1 De databank De databank bevat het overgrote deel van de informatie die we zullen gebruiken. Ze draait op een MS SQL Server. De data loopt terug tot september 2008, toen Edumatic 3 voor het eerst online kwam. De eerste paar maanden is er relatief weinig activiteit, en zijn er softwaretests die valse activiteit genereren, en om die reden, en om het tellen makkelijk te maken, gebruiken we de data vanaf 1 juli De back-up werd gemaakt op 1 juli 2013, zodat we volledige data hebben tot en met 30 juni van dat jaar. Het gebruik van gehele jaren heeft ook als voordeel dat onze algoritmes evenveel belang zullen hechten aan elk moment van het jaar. De databank bevat natuurlijk veel informatie, en een groot deel daarvan hebben we niet nodig. Hieronder een overzicht van de tabellen die voor ons wel interessant zijn. 15

16 Hoofdstuk 2. Data 16 PackageSessionNodes en PackageSessionNodesFinished: Bevat oefeningen die gebruikers aan het maken zijn of gemaakt hebben. Omdat elke gebruiker slechts één oefening tegelijk kan maken; kunnen we hieraan zien hoeveel gebruikers er oefeningen aan het maken zijn. Van elke oefening wordt het begintijdstip en de duur bijgehouden, we kunnen dus op elk tijdstip zien hoeveel gebruikers er online zijn. Channel: Bevat de channels, of gebruikersgroepen. Per school komen er één of meerdere channels bij, dus met het aantal channels hebben we een ruwe maat voor het aantal ingeschreven scholen. Enkel het tijdstip waarop ze zijn aangemaakt interesseert ons. MaxChannelUsers: Bevat voor elk channel het maximaal aantal users dat ooit tegelijk online was. We hebben dus met andere woorden een maat voor de grootte van de channels. ChannelUserManagement: Een andere mogelijkheid is gewoon het tellen van het aantal users in een channel, maar het is zeer goed mogelijk dat een channel een groot aantal inactieve users bevat. In ieder geval is het deze koppeltabel die deze optie mogelijk maakt. Ze verbindt gewoon een aantal Users en channels. 2.2 Logs Behalve de databank zijn er ook log files, die de up-en downloadhoeveelheden van de server bijhouden. Deze worden automatisch gegenereerd door de (Windows) Server en kunnen gemakkelijk geparset worden met behulp van Microsoft Log Parser. De enige informatie die hier voor ons van belang is, is de hoeveelheid up- en download in bytes. Microsoft Log Parser werkt met SQL-achtige query s. Een probleem dat we zijn tegengekomen is dat de som van de waarden die we nodig hebben vaak te groot is voor Log Parser, waardoor we zullen moeten werken met het gemiddelde, dat we dan pas in het programma zelf vermenigvuldigen met het aantal records. 2.3 Features Overzicht van Gebruikte Features Uit deze bronnen hebben we een aantal features gehaald, die we zullen gebruiken om onze data te modelleren. De meeste ervan kunnen we verdelen in logische groepen. 16

17 Hoofdstuk 2. Data 17 DateTime Features Deze beschrijven de dag waarop het meetpunt valt. We hebben: DayOfMonth DayOfYear Month Weekday Hiervan is enkel Weekday een nominale feature, de andere zijn numeriek. Holiday Features Waar de DateTime features enkel rekening houden met de absolute datum, gaan we bij de Holiday features kijken hoe een datum zich verhoudt tot de vakantie. Terwijl dit weinig verschil maakt in juli en augustus, zal bijvoorbeeld de paasvakantie elk jaar op een ander, moeilijk te voorspellen moment vallen 1. Daarom zullen we het programma laten weten waar de vakanties vallen. Holiday : Is ofwel de vakantie die op die datum valt, ofwel wordt hierin aangeduid dat er geen vakantie is en wat de vorige vakantie was. Het jaar wordt zo opgedeeld in 12 nominale periodes. DayOfHoliday : Hoe lang de vakantie al bezig is. DaysTillEndOfHoliday : Hoe lang de vakantie nog duurt. De Holiday Features zijn er gekomen nadat de resultaten van de eerste Weka-tests (Hoofdstuk 4) duidelijke afwijkingen vertoonden tijdens de vakanties. Het verschil is duidelijk te zien op figuren 2.1 en 2.2. Lookback Features De lookback features geven het algoritme inzicht in wat er de voorbije dagen gebeurd is. De features zijn steeds van de vorm LookbackXY waarbij X een eigenschap is en Y het aantal dagen dat men terugkijkt. Zo geeft de feature LookbackClass2 de klasse van twee dagen geleden. De meeste van deze lookbacks komen terug als target of te zoeken waarde voor de huidige dag. 1 Het voorspellen is eigenlijk makkelijk, maar enkel als men het algoritme kent. 17

18 Hoofdstuk 2. Data 18 Figuur 2.1: Grafiek van voorspelde waarden, zonder Holiday Features. Figuur 2.2: Grafiek van voorspelde waarden, met Holiday Features. LookbackClassY: De klasse van het datapunt Y dagen geleden LookbackExcercisesY: Het totaal aantal gemaakte oefeningen Y dagen geleden 18

19 Hoofdstuk 2. Data 19 LookbackUniqueY: Het aantal unieke gebruikers Y dagen geleden LookbackUPCY: Het aantal UPC (Users Per Channel) Y dagen geleden. Met andere woorden: het gemiddelde aantal gebruikers per ingeschreven kanaal. LookbackUPMY: Het aantal UPM (Users Per Max) Y dagen geleden. Welk deel van de ingeschreven gebruikers er online was. LookbackUsersY: Het absolute aantal gebruikers Y dagen geleden Feature Selection De meeste classifiers hebben moeite met meer dan 50 features, en werken beter (en natuurlijk veel sneller) met een tiental attributen. Welke attributen dat zijn, en waar het kantelpunt ligt waarop meer attributen het model minder goed maken, hangt af van het algoritme en de data. In een ideale situatie zouden we dus alle beschikbare algoritmes moeten testen tegenover alle mogelijke subsets van features. Met onze 66 features hebben we 2 66 subsets 2. Uit de Weka-classifiers hebben we er voor regressie 13 uitgekozen om te testen (Zie Classifiers, 4.4). Als we dus elke milliseconde één classifier met één subset zouden testen, zijn we /1000 = seconden bezig, of twee maal de (vermoedelijke) leeftijd van het universum. We zullen dus een andere strategie moeten gebruiken. Hill Climbing Hill Climbing is het eenvoudigste feature selection algoritme dat we hebben. We beginnen met een lege set features. In elke stap gaan we dan kijken wat het beste attribuut is om toe te voegen aan, of te verwijderen uit, de set van features waarop we ons model baseren. Uit alle opties kiezen we de beste, en dan gaan we voort met de nieuwe set. Dit is een greedy algoritme, en het heeft een groot nadeel: het raakt makkelijk vast in lokale maxima. Als we bijvoorbeeld vinden dat de set Holiday, DayOfWeek, LookbackUsers1 een goed resultaat geeft, beter dan alle sets die we kunnen maken door er een feature aan toe te voegen of uit te verwijderen, dan gaan we niet verder zoeken, zelfs al zou de toevoeging van LookbackUnique7 en DayOfHoliday de resultaten nog beter maken. Simulated Annealing Simulated Annealing (Press et al., 2007) is een alternatief voor Hill Climbing. Het algoritme is gelijkaardig: in elke stap bekijken we de buren van onze huidige set, en we vervangen 2 Of , als we de lege set niet meerekenen. 19

20 Hoofdstuk 2. Data 20 de huidige set door een betere buur. interessanter maken. Toch zijn er enkele verschillen die het algoritme - We bekijken niet alle buren van de huidige set. In plaats daarvan bekijken we telkens één willekeurige buur en beslissen of deze al dan niet goed genoeg is om de huidige set te vervangen. - We verruimen de definitie van het woord beter een beetje. Een buur moet niet per se een beter resultaat geven om voor vervanging in aanmerking te komen. Hoe langer het algoritme bezig is, hoe strenger we worden op de selectie van nieuwe sets. Dit geeft het algoritme de kans om voorbij lokale minima te bewegen. - We stoppen niet als we in een (mogelijk lokaal) optimum zijn aangekomen, maar na een vast aantal stappen. Vooral het tweede puntje is hier belangrijk. Bij Hill Climbing gaan we onze huidige set enkel wisselen met een buur als de buur beter is. Bij Simulated Annealing gaan we sowieso wisselen als de buur beter is, maar heeft een mindere buur ook een kans om gewisseld te worden, afhankelijk van de temperatuur. In het begin is die temperatuur hoog, en hebben ook minder goede buren veel kans om toch aan bod te komen. Dan laten we de temperatuur afkoelen, waardoor de kans dat sets gewisseld worden voor slechtere sets ook daalt. De formule die we gebruiken is: ( p = min 1, e s nieuw s oud T Hierbij is s een score voor hoe goed een set is, en T de huidige temperatuur. p is dan de kans op wisselen. We kunnen natuurlijk voor s ook een foutwaarde gebruiken (hoe slecht een set is) en deze negatief maken. Bij goede buren bekomen we: ) s nieuw s oud (T >0) s nieuw s oud T 0 e s nieuw s oud T 1 p = 1 20

21 Hoofdstuk 2. Data 21 Op dezelfde manier zien we dat p voor slechte buren kleiner is dan 1, en dichter bij 1 ligt naarmate het verschil kleiner wordt, of t groter is. Voor T = 0 hebben we het speciaal geval waar de breuk gelijk is aan ±, wat neerkomt op p = 0 voor slechte scores en p = 1 voor goede scores 3. Dan zijn we dus terug bij het Hill Climbing systeem. We hebben vermeld dat de temperatuur daalt in ons systeem, maar nog niet hoe. We lezen (Press et al., 2007) dat het bepaalen van een koelschema afhankelijk is van de data, en vaak een trial-en-error proces. We zullen dus zelf een schema ontwikkelen. Gegeven een aantal iteraties n en een begintemperatuur t 0 zoeken we een temperatuursfunctie t(i), waar i het aantal gepasseerde iteraties is. We weten dat t(0) = T 0 en t(n) = 0. Nu willen we niet dat ons algoritme te lang in de hogere temperaturen blijft hangen, aangezien dat na een bepaald aantal iteraties zo goed als zinloos wordt - we gaan dus een exponentiële functie gebruiken om de temperatuur te doen afnemen. Met t(i) = T 0 a i (a ]0, 1[) komen we natuurlijk nooit aan 0, dus voegen we een lineaire term toe: t(i) = T 0 (a i bi). Om op 0 uit te komen voor n = i krijgen we b = an. Op grafiek 2.3 n zien we deze functie voor verschillende waarden van a (voor T 0 = 1). Figuur 2.3: Temperatuurwaardes voor exponentiële koeling. 3 En p onbepaald voor s nieuw = s oud. 21

22 Hoofdstuk 2. Data 22 Als alternatief voor de exponentiële functie onderzoeken we ook eens een polynomiale. Een tweedegraadsveelterm lijkt een goede start als we de gewenste vorm van de curve in gedachten houden. Omdat we willen dat de de functie vlak wordt als onze temperatuur naar 0 gaat, stellen we als extra voorwaarde dat t (n) = 0. We hebben dan drie vergelijkingen (ook t(0) = T 0 en t(n) = 0) voor drie onbekenden, en kunnen onze tweedegraadsvergelijking berekenen: t(i) = ai 2 + bi + c t(0) = T 0 a0 2 + b0 + c = T 0 c = T 0 t(n) = 0 an 2 + bn + T 0 = 0 b = an T 0 n t (n) = 0 2an + b = 0 2an an T 0 n = 0 a = T 0 b = 2T 0 n 2 n Dit geeft een functie die voldoet aan onze voorwaarden, maar ze is niet verder aanpasbaar. We houden graag een beetje flexibiliteit. Voor 3 vergelijkingen en 1 vrijheidsgraad hebben we een derdegraadsvergelijking nodig. We berekenen: t(i) = ai 3 + bi 2 + ci + d t(0) = T 0 a0 3 + b0 2 + c0 + d = T 0 d = T 0 t(n) = 0 an 3 + bn 2 + cn + T 0 = 0 c = an 2 bn T 0 n t (n) = 0 3an 2 + 2bn + c = 0 2an 2 + bn T 0 n = 0 b = 2an + T 0 c = an 2 2T 0 n 2 n De waarde van a is vrij te kiezen. Daar moeten wel een opmerking bijkomen: de temperatuur moet steeds tussen T 0 en 0 blijven. Of: t(i) [0, T 0 ] i [0, n]. We hebben een derdegraadsfunctie, met een lokaal extremum (t = 0) op n. Een derdegraadsfunctie heeft maximum 2 lokale extrema. Aangezien één ervan in n ligt, zal er ten hoogste één in [0, n[ liggen. De functie kan dus in dat interval één maal van stijgend naar dalend gaan, of omgekeerd. Dan geldt: - Ofwel stijgt de functie in 0. Dan is er een interval ]0, m] waarbinnen alle waarden boven T 0 liggen (met m < n). - Ofwel daalt de functie in 0. Dan ligt geen enkele waarde in [0, n] boven T 0. Dat zou namelijk twee extrema nodig hebben - één om te beginnen stijgen, één om terug in de richting van 0 te gaan. 22

23 Hoofdstuk 2. Data 23 - Ofwel is er een extremum in 0. Dan is de functie strikt dalend in ]0, n[ en ligt dus ook geen enkele functiewaarde in dat interval boven T 0. i is in dit geval steeds discreet. We weten dus dat, als er een discrete waarde m in [0, n] is groter dan T 0, is het interval ]0, m] T 0 met m 1 en dus t(1) T 0. We hebben dan een nodige en voldoende voorwaarde voor t(i) T 0 : t(i) T 0 i [0, n] t(1) T 0 Analoog kunnen we aantonen dat t(i) 0 i [0, n] t(n 1) 0 Hieruit kunnen we grenzen voor a afleiden: t(1) T 0 a 2an + 1 n 2 + an2 2 n 0 2n 1 a n 2 2n 3 + n 4 a 2n 1 n 2 (n 1) 2 t(n 1) 0 an a + 1 n 2 0 a 1 n 3 1 2n 1 Merk op dat 0 en 1 0; a kan altijd 0 zijn wat ons de tweedegraadsvergelijking van hiervoor oplevert. Voor n = 1 hebben we als grenzen ±, omdat de n 2 (n 1) 2 n 3 1 waarde van a dan niet meer uitmaakt: t(0) = T 0 t(1) = 0 voor alle mogelijke a. Ook hebben we in de voorwaarde niet gesteld dat een (theoretische) t( 1 2 ) kleiner moet zijn dan T 0, de voorwaarden gelden enkel voor discrete waarden. Grafiek 2.4 toont de functies voor maximale en minimale a, en een aantal tussenstappen. De functie voldoet uiteraard aan de voorwaarden, maar lijkt toch niet zo flexibel als de originele exponentiële functie. Zelfs op de minimumwaarde duurt het zeer lang voor we aan een lage temperatuur komen. We zullen dus de exponentiële blijven gebruiken. 23

24 Hoofdstuk 2. Data 24 Figuur 2.4: Temperatuurwaardes voor polynomiale (3e graad) koeling. 24

25 Hoofdstuk 3 C4.5 Een eerste algoritme dat zal besproken worden is het C4.5-algoritme (Quinlan, 1993). Dit werd ontwikkeld door J.R Quinlan tussen 1987 en 1998, en is nu, 16 jaar later, nog steeds één van de werkpaardjes van de Machine Learning algoritmes. Het heeft voor vele problemen een goede balans tussen snelheid en precisie, wat zijn populariteit natuurlijk deels verklaart, maar het is ook relatief eenvoudig - zowel in programmatie als in het begrijpen van de output. In 2006 vroeg de International Conference of Data Mining de onderzoeksgemeenschap te stemmen op hun favoriete Data Mining algoritme, met als resultaat dat C4.5 (nipt) het populairst was (Wu & Kumar, 2006). Het algoritme toont dus nog geen sporen van ouderdom. C4.5 bouwt een beslissingsboom op uit een hoeveelheid traindata. Dit gebeurt recursief, en top-down. Een algemene beschrijving van de werking van beslissingsbomen vindt u in de inleiding, we gaan hier verder in op de punten die C4.5 zo succesvol gemaakt hebben. C4.5 is, zoals de meeste beslissingsbomen, een classificatie-algoritme, wat betekent dat al onze tests gebeurden op de tweeledige verdeling PIEK - GEEN PIEK. Met PIEK bedoelen we dan dat het aantal voorspelde gebruikers boven een bepaalde waarde ligt. De boom wordt recursief opgebouwd uit de traindata. We beginnen met een zeer eenvoudige boom, die één enkele knoop bevat en alle trainingsdata. Afhankelijk van de data hebben we dan volgende mogelijkheden: - Alle trainingsdata behoort tot dezelfde klasse. De knoop is een blad, en dit deel van de recursie is voorbij. - De trainingsdata behoort tot verschillende klassen. We zoeken een attribuut waarop 25

26 Hoofdstuk 3. C we de boom kunnen indelen - bijvoorbeeld, dag van de week. De knoop wordt een Node en we krijgen deelbomen - in dit geval 7. We gaan dan verder met elk van de deelbomen tot uiteindelijk alle traindata in een blad terechtkomt. Maar hoe bepalen we op welk attribuut we splitsen? Daarvoor gebruiken we een maat die Gain Ratio wordt genoemd. Deze is afhankelijk van de informatie of entropie in de data. Entropie berekenen we als volgt: m E(S) = ((F i ) log (F i )) i=1 Hierbij is m het aantal klassen, in ons geval dus 2, en F i is de frequentie van klasse i in de trainingsdata. Bij ons zal F piek bijvoorbeeld veel kleiner zijn dan F geenpiek. Deze formule is een maat voor de heterogeniteit van een dataset: hoe hoger de entropie, hoe meer verschillende klassen er zijn en hoe gelijker ze verdeeld zijn over de dataset. Voor elke mogelijke keuze van attribuut bekijken we de entropie van de deelgroepen, gewogen samengeteld. volgende formule gebruiken: Indien we bijvoorbeeld zouden splitsen op weekend zouden we E(S weekend + S week ) = 2 7 E(S weekend) E(S week) Deze gebruiken we om de entropie te berekenen die overblijft na de splitsing. Een goede splitsing zal ervoor zorgen dat de klassen ongelijk verdeeld worden over de deelbomen, waardoor de nieuwe entropie minder zal zijn dan de originele entropie. We kunnen dan de Info Gain berekenen: G splitsing = E(S) E(S gesplitst ). Deze Info Gain is een eerste waarde waarop we een attribuut kunnen kiezen, maar die blijkt in praktijk een voorkeur te hebben voor attributen met veel verschillende waarden. We moeten dus een factor inbouwen die deze voorkeur neutraliseert. Daarvoor kiest Quinlan voor Split Info : In plaats van te kijken naar de informatie bij verdeling in klassen, gaan we eens kijken naar de informatie van de verdeling in subsets volgens het gekozen attribuut. We hebben dus: SI splitsing = k ((T i ) log (T i )) i=1 Waarbij T i de (relatieve) hoeveelheid training cases is die in subset (en subtree) i zou terechtkomen. We berekenen dan de uiteindelijke beslissende factor, de Gain Ratio, als: GR splitsing = E(S) E(S gesplitst) SI splitsing 26

27 Hoofdstuk 3. C Als we die Gain Ratio berekenen voor elk attribuut, en we dat herhalen in elke knoop, splitst de boom op elk niveau op het interessantste attribuut - het attribuut dat zoveel mogelijk instances met een gelijke klasse in dezelfde groep stopt, en zo weinig mogelijk verschillende klassen bij elkaar. Het C4.5 algoritme is dus een greedy algoritme in het kiezen van zijn attributen - het kiest altijd voor wat op dat moment het best lijkt. Een laatste deel van het basisalgoritme is pruning. Deelbomen hebben de neiging te gaan overfitten : ze passen zich zodanig aan aan de trainingsdata dat ze ook de fouten en uitlopers, die er bij real-life data onvermijdelijk insluipen, zoveel mogelijk gaan meerekenen. Een voorbeeld: bij studietijdmetingen worden de resultaten van studenten en de tijd besteed aan het studeren vergeleken. Een goede student is echter vergeten zijn gestudeerde uren in te geven, waardoor in de databank bij die student 0 uur wordt opgeslagen. Een typische beslissingsboom zal dan besluiten dat studenten goeie punten halen als ze meer dan 3 uur leren voor een bepaalde test, maar ook als ze helemaal niet leren. Daarom wordt de C4.5-boom gepruned (gesnoeid): takken die niet goed zijn worden weer uit de boom geknipt en vervangen door een blad. Hoe gebeurt dat prunen? De basisimplementatie (we gaan later zien dat het anders kan) voorziet een deel van de trainingsdata als prune data. Deze data wordt niet gebruikt bij het trainen zoals hierboven beschreven, maar wordt achter de hand gehouden tot de boom klaar is. Daarna gaan we de prune data laten classificeren door de boom, en elke tak die te veel fouten geeft op de prunedata, wordt weer gesnoeid. 3.1 Initiële status Toen we begonnen met de masterproef was er al een eenvoudige C#-implementatie van het C4.5 algoritme. Deze bevatte de functionaliteit zoals hierboven beschreven, voor nominale attributen. Nominale attributen zijn deze die één bepaalde waarde uit een groep mogelijke waarden meegeven aan een instance; bijvoorbeeld, weekend (ja, nee) of kleur (rood, geel, blauw). Initiële tests toonden een accuraatheid van het basisalgoritme van 96 tot 97%, afhankelijk van de (willekeurige) verdeling tussen train en prune data. Op het eerste zicht is dit zeer goed: een algoritme dat in 96% van de gevallen kan voorspellen wat er zal gebeuren. We moeten hier echter een zeer belangrijke opmerking bij maken: 97% van de gevallen zijn 27

28 Hoofdstuk 3. C geen pieken. Met andere woorden: een service die gewoon nooit van zich laat horen zou ook voor 97% correct zijn. We bekijken het probleem eens van naderbij. V K GEEN PIEK PIEK GEEN PIEK PIEK Tabel 3.1: Fouten bij klassering van gebruikersdata, Klasse vs Voorspelling Zoals te zien in tabel 3.1 zijn er relatief veel fouten op de pieken zelf. Het algoritme weet wel dat er meestal geen pieken zijn, maar weet niet goed wanneer er dan wel zijn: minder dan een derde van de pieken wordt voorspeld, en voor elke voorspelde piek is er ook een false positive. Dit is natuurlijk niet goed genoeg voor wat we nodig hebben, tijd dus om het algoritme onder handen te nemen. 3.2 Uitbreidingen Grouping Met grouping bedoelen we het samennemen van mogelijke attribuutwaarden, en de behandeling ervan als één waarde. We kunnen bijvoorbeeld splitsen op het attribuut weekdag. Bij een standaardboom zouden we natuurlijk zeven deelbomen uitkomen, maar het kan zijn dat de deelbomen van zaterdag en zondag goed op elkaar lijken, en misschien deze van dinsdag en donderdag ook wel. Dan is het misschien interessant om die bomen ook samen te nemen. Grouping doet juist dat, en ook hier wordt een greedy algoritme gebruikt. We beginnen, zoals bij de standaardboom, met het berekenen van de gain ratio van een attribuut. Maar daar blijft het niet bij: we gaan voor elke combinatie van twee waarden kijken of de groepering een beter resultaat oplevert. Als dat zo is, herhalen we het procedé voor de nieuwe splitsing. Een voorbeeld kan misschien wat duidelijkheid brengen. Dit keer gebruiken we een weersvoorspelling, en het attribuut dat we bekijken is Windrichting. We houden het simpel: de wind kan uit een van de vier hoofdrichtingen komen. De gewone verdeling op windrichting zoals bovenaan in tabel 3.2 geeft een gain ratio van We kijken of dat beter kan in het tweede blok, waar we alle mogelijke combinaties 28

29 Hoofdstuk 3. C Verdeling Gain Ratio noord, oost, zuid, west (noord, oost), zuid, west (noord, zuid), oost, west (noord, west), zuid, oost noord, (oost, zuid), west noord, (oost, west), zuid noord, oost, (zuid, west) (noord, oost, zuid), west (noord, oost, west), zuid (noord, oost), (zuid, west) Tabel 3.2: Groepering van windrichtingen van 2 richtingen uitproberen. Blijkbaar is de verdeling (noord, oost) interessant: een gain ratio van We zijn bezig met een greedy algoritme, dus laten we de rest vallen, en doen we verder met (noord, oost), zuid, west. Dan zijn er 3 mogelijkheden: we kunnen zuid of west bij de noordoostgroep voegen, of ze hun eigen zuidwestgroep laten vormen. Blijkbaar geeft geen enkele van deze mogelijkheden echter een gain ratio van meer dan 0.571, dus laten we het daarbij. We kiezen voor de groepering zoals op de tweede lijn in de tabel. We gaan over elke waarde, en proberen ze te combineren met elke andere. Voor het koppel dat dat het best doet, doen we dit opnieuw, mogelijks tot de waardes verdeeld zijn in twee groepen. Bij elke verdeling moeten we O(n) mogelijke eerste keuzes groeperen met O(n-1) mogelijke tweede keuzes 1, en in totaal gebeurt dit misschien wel tot alle keuzes in 2 groepen verdeeld zijn: O(n-2) maal dus. Dit is dus een O( (n)(n 1) 2 (n 2)) of O(n 3 ) proces 2. En dit voor elk attribuut. Daarbij komt nog dat de diepte van de boom vergroot (als prijs omdat de breedte afneemt), en de vraag is natuurlijk of het dat ook waard is. Het antwoord? Neen. Grouping voegt, althans voor ons probleem, geen meerwaarde toe, zo blijkt uit de tests, terwijl de uitvoeringstijd gemiddeld 100 maal zo groot is. 1 We delen daarna wel door twee, omdat elke groepering nu twee maal geteld is: eens als AB en eens als BA 2 De vraag die ik mij dan stelde was Is een exhaustief algoritme dan slechter dan O(n 3 )? Dat ( kan zeker. ) De Bell Nummers, die weergeven hoeveel mogelijke opdelingen een groep heeft, stijgen aan O. n n log(n) 29

30 Hoofdstuk 3. C Foutberekening De pruning gebeurde oorspronkelijk met aparte prune data. Dat heeft als nadeel dat we niet de hele trainingsset kunnen gebruiken om de boom op te bouwen, en een deel ervan moeten reserveren voor te prunen. Een andere optie is het gebruik van een foutfunctie: gebaseerd op de fouten in de trainingsset doen we een voorspelling van het aantal fouten dat we zouden hebben indien we prune data gehad zouden hebben. Dat klinkt een beetje abstract: we berekenen het aantal fouten op onbestaande data. Toch zijn er formules voor het bepalen van dat virtuele nummer. We gebruiken in onze implementatie de Betafunctie, om precies te zijn de Geregulariseerde Inverse Incomplete Betafunctie. Een woordje uitleg: De Incomplete Betafunctie is een uitbreiding van de gewone Betafunctie. Beiden zijn afhankelijk van twee parameters, a en b. Bij de incomplete betafunctie komt daar een variabele x bij. De regularisering vergelijkt de incomplete en complete betafuncties. De gewone betafunctie: De incomplete betafunctie: B(a, b) = B x (a, b) = 1 0 x De geregulariseerde incomplete betafunctie: 0 t a 1 (1 t) b 1 dt t a 1 (1 t) b 1 dt I x (a, b) = B x(a, b) B(a, b) Deze laatste functie heeft de interessante eigenschap dat er een andere functie mee berekend kan worden: de cumulatieve probabiliteitsfunctie F van een willekeurige, binomiaal verdeelde variabele. Wat kunnen we daarmee doen? Wel, stel dat we een verzwaarde munt hebben. We gooien hem 100 keer op en daarbij gooien we 74 keer kop. Nu weten we natuurlijk dat de kans op kop rond de 74% ligt, maar we weten niet hoe precies onze meting was. Het kan zijn dat we, als we de munt nog eens 100 keer opgooien, we 71 keer kop hebben. Nu kunnen we natuurlijk blijven gooien tot we een enorm aantal worpen hebben, en dan zal de exacte waarde wel vrij dicht bij de uitkomst van het experiment liggen. Deze luxe hebben we helaas niet bij onze gebruikersdata: we hebben de datapunten, en daarmee moeten we het doen. We kunnen geen dagen bijmaken om te kijken hoeveel gebruikers er komen opdagen. We moeten dus als het ware na 100 muntworpen voorspellen wat de kans 30

31 Hoofdstuk 3. C is op kop. Aangezien we dat nooit 100% zeker zijn, werken we met betrouwbaarheidsintervallen. De vraag wordt dan bijvoorbeeld: hoe zeker zijn we dat de echte kans boven de 70 ligt? De binomiale probabiliteitsfunctie geeft daar een antwoord op. Als we uit n = 100 tests een gemiddelde van p = 0.74 successen hebben, wat is de kans dat de gemiddelde waarde boven de k = 0.7 ligt? Wel, die is gelijk aan F (k; n, p), en dan zijn we bijna rond, want die F () valt af te leiden uit de geregulariseerde incomplete betafunctie. F (k; n, p) = I 1 p (n k, k + 1) Dat is natuurlijk handig, maar nog niet wat wij nodig hebben: wij willen net het omgekeerde doen. Gegeven n = 100 en p = 0.74, van welke waarde kunnen we met 0.95 (of 95%) zekerheid zeggen dat de echte waarde erboven ligt? Daarvoor hebben we de inverse betafunctie nodig: F (k; n, p) = a F 1 (a; n, p) = I 1 1 p(a, n) = k Het bepalen van die inverse functie is echter niet makkelijk, en we gebruiken dus een numerieke benadering. Hiervoor gebruiken we de gratis Alglib (Bochkanov (2013)) library. Hoe passen we dit toe op C4.5? Wel, we tellen het aantal tests in de trainingsdata voor een bepaalde tak, en het aantal correcte tests, en we berekenen de waarde waar de echte accuraatheid met 75% zekerheid boven zit. Met andere woorden: we berekenen F 1 (0.75; n, p). Deze waarde zal een pessimistische voorspelling van de accuraatheid berekenen, en deze voorspelling gebruiken we om te bepalen welke takken gepruned mogen worden. Bij het gebruik van de nieuwe errorfunctie is er een lichte verhoging van zowel uitvoeringstijd als correctheid. De uitvoeringstijd is verhoogd door het toevoegen van de I 1 -functie. De alglib library maakt gebruik van numerieke benaderingsmethodes die wel enige tijd vergen voor elke tak van de boom. We zien dat de uitvoeringstijd met 3 tot 4 seconden, of ongeveer 50% stijgt. Wat betreft de accuraatheid: deze ligt gemiddeld iets boven de 97%, wat dus betekent dat we een lichte verbetering hebben, en dat we nu wel beter kunnen voorspellen dan onze altijd neen -service. Als we kijken naar de foutentabel (tabel 3.3) zien we dat er veel minder false positives zijn, maar ook minder correcte positieve waarden. Dit kunnen we intuïtief verklaren: de betafunctie zal bladeren of deeltakken waar veel piekwaarden in zitten eerder wegsnoeien, omdat de voorspelde fout daarop groter is. Er zijn namelijk meer niet-pieken, dus ook in piekbladeren zullen er nog relatief vaak niet-pieken zitten. 31

32 Hoofdstuk 3. C V K GEEN PIEK PIEK GEEN PIEK PIEK Tabel 3.3: Fouten bij klassering van gebruikersdata, beta pruning, Klasse vs Voorspelling Windowing Bij windowing wordt er geprobeerd de traindataset zo klein mogelijk te houden, zonder daarbij in te boeten aan informatie. Dat heeft verschillende voordelen: - De bekomen boom is kleiner. Dat lijkt misschien geen voordeel - een kleinere boom bevat minder nuances, en ook gewoon minder informatie - maar bij windowing wordt ervoor gezorgd dat de boom dezelfde informatie op een eenvoudigere manier weergeeft. Dat is toch de theorie. - De trainingsset kan beter verdeeld worden. We kunnen er van elke klasse een gelijk aantal instances uitpikken. Dit heeft als voordeel dat de boom beter gebalanceerd is tussen de verschillende klassen, toch in het begin, en dat hij in ons geval meer rekening zal houden met de zeldzame pieken dan met de overvloed aan niet-pieken. - De boom is voor een deel random, aangezien de subset waarmee gestart wordt dat ook is. Ook dit lijkt misschien niet meteen een voordeel - maar het stelt ons in staat om verschillende bomen te maken, en daaruit de beste te kiezen, iets wat niet kan zonder windowing. Deze techniek lijkt sterk op het bagging-algoritme, dat we later zullen behandelen. Windowing werkt als volgt: uit de beschikbare traindata kiezen we een subset. Deze set bevat best van elke klasse een gelijke of gelijkaardige hoeveelheid data. Aan de hand van deze subset bouwen we een eerste boom. Dan gaan we kijken in de rest van de data, die nog niet in de boom zit, en zoeken we daar alle instances die fouten opleveren. Daarvan kiezen we opnieuw willekeurig een deel uit, en dat deel voegen we toe aan ons window. Dit blijven we herhalen tot we ofwel de gewenste accuraatheid hebben op de testset, of we de volledige set hebben gebruikt, of de boom niet meer verandert tussen iteraties. De boom die we dan hebben, is de windowed tree. 32

33 Hoofdstuk 3. C Dit algoritme kan eenvoudig herhaald worden. We beginnen met een bepaalde gewenste accuraatheid, bijvoorbeeld 90%. We bouwen een window tree tot we deze accuraatheid overschrijden, en die nieuwe waarde wordt dan het doelwit voor de volgende boom. Zo maken we steeds nieuwe, iets betere windowed trees, tot we merken dat er weinig of geen verbetering meer komt in de bomen en dan nemen we de tot dan toe meest accurate boom. Uiteraard kan dit lang duren, wat dan ook meteen het grootste nadeel is van windowing. Deze multi-window methode geeft een accuraatheid van gemiddeld één procent meer dan de niet-gewindowde boom. Dit kan weinig lijken, maar bedenk dat dat betekent dat de hoeveelheid fouten met een derde tot een vierde afneemt. Dit is ook zichtbaar in het voorbeeld van tabel 3.4: er wordt nu bijna 40% van de pieken correct geklasseerd, in plaats van 30, en het aantal false positives wordt zelfs bijna gehalveerd in vergelijking met het originele algoritme. De uitvoeringstijd is er dan ook naar: het bouwen van een standaardboom duurde op onze machine tussen de 6 en 7 seconden, en bij onze windowtests worden er zo rond de 100 gebouwd, wat neerkomt op een tiental minuten. Toch hoeft een uitvoeringstijd van tien minuten geen obstakel te zijn, aangezien het algoritme niet meer dan een maal per dag moeten trainen. V GEEN PIEK PIEK K GEEN PIEK PIEK Tabel 3.4: Fouten bij klassering van gebruikersdata, windowmethode, Klasse vs Voorspelling Rulesets We kijken eens naar de boom zelf. Een vlugge recursieve printfunctie gaf het resultaat in figuur 3.1. We zien daarin meteen een probleem opduiken: vele bladeren bevatten maar één trainingscase. Dat is een veelvoorkomend probleem in data mining. Data met veel attributen, of dimensies, is voor de meeste ML-algoritmes veel moeilijker aan te pakken dan data met weinig attributen. Dit heet the curse of dimensionality, en een mooie uitleg daarvan is de volgende: Stel, je hebt data met één attribuut. Dan kunnen we die datapunten uitzetten op een ééndimensionale grafiek, een lijnstuk dus. Laat ons zeggen dat alle waarden uniform verdeeld zijn tussen 0 en 1. We willen nu de klasse voorspellen van een datapunt met een bepaalde waarde op die as, en daarvoor bekijken we één honderdste van de data die we al hebben, namelijk het deeltje dat rond het gezochte datapunt ligt. 33

34 Hoofdstuk 3. C Figuur 3.1: Deel van de boom, uitgeprint in consolevenster. HourOfDay en DayOfWeek zijn de attributen, 10, 11, 17,... zijn de mogelijke waarden. De 20, 10, 24, 27 onderaan zijn nog een deel van de keuzemogelijkheden voor het attribuut DayOfMonth. Als we bijvoorbeeld willen weten welke klasse een datapunt met attribuutwaarde 0.4 zou hebben, gaan we de meest voorkomende klasse nemen in het interval [0.395; 0.405] 3. Tot daartoe geen probleem. Maar stel dat we de dan de data gaan uitbreiden naar twee dimensies (of attributen). We zetten de data opnieuw uit op een grafiek, ditmaal eentje met 3 Dit is een variant op het K-nearest-neighbours algoritme 34

35 Hoofdstuk 3. C twee dimensies. We willen dan de klasse bepalen van het punt (0.1, 0.5). Als we opnieuw het meest naburige honderdste van de data willen, moeten we een vierkant 4 tekenen rond het gezochte punt, met oppervlakte 1/100. De zijden van dat vierkant hebben als lengte natuurlijk 0.1; 10 maal zo lang als het daarnet gebruikte interval. Met 1 attribuut hadden we genoeg aan een verschil van 0.01; maar nu kunnen beide attributen 0.1 verschillen! Dit zorgt er natuurlijk voor dat de datapunten die we onderzoeken verder uit elkaar liggen, en dus waarschijnlijk minder goed reflecteren wat de klasse van het zoekpunt zou moeten zijn. We kunnen dit ook uitleggen bij onze boom: we hebben zodanig veel attributen in onze data, dat er weinig punten dicht bij elkaar liggen. Er is data van vier jaar, wat veel lijkt, maar op die vier jaar was er maar één datapunt op woensdag 4 april. De dag van het jaar en de weekdag zijn 2 attributen waarop we kunnen splitsen, en inderdaad, als we op beide splitsen hebben we een zeer uitgewaaierde boom. Tijd om daar iets aan te veranderen, bijvoorbeeld met rulesets. Rulesets zijn eigenlijk zeer eenvoudig, en aan de boom zelf verandert er alvast niet veel. Wat we doen is het volgende: we beginnen met een standaard (of gewindowde) boom en gaan dan één voor één de bladeren af. Van elk blad maken we een regel, met als voorwaarde de beslissingen in de nodes en als resultaat de klasse van het blad. Bijvoorbeeld: in onze boom zouden we een regel kunnen hebben van de vorm: MONT H = December DAY OF MONT H = 11 HOUR = 19 DAY = Maandag CLASS = P iek Deze gaan we dan verfijnen. Eerst gaan we de onnuttige zaken weghalen. In onze regel kan het bijvoorbeeld zijn dat de voorwaarde DAY = M aandag niet echt belangrijk is. Dit kunnen we uitzoeken door er opnieuw onze IRIB (Inverse Regularized Incomplete Betafunction) op los te laten. Dit doen we, een beetje zoals bij groeperen, greedy: we halen maandag zonder meer weg als het niet nuttig lijkt, en gaan dan verder met de resterende drie voorwaarden. 4 of cirkel, maar een vierkant is makkelijker en het resultaat is hetzelfde 35

36 Hoofdstuk 3. C Na aanmaken en verfijnen is er een derde belangrijke stap: het vergelijken. Stel dat we de regel van hierboven eens hebben met maandag en eens met dinsdag. Bij de eerste regel wordt de dag weggelaten, bij de tweede niet. Dan valt de tweede regel volledig binnen de eerste, en kunnen we die dus weglaten. Ook als bij beide regels de dag verdwijnt kunnen we één ervan weglaten. Regels kunnen elkaar ook tegenspreken: een eerste regel kan zeggen dat alle dinsdagen in april niet pieken, een tweede dat elke 14 e van april dat wel doen. Dit lossen we niet op: we gaan enkel de rulesets in volgorde zetten, van accuraatste naar minst accurate. In een laatste stap halen we die regels die nog steeds te veel fouten geven weg. Omdat het dan mogelijk is dat we een testdatapunt niet kunnen classificeren, hebben we ook een defaultklasse nodig. Dat wordt de klasse die het meest voorkomt in de trainingscases die nu geen regel hebben. Een eerste minpunt van die rulesets is dat ze heel lang duren. Net zoals grouping een O(w 3 ) stap toevoegde, is dat bij regels O(a 2 ). Hierbij is a het aantal attributen. Vergeet echter niet dat voor elk van die a 2 waarden een relatief dure numerieke benadering moet gebeuren. In praktijk zien we dat het algoritme van grootteorde 10 seconden niet naar 10 minuten, maar meteen naar enkele uren verhoogt. Dit is natuurlijk te lang als we dat elke dag moeten doen, maar stel dat we één maal per jaar het algoritme hertrainen, dan is dat nog aanvaardbaar. Op voorwaarde natuurlijk dat de verhoging in accuraatheid voldoende is. Uit onze tests blijkt een gemiddelde accuraatheid van net geen 97%. We doen het dus niet beter dan een standaard tree, en dat voor een enorm verschil in uitvoeringstijd, dus dit is niet wat we nodig hebben. Met een totale accuraatheid tussen de 97 en 98% (met windowing) en een uitvoeringstijd (of beter: traintijd) van een tiental minuten ronden we de experimenten met C4.5 af. We hebben het algoritme uitvoerig getest omdat het al deels aanwezig was en we daarop konden voortbouwen, maar we moeten ons natuurlijk ook afvragen of we met andere algoritmes betere resultaten kunnen halen. 36

37 Hoofdstuk 4 Weka C4.5 is een mooi algoritme, maar we blijken in de praktijk geen fantastische resultaten te halen. Dat kan aan de dataset liggen, maar het is ook mogelijk dat C4.5 niet het beste algoritme is voor de taak die we hebben. Verder is C4.5 oorspronkelijk ontwikkeld voor classificatie (hoewel het kan uitgebreid worden om ook regressie aan te kunnen). Via regressie hebben we de mogelijkheid om niet alleen de klasse (PIEK - GEEN PIEK) te voorspellen, maar ook het exacte aantal gebruikers. Om deze redenen is het zeker de moeite om andere mogelijke algoritmen onder de loep te nemen. We worden in deze taak bijgestaan door de Weka tool (Hall et al., 2009), die een groot aantal algoritmes verzamelt onder een handige UI. Hiermee kunnen we een aantal verschillende aanpakken testen zonder daarbij elk te testen algoritme te moeten implementeren. 4.1 Verkenning Als we de Weka-software opstarten, krijgen we de keuze tussen 4 deelapplicaties. Twee ervan, de KnowledgeFlow en de Simple Command Line Interface, zullen we niet gebruiken. De derde applicatie is de Explorer - deze laat ons toe de data op verschillende manieren te onderzoeken, en er de algoritmes van Weka op toe te passen. De vierde, de Experimenter, is misschien nog handiger dan de Explorer: de experimenter laat ons toe een databestand door verschillende algoritmes te halen in één enkel experiment, waarna we dan de resultaten kunnen vergelijken. Ook kunnen we verschillende databestanden na elkaar laten classificeren, om bijvoorbeeld te onderzoeken hoe goede traindata in elkaar zit. 37

38 Hoofdstuk 4. Weka Het arff - formaat Weka leest data in in het arff-formaat. Arff, wat staat voor Attribute Relation File Format, is een uitbreiding op het eenvoudige csv-formaat. Net als bij csv worden de waarden van de features van één enkel datapunt na elkaar gezet, gescheiden door een komma. Ontbrekende waarden worden vervangen door een vraagteken. Verder voegt arff nog een hoofding toe: de 1, met daarna de naam van de dataset, dan een elk gevolgd door de naam van een attribuut en zijn type, en dan een die aangeeft dat vanaf daar de csv-data begint. In het csvgedeelte staan de attributen telkens in de volgorde zoals gedeclareerd in de hoofding. Het type attribuut is ofwel NUMERIC, ofwel een opsomming van klassen. Weka ondersteunt ook DATE en STRING, maar deze gebruiken we niet. DATE geeft namelijk minder goede resultaten dan datums 2 verpakt in een aantal numerieke attributen (zoals Maand, DagVanMaand, DagVanJaar), terwijl STRING bedoeld is om bijvoorbeeld kernwoorden uit teksten te halen. Een weekdag Ma, Di, Wo, Do, Vr, Za, dagvanmaand maand vakantie Kerst, Krokus, Paas, Zomer, Herfst, gebruikers Vr,4,4,Geen,75 Za,5,4,Geen,14 Zo,6,4,?,12 Ma,7,4,Paas,23 declaraties zijn niet case-sensitive, maar de hoofdletters maken de structuur van het document duidelijker. 2 Het meervoud data zou voor verwarring kunnen zorgen. 38

39 Hoofdstuk 4. Weka 39 Het grote voordeel van arff over csv is dat Weka al weet welke attributen er voor elke instance zullen zijn voor ze ingelezen worden. Ook is het onderscheid tussen nominale en numerieke attributen meteen duidelijk. Stel bijvoorbeeld dat een aantal studenten verdeeld wordt in groepen 1, 2 en 3. Als we deze groepen zonder meer opnemen in een csv-bestand, zal Weka er van uitgaan dat de groep een numeriek attribuut is. Het is echter nominaal bedoeld: de nummers 1, 2 en 3 hebben niets te maken met hun eigenlijke waarden, maar zijn enkel bedoeld om de groepen aan te duiden. Hierbij ligt 1 niet verder van 3 dan van 2. We hadden de groepen ook A, B en C kunnen noemen, en dan zou Weka er wel een nominaal attribuut van gemaakt hebben. We verkiezen dus arff. Om de Televicdata in dat formaat om te zetten maakten we een ArffWriter in C#. Deze kunnen we eerst een aantal attributen meegeven, en daarna een lijst met instances. Al deze instances worden gecontroleerd op het bevatten van de nodige attributen (zelfs als de waarden onbepaald zijn, moet de instance een leeg attribuut hebben). Daarna wordt voor elke instance een Dictionary (Map) bijgehouden, die namen van attributen afbeeldt op de waarden voor die instance. Aan de hand daarvan kunnen we dan snel het arff-bestand opbouwen. 4.3 Correlatiecoëfficient Eens we de arff-bestanden van onze data hebben, kunnen we deze door Weka sturen. Afhankelijk van onze instellingen wordt dan één of ander algoritme getraind op een deel van de dat, en onmiddellijk getest op de rest. In de inleiding hebben we 4 foutmaten aangehaald, die allemaal door Weka berekend worden. Een vijfde, zo mogelijk nog belangrijkere maat, is de correlatiecoëfficiënt 3 r. Deze coëfficiënt bepaalt hoe sterk twee waarden gerelateerd zijn. Zo zullen bijvoorbeeld, bij medische dossiers, lengte en gewicht een hoge coëfficiënt hebben, maar lengte en haarkleur niet. De correlatiecoëfficiënt kunnen we ook als volgt opvatten: als we de twee waarden uitzetten op een grafiek voor alle datapunten, in welke mate liggen ze dan op een lijn? Hierbij betekent een r van 1 dat de waarden perfect op één rechte liggen, net als -1, maar bij -1 is de rechte dalend. Bij een r van 0 liggen de punten willekeurig door elkaar. We hebben als voorbeeld een aantal punten met verschillende coëfficiënten uitgezet op vier grafieken (figuur 4.1). 3 Meer bepaald Pearsons Productmomentcorrelatiecoëfficiënt 39

40 Hoofdstuk 4. Weka 40 Figuur 4.1: Datapunten met 2 features met verschillende correlatiecoëfficiënten. Een andere interessante eigenschap van de correlatiecoëfficiënt is dat het kwadraat ervan de determinatiecoëfficiënt is. Die determinatiecoëfficiënt, R 2, bepaalt hoe sterk twee variabelen elkaar verklaren. Met andere woorden: bij twee parameters met een R 2 van 0.44, is 44% van de variatie in de eerste variabele een gevolg van variatie in de tweede. Bijvoorbeeld: 44% van de prijs van een huis hangt af van de oppervlakte. Dat is misschien een wat abstract concept, maar het stelt ons in staat een cijfer te plakken op de accuraatheid van een regressie-classifier. Als we namelijk R 2 berekenen, met als variabelen de echte oplossing en de voorspelling, hebben we een getal dat aangeeft hoe dicht onze voorspelling aanleunt bij de oplossing. Om algoritmes te vergelijken zullen we dan ook vaak gebruik maken van r. 4.4 Classifiers De accuraatheid van een algoritme hangt in principe van twee zaken af. Enerzijds moet het algoritme een model maken dat de data goed kan benaderen. Een algoritme dat een lineair model opstelt zal bijvoorbeeld zeer slecht een kwadratische functie kunnen voorspellen. Dit punt komt overeen met de eerder besproken bias. Anderzijds moet de 40

41 Hoofdstuk 4. Weka 41 selectie van datapunten en features zodanig zijn dat het algoritme aan de hand daarvan een goed beeld krijgt van de situatie. Sommige algoritmes worden erg verward door onnuttige, redundante of zelfs misleidende features, waardoor de variance omhoog gaat. De interessante features hangen echter voor een deel af van het algoritme, en dus ook omgekeerd: het beste algoritme is afhankelijk van welke features we gebruiken. De keuze van features hebben we besproken in Feature Selection (2.3.2), de selectie van het best passende algoritme doen we met Weka. Van de regressiealgoritmes in Weka pikten we er 13 uit - deze die zonder problemen werkten op onze data. Deze 13 gebruikten we voor onze preselecties. We verdeelden de data in datapunten van één dag, en lieten onze 13 algoritmes los op deze traindata. De resultaten daarvan staan in tabel 4.1. Ook hadden we eerder al een gelijkaardig experiment gedaan, met wat minder features, en daar als maat voor de RMS gekozen. We halen die resultaten er even opnieuw bij, in de laatste kolom. Als we kijken naar het experiment Naam Correlatiecoëfficiënt RMS, eenvoudige dataset Least Med Sq Linear Regression Multilayer Perceptron RBFNetwork Kstar LWL ConjunctiveRule DecisionTable M5Rules ZeroR DecisionStump M5P Reptree Tabel 4.1: Correlatiecoëfficiënten bij classificatie op alle features met de kleinere dataset, zien we duidelijk dat REPTree, Least Median Square en M5P het goed doen. Tijdens de r-test hebben we als uitschieters vooral Lineaire Regressie en Least Median Square. Deze 4 algoritmes zullen we dan ook van naderbij bekijken. M5Rules doen het ook goed, maar deze zijn een variant op M5P en zullen we dus daarbij rekenen. 41

42 Hoofdstuk 4. Weka Meta-classifiers Meta-classifiers zijn classifiers die bestaande classifiers verbeteren of combineren. Ze vormen een wrapper rond één of meerdere andere classifiers en passen de in- en output van die classifiers aan. De volgende algoritmes verbeterden de resultaten duidelijk: Bagging is een vaakgebruikte afkorting voor Bootstrap Aggregating. Eerst worden uit de dataset een aantal nieuwe datasets opgebouwd, door willekeurige keuze met herhaling. Het is dus goed mogelijk dat een instantie twee keer voorkomt in één subset. Meestal kiest men uit n instanties er ook n uit. Behalve in het zeer zeldzame geval dat elk punt één maal gekozen wordt, hebben we dus zeker dubbels. Daarna worden een aantal classifiers opgebouwd uit de bekomen subsets. Voor de uiteindelijke classificatie nemen we de meest voorspelde klasse (bij classificatie) of het gemiddelde van de voorspellingen (bij regressie). De theorie is dat de veelvoorkomende gevallen meer kans hebben om twee maal in een dataset te zitten, en dat outliers minder gewicht krijgen, waardoor de voorspelling minder variantie vertoont. Stacking staat letterlijk voor het stapelen van algoritmes. Een aantal classifiers wordt gekozen als basislaag, daarboven komt een andere classifier, die als input de outputs van de basislaag heeft. Deze laatste classifier maakt dan een uiteindelijke voorspelling gebaseerd op de voorspellingen van de basislaag-classifiers. Het idee achter stacking is dat er verschillende experts zijn, die elk beter zijn in bepaalde gevallen, en dat één arbiter dan een combinatie maakt van de meningen van die specialisten. Hopelijk in het voordeel van de specialist die dan ook effectief in dat geval beter is. Attribute Selection maakt vooraf een keuze van welke attributen of features zullen gebruikt worden door het algoritme. Dit selectieproces gebeurt op basis van een door de gebruiker vastgelegde heuristiek. De bedoeling is een kleine subset van belangerijke features te bekomen. Niet alleen versnelt dit de berekening door de classifier, die met minder features rekening moet houden, ook het resulterende model is eenvoudiger, sneller, en meer begrijpbaar. Verder lossen we ook voor een deel het probleem (The Curse) van multidimensionaliteit op, waardoor onze modellen vaak ook gewoon beter zijn als we met minder attributen rekening houden. De moeilijkheid ligt natuurlijk, zoals zo vaak, in het vinden van een goede heuristiek. In onze eigen implementatie zullen we Simulated Annealing gebruiken, met als score de Correlatiecoëfficiënt. Elk van deze meta-algoritmes gaf sterk verbeterde resultaten op de geselecteerde regressiealgoritmes. In tabel 4.2 staan de correlatiecoëfficiënten voor deze algoritmes, toegepast op 42

43 Hoofdstuk 4. Weka 43 de algoritmes gekozen in paragraaf 4.4. Aangezien de dataset op dit punt geavanceerder was, zijn deze waarden niet vergelijkbaar met die in tabel 4.1. We zien dat de meta- Naam Reptree LinReg LMS M5P Geen meta Attribute Selection Bagging Stacking Tabel 4.2: Correlatiecoëfficiënten bij verschillende meta-algoritmes. algoritmes over het algemeen een duidelijk verschil maken, maar merken wel op dat de efficiëntie van deze algoritmes afhangt van het gebruikte basisalgoritme. Er zijn twee nadelen aan de meeste meta classifiers. Ten eerste verhogen ze de rekentijd door verschillende algoritmes uit te voeren of eenzelfde algoritme verschillende malen. Het proces vertraagt ook door de overhead van het linken van die classifiers; dit verschil is echter verwaarloosbaar in vergelijking met de tijdskost van de basis-ml-algoritmes. Ten tweede maken ze het model moeilijker te begrijpen 4. In combinatie met bijvoorbeeld neurale netwerken is dit niet echt een probleem - deze waren al onbegrijpelijk op zichzelf. Als we echter meta-classifiers toepassen op een boomstructuur, verliezen we één van de voordelen van die boom: dat een mens onmiddellijk ziet hoe het model aan zijn voorspellingen komt. Cross Validation Cross Validation is niet echt een metaclassifier, maar heeft er wel een aantal kenmerken van. De theorie is redelijk eenvoudig: we verdelen onze set trainingsdata in 10 folds 5. Dan gaan we telkens één van die 10 folds opzij zetten, een classifier trainen met de andere 9 folds, en er de tiende weer bijhalen om te testen hoe goed het resultaat was. Als we dat herhalen voor elke fold hebben we een goed zicht op wat we kunnen verwachten voor de echte training. Nog een voordeel is dat we 10 verschillende, getrainde versies van onze classifier hebben. We kunnen dus eenvoudigweg kijken welke de beste resultaten geeft, en deze kiezen als 4 Enkele meta-classifiers, onder andere windowing en attribute selection, hebben als resultaat slechts één basismodel en hebben dit tweede nadeel dus niet. Beide gaan zelfs de uitkomst vereenvoudigen. 5 Dat hoeft natuurlijk niet 10 te zijn, maar 10 is een mooi rond getal en heeft door de jaren heen bewezen een goede balans te hebben tussen uitvoeringstijd en resultaat. 43

44 Hoofdstuk 4. Weka 44 uiteindelijke classifier. In dat geval laten we Cross Validation echt werken als metaclassifier, vergelijkbaar met bagging. 4.5 Retraining De omstandigheden waarin we deze experimenten uitgevoerd hebben zijn nog niet zoals ze in realiteit zullen voorkomen. Een belangrijk verschil is dat we, eens de service online gaat, telkens slechts voor één of twee dagen een voorspelling moeten maken, waarbij we beschikken over alle data tot en met de huidige datum. Om de geschiktheid van de classifiers in deze situatie te beoordelen, stellen we een tweede trainingsscenario op. Elke classifier wordt getraind met alle data van de voorbije vier jaar en maakt een voorspelling voor de dag erna. Dan wordt de trainingsdata één dag verschoven. We verwijderen dus de vroegste dag uit de trainingsset en voegen de juist geclassificeerde dag toe (met de correcte regressievariabele, niet de voorspelde). Dan wordt de classifier opnieuw getraind, waarna de volgende dag voorspeld moet worden. Dit herhalen we voor elke dag in onze testdata. Dit proces van steeds hertrainen neemt uiteraard veel tijd in beslag, waardoor we dit niet kunnen doen voor elk algoritme. We beperken ons tot de algoritmes die in de vorige experimenten het interessantst waren. Om het experiment een beetje te automatiseren, maken we gebruik van de command line-mogelijkheden van Weka. We gebruiken hiervoor niet de command line die in het programma zelf zit, maar wel het vertrouwde windows batch-scripting. De command line parameters zijn nogal ingewikkeld, vooral het gebruik van metaclassifiers kan voor problemen zorgen wanneer een parameter zowel voor de binnenste classifiers als voor de metaclassifier kan gebruikt worden. Gelukkig biedt Weka de mogelijkheid om in het programma een classifier op te stellen en deze dan te kopiëren in command line-vorm. We genereren eerst onze train- en testdata, waarbij in elk testdatabestand slechts 1 record zit. Daarna voeren we het algoritme uit voor elk paar databestanden. We gebruiken bijvoorbeeld volgend off for /l %%x in (1, 1, 365) do ( echo %%x java -cp "C:\Program Files (x86)\weka-3-6\weka.jar" weka.classifiers.meta.bagging -t traindata_regr_retrain_%%x.arff -T 44

45 Hoofdstuk 4. Weka 45 ) testdata_regr_retrain_%%x.arff -p 0 -P 100 -S 1 -I 10 -W weka.classifiers.meta.cvparameterselection -- -X 10 -S 1 -W weka.classifiers.meta.attributeselectedclassifier -- -E "weka.attributeselection.cfssubseteval " -S "weka.attributeselection.bestfirst -D 1 -N 5" -W weka.classifiers.trees.reptree -- -M 2 -V N 3 -S 1 -L -1 > result_regr_retrain_%%x.txt Deze haalt de data door een Bagging van Reptrees, elk met parameter- en attributeselection. Elk resultaat wordt uitgeprint in een apart bestandje. Om ze aan elkaar te plakken en in cvs te gieten is er natuurlijk Perl: open(out, ">result_regr_retrain_all.csv"); print OUT "actual;predicted;error;\n"; for my $x (1..365) open(result, "result_regr_retrain_$x.txt"); while(<result>) chomp; if(m/^\s*[0-9]+\s*([-0-9.]+)\s*([-0-9.]+)\s*([-0-9.]+)\s*$/) print OUT "$1;$2;$3;\n" print "$x\n"; We hebben 2 extra zaken getest tijdens deze retraining: - Hoeveel jaar voorzien we als training data? - Is het nuttig om het aantal gebruikers per channel te proberen voorspellen in plaats van het totaal aantal gebruikers? Om deze zaken te testen hebben we de retraintest in totaal 8 keer uitgevoerd voor onze Reptree Bagging. Op figuur 4.2 zijn de resultaten afgebeeld, en die zijn zeer interessant. We berekenden drie maten: gemiddelde fout (Mean, kleiner is beter), gemiddelde kwadraat van de fout (RMS, kleiner is beter) en de correlatiecoëfficiënt (CoCo, groter is beter). We zien ten eerste dat de blauwe balken van de voorspellingen van aantal gebruikers het beter 45

46 Hoofdstuk 4. Weka 46 doen dan de rode balken van gebruikers per channel. Dit is misschien een beetje vreemd: op het eerste zicht zou men verwachten dat het aantal gebruikers sterk stijgt als er een channel bijkomt, maar dat het aantal gebruikers per channel ongeveer gelijk blijft. We vermoeden dat de nieuwe gebruikers die geleidelijk aan bijkomen na het toevoegen van een nieuw channel gebalanceerd worden door het verminderen van gebruik op oudere channels. Figuur 4.2: Resultaten na retrainingstesten, Users vs Users Per Channel en 1 tot 4 jaar traindata. Verder zien we dat, voor het aantal gebruikers per channel, één enkel jaar het best werkt. Als we jaren toevoegen aan de traindata, gaat de accuraatheid erop achteruit. Dit bevestigt onze eerdere theorie: als het aantal gebruikers ongeveer gelijk blijft, of in ieder geval niet evenredig stijgt met het aantal channels, verandert het aantal gebruikers per channel door de jaren heen wel relatief sterk. In tegenstelling tot het aantal gebruikers per channel blijft het totaal aantal gebruikers door de jaren heen wel redelijk gelijk, en is de data van vier jaar terug nog steed goed bruikbaar. In dit geval zien we gewoon dat meer datapunten leiden tot een beter model, wat uiteraard logisch is. Wat betreft de uiteindelijke resultaten van de hertraining zien we weinig verschil met de resultaten zonder hertraining. Hoewel we dus in de uiteindelijke service graag elke dag zouden hertrainen, zijn de resultaten van onze vorige experimenten nog steeds toepasbaar op de reële situatie. 46

47 Hoofdstuk 4. Weka besluit Samengevat hebben we vier basisalgoritmes en drie meta-algoritmes die interessant lijken, en die we dus zullen implementeren in C#. We zien ook dat we, met nog niet te veel optimalisatie, aan een correlatiecoëfficiënt van 0.6 raken. 47

48 Hoofdstuk 5 Implementatie 5.1 Service en Mailing Het hoofddoel van deze thesis is natuurlijk het machine-learning algoritme ontwikkelen, maar er zijn ook een aantal andere belangrijke zaken. Zo moet de eigenlijke service ook werken zoals het hoort en moet op gepaste momenten een mail gestuurd worden. Verder willen we ook beschikken over logging. De implementatie van deze omkadering is niet bijzonder interessant, we beperken ons dus tot een vlug overzicht. Het aanmaken van een Windows Service is relatief eenvoudig in Visual Studio - terwijl het mogelijk is om from scratch te beginnen kunnen we ook gewoon een Service-project aanmaken, waarna Visual Studio het aanmaken van de service op zich neemt. Deze lege service bevat dan twee invulbare methoden, OnStart() en OnStop(), die we kunnen invullen. Voorwaarde is wel dat beide methoden vlug een antwoord geven en we dus alle zware of langdurige methodes asynchroon moeten uitvoeren. Onze implementatie leest informatie in uit een xml-bestand, en maakt aan de hand van die instellingen één of meerdere predictor-objecten aan. Deze predictor-objecten zijn verantwoordelijk voor het eigenlijke uitvoeren van de service. Elke predictor bevat één enkele classifier, een timer voor training en een timer voor prediction. Verder hebben de predictors een aantal properties, waarin instellingen zoals adressen, precieze timing, gebruikte features en dergelijke worden opgeslaan. Telkens één van de timers afloopt worden dan de relevante instellingen ingelezen en wordt de training of voorspelling uitgevoerd. Beide timers worden automatisch gereset. 48

49 Hoofdstuk 5. Implementatie 49 De Logger-klasse doet niet veel meer dan het opslaan van een gegeven string in een tekstdocument. Dit document bevindt zich in de map./puplogs/ en heeft als naam de datum waarop gelogd wordt met een.log-extensie. Verder kunnen de predictors aan de Logger laten weten waar ze mee bezig zijn, door de methodes int SetState(string state) en void EndState(int state). De eerste geeft een volgnummer voor de state terug, dat gebruikt kan worden in de tweede methode. Intern houdt de logger een dictionary van states bij. States worden niet gelogd, totdat de methode void Logger.LogStates() opgeroepen wordt. Dit gebeurt momenteel enkel bij fouten, zodat men in de logs kan zien waar elke predictor mee bezig was op het moment van de fout. Bijlage A bevat de handleiding voor de service, die ook de structuur van het xmlconfiguratiebestand beschrijft. 5.2 Classificatie Library We beginnen met een aantal abstracte klassen en hulpklassen, die de indeling van onze applicatie duidelijk zullen maken. Alle classificatie-algoritmes en bijbehorende klassen worden geïmplementeerd in een library (DLL), die dan kan aangesproken worden door onze service maar natuurlijk ook bruikbaar is door andere programmas. Misschien de belangerijkste file in deze library is de Classifier-interface: public abstract class Classifier public abstract void train(datalist TrainData); public abstract Instance classify(instance instance); public abstract Feature.FeatureType outputtype(); public abstract Classifier copy(); public List<Instance> classify(ienumerable<instance> instances) Datalist classified = new Datalist(instances.Count()); foreach (Instance i in instances) classified.add(classify(i)); 49

50 Hoofdstuk 5. Implementatie 50 return classified; Ze definieert natuurlijk methodes voor het trainen en classificeren, maar ook een output- Type, waaraan we bijvoorbeeld kunnen zien of een classifier geschikt is voor regressie of classificatie, en een copy-methode, die we gebruiken voor bepaalde meta-algoritmes. De naam Classifier is eigenlijk niet 100% juist, aangezien we ook regressie-algoritmes onder deze klasse groeperen, maar het is gewoon gemakkelijker om elke keer het woord Classifier te gebruiken in plaats van Classifier en/of Regressor. SingleRandom is een eenvoudige abstracte klasse die met behulp van een aantal statische methodes een enkele Random bijhoudt, om te vermijden dat we twee verschillende random generatoren hebben die kort na elkaar (en dus met dezelfde seed) aangemaakt worden. Ook wordt de standaard Random functionaliteit aangevuld met een binomiaal-random methode, die een willekeurig getal teruggeeft volgens een normale verdeling, met gegeven gemiddelde en standaardafwijking. Tenslotte hebben we ervoor gezorgd dat onze randomklasse volledig thread safe is, de standaard Random is dat namelijk niet. public abstract class SingleRandom private static Random r = new Random(); public static double getdouble(double min, double max) lock (r) // Thread safety: only let r be used by one thread at a time. return (r.nextdouble() * (max - min)) + min; public static int getint(int min, int max) lock (r) return r.next(min, max); 50

51 Hoofdstuk 5. Implementatie 51 public static double getnormalrandom(double mean, double dev) // Box-Muller method double u = getdouble(0, 1); double v = getdouble(0, 1); double x = Math.Sqrt(-2 * (Math.Log(u))) * Math.Cos(2 * Math.PI * v); return mean + x * dev; Een andere klasse die we even van naderbij gaan bekijken is de RandSet. We willen onze features opslaan in een Set, zodat er zeker geen dubbels voorkomen. Voor de Extra Tree (zie 5.3.2) hebben we toegang nodig tot een willekeurig element van de Set, wat normaalgezien een O(n) operatie is. We willen het toevoegen zonder dubbels in O(1) van de set en de willekeurige toegang in O(1) van de List. We hebben dit geïmplementeerd in de RandSet; een Set met random acces mogelijkheden. Voor de random acces houden we gewoon een List bij, maar we maken ook een Dictionary aan die de elementen afbeeldt op hun plaats in de List, zodat we vlug elementen kunnen toevoegen en veranderen. Verwijderen is niet geïmplementeerd, maar het zou ook kunnen in O(1) 1. Nadeel is natuurlijk dat twee keer zo veel plaats nodig hebben, één maal voor indexen af te beelden op items (List) en eens voor items af te beelden op indexen (Dictionary). public class RandSet<T> : IEnumerable<T> List<T> elements; Dictionary<T, int> location; public int Count get; private set; public RandSet() elements = new List<T>(); location = new Dictionary<T, int>(); 1 Zoek index van te verwijderen item in dictionary, vervang het door laatste item in lijst, pas volgnummer aan in dictionary, wis laatste item uit lijst en gevonden item uit dictionary. 51

52 Hoofdstuk 5. Implementatie 52 public void Add(T value) if (!location.containskey(value)) location[value] = elements.count; elements.add(value); Count++; public HashSet<T> ToHash() return new HashSet<T>(location.Keys); public List<T> ToList() return new List<T>(elements); public IEnumerator<T> GetEnumerator() return elements.getenumerator(); System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() return this.getenumerator(); public T this[int i] get return elements[i]; set if (!location.containskey(value)) location.remove(elements[i]); 52

53 Hoofdstuk 5. Implementatie 53 elements[i] = value; location[value] = i; 5.3 Trees Trees zijn veelvoorkomde ML-structuren. We hebben bijvoorbeeld C4.5, de Reptree en M5P al vermeld, maar er zijn nog vele variaties mogelijk. Aangezien we zowel Reptrees als M5P willen testen, ligt het voor de hand eerst een abstracte klasse Tree te maken, die het algemene tree-werk voor zich neemt. Als top bevat dezetree een enkel TreeElement, dat ofwel een Leaf zal zijn of een Node die dan weer sub-elementen kan bevatten. Tot zover zijn alle bomen gelijk. Eens deze generale Tree-code klaar is, willen we deze natuurlijk testen met een eenvoudig soort tree. Zowel de Reptree als M5P bevatten een aantal minder evidente elementen, en het herimplementeren van C4.5 is niet echt nuttig 2, waardoor we op zoek gegaan zijn naar een eenvoudigere tree. Als die tree nog een beetje goed werkt, is dat natuurlijk nog beter en na wat zoeken kwamen we uit bij Extra Trees. Maar eerst: de basis Abstract Tree In onze implementatie is Tree een subklasse van Classifier, en is elke mogelijke tree een subklasse van Tree. Alle trees maken gebruik van dezelfde TreeElements: Node en Leaf. Er is ook voor elke Tree een enkel TreeElement top. Enkel het trainen verschilt per tree, en dus zal elke tree een recursieve functie hebben die bepaalt hoe er getraind wordt. Die methode geeft ofwel een Leaf terug, ofwel een Node met enkele deelbomen, die eerst recursief opgebouwd worden. De classificatiemethode is hetzelfde bij elke tree: men vraagt aan de top om de instantie te classificeren. Deze geeft het request door aan de goede deelboom, die het op zijn beurt verder doorgeeft naar onder, tot we bij een blad komen. 2 De C4.5 code zat goed in elkaar, maar was niet gericht op het generaliseren van trees, waardoor het eenvoudiger was om van de grond af een tree-framework te implementeren. Natuurlijk is er wel inspiratie gehaald bij het C4.5-algoritme 53

54 Hoofdstuk 5. Implementatie 54 public abstract class Tree : Classifier protected TreeElement top; public int getsize() return top.size(); public override Instance classify(instance instance) if (top == null) throw new InvalidOperationException("Classifier is untrained."); else return top.classify(instance); public override void train(datalist TrainData) top = trainrecursive(traindata); public string print() return top.print(0); protected abstract TreeElement trainrecursive(datalist TrainData); // Train defined depending on tree 54

55 Hoofdstuk 5. Implementatie Extra Trees Deze Extra Trees (Geurts, 2002), kort voor Extremely Random Trees, bepalen alles zo goed als willekeurig: zowel het attribuut waarop men splitst als de splitswaarde bij numerieke attributen. Op het eerste zicht is deze aanpak weliswaar makkelijk te implementeren, maar zeer inefficiënt. De bomen zullen uiteraard dieper zijn dan intelligentere soortgenoten, en minder accuraat, maar ze blijken wel verrassend snel. Waar andere boomsoorten gebruikelijk een zware O(n m) berekening moeten maken in elke node 3, is dat bij de Extra Tree gewoonweg O(n): het pikken van een willekeurig getal en het verdelen van de waarden. Dit zorgt ervoor dat we, zoals Geurts voorstelt, een aantal Extra Trees kunnen maken op dezelfde tijd als één andere boom. Deze kunnen we dan met een meta-algoritme combineren om tot een sterkere voorspelling te komen. We gebruiken bijvoorbeeld 25 bomen, en nemen het gemiddelde van hun voorspellingen. Om te voorkomen dat we al te triviale of nutteloze splitsingen gaan doorvoeren, wat met willekeurigheid wel eens kan voorkomen, gaan we toch de score van de te testen splitsing berekenen. Als die kleiner is dan een (zeer lage) drempelwaarde, proberen we opnieuw. Om te voorkomen dat we bij extreme gevallen (geen goede opties meer) in een oneindige lus terechtkomen, zetten we een maximum op het aantal keren proberen. Dit zorgt ervoor dat het algoritme O(n) blijft per node. Een tweede opmerking die we moeten maken is dat we niet zomaar de O(n) en O(n m) of O(n 2 m) mogen vergelijken. Dit is namelijk de complexiteit per node, en een gewone boom zal minder nodes hebben dan een Extratree, waardoor die tweede zeker niet m maal sneller zal gaan. In totaal zien we dat een enkele Extratrees wel nog steeds sneller traint dan bijvoorbeeld een Reptree Reptrees De rep in Reptree staat voor Reduced Error Pruning, de manier waarop Reptrees gesnoeid worden. Voor de rest is de Reptree een redelijk normale boom, die gebruik maakt van veelvoorkomende en standaard technieken. De splitsing gebeurt grotendeels zoals bij de C4.5-boom (hoofdstuk 3), met dat verschil dat we ook rekening houden met regressie. In het geval van regressie gebruiken we de vermindering in variatie als splitsingsscore. Om te zorgen dat Reptrees nog wat sneller zijn, zorgen we ervoor dat we niet te veel moeten 3 Voor elk attribuut (m) de fout op elke instantie (n) berekenen. Bij numerieke attributen wordt er getest op O(n) splitswaarden getest, wat neerkomt op een O(n 2 m) als we uitgaan van O(n) verschillende waarden voor het attribuut. De verdeling die er op volgt is O(n), maar moet maar één maal per node gebeuren. 55

56 Hoofdstuk 5. Implementatie 56 sorteren. We houden dus een Dictionary bij die voor elke feature een lijst van alle instanties bijhoudt, gesorteerd volgens dat attribuut. Het is dan relatief duur om de trainingsset op te splitsen in subsets (van O(n) naar O(m n)) maar er moet niet meer gesorteerd worden voor er door de waarden gelopen wordt, wat zorgt voor een versnelling van O(mn log(n)) naar O(n) in dat deel van de code. Voor lijsten met relatief veel instanties (n) en weinig attributen (m) is dit een groot voordeel, wat nog een reden is om het aantal attributen klein te houden. Wel hebben we O(m n) meer ruimte nodig. De pruning, die de Reptree zijn naam geeft, is eerst beschreven door een oude bekende: Quinlan, de bezieler van C4.5 (Quinlan, 1986) (Elomaa & Kääriäinen, 2001). Kort uitgelegd doen we het volgende: we splitsen een deel van de traindata af als prunedata, en die gebruiken we niet om te trainen. Nadat we de boom getraind 4 hebben (met de rest van de data) halen we de prunedata door de boom. We doen dat bottom-up, en op de volgende manier: - Als we een blad tegenkomen, doen we niets. - Als we een node tegenkomen, hebben we een deelset van de prunedata die, bij classificatie, in die node zou terechtkomen. Die prunedata gaan we laten classificeren, en we berekenen de fout erop. Het kan zijn dat die deelboom al eerder geprunede takken bevat, we gebruiken dan altijd de laatste versie van de deelboom. Omdat we bottom-up werken zijn alle mogelijke prunings onder de node al gebeurd. - Voor dezelfde data berekenen we wat de fout zou zijn als de node vervangen wordt door een blad, met de meest voorkomende klasse (Classificatie) of de verwachte waarde (Regressie) van de deelset. - Als de fout op het blad kleiner is, gooien we de hele boom weg en vervangen we hem door het blad. - Herhaal tot we in de top zijn (die eventueel ook gepruned kan worden) M5P M5P is een kruising tussen Trees en Linear Regression, en opnieuw hebben we dit algoritme te danken aan Quinlan (Wang & Witten, 1996). De structuur van de boom blijft nog 4 In de literatuur wordt vaak growing gebruikt, letterlijk kweken, in plaats van training. Om consequent te blijven met andere (niet-boom) algoritmes gebruiken wij training. 56

57 Hoofdstuk 5. Implementatie 57 steeds dezelfde: we hebben nodes, die weer nodes als kinderen hebben, tot we bij de leaves uitkomen, en elke node bevat een attribuut waarop gesplitst wordt en, in het geval van numerieke attributen, een waarde om op te splitsen. Het verschil is dat een M5P tree geen constante waarden in de bladeren heeft, maar lineaire vergelijkingen. Om een M5P-boom te maken beginnen we zoals bij onze andere bomen: we zoeken het beste attribuut, en splitsen, gaan recursief verder. In het geval van M5P gaan we niet splitsen op variantie (zoals bij Reptrees), maar wel op standaardafwijking. We zoeken dus telkens de splitsing die de nieuwe standaardafwijkingen zo klein mogelijk maakt. Vervolgens wordt voor elk blad het gemiddelde van de trainwaarden genomen als voorspelling: we beginnen dus wel met een constante waarde in de bladeren, maar dat zal later veranderen. Elke knoop krijgt ook een voorspelling: een lineaire vergelijking, waarbij enkel rekening wordt gehouden met splitsattributen voor die knoop en zijn nakomelingen. Een knoop die enkel bladeren als kinderen heeft, zal een lineaire vergelijking in één onbekende krijgen: zijn eigen splitsattribuut. De ouder van die knoop heeft een lineaire vergelijking gebaseerd op minstens 2 onbekenden: zijn eigen attribuut en dat van zijn kinderen (en kleinkinderen, achterkleinkinderen,...). Deze methode gaat in feite ook op voor de bladeren: zij krijgen een lineaire vergelijking met 0 onbekenden, dus een constante. De nodige lineare vergelijkingen berekenen we met behulp van Lineaire Regressie (5.4). De volgende stap is het prunen van de boom. We beginnen met het prunen van de Lineaire Regressies zelf. We doen dit met behulp van dezelfde trainingsdata, maar dan onderschatten we natuurlijk de fout. We gaan dus elke fout vermenigvuldigen met een factor k. Deze k is kleiner als we meer datapunten hebben en wordt groter als we meer attributen gebruiken in de vergelijking. Als formule gebruiken we: k = (n + a)/(n a) met a het aantal attributen we gebruiken en n het aantal trainingscases in onze node. Door het gebruik van deze formule wordt het mogelijk onze lineaire vergelijkingen te prunen door er attributen uit te laten vallen. De fout op de traindata wordt dan weliswaar groter, maar de factor a wordt kleiner, dus kan het zijn dat onze voorspelde fout ook verkleint. Na het prunen van de lineaire vergelijkingen is het prunen van de boom aan de beurt. Dit gebeurt op dezelfde manier als bij de andere bomen: we bekijken of de lineaire vergelijking van de huidige node betere resultaten geeft dan de volledige subtree. Als dat zo is, wordt de node een blad, maar houdt hij dezelfde lineaire vergelijking. Ook bij deze prune-stap 57

58 Hoofdstuk 5. Implementatie 58 wordt de traindata gebruikt, en de fout wordt vermenigvuldigt met dezelfde factor k die we gebruikten bij het prunen van de lineaire regressie. 5.4 Linear en Ridge Regression We hebben tot nu toe enkel Trees bekeken, en met reden: de familie van ML-Trees is groot, populair, en flexibel. Maar er zijn natuurlijk andere manieren om het probleem aan te pakken. Eén daarvan is Linear Regression. Waar het op neerkomt is het volgende: we zoeken een lineaire combinatie van onze attributen die de best mogelijke benadering vormen voor onze regressiewaarde. In de praktijk gebruiken we de RMS als foutmaat, en we zoeken dus de functie die het gemiddelde van de kwadraten van de fouten zo klein mogelijk houdt. Dit is een veelvoorkomend probleem in de statistiek, en er is een grote hoeveelheid aan oplossings- en benaderingsmethodes. De manier die wij gebruiken maakt gebruik van matrixvergelijkingen. De oplossingenmatrix ˆX, die de coëfficiënten van alle features bevat, wordt gehaald uit de volgende vergelijking: ˆX = (A T A) 1 A T b Hierbij is A de matrix van alle waarden van features voor alle datapunten, en b is de kolommatrix die van elk datapunt de regressiewaarde bevat. De berekening ervan laten we nog even achterwege, want we moeten eerst nog een ander probleem oplossen. Lineaire Regressie is een algoritme met een zeer grote variantiefactor: als ook maar één van de datapunten sterk afwijkt zal deze de hele regressievergelijking scheeftrekken. Verder is Linear Regression zeer onderhevig aan hoge dimensionaliteit: veel features, vooral features die deels of volledig samenhangen (bv, users gisteren en oefeningen gisteren), zullen de coëfficiënten van die features scheeftrekken. Het algoritme begint zich dan aan te passen aan de ruis op die attributen, en we krijgen bijvoorbeeld zeer grote coëfficienten (orde ). Zo hadden we tijdens één van de tests een reeks coëfficiënten van rond de 500, sommige positief en sommige negatief, die elkaar tijdens de testcases mooi uitbalanceerden. Dat geldt niet meer tijdens daaropvolgende tests: de grote coëfficiënten maken de functie zeer wispelturig, en een kleine fout op een feature kan al gauw zorgen dat de voorspelde waarde compleet de mist in gaat. We zullen dus een variatie gebruiken, die speciaal bedoeld is om de variance te verlagen: Ridge Regression. Deze verhoogt wel de bias van het algoritme. Ridge Regression kent een straf toe aan grote coëfficiënten, die gewoon bij de fout geteld wordt. Deze straf is afhankelijk van een matrix Γ. De berekening wordt dan: ˆX = ( A T A + Γ T Γ ) 1 A T b 58

59 Hoofdstuk 5. Implementatie Matrices Voor Ridge Regression hebben we matrices nodig. De formule die we hanteren voor de oplossing is ˆx = ( A T A + Γ T Γ ) 1 A T b. Hierbij is A de matrix van instanties (rijen) en hun waarden voor alle features (kolommen), Γ is de zogenaamde Tikhonov-Matrix. Wij gebruiken hier een eenheidsmatrix vermenigvuldigd met een ridgeparameter λ, die ervoor zorgt dat kleinere coëfficiënten verkozen worden in de uiteindelijke oplossing: Γ = λi. Door het gebruik van de eenheidsmatrix kunnen we alvast een eerste vereenvoudiging doorvoeren: (λi) T (λi) = (λi) 2 = λ 2 I. Het is niet moeilijk om voor matrices de vermenigvuldiging (Matrix en scalair), optelling, en transpositie te implementeren, maar de inverse van een matrix vraagt wat meer werk. We hebben de determinant en de adjuncte matrix nodig, waarvoor we als tussenstappen de cofactormatrix en de minormatrix nodig hebben. Voor die laatste, en voor de determinant, moeten we ook deeldeterminanten berekenen. We bekijken het algoritme stap voor stap. private double subdeterminant(list<int> rs, List<int> cs) // Assumes lists are sorted if (rs.count!= cs.count) throw new InvalidOperationException("Only possible on square matrix."); if (rs.count == 2) return (this[rs[0]][cs[0]] * this[rs[1]][cs[1]]) - (this[rs[0]][cs[1]] * this[rs[1]][cs[0]]); else int row = rs[0]; List<int> temprs = new List<int>(rs); temprs.removeat(0); double det = 0.0; int sgn = 1; 59

60 Hoofdstuk 5. Implementatie 60 for(int i = 0; i < cs.count; i++) List<int> tempcs = new List<int>(cs); tempcs.removeat(i); det += subdeterminant(temprs, tempcs) * this[row][cs[i]] * sgn; sgn *= -1; return det; De methode subdeterminant berekent de determinant van een deel van een matrix, namelijk voor de rijen en kolommen meegegeven in rs en cs. De determinant wordt berekend op basis van de eerste rij, door elk element van die rij te vermenigvuldigen met de determinant van de kleinere matrix die overblijft als we de rij en kolom van dat element weglaten. Deze termen worden dan afwisselend opgeteld en afgetrokken van de determinant. We gebruiken een lijst van indices van rijen en kolommen in plaats van een volledige deelmatrix, omdat we het kopiëren van matrices zo kunnen vermijden, zonder dat we snelheidsverlies hebben. Deze methode is O(1) voor een 2x2-matrix, voor 3x3 hebben we dan O 3 = 3 O 2 en in het algemeen voor een n x n-matrix hebben we O n = n O n 1. Dit algoritme is dus O(n!). Met dynamisch programmeren kunnen we dit wat verbeteren: we moeten dan de determinant berekenen, voor alle m, van alle deelmatrixen van m willekeurige kolommen en de onderste m rijen 5. Een determinant van een deelmatrix van k kolommen is dan O(k), want k=2 ( n k we( kennen alle kleinere determinanten al, en we hebben dus een totale uitvoeringstijd van n ) ) O k, wat overeenkomt met een O(n 2 n ) algoritme om de determinant te berekenen. We kunnen dit op twee manieren zien: er zijn 2 n mogelijkheden om een willekeurig aantal kolommen te kiezen waarvan we de deeldeterminant moeten berekenen, en voor elk van die deelmatrices hebben we (gemiddeld) n vermenigvuldigingen, wat neerkomt op 2 O(2 n n) = n ) 2 O(2n 1 n); en er bestaat ook een rechtstreekse formule: k = 2 n 1 n. We moeten nog de matrix van cofactoren berekenen. Dit gebeurt door elk element te vermenigvuldigen met de determinant van de n-1 x n-1 matrix die overblijft als we de rij en kolom van dat element weghalen. Voor de eerste rij is het berekenen van die factoren al 5 We halen namelijk telkens de bovenste rij weg in onze recursie, maar moeten wel de recursie berekenen voor het weghalen van elke kolom. 60 k=2 ( n k

61 Hoofdstuk 5. Implementatie 61 gebeurd in de vorige stap. Voor de tweede rij moeten we de eerste determinanten opnieuw berekenen, maar deze zijn gebaseerd op al berekende determinanten. Voor de derde rij berekenen we twee extra determinanten, en zo verder tot we voor de laatste rij weer de volle n determinanten moeten berekenen. Voor rij 1 is dat O(n), omdat we al alle nodige deeldeterminanten berekend hebben. Voor rij 2 hebben we ze bijna allemaal, en hebben we één extra tussenstap nodig: O(n(n 1)). Dit gaat zo door tot de laatste rij, rij n, die (net als de oorspronkelijke determinant) berekend wordt in O(n!). We komen uit op k=0 n 1 n! k! wat gelijk is aan e n!, opnieuw een O(n!) algoritme dus 6. Uit die cofactormatrix halen we uiteindelijk de adjuncte matrix in O(n 2 ). Maar opnieuw, mat dynamisch programmeren kunnen we dat al heel wat verkorten, op dezelfde( manier, ( wat ons)) een O(2 n ) bezorgt voor de n n ) moeilijkste (onderste) rij, en een totaal van O k. We hebben namelijk de volledige berekening nodig voor de onderste rij (j = 2), maar voor hogere rijen kunnen we gebruik maken van reeds berekende deeldeterminanten - eerst deze van graad 2 (en dus j = 3), dan die van graad 3, en zo voort. In deze som komt ( k = 2 één maal voor, k = 3 n ) ) twee keer, en k = n, n-1 keer. De som is dus gelijk aan O k (k 1). Deze is O(n 2 2 n ), wat naast mooi symmetrisch ook zeer inefficiënt is. j=2 k=j ( n k k=2 ( n k Terug naar de tekentafel. We weten dat ˆX = ( A T A + Γ T Γ ) 1 A T b We beginnen met het berekenen van B = ( A T A + Γ T Γ ), en C = (A T b) - dat gaat snel 7, O(n 2 ) voor op te tellen en te transponeren en O(n 3 ) voor te vermenigvuldigen. We hebben dan: ˆX = B 1 C B ˆX = C Deze laatste vergelijking kunnen we oplossen met Gauss-Jordan. We bekijken de code: public static Matrix SolveAXeqB(Matrix A, Matrix B) if (B.cols!= 1) 6 Een mooi bewijs voor die laatste gelijkheid: De Maclaurinreeks voor e. f(x) = f(0)+ f (0)x 1! + f (0)x 2 2! + f (0)x 3 3! +... Als we nu nemen dat f(x) = e x en x = 1 krijgen we: e 1 = e 0 + e0 1 1! + e ! + e0 x 3 3! +... of e = ! + 1 2! + 1 3! Vergeleken met de inverse-operatie 61

62 Hoofdstuk 5. Implementatie 62 throw new Exception("Can t solve for B with more than one column."); if (A.rows!= B.rows) throw new Exception("Can t solve for A and B with different number of rows."); for (int row = 0; row < A.rows; row++) // For each row, get a 1 at [row][row] if (A[row][row] == 0) // Try to get a number at [row][row] int j = row + 1; while (j < A.rows && A[j][row] == 0) j++; if (j < A.rows) B.RowSwap(row, j); A.RowSwap(row, j); if (A[row][row]!= 0) // Check if we do have a number B.RowMult(row, 1 / A[row][row]); A.RowMult(row, 1 / A[row][row]); // Make that number a 1 for(int j = row + 1; j < A.rows; j++) // Make rest of column 0 B.RowAdd(row, j, -A[j][row]); A.RowAdd(row, j, -A[j][row]); else throw new Exception("No singular solution."); 62

63 Hoofdstuk 5. Implementatie 63 // We now have the matrix in triangular form. Step 2: backpropagation to get solution. for (int row = A.rows - 1; row >= 0 ; row--) for (int j = row - 1; j >= 0; j--) // Make rest of column 0 B.RowAdd(row, j, -A[j][row]); A.RowAdd(row, j, -A[j][row]); return B; Deze code gaat er wat naïef van uit dat de matrix [A B] reduceerbaar is tot [I X], dus dat X een oplossing heeft, maar in de praktijk werkt dit voor ridge regression. De kans dat een matrix gevormd door berekeningen op real-life datapunten een aantal lineair afhankelijke rijen heeft is miniem, op voorwaarde natuurlijk dat er genoeg features met genoeg mogelijke waarden zijn. Dit algoritme heeft een totale tijdscomplexiteit van O(n 3 ), veel beter dan onze eerdere O(n 2 2 n ). We kunnen deze Gauss-Jordan eliminatie ook gebruiken om inverse matrices te berekenen, wat sneller gaat dan het eerder besproken algoritme, maar op dit punt is dat niet nuttig meer: met het huidige algoritme hebben we een even goede O(n 3 ) oplossing. Om precies te zijn: we hebben één matrixvermenigvuldiging die O(n 2 m) is, en eentje van O(nm 2 ) met n het aantal datapunten en m het aantal attributen, en alle andere berekeningen samen zijn een (in ons geval veel kleinere) O(m 3 ) Least Median Squared Voor Least Median Square bestaan een aantal exacte algoritmes. Deze zijn echter zeer beperkt in dimensies (Steele & Steiger, 1986), en dus in aantal bruikbare attributen. We gebruiken hier de Weka-implementatie, en berekenen een aantal Lineaire Regressies op basis van een klein aantal willekeurige datapunten, waarna we er die regressie uitkiezen die de kleinste Root Median Squared fout geeft. 63

64 Hoofdstuk 5. Implementatie Metaclassifiers Metaclassifiers zijn natuurlijk ook Classifiers, en erven dus over van de Classifier-klasse. Ze implementeren, net als gewone classifiers, de train- en classify-methode, die beide gebruik maken van de overeenkomstige functies van hun subclassifiers Averager en Voter De eenvoudigste meta-algoritmes die er zijn zijn de Averager (voor regressie) en de Voter (voor classificatie). Dat is ook de reden dat we ze implementeerden: om de experimenten met metaclassifiers op een eenvoudige, makkelijk controleerbare manier te beginnen. Veel valt er over deze algoritmes niet te zeggen. We geven ze een lijst van classifiers mee, de trainmethode van de metaclassifiers roept de trainmethode van alle gegeven classifiers op, en om te classificeren geven we respectievelijk het gemiddelde van alle classifiers of de vaakst voorspelde klasse terug. Om te kunnen vergelijken met latere classifiers geven we hier wel de trainmethode, die voor beide meta-algoritmes gelijk is: public override void Train(Datalist TrainData) foreach (Classifier c in subs) c.train(traindata); Bagging Bagging gebruikt dezelfde voorspelmethode als de Averager: het gemiddelde van een reeks subclassifiers is de uiteindelijke voorspelling. Het verschil tussen Bagging en Averaging is de trainingsmethode: public override void Train(Datalist TrainData) foreach (Classifier c in subs) // Create random subset, doubles allowed. Datalist subdata = new Datalist(TrainData, TrainData.Count); for (int i = 0; i < (Part * TrainData.Count); i++) 64

65 Hoofdstuk 5. Implementatie 65 subdata.add(traindata[singlerandom.getint(0, TrainData.Count)]); c.train(subdata); Bij bagging worden de aparte algoritmes niet getraind met de hele trainingsset, maar met een willekeurig deel ervan. Dit deel wordt bepaald door er een aantal willekeurige datapunten uit te kiezen, waarbij dubbels mogelijk zijn. Dat aantal is meestal gelijk aan het aantal datapunten in de hele set. De kans dat een willekeurig punt gekozen wordt is 1 ( ) n 1 n, n voor voldoende grote waarden van n convergeert dat naar 1 1 of 63.3%. e LinRegRidgeTester Bij Ridge Regression (5.4) gebruiken we een parameter λ die het gebruik van grote coëfficiënten afstraft. Hoe groter deze λ, hoe meer het algoritme in de richting van kleinere coëfficiënten geduwd zal worden, waardoor we dus de bias van het algoritme verhogen en de variance verlagen. De beste waarde voor λ is afhankelijk van de data - zoals besproken in paragraaf 1.3 profiteren sommige datasets van een lage bias waar andere vooral een lage variance nodig hebben. De LinRegRidgeTester is bedoeld om deze onzekerheid weg te werken en een geschikte λ te berekenen. De RidgeTester is niet meer dan een wrapper rond een enkele Ridge Regression, waarbij de trainmethode uitgebreid wordt om verschillende λ-waarden te testen. Uit onze experimenten blijkt dat de accuraatheid van het algoritme, in functie van λ, een vorm heeft zoals in figuur 5.1. Hoewel we geen bewijs hebben om aan te nemen dat de grafiek altijd een gelijkaardige vorm zal hebben, kunnen we dit toch intuïtief aannemen: een lage lambdaparameter zal vlug de veel te grote coëfficiënten uit een lineaire vergelijking filteren, wat tot een relatief snelle stijging van de accuraatheid leidt, tot op het punt dat de parameter zodanig groot (en streng) wordt, dat de coëfficiënten die we eigenlijk willen hebben ook als te groot worden gezien. Op dat punt wordt de winst door vermindering van variance kleiner dan het verlies door verhoging van de bias, en gaat onze grafiek weer dalen. Op λ = zullen alle coëfficiënten geforceerd worden op 0, waardoor r ook uiteindelijk naar 0 zal convergeren. Alle geteste subsets volgen een gelijkvormige grafiek, en we vinden ongeveer dezelfde vorm terug bij andere foutmaten (zoals de RMS, gebaseerd op berekening 65

66 Hoofdstuk 5. Implementatie 66 Figuur 5.1: Correlatiecoëfficiënten voor verschillende ridge parameters. van bias en variance, in (Tibshirani, 2013)). De vorm van die grafiek is zo belangrijk omdat we dan juist één lokaal maximum hebben (dat natuurlijk ook het globaal maximum is). De grafiek stijgt voor dat maximum en daalt erna, dus we kunnen met een eenvoudig zoekalgoritme vlug een goede λ vinden. We implementeren dit zoeken als volgt: 1. We beginnen met een arbitrair grote stapgrootte, bv. p = Beginnend van nul berekenen we de correlatiecoëfficiënt voor elke stap: r 0, r s, r 2 s,... totdat r n s < r (n 1) s 3. r n s < r (n 1) s en r (n 2) s < r (n 1) s dus ligt de gezochte top ergens in [r (n 2) s, r n s ]. 4. We delen s door een vooraf bepaalde deler (bv. 10) en beginnen opnieuw vanaf item 2. In plaats van op 0 te beginnen beginnen we op r (n 2) s. 5. Dit doen we totdat de stapgrootte s een arbitrair kleine waarde heeft, r (n 1) s is dan de gezochte waarde, nauwkeurig tot op s. In code: public override void Train(Datalist TrainData) 66

67 Hoofdstuk 5. Implementatie 67 double x = 0; double newr = 0; double prevr = -1; CocoCalc ccc = new CocoCalc(); double precision = startprecision; // Step size // stopprecision divided by precisiondivisor so we stop at AT LEAST the requested precision while (precision > (stopprecision / precisiondivisor)) precision = Math.Max(precision, stopprecision); // R has only one extremum, so we can search for it efficiently while (newr > prevr) prevr = newr; x += precision; linreg = new LinearRegression(x); Datalist Tester = TrainData.Split(0.9); linreg.train(traindata); ErrorValueList val1 = new FeatureEVL(Tester, Tester.mainAttribute); ErrorValueList val2 = new ClassificationEVL(Tester, linreg); newr = ccc.calcerror(val1, val2); x -= 2 * precision; precision /= precisiondivisor; x += precision; linreg = new LinearRegression(x); linreg.train(traindata); Stacking Stacking is letterlijk het stapelen van algoritmes, en werkt met een hoofdalgoritme dat de uitkomsten van verschillende subalgoritmes combineert. De traindata wordt verdeeld 67

68 Hoofdstuk 5. Implementatie 68 in een deel waar de sub-algoritmes mee trainen en een deel waar het hoofdalgoritme mee traint. De gebruikte code: public override void Train(Data.Datalist TrainData) // Split data main = TrainData.mainAttribute; int splitter = (int)(traindata.count * SubPart); Datalist sublist = TrainData.Sublist(0, splitter); Datalist toplist = TrainData.Sublist(splitter, TrainData.Count - splitter); // Train subs foreach (Classifier sub in subs) sub.train(sublist); // Create traindatalist for top Datalist TopData = new Datalist(toplist.Count); subresults = new List<Feature>(); int num = 0; foreach (Classifier sub in subs) Feature f = new NumberDoubleFeature("classifier_" + num + "_output", null); subresults.add(f); TopData.features.Add(f); num++; TopData.mainAttribute = main; // Fill traindatalist with sub results foreach(instance inst in toplist) Instance topinst = new Instance(); for(int i = 0; i < subs.count; i++) double subresult = subs[i].classify(inst)[main].getnumericvalue(); 68

69 Hoofdstuk 5. Implementatie 69 topinst.values.add(subresults[i], new Value(subresults[i], subresult)); topinst[main] = inst[main]; TopData.Add(topinst); top.train(topdata); We splitsen de traindata eerst in twee lijsten. Daarna gebruiken we de eerste lijst om de subclassifiers te trainen. Daarna maken we een topdata lijst, waar we als features de voorspellingen van de subclassifiers op de tweede lijst insteken, en als voorspellingsfeature (main) de main feature van diezelfde tweede lijst. Afhankelijk van de voorspellingen van de sublaag zal de topclassifier dan een voorspelling maken TrainAndPrune Dit is weer een zeer eenvoudige metaclassifier, die één enkele subclassifier heeft. Deze laatste moet Prunable zijn. TrainAndPrune doet dan niet meer dan ervoor te zorgen dat de Prunable bij een aanroep van Train() ook gepruned wordt, eventueel met een verschillend onderdeel van de traindata. 5.6 Threading Tot nu toe hebben we onze hardware een beetje gespaard. Zowel Weka als de eerdere implementaties van de service zijn single-threaded, wat betekent dat we maar één van de 8 cores 8 gebruiken. Als we dat een beetje naief bekijken, betekent dat ook dat het 8 keer sneller kan, op voorwaarde natuurlijk dat we ons programma in threads kunnen verdelen Threading bij Cross Validation Van Cross Validation hebben we de basisimplementatie nog niet besproken, om de eenvoudige reden dat het hele algoritme niet veel meer is dan een stevige for-lus. Als we er threading op loslaten wordt het wel interessant. 8 Logische cores, welteverstaan. We beschikken over 4 fysieke cores met elk 2 logische cores 69

70 Hoofdstuk 5. Implementatie 70 Cross Validation is één van de meest eenvoudige processen om in threads te verdelen: we doen een aantal keer exact hetzelfde, met andere data, en de verschillende stappen zijn compleet onafhankelijk. De ideale kandidaat dus om de belasting over een aantal threads te spreiden. Threading in C# is relatief eenvoudig, maar voor het meegeven van meerdere parameters aan elke thread hebben we toch wat lambda-gegoochel nodig. We gebruiken volgende code: bestcoco = 0.0; List<Thread> threads = new List<Thread>(); for (int i = 0; i < folds; i++) if (enablemultithread) int k = i; // i might get changed before t uses it. // k won t because it exists only in this iteration. Thread t = new Thread(() => TestFold(data, k, folds)); t.start(); threads.add(t); else TestFold(data, i, folds); foreach (var t in threads) t.join(); Twee zaken die het vermelden waard zijn. Ten eerste, we kopiëren de lusteller i in een nieuwe variabele k. Zoals de commentaar aangeeft, kan het zijn dat de lus naar de volgende waarde voor i gaat voordat de nieuwe thread deze kan gebruiken, en dan zal die thread de verhoogde waarde van i gebruiken. Om dat te vermijden kunnen we eenvoudigweg de waarde kopiëren naar een tijdelijke variabele, die niet meer zal veranderen. Een ander interessant punt is de constructor van de thread zelf. Normaalgezien zouden we hiervoor new Thread(new ThreadStart(TestFold)) gebruiken, maar daarmee kunnen we geen parameters meegeven. Het aanmaken van een nieuwe klasse waarin we de variabelen opslaan zou een hoop onnodig schrijfwerk meebrengen, dus gebruiken we de derde mogelijkheid: 70

71 Hoofdstuk 5. Implementatie 71 de lambda-expressie () => TestFold(data, k, folds). Wat is nu het resultaat van deze ingreep? Om te beginnen kijken we eens naar het processorgebruik. Wat men ook zegt, Windows 8 zorgt wél voor zeer mooie grafiekjes: figuur 5.2 toont het gebruik tijdens Cross Validation met en zonder multithreading. Het Figuur 5.2: CPU-gebruik tijdens Cross Validation. belangrijkste is natuurlijk niet dat onze CPU overbelast wordt, we willen vooral dat ons programma snel gaat. De resultaten spreken voor zich: het opbouwen van een 10-voudige 71