咱們在使用各種型的軟件的時候,老是能在各大app中獲取到推薦信息的數據,並且會發現推薦的信息數據還比較適合我的的口味,例如說某些共同興趣愛好的好友推薦,某些好聽的音樂推薦等等。java
在進行推薦系統的核心算法介紹以前,咱們須要先來回顧一下之前所學過的數學知識內容。web
歐幾里得距離redis
二維的歐幾里得距離:算法
例以下圖所示,在這樣的一個簡單的二維空間圖裏面,根據對於a點的座標和b點的座標進行二維空間距離的計算,假設p爲點a到點b的歐式距離,那麼能夠根據勾股定理來計算出兩點之間的向量距離爲:spring
三維空間的歐幾里得距離:數據庫
除了常見的二維空間以外,經常使用於的計算場景還有多是基於三維空間運算的。數據結構
在這種場景下,假設計算A點和B點之間的距離爲p,那麼計算能夠得出p的值爲:app
在瞭解了這些基本的知識點以後,咱們再結合實際的應用場景來展開應用。數據結構和算法
例如說一個電影影評網站,須要加入一個推薦喜歡觀看同類電影的好友功能。ide
首先模擬出一個具體的數據場景:
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); } }
一般咱們會給用戶類似度設置一個閾值,當類似程度超過該閾值的時候,就會被引入到好友推薦列表中作成推薦人名單。
推薦系統這個典型案例的思路讓咱們明白了向量的強大之處,這也是數據結構和算法所具備的魅力,利用向量空間來計算出歐幾里得距離,從而解決掉如此複雜的問題。
上述的代碼案例只能說是一個簡單的模型,真實生產中的實踐可要比這複雜得多,好比說針對於初期應用程序的基礎數據量不足的狀況下,使用這類方式來作推薦功能可能會有點牽強,所以仍是須要在落地實踐中不斷的嘗試和探索。