我是靠谱客的博主 灵巧果汁,最近开发中收集的这篇文章主要介绍TP5 实现基于标签简单的推荐算法,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

1、算法思想

1.1、理解算法过程
  • 我们在写算法的时候要先理解我们的对象和之间的关系,我这里举例供求信息用户设置标签,两者关系是,系统会根据用户设置的标签来匹配与其相似度较高的,同时用户发布的供求信息的标签也会影响系统推荐的供求信息,这里还需要涉及到权重问题

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

(2)还需要考虑以下三大方面的影响因素 :

  • 个人标签设置(A:时间衰减度,B:相似度计算)
  • 发布供求标签(C:时间衰减度,D:相似度计算,E:商机类型占比)
  • 其他因素(F:供求发布时间衰减度,G:移除自己发布的供求,H:企业认证/个人认证/VIP特权/权重等)

(3)以下是相关计算:

  • A/C/F:时间衰减度 = 更新时间戳 / 当前时间 * β(β为一个设置的稳定参数,根据数据分析去设置)
  • B/D:相似度计算如下:另外还有其他的 相似度计算方法

假设 A = 某条供求与用户标签的相似度
假设 B = 某条供求与用户发布供求的标签相似度
假设 C = 某条供求标签与用户标签交集总数
假设 D = 某条供求标签与用户标签并集总数
假设 X = 某条供求标签与用户发布供求的标签交集总数
假设 Y = 某条供求标签与用户发布供求的标签并集总数

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

  • E:用户发布商机类型占比是根据自己业务需求去加的,不是必须项。

假设A = 用户发布的出售类型商机数量
假设B = 用户发布的求购类型商机数量

用户发布出售类型商机占比 C = A / ( A + B )
用户发布求购类型商机占比 D = B / ( A + B )
那么用户需求则正好相反,推荐的出售商机占比为 D,推荐的求购商机占比为 C
另外如果用户未发不过商机和求购则按照 1:1
如果用户发布的全是求购则推给他的 出售:求购 = 9 : 1(这里的9:1自行设置)

  • G:将个人发布的供求 排除 在推荐列表中,供求数据采用 缓存存储
  • H:这里的其他参数也是需要平衡后才能加入进行相似度计算的。

(4)采用 自定义分页 筛选后再进行 数据库查询

1.2、实操分步解析

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

  • 保存的时候注意这里的域key是 对应供求的ID ,值则是 供求的数据 ,里面的field最好是用到的才存进去,不然数据量大的话取出来的速度也会降低,影响首页内容输出速率。

image.png

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

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

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

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

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

2、代码实现

2.1、获取推荐列表的方法(我是封装成服务类方法)
/**
* 推荐算法返回商机
* @param int $userId 用户ID
* @param int $page 页码
* @param int $pagesize 每页条数
* @return bool
* @throws thinkException
* @throws thinkdbexceptionDataNotFoundException
* @throws thinkdbexceptionModelNotFoundException
* @throws thinkexceptionDbException
*/
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', 'ui.update_time'])
->find()->toArray();
//查询当前用户发布的供求
$business = (new BusinessModel)->where('status', 1)
->where('user_id', $userId)
->field(['id', 'type', 'label_ids', 'update_time'])
->select();
if (!empty($business) && $user['p_name'] != null) {
$userBusinessLabel = []; //存放用户发布商机标签的数组(有重复数据,需要计算出现概率)
$sellCount = 0; //发布的出售数量
$buyCount = 0; //发布的求购数量
foreach ($business as $k => $v) {
//根据发布时间计算衰减度
$timeRate = 1;
$busTimeRate = strtotime($v['update_time']) / time() * $timeRate;
//商机类型:0=求购,1=出售
if ($v['type'] == 0) $buyCount += $busTimeRate;
if ($v['type'] == 1) $sellCount += $busTimeRate;
//把当前用户的供求给移除推荐列表
$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) {
//影响因素1:计算求购需求和出售需求占比
if ($sellCount != 0 && $buyCount != 0) {
//如果都占有则计算占比
$allNeedRate = bcadd($sellCount, $buyCount, 4);
$buyNeedRate = bcdiv($sellCount, $allNeedRate, 4);
$sellNeedRate = bcdiv($buyCount, $allNeedRate, 4);
} elseif ($buyCount == 0 && $sellCount == 0) {
//如果都为0时候则需要
$sellNeedRate = 0.5;
$buyNeedRate = 0.5;
} elseif ($sellCount == 0) {
//如果未发布过出售,只发布求购则推10%的求购单,90%的出售单
$buyNeedRate = 0.1;
$sellNeedRate = 0.9;
} else {
//如果未发布过求购,只发布出售则推90%的求购单,10%的出售单
$buyNeedRate = 0.9;
$sellNeedRate = 0.1;
}
//影响因素2:标签设置时间进行兴趣衰减
$timeRate2 = 1; //兴趣衰弱占比
$labelTimeRate = strtotime($user['update_time']) / time() * $timeRate2;
//影响因素3:商机发布的时间衰减度
$timeRate3 = 1; //兴趣衰弱占比
$busTimeRate = strtotime($val['update_time']) / time() * $timeRate3;
//商机类型:0=求购,1=出售
//最终权重 = (标签相似度 * 标签设置兴趣衰减度 * 占比) + (发布供求相似度 * 发布供求需求占比 * 占比)
if ($val['type'] == 0) $similarBusArr = $similarBusinessArr[$key] * $buyNeedRate;
if ($val['type'] == 1) $similarBusArr = $similarBusinessArr[$key] * $sellNeedRate;
$weigh[$key] = ($similarIndustryArr[$key] * $labelTimeRate * 0.35 + $similarBusArr * 0.65) * $busTimeRate;
}
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);
if (!empty($businessIds)) $list->whereIn('b.id', $businessIds)->order($exp);
$list = $list->field($field)->select();
} 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')
->page($page, $pagesize)
->select();
}
} 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')
->page($page, $pagesize)
->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; //转换成ID作为key,出现次数作为value 的一维数组(主要用于计算用户发布过的商机)
$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 ($total != 0) {
if ($countArr) {
//如果有则计算概率,其中一二级都会
foreach ($countArr as $k => $v)
if ($k == $val[0] || $k == $val[1]) $probability = $v / $total;
} else {
$probability = 1 / $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);
//--------------------------------------------------------------------------------------------------
  • 函数中传的参数必须是一维数组,其中

  • $userBusinessLabel 当前用户发布的供求标签一维数组(有重复数据,需要计算出现概率)
    image.png

  • $userLabel 用户自定义的标签,无重复数据
    image.png

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); //按相似度,最相似的排最前面
  • 其中我根据出售和求购的两种类型进行占比计算,我们还可以另外加其他的占比情况。
  • 我们在计算过程中需要考虑分母不能为0的情况。
  • 除此之外我们还可以用权重算法进行计算,需要根据业务和数据测试之后确定适合自己的算法。
  • 我们在计算过程中需要考虑到极限的情况,我们需要在这种情况下加上基数稳定推荐算法。

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

最后

以上就是灵巧果汁为你收集整理的TP5 实现基于标签简单的推荐算法的全部内容,希望文章能够帮你解决TP5 实现基于标签简单的推荐算法所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(30)

评论列表共有 0 条评论

立即
投稿
返回
顶部