I am working on building a collaborative filtering recommender system in Keras for a school project, following an approach from this article. The approach is to
- take tabular user, item and review data;
- build an embedding for the user and the item;
- create a deep neural network trained to predict the rating
I am planning to extend this approach by leveraging additional features and content (e.g. NLP on review text, price, quantity, etc.) to enhance the embeddings and then perform an additional step:
use the embedding to find similar products using cosine similarity (or alternate distance metrics).
Because I want to add more features, I can't create a typical NxM matrix where each row is a user (N) and each column (M) is a item, but rather need to create a reduced dimension embedding based on those inputs (at least that's my thought). That all appears to work fine, but after the embeddings are trained, I have attempted to use cosine similarity (using Scipy because Skit-learn throws memory errors due to my embedding matrices being so large) to identify similar products, but they don't seem similar at all.
For example, I am using the Amazon electronics review data set for my own project and when I train the model and feed it a random product (e.g. Composite S-Video cable), the products returned (sorted by distance), don't make sense (e.g. Wireless charger for TouchPad).
The article I referenced uses a (goodbooks) dataset with ~6M reviews across ~50K users and 10K books. The input to the DNN is the tabular data (6M reviews with user and item combinations). The embeddings are reduced to ~50K and 10K accordingly so that each row of the embedding represents a user or book. My confusion is how the network is indexing the embeddings so that user X is the X-th row of the user embeddings (or vice-versa for the items)
In this article example, the embeddings have different dimensions, as does my project, but when I take the item embeddings and attempt to perform a similarity distance metric on them (cosine) the results just don't make sense. This made me think that maybe when I choose an item index (e.g. 1234) and then go look it up in the embedding (e.g. item_df.iloc), it isn't actually picking up the 'correct' item that I am anticipating? or since the matrix is trained on the tabular data, is the size of the embeddings being set just arbitrary?
If anyone has thoughts or suggestions of how to help understand what I'm doing wrong or if my interpretation of the solution isn't correct, that would be immensely helpful. Below is the primary DNN Keras code that is being used to train the embeddings. I have omitted the cosine code for similarity as I suspect the issue is either in the code below or in my interpretation of the approach. Thank you in advance!
Primary python code reference from article to build embeddings DNN:
## read data ratings = pd.read_csv("https://raw.githubusercontent.com/zygmuntz/goodbooks-10k/master/ratings.csv") books = pd.read_csv("https://raw.githubusercontent.com/zygmuntz/goodbooks-10k/master/books.csv") ## unique users, books n_users, n_books = len(ratings.user_id.unique()), len(ratings.book_id.unique()) ## define the number of latent factors (can be different for the users and books) dim_embedding_user = 50 dim_embedding_book = 50 ## book embedding book_input= Input(shape=, name='Book') book_embedding = Embedding(n_books + 1, dim_embedding_book, name='Book-Embedding')(book_input) book_vec = Flatten(name='Book-Flatten')(book_embedding) book_vec = Dropout(0.2)(book_vec) ## user embedding user_input = Input(shape=, name='User') user_embedding = Embedding(n_users + 1, dim_embedding_user, name ='User-Embedding')(user_input) user_vec = Flatten(name ='User-Flatten')(user_embedding) user_vec = Dropout(0.2)(user_vec) ## concatenate flattened values concat = concatenate([book_vec, user_vec]) concat_dropout = Dropout(0.2)(concat) ## add dense layer (can try more) dense_1 = Dense(20, name ='Fully-Connected1', activation='relu')(concat) ## define output (can try sigmoid instead of relu) result = Dense(1, activation ='relu',name ='Activation')(dense_1) ## define model with 2 inputs and 1 output model_tabular = Model([user_input, book_input], result) ## show model summary model_tabular.summary()