译者 | 朱先忠
审校 | 重楼
本文将探讨直接在关系数据库上执行机器学习的新方法——关系型深度学习。
本文示例项目数据集的关系模式(作者提供图片)
在本文中,我们将深入探讨一种有趣的深度学习(DL)新方法,称为关系型深度学习(RDL)。我们还将通过在一家电子商务公司的真实数据库(不是数据集!)上做一些RDL来获得一些实践经验。
简介
在现实世界中,我们通常有一个关系数据库,我们想在这个数据库上运行一些机器学习任务。但是,有时候数据库需要高度规范化;这意味着,大量耗时的特征工程和粒度损失,因为我们必须进行大量的聚合操作。更重要的是,我们可以构建无数种可能的特征组合,每种组合都可能产生良好的性能(【文献2】)。这意味着,我们可能会在数据库表格中留下一些与ML任务相关的信息。
这类似于计算机视觉的早期,在深度神经网络出现之前,特征工程任务是基于像素值形式手工完成的。如今,模型直接使用原始像素,而不再依赖于这个中间环节。
关系型深度学习
关系型深度学习(RDL)承诺用表格形式学习实现同样的事情。也就是说,它消除了通过直接在关系数据库上学习来构建特征矩阵的额外步骤。RDL通过将数据库及其关系转换为图来实现这一点;其中,表中的一行成为节点,表之间的关系成为边,行值作为节点特征存储在节点内。
在本文中,我们将使用Kaggle的电子商务数据集,该数据集包含有关星形模式中电子商务平台的交易数据,其中包含一个核心事实表(交易)和一些维度表。完整的代码可以在链接处的笔记本文件中找到。
在本文中,我们将使用relbench库来执行RDL。在relbench中,我们必须做的第一件事是指定关系数据库的模式。下面给出一个示例,说明我们如何对数据库中的“事务”表执行此操作。我们将表作为pandas数据帧给出,并指定主键和时间戳列。主键列用于唯一标识实体。时间戳确保我们只能在预测未来交易时从过去的交易中学习。在这种构图中,这意味着信息只能从时间戳较低的节点(即过去)流向时间戳较高的节点。此外,我们指定关系中存在的外键。在这种情况下,事务表具有列“customer_key”,该列是指向“customer_dim”表的外键。
复制
tables['transactions'] = Table( df=pd.DataFrame(t), pkey_col='t_id', fkey_col_to_pkey_table={ 'customer_key': 'customers', 'item_key': 'products', 'store_key': 'stores' }, time_col='date' )
其余的表需要以相同的方式定义。请注意,如果你已经有了数据库模式,这也可以通过自动化的方式实现。由于数据集来自Kaggle,所以我需要手动创建模式。我们还需要将日期列转换为实际的pandas日期时间对象,并删除任何NaN值。
复制
class EcommerceDataBase(Dataset): #创建你自己的数据集的示例:https://github.com/snap-stanford/relbench/blob/main/tutorials/custom_dataset.ipynb val_timestamp = pd.Timestamp(year=2018, month=1, day=1) test_timestamp = pd.Timestamp(year=2020, month=1, day=1) def make_db(self) -> Database: tables = {} customers = load_csv_to_db(BASE_DIR + '/customer_dim.csv').drop(columns=['contact_no', 'nid']).rename(columns={'coustomer_key': 'customer_key'}) stores = load_csv_to_db(BASE_DIR + '/store_dim.csv').drop(columns=['upazila']) products = load_csv_to_db(BASE_DIR + '/item_dim.csv') transactions = load_csv_to_db(BASE_DIR + '/fact_table.csv').rename(columns={'coustomer_key': 'customer_key'}) times = load_csv_to_db(BASE_DIR + '/time_dim.csv') t = transactions.merge(times[['time_key', 'date']], on='time_key').drop(columns=['payment_key', 'time_key', 'unit']) t['date'] = pd.to_datetime(t.date) t = t.reset_index().rename(columns={'index': 't_id'}) t['quantity'] = t.quantity.astype(int) t['unit_price'] = t.unit_price.astype(float) products['unit_price'] = products.unit_price.astype(float) t['total_price'] = t.total_price.astype(float) print(t.isna().sum(axis=0)) print(products.isna().sum(axis=0)) print(stores.isna().sum(axis=0)) print(customers.isna().sum(axis=0)) tables['products'] = Table( df=pd.DataFrame(products), pkey_col='item_key', fkey_col_to_pkey_table={}, time_col=None ) tables['customers'] = Table( df=pd.DataFrame(customers), pkey_col='customer_key', fkey_col_to_pkey_table={}, time_col=None ) tables['transactions'] = Table( df=pd.DataFrame(t), pkey_col='t_id', fkey_col_to_pkey_table={ 'customer_key': 'customers', 'item_key': 'products', 'store_key': 'stores' }, time_col='date' ) tables['stores'] = Table( df=pd.DataFrame(stores), pkey_col='store_key', fkey_col_to_pkey_table={} ) return Database(tables)
至关重要的是,作者引入了训练表的概念。这个训练表基本上定义了ML任务。这里的想法是,我们想预测数据库中某个实体的未来状态(即未来值)。我们通过指定一个表来实现这一点,其中每一行都有一个时间戳、实体的标识符和我们想要预测的一些值。id用于指定实体,时间戳指定我们需要预测实体的时间点。这也将限制可用于推断此实体值的数据(即仅过去的数据)。值本身就是我们想要预测的(即真实数据值)。
就我们而言,我们有一个与客户互动的在线平台。我们希望预测客户在未来30天内的收入。我们可以使用DuckDB执行的SQL语句创建训练表。这是RDL的一大优势,因为我们可以仅使用SQL创建任何类型的ML任务。例如,我们可以定义一个查询来选择未来30天内买家的购买数量,以进行流失预测。
复制
df = duckdb.sql(f""" select timestamp, customer_key, sum(total_price) as revenue from timestamp_df t left join transactions ta on ta.date <= t.timestamp + INTERVAL '{self.timedelta}' and ta.date > t.timestamp group by timestamp, customer_key """).df().dropna()
结果将是一个数据库表格,其中seller_id是我们想要预测的实体的关键字,收入是目标,时间戳是我们需要进行预测的时间(即我们只能使用到目前为止的数据进行预测)。
训练表(作者提供图片)
下面是创建“customer_venue”任务的完整代码。
复制
class CustomerRevenueTask(EntityTask): # 自定义任务示例:https://github.com/snap-stanford/relbench/blob/main/tutorials/custom_task.ipynb task_type = TaskType.REGRESSION entity_col = "customer_key" entity_table = "customers" time_col = "timestamp" target_col = "revenue" timedelta = pd.Timedelta(days=30) # 我们想要预测未来的收入。 metrics = [r2, mae] num_eval_timestamps = 40 def make_table(self, db: Database, timestamps: "pd.Series[pd.Timestamp]") -> Table: timestamp_df = pd.DataFrame({"timestamp": timestamps}) transactions = db.table_dict["transactions"].df df = duckdb.sql(f""" select timestamp, customer_key, sum(total_price) as revenue from timestamp_df t left join transactions ta on ta.date <= t.timestamp + INTERVAL '{self.timedelta}' and ta.date > t.timestamp group by timestamp, customer_key """).df().dropna() print(df) return Table( df=df, fkey_col_to_pkey_table={self.entity_col: self.entity_table}, pkey_col=None, time_col=self.time_col, )
至此,我们已经完成了大部分工作。其余的工作流程都是类似的,独立于机器学习任务。我能够从relbench提供的示例笔记本文件中复制大部分代码。
例如,我们需要对节点特征进行编码。在这里,我们可以使用GloVe嵌入(【译者注】个别网文中翻译为“手套嵌入”)来编码所有文本特征,如产品描述和产品名称。
复制
from typing import List, Optional from sentence_transformers import SentenceTransformer from torch import Tensor class GloveTextEmbedding: def __init__(self, device: Optional[torch.device ] = None): self.model = SentenceTransformer( "sentence-transformers/average_word_embeddings_glove.6B.300d", device=device, ) def __call__(self, sentences: List[str]) -> Tensor: return torch.from_numpy(self.model.encode(sentences))
之后,我们可以将这些转换应用于我们的数据并构建图表。
复制
from torch_frame.config.text_embedder import TextEmbedderConfig from relbench.modeling.graph import make_pkey_fkey_graph text_embedder_cfg = TextEmbedderConfig( text_embedder=GloveTextEmbedding(device=device), batch_size=256 ) data, col_stats_dict = make_pkey_fkey_graph( db, col_to_stype_dict=col_to_stype_dict, # speficied column types text_embedder_cfg=text_embedder_cfg, # our chosen text encoder cache_dir=os.path.join( root_dir, f"rel-ecomm_materialized_cache" ), # store materialized graph for convenience )
其余的代码将从标准层构建GNN(图神经网络),对循环训练进行编码,并进行一些评估。为了简单起见,我将把这段代码从本文中删除,因为它非常标准,在各个任务中都是一样的。你可以在链接https://github.com/LaurinBrechter/GraphTheory/tree/main/rdl处查看对应的笔记本文件。
训练结果(作者提供图片)
因此,我们可以训练这个GNN,使其r2达到0.3左右,MAE达到500。这意味着,它预测卖家在未来30天的收入,平均误差为+-500美元。当然,我们不知道这是好是坏,也许通过经典机器学习和特征工程的结合,我们可以得到80%的r2。
结论
关系型深度学习是一种有趣的机器学习新方法,特别是当我们有一个复杂的关系模式时,手动特征工程太费力了。它使我们能够仅使用SQL定义ML任务,这对于那些不深入研究数据科学但仅了解一些SQL的人来说尤其有用。这也意味着,我们可以快速迭代,并对不同的任务进行大量实验。
同时,这种方法也存在自己的问题,例如训练GNN和从关系模式构建图存在不少困难。此外,还有一个问题是,RDL在性能方面能在多大程度上与经典ML模型竞争。过去,我们已经看到,在表格预测问题上,XGboost等模型已被证明比神经网络更好。
参考文献
【1】Robinson,Joshua等人,《RelBench:关系数据库深度学习的基准》,arXiv,2024,https://arxiv.org/abs/2407.20060。
【2】Fey、Matthias等人,《关系深度学习:关系数据库上的图表示学习》,arXiv预印本arXiv:2312.04615(2023)。
【3】Schlichtkrull,Michael等人。《用图卷积网络建模关系数据》,语义网:第15届国际会议,2018年ESWC,希腊克里特岛伊拉克利翁,2018年6月3日至7日,会议记录#15。施普林格国际出版社,2018年。
译者介绍
朱先忠,51CTO社区编辑,51CTO专家博客、讲师,潍坊一所高校计算机教师,自由编程界老兵一枚。
原文标题:Self-Service ML with Relational Deep Learning,作者:Laurin Brechter