From 00b407e68cc5845da010065eb34cb7f933a94149 Mon Sep 17 00:00:00 2001 From: The Google Earth Engine Community Authors Date: Wed, 3 Jan 2024 15:35:52 -0800 Subject: [PATCH] Add AutoML + Vertex AI demo to examples page. PiperOrigin-RevId: 595520974 --- .../Earth_Engine_AutoML_Vertex_AI.ipynb | 597 ++++++++++++++++++ 1 file changed, 597 insertions(+) create mode 100644 guides/linked/Earth_Engine_AutoML_Vertex_AI.ipynb diff --git a/guides/linked/Earth_Engine_AutoML_Vertex_AI.ipynb b/guides/linked/Earth_Engine_AutoML_Vertex_AI.ipynb new file mode 100644 index 000000000..2bf5c0e55 --- /dev/null +++ b/guides/linked/Earth_Engine_AutoML_Vertex_AI.ipynb @@ -0,0 +1,597 @@ +{ + "nbformat": 4, + "nbformat_minor": 0, + "metadata": { + "colab": { + "private_outputs": true, + "provenance": [], + "toc_visible": true + }, + "kernelspec": { + "name": "python3", + "display_name": "Python 3" + }, + "gpuClass": "standard" + }, + "cells": [ + { + "cell_type": "code", + "metadata": { + "id": "fSIfBsgi8dNK" + }, + "source": [ + "#@title Copyright 2023 Google LLC. { display-mode: \"form\" }\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "aV1xZ1CPi3Nw" + }, + "source": [ + "
\n", + "\n", + " Run in Google Colab\n", + "\n", + " View source on GitHub
\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "AC8adBmw-5m3" + }, + "source": [ + "# Crop Classification with AutoML and Earth Engine\n", + "\n", + "This notebook demonstrates how to use the Vertex AI Python SDK to train and deploy an AutoML model and then connect to it from Earth Engine using `ee.model.fromVertexAi` with the `RAW_JSON` payloadFormat parameter.\n", + "\n", + "\n", + "**Note:** This demo was presented at Geo for Good 2023 but used the Google Cloud Console UI and the Earth Engine Code Editor rather than a Python Colab notebook. See the [recording](https://youtu.be/_BjtxPSO1Ho?t=4068) and the [slides](https://docs.google.com/presentation/d/1e5ppyNts-KJx1YIawP-04QxkAXelOrjzKJ0HPpwNILY/edit#slide=id.g28dc28cb8c7_0_0) for that presentation.\n", + "\n", + "This demo was adapted from that demo as well as the [AutoML Text Classification](https://colab.research.google.com/github/GoogleCloudPlatform/vertex-ai-samples/blob/main/notebooks/official/automl/automl-text-classification.ipynb) example.\n", + "\n", + "**Running this demo may incur charges to your Google Cloud Account!**" + ] + }, + { + "cell_type": "markdown", + "source": [ + "# Set up" + ], + "metadata": { + "id": "pGOTiIJncGWu" + } + }, + { + "cell_type": "code", + "source": [ + "from google.colab import auth\n", + "\n", + "from google.cloud import aiplatform, storage\n", + "from google.cloud.aiplatform import jobs" + ], + "metadata": { + "id": "MZSiZMBic27f" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "PROJECT_ID = \"[your-project-id]\" # @param {type:\"string\"}\n", + "\n", + "REGION = \"us-central1\" # @param {type: \"string\"}\n", + "\n", + "# The diplay name of your model (this can be any string).\n", + "MODEL_NAME = \"[model-name]\" # @param {type: \"string\"}" + ], + "metadata": { + "id": "nWUKF-AEcIO5" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "# Authenticate the notebook.\n", + "auth.authenticate_user()\n", + "\n", + "# Set the gcloud project.\n", + "! gcloud config set project $PROJECT_ID\n", + "\n", + "# Initialize the Vertex AI Python SDK.\n", + "aiplatform.init(project=PROJECT_ID, location=REGION)" + ], + "metadata": { + "id": "fRrt_UUIdXr6" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "# Create a Vertex AI `Dataset` resource" + ], + "metadata": { + "id": "ENqBnp1TR402" + } + }, + { + "cell_type": "markdown", + "source": [ + "The training data you use to train your model depends on your objective and can come from a variety of places. In this demo, we generated the training data using the Code Editor and exported it to GCS as a CSV file.\n", + "\n", + "See the script used to generate and export the data here: https://code.earthengine.google.com/a3304c5bb365d32d9692ec0e4011641c" + ], + "metadata": { + "id": "tYM1oCNCKYdV" + } + }, + { + "cell_type": "code", + "source": [ + "# The GCS bucket the Code Editor script exported the training data to.\n", + "GCS_BUCKET = 'gs://ee-docs-demos/object_based_classififcation_demo_45e12596da31631aa2e785997346b491.csv'" + ], + "metadata": { + "id": "_T5g9vAAKbKs" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "Once the data is stored in GCS, you can use the Vertex AI Python SDK to create a `TabularDataset`, which is what you'll use to train the model in the next section.\n", + "\n", + "Creating data is a long-running operation. This next step can take a while. The `create()` method waits for the operation to complete, outputting statements as the operation progresses. The statements contain the full name of the dataset that you use in the following section.\n", + "\n", + "**Note**: You can close the noteboook while you wait for this operation to complete." + ], + "metadata": { + "id": "A1ZdO3ueKLsd" + } + }, + { + "cell_type": "code", + "source": [ + "dataset = aiplatform.TabularDataset.create(\n", + " display_name='demo-dataset',\n", + " gcs_source=GCS_BUCKET\n", + ")" + ], + "metadata": { + "id": "Tu4cny3KR7qh" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "# Train model" + ], + "metadata": { + "id": "WzucpMqKO-8X" + } + }, + { + "cell_type": "markdown", + "source": [ + "Now you can begin training your model. Training the model is a two part process:\n", + "\n", + "1. **Define the training job.** You must provide a display name and the type of training you want when you define the training job.\n", + "2. **Run the training job.** When you run the training job, you need to supply a reference to the dataset to use for training. At this step, you can also configure the data split percentages.\n", + "\n", + "You do not need to specify [data splits](https://cloud.google.com/vertex-ai/docs/general/ml-use). The training job has a default setting of training 80%/ testing 10%/ validate 10% if you don't provide these values.\n", + "\n", + "As with importing data into the dataset, training your model can take a substantial amount of time. The client library prints out operation status messages while the training pipeline operation processes. You must wait for the training process to complete before you can get the resource name and ID of your new model, which is required for model evaluation and model deployment.\n", + "\n", + "**Note:** You can close the notebook while you wait for the operation to complete." + ], + "metadata": { + "id": "Rh1m5O24mha9" + } + }, + { + "cell_type": "code", + "source": [ + "# Define the training job\n", + "job = aiplatform.AutoMLTabularTrainingJob(\n", + " display_name='demo-training-job',\n", + " optimization_prediction_type=\"classification\"\n", + ")" + ], + "metadata": { + "id": "g0-cAQtYPA7G" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "# Run the training job (this may take a few hours to complete).\n", + "model = job.run(\n", + " dataset=dataset,\n", + " model_display_name=MODEL_NAME,\n", + " target_column = 'crop_index',\n", + " sync=True\n", + ")" + ], + "metadata": { + "id": "QqPDuLc4e8U5" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "# Evaluate model" + ], + "metadata": { + "id": "3in30dayPBS2" + } + }, + { + "cell_type": "markdown", + "source": [ + "After your model training has finished, you can review the evaluation scores for it using the list_model_evaluations() method. This method will return an iterator for each evaluation slice. You can see further evaluation details by visiting the Vertex AI section of the Google Cloud Console." + ], + "metadata": { + "id": "FowiNPL1ls3-" + } + }, + { + "cell_type": "code", + "source": [ + "model_evaluations = model.list_model_evaluations()\n", + "\n", + "for model_evaluation in model_evaluations:\n", + " print(model_evaluation.to_dict())" + ], + "metadata": { + "id": "FwTeDn6qPFtT" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "# Deploy to Vertex AI" + ], + "metadata": { + "id": "msy_hCJHd89F" + } + }, + { + "cell_type": "markdown", + "source": [ + "Now you can deploy the model to an endpoint! Deploying to an endpoint is necessary for actually connecting to the model from Earth Engine and getting online predictions from it. When you deploy the model to an endpoint, a copy of the model is made on the endpoint with a new resource name and display name." + ], + "metadata": { + "id": "7GlFWaE24vCO" + } + }, + { + "cell_type": "code", + "source": [ + "endpoint = model.deploy(deployed_model_display_name=MODEL_NAME, sync=True)\n", + "\n", + "# Print the endpoint ID once the model is deployed.\n", + "ENDPOINT_ID = endpoint.resource_name\n", + "ENDPOINT_ID" + ], + "metadata": { + "id": "O3SDIj_fjuvk" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5aTGza-rWIjp" + }, + "source": [ + "# Connect to the model from Earth Engine\n", + "\n", + "Now that the model is deployed to an endpoint, you can get online predictions from it. From Earth Engine, you will prepare the prediction input to send to your model, connect to the hosted model and get predictions from it, then visualize the results." + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Set up and authentication" + ], + "metadata": { + "id": "mQlFm5qgNlFU" + } + }, + { + "cell_type": "code", + "source": [ + "import google\n", + "import geemap\n", + "import ee\n", + "\n", + "# Authenticate to Earth Engine.\n", + "credentials, _ = google.auth.default()\n", + "ee.Initialize(credentials, project=PROJECT_ID, opt_url='https://earthengine-highvolume.googleapis.com')" + ], + "metadata": { + "id": "cyzXFTbeQvdl" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "# The region used to generate training data. This is used to determine spectral\n", + "# stats of the prediction input, and also defines the possible class values of our model.\n", + "trainingRegion = ee.Geometry.Polygon(\n", + " [[[-121.89511299133301, 38.98496606984683],\n", + " [-121.89511299133301, 38.909335196675435],\n", + " [-121.69358253479004, 38.909335196675435],\n", + " [-121.69358253479004, 38.98496606984683]]], None, False)\n", + "\n", + "\n", + "# The geometry of the field we want to get predictions for. We'll add additional\n", + "# properties to this FeatureCollection before sending it to Vertex.\n", + "predictInput = ee.FeatureCollection(\n", + " [ee.Feature(\n", + " ee.Geometry.Polygon(\n", + " [[[-121.79220199584975, 38.96437397212627],\n", + " [-121.79220199584975, 38.953228092306446],\n", + " [-121.78310394287124, 38.953228092306446],\n", + " [-121.78310394287124, 38.96437397212627]]], None, False),\n", + " {\n", + " \"system:index\": \"0\"\n", + " })]);\n", + "\n", + "# The properties that will be sent to Vertex for prediction.\n", + "PREDICTORS = [\n", + " \"R\", \"G\", \"B\", \"N\", \"R_mean\", \"G_mean\", \"B_mean\", \"N_mean\",\n", + " \"area\", \"perimeter\", \"width\", \"height\"]\n", + "\n", + "naipImage = ee.ImageCollection('USDA/NAIP/DOQQ').filterDate('2020-01-01', '2021-01-01').filterBounds(trainingRegion).mosaic()\n", + "naipImage = ee.Image(naipImage).clip(trainingRegion).divide(255).select(['R', 'G', 'B', 'N'])\n", + "\n", + "cdl = ee.Image(\"USDA/NASS/CDL/2020\")" + ], + "metadata": { + "id": "1JxlwpL8OEVC" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "###Prepare the prediction input" + ], + "metadata": { + "id": "lANTzN22msh0" + } + }, + { + "cell_type": "code", + "source": [ + "'''Adds additional metadata (area, perimeter, width, height) to a feature.'''\n", + "def addProperties(f):\n", + " spectralStats = naipImage.reduceRegion(\n", + " reducer= ee.Reducer.mean().combine(\n", + " reducer2= ee.Reducer.stdDev(),\n", + " sharedInputs= True\n", + " ),\n", + " geometry= f.geometry(),\n", + " scale= 1\n", + " ).rename(\n", + " ['B_stdDev', 'G_stdDev', 'R_stdDev', 'N_stdDev'],\n", + " ['B', 'G', 'R', 'N']\n", + " )\n", + " coords = ee.Image.pixelLonLat().reduceRegion(\n", + " reducer= ee.Reducer.minMax(),\n", + " geometry= f.geometry(),\n", + " scale= 1\n", + " )\n", + " height = ee.Number(coords.get('latitude_max')).subtract(coords.get('latitude_min'))\n", + " width = ee.Number(coords.get('longitude_max')).subtract(coords.get('longitude_min'))\n", + " area = f.geometry().area(1)\n", + " perimeter = f.geometry().perimeter(1)\n", + " return f.set(spectralStats).set({\n", + " 'area': area,\n", + " 'perimeter': perimeter,\n", + " 'width': width,\n", + " 'height': height\n", + " })\n", + "\n", + "'''Converts a given property (p) of a feature (f) to a string'''\n", + "def toString(p):\n", + " def f(f):\n", + " return f.set(p, ee.String.encodeJSON(f.get(p)))\n", + " return f\n", + "\n", + "# Add the properties that the model expects.\n", + "predictInput = predictInput.map(addProperties)\n", + "\n", + "# Convert the feature values to strings, which is necessary for Auto ML prediction.\n", + "for i in range(len(PREDICTORS)):\n", + " predictInput = predictInput.map(toString(PREDICTORS[i]))" + ], + "metadata": { + "id": "zdGblsd2X9zt" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "### Get predictions" + ], + "metadata": { + "id": "HGeGnisJnM6g" + } + }, + { + "cell_type": "code", + "source": [ + "# Connect to the hosted model.\n", + "vertex_model = ee.Model.fromVertexAi(\n", + " endpoint=ENDPOINT_ID,\n", + " inputProperties=PREDICTORS,\n", + " outputProperties={\n", + " 'classes': {'type': ee.PixelType.float(), 'dimensions': 1},\n", + " 'scores': {'type': ee.PixelType.float(), 'dimensions': 1},\n", + " },\n", + " payloadFormat='RAW_JSON'\n", + " )\n", + "\n", + "\n", + "predictions = vertex_model.predictProperties(predictInput)\n", + "\n", + "# Print the raw predictions returned from Vertex.\n", + "print(predictions.getInfo())" + ], + "metadata": { + "id": "jjsbdhUGApFU" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "### Postprocess and visualize predictions" + ], + "metadata": { + "id": "2IBVKBqynbSv" + } + }, + { + "cell_type": "markdown", + "source": [ + "When we generated the training data, we created a FeatureCollection of only the commonly occurring crops in our region of interest and used only those crops to train the model. Now you'll re-create that FeatureCollection so that you have a mapping of the possible classes returned from Vertex and what crop name each maps to." + ], + "metadata": { + "id": "CoK2a5-umKTF" + } + }, + { + "cell_type": "code", + "source": [ + "names = ee.List(cdl.get('cropland_class_names'))\n", + "values = ee.List(cdl.get('cropland_class_values'))\n", + "cropland = cdl.select('cropland').int()\n", + "cropMask = cropland.lt(100)\n", + "\n", + "# Get the set of crops present and their frequencies.\n", + "cropFrequencies= ee.Dictionary(cropland.updateMask(cropMask).reduceRegion(\n", + " reducer= ee.Reducer.frequencyHistogram(),\n", + " geometry= trainingRegion,\n", + " scale= 30\n", + ").get('cropland'))\n", + "\n", + "# Keep only the commonly occurring crops.\n", + "cropsFeatures = ee.FeatureCollection(cropFrequencies.map(\n", + " lambda k, v:\n", + " ee.Feature(None, {'code': ee.Number.parse(k).int(), 'size': v})\n", + ").values())\n", + "cropsFeatures = cropsFeatures.filter(ee.Filter.gt('size', 1000))\n", + "\n", + "# Make the crops codes consecutive integers.\n", + "indicesArray = cropsFeatures.aggregate_array('code')\n", + "\n", + "# Create a FeatureCollection of the final crops we're interested in.\n", + "# This will be used to lookup the name of a crop for an index.\n", + "cropsFeatures = cropsFeatures.map(\n", + " lambda f:\n", + " f.set({\n", + " 'name': names.get(values.indexOf(ee.Number(f.get('code')).int())),\n", + " 'index': indicesArray.indexOf(ee.Number(f.get('code')).int())\n", + " })\n", + ")" + ], + "metadata": { + "id": "8ahI_GjunZnu" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "# There should only be one element in the returned FeatureCollection, so we can extract it by calling first().\n", + "prediction = predictions.first()\n", + "\n", + "# Determine the name of crop that corresponds to the index returned from Vertex.\n", + "predictedClassIndex = ee.Array(prediction.get('classes')).get(ee.Array(prediction.get('scores')).argmax())\n", + "predictedClassName = cropsFeatures.filter(ee.Filter.eq('index', predictedClassIndex)).first().get('name')\n", + "\n", + "print(predictedClassName.getInfo())\n", + "\n", + "# Add the name to the FeatureCollection for visualizing on a Map.\n", + "processedFc = predictInput.map(\n", + " lambda f:\n", + " f.set({\n", + " 'crop_name': predictedClassName\n", + " })\n", + ")" + ], + "metadata": { + "id": "K9rdnmRKaKBX" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "# Visualize the base imagery and the predicted crop cover.\n", + "Map = geemap.Map(zoom=15)\n", + "Map.centerObject(trainingRegion)\n", + "\n", + "Map.addLayer(naipImage)\n", + "\n", + "style = {'color': 'yellow', 'fillColor': \"orange\"}\n", + "Map.addLayer(processedFc.style(**style))\n", + "\n", + "Map.add_labels(\n", + " processedFc,\n", + " \"crop_name\",\n", + " font_size=\"12pt\",\n", + " font_family=\"arial\",\n", + " font_weight=\"bold\",\n", + ")\n", + "\n", + "Map" + ], + "metadata": { + "id": "nJ4gBq0rno8Q" + }, + "execution_count": null, + "outputs": [] + } + ] +} \ No newline at end of file