优化模型的超参数

优化模型的最简单方法是使用pykeen.hpo.hpo_pipeline()函数。

以下所有示例都是关于在pykeen.datasets.Nations数据集上训练pykeen.models.TransE时获取最佳模型的。 每个示例都提供了一些关于hpo_pipeline()函数使用的见解。

超参数优化的最基本用法是指定数据集、模型以及运行次数。以下示例展示了如何使用n_trials参数在Nations数据集上优化TransE模型。

>>> from pykeen.hpo import hpo_pipeline
>>> hpo_pipeline_result = hpo_pipeline(
...     n_trials=30,
...     dataset='Nations',
...     model='TransE',
... )

或者,可以设置timeout。在以下示例中,将在60秒内运行尽可能多的试验。

>>> from pykeen.hpo import hpo_pipeline
>>> hpo_pipeline_result = hpo_pipeline(
...     timeout=60,
...     dataset='Nations',
...     model='TransE',
... )

超参数优化管道能够优化对应pykeen.pipeline.pipeline()中的*_kwargs参数的超参数:

  • model

  • loss

  • regularizer

  • optimizer

  • negative_sampler

  • training

默认值

每个组件的超参数都有合理的默认值。例如,PyKEEN中的每个模型都有从其原始论文中报告的最佳值中选择的超参数默认值,除非在模型的参考页面上另有说明。如果某个模型在特定数据集上的超参数不可用,我们会根据我们的大规模基准测试中的发现选择超参数[ali2020a]。对于大多数组件(例如,模型、损失函数、正则化器、负样本、训练循环),这些值存储在各自类的__init__()函数的默认值中。它们可以在文档的相应参考部分中查看。

一些组件包含进行超参数优化的策略。当你调用 pykeen.hpo.hpo_pipeline()时,将采取以下步骤来确定每个组件中每个超参数的处理方式:

  1. 如果传递了一个明确的值,就使用它。

  2. 如果没有传递显式值并且传递了HPO策略,则使用显式策略。

  3. 如果没有传递显式值且没有传递HPO策略,并且存在默认的HPO策略,则使用默认策略。

  4. 如果没有传递明确的值,没有传递HPO策略,并且没有默认的HPO策略,则使用默认的超参数值

  5. 如果没有传递显式值,没有传递HPO策略,并且没有默认的HPO策略,也没有默认的超参数值,则引发TypeError

例如,TransE模型对其embedding_dim参数的默认HPO策略是在\([16, 256]\)之间以16为步长进行搜索。\(l_p\)范数设置为搜索1或2。在以下代码中,这将用50覆盖:

>>> from pykeen.hpo import hpo_pipeline
>>> hpo_pipeline_result = hpo_pipeline(
...     dataset='Nations',
...     model='TransE',
...     model_kwargs=dict(embedding_dim=50),
... )

该策略可以通过以下方式明确覆盖:

>>> from pykeen.hpo import hpo_pipeline
>>> hpo_pipeline_result = hpo_pipeline(
...     dataset='Nations',
...     model='TransE',
...     model_kwargs_ranges=dict(
...         embedding_dim=dict(type=int, low=16, high=256, step=32),
...     ),
... )

每个模型、损失函数、正则化器、负采样器和训练循环都指定了一个名为hpo_defaults的类变量,其中包含一个包含所有默认策略的字典。它们的键与各自__init__()函数中的参数相匹配。

由于优化器在PyKEEN中没有重新实现,因此在pykeen.optimizers.optimizers_hpo_defaults中有一个特定的字典包含它们的策略。是否应该优化优化器(yo dawg)是有争议的,因此你可以选择将学习率lr设置为一个常数值。

策略

HPO策略是一个Python dict,其中包含一个type键,对应于分类变量、布尔变量、整数变量或浮点数变量。type的值本身应为以下之一:

  1. "categorical"

  2. bool"bool"

  3. int"int"

  4. float"float"

可以将几种策略组合在一个字典中,其中键是组件在HPO管道的*_kwargs_ranges参数中的超参数名称。

分类

在分类变量中使用的另一个关键键是 choices。例如,如果你想在KG2E模型中选择使用Kullback-Leibler散度或期望似然作为相似度,你可以编写如下策略:

>>> from pykeen.hpo import hpo_pipeline
>>> hpo_pipeline_result = hpo_pipeline(
...     dataset='Nations',
...     model='KG2E',
...     model_kwargs_ranges=dict(
...         dist_similarity=dict(type='categorical', choices=['KL', 'EL']),
...     ),
... )

布尔值

布尔变量实际上不需要除了类型之外的任何额外键,因此布尔变量的策略总是看起来像 dict(type='bool')。在底层,这自动转换为一个分类变量,其 choices=[True, False]

>>> from pykeen.hpo import hpo_pipeline
>>> hpo_pipeline_result = hpo_pipeline(
...     dataset='Nations',
...     model='TransE',
...     training_loop='sLCWA',
...     negative_sampler_kwargs_ranges=dict(
...         filtered=dict(type=boolean),
...     ),
... )

整数和浮点数

整数和浮点数策略有几个共同点。两者都需要一个lowhigh条目,例如在dict(type=float, low=0.0, high=1.0)dict(type=int, low=1, high=10)中。

线性比例

默认情况下,您不需要指定scale,但您可以通过设置scale='linear'来明确指定。 这种行为应该是自解释的——没有重新缩放,您将在lowhigh参数指定的范围内获得均匀分布。 这适用于type=inttype=float。以下示例从[1,100]中均匀选择:

>>> from pykeen.hpo import hpo_pipeline
>>> hpo_pipeline_result = hpo_pipeline(
...     dataset='Nations',
...     model='TransE',
...     training_loop='sLCWA',
...     negative_sampler_kwargs_ranges=dict(
...         num_negs_per_pos=dict(type=int, low=1, high=100),
...     ),
... )

功率刻度(仅限type=int

功率尺度最初被实现为scale='power_two',以支持pykeen.models.ConvEoutput_channels参数。然而,使用二作为基数有些限制,因此我们还实现了一个更通用的scale='power',其中你可以设置base。这里有一个使用base=10来优化每个正例的负例比例的示例:

>>> from pykeen.hpo import hpo_pipeline
>>> hpo_pipeline_result = hpo_pipeline(
...     dataset='Nations',
...     model='TransE',
...     training_loop='sLCWA',
...     negative_sampler_kwargs_ranges=dict(
...         num_negs_per_pos=dict(type=int, scale='power', base=10, low=0, high=2),
...     ),
... )

功率尺度只能与type=int一起使用,不能与布尔型、分类型或浮点型一起使用。我喜欢这个尺度,因为它可以快速离散化一个大的搜索空间。在这个例子中,你将得到[10**0, 10**1, 10**2]作为选择,然后从中均匀选择。

对数重加权

线性尺度上的对数重加权是幂尺度的邪恶双胞胎。这适用于type=inttype=float。与改变选择本身不同,对数尺度使用Optuna内置的log功能 在对数分布上均匀地重新分配概率。上面的相同示例可以通过以下方式实现:

>>> from pykeen.hpo import hpo_pipeline
>>> hpo_pipeline_result = hpo_pipeline(
...     dataset='Nations',
...     model='TransE',
...     training_loop='sLCWA',
...     negative_sampler_kwargs_ranges=dict(
...         num_negs_per_pos=dict(type=int, low=1, high=100, log=True),
...     ),
... )

但这次,它没有被离散化。然而,你从\([1,10]\)中选择的概率与从\([10, 100]\)中选择的概率相同。

步进

使用线性比例,您可以指定step大小。这将在线性空间中离散化分布,因此如果您想从\(10, 20, ... 100\)中选择,您可以这样做:

>>> from pykeen.hpo import hpo_pipeline
>>> hpo_pipeline_result = hpo_pipeline(
...     dataset='Nations',
...     model='TransE',
...     training_loop='sLCWA',
...     negative_sampler_kwargs_ranges=dict(
...         num_negs_per_pos=dict(type=int, low=10, high=100, step=10),
...     ),
... )

这实际上也适用于对数重加权,因为它在技术上仍然是在线性尺度上, 但是概率是对数重加权的。所以现在你会以相同的概率从\([10]\)\([20, 30, 40, ..., 100]\)中选择一个

>>> from pykeen.hpo import hpo_pipeline
>>> hpo_pipeline_result = hpo_pipeline(
...     dataset='Nations',
...     model='TransE',
...     training_loop='sLCWA',
...     negative_sampler_kwargs_ranges=dict(
...         num_negs_per_pos=dict(type=int, low=10, high=100, step=10, log=True),
...     ),
... )

自定义策略

虽然超参数的默认值是用Python语法编码的,用于每个模型的__init__()函数的默认值,但范围/比例可以在类变量pykeen.models.Model.hpo_default中找到。例如,TransE的嵌入维度的范围设置为在50到350之间以25为增量进行优化,在pykeen.models.TransE.hpo_default中。TransE还有一个评分函数范数,默认情况下将通过{1, 2}的分类选择进行优化。

注意

这些超参数范围被选择为基准数据集FB15k-237 / WN18RR的合理默认值。当使用不同的数据集时,这些范围可能不是最优的。

在您选择的模型的hpo_default中定义的所有超参数将默认进行优化。如果您对其中一个参数已经有满意的值,您可以使用model_kwargs属性来指定它。在以下示例中,TransE模型的embedding_dim固定为200,而其余参数将使用模型中预定义的HPO策略进行优化。对于TransE,这意味着评分函数的范数将被优化为1或2。

>>> from pykeen.hpo import hpo_pipeline
>>> hpo_pipeline_result = hpo_pipeline(
...     model='TransE',
...     model_kwargs=dict(
...         embedding_dim=200,
...     ),
...     dataset='Nations',
...     n_trials=30,
... )

如果你想为模型的超参数设置自己的HPO策略,你可以使用model_kwargs_ranges参数来实现。在下面的示例中,嵌入的搜索范围更大(lowhigh),但步长更大(q),以便搜索100、200、300、400和500。

>>> from pykeen.hpo import hpo_pipeline
>>> hpo_result = hpo_pipeline(
...     n_trials=30,
...     dataset='Nations',
...     model='TransE',
...     model_kwargs_ranges=dict(
...         embedding_dim=dict(type=int, low=100, high=500, q=100),
...     ),
... )

警告

如果给定的范围不能被步长整除,那么上限将被省略。

如果你想优化实体初始化器,你可以使用type='categorical'类型, 这需要一个带有选择列表的choices=[...]键。这适用于字符串、整数、浮点数等。

>>> from pykeen.hpo import hpo_pipeline
>>> hpo_result = hpo_pipeline(
...     n_trials=30,
...     dataset='Nations',
...     model='TransE',
...     model_kwargs_ranges=dict(
...         entity_initializer=dict(type='categorical', choices=[
...             'xavier_uniform',
...             'xavier_uniform_norm',
...             'uniform',
...         ]),
...     ),
... )

同样可以用于实体和关系的约束器、归一化器和正则化器。然而,不同的模型可能对初始化器、归一化器、约束器和正则化器有不同的命名,因为实体、关系或两者可能有多种表示形式。请查看您所需模型的文档页面,了解您可以优化的kwargs。

pykeen.nn.representation.initializers 的键可以作为初始化器以字符串形式传递,而 pykeen.nn.representation.constrainers 的键可以作为约束器以字符串形式传递。

HPO管道不支持对每个初始化器的超参数进行优化。如果您对此感兴趣,请考虑构建自己的消融研究管道。

优化损失

虽然每个模型都有自己的默认损失函数,但你可以像在pykeen.pipeline.pipeline()中一样明确指定一个损失函数。

>>> from pykeen.hpo import hpo_pipeline
>>> hpo_pipeline_result = hpo_pipeline(
...     n_trials=30,
...     dataset='Nations',
...     model='TransE',
...     loss='MarginRankingLoss',
... )

pykeen.pipeline.pipeline()文档中所述,每个模型在pykeen.models.Model.loss_default中指定其默认的损失函数。例如,TransE模型在pykeen.models.TransE.loss_default中定义了边际排序损失作为其默认值。

每个模型还指定了损失函数的默认超参数在 pykeen.models.Model.loss_default_kwargs。例如,DistMultLiteral 在pykeen.models.DistMultLiteral.loss_default_kwargs中明确将边距设置为0.0

与模型的超参数不同,模型不存储用于优化损失函数超参数的策略。预配置的策略存储在损失函数的类变量 pykeen.models.Loss.hpo_default 中。

然而,类似于你如何指定model_kwargs_ranges,你也可以明确指定loss_kwargs_ranges,如下例所示。

>>> from pykeen.hpo import hpo_pipeline
>>> hpo_pipeline_result = hpo_pipeline(
...     n_trials=30,
...     dataset='Nations',
...     model='TransE',
...     loss='MarginRankingLoss',
...     loss_kwargs_ranges=dict(
...         margin=dict(type=float, low=1.0, high=2.0),
...     ),
... )

优化负采样器

当使用随机局部封闭世界假设(sLCWA)训练方法进行训练时,会选择一个负采样器 (pykeen.sampling.NegativeSampler的子类)。 每个负采样器都有一个策略存储在pykeen.sampling.NegativeSampler.hpo_default中。

与模型和正则化器一样,指定negative_samplernegative_sampler_kwargsnegative_sampler_kwargs_ranges的规则是相同的。

优化优化器

兄弟,我听说你喜欢优化,所以我们在你的优化器周围放了一个优化器,这样你就可以在优化的同时进行优化。由于PyKEEN中使用的所有优化器都来自PyTorch的实现,它们显然没有hpo_defaults类变量。相反,每个优化器都有一个默认的优化策略,存储在pykeen.optimizers.optimizers_hpo_defaults中,就像损失函数的默认策略一样。

优化优化器 - 即学习率调度器

如果优化你的优化器对你来说还不够,你可以更进一步,使用学习率调度器(lr_scheduler)来改变优化器的学习率。例如,这在开始时使用更激进的学习率以快速取得进展,同时随着时间的推移降低学习率,使模型能够平稳地收敛到最佳状态,可能会很有用。

PyKEEN 允许你使用 PyTorch 提供的学习率调度器,你可以像在 pykeen.pipeline.pipeline() 中一样简单地指定它们。

>>> from pykeen.hpo import hpo_pipeline
>>> hpo_pipeline_result = hpo_pipeline(
...     dataset='Nations',
...     model='TransE',
...     lr_scheduler='ExponentialLR',
... )
>>> pipeline_result.save_to_directory('nations_transe')

与优化器不带有hpo_defaults类变量的方式相同,学习率调度器依赖于它们自己的优化策略,这些策略在pykeen.lr_schedulers.lr_schedulers_hpo_defaults中提供。如果你准备进一步探索,当然也可以通过lr_scheduler_kwargs_ranges关键字参数设置自己的范围,如下所示:

>>> from pykeen.hpo import hpo_pipeline
>>> hpo_pipeline_result = hpo_pipeline(
...     dataset='Nations',
...     model='TransE',
...     lr_scheduler='ExponentialLR',
...     lr_scheduler_kwargs_ranges=dict(
...         gamma=dict(type=float, low=0.8, high=1.0),
...     ),
... )
>>> pipeline_result.save_to_directory('nations_transe')

优化其他一切

在不失一般性的情况下,以下参数到pykeen.pipeline.pipeline() 有相应的*_kwargs*_kwargs_ranges

  • training_loop (仅限关键字参数,不包括kwargs_ranges)

  • evaluator

  • evaluation

早停

早停可以直接集成到optuna优化中。

重要的键是 stopper='early'stopper_kwargs。 当使用早停时,hpo_pipeline() 会自动处理添加适当的回调以与 optuna 交互。

>>> from pykeen.hpo import hpo_pipeline
>>> hpo_pipeline_result = hpo_pipeline(
...     n_trials=30,
...     dataset='Nations',
...     model='TransE',
...     stopper='early',
...     stopper_kwargs=dict(frequency=5, patience=2, relative_delta=0.002),
... )

这些stopper kwargs被选择以使示例运行得更快。您可能会想要使用不同的参数。

配置Optuna

选择搜索算法

因为PyKEEN的超参数优化管道由Optuna驱动,它可以直接使用Optuna内置的所有采样器,这些采样器列在optuna.samplers上,或者任何自定义的子类optuna.samplers.BaseSampler

默认情况下,PyKEEN 使用树结构 Parzen 估计器(TPE;optuna.samplers.TPESampler), 一种概率搜索算法。您可以使用 sampler 参数显式设置采样器 (不要与在 sLCWA 下训练时使用的负采样器混淆):

>>> from pykeen.hpo import hpo_pipeline
>>> from optuna.samplers import TPESampler
>>> hpo_pipeline_result = hpo_pipeline(
...     n_trials=30,
...     sampler=TPESampler,
...     dataset='Nations',
...     model='TransE',
... )

你也可以传递一个字符串,这样你就不必担心导入Optuna。PyKEEN知道采样器类总是以“Sampler”结尾,所以你可以传递“TPE”或“TPESampler”作为字符串。这是不区分大小写的。

>>> from pykeen.hpo import hpo_pipeline
>>> hpo_pipeline_result = hpo_pipeline(
...     n_trials=30,
...     sampler="tpe",
...     dataset='Nations',
...     model='TransE',
... )

也可以直接传递一个采样器实例:

>>> from pykeen.hpo import hpo_pipeline
>>> from optuna.samplers import TPESampler
>>> sampler = TPESampler(prior_weight=1.1)
>>> hpo_pipeline_result = hpo_pipeline(
...     n_trials=30,
...     sampler=sampler,
...     dataset='Nations',
...     model='TransE',
... )

如果您在基于JSON的配置环境中工作,您将无法像这样使用您想要的设置实例化采样器。作为解决方案,您可以通过sampler_kwargs参数传递关键字参数,并结合将采样器指定为字符串/类传递给HPO管道,如下所示:

>>> from pykeen.hpo import hpo_pipeline
>>> hpo_pipeline_result = hpo_pipeline(
...     n_trials=30,
...     sampler="tpe",
...     sampler_kwargs=dict(prior_weight=1.1),
...     dataset='Nations',
...     model='TransE',
... )

为了模拟大多数使用随机采样的超参数优化,请使用optuna.samplers.RandomSampler,如下所示:

>>> from pykeen.hpo import hpo_pipeline
>>> from optuna.samplers import RandomSampler
>>> hpo_pipeline_result = hpo_pipeline(
...     n_trials=30,
...     sampler=RandomSampler,
...     dataset='Nations',
...     model='TransE',
... )

可以使用optuna.samplers.GridSampler进行网格搜索。请注意,此采样器在其sampler_kwargs中需要一个额外的search_space参数,例如,

>>> from pykeen.hpo import hpo_pipeline
>>> from optuna.samplers import GridSampler
>>> hpo_pipeline_result = hpo_pipeline(
...     n_trials=30,
...     sampler=GridSampler,
...     sampler_kwargs=dict(
...         search_space={
...             "model.embedding_dim": [32, 64, 128],
...             "model.scoring_fct_norm": [1, 2],
...             "loss.margin": [1.0],
...             "optimizer.lr": [1.0e-03],
...             "negative_sampler.num_negs_per_pos": [32],
...             "training.num_epochs": [100],
...             "training.batch_size": [128],
...         },
...     ),
...     dataset='Nations',
...     model='TransE',
... )

还要注意的是,随着研究的超参数数量的增加,网格搜索的搜索空间迅速增长,因此网格搜索在寻找良好配置方面比其他搜索策略效率低,参见 https://jmlr.csail.mit.edu/papers/v13/bergstra12a.html

完整示例

上面的例子展示了每次一个设置的排列。本节有一些更完整的例子。

以下示例设置了优化器、损失、训练、负采样、评估和早停设置。

>>> from pykeen.hpo import hpo_pipeline
>>> hpo_pipeline_result = hpo_pipeline(
...     n_trials=30,
...     dataset='Nations',
...     model='TransE',
...     model_kwargs=dict(embedding_dim=20, scoring_fct_norm=1),
...     optimizer='SGD',
...     optimizer_kwargs=dict(lr=0.01),
...     loss='marginranking',
...     loss_kwargs=dict(margin=1),
...     training_loop='slcwa',
...     training_kwargs=dict(num_epochs=100, batch_size=128),
...     negative_sampler='basic',
...     negative_sampler_kwargs=dict(num_negs_per_pos=1),
...     evaluator_kwargs=dict(filtered=True),
...     evaluation_kwargs=dict(batch_size=128),
...     stopper='early',
...     stopper_kwargs=dict(frequency=5, patience=2, relative_delta=0.002),
... )

如果您有作为字典的配置:

>>> from pykeen.hpo import hpo_pipeline_from_config
>>> config = {
...     'optuna': dict(
...         n_trials=30,
...     ),
...     'pipeline': dict(
...         dataset='Nations',
...         model='TransE',
...         model_kwargs=dict(embedding_dim=20, scoring_fct_norm=1),
...         optimizer='SGD',
...         optimizer_kwargs=dict(lr=0.01),
...         loss='marginranking',
...         loss_kwargs=dict(margin=1),
...         training_loop='slcwa',
...         training_kwargs=dict(num_epochs=100, batch_size=128),
...         negative_sampler='basic',
...         negative_sampler_kwargs=dict(num_negs_per_pos=1),
...         evaluator_kwargs=dict(filtered=True),
...         evaluation_kwargs=dict(batch_size=128),
...         stopper='early',
...         stopper_kwargs=dict(frequency=5, patience=2, relative_delta=0.002),
...     )
... }
... hpo_pipeline_result = hpo_pipeline_from_config(config)

如果您在JSON文件中有一个配置(格式相同):

>>> import json
>>> config = {
...     'optuna': dict(
...         n_trials=30,
...     ),
...     'pipeline': dict(
...         dataset='Nations',
...         model='TransE',
...         model_kwargs=dict(embedding_dim=20, scoring_fct_norm=1),
...         optimizer='SGD',
...         optimizer_kwargs=dict(lr=0.01),
...         loss='marginranking',
...         loss_kwargs=dict(margin=1),
...         training_loop='slcwa',
...         training_kwargs=dict(num_epochs=100, batch_size=128),
...         negative_sampler='basic',
...         negative_sampler_kwargs=dict(num_negs_per_pos=1),
...         evaluator_kwargs=dict(filtered=True),
...         evaluation_kwargs=dict(batch_size=128),
...         stopper='early',
...         stopper_kwargs=dict(frequency=5, patience=2, relative_delta=0.002),
...     )
... }
... with open('config.json', 'w') as file:
...     json.dump(config, file, indent=2)
... hpo_pipeline_result = hpo_pipeline_from_path('config.json')