{
"cells": [
{
"cell_type": "markdown",
"id": "e676831e",
"metadata": {},
"source": [
"# SageMaker Batch Transform Inference job with Hugging Face Transformers\n",
"---\n"
]
},
{
"cell_type": "markdown",
"id": "2c8fbfa1",
"metadata": {},
"source": [
"\n",
"## Introduction\n",
"---\n",
"\n",
"SageMaker 리얼타임 엔드포인트(SageMaker real-time endpoint)는 실시간으로 추론 결괏값을 빠른 응답속도 내에 전송받을 수 있지만, **호스팅 서버가 최소 1대 이상 구동**되어야 하므로 비용적인 측면에서 부담이 됩니다. 이런 경우 아래의 유즈케이스들에 해당하면 SageMaker 배치 변환(batch transform) 기능을 사용해 훈련 인스턴스처럼 배치 변환을 수행하는 때에만 컴퓨팅 인스턴스를 사용하여 비용을 절감할 수 있습니다.\n",
"\n",
"- 일/주/월 단위 정기적인 마케팅 캠페인이나 실시간 추천이 필요 없는 경우 전체 데이터셋에 대한 추론 결괏값 계산\n",
"- 일부 추론 결괏값을 데이터베이스나 스토리지에 저장\n",
"- SageMaker 호스팅 엔드포인트가 제공하는 1초 미만의 대기 시간이 필요하지 않은 경우\n",
"\n",
"자세한 내용은 아래 웹페이지를 참조해 주세요.\n",
"- Amazon SageMaker Batch Transform: (https://docs.aws.amazon.com/sagemaker/latest/dg/how-it-works-batch.html) \n",
"- API docs: https://sagemaker.readthedocs.io/en/stable/overview.html#sagemaker-batch-transform"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "22d33988",
"metadata": {},
"outputs": [],
"source": [
"import csv\n",
"import json\n",
"import sys\n",
"import numpy as np\n",
"import logging\n",
"import sagemaker\n",
"from sagemaker.s3 import S3Uploader, s3_path_join\n",
"from sagemaker.huggingface import HuggingFaceModel\n",
"\n",
"logging.basicConfig(\n",
" level=logging.INFO, \n",
" format='[{%(filename)s:%(lineno)d} %(levelname)s - %(message)s',\n",
" handlers=[\n",
" logging.FileHandler(filename='tmp.log'),\n",
" logging.StreamHandler(sys.stdout)\n",
" ]\n",
")\n",
"logger = logging.getLogger(__name__)\n",
"\n",
"sess = sagemaker.Session()\n",
"role = sagemaker.get_execution_role()\n",
"\n",
"# sagemaker session bucket -> used for uploading data, models and logs\n",
"# sagemaker will automatically create this bucket if it not exists\n",
"sagemaker_session_bucket=None\n",
"if sagemaker_session_bucket is None and sess is not None:\n",
" # set to default bucket if a bucket name is not given\n",
" sagemaker_session_bucket = sess.default_bucket()"
]
},
{
"cell_type": "markdown",
"id": "3f0b456f",
"metadata": {},
"source": [
"
\n",
"\n",
"## 1. Run Batch Transform after training a model \n",
"---\n",
"\n",
"추론을 수행할 데이터셋에 대한 S3 uri를 지정하고 transformer job을 실행하면, SageMaker는 배치 변환을 위한 컴퓨팅 인스턴스를 프로비저닝 후 S3에 저장된 데이터셋을 다운로드하고 추론을 수행한 후, 결과를 S3에 업로드합니다.\n",
"\n",
"\n",
"```python\n",
"batch_job = huggingface_estimator.transformer(\n",
" instance_count=1,\n",
" instance_type='ml.c5.2xlarge',\n",
" strategy='SingleRecord')\n",
"\n",
"batch_job.transform(\n",
" data='s3://s3-uri-to-batch-data',\n",
" content_type='application/json', \n",
" split_type='Line')\n",
"```"
]
},
{
"cell_type": "markdown",
"id": "9914b6bf",
"metadata": {},
"source": [
"### Data Pre-Processing\n",
"\n",
"배치 변환을 위해 https://github.com/e9t/nsmc/ 에 공개된 네이버 영화 리뷰 테스트 데이터셋을 다운로드합니다. 테스트 데이터셋은 총 5만건입니다."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "aa4262f9",
"metadata": {},
"outputs": [],
"source": [
"!wget https://raw.githubusercontent.com/e9t/nsmc/master/ratings_test.txt"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "c8f42599",
"metadata": {},
"outputs": [],
"source": [
"num_lines = sum(1 for line in open('ratings_test.txt')) - 1\n",
"y_true = np.zeros((num_lines))"
]
},
{
"cell_type": "markdown",
"id": "7d076a6b",
"metadata": {},
"source": [
"Hugging Face 추론 컨테이너는 `{'input' : '입력 데이터'}` 포맷으로 된 요청을 인식하기에 테스트 데이터셋을 아래와 같은 포맷으로 변환해야 합니다.\n",
"\n",
"```json\n",
"{'inputs': '뭐야 이 평점들은.... 나쁘진 않지만 10점 짜리는 더더욱 아니잖아'}\n",
"{'inputs': '지루하지는 않은데 완전 막장임... 돈주고 보기에는....'}\n",
"{'inputs': '3D만 아니었어도 별 다섯 개 줬을텐데.. 왜 3D로 나와서 제 심기를 불편하게 하죠??'}\n",
"{'inputs': '음악이 주가 된, 최고의 음악영화'}\n",
"...\n",
"```"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "2140b277",
"metadata": {},
"outputs": [],
"source": [
"dataset_csv_file = 'ratings_test.txt'\n",
"dataset_jsonl_file=\"./ratings_test.jsonl\"\n",
"with open(dataset_csv_file, \"r+\") as infile, open(dataset_jsonl_file, \"w+\") as outfile:\n",
" reader = csv.DictReader(infile, delimiter=\"\\t\")\n",
" for idx, row in enumerate(reader):\n",
" row_dict = {'inputs': row['document']}\n",
"\n",
" y_true[idx] = row['label']\n",
" json.dump(row_dict, outfile)\n",
" outfile.write('\\n')"
]
},
{
"cell_type": "markdown",
"id": "872b504e",
"metadata": {},
"source": [
"### Upload dataset to S3"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "db56b395",
"metadata": {},
"outputs": [],
"source": [
"# uploads a given file to S3.\n",
"input_s3_path = s3_path_join(\"s3://\", sagemaker_session_bucket, \"batch_transform/input\")\n",
"output_s3_path = s3_path_join(\"s3://\", sagemaker_session_bucket, \"batch_transform/output\")\n",
"s3_file_uri = S3Uploader.upload(dataset_jsonl_file, input_s3_path)\n",
"\n",
"print(f\"{dataset_jsonl_file} uploaded to {s3_file_uri}\")"
]
},
{
"cell_type": "markdown",
"id": "55aa859a",
"metadata": {},
"source": [
"### Create Inference Transformer to run the batch job"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "d4f93b70",
"metadata": {},
"outputs": [],
"source": [
"# Hub Model configuration. \n",
"hub = {\n",
" 'HF_MODEL_ID':'daekeun-ml/koelectra-small-v3-nsmc', # model_id from hf.co/models\n",
" 'HF_TASK':'text-classification' # NLP task you want to use for predictions\n",
"}\n",
"\n",
"# create Hugging Face Model Class\n",
"huggingface_model = HuggingFaceModel(\n",
" env=hub, # configuration for loading model from Hub\n",
" role=role, # iam role with permissions to create an Endpoint\n",
" transformers_version=\"4.12.3\", # transformers version used\n",
" pytorch_version=\"1.9.1\", # pytorch version used\n",
" py_version='py38', # python version used\n",
")\n",
"\n",
"# create Transformer to run our batch job\n",
"batch_job = huggingface_model.transformer(\n",
" instance_count=1, # number of instances used for running the batch job\n",
" instance_type='ml.g4dn.xlarge',# instance type for the batch job\n",
" output_path=output_s3_path, # we are using the same s3 path to save the output with the input\n",
" strategy='SingleRecord') # How we are sending the \"requests\" to the endpoint\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "85f6ac25",
"metadata": {},
"outputs": [],
"source": [
"# starts batch transform job and uses s3 data as input\n",
"batch_job.transform(\n",
" data=s3_file_uri, # preprocessed file location on s3 \n",
" content_type='application/json',# mime-type of the file \n",
" split_type='Line', # how the datapoints are split, here lines since it is `.jsonl`\n",
" wait=False\n",
") "
]
},
{
"cell_type": "markdown",
"id": "2591cee6",
"metadata": {},
"source": [
"### Wait for the batch transform jobs to complete\n",
"\n",
"배치 추론 작업이 완료될 때까지 기다립니다. 약 15분의 시간이 소요됩니다."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "657de359",
"metadata": {},
"outputs": [],
"source": [
"batch_job.wait(logs=True)"
]
},
{
"cell_type": "markdown",
"id": "9eeb04a4",
"metadata": {},
"source": [
"
\n",
"\n",
"## 2. Access Prediction file\n",
"---\n",
"\n",
"\n",
"배치 변환 작업이 성공적으로 완료되면 `.out` 확장자의 출력 파일이 S3에 저장됩니다. `Transformer`에서 `join_source` 매개변수를 사용해서 입력 파일과 출력 파일을 병합하는 것도 가능하며, 자세한 내용은 (https://docs.aws.amazon.com/sagemaker/latest/dg/batch-transform-data-processing.html 를 참조해 주세요."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "bac37ed7",
"metadata": {},
"outputs": [],
"source": [
"import os\n",
"import json\n",
"from sagemaker.s3 import S3Downloader\n",
"from ast import literal_eval\n",
"# creating s3 uri for result file -> input file + .out\n",
"batch_transform_dir = './batch'\n",
"!rm -rf {batch_transform_dir}\n",
"os.makedirs(batch_transform_dir, exist_ok=True)\n",
"\n",
"output_file = f\"{dataset_jsonl_file}.out\"\n",
"local_output_path = f\"{batch_transform_dir}/{output_file}\"\n",
"output_s3_filepath = s3_path_join(output_s3_path, output_file)\n",
"\n",
"logger.info(output_s3_filepath)\n",
"\n",
"# download file\n",
"S3Downloader.download(output_s3_filepath, batch_transform_dir)"
]
},
{
"cell_type": "markdown",
"id": "824a40d8",
"metadata": {},
"source": [
"### Processing data\n",
"\n",
"예측 레이블 및 확률값을 받아옵니다."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "8e861d9c",
"metadata": {},
"outputs": [],
"source": [
"# Read in the file\n",
"with open(local_output_path, 'r') as file :\n",
" filedata = file.read()\n",
"\n",
"# Replace the target string\n",
"filedata = filedata.replace('][', '\\n').replace('[', '').replace(']', '')\n",
"\n",
"# Write the file out again\n",
"with open(f'{batch_transform_dir}/file.txt', 'w') as file:\n",
" file.write(filedata)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cf75ae7c",
"metadata": {},
"outputs": [],
"source": [
"import numpy as np\n",
"y_pred = np.zeros((num_lines), dtype='int')\n",
"y_score = np.zeros((num_lines), dtype='float32')"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "bb4c6fb9",
"metadata": {},
"outputs": [],
"source": [
"batch_transform_result = []\n",
"with open(f'{batch_transform_dir}/file.txt') as f:\n",
" for idx, line in enumerate(f):\n",
" result = literal_eval(line)\n",
" y_pred[idx] = result['label']\n",
" y_score[idx] = result['score']"
]
},
{
"cell_type": "markdown",
"id": "d28af58f",
"metadata": {},
"source": [
"### Confusion Matrix"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "8f8ebe27",
"metadata": {},
"outputs": [],
"source": [
"def plot_confusion_matrix(cm, target_names=None, cmap=None, normalize=True, labels=True, title='Confusion matrix'):\n",
" import itertools\n",
" import matplotlib.pyplot as plt\n",
" accuracy = np.trace(cm) / float(np.sum(cm))\n",
" misclass = 1 - accuracy\n",
"\n",
" if cmap is None:\n",
" cmap = plt.get_cmap('Blues')\n",
"\n",
" if normalize:\n",
" cm = cm.astype('float') / cm.sum(axis=1)[:, np.newaxis]\n",
" \n",
" plt.figure(figsize=(8, 6))\n",
" plt.imshow(cm, interpolation='nearest', cmap=cmap)\n",
" plt.title(title)\n",
" plt.colorbar()\n",
"\n",
" thresh = cm.max() / 1.5 if normalize else cm.max() / 2\n",
" \n",
" if target_names is not None:\n",
" tick_marks = np.arange(len(target_names))\n",
" plt.xticks(tick_marks, target_names)\n",
" plt.yticks(tick_marks, target_names)\n",
" \n",
" if labels:\n",
" for i, j in itertools.product(range(cm.shape[0]), range(cm.shape[1])):\n",
" if normalize:\n",
" plt.text(j, i, \"{:0.4f}\".format(cm[i, j]),\n",
" horizontalalignment=\"center\",\n",
" color=\"white\" if cm[i, j] > thresh else \"black\")\n",
" else:\n",
" plt.text(j, i, \"{:,}\".format(cm[i, j]),\n",
" horizontalalignment=\"center\",\n",
" color=\"white\" if cm[i, j] > thresh else \"black\")\n",
"\n",
" plt.tight_layout()\n",
" plt.ylabel('True label')\n",
" plt.xlabel('Predicted label\\naccuracy={:0.4f}; misclass={:0.4f}'.format(accuracy, misclass))\n",
" plt.show()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "722f51da",
"metadata": {},
"outputs": [],
"source": [
"from sklearn.metrics import confusion_matrix\n",
"\n",
"cf = confusion_matrix(y_true, y_pred)\n",
"plot_confusion_matrix(cf, normalize=False)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "5a017c7e",
"metadata": {},
"outputs": [],
"source": [
"from sklearn.metrics import confusion_matrix, classification_report\n",
"print(classification_report(y_true, y_pred, target_names=['0','1']))"
]
}
],
"metadata": {
"interpreter": {
"hash": "c281c456f1b8161c8906f4af2c08ed2c40c50136979eaae69688b01f70e9f4a9"
},
"kernelspec": {
"display_name": "conda_pytorch_latest_p37",
"language": "python",
"name": "conda_pytorch_latest_p37"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.7.12"
}
},
"nbformat": 4,
"nbformat_minor": 5
}