AmazonSageMakerでトピックを調べる -NTM編-

こんにちは、大澤です。

当エントリではAmazon SageMakerの組み込みアルゴリズムの1つ、「ニューラルトピックモデル (Neural Topic Model, NTM)」についてご紹介していきたいと思います。

「組み込みアルゴリズム」の解説については下記エントリをご参照ください。

目次

概要説明:NTMとは

NTMとは文章がどういうトピックで構成されているかを調べるための機械学習アルゴリズムです。

トピックというのはカテゴリや分類みたいなものです。
一つの文章は潜在的に複数のトピックで混ざり合って構成されていて、一つのトピックは潜在的に幾つかの単語で構成されているという仮定をすることができます *1
その仮定の元でNTMは、文章を各トピックがどれだけの割合で構成しているのか、トピックを各単語がどれだけの割合で構成しているのかを調べることができます。

用途としては、文章の分類器の前段などが考えられます。

SageMakerの組み込みアルゴリズムには同様の用途で使われるアルゴリズム、Latent Dirichlet Allocation(LDA)があります。 アルゴリズムは違うので、同じものをLDAとNTMに入れても違う結果が出る可能性があります。 目的に応じて精度が良いものを利用するのが良いと思います。

組み込みアルゴリズム:NTMの解説と実践

SageMakerのサンプルプログラムに沿って進めていきます。

ざっくりいうと、次のような流れになります。

  1. サンプルの文章データとトピックデータを作成
  2. 文章データを用いて訓練
  3. トピックデータを推論
  4. 推論したデータの検証

ノートブックの作成

SageMakerのノートブックインスタンスを立ち上げて、 SageMaker Examples

Introduction to Amazon algorithms

ntm_synthetic.ipynb

use
でサンプルからノートブックをコピーして、開きます。
ノートブックインスタンスの作成についてはこちらをご参照ください。

環境変数とロールの確認

訓練データ等を保存するS3のバケット名と保存オブジェクト名の接頭辞を決めます。

bucket = '<your_s3_bucket_name_here>'
prefix = 'sagemaker/DEMO-ntm-synthetic'
 
# Define IAM role
import boto3
import re
from sagemaker import get_execution_role

role = get_execution_role()

モジュールの読み込み

これから進めていく中で必要なモジュールを読み込みます。

import numpy as np
from generate_example_data import generate_griffiths_data, plot_topic_data
import io
import os
import time
import json
import sys
import pandas as pd
import matplotlib.pyplot as plt
from IPython.display import display
import scipy
import sagemaker
import sagemaker.amazon.common as smac
from sagemaker.predictor import csv_serializer, json_deserializer

generate_example_dataは同じディレクトリに配置されたモジュールです。 これから進めていく中で便利な関数が定義されています。

データ取得

BagOfWords形式の文章データ(各単語の出現回数ベクトル)とトピックデータを作成し、訓練用とテスト用に分けます。

# generate the sample data
num_documents = 5000
num_topics = 5
vocabulary_size = 25
known_alpha, known_beta, documents, topic_mixtures = generate_griffiths_data(
    num_documents=num_documents, num_topics=num_topics, vocabulary_size=vocabulary_size)

# separate the generated data into training and tests subsets
num_documents_training = int(0.8*num_documents)
num_documents_test = num_documents - num_documents_training

documents_training = documents[:num_documents_training]
documents_test = documents[num_documents_training:]

topic_mixtures_training = topic_mixtures[:num_documents_training]
topic_mixtures_test = topic_mixtures[num_documents_training:]

data_training = (documents_training, np.zeros(num_documents_training))
data_test = (documents_test, np.zeros(num_documents_test))

generate_griffiths_dataはLatent Dirichlet Allocationの理論を利用して、文章データを作成する関数です。

データの確認

先ほど作成したトピックデータ(topic_mixtures)は、対応する文章を構成する各トピックの寄与率を表しています。

一つ目の文章とそれに対応したトピックデータを実際に確認してみます。

print('First training document = {}'.format(documents[0]))
print('\nVocabulary size = {}'.format(vocabulary_size))

np.set_printoptions(precision=4, suppress=True)

print('Known topic mixture of first training document = {}'.format(topic_mixtures_training[0]))
print('\nNumber of topics = {}'.format(num_topics))

実行してみると上のような表が出てくると思います。 (文章生成処理はランダム要素があるので、結果が異なる場合があります。)

1番目の画像が文章データで、2番目の画像が対応するトピックデータです。 練習のために作られた文章データを使っているので、文章データの列がどういった単語に該当するのかという紐付けは存在しません。

今回の場合だと5つのトピックがあり、1つ目のトピックが0.91%、2つ目が9.5%、3つ目が0.0.07%、4つ目が89.51%、5つ目が0%という感じで文章を構成しているようです。

次に、視覚的に表現されたものの方が分かりやすいと思うので、最初の12個の文章をグラフに描画してみます。 また、各文章データは25個の単語で構成されているので、5*5の形に書き換えて、グラフに描画します。

%matplotlib inline

fig = plot_topic_data(documents_training[:10], nrows=2, ncols=5, cmap='gray_r', with_colorbar=False)
fig.suptitle('Example Documents')
fig.set_dpi(160)

各ピクセルは単語を表していて、色の濃さがその単語の出現頻度を表しています。 文章によって様々なグラフが出てきましたが、色の濃いピクセルと薄いピクセルがくっきり分かれている場合は文章中の単語が特定の単語が多い文章という感じでしょうか。 ただ、今回の場合は文章データを機械的にBagOfWords形式で生成したものなので、よくみる言語的に理解ができるものがあるわけではありません。
なので、上のグラフのような傾向の文章について分析しているという仮定のもとで、進めていきます。

データを変換して、S3に保存する

訓練データをRecordIO形式に変換して、S3の冒頭で指定したバケットに保存します。

buf = io.BytesIO()
smac.write_numpy_to_dense_tensor(buf, data_training[0].astype('float32'))
buf.seek(0)

key = 'ntm.data'
boto3.resource('s3').Bucket(bucket).Object(os.path.join(prefix, 'train', key)).upload_fileobj(buf)
s3_train_data = 's3://{}/{}/train/{}'.format(bucket, prefix, key)

訓練

NTM用のコンテナの名前を取得します。

from sagemaker.amazon.amazon_estimator import get_image_uri
container = get_image_uri(boto3.Session().region_name, 'ntm')

ハイパーパラメータを設定して、訓練処理を実行します。
今回設定する各ハイパーパラメータについては以下の通りです。

num_topics
トピックの数
feature_dim
特徴空間の次元数(今回の例では単語数)

その他に、チューニングが必要なハイパーパラメータが幾つかあります。 詳細はドキュメントをご確認ください。

sess = sagemaker.Session()

ntm = sagemaker.estimator.Estimator(container,
                                    role, 
                                    train_instance_count=1, 
                                    train_instance_type='ml.c4.xlarge',
                                    output_path='s3://{}/{}/output'.format(bucket, prefix),
                                    sagemaker_session=sess)
ntm.set_hyperparameters(num_topics=num_topics,
                        feature_dim=vocabulary_size)

ntm.fit({'train': s3_train_data})

モデルの展開

エンドポイントを作成し、訓練したモデルを展開します。
エンドポイントが立ち上がっている間は課金が発生するので、注意が必要です。

ntm_predictor = ntm.deploy(initial_instance_count=1,
                           instance_type='ml.m4.xlarge')

モデルの確認

先ほど作成したエンドポイントへデータを渡すときのシリアライズ方法と、データを受け取るときのデシリアライズ方法を指定します。

ntm_predictor.content_type = 'text/csv'
ntm_predictor.serializer = csv_serializer
ntm_predictor.deserializer = json_deserializer

いくつかデータを投げてみて、動きを確認してみます。

results = ntm_predictor.predict(documents_training[:10])
print(results)

推論によって計算されたトピックデータを整形して表示します。

predictions = np.array([prediction['topic_weights'] for prediction in results['predictions']])

print(predictions)

それっぽい値が表示されるのが分かると思います。

次に、実際のデータと推論のデータを比較してみます。

predictions = np.array([prediction['topic_weights'] for prediction in results['predictions']])

print(predictions)

推論によって出されるトピックの順序は意味を持たないので、どのトピック同士が対応しているのかが分かりにくいですね。 そこで、正しいトピックデータと推論によって出されたトピックデータの相関関係から探っていきます。

def predict_batches(data, rows=1000):
    split_array = np.array_split(data, int(data.shape[0] / float(rows) + 1))
    predictions = []
    for array in split_array:
        results = ntm_predictor.predict(array)
        predictions += [r['topic_weights'] for r in results['predictions']]
    return np.array(predictions)

処理が作成できたので、まとめて推論します。

predictions = predict_batches(documents_training)

推論処理が問題なく終わったので、正しいトピックデータと推論ででたトピックデータの相関を見てみます。

data = pd.DataFrame(np.concatenate([topic_mixtures_training, predictions], axis=1), 
                    columns=['actual_{}'.format(i) for i in range(5)] + ['predictions_{}'.format(i) for i in range(5)])
display(data.corr())
pd.plotting.scatter_matrix(pd.DataFrame(np.concatenate([topic_mixtures_training, predictions], axis=1)), figsize=(12, 12))
plt.show()

正しいトピックデータと推論値のトピックデータに関する相関係数の表と対散布図が表示されました。対散布図の対角線上の図はヒストグラムです。 正しいトピックデータ同士の対散布図(左上の5×5)は綺麗な三角形になっています。人口的に作成したトピックデータのため、このようになっているんだと考えられます。

相関係数を見てみると、actual_0とprediction_2、actual_1とprediction_0、actual_2とprediction_1、actual_3とprediction_3、actual_4とprediction_4という感じの組み合わせのように見えます。ただ、その組み合わせ同士でも相関係数がそれほど高くないものもあるのですが、ハイパーパラメータのチューニングによってこの辺りは改善するものと思われます。

エンドポイントの削除

余分な課金が発生しないよう、最後にエンドポイントを削除します。

sagemaker.Session().delete_endpoint(lda_inference.endpoint)

まとめ

Amazon SageMakerの組み込みアルゴリズムの一つであるNTMを用いることで、文章データからトピックデータの推論を行うことができました。

同じ用途のアルゴリズムであるLDAについても同様の紹介ブログを書いたんですが、同じ入力データを用いているため、 ほぼほぼ同じ感じで進めることができました。それぞれ試しやすくて良いですね。

以下シリーズではAmazon SageMakerのその他の組み込みアルゴリズムについても解説しています。宜しければ御覧ください。

脚注

  1. 実際は確率分布が絡んできますが、今回は概要ということでぼかして表現しています