PHP开发PHP经验分享

TP5 实现基于标签简单的推荐算法

2020-04-28  本文已影响0人  华仔233

1、算法思想

1.1、理解算法过程

1、我们应该采用什么计算方式来计算,我这里采用简单 交集 / 并集 计算相似度的计算方法。

2、还需要考虑 个人设置标签发布供求标签占比 以及 发布供求标签出现次数

3、另外根据业务需求,可以考虑 出售求购 推的占比,时间 先后,权重 高低 等。

4、将个人发布的供求 排除 在推荐列表中,供求数据采用 缓存存储

5、采用 分页 筛选后再进行 数据库查询

6、相似度计算的方法还有:https://www.cnblogs.com/chenxiangzhen/p/10648503.html

假设 A = 某条供求与用户标签的相似度
假设 B = 某条供求与用户发布供求的标签相似度
假设 C = 某条供求标签与用户标签交集总数
假设 D = 某条供求标签与用户标签并集总数
假设 X = 某条供求标签与用户发布供求的标签交集总数
假设 Y = 某条供求标签与用户发布供求的标签并集总数
:
公式1:某条供求的相似度 = A * 占比 + B * 占比
公式2:A = C / D * 出现概率(默认是1,因为用户无重复标签)
公式3:B = X / Y * 出现概率

1.2、分步解析

1、将数据库供求列表存储到 Redis 中,可以用 hash 存储,如下图:

image.png

我们要注意的是每次 发布一条供求 或者 审核通过 时候将该条 保存到redis 中,这样就不用全部导入了

2、需要分 游客用户 两种登录情况的推荐。正常情况下,游客就按照数据库的排序就行了。

3、需要将 自己发布的供求 移除 推荐列表

4、封装统一的 计算相似度的方法,这样便于用,同时要考虑 用户未设置标签或未发布一条供求的情况

5、封装对应的 分页方法,我在下面也会提供我封装的方法。

2、代码实现

2.1、获取推荐列表的方法(我是封装成服务类方法)
/**
 * 推荐算法返回商机
 * @param int $userId 用户ID
 * @param int $page 页码
 * @param int $pagesize 每页条数
 * @return bool
 * @throws \think\Exception
 * @throws \think\db\exception\DataNotFoundException
 * @throws \think\db\exception\ModelNotFoundException
 * @throws \think\exception\DbException
 */
public function recommendBusiness($userId, $page = 1, $pagesize = 10)
{
    //从缓存中取出所有的文章信息
    $redis = RedisService::connect();
    $redisKey = RedisService::SU_CACHE_BUSINESS_TAGS;
    $data = $redis->hgetall($redisKey);

    //注意保存的数组中需要保存原始的key,因为该key是供求ID
    $businessArr = []; //存放供求列表内容
    $labelArr = []; //存放供求列表标签
    foreach ($data as $key => $val) {
        $val = json_decode($val, 1);
        $businessArr[$key] = $val;
        $labelArr[$key] = explode('-', $val['label_ids']);
    }

    //组建查所有的商机的sql
    $field = 'b.id,substring_index(b.images,\',\',1) as image,b.purpose,b.type,b.desc,b.price,b.number,u.vip_id,u.company,u.avatar,u.nickname,u.credit_score,b.city,b.label_name,b.color,b.update_time,vl.icon,b.create_time';

    if ($userId) {

        //取出当前用户的行业标签
        $user = (new UserModel)->alias('u')
            ->join('user_industry ui', 'u.id = ui.user_id', 'LEFT')
            ->where('u.id', $userId)
            ->field(['u.id', 'ui.p_name', 'ui.s_name'])
            ->find()->toArray();

        //查询当前用户发布的供求
        $business = (new BusinessModel)->where('status', 1)
            ->where('user_id', $userId)
            ->field(['id', 'type', 'label_ids'])
            ->select();
        $userBusinessLabel = []; //存放用户发布商机标签的数组(有重复数据,需要计算出现概率)
        $sellCount = 0; //发布的出售数量
        $buyCount = 0; //发布的求购数量
        foreach ($business as $k => $v) {

            //商机类型:0=求购,1=出售
            if ($v['type'] == 0) $buyCount += 1;
            if ($v['type'] == 1) $sellCount += 1;

            //把当前用户的供求给移除推荐列表
            $bId = $v['id'];
            unset($businessArr[$bId]);

            //合并数组,存放用户发布商机标签的数组
            $val = explode('-', $v['label_ids']);
            $userBusinessLabel = array_merge($userBusinessLabel, $val);

        }

        //----------------------------查出用户最近发布的供求的品类ID进行计算相似度----------------------------
        //用于用户发布供求标签匹配的相似度
        $similarBusinessArr = $this->calculateSimilar($labelArr, $userBusinessLabel);
        //--------------------------------------------------------------------------------------------------


        //------------------------------以下是求行业标签与发布的品类标签的相似度------------------------------
        //拼接用户的行业标签名称去匹配品类ID数组
        $sonNames = explode(',', $user['s_name']);
        $pNames = explode(',', $user['p_name']);
        $nameArr = array_merge($sonNames, $pNames);
        $userLabel = (new TexturetypeModel)->whereIn('name', $nameArr)->column('id');
        //用于用户标签匹配的相似度
        $similarIndustryArr = $this->calculateSimilar($labelArr, $userLabel);
        //--------------------------------------------------------------------------------------------------

        //权重计算
        $weigh = []; //用于存放推荐算法之后的权重数组
        foreach ($businessArr as $key => $val) {
            //计算求购需求和出售需求占比
            if ($buyCount == 0 && $sellCount == 0) {
                $sellNeedRate = 0.5;
                $buyNeedRate = 0.5;
            } else {
                $buyNeedRate = $sellCount / ($sellCount + $buyCount);
                $sellNeedRate = $buyCount / ($sellCount + $buyCount);
                //如果是比例是0和1的话需要对应加10%和90%的基数
                if ($buyNeedRate == 0) $buyNeedRate = 0.1;
                if ($buyNeedRate == 1) $buyNeedRate = 0.9;
                if ($sellNeedRate == 0) $sellNeedRate = 0.1;
                if ($sellNeedRate == 1) $sellNeedRate = 0.9;
            }
            $similar = $similarIndustryArr[$key] * 0.25 + $similarBusinessArr[$key] * 0.75;
            //商机类型:0=求购,1=出售
            if ($val['type'] == 0) $weigh[$key] = $similar * $buyNeedRate;
            if ($val['type'] == 1) $weigh[$key] = $similar * $sellNeedRate;
        }

        arsort($weigh); //按相似度,最相似的排最前面

        arrayToPage($weigh, $page, $pagesize, 0, true); //进行自定义分页处理
        $businessIds = array_keys($weigh); //取出所有的键值
        $exp = new Expression('field(b.id,' . implode(',', $businessIds) . ')'); //用于排序

        $list = (new BusinessModel)->alias('b')
            ->join('user u', 'b.user_id = u.id', 'left')
            ->join('sulink_vip_level vl', 'vl.id = u.vip_id', 'left')
            ->where('b.status', 1)
            ->whereIn('b.id', $businessIds)
            ->order($exp)
            ->field($field);

    } else {
        //游客游览时候
        $list = (new BusinessModel)->alias('b')
            ->join('user u', 'b.user_id = u.id', 'left')
            ->join('sulink_vip_level vl', 'vl.id = u.vip_id', 'left')
            ->where('b.status', 1)
            ->field($field)
            ->order('vl.weigh', 'DESC')
            ->order('b.weigh', 'DESC')
            ->order('u.is_enterprise_certification', 'DESC')
            ->order('u.is_certification', 'DESC')
            ->order('b.update_time', 'DESC');
    }

    $list = $list->select();
    $list = collection($list)->toArray();

    //分隔符
    foreach ($list as $index => &$item) {
        $city = explode('/', $item['city']);
        $city = mb_substr($city[0], 0, 2, 'UTF-8');
        $item['label_name'] = explode(' - ', $item['label_name']);
        array_unshift($item['label_name'], $city);
        if ($item['color'] ?? null) {
            $item['label_name'][] = $item['color'];
        }

        //多少时间前
        $list[$index]['update_time'] = timeToBefore(strtotime($list[$index]['update_time']));

        //删除不需要的字段
        unset($list[$index]['city'], $list[$index]['color']);
    }

    return $list;
}
2.2、计算相似度的代码
/**
 * 用于计算相似度(传入的必须是一位数组,value是对应的标签ID)
 * @param array $data 大数组(大数组的key是供求ID)
 * @param array $inArr 小数组
 * @return array
 */
private function calculateSimilar($data, $inArr)
{
    //计算$inArr中标签出现概率
    $total = count($inArr);
    $countArr = $total != 0 ? array_count_values($inArr) : 0;
    $probability = $total != 0 ? 1 / $total : 1;//默认概率

    $arr = []; //相似度数组
    foreach ($data as $key => $val) {

        $intersect = array_intersect($val, $inArr); //计算交集
        $union = array_unique(array_merge($val, $inArr)); //计算并集
        if ($countArr && $total != 0) {
            foreach ($countArr as $k => $v) {
                if ($k == $val) $probability = $v / $total; //如果有则计算概率
            }
        }
        $arr[$key] = (float)(count($intersect) / count($union) * $probability);
    }
    return $arr;
}
2.3、封装的自定义分页
/**
 * 将多维数组继续分页,自定义分页效果
 * @param array &$array 数组
 * @param int $page 当前页数
 * @param int $limit 每页页数
 * @param int $order 0-不变 1-反序
 * @param bool $preserveKey true - 保留键名  false - 默认。重置键名
 */
function arrayToPage(Array &$array, int $page = 1, int $limit = 20, int $order = 0,bool $preserveKey = false)
{
    $start = ($page - 1) * $limit; //计算每次分页的开始位置

    //反序
    if ($order == 1) $array = array_reverse($array);

    $array = array_slice($array, $start, $limit,$preserveKey);
}

3、注意要点,解释上面代码中主要部分

3.1、其中主要的计算部分如下

//----------------------------查出用户最近发布的供求的品类ID进行计算相似度----------------------------
//用于用户发布供求标签匹配的相似度
$similarBusinessArr = $this->calculateSimilar($labelArr, $userBusinessLabel);
//--------------------------------------------------------------------------------------------------


//------------------------------以下是求行业标签与发布的品类标签的相似度------------------------------
//拼接用户的行业标签名称去匹配品类ID数组
$sonNames = explode(',', $user['s_name']);
$pNames = explode(',', $user['p_name']);
$nameArr = array_merge($sonNames, $pNames);
$userLabel = (new TexturetypeModel)->whereIn('name', $nameArr)->column('id');
//用于用户标签匹配的相似度
$similarIndustryArr = $this->calculateSimilar($labelArr, $userLabel);
//--------------------------------------------------------------------------------------------------

3.2、最后总的计算并排序
//权重计算
$weigh = []; //用于存放推荐算法之后的权重数组
foreach ($businessArr as $key => $val) {
    //计算求购需求和出售需求占比
    if ($buyCount == 0 && $sellCount == 0) {
        $sellNeedRate = 0.5;
        $buyNeedRate = 0.5;
    } else {
        $buyNeedRate = $sellCount / ($sellCount + $buyCount);
        $sellNeedRate = $buyCount / ($sellCount + $buyCount);
        //如果是比例是0和1的话需要对应加10%和90%的基数
        if ($buyNeedRate == 0) $buyNeedRate = 0.1;
        if ($buyNeedRate == 1) $buyNeedRate = 0.9;
        if ($sellNeedRate == 0) $sellNeedRate = 0.1;
        if ($sellNeedRate == 1) $sellNeedRate = 0.9;
    }

    $similar = $similarIndustryArr[$key] * 0.25 + $similarBusinessArr[$key] * 0.75;
    //商机类型:0=求购,1=出售
    if ($val['type'] == 0) $weigh[$key] = $similar * $buyNeedRate;
    if ($val['type'] == 1) $weigh[$key] = $similar * $sellNeedRate;
}

arsort($weigh); //按相似度,最相似的排最前面

PS : 上面是我初次接触使用简单的推荐算法,如果有什么不对的地方请指教,我还会一直完善。由于数据的不足,还没有进入测试阶段,所以我还是需要去不断改进。

上一篇 下一篇

猜你喜欢

热点阅读