Met de opkomst van Grote Taalmodellen (LLM’s) zoals Chat-GPT, is Kunstmatige Intelligentie (AI) een veelbesproken onderwerp geworden en staat het centraal bij veel bedrijven. Er is geen weg meer omheen, AI beïnvloedt wereldwijd het leven van iedereen en zal de komende jaren alleen maar invloedrijker worden. Aangezien enorme hoeveelheden financiering in AI-onderzoek worden gestoken, vordert de vooruitgang zo snel dat werken met de nieuwste technologieën zeer opwindend is geworden. Dingen die een paar maanden geleden nog niet mogelijk waren, zijn nu wel mogelijk, en dingen die nu niet mogelijk zijn, zullen over een paar maanden wel mogelijk zijn.
Een van de gebieden die de afgelopen maanden steeds meer aandacht heeft gekregen, is het fine-tunen van LLM’s. Tijdens mijn drie maanden durende stage bij Squadra Machine Learning Company heb ik aan dat onderwerp gewerkt. Het doel van mijn stage was om een generatief model te fine-tunen, zodat het in staat zou zijn om kenmerken en de waarden van die kenmerken uit een productbeschrijving te halen. Een eerdere aanpak om dit probleem aan te pakken was om kenmerken te extraheren met behulp van Named Entity Recognition (NER), maar deze methode heeft geen bevredigende resultaten opgeleverd. Nu LLM’s ongelooflijk goed zijn geworden in verschillende taken met betrekking tot Natural Language Processing (NLP), zou het mogelijk moeten zijn om kenmerken met behulp van hen te extraheren. In deze blogpost zal ik de verschillende stappen doornemen die ik heb doorlopen om een generatief model voor deze taak te fine-tunen.
Maar wat doet het fine-tunen van een generatief model precies? Door een voorgeleerd generatief model aan te passen, wordt het model op een bepaalde manier aangepast zodat het bepaalde taken zeer goed kan uitvoeren. De basismodellen zijn getraind op een breed scala aan teksten en zijn goed in het beantwoorden van algemene vragen of het voeren van gesprekken, maar het extraheren van de kenmerken uit een productbeschrijving kan een moeilijkere taak zijn voor zo’n model. Hier kan fine-tuning nuttig zijn, door een model te trainen om een enkele taak zeer goed uit te voeren, terwijl de kennis van het oorspronkelijke model behouden blijft.
Model selecteren
De eerste stap was het selecteren van een voorgeleerd model dat ik wilde fine-tunen. Er zijn duizenden open-source modellen om uit te kiezen, elk met zijn eigen sterke en zwakke punten. Een van de doelen van mijn stage was om alles te doen op een consumenten (16GB) GPU. Dit neemt al de mogelijkheid weg om veel modellen te gebruiken omdat ze te groot zijn om op een 16GB GPU te passen. Ik ontdekte dat het met enkele trucjes, waar ik later op terug zal komen in de post, mogelijk was om modellen met maximaal 7 miljard parameters te fine-tunen. Ter vergelijking heeft het GPT-4-model van OpenAI 1,76 biljoen parameters. De prestaties van deze ‘kleine’ LLM’s zijn de afgelopen paar maanden ongelooflijk verbeterd, en tal van topmodellen zijn open source beschikbaar, voor iedereen om te gebruiken. Op de website van Huggingface kun je veel van deze modellen vinden. Huggingface is een bedrijf dat een reeks nuttige tools heeft ontwikkeld die kunnen worden gebruikt om je leven gemakkelijker te maken bij het bouwen van dit soort toepassingen. Aan het begin van mijn stage waren de twee 7b-modellen die het beste presteerden Mistral Ai’s Mistral-7b en Meta’s Llama2-7b, dus mijn plan was om te proberen deze modellen te fine-tunen.
Dataset maken
Je moet het model voorbeelden laten zien van wat het moet genereren, zodat het zijn gewichten dienovereenkomstig kan aanpassen. Ik heb een dataset gemaakt die bestond uit prompts in dit formaat en de data opgesplitst in trainings-, validatie- en testdata. In mijn geval eindigde ik na wat experimenteren met het volgende promptformaat:
Het gebruik van hashtags in de trainingsprompt fungeert als een scheidingsteken, waardoor het model de structuur van de prompt gemakkelijker kan leren, dit leek zeer goed te werken voor mijn gebruik.
Fine-tuning
Om het voorgeleerde model te fine-tunen, moet het eerst worden geladen. Om een 7b-model in volledige precisie uit te voeren, is ongeveer 28 GB aan GPU-RAM nodig. Om problemen met onvoldoende geheugen te voorkomen, kan het model worden geladen met kwantisatie om de omvang ervan te verkleinen. De bitsandbytes-bibliotheek maakt het mogelijk om een model in 8-bit of 4-bit te laden. Met 4-bit kwantisatie heb je slechts ongeveer 7 GB RAM nodig om het model en de activaties en aandachtcache te laden, wat klein genoeg is om op een consumenten-GPU te passen. Nadat het model is geladen, moet ook een tokenizer worden gemaakt. Tokenizers kunnen ook worden geladen vanuit huggingface en worden samen met het voorgeleerde model opgeslagen. Tokenizers zorgen ervoor dat de invoer voor een model in de juiste vorm is. De invoerprompts worden eerst getokeniseerd in subwoorden, die vervolgens worden omgezet naar ids. De tokenizer voegt ook paddingtokens toe die worden gebruikt om ervoor te zorgen dat de invoeren dezelfde lengte hebben; dit is essentieel voor het trainen van het model met batches van meerdere prompts van verschillende lengtes. Nadat alle invoeren zijn getokeniseerd, kan het fine-tunen beginnen. Om alle 7 miljard parameters van het model fijn te tunen, is te veel rekenkracht en tijd nodig om op een consumenten-GPU te worden uitgevoerd. Een manier om het aantal benodigde middelen te verminderen om een LLM fijn te tunen, is het gebruik van de Parameter Efficiënt Fine-Tuning (PEFT)-bibliotheek, en meer specifiek, Low-Rank Adaptation (LoRA). LoRA is een PEFT-methode die de oorspronkelijke gewichten van het netwerk bevriest en alleen een klein percentage van de oorspronkelijke gewichten opnieuw traint. Nadat LoRA is opgezet, wordt de trainingsdata aan het model gevoed met behulp van de Trainer-klasse ontwikkeld door huggingface. Het resulterende adaptermodel kan vervolgens boven op het oorspronkelijke model worden geladen en het resulterende model geeft vergelijkbare prestaties als een volledig fijngetuned model. Het was moeilijk om een juiste hyperparameterafstemming te doen omdat het controleren van de validatieloss meerdere keren per combinatie van instellingen veel te lang zou duren, dus dit moest experimenteel met de hand worden gedaan.
Resultaten
Ik heb een testset gemaakt, die net als de trainingsset, bestond uit 50% kenmerken die waarden hadden, en 50% kenmerken die geen waarde hadden. Met behulp van deze methode geven de best presterende modellen een nauwkeurigheid van maximaal 94% op een onafhankelijke testset, waarbij ongeveer 8% van de kenmerken verkeerd werden geclassificeerd door het model, en ongeveer 4% van de gevallen waarin het model geen waarde moest geven, werd een waarde gegeven. Deze resultaten zijn veel beter dan de vorige NER-methode, die een nauwkeurigheid van ongeveer 80% bereikte. Een ander interessant resultaat dat ik heb gevonden, is dat er niet veel trainingsdata nodig is om deze resultaten te krijgen, met ongeveer 100 trainingsvoorbeelden die voldoende zijn voor fine-tuning in mijn geval. Er is slechts één probleem, het resulterende model is te langzaam om in de praktijk te worden gebruikt. Het kost ongeveer twee seconden om één kenmerk te voorspellen, dus als er 50 verschillende kenmerken in een dataset zijn, zou het ongeveer tweeënhalve minuut duren om deze te bevriezen. De laatste paar weken van mijn stage heb ik geprobeerd deze snelheid te verhogen.
Versnellen van de inferentie
De belangrijkste reden waarom het model zo lang duurt per kenmerk is omdat het de hele prompt moet doorlopen voordat het de kenmerkwaarde kan voorspellen, en de prompt is vrij lang voor een enkel kenmerk. Om het aantal tekens te verminderen dat het model elke keer moet doorlopen, heb ik tijdens de inferentie hidden state caching geïmplementeerd. Dit is een hulpmiddel dat kan worden gebruikt als de prompts die tijdens de inferentie worden gebruikt, een vergelijkbare sequentie aan het begin van de prompt bevatten. Met behulp van hidden state caching kunt u een voorwaartse pass door het model maken op de groene tekst, de verborgen toestanden opslaan, en vervolgens hoeft het model voor elk kenmerk alleen door de rode tekst te gaan wat ongeveer de helft van de leestijd bespaart.
Daarna heb ik een model getraind om meerdere prompts tegelijk te voorspellen, dit zou ook vooral goed werken in combinatie met hidden state caching, aangezien alleen de groene tekst langer zou worden terwijl de lengte van de rode tekst hetzelfde blijft. Dit werkte wel, maar er moet nog meer getest worden hoeveel de inferentiesnelheid daadwerkelijk toeneemt en het mogelijke verlies aan nauwkeurigheid dat zou kunnen optreden.
Een andere manier die ik heb geprobeerd om de inferentiesnelheid te verlagen is het gebruik van een kleiner model. Een paar maanden in mijn stage werden modellen met 3 miljard parameters steeds populairder en hadden ze prestaties laten zien die gelijk waren aan talloze 7b-modellen. Ik heb een beetje geëxperimenteerd met het phi-2-model van Microsoft en kreeg vergelijkbare resultaten als het mistral-7b-model. Deze 3b-modellen versnellen de inferentiesnelheid al met een factor twee vergeleken met de 7b-modellen. Een laatste ding dat ik wilde uitproberen was het gebruik van de vLLM-bibliotheek, die volgens sommige bronnen de inferentiesnelheid met 20 keer kan verhogen in vergelijking met de methode die ik gebruikte. De 3b-model die ik echter gebruikte, werd op het moment dat mijn stage eindigde nog niet ondersteund door vLLM, dus ik heb dit niet kunnen proberen. Om samen te vatten, functie-extractie met behulp van fijngetunede generatieve modellen is zeker mogelijk, maar op dit moment te langzaam om in de praktijk te worden gebruikt. Mijn ervaring is dat er veel onderzoek wordt gedaan naar de ontwikkeling van deze modellen, maar minder naar de daadwerkelijke implementatiekant.
Mik van der Drift
Stagiair Data Science bij Squadra Machine Learning Company