Nogle vil kalde stylometri en nørdet måde at misbruge god litteratur på, og det kan der være noget om. Til gengæld er det på mange måder et fascinerende misbrug. Min undskyldning for at kaste mig over det i denne blog-serie er, at jeg skal bruge nogle rutiner til klargøring af online-tekster til analyse i Microsoft’s Machine Learning modul ML.NET.
Ordet Stylometri skriver sig tilbage til Wincenty Lutosławski’s bog ”Principes de stylométrie” fra 1890, hvor han med baggrund i kvantitative metoder søger at fastslå en kronologi i Platons klassiske dialoger. Stylometrien arbejder med at afdække statistisk målbare forfatter-karakteristika gennem analyser af tekster. En klassisk opgave for stylometrien er, at kunne påvise eller sandsynliggøre, hvem forfatteren er til en given tekst af ukendt oprindelse. Stylometrien har også undersøgt forskelle mellem mandlige og kvindelige forfatteres stilmæssige karakteristika, udvikling i løbet af en forfatters karriere, herunder f.eks. hvorvidt det stylometrisk kan spores i forfatterskaber, at de pågældende forfattere sent i produktionen udviklede Alzheimers sygdom eller led af bipolar affektiv lidelse i perioder under forfatterskabet.
I en mere åndløs digital tidsalder gør stylometrien sig nyttig ved at bidrage med metoder til f.eks. at identificere spam-mails, falske blog-indlæg og såmænd også identificere hvilke opslag på Donald Trumps Twitterprofil, han selv har skrevet. Det viser sig at være indlæg med overvægt af ord som “dumb”, “dead”, “badly”, “weak”, “crazy”, “guns”. Hvem havde gættet det?
Som bekendt er der på nettet et væld af fuld-tekster at tage fat på, bl.a. på Gutenberg.org og Arkiv for Dansk Litteratur. Digitale tekster kan være af meget svingende kvalitet med OCR-læsninger, der lader en del tilbage at ønske. Ideelt set burde man vel gå dem manuelt igennem før videre bearbejdning, men det er kun fristende, hvis man har et meget kedeligt liv. Så det kommer ikke på tale.
Hent de ønskede tekster ned og gem dem i en UTF8-fil. Fjern tekstdele som ikke hører til det egentlige værk, f.eks. kolofon, copyrightnoter, udgivers forord etc. Herefter kan følgende kode bruges til at indlæse teksten og foretage en “grov-vaskning”:
public static string PreProcessText(string fname, Encoding enc, bool fixDoubleA = true) { StringBuilder builder = new StringBuilder(); using (var file = new StreamReader(fname, enc)) { string line; while ((line = file.ReadLine()) != null) { //Ændre dobbelt-a til å line = (fixDoubleA) ? line.Replace("aa", "å").Replace("Aa", "Å").Trim() : line; // sammenføj evt. orddeling ved libnjeafslutning if ((line.Length > 0) && (line.Last() == '-')) builder.Append(line.Remove(line.Length - 1, 1).Trim()); else builder.AppendLine(line.Trim()); } } // Fjern uønskede karakterer fra teksten Regex rgx = new Regex("[^a-zøæåöüA-ZÆØÅÖÜ0-9.,;\'-:!?()=\"”'»«(\r\n)-]"); string Text = rgx.Replace(builder.ToString(), " "); //Ensart linjeskift Text = Regex.Replace(Text, @"(\r\n)+", "\r\n"); return Text; }
Et teksteksempel viser den indlæste tekst før grov-vaskningen:
||Jeg var et Øieblik raadvild, derpaa henvendte jeg mig til Guderne saaledes: Høistærede Samtidige, jeg vælger een Ting, at jeg altid maa have Latteren paa min Side. Der var ikke en Gud, der svarede et Ord, derimod gave de sig alle til at lee.
Deraf sluttede jeg, at min Bøn var opfyldt, og fandt, at Guderne vidste at udtrykke sig med Smag; thi det havde jo dog været upassende, alvorligt at svare: det er Dig indrømmet.
-
De umiddelbare erotiske Stadier eller det Musikalsk-Erotiske <
Og efter :
Jeg var et Øieblik rådvild, derpå henvendte jeg mig til Guderne således: Høistærede Samtidige, jeg vælger een Ting, at jeg altid må have Latteren på min Side. Der var ikke en Gud, der svarede et Ord, derimod gave de sig alle til at lee.
Deraf sluttede jeg, at min Bøn var opfyldt, og fandt, at Guderne vidste at udtrykke sig med Smag; thi det havde jo dog været upassende, alvorligt at svare: det er Dig indrømmet.
De umiddelbare erotiske Stadier eller det Musikalsk-Erotiske
I denne første manipulation laves de gamle teksters dobbelt-a om til å. Det er for at gøre dem mere kompatible med stopords-lister, sentiments-lister og lignende. Desuden fjernes lidt andet overflødigt skrammel fra teksterne. Der er givetvis plads til forbedringer i rutinen, men funktionen indlæser tekster på et pænt tilfredsstillende niveau.
Næste skridt er muligheden for at få splittet den indlæste tekst op i en liste af ord. Følgende giver i bogstaveligste forstand rene ord for pengene:
private static readonly char[] wordDelimiters = new[] { ' ', '.', ',', ';', '\'', '-', ':', '!', '?', '(', ')', '=', '"', '”', '\t', '\r', '\n' }; public static List<string> RemoveStopWords(List<string> words) { List<string> reduced = new List<string>(); foreach (string word in words) { if ((!Resources.stopwords.Contains(word.ToLower())) && (Regex.Replace(word, @"[\d-]", string.Empty) != "")) reduced.Add(word); } return reduced; } public static List<string> GetWords(string text, bool removeStopwords) { var normalizedString = text.Split(wordDelimiters, StringSplitOptions.RemoveEmptyEntries); var words = Array.ConvertAll(normalizedString, a => a.ToLower()).ToList(); return removeStopwords ? RemoveStopWords(words) : words; }
Genereringen af ord-listen kan vælges med eller uden stopord. Hvad er så lige stopord??? Ifølge den danske ordbog er stopord: ”meget almindeligt eller udbredt ord eller tegn som er uegnet som søgeord ved søgning i elektroniske tekster og derfor ofte udelukkes ved søgningen fx og,eller”. I mange tilfælde kan de med fordel fjernes af teksten. Der findes på nettet en dansk stopord-liste, f.eks. her og her.
Ovenstående teksteksempel ser herefter således ud – uden fjernelse af stopord:
jeg var et øieblik rådvild derpå henvendte jeg mig til guderne således høistærede samtidige jeg vælger een ting at jeg altid må have latteren på min side der var ikke en gud der svarede et ord derimod gave de sig alle til at lee deraf sluttede jeg at min bøn var opfyldt og fandt at guderne vidste at udtrykke sig med smag thi det havde jo dog været upassende alvorligt at svare det er dig indrømmet de umiddelbare erotiske stadier eller det musikalsk erotiske
Og efter fjernelse af stopord:
øieblik rådvild henvendte guderne høistærede samtidige vælger een ting latteren gud svarede derimod gave lee deraf sluttede bøn opfyldt fandt guderne vidste udtrykke smag upassende alvorligt svare indrømmet umiddelbare erotiske stadier musikalsk erotiske
En yderligere brugbar funktion er muligheden for at kunne opdele en længere tekst i mindre sammenlignelige bidder. De kan efter behag kaldes tekst-blokke, klynger, clustrer. Funktionen getWordClusters er et bud på at løse den problemstilling:
public static List<string> getWordClusters(string text, int ClusterSize, bool removeStopwords = false) { var wordlist = GetWords(text, removeStopwords); decimal numberOfClusters = wordlist.Count / ClusterSize; numberOfClusters = Math.Floor(numberOfClusters); var resultlist = new List<string>(); for (var counter = 0; counter < numberOfClusters; counter++) { resultlist.Add(wordlist.GetRange(counter * ClusterSize, ClusterSize).Aggregate((ii, jj) => ii + " " + jj)); } return resultlist; }
Med en valgt clusterSize på 10, vil ovenstående teksteksempel ende med tre tekstblokke af hver 10 ord:
øieblik rådvild henvendte guderne høistærede samtidige vælger een ting latteren
gud svarede derimod gave lee deraf sluttede bøn opfyldt fandt
guderne vidste udtrykke smag upassende alvorligt svare indrømmet umiddelbare erotiske
Den samme øvelse, som her er gjort med ord, kan gøres på sætninger. Efter en flaske rødvin vil man være tilbøjelig til at argumentere for, at en sætning er et tilfældigt antal på hinanden følgende ord, der enten afsluttes med punktum, udråbstegn eller spørgsmålstegn. Efter at være blevet ædru igen, vil de fleste sædvanligvis erkende, at det ikke er helt så enkelt. F.eks. vil teksten ”Hr. P.G. Philipsen betalte 12 rd. og 10 sk. for varerne.” ende op som 6 sætninger, hvilket i betydelig grad udvander begrebet en sætning. Jeg er ikke bekendt med, at der findes en endegyldig formel til at hive sætninger ud af tekster på en helt fejlfri måde. I arbejde med ældre tekster har jeg brugt en liste, sentenceIgnoreList, med hyppigst forekommende forkortelser, efter hvilke et punktum ikke udgør en sætnings-afslutning. Følgende funktion giver endvidere mulighed for at træffe en diktatorisk beslutning om, at en gyldig sætning har i det mindste minWords ord (default=3) og minChars antal bogstaver (default=50). Det optimerer i en vis grad og efterfølgende manuel gennemgang viser, at det giver resultater tæt på det ønskede.
private const string sentenceIgnoreList = "sk,hr,frk,rbd,f,ex,etc"; public static string StringWordsRemove(string stringToClean, List<string> stopwords) { return string.Join(" ", stringToClean .Split(new[] { ' ', ',', '.', '?', '!' }, StringSplitOptions.RemoveEmptyEntries) .Except(stopwords)); } public static List<string> GetSentences(string text, bool removeStopwords = false, int wordMin = 3, int charMin = 50) { var stopwords = Properties.Resources.stopwords.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries).ToList(); var sentences = new List<string>(); string sentence = ""; var temporarySentences = Regex.Split(text, @"(?<=[\.!\?])\s+").ToList(); foreach (var temporarySentence in temporarySentences) { sentence += (removeStopwords) ? removeLineBreaksAndMultipleSpaces(StringWordsRemove(temporarySentence, stopwords)) : removeLineBreaksAndMultipleSpaces(temporarySentence); //Split sætningen op i ord for kontrol af længde m.v. var words = GetWords(sentence, false); if ((words.Count >= wordMin) && (sentence.Length >= charMin) && (!sentenceIgnoreList.Contains(words.Last().ToLower()))) { //Sætningen valideret - tilføj sentences.Add(sentence); sentence = ""; } else { //Sætning ikke valid - fortsæt med tilføjelse af næste sentence += " "; } } return sentences; } public static List<string> getSentenceClusters(string text, int ClusterSize, bool removeStopWords = false) { var sentencelist = GetSentences(text, removeStopWords); decimal numberOfClusters = sentencelist.Count / ClusterSize; numberOfClusters = Math.Floor(numberOfClusters); var resultlist = new List<string>(); for (var counter = 0; counter < numberOfClusters; counter++) { resultlist.Add( sentencelist.GetRange(counter * ClusterSize, ClusterSize).Aggregate((ii, jj) => ii + " " + jj)); } return resultlist; }
Hermed er meget af det grove arbejde gjort for at komme i gang med stylometriens sjovere og mere meningsfulde sider. Herom mere i del 2, del 3 og del 4.
