TP5实现基于标签简单的推荐算法
1、算法思想什么是cookie
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。
relap另外如果⽤户未发不过商机和求购则按照 1:1
如果⽤户发布的全是求购则推给他的 出售:求购 = 9 : 1(这⾥的9:1⾃⾏设置)
G:将个⼈发布的供求 排除 在推荐列表中,供求数据采⽤ 缓存存储 ,
H:这⾥的其他参数也是需要平衡后才能加⼊进⾏相似度计算的。
(4)采⽤ ⾃定义分页 筛选后再进⾏ 数据库查询 。
1.2、实操分步解析
1、将数据库供求列表存储到 Redis 中,可以⽤ hash 存储,如下图:
保存的时候注意这⾥的域key是 对应供求的ID ,值则是 供求的数据 ,⾥⾯的field最好是⽤到的才存进去,不然数据量⼤的话取出来的速度也会降低,影响⾸页内容输出速率。
我们要注意的是每次 发布⼀条供求 或者 审核通过 时候将该条 保存到redis 中,这样就不⽤全部导⼊了
2、需要分 游客 和 ⽤户 两种登录情况的推荐。正常情况下,游客就按照数据库的排序就⾏了。
3、需要将 ⾃⼰发布的供求 移除 推荐列表
4、封装统⼀的 计算相似度的⽅法,这样便于⽤,同时要考虑 ⽤户未设置标签或未发布⼀条供求的情况
5、封装对应的 分页⽅法,我在下⾯也会提供我封装的⽅法。
2、代码实现
2.1、获取推荐列表的⽅法(我是封装成服务类⽅法)
/**
* 推荐算法返回商机
* @param int $urId ⽤户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($urId, $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.pe,b.desc,b.price,b.number,u.vi
p_pany,u.avatar,u.dit_scor e,b.city,b.label_lor,b.update_time,vl.ate_time';
if ($urId) {
//取出当前⽤户的⾏业标签
//取出当前⽤户的⾏业标签
$ur = (new UrModel)->alias('u')
->join('ur_industry ui', 'u.id = ui.ur_id', 'LEFT')
->where('u.id', $urId)
->field(['u.id', 'ui.p_name', 'ui.s_name', 'ui.update_time'])
->find()->toArray();
//查询当前⽤户发布的供求
$business = (new BusinessModel)->where('status', 1)
->where('ur_id', $urId)
->field(['id', 'type', 'label_ids', 'update_time'])
->lect();
if (!empty($business) && $ur['p_name'] != null) {
$urBusinessLabel = []; //存放⽤户发布商机标签的数组(有重复数据,需要计算出现概率)
$llCount = 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) $llCount += $busTimeRate;
//把当前⽤户的供求给移除推荐列表
$bId = $v['id'];
unt($businessArr[$bId]);
//合并数组,存放⽤户发布商机标签的数组
$val = explode('-', $v['label_ids']);
$urBusinessLabel = array_merge($urBusinessLabel, $val);
}
//----------------------------查出⽤户最近发布的供求的品类ID进⾏计算相似度---------------------------- //⽤于⽤户发布供求标签匹配的相似度
$similarBusinessArr = $this->calculateSimilar($labelArr, $urBusinessLabel);
//--------------------------------------------------------------------------------------------------
//------------------------------以下是求⾏业标签与发布的品类标签的相似度------------------------------ //拼接⽤户的⾏业标签名称去匹配品类ID数组
$sonNames = explode(',', $ur['s_name']);
$pNames = explode(',', $ur['p_name']);
$nameArr = array_merge($sonNames, $pNames);
$urLabel = (new TexturetypeModel)->whereIn('name', $nameArr)->column('id');
//⽤于⽤户标签匹配的相似度
$similarIndustryArr = $this->calculateSimilar($labelArr, $urLabel);中国旅游景点介绍
//--------------------------------------------------------------------------------------------------
//权重计算
$weigh = []; //⽤于存放推荐算法之后的权重数组
foreach ($businessArr as $key => $val) {
//影响因素1:计算求购需求和出售需求占⽐
if ($llCount != 0 && $buyCount != 0) {
//如果都占有则计算占⽐
$allNeedRate = bcadd($llCount, $buyCount, 4);
$buyNeedRate = bcdiv($llCount, $allNeedRate, 4);
$llNeedRate = bcdiv($buyCount, $allNeedRate, 4);
} elif ($buyCount == 0 && $llCount == 0) {
//如果都为0时候则需要
$llNeedRate = 0.5;
$buyNeedRate = 0.5;
} elif ($llCount == 0) {
//如果未发布过出售,只发布求购则推10%的求购单,90%的出售单
$buyNeedRate = 0.1;
$buyNeedRate = 0.1;
$llNeedRate = 0.9;我爱你英文怎么写
} el {
//如果未发布过求购,只发布出售则推90%的求购单,10%的出售单
$buyNeedRate = 0.9;
$llNeedRate = 0.1;
}
/
/影响因素2:标签设置时间进⾏兴趣衰减
$timeRate2 = 1; //兴趣衰弱占⽐
$labelTimeRate = strtotime($ur['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] * $llNeedRate;
$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('ur u', 'b.ur_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)->lect();
} el {
//当⽤户未发布商机和供求时候游览
$list = (new BusinessModel)->alias('b')
->join('ur u', 'b.ur_id = u.id', 'left')
zoo是什么意思
->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_enterpri_certification', 'DESC')
->order('u.is_certification', 'DESC')
-
>order('b.update_time', 'DESC')
->page($page, $pagesize)
->lect();
}
} el {
//游客游览时候
$list = (new BusinessModel)->alias('b')
->join('ur u', 'b.ur_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_enterpri_certification', 'DESC')
->order('u.is_certification', 'DESC')
ousia->order('b.update_time', 'DESC')
->page($page, $pagesize)
->lect();
进球英语}
$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']));
//删除不需要的字段
unt($list[$index]['city'], $list[$index]['color']);
}
return $list;
}
哆啦a梦全集
2.2、计算相似度的代码
/**
* ⽤于计算相似度(传⼊的必须是⼀位数组,value是对应的标签ID)
* @param array $data ⼤数组(⼤数组的key是供求ID)
* @param array $inArr ⼩数组
* @return array
*/
private function calculateSimilar($data, $inArr)
toy box
{
//计算$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) {
//公式:相似度 = 交集/并集 * 概率
$interct = array_interct($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;
} el {
$probability = 1 / $total;
}
}
$arr[$key] = (float)(count($interct) / count($union) * $probability);
}
return $arr;
}
2.3、封装的⾃定义分页