利用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);

更多前沿技术,面试技巧,内推信息请扫码关注公众号“云计算平台技术”

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