openvino/docs/notebooks/213-question-answering-with...

624 lines
22 KiB
ReStructuredText

Interactive question answering with OpenVINO™
=============================================
This demo shows interactive question answering with OpenVINO, using
`small BERT-large-like
model <https://github.com/openvinotoolkit/open_model_zoo/tree/master/models/intel/bert-small-uncased-whole-word-masking-squad-int8-0002>`__
distilled and quantized to ``INT8`` on SQuAD v1.1 training set from
larger BERT-large model. The model comes from `Open Model
Zoo <https://github.com/openvinotoolkit/open_model_zoo/>`__. Final part
of this notebook provides live inference results from your inputs.
Table of contents:
^^^^^^^^^^^^^^^^^^
- `Imports <#imports>`__
- `The model <#the-model>`__
- `Download the model <#download-the-model>`__
- `Load the model <#load-the-model>`__
- `Select inference device <#select-inference-device>`__
- `Processing <#processing>`__
- `Preprocessing <#preprocessing>`__
- `Postprocessing <#postprocessing>`__
- `Main Processing Function <#main-processing-function>`__
- `Run <#run>`__
- `Run on local paragraphs <#run-on-local-paragraphs>`__
- `Run on websites <#run-on-websites>`__
.. code:: ipython3
%pip install -q "openvino>=2023.1.0"
.. parsed-literal::
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
.. parsed-literal::
Note: you may need to restart the kernel to use updated packages.
Imports
-------
.. code:: ipython3
# 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
.. code:: ipython3
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
.. parsed-literal::
html_reader.py: 0%| | 0.00/635 [00:00<?, ?B/s]
.. parsed-literal::
tokens_bert.py: 0%| | 0.00/929 [00:00<?, ?B/s]
The model
---------
Download the model
~~~~~~~~~~~~~~~~~~
Download pretrained models from
https://storage.openvinotoolkit.org/repositories/open_model_zoo. If the
model is already downloaded, this step is skipped.
You can download and use any of the following models:
``bert-large-uncased-whole-word-masking-squad-0001``,
``bert-large-uncased-whole-word-masking-squad-int8-0001``,
``bert-small-uncased-whole-word-masking-squad-0001``,
``bert-small-uncased-whole-word-masking-squad-0002``,
``bert-small-uncased-whole-word-masking-squad-int8-0002``, just change
the model name in the code below. All of these models are already
converted to OpenVINO Intermediate Representation (OpenVINO IR).
.. code:: ipython3
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]
.. parsed-literal::
model/bert-small-uncased-whole-word-masking-squad-int8-0002.xml: 0%| | 0.00/1.11M [00:00<?, ?B/s]
.. parsed-literal::
model/bert-small-uncased-whole-word-masking-squad-int8-0002.bin: 0%| | 0.00/39.3M [00:00<?, ?B/s]
.. code:: ipython3
model_path
.. parsed-literal::
PosixPath('model/bert-small-uncased-whole-word-masking-squad-int8-0002.xml')
Load the model
~~~~~~~~~~~~~~
Downloaded models are located in a fixed structure, which indicates a
vendor, a model name and a precision. Only a few lines of code are
required to run the model. First, create an OpenVINO Runtime object.
Then, read the network architecture and model weights from the ``.xml``
and ``.bin`` files. Finally, compile the network for the desired device.
You can choose ``CPU`` or ``GPU`` for this model.
.. code:: ipython3
# Initialize OpenVINO Runtime.
core = ov.Core()
# Read the network and corresponding weights from a file.
model = core.read_model(model_path)
Select inference device
^^^^^^^^^^^^^^^^^^^^^^^
select device from dropdown list for running inference using OpenVINO
.. code:: ipython3
import ipywidgets as widgets
core = ov.Core()
device = widgets.Dropdown(
options=core.available_devices + ["AUTO"],
value='AUTO',
description='Device:',
disabled=False,
)
device
.. parsed-literal::
Dropdown(description='Device:', index=1, options=('CPU', 'AUTO'), value='AUTO')
.. code:: ipython3
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]
Input keys are the names of the input nodes and output keys contain
names of output nodes of the network. There are 4 inputs and 2 outputs
for BERT-large-like model.
.. code:: ipython3
[i.any_name for i in input_keys], [o.any_name for o in output_keys]
.. parsed-literal::
(['input_ids', 'attention_mask', 'token_type_ids', 'position_ids'],
['output_s', 'output_e'])
Processing
----------
NLP models usually take a list of tokens as a standard input. A token is
a single word converted to some integer. To provide the proper input,
you need the vocabulary for such mapping. You also need to define some
special tokens, such as separators or padding and a function to load the
content from provided URLs.
.. code:: ipython3
# 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)
.. parsed-literal::
data/vocab.txt: 0%| | 0.00/226k [00:00<?, ?B/s]
Preprocessing
~~~~~~~~~~~~~
The input size in this case is 384 tokens long. The main input
(``input_ids``) to used BERT model consists of two parts: question
tokens and context tokens separated by some special tokens.
If ``question + context`` are shorter than 384 tokens, padding tokens
are added. If ``question + context`` is longer than 384 tokens, the
context must be split into parts and the question with different parts
of context must be fed to the network many times.
Use overlapping, so neighbor parts of the context are overlapped by half
size of the context part (if the context part equals 300 tokens,
neighbor context parts overlap with 150 tokens). You also need to
provide the following sequences of integer values:
- ``attention_mask`` - a sequence of integer values representing the
mask of valid values in the input.
- ``token_type_ids`` - a sequence of integer values representing the
segmentation of ``input_ids`` into question and context.
- ``position_ids`` - a sequence of integer values from 0 to 383
representing the position index for each input token.
For more information, refer to the **Input** section of `BERT model
documentation <https://github.com/openvinotoolkit/open_model_zoo/tree/master/models/intel/bert-small-uncased-whole-word-masking-squad-int8-0002#input>`__.
.. code:: ipython3
# 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
Postprocessing
~~~~~~~~~~~~~~
The results from the network are raw (logits). Use the softmax function
to get the probability distribution. Then, find the best answer in the
current part of the context (the highest score) and return the score and
the context range for the answer.
.. code:: ipython3
# 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
First, create a list of tokens from the context and the question. Then,
find the best answer by trying different parts of the context. The best
answer should come with the highest score.
.. code:: ipython3
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]
Main Processing Function
~~~~~~~~~~~~~~~~~~~~~~~~
Run question answering on a specific knowledge base (websites) and
iterate through the questions.
.. code:: ipython3
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")
Run
---
Run on local paragraphs
~~~~~~~~~~~~~~~~~~~~~~~
Change sources to your own to answer your questions. You can use as many
sources as you want. Usually, you need to wait a few seconds for the
answer, but the longer the context, the longer the waiting time. The
model is very limited and sensitive for the input. The answer can depend
on whether there is a question mark at the end. The model will try to
answer any of your questions even if there is no good answer in the
context. Therefore, in such cases, you can see random results.
Sample source: a paragraph from `Computational complexity
theory <https://rajpurkar.github.io/SQuAD-explorer/explore/v2.0/dev/Computational_complexity_theory.html>`__
Sample questions:
- 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?
If you want to stop the processing just put an empty string.
**First, run the code below. If you want to run it in interactive mode
set ``example_question`` as ``None``, run the code, and then put your
questions in the box.**
.. code:: ipython3
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)
.. parsed-literal::
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.']
.. parsed-literal::
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.04s
Run on websites
~~~~~~~~~~~~~~~
You can also provide URLs. Note that the context (a knowledge base) is
built from paragraphs on websites. If some information is outside the
paragraphs, the algorithm will not be able to find it.
Sample source: `OpenVINO
wiki <https://en.wikipedia.org/wiki/OpenVINO>`__
Sample questions:
- What does OpenVINO mean?
- What is the license for OpenVINO?
- Where can you deploy OpenVINO code?
If you want to stop the processing just put an empty string.
**First, run the code below. If you want to run it in interactive mode
set ``example_question`` as ``None``, run the code, and then put your
questions in the box.**
.. code:: ipython3
sources = ["https://en.wikipedia.org/wiki/OpenVINO"]
question = "What does OpenVINO mean?"
run_question_answering(sources, example_question=question)
.. parsed-literal::
Context: ['https://en.wikipedia.org/wiki/OpenVINO']
.. parsed-literal::
Question: What does OpenVINO mean?
Answer: an open-source software toolkit for optimizing and deploying deep learning models
Score: 0.09
Time: 0.04s