如何設計一個最簡化的推薦系統

陳國利發表於2023-03-17

本文目錄結構

1、背景

2、推薦系統初識

3、通用推薦系統架構

4、經典推薦演算法

5、實現一個推薦系統

6、存在問題與展望

 

1、背景

近期由於公司業務系統需要做一個推薦系統,應該說是實現一個相當簡單推薦邏輯。畢竟業務場景相當簡單,企業的資料規模也比較小,各種使用者資料、交易資料、訂單資料、行為資料等加在一起100多TB量級,這點資料量在巨大的平臺商面前都談不資料規模。資料級量小,業務場景也簡單,實現起比較的容易,但原理基本上大同小異。

本文嘗試從最簡單的推薦入手,我們暫不去討論大規模資料分析和演算法。更多從軟體工程角度思考問題,那些高大上演算法留給讀者去思考。

下面就來談談整個推薦的設計實現過程。

 

2、推薦系統介紹

 

2.1、為什麼需要推薦系統?

  首先推薦系統作用是很大的。推薦系統在很多業務場景有廣泛使用和發揮的空間,它的應用和影子無處不在,在多媒體內容、廣告平臺和電商平臺尤為常見。大多平臺商或企業都是基於大資料演算法分析做推薦系統。推薦演算法也是層出不窮,比如相似度計算、近鄰推薦、機率矩陣分解、機率圖等,當然也有綜合多種演算法融合使用。

  網際網路時代,資料呈爆炸式增長,前所未有的資料量遠遠超過受眾的接收和處理能力,因此,從海量複雜資料中有效獲取關鍵性有用資訊成為必須解決的問題。
面對資訊過載問題,人們迫切需要一種高效的資訊過濾系統,“推薦系統”應運而生。

  20世紀90年代以來,儘管推薦系統在理論、方法和應用方面取得了系列重要進展,但資料的稀疏性與長尾性、使用者行為模式挖掘、可解釋性、社會化推薦等問題仍然是其面臨的重要挑戰。

  進一步地,伴隨網際網路及資訊科技的持續飛速發展,使用者規模與專案數量急劇增長,相應地,使用者行為資料的稀疏性、長尾性問題更加凸顯。也就是說目前各大平臺雖然已經推薦系統,但是實際應用當中還是面臨很多問題,仍然有很大的提升空間。這是技術挑戰也機會,當然這也是我們這些從業者可以發揮的地方。

2.2、推薦系統解決什麼問題?

  推薦系統從20世紀90年代就被提出來了,但是真正進入大眾視野以及在各大網際網路公司中流行起來,還是最近幾年的事情。

  隨著移動網際網路的發展,越來越多的資訊開始在網際網路上傳播,產生了嚴重的資訊過載。因此,如何從眾多資訊中找到使用者感興趣的資訊,這個便是推薦系統的價值。精準推薦解決了使用者痛點,提升了使用者體驗,最終便能留住使用者。

  推薦系統本質上就是一個資訊過濾系統,通常分為:召回、排序、重排序這3個環節,每個環節逐層過濾,最終從海量的物料庫中篩選出幾十個使用者可能感興趣的物品推薦給使用者。

推薦系統的分階段過濾流程如下圖所示:

如何設計一個最簡化的推薦系統

 

2.3、推薦系統應用場景

  哪裡有海量資訊,哪裡就有推薦系統,我們每天最常用的APP都涉及到推薦功能:

  • 資訊類:今日頭條、騰訊新聞等
  • 電商類:淘寶、京東、拼多多、亞馬遜等
  • 娛樂類:抖音、快手、愛奇藝等
  • 生活服務類:美團、大眾點評、攜程等
  • 社交類:微信、陌陌、脈脈等

 

如何設計一個最簡化的推薦系統

 

實際例子還有很多,稍微上一點規模的平臺或APP都有這一個推薦模組。

推薦系統的應用場景通常分為以下兩類:

  • 基於使用者維度的推薦:根據使用者的歷史行為和興趣進行推薦,比如淘寶首頁的猜你喜歡、抖音的首頁推薦等。
  • 基於物品維度的推薦:根據使用者當前瀏覽的標的物進行推薦,比如開啟京東APP的商品詳情頁,會推薦和主商品相關的商品給你。

 

2.4、搜尋、推薦、廣告三者的異同

搜尋和推薦是AI演算法最常見的兩個應用場景,在技術上有相通的地方。

  • 搜尋:有明確的搜尋意圖,搜尋出來的結果和使用者的搜尋詞相關。
  • 推薦:不具有目的性,依賴使用者的歷史行為和畫像資料進行個性化推薦。
  • 廣告:藉助搜尋和推薦技術實現廣告的精準投放,可以將廣告理解成搜尋推薦的一種應用場景,技術方案更復雜,涉及到智慧預算控制、廣告競價等。

 

3、推薦系統通用框架

  推薦系統涉及周邊和自己模組還是比較多,這裡主要從最簡單推薦系統自身功能去構思設計簡單結構。

如何設計一個最簡化的推薦系統

  上面這個圖基本把推薦處理過程畫出來,結構比較清晰,看圖理想即可。

 

從分層架構設計視角來說可以分成多層架構形式

分層:排序層、過濾層、召回層、資料儲存層、計算平臺、資料來源。

  可以說市面上推薦系統設計都是差不多是這個樣子,只是裡面使用技術或元件不同而已。

如何設計一個最簡化的推薦系統

上面是推薦系統的整體架構圖,自下而上分成了多層,各層的主要作用如下:

  • 資料來源:推薦演算法所依賴的各種資料來源,包括物品資料、使用者資料、行為日誌、其他可利用的業務資料、甚至公司外部的資料。
  • 計算平臺:負責對底層的各種異構資料進行清洗、加工,離線計算和實時計算。
  • 資料儲存層:儲存計算平臺處理後的資料,根據需要可落地到不同的儲存系統中,比如Redis中可以儲存使用者特徵和使用者畫像資料,ES中可以用來索引物品資料,Faiss中可以儲存使用者或者物品的embedding向量等。
  • 召回層:包括各種推薦策略或者演算法,比如經典的協同過濾,基於內容的召回,基於向量的召回,用於託底的熱門推薦等。為了應對線上高併發的流量,召回結果通常會預計算好,建立好倒排索引後存入快取中。
  • 融合過濾層:觸發多路召回,由於召回層的每個召回源都會返回一個候選集,因此這一層需要進行融合和過濾。
  • 排序層:利用機器學習或者深度學習模型,以及更豐富的特徵進行重排序,篩選出更小、更精準的推薦集合返回給上層業務。

從資料儲存層到召回層、再到融合過濾層和排序層,候選集逐層減少,但是精準性要求越來越高,因此也帶來了計算複雜度的逐層增加,這個便是推薦系統的最大挑戰。

其實對於推薦引擎來說,最核心的部分主要是兩塊:特徵和演算法。

如何設計一個最簡化的推薦系統

這些工具和技術框架都是比較成熟穩定的,是眾多廠商在實際業務場景中選擇應用的。所以也沒有太多特殊的地方。

特徵計算由於資料量大,通常採用大資料的離線和實時處理技術,像Spark、Flink等,然後將計算結果儲存在Redis或者其他儲存系統中(比如HBase、MongoDB或者ES),供召回和排序模組使用。

召回演算法的作用是:從海量資料中快速獲取一批候選資料,要求是快和儘可能的準。這一層通常有豐富的策略和演算法,用來確保多樣性,為了更好的推薦效果,某些演算法也會做成近實時的。

排序演算法的作用是:對多路召回的候選集進行精細化排序。它會利用物品、使用者以及它們之間的交叉特徵,然後透過複雜的機器學習或者深度學習模型進行打分排序,這一層的特點是計算複雜但是結果更精準。

 

4、經典演算法

  瞭解了推薦系統的整體架構和技術方案後,下面帶大家深入一下演算法細節。這裡選擇圖解的是推薦系統中的明星演算法:協同過濾(Collaborative Filtering,CF)。

對於很多同學來說,可能覺得 AI 演算法晦澀難懂,門檻太高,確實很多深度學習演算法的確是這樣,但是協同過濾卻是一個簡單同時效果很好的演算法,只要你有初中數學的基礎就能看懂。

4.1、協同過濾是什麼?

協同過濾演算法的核心就是「找相似」,它基於使用者的歷史行為(瀏覽、收藏、評論等),去發現使用者對物品的喜好,並對喜好進行度量和打分,最終篩選出推薦集合。它又包括兩個分支:

  • 基於使用者的協同過濾:User-CF,核心是找相似的人。比如下圖中,使用者 A 和使用者 C 都購買過物品 a 和物品 b,那麼可以認為 A 和 C 是相似的,因為他們共同喜歡的物品多。這樣,就可以將使用者 A 購買過的物品 d 推薦給使用者 C。

如何設計一個最簡化的推薦系統

 

   基於使用者的協同過濾示例

  • 基於物品的協同過濾:Item-CF,核心是找相似的物品。比如下圖中,物品 a 和物品 b 同時被使用者 A,B,C 購買了,那麼物品a 和物品 b 被認為是相似的,因為它們的共現次數很高。這樣,如果使用者 D 購買了物品 a,則可以將和物品 a 最相似的物品 b 推薦給使用者 D。

如何設計一個最簡化的推薦系統

4.2、如何找相似?

  協同過濾的核心就是找相似,User-CF是找使用者之間的相似,Item-CF是找物品之間的相似,那到底如何衡量兩個使用者或者物品之間的相似性呢?

我們都知道,對於座標中的兩個點,如果它們之間的夾角越小,這兩個點越相似。

這就是初中學過的餘弦距離,它的計算公式如下:

如何設計一個最簡化的推薦系統

舉個例子,A座標是(0,3,1),B座標是(4,3,0),那麼這兩個點的餘弦距離是0.569,餘弦距離越接近1,表示它們越相似。

如何設計一個最簡化的推薦系統

  除了餘弦距離,衡量相似性的方法還有很多種,比如:歐式距離、皮爾遜相關係數、Jaccard 相似係數等等,這裡不做展開,只是計算公式上的差異而已。

4.3、Item-CF的演算法流程

  清楚了相似性的定義後,下面以Item-CF為例,詳細說下這個演算法到底是如何選出推薦物品的?

4.3.1 、整理物品的共現矩陣

假設有 A、B、C、D、E 5個使用者,其中使用者 A 喜歡物品 a、b、c,使用者 B 喜歡物品 a、b等等。

如何設計一個最簡化的推薦系統

  所謂共現,即:兩個物品被同一個使用者喜歡了。比如物品 a 和 b,由於他們同時被使用者 A、B、C 喜歡,所以 a 和 b 的共現次數是3,採用這種統計方法就可以快速構建出共現矩陣。

4.3.2、 計算物品的相似度矩陣

  對於 Item-CF 演算法來說,一般不採用前面提到的餘弦距離來衡量物品的相似度,而是採用下面的公式:

如何設計一個最簡化的推薦系統

其中,N(u) 表示喜歡物品 u 的使用者數,N(v) 表示喜歡物品 v 的使用者數,兩者的交集表示同時喜歡物品 u 和物品 v 的使用者數。

很顯然,如果兩個物品同時被很多人喜歡,那麼這兩個物品越相似。

基於第1步計算出來的共現矩陣以及每個物品的喜歡人數,便可以構造出物品的相似度矩陣:

如何設計一個最簡化的推薦系統

4.3.2、 推薦物品

最後一步,便可以基於相似度矩陣推薦物品了,公式如下:

如何設計一個最簡化的推薦系統

其中,Puj 表示使用者 u 對物品 j 的感興趣程度,值越大,越值得被推薦。

N(u) 表示使用者 u 感興趣的物品集合,S(j,N) 表示和物品 j 最相似的前 N 個物品,Wij 表示物品 i 和物品 j 的相似度,Rui 表示使用者 u 對物品 i 的興趣度。

上面的公式有點抽象,直接看例子更容易理解,假設我要給使用者 E 推薦物品,前面我們已經知道使用者 E 喜歡物品 b 和物品 c,喜歡程度假設分別為 0.6 和 0.4。

那麼,利用上面的公式計算出來的推薦結果如下:

如何設計一個最簡化的推薦系統

因為物品 b 和物品 c 已經被使用者 E 喜歡過了,所以不再重複推薦。最終對比使用者 E 對物品 a 和物品 d 的感興趣程度,因為 0.682 > 0.3,因此選擇推薦物品 a。

 

5、如何實現推薦系統

5.1、選擇資料集

  這裡採用的是推薦領域非常經典的 MovieLens 資料集,它是一個關於電影評分的資料集,官網上提供了多個不同大小的版本,下面以 ml-1m 資料集(大約100萬條使用者評分記錄)為例。

下載解壓後,資料夾中包含:ratings.dat、movies.dat、users.dat 3個檔案,共6040個使用者,3900部電影,1000209條評分記錄。各個檔案的格式都是一樣的,每行表示一條記錄,欄位之間採用 :: 進行分割。

以ratings.dat為例,每一行包括4個屬性:UserID, MovieID, Rating, Timestamp。

透過指令碼可以統計出不同評分的人數分佈:

如何設計一個最簡化的推薦系統

 

 

5.2、讀取原始資料

程式主要使用資料集中的 ratings.dat 這個檔案,透過解析該檔案,抽取出 user_id、movie_id、rating 3個欄位,最終構造出演算法依賴的資料,並儲存在變數 dataset 中,它的格式為:dict[user_id][movie_id] = rate

5.3、構造物品的相似度矩陣

基於第 2 步的 dataset,可以進一步統計出每部電影的評分次數以及電影的共生矩陣,然後再生成相似度矩陣。

 

5.4、基於相似度矩陣推薦物品

最後,可以基於相似度矩陣進行推薦了,輸入一個使用者id,先針對該使用者評分過的電影,依次選出 top 10 最相似的電影,然後加權求和後計算出每個候選電影的最終評分,最後再選擇得分前 5 的電影進行推薦。

 

5.5、呼叫推薦系統

下面選擇 UserId=1 這個使用者,看下程式的執行結果。由於推薦程式輸出的是 movieId 列表,為了更直觀的瞭解推薦結果,這裡轉換成電影的標題進行輸出。

 

Java程式碼示例

import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;

public class CFRecommendation {
    // 使用MovieLens資料集
    private static final String RATINGS_FILE = "ratings.csv";
    // 使用者ID-電影ID-打分
    private static Map<Integer, Map<Integer, Double>> ratings;

    // 載入ratings.csv檔案
    private static void loadRatings() throws IOException {
        File file = new File(RATINGS_FILE);
        Scanner scanner = new Scanner(file);
        ratings = new HashMap<>();
        while (scanner.hasNextLine()) {
            String line = scanner.nextLine();
            String[] data = line.split(",");
            int userId = Integer.parseInt(data[0]);
            int movieId = Integer.parseInt(data[1]);
            double rating = Double.parseDouble(data[2]);
            // 使用者-電影-打分
            Map<Integer, Double> movieRatings = ratings.get(userId);
            if (movieRatings == null) {
                movieRatings = new HashMap<>();
                ratings.put(userId, movieRatings);
            }
            movieRatings.put(movieId, rating);
        }
    }

    // 計算兩個使用者的相似度
    private static double calculateSimilarity(int user1, int user2) {
        Map<Integer, Double> rating1 = ratings.get(user1);
        Map<Integer, Double> rating2 = ratings.get(user2);
        if (rating1 == null || rating2 == null) {
            return 0;
        }
        double sum1 = 0;
        double sum2 = 0;
        double sumProduct = 0;
        for (int movieId : rating1.keySet()) {
            if (rating2.containsKey(movieId)) {
                double rating1Value = rating1.get(movieId);
                double rating2Value = rating2.get(movieId);
                sum1 += rating1Value * rating1Value;
                sum2 += rating2Value * rating2Value;
                sumProduct += rating1Value * rating2Value;
            }
        }
        return sumProduct / (Math.sqrt(sum1) * Math.sqrt(sum2));
    }
    
    // 計算餘弦相似度
    public static double cosineSim(Map<String, Integer> user1, Map<String, Integer> user2){
        double result = 0;
        double denominator1 = 0;
        double denominator2 = 0;
        double numerator = 0;
        for(String key : user1.keySet()){
            numerator += user1.get(key) * user2.get(key);
            denominator1 += Math.pow(user1.get(key), 2);
            denominator2 += Math.pow(user2.get(key), 2);
        }
        result = numerator / (Math.sqrt(denominator1) * Math.sqrt(denominator2));
        return result;
    }

    // 使用協同過濾演算法獲取使用者的推薦列表
    private static Map<Integer, Double> recommend(int userId) {
        Map<Integer, Double> recommendList = new HashMap<>();
        // 遍歷所有使用者
        for (int otherUserId : ratings.keySet()) {
            if (otherUserId != userId) {
                double similarity = calculateSimilarity(userId, otherUserId);
                Map<Integer, Double> otherRating = ratings.get(otherUserId);
                // 遍歷其他使用者的評分,如果當前使用者沒有評分,則將其推薦給當前使用者
                for (int movieId : otherRating.keySet()) {
                    if (!ratings.get(userId).containsKey(movieId)) {
                        double recommendScore = otherRating.get(movieId) * similarity;
                        recommendList.put(movieId, recommendScore);
                    }
                }
            }
        }
        return recommendList;
    }

    public static void main(String[] args) throws IOException {
        loadRatings();
        // 測試用例:計算使用者1與使用者2的相似度
        int user1 = 1;
        int user2 = 2;
        double similarity = calculateSimilarity(user1, user2);
        System.out.println("使用者1與使用者2的相似度:" + similarity);
        // 測試用例:為使用者1推薦電影
        int userId = 1;
        Map<Integer, Double> recommendList = recommend(userId);
        System.out.println("為使用者1推薦的電影:");
        for (int movieId : recommendList.keySet()) {
            System.out.println("電影ID:" + movieId + ",推薦分數:" + recommendList.get(movieId));
        }
    }
}

 

 

5、問題與展望

  透過上面的介紹,大家對推薦系統的基本構成應該有了一個初步認識,但是真正運用到線上真實環境時,還會遇到很多演算法和工程上的挑戰,絕對不是幾十行程式碼可以搞定的。

問題:

    1. 上面的示例使用了標準化的資料集,而線上環境的資料是非標準化的,因此涉及到海量資料的收集、清洗和加工,最終構造出模型可使用的資料集。
    2. 複雜且繁瑣的特徵工程,都說演算法模型的上限由資料和特徵決定。對於線上環境,需要從業務角度選擇出可用的特徵,然後對資料進行清洗、標準化、歸一化、離散化,並透過實驗效果進一步驗證特徵的有效性。
    3. 演算法複雜度如何降低?比如上面介紹的Item-CF演算法,時間和空間複雜度都是O(N×N),而線上環境的資料都是千萬甚至上億級別的,如果不做演算法最佳化,可能幾天都跑不出資料,或者記憶體中根本放不下如此大的矩陣資料。
    4. 實時性如何滿足?因為使用者的興趣隨著他們最新的行為在實時變化的,如果模型只是基於歷史資料進行推薦,可能結果不夠精準。因此,如何滿足實時性要求,以及對於新加入的物品或者使用者該如何推薦,都是要解決的問題。
    5. 演算法效果和效能的權衡。從演算法角度追求多樣性和準確性,從工程角度追求效能,這兩者之間必須找到一個平衡點。
    6. 推薦系統的穩定性和效果追蹤。需要有一套完善的資料監控和應用監控體系,同時有 ABTest 平臺進行灰度實驗,進行效果對比。

展望:

AI時代,演算法會更加複雜和完善,推薦的效果也會越來越好,特別是隨著OpenAI ChatGPT橫空出現,推薦系統最有條件和最適合GPT模型去結合使用,當然也會更加高效和智慧。期待我們智慧版推薦系統早日面世。

 

相關文章