org.lenskit.mf.funksvd.FunkSVDModelProvider.java Source code

Java tutorial

Introduction

Here is the source code for org.lenskit.mf.funksvd.FunkSVDModelProvider.java

Source

/*
 * LensKit, an open-source toolkit for recommender systems.
 * Copyright 2014-2017 LensKit contributors (see CONTRIBUTORS.md)
 * Copyright 2010-2014 Regents of the University of Minnesota
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
package org.lenskit.mf.funksvd;

import org.apache.commons.lang3.time.StopWatch;
import org.apache.commons.math3.linear.MatrixUtils;
import org.apache.commons.math3.linear.RealMatrix;
import org.apache.commons.math3.linear.RealVector;
import org.lenskit.data.ratings.RatingMatrix;
import org.lenskit.data.ratings.RatingMatrixEntry;
import org.lenskit.inject.Transient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import javax.inject.Inject;
import javax.inject.Provider;
import java.util.ArrayList;
import java.util.List;

/**
 * SVD recommender builder using gradient descent (Funk SVD).
 *
 * <p>
 * This recommender builder constructs an SVD-based recommender using gradient
 * descent, as pioneered by Simon Funk.  It also incorporates the regularizations
 * Funk did. These are documented in
 * <a href="http://sifter.org/~simon/journal/20061211.html">Netflix Update: Try
 * This at Home</a>. This implementation is based in part on
 * <a href="http://www.timelydevelopment.com/demos/NetflixPrize.aspx">Timely
 * Development's sample code</a>.</p>
 *
 * @author <a href="http://www.grouplens.org">GroupLens Research</a>
 */
public class FunkSVDModelProvider implements Provider<FunkSVDModel> {
    private static Logger logger = LoggerFactory.getLogger(FunkSVDModelProvider.class);

    protected final int featureCount;
    protected final RatingMatrix snapshot;
    protected final double initialValue;

    protected final FunkSVDUpdateRule rule;

    @Inject
    public FunkSVDModelProvider(@Transient @Nonnull RatingMatrix snapshot,
            @Transient @Nonnull FunkSVDUpdateRule rule, @FeatureCount int featureCount,
            @InitialFeatureValue double initVal) {
        this.featureCount = featureCount;
        this.initialValue = initVal;
        this.snapshot = snapshot;
        this.rule = rule;
    }

    @Override
    public FunkSVDModel get() {
        int userCount = snapshot.getUserIds().size();
        RealMatrix userFeatures = MatrixUtils.createRealMatrix(userCount, featureCount);

        int itemCount = snapshot.getItemIds().size();
        RealMatrix itemFeatures = MatrixUtils.createRealMatrix(itemCount, featureCount);

        logger.debug("Learning rate is {}", rule.getLearningRate());
        logger.debug("Regularization term is {}", rule.getTrainingRegularization());

        logger.info("Building SVD with {} features for {} ratings", featureCount, snapshot.getRatings().size());

        TrainingEstimator estimates = rule.makeEstimator(snapshot);

        List<FeatureInfo> featureInfo = new ArrayList<>(featureCount);

        // Use scratch vectors for each feature for better cache locality
        // Per-feature vectors are strided in the output matrices
        RealVector uvec = MatrixUtils.createRealVector(new double[userCount]);
        RealVector ivec = MatrixUtils.createRealVector(new double[itemCount]);

        for (int f = 0; f < featureCount; f++) {
            logger.debug("Training feature {}", f);
            StopWatch timer = new StopWatch();
            timer.start();

            uvec.set(initialValue);
            ivec.set(initialValue);

            FeatureInfo.Builder fib = new FeatureInfo.Builder(f);
            double rmse = trainFeature(f, estimates, uvec, ivec, fib);
            summarizeFeature(uvec, ivec, fib);
            featureInfo.add(fib.build());

            // Update each rating's cached value to accommodate the feature values.
            estimates.update(uvec, ivec);

            // And store the data into the matrix
            userFeatures.setColumnVector(f, uvec);
            assert Math.abs(userFeatures.getColumnVector(f).getL1Norm()
                    - uvec.getL1Norm()) < 1.0e-4 : "user column sum matches";
            itemFeatures.setColumnVector(f, ivec);
            assert Math.abs(itemFeatures.getColumnVector(f).getL1Norm()
                    - ivec.getL1Norm()) < 1.0e-4 : "item column sum matches";

            timer.stop();
            logger.info("Finished feature {} in {} (RMSE={})", f, timer, rmse);
        }

        // Wrap the user/item matrices because we won't use or modify them again
        return new FunkSVDModel(userFeatures, itemFeatures, snapshot.userIndex(), snapshot.itemIndex(),
                featureInfo);
    }

    /**
     * Train a feature using a collection of ratings.  This method iteratively calls {@link
     * #doFeatureIteration(TrainingEstimator, List, RealVector, RealVector, double)}  to train
     * the feature.  It can be overridden to customize the feature training strategy.
     *
     * <p>We use the estimator to maintain the estimate up through a particular feature value,
     * rather than recomputing the entire kernel value every time.  This hopefully speeds up training.
     * It means that we always tell the updater we are training feature 0, but use a subvector that
     * starts with the current feature.</p>
     *
     *
     * @param feature   The number of the current feature.
     * @param estimates The current estimator.  This method is <b>not</b> expected to update the
     *                  estimator.
     * @param userFeatureVector      The user feature values.  This has been initialized to the initial value,
     *                  and may be reused between features.
     * @param itemFeatureVector      The item feature values.  This has been initialized to the initial value,
     *                  and may be reused between features.
     * @param fib       The feature info builder. This method is only expected to add information
     *                  about its training rounds to the builder; the caller takes care of feature
     *                  number and summary data.
     * @see #doFeatureIteration(TrainingEstimator, List, RealVector, RealVector, double)
     * @see #summarizeFeature(RealVector, RealVector, FeatureInfo.Builder)
     */
    protected double trainFeature(int feature, TrainingEstimator estimates, RealVector userFeatureVector,
            RealVector itemFeatureVector, FeatureInfo.Builder fib) {
        double oldRMSE = Double.POSITIVE_INFINITY;
        double rmse = Double.MAX_VALUE * 0.5;
        double trail = initialValue * initialValue * (featureCount - feature - 1);
        List<RatingMatrixEntry> ratings = snapshot.getRatings();
        int epoch = 0;
        while (rule.keepGoing(epoch, rmse, oldRMSE)) {
            epoch += 1;
            oldRMSE = rmse;
            rmse = doFeatureIteration(estimates, ratings, userFeatureVector, itemFeatureVector, trail);
            fib.addTrainingRound(rmse);
            logger.trace("iteration {} finished with RMSE {}", epoch, rmse);
        }
        return rmse;
    }

    /**
     * Do a single feature iteration.
     *
     *
     *
     * @param estimates The estimates.
     * @param ratings   The ratings to train on.
     * @param userFeatureVector The user column vector for the current feature.
     * @param itemFeatureVector The item column vector for the current feature.
     * @param trail The sum of the remaining user-item-feature values.
     * @return The RMSE of the feature iteration.
     */
    protected double doFeatureIteration(TrainingEstimator estimates, List<RatingMatrixEntry> ratings,
            RealVector userFeatureVector, RealVector itemFeatureVector, double trail) {
        // We'll create a fresh updater for each feature iteration
        // Not much overhead, and prevents needing another parameter
        FunkSVDTrainingUpdater updater = rule.createUpdater();

        double acc_ud = 0;
        double acc_id = 0;

        for (RatingMatrixEntry r : ratings) {
            final int uidx = r.getUserIndex();
            final int iidx = r.getItemIndex();

            updater.prepare(0, r.getValue(), estimates.get(r), userFeatureVector.getEntry(uidx),
                    itemFeatureVector.getEntry(iidx), trail);

            // Step 3: Update feature values
            double ud = updater.getUserFeatureUpdate();
            acc_ud += ud * ud;
            userFeatureVector.addToEntry(uidx, ud);
            double id = updater.getItemFeatureUpdate();
            acc_id += id * id;
            itemFeatureVector.addToEntry(iidx, id);
        }

        logger.trace("finished iteration with |du|={}, |di|={}", Math.sqrt(acc_ud), Math.sqrt(acc_id));
        return updater.getRMSE();
    }

    /**
     * Add a feature's summary to the feature info builder.
     *
     * @param ufv The user values.
     * @param ifv The item values.
     * @param fib  The feature info builder.
     */
    protected void summarizeFeature(RealVector ufv, RealVector ifv, FeatureInfo.Builder fib) {
        fib.setUserAverage(realVectorSum(ufv) / ufv.getDimension())
                .setItemAverage(realVectorSum(ifv) / ifv.getDimension())
                .setSingularValue(ufv.getNorm() * ifv.getNorm());
    }

    // TODO Find a better solution than this
    private double realVectorSum(RealVector rv) {
        double total = 0;
        for (double i : rv.toArray()) {
            total += i;
        }
        return total;
    }
}