検索
連載

初学者向け「Amazon Comprehend」(AI自然言語処理サービス)をPythonで利用するにはAWSチートシート

AWS活用における便利な小技を簡潔に紹介する連載「AWSチートシート」。今回は、AWSのAI自然言語処理サービス「Amazon Comprehend」をPythonで利用する方法を紹介する。

PC用表示 関連情報
Share
Tweet
LINE
Hatena

 「Amazon Web Services」(AWS)活用における便利な小技を簡潔に紹介する連載「AWSチートシート」。今回は、AWSのAI自然言語処理サービス「Amazon Comprehend」をPythonで利用します。以下、Amazon Comprehendに用意されているメソッドを概観し、幾つかの使い方を紹介します。

「Amazon Comprehend」とは

 AWSには、事前トレーニング済みのAI(人工知能)を手軽に利用できる「AIサービス」が多数用意されており、その内容はコンピュータビジョンから言語、レコメンデーション、予測と多岐にわたります。

 今回紹介するAmazon Comprehend(以下、Comprehend)は、事前トレーニング済みモデルを使用して、テキストからさまざまなインサイトを取得できる自然言語処理(NLP)サービスです。Comprehendを使用することで、テキストから主要言語の識別、人物や場所などの情報(「エンティティ」)の検出、キーフレーズの検出、感情分析、トピックモデリング、個人識別情報(PII)の検出、構文解析(形態素解析)などが可能になります。また、特定のビジネス要件に合わせた転移学習によって、カスタムエンティティの検出とドキュメントのカスタム分類もできます。


Amazon Comprehend

 このようなComprehendの機能を利用することで、例えば、多言語で書かれたカスタマーレビューを言語別、感情別に分類し、その後さらにキーフレーズ検出やトピック分類をすることによって、より詳細なインサイトを得ることができます。

 なお、本稿執筆時点で日本語はComprehendの全ての機能には対応していません。日本語対応機能は前出順に、主要言語の識別からトピックモデリングまでになります。

 また、Comprehendには医療分野での利用に特化した「Amazon Comprehend Medical」(英語のみ対応)が別途提供されていますが、本稿では割愛します。

AWSの「AIサービス」はコンソール画面から利用できますが、開発を念頭に置かなくても、慣れてくれば今回のようにAPIを利用する方がより便利で効率的に感じてくるでしょう。本稿がそのように利用するきっかけになれば幸いです。


利用料金について

 Comprehendは従量課金制で、4つのカテゴリーに大別される各処理に対して、個別に料金が設定されています。なお、以下においてリクエストは100文字を1ユニットとしており、各リクエストには3ユニットの最低料金が発生します。

自然言語処理

 このカテゴリーには、主要言語の識別、エンティティの検出、キーフレーズの検出、感情分析、イベント検出、構文解析が含まれます。

 1ユニット当たりの料金は、主要言語の識別、エンティティの検出、キーフレーズの検出、感情分析の場合、1000万ユニットまでは0.0001ドル、その後5000万ユニットまでは0.00005ドル、その後は0.000025ドルになります。

 イベント検出の場合、1ユニット当たりの料金は1000万ユニットまでは0.003ドル、その後5000万ユニットまでは0.0015ドル、その後は0.00075ドルになります。

 構文解析の場合、1ユニット当たりの料金は1000万ユニットまでは0.00005ドル、その後5000万ユニットまでは0.000025、その後は0.0000125ドルになります。

個人識別情報(PII)(英語のみ対応)

 このカテゴリーには、ドキュメント内の個人識別情報の検出が含まれます。

 Detect PIIの場合、1ユニット当たりの料金は1000万ユニットまでは0.0001ドル、その後5000万ユニットまでは0.00005ドル、その後1億ユニットまでは0.000025ドル、その後は0.000005ドルになります。

 Contains PIIの場合、1ユニット当たりの料金は1000万ユニットまでは0.000002ドル、その後5000万ユニットまでは0.000001ドル、その後1億ユニットまでは0.0000005ドル、その後は0.0000001ドルになります。

カスタム Comprehend

 このカテゴリーには、カスタムモデルのトレーニングと、トレーニング済みカスタムモデルを使用した、テキストのカスタム分類とカスタムエンティティ検出が含まれます。

 モデルトレーニングは1時間に3ドル(秒単位で課金)で、カスタムモデル管理は1カ月に0.50ドルになります。カスタムモデルを使用したカスタム分類とカスタムエンティティ検出の非同期処理は、1ユニット当たり0.0005ドルです。

 また、同期処理は1推論ユニット、1秒当たり0.0005ドルです。ここで、1推論ユニットのスループットは1秒当たり100文字で、追加でプロビジョニングすることもできます。エンドポイントはドキュメント処理実行の有無にかかわらず、起動してから削除されるまでの時間に対して秒単位(最低60秒)で課金されます。

トピックモデリング

 このカテゴリーには、「Amazon S3」に保存されたドキュメントに対するトピック分類が含まれます。料金はジョブで処理されるドキュメントの合計サイズに基づいて課金されます。最初の100MBまでは一律に1.00ドル、100MBを超えた分は1MB当たり0.004ドルになります。

無料利用枠

 なお、Comprehendは無料利用枠の対象になっており、最初のリクエストから1カ月間は8万5000ユニットのテキスト処理が無料です。また、最初のリクエストから12カ月間、1MBまでの5つのトピックモデリングジョブが無料です(ただし、無料利用枠を超えた場合には従量課金が適用されます)。

必要条件

 本稿では、読者の環境で下記要件が満たされていることを仮定しています。

  • AWSアカウントを有しており、「AWS Identity and Access Management」(IAM)ユーザーに必要な権限(今回ならComprehendとS3関連)が付与されていること。ホームディレクトリにAPIを利用するための認証情報が保存されていること
  • AWSが提供するPython用のSDK「Boto3」がインストールされていること

 なおこれは必須ではありませんが、以下のサンプルコードは「Jupyter Notebook」での実行を想定しています。

メソッド一覧

 Comprehendには以下のメソッドが用意されています。なお、ここではバッチ処理と非同期処理に関するメソッドを一部省略しています(これらについては、detect_dominant_language処理の類似メソッドをご参考ください)。また、個人識別情報(PII)関連のメソッドとカスタムモデル関連のメソッドについては割愛します。

メソッド名 機能 引数 戻り値
detect_dominant_language 入力テキストの主要言語を特定する テキスト 辞書
batch_detect_dominant_language バッチ入力ドキュメントの主要言語を特定する ドキュメントのテキストを含むリスト 辞書
start_dominant_language_detection_job ドキュメントコレクションの主要言語を特定する非同期ジョブを開始する 入力データの構成、出力データの構成、データアクセスロールARN、ジョブ名、ClientRequestトークン、Volume用KMSキーID、VPC構成、タグ 辞書
stop_dominant_language_detection_job 実行中の主要言語特定ジョブを停止する JobID 辞書
list_dominant_language_detection_jobs 主要言語特定ジョブ一覧を取得する 戻り値に対するフィルタリング設定、NextToken、レスポンスの最大値 辞書
describe_dominant_language_detection_job 特定の主要言語特定ジョブに関する情報を取得する JobID 辞書
detect_entities テキストからエンティティを検出する テキスト、言語コード、エンドポイントARN(カスタムモデル使用時) 辞書
上記のバッチ、非同期処理関連メソッド - - -
detect_key_phrases テキスト中のキーフレーズ(名詞句)を検出する テキスト、言語コード 辞書
上記のバッチ、非同期処理関連メソッド - - -
detect_sentiment テキスト中の支配的な感情についての推論結果を返す テキスト、言語コード 辞書
上記のバッチ、非同期処理関連メソッド - - -
start_events_detection_job ドキュメントコレクションに対する非同期のイベント検出ジョブを開始する 入力データの構成、出力データの構成、データアクセスロールARN、ジョブ名、言語コード、ClientRequestトークン、対象イベントタイプ、タグ 辞書
stop_events_detection_job 実行中のイベント検出ジョブを停止する JobID 辞書
list_events_detection_jobs イベント検出ジョブ一覧を取得する 戻り値に対するフィルタリング設定、NextToken、レスポンスの最大値 辞書
describe_events_detection_job 特定のイベント検出ジョブに関する情報を取得する JobID 辞書
detect_syntax テキストの形態素解析を行う テキスト、言語コード 辞書
batch_detect_syntax バッチドキュメントの形態素解析 ドキュメントのテキストを含むリスト、言語コード 辞書
contains_pii_entities 検出した個人識別情報(PII)のラベルを返す テキスト、言語コード 辞書
detect_pii_entities 検出した個人識別情報(PII)の情報を返す テキスト、言語コード 辞書
上記の非同期処理関連メソッド - - -
start_topics_detection_job 非同期のトピック検出ジョブを開始する 入力データの構成、出力データの構成、データアクセスロールARN、ジョブ名、言語コード、トピック数、ClientRequestトークン、VolumeKMSキーID、VPC構成、タグ 辞書
list_topics_detection_jobs トピック検出ジョブ一覧を取得する 戻り値に対するフィルタリング設定、NextToken、レスポンスの最大値 辞書
describe_topics_detection_job 特定のトピック検出ジョブに関する情報を取得する JobID 辞書
tag_resource Comprehendのリソースにタグを付ける リソースARN、タグ 辞書
untag_resource Comprehendのリソースから特定のタグを取り除く リソースARN、タグ 辞書
list_tags_for_resource Comprehendの特定のリソースに付けられたタグ一覧を取得する リソースARN 辞書
can_paginate 各メソッドのページネーション有無を調べる メソッド名 真偽値
get_paginator メソッドに関するページネータを生成する メソッド名 ページネータオブジェクト

 Comprehendのドキュメント処理には、同期処理と非同期処理があります。

 同期処理では、メソッドの引数にドキュメントのテキストを直接渡します。同期処理には単一のドキュメントを処理するメソッドと、最大25のドキュメントを同時に処理するバッチ処理用のメソッドが用意されています。1つのドキュメントは5000bytes未満で、UTF-8でエンコードされている必要があります。

 非同期処理では、ドキュメントの読み込みと分析結果の書き込みにS3バケットを使用します。ファイルはUTF-8形式のテキストファイル、PDF、Wordファイルを処理できます。

 なお、S3バケットはComprehendを呼び出す際のAPIエンドポイントと同じリージョンに作成する必要があります。これに関連して本稿執筆時点で、Comprehendは東京リージョンでは提供されていますが、大阪リージョンでは提供されていません。

 以降、幾つかComprehendのメソッドを実行して解説します。サンプルコードの実行に際しては、本稿と必ずしも同じ結果が得られるものではない点にご留意ください。

detect_dominant_languageメソッド

 detect_dominant_languageメソッドは、入力テキストの主要言語を特定します。以下では、ECサイトの書籍関連カスタマーレビューを参考に筆者が作成した疑似レビュー使用して、主要言語を特定するバッチ処理用メソッドを実行します。

reviews = ['My wife really enjoyed this book and so did I!',
           'This book becomes less interesting towards the end. '
           'この本は終盤に向かうにつれてつまらなくなりました。',
           'Cathedral, the last story collected in this book, is one of my '
           'favorite short stories by Raymond Carver.',
           'すごくわかりやすい!要点が簡潔にまとめてある。1ヶ月で無事合格できた。',
           '出題範囲を一通り網羅してるのは良いと思います。'
           'ただ、この本だけで対策するのは厳しいと思います。',
           '他の方のレビューにもありますが、著者の意見は賛否が分かれる内容だと思いました。',
           'ベストセラー作品なので期待して購入しましたが、私にはイマイチでした。',
           'この本に収録されている背の曲がった男は、シャーロック・ホームズシリーズの中でも'
           '特に好きな作品の一つです。']
 
import boto3
 
# リージョンを(東京リージョンに)指定
region = 'ap-northeast-1'
 
# クライアントを作成
comprehend = boto3.client('comprehend', region)
 
# 主要言語を特定するバッチ処理を実行
response = comprehend.batch_detect_dominant_language(TextList=reviews)
 
# ドキュメントごとに結果を取得
for result in response['ResultList']:
    # 原文を表示
    print(result['Index'] + 1, reviews[result['Index']])
    # 検出された言語情報を取得して表示
    for language in result['Languages']:
        print(f"言語コード:{language['LanguageCode']} "
              f"信頼度:{language['Score']:.2f}")
    print()
1 My wife really enjoyed this book and so did I!
言語コード:en 信頼度:0.98
 
2 This book becomes less interesting towards the end. この本は終盤に向かうにつれてつまらなくなりました。
言語コード:ja 信頼度:0.41
言語コード:en 信頼度:0.58
 
3 Cathedral, the last story collected in this book, is one of my favorite short stories by Raymond Carver.
言語コード:en 信頼度:0.97
 
4 すごくわかりやすい!要点が簡潔にまとめてある。1ヶ月で無事合格できた。
言語コード:ja 信頼度:1.00
 
5 出題範囲を一通り網羅してるのは良いと思います。ただ、この本だけで対策するのは厳しいと思います。
言語コード:ja 信頼度:1.00
 
6 他の方のレビューにもありますが、著者の意見は賛否が分かれる内容だと思いました。
言語コード:ja 信頼度:1.00
 
7 ベストセラー作品なので期待して購入しましたが、私にはイマイチでした。
言語コード:ja 信頼度:1.00
 
8 この本に収録されている背の曲がった男は、シャーロック・ホームズシリーズの中でも特に好きな作品の一つです。
言語コード:ja 信頼度:1.00

 英語と日本語のレビューに対して、それぞれ言語を正しく特定できました。

detect_entitiesメソッド

 detect_entitiesメソッドは、入力テキストからエンティティを検出します。ここでも先ほどの疑似レビュー使用して、バッチ処理用のメソッドを実行します。

# エンティティを検出するバッチ処理を実行
response = comprehend.batch_detect_entities(TextList=reviews,
                                            LanguageCode='ja')
 
# ドキュメントごとに結果を取得
for result in response['ResultList']:
    # エンティティが検出さた場合
    entities = result['Entities']
    if entities:
        # 原文を表示
        print(result['Index'] + 1, reviews[result['Index']])
        for entity in entities:
            # エンティティを表示
            print(f"Score:{entity['Score']:.2f} "
                  f"Type:{entity['Type'].ljust(8)} "
                  f"Text:{entity['Text']}")
        print()
3 Cathedral, the last story collected in this book, is one of my favorite short stories by Raymond Carver.
Score:0.83 Type:TITLE    Text:Cathedral
Score:0.62 Type:QUANTITY Text:story
Score:0.97 Type:QUANTITY Text:one of my favorite short stories
Score:1.00 Type:PERSON   Text:Raymond Carver
 
4 すごくわかりやすい!要点が簡潔にまとめてある。1ヶ月で無事合格できた。
Score:0.99 Type:QUANTITY Text:1ヶ月
 
5 出題範囲を一通り網羅してるのは良いと思います。ただ、この本だけで対策するのは厳しいと思います。
Score:0.53 Type:QUANTITY Text:通り
 
8 この本に収録されている背の曲がった男は、シャーロック・ホームズシリーズの中でも特に好きな作品の一つです。
Score:0.76 Type:TITLE    Text:背の曲がった男
Score:0.60 Type:TITLE    Text:シャーロック・ホームズシリーズ
Score:0.56 Type:QUANTITY Text:一つ

 Comprehendが検出するエンティティのタイプには、商品(COMMERCIAL_ITEM)、日付(DATE)、イベント(EVENT)、場所(LOCATION)、組織(ORGANIZATION)、人物(PERSON)、数量(QUANTITY)、作品(TITLE)、その他が(OTHER)があります。

 今回の結果では、確かにこれらのタイプに該当するエンティティが検出されました。特に、疑似レビューでは本来作品名である「Cathedral(大聖堂)」と「背の曲がった男」に意図的に引用符を付けませんでしたが、このような一般名詞(句)も作品名として正しく検出されました。

 なお、今回はメソッドの必須引数「言語コード」に日本語を指定したものの、英語のドキュメントも正しく解析されました。ただし公式ドキュメントには「ドキュメント中の言語は全て統一されている必要がある」との記載があります。

detect_sentimentメソッド

 detect_sentimentメソッドは、入力テキスト中の支配的な感情について推論します。ここでもこれまでと同じ疑似レビュー使用して、バッチ処理のメソッドを実行します。

# 感情分析のバッチ処理を実行
response = comprehend.batch_detect_sentiment(TextList=reviews,
                                             LanguageCode='ja')
 
# ドキュメントごとに結果を取得
for result in response['ResultList']:
    # 原文を表示
    print(result['Index'] + 1, reviews[result['Index']])
    # 結果を表示
    score = result['SentimentScore']
    print(f"総合:{result['Sentiment'].ljust(8)} ("
          f"Positive:{score['Positive']:.2f} "
          f"Negative:{score['Negative']:.2f} "
          f"Neutral:{score['Neutral']:.2f} "
          f"Mixed:{score['Mixed']:.2f})\n")
1 My wife really enjoyed this book and so did I!
総合:POSITIVE (Positive:1.00 Negative:0.00 Neutral:0.00 Mixed:0.00)
 
2 This book becomes less interesting towards the end. この本は終盤に向かうにつれてつまらなくなりました。
総合:POSITIVE (Positive:0.62 Negative:0.36 Neutral:0.00 Mixed:0.02)
 
3 Cathedral, the last story collected in this book, is one of my favorite short stories by Raymond Carver.
総合:POSITIVE (Positive:0.99 Negative:0.00 Neutral:0.01 Mixed:0.00)
 
4 すごくわかりやすい!要点が簡潔にまとめてある。1ヶ月で無事合格できた。
総合:POSITIVE (Positive:1.00 Negative:0.00 Neutral:0.00 Mixed:0.00)
 
5 出題範囲を一通り網羅してるのは良いと思います。ただ、この本だけで対策するのは厳しいと思います。
総合:MIXED    (Positive:0.00 Negative:0.01 Neutral:0.00 Mixed:0.99)
 
6 他の方のレビューにもありますが、著者の意見は賛否が分かれる内容だと思いました。
総合:NEUTRAL  (Positive:0.00 Negative:0.12 Neutral:0.88 Mixed:0.00)
 
7 ベストセラー作品なので期待して購入しましたが、私にはイマイチでした。
総合:MIXED    (Positive:0.01 Negative:0.45 Neutral:0.00 Mixed:0.54)
 
8 この本に収録されている背の曲がった男は、シャーロック・ホームズシリーズの中でも特に好きな作品の一つです。
総合:POSITIVE (Positive:1.00 Negative:0.00 Neutral:0.00 Mixed:0.00)

 Comprehendが検出する感情のタイプには、肯定的(positive)、否定的(negative)、中立的(neutral)、混在した(mixed)の4つがありますが、今回の検出結果は基本的に人間の感覚に近いものになったのではないでしょうか。

 7番のレビューはNEGATIVEではなくMIXEDと判定されましたが、これは「ベストセラー作品なので」という部分が影響したものと考えられます。この部分を削除するとNEGATIVEと判定されました。

 なお、今回はメソッドの必須引数「言語コード」に日本語を指定したものの、英語のドキュメントも正しく解析されました。ただし公式ドキュメントには「ドキュメント中の言語は全て統一されている必要がある」との記載があります。

 2番のレビューがPOSITIVEと判定されたのは、言語が混ざっていたことも影響したのかもしれません。

トピックモデリング

 Comprehendによるトピックモデリングを行います。

 今回はサンプルデータセットとして、「Amazon Review Data(2018)」から「化粧品」と「雑誌」に関する下記の英語レビューデータを使用します。この2種類のカテゴリーのレビューを1つにまとめたデータセットを「入力」としたときに、トピックモデリングによって各レビューを元のカテゴリーに分類できるかどうか試します。

※データ取得元

 初めに、上記リンクからダウンロードしたファイルを加工して、トピックモデリング用の入力ファイルを作成します。次に、S3にバケットを作成して、加工したファイルをアップロードします。その後、Comprehendのトピック検出ジョブを実行して、完了後に書き出されたファイルをダウンロードして結果を確認します。

入力ファイルを作成

 上記リンクからダウンロードしたファイルを実行中のipynbファイルと同じフォルダに置いた上で、下記スクリプトを実行します。

 なお、Comprehendのトピックモデリングジョブでは、少なくとも1000個のドキュメントを使用する必要があります。また、各ドキュメントの長さは3文以上でなければなりません。そこで、下記スクリプトでは各レビューの文章数を(トークナイザーを用いずに)簡易的にカウントして、3文以上のレビューがあるレコードのみ残します。その後、「化粧品」(beauty)と「雑誌」(magazine)カテゴリーからそれぞれ550件ずつランダムにレコードを抽出して、最終的な入力データを作成します。

import gzip
import json
import pandas as pd
import re
 
# レビューデータをロード
beauty_data = []
with gzip.open('All_Beauty_5.json.gz') as f:
    for l in f:
        beauty_data.append(json.loads(l.strip()))
 
magazine_data = []
with gzip.open('Magazine_Subscriptions_5.json.gz') as f:
    for l in f:
        magazine_data.append(json.loads(l.strip()))
 
# データをPandasのデータフレームに変換
df_beauty = pd.DataFrame.from_records(beauty_data)
df_magazine = pd.DataFrame.from_records(magazine_data)
 
# 「トピック」列を追加
df_beauty['topic'] = 'beauty'
df_magazine['topic'] = 'magazine'
 
# データフレームを結合
df = pd.concat([df_beauty, df_magazine])
 
# レビューが重複しているレコードを削除
df.drop_duplicates(subset='reviewText', inplace=True)
 
# レビュー中の改行文字を削除
df['reviewText'] = df['reviewText'].apply(lambda x: str(x).replace('\n', ' '))
 
# 「文章数」列を追加して、文章数が3以上のレコードのみ残す
eos = r'\.+|\?!|!\?|\?+|!+'
df['numSent'] = df['reviewText'].apply(lambda x: len(re.split(eos, str(x))) - 1)
df = df[df['numSent'] >= 3]
 
# ピックごとに550件ずつサンプリング
df = pd.concat([df[df.topic == 'beauty'].sample(550, random_state=1), 
                df[df.topic == 'magazine'].sample(550, random_state=1)])
 
# データを確認
print(df.groupby('topic').agg(reviewText_count=('reviewText', 'count'),
                              overall_mean=('overall', 'mean'),
                              numSent_mean=('numSent', 'mean')
                              ).round(2))
 
# 以降のトピックモデリングで使用する入力ファイル名
input_file = 'amazon_reviews.txt'
 
# レビューをファイルに書き出し
df.reviewText.to_csv(input_file, header=None, index=None)
          reviewText_count  overall_mean  numSent_mean
topic                                                 
beauty                 550          4.52          7.02
magazine               550          4.11          7.41

 以上で入力データの用意ができました。なお、最終段階でのデータの集約結果を確認した限りでは、評価(overall)の平均値と文章数(numSent)の平均値にトピック間で大きな差異はないようです。

S3にバケットを作成してレビューファイルをアップロード

 次に、S3にバケットを作成して、先ほど作成したレビューファイルをアップロードします。

import uuid
 
# S3クライアントを作成
s3 = boto3.client('s3', region)
 
# 一意識別子を利用してバケット名を作成
bucket = 'comprehend-' + str(uuid.uuid1())
 
# S3にバケットを作成
response = s3.create_bucket(
                Bucket=bucket,
                CreateBucketConfiguration={'LocationConstraint': region})
 
# 入力ファイルのオブジェクトキー
input_object_key = f'input/{input_file}'
 
# S3にファイルをアップロード
s3.upload_file(input_file, bucket, input_object_key)

 以上で準備ができました。

トピック検出ジョブの実行

 トピック検出ジョブを実行します。今回はトピック数を2に設定して、Comprehendによって「化粧品」と「雑誌」というカテゴリーをトピック分類の観点から再構成することができるかどうか試してみます。なお、下記ジョブの完了までには30分程度を要しました。

import time
 
# 入出力データの構成を設定
input_s3_url = f's3://{bucket}/{input_object_key}'
input_doc_format = 'ONE_DOC_PER_LINE'
output_s3_url = f's3://{bucket}/output'
input_data_config = {'S3Uri': input_s3_url, 'InputFormat': input_doc_format}
output_data_config = {'S3Uri': output_s3_url}
 
# データアクセスロールARNを設定
data_access_role_arn = '<data_access_role_arn>'
# ジョブ名を指定
job_name = 'topic-detection-sample-job'
# トピック数を指定
number_of_topics = 2
 
# トピック検出ジョブを開始
response = comprehend.start_topics_detection_job(
                            InputDataConfig=input_data_config,
                            OutputDataConfig=output_data_config,
                            DataAccessRoleArn=data_access_role_arn,
                            JobName=job_name,
                            NumberOfTopics=number_of_topics)
 
# ジョブIDを取得 
job_id = response['JobId']
 
while True:
    # ジョブの情報を取得
    response = comprehend.describe_topics_detection_job(
        JobId=job_id
    )
    # ステータスを取得
    status = response['TopicsDetectionJobProperties']['JobStatus']
    
    # ステータスが次のいずれかになったらwhile文を抜け出す
    if status in ['COMPLETED', 'FAILED', 'STOP_REQUESTED', 'STOPPED']:
        print(' Status:', status)
        break
 
    print('>', end='')
    time.sleep(60)
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>Status: COMPLETED

 ジョブが無事完了したら、結果ファイルをダウンロードします。なお、結果ファイルは2つのCSVファイル「topic-terms.csv」「doc-topics.csv」をまとめた圧縮ファイルになっています。

# 出力ファイルのオブジェクトキーを取得
output_uri = response['TopicsDetectionJobProperties']['OutputDataConfig']['S3Uri']
output_object_key = ''.join(output_uri.partition('output')[1:])
 
# S3から結果ファイルをダウンロード
output_file = 'output.tar.gz'
s3.download_file(bucket, output_object_key, output_file)

出力結果を確認

 初めに、topic-terms.csvファイルの内容を確認します。このファイルには、Comprehendが検出したトピック(整数値)に関連付く単語とそのウェイトが記載されています。今回は「化粧品」と「雑誌」のカテゴリーに含まれるレビューをトピック数2でトピック分類しています。

import tarfile
# ファイルを抽出
with tarfile.open(output_file, 'r:gz') as t:
    t.extractall(path='output')
# topic-terms.csvをデータフレームとして読み込み
df_terms = pd.read_csv('output/topic-terms.csv')
df_terms
	topic	term		weight
0	0	magazine	0.072765
1	0	read		0.028974
2	0	article		0.027116
3	0	lot		0.016908
4	0	interest	0.015990
5	0	year		0.014571
6	0	issue		0.013651
7	0	subscription	0.013586
8	0	enjoy		0.013173
9	0	recipe		0.011919
10	1	soap		0.052583
11	1	scent		0.041479
12	1	smell		0.039884
13	1	skin		0.029635
14	1	bar		0.022433
15	1	feel		0.017296
16	1	body		0.015292
17	1	leave		0.013945
18	1	nice		0.017049
19	1	fragrance	0.013848

 結果を見ると、トピック0には、magazine(雑誌)、read(読む)、article(記事)、issue(号)、subscription(購読)など、雑誌に関する単語が含まれています。また、トピック1には、soap(せっけん)、scent(香り)、smell(匂い)、skin(肌)、fragrance(芳香)など、化粧品に関する単語が関連付いています。Comprehendがトピック分類を期待した通りに行ったことが確認できます。

 次に、doc-topics.csv ファイルの内容を確認します。このファイルには、入力ファイルの各行(各レビュー)が含むトピックとその割合が記載されています。先ほどの結果を踏まえると、Comprehendが分類したトピックは、0が「雑誌」、1が「化粧品」に対応すると考えられます。

# doc-topics.csvをデータフレームとして読み込み
df_topics = pd.read_csv('output/doc-topics.csv')
 
# ドキュメント名(docname)列からドキュメント番号(doc_num)列を作成
df_topics['doc_num'] = df_topics.docname.apply(lambda x: int(x.split(':')[1]))
 
# ドキュメント番号順にデータをソート
df_topics.sort_values('doc_num', inplace=True)
df_topics.head(10)
	docname	topic	proportion	doc_num
596	amazon_reviews.txt:0	1	1.00000	0
818	amazon_reviews.txt:1	1	1.00000	1
205	amazon_reviews.txt:2	1	1.00000	2
66	amazon_reviews.txt:3	1	1.00000	3
273	amazon_reviews.txt:4	1	0.71736	4
274	amazon_reviews.txt:4	0	0.28264	4
451	amazon_reviews.txt:5	1	1.00000	5
743	amazon_reviews.txt:6	1	1.00000	6
0	amazon_reviews.txt:7	1	1.00000	7
885	amazon_reviews.txt:8	1	1.00000	8

 出力結果を見ると、トピックを「1つだけ含む」と判断されたレビューが多くを占めますが、中には「2つ含む」と判断されたレビューもあります。

「正解率」を計算してみる

 この結果に対して、「正解率」を計算してみます。具体的には、2つのトピックを含むと判断されたドキュメントにはproportion値が大きい方のトピックを割り当てた上で、ドキュメント全体に対して、Comprehendが予測したトピックとそのドキュメントが本来属していたレビューのカテゴリーが一致する割合を求めます。

# 「正解データ」列を作成
df_topics.loc[df_topics.doc_num<550, 'true'] = 1
df_topics.loc[df_topics.doc_num>=550, 'true'] = 0
 
# proportionが0.5より大きいレコードだけ残す
df_topics = df_topics[df_topics.proportion.round() > 0].reset_index(drop=True)
 
# 「正解率」
print(f'正解率:{(df_topics.topic == df_topics.true).sum()/len(df_topics):.2f}')
正解率:0.94

 筆者が行った際の「正解」率は0.94で、1100件のレビューデータに対してComrehendが検出したトピックは、そのドキュメントが本来属していたカテゴリーとほとんど一致しました。

 なお、ここでは検出されたトピックともとのカテゴリーが一致することを「正解」としましが、トピックモデリングの観点からはそれが必ずしも正解とは言えない点を確認しておきます。それでも、今回一致しなかったレビューにどのような特徴があるか気になる方は、個々のレビューを具体的に確認してみてください。

後片付け

 最後に、S3バケット内のオブジェクトを全て削除して、S3バケット自体も削除します。

# オブジェクトを削除
response = s3.list_objects_v2(Bucket=bucket)
for content in response['Contents']:
    s3.delete_object(Bucket=bucket, Key=content['Key'])
 
# バケットを削除
s3.delete_bucket(Bucket=bucket)

最後に

 いかがだったでしょうか。Comprehendには今回試したメソッド以外にも、さまざまなものが用意されています。興味を持った方は公式ドキュメントを参考にしながら、いろいろと試してみてください。

筆者紹介

金 晟基(キム ソンギ)

株式会社システムシェアード

東京ITスクールでJava研修の講師、IT専門学校の教材、カリキュラム開発、一般社団法人とのプログラミング教育を通じた国際貢献事業などを担当。AWS認定資格は「機械学習 - 専門知識」など


Copyright © ITmedia, Inc. All Rights Reserved.

ページトップに戻る