From 682e4ed480f6e995661c1ffbcf23e295ee13a48a Mon Sep 17 00:00:00 2001 From: Amy Unruh Date: Tue, 17 Mar 2020 12:02:41 -0700 Subject: [PATCH 1/9] initial commit for Tables KFP e2e example --- ml/automl/tables/kfp_e2e/README.md | 0 .../tables_component.py | 78 +++++++ .../tables_component.yaml | 146 ++++++++++++ .../tables_component.py | 87 +++++++ .../tables_component.yaml | 158 +++++++++++++ .../tables_eval_component.py | 199 ++++++++++++++++ .../tables_eval_component.yaml | 155 ++++++++++++ .../tables_eval_metrics_component.py | 220 ++++++++++++++++++ .../tables_eval_metrics_component.yaml | 208 +++++++++++++++++ .../deploy_model_for_tables/convert_oss.py | 60 +++++ .../exported_model_deploy.py | 65 ++++++ .../deploy_model_for_tables/instances.json | 58 +++++ .../model_serve_template.yaml | 51 ++++ .../tables_deploy_component.py | 76 ++++++ .../tables_deploy_component.yaml | 83 +++++++ .../tables_component.py | 99 ++++++++ .../tables_component.yaml | 147 ++++++++++++ .../tables_schema_component.py | 119 ++++++++++ .../tables_schema_component.yaml | 192 +++++++++++++++ .../model-service-launcher/Dockerfile | 49 ++++ .../model-service-launcher/build.sh | 31 +++ .../tables/kfp_e2e/tables_pipeline_caip.py | 151 ++++++++++++ .../tables/kfp_e2e/tables_pipeline_kf.py | 151 ++++++++++++ 23 files changed, 2583 insertions(+) create mode 100644 ml/automl/tables/kfp_e2e/README.md create mode 100644 ml/automl/tables/kfp_e2e/create_dataset_for_tables/tables_component.py create mode 100644 ml/automl/tables/kfp_e2e/create_dataset_for_tables/tables_component.yaml create mode 100644 ml/automl/tables/kfp_e2e/create_model_for_tables/tables_component.py create mode 100644 ml/automl/tables/kfp_e2e/create_model_for_tables/tables_component.yaml create mode 100644 ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_component.py create mode 100644 ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_component.yaml create mode 100644 ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_metrics_component.py create mode 100644 ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_metrics_component.yaml create mode 100644 ml/automl/tables/kfp_e2e/deploy_model_for_tables/convert_oss.py create mode 100644 ml/automl/tables/kfp_e2e/deploy_model_for_tables/exported_model_deploy.py create mode 100644 ml/automl/tables/kfp_e2e/deploy_model_for_tables/instances.json create mode 100644 ml/automl/tables/kfp_e2e/deploy_model_for_tables/model_serve_template.yaml create mode 100644 ml/automl/tables/kfp_e2e/deploy_model_for_tables/tables_deploy_component.py create mode 100644 ml/automl/tables/kfp_e2e/deploy_model_for_tables/tables_deploy_component.yaml create mode 100644 ml/automl/tables/kfp_e2e/import_data_from_bigquery/tables_component.py create mode 100644 ml/automl/tables/kfp_e2e/import_data_from_bigquery/tables_component.yaml create mode 100644 ml/automl/tables/kfp_e2e/import_data_from_bigquery/tables_schema_component.py create mode 100644 ml/automl/tables/kfp_e2e/import_data_from_bigquery/tables_schema_component.yaml create mode 100644 ml/automl/tables/kfp_e2e/tables_containers/model-service-launcher/Dockerfile create mode 100755 ml/automl/tables/kfp_e2e/tables_containers/model-service-launcher/build.sh create mode 100644 ml/automl/tables/kfp_e2e/tables_pipeline_caip.py create mode 100644 ml/automl/tables/kfp_e2e/tables_pipeline_kf.py diff --git a/ml/automl/tables/kfp_e2e/README.md b/ml/automl/tables/kfp_e2e/README.md new file mode 100644 index 0000000..e69de29 diff --git a/ml/automl/tables/kfp_e2e/create_dataset_for_tables/tables_component.py b/ml/automl/tables/kfp_e2e/create_dataset_for_tables/tables_component.py new file mode 100644 index 0000000..c494064 --- /dev/null +++ b/ml/automl/tables/kfp_e2e/create_dataset_for_tables/tables_component.py @@ -0,0 +1,78 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import NamedTuple + + +def automl_create_dataset_for_tables( + gcp_project_id: str, + gcp_region: str, + dataset_display_name: str, + api_endpoint: str = None, + tables_dataset_metadata: dict = {}, + # retry=None, #=google.api_core.gapic_v1.method.DEFAULT, + # timeout: float = None, #=google.api_core.gapic_v1.method.DEFAULT, + # metadata: dict = None, +) -> NamedTuple('Outputs', [('dataset_path', str), ('create_time', str), ('dataset_id', str)]): + '''automl_create_dataset_for_tables creates an empty Dataset for AutoML tables + ''' + import sys + import subprocess + subprocess.run([sys.executable, '-m', 'pip', 'install', 'googleapis-common-protos==1.6.0', '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) + subprocess.run([sys.executable, '-m', 'pip', 'install', 'google-cloud-automl==0.9.0', '--quiet', '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) + + import google + import logging + from google.api_core.client_options import ClientOptions + from google.cloud import automl_v1beta1 as automl + + logging.getLogger().setLevel(logging.INFO) # TODO: make level configurable + # TODO: we could instead check for region 'eu' and use 'eu-automl.googleapis.com:443'endpoint + # in that case, instead of requiring endpoint to be specified. + if api_endpoint: + client_options = ClientOptions(api_endpoint=api_endpoint) + client = automl.TablesClient(project=gcp_project_id, region=gcp_region, + client_options=client_options) + else: + client = automl.TablesClient(project=gcp_project_id, region=gcp_region) + + try: + # Create a dataset with the given display name + # TODO: not clear that description, timeout & retry args still supported?.. + dataset = client.create_dataset(dataset_display_name, metadata=tables_dataset_metadata) + # Log info about the created dataset + logging.info("Dataset name: {}".format(dataset.name)) + logging.info("Dataset id: {}".format(dataset.name.split("/")[-1])) + logging.info("Dataset display name: {}".format(dataset.display_name)) + logging.info("Dataset metadata:") + logging.info("\t{}".format(dataset.tables_dataset_metadata)) + logging.info("Dataset example count: {}".format(dataset.example_count)) + logging.info("Dataset create time:") + logging.info("\tseconds: {}".format(dataset.create_time.seconds)) + logging.info("\tnanos: {}".format(dataset.create_time.nanos)) + print(str(dataset)) + dataset_id = dataset.name.rsplit('/', 1)[-1] + return (dataset.name, str(dataset.create_time), dataset_id) + except google.api_core.exceptions.GoogleAPICallError as e: + logging.warn(e) + raise e # TODO: other exception? return values rather than raise exception? + + +# if __name__ == "__main__": +# automl_create_dataset_for_tables('aju-vtests2', 'us-central1', 'component_test2') + +if __name__ == '__main__': + import kfp + kfp.components.func_to_container_op(automl_create_dataset_for_tables, + output_component_file='tables_component.yaml', base_image='python:3.7') diff --git a/ml/automl/tables/kfp_e2e/create_dataset_for_tables/tables_component.yaml b/ml/automl/tables/kfp_e2e/create_dataset_for_tables/tables_component.yaml new file mode 100644 index 0000000..dbeba33 --- /dev/null +++ b/ml/automl/tables/kfp_e2e/create_dataset_for_tables/tables_component.yaml @@ -0,0 +1,146 @@ +name: Automl create dataset for tables +description: | + automl_create_dataset_for_tables creates an empty Dataset for AutoML tables +inputs: +- name: gcp_project_id + type: String +- name: gcp_region + type: String +- name: dataset_display_name + type: String +- name: api_endpoint + type: String + optional: true +- name: tables_dataset_metadata + type: JsonObject + default: '{}' + optional: true +outputs: +- name: dataset_path + type: String +- name: create_time + type: String +- name: dataset_id + type: String +implementation: + container: + image: python:3.7 + command: + - python3 + - -u + - -c + - | + from typing import NamedTuple + + def automl_create_dataset_for_tables( + gcp_project_id: str, + gcp_region: str, + dataset_display_name: str, + api_endpoint: str = None, + tables_dataset_metadata: dict = {}, + # retry=None, #=google.api_core.gapic_v1.method.DEFAULT, + # timeout: float = None, #=google.api_core.gapic_v1.method.DEFAULT, + # metadata: dict = None, + ) -> NamedTuple('Outputs', [('dataset_path', str), ('create_time', str), ('dataset_id', str)]): + '''automl_create_dataset_for_tables creates an empty Dataset for AutoML tables + ''' + import sys + import subprocess + subprocess.run([sys.executable, '-m', 'pip', 'install', 'googleapis-common-protos==1.6.0', '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) + subprocess.run([sys.executable, '-m', 'pip', 'install', 'google-cloud-automl==0.9.0', '--quiet', '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) + + import google + import logging + from google.api_core.client_options import ClientOptions + from google.cloud import automl_v1beta1 as automl + + logging.getLogger().setLevel(logging.INFO) # TODO: make level configurable + # TODO: we could instead check for region 'eu' and use 'eu-automl.googleapis.com:443'endpoint + # in that case, instead of requiring endpoint to be specified. + if api_endpoint: + client_options = ClientOptions(api_endpoint=api_endpoint) + client = automl.TablesClient(project=gcp_project_id, region=gcp_region, + client_options=client_options) + else: + client = automl.TablesClient(project=gcp_project_id, region=gcp_region) + + try: + # Create a dataset with the given display name + # TODO: not clear that description, timeout & retry args still supported?.. + dataset = client.create_dataset(dataset_display_name, metadata=tables_dataset_metadata) + # Log info about the created dataset + logging.info("Dataset name: {}".format(dataset.name)) + logging.info("Dataset id: {}".format(dataset.name.split("/")[-1])) + logging.info("Dataset display name: {}".format(dataset.display_name)) + logging.info("Dataset metadata:") + logging.info("\t{}".format(dataset.tables_dataset_metadata)) + logging.info("Dataset example count: {}".format(dataset.example_count)) + logging.info("Dataset create time:") + logging.info("\tseconds: {}".format(dataset.create_time.seconds)) + logging.info("\tnanos: {}".format(dataset.create_time.nanos)) + print(str(dataset)) + dataset_id = dataset.name.rsplit('/', 1)[-1] + return (dataset.name, str(dataset.create_time), dataset_id) + except google.api_core.exceptions.GoogleAPICallError as e: + logging.warn(e) + raise e # TODO: other exception? return values rather than raise exception? + + import json + def _serialize_str(str_value: str) -> str: + if not isinstance(str_value, str): + raise TypeError('Value "{}" has type "{}" instead of str.'.format(str(str_value), str(type(str_value)))) + return str_value + + import argparse + _parser = argparse.ArgumentParser(prog='Automl create dataset for tables', description='automl_create_dataset_for_tables creates an empty Dataset for AutoML tables\n') + _parser.add_argument("--gcp-project-id", dest="gcp_project_id", type=str, required=True, default=argparse.SUPPRESS) + _parser.add_argument("--gcp-region", dest="gcp_region", type=str, required=True, default=argparse.SUPPRESS) + _parser.add_argument("--dataset-display-name", dest="dataset_display_name", type=str, required=True, default=argparse.SUPPRESS) + _parser.add_argument("--api-endpoint", dest="api_endpoint", type=str, required=False, default=argparse.SUPPRESS) + _parser.add_argument("--tables-dataset-metadata", dest="tables_dataset_metadata", type=json.loads, required=False, default=argparse.SUPPRESS) + _parser.add_argument("----output-paths", dest="_output_paths", type=str, nargs=3) + _parsed_args = vars(_parser.parse_args()) + _output_files = _parsed_args.pop("_output_paths", []) + + _outputs = automl_create_dataset_for_tables(**_parsed_args) + + if not hasattr(_outputs, '__getitem__') or isinstance(_outputs, str): + _outputs = [_outputs] + + _output_serializers = [ + _serialize_str, + _serialize_str, + _serialize_str + ] + + import os + for idx, output_file in enumerate(_output_files): + try: + os.makedirs(os.path.dirname(output_file)) + except OSError: + pass + with open(output_file, 'w') as f: + f.write(_output_serializers[idx](_outputs[idx])) + args: + - --gcp-project-id + - inputValue: gcp_project_id + - --gcp-region + - inputValue: gcp_region + - --dataset-display-name + - inputValue: dataset_display_name + - if: + cond: + isPresent: api_endpoint + then: + - --api-endpoint + - inputValue: api_endpoint + - if: + cond: + isPresent: tables_dataset_metadata + then: + - --tables-dataset-metadata + - inputValue: tables_dataset_metadata + - '----output-paths' + - outputPath: dataset_path + - outputPath: create_time + - outputPath: dataset_id diff --git a/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_component.py b/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_component.py new file mode 100644 index 0000000..104204a --- /dev/null +++ b/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_component.py @@ -0,0 +1,87 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import NamedTuple + + +def automl_create_model_for_tables( + gcp_project_id: str, + gcp_region: str, + dataset_display_name: str, + api_endpoint: str = None, + model_display_name: str = None, + model_prefix: str = 'bwmodel', + optimization_objective: str = None, + include_column_spec_names: list = None, + exclude_column_spec_names: list = None, + train_budget_milli_node_hours: int = 1000, +) -> NamedTuple('Outputs', [('model_display_name', str), ('model_name', str), ('model_id', str)]): + + import subprocess + import sys + subprocess.run([sys.executable, '-m', 'pip', 'install', 'googleapis-common-protos==1.6.0', '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) + subprocess.run([sys.executable, '-m', 'pip', 'install', 'google-cloud-automl==0.9.0', '--quiet', '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) + + import google + import logging + from google.api_core.client_options import ClientOptions + from google.cloud import automl_v1beta1 as automl + import time + + logging.getLogger().setLevel(logging.INFO) # TODO: make level configurable + # TODO: we could instead check for region 'eu' and use 'eu-automl.googleapis.com:443'endpoint + # in that case, instead of requiring endpoint to be specified. + if api_endpoint: + client_options = ClientOptions(api_endpoint=api_endpoint) + client = automl.TablesClient(project=gcp_project_id, region=gcp_region, + client_options=client_options) + else: + client = automl.TablesClient(project=gcp_project_id, region=gcp_region) + + if not model_display_name: + model_display_name = '{}_{}'.format(model_prefix, str(int(time.time()))) + + logging.info('Training model {}...'.format(model_display_name)) + response = client.create_model( + model_display_name, + train_budget_milli_node_hours=train_budget_milli_node_hours, + dataset_display_name=dataset_display_name, + optimization_objective=optimization_objective, + include_column_spec_names=include_column_spec_names, + exclude_column_spec_names=exclude_column_spec_names, + ) + + logging.info("Training operation: {}".format(response.operation)) + logging.info("Training operation name: {}".format(response.operation.name)) + logging.info("Training in progress. This operation may take multiple hours to complete.") + # block termination of the op until training is finished. + result = response.result() + logging.info("Training completed: {}".format(result)) + model_name = result.name + model_id = model_name.rsplit('/', 1)[-1] + print('model name: {}, model id: {}'.format(model_name, model_id)) + return (model_display_name, model_name, model_id) + + + +if __name__ == '__main__': + import kfp + kfp.components.func_to_container_op(automl_create_model_for_tables, output_component_file='tables_component.yaml', + base_image='python:3.7') + + +# if __name__ == "__main__": +# automl_create_model_for_tables('aju-vtests2', 'us-central1', 'so_digest2_32', +# include_column_spec_names=["title", "body", "answer_count", "comment_count", "creation_date", "favorite_count", "owner_user_id", "score", "view_count"] +# ) diff --git a/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_component.yaml b/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_component.yaml new file mode 100644 index 0000000..8ca6e7b --- /dev/null +++ b/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_component.yaml @@ -0,0 +1,158 @@ +name: Automl create model for tables +inputs: +- name: gcp_project_id + type: String +- name: gcp_region + type: String +- name: dataset_display_name + type: String +- name: api_endpoint + type: String + optional: true +- name: model_display_name + type: String + optional: true +- name: model_prefix + type: String + default: bwmodel + optional: true +- name: optimization_objective + type: String + optional: true +- name: include_column_spec_names + type: JsonArray + optional: true +- name: exclude_column_spec_names + type: JsonArray + optional: true +- name: train_budget_milli_node_hours + type: Integer + default: '1000' + optional: true +outputs: +- name: model_display_name + type: String +- name: model_name + type: String +- name: model_id + type: String +implementation: + container: + image: python:3.7 + command: + - python3 + - -u + - -c + - "from typing import NamedTuple\n\ndef automl_create_model_for_tables(\n\tgcp_project_id:\ + \ str,\n\tgcp_region: str,\n\tdataset_display_name: str,\n api_endpoint: str\ + \ = None,\n model_display_name: str = None,\n model_prefix: str = 'bwmodel',\n\ + \ optimization_objective: str = None,\n include_column_spec_names: list =\ + \ None,\n exclude_column_spec_names: list = None,\n\ttrain_budget_milli_node_hours:\ + \ int = 1000,\n) -> NamedTuple('Outputs', [('model_display_name', str), ('model_name',\ + \ str), ('model_id', str)]):\n\n import subprocess\n import sys\n subprocess.run([sys.executable,\ + \ '-m', 'pip', 'install', 'googleapis-common-protos==1.6.0', '--no-warn-script-location'],\ + \ env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True)\n subprocess.run([sys.executable,\ + \ '-m', 'pip', 'install', 'google-cloud-automl==0.9.0', '--quiet', '--no-warn-script-location'],\ + \ env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True)\n\n import google\n\ + \ import logging\n from google.api_core.client_options import ClientOptions\n\ + \ from google.cloud import automl_v1beta1 as automl\n import time\n\n logging.getLogger().setLevel(logging.INFO)\ + \ # TODO: make level configurable\n # TODO: we could instead check for region\ + \ 'eu' and use 'eu-automl.googleapis.com:443'endpoint\n # in that case, instead\ + \ of requiring endpoint to be specified.\n if api_endpoint:\n client_options\ + \ = ClientOptions(api_endpoint=api_endpoint)\n client = automl.TablesClient(project=gcp_project_id,\ + \ region=gcp_region,\n client_options=client_options)\n else:\n client\ + \ = automl.TablesClient(project=gcp_project_id, region=gcp_region)\n\n if not\ + \ model_display_name:\n model_display_name = '{}_{}'.format(model_prefix,\ + \ str(int(time.time())))\n\n logging.info('Training model {}...'.format(model_display_name))\n\ + \ response = client.create_model(\n model_display_name,\n train_budget_milli_node_hours=train_budget_milli_node_hours,\n\ + \ dataset_display_name=dataset_display_name,\n optimization_objective=optimization_objective,\n\ + \ include_column_spec_names=include_column_spec_names,\n exclude_column_spec_names=exclude_column_spec_names,\n\ + \ )\n\n logging.info(\"Training operation: {}\".format(response.operation))\n\ + \ logging.info(\"Training operation name: {}\".format(response.operation.name))\n\ + \ logging.info(\"Training in progress. This operation may take multiple hours\ + \ to complete.\")\n # block termination of the op until training is finished.\n\ + \ result = response.result()\n logging.info(\"Training completed: {}\".format(result))\n\ + \ model_name = result.name\n model_id = model_name.rsplit('/', 1)[-1]\n print('model\ + \ name: {}, model id: {}'.format(model_name, model_id))\n return (model_display_name,\ + \ model_name, model_id)\n\nimport json\ndef _serialize_str(str_value: str) ->\ + \ str:\n if not isinstance(str_value, str):\n raise TypeError('Value\ + \ \"{}\" has type \"{}\" instead of str.'.format(str(str_value), str(type(str_value))))\n\ + \ return str_value\n\nimport argparse\n_parser = argparse.ArgumentParser(prog='Automl\ + \ create model for tables', description='')\n_parser.add_argument(\"--gcp-project-id\"\ + , dest=\"gcp_project_id\", type=str, required=True, default=argparse.SUPPRESS)\n\ + _parser.add_argument(\"--gcp-region\", dest=\"gcp_region\", type=str, required=True,\ + \ default=argparse.SUPPRESS)\n_parser.add_argument(\"--dataset-display-name\"\ + , dest=\"dataset_display_name\", type=str, required=True, default=argparse.SUPPRESS)\n\ + _parser.add_argument(\"--api-endpoint\", dest=\"api_endpoint\", type=str, required=False,\ + \ default=argparse.SUPPRESS)\n_parser.add_argument(\"--model-display-name\"\ + , dest=\"model_display_name\", type=str, required=False, default=argparse.SUPPRESS)\n\ + _parser.add_argument(\"--model-prefix\", dest=\"model_prefix\", type=str, required=False,\ + \ default=argparse.SUPPRESS)\n_parser.add_argument(\"--optimization-objective\"\ + , dest=\"optimization_objective\", type=str, required=False, default=argparse.SUPPRESS)\n\ + _parser.add_argument(\"--include-column-spec-names\", dest=\"include_column_spec_names\"\ + , type=json.loads, required=False, default=argparse.SUPPRESS)\n_parser.add_argument(\"\ + --exclude-column-spec-names\", dest=\"exclude_column_spec_names\", type=json.loads,\ + \ required=False, default=argparse.SUPPRESS)\n_parser.add_argument(\"--train-budget-milli-node-hours\"\ + , dest=\"train_budget_milli_node_hours\", type=int, required=False, default=argparse.SUPPRESS)\n\ + _parser.add_argument(\"----output-paths\", dest=\"_output_paths\", type=str,\ + \ nargs=3)\n_parsed_args = vars(_parser.parse_args())\n_output_files = _parsed_args.pop(\"\ + _output_paths\", [])\n\n_outputs = automl_create_model_for_tables(**_parsed_args)\n\ + \nif not hasattr(_outputs, '__getitem__') or isinstance(_outputs, str):\n \ + \ _outputs = [_outputs]\n\n_output_serializers = [\n _serialize_str,\n \ + \ _serialize_str,\n _serialize_str\n]\n\nimport os\nfor idx, output_file\ + \ in enumerate(_output_files):\n try:\n os.makedirs(os.path.dirname(output_file))\n\ + \ except OSError:\n pass\n with open(output_file, 'w') as f:\n\ + \ f.write(_output_serializers[idx](_outputs[idx]))\n" + args: + - --gcp-project-id + - inputValue: gcp_project_id + - --gcp-region + - inputValue: gcp_region + - --dataset-display-name + - inputValue: dataset_display_name + - if: + cond: + isPresent: api_endpoint + then: + - --api-endpoint + - inputValue: api_endpoint + - if: + cond: + isPresent: model_display_name + then: + - --model-display-name + - inputValue: model_display_name + - if: + cond: + isPresent: model_prefix + then: + - --model-prefix + - inputValue: model_prefix + - if: + cond: + isPresent: optimization_objective + then: + - --optimization-objective + - inputValue: optimization_objective + - if: + cond: + isPresent: include_column_spec_names + then: + - --include-column-spec-names + - inputValue: include_column_spec_names + - if: + cond: + isPresent: exclude_column_spec_names + then: + - --exclude-column-spec-names + - inputValue: exclude_column_spec_names + - if: + cond: + isPresent: train_budget_milli_node_hours + then: + - --train-budget-milli-node-hours + - inputValue: train_budget_milli_node_hours + - '----output-paths' + - outputPath: model_display_name + - outputPath: model_name + - outputPath: model_id diff --git a/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_component.py b/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_component.py new file mode 100644 index 0000000..a8e293f --- /dev/null +++ b/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_component.py @@ -0,0 +1,199 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import NamedTuple +from kfp.components import InputPath, OutputPath + + +def automl_eval_tables_model( + gcp_project_id: str, + gcp_region: str, + model_display_name: str, + bucket_name: str, + gcs_path: str, + eval_data_path: OutputPath('evals'), + mlpipeline_ui_metadata_path: OutputPath('UI_metadata'), + api_endpoint: str = None, + +) -> NamedTuple('Outputs', [ + # ('evals_gcs_path', str), + ('feat_list', str)]): + import subprocess + import sys + subprocess.run([sys.executable, '-m', 'pip', 'install', 'googleapis-common-protos==1.6.0', + '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) + subprocess.run([sys.executable, '-m', 'pip', 'install', 'google-cloud-automl==0.9.0', + '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) + subprocess.run([sys.executable, '-m', 'pip', 'install', + 'matplotlib', 'pathlib2', 'google-cloud-storage', + '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) + + + import google + import json + import logging + import pickle + import pathlib2 + + + from google.api_core.client_options import ClientOptions + from google.api_core import exceptions + from google.cloud import automl_v1beta1 as automl + from google.cloud.automl_v1beta1 import enums + from google.cloud import storage + + + def upload_blob(bucket_name, source_file_name, destination_blob_name, + public_url=False): + """Uploads a file to the bucket.""" + + storage_client = storage.Client() + bucket = storage_client.bucket(bucket_name) + blob = bucket.blob(destination_blob_name) + + blob.upload_from_filename(source_file_name) + + logging.info("File {} uploaded to {}.".format( + source_file_name, destination_blob_name)) + if public_url: + blob.make_public() + logging.info("Blob {} is publicly accessible at {}".format( + blob.name, blob.public_url)) + return blob.public_url + + + def get_model_details(client, model_display_name): + try: + model = client.get_model(model_display_name=model_display_name) + except exceptions.NotFound: + logging.info("Model %s not found." % model_display_name) + return (None, None) + + model = client.get_model(model_display_name=model_display_name) + # Retrieve deployment state. + if model.deployment_state == enums.Model.DeploymentState.DEPLOYED: + deployment_state = "deployed" + else: + deployment_state = "undeployed" + # get features of top global importance + feat_list = [ + (column.feature_importance, column.column_display_name) + for column in model.tables_model_metadata.tables_model_column_info + ] + feat_list.sort(reverse=True) + if len(feat_list) < 10: + feat_to_show = len(feat_list) + else: + feat_to_show = 10 + + # Display the model information. + # TODO: skip this? + logging.info("Model name: {}".format(model.name)) + logging.info("Model id: {}".format(model.name.split("/")[-1])) + logging.info("Model display name: {}".format(model.display_name)) + logging.info("Features of top importance:") + for feat in feat_list[:feat_to_show]: + logging.info(feat) + logging.info("Model create time:") + logging.info("\tseconds: {}".format(model.create_time.seconds)) + logging.info("\tnanos: {}".format(model.create_time.nanos)) + logging.info("Model deployment state: {}".format(deployment_state)) + + generate_fi_ui(feat_list) + return (model, feat_list) + + + def generate_fi_ui(feat_list): + import matplotlib.pyplot as plt + + image_suffix = '{}/gfi.png'.format(gcs_path) + res = list(zip(*feat_list)) + x = list(res[0]) + y = list(res[1]) + y_pos = list(range(len(y))) + plt.barh(y_pos, x, alpha=0.5) + plt.yticks(y_pos, y) + plt.savefig('/gfi.png') + public_url = upload_blob(bucket_name, '/gfi.png', image_suffix, public_url=True) + logging.info('using image url {}'.format(public_url)) + + html_suffix = '{}/gfi.html'.format(gcs_path) + with open('/gfi.html', 'w') as f: + f.write('

Global Feature Importance

\n'.format(public_url)) + upload_blob(bucket_name, '/gfi.html', html_suffix) + html_source = 'gs://{}/{}'.format(bucket_name, html_suffix) + logging.info('metadata html source: {}'.format(html_source)) + + metadata = { + 'outputs' : [ + { + 'type': 'web-app', + 'storage': 'gcs', + 'source': html_source + }]} + logging.info('using metadata dict {}'.format(json.dumps(metadata))) + # with open('/mlpipeline-ui-metadata.json', 'w') as f: + # json.dump(metadata, f) + logging.info('using metadata ui path: {}'.format(mlpipeline_ui_metadata_path)) + with open(mlpipeline_ui_metadata_path, 'w') as mlpipeline_ui_metadata_file: + mlpipeline_ui_metadata_file.write(json.dumps(metadata)) + + + logging.getLogger().setLevel(logging.INFO) # TODO: make level configurable + # TODO: we could instead check for region 'eu' and use 'eu-automl.googleapis.com:443'endpoint + # in that case, instead of requiring endpoint to be specified. + if api_endpoint: + client_options = ClientOptions(api_endpoint=api_endpoint) + client = automl.TablesClient(project=gcp_project_id, region=gcp_region, + client_options=client_options) + else: + client = automl.TablesClient(project=gcp_project_id, region=gcp_region) + + (model, feat_list) = get_model_details(client, model_display_name) + + + evals = list(client.list_model_evaluations(model_display_name=model_display_name)) + with open('temp_oput_regression', "w") as f: + f.write('Model evals:\n{}'.format(evals)) + pstring = pickle.dumps(evals) + + # write to eval_data_path + if eval_data_path: + logging.info("eval_data_path: %s", eval_data_path) + try: + pathlib2.Path(eval_data_path).parent.mkdir(parents=True) + except FileExistsError: + pass + pathlib2.Path(eval_data_path).write_bytes(pstring) + + feat_list_string = json.dumps(feat_list) + # return(gcs_path, feat_list_string) + return(feat_list_string) + + +if __name__ == '__main__': + import kfp + kfp.components.func_to_container_op(automl_eval_tables_model, + output_component_file='tables_eval_component.yaml', base_image='python:3.7') + +# if __name__ == '__main__': + +# # (eval_hex, features) = automl_eval_tables_model('aju-vtests2', 'us-central1', model_display_name='somodel_1579284627') +# (eval_hex, features) = automl_eval_tables_model('aju-vtests2', 'us-central1', +# bucket_name='aju-pipelines', model_display_name='bwmodel_1579017140', +# # gcs_path='automl_evals/testing/somodel_1579284627', +# eval_data_path=None) +# # with open('temp_oput', "w") as f: +# # f.write(eval_hex) + diff --git a/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_component.yaml b/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_component.yaml new file mode 100644 index 0000000..a63cffb --- /dev/null +++ b/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_component.yaml @@ -0,0 +1,155 @@ +name: Automl eval tables model +inputs: +- name: gcp_project_id + type: String +- name: gcp_region + type: String +- name: model_display_name + type: String +- name: bucket_name + type: String +- name: gcs_path + type: String +- name: api_endpoint + type: String + optional: true +outputs: +- name: eval_data + type: evals +- name: mlpipeline_ui_metadata + type: UI_metadata +- name: feat_list + type: String +implementation: + container: + image: python:3.7 + command: + - python3 + - -u + - -c + - "class OutputPath:\n '''When creating component from function, OutputPath\ + \ should be used as function parameter annotation to tell the system that the\ + \ function wants to output data by writing it into a file with the given path\ + \ instead of returning the data from the function.'''\n def __init__(self,\ + \ type=None):\n self.type = type\n\ndef _make_parent_dirs_and_return_path(file_path:\ + \ str):\n import os\n os.makedirs(os.path.dirname(file_path), exist_ok=True)\n\ + \ return file_path\n\nfrom typing import NamedTuple\n\ndef automl_eval_tables_model(\n\ + \tgcp_project_id: str,\n\tgcp_region: str,\n model_display_name: str,\n bucket_name:\ + \ str,\n gcs_path: str,\n eval_data_path: OutputPath('evals'),\n mlpipeline_ui_metadata_path:\ + \ OutputPath('UI_metadata'),\n api_endpoint: str = None,\n\n) -> NamedTuple('Outputs',\ + \ [\n # ('evals_gcs_path', str),\n ('feat_list', str)]):\n import subprocess\n\ + \ import sys\n subprocess.run([sys.executable, '-m', 'pip', 'install', 'googleapis-common-protos==1.6.0',\n\ + \ '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'},\ + \ check=True)\n subprocess.run([sys.executable, '-m', 'pip', 'install', 'google-cloud-automl==0.9.0',\n\ + \ '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'},\ + \ check=True)\n subprocess.run([sys.executable, '-m', 'pip', 'install',\n \ + \ 'matplotlib', 'pathlib2', 'google-cloud-storage',\n '--no-warn-script-location'],\ + \ env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True)\n\n import google\n\ + \ import json\n import logging\n import pickle\n import pathlib2\n\n from\ + \ google.api_core.client_options import ClientOptions\n from google.api_core\ + \ import exceptions\n from google.cloud import automl_v1beta1 as automl\n \ + \ from google.cloud.automl_v1beta1 import enums\n from google.cloud import\ + \ storage\n\n def upload_blob(bucket_name, source_file_name, destination_blob_name,\n\ + \ public_url=False):\n \"\"\"Uploads a file to the bucket.\"\"\"\n\n\ + \ storage_client = storage.Client()\n bucket = storage_client.bucket(bucket_name)\n\ + \ blob = bucket.blob(destination_blob_name)\n\n blob.upload_from_filename(source_file_name)\n\ + \n logging.info(\"File {} uploaded to {}.\".format(\n source_file_name,\ + \ destination_blob_name))\n if public_url:\n blob.make_public()\n \ + \ logging.info(\"Blob {} is publicly accessible at {}\".format(\n \ + \ blob.name, blob.public_url))\n return blob.public_url\n\n def get_model_details(client,\ + \ model_display_name):\n try:\n model = client.get_model(model_display_name=model_display_name)\n\ + \ except exceptions.NotFound:\n logging.info(\"Model %s not found.\"\ + \ % model_display_name)\n return (None, None)\n\n model = client.get_model(model_display_name=model_display_name)\n\ + \ # Retrieve deployment state.\n if model.deployment_state == enums.Model.DeploymentState.DEPLOYED:\n\ + \ deployment_state = \"deployed\"\n else:\n deployment_state\ + \ = \"undeployed\"\n # get features of top global importance\n feat_list\ + \ = [\n (column.feature_importance, column.column_display_name)\n \ + \ for column in model.tables_model_metadata.tables_model_column_info\n \ + \ ]\n feat_list.sort(reverse=True)\n if len(feat_list) < 10:\n \ + \ feat_to_show = len(feat_list)\n else:\n feat_to_show = 10\n\n\ + \ # Display the model information.\n # TODO: skip this?\n logging.info(\"\ + Model name: {}\".format(model.name))\n logging.info(\"Model id: {}\".format(model.name.split(\"\ + /\")[-1]))\n logging.info(\"Model display name: {}\".format(model.display_name))\n\ + \ logging.info(\"Features of top importance:\")\n for feat in feat_list[:feat_to_show]:\n\ + \ logging.info(feat)\n logging.info(\"Model create time:\")\n logging.info(\"\ + \\tseconds: {}\".format(model.create_time.seconds))\n logging.info(\"\\tnanos:\ + \ {}\".format(model.create_time.nanos))\n logging.info(\"Model deployment\ + \ state: {}\".format(deployment_state))\n\n generate_fi_ui(feat_list)\n \ + \ return (model, feat_list)\n\n def generate_fi_ui(feat_list):\n import\ + \ matplotlib.pyplot as plt\n\n image_suffix = '{}/gfi.png'.format(gcs_path)\n\ + \ res = list(zip(*feat_list))\n x = list(res[0])\n y = list(res[1])\n\ + \ y_pos = list(range(len(y)))\n plt.barh(y_pos, x, alpha=0.5)\n plt.yticks(y_pos,\ + \ y)\n plt.savefig('/gfi.png')\n public_url = upload_blob(bucket_name,\ + \ '/gfi.png', image_suffix, public_url=True)\n logging.info('using image\ + \ url {}'.format(public_url))\n\n html_suffix = '{}/gfi.html'.format(gcs_path)\n\ + \ with open('/gfi.html', 'w') as f:\n f.write('

Global\ + \ Feature Importance

\\n'.format(public_url))\n\ + \ upload_blob(bucket_name, '/gfi.html', html_suffix)\n html_source = 'gs://{}/{}'.format(bucket_name,\ + \ html_suffix)\n logging.info('metadata html source: {}'.format(html_source))\n\ + \n metadata = {\n 'outputs' : [\n {\n 'type': 'web-app',\n\ + \ 'storage': 'gcs',\n 'source': html_source\n }]}\n logging.info('using\ + \ metadata dict {}'.format(json.dumps(metadata)))\n # with open('/mlpipeline-ui-metadata.json',\ + \ 'w') as f:\n # json.dump(metadata, f)\n logging.info('using metadata\ + \ ui path: {}'.format(mlpipeline_ui_metadata_path))\n with open(mlpipeline_ui_metadata_path,\ + \ 'w') as mlpipeline_ui_metadata_file:\n mlpipeline_ui_metadata_file.write(json.dumps(metadata))\n\ + \n logging.getLogger().setLevel(logging.INFO) # TODO: make level configurable\n\ + \ # TODO: we could instead check for region 'eu' and use 'eu-automl.googleapis.com:443'endpoint\n\ + \ # in that case, instead of requiring endpoint to be specified.\n if api_endpoint:\n\ + \ client_options = ClientOptions(api_endpoint=api_endpoint)\n client =\ + \ automl.TablesClient(project=gcp_project_id, region=gcp_region,\n client_options=client_options)\n\ + \ else:\n client = automl.TablesClient(project=gcp_project_id, region=gcp_region)\n\ + \n (model, feat_list) = get_model_details(client, model_display_name)\n\n \ + \ evals = list(client.list_model_evaluations(model_display_name=model_display_name))\n\ + \ with open('temp_oput_regression', \"w\") as f:\n f.write('Model evals:\\\ + n{}'.format(evals))\n pstring = pickle.dumps(evals)\n\n # write to eval_data_path\n\ + \ if eval_data_path:\n logging.info(\"eval_data_path: %s\", eval_data_path)\n\ + \ try:\n pathlib2.Path(eval_data_path).parent.mkdir(parents=True)\n\ + \ except FileExistsError:\n pass\n pathlib2.Path(eval_data_path).write_bytes(pstring)\n\ + \n feat_list_string = json.dumps(feat_list)\n # return(gcs_path, feat_list_string)\n\ + \ return(feat_list_string)\n\ndef _serialize_str(str_value: str) -> str:\n\ + \ if not isinstance(str_value, str):\n raise TypeError('Value \"{}\"\ + \ has type \"{}\" instead of str.'.format(str(str_value), str(type(str_value))))\n\ + \ return str_value\n\nimport argparse\n_parser = argparse.ArgumentParser(prog='Automl\ + \ eval tables model', description='')\n_parser.add_argument(\"--gcp-project-id\"\ + , dest=\"gcp_project_id\", type=str, required=True, default=argparse.SUPPRESS)\n\ + _parser.add_argument(\"--gcp-region\", dest=\"gcp_region\", type=str, required=True,\ + \ default=argparse.SUPPRESS)\n_parser.add_argument(\"--model-display-name\"\ + , dest=\"model_display_name\", type=str, required=True, default=argparse.SUPPRESS)\n\ + _parser.add_argument(\"--bucket-name\", dest=\"bucket_name\", type=str, required=True,\ + \ default=argparse.SUPPRESS)\n_parser.add_argument(\"--gcs-path\", dest=\"gcs_path\"\ + , type=str, required=True, default=argparse.SUPPRESS)\n_parser.add_argument(\"\ + --api-endpoint\", dest=\"api_endpoint\", type=str, required=False, default=argparse.SUPPRESS)\n\ + _parser.add_argument(\"--eval-data\", dest=\"eval_data_path\", type=_make_parent_dirs_and_return_path,\ + \ required=True, default=argparse.SUPPRESS)\n_parser.add_argument(\"--mlpipeline-ui-metadata\"\ + , dest=\"mlpipeline_ui_metadata_path\", type=_make_parent_dirs_and_return_path,\ + \ required=True, default=argparse.SUPPRESS)\n_parser.add_argument(\"----output-paths\"\ + , dest=\"_output_paths\", type=str, nargs=1)\n_parsed_args = vars(_parser.parse_args())\n\ + _output_files = _parsed_args.pop(\"_output_paths\", [])\n\n_outputs = automl_eval_tables_model(**_parsed_args)\n\ + \nif not hasattr(_outputs, '__getitem__') or isinstance(_outputs, str):\n \ + \ _outputs = [_outputs]\n\n_output_serializers = [\n _serialize_str,\n\n\ + ]\n\nimport os\nfor idx, output_file in enumerate(_output_files):\n try:\n\ + \ os.makedirs(os.path.dirname(output_file))\n except OSError:\n \ + \ pass\n with open(output_file, 'w') as f:\n f.write(_output_serializers[idx](_outputs[idx]))\n" + args: + - --gcp-project-id + - inputValue: gcp_project_id + - --gcp-region + - inputValue: gcp_region + - --model-display-name + - inputValue: model_display_name + - --bucket-name + - inputValue: bucket_name + - --gcs-path + - inputValue: gcs_path + - if: + cond: + isPresent: api_endpoint + then: + - --api-endpoint + - inputValue: api_endpoint + - --eval-data + - outputPath: eval_data + - --mlpipeline-ui-metadata + - outputPath: mlpipeline_ui_metadata + - '----output-paths' + - outputPath: feat_list diff --git a/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_metrics_component.py b/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_metrics_component.py new file mode 100644 index 0000000..1802208 --- /dev/null +++ b/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_metrics_component.py @@ -0,0 +1,220 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import NamedTuple +from kfp.components import InputPath, OutputPath + + +# An example of how the model eval info could be used to make decisions aboiut whether or not +# to deploy the model. +def automl_eval_metrics( + gcp_project_id: str, + gcp_region: str, + model_display_name: str, + bucket_name: str, + # gcs_path: str, + eval_data_path: InputPath('evals'), + mlpipeline_ui_metadata_path: OutputPath('UI_metadata'), + api_endpoint: str = None, + # thresholds: str = '{"au_prc": 0.9}', + thresholds: str = '{"mean_absolute_error": 450}', + confidence_threshold: float = 0.5 # for classification + +) -> NamedTuple('Outputs', [('deploy', bool)]): + import subprocess + import sys + subprocess.run([sys.executable, '-m', 'pip', 'install', 'googleapis-common-protos==1.6.0', + '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) + subprocess.run([sys.executable, '-m', 'pip', 'install', 'google-cloud-automl==0.9.0', + 'google-cloud-storage', + '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) + + + import google + import json + import logging + import pickle + from google.api_core.client_options import ClientOptions + from google.api_core import exceptions + from google.cloud import automl_v1beta1 as automl + from google.cloud.automl_v1beta1 import enums + from google.cloud import storage + + + # def get_string_from_gcs(project, bucket_name, gcs_path): + # logging.info('Using bucket {} and path {}'.format(bucket_name, gcs_path)) + # storage_client = storage.Client(project=project) + # bucket = storage_client.get_bucket(bucket_name) + # blob = bucket.blob(gcs_path) + # return blob.download_as_string() + + # def upload_blob(bucket_name, source_file_name, destination_blob_name, + # public_url=False): + # """Uploads a file to the bucket.""" + + # storage_client = storage.Client() + # bucket = storage_client.bucket(bucket_name) + # blob = bucket.blob(destination_blob_name) + + # blob.upload_from_filename(source_file_name) + + # logging.info("File {} uploaded to {}.".format( + # source_file_name, destination_blob_name)) + # if public_url: + # blob.make_public() + # logging.info("Blob {} is publicly accessible at {}".format( + # blob.name, blob.public_url)) + # return blob.public_url + + logging.getLogger().setLevel(logging.INFO) # TODO: make level configurable + # TODO: we could instead check for region 'eu' and use 'eu-automl.googleapis.com:443'endpoint + # in that case, instead of requiring endpoint to be specified. + if api_endpoint: + client_options = ClientOptions(api_endpoint=api_endpoint) + client = automl.TablesClient(project=gcp_project_id, region=gcp_region, + client_options=client_options) + else: + client = automl.TablesClient(project=gcp_project_id, region=gcp_region) + + thresholds_dict = json.loads(thresholds) + logging.info('thresholds dict: {}'.format(thresholds_dict)) + + def regression_threshold_check(eval_info): + eresults = {} + rmetrics = eval_info[1].regression_evaluation_metrics + logging.info('got regression eval {}'.format(eval_info[1])) + eresults['root_mean_squared_error'] = rmetrics.root_mean_squared_error + eresults['mean_absolute_error'] = rmetrics.mean_absolute_error + eresults['r_squared'] = rmetrics.r_squared + eresults['mean_absolute_percentage_error'] = rmetrics.mean_absolute_percentage_error + eresults['root_mean_squared_log_error'] = rmetrics.root_mean_squared_log_error + for k,v in thresholds_dict.items(): + logging.info('k {}, v {}'.format(k, v)) + if k in ['root_mean_squared_error', 'mean_absolute_error', 'mean_absolute_percentage_error']: + if eresults[k] > v: + logging.info('{} > {}; returning False'.format( + eresults[k], v)) + return (False, eresults) + elif eresults[k] < v: + logging.info('{} < {}; returning False'.format( + eresults[k], v)) + return (False, eresults) + return (True, eresults) + + def classif_threshold_check(eval_info): + eresults = {} + example_count = eval_info[0].evaluated_example_count + print('Looking for example_count {}'.format(example_count)) + for e in eval_info[1:]: # we know we don't want the first elt + if e.evaluated_example_count == example_count: + eresults['au_prc'] = e.classification_evaluation_metrics.au_prc + eresults['au_roc'] = e.classification_evaluation_metrics.au_roc + eresults['log_loss'] = e.classification_evaluation_metrics.log_loss + for i in e.classification_evaluation_metrics.confidence_metrics_entry: + if i.confidence_threshold >= confidence_threshold: + eresults['recall'] = i.recall + eresults['precision'] = i.precision + eresults['f1_score'] = i.f1_score + break + break + logging.info('eresults: {}'.format(eresults)) + for k,v in thresholds_dict.items(): + logging.info('k {}, v {}'.format(k, v)) + if k == 'log_loss': + if eresults[k] > v: + logging.info('{} > {}; returning False'.format( + eresults[k], v)) + return (False, eresults) + else: + if eresults[k] < v: + logging.info('{} < {}; returning False'.format( + eresults[k], v)) + return (False, eresults) + return (True, eresults) + + + # testing... + with open(eval_data_path, 'rb') as f: + logging.info('successfully opened eval_data_path {}'.format(eval_data_path)) + try: + eval_info = pickle.loads(f.read()) + + classif = False + regression = False + # TODO: what's the right way to figure out the model type? + if eval_info[1].regression_evaluation_metrics and eval_info[1].regression_evaluation_metrics.root_mean_squared_error: + regression=True + logging.info('found regression metrics {}'.format(eval_info[1].regression_evaluation_metrics)) + elif eval_info[1].classification_evaluation_metrics and eval_info[1].classification_evaluation_metrics.au_prc: + classif = True + logging.info('found classification metrics {}'.format(eval_info[1].classification_evaluation_metrics)) + + if regression and thresholds_dict: + res, eresults = regression_threshold_check(eval_info) + # logging.info('eresults: {}'.format(eresults)) + metadata = { + 'outputs' : [ + { + 'storage': 'inline', + 'source': '# Regression metrics:\n\n```{}```\n'.format(eresults), + 'type': 'markdown', + }]} + # TODO: is it possible to get confusion matrix info via the API, for the binary + # classifcation case? doesn't seem to be. + logging.info('using metadata dict {}'.format(json.dumps(metadata))) + logging.info('using metadata ui path: {}'.format(mlpipeline_ui_metadata_path)) + with open(mlpipeline_ui_metadata_path, 'w') as mlpipeline_ui_metadata_file: + mlpipeline_ui_metadata_file.write(json.dumps(metadata)) + logging.info('deploy flag: {}'.format(res)) + return res + + elif classif and thresholds_dict: + res, eresults = classif_threshold_check(eval_info) + # logging.info('eresults: {}'.format(eresults)) + metadata = { + 'outputs' : [ + { + 'storage': 'inline', + 'source': '# classification metrics for confidence threshold {}:\n\n```{}```\n'.format( + confidence_threshold, eresults), + 'type': 'markdown', + }]} + logging.info('using metadata dict {}'.format(json.dumps(metadata))) + logging.info('using metadata ui path: {}'.format(mlpipeline_ui_metadata_path)) + with open(mlpipeline_ui_metadata_path, 'w') as mlpipeline_ui_metadata_file: + mlpipeline_ui_metadata_file.write(json.dumps(metadata)) + logging.info('deploy flag: {}'.format(res)) + return res + else: + return True + except Exception as e: + logging.warning(e) + # If can't reconstruct the eval, or don't have thresholds defined, + # return True as a signal to deploy. + # TODO: is this the right default? + return True + + + + +if __name__ == '__main__': + import kfp + kfp.components.func_to_container_op(automl_eval_metrics, + output_component_file='tables_eval_metrics_component.yaml', base_image='python:3.7') + + +# if __name__ == "__main__": +# automl_eval_threshold('aju-vtests2', 'us-central1', +# # model_display_name='amy_test3_20191219032001', +# gcs_path='automl_evals/testing/somodel_1579284627', bucket_name='aju-pipelines') diff --git a/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_metrics_component.yaml b/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_metrics_component.yaml new file mode 100644 index 0000000..8264ceb --- /dev/null +++ b/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_metrics_component.yaml @@ -0,0 +1,208 @@ +name: Automl eval metrics +inputs: +- name: gcp_project_id + type: String +- name: gcp_region + type: String +- name: model_display_name + type: String +- name: bucket_name + type: String +- name: eval_data + type: evals +- name: api_endpoint + type: String + optional: true +- name: thresholds + type: String + default: '{"mean_absolute_error": 450}' + optional: true +- name: confidence_threshold + type: Float + default: '0.5' + optional: true +outputs: +- name: mlpipeline_ui_metadata + type: UI_metadata +- name: deploy + type: Boolean +implementation: + container: + image: python:3.7 + command: + - python3 + - -u + - -c + - "class InputPath:\n '''When creating component from function, InputPath should\ + \ be used as function parameter annotation to tell the system to pass the *data\ + \ file path* to the function instead of passing the actual data.'''\n def\ + \ __init__(self, type=None):\n self.type = type\n\nclass OutputPath:\n\ + \ '''When creating component from function, OutputPath should be used as\ + \ function parameter annotation to tell the system that the function wants to\ + \ output data by writing it into a file with the given path instead of returning\ + \ the data from the function.'''\n def __init__(self, type=None):\n \ + \ self.type = type\n\ndef _make_parent_dirs_and_return_path(file_path: str):\n\ + \ import os\n os.makedirs(os.path.dirname(file_path), exist_ok=True)\n\ + \ return file_path\n\nfrom typing import NamedTuple\n\ndef automl_eval_metrics(\n\ + \tgcp_project_id: str,\n\tgcp_region: str,\n model_display_name: str,\n bucket_name:\ + \ str,\n # gcs_path: str,\n eval_data_path: InputPath('evals'),\n mlpipeline_ui_metadata_path:\ + \ OutputPath('UI_metadata'),\n api_endpoint: str = None,\n # thresholds: str\ + \ = '{\"au_prc\": 0.9}',\n thresholds: str = '{\"mean_absolute_error\": 450}',\n\ + \ confidence_threshold: float = 0.5 # for classification\n\n) -> NamedTuple('Outputs',\ + \ [('deploy', bool)]):\n import subprocess\n import sys\n subprocess.run([sys.executable,\ + \ '-m', 'pip', 'install', 'googleapis-common-protos==1.6.0',\n '--no-warn-script-location'],\ + \ env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True)\n subprocess.run([sys.executable,\ + \ '-m', 'pip', 'install', 'google-cloud-automl==0.9.0',\n 'google-cloud-storage',\n\ + \ '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'},\ + \ check=True)\n\n import google\n import json\n import logging\n import\ + \ pickle\n from google.api_core.client_options import ClientOptions\n from\ + \ google.api_core import exceptions\n from google.cloud import automl_v1beta1\ + \ as automl\n from google.cloud.automl_v1beta1 import enums\n from google.cloud\ + \ import storage\n\n # def get_string_from_gcs(project, bucket_name, gcs_path):\n\ + \ # logging.info('Using bucket {} and path {}'.format(bucket_name, gcs_path))\n\ + \ # storage_client = storage.Client(project=project)\n # bucket = storage_client.get_bucket(bucket_name)\n\ + \ # blob = bucket.blob(gcs_path)\n # return blob.download_as_string()\n\ + \n # def upload_blob(bucket_name, source_file_name, destination_blob_name,\n\ + \ # public_url=False):\n # \"\"\"Uploads a file to the bucket.\"\"\"\ + \n\n # storage_client = storage.Client()\n # bucket = storage_client.bucket(bucket_name)\n\ + \ # blob = bucket.blob(destination_blob_name)\n\n # blob.upload_from_filename(source_file_name)\n\ + \n # logging.info(\"File {} uploaded to {}.\".format(\n # source_file_name,\ + \ destination_blob_name))\n # if public_url:\n # blob.make_public()\n\ + \ # logging.info(\"Blob {} is publicly accessible at {}\".format(\n #\ + \ blob.name, blob.public_url))\n # return blob.public_url\n\n\ + \ logging.getLogger().setLevel(logging.INFO) # TODO: make level configurable\n\ + \ # TODO: we could instead check for region 'eu' and use 'eu-automl.googleapis.com:443'endpoint\n\ + \ # in that case, instead of requiring endpoint to be specified.\n if api_endpoint:\n\ + \ client_options = ClientOptions(api_endpoint=api_endpoint)\n client =\ + \ automl.TablesClient(project=gcp_project_id, region=gcp_region,\n client_options=client_options)\n\ + \ else:\n client = automl.TablesClient(project=gcp_project_id, region=gcp_region)\n\ + \n thresholds_dict = json.loads(thresholds)\n logging.info('thresholds dict:\ + \ {}'.format(thresholds_dict))\n\n def regression_threshold_check(eval_info):\n\ + \ eresults = {}\n rmetrics = eval_info[1].regression_evaluation_metrics\n\ + \ logging.info('got regression eval {}'.format(eval_info[1]))\n eresults['root_mean_squared_error']\ + \ = rmetrics.root_mean_squared_error\n eresults['mean_absolute_error'] =\ + \ rmetrics.mean_absolute_error\n eresults['r_squared'] = rmetrics.r_squared\n\ + \ eresults['mean_absolute_percentage_error'] = rmetrics.mean_absolute_percentage_error\n\ + \ eresults['root_mean_squared_log_error'] = rmetrics.root_mean_squared_log_error\n\ + \ for k,v in thresholds_dict.items():\n logging.info('k {}, v {}'.format(k,\ + \ v))\n if k in ['root_mean_squared_error', 'mean_absolute_error', 'mean_absolute_percentage_error']:\n\ + \ if eresults[k] > v:\n logging.info('{} > {}; returning False'.format(\n\ + \ eresults[k], v))\n return (False, eresults)\n elif\ + \ eresults[k] < v:\n logging.info('{} < {}; returning False'.format(\n\ + \ eresults[k], v))\n return (False, eresults)\n return\ + \ (True, eresults)\n\n def classif_threshold_check(eval_info):\n eresults\ + \ = {}\n example_count = eval_info[0].evaluated_example_count\n print('Looking\ + \ for example_count {}'.format(example_count))\n for e in eval_info[1:]:\ + \ # we know we don't want the first elt\n if e.evaluated_example_count\ + \ == example_count:\n eresults['au_prc'] = e.classification_evaluation_metrics.au_prc\n\ + \ eresults['au_roc'] = e.classification_evaluation_metrics.au_roc\n \ + \ eresults['log_loss'] = e.classification_evaluation_metrics.log_loss\n\ + \ for i in e.classification_evaluation_metrics.confidence_metrics_entry:\n\ + \ if i.confidence_threshold >= confidence_threshold:\n eresults['recall']\ + \ = i.recall\n eresults['precision'] = i.precision\n eresults['f1_score']\ + \ = i.f1_score\n break\n break\n logging.info('eresults:\ + \ {}'.format(eresults))\n for k,v in thresholds_dict.items():\n logging.info('k\ + \ {}, v {}'.format(k, v))\n if k == 'log_loss':\n if eresults[k]\ + \ > v:\n logging.info('{} > {}; returning False'.format(\n \ + \ eresults[k], v))\n return (False, eresults)\n else:\n \ + \ if eresults[k] < v:\n logging.info('{} < {}; returning False'.format(\n\ + \ eresults[k], v))\n return (False, eresults)\n return\ + \ (True, eresults)\n\n def generate_cm_metadata():\n pass\n\n # testing...\n\ + \ with open(eval_data_path, 'rb') as f:\n logging.info('successfully opened\ + \ eval_data_path {}'.format(eval_data_path))\n try:\n eval_info = pickle.loads(f.read())\n\ + \ # TODO: add handling of confusion matrix stuff for binary classif case..\n\ + \ # eval_string = get_string_from_gcs(gcp_project_id, bucket_name, gcs_path)\n\ + \ # eval_info = pickle.loads(eval_string)\n\n classif = False\n \ + \ binary_classif = False\n regression = False\n # TODO: ughh...\ + \ what's the right way to figure out the model type?\n if eval_info[1].regression_evaluation_metrics\ + \ and eval_info[1].regression_evaluation_metrics.root_mean_squared_error:\n\ + \ regression=True\n logging.info('found regression metrics {}'.format(eval_info[1].regression_evaluation_metrics))\n\ + \ elif eval_info[1].classification_evaluation_metrics and eval_info[1].classification_evaluation_metrics.au_prc:\n\ + \ classif = True\n logging.info('found classification metrics\ + \ {}'.format(eval_info[1].classification_evaluation_metrics))\n # TODO:\ + \ detect binary classification case\n\n if regression and thresholds_dict:\n\ + \ res, eresults = regression_threshold_check(eval_info)\n # logging.info('eresults:\ + \ {}'.format(eresults))\n metadata = {\n 'outputs' : [\n \ + \ {\n 'storage': 'inline',\n 'source': '# Regression\ + \ metrics:\\n\\n```{}```\\n'.format(eresults),\n 'type': 'markdown',\n\ + \ }]}\n logging.info('using metadata dict {}'.format(json.dumps(metadata)))\n\ + \ logging.info('using metadata ui path: {}'.format(mlpipeline_ui_metadata_path))\n\ + \ with open(mlpipeline_ui_metadata_path, 'w') as mlpipeline_ui_metadata_file:\n\ + \ mlpipeline_ui_metadata_file.write(json.dumps(metadata))\n \ + \ logging.info('deploy flag: {}'.format(res))\n return res\n\n elif\ + \ classif and thresholds_dict:\n res, eresults = classif_threshold_check(eval_info)\n\ + \ # logging.info('eresults: {}'.format(eresults))\n metadata =\ + \ {\n 'outputs' : [\n {\n 'storage': 'inline',\n\ + \ 'source': '# classification metrics for confidence threshold {}:\\\ + n\\n```{}```\\n'.format(\n confidence_threshold, eresults),\n\ + \ 'type': 'markdown',\n }]}\n logging.info('using\ + \ metadata dict {}'.format(json.dumps(metadata)))\n logging.info('using\ + \ metadata ui path: {}'.format(mlpipeline_ui_metadata_path))\n with open(mlpipeline_ui_metadata_path,\ + \ 'w') as mlpipeline_ui_metadata_file:\n mlpipeline_ui_metadata_file.write(json.dumps(metadata))\n\ + \ # with open('/mlpipeline-ui-metadata.json', 'w') as f:\n #\ + \ json.dump(metadata, f)\n logging.info('deploy flag: {}'.format(res))\n\ + \ # TODO: generate confusion matrix ui-metadata as approp etc.\n \ + \ if binary_classif:\n generate_cm_metadata()\n return res\n\ + \ else:\n return True\n except Exception as e:\n logging.warning(e)\n\ + \ # If can't reconstruct the eval, or don't have thresholds defined,\n\ + \ # return True as a signal to deploy.\n # TODO: is this the right\ + \ default?\n return True\n\ndef _serialize_bool(bool_value: bool) -> str:\n\ + \ if isinstance(bool_value, str):\n return bool_value\n if not\ + \ isinstance(bool_value, bool):\n raise TypeError('Value \"{}\" has type\ + \ \"{}\" instead of bool.'.format(str(bool_value), str(type(bool_value))))\n\ + \ return str(bool_value)\n\nimport argparse\n_parser = argparse.ArgumentParser(prog='Automl\ + \ eval metrics', description='')\n_parser.add_argument(\"--gcp-project-id\"\ + , dest=\"gcp_project_id\", type=str, required=True, default=argparse.SUPPRESS)\n\ + _parser.add_argument(\"--gcp-region\", dest=\"gcp_region\", type=str, required=True,\ + \ default=argparse.SUPPRESS)\n_parser.add_argument(\"--model-display-name\"\ + , dest=\"model_display_name\", type=str, required=True, default=argparse.SUPPRESS)\n\ + _parser.add_argument(\"--bucket-name\", dest=\"bucket_name\", type=str, required=True,\ + \ default=argparse.SUPPRESS)\n_parser.add_argument(\"--eval-data\", dest=\"\ + eval_data_path\", type=str, required=True, default=argparse.SUPPRESS)\n_parser.add_argument(\"\ + --api-endpoint\", dest=\"api_endpoint\", type=str, required=False, default=argparse.SUPPRESS)\n\ + _parser.add_argument(\"--thresholds\", dest=\"thresholds\", type=str, required=False,\ + \ default=argparse.SUPPRESS)\n_parser.add_argument(\"--confidence-threshold\"\ + , dest=\"confidence_threshold\", type=float, required=False, default=argparse.SUPPRESS)\n\ + _parser.add_argument(\"--mlpipeline-ui-metadata\", dest=\"mlpipeline_ui_metadata_path\"\ + , type=_make_parent_dirs_and_return_path, required=True, default=argparse.SUPPRESS)\n\ + _parser.add_argument(\"----output-paths\", dest=\"_output_paths\", type=str,\ + \ nargs=1)\n_parsed_args = vars(_parser.parse_args())\n_output_files = _parsed_args.pop(\"\ + _output_paths\", [])\n\n_outputs = automl_eval_metrics(**_parsed_args)\n\nif\ + \ not hasattr(_outputs, '__getitem__') or isinstance(_outputs, str):\n _outputs\ + \ = [_outputs]\n\n_output_serializers = [\n _serialize_bool,\n\n]\n\nimport\ + \ os\nfor idx, output_file in enumerate(_output_files):\n try:\n os.makedirs(os.path.dirname(output_file))\n\ + \ except OSError:\n pass\n with open(output_file, 'w') as f:\n\ + \ f.write(_output_serializers[idx](_outputs[idx]))\n" + args: + - --gcp-project-id + - inputValue: gcp_project_id + - --gcp-region + - inputValue: gcp_region + - --model-display-name + - inputValue: model_display_name + - --bucket-name + - inputValue: bucket_name + - --eval-data + - inputPath: eval_data + - if: + cond: + isPresent: api_endpoint + then: + - --api-endpoint + - inputValue: api_endpoint + - if: + cond: + isPresent: thresholds + then: + - --thresholds + - inputValue: thresholds + - if: + cond: + isPresent: confidence_threshold + then: + - --confidence-threshold + - inputValue: confidence_threshold + - --mlpipeline-ui-metadata + - outputPath: mlpipeline_ui_metadata + - '----output-paths' + - outputPath: deploy diff --git a/ml/automl/tables/kfp_e2e/deploy_model_for_tables/convert_oss.py b/ml/automl/tables/kfp_e2e/deploy_model_for_tables/convert_oss.py new file mode 100644 index 0000000..52e5a4d --- /dev/null +++ b/ml/automl/tables/kfp_e2e/deploy_model_for_tables/convert_oss.py @@ -0,0 +1,60 @@ +# Copyright 2019 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# tested with TF1.14 +import sys +import tensorflow as tf + +from absl import app +from absl import flags +from tensorflow.core.protobuf import saved_model_pb2 +from tensorflow.python.summary import summary + +FLAGS = flags.FLAGS + +flags.DEFINE_string('saved_model', '', 'The location of the saved_model.pb to visualize.') +flags.DEFINE_string('output_dir', '', 'The location for the Tensorboard log to begin visualization from.') + +def import_to_tensorboard(saved_model, output_dir): + """View an imported saved_model.pb as a graph in Tensorboard. + + Args: + saved_model: The location of the saved_model.pb to visualize. + output_dir: The location for the Tensorboard log to begin visualization from. + + Usage: + Call this function with your model location and desired log directory. + Launch Tensorboard by pointing it to the log directory. + View your imported `.pb` model as a graph. + """ + with open(saved_model, "rb") as f: + sm = saved_model_pb2.SavedModel() + sm.ParseFromString(f.read()) + if 1 != len(sm.meta_graphs): + print('More than one graph found. Not sure which to write') + sys.exit(1) + graph_def = sm.meta_graphs[0].graph_def + + pb_visual_writer = summary.FileWriter(output_dir) + pb_visual_writer.add_graph(None, graph_def=graph_def) + print("Model Imported. Visualize by running: " + "tensorboard --logdir={}".format(output_dir)) + + +def main(argv): + import_to_tensorboard(FLAGS.saved_model, FLAGS.output_dir) + + +if __name__ == '__main__': + app.run(main) diff --git a/ml/automl/tables/kfp_e2e/deploy_model_for_tables/exported_model_deploy.py b/ml/automl/tables/kfp_e2e/deploy_model_for_tables/exported_model_deploy.py new file mode 100644 index 0000000..2106342 --- /dev/null +++ b/ml/automl/tables/kfp_e2e/deploy_model_for_tables/exported_model_deploy.py @@ -0,0 +1,65 @@ +# Copyright 2020 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import os +import logging +import subprocess + + +def main(): + parser = argparse.ArgumentParser(description='Serving webapp') + parser.add_argument( + '--model_name', + required=True) + parser.add_argument( + '--image_name', + required=True) + parser.add_argument( + '--namespace', + default='default') + + args = parser.parse_args() + + NAMESPACE = 'default' + + logging.getLogger().setLevel(logging.INFO) + args_dict = vars(args) + + + logging.info('Generating training template.') + + template_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'model_serve_template.yaml') + target_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'model_serve.yaml') + mname = args.model_name.replace('_', '-') + logging.info("using model name: {}, image {}, and namespace: {}".format( + mname, args.image_name, NAMESPACE)) + + + with open(template_file, 'r') as f: + with open(target_file, "w") as target: + data = f.read() + changed = data.replace('MODEL_NAME', mname).replace( + 'IMAGE_NAME', args.image_name).replace('NAMESPACE', NAMESPACE) + target.write(changed) + + + logging.info('deploying...') + subprocess.call(['kubectl', 'create', '-f', '/ml/model_serve.yaml']) + + # kubectl -n default port-forward svc/ 8080:80 + # curl -X POST --data @./instances.json http://localhost:8080/predict + +if __name__ == "__main__": + main() diff --git a/ml/automl/tables/kfp_e2e/deploy_model_for_tables/instances.json b/ml/automl/tables/kfp_e2e/deploy_model_for_tables/instances.json new file mode 100644 index 0000000..7b7e5e1 --- /dev/null +++ b/ml/automl/tables/kfp_e2e/deploy_model_for_tables/instances.json @@ -0,0 +1,58 @@ +{ + "instances": [ + { + "bike_id": "6179", + "day_of_week": "6", + "end_latitude": 51.50379168, + "end_longitude": -0.11282408, + "end_station_id": "154", + "euclidean": 2513.254047872678, + "loc_cross": "POINT(-0.08 51.52)POINT(-0.11 51.5)", + "max": 56.8, + "min": 50.9, + "prcp": 0, + "ts": 1445624280, + "start_latitude": 51.51615461, + "start_longitude": -0.082422399, + "start_station_id": "217", + "temp": 54, + "dewp": 44 + }, + { + "bike_id": "5373", + "day_of_week": "3", + "end_latitude": 51.52059681, + "end_longitude": -0.116688468, + "end_station_id": "68", + "euclidean": 1181.215448450556, + "loc_cross": "POINT(-0.13 51.53)POINT(-0.12 51.52)", + "max": 56.7, + "min": 45.9, + "prcp": 0, + "ts": 1494317220, + "start_latitude": 51.52683806, + "start_longitude": -0.130504336, + "start_station_id": "214", + "temp": 50.5, + "dewp": 37.1 + }, + { + "bike_id": "5373", + "day_of_week": "3", + "end_latitude": 51.52059681, + "end_longitude": -0.116688468, + "end_station_id": "68", + "euclidean": 3589.5146210024977, + "loc_cross": "POINT(-0.07 51.52)POINT(-0.12 51.52)", + "max": 44.6, + "min": 34.0, + "prcp": 0, + "ts": 1480407420, + "start_latitude": 51.52388, + "start_longitude": -0.065076, + "start_station_id": "445", + "temp": 38.2, + "dewp": 28.6 + } + ] +} diff --git a/ml/automl/tables/kfp_e2e/deploy_model_for_tables/model_serve_template.yaml b/ml/automl/tables/kfp_e2e/deploy_model_for_tables/model_serve_template.yaml new file mode 100644 index 0000000..3727511 --- /dev/null +++ b/ml/automl/tables/kfp_e2e/deploy_model_for_tables/model_serve_template.yaml @@ -0,0 +1,51 @@ +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app: MODEL_NAME + name: MODEL_NAME + namespace: NAMESPACE +spec: + ports: + - name: model-serving + port: 80 + targetPort: "http-server" + selector: + app: MODEL_NAME + type: ClusterIP +--- +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + labels: + app: MODEL_NAME + name: MODEL_NAME-dep + namespace: NAMESPACE +spec: + replicas: 2 + template: + metadata: + labels: + app: MODEL_NAME + version: v1 + spec: + containers: + - name: MODEL_NAME + image: IMAGE_NAME + imagePullPolicy: Always + livenessProbe: + initialDelaySeconds: 30 + periodSeconds: 30 + tcpSocket: + port: 8080 + ports: + - name: http-server + containerPort: 8080 + resources: + limits: + cpu: "4" + memory: 4Gi + requests: + cpu: "1" + memory: 1Gi diff --git a/ml/automl/tables/kfp_e2e/deploy_model_for_tables/tables_deploy_component.py b/ml/automl/tables/kfp_e2e/deploy_model_for_tables/tables_deploy_component.py new file mode 100644 index 0000000..6df7c4e --- /dev/null +++ b/ml/automl/tables/kfp_e2e/deploy_model_for_tables/tables_deploy_component.py @@ -0,0 +1,76 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import NamedTuple + +def automl_deploy_tables_model( + gcp_project_id: str, + gcp_region: str, + # dataset_display_name: str, + model_display_name: str, + api_endpoint: str = None, +) -> NamedTuple('Outputs', [('model_display_name', str), ('status', str)]): + import subprocess + import sys + subprocess.run([sys.executable, '-m', 'pip', 'install', 'googleapis-common-protos==1.6.0', '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) + subprocess.run([sys.executable, '-m', 'pip', 'install', 'google-cloud-automl==0.9.0', '--quiet', '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) + + import google + import logging + from google.api_core.client_options import ClientOptions + from google.api_core import exceptions + from google.cloud import automl_v1beta1 as automl + from google.cloud.automl_v1beta1 import enums + + logging.getLogger().setLevel(logging.INFO) # TODO: make level configurable + # TODO: we could instead check for region 'eu' and use 'eu-automl.googleapis.com:443'endpoint + # in that case, instead of requiring endpoint to be specified. + if api_endpoint: + client_options = ClientOptions(api_endpoint=api_endpoint) + client = automl.TablesClient(project=gcp_project_id, region=gcp_region, + client_options=client_options) + else: + client = automl.TablesClient(project=gcp_project_id, region=gcp_region) + + + try: + model = client.get_model(model_display_name=model_display_name) + if model.deployment_state == enums.Model.DeploymentState.DEPLOYED: + status = 'deployed' + logging.info('Model {} already deployed'.format(model_display_name)) + else: + logging.info('Deploying model {}'.format(model_display_name)) + response = client.deploy_model(model_display_name=model_display_name) + # synchronous wait + logging.info("Model deployed. {}".format(response.result())) + status = 'deployed' + except exceptions.NotFound as e: + logging.warning(e) + status = 'not_found' + except Exception as e: + logging.warning(e) + status = 'undeployed' + + logging.info('Model status: {}'.format(status)) + return (model_display_name, status) + + + +if __name__ == '__main__': + import kfp + kfp.components.func_to_container_op(automl_deploy_tables_model, output_component_file='tables_deploy_component.yaml', base_image='python:3.7') + + +# if __name__ == "__main__": + # automl_deploy_tables_model('aju-vtests2', 'us-central1', model_display_name='so_digest2_20191220032828' ) diff --git a/ml/automl/tables/kfp_e2e/deploy_model_for_tables/tables_deploy_component.yaml b/ml/automl/tables/kfp_e2e/deploy_model_for_tables/tables_deploy_component.yaml new file mode 100644 index 0000000..37949d2 --- /dev/null +++ b/ml/automl/tables/kfp_e2e/deploy_model_for_tables/tables_deploy_component.yaml @@ -0,0 +1,83 @@ +name: Automl deploy tables model +inputs: +- name: gcp_project_id + type: String +- name: gcp_region + type: String +- name: model_display_name + type: String +- name: api_endpoint + type: String + optional: true +outputs: +- name: model_display_name + type: String +- name: status + type: String +implementation: + container: + image: python:3.7 + command: + - python3 + - -u + - -c + - "from typing import NamedTuple\n\ndef automl_deploy_tables_model(\n\tgcp_project_id:\ + \ str,\n\tgcp_region: str,\n\t# dataset_display_name: str,\n model_display_name:\ + \ str,\n api_endpoint: str = None,\n) -> NamedTuple('Outputs', [('model_display_name',\ + \ str), ('status', str)]):\n import subprocess\n import sys\n subprocess.run([sys.executable,\ + \ '-m', 'pip', 'install', 'googleapis-common-protos==1.6.0', '--no-warn-script-location'],\ + \ env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True)\n subprocess.run([sys.executable,\ + \ '-m', 'pip', 'install', 'google-cloud-automl==0.9.0', '--quiet', '--no-warn-script-location'],\ + \ env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True)\n\n import google\n\ + \ import logging\n from google.api_core.client_options import ClientOptions\n\ + \ from google.api_core import exceptions\n from google.cloud import automl_v1beta1\ + \ as automl\n from google.cloud.automl_v1beta1 import enums\n\n logging.getLogger().setLevel(logging.INFO)\ + \ # TODO: make level configurable\n # TODO: we could instead check for region\ + \ 'eu' and use 'eu-automl.googleapis.com:443'endpoint\n # in that case, instead\ + \ of requiring endpoint to be specified.\n if api_endpoint:\n client_options\ + \ = ClientOptions(api_endpoint=api_endpoint)\n client = automl.TablesClient(project=gcp_project_id,\ + \ region=gcp_region,\n client_options=client_options)\n else:\n client\ + \ = automl.TablesClient(project=gcp_project_id, region=gcp_region)\n\n try:\n\ + \ model = client.get_model(model_display_name=model_display_name)\n if\ + \ model.deployment_state == enums.Model.DeploymentState.DEPLOYED:\n status\ + \ = 'deployed'\n logging.info('Model {} already deployed'.format(model_display_name))\n\ + \ else:\n logging.info('Deploying model {}'.format(model_display_name))\n\ + \ response = client.deploy_model(model_display_name=model_display_name)\n\ + \ # synchronous wait\n logging.info(\"Model deployed. {}\".format(response.result()))\n\ + \ status = 'deployed'\n except exceptions.NotFound as e:\n logging.warning(e)\n\ + \ status = 'not_found'\n except Exception as e:\n logging.warning(e)\n\ + \ status = 'undeployed'\n\n logging.info('Model status: {}'.format(status))\n\ + \ return (model_display_name, status)\n\ndef _serialize_str(str_value: str)\ + \ -> str:\n if not isinstance(str_value, str):\n raise TypeError('Value\ + \ \"{}\" has type \"{}\" instead of str.'.format(str(str_value), str(type(str_value))))\n\ + \ return str_value\n\nimport argparse\n_parser = argparse.ArgumentParser(prog='Automl\ + \ deploy tables model', description='')\n_parser.add_argument(\"--gcp-project-id\"\ + , dest=\"gcp_project_id\", type=str, required=True, default=argparse.SUPPRESS)\n\ + _parser.add_argument(\"--gcp-region\", dest=\"gcp_region\", type=str, required=True,\ + \ default=argparse.SUPPRESS)\n_parser.add_argument(\"--model-display-name\"\ + , dest=\"model_display_name\", type=str, required=True, default=argparse.SUPPRESS)\n\ + _parser.add_argument(\"--api-endpoint\", dest=\"api_endpoint\", type=str, required=False,\ + \ default=argparse.SUPPRESS)\n_parser.add_argument(\"----output-paths\", dest=\"\ + _output_paths\", type=str, nargs=2)\n_parsed_args = vars(_parser.parse_args())\n\ + _output_files = _parsed_args.pop(\"_output_paths\", [])\n\n_outputs = automl_deploy_tables_model(**_parsed_args)\n\ + \nif not hasattr(_outputs, '__getitem__') or isinstance(_outputs, str):\n \ + \ _outputs = [_outputs]\n\n_output_serializers = [\n _serialize_str,\n \ + \ _serialize_str\n]\n\nimport os\nfor idx, output_file in enumerate(_output_files):\n\ + \ try:\n os.makedirs(os.path.dirname(output_file))\n except OSError:\n\ + \ pass\n with open(output_file, 'w') as f:\n f.write(_output_serializers[idx](_outputs[idx]))\n" + args: + - --gcp-project-id + - inputValue: gcp_project_id + - --gcp-region + - inputValue: gcp_region + - --model-display-name + - inputValue: model_display_name + - if: + cond: + isPresent: api_endpoint + then: + - --api-endpoint + - inputValue: api_endpoint + - '----output-paths' + - outputPath: model_display_name + - outputPath: status diff --git a/ml/automl/tables/kfp_e2e/import_data_from_bigquery/tables_component.py b/ml/automl/tables/kfp_e2e/import_data_from_bigquery/tables_component.py new file mode 100644 index 0000000..a62e5e2 --- /dev/null +++ b/ml/automl/tables/kfp_e2e/import_data_from_bigquery/tables_component.py @@ -0,0 +1,99 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from typing import NamedTuple + +def automl_import_data_for_tables( + # dataset_path, + path: str, + gcp_project_id: str, + gcp_region: str, + dataset_display_name: str, + api_endpoint: str = None, +) -> NamedTuple('Outputs', [('dataset_display_name', str)]): + import sys + import subprocess + subprocess.run([sys.executable, '-m', 'pip', 'install', 'googleapis-common-protos==1.6.0', '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) + subprocess.run([sys.executable, '-m', 'pip', 'install', 'google-cloud-automl==0.9.0', '--quiet', '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) + + import google + import logging + from google.api_core.client_options import ClientOptions + from google.cloud import automl_v1beta1 as automl + + def list_column_specs(client, + dataset_display_name, + filter_=None): + """List all column specs.""" + result = [] + + # List all the table specs in the dataset + response = client.list_column_specs( + dataset_display_name=dataset_display_name, filter_=filter_) + logging.info("List of column specs:") + for column_spec in response: + # Display the column_spec information. + logging.info("Column spec name: {}".format(column_spec.name)) + logging.info("Column spec id: {}".format(column_spec.name.split("/")[-1])) + logging.info("Column spec display name: {}".format(column_spec.display_name)) + logging.info("Column spec data type: {}".format(column_spec.data_type)) + + result.append(column_spec) + return result + + logging.getLogger().setLevel(logging.INFO) # TODO: make level configurable + + # TODO: we could instead check for region 'eu' and use 'eu-automl.googleapis.com:443'endpoint + # in that case, instead of requiring endpoint to be specified. + if api_endpoint: + client_options = ClientOptions(api_endpoint=api_endpoint) + client = automl.TablesClient(project=gcp_project_id, region=gcp_region, + client_options=client_options) + else: + client = automl.TablesClient(project=gcp_project_id, region=gcp_region) + + response = None + if path.startswith('bq'): + response = client.import_data( + dataset_display_name=dataset_display_name, bigquery_input_uri=path + ) + else: + # Get the multiple Google Cloud Storage URIs. + input_uris = path.split(",") + response = client.import_data( + dataset_display_name=dataset_display_name, + gcs_input_uris=input_uris + ) + logging.info("Processing import... This can take a while.") + # synchronous check of operation status. + logging.info("Data imported. {}".format(response.result())) + logging.info("Response metadata: {}".format(response.metadata)) + logging.info("Operation name: {}".format(response.operation.name)) + + # now list the inferred col schema + list_column_specs(client, dataset_display_name) + return (dataset_display_name) + + + +# if __name__ == "__main__": +# automl_import_data_for_tables('bq://aju-dev-demos.london_bikes_weather.bikes_weather', +# 'aju-vtests2', 'us-central1', 'comp_test1') + + +if __name__ == '__main__': + import kfp + kfp.components.func_to_container_op(automl_import_data_for_tables, + output_component_file='tables_component.yaml', base_image='python:3.7') diff --git a/ml/automl/tables/kfp_e2e/import_data_from_bigquery/tables_component.yaml b/ml/automl/tables/kfp_e2e/import_data_from_bigquery/tables_component.yaml new file mode 100644 index 0000000..921684f --- /dev/null +++ b/ml/automl/tables/kfp_e2e/import_data_from_bigquery/tables_component.yaml @@ -0,0 +1,147 @@ +name: Automl import data for tables +inputs: +- name: path + type: String +- name: gcp_project_id + type: String +- name: gcp_region + type: String +- name: dataset_display_name + type: String +- name: api_endpoint + type: String + optional: true +outputs: +- name: dataset_display_name + type: String +implementation: + container: + image: python:3.7 + command: + - python3 + - -u + - -c + - | + from typing import NamedTuple + + def automl_import_data_for_tables( + # dataset_path, + path: str, + gcp_project_id: str, + gcp_region: str, + dataset_display_name: str, + api_endpoint: str = None, + ) -> NamedTuple('Outputs', [('dataset_display_name', str)]): + import sys + import subprocess + subprocess.run([sys.executable, '-m', 'pip', 'install', 'googleapis-common-protos==1.6.0', '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) + subprocess.run([sys.executable, '-m', 'pip', 'install', 'google-cloud-automl==0.9.0', '--quiet', '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) + + import google + import logging + from google.api_core.client_options import ClientOptions + from google.cloud import automl_v1beta1 as automl + + def list_column_specs(client, + dataset_display_name, + filter_=None): + """List all column specs.""" + result = [] + + # List all the table specs in the dataset + response = client.list_column_specs( + dataset_display_name=dataset_display_name, filter_=filter_) + logging.info("List of column specs:") + for column_spec in response: + # Display the column_spec information. + logging.info("Column spec name: {}".format(column_spec.name)) + logging.info("Column spec id: {}".format(column_spec.name.split("/")[-1])) + logging.info("Column spec display name: {}".format(column_spec.display_name)) + logging.info("Column spec data type: {}".format(column_spec.data_type)) + + result.append(column_spec) + return result + + logging.getLogger().setLevel(logging.INFO) # TODO: make level configurable + + # TODO: we could instead check for region 'eu' and use 'eu-automl.googleapis.com:443'endpoint + # in that case, instead of requiring endpoint to be specified. + if api_endpoint: + client_options = ClientOptions(api_endpoint=api_endpoint) + client = automl.TablesClient(project=gcp_project_id, region=gcp_region, + client_options=client_options) + else: + client = automl.TablesClient(project=gcp_project_id, region=gcp_region) + + response = None + if path.startswith('bq'): + response = client.import_data( + dataset_display_name=dataset_display_name, bigquery_input_uri=path + ) + else: + # Get the multiple Google Cloud Storage URIs. + input_uris = path.split(",") + response = client.import_data( + dataset_display_name=dataset_display_name, + gcs_input_uris=input_uris + ) + logging.info("Processing import... This can take a while.") + # synchronous check of operation status. + logging.info("Data imported. {}".format(response.result())) + logging.info("Response metadata: {}".format(response.metadata)) + logging.info("Operation name: {}".format(response.operation.name)) + + # now list the inferred col schema + list_column_specs(client, dataset_display_name) + return (dataset_display_name) + + def _serialize_str(str_value: str) -> str: + if not isinstance(str_value, str): + raise TypeError('Value "{}" has type "{}" instead of str.'.format(str(str_value), str(type(str_value)))) + return str_value + + import argparse + _parser = argparse.ArgumentParser(prog='Automl import data for tables', description='') + _parser.add_argument("--path", dest="path", type=str, required=True, default=argparse.SUPPRESS) + _parser.add_argument("--gcp-project-id", dest="gcp_project_id", type=str, required=True, default=argparse.SUPPRESS) + _parser.add_argument("--gcp-region", dest="gcp_region", type=str, required=True, default=argparse.SUPPRESS) + _parser.add_argument("--dataset-display-name", dest="dataset_display_name", type=str, required=True, default=argparse.SUPPRESS) + _parser.add_argument("--api-endpoint", dest="api_endpoint", type=str, required=False, default=argparse.SUPPRESS) + _parser.add_argument("----output-paths", dest="_output_paths", type=str, nargs=1) + _parsed_args = vars(_parser.parse_args()) + _output_files = _parsed_args.pop("_output_paths", []) + + _outputs = automl_import_data_for_tables(**_parsed_args) + + if not hasattr(_outputs, '__getitem__') or isinstance(_outputs, str): + _outputs = [_outputs] + + _output_serializers = [ + _serialize_str + ] + + import os + for idx, output_file in enumerate(_output_files): + try: + os.makedirs(os.path.dirname(output_file)) + except OSError: + pass + with open(output_file, 'w') as f: + f.write(_output_serializers[idx](_outputs[idx])) + args: + - --path + - inputValue: path + - --gcp-project-id + - inputValue: gcp_project_id + - --gcp-region + - inputValue: gcp_region + - --dataset-display-name + - inputValue: dataset_display_name + - if: + cond: + isPresent: api_endpoint + then: + - --api-endpoint + - inputValue: api_endpoint + - '----output-paths' + - outputPath: dataset_display_name diff --git a/ml/automl/tables/kfp_e2e/import_data_from_bigquery/tables_schema_component.py b/ml/automl/tables/kfp_e2e/import_data_from_bigquery/tables_schema_component.py new file mode 100644 index 0000000..36e8765 --- /dev/null +++ b/ml/automl/tables/kfp_e2e/import_data_from_bigquery/tables_schema_component.py @@ -0,0 +1,119 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from typing import NamedTuple + + +def automl_set_dataset_schema( + gcp_project_id: str, + gcp_region: str, + display_name: str, + target_col_name: str, + schema_info: str = '{}', # dict with key of col name, value an array with [type, nullable] + time_col_name: str = None, + test_train_col_name: str = None, + api_endpoint: str = None, +) -> NamedTuple('Outputs', [('display_name', str)]): + import sys + import subprocess + subprocess.run([sys.executable, '-m', 'pip', 'install', 'googleapis-common-protos==1.6.0', '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) + subprocess.run([sys.executable, '-m', 'pip', 'install', 'google-cloud-automl==0.9.0', '--quiet', '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) + + import json + import google + import logging + from google.api_core.client_options import ClientOptions + from google.cloud import automl_v1beta1 as automl + + def update_column_spec(client, + dataset_display_name, + column_spec_display_name, + type_code, + nullable=None + ): + + logging.info("Setting {} to type {} and nullable {}".format( + column_spec_display_name, type_code, nullable)) + response = client.update_column_spec( + dataset_display_name=dataset_display_name, + column_spec_display_name=column_spec_display_name, + type_code=type_code, + nullable=nullable + ) + + # synchronous check of operation status. + print("Table spec updated. {}".format(response)) + + def update_dataset(client, + dataset_display_name, + target_column_spec_name=None, + time_column_spec_name=None, + test_train_column_spec_name=None): + + if target_column_spec_name: + response = client.set_target_column( + dataset_display_name=dataset_display_name, + column_spec_display_name=target_column_spec_name + ) + print("Target column updated. {}".format(response)) + if time_column_spec_name: + response = client.set_time_column( + dataset_display_name=dataset_display_name, + column_spec_display_name=time_column_spec_name + ) + print("Time column updated. {}".format(response)) + + logging.getLogger().setLevel(logging.INFO) # TODO: make level configurable + + + # TODO: we could instead check for region 'eu' and use 'eu-automl.googleapis.com:443'endpoint + # in that case, instead of requiring endpoint to be specified. + if api_endpoint: + client_options = ClientOptions(api_endpoint=api_endpoint) + client = automl.TablesClient(project=gcp_project_id, region=gcp_region, + client_options=client_options) + else: + client = automl.TablesClient(project=gcp_project_id, region=gcp_region) + + schema_dict = json.loads(schema_info) + # Update cols for which the desired schema was not inferred. + if schema_dict: + for k,v in schema_dict.items(): + update_column_spec(client, display_name, k, v[0], nullable=v[1]) + + # Update the dataset with info about the target col, plus optionally info on how to split on + # a time col or a test/train col. + update_dataset(client, display_name, + target_column_spec_name=target_col_name, + time_column_spec_name=time_col_name, + test_train_column_spec_name=test_train_col_name) + + return (display_name) + + +# if __name__ == "__main__": +# import json +# # sdict = {"end_station_id": "CATEGORY", "start_station_id":"CATEGORY", "loc_cross": "CATEGORY", "bike_id": "CATEGORY"} +# sdict = {"accepted_answer_id": ["CATEGORY", True], "id": ["CATEGORY", True], "last_editor_display_name": ["CATEGORY", True], "last_editor_user_id": ["CATEGORY", True], +# "owner_display_name": ["CATEGORY", True], "owner_user_id": ["CATEGORY", True], +# "parent_id": ["CATEGORY", True], "post_type_id": ["CATEGORY", True], "tags": ["CATEGORY", True]} +# automl_set_dataset_schema('aju-vtests2', 'us-central1', 'so_test1', +# 't1', json.dumps(sdict)) + + +if __name__ == '__main__': + import kfp + kfp.components.func_to_container_op(automl_set_dataset_schema, + output_component_file='tables_schema_component.yaml', base_image='python:3.7') diff --git a/ml/automl/tables/kfp_e2e/import_data_from_bigquery/tables_schema_component.yaml b/ml/automl/tables/kfp_e2e/import_data_from_bigquery/tables_schema_component.yaml new file mode 100644 index 0000000..0b6da83 --- /dev/null +++ b/ml/automl/tables/kfp_e2e/import_data_from_bigquery/tables_schema_component.yaml @@ -0,0 +1,192 @@ +name: Automl set dataset schema +inputs: +- name: gcp_project_id + type: String +- name: gcp_region + type: String +- name: display_name + type: String +- name: target_col_name + type: String +- name: schema_info + type: String + default: '{}' + optional: true +- name: time_col_name + type: String + optional: true +- name: test_train_col_name + type: String + optional: true +- name: api_endpoint + type: String + optional: true +outputs: +- name: display_name + type: String +implementation: + container: + image: python:3.7 + command: + - python3 + - -u + - -c + - | + from typing import NamedTuple + + def automl_set_dataset_schema( + gcp_project_id: str, + gcp_region: str, + display_name: str, + target_col_name: str, + schema_info: str = '{}', # dict with key of col name, value an array with [type, nullable] + time_col_name: str = None, + test_train_col_name: str = None, + api_endpoint: str = None, + ) -> NamedTuple('Outputs', [('display_name', str)]): + import sys + import subprocess + subprocess.run([sys.executable, '-m', 'pip', 'install', 'googleapis-common-protos==1.6.0', '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) + subprocess.run([sys.executable, '-m', 'pip', 'install', 'google-cloud-automl==0.9.0', '--quiet', '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) + + import json + import google + import logging + from google.api_core.client_options import ClientOptions + from google.cloud import automl_v1beta1 as automl + + def update_column_spec(client, + dataset_display_name, + column_spec_display_name, + type_code, + nullable=None + ): + + logging.info("Setting {} to type {} and nullable {}".format( + column_spec_display_name, type_code, nullable)) + response = client.update_column_spec( + dataset_display_name=dataset_display_name, + column_spec_display_name=column_spec_display_name, + type_code=type_code, + nullable=nullable + ) + + # synchronous check of operation status. + print("Table spec updated. {}".format(response)) + + def update_dataset(client, + dataset_display_name, + target_column_spec_name=None, + time_column_spec_name=None, + test_train_column_spec_name=None): + + if target_column_spec_name: + response = client.set_target_column( + dataset_display_name=dataset_display_name, + column_spec_display_name=target_column_spec_name + ) + print("Target column updated. {}".format(response)) + if time_column_spec_name: + response = client.set_time_column( + dataset_display_name=dataset_display_name, + column_spec_display_name=time_column_spec_name + ) + print("Time column updated. {}".format(response)) + + logging.getLogger().setLevel(logging.INFO) # TODO: make level configurable + + # TODO: we could instead check for region 'eu' and use 'eu-automl.googleapis.com:443'endpoint + # in that case, instead of requiring endpoint to be specified. + if api_endpoint: + client_options = ClientOptions(api_endpoint=api_endpoint) + client = automl.TablesClient(project=gcp_project_id, region=gcp_region, + client_options=client_options) + else: + client = automl.TablesClient(project=gcp_project_id, region=gcp_region) + + schema_dict = json.loads(schema_info) + # Update cols for which the desired schema was not inferred. + if schema_dict: + for k,v in schema_dict.items(): + update_column_spec(client, display_name, k, v[0], nullable=v[1]) + + # Update the dataset with info about the target col, plus optionally info on how to split on + # a time col or a test/train col. + update_dataset(client, display_name, + target_column_spec_name=target_col_name, + time_column_spec_name=time_col_name, + test_train_column_spec_name=test_train_col_name) + + return (display_name) + + def _serialize_str(str_value: str) -> str: + if not isinstance(str_value, str): + raise TypeError('Value "{}" has type "{}" instead of str.'.format(str(str_value), str(type(str_value)))) + return str_value + + import argparse + _parser = argparse.ArgumentParser(prog='Automl set dataset schema', description='') + _parser.add_argument("--gcp-project-id", dest="gcp_project_id", type=str, required=True, default=argparse.SUPPRESS) + _parser.add_argument("--gcp-region", dest="gcp_region", type=str, required=True, default=argparse.SUPPRESS) + _parser.add_argument("--display-name", dest="display_name", type=str, required=True, default=argparse.SUPPRESS) + _parser.add_argument("--target-col-name", dest="target_col_name", type=str, required=True, default=argparse.SUPPRESS) + _parser.add_argument("--schema-info", dest="schema_info", type=str, required=False, default=argparse.SUPPRESS) + _parser.add_argument("--time-col-name", dest="time_col_name", type=str, required=False, default=argparse.SUPPRESS) + _parser.add_argument("--test-train-col-name", dest="test_train_col_name", type=str, required=False, default=argparse.SUPPRESS) + _parser.add_argument("--api-endpoint", dest="api_endpoint", type=str, required=False, default=argparse.SUPPRESS) + _parser.add_argument("----output-paths", dest="_output_paths", type=str, nargs=1) + _parsed_args = vars(_parser.parse_args()) + _output_files = _parsed_args.pop("_output_paths", []) + + _outputs = automl_set_dataset_schema(**_parsed_args) + + if not hasattr(_outputs, '__getitem__') or isinstance(_outputs, str): + _outputs = [_outputs] + + _output_serializers = [ + _serialize_str + ] + + import os + for idx, output_file in enumerate(_output_files): + try: + os.makedirs(os.path.dirname(output_file)) + except OSError: + pass + with open(output_file, 'w') as f: + f.write(_output_serializers[idx](_outputs[idx])) + args: + - --gcp-project-id + - inputValue: gcp_project_id + - --gcp-region + - inputValue: gcp_region + - --display-name + - inputValue: display_name + - --target-col-name + - inputValue: target_col_name + - if: + cond: + isPresent: schema_info + then: + - --schema-info + - inputValue: schema_info + - if: + cond: + isPresent: time_col_name + then: + - --time-col-name + - inputValue: time_col_name + - if: + cond: + isPresent: test_train_col_name + then: + - --test-train-col-name + - inputValue: test_train_col_name + - if: + cond: + isPresent: api_endpoint + then: + - --api-endpoint + - inputValue: api_endpoint + - '----output-paths' + - outputPath: display_name diff --git a/ml/automl/tables/kfp_e2e/tables_containers/model-service-launcher/Dockerfile b/ml/automl/tables/kfp_e2e/tables_containers/model-service-launcher/Dockerfile new file mode 100644 index 0000000..cb31ca1 --- /dev/null +++ b/ml/automl/tables/kfp_e2e/tables_containers/model-service-launcher/Dockerfile @@ -0,0 +1,49 @@ +# Copyright 2020 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +FROM ubuntu:18.04 + +RUN apt-get update \ + && apt-get install -y python3-pip python3-dev \ + && cd /usr/local/bin \ + && ln -s /usr/bin/python3 python \ + && pip3 install --upgrade pip + +RUN apt-get install -y wget unzip git + +RUN pip install --upgrade pip +RUN pip install urllib3 certifi retrying + +# RUN apt-get install --no-install-recommends -y -q ca-certificates python-dev python-setuptools wget unzip + +# RUN pip install pyyaml==3.12 six==1.11.0 requests==2.18.4 + +RUN wget -nv https://dl.google.com/dl/cloudsdk/release/google-cloud-sdk.zip && \ + unzip -qq google-cloud-sdk.zip -d tools && \ + rm google-cloud-sdk.zip && \ + tools/google-cloud-sdk/install.sh --usage-reporting=false \ + --path-update=false --bash-completion=false \ + --disable-installation-options && \ + tools/google-cloud-sdk/bin/gcloud -q components update \ + gcloud core gsutil && \ + tools/google-cloud-sdk/bin/gcloud -q components install kubectl && \ + tools/google-cloud-sdk/bin/gcloud config set component_manager/disable_update_check true && \ + touch /tools/google-cloud-sdk/lib/third_party/google.py + + +ENV PATH $PATH:/tools/google-cloud-sdk/bin + +ADD build /ml + +ENTRYPOINT ["python", "/ml/exported_model_deploy.py"] diff --git a/ml/automl/tables/kfp_e2e/tables_containers/model-service-launcher/build.sh b/ml/automl/tables/kfp_e2e/tables_containers/model-service-launcher/build.sh new file mode 100755 index 0000000..3ff5cda --- /dev/null +++ b/ml/automl/tables/kfp_e2e/tables_containers/model-service-launcher/build.sh @@ -0,0 +1,31 @@ +#!/bin/bash -e +# Copyright 2020 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +if [ -z "$1" ] + then + PROJECT_ID=$(gcloud config config-helper --format "value(configuration.properties.core.project)") +else + PROJECT_ID=$1 +fi + +mkdir -p ./build +rsync -arvp "../../deploy_model_for_tables"/ ./build/ + +docker build -t model-service-launcher . +rm -rf ./build + +docker tag model-service-launcher gcr.io/${PROJECT_ID}/model-service-launcher +docker push gcr.io/${PROJECT_ID}/model-service-launcher diff --git a/ml/automl/tables/kfp_e2e/tables_pipeline_caip.py b/ml/automl/tables/kfp_e2e/tables_pipeline_caip.py new file mode 100644 index 0000000..9be2e67 --- /dev/null +++ b/ml/automl/tables/kfp_e2e/tables_pipeline_caip.py @@ -0,0 +1,151 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import kfp.dsl as dsl +import kfp.gcp as gcp +import kfp.components as comp +# from kfp.dsl.types import GCSPath, String, Dict +import json +import time + +DEFAULT_SCHEMA = json.dumps({"end_station_id": ["CATEGORY", True], "start_station_id": ["CATEGORY", True], + "loc_cross": ["CATEGORY", True], "bike_id": ["CATEGORY", True]}) +# DEFAULT_SCHEMA = json.dumps({"accepted_answer_id": ["CATEGORY", True], "id": ["CATEGORY", True], + # "last_editor_display_name": ["CATEGORY", True], "last_editor_user_id": ["CATEGORY", True], + # "owner_display_name": ["CATEGORY", True], "owner_user_id": ["CATEGORY", True], + # "parent_id": ["CATEGORY", True], "post_type_id": ["CATEGORY", True], "tags": ["CATEGORY", True]}) + + +create_dataset_op = comp.load_component_from_file( + './create_dataset_for_tables/tables_component.yaml' + ) +import_data_op = comp.load_component_from_file( + './import_data_from_bigquery/tables_component.yaml' + ) +set_schema_op = comp.load_component_from_file( + './import_data_from_bigquery/tables_schema_component.yaml' + ) +train_model_op = comp.load_component_from_file( + './create_model_for_tables/tables_component.yaml') +eval_model_op = comp.load_component_from_file( + './create_model_for_tables/tables_eval_component.yaml') +eval_metrics_op = comp.load_component_from_file( + './create_model_for_tables/tables_eval_metrics_component.yaml') +deploy_model_op = comp.load_component_from_file( + './deploy_model_for_tables/tables_deploy_component.yaml' + ) + +@dsl.pipeline( + name='AutoML Tables', + description='Demonstrate an AutoML Tables workflow' +) +def automl_tables( #pylint: disable=unused-argument + gcp_project_id: str = 'YOUR_PROJECT_HERE', + gcp_region: str = 'us-central1', + dataset_display_name: str = 'YOUR_DATASET_NAME', + api_endpoint: str = '', + path: str = 'bq://aju-dev-demos.london_bikes_weather.bikes_weather', + target_col_name: str = 'duration', + time_col_name: str = '', + # test_train_col_name: str = '', + # schema dict with col name as key, type as value + schema_info: str = DEFAULT_SCHEMA, + train_budget_milli_node_hours: 'Integer' = 1000, + model_prefix: str = 'bwmodel', + # one of strings: [MAXIMIZE_AU_ROC, MAXIMIZE_AU_PRC, MINIMIZE_LOG_LOSS, MAXIMIZE_RECALL_AT_PRECISION, MAXIMIZE_PRECISION_AT_RECALL, MINIMIZE_RMSE, MINIMIZE_MAE, MINIMIZE_RMSLE] + optimization_objective: str = '', # if not set, will use default + # ["title", "body", "answer_count", "comment_count", "creation_date", "favorite_count", "owner_user_id", "score", "view_count"] + include_column_spec_names: str = '', + exclude_column_spec_names: str = '', + bucket_name: str = 'aju-pipelines', + # thresholds: str = '{"au_prc": 0.9}', + thresholds: str = '{"mean_absolute_error": 480}', + ): + + create_dataset = create_dataset_op( + gcp_project_id=gcp_project_id, + gcp_region=gcp_region, + dataset_display_name=dataset_display_name, + api_endpoint=api_endpoint, + ) #.apply(gcp.use_gcp_secret('user-gcp-sa')) + + + import_data = import_data_op( + gcp_project_id=gcp_project_id, + gcp_region=gcp_region, + dataset_display_name=dataset_display_name, + api_endpoint=api_endpoint, + path=path + ) #.apply(gcp.use_gcp_secret('user-gcp-sa')) + + set_schema = set_schema_op( + gcp_project_id=gcp_project_id, + gcp_region=gcp_region, + display_name=dataset_display_name, + api_endpoint=api_endpoint, + target_col_name=target_col_name, + schema_info=schema_info, + time_col_name=time_col_name + # test_train_col_name=test_train_col_name + ) #.apply(gcp.use_gcp_secret('user-gcp-sa')) + + + import_data.after(create_dataset) + set_schema.after(import_data) + + train_model = train_model_op( + gcp_project_id=gcp_project_id, + gcp_region=gcp_region, + dataset_display_name=dataset_display_name, + api_endpoint=api_endpoint, + model_prefix=model_prefix, + train_budget_milli_node_hours=train_budget_milli_node_hours, + optimization_objective=optimization_objective + ) #.apply(gcp.use_gcp_secret('user-gcp-sa')) + + train_model.after(set_schema) + + eval_model = eval_model_op( + gcp_project_id=gcp_project_id, + gcp_region=gcp_region, + bucket_name=bucket_name, + gcs_path='automl_evals/{}'.format(dsl.RUN_ID_PLACEHOLDER), + api_endpoint=api_endpoint, + model_display_name=train_model.outputs['model_display_name'] + ) #.apply(gcp.use_gcp_secret('user-gcp-sa')) + + eval_metrics = eval_metrics_op( + gcp_project_id=gcp_project_id, + gcp_region=gcp_region, + bucket_name=bucket_name, + api_endpoint=api_endpoint, + model_display_name=train_model.outputs['model_display_name'], + thresholds=thresholds, + eval_data=eval_model.outputs['eval_data'], + # gcs_path=eval_model.outputs['evals_gcs_path'] + ) #.apply(gcp.use_gcp_secret('user-gcp-sa')) + + with dsl.Condition(eval_metrics.outputs['deploy'] == True): + deploy_model = deploy_model_op( + gcp_project_id=gcp_project_id, + gcp_region=gcp_region, + api_endpoint=api_endpoint, + model_display_name=train_model.outputs['model_display_name'], + ) #.apply(gcp.use_gcp_secret('user-gcp-sa')) + + +if __name__ == '__main__': + import kfp.compiler as compiler + compiler.Compiler().compile(automl_tables, __file__ + '.tar.gz') diff --git a/ml/automl/tables/kfp_e2e/tables_pipeline_kf.py b/ml/automl/tables/kfp_e2e/tables_pipeline_kf.py new file mode 100644 index 0000000..4ec1de7 --- /dev/null +++ b/ml/automl/tables/kfp_e2e/tables_pipeline_kf.py @@ -0,0 +1,151 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import kfp.dsl as dsl +import kfp.gcp as gcp +import kfp.components as comp +from kfp.dsl.types import GCSPath, String, Dict +import json +import time + +DEFAULT_SCHEMA = json.dumps({"end_station_id": ["CATEGORY", True], "start_station_id": ["CATEGORY", True], + "loc_cross": ["CATEGORY", True], "bike_id": ["CATEGORY", True]}) +# DEFAULT_SCHEMA = json.dumps({"accepted_answer_id": ["CATEGORY", True], "id": ["CATEGORY", True], + # "last_editor_display_name": ["CATEGORY", True], "last_editor_user_id": ["CATEGORY", True], + # "owner_display_name": ["CATEGORY", True], "owner_user_id": ["CATEGORY", True], + # "parent_id": ["CATEGORY", True], "post_type_id": ["CATEGORY", True], "tags": ["CATEGORY", True]}) + + +create_dataset_op = comp.load_component_from_file( + './create_dataset_for_tables/tables_component.yaml' + ) +import_data_op = comp.load_component_from_file( + './import_data_from_bigquery/tables_component.yaml' + ) +set_schema_op = comp.load_component_from_file( + './import_data_from_bigquery/tables_schema_component.yaml' + ) +train_model_op = comp.load_component_from_file( + './create_model_for_tables/tables_component.yaml') +eval_model_op = comp.load_component_from_file( + './create_model_for_tables/tables_eval_component.yaml') +eval_metrics_op = comp.load_component_from_file( + './create_model_for_tables/tables_eval_metrics_component.yaml') +deploy_model_op = comp.load_component_from_file( + './deploy_model_for_tables/tables_deploy_component.yaml' + ) + +@dsl.pipeline( + name='AutoML Tables', + description='Demonstrate an AutoML Tables workflow' +) +def automl_tables( #pylint: disable=unused-argument + gcp_project_id: str = 'YOUR_PROJECT_HERE', + gcp_region: str = 'us-central1', + dataset_display_name: str = 'YOUR_DATASET_NAME', + api_endpoint: str = '', + path: str = 'bq://aju-dev-demos.london_bikes_weather.bikes_weather', + target_col_name: str = 'duration', + time_col_name: str = '', + # test_train_col_name: str = '', + # schema dict with col name as key, type as value + schema_info: str = DEFAULT_SCHEMA, + train_budget_milli_node_hours: 'Integer' = 1000, + model_prefix: str = 'bwmodel', + # one of strings: [MAXIMIZE_AU_ROC, MAXIMIZE_AU_PRC, MINIMIZE_LOG_LOSS, MAXIMIZE_RECALL_AT_PRECISION, MAXIMIZE_PRECISION_AT_RECALL, MINIMIZE_RMSE, MINIMIZE_MAE, MINIMIZE_RMSLE] + optimization_objective: str = '', # if not set, will use default + # ["title", "body", "answer_count", "comment_count", "creation_date", "favorite_count", "owner_user_id", "score", "view_count"] + include_column_spec_names: str = '', + exclude_column_spec_names: str = '', + bucket_name: str = 'aju-pipelines', + # thresholds: str = '{"au_prc": 0.9}', + thresholds: str = '{"mean_absolute_error": 480}', + ): + + create_dataset = create_dataset_op( + gcp_project_id=gcp_project_id, + gcp_region=gcp_region, + dataset_display_name=dataset_display_name, + api_endpoint=api_endpoint, + ).apply(gcp.use_gcp_secret('user-gcp-sa')) + + + import_data = import_data_op( + gcp_project_id=gcp_project_id, + gcp_region=gcp_region, + dataset_display_name=dataset_display_name, + api_endpoint=api_endpoint, + path=path + ).apply(gcp.use_gcp_secret('user-gcp-sa')) + + set_schema = set_schema_op( + gcp_project_id=gcp_project_id, + gcp_region=gcp_region, + display_name=dataset_display_name, + api_endpoint=api_endpoint, + target_col_name=target_col_name, + schema_info=schema_info, + time_col_name=time_col_name + # test_train_col_name=test_train_col_name + ).apply(gcp.use_gcp_secret('user-gcp-sa')) + + + import_data.after(create_dataset) + set_schema.after(import_data) + + train_model = train_model_op( + gcp_project_id=gcp_project_id, + gcp_region=gcp_region, + dataset_display_name=dataset_display_name, + api_endpoint=api_endpoint, + model_prefix=model_prefix, + train_budget_milli_node_hours=train_budget_milli_node_hours, + optimization_objective=optimization_objective + ).apply(gcp.use_gcp_secret('user-gcp-sa')) + + train_model.after(set_schema) + + eval_model = eval_model_op( + gcp_project_id=gcp_project_id, + gcp_region=gcp_region, + bucket_name=bucket_name, + gcs_path='automl_evals/{}'.format(dsl.RUN_ID_PLACEHOLDER), + api_endpoint=api_endpoint, + model_display_name=train_model.outputs['model_display_name'] + ).apply(gcp.use_gcp_secret('user-gcp-sa')) + + eval_metrics = eval_metrics_op( + gcp_project_id=gcp_project_id, + gcp_region=gcp_region, + bucket_name=bucket_name, + api_endpoint=api_endpoint, + model_display_name=train_model.outputs['model_display_name'], + thresholds=thresholds, + eval_data=eval_model.outputs['eval_data'], + # gcs_path=eval_model.outputs['evals_gcs_path'] + ).apply(gcp.use_gcp_secret('user-gcp-sa')) + + with dsl.Condition(eval_metrics.outputs['deploy'] == True): + deploy_model = deploy_model_op( + gcp_project_id=gcp_project_id, + gcp_region=gcp_region, + api_endpoint=api_endpoint, + model_display_name=train_model.outputs['model_display_name'], + ).apply(gcp.use_gcp_secret('user-gcp-sa')) + + +if __name__ == '__main__': + import kfp.compiler as compiler + compiler.Compiler().compile(automl_tables, __file__ + '.tar.gz') From 75eac6472d583a8578d669cd6e6e91caabd39391 Mon Sep 17 00:00:00 2001 From: Amy Unruh Date: Wed, 18 Mar 2020 14:42:59 -0700 Subject: [PATCH 2/9] metrics viz experimentation --- .../tables_eval_metrics_component.py | 46 +++---- .../tables_eval_metrics_component.yaml | 121 +++++++++--------- 2 files changed, 74 insertions(+), 93 deletions(-) diff --git a/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_metrics_component.py b/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_metrics_component.py index 1802208..ea405cd 100644 --- a/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_metrics_component.py +++ b/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_metrics_component.py @@ -26,6 +26,7 @@ def automl_eval_metrics( # gcs_path: str, eval_data_path: InputPath('evals'), mlpipeline_ui_metadata_path: OutputPath('UI_metadata'), + mlpipeline_metrics_path: OutputPath('UI_metrics'), api_endpoint: str = None, # thresholds: str = '{"au_prc": 0.9}', thresholds: str = '{"mean_absolute_error": 450}', @@ -52,31 +53,6 @@ def automl_eval_metrics( from google.cloud import storage - # def get_string_from_gcs(project, bucket_name, gcs_path): - # logging.info('Using bucket {} and path {}'.format(bucket_name, gcs_path)) - # storage_client = storage.Client(project=project) - # bucket = storage_client.get_bucket(bucket_name) - # blob = bucket.blob(gcs_path) - # return blob.download_as_string() - - # def upload_blob(bucket_name, source_file_name, destination_blob_name, - # public_url=False): - # """Uploads a file to the bucket.""" - - # storage_client = storage.Client() - # bucket = storage_client.bucket(bucket_name) - # blob = bucket.blob(destination_blob_name) - - # blob.upload_from_filename(source_file_name) - - # logging.info("File {} uploaded to {}.".format( - # source_file_name, destination_blob_name)) - # if public_url: - # blob.make_public() - # logging.info("Blob {} is publicly accessible at {}".format( - # blob.name, blob.public_url)) - # return blob.public_url - logging.getLogger().setLevel(logging.INFO) # TODO: make level configurable # TODO: we could instead check for region 'eu' and use 'eu-automl.googleapis.com:443'endpoint # in that case, instead of requiring endpoint to be specified. @@ -170,12 +146,25 @@ def classif_threshold_check(eval_info): 'source': '# Regression metrics:\n\n```{}```\n'.format(eresults), 'type': 'markdown', }]} + metrics = { + 'metrics': [{ + 'name': 'MAE', + 'numberValue': eresults['mean_absolute_error'], + 'format': "RAW", + }] + } # TODO: is it possible to get confusion matrix info via the API, for the binary # classifcation case? doesn't seem to be. logging.info('using metadata dict {}'.format(json.dumps(metadata))) logging.info('using metadata ui path: {}'.format(mlpipeline_ui_metadata_path)) with open(mlpipeline_ui_metadata_path, 'w') as mlpipeline_ui_metadata_file: mlpipeline_ui_metadata_file.write(json.dumps(metadata)) + logging.info('using metrics path: {}'.format(mlpipeline_metrics_path)) + with open(mlpipeline_metrics_path, 'w') as mlpipeline_metrics_file: + mlpipeline_metrics_file.write(json.dumps(metrics)) + # temp test this variant as well + with open('/mlpipeline-metrics.json', 'w') as f: + json.dump(metrics, f) logging.info('deploy flag: {}'.format(res)) return res @@ -190,6 +179,7 @@ def classif_threshold_check(eval_info): confidence_threshold, eresults), 'type': 'markdown', }]} + # TODO: generate 'metrics' dict logging.info('using metadata dict {}'.format(json.dumps(metadata))) logging.info('using metadata ui path: {}'.format(mlpipeline_ui_metadata_path)) with open(mlpipeline_ui_metadata_path, 'w') as mlpipeline_ui_metadata_file: @@ -212,9 +202,3 @@ def classif_threshold_check(eval_info): import kfp kfp.components.func_to_container_op(automl_eval_metrics, output_component_file='tables_eval_metrics_component.yaml', base_image='python:3.7') - - -# if __name__ == "__main__": -# automl_eval_threshold('aju-vtests2', 'us-central1', -# # model_display_name='amy_test3_20191219032001', -# gcs_path='automl_evals/testing/somodel_1579284627', bucket_name='aju-pipelines') diff --git a/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_metrics_component.yaml b/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_metrics_component.yaml index 8264ceb..9b85bd2 100644 --- a/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_metrics_component.yaml +++ b/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_metrics_component.yaml @@ -24,6 +24,8 @@ inputs: outputs: - name: mlpipeline_ui_metadata type: UI_metadata +- name: mlpipeline_metrics + type: UI_metrics - name: deploy type: Boolean implementation: @@ -36,20 +38,21 @@ implementation: - "class InputPath:\n '''When creating component from function, InputPath should\ \ be used as function parameter annotation to tell the system to pass the *data\ \ file path* to the function instead of passing the actual data.'''\n def\ - \ __init__(self, type=None):\n self.type = type\n\nclass OutputPath:\n\ - \ '''When creating component from function, OutputPath should be used as\ - \ function parameter annotation to tell the system that the function wants to\ - \ output data by writing it into a file with the given path instead of returning\ - \ the data from the function.'''\n def __init__(self, type=None):\n \ - \ self.type = type\n\ndef _make_parent_dirs_and_return_path(file_path: str):\n\ - \ import os\n os.makedirs(os.path.dirname(file_path), exist_ok=True)\n\ - \ return file_path\n\nfrom typing import NamedTuple\n\ndef automl_eval_metrics(\n\ - \tgcp_project_id: str,\n\tgcp_region: str,\n model_display_name: str,\n bucket_name:\ - \ str,\n # gcs_path: str,\n eval_data_path: InputPath('evals'),\n mlpipeline_ui_metadata_path:\ - \ OutputPath('UI_metadata'),\n api_endpoint: str = None,\n # thresholds: str\ - \ = '{\"au_prc\": 0.9}',\n thresholds: str = '{\"mean_absolute_error\": 450}',\n\ - \ confidence_threshold: float = 0.5 # for classification\n\n) -> NamedTuple('Outputs',\ - \ [('deploy', bool)]):\n import subprocess\n import sys\n subprocess.run([sys.executable,\ + \ __init__(self, type=None):\n self.type = type\n\ndef _make_parent_dirs_and_return_path(file_path:\ + \ str):\n import os\n os.makedirs(os.path.dirname(file_path), exist_ok=True)\n\ + \ return file_path\n\nclass OutputPath:\n '''When creating component from\ + \ function, OutputPath should be used as function parameter annotation to tell\ + \ the system that the function wants to output data by writing it into a file\ + \ with the given path instead of returning the data from the function.'''\n\ + \ def __init__(self, type=None):\n self.type = type\n\nfrom typing\ + \ import NamedTuple\n\ndef automl_eval_metrics(\n\tgcp_project_id: str,\n\t\ + gcp_region: str,\n model_display_name: str,\n bucket_name: str,\n # gcs_path:\ + \ str,\n eval_data_path: InputPath('evals'),\n mlpipeline_ui_metadata_path:\ + \ OutputPath('UI_metadata'),\n mlpipeline_metrics_path: OutputPath('UI_metrics'),\n\ + \ api_endpoint: str = None,\n # thresholds: str = '{\"au_prc\": 0.9}',\n \ + \ thresholds: str = '{\"mean_absolute_error\": 450}',\n confidence_threshold:\ + \ float = 0.5 # for classification\n\n) -> NamedTuple('Outputs', [('deploy',\ + \ bool)]):\n import subprocess\n import sys\n subprocess.run([sys.executable,\ \ '-m', 'pip', 'install', 'googleapis-common-protos==1.6.0',\n '--no-warn-script-location'],\ \ env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True)\n subprocess.run([sys.executable,\ \ '-m', 'pip', 'install', 'google-cloud-automl==0.9.0',\n 'google-cloud-storage',\n\ @@ -58,26 +61,14 @@ implementation: \ pickle\n from google.api_core.client_options import ClientOptions\n from\ \ google.api_core import exceptions\n from google.cloud import automl_v1beta1\ \ as automl\n from google.cloud.automl_v1beta1 import enums\n from google.cloud\ - \ import storage\n\n # def get_string_from_gcs(project, bucket_name, gcs_path):\n\ - \ # logging.info('Using bucket {} and path {}'.format(bucket_name, gcs_path))\n\ - \ # storage_client = storage.Client(project=project)\n # bucket = storage_client.get_bucket(bucket_name)\n\ - \ # blob = bucket.blob(gcs_path)\n # return blob.download_as_string()\n\ - \n # def upload_blob(bucket_name, source_file_name, destination_blob_name,\n\ - \ # public_url=False):\n # \"\"\"Uploads a file to the bucket.\"\"\"\ - \n\n # storage_client = storage.Client()\n # bucket = storage_client.bucket(bucket_name)\n\ - \ # blob = bucket.blob(destination_blob_name)\n\n # blob.upload_from_filename(source_file_name)\n\ - \n # logging.info(\"File {} uploaded to {}.\".format(\n # source_file_name,\ - \ destination_blob_name))\n # if public_url:\n # blob.make_public()\n\ - \ # logging.info(\"Blob {} is publicly accessible at {}\".format(\n #\ - \ blob.name, blob.public_url))\n # return blob.public_url\n\n\ - \ logging.getLogger().setLevel(logging.INFO) # TODO: make level configurable\n\ - \ # TODO: we could instead check for region 'eu' and use 'eu-automl.googleapis.com:443'endpoint\n\ - \ # in that case, instead of requiring endpoint to be specified.\n if api_endpoint:\n\ - \ client_options = ClientOptions(api_endpoint=api_endpoint)\n client =\ - \ automl.TablesClient(project=gcp_project_id, region=gcp_region,\n client_options=client_options)\n\ - \ else:\n client = automl.TablesClient(project=gcp_project_id, region=gcp_region)\n\ - \n thresholds_dict = json.loads(thresholds)\n logging.info('thresholds dict:\ - \ {}'.format(thresholds_dict))\n\n def regression_threshold_check(eval_info):\n\ + \ import storage\n\n logging.getLogger().setLevel(logging.INFO) # TODO: make\ + \ level configurable\n # TODO: we could instead check for region 'eu' and use\ + \ 'eu-automl.googleapis.com:443'endpoint\n # in that case, instead of requiring\ + \ endpoint to be specified.\n if api_endpoint:\n client_options = ClientOptions(api_endpoint=api_endpoint)\n\ + \ client = automl.TablesClient(project=gcp_project_id, region=gcp_region,\n\ + \ client_options=client_options)\n else:\n client = automl.TablesClient(project=gcp_project_id,\ + \ region=gcp_region)\n\n thresholds_dict = json.loads(thresholds)\n logging.info('thresholds\ + \ dict: {}'.format(thresholds_dict))\n\n def regression_threshold_check(eval_info):\n\ \ eresults = {}\n rmetrics = eval_info[1].regression_evaluation_metrics\n\ \ logging.info('got regression eval {}'.format(eval_info[1]))\n eresults['root_mean_squared_error']\ \ = rmetrics.root_mean_squared_error\n eresults['mean_absolute_error'] =\ @@ -107,43 +98,45 @@ implementation: \ eresults[k], v))\n return (False, eresults)\n else:\n \ \ if eresults[k] < v:\n logging.info('{} < {}; returning False'.format(\n\ \ eresults[k], v))\n return (False, eresults)\n return\ - \ (True, eresults)\n\n def generate_cm_metadata():\n pass\n\n # testing...\n\ - \ with open(eval_data_path, 'rb') as f:\n logging.info('successfully opened\ - \ eval_data_path {}'.format(eval_data_path))\n try:\n eval_info = pickle.loads(f.read())\n\ - \ # TODO: add handling of confusion matrix stuff for binary classif case..\n\ - \ # eval_string = get_string_from_gcs(gcp_project_id, bucket_name, gcs_path)\n\ - \ # eval_info = pickle.loads(eval_string)\n\n classif = False\n \ - \ binary_classif = False\n regression = False\n # TODO: ughh...\ - \ what's the right way to figure out the model type?\n if eval_info[1].regression_evaluation_metrics\ - \ and eval_info[1].regression_evaluation_metrics.root_mean_squared_error:\n\ + \ (True, eresults)\n\n # testing...\n with open(eval_data_path, 'rb') as f:\n\ + \ logging.info('successfully opened eval_data_path {}'.format(eval_data_path))\n\ + \ try:\n eval_info = pickle.loads(f.read())\n\n classif = False\n\ + \ regression = False\n # TODO: what's the right way to figure out\ + \ the model type?\n if eval_info[1].regression_evaluation_metrics and eval_info[1].regression_evaluation_metrics.root_mean_squared_error:\n\ \ regression=True\n logging.info('found regression metrics {}'.format(eval_info[1].regression_evaluation_metrics))\n\ \ elif eval_info[1].classification_evaluation_metrics and eval_info[1].classification_evaluation_metrics.au_prc:\n\ \ classif = True\n logging.info('found classification metrics\ - \ {}'.format(eval_info[1].classification_evaluation_metrics))\n # TODO:\ - \ detect binary classification case\n\n if regression and thresholds_dict:\n\ - \ res, eresults = regression_threshold_check(eval_info)\n # logging.info('eresults:\ - \ {}'.format(eresults))\n metadata = {\n 'outputs' : [\n \ - \ {\n 'storage': 'inline',\n 'source': '# Regression\ - \ metrics:\\n\\n```{}```\\n'.format(eresults),\n 'type': 'markdown',\n\ - \ }]}\n logging.info('using metadata dict {}'.format(json.dumps(metadata)))\n\ + \ {}'.format(eval_info[1].classification_evaluation_metrics))\n\n if regression\ + \ and thresholds_dict:\n res, eresults = regression_threshold_check(eval_info)\n\ + \ # logging.info('eresults: {}'.format(eresults))\n metadata =\ + \ {\n 'outputs' : [\n {\n 'storage': 'inline',\n\ + \ 'source': '# Regression metrics:\\n\\n```{}```\\n'.format(eresults),\n\ + \ 'type': 'markdown',\n }]}\n metrics = {\n \ + \ 'metrics': [{\n 'name': 'MAE',\n 'numberValue':\ + \ eresults['mean_absolute_error'],\n 'format': \"RAW\",\n \ + \ }]\n }\n # TODO: is it possible to get confusion matrix info\ + \ via the API, for the binary\n # classifcation case? doesn't seem to\ + \ be.\n logging.info('using metadata dict {}'.format(json.dumps(metadata)))\n\ \ logging.info('using metadata ui path: {}'.format(mlpipeline_ui_metadata_path))\n\ \ with open(mlpipeline_ui_metadata_path, 'w') as mlpipeline_ui_metadata_file:\n\ \ mlpipeline_ui_metadata_file.write(json.dumps(metadata))\n \ - \ logging.info('deploy flag: {}'.format(res))\n return res\n\n elif\ - \ classif and thresholds_dict:\n res, eresults = classif_threshold_check(eval_info)\n\ - \ # logging.info('eresults: {}'.format(eresults))\n metadata =\ - \ {\n 'outputs' : [\n {\n 'storage': 'inline',\n\ - \ 'source': '# classification metrics for confidence threshold {}:\\\ - n\\n```{}```\\n'.format(\n confidence_threshold, eresults),\n\ - \ 'type': 'markdown',\n }]}\n logging.info('using\ + \ logging.info('using metrics path: {}'.format(mlpipeline_metrics_path))\n\ + \ with open(mlpipeline_metrics_path, 'w') as mlpipeline_metrics_file:\n\ + \ mlpipeline_metrics_file.write(json.dumps(metrics))\n # temp\ + \ test\n with open('/mlpipeline-metrics.json', 'w') as f:\n \ + \ json.dump(metrics, f)\n logging.info('deploy flag: {}'.format(res))\n\ + \ return res\n\n elif classif and thresholds_dict:\n res,\ + \ eresults = classif_threshold_check(eval_info)\n # logging.info('eresults:\ + \ {}'.format(eresults))\n metadata = {\n 'outputs' : [\n \ + \ {\n 'storage': 'inline',\n 'source': '# classification\ + \ metrics for confidence threshold {}:\\n\\n```{}```\\n'.format(\n \ + \ confidence_threshold, eresults),\n 'type': 'markdown',\n\ + \ }]}\n # TODO: generate 'metrics' dict\n logging.info('using\ \ metadata dict {}'.format(json.dumps(metadata)))\n logging.info('using\ \ metadata ui path: {}'.format(mlpipeline_ui_metadata_path))\n with open(mlpipeline_ui_metadata_path,\ \ 'w') as mlpipeline_ui_metadata_file:\n mlpipeline_ui_metadata_file.write(json.dumps(metadata))\n\ - \ # with open('/mlpipeline-ui-metadata.json', 'w') as f:\n #\ - \ json.dump(metadata, f)\n logging.info('deploy flag: {}'.format(res))\n\ - \ # TODO: generate confusion matrix ui-metadata as approp etc.\n \ - \ if binary_classif:\n generate_cm_metadata()\n return res\n\ - \ else:\n return True\n except Exception as e:\n logging.warning(e)\n\ + \ logging.info('deploy flag: {}'.format(res))\n return res\n \ + \ else:\n return True\n except Exception as e:\n logging.warning(e)\n\ \ # If can't reconstruct the eval, or don't have thresholds defined,\n\ \ # return True as a signal to deploy.\n # TODO: is this the right\ \ default?\n return True\n\ndef _serialize_bool(bool_value: bool) -> str:\n\ @@ -165,6 +158,8 @@ implementation: , dest=\"confidence_threshold\", type=float, required=False, default=argparse.SUPPRESS)\n\ _parser.add_argument(\"--mlpipeline-ui-metadata\", dest=\"mlpipeline_ui_metadata_path\"\ , type=_make_parent_dirs_and_return_path, required=True, default=argparse.SUPPRESS)\n\ + _parser.add_argument(\"--mlpipeline-metrics\", dest=\"mlpipeline_metrics_path\"\ + , type=_make_parent_dirs_and_return_path, required=True, default=argparse.SUPPRESS)\n\ _parser.add_argument(\"----output-paths\", dest=\"_output_paths\", type=str,\ \ nargs=1)\n_parsed_args = vars(_parser.parse_args())\n_output_files = _parsed_args.pop(\"\ _output_paths\", [])\n\n_outputs = automl_eval_metrics(**_parsed_args)\n\nif\ @@ -204,5 +199,7 @@ implementation: - inputValue: confidence_threshold - --mlpipeline-ui-metadata - outputPath: mlpipeline_ui_metadata + - --mlpipeline-metrics + - outputPath: mlpipeline_metrics - '----output-paths' - outputPath: deploy From 0cdf9388d8be37261059c63cb826936772134358 Mon Sep 17 00:00:00 2001 From: Amy Unruh Date: Wed, 18 Mar 2020 15:39:21 -0700 Subject: [PATCH 3/9] adding some compiled pipeline archives --- .../kfp_e2e/tables_pipeline_caip.py.tar.gz | Bin 0 -> 10492 bytes .../tables/kfp_e2e/tables_pipeline_kf.py.tar.gz | Bin 0 -> 10756 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 ml/automl/tables/kfp_e2e/tables_pipeline_caip.py.tar.gz create mode 100644 ml/automl/tables/kfp_e2e/tables_pipeline_kf.py.tar.gz diff --git a/ml/automl/tables/kfp_e2e/tables_pipeline_caip.py.tar.gz b/ml/automl/tables/kfp_e2e/tables_pipeline_caip.py.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..06daf103f2d1f2702f535ee127fdd97b4be480bd GIT binary patch literal 10492 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 literal 0 HcmV?d00001 diff --git a/ml/automl/tables/kfp_e2e/tables_pipeline_kf.py.tar.gz b/ml/automl/tables/kfp_e2e/tables_pipeline_kf.py.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..ac54deb60a10ab6e1d9fbf003f474c484bb56338 GIT binary patch literal 10756 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 literal 0 HcmV?d00001 From a8f381250b676f758d906fffad2e1361f015db88 Mon Sep 17 00:00:00 2001 From: Amy Unruh Date: Thu, 19 Mar 2020 10:59:09 -0700 Subject: [PATCH 4/9] checkpointing --- .../tables_eval_component.py | 1 + .../tables_eval_component.yaml | 15 ++++++++------- ml/automl/tables/kfp_e2e/tables_pipeline_caip.py | 1 - ml/automl/tables/kfp_e2e/tables_pipeline_kf.py | 1 - 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_component.py b/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_component.py index a8e293f..4ddbeb7 100644 --- a/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_component.py +++ b/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_component.py @@ -122,6 +122,7 @@ def generate_fi_ui(feat_list): x = list(res[0]) y = list(res[1]) y_pos = list(range(len(y))) + plt.figure(figsize=(10, 6)) plt.barh(y_pos, x, alpha=0.5) plt.yticks(y_pos, y) plt.savefig('/gfi.png') diff --git a/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_component.yaml b/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_component.yaml index a63cffb..a750897 100644 --- a/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_component.yaml +++ b/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_component.yaml @@ -78,13 +78,14 @@ implementation: \ return (model, feat_list)\n\n def generate_fi_ui(feat_list):\n import\ \ matplotlib.pyplot as plt\n\n image_suffix = '{}/gfi.png'.format(gcs_path)\n\ \ res = list(zip(*feat_list))\n x = list(res[0])\n y = list(res[1])\n\ - \ y_pos = list(range(len(y)))\n plt.barh(y_pos, x, alpha=0.5)\n plt.yticks(y_pos,\ - \ y)\n plt.savefig('/gfi.png')\n public_url = upload_blob(bucket_name,\ - \ '/gfi.png', image_suffix, public_url=True)\n logging.info('using image\ - \ url {}'.format(public_url))\n\n html_suffix = '{}/gfi.html'.format(gcs_path)\n\ - \ with open('/gfi.html', 'w') as f:\n f.write('

Global\ - \ Feature Importance

\\n'.format(public_url))\n\ - \ upload_blob(bucket_name, '/gfi.html', html_suffix)\n html_source = 'gs://{}/{}'.format(bucket_name,\ + \ y_pos = list(range(len(y)))\n plt.figure(figsize=(10, 6))\n plt.barh(y_pos,\ + \ x, alpha=0.5)\n plt.yticks(y_pos, y)\n plt.savefig('/gfi.png')\n \ + \ public_url = upload_blob(bucket_name, '/gfi.png', image_suffix, public_url=True)\n\ + \ logging.info('using image url {}'.format(public_url))\n\n html_suffix\ + \ = '{}/gfi.html'.format(gcs_path)\n with open('/gfi.html', 'w') as f:\n\ + \ f.write('

Global Feature Importance

\\\ + n'.format(public_url))\n upload_blob(bucket_name,\ + \ '/gfi.html', html_suffix)\n html_source = 'gs://{}/{}'.format(bucket_name,\ \ html_suffix)\n logging.info('metadata html source: {}'.format(html_source))\n\ \n metadata = {\n 'outputs' : [\n {\n 'type': 'web-app',\n\ \ 'storage': 'gcs',\n 'source': html_source\n }]}\n logging.info('using\ diff --git a/ml/automl/tables/kfp_e2e/tables_pipeline_caip.py b/ml/automl/tables/kfp_e2e/tables_pipeline_caip.py index 9be2e67..361eae1 100644 --- a/ml/automl/tables/kfp_e2e/tables_pipeline_caip.py +++ b/ml/automl/tables/kfp_e2e/tables_pipeline_caip.py @@ -16,7 +16,6 @@ import kfp.dsl as dsl import kfp.gcp as gcp import kfp.components as comp -# from kfp.dsl.types import GCSPath, String, Dict import json import time diff --git a/ml/automl/tables/kfp_e2e/tables_pipeline_kf.py b/ml/automl/tables/kfp_e2e/tables_pipeline_kf.py index 4ec1de7..7ae9913 100644 --- a/ml/automl/tables/kfp_e2e/tables_pipeline_kf.py +++ b/ml/automl/tables/kfp_e2e/tables_pipeline_kf.py @@ -16,7 +16,6 @@ import kfp.dsl as dsl import kfp.gcp as gcp import kfp.components as comp -from kfp.dsl.types import GCSPath, String, Dict import json import time From da0c214a95c6b3247cb28655c8dee57c02436629 Mon Sep 17 00:00:00 2001 From: Amy Unruh Date: Fri, 27 Mar 2020 12:41:38 -0700 Subject: [PATCH 5/9] some work on the README --- ml/automl/tables/kfp_e2e/README.md | 454 ++++++++++++++++++ .../tables_deploy_component.py | 8 +- .../tables_deploy_component.yaml | 2 +- .../tables/kfp_e2e/tables_pipeline_caip.py | 17 +- .../kfp_e2e/tables_pipeline_caip.py.tar.gz | Bin 10492 -> 10466 bytes .../tables/kfp_e2e/tables_pipeline_kf.py | 3 +- .../kfp_e2e/tables_pipeline_kf.py.tar.gz | Bin 10756 -> 10730 bytes 7 files changed, 467 insertions(+), 17 deletions(-) diff --git a/ml/automl/tables/kfp_e2e/README.md b/ml/automl/tables/kfp_e2e/README.md index e69de29..0952880 100644 --- a/ml/automl/tables/kfp_e2e/README.md +++ b/ml/automl/tables/kfp_e2e/README.md @@ -0,0 +1,454 @@ + +# AutoML Tables: end-to-end workflow on Cloud AI Platform Pipelines + +- [Introduction](#introduction) + - [About the example dataset and scenario](#about-the-example-dataset-and-scenario) +- [Using Cloud AI Platform Pipelines or Kubeflow Pipelines to orchestrate a Tables workflow](#using-cloud-ai-platform-pipelines-or-kubeflow-pipelines-to-orchestrate-a-tables-workflow) + - [Install a Cloud AI Platform Pipelines cluster](#install-a-cloud-ai-platform-pipelines-cluster) + - [Or, install Kubeflow to use Kubeflow Pipelines](#or-install-kubeflow-to-use-kubeflow-pipelines) + - [Upload and run the Tables end-to-end Pipeline](#upload-and-run-the-tables-end-to-end-pipeline) +- [The steps executed by the pipeline](#the-steps-executed-by-the-pipeline) + - [Create a Tables dataset and adjust its schema](#create-a-tables-dataset-and-adjust-its-schema) + - [Train a custom model on the dataset](#train-a-custom-model-on-the-dataset) + - [View model search information via Cloud Logging](#view-model-search-information-via-cloud-logging) + - [Custom model evaluation](#custom-model-evaluation) + - [(Conditional) model deployment](#conditional-model-deployment) + - [Putting it together: The full pipeline execution](#putting-it-together-the-full-pipeline-execution) +- [Getting explanations about your model’s predictions](#getting-explanations-about-your-models-predictions) +- [The AutoML Tables UI in the Cloud Console](#the-automl-tables-ui-in-the-cloud-console) +- [Export the trained model and serve it on a GKE cluster](#export-the-trained-model-and-serve-it-on-a-gke-cluster) + - [Send prediction requests to your deployed model service](#send-prediction-requests-to-your-deployed-model-service) +- [A deeper dive into the pipeline code](#a-deeper-dive-into-the-pipeline-code) + - [Using the ‘lightweight python components’ functionality to build pipeline steps](#using-the-lightweight-python-components--functionality-to-build-pipeline-steps) + - [Specifying the Tables pipeline](#specifying-the-tables-pipeline) + +## Introduction + +[AutoML Tables][1] lets you automatically build, analyze, and deploy state-of-the-art machine learning models using your own structured data. + +A number of new AutoML Tables features have been released recently. These include: +- An improved [Python client library][2] +- The ability to obtain [explanations][3] for your online predictions +- The ability to [export your model and serve it in a container][4] anywhere +- The ability to view model search progress and final model hyperparameters [in Cloud Logging][5] + +This tutorial gives a tour of some of these new features via a [Cloud AI Platform Pipelines][6] example, that shows end-to-end management of an AutoML Tables workflow. + +The example pipeline [creates a _dataset_][7], [imports][8] data into the dataset from a [BigQuery][9] _view_, and [trains][10] a custom model on that data. Then, it fetches [evaluation and metrics][11] information about the trained model, and based on specified criteria about model quality, uses that information to automatically determine whether to [deploy][12] the model for online prediction. Once the model is deployed, you can make prediction requests, and optionally obtain prediction [explanations][13] as well as the prediction result. +In addition, the example shows how to scalably **_serve_** your exported trained model from your Cloud AI Platform Pipelines installation for prediction requests. + +You can manage all the parts of this workflow from the [Tables UI][14] as well, or programmatically via a [notebook][15] or script. But specifying this process as a workflow has some advantages: the workflow becomes reliable and repeatable, and Pipelines makes it easy to monitor the results and schedule recurring runs. +For example, if your dataset is updated regularly—say once a day— you could schedule a workflow to run daily, each day building a model that trains on an updated dataset. +(With a bit more work, you could also set up event-based triggering pipeline runs, for example [when new data is added][16] to a [Google Cloud Storage][17] bucket.) + +### About the example dataset and scenario + +The [Cloud Public Datasets Program][18] makes available public datasets that are useful for experimenting with machine learning. For our examples, we’ll use data that is essentially a join of two public datasets stored in [BigQuery][19]: [London Bike rentals][20] and [NOAA weather data][21], with some additional processing to clean up outliers and derive additional GIS and day-of-week fields. Using this dataset, we’ll build a regression model to predict the _duration_ of a bike rental based on information about the start and end rental stations, the day of the week, the weather on that day, and other data. If we were running a bike rental company, we could use these predictions—and their [explanations][22]—to help us anticipate demand and even plan how to stock each location. +While we’re using bike and weather data here, you can use AutoML Tables for tasks as varied as asset valuations, fraud detection, credit risk analysis, customer retention prediction, analyzing item layouts in stores, and many more. + +## Using Cloud AI Platform Pipelines or Kubeflow Pipelines to orchestrate a Tables workflow + +You can run this example via a [Cloud AI Platform Pipelines][23] installation, or via [Kubeflow Pipelines][24] on a [Kubeflow on GKE][25] installation. [Cloud AI Platform Pipelines][26] was recently launched in Beta. Slightly different variants of the pipeline specification are required depending upon which you’re using. (It would be possible to run the example on other Kubeflow installations too, but that would require additional credentials setup not covered in this tutorial). + +### Install a Cloud AI Platform Pipelines cluster + +You can create an AI Platform Pipelines installation with a few clicks. Access AI Platform Pipelines by visiting the [AI Platform Panel][27] in the [Cloud Console][28]. + +
+ +

Create a new Pipelines instance.
+
+ +See the [documentation][29] for more detail. + +(You can also do this installation [from the command line][30] onto an existing GKE cluster if you prefer. If you do, for consistency with the UI installation, create the GKE cluster with `--scopes cloud-platform`). + +### Or, install Kubeflow to use Kubeflow Pipelines + +You can also run this example from a [Kubeflow][31] installation. For the example to work out of the box, you’ll need a Kubeflow on [GKE][32] installation, set up to use [IAP][33]. An easy way to do this is via the Kubeflow [‘click to deploy’ web app][34], or you can follow the command-line instructions [here][35]. + +### Upload and run the Tables end-to-end Pipeline + +Once a Pipelines installation is running, we can upload the example AutoML Tables pipeline. +Click on **Pipelines** in the left nav bar of the Pipelines Dashboard. Click on **Upload Pipeline**. + +- For Cloud AI Platform Pipelines, upload [`tables_pipeline_caip.py.tar.gz`][36], from this directory. This archive points to the compiled version of [this pipeline][37], specified and compiled using the [Kubeflow Pipelines SDK][38]. +- For Kubeflow Pipelines on a Kubeflow installation, upload [`tables_pipeline_kf.py.tar.gz`][39]. This archive points to the compiled version of [this pipeline][40]. + +> Note: The difference between the two pipelines relates to how GCP authentication is handled. For the Kubeflow pipeline, we’ve added `.apply(gcp.use_gcp_secret('user-gcp-sa'))` annotations to the pipeline steps. This tells the pipeline to use the mounted _secret_—set up during the installation process— that provides GCP account credentials. With the Cloud AI Platform Pipelines installation, the GKE cluster nodes have been set up to use the `cloud-platform` scope. With an upcoming Kubeflow release, specification of the mounted secret will no longer be necessary. + +The uploaded pipeline graph will look similar to this: + +
+ +

The uploaded Tables 'end-to-end' pipeline.
+
+ +Click the **+Create Run** button to run the pipeline. You will need to fill in some pipeline parameters. +Specifically, replace `YOUR_PROJECT_HERE` with the name of your project; replace `YOUR_DATASET_NAME` with the name you want to give your new dataset (make it unique, and use letters, numbers and underscores up to 32 characters); and replace `YOUR_BUCKET_NAME` with the name of a [GCS bucket][41]. This bucket should be in the [same _region_][42] as that specified by the `gcp_region` parameter. E.g., if you keep the default `us-central1` region, your bucket should also be a _regional_ (not multi-regional) bucket in the `us-central1` region. ++double check that this is necessary.++ + + If you want to schedule a recurrent set of runs, you can do that instead. If your data is in [BigQuery][43]— as is the case for this example pipeline— and has a temporal aspect, you could define a _view_ to reflect that, e.g. to return data from a window over the last `N` days or hours. Then, the AutoML pipeline could specify ingestion of data from that view, grabbing an updated data window each time the pipeline is run, and building a new model based on that updated window. + +## The steps executed by the pipeline + +The example pipeline [creates a _dataset_][44], [imports][45] data into the dataset from a [BigQuery][46] _view_, and [trains][47] a custom model on that data. Then, it fetches [evaluation and metrics][48] information about the trained model, and based on specified criteria about model quality, uses that information to automatically determine whether to [deploy][49] the model for online prediction. We’ll take a closer look at each of the pipeline steps, and how they’re implemented. + +### Create a Tables dataset and adjust its schema + +This pipeline creates a new Tables _dataset_, and ingests data from a [BigQuery][50] table for the “bikes and weather” dataset described above. These actions are implemented by the first two steps in the pipeline (the `automl-create-dataset-for-tables` and `automl-import-data-for-tables` steps). + +While we’re not showing it in this example, AutoML Tables supports ingestion from BigQuery _views_ as well as tables. This can be an easy way to do **_feature engineering_**: leverage BigQuery’s rich set of functions and operators to clean and transform your data before you ingest it. + +When the data is ingested, AutoML Tables infers the _data type_ for each field (column). In some cases, those inferred types may not be what you want. For example, for our “bikes and weather” dataset, several ID fields (like the rental station IDs) are set by default to be numeric, but we want them treated as categorical when we train our model. In addition, we want to treat the `loc_cross` strings as categorical rather than text. + +We make these adjustments programmatically, by defining a pipeline parameter that specifies the schema changes we want to make. Then, in the `automl-set-dataset-schema` pipeline step, for each indicated schema adjustment, we call `update_column_spec`: + +```python +client.update_column_spec( + dataset_display_name=dataset_display_name, + column_spec_display_name=column_spec_display_name, + type_code=type_code, + nullable=nullable + ) +``` + +Before we can train the model, we also need to specify the _target_ column— what we want our model to predict. In this case, we’ll train the model to predict rental _duration_. This is a numeric value, so we’ll be training a [regression][51] model. + +```python +client.set_target_column( + dataset_display_name=dataset_display_name, + column_spec_display_name=target_column_spec_name + ) +``` + +### Train a custom model on the dataset + +Once the dataset is defined and its schema set properly, the pipeline will train the model. This happens in the `automl-create-model-for-tables` pipeline step. Via pipeline parameters, we can specify the training budget, the _optimization objective_ (if not using the default), and can additionally specify which columns to include or exclude from the model inputs. + +You may want to specify a non-default optimization objective depending upon the characteristics of your dataset. [This table][52] describes the available optimization objectives and when you might want to use them. For example, if you were training a classification model using an imbalanced dataset, you might want to specify use of AUC PR (`MAXIMIZE_AU_PRC`), which optimizes results for predictions for the less common class. + +```python +client.create_model( + model_display_name, + train_budget_milli_node_hours=train_budget_milli_node_hours, + dataset_display_name=dataset_display_name, + optimization_objective=optimization_objective, + include_column_spec_names=include_column_spec_names, + exclude_column_spec_names=exclude_column_spec_names, + ) +``` + +### View model search information via Cloud Logging + +You can view details about an AutoML Tables model [via Cloud Logging][53]. Using Logging, you can see the final model hyperparameters as well as the hyperparameters and object values used during model training and tuning. + +An easy way to access these logs is to go to the [AutoML Tables page][54] in the Cloud Console. Select the Models tab in the left navigation pane and click on the model you’re interested in. Click the “Model” link to see the final hyperparameter logs. To see the tuning trial hyperparameters, click the “Trials” link. + +
+ +

View a model's search logs from its evaluation information.
+
+ +For example, here is a look at the Trials logs a custom model trained on the “bikes and weather” dataset, with one of the entries expanded in the logs: + +
+ +

The 'Trials' logs for a "bikes and weather" model
+
+ + + +### Custom model evaluation + +Once your custom model has finished training, the pipeline moves on to its next step: model evaluation. We can access evaluation metrics via the API. We’ll use this information to decide whether or not to deploy the model. + +These actions are factored into two steps. The process of fetching the evaluation information can be a general-purpose component (pipeline step) used in many situations; and then we’ll follow that with a more special-purpose step, that analyzes that information and uses it to decide whether or not to deploy the trained model. + +In the first of these pipeline steps— the `automl-eval-tables-model` step— we’ll retrieve the evaluation and _global feature importance_ information. + +```python +model = client.get_model(model_display_name=model_display_name) +feat_list = [(column.feature_importance, column.column_display_name) + for column in model.tables_model_metadata.tables_model_column_info] +evals = list(client.list_model_evaluations(model_display_name=model_display_name)) + +``` + +AutoML Tables automatically computes global feature importance for a trained model. This shows, across the evaluation set, the average absolute attribution each feature receives. Higher values mean the feature generally has greater influence on the model’s predictions. +This information is useful for debugging and improving your model. If a feature’s contribution is negligible—if it has a low value—you can simplify the model by excluding it from future training. +The pipeline step renders the global feature importance data as part of the pipeline run’s output: + +
+ +

Global feature importance for the model inputs, rendered by a Kubeflow Pipeline step.
+
+ + +For our example, based on the graphic above, we might try training a model without including `bike_id`. + +In the following pipeline step— the `automl-eval-metrics` step— the evaluation output from the previous step is grabbed as input, and parsed to extract metrics that we’ll use in conjunction with pipeline parameters to decide whether or not to deploy the model. One of the pipeline input parameters allows specification of metric thresholds. In this example, we’re training a regression model, and we’re specifying a `mean_absolute_error` (MAE) value as a threshold in the pipeline input parameters: +```python +{"mean_absolute_error": 450} +``` + +The pipeline step compares the model evaluation information to the given threshold constraints. In this case, if the MAE is \< `450`, the model will not be deployed. The pipeline step outputs that decision, and displays the evaluation information it’s using as part of the pipeline run’s output: + +
+ +

Information about a model's evaluation, rendered by a Kubeflow Pipeline step.
+
+ + +### (Conditional) model deployment + +You can _deploy_ any of your custom Tables models to make them accessible for online prediction requests. +The pipeline code uses a _conditional test_ to determine whether or not to run the step that deploys the model, based on the output of the evaluation step described above: + +```python +with dsl.Condition(eval_metrics.outputs['deploy'] == True): + deploy_model = deploy_model_op( ... ) +``` + +Only if the model meets the given criteria, will the deployment step (called `automl-deploy-tables-model`) be run, and the model be deployed automatically as part of the pipeline run: +```python +response = client.deploy_model(model_display_name=model_display_name) +``` + +You can always deploy a model later if you like. + +### Putting it together: The full pipeline execution + +The figure below shows the result of a pipeline run. In this case, the conditional step was executed— based on the model evaluation metrics— and the trained model was deployed. +Via the UI, you can view outputs, logs for each step, run artifacts and lineage information, and more. See [this post][55] for more detail. + +++TODO: replace the following figure with something better++ + +
+ +

Execution of a pipeline run. You can view outputs, logs for each step, run artifacts and lineage information, and more.
+
+ +## Getting explanations about your model’s predictions + +Once a model is deployed, you can request predictions from that model. You can additionally request _explanations for local feature importance_: a score showing how much (and in which direction) each feature influenced the prediction for a single example. See [this blog post][56] for more information on how those values are calculated. + +Here is a [notebook example][57] of how to request a prediction and its explanation using the Python client libraries. + +```python +from google.cloud import automl_v1beta1 as automl +client = automl.TablesClient(project=PROJECT_ID, region=REGION) + +response = client.predict( + model_display_name=model_display_name, + inputs=inputs, + feature_importance=True, +) +``` + +The prediction response will have a structure like [this][58]. (The notebook above shows how to visualize the local feature importance results using `matplotlib`.) + +It’s easy to explore local feature importance through the Cloud Console’s [AutoML Tables UI ][59]as well. After you deploy a model, go to the **TEST & USE** tab of the Tables panel, select **ONLINE PREDICTION**, enter the field values for the prediction, and then check the **Generate feature importance** box at the bottom of the page. The result will show the feature importance values as well as the prediction. This [blog post][60] gives some examples of how these explanations can be used to find potential issues with your data or help you better understand your problem domain. + +## The AutoML Tables UI in the Cloud Console + +With this example we’ve focused on how you can automate a Tables workflow using Kubeflow pipelines and the Python client libraries. + +All of the pipeline steps can also be accomplished via the [AutoML Tables UI][61] in the Cloud Console, including many useful visualizations, and other functionality not implemented by this example pipeline— such as the ability to export the model’s test set and prediction results to BigQuery for further analysis. + +## Export the trained model and serve it on a GKE cluster + +Recently, Tables launched a feature to let you export your full custom model, packaged so that you can serve it via a Docker container. (Under the hood, it is using TensorFlow Serving). This lets you serve your models anywhere that you can run a container, including a GKE cluster. +This means that you can run a model serving service on your AI Platform Pipelines or Kubeflow installation, both of which run on GKE. + +[This blog post][62] walks through the steps to serve the exported model (in this case, using [Cloud Run][63]). Follow the instructions in the post through the “View information about your exported model in TensorBoard” [section][64]. +Here, we’ll diverge from the rest of the post and create a GKE service instead. + +Make a copy of the [`model_serve_template.yaml`][65] file and name it `model_serve.yaml`. Edit this new file, **replacing** `MODEL_NAME` with some meaningful name for your model, `IMAGE_NAME` with the name of the container image you built (as described in the [blog post][66], and `NAMESPACE` with the namespace in which you want to run your service (e.g. `default`). + +Then, from the command line, run: +```bash +kubectl apply -f model_serve.yaml +``` + +to set up your model serving _service_ and its underlying _deployment_. (Before you do that, make sure that kubectl is set to use your GKE cluster’s credentials. One way to do that is to visit the [GKE panel in the Cloud Console][67], and click **Connect** for that cluster.) + +You can later take down the service and its deployment by running: +```bash +kubectl delete -f model_serve.yaml +``` + +### Send prediction requests to your deployed model service + +Once your model serving service is deployed, you can send prediction requests to it. Because we didn’t set up an external endpoint for our service in this simple example, we’ll connect to the service via port forwarding. +From the command line, run the following, **replacing** `` with the value you replaced `MODEL_NAME` by, when creating your `yaml` file, and `` with the namespace in which your service is running— the same namespace value you used in the yaml file. + +```bash +kubectl -n port-forward svc/ 8080:80 +``` + +> **Note**: it would be possible to add this deployment step to the pipeline too. However, the [Python client library][68] does not yet support the ‘export’ operation. Once deployment is supported by the client library, this would be a natural addition to the workflow. While not tested, it should also be possible to do the export programmatically via the [REST API][69]. + +## A deeper dive into the pipeline code + +The updated [Tables Python client library][70] makes it very straightforward to build the Pipelines components that support each stage of the workflow. +Kubeflow Pipeline steps are container-based, so that any action you can support via a Docker container image can become a pipeline step. +That doesn’t mean that an end-user necessarily needs to have Docker installed. For many straightforward cases, building your pipeline steps + +### Using the ‘lightweight python components’ functionality to build pipeline steps + +For most of the components in this example, we’re building them using the [“lightweight python components”][71] functionality as shown in [this example notebook][72], including compilation of the code into a component package. This feature allows you to create components based on Python functions, building on an appropriate base image, so that you do not need to have docker installed or rebuild a container image each time your code changes. + +Each component’s python file includes a function definition, and then a `func_to_container_op` call, passing the function definition, to generate the component’s `yaml` package file. As we’ll see below, these component package files make it very straightforward to put these steps together to form a pipeline. + + +The [`deploy_model_for_tables/tables_deploy_component.py`][73] file is representative. It contains an `automl_deploy_tables_model` function definition. + +``` +def automl_deploy_tables_model( + gcp_project_id: str, + gcp_region: str, + model_display_name: str, + api_endpoint: str = None, + ) -> NamedTuple('Outputs', [('model_display_name', str), ('status', str)]): +... + return (model_display_name, status) + +``` + +The function defines the component’s inputs and outputs, and this information will be used to support static checking when we compose these components to build the pipeline. + +To build the component `yaml` file corresponding to this function, we add the following to the components’ Python script, then can run `python .py` from the command line to generate it (you must have the Kubeflow Pipelines (KFP) sdk [installed][74]). + +```python +if __name__ == '__main__': + import kfp + kfp.components.func_to_container_op( + automl_deploy_tables_model, output_component_file='tables_deploy_component.yaml', + base_image='python:3.7') +``` + +Whenever you change the python function definition, just recompile to regenerate the corresponding component file. + +### Specifying the Tables pipeline + +With the components packaged into `yaml` files, it becomes very straightforward to specify a pipeline, such as [`tables_pipeline_caip.py`][75], that uses them. Here, we’re just using the `load_component_from_file()` method, since the `yaml` files are all local (in the same repo). However, there is also a `load_component_from_url()` method, which makes it easy to share components. (If your URL points to a file in GitHub, be sure to use raw mode). + +```python +create_dataset_op = comp.load_component_from_file( + './create_dataset_for_tables/tables_component.yaml') +import_data_op = comp.load_component_from_file( + './import_data_from_bigquery/tables_component.yaml') +set_schema_op = comp.load_component_from_file( + './import_data_from_bigquery/tables_schema_component.yaml') +train_model_op = comp.load_component_from_file( + './create_model_for_tables/tables_component.yaml') +eval_model_op = comp.load_component_from_file( + './create_model_for_tables/tables_eval_component.yaml') +eval_metrics_op = comp.load_component_from_file( + './create_model_for_tables/tables_eval_metrics_component.yaml') +deploy_model_op = comp.load_component_from_file( + './deploy_model_for_tables/tables_deploy_component.yaml') +``` + +Once all our pipeline ops (steps) are defined using the component definitions, then we can specify the pipeline by calling the constructors, e.g.: +```python + create_dataset = create_dataset_op( + gcp_project_id=gcp_project_id, + gcp_region=gcp_region, + dataset_display_name=dataset_display_name, + api_endpoint=api_endpoint, + ) +``` + +If a pipeline component has been defined to have outputs, other components can access those outputs. E.g., here, the ‘eval’ step is grabbing an output from the ‘train’ step, specifically information about the model display name: + +```python + eval_model = eval_model_op( + gcp_project_id=gcp_project_id, + gcp_region=gcp_region, + bucket_name=bucket_name, + gcs_path='automl_evals/{}'.format(dsl.RUN_ID_PLACEHOLDER), + api_endpoint=api_endpoint, + model_display_name=train_model.outputs['model_display_name'] + ) +``` + +In this manner it is straightforward to put together a pipeline from your component definitions. Just don’t forget to recompile the pipeline script (to generate its corresponding `.tar.gz` archive) if any of its component definitions changed, e.g. `python tables_pipeline_caip.py`. + + + +[1]: https://cloud.google.com/automl-tables/docs/ +[2]: https://googleapis.dev/python/automl/latest/gapic/v1beta1/tables.html +[3]: https://cloud.google.com/blog/products/ai-machine-learning/explaining-model-predictions-structured-data +[4]: http://amygdala.github.io/automl/ml/2019/12/05/automl_tables_export.html +[5]: https://cloud.google.com/automl-tables/docs/logging +[6]: %20https://cloud.google.com/blog/products/ai-machine-learning/introducing-cloud-ai-platform-pipelines +[7]: https://cloud.google.com/automl-tables/docs/import#create +[8]: https://cloud.google.com/automl-tables/docs/import#import-data +[9]: https://cloud.google.com/bigquery +[10]: https://cloud.google.com/automl-tables/docs/train +[11]: https://cloud.google.com/automl-tables/docs/evaluate +[12]: https://cloud.google.com/automl-tables/docs/predict +[13]: https://cloud.google.com/blog/products/ai-machine-learning/explaining-model-predictions-structured-data +[14]: https://console.cloud.google.com/automl-tables +[15]: https://github.com/amygdala/code-snippets/blob/master/ml/automl/tables/xai/automl_tables_xai.ipynb +[16]: http://amygdala.github.io/kubeflow/ml/2019/08/22/remote-deploy.html#using-cloud-function-triggers +[17]: https://cloud.google.com/storage +[18]: https://cloud.google.com/bigquery/public-data/ +[19]: https://cloud.google.com/bigquery/ +[20]: https://console.cloud.google.com/bigquery?p=bigquery-public-data&d=london_bicycles&page=dataset +[21]: https://console.cloud.google.com/bigquery?p=bigquery-public-data&d=noaa_gsod&page=dataset +[22]: xxx +[23]: https://cloud.google.com/ai-platform/pipelines/docs +[24]: xxx +[25]: xxx +[26]: https://cloud.google.com/blog/products/ai-machine-learning/introducing-cloud-ai-platform-pipelines +[27]: https://console.cloud.google.com/ai-platform/pipelines/clusters +[28]: https://console.cloud.google.com +[29]: https://cloud.google.com/ai-platform/pipelines/docs +[30]: https://github.com/kubeflow/pipelines/tree/master/manifests/gcp_marketplace +[31]: https://www.kubeflow.org/ +[32]: https://cloud.google.com/kubernetes-engine +[33]: https://cloud.google.com/iap +[34]: https://deploy.kubeflow.cloud/#/deploy +[35]: https://www.kubeflow.org/docs/gke/deploy/deploy-cli/ +[36]: ./tables_pipeline_caip.py.tar.gz +[37]: ./tables_pipeline_caip.py +[38]: xxx +[39]: ./tables_pipeline_kf.py.tar.gz +[40]: ./tables_pipeline_kf.py +[41]: xxx +[42]: https://cloud.google.com/automl-tables/docs/locations#buckets +[43]: xxx +[44]: https://cloud.google.com/automl-tables/docs/import#create +[45]: https://cloud.google.com/automl-tables/docs/import#import-data +[46]: https://cloud.google.com/bigquery +[47]: https://cloud.google.com/automl-tables/docs/train +[48]: https://cloud.google.com/automl-tables/docs/evaluate +[49]: https://cloud.google.com/automl-tables/docs/predict +[50]: https://cloud.google.com/bigquery +[51]: xxx +[52]: https://cloud.google.com/automl-tables/docs/train#opt-obj +[53]: https://cloud.google.com/automl-tables/docs/logging +[54]: https://console.cloud.google.com/automl-tables +[55]: https://cloud.google.com/blog/products/ai-machine-learning/introducing-cloud-ai-platform-pipelines +[56]: https://cloud.google.com/blog/products/ai-machine-learning/explaining-model-predictions-structured-data +[57]: https://github.com/amygdala/code-snippets/blob/master/ml/automl/tables/xai/automl_tables_xai.ipynb +[58]: https://gist.github.com/amygdala/c96d45bdf694737d77d91597ca3ef1f0 +[59]: https://console.cloud.google.com/automl-tables +[60]: https://cloud.google.com/blog/products/ai-machine-learning/explaining-model-predictions-structured-data +[61]: https://console.cloud.google.com/automl-tables +[62]: http://amygdala.github.io/automl/ml/2019/12/05/automl_tables_export.html +[63]: https://cloud.google.com/run +[64]: http://amygdala.github.io/automl/ml/2019/12/05/automl_tables_export.html#view-information-about-your-exported-model-in-tensorboard +[65]: xxx +[66]: http://amygdala.github.io/automl/ml/2019/12/05/automl_tables_export.html +[67]: https://console.cloud.google.com/kubernetes/list +[68]: https://googleapis.dev/python/automl/latest/gapic/v1beta1/tables.html +[69]: https://cloud.google.com/automl/docs/reference/rest/v1/projects.locations.models/export +[70]: https://googleapis.dev/python/automl/latest/gapic/v1beta1/tables.html +[71]: https://www.kubeflow.org/docs/pipelines/sdk/lightweight-python-components/ +[72]: https://github.com/kubeflow/pipelines/blob/master/samples/tutorials/mnist/01_Lightweight_Python_Components.ipynb +[73]: ./deploy_model_for_tables/tables_deploy_component.py +[74]: https://www.kubeflow.org/docs/pipelines/sdk/install-sdk/ +[75]: ./tables_pipeline_caip.py \ No newline at end of file diff --git a/ml/automl/tables/kfp_e2e/deploy_model_for_tables/tables_deploy_component.py b/ml/automl/tables/kfp_e2e/deploy_model_for_tables/tables_deploy_component.py index 6df7c4e..6acf035 100644 --- a/ml/automl/tables/kfp_e2e/deploy_model_for_tables/tables_deploy_component.py +++ b/ml/automl/tables/kfp_e2e/deploy_model_for_tables/tables_deploy_component.py @@ -69,8 +69,6 @@ def automl_deploy_tables_model( if __name__ == '__main__': import kfp - kfp.components.func_to_container_op(automl_deploy_tables_model, output_component_file='tables_deploy_component.yaml', base_image='python:3.7') - - -# if __name__ == "__main__": - # automl_deploy_tables_model('aju-vtests2', 'us-central1', model_display_name='so_digest2_20191220032828' ) + kfp.components.func_to_container_op( + automl_deploy_tables_model, output_component_file='tables_deploy_component.yaml', + base_image='python:3.7') diff --git a/ml/automl/tables/kfp_e2e/deploy_model_for_tables/tables_deploy_component.yaml b/ml/automl/tables/kfp_e2e/deploy_model_for_tables/tables_deploy_component.yaml index 37949d2..db6d2a9 100644 --- a/ml/automl/tables/kfp_e2e/deploy_model_for_tables/tables_deploy_component.yaml +++ b/ml/automl/tables/kfp_e2e/deploy_model_for_tables/tables_deploy_component.yaml @@ -62,7 +62,7 @@ implementation: _output_files = _parsed_args.pop(\"_output_paths\", [])\n\n_outputs = automl_deploy_tables_model(**_parsed_args)\n\ \nif not hasattr(_outputs, '__getitem__') or isinstance(_outputs, str):\n \ \ _outputs = [_outputs]\n\n_output_serializers = [\n _serialize_str,\n \ - \ _serialize_str\n]\n\nimport os\nfor idx, output_file in enumerate(_output_files):\n\ + \ _serialize_str,\n\n]\n\nimport os\nfor idx, output_file in enumerate(_output_files):\n\ \ try:\n os.makedirs(os.path.dirname(output_file))\n except OSError:\n\ \ pass\n with open(output_file, 'w') as f:\n f.write(_output_serializers[idx](_outputs[idx]))\n" args: diff --git a/ml/automl/tables/kfp_e2e/tables_pipeline_caip.py b/ml/automl/tables/kfp_e2e/tables_pipeline_caip.py index 361eae1..732808f 100644 --- a/ml/automl/tables/kfp_e2e/tables_pipeline_caip.py +++ b/ml/automl/tables/kfp_e2e/tables_pipeline_caip.py @@ -68,7 +68,7 @@ def automl_tables( #pylint: disable=unused-argument # ["title", "body", "answer_count", "comment_count", "creation_date", "favorite_count", "owner_user_id", "score", "view_count"] include_column_spec_names: str = '', exclude_column_spec_names: str = '', - bucket_name: str = 'aju-pipelines', + bucket_name: str = 'YOUR_BUCKET_NAME', # thresholds: str = '{"au_prc": 0.9}', thresholds: str = '{"mean_absolute_error": 480}', ): @@ -78,7 +78,7 @@ def automl_tables( #pylint: disable=unused-argument gcp_region=gcp_region, dataset_display_name=dataset_display_name, api_endpoint=api_endpoint, - ) #.apply(gcp.use_gcp_secret('user-gcp-sa')) + ) import_data = import_data_op( @@ -87,7 +87,7 @@ def automl_tables( #pylint: disable=unused-argument dataset_display_name=dataset_display_name, api_endpoint=api_endpoint, path=path - ) #.apply(gcp.use_gcp_secret('user-gcp-sa')) + ) set_schema = set_schema_op( gcp_project_id=gcp_project_id, @@ -98,7 +98,7 @@ def automl_tables( #pylint: disable=unused-argument schema_info=schema_info, time_col_name=time_col_name # test_train_col_name=test_train_col_name - ) #.apply(gcp.use_gcp_secret('user-gcp-sa')) + ) import_data.after(create_dataset) @@ -112,7 +112,7 @@ def automl_tables( #pylint: disable=unused-argument model_prefix=model_prefix, train_budget_milli_node_hours=train_budget_milli_node_hours, optimization_objective=optimization_objective - ) #.apply(gcp.use_gcp_secret('user-gcp-sa')) + ) train_model.after(set_schema) @@ -123,7 +123,7 @@ def automl_tables( #pylint: disable=unused-argument gcs_path='automl_evals/{}'.format(dsl.RUN_ID_PLACEHOLDER), api_endpoint=api_endpoint, model_display_name=train_model.outputs['model_display_name'] - ) #.apply(gcp.use_gcp_secret('user-gcp-sa')) + ) eval_metrics = eval_metrics_op( gcp_project_id=gcp_project_id, @@ -133,8 +133,7 @@ def automl_tables( #pylint: disable=unused-argument model_display_name=train_model.outputs['model_display_name'], thresholds=thresholds, eval_data=eval_model.outputs['eval_data'], - # gcs_path=eval_model.outputs['evals_gcs_path'] - ) #.apply(gcp.use_gcp_secret('user-gcp-sa')) + ) with dsl.Condition(eval_metrics.outputs['deploy'] == True): deploy_model = deploy_model_op( @@ -142,7 +141,7 @@ def automl_tables( #pylint: disable=unused-argument gcp_region=gcp_region, api_endpoint=api_endpoint, model_display_name=train_model.outputs['model_display_name'], - ) #.apply(gcp.use_gcp_secret('user-gcp-sa')) + ) if __name__ == '__main__': diff --git a/ml/automl/tables/kfp_e2e/tables_pipeline_caip.py.tar.gz b/ml/automl/tables/kfp_e2e/tables_pipeline_caip.py.tar.gz index 06daf103f2d1f2702f535ee127fdd97b4be480bd..d33fd0ff7840a50a4d7a065e25b3e7aa9fb1ff24 100644 GIT binary patch literal 10466 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 literal 10492 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 diff --git a/ml/automl/tables/kfp_e2e/tables_pipeline_kf.py b/ml/automl/tables/kfp_e2e/tables_pipeline_kf.py index 7ae9913..5812675 100644 --- a/ml/automl/tables/kfp_e2e/tables_pipeline_kf.py +++ b/ml/automl/tables/kfp_e2e/tables_pipeline_kf.py @@ -68,7 +68,7 @@ def automl_tables( #pylint: disable=unused-argument # ["title", "body", "answer_count", "comment_count", "creation_date", "favorite_count", "owner_user_id", "score", "view_count"] include_column_spec_names: str = '', exclude_column_spec_names: str = '', - bucket_name: str = 'aju-pipelines', + bucket_name: str = 'YOUR_BUCKET_NAME', # thresholds: str = '{"au_prc": 0.9}', thresholds: str = '{"mean_absolute_error": 480}', ): @@ -133,7 +133,6 @@ def automl_tables( #pylint: disable=unused-argument model_display_name=train_model.outputs['model_display_name'], thresholds=thresholds, eval_data=eval_model.outputs['eval_data'], - # gcs_path=eval_model.outputs['evals_gcs_path'] ).apply(gcp.use_gcp_secret('user-gcp-sa')) with dsl.Condition(eval_metrics.outputs['deploy'] == True): diff --git a/ml/automl/tables/kfp_e2e/tables_pipeline_kf.py.tar.gz b/ml/automl/tables/kfp_e2e/tables_pipeline_kf.py.tar.gz index ac54deb60a10ab6e1d9fbf003f474c484bb56338..37870db9c8f1fa8b4a46f9da6ef7f464823b84da 100644 GIT binary patch literal 10730 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 From 0fe037a39b71222d4afa12002de97d2750df0a87 Mon Sep 17 00:00:00 2001 From: Amy Unruh Date: Wed, 1 Apr 2020 12:56:53 -0700 Subject: [PATCH 6/9] checkpointing; readme updates and cleanup --- ml/automl/tables/kfp_e2e/README.md | 64 +-- .../tables_component.py | 22 +- .../tables_component.yaml | 26 +- .../tables_component.py | 27 +- .../tables_component.yaml | 167 +++++--- .../tables_eval_component.py | 48 +-- .../tables_eval_component.yaml | 310 +++++++++----- .../tables_eval_metrics_component.py | 78 ++-- .../tables_eval_metrics_component.yaml | 379 ++++++++++-------- .../deploy_model_for_tables/convert_oss.py | 3 +- .../exported_model_deploy.py | 9 +- .../tables_deploy_component.py | 16 +- .../tables_deploy_component.yaml | 132 ++++-- .../tables_component.py | 55 ++- .../tables_component.yaml | 52 +-- .../tables_schema_component.py | 71 ++-- .../tables_schema_component.yaml | 64 +-- .../tables/kfp_e2e/tables_pipeline_caip.py | 10 +- .../kfp_e2e/tables_pipeline_caip.py.tar.gz | Bin 10466 -> 9019 bytes .../tables/kfp_e2e/tables_pipeline_kf.py | 10 +- .../kfp_e2e/tables_pipeline_kf.py.tar.gz | Bin 10730 -> 9228 bytes 21 files changed, 884 insertions(+), 659 deletions(-) diff --git a/ml/automl/tables/kfp_e2e/README.md b/ml/automl/tables/kfp_e2e/README.md index 0952880..ed00330 100644 --- a/ml/automl/tables/kfp_e2e/README.md +++ b/ml/automl/tables/kfp_e2e/README.md @@ -1,3 +1,6 @@ + + + # AutoML Tables: end-to-end workflow on Cloud AI Platform Pipelines @@ -35,7 +38,7 @@ A number of new AutoML Tables features have been released recently. These includ This tutorial gives a tour of some of these new features via a [Cloud AI Platform Pipelines][6] example, that shows end-to-end management of an AutoML Tables workflow. The example pipeline [creates a _dataset_][7], [imports][8] data into the dataset from a [BigQuery][9] _view_, and [trains][10] a custom model on that data. Then, it fetches [evaluation and metrics][11] information about the trained model, and based on specified criteria about model quality, uses that information to automatically determine whether to [deploy][12] the model for online prediction. Once the model is deployed, you can make prediction requests, and optionally obtain prediction [explanations][13] as well as the prediction result. -In addition, the example shows how to scalably **_serve_** your exported trained model from your Cloud AI Platform Pipelines installation for prediction requests. +In addition, the example shows how to scalably **_serve_** your exported trained model from your Cloud AI Platform Pipelines installation for prediction requests. You can manage all the parts of this workflow from the [Tables UI][14] as well, or programmatically via a [notebook][15] or script. But specifying this process as a workflow has some advantages: the workflow becomes reliable and repeatable, and Pipelines makes it easy to monitor the results and schedule recurring runs. For example, if your dataset is updated regularly—say once a day— you could schedule a workflow to run daily, each day building a model that trains on an updated dataset. @@ -263,7 +266,7 @@ This means that you can run a model serving service on your AI Platform Pipeline [This blog post][62] walks through the steps to serve the exported model (in this case, using [Cloud Run][63]). Follow the instructions in the post through the “View information about your exported model in TensorBoard” [section][64]. Here, we’ll diverge from the rest of the post and create a GKE service instead. -Make a copy of the [`model_serve_template.yaml`][65] file and name it `model_serve.yaml`. Edit this new file, **replacing** `MODEL_NAME` with some meaningful name for your model, `IMAGE_NAME` with the name of the container image you built (as described in the [blog post][66], and `NAMESPACE` with the namespace in which you want to run your service (e.g. `default`). +Make a copy of [`deploy_model_for_tables/model_serve_template.yaml`][65] file and name it `model_serve.yaml`. Edit this new file, **replacing** `MODEL_NAME` with some meaningful name for your model, `IMAGE_NAME` with the name of the container image you built (as described in the [blog post][66], and `NAMESPACE` with the namespace in which you want to run your service (e.g. `default`). Then, from the command line, run: ```bash @@ -286,22 +289,34 @@ From the command line, run the following, **replacing** `` with kubectl -n port-forward svc/ 8080:80 ``` -> **Note**: it would be possible to add this deployment step to the pipeline too. However, the [Python client library][68] does not yet support the ‘export’ operation. Once deployment is supported by the client library, this would be a natural addition to the workflow. While not tested, it should also be possible to do the export programmatically via the [REST API][69]. +Then, from the `deploy_model_for_tables` directory, send a prediction request to your service like this: +```bash +curl -X POST --data @./instances.json http://localhost:8080/predict +``` + +You should see a result like this, with a prediction for each instance in the `instances.json` file: +```bash +{"predictions": [860.79833984375, 460.5323486328125, 1211.7664794921875]} +``` + +(If you get an error, make sure you’re in the correct directory and see the `instances.json` file listed). + +> **Note**: it would be possible to add this deployment step to the pipeline too. (See [`deploy_model_for_tables/exported_model_deploy.py`][68]). However, the [Python client library][69] does not yet support the ‘export’ operation. Once deployment is supported by the client library, this would be a natural addition to the workflow. While not tested, it should also be possible to do the export programmatically via the [REST API][70]. ## A deeper dive into the pipeline code -The updated [Tables Python client library][70] makes it very straightforward to build the Pipelines components that support each stage of the workflow. +The updated [Tables Python client library][71] makes it very straightforward to build the Pipelines components that support each stage of the workflow. Kubeflow Pipeline steps are container-based, so that any action you can support via a Docker container image can become a pipeline step. That doesn’t mean that an end-user necessarily needs to have Docker installed. For many straightforward cases, building your pipeline steps ### Using the ‘lightweight python components’ functionality to build pipeline steps -For most of the components in this example, we’re building them using the [“lightweight python components”][71] functionality as shown in [this example notebook][72], including compilation of the code into a component package. This feature allows you to create components based on Python functions, building on an appropriate base image, so that you do not need to have docker installed or rebuild a container image each time your code changes. +For most of the components in this example, we’re building them using the [“lightweight python components”][72] functionality as shown in [this example notebook][73], including compilation of the code into a component package. This feature allows you to create components based on Python functions, building on an appropriate base image, so that you do not need to have docker installed or rebuild a container image each time your code changes. Each component’s python file includes a function definition, and then a `func_to_container_op` call, passing the function definition, to generate the component’s `yaml` package file. As we’ll see below, these component package files make it very straightforward to put these steps together to form a pipeline. -The [`deploy_model_for_tables/tables_deploy_component.py`][73] file is representative. It contains an `automl_deploy_tables_model` function definition. +The [`deploy_model_for_tables/tables_deploy_component.py`][74] file is representative. It contains an `automl_deploy_tables_model` function definition. ``` def automl_deploy_tables_model( @@ -317,7 +332,7 @@ def automl_deploy_tables_model( The function defines the component’s inputs and outputs, and this information will be used to support static checking when we compose these components to build the pipeline. -To build the component `yaml` file corresponding to this function, we add the following to the components’ Python script, then can run `python .py` from the command line to generate it (you must have the Kubeflow Pipelines (KFP) sdk [installed][74]). +To build the component `yaml` file corresponding to this function, we add the following to the components’ Python script, then can run `python .py` from the command line to generate it (you must have the Kubeflow Pipelines (KFP) sdk [installed][75]). ```python if __name__ == '__main__': @@ -331,7 +346,7 @@ Whenever you change the python function definition, just recompile to regenerate ### Specifying the Tables pipeline -With the components packaged into `yaml` files, it becomes very straightforward to specify a pipeline, such as [`tables_pipeline_caip.py`][75], that uses them. Here, we’re just using the `load_component_from_file()` method, since the `yaml` files are all local (in the same repo). However, there is also a `load_component_from_url()` method, which makes it easy to share components. (If your URL points to a file in GitHub, be sure to use raw mode). +With the components packaged into `yaml` files, it becomes very straightforward to specify a pipeline, such as [`tables_pipeline_caip.py`][76], that uses them. Here, we’re just using the `load_component_from_file()` method, since the `yaml` files are all local (in the same repo). However, there is also a `load_component_from_url()` method, which makes it easy to share components. (If your URL points to a file in GitHub, be sure to use raw mode). ```python create_dataset_op = comp.load_component_from_file( @@ -398,10 +413,10 @@ In this manner it is straightforward to put together a pipeline from your compon [19]: https://cloud.google.com/bigquery/ [20]: https://console.cloud.google.com/bigquery?p=bigquery-public-data&d=london_bicycles&page=dataset [21]: https://console.cloud.google.com/bigquery?p=bigquery-public-data&d=noaa_gsod&page=dataset -[22]: xxx +[22]: https://cloud.google.com/blog/products/ai-machine-learning/explaining-model-predictions-structured-data [23]: https://cloud.google.com/ai-platform/pipelines/docs -[24]: xxx -[25]: xxx +[24]: https://kubeflow.org/ +[25]: https://www.kubeflow.org/docs/gke/deploy/ [26]: https://cloud.google.com/blog/products/ai-machine-learning/introducing-cloud-ai-platform-pipelines [27]: https://console.cloud.google.com/ai-platform/pipelines/clusters [28]: https://console.cloud.google.com @@ -414,12 +429,12 @@ In this manner it is straightforward to put together a pipeline from your compon [35]: https://www.kubeflow.org/docs/gke/deploy/deploy-cli/ [36]: ./tables_pipeline_caip.py.tar.gz [37]: ./tables_pipeline_caip.py -[38]: xxx +[38]: https://www.kubeflow.org/docs/pipelines/sdk/install-sdk/ [39]: ./tables_pipeline_kf.py.tar.gz [40]: ./tables_pipeline_kf.py -[41]: xxx +[41]: https://cloud.google.com/storage [42]: https://cloud.google.com/automl-tables/docs/locations#buckets -[43]: xxx +[43]: https://cloud.google.com/bigquery [44]: https://cloud.google.com/automl-tables/docs/import#create [45]: https://cloud.google.com/automl-tables/docs/import#import-data [46]: https://cloud.google.com/bigquery @@ -427,7 +442,7 @@ In this manner it is straightforward to put together a pipeline from your compon [48]: https://cloud.google.com/automl-tables/docs/evaluate [49]: https://cloud.google.com/automl-tables/docs/predict [50]: https://cloud.google.com/bigquery -[51]: xxx +[51]: https://cloud.google.com/automl-tables/docs/problem-types#regression_problems [52]: https://cloud.google.com/automl-tables/docs/train#opt-obj [53]: https://cloud.google.com/automl-tables/docs/logging [54]: https://console.cloud.google.com/automl-tables @@ -441,14 +456,15 @@ In this manner it is straightforward to put together a pipeline from your compon [62]: http://amygdala.github.io/automl/ml/2019/12/05/automl_tables_export.html [63]: https://cloud.google.com/run [64]: http://amygdala.github.io/automl/ml/2019/12/05/automl_tables_export.html#view-information-about-your-exported-model-in-tensorboard -[65]: xxx +[65]: ./deploy_model_for_tables/model_serve_template.yaml [66]: http://amygdala.github.io/automl/ml/2019/12/05/automl_tables_export.html [67]: https://console.cloud.google.com/kubernetes/list -[68]: https://googleapis.dev/python/automl/latest/gapic/v1beta1/tables.html -[69]: https://cloud.google.com/automl/docs/reference/rest/v1/projects.locations.models/export -[70]: https://googleapis.dev/python/automl/latest/gapic/v1beta1/tables.html -[71]: https://www.kubeflow.org/docs/pipelines/sdk/lightweight-python-components/ -[72]: https://github.com/kubeflow/pipelines/blob/master/samples/tutorials/mnist/01_Lightweight_Python_Components.ipynb -[73]: ./deploy_model_for_tables/tables_deploy_component.py -[74]: https://www.kubeflow.org/docs/pipelines/sdk/install-sdk/ -[75]: ./tables_pipeline_caip.py \ No newline at end of file +[68]: ./deploy_model_for_tables/exported_model_deploy.py +[69]: https://googleapis.dev/python/automl/latest/gapic/v1beta1/tables.html +[70]: https://cloud.google.com/automl/docs/reference/rest/v1/projects.locations.models/export +[71]: https://googleapis.dev/python/automl/latest/gapic/v1beta1/tables.html +[72]: https://www.kubeflow.org/docs/pipelines/sdk/lightweight-python-components/ +[73]: https://github.com/kubeflow/pipelines/blob/master/samples/tutorials/mnist/01_Lightweight_Python_Components.ipynb +[74]: ./deploy_model_for_tables/tables_deploy_component.py +[75]: https://www.kubeflow.org/docs/pipelines/sdk/install-sdk/ +[76]: ./tables_pipeline_caip.py diff --git a/ml/automl/tables/kfp_e2e/create_dataset_for_tables/tables_component.py b/ml/automl/tables/kfp_e2e/create_dataset_for_tables/tables_component.py index c494064..dbb3652 100644 --- a/ml/automl/tables/kfp_e2e/create_dataset_for_tables/tables_component.py +++ b/ml/automl/tables/kfp_e2e/create_dataset_for_tables/tables_component.py @@ -21,16 +21,16 @@ def automl_create_dataset_for_tables( dataset_display_name: str, api_endpoint: str = None, tables_dataset_metadata: dict = {}, - # retry=None, #=google.api_core.gapic_v1.method.DEFAULT, - # timeout: float = None, #=google.api_core.gapic_v1.method.DEFAULT, - # metadata: dict = None, ) -> NamedTuple('Outputs', [('dataset_path', str), ('create_time', str), ('dataset_id', str)]): - '''automl_create_dataset_for_tables creates an empty Dataset for AutoML tables - ''' + import sys import subprocess - subprocess.run([sys.executable, '-m', 'pip', 'install', 'googleapis-common-protos==1.6.0', '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) - subprocess.run([sys.executable, '-m', 'pip', 'install', 'google-cloud-automl==0.9.0', '--quiet', '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) + subprocess.run([sys.executable, '-m', 'pip', 'install', 'googleapis-common-protos==1.6.0', + '--no-warn-script-location'], + env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) + subprocess.run([sys.executable, '-m', 'pip', 'install', 'google-cloud-automl==0.9.0', + '--quiet', '--no-warn-script-location'], + env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) import google import logging @@ -49,7 +49,6 @@ def automl_create_dataset_for_tables( try: # Create a dataset with the given display name - # TODO: not clear that description, timeout & retry args still supported?.. dataset = client.create_dataset(dataset_display_name, metadata=tables_dataset_metadata) # Log info about the created dataset logging.info("Dataset name: {}".format(dataset.name)) @@ -65,12 +64,9 @@ def automl_create_dataset_for_tables( dataset_id = dataset.name.rsplit('/', 1)[-1] return (dataset.name, str(dataset.create_time), dataset_id) except google.api_core.exceptions.GoogleAPICallError as e: - logging.warn(e) - raise e # TODO: other exception? return values rather than raise exception? - + logging.warning(e) + raise e -# if __name__ == "__main__": -# automl_create_dataset_for_tables('aju-vtests2', 'us-central1', 'component_test2') if __name__ == '__main__': import kfp diff --git a/ml/automl/tables/kfp_e2e/create_dataset_for_tables/tables_component.yaml b/ml/automl/tables/kfp_e2e/create_dataset_for_tables/tables_component.yaml index dbeba33..9daa343 100644 --- a/ml/automl/tables/kfp_e2e/create_dataset_for_tables/tables_component.yaml +++ b/ml/automl/tables/kfp_e2e/create_dataset_for_tables/tables_component.yaml @@ -1,6 +1,4 @@ name: Automl create dataset for tables -description: | - automl_create_dataset_for_tables creates an empty Dataset for AutoML tables inputs: - name: gcp_project_id type: String @@ -38,16 +36,16 @@ implementation: dataset_display_name: str, api_endpoint: str = None, tables_dataset_metadata: dict = {}, - # retry=None, #=google.api_core.gapic_v1.method.DEFAULT, - # timeout: float = None, #=google.api_core.gapic_v1.method.DEFAULT, - # metadata: dict = None, ) -> NamedTuple('Outputs', [('dataset_path', str), ('create_time', str), ('dataset_id', str)]): - '''automl_create_dataset_for_tables creates an empty Dataset for AutoML tables - ''' + import sys import subprocess - subprocess.run([sys.executable, '-m', 'pip', 'install', 'googleapis-common-protos==1.6.0', '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) - subprocess.run([sys.executable, '-m', 'pip', 'install', 'google-cloud-automl==0.9.0', '--quiet', '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) + subprocess.run([sys.executable, '-m', 'pip', 'install', 'googleapis-common-protos==1.6.0', + '--no-warn-script-location'], + env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) + subprocess.run([sys.executable, '-m', 'pip', 'install', 'google-cloud-automl==0.9.0', + '--quiet', '--no-warn-script-location'], + env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) import google import logging @@ -66,7 +64,6 @@ implementation: try: # Create a dataset with the given display name - # TODO: not clear that description, timeout & retry args still supported?.. dataset = client.create_dataset(dataset_display_name, metadata=tables_dataset_metadata) # Log info about the created dataset logging.info("Dataset name: {}".format(dataset.name)) @@ -82,8 +79,8 @@ implementation: dataset_id = dataset.name.rsplit('/', 1)[-1] return (dataset.name, str(dataset.create_time), dataset_id) except google.api_core.exceptions.GoogleAPICallError as e: - logging.warn(e) - raise e # TODO: other exception? return values rather than raise exception? + logging.warning(e) + raise e import json def _serialize_str(str_value: str) -> str: @@ -92,7 +89,7 @@ implementation: return str_value import argparse - _parser = argparse.ArgumentParser(prog='Automl create dataset for tables', description='automl_create_dataset_for_tables creates an empty Dataset for AutoML tables\n') + _parser = argparse.ArgumentParser(prog='Automl create dataset for tables', description='') _parser.add_argument("--gcp-project-id", dest="gcp_project_id", type=str, required=True, default=argparse.SUPPRESS) _parser.add_argument("--gcp-region", dest="gcp_region", type=str, required=True, default=argparse.SUPPRESS) _parser.add_argument("--dataset-display-name", dest="dataset_display_name", type=str, required=True, default=argparse.SUPPRESS) @@ -110,7 +107,8 @@ implementation: _output_serializers = [ _serialize_str, _serialize_str, - _serialize_str + _serialize_str, + ] import os diff --git a/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_component.py b/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_component.py index 104204a..d97ccb6 100644 --- a/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_component.py +++ b/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_component.py @@ -16,22 +16,26 @@ def automl_create_model_for_tables( - gcp_project_id: str, - gcp_region: str, - dataset_display_name: str, + gcp_project_id: str, + gcp_region: str, + dataset_display_name: str, api_endpoint: str = None, model_display_name: str = None, model_prefix: str = 'bwmodel', optimization_objective: str = None, include_column_spec_names: list = None, exclude_column_spec_names: list = None, - train_budget_milli_node_hours: int = 1000, + train_budget_milli_node_hours: int = 1000, ) -> NamedTuple('Outputs', [('model_display_name', str), ('model_name', str), ('model_id', str)]): import subprocess import sys - subprocess.run([sys.executable, '-m', 'pip', 'install', 'googleapis-common-protos==1.6.0', '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) - subprocess.run([sys.executable, '-m', 'pip', 'install', 'google-cloud-automl==0.9.0', '--quiet', '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) + # we could build a base image that includes these libraries if we don't want to do + # the dynamic installation when the step runs. + subprocess.run([sys.executable, '-m', 'pip', 'install', 'googleapis-common-protos==1.6.0', '--no-warn-script-location'], + env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) + subprocess.run([sys.executable, '-m', 'pip', 'install', 'google-cloud-automl==0.9.0', '--quiet', '--no-warn-script-location'], + env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) import google import logging @@ -76,12 +80,7 @@ def automl_create_model_for_tables( if __name__ == '__main__': - import kfp - kfp.components.func_to_container_op(automl_create_model_for_tables, output_component_file='tables_component.yaml', + import kfp + kfp.components.func_to_container_op(automl_create_model_for_tables, + output_component_file='tables_component.yaml', base_image='python:3.7') - - -# if __name__ == "__main__": -# automl_create_model_for_tables('aju-vtests2', 'us-central1', 'so_digest2_32', -# include_column_spec_names=["title", "body", "answer_count", "comment_count", "creation_date", "favorite_count", "owner_user_id", "score", "view_count"] -# ) diff --git a/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_component.yaml b/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_component.yaml index 8ca6e7b..3006bd6 100644 --- a/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_component.yaml +++ b/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_component.yaml @@ -43,66 +43,113 @@ implementation: - python3 - -u - -c - - "from typing import NamedTuple\n\ndef automl_create_model_for_tables(\n\tgcp_project_id:\ - \ str,\n\tgcp_region: str,\n\tdataset_display_name: str,\n api_endpoint: str\ - \ = None,\n model_display_name: str = None,\n model_prefix: str = 'bwmodel',\n\ - \ optimization_objective: str = None,\n include_column_spec_names: list =\ - \ None,\n exclude_column_spec_names: list = None,\n\ttrain_budget_milli_node_hours:\ - \ int = 1000,\n) -> NamedTuple('Outputs', [('model_display_name', str), ('model_name',\ - \ str), ('model_id', str)]):\n\n import subprocess\n import sys\n subprocess.run([sys.executable,\ - \ '-m', 'pip', 'install', 'googleapis-common-protos==1.6.0', '--no-warn-script-location'],\ - \ env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True)\n subprocess.run([sys.executable,\ - \ '-m', 'pip', 'install', 'google-cloud-automl==0.9.0', '--quiet', '--no-warn-script-location'],\ - \ env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True)\n\n import google\n\ - \ import logging\n from google.api_core.client_options import ClientOptions\n\ - \ from google.cloud import automl_v1beta1 as automl\n import time\n\n logging.getLogger().setLevel(logging.INFO)\ - \ # TODO: make level configurable\n # TODO: we could instead check for region\ - \ 'eu' and use 'eu-automl.googleapis.com:443'endpoint\n # in that case, instead\ - \ of requiring endpoint to be specified.\n if api_endpoint:\n client_options\ - \ = ClientOptions(api_endpoint=api_endpoint)\n client = automl.TablesClient(project=gcp_project_id,\ - \ region=gcp_region,\n client_options=client_options)\n else:\n client\ - \ = automl.TablesClient(project=gcp_project_id, region=gcp_region)\n\n if not\ - \ model_display_name:\n model_display_name = '{}_{}'.format(model_prefix,\ - \ str(int(time.time())))\n\n logging.info('Training model {}...'.format(model_display_name))\n\ - \ response = client.create_model(\n model_display_name,\n train_budget_milli_node_hours=train_budget_milli_node_hours,\n\ - \ dataset_display_name=dataset_display_name,\n optimization_objective=optimization_objective,\n\ - \ include_column_spec_names=include_column_spec_names,\n exclude_column_spec_names=exclude_column_spec_names,\n\ - \ )\n\n logging.info(\"Training operation: {}\".format(response.operation))\n\ - \ logging.info(\"Training operation name: {}\".format(response.operation.name))\n\ - \ logging.info(\"Training in progress. This operation may take multiple hours\ - \ to complete.\")\n # block termination of the op until training is finished.\n\ - \ result = response.result()\n logging.info(\"Training completed: {}\".format(result))\n\ - \ model_name = result.name\n model_id = model_name.rsplit('/', 1)[-1]\n print('model\ - \ name: {}, model id: {}'.format(model_name, model_id))\n return (model_display_name,\ - \ model_name, model_id)\n\nimport json\ndef _serialize_str(str_value: str) ->\ - \ str:\n if not isinstance(str_value, str):\n raise TypeError('Value\ - \ \"{}\" has type \"{}\" instead of str.'.format(str(str_value), str(type(str_value))))\n\ - \ return str_value\n\nimport argparse\n_parser = argparse.ArgumentParser(prog='Automl\ - \ create model for tables', description='')\n_parser.add_argument(\"--gcp-project-id\"\ - , dest=\"gcp_project_id\", type=str, required=True, default=argparse.SUPPRESS)\n\ - _parser.add_argument(\"--gcp-region\", dest=\"gcp_region\", type=str, required=True,\ - \ default=argparse.SUPPRESS)\n_parser.add_argument(\"--dataset-display-name\"\ - , dest=\"dataset_display_name\", type=str, required=True, default=argparse.SUPPRESS)\n\ - _parser.add_argument(\"--api-endpoint\", dest=\"api_endpoint\", type=str, required=False,\ - \ default=argparse.SUPPRESS)\n_parser.add_argument(\"--model-display-name\"\ - , dest=\"model_display_name\", type=str, required=False, default=argparse.SUPPRESS)\n\ - _parser.add_argument(\"--model-prefix\", dest=\"model_prefix\", type=str, required=False,\ - \ default=argparse.SUPPRESS)\n_parser.add_argument(\"--optimization-objective\"\ - , dest=\"optimization_objective\", type=str, required=False, default=argparse.SUPPRESS)\n\ - _parser.add_argument(\"--include-column-spec-names\", dest=\"include_column_spec_names\"\ - , type=json.loads, required=False, default=argparse.SUPPRESS)\n_parser.add_argument(\"\ - --exclude-column-spec-names\", dest=\"exclude_column_spec_names\", type=json.loads,\ - \ required=False, default=argparse.SUPPRESS)\n_parser.add_argument(\"--train-budget-milli-node-hours\"\ - , dest=\"train_budget_milli_node_hours\", type=int, required=False, default=argparse.SUPPRESS)\n\ - _parser.add_argument(\"----output-paths\", dest=\"_output_paths\", type=str,\ - \ nargs=3)\n_parsed_args = vars(_parser.parse_args())\n_output_files = _parsed_args.pop(\"\ - _output_paths\", [])\n\n_outputs = automl_create_model_for_tables(**_parsed_args)\n\ - \nif not hasattr(_outputs, '__getitem__') or isinstance(_outputs, str):\n \ - \ _outputs = [_outputs]\n\n_output_serializers = [\n _serialize_str,\n \ - \ _serialize_str,\n _serialize_str\n]\n\nimport os\nfor idx, output_file\ - \ in enumerate(_output_files):\n try:\n os.makedirs(os.path.dirname(output_file))\n\ - \ except OSError:\n pass\n with open(output_file, 'w') as f:\n\ - \ f.write(_output_serializers[idx](_outputs[idx]))\n" + - | + from typing import NamedTuple + + def automl_create_model_for_tables( + gcp_project_id: str, + gcp_region: str, + dataset_display_name: str, + api_endpoint: str = None, + model_display_name: str = None, + model_prefix: str = 'bwmodel', + optimization_objective: str = None, + include_column_spec_names: list = None, + exclude_column_spec_names: list = None, + train_budget_milli_node_hours: int = 1000, + ) -> NamedTuple('Outputs', [('model_display_name', str), ('model_name', str), ('model_id', str)]): + + import subprocess + import sys + # we could build a base image that includes these libraries if we don't want to do + # the dynamic installation when the step runs. + subprocess.run([sys.executable, '-m', 'pip', 'install', 'googleapis-common-protos==1.6.0', '--no-warn-script-location'], + env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) + subprocess.run([sys.executable, '-m', 'pip', 'install', 'google-cloud-automl==0.9.0', '--quiet', '--no-warn-script-location'], + env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) + + import google + import logging + from google.api_core.client_options import ClientOptions + from google.cloud import automl_v1beta1 as automl + import time + + logging.getLogger().setLevel(logging.INFO) # TODO: make level configurable + # TODO: we could instead check for region 'eu' and use 'eu-automl.googleapis.com:443'endpoint + # in that case, instead of requiring endpoint to be specified. + if api_endpoint: + client_options = ClientOptions(api_endpoint=api_endpoint) + client = automl.TablesClient(project=gcp_project_id, region=gcp_region, + client_options=client_options) + else: + client = automl.TablesClient(project=gcp_project_id, region=gcp_region) + + if not model_display_name: + model_display_name = '{}_{}'.format(model_prefix, str(int(time.time()))) + + logging.info('Training model {}...'.format(model_display_name)) + response = client.create_model( + model_display_name, + train_budget_milli_node_hours=train_budget_milli_node_hours, + dataset_display_name=dataset_display_name, + optimization_objective=optimization_objective, + include_column_spec_names=include_column_spec_names, + exclude_column_spec_names=exclude_column_spec_names, + ) + + logging.info("Training operation: {}".format(response.operation)) + logging.info("Training operation name: {}".format(response.operation.name)) + logging.info("Training in progress. This operation may take multiple hours to complete.") + # block termination of the op until training is finished. + result = response.result() + logging.info("Training completed: {}".format(result)) + model_name = result.name + model_id = model_name.rsplit('/', 1)[-1] + print('model name: {}, model id: {}'.format(model_name, model_id)) + return (model_display_name, model_name, model_id) + + import json + def _serialize_str(str_value: str) -> str: + if not isinstance(str_value, str): + raise TypeError('Value "{}" has type "{}" instead of str.'.format(str(str_value), str(type(str_value)))) + return str_value + + import argparse + _parser = argparse.ArgumentParser(prog='Automl create model for tables', description='') + _parser.add_argument("--gcp-project-id", dest="gcp_project_id", type=str, required=True, default=argparse.SUPPRESS) + _parser.add_argument("--gcp-region", dest="gcp_region", type=str, required=True, default=argparse.SUPPRESS) + _parser.add_argument("--dataset-display-name", dest="dataset_display_name", type=str, required=True, default=argparse.SUPPRESS) + _parser.add_argument("--api-endpoint", dest="api_endpoint", type=str, required=False, default=argparse.SUPPRESS) + _parser.add_argument("--model-display-name", dest="model_display_name", type=str, required=False, default=argparse.SUPPRESS) + _parser.add_argument("--model-prefix", dest="model_prefix", type=str, required=False, default=argparse.SUPPRESS) + _parser.add_argument("--optimization-objective", dest="optimization_objective", type=str, required=False, default=argparse.SUPPRESS) + _parser.add_argument("--include-column-spec-names", dest="include_column_spec_names", type=json.loads, required=False, default=argparse.SUPPRESS) + _parser.add_argument("--exclude-column-spec-names", dest="exclude_column_spec_names", type=json.loads, required=False, default=argparse.SUPPRESS) + _parser.add_argument("--train-budget-milli-node-hours", dest="train_budget_milli_node_hours", type=int, required=False, default=argparse.SUPPRESS) + _parser.add_argument("----output-paths", dest="_output_paths", type=str, nargs=3) + _parsed_args = vars(_parser.parse_args()) + _output_files = _parsed_args.pop("_output_paths", []) + + _outputs = automl_create_model_for_tables(**_parsed_args) + + if not hasattr(_outputs, '__getitem__') or isinstance(_outputs, str): + _outputs = [_outputs] + + _output_serializers = [ + _serialize_str, + _serialize_str, + _serialize_str, + + ] + + import os + for idx, output_file in enumerate(_output_files): + try: + os.makedirs(os.path.dirname(output_file)) + except OSError: + pass + with open(output_file, 'w') as f: + f.write(_output_serializers[idx](_outputs[idx])) args: - --gcp-project-id - inputValue: gcp_project_id diff --git a/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_component.py b/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_component.py index 4ddbeb7..0321be9 100644 --- a/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_component.py +++ b/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_component.py @@ -17,8 +17,8 @@ def automl_eval_tables_model( - gcp_project_id: str, - gcp_region: str, + gcp_project_id: str, + gcp_region: str, model_display_name: str, bucket_name: str, gcs_path: str, @@ -27,10 +27,11 @@ def automl_eval_tables_model( api_endpoint: str = None, ) -> NamedTuple('Outputs', [ - # ('evals_gcs_path', str), ('feat_list', str)]): import subprocess import sys + # we could build a base image that includes these libraries if we don't want to do + # the dynamic installation when the step runs. subprocess.run([sys.executable, '-m', 'pip', 'install', 'googleapis-common-protos==1.6.0', '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) subprocess.run([sys.executable, '-m', 'pip', 'install', 'google-cloud-automl==0.9.0', @@ -75,17 +76,17 @@ def upload_blob(bucket_name, source_file_name, destination_blob_name, def get_model_details(client, model_display_name): try: - model = client.get_model(model_display_name=model_display_name) + model = client.get_model(model_display_name=model_display_name) except exceptions.NotFound: - logging.info("Model %s not found." % model_display_name) - return (None, None) + logging.info("Model %s not found." % model_display_name) + return (None, None) model = client.get_model(model_display_name=model_display_name) # Retrieve deployment state. if model.deployment_state == enums.Model.DeploymentState.DEPLOYED: - deployment_state = "deployed" + deployment_state = "deployed" else: - deployment_state = "undeployed" + deployment_state = "undeployed" # get features of top global importance feat_list = [ (column.feature_importance, column.column_display_name) @@ -93,18 +94,17 @@ def get_model_details(client, model_display_name): ] feat_list.sort(reverse=True) if len(feat_list) < 10: - feat_to_show = len(feat_list) + feat_to_show = len(feat_list) else: - feat_to_show = 10 + feat_to_show = 10 - # Display the model information. - # TODO: skip this? + # Log some information about the model logging.info("Model name: {}".format(model.name)) logging.info("Model id: {}".format(model.name.split("/")[-1])) logging.info("Model display name: {}".format(model.display_name)) logging.info("Features of top importance:") for feat in feat_list[:feat_to_show]: - logging.info(feat) + logging.info(feat) logging.info("Model create time:") logging.info("\tseconds: {}".format(model.create_time.seconds)) logging.info("\tnanos: {}".format(model.create_time.nanos)) @@ -144,13 +144,10 @@ def generate_fi_ui(feat_list): 'source': html_source }]} logging.info('using metadata dict {}'.format(json.dumps(metadata))) - # with open('/mlpipeline-ui-metadata.json', 'w') as f: - # json.dump(metadata, f) logging.info('using metadata ui path: {}'.format(mlpipeline_ui_metadata_path)) with open(mlpipeline_ui_metadata_path, 'w') as mlpipeline_ui_metadata_file: mlpipeline_ui_metadata_file.write(json.dumps(metadata)) - logging.getLogger().setLevel(logging.INFO) # TODO: make level configurable # TODO: we could instead check for region 'eu' and use 'eu-automl.googleapis.com:443'endpoint # in that case, instead of requiring endpoint to be specified. @@ -163,7 +160,6 @@ def generate_fi_ui(feat_list): (model, feat_list) = get_model_details(client, model_display_name) - evals = list(client.list_model_evaluations(model_display_name=model_display_name)) with open('temp_oput_regression', "w") as f: f.write('Model evals:\n{}'.format(evals)) @@ -179,22 +175,10 @@ def generate_fi_ui(feat_list): pathlib2.Path(eval_data_path).write_bytes(pstring) feat_list_string = json.dumps(feat_list) - # return(gcs_path, feat_list_string) - return(feat_list_string) + return feat_list_string if __name__ == '__main__': - import kfp - kfp.components.func_to_container_op(automl_eval_tables_model, + import kfp + kfp.components.func_to_container_op(automl_eval_tables_model, output_component_file='tables_eval_component.yaml', base_image='python:3.7') - -# if __name__ == '__main__': - -# # (eval_hex, features) = automl_eval_tables_model('aju-vtests2', 'us-central1', model_display_name='somodel_1579284627') -# (eval_hex, features) = automl_eval_tables_model('aju-vtests2', 'us-central1', -# bucket_name='aju-pipelines', model_display_name='bwmodel_1579017140', -# # gcs_path='automl_evals/testing/somodel_1579284627', -# eval_data_path=None) -# # with open('temp_oput', "w") as f: -# # f.write(eval_hex) - diff --git a/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_component.yaml b/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_component.yaml index a750897..0f05e2b 100644 --- a/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_component.yaml +++ b/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_component.yaml @@ -27,110 +27,212 @@ implementation: - python3 - -u - -c - - "class OutputPath:\n '''When creating component from function, OutputPath\ - \ should be used as function parameter annotation to tell the system that the\ - \ function wants to output data by writing it into a file with the given path\ - \ instead of returning the data from the function.'''\n def __init__(self,\ - \ type=None):\n self.type = type\n\ndef _make_parent_dirs_and_return_path(file_path:\ - \ str):\n import os\n os.makedirs(os.path.dirname(file_path), exist_ok=True)\n\ - \ return file_path\n\nfrom typing import NamedTuple\n\ndef automl_eval_tables_model(\n\ - \tgcp_project_id: str,\n\tgcp_region: str,\n model_display_name: str,\n bucket_name:\ - \ str,\n gcs_path: str,\n eval_data_path: OutputPath('evals'),\n mlpipeline_ui_metadata_path:\ - \ OutputPath('UI_metadata'),\n api_endpoint: str = None,\n\n) -> NamedTuple('Outputs',\ - \ [\n # ('evals_gcs_path', str),\n ('feat_list', str)]):\n import subprocess\n\ - \ import sys\n subprocess.run([sys.executable, '-m', 'pip', 'install', 'googleapis-common-protos==1.6.0',\n\ - \ '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'},\ - \ check=True)\n subprocess.run([sys.executable, '-m', 'pip', 'install', 'google-cloud-automl==0.9.0',\n\ - \ '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'},\ - \ check=True)\n subprocess.run([sys.executable, '-m', 'pip', 'install',\n \ - \ 'matplotlib', 'pathlib2', 'google-cloud-storage',\n '--no-warn-script-location'],\ - \ env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True)\n\n import google\n\ - \ import json\n import logging\n import pickle\n import pathlib2\n\n from\ - \ google.api_core.client_options import ClientOptions\n from google.api_core\ - \ import exceptions\n from google.cloud import automl_v1beta1 as automl\n \ - \ from google.cloud.automl_v1beta1 import enums\n from google.cloud import\ - \ storage\n\n def upload_blob(bucket_name, source_file_name, destination_blob_name,\n\ - \ public_url=False):\n \"\"\"Uploads a file to the bucket.\"\"\"\n\n\ - \ storage_client = storage.Client()\n bucket = storage_client.bucket(bucket_name)\n\ - \ blob = bucket.blob(destination_blob_name)\n\n blob.upload_from_filename(source_file_name)\n\ - \n logging.info(\"File {} uploaded to {}.\".format(\n source_file_name,\ - \ destination_blob_name))\n if public_url:\n blob.make_public()\n \ - \ logging.info(\"Blob {} is publicly accessible at {}\".format(\n \ - \ blob.name, blob.public_url))\n return blob.public_url\n\n def get_model_details(client,\ - \ model_display_name):\n try:\n model = client.get_model(model_display_name=model_display_name)\n\ - \ except exceptions.NotFound:\n logging.info(\"Model %s not found.\"\ - \ % model_display_name)\n return (None, None)\n\n model = client.get_model(model_display_name=model_display_name)\n\ - \ # Retrieve deployment state.\n if model.deployment_state == enums.Model.DeploymentState.DEPLOYED:\n\ - \ deployment_state = \"deployed\"\n else:\n deployment_state\ - \ = \"undeployed\"\n # get features of top global importance\n feat_list\ - \ = [\n (column.feature_importance, column.column_display_name)\n \ - \ for column in model.tables_model_metadata.tables_model_column_info\n \ - \ ]\n feat_list.sort(reverse=True)\n if len(feat_list) < 10:\n \ - \ feat_to_show = len(feat_list)\n else:\n feat_to_show = 10\n\n\ - \ # Display the model information.\n # TODO: skip this?\n logging.info(\"\ - Model name: {}\".format(model.name))\n logging.info(\"Model id: {}\".format(model.name.split(\"\ - /\")[-1]))\n logging.info(\"Model display name: {}\".format(model.display_name))\n\ - \ logging.info(\"Features of top importance:\")\n for feat in feat_list[:feat_to_show]:\n\ - \ logging.info(feat)\n logging.info(\"Model create time:\")\n logging.info(\"\ - \\tseconds: {}\".format(model.create_time.seconds))\n logging.info(\"\\tnanos:\ - \ {}\".format(model.create_time.nanos))\n logging.info(\"Model deployment\ - \ state: {}\".format(deployment_state))\n\n generate_fi_ui(feat_list)\n \ - \ return (model, feat_list)\n\n def generate_fi_ui(feat_list):\n import\ - \ matplotlib.pyplot as plt\n\n image_suffix = '{}/gfi.png'.format(gcs_path)\n\ - \ res = list(zip(*feat_list))\n x = list(res[0])\n y = list(res[1])\n\ - \ y_pos = list(range(len(y)))\n plt.figure(figsize=(10, 6))\n plt.barh(y_pos,\ - \ x, alpha=0.5)\n plt.yticks(y_pos, y)\n plt.savefig('/gfi.png')\n \ - \ public_url = upload_blob(bucket_name, '/gfi.png', image_suffix, public_url=True)\n\ - \ logging.info('using image url {}'.format(public_url))\n\n html_suffix\ - \ = '{}/gfi.html'.format(gcs_path)\n with open('/gfi.html', 'w') as f:\n\ - \ f.write('

Global Feature Importance

\\\ - n'.format(public_url))\n upload_blob(bucket_name,\ - \ '/gfi.html', html_suffix)\n html_source = 'gs://{}/{}'.format(bucket_name,\ - \ html_suffix)\n logging.info('metadata html source: {}'.format(html_source))\n\ - \n metadata = {\n 'outputs' : [\n {\n 'type': 'web-app',\n\ - \ 'storage': 'gcs',\n 'source': html_source\n }]}\n logging.info('using\ - \ metadata dict {}'.format(json.dumps(metadata)))\n # with open('/mlpipeline-ui-metadata.json',\ - \ 'w') as f:\n # json.dump(metadata, f)\n logging.info('using metadata\ - \ ui path: {}'.format(mlpipeline_ui_metadata_path))\n with open(mlpipeline_ui_metadata_path,\ - \ 'w') as mlpipeline_ui_metadata_file:\n mlpipeline_ui_metadata_file.write(json.dumps(metadata))\n\ - \n logging.getLogger().setLevel(logging.INFO) # TODO: make level configurable\n\ - \ # TODO: we could instead check for region 'eu' and use 'eu-automl.googleapis.com:443'endpoint\n\ - \ # in that case, instead of requiring endpoint to be specified.\n if api_endpoint:\n\ - \ client_options = ClientOptions(api_endpoint=api_endpoint)\n client =\ - \ automl.TablesClient(project=gcp_project_id, region=gcp_region,\n client_options=client_options)\n\ - \ else:\n client = automl.TablesClient(project=gcp_project_id, region=gcp_region)\n\ - \n (model, feat_list) = get_model_details(client, model_display_name)\n\n \ - \ evals = list(client.list_model_evaluations(model_display_name=model_display_name))\n\ - \ with open('temp_oput_regression', \"w\") as f:\n f.write('Model evals:\\\ - n{}'.format(evals))\n pstring = pickle.dumps(evals)\n\n # write to eval_data_path\n\ - \ if eval_data_path:\n logging.info(\"eval_data_path: %s\", eval_data_path)\n\ - \ try:\n pathlib2.Path(eval_data_path).parent.mkdir(parents=True)\n\ - \ except FileExistsError:\n pass\n pathlib2.Path(eval_data_path).write_bytes(pstring)\n\ - \n feat_list_string = json.dumps(feat_list)\n # return(gcs_path, feat_list_string)\n\ - \ return(feat_list_string)\n\ndef _serialize_str(str_value: str) -> str:\n\ - \ if not isinstance(str_value, str):\n raise TypeError('Value \"{}\"\ - \ has type \"{}\" instead of str.'.format(str(str_value), str(type(str_value))))\n\ - \ return str_value\n\nimport argparse\n_parser = argparse.ArgumentParser(prog='Automl\ - \ eval tables model', description='')\n_parser.add_argument(\"--gcp-project-id\"\ - , dest=\"gcp_project_id\", type=str, required=True, default=argparse.SUPPRESS)\n\ - _parser.add_argument(\"--gcp-region\", dest=\"gcp_region\", type=str, required=True,\ - \ default=argparse.SUPPRESS)\n_parser.add_argument(\"--model-display-name\"\ - , dest=\"model_display_name\", type=str, required=True, default=argparse.SUPPRESS)\n\ - _parser.add_argument(\"--bucket-name\", dest=\"bucket_name\", type=str, required=True,\ - \ default=argparse.SUPPRESS)\n_parser.add_argument(\"--gcs-path\", dest=\"gcs_path\"\ - , type=str, required=True, default=argparse.SUPPRESS)\n_parser.add_argument(\"\ - --api-endpoint\", dest=\"api_endpoint\", type=str, required=False, default=argparse.SUPPRESS)\n\ - _parser.add_argument(\"--eval-data\", dest=\"eval_data_path\", type=_make_parent_dirs_and_return_path,\ - \ required=True, default=argparse.SUPPRESS)\n_parser.add_argument(\"--mlpipeline-ui-metadata\"\ - , dest=\"mlpipeline_ui_metadata_path\", type=_make_parent_dirs_and_return_path,\ - \ required=True, default=argparse.SUPPRESS)\n_parser.add_argument(\"----output-paths\"\ - , dest=\"_output_paths\", type=str, nargs=1)\n_parsed_args = vars(_parser.parse_args())\n\ - _output_files = _parsed_args.pop(\"_output_paths\", [])\n\n_outputs = automl_eval_tables_model(**_parsed_args)\n\ - \nif not hasattr(_outputs, '__getitem__') or isinstance(_outputs, str):\n \ - \ _outputs = [_outputs]\n\n_output_serializers = [\n _serialize_str,\n\n\ - ]\n\nimport os\nfor idx, output_file in enumerate(_output_files):\n try:\n\ - \ os.makedirs(os.path.dirname(output_file))\n except OSError:\n \ - \ pass\n with open(output_file, 'w') as f:\n f.write(_output_serializers[idx](_outputs[idx]))\n" + - | + def _make_parent_dirs_and_return_path(file_path: str): + import os + os.makedirs(os.path.dirname(file_path), exist_ok=True) + return file_path + + class OutputPath: + '''When creating component from function, OutputPath should be used as function parameter annotation to tell the system that the function wants to output data by writing it into a file with the given path instead of returning the data from the function.''' + def __init__(self, type=None): + self.type = type + + from typing import NamedTuple + + def automl_eval_tables_model( + gcp_project_id: str, + gcp_region: str, + model_display_name: str, + bucket_name: str, + gcs_path: str, + eval_data_path: OutputPath('evals'), + mlpipeline_ui_metadata_path: OutputPath('UI_metadata'), + api_endpoint: str = None, + + ) -> NamedTuple('Outputs', [ + ('feat_list', str)]): + import subprocess + import sys + # we could build a base image that includes these libraries if we don't want to do + # the dynamic installation when the step runs. + subprocess.run([sys.executable, '-m', 'pip', 'install', 'googleapis-common-protos==1.6.0', + '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) + subprocess.run([sys.executable, '-m', 'pip', 'install', 'google-cloud-automl==0.9.0', + '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) + subprocess.run([sys.executable, '-m', 'pip', 'install', + 'matplotlib', 'pathlib2', 'google-cloud-storage', + '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) + + import google + import json + import logging + import pickle + import pathlib2 + + from google.api_core.client_options import ClientOptions + from google.api_core import exceptions + from google.cloud import automl_v1beta1 as automl + from google.cloud.automl_v1beta1 import enums + from google.cloud import storage + + def upload_blob(bucket_name, source_file_name, destination_blob_name, + public_url=False): + """Uploads a file to the bucket.""" + + storage_client = storage.Client() + bucket = storage_client.bucket(bucket_name) + blob = bucket.blob(destination_blob_name) + + blob.upload_from_filename(source_file_name) + + logging.info("File {} uploaded to {}.".format( + source_file_name, destination_blob_name)) + if public_url: + blob.make_public() + logging.info("Blob {} is publicly accessible at {}".format( + blob.name, blob.public_url)) + return blob.public_url + + def get_model_details(client, model_display_name): + try: + model = client.get_model(model_display_name=model_display_name) + except exceptions.NotFound: + logging.info("Model %s not found." % model_display_name) + return (None, None) + + model = client.get_model(model_display_name=model_display_name) + # Retrieve deployment state. + if model.deployment_state == enums.Model.DeploymentState.DEPLOYED: + deployment_state = "deployed" + else: + deployment_state = "undeployed" + # get features of top global importance + feat_list = [ + (column.feature_importance, column.column_display_name) + for column in model.tables_model_metadata.tables_model_column_info + ] + feat_list.sort(reverse=True) + if len(feat_list) < 10: + feat_to_show = len(feat_list) + else: + feat_to_show = 10 + + # Log some information about the model + logging.info("Model name: {}".format(model.name)) + logging.info("Model id: {}".format(model.name.split("/")[-1])) + logging.info("Model display name: {}".format(model.display_name)) + logging.info("Features of top importance:") + for feat in feat_list[:feat_to_show]: + logging.info(feat) + logging.info("Model create time:") + logging.info("\tseconds: {}".format(model.create_time.seconds)) + logging.info("\tnanos: {}".format(model.create_time.nanos)) + logging.info("Model deployment state: {}".format(deployment_state)) + + generate_fi_ui(feat_list) + return (model, feat_list) + + def generate_fi_ui(feat_list): + import matplotlib.pyplot as plt + + image_suffix = '{}/gfi.png'.format(gcs_path) + res = list(zip(*feat_list)) + x = list(res[0]) + y = list(res[1]) + y_pos = list(range(len(y))) + plt.figure(figsize=(10, 6)) + plt.barh(y_pos, x, alpha=0.5) + plt.yticks(y_pos, y) + plt.savefig('/gfi.png') + public_url = upload_blob(bucket_name, '/gfi.png', image_suffix, public_url=True) + logging.info('using image url {}'.format(public_url)) + + html_suffix = '{}/gfi.html'.format(gcs_path) + with open('/gfi.html', 'w') as f: + f.write('

Global Feature Importance

\n'.format(public_url)) + upload_blob(bucket_name, '/gfi.html', html_suffix) + html_source = 'gs://{}/{}'.format(bucket_name, html_suffix) + logging.info('metadata html source: {}'.format(html_source)) + + metadata = { + 'outputs' : [ + { + 'type': 'web-app', + 'storage': 'gcs', + 'source': html_source + }]} + logging.info('using metadata dict {}'.format(json.dumps(metadata))) + logging.info('using metadata ui path: {}'.format(mlpipeline_ui_metadata_path)) + with open(mlpipeline_ui_metadata_path, 'w') as mlpipeline_ui_metadata_file: + mlpipeline_ui_metadata_file.write(json.dumps(metadata)) + + logging.getLogger().setLevel(logging.INFO) # TODO: make level configurable + # TODO: we could instead check for region 'eu' and use 'eu-automl.googleapis.com:443'endpoint + # in that case, instead of requiring endpoint to be specified. + if api_endpoint: + client_options = ClientOptions(api_endpoint=api_endpoint) + client = automl.TablesClient(project=gcp_project_id, region=gcp_region, + client_options=client_options) + else: + client = automl.TablesClient(project=gcp_project_id, region=gcp_region) + + (model, feat_list) = get_model_details(client, model_display_name) + + evals = list(client.list_model_evaluations(model_display_name=model_display_name)) + with open('temp_oput_regression', "w") as f: + f.write('Model evals:\n{}'.format(evals)) + pstring = pickle.dumps(evals) + + # write to eval_data_path + if eval_data_path: + logging.info("eval_data_path: %s", eval_data_path) + try: + pathlib2.Path(eval_data_path).parent.mkdir(parents=True) + except FileExistsError: + pass + pathlib2.Path(eval_data_path).write_bytes(pstring) + + feat_list_string = json.dumps(feat_list) + return feat_list_string + + def _serialize_str(str_value: str) -> str: + if not isinstance(str_value, str): + raise TypeError('Value "{}" has type "{}" instead of str.'.format(str(str_value), str(type(str_value)))) + return str_value + + import argparse + _parser = argparse.ArgumentParser(prog='Automl eval tables model', description='') + _parser.add_argument("--gcp-project-id", dest="gcp_project_id", type=str, required=True, default=argparse.SUPPRESS) + _parser.add_argument("--gcp-region", dest="gcp_region", type=str, required=True, default=argparse.SUPPRESS) + _parser.add_argument("--model-display-name", dest="model_display_name", type=str, required=True, default=argparse.SUPPRESS) + _parser.add_argument("--bucket-name", dest="bucket_name", type=str, required=True, default=argparse.SUPPRESS) + _parser.add_argument("--gcs-path", dest="gcs_path", type=str, required=True, default=argparse.SUPPRESS) + _parser.add_argument("--api-endpoint", dest="api_endpoint", type=str, required=False, default=argparse.SUPPRESS) + _parser.add_argument("--eval-data", dest="eval_data_path", type=_make_parent_dirs_and_return_path, required=True, default=argparse.SUPPRESS) + _parser.add_argument("--mlpipeline-ui-metadata", dest="mlpipeline_ui_metadata_path", type=_make_parent_dirs_and_return_path, required=True, default=argparse.SUPPRESS) + _parser.add_argument("----output-paths", dest="_output_paths", type=str, nargs=1) + _parsed_args = vars(_parser.parse_args()) + _output_files = _parsed_args.pop("_output_paths", []) + + _outputs = automl_eval_tables_model(**_parsed_args) + + if not hasattr(_outputs, '__getitem__') or isinstance(_outputs, str): + _outputs = [_outputs] + + _output_serializers = [ + _serialize_str, + + ] + + import os + for idx, output_file in enumerate(_output_files): + try: + os.makedirs(os.path.dirname(output_file)) + except OSError: + pass + with open(output_file, 'w') as f: + f.write(_output_serializers[idx](_outputs[idx])) args: - --gcp-project-id - inputValue: gcp_project_id diff --git a/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_metrics_component.py b/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_metrics_component.py index ea405cd..d220b85 100644 --- a/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_metrics_component.py +++ b/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_metrics_component.py @@ -19,15 +19,13 @@ # An example of how the model eval info could be used to make decisions aboiut whether or not # to deploy the model. def automl_eval_metrics( - gcp_project_id: str, - gcp_region: str, - model_display_name: str, - bucket_name: str, - # gcs_path: str, + # gcp_project_id: str, + # gcp_region: str, + # model_display_name: str, eval_data_path: InputPath('evals'), mlpipeline_ui_metadata_path: OutputPath('UI_metadata'), mlpipeline_metrics_path: OutputPath('UI_metrics'), - api_endpoint: str = None, + # api_endpoint: str = None, # thresholds: str = '{"au_prc": 0.9}', thresholds: str = '{"mean_absolute_error": 450}', confidence_threshold: float = 0.5 # for classification @@ -35,33 +33,32 @@ def automl_eval_metrics( ) -> NamedTuple('Outputs', [('deploy', bool)]): import subprocess import sys - subprocess.run([sys.executable, '-m', 'pip', 'install', 'googleapis-common-protos==1.6.0', - '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) - subprocess.run([sys.executable, '-m', 'pip', 'install', 'google-cloud-automl==0.9.0', - 'google-cloud-storage', - '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) - - - import google + # we could build a base image that includes these libraries if we don't want to do + # the dynamic installation when the step runs. + # subprocess.run([sys.executable, '-m', 'pip', 'install', 'googleapis-common-protos==1.6.0', + # '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) + # subprocess.run([sys.executable, '-m', 'pip', 'install', 'google-cloud-automl==0.9.0', + # 'google-cloud-storage', + # '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) + + # import google import json import logging import pickle - from google.api_core.client_options import ClientOptions - from google.api_core import exceptions - from google.cloud import automl_v1beta1 as automl - from google.cloud.automl_v1beta1 import enums - from google.cloud import storage - + # from google.api_core.client_options import ClientOptions + # from google.api_core import exceptions + # from google.cloud import automl_v1beta1 as automl + # from google.cloud import storage logging.getLogger().setLevel(logging.INFO) # TODO: make level configurable # TODO: we could instead check for region 'eu' and use 'eu-automl.googleapis.com:443'endpoint # in that case, instead of requiring endpoint to be specified. - if api_endpoint: - client_options = ClientOptions(api_endpoint=api_endpoint) - client = automl.TablesClient(project=gcp_project_id, region=gcp_region, - client_options=client_options) - else: - client = automl.TablesClient(project=gcp_project_id, region=gcp_region) + # if api_endpoint: + # client_options = ClientOptions(api_endpoint=api_endpoint) + # client = automl.TablesClient(project=gcp_project_id, region=gcp_region, + # client_options=client_options) + # else: + # client = automl.TablesClient(project=gcp_project_id, region=gcp_region) thresholds_dict = json.loads(thresholds) logging.info('thresholds dict: {}'.format(thresholds_dict)) @@ -75,7 +72,7 @@ def regression_threshold_check(eval_info): eresults['r_squared'] = rmetrics.r_squared eresults['mean_absolute_percentage_error'] = rmetrics.mean_absolute_percentage_error eresults['root_mean_squared_log_error'] = rmetrics.root_mean_squared_log_error - for k,v in thresholds_dict.items(): + for k, v in thresholds_dict.items(): logging.info('k {}, v {}'.format(k, v)) if k in ['root_mean_squared_error', 'mean_absolute_error', 'mean_absolute_percentage_error']: if eresults[k] > v: @@ -105,7 +102,7 @@ def classif_threshold_check(eval_info): break break logging.info('eresults: {}'.format(eresults)) - for k,v in thresholds_dict.items(): + for k, v in thresholds_dict.items(): logging.info('k {}, v {}'.format(k, v)) if k == 'log_loss': if eresults[k] > v: @@ -119,8 +116,6 @@ def classif_threshold_check(eval_info): return (False, eresults) return (True, eresults) - - # testing... with open(eval_data_path, 'rb') as f: logging.info('successfully opened eval_data_path {}'.format(eval_data_path)) try: @@ -131,10 +126,12 @@ def classif_threshold_check(eval_info): # TODO: what's the right way to figure out the model type? if eval_info[1].regression_evaluation_metrics and eval_info[1].regression_evaluation_metrics.root_mean_squared_error: regression=True - logging.info('found regression metrics {}'.format(eval_info[1].regression_evaluation_metrics)) + logging.info('found regression metrics {}'.format( + eval_info[1].regression_evaluation_metrics)) elif eval_info[1].classification_evaluation_metrics and eval_info[1].classification_evaluation_metrics.au_prc: classif = True - logging.info('found classification metrics {}'.format(eval_info[1].classification_evaluation_metrics)) + logging.info('found classification metrics {}'.format( + eval_info[1].classification_evaluation_metrics)) if regression and thresholds_dict: res, eresults = regression_threshold_check(eval_info) @@ -153,8 +150,6 @@ def classif_threshold_check(eval_info): 'format': "RAW", }] } - # TODO: is it possible to get confusion matrix info via the API, for the binary - # classifcation case? doesn't seem to be. logging.info('using metadata dict {}'.format(json.dumps(metadata))) logging.info('using metadata ui path: {}'.format(mlpipeline_ui_metadata_path)) with open(mlpipeline_ui_metadata_path, 'w') as mlpipeline_ui_metadata_file: @@ -162,13 +157,10 @@ def classif_threshold_check(eval_info): logging.info('using metrics path: {}'.format(mlpipeline_metrics_path)) with open(mlpipeline_metrics_path, 'w') as mlpipeline_metrics_file: mlpipeline_metrics_file.write(json.dumps(metrics)) - # temp test this variant as well - with open('/mlpipeline-metrics.json', 'w') as f: - json.dump(metrics, f) logging.info('deploy flag: {}'.format(res)) return res - elif classif and thresholds_dict: + if classif and thresholds_dict: res, eresults = classif_threshold_check(eval_info) # logging.info('eresults: {}'.format(eresults)) metadata = { @@ -179,15 +171,13 @@ def classif_threshold_check(eval_info): confidence_threshold, eresults), 'type': 'markdown', }]} - # TODO: generate 'metrics' dict logging.info('using metadata dict {}'.format(json.dumps(metadata))) logging.info('using metadata ui path: {}'.format(mlpipeline_ui_metadata_path)) with open(mlpipeline_ui_metadata_path, 'w') as mlpipeline_ui_metadata_file: mlpipeline_ui_metadata_file.write(json.dumps(metadata)) logging.info('deploy flag: {}'.format(res)) return res - else: - return True + return True except Exception as e: logging.warning(e) # If can't reconstruct the eval, or don't have thresholds defined, @@ -196,9 +186,7 @@ def classif_threshold_check(eval_info): return True - - if __name__ == '__main__': - import kfp - kfp.components.func_to_container_op(automl_eval_metrics, + import kfp + kfp.components.func_to_container_op(automl_eval_metrics, output_component_file='tables_eval_metrics_component.yaml', base_image='python:3.7') diff --git a/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_metrics_component.yaml b/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_metrics_component.yaml index 9b85bd2..c69d0eb 100644 --- a/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_metrics_component.yaml +++ b/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_metrics_component.yaml @@ -1,18 +1,7 @@ name: Automl eval metrics inputs: -- name: gcp_project_id - type: String -- name: gcp_region - type: String -- name: model_display_name - type: String -- name: bucket_name - type: String - name: eval_data type: evals -- name: api_endpoint - type: String - optional: true - name: thresholds type: String default: '{"mean_absolute_error": 450}' @@ -35,156 +24,230 @@ implementation: - python3 - -u - -c - - "class InputPath:\n '''When creating component from function, InputPath should\ - \ be used as function parameter annotation to tell the system to pass the *data\ - \ file path* to the function instead of passing the actual data.'''\n def\ - \ __init__(self, type=None):\n self.type = type\n\ndef _make_parent_dirs_and_return_path(file_path:\ - \ str):\n import os\n os.makedirs(os.path.dirname(file_path), exist_ok=True)\n\ - \ return file_path\n\nclass OutputPath:\n '''When creating component from\ - \ function, OutputPath should be used as function parameter annotation to tell\ - \ the system that the function wants to output data by writing it into a file\ - \ with the given path instead of returning the data from the function.'''\n\ - \ def __init__(self, type=None):\n self.type = type\n\nfrom typing\ - \ import NamedTuple\n\ndef automl_eval_metrics(\n\tgcp_project_id: str,\n\t\ - gcp_region: str,\n model_display_name: str,\n bucket_name: str,\n # gcs_path:\ - \ str,\n eval_data_path: InputPath('evals'),\n mlpipeline_ui_metadata_path:\ - \ OutputPath('UI_metadata'),\n mlpipeline_metrics_path: OutputPath('UI_metrics'),\n\ - \ api_endpoint: str = None,\n # thresholds: str = '{\"au_prc\": 0.9}',\n \ - \ thresholds: str = '{\"mean_absolute_error\": 450}',\n confidence_threshold:\ - \ float = 0.5 # for classification\n\n) -> NamedTuple('Outputs', [('deploy',\ - \ bool)]):\n import subprocess\n import sys\n subprocess.run([sys.executable,\ - \ '-m', 'pip', 'install', 'googleapis-common-protos==1.6.0',\n '--no-warn-script-location'],\ - \ env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True)\n subprocess.run([sys.executable,\ - \ '-m', 'pip', 'install', 'google-cloud-automl==0.9.0',\n 'google-cloud-storage',\n\ - \ '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'},\ - \ check=True)\n\n import google\n import json\n import logging\n import\ - \ pickle\n from google.api_core.client_options import ClientOptions\n from\ - \ google.api_core import exceptions\n from google.cloud import automl_v1beta1\ - \ as automl\n from google.cloud.automl_v1beta1 import enums\n from google.cloud\ - \ import storage\n\n logging.getLogger().setLevel(logging.INFO) # TODO: make\ - \ level configurable\n # TODO: we could instead check for region 'eu' and use\ - \ 'eu-automl.googleapis.com:443'endpoint\n # in that case, instead of requiring\ - \ endpoint to be specified.\n if api_endpoint:\n client_options = ClientOptions(api_endpoint=api_endpoint)\n\ - \ client = automl.TablesClient(project=gcp_project_id, region=gcp_region,\n\ - \ client_options=client_options)\n else:\n client = automl.TablesClient(project=gcp_project_id,\ - \ region=gcp_region)\n\n thresholds_dict = json.loads(thresholds)\n logging.info('thresholds\ - \ dict: {}'.format(thresholds_dict))\n\n def regression_threshold_check(eval_info):\n\ - \ eresults = {}\n rmetrics = eval_info[1].regression_evaluation_metrics\n\ - \ logging.info('got regression eval {}'.format(eval_info[1]))\n eresults['root_mean_squared_error']\ - \ = rmetrics.root_mean_squared_error\n eresults['mean_absolute_error'] =\ - \ rmetrics.mean_absolute_error\n eresults['r_squared'] = rmetrics.r_squared\n\ - \ eresults['mean_absolute_percentage_error'] = rmetrics.mean_absolute_percentage_error\n\ - \ eresults['root_mean_squared_log_error'] = rmetrics.root_mean_squared_log_error\n\ - \ for k,v in thresholds_dict.items():\n logging.info('k {}, v {}'.format(k,\ - \ v))\n if k in ['root_mean_squared_error', 'mean_absolute_error', 'mean_absolute_percentage_error']:\n\ - \ if eresults[k] > v:\n logging.info('{} > {}; returning False'.format(\n\ - \ eresults[k], v))\n return (False, eresults)\n elif\ - \ eresults[k] < v:\n logging.info('{} < {}; returning False'.format(\n\ - \ eresults[k], v))\n return (False, eresults)\n return\ - \ (True, eresults)\n\n def classif_threshold_check(eval_info):\n eresults\ - \ = {}\n example_count = eval_info[0].evaluated_example_count\n print('Looking\ - \ for example_count {}'.format(example_count))\n for e in eval_info[1:]:\ - \ # we know we don't want the first elt\n if e.evaluated_example_count\ - \ == example_count:\n eresults['au_prc'] = e.classification_evaluation_metrics.au_prc\n\ - \ eresults['au_roc'] = e.classification_evaluation_metrics.au_roc\n \ - \ eresults['log_loss'] = e.classification_evaluation_metrics.log_loss\n\ - \ for i in e.classification_evaluation_metrics.confidence_metrics_entry:\n\ - \ if i.confidence_threshold >= confidence_threshold:\n eresults['recall']\ - \ = i.recall\n eresults['precision'] = i.precision\n eresults['f1_score']\ - \ = i.f1_score\n break\n break\n logging.info('eresults:\ - \ {}'.format(eresults))\n for k,v in thresholds_dict.items():\n logging.info('k\ - \ {}, v {}'.format(k, v))\n if k == 'log_loss':\n if eresults[k]\ - \ > v:\n logging.info('{} > {}; returning False'.format(\n \ - \ eresults[k], v))\n return (False, eresults)\n else:\n \ - \ if eresults[k] < v:\n logging.info('{} < {}; returning False'.format(\n\ - \ eresults[k], v))\n return (False, eresults)\n return\ - \ (True, eresults)\n\n # testing...\n with open(eval_data_path, 'rb') as f:\n\ - \ logging.info('successfully opened eval_data_path {}'.format(eval_data_path))\n\ - \ try:\n eval_info = pickle.loads(f.read())\n\n classif = False\n\ - \ regression = False\n # TODO: what's the right way to figure out\ - \ the model type?\n if eval_info[1].regression_evaluation_metrics and eval_info[1].regression_evaluation_metrics.root_mean_squared_error:\n\ - \ regression=True\n logging.info('found regression metrics {}'.format(eval_info[1].regression_evaluation_metrics))\n\ - \ elif eval_info[1].classification_evaluation_metrics and eval_info[1].classification_evaluation_metrics.au_prc:\n\ - \ classif = True\n logging.info('found classification metrics\ - \ {}'.format(eval_info[1].classification_evaluation_metrics))\n\n if regression\ - \ and thresholds_dict:\n res, eresults = regression_threshold_check(eval_info)\n\ - \ # logging.info('eresults: {}'.format(eresults))\n metadata =\ - \ {\n 'outputs' : [\n {\n 'storage': 'inline',\n\ - \ 'source': '# Regression metrics:\\n\\n```{}```\\n'.format(eresults),\n\ - \ 'type': 'markdown',\n }]}\n metrics = {\n \ - \ 'metrics': [{\n 'name': 'MAE',\n 'numberValue':\ - \ eresults['mean_absolute_error'],\n 'format': \"RAW\",\n \ - \ }]\n }\n # TODO: is it possible to get confusion matrix info\ - \ via the API, for the binary\n # classifcation case? doesn't seem to\ - \ be.\n logging.info('using metadata dict {}'.format(json.dumps(metadata)))\n\ - \ logging.info('using metadata ui path: {}'.format(mlpipeline_ui_metadata_path))\n\ - \ with open(mlpipeline_ui_metadata_path, 'w') as mlpipeline_ui_metadata_file:\n\ - \ mlpipeline_ui_metadata_file.write(json.dumps(metadata))\n \ - \ logging.info('using metrics path: {}'.format(mlpipeline_metrics_path))\n\ - \ with open(mlpipeline_metrics_path, 'w') as mlpipeline_metrics_file:\n\ - \ mlpipeline_metrics_file.write(json.dumps(metrics))\n # temp\ - \ test\n with open('/mlpipeline-metrics.json', 'w') as f:\n \ - \ json.dump(metrics, f)\n logging.info('deploy flag: {}'.format(res))\n\ - \ return res\n\n elif classif and thresholds_dict:\n res,\ - \ eresults = classif_threshold_check(eval_info)\n # logging.info('eresults:\ - \ {}'.format(eresults))\n metadata = {\n 'outputs' : [\n \ - \ {\n 'storage': 'inline',\n 'source': '# classification\ - \ metrics for confidence threshold {}:\\n\\n```{}```\\n'.format(\n \ - \ confidence_threshold, eresults),\n 'type': 'markdown',\n\ - \ }]}\n # TODO: generate 'metrics' dict\n logging.info('using\ - \ metadata dict {}'.format(json.dumps(metadata)))\n logging.info('using\ - \ metadata ui path: {}'.format(mlpipeline_ui_metadata_path))\n with open(mlpipeline_ui_metadata_path,\ - \ 'w') as mlpipeline_ui_metadata_file:\n mlpipeline_ui_metadata_file.write(json.dumps(metadata))\n\ - \ logging.info('deploy flag: {}'.format(res))\n return res\n \ - \ else:\n return True\n except Exception as e:\n logging.warning(e)\n\ - \ # If can't reconstruct the eval, or don't have thresholds defined,\n\ - \ # return True as a signal to deploy.\n # TODO: is this the right\ - \ default?\n return True\n\ndef _serialize_bool(bool_value: bool) -> str:\n\ - \ if isinstance(bool_value, str):\n return bool_value\n if not\ - \ isinstance(bool_value, bool):\n raise TypeError('Value \"{}\" has type\ - \ \"{}\" instead of bool.'.format(str(bool_value), str(type(bool_value))))\n\ - \ return str(bool_value)\n\nimport argparse\n_parser = argparse.ArgumentParser(prog='Automl\ - \ eval metrics', description='')\n_parser.add_argument(\"--gcp-project-id\"\ - , dest=\"gcp_project_id\", type=str, required=True, default=argparse.SUPPRESS)\n\ - _parser.add_argument(\"--gcp-region\", dest=\"gcp_region\", type=str, required=True,\ - \ default=argparse.SUPPRESS)\n_parser.add_argument(\"--model-display-name\"\ - , dest=\"model_display_name\", type=str, required=True, default=argparse.SUPPRESS)\n\ - _parser.add_argument(\"--bucket-name\", dest=\"bucket_name\", type=str, required=True,\ - \ default=argparse.SUPPRESS)\n_parser.add_argument(\"--eval-data\", dest=\"\ - eval_data_path\", type=str, required=True, default=argparse.SUPPRESS)\n_parser.add_argument(\"\ - --api-endpoint\", dest=\"api_endpoint\", type=str, required=False, default=argparse.SUPPRESS)\n\ - _parser.add_argument(\"--thresholds\", dest=\"thresholds\", type=str, required=False,\ - \ default=argparse.SUPPRESS)\n_parser.add_argument(\"--confidence-threshold\"\ - , dest=\"confidence_threshold\", type=float, required=False, default=argparse.SUPPRESS)\n\ - _parser.add_argument(\"--mlpipeline-ui-metadata\", dest=\"mlpipeline_ui_metadata_path\"\ - , type=_make_parent_dirs_and_return_path, required=True, default=argparse.SUPPRESS)\n\ - _parser.add_argument(\"--mlpipeline-metrics\", dest=\"mlpipeline_metrics_path\"\ - , type=_make_parent_dirs_and_return_path, required=True, default=argparse.SUPPRESS)\n\ - _parser.add_argument(\"----output-paths\", dest=\"_output_paths\", type=str,\ - \ nargs=1)\n_parsed_args = vars(_parser.parse_args())\n_output_files = _parsed_args.pop(\"\ - _output_paths\", [])\n\n_outputs = automl_eval_metrics(**_parsed_args)\n\nif\ - \ not hasattr(_outputs, '__getitem__') or isinstance(_outputs, str):\n _outputs\ - \ = [_outputs]\n\n_output_serializers = [\n _serialize_bool,\n\n]\n\nimport\ - \ os\nfor idx, output_file in enumerate(_output_files):\n try:\n os.makedirs(os.path.dirname(output_file))\n\ - \ except OSError:\n pass\n with open(output_file, 'w') as f:\n\ - \ f.write(_output_serializers[idx](_outputs[idx]))\n" + - | + class OutputPath: + '''When creating component from function, OutputPath should be used as function parameter annotation to tell the system that the function wants to output data by writing it into a file with the given path instead of returning the data from the function.''' + def __init__(self, type=None): + self.type = type + + def _make_parent_dirs_and_return_path(file_path: str): + import os + os.makedirs(os.path.dirname(file_path), exist_ok=True) + return file_path + + class InputPath: + '''When creating component from function, InputPath should be used as function parameter annotation to tell the system to pass the *data file path* to the function instead of passing the actual data.''' + def __init__(self, type=None): + self.type = type + + from typing import NamedTuple + + def automl_eval_metrics( + # gcp_project_id: str, + # gcp_region: str, + # model_display_name: str, + eval_data_path: InputPath('evals'), + mlpipeline_ui_metadata_path: OutputPath('UI_metadata'), + mlpipeline_metrics_path: OutputPath('UI_metrics'), + # api_endpoint: str = None, + # thresholds: str = '{"au_prc": 0.9}', + thresholds: str = '{"mean_absolute_error": 450}', + confidence_threshold: float = 0.5 # for classification + + ) -> NamedTuple('Outputs', [('deploy', bool)]): + import subprocess + import sys + # we could build a base image that includes these libraries if we don't want to do + # the dynamic installation when the step runs. + # subprocess.run([sys.executable, '-m', 'pip', 'install', 'googleapis-common-protos==1.6.0', + # '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) + # subprocess.run([sys.executable, '-m', 'pip', 'install', 'google-cloud-automl==0.9.0', + # 'google-cloud-storage', + # '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) + + # import google + import json + import logging + import pickle + # from google.api_core.client_options import ClientOptions + # from google.api_core import exceptions + # from google.cloud import automl_v1beta1 as automl + # from google.cloud import storage + + logging.getLogger().setLevel(logging.INFO) # TODO: make level configurable + # TODO: we could instead check for region 'eu' and use 'eu-automl.googleapis.com:443'endpoint + # in that case, instead of requiring endpoint to be specified. + # if api_endpoint: + # client_options = ClientOptions(api_endpoint=api_endpoint) + # client = automl.TablesClient(project=gcp_project_id, region=gcp_region, + # client_options=client_options) + # else: + # client = automl.TablesClient(project=gcp_project_id, region=gcp_region) + + thresholds_dict = json.loads(thresholds) + logging.info('thresholds dict: {}'.format(thresholds_dict)) + + def regression_threshold_check(eval_info): + eresults = {} + rmetrics = eval_info[1].regression_evaluation_metrics + logging.info('got regression eval {}'.format(eval_info[1])) + eresults['root_mean_squared_error'] = rmetrics.root_mean_squared_error + eresults['mean_absolute_error'] = rmetrics.mean_absolute_error + eresults['r_squared'] = rmetrics.r_squared + eresults['mean_absolute_percentage_error'] = rmetrics.mean_absolute_percentage_error + eresults['root_mean_squared_log_error'] = rmetrics.root_mean_squared_log_error + for k, v in thresholds_dict.items(): + logging.info('k {}, v {}'.format(k, v)) + if k in ['root_mean_squared_error', 'mean_absolute_error', 'mean_absolute_percentage_error']: + if eresults[k] > v: + logging.info('{} > {}; returning False'.format( + eresults[k], v)) + return (False, eresults) + elif eresults[k] < v: + logging.info('{} < {}; returning False'.format( + eresults[k], v)) + return (False, eresults) + return (True, eresults) + + def classif_threshold_check(eval_info): + eresults = {} + example_count = eval_info[0].evaluated_example_count + print('Looking for example_count {}'.format(example_count)) + for e in eval_info[1:]: # we know we don't want the first elt + if e.evaluated_example_count == example_count: + eresults['au_prc'] = e.classification_evaluation_metrics.au_prc + eresults['au_roc'] = e.classification_evaluation_metrics.au_roc + eresults['log_loss'] = e.classification_evaluation_metrics.log_loss + for i in e.classification_evaluation_metrics.confidence_metrics_entry: + if i.confidence_threshold >= confidence_threshold: + eresults['recall'] = i.recall + eresults['precision'] = i.precision + eresults['f1_score'] = i.f1_score + break + break + logging.info('eresults: {}'.format(eresults)) + for k, v in thresholds_dict.items(): + logging.info('k {}, v {}'.format(k, v)) + if k == 'log_loss': + if eresults[k] > v: + logging.info('{} > {}; returning False'.format( + eresults[k], v)) + return (False, eresults) + else: + if eresults[k] < v: + logging.info('{} < {}; returning False'.format( + eresults[k], v)) + return (False, eresults) + return (True, eresults) + + with open(eval_data_path, 'rb') as f: + logging.info('successfully opened eval_data_path {}'.format(eval_data_path)) + try: + eval_info = pickle.loads(f.read()) + + classif = False + regression = False + # TODO: what's the right way to figure out the model type? + if eval_info[1].regression_evaluation_metrics and eval_info[1].regression_evaluation_metrics.root_mean_squared_error: + regression=True + logging.info('found regression metrics {}'.format( + eval_info[1].regression_evaluation_metrics)) + elif eval_info[1].classification_evaluation_metrics and eval_info[1].classification_evaluation_metrics.au_prc: + classif = True + logging.info('found classification metrics {}'.format( + eval_info[1].classification_evaluation_metrics)) + + if regression and thresholds_dict: + res, eresults = regression_threshold_check(eval_info) + # logging.info('eresults: {}'.format(eresults)) + metadata = { + 'outputs' : [ + { + 'storage': 'inline', + 'source': '# Regression metrics:\n\n```{}```\n'.format(eresults), + 'type': 'markdown', + }]} + metrics = { + 'metrics': [{ + 'name': 'MAE', + 'numberValue': eresults['mean_absolute_error'], + 'format': "RAW", + }] + } + logging.info('using metadata dict {}'.format(json.dumps(metadata))) + logging.info('using metadata ui path: {}'.format(mlpipeline_ui_metadata_path)) + with open(mlpipeline_ui_metadata_path, 'w') as mlpipeline_ui_metadata_file: + mlpipeline_ui_metadata_file.write(json.dumps(metadata)) + logging.info('using metrics path: {}'.format(mlpipeline_metrics_path)) + with open(mlpipeline_metrics_path, 'w') as mlpipeline_metrics_file: + mlpipeline_metrics_file.write(json.dumps(metrics)) + logging.info('deploy flag: {}'.format(res)) + return res + + if classif and thresholds_dict: + res, eresults = classif_threshold_check(eval_info) + # logging.info('eresults: {}'.format(eresults)) + metadata = { + 'outputs' : [ + { + 'storage': 'inline', + 'source': '# classification metrics for confidence threshold {}:\n\n```{}```\n'.format( + confidence_threshold, eresults), + 'type': 'markdown', + }]} + logging.info('using metadata dict {}'.format(json.dumps(metadata))) + logging.info('using metadata ui path: {}'.format(mlpipeline_ui_metadata_path)) + with open(mlpipeline_ui_metadata_path, 'w') as mlpipeline_ui_metadata_file: + mlpipeline_ui_metadata_file.write(json.dumps(metadata)) + logging.info('deploy flag: {}'.format(res)) + return res + return True + except Exception as e: + logging.warning(e) + # If can't reconstruct the eval, or don't have thresholds defined, + # return True as a signal to deploy. + # TODO: is this the right default? + return True + + def _serialize_bool(bool_value: bool) -> str: + if isinstance(bool_value, str): + return bool_value + if not isinstance(bool_value, bool): + raise TypeError('Value "{}" has type "{}" instead of bool.'.format(str(bool_value), str(type(bool_value)))) + return str(bool_value) + + import argparse + _parser = argparse.ArgumentParser(prog='Automl eval metrics', description='') + _parser.add_argument("--eval-data", dest="eval_data_path", type=str, required=True, default=argparse.SUPPRESS) + _parser.add_argument("--thresholds", dest="thresholds", type=str, required=False, default=argparse.SUPPRESS) + _parser.add_argument("--confidence-threshold", dest="confidence_threshold", type=float, required=False, default=argparse.SUPPRESS) + _parser.add_argument("--mlpipeline-ui-metadata", dest="mlpipeline_ui_metadata_path", type=_make_parent_dirs_and_return_path, required=True, default=argparse.SUPPRESS) + _parser.add_argument("--mlpipeline-metrics", dest="mlpipeline_metrics_path", type=_make_parent_dirs_and_return_path, required=True, default=argparse.SUPPRESS) + _parser.add_argument("----output-paths", dest="_output_paths", type=str, nargs=1) + _parsed_args = vars(_parser.parse_args()) + _output_files = _parsed_args.pop("_output_paths", []) + + _outputs = automl_eval_metrics(**_parsed_args) + + if not hasattr(_outputs, '__getitem__') or isinstance(_outputs, str): + _outputs = [_outputs] + + _output_serializers = [ + _serialize_bool, + + ] + + import os + for idx, output_file in enumerate(_output_files): + try: + os.makedirs(os.path.dirname(output_file)) + except OSError: + pass + with open(output_file, 'w') as f: + f.write(_output_serializers[idx](_outputs[idx])) args: - - --gcp-project-id - - inputValue: gcp_project_id - - --gcp-region - - inputValue: gcp_region - - --model-display-name - - inputValue: model_display_name - - --bucket-name - - inputValue: bucket_name - --eval-data - inputPath: eval_data - - if: - cond: - isPresent: api_endpoint - then: - - --api-endpoint - - inputValue: api_endpoint - if: cond: isPresent: thresholds diff --git a/ml/automl/tables/kfp_e2e/deploy_model_for_tables/convert_oss.py b/ml/automl/tables/kfp_e2e/deploy_model_for_tables/convert_oss.py index 52e5a4d..71d64e4 100644 --- a/ml/automl/tables/kfp_e2e/deploy_model_for_tables/convert_oss.py +++ b/ml/automl/tables/kfp_e2e/deploy_model_for_tables/convert_oss.py @@ -24,7 +24,8 @@ FLAGS = flags.FLAGS flags.DEFINE_string('saved_model', '', 'The location of the saved_model.pb to visualize.') -flags.DEFINE_string('output_dir', '', 'The location for the Tensorboard log to begin visualization from.') +flags.DEFINE_string('output_dir', '', + 'The location for the Tensorboard log to begin visualization from.') def import_to_tensorboard(saved_model, output_dir): """View an imported saved_model.pb as a graph in Tensorboard. diff --git a/ml/automl/tables/kfp_e2e/deploy_model_for_tables/exported_model_deploy.py b/ml/automl/tables/kfp_e2e/deploy_model_for_tables/exported_model_deploy.py index 2106342..e36e4dc 100644 --- a/ml/automl/tables/kfp_e2e/deploy_model_for_tables/exported_model_deploy.py +++ b/ml/automl/tables/kfp_e2e/deploy_model_for_tables/exported_model_deploy.py @@ -29,15 +29,10 @@ def main(): parser.add_argument( '--namespace', default='default') - args = parser.parse_args() NAMESPACE = 'default' - logging.getLogger().setLevel(logging.INFO) - args_dict = vars(args) - - logging.info('Generating training template.') template_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'model_serve_template.yaml') @@ -46,15 +41,13 @@ def main(): logging.info("using model name: {}, image {}, and namespace: {}".format( mname, args.image_name, NAMESPACE)) - with open(template_file, 'r') as f: with open(target_file, "w") as target: data = f.read() changed = data.replace('MODEL_NAME', mname).replace( - 'IMAGE_NAME', args.image_name).replace('NAMESPACE', NAMESPACE) + 'IMAGE_NAME', args.image_name).replace('NAMESPACE', NAMESPACE) target.write(changed) - logging.info('deploying...') subprocess.call(['kubectl', 'create', '-f', '/ml/model_serve.yaml']) diff --git a/ml/automl/tables/kfp_e2e/deploy_model_for_tables/tables_deploy_component.py b/ml/automl/tables/kfp_e2e/deploy_model_for_tables/tables_deploy_component.py index 6acf035..a8ca458 100644 --- a/ml/automl/tables/kfp_e2e/deploy_model_for_tables/tables_deploy_component.py +++ b/ml/automl/tables/kfp_e2e/deploy_model_for_tables/tables_deploy_component.py @@ -15,16 +15,17 @@ from typing import NamedTuple def automl_deploy_tables_model( - gcp_project_id: str, - gcp_region: str, - # dataset_display_name: str, + gcp_project_id: str, + gcp_region: str, model_display_name: str, api_endpoint: str = None, ) -> NamedTuple('Outputs', [('model_display_name', str), ('status', str)]): import subprocess import sys - subprocess.run([sys.executable, '-m', 'pip', 'install', 'googleapis-common-protos==1.6.0', '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) - subprocess.run([sys.executable, '-m', 'pip', 'install', 'google-cloud-automl==0.9.0', '--quiet', '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) + subprocess.run([sys.executable, '-m', 'pip', 'install', 'googleapis-common-protos==1.6.0', '--no-warn-script-location'], + env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) + subprocess.run([sys.executable, '-m', 'pip', 'install', 'google-cloud-automl==0.9.0', '--quiet', '--no-warn-script-location'], + env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) import google import logging @@ -43,7 +44,6 @@ def automl_deploy_tables_model( else: client = automl.TablesClient(project=gcp_project_id, region=gcp_region) - try: model = client.get_model(model_display_name=model_display_name) if model.deployment_state == enums.Model.DeploymentState.DEPLOYED: @@ -68,7 +68,7 @@ def automl_deploy_tables_model( if __name__ == '__main__': - import kfp - kfp.components.func_to_container_op( + import kfp + kfp.components.func_to_container_op( automl_deploy_tables_model, output_component_file='tables_deploy_component.yaml', base_image='python:3.7') diff --git a/ml/automl/tables/kfp_e2e/deploy_model_for_tables/tables_deploy_component.yaml b/ml/automl/tables/kfp_e2e/deploy_model_for_tables/tables_deploy_component.yaml index db6d2a9..93dbcfe 100644 --- a/ml/automl/tables/kfp_e2e/deploy_model_for_tables/tables_deploy_component.yaml +++ b/ml/automl/tables/kfp_e2e/deploy_model_for_tables/tables_deploy_component.yaml @@ -21,50 +21,94 @@ implementation: - python3 - -u - -c - - "from typing import NamedTuple\n\ndef automl_deploy_tables_model(\n\tgcp_project_id:\ - \ str,\n\tgcp_region: str,\n\t# dataset_display_name: str,\n model_display_name:\ - \ str,\n api_endpoint: str = None,\n) -> NamedTuple('Outputs', [('model_display_name',\ - \ str), ('status', str)]):\n import subprocess\n import sys\n subprocess.run([sys.executable,\ - \ '-m', 'pip', 'install', 'googleapis-common-protos==1.6.0', '--no-warn-script-location'],\ - \ env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True)\n subprocess.run([sys.executable,\ - \ '-m', 'pip', 'install', 'google-cloud-automl==0.9.0', '--quiet', '--no-warn-script-location'],\ - \ env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True)\n\n import google\n\ - \ import logging\n from google.api_core.client_options import ClientOptions\n\ - \ from google.api_core import exceptions\n from google.cloud import automl_v1beta1\ - \ as automl\n from google.cloud.automl_v1beta1 import enums\n\n logging.getLogger().setLevel(logging.INFO)\ - \ # TODO: make level configurable\n # TODO: we could instead check for region\ - \ 'eu' and use 'eu-automl.googleapis.com:443'endpoint\n # in that case, instead\ - \ of requiring endpoint to be specified.\n if api_endpoint:\n client_options\ - \ = ClientOptions(api_endpoint=api_endpoint)\n client = automl.TablesClient(project=gcp_project_id,\ - \ region=gcp_region,\n client_options=client_options)\n else:\n client\ - \ = automl.TablesClient(project=gcp_project_id, region=gcp_region)\n\n try:\n\ - \ model = client.get_model(model_display_name=model_display_name)\n if\ - \ model.deployment_state == enums.Model.DeploymentState.DEPLOYED:\n status\ - \ = 'deployed'\n logging.info('Model {} already deployed'.format(model_display_name))\n\ - \ else:\n logging.info('Deploying model {}'.format(model_display_name))\n\ - \ response = client.deploy_model(model_display_name=model_display_name)\n\ - \ # synchronous wait\n logging.info(\"Model deployed. {}\".format(response.result()))\n\ - \ status = 'deployed'\n except exceptions.NotFound as e:\n logging.warning(e)\n\ - \ status = 'not_found'\n except Exception as e:\n logging.warning(e)\n\ - \ status = 'undeployed'\n\n logging.info('Model status: {}'.format(status))\n\ - \ return (model_display_name, status)\n\ndef _serialize_str(str_value: str)\ - \ -> str:\n if not isinstance(str_value, str):\n raise TypeError('Value\ - \ \"{}\" has type \"{}\" instead of str.'.format(str(str_value), str(type(str_value))))\n\ - \ return str_value\n\nimport argparse\n_parser = argparse.ArgumentParser(prog='Automl\ - \ deploy tables model', description='')\n_parser.add_argument(\"--gcp-project-id\"\ - , dest=\"gcp_project_id\", type=str, required=True, default=argparse.SUPPRESS)\n\ - _parser.add_argument(\"--gcp-region\", dest=\"gcp_region\", type=str, required=True,\ - \ default=argparse.SUPPRESS)\n_parser.add_argument(\"--model-display-name\"\ - , dest=\"model_display_name\", type=str, required=True, default=argparse.SUPPRESS)\n\ - _parser.add_argument(\"--api-endpoint\", dest=\"api_endpoint\", type=str, required=False,\ - \ default=argparse.SUPPRESS)\n_parser.add_argument(\"----output-paths\", dest=\"\ - _output_paths\", type=str, nargs=2)\n_parsed_args = vars(_parser.parse_args())\n\ - _output_files = _parsed_args.pop(\"_output_paths\", [])\n\n_outputs = automl_deploy_tables_model(**_parsed_args)\n\ - \nif not hasattr(_outputs, '__getitem__') or isinstance(_outputs, str):\n \ - \ _outputs = [_outputs]\n\n_output_serializers = [\n _serialize_str,\n \ - \ _serialize_str,\n\n]\n\nimport os\nfor idx, output_file in enumerate(_output_files):\n\ - \ try:\n os.makedirs(os.path.dirname(output_file))\n except OSError:\n\ - \ pass\n with open(output_file, 'w') as f:\n f.write(_output_serializers[idx](_outputs[idx]))\n" + - | + from typing import NamedTuple + + def automl_deploy_tables_model( + gcp_project_id: str, + gcp_region: str, + model_display_name: str, + api_endpoint: str = None, + ) -> NamedTuple('Outputs', [('model_display_name', str), ('status', str)]): + import subprocess + import sys + subprocess.run([sys.executable, '-m', 'pip', 'install', 'googleapis-common-protos==1.6.0', '--no-warn-script-location'], + env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) + subprocess.run([sys.executable, '-m', 'pip', 'install', 'google-cloud-automl==0.9.0', '--quiet', '--no-warn-script-location'], + env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) + + import google + import logging + from google.api_core.client_options import ClientOptions + from google.api_core import exceptions + from google.cloud import automl_v1beta1 as automl + from google.cloud.automl_v1beta1 import enums + + logging.getLogger().setLevel(logging.INFO) # TODO: make level configurable + # TODO: we could instead check for region 'eu' and use 'eu-automl.googleapis.com:443'endpoint + # in that case, instead of requiring endpoint to be specified. + if api_endpoint: + client_options = ClientOptions(api_endpoint=api_endpoint) + client = automl.TablesClient(project=gcp_project_id, region=gcp_region, + client_options=client_options) + else: + client = automl.TablesClient(project=gcp_project_id, region=gcp_region) + + try: + model = client.get_model(model_display_name=model_display_name) + if model.deployment_state == enums.Model.DeploymentState.DEPLOYED: + status = 'deployed' + logging.info('Model {} already deployed'.format(model_display_name)) + else: + logging.info('Deploying model {}'.format(model_display_name)) + response = client.deploy_model(model_display_name=model_display_name) + # synchronous wait + logging.info("Model deployed. {}".format(response.result())) + status = 'deployed' + except exceptions.NotFound as e: + logging.warning(e) + status = 'not_found' + except Exception as e: + logging.warning(e) + status = 'undeployed' + + logging.info('Model status: {}'.format(status)) + return (model_display_name, status) + + def _serialize_str(str_value: str) -> str: + if not isinstance(str_value, str): + raise TypeError('Value "{}" has type "{}" instead of str.'.format(str(str_value), str(type(str_value)))) + return str_value + + import argparse + _parser = argparse.ArgumentParser(prog='Automl deploy tables model', description='') + _parser.add_argument("--gcp-project-id", dest="gcp_project_id", type=str, required=True, default=argparse.SUPPRESS) + _parser.add_argument("--gcp-region", dest="gcp_region", type=str, required=True, default=argparse.SUPPRESS) + _parser.add_argument("--model-display-name", dest="model_display_name", type=str, required=True, default=argparse.SUPPRESS) + _parser.add_argument("--api-endpoint", dest="api_endpoint", type=str, required=False, default=argparse.SUPPRESS) + _parser.add_argument("----output-paths", dest="_output_paths", type=str, nargs=2) + _parsed_args = vars(_parser.parse_args()) + _output_files = _parsed_args.pop("_output_paths", []) + + _outputs = automl_deploy_tables_model(**_parsed_args) + + if not hasattr(_outputs, '__getitem__') or isinstance(_outputs, str): + _outputs = [_outputs] + + _output_serializers = [ + _serialize_str, + _serialize_str, + + ] + + import os + for idx, output_file in enumerate(_output_files): + try: + os.makedirs(os.path.dirname(output_file)) + except OSError: + pass + with open(output_file, 'w') as f: + f.write(_output_serializers[idx](_outputs[idx])) args: - --gcp-project-id - inputValue: gcp_project_id diff --git a/ml/automl/tables/kfp_e2e/import_data_from_bigquery/tables_component.py b/ml/automl/tables/kfp_e2e/import_data_from_bigquery/tables_component.py index a62e5e2..185c878 100644 --- a/ml/automl/tables/kfp_e2e/import_data_from_bigquery/tables_component.py +++ b/ml/automl/tables/kfp_e2e/import_data_from_bigquery/tables_component.py @@ -25,8 +25,10 @@ def automl_import_data_for_tables( ) -> NamedTuple('Outputs', [('dataset_display_name', str)]): import sys import subprocess - subprocess.run([sys.executable, '-m', 'pip', 'install', 'googleapis-common-protos==1.6.0', '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) - subprocess.run([sys.executable, '-m', 'pip', 'install', 'google-cloud-automl==0.9.0', '--quiet', '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) + subprocess.run([sys.executable, '-m', 'pip', 'install', 'googleapis-common-protos==1.6.0', + '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) + subprocess.run([sys.executable, '-m', 'pip', 'install', 'google-cloud-automl==0.9.0', '--quiet', + '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) import google import logging @@ -36,22 +38,22 @@ def automl_import_data_for_tables( def list_column_specs(client, dataset_display_name, filter_=None): - """List all column specs.""" - result = [] - - # List all the table specs in the dataset - response = client.list_column_specs( - dataset_display_name=dataset_display_name, filter_=filter_) - logging.info("List of column specs:") - for column_spec in response: - # Display the column_spec information. - logging.info("Column spec name: {}".format(column_spec.name)) - logging.info("Column spec id: {}".format(column_spec.name.split("/")[-1])) - logging.info("Column spec display name: {}".format(column_spec.display_name)) - logging.info("Column spec data type: {}".format(column_spec.data_type)) - - result.append(column_spec) - return result + """List all column specs.""" + result = [] + + # List all the table specs in the dataset + response = client.list_column_specs( + dataset_display_name=dataset_display_name, filter_=filter_) + logging.info("List of column specs:") + for column_spec in response: + # Display the column_spec information. + logging.info("Column spec name: {}".format(column_spec.name)) + logging.info("Column spec id: {}".format(column_spec.name.split("/")[-1])) + logging.info("Column spec display name: {}".format(column_spec.display_name)) + logging.info("Column spec data type: {}".format(column_spec.data_type)) + + result.append(column_spec) + return result logging.getLogger().setLevel(logging.INFO) # TODO: make level configurable @@ -60,22 +62,21 @@ def list_column_specs(client, if api_endpoint: client_options = ClientOptions(api_endpoint=api_endpoint) client = automl.TablesClient(project=gcp_project_id, region=gcp_region, - client_options=client_options) + client_options=client_options) else: client = automl.TablesClient(project=gcp_project_id, region=gcp_region) response = None if path.startswith('bq'): response = client.import_data( - dataset_display_name=dataset_display_name, bigquery_input_uri=path + dataset_display_name=dataset_display_name, bigquery_input_uri=path ) else: # Get the multiple Google Cloud Storage URIs. input_uris = path.split(",") response = client.import_data( - dataset_display_name=dataset_display_name, - gcs_input_uris=input_uris - ) + dataset_display_name=dataset_display_name, + gcs_input_uris=input_uris) logging.info("Processing import... This can take a while.") # synchronous check of operation status. logging.info("Data imported. {}".format(response.result())) @@ -84,13 +85,7 @@ def list_column_specs(client, # now list the inferred col schema list_column_specs(client, dataset_display_name) - return (dataset_display_name) - - - -# if __name__ == "__main__": -# automl_import_data_for_tables('bq://aju-dev-demos.london_bikes_weather.bikes_weather', -# 'aju-vtests2', 'us-central1', 'comp_test1') + return dataset_display_name if __name__ == '__main__': diff --git a/ml/automl/tables/kfp_e2e/import_data_from_bigquery/tables_component.yaml b/ml/automl/tables/kfp_e2e/import_data_from_bigquery/tables_component.yaml index 921684f..10112fc 100644 --- a/ml/automl/tables/kfp_e2e/import_data_from_bigquery/tables_component.yaml +++ b/ml/automl/tables/kfp_e2e/import_data_from_bigquery/tables_component.yaml @@ -34,8 +34,10 @@ implementation: ) -> NamedTuple('Outputs', [('dataset_display_name', str)]): import sys import subprocess - subprocess.run([sys.executable, '-m', 'pip', 'install', 'googleapis-common-protos==1.6.0', '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) - subprocess.run([sys.executable, '-m', 'pip', 'install', 'google-cloud-automl==0.9.0', '--quiet', '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) + subprocess.run([sys.executable, '-m', 'pip', 'install', 'googleapis-common-protos==1.6.0', + '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) + subprocess.run([sys.executable, '-m', 'pip', 'install', 'google-cloud-automl==0.9.0', '--quiet', + '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) import google import logging @@ -45,22 +47,22 @@ implementation: def list_column_specs(client, dataset_display_name, filter_=None): - """List all column specs.""" - result = [] - - # List all the table specs in the dataset - response = client.list_column_specs( - dataset_display_name=dataset_display_name, filter_=filter_) - logging.info("List of column specs:") - for column_spec in response: - # Display the column_spec information. - logging.info("Column spec name: {}".format(column_spec.name)) - logging.info("Column spec id: {}".format(column_spec.name.split("/")[-1])) - logging.info("Column spec display name: {}".format(column_spec.display_name)) - logging.info("Column spec data type: {}".format(column_spec.data_type)) - - result.append(column_spec) - return result + """List all column specs.""" + result = [] + + # List all the table specs in the dataset + response = client.list_column_specs( + dataset_display_name=dataset_display_name, filter_=filter_) + logging.info("List of column specs:") + for column_spec in response: + # Display the column_spec information. + logging.info("Column spec name: {}".format(column_spec.name)) + logging.info("Column spec id: {}".format(column_spec.name.split("/")[-1])) + logging.info("Column spec display name: {}".format(column_spec.display_name)) + logging.info("Column spec data type: {}".format(column_spec.data_type)) + + result.append(column_spec) + return result logging.getLogger().setLevel(logging.INFO) # TODO: make level configurable @@ -69,22 +71,21 @@ implementation: if api_endpoint: client_options = ClientOptions(api_endpoint=api_endpoint) client = automl.TablesClient(project=gcp_project_id, region=gcp_region, - client_options=client_options) + client_options=client_options) else: client = automl.TablesClient(project=gcp_project_id, region=gcp_region) response = None if path.startswith('bq'): response = client.import_data( - dataset_display_name=dataset_display_name, bigquery_input_uri=path + dataset_display_name=dataset_display_name, bigquery_input_uri=path ) else: # Get the multiple Google Cloud Storage URIs. input_uris = path.split(",") response = client.import_data( - dataset_display_name=dataset_display_name, - gcs_input_uris=input_uris - ) + dataset_display_name=dataset_display_name, + gcs_input_uris=input_uris) logging.info("Processing import... This can take a while.") # synchronous check of operation status. logging.info("Data imported. {}".format(response.result())) @@ -93,7 +94,7 @@ implementation: # now list the inferred col schema list_column_specs(client, dataset_display_name) - return (dataset_display_name) + return dataset_display_name def _serialize_str(str_value: str) -> str: if not isinstance(str_value, str): @@ -117,7 +118,8 @@ implementation: _outputs = [_outputs] _output_serializers = [ - _serialize_str + _serialize_str, + ] import os diff --git a/ml/automl/tables/kfp_e2e/import_data_from_bigquery/tables_schema_component.py b/ml/automl/tables/kfp_e2e/import_data_from_bigquery/tables_schema_component.py index 36e8765..7038c12 100644 --- a/ml/automl/tables/kfp_e2e/import_data_from_bigquery/tables_schema_component.py +++ b/ml/automl/tables/kfp_e2e/import_data_from_bigquery/tables_schema_component.py @@ -28,8 +28,11 @@ def automl_set_dataset_schema( ) -> NamedTuple('Outputs', [('display_name', str)]): import sys import subprocess - subprocess.run([sys.executable, '-m', 'pip', 'install', 'googleapis-common-protos==1.6.0', '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) - subprocess.run([sys.executable, '-m', 'pip', 'install', 'google-cloud-automl==0.9.0', '--quiet', '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) + subprocess.run([sys.executable, '-m', 'pip', 'install', 'googleapis-common-protos==1.6.0', + '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) + subprocess.run([sys.executable, '-m', 'pip', 'install', 'google-cloud-automl==0.9.0', + '--quiet', '--no-warn-script-location'], + env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) import json import google @@ -44,17 +47,17 @@ def update_column_spec(client, nullable=None ): - logging.info("Setting {} to type {} and nullable {}".format( - column_spec_display_name, type_code, nullable)) - response = client.update_column_spec( - dataset_display_name=dataset_display_name, - column_spec_display_name=column_spec_display_name, - type_code=type_code, - nullable=nullable - ) + logging.info("Setting {} to type {} and nullable {}".format( + column_spec_display_name, type_code, nullable)) + response = client.update_column_spec( + dataset_display_name=dataset_display_name, + column_spec_display_name=column_spec_display_name, + type_code=type_code, + nullable=nullable + ) - # synchronous check of operation status. - print("Table spec updated. {}".format(response)) + # synchronous check of operation status. + print("Table spec updated. {}".format(response)) def update_dataset(client, dataset_display_name, @@ -62,18 +65,18 @@ def update_dataset(client, time_column_spec_name=None, test_train_column_spec_name=None): - if target_column_spec_name: - response = client.set_target_column( - dataset_display_name=dataset_display_name, - column_spec_display_name=target_column_spec_name - ) - print("Target column updated. {}".format(response)) - if time_column_spec_name: - response = client.set_time_column( - dataset_display_name=dataset_display_name, - column_spec_display_name=time_column_spec_name - ) - print("Time column updated. {}".format(response)) + if target_column_spec_name: + response = client.set_target_column( + dataset_display_name=dataset_display_name, + column_spec_display_name=target_column_spec_name + ) + print("Target column updated. {}".format(response)) + if time_column_spec_name: + response = client.set_time_column( + dataset_display_name=dataset_display_name, + column_spec_display_name=time_column_spec_name + ) + print("Time column updated. {}".format(response)) logging.getLogger().setLevel(logging.INFO) # TODO: make level configurable @@ -90,27 +93,17 @@ def update_dataset(client, schema_dict = json.loads(schema_info) # Update cols for which the desired schema was not inferred. if schema_dict: - for k,v in schema_dict.items(): + for k, v in schema_dict.items(): update_column_spec(client, display_name, k, v[0], nullable=v[1]) # Update the dataset with info about the target col, plus optionally info on how to split on # a time col or a test/train col. update_dataset(client, display_name, - target_column_spec_name=target_col_name, - time_column_spec_name=time_col_name, - test_train_column_spec_name=test_train_col_name) - - return (display_name) - + target_column_spec_name=target_col_name, + time_column_spec_name=time_col_name, + test_train_column_spec_name=test_train_col_name) -# if __name__ == "__main__": -# import json -# # sdict = {"end_station_id": "CATEGORY", "start_station_id":"CATEGORY", "loc_cross": "CATEGORY", "bike_id": "CATEGORY"} -# sdict = {"accepted_answer_id": ["CATEGORY", True], "id": ["CATEGORY", True], "last_editor_display_name": ["CATEGORY", True], "last_editor_user_id": ["CATEGORY", True], -# "owner_display_name": ["CATEGORY", True], "owner_user_id": ["CATEGORY", True], -# "parent_id": ["CATEGORY", True], "post_type_id": ["CATEGORY", True], "tags": ["CATEGORY", True]} -# automl_set_dataset_schema('aju-vtests2', 'us-central1', 'so_test1', -# 't1', json.dumps(sdict)) + return display_name if __name__ == '__main__': diff --git a/ml/automl/tables/kfp_e2e/import_data_from_bigquery/tables_schema_component.yaml b/ml/automl/tables/kfp_e2e/import_data_from_bigquery/tables_schema_component.yaml index 0b6da83..4b19689 100644 --- a/ml/automl/tables/kfp_e2e/import_data_from_bigquery/tables_schema_component.yaml +++ b/ml/automl/tables/kfp_e2e/import_data_from_bigquery/tables_schema_component.yaml @@ -46,8 +46,11 @@ implementation: ) -> NamedTuple('Outputs', [('display_name', str)]): import sys import subprocess - subprocess.run([sys.executable, '-m', 'pip', 'install', 'googleapis-common-protos==1.6.0', '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) - subprocess.run([sys.executable, '-m', 'pip', 'install', 'google-cloud-automl==0.9.0', '--quiet', '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) + subprocess.run([sys.executable, '-m', 'pip', 'install', 'googleapis-common-protos==1.6.0', + '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) + subprocess.run([sys.executable, '-m', 'pip', 'install', 'google-cloud-automl==0.9.0', + '--quiet', '--no-warn-script-location'], + env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) import json import google @@ -62,17 +65,17 @@ implementation: nullable=None ): - logging.info("Setting {} to type {} and nullable {}".format( - column_spec_display_name, type_code, nullable)) - response = client.update_column_spec( - dataset_display_name=dataset_display_name, - column_spec_display_name=column_spec_display_name, - type_code=type_code, - nullable=nullable - ) + logging.info("Setting {} to type {} and nullable {}".format( + column_spec_display_name, type_code, nullable)) + response = client.update_column_spec( + dataset_display_name=dataset_display_name, + column_spec_display_name=column_spec_display_name, + type_code=type_code, + nullable=nullable + ) - # synchronous check of operation status. - print("Table spec updated. {}".format(response)) + # synchronous check of operation status. + print("Table spec updated. {}".format(response)) def update_dataset(client, dataset_display_name, @@ -80,18 +83,18 @@ implementation: time_column_spec_name=None, test_train_column_spec_name=None): - if target_column_spec_name: - response = client.set_target_column( - dataset_display_name=dataset_display_name, - column_spec_display_name=target_column_spec_name - ) - print("Target column updated. {}".format(response)) - if time_column_spec_name: - response = client.set_time_column( - dataset_display_name=dataset_display_name, - column_spec_display_name=time_column_spec_name - ) - print("Time column updated. {}".format(response)) + if target_column_spec_name: + response = client.set_target_column( + dataset_display_name=dataset_display_name, + column_spec_display_name=target_column_spec_name + ) + print("Target column updated. {}".format(response)) + if time_column_spec_name: + response = client.set_time_column( + dataset_display_name=dataset_display_name, + column_spec_display_name=time_column_spec_name + ) + print("Time column updated. {}".format(response)) logging.getLogger().setLevel(logging.INFO) # TODO: make level configurable @@ -107,17 +110,17 @@ implementation: schema_dict = json.loads(schema_info) # Update cols for which the desired schema was not inferred. if schema_dict: - for k,v in schema_dict.items(): + for k, v in schema_dict.items(): update_column_spec(client, display_name, k, v[0], nullable=v[1]) # Update the dataset with info about the target col, plus optionally info on how to split on # a time col or a test/train col. update_dataset(client, display_name, - target_column_spec_name=target_col_name, - time_column_spec_name=time_col_name, - test_train_column_spec_name=test_train_col_name) + target_column_spec_name=target_col_name, + time_column_spec_name=time_col_name, + test_train_column_spec_name=test_train_col_name) - return (display_name) + return display_name def _serialize_str(str_value: str) -> str: if not isinstance(str_value, str): @@ -144,7 +147,8 @@ implementation: _outputs = [_outputs] _output_serializers = [ - _serialize_str + _serialize_str, + ] import os diff --git a/ml/automl/tables/kfp_e2e/tables_pipeline_caip.py b/ml/automl/tables/kfp_e2e/tables_pipeline_caip.py index 732808f..a3c2caa 100644 --- a/ml/automl/tables/kfp_e2e/tables_pipeline_caip.py +++ b/ml/automl/tables/kfp_e2e/tables_pipeline_caip.py @@ -126,11 +126,11 @@ def automl_tables( #pylint: disable=unused-argument ) eval_metrics = eval_metrics_op( - gcp_project_id=gcp_project_id, - gcp_region=gcp_region, - bucket_name=bucket_name, - api_endpoint=api_endpoint, - model_display_name=train_model.outputs['model_display_name'], + # gcp_project_id=gcp_project_id, + # gcp_region=gcp_region, + # bucket_name=bucket_name, + # api_endpoint=api_endpoint, + # model_display_name=train_model.outputs['model_display_name'], thresholds=thresholds, eval_data=eval_model.outputs['eval_data'], ) diff --git a/ml/automl/tables/kfp_e2e/tables_pipeline_caip.py.tar.gz b/ml/automl/tables/kfp_e2e/tables_pipeline_caip.py.tar.gz index d33fd0ff7840a50a4d7a065e25b3e7aa9fb1ff24..99e6390319f768d8e5a34e1aa43229a19cb33147 100644 GIT binary patch literal 9019 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 literal 10466 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 diff --git a/ml/automl/tables/kfp_e2e/tables_pipeline_kf.py b/ml/automl/tables/kfp_e2e/tables_pipeline_kf.py index 5812675..1bf7f78 100644 --- a/ml/automl/tables/kfp_e2e/tables_pipeline_kf.py +++ b/ml/automl/tables/kfp_e2e/tables_pipeline_kf.py @@ -126,11 +126,11 @@ def automl_tables( #pylint: disable=unused-argument ).apply(gcp.use_gcp_secret('user-gcp-sa')) eval_metrics = eval_metrics_op( - gcp_project_id=gcp_project_id, - gcp_region=gcp_region, - bucket_name=bucket_name, - api_endpoint=api_endpoint, - model_display_name=train_model.outputs['model_display_name'], + # gcp_project_id=gcp_project_id, + # gcp_region=gcp_region, + # bucket_name=bucket_name, + # api_endpoint=api_endpoint, + # model_display_name=train_model.outputs['model_display_name'], thresholds=thresholds, eval_data=eval_model.outputs['eval_data'], ).apply(gcp.use_gcp_secret('user-gcp-sa')) diff --git a/ml/automl/tables/kfp_e2e/tables_pipeline_kf.py.tar.gz b/ml/automl/tables/kfp_e2e/tables_pipeline_kf.py.tar.gz index 37870db9c8f1fa8b4a46f9da6ef7f464823b84da..1f7f353b03a0d43ea570adace89c229abac9803a 100644 GIT binary patch literal 9228 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 literal 10730 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 Date: Thu, 23 Apr 2020 11:05:51 -0700 Subject: [PATCH 7/9] kfp bug? --- ml/automl/tables/kfp_e2e/README.md | 6 +- .../tables_eval_metrics_component.py | 51 ++++------ .../tables_eval_metrics_component.yaml | 90 ++++++------------ .../tables/kfp_e2e/tables_pipeline_caip.py | 7 +- .../kfp_e2e/tables_pipeline_caip.py.tar.gz | Bin 9019 -> 8831 bytes .../tables/kfp_e2e/tables_pipeline_kf.py | 7 +- .../kfp_e2e/tables_pipeline_kf.py.tar.gz | Bin 9228 -> 9013 bytes 7 files changed, 50 insertions(+), 111 deletions(-) diff --git a/ml/automl/tables/kfp_e2e/README.md b/ml/automl/tables/kfp_e2e/README.md index ed00330..fb9a0d2 100644 --- a/ml/automl/tables/kfp_e2e/README.md +++ b/ml/automl/tables/kfp_e2e/README.md @@ -76,9 +76,9 @@ Once a Pipelines installation is running, we can upload the example AutoML Table Click on **Pipelines** in the left nav bar of the Pipelines Dashboard. Click on **Upload Pipeline**. - For Cloud AI Platform Pipelines, upload [`tables_pipeline_caip.py.tar.gz`][36], from this directory. This archive points to the compiled version of [this pipeline][37], specified and compiled using the [Kubeflow Pipelines SDK][38]. -- For Kubeflow Pipelines on a Kubeflow installation, upload [`tables_pipeline_kf.py.tar.gz`][39]. This archive points to the compiled version of [this pipeline][40]. +- For Kubeflow Pipelines on a Kubeflow installation, upload [`tables_pipeline_kf.py.tar.gz`][39]. This archive points to the compiled version of [this pipeline][40]. **To run this example on a KF installation, you will need to give the `-user@.iam.gserviceaccount.com` service account `AutoML Admin` privileges**. -> Note: The difference between the two pipelines relates to how GCP authentication is handled. For the Kubeflow pipeline, we’ve added `.apply(gcp.use_gcp_secret('user-gcp-sa'))` annotations to the pipeline steps. This tells the pipeline to use the mounted _secret_—set up during the installation process— that provides GCP account credentials. With the Cloud AI Platform Pipelines installation, the GKE cluster nodes have been set up to use the `cloud-platform` scope. With an upcoming Kubeflow release, specification of the mounted secret will no longer be necessary. +> Note: The difference between the two pipelines relates to how GCP authentication is handled. For the Kubeflow pipeline, we’ve added `.apply(gcp.use_gcp_secret('user-gcp-sa'))` annotations to the pipeline steps. This tells the pipeline to use the mounted _secret_—set up during the installation process— that provides GCP account credentials. With the Cloud AI Platform Pipelines installation, the GKE cluster nodes have been set up to use the `cloud-platform` scope. With recent Kubeflow releases, specification of the mounted secret is no longer necessary, but we include both versions for compatibility. The uploaded pipeline graph will look similar to this: @@ -88,7 +88,7 @@ The uploaded pipeline graph will look similar to this: Click the **+Create Run** button to run the pipeline. You will need to fill in some pipeline parameters. -Specifically, replace `YOUR_PROJECT_HERE` with the name of your project; replace `YOUR_DATASET_NAME` with the name you want to give your new dataset (make it unique, and use letters, numbers and underscores up to 32 characters); and replace `YOUR_BUCKET_NAME` with the name of a [GCS bucket][41]. This bucket should be in the [same _region_][42] as that specified by the `gcp_region` parameter. E.g., if you keep the default `us-central1` region, your bucket should also be a _regional_ (not multi-regional) bucket in the `us-central1` region. ++double check that this is necessary.++ +Specifically, replace `YOUR_PROJECT_HERE` with the name of your project; replace `YOUR_DATASET_NAME` with the name you want to give your new dataset (make it unique, and use letters, numbers and underscores up to 32 characters); and replace `YOUR_BUCKET_NAME` with the name of a [GCS bucket][41]. Do not include the `gs://` prefix— just enter the name. This bucket should be in the [same _region_][42] as that specified by the `gcp_region` parameter. E.g., if you keep the default `us-central1` region, your bucket should also be a _regional_ (not multi-regional) bucket in the `us-central1` region. ++double check that this is necessary.++ If you want to schedule a recurrent set of runs, you can do that instead. If your data is in [BigQuery][43]— as is the case for this example pipeline— and has a temporal aspect, you could define a _view_ to reflect that, e.g. to return data from a window over the last `N` days or hours. Then, the AutoML pipeline could specify ingestion of data from that view, grabbing an updated data window each time the pipeline is run, and building a new model based on that updated window. diff --git a/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_metrics_component.py b/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_metrics_component.py index d220b85..ac7a48d 100644 --- a/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_metrics_component.py +++ b/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_metrics_component.py @@ -19,46 +19,29 @@ # An example of how the model eval info could be used to make decisions aboiut whether or not # to deploy the model. def automl_eval_metrics( - # gcp_project_id: str, - # gcp_region: str, - # model_display_name: str, eval_data_path: InputPath('evals'), mlpipeline_ui_metadata_path: OutputPath('UI_metadata'), mlpipeline_metrics_path: OutputPath('UI_metrics'), - # api_endpoint: str = None, # thresholds: str = '{"au_prc": 0.9}', - thresholds: str = '{"mean_absolute_error": 450}', + thresholds: str = '{"mean_absolute_error": 460}', confidence_threshold: float = 0.5 # for classification -) -> NamedTuple('Outputs', [('deploy', bool)]): +# ) -> NamedTuple('Outputs', [('deploy', str)]): # this gives the same result +) -> NamedTuple('Outputs', [('deploy', 'String')]): import subprocess import sys - # we could build a base image that includes these libraries if we don't want to do - # the dynamic installation when the step runs. - # subprocess.run([sys.executable, '-m', 'pip', 'install', 'googleapis-common-protos==1.6.0', - # '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) - # subprocess.run([sys.executable, '-m', 'pip', 'install', 'google-cloud-automl==0.9.0', - # 'google-cloud-storage', - # '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) + subprocess.run([sys.executable, '-m', 'pip', 'install', 'googleapis-common-protos==1.6.0', + '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) + subprocess.run([sys.executable, '-m', 'pip', 'install', 'google-cloud-automl==0.9.0', + 'google-cloud-storage', + '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) - # import google + import google import json import logging import pickle - # from google.api_core.client_options import ClientOptions - # from google.api_core import exceptions - # from google.cloud import automl_v1beta1 as automl - # from google.cloud import storage logging.getLogger().setLevel(logging.INFO) # TODO: make level configurable - # TODO: we could instead check for region 'eu' and use 'eu-automl.googleapis.com:443'endpoint - # in that case, instead of requiring endpoint to be specified. - # if api_endpoint: - # client_options = ClientOptions(api_endpoint=api_endpoint) - # client = automl.TablesClient(project=gcp_project_id, region=gcp_region, - # client_options=client_options) - # else: - # client = automl.TablesClient(project=gcp_project_id, region=gcp_region) thresholds_dict = json.loads(thresholds) logging.info('thresholds dict: {}'.format(thresholds_dict)) @@ -78,12 +61,12 @@ def regression_threshold_check(eval_info): if eresults[k] > v: logging.info('{} > {}; returning False'.format( eresults[k], v)) - return (False, eresults) + return ('False', eresults) elif eresults[k] < v: logging.info('{} < {}; returning False'.format( eresults[k], v)) - return (False, eresults) - return (True, eresults) + return ('False', eresults) + return ('deploy', eresults) def classif_threshold_check(eval_info): eresults = {} @@ -108,13 +91,13 @@ def classif_threshold_check(eval_info): if eresults[k] > v: logging.info('{} > {}; returning False'.format( eresults[k], v)) - return (False, eresults) + return ('False', eresults) else: if eresults[k] < v: logging.info('{} < {}; returning False'.format( eresults[k], v)) - return (False, eresults) - return (True, eresults) + return ('False', eresults) + return ('deploy', eresults) with open(eval_data_path, 'rb') as f: logging.info('successfully opened eval_data_path {}'.format(eval_data_path)) @@ -177,13 +160,13 @@ def classif_threshold_check(eval_info): mlpipeline_ui_metadata_file.write(json.dumps(metadata)) logging.info('deploy flag: {}'.format(res)) return res - return True + return 'deploy' except Exception as e: logging.warning(e) # If can't reconstruct the eval, or don't have thresholds defined, # return True as a signal to deploy. # TODO: is this the right default? - return True + return 'deploy' if __name__ == '__main__': diff --git a/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_metrics_component.yaml b/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_metrics_component.yaml index c69d0eb..757d5ee 100644 --- a/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_metrics_component.yaml +++ b/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_metrics_component.yaml @@ -4,7 +4,7 @@ inputs: type: evals - name: thresholds type: String - default: '{"mean_absolute_error": 450}' + default: '{"mean_absolute_error": 460}' optional: true - name: confidence_threshold type: Float @@ -16,7 +16,7 @@ outputs: - name: mlpipeline_metrics type: UI_metrics - name: deploy - type: Boolean + type: String implementation: container: image: python:3.7 @@ -25,64 +25,35 @@ implementation: - -u - -c - | - class OutputPath: - '''When creating component from function, OutputPath should be used as function parameter annotation to tell the system that the function wants to output data by writing it into a file with the given path instead of returning the data from the function.''' - def __init__(self, type=None): - self.type = type - def _make_parent_dirs_and_return_path(file_path: str): import os os.makedirs(os.path.dirname(file_path), exist_ok=True) return file_path - class InputPath: - '''When creating component from function, InputPath should be used as function parameter annotation to tell the system to pass the *data file path* to the function instead of passing the actual data.''' - def __init__(self, type=None): - self.type = type - - from typing import NamedTuple - def automl_eval_metrics( - # gcp_project_id: str, - # gcp_region: str, - # model_display_name: str, - eval_data_path: InputPath('evals'), - mlpipeline_ui_metadata_path: OutputPath('UI_metadata'), - mlpipeline_metrics_path: OutputPath('UI_metrics'), - # api_endpoint: str = None, + eval_data_path , + mlpipeline_ui_metadata_path , + mlpipeline_metrics_path , # thresholds: str = '{"au_prc": 0.9}', - thresholds: str = '{"mean_absolute_error": 450}', - confidence_threshold: float = 0.5 # for classification + thresholds = '{"mean_absolute_error": 460}', + confidence_threshold = 0.5 # for classification - ) -> NamedTuple('Outputs', [('deploy', bool)]): + # ) -> NamedTuple('Outputs', [('deploy', str)]): + ) : import subprocess import sys - # we could build a base image that includes these libraries if we don't want to do - # the dynamic installation when the step runs. - # subprocess.run([sys.executable, '-m', 'pip', 'install', 'googleapis-common-protos==1.6.0', - # '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) - # subprocess.run([sys.executable, '-m', 'pip', 'install', 'google-cloud-automl==0.9.0', - # 'google-cloud-storage', - # '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) + subprocess.run([sys.executable, '-m', 'pip', 'install', 'googleapis-common-protos==1.6.0', + '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) + subprocess.run([sys.executable, '-m', 'pip', 'install', 'google-cloud-automl==0.9.0', + 'google-cloud-storage', + '--no-warn-script-location'], env={'PIP_DISABLE_PIP_VERSION_CHECK': '1'}, check=True) - # import google + import google import json import logging import pickle - # from google.api_core.client_options import ClientOptions - # from google.api_core import exceptions - # from google.cloud import automl_v1beta1 as automl - # from google.cloud import storage logging.getLogger().setLevel(logging.INFO) # TODO: make level configurable - # TODO: we could instead check for region 'eu' and use 'eu-automl.googleapis.com:443'endpoint - # in that case, instead of requiring endpoint to be specified. - # if api_endpoint: - # client_options = ClientOptions(api_endpoint=api_endpoint) - # client = automl.TablesClient(project=gcp_project_id, region=gcp_region, - # client_options=client_options) - # else: - # client = automl.TablesClient(project=gcp_project_id, region=gcp_region) thresholds_dict = json.loads(thresholds) logging.info('thresholds dict: {}'.format(thresholds_dict)) @@ -102,12 +73,12 @@ implementation: if eresults[k] > v: logging.info('{} > {}; returning False'.format( eresults[k], v)) - return (False, eresults) + return ('False', eresults) elif eresults[k] < v: logging.info('{} < {}; returning False'.format( eresults[k], v)) - return (False, eresults) - return (True, eresults) + return ('False', eresults) + return ('deploy', eresults) def classif_threshold_check(eval_info): eresults = {} @@ -132,13 +103,13 @@ implementation: if eresults[k] > v: logging.info('{} > {}; returning False'.format( eresults[k], v)) - return (False, eresults) + return ('False', eresults) else: if eresults[k] < v: logging.info('{} < {}; returning False'.format( eresults[k], v)) - return (False, eresults) - return (True, eresults) + return ('False', eresults) + return ('deploy', eresults) with open(eval_data_path, 'rb') as f: logging.info('successfully opened eval_data_path {}'.format(eval_data_path)) @@ -201,20 +172,18 @@ implementation: mlpipeline_ui_metadata_file.write(json.dumps(metadata)) logging.info('deploy flag: {}'.format(res)) return res - return True + return 'deploy' except Exception as e: logging.warning(e) # If can't reconstruct the eval, or don't have thresholds defined, # return True as a signal to deploy. # TODO: is this the right default? - return True + return 'deploy' - def _serialize_bool(bool_value: bool) -> str: - if isinstance(bool_value, str): - return bool_value - if not isinstance(bool_value, bool): - raise TypeError('Value "{}" has type "{}" instead of bool.'.format(str(bool_value), str(type(bool_value)))) - return str(bool_value) + def _serialize_str(str_value: str) -> str: + if not isinstance(str_value, str): + raise TypeError('Value "{}" has type "{}" instead of str.'.format(str(str_value), str(type(str_value)))) + return str_value import argparse _parser = argparse.ArgumentParser(prog='Automl eval metrics', description='') @@ -229,11 +198,8 @@ implementation: _outputs = automl_eval_metrics(**_parsed_args) - if not hasattr(_outputs, '__getitem__') or isinstance(_outputs, str): - _outputs = [_outputs] - _output_serializers = [ - _serialize_bool, + _serialize_str, ] diff --git a/ml/automl/tables/kfp_e2e/tables_pipeline_caip.py b/ml/automl/tables/kfp_e2e/tables_pipeline_caip.py index a3c2caa..a6a391c 100644 --- a/ml/automl/tables/kfp_e2e/tables_pipeline_caip.py +++ b/ml/automl/tables/kfp_e2e/tables_pipeline_caip.py @@ -126,16 +126,11 @@ def automl_tables( #pylint: disable=unused-argument ) eval_metrics = eval_metrics_op( - # gcp_project_id=gcp_project_id, - # gcp_region=gcp_region, - # bucket_name=bucket_name, - # api_endpoint=api_endpoint, - # model_display_name=train_model.outputs['model_display_name'], thresholds=thresholds, eval_data=eval_model.outputs['eval_data'], ) - with dsl.Condition(eval_metrics.outputs['deploy'] == True): + with dsl.Condition(eval_metrics.outputs['deploy'] == 'True'): deploy_model = deploy_model_op( gcp_project_id=gcp_project_id, gcp_region=gcp_region, diff --git a/ml/automl/tables/kfp_e2e/tables_pipeline_caip.py.tar.gz b/ml/automl/tables/kfp_e2e/tables_pipeline_caip.py.tar.gz index 99e6390319f768d8e5a34e1aa43229a19cb33147..42c07f2b7359ef585477ceac17d06f3f13ced06a 100644 GIT binary patch literal 8831 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 literal 9019 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 diff --git a/ml/automl/tables/kfp_e2e/tables_pipeline_kf.py b/ml/automl/tables/kfp_e2e/tables_pipeline_kf.py index 1bf7f78..b93f028 100644 --- a/ml/automl/tables/kfp_e2e/tables_pipeline_kf.py +++ b/ml/automl/tables/kfp_e2e/tables_pipeline_kf.py @@ -126,16 +126,11 @@ def automl_tables( #pylint: disable=unused-argument ).apply(gcp.use_gcp_secret('user-gcp-sa')) eval_metrics = eval_metrics_op( - # gcp_project_id=gcp_project_id, - # gcp_region=gcp_region, - # bucket_name=bucket_name, - # api_endpoint=api_endpoint, - # model_display_name=train_model.outputs['model_display_name'], thresholds=thresholds, eval_data=eval_model.outputs['eval_data'], ).apply(gcp.use_gcp_secret('user-gcp-sa')) - with dsl.Condition(eval_metrics.outputs['deploy'] == True): + with dsl.Condition(eval_metrics.outputs['deploy'] == 'd'): deploy_model = deploy_model_op( gcp_project_id=gcp_project_id, gcp_region=gcp_region, diff --git a/ml/automl/tables/kfp_e2e/tables_pipeline_kf.py.tar.gz b/ml/automl/tables/kfp_e2e/tables_pipeline_kf.py.tar.gz index 1f7f353b03a0d43ea570adace89c229abac9803a..2676987583829866576b63b080b7f024e9402c83 100644 GIT binary patch literal 9013 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 bX`l9KpY~~=_GzE?*~|0)daSY$06+l%KS`9* literal 9228 zcmV+nB=g%JiwFoIHiljT|8!wuY-Mv_aA|O5Y-w&~Uu$MAaCt6tVR8WNJ^gdrxUv1& ze+5RK8%Z}39p6`9^`h(e65lm#F3u!QJDtSCQW7Pzrbrz>?CAXbzrS4oBmod0DOqyj z95s_zA{UFr*M4Akft9y%eJ^mWwX^j9w4cut`26tQcl0lO|5{#nbbajj zt4rtjpmXH~vkc;2VRSY3!<&Q7(oLM1lQ^9bJnlF_5GGCnxMKPM9@2C(+n+sZaXtKo<`m(VH9o;eAKYey?|Lye6DZ&QM(#4mH>B`2Yy3@q=X80zg z6+1q>O`_C=C&_w6Vc#Z^7c4q=10vy1Z{Y{WKLDb>$U&QIQ+p4+cZ zewC=k4v*c$o_X=gch)w2zD+G6QkVy}8_ZUr7bIIDoBTF9I&%J-4rlH){9A^x<%hv6 z4D5+_<;M1n>m-+MWJ$k73RduBtD0wNL?@ky3>NhQx@a2uyXl_9Te|ndcKe|N{cFc; zfxvY8fOGXj=h?~m=`Uw*{`~>4BBqN0`Orb8lr)6(!>K)u!Z@x1!d|byb0@Yno?g03 z$M%AGxK&G!f&WQ;7dalxf^>!>vh;l4vx9Kv+LvJ(#nm;u2oiSz)1)-nZkB+-7i*yp zU^TmQ@8i9P8w)SJ-&yyE6J!wH_5Ej&7fk(hMki9b3@G!X6DqzpX7}SRF;n*Q)4ON? z&~TqgI=zB%yw5sVx=vs_lNe}CT-%MJFv5xX-Tyq1=3#Ofx$$M_&l)!3LMSWAiOwBL zr-d81C`Mi}5H4~Y09^XRgaQr^I!qu@X%;QgC9w8FmN^j=O+Z@mC&RqHTz(NE%M|{& zbM5%40$7qv)i{|nslX>lA~PnP?5LwrltX6Bq5x4bb~@&A$o4BJ*h;LJYg6PxMa7v^ zslX-5giV?(FxUzLZWVcftUKr21#d6O3KLu|CrkibMrdE(RdhoYd#`98iU~7Tk)2mf z4<-oAc=VinlhHCV?M;vv`ST`71lC)(W(DS5F>*PBt(=A$cFnmpvPon)Yo<2pnJNOs zZglNU-IM7wOoK$`z?m&DjM5;WYqyiQOW;n4%NQ-tJ`IBeW*KM#ql{Eo$Z`H_C{jEe z0>2z`9z4u>ump2^%j^ZKVCt64x$o}GxFufP2rnyP!pXVvkaOjsFv7LibsXJP5Af)r z!lP^Pi+tULcQ_2w1PTt3na4Vqqhz@{VnFdxMlwRvAGIZ>5ioYwMvmL8Kvsn}6X9VV zEtd`&vN{B-b#fU7-)ftfYM)Mvr}@Kq6fOyK(^h!5DNpuPaUfcmmb+2c*{(Mp<}T3?+Yf(H|WMWbsap*Rked zI)P4`y0Q4KcxVC3>Ai=Nmiy72rnG?r(j6|LrY`U${Nn{6T74hCF2Zo(yU~!t)(_T`uGm@l=?;fMIJ|M9V953~h9HBfB$&*;-L_RxTW z_D)uSFWl?n39Nx*;>7%kNHC|!0OB|oM z%v+?Hp$x}Z9*Le&NbB z4_PhBS0#UtqCmB>pbTqGHG>t*Rf!3FP)q=3t#m64KSdz zG?cfXd0x`%9CiBdhsPI<(21R@MJ#q&BaFEq?NmX3NHlesMxsfz?nei-Y|2Ma=4-HX z2%ADQ!pX*$TE=y)iCtiEvshcPpa@tTu3wEUaDuQN7ll$Vt$>v!JS;{B_%<|9wQ^Nexa{UUcYz- z^86I7GZaV)L(2LKQ=5_M zj8y4^zjDgVN6wyVCoj7m_9cuab8Bw`FiEj_Vl{{L}=21;U$;QINbSTzXZ7i zsH|=_`Vw({*6mnIc-U74nvYiwQ0fe@IKeK83s!2=pQtKludEZkY51DHK~}#QcTcDg z$QDP&d{I`j^$YVy@Oq8A-M(6d<;-SwwjtT;s1|OAB1pz%hZeSZjJiG-+wP32RtQgb zWXCz>Z{NLs{pR%T+h%p=zOAC({Lwz@)LF=R?FNU~KIjlF=)8W#cc!WLXUC84S25q9 z&iWyDyk;~QBtb?FE@aCOomsq7s$|1IC&p$^?d)mMBLQyPjK5VeQ>>@}Uc;|mRvZ08 z-}HpFnjxQi=mH)~7_3#e>XoX0e<5g6{AHXgXBGDM*I$ds1tgrO0WWeA;4B$Vu!?LO z6n-ypm$u#Q6IkK`-z&h%Y)>HTefIsLM0LS_BMK>_ka+W8>v#FfixLYA3-busmpA)3 zAi{8<(clI!JJ8S4lLks@Zb_S^d$qBJ7SPOtIS}F|RB{R0RD{d9C*V@}HkXiRZ>g*( zV_i9t^-q|V9Io6zL<&Q90|N%aWnM-xw{9X&Xpt}q@1YMa@{y&#a{4-8_X=B0njKg| zemOh)1?=II*RNl`cy@A5?cq14&re^Szc_jMHV4%0%o1`0$`OdIBjm{NWd^Gf=!%cP zZiEZ+gak1*BCF*jEaJGUHcW_p+GWo{AHmf8CfMqps{N;PB8;y zg=21LB)jZrtbi3Zt>w5Kww+#z-KmP-nTEg*0yKx!03PNt5QA~d!8STiu|9yN3epq~ zQ&r~P(Jr^Lr`jldD=_~ZR>K)}NR?gJRoWuO%GizE!Q!rJT?YuJN7Me*j~Vw2mS!Dy z0kL{=wFO#Hc!h}2j>4;xcyni()UT`>{6nFI!gSX0j7E-z?T9G)ZUY+hzKi>kAo$rg zAwu@iaE@FT08z;OFiOr2hD!T z<#DTqWJ@E1>GG%-$vvl^;U=`kpq3Hwy*Sa7Fh;N#OH?j^5%H*l1fzhOx{b@dCb)@< zDP4M4=Q-9niM3wk!@}=2Nj>=AArqJogc>u~P1=2&}A(^eA3Eq@&|12F&a&zeh^lhBDD-xwaY(0oS`%Ca(w?69{pLK~Ysxr5p z?ADXLCr>utJk`iRspj%~6n(dM_U&En<`JbMbMl}wkNW!XzaDk+92ne&-E$OBIQ$e1 zxxKS2OF?aP=0C$oqTk|mCHi8CI4_XfL8um8s9oxW&eK}VL3*@fE73tdI}?y zeB}Jni-mfY&YC31O_!;kc<8XF1CN{vcj0_6HY~~2en=)D)vgE_Axke{NPyX)`Vg*2 z3Rbz#W*n_KCUf`}U&_1=D1q8wZs(0+PkId+lTkHOdk4x(ol$6PtRi@CDWNKv{ssUA zbhYE1`SY3Pmk-i_FL4$NqO5Tq-NLIYi@v!>j0qb6A(-SIu_SHOMjN$Ji6Sdgq-p*H zmuLSzeO}%KWb7ZKP#8{k)-A!v5hlO#z!Kv4puEi1Brn)7%tSX5Y9*`zVjN3z?7Bb} zF21+!d0cAoI+$KYVGu%p-8f!S)qoD`rL1c#ZBRuPe^8>odQkY*u#hhQDolP3Q(z(+ zC&?Ez1tF%10uoJCr&(3^sQ^&tjX9{ET&$23*1#S$4rJiq2W^WD zJM>{Oy><(~6eCNTQ#WjjzV&bC$OX)r}bI1n%p7+sn&>f%(T8xI*6 z$>#wnga__DCa&+ZjL$XLy-T)liNEubm_tglKv;Lt@F6l;6I8*ds|IsX;k84k%BdU; ziX$NJfTB%|GZEYB6o6$uLkRG;!ej90&|FY&?rz+>@`U6moy*RPMvU4NfEy$q{l$r7( zxaUO45$t51gjA8IkG^KZh@+2$e@)@VDI|ZD7#?Pl;K4iTvzKXkj$xEr_yS!+@7JA)xFY>oW47|a{()dy?eAws& zfksSCt>1yi_;BxM)zfy!z;O~r zFoGJey|h+#JXX7ur5wQuM>Bu1@}^gc2XzN7=)=vO(1-VOLsyjMW7YbeOTOpBD(~~* zj{MJud+|UYZt8U_cNv_bT2UNo0DJ}fD9-#dWVco17M;qt5BgQ zd)$bEl^fyRd%&~miBVx2_Zo~$v!2TVCDh2wt_I|qu_9?wTIg+wd;RJ-gDaXye=Uwb z;wyRNfkRzkCFYR_L9QFY(&KWsmQ4((E$_6e3-TkmF2krTxxE9p+q?f2%wFok%iE>F zAdSw|g|6YEqn*11vl1#ODEBhmPjz&csD8RR)wMJ?qBsL$9@YXz;bmNZ-vJ|&?w*ji z*^>*4voM@bBADi0hwjTTyuzNs*)QW1*;vIz7qlSt*cH5PbTQ(F!c`F7s5(EI*j^Na z(&Q)QrQw>Wr*TQ*gYtYRxC67(C?i1KNwk_Y4Blek)%ZXk-vl3kSKvby{LVtD((LUSOh~eGo0!DggTZ0W8qmk%ar^$)?2LA>#G~NQ?ydB#h%4})Rb5OlowM= zgBHqv*OZ$d+c9eAyjb>ATWS)y&ecKvQ<>LfbxEpBzU=#-P_V!%DQ4G~BM!U&q-~zdcaXQ7FgL&%vYl_F6DLZr}6*YgOG)%)9ayAc3 zi&@&-5SUvS!r7A*_C4g3osb%xhDK3}eUy~BUiRvAJr0Ep4gsfoYF2(*t>4@}ETNBixb1&H3)Y02roVw; z^e#ZwS?UHPtRtX&=grB#I{N079^Tcgpp^Qx@)&auxTA+w=OH;Y>Zk z+%sb2z1az*L0^c(igHElodF14eL4-H0-Q(eX?vk*C(y}N)d8Xpt7+rKGTMe`01X-~ zf@&MA6%%TEd9c#lcNS7ZVXB+0YIv#Ps+kDbilx@u*k3X~HHbp$Jj`vjqEn%PCVp#B zg!SG^L6zM#dv4j)gRrZum#_9kNp)+G{ZpI&gA4Tqa0dtXk|MnKG)|&)%JhC*-UEz= zXIo8|&b2VjW88g%4x0^12nB+v8;-=@B5-`_nx36`7;}hN^#x!}Qj&;~Qn917B*Z1>B^5P^iAlm!b7qnVwe$QW z>cz>_zReSp@_5POR7A)KGQ5V7_oGZbgasZnnrn74RUMhYF4AKHmX&dZBxqzcqyLxwaIm$`AyR4m78o-H!D(!bpbO|xF}b8E9lzs(e3R`7+q0ul$!f~U*`?hXDnum zqe;r*L!}pPnEp41DPrT*k`J8$_?WZN+PuYw%v; zQe8sZL!`6qIhiec&YOMkhdF4qL7kesIFv2G2193NPyBGw6WBcPJnA^7JB<7Z?r!r- zAt}7(Hc(Kb(uwa)?KJY4+d`3volfT+tufAoG*2vNbzAWMKsnctl5OXyTl|5=v$kXv zhhZt8crmWNiw#uDL35xKCxUi{wzW!_8$P#q4`RpD-l8g*+Ivbt`3%dSu^(>l_*j6p z1buRQXLTy04H}M5zjU%`F+!@kLYu>sR|?bXS026j6LuPq?Zq6Yza|b1ul0aq5hoE3 z<1|D}&?*`E^lL$!-o2>+sk1ph_0^uiBJupV$0oo)qvh1^>+kYNS@U6{qXy@jeic*q z3ytWsI^-)-e~xMvOieW)ss|}^aF?yaSj7Z%m1~z2N9NMQL0xOVC+n8+Wc!5ftW$d! zuC8=(eriW)BXki#wJ?o9DZ^`V!xdS;gmiq)WYOs*r#ECuWXX3$l}vVbjKy)<1riXT z>=nOVrfQ%G4`4z-bYrpp743vvUzVS8+&Ir9lo$FYSTPXSi(rhS*wvVqFtot@dO3Wb zJSE3Z)E=j=lh6kH@CG_XE>SyPO6lV#2g(q=3>Ux|m*~%9jE9EPOr!w#gSypLx}KSJ zsfL~EN_#UyS&Lmg+&SvddxsmK;=^ttsN!s9MZhSNwwP$6PB|Y3I~gZZJ}&P^V!vNh zGAx9wZy3LX2=7@}DUiWGBr(P^&02~VQ_4(#m1^|VWA(k?y;bk)f*Kj_V+I8U-aomJU#`WM$LdRa73OtBPOxx$ z$Vb*{NEEcmVi_MIb&dcR!*WFN>zghTR5KX7JwpHVH62Iwa_qQ8JUTjp z-V-`qLTbb#k6|8ri|}0Sm!eRGYSp2v2)?cgyefT}L?YF}itwk$I=U8SWaicO zMg`$p9k{D?ir&ar8{Sw7Zmh1Gc9uO^CvM#1eJpinJ`e4D00f?>Rj@OEs8X`M%Vejy z0_{yEkM1TDAwDLrT+m$i*4r$8M(FE!pE)6)?7o(!IWV6p<>E19hrUVpQn>u+RLs)meQ1=|0OIwf`8nE6qc4YPv`k=!cV;trlt*3#om z`PfMwR!uOeSSqQHZnn5>~d88F8tGP?Wc!X5d zGO8~HCMhsRTbGa0>2zLVC>o4%7A{69qCvMx1V`%AkhP0ad{u@3qp9dTVmO$8&drEc z(J8mYmpkR4zU^aehvW_Bf2#s_DDmN3YP>E0N}vqemC-*c9d`7b$7f;7({mxX}fVs=gU9N;iZkpEt_pEG+XC zNENY=*5|>X_vIgN9ptTpT;zJli{&n+JCq=Aqa?;n#a?&vTUX)MWv(fxc;#^V4LIh+ zTl|)~(b}f!w4FxYSn)f9@>SVghvXNR2h_qc@<2NyztCw469Q%ATNVjK-o1GdEAii1 zl{kA?FECa#==2|^`K6+`$5N0rF1{M??CHE_YE_|~Tb4!6@xZ&O1IrNEf&YzvKkoqXYPl7FwX&$H{Bgt&K0u@%}|PjSU+;t>?J) z9Jik1)^ogp=UA%_?aSwDgv(8jPvRAB;#}RA=hz@}_wVS{YidSsZ&NyJyt1rTs)Yyv z3iox*Rw5r(Oz}o1*HyZL+QZ`Z?|R&`%Q1n@0*x4q%8jaE#n2Y5#Lg_(p^#u*idVy#reuy}+0> zM-|d5cg@`)gzXGcHH$a_82pj5WEVL-c)UWeQXvS=p4hj>o zyE6KH!{XrG)@)&Hu_y}Zns+bl zZ&hCDigJ;-c|l1`mW!*)!JC`N9F;xe%6!G{kP7Qg=eoy)t(dX~niUjk%s@VGuy7W> zQ8RVa&e9D`!>%fdK43@b=xx|dP_$tuwE+-cP5{K`75>025Nd4jRBUz?wqTmbuJLzt zs^Y|DS2EsQdQ*0M^URIW1H^HX8;6~SmwAho(GzMO8Sk1GfK2zyn>8^as|<+q{gaDA zVHjW2Ba3vc6Jq3;c|~svFAgY6auzorD<33lw!!DG87M3iJo^kLFb$j_lE!;T9LngZ zNe8WUs8~6oVg;XxJ*AhWq@kEJiAcpTt-_#{l#)qVRk}i4YAjAAGKWP2v{3+ihyuWg zolS7I$l9d;de6MpBfo!7yt4gS+>I!&;yu+aY>(zO3({d$FMef}D zc#n!jG)=2U5xew$XBPp56YMYV`aYwtN7$4cra?^789g1?^L@__pjGx|m`3qEsH(&( z@{^g1Il*o)^;~i34janpO&F7U{MM#y$xzdzp$lh157GsNbn8EwUIm7C(XoL4!LttXNG0IIqWy-P@*I-sFU|&6cjFWUGRZ0j2HzEvwh& zByGxR>UhOQd$NsLjIn5|d_OkjyVZdk>A=GcY*t-c@qSJZcaVn1^{Y$^d~@jrDqu&q zpTX&=m_;JkkX_b!ch|u?h&ve8)Ok2RaO3mVKM#koW?Rz$S-A^YcNV}yJFL}xqsESH zGidFur^yAGue(2lM{R$CMnhjKLU!njV{CJ~tq)V#$9QALeJ;XKVMMw2r}oo++E4pw iKkcXew4e6Ve%ep_X+Q0!{j{G?{rP{Tp_d~7Kmh;`lpAvZ From 062494865ccfab102446dcdc51c658770cde51f6 Mon Sep 17 00:00:00 2001 From: Amy Unruh Date: Thu, 23 Apr 2020 14:11:10 -0700 Subject: [PATCH 8/9] hmm --- .../tables/kfp_e2e/tables_pipeline_caip.py | 2 +- .../kfp_e2e/tables_pipeline_caip.py.tar.gz | Bin 8831 -> 8835 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/ml/automl/tables/kfp_e2e/tables_pipeline_caip.py b/ml/automl/tables/kfp_e2e/tables_pipeline_caip.py index a6a391c..b5938bd 100644 --- a/ml/automl/tables/kfp_e2e/tables_pipeline_caip.py +++ b/ml/automl/tables/kfp_e2e/tables_pipeline_caip.py @@ -130,7 +130,7 @@ def automl_tables( #pylint: disable=unused-argument eval_data=eval_model.outputs['eval_data'], ) - with dsl.Condition(eval_metrics.outputs['deploy'] == 'True'): + with dsl.Condition(eval_metrics.outputs['deploy'] == 'd'): deploy_model = deploy_model_op( gcp_project_id=gcp_project_id, gcp_region=gcp_region, diff --git a/ml/automl/tables/kfp_e2e/tables_pipeline_caip.py.tar.gz b/ml/automl/tables/kfp_e2e/tables_pipeline_caip.py.tar.gz index 42c07f2b7359ef585477ceac17d06f3f13ced06a..7a65e79c7b73ced8f170f1ca4e5e22eea6203935 100644 GIT binary patch delta 8242 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 bM)-dt`hWBP>H6}6ci3>n1PK^1SeX9-Z(}Cp delta 8225 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 E59$37B>(^b From 513f758eed673287873adbbb3ebf77880d8fb89d Mon Sep 17 00:00:00 2001 From: Amy Unruh Date: Thu, 23 Apr 2020 17:19:16 -0700 Subject: [PATCH 9/9] accommodate change in sdk as to how output return values are handled --- .../tables_eval_metrics_component.py | 11 +++++------ .../tables_eval_metrics_component.yaml | 9 ++++----- .../tables/kfp_e2e/tables_pipeline_caip.py | 2 +- .../kfp_e2e/tables_pipeline_caip.py.tar.gz | Bin 8835 -> 8827 bytes .../tables/kfp_e2e/tables_pipeline_kf.py | 2 +- .../kfp_e2e/tables_pipeline_kf.py.tar.gz | Bin 9013 -> 9006 bytes 6 files changed, 11 insertions(+), 13 deletions(-) diff --git a/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_metrics_component.py b/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_metrics_component.py index ac7a48d..3f4df2d 100644 --- a/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_metrics_component.py +++ b/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_metrics_component.py @@ -26,8 +26,7 @@ def automl_eval_metrics( thresholds: str = '{"mean_absolute_error": 460}', confidence_threshold: float = 0.5 # for classification -# ) -> NamedTuple('Outputs', [('deploy', str)]): # this gives the same result -) -> NamedTuple('Outputs', [('deploy', 'String')]): +) -> NamedTuple('Outputs', [('deploy', str)]): import subprocess import sys subprocess.run([sys.executable, '-m', 'pip', 'install', 'googleapis-common-protos==1.6.0', @@ -141,7 +140,7 @@ def classif_threshold_check(eval_info): with open(mlpipeline_metrics_path, 'w') as mlpipeline_metrics_file: mlpipeline_metrics_file.write(json.dumps(metrics)) logging.info('deploy flag: {}'.format(res)) - return res + return (res,) if classif and thresholds_dict: res, eresults = classif_threshold_check(eval_info) @@ -159,14 +158,14 @@ def classif_threshold_check(eval_info): with open(mlpipeline_ui_metadata_path, 'w') as mlpipeline_ui_metadata_file: mlpipeline_ui_metadata_file.write(json.dumps(metadata)) logging.info('deploy flag: {}'.format(res)) - return res - return 'deploy' + return (res,) + return ('deploy',) except Exception as e: logging.warning(e) # If can't reconstruct the eval, or don't have thresholds defined, # return True as a signal to deploy. # TODO: is this the right default? - return 'deploy' + return ('deploy',) if __name__ == '__main__': diff --git a/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_metrics_component.yaml b/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_metrics_component.yaml index 757d5ee..80dbf48 100644 --- a/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_metrics_component.yaml +++ b/ml/automl/tables/kfp_e2e/create_model_for_tables/tables_eval_metrics_component.yaml @@ -38,7 +38,6 @@ implementation: thresholds = '{"mean_absolute_error": 460}', confidence_threshold = 0.5 # for classification - # ) -> NamedTuple('Outputs', [('deploy', str)]): ) : import subprocess import sys @@ -153,7 +152,7 @@ implementation: with open(mlpipeline_metrics_path, 'w') as mlpipeline_metrics_file: mlpipeline_metrics_file.write(json.dumps(metrics)) logging.info('deploy flag: {}'.format(res)) - return res + return (res,) if classif and thresholds_dict: res, eresults = classif_threshold_check(eval_info) @@ -171,14 +170,14 @@ implementation: with open(mlpipeline_ui_metadata_path, 'w') as mlpipeline_ui_metadata_file: mlpipeline_ui_metadata_file.write(json.dumps(metadata)) logging.info('deploy flag: {}'.format(res)) - return res - return 'deploy' + return (res,) + return ('deploy',) except Exception as e: logging.warning(e) # If can't reconstruct the eval, or don't have thresholds defined, # return True as a signal to deploy. # TODO: is this the right default? - return 'deploy' + return ('deploy',) def _serialize_str(str_value: str) -> str: if not isinstance(str_value, str): diff --git a/ml/automl/tables/kfp_e2e/tables_pipeline_caip.py b/ml/automl/tables/kfp_e2e/tables_pipeline_caip.py index b5938bd..5ac91d9 100644 --- a/ml/automl/tables/kfp_e2e/tables_pipeline_caip.py +++ b/ml/automl/tables/kfp_e2e/tables_pipeline_caip.py @@ -130,7 +130,7 @@ def automl_tables( #pylint: disable=unused-argument eval_data=eval_model.outputs['eval_data'], ) - with dsl.Condition(eval_metrics.outputs['deploy'] == 'd'): + with dsl.Condition(eval_metrics.outputs['deploy'] == 'deploy'): deploy_model = deploy_model_op( gcp_project_id=gcp_project_id, gcp_region=gcp_region, diff --git a/ml/automl/tables/kfp_e2e/tables_pipeline_caip.py.tar.gz b/ml/automl/tables/kfp_e2e/tables_pipeline_caip.py.tar.gz index 7a65e79c7b73ced8f170f1ca4e5e22eea6203935..a125f4095b4f18471f06fe37850ac1ac236da334 100644 GIT binary patch delta 8551 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 Oj?B2x^an-^7Uq8!(b{bQ delta 8539 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 U`?OE{eA4It0fl(ZIsiZc0P?M#XaE2J literal 9013 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 bX`l9KpY~~=_GzE?*~|0)daSY$06+l%KS`9*