Java tutorial
/* * 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; } }