knitr::include_graphics('Francesco.JPG')
...

INTRODUCTION

This project proposes and seeks to show that song lyrics can be a determing factor of a song’s genre. I do not discount the musical power of harmony, melody, and rhythm to differentiate genre, but it’s not common for musicians, educators and others to classify music limited to a song’s lyrics. I accept this challenge.

Business Case:

The main question is: Why would anyone or any company care whether lyrics define the song’s genre?

Let’s look at potential uses. Businesses such at Pandora and Spotify have been analyzing and classifying music (including songs with lyrics) for their customers, to provide an inclusive listening experience. But, many other business can benefit from music and lyrics analysis.

Let’s look at songwriters, their management, licensing companies such as ASCAP and BMI, and music libraries such as HD Publishing.

For songwriters, there is a sub-group that are only lyricists, meaning someone else composes the music. Composers look for lyrics to compose to. Lyricists look for composers who are known for specific genres. Many haven’t worked together before. Managers often have knowledge of both sides, and will broker deals that combine their talents. Giving songwriters and composers the ability look for each other by matching lyrics with music could be a further democratization of the music industry. It could remove the role of the manager (and associated fees) to connect songwriters with music composers and bands.

Licensing companies deal with both lyrics and songs as legal assets they manage for artists. While this is not the main part of their business, they would benefit from validating the lyric’s genre compared to what the artist might think it is.

When another artist wants to record someone else’s song, they have to pay license fees to a licensing company such as ASCAP. ASCAP has a website that deals with this from a financial aspect, but not from a search and song selection aspect. Having this capability would enhance their service offering to their clients, would be a competitive differentiator; and potentially increase profits through “premium” chargeable services.

GOAL

The goal of this project is to use data science to analyze a library of popular song lyrics, put the data in a clean and useable form, and build and train a model to classify the words that best characterize the song’s genre. Ultimately, I plan to be able to use song lyrics not in the sample DB to test the model, using lyrics from major artists and others.

Methodology

I will use the readily available subset of lyrics from The Million Song library. My sample size will be 57,000 songs from multiple genres and artists. From this, I will cut it into training and test data to build and test my model. I will use packages that are designed to be used for Natural Language Processing (NLP) including tidytext, syuzhet(sentiment) and others.

To train the prediction, I will need to give the model the most likely genre for each song. This is known as supervised training. Unfortunately, this is not provided with the freely available data. Due to the huge task to select the genre for each of the 57,000 songs, I will focus on the artist’s best known genre using multiple readily available sources that categorize music. I have done this for 632 artists.

Note: I have combined some genres to make the analysis work better due to similarities in genres.

# head(raw_dat)
# kable(head(raw_dat))
  #column_spec(5:7, bold = T) %>%
  #row_spec(3:5, bold = T, color = "white", background = "#D7261E")
# table_output(raw_data)

Wrangle Data

Add Sentiment

Compute: Number of Words, Letters, and Average Word Length per Song.

dat = raw_dat %>%
  mutate(sentiment = get_sentiment(text),
         number_of_words = stri_count(text, regex="\\S+"),
         number_of_letters = nchar(text),
         avg_word_length = number_of_letters / number_of_words)

EDA

Take a Peek at the Data

table_output(raw_dat)
artist song text genre id
Nickelback Just To Get High He was my Country 11107
Kirk Franklin Still In Love [Verse 1] Religious 30279
Alphaville Faith Woke up in Pop 15763
Olivia Newton-John Reach Out For Me When you g Pop 33842
Sia Loved Me Back To Life I was walk Pop 36008
Britney Spears Mannequin Always tal Pop 1883
David Bowie Baby Can Dance I’m rollin Pop 20421
Patsy Cline Pick Me Up On Your Way Down Once my lo Country 34184
Don Henley Gimme What You Got Baby picks Rock 21406
Bob Dylan Days Of 49 I’m old To Folk 17684
table_output(dat)
artist song text genre id sentiment number_of_words number_of_letters avg_word_length
Nickelback Just To Get High He was my Country 11107 -2.45 307 1651 5.377850
Kirk Franklin Still In Love [Verse 1] Religious 30279 7.75 394 1988 5.045685
Alphaville Faith Woke up in Pop 15763 7.55 160 845 5.281250
Olivia Newton-John Reach Out For Me When you g Pop 33842 1.10 233 1196 5.133047
Sia Loved Me Back To Life I was walk Pop 36008 0.05 238 1268 5.327731
Britney Spears Mannequin Always tal Pop 1883 1.85 352 1994 5.664773
David Bowie Baby Can Dance I’m rollin Pop 20421 0.75 285 1516 5.319298
Patsy Cline Pick Me Up On Your Way Down Once my lo Country 34184 3.10 155 811 5.232258
Don Henley Gimme What You Got Baby picks Rock 21406 3.40 316 1746 5.525316
Bob Dylan Days Of 49 I’m old To Folk 17684 -1.05 508 2412 4.748031

Plot Mean Sentiment for Genres

dat %>%
  filter(sentiment != 0) %>%
  group_by(genre) %>%
  summarize(mean_sentiment = mean(sentiment)) %>%
  ggplot(aes(x = fct_reorder(genre, mean_sentiment), y = mean_sentiment, fill=mean_sentiment)) + 
  geom_col() + 
  coord_flip() + 
  labs(x = '', y = 'Sentiment', title = 'Lyric Sentiment by Genre') + 
  theme(legend.position = 'none')

Most genres have a positive sentiment.

  • The genres with the most positive sentiment are:
    • Religous
    • R&B
    • Blues

The genres with the most negative sentiment are: - Hip-Hop-Rap and Metal (just slightly negative).

Sentiment Density Plot

dat %>%
  ggplot(aes(x = sentiment, col = genre, fill = genre)) +
  geom_density(alpha = 0.5) + 
  labs(x = 'Sentiment', y = 'Density', title = 'Distribution of Sentiment by Genre')

All genres seem to have a normal distribution of sentiments, but have a different mean and SD.

Programmatically Identify Key Words in Genres

Exceeded R’s object capacity when unnesting all 57,000 songs (many million words). Need to break into 4 pieces, unnest into words, and stitch back together

# Break text_dat_raw into 4 parts
text_dat_raw_1 = raw_dat[1:10000,]
text_dat_raw_2 = raw_dat[10001:20000,]
text_dat_raw_3 = raw_dat[20001:30000,]
text_dat_raw_4 = raw_dat[30001:nrow(raw_dat),]
#Unnest all 4 parts and remove stop_words

text_dat_raw_1 = text_dat_raw_1%>%
unnest_tokens(word, text)

text_dat_raw_2 = text_dat_raw_2%>%
unnest_tokens(word, text)

text_dat_raw_3 = text_dat_raw_3%>%
unnest_tokens(word, text)

text_dat_raw_4 = text_dat_raw_4%>%
unnest_tokens(word, text)


# Combine 4 parts back into 1
text_dat_raw = text_dat_raw_1 %>%
  bind_rows(text_dat_raw_2) %>%
  bind_rows(text_dat_raw_3) %>%
  bind_rows(text_dat_raw_4)

  

text_dat_grouped = text_dat_raw %>%
  group_by(genre, id, word) %>%
  summarize(n = 1) %>%
  group_by(genre, word) %>%
  summarize(n = n()) %>%
  group_by(word) %>%
  mutate(pct_of_total = n / sum(n)) %>%
  ungroup() %>%
  # Increase n below for fewer columns!
  filter(n > 1000, 
         pct_of_total > 0.1) %>% # must be in +70% favor of # one review type
  select(word)

text_dat_spread = text_dat_grouped %>% 
  left_join(text_dat_raw, by = 'word') %>%
  select(id, word) %>%
  distinct(id, word) %>%
  mutate(n = 1) %>%
  spread(key = word, value = n)

text_dat_spread[is.na(text_dat_spread)]=0

Join dat & text_dat

final_dat = dat %>% select(-song) %>% 
  left_join(text_dat_spread, by = 'id') %>%
  select(-text, -artist) %>%
  janitor::clean_names() %>%
  rename(Class = genre) %>%
  mutate(Class = as.factor(Class))
final_dat[is.na(final_dat)] = 0
table_output(final_dat)
Class id sentiment number_of_words number_of_letters avg_word_length
Country 11107 -2.45 307 1651 5.377850
Religious 30279 7.75 394 1988 5.045685
Pop 15763 7.55 160 845 5.281250
Pop 33842 1.10 233 1196 5.133047
Pop 36008 0.05 238 1268 5.327731
Pop 1883 1.85 352 1994 5.664773
Pop 20421 0.75 285 1516 5.319298
Country 34184 3.10 155 811 5.232258
Rock 21406 3.40 316 1746 5.525316
Folk 17684 -1.05 508 2412 4.748031

Build Model

Steps:

  • Split data 75% to Training Data… 25% to Testing Data
  • Do NOT include genre (i.e “Class”) or “id” variable in model training!
  • Balance the number words per genre by using Up Sampling technique.
training_split = 0.75
smp_size = floor(training_split * nrow(final_dat))
dat_index = sample(seq_len(nrow(final_dat)), size = smp_size)
dat_train = as.data.frame(final_dat[dat_index,])

dat_train = upSample(x = dat_train %>% select(-Class),
                     y = dat_train$Class)

dat_test = as.data.frame(final_dat[-dat_index,])
dat_train = dat_train %>%
  select(-id)

Training Data

table_output(dat_train)
sentiment number_of_words number_of_letters avg_word_length Class
8.75 178 924 5.191011 Blues
5.70 263 1491 5.669201 Blues
1.35 162 853 5.265432 Blues
1.95 239 1210 5.062761 Blues
3.00 384 2232 5.812500 Blues
5.25 248 1376 5.548387 Blues
2.15 262 1321 5.041985 Blues
1.40 136 814 5.985294 Blues
1.05 280 1524 5.442857 Blues
-0.20 126 687 5.452381 Blues
dat_test = dat_test %>%
  select(-id)

Test Data

table_output(dat_test)
Class sentiment number_of_words number_of_letters avg_word_length
Religious 7.75 394 1988 5.045685
Pop 7.55 160 845 5.281250
Pop 0.75 285 1516 5.319298
Country 3.45 198 1031 5.207071
Rock -4.60 128 790 6.171875
R&B 3.45 306 1468 4.797386
Pop 1.20 236 1159 4.911017
Jazz 3.40 122 682 5.590164
Pop 5.20 455 2270 4.989011
Religious 4.80 108 576 5.333333

Define Random Forest Model

  • Use Cross Validation across variables to avoid correlations
  • Don’t include genre (i.e. “Class”)
  • ranger = Random Forest
  • Use 50 trees
  • Use “impurity” method for brancing classification
  • Run the prediction model
  • Report the confusion matrix
train_control = trainControl(method = "cv")

model_rf = train(dat_train %>% select(-Class),
            dat_train$Class,
            method = "ranger",
            num.trees = 50,
            importance = "impurity",
            trControl = train_control)

predictions_rf = predict(model_rf, dat_test)
confusionMatrix(predictions_rf, as.factor(dat_test$Class))
## Confusion Matrix and Statistics
## 
##               Reference
## Prediction     Blues Country D-DJ-E-D-E-H Folk Hip-Hop-Rap Jazz Metal Pop
##   Blues            0       0            0    0           0    0     0   2
##   Country          1       1            2    1           0    0     0   8
##   D-DJ-E-D-E-H     0       0            0    0           0    0     0   3
##   Folk             1       2            2    3           1    1     1  11
##   Hip-Hop-Rap      0       0            1    1           9    0     0   8
##   Jazz             0       0            0    0           0    0     0   4
##   Metal            0       0            0    0           0    0     0   2
##   Pop              4      16            7   10           3    3     2  79
##   R&B              0       0            0    1           0    2     0   5
##   Religious        0       3            0    0           0    2     1   2
##   Rock             3      19            2   15           1    3     4  56
##               Reference
## Prediction     R&B Religious Rock
##   Blues          0         0    2
##   Country        2         0   19
##   D-DJ-E-D-E-H   0         0    3
##   Folk           3         1   12
##   Hip-Hop-Rap    1         0    2
##   Jazz           0         0    4
##   Metal          0         0    4
##   Pop            6         7   51
##   R&B            0         0    1
##   Religious      0         2    1
##   Rock           1         2   68
## 
## Overall Statistics
##                                          
##                Accuracy : 0.324          
##                  95% CI : (0.2831, 0.367)
##     No Information Rate : 0.36           
##     P-Value [Acc > NIR] : 0.9585         
##                                          
##                   Kappa : 0.0801         
##                                          
##  Mcnemar's Test P-Value : NA             
## 
## Statistics by Class:
## 
##                      Class: Blues Class: Country Class: D-DJ-E-D-E-H
## Sensitivity                0.0000        0.02439              0.0000
## Specificity                0.9919        0.92810              0.9877
## Pos Pred Value             0.0000        0.02941              0.0000
## Neg Pred Value             0.9819        0.91416              0.9717
## Prevalence                 0.0180        0.08200              0.0280
## Detection Rate             0.0000        0.00200              0.0000
## Detection Prevalence       0.0080        0.06800              0.0120
## Balanced Accuracy          0.4959        0.47625              0.4938
##                      Class: Folk Class: Hip-Hop-Rap Class: Jazz
## Sensitivity              0.09677             0.6429      0.0000
## Specificity              0.92537             0.9733      0.9836
## Pos Pred Value           0.07895             0.4091      0.0000
## Neg Pred Value           0.93939             0.9895      0.9776
## Prevalence               0.06200             0.0280      0.0220
## Detection Rate           0.00600             0.0180      0.0000
## Detection Prevalence     0.07600             0.0440      0.0160
## Balanced Accuracy        0.51107             0.8081      0.4918
##                      Class: Metal Class: Pop Class: R&B Class: Religious
## Sensitivity                0.0000     0.4389     0.0000           0.1667
## Specificity                0.9878     0.6594     0.9815           0.9816
## Pos Pred Value             0.0000     0.4202     0.0000           0.1818
## Neg Pred Value             0.9838     0.6763     0.9735           0.9796
## Prevalence                 0.0160     0.3600     0.0260           0.0240
## Detection Rate             0.0000     0.1580     0.0000           0.0040
## Detection Prevalence       0.0120     0.3760     0.0180           0.0220
## Balanced Accuracy          0.4939     0.5491     0.4908           0.5741
##                      Class: Rock
## Sensitivity               0.4072
## Specificity               0.6817
## Pos Pred Value            0.3908
## Neg Pred Value            0.6963
## Prevalence                0.3340
## Detection Rate            0.1360
## Detection Prevalence      0.3480
## Balanced Accuracy         0.5444

Plot the Importance of Variables

model_rf$finalModel %>%
  # extract variable importance metrics
  ranger::importance() %>%
  # convert to a data frame
  enframe(name = "variable", value = "varimp") %>%
  top_n(n = 20, wt = varimp) %>%
  # plot the metrics
  ggplot(aes(x = fct_reorder(variable, varimp), y = varimp, fill=varimp)) +
  geom_col() +
  coord_flip() +
  labs(x = "Token",
       y = "Variable importance (higher is more important)")

Save the Random Forest Model for later use.

a = dat_train %>% as_tibble()
model_rf_data_structure = a[0,]
timestamp = Sys.time()
saveRDS(model_rf_data_structure, paste0('models/model_rf_data_structure - ', timestamp, '.rds'))
saveRDS(model_rf, file = paste0('models/model_rf - ', timestamp, '.rds'))


saveRDS(model_rf_data_structure, paste0('models/model_rf_data_structure - ', '.rds'))
saveRDS(model_rf, file = paste0('models/model_rf - ', '.rds'))

Conclusion

To a resonable degree, lyrics alone can be used to predict a song’s genre. I acheived a 42% prediction accuracy across the ? genres in my statistical prediction model. This is better than pure guessing (which would be closer to ?%). And some genres can be predicted better than others.

Here are the results of the top 11 genres:

  • Hip-Hop-Rap – 73%
  • Religious – 78%
  • Rock – 60%, Pop – 56% …
  • Religious, Folk, Country, Blues, Jazz, Metal – 50%

Engineered variables including number of words, number of letters, and sentiment led the list of importance in the prediction model. While down, like, ain’t, baby, and love were the most important words.

Most genres have a positive sentiment with the exception of Hip-Hop-Rap, and Metal. More precise analysis could further differentiate sentiment into categories such foul language, cultural meanings, and other categories. For example Raggae music commonly uses words such as ra and wit.

Random Forest is a good prediction model for this Natural Language Processing project. And further tuning might get the accuracy higher, but considerable compute resources are needed to run this model. I was able to run an optional Neural Network model that achieved 48% accuracy, but the results weren’t stable enough for me to chose it for my conclusion. And, I did use a Random Forest pre-processing method for the Neural Networks model, showing that they can work together.

Dealing with Big Data in this project was a challenge, but taught me a great deal about several techniques to help with both the data preparation and model running stages. With Big Data there is no substitute for more compute power and memory. But, even with those, limitations in the R language required techniques to work around them.

Combining my lyrics prediction analysis with existing music analysis would likely produce an even higher prediction accuracy. This will be a personal project for me after I leave this certification training.

On a personal note, Data Science has sparked an interest in me to seek work in this field that combines it with my previous experience.