OpenVINO™ によるインタラクティブな質疑応答

この Jupyter ノートブックはオンラインで起動でき、ブラウザーのウィンドウで対話型環境を開きます。ローカルにインストールすることもできます。次のオプションのいずれかを選択します。

Binder Google Colab GitHub

このデモでは、より大きな BERT-large モデルから抽出され、SQuAD v1.1 トレーニング・セットで INT8 に量子化された小さな BERT-large-like モデルを使用して、OpenVINO による対話型の質問応答を示します。このモデルは Open Model Zoo から入手できます。このノートブックの最後の部分では、入力からのリアルタイムの推論結果が提供されます。

目次

%pip install -q "openvino>=2023.1.0"
DEPRECATION: pytorch-lightning 1.6.5 has a non-standard dependency specifier torch>=1.8.*. pip 24.1 will enforce this behaviour change. A possible replacement is to upgrade to a newer version of pytorch-lightning or contact the author to suggest that they release a version with a conforming dependency specifiers. Discussion can be found at https://github.com/pypa/pip/issues/12063
Note: you may need to restart the kernel to use updated packages.

インポート

# Fetch `notebook_utils` module
import urllib.request
urllib.request.urlretrieve(
    url='https://raw.githubusercontent.com/openvinotoolkit/openvino_notebooks/main/notebooks/utils/notebook_utils.py',
    filename='notebook_utils.py'
)

from notebook_utils import download_file
import operator
import time
from urllib import parse
from pathlib import Path

import numpy as np
import openvino as ov


download_file(
    url='https://raw.githubusercontent.com/openvinotoolkit/openvino_notebooks/main/notebooks/213-question-answering/html_reader.py',
    filename='html_reader.py'
)
import html_reader as reader

download_file(
    url='https://raw.githubusercontent.com/openvinotoolkit/openvino_notebooks/main/notebooks/213-question-answering/tokens_bert.py',
    filename='tokens_bert.py'
)
import tokens_bert as tokens
html_reader.py:   0%|          | 0.00/635 [00:00<?, ?B/s]
tokens_bert.py:   0%|          | 0.00/929 [00:00<?, ?B/s]

モデル

モデルのダウンロード

事前トレーニング済みモデルを https://storage.openvinotoolkit.org/repositories/open_model_zoo からダウンロードします。モデルがすでにダウンロードされている場合、この手順はスキップされます。

次のモデルをダウンロードして使用できます: bert-large-uncased-whole-word-masking-squad-0001bert-large-uncased-whole-word-masking-squad-int8-0001bert-small-uncased-whole-word-masking-squad-0001bert-small-uncased-whole-word-masking-squad-0002bert-small-uncased-whole-word-masking-squad-int8-0002。以下のコードでモデル名を変更するだけです。これらのモデルはすべて、すでに OpenVINO 中間表現 (OpenVINO IR) に変換されています。

MODEL_DIR = Path("model")
MODEL_DIR.mkdir(exist_ok=True)

model_xml_url = "https://storage.openvinotoolkit.org/repositories/open_model_zoo/2023.0/models_bin/1/bert-small-uncased-whole-word-masking-squad-int8-0002/FP16-INT8/bert-small-uncased-whole-word-masking-squad-int8-0002.xml"
model_bin_url = "https://storage.openvinotoolkit.org/repositories/open_model_zoo/2023.0/models_bin/1/bert-small-uncased-whole-word-masking-squad-int8-0002/FP16-INT8/bert-small-uncased-whole-word-masking-squad-int8-0002.bin"

download_file(model_xml_url, model_xml_url.split("/")[-1], MODEL_DIR)
download_file(model_bin_url, model_bin_url.split("/")[-1], MODEL_DIR)

model_path = MODEL_DIR / model_xml_url.split("/")[-1]
model/bert-small-uncased-whole-word-masking-squad-int8-0002.xml:   0%|          | 0.00/1.11M [00:00<?, ?B/s]
model/bert-small-uncased-whole-word-masking-squad-int8-0002.bin:   0%|          | 0.00/39.3M [00:00<?, ?B/s]
model_path
PosixPath('model/bert-small-uncased-whole-word-masking-squad-int8-0002.xml')

モデルのロード

ダウンロードされたモデルは、ベンダー、モデル名、精度を示す固定構造に配置されます。モデルを実行するには、数行のコードで済みます。OpenVINO ランタイム・オブジェクトを作成します。次に、.xml.bin ファイルからネットワーク・アーキテクチャーとモデルの重みを読み取ります。最後に、目的のデバイスのネットワークをコンパイルします。このモデルでは CPU または GPU を選択できます。

# Initialize OpenVINO Runtime.
core = ov.Core()
# Read the network and corresponding weights from a file.
model = core.read_model(model_path)

推論デバイスの選択

OpenVINO を使用して推論を実行するためにドロップダウン・リストからデバイスを選択します。

import ipywidgets as widgets

core = ov.Core()

device = widgets.Dropdown(
    options=core.available_devices + ["AUTO"],
    value='AUTO',
    description='Device:',
    disabled=False,
)

device
Dropdown(description='Device:', index=1, options=('CPU', 'AUTO'), value='AUTO')
compiled_model = core.compile_model(model=model, device_name=device.value)

# Get input and output names of nodes.
input_keys = list(compiled_model.inputs)
output_keys = list(compiled_model.outputs)

# Get the network input size.
input_size = compiled_model.input(0).shape[1]

入力キーは入力ノードの名前であり、出力キーにはネットワークの出力ノードの名前が含まれます。BERT-large-like モデルには 4 つの入力と 2 つの出力があります。

[i.any_name for i in input_keys], [o.any_name for o in output_keys]
(['input_ids', 'attention_mask', 'token_type_ids', 'position_ids'],
                                    ['output_s', 'output_e'])

処理

NLP モデルは通常、トークンのリストを標準入力として受け取ります。トークンは、単一の単語を整数に変換したものです。適切な入力を提供するには、マッピングのため語彙が必要です。また、セパレーターやパディングなどの特殊なトークンや、指定された URL からコンテンツを読み込む関数も定義する必要があります。

# Download the vocabulary from the openvino_notebooks storage
vocab_file_path = download_file(
    "https://storage.openvinotoolkit.org/repositories/openvino_notebooks/data/data/text/bert-uncased/vocab.txt",
    directory="data"
)

# Create a dictionary with words and their indices.
vocab = tokens.load_vocab_file(str(vocab_file_path))

# Define special tokens.
cls_token = vocab["[CLS]"]
pad_token = vocab["[PAD]"]
sep_token = vocab["[SEP]"]


# A function to load text from given urls.
def load_context(sources):
    input_urls = []
    paragraphs = []
    for source in sources:
        result = parse.urlparse(source)
        if all([result.scheme, result.netloc]):
            input_urls.append(source)
        else:
            paragraphs.append(source)

    paragraphs.extend(reader.get_paragraphs(input_urls))
    # Produce one big context string.
    return "\n".join(paragraphs)
data/vocab.txt:   0%|          | 0.00/226k [00:00<?, ?B/s]

前処理

この場合の入力サイズは 384 トークン長になります。使用される BERT モデルへの主入力 (input_ids) は、いくつかの特殊トークンで区切られた質問トークンとコンテキスト・トークンの 2 つで構成されます。

質問 + コンテキストが 384 トークンより短い場合は、パディングトークンが追加されます。質問 + コンテキストが 384 トークンより長い場合、コンテキストを複数の部分に分割し、コンテキストの異なる部分を含む質問をネットワークに何度も送信する必要があります。

オーバーラップを使用すると、コンテキストの隣接部分はコンテキスト部分の半分のサイズだけ重複します (コンテキスト部分が 300 トークンの場合、隣接コンテキスト部分は 150 トークンでオーバーラップします)。次の整数値のシーケンスも提供する必要があります。

  • attention_mask - 入力内の有効な値のマスクを表す整数値のシーケンス。

  • token_type_ids - input_ids を質問とコンテキストに分割することを表す整数値のシーケンス。

  • position_ids - 各入力トークンの位置インデックスを表す 0 から 383 までの整数値のシーケンス。

詳細については、BERT モデルのドキュメント入力セクションを参照してください。

# A generator of a sequence of inputs.
def prepare_input(question_tokens, context_tokens):
    # A length of question in tokens.
    question_len = len(question_tokens)
    # The context part size.
    context_len = input_size - question_len - 3

    if context_len < 16:
        raise RuntimeError("Question is too long in comparison to input size. No space for context")

    # Take parts of the context with overlapping by 0.5.
    for start in range(0, max(1, len(context_tokens) - context_len), context_len // 2):
        # A part of the context.
        part_context_tokens = context_tokens[start:start + context_len]
        # The input: a question and the context separated by special tokens.
        input_ids = [cls_token] + question_tokens + [sep_token] + part_context_tokens + [sep_token]
        # 1 for any index if there is no padding token, 0 otherwise.
        attention_mask = [1] * len(input_ids)
        # 0 for question tokens, 1 for context part.
        token_type_ids = [0] * (question_len + 2) + [1] * (len(part_context_tokens) + 1)

        # Add padding at the end.
        (input_ids, attention_mask, token_type_ids), pad_number = pad(input_ids=input_ids,
                                                                      attention_mask=attention_mask,
                                                                      token_type_ids=token_type_ids)

        # Create an input to feed the model.
        input_dict = {
            "input_ids": np.array([input_ids], dtype=np.int32),
            "attention_mask": np.array([attention_mask], dtype=np.int32),
            "token_type_ids": np.array([token_type_ids], dtype=np.int32),
        }

        # Some models require additional position_ids.
        if "position_ids" in [i_key.any_name for i_key in input_keys]:
            position_ids = np.arange(len(input_ids))
            input_dict["position_ids"] = np.array([position_ids], dtype=np.int32)

        yield input_dict, pad_number, start


# A function to add padding.
def pad(input_ids, attention_mask, token_type_ids):
    # How many padding tokens.
    diff_input_size = input_size - len(input_ids)

    if diff_input_size > 0:
        # Add padding to all the inputs.
        input_ids = input_ids + [pad_token] * diff_input_size
        attention_mask = attention_mask + [0] * diff_input_size
        token_type_ids = token_type_ids + [0] * diff_input_size

    return (input_ids, attention_mask, token_type_ids), diff_input_size

後処理

ネットワークからの結果は生データ (ロジット) です。確率分布を取得するには、softmax 関数を使用します。次に、コンテキストの現在の部分で最適な回答 (最高スコア) を見つけ、回答のスコアとコンテキスト範囲を返します。

# Based on https://github.com/openvinotoolkit/open_model_zoo/blob/bf03f505a650bafe8da03d2747a8b55c5cb2ef16/demos/common/python/openvino/model_zoo/model_api/models/bert.py#L163
def postprocess(output_start, output_end, question_tokens, context_tokens_start_end, padding, start_idx):

    def get_score(logits):
        out = np.exp(logits)
        return out / out.sum(axis=-1)

    # Get start-end scores for the context.
    score_start = get_score(output_start)
    score_end = get_score(output_end)

    # An index of the first context token in a tensor.
    context_start_idx = len(question_tokens) + 2
    # An index of the last+1 context token in a tensor.
    context_end_idx = input_size - padding - 1

    # Find product of all start-end combinations to find the best one.
    max_score, max_start, max_end = find_best_answer_window(start_score=score_start,
                                                            end_score=score_end,
                                                            context_start_idx=context_start_idx,
                                                            context_end_idx=context_end_idx)

    # Convert to context text start-end index.
    max_start = context_tokens_start_end[max_start + start_idx][0]
    max_end = context_tokens_start_end[max_end + start_idx][1]

    return max_score, max_start, max_end


# Based on https://github.com/openvinotoolkit/open_model_zoo/blob/bf03f505a650bafe8da03d2747a8b55c5cb2ef16/demos/common/python/openvino/model_zoo/model_api/models/bert.py#L188
def find_best_answer_window(start_score, end_score, context_start_idx, context_end_idx):
    context_len = context_end_idx - context_start_idx
    score_mat = np.matmul(
        start_score[context_start_idx:context_end_idx].reshape((context_len, 1)),
        end_score[context_start_idx:context_end_idx].reshape((1, context_len)),
    )
    # Reset candidates with end before start.
    score_mat = np.triu(score_mat)
    # Reset long candidates (>16 words).
    score_mat = np.tril(score_mat, 16)
    # Find the best start-end pair.
    max_s, max_e = divmod(score_mat.flatten().argmax(), score_mat.shape[1])
    max_score = score_mat[max_s, max_e]

    return max_score, max_s, max_e

まず、コンテキストと質問からトークンのリストを作成します。次に、コンテキストのさまざまな部分を試して、最適な答えを見つけます。最良の回答には最高得点が付くはずです。

def get_best_answer(question, context):
    # Convert the context string to tokens.
    context_tokens, context_tokens_start_end = tokens.text_to_tokens(text=context.lower(),
                                                                     vocab=vocab)
    # Convert the question string to tokens.
    question_tokens, _ = tokens.text_to_tokens(text=question.lower(), vocab=vocab)

    results = []
    # Iterate through different parts of the context.
    for network_input, padding, start_idx in prepare_input(question_tokens=question_tokens,
                                                           context_tokens=context_tokens):
        # Get output layers.
        output_start_key = compiled_model.output("output_s")
        output_end_key = compiled_model.output("output_e")

        # OpenVINO inference.
        result = compiled_model(network_input)
        # Postprocess the result, getting the score and context range for the answer.
        score_start_end = postprocess(output_start=result[output_start_key][0],
                                      output_end=result[output_end_key][0],
                                      question_tokens=question_tokens,
                                      context_tokens_start_end=context_tokens_start_end,
                                      padding=padding,
                                      start_idx=start_idx)
        results.append(score_start_end)

    # Find the highest score.
    answer = max(results, key=operator.itemgetter(0))
    # Return the part of the context, which is already an answer.
    return context[answer[1]:answer[2]], answer[0]

メイン処理関数

特定のナレッジベース (ウェブサイト) に対して質問回答を実行し、質問を反復処理します。

def run_question_answering(sources, example_question=None):
    print(f"Context: {sources}", flush=True)
    context = load_context(sources)

    if len(context) == 0:
        print("Error: Empty context or outside paragraphs")
        return

    if example_question is not None:
        start_time = time.perf_counter()
        answer, score = get_best_answer(question=example_question, context=context)
        end_time = time.perf_counter()

        print(f"Question: {example_question}")
        print(f"Answer: {answer}")
        print(f"Score: {score:.2f}")
        print(f"Time: {end_time - start_time:.2f}s")
    else:
        while True:
            question = input()
            # if no question - break
            if question == "":
                break

            # measure processing time
            start_time = time.perf_counter()
            answer, score = get_best_answer(question=question, context=context)
            end_time = time.perf_counter()

            print(f"Question: {question}")
            print(f"Answer: {answer}")
            print(f"Score: {score:.2f}")
            print(f"Time: {end_time - start_time:.2f}s")

実行

ローカル段落で実行

質問に答えるため、ソースを変更します。必要なだけソースを使用できます。通常、回答を得るには数秒かかりますが、コンテキストが長くなると、待機時間も長くなります。モデルは入力に対して制限があり、かつセンシティブです。答えは、最後に疑問符があるかどうかによって異なります。モデルは、コンテキスト内に適切な回答がない場合でも、あらゆる質問に答えようとします。したがって、そのような場合はランダムな結果が表示されます。

サンプルソース: 計算複雑性理論からの段落

サンプルの質問:

  • What is the term for a task that generally lends itself to being solved by a computer? (一般的にコンピューターで解決できるタスクのことを何というでしょうか?)

  • By what main attribute are computational problems classified utilizing computational complexity theory? (計算複雑性理論を利用して計算上の問題を分類する主な属性は何ですか?)

  • What branch of theoretical computer science deals with broadly classifying computational problems by difficulty and class of relationship? (計算する問題を難易度と関係のクラスによって大まかに分類する理論コンピューター・サイエンスの分野は何ですか?)

処理を停止したい場合は、空の文字列を入力してください。

まず、以下のコードを実行します。対話モードで実行したい場合は、``example_question`` を ``None`` に設定し、コードを実行してからボックスに質問を入力してください。

sources = ["Computational complexity theory is a branch of the theory of computation in theoretical computer "
           "science that focuses on classifying computational problems according to their inherent difficulty, "
           "and relating those classes to each other. A computational problem is understood to be a task that "
           "is in principle amenable to being solved by a computer, which is equivalent to stating that the "
           "problem may be solved by mechanical application of mathematical steps, such as an algorithm."]

question = "What is the term for a task that generally lends itself to being solved by a computer?"

run_question_answering(sources, example_question=question)
Context: ['Computational complexity theory is a branch of the theory of computation in theoretical computer science that focuses on classifying computational problems according to their inherent difficulty, and relating those classes to each other. A computational problem is understood to be a task that is in principle amenable to being solved by a computer, which is equivalent to stating that the problem may be solved by mechanical application of mathematical steps, such as an algorithm.']
Question: What is the term for a task that generally lends itself to being solved by a computer?
Answer: A computational problem
Score: 0.53
Time: 0.03s

ウェブサイトで実行

URL を指定することもできます。コンテキスト (知識ベース) はウェブサイト上の段落から構築されることに注意してください。一部の情報が段落外にある場合、アルゴリズムはそれを見つけることができません。

サンプルソース: OpenVINO wiki

サンプルの質問:

  • What does OpenVINO mean? (OpenVINO とはどういう意味ですか?)

  • What is the license for OpenVINO? (OpenVINO に必要なライセンスは何ですか?)

  • Where can you deploy OpenVINO code? (OpenVINO コードはどこにデプロイできますか?)

処理を停止したい場合は、空の文字列を入力してください。

まず、以下のコードを実行します。対話モードで実行したい場合は、``example_question`` を ``None`` に設定し、コードを実行してからボックスに質問を入力してください。

sources = ["https://en.wikipedia.org/wiki/OpenVINO"]

question = "What does OpenVINO mean?"

run_question_answering(sources, example_question=question)
Context: ['https://en.wikipedia.org/wiki/OpenVINO']
Question: What does OpenVINO mean?
Answer: open-source software toolkit for optimizing and deploying deep learning models
Score: 0.06
Time: 0.06s