以下是本人在学习 LibRec 库时,仔细走读的 SBPR 算法的笔记,文件名是 SBPRRecommender.java,有需要的同学可以往下看。
package net.librec.recommender.context.ranking;
import com.google.common.cache.LoadingCache;
import net.librec.annotation.ModelData;
import net.librec.common.LibrecException;
import net.librec.math.algorithm.Maths;
import net.librec.math.algorithm.Randoms;
import net.librec.math.structure.VectorBasedDenseVector;
import net.librec.recommender.SocialRecommender;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutionException;
/**
* Social Bayesian Personalized Ranking (SBPR)
* Zhao et al., Leveraging Social Connections to Improve Personalized Ranking for Collaborative
* Filtering, CIKM 2014.
* @author guoguibing and Keqiang Wang
*/
@ModelData({"isRanking", "sbpr", "userFactors", "itemFactors", "itemBiases"})
public class SBPRRecommender extends SocialRecommender {//继承的是SocialRecommender类
private VectorBasedDenseVector itemBiases;//物品偏置向量
protected float regBias;//偏置正则化项
protected LoadingCache<Integer, List<Integer>> userItemsCache;//加载user-items cache, item-users cache缓存
protected static String cacheSpec;//Guava cache configuration???
private List<List<Integer>> userSocialItemsSetList;//存放用户信任的人打分的物品
@Override//覆盖SocialRecommender中的setup方法
public void setup() throws LibrecException {
super.setup();//关于这个SocialRecommender中的setup方法里有什么,详见问题1
regBias = conf.getFloat("rec.bias.regularization", 0.01f);//偏置正则化项,默认值为0.01
cacheSpec = conf.get("guava.cache.spec", "maximumSize=5000,expireAfterAccess=50m");//设置内存缓存模块
itemBiases = new VectorBasedDenseVector(numItems);//初始化物品偏置向量,初始化的值大小在0,1直接随机分配
itemBiases.init();
userItemsCache = trainMatrix.rowColumnsCache(cacheSpec);//rowColumnsCache这个方法已经被弃用,但是还是可以正常。这句代码的作用是得到一个用户-物品的缓存模块。
userSocialItemsSetList = new ArrayList<>(numUsers);// 找到信任的人打过分的物品,设置为列表中的列表
for (int userIdx = 0; userIdx < numUsers; userIdx++) {
userSocialItemsSetList.add(new ArrayList<Integer>());
}
//循环遍历每一个用户
for (int userIdx = 0; userIdx < numUsers; userIdx++) {
//找到用户自己打过分的物品,如果没有,则跳过这个用户
List<Integer> uRatedItems = null;
try {
uRatedItems = userItemsCache.get(userIdx);
} catch (ExecutionException e) {
e.printStackTrace();
}
if (uRatedItems.size() == 0)
continue; // no rated items
//找到仅仅被用户信任的人打过分的物品(其中要去除自己打过分的物品)
int[] trustedUsers = socialMatrix.row(userIdx).getIndices();//从社交矩阵中获取用户信任的 人
List<Integer> items = new ArrayList<>();
for (int trustedUserIdx : trustedUsers) {//循环遍历,找到仅仅被用户信任的人打过分的物品(其中要去除自己打过分的物品)
List<Integer> trustedRatedItems = null;
try {
trustedRatedItems = userItemsCache.get(trustedUserIdx);//该行代码的意思是炸弹该用户打过分的物品
} catch (ExecutionException e) {
e.printStackTrace();
}
for (int trustedRatedItemIdx : trustedRatedItems) {//去除用户本身打过分的物品
// v's rated items
if (!uRatedItems.contains(trustedRatedItemIdx) && !items.contains(trustedRatedItemIdx)) // if not rated by user u and not already added to item list
items.add(trustedRatedItemIdx);
}
}
userSocialItemsSetList.set(userIdx, items);//items里面保存的是该userIdx的朋友们打过分而自己没有打过分的物品集合
}
}
@Override
protected void trainModel() throws LibrecException {
int maxSample = trainMatrix.size();//设置最大的sample次数为训练集中所有打分个数的总值,因为不可能不满足条件而一直sample下去
/**
* 开始进行每一次迭代,内容包括sample、更新参数值,这里设置的sample次数是训练集中有多少个打分,也即是有多少个posItem,就sample多少次,而不是每一个用户sample一次。*/
for (int iter = 1; iter <= numIterations; iter++) {
loss = 0.0d;//设置每次迭代初始loss值为0
for (int sample = 0; sample < maxSample; sample++) {
// uniformly 选择 userIdx, posItemIdx, k, negItemIdx)
int userIdx, posItemIdx, negItemIdx;
// 从所有用户里面随机选择一个用户出来,记为userIdx,当然这个用户得有打过分的物品
List<Integer> ratedItems = null;
do {
userIdx = Randoms.uniform(numUsers);
try {
ratedItems = userItemsCache.get(userIdx);
} catch (ExecutionException e) {
e.printStackTrace();
}
} while (ratedItems.size() == 0);
//从选择出的用户userIdx所打分的物品中随机选择一个物品,记为posItemIdx
posItemIdx = Randoms.random(ratedItems);
//得到用户userIdx对物品posItemIdx的预测分数值
double posPredictRating = predict(userIdx, posItemIdx);
//得到用户userIdx信任的朋友们的打分物品,具体userSocialItemsSetList的值已经在setup()函数中得到
List<Integer> socialItemsList = userSocialItemsSetList.get(userIdx);
//得到用户userIdx以及他信任的朋友们都没有打过分的物品,记为negItemIdx
do {
negItemIdx = Randoms.uniform(numItems);
} while (ratedItems.contains(negItemIdx) || socialItemsList.contains(negItemIdx));
//得到用户userIdx对物品negItemIdx的预测分数值
double negPredictRating = predict(userIdx, negItemIdx);
if (socialItemsList.size() > 0) {//如果用户的朋友们有打分的物品,则进行SBPR方式进行更新,如果没有,则进行BPR方式更新,下面的代码块是进行SBPR的更新
//随机从用户userIdx信任的朋友们打过分的物品,记为socialItemIdx,本身已经剔除用户与朋友都打过分的物品
int socialItemIdx = Randoms.random(socialItemsList);
//得到用户userIdx对物品socialItemIdx的预测分数值
double socialPredictRating = predict(userIdx, socialItemIdx);
/**以下几行代码的目的是得到socialWeight,根据论文所写,使用这个参数是用来控制每个采样训练对目标的贡献,
* 较大的socialWeight值表示用户u可能更喜欢这个物品,因为他的许多朋友们在这件物品上都表现出了他们的偏好。
* 只要有用户的朋友对这个socialItemIdx物品进行过打分,就socialWeight加一。
*/
int[] trustedUserIdices = socialMatrix.row(userIdx).getIndices();//从社交矩阵中获取用户userIdx信任的 人
double socialWeight = 0;
for (int trustedUserIdx : trustedUserIdices) {//遍历userIdx所有信任的朋友
int[] indices = trainMatrix.row(trustedUserIdx).getIndices();//得到朋友trustedUserIdx所有打过分物品的下标值集合
int socialRating = Arrays.binarySearch(indices, socialItemIdx);//使用二分查找去找到朋友trustedUserIdx是否对socialItemIdx这个物品
//返回的是下标值,所以如果下标值大于0,说明找到了,没有找到就会返回0
if (socialRating > 0)
socialWeight += 1;
}
//目标函数使用(1+socialWeight)的倒数来控制用户userIdx对物品posItemIdx和物品socialItemIdx之间的偏好差异
double posSocialDiffValue = (posPredictRating - socialPredictRating) / (1 + socialWeight);
double socialNegDiffValue = socialPredictRating - negPredictRating;
//Maths.logistic是sigmoid函数,下面这行代码得到的是loss值(以及下面的要加上正则化项)
double error = -Math.log(Maths.logistic(posSocialDiffValue)) - Math.log(Maths.logistic(socialNegDiffValue));
loss += error;
double posSocialGradient = Maths.logistic(-posSocialDiffValue), socialNegGradient = Maths.logistic(-socialNegDiffValue);
// update bi, bk, bj
double posItemBiasValue = itemBiases.get(posItemIdx);
itemBiases.plus(posItemIdx, learnRate * (posSocialGradient / (1 + socialWeight) - regBias * posItemBiasValue));
loss += regBias * posItemBiasValue * posItemBiasValue;
double socialItemBiasValue = itemBiases.get(socialItemIdx);
itemBiases.plus(socialItemIdx, learnRate * (-posSocialGradient / (1 + socialWeight) + socialNegGradient - regBias * socialItemBiasValue));
loss += regBias * socialItemBiasValue * socialItemBiasValue;
double negItemBiasValue = itemBiases.get(negItemIdx);
itemBiases.plus(negItemIdx, learnRate * (-socialNegGradient - regBias * negItemBiasValue));
loss += regBias * negItemBiasValue * negItemBiasValue;
// update P, Q
for (int factorIdx = 0; factorIdx < numFactors; factorIdx++) {
double userFactorValue = userFactors.get(userIdx, factorIdx);
double posItemFactorValue = itemFactors.get(posItemIdx, factorIdx);
double socialItemFactorValue = itemFactors.get(socialItemIdx, factorIdx);
double negItemFactorValue = itemFactors.get(negItemIdx, factorIdx);
double delta_puf = posSocialGradient * (posItemFactorValue - socialItemFactorValue) / (1 + socialWeight)
+ socialNegGradient * (socialItemFactorValue - negItemFactorValue);
userFactors.plus(userIdx, factorIdx, learnRate * (delta_puf - regUser * userFactorValue));
itemFactors.plus(posItemIdx, factorIdx, learnRate * (posSocialGradient * userFactorValue / (1 + socialWeight)
- regItem * posItemFactorValue));
double delta_qkf = posSocialGradient * (-userFactorValue / (1 + socialWeight)) + socialNegGradient * userFactorValue;
itemFactors.plus(socialItemIdx, factorIdx, learnRate * (delta_qkf - regItem * socialItemFactorValue));
itemFactors.plus(negItemIdx, factorIdx, learnRate * (socialNegGradient * (-userFactorValue) -
regItem * negItemFactorValue));
loss += regUser * userFactorValue * userFactorValue + regItem * posItemFactorValue * posItemFactorValue +
regItem * negItemFactorValue * negItemFactorValue + regItem * socialItemFactorValue * socialItemFactorValue;
}
} else {
//如果用户的朋友们有打分的物品,则进行SBPR方式进行更新,如果没有,则进行BPR方式更新,下面的代码块是进行BPR方式的更新
double posNegDiffValue = posPredictRating - negPredictRating;
loss += posNegDiffValue;
double posNegGradient = Maths.logistic(-posNegDiffValue);
// update bi, bj
double posItemBiasValue = itemBiases.get(posItemIdx);
itemBiases.plus(posItemIdx, learnRate * (posNegGradient - regBias * posItemBiasValue));
loss += regBias * posItemBiasValue * posItemBiasValue;
double negItemBiasValue = itemBiases.get(negItemIdx);
itemBiases.plus(negItemIdx, learnRate * (-posNegGradient - regBias * negItemBiasValue));
loss += regBias * negItemBiasValue * negItemBiasValue;
// update user factors, item factors
for (int factorIdx = 0; factorIdx < numFactors; factorIdx++) {
double userFactorValue = userFactors.get(userIdx, factorIdx);
double posItemFactorValue = itemFactors.get(posItemIdx, factorIdx);
double negItemFactorValue = itemFactors.get(negItemIdx, factorIdx);
userFactors.plus(userIdx, factorIdx, learnRate * (posNegGradient * (posItemFactorValue - negItemFactorValue) - regUser * userFactorValue));
itemFactors.plus(posItemIdx, factorIdx, learnRate * (posNegGradient * userFactorValue - regItem * posItemFactorValue));
itemFactors.plus(negItemIdx, factorIdx, learnRate * (posNegGradient * (-userFactorValue) - regItem * negItemFactorValue));
loss += regUser * userFactorValue * userFactorValue + regItem * posItemFactorValue * posItemFactorValue +
regItem * negItemFactorValue * negItemFactorValue;
}
}
}
//判断是否收敛,输出相关信息,详见下面判断是否收敛的解释
if (isConverged(iter) && earlyStop) {//两个都是真才是真,收敛+早停,则停止迭代
break;
}
//在每一次迭代之后,更新现在的学习率,详见下面学习率的解释
updateLRate(iter);
}
}
//预测具体的userIdx和itemIdx的分数,详细见下面的解释
protected double predict(int userIdx, int itemIdx) throws LibrecException {
double predictRating = itemBiases.get(itemIdx) + userFactors.row(userIdx).dot(itemFactors.row(itemIdx));//这个预测分数值还加入了每一个物品的偏置项值,详细看解释3
return predictRating;
}
}
由于 SBPRRecommender 类继承的是 SocialRecommender类,所以下面是SocialRecommender.java 中的 setup() 方法。
public void setup() throws LibrecException {
super.setup(); //详见下面解释
regSocial = conf.getFloat("rec.social.regularization", 0.01f);//社交正则化项,默认值为0.01
socialMatrix = ((SocialDataAppender) getDataModel().getDataAppender()).getUserAppender();// 返回的是一个用户社交矩阵,该社交矩阵读取的路径是配置文件的“data.appender.path”
}
而 SocialRecommender 类继承的是 MatrixFactorizationRecommender 类,所以下面是 MatrixFactorizationRecommender.java 中的 setup()方法。
protected void setup() throws LibrecException{
super.setup();//详见下面解释
numIterations = conf.getInt("rec.iterator.maximum", 100); //设置最大迭代次数,默认值是100
learnRate = conf.getFloat("rec.iterator.learnrate", 0.01f);//设置学习率,默认值是0.01
maxLearnRate = conf.getFloat("rec.iterator.learnrate.maximum", 1000.0f);//设置最大学习率,默认值是1000
regUser = conf.getFloat("rec.user.regularization", 0.01f);//设置用户正则化项,默认值是0.01
regItem = conf.getFloat("rec.item.regularization", 0.01f);//设置物品正则化项,默认值是0.01
numFactors = conf.getInt("rec.factor.number", 10);//设置隐因子大小,默认值是10
isBoldDriver = conf.getBoolean("rec.learnrate.bolddriver", false);//设置是否在每次迭代中调整学习率,默认值是false
decay = conf.getFloat("rec.learnrate.decay", 1.0f);//设置每次迭代中调整学习率的大小,默认值是1
userFactors = new DenseMatrix(numUsers, numFactors//定义用户隐因子矩阵,大小是(用户数量*隐因子大小)
itemFactors = new DenseMatrix(numItems, numFactors);//定义物品隐因子矩阵,大小是(物品数量*隐因子大小)
initMean = 0.0f;//设置初始平均值为0.0
initStd = 0.001f;//设置初始标准差0.001
// initialize factors
userFactors.init(initMean, initStd);//使用初始平均值和初始标准差初始化用户隐因子矩阵
itemFactors.init(initMean, initStd);//使用初始平均值和初始标准差初始化物品隐因子矩阵
}
而 MatrixFactorizationRecommender 类继承的是 MatrixRecommender 类,所以下面是 MatrixRecommender.java 中的 setup()方法。
protected void setup() throws LibrecException{
super.setup();//详见下面的解释
trainMatrix = (SequentialAccessSparseMatrix) getDataModel().getTrainDataSet();//根据训练集建立一个序列化稀疏矩阵
testMatrix = (SequentialAccessSparseMatrix) getDataModel().getTestDataSet();//根据测试集建立一个序列化稀疏矩阵
validMatrix = (SequentialAccessSparseMatrix) getDataModel().getValidDataSet();//根据验证集建立一个序列化稀疏矩阵
numUsers = trainMatrix.rowSize();//得到训练集中的用户数量
numItems = trainMatrix.columnSize();//得到训练集中的用户数量
numRates = trainMatrix.size();//得到训练集中的所有非0值的数字大小,也即训练集中的评分数量
Set<Double> ratingSet = new HashSet<>();//设置一个哈希集合
for (MatrixEntry matrixEntry : trainMatrix) {//MatrixEntry是矩阵中的每一个值
ratingSet.add(matrixEntry.get());//把训练集中的所有分数放入到这个哈希集合中
}
ratingScale = new ArrayList<>(ratingSet);
Collections.sort(ratingScale);
maxRate = Collections.max(ratingScale);//得到所有打分数的最大值
minRate = Collections.min(ratingScale);//得到所有打分数的最小值
if (minRate == maxRate) {
minRate = 0;
}
globalMean = trainMatrix.mean();//获取训练集所有打分的平均值
//下面的设置是 关于 AUC、Novelty、NDCG、Entropy几个评估值的特殊设置
int[] numDroppedItemsArray = new int[numUsers]; // for AUCEvaluator
int maxNumTestItemsByUser = 0; //for idcg
for (int userIdx = 0; userIdx < numUsers; ++userIdx) {
numDroppedItemsArray[userIdx] = numItems - trainMatrix.row(userIdx).getNumEntries();
int numTestItemsByUser = testMatrix.row(userIdx).getNumEntries();
maxNumTestItemsByUser = maxNumTestItemsByUser < numTestItemsByUser ? numTestItemsByUser : maxNumTestItemsByUser;
}
int[] itemPurchasedCount = new int[numItems]; // for NoveltyEvaluator
for (int itemIdx = 0; itemIdx < numItems; ++itemIdx) {
itemPurchasedCount[itemIdx] = trainMatrix.column(itemIdx).getNumEntries()
+ testMatrix.column(itemIdx).getNumEntries();
}
conf.setInts("rec.eval.auc.dropped.num", numDroppedItemsArray);
conf.setInt("rec.eval.key.test.max.num", maxNumTestItemsByUser); //for nDCGEvaluator
conf.setInt("rec.eval.item.num", testMatrix.columnSize()); // for EntropyEvaluator
conf.setInts("rec.eval.item.purchase.num", itemPurchasedCount); // for NoveltyEvaluator
}
而 MatrixRecommender 类继承的是 AbstractRecommender 类,所以下面是 AbstractRecommender .java 中的 setup()方法。
protected void setup() throws LibrecException {
conf = context.getConf();//通过 RecommenderContext类获取所有的配置项到 conf 变量中,而这些变量就是
//librec-default.properties和具体算法中的配置项,比如sbpr-test.properties
isRanking = conf.getBoolean("rec.recommender.isranking");//获取是否进行排序配置项,比如topN任务这一项都有
if (isRanking) {
topN = conf.getInt("rec.recommender.ranking.topn", 10);//TopN值,默认是10
if (this.topN <= 0) {
throw new IndexOutOfBoundsException("rec.recommender.ranking.topn should be more than 0!");
}
}
earlyStop = conf.getBoolean("rec.recommender.earlystop", false);//是否进行早停策略,默认值是false
verbose = conf.getBoolean("rec.recommender.verbose", true);//是否输出打印信息,就是控制台输出的那些信息,默认值是true
userMappingData = getDataModel().getUserMappingData();//得到用户隐射数据(具体用途,暂时不详)
itemMappingData = getDataModel().getItemMappingData();//得到物品隐射数据(具体用途,暂时不详)
if (verbose) {//如果可以输出打印消息,则设置进度条的大小
progressBar = new ProgressBar(100, 100);
}
}
以上是 SBPR 算法中调用的 super.setup() 方法中包含的所有操作,这些操作可以概括为从 conf 配置项中得到输入的值,进行成员变量的初始化。
值得注意的是,如果具体算法还需要配置额外的参数,可以直接重写这个方法,但记得要调用 super.setup()方法,保证算法的基本参数得到初始化。就像 SBPR 中还设置了很多参数。
根据论文所写,使用这个参数是用来控制每个采样训练对目标的贡献,这里的代码里用 socialWeight 表示,论文里面提出较大的 socialWeight 值表示用户 u 可能更喜欢这个物品,因为他的许多朋友们在这件物品上都表现出了他们的偏好。 所以只要有用户的朋友对这个 socialItemIdx 物品进行过打分,socialWeight 这个值就加一。
根据论文中的训练过程的伪代码示意图,外层循环是迭代次数,内层循环是每一次迭代的 Sample 次数,并且 Sample 次数等于训练集中的正例个数。和这里的代码是符合的(以前我复现 SBPR 时设置的 Sample次数是用户数!!!)
以下是每一次 Sample 的步骤:
我们这里需要更新的参数包括1)用户隐因子矩阵、2)物品隐因子矩阵、3)物品偏置项向量。(代码在上面有,至于具体的推导公式,有空再补上来)
下面是 isConverged(iter) 方法的细节:
protected boolean isConverged(int iter) throws LibrecException {
float delta_loss = (float) (lastLoss - loss);
// 如果verbose为真,输出信息
if (verbose) {
String recName = getClass().getSimpleName();
String info = recName + " iter " + iter + ": loss = " + loss + ", delta_loss = " + delta_loss;
LOG.info(info);
}
//判断是否有异常
if (Double.isNaN(loss) || Double.isInfinite(loss)) {
//LOG.error("Loss = NaN or Infinity: current settings does not fit the recommender! Change the settings and try again!");
throw new LibrecException("Loss = NaN or Infinity: current settings does not fit the recommender! Change the settings and try again!");
}
//判断是否收敛
return Math.abs(delta_loss) < 1e-5;
}
下面是 updateLRate(iter) 方法的细节:
//在每一次迭代之后,根据配置项决定是否更新学习率
protected void updateLRate(int iter) {
//如果学习率小于0,直接结束
if (learnRate < 0.0) {
lastLoss = loss;
return;
}
//如果参数isBoldDriver(是否调整学习率)为真,并且迭代次数大于1,则更新学习率
if (isBoldDriver && iter > 1) {
learnRate = Math.abs(lastLoss) > Math.abs(loss) ? learnRate * 1.05f : learnRate * 0.5f;
} else if (decay > 0 && decay < 1) {
learnRate *= decay;
}
//限制更新的最大学习率为maxLearnRate,配置项里默认值是1000
// limit to max-learn-rate after update
if (maxLearnRate > 0 && learnRate > maxLearnRate) {
learnRate = maxLearnRate;
}
//更新loss值
lastLoss = loss;
}
如果用户 u 没有朋友,或者用户 u 和朋友没有共同都没有打过分的物品,则SBPR 训练降为 BPR 训练。
以下是 SBPRRecommender.java 中的 predict() 方法,这里直接重写了基类的 predict 方法,一般预测用户对某一个物品具体的分数值的代码是double predictRating = userFactors.row(userIdx).dot(itemFactors.row(itemIdx))
,而这里的 predict 方法加了具体物品的偏置项
//预测具体的userIdx和itemIdx的分数
protected double predict(int userIdx, int itemIdx) throws LibrecException {
double predictRating = itemBiases.get(itemIdx) + userFactors.row(userIdx).dot(itemFactors.row(itemIdx));//这个预测分数值还加入了每一个物品的偏置项值,详细看解释3
return predictRating;
}
以上,完(๑′ᴗ‵๑)I Lᵒᵛᵉᵧₒᵤ❤
以上是本人走读 SBPR 算法的笔记,希望对研究 SBPR 代码如何写的你有帮助
ヾ(◍°∇°◍)ノ゙
本人目前刚开始学习使用 LibRec,欢迎同伴一起交流进步,哪里有写的不对的地方,欢迎评论指正呀!ヾ(◍°∇°◍)ノ゙
如果这篇博客帮助了您,可以请我吃包5毛钱的辣条吗?(下面为微信收款码)或者点个赞也行呀!您小小的鼓励会是我持续更新的动力!ヾ(◍°∇°◍)ノ゙