Find nearest neighbors change algorithm

Viewed 1196

I am in the process of creating a recommender system that suggests 20 most suitable songs to a user. I've trained my model, I'm ready to recommend songs for a given playlist! However, one issue that I encountered is that I need the embedding of that new playlist in order to find the closest relevant playlists in that embedding space using kmeans.

To recommend songs, I first cluster the learned embeddings for all of the training playlists, and then select "neighbor" playlists for my given test playlist as all of the other playlists in that same cluster. I then take all of the tracks from these playlists and feed the test playlist embedding and these "neighboring" tracks into my model for prediction. This ranks the "neighboring" tracks by how likely they are (under my model) to occur next in the given test playlist.

desired_user_id = 123
model_path = Path(PATH, 'model.h5')
print('using model: %s' % model_path)
model =keras.models.load_model(model_path)
print('Loaded model!')

mlp_user_embedding_weights = (next(iter(filter(lambda x: x.name == 'mlp_user_embedding', model.layers))).get_weights())

# get the latent embedding for your desired user
user_latent_matrix = mlp_user_embedding_weights[0]
one_user_vector = user_latent_matrix[desired_user_id,:]
one_user_vector = np.reshape(one_user_vector, (1,32))

print('\nPerforming kmeans to find the nearest users/playlists...')
# get 100 similar users
kmeans = KMeans(n_clusters=100, random_state=0, verbose=0).fit(user_latent_matrix)
desired_user_label = kmeans.predict(one_user_vector)
user_label = kmeans.labels_
neighbors = []
for user_id, user_label in enumerate(user_label):
    if user_label == desired_user_label:
        neighbors.append(user_id)
print('Found {0} neighbor users/playlists.'.format(len(neighbors)))

tracks = []
for user_id in neighbors:
    tracks += list(df[df['pid'] == int(user_id)]['trackindex'])
print('Found {0} neighbor tracks from these users.'.format(len(tracks))) 

users = np.full(len(tracks), desired_user_id, dtype='int32')
items = np.array(tracks, dtype='int32')

# and predict tracks for my user
results = model.predict([users,items],batch_size=100, verbose=0) 
results = results.tolist()
print('Ranked the tracks!')

results_df = pd.DataFrame(np.nan, index=range(len(results)), columns=['probability','track_name', 'track artist'])
print(results_df.shape)

# loop through and get the probability (of being in the playlist according to my model), the track, and the track's artist 
for i, prob in enumerate(results):
    results_df.loc[i] = [prob[0], df[df['trackindex'] == i].iloc[0]['track_name'], df[df['trackindex'] == i].iloc[0]['artist_name']]
results_df = results_df.sort_values(by=['probability'], ascending=False)

results_df.head(20)

Instead of this code above, I would like to use this https://www.tensorflow.org/recommenders/examples/basic_retrieval#building_a_candidate_ann_index or the official GitHub repository from Spotify https://github.com/spotify/annoy. Unfortunately I don't know exactly how to use this so that the new program gives me the 20 most popular tracks for a user. How do I have to change this?


Edit:

What I tried:

from annoy import AnnoyIndex
import random
desired_user_id = 123
model_path = Path(PATH, 'model.h5')
print('using model: %s' % model_path)
model =keras.models.load_model(model_path)
print('Loaded model!')
    
mlp_user_embedding_weights = (next(iter(filter(lambda x: x.name == 'mlp_user_embedding', model.layers))).get_weights())
    
# get the latent embedding for your desired user
user_latent_matrix = mlp_user_embedding_weights[0]
one_user_vector = user_latent_matrix[desired_user_id,:]
one_user_vector = np.reshape(one_user_vector, (1,32))

t = AnnoyIndex(desired_user_id , one_user_vector)  #Length of item vector that will be indexed
for i in range(1000):
    v = [random.gauss(0, 1) for z in range(f)]
    t.add_item(i, v)

t.build(10) # 10 trees
t.save('test.ann')

u = AnnoyIndex(desired_user_id , one_user_vector)
u.load('test.ann') # super fast, will just mmap the file
print(u.get_nns_by_item(0, 1000)) # will find the 1000 nearest neighbors
# Now how to I get the probability and the values? 
2 Answers

You're almost there!

In your code starting with desired_user_id = 123, you have 4 main steps:
1 (L 1-12): retrieve the matrix of user embeddings (user_latent_matrix) from your saved model
2 (L 14-23): find the user's cluster label (desired_user_label) with kmeans and list other users in the cluster (neighbors). users in the same cluster should listen to similar songs to you.
3 (L 25-31): list songs that other users in the cluster like (tracks). music that you like will be similar to music listened to other people in your cluster. steps 2 and 3 are just to filter out 99% of all music so you only have to run your model on the last 1% to save time & money. Removing 2 and 3 and adding every song to tracks would still work (but take 100x longer).
4 (L 33+): use the saved model to predict if the songs liked by other users in the cluster are suitable to you (results_df)

Annoy is a drop in replacement for finding the similar users (step 2). Instead of using kmeans to find the user's cluster then finding other users in the cluster, it uses a k nearest neighbors style algorithm to find close users directly.

after you've found one_user_vector on line 12, replace step 2 (Lines 14-23) with something like

from annoy import AnnoyIndex

user_embedding_length = 23
t = AnnoyIndex(user_embedding_length, 'angular')

# add the user embeddings to annoy (your annoy userids will be the row indexes)
for user_id, user_embedding in enumerate(user_latent_matrix):
    t.add_item(user_id, user_embedding)

# build the forrest
t.build(10) # 10 trees

# save the forest for later if you're using this again and don't want to rebuild the trees every time
t.save('test.ann')

# find the 100 nearest neighbor users
neighbors = t.get_nns_by_item(desired_user_id, 100) 

if you want to run your stuff again but don't want to rebuild the trees and have already run it once, replace step 2 with just

from annoy import AnnoyIndex

user_embedding_length = 23
t = AnnoyIndex(user_embedding_length, 'angular')

# load the trees
t.load('test.ann')

# find the 100 nearest neighbor users
neighbors = t.get_nns_by_item(desired_user_id, 100) 

After replacing the stuff in step 2, just run steps 3 and 4 (Lines 25+) like normal

In the existing code, you really seem to have two predictions steps: one for finding the 100 nearest neighbor users and another one for ranking all the tracks of all these users in relation to the current user. Now in general, you should decide which of these steps (or both) you want to replace by the Annoy algorithm.

Looking at the example code from GitHub, you do not need the t = AnnoyIndex ... part here, this just creates some sample data for showing the usage.

u = AnnoyIndex(f, metric) needs the number of dimensions as the input parameter f and one of the values "angular", "euclidean", "manhattan", "hamming", or "dot" as the metric. From your question I cannot tell what the number of dimensions is in your case, and you will probably have to experiment with the metric yourself to find out what yields the best results.

After that you will have to import your data into the AnnoyIndex object, which must probably be derived from user_latent_matrix and/or users/items.

Finally, you should be able to retrieve the 20 nearest neighbors by running u.get_nns_by_item(i, 20) where i is the id of the concerning user or track. Setting include_distances=True will also give you the corresponding distances (not probabilities as in your approach).

Hope this gives you some hints for getting ahead.

Related