graphaware/reco4php

基于Neo4j的PHP推荐引擎框架

2.0.5 2018-02-04 21:05 UTC

README

基于Neo4j的PHP推荐引擎框架

GraphAware Reco4PHP是一个用于在Neo4j之上构建复杂推荐引擎的库。

Build Status

特性

  • 干净灵活的设计
  • 内置算法和函数
  • 测量推荐质量的能力
  • 内置Cypher事务管理

要求

  • PHP7.0+
  • Neo4j 2.2.6+(推荐使用Neo4j 3.0+)

该库强加了一种特定的推荐引擎架构,这种架构源于我们构建推荐引擎的经验,并解决了通过Cypher在远程运行推荐引擎的架构挑战。作为回报,它处理了所有管道,这样您只需编写特定于您用例的推荐业务逻辑。

推荐引擎架构

发现引擎和推荐

推荐引擎的目的是进行推荐,比如推荐您应该关注的人、您应该购买的产品、您应该阅读的文章。

推荐过程的第一步是找到推荐的项目,这被称为发现过程。

在Reco4PHP中,DiscoveryEngine负责以某种可能的方式发现推荐的项目。

通常,推荐系统将包含多个发现引擎。如果您编写的是“您应该在GitHub上关注的用户”推荐引擎,您可能会得到一个不完整的Discovery Engines列表。

  • 找到与我共同贡献过相同仓库的人
  • 找到与我关注的相同人关注的人
  • 找到与我关注的相同仓库的人
  • ...

每个Discovery Engine将生成一组包含发现的Item及其分数(下面将进一步说明)的Recommendations

过滤器和黑名单

Filters的目的是将原始input与发现的项进行比较,并决定此项是否应该被推荐给用户。一个非常简单的过滤器可能是ExcludeSelf,它会排除与输入相同的节点,这在密集连接的图中可能会相对发生。

另一方面,BlackLists是一组不应向用户推荐的预定义节点。一个例子可能是创建一个包含用户已购买物品的BlackList,如果您想推荐他应该购买的产品。

后处理器

PostProcessors提供在推荐通过过滤器和黑名单过程后的后处理能力。

例如,如果您会奖励一个住在您同一个城市的人,那么在发现阶段(以伦敦为例,这可能需要数百万)加载所有住在该城市的人就不合理了。

然后,您可以创建一个RewardSameCity后处理器,如果输入节点和推荐的项目住在同一个城市,它会调整生成的推荐的分数。

总结

总结一下,一个典型的推荐引擎将是一组

  • 一个或多个Discovery Engines
  • 零个或多个FitlersBlackLists
  • 零个或多个PostProcessors

让我们开始吧!

通过示例使用

我们将使用来自MovieLens的小数据集,其中包含电影、用户、评分以及流派。

数据集公开可用于此处:http://grouplens.org/datasets/movielens/。要下载的数据集在 MovieLens 最新数据集 部分中,名为 ml-latest-small.zip

下载并解压缩存档后,您可以运行以下 Cypher 语句来导入数据集,只需将文件 URL 调整为匹配您实际文件路径即可

CREATE CONSTRAINT ON (m:Movie) ASSERT m.id IS UNIQUE;
CREATE CONSTRAINT ON (g:Genre) ASSERT g.name IS UNIQUE;
CREATE CONSTRAINT ON (u:User) ASSERT u.id IS UNIQUE;
LOAD CSV WITH HEADERS FROM "file:///Users/ikwattro/dev/movielens/movies.csv" AS row
WITH row
MERGE (movie:Movie {id: toInt(row.movieId)})
ON CREATE SET movie.title = row.title
WITH movie, row
UNWIND split(row.genres, '|') as genre
MERGE (g:Genre {name: genre})
MERGE (movie)-[:HAS_GENRE]->(g)
USING PERIODIC COMMIT 500
LOAD CSV WITH HEADERS FROM "file:///Users/ikwattro/dev/movielens/ratings.csv" AS row
WITH row
MATCH (movie:Movie {id: toInt(row.movieId)})
MERGE (user:User {id: toInt(row.userId)})
MERGE (user)-[r:RATED]->(movie)
ON CREATE SET r.rating = toInt(row.rating), r.timestamp = toInt(row.timestamp)

为了举例说明,我们将假设我们正在为用户 ID 为 460 的用户推荐电影。

安装

需要使用 composer 安装依赖项

composer require graphaware/reco4php

用法

发现

为了推荐人们应该观看的电影,您决定以以下方式找到潜在推荐:

  • 找到与我评分相同的电影的人评分的电影,但我尚未评分的电影

如前所述,reco4php 推荐引擎框架负责所有管道,因此您只需关注业务逻辑,这就是为什么它提供了一个基类,您应该扩展该类并仅实现上层接口的方法,以下是创建您的第一个发现引擎的方式:

<?php

namespace GraphAware\Reco4PHP\Tests\Example\Discovery;

use GraphAware\Common\Cypher\Statement;
use GraphAware\Common\Type\Node;
use GraphAware\Reco4PHP\Context\Context;
use GraphAware\Reco4PHP\Engine\SingleDiscoveryEngine;

class RatedByOthers extends SingleDiscoveryEngine
{
    public function discoveryQuery(Node $input, Context $context)
    {
        $query = 'MATCH (input:User) WHERE id(input) = {id}
        MATCH (input)-[:RATED]->(m)<-[:RATED]-(o)
        WITH distinct o
        MATCH (o)-[:RATED]->(reco)
        RETURN distinct reco LIMIT 500';

        return Statement::create($query, ['id' => $input->identity()]);
    }

    public function name()
    {
        return "rated_by_others";
    }
}

discoveryMethod 方法应返回一个包含查找推荐查询的 Statement 对象,name 方法应返回一个字符串,描述您引擎的名称(这主要用于日志记录目的)。

这里的查询有一些逻辑,我们不希望返回所有找到的电影作为候选人,因为在初始数据集中有 10k+,想象一下在 100M 数据集中的情况。因此,我们正在计算评分的总和,并返回评分最高的电影,将结果限制为 500 个潜在推荐。

基类假设推荐节点将具有标识符 reco,产生的推荐评分的标识符为 score。评分不是必需的,它将被赋予默认评分 1

所有这些默认值都可以通过覆盖基类的方法进行自定义(请参阅自定义部分)。

此发现引擎将生成一组 500 个评分的 Recommendation 对象,您可以使用这些对象在您的过滤器或后处理程序中。

过滤

作为过滤器的示例,我们将过滤在 1999 年之前制作的电影。年份写在电影标题中,因此我们将在过滤器中使用正则表达式提取年份。

<?php

namespace GraphAware\Reco4PHP\Tests\Example\Filter;

use GraphAware\Common\Type\Node;
use GraphAware\Reco4PHP\Filter\Filter;

class ExcludeOldMovies implements Filter
{
    public function doInclude(Node $input, Node $item)
    {
        $title = $item->value("title");
        preg_match('/(?:\()\d+(?:\))/', $title, $matches);

        if (isset($matches[0])) {
            $y = str_replace('(','',$matches[0]);
            $y = str_replace(')','', $y);
            $year = (int) $y;
            if ($year < 1999) {
                return false;
            }

            return true;
        }

        return false;
    }
}

Filter 接口强制您实现 doInclude 方法,该方法应返回一个布尔值。您可以通过方法参数访问推荐节点以及输入。

黑名单

当然,我们不希望推荐当前用户已经评分的电影,为此我们将创建一个黑名单,构建一组这些已经评分的电影节点。

<?php

namespace GraphAware\Reco4PHP\Tests\Example\Filter;

use GraphAware\Common\Cypher\Statement;
use GraphAware\Common\Type\Node;
use GraphAware\Reco4PHP\Filter\BaseBlacklistBuilder;

class AlreadyRatedBlackList extends BaseBlacklistBuilder
{
    public function blacklistQuery(Node $input)
    {
        $query = 'MATCH (input) WHERE id(input) = {inputId}
        MATCH (input)-[:RATED]->(movie)
        RETURN movie as item';

        return Statement::create($query, ['inputId' => $input->identity()]);
    }

    public function name()
    {
        return 'already_rated';
    }
}

您只需要添加匹配应被列入黑名单的节点的逻辑,框架会负责将推荐节点与提供的黑名单进行过滤。

后处理程序

Post Processors 用于向推荐项目添加额外的评分。在我们的示例中,如果产生的推荐有超过 10 个评分,我们可以奖励该推荐。

<?php

namespace GraphAware\Reco4PHP\Tests\Example\PostProcessing;

use GraphAware\Common\Cypher\Statement;
use GraphAware\Common\Result\Record;
use GraphAware\Common\Type\Node;
use GraphAware\Reco4PHP\Post\RecommendationSetPostProcessor;
use GraphAware\Reco4PHP\Result\Recommendation;
use GraphAware\Reco4PHP\Result\Recommendations;
use GraphAware\Reco4PHP\Result\SingleScore;

class RewardWellRated extends RecommendationSetPostProcessor
{
    public function buildQuery(Node $input, Recommendations $recommendations)
    {
        $query = 'UNWIND {ids} as id
        MATCH (n) WHERE id(n) = id
        MATCH (n)<-[r:RATED]-(u)
        RETURN id(n) as id, sum(r.rating) as score';

        $ids = [];
        foreach ($recommendations->getItems() as $item) {
            $ids[] = $item->item()->identity();
        }

        return Statement::create($query, ['ids' => $ids]);
    }

    public function postProcess(Node $input, Recommendation $recommendation, Record $record)
    {
        $recommendation->addScore($this->name(), new SingleScore($record->get('score'), 'total_ratings_relationships'));
    }

    public function name()
    {
        return "reward_well_rated";
    }
}

整合所有组件

现在,我们的组件已创建,我们需要有效地构建我们的推荐引擎

<?php

namespace GraphAware\Reco4PHP\Tests\Example;

use GraphAware\Reco4PHP\Engine\BaseRecommendationEngine;
use GraphAware\Reco4PHP\Tests\Example\Filter\AlreadyRatedBlackList;
use GraphAware\Reco4PHP\Tests\Example\Filter\ExcludeOldMovies;
use GraphAware\Reco4PHP\Tests\Example\PostProcessing\RewardWellRated;
use GraphAware\Reco4PHP\Tests\Example\Discovery\RatedByOthers;

class ExampleRecommendationEngine extends BaseRecommendationEngine
{
    public function name()
    {
        return "example";
    }

    public function discoveryEngines()
    {
        return array(
            new RatedByOthers()
        );
    }

    public function blacklistBuilders()
    {
        return array(
            new AlreadyRatedBlackList()
        );
    }

    public function postProcessors()
    {
        return array(
            new RewardWellRated()
        );
    }

    public function filters()
    {
        return array(
            new ExcludeOldMovies()
        );
    }
}

在您的推荐服务中,您可能有多多个推荐引擎提供不同的推荐,最后一步是创建此服务并注册您创建的每个 RecommendationEngine。您还需要提供对您的 Neo4j 数据库的连接,在您的应用程序中,这可能看起来像这样:

<?php

namespace GraphAware\Reco4PHP\Tests\Example;

use GraphAware\Reco4PHP\Context\SimpleContext;
use GraphAware\Reco4PHP\RecommenderService;

class ExampleRecommenderService
{
    /**
     * @var \GraphAware\Reco4PHP\RecommenderService
     */
    protected $service;

    /**
     * ExampleRecommenderService constructor.
     * @param string $databaseUri
     */
    public function __construct($databaseUri)
    {
        $this->service = RecommenderService::create($databaseUri);
        $this->service->registerRecommendationEngine(new ExampleRecommendationEngine());
    }

    /**
     * @param int $id
     * @return \GraphAware\Reco4PHP\Result\Recommendations
     */
    public function recommendMovieForUserWithId($id)
    {
        $input = $this->service->findInputBy('User', 'id', $id);
        $recommendationEngine = $this->service->getRecommender("user_movie_reco");

        return $recommendationEngine->recommend($input, new SimpleContext());
    }
}

检查推荐

推荐引擎中的recommend()方法会返回一个包含一系列RecommendationRecommendations对象,每个Recommendation包含推荐的物品及其评分。

每个评分都会被插入,这样您可以轻松地检查为什么会产生这样的推荐,例如

$recommender = new ExampleRecommendationService("http://localhost:7474");
$recommendation = $recommender->recommendMovieForUserWithId(460);

print_r($recommendations->getItems(1));

Array
(
    [0] => GraphAware\Reco4PHP\Result\Recommendation Object
        (
            [item:protected] => GraphAware\Bolt\Result\Type\Node Object
                (
                    [identity:protected] => 13248
                    [labels:protected] => Array
                        (
                            [0] => Movie
                        )

                    [properties:protected] => Array
                        (
                            [id] => 2571
                            [title] => Matrix, The (1999)
                        )

                )

            [scores:protected] => Array
                (
                    [rated_by_others] => GraphAware\Reco4PHP\Result\Score Object
                        (
                            [score:protected] => 1067
                            [scores:protected] => Array
                                (
                                    [0] => GraphAware\Reco4PHP\Result\SingleScore Object
                                        (
                                            [score:GraphAware\Reco4PHP\Result\SingleScore:private] => 1067
                                            [reason:GraphAware\Reco4PHP\Result\SingleScore:private] =>
                                        )

                                )

                        )

                    [reward_well_rated] => GraphAware\Reco4PHP\Result\Score Object
                        (
                            [score:protected] => 261
                            [scores:protected] => Array
                                (
                                    [0] => GraphAware\Reco4PHP\Result\SingleScore Object
                                        (
                                            [score:GraphAware\Reco4PHP\Result\SingleScore:private] => 261
                                            [reason:GraphAware\Reco4PHP\Result\SingleScore:private] =>
                                        )

                                )

                        )

                )

            [totalScore:protected] => 261
        )
)

许可证

本库采用Apache v2许可证发布,请阅读附带的LICENSE文件。

如有商业支持或定制开发/扩展需求,请发送邮件至info@graphaware.com