我们在使用各类型的软件的时候,总是能在各大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); } }
通常我们会给用户相似度设置一个阈值,当相似程度超过该阈值的时候,就会被引入到好友推荐列表中做成推荐人名单。
推荐系统这个典型案例的思路让我们明白了向量的强大之处,这也是数据结构和算法所具有的魅力,利用向量空间来计算出欧几里得距离,从而解决掉如此复杂的问题。
上述的代码案例只能说是一个简单的模型,真实生产中的实践可要比这复杂得多,比如说针对于初期应用程序的基础数据量不足的情况下,使用这类方式来做推荐功能可能会有点牵强,因此还是需要在落地实践中不断的尝试和探索。