File size: 9,315 Bytes
b5feb1d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
597f3ab
2529603
b5feb1d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2529603
b5feb1d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
import numpy as np
import gradio as gr
import pandas as pd
from sklearn.metrics.pairwise import cosine_similarity
import plotly.express as px
import plotly.graph_objects as go
import umap

embedding_df = pd.read_csv('all-MiniLM-L12-v2_embeddings.csv')
embeddings = np.array(embedding_df.drop('id', axis=1))

feature_df = pd.read_csv('feature_df.csv', index_col=0)
feature_df= (feature_df - feature_df.mean() ) / feature_df.std() #standardize

info_df = pd.read_csv('song_info_df.csv')
info_df.sort_values(['artist_name','song_title'], inplace=True)

def feature_similarity(song_id):


    std_drop = 4 #drop songs with strange values
        
    song_vec = feature_df[feature_df.index.isin([song_id])].to_numpy()
    songs_matrix = feature_df[~feature_df.index.isin([song_id])].copy()
    songs_matrix = songs_matrix[(songs_matrix<std_drop).any(axis=1)]
    song_ids = list(songs_matrix.index)
    songs_matrix=songs_matrix.to_numpy()
    
    num_dims=songs_matrix.shape[1]
    distances = np.sqrt(np.square(songs_matrix-song_vec) @ np.ones(num_dims)) #compute euclidean distance
    max_distance = np.nanmax(distances)
    similarities = (max_distance - distances)/max_distance #low distance -> high similarity
    
    return pd.DataFrame({'song_id': song_ids, 'feature_similarity': similarities})


def embedding_similarity(song_id):
    
    song_index = embedding_df[embedding_df.id==song_id].index.values[0]
    song_ids = embedding_df[embedding_df.id != song_id].id.to_list()
    emb_matrix = np.delete(np.copy(embeddings), song_index, axis=0)
    similarities = cosine_similarity(emb_matrix, np.expand_dims(np.copy(embeddings[song_index,:]), axis=0))
    
    return pd.DataFrame({'song_id': song_ids, 'cosine_similarity': similarities[:,0]})

def decode(song_id):
    temp_df = info_df[info_df.song_id == song_id]
    artist = temp_df.artist_name.values[0]
    song = temp_df.song_title.values[0]
    youtube_url = f"""<a href=https://www.youtube.com/results?search_query=
    {artist.replace(' ','+')}+{song}.replace(' ','+') target=_blank>{song}</a>""" 
    url = f'''<a href="https://www.youtube.com/results?search_query=
    {artist.strip().replace(' ','+')}+{song.strip().replace(' ','+')}" target="_blank" style="color:blue; text-decoration: underline">
    {song} </a> by {artist}'''
    return url

def plot(artist, song):
        plot_df['color'] = 'blue'
        plot_df.loc[(plot_df.artist_name==artist) & (plot_df.song_title==song), 'color'] = 'red'
        plot_df['size'] = 1.5
        plot_df.loc[(plot_df.artist_name==artist) & (plot_df.song_title==song), 'size'] = 3
        try:
            fig2.data=[]
        except:
            pass
        
        fig2 = px.scatter(plot_df[~((plot_df.artist_name==artist) & (plot_df.song_title==song))],
                               'x',
                               'y',
                               template='simple_white',
                               hover_data=['artist_name', 'song_title']).update_traces(marker_size=1.5, marker_opacity=0.7)
        fig2.add_trace(go.Scatter(x=[plot_df.loc[(plot_df.artist_name==artist) & (plot_df.song_title==song), 'x'].values[0]],
                                  y=[plot_df.loc[(plot_df.artist_name==artist) & (plot_df.song_title==song), 'y'].values[0]],
                                  mode = 'markers',
                                  marker_color='red',
                                  hovertemplate="Your selected song<extra></extra>",
                         marker_size = 4))
        fig2.update_xaxes(visible=False)
        fig2.update_yaxes(visible=False).update_layout(height = 800,
                                              width = 1000,
					      showlegend=False,
                                              title = {
         'text': "UMAP Projection of Lyric Embeddings",
         'y':0.9, 
         'x':0.5,
         'xanchor': 'center',
         'yanchor': 'top' 
        })
        fig2.data = [fig2.data[1], fig2.data[0]]
        return fig2
    
def recommend(artist, song_title, embedding_importance, topk=5):

    feature_importance = 1 - embedding_importance
    song_id = info_df[(info_df.artist_name == artist) & (info_df.song_title == song_title)]['song_id'].values[0]
    
    feature_sim = feature_similarity(song_id)
    embedding_sim = embedding_similarity(song_id)
    result = embedding_sim.merge(feature_sim, how='left',on='song_id').dropna()
    
    result['cosine_similarity'] = (result['cosine_similarity'] - result['cosine_similarity'].min())/ \
    (result['cosine_similarity'].max() - result['cosine_similarity'].min()) 
    result['feature_similarity'] = (result['feature_similarity'] - result['feature_similarity'].min())/ \
    (result['feature_similarity'].max() - result['feature_similarity'].min())
    
    result['score'] = embedding_importance*result.cosine_similarity + feature_importance*result.feature_similarity
    
    exclude_phrases = [r'clean', 'interlude', 'acoustic', r'mix', 'intro', r'original', 'version',\
                      'edited', 'extended']
    
    result = result[~result.song_id.isin(info_df[info_df.song_title.str.lower().str.contains('|'.join(exclude_phrases))].song_id)]
    
    body='<br>'.join([decode(x) for x in result.sort_values('score', ascending=False).head(topk).song_id.to_list()])
    fig = plot(artist, song_title)
    return f'<h3 style="text-align: center;">Recommendations</h3><p style="text-align: center;"><br>{body}</p>', fig


out = umap.UMAP(n_neighbors=30, min_dist=0.2).fit_transform(embedding_df.iloc[:,:-1])
plot_df = pd.DataFrame({'x':out[:,0],'y':out[:,1],'id':embedding_df.id, 'size':0.1})
plot_df['x'] = ((plot_df['x'] - plot_df['x'].mean())/plot_df['x'].std())
plot_df['y'] = ((plot_df['y'] - plot_df['y'].mean())/plot_df['y'].std())
plot_df = plot_df.merge(info_df, left_on='id', right_on='song_id')
plot_df = plot_df[(plot_df.x.abs()<4) & (plot_df.y.abs()<4)]

fig = px.scatter(plot_df,
                   'x',
                   'y',
                  template='simple_white',
                  hover_data=['artist_name', 'song_title']
                        ).update_traces(marker_size=1.5,
                                        opacity=0.7,
                                        
                                        )
fig.update_xaxes(visible=False)
fig.update_yaxes(visible=False).update_layout(height = 800,
                                              width =1000,
                                              title = {
         'text': "UMAP Projection of Lyric Embeddings",
         'y':0.9, 
         'x':0.5,
         'xanchor': 'center',
         'yanchor': 'top' 
        })

app = gr.Blocks()


with app:
    gr.Markdown("# Hip Hop gRadio - A Lyric Based Recommender")
    
    gr.Markdown("""### About this space
    The goal of this space is to provide recommendations for hip-hop/rap songs strictly by utilizing lyrics. The recommendations 
    are a combination of ranked similarity scores. We calculate euclidean distances between our engineered feature vectors for each song,
    as well as a cosine distance between document embeddings of the lyrics themselves. A weighted average of these two results in our
    final similarity score that we use for recommendation. (feature importance = (1 - embedding importance))
    
    Additionally, we provide a 2-D projection of all document embeddings below. After entering a song of your choice, you will see it as
    a red dot, allowing you to explore both near and far. This projection reduces 384-dimensional embeddings down to 2-d, allowing visualization.
    This is done using Uniform Manifold Approximation and Projection [(UMAP)](https://umap-learn.readthedocs.io/en/latest/), a very interesting approach to dimensionalty 
    reduction, I encourage you to look into it if you are interested! ([paper](https://arxiv.org/abs/1802.03426))
    
    The engineered features used are the following: song duration, number of lines, syllables per line, variance in syllables per line,
    total unique tokens, lexical diversity (measure of repitition), sentiment (using nltk VADER), tokens per second, and syllables per second.
    
    **Model used for embedding**: [all-MiniLM-L12-v2](https://huggingface.co/sentence-transformers/all-MiniLM-L12-v2)<br/>
    **Lyrics**: from [genius](https://genius.com/)
                """)
    with gr.Row():
        with gr.Column():
            artist = gr.Dropdown(choices = list(info_df.artist_name.unique()),
                                 value = 'Kanye West',
                                 label='Artist')
            song = gr.Dropdown(choices = list(info_df.loc[info_df.artist_name=='Kanye West','song_title']),
                               label = 'Song Title')
            slider = gr.Slider(0,1,value=0.5, label='Embedding Importance')
            but = gr.Button()
        with gr.Column():
            t = gr.Markdown('<h3 style="text-align: center;">Recomendations</h3>')

    with gr.Row():
        p = gr.Plot(fig)
        
    def artist_songs(artist):
        return gr.components.Dropdown.update(choices=info_df[info_df.artist_name == artist]['song_title'].to_list())
    
    
    
    artist.change(artist_songs, artist, outputs=song)
    but.click(recommend, inputs=[artist, song,slider], outputs=[t, p])
    
app.launch()