Đây là Phần Một trong loạt hướng dẫn gồm ba phần, trong đó bạn sẽ dùng R để thực hiện nhiều tác vụ phân tích trên một nghiên cứu tình huống về ca từ của nghệ sĩ huyền thoại Prince. Ba bài hướng dẫn bao gồm:
Ca từ có thể phản ánh góc nhìn của nghệ sĩ, nhưng các ca khúc phổ biến cho thấy xã hội muốn nghe gì. Phân tích ca từ không hề đơn giản. Vì cấu trúc của ca từ khác văn xuôi rất nhiều, nó đòi hỏi thận trọng trong giả định và lựa chọn kỹ thuật phân tích mang tính phân biệt độc đáo. Ca từ len lỏi vào đời sống và ảnh hưởng đến suy nghĩ của chúng ta một cách tinh vi. Khái niệm Lời bài hát dự báo (Predictive Lyrics) bắt đầu được nhắc đến nhiều và ngày càng xuất hiện trong các bài báo khoa học và luận văn. Nghiên cứu tình huống này sẽ chỉ chạm tới một vài khía cạnh của chủ đề đang nổi này.
Prince: The Artist
Để tôn vinh sự nghiệp phong phú, đa dạng đầy cảm hứng mà Prince để lại, bạn sẽ khám phá những thông điệp đôi khi hiển nhiên nhưng thường ẩn giấu trong ca từ của ông. Tuy nhiên, bạn không cần phải yêu thích âm nhạc của Prince để trân trọng ảnh hưởng của ông đối với sự phát triển của nhiều thể loại trên toàn cầu. Tạp chí Rolling Stone đã xếp Prince là nhạc sĩ sáng tác vĩ đại thứ 18 mọi thời đại, ngay sau Bob Dylan, John Lennon, Paul Simon, Joni Mitchell và Stevie Wonder. Phân tích ca từ đang dần xuất hiện trong cộng đồng khoa học dữ liệu khi khả năng dự đoán "Bài hát Hit" tiến gần hiện thực.
Prince là một con người tràn đầy âm nhạc — một nhạc sĩ sáng tác cực kỳ mắn, bậc thầy guitar, keyboard và trống, và là kiến trúc sư của funk, rock, R&B và pop, dù âm nhạc của ông vượt khỏi mọi khuôn thể loại. - Jon Pareles (NY Times)
Trong hướng dẫn này, Phần Một của loạt bài, bạn sẽ sử dụng các kỹ thuật khai phá văn bản trên một tập ca từ theo khuôn khổ tidy text. Dữ liệu tidy có cấu trúc cụ thể: mỗi biến là một cột, mỗi quan sát là một hàng, và mỗi đơn vị quan sát là một bảng. Sau khi làm sạch và điều kiện hóa dữ liệu, bạn sẽ tạo thống kê mô tả và trực quan hóa khám phá khi quan sát các khía cạnh khác nhau trong ca từ của Prince.
Yêu cầu tiên quyết
Phần Một yêu cầu hiểu biết cơ bản về dữ liệu tidy — cụ thể là các gói như dplyr để biến đổi dữ liệu, ggplot2 để trực quan hóa, và toán tử %>% pipe có nguồn gốc từ gói magrittr. Mỗi hướng dẫn sẽ mô tả các công cụ bạn có thể dùng cho phân tích, nhưng có thể không đi sâu vào mọi bước. Bạn sẽ thấy nhiều bước thường được kết hợp bằng toán tử %>%. Vì đây cũng là một nghiên cứu tình huống, cần nhớ rằng mọi suy luận bạn đưa ra đều mang tính quan sát; do đó, tương quan không hàm ý quan hệ nhân quả.
Mẹo: để có kiến thức nền về các công cụ sử dụng, hai tài nguyên tốt là R for Data Science của Garrett Grolemund và Hadley Wickham; và Text Mining with R của Julia Silge và David Robinson.
Phần Hai và Ba
Trong một hướng dẫn riêng, Phần Hai, bạn sẽ thực hiện Phân tích cảm xúc và Mô hình chủ đề để nắm bắt tâm trạng và chủ đề tổng thể trong âm nhạc của Prince và cách chúng phản ánh góc nhìn xã hội. Bạn sẽ làm việc với một từ điển cảm xúc, đánh giá cảm xúc nhị phân và phân loại, vẽ xu hướng theo thời gian, và xem n-gram và liên kết từ. Bạn cũng sẽ dùng Xử lý ngôn ngữ tự nhiên (NLP) và các kỹ thuật phân cụm như Phân bổ Dirichlet Tiềm ẩn (LDA) và K-Means để gợi mở mô-típ trong ca từ.
Trong một hướng dẫn khác nữa, Phần Ba, bạn sẽ khép lại vòng tròn bằng cách dùng kết quả khám phá để giúp dự đoán bài hát phát hành vào thập niên nào, và thú vị hơn, liệu một bài hát có lọt vào bảng xếp hạng Billboard dựa trên ca từ hay không. Bạn sẽ dùng các công cụ máy học như Cây quyết định (rpart và C50), K-Láng giềng gần nhất (class), và Naive Bayes (e1071) để tạo bộ phân loại có thể nhận văn bản.
Cả ba phần sẽ dùng cùng một tập dữ liệu gồm ca từ của Prince, năm phát hành và vị trí trên bảng xếp hạng Billboard. Các kỹ thuật trong nghiên cứu này cũng có thể áp dụng cho nhiều loại văn bản khác. Thực tế, kết quả cho văn xuôi tiêu chuẩn thường dễ diễn giải hơn vì ca từ nói chung thường được thiết kế với thông điệp gián tiếp và sắc thái tinh tế.
Tóm lại, có rất nhiều phương pháp để phân tích ca từ. Các hướng dẫn này đề cập đến những phương pháp được tô đỏ trong hình dưới đây. Lưu ý hình này chỉ là đại diện cấp cao cho một bức tranh rất "mờ". Và không, tôi không nói bức ảnh đâu! Sự thật là các khía cạnh khác nhau của mô hình hóa và kỹ thuật máy học giờ đây khá nhập nhằng và không nhất thiết nằm gọn trong một ô như hình. Vậy nên hãy đeo kính 3D khi nhìn vào ảnh, có thể sẽ dễ hiểu hơn!
Bên cạnh việc học và thực hành kỹ năng mới, hướng dẫn này nhằm giải quyết các câu hỏi cơ bản về khái niệm phân tích ca từ. Những nghiên cứu gần đây cho thấy "chỉ số thông minh ca từ" có thể đang suy giảm trong nhạc đại chúng. Một số nghiên cứu còn gợi ý rằng từ ngữ trong các Bài hát No.1 phù hợp với trình độ đọc của học sinh lớp 3 ở Mỹ. Liệu có thể sử dụng khai phá văn bản, NLP, máy học và các phương pháp khoa học dữ liệu khác để soi chiếu chủ đề này? Liệu có thể xác định các chủ đề hấp dẫn xã hội dựa trên mức độ đón nhận của bài hát? Liệu có thể dự đoán một bài hát sẽ thành công chỉ dựa vào phân tích ca từ? Trong hướng dẫn đầu tiên này, bạn sẽ khảo sát độ phức tạp ngôn từ trong âm nhạc của Prince như một bài tập khám phá.
Câu hỏi
Trước khi bắt đầu, hãy nghĩ về điều bạn muốn khám phá. Những câu hỏi nào đáng quan tâm? Trước tiên bạn sẽ đi qua phần phân tích tập dữ liệu. Nó trông như thế nào? Có bao nhiêu bài hát? Ca từ có cấu trúc ra sao? Cần làm sạch và xử lý dữ liệu đến mức nào? Sự thật là gì? Tần suất từ thế nào và tại sao điều đó quan trọng? Về mặt kỹ thuật, bạn muốn hiểu và chuẩn bị dữ liệu cho phân tích cảm xúc, NLP và các mô hình máy học.
Âm nhạc từ lâu đã là cách hiệu quả để truyền đạt tới số đông, và ca từ đóng vai trò to lớn trong việc truyền tải thông điệp này. Tuy nhiên cơ hội nghiên cứu vai trò của ca từ đối với sự thịnh vượng [của xã hội] còn bị khai thác rất ít. - Patricia Fox Ransom
Dữ liệu
Một phương pháp phổ biến để lấy dữ liệu phục vụ khai phá văn bản là dùng gói rvest để quét nội dung từ web. Tôi đã quét được thông tin Bảng xếp hạng Billboard và ca từ của Prince từ nhiều trang, rồi ghép chúng theo tựa bài hát. Có chút xử lý vì quy ước đặt tên tiêu đề không nhất quán. Sau đó tôi đưa ra quyết định chủ quan là loại bỏ tất cả các bài không phải bản gốc, như remix, bản mở rộng, club mix, bản làm lại, v.v. Tôi cũng loại các album tuyển tập các Bài hát Hit để tránh lặp. Tôi làm sạch nhẹ và lưu kết quả vào tệp csv để dùng cho hướng dẫn này.
Vì Phần Một tập trung vào khai phá văn bản, tôi không đưa phần mã đó vào hướng dẫn này, nhưng tập dữ liệu có sẵn để bạn tải về tại đây nếu muốn làm theo.
Nạp các thư viện
#most of the libraries needed
library(dplyr) #data manipulation
library(ggplot2) #visualizations
library(gridExtra) #viewing multiple plots together
library(tidytext) #text mining
library(wordcloud2) #creative visualizations
Đọc dữ liệu
Có nhiều cách đọc dữ liệu từ tệp csv, nhưng tôi chọn dùng read.csv() để nạp một data frame gồm ca từ, năm phát hành và vị trí trên bảng xếp hạng Billboard. Lưu ý theo mặc định, R chuyển mọi chuỗi ký tự thành factor. Điều này có thể gây vấn đề về sau, nhưng bạn có thể xử lý bằng cách đặt tham số stringsAsFactors là FALSE. Giờ hãy xem dữ liệu...
Vì tôi tạo tệp này nên tôi biết X chỉ là số hàng và text là ca từ thực tế. Các trường cần thiết khác gồm song, year và peak (thể hiện vị trí trên bảng xếp hạng Billboard). US.Pop và US.R.B là vị trí cao nhất trên bảng xếp hạng của Mỹ (Pop và R&B), nên giữ lại các trường này và tạm bỏ các trường khác.
Làm điều này bằng cách lấy tập dữ liệu gốc prince_orig và pipe vào select() bằng %>%. Cách này giúp bạn đọc mã từ trái sang phải.
Đồng thời, lưu ý rằng select() cho phép bạn đổi tên cột trong một bước. Vì vậy hãy đặt text thành lyrics và đổi tên các cột US theo phong cách tidyverse dùng "_" thay vì ".". Sau đó lưu kết quả vào prince, sẽ dùng xuyên suốt loạt bài. dplyr cung cấp hàm glimpse() giúp xem dữ liệu ở dạng chuyển vị thuận tiện.
prince <- prince_orig %>%
select(lyrics = text, song, year, album, peak,
us_pop = US.Pop, us_rnb = US.R.B)
glimpse(prince[139,])
Observations: 1
Variables: 7
$ lyrics <chr> "I just can't believe all the things people say, controversy\nAm I ...
$ song <chr> "controversy"
$ year <int> 1981
$ album <chr> "Controversy"
$ peak <int> 3
$ us_pop <chr> "70"
$ us_rnb <chr> "3"
Câu hỏi hiển nhiên đầu tiên: có bao nhiêu quan sát và cột?
dim(prince)
[1] 824 7
Dùng hàm dim(), bạn thấy có 7 cột và 824 quan sát. Mỗi quan sát là một bài hát. Như tôi đã nói... mắn bút!
Nhìn vào cột lyrics của một bài hát, bạn có thể thấy cấu trúc của ca từ.
str(prince[139, ]$lyrics, nchar.max = 300)
chr "I just can't believe all the things people say, controversy\nAm I Black or White? Am I straight or gay? Controversy\nDo I believe in God? Do I believe in me? Controversy\nControversy, controversy\nI can't understand human curiosity, controversy\nWas it good for you? Was I what you w"| __truncated__
Có rất nhiều cơ hội để làm sạch dữ liệu, vậy hãy bắt đầu.
Điều kiện hóa dữ liệu
Làm sạch cơ bản
Có nhiều phương pháp để điều kiện hóa dữ liệu. Một lựa chọn là chuyển data frame thành Corpus và Document Term Matrix bằng gói tm rồi dùng hàm tm_map() để làm sạch, nhưng hướng dẫn này sẽ bám vào những điều cơ bản và dùng gsub() và các hàm apply() để làm phần nặng nhọc.
Trước hết, loại bỏ các dạng viết tắt khó chịu bằng cách tạo một hàm nhỏ xử lý hầu hết các tình huống bằng gsub(), rồi áp dụng hàm đó cho toàn bộ ca từ.
# function to expand contractions in an English-language source
fix.contractions <- function(doc) {
# "won't" is a special case as it does not expand to "wo not"
doc <- gsub("won't", "will not", doc)
doc <- gsub("can't", "can not", doc)
doc <- gsub("n't", " not", doc)
doc <- gsub("'ll", " will", doc)
doc <- gsub("'re", " are", doc)
doc <- gsub("'ve", " have", doc)
doc <- gsub("'m", " am", doc)
doc <- gsub("'d", " would", doc)
# 's could be 'is' or could be possessive: it has no expansion
doc <- gsub("'s", "", doc)
return(doc)
}
# fix (expand) contractions
prince$lyrics <- sapply(prince$lyrics, fix.contractions)
Bạn cũng sẽ thấy các ký tự đặc biệt làm đục văn bản. Có thể loại bỏ bằng gsub() và một biểu thức chính quy đơn giản. Lưu ý cần mở rộng viết tắt trước khi làm bước này!
# function to remove special characters
removeSpecialChars <- function(x) gsub("[^a-zA-Z0-9 ]", " ", x)
# remove special characters
prince$lyrics <- sapply(prince$lyrics, removeSpecialChars)
Để nhất quán, hãy chuyển mọi thứ về chữ thường với hàm tolower().
# convert everything to lower case
prince$lyrics <- sapply(prince$lyrics, tolower)
Giờ xem lại ca từ sẽ thấy phiên bản sạch hơn nhiều so với văn bản thô ban đầu.
str(prince[139, ]$lyrics, nchar.max = 300)
chr "i just can not believe all the things people say controversy am i black or white
Một bước phổ biến khác khi điều kiện hóa dữ liệu cho khai phá văn bản là stemming, hay tách từ về gốc nghĩa. Đây là một chủ đề riêng có thể bàn sau. Hiện tại, hãy xem tóm tắt của data frame prince.
#get facts about the full dataset
summary(prince)
lyrics song year album
Length:824 Length:824 Min. :1978 Length:824
Class :character Class :character 1st Qu.:1989 Class :character
Mode :character Mode :character Median :1996 Mode :character
Mean :1995
3rd Qu.:1999
Max. :2015
NA's :495
peak us_pop us_rnb
Min. : 0.00 Length:824 Length:824
1st Qu.: 2.00 Class :character Class :character
Median : 7.00 Mode :character Mode :character
Mean :15.48
3rd Qu.:19.00
Max. :88.00
NA's :751
Như bạn thấy, có 37 năm bài hát, và bài xếp hạng thấp nhất (tồn tại trong dữ liệu) ở vị trí 88. Bạn cũng thấy khá nhiều NA cho year và peak. Vì bạn sẽ làm các loại phân tích khác nhau, hãy giữ toàn bộ tập dữ liệu trong data frame prince và chỉ lọc khi cần.
Thêm một vài trường
Vì một trong các câu hỏi mục tiêu là tìm xu hướng bài hát theo thời gian, và tập dữ liệu có năm phát hành, bạn có thể tạo “rổ” và gom năm thành thập niên. Dùng động từ mutate() của dplyr để tạo trường decade mới. Một cách là dùng ifelse() kết hợp toán tử %in% để lọc theo năm và phân loại bài hát vào các thập niên. Sau đó lưu lại vào prince (tức là thêm trường mới).
Bạn có thể làm tương tự cho chart_level, biểu thị một bài đạt đỉnh Top 10, Top 100, hay không vào bảng (uncharted). Chúng loại trừ lẫn nhau, nên Top 100 không bao gồm Top 10.
#create the chart level column
prince <- prince %>%
mutate(chart_level =
ifelse(prince$peak %in% 1:10, "Top 10",
ifelse(prince$peak %in% 11:100, "Top 100", "Uncharted")))
Thêm vào đó, tạo trường nhị phân charted cho biết một bài có lọt vào bảng xếp hạng Billboard hay không (tức là bài phổ biến). Dùng write.csv() để lưu cho các hướng dẫn sau.
#create binary field called charted showing if a song hit the charts at all
prince <- prince %>%
mutate(charted =
ifelse(prince$peak %in% 1:100, "Charted", "Uncharted"))
#save the new dataset to .csv for use in later tutorials
write.csv(prince, file = "prince_new.csv")
Thống kê mô tả
Để tùy biến biểu đồ, tôi thích tạo một danh sách màu riêng để giữ tính nhất quán. Có rất nhiều nơi trên web cung cấp các màu phân biệt qua mã hex như dưới đây. Nếu bạn có sở thích cho biểu đồ, bạn cũng có thể tạo một theme riêng để áp dụng cho ggplot() khi cần.
#define some colors to use throughout
my_colors <- c("#E69F00", "#56B4E9", "#009E73", "#CC79A7", "#D55E00")
theme_lyrics <- function()
{
theme(plot.title = element_text(hjust = 0.5),
axis.text.x = element_blank(),
axis.ticks = element_blank(),
panel.grid.major = element_blank(),
panel.grid.minor = element_blank(),
legend.position = "none")
}
Trước khi vào khai phá văn bản, hãy bắt đầu với cái nhìn cơ bản về dữ liệu ở cấp bài hát. Giờ là lúc trực quan hóa số bài Prince phát hành theo thập niên. Nhắc lại, sự nghiệp chuyên nghiệp của ông bắt đầu từ 1978 tới 2015 (như thống kê summary() ở trên). Nhưng vì ta nhìn xu hướng, và tập dữ liệu có khá nhiều giá trị trống cho year, bạn sẽ muốn lọc bỏ tất cả NA của năm phát hành cho biểu đồ đầu tiên.
Thống kê bài hát
Dùng các hàm filter(), group_by() và summarise() của dplyr, bạn có thể nhóm theo thập niên rồi đếm số bài. Hàm n() là một trong nhiều hàm tổng hợp hữu ích khi dùng với summarise() trên dữ liệu đã nhóm. Sau đó dùng ggplot() và geom_bar() để tạo biểu đồ cột và tô màu cột theo danh mục charted.
prince %>%
filter(decade != "NA") %>%
group_by(decade, charted) %>%
summarise(number_of_songs = n()) %>%
ggplot() +
geom_bar(aes(x = decade, y = number_of_songs,
fill = charted), stat = "identity") +
theme(plot.title = element_text(hjust = 0.5),
legend.title = element_blank(),
panel.grid.minor = element_blank()) +
ggtitle("Released Songs") +
labs(x = NULL, y = "Song Count")
Biểu đồ cho thấy thập niên hoạt động mạnh nhất của ông là thập niên 1990.
Giờ hãy tạo biểu đồ tương tự với chart_level.
Nhớ group_by() cả decade và chart_level để thấy xu hướng.
Trong biểu đồ này, bạn chỉ xem các bài lọt bảng nên lọc mọi thứ khác bằng peak > 0. Pipe đối tượng group_by vào summarise() dùng n() để đếm số bài. Khi lưu vào biến, bạn có thể pipe sang ggplot() để vẽ biểu đồ cột đơn giản.
Lưu ý rằng trong số các bài lọt bảng của Prince, đa số vào Top 10. Nhưng thú vị hơn là thập niên sáng tác nhiều nhất là 1990s, tuy nhiên số hit lọt bảng lại nhiều hơn ở 1980s. Tại sao vậy? Hãy ghi nhớ câu hỏi này khi bạn đi tiếp phần khai phá văn bản.
Để tận dụng sức mạnh của toàn bộ tập dữ liệu cho phân tích ca từ, bạn có thể bỏ các tham chiếu đến cấp bảng xếp hạng và năm phát hành để có tập bài lớn hơn nhiều để khai phá. Xem thử:
#look at the full data set at your disposal
prince %>%
group_by(decade, chart_level) %>%
summarise(number_of_songs = n()) %>%
ggplot() +
geom_bar(aes(x = decade, y = number_of_songs,
fill = chart_level), stat = "identity") +
theme(plot.title = element_text(hjust = 0.5),
legend.title = element_blank(),
panel.grid.minor = element_blank()) +
labs(x = NULL, y = "Song Count") +
ggtitle("All Songs in Data")
Như bạn thấy, Prince viết hàng trăm bài không có ngày phát hành trong dữ liệu. Với phân tích cảm xúc hay khám phá, bạn có thể dùng toàn bộ dữ liệu, nhưng với xu hướng theo thời gian bạn chỉ làm việc với tập nhỏ hơn nhiều. Điều này chấp nhận được — chỉ cần lưu ý.
Các bài No. 1!
Với những người hâm mộ Prince, dưới đây là cái nhìn nhanh về các bài đứng No. 1 trên bảng xếp hạng. (Lưu ý bạn có thể dùng kable() và kable_styling() từ các gói knitr và kableExtra và color_tile() từ formattable để tạo đầu ra HTML đẹp.)
library(knitr) # for dynamic reporting
library(kableExtra) # create a nicely formated HTML table
library(formattable) # for the color_tile function
prince %>%
filter(peak == "1") %>%
select(year, song, peak) %>%
arrange(year) %>%
mutate(year = color_tile("lightblue", "lightgreen")(year)) %>%
mutate(peak = color_tile("lightgreen", "lightgreen")(peak)) %>%
kable("html", escape = FALSE, align = "c", caption = "Prince's No. 1 Songs") %>%
kable_styling(bootstrap_options =
c("striped", "condensed", "bordered"),
full_width = FALSE)
Prince's No. 1 Songs
year
song
peak
1979
i wanna be your lover
1
1984
erotic city
1
1984
purple rain
1
1984
when doves cry
1
1985
around the world in a day
1
1986
kiss
1
1988
lovesexy
1
1989
batdance
1
1990
thieves in the temple
1
1991
diamonds and pearls
1
1995
the most beautiful girl in the world
1
2006
3121
1
2007
planet earth
1
Khai phá văn bản
Khai phá văn bản cũng có thể hiểu là phân tích văn bản. Mục tiêu là khám phá thông tin liên quan có thể chưa biết hoặc bị chôn vùi dưới những điều hiển nhiên. Xử lý ngôn ngữ tự nhiên (NLP) là một phương pháp dùng trong khai phá văn bản. Nó cố gắng giải mã mơ hồ trong ngôn ngữ viết bằng cách tách token, phân cụm, trích thực thể và mối quan hệ từ, và dùng thuật toán để xác định chủ đề và định lượng thông tin chủ quan. Bạn sẽ bắt đầu bằng cách phân tích khái niệm độ phức tạp từ vựng.
Độ phức tạp từ vựng có thể mang nghĩa khác nhau theo ngữ cảnh, nhưng tạm thời, giả định rằng nó có thể mô tả bằng tổ hợp các thước đo sau:
Tần suất từ: số lượng từ mỗi bài
Độ dài từ: độ dài trung bình của các từ trong văn bản
Đa dạng từ vựng: số lượng từ độc nhất dùng trong văn bản (vốn từ của bài hát)
Mật độ từ vựng: số từ độc nhất chia cho tổng số từ (mức lặp từ)
Định dạng Tidy Text
Để bắt đầu phân tích, bạn cần tách ca từ thành từng từ riêng lẻ và bắt đầu khai phá thông tin. Quy trình này gọi là tokenization.
Định dạng dữ liệu và Tokenization
Hãy nhớ rằng có nhiều phương pháp và định dạng dữ liệu có thể dùng để khai phá văn bản:
Corpus: một tập hợp tài liệu được tạo bởi gói khai phá văn bản tm
Ma trận tài liệu-từ: ma trận liệt kê tất cả lần xuất hiện của từ trong corpus, theo tài liệu, trong đó tài liệu là hàng và từ là cột
Tidy Text: một bảng với mỗi token trên một hàng. Trong nghiên cứu này, token sẽ là một từ (hoặc n-gram sẽ bàn ở Phần Hai). Tokenization do đó là quá trình tách ca từ thành token. Hướng dẫn này sẽ dùng unnest_tokens() của tidytext để làm việc này. Xem tài liệu tidytext để biết chi tiết.
Nhưng trước khi token hóa, còn một bước làm sạch dữ liệu. Nhiều ca từ khi chép lại có các cụm như "Repeat Chorus", hoặc nhãn như "Bridge" và "Verse". Cũng có rất nhiều từ không mong muốn khác làm nhiễu kết quả. Sau một số phân tích trước, tôi chọn ra vài từ muốn loại bỏ.
Dưới đây là danh sách các từ thừa cần loại bỏ thủ công:
undesirable_words <- c("prince", "chorus", "repeat", "lyrics",
"theres", "bridge", "fe0f", "yeah", "baby",
"alright", "wanna", "gonna", "chorus", "verse",
"whoa", "gotta", "make", "miscellaneous", "2",
"4", "ooh", "uurh", "pheromone", "poompoom", "3121",
"matic", " ai ", " ca ", " la ", "hey", " na ",
" da ", " uh ", " tin ", " ll", "transcription",
"repeats")
Để unnest token, dùng thư viện tidytext đã được nạp. Giờ bạn có thể tận dụng khả năng của dplyr và nối nhiều bước với nhau.
Theo khuôn khổ tidy text, bạn cần vừa tách văn bản thành token riêng lẻ (tokenization) vừa chuyển đổi sang cấu trúc dữ liệu tidy. Để làm điều này, dùng hàm unnest_tokens() của tidytext. unnest_tokens() cần ít nhất hai đối số: tên cột đầu ra sẽ được tạo khi bung văn bản vào đó ("word" trong trường hợp này), và cột đầu vào đang giữ văn bản hiện tại (tức lyrics).
Bạn có thể lấy tập prince và pipe vào unnest_tokens() rồi loại bỏ stop words. Stop words là gì? Bạn biết quá rõ rồi. Chúng là các từ quá phổ biến có thể không thêm ý nghĩa cho kết quả. Có nhiều danh sách để chọn, nhưng ở đây bạn sẽ dùng từ điển stop_words từ gói tidytext.
Dùng sample() để hiển thị danh sách ngẫu nhiên các stop words này và head() để giới hạn 15 từ.
Vậy, sau khi bạn token hóa ca từ thành các từ, bạn có thể dùng anti_join() của dplyr để loại bỏ stop words. Tiếp theo, loại các từ không mong muốn đã định nghĩa ở trên bằng filter() của dplyr với toán tử %in%. Sau đó dùng distinct() để loại bản ghi trùng. Cuối cùng, loại mọi từ có ít hơn bốn ký tự. Đây là quyết định chủ quan khác, nhưng trong ca từ, chúng thường là thán từ như "yea" và "hey". Sau đó lưu kết quả vào prince_words_filtered.
Lưu ý: về sau, prince_words_filtered là phiên bản tidy text của data frame prince đã loại 1) stop words, 2) từ không mong muốn, và 3) từ dài 1-3 ký tự. Bạn sẽ dùng tập này trong một số, nhưng không phải tất cả các phân tích.
#unnest and remove stop, undesirable and short words
prince_words_filtered <- prince %>%
unnest_tokens(word, lyrics) %>%
anti_join(stop_words) %>%
distinct() %>%
filter(!word %in% undesirable_words) %>%
filter(nchar(word) > 3)
Lưu ý stop_words có cột word, và một cột mới tên word đã được tạo bởi unnest_tokens(), nên anti_join() tự động nối theo cột word.
Giờ bạn có thể xem lớp và kích thước cấu trúc dữ liệu tidy mới:
class(prince_words_filtered)
[1] "data.frame"
dim(prince_words_filtered)
[1] 36916 10
prince_words_filtered là một data frame với 36916 từ (không phải từ duy nhất) và 10 cột. Đây là ảnh chụp nhanh: (Tôi chọn một từ và giới hạn ở 10 bài, rồi dùng select() để in các trường thú vị theo thứ tự, lại dùng knitr để định dạng). Điều này cho thấy cấu trúc dữ liệu tidy đã token hóa, chưa tổng hợp.
Bạn thấy mỗi hàng chứa một từ riêng lẻ, sẽ lặp lại cho mỗi bài mà nó xuất hiện.
Tần suất từ
Trong âm nhạc, tần suất từ riêng lẻ rất quan trọng, dù là lặp lại hay hiếm gặp. Cả hai đều ảnh hưởng tới khả năng ghi nhớ của cả bài hát. Một câu hỏi người viết nhạc có thể muốn biết là liệu có tương quan giữa tần suất từ và bài hit hay không. Giờ bạn muốn đưa định dạng tidy tiến thêm một bước và lấy số đếm từ theo từng bài.
Để khảo sát điều này trong ca từ của Prince, hãy tạo biểu đồ histogram thể hiện phân phối số lượng từ, nhóm theo bài, theo vị trí trên Bảng xếp hạng Billboard. Unnest ca từ prince lần nữa mà không lọc từ nào để có số đếm chân thực về tần suất từ trên các bài. Lại dùng group_by() và summarise() để lấy số đếm. Sau đó dùng arrange() của dplyr để sắp theo count. Trước hết, xem các số đếm cao nhất, rồi dùng ggplot() để vẽ histogram.
Tôi đã tìm hiểu và phát hiện bài này có nghệ sĩ khách mời thể hiện phần rap. Điều đó giải thích rồi! Nhớ rằng tập dữ liệu này bao phủ bảng Pop và R&B toàn cầu, nên thể loại và địa lý có thể ảnh hưởng lớn tới giả định của bạn. Lưu ý xếp hạng US Pop thấp hơn nhiều so với các bảng khác, ví dụ đạt đỉnh 5 ở Canada. Chỉ là điều cần ghi nhớ.
Thử thách: Nếu bạn muốn tự phân tích âm nhạc, bạn có thể xem The Million Song Dataset có hơn 50 đặc trưng (ví dụ: tempo, độ lớn âm, khả năng khiêu vũ, v.v.) cho gần 50.000 nghệ sĩ! Thêm đặc trưng âm nhạc vào ca từ sẽ cho phân tích rất toàn diện.
Từ phổ biến nhất
Để đánh giá đơn giản các từ được dùng thường xuyên nhất trong toàn bộ ca từ, bạn có thể dùng count() và top_n() để lấy n từ hàng đầu từ tập dữ liệu sạch đã lọc. Sau đó dùng reorder() để sắp từ theo số đếm và dùng mutate() của dplyr để gán lại giá trị đã sắp cho word. Điều này giúp ggplot() hiển thị đẹp.
prince_words_filtered %>%
count(word, sort = TRUE) %>%
top_n(10) %>%
ungroup() %>%
mutate(word = reorder(word, n)) %>%
ggplot() +
geom_col(aes(word, n), fill = my_colors[4]) +
theme(legend.position = "none",
plot.title = element_text(hjust = 0.5),
panel.grid.major = element_blank()) +
xlab("") +
ylab("Song Count") +
ggtitle("Most Frequently Used Words in Prince Lyrics") +
coord_flip()
Như trong đa số nhạc đại chúng, tình yêu có vẻ là chủ đề chung. Tôi sẽ không đưa ra nhiều giả định chỉ từ nhóm từ hàng đầu này, nhưng bạn chắc chắn có thể cảm nhận phần nào góc nhìn của nghệ sĩ, dù chưa phải bức tranh toàn cảnh. Trước khi đi sâu hơn, dành một phút để giải trí bằng hình ảnh.
Đám mây từ
Word cloud bị chê trong nhiều cộng đồng, và nếu không cẩn trọng, chúng có thể bị dùng sai ngữ cảnh. Tuy nhiên, vốn dĩ chúng ta là những sinh vật rất ưa trực quan, và có những trường hợp thực sự đem lại insight. Xem một vài ví dụ của Sandy McKee tại đây. Hãy dùng chúng có chừng mực.
Nhưng giờ, hãy xem gói mới wordcloud2 để làm vài thứ thú vị. Gói này cung cấp bộ sưu tập cloud sáng tạo tạo ra html widget. Bạn có thể di chuột qua một từ để xem tần suất xuất hiện của nó. (Gói này có thể render chậm trong rMarkdown và thường kén trình duyệt. Hy vọng sẽ được cải tiến.)
Và để vui hơn nữa, bạn có thể thêm chút “nghệ thuật dữ liệu” ...
wordcloud2(prince_words_counts[1:300, ], figPath = "guitar_icon.png",
color = "random-dark", size = 1.5)
letterCloud(prince_words_counts[1:300, ], word = "PRINCE", size = 2)
Từ phổ biến
Đến giờ bạn chỉ xem các từ hàng đầu trên tất cả bài. Điều gì xảy ra nếu tách theo mức chart? Có phải một số từ xuất hiện thường xuyên hơn trong các bài lọt bảng so với bài không vào bảng? Đây được coi là các từ phổ biến với xã hội.
Lưu ý trong đoạn mã dưới đây việc dùng slice(seq_len(n)) để lấy n từ đầu ở mỗi chart_level. Cách này khác với top_n() và tỏ ra phù hợp khi dùng faceting trong biểu đồ. (Luôn nhớ có nhiều cách để làm các mẹo như vậy.) Bạn cũng có thể dùng hàm row_number() để đảm bảo liệt kê từ theo đúng thứ tự trên biểu đồ. Mặc định ggplot() sẽ sắp từ theo bảng chữ cái và thực hành tốt là tự sắp trước khi vẽ.
popular_words <- prince_words_filtered %>%
group_by(chart_level) %>%
count(word, chart_level, sort = TRUE) %>%
slice(seq_len(8)) %>%
ungroup() %>%
arrange(chart_level,n) %>%
mutate(row = row_number())
popular_words %>%
ggplot(aes(row, n, fill = chart_level)) +
geom_col(show.legend = NULL) +
labs(x = NULL, y = "Song Count") +
ggtitle("Popular Words by Chart Level") +
theme_lyrics() +
facet_wrap(~chart_level, scales = "free") +
scale_x_continuous( # This handles replacement of row
breaks = popular_words$row, # notice need to reuse data frame
labels = popular_words$word) +
coord_flip()
Những nhận định bạn rút ra là gì?
Rõ ràng, các từ hàng đầu giữa các mức bảng rất, rất giống nhau. Điều này không có lợi cho kỳ vọng dự đoán một bài có thành công dựa trên ca từ! Nhưng bạn mới chỉ chạm bề mặt những gì có thể làm với khai phá văn bản, và sau đó là NLP và mô hình dự báo.
Từ vượt thời gian
Một số từ trong âm nhạc được coi là vượt thời gian. Từ vượt thời gian bền bỉ qua năm tháng, hấp dẫn đông đảo khán thính giả. Nếu bạn phân tích theo thập niên, các từ này sẽ nổi lên đầu. Dùng lọc, nhóm và tổng hợp để lấy các từ hàng đầu cho mỗi thập niên từ ca từ của Prince và xem từ nào có thể coi là vượt thời gian hay theo trào lưu. Bạn có thể dùng ggplot() với facet_wrap() để xem theo thập niên.
timeless_words <- prince_words_filtered %>
filter(decade != 'NA') %>%
group_by(decade) %>%
count(word, decade, sort = TRUE) %>%
slice(seq_len(8)) %>%
ungroup() %>%
arrange(decade,n) %>%
mutate(row = row_number())
timeless_words %>%
ggplot(aes(row, n, fill = decade)) +
geom_col(show.legend = NULL) +
labs(x = NULL, y = "Song Count") +
ggtitle("Timeless Words") +
theme_lyrics() +
facet_wrap(~decade, scales = "free", ncol = 5) +
scale_x_continuous( # This handles replacement of row
breaks = timeless_words$row, # notice need to reuse data frame
labels = timeless_words$word) +
coord_flip()
Rõ ràng love, time và girl là những từ vượt thời gian. Nhưng liệu có dễ phát hiện các từ theo trào lưu? Truth có “lên ngôi” vào đầu thế kỷ không? Hay đơn giản nó chỉ là từ lặp nhiều trong một số bài? Liệu tần suất có thực sự xác định chủ đề? Cách này có hiệu quả với ca từ như khi khai phá các văn bản khác, chẳng hạn phân tích các bài Diễn văn Thông điệp Liên bang (State of the Union)?
Độ dài từ
Độ dài từ là chủ đề thú vị với người viết nhạc. Từ càng dài càng khó gieo vần và “đóng khung” vào nhịp điệu. Biểu đồ histogram độ dài từ dưới đây đúng như bạn mong đợi, với vài ngoại lệ cực dài.
#unnest and remove undesirable words, but leave in stop and short words
prince_word_lengths <- prince %>%
unnest_tokens(word, lyrics) %>%
group_by(song,decade) %>%
distinct() %>%
filter(!word %in% undesirable_words) %>%
mutate(word_length = nchar(word))
prince_word_lengths %>%
count(word_length, sort = TRUE) %>%
ggplot(aes(word_length),
binwidth = 10) +
geom_histogram(aes(fill = ..count..),
breaks = seq(1,25, by = 2),
show.legend = FALSE) +
xlab("Word Length") +
ylab("Word Count") +
ggtitle("Word Length Distribution") +
theme(plot.title = element_text(hjust = 0.5),
panel.grid.minor = element_blank())
Những từ siêu dài đó là gì? Tôi nghĩ điều này xứng đáng có một word cloud giải trí! Lần này dựa trên độ dài từ thay vì tần suất. Xem nhé:
Văn bản có vốn từ càng phong phú thì đa dạng từ vựng càng cao. Vốn từ của bài hát là biểu hiện của số lượng từ độc nhất được dùng trong bài. Điều này có thể biểu diễn bằng một biểu đồ đơn giản của số từ độc nhất trung bình mỗi bài qua các năm. Bạn sẽ muốn token hóa tập dữ liệu gốc lần nữa nhưng giữ lại tất cả stop words và từ ngắn vì bạn quan tâm tới insight định lượng trong biểu đồ này.
Điều này có ý nghĩa gì? Trong vài thập niên qua, có xu hướng tăng nhẹ trong đa dạng ca từ của Prince. Nó tương quan thế nào với thành công trên bảng xếp hạng? Khó nói, nhưng hãy vẽ thêm mật độ và lịch sử bảng xếp hạng để so sánh.
Mật độ từ vựng
Nhớ rằng trong hướng dẫn này, mật độ từ vựng được định nghĩa là số từ độc nhất chia cho tổng số từ. Đây là chỉ báo mức lặp từ, vốn là công cụ quan trọng của người viết nhạc. Khi mật độ tăng, lặp lại giảm. (Lưu ý: điều này không hàm ý lặp theo chuỗi, vốn là một mẹo sáng tác khác.)
Xem mật độ từ vựng của Prince qua các năm. Với mật độ, tốt nhất là giữ tất cả từ, kể cả stop words. Vậy bắt đầu với tập dữ liệu gốc và unnest từ. Nhóm theo bài và năm và dùng n_distinct() và n() để tính mật độ. Pipe vào ggplot() với geom_smooth(). Thêm stat_smooth() với method="lm" để có đường làm mượt tuyến tính.
Để so sánh xu hướng, hãy tạo biểu đồ lịch sử xếp hạng (tức các bài thành công lọt bảng) và so với đa dạng và mật độ. Dùng grid.arrange() từ gridExtra để vẽ cạnh nhau.
Từ các biểu đồ trên, bạn thấy có sự tăng nhẹ trong đa dạng và mật độ ca từ của Prince theo thời gian. Xu hướng này so với toàn bộ nhạc đại chúng thế nào? Một nghiên cứu cho thấy các bài Hit có mật độ từ vựng giảm dần, hàm ý lặp lại tăng (tức "nhiều từ nhưng ít điều để nói"). Điều đó không diễn ra với các bài của Prince. Một nghiên cứu khác cho thấy trên toàn bộ nhạc đại chúng, đa dạng (từ độc nhất) tăng theo thời gian, phù hợp tích cực với âm nhạc của Prince. Có lẽ điều này gắn với sự đa dạng thể loại lớn hơn trong thứ hạng qua lịch sử. Thể loại là mảnh dữ liệu quan trọng không có trong tập này. Liệu lịch sử xếp hạng của ông giảm ở nhạc đại chúng nhưng tăng ở R&B/Rap?
Thử thách: Tôi để bạn tự suy ngẫm các kết quả này và khuyến khích bạn tìm những tập dữ liệu khác để vận dụng các kỹ năng này! Hãy nhớ, tương quan không hàm ý nhân quả.
TF-IDF
Phương pháp bạn dùng đến giờ nhìn vào toàn bộ tập dữ liệu, nhưng chưa xử lý cách định lượng mức quan trọng của các thuật ngữ trong một tài liệu so với toàn bộ sưu tập. Bạn đã xem tần suất thuật ngữ và loại bỏ stop words, nhưng đó có thể chưa phải cách tinh vi nhất.
Giới thiệu TF-IDF. TF là "term frequency" (tần suất thuật ngữ). IDF là "inverse document frequency" (nghịch đảo tần suất tài liệu), gán trọng số thấp cho từ dùng phổ biến và trọng số cao cho từ ít dùng trong tập văn bản. Khi kết hợp TF và IDF, mức quan trọng của một thuật ngữ được điều chỉnh theo mức độ hiếm của nó. Giả định đằng sau TF-IDF là thuật ngữ xuất hiện nhiều hơn trong một tài liệu nên được gán trọng số cao hơn, trừ khi nó cũng xuất hiện trong nhiều tài liệu. Công thức tóm tắt như sau:
Tần suất thuật ngữ (TF): Số lần một thuật ngữ xuất hiện trong một tài liệu
Tần suất tài liệu (DF): Số tài liệu chứa mỗi từ
Nghịch đảo tần suất tài liệu (IDF) = 1/DF
TF-IDF = TF * IDF
IDF của bất kỳ thuật ngữ nào vì vậy sẽ cao hơn cho các từ xuất hiện trong ít tài liệu hơn trong tập. Bạn có thể dùng cách tiếp cận mới này để xem các từ quan trọng nhất theo mức chart bằng hàm bind_tf_idf() của tidytext. Hàm này tính và gắn TF và IDF, cùng tích TF*IDF. Nó nhận một tập dữ liệu tidy văn bản với một hàng mỗi token (từ), mỗi tài liệu (bài). Bạn sẽ thấy kết quả ở các cột mới.
Vậy, lấy data frame Prince gốc và unnest token sang từ, loại từ không mong muốn, nhưng giữ stop words. Sau đó dùng bind_tf_idf() để chạy công thức và tạo cột mới.
## # A tibble: 6 x 6
## chart_level word n tf idf tf_idf
## <chr> <chr> <int> <dbl> <dbl> <dbl>
## 1 Uncharted that 622 0.010942420 0 0
## 2 Uncharted your 529 0.009306335 0 0
## 3 Uncharted what 496 0.008725789 0 0
## 4 Uncharted will 481 0.008461904 0 0
## 5 Uncharted this 464 0.008162834 0 0
## 6 Uncharted know 441 0.007758211 0 0
Giờ bạn thấy IDF và TF-IDF bằng 0 cho các từ cực kỳ phổ biến. (Về kỹ thuật, IDF sẽ là log tự nhiên của 1 nên bằng 0 cho các từ này.) Pipe kết quả ở trên vào arrange() và sắp giảm dần theo tf-idf. Thêm vài bước nữa và bạn sẽ thấy một cách nhìn khác về các từ trong ca từ của Prince.
top_popular_tfidf_words <- popular_tfidf_words %>%
arrange(desc(tf_idf)) %>%
mutate(word = factor(word, levels = rev(unique(word)))) %>%
group_by(chart_level) %>
slice(seq_len(8)) %>%
ungroup() %>%
arrange(chart_level, tf_idf) %>%
mutate(row = row_number())
top_popular_tfidf_words %>%
ggplot(aes(x = row, tf_idf,
fill = chart_level)) +
geom_col(show.legend = NULL) +
labs(x = NULL, y = "TF-IDF") +
ggtitle("Important Words using TF-IDF by Chart Level") +
theme_lyrics() +
facet_wrap(~chart_level, ncol = 3, scales = "free") +
scale_x_continuous( # This handles replacement of row
breaks = top_popular_tfidf_words$row, # notice need to reuse data frame
labels = top_popular_tfidf_words$word) +
coord_flip()
Wow, dùng TF-IDF thực sự cho ta một góc nhìn khác về các từ tiềm năng quan trọng. Tất nhiên, diễn giải còn mang tính chủ quan. Bạn có thấy mẫu hình nào không?
Tiếp theo, xem TF-IDF theo thời gian:
tfidf_words_decade <- prince %>%
unnest_tokens(word, lyrics) %>%
distinct() %>%
filter(!word %in% undesirable_words & decade != 'NA') %>%
filter(nchar(word) > 3) %>%
count(decade, word, sort = TRUE) %>%
ungroup() %>%
bind_tf_idf(word, decade, n) %>%
arrange(desc(tf_idf))
top_tfidf_words_decade <- tfidf_words_decade %>
group_by(decade) %>%
slice(seq_len(8)) %>%
ungroup() %>%
arrange(decade, tf_idf) %>%
mutate(row = row_number())
top_tfidf_words_decade %>%
ggplot(aes(x = row, tf_idf, fill = decade)) +
geom_col(show.legend = NULL) +
labs(x = NULL, y = "TF-IDF") +
ggtitle("Important Words using TF-IDF by Decade") +
theme_lyrics() +
facet_wrap(~decade,
ncol = 3, nrow = 2,
scales = "free") +
scale_x_continuous( # this handles replacement of row
breaks = top_tfidf_words_decade$row, # notice need to reuse data frame
labels = top_tfidf_words_decade$word) +
coord_flip()
Bây giờ bạn chắc chắn đang thu được insight sâu hơn. Các từ vượt thời gian không còn chiếm ưu thế. Bạn có bắt đầu thấy chủ đề hay đề tài hiện ra? Liệu điều đó thực sự có thể chỉ với vài từ mỗi nhóm? Bạn sẽ nâng tầm điều này trong hướng dẫn thứ hai (Phần Hai: Phân tích cảm xúc và Mô hình chủ đề với NLP).
Một word cloud nhanh với phương pháp này mang đến góc nhìn mới về các từ quan trọng trong ca từ của Prince và thú vị hơn một chút...
Trong nghiên cứu tình huống này, trước hết bạn nhìn lướt vào dữ liệu thực, chỉ ở mức cơ bản. Sau khi thực hiện một số điều kiện hóa như làm sạch dữ liệu và loại bỏ từ không mang thông tin, bạn bắt đầu phân tích khám phá ở cấp bài hát.
Tiếp theo, bạn đi sâu vào khai phá văn bản bằng cách unnest ca từ thành các từ đã token hóa để nhìn vào độ phức tạp ngôn từ. Kết quả cung cấp insight quan trọng cho các bước tiếp theo về phân tích cảm xúc và mô hình chủ đề.
Cuối cùng, bạn dùng phân tích TF-IDF để biểu diễn thông tin phía sau một từ trong tài liệu liên quan đến một kết quả quan tâm. Bạn có thể nghĩ đây là cách hay để xác định chủ đề trong âm nhạc, nhưng thực ra bạn mới chỉ có một nửa câu chuyện. Phần Hai sẽ đề cập đến phương pháp học không giám sát gọi là Phân bổ Dirichlet Tiềm ẩn (LDA). Như trong mọi khía cạnh của Khoa học Dữ liệu, có rất nhiều phương pháp để thu được insight. Và trong Phần Hai và Ba của nghiên cứu này, bạn sẽ bàn đến thêm các lựa chọn đó.
Hy vọng bạn cũng háo hức như tôi để tiếp tục hành trình từ phân tích khám phá đến phân tích cảm xúc, mô hình chủ đề và insight dự báo.
Cảm ơn đã đọc, hẹn gặp lại bạn trong hướng dẫn tiếp theo!
Đây là Phần Một trong loạt hướng dẫn gồm ba phần, trong đó bạn sẽ dùng R để thực hiện nhiều tác vụ phân tích trên một nghiên cứu tình huống về ca từ của nghệ sĩ huyền thoại Prince. Ba bài hướng dẫn bao gồm:
Giới thiệu
Ca từ có thể phản ánh góc nhìn của nghệ sĩ, nhưng các ca khúc phổ biến cho thấy xã hội muốn nghe gì. Phân tích ca từ không hề đơn giản. Vì cấu trúc của ca từ khác văn xuôi rất nhiều, nó đòi hỏi thận trọng trong giả định và lựa chọn kỹ thuật phân tích mang tính phân biệt độc đáo. Ca từ len lỏi vào đời sống và ảnh hưởng đến suy nghĩ của chúng ta một cách tinh vi. Khái niệm Lời bài hát dự báo (Predictive Lyrics) bắt đầu được nhắc đến nhiều và ngày càng xuất hiện trong các bài báo khoa học và luận văn. Nghiên cứu tình huống này sẽ chỉ chạm tới một vài khía cạnh của chủ đề đang nổi này.
Prince: The Artist
Để tôn vinh sự nghiệp phong phú, đa dạng đầy cảm hứng mà Prince để lại, bạn sẽ khám phá những thông điệp đôi khi hiển nhiên nhưng thường ẩn giấu trong ca từ của ông. Tuy nhiên, bạn không cần phải yêu thích âm nhạc của Prince để trân trọng ảnh hưởng của ông đối với sự phát triển của nhiều thể loại trên toàn cầu. Tạp chí Rolling Stone đã xếp Prince là nhạc sĩ sáng tác vĩ đại thứ 18 mọi thời đại, ngay sau Bob Dylan, John Lennon, Paul Simon, Joni Mitchell và Stevie Wonder. Phân tích ca từ đang dần xuất hiện trong cộng đồng khoa học dữ liệu khi khả năng dự đoán "Bài hát Hit" tiến gần hiện thực.
Trong hướng dẫn này, Phần Một của loạt bài, bạn sẽ sử dụng các kỹ thuật khai phá văn bản trên một tập ca từ theo khuôn khổ tidy text. Dữ liệu tidy có cấu trúc cụ thể: mỗi biến là một cột, mỗi quan sát là một hàng, và mỗi đơn vị quan sát là một bảng. Sau khi làm sạch và điều kiện hóa dữ liệu, bạn sẽ tạo thống kê mô tả và trực quan hóa khám phá khi quan sát các khía cạnh khác nhau trong ca từ của Prince.
Yêu cầu tiên quyết
Phần Một yêu cầu hiểu biết cơ bản về dữ liệu tidy — cụ thể là các gói như dplyr để biến đổi dữ liệu, ggplot2 để trực quan hóa, và toán tử %>% pipe có nguồn gốc từ gói magrittr. Mỗi hướng dẫn sẽ mô tả các công cụ bạn có thể dùng cho phân tích, nhưng có thể không đi sâu vào mọi bước. Bạn sẽ thấy nhiều bước thường được kết hợp bằng toán tử %>%. Vì đây cũng là một nghiên cứu tình huống, cần nhớ rằng mọi suy luận bạn đưa ra đều mang tính quan sát; do đó, tương quan không hàm ý quan hệ nhân quả.
Mẹo: để có kiến thức nền về các công cụ sử dụng, hai tài nguyên tốt là R for Data Science của Garrett Grolemund và Hadley Wickham; và Text Mining with R của Julia Silge và David Robinson.
Phần Hai và Ba
Trong một hướng dẫn riêng, Phần Hai, bạn sẽ thực hiện Phân tích cảm xúc và Mô hình chủ đề để nắm bắt tâm trạng và chủ đề tổng thể trong âm nhạc của Prince và cách chúng phản ánh góc nhìn xã hội. Bạn sẽ làm việc với một từ điển cảm xúc, đánh giá cảm xúc nhị phân và phân loại, vẽ xu hướng theo thời gian, và xem n-gram và liên kết từ. Bạn cũng sẽ dùng Xử lý ngôn ngữ tự nhiên (NLP) và các kỹ thuật phân cụm như Phân bổ Dirichlet Tiềm ẩn (LDA) và K-Means để gợi mở mô-típ trong ca từ.
Trong một hướng dẫn khác nữa, Phần Ba, bạn sẽ khép lại vòng tròn bằng cách dùng kết quả khám phá để giúp dự đoán bài hát phát hành vào thập niên nào, và thú vị hơn, liệu một bài hát có lọt vào bảng xếp hạng Billboard dựa trên ca từ hay không. Bạn sẽ dùng các công cụ máy học như Cây quyết định (rpart và C50), K-Láng giềng gần nhất (class), và Naive Bayes (e1071) để tạo bộ phân loại có thể nhận văn bản.
Cả ba phần sẽ dùng cùng một tập dữ liệu gồm ca từ của Prince, năm phát hành và vị trí trên bảng xếp hạng Billboard. Các kỹ thuật trong nghiên cứu này cũng có thể áp dụng cho nhiều loại văn bản khác. Thực tế, kết quả cho văn xuôi tiêu chuẩn thường dễ diễn giải hơn vì ca từ nói chung thường được thiết kế với thông điệp gián tiếp và sắc thái tinh tế.
Tóm lại, có rất nhiều phương pháp để phân tích ca từ. Các hướng dẫn này đề cập đến những phương pháp được tô đỏ trong hình dưới đây. Lưu ý hình này chỉ là đại diện cấp cao cho một bức tranh rất "mờ". Và không, tôi không nói bức ảnh đâu! Sự thật là các khía cạnh khác nhau của mô hình hóa và kỹ thuật máy học giờ đây khá nhập nhằng và không nhất thiết nằm gọn trong một ô như hình. Vậy nên hãy đeo kính 3D khi nhìn vào ảnh, có thể sẽ dễ hiểu hơn!
Mục tiêu
Bên cạnh việc học và thực hành kỹ năng mới, hướng dẫn này nhằm giải quyết các câu hỏi cơ bản về khái niệm phân tích ca từ. Những nghiên cứu gần đây cho thấy "chỉ số thông minh ca từ" có thể đang suy giảm trong nhạc đại chúng. Một số nghiên cứu còn gợi ý rằng từ ngữ trong các Bài hát No.1 phù hợp với trình độ đọc của học sinh lớp 3 ở Mỹ. Liệu có thể sử dụng khai phá văn bản, NLP, máy học và các phương pháp khoa học dữ liệu khác để soi chiếu chủ đề này? Liệu có thể xác định các chủ đề hấp dẫn xã hội dựa trên mức độ đón nhận của bài hát? Liệu có thể dự đoán một bài hát sẽ thành công chỉ dựa vào phân tích ca từ? Trong hướng dẫn đầu tiên này, bạn sẽ khảo sát độ phức tạp ngôn từ trong âm nhạc của Prince như một bài tập khám phá.
Câu hỏi
Trước khi bắt đầu, hãy nghĩ về điều bạn muốn khám phá. Những câu hỏi nào đáng quan tâm? Trước tiên bạn sẽ đi qua phần phân tích tập dữ liệu. Nó trông như thế nào? Có bao nhiêu bài hát? Ca từ có cấu trúc ra sao? Cần làm sạch và xử lý dữ liệu đến mức nào? Sự thật là gì? Tần suất từ thế nào và tại sao điều đó quan trọng? Về mặt kỹ thuật, bạn muốn hiểu và chuẩn bị dữ liệu cho phân tích cảm xúc, NLP và các mô hình máy học.
Dữ liệu
Một phương pháp phổ biến để lấy dữ liệu phục vụ khai phá văn bản là dùng gói rvest để quét nội dung từ web. Tôi đã quét được thông tin Bảng xếp hạng Billboard và ca từ của Prince từ nhiều trang, rồi ghép chúng theo tựa bài hát. Có chút xử lý vì quy ước đặt tên tiêu đề không nhất quán. Sau đó tôi đưa ra quyết định chủ quan là loại bỏ tất cả các bài không phải bản gốc, như remix, bản mở rộng, club mix, bản làm lại, v.v. Tôi cũng loại các album tuyển tập các Bài hát Hit để tránh lặp. Tôi làm sạch nhẹ và lưu kết quả vào tệp csv để dùng cho hướng dẫn này.
Vì Phần Một tập trung vào khai phá văn bản, tôi không đưa phần mã đó vào hướng dẫn này, nhưng tập dữ liệu có sẵn để bạn tải về tại đây nếu muốn làm theo.
Nạp các thư viện
Đọc dữ liệu
Có nhiều cách đọc dữ liệu từ tệp csv, nhưng tôi chọn dùng read.csv() để nạp một data frame gồm ca từ, năm phát hành và vị trí trên bảng xếp hạng Billboard. Lưu ý theo mặc định, R chuyển mọi chuỗi ký tự thành factor. Điều này có thể gây vấn đề về sau, nhưng bạn có thể xử lý bằng cách đặt tham số stringsAsFactors là FALSE. Giờ hãy xem dữ liệu...
prince_orig <- read.csv("prince_raw_data.csv", stringsAsFactors = FALSE)Bạn có thể dùng hàm names() để xem các cột trong data frame:
Vì tôi tạo tệp này nên tôi biết X chỉ là số hàng và text là ca từ thực tế. Các trường cần thiết khác gồm song, year và peak (thể hiện vị trí trên bảng xếp hạng Billboard). US.Pop và US.R.B là vị trí cao nhất trên bảng xếp hạng của Mỹ (Pop và R&B), nên giữ lại các trường này và tạm bỏ các trường khác.
Làm điều này bằng cách lấy tập dữ liệu gốc prince_orig và pipe vào select() bằng %>%. Cách này giúp bạn đọc mã từ trái sang phải.
Đồng thời, lưu ý rằng select() cho phép bạn đổi tên cột trong một bước. Vì vậy hãy đặt text thành lyrics và đổi tên các cột US theo phong cách tidyverse dùng "_" thay vì ".". Sau đó lưu kết quả vào prince, sẽ dùng xuyên suốt loạt bài. dplyr cung cấp hàm glimpse() giúp xem dữ liệu ở dạng chuyển vị thuận tiện.
prince <- prince_orig %>% select(lyrics = text, song, year, album, peak, us_pop = US.Pop, us_rnb = US.R.B) glimpse(prince[139,])Câu hỏi hiển nhiên đầu tiên: có bao nhiêu quan sát và cột?
Dùng hàm dim(), bạn thấy có 7 cột và 824 quan sát. Mỗi quan sát là một bài hát. Như tôi đã nói... mắn bút!
Nhìn vào cột lyrics của một bài hát, bạn có thể thấy cấu trúc của ca từ.
Có rất nhiều cơ hội để làm sạch dữ liệu, vậy hãy bắt đầu.
Điều kiện hóa dữ liệu
Làm sạch cơ bản
Có nhiều phương pháp để điều kiện hóa dữ liệu. Một lựa chọn là chuyển data frame thành Corpus và Document Term Matrix bằng gói tm rồi dùng hàm tm_map() để làm sạch, nhưng hướng dẫn này sẽ bám vào những điều cơ bản và dùng gsub() và các hàm apply() để làm phần nặng nhọc.
Trước hết, loại bỏ các dạng viết tắt khó chịu bằng cách tạo một hàm nhỏ xử lý hầu hết các tình huống bằng gsub(), rồi áp dụng hàm đó cho toàn bộ ca từ.
# function to expand contractions in an English-language source fix.contractions <- function(doc) { # "won't" is a special case as it does not expand to "wo not" doc <- gsub("won't", "will not", doc) doc <- gsub("can't", "can not", doc) doc <- gsub("n't", " not", doc) doc <- gsub("'ll", " will", doc) doc <- gsub("'re", " are", doc) doc <- gsub("'ve", " have", doc) doc <- gsub("'m", " am", doc) doc <- gsub("'d", " would", doc) # 's could be 'is' or could be possessive: it has no expansion doc <- gsub("'s", "", doc) return(doc) } # fix (expand) contractions prince$lyrics <- sapply(prince$lyrics, fix.contractions)Bạn cũng sẽ thấy các ký tự đặc biệt làm đục văn bản. Có thể loại bỏ bằng gsub() và một biểu thức chính quy đơn giản. Lưu ý cần mở rộng viết tắt trước khi làm bước này!
# function to remove special characters removeSpecialChars <- function(x) gsub("[^a-zA-Z0-9 ]", " ", x) # remove special characters prince$lyrics <- sapply(prince$lyrics, removeSpecialChars)Để nhất quán, hãy chuyển mọi thứ về chữ thường với hàm tolower().
Giờ xem lại ca từ sẽ thấy phiên bản sạch hơn nhiều so với văn bản thô ban đầu.
Một bước phổ biến khác khi điều kiện hóa dữ liệu cho khai phá văn bản là stemming, hay tách từ về gốc nghĩa. Đây là một chủ đề riêng có thể bàn sau. Hiện tại, hãy xem tóm tắt của data frame prince.
lyrics song year album Length:824 Length:824 Min. :1978 Length:824 Class :character Class :character 1st Qu.:1989 Class :character Mode :character Mode :character Median :1996 Mode :character Mean :1995 3rd Qu.:1999 Max. :2015 NA's :495 peak us_pop us_rnb Min. : 0.00 Length:824 Length:824 1st Qu.: 2.00 Class :character Class :character Median : 7.00 Mode :character Mode :character Mean :15.48 3rd Qu.:19.00 Max. :88.00 NA's :751Như bạn thấy, có 37 năm bài hát, và bài xếp hạng thấp nhất (tồn tại trong dữ liệu) ở vị trí 88. Bạn cũng thấy khá nhiều NA cho year và peak. Vì bạn sẽ làm các loại phân tích khác nhau, hãy giữ toàn bộ tập dữ liệu trong data frame prince và chỉ lọc khi cần.
Thêm một vài trường
Vì một trong các câu hỏi mục tiêu là tìm xu hướng bài hát theo thời gian, và tập dữ liệu có năm phát hành, bạn có thể tạo “rổ” và gom năm thành thập niên. Dùng động từ mutate() của dplyr để tạo trường decade mới. Một cách là dùng ifelse() kết hợp toán tử %in% để lọc theo năm và phân loại bài hát vào các thập niên. Sau đó lưu lại vào prince (tức là thêm trường mới).
#create the decade column prince <- prince %>% mutate(decade = ifelse(prince$year %in% 1978:1979, "1970s", ifelse(prince$year %in% 1980:1989, "1980s", ifelse(prince$year %in% 1990:1999, "1990s", ifelse(prince$year %in% 2000:2009, "2000s", ifelse(prince$year %in% 2010:2015, "2010s", "NA"))))))Bạn có thể làm tương tự cho chart_level, biểu thị một bài đạt đỉnh Top 10, Top 100, hay không vào bảng (uncharted). Chúng loại trừ lẫn nhau, nên Top 100 không bao gồm Top 10.
#create the chart level column prince <- prince %>% mutate(chart_level = ifelse(prince$peak %in% 1:10, "Top 10", ifelse(prince$peak %in% 11:100, "Top 100", "Uncharted")))Thêm vào đó, tạo trường nhị phân charted cho biết một bài có lọt vào bảng xếp hạng Billboard hay không (tức là bài phổ biến). Dùng write.csv() để lưu cho các hướng dẫn sau.
#create binary field called charted showing if a song hit the charts at all prince <- prince %>% mutate(charted = ifelse(prince$peak %in% 1:100, "Charted", "Uncharted")) #save the new dataset to .csv for use in later tutorials write.csv(prince, file = "prince_new.csv")Thống kê mô tả
Để tùy biến biểu đồ, tôi thích tạo một danh sách màu riêng để giữ tính nhất quán. Có rất nhiều nơi trên web cung cấp các màu phân biệt qua mã hex như dưới đây. Nếu bạn có sở thích cho biểu đồ, bạn cũng có thể tạo một theme riêng để áp dụng cho ggplot() khi cần.
#define some colors to use throughout my_colors <- c("#E69F00", "#56B4E9", "#009E73", "#CC79A7", "#D55E00") theme_lyrics <- function() { theme(plot.title = element_text(hjust = 0.5), axis.text.x = element_blank(), axis.ticks = element_blank(), panel.grid.major = element_blank(), panel.grid.minor = element_blank(), legend.position = "none") }Trước khi vào khai phá văn bản, hãy bắt đầu với cái nhìn cơ bản về dữ liệu ở cấp bài hát. Giờ là lúc trực quan hóa số bài Prince phát hành theo thập niên. Nhắc lại, sự nghiệp chuyên nghiệp của ông bắt đầu từ 1978 tới 2015 (như thống kê summary() ở trên). Nhưng vì ta nhìn xu hướng, và tập dữ liệu có khá nhiều giá trị trống cho year, bạn sẽ muốn lọc bỏ tất cả NA của năm phát hành cho biểu đồ đầu tiên.
Thống kê bài hát
Dùng các hàm filter(), group_by() và summarise() của dplyr, bạn có thể nhóm theo thập niên rồi đếm số bài. Hàm n() là một trong nhiều hàm tổng hợp hữu ích khi dùng với summarise() trên dữ liệu đã nhóm. Sau đó dùng ggplot() và geom_bar() để tạo biểu đồ cột và tô màu cột theo danh mục charted.
prince %>% filter(decade != "NA") %>% group_by(decade, charted) %>% summarise(number_of_songs = n()) %>% ggplot() + geom_bar(aes(x = decade, y = number_of_songs, fill = charted), stat = "identity") + theme(plot.title = element_text(hjust = 0.5), legend.title = element_blank(), panel.grid.minor = element_blank()) + ggtitle("Released Songs") + labs(x = NULL, y = "Song Count")Biểu đồ cho thấy thập niên hoạt động mạnh nhất của ông là thập niên 1990.
Giờ hãy tạo biểu đồ tương tự với chart_level.
Nhớ group_by() cả decade và chart_level để thấy xu hướng.
Trong biểu đồ này, bạn chỉ xem các bài lọt bảng nên lọc mọi thứ khác bằng peak > 0. Pipe đối tượng group_by vào summarise() dùng n() để đếm số bài. Khi lưu vào biến, bạn có thể pipe sang ggplot() để vẽ biểu đồ cột đơn giản.
charted_songs_over_time <- prince %>% filter(peak > 0) %>% group_by(decade, chart_level) %>% summarise(number_of_songs = n()) charted_songs_over_time %>% ggplot() + geom_bar(aes(x = decade, y = number_of_songs, fill = chart_level), stat = "identity") + theme(plot.title = element_text(hjust = 0.5), legend.title = element_blank(), panel.grid.minor = element_blank()) + labs(x = NULL, y = "Song Count") + ggtitle("Charted Songs")Nhận định
Lưu ý rằng trong số các bài lọt bảng của Prince, đa số vào Top 10. Nhưng thú vị hơn là thập niên sáng tác nhiều nhất là 1990s, tuy nhiên số hit lọt bảng lại nhiều hơn ở 1980s. Tại sao vậy? Hãy ghi nhớ câu hỏi này khi bạn đi tiếp phần khai phá văn bản.
Để tận dụng sức mạnh của toàn bộ tập dữ liệu cho phân tích ca từ, bạn có thể bỏ các tham chiếu đến cấp bảng xếp hạng và năm phát hành để có tập bài lớn hơn nhiều để khai phá. Xem thử:
#look at the full data set at your disposal prince %>% group_by(decade, chart_level) %>% summarise(number_of_songs = n()) %>% ggplot() + geom_bar(aes(x = decade, y = number_of_songs, fill = chart_level), stat = "identity") + theme(plot.title = element_text(hjust = 0.5), legend.title = element_blank(), panel.grid.minor = element_blank()) + labs(x = NULL, y = "Song Count") + ggtitle("All Songs in Data")Như bạn thấy, Prince viết hàng trăm bài không có ngày phát hành trong dữ liệu. Với phân tích cảm xúc hay khám phá, bạn có thể dùng toàn bộ dữ liệu, nhưng với xu hướng theo thời gian bạn chỉ làm việc với tập nhỏ hơn nhiều. Điều này chấp nhận được — chỉ cần lưu ý.
Các bài No. 1!
Với những người hâm mộ Prince, dưới đây là cái nhìn nhanh về các bài đứng No. 1 trên bảng xếp hạng. (Lưu ý bạn có thể dùng kable() và kable_styling() từ các gói knitr và kableExtra và color_tile() từ formattable để tạo đầu ra HTML đẹp.)
library(knitr) # for dynamic reporting library(kableExtra) # create a nicely formated HTML table library(formattable) # for the color_tile function prince %>% filter(peak == "1") %>% select(year, song, peak) %>% arrange(year) %>% mutate(year = color_tile("lightblue", "lightgreen")(year)) %>% mutate(peak = color_tile("lightgreen", "lightgreen")(peak)) %>% kable("html", escape = FALSE, align = "c", caption = "Prince's No. 1 Songs") %>% kable_styling(bootstrap_options = c("striped", "condensed", "bordered"), full_width = FALSE)Khai phá văn bản
Khai phá văn bản cũng có thể hiểu là phân tích văn bản. Mục tiêu là khám phá thông tin liên quan có thể chưa biết hoặc bị chôn vùi dưới những điều hiển nhiên. Xử lý ngôn ngữ tự nhiên (NLP) là một phương pháp dùng trong khai phá văn bản. Nó cố gắng giải mã mơ hồ trong ngôn ngữ viết bằng cách tách token, phân cụm, trích thực thể và mối quan hệ từ, và dùng thuật toán để xác định chủ đề và định lượng thông tin chủ quan. Bạn sẽ bắt đầu bằng cách phân tích khái niệm độ phức tạp từ vựng.
Độ phức tạp từ vựng có thể mang nghĩa khác nhau theo ngữ cảnh, nhưng tạm thời, giả định rằng nó có thể mô tả bằng tổ hợp các thước đo sau:
Định dạng Tidy Text
Để bắt đầu phân tích, bạn cần tách ca từ thành từng từ riêng lẻ và bắt đầu khai phá thông tin. Quy trình này gọi là tokenization.
Định dạng dữ liệu và Tokenization
Hãy nhớ rằng có nhiều phương pháp và định dạng dữ liệu có thể dùng để khai phá văn bản:
Nhưng trước khi token hóa, còn một bước làm sạch dữ liệu. Nhiều ca từ khi chép lại có các cụm như "Repeat Chorus", hoặc nhãn như "Bridge" và "Verse". Cũng có rất nhiều từ không mong muốn khác làm nhiễu kết quả. Sau một số phân tích trước, tôi chọn ra vài từ muốn loại bỏ.
Dưới đây là danh sách các từ thừa cần loại bỏ thủ công:
undesirable_words <- c("prince", "chorus", "repeat", "lyrics", "theres", "bridge", "fe0f", "yeah", "baby", "alright", "wanna", "gonna", "chorus", "verse", "whoa", "gotta", "make", "miscellaneous", "2", "4", "ooh", "uurh", "pheromone", "poompoom", "3121", "matic", " ai ", " ca ", " la ", "hey", " na ", " da ", " uh ", " tin ", " ll", "transcription", "repeats")Để unnest token, dùng thư viện tidytext đã được nạp. Giờ bạn có thể tận dụng khả năng của dplyr và nối nhiều bước với nhau.
Theo khuôn khổ tidy text, bạn cần vừa tách văn bản thành token riêng lẻ (tokenization) vừa chuyển đổi sang cấu trúc dữ liệu tidy. Để làm điều này, dùng hàm unnest_tokens() của tidytext. unnest_tokens() cần ít nhất hai đối số: tên cột đầu ra sẽ được tạo khi bung văn bản vào đó ("word" trong trường hợp này), và cột đầu vào đang giữ văn bản hiện tại (tức lyrics).
Bạn có thể lấy tập prince và pipe vào unnest_tokens() rồi loại bỏ stop words. Stop words là gì? Bạn biết quá rõ rồi. Chúng là các từ quá phổ biến có thể không thêm ý nghĩa cho kết quả. Có nhiều danh sách để chọn, nhưng ở đây bạn sẽ dùng từ điển stop_words từ gói tidytext.
Dùng sample() để hiển thị danh sách ngẫu nhiên các stop words này và head() để giới hạn 15 từ.
Vậy, sau khi bạn token hóa ca từ thành các từ, bạn có thể dùng anti_join() của dplyr để loại bỏ stop words. Tiếp theo, loại các từ không mong muốn đã định nghĩa ở trên bằng filter() của dplyr với toán tử %in%. Sau đó dùng distinct() để loại bản ghi trùng. Cuối cùng, loại mọi từ có ít hơn bốn ký tự. Đây là quyết định chủ quan khác, nhưng trong ca từ, chúng thường là thán từ như "yea" và "hey". Sau đó lưu kết quả vào prince_words_filtered.
Lưu ý: về sau, prince_words_filtered là phiên bản tidy text của data frame prince đã loại 1) stop words, 2) từ không mong muốn, và 3) từ dài 1-3 ký tự. Bạn sẽ dùng tập này trong một số, nhưng không phải tất cả các phân tích.
Lưu ý stop_words có cột word, và một cột mới tên word đã được tạo bởi unnest_tokens(), nên anti_join() tự động nối theo cột word.
Giờ bạn có thể xem lớp và kích thước cấu trúc dữ liệu tidy mới:
prince_words_filtered là một data frame với 36916 từ (không phải từ duy nhất) và 10 cột. Đây là ảnh chụp nhanh: (Tôi chọn một từ và giới hạn ở 10 bài, rồi dùng select() để in các trường thú vị theo thứ tự, lại dùng knitr để định dạng). Điều này cho thấy cấu trúc dữ liệu tidy đã token hóa, chưa tổng hợp.
prince_words_filtered %>% filter(word == "race") %>% select(word, song, year, peak, decade, chart_level, charted) %>% arrange() %>% top_n(10,song) %>% mutate(song = color_tile("lightblue","lightblue")(song)) %>% mutate(word = color_tile("lightgreen","lightgreen")(word)) %>% kable("html", escape = FALSE, align = "c", caption = "Tokenized Format Example") %>% kable_styling(bootstrap_options = c("striped", "condensed", "bordered"), full_width = FALSE)Bạn thấy mỗi hàng chứa một từ riêng lẻ, sẽ lặp lại cho mỗi bài mà nó xuất hiện.
Tần suất từ
Trong âm nhạc, tần suất từ riêng lẻ rất quan trọng, dù là lặp lại hay hiếm gặp. Cả hai đều ảnh hưởng tới khả năng ghi nhớ của cả bài hát. Một câu hỏi người viết nhạc có thể muốn biết là liệu có tương quan giữa tần suất từ và bài hit hay không. Giờ bạn muốn đưa định dạng tidy tiến thêm một bước và lấy số đếm từ theo từng bài.
Để khảo sát điều này trong ca từ của Prince, hãy tạo biểu đồ histogram thể hiện phân phối số lượng từ, nhóm theo bài, theo vị trí trên Bảng xếp hạng Billboard. Unnest ca từ prince lần nữa mà không lọc từ nào để có số đếm chân thực về tần suất từ trên các bài. Lại dùng group_by() và summarise() để lấy số đếm. Sau đó dùng arrange() của dplyr để sắp theo count. Trước hết, xem các số đếm cao nhất, rồi dùng ggplot() để vẽ histogram.
full_word_count <- prince %>% unnest_tokens(word, lyrics) %>% group_by(song,chart_level) %>% summarise(num_words = n()) %>% arrange(desc(num_words)) full_word_count[1:10,] %>% ungroup(num_words, song) %>% mutate(num_words = color_bar("lightblue")(num_words)) %>% mutate(song = color_tile("lightpink","lightpink")(song)) %>% kable("html", escape = FALSE, align = "c", caption = "Songs With Highest Word Count") %>% kable_styling(bootstrap_options = c("striped", "condensed", "bordered"), full_width = FALSE)full_word_count %>% ggplot() + geom_histogram(aes(x = num_words, fill = chart_level )) + ylab("Song Count") + xlab("Word Count per Song") + ggtitle("Word Count Distribution") + theme(plot.title = element_text(hjust = 0.5), legend.title = element_blank(), panel.grid.minor.y = element_blank())Hãy chú ý nó lệch phải. Với bản chất của việc chép ca từ, tôi nghi ngờ lỗi nhập liệu. Vì tò mò, hãy xem bài Top 10 có hơn 800 từ đó.
full_word_count %>% filter(chart_level == 'Top 10' & num_words > 800) %>% left_join(prince_orig, by = "song") %>% select(Song = song, "Word Count" = num_words, "Peak Position" = peak, "US Pop" = US.Pop, "US R&B" = US.R.B, Canada = CA, Ireland = IR) %>% kable("html", escape = FALSE) %>% kable_styling(bootstrap_options = c("striped", "condensed", "bordered"))Tôi đã tìm hiểu và phát hiện bài này có nghệ sĩ khách mời thể hiện phần rap. Điều đó giải thích rồi! Nhớ rằng tập dữ liệu này bao phủ bảng Pop và R&B toàn cầu, nên thể loại và địa lý có thể ảnh hưởng lớn tới giả định của bạn. Lưu ý xếp hạng US Pop thấp hơn nhiều so với các bảng khác, ví dụ đạt đỉnh 5 ở Canada. Chỉ là điều cần ghi nhớ.
Thử thách: Nếu bạn muốn tự phân tích âm nhạc, bạn có thể xem The Million Song Dataset có hơn 50 đặc trưng (ví dụ: tempo, độ lớn âm, khả năng khiêu vũ, v.v.) cho gần 50.000 nghệ sĩ! Thêm đặc trưng âm nhạc vào ca từ sẽ cho phân tích rất toàn diện.
Từ phổ biến nhất
Để đánh giá đơn giản các từ được dùng thường xuyên nhất trong toàn bộ ca từ, bạn có thể dùng count() và top_n() để lấy n từ hàng đầu từ tập dữ liệu sạch đã lọc. Sau đó dùng reorder() để sắp từ theo số đếm và dùng mutate() của dplyr để gán lại giá trị đã sắp cho word. Điều này giúp ggplot() hiển thị đẹp.
prince_words_filtered %>% count(word, sort = TRUE) %>% top_n(10) %>% ungroup() %>% mutate(word = reorder(word, n)) %>% ggplot() + geom_col(aes(word, n), fill = my_colors[4]) + theme(legend.position = "none", plot.title = element_text(hjust = 0.5), panel.grid.major = element_blank()) + xlab("") + ylab("Song Count") + ggtitle("Most Frequently Used Words in Prince Lyrics") + coord_flip()Như trong đa số nhạc đại chúng, tình yêu có vẻ là chủ đề chung. Tôi sẽ không đưa ra nhiều giả định chỉ từ nhóm từ hàng đầu này, nhưng bạn chắc chắn có thể cảm nhận phần nào góc nhìn của nghệ sĩ, dù chưa phải bức tranh toàn cảnh. Trước khi đi sâu hơn, dành một phút để giải trí bằng hình ảnh.
Đám mây từ
Word cloud bị chê trong nhiều cộng đồng, và nếu không cẩn trọng, chúng có thể bị dùng sai ngữ cảnh. Tuy nhiên, vốn dĩ chúng ta là những sinh vật rất ưa trực quan, và có những trường hợp thực sự đem lại insight. Xem một vài ví dụ của Sandy McKee tại đây. Hãy dùng chúng có chừng mực.
Nhưng giờ, hãy xem gói mới wordcloud2 để làm vài thứ thú vị. Gói này cung cấp bộ sưu tập cloud sáng tạo tạo ra html widget. Bạn có thể di chuột qua một từ để xem tần suất xuất hiện của nó. (Gói này có thể render chậm trong rMarkdown và thường kén trình duyệt. Hy vọng sẽ được cải tiến.)
Và để vui hơn nữa, bạn có thể thêm chút “nghệ thuật dữ liệu” ...
wordcloud2(prince_words_counts[1:300, ], figPath = "guitar_icon.png", color = "random-dark", size = 1.5) letterCloud(prince_words_counts[1:300, ], word = "PRINCE", size = 2)Từ phổ biến
Đến giờ bạn chỉ xem các từ hàng đầu trên tất cả bài. Điều gì xảy ra nếu tách theo mức chart? Có phải một số từ xuất hiện thường xuyên hơn trong các bài lọt bảng so với bài không vào bảng? Đây được coi là các từ phổ biến với xã hội.
Lưu ý trong đoạn mã dưới đây việc dùng slice(seq_len(n)) để lấy n từ đầu ở mỗi chart_level. Cách này khác với top_n() và tỏ ra phù hợp khi dùng faceting trong biểu đồ. (Luôn nhớ có nhiều cách để làm các mẹo như vậy.) Bạn cũng có thể dùng hàm row_number() để đảm bảo liệt kê từ theo đúng thứ tự trên biểu đồ. Mặc định ggplot() sẽ sắp từ theo bảng chữ cái và thực hành tốt là tự sắp trước khi vẽ.
popular_words <- prince_words_filtered %>% group_by(chart_level) %>% count(word, chart_level, sort = TRUE) %>% slice(seq_len(8)) %>% ungroup() %>% arrange(chart_level,n) %>% mutate(row = row_number()) popular_words %>% ggplot(aes(row, n, fill = chart_level)) + geom_col(show.legend = NULL) + labs(x = NULL, y = "Song Count") + ggtitle("Popular Words by Chart Level") + theme_lyrics() + facet_wrap(~chart_level, scales = "free") + scale_x_continuous( # This handles replacement of row breaks = popular_words$row, # notice need to reuse data frame labels = popular_words$word) + coord_flip()Những nhận định bạn rút ra là gì?
Rõ ràng, các từ hàng đầu giữa các mức bảng rất, rất giống nhau. Điều này không có lợi cho kỳ vọng dự đoán một bài có thành công dựa trên ca từ! Nhưng bạn mới chỉ chạm bề mặt những gì có thể làm với khai phá văn bản, và sau đó là NLP và mô hình dự báo.
Từ vượt thời gian
Một số từ trong âm nhạc được coi là vượt thời gian. Từ vượt thời gian bền bỉ qua năm tháng, hấp dẫn đông đảo khán thính giả. Nếu bạn phân tích theo thập niên, các từ này sẽ nổi lên đầu. Dùng lọc, nhóm và tổng hợp để lấy các từ hàng đầu cho mỗi thập niên từ ca từ của Prince và xem từ nào có thể coi là vượt thời gian hay theo trào lưu. Bạn có thể dùng ggplot() với facet_wrap() để xem theo thập niên.
timeless_words <- prince_words_filtered %> filter(decade != 'NA') %>% group_by(decade) %>% count(word, decade, sort = TRUE) %>% slice(seq_len(8)) %>% ungroup() %>% arrange(decade,n) %>% mutate(row = row_number()) timeless_words %>% ggplot(aes(row, n, fill = decade)) + geom_col(show.legend = NULL) + labs(x = NULL, y = "Song Count") + ggtitle("Timeless Words") + theme_lyrics() + facet_wrap(~decade, scales = "free", ncol = 5) + scale_x_continuous( # This handles replacement of row breaks = timeless_words$row, # notice need to reuse data frame labels = timeless_words$word) + coord_flip()Rõ ràng love, time và girl là những từ vượt thời gian. Nhưng liệu có dễ phát hiện các từ theo trào lưu? Truth có “lên ngôi” vào đầu thế kỷ không? Hay đơn giản nó chỉ là từ lặp nhiều trong một số bài? Liệu tần suất có thực sự xác định chủ đề? Cách này có hiệu quả với ca từ như khi khai phá các văn bản khác, chẳng hạn phân tích các bài Diễn văn Thông điệp Liên bang (State of the Union)?
Độ dài từ
Độ dài từ là chủ đề thú vị với người viết nhạc. Từ càng dài càng khó gieo vần và “đóng khung” vào nhịp điệu. Biểu đồ histogram độ dài từ dưới đây đúng như bạn mong đợi, với vài ngoại lệ cực dài.
#unnest and remove undesirable words, but leave in stop and short words prince_word_lengths <- prince %>% unnest_tokens(word, lyrics) %>% group_by(song,decade) %>% distinct() %>% filter(!word %in% undesirable_words) %>% mutate(word_length = nchar(word)) prince_word_lengths %>% count(word_length, sort = TRUE) %>% ggplot(aes(word_length), binwidth = 10) + geom_histogram(aes(fill = ..count..), breaks = seq(1,25, by = 2), show.legend = FALSE) + xlab("Word Length") + ylab("Word Count") + ggtitle("Word Length Distribution") + theme(plot.title = element_text(hjust = 0.5), panel.grid.minor = element_blank())Những từ siêu dài đó là gì? Tôi nghĩ điều này xứng đáng có một word cloud giải trí! Lần này dựa trên độ dài từ thay vì tần suất. Xem nhé:
wc <- prince_word_lengths %>% ungroup() %>% select(word, word_length) %>% distinct() %>% arrange(desc(word_length)) wordcloud2(wc[1:300, ], size = .15, minSize = .0005, ellipticity = .3, rotateRatio = 1, fontWeight = "bold")Đa dạng từ vựng
Văn bản có vốn từ càng phong phú thì đa dạng từ vựng càng cao. Vốn từ của bài hát là biểu hiện của số lượng từ độc nhất được dùng trong bài. Điều này có thể biểu diễn bằng một biểu đồ đơn giản của số từ độc nhất trung bình mỗi bài qua các năm. Bạn sẽ muốn token hóa tập dữ liệu gốc lần nữa nhưng giữ lại tất cả stop words và từ ngắn vì bạn quan tâm tới insight định lượng trong biểu đồ này.
lex_diversity_per_year <- prince %>% filter(decade != "NA") %>% unnest_tokens(word, lyrics) %>% group_by(song,year) %>% summarise(lex_diversity = n_distinct(word)) %>% arrange(desc(lex_diversity)) diversity_plot <- lex_diversity_per_year %>% ggplot(aes(year, lex_diversity)) + geom_point(color = my_colors[3], alpha = .4, size = 4, position = "jitter") + stat_smooth(color = "black", se = FALSE, method = "lm") + geom_smooth(aes(x = year, y = lex_diversity), se = FALSE, color = "blue", lwd = 2) + ggtitle("Lexical Diversity") + xlab("") + ylab("") + scale_color_manual(values = my_colors) + theme_classic() + theme_lyrics() diversity_plotĐiều này có ý nghĩa gì? Trong vài thập niên qua, có xu hướng tăng nhẹ trong đa dạng ca từ của Prince. Nó tương quan thế nào với thành công trên bảng xếp hạng? Khó nói, nhưng hãy vẽ thêm mật độ và lịch sử bảng xếp hạng để so sánh.
Mật độ từ vựng
Nhớ rằng trong hướng dẫn này, mật độ từ vựng được định nghĩa là số từ độc nhất chia cho tổng số từ. Đây là chỉ báo mức lặp từ, vốn là công cụ quan trọng của người viết nhạc. Khi mật độ tăng, lặp lại giảm. (Lưu ý: điều này không hàm ý lặp theo chuỗi, vốn là một mẹo sáng tác khác.)
Xem mật độ từ vựng của Prince qua các năm. Với mật độ, tốt nhất là giữ tất cả từ, kể cả stop words. Vậy bắt đầu với tập dữ liệu gốc và unnest từ. Nhóm theo bài và năm và dùng n_distinct() và n() để tính mật độ. Pipe vào ggplot() với geom_smooth(). Thêm stat_smooth() với method="lm" để có đường làm mượt tuyến tính.
lex_density_per_year <- prince %>% filter(decade != "NA") %>% unnest_tokens(word, lyrics) %>% group_by(song,year) %>% summarise(lex_density = n_distinct(word)/n()) %>% arrange(desc(lex_density)) density_plot <- lex_density_per_year %>% ggplot(aes(year, lex_density)) + geom_point(color = my_colors[4], alpha = .4, size = 4, position = "jitter") + stat_smooth(color = "black", se = FALSE, method = "lm") + geom_smooth(aes(x = year, y = lex_density), se = FALSE, color = "blue", lwd = 2) + ggtitle("Lexical Density") + xlab("") + ylab("") + scale_color_manual(values = my_colors) + theme_classic() + theme_lyrics() density_plotĐể so sánh xu hướng, hãy tạo biểu đồ lịch sử xếp hạng (tức các bài thành công lọt bảng) và so với đa dạng và mật độ. Dùng grid.arrange() từ gridExtra để vẽ cạnh nhau.
chart_history <- prince %>% filter(peak > 0) %>% group_by(year, chart_level) %>% summarise(number_of_songs = n()) %>% ggplot(aes(year, number_of_songs)) + geom_point(color = my_colors[5], alpha = .4, size = 4, position = "jitter") + geom_smooth(aes(x = year, y = number_of_songs), se = FALSE, method = "lm", color = "black" ) + geom_smooth(aes(x = year, y = number_of_songs), se = FALSE, color = "blue", lwd = 2) + ggtitle("Chart History") + xlab("") + ylab("") + scale_color_manual(values = my_colors) + theme_classic() + theme_lyrics() grid.arrange(diversity_plot, density_plot, chart_history, ncol = 3)Từ các biểu đồ trên, bạn thấy có sự tăng nhẹ trong đa dạng và mật độ ca từ của Prince theo thời gian. Xu hướng này so với toàn bộ nhạc đại chúng thế nào? Một nghiên cứu cho thấy các bài Hit có mật độ từ vựng giảm dần, hàm ý lặp lại tăng (tức "nhiều từ nhưng ít điều để nói"). Điều đó không diễn ra với các bài của Prince. Một nghiên cứu khác cho thấy trên toàn bộ nhạc đại chúng, đa dạng (từ độc nhất) tăng theo thời gian, phù hợp tích cực với âm nhạc của Prince. Có lẽ điều này gắn với sự đa dạng thể loại lớn hơn trong thứ hạng qua lịch sử. Thể loại là mảnh dữ liệu quan trọng không có trong tập này. Liệu lịch sử xếp hạng của ông giảm ở nhạc đại chúng nhưng tăng ở R&B/Rap?
Thử thách: Tôi để bạn tự suy ngẫm các kết quả này và khuyến khích bạn tìm những tập dữ liệu khác để vận dụng các kỹ năng này! Hãy nhớ, tương quan không hàm ý nhân quả.
TF-IDF
Phương pháp bạn dùng đến giờ nhìn vào toàn bộ tập dữ liệu, nhưng chưa xử lý cách định lượng mức quan trọng của các thuật ngữ trong một tài liệu so với toàn bộ sưu tập. Bạn đã xem tần suất thuật ngữ và loại bỏ stop words, nhưng đó có thể chưa phải cách tinh vi nhất.
Giới thiệu TF-IDF. TF là "term frequency" (tần suất thuật ngữ). IDF là "inverse document frequency" (nghịch đảo tần suất tài liệu), gán trọng số thấp cho từ dùng phổ biến và trọng số cao cho từ ít dùng trong tập văn bản. Khi kết hợp TF và IDF, mức quan trọng của một thuật ngữ được điều chỉnh theo mức độ hiếm của nó. Giả định đằng sau TF-IDF là thuật ngữ xuất hiện nhiều hơn trong một tài liệu nên được gán trọng số cao hơn, trừ khi nó cũng xuất hiện trong nhiều tài liệu. Công thức tóm tắt như sau:
IDF của bất kỳ thuật ngữ nào vì vậy sẽ cao hơn cho các từ xuất hiện trong ít tài liệu hơn trong tập. Bạn có thể dùng cách tiếp cận mới này để xem các từ quan trọng nhất theo mức chart bằng hàm bind_tf_idf() của tidytext. Hàm này tính và gắn TF và IDF, cùng tích TF*IDF. Nó nhận một tập dữ liệu tidy văn bản với một hàng mỗi token (từ), mỗi tài liệu (bài). Bạn sẽ thấy kết quả ở các cột mới.
Vậy, lấy data frame Prince gốc và unnest token sang từ, loại từ không mong muốn, nhưng giữ stop words. Sau đó dùng bind_tf_idf() để chạy công thức và tạo cột mới.
Giờ bạn thấy IDF và TF-IDF bằng 0 cho các từ cực kỳ phổ biến. (Về kỹ thuật, IDF sẽ là log tự nhiên của 1 nên bằng 0 cho các từ này.) Pipe kết quả ở trên vào arrange() và sắp giảm dần theo tf-idf. Thêm vài bước nữa và bạn sẽ thấy một cách nhìn khác về các từ trong ca từ của Prince.
top_popular_tfidf_words <- popular_tfidf_words %>% arrange(desc(tf_idf)) %>% mutate(word = factor(word, levels = rev(unique(word)))) %>% group_by(chart_level) %> slice(seq_len(8)) %>% ungroup() %>% arrange(chart_level, tf_idf) %>% mutate(row = row_number()) top_popular_tfidf_words %>% ggplot(aes(x = row, tf_idf, fill = chart_level)) + geom_col(show.legend = NULL) + labs(x = NULL, y = "TF-IDF") + ggtitle("Important Words using TF-IDF by Chart Level") + theme_lyrics() + facet_wrap(~chart_level, ncol = 3, scales = "free") + scale_x_continuous( # This handles replacement of row breaks = top_popular_tfidf_words$row, # notice need to reuse data frame labels = top_popular_tfidf_words$word) + coord_flip()Wow, dùng TF-IDF thực sự cho ta một góc nhìn khác về các từ tiềm năng quan trọng. Tất nhiên, diễn giải còn mang tính chủ quan. Bạn có thấy mẫu hình nào không?
Tiếp theo, xem TF-IDF theo thời gian:
tfidf_words_decade <- prince %>% unnest_tokens(word, lyrics) %>% distinct() %>% filter(!word %in% undesirable_words & decade != 'NA') %>% filter(nchar(word) > 3) %>% count(decade, word, sort = TRUE) %>% ungroup() %>% bind_tf_idf(word, decade, n) %>% arrange(desc(tf_idf)) top_tfidf_words_decade <- tfidf_words_decade %> group_by(decade) %>% slice(seq_len(8)) %>% ungroup() %>% arrange(decade, tf_idf) %>% mutate(row = row_number()) top_tfidf_words_decade %>% ggplot(aes(x = row, tf_idf, fill = decade)) + geom_col(show.legend = NULL) + labs(x = NULL, y = "TF-IDF") + ggtitle("Important Words using TF-IDF by Decade") + theme_lyrics() + facet_wrap(~decade, ncol = 3, nrow = 2, scales = "free") + scale_x_continuous( # this handles replacement of row breaks = top_tfidf_words_decade$row, # notice need to reuse data frame labels = top_tfidf_words_decade$word) + coord_flip()Bây giờ bạn chắc chắn đang thu được insight sâu hơn. Các từ vượt thời gian không còn chiếm ưu thế. Bạn có bắt đầu thấy chủ đề hay đề tài hiện ra? Liệu điều đó thực sự có thể chỉ với vài từ mỗi nhóm? Bạn sẽ nâng tầm điều này trong hướng dẫn thứ hai (Phần Hai: Phân tích cảm xúc và Mô hình chủ đề với NLP).
Một word cloud nhanh với phương pháp này mang đến góc nhìn mới về các từ quan trọng trong ca từ của Prince và thú vị hơn một chút...
wc <- tfidf_words_decade %>% arrange(desc(tf_idf)) %>% select(word, tf_idf) wordcloud2(wc[1:300, ], color = "random-dark", minRotation = -pi / 6, maxRotation = -pi / 3, minSize = .002, ellipticity = .3, rotateRatio = 1, size = .2, fontWeight = "bold", gridSize = 1.5 )Kết luận
Trong nghiên cứu tình huống này, trước hết bạn nhìn lướt vào dữ liệu thực, chỉ ở mức cơ bản. Sau khi thực hiện một số điều kiện hóa như làm sạch dữ liệu và loại bỏ từ không mang thông tin, bạn bắt đầu phân tích khám phá ở cấp bài hát.
Tiếp theo, bạn đi sâu vào khai phá văn bản bằng cách unnest ca từ thành các từ đã token hóa để nhìn vào độ phức tạp ngôn từ. Kết quả cung cấp insight quan trọng cho các bước tiếp theo về phân tích cảm xúc và mô hình chủ đề.
Cuối cùng, bạn dùng phân tích TF-IDF để biểu diễn thông tin phía sau một từ trong tài liệu liên quan đến một kết quả quan tâm. Bạn có thể nghĩ đây là cách hay để xác định chủ đề trong âm nhạc, nhưng thực ra bạn mới chỉ có một nửa câu chuyện. Phần Hai sẽ đề cập đến phương pháp học không giám sát gọi là Phân bổ Dirichlet Tiềm ẩn (LDA). Như trong mọi khía cạnh của Khoa học Dữ liệu, có rất nhiều phương pháp để thu được insight. Và trong Phần Hai và Ba của nghiên cứu này, bạn sẽ bàn đến thêm các lựa chọn đó.
Hy vọng bạn cũng háo hức như tôi để tiếp tục hành trình từ phân tích khám phá đến phân tích cảm xúc, mô hình chủ đề và insight dự báo.
Cảm ơn đã đọc, hẹn gặp lại bạn trong hướng dẫn tiếp theo!