Java實現算法推薦:Mahout實踐

推薦系統概述

推薦系統概述

推薦算法分類:

按數據使用劃分:

  • 協同過濾算法:UserCF, ItemCF, ModelCF
  • 基於內容的推薦: 用戶內容屬性和物品內容屬性
  • 社會化過濾:基於用戶的社會網絡關係

按模型劃分:

  • 最近鄰模型:基於距離的協同過濾算法
  • Latent Factor Mode(SVD):基於矩陣分解的模型
  • Graph:圖模型,社會網絡圖模型

基於用戶的協同過濾算法UserCF

基於用戶的協同過濾,通過不同用戶對物品的評分來評測用戶之間的相似性,基於用戶之間的相似性做出推薦。簡單來講就是:給用戶推薦和他興趣相似的其他用戶喜歡的物品。
image

基於用戶的 CF 的基本思想相當簡單,基於用戶對物品的偏好找到相鄰鄰居用戶,然後將鄰居用戶喜歡的推薦給當前用戶。計算上,就是將一個用戶對所有物品的偏好作爲一個向量 來計算用戶之間的相似度,找到 K 鄰居後,根據鄰居的相似度權重以及他們對物品的偏好,預測當前用戶沒有偏好的未涉及物品,計算得到一個排序的物品列表作爲推薦。圖 2 給出了一個例子,對於用戶 A,根據用戶的歷史偏好,這裏只計算得到一個鄰居 – 用戶 C,然後將用戶 C 喜歡的物品 D 推薦給用戶 A。

基於物品的協同過濾算法ItemCF

於item的協同過濾,通過用戶對不同item的評分來評測item之間的相似性,基於item之間的相似性做出推薦。簡單來講就是:給用戶推薦和他之前喜歡的物品相似的物品。

image

基於物品的 CF 的原理和基於用戶的 CF 類似,只是在計算鄰居時採用物品本身,而不是從用戶的角度,即基於用戶對物品的偏好找到相似的物品,然後根據用戶的歷史偏好,推薦相似的物品給他。從計算 的角度看,就是將所有用戶對某個物品的偏好作爲一個向量來計算物品之間的相似度,得到物品的相似物品後,根據用戶歷史的偏好預測當前用戶還沒有表示偏好的 物品,計算得到一個排序的物品列表作爲推薦。圖 3 給出了一個例子,對於物品 A,根據所有用戶的歷史偏好,喜歡物品 A 的用戶都喜歡物品 C,得出物品 A 和物品 C 比較相似,而用戶 C 喜歡物品 A,那麼可以推斷出用戶 C 可能也喜歡物品 C。

注:基於物品的協同過濾算法,是目前商用最廣泛的推薦算法。

協同過濾算法實現,分爲2個步驟

  1. 計算物品之間的相似度
  2. 根據物品的相似度和用戶的歷史行爲給用戶生成推薦列表

Mathout:

東西太多……還是自己看博客吧


實戰

數據源

取數據庫t_recent_listen_00數據,總數據5323626條,導出sql文件導入本地數據庫
導入時間大約15個小時後中斷,刪除user_id爲0的數據等數據清理操作,共導入了2019-02-16及之前的收聽記錄,總條數4866861條

  • 節目收聽記錄538805條
  • 書籍收聽記錄3842982條
  • 閱讀記錄484644條

image

生成csv格式文件

  • book_csv_file.csv
  • album_csv_file.csv
  • read_csv_file.csv

python生成csv

import pymysql
import csv
import sys
import os


class ToCsv:

    def __init__(self):
        self.db = pymysql.connect(host='', user='', passwd='', port=3306, db='hi')
        self.cursor = self.db.cursor()
        self.last_id = 0
        self.count = 0
        self.book_count = 0
        self.album_count = 0
        self.read_count = 0

    def _release_db(self):
        self.db.close()
        self.cursor.close()

    def _do(self):
        while 1:
            ret = self._search_data()
            if ret == 0:
                break

    def _search_data(self):
        self.cursor.size = 50
        sql = 'select * from t_recent_listen_00 where id>%s limit 50'
        self.cursor.execute(sql, self.last_id)
        lines = self.cursor.fetchall()
        if (len(lines) > 0):
            self.last_id = lines[len(lines)-1][0]
            self.count = len(lines) + self.count;
            self._write_cvs(lines)
        if (self.count % 1000 == 0):
            print(self.count)
        return len(lines)
        # print(self.last_id)

    def _write_cvs(self, lines):
        for item in lines:
            if (item[10] == 4):
                book_cvs_file = open('book_csv_file.csv', 'a', newline='')
                book_writers = csv.writer(book_cvs_file)
                # book_writers.writerow(['user_id', 'book_id'])
                user_id = item[1]
                book_id = item[2]
                self.book_count += 1
                if (self.book_count % 1000 == 0):
                    print("book count", self.book_count)

                book_writers.writerow([str(user_id), str(book_id)])
            if (item[10] == 2):
                album_cvs_file = open('album_csv_file.csv', 'a', newline='')
                album_writers = csv.writer(album_cvs_file)
                # album_writers.writerow(['user_id', 'book_id'])
                user_id = item[1]
                book_id = item[2]
                self.album_count += 1
                if (self.album_count % 1000 == 0):
                    print("album count", self.album_count)
                album_writers.writerow([str(user_id), str(book_id)])
            if (item[10] == 10):
                read_cvs_file = open('read_csv_file.csv', 'a', newline='')
                read_writers = csv.writer(read_cvs_file)
                # album_writers.writerow(['user_id', 'book_id'])
                user_id = item[1]
                book_id = item[2]
                self.read_count += 1
                if (self.read_count % 1000 == 0):
                    print("read count", self.read_count)
                read_writers.writerow([str(user_id), str(book_id)])

    def main(self):
        pass


if __name__ == '__main__':
    c = ToCsv()
    c._do()
    c._release_db()

簡單講解一下,就是讀數據庫,每次查詢50條並寫在csv文件最後,自己寫的demo寫註釋太累了

book_csv_file.csv文件大小60M
image

java實現推薦系統

pom.xml

  <properties>
        <mahout.version>0.9</mahout.version>
    </properties>
    <dependencies>
        ...
        <dependency>
            <groupId>org.apache.mahout</groupId>
            <artifactId>mahout-core</artifactId>
            <version>${mahout.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.mahout</groupId>
            <artifactId>mahout-integration</artifactId>
            <version>${mahout.version}</version>
            <exclusions>
                <exclusion>
                    <groupId>org.mortbay.jetty</groupId>
                    <artifactId>jetty</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.apache.cassandra</groupId>
                    <artifactId>cassandra-all</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>me.prettyprint</groupId>
                    <artifactId>hector-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

MahoutController

@RestController
@Slf4j
@RequestMapping("/")
public class MahoutController {

    @GetMapping
    public List<RecommendedItem> mahout(@RequestParam String userId,@RequestParam int type){
        //數據模型
        DataModel model = null;
        List<RecommendedItem> list = Lists.newArrayList();
        File cvsFile = null;
        try {
            long startTime = System.currentTimeMillis();
//            File bookCsvFile = ResourceUtils.getFile("classpath:csv/book_cvs_file.csv");
            if (type == 1) {
                cvsFile = new File("E:\\code\\python\\to_csv\\book_csv_file.csv");
            }else if(type == 2){
                cvsFile = new File("E:\\code\\python\\to_csv\\read_csv_file.csv");
            }else if(type == 3){
                cvsFile = new File("E:\\code\\python\\to_csv\\album_csv_file.csv");
            }
            model = new GenericBooleanPrefDataModel(
                    GenericBooleanPrefDataModel
                            .toDataMap(new FileDataModel(cvsFile)));
            ItemSimilarity item=new LogLikelihoodSimilarity(model);
            //物品推薦算法
            Recommender r=new GenericItemBasedRecommender(model,item);
             list = r.recommend(Long.parseLong(userId), 10);
            list.forEach(System.out::println);
            long endTime = System.currentTimeMillis();
            log.info((endTime-startTime) +"");
        } catch (IOException e) {
            e.printStackTrace();
        } catch (TasteException e) {
            e.printStackTrace();
        }

        return list;
    }
    @GetMapping("/user")
    public List<RecommendedItem> mahoutUser(@RequestParam String userId,@RequestParam int type){
        //數據模型
        DataModel model = null;
        List<RecommendedItem> list = Lists.newArrayList();
        File cvsFile = null;
        try {
            long startTime = System.currentTimeMillis();
//            File bookCsvFile = ResourceUtils.getFile("classpath:csv/book_cvs_file.csv");
            if (type == 1) {
                cvsFile = new File("E:\\code\\python\\to_csv\\book_csv_file.csv");
            }else if(type == 2){
                cvsFile = new File("E:\\code\\python\\to_csv\\read_csv_file.csv");
            }else if(type == 3){
                cvsFile = new File("E:\\code\\python\\to_csv\\album_csv_file.csv");
            }
            model = new FileDataModel(cvsFile);
            //用戶相識度算法
            UserSimilarity userSimilarity=new LogLikelihoodSimilarity(model);
            UserNeighborhood neighborhood = new NearestNUserNeighborhood(20,userSimilarity, model);
            Recommender r=new GenericUserBasedRecommender(model, neighborhood, userSimilarity);

            list = r.recommend(Long.parseLong(userId), 10);
            list.forEach(System.out::println);
            long endTime = System.currentTimeMillis();
            log.info((endTime-startTime) +"");
        } catch (IOException e) {
            e.printStackTrace();
        } catch (TasteException e) {
            e.printStackTrace();
        }
        return list;
    }
}

根據傳入user_id來計算出推薦的物品id

結果

測試基於用戶協同過濾和基於物品協同過濾輸出結果
image

380萬的數據計算一次需要4-6秒,如果實時解析文件生成推薦內容明顯會拖累接口相應速度

測試結果還有一點,因爲數據的問題csv只生成了用戶id與物品id的對應關係,並沒有生成用戶對物品的打分數據,在實際過程中可以通過最近收聽的數據庫記錄,計算用戶聽了改資源多長時間來判斷用戶對物品的喜好程度。
基於用戶協同過濾和基於物品協同過濾輸出的結果迥異,代碼中物品協同過濾算法在不同用戶id下返回的數據重複性很高,不大使用,而相比之下,代碼中用戶協同過濾算法在不同用戶id下返回數據看起來比較可靠。實際開發過程中也需要對實際的數據進行判斷,否則無法進行很好的推薦,反而會變成一個用戶體驗很差的功能。

小結

單機版的mahout在實際開發過程中無法勝任實時推薦,特別是在大數量的情況下。
可以考慮後臺啓動定時任務,每天/每週生成csv文件(python代碼中生成csv的代碼查數據庫很慢……生成了N個小時,java應該會好一些),再通過定時任務計算用戶並保存不同用戶的不同推薦內容(需要根據實際情況進行判斷,畢竟這個量也不小)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章