天天看點

從技術角度分析推薦系統案例

我們在使用各類型的軟體的時候,總是能在各大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);
 }
}