Ga naar hoofdinhoud

Machine Learning en NLP met R: Topic Modeling en Muziekclassificatie

In deze tutorial bouw je vier modellen met de machinelearning-algoritmen Latent Dirichlet Allocation (LDA) en K-Means clustering.
· 15 min lezen

Verken met AI

Gebruik geavanceerde technieken met R, NLP en Machine Learning om onderwerpen in tekst te modelleren en bouw je eigen muziekaanbevelingssysteem!

Dit is deel Twee-B van een driedelige tutorialreeks waarin je R blijft gebruiken om allerlei analysetaken uit te voeren op een casestudy van songteksten van de legendarische artiest Prince, evenals andere artiesten en auteurs. De drie tutorials behandelen het volgende:

Stel je voor: je bent een Data Scientist bij NASA. Je krijgt de opdracht om twee ruimtevaartuigen te monitoren die bedoeld zijn voor maanbanen en die de interacties van de maan met de zon zullen bestuderen. Zoals bij elk ruimtevaartuig kunnen er problemen optreden die tijdens tests of na lancering worden vastgelegd. Deze afwijkingen worden in natuurlijke taal gedocumenteerd in bijna 20.000 probleemrapporten. Jouw taak is om de populairste onderwerpen en trending thema’s in het corpus van rapporten te identificeren en prescriptieve analyses te leveren aan de ingenieurs en programmadirecteuren, en zo de toekomst van ruimteverkenning te beïnvloeden.

Dit is een voorbeeld van een echtsituatie en laat zien welke kracht je tot je beschikking hebt nadat je cutting-edgetechnieken hebt geleerd, zoals topic modeling met machine learning, Natural Language Processing (NLP) en programmeren in R. Gelukkig hoef je niet bij NASA te werken om iets net zo innovatiefs te doen. In deze tutorial is je missie, als je ervoor kiest om die te accepteren, om te leren over topic modeling en hoe je een eenvoudig aanbevelingssysteem (a.k.a. recommender) bouwt op basis van songteksten en non-fictieboeken.

Gewoon voor de lol!

Dit heeft niets met topic modeling te maken, maar gebruik voor de lol een geweldig pakket genaamd circlize, geïntroduceerd in een eerdere tutorial, om je fotoreceptoren aan het werk te zetten en te genieten van dit cirkeldiagram van alle bronnen die je in deze tutorial gebruikt.

 library(jpeg)
library(circlize) #you'll use this package later
#read in the list of jpg files of album/book covers
files = list.files("jpg\\", full.names = TRUE)
#clean up the file names so we can use them in the diagram
removeSpecialChars <- function(x) gsub("[^a-zA-Z]", " ", x)
names <- lapply(files, removeSpecialChars)
names <- gsub("jpg","", names )

#read up on the circlize package for details
#but there will be comments in later sections
circos.clear()
circos.par("points.overflow.warning" = FALSE)
circos.initialize(names, xlim = c(0, 2))
circos.track(ylim = c(0, 1), panel.fun = function(x, y) {
  image = as.raster(readJPEG(files[CELL_META$sector.numeric.index]))
  circos.text(CELL_META$xcenter, CELL_META$cell.ylim[1] - uy(1.5, "mm"),
              CELL_META$sector.index,
              CELL_META$sector.index, facing = "clockwise", niceFacing = TRUE,
              adj = c(1, 0.5), cex = 0.9)
  circos.raster(image, CELL_META$xcenter, CELL_META$ycenter, width = "1.5cm",
        facing = "downward")
}, bg.border = 1, track.height = .4)
wiel met albumcovers

Structuur

In eerdere tutorials heb je de inhoud van Prince’ songteksten bestudeerd met woordfrequentie, de tf-idf-statistiek en sentimentanalyse. Je kunt dieper inzicht krijgen in songteksten en andere tekst door het concept van topic modeling te gebruiken.

In deze tutorial bouw je vier modellen met de machinelearning-algoritmen Latent Dirichlet Allocation (LDA) en K-Means clustering. Ik leg de algoritmen later in de tutorial uit.

Model Eén gebruikt tekst uit Prince’ songteksten gecombineerd met twee non-fictieboeken. Het doel van dit model is je een eenvoudig voorbeeld te geven van het gebruik van LDA, een unsupervised algoritme, om collecties woorden te genereren die samen thema’s suggereren. Deze thema’s zijn ongetagd en vereisen menselijke interpretatie. Nadat je een vooraf bepaald aantal thema’s hebt geïdentificeerd, neem je de resultaten van je model en classificeer je elk nummer of elke boekpagina in een categorie. Een nummer kan alle gegenereerde topics bevatten, maar past vaak duidelijker in het ene dan in het andere. Dit heet documentclassificatie, maar omdat je de schrijver van het document kent, classificeer je in feite schrijvers in thematische groepen.

Model Twee gebruikt de dataset van Model Eén en geeft een snelle blik op het genereren van thema’s met een ander algoritme, k-means, en hoe dat mogelijk niet de beste keuze is voor topic modeling.

Wederom met LDA gebruikt Model Drie een grotere dataset met meerdere artiesten uit verschillende genres en meer boekauteurs. Wanneer je de meerderheid van documenten van een auteur/artiest in hun meest waarschijnlijke topic classificeert, kun je schrijvers met vergelijkbare thematische samenstelling groeperen. Met een degelijk model zie je vergelijkbare genres in dezelfde topicgroepering verschijnen. Bijvoorbeeld: rapartiesten verschijnen in één topic, en twee boeken over een vergelijkbaar onderwerp zouden samen in een ander topic moeten staan. Zo kun je potentieel vergelijkbare schrijvers aanbevelen! Uiteraard zijn de schrijvers en hun genre gelabeld, maar de topics niet en zijn dus unsupervised.

Model Vier gebruikt LDA alleen op de songteksten van Prince. Deze keer gebruik je een geannoteerde dataset die buiten deze tutorial is gegenereerd, waarin elk woord is getagd met een woordsoort (bijv. zelfstandig naamwoord, werkwoord, etc.) en de grondvorm (lemmatized) van het woord. Je bekijkt ook hoe een topic in de tijd verandert.

Introductie

NLP en Machine Learning zijn deelgebieden van Kunstmatige Intelligentie. Er zijn recente pogingen om AI in te zetten voor songschrijven. Dat is niet het doel van deze tutorial, maar het is een voorbeeld van AI als kunst. De eerste drie letters zijn tenslotte A-R-T! Vergelijk AI even met songschrijven: je kunt gemakkelijk een patroon volgen en een structuur maken (couplet, refrein, couplet, etc.), maar een nummer wordt pas echt goed als de schrijver creativiteit injecteert in het proces. In deze tutorial moet je je technische skills inzetten om modellen te bouwen, maar je moet ook je artistieke creativiteit aanspreken en leren je resultaten te interpreteren en uit te leggen.

Denk hier eens over na: Als topic modeling een kunst is, dan zijn lyrics de code, is songschrijven het algo-ritme, en is het nummer het model! Wat is jouw analogie?

Typische aanbevelingssystemen zijn niet volledig op tekst gebaseerd. Netflix kan bijvoorbeeld films aanbevelen op basis van eerdere keuzes en bijbehorende metadata. Pandora pakt het anders aan en vertrouwt op een Music Genome met 400 muzikale kenmerken, waaronder melodie, ritme, compositie en samenvattingen van lyrics; echter, de ontwikkeling van dit genome begon in 2000 en duurde vijf jaar en 30 experts in muziektheorie om te voltooien. Er zijn talloze onderzoeksartikelen die filmrecommendaties op basis van plot-samenvattingen behandelen. Voorbeelden van muziekaanbevelingssystemen die op daadwerkelijke songtekstinhoud zijn gebaseerd, zijn lastiger te vinden. Wil je het proberen?

Vereisten

Deel Twee-B van deze tutorialreeks vereist basiskennis van tidy data – specifiek pakketten als dplyr en ggplot2. Oefening met text mining met tidytext wordt ook aanbevolen, aangezien dit in eerdere tutorials aan bod kwam. Je moet ook bekend zijn met R-functies, omdat je er een paar gebruikt om je code per model te hergebruiken. Om ons te focussen op inzichten, heb ik code opgenomen die is ontworpen om op je eigen dataset te gebruiken, maar een gedetailleerde uitleg kan nader onderzoek van jouw kant vereisen.

Bereid je vragen voor

Wat kun jij met topic modeling? Misschien zijn er documenten met cruciale informatie voor je bedrijf die je kunt minen. Misschien kun je data van Twitter scrapen en ontdekken wat mensen over de producten van je bedrijf zeggen. Misschien bouw je je eigen classificatiesysteem om bepaalde documenten naar de juiste afdelingen te sturen voor analyse. Er is een wereld aan mogelijkheden die je kunt verkennen! Ga ervoor!

Waarom is Topic Modeling nuttig?

Enkele toepassingen van topic modeling:

  • Documentsamenvattingen: Gebruik topicmodellen om wetenschappelijke artikelen te begrijpen en samen te vatten, wat sneller onderzoek en ontwikkeling mogelijk maakt. Hetzelfde geldt voor historische documenten, kranten, blogs en zelfs fictie.
  • Tekstclassificatie: Topic modeling kan classificatie verbeteren door vergelijkbare woorden in topics te groeperen in plaats van elk woord als afzonderlijk kenmerk te gebruiken.
  • Aanbevelingssystemen: Met probabiliteiten op basis van gelijkenis kun je aanbevelingssystemen bouwen. Je zou lezers artikelen kunnen aanbevelen met een topicstructuur die lijkt op artikelen die ze al hebben gelezen.
  • Heb je andere ideeën?

Voorbereidend werk

Libraries en functies

De enige nieuwe pakketten in deze tutorial die niet in Deel Eén of Deel Twee-A zitten, zijn topicmodels, tm en plotly. Je gebruikt de functie LDA() uit topicmodels waarmee modellen kunnen leren onderscheid te maken tussen verschillende documenten. Het pakket tm is voor text mining, en je gebruikt slechts één keer de functie inspect() om de binnenkant van een document-termmatrix te bekijken, wat ik later zal uitleggen. plotly gebruik je één keer om een interactieve versie van een ggplot2-grafiek te maken.

 library(tidytext) #text mining, unnesting
library(topicmodels) #the LDA algorithm
library(tidyr) #gather()
library(dplyr) #awesome tools
library(ggplot2) #visualization
library(kableExtra) #create attractive tables
library(knitr) #simple table generator
library(ggrepel) #text and label geoms for ggplot2
library(gridExtra)
library(formattable) #color tile and color bar in `kables`
library(tm) #text mining
library(circlize) #already loaded, but just being comprehensive
library(plotly) #interactive ggplot graphs

De volgende functies zijn in eerdere tutorials gedefinieerd met uitzondering van word_chart(); de inhoud en beschrijving van word_chart() vind je echter ook in Deel Twee-B. Het belangrijkste om te weten is dat het ggplot() op een unieke manier gebruikt om een visual te maken op basis van woorden in plaats van punten.

 #define some colors to use throughout
my_colors <- c("#E69F00", "#56B4E9", "#009E73", "#CC79A7", "#D55E00", "#D65E00")

#customize ggplot2's default theme settings
#this tutorial doesn't actually pass any parameters, but you may use it again in future tutorials so it's nice to have the options
theme_lyrics <- function(aticks = element_blank(),
                         pgminor = element_blank(),
                         lt = element_blank(),
                         lp = "none")
{
  theme(plot.title = element_text(hjust = 0.5), #center the title
        axis.ticks = aticks, #set axis ticks to on or off
        panel.grid.minor = pgminor, #turn on or off the minor grid lines
        legend.title = lt, #turn on or off the legend title
        legend.position = lp) #turn on or off the legend
}

#customize the text tables for consistency using HTML formatting
my_kable_styling <- function(dat, caption) {
  kable(dat, "html", escape = FALSE, caption = caption) %>%
  kable_styling(bootstrap_options = c("striped", "condensed", "bordered"),
                full_width = FALSE)
}

word_chart <- function(data, input, title) {
  data %>%
  #set y = 1 to just plot one variable and use word as the label
  ggplot(aes(as.factor(row), 1, label = input, fill = factor(topic) )) +
  #you want the words, not the points
  geom_point(color = "transparent") +
  #make sure the labels don't overlap
  geom_label_repel(nudge_x = .2,  
                   direction = "y",
                   box.padding = 0.1,
                   segment.color = "transparent",
                   size = 3) +
  facet_grid(~topic) +
  theme_lyrics() +
  theme(axis.text.y = element_blank(), axis.text.x = element_blank(),
        #axis.title.x = element_text(size = 9),
        panel.grid = element_blank(), panel.background = element_blank(),
        panel.border = element_rect("lightgray", fill = NA),
        strip.text.x = element_text(size = 9)) +
  labs(x = NULL, y = NULL, title = title) +
    #xlab(NULL) + ylab(NULL) +
  #ggtitle(title) +
  coord_flip()
}

Muziek en boeken

Om de kracht van topic modeling en classificatie echt te zien, gebruik je niet alleen Prince’ songteksten zoals in eerdere tutorials, maar werk je ook met verschillende andere artiesten uit uiteenlopende genres die op de Billboard Charts staan. Daarnaast heb ik een paar boeken toegevoegd over uiteenlopende onderwerpen, zoals sportvoeding, ijsbergen en zelfs machine learning! Met een gevarieerde dataset kun je documenten in bijbehorende topics classificeren en vergelijkbare artiesten/auteurs onderscheiden.

Tip: Ik verwijs naar nummers en boekpagina’s als documenten, en naar muzikanten/auteurs als schrijvers. In de code zie je de term source. Dit verwijst simpelweg naar welke schrijver de data betreft. Ik gebruik het woord genre zowel voor het muziekgenre als de categorie van een boek. In plaats van de namen van boekauteurs heb ik boektitels gebruikt, voor de duidelijkheid. Deze referenties worden logisch naarmate je vordert in de tutorial.

Haal de data op

Om te focussen op modelleren heb ik de dataconditionering buiten deze tutorial uitgevoerd en krijg je alle data die je nodig hebt in de drie onderstaande bestanden, met de volgende bewerking al toegepast:

  • de songteksten van acht artiesten van het web gescraped
  • de functie pdf_text() uit het pdftools-pakket gebruikt om de inhoud van vier boeken te verzamelen (elke pagina staat voor een apart document)
  • alle data opgeschoond, stopwoorden verwijderd en de tidy versies gemaakt met het pakket tidytext zoals beschreven in Deel Eén
  • de data gecombineerd en uitgebalanceerd zodat elke schrijver (source) hetzelfde aantal woorden heeft

Je hebt in deze tutorial drie aparte datasets om mee te werken:

  • three_sources_tidy_balanced: bevat Prince’ songteksten en twee boeken
  • all_sources_tidy_balanced: bevat songteksten van acht artiesten en vier boeken
  • prince_tidy: bevat alleen Prince’ songteksten

Ik heb ook een geannoteerde dataset van Prince’ songteksten toegevoegd. Ik leg uit wat geannoteerd betekent bij Model Vier.

 #Get Tidy Prince Dataset and Balanced Tidy Dataset of All Sources and 3 Sources

three_sources_tidy_balanced <- read.csv("three_sources_tidy_balanced.csv",
                                        stringsAsFactors = FALSE)

all_sources_tidy_balanced <- read.csv("all_sources_tidy_balanced.csv",
                                      stringsAsFactors = FALSE)

prince_tidy <- read.csv("prince_tidy.csv",
                        stringsAsFactors = FALSE)

Modellen bouwen met LDA

Het hoofddoel van topic modeling is het vinden van significante thematisch gerelateerde termen (topics) in ongestructureerde tekstdata, gemeten als patronen van samenvoorkomende woorden. De basisonderdelen van topicmodellen zijn documenten, termen en topics. Latent Dirichlet Allocation (LDA) is een unsupervised leermethode die verschillende topics ontdekt die ten grondslag liggen aan een verzameling documenten, waarbij elk document uit woorden bestaat. LDA gaat uit van het volgende:

  • Elk document is een combinatie van één of meer topic(s)
  • Elk topic is een mix van woorden

LDA probeert groepen gerelateerde woorden te vinden. Het is een iteratief, generatief algoritme. Dit zijn de twee hoofdtappen:

  • Tijdens de initialisatie wordt elk woord aan een willekeurig topic toegewezen
  • Het algoritme doorloopt elk woord iteratief en wijst het opnieuw toe aan een topic met de volgende overwegingen:
    • de kans dat het woord bij een topic hoort
    • de kans dat het document door een topic wordt gegenereerd

Het idee achter het LDA-topicmodel is dat woorden die tot een topic behoren samen in documenten voorkomen. Het probeert elk document te modelleren als een mix van topics en elk topic als een mix van woorden. (Je ziet dit weleens een mixed-membership-model genoemd.) Je kunt vervolgens de kans dat een document tot een bepaald topic behoort gebruiken om het dienovereenkomstig te classificeren. In jouw geval, omdat je de schrijver van het document kent uit de oorspronkelijke data, kun je dan een artiest/auteur aanbevelen op basis van vergelijkbare topicstructuren. Tijd voor een voorbeeld.

Model Eén: LDA voor drie schrijvers

Bekijk de data

Voor dit model gebruik je de dataset met een combinatie van drie verschillende schrijvers (d.w.z. sources) en zoek je naar verborgen topics. Om de kracht van topic modeling duidelijk te maken, zijn deze drie gekozen bronnen erg verschillend en weet je intuïtief dat ze drie verschillende onderwerpen bestrijken. Daarom geef je je model de opdracht om drie topics te maken en kijk je hoe slim het is. Eerst bekijk je je dataset met je functie my_kable_styling() samen met color_bar() en color_tile() uit het formattable-pakket.

 #group the dataset by writer (source) and count the words
three_sources_tidy_balanced %>%
  group_by(source) %>%
  mutate(word_count = n()) %>%
  select(source, genre, word_count) %>% #only need these fields
  distinct() %>%
  ungroup() %>%
  #assign color bar for word_count that varies according to size
  #create static color for source and genre
  mutate(word_count = color_bar("lightpink")(word_count),  
         source = color_tile("lightblue","lightblue")(source),
         genre = color_tile("lightgreen","lightgreen")(genre)) %>%
  my_kable_styling("Three Sources Stats")

Hier gebruik je Prince’ songteksten, een boek genaamd "Machine Learning For Dummies", en een ander boek genaamd "Why Icebergs Float", elk met meer dan 30.000 (niet-unieke) woorden.

statistieken drie bronnen

Maak de document-termmatrix

Omdat je nu een gebalanceerde, tidy dataset met Prince’ songteksten en twee boeken hebt, wil je eerst een document-termmatrix (DTM) maken waarin elk document een rij is en elke kolom een term. Dit formaat is vereist voor het LDA-algoritme. Een document verwijst naar een nummer bij de songteksten en een paginanummer bij de boeken. Eerst neem je je tidy data en tel je de woorden in een document, daarna pipe je dat naar de functie cast_dtm() van tidytext, waarbij document de naam is van het veld met de documentnaam en word de naam van het veld met de term. Er zijn andere parameters die je kunt meegeven, maar deze twee zijn vereist.

 three_sources_dtm_balanced <- three_sources_tidy_balanced %>%
  #get word count per document to pass to cast_dtm
  count(document, word, sort = TRUE) %>%
  ungroup() %>%
  #create a DTM with docs as rows and words as columns
  cast_dtm(document, word, n)
#examine the structure of the DTM
three_sources_dtm_balanced

<<DocumentTermMatrix (documents: 1449, terms: 13608)>>
Non-/sparse entries: 71386/19646606
Sparsity           : 100%
Maximal term length: 27
Weighting          : term frequency (tf)

Dit vertelt je hoeveel documenten en termen je hebt en dat dit een zeer sparse matrix is. Sparse betekent dat de DTM grotendeels lege velden bevat. Door de enorme woordenschat aan mogelijke termen uit alle documenten worden er in elk afzonderlijk document maar weinig gebruikt.

Als je naar de inhoud van een paar rijen en kolommen van de DTM kijkt met de functie inspect() uit het tm-pakket, zie je dat elke rij een document is en elke kolom een term. (Gebruik gerust str() om de daadwerkelijke structuur te zien.)

 #look at 4 documents and 8 words of the DTM
inspect(three_sources_dtm_balanced[1:4,1:8])
<<DocumentTermMatrix (documents: 4, terms: 8)>>
Non-/sparse entries: 9/23
Sparsity           : 72%
Maximal term length: 9
Weighting          : term frequency (tf)
Sample             :
            Terms
Docs         beautiful dance party push rock roll shake style
  party up           0     0    40    0    1    2     0     0
  push it up         0     1     1   56    0    0     0     0
  shake              0     0     1    0    0    0    52     0
  style              0     0     0    0    0    0     0    32

Variabelen instellen

Nu je weet wat er als input nodig is om je model te bouwen, stel je twee variabelen in die je eenvoudig kunt resetten bij het construeren van elk model. Je noemt ze source_dtm en source_tidy.

 #assign the source dataset to generic var names
#so we can use a generic function per model
source_dtm <- three_sources_dtm_balanced
source_tidy <- three_sources_tidy_balanced

Model fitten

Eindelijk kom je bij het leuke deel: het model fitten. De code is simpel, maar om een goede Data Scientist te zijn, wil je begrijpen wat er onder de motorkap gebeurt. Hoewel dit een beknopte beschrijving is, is er online een schat aan informatie over LDA. Voor jouw doel hoef je slechts vier parameters te kennen: de input, het aantal topics, de steekproefmethode en de seed.

Omdat je weet dat je dataset drie verschillende schrijvers bevat, stel je k in op 3 en seed op een constante, zodat je reproduceerbare resultaten krijgt. Er zijn andere control-parameters, vandaar het list-formaat in de code, maar je gebruikt hier alleen seed. De parameter method definieert het te gebruiken steekproefalgoritme. Je gebruikt de GIBBS-steekproefmethode. De default is VEM, maar naar mijn ervaring presteert die minder goed dan de alternatieve GIBBS-methode.

Een korte technische uitleg van GIBBS-sampling: het voert een random walk uit die begint op een punt dat je kunt initialiseren (je gebruikt hier de default 0) en bij elke stap plus of min één beweegt met gelijke kans. Lees hier een gedetailleerde beschrijving als je de details wilt.

Let op: Ik ben over de details van het LDA-algoritme heen gestapt. Hoewel je de statistische theorie niet hoeft te begrijpen om het te gebruiken, raad ik aan om dit document van Ethen Liu te lezen, dat een solide uitleg biedt.

 k <- 3 #number of topics
seed = 1234 #necessary for reproducibility
#fit the model passing the parameters discussed above
#you could have more control parameters but will just use seed here
lda <- LDA(source_dtm, k = k, method = "GIBBS", control = list(seed = seed))
#examine the class of the LDA object
class(lda)
[1] "LDA_Gibbs"
attr(,"package")
[1] "topicmodels"

Je ziet dat dit een LDA_Gibbs-object met drie topics heeft gemaakt. Ik heb hier geen str(lda) uitgevoerd vanwege de grote output, maar doe dat vooral zelf om te zien wat het bevat.

De volgende stap is om dat object te openen en te kijken wat de resultaten zijn. Je kunt de functie tidy() gebruiken om het in een gemakkelijk te begrijpen formaat te zetten. Geef binnen de aanroep van tidy() het LDA-object en beta door voor het argument matrix. Met beta krijg je de per-topic-per-woordkansen uit het model. Het volgende voorbeeld, met de term iceberg, laat de kans zien dat dat woord in elk topic zit. Het woord iceberg heeft een hogere kans om in Topic 1 te worden gegenereerd.

 #convert the LDA object into a tidy format
#passing "beta" shows the word probabilities
#filter on the word iceberg as an example
#results show probability of iceberg for each topic
tidy(lda, matrix = "beta") %>% filter(term == "iceberg")

[# A tibble: 3 x 3 topic term beta 1 1 iceberg 0.000198
2 2 iceberg 0.00000292 3 3 iceberg 0.00000315]

Identificeer thema’s met topwoorden

Om het model goed te begrijpen, moet je zien welke termen in elk topic zitten. Bouw een functie die het LDA-model tidy maakt en de toptermen per topic extraheert. Met andere woorden: de termen per topic met de grootste beta-waarden. Binnen deze functie verwijs je naar word_chart(), dat je eerder hebt gebouwd, om een nette weergave te genereren van de drie topics en de topwoorden. Stel num_words in op 10 voor dit voorbeeld, maar speel vooral met dat aantal voor andere perspectieven op de topics.

 num_words <- 10 #number of words to visualize

#create function that accepts the lda model and num word to display
top_terms_per_topic <- function(lda_model, num_words) {

  #tidy LDA object to get word, topic, and probability (beta)
  topics_tidy <- tidy(lda_model, matrix = "beta")


  top_terms <- topics_tidy %>%
  group_by(topic) %>%
  arrange(topic, desc(beta)) %>%
  #get the top num_words PER topic
  slice(seq_len(num_words)) %>%
  arrange(topic, beta) %>%
  #row is required for the word_chart() function
  mutate(row = row_number()) %>%
  ungroup() %>%
  #add the word Topic to the topic labels
  mutate(topic = paste("Topic", topic, sep = " "))
  #create a title to pass to word_chart
  title <- paste("LDA Top Terms for", k, "Topics")
  #call the word_chart function you built in prep work
  word_chart(top_terms, top_terms$term, title)
}
#call the function you just built!
top_terms_per_topic(lda, num_words)
lda toptermen voor drie topics

Unsupervised learning vereist altijd menselijke interpretatie... maar deze topics weerspiegelen duidelijk de drie bronnen: Icebergs, Machine Learning en Prince! Ook al is dit een heel eenvoudig voorbeeld, het is behoorlijk indrukwekkend als je aan de mogelijkheden denkt!

Classificeer documenten

Naast het schatten van elk topic als een mix van woorden, laat LDA ook elk document zien als een mix van topics. Wanneer je het LDA-model tidy maakt, gebruik nu matrix = "gamma" en je krijgt de per-document-per-topickansen. Kijk bijvoorbeeld naar Prince’ nummer 1999. Net zoals een woord met meerdere topics geassocieerd is, geldt dat ook voor documenten. Dit verwijst terug naar het mixed-membership-concept. Hieronder zie je dat 1999 het meest waarschijnlijk in Topic 3 voorkomt.

 #this time use gamma to look at the prob a doc is in a topic
#just look at the Prince song 1999 as an example
tidy(lda, matrix = "gamma") %>% filter(document == "1999")
# A tibble: 3 x 3
  document topic gamma
  <chr>    <int> <dbl>
1 1999         1 0.170
2 1999         2 0.170
3 1999         3 0.661

Dit nummer hoort bij elk topic, maar in een andere verhouding, weergegeven door de gamma-waarde. Sommige documenten passen beter in bepaalde topics dan andere. Zoals hierboven te zien, heeft 1999 een hogere gamma-waarde voor Topic 3.

Chord-diagram

Om je begrip van wat LDA doet te verduidelijken, bekijk je deze cirkelplot. Deze plot toont welk percentage documenten van elke bron tot elk topic behoort. Om deze te maken, voeg je het tidy LDA-model samen met source_tidy, waar je de daadwerkelijke bron (d.w.z. de schrijver) hebt opgeslagen, en je voegt samen op document. Als je vervolgens correct groepeert, kun je de gemiddelde gamma-waarde per bron, per topic krijgen. Nu zie je voor elke bron in welk topic de meeste documenten vallen.

Neem even de tijd om dit echt te bekijken als je niet eerder met cirkelplots hebt gewerkt. (Bekijk Deel Twee-A voor oefening met chordDiagrams() en het circlize-pakket. Alles wat je ooit over het pakket moet weten, staat in dit boek van Zuguang Gu.)

 #using tidy with gamma gets document probabilities into topic
#but you only have document, topic and gamma
source_topic_relationship <- tidy(lda, matrix = "gamma") %>%
  #join to orig tidy data by doc to get the source field
  inner_join(three_sources_tidy_balanced, by = "document") %>%
  select(source, topic, gamma) %>%
  group_by(source, topic) %>%
  #get the avg doc gamma value per source/topic
  mutate(mean = mean(gamma)) %>%
  #remove the gamma value as you only need the mean
  select(-gamma) %>%
  #removing gamma created duplicates so remove them
  distinct()

#relabel topics to include the word Topic
source_topic_relationship$topic = paste("Topic", source_topic_relationship$topic, sep = " ")

circos.clear() #very important! Reset the circular layout parameters
#assign colors to the outside bars around the circle
grid.col = c("prince" = my_colors[1],
             "icebergs" = my_colors[2],
             "machine_learning" = my_colors[3],
             "Topic 1" = "grey", "Topic 2" = "grey", "Topic 3" = "grey")

# set the global parameters for the circular layout. Specifically the gap size (15)
#this also determines that topic goes on top half and source on bottom half
circos.par(gap.after = c(rep(5, length(unique(source_topic_relationship[[1]])) - 1), 15,
                         rep(5, length(unique(source_topic_relationship[[2]])) - 1), 15))
#main function that draws the diagram. transparancy goes from 0-1
chordDiagram(source_topic_relationship, grid.col = grid.col, transparency = .2)
title("Relationship Between Topic and Source")
relatie tussen topic en bron

Dit is een heel interessante manier om te zien dat, hoewel de documenten van elke bron in elk topic voorkomen, elke bron duidelijk een meerderheid in een specifiek topic heeft. Bijvoorbeeld: Prince-nummers worden vooral geassocieerd met Topic 3, aangezien dat koord groter is dan de koorden naar topics één en twee. Hetzelfde geldt voor Icebergs en Topic 1, en Machine Learning en Topic 2. Zodra je dit patroon ziet, wordt het heel duidelijk wat er gebeurt.

Als je bedenkt wat je zojuist hebt gedaan, heb je documenten eigenlijk in Topics geclassificeerd op basis van het gemiddelde van gamma voor een topic/bron. Als je de schrijver van een document kent, en je weet dat die schrijver de meeste lyrics of boeken over een specifiek topic schrijft, zou je kunnen aannemen dat artiesten van vergelijkbare genres in dezelfde topics vallen en je dus vergelijkbare schrijvers kunt aanbevelen... Bij de grotere dataset ga je dit in de praktijk brengen.

Topdocumenten per topic

Bekijk nu op individueel documentniveau de topdocumenten per topic.

 number_of_documents = 5 #number of top docs to view
title <- paste("LDA Top Documents for", k, "Topics")

#create tidy form showing topic, document and its gamma value
topics_tidy <- tidy(lda, matrix = "gamma")

#same process as used with the top words
top_documents <- topics_tidy %>%
  group_by(topic) %>%
  arrange(topic, desc(gamma)) %>%
  slice(seq_len(number_of_documents)) %>%
  arrange(topic, gamma) %>%
  mutate(row = row_number()) %>%
  ungroup() %>%
  #re-label topics
  mutate(topic = paste("Topic", topic, sep = " "))

title <- paste("LDA Top Documents for", k, "Topics")
word_chart(top_documents, top_documents$document, title)
lda topdocumenten voor drie topics

Identificeer artiesten/auteurs

Omdat je de naamgeving van de document-/songtitels misschien niet kent (d.w.z. pagina’s met een underscore komen uit het machinelearning-boek), vervang ik de documentnaam door de bronnaam.

 title <- paste("Sources for Top Documents for", k, "Topics")

topics_tidy <- tidy(lda, matrix = "gamma")

top_sources <- top_documents %>%
  #join back to the tidy form to get the source field
  inner_join(source_tidy) %>%
  select(document, source, topic) %>%
  distinct() %>%
  group_by(topic) %>%
  #needed by word_chart (not relevant here)
  mutate(row = row_number()) %>%
  ungroup()

word_chart(top_sources, top_sources$source, title)
bronnen voor topdocumenten voor drie topics

De topdocumenten per topic zoals in de vorige grafiek zijn hier vervangen door hun bron. Je code heeft deze documenten goed geclassificeerd, want ze vielen in dezelfde categorie (in elk geval voor deze top 5)! De grote vraag is of dit werkt met meer dan drie bronnen. Als je resultaten accuraat zijn, kun je vervolgens schrijvers met vergelijkbare topics aanbevelen.

Model Twee: K-Means voor drie schrijvers

Je maakt je tweede model met een andere machinelearningtechniek: het k-means-algoritme. Net als LDA is k-means een unsupervised leeralgoritme en moet je het aantal topics vooraf bepalen. In tegenstelling tot het LDA mixed-membership-model, deelt k-means de documenten op in schijnbaar disjuncte clusters (d.w.z. topics). Het algoritme clustert documenten in verschillende groepen op basis van een gelijkenismaat. Het zet documenten om in een numerieke vector met gewichten per woord per document (vergelijkbaar met tf-idf). Elk document komt precies in één cluster terecht. Dit heet harde clustering. De output is een set clusters met hun documenten. LDA is daarentegen een fuzzy (soft) clusteringtechniek waarbij een datapunt tot meer dan één cluster kan behoren.

Conceptueel zou LDA, toegepast op natuurlijke taal, realistischer resultaten moeten geven dan k-means voor topictoewijzing, omdat tekst meestal meer dan één onderwerp omvat. Bouw nu je model, bekijk het k-means-object en kijk of je aanname klopt.

Variabelen instellen en model fitten

 #use the same three sources you started with
source_dtm <- three_sources_dtm_balanced
source_tidy <- three_sources_tidy_balanced

#Set a seed for replicable results
set.seed(1234)
k <- 3
kmeansResult <- kmeans(source_dtm, k)
str(kmeansResult)
List of 9
 $ cluster     : Named int [1:1449] 1 3 2 2 3 1 3 2 2 3 ...
  ..- attr(*, "names")= chr [1:1449] "push it up" "shake" "party up" "style" ...
 $ centers     : num [1:3, 1:13608] 42 0.01065 0.00758 0 0.01141 ...
  ..- attr(*, "dimnames")=List of 2
  .. ..$ : chr [1:3] "1" "2" "3"
  .. ..$ : chr [1:13608] "push" "shake" "party" "style" ...
 $ totss       : num 237535
 $ withinss    : num [1:3] 640 202211 28285
 $ tot.withinss: num 231137
 $ betweenss   : num 6398
 $ size        : int [1:3] 2 1315 132
 $ iter        : int 3
 $ ifault      : int 0
 - attr(*, "class")= chr "kmeans"

De structuur van het k-means-object onthult twee belangrijke informatiebronnen: clusters en centers. Je kunt onderstaand format gebruiken om de inhoud van elke variabele te zien voor het nummer 1999 en het woord party dat in dat nummer voorkomt.

 head(kmeansResult$cluster["1999"])
1999
   2
head(kmeansResult$centers[,"party"])
1         2         3
0.5000000 0.1269962 0.1439394

Deze velden tonen dat 1999 in één cluster is geplaatst (cluster twee), en party in alle drie de clusters voorkomt, maar vooral in cluster één. Bekijk nu de topwoorden op basis van de centers-waarden om thema’s te identificeren.

Identificeer k-means-thema’s met topwoorden

Onthoud dat deze dataset drie bronnen bevat: Prince, Icebergs en Machine Learning. Print de topwoorden per cluster en vergelijk je resultaten met de LDA-output. Je moet het dataformaat een beetje manipuleren om je functie word_chart() te kunnen gebruiken. Je kunt de code volgen, maar de details zijn hier minder belangrijk dan de eigenlijke analyse van de topwoorden.

 num_words <- 8 #number of words to display
#get the top words from the kmeans centers
kmeans_topics <- lapply(1:k, function(i) {
  s <- sort(kmeansResult$centers[i, ], decreasing = T)
  names(s)[1:num_words]
})

#make sure it's a data frame
kmeans_topics_df <- as.data.frame(kmeans_topics)
#label the topics with the word Topic
names(kmeans_topics_df) <- paste("Topic", seq(1:k), sep = " ")
#create a sequential row id to use with gather()
kmeans_topics_df <- cbind(id = rownames(kmeans_topics_df),
                          kmeans_topics_df)
#transpose it into the format required for word_chart()
kmeans_top_terms <- kmeans_topics_df %>% gather(id, 1:k)
colnames(kmeans_top_terms) = c("topic", "term")

kmeans_top_terms <- kmeans_top_terms %>%
  group_by(topic) %>%
  mutate(row = row_number()) %>% #needed by word_chart()
  ungroup()

title <- paste("K-Means Top Terms for", k, "Topics")
word_chart(kmeans_top_terms, kmeans_top_terms$term, title)
k-means toptermen voor drie topics

Het k-means-algoritme is slim genoeg om thema’s tussen boeken en muziek te scheiden. Met andere woorden: Topics 1 en 3 zijn duidelijk Prince-thema’s, en Topic 2 is een combinatie van de twee boeken. Het was echter niet in staat de twee boeken onderling te onderscheiden, zoals je met LDA wel kon. K-means is te tunen voor betere prestaties, maar dat geldt ook voor LDA. Op het eerste gezicht, met standaardparameters, lijkt k-means minder goed te presteren dan LDA, en harde clustering wordt in feite meestal minder gebruikt voor topic modeling. (Voor een andere optie, bekijk het mallet-pakket.)

Model Drie: LDA voor 12 schrijvers

Voor dit model gebruik je het LDA-algoritme op de dataset met 12 verschillende bronnen/schrijvers uit meerdere genres. De dataset is al gebalanceerd met een vergelijkbaar aantal documenten en woorden per schrijver. Hier is een overzicht van de data:

Bekijk de data

 all_sources_tidy_balanced %>%
  group_by(source) %>%
  #get the word count and doc count per source
  mutate(word_count = n(),
         source_document_count = n_distinct(document)) %>%
  select(source, genre, word_count, source_document_count) %>%
  distinct() %>%
  ungroup() %>%
  #bars change size according to number
  #tiles are static sizes
  mutate(word_count = color_bar("lightpink")(word_count),
         source_document_count = color_bar("lightpink")(source_document_count),
         source = color_tile("lightblue","lightblue")(source),
         genre = color_tile("lightgreen","lightgreen")(genre)) %>%
  my_kable_styling("All Sources Stats")
statistieken alle bronnen

Deze dataset bevat een grote variatie aan artiesten, van Prince, tot Johnny Cash (country), tot Eminem (rap), evenals vier verschillende boeken. Deze artiesten zijn gekozen omdat ze als meest productief in hun respectieve genres werden gerangschikt. De inhoud is van een songtekstwebsite gescraped. Hoewel er twee machinelearningboeken zijn, richt de ene zich op algemene data science en de andere meer op machine learning specifiek voor R-programmering. Alle boeken zijn beschikbaar als downloadbare pdf’s. (De ruwe pdf’s zijn niet nodig voor deze analyse omdat de tidy versies zijn aangeleverd).

Even een kleine opschoningsstap: sommige genreomschrijvingen zijn te lang voor onze grafieken, dus maak verkorte vormen om ze leesbaarder te maken.

 all_sources_tidy_balanced <- all_sources_tidy_balanced %>%
  mutate(source = ifelse(source == "machine_learning", "m_learn",
         ifelse(source == "machine_learning_r", "m_learn_r",
         ifelse(source == "michael_jackson", "mi_jackson",
         ifelse(source == "sports_nutrition", "nutrition", source))))) %>%
  mutate(genre = ifelse(genre == "machine_learning", "m_learn",
                 ifelse(genre == "sports_nutrition", "nutrition", genre)))

Maak de DTM en stel de variabelen in

 #this time use the dataset with 12 sources
all_sources_dtm_balanced <- all_sources_tidy_balanced %>%
  count(document, word, sort = TRUE) %>%
  ungroup() %>%
  cast_dtm(document, word, n)

source_dtm <- all_sources_dtm_balanced
source_tidy <- all_sources_tidy_balanced

Model fitten en thema’s identificeren

Deze keer heb je twaalf schrijvers en acht genres. Ik hoop dat schrijvers uit hetzelfde genre in hetzelfde topic worden gegroepeerd. Dat is de echte test. Stel dus het aantal topics in op acht en fit het model opnieuw met deze grotere dataset.

 k <- 8 #number of topics chosen to match the number of genres
num_words <- 10 #number of words we want to see in each topic
seed = 1234 #make it repeatable
#same as before
lda <- LDA(source_dtm, k = k, method = "GIBBS", control = list(seed = seed))

top_terms_per_topic(lda, num_words)
lda toptermen voor acht topics

Ik wil je mening niet beïnvloeden, maar mijn reactie is: "Wow"! Gegeven de input voor dit model, zie je de thema’s? Kijk naar elk topic en probeer ze te matchen met de acht bekende genres. Sommige zijn lastig te onderscheiden, maar andere vrij duidelijk. Enkele mogelijke interpretaties:

  • Topic Eén - machine learning en data science
  • Topic Twee - pop/rock
  • Topic Drie - sportvoeding
  • Topic Vier - country
  • Topic Vijf - hiphop-rap
  • Topic Zes - country of pop-rock
  • Topic Zeven - ijsbergen, aardwetenschappen
  • Topic Acht - religieus

Vergeet het mixed-membership-concept niet en dat deze topics niet bedoeld zijn om volledig disjunct te zijn, maar ik was verrast om de resultaten zo discreet te zien als hier. Terzijde: het LDA-algoritme kent meer parameters dan je hier hebt gebruikt en je kunt je model dus nog sterker tunen.

Het volgende deel is echt spannend. Hoe maak je duidelijker waar deze topics over gaan? In veel gevallen is dat lastig te interpreteren, maar met de juiste dataset kan je model goed presteren. Als je model kan bepalen welke documenten het waarschijnlijkst in elk topic vallen, kun je schrijvers gaan groeperen. En omdat je in jouw geval het genre van een schrijver kent, kun je je resultaten tot op zekere hoogte valideren! De eerste stap is het classificeren van elk document.

Classificeer documenten

Chord-diagram

Ter herinnering: elk document bevat meerdere topics, maar in verschillende verhoudingen, weergegeven door de gamma-waarde. Sommige documenten passen beter in bepaalde topics dan andere. Deze plot toont de gemiddelde gamma-waarde van documenten van elke bron voor elk topic. Dit is dezelfde aanpak als hierboven met drie schrijvers, behalve dat je nu naar het genrefeld kijkt. Het is een fantastische manier om de relatie tussen het genre van de schrijvers en de topics te tonen.

 source_topic_relationship <- tidy(lda, matrix = "gamma") %>%
  #join to the tidy form to get the genre field
  inner_join(source_tidy, by = "document") %>%
  select(genre, topic, gamma) %>%
  group_by(genre, topic) %>%
  #avg gamma (document) probability per genre/topic
  mutate(mean = mean(gamma)) %>%
  select(genre, topic, mean) %>%
  ungroup() %>%
  #re-label topics
  mutate(topic = paste("Topic", topic, sep = " ")) %>%
  distinct()

circos.clear() #very important! Reset the circular layout parameters
#this is the long form of grid.col just to show you what I'm doing
#you can also assign the genre names individual colors as well
grid.col = c("Topic 1" = "grey", "Topic 2" = "grey", "Topic 3" = "grey",
             "Topic 4" = "grey", "Topic 5" = "grey", "Topic 6" = "grey",
             "Topic 7" = "grey", "Topic 8" = "grey")

#set the gap size between top and bottom halves set gap size to 15
circos.par(gap.after = c(rep(5, length(unique(source_topic_relationship[[1]])) - 1), 15,
                         rep(5, length(unique(source_topic_relationship[[2]])) - 1), 15))
chordDiagram(source_topic_relationship,  grid.col = grid.col, annotationTrack = "grid",
             preAllocateTracks = list(track.height = max(strwidth(unlist(dimnames(source_topic_relationship))))))
#go back to the first track and customize sector labels
#use niceFacing to pivot the label names to be perpendicular
circos.track(track.index = 1, panel.fun = function(x, y) {
  circos.text(CELL_META$xcenter, CELL_META$ylim[1], CELL_META$sector.index,
              facing = "clockwise", niceFacing = TRUE, adj = c(0, 0.5))
}, bg.border = NA) # here set bg.border to NA is important
title("Relationship Between Topic and Genre")
relatie tussen topic en genre

Elk koord (d.w.z. link) staat voor de gemiddelde gammawaarde van documenten per genre die tot een topic behoren. Kijk je naar het gedeelte sports_nutrition, dan zie je dat het koord naar Topic 3 groter is dan de andere koorden. Dit betekent dat meer pagina’s over sportvoeding in dat topic vallen. Duidelijk is dat de meerderheid van Machine Learning- en Data Science-pagina’s in Topic 1 valt; de meerderheid van Aardwetenschappenteksten in Topic 7, en zoals je zou verwachten is Pop-Rock redelijk verdeeld over meerdere topics. Dat is logisch, want Pop staat per definitie voor populair en is een algemeen genre. Rap-Hip-Hop daarentegen komt vaker voor in Topic 5. Wat kun je met deze informatie? Je kunt luisteraars aanbevelingen doen op basis van thematische inhoud!

Natuurlijk hoeft songtekstinhoud niet de enige feature te zijn die aanbevelingssystemen gebruiken; die kan worden gecombineerd met metadata zoals ritme, instrumentatie, tempo, enz. voor een betere luisterervaring.

Aanbevelen van vergelijkbare schrijvers

Nu je de relatie tussen documenten en topics kunt zien, groepeer je op source (d.w.z. schrijver) en topic en neem je de som van de gamma-waarden per groep. Selecteer vervolgens de schrijver met de hoogste topic_sum voor elk topic met top_n(1). Omdat je hetzelfde wilt doen voor genre als wat je hier met writer doet, maak je een functie genaamd top_items_per_topic() en geef je source door als type. Zo kun je hem later nogmaals aanroepen wanneer je documenten op genre classificeert.

Dit is het kritieke moment waarop je de schrijver daadwerkelijk aan een specifiek topic koppelt. Wat verwacht je dat er gebeurt?

 #this function can be used to show genre and source via passing the "type"
top_items_per_topic <- function(lda_model, source_tidy, type) {
  #get the tidy version by passing gamma for the per document per topic probs
  document_lda_gamma <- tidy(lda_model, matrix = "gamma") %>%
  #join to the tidy form to get source and genre
  inner_join(source_tidy) %>%
  select(document, gamma, source, genre, topic) %>%
  distinct() %>% #remove duplicates
  #group so that you can get sum per topic/source
  group_by(source, topic) %>%
  #sort by decending gamma value
  arrange(desc(gamma)) %>%
  #create the sum of all document gamma vals per topic/source. Important!
  mutate(topic_sum = sum(gamma)) %>%
  select(topic, topic_sum, source, genre) %>%
  distinct() %>%
  ungroup() %>%
  #type will be either source or genre
  group_by(source, genre ) %>%
  #get the highest topic_sum per type
  top_n(1, topic_sum) %>%
  mutate(row = row_number()) %>%
  mutate(label = ifelse(type == "source", source, genre),
         title = ifelse(type == "source", "Recommended Writers Per Topic",
                        "Genres Per Topic")) %>%
  ungroup() %>%
   #re-label topics
  mutate(topic = paste("Topic", topic, sep = " ")) %>%
  select(label, topic, title)

#slightly different format from word_chart input, so use this version
document_lda_gamma %>%
#use 1, 1, and label to use words without numeric values
ggplot(aes(1, 1, label = label, fill = factor(topic) )) +
  #you want the words, not the points
  geom_point(color = "transparent") +
  #make sure the labels don't overlap
  geom_label_repel(nudge_x = .2,
                   direction = "y",
                   box.padding = 0.1,
                   segment.color = "transparent",
                   size = 3) +
  facet_grid(~topic) +
  theme_lyrics() +
  theme(axis.text.y = element_blank(), axis.text.x = element_blank(),
        axis.title.y = element_text(size = 4),
        panel.grid = element_blank(), panel.background = element_blank(),
        panel.border = element_rect("lightgray", fill = NA),
        strip.text.x = element_text(size = 9)) +
  xlab(NULL) + ylab(NULL) +
  ggtitle(document_lda_gamma$title) +
  coord_flip()
}

top_items_per_topic(lda, source_tidy, "source")
aanbevolen schrijvers per topic

Dit is gaaf! Maar omdat je deze artiesten misschien niet goed kent, maak ik het duidelijker door de schrijver te vervangen door het genre. Maak je klaar...

Vergelijkbare documenten aanbevelen per genre

 top_items_per_topic(lda, source_tidy, "genre")
genres per topic

De hiphop-rapartiesten zijn gegroepeerd; de christelijke artiesten staan bij elkaar; twee van de pop-rockartiesten staan bij elkaar, en elk boek heeft zijn eigen topic! Het is best bijzonder om te zien dat unsupervised learning dit soort resultaten kan opleveren (het herkennen van verschillende genres) puur op basis van tekst. Wat zou er gebeuren als je een ander aantal topics zou kiezen? Welke genres zouden worden gecombineerd of gescheiden? Misschien kun je dat zelf eens proberen. Voor nu richt je volgende model zich op de Prince-dataset die je in eerdere tutorials gebruikte.

Model Vier: LDA voor Prince-nummers

Voor dit model gebruik je je dataset met alleen Prince-nummers. Hoewel songteksten duidelijke onderwerpen bevatten, is het vaak zo dat één nummer meer dan één onderwerp behandelt. Als je dataset maar één artiest bevat, zullen je topics meer gemengd en minder disjunct zijn. Dat maakt interpretatie een heel cruciale stap. In plaats van de ruwe data uit Prince’ songteksten te gebruiken, gebruik je een iets aangepaste versie zoals hieronder beschreven.

Gebruik NLP om het model te verbeteren

Eerder heb je woorden gebruikt zoals ze in de tekst voorkomen. Nu gebruik je een geannoteerde vorm van de data, afkomstig van een krachtig NLP-pakket genaamd cleanNLP. Dit pakket is een tidy datamodel voor Natural Language Processing dat annotatietaken biedt zoals tokenization, part-of-speech-tagging, named entity recognition, entity linking, sentimentanalyse en nog veel meer. Deze exercitie is buiten deze tutorial uitgevoerd, maar ik heb alles wat je nodig hebt voor topic modeling aangeleverd.

Bekijk de NLP-output

In de geannoteerde dataset is er één rij voor elk woord in het corpus van Prince’ songteksten met belangrijke informatie over elk woord.

Kijk eens dichter naar het geannoteerde tokenobject door de veldnamen te bekijken.

 #read in the provided annotated dataset
prince_annotated <- read.csv("prince_data_annotated.csv")
#look at the fields provided in the dataset
names(prince_annotated)
[1] "X"         "id"        "sid"       "tid"       "word"     
[6] "lemma"     "upos"      "pos"       "cid"       "pid"      
[11] "case"      "definite"  "degree"    "foreign"   "gender"   
[16] "mood"      "num_type"  "number"    "person"    "poss"     
[21] "pron_type" "reflex"    "tense"     "verb_form" "voice"

Voor nu gebruik je slechts twee informatie-elementen:

  • lemma: de gelemmatiseerde woordvorm (d.w.z. de gereduceerde versie van een verbogen woord naar zijn basisvorm)
  • upos: de universele woordsoort van elk woord

Het veld word is het originele woord en lemma is de gelemmatiseerde versie. upos is de universele part-of-speech-code. Als je table() aanroept op upos, zie je de telling van woorden die in elke getagde woordsoort vallen. (Merk op dat sommige woorden meerdere keren getagd kunnen zijn)

 table(prince_annotated$upos)
ADJ   ADP   ADV   AUX CCONJ   DET  INTJ  NOUN   NUM  PART  PRON PROPN
15193 19157 20439 19855  5601 20777  5399 48047  5439 10087 50932   523
SCONJ   SYM  VERB     X
5626    72 43673  3095

Om je een idee te geven van hoe de data eruitziet, zoom je in op een paar nummers waarbij de lemma niet hetzelfde is als het originele woord, en verwijder je de stopwoorden.

 prince_annotated %>%
  #most lemmas are the same as the raw word so ignore those
  filter((as.character(word) != as.character(lemma))
         & (id %in% c("broken", "1999"))) %>% #filter on 2 songs
  anti_join(stop_words) %>%
  select(song = id, word, lemma, upos) %>%
  distinct() %>%
  my_kable_styling("Annotated Subset")
geannoteerde subset

Dit laat zien hoe selectief je kunt zijn in wat je in je modellen stopt (d.w.z. het woord, de lemma, of een bepaalde woordsoort). Het is echt een afweging of je de gelemmatiseerde vorm of het oorspronkelijke woord gebruikt. Of misschien wil je zelfs een andere vorm van het woord gebruiken, de stam, die werd genoemd in Deel Twee-A. Je kunt ook vraagtekens zetten bij de resultaten van cleanNLP en wat als zelfstandig naamwoord versus bijvoeglijk naamwoord wordt bepaald. Ik raad aan om met verschillende configuraties te spelen totdat je vindt wat voor jou het beste werkt.

Voor deze tutorial modelleer je alleen de zelfstandige naamwoorden. Om de bijbehorende Prince-metadata zoals genre en jaar te krijgen, voeg je prince_annotated samen met prince_tidy op word en document, en maak je vervolgens de DTM zoals gebruikelijk. Ik heb ervoor gekozen om enkele zeer algemene woorden die in alle topics voorkomen te verwijderen, puur om het interessant te houden. (Merk op dat het tf-idf-concept uit Deel Eén een andere methode is die je kunt overwegen om algemene woorden te verwijderen.)

Variabelen instellen met NLP

 source_tidy <- prince_annotated %>%
  select(document = id, word, lemma, upos) %>%
  filter(upos == "NOUN") %>% #choose only the nouns
  inner_join(prince_tidy, by = c("word", "document")) %>%
  select(document, word, lemma, upos, source, genre, year) %>%
  distinct()

source_dtm <- source_tidy %>%
  #filter out some words that exist across themes just for our purposes
  filter(!word %in% c("love", "time", "day", "night", "girl")) %>%
  count(document, word, sort = TRUE) %>%
  ungroup() %>%
  cast_dtm(document, word, n)

Model fitten en thema’s identificeren

 #Changing these parameters or the source data will cause different results!!
k <- 7
num_words <- 6
seed = 4321
lda <- LDA(source_dtm, k = k, method = "GIBBS",

control = list(seed = seed))

top_terms_per_topic(lda, num_words)
lda toptermen voor zeven topics

Hieronder staan thema’s die mensen door de jaren heen als mogelijke thema’s in Prince’ muziek hebben gelabeld. Ze zijn getagd met bepaalde woorden, en zoals bij alle topic modeling is er overlap. Maar kijk naar deze handmatig getagde thema’s (in willekeurige volgorde!) en kijk of je ze terugvindt in het model dat je hebt gebouwd.

  • love, time, life
  • people, family
  • war, peace, controversy, freedom
  • pain, sad, loss
  • sex, drugs
  • self, society, religion
  • music, dance, party

Het lijkt erop dat jouw Topic 1 over mensen en familie gaat; Topic 7 over muziek, dans en feest; en Topic 4 over zelf, maatschappij en religie. Dit vereist een zeer subjectieve interpretatie. In een zakelijke context wordt deze stap het best uitgevoerd door een materiedeskundige die een idee heeft wat te verwachten. Nu zie je waarom NLP echt de kruising is van kunstmatige intelligentie en computationele taalkunde!

Probeer dit eens met een ander aantal topics, of alleen met werkwoorden, of met het ruwe woord versus de gelemmatiseerde vorm, of met een ander aantal topwoorden en kijk welke inzichten je krijgt. Er zijn nieuwe methoden om het aantal topics te bepalen: zie het concept perplexity hier, of het ldatuning-pakket hier.

Thema’s in de tijd

Aangezien de Prince-dataset year bevat, kun je thema’s in de tijd volgen. Je kunt hier erg creatief zijn, maar bekijk nu een paar woorden uit twee topics en bekijk de frequentie in de tijd. Trending kan een complete tutorial op zich zijn; als je echt geïnteresseerd bent, bekijk dan deze bron.

 p1 <-  prince_tidy %>%
  filter(!year == "NA") %>% #remove songs without years
  filter(word %in% c("music", "party", "dance")) %>%
  group_by(year) %>%
  mutate(topic_7_count = n()) %>%
  select(year, topic_7_count) %>%
  distinct() %>%
  ggplot(aes(year, topic_7_count)) + geom_smooth(se = FALSE, col = "red")

p2 <-  prince_tidy %>%
  filter(!year == "NA") %>% #remove songs without years
  filter(word %in% c("heaven","hand","soul")) %>%
  group_by(year) %>%
  mutate(topic_4_count = n()) %>%
  select(year, topic_4_count) %>%
  distinct() %>%
  ggplot(aes(year, topic_4_count)) + geom_smooth(se = FALSE)

grid.arrange(p1, p2, ncol = 2)
lijngrafieken

Dit is een uiterst rudimentair voorbeeld van hoe thema’s in de tijd veranderen, maar wat als je dit soort analyse buiten muziek zou uitbreiden? Misschien is je bedrijf geïnteresseerd in de correlatie tussen productverkopen en trending thema’s in klantentweets over je product. Er zijn veel toepassingen om te begrijpen hoe topics in de tijd veranderen.

Conclusie

Deze tutorial raakt slechts het oppervlak van wat mogelijk is met topic modeling, NLP en machine learning. Dit is een bloeiend gebied binnen data science en zal blijven groeien naarmate meer wetenschappers de onbegane oceanen van ongestructureerde data verkennen.

Je gebruikte data uit songteksten en non-fictieboeken om thema’s te identificeren, afzonderlijke documenten in de meest waarschijnlijke topics te classificeren en vergelijkbare schrijvers te identificeren op basis van thematische inhoud. Zo kon je een eenvoudig aanbevelingssysteem bouwen op basis van tekst en gelabelde schrijvers.

Onthoud: je probeert kunst met machines te modelleren. Een artiest kan zeggen: "You are the apple of my eye", maar daarmee doelen ze niet op fruit of lichaamsdelen. Je moet vinden wat verborgen is. Als je naar muziek luistert, hoor je niet altijd de achtergrondzang, maar die is er wel. Zet je favoriete nummer harder en luister voorbij de leadzang; je zult verrast zijn door een wereld aan verborgen informatie. Zet logica op de achtergrond en breng creativiteit naar de voorgrond tijdens je verkenning!

Zoals in alle tutorials in deze serie moedig ik je aan om de hier gepresenteerde data te gebruiken als een leuke casestudy, maar ook als ideeënbron om tekst te analyseren die jou interesseert. Dat kunnen je eigen favoriete artiesten zijn, social media, bedrijfs-, wetenschappelijke of sociaal-economische tekst. Wat zijn jouw gedachten over potentiële toepassingen van topic modeling? Laat hieronder een reactie achter met je ideeën!

Blijf op de hoogte voor Deel Drie (het vierde artikel) van deze serie: Lyric Analysis: Predictive Analytics using Machine Learning with R. Daar gebruik je songteksten en gelabelde metadata om de toekomst van een nummer te voorspellen. Je werkt met verschillende algoritmen zoals Decision Trees, Naive Bayes en meer om genre, decennium en mogelijk zelfs of een nummer de hitlijsten haalt te voorspellen! Hopelijk tot snel!


Kanttekeningen:

Kanttekening één: De enige manier om exact dezelfde resultaten te krijgen als ik, is door de code in exact dezelfde volgorde te draaien met dezelfde, ongewijzigde databestanden. Anders zullen verwijzingen naar specifieke topics niet logisch zijn.

Kanttekening twee: Er zijn geen keiharde regels voor dit werk, en het is gebaseerd op mijn eigen onderzoek. Hoewel topic modeling in real-world toepassingen bestaat, is er niet veel activiteit rond het ontwikkelen van muziekaanbevelers die volledig op lyrics zijn gebaseerd.

Kanttekening drie: Topic modeling is lastig. Het model bouwen is één ding, maar de resultaten interpreteren is uitdagend, subjectief en moeilijk volledig te valideren (hoewel er onderzoek gaande is: zie dit artikel van Thomas W. Jones). Het proces is unsupervised, generatief, iteratief en gebaseerd op waarschijnlijkheden. Topics zijn subtiel, verborgen en impliciet, niet expliciet.

Wil je meer leren over machine learning met R, bekijk dan onze beginners-tutorial. Volg ook onze cursus Introduction to Machine Learning course.

Onderwerpen

Gerelateerd