文章目录
前言
在视频网站中,一个很重要的功能就是个性化视频推荐功能,在此,我们使用基于物品的协同过滤算法和ElasticSearch的相似搜索功能来实现一个简单的个性化推荐功能。
一、基于物品的协同过滤推荐算法
基于物品的协同过滤算法原理如下:
① 分析各个用户对物品的浏览记录;
② 依据浏览记录分析得出所有物品之间的相似度;
③ 对于目标用户评价高的物品,找出与之相似度最高的K个物品;
④ 将这K个物品中目标用户没有浏览过的物品推荐给目标用户
二、实现步骤
1.编写控制类
代码如下:
@GetMapping("/getCommendVideo/{userId}/{videoId}")
@ApiOperation("获取推荐视频")
public Result<List<CommendVideoResponse>> getRecommendVideo(@PathVariable Integer userId, @PathVariable String videoId) {
return playService.getRecommendVideo(userId, videoId);
}
2.编写服务类
代码如下:
public Result<List<CommendVideoResponse>> getRecommendVideo(Integer userId, String videoId) {
List<RecommendVideo> list;
List<CommendVideoResponse> list1=new ArrayList<>();
// 如果是用户已登录,个性化推荐
if (userId > 0) {
// 获取个性化推荐视频
list = recommendService.recommendVideo(userId);
if (list == null || list.isEmpty()) {
// 用户本身数据不足,无法获取足够的个性化视频。
// 使用ES的相似搜索获取和当前观看视频类似的视频进行推荐
list=searchClient.getRecommendVideo(videoId);
}
} else {
// 用户未登录,直接使用ES的相似搜索获取和当前观看视频类似的视频进行推荐
list=searchClient.getRecommendVideo(videoId);
}
for(RecommendVideo recommendVideo : list){
list1.add(new CommendVideoResponse(recommendVideo));
}
return Result.data(list1);
}
3.编写推荐业务实现类
业务实现类内部元素和基础方法
@Service
public class RecommendService {
Map<Integer,Integer> itemToIdMap = new HashMap<>();//视频id对应顺序id
Map<Integer,Integer> idToItemMap = new HashMap<>();//顺序id对应视频id
Map<Integer, Map<Integer, Double>> itemMap = new HashMap<>(); //针对每个视频,存储所有用户对该视频的操作,播放1,点赞2,收藏4
Map<Integer, Double> userMap = new HashMap<>(); //针对当前要推荐的用户,记录该用户对于视频的操作
double[][] simMatrix; //产品之间的相似矩阵
int TOP_K = 25; //选择的相似item的数量
//求两个集合交集的数量
public int interCount(Set<Integer> set_a,Set<Integer> set_b) {
int samObj = 0;
for(Object obj : set_a) {
if(set_b.contains(obj))
samObj++;
}
return samObj;
}
//求两个集合交集
public Set<Integer> interSet(Set<Integer> set_a,Set<Integer> set_b) {
Set<Integer> tempSet = new HashSet<>();
for(Integer obj:set_a) {
if(set_b.contains(obj))
tempSet.add(obj);
}
return tempSet;
}
//对map进行从大到小排序
public <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;
}
}
3.1 分析物品浏览记录
要把用户对各个视频的操作进行统计,给用户对于视频的喜好程度评分,如播放视频为1分,点赞为2分,收藏为4分,整合出浏览记录。
首先,写一个sql语句,查出每个用户对视频的操作记录,包括播放,点赞,收藏
@Mapper
public interface GetUserToVideoRecordMapper {
@Select("select p.user_id as userId, p.video_id as videoId, l.id as likedId, c.id as collectId from play p " +
"left join likes l on p.video_id = l.video_id and p.user_id = l.user_id left join collect_group cg " +
"on p.user_id = cg.user_id left join collect c on cg.id = c.collect_group_id and p.video_id = c.video_id")
List<UserToVideoRecord> getUserToVideoRecord();
}
然后将查询出来的操作记录填充到元素itemMap中
/**
* 填充每个产品不同用户的评分,当前用户对于产品的评分
* @param user
*/
public void fillMap(int user) {
// 查询出所有视频的操作记录
List<UserToVideoRecord> list = getUserToVideoRecordMapper.getUserToVideoRecord();
int itemId = 0; //产品计数
// 遍历处理
for (UserToVideoRecord record : list) {
// 判断是否已经记录当前视频
if (!itemToIdMap.containsKey(record.getVideoId())) {
// 保存用户对当前视频的喜好评分
Map<Integer, Double> currentUserMap = new HashMap<>();
int userId = record.getUserId();
// 记录中都为播放过的视频,所以默认为1分
currentUserMap.put(userId, 1.0);
if (record.getLikedId() != null) {
// 点赞加2分
currentUserMap.put(userId, currentUserMap.get(userId) + 2.0);
}
if (record.getCollectId() != null) {
// 收藏加4分
currentUserMap.put(userId, currentUserMap.get(userId) + 4.0);
}
// 保存到itemMap中
itemMap.put(record.getVideoId(), currentUserMap);
// 保存视频出现的顺序和视频id的对应关系,方便后续构建相似矩阵
idToItemMap.put(itemId, record.getVideoId());
itemToIdMap.put(record.getVideoId(), itemId);
itemId++;
} else {
Map<Integer, Double> currentUserMap = itemMap.get(record.getVideoId());
int userId = record.getUserId();
currentUserMap.put(userId, 1.0);
if (record.getLikedId() != null) {
currentUserMap.put(userId, currentUserMap.get(userId) + 2.0);
}
if (record.getCollectId() != null) {
currentUserMap.put(userId, currentUserMap.get(userId) + 4.0);
}
itemMap.put(record.getVideoId(), currentUserMap);
}
// 保存当前用户对于视频的操作
if (record.getUserId() == user) {
userMap.put(record.getVideoId(), 1.0);
if (record.getLikedId() != null) {
userMap.put(record.getVideoId(), userMap.get(record.getVideoId()) + 2.0);
}
if (record.getCollectId() != null) {
userMap.put(record.getVideoId(), userMap.get(record.getVideoId()) + 4.0);
}
}
}
}
3.2 构建相似矩阵
根据不同用户的视频浏览记录,可以构建出视频的相似矩阵simMatrax
/**
* 计算相似矩阵
*/
public void itemSimilarity() {
simMatrix = new double[itemMap.size()][itemMap.size()];
// 循环计算每个视频之间的相似度
for (Map.Entry<Integer, Map<Integer, Double>> itemEntry1 : itemMap.entrySet()) {
// 获取操作过当前视频用户集合
Set<Integer> ratedUserSet1 = itemEntry1.getValue().keySet();
int ratedUserSize1 = ratedUserSet1.size();
// 循环计算当前视频和其他视频的相似度
for (Map.Entry<Integer, Map<Integer, Double>> itemEntry2 : itemMap.entrySet()) {
// 大于表示两个视频还未计算相似度
if (itemToIdMap.get(itemEntry2.getKey()) > itemToIdMap.get(itemEntry1.getKey())) {
// 同样获取操作该视频的用户集合
Set<Integer> ratedUserSet2 = itemEntry2.getValue().keySet();
int ratedUserSize2 = ratedUserSet2.size();
// 获取两个视频之间相同的用户个数
int sameUserSize = interCount(ratedUserSet1, ratedUserSet2);
// 计算相似度
double similarity = sameUserSize / (Math.sqrt(ratedUserSize1 * ratedUserSize2));
// 构建矩阵
simMatrix[itemToIdMap.get(itemEntry1.getKey())][itemToIdMap.get(itemEntry2.getKey())] = similarity;
simMatrix[itemToIdMap.get(itemEntry2.getKey())][itemToIdMap.get(itemEntry1.getKey())] = similarity;
}
}
}
}
3.3 获取相似度最高的视频
构建出相似矩阵之后,就可以根据相似度和操作评分进行排序,获取到相似的视频
public Set<Integer> recommend() {
//根据视频相似度获取每个视频最相似的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()); //获取相似视频id存入集合中
simItemCount++;
}else
break;
}
//相似视频结果存入map中
nearestItemMap.put(i,nearestItemSet);
}
Set<Integer> currentUserSet = new HashSet<>();
for (int item : userMap.keySet()) {
currentUserSet.add(itemToIdMap.get(item));
}
Map<Integer,Double> preRatingMap = new HashMap<>();
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 id : interSet) {
sumSim += simMatrix[j][id];
preRating += simMatrix[j][id] * userMap.get(idToItemMap.get(id));
}
if(sumSim != 0) preRating = preRating/sumSim; //如果相似性之和不为0计算得分,否则得分为0
else preRating = 0;
}
// 记录视频id
preRatingMap.put(idToItemMap.get(j), preRating);
}
// 按照评分进行排序
preRatingMap = sortMapByValues(preRatingMap);
// 返回要推荐的视频id
return preRatingMap.keySet();
}
3.4 根据视频id获取视频信息推荐给用户
public List<RecommendVideo> recommendVideo(int userId) {
fillMap(userId);
itemSimilarity();
Set<Integer> videoIds = recommend();
List<RecommendVideo> list;
if (videoIds == null || videoIds.isEmpty()) return null;
MPJLambdaWrapper<Video> wrapper = new MPJLambdaWrapper<>();
wrapper.in(Video::getId, videoIds);
wrapper.leftJoin(User.class, User::getId, Video::getUserId);
wrapper.leftJoin(VideoData.class, VideoData::getVideoId, Video::getId);
wrapper.select(Video::getCover, Video::getIntro, Video::getCreateTime, Video::getLength, Video::getUrl);
wrapper.select(VideoData::getDanmakuCount, VideoData::getPlayCount, VideoData::getCollectCount);
wrapper.selectAs(Video::getName, RecommendVideo::getVideoName);
wrapper.selectAs(User::getNickname, RecommendVideo::getAuthorName);
wrapper.selectAs(Video::getId, RecommendVideo::getVideoId);
wrapper.selectAs(User::getId, RecommendVideo::getAuthorId);
list = videoMapper.selectJoinList(RecommendVideo.class, wrapper);
return list;
}
总结
以上就是根据基于物品的协同过滤算法的推荐功能实现,增加ES的相似搜索可以弥补当用户数据不足时出现的问题。