基于ThinkPHP 5~8兼容的推荐算法类实现,

在现代推荐系统中,随着用户量和物品量的增长,传统的推荐算法可能会面临性能瓶颈。本文将介绍如何基于 ThinkPHP 实现一个高性能的推荐系统,结合显性反馈(如兴趣选择)、隐性反馈(如观看时长、评论、点赞、搜索等)、行为序列分析和关键词拆分(支持中文)等功能,并通过优化方案支持大规模用户场景。

目录

推荐系统简介

数据库设计

推荐算法类的实现

优化方案

总结与扩展

推荐系统简介

推荐系统的目标是根据用户的历史行为,预测用户可能感兴趣的物品。常见的推荐算法包括:

基于内容的推荐:根据物品的属性推荐相似物品。

协同过滤:根据用户的行为推荐相似用户喜欢的物品。

混合推荐:结合多种推荐算法,提升推荐效果。

本文将重点介绍基于协同过滤的推荐算法,并结合显性反馈和隐性反馈数据。


数据库设计

1. 用户表 (users)

存储用户的基本信息。

sql 复制代码
CREATE TABLE users (
    user_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID,主键,自增',
    username VARCHAR(50) NOT NULL COMMENT '用户名,唯一标识用户'
) COMMENT='用户表,存储用户的基本信息';

2. 物品表 (items)

存储物品的基本信息。

sql 复制代码
CREATE TABLE items (
    item_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '物品ID,主键,自增',
    item_name VARCHAR(100) NOT NULL COMMENT '物品名称,唯一标识物品',
    description TEXT COMMENT '物品描述,用于关键词拆分',
    tags TEXT COMMENT '物品标签,用于关键词拆分'
) COMMENT='物品表,存储物品的基本信息';

3. 显性反馈表 (explicit_feedback)

存储用户对物品的显性反馈,如兴趣选择、评分等。

sql 复制代码
CREATE TABLE explicit_feedback (
    feedback_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '反馈ID,主键,自增',
    user_id INT COMMENT '用户ID,外键,关联用户表',
    item_id INT COMMENT '物品ID,外键,关联物品表',
    rating INT COMMENT '用户对物品的评分(1-5)',
    interest_level ENUM('low', 'medium', 'high') COMMENT '用户兴趣选择(低、中、高)',
    FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE, -- 用户删除时,关联的反馈也删除
    FOREIGN KEY (item_id) REFERENCES items(item_id) ON DELETE CASCADE -- 物品删除时,关联的反馈也删除
) COMMENT='显性反馈表,存储用户对物品的显性反馈';

4. 隐性反馈表 (implicit_feedback)

存储用户的隐性行为,如观看时长、评论、点赞、搜索等。

sql 复制代码
CREATE TABLE implicit_feedback (
    feedback_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '反馈ID,主键,自增',
    user_id INT COMMENT '用户ID,外键,关联用户表',
    item_id INT COMMENT '物品ID,外键,关联物品表',
    action_type ENUM('view', 'comment', 'like', 'search') COMMENT '行为类型(观看、评论、点赞、搜索)',
    action_value FLOAT COMMENT '行为值(如观看时长、点赞次数等)',
    FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE, -- 用户删除时,关联的反馈也删除
    FOREIGN KEY (item_id) REFERENCES items(item_id) ON DELETE CASCADE -- 物品删除时,关联的反馈也删除
) COMMENT='隐性反馈表,存储用户的隐性行为';

5. 行为序列表 (behavior_sequence)

存储用户的行为序列,按时间排序。

sql 复制代码
CREATE TABLE behavior_sequence (
    sequence_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '序列ID,主键,自增',
    user_id INT COMMENT '用户ID,外键,关联用户表',
    item_id INT COMMENT '物品ID,外键,关联物品表',
    action_type ENUM('view', 'comment', 'like', 'search') COMMENT '行为类型(观看、评论、点赞、搜索)',
    action_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '行为发生时间,默认为当前时间',
    FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE, -- 用户删除时,关联的行为也删除
    FOREIGN KEY (item_id) REFERENCES items(item_id) ON DELETE CASCADE -- 物品删除时,关联的行为也删除
) COMMENT='行为序列表,存储用户的行为序列';

6. 关键词表 (keywords)

存储从物品描述和标签中提取的关键词。

sql 复制代码
CREATE TABLE keywords (
    keyword_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '关键词ID,主键,自增',
    keyword VARCHAR(50) NOT NULL COMMENT '关键词,用于推荐算法',
    item_id INT COMMENT '物品ID,外键,关联物品表',
    FOREIGN KEY (item_id) REFERENCES items(item_id) ON DELETE CASCADE -- 物品删除时,关联的关键词也删除
) COMMENT='关键词表,存储从物品描述和标签中提取的关键词';

ThinkPHP 推荐算法类

以下是基于 ThinkPHP 的推荐算法类实现,每行代码都有详细注释。

php 复制代码
<?php

namespace app\common\service;

use think\Db;
use think\facade\Cache;

class Recommender
{
    // 获取用户的显性反馈(兴趣选择和评分)
    private function getExplicitFeedback(int $userId): array
    {
        // 查询用户的显性反馈数据
        return Db::name('explicit_feedback')
            ->where('user_id', $userId) // 过滤指定用户
            ->field('item_id, rating, interest_level') // 选择需要的字段
            ->select(); // 返回查询结果
    }

    // 获取用户的隐性反馈(观看时长、评论、点赞、搜索等)
    private function getImplicitFeedback(int $userId): array
    {
        // 查询用户的隐性反馈数据,按物品和行为类型分组
        return Db::name('implicit_feedback')
            ->where('user_id', $userId) // 过滤指定用户
            ->field('item_id, action_type, SUM(action_value) as total_value') // 选择需要的字段,并计算行为值的总和
            ->group('item_id, action_type') // 按物品和行为类型分组
            ->select(); // 返回查询结果
    }

    // 获取用户的行为序列
    private function getBehaviorSequence(int $userId): array
    {
        // 查询用户的行为序列,按时间排序
        return Db::name('behavior_sequence')
            ->where('user_id', $userId) // 过滤指定用户
            ->order('action_time ASC') // 按行为时间升序排序
            ->field('item_id, action_type, action_time') // 选择需要的字段
            ->select(); // 返回查询结果
    }

    // 从物品描述和标签中提取关键词(支持中文)
    private function extractKeywords(string $text): array
    {
        // 使用 jieba-php 分词库进行中文分词
        $words = \Fukuball\Jieba\Jieba::cut($text); // 将文本拆分为单词
        $stopWords = ['的', '了', '在', '是', '我']; // 中文停用词列表
        return array_diff($words, $stopWords); // 去除停用词,返回关键词数组
    }

    // 获取物品的关键词
    private function getItemKeywords(int $itemId): array
    {
        // 查询物品的描述和标签
        $item = Db::name('items')
            ->where('item_id', $itemId) // 过滤指定物品
            ->field('description, tags') // 选择需要的字段
            ->find(); // 返回单行数据

        $keywords = [];
        if ($item) {
            // 从描述和标签中提取关键词
            $keywords = array_merge(
                $this->extractKeywords($item['description']), // 提取描述中的关键词
                $this->extractKeywords($item['tags']) // 提取标签中的关键词
            );
        }
        return array_unique($keywords); // 去重后返回关键词数组
    }

    // 计算用户相似度(基于显性和隐性反馈)
    private function calculateUserSimilarity(int $user1, int $user2): float
    {
        // 获取用户1的显性反馈
        $feedback1 = $this->getExplicitFeedback($user1);
        // 获取用户2的显性反馈
        $feedback2 = $this->getExplicitFeedback($user2);

        $dotProduct = 0; // 点积
        $magnitude1 = 0; // 用户1的模长
        $magnitude2 = 0; // 用户2的模长

        // 遍历用户1的反馈数据
        foreach ($feedback1 as $item) {
            $itemId = $item['item_id']; // 物品ID
            $rating1 = $item['rating']; // 用户1的评分
            $interestLevel1 = $item['interest_level'] === 'high' ? 1 : 0; // 用户1的兴趣选择(高为1,否则为0)

            // 遍历用户2的反馈数据
            foreach ($feedback2 as $item2) {
                if ($item2['item_id'] === $itemId) { // 如果两个用户对同一物品有反馈
                    $rating2 = $item2['rating']; // 用户2的评分
                    $interestLevel2 = $item2['interest_level'] === 'high' ? 1 : 0; // 用户2的兴趣选择(高为1,否则为0)

                    // 计算点积和模长
                    $dotProduct += ($rating1 * $rating2) + ($interestLevel1 * $interestLevel2);
                    $magnitude1 += ($rating1 * $rating1) + ($interestLevel1 * $interestLevel1);
                    $magnitude2 += ($rating2 * $rating2) + ($interestLevel2 * $interestLevel2);
                }
            }
        }

        $magnitude1 = sqrt($magnitude1); // 用户1的模长
        $magnitude2 = sqrt($magnitude2); // 用户2的模长

        if ($magnitude1 == 0 || $magnitude2 == 0) {
            return 0; // 避免除零错误
        }

        return $dotProduct / ($magnitude1 * $magnitude2); // 返回余弦相似度
    }

    // 获取与目标用户最相似的用户
    private function getSimilarUsers(int $targetUserId): array
    {
        // 查询所有其他用户
        $users = Db::name('users')
            ->where('user_id', '<>', $targetUserId) // 过滤掉目标用户
            ->column('user_id'); // 返回用户ID列表

        $similarities = [];
        // 遍历所有用户,计算与目标用户的相似度
        foreach ($users as $userId) {
            $similarity = $this->calculateUserSimilarity($targetUserId, $userId); // 计算相似度
            $similarities[$userId] = $similarity; // 存储相似度
        }

        arsort($similarities); // 按相似度降序排序
        return $similarities; // 返回相似用户列表
    }

    // 为目标用户推荐物品(基于缓存)
    public function recommendItems(int $targetUserId, int $numRecommendations = 5): array
    {
        $cacheKey = "recommendations_{$targetUserId}"; // 缓存键
        $recommendations = Cache::get($cacheKey); // 从缓存中获取推荐结果

        if (!$recommendations) { // 如果缓存中没有推荐结果
            $similarUsers = $this->getSimilarUsers($targetUserId); // 获取相似用户
            $recommendations = [];

            // 遍历相似用户
            foreach ($similarUsers as $userId => $similarity) {
                $feedback = $this->getExplicitFeedback($userId); // 获取相似用户的显性反馈
                // 遍历相似用户的反馈数据
                foreach ($feedback as $item) {
                    $itemId = $item['item_id']; // 物品ID
                    $rating = $item['rating']; // 评分
                    $interestLevel = $item['interest_level'] === 'high' ? 1 : 0; // 兴趣选择(高为1,否则为0)

                    // 如果目标用户未评价过该物品,且评分或兴趣选择较高
                    if (!isset($this->getExplicitFeedback($targetUserId)[$itemId]) && ($rating >= 4 || $interestLevel)) {
                        if (!isset($recommendations[$itemId])) {
                            $recommendations[$itemId] = 0; // 初始化推荐分数
                        }
                        // 计算推荐分数
                        $recommendations[$itemId] += ($rating * $similarity) + ($interestLevel * $similarity);
                    }
                }
            }

            arsort($recommendations); // 按推荐分数降序排序
            $recommendations = array_slice($recommendations, 0, $numRecommendations, true); // 返回前N个推荐物品

            // 缓存推荐结果,有效期1小时
            Cache::set($cacheKey, $recommendations, 3600);
        }

        return $recommendations; // 返回推荐结果
    }
}

优化方案

1.用户相似度计算的优化

  • 离线计算:将用户相似度计算任务放到离线任务中,定期更新相似度矩阵。
  • 分块计算:将用户分成多个块,分别计算块内用户的相似度,减少计算量。
  • 近似算法:使用局部敏感哈希(LSH)或随机投影等近似算法,快速找到相似用户。

2、推荐物品的优化

  • 基于物品的协同过滤:计算物品之间的相似度,推荐与用户历史行为相似的物品。
  • 矩阵分解:使用矩阵分解(如SVD)将用户-物品评分矩阵分解为低维矩阵,减少计算量。
  • 缓存推荐结果:将推荐结果缓存到Redis等内存数据库中,减少实时计算的压力。

3、数据库查询的优化

  • 索引优化:为常用查询字段(如user_id、item_id)添加索引。
  • 分库分表:将用户和物品数据分散到多个数据库或表中,减少单表数据量。
  • 批量查询:减少数据库查询次数,尽量使用批量查询。

4、引入分布式计算

  • 使用分布式计算框架(如Hadoop、Spark)处理大规模数据。
  • 将用户相似度计算和推荐任务分布到多个节点上执行。

总结与扩展

通过上述代码和优化方案,可以实现一个高性能的推荐系统,支持大规模用户场景。以下是扩展方向:

  1. 引入机器学习模型:如矩阵分解(SVD)或深度学习模型,提升推荐效果。
  2. 实时推荐:根据用户的最新行为动态调整推荐结果。
  3. 多维度权重:为不同类型的隐性反馈(如观看时长、点赞)设置不同的权重。

希望本文对你理解和实现推荐系统有所帮助!如果有其他问题,欢迎留言讨论。


相关推荐
带娃的IT创业者8 分钟前
机器学习实战(8):降维技术——主成分分析(PCA)
人工智能·机器学习·分类·聚类
饮长安千年月2 小时前
Linksys WRT54G路由器溢出漏洞分析–运行环境修复
网络·物联网·学习·安全·机器学习
flying robot3 小时前
人工智能基础之数学基础:01高等数学基础
人工智能·机器学习
Moutai码农3 小时前
机器学习-生命周期
人工智能·python·机器学习·数据挖掘
Jackilina_Stone3 小时前
【DL】浅谈深度学习中的知识蒸馏 | 输出层知识蒸馏
人工智能·深度学习·机器学习·蒸馏
尼尔森系3 小时前
排序与算法:希尔排序
c语言·算法·排序算法
AC使者4 小时前
A. C05.L08.贪心算法入门
算法·贪心算法
冠位观测者4 小时前
【Leetcode 每日一题】624. 数组列表中的最大距离
数据结构·算法·leetcode
yadanuof4 小时前
leetcode hot100 滑动窗口&子串
算法·leetcode
可爱de艺艺4 小时前
Go入门之函数
算法