推薦算法在商城系統(tǒng)實踐

一、簡介

本文博主給大家講解如何在自己開源的電商項目newbee-mall-pro中應用協(xié)同過濾算法來達到給用戶更好的購物體驗效果雳窟。

newbee-mall-pro項目地址:


二孽水、協(xié)同過濾算法

協(xié)同過濾算法是一種基于用戶或者物品的相似度來推薦商品的方法票腰,它可以有效地解決商城系統(tǒng)中的信息過載問題。協(xié)同過濾算法的實踐主要包括以下幾個步驟:

  1. 數(shù)據(jù)收集和預處理女气。這一步需要從商城系統(tǒng)中獲取用戶的行為數(shù)據(jù)杏慰,如瀏覽、購買炼鞠、評價等缘滥,然后進行一些必要的清洗和轉(zhuǎn)換,以便后續(xù)的分析和計算谒主。
  2. 相似度計算朝扼。這一步需要根據(jù)用戶或者物品的特征或者行為,采用合適的相似度度量方法霎肯,如余弦相似度擎颖、皮爾遜相關系數(shù)、Jaccard指數(shù)等观游,來計算用戶之間或者物品之間的相似度矩陣搂捧。
  3. 推薦生成。這一步需要根據(jù)相似度矩陣和用戶的歷史行為懂缕,采用合適的推薦策略允跑,如基于鄰域的方法、基于模型的方法搪柑、基于矩陣分解的方法等聋丝,來生成針對每個用戶的個性化推薦列表。
  4. 推薦評估和優(yōu)化工碾。這一步需要根據(jù)一些評價指標弱睦,如準確率、召回率倚喂、覆蓋率每篷、多樣性等,來評估推薦系統(tǒng)的效果端圈,并根據(jù)反饋信息和業(yè)務需求焦读,進行一些參數(shù)調(diào)整和算法優(yōu)化,以提高推薦系統(tǒng)的性能和用戶滿意度舱权。

在原有的商城首頁為你推薦欄目是使用后臺配置的商品列表矗晃,基于人為配置。在項目商品用戶持續(xù)增長的情況下宴倍,不一定能給用戶推薦用戶可能想要的商品张症。

因此在v2.4.1版本中仓技,商城首頁為你推薦欄目添加了協(xié)同過濾算法。按照UserCF基于用戶的協(xié)同過濾俗他、ItemCF基于物品的協(xié)同過濾脖捻。 實現(xiàn)了兩種不同的推薦邏輯。

  • UserCF:基于用戶的協(xié)同過濾兆衅。當一個用戶A需要個性化推薦的時候地沮,我們可以先找到和他有相似興趣的其他用戶,然后把那些用戶喜歡的羡亩,而用戶A沒有聽說過的物品推薦給A摩疑。
    [圖片上傳失敗...(image-29b6c-1681048334517)]
    假設用戶 A 喜歡物品 A、物品 C畏铆,用戶 B 喜歡物品 B雷袋,用戶 C 喜歡物品 A 、物品 C 和物品 D辞居;從這些用戶的歷史喜好信息中楷怒,我們可以發(fā)現(xiàn)用戶 A 和用戶 C 的口味和偏好是比較類似的,同時用戶 C 還喜歡物品 D速侈,那么我們可以推斷用戶 A 可能也喜歡物品 D率寡,因此可以將物品 D 推薦給用戶 A迫卢。具體代碼在 ltd.newbee.mall.recommend.core.UserCF 中倚搬。

  • itemCF:基于物品的協(xié)同過濾。預先根據(jù)所有用戶的歷史偏好數(shù)據(jù)計算物品之間的相似度乾蛤,然后把與用戶喜歡的物品相類似的物品推薦給用戶每界。
    [圖片上傳失敗...(image-b7723d-1681048334518)]
    假如用戶A喜歡物品A和物品C,用戶B喜歡物品A家卖、物品B和物品C眨层,用戶C喜歡物品A,從這些用戶的歷史喜好中可以認為物品A與物品C比較類似上荡,喜歡物品A的都喜歡物品C趴樱,基于這個判斷用戶C可能也喜歡物品C,所以推薦系統(tǒng)將物品C推薦給用戶C酪捡。 具體代碼在 ltd.newbee.mall.recommend.core.ItemCF 中叁征。

三、推薦算法代碼實踐

3.1 數(shù)據(jù)收集和預處理

newbee-mall-pro中逛薇,我們基于用戶下單的商品數(shù)據(jù)進行收集和預處理捺疼。

/**
 * 根據(jù)所有用戶購買商品的記錄進行數(shù)據(jù)手機
 *
 * @return List<RelateDTO>
 */
@Override
public List<RelateDTO> getRelateData() {
    List<RelateDTO> relateDTOList = new ArrayList<>();
    // 獲取所有訂單以及訂單關聯(lián)商品的集合
    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));
    // 遍歷訂單,生成預處理數(shù)據(jù)
    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中官扣,我們將用戶之間下單的商品進行相似度計算翅敌,因為如果兩個用戶購買了同一個商品,那么我們認為這兩個用戶之間是存在聯(lián)系并且都存在付費行為惕蹄。

// 遍歷訂單商品
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);
    // 通過計算商品購買次數(shù)哼御,來建立相似度
    List<OrderItemVO> list = goodsListMap.getOrDefault(goodsId, Collections.emptyList());
    int sum = list.stream().mapToInt(OrderItemVO::getGoodsCount).sum();
    relateDTO.setIndex(sum);
    relateDTOList.add(relateDTO);
}

通過余弦相似度算法計算用戶與商品之間的相似度,從而為用戶推薦最相似的商品焊唬。當兩個用戶購買了同一個商品時恋昼,我們就認為兩個用戶產(chǎn)生了關聯(lián),因此針對兩個用戶購買的同一個商品進行相似度計算赶促,來建立用戶之間的相似度液肌。

余弦相似度是一種用于衡量兩個向量之間的相似度的方法,它通過計算兩個向量的夾角的余弦值來得到鸥滨。在商城系統(tǒng)中嗦哆,余弦相似度可以用于實現(xiàn)基于內(nèi)容的推薦算法,即根據(jù)用戶的歷史購買或瀏覽行為婿滓,為用戶推薦與其興趣相似的商品老速。具體來說,可以將每個商品表示為一個特征向量凸主,例如商品的類別橘券、價格、評分等卿吐,然后將每個用戶表示為一個偏好向量旁舰,例如用戶購買或瀏覽過的商品的特征向量的加權平均。這樣嗡官,就可以利用余弦相似度來計算用戶和商品之間的相似度箭窜,從而為用戶推薦最相似的商品。

計算相關系數(shù)衍腥,傳入用戶ID或者物品ID磺樱,計算相似度

/**
 * 計算相關系數(shù)并排序
 *
 * @param key  基于用戶協(xié)同代表用戶id,基于物品協(xié)同代表武平id
 * @param map  預處理數(shù)據(jù)集
 * @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)) {
            // 計算關系系數(shù)
            double coefficient = relateDist(v, items, type);
            distMap.put(coefficient, k);
        }
    });
    return distMap;
}

計算兩個用戶間的相關系數(shù)

/**
 * 計算兩個序列間的相關系數(shù)
 *
 * @param xList
 * @param yList
 * @param type  類型0基于用戶推薦使用余弦相似度 1基于物品推薦使用余弦相似度 2基于用戶推薦使用皮爾森系數(shù)計算
 * @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 推薦生成

基于用戶協(xié)同的推薦生成擅耽,我們可以先找到和目標用戶有相似興趣的其他用戶活孩,然后把其他用戶喜歡的,而目標用戶沒有買過的物品推薦給目標用戶。

public class UserCF {
    /**
     * 物用戶協(xié)同推薦
     *
     * @param userId 用戶ID
     * @param num    返回數(shù)量
     * @param list   預處理數(shù)據(jù)
     * @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();
    }
}

基于物品協(xié)同的推薦生成询兴,找出與目標用戶購買過的商品中最相似的前幾個商品中目標用戶也沒有買過的商品推薦給用戶。

public class ItemCF {

    /**
     * 物品協(xié)同推薦
     *
     * @param userId 用戶ID
     * @param num    返回數(shù)量
     * @param list   預處理數(shù)據(jù)
     * @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 推薦評估和優(yōu)化

newbee-mall-pro中可以針對為你推薦欄目中推薦的商品做曝光率起趾、點擊率诗舰、下單數(shù)等作為監(jiān)控指標來評估推薦效果。

四训裆、用戶協(xié)同和物品協(xié)同應用場景

用戶協(xié)同和物品協(xié)同都是兩種常用的推薦系統(tǒng)算法眶根,它們分別利用用戶之間和物品之間的相似度來給用戶提供個性化的推薦。用戶協(xié)同和物品協(xié)同的應用場景有以下幾種:

  • 用戶協(xié)同適用于用戶數(shù)量相對較少边琉,用戶興趣相對穩(wěn)定属百,物品數(shù)量相對較多,物品更新頻率較高的場景变姨。例如族扰,電影推薦、音樂推薦定欧、圖書推薦等渔呵。
  • 物品協(xié)同適用于用戶數(shù)量相對較多,用戶興趣相對多變砍鸠,物品數(shù)量相對較少扩氢,物品更新頻率較低的場景。例如爷辱,新聞推薦录豺、廣告推薦、社交網(wǎng)絡推薦等托嚣。
  • 用戶協(xié)同和物品協(xié)同也可以結(jié)合起來巩检,形成混合推薦系統(tǒng)厚骗,以提高推薦的準確性和覆蓋率示启。例如,電商平臺可以根據(jù)用戶的購買歷史和評價领舰,以及物品的屬性和銷量夫嗓,綜合使用用戶協(xié)同和物品協(xié)同來給用戶推薦商品。

商城系統(tǒng)使用用戶協(xié)同還是物品協(xié)同冲秽,這是一個需要根據(jù)具體情況進行選擇的問題舍咖。用戶協(xié)同是指根據(jù)用戶之間的相似度,為用戶推薦他們可能感興趣的物品锉桑。物品協(xié)同是指根據(jù)物品之間的相似度排霉,為用戶推薦與他們已經(jīng)購買或瀏覽過的物品相似的物品。兩種方法各有優(yōu)缺點民轴,需要綜合考慮商城系統(tǒng)的目標攻柠、規(guī)模球订、數(shù)據(jù)量、稀疏度等因素瑰钮。一般來說冒滩,如果商城系統(tǒng)的目標是增加用戶的多樣性和探索性,那么用戶協(xié)同可能更合適浪谴,因為它可以為用戶提供更廣泛的選擇开睡。如果商城系統(tǒng)的目標是增加用戶的滿意度和忠誠度,那么物品協(xié)同可能更合適苟耻,因為它可以為用戶提供更精準的推薦

在一般商城系統(tǒng)中篇恒,初期用戶數(shù)量少可以使用用戶協(xié)同,后期用戶數(shù)遠超商品數(shù)凶杖,使用物品協(xié)同會更好些婚度,這兩者也可以結(jié)合使用。推薦算法是不會一成不變的官卡,它需要根據(jù)某些指標數(shù)據(jù)不斷優(yōu)化調(diào)整升值甚至重構(gòu)使用另外的算法蝗茁。

五、冷啟動問題

商城協(xié)同算法冷啟動問題是指在商城系統(tǒng)中寻咒,當新用戶或新商品加入時哮翘,由于缺乏足夠的交互數(shù)據(jù),導致協(xié)同過濾算法無法為其提供準確的推薦結(jié)果毛秘。

newbee-mall-pro就是指新用戶還未下單

這種問題會影響商城的用戶體驗和轉(zhuǎn)化率饭寺,因此需要有效的解決方案。一種常見的方法是使用流行度算法叫挟。

利用基于流行度的算法非常簡單粗暴艰匙,類似于各大新聞、微博熱榜抹恳、商城等员凝,根據(jù)PV、UV奋献、點擊率健霹、搜索率、下單商品排行等數(shù)據(jù)來按某種熱度排序來推薦給用戶瓶蚂。

總結(jié)

到這里糖埋,本文所分享推薦算法在商城系統(tǒng)實踐就全部介紹完了,希望對大家實現(xiàn)推薦系統(tǒng)落地有所幫助窃这,喜歡的朋友們可以點贊加關注??瞳别。

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

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市祟敛,隨后出現(xiàn)的幾起案子倍奢,更是在濱河造成了極大的恐慌,老刑警劉巖垒棋,帶你破解...
    沈念sama閱讀 217,542評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件卒煞,死亡現(xiàn)場離奇詭異,居然都是意外死亡叼架,警方通過查閱死者的電腦和手機畔裕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評論 3 394
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來乖订,“玉大人扮饶,你說我怎么就攤上這事≌Ч梗” “怎么了甜无?”我有些...
    開封第一講書人閱讀 163,912評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長哥遮。 經(jīng)常有香客問我岂丘,道長,這世上最難降的妖魔是什么眠饮? 我笑而不...
    開封第一講書人閱讀 58,449評論 1 293
  • 正文 為了忘掉前任奥帘,我火速辦了婚禮,結(jié)果婚禮上仪召,老公的妹妹穿的比我還像新娘寨蹋。我一直安慰自己,他們只是感情好扔茅,可當我...
    茶點故事閱讀 67,500評論 6 392
  • 文/花漫 我一把揭開白布已旧。 她就那樣靜靜地躺著,像睡著了一般召娜。 火紅的嫁衣襯著肌膚如雪运褪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,370評論 1 302
  • 那天萤晴,我揣著相機與錄音吐句,去河邊找鬼。 笑死店读,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的攀芯。 我是一名探鬼主播屯断,決...
    沈念sama閱讀 40,193評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了殖演?” 一聲冷哼從身側(cè)響起氧秘,我...
    開封第一講書人閱讀 39,074評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎趴久,沒想到半個月后丸相,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,505評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡彼棍,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,722評論 3 335
  • 正文 我和宋清朗相戀三年灭忠,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片座硕。...
    茶點故事閱讀 39,841評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡弛作,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出华匾,到底是詐尸還是另有隱情映琳,我是刑警寧澤,帶...
    沈念sama閱讀 35,569評論 5 345
  • 正文 年R本政府宣布蜘拉,位于F島的核電站萨西,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏旭旭。R本人自食惡果不足惜原杂,卻給世界環(huán)境...
    茶點故事閱讀 41,168評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望您机。 院中可真熱鬧穿肄,春花似錦、人聲如沸际看。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽仲闽。三九已至脑溢,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間赖欣,已是汗流浹背屑彻。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留顶吮,地道東北人社牲。 一個月前我還...
    沈念sama閱讀 47,962評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像悴了,于是被迫代替她去往敵國和親搏恤。 傳聞我的和親對象是個殘疾皇子违寿,可洞房花燭夜當晚...
    茶點故事閱讀 44,781評論 2 354

推薦閱讀更多精彩內(nèi)容