自制视频网站实现个性化推荐视频功能

文章目录


前言

在视频网站中,一个很重要的功能就是个性化视频推荐功能,在此,我们使用基于物品的协同过滤算法和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的相似搜索可以弥补当用户数据不足时出现的问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值