ngrams in ruby
Monday, November 5th, 2007Een technisch stukje. Voor een project moest ik N-Grams bepalen van teksten. Wat is dit nu juist: wanneer je een woord neemt, en een bepaald getal (neem 3), dan zijn alle n-grams met lengte 3 (ook wel ‘trigrams’ genoemd dan) elk stukje van 3 letters uit het woord. Als het woord dus “voorbeeld” is, dan is dat “voo”, “oor”, “orb”, etc. Voor bepaalde taalproblemen zijn niet de woorden, maar wel ngrams de werkmiddelen.
Wat op het eerste zicht een triviaal probleem lijkt, resulteert al snel in een dubbele for-lus, met daarin de “substr”-variant van de gebruikte taal. Aangezien dat er volgens mij ongelooflijk lelijk uitziet, heb ik blijven zoeken naar een andere oplossing. Met een goeie voorzet van iemand op IRC kwam ik tot de volgende oplossing:
>> word = "voorbeeld" >> word.split('').each_cons(3){|n| puts n.join } voo oor orb rbe bee eel eld => nil
Dit gebruikt ‘enumerator’ voor de each_cons-methode!. Natuurlijk ga ik de resultaten niet gewoon uitschrijven, maar ik sla ze op in een gewone array om dan verder te gebruiken.
Op zich ziet het er wel beter uit dan een dubbele for-lus, maar natuurlijk blijft het een heel kostelijke operatie. Er wordt gegoocheld met arrays, deze worden omgezet in dubbele arrays, en uiteindelijk wordt elk onderdeel terug samengesteld. De split met de lege string vind ik ook geen schitterende oplossing…
Op zich dus een moeilijke keuze (one-liner tegen ik-vind-dat-vieze-code). De dubbele lus zou al snel duidelijker zijn voor mensen die voor het eerst naar de code kijken, zeker met een substr erin. Maar dan opteer ik voor goed commentaar:
... # Make ngrams with length: word="example", length=3 => "exa", "xam", "amp", "mpl", "ple" word.split('').each_cons(length){|n| ngrams < < n.join } ...
Dat is toch een stuk duidelijker lijkt me.