一、簡介 本文博主給大家講解如何在自己開源的電商項目newbee-mall-pro中應用協同過濾演算法來達到給用戶更好的購物體驗效果。 newbee-mall-pro項目地址: 源碼地址:https://github.com/wayn111/newbee-mall-pro 線上地址:http://12 ...
一、簡介
本文博主給大家講解如何在自己開源的電商項目newbee-mall-pro中應用協同過濾演算法來達到給用戶更好的購物體驗效果。
newbee-mall-pro項目地址:
二、協同過濾演算法
協同過濾演算法是一種基於用戶或者物品的相似度來推薦商品的方法,它可以有效地解決商城系統中的信息過載問題。協同過濾演算法的實踐主要包括以下幾個步驟:
- 數據收集和預處理。這一步需要從商城系統中獲取用戶的行為數據,如瀏覽、購買、評價等,然後進行一些必要的清洗和轉換,以便後續的分析和計算。
- 相似度計算。這一步需要根據用戶或者物品的特征或者行為,採用合適的相似度度量方法,如餘弦相似度、皮爾遜相關係數、Jaccard指數等,來計算用戶之間或者物品之間的相似度矩陣。
- 推薦生成。這一步需要根據相似度矩陣和用戶的歷史行為,採用合適的推薦策略,如基於鄰域的方法、基於模型的方法、基於矩陣分解的方法等,來生成針對每個用戶的個性化推薦列表。
- 推薦評估和優化。這一步需要根據一些評價指標,如準確率、召回率、覆蓋率、多樣性等,來評估推薦系統的效果,並根據反饋信息和業務需求,進行一些參數調整和演算法優化,以提高推薦系統的性能和用戶滿意度。
在原有的商城首頁為你推薦欄目是使用後臺配置的商品列表,基於人為配置。在項目商品用戶持續增長的情況下,不一定能給用戶推薦用戶可能想要的商品。
因此在v2.4.1版本中,商城首頁為你推薦欄目添加了協同過濾演算法。按照UserCF基於用戶的協同過濾、ItemCF基於物品的協同過濾。 實現了兩種不同的推薦邏輯。
-
UserCF:基於用戶的協同過濾。當一個用戶A需要個性化推薦的時候,我們可以先找到和他有相似興趣的其他用戶,然後把那些用戶喜歡的,而用戶A沒有聽說過的物品推薦給A。
假設用戶 A 喜歡物品 A、物品 C,用戶 B 喜歡物品 B,用戶 C 喜歡物品 A 、物品 C 和物品 D;從這些用戶的歷史喜好信息中,我們可以發現用戶 A 和用戶 C 的口味和偏好是比較類似的,同時用戶 C 還喜歡物品 D,那麼我們可以推斷用戶 A 可能也喜歡物品 D,因此可以將物品 D 推薦給用戶 A。具體代碼在ltd.newbee.mall.recommend.core.UserCF
中。 -
itemCF:基於物品的協同過濾。預先根據所有用戶的歷史偏好數據計算物品之間的相似度,然後把與用戶喜歡的物品相類似的物品推薦給用戶。
假如用戶A喜歡物品A和物品C,用戶B喜歡物品A、物品B和物品C,用戶C喜歡物品A,從這些用戶的歷史喜好中可以認為物品A與物品C比較類似,喜歡物品A的都喜歡物品C,基於這個判斷用戶C可能也喜歡物品C,所以推薦系統將物品C推薦給用戶C。 具體代碼在ltd.newbee.mall.recommend.core.ItemCF
中。
三、推薦演算法代碼實踐
3.1 數據收集和預處理
在newbee-mall-pro中,我們基於用戶下單的商品數據進行收集和預處理。
/**
* 根據所有用戶購買商品的記錄進行數據手機
*
* @return List<RelateDTO>
*/
@Override
public List<RelateDTO> getRelateData() {
List<RelateDTO> relateDTOList = new ArrayList<>();
// 獲取所有訂單以及訂單關聯商品的集合
List<Order> newBeeMallOrders = orderDao.selectOrderIds();
List<Long> orderIds = newBeeMallOrders.stream().map(Order::getOrderId).toList();
List<OrderItemVO> newBeeMallOrderItems = orderItemDao.selectByOrderIds(orderIds);
Map<Long, List<OrderItemVO>> listMap = newBeeMallOrderItems.stream()
.collect(Collectors.groupingBy(OrderItemVO::getOrderId));
Map<Long, List<OrderItemVO>> goodsListMap = newBeeMallOrderItems.stream()
.collect(Collectors.groupingBy(OrderItemVO::getGoodsId));
// 遍歷訂單,生成預處理數據
for (Order newBeeMallOrder : newBeeMallOrders) {
Long orderId = newBeeMallOrder.getOrderId();
for (OrderItemVO newBeeMallOrderItem : listMap.getOrDefault(orderId, Collections.emptyList())) {
Long goodsId = newBeeMallOrderItem.getGoodsId();
Long categoryId = newBeeMallOrderItem.getCategoryId();
RelateDTO relateDTO = new RelateDTO();
...
relateDTOList.add(relateDTO);
}
}
return relateDTOList;
}
3.2 相似度計算
在推薦演算法中,相似度建立是一個非常重要的過程,它標志著演算法準不准確,能不能給用戶帶來好的推薦體驗。在newbee-mall-pro中,我們將用戶之間下單的商品進行相似度計算,因為如果兩個用戶購買了同一個商品,那麼我們認為這兩個用戶之間是存在聯繫並且都存在付費行為。
// 遍歷訂單商品
for (OrderItemVO newBeeMallOrderItem : listMap.getOrDefault(orderId, Collections.emptyList())) {
Long goodsId = newBeeMallOrderItem.getGoodsId();
Long categoryId = newBeeMallOrderItem.getCategoryId();
RelateDTO relateDTO = new RelateDTO();
relateDTO.setUserId(newBeeMallOrder.getUserId());
relateDTO.setProductId(goodsId);
relateDTO.setCategoryId(categoryId);
// 通過計算商品購買次數,來建立相似度
List<OrderItemVO> list = goodsListMap.getOrDefault(goodsId, Collections.emptyList());
int sum = list.stream().mapToInt(OrderItemVO::getGoodsCount).sum();
relateDTO.setIndex(sum);
relateDTOList.add(relateDTO);
}
通過餘弦相似度演算法計算用戶與商品之間的相似度,從而為用戶推薦最相似的商品。當兩個用戶購買了同一個商品時,我們就認為兩個用戶產生了關聯,因此針對兩個用戶購買的同一個商品進行相似度計算,來建立用戶之間的相似度。
餘弦相似度是一種用於衡量兩個向量之間的相似度的方法,它通過計算兩個向量的夾角的餘弦值來得到。在商城系統中,餘弦相似度可以用於實現基於內容的推薦演算法,即根據用戶的歷史購買或瀏覽行為,為用戶推薦與其興趣相似的商品。具體來說,可以將每個商品表示為一個特征向量,例如商品的類別、價格、評分等,然後將每個用戶表示為一個偏好向量,例如用戶購買或瀏覽過的商品的特征向量的加權平均。這樣,就可以利用餘弦相似度來計算用戶和商品之間的相似度,從而為用戶推薦最相似的商品。
計算相關係數,傳入用戶ID或者物品ID,計算相似度
/**
* 計算相關係數併排序
*
* @param key 基於用戶協同代表用戶id,基於物品協同代表武平id
* @param map 預處理數據集
* @param type 類型0基於用戶推薦使用餘弦相似度 1基於物品推薦使用餘弦相似度
* @return Map<Double, Long>
*/
public static Map<Double, Long> computeNeighbor(Long key,
Map<Long, List<RelateDTO>> map, int type) {
Map<Double, Long> distMap = new TreeMap<>();
List<RelateDTO> items = map.get(key);
map.forEach((k, v) -> {
// 排除此用戶
if (!k.equals(key)) {
// 計算關係繫數
double coefficient = relateDist(v, items, type);
distMap.put(coefficient, k);
}
});
return distMap;
}
計算兩個用戶間的相關係數
/**
* 計算兩個序列間的相關係數
*
* @param xList
* @param yList
* @param type 類型0基於用戶推薦使用餘弦相似度 1基於物品推薦使用餘弦相似度 2基於用戶推薦使用皮爾森繫數計算
* @return
*/
private static double relateDist(List<RelateDTO> xList,
List<RelateDTO> yList, Integer type) {
List<Integer> xs = Lists.newArrayList();
List<Integer> ys = Lists.newArrayList();
xList.forEach(x -> yList.forEach(y -> {
if (type == 0) {
// 基於用戶推薦時如果兩個用戶購買的商品相同,則計算相似度
if (x.getProductId().longValue() == y.getProductId().longValue()) {
xs.add(x.getIndex());
ys.add(y.getIndex());
}
} else if (type == 1) {
// 基於物品推薦時如果兩個用戶id相同,則計算相似度
if (x.getUserId().longValue() == y.getUserId().longValue()) {
xs.add(x.getIndex());
ys.add(y.getIndex());
}
}
}));
if (ys.size() == 0 || xs.size() == 0) {
return 0d;
}
// 餘弦相似度計算
return cosineSimilarity(xs, ys);
}
餘弦相似度計算
/**
* 來計算向量之間的餘弦相似度,
* 也就是計算兩個用戶或者兩個物品之間的相似度
* @param xs
* @param xs
* @return double
*/
private static double cosineSimilarity(List<Integer> xs,
List<Integer> ys) {
double dotProduct = 0;
double norm1 = 0;
double norm2 = 0;
for (int i = 0; i < xs.size(); i++) {
Integer x = xs.get(i);
Integer y = ys.get(i);
dotProduct += x * y;
norm1 += Math.pow(x, 2);
norm2 += Math.pow(y, 2);
}
return dotProduct / (Math.sqrt(norm1) * Math.sqrt(norm2));
}
3.3 推薦生成
基於用戶協同的推薦生成,我們可以先找到和目標用戶有相似興趣的其他用戶,然後把其他用戶喜歡的,而目標用戶沒有買過的物品推薦給目標用戶。
public class UserCF {
/**
* 物用戶協同推薦
*
* @param userId 用戶ID
* @param num 返回數量
* @param list 預處理數據
* @return 商品id集合
*/
public static List<Long> recommend(Long userId, Integer num,
List<RelateDTO> list, Integer type) {
// 對每個用戶的購買商品記錄進行分組
Map<Long, List<RelateDTO>> userMap = list.stream()
.collect(Collectors.groupingBy(RelateDTO::getUserId));
// 獲取其他用戶與當前用戶的關係值
Map<Double, Long> userDisMap = CoreMath.computeNeighbor(userId, userMap, type);
List<Long> similarUserIdList = new ArrayList<>();
List<Double> values = new ArrayList<>(userDisMap.keySet());
values.sort(Collections.reverseOrder());
List<Double> scoresList = values.stream().limit(3).toList();
// 獲取關係最近的用戶
for (Double aDouble : scoresList) {
similarUserIdList.add(userDisMap.get(aDouble));
}
List<Long> similarProductIdList = new ArrayList<>();
for (Long similarUserId : similarUserIdList) {
// 獲取相似用戶購買商品的記錄
List<Long> collect = userMap.get(similarUserId).stream()
.map(RelateDTO::getProductId).toList();
// 過濾掉重覆的商品
List<Long> collect1 = collect.stream()
.filter(e -> !similarProductIdList.contains(e)).toList();
similarProductIdList.addAll(collect1);
}
// 當前登錄用戶購買過的商品
List<Long> userProductIdList = userMap.getOrDefault(userId,
Collections.emptyList()).stream().map(RelateDTO::getProductId).toList();
// 相似用戶買過,但是當前用戶沒買過的商品作為推薦
List<Long> recommendList = new ArrayList<>();
for (Long similarProduct : similarProductIdList) {
if (!userProductIdList.contains(similarProduct)) {
recommendList.add(similarProduct);
}
}
Collections.sort(recommendList);
return recommendList.stream().distinct().limit(num).toList();
}
}
基於物品協同的推薦生成,找出與目標用戶購買過的商品中最相似的前幾個商品中目標用戶也沒有買過的商品推薦給用戶。
public class ItemCF {
/**
* 物品協同推薦
*
* @param userId 用戶ID
* @param num 返回數量
* @param list 預處理數據
* @return 商品id集合
*/
public static List<Long> recommend(Long userId, Integer num,
List<RelateDTO> list) {
// 按物品分組
Map<Long, List<RelateDTO>> userMap = list.stream()
.collect(Collectors.groupingBy(RelateDTO::getUserId));
List<Long> userProductItems = userMap.get(userId).stream()
.map(RelateDTO::getProductId).toList();
Map<Long, List<RelateDTO>> itemMap = list.stream()
.collect(Collectors.groupingBy(RelateDTO::getProductId));
List<Long> similarProductIdList = new ArrayList<>();
Multimap<Double, Long> itemTotalDisMap = TreeMultimap.create();
for (Long itemId : userProductItems) {
// 獲取其他物品與當前物品的關係值
Map<Double, Long> itemDisMap = CoreMath.computeNeighbor(itemId, itemMap, 1);
itemDisMap.forEach(itemTotalDisMap::put);
}
List<Double> values = new ArrayList<>(itemTotalDisMap.keySet());
values.sort(Collections.reverseOrder());
List<Double> scoresList = values.stream().limit(num).toList();
// 獲取關係最近的用戶
for (Double aDouble : scoresList) {
Collection<Long> longs = itemTotalDisMap.get(aDouble);
for (Long productId : longs) {
if (!userProductItems.contains(productId)) {
similarProductIdList.add(productId);
}
}
}
return similarProductIdList.stream().distinct().limit(num).toList();
}
}
3.4 推薦評估和優化
在newbee-mall-pro中可以針對為你推薦欄目中推薦的商品做曝光率、點擊率、下單數等作為監控指標來評估推薦效果。
四、用戶協同和物品協同應用場景
用戶協同和物品協同都是兩種常用的推薦系統演算法,它們分別利用用戶之間和物品之間的相似度來給用戶提供個性化的推薦。用戶協同和物品協同的應用場景有以下幾種:
- 用戶協同適用於用戶數量相對較少,用戶興趣相對穩定,物品數量相對較多,物品更新頻率較高的場景。例如,電影推薦、音樂推薦、圖書推薦等。
- 物品協同適用於用戶數量相對較多,用戶興趣相對多變,物品數量相對較少,物品更新頻率較低的場景。例如,新聞推薦、廣告推薦、社交網路推薦等。
- 用戶協同和物品協同也可以結合起來,形成混合推薦系統,以提高推薦的準確性和覆蓋率。例如,電商平臺可以根據用戶的購買歷史和評價,以及物品的屬性和銷量,綜合使用用戶協同和物品協同來給用戶推薦商品。
商城系統使用用戶協同還是物品協同,這是一個需要根據具體情況進行選擇的問題。用戶協同是指根據用戶之間的相似度,為用戶推薦他們可能感興趣的物品。物品協同是指根據物品之間的相似度,為用戶推薦與他們已經購買或瀏覽過的物品相似的物品。兩種方法各有優缺點,需要綜合考慮商城系統的目標、規模、數據量、稀疏度等因素。一般來說,如果商城系統的目標是增加用戶的多樣性和探索性,那麼用戶協同可能更合適,因為它可以為用戶提供更廣泛的選擇。如果商城系統的目標是增加用戶的滿意度和忠誠度,那麼物品協同可能更合適,因為它可以為用戶提供更精準的推薦
在一般商城系統中,初期用戶數量少可以使用用戶協同,後期用戶數遠超商品數,使用物品協同會更好些,這兩者也可以結合使用。推薦演算法是不會一成不變的,它需要根據某些指標數據不斷優化調整升值甚至重構使用另外的演算法。
五、冷啟動問題
商城協同演算法冷啟動問題是指在商城系統中,當新用戶或新商品加入時,由於缺乏足夠的交互數據,導致協同過濾演算法無法為其提供準確的推薦結果。
在newbee-mall-pro就是指新用戶還未下單
這種問題會影響商城的用戶體驗和轉化率,因此需要有效的解決方案。一種常見的方法是使用流行度演算法。
利用基於流行度的演算法非常簡單粗暴,類似於各大新聞、微博熱榜、商城等,根據PV、UV、點擊率、搜索率、下單商品排行等數據來按某種熱度排序來推薦給用戶。
總結
到這裡,本文所分享推薦演算法在商城系統實踐就全部介紹完了,希望對大家實現推薦系統落地有所幫助,喜歡的朋友們可以點贊加關註