net.librec.recommender.cf.rating.LLORMARecommender Maven / Gradle / Ivy
package net.librec.recommender.cf.rating;
import net.librec.common.LibrecException;
import net.librec.math.algorithm.KernelSmoothing;
import net.librec.math.algorithm.Randoms;
import net.librec.math.structure.DenseMatrix;
import net.librec.math.structure.DenseVector;
import net.librec.math.structure.MatrixEntry;
import net.librec.math.structure.SparseMatrix;
import net.librec.recommender.MatrixFactorizationRecommender;
import java.util.List;
/**
* Local Low-Rank Matrix Approximation
*
* This implementation refers to the method proposed by Lee et al. at ICML 2013.
*
* Lcoal Structure: Joonseok Lee, Local Low-Rank Matrix Approximation
* , ICML. 2013: 82-90.
*
* @author GuoGuibing and Keqiang Wang
*/
public class LLORMARecommender extends MatrixFactorizationRecommender {
private int globalNumFactors, localNumFactors;
private int globalNumIterations, localNumIterations;
private int numThreads;
protected double globalRegUser, globalRegItem, localRegUser, localRegItem;
private double globalLearnRate, localLearnRate;
private SparseMatrix predictMatrix;
private int numLocalModels;
private DenseMatrix globalUserFactors, globalItemFactors;
/*
* (non-Javadoc)
*
* @see net.librec.recommender.AbstractRecommender#setup()
*/
@Override
protected void setup() throws LibrecException {
super.setup();
globalNumFactors = conf.getInt("rec.global.factors.num", 20);
localNumFactors = numFactors;
globalNumIterations = conf.getInt("rec.global.iteration.maximum", 100);
localNumIterations = numIterations;
globalRegUser = conf.getDouble("rec.global.user.regularization", 0.01);
globalRegItem = conf.getDouble("rec.global.item.regularization", 0.01);
localRegUser = regUser;
localRegItem = regItem;
globalLearnRate = conf.getDouble("rec.global.iteration.learnrate", 0.01);
localLearnRate = conf.getDouble("rec.iteration.learnrate", 0.01);
numThreads = conf.getInt("rec.thread.count", 4);
numLocalModels = conf.getInt("rec.model.num", 50);
numThreads = numThreads > numLocalModels ? numLocalModels : numThreads;
//global svd P Q to calculate the kernel value between users (or items)
globalUserFactors = new DenseMatrix(numUsers, globalNumFactors);
globalItemFactors = new DenseMatrix(numItems, globalNumFactors);
// initialize model
globalUserFactors.init(initMean, initStd);
globalItemFactors.init(initMean, initStd);
this.buildGlobalModel();
predictMatrix = new SparseMatrix(testMatrix);
}
//global svd P Q
private void buildGlobalModel() {
for (int globalIter = 1; globalIter <= globalNumIterations; globalIter++) {
for (MatrixEntry matrixEntry : trainMatrix) {
int userIdx = matrixEntry.row(); // user
int itemIdx = matrixEntry.column(); // item
double rating = matrixEntry.get();
double predictRating = DenseMatrix.rowMult(globalUserFactors, userIdx, globalItemFactors, itemIdx);
double error = rating - predictRating;
// update factors
for (int factorIdx = 0; factorIdx < globalNumFactors; factorIdx++) {
double puf = globalUserFactors.get(userIdx, factorIdx);
double qif = globalItemFactors.get(itemIdx, factorIdx);
globalUserFactors.add(userIdx, factorIdx, globalLearnRate * (error * qif - globalRegUser * puf));
globalItemFactors.add(itemIdx, factorIdx, globalLearnRate * (error * puf - globalRegItem * qif));
}
}
}// end of training
}
@Override
protected void trainModel() throws LibrecException {
// Pre-calculating similarity:
int completeModelCount = 0;
LLORMAUpdater[] learners = new LLORMAUpdater[numThreads];
int[] anchorArrayUser = new int[numLocalModels];
int[] anchorArrayItem = new int[numLocalModels];
int modelCount = 0;
int[] runningThreadList = new int[numThreads];
int runningThreadCount = 0;
int waitingThreadPointer = 0;
int nextRunningSlot = 0;
SparseMatrix cumPredictionMatrix = new SparseMatrix(testMatrix);
SparseMatrix cumWeightMatrix = new SparseMatrix(testMatrix);
for (MatrixEntry matrixEntry : testMatrix) {
int userIdx = matrixEntry.row();
int itemIdx = matrixEntry.column();
cumPredictionMatrix.set(userIdx, itemIdx, 0.0);
cumWeightMatrix.set(userIdx, itemIdx, 0.0);
}
// Parallel training:
while (completeModelCount < numLocalModels) {
int anchorUser = Randoms.uniform(numUsers);
List itemList = trainMatrix.getColumns(anchorUser);
if (itemList != null && itemList.size() > 0) {
if (runningThreadCount < numThreads && modelCount < numLocalModels) {
// Selecting a new anchor point:
int itemListIdx = Randoms.uniform(itemList.size());
int anchorItem = itemList.get(itemListIdx);
anchorArrayUser[modelCount] = anchorUser;
anchorArrayItem[modelCount] = anchorItem;
// Preparing weight vectors:
DenseVector userWeights = kernelSmoothing(numUsers, anchorUser, KernelSmoothing.EPANECHNIKOV_KERNEL, 0.8, false);
DenseVector itemWeights = kernelSmoothing(numItems, anchorItem, KernelSmoothing.EPANECHNIKOV_KERNEL, 0.8, true);
// Starting a new local model learning:
learners[nextRunningSlot] = new LLORMAUpdater(modelCount, localNumFactors, numUsers, numItems, anchorUser,
anchorItem, localLearnRate, localRegUser, localRegItem, localNumIterations, userWeights, itemWeights, trainMatrix);
learners[nextRunningSlot].start();
runningThreadList[runningThreadCount] = modelCount;
runningThreadCount++;
modelCount++;
nextRunningSlot++;
} else if (runningThreadCount > 0) {
// Joining a local model which was done with learning:
try {
learners[waitingThreadPointer].join();
} catch (InterruptedException ie) {
LOG.error("Join failed: " + ie);
}
int currentModelThreadIdx = waitingThreadPointer;
int currentModelAnchorIdx = completeModelCount;
completeModelCount++;
// Predicting with the new local model and all previous models:
predictMatrix = new SparseMatrix(testMatrix);
for (MatrixEntry matrixEntry : testMatrix) {
int userIdx = matrixEntry.row();
int itemIdx = matrixEntry.column();
double weight = KernelSmoothing.kernelize(getUserSimilarity(anchorArrayUser[currentModelAnchorIdx], userIdx),
0.8, KernelSmoothing.EPANECHNIKOV_KERNEL) * KernelSmoothing.kernelize(getItemSimilarity(anchorArrayItem[currentModelAnchorIdx],
itemIdx), 0.8, KernelSmoothing.EPANECHNIKOV_KERNEL);
double newPrediction = (learners[currentModelThreadIdx].getLocalUserFactors().row(userIdx, false)
.inner(learners[currentModelThreadIdx].getLocalItemFactors().row(itemIdx, false))) * weight;
cumWeightMatrix.set(userIdx, itemIdx, cumWeightMatrix.get(userIdx, itemIdx) + weight);
cumPredictionMatrix.set(userIdx, itemIdx, cumPredictionMatrix.get(userIdx, itemIdx) + newPrediction);
double prediction = cumPredictionMatrix.get(userIdx, itemIdx) / cumWeightMatrix.get(userIdx, itemIdx);
prediction = Double.isNaN(prediction) || prediction == 0.0 ? globalMean : prediction;
prediction = prediction < minRate ? minRate : prediction;
prediction = prediction > maxRate ? maxRate : prediction;
predictMatrix.set(userIdx, itemIdx, prediction);
}
nextRunningSlot = waitingThreadPointer;
waitingThreadPointer = (waitingThreadPointer + 1) % numThreads;
runningThreadCount--;
}
}
}
}
/**
* Calculate similarity between two users, based on the global base SVD.
*
* @param userIdx1 The first user's ID.
* @param userIdx2 The second user's ID.
* @return The similarity value between two users idx1 and idx2.
*/
private double getUserSimilarity(int userIdx1, int userIdx2) {
double sim;
DenseVector userVector1 = globalUserFactors.row(userIdx1);
DenseVector userVector2 = globalUserFactors.row(userIdx2);
sim = 1 - 2.0 / Math.PI * Math.acos(userVector1.inner(userVector2) / (Math.sqrt(userVector1.inner(userVector1))
* Math.sqrt(userVector2.inner(userVector2))));
if (Double.isNaN(sim)) {
sim = 0.0;
}
return sim;
}
/**
* Calculate similarity between two items, based on the global base SVD.
*
* @param itemIdx1 The first item's ID.
* @param itemIdx2 The second item's ID.
* @return The similarity value between two items idx1 and idx2.
*/
private double getItemSimilarity(int itemIdx1, int itemIdx2) {
double sim;
DenseVector itemVector1 = globalItemFactors.row(itemIdx1);
DenseVector itemVector2 = globalItemFactors.row(itemIdx2);
sim = 1 - 2.0 / Math.PI * Math.acos(itemVector1.inner(itemVector2) / (Math.sqrt(itemVector1.inner(itemVector1))
* Math.sqrt(itemVector2.inner(itemVector2))));
if (Double.isNaN(sim)) {
sim = 0.0;
}
return sim;
}
/**
* Given the similarity, it applies the given kernel.
* This is done either for all users or for all items.
*
* @param size The length of user or item vector.
* @param anchorIdx The identifier of anchor point.
* @param kernelType The type of kernel.
* @param width Kernel width.
* @param isItemFeature return item kernel if yes, return user kernel otherwise.
* @return The kernel-smoothed values for all users or all items.
*/
private DenseVector kernelSmoothing(int size, int anchorIdx, int kernelType, double width, boolean isItemFeature) {
DenseVector newFeatureVector = new DenseVector(size);
newFeatureVector.set(anchorIdx, 1.0);
for (int index = 0; index < size; index++) {
double sim;
if (isItemFeature) {
sim = getItemSimilarity(index, anchorIdx);
} else { // userFeature
sim = getUserSimilarity(index, anchorIdx);
}
newFeatureVector.set(index, KernelSmoothing.kernelize(sim, width, kernelType));
}
return newFeatureVector;
}
@Override
protected double predict(int userIdx, int itemIdx) {
return predictMatrix.get(userIdx, itemIdx);
}
}