{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# GluonNLP の BERT モデル を利用した感情分析\n", "\n", "## 概要\n", "\n", "このノートブックでは、Amazon の商品レビューに対する感情分析、つまり、そのレビューが Positive (Rating が 5 or 4) か、Negative (Rating が 1 or 2)なのかを判定します。これは文書を Positive か Negative に分類する2クラスの分類問題となります。そこで、BERT (Bidirectional Encoder Representations from Transformers) モデルを利用して解きます。\n", "\n", "### BERT とは\n", "\n", "BERT は大規模なコーパスで学習された汎用的な自然言語処理のためのモデルです。今回対象とする文書の分類問題だけでなく、文書のペアを分類するような質問応答の問題に対しても、転移学習を行うことで良い精度を示しています。BERT は大規模なモデルであり、学習には多くの時間が必要です。\n", "\n", "### GluonNLPとは\n", "MXNet をより簡単に利用するためのライブラリとして Gluon が開発されています。Gluon には、自然言語処理に特化した GluonNLP という派生のライブラリがあります。その中には、BERT モデルの学習済みモデルも提供されており、BERT の事前の学習時間の削減や、実装の効率化に有効です。詳細はURLをごらんください。\n", "\n", "https://gluon-nlp.mxnet.io/index.html\n", "\n", "\n", "## データの準備\n", "\n", "Amazon の商品レビューデータセットは [Registry of Open Data on AWS](https://registry.opendata.aws/) で公開されており、 \n", "以下からダウンロード可能です。このノートブックでは、日本語のデータセットをダウンロードします。\n", "- データセットの概要 \n", "https://registry.opendata.aws/amazon-reviews/\n", "\n", "- 日本語のデータセット(readme.htmlからたどることができます) \n", "https://s3.amazonaws.com/amazon-reviews-pds/tsv/amazon_reviews_multilingual_JP_v1_00.tsv.gz\n", "\n", "以下では、データをダウンロードして解凍 (unzip) します。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import urllib.request\n", "import os\n", "import gzip\n", "import shutil\n", "\n", "download_url = \"https://s3.amazonaws.com/amazon-reviews-pds/tsv/amazon_reviews_multilingual_JP_v1_00.tsv.gz\" \n", "dir_name = \"data\"\n", "file_name = \"amazon_review.tsv.gz\"\n", "tsv_file_name = \"amazon_review.tsv\"\n", "file_path = os.path.join(dir_name,file_name)\n", "tsv_file_path = os.path.join(dir_name,tsv_file_name)\n", "\n", "os.makedirs(dir_name, exist_ok=True)\n", "\n", "if os.path.exists(file_path):\n", " print(\"File {} already exists. Skipped download.\".format(file_name))\n", "else:\n", " urllib.request.urlretrieve(download_url, file_path)\n", " print(\"File downloaded: {}\".format(file_path))\n", " \n", "if os.path.exists(tsv_file_path):\n", " print(\"File {} already exists. Skipped unzip.\".format(tsv_file_name))\n", "else:\n", " with gzip.open(file_path, mode='rb') as fin:\n", " with open(tsv_file_path, 'wb') as fout:\n", " shutil.copyfileobj(fin, fout)\n", " print(\"File uznipped: {}\".format(tsv_file_path))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## データの前処理\n", "\n", "ダウンロードしたデータには学習に不要なデータや直接利用できないデータもあります。以下の前処理で利用できるようにします。\n", "\n", "1. ダウンロードしたデータには不要なデータも含まれているので削除し、2クラス分類 (positive が 1, negative が 0)となるように評価データを加工します。\n", "2. 学習データ、テストデータに分けて、学習用にS3にデータをアップロードします。\n", "\n", "### データの加工\n", "\n", "今回利用しないデータは以下の2つです。必要なデータだけ選んで保存します。\n", "- 評価データ `star_rating` と レビューのテキストデータ `review_body` 以外のデータ\n", "- 評価が 3 のデータ (positive でも negative でもないデータ)\n", "\n", "また、評価が1, 2 のデータはラベル 0 (negative) に、評価が4, 5 のデータはラベル 1 (positive) にします。BERTには、Tokenizer という分かち書きのための機能が備わっています。従って、学習を実行する直前に分かち書きを行うようにし、ここでは行いません。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import pandas as pd\n", "df = pd.read_csv(tsv_file_path, sep ='\\t')\n", "df_pos_neg = df.loc[:, [\"star_rating\", \"review_body\"]]\n", "df_pos_neg = df_pos_neg[df_pos_neg.star_rating != 3]\n", "df_pos_neg.loc[df_pos_neg.star_rating < 3, \"star_rating\"] = 0\n", "df_pos_neg.loc[df_pos_neg.star_rating > 3, \"star_rating\"] = 1" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### データの分割\n", "\n", "すべてのデータを学習データとすると、データを使って作成したモデルが良いのか悪いのか評価するデータが別途必要になります。\n", "そこで、データを学習データ、テストデータに分割して利用します。学習データはモデルの学習に利用し、最終的に作成されたモデルに対してテストデータによる評価を行います。\n", "\n", "`train_ratio` で設定した割合のデータを学習データとし、残ったデータをテストデータに分割して利用します。学習データは、後にSageMakerで利用するために、`savetxt` を利用してスペース区切りの csv に保存します。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "\n", "# Swap positions of \"review_body\",\"star_rating\" because transform.py requires this order.\n", "labeled_df = df_pos_neg.loc[:, [\"review_body\",\"star_rating\"]]\n", "data_size = len(labeled_df.index)\n", "train_ratio = 0.9\n", "train_index = np.random.choice(data_size, int(data_size*train_ratio), replace=False)\n", "test_index = np.setdiff1d(np.arange(data_size), train_index)\n", "\n", "np.savetxt('train.tsv',labeled_df.iloc[train_index].values, fmt=\"%s\\t%i\") \n", "\n", "print(\"Data is splitted into:\")\n", "print(\"Training data: {} records.\".format(len(train_index)))\n", "print(\"Test data: {} records.\".format(len(test_index)))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### データのアップロード\n", "\n", "SageMaker での学習に利用するために、学習データを S3 にアップロードします。SageMaker Python SDK の upload_data を利用すると、S3 にファイルをアップロードできます。アップロード先のバケットは `sagemaker-{リージョン名}-{アカウントID}`で、バケットがない場合は自動作成されます。もし存在するバケットにアップロードする場合は、このバケット名を引数で指定できます。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import sagemaker\n", "\n", "sess = sagemaker.Session()\n", "\n", "s3_train_data = sess.upload_data(path='train.tsv', key_prefix='amazon-review-data')\n", "print(\"Training data is uploaded to {}\".format(s3_train_data))\n", "\n", "data_channels = {'train': s3_train_data}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 学習の実行\n", "\n", "### 学習コードの作成\n", "\n", "GluonNLP を利用した BERT の学習とデプロイ用のコードを `train_and_deploy.py` として作成します。コード作成にあたっては、GluonNLP の公式ページのチュートリアルのコードを流用可能です。\n", "\n", "https://gluon-nlp.mxnet.io/examples/sentence_embedding/bert.html\n", "\n", "ただし、この公式チュートリアルはペアの文書を分類する前提になっています。今回はペアを利用しないため、以下の点を修正して利用します。\n", "\n", "```python\n", "pair = False\n", "```\n", "\n", "### GluonNLP のインストール\n", "\n", "SageMaker のコンテナでは、mxnet 1.6.0のバージョンから GluonNLP がデフォルトでインストールされています。このため、GluonNLP をインストールするためのスクリプトは不要です。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 学習ジョブの実行\n", "\n", "MXNet のコンテナを呼び出し、学習用のインスタンスを指定して、学習を実行します。`hyperparameters`で渡すパラメータは train_and_deploy.py でパースして利用することができます。BERTのモデルの学習は非常に時間がかかるため、1エポックだけ回すようにします。それでも20分以上かかりますのでご注意ください。\n", "\n", "fit()を指定すれば、S3のデータを渡して学習することが可能です。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from sagemaker.mxnet import MXNet\n", "\n", "gluon_bert = MXNet(\"train_and_deploy.py\", \n", " role=sagemaker.get_execution_role(), \n", " source_dir = \"src\",\n", " instance_count=1, \n", " instance_type=\"ml.m4.xlarge\",\n", " framework_version=\"1.6.0\",\n", " distribution={'parameter_server': {'enabled': True}},\n", " py_version = \"py3\",\n", " hyperparameters={'batch-size': 16, \n", " 'epochs': 1, \n", " 'log-interval': 1})" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "gluon_bert.fit(data_channels)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 推論の実行\n", "\n", "学習が終わると、作成されたモデルをデプロイして、推論を実行することができます。デプロイは deploy を呼び出すだけでできます。`---`といった出力があるときはデプロイ中で、`!`が出力されるとデプロイが完了です。\n", "\n", "### 推論のコード\n", "\n", "SageMakerがサポートしている機械学習フレームワークコンテナで推論を行う場合は、モデルの読み込みや前処理・後処理を容易に実装できます。MXNet の場合は、モデル読み込みに `model_fn`、前処理・後処理に `transform_fn` を実装します。model_fnでは学習したモデルだけでなく、Tokenizerも読み込んでおき、`transformer_fn`で利用します。\n", "\n", "```python\n", "def model_fn(model_dir):\n", " bert_tokenizer = nlp.data.BERTTokenizer(vocabulary, lower=True)\n", " bert_classifier = gluon.SymbolBlock.imports(\n", " '%s/model-symbol.json' % model_dir,\n", " ['data0', 'data1', 'data2'],\n", " '%s/model-0000.params' % model_dir,\n", " )\n", " return {\"net\": bert_classifier, \"tokenizer\": bert_tokenizer}\n", "\n", "def transform_fn(net, data, input_content_type, output_content_type):\n", " \n", " bert_classifier = net[\"net\"]\n", " bert_tokenizer = net[\"tokenizer\"]\n", " \n", " parsed = json.loads(data)\n", " logging.info(\"Received_data: {}\".format(parsed))\n", " tokens = bert_tokenizer(parsed)\n", " logging.info(\"Tokens: {}\".format(tokens))\n", " token_ids = bert_tokenizer.convert_tokens_to_ids(tokens)\n", " valid_length = len(token_ids)\n", " segment_ids = mx.nd.zeros([1, valid_length])\n", "\n", " output = bert_classifier(mx.nd.array([token_ids]), \n", " segment_ids, \n", " mx.nd.array([valid_length]).astype('float32'))\n", " response_body = json.dumps(output.asnumpy().tolist()[0])\n", " return response_body, output_content_type\n", "```\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "predictor = gluon_bert.deploy(initial_instance_count=1, instance_type='ml.m4.xlarge')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "デプロイが終わったら推論を実行してみましょう。ここでは negative なレビューを 5件、 positive なレビューを 5件ランダムに選択して推論を実行します。1エポックしか実行しない場合、良い精度を得ることは難しいと思います。ぜひ多くのエポックを試してみてください。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import mxnet as mx\n", "\n", "num_test = 5\n", "test_data = labeled_df.iloc[test_index]\n", "\n", "neg_test_data = test_data[test_data.star_rating == 0]\n", "pos_test_data = test_data[test_data.star_rating == 1]\n", "\n", "neg_index = np.random.choice(neg_test_data.index, num_test)\n", "pos_index = np.random.choice(pos_test_data.index, num_test)\n", "\n", "for i in neg_index:\n", " pred = predictor.predict(neg_test_data.loc[i, \"review_body\"])\n", " prob = mx.nd.softmax(mx.nd.array(pred).reshape([1,2]))\n", " pred_label =np.argmax(prob, axis = 1)\n", " print(\"Ground Truth: {}, Prediction: {} (probability: {})\"\n", " .format(0, pred_label.asscalar(), prob[0, pred_label[0]].asscalar()))\n", " print(neg_test_data.loc[i, \"review_body\"])\n", " print()\n", " \n", "\n", "for i in pos_index:\n", " pred = predictor.predict(pos_test_data.loc[i, \"review_body\"])\n", " prob = mx.nd.softmax(mx.nd.array(pred).reshape([1,2]))\n", " pred_label =np.argmax(prob, axis = 1)\n", " print(\"Ground Truth: {}, Prediction: {} (probability: {})\"\n", " .format(1, pred_label.asscalar(), prob[0, pred_label[0]].asscalar()))\n", " print(pos_test_data.loc[i, \"review_body\"])\n", " print()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 不要になったエンドポイントを削除" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "predictor.delete_endpoint()" ] } ], "metadata": { "kernelspec": { "display_name": "conda_mxnet_p36", "language": "python", "name": "conda_mxnet_p36" }, "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.6.10" } }, "nbformat": 4, "nbformat_minor": 2 }