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

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは、大澤です。

当エントリではAmazon SageMakerの組み込みアルゴリズムの1つ、「潜在的ディリクレ割り当て(Latent Dirichlet Allocation, LDA)」についてご紹介していきたいと思います。

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

目次

概要説明:LDAとは

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

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

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

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

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

大体次のような流れになります。

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

ノートブックの作成

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

Introduction to Amazon algorithms

LDA-Introduction.ipynb

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

モジュールの読み込み

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

%matplotlib inline
  
import os, re
  
import boto3
import matplotlib.pyplot as plt
import numpy as np
np.set_printoptions(precision=3, suppress=True)
  
# some helpful utility functions are defined in the Python module
# "generate_example_data" located in the same directory as this
# notebook
from generate_example_data import generate_griffiths_data, plot_lda, match_estimated_topics
  
import sagemaker
from sagemaker.amazon.common import numpy_to_record_serializer
from sagemaker.predictor import csv_serializer, json_deserializer

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

環境変数とロールの確認

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

from sagemaker import get_execution_role
 
role = get_execution_role()
bucket = '<your_s3_bucket_name_here>'
prefix = 'sagemaker/DEMO-lda-introduction'
  
print('Training input/output will be stored in {}/{}'.format(bucket, prefix))
print('\nIAM Role: {}'.format(role))

データ取得

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

print('Generating example data...')
num_documents = 6000
num_topics = 5
known_alpha, known_beta, documents, topic_mixtures = generate_griffiths_data(
num_documents=num_documents, num_topics=num_topics)
vocabulary_size = len(documents[0])
  

# separate the generated data into training and tests subsets
num_documents_training = int(0.9*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:]
  
print('documents_training.shape = {}'.format(documents_training.shape))
print('documents_test.shape = {}'.format(documents_test.shape))

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

データの確認

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

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

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

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

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

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

今回の場合だと5つのトピックがあり、1つ目のトピックが99%を構成していて、2つ目と5つ目が0%、3つ目が0.8%、4つ目が0.2%という感じです。

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

%matplotlib inline

fig = plot_lda(documents_training, nrows=3, ncols=4, cmap='gray_r', with_colorbar=True)
fig.suptitle('Example Document Word Counts')
fig.set_dpi(160)

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

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

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

# convert documents_training to Protobuf RecordIO format
recordio_protobuf_serializer = numpy_to_record_serializer()
fbuffer = recordio_protobuf_serializer(documents_training)

# upload to S3 in bucket/prefix/train
fname = 'lda.data'
s3_object = os.path.join(prefix, 'train', fname)
boto3.Session().resource('s3').Bucket(bucket).Object(s3_object).upload_fileobj(fbuffer)
s3_train_data = 's3://{}/{}'.format(bucket, s3_object)

print('Uploaded data to S3: {}'.format(s3_train_data))

訓練

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

from sagemaker.amazon.amazon_estimator import get_image_uri
# select the algorithm container based on this notebook's current location
  
region_name = boto3.Session().region_name
container = get_image_uri(region_name, 'lda')
  
print('Using SageMaker LDA container: {} ({})'.format(container, region_name))

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

num_topics
トピックの数
feature_dim
特徴空間の次元数(今回の例では単語数)
mini_batch_size
文章データの数
alpha0
トピックの事前分布の初期状態をどういう形にするか

詳細はドキュメントをご確認ください。

session = sagemaker.Session()
  
# specify general training job information
lda = sagemaker.estimator.Estimator(
	container,
	role,
	output_path='s3://{}/{}/output'.format(bucket, prefix),
	train_instance_count=1,
	train_instance_type='ml.c4.2xlarge',
	sagemaker_session=session,
)
  
# set algorithm-specific hyperparameters
lda.set_hyperparameters(
	num_topics=num_topics,
	feature_dim=vocabulary_size,
	mini_batch_size=num_documents_training,
	alpha0=1.0,
)
  
# run the training job on input data stored in S3
lda.fit({'train': s3_train_data})

モデルの展開

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

lda_inference = lda.deploy(
	initial_instance_count=1,
	instance_type='ml.m4.xlarge', # LDA inference may work better at scale on ml.c4 instances
)

モデルの確認

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

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

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

results = lda_inference.predict(documents_test[:12])
print(results)

ごちゃごちゃしすぎてて、何も分からないですね…。 推論によって計算されたトピックデータのみを表示してみます。

computed_topic_mixtures = np.array([prediction['topic_mixture'] for prediction in results['predictions']])
  
print(computed_topic_mixtures)

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

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

print(topic_mixtures_test[0]) # known test topic mixture
print(computed_topic_mixtures[0]) # computed topic mixture (topics permuted)

トピックの順序はバラバラですが、割合はそれなりに似た値が出ていますね。 推論によって出されるトピックの順序は意味を持たないので、注意が必要です。

エンドポイントの削除

余分なお金を使わないように、エンドポイントを削除します。

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

まとめ

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

組み込みアルゴリズムの中には同じ用途のアルゴリズムとして、ニューラルトピックモデルというものもあります。そちらも同様に紹介しいますので、宜しければご覧ください。

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

おまけ

Bag of Words形式とは?

Bag of Words(BoW)形式とは、文章データをベクトルで表現する方法の一つです。

以下のような二つの形式があります。

  1. 各単語が出現するかどうかを0,1で数値化して構成されるベクトル
  2. 各単語の出現回数で構成されるベクトル

例えば、文章1I bought a pen. But I lost the pen.と文章2She picked up a pen.という2つの文章があるとすると、
ラベルが["I","she", "picked", "up", "bought", "a", "pen", "But", "lost", "the"]となるBoW形式の文章データは、次のようになります。

1. の形式の場合:
文章1: [1, 0, 0, 0, 1, 1, 1, 1, 1, 1]
文章2: [0, 1, 1, 1, 0, 1, 1, 0, 0, 0]
2. の形式の場合:
文章1: [2, 0, 0, 0, 1, 1, 2, 1, 1, 1]
文章2: [0, 1, 1, 1, 0, 1, 1, 0, 0, 0]

脚注

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