7 минут чтения
5 марта 2021 г.
Python и тексты нового альбома Земфиры: анализируем суть песен
Неделю назад вышёл первый за 8 лет студийный альбом Земфиры «Бордерлайн». К работе помимо рок-певицы приложили руку разные люди, в том числе и её родственники — рифф для песни «таблетки» написал её племянник из Лондона. Альбом получился разнообразным: например, песня «остин» посвящена главному персонажу игры Homescapes российской студии Playrix (кстати, посмотрите свежие Бизнес-секреты с братьями Бухманами, там они тоже про это рассказывают) — Земфире нравится игра, и для трека она связалась со студией. А сингл «крым» был написан в качестве саундтрека к новой картине соратницы Земфиры — Ренаты Литвиновой.
Послушать альбом в Apple Music / Яндекс.Музыке / Spotify
Тем не менее, дух всего альбома довольно мрачен — в песнях часто повторяются слова «боль», «ад», «бесишь» и прочие по смыслу. Мы решили провести разведочный анализ нового альбома, а затем при помощи модели Word2Vec и косинусной меры посмотреть на семантическую близость песен между собой и вычислить общее настроение альбома.
Для тех, кому скучно читать про подготовку данных и шаги анализа можно перейти сразу к результатам.
Подготовка данных
Для начала работы напишем скрипт обработки данных. Цель скрипта — из множества текстовых файлов, в каждом из которых лежит по песне, собрать единую csv-таблицу. При этом текст треков очищаем от знаков пунктуации и ненужных слов.
import pandas as pd
import re
import string
import pymorphy2
from nltk.corpus import stopwords
Создаём морфологический анализатор и расширяем список всего, что нужно отбросить:
morph = pymorphy2.MorphAnalyzer()
stopwords_list = stopwords.words(‘russian’)
stopwords_list.extend([‘куплет’, ‘это’, ‘я’, ‘мы’, ‘ты’, ‘припев’, ‘аутро’, ‘предприпев’, ‘lyrics’, ‘1’, ‘2’, ‘3’, ‘то’])
string.punctuation += ‘—’
Названия песен приведены на английском — создадим словарь для перевода на русский и словарь, из которого позднее сделаем таблицу:
result_dict = dict()
songs_dict = {
‘snow’:’снег идёт’,
‘crimea’:’крым’,
‘mother’:’мама’,
‘ostin’:’остин’,
‘abuse’:’абьюз’,
‘wait_for_me’:’жди меня’,
‘tom’:’том’,
‘come_on’:’камон’,
‘coat’:’пальто’,
‘this_summer’:’этим летом’,
‘ok’:’ок’,
‘pills’:’таблетки’
}
Опишем несколько функций. Первая читает целиком песню из файла и удаляет переносы строки, вторая очищает текст от ненужных символов и слов, а третья при помощи морфологического анализатора pymorphy2 приводит слова к нормальной форме. Модуль pymorphy2 не всегда хорошо справляется с неоднозначностью — для слов «ад» и «рай» потребуется дополнительная обработка.
def read_song(filename):
f = open(f'{filename}.txt’, ‘r’).read()
f = f.replace(‘\n’, ‘ ‘)
return f
def clean_string(text):
text = re.split(‘ |:|\.|\(|\)|,|»|;|/|\n|\t|-|\?|\[|\]|!’, text)
text = ‘ ‘.join([word for word in text if word not in string.punctuation])
text = text.lower()
text = ‘ ‘.join([word for word in text.split() if word not in stopwords_list])
return text
def string_to_normal_form(string):
string_lst = string.split()
for i in range(len(string_lst)):
string_lst[i] = morph.parse(string_lst[i])[0].normal_form
if (string_lst[i] == ‘аду’):
string_lst[i] = ‘ад’
if (string_lst[i] == ‘рая’):
string_lst[i] = ‘рай’
string = ‘ ‘.join(string_lst)
return string
Проходим по каждой песне и читаем файл с соответствующим названием:
name_list = []
text_list = []
for song, name in songs_dict.items():
text = string_to_normal_form(clean_string(read_song(song)))
name_list.append(name)
text_list.append(text)
Затем объединяем всё в DataFrame и сохраняем в виде csv-файла.
df = pd.DataFrame()
df[‘name’] = name_list
df[‘text’] = text_list
df[‘time’] = [290, 220, 187, 270, 330, 196, 207, 188, 269, 189, 245, 244]
df.to_csv(‘borderline.csv’, index=False)
Результат:
Облако слов по всему альбому
Начнём анализ с построения облака слов – оно отобразит, какие слова чаще всего встречаются в песнях. Импортируем нужные библиотеки, читаем csv-файл и устанавливаем конфигурации:
import nltk
from wordcloud import WordCloud
import pandas as pd
import matplotlib.pyplot as plt
from nltk import word_tokenize, ngrams
%matplotlib inline
nltk.download(‘punkt’)
df = pd.read_csv(‘borderline.csv’)
Теперь создаём новую фигуру, устанавливаем параметры оформления и при помощи библиотеки wordcloud отображаем слова с размером прямо пропорциональным частоте упоминания слова. Над каждым графиком дополнительно указываем название песни.
fig = plt.figure()
fig.patch.set_facecolor(‘white’)
plt.subplots_adjust(wspace=0.3, hspace=0.2)
i = 1
for name, text in zip(df.name, df.text):
tokens = word_tokenize(text)
text_raw = » «.join(tokens)
wordcloud = WordCloud(colormap=’PuBu’, background_color=’white’, contour_width=10).generate(text_raw)
plt.subplot(4, 3, i, label=name,frame_on=True)
plt.tick_params(labelsize=10)
plt.imshow(wordcloud)
plt.axis(«off»)
plt.title(name,fontdict={‘fontsize’:7,’color’:’grey’},y=0.93)
plt.tick_params(labelsize=10)
i += 1
EDA текстов альбома
Теперь проанализируем тексты песен — импортируем библиотеки для работы с данными и визуализации:
import plotly.graph_objects as go
import plotly.figure_factory as ff
from scipy import spatial
import collections
import pymorphy2
import gensim
morph = pymorphy2.MorphAnalyzer()
Сначала посчитаем число слов в каждой песне, число уникальных слов и процентное соотношение:
songs = []
total = []
uniq = []
percent = []
for song, text in zip(df.name, df.text):
songs.append(song)
total.append(len(text.split()))
uniq.append(len(set(text.split())))
percent.append(round(len(set(text.split())) / len(text.split()), 2) * 100)
А теперь составим из этого DataFrame и дополнительно посчитаем число слов в минуту для каждой песни:
df_words = pd.DataFrame()
df_words[‘song’] = songs
df_words[‘total words’] = total
df_words[‘uniq words’] = uniq
df_words[‘percent’] = percent
df_words[‘time’] = df[‘time’]
df_words[‘words per minute’] = round(total / (df[‘time’] // 60))
df_words = df_words[::-1]
Данные хорошо бы визуализировать — построим две столбиковые диаграммы: одну для числа слов в песне, а другую для числа слов в минуту.
colors_1 = [‘rgba(101,181,205,255)’] * 12
colors_2 = [‘rgba(62,142,231,255)’] * 12
fig = go.Figure(data=[
go.Bar(name=’📝 Всего слов’,
text=df_words[‘total words’],
textposition=’auto’,
x=df_words.song,
y=df_words[‘total words’],
marker_color=colors_1,
marker=dict(line=dict(width=0)),),
go.Bar(name=’🌀 Уникальных слов’,
text=df_words[‘uniq words’].astype(str) + ‘<br>‘+ df_words.percent.astype(int).astype(str) + ‘%’ ,
textposition=’inside’,
x=df_words.song,
y=df_words[‘uniq words’],
textfont_color=’white’,
marker_color=colors_2,
marker=dict(line=dict(width=0)),),
])
fig.update_layout(barmode=’group’)
fig.update_layout(
title =
{‘text’:’<b>Соотношение числа уникальных слов к общему количеству</b><br><span style="color:#666666"></span>‘},
showlegend = True,
height=650,
font={
‘family’:’Open Sans, light’,
‘color’:’black’,
‘size’:14
},
plot_bgcolor=’rgba(0,0,0,0)’,
)
fig.update_layout(legend=dict(
yanchor=»top»,
xanchor=»right»,
))
fig.show()
colors_1 = [‘rgba(101,181,205,255)’] * 12
colors_2 = [‘rgba(238,85,59,255)’] * 12
fig = go.Figure(data=[
go.Bar(name=’⏱️ Длина трека, мин.’,
text=round(df_words[‘time’] / 60, 1),
textposition=’auto’,
x=df_words.song,
y=-df_words[‘time’] // 60,
marker_color=colors_1,
marker=dict(line=dict(width=0)),
),
go.Bar(name=’🔄 Слов в минуту’,
text=df_words[‘words per minute’],
textposition=’auto’,
x=df_words.song,
y=df_words[‘words per minute’],
marker_color=colors_2,
textfont_color=’white’,
marker=dict(line=dict(width=0)),
),
])
fig.update_layout(barmode=’overlay’)
fig.update_layout(
title =
{‘text’:’<b>Длина трека и число слов в минуту</b><br><span style="color:#666666"></span>‘},
showlegend = True,
height=650,
font={
‘family’:’Open Sans, light’,
‘color’:’black’,
‘size’:14
},
plot_bgcolor=’rgba(0,0,0,0)’
)
fig.show()
Работа с Word2Vec моделью
При помощи модуля gensim загружаем модель, указывая на бинарный файл:
model = gensim.models.KeyedVectors.load_word2vec_format(‘model.bin’, binary=True)
Для материала мы использовали готовую обученную на Национальном Корпусе Русского Языка модель от сообщества RusVectōrēs
Модель Word2Vec основана на нейронных сетях и позволяет представлять слова в виде векторов, учитывая семантическую составляющую. Это означает, что если мы возьмём два слова — например, «мама» и «папа», представим их в виде двух векторов и посчитаем косинус, значения будет близко к 1. Аналогично, у двух слов, не имеющих ничего общего по смыслу косинусная мера близка к 0.
Опишем функцию get_vector: она будет принимать список слов, распознавать для каждого часть речи, а затем получать и суммировать вектора — так мы сможем находить вектора не для одного слова, а для целых предложений и текстов.
def get_vector(word_list):
vector = 0
for word in word_list:
pos = morph.parse(word)[0].tag.POS
if pos == ‘INFN’:
pos = ‘VERB’
if pos in [‘ADJF’, ‘PRCL’, ‘ADVB’, ‘NPRO’]:
pos = ‘NOUN’
if word and pos:
try:
word_pos = word + ‘_’ + pos
this_vector = model.word_vec(word_pos)
vector += this_vector
except KeyError:
continue
return vector
Для каждой песни находим вектор и собираем соответствующий столбец в DataFrame:
vec_list = []
for word in df[‘text’]:
vec_list.append(get_vector(word.split()))
df[‘vector’] = vec_list
Теперь сравним вектора между собой, посчитав их косинусную близость. Те песни, у которых косинусная метрика выше 0,5 запомним отдельно — так мы получим самые близкие пары песен. Данные о сравнении векторов запишем в двумерный список result.
similar = dict()
result = []
for song_1, vector_1 in zip(df.name, df.vector):
sub_list = []
for song_2, vector_2 in zip(df.name.iloc[::-1], df.vector.iloc[::-1]):
res = 1 — spatial.distance.cosine(vector_1, vector_2)
if res > 0.5 and song_1 != song_2 and (song_1 + ‘ / ‘ + song_2 not in similar.keys() and song_2 + ‘ / ‘ + song_1 not in similar.keys()):
similar[song_1 + ‘ / ‘ + song_2] = round(res, 2)
sub_list.append(round(res, 2))
result.append(sub_list)
Самые похожие треки соберём в отдельный DataFrame:
df_top_sim = pd.DataFrame()
df_top_sim[‘name’] = list(similar.keys())
df_top_sim[‘value’] = list(similar.values())
df_top_sim.sort_values(by=’value’, ascending=False)
И построим такой же bar chart:
colors = [‘rgba(101,181,205,255)’] * 5
fig = go.Figure([go.Bar(x=df_top_sim[‘name’],
y=df_top_sim[‘value’],
marker_color=colors,
width=[0.4,0.4,0.4,0.4,0.4],
text=df_top_sim[‘value’],
textfont_color=’white’,
textposition=’auto’)])
fig.update_layout(
title =
{‘text’:’<b>Топ-5 схожих песен</b><br><span style="color:#666666"></span>‘},
showlegend = False,
height=650,
font={
‘family’:’Open Sans, light’,
‘color’:’black’,
‘size’:14
},
plot_bgcolor=’rgba(0,0,0,0)’,
xaxis={‘categoryorder’:’total descending’}
)
fig.show()
Имея вектор каждой песни, давайте посчитаем вектор всего альбома — сложим вектора песен. Затем для такого вектора при помощи модели получим самые близкие по духу и смыслу слова.
def get_word_from_tlist(lst):
for word in lst:
word = word[0].split(‘_’)[0]
print(word, end=’ ‘)
vec_sum = 0
for vec in df.vector:
vec_sum += vec
sim_word = model.similar_by_vector(vec_sum)
get_word_from_tlist(sim_word)
небо тоска тьма пламень плакать горе печаль сердце солнце мрак
Наверное, это ключевой результат и описание альбома Земфиры всего лишь в 10 словах.
Наконец, построим общую тепловую карту, каждая ячейка которой — результат сравнения косинусной мерой текстов двух треков.
colorscale=[[0.0, «rgba(255,255,255,255)»],
[0.1, «rgba(229,232,237,255)»],
[0.2, «rgba(216,222,232,255)»],
[0.3, «rgba(205,214,228,255)»],
[0.4, «rgba(182,195,218,255)»],
[0.5, «rgba(159,178,209,255)»],
[0.6, «rgba(137,161,200,255)»],
[0.7, «rgba(107,137,188,255)»],
[0.8, «rgba(96,129,184,255)»],
[1.0, «rgba(76,114,176,255)»]]
font_colors = [‘black’]
x = list(df.name.iloc[::-1])
y = list(df.name)
fig = ff.create_annotated_heatmap(result, x=x, y=y, colorscale=colorscale, font_colors=font_colors)
fig.show()
Результаты анализа и интерпретация данных
Давайте ещё раз посмотрим на всё, что у нас получилось — начнём с облака слов. Нетрудно заметить, что у слов «боль», «невозможно», «сорваться», «растерзаны», «сложно», «терпеть», «любить» размер весьма приличный — всё потому, что такие слова встречаются часто на протяжении всего текста песен:
Одной из самых «разнообразных» песен оказался сингл «крым» — в нём 74% уникальных слов. А в песне «снег идёт» слов совсем мало, поэтому большинство — 82% уникальны. Самой большой песней в альбоме получился трек «таблетки» — суммарно там около 150 слов.
Как было выяснено на прошлом графике, самый «динамичный» трек — «таблетки», целых 37 слов в минуту — практически по слову на каждые две секунды. А самый длинный трек — «абъюз», в нём же и согласно предыдущему графику практически самый низкий процент уникальных слов — 46%.
Топ-5 самых семантически похожих пар текстов:
Ещё мы получили вектор всего альбома и подобрали самые близкие слова. Только посмотрите на них — «тьма», «тоска», «плакать», «горе», «печаль», «сердце» — это же ведь и есть тот перечень слов, который характеризует лирику Земфиры!
небо тоска тьма пламень плакать горе печаль сердце солнце мрак
Финал — тепловая карта. По визуализации заметно, что практически все песни достаточно схожи между собой — косинусная мера у многих пар превышает значение в 0.4.
Выводы
В материале мы провели EDA всего текста нового альбома и при помощи предобученной модели Word2Vec доказали гипотезу — большинство песен «бордерлайна» пронизывают довольно мрачные и тексты. И это нормально, ведь Земфиру мы любим именно за искренность и прямолинейность.
Комментарии
Добавить комментарий
[ Рекомендации ]
Читайте также
[ Связаться ]
Давайте раскроем потенциал вашего бизнеса вместе
Заполните форму на бесплатную консультацию
«большинство песен бордерлайна пронизывают довольно мрачные и тексты. И это нормально, ведь Земфиру мы любим именно за искренность и прямолинейность.»
И чего же в этом нормального? Просто Земфира смотрит на окружающий мир невесело. А есть другие люди вполне жизнерадостные. И они жизнерадостны вполне искренне.
Нормально то, что это мало чем отличается от других текстов Земфиры. То есть, если читать это в контексте, можно прочесть, что это нормально для ее творчества 🙂