{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# BlazingText を利用した感情分析\n", "\n", "## 概要\n", "\n", "このノートブックでは、Amazon の商品レビューに対する感情分析、つまり、そのレビューが Positive (Rating が 5 or 4) か、Negative (Rating が 1 or 2)なのかを判定します。これは、文書を Positive か Negative に分類する2クラスの分類問題なので、**BlazingText**による教師あり学習を適用することができます。\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": [ "## MeCab のインストール\n", "\n", "BlazingText は、文章をそのまま学習・推論に利用することはできず、語ごとにスペースで区切って利用する必要があります。これは、スペースで区切られている英語などでは問題ありませんが、スペースで区切られていない日本語では追加の処理が必要になります。\n", "\n", "ここでは、形態素とよばれる語の単位に分解(分かち書き)する形態素解析ツール MeCab を利用します。MeCab は pip でインストールして利用することができます。冒頭に`!`を入れることで、シェルコマンドを実行できます。`import MeCab` としても問題ないか確認しましょう。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import sys\n", "!{sys.executable} -m pip install --upgrade pip\n", "!{sys.executable} -m pip install mecab-python3\n", "!{sys.executable} -m pip install unidic-lite\n", "import MeCab" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## データの前処理\n", "\n", "ダウンロードしたデータには学習に不要なデータや直接利用できないデータもあります。以下の前処理で利用できるようにします。\n", "\n", "1. ダウンロードしたデータには不要なデータも含まれているので削除します。\n", "2. 2クラス分類 (positive が 1, negative が 0)となるように評価データを加工し、レビューデータをMeCabを使ってスペース区切りのデータにします。\n", "3. 学習データ、バリデーションデータ、テストデータに分けて、学習用にS3にデータをアップロードします。\n", "\n", "### データの確認\n", "\n", "タブ区切りの tsv ファイルを読んで1行目を表示してみます。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import pandas as pd\n", "\n", "df = pd.read_csv(tsv_file_path, sep ='\\t')\n", "df.head(1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 不要なデータの削除\n", "\n", "今回利用しないデータは以下の2つです。必要なデータだけ選んで保存します。\n", "\n", "- 評価データ `star_rating` と レビューのテキストデータ `review_body` 以外のデータ\n", "- 評価が 3 のデータ (positive でも negative でもないデータ)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df_pos_neg = df.loc[:, [\"star_rating\", \"review_body\"]]\n", "df_pos_neg = df_pos_neg[df_pos_neg.star_rating != 3]" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df_pos_neg.head(1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 評価データ・レビューデータの加工\n", "\n", "BlazingText では以下のようなデータが必要です。\n", "\n", "```\n", "__label__1 私 は これ が 好き です 。\n", "__label__0 私 は これ が きらい です 。\n", "```\n", "\n", "`__label__数字` は文書のラベルを表します。negative `__label__0`、positive なら `__label__1` とします。ラベル以降は、文書をスペース区切りにしたものですので、各文に対して MeCab による形態素解析を実行します。全文の処理に2, 3分必要になる場合があります。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "mecab = MeCab.Tagger(\"-Owakati\")\n", "\n", "def func_to_row(x):\n", " if x[\"star_rating\"] < 3:\n", " label = '0'\n", " else:\n", " label = '1'\n", " x[\"star_rating\"] = \"__label__\" + label\n", " x[\"review_body\"] = mecab.parse(x[\"review_body\"].replace('
', '')).replace('\\n', '')\n", " return x\n", "\n", "labeled_df = df_pos_neg.apply(lambda x: func_to_row(x), axis =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", "data_size = len(labeled_df.index)\n", "train_ratio = 0.8\n", "train_index = np.random.choice(data_size, int(data_size*train_ratio), replace=False)\n", "other_index = np.setdiff1d(np.arange(data_size), train_index)\n", "valid_index = np.random.choice(other_index, int(len(other_index)/2), replace=False)\n", "test_index = np.setdiff1d(np.arange(data_size), np.concatenate([train_index, valid_index]))\n", "\n", "np.savetxt('train.csv',labeled_df.iloc[train_index].values, fmt=\"%s %s\", delimiter=' ') \n", "np.savetxt('validation.csv',labeled_df.iloc[valid_index].values, fmt=\"%s %s\", delimiter=' ') \n", "\n", "print(\"Data is splitted into:\")\n", "print(\"Training data: {} records.\".format(len(train_index)))\n", "print(\"Validation data: {} records.\".format(len(valid_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}`で、バケットがない場合は自動作成されます。もし存在するバケットにアップロードする場合は、このバケット名を引数で指定できます。\n", "\n", "アップロードが終われば、TrainingInput を利用して、アップロードしたファイルの content_type などを指定します。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import sagemaker\n", "from sagemaker.inputs import TrainingInput\n", "\n", "sess = sagemaker.Session()\n", "\n", "s3_train_data = sess.upload_data(path='train.csv', key_prefix='amazon-review-data')\n", "s3_validation_data = sess.upload_data(path='validation.csv', key_prefix='amazon-review-data')\n", "print(\"Training data is uploaded to {}\".format(s3_train_data))\n", "print(\"Validation data is uploaded to {}\".format(s3_validation_data))\n", "\n", "train_data = TrainingInput(s3_train_data, distribution='FullyReplicated', \n", " content_type='text/plain', s3_data_type='S3Prefix')\n", "validation_data = TrainingInput(s3_validation_data, distribution='FullyReplicated', \n", " content_type='text/plain', s3_data_type='S3Prefix')\n", "data_channels = {'train': train_data, 'validation': validation_data}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 学習の実行\n", "\n", "BlazingText はビルトインアルゴリズムなので、アルゴリズムの実装は不要です。BlazingTextのコンテナイメージを呼び出して実行します。`get_image_uri` を利用すればコンテナイメージの URI を取得することができます。 取得した URI とそれを実行するインスタンスなどを指定して、Estimator を呼び出すことで学習の設定を行うことができます。\n", "\n", "ビルトインアルゴリズムでは、実行内容を設定するいくつかのハイパーパラメータを設定する必要があります。BlazingText では `mode` のハイパーパラメータが必須です。テキスト分類を行う場合は `mode=\"supervised\"` の指定が必要です。\n", "\n", "最後に S3 のデータを指定して fit を呼べば学習を始めることができます。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import boto3\n", "region_name = boto3.Session().region_name\n", "container = sagemaker.image_uris.retrieve(\"blazingtext\", region_name)\n", "print('Using SageMaker BlazingText container: {} ({})'.format(container, region_name))" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "bt_model = sagemaker.estimator.Estimator(container,\n", " role=sagemaker.get_execution_role(),\n", " instance_count=1, \n", " instance_type='ml.m4.xlarge',\n", " input_mode= 'File',\n", " sagemaker_session=sess)\n", "\n", "bt_model.set_hyperparameters(mode=\"supervised\",\n", " epochs=10,\n", " vector_dim=10,\n", " early_stopping=True,\n", " patience=4,\n", " min_epochs=5)\n", "\n", "bt_model.fit(inputs=data_channels, logs=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 推論の実行\n", "\n", "学習が終わると、作成されたモデルをデプロイして、推論を実行することができます。デプロイは deploy を呼び出すだけでできます。`---`といった出力があるときはデプロイ中で、`!`が出力されるとデプロイが完了です。\n", "\n", "エンドポイントは json 形式でリクエストを受け付けますので、serializer の content_type に `application/json` を指定します。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "text_classifier = bt_model.deploy(initial_instance_count = 1,instance_type = 'ml.m4.xlarge')\n", "text_classifier.serializer = sagemaker.serializers.IdentitySerializer(content_type = 'application/json')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "デプロイが終わったら推論を実行してみましょう。ここでは negative なレビューを 5件、 positive なレビューを 5件ランダムに選択して推論を実行します。" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "scrolled": false }, "outputs": [], "source": [ "import json\n", "\n", "num_test = 5\n", "test_data = labeled_df.iloc[test_index]\n", "\n", "neg_test_data = test_data[test_data.star_rating == '__label__0']\n", "pos_test_data = test_data[test_data.star_rating == '__label__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", "neg_test_sentences = [text for text in neg_test_data.loc[neg_index][\"review_body\"].values]\n", "payload = {\"instances\" : neg_test_sentences}\n", "response = text_classifier.predict(json.dumps(payload))\n", "predictions = json.loads(response)\n", "\n", "for i, pred in enumerate(predictions):\n", " print(\"Ground Truth: {}, Prediction: {} (probability: {})\"\n", " .format(0, pred[\"label\"][0][-1], pred[\"prob\"]))\n", " print(neg_test_sentences[i].replace(' ', ''))\n", " print()\n", " \n", "pos_test_sentences = [text for text in pos_test_data.loc[pos_index][\"review_body\"].values]\n", "payload = {\"instances\" : pos_test_sentences}\n", "response = text_classifier.predict(json.dumps(payload))\n", "predictions = json.loads(response)\n", "\n", "for i, pred in enumerate(predictions):\n", " print(\"Ground Truth: {}, Prediction: {} (probability: {})\"\n", " .format(1, pred[\"label\"][0][-1], pred[\"prob\"]))\n", " print(pos_test_sentences[i].replace(' ', ''))\n", " print()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "自由に文章を入力して感情分析を行うことも可能です。以下の sentence に自由にレビューを書いて実行してみましょう。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "sentence = \"自由にレビューをかいてみましょう\"\n", "payload = {\"instances\" : [mecab.parse(sentence).replace('\\n','')]}\n", "response = text_classifier.predict(json.dumps(payload))\n", "predictions = json.loads(response)\n", "\n", "for i, pred in enumerate(predictions):\n", " print(\"Prediction: {} (probability: {})\"\n", " .format(pred[\"label\"][0][-1], pred[\"prob\"]))\n", " print(sentence)\n", " print()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 不要になったエンドポイントを削除" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "text_classifier.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 }