在我的很多项目中,我使用luigi作为流水线工具。这让我想到使用它来实现参数搜索。标准luigi.file.LocalTarget
有一个非常天真的方法来处理参数,这也在文档的例子中显示:
def output(self):
return luigi.LocalTarget("data/artist_streams_%s.tsv" % self.date_interval)
即,参数保存在文件名中。这使得检查是否已经计算某个参数组合变得容易。一旦任务的参数更复杂,这就会变得混乱。
以下是参数搜索的简单概念:
import luigi
class Sum(luigi.Task):
long_ = luigi.Parameter()
list_ = luigi.Parameter()
of = luigi.Parameter()
parameters = luigi.Parameter()
def output(self):
return luigi.LocalTarget("task{}_{}_{}_{}.txt".format(self.long_,
self.list_,
self.of,
self.parameters))
def run(self):
sum_ = self.long_ + self.list_ + self.of + self.parameters
with self.output().open('w') as out_file:
out_file.write(str(sum_))
class ParameterSearch(luigi.Task):
def requires(self):
list_of_parameter_combinations = [
{
"long_" : 1,
"list_" : 2,
"of" : 3,
"parameters" : 4
},{
"long_" : 5,
"list_" : 6,
"of" : 7,
"parameters" : 8
}
]
for pc in list_of_parameter_combinations:
yield Sum(**pc)
当然,在这个例子中,所有四个参数都可以在文件名中编码,但它不需要太多的幻想,这种方法可以达到边界。想想类似于数组的参数。
我的后续想法是将参数和结果存储在某种信封对象中,然后可以将其保存为目标。 然后,文件名可以是第一次模糊搜索的参数的某种散列。
有信封类
class Envelope(object):
@classmethod
def hashify(cls, params):
return hash(frozenset(params.items()))
def __init__(self, result, **params):
self.params = {}
for k in params:
self.params[k] = params.get(k)
def hash(self):
return Envelope.hashify(self.params)
然后,有新的Target,它增强了LocalTarget,并能够检查信封内的所有参数是否匹配:
class EnvelopedTarget(luigi.file.LocalTarget):
fs = luigi.file.LocalFileSystem()
def __init__(self, params, path=None, format=None, is_tmp=False):
self.path = path
self.params = params
if format is None:
format = luigi.file.get_default_format()
if not path:
if not is_tmp:
raise Exception('path or is_tmp must be set')
path = os.path.join(tempfile.gettempdir(), 'luigi-tmp-%09d' % random.randint(0, 999999999))
super(EnvelopedTarget, self).__init__(path)
self.format = format
self.is_tmp = is_tmp
def exists(self):
path = self.path
if '*' in path or '?' in path or '[' in path or '{' in path:
logger.warning("Using wildcards in path %s might lead to processing of an incomplete dataset; "
"override exists() to suppress the warning.", path)
if self.fs.exists(path):
with self.open() as fin:
envelope = pickle.load(fin)
try:
assert len(envelope.params) == len(self.params)
for param,paramval in self.params.items():
assert paramval == envelope.params.get(param)
except(AssertionError):
return False
return True
else:
return False
这里的问题是,使用这个目标增加了一些原本是luigi旨在最小化的样板。我设置了一个新的基本任务
class BaseTask(luigi.Task):
def output(self, envelope):
path = '{}{}.txt'.format(type(self).__name__, envelope.hash())
params = envelope.params
return EnvelopedTarget(params, path=path)
def complete(self):
envelope = Envelope(None, **self.param_kwargs)
outputs = flatten(self.output(envelope))
if len(outputs) == 0:
warnings.warn(
"Task %r without outputs has no custom complete() method" % self,
stacklevel=2
)
return False
return all(map(lambda output: output.exists(), outputs))
def run(self):
result, outparams = self.my_run()
envelope = Envelope(result, **outparams)
with self.output(envelope).open('w') as fout:
pickle.dump(envelope, fout)
结果EnvelopedSum
任务将非常小:
class EnvelopedSum(BaseTask):
long_ = luigi.Parameter()
list_ = luigi.Parameter()
of = luigi.Parameter()
parameters = luigi.Parameter()
def my_run(self):
return sum(self.param_kwargs.values()), self.param_kwargs
此任务可以在开始时以与Sum
任务相同的方式运行。
注意:这个如何包含luigi-task-results的示例实现远非稳定,而是通过包含结果和参数来说明我的意思。
我的问题是:在luigi中有没有更简单的方法来处理很多复杂的参数?
后续问题:有没有人考虑过保存已执行参数搜索的代码版本(和/或子工具包版本)的记录?
也欢迎任何有关此主题阅读内容的评论。
注意:
您可能需要一些导入才能使其运行:
from luigi.task import flatten
import warnings
import pickle
答案 0 :(得分:2)
您可以在邮件列表上更好地回复此类提案。 Luigi任务代码已经生成了参数的MD5哈希,以生成您可以获取的唯一任务标识符。
# task_id is a concatenation of task family, the first values of the first 3 parameters
# sorted by parameter name and a md5hash of the family/parameters as a cananocalised json.
param_str = json.dumps(params, separators=(',', ':'), sort_keys=True)
param_hash = hashlib.md5(param_str.encode('utf-8')).hexdigest()