Amazon Web Services ブログ

ノバルティスAG、Amazon SageMakerと Amazon Neptuneを使い、BERTによるナレッジグラフの構築と充実を図る (Part 2/4)

このブログは、AWSとNovartis AGの戦略的コラボレーションのもと、AWSプロフェッショナルサービスチームがBuying Engineプラットフォームを構築したことを紹介する4部構成のシリーズ第2回の記事を取り出して翻訳したものです。このシリーズの内容は以下の通りです。

この記事では、ビジネスアナリストにカタログデータの全体ビューとインサイトを提供する購買エンジンのナレッジベースに焦点を当てています。Novartis AGが
TensorFlow 2やAmazon Neptuneとともに、Amazon SageMakerをどのように併用してインハウスのナレッジベースの構築と充実を図っているかについて詳しく説明します。

Amazon SageMakerは、機械学習 (ML) モデルを迅速に構築、トレーニング、デプロイする機能を開発者とデータサイエンティストに提供するフルマネージドサービスです。Amazon SageMakerは、機械学習プロセスの各ステップから面倒な作業を取り除き、高品質なモデルを容易に開発することを可能にします。SageMaker Python SDKは、いくつかの異なる機械学習およびディープラーニングフレームワークを使用して、Amazon SageMakerでのモデルのトレーニングとデプロイを可能にするオープンソース API とコンテナを提供します。コードの実行には、Amazon SageMaker ノートブックインスタンスを使用します。Amazon SageMakerノートブックインスタンスの使用方法については、AWS Documentationを参照してください。

Amazon Neptuneは高速かつ信頼性の高いフルマネージド型グラフデータベースサービスです。このサービスでは、高度に接続されたデータセットと連携するアプリケーションを簡単に構築および実行できます。Neptuneの中核となるのは専用の高性能グラフデータベースエンジンです。このエンジンは、数十億の関係を保存し、ミリ秒単位のレイテンシーでグラフをクエリできるよう最適化されています。Neptuneは、推奨エンジン、不正検出、ナレッジグラフ、創薬、ネットワークセキュリティなどのグラフのユースケースを強化します。

プロジェクトの動機

ナレッジベースを構築するために、Amazon Neptune、Amazon DynamoDBまたはAmazon S3など、いくつかのオプションを検討しました。お客様の要件を起点に逆算して考えたところ、Amazon Neptuneのナレッジグラフを使用すると、ユースケースにより適切に対処できることがわかりました。ナレッジグラフは、概念の定義、そのプロパティ、それらの間の関係、および想定される論理的制約のセットを用いて、特定のドメインのセマンティクスをキャプチャします。たとえば、製品とプロパティの間の関連性やつながりは、グラフとして見た方がわかりやすくなります。これは、より良いアイテムの選択やレコメンデーションにつながります。さらに、グラフには、複数のレイヤー(製品レイヤー、クリックストリームデータレイヤー)が含まれている場合があり、プロセスの過程でそれらのデータによる充実が図られます。詳細については、「Knowledge Graphs on AWS」を参照してください。

Novartisの購買エンジンカタログには、さまざまなベンダーやサプライヤーの製品が含まれています。製品は、構造化されていないテキストによる記述と一貫性のないプロパティマッピングで構成されることが多いため、このような一元化されたグラフ表現を構築する場合、一貫性を確保することは困難です。この課題に対処するために、SageMakerを活用して、BERTに基づくカスタマイズされた固有表現抽出 (NER) モデルを構築し、トレーニングします。これにより、製品属性を抽出し、それをナレッジグラフの標準化された入力として使用することが可能になります。

この記事では、同様の課題に取り組むデータサイエンティストのために、固有表現抽出の技術を使用してナレッジグラフを構築し、データを充実させるための技術ガイドラインを提供します。

また、これ以降の内容においては、次の手順について紹介します。

  1. ノートブックのセットアップ
  2. SageMaker上のNER BERTのためのカスタムトレーニングスクリプト
  3. SageMakerで大規模に推論をデプロイするためのカスタム推論スクリプト
  4. ナレッジグラフのセットアップとNeptuneを使用したクエリの実行

ノートブックのセットアップ

  1. S3バケットを作成します。これは、サンプルとして生成されたファイルを保存するためにノートブック全体で使用されます。
  2. SageMakerノートブックインスタンスを作成します。次の点に注意してください。
    • ステップ1で作成したS3バケットからの読み取り/書き込みができるよう、実行ロールに追加の権限を付与する必要があります。
    • ノートブックインスタンスをVirtual Private Cloud (VPC) 内に配置する場合は、VPCがパブリックPypiリポジトリとaws-samples/リポジトリへのアクセスを許可していることを確認してください。
    • 次のスクリーンショットにある通り、このGitリポジトリをノートブックに添付してください。

BERTおよびAmazonSageMakerを使用して NERモデルをトレーニングする

固有表現抽出アルゴリズムを構築するために、私たちはBERTの使用を決めました。BERTは、大規模なテキストコーパスを用いて事前にトレーニングされたTransformerエンコーダの基本アーキテクチャを転移学習に活用することで広く知られているNLPモデルです。これにより、ラボ用品の製品特性に対する固有表現抽出など、具体的なタスクとデータに対応できます。

また、私たちはGitHubのpos-tagger-bertよりインスピレーションを得ました。これは、KerasとTensorFlow2での固有表現抽出にBERTを用いるための優れた総合的な入門書と言えます。

arxiv:1810.04805から引用した図

必要なカスタマイズのレベルと柔軟性を考慮したところ、私たちはカスタムディープラーニングモデルを構築するためにAmazon SageMaker Python SDKを使用することにしました。これにより、事前に構築されたAmazon SageMaker TensorFlow コンテナを使用できるようになり、トレーニングと推論コードを簡単にまとめることが可能です。

そのためには、まずBERTに必要な前処理を適用するトレーニングエントリポイントを指定します。次に、Kerasを使用してモデルを構築し、トレーニングを行います。最後に、SageMaker TensorFlow Servingコンテナで使用できるように、ウエイトとアーキテクチャを適切な形式に保存します。

コードをさらに読みやすくするために、大半の関数はリポジトリの「ソース」フォルダのモジュールに保存されます。

SageMaker スクリプトモードで実行するトレーニングスクリプトの作成

BERTの前処理

BERTは、入力が単語のリスト形式であることを想定しています。たとえば、製品の説明に関する「General Purpose Couplings with valve, PMC Series, Acetal」、およびそれに関連するタグは、次のリストに変換されます。

[(“General”, Type), (“Purpose”, Type), (“Couplings”, Product), (“with”, Other), (“valve”, Addon), (“PMC”, Other), (“Series”, Other), (“Acetal”, Other)],

BERTの入力には、最大シーケンス長の制約があります。これにより、長い記述が適切な長さに分割されることがあります。max_sequence_lengthを3にした場合、今回のデータは次のようになります。

[[(“General”, Type),  (“Purpose”, Type), (“Couplings”, Product)],

[(“with”, O), (“valve”, Addon), (“PMC”, O)],

[(“Series”, O), (“Acetal”, O)],

デフォルトでは、BERTの最大入力長は512です。ただし、計算時間は入力の長さに対して二次関数的に増加します。このため、大きな入力を使用して大きなコンテキストを利用する場合、現実的な処理時間で精度を高めるためにはスイートスポットを見つける必要があります。今回の場合には、max_sequence_lengthが64であれば、計算時間を比較的低く抑えながら良好な結果が得られることがわかりました。

最後に、BERTにフィードするために、単語はWordPiece トークナイザによってトークン化されます。このトークナイザは、トレーニングで使われた語彙から単語を見つけ出すと、単語が2つ以上のサブワード (Johnson→John ##son) に分割されることがあります。トークナイザを使用すると、記述内の各単語はinput_ids (単語/サブワードの語彙ID) に変換されます。これらに加えて、BERTにはinput_mask (パディング入力と実際の単語を区別) とsegment_ids (ここでの設定は0) が必要です。 また、このコードは、ラベルをワンホットエンコードされたベクトルに変換し、label_idsも生成します。これがトレーニングタスクのターゲットになります。

# Read the data
train_sentences = pd.read_csv(os.path.join(args.train, 'train.csv'), engine='python')
val_sentences = pd.read_csv(os.path.join(args.validation, 'val.csv'), engine='python')

# Create the labels dictionnaries from the data
tag2int, int2tag = prepro.create_tag_dictionnaries(train_sentences, val_sentences)

# Reading the parameters passed to the training job and saving for inference
MAX_SEQUENCE_LENGTH=args.max_sequence_length
BERT_PATH=args.bert_path

save_parameters_for_inference(MAX_SEQUENCE_LENGTH, BERT_PATH, tag2int, int2tag)

# Sequence pre-processing
# Splitting the sequences
train_sentences, val_sentences = prepro.split(train_sentences, val_sentences, MAX_SEQUENCE_LENGTH)

# Separating text and labels
train_text, val_text  = prepro.text_sequence(train_sentences, val_sentences)
train_label, val_label = prepro.tag_sequence(train_sentences, val_sentences)

# BERT pre-processing
# Instantiate tokenizer
tokenizer = prepro.create_tokenizer_from_hub_module(BERT_PATH)

# Convert to BERT features
(train_input_ids, train_input_masks, train_segment_ids, train_labels
) = prepro.convert_examples_to_features(tokenizer,
                                        train_text,
                                        train_label,
                                        tag2int,
                                        max_seq_length=MAX_SEQUENCE_LENGTH+2)
(val_input_ids, val_input_masks, val_segment_ids, val_labels
) = prepro.convert_examples_to_features(tokenizer,
                                        val_text,
                                        val_label,
                                        tag2int,
                                        max_seq_length=MAX_SEQUENCE_LENGTH+2)

推論時とトレーニング時の前処理が同じになるように、このステップで前処理パラメータを保存します。これは、後に生成されるトレーニングウェイトとともに、
SM_MODEL_DIRフォルダの下に保存されます。また、これらは、トレーニングジョブの最後に指定するAmazon S3バケットに保存されます。トレーニング済みモデルを
SageMakerモデルとしてデプロイするために必要なものは、すべてSM_MODEL_DIRに保存されます。

モデルの構築とトレーニング

BERT入力への変換が終了すると、Kerasを使用してモデルが構築され、トレーニングされます。

TensorFlowハブからBERTの事前にトレーニングされたウエイトをロードするのに、ここではデフォルトの小文字のみ(uncased)のEnglish BERTが使用されます。アーキテクチャの詳細はソースコードで参照できます。

#Building a keras model based on the previous section
model = build_model(max_seq_length = MAX_SEQUENCE_LENGTH+2,
                            n_tags=len(tag2int),
                            lr=args.learning_rate,
                            drop_out=args.drop_out,
                            bert_path=args.bert_path,
                            loss=args.loss
                            )
model_dir = "/opt/ml/model"
checkpoint = ModelCheckpoint(filepath=os.path.join(model_dir, "ner_model.h5"),
                        monitor='val_acc',
                        save_best_only=True,
                        save_weights_only=True,
                        verbose=1)

history = model.fit(training_data=([train_input_ids, train_input_masks, train_segment_ids], train_labels),
                    validation_data=([val_input_ids, val_input_masks, val_segment_ids], val_labels),
                    epochs=args.epochs,
                    batch_size=args.batch_size,
                    shuffle=True,
                    verbose=1,
                    callbacks=[checkpoint,
                               EarlyStopping(monitor = 'val_accuracy', patience = 5)])

TensorFlow Servingのモデルを保存する

トレーニングが終了すると、最適なウエイトがロードされます。ウエイトと一緒にモデルがProtobuff形式に変換され、SageMaker TensorFlow Servingコンテナで使用できるように適切なフォルダに保存されます。次のコードは、モデルを保存する方法の詳細を示しています。

# Reload best saved checkpoint: 
model.load_weights(os.path.join(model_dir, 'ner_model.h5')) 

# Note: This directory structure will need to be followed
model_version = '1'
export_dir = os.path.join(model_dir, 'model/', model_version)

tf.saved_model.save(obj=model, export_dir=export_dir)

製品プロパティを抽出する推論スクリプトの作成

NERモデルのトレーニングが完了したら、製品プロパティの生成に進み、ナレッジグラフを作成します。

SageMakerエンドポイントをデプロイするか、バッチ推論を実行して大規模に推論を実行するには、SageMaker Python SDKで作成されるTensorflow SageMakerコンテナで動作する推論スクリプトを作成する必要があります。次のセクションでは、このスクリプトの作成方法を紹介します。

PyTorchコンテナとApache/MXNet SageMakerコンテナでは、model_fnを変更することでモデルをロードする方法を指定できます。一方、SageMaker TensorFlow Servingコンテナのモデルデプロイメントは「暗黙的」であり、前セクションで定義したフォルダ構造と設定された名前を前提としています。ただし、特定の入出力処理、つまり
input_handlerとoutput_handlerへの対応においては、二つの関数を変更することができます。ここにおいて、特定のBERT入力前処理を行い、出力のフォーマットを実行します。

前処理パラメータの読み込み

推論スクリプトの最初のステップは、トレーニング中にダンプされたパラメータを読み取って前処理を再現し、同じトークナイザをインスタンス化することです。モデルを呼び出すたびにトークナイザがインスタンス化されないようにするには、これをinput_handler関数とoutput_handler関数の外に配置することが重要です。

# Reading BERT processing input parameters
with open('/opt/ml/model/int2tag.json') as f:
    int2tag = json.load(f)
with open('/opt/ml/model/tag2int.json') as f:
    tag2int = json.load(f)

n_tags = len(tag2int)

with open('/opt/ml/model/max_sequence_length.txt') as f:
    MAX_SEQUENCE_LENGTH = int(f.read())

with open('/opt/ml/model/bert_path.txt') as f:
    bert_path = f.read()

# Instantiate tokenizer (outside input_handler())
tokenizer = create_tokenizer_from_hub_module(BERT_PATH)

入力ハンドラ

入力ハンドラ関数は、予測リクエスト毎に呼び出されます。予測ペイロードで指定されたテキストデータは、トレーニングの段階で使用されたものと同じ前処理を受けます。 これにより「入力」テンソルが生成され、その後TensorFlow Serving REST APIに送信されます。次のコードは、モデルに到達する前のデータ処理の詳細を示しています。

def input_handler(data, context):
    if context.request_content_type == 'application/json':
        d = json.load(data)

    # Splitting input into words
    global sentences
    sentences = [line.split() for line in d]

    # Sentence preprocessing (split with max sequence length)"
    global split_sentences
    global split_idx
    split_sentences, split_idx = prepro.split_and_duplicate_index(sentences,
                                                                  MAX_SEQUENCE_LENGTH)

    # Creating tags placement for unlabelled data (-PAD-)")
    tags_placement = []
    for sentence in split_sentences:
        tags_placement.append(['-PAD-']*len(sentence))

    # BERT pre-processing
    (input_ids, input_masks, segment_ids, _
    ) = prepro.convert_examples_to_features(tokenizer,
                                            split_sentences,
                                            tags_placement,
                                            tag2int,
                                            max_seq_length=MAX_SEQUENCE_LENGTH+2)

    # Convert BERT features to necessary format for TensorFlow Serving
    input_ids = input_ids.tolist()
    input_masks = input_masks.tolist()
    segment_ids = segment_ids.tolist()

    result={
    'inputs':{
        "input_word_ids": input_ids,
        "input_mask": input_masks,
        "input_type_ids": segment_ids
        }
    }

    return json.dumps(result)

BERTでは、記述がモデルの入力サイズよりも大きい場合は記述を分割する必要があるため、元の記述を追跡できることが重要です。これを可能にするために、
split_and_duplicate_index () は元の各シーケンスのインデックスを作成します。このインデックスは、後で予測を元の記述にマッピングし直すために使用されます。インデックスと元のシーケンスの両方が、output_handlerで使用できるグローバル変数として保存されます。

出力ハンドラ

input_handlerによって生成された入力がモデルに送信された後、TensorFlowモデルはタグインデックスに対応する数値配列の形式で予測を出力します。output_handler を使用すると、いくつかの後処理ステップを追加できます(すなわち、予測をラベルに戻し、分割されたセンテンスを再グループ化し、各シーケンス予測をタグと値の読みやすい辞書に変換します)。

def output_handler(data, context):
    if data.status_code != 200:
        raise ValueError(data.content.decode('utf-8'))

    # Reading model predictions as data.content
    response_content_type = context.accept_header
    prediction = data.content
    pred = json.loads(prediction.decode('utf-8'))
    
    # Select the tag prediction with the highest score for each word
    pred_max = [list(np.array(k).argmax(-1)) for k in pred['outputs']]
    
    # Convert label_ids (y) to labels
    y_pred = postpro.y2label(pred_max, int2tag, mask=0)
    
    # Remapping split sequences to origin index"
    flat_y_pred, _ = postpro.map_split_preds_to_idx(y_pred, split_idx)
    
    # Format output to dicts for compatibility with Knowledge graph
    nerc_prop_list = [postpro.preds_to_dict_single_lower(s,y) for s,y in zip(sentences,flat_y_pred)]
    pred_dict_list = [{'ner_properties':nerc_prop_list[i]} for i, x in enumerate(sentences)]

    return json.dumps(pred_dict_list), response_content_type

推論スクリプトをこのように構築すると、ナレッジグラフへの入力が簡単になります。 各製品には、値が付けられた複数の文字が関連付けられています。 (これは、このブログ記事の最初の画像に示されています。)

ナレッジグラフを構築し、データの充実を図る

Amazon Neptune は高速かつ信頼性の高いフルマネージドグラフデータベースサービスです。このサービスでは、高度に接続されたデータセットと連携するアプリケーションを簡単に構築および実行できます。Amazon Neptuneは、数十億の関係を保存し、ミリ秒単位のレイテンシーでグラフをクエリするために設計されています。このユースケースでは、リソース記述フレームワーク(RDF)を使用してデータをモデル化し、
SPARQLを使用してクエリします。

製品説明の記述から製品プロパティを抽出した後、ナレッジグラフモデルを定義し、Amazon Neptuneを設定し、SPARQLを使用してクエリを実行できます。データ処理の完全なコードについては、GitHubリポジトリを参照してください。

ナレッジグラフモデル

形式的に構造化されたグラフモデルは、製品とその価値を表します。このグラフモデルには、hasPropertyリレーションシップによって表されるプロパティ、および価格、製造元、通貨などの製品の他のフィールドが含まれます。

各プロパティは、ノード、タイプ (つまり、色、マテリアル)、および値で表されます。 また、このプロパティがNERモデルの結果である場合、またはベンダーデータから定義されたプロパティである場合は、それを示すフラグも含まれます。プロパティ値とプロパティタイプの組み合わせは、プロパティノードの一意の識別子として使用されます。これにより、他の製品からの参照が可能になります。

Amazon Neptuneのセットアップ

Neptuneクラスターを設定するには、CloudFormationスタックを使用し、Getting
started with Neptune(Neptuneの開始方法)のドキュメントを参照してください。スタックがデプロイされると、Neptuneインスタンスのステータスを確認することができます。

その後、NERモデルの出力は、ナレッジグラフを構築するためのグラフィカルデータベースとしてNeptuneにフィードされます。SPARQLが私たちのプロジェクトの言語として選択されたため、JSONの出力はRDFトリプルに変換されました。

グラフモデルで定義されているとおりにトリプルが作成されたら、Neptuneのロードエンドポイントを使用して一括ロードを実行します。

SPARQLを使用してAmazon Neptuneのクエリを実行

Neptune Workbenchを使用して、グラフに対してクエリを実行します。たとえば、ID-012345の製品と他の製品に共通するつながりの数に基づいて類似製品を検索するには、次のクエリを使用できます。

BASE <http://example.com/>

PREFIX pr: <resource/product/>

SELECT ?similar (COUNT(?p) as ?similarity) ( group_concat(?p; separator=",") as ?relationships) 
FROM <products>
WHERE {
    VALUES ?product { pr:012345 }
    ?similar ?p ?o .
    ?product ?p ?o .
}
GROUP BY ?similar ?product
HAVING ( COUNT(?p) > 1 )
ORDER BY DESC(?similarity)
LIMIT 5

次の画像は、このクエリの出力を示しています。

次の画像は、上位2つの製品がNeptune Workbenchからどのように視覚化されるかを示しています。

クリーンアップ

この演習を終了したら、次の手順でリソースを削除します。

  1. ノートブックインスタンスと作成したエンドポイントを削除
  2. オプションで、登録されたモデルを削除
  3. オプションで、SageMaker実行ロールを削除
  4. オプションで、S3バケットを空にして削除、または必要なものは保持

サマリー

これで、BERTとSageMakerスクリプトモードでカスタム固有表現抽出モデルを構築し、トレーニングと推論を大規模に実行するための基礎知識が得られたでしょう。 さらに、これらの予測を使用してナレッジグラフを構築し、クエリすることでNeptuneの製品に関するインサイトを分析し、抽出することができます。

このブログ記事にて抜粋をご覧いただきましたが、AWSプロフェッショナルサービスによるモデルは、本番環境に対応した機械翻訳(ML)ソリューションを提供する機会を実現しました。また、Novartis AGチームがMLの取り組みを維持、反復、改善できるように、MLのベストプラクティスの本番化に向けて彼らをトレーニングする機会も私たちは得ることができました。提供されたリソースがあれば、彼らは将来考えられる他のユースケースにも拡大することができるでしょう。

今すぐ使用を開始しましょう!Amazon SageMakerコンソールにアクセスして
SageMakerについて詳しく学び、独自の機械学習ソリューションを開始することができます。

AWSでは皆様からのフィードバックをお待ちしています。ご質問やコメントをお寄せください。

Buying Engineプロジェクトに携わったNovartisチームに心より感謝します。また、 このブログ記事にご協力いただいた次の方々に心より感謝します。

  • Srayanta Mukherjee: Srayantaは、NovartisのData Science & Artificial IntelligenceチームのDirector of Data Scienceです。Novartis Buying Engineの納入時には、データサイエンスの責任者を務めていました。
  • Petr Hantych: PetrはNovartis ITのPrincipal Data Scientistです。立ち上げから最終的なアプリケーションを提供するまでこのプロジェクトに携わりました。余暇には、最後にフィニッシュしないことを心がけながら、トライアスロンレースを楽しんでいます。
  • Adithya N: AdithyaはNovartis ITのData Scientistで、開始段階から工業化段階まで、データサイエンスソリューションの開発に取り組んでいます。Adithyaは自由時間に、複数の楽器を演奏したり、チェスをしたり、行動経済学のジャンルの本を読んだりすることを楽しんでいます。

Othmane Hamzaoui

Othmane Hamzaouiは、AWSプロフェッショナルサービスチームで働くData Scientistです。彼は機械学習を使用してお客様の課題を解決することに情熱を注いでおり、研究とビジネスのギャップを埋めて、インパクトのある結果を達成することに重点を置いています。余暇には、美しいパリの街でランニングをしたり、新しいコーヒーショップを発掘したりすることを楽しんでいます。

Fatema Alkhanaizi

Fatema Alkhanaiziは、AWSプロフェッショナルサービスのML Engineerです。彼女は新しい技術について学ぶことや複雑な技術的問題を解決することを楽しんでいます。 余暇には、今まで行ったことのない場所を歩いて回ることを楽しんでいます。

Viktor Malesevic

Viktor Malesevicは、AWSプロフェッショナルサービスのData Scientistで、自然言語処理とMLOpsに情熱を注いでいます。彼はお客様と協力して、挑戦しがいのあるディープラーニングモデルを開発し、AWSの本番環境に導入しています。余暇には、赤ワインやチーズを友達とともに楽しんでいます。

本ブログの翻訳はソリューションアーキテクトの五十嵐が担当しました。原文はこちらのリンクから参照できます。