從技術角度分析推薦系統案例
我們在使用各型別的軟體的時候,總是能在各大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); } }
通常我們會給使用者相似度設定一個閾值,當相似程度超過該閾值的時候,就會被引入到好友推薦列表中做成推薦人名單。
推薦系統這個典型案例的思路讓我們明白了向量的強大之處,這也是資料結構和演算法所具有的魅力,利用向量空間來計算出歐幾里得距離,從而解決掉如此複雜的問題。
上述的程式碼案例只能說是一個簡單的模型,真實生產中的實踐可要比這複雜得多,比如說針對於初期應用程式的基礎資料量不足的情況下,使用這類方式來做推薦功能可能會有點牽強,因此還是需要在落地實踐中不斷的嘗試和探索。
&n