{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Lab 1-1: Train Hugging Face Transformers on Local Environment\n", "\n", "### Multi-Class Classification with Naver Movie dataset and Hugging Face `Trainer` \n", "---" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Introduction\n", "---\n", "\n", "본 모듈에서는 Hugging Face `transformers` 및 `datasets` 라이브러리를 사용하여 한국어 텍스트 감성 분류 파인 튜닝을 수행합니다. 파인 튜닝한 모델은 SageMaker 상에서 실시간 엔드포인트(real-time endpoint)/비동기 엔드포인트(asynchronous endpoint)/배치 변환(batch transform)/서버리스 엔드포인트(serverless endpoint)의 다양한 형태로 배포할 수 있으며, 모델 아티팩트를 Hugging Face Hub에 등록하여 아래와 같이 추론 결과를 웹으로 확인할 수도 있습니다.\n", "\n", "Reference: https://huggingface.co/docs/transformers/training\n", "\n", "_**Note: SageMaker Studio Lab, SageMaker Studio, SageMaker 노트북 인스턴스, 또는 여러분의 로컬 머신에서 이 데모를 실행할 수 있습니다. SageMaker Studio Lab을 사용하는 경우 GPU를 활성화하세요.**_" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import os\n", "\n", "try:\n", " import torch\n", "except ImportError:\n", " os.system('pip install torch==1.8.1')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "

주의

\n", " \n", "이 예제 노트북은 **transformers v4.11.0** 이상이 필요합니다. \n", "아래 코드 셀은 핸즈온에 필요한 라이브러리들을 설치하고, 주피터 노트북 커널을 셧다운시킵니다. \n", " \n", "노트북 커널이 셧다운된다면, 아래 코드 셀에서 install_needed = False로 변경 후, 코드 셀을 다시 실행해 주세요. 이 작업은 한 번만 수행하면 됩니다. \n", "

" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%load_ext autoreload\n", "%autoreload 2\n", "import sys\n", "import IPython\n", "\n", "#install_needed = True\n", "install_needed = False\n", "\n", "if install_needed:\n", " print(\"===> Installing deps and restarting kernel. Please change 'install_needed = False' and run this code cell again.\")\n", " !{sys.executable} -m pip install -U transformers s3fs datasets\n", " IPython.Application.instance().kernel.do_shutdown(True)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import os\n", "import json\n", "import sys\n", "import logging\n", "import argparse\n", "from datasets import load_dataset\n", "from transformers import (\n", " ElectraModel, ElectraTokenizer, ElectraForSequenceClassification, Trainer, TrainingArguments, set_seed\n", ")\n", "from transformers.trainer_utils import get_last_checkpoint\n", "\n", "logging.basicConfig(\n", " level=logging.INFO, \n", " format='[{%(filename)s:%(lineno)d} %(levelname)s - %(message)s',\n", " handlers=[\n", " logging.StreamHandler(sys.stdout)\n", " ]\n", ")\n", "logger = logging.getLogger(__name__)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "\n", "## 1. Preparation\n", "---" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "빠른 핸즈온을 위해 네이버 영화 리뷰 말뭉치 데이터셋으로 사전에 파인튜닝된 모델을 그대로 사용합니다. (즉, 파인튜닝 없이도 이미 해당 데이터셋에 대해 정확도; 약 89% accuracy 를 보입니다) 처음부터 파인튜닝을 진행하고 싶다면 주석을 해제해 주세요." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Define the model repo\n", "tokenizer_id = 'daekeun-ml/koelectra-small-v3-nsmc'\n", "model_id = \"daekeun-ml/koelectra-small-v3-nsmc\"\n", "# tokenizer_id = 'monologg/koelectra-small-v3-discriminator'\n", "# model_id = \"monologg/koelectra-small-v3-discriminator\"\n", "\n", "# dataset used\n", "dataset_name = 'nsmc'" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Dataset\n", "\n", "본 핸즈온에서 사용할 말뭉치 데이터셋은 네이버 영화 리뷰 감성 분류 데이터(https://github.com/e9t/nsmc/) 공개 데이터셋으로 15만 건의 훈련 데이터와 5만 건의 테스트 데이터로 구성되어 있습니다. 이 데이터셋은 한국어 자연어 처리 모델 벤치마킹에 자주 사용됩니다.\n", "\n", "![emotion-widget.png](../imgs/nsmc-classification.png)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "빠른 실험을 위해 1%의 데이터셋만 사용합니다. 본 데이터셋 기준으로는 총 1500건의 훈련 데이터를 사용합니다." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# load dataset\n", "train_dataset, test_dataset = load_dataset(dataset_name, split=['train[:1%]', 'test[:1%]'])\n", "\n", "# num_samples_for_debug = 200\n", "# train_dataset = train_dataset.shuffle(seed=42).select(range(num_samples_for_debug))\n", "# test_dataset = test_dataset.shuffle(seed=42).select(range(num_samples_for_debug))\n", "\n", "logger.info(f\" loaded train_dataset length is: {len(train_dataset)}\")\n", "logger.info(f\" loaded test_dataset length is: {len(test_dataset)}\")\n", "logger.info(train_dataset[0])" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "train_dir = 'datasets/train'\n", "test_dir = 'datasets/test'\n", "!rm -rf {train_dir} {test_dir}\n", "\n", "os.makedirs(train_dir, exist_ok=True)\n", "os.makedirs(test_dir, exist_ok=True) \n", "\n", "if not os.listdir(train_dir):\n", " train_dataset.save_to_disk(train_dir)\n", "if not os.listdir(test_dir):\n", " test_dataset.save_to_disk(test_dir)\n", "\n", "# from datasets import load_from_disk\n", "# train_dataset = load_from_disk(train_dir)\n", "# test_dataset = load_from_disk(test_dir)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Tokenization\n", "\n", "자연어 처리 모델을 훈련하려면, 토큰화(Tokenization)를 통해 말뭉치(corpus; 자연어 처리를 위한 대량의 텍스트 데이터)를 토큰 시퀀스로 나누는 과정이 필요합니다. BERT 이전의 자연어 처리 모델은 주로 도메인 전문가들이 직접 토큰화해놓은 토크아니저(Mecab, Kkma 등)들을 사용했지만, BERT를 훈련하기 위한 토크나이저는 도메인 지식 필요 없이 말뭉치에서 자주 등장하는 서브워드(subword)를 토큰화합니다. GPT 기반 모델은 BPE(Byte-pair Encoding)라는 통계적 기법을 사용하며, BERT 및 ELECTRA 기반 모델은 BPE와 유사한 Wordpiece를 토크나이저로 사용합니다." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# download tokenizer\n", "tokenizer = ElectraTokenizer.from_pretrained(tokenizer_id)\n", "\n", "# tokenizer helper function\n", "def tokenize(batch):\n", " return tokenizer(batch['document'], padding='max_length', max_length=128, truncation=True)\n", "\n", "# tokenize dataset\n", "train_dataset = train_dataset.map(tokenize, batched=True)\n", "test_dataset = test_dataset.map(tokenize, batched=True)\n", "\n", "# set format for pytorch\n", "train_dataset.set_format('torch', columns=['input_ids', 'attention_mask', 'label'])\n", "test_dataset.set_format('torch', columns=['input_ids', 'attention_mask', 'label'])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Argument parser" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def parser_args(train_notebook=False):\n", " parser = argparse.ArgumentParser()\n", "\n", " # Default Setting\n", " parser.add_argument(\"--epochs\", type=int, default=5)\n", " parser.add_argument(\"--seed\", type=int, default=42)\n", " parser.add_argument(\"--train_batch_size\", type=int, default=32)\n", " parser.add_argument(\"--eval_batch_size\", type=int, default=128)\n", " parser.add_argument(\"--warmup_steps\", type=int, default=0)\n", " parser.add_argument(\"--learning_rate\", type=str, default=5e-5)\n", " parser.add_argument(\"--disable_tqdm\", type=bool, default=False)\n", " parser.add_argument(\"--fp16\", type=bool, default=True)\n", " parser.add_argument(\"--tokenizer_id\", type=str, default='monologg/koelectra-small-v3-discriminator')\n", " parser.add_argument(\"--model_id\", type=str, default='monologg/koelectra-small-v3-discriminator') \n", "\n", " # SageMaker Container environment\n", " parser.add_argument(\"--output_data_dir\", type=str, default=os.environ[\"SM_OUTPUT_DATA_DIR\"])\n", " parser.add_argument(\"--model_dir\", type=str, default=os.environ[\"SM_MODEL_DIR\"])\n", " parser.add_argument(\"--n_gpus\", type=str, default=os.environ[\"SM_NUM_GPUS\"])\n", " parser.add_argument(\"--training_dir\", type=str, default=os.environ[\"SM_CHANNEL_TRAIN\"])\n", " parser.add_argument(\"--test_dir\", type=str, default=os.environ[\"SM_CHANNEL_TEST\"])\n", " parser.add_argument('--chkpt_dir', type=str, default='/opt/ml/checkpoints') \n", "\n", " if train_notebook:\n", " args = parser.parse_args([])\n", " else:\n", " args = parser.parse_args()\n", " return args" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Load Arguments\n", "\n", "주피터 노트북에서 곧바로 실행할 수 있도록 설정값들을 로드합니다. 물론 노트북 환경이 아닌 커맨드라인에서도 아래 커맨드로 훈련 스크립트를 실행할 수 있습니다.\n", "```shell\n", "cd scripts & python3 train.py\n", "```" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "chkpt_dir = 'chkpt'\n", "model_dir = 'model'\n", "output_data_dir = 'data' \n", "!rm -rf {chkpt_dir} {model_dir} {output_data_dir} \n", "\n", "if os.environ.get('SM_CURRENT_HOST') is None:\n", " is_sm_container = False\n", "\n", " #src_dir = '/'.join(os.getcwd().split('/')[:-1])\n", " src_dir = os.getcwd()\n", " os.environ['SM_MODEL_DIR'] = f'{src_dir}/{model_dir}'\n", " os.environ['SM_OUTPUT_DATA_DIR'] = f'{src_dir}/{output_data_dir}'\n", " os.environ['SM_NUM_GPUS'] = str(1)\n", " os.environ['SM_CHANNEL_TRAIN'] = f'{src_dir}/{train_dir}'\n", " os.environ['SM_CHANNEL_TEST'] = f'{src_dir}/{test_dir}'\n", "\n", "args = parser_args(train_notebook=True) \n", "args.chkpt_dir = chkpt_dir\n", "logger.info(\"***** Arguments *****\")\n", "logger.info(''.join(f'{k}={v}\\n' for k, v in vars(args).items()))\n", "\n", "os.makedirs(args.chkpt_dir, exist_ok=True) \n", "os.makedirs(args.model_dir, exist_ok=True)\n", "os.makedirs(args.output_data_dir, exist_ok=True) " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Load pre-trained model" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Prepare model labels - useful in inference API\n", "labels = train_dataset.features[\"label\"].names\n", "num_labels = len(labels)\n", "label2id, id2label = dict(), dict()\n", "for i, label in enumerate(labels):\n", " label2id[label] = str(i)\n", " id2label[str(i)] = label\n", "\n", "# Set seed before initializing model\n", "set_seed(args.seed)\n", " \n", "# Download pytorch model\n", "model = ElectraForSequenceClassification.from_pretrained(\n", " model_id, num_labels=num_labels, label2id=label2id, id2label=id2label\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "\n", "## 2. Hugging Face Fine-tuning\n", "---" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "먼저, `TrainingArguments` 클래스를 인스턴스화화합니다. 이 클래스로 훈련에 필요한 다양한 하이퍼파라메터를 지정할 수 있습니다." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# define training args\n", "training_args = TrainingArguments(\n", " output_dir=args.chkpt_dir,\n", " overwrite_output_dir=True if get_last_checkpoint(args.chkpt_dir) is not None else False,\n", " num_train_epochs=args.epochs,\n", " per_device_train_batch_size=args.train_batch_size,\n", " per_device_eval_batch_size=args.eval_batch_size,\n", " warmup_steps=args.warmup_steps,\n", " fp16=args.fp16,\n", " evaluation_strategy=\"epoch\",\n", " save_strategy=\"epoch\",\n", " save_total_limit=1,\n", " disable_tqdm=args.disable_tqdm,\n", " logging_dir=f\"{args.output_data_dir}/logs\",\n", " learning_rate=float(args.learning_rate),\n", " load_best_model_at_end=True,\n", " metric_for_best_model=\"accuracy\",\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "매 에폭(epoch)마다 검증 데이터셋으로 정확도(accuracy), 정밀도(precision), 재현율(recall), F1 스코어를 계산하기 위한 함수를 정의합니다." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from sklearn.metrics import accuracy_score, precision_recall_fscore_support\n", "\n", "# compute metrics function for binary classification\n", "def compute_metrics(pred):\n", " labels = pred.label_ids\n", " preds = pred.predictions.argmax(-1)\n", " precision, recall, f1, _ = precision_recall_fscore_support(labels, preds, average=\"binary\")\n", " acc = accuracy_score(labels, preds)\n", " return {\"accuracy\": acc, \"f1\": f1, \"precision\": precision, \"recall\": recall}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "훈련을 수행하기 위한 `Trainer` 클래스를 인스턴스화화합니다." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# create Trainer instance\n", "trainer = Trainer(\n", " model=model,\n", " args=training_args,\n", " train_dataset=train_dataset,\n", " eval_dataset=test_dataset,\n", " tokenizer=tokenizer,\n", " compute_metrics=compute_metrics \n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Start Training" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "훈련을 수행합니다. 딥러닝 기반 자연어 처리 모델 훈련에는 GPU가 필수이며, 본격적인 훈련을 위해서는 멀티 GPU 및 분산 훈련을 권장합니다.\n", "만약 멀티 GPU가 장착되어 있다면 `Trainer`에서 `총 배치 크기 = 배치 크기 x 멀티 GPU`로 지정한 다음 데이터 병렬화를 자동으로 수행합니다. " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%time\n", "# train model\n", "if get_last_checkpoint(args.chkpt_dir) is not None:\n", " logger.info(\"***** Continue Training *****\")\n", " last_checkpoint = get_last_checkpoint(args.chkpt_dir)\n", " trainer.train(resume_from_checkpoint=last_checkpoint)\n", "else:\n", " trainer.train()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "\n", "## 3. Evaluation and Prediction\n", "---\n", "\n", "### Evaluation\n", "검증 셋에 대해 평가를 수행합니다." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "eval_result = trainer.evaluate(test_dataset)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# writes eval result to file which can be accessed later in s3 ouput\n", "with open(os.path.join(args.output_data_dir, \"eval_results.txt\"), \"w\") as writer:\n", " print(f\"***** Evaluation results *****\")\n", " for key, value in sorted(eval_result.items()):\n", " writer.write(f\"{key} = {value}\\n\")\n", " logger.info(f\"{key} = {value}\\n\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Prediction\n", "\n", "정답 레이블과 예측 레이블의 결과를 혼동 행렬(confusion matrix)로 비교해 봅니다." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "results = trainer.predict(test_dataset)\n", "y_true = results.label_ids\n", "y_pred = np.argmax(results.predictions, axis=1)" ] }, { "cell_type": "code", "execution_count": null, "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, "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, "metadata": {}, "outputs": [], "source": [ "# Remove checkpoints\n", "import shutil\n", "dir_list = os.listdir(args.chkpt_dir)\n", "for d in dir_list:\n", " shutil.rmtree(os.path.join(args.chkpt_dir, d))\n", " \n", "trainer.save_model(args.model_dir)" ] } ], "metadata": { "instance_type": "ml.t3.medium", "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": 4 }