简介
随着电商网站中用户数量的迅速增长,基于用户的协同过滤User-based CF存在计算用户之间相似度时复杂度太高,不利于及时为用户产生个性化推荐。相比用户数量,电商网站上的产品数量则相对较少,基于项目的协同过滤(Item-Based CF)被亚马逊提出,并应用于亚马逊网站上。Item-Based CF的原理是计算产品之间的相似度,并根据用户已经购买过的产品为该用户提供相似的产品。由于电商网站上,产品的种类与数量相对固定,产品之间的相似性可以通过线下直接计算后再直接为用户推荐,大大节约了线上推荐的计算复杂度。
本文的是基于java实现Item-Based CF,所用数据与之前User-based CF结构相同。
Java代码实现
Item-Based CF的java实现主要分为三部分(1)用户产品交互历史读取;(2)产品相似度计算;(3)为用户推荐产品。
数据输入格式:
第一列表示用户ID,第二列为电影ID,第三列为用户对电影的评分,第四列对应的是电影在IMBD网站的ID(这个ID这里用不到)。
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
public class Item_CF {
static Map<String,Integer> itemIDMap = new HashMap<String,Integer>();//产品ID列表 产品-id
static Map<Integer,String> idToItemMap = new HashMap<Integer,String>();//产品ID转产品原名称 id-产品
static Map<String, HashMap<String, Double>> itemMap = new HashMap<>(); //针对每个产品,存储所有用户对该产品的评分
static Map<String,Integer> userIDMap = new HashMap<String,Integer>();//用户ID列表
static Map<Integer,String> idToUserMap = new HashMap<Integer,String>();//用户ID转用户原名称
static Map<String,HashMap<String,Double>> userMap = new HashMap<String,HashMap<String, Double>>(); //针对每个用户,记录用户对于产品的评分
static double[][] simMatrix; //产品之间的相似矩阵
static int TOP_K = 25; //选择的相似item的数量
static int TOP_N = 20; //定义最长推荐列表
public static void main(String[] args) throws IOException {
readUI();
item_similarity();
recommend();
}
//读取用户UI交互
public static void readUI() throws IOException{
String uiFile = "data\\training_topicAttack_101.txt";
BufferedReader bfr_ui = new BufferedReader(new InputStreamReader(new FileInputStream(new File(uiFile)),"UTF-8"));
String line;
String[] SplitLine;
int itemId = 0; //产品计数
int userId = 0;//用户计数
while((line = bfr_ui.readLine()) != null){
SplitLine = line.split("\t");
//如果不包含当前产品,存入产品map以及产品idmap中
if(!itemIDMap.containsKey(SplitLine[1])) {
HashMap<String, Double> currentUserMap = new HashMap<>();//存入当前的用户评分
currentUserMap.put(SplitLine[0], Double.parseDouble(SplitLine[2])); //用户-评分
itemMap.put(SplitLine[1], currentUserMap); //在itemMap中存入产品-评分
itemIDMap.put(SplitLine[1], itemId);
idToItemMap.put(itemId, SplitLine[1]);
itemId ++;
}else { //如果已经存在,进行Map更新
HashMap<String, Double> currentUserMap = itemMap.get(SplitLine[1]); //获取已有产品所包含的评分
currentUserMap.put(SplitLine[0], Double.parseDouble(SplitLine[2]));//加入新的用户-评分
itemMap.put(SplitLine[1], currentUserMap);
}
//如果不包含当前的用户,存入map中
if(!userMap.containsKey(SplitLine[0])) {
userIDMap.put(SplitLine[0], userId);
idToUserMap.put(userId, SplitLine[0]);
userId++;
//新建Map用于存储当前用户的评分列表
HashMap<String, Double> curentUserMap = new HashMap<String,Double>();
//将当前用户评分加入当前评分列表中
curentUserMap.put(SplitLine[1], Double.parseDouble(SplitLine[2]));
userMap.put(SplitLine[0], curentUserMap);
}else { //如果已存在当前用户,将该用户先前的评分拿出来,再加入新的评分
HashMap<String, Double> curentUserMap = userMap.get(SplitLine[0]);
curentUserMap.put(SplitLine[1], Double.parseDouble(SplitLine[2]));
userMap.put(SplitLine[0], curentUserMap);
}
}
}
//获取产品之间的相似性
public static void item_similarity() {
//初始化用户相似矩阵
simMatrix = new double[itemMap.size()][itemMap.size()];
int itemCount = 0;
//循环每个产品计算相似性:Jaccard 相似性
for(Map.Entry<String, HashMap<String, Double>> itemEntry_1 : itemMap.entrySet()) {
System.out.println("计算"+itemCount);
//获取为当前产品评分的所有用户
Set<String> ratedUserSet_1 = new HashSet<>();
for(Map.Entry<String, Double> userEntry : itemEntry_1.getValue().entrySet()) {
//将已评分用户存入set集合中
ratedUserSet_1.add(userEntry.getKey());
}
int ratedUserSize_1 = ratedUserSet_1.size();//第一个产品所有评论数
//循环其他产品
for(Map.Entry<String, HashMap<String, Double>> itemEntry_2 : itemMap.entrySet()) {
//首先判断第二个产品的id是否大于第一个,是的话再进行计算,避免重复计算
if(itemIDMap.get(itemEntry_2.getKey())>itemIDMap.get(itemEntry_1.getKey())) {
//同样获取为当前产品评分的所有用户
Set<String> ratedUserSet_2 = new HashSet<>();
for(Map.Entry<String, Double> userEntry : itemEntry_2.getValue().entrySet()) {
ratedUserSet_2.add(userEntry.getKey());
}
//通过jaccard相似度计算产品相似度
int ratedUserSize_2 = ratedUserSet_2.size();//第二个产品所有评论数
int sameUerSize = interCount(ratedUserSet_1,ratedUserSet_2); //取两个集合的交集的数量
double similarity = sameUerSize/(Math.sqrt(ratedUserSize_1*ratedUserSize_2));
//把相似性存入相似矩阵中
simMatrix[itemIDMap.get(itemEntry_1.getKey())][itemIDMap.get(itemEntry_2.getKey())] = similarity;
simMatrix[itemIDMap.get(itemEntry_2.getKey())][itemIDMap.get(itemEntry_1.getKey())] = similarity;
}
}
itemCount++;
// for (int i = 0; i < simMatrix.length; i++) {
// for (int j = 0; j < simMatrix.length; j++) {
// System.out.print(simMatrix[i][j]+" ");
// }
// System.out.println();
// }
}
}
//根据产品的相似性进行推荐
public static void recommend() throws IOException{
String resultFile = "data//topicAttack_101_ItemCF_result.txt";
BufferedWriter bfw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(new File(resultFile)),"UTF-8"));
//根据item相似度获取每个item最相似的TOP_K个产品
Map<Integer, HashSet<Integer>> nearestItemMap = new HashMap<>();
for(int i = 0;i<itemMap.size();i++) {
Map<Integer, Double> simMap = new HashMap<>();
for(int j = 0;j<itemMap.size();j++) {
simMap.put(j,simMatrix[i][j]);
}
//对产品相似性进行排序
simMap = sortMapByValues(simMap);
int simItemCount = 0;
HashSet<Integer> nearestItemSet = new HashSet<>();
for(Map.Entry<Integer, Double> entry : simMap.entrySet()) {
if(simItemCount<TOP_K) {
nearestItemSet.add(entry.getKey()); //获取相似itemID存入集合中
simItemCount++;
}else
break;
}
//相似物品结果存入map中
nearestItemMap.put(i,nearestItemSet);
}
//循环每个用户,循环每个产品,计算用户对没有买过的产品的打分,取TOP_N得分最高的产品进行推荐
for(int i = 0;i<userMap.size();i++) {
System.out.println("为用户"+i+"推荐");
//获取当前用户所有评论过的产品
HashSet<Integer> currentUserSet = new HashSet<>();
Map<String,Double> preRatingMap = new HashMap<String,Double>();
for(Map.Entry<String, Double> entry :userMap.get(idToUserMap.get(i)).entrySet()) {
currentUserSet.add(itemIDMap.get(entry.getKey())); //将该用户评论过的产品以产品id的形式存入集合中
}
//循环每个产品
for(int j = 0;j<itemMap.size();j++) {
double preRating = 0;
double sumSim = 0;
//首先判断用户购买的列表中是否包含当前商品,如果包含直接跳过
if(currentUserSet.contains(j))
continue;
//判断当前产品的近邻中是否包含这个产品
Set<Integer> interSet = interSet(currentUserSet, nearestItemMap.get(j));//获取当前用户的购买列表与产品相似品的交集
//如果交集为空,则该产品预测评分为0
if(!interSet.isEmpty()) {
for(int item :interSet) {
sumSim += simMatrix[j][item];
preRating += simMatrix[j][item]* userMap.get(idToUserMap.get(i)).get(idToItemMap.get(item));
}
if(sumSim != 0) {
preRating = preRating/sumSim; //如果相似性之和不为0计算得分,否则得分为0
}else
preRating = 0;
}else //如果交集为空的话,直接评分为0
preRating = 0;
preRatingMap.put(idToItemMap.get(j), preRating);
}
preRatingMap = sortMapByValues(preRatingMap);
if(!preRatingMap.isEmpty()) {
bfw.append(idToUserMap.get(i)+":");
}
//推荐TOP_N个产品
int recCount = 0;
for(Map.Entry<String, Double> entry : preRatingMap.entrySet()) {
if(recCount < TOP_N) {
bfw.append(entry.getKey() + " ");
recCount ++;
bfw.flush();
}
}
bfw.newLine();
bfw.flush();
}
bfw.flush();
bfw.close();
}
//求两个集合交集
public static int interCount(Set<String> set_a,Set<String> set_b) {
int samObj = 0;
for(Object obj:set_a) {
if(set_b.contains(obj))
samObj++;
}
return samObj;
}
//求两个集合交集的数量
public static Set<Integer> interSet(Set<Integer> set_a,Set<Integer> set_b) {
Set<Integer> tempSet = new HashSet<>();
for(Object obj:set_a) {
if(set_b.contains(obj))
tempSet.add((Integer) obj);
}
return tempSet;
}
//对map进行从大到小排序
public static <K extends Comparable, V extends Comparable> Map<K, V> sortMapByValues(Map<K, V> aMap) {
HashMap<K, V> finalOut = new LinkedHashMap<>();
aMap.entrySet().stream().sorted((p1, p2) -> p2.getValue().compareTo(p1.getValue())).collect(Collectors.toList())
.forEach(ele -> finalOut.put(ele.getKey(), ele.getValue()));
return finalOut;
}
}
数据输出格式:
其中,第一列表示用户ID,后面是为该用户推荐的TOP_N个电影。