IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    [原]Libimseti推荐系统

    fansy1990发表于 2014-09-30 11:34:21
    love 0

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

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

    系统可以从这里下载:libimesti推荐系统。

    1. 系统部署

    1.1 数据库

    (1)修改Configuration目录中的db.properties中的数据库配置;
    (2)从http://www.occamslab.com/petricek/data/libimseticomplete.zip下载所需要的数据,解压后可以看到gender.dat 和ratings.dat文件;
    (3)启动工程,自动生成相关表;
    (4)在数据库中运行sql目录下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 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 ratings = baseDAO.find(hql,new Object[]{},page,rows);
    		List userRatings = new ArrayList();
    		
    		if(ratings.size()<=0){
    			return null;
    		}
    		
    		// 获取用户Gender
    	//	List 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 gender
    			hqlGender="from Gender g where UID="+rating.getItemId();
    			itemGender =genderDAO.find(hqlGender).get(0).getGender();
    			ur.setItemGender(itemGender);
    			userRatings.add(ur);
    		}
    		Map jsonMap = new HashMap();
    		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页面就可以简单的使用下面的代码即可:

    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;
    	@Resource
    	private BaseDAO 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);
    	}
    	
    	@Override
    	public void refresh(Collection alreadyRefreshed) {
    		delegate.refresh(alreadyRefreshed);
    	}
    
    	@Override
    	public List 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 recommend(long userID,int rows,int page,boolean filter) throws TasteException{
    		this.filter=filter;
    		String gHql = "from Gender g where g.uId=?";
    		List recommend = recommend(userID,20);
    		Map jsonMap = new HashMap();
    		List tmp = new ArrayList();
    		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 recommendRatings = new ArrayList();
    		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 recommend(long userID, int howMany,
    			boolean includeKnownItems) throws TasteException {
    		return delegate.recommend(userID, howMany, includeKnownItems);
    	}
    
    	@Override
    	public List recommend(long userID, int howMany,
    			IDRescorer rescorer) throws TasteException {
    		return delegate.recommend(userID, howMany, rescorer);
    	}
    
    	@Override
    	public List recommend(long userID, int howMany,
    			IDRescorer rescorer, boolean includeKnownItems)
    			throws TasteException {
    		return delegate.recommend(userID, howMany, rescorer, includeKnownItems);
    	}
    
    	@Override
    	public float estimatePreference(long userID, long itemID)
    			throws TasteException {
    		IDRescorer rescorer= new GenderRescorer(men,women,
    				usersRateMoreMen,usersRateLessMen,userID,model);
    		return (float)rescorer.rescore(userID, itemID);
    	}
    
    	@Override
    	public void setPreference(long userID, long itemID, float value)
    			throws TasteException {
    		delegate.setPreference(userID, itemID, value);
    	}
    
    	@Override
    	public void removePreference(long userID, long itemID)
    			throws TasteException {
    
    		delegate.removePreference(userID, itemID);
    	}
    
    	@Override
    	public 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 stub
    		return filterMen? men.contains(id):women.contains(id);
    	}
    需要改为:
    	public boolean isFiltered(long id) {
    		// TODO Auto-generated method stub
    		return 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 = '';
    											var btn_remove = '';
    											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 '  '
    								+ row.text + '';
    					}
    				});


    分享,成长,快乐

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





沪ICP备19023445号-2号
友情链接