基于向量模型的推荐系统案例(java版本)

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介: 基于向量模型的推荐系统案例(java版本)

我们在使用各类型的软件的时候,总是能在各大app中获取到推荐信息的数据,而且会发现推荐的信息数据还比较适合个人的口味,例如说某些共同兴趣爱好的好友推荐,某些好听的音乐推荐等等。


在进行推荐系统的核心算法介绍之前,我们需要先来回顾一下以前所学过的数学知识内容。


欧几里得距离


二维的欧几里得距离:


例如下图所示,在这样的一个简单的二维空间图里面,根据对于a点的坐标和b点的坐标进行二维空间距离的计算,假设p为点a到点b的欧式距离,那么可以根据勾股定理来计算出两点之间的向量距离为:


网络异常,图片无法展示
|

网络异常,图片无法展示
|


三维空间的欧几里得距离:


除了常见的二维空间之外,常用于的计算场景还有可能是基于三维空间运算的。


网络异常,图片无法展示
|


在这种场景下,假设计算A点和B点之间的距离为p,那么计算可以得出p的值为:


网络异常,图片无法展示
|


在了解了这些基本的知识点之后,我们再结合实际的应用场景来展开应用。

例如说一个电影影评网站,需要加入一个推荐喜欢观看同类电影的好友功能。


首先模拟出一个具体的数据场景:


网络异常,图片无法展示
|


1对该电影进行过评价,0没有对该电影进行过评价


有了这样的一个数据统计场景之后,我们可以根据对电影是否有共同评价进行共同兴趣爱好的匹配推荐。但是这种场景下也有一定的缺陷,那就是对于电影的评价有好有坏,需要将共同喜爱同一类电影的用户进行匹配推荐,将不喜欢同一类电影的用户进行匹配推荐就属于推荐失误的场景了。


改进点


在用户评论里面加入对于电影的打分功能,我们将打分等级也进行一个分类


网络异常,图片无法展示
|


那么我们将这里的打分等级和上述的电影评价相互结合之后便可得出下表:


网络异常,图片无法展示
|


根据上述的这张表,我们再回顾到本文开始时候所说的二维和三维空间里面的欧几里得距离计算。


假设A点的坐标为A(a1,a2…),B点坐标为B(b1,b2…)


二维空间距离计算:


网络异常,图片无法展示
|


三维空间距离计算:


网络异常,图片无法展示
|


类比一维、二维、三维的表示方法,n 维空间中的某个位置,我们可以写作(X1X1,X2X2,X3X3,…,XKXK)。这种表示方法我们称之为向量。


n维空间的距离计算:


网络异常,图片无法展示
|


那么集合上边的具体应用场景,我们便可以展开相应的计算了:


首先罗列出每个用户的空间坐标


小明(5,-1,-1,4,-1,-1,3,-1,1)(当前用户)

小王(4,-1,3,2,5,-1,-1,5,-1)

小东(-1,5,-1,-1,2,2,-1,-1,2)

小红(2,5,-1,3,3,-1,4,5,-1)

小乔(-1,-1,-1,-1,-1,-1,-1,5,-1)

小芳(-1,4,-1,3,3,5,5,-1,4)


然后再通过计算的时候,假设当前用户是小明,那么我们再进行用户匹配推荐的时候需要计算各个点和小明的欧几里得距离:


套用以下公式:


网络异常,图片无法展示
|


计算出小王和各个人之间的向量差值,值越小,即表示两者之间的相似度越高。

计算出来小王相对于小明的向量差为:


网络异常,图片无法展示
|


小东相对于小明的向量差为:


网络异常,图片无法展示
|


等等…


说了这么多,还是用实际的代码案例来进行讲解会好些

首先是 网站会员,电影信息,影评 三种基本模型


import lombok.AllArgsConstructor;
import lombok.Data;
/**
 * @author idea
 * @date 2019/5/4
 * @Version V1.0
 */
@Data
@AllArgsConstructor
public class MemberPO {
    private int id;
    private String memberName;
}
复制代码


import lombok.AllArgsConstructor;
import lombok.Data;
/**
 * @author idea
 * @date 2019/5/4
 * @Version V1.0
 */
@Data
@AllArgsConstructor
public class MoviePO {
    private int id;
    private String movieName;
}
复制代码


import lombok.AllArgsConstructor;
import lombok.Data;
/**
 * @author idea
 * @date 2019/5/4
 * @Version V1.0
 */
@Data
@AllArgsConstructor
public class MovieReviewPO {
    private int movieId;
    private int memberId;
    private int reviewScore;
}
复制代码


为了方便,这里的数据暂时用模拟的形式展示,忽略了从数据库读取的环节:


import com.sise.model.MoviePO;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
 * @author idea
 * @date 2019/5/4
 * @Version V1.0
 */
@Service
public class MovieService {
    public static List<MoviePO> MOVIE_LIST = new ArrayList<>();
    static {
        List<String> movieNames = Arrays.asList("绿皮书", "复仇者联盟", "月光男孩", "海边的曼彻斯特",
                "盗梦空间", "记忆碎片", "致命魔术", "流浪地球", "正义联盟");
        int id = 0;
        for (String movieName : movieNames) {
            MOVIE_LIST.add(new MoviePO(id++, movieName));
        }
    }
    /**
     * 根据名称获取用户信息
     *
     * @param name
     * @return
     */
    public MoviePO getMovieByName(String name) {
        return MOVIE_LIST.stream().filter(moviePO -> {
            return moviePO.getMovieName().equals(name);
        }).findFirst().get();
    }
}
复制代码


import com.sise.model.MemberPO;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
 * @author idea
 * @date 2019/5/4
 * @Version V1.0
 */
@Service
public class MemberService {
    public static List<MemberPO> MEMBER_LIST = new ArrayList<>();
    static {
        List<String> memberNameS = Arrays.asList("小明", "小王", "小东", "小红", "小乔", "小芳");
        int id = 0;
        for (String memberName : memberNameS) {
            MEMBER_LIST.add(new MemberPO(id++, memberName));
        }
    }
    /**
     * 根据名称获取用户信息
     *
     * @param name
     * @return
     */
    public MemberPO getMemberByName(String name) {
        return MEMBER_LIST.stream().filter(memberPO -> {
            return memberPO.getMemberName().equals(name);
        }).findFirst().get();
    }
}
复制代码


用户对电影打分的数据是存储在了Redis里面的,这里的为了方便,所以创建了一个mock使用的测试接口:


首先需要配置好SpringBoot和RedisTemplate,这部分的配置比较简单,这里暂时就先省略了。


电影评论service


import com.sise.model.MemberPO;
import com.sise.model.MoviePO;
import com.sise.model.MovieReviewPO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.*;
/**
 * @author idea
 * @date 2019/5/4
 * @Version V1.0
 */
@Service
@Slf4j
public class MovieReviewService {
    @Resource
    private RedisTemplate<String, MovieReviewPO> redisTemplate;
    public void mockData(MemberPO memberPO, MoviePO moviePO, Integer score) {
        Map<Object, Object> scoreMap = redisTemplate.opsForHash().entries(String.valueOf(memberPO.getId()));
        if (scoreMap == null) {
            scoreMap = new HashMap<>();
        }
        scoreMap.put(moviePO.getId(), score);
        redisTemplate.opsForHash().putAll(String.valueOf(memberPO.getId()), scoreMap);
        log.info("[MovieReviewService]保存信息成功!");
    }
    /**
     * 获取到list类型的统计数目
     *
     * @param memberId
     * @return
     */
    public List<Integer> getScoreList(int memberId) {
        Map<Object, Object> scoreMap = redisTemplate.opsForHash().entries(String.valueOf(memberId));
        List<Integer> result = new ArrayList();
        Map<Integer, Integer> sortMap = new TreeMap<Integer, Integer>(
                new Comparator<Integer>() {
                    @Override
                    public int compare(Integer obj1, Integer obj2) {
                        // 降序排序
                        return obj2.compareTo(obj1);
                    }
                });
        for (Object key : scoreMap.keySet()) {
            Integer movieIndex = (Integer) key;
            Integer score = (Integer) scoreMap.get(key);
            sortMap.put(movieIndex, score);
        }
        for (Object key : sortMap.keySet()) {
            result.add(sortMap.get(key));
        }
        return result;
    }
}
复制代码


然后是mock评论数据的接口


import com.sise.model.MemberPO;
import com.sise.model.MoviePO;
import com.sise.service.MemberService;
import com.sise.service.MovieReviewService;
import com.sise.service.MovieService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
 * @author idea
 * @date 2019/5/4
 * @Version V1.0
 */
@RestController
public class MockDataController {
    @Autowired
    private MovieReviewService movieReviewService;
    @Autowired
    private MemberService memberService;
    @Autowired
    private MovieService movieService;
    @GetMapping(value = "/mockData")
    public String mockData() {
        List<String> list =
                MovieService.MOVIE_LIST
                        .stream()
                        .map(moviePO -> moviePO.getMovieName())
                        .collect(Collectors.toList());
        //不同的用户打分程度匹配不一致
        List<Integer> score = Arrays.asList(-1, 4, -1, 3, 3, 5, 5, -1, 4);
        String name="小芳";
        int index = 0;
        for (String movieName : list) {
            this.mockData(name, movieName, score.get(index));
            index++;
        }
        return "success";
    }
    private void mockData(String memberName, String movieName, int score) {
        MemberPO memberPO = memberService.getMemberByName(memberName);
        MoviePO moviePO = movieService.getMovieByName(movieName);
        movieReviewService.mockData(memberPO, moviePO, score);
        System.out.println(memberPO.toString() + " " + moviePO.toString());
    }
}
复制代码


有了基本的测试数据之后,便可以来对核心的向量计算模块进行编写代码了:


import com.sise.model.MemberPO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.stream.Collectors;
/**
 * 推荐的核心部分
 *
 * @author linhao
 * @date 2019/5/4
 * @Version V1.0
 */
@Service
public class RecommendService {
    @Autowired
    private MovieReviewService movieReviewService;
    /**
     * 计算两个用户之间的爱好相似度
     *
     * @param currentMemberId
     * @param compareMemberId
     * @return double  degree 相似度
     */
    public double countSimilarityDegree(int currentMemberId, int compareMemberId) {
        List<Integer> currentIndexList = movieReviewService.getScoreList(currentMemberId);
        List<Integer> compareMemberList = movieReviewService.getScoreList(compareMemberId);
        //两个人的评分统计是相同个数的
        if (currentIndexList.size() == compareMemberList.size()) {
            int total = MovieService.MOVIE_LIST.size();
            int result = 0;
            //计算向量的和
            for (int i = 0; i < total; i++) {
                int x1 = currentIndexList.get(i);
                int x2 = compareMemberList.get(i);
                result = result + (int) Math.pow((x1 - x2), 2);
            }
            double degree = Math.sqrt(result);
            return degree;
        }
        return 0;
    }
     /**
     * 计算爱好相似的用户 从高往底
     *
     * @param currentMemberId
     * @return List 
     */
    public List countSimilarityList(int currentMemberId) {
        List<Integer> idList = MemberService.MEMBER_LIST
                .stream()
                .filter(memberPO -> memberPO.getId() != currentMemberId)
                .map(MemberPO::getId)
                .collect(Collectors.toList());
        Map<Integer, Double> hashMap = new HashMap<>();
        for (Integer memberId : idList) {
            double degree = countSimilarityDegree(currentMemberId, memberId);
            hashMap.put(memberId, degree);
        }
        //这里将map.entrySet()转换成list
        List<Map.Entry<Integer, Double>> list = new ArrayList<>(hashMap.entrySet());
        //然后通过比较器来实现排序
        Collections.sort(list,new Comparator<Map.Entry<Integer, Double>>() {
            //升序排序
            @Override
            public int compare(Map.Entry<Integer, Double> o1,
                               Map.Entry<Integer, Double> o2) {
                return o2.getValue().compareTo(o1.getValue());
            }
        });
        return list;
    }
}
复制代码


测试所用的接口


import com.sise.service.RecommendService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
 * @author idea
 * @date 2019/5/4
 * @Version V1.0
 */
@RestController
public class RecommendController {
    @Autowired
    private RecommendService recommendService;
    @GetMapping(value = "count")
    public List countDegree(int curId) {
        return recommendService.countSimilarityList(curId);
    }
}
复制代码


通常我们会给用户相似度设置一个阈值,当相似程度超过该阈值的时候,就会被引入到好友推荐列表中做成推荐人名单。


推荐系统这个典型案例的思路让我们明白了向量的强大之处,这也是数据结构和算法所具有的魅力,利用向量空间来计算出欧几里得距离,从而解决掉如此复杂的问题。

上述的代码案例只能说是一个简单的模型,真实生产中的实践可要比这复杂得多,比如说针对于初期应用程序的基础数据量不足的情况下,使用这类方式来做推荐功能可能会有点牵强,因此还是需要在落地实践中不断的尝试和探索。

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
1月前
使用ueditor实现多图片上传案例——实体类(Shopping.java)
使用ueditor实现多图片上传案例——实体类(Shopping.java)
18 0
|
29天前
Mybatis+mysql动态分页查询数据案例——分页工具类(Page.java)
Mybatis+mysql动态分页查询数据案例——分页工具类(Page.java)
21 1
|
21天前
|
SQL 设计模式 安全
Java单例模式几种写法以及代码案例拿来直接使用
Java单例模式几种写法以及代码案例拿来直接使用
31 0
|
29天前
Mybatis+mysql动态分页查询数据案例——工具类(MybatisUtil.java)
Mybatis+mysql动态分页查询数据案例——工具类(MybatisUtil.java)
15 1
|
1月前
|
Java 数据库连接 mybatis
mybatis简单案例源码详细【注释全面】——实体层(User.java)
mybatis简单案例源码详细【注释全面】——实体层(User.java)
13 0
|
5天前
|
Java 关系型数据库 MySQL
一套java+ spring boot与vue+ mysql技术开发的UWB高精度工厂人员定位全套系统源码有应用案例
UWB (ULTRA WIDE BAND, UWB) 技术是一种无线载波通讯技术,它不采用正弦载波,而是利用纳秒级的非正弦波窄脉冲传输数据,因此其所占的频谱范围很宽。一套UWB精确定位系统,最高定位精度可达10cm,具有高精度,高动态,高容量,低功耗的应用。
一套java+ spring boot与vue+ mysql技术开发的UWB高精度工厂人员定位全套系统源码有应用案例
|
6天前
|
Oracle Java 关系型数据库
Java 开发者必备:JDK 版本详解与选择策略(含安装与验证)
Oracle Java SE 支持路线图显示,JDK 8(LTS)支持至2030年,非LTS版本如9-11每6个月发布且支持有限。JDK 11(LTS)支持至2032年,而JDK 17及以上版本现在提供免费商用许可。LTS版本提供长达8年的支持,每2年发布一次。Oracle JDK与OpenJDK有多个社区和公司构建版本,如Adoptium、Amazon Corretto和Azul Zulu,它们在许可证、商业支持和更新方面有所不同。个人选择JDK时,可考虑稳定性、LTS、第三方兼容性和提供商支持。
10 0
|
25天前
|
算法 搜索推荐 Python
探索Python中的推荐系统:混合推荐模型
探索Python中的推荐系统:混合推荐模型
40 1
|
1月前
|
Java API 计算机视觉
java实现人脸识别V3版本开发
java实现人脸识别V3版本开发
17 0
|
1月前
|
Java 数据库连接 mybatis
mybatis简单案例源码详细【注释全面】——Utils层(MybatisUtils.java)
mybatis简单案例源码详细【注释全面】——Utils层(MybatisUtils.java)
12 0