首页 技术 正文
技术 2022年11月15日
0 收藏 460 点赞 4,420 浏览 10658 个字

最近在拜读项亮博士的《推荐系统实践》,系统的学习一下推荐系统的相关知识。今天学习了其中的隐语义模型在Top-N推荐中的应用,在此做一个总结。

隐语义模型LFM和LSI,LDA,Topic Model其实都属于隐含语义分析技术,是一类概念,他们在本质上是相通的,都是找出潜在的主题或分类。这些技术一开始都是在文本挖掘领域中提出来的,近些年它们也被不断应用到其他领域中,并得到了不错的应用效果。比如,在推荐系统中它能够基于用户的行为对item进行自动聚类,也就是把item划分到不同类别/主题,这些主题/类别可以理解为用户的兴趣。

对于一个用户来说,他们可能有不同的兴趣。就以作者举的豆瓣书单的例子来说,用户A会关注数学,历史,计算机方面的书,用户B喜欢机器学习,编程语言,离散数学方面的书, 用户C喜欢大师Knuth, Jiawei Han等人的著作。那我们在推荐的时候,肯定是向用户推荐他感兴趣的类别下的图书。那么前提是我们要对所有item(图书)进行分类。那如何分呢?大家注意到没有,分类标准这个东西是因人而异的,每个用户的想法都不一样。拿B用户来说,他喜欢的三个类别其实都可以算作是计算机方面的书籍,也就是说B的分类粒度要比A小;拿离散数学来讲,他既可以算作数学,也可当做计算机方面的类别,也就是说有些item不能简单的将其划归到确定的单一类别;拿C用户来说,他倾向的是书的作者,只看某几个特定作者的书,那么跟A,B相比它的分类角度就完全不同了。

显然我们不能靠由单个人(编辑)或team的主观想法建立起来的分类标准对整个平台用户喜好进行标准化。

此外我们还需要注意的两个问题:

  1. 我们在可见的用户书单中归结出3个类别,不等于该用户就只喜欢这3类,对其他类别的书就一点兴趣也没有。也就是说,我们需要了解用户对于所有类别的兴趣度。
  2. 对于一个给定的类来说,我们需要确定这个类中每本书属于该类别的权重。权重有助于我们确定该推荐哪些书给用户。

下面我们就来看看LFM是如何解决上面的问题的?对于一个给定的用户行为数据集(数据集包含的是所有的user, 所有的item,以及每个user有过行为的item列表),使用LFM对其建模后,我们可以得到如下图所示的模型:(假设数据集中有3个user, 4个item, LFM建模的分类数为4)

 使用LFM(Latent factor model)隐语义模型进行Top-N推荐

R矩阵是user-item矩阵,矩阵值Rij表示的是user i 对item j的兴趣度,这正是我们要求的值。对于一个user来说,当计算出他对所有item的兴趣度后,就可以进行排序并作出推荐。LFM算法从数据集中抽取出若干主题,作为user和item之间连接的桥梁,将R矩阵表示为P矩阵和Q矩阵相乘。其中P矩阵是user-class矩阵,矩阵值Pij表示的是user i对class j的兴趣度;Q矩阵式class-item矩阵,矩阵值Qij表示的是item j在class i中的权重,权重越高越能作为该类的代表。所以LFM根据如下公式来计算用户U对物品I的兴趣度

使用LFM(Latent factor model)隐语义模型进行Top-N推荐

我们发现使用LFM后,

  1. 我们不需要关心分类的角度,结果都是基于用户行为统计自动聚类的,全凭数据自己说了算。
  2. 不需要关心分类粒度的问题,通过设置LFM的最终分类数就可控制粒度,分类数越大,粒度约细。
  3. 对于一个item,并不是明确的划分到某一类,而是计算其属于每一类的概率,是一种标准的软分类。
  4. 对于一个user,我们可以得到他对于每一类的兴趣度,而不是只关心可见列表中的那几个类。
  5. 对于每一个class,我们可以得到类中每个item的权重,越能代表这个类的item,权重越高。

那么,接下去的问题就是如何计算矩阵P和矩阵Q中参数值。一般做法就是最优化损失函数来求参数。在定义损失函数之前,我们需要准备一下数据集并对兴趣度的取值做一说明。

数据集应该包含所有的user和他们有过行为的(也就是喜欢)的item。所有的这些item构成了一个item全集。对于每个user来说,我们把他有过行为的item称为正样本,规定兴趣度RUI=1,此外我们还需要从item全集中随机抽样,选取与正样本数量相当的样本作为负样本,规定兴趣度为RUI=0。因此,兴趣的取值范围为[0,1]。

采样之后原有的数据集得到扩充,得到一个新的user-item集K={(U,I)},其中如果(U,I)是正样本,则RUI=1,否则RUI=0。损失函数如下所示:

使用LFM(Latent factor model)隐语义模型进行Top-N推荐

上式中的使用LFM(Latent factor model)隐语义模型进行Top-N推荐是用来防止过拟合的正则化项,λ需要根据具体应用场景反复实验得到。损失函数的优化使用随机梯度下降算法:

  1. 通过求参数PUK和QKI的偏导确定最快的下降方向;

使用LFM(Latent factor model)隐语义模型进行Top-N推荐

  1. 迭代计算不断优化参数(迭代次数事先人为设置),直到参数收敛。

使用LFM(Latent factor model)隐语义模型进行Top-N推荐

其中,α是学习速率,α越大,迭代下降的越快。α和λ一样,也需要根据实际的应用场景反复实验得到。本书中,作者在MovieLens数据集上进行实验,他取分类数F=100,α=0.02,λ=0.01。
               【注意】:书中在上面四个式子中都缺少了使用LFM(Latent factor model)隐语义模型进行Top-N推荐

综上所述,执行LFM需要:

  1. 根据数据集初始化P和Q矩阵(这是我暂时没有弄懂的地方,这个初始化过程到底是怎么样进行的,还恳请各位童鞋予以赐教。)
  2. 确定4个参数:分类数F,迭代次数N,学习速率α,正则化参数λ。

LFM的伪代码可以表示如下:

  1. def LFM(user_items, F, N, alpha, lambda):
  2. #初始化P,Q矩阵
  3. [P, Q] = InitModel(user_items, F)
  4. #开始迭代
  5. For step in range(0, N):
  6. #从数据集中依次取出user以及该user喜欢的iterms集
  7. for user, items in user_item.iterms():
  8. #随机抽样,为user抽取与items数量相当的负样本,并将正负样本合并,用于优化计算
  9. samples = RandSelectNegativeSamples(items)
  10. #依次获取item和user对该item的兴趣度
  11. for item, rui in samples.items():
  12. #根据当前参数计算误差  PS:转载的博客中rui写成了eui
  13. eui = rui – Predict(user, item)
  14. #优化参数
  15. for f in range(0, F):
  16. P[user][f] += alpha * (eui * Q[f][item] – lambda * P[user][f])
  17. Q[f][item] += alpha * (eui * P[user][f] – lambda * Q[f][item])
  18. #每次迭代完后,都要降低学习速率。一开始的时候由于离最优值相差甚远,因此快速下降;
  19. #当优化到一定程度后,就需要放慢学习速率,慢慢的接近最优值。
  20. alpha *= 0.9

本人对书中的伪代码追加了注释,有不对的地方还请指正。

当估算出P和Q矩阵后,我们就可以使用(*)式计算用户U对各个item的兴趣度值,并将兴趣度值最高的N个iterm(即TOP N)推荐给用户。

总结来说,LFM具有成熟的理论基础,它是一个纯种的学习算法,通过最优化理论来优化指定的参数,建立最优的模型.

========================我是分割线我自豪=============================

我不懂Python, 所以按照书里的步骤用java实现了, 期间走了好多弯路

在这里说一下, 初始值的选择, 在迭代的时候alpha的初始值切记不要选太大了, 我之前一直用0.1, 然后每次都没收敛, 晕死我了, 还一直改代码+调试

以为是其它原因, 浪费了大半天时间

最后不小心把alpha改为0.5后发现就正常了

改时间把代码扔上来

准备下班了……………………

————————————————-我是不需要理由的分割线—————————————————-

时隔好久,为了准备面试,重要把之前的代码复习了一遍,顺便整理好放到Github上

现在就扔到博客上来吧

LFM的核心代码模块

 package org.juefan.alg; import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set; public class LFM { public static final int latent = 100;
public static double alpha = 0.03;
public static double lambda = 0.01;
public static final int iteration = 1;
public static final int resys = 10; public static Map<Integer, List<Float>> UserMap = new HashMap<Integer, List<Float>>();
public static Map<Integer, List<Float>> ItemMap = new HashMap<Integer, List<Float>>(); public static compares compare = new compares(); public class State{
public int TemID;
public Set<Integer> set = new HashSet<Integer>();
public float sim; /**用户集排序*/
public State(Set<Integer> s, float s2){
set.addAll(s);
sim = s2;
} /**Item排序*/
public State(Integer i, float s){
TemID = i;
sim = s;
}
} public static class compares implements Comparator<Object>{
@Override
public int compare(Object o1, Object o2) {
State s1 = (State)o1;
State s2 = (State)o2;
return s1.sim < s2.sim ? 1:-1;
}
} public String toString(){
return "LFM";
}
/**
* 加载用户与项目的集合并初始化隐含矩阵
* 注意隐含层的数值不能太大,建议在0.05左右
* @param user
* @param item
*/
public LFM(Set<Integer> user, Set<Integer> item){
for(Integer u:user){
List<Float> tList = new ArrayList<Float>();
for(int i = 0; i < latent; i++)
tList.add((float) ((float) Math.random() * 0.1));
UserMap.put(u, tList);
}
for(Integer u:item){
List<Float> tList = new ArrayList<Float>();
for(int i = 0; i < latent; i++)
tList.add((float) ((float) Math.random() *0.1));
ItemMap.put(u, tList);
}
}
public LFM() {
// TODO Auto-generated constructor stub
}
/**
* 计算用户对某个物品的兴趣
* @param uLV 用户与隐含类的关系
* @param iLV 隐含类与物品的关系
* @return 返回用户对某个物品的兴趣
*/
public static float getPreference(List<Float> uLV, List<Float> iLV){
float p = 0;
for(int i = 0; i < latent; i++){
p = p + uLV.get(i) * iLV.get(i);
}
return p;
} /**预测评分差*/
public static float Predict(float i1, float i2){
return i1 - i2;
} /**
* 迭代求解隐含层
* @param UserItem
*/
public static void LatentFactorModel(Map<Integer, Map<Integer, Float>> UserItem){
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd_HH-mm");//设置日期格式
for(int i = 0; i < iteration; i++){
System.out.println( df.format(new Date()) + "\t第 " + (i + 1) + " 次迭代");
for(int user: UserItem.keySet()){
for(int item: UserItem.get(user).keySet()){
float error = Predict(UserItem.get(user).get(item),
getPreference(UserMap.get(user), ItemMap.get(item)));
for(int i1 = 0; i1 < latent; i1++){
UserMap.get(user).set(i1, (float) (UserMap.get(user).get(i1) + alpha *
(ItemMap.get(item).get(i1) * error - lambda * UserMap.get(user).get(i1))));
if (Float.isNaN(UserMap.get(user).get(i1) )) {
System.err.println("矩阵初始化或者参数有问题导致矩阵出现数值溢出");
}
ItemMap.get(item).set(i1, (float) (ItemMap.get(item).get(i1) + alpha *
(UserMap.get(user).get(i1) * error - lambda * ItemMap.get(item).get(i1))));
}
}
}
alpha = (float) (alpha * 0.9);
}
} /**
* 获取用户的最终推荐列表
* @param map 项目的得分值表
* @return
*/
public Set<Integer> getResysK(Map<Integer, Float> map){
List<State> tList = new ArrayList<State>();
Set<Integer> set = new HashSet<Integer>();
for(Integer key: map.keySet()){
tList.add(new State(key, map.get(key)));
}
Collections.sort(tList, compare);
for(int i = 0; i < tList.size() && i < resys; i++){
set.add(tList.get(i).TemID);
}
return set;
} /**
* 计算用户的推荐列表
* @param user 用户的ID
* @param item 用户的训练集
* @return 用户的推荐列表
*/
public Set<Integer> getResysList(int user, Map<Integer, Float> item){
Map<Integer, Float> map = new HashMap<Integer, Float>();
for(int i: ItemMap.keySet()){
if(!item.containsKey(i))
map.put(i, getPreference(UserMap.get(user), ItemMap.get(i)));
}
return getResysK(map);
}
}

接下来是具体的训练操作,比较重点的是训练数据和测试数据的选择,还有负例的生成

 package org.juefan.alg.test; import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set; import org.juefan.IO.FileIO;
import org.juefan.alg.LFM;
import org.juefan.data.RatingData;
import org.juefan.eva.Evaluation; public class TestLFM { public static Set<Integer> user = new HashSet<Integer>();
public static Set<Integer> item = new HashSet<Integer>();
public static List<Integer> itemList = new ArrayList<Integer>();
public static Map<Integer, Integer> map = new HashMap<Integer, Integer>();
public static Map<Integer, Integer> randMap = new HashMap<Integer, Integer>(); //倾向选择热门且用户未评价的为负例 /**用户项目训练数据*/
public static Map<Integer, Map<Integer, Float>> UserItemTrain = new HashMap<Integer, Map<Integer, Float>> ();
/**用户项目测试数据*/
public static Map<Integer, Map<Integer, Float>> UserItemTest = new HashMap<Integer, Map<Integer, Float>> (); public static LFM lfm = new LFM(); public static Map<Integer, Float> getFu(Map<Integer, Float> item){
Map<Integer, Float> map = new HashMap<Integer, Float>();
while(map.size() < item.size()*4 && item.size() + map.size() < TestLFM.item.size() * 0.8){
/**抑制热门方式*/
/*int rand = (int) (Math.random() * randMap.size());
if(!item.containsKey(randMap.get(rand))){
map.put(randMap.get(rand), (float) 0);
}*/
/**同等对待方式*/
int rand = (int) (Math.random() * TestLFM.itemList.size());
if(!item.containsKey( TestLFM.itemList.get(rand))){
map.put( TestLFM.itemList.get(rand), (float) 0);
}
}
return map;
} /**将Map的key加载进Set类*/
public static Set<Integer> MapToSet(Map<Integer, Float> item){
Set<Integer> tSet = new HashSet<Integer>();
for(int k: item.keySet())
tSet.add(k);
return tSet;
} /**
* 测试入口
*/
public static void main(String[] args) {
System.setProperty("java.util.Arrays.useLegacyMergeSort", "true");
FileIO fileIO = new FileIO();
fileIO.SetfileName(System.getProperty("user.dir") + "\\data\\input\\ml-1m\\ratings.dat");
fileIO.FileRead();
List<String> list = fileIO.cloneList();
int num = 0;
for(String s:list){
RatingData data = new RatingData(s);
float rand = (float) Math.random();
if(rand >= (float)1/8){ //将数据随机分成训练数据和测试数据
if(UserItemTrain.containsKey(data.userID)){
UserItemTrain.get(data.userID).put(data.movieID, (float) 1);
}else {
Map<Integer, Float> tMap = new HashMap<Integer, Float>();
tMap.put(data.movieID, (float) 1);
UserItemTrain.put(data.userID, tMap);
}
//计算每个项目的热度
if(map.containsKey(data.movieID)){
map.put(data.movieID, map.get(data.movieID) + 1);
}else {
map.put(data.movieID, 1);
}
//构造项目分布映射
randMap.put(num++, data.movieID);
//收集用户列表和项目列表
user.add(data.userID);
item.add(data.movieID);
}else {
if(UserItemTest.containsKey(data.userID)){
UserItemTest.get(data.userID).put(data.movieID, (float) 1);
}else {
Map<Integer, Float> tMap = new HashMap<Integer, Float>();
tMap.put(data.movieID, (float) 1);
UserItemTest.put(data.userID, tMap);
}
}
} System.out.println("正在构造罗盘赌");
for(Integer item: TestLFM.item){
itemList.add(item);
}
int Fu = 0;
for(int user: UserItemTrain.keySet()){
UserItemTrain.get(user).putAll(getFu(UserItemTrain.get(user)));
if(++Fu % 1000 == 0)
System.out.println("已构造 " + Fu +" 个负样本用户数据");
}
System.out.println("负样本生成完毕"); SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd_HH-mm");//设置日期格式
String dataString = "\\data\\output\\Result\\" + df.format(new Date()) + "_result.txt";
LFM lfm = new LFM(user, item);
for(int trac = 0; trac <= 20; trac++){
LFM.LatentFactorModel(UserItemTrain);
for(int user:UserItemTrain.keySet()){
if(UserItemTest.containsKey(user)){
Evaluation.setEvaluation(MapToSet(UserItemTest.get(user)), lfm.getResysList(user, UserItemTrain.get(user)));
}
}
System.out.println("准确率 = " + Evaluation.getPrecision() * 100 + "%\t\t召回率 = " + Evaluation.getRecall() * 100 + "%\t\t覆盖率 = " + Evaluation.getCoverage()/item.size() * 100 + "%");
FileIO.FileWrite(System.getProperty("user.dir") + dataString, "===================使用算法 : " + lfm.toString()
+ "=====================\n具体参数: "
+ "\nlatent = " + LFM.latent
+"\nalpha = " + LFM.alpha
+"\nlambda = " + LFM.lambda
+ "\n准确率 = " + Evaluation.getPrecision() * 100 + "%\t\t召回率 = " + Evaluation.getRecall() * 100 + "%\t\t覆盖率 = " + Evaluation.getCoverage()/item.size() * 100 + "%\n", true);
}
} }

好了,基本上就这样了

如果要看完整的代码欢迎到本人的Github上查看,里面还有相应的数据,还有一个UserCF代码模块

Github地址:https://github.com/JueFan/RecommendSystem

参考文章: http://blog.csdn.net/harryhuang1990/article/details/9924377

相关推荐
python开发_常用的python模块及安装方法
adodb:我们领导推荐的数据库连接组件bsddb3:BerkeleyDB的连接组件Cheetah-1.0:我比较喜欢这个版本的cheeta…
日期:2022-11-24 点赞:878 阅读:8,991
Educational Codeforces Round 11 C. Hard Process 二分
C. Hard Process题目连接:http://www.codeforces.com/contest/660/problem/CDes…
日期:2022-11-24 点赞:807 阅读:5,505
下载Ubuntn 17.04 内核源代码
zengkefu@server1:/usr/src$ uname -aLinux server1 4.10.0-19-generic #21…
日期:2022-11-24 点赞:569 阅读:6,349
可用Active Desktop Calendar V7.86 注册码序列号
可用Active Desktop Calendar V7.86 注册码序列号Name: www.greendown.cn Code: &nb…
日期:2022-11-24 点赞:733 阅读:6,134
Android调用系统相机、自定义相机、处理大图片
Android调用系统相机和自定义相机实例本博文主要是介绍了android上使用相机进行拍照并显示的两种方式,并且由于涉及到要把拍到的照片显…
日期:2022-11-24 点赞:512 阅读:7,766
Struts的使用
一、Struts2的获取  Struts的官方网站为:http://struts.apache.org/  下载完Struts2的jar包,…
日期:2022-11-24 点赞:671 阅读:4,844