[community-content] PyTorch on Google Cloud Vertex AI - Blog related notebook and scripts (#23)
* pytorch on vertex: initial commit * pytorch on vertex: reorg dir structure with new repo changes * pytorch on vertex: add cleanup script and update README files * pytorch on vertex: remove references to bucket names * PyTorch on Vertex: Updated with linter suggested changes * PyTorch on Vertex: dry-run and set resource names consistent with app name * PyTorch on Vertex: update CODEOWNERS * Pytorch on Vertex: Reorganized directory structure
@@ -0,0 +1,20 @@
|
|||||||
|
# PyTorch on Google Cloud: Text Classification
|
||||||
|
|
||||||
|
In the PyTorch on Google Cloud series of blog posts, we aim to share how to build, train and deploy PyTorch models at scale and how to create reproducible machine learning pipelines on Google Cloud with [Vertex AI](https://cloud.google.com/vertex-ai).
|
||||||
|
|
||||||
|
This tutorial on text classification shows how to train a PyTorch based text classification model by fine tuning a pre-trained Huggingface Transformers model and deploy the model on [Vertex AI](https://cloud.google.com/vertex-ai/docs/start/client-libraries#python) using Vertex SDK and [`gcloud ai`](https://cloud.google.com/sdk/gcloud/reference/beta/ai).
|
||||||
|
|
||||||
|
## Notebooks
|
||||||
|
|
||||||
|
| <h4>Notebook</h4> | <h4>Description</h4> |
|
||||||
|
| :-------- | :------- |
|
||||||
|
| [pytorch-text-classification-vertex-ai-train-tune-deploy.ipynb](./pytorch-text-classification-vertex-ai-train-tune-deploy.ipynb) | Notebook to show training, hyper-parameter tuning and deploying a PyTorch model on Vertex AI |
|
||||||
|
|
||||||
|
## Folders
|
||||||
|
|
||||||
|
|
||||||
|
| <h4>Folder Name</h4> | <h4>Description</h4> |
|
||||||
|
| :-------- | :------- |
|
||||||
|
| [`python_package`](./python_package) | Folder with scripts to train and tune the text classification model using PyTorch and Hugging Face Transformers. In the [notebook](./pytorch-text-classification-vertex-ai-train-tune-deploy.ipynb), this folder is used for submitting a training job on Vertex AI using pre-built PyTorch containers. |
|
||||||
|
| [`custom_container`](./custom_container) | Folder with reference to training scripts in [`python_package`](./python_package)folder including a `Dockerfile` to build a custom container. In the [notebook](./pytorch-text-classification-vertex-ai-train-tune-deploy.ipynb), this folder is used for submitting a training job and hyper-parameter tuning job on Vertex AI using custom containers. |
|
||||||
|
| [`predictor`](./predictor) | Folder with TorchServe prediction handler and Dockerfile to build a custom container with TorchServe. In the [notebook](./pytorch-text-classification-vertex-ai-train-tune-deploy.ipynb), this folder is used for deploying a PyTorch model on Vertex AI using custom containers by running [TorchServe HTTP server](https://pytorch.org/serve/) |
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
|
||||||
|
# Use pytorch GPU base image
|
||||||
|
FROM gcr.io/cloud-aiplatform/training/pytorch-gpu.1-7
|
||||||
|
|
||||||
|
# set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install required packages
|
||||||
|
RUN pip install google-cloud-storage transformers datasets tqdm cloudml-hypertune
|
||||||
|
|
||||||
|
# Copies the trainer code to the docker image.
|
||||||
|
COPY ./trainer/__init__.py /app/trainer/__init__.py
|
||||||
|
COPY ./trainer/experiment.py /app/trainer/experiment.py
|
||||||
|
COPY ./trainer/utils.py /app/trainer/utils.py
|
||||||
|
COPY ./trainer/metadata.py /app/trainer/metadata.py
|
||||||
|
COPY ./trainer/model.py /app/trainer/model.py
|
||||||
|
COPY ./trainer/task.py /app/trainer/task.py
|
||||||
|
|
||||||
|
# Set up the entry point to invoke the trainer.
|
||||||
|
ENTRYPOINT ["python", "-m", "trainer.task"]
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# PyTorch Custom Containers GPU Template
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The directory provides code to fine tune a transformer model ([BERT-base](https://huggingface.co/bert-base-cased)) from Huggingface Transformers Library for sentiment analysis task. [BERT](https://ai.googleblog.com/2018/11/open-sourcing-bert-state-of-art-pre.html) (Bidirectional Encoder Representations from Transformers) is a transformers model pre-trained on a large corpus of unlabeled text in a self-supervised fashion. In this sample, we use [IMDB sentiment classification dataset](https://huggingface.co/datasets/imdb) for the task. We show you packaging a PyTorch training model to submit it to Vertex AI using pre-built PyTorch containers and handling Python dependencies using [Vertex Training custom containers](https://cloud.google.com/vertex-ai/docs/training/create-custom-container?hl=hr).
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
* Setup your project by following the instructions from [documentation](https://cloud.google.com/vertex-ai/docs/start/cloud-environment)
|
||||||
|
* [Setup docker with Cloud Container Registry](https://cloud.google.com/container-registry/docs/pushing-and-pulling)
|
||||||
|
* Change the directory to this sample and run
|
||||||
|
|
||||||
|
`Note:` These instructions are used for local testing. When you submit a training job, no code will be executed on your local machine.
|
||||||
|
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
* `trainer` directory: all Python modules to train the model.
|
||||||
|
* `scripts` directory: command-line scripts to train the model on Vertex AI.
|
||||||
|
* `setup.py`: `setup.py` scripts specifies Python dependencies required for the training job. Vertex Training uses pip to install the package on the training instances allocated for the job.
|
||||||
|
|
||||||
|
### Trainer Modules
|
||||||
|
| File Name | Purpose |
|
||||||
|
| :-------- | :------ |
|
||||||
|
| [metadata.py](trainer/metadata.py) | Defines: metadata for classification task such as predefined model dataset name, target labels. |
|
||||||
|
| [utils.py](trainer/utils.py) | Includes: utility functions such as data input functions to read data, save model to GCS bucket. |
|
||||||
|
| [model.py](trainer/model.py) | Includes: function to create model with a sequence classification head from a pretrained model. |
|
||||||
|
| [experiment.py](trainer/experiment.py) | Runs the model training and evaluation experiment, and exports the final model. |
|
||||||
|
| [task.py](trainer/task.py) | Includes: 1) Initialize and parse task arguments (hyper parameters), and 2) Entry point to the trainer. |
|
||||||
|
|
||||||
|
### Scripts
|
||||||
|
|
||||||
|
* [train-cloud.sh](scripts/train-cloud.sh) This script builds your Docker image locally, pushes the image to Container Registry and submits a custom container training job to Vertex AI.
|
||||||
|
|
||||||
|
Please read the [documentation](https://cloud.google.com/vertex-ai/docs/training/containers-overview?hl=hr) on Vertex Training with Custom Containers for more details.
|
||||||
|
|
||||||
|
## How to run
|
||||||
|
|
||||||
|
Once the prerequisites are satisfied, you may:
|
||||||
|
|
||||||
|
1. For local testing, run (refer [notebook](../pytorch-text-classification-vertex-ai-train-tune-deploy.ipynb) for instructions):
|
||||||
|
```
|
||||||
|
CUSTOM_TRAIN_IMAGE_URI='gcr.io/{PROJECT_ID}/pytorch_gpu_train_{APP_NAME}'
|
||||||
|
cd ./custom_container/ && docker build -f Dockerfile -t $CUSTOM_TRAIN_IMAGE_URI ../python_package
|
||||||
|
docker run --gpus all -it --rm $CUSTOM_TRAIN_IMAGE_URI
|
||||||
|
```
|
||||||
|
2. For cloud testing, run:
|
||||||
|
```
|
||||||
|
source ./scripts/train-cloud.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run on GPU
|
||||||
|
The provided trainer code runs on a GPU if one is available including data loading and model creation.
|
||||||
|
|
||||||
|
To run the trainer code on a different GPU configuration or latest PyTorch pre-built container image, make the following changes to the trainer script.
|
||||||
|
* Update the PyTorch image URI to one of [PyTorch pre-built containers](https://cloud.google.com/vertex-ai/docs/training/pre-built-containers#available_container_images)
|
||||||
|
* Update the [`worker-pool-spec`](https://cloud.google.com/vertex-ai/docs/training/configure-compute?hl=hr) in the gcloud command that includes a GPU
|
||||||
|
|
||||||
|
Then, run the script to submit a Custom Job on Vertex Training job:
|
||||||
|
```
|
||||||
|
source ./scripts/train-cloud.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Versions
|
||||||
|
This script uses the pre-built PyTorch containers for PyTorch 1.7.
|
||||||
|
* `us-docker.pkg.dev/vertex-ai/training/pytorch-gpu.1-7:latest`
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Copyright 2019 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.
|
||||||
|
# ==============================================================================
|
||||||
|
# This script performs cloud training for a PyTorch model.
|
||||||
|
|
||||||
|
echo "Submitting PyTorch model training job to Vertex AI"
|
||||||
|
|
||||||
|
# PROJECT_ID: Change to your project id
|
||||||
|
PROJECT_ID=$(gcloud config list --format 'value(core.project)')
|
||||||
|
|
||||||
|
# BUCKET_NAME: Change to your bucket name.
|
||||||
|
BUCKET_NAME="[your-bucket-name]" # <-- CHANGE TO YOUR BUCKET NAME
|
||||||
|
BUCKET_NAME=cloud-ai-platform-2f444b6a-a742-444b-b91a-c7519f51bd77
|
||||||
|
|
||||||
|
# JOB_NAME: the name of your job running on AI Platform.
|
||||||
|
JOB_PREFIX="finetuned-bert-classifier-pytorch-cstm-cntr-"
|
||||||
|
JOB_NAME=${JOB_PREFIX}-$(date +%Y%m%d%H%M%S)-custom-job
|
||||||
|
|
||||||
|
# This can be a GCS location to a zipped and uploaded package
|
||||||
|
PACKAGE_PATH=./trainer
|
||||||
|
|
||||||
|
# REGION: select a region from https://cloud.google.com/vertex-ai/docs/general/locations#available_regions
|
||||||
|
# or use the default '`us-central1`'. The region is where the job will be run.
|
||||||
|
REGION="us-central1"
|
||||||
|
|
||||||
|
# JOB_DIR: Where to store prepared package and upload output model.
|
||||||
|
JOB_DIR=gs://${BUCKET_NAME}/${JOB_PREFIX}/models/${JOB_NAME}
|
||||||
|
|
||||||
|
# IMAGE_REPO_NAME: set a local repo name to distinquish our image
|
||||||
|
IMAGE_REPO_NAME=pytorch_gpu_train_finetuned-bert-classifier
|
||||||
|
|
||||||
|
# IMAGE_TAG: an easily identifiable tag for your docker image
|
||||||
|
IMAGE_TAG=latest
|
||||||
|
|
||||||
|
# IMAGE_URI: the complete URI location for Cloud Container Registry
|
||||||
|
CUSTOM_TRAIN_IMAGE_URI=gcr.io/${PROJECT_ID}/${IMAGE_REPO_NAME}:${IMAGE_TAG}
|
||||||
|
|
||||||
|
# Build the docker image
|
||||||
|
docker build --no-cache -f Dockerfile -t $CUSTOM_TRAIN_IMAGE_URI ../python_package
|
||||||
|
|
||||||
|
# Deploy the docker image to Cloud Container Registry
|
||||||
|
docker push ${CUSTOM_TRAIN_IMAGE_URI}
|
||||||
|
|
||||||
|
# Submit Custom Job to Vertex AI
|
||||||
|
gcloud beta ai custom-jobs create \
|
||||||
|
--display-name=${JOB_NAME} \
|
||||||
|
--region ${REGION} \
|
||||||
|
--worker-pool-spec=replica-count=1,machine-type='n1-standard-8',accelerator-type='NVIDIA_TESLA_V100',accelerator-count=1,container-image-uri=${CUSTOM_TRAIN_IMAGE_URI} \
|
||||||
|
--args="--model-name","finetuned-bert-classifier","--job-dir",$JOB_DIR
|
||||||
|
|
||||||
|
echo "After the job is completed successfully, model files will be saved at $JOB_DIR/"
|
||||||
|
|
||||||
|
# uncomment following lines to monitor the job progress by streaming logs
|
||||||
|
|
||||||
|
# Stream the logs from the job
|
||||||
|
# gcloud ai custom-jobs stream-logs $(gcloud ai custom-jobs list --region=$REGION --filter="displayName:"$JOB_NAME --format="get(name)")
|
||||||
|
|
||||||
|
# # Verify the model was exported
|
||||||
|
# echo "Verify the model was exported:"
|
||||||
|
# gsutil ls ${JOB_DIR}/
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
../python_package/trainer
|
||||||
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 94 KiB |
@@ -0,0 +1,27 @@
|
|||||||
|
|
||||||
|
FROM pytorch/torchserve:latest-cpu
|
||||||
|
|
||||||
|
# install dependencies
|
||||||
|
RUN pip3 install transformers
|
||||||
|
|
||||||
|
# copy model artifacts, custom handler and other dependencies
|
||||||
|
COPY ./custom_text_handler.py /home/model-server/
|
||||||
|
COPY ./index_to_name.json /home/model-server/
|
||||||
|
COPY ./model/finetuned-bert-classifier/ /home/model-server/
|
||||||
|
|
||||||
|
# create torchserve configuration file
|
||||||
|
USER root
|
||||||
|
RUN printf "\nservice_envelope=json" >> /home/model-server/config.properties
|
||||||
|
RUN printf "\ninference_address=http://0.0.0.0:7080" >> /home/model-server/config.properties
|
||||||
|
RUN printf "\nmanagement_address=http://0.0.0.0:7081" >> /home/model-server/config.properties
|
||||||
|
USER model-server
|
||||||
|
|
||||||
|
# expose health and prediction listener ports from the image
|
||||||
|
EXPOSE 7080
|
||||||
|
EXPOSE 7081
|
||||||
|
|
||||||
|
# create model archive file packaging model artifacts and dependencies
|
||||||
|
RUN torch-model-archiver -f --model-name=finetuned-bert-classifier --version=1.0 --serialized-file=/home/model-server/pytorch_model.bin --handler=/home/model-server/custom_text_handler.py --extra-files "/home/model-server/config.json,/home/model-server/tokenizer.json,/home/model-server/training_args.bin,/home/model-server/tokenizer_config.json,/home/model-server/special_tokens_map.json,/home/model-server/vocab.txt,/home/model-server/index_to_name.json" --export-path=/home/model-server/model-store
|
||||||
|
|
||||||
|
# run Torchserve HTTP serve to respond to prediction requests
|
||||||
|
CMD ["torchserve", "--start", "--ts-config=/home/model-server/config.properties", "--models", "finetuned-bert-classifier=finetuned-bert-classifier.mar", "--model-store", "/home/model-server/model-store"]
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import torch
|
||||||
|
from transformers import AutoModelForSequenceClassification, AutoTokenizer
|
||||||
|
from ts.torch_handler.base_handler import BaseHandler
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class TransformersClassifierHandler(BaseHandler):
|
||||||
|
"""
|
||||||
|
The handler takes an input string and returns the classification text
|
||||||
|
based on the serialized transformers checkpoint.
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
super(TransformersClassifierHandler, self).__init__()
|
||||||
|
self.initialized = False
|
||||||
|
|
||||||
|
def initialize(self, ctx):
|
||||||
|
""" Loads the model.pt file and initialized the model object.
|
||||||
|
Instantiates Tokenizer for preprocessor to use
|
||||||
|
Loads labels to name mapping file for post-processing inference response
|
||||||
|
"""
|
||||||
|
self.manifest = ctx.manifest
|
||||||
|
|
||||||
|
properties = ctx.system_properties
|
||||||
|
model_dir = properties.get("model_dir")
|
||||||
|
self.device = torch.device("cuda:" + str(properties.get("gpu_id")) if torch.cuda.is_available() else "cpu")
|
||||||
|
|
||||||
|
# Read model serialize/pt file
|
||||||
|
serialized_file = self.manifest["model"]["serializedFile"]
|
||||||
|
model_pt_path = os.path.join(model_dir, serialized_file)
|
||||||
|
if not os.path.isfile(model_pt_path):
|
||||||
|
raise RuntimeError("Missing the model.pt or pytorch_model.bin file")
|
||||||
|
|
||||||
|
# Load model
|
||||||
|
self.model = AutoModelForSequenceClassification.from_pretrained(model_dir)
|
||||||
|
self.model.to(self.device)
|
||||||
|
self.model.eval()
|
||||||
|
logger.debug('Transformer model from path {0} loaded successfully'.format(model_dir))
|
||||||
|
|
||||||
|
# Ensure to use the same tokenizer used during training
|
||||||
|
self.tokenizer = AutoTokenizer.from_pretrained('bert-base-cased')
|
||||||
|
|
||||||
|
# Read the mapping file, index to object name
|
||||||
|
mapping_file_path = os.path.join(model_dir, "index_to_name.json")
|
||||||
|
|
||||||
|
if os.path.isfile(mapping_file_path):
|
||||||
|
with open(mapping_file_path) as f:
|
||||||
|
self.mapping = json.load(f)
|
||||||
|
else:
|
||||||
|
logger.warning('Missing the index_to_name.json file. Inference output will not include class name.')
|
||||||
|
|
||||||
|
self.initialized = True
|
||||||
|
|
||||||
|
def preprocess(self, data):
|
||||||
|
""" Preprocessing input request by tokenizing
|
||||||
|
Extend with your own preprocessing steps as needed
|
||||||
|
"""
|
||||||
|
text = data[0].get("data")
|
||||||
|
if text is None:
|
||||||
|
text = data[0].get("body")
|
||||||
|
sentences = text.decode('utf-8')
|
||||||
|
logger.info("Received text: '%s'", sentences)
|
||||||
|
|
||||||
|
# Tokenize the texts
|
||||||
|
tokenizer_args = ((sentences,))
|
||||||
|
inputs = self.tokenizer(*tokenizer_args,
|
||||||
|
padding='max_length',
|
||||||
|
max_length=128,
|
||||||
|
truncation=True,
|
||||||
|
return_tensors = "pt")
|
||||||
|
return inputs
|
||||||
|
|
||||||
|
def inference(self, inputs):
|
||||||
|
""" Predict the class of a text using a trained transformer model.
|
||||||
|
"""
|
||||||
|
prediction = self.model(inputs['input_ids'].to(self.device))[0].argmax().item()
|
||||||
|
|
||||||
|
if self.mapping:
|
||||||
|
prediction = self.mapping[str(prediction)]
|
||||||
|
|
||||||
|
logger.info("Model predicted: '%s'", prediction)
|
||||||
|
return [prediction]
|
||||||
|
|
||||||
|
def postprocess(self, inference_output):
|
||||||
|
return inference_output
|
||||||
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
|
||||||
|
{
|
||||||
|
"0": "Negative",
|
||||||
|
"1": "Positive"
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# PyTorch - Python Package Training
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The directory provides code to fine tune a transformer model ([BERT-base](https://huggingface.co/bert-base-cased)) from Huggingface Transformers Library for sentiment analysis task. [BERT](https://ai.googleblog.com/2018/11/open-sourcing-bert-state-of-art-pre.html) (Bidirectional Encoder Representations from Transformers) is a transformers model pre-trained on a large corpus of unlabeled text in a self-supervised fashion. In this sample, we use [IMDB sentiment classification dataset](https://huggingface.co/datasets/imdb) for the task. We show you packaging a PyTorch training model to submit it to Vertex AI using pre-built PyTorch containers and handling Python dependencies through Python build scripts (`setup.py`).
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
* Setup your project by following the instructions from [documentation](https://cloud.google.com/vertex-ai/docs/start/cloud-environment)
|
||||||
|
* Change directories to this sample.
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
* `trainer` directory: all Python modules to train the model.
|
||||||
|
* `scripts` directory: command-line scripts to train the model on Vertex AI.
|
||||||
|
* `setup.py`: `setup.py` scripts specifies Python dependencies required for the training job. Vertex Training uses pip to install the package on the training instances allocated for the job.
|
||||||
|
|
||||||
|
### Trainer Modules
|
||||||
|
| File Name | Purpose |
|
||||||
|
| :-------- | :------ |
|
||||||
|
| [metadata.py](trainer/metadata.py) | Defines: metadata for classification task such as predefined model dataset name, target labels. |
|
||||||
|
| [utils.py](trainer/utils.py) | Includes: utility functions such as data input functions to read data, save model to GCS bucket. |
|
||||||
|
| [model.py](trainer/model.py) | Includes: function to create model with a sequence classification head from a pretrained model. |
|
||||||
|
| [experiment.py](trainer/experiment.py) | Runs the model training and evaluation experiment, and exports the final model. |
|
||||||
|
| [task.py](trainer/task.py) | Includes: 1) Initialize and parse task arguments (hyper parameters), and 2) Entry point to the trainer. |
|
||||||
|
|
||||||
|
### Scripts
|
||||||
|
|
||||||
|
* [train-cloud.sh](scripts/train-cloud.sh) This script submits a training job to Vertex AI
|
||||||
|
|
||||||
|
## How to run
|
||||||
|
For local testing, run:
|
||||||
|
```
|
||||||
|
!cd python_package && python -m trainer.task
|
||||||
|
```
|
||||||
|
|
||||||
|
For cloud training, once the prerequisites are satisfied, update the
|
||||||
|
`BUCKET_NAME` environment variable in `scripts/train-cloud.sh`. You may then
|
||||||
|
run the following script to submit an AI Platform Training job:
|
||||||
|
```
|
||||||
|
source ./python_package/scripts/train-cloud.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run on GPU
|
||||||
|
The provided trainer code runs on a GPU if one is available including data loading and model creation.
|
||||||
|
|
||||||
|
To run the trainer code on a different GPU configuration or latest PyTorch pre-built container image, make the following changes to the trainer script.
|
||||||
|
* Update the PyTorch image URI to one of [PyTorch pre-built containers](https://cloud.google.com/vertex-ai/docs/training/pre-built-containers#available_container_images)
|
||||||
|
* Update the [`worker-pool-spec`](https://cloud.google.com/vertex-ai/docs/training/configure-compute?hl=hr) in the gcloud command that includes a GPU
|
||||||
|
|
||||||
|
Then, run the script to submit a Custom Job on Vertex Training job:
|
||||||
|
```
|
||||||
|
source ./scripts/train-cloud.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Versions
|
||||||
|
This script uses the pre-built PyTorch containers for PyTorch 1.7.
|
||||||
|
* `us-docker.pkg.dev/vertex-ai/training/pytorch-gpu.1-7:latest`
|
||||||
|
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Copyright 2019 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.
|
||||||
|
# ==============================================================================
|
||||||
|
# This script performs cloud training for a PyTorch model.
|
||||||
|
|
||||||
|
echo "Submitting Custom Job to Vertex AI to train PyTorch model"
|
||||||
|
|
||||||
|
# BUCKET_NAME: Change to your bucket name
|
||||||
|
BUCKET_NAME="[your-bucket-name]" # <-- CHANGE TO YOUR BUCKET NAME
|
||||||
|
BUCKET_NAME="cloud-ai-platform-2f444b6a-a742-444b-b91a-c7519f51bd77"
|
||||||
|
|
||||||
|
# The PyTorch image provided by Vertex AI Training.
|
||||||
|
IMAGE_URI="us-docker.pkg.dev/vertex-ai/training/pytorch-gpu.1-7:latest"
|
||||||
|
|
||||||
|
# JOB_NAME: the name of your job running on Vertex AI.
|
||||||
|
JOB_PREFIX="finetuned-bert-classifier-pytorch-pkg-ar-"
|
||||||
|
JOB_NAME=${JOB_PREFIX}-$(date +%Y%m%d%H%M%S)-custom-job
|
||||||
|
|
||||||
|
# REGION: select a region from https://cloud.google.com/vertex-ai/docs/general/locations#available_regions
|
||||||
|
# or use the default '`us-central1`'. The region is where the job will be run.
|
||||||
|
REGION="us-central1"
|
||||||
|
|
||||||
|
# JOB_DIR: Where to store prepared package and upload output model.
|
||||||
|
JOB_DIR=gs://${BUCKET_NAME}/${JOB_PREFIX}/model/${JOB_NAME}
|
||||||
|
|
||||||
|
# validate bucket name
|
||||||
|
if [ "${BUCKET_NAME}" = "[your-bucket-name]" ]
|
||||||
|
then
|
||||||
|
echo "[ERROR] INVALID VALUE: Please update the variable BUCKET_NAME with valid Cloud Storage bucket name. Exiting the script..."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Submit Custom Job to Vertex AI
|
||||||
|
gcloud beta ai custom-jobs create \
|
||||||
|
--display-name=${JOB_NAME} \
|
||||||
|
--region ${REGION} \
|
||||||
|
--python-package-uris=${PACKAGE_PATH} \
|
||||||
|
--worker-pool-spec=replica-count=1,machine-type='n1-standard-8',accelerator-type='NVIDIA_TESLA_V100',accelerator-count=1,executor-image-uri=${IMAGE_URI},python-module='trainer.task',local-package-path="../python_package/" \
|
||||||
|
--args="--model-name","finetuned-bert-classifier","--job-dir",$JOB_DIR
|
||||||
|
|
||||||
|
echo "After the job is completed successfully, model files will be saved at $JOB_DIR/"
|
||||||
|
|
||||||
|
# uncomment following lines to monitor the job progress by streaming logs
|
||||||
|
|
||||||
|
# Stream the logs from the job
|
||||||
|
# gcloud ai custom-jobs stream-logs $(gcloud ai custom-jobs list --region=$REGION --filter="displayName:"$JOB_NAME --format="get(name)")
|
||||||
|
|
||||||
|
# # Verify the model was exported
|
||||||
|
# echo "Verify the model was exported:"
|
||||||
|
# gsutil ls ${JOB_DIR}/
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
|
||||||
|
from setuptools import find_packages
|
||||||
|
from setuptools import setup
|
||||||
|
import setuptools
|
||||||
|
|
||||||
|
from distutils.command.build import build as _build
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
|
||||||
|
REQUIRED_PACKAGES = [
|
||||||
|
'transformers',
|
||||||
|
'datasets',
|
||||||
|
'tqdm',
|
||||||
|
'cloudml-hypertune'
|
||||||
|
]
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name='trainer',
|
||||||
|
version='0.1',
|
||||||
|
install_requires=REQUIRED_PACKAGES,
|
||||||
|
packages=find_packages(),
|
||||||
|
include_package_data=True,
|
||||||
|
description='Vertex AI | Training | PyTorch | Text Classification | Python Package'
|
||||||
|
)
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
# Copyright 2019 Google LLC
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the \"License\");
|
||||||
|
# you may not use this file except in compliance with the License.\n",
|
||||||
|
# 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 os
|
||||||
|
import numpy as np
|
||||||
|
import hypertune
|
||||||
|
|
||||||
|
from transformers import (
|
||||||
|
AutoTokenizer,
|
||||||
|
EvalPrediction,
|
||||||
|
Trainer,
|
||||||
|
TrainingArguments,
|
||||||
|
default_data_collator,
|
||||||
|
TrainerCallback
|
||||||
|
)
|
||||||
|
|
||||||
|
from trainer import model, metadata, utils
|
||||||
|
|
||||||
|
|
||||||
|
class HPTuneCallback(TrainerCallback):
|
||||||
|
"""
|
||||||
|
A custom callback class that reports a metric to hypertuner
|
||||||
|
at the end of each epoch.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, metric_tag, metric_value):
|
||||||
|
super(HPTuneCallback, self).__init__()
|
||||||
|
self.metric_tag = metric_tag
|
||||||
|
self.metric_value = metric_value
|
||||||
|
self.hpt = hypertune.HyperTune()
|
||||||
|
|
||||||
|
def on_evaluate(self, args, state, control, **kwargs):
|
||||||
|
print(f"HP metric {self.metric_tag}={kwargs['metrics'][self.metric_value]}")
|
||||||
|
self.hpt.report_hyperparameter_tuning_metric(
|
||||||
|
hyperparameter_metric_tag=self.metric_tag,
|
||||||
|
metric_value=kwargs['metrics'][self.metric_value],
|
||||||
|
global_step=state.epoch)
|
||||||
|
|
||||||
|
|
||||||
|
def compute_metrics(p: EvalPrediction):
|
||||||
|
preds = p.predictions[0] if isinstance(p.predictions, tuple) else p.predictions
|
||||||
|
preds = np.argmax(preds, axis=1)
|
||||||
|
return {"accuracy": (preds == p.label_ids).astype(np.float32).mean().item()}
|
||||||
|
|
||||||
|
|
||||||
|
def train(args, model, train_dataset, test_dataset):
|
||||||
|
"""Create the training loop to load pretrained model and tokenizer and
|
||||||
|
start the training process
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args: read arguments from the runner to set training hyperparameters
|
||||||
|
model: The neural network that you are training
|
||||||
|
train_dataset: The training dataset
|
||||||
|
test_dataset: The test dataset for evaluation
|
||||||
|
"""
|
||||||
|
|
||||||
|
# initialize the tokenizer
|
||||||
|
tokenizer = AutoTokenizer.from_pretrained(
|
||||||
|
metadata.PRETRAINED_MODEL_NAME,
|
||||||
|
use_fast=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# set training arguments
|
||||||
|
training_args = TrainingArguments(
|
||||||
|
evaluation_strategy="epoch",
|
||||||
|
learning_rate=args.learning_rate,
|
||||||
|
per_device_train_batch_size=args.batch_size,
|
||||||
|
per_device_eval_batch_size=args.batch_size,
|
||||||
|
num_train_epochs=args.num_epochs,
|
||||||
|
weight_decay=args.weight_decay,
|
||||||
|
output_dir=os.path.join("/tmp", args.model_name)
|
||||||
|
)
|
||||||
|
|
||||||
|
# initialize our Trainer
|
||||||
|
trainer = Trainer(
|
||||||
|
model,
|
||||||
|
training_args,
|
||||||
|
train_dataset=train_dataset,
|
||||||
|
eval_dataset=test_dataset,
|
||||||
|
data_collator=default_data_collator,
|
||||||
|
tokenizer=tokenizer,
|
||||||
|
compute_metrics=compute_metrics
|
||||||
|
)
|
||||||
|
|
||||||
|
# add hyperparameter tuning callback to report metrics when enabled
|
||||||
|
if args.hp_tune == "y":
|
||||||
|
trainer.add_callback(HPTuneCallback("accuracy", "eval_accuracy"))
|
||||||
|
|
||||||
|
# training
|
||||||
|
trainer.train()
|
||||||
|
|
||||||
|
return trainer
|
||||||
|
|
||||||
|
|
||||||
|
def run(args):
|
||||||
|
"""Load the data, train, evaluate, and export the model for serving and
|
||||||
|
evaluating.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args: experiment parameters.
|
||||||
|
"""
|
||||||
|
# Open our dataset
|
||||||
|
train_dataset, test_dataset = utils.load_data(args)
|
||||||
|
|
||||||
|
label_list = train_dataset.unique("label")
|
||||||
|
num_labels = len(label_list)
|
||||||
|
|
||||||
|
# Create the model, loss function, and optimizer
|
||||||
|
text_classifier = model.create(num_labels=num_labels)
|
||||||
|
|
||||||
|
# Train / Test the model
|
||||||
|
trainer = train(args, text_classifier, train_dataset, test_dataset)
|
||||||
|
|
||||||
|
# Export the trained model
|
||||||
|
trainer.save_model(os.path.join("/tmp", args.model_name))
|
||||||
|
|
||||||
|
# Save the model to GCS
|
||||||
|
if args.job_dir:
|
||||||
|
utils.save_model(args)
|
||||||
|
else:
|
||||||
|
print(f"Saved model files at {os.path.join('/tmp', args.model_name)}")
|
||||||
|
print(f"To save model files in GCS bucket, please specify job_dir starting with gs://")
|
||||||
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# Copyright 2019 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.
|
||||||
|
|
||||||
|
# Task type can be either 'classification', 'regression', or 'custom'.
|
||||||
|
# This is based on the target feature in the dataset.
|
||||||
|
TASK_TYPE = 'classification'
|
||||||
|
|
||||||
|
# Dataset name
|
||||||
|
DATASET_NAME = 'imdb'
|
||||||
|
|
||||||
|
# pre-trained model name
|
||||||
|
PRETRAINED_MODEL_NAME = 'bert-base-cased'
|
||||||
|
|
||||||
|
# List of the class values (labels) in a classification dataset.
|
||||||
|
TARGET_LABELS = {1:1, 0:0, -1:0}
|
||||||
|
|
||||||
|
|
||||||
|
# maximum sequence length
|
||||||
|
MAX_SEQ_LENGTH = 128
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Copyright 2019 Google LLC
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the \"License\");
|
||||||
|
# you may not use this file except in compliance with the License.\n",
|
||||||
|
# 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 transformers import AutoModelForSequenceClassification
|
||||||
|
from trainer import metadata
|
||||||
|
|
||||||
|
def create(num_labels):
|
||||||
|
"""create the model by loading a pretrained model or define your
|
||||||
|
own
|
||||||
|
|
||||||
|
Args:
|
||||||
|
num_labels: number of target labels
|
||||||
|
"""
|
||||||
|
# Create the model, loss function, and optimizer
|
||||||
|
model = AutoModelForSequenceClassification.from_pretrained(
|
||||||
|
metadata.PRETRAINED_MODEL_NAME,
|
||||||
|
num_labels=num_labels
|
||||||
|
)
|
||||||
|
|
||||||
|
return model
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
# Copyright 2019 Google LLC
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the \"License\");
|
||||||
|
# you may not use this file except in compliance with the License.\n",
|
||||||
|
# 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 argparse
|
||||||
|
import os
|
||||||
|
|
||||||
|
from trainer import experiment
|
||||||
|
|
||||||
|
|
||||||
|
def get_args():
|
||||||
|
"""Define the task arguments with the default values.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
experiment parameters
|
||||||
|
"""
|
||||||
|
args_parser = argparse.ArgumentParser()
|
||||||
|
|
||||||
|
|
||||||
|
# Experiment arguments
|
||||||
|
args_parser.add_argument(
|
||||||
|
'--batch-size',
|
||||||
|
help='Batch size for each training and evaluation step.',
|
||||||
|
type=int,
|
||||||
|
default=16)
|
||||||
|
args_parser.add_argument(
|
||||||
|
'--num-epochs',
|
||||||
|
help="""\
|
||||||
|
Maximum number of training data epochs on which to train.
|
||||||
|
If both --train-size and --num-epochs are specified,
|
||||||
|
--train-steps will be: (train-size/train-batch-size) * num-epochs.\
|
||||||
|
""",
|
||||||
|
default=1,
|
||||||
|
type=int,
|
||||||
|
)
|
||||||
|
args_parser.add_argument(
|
||||||
|
'--seed',
|
||||||
|
help='Random seed (default: 42)',
|
||||||
|
type=int,
|
||||||
|
default=42,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Estimator arguments
|
||||||
|
args_parser.add_argument(
|
||||||
|
'--learning-rate',
|
||||||
|
help='Learning rate value for the optimizers.',
|
||||||
|
default=2e-5,
|
||||||
|
type=float)
|
||||||
|
args_parser.add_argument(
|
||||||
|
'--weight-decay',
|
||||||
|
help="""
|
||||||
|
The factor by which the learning rate should decay by the end of the
|
||||||
|
training.
|
||||||
|
|
||||||
|
decayed_learning_rate =
|
||||||
|
learning_rate * decay_rate ^ (global_step / decay_steps)
|
||||||
|
|
||||||
|
If set to 0 (default), then no decay will occur.
|
||||||
|
If set to 0.5, then the learning rate should reach 0.5 of its original
|
||||||
|
value at the end of the training.
|
||||||
|
Note that decay_steps is set to train_steps.
|
||||||
|
""",
|
||||||
|
default=0.01,
|
||||||
|
type=float)
|
||||||
|
|
||||||
|
# Enable hyperparameter
|
||||||
|
args_parser.add_argument(
|
||||||
|
'--hp-tune',
|
||||||
|
default="n",
|
||||||
|
help='Enable hyperparameter tuning. Valida values are: "y" - enable, "n" - disable')
|
||||||
|
|
||||||
|
# Saved model arguments
|
||||||
|
args_parser.add_argument(
|
||||||
|
'--job-dir',
|
||||||
|
default=os.getenv('AIP_MODEL_DIR'),
|
||||||
|
help='GCS location to export models')
|
||||||
|
args_parser.add_argument(
|
||||||
|
'--model-name',
|
||||||
|
default="finetuned-bert-classifier",
|
||||||
|
help='The name of your saved model')
|
||||||
|
|
||||||
|
return args_parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Setup / Start the experiment
|
||||||
|
"""
|
||||||
|
args = get_args()
|
||||||
|
print(args)
|
||||||
|
experiment.run(args)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
# Copyright 2019 Google LLC
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the \"License\");
|
||||||
|
# you may not use this file except in compliance with the License.\n",
|
||||||
|
# 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 os
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from google.cloud import storage
|
||||||
|
|
||||||
|
from transformers import AutoTokenizer
|
||||||
|
from datasets import load_dataset, load_metric, ReadInstruction
|
||||||
|
from trainer import metadata
|
||||||
|
|
||||||
|
|
||||||
|
def preprocess_function(examples):
|
||||||
|
tokenizer = AutoTokenizer.from_pretrained(
|
||||||
|
metadata.PRETRAINED_MODEL_NAME,
|
||||||
|
use_fast=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Tokenize the texts
|
||||||
|
tokenizer_args = (
|
||||||
|
(examples['text'],)
|
||||||
|
)
|
||||||
|
result = tokenizer(*tokenizer_args,
|
||||||
|
padding='max_length',
|
||||||
|
max_length=metadata.MAX_SEQ_LENGTH,
|
||||||
|
truncation=True)
|
||||||
|
|
||||||
|
# TEMP: We can extract this automatically but Unique method of the dataset
|
||||||
|
# is not reporting the label -1 which shows up in the pre-processing
|
||||||
|
# Hence the additional -1 term in the dictionary
|
||||||
|
label_to_id = metadata.TARGET_LABELS
|
||||||
|
|
||||||
|
# Map labels to IDs (not necessary for GLUE tasks)
|
||||||
|
if label_to_id is not None and "label" in examples:
|
||||||
|
result["label"] = [label_to_id[l] for l in examples["label"]]
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def load_data(args):
|
||||||
|
"""Loads the data into two different data loaders. (Train, Test)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args: arguments passed to the python script
|
||||||
|
"""
|
||||||
|
# Dataset loading repeated here to make this cell idempotent
|
||||||
|
# Since we are over-writing datasets variable
|
||||||
|
dataset = load_dataset(metadata.DATASET_NAME)
|
||||||
|
|
||||||
|
dataset = dataset.map(preprocess_function,
|
||||||
|
batched=True,
|
||||||
|
load_from_cache_file=True)
|
||||||
|
|
||||||
|
train_dataset, test_dataset = dataset["train"], dataset["test"]
|
||||||
|
|
||||||
|
return train_dataset, test_dataset
|
||||||
|
|
||||||
|
|
||||||
|
def save_model(args):
|
||||||
|
"""Saves the model to Google Cloud Storage or local file system
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args: contains name for saved model.
|
||||||
|
"""
|
||||||
|
scheme = 'gs://'
|
||||||
|
if args.job_dir.startswith(scheme):
|
||||||
|
job_dir = args.job_dir.split("/")
|
||||||
|
bucket_name = job_dir[2]
|
||||||
|
object_prefix = "/".join(job_dir[3:]).rstrip("/")
|
||||||
|
|
||||||
|
if object_prefix:
|
||||||
|
model_path = '{}/{}'.format(object_prefix, args.model_name)
|
||||||
|
else:
|
||||||
|
model_path = '{}'.format(args.model_name)
|
||||||
|
|
||||||
|
bucket = storage.Client().bucket(bucket_name)
|
||||||
|
local_path = os.path.join("/tmp", args.model_name)
|
||||||
|
files = [f for f in os.listdir(local_path) if os.path.isfile(os.path.join(local_path, f))]
|
||||||
|
for file in files:
|
||||||
|
local_file = os.path.join(local_path, file)
|
||||||
|
blob = bucket.blob("/".join([model_path, file]))
|
||||||
|
blob.upload_from_filename(local_file)
|
||||||
|
print(f"Saved model files in gs://{bucket_name}/{model_path}")
|
||||||
|
else:
|
||||||
|
print(f"Saved model files at {os.path.join('/tmp', args.model_name)}")
|
||||||
|
print(f"To save model files in GCS bucket, please specify job_dir starting with gs://")
|
||||||
|
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
# Community Content
|
# Community Content
|
||||||
/community-content @morgandu
|
/community-content @morgandu
|
||||||
/community-content/tf_agents_bandits_movie_recommendation_with_kfp_and_vertex_sdk @yinghsienwu
|
/community-content/tf_agents_bandits_movie_recommendation_with_kfp_and_vertex_sdk @yinghsienwu
|
||||||
|
/community-content/pytorch_text_classification_using_vertex_sdk_and_gcloud @RajeshThallam @ultrons
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||