本文将探讨直接在关系数据库上执行机器学习的新方法——关系型深度学习。
本文示例项目数据集的关系模式(作者提供图片)
在本文中,我们将深入探讨一种有趣的深度学习(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等模型已被证明比神经网络更好。