Surprise icon indicating copy to clipboard operation
Surprise copied to clipboard

Allow partial fitting

Open nickgreenquist opened this issue 6 years ago • 12 comments

Description

Currently, there is no way to use a trained model to serve recommendations for a new user vector that was never used in training the model.

This feature is critical for a real time deployed recommendation system. Without it, there is no way to serve recommendations to a new user until the model has been retrained with their vector added to the data.

For example, with the SVD models, there is one way to serve recommendations by mapping the new user vector to the latent factor space, and then back to the item space. This is the qVV^T trick discussed in Mining of Massive Datasets. However, this naively maps the user to the factor space without actually going through the training process (it assumes the missing ratings are zero which is bad). This produces poor recommendations.

It would be great to have a function like 'adduser' that can be used to add a user to a model without retraining the model, but correctly mapping the user to the correct latent factor space.

nickgreenquist avatar Sep 22 '18 23:09 nickgreenquist

Yes this would be a welcome addition, and it's part of the TODO list.

If you have a plan to tackle this I'd be happy to discuss it.

NicolasHug avatar Sep 23 '18 00:09 NicolasHug

do you have any links to literature on the topic of partial fitting? I could try and implement a solution and see how it works. My guess would be there is a way to run GD or SGD on that one user vector and map it to latent factors but don't change the item to conept (qi) matrix

nickgreenquist avatar Sep 25 '18 02:09 nickgreenquist

I don't think there's a general resource for this because the way incremental fitting can be done (when it can) completely depends on the model you're using. For example for basic neighborhood models there's just no other way than to recompute the whole similarity matrix.

For MF models, one way to update your model is to simply run SGD on the new ratings, using the current factor matrices as a warm start. You could hack your way into this with the current code base, but it could be messy.

But I'm not too concerned about the particularities of each model, I'm more interested in the general changes that would need to be made to surprise to support this for any model and any update. I understand that you're interested in adding new users to an SVD model, but in general one can want to add new ratings to any model, with potentially new users and / or items.

So what I would like to discuss is what kind of API do we want to expose / support, and how it can be integrated with the current code base.

NicolasHug avatar Sep 25 '18 13:09 NicolasHug

API-wise should be simple, we can simply take a warm_start parameter that defaults to False. sklearn does this. For example:

algo = SVD(warm_start=True)
algo.fit(data)
algo.fit(more_data)

For algorithms that do not have incremental fitting, we can show a user warning when people attempt to use the warm_start parameter.

donfour avatar Oct 30 '18 09:10 donfour

I'm working on this for another project of mine and it's working pretty well. I should have some time soon to try and get a PR for here after discussing best plan for the approach.

nickgreenquist avatar Dec 11 '18 21:12 nickgreenquist

@nickgreenquist Can you please share your approach here?

saeedesmaili avatar Jan 13 '19 12:01 saeedesmaili

@saeedesmaili Here is a link to how we do it on the backend of a webapp to partially fit a user relatively quickly: https://github.com/dorukkilitcioglu/books2rec/blob/1a6f79fdb1158f83dbc410c8413f85a8c277d9c1/WebApp/recommendations.py#L54

Its not too complex. We load in the trained item-to-concept components from Surprise model and the trained itemBiases and then just run a reduced number of SGD steps on a new user vector. Results are good IMO

Also, the key thing is we don't update anything related to items here, just the user-to-concept weights and userBias

Feel free to add anything I missed @dorukkilitcioglu

nickgreenquist avatar Jan 14 '19 16:01 nickgreenquist

I have also been working on this for a project, and have an online SVD implementation in this fork/branch that might be useful. It's called mySVD, a name that should change (also could see this getting incorporated into SVD()).

regarding warm_start vs partial_fit, i think partial_fit is a more natural choice for this. they are similar, but sklearn defines partial_fit as the solution for online learning (fitting new data), whereas warm_start is supposed to aid in hyperparameter tuning (same data). so I implemented a partial_fit() method in mySVD.

the other thing I had to deal with is changing training_data, so I had to store the previous conversion dictionaries at the end of partial_fit():

self.raw2inner_id_users = trainset._raw2inner_id_users
self.raw2inner_id_items = trainset._raw2inner_id_items

then when fitting to new data, you have to map the learned values into the correct indices of the new matrices

if (trainset._raw2inner_id_users != self.raw2inner_id_users
                    or trainset._raw2inner_id_items != self.raw2inner_id_items):
                # the trainset has changed, so we need to map the previous values  
                # of bu,pu,bi and qi to the appropriate indices
                # store previous values in appropriate locations in bu,pu,bi and qi
                for u in np.arange(trainset.n_users):
                    raw_u = trainset.to_raw_uid(u)
                    if raw_u in self.raw2inner_id_users.keys():
                        bu[u] = self.bu[self.raw2inner_id_users[raw_u]]
                        pu[u,:] = self.pu[self.raw2inner_id_users[raw_u],:]

                for i in np.arange(trainset.n_items):
                    raw_i = trainset.to_raw_iid(i)
                    if raw_i in self.raw2inner_id_items.keys():
                        bi[i] = self.bi[self.raw2inner_id_items[raw_i]]
                        qi[i,:] = self.qi[self.raw2inner_id_items[raw_i],:]

also, somewhere self.bu and self.bi are overwritten between calls to fit(). so I had to save additional versions of them:

self.bu = bu
self.bu0 = bu
self.bi = bi
self.bi0 = bi

hope this is helpful

lacava avatar Jan 18 '19 18:01 lacava

Is this feature currently available ?

lucasgsfelix avatar Aug 29 '21 21:08 lucasgsfelix

This feature would be incredibly useful.

poochiekittie avatar Feb 28 '22 11:02 poochiekittie

Are there any official solution to this problem?

Online movie recommendation system are not possible without this functionality

Ian2012 avatar Sep 22 '22 02:09 Ian2012

river seems to do that.

vibl avatar Jan 05 '23 06:01 vibl