TP5 实现基于标签简单的推荐算法
1、算法思想
1.1、理解算法過程
- 我們?cè)趯懰惴ǖ臅r(shí)候要先理解我們的對(duì)象和之間的關(guān)系,我這里舉例供求信息和用戶設(shè)置標(biāo)簽,兩者關(guān)系是,系統(tǒng)會(huì)根據(jù)用戶設(shè)置的標(biāo)簽來匹配與其相似度較高的,同時(shí)用戶發(fā)布的供求信息的標(biāo)簽也會(huì)影響系統(tǒng)推薦的供求信息,這里還需要涉及到權(quán)重問題。
(1)我們應(yīng)該采用什么計(jì)算方式來計(jì)算,我這里采用簡(jiǎn)單 交集 / 并集 計(jì)算相似度的計(jì)算方法。
(2)還需要考慮以下三大方面的影響因素 :
- 個(gè)人標(biāo)簽設(shè)置(A:時(shí)間衰減度,B:相似度計(jì)算)
- 發(fā)布供求標(biāo)簽(C:時(shí)間衰減度,D:相似度計(jì)算,E:商機(jī)類型占比)
- 其他因素(F:供求發(fā)布時(shí)間衰減度,G:移除自己發(fā)布的供求,H:企業(yè)認(rèn)證/個(gè)人認(rèn)證/VIP特權(quán)/權(quán)重等)
(3)以下是相關(guān)計(jì)算:
- A/C/F:時(shí)間衰減度 = 更新時(shí)間戳 / 當(dāng)前時(shí)間 * β(β為一個(gè)設(shè)置的穩(wěn)定參數(shù),根據(jù)數(shù)據(jù)分析去設(shè)置)
- B/D:相似度計(jì)算如下:另外還有其他的 相似度計(jì)算方法
假設(shè) A = 某條供求與用戶標(biāo)簽的相似度
假設(shè) B = 某條供求與用戶發(fā)布供求的標(biāo)簽相似度
假設(shè) C = 某條供求標(biāo)簽與用戶標(biāo)簽交集總數(shù)
假設(shè) D = 某條供求標(biāo)簽與用戶標(biāo)簽并集總數(shù)
假設(shè) X = 某條供求標(biāo)簽與用戶發(fā)布供求的標(biāo)簽交集總數(shù)
假設(shè) Y = 某條供求標(biāo)簽與用戶發(fā)布供求的標(biāo)簽并集總數(shù)
公式1:某條供求的相似度 = A * 占比 + B * 占比
公式2:A = C / D * 出現(xiàn)概率(默認(rèn)是1,因?yàn)橛脩魺o重復(fù)標(biāo)簽)
公式3:B = X / Y * 出現(xiàn)概率
- E:用戶發(fā)布商機(jī)類型占比是根據(jù)自己業(yè)務(wù)需求去加的,不是必須項(xiàng)。
假設(shè)A = 用戶發(fā)布的出售類型商機(jī)數(shù)量
假設(shè)B = 用戶發(fā)布的求購類型商機(jī)數(shù)量
用戶發(fā)布出售類型商機(jī)占比 C = A / ( A + B )
用戶發(fā)布求購類型商機(jī)占比 D = B / ( A + B )
那么用戶需求則正好相反,推薦的出售商機(jī)占比為 D,推薦的求購商機(jī)占比為 C。
另外如果用戶未發(fā)不過商機(jī)和求購則按照 1:1
如果用戶發(fā)布的全是求購則推給他的 出售:求購 = 9 : 1(這里的9:1自行設(shè)置)
- G:將個(gè)人發(fā)布的供求 排除 在推薦列表中,供求數(shù)據(jù)采用 緩存存儲(chǔ) ,
- H:這里的其他參數(shù)也是需要平衡后才能加入進(jìn)行相似度計(jì)算的。
(4)采用 自定義分頁 篩選后再進(jìn)行 數(shù)據(jù)庫查詢 。
1.2、實(shí)操分步解析
1、將數(shù)據(jù)庫供求列表存儲(chǔ)到 Redis 中,可以用 hash 存儲(chǔ),如下圖:
- 保存的時(shí)候注意這里的域key是 對(duì)應(yīng)供求的ID ,值則是 供求的數(shù)據(jù) ,里面的field最好是用到的才存進(jìn)去,不然數(shù)據(jù)量大的話取出來的速度也會(huì)降低,影響首頁內(nèi)容輸出速率。
我們要注意的是每次 發(fā)布一條供求 或者 審核通過 時(shí)候?qū)⒃摋l 保存到redis 中,這樣就不用全部導(dǎo)入了
2、需要分 游客 和 用戶 兩種登錄情況的推薦。正常情況下,游客就按照數(shù)據(jù)庫的排序就行了。
3、需要將 自己發(fā)布的供求 移除 推薦列表
4、封裝統(tǒng)一的 計(jì)算相似度的方法,這樣便于用,同時(shí)要考慮 用戶未設(shè)置標(biāo)簽或未發(fā)布一條供求的情況
5、封裝對(duì)應(yīng)的 分頁方法,我在下面也會(huì)提供我封裝的方法。
2、代碼實(shí)現(xiàn)
2.1、獲取推薦列表的方法(我是封裝成服務(wù)類方法)
/*** 推薦算法返回商機(jī)* @param int $userId 用戶ID* @param int $page 頁碼* @param int $pagesize 每頁條數(shù)* @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);//注意保存的數(shù)組中需要保存原始的key,因?yàn)樵搆ey是供求ID$businessArr = []; //存放供求列表內(nèi)容$labelArr = []; //存放供求列表標(biāo)簽foreach ($data as $key => $val) {$val = json_decode($val, 1);$businessArr[$key] = $val;$labelArr[$key] = explode('-', $val['label_ids']);}//組建查所有的商機(jī)的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) {//取出當(dāng)前用戶的行業(yè)標(biāo)簽$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();//查詢當(dāng)前用戶發(fā)布的供求$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 = []; //存放用戶發(fā)布商機(jī)標(biāo)簽的數(shù)組(有重復(fù)數(shù)據(jù),需要計(jì)算出現(xiàn)概率)$sellCount = 0; //發(fā)布的出售數(shù)量$buyCount = 0; //發(fā)布的求購數(shù)量foreach ($business as $k => $v) {//根據(jù)發(fā)布時(shí)間計(jì)算衰減度$timeRate = 1;$busTimeRate = strtotime($v['update_time']) / time() * $timeRate;//商機(jī)類型:0=求購,1=出售if ($v['type'] == 0) $buyCount += $busTimeRate;if ($v['type'] == 1) $sellCount += $busTimeRate;//把當(dāng)前用戶的供求給移除推薦列表$bId = $v['id'];unset($businessArr[$bId]);//合并數(shù)組,存放用戶發(fā)布商機(jī)標(biāo)簽的數(shù)組$val = explode('-', $v['label_ids']);$userBusinessLabel = array_merge($userBusinessLabel, $val);}//----------------------------查出用戶最近發(fā)布的供求的品類ID進(jìn)行計(jì)算相似度----------------------------//用于用戶發(fā)布供求標(biāo)簽匹配的相似度$similarBusinessArr = $this->calculateSimilar($labelArr, $userBusinessLabel);//--------------------------------------------------------------------------------------------------//------------------------------以下是求行業(yè)標(biāo)簽與發(fā)布的品類標(biāo)簽的相似度------------------------------//拼接用戶的行業(yè)標(biāo)簽名稱去匹配品類ID數(shù)組$sonNames = explode(',', $user['s_name']);$pNames = explode(',', $user['p_name']);$nameArr = array_merge($sonNames, $pNames);$userLabel = (new TexturetypeModel)->whereIn('name', $nameArr)->column('id');//用于用戶標(biāo)簽匹配的相似度$similarIndustryArr = $this->calculateSimilar($labelArr, $userLabel);//--------------------------------------------------------------------------------------------------//權(quán)重計(jì)算$weigh = []; //用于存放推薦算法之后的權(quán)重?cái)?shù)組foreach ($businessArr as $key => $val) {//影響因素1:計(jì)算求購需求和出售需求占比if ($sellCount != 0 && $buyCount != 0) {//如果都占有則計(jì)算占比$allNeedRate = bcadd($sellCount, $buyCount, 4);$buyNeedRate = bcdiv($sellCount, $allNeedRate, 4);$sellNeedRate = bcdiv($buyCount, $allNeedRate, 4);} elseif ($buyCount == 0 && $sellCount == 0) {//如果都為0時(shí)候則需要$sellNeedRate = 0.5;$buyNeedRate = 0.5;} elseif ($sellCount == 0) {//如果未發(fā)布過出售,只發(fā)布求購則推10%的求購單,90%的出售單$buyNeedRate = 0.1;$sellNeedRate = 0.9;} else {//如果未發(fā)布過求購,只發(fā)布出售則推90%的求購單,10%的出售單$buyNeedRate = 0.9;$sellNeedRate = 0.1;}//影響因素2:標(biāo)簽設(shè)置時(shí)間進(jìn)行興趣衰減$timeRate2 = 1; //興趣衰弱占比$labelTimeRate = strtotime($user['update_time']) / time() * $timeRate2;//影響因素3:商機(jī)發(fā)布的時(shí)間衰減度$timeRate3 = 1; //興趣衰弱占比$busTimeRate = strtotime($val['update_time']) / time() * $timeRate3;//商機(jī)類型:0=求購,1=出售//最終權(quán)重 = (標(biāo)簽相似度 * 標(biāo)簽設(shè)置興趣衰減度 * 占比) + (發(fā)布供求相似度 * 發(fā)布供求需求占比 * 占比)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); //進(jìn)行自定義分頁處理$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 {//當(dāng)用戶未發(fā)布商機(jī)和供求時(shí)候游覽$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 {//游客游覽時(shí)候$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'];}//多少時(shí)間前$list[$index]['update_time'] = timeToBefore(strtotime($list[$index]['update_time']));//刪除不需要的字段unset($list[$index]['city'], $list[$index]['color']);}return $list; }2.2、計(jì)算相似度的代碼
/*** 用于計(jì)算相似度(傳入的必須是一位數(shù)組,value是對(duì)應(yīng)的標(biāo)簽ID)* @param array $data 大數(shù)組(大數(shù)組的key是供求ID)* @param array $inArr 小數(shù)組* @return array*/ private function calculateSimilar($data, $inArr) {//計(jì)算$inArr中標(biāo)簽出現(xiàn)概率$total = count($inArr);$countArr = $total != 0 ? array_count_values($inArr) : 0; //轉(zhuǎn)換成ID作為key,出現(xiàn)次數(shù)作為value 的一維數(shù)組(主要用于計(jì)算用戶發(fā)布過的商機(jī))$probability = $total != 0 ? 1 / $total : 1;//默認(rèn)概率$arr = []; //相似度數(shù)組foreach ($data as $key => $val) {//公式:相似度 = 交集/并集 * 概率$intersect = array_intersect($val, $inArr); //計(jì)算交集$union = array_unique(array_merge($val, $inArr)); //計(jì)算并集if ($total != 0) {if ($countArr) {//如果有則計(jì)算概率,其中一二級(jí)都會(huì)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、封裝的自定義分頁
/*** 將多維數(shù)組繼續(xù)分頁,自定義分頁效果* @param array &$array 數(shù)組* @param int $page 當(dāng)前頁數(shù)* @param int $limit 每頁頁數(shù)* @param int $order 0-不變 1-反序* @param bool $preserveKey true - 保留鍵名 false - 默認(rèn)。重置鍵名*/ function arrayToPage(Array &$array, int $page = 1, int $limit = 20, int $order = 0,bool $preserveKey = false) {$start = ($page - 1) * $limit; //計(jì)算每次分頁的開始位置//反序if ($order == 1) $array = array_reverse($array);$array = array_slice($array, $start, $limit,$preserveKey); }3、注意要點(diǎn),解釋上面代碼中主要部分
3.1、其中主要的計(jì)算部分如下
//----------------------------查出用戶最近發(fā)布的供求的品類ID進(jìn)行計(jì)算相似度---------------------------- //用于用戶發(fā)布供求標(biāo)簽匹配的相似度 $similarBusinessArr = $this->calculateSimilar($labelArr, $userBusinessLabel); //--------------------------------------------------------------------------------------------------//------------------------------以下是求行業(yè)標(biāo)簽與發(fā)布的品類標(biāo)簽的相似度------------------------------ //拼接用戶的行業(yè)標(biāo)簽名稱去匹配品類ID數(shù)組 $sonNames = explode(',', $user['s_name']); $pNames = explode(',', $user['p_name']); $nameArr = array_merge($sonNames, $pNames); $userLabel = (new TexturetypeModel)->whereIn('name', $nameArr)->column('id'); //用于用戶標(biāo)簽匹配的相似度 $similarIndustryArr = $this->calculateSimilar($labelArr, $userLabel); //---------------------------------------------------------------------------------------------------
函數(shù)中傳的參數(shù)必須是一維數(shù)組,其中
-
$userBusinessLabel 當(dāng)前用戶發(fā)布的供求標(biāo)簽一維數(shù)組(有重復(fù)數(shù)據(jù),需要計(jì)算出現(xiàn)概率)
-
$userLabel 用戶自定義的標(biāo)簽,無重復(fù)數(shù)據(jù)
3.2、最后總的計(jì)算并排序
//權(quán)重計(jì)算 $weigh = []; //用于存放推薦算法之后的權(quán)重?cái)?shù)組 foreach ($businessArr as $key => $val) {//計(jì)算求購需求和出售需求占比if ($buyCount == 0 && $sellCount == 0) {$sellNeedRate = 0.5;$buyNeedRate = 0.5;} else {$buyNeedRate = $sellCount / ($sellCount + $buyCount);$sellNeedRate = $buyCount / ($sellCount + $buyCount);//如果是比例是0和1的話需要對(duì)應(yīng)加10%和90%的基數(shù)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;//商機(jī)類型:0=求購,1=出售if ($val['type'] == 0) $weigh[$key] = $similar * $buyNeedRate;if ($val['type'] == 1) $weigh[$key] = $similar * $sellNeedRate; }arsort($weigh); //按相似度,最相似的排最前面- 其中我根據(jù)出售和求購的兩種類型進(jìn)行占比計(jì)算,我們還可以另外加其他的占比情況。
- 我們?cè)谟?jì)算過程中需要考慮分母不能為0的情況。
- 除此之外我們還可以用權(quán)重算法進(jìn)行計(jì)算,需要根據(jù)業(yè)務(wù)和數(shù)據(jù)測(cè)試之后確定適合自己的算法。
- 我們?cè)谟?jì)算過程中需要考慮到極限的情況,我們需要在這種情況下加上基數(shù)穩(wěn)定推薦算法。
PS : 上面是我初次接觸使用簡(jiǎn)單的推薦算法,如果有什么不對(duì)的地方請(qǐng)指教,我還會(huì)一直完善。由于數(shù)據(jù)的不足,還沒有進(jìn)入測(cè)試階段,所以我還是需要去不斷改進(jìn)。
總結(jié)
以上是生活随笔為你收集整理的TP5 实现基于标签简单的推荐算法的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: TP5 实现转盘抽奖
- 下一篇: java学习笔记(一) ----java