推薦演算法在商城系統實踐

waynaqua發表於2023-04-09

一、簡介

本文博主給大家講解如何在自己開源的電商專案newbee-mall-pro中應用協同過濾演算法來達到給使用者更好的購物體驗效果。

newbee-mall-pro專案地址:


二、協同過濾演算法

協同過濾演算法是一種基於使用者或者物品的相似度來推薦商品的方法,它可以有效地解決商城系統中的資訊過載問題。協同過濾演算法的實踐主要包括以下幾個步驟:

  1. 資料收集和預處理。這一步需要從商城系統中獲取使用者的行為資料,如瀏覽、購買、評價等,然後進行一些必要的清洗和轉換,以便後續的分析和計算。
  2. 相似度計算。這一步需要根據使用者或者物品的特徵或者行為,採用合適的相似度度量方法,如餘弦相似度、皮爾遜相關係數、Jaccard指數等,來計算使用者之間或者物品之間的相似度矩陣。
  3. 推薦生成。這一步需要根據相似度矩陣和使用者的歷史行為,採用合適的推薦策略,如基於鄰域的方法、基於模型的方法、基於矩陣分解的方法等,來生成針對每個使用者的個性化推薦列表。
  4. 推薦評估和最佳化。這一步需要根據一些評價指標,如準確率、召回率、覆蓋率、多樣性等,來評估推薦系統的效果,並根據反饋資訊和業務需求,進行一些引數調整和演算法最佳化,以提高推薦系統的效能和使用者滿意度。

在原有的商城首頁為你推薦欄目是使用後臺配置的商品列表,基於人為配置。在專案商品使用者持續增長的情況下,不一定能給使用者推薦使用者可能想要的商品。

因此在v2.4.1版本中,商城首頁為你推薦欄目新增了協同過濾演算法。按照UserCF基於使用者的協同過濾、ItemCF基於物品的協同過濾。 實現了兩種不同的推薦邏輯。

  • UserCF:基於使用者的協同過濾。當一個使用者A需要個性化推薦的時候,我們可以先找到和他有相似興趣的其他使用者,然後把那些使用者喜歡的,而使用者A沒有聽說過的物品推薦給A。
    userCF.png
    假設使用者 A 喜歡物品 A、物品 C,使用者 B 喜歡物品 B,使用者 C 喜歡物品 A 、物品 C 和物品 D;從這些使用者的歷史喜好資訊中,我們可以發現使用者 A 和使用者 C 的口味和偏好是比較類似的,同時使用者 C 還喜歡物品 D,那麼我們可以推斷使用者 A 可能也喜歡物品 D,因此可以將物品 D 推薦給使用者 A。具體程式碼在 ltd.newbee.mall.recommend.core.UserCF 中。

  • itemCF:基於物品的協同過濾。預先根據所有使用者的歷史偏好資料計算物品之間的相似度,然後把與使用者喜歡的物品相類似的物品推薦給使用者。 
    itemCF.png
    假如使用者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、點選率、搜尋率、下單商品排行等資料來按某種熱度排序來推薦給使用者。

總結

到這裡,本文所分享推薦演算法在商城系統實踐就全部介紹完了,希望對大家實現推薦系統落地有所幫助,喜歡的朋友們可以點贊加關注?。

公眾號【waynblog】每週更新博主最新技術文章,歡迎大家關注

相關文章