Skip to content

Commit a442688

Browse files
PGijsbersBilgecelikmarcoslbuenosahithyaraviNeeratyoy
authored
Feature #753 (#932)
* Create first section: Creating Custom Flow * Add Section: Using the Flow It is incomplete as while trying to explain how to format the predictions, I realized a utility function is required. * Allow run description text to be custom Previously the description text that accompanies the prediction file was auto-generated with the assumption that the corresponding flow had an extension. To support custom flows (with no extension), this behavior had to be changed. The description can now be passed on initialization. The description describing it was auto generated from run_task is now correctly only added if the run was generated through run_flow_on_task. * Draft for Custom Flow tutorial * Add minimal docstring to OpenMLRun I am not for each field what the specifications are. * Process code review feedback In particular: - text changes - fetch true labels from the dataset instead * Use the format utility function in automatic runs To format the predictions. * Process @mfeurer feedback * Rename arguments of list_evaluations (#933) * list evals name change * list evals - update * adding config file to user guide (#931) * adding config file to user guide * finished requested changes * Edit api (#935) * version1 * minor fixes * tests * reformat code * check new version * remove get data * code format * review comments * fix duplicate * type annotate * example * tests for exceptions * fix pep8 * black format * Adding support for scikit-learn > 0.22 (#936) * Preliminary changes * Updating unit tests for sklearn 0.22 and above * Triggering sklearn tests + fixes * Refactoring to inspect.signature in extensions * Add flake8-print in pre-commit (#939) * Add flake8-print in pre-commit config * Replace print statements with logging * Fix edit api (#940) * fix edit api * Update subflow paragraph * Check the ClassificationTask has class label set * Test task is of supported type * Add tests for format_prediction * Adding Python 3.8 support (#916) * Adding Python 3.8 support * Fixing indentation * Execute test cases for 3.8 * Testing * Making install script fail * Process feedback Neeratyoy * Test Exception with Regex Also throw NotImplementedError instead of TypeError for unsupported task types. Added links in the example. * change edit_api to reflect server (#941) * change edit_api to reflect server * change test and example to reflect rest API changes * tutorial comments * Update datasets_tutorial.py * Create first section: Creating Custom Flow * Add Section: Using the Flow It is incomplete as while trying to explain how to format the predictions, I realized a utility function is required. * Allow run description text to be custom Previously the description text that accompanies the prediction file was auto-generated with the assumption that the corresponding flow had an extension. To support custom flows (with no extension), this behavior had to be changed. The description can now be passed on initialization. The description describing it was auto generated from run_task is now correctly only added if the run was generated through run_flow_on_task. * Draft for Custom Flow tutorial * Add minimal docstring to OpenMLRun I am not for each field what the specifications are. * Process code review feedback In particular: - text changes - fetch true labels from the dataset instead * Use the format utility function in automatic runs To format the predictions. * Process @mfeurer feedback * Update subflow paragraph * Check the ClassificationTask has class label set * Test task is of supported type * Add tests for format_prediction * Process feedback Neeratyoy * Test Exception with Regex Also throw NotImplementedError instead of TypeError for unsupported task types. Added links in the example. Co-authored-by: Bilgecelik <[email protected]> Co-authored-by: marcoslbueno <[email protected]> Co-authored-by: Sahithya Ravi <[email protected]> Co-authored-by: Neeratyoy Mallik <[email protected]> Co-authored-by: zikun <[email protected]>
1 parent f70c720 commit a442688

File tree

4 files changed

+375
-31
lines changed

4 files changed

+375
-31
lines changed
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
"""
2+
================================
3+
Creating and Using a Custom Flow
4+
================================
5+
6+
The most convenient way to create a flow for your machine learning workflow is to generate it
7+
automatically as described in the `Obtain Flow IDs <https://openml.github.io/openml-python/master/examples/30_extended/flow_id_tutorial.html#sphx-glr-examples-30-extended-flow-id-tutorial-py>`_ tutorial. # noqa E501
8+
However, there are scenarios where this is not possible, such
9+
as when the flow uses a framework without an extension or when the flow is described by a script.
10+
11+
In those cases you can still create a custom flow by following the steps of this tutorial.
12+
As an example we will use the flows generated for the `AutoML Benchmark <https://openml.github.io/automlbenchmark/>`_,
13+
and also show how to link runs to the custom flow.
14+
"""
15+
16+
####################################################################################################
17+
18+
# License: BSD 3-Clause
19+
# .. warning:: This example uploads data. For that reason, this example
20+
# connects to the test server at test.openml.org. This prevents the main
21+
# server from crowding with example datasets, tasks, runs, and so on.
22+
from collections import OrderedDict
23+
import numpy as np
24+
25+
import openml
26+
from openml import OpenMLClassificationTask
27+
from openml.runs.functions import format_prediction
28+
29+
openml.config.start_using_configuration_for_example()
30+
31+
####################################################################################################
32+
# 1. Defining the flow
33+
# ====================
34+
# The first step is to define all the hyperparameters of your flow.
35+
# The API pages feature a descriptions of each variable of the `OpenMLFlow <https://openml.github.io/openml-python/master/generated/openml.OpenMLFlow.html#openml.OpenMLFlow>`_. # noqa E501
36+
# Note that `external version` and `name` together uniquely identify a flow.
37+
#
38+
# The AutoML Benchmark runs AutoML systems across a range of tasks.
39+
# OpenML stores Flows for each AutoML system. However, the AutoML benchmark adds
40+
# preprocessing to the flow, so should be described in a new flow.
41+
#
42+
# We will break down the flow arguments into several groups, for the tutorial.
43+
# First we will define the name and version information.
44+
# Make sure to leave enough information so others can determine exactly which
45+
# version of the package/script is used. Use tags so users can find your flow easily.
46+
47+
general = dict(
48+
name="automlbenchmark_autosklearn",
49+
description=(
50+
"Auto-sklearn as set up by the AutoML Benchmark"
51+
"Source: https://github.com/openml/automlbenchmark/releases/tag/v0.9"
52+
),
53+
external_version="amlb==0.9",
54+
language="English",
55+
tags=["amlb", "benchmark", "study_218"],
56+
dependencies="amlb==0.9",
57+
)
58+
59+
####################################################################################################
60+
# Next we define the flow hyperparameters. We define their name and default value in `parameters`,
61+
# and provide meta-data for each hyperparameter through `parameters_meta_info`.
62+
# Note that even though the argument name is `parameters` they describe the hyperparameters.
63+
# The use of ordered dicts is required.
64+
65+
flow_hyperparameters = dict(
66+
parameters=OrderedDict(time="240", memory="32", cores="8"),
67+
parameters_meta_info=OrderedDict(
68+
cores=OrderedDict(description="number of available cores", data_type="int"),
69+
memory=OrderedDict(description="memory in gigabytes", data_type="int"),
70+
time=OrderedDict(description="time in minutes", data_type="int"),
71+
),
72+
)
73+
74+
####################################################################################################
75+
# It is possible to build a flow which uses other flows.
76+
# For example, the Random Forest Classifier is a flow, but you could also construct a flow
77+
# which uses a Random Forest Classifier in a ML pipeline. When constructing the pipeline flow,
78+
# you can use the Random Forest Classifier flow as a *subflow*. It allows for
79+
# all hyperparameters of the Random Classifier Flow to also be specified in your pipeline flow.
80+
#
81+
# In this example, the auto-sklearn flow is a subflow: the auto-sklearn flow is entirely executed as part of this flow.
82+
# This allows people to specify auto-sklearn hyperparameters used in this flow.
83+
# In general, using a subflow is not required.
84+
#
85+
# Note: flow 15275 is not actually the right flow on the test server,
86+
# but that does not matter for this demonstration.
87+
88+
autosklearn_flow = openml.flows.get_flow(15275) # auto-sklearn 0.5.1
89+
subflow = dict(components=OrderedDict(automl_tool=autosklearn_flow),)
90+
91+
####################################################################################################
92+
# With all parameters of the flow defined, we can now initialize the OpenMLFlow and publish.
93+
# Because we provided all the details already, we do not need to provide a `model` to the flow.
94+
#
95+
# In our case, we don't even have a model. It is possible to have a model but still require
96+
# to follow these steps when the model (python object) does not have an extensions from which
97+
# to automatically extract the hyperparameters.
98+
# So whether you have a model with no extension or no model at all, explicitly set
99+
# the model of the flow to `None`.
100+
101+
autosklearn_amlb_flow = openml.flows.OpenMLFlow(
102+
**general, **flow_hyperparameters, **subflow, model=None,
103+
)
104+
autosklearn_amlb_flow.publish()
105+
print(f"autosklearn flow created: {autosklearn_amlb_flow.flow_id}")
106+
107+
####################################################################################################
108+
# 2. Using the flow
109+
# ====================
110+
# This Section will show how to upload run data for your custom flow.
111+
# Take care to change the values of parameters as well as the task id,
112+
# to reflect the actual run.
113+
# Task and parameter values in the example are fictional.
114+
115+
flow_id = autosklearn_amlb_flow.flow_id
116+
117+
parameters = [
118+
OrderedDict([("oml:name", "cores"), ("oml:value", 4), ("oml:component", flow_id)]),
119+
OrderedDict([("oml:name", "memory"), ("oml:value", 16), ("oml:component", flow_id)]),
120+
OrderedDict([("oml:name", "time"), ("oml:value", 120), ("oml:component", flow_id)]),
121+
]
122+
123+
task_id = 1408 # Iris Task
124+
task = openml.tasks.get_task(task_id)
125+
dataset_id = task.get_dataset().dataset_id
126+
127+
128+
####################################################################################################
129+
# The last bit of information for the run we need are the predicted values.
130+
# The exact format of the predictions will depend on the task.
131+
#
132+
# The predictions should always be a list of lists, each list should contain:
133+
# - the repeat number: for repeated evaluation strategies. (e.g. repeated cross-validation)
134+
# - the fold number: for cross-validation. (what should this be for holdout?)
135+
# - 0: this field is for backward compatibility.
136+
# - index: the row (of the original dataset) for which the prediction was made.
137+
# - p_1, ..., p_c: for each class the predicted probability of the sample
138+
# belonging to that class. (no elements for regression tasks)
139+
# Make sure the order of these elements follows the order of `task.class_labels`.
140+
# - the predicted class/value for the sample
141+
# - the true class/value for the sample
142+
#
143+
# When using openml-python extensions (such as through `run_model_on_task`),
144+
# all of this formatting is automatic.
145+
# Unfortunately we can not automate this procedure for custom flows,
146+
# which means a little additional effort is required.
147+
#
148+
# Here we generated some random predictions in place.
149+
# You can ignore this code, or use it to better understand the formatting of the predictions.
150+
#
151+
# Find the repeats/folds for this task:
152+
n_repeats, n_folds, _ = task.get_split_dimensions()
153+
all_test_indices = [
154+
(repeat, fold, index)
155+
for repeat in range(n_repeats)
156+
for fold in range(n_folds)
157+
for index in task.get_train_test_split_indices(fold, repeat)[1]
158+
]
159+
160+
# random class probabilities (Iris has 150 samples and 3 classes):
161+
r = np.random.rand(150 * n_repeats, 3)
162+
# scale the random values so that the probabilities of each sample sum to 1:
163+
y_proba = r / r.sum(axis=1).reshape(-1, 1)
164+
y_pred = y_proba.argmax(axis=1)
165+
166+
class_map = dict(zip(range(3), task.class_labels))
167+
_, y_true = task.get_X_and_y()
168+
y_true = [class_map[y] for y in y_true]
169+
170+
# We format the predictions with the utility function `format_prediction`.
171+
# It will organize the relevant data in the expected format/order.
172+
predictions = []
173+
for where, y, yp, proba in zip(all_test_indices, y_true, y_pred, y_proba):
174+
repeat, fold, index = where
175+
176+
prediction = format_prediction(
177+
task=task,
178+
repeat=repeat,
179+
fold=fold,
180+
index=index,
181+
prediction=class_map[yp],
182+
truth=y,
183+
proba={c: pb for (c, pb) in zip(task.class_labels, proba)},
184+
)
185+
predictions.append(prediction)
186+
187+
####################################################################################################
188+
# Finally we can create the OpenMLRun object and upload.
189+
# We use the argument setup_string because the used flow was a script.
190+
191+
benchmark_command = f"python3 runbenchmark.py auto-sklearn medium -m aws -t 119"
192+
my_run = openml.runs.OpenMLRun(
193+
task_id=task_id,
194+
flow_id=flow_id,
195+
dataset_id=dataset_id,
196+
parameter_settings=parameters,
197+
setup_string=benchmark_command,
198+
data_content=predictions,
199+
tags=["study_218"],
200+
description_text="Run generated by the Custom Flow tutorial.",
201+
)
202+
my_run.publish()
203+
print("run created:", my_run.run_id)
204+
205+
openml.config.stop_using_configuration_for_example()

openml/runs/functions.py

Lines changed: 86 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import io
55
import itertools
66
import os
7+
import time
78
from typing import Any, List, Dict, Optional, Set, Tuple, Union, TYPE_CHECKING # noqa F401
89
import warnings
910

@@ -250,7 +251,8 @@ def run_flow_on_task(
250251
)
251252

252253
data_content, trace, fold_evaluations, sample_evaluations = res
253-
254+
fields = [*run_environment, time.strftime("%c"), "Created by run_flow_on_task"]
255+
generated_description = "\n".join(fields)
254256
run = OpenMLRun(
255257
task_id=task.task_id,
256258
flow_id=flow_id,
@@ -262,6 +264,7 @@ def run_flow_on_task(
262264
data_content=data_content,
263265
flow=flow,
264266
setup_string=flow.extension.create_setup_string(flow.model),
267+
description_text=generated_description,
265268
)
266269

267270
if (upload_flow or avoid_duplicate_runs) and flow.flow_id is not None:
@@ -478,13 +481,17 @@ def _calculate_local_measure(sklearn_fn, openml_name):
478481

479482
for i, tst_idx in enumerate(test_indices):
480483

481-
arff_line = [rep_no, fold_no, sample_no, tst_idx] # type: List[Any]
482484
if task.class_labels is not None:
483-
for j, class_label in enumerate(task.class_labels):
484-
arff_line.append(proba_y[i][j])
485-
486-
arff_line.append(task.class_labels[pred_y[i]])
487-
arff_line.append(task.class_labels[test_y[i]])
485+
arff_line = format_prediction(
486+
task=task,
487+
repeat=rep_no,
488+
fold=fold_no,
489+
sample=sample_no,
490+
index=tst_idx,
491+
prediction=task.class_labels[pred_y[i]],
492+
truth=task.class_labels[test_y[i]],
493+
proba=dict(zip(task.class_labels, proba_y[i])),
494+
)
488495
else:
489496
raise ValueError("The task has no class labels")
490497

@@ -498,7 +505,15 @@ def _calculate_local_measure(sklearn_fn, openml_name):
498505
elif isinstance(task, OpenMLRegressionTask):
499506

500507
for i in range(0, len(test_indices)):
501-
arff_line = [rep_no, fold_no, test_indices[i], pred_y[i], test_y[i]]
508+
arff_line = format_prediction(
509+
task=task,
510+
repeat=rep_no,
511+
fold=fold_no,
512+
index=test_indices[i],
513+
prediction=pred_y[i],
514+
truth=test_y[i],
515+
)
516+
502517
arff_datacontent.append(arff_line)
503518

504519
if add_local_measures:
@@ -815,7 +830,7 @@ def list_runs(
815830
study: Optional[int] = None,
816831
display_errors: bool = False,
817832
output_format: str = "dict",
818-
**kwargs
833+
**kwargs,
819834
) -> Union[Dict, pd.DataFrame]:
820835
"""
821836
List all runs matching all of the given filters.
@@ -887,7 +902,7 @@ def list_runs(
887902
tag=tag,
888903
study=study,
889904
display_errors=display_errors,
890-
**kwargs
905+
**kwargs,
891906
)
892907

893908

@@ -900,7 +915,7 @@ def _list_runs(
900915
study: Optional[int] = None,
901916
display_errors: bool = False,
902917
output_format: str = "dict",
903-
**kwargs
918+
**kwargs,
904919
) -> Union[Dict, pd.DataFrame]:
905920
"""
906921
Perform API call `/run/list/{filters}'
@@ -1004,3 +1019,63 @@ def __list_runs(api_call, output_format="dict"):
10041019
runs = pd.DataFrame.from_dict(runs, orient="index")
10051020

10061021
return runs
1022+
1023+
1024+
def format_prediction(
1025+
task: OpenMLSupervisedTask,
1026+
repeat: int,
1027+
fold: int,
1028+
index: int,
1029+
prediction: Union[str, int, float],
1030+
truth: Union[str, int, float],
1031+
sample: Optional[int] = None,
1032+
proba: Optional[Dict[str, float]] = None,
1033+
) -> List[Union[str, int, float]]:
1034+
""" Format the predictions in the specific order as required for the run results.
1035+
1036+
Parameters
1037+
----------
1038+
task: OpenMLSupervisedTask
1039+
Task for which to format the predictions.
1040+
repeat: int
1041+
From which repeat this predictions is made.
1042+
fold: int
1043+
From which fold this prediction is made.
1044+
index: int
1045+
For which index this prediction is made.
1046+
prediction: str, int or float
1047+
The predicted class label or value.
1048+
truth: str, int or float
1049+
The true class label or value.
1050+
sample: int, optional (default=None)
1051+
From which sample set this prediction is made.
1052+
Required only for LearningCurve tasks.
1053+
proba: Dict[str, float], optional (default=None)
1054+
For classification tasks only.
1055+
A mapping from each class label to their predicted probability.
1056+
The dictionary should contain an entry for each of the `task.class_labels`.
1057+
E.g.: {"Iris-Setosa": 0.2, "Iris-Versicolor": 0.7, "Iris-Virginica": 0.1}
1058+
1059+
Returns
1060+
-------
1061+
A list with elements for the prediction results of a run.
1062+
1063+
"""
1064+
if isinstance(task, OpenMLClassificationTask):
1065+
if proba is None:
1066+
raise ValueError("`proba` is required for classification task")
1067+
if task.class_labels is None:
1068+
raise ValueError("The classification task must have class labels set")
1069+
if not set(task.class_labels) == set(proba):
1070+
raise ValueError("Each class should have a predicted probability")
1071+
if sample is None:
1072+
if isinstance(task, OpenMLLearningCurveTask):
1073+
raise ValueError("`sample` can not be none for LearningCurveTask")
1074+
else:
1075+
sample = 0
1076+
probabilities = [proba[c] for c in task.class_labels]
1077+
return [repeat, fold, sample, index, *probabilities, truth, prediction]
1078+
elif isinstance(task, OpenMLRegressionTask):
1079+
return [repeat, fold, index, truth, prediction]
1080+
else:
1081+
raise NotImplementedError(f"Formatting for {type(task)} is not supported.")

0 commit comments

Comments
 (0)