Libimseti推荐系统

来源:互联网 发布:ubuntu rar 解压命令 编辑:程序博客网 时间:2024/04/29 14:51

技术:easyUI、jQuery、Spring、Struts、Hibernate、Mahout、MySQL

本Libimseti推荐系统使用数据、代码参考《Mahout in action》第五章内容。

系统可以从这里下载:libimesti推荐系统 或  http://pan.baidu.com/s/1nvzqWcx (包含源码)。

1. 系统部署

1.1 数据库

(1)修改Configuration目录中的db.properties中的数据库配置;
(2)从http://pan.baidu.com/s/1bOzCtC 下载所需要的数据,解压后可以看到gender.dat 和ratings.dat文件;
(3)启动工程,自动生成相关表;
(4)在数据库中运行src目录下*.sql,导入相关数据;

1.2 公共配置

(1)修改src目录下com.fz.util.Utils中的genderFile和ratingsFile变量为正确文件地址;

2. 系统功能

2.1 Libimseti推荐

启动tomcat,访问http://localhost:8080/rec 即可访问系统主页,如下:

2.1.1 用户评分档案查询

在推荐算法页面点击”查询”按钮,即可根据用户ID输入框里面的用户ID查询用户对其他档案的评分,同时这里把用户的性别和档案的性别一起查出来了。
这里显示使用的是easyUI的datagrid,其后台代码如下:
$('#ratingId').datagrid({border:false,//fitColumns:true,singleSelect:true,//width:600,height:200,nowrap:false,//fit:true,pagination:true,//分页控件pageSize:4,  // 每页记录数,需要和pageList保持倍数关系pageList:[4,8,12],rownumbers:true,//行号//pagePosition:'bottom',url:'rating/rating_getRatingData.action',queryParams: {uid: userIdValue,selectgender:selectGender},toolbar: "#toolbar",columns:[[{field:'id',title:'用户ID',width:'50'},{field:'gender',title:'用户Gender',width:'80'},{field:'itemId',title:'档案ID',width:'120'},{field:'pref',title:'档案评分',width:'150'},{field:'itemGender',title:'档案Gender',width:'100'},{field:'desc',title:'档案描述',width:'200',},]]});
使用了toolbar,提供“添加”、“修改”和“删除“功能,使用分页组件用于分页显示查询数据;由于用户评分数据和用户性别数据是在两个表中,所以新建了一个中间类UserRating用于组装数据传入前台,代码如下:
public Map<String,Object> getRatingByUId(Integer uId,int rows,int page, char selectgender){String hql = "from Rating r where UID="+uId +" order by r.uId,r.itemId";String hqlCount ="select count(1) from Rating where UID="+uId;String hqlGender = "from Gender where UID="+uId;List<Rating> ratings = baseDAO.find(hql,new Object[]{},page,rows);List<UserRating> userRatings = new ArrayList<UserRating>();if(ratings.size()<=0){return null;}// 获取用户Gender//List<Gender> gender =genderDAO.find(hqlGender);char uGender = genderDAO.find(hqlGender).get(0).getGender();char itemGender;UserRating ur = null;for(Rating rating:ratings){ur= new UserRating();ur.setId(uId);ur.setDesc(rating.getDesc());ur.setItemId(rating.getItemId());ur.setPref(rating.getPref());ur.setGender(uGender);// 获取ITEM genderhqlGender="from Gender g where UID="+rating.getItemId();itemGender =genderDAO.find(hqlGender).get(0).getGender();ur.setItemGender(itemGender);userRatings.add(ur);}Map<String ,Object> jsonMap = new HashMap<String,Object>();jsonMap.put("total", baseDAO.count(hqlCount));jsonMap.put("rows", userRatings);return jsonMap;}
这里的selectgender变量,本来是在页面添加的一个用于在查询时过滤性别的变量,后面感觉有点麻烦就没做了(性别数据在gender表,分页针对的是rating表);

2.1.2 用户添加对其他未评分档案数据

用户添加对其他未评分档案数据的页面如下(点击toolbar中的”添加“按钮):

这里使用的easyUI的window组件,打开页面后根据用户的信息先初始化用户ID和用户性别两个性别,且不可修改,用户需要输入档案ID(必选项)、档案性别和档案描述;
用户输入档案ID的时候,使用ajax实时向后台发送消息,查询用户是否对档案ID已经评分过,如果评分过就进行如图的提示,此功能首先对validatebox进行扩展,然后使用Validator的框架进行验证,代码如下:
// 用户在增加对其他项目评分的时候,需要检查是否该项目用户已经评过分 $.extend($.fn.validatebox.defaults.rules, {hasItem : {validator : function(value,param) {var uid = $('#uidId').val();console.info("value:"+value+",user:"+uid);return hasItem(uid,value);},message : '用户已对该项目评过分!'}});// 检查用户是否对项目评过分function hasItem(user,item){if(isNaN(parseInt(item))){return false;}var boolean =false;$.ajax({ // 获取数据url : "rating/rating_hasItem.action",data : "uid=" + user+"&itemid="+item,dataType : "json",async:false,success : function(data) {console.info("用户"+user+"是否对项目"+item+"评分?"+data);// 设置if(data==false||data=="false"){boolean=true;}}});return boolean;}
这样在jsp页面就可以简单的使用下面的代码即可:
<input class="easyui-validatebox" type="text" name="itemid" id="itemidId" style="width:100px" data-options="required:true,validType:'hasItem'"/>

2.1.3 用户修改当前档案信息

修改用户当前档案信息界面如下:

其中的用户ID和档案ID是不可修改的;这里弹出的window和添加功能界面的window是一样的,这里在弹出界面的时候修改其title。

2.1.4 删除用户对当前档案数据

删除用户对当前档案数据需要用户进一步确认:

2.1.5 非过滤推荐

在tomcat启动过程中会对推荐系统进行初始化,这样在推荐的时候直接可以使用推荐模型进行推荐,这样推荐的时候就不用等待过多时间;
默认使用过滤推荐,非过滤推荐即不使用用户的gender数据对最后的推荐数据进行过滤;
jquery获取是否过滤推荐的checkbox的状态:
$('#filterId').click(function() {        if(this.checked){    filter=true;    }else{    filter=false;    }    console.info("filter:"+filter);});
推荐同样使用easyUI的datagrid,其js如下:
$('#recommendId').datagrid({border:false,singleSelect:true,height:180,nowrap:false,pagination:true,//分页控件pageSize:4,  // 每页记录数,需要和pageList保持倍数关系pageList:[4,8,12],rownumbers:true,//行号url:'rec/rec_getRecommendData.action',queryParams: {uid: userIdValue,filter:filter},columns:[[{field:'uid',title:'用户ID',width:'50'},{field:'ugender',title:'用户Gender',width:'80'},{field:'itemid',title:'档案ID',width:'120'},{field:'pref',title:'档案评分',width:'150'},{field:'itemgender',title:'档案Gender',width:'100'},]]});
这里传入后台的参数中包括filter和uid,filter即是否使用过滤;

2.1.6 过滤推荐

首先,这里使用Mahout的基于用户的协同过滤算法进行推荐(非MR方式);
其次,这里的过滤规则如下:首先计算出用户评价过的档案中的性别的较大值,比如M(men)(即对哪类性别的档案评分比较多),然后在对用户进行推荐的可能档案中不对非M的进行计算,直接去掉,这样在最后推荐的时候就不会出现非M性别的档案了。
推荐使用Mahout的基于用户的协同过滤算法,同时在《Mahout in action》中对这个代码进行了包装,代码如下:
package com.fz.service;import java.io.File;import java.io.IOException;import java.util.ArrayList;import java.util.Collection;import java.util.HashMap;import java.util.List;import java.util.Map;import javax.annotation.Resource;import org.apache.mahout.cf.taste.common.Refreshable;import org.apache.mahout.cf.taste.common.TasteException;import org.apache.mahout.cf.taste.impl.common.FastIDSet;import org.apache.mahout.cf.taste.impl.model.file.FileDataModel;import org.apache.mahout.cf.taste.impl.neighborhood.NearestNUserNeighborhood;import org.apache.mahout.cf.taste.impl.recommender.GenericUserBasedRecommender;import org.apache.mahout.cf.taste.impl.similarity.EuclideanDistanceSimilarity;import org.apache.mahout.cf.taste.model.DataModel;import org.apache.mahout.cf.taste.neighborhood.UserNeighborhood;import org.apache.mahout.cf.taste.recommender.IDRescorer;import org.apache.mahout.cf.taste.recommender.RecommendedItem;import org.apache.mahout.cf.taste.recommender.Recommender;import org.apache.mahout.cf.taste.similarity.UserSimilarity;import org.springframework.stereotype.Service;import com.fz.dao.BaseDAO;import com.fz.model.Gender;import com.fz.model.RecommendRating;import com.fz.util.GenderRescorer;import com.fz.util.Utils;/** * libimseti 推荐 * 使用《Mahout in action 》第五章代码 * 使用MySQL数据库作为数据源,则算法很慢 * @author fansy * */@Service("recommend")public class LibimsetiRecommender implements Recommender {private  Recommender delegate;private DataModel model;private FastIDSet men;private FastIDSet women;private FastIDSet usersRateMoreMen;private FastIDSet usersRateLessMen;@Resourceprivate BaseDAO<Gender> genderDAO;private boolean filter=true;/** * 从数据库中获取DataModel * @return * @throws IOException  * @throws TasteException  * @throws NumberFormatException  */public LibimsetiRecommender() throws NumberFormatException, TasteException, IOException{this(localDataModel());}private static  DataModel localDataModel() throws IOException {FileDataModel dataModel = new FileDataModel(new File(Utils.ratingsFile));return dataModel;}public LibimsetiRecommender(DataModel model) throws TasteException, NumberFormatException, IOException{UserSimilarity similarity = new EuclideanDistanceSimilarity(model);UserNeighborhood neighborhood =new NearestNUserNeighborhood(4,similarity,model);// 增大n值可以获得更多推荐delegate =new GenericUserBasedRecommender(model,neighborhood,similarity);this.model=model;FastIDSet[] menWomen = GenderRescorer.parseMenWomen(new File(Utils.genderFile));men = menWomen[0];women = menWomen[1];usersRateMoreMen = new FastIDSet(50000);usersRateLessMen = new FastIDSet(50000);}@Overridepublic void refresh(Collection<Refreshable> alreadyRefreshed) {delegate.refresh(alreadyRefreshed);}@Overridepublic List<RecommendedItem> recommend(long userID, int howMany)throws TasteException {IDRescorer rescorer= null;if(filter){rescorer=new GenderRescorer(men,women,usersRateMoreMen,usersRateLessMen,userID,model);}return delegate.recommend(userID, howMany, rescorer);}/** * 推荐整合 * @throws TasteException  */public Map<String, Object> recommend(long userID,int rows,int page,boolean filter) throws TasteException{this.filter=filter;String gHql = "from Gender g where g.uId=?";List<RecommendedItem> recommend = recommend(userID,20);Map<String ,Object> jsonMap = new HashMap<String,Object>();List<RecommendRating> tmp = new ArrayList<RecommendRating>();RecommendRating rating =null;if(recommend.size()<=0){ rating = new RecommendRating(); rating.setUid(-1);rating.setUgender('U');rating.setItemid(-1);rating.setItemgender('U');rating.setPref(-1);tmp.add(rating);jsonMap.put("total", recommend.size());jsonMap.put("rows", tmp);return jsonMap;}List<RecommendRating> recommendRatings = new ArrayList<RecommendRating>();char uGender = genderDAO.get(gHql, new Object[]{(int)userID}).getGender();for(RecommendedItem re:recommend){rating = new RecommendRating();rating.setUid(userID);rating.setUgender(uGender);rating.setItemid(re.getItemID());rating.setItemgender(genderDAO.get(gHql, new Object[]{(int)re.getItemID()}).getGender());rating.setPref(re.getValue());recommendRatings.add(rating);}for(int i=(page-1)*rows;i<page*rows&&i<recommend.size();i++){tmp.add(recommendRatings.get(i));}jsonMap.put("total", recommend.size());jsonMap.put("rows", tmp);return jsonMap;}@Overridepublic List<RecommendedItem> recommend(long userID, int howMany,boolean includeKnownItems) throws TasteException {return delegate.recommend(userID, howMany, includeKnownItems);}@Overridepublic List<RecommendedItem> recommend(long userID, int howMany,IDRescorer rescorer) throws TasteException {return delegate.recommend(userID, howMany, rescorer);}@Overridepublic List<RecommendedItem> recommend(long userID, int howMany,IDRescorer rescorer, boolean includeKnownItems)throws TasteException {return delegate.recommend(userID, howMany, rescorer, includeKnownItems);}@Overridepublic float estimatePreference(long userID, long itemID)throws TasteException {IDRescorer rescorer= new GenderRescorer(men,women,usersRateMoreMen,usersRateLessMen,userID,model);return (float)rescorer.rescore(userID, itemID);}@Overridepublic void setPreference(long userID, long itemID, float value)throws TasteException {delegate.setPreference(userID, itemID, value);}@Overridepublic void removePreference(long userID, long itemID)throws TasteException {delegate.removePreference(userID, itemID);}@Overridepublic DataModel getDataModel() {return delegate.getDataModel();}public Recommender getDelegate() {return delegate;}public void setDelegate(Recommender delegate) {this.delegate = delegate;}public DataModel getModel() {return model;}public void setModel(DataModel model) {this.model = model;}public FastIDSet getMen() {return men;}public void setMen(FastIDSet men) {this.men = men;}public FastIDSet getWomen() {return women;}public void setWomen(FastIDSet women) {this.women = women;}public FastIDSet getUsersRateMoreMen() {return usersRateMoreMen;}public void setUsersRateMoreMen(FastIDSet usersRateMoreMen) {this.usersRateMoreMen = usersRateMoreMen;}public FastIDSet getUsersRateLessMen() {return usersRateLessMen;}public void setUsersRateLessMen(FastIDSet usersRateLessMen) {this.usersRateLessMen = usersRateLessMen;}public boolean isFilter() {return filter;}public void setFilter(boolean filter) {this.filter = filter;}}
代码分析:
1. 初始化时首先会调用localDataModel方法,这个方法用于初始化数据模型,我曾试过使用mysqlDataSource做为数据源,但是计算太慢了。
2. 带参数的LibimsetiRecommender构造方法,就是基本的推荐代码了创建UserSimilarity、UserNeighborhood对象,这里的n值(代码中为4)可以根据自己的需要进行调整,原书中为2;同时在这个构造方法中还对gender数据进行了读取,把数据放入内存,方便根据用户ID查询性别。
3. 推荐使用recommend(int userid ,int howmany)即可,这里代码使用的howmany固定为20;同时由于数据需要传入前台,同时考虑到分页,所以写了一个recommend(long userID,int rows,int page,boolean filter)方法,用于进行数据分页处理。
4. 在recommend(int userid,int howmany)中如果使用了过滤,那么就初始化IDRescorer为GenderRescorer,其中GenderRescorer为自定义过滤器,这里需要注意代码清单 Listing5.4 Gender-based rescoring Implementation中的代码有一个地方有问题,原版为:
public boolean isFiltered(long id) {// TODO Auto-generated method stubreturn filterMen? men.contains(id):women.contains(id);}
需要改为:
public boolean isFiltered(long id) {// TODO Auto-generated method stubreturn filterMen? (!men.contains(id)):(!women.contains(id));}
isFiltered方法其解释为 true to exclude, false otherwise,这个解释和代码是不一样的;

2.1.7 过滤推荐和非过滤推荐对比

比如针对用户ID为8的用户,其过滤推荐为:

这里其实现实的是没有推荐,再看非过滤推荐:

这里可以看到有3个推荐,但是如果对用户ID为8的用户使用非过滤推荐,那么可以看到这个用户可能是GAY,但是从用户8的评分数据来看,其对F(Female)的档案评分比较多,这说明这3个推荐是不合理的,需要过滤,那么过滤推荐就可以过滤掉这三个推荐数据了。

2.1.8 匿名推荐

待更新。

2.2 目录维护

2.2.1 目录修改

点击导航配置Tab,可以看到目录维护的界面:

这里的操作里面的按钮,使用下面的方式生成:
$(function() {$('#catalogId').datagrid({border : false,fitColumns : true,singleSelect : true,width : 600,height : 250,nowrap : false,fit : true,pagination : true,// 分页控件pageSize : 4, // 每页记录数,需要和pageList保持倍数关系pageList : [ 4, 8, 12 ],rownumbers : true,// 行号pagePosition : 'top',url : 'catalog/catalog_getTreeData.action',columns : [ [{field : 'id',title : '节点ID',width : '40'},{field : 'text',title : '节点名称',width : '120'},{field : 'url',title : 'URL',width : '150'},{field : 'pid',title : '父节点ID',width : '60'},{field : 'iconCls',title : '图标',width : '100'},{field : 'opt',title : '操作',width : '40',formatter : function(value, row, index) {var btn_edit = '<button type="button"  onclick="update('+ row.id + ')">编辑</button>';var btn_remove = '<button type="button"  onclick="deleteRow('+ row.id + ')">删除</button>';return btn_edit + btn_remove;}} ] ]});});

2.2.1 目录添加

点击添加按钮,可以对目录进行添加,其界面如下:


其中,图标使用combobox,其图标添加代码如下:

$('#iconId').combobox({formatter : function(row) {var imageFile = 'themes/icons/' + row.icon;console.info('imageFile' + imageFile);return '<img class="item-img" src="' + imageFile+ '"/>  <span class="item-text">'+ row.text + '</span>';}});


分享,成长,快乐

转载请注明blog地址:http://blog.csdn.net/fansy1990



0 0
原创粉丝点击