利用Java+機器學習開發一套推薦系統 ALS,LR算法原理與實戰

本篇文章會通過 推薦系統介紹 算法原理講解 推薦系統架構 Java代碼實戰四部分,讓您對推薦系統有一定了解。

1 推薦系統介紹

一個基本的推薦系統需要包含召回,排序,根據業務規則重排三個部分。

召回

召回指在海量的待推薦數據中過濾出較爲符合對應用戶及場景的推薦數據候選集。

  • 在線召回:實時運行召回算法,從海量數據中拿出對應的候選數據集

  • 離線召回:離線運行召回算法,從海量數據中拿出對應的候選數據集並預存入某種存儲中,供在線系統直接拿取對應場景的召回數據

由於召回是從海量數據中過濾出一部分數據,運算時效往往很長,因此一般都採用離線召回算法。

排序

在召回出的候選推薦數據集內利用算法給每個結果集打分,最終排序出對應候選集內top n的數據並返回外部系統
排序內分爲:

  • 在線排序:實時運行排序算法,給對應召回的數據集打分並輸出
  • 離線排序:離線運行排序算法,算好對應的數據打分

由於排序往往結合了特徵工程,需要根據當前用戶和場景的特徵作爲輸入做打分,不太現實離線的給所有用戶的所有特徵打分後預先存在某個存儲
(數據量太大,一般爲用戶數量*商品數量)。因此一般都採用在線排序,由於召回已經過濾了絕大部分數據,因此實時的打分性能是可以接受的。

常用召回算法

  • 協同過濾:基於用戶和物品的交易或瀏覽行爲組成矩陣後計算用戶或物品相似度,再推薦不同的商品給對應的用戶
  • ALS:基於用戶和物品的交易或瀏覽行爲組成矩陣後通過最小二乘法預測矩陣中未有行爲的用戶和物品的評分,組成推薦結果
  • 用戶畫像:通過商品畫像推薦給指定帶有指定標籤的用戶

常用排序算法

  • LR:邏輯迴歸,通過特徵組成多緯座標系,計算一組可能的特徵下的輸出結果是0還是1的概率
  • GBDT:決策樹,通過屬性的結構在多緯度的特徵下做選擇,最終達到一個最有可能的輸出結果

2 算法原理講解

ALS交替最小二乘

首先了解他是做什麼的

假如你的老闆給你如下一張數據庫表

其中y軸代表四個用戶,x軸代表3個商品。數據代表某用戶對某件商品的點擊次數(喜歡程度),如user1對商品product1有3個點擊數。

老闆讓你預測出user1對product2,product3的喜歡程度。

我們就要尋找這些數據的規律,會不會聯想到如果存在兩個矩陣相乘(user矩陣乘product矩陣),恰好會得到上圖中的矩陣呢?

但user,product都是一維的,怎麼讓他變成二維的呢?我們要分別爲user,product虛擬出幾個屬性(隱式特徵)

f1-f5代表用戶5個隱式特徵(如:性別,性格,身高,年齡等)

f1-f5代表商品5個隱式特徵(如:價格,分類等)

假設用上圖兩個矩陣相乘得到一個新的完整矩陣

新的完整矩陣結果(紅色)與老闆給的矩陣所有數據基本對應(如user1 與product1的對應座標值原值3.0,預測值3.1),也就意味着新矩陣的其他屬性也基本正確。

因此可以得出每個用戶對每個商品的喜歡程度,通過對喜歡程度進行排序,得到分值高的就是要推薦的商品。

不過問題來了,上面說的(隱式特徵)的值,是我們假想的,我們根本不知道那些值是什麼呀。這就需要機器不斷嘗試,使兩個矩陣相乘的結果儘可能的接近真實值,最終得到不存在的隱式特徵。

線性迴歸與邏輯迴歸

機器學習有兩個經典問題:迴歸與分類問題。
股票,房價走勢圖是一個x對應一個y可以理解爲迴歸問題,而根據身高體重相關預測人的性別則屬於分類問題。分類問題的y值更離散化一些,同一個y值對應很多x,這些x具有一定範圍的,所以分類問更多的是一定區域內的一些x,對應着一個y。

線性迴歸:

利用一個直線較爲精確的描述數據之間的關係,這樣當出現新的數據時候,能夠預測一個簡單的值。

舉例:根據學習時長,判斷學習成績好壞,x軸代表學習時長,y軸代表成績好壞。

可以發現這些點連接起來大概類似於一條直線,滿足y=kx+b。

如下圖,k和b究竟是多少,我們根本不知道,因此會有無數線,然而只有一條是真正的那條線。

程序要想尋找出真正的k與b。就要做無數次的嘗試,這是學習的一種過程,那他怎麼知道哪條直線纔是想要的結果呢?評判標準又是什麼?我們接着看下面這張圖。

只需不停地拿不同的k與b做嘗試,將每個真實值與預測值的差值相加,尋找出一個最小差值即可。

然而真實場景不可能只考慮一個單純的因素。比如學習時長並不是成績的唯一因素,還與睡眠時間等等相關,此時就用到了多維線性迴歸。

此時的預測函數如下

損失函數如下

利用梯度下降求偏導數、正規方程解等方法找到使損失函數達到最小的方式。

邏輯迴歸

邏輯迴歸雖然名字叫回歸,但解決的是二分類問題。

假設有兩個分類,男生和女生分別代表0和1。如果讓你選擇一個函數,能夠抽象成分類問題,使值介於0和1之間,你會選擇哪個呢?顯然上面的線性函數肯定不能滿足的。

此時就用到了Sigmoid函數,他是一種常見的S函數是。

可以看出,sigmoid函數連續,光滑,嚴格單調,以(0,0.5)中心對稱,是一個非常良好的閾值函數。

當x趨近負無窮時,y趨近於0;趨近於正無窮時,y趨近於1;x=0時,y=0.5。當然,在x超出[-6,6]的範圍後,函數值基本上沒有變化,值非常接近,在應用中一般不考慮。

Sigmoid函數的值域範圍限制在(0,1)之間,我們知道[0,1]與概率值的範圍是相對應的,這樣sigmoid函數就能與一個概率分佈聯繫起來了。

把線性迴歸的值映射到s函數,如果映射在s函數中的值>0.5說明正樣本概率大。

求邏輯迴歸和線性迴歸方法一樣,要找到一個預測函數.

構造預測函數

構造損失函數(極大似然推導出)

差值最小化(梯度下降)

3 推薦系統架構

知道了大概原理,開發推薦系統就順理成章了,我們把該流程走一遍。

首先,我們以ALS爲例,實現粗排

  • 大數據平臺蒐集前端埋點數據或Log日誌數據,進行數據清洗,得到指定結構的數據,並對其進行據存儲。

數據示例

用戶ID 商品ID 點擊次數
1 1
1 2 10
1 3
2 1
2 2 10
2 3 10
  • 利用ALS算法對大數據平臺蒐集的數據進行訓練,得到粗排模型,並對模型進行存儲(Redis,大數據平臺,甚至文件均可)。

  • 讀取模型到內存,模型輸入用戶Id,預測出的召回的商品ID,並存儲到數據庫。

  • C端頁面發送請求或與後端保持長連接獲取數據庫中的召回商品

其次我們以LR算法爲例,實現精排

  • 大數據平臺蒐集前端埋點數據或Log日誌數據,進行數據清洗,得到指定結構的數據,進行特徵提取,並對其進行數據存儲。

特徵提取大概分爲兩類:離散特徵處理,連續特徵處理。

離散特徵可以用一個數字表示(0或1)
性別:

性別
1
0

是否購買:

是否購買
1
0

連續特徵處理:用多個數字表示(0000)
年齡:

年齡
0-18歲 0000
18-30歲 0100

分類:

商品 分類
手機 0000 電子
電腦 0000 電子
女上衣 0001 女裝

如“1 0100 0000 1”代表18-30的男生購買了電子類型產品

  • 利用LR算法對大數據平臺蒐集的數據進行訓練,得到精排模型,並對模型進行存儲。

  • C端頁面發送請求或與後端保持長連接首先取數據庫中粗排得到的召回商品,此時並不是直接響應給前端,而是獲得特徵,輸入到精排模型,獲取精排結果(介於0到1之間),並對排序結果排序後進行返回。

4 代碼實戰

 需要引入spark-mllib類庫依賴

ALS

  • input文件內容(模擬大數據平臺的數據)
1,1,1
2,2,2
...
  • 粗排Java訓練代碼

import org.apache.spark.api.java.function.Function;
import org.apache.commons.lang3.StringUtils;
import org.apache.spark.api.java.JavaRDD;
import org.apache.spark.api.java.function.ForeachPartitionFunction;
import org.apache.spark.ml.classification.LogisticRegression;
import org.apache.spark.ml.classification.LogisticRegressionModel;
import org.apache.spark.ml.evaluation.MulticlassClassificationEvaluator;
import org.apache.spark.ml.evaluation.RegressionEvaluator;
import org.apache.spark.ml.recommendation.ALS;
import org.apache.spark.ml.recommendation.ALSModel;
import org.apache.spark.sql.Dataset;
import org.apache.spark.sql.Row;
import org.apache.spark.sql.RowFactory;
import org.apache.spark.sql.SparkSession;
import org.apache.spark.sql.catalyst.expressions.GenericRowWithSchema;
import org.apache.spark.sql.types.DataTypes;
import org.apache.spark.sql.types.Metadata;
import org.apache.spark.sql.types.StructField;
import org.apache.spark.sql.types.StructType;
import org.mortbay.log.Log;
import java.io.Serializable;
import java.util.*;
import org.apache.spark.ml.linalg.VectorUDT;
import org.apache.spark.ml.linalg.Vectors;

/**
 * 描述:
 *
 * @author wangpp-b
 * @create 2020-05-08 16:04
 */
public class TrainUtil {

    /**
     * @param textFilePath 輸入文件目錄
     * @param number       給number個用戶做離線的召回結果預測
     * @param trainId      訓練任務Id
     * @param trainHost    訓練任務數量
     * @return 模型訓練誤差
     */
    public static double alsTrain(String textFilePath, Integer number, Long trainId, String trainHost) throws Exception {
        //初始化spark運行環境
        SparkSession spark = SparkSession.builder().master(trainHost).appName("appName").getOrCreate();
        JavaRDD<String> ioFile = spark.read().textFile(textFilePath).toJavaRDD();
        JavaRDD<Rating> ratingJavaRDD = ioFile.map(new Function<String, Rating>() {
            @Override
            public Rating call(String s) throws Exception {
                return Rating.parseRating(s);
            }
        });
        //轉化爲spark中通用的數據結構,以Rating爲結構的數據表
        Dataset<Row> rating = spark.createDataFrame(ratingJavaRDD, Rating.class);
        //將所有的rating數據分詞8 2份 80%做訓練 20%做測試
        Dataset<Row>[] splits = rating.randomSplit(new double[]{0.8, 0.2});
        Dataset<Row> trainingData = splits[0];
        Dataset<Row> testingData = splits[1];
        //過擬合:增大數據規模,減少RANK,增大正則化的係數
        //欠擬合:增加rank,減少正則化係數
        ALS als = new ALS().setMaxIter(10).setRank(5).setRegParam(0.01).
                setUserCol("userId").setItemCol("shopId").setRatingCol("rating");
        //模型訓練的過程
        ALSModel alsModel = als.fit(trainingData);
        //模型評測
        Dataset<Row> predictions = alsModel.transform(testingData);
        //rmse 均方根誤差,預測值與真實值的偏差的平方除以觀測次數,開個根號
        RegressionEvaluator evaluator = new RegressionEvaluator().setMetricName("rmse")
                .setLabelCol("rating").setPredictionCol("prediction");
        double rmse = evaluator.evaluate(predictions);
        //給number個用戶做離線的召回結果預測
        Dataset<Row> users = rating.select(alsModel.getUserCol()).distinct().limit(number);
        Dataset<Row> userRecs = alsModel.recommendForUserSubset(users, 20);
        //分片方式處理
        userRecs.foreachPartition(new ForeachPartitionFunction<Row>() {
            @Override
            public void call(Iterator<Row> t) throws Exception {
                List<Map<String, Object>> data = new ArrayList<Map<String, Object>>();
                t.forEachRemaining(action -> {
                    int userId = action.getInt(0);
                    List<GenericRowWithSchema> recommendationList = action.getList(1);
                    List<Integer> shopIdList = new ArrayList<Integer>();
                    recommendationList.forEach(row -> {
                        Integer shopId = row.getInt(0);
                        shopIdList.add(shopId);
                    });
                    String recommendData = StringUtils.join(shopIdList, ",");
                    Map<String, Object> map = new HashMap<String, Object>();
                    map.put("userId", userId);
                    map.put("recommend", recommendData);
                    data.add(map);
                });
                data.forEach(stringObjectMap -> {
                    // 召回結果
                    Log.info("用戶Id:" + stringObjectMap.get("userId") + " 推薦商品Id列表:" + stringObjectMap.get("recommend") + " 訓練任務Id:" + trainId);
                });
            }
        });
        return rmse;
    }

    //als矩陣中的元素
    public static class Rating implements Serializable {
        private int userId;
        private int shopId;
        private int rating;

        static Rating parseRating(String str) {
            str = str.replace("\"", "");
            String[] strArr = str.split(",");
            int userId = Integer.parseInt(strArr[0]);
            int shopId = Integer.parseInt(strArr[1]);
            int rating = Integer.parseInt(strArr[2]);
            return new Rating(userId, shopId, rating);
        }

        Rating(int userId, int shopId, int rating) {
            this.userId = userId;
            this.shopId = shopId;
            this.rating = rating;
        }

        public int getUserId() {
            return userId;
        }

        public int getShopId() {
            return shopId;
        }

        public int getRating() {
            return rating;
        }
    }
}
  • 調用
TrainUtil.ALSTrain(".\\input"100, System.currentTimeMillis()"local");

LR

  • input文件準備(象徵性的用四個數字表示(3個特徵向量,最後一個代表是否點擊))
"1","1","1","1"
"1","1","1","1"
"1","0","1","1"
"1","0","1","0"
...
  • 精排Java訓練代碼
      public static LogisticRegressionModel lrTrain(String textFilePath, String trainHost) {
        //初始化spark運行環境
        SparkSession spark = SparkSession.builder().master(trainHost).appName("ffs").getOrCreate();
        //加載特徵及label訓練文件
        JavaRDD<String> csvFile = spark.read().textFile(textFilePath).toJavaRDD();
        //做轉化
        JavaRDD<Row> rowJavaRDD = csvFile.map(new Function<String, Row>() {
            @Override
            public Row call(String v1) throws Exception {
                v1 = v1.replace("\"", "");
                String[] strArr = v1.split(",");
                return RowFactory.create(new Double(strArr[3]), Vectors.dense(Double.valueOf(strArr[0]), Double.valueOf(strArr[1]), Double.valueOf(strArr[2])));
            }
        });
        StructType schema = new StructType(
                new StructField[]{
                        new StructField("label", DataTypes.DoubleType, false, Metadata.empty()),
                        new StructField("features", new VectorUDT(), false, Metadata.empty())
                }
        );
        Dataset<Row> data = spark.createDataFrame(rowJavaRDD, schema);
        //分開訓練和測試集
        Dataset<Row>[] dataArr = data.randomSplit(new double[]{0.8, 0.2});
        Dataset<Row> trainData = dataArr[0];
        Dataset<Row> testData = dataArr[1];
        LogisticRegression lr = new LogisticRegression().
                setMaxIter(10).setRegParam(0.3).setElasticNetParam(0.8).setFamily("multinomial");
        LogisticRegressionModel lrModel = lr.fit(trainData);
        //測試評估
        Dataset<Row> predictions = lrModel.transform(testData);
        //評價指標
        MulticlassClassificationEvaluator evaluator = new MulticlassClassificationEvaluator();
        double accuracy = evaluator.setMetricName("accuracy").evaluate(predictions);
        Log.info(String.valueOf(accuracy));
        return lrModel;
    }
  • 調用模型進行預測
Vector v = Vectors.dense(000);
Vector resultVector = logisticRegressionModel.predictProbability(v);

更多前沿技術,面試技巧,內推信息請掃碼關注公衆號“雲計算平臺技術”

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