Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ba7b0f6cc | ||
|
|
f9a057392f | ||
|
|
c145a9e87b | ||
|
|
0e3e30779c | ||
|
|
ba6633e9a5 | ||
|
|
1b9edfee3e | ||
|
|
ba0252b7f9 | ||
|
|
41e55cb75a | ||
|
|
e043021108 | ||
|
|
fc10e4bcf8 | ||
|
|
0dc6fe8ecb | ||
|
|
17021e54a1 | ||
|
|
a67016442c | ||
|
|
0cd2ed7521 | ||
|
|
974aab8bb0 | ||
|
|
78f8e10cf3 | ||
|
|
338f224936 |
No files matched your search
@@ -19,49 +19,39 @@
|
||||
import warnings
|
||||
from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple, Union
|
||||
|
||||
from transformers.configuration_utils import PretrainedConfig
|
||||
from transformers.file_utils import is_tf_available, is_torch_available
|
||||
from transformers.modelcard import ModelCard
|
||||
from transformers.models.auto.tokenization_auto import AutoTokenizer
|
||||
from transformers.pipelines.base import (
|
||||
from ..configuration_utils import PretrainedConfig
|
||||
from ..file_utils import is_tf_available, is_torch_available
|
||||
from ..modelcard import ModelCard
|
||||
from ..models.auto.tokenization_auto import AutoTokenizer
|
||||
from ..tokenization_utils import PreTrainedTokenizer
|
||||
from ..utils import logging
|
||||
from .base import (
|
||||
ArgumentHandler,
|
||||
Conversation,
|
||||
ConversationalPipeline,
|
||||
CsvPipelineDataFormat,
|
||||
JsonPipelineDataFormat,
|
||||
NerPipeline,
|
||||
PipedPipelineDataFormat,
|
||||
Pipeline,
|
||||
PipelineDataFormat,
|
||||
PipelineException,
|
||||
QuestionAnsweringArgumentHandler,
|
||||
QuestionAnsweringPipeline,
|
||||
SummarizationPipeline,
|
||||
TableQuestionAnsweringArgumentHandler,
|
||||
TableQuestionAnsweringPipeline,
|
||||
Text2TextGenerationPipeline,
|
||||
TokenClassificationArgumentHandler,
|
||||
TokenClassificationPipeline,
|
||||
TranslationPipeline,
|
||||
get_default_model,
|
||||
get_framework,
|
||||
)
|
||||
from transformers.pipelines.feature_extraction import FeatureExtractionPipeline
|
||||
from transformers.pipelines.fill_mask import FillMaskPipeline
|
||||
from transformers.pipelines.text_classification import TextClassificationPipeline
|
||||
from transformers.pipelines.text_generation import TextGenerationPipeline
|
||||
from transformers.pipelines.zero_shot_classification import (
|
||||
ZeroShotClassificationArgumentHandler,
|
||||
ZeroShotClassificationPipeline,
|
||||
)
|
||||
from transformers.tokenization_utils import PreTrainedTokenizer
|
||||
from transformers.utils import logging
|
||||
from .conversational import Conversation, ConversationalPipeline
|
||||
from .feature_extraction import FeatureExtractionPipeline
|
||||
from .fill_mask import FillMaskPipeline
|
||||
from .question_answering import QuestionAnsweringArgumentHandler, QuestionAnsweringPipeline
|
||||
from .table_question_answering import TableQuestionAnsweringArgumentHandler, TableQuestionAnsweringPipeline
|
||||
from .text2text_generation import SummarizationPipeline, Text2TextGenerationPipeline, TranslationPipeline
|
||||
from .text_classification import TextClassificationPipeline
|
||||
from .text_generation import TextGenerationPipeline
|
||||
from .token_classification import NerPipeline, TokenClassificationArgumentHandler, TokenClassificationPipeline
|
||||
from .zero_shot_classification import ZeroShotClassificationArgumentHandler, ZeroShotClassificationPipeline
|
||||
|
||||
|
||||
if is_tf_available():
|
||||
import tensorflow as tf
|
||||
|
||||
from transformers.models.auto.modeling_tf_auto import (
|
||||
from ..models.auto.modeling_tf_auto import (
|
||||
TF_MODEL_FOR_QUESTION_ANSWERING_MAPPING,
|
||||
TF_MODEL_FOR_SEQ_TO_SEQ_CAUSAL_LM_MAPPING,
|
||||
TF_MODEL_FOR_SEQUENCE_CLASSIFICATION_MAPPING,
|
||||
@@ -79,7 +69,7 @@ if is_tf_available():
|
||||
if is_torch_available():
|
||||
import torch
|
||||
|
||||
from transformers.models.auto.modeling_auto import (
|
||||
from ..models.auto.modeling_auto import (
|
||||
MODEL_FOR_MASKED_LM_MAPPING,
|
||||
MODEL_FOR_QUESTION_ANSWERING_MAPPING,
|
||||
MODEL_FOR_SEQ_TO_SEQ_CAUSAL_LM_MAPPING,
|
||||
@@ -96,8 +86,8 @@ if is_torch_available():
|
||||
AutoModelForTokenClassification,
|
||||
)
|
||||
if TYPE_CHECKING:
|
||||
from transformers.modeling_tf_utils import TFPreTrainedModel
|
||||
from transformers.modeling_utils import PreTrainedModel
|
||||
from ..modeling_tf_utils import TFPreTrainedModel
|
||||
from ..modeling_utils import PreTrainedModel
|
||||
|
||||
logger = logging.get_logger(__name__)
|
||||
|
||||
|
||||
+13
-1697
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,368 @@
|
||||
import uuid
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from ..file_utils import add_end_docstrings, is_tf_available, is_torch_available
|
||||
from ..tokenization_utils import TruncationStrategy
|
||||
from ..utils import logging
|
||||
from .base import PIPELINE_INIT_ARGS, Pipeline
|
||||
|
||||
|
||||
if is_tf_available():
|
||||
import tensorflow as tf
|
||||
|
||||
if is_torch_available():
|
||||
import torch
|
||||
|
||||
|
||||
logger = logging.get_logger(__name__)
|
||||
|
||||
|
||||
class Conversation:
|
||||
"""
|
||||
Utility class containing a conversation and its history. This class is meant to be used as an input to the
|
||||
:class:`~transformers.ConversationalPipeline`. The conversation contains a number of utility function to manage the
|
||||
addition of new user input and generated model responses. A conversation needs to contain an unprocessed user input
|
||||
before being passed to the :class:`~transformers.ConversationalPipeline`. This user input is either created when
|
||||
the class is instantiated, or by calling :obj:`conversational_pipeline.append_response("input")` after a
|
||||
conversation turn.
|
||||
|
||||
Arguments:
|
||||
text (:obj:`str`, `optional`):
|
||||
The initial user input to start the conversation. If not provided, a user input needs to be provided
|
||||
manually using the :meth:`~transformers.Conversation.add_user_input` method before the conversation can
|
||||
begin.
|
||||
conversation_id (:obj:`uuid.UUID`, `optional`):
|
||||
Unique identifier for the conversation. If not provided, a random UUID4 id will be assigned to the
|
||||
conversation.
|
||||
|
||||
Usage::
|
||||
|
||||
conversation = Conversation("Going to the movies tonight - any suggestions?")
|
||||
|
||||
# Steps usually performed by the model when generating a response:
|
||||
# 1. Mark the user input as processed (moved to the history)
|
||||
conversation.mark_processed()
|
||||
# 2. Append a mode response
|
||||
conversation.append_response("The Big lebowski.")
|
||||
|
||||
conversation.add_user_input("Is it good?")
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, text: str = None, conversation_id: uuid.UUID = None, past_user_inputs=None, generated_responses=None
|
||||
):
|
||||
if not conversation_id:
|
||||
conversation_id = uuid.uuid4()
|
||||
if past_user_inputs is None:
|
||||
past_user_inputs = []
|
||||
if generated_responses is None:
|
||||
generated_responses = []
|
||||
|
||||
self.uuid: uuid.UUID = conversation_id
|
||||
self.past_user_inputs: List[str] = past_user_inputs
|
||||
self.generated_responses: List[str] = generated_responses
|
||||
self.new_user_input: Optional[str] = text
|
||||
self._index: int = 0
|
||||
self._history: List[int] = []
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, Conversation):
|
||||
return False
|
||||
if self.uuid == other.uuid:
|
||||
return True
|
||||
return (
|
||||
self.new_user_input == other.new_user_input
|
||||
and self.past_user_inputs == other.past_user_inputs
|
||||
and self.generated_responses == other.generated_responses
|
||||
)
|
||||
|
||||
def add_user_input(self, text: str, overwrite: bool = False):
|
||||
"""
|
||||
Add a user input to the conversation for the next round. This populates the internal :obj:`new_user_input`
|
||||
field.
|
||||
|
||||
Args:
|
||||
text (:obj:`str`): The user input for the next conversation round.
|
||||
overwrite (:obj:`bool`, `optional`, defaults to :obj:`False`):
|
||||
Whether or not existing and unprocessed user input should be overwritten when this function is called.
|
||||
"""
|
||||
if self.new_user_input:
|
||||
if overwrite:
|
||||
logger.warning(
|
||||
'User input added while unprocessed input was existing: "{}" was overwritten with: "{}".'.format(
|
||||
self.new_user_input, text
|
||||
)
|
||||
)
|
||||
self.new_user_input = text
|
||||
else:
|
||||
logger.warning(
|
||||
'User input added while unprocessed input was existing: "{}" new input ignored: "{}". '
|
||||
"Set `overwrite` to True to overwrite unprocessed user input".format(self.new_user_input, text)
|
||||
)
|
||||
else:
|
||||
self.new_user_input = text
|
||||
|
||||
def mark_processed(self):
|
||||
"""
|
||||
Mark the conversation as processed (moves the content of :obj:`new_user_input` to :obj:`past_user_inputs`) and
|
||||
empties the :obj:`new_user_input` field.
|
||||
"""
|
||||
if self.new_user_input:
|
||||
self.past_user_inputs.append(self.new_user_input)
|
||||
self.new_user_input = None
|
||||
|
||||
def append_response(self, response: str):
|
||||
"""
|
||||
Append a response to the list of generated responses.
|
||||
|
||||
Args:
|
||||
response (:obj:`str`): The model generated response.
|
||||
"""
|
||||
self.generated_responses.append(response)
|
||||
|
||||
def __repr__(self):
|
||||
"""
|
||||
Generates a string representation of the conversation.
|
||||
|
||||
Return:
|
||||
:obj:`str`:
|
||||
|
||||
Example: Conversation id: 7d15686b-dc94-49f2-9c4b-c9eac6a1f114 user >> Going to the movies tonight - any
|
||||
suggestions? bot >> The Big Lebowski
|
||||
"""
|
||||
output = "Conversation id: {} \n".format(self.uuid)
|
||||
for user_input, generated_response in zip(self.past_user_inputs, self.generated_responses):
|
||||
output += "user >> {} \n".format(user_input)
|
||||
output += "bot >> {} \n".format(generated_response)
|
||||
if self.new_user_input is not None:
|
||||
output += "user >> {} \n".format(self.new_user_input)
|
||||
return output
|
||||
|
||||
|
||||
@add_end_docstrings(
|
||||
PIPELINE_INIT_ARGS,
|
||||
r"""
|
||||
min_length_for_response (:obj:`int`, `optional`, defaults to 32):
|
||||
The minimum length (in number of tokens) for a response.
|
||||
""",
|
||||
)
|
||||
class ConversationalPipeline(Pipeline):
|
||||
"""
|
||||
Multi-turn conversational pipeline.
|
||||
|
||||
This conversational pipeline can currently be loaded from :func:`~transformers.pipeline` using the following task
|
||||
identifier: :obj:`"conversational"`.
|
||||
|
||||
The models that this pipeline can use are models that have been fine-tuned on a multi-turn conversational task,
|
||||
currently: `'microsoft/DialoGPT-small'`, `'microsoft/DialoGPT-medium'`, `'microsoft/DialoGPT-large'`. See the
|
||||
up-to-date list of available models on `huggingface.co/models
|
||||
<https://huggingface.co/models?filter=conversational>`__.
|
||||
|
||||
Usage::
|
||||
|
||||
conversational_pipeline = pipeline("conversational")
|
||||
|
||||
conversation_1 = Conversation("Going to the movies tonight - any suggestions?")
|
||||
conversation_2 = Conversation("What's the last book you have read?")
|
||||
|
||||
conversational_pipeline([conversation_1, conversation_2])
|
||||
|
||||
conversation_1.add_user_input("Is it an action movie?")
|
||||
conversation_2.add_user_input("What is the genre of this book?")
|
||||
|
||||
conversational_pipeline([conversation_1, conversation_2])
|
||||
"""
|
||||
|
||||
def __init__(self, min_length_for_response=32, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# We need at least an eos_token
|
||||
assert self.tokenizer.eos_token_id is not None, "DialoguePipeline tokenizer should have an EOS token set"
|
||||
if self.tokenizer.pad_token_id is None:
|
||||
self.tokenizer.pad_token = self.tokenizer.eos_token
|
||||
|
||||
self.min_length_for_response = min_length_for_response
|
||||
|
||||
def _get_history(self, conversation):
|
||||
# history = conversation._history[:]
|
||||
# index = conversation._index
|
||||
history = []
|
||||
index = 0
|
||||
for i, (past_user_input, generated_response) in enumerate(
|
||||
zip(conversation.past_user_inputs[index:], conversation.generated_responses[index:])
|
||||
):
|
||||
for el in (past_user_input, generated_response):
|
||||
new_history = self._parse_and_tokenize([el])[0]
|
||||
history.extend(new_history)
|
||||
conversation._index = i + index + 1
|
||||
conversation._history = history
|
||||
return history[:]
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
conversations: Union[Conversation, List[Conversation]],
|
||||
clean_up_tokenization_spaces=True,
|
||||
**generate_kwargs
|
||||
):
|
||||
r"""
|
||||
Generate responses for the conversation(s) given as inputs.
|
||||
|
||||
Args:
|
||||
conversations (a :class:`~transformers.Conversation` or a list of :class:`~transformers.Conversation`):
|
||||
Conversations to generate responses for.
|
||||
clean_up_tokenization_spaces (:obj:`bool`, `optional`, defaults to :obj:`False`):
|
||||
Whether or not to clean up the potential extra spaces in the text output.
|
||||
generate_kwargs:
|
||||
Additional keyword arguments to pass along to the generate method of the model (see the generate method
|
||||
corresponding to your framework `here <./model.html#generative-models>`__).
|
||||
|
||||
Returns:
|
||||
:class:`~transformers.Conversation` or a list of :class:`~transformers.Conversation`: Conversation(s) with
|
||||
updated generated responses for those containing a new user input.
|
||||
"""
|
||||
|
||||
if isinstance(conversations, Conversation):
|
||||
conversations = [conversations]
|
||||
# Input validation
|
||||
if isinstance(conversations, list):
|
||||
for i, conversation in enumerate(conversations):
|
||||
assert isinstance(
|
||||
conversation, Conversation
|
||||
), "DialoguePipeline expects a Conversation or list of Conversations as an input"
|
||||
if conversation.new_user_input is None:
|
||||
raise ValueError(
|
||||
"Conversation with UUID {} does not contain new user input to process. "
|
||||
"Add user inputs with the conversation's `add_user_input` method".format(
|
||||
type(conversation.uuid)
|
||||
)
|
||||
)
|
||||
assert (
|
||||
self.tokenizer.pad_token_id is not None or self.tokenizer.eos_token_id is not None
|
||||
), "Please make sure that the tokenizer has a pad_token_id or eos_token_id when using a batch input"
|
||||
else:
|
||||
raise ValueError("DialoguePipeline expects a Conversation or list of Conversations as an input")
|
||||
|
||||
with self.device_placement():
|
||||
|
||||
inputs = self._parse_and_tokenize([conversation.new_user_input for conversation in conversations])
|
||||
histories = [self._get_history(conversation) for conversation in conversations]
|
||||
max_length = generate_kwargs.get("max_length", self.model.config.max_length)
|
||||
inputs = self._concat_inputs_history(inputs, histories, max_length)
|
||||
|
||||
if self.framework == "pt":
|
||||
inputs = self.ensure_tensor_on_device(**inputs)
|
||||
input_length = inputs["input_ids"].shape[-1]
|
||||
|
||||
elif self.framework == "tf":
|
||||
input_length = tf.shape(inputs["input_ids"])[-1].numpy()
|
||||
|
||||
if input_length > 0.9 * max_length:
|
||||
logger.warning(
|
||||
"Longest conversation length: {} is bigger than 0.9 * max_length: {}. "
|
||||
"You might consider trimming the early phase of the conversation".format(input_length, max_length)
|
||||
)
|
||||
generated_responses = self.model.generate(
|
||||
inputs["input_ids"],
|
||||
attention_mask=inputs["attention_mask"],
|
||||
**generate_kwargs,
|
||||
)
|
||||
|
||||
if self.model.config.is_encoder_decoder:
|
||||
if self.framework == "pt":
|
||||
history = torch.cat((inputs["input_ids"], generated_responses[:, 1:]), 1)
|
||||
elif self.framework == "tf":
|
||||
history = tf.concat([inputs["input_ids"], generated_responses[:, 1:]], 1)
|
||||
else:
|
||||
history = generated_responses
|
||||
|
||||
history = self._clean_padding_history(history)
|
||||
if self.model.config.is_encoder_decoder:
|
||||
start_position = 1
|
||||
else:
|
||||
start_position = input_length
|
||||
|
||||
output = []
|
||||
for conversation_index, conversation in enumerate(conversations):
|
||||
conversation.mark_processed()
|
||||
conversation.generated_responses.append(
|
||||
self.tokenizer.decode(
|
||||
generated_responses[conversation_index][start_position:],
|
||||
skip_special_tokens=True,
|
||||
clean_up_tokenization_spaces=clean_up_tokenization_spaces,
|
||||
)
|
||||
)
|
||||
output.append(conversation)
|
||||
if len(output) == 1:
|
||||
return output[0]
|
||||
else:
|
||||
return output
|
||||
|
||||
def _parse_and_tokenize(
|
||||
self, inputs, add_special_tokens=False, padding=False, truncation=TruncationStrategy.DO_NOT_TRUNCATE, **kwargs
|
||||
):
|
||||
"""
|
||||
Parse arguments and tokenize, adding an EOS token at the end of the user input
|
||||
"""
|
||||
# Parse arguments
|
||||
inputs = self.tokenizer(inputs, add_special_tokens=add_special_tokens, padding=padding).get("input_ids", [])
|
||||
for input in inputs:
|
||||
input.append(self.tokenizer.eos_token_id)
|
||||
return inputs
|
||||
|
||||
def _clean_padding_history(self, generated_tensor) -> List[List[int]]:
|
||||
"""
|
||||
Cleans the padding history. Padding may be generated in two places when multiple conversations are provided as
|
||||
an input:
|
||||
|
||||
- at the end of the concatenated history and new user input, so that all input to the model have the same
|
||||
length
|
||||
- at the end of the generated response, as some responses will be longer than others
|
||||
This method cleans up these padding token so that the history for each conversation is not impacted by the
|
||||
batching process.
|
||||
"""
|
||||
outputs = []
|
||||
for sequence in generated_tensor:
|
||||
sequence_tokens = []
|
||||
is_previous_pad = False
|
||||
for token in sequence:
|
||||
if token == self.tokenizer.pad_token_id:
|
||||
if self.tokenizer.pad_token_id != self.tokenizer.eos_token_id:
|
||||
continue
|
||||
if is_previous_pad:
|
||||
continue
|
||||
else:
|
||||
is_previous_pad = True
|
||||
else:
|
||||
is_previous_pad = False
|
||||
if self.framework == "pt":
|
||||
sequence_tokens.append(token.item())
|
||||
else:
|
||||
sequence_tokens.append(int(token.numpy()))
|
||||
|
||||
outputs.append(sequence_tokens)
|
||||
return outputs
|
||||
|
||||
def _concat_inputs_history(self, inputs: List[List[int]], histories: List[Optional[List[int]]], max_length: int):
|
||||
"""
|
||||
Builds an input prepended by the history for this conversation, allowing multi-turn conversation with context
|
||||
"""
|
||||
outputs = []
|
||||
for new_input, history in zip(inputs, histories):
|
||||
if history is not None:
|
||||
new_input = history + new_input
|
||||
if len(new_input) > max_length - self.min_length_for_response:
|
||||
cutoff_eos_index = 0
|
||||
while len(new_input) - cutoff_eos_index > max_length - self.min_length_for_response:
|
||||
if cutoff_eos_index >= len(new_input):
|
||||
break
|
||||
cutoff_eos_index = new_input[cutoff_eos_index:].index(self.tokenizer.eos_token_id)
|
||||
if cutoff_eos_index == 0 or cutoff_eos_index == len(new_input) - 1:
|
||||
break
|
||||
else:
|
||||
logger.warning("Cutting history off because it's too long for underlying model")
|
||||
new_input = new_input[cutoff_eos_index + 1 :]
|
||||
outputs.append(new_input)
|
||||
padded_outputs = self.tokenizer.pad(
|
||||
{"input_ids": outputs}, padding="longest", return_attention_mask=True, return_tensors=self.framework
|
||||
)
|
||||
return padded_outputs
|
||||
@@ -1,14 +1,13 @@
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
|
||||
from transformers.modelcard import ModelCard
|
||||
from transformers.tokenization_utils import PreTrainedTokenizer
|
||||
|
||||
from ..modelcard import ModelCard
|
||||
from ..tokenization_utils import PreTrainedTokenizer
|
||||
from .base import ArgumentHandler, Pipeline
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from transformers.modeling_tf_utils import TFPreTrainedModel
|
||||
from transformers.modeling_utils import PreTrainedModel
|
||||
from ..modeling_tf_utils import TFPreTrainedModel
|
||||
from ..modeling_utils import PreTrainedModel
|
||||
|
||||
|
||||
# Can't use @add_end_docstrings(PIPELINE_INIT_ARGS) here because this one does not accept `binary_output`
|
||||
|
||||
@@ -2,27 +2,26 @@ from typing import TYPE_CHECKING, Optional, Union
|
||||
|
||||
import numpy as np
|
||||
|
||||
from transformers.file_utils import add_end_docstrings, is_tf_available, is_torch_available
|
||||
from transformers.modelcard import ModelCard
|
||||
from transformers.tokenization_utils import PreTrainedTokenizer
|
||||
from transformers.utils import logging
|
||||
|
||||
from ..file_utils import add_end_docstrings, is_tf_available, is_torch_available
|
||||
from ..modelcard import ModelCard
|
||||
from ..tokenization_utils import PreTrainedTokenizer
|
||||
from ..utils import logging
|
||||
from .base import PIPELINE_INIT_ARGS, ArgumentHandler, Pipeline, PipelineException
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from transformers.modeling_tf_utils import TFPreTrainedModel
|
||||
from transformers.modeling_utils import PreTrainedModel
|
||||
from ..modeling_tf_utils import TFPreTrainedModel
|
||||
from ..modeling_utils import PreTrainedModel
|
||||
|
||||
if is_tf_available():
|
||||
import tensorflow as tf
|
||||
|
||||
from transformers.models.auto.modeling_tf_auto import TF_MODEL_WITH_LM_HEAD_MAPPING
|
||||
from ..models.auto.modeling_tf_auto import TF_MODEL_WITH_LM_HEAD_MAPPING
|
||||
|
||||
if is_torch_available():
|
||||
import torch
|
||||
|
||||
from transformers.models.auto.modeling_auto import MODEL_FOR_MASKED_LM_MAPPING
|
||||
from ..models.auto.modeling_auto import MODEL_FOR_MASKED_LM_MAPPING
|
||||
|
||||
|
||||
logger = logging.get_logger(__name__)
|
||||
|
||||
@@ -0,0 +1,488 @@
|
||||
from collections.abc import Iterable
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Union
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ..data import SquadExample, SquadFeatures, squad_convert_examples_to_features
|
||||
from ..file_utils import add_end_docstrings, is_tf_available, is_torch_available
|
||||
from ..modelcard import ModelCard
|
||||
from ..tokenization_utils import PreTrainedTokenizer
|
||||
from ..tokenization_utils_base import PaddingStrategy
|
||||
from .base import PIPELINE_INIT_ARGS, ArgumentHandler, Pipeline
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..modeling_tf_utils import TFPreTrainedModel
|
||||
from ..modeling_utils import PreTrainedModel
|
||||
|
||||
if is_tf_available():
|
||||
import tensorflow as tf
|
||||
|
||||
from ..models.auto.modeling_tf_auto import TF_MODEL_FOR_QUESTION_ANSWERING_MAPPING
|
||||
|
||||
if is_torch_available():
|
||||
import torch
|
||||
|
||||
from ..models.auto.modeling_auto import MODEL_FOR_QUESTION_ANSWERING_MAPPING
|
||||
|
||||
|
||||
class QuestionAnsweringArgumentHandler(ArgumentHandler):
|
||||
"""
|
||||
QuestionAnsweringPipeline requires the user to provide multiple arguments (i.e. question & context) to be mapped to
|
||||
internal :class:`~transformers.SquadExample`.
|
||||
|
||||
QuestionAnsweringArgumentHandler manages all the possible to create a :class:`~transformers.SquadExample` from the
|
||||
command-line supplied arguments.
|
||||
"""
|
||||
|
||||
def normalize(self, item):
|
||||
if isinstance(item, SquadExample):
|
||||
return item
|
||||
elif isinstance(item, dict):
|
||||
for k in ["question", "context"]:
|
||||
if k not in item:
|
||||
raise KeyError("You need to provide a dictionary with keys {question:..., context:...}")
|
||||
elif item[k] is None:
|
||||
raise ValueError("`{}` cannot be None".format(k))
|
||||
elif isinstance(item[k], str) and len(item[k]) == 0:
|
||||
raise ValueError("`{}` cannot be empty".format(k))
|
||||
|
||||
return QuestionAnsweringPipeline.create_sample(**item)
|
||||
raise ValueError("{} argument needs to be of type (SquadExample, dict)".format(item))
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
# Detect where the actual inputs are
|
||||
if args is not None and len(args) > 0:
|
||||
if len(args) == 1:
|
||||
inputs = args[0]
|
||||
elif len(args) == 2 and {type(el) for el in args} == {str}:
|
||||
inputs = [{"question": args[0], "context": args[1]}]
|
||||
else:
|
||||
inputs = list(args)
|
||||
# Generic compatibility with sklearn and Keras
|
||||
# Batched data
|
||||
elif "X" in kwargs:
|
||||
inputs = kwargs["X"]
|
||||
elif "data" in kwargs:
|
||||
inputs = kwargs["data"]
|
||||
elif "question" in kwargs and "context" in kwargs:
|
||||
if isinstance(kwargs["question"], list) and isinstance(kwargs["context"], str):
|
||||
inputs = [{"question": Q, "context": kwargs["context"]} for Q in kwargs["question"]]
|
||||
elif isinstance(kwargs["question"], list) and isinstance(kwargs["context"], list):
|
||||
if len(kwargs["question"]) != len(kwargs["context"]):
|
||||
raise ValueError("Questions and contexts don't have the same lengths")
|
||||
|
||||
inputs = [{"question": Q, "context": C} for Q, C in zip(kwargs["question"], kwargs["context"])]
|
||||
elif isinstance(kwargs["question"], str) and isinstance(kwargs["context"], str):
|
||||
inputs = [{"question": kwargs["question"], "context": kwargs["context"]}]
|
||||
else:
|
||||
raise ValueError("Arguments can't be understood")
|
||||
else:
|
||||
raise ValueError("Unknown arguments {}".format(kwargs))
|
||||
|
||||
# Normalize inputs
|
||||
if isinstance(inputs, dict):
|
||||
inputs = [inputs]
|
||||
elif isinstance(inputs, Iterable):
|
||||
# Copy to avoid overriding arguments
|
||||
inputs = [i for i in inputs]
|
||||
else:
|
||||
raise ValueError("Invalid arguments {}".format(inputs))
|
||||
|
||||
for i, item in enumerate(inputs):
|
||||
inputs[i] = self.normalize(item)
|
||||
|
||||
return inputs
|
||||
|
||||
|
||||
@add_end_docstrings(PIPELINE_INIT_ARGS)
|
||||
class QuestionAnsweringPipeline(Pipeline):
|
||||
"""
|
||||
Question Answering pipeline using any :obj:`ModelForQuestionAnswering`. See the `question answering examples
|
||||
<../task_summary.html#question-answering>`__ for more information.
|
||||
|
||||
This question answering pipeline can currently be loaded from :func:`~transformers.pipeline` using the following
|
||||
task identifier: :obj:`"question-answering"`.
|
||||
|
||||
The models that this pipeline can use are models that have been fine-tuned on a question answering task. See the
|
||||
up-to-date list of available models on `huggingface.co/models
|
||||
<https://huggingface.co/models?filter=question-answering>`__.
|
||||
"""
|
||||
|
||||
default_input_names = "question,context"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model: Union["PreTrainedModel", "TFPreTrainedModel"],
|
||||
tokenizer: PreTrainedTokenizer,
|
||||
modelcard: Optional[ModelCard] = None,
|
||||
framework: Optional[str] = None,
|
||||
device: int = -1,
|
||||
task: str = "",
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(
|
||||
model=model,
|
||||
tokenizer=tokenizer,
|
||||
modelcard=modelcard,
|
||||
framework=framework,
|
||||
device=device,
|
||||
task=task,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
self._args_parser = QuestionAnsweringArgumentHandler()
|
||||
self.check_model_type(
|
||||
TF_MODEL_FOR_QUESTION_ANSWERING_MAPPING if self.framework == "tf" else MODEL_FOR_QUESTION_ANSWERING_MAPPING
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def create_sample(
|
||||
question: Union[str, List[str]], context: Union[str, List[str]]
|
||||
) -> Union[SquadExample, List[SquadExample]]:
|
||||
"""
|
||||
QuestionAnsweringPipeline leverages the :class:`~transformers.SquadExample` internally. This helper method
|
||||
encapsulate all the logic for converting question(s) and context(s) to :class:`~transformers.SquadExample`.
|
||||
|
||||
We currently support extractive question answering.
|
||||
|
||||
Arguments:
|
||||
question (:obj:`str` or :obj:`List[str]`): The question(s) asked.
|
||||
context (:obj:`str` or :obj:`List[str]`): The context(s) in which we will look for the answer.
|
||||
|
||||
Returns:
|
||||
One or a list of :class:`~transformers.SquadExample`: The corresponding :class:`~transformers.SquadExample`
|
||||
grouping question and context.
|
||||
"""
|
||||
if isinstance(question, list):
|
||||
return [SquadExample(None, q, c, None, None, None) for q, c in zip(question, context)]
|
||||
else:
|
||||
return SquadExample(None, question, context, None, None, None)
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
"""
|
||||
Answer the question(s) given as inputs by using the context(s).
|
||||
|
||||
Args:
|
||||
args (:class:`~transformers.SquadExample` or a list of :class:`~transformers.SquadExample`):
|
||||
One or several :class:`~transformers.SquadExample` containing the question and context.
|
||||
X (:class:`~transformers.SquadExample` or a list of :class:`~transformers.SquadExample`, `optional`):
|
||||
One or several :class:`~transformers.SquadExample` containing the question and context (will be treated
|
||||
the same way as if passed as the first positional argument).
|
||||
data (:class:`~transformers.SquadExample` or a list of :class:`~transformers.SquadExample`, `optional`):
|
||||
One or several :class:`~transformers.SquadExample` containing the question and context (will be treated
|
||||
the same way as if passed as the first positional argument).
|
||||
question (:obj:`str` or :obj:`List[str]`):
|
||||
One or several question(s) (must be used in conjunction with the :obj:`context` argument).
|
||||
context (:obj:`str` or :obj:`List[str]`):
|
||||
One or several context(s) associated with the question(s) (must be used in conjunction with the
|
||||
:obj:`question` argument).
|
||||
topk (:obj:`int`, `optional`, defaults to 1):
|
||||
The number of answers to return (will be chosen by order of likelihood).
|
||||
doc_stride (:obj:`int`, `optional`, defaults to 128):
|
||||
If the context is too long to fit with the question for the model, it will be split in several chunks
|
||||
with some overlap. This argument controls the size of that overlap.
|
||||
max_answer_len (:obj:`int`, `optional`, defaults to 15):
|
||||
The maximum length of predicted answers (e.g., only answers with a shorter length are considered).
|
||||
max_seq_len (:obj:`int`, `optional`, defaults to 384):
|
||||
The maximum length of the total sentence (context + question) after tokenization. The context will be
|
||||
split in several chunks (using :obj:`doc_stride`) if needed.
|
||||
max_question_len (:obj:`int`, `optional`, defaults to 64):
|
||||
The maximum length of the question after tokenization. It will be truncated if needed.
|
||||
handle_impossible_answer (:obj:`bool`, `optional`, defaults to :obj:`False`):
|
||||
Whether or not we accept impossible as an answer.
|
||||
|
||||
Return:
|
||||
A :obj:`dict` or a list of :obj:`dict`: Each result comes as a dictionary with the following keys:
|
||||
|
||||
- **score** (:obj:`float`) -- The probability associated to the answer.
|
||||
- **start** (:obj:`int`) -- The start index of the answer (in the tokenized version of the input).
|
||||
- **end** (:obj:`int`) -- The end index of the answer (in the tokenized version of the input).
|
||||
- **answer** (:obj:`str`) -- The answer to the question.
|
||||
"""
|
||||
# Set defaults values
|
||||
kwargs.setdefault("padding", "longest")
|
||||
kwargs.setdefault("topk", 1)
|
||||
kwargs.setdefault("doc_stride", 128)
|
||||
kwargs.setdefault("max_answer_len", 15)
|
||||
kwargs.setdefault("max_seq_len", 384)
|
||||
kwargs.setdefault("max_question_len", 64)
|
||||
kwargs.setdefault("handle_impossible_answer", False)
|
||||
|
||||
if kwargs["topk"] < 1:
|
||||
raise ValueError("topk parameter should be >= 1 (got {})".format(kwargs["topk"]))
|
||||
|
||||
if kwargs["max_answer_len"] < 1:
|
||||
raise ValueError("max_answer_len parameter should be >= 1 (got {})".format(kwargs["max_answer_len"]))
|
||||
|
||||
# Convert inputs to features
|
||||
examples = self._args_parser(*args, **kwargs)
|
||||
if not self.tokenizer.is_fast:
|
||||
features_list = [
|
||||
squad_convert_examples_to_features(
|
||||
examples=[example],
|
||||
tokenizer=self.tokenizer,
|
||||
max_seq_length=kwargs["max_seq_len"],
|
||||
doc_stride=kwargs["doc_stride"],
|
||||
max_query_length=kwargs["max_question_len"],
|
||||
padding_strategy=PaddingStrategy.MAX_LENGTH.value,
|
||||
is_training=False,
|
||||
tqdm_enabled=False,
|
||||
)
|
||||
for example in examples
|
||||
]
|
||||
else:
|
||||
features_list = []
|
||||
for example in examples:
|
||||
# Define the side we want to truncate / pad and the text/pair sorting
|
||||
question_first = bool(self.tokenizer.padding_side == "right")
|
||||
|
||||
encoded_inputs = self.tokenizer(
|
||||
text=example.question_text if question_first else example.context_text,
|
||||
text_pair=example.context_text if question_first else example.question_text,
|
||||
padding=kwargs["padding"],
|
||||
truncation="only_second" if question_first else "only_first",
|
||||
max_length=kwargs["max_seq_len"],
|
||||
stride=kwargs["doc_stride"],
|
||||
return_tensors="np",
|
||||
return_token_type_ids=True,
|
||||
return_overflowing_tokens=True,
|
||||
return_offsets_mapping=True,
|
||||
return_special_tokens_mask=True,
|
||||
)
|
||||
|
||||
# When the input is too long, it's converted in a batch of inputs with overflowing tokens
|
||||
# and a stride of overlap between the inputs. If a batch of inputs is given, a special output
|
||||
# "overflow_to_sample_mapping" indicate which member of the encoded batch belong to which original batch sample.
|
||||
# Here we tokenize examples one-by-one so we don't need to use "overflow_to_sample_mapping".
|
||||
# "num_span" is the number of output samples generated from the overflowing tokens.
|
||||
num_spans = len(encoded_inputs["input_ids"])
|
||||
|
||||
# p_mask: mask with 1 for token than cannot be in the answer (0 for token which can be in an answer)
|
||||
# We put 0 on the tokens from the context and 1 everywhere else (question and special tokens)
|
||||
p_mask = np.asarray(
|
||||
[
|
||||
[tok != 1 if question_first else 0 for tok in encoded_inputs.sequence_ids(span_id)]
|
||||
for span_id in range(num_spans)
|
||||
]
|
||||
)
|
||||
|
||||
# keep the cls_token unmasked (some models use it to indicate unanswerable questions)
|
||||
if self.tokenizer.cls_token_id:
|
||||
cls_index = np.nonzero(encoded_inputs["input_ids"] == self.tokenizer.cls_token_id)
|
||||
p_mask[cls_index] = 0
|
||||
|
||||
features = []
|
||||
for span_idx in range(num_spans):
|
||||
features.append(
|
||||
SquadFeatures(
|
||||
input_ids=encoded_inputs["input_ids"][span_idx],
|
||||
attention_mask=encoded_inputs["attention_mask"][span_idx],
|
||||
token_type_ids=encoded_inputs["token_type_ids"][span_idx],
|
||||
p_mask=p_mask[span_idx].tolist(),
|
||||
encoding=encoded_inputs[span_idx],
|
||||
# We don't use the rest of the values - and actually
|
||||
# for Fast tokenizer we could totally avoid using SquadFeatures and SquadExample
|
||||
cls_index=None,
|
||||
token_to_orig_map={},
|
||||
example_index=0,
|
||||
unique_id=0,
|
||||
paragraph_len=0,
|
||||
token_is_max_context=0,
|
||||
tokens=[],
|
||||
start_position=0,
|
||||
end_position=0,
|
||||
is_impossible=False,
|
||||
qas_id=None,
|
||||
)
|
||||
)
|
||||
features_list.append(features)
|
||||
|
||||
all_answers = []
|
||||
for features, example in zip(features_list, examples):
|
||||
model_input_names = self.tokenizer.model_input_names + ["input_ids"]
|
||||
fw_args = {k: [feature.__dict__[k] for feature in features] for k in model_input_names}
|
||||
|
||||
# Manage tensor allocation on correct device
|
||||
with self.device_placement():
|
||||
if self.framework == "tf":
|
||||
fw_args = {k: tf.constant(v) for (k, v) in fw_args.items()}
|
||||
start, end = self.model(fw_args)[:2]
|
||||
start, end = start.numpy(), end.numpy()
|
||||
else:
|
||||
with torch.no_grad():
|
||||
# Retrieve the score for the context tokens only (removing question tokens)
|
||||
fw_args = {k: torch.tensor(v, device=self.device) for (k, v) in fw_args.items()}
|
||||
# On Windows, the default int type in numpy is np.int32 so we get some non-long tensors.
|
||||
fw_args = {k: v.long() if v.dtype == torch.int32 else v for (k, v) in fw_args.items()}
|
||||
start, end = self.model(**fw_args)[:2]
|
||||
start, end = start.cpu().numpy(), end.cpu().numpy()
|
||||
|
||||
min_null_score = 1000000 # large and positive
|
||||
answers = []
|
||||
for (feature, start_, end_) in zip(features, start, end):
|
||||
# Ensure padded tokens & question tokens cannot belong to the set of candidate answers.
|
||||
undesired_tokens = np.abs(np.array(feature.p_mask) - 1) & feature.attention_mask
|
||||
|
||||
# Generate mask
|
||||
undesired_tokens_mask = undesired_tokens == 0.0
|
||||
|
||||
# Make sure non-context indexes in the tensor cannot contribute to the softmax
|
||||
start_ = np.where(undesired_tokens_mask, -10000.0, start_)
|
||||
end_ = np.where(undesired_tokens_mask, -10000.0, end_)
|
||||
|
||||
# Normalize logits and spans to retrieve the answer
|
||||
start_ = np.exp(start_ - np.log(np.sum(np.exp(start_), axis=-1, keepdims=True)))
|
||||
end_ = np.exp(end_ - np.log(np.sum(np.exp(end_), axis=-1, keepdims=True)))
|
||||
|
||||
if kwargs["handle_impossible_answer"]:
|
||||
min_null_score = min(min_null_score, (start_[0] * end_[0]).item())
|
||||
|
||||
# Mask CLS
|
||||
start_[0] = end_[0] = 0.0
|
||||
|
||||
starts, ends, scores = self.decode(start_, end_, kwargs["topk"], kwargs["max_answer_len"])
|
||||
if not self.tokenizer.is_fast:
|
||||
char_to_word = np.array(example.char_to_word_offset)
|
||||
|
||||
# Convert the answer (tokens) back to the original text
|
||||
# Score: score from the model
|
||||
# Start: Index of the first character of the answer in the context string
|
||||
# End: Index of the character following the last character of the answer in the context string
|
||||
# Answer: Plain text of the answer
|
||||
answers += [
|
||||
{
|
||||
"score": score.item(),
|
||||
"start": np.where(char_to_word == feature.token_to_orig_map[s])[0][0].item(),
|
||||
"end": np.where(char_to_word == feature.token_to_orig_map[e])[0][-1].item(),
|
||||
"answer": " ".join(
|
||||
example.doc_tokens[feature.token_to_orig_map[s] : feature.token_to_orig_map[e] + 1]
|
||||
),
|
||||
}
|
||||
for s, e, score in zip(starts, ends, scores)
|
||||
]
|
||||
else:
|
||||
# Convert the answer (tokens) back to the original text
|
||||
# Score: score from the model
|
||||
# Start: Index of the first character of the answer in the context string
|
||||
# End: Index of the character following the last character of the answer in the context string
|
||||
# Answer: Plain text of the answer
|
||||
question_first = bool(self.tokenizer.padding_side == "right")
|
||||
enc = feature.encoding
|
||||
|
||||
# Sometimes the max probability token is in the middle of a word so:
|
||||
# - we start by finding the right word containing the token with `token_to_word`
|
||||
# - then we convert this word in a character span with `word_to_chars`
|
||||
answers += [
|
||||
{
|
||||
"score": score.item(),
|
||||
"start": enc.word_to_chars(
|
||||
enc.token_to_word(s), sequence_index=1 if question_first else 0
|
||||
)[0],
|
||||
"end": enc.word_to_chars(enc.token_to_word(e), sequence_index=1 if question_first else 0)[
|
||||
1
|
||||
],
|
||||
"answer": example.context_text[
|
||||
enc.word_to_chars(enc.token_to_word(s), sequence_index=1 if question_first else 0)[
|
||||
0
|
||||
] : enc.word_to_chars(enc.token_to_word(e), sequence_index=1 if question_first else 0)[
|
||||
1
|
||||
]
|
||||
],
|
||||
}
|
||||
for s, e, score in zip(starts, ends, scores)
|
||||
]
|
||||
|
||||
if kwargs["handle_impossible_answer"]:
|
||||
answers.append({"score": min_null_score, "start": 0, "end": 0, "answer": ""})
|
||||
|
||||
answers = sorted(answers, key=lambda x: x["score"], reverse=True)[: kwargs["topk"]]
|
||||
all_answers += answers
|
||||
|
||||
if len(all_answers) == 1:
|
||||
return all_answers[0]
|
||||
return all_answers
|
||||
|
||||
def decode(self, start: np.ndarray, end: np.ndarray, topk: int, max_answer_len: int) -> Tuple:
|
||||
"""
|
||||
Take the output of any :obj:`ModelForQuestionAnswering` and will generate probabilities for each span to be the
|
||||
actual answer.
|
||||
|
||||
In addition, it filters out some unwanted/impossible cases like answer len being greater than max_answer_len or
|
||||
answer end position being before the starting position. The method supports output the k-best answer through
|
||||
the topk argument.
|
||||
|
||||
Args:
|
||||
start (:obj:`np.ndarray`): Individual start probabilities for each token.
|
||||
end (:obj:`np.ndarray`): Individual end probabilities for each token.
|
||||
topk (:obj:`int`): Indicates how many possible answer span(s) to extract from the model output.
|
||||
max_answer_len (:obj:`int`): Maximum size of the answer to extract from the model's output.
|
||||
"""
|
||||
# Ensure we have batch axis
|
||||
if start.ndim == 1:
|
||||
start = start[None]
|
||||
|
||||
if end.ndim == 1:
|
||||
end = end[None]
|
||||
|
||||
# Compute the score of each tuple(start, end) to be the real answer
|
||||
outer = np.matmul(np.expand_dims(start, -1), np.expand_dims(end, 1))
|
||||
|
||||
# Remove candidate with end < start and end - start > max_answer_len
|
||||
candidates = np.tril(np.triu(outer), max_answer_len - 1)
|
||||
|
||||
# Inspired by Chen & al. (https://github.com/facebookresearch/DrQA)
|
||||
scores_flat = candidates.flatten()
|
||||
if topk == 1:
|
||||
idx_sort = [np.argmax(scores_flat)]
|
||||
elif len(scores_flat) < topk:
|
||||
idx_sort = np.argsort(-scores_flat)
|
||||
else:
|
||||
idx = np.argpartition(-scores_flat, topk)[0:topk]
|
||||
idx_sort = idx[np.argsort(-scores_flat[idx])]
|
||||
|
||||
start, end = np.unravel_index(idx_sort, candidates.shape)[1:]
|
||||
return start, end, candidates[0, start, end]
|
||||
|
||||
def span_to_answer(self, text: str, start: int, end: int) -> Dict[str, Union[str, int]]:
|
||||
"""
|
||||
When decoding from token probabilities, this method maps token indexes to actual word in the initial context.
|
||||
|
||||
Args:
|
||||
text (:obj:`str`): The actual context to extract the answer from.
|
||||
start (:obj:`int`): The answer starting token index.
|
||||
end (:obj:`int`): The answer end token index.
|
||||
|
||||
Returns:
|
||||
Dictionary like :obj:`{'answer': str, 'start': int, 'end': int}`
|
||||
"""
|
||||
words = []
|
||||
token_idx = char_start_idx = char_end_idx = chars_idx = 0
|
||||
|
||||
for i, word in enumerate(text.split(" ")):
|
||||
token = self.tokenizer.tokenize(word)
|
||||
|
||||
# Append words if they are in the span
|
||||
if start <= token_idx <= end:
|
||||
if token_idx == start:
|
||||
char_start_idx = chars_idx
|
||||
|
||||
if token_idx == end:
|
||||
char_end_idx = chars_idx + len(word)
|
||||
|
||||
words += [word]
|
||||
|
||||
# Stop if we went over the end of the answer
|
||||
if token_idx > end:
|
||||
break
|
||||
|
||||
# Append the subtokenization length to the running index
|
||||
token_idx += len(token)
|
||||
chars_idx += len(word) + 1
|
||||
|
||||
# Join text with spaces
|
||||
return {
|
||||
"answer": " ".join(words),
|
||||
"start": max(0, char_start_idx),
|
||||
"end": min(len(text), char_end_idx),
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
import collections
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ..file_utils import add_end_docstrings, is_torch_available, requires_pandas
|
||||
from .base import PIPELINE_INIT_ARGS, ArgumentHandler, Pipeline
|
||||
|
||||
|
||||
if is_torch_available():
|
||||
import torch
|
||||
|
||||
from ..models.auto.modeling_auto import MODEL_FOR_TABLE_QUESTION_ANSWERING_MAPPING
|
||||
|
||||
|
||||
class TableQuestionAnsweringArgumentHandler(ArgumentHandler):
|
||||
"""
|
||||
Handles arguments for the TableQuestionAnsweringPipeline
|
||||
"""
|
||||
|
||||
def __call__(self, table=None, query=None, sequential=False, padding=True, truncation=True):
|
||||
# Returns tqa_pipeline_inputs of shape:
|
||||
# [
|
||||
# {"table": pd.DataFrame, "query": List[str]},
|
||||
# ...,
|
||||
# {"table": pd.DataFrame, "query" : List[str]}
|
||||
# ]
|
||||
requires_pandas(self)
|
||||
import pandas as pd
|
||||
|
||||
if table is None:
|
||||
raise ValueError("Keyword argument `table` cannot be None.")
|
||||
elif query is None:
|
||||
if isinstance(table, dict) and table.get("query") is not None and table.get("table") is not None:
|
||||
tqa_pipeline_inputs = [table]
|
||||
elif isinstance(table, list) and len(table) > 0:
|
||||
if not all(isinstance(d, dict) for d in table):
|
||||
raise ValueError(
|
||||
f"Keyword argument `table` should be a list of dict, but is {(type(d) for d in table)}"
|
||||
)
|
||||
|
||||
if table[0].get("query") is not None and table[0].get("table") is not None:
|
||||
tqa_pipeline_inputs = table
|
||||
else:
|
||||
raise ValueError(
|
||||
f"If keyword argument `table` is a list of dictionaries, each dictionary should have a `table` "
|
||||
f"and `query` key, but only dictionary has keys {table[0].keys()} `table` and `query` keys."
|
||||
)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Invalid input. Keyword argument `table` should be either of type `dict` or `list`, but "
|
||||
f"is {type(table)})"
|
||||
)
|
||||
else:
|
||||
tqa_pipeline_inputs = [{"table": table, "query": query}]
|
||||
|
||||
for tqa_pipeline_input in tqa_pipeline_inputs:
|
||||
if not isinstance(tqa_pipeline_input["table"], pd.DataFrame):
|
||||
if tqa_pipeline_input["table"] is None:
|
||||
raise ValueError("Table cannot be None.")
|
||||
|
||||
tqa_pipeline_input["table"] = pd.DataFrame(tqa_pipeline_input["table"])
|
||||
|
||||
return tqa_pipeline_inputs, sequential, padding, truncation
|
||||
|
||||
|
||||
@add_end_docstrings(PIPELINE_INIT_ARGS)
|
||||
class TableQuestionAnsweringPipeline(Pipeline):
|
||||
"""
|
||||
Table Question Answering pipeline using a :obj:`ModelForTableQuestionAnswering`. This pipeline is only available in
|
||||
PyTorch.
|
||||
|
||||
This tabular question answering pipeline can currently be loaded from :func:`~transformers.pipeline` using the
|
||||
following task identifier: :obj:`"table-question-answering"`.
|
||||
|
||||
The models that this pipeline can use are models that have been fine-tuned on a tabular question answering task.
|
||||
See the up-to-date list of available models on `huggingface.co/models
|
||||
<https://huggingface.co/models?filter=table-question-answering>`__.
|
||||
"""
|
||||
|
||||
default_input_names = "table,query"
|
||||
|
||||
def __init__(self, args_parser=TableQuestionAnsweringArgumentHandler(), *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._args_parser = args_parser
|
||||
|
||||
if self.framework == "tf":
|
||||
raise ValueError("The TableQuestionAnsweringPipeline is only available in PyTorch.")
|
||||
|
||||
self.check_model_type(MODEL_FOR_TABLE_QUESTION_ANSWERING_MAPPING)
|
||||
|
||||
self.aggregate = bool(getattr(self.model.config, "aggregation_labels")) and bool(
|
||||
getattr(self.model.config, "num_aggregation_labels")
|
||||
)
|
||||
|
||||
def batch_inference(self, **inputs):
|
||||
with torch.no_grad():
|
||||
return self.model(**inputs)
|
||||
|
||||
def sequential_inference(self, **inputs):
|
||||
"""
|
||||
Inference used for models that need to process sequences in a sequential fashion, like the SQA models which
|
||||
handle conversational query related to a table.
|
||||
"""
|
||||
with torch.no_grad():
|
||||
all_logits = []
|
||||
all_aggregations = []
|
||||
prev_answers = None
|
||||
batch_size = inputs["input_ids"].shape[0]
|
||||
|
||||
input_ids = inputs["input_ids"].to(self.device)
|
||||
attention_mask = inputs["attention_mask"].to(self.device)
|
||||
token_type_ids = inputs["token_type_ids"].to(self.device)
|
||||
token_type_ids_example = None
|
||||
|
||||
for index in range(batch_size):
|
||||
# If sequences have already been processed, the token type IDs will be created according to the previous
|
||||
# answer.
|
||||
if prev_answers is not None:
|
||||
prev_labels_example = token_type_ids_example[:, 3] # shape (seq_len,)
|
||||
model_labels = np.zeros_like(prev_labels_example.cpu().numpy()) # shape (seq_len,)
|
||||
|
||||
token_type_ids_example = token_type_ids[index] # shape (seq_len, 7)
|
||||
for i in range(model_labels.shape[0]):
|
||||
segment_id = token_type_ids_example[:, 0].tolist()[i]
|
||||
col_id = token_type_ids_example[:, 1].tolist()[i] - 1
|
||||
row_id = token_type_ids_example[:, 2].tolist()[i] - 1
|
||||
|
||||
if row_id >= 0 and col_id >= 0 and segment_id == 1:
|
||||
model_labels[i] = int(prev_answers[(col_id, row_id)])
|
||||
|
||||
token_type_ids_example[:, 3] = torch.from_numpy(model_labels).type(torch.long).to(self.device)
|
||||
|
||||
input_ids_example = input_ids[index]
|
||||
attention_mask_example = attention_mask[index] # shape (seq_len,)
|
||||
token_type_ids_example = token_type_ids[index] # shape (seq_len, 7)
|
||||
outputs = self.model(
|
||||
input_ids=input_ids_example.unsqueeze(0),
|
||||
attention_mask=attention_mask_example.unsqueeze(0),
|
||||
token_type_ids=token_type_ids_example.unsqueeze(0),
|
||||
)
|
||||
logits = outputs.logits
|
||||
|
||||
if self.aggregate:
|
||||
all_aggregations.append(outputs.logits_aggregation)
|
||||
|
||||
all_logits.append(logits)
|
||||
|
||||
dist_per_token = torch.distributions.Bernoulli(logits=logits)
|
||||
probabilities = dist_per_token.probs * attention_mask_example.type(torch.float32).to(
|
||||
dist_per_token.probs.device
|
||||
)
|
||||
|
||||
coords_to_probs = collections.defaultdict(list)
|
||||
for i, p in enumerate(probabilities.squeeze().tolist()):
|
||||
segment_id = token_type_ids_example[:, 0].tolist()[i]
|
||||
col = token_type_ids_example[:, 1].tolist()[i] - 1
|
||||
row = token_type_ids_example[:, 2].tolist()[i] - 1
|
||||
if col >= 0 and row >= 0 and segment_id == 1:
|
||||
coords_to_probs[(col, row)].append(p)
|
||||
|
||||
prev_answers = {key: np.array(coords_to_probs[key]).mean() > 0.5 for key in coords_to_probs}
|
||||
|
||||
logits_batch = torch.cat(tuple(all_logits), 0)
|
||||
|
||||
return (logits_batch,) if not self.aggregate else (logits_batch, torch.cat(tuple(all_aggregations), 0))
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
r"""
|
||||
Answers queries according to a table. The pipeline accepts several types of inputs which are detailed below:
|
||||
|
||||
- ``pipeline(table, query)``
|
||||
- ``pipeline(table, [query])``
|
||||
- ``pipeline(table=table, query=query)``
|
||||
- ``pipeline(table=table, query=[query])``
|
||||
- ``pipeline({"table": table, "query": query})``
|
||||
- ``pipeline({"table": table, "query": [query]})``
|
||||
- ``pipeline([{"table": table, "query": query}, {"table": table, "query": query}])``
|
||||
|
||||
The :obj:`table` argument should be a dict or a DataFrame built from that dict, containing the whole table:
|
||||
|
||||
Example::
|
||||
|
||||
data = {
|
||||
"actors": ["brad pitt", "leonardo di caprio", "george clooney"],
|
||||
"age": ["56", "45", "59"],
|
||||
"number of movies": ["87", "53", "69"],
|
||||
"date of birth": ["7 february 1967", "10 june 1996", "28 november 1967"],
|
||||
}
|
||||
|
||||
This dictionary can be passed in as such, or can be converted to a pandas DataFrame:
|
||||
|
||||
Example::
|
||||
|
||||
import pandas as pd
|
||||
table = pd.DataFrame.from_dict(data)
|
||||
|
||||
|
||||
Args:
|
||||
table (:obj:`pd.DataFrame` or :obj:`Dict`):
|
||||
Pandas DataFrame or dictionary that will be converted to a DataFrame containing all the table values.
|
||||
See above for an example of dictionary.
|
||||
query (:obj:`str` or :obj:`List[str]`):
|
||||
Query or list of queries that will be sent to the model alongside the table.
|
||||
sequential (:obj:`bool`, `optional`, defaults to :obj:`False`):
|
||||
Whether to do inference sequentially or as a batch. Batching is faster, but models like SQA require the
|
||||
inference to be done sequentially to extract relations within sequences, given their conversational
|
||||
nature.
|
||||
padding (:obj:`bool`, :obj:`str` or :class:`~transformers.tokenization_utils_base.PaddingStrategy`, `optional`, defaults to :obj:`False`):
|
||||
Activates and controls padding. Accepts the following values:
|
||||
|
||||
* :obj:`True` or :obj:`'longest'`: Pad to the longest sequence in the batch (or no padding if only a
|
||||
single sequence if provided).
|
||||
* :obj:`'max_length'`: Pad to a maximum length specified with the argument :obj:`max_length` or to the
|
||||
maximum acceptable input length for the model if that argument is not provided.
|
||||
* :obj:`False` or :obj:`'do_not_pad'` (default): No padding (i.e., can output a batch with sequences of
|
||||
different lengths).
|
||||
|
||||
truncation (:obj:`bool`, :obj:`str` or :class:`~transformers.TapasTruncationStrategy`, `optional`, defaults to :obj:`False`):
|
||||
Activates and controls truncation. Accepts the following values:
|
||||
|
||||
* :obj:`True` or :obj:`'drop_rows_to_fit'`: Truncate to a maximum length specified with the argument
|
||||
:obj:`max_length` or to the maximum acceptable input length for the model if that argument is not
|
||||
provided. This will truncate row by row, removing rows from the table.
|
||||
* :obj:`False` or :obj:`'do_not_truncate'` (default): No truncation (i.e., can output batch with
|
||||
sequence lengths greater than the model maximum admissible input size).
|
||||
|
||||
|
||||
Return:
|
||||
A dictionary or a list of dictionaries containing results: Each result is a dictionary with the following
|
||||
keys:
|
||||
|
||||
- **answer** (:obj:`str`) -- The answer of the query given the table. If there is an aggregator, the answer
|
||||
will be preceded by :obj:`AGGREGATOR >`.
|
||||
- **coordinates** (:obj:`List[Tuple[int, int]]`) -- Coordinates of the cells of the answers.
|
||||
- **cells** (:obj:`List[str]`) -- List of strings made up of the answer cell values.
|
||||
- **aggregator** (:obj:`str`) -- If the model has an aggregator, this returns the aggregator.
|
||||
"""
|
||||
pipeline_inputs, sequential, padding, truncation = self._args_parser(*args, **kwargs)
|
||||
batched_answers = []
|
||||
for pipeline_input in pipeline_inputs:
|
||||
table, query = pipeline_input["table"], pipeline_input["query"]
|
||||
inputs = self.tokenizer(
|
||||
table, query, return_tensors=self.framework, truncation="drop_rows_to_fit", padding=padding
|
||||
)
|
||||
|
||||
outputs = self.sequential_inference(**inputs) if sequential else self.batch_inference(**inputs)
|
||||
|
||||
if self.aggregate:
|
||||
logits, logits_agg = outputs[:2]
|
||||
predictions = self.tokenizer.convert_logits_to_predictions(inputs, logits.detach(), logits_agg)
|
||||
answer_coordinates_batch, agg_predictions = predictions
|
||||
aggregators = {i: self.model.config.aggregation_labels[pred] for i, pred in enumerate(agg_predictions)}
|
||||
|
||||
no_agg_label_index = self.model.config.no_aggregation_label_index
|
||||
aggregators_prefix = {
|
||||
i: aggregators[i] + " > " for i, pred in enumerate(agg_predictions) if pred != no_agg_label_index
|
||||
}
|
||||
else:
|
||||
logits = outputs[0]
|
||||
predictions = self.tokenizer.convert_logits_to_predictions(inputs, logits.detach())
|
||||
answer_coordinates_batch = predictions[0]
|
||||
aggregators = {}
|
||||
aggregators_prefix = {}
|
||||
|
||||
answers = []
|
||||
for index, coordinates in enumerate(answer_coordinates_batch):
|
||||
cells = [table.iat[coordinate] for coordinate in coordinates]
|
||||
aggregator = aggregators.get(index, "")
|
||||
aggregator_prefix = aggregators_prefix.get(index, "")
|
||||
answer = {
|
||||
"answer": aggregator_prefix + ", ".join(cells),
|
||||
"coordinates": coordinates,
|
||||
"cells": [table.iat[coordinate] for coordinate in coordinates],
|
||||
}
|
||||
if aggregator:
|
||||
answer["aggregator"] = aggregator
|
||||
|
||||
answers.append(answer)
|
||||
batched_answers.append(answers if len(answers) > 1 else answers[0])
|
||||
return batched_answers if len(batched_answers) > 1 else batched_answers[0]
|
||||
@@ -0,0 +1,255 @@
|
||||
from ..file_utils import add_end_docstrings, is_tf_available, is_torch_available
|
||||
from ..utils import logging
|
||||
from .base import PIPELINE_INIT_ARGS, Pipeline
|
||||
|
||||
|
||||
if is_tf_available():
|
||||
import tensorflow as tf
|
||||
|
||||
from ..models.auto.modeling_tf_auto import TF_MODEL_FOR_SEQ_TO_SEQ_CAUSAL_LM_MAPPING
|
||||
|
||||
if is_torch_available():
|
||||
from ..models.auto.modeling_auto import MODEL_FOR_SEQ_TO_SEQ_CAUSAL_LM_MAPPING
|
||||
|
||||
logger = logging.get_logger(__name__)
|
||||
|
||||
|
||||
@add_end_docstrings(PIPELINE_INIT_ARGS)
|
||||
class Text2TextGenerationPipeline(Pipeline):
|
||||
"""
|
||||
Pipeline for text to text generation using seq2seq models.
|
||||
|
||||
This Text2TextGenerationPipeline pipeline can currently be loaded from :func:`~transformers.pipeline` using the
|
||||
following task identifier: :obj:`"text2text-generation"`.
|
||||
|
||||
The models that this pipeline can use are models that have been fine-tuned on a translation task. See the
|
||||
up-to-date list of available models on `huggingface.co/models <https://huggingface.co/models?filter=seq2seq>`__.
|
||||
|
||||
Usage::
|
||||
|
||||
text2text_generator = pipeline("text2text-generation")
|
||||
text2text_generator("question: What is 42 ? context: 42 is the answer to life, the universe and everything")
|
||||
"""
|
||||
|
||||
return_name = "generated"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.check_model_type(
|
||||
TF_MODEL_FOR_SEQ_TO_SEQ_CAUSAL_LM_MAPPING
|
||||
if self.framework == "tf"
|
||||
else MODEL_FOR_SEQ_TO_SEQ_CAUSAL_LM_MAPPING
|
||||
)
|
||||
|
||||
def check_inputs(self, input_length: int, min_length: int, max_length: int):
|
||||
"""
|
||||
Checks wether there might be something wrong with given input with regard to the model.
|
||||
"""
|
||||
return True
|
||||
|
||||
def __call__(
|
||||
self, *args, return_tensors=False, return_text=True, clean_up_tokenization_spaces=False, **generate_kwargs
|
||||
):
|
||||
r"""
|
||||
Generate the output text(s) using text(s) given as inputs.
|
||||
|
||||
Args:
|
||||
args (:obj:`str` or :obj:`List[str]`):
|
||||
Input text for the encoder.
|
||||
return_tensors (:obj:`bool`, `optional`, defaults to :obj:`False`):
|
||||
Whether or not to include the tensors of predictions (as token indices) in the outputs.
|
||||
return_text (:obj:`bool`, `optional`, defaults to :obj:`True`):
|
||||
Whether or not to include the decoded texts in the outputs.
|
||||
clean_up_tokenization_spaces (:obj:`bool`, `optional`, defaults to :obj:`False`):
|
||||
Whether or not to clean up the potential extra spaces in the text output.
|
||||
generate_kwargs:
|
||||
Additional keyword arguments to pass along to the generate method of the model (see the generate method
|
||||
corresponding to your framework `here <./model.html#generative-models>`__).
|
||||
|
||||
Return:
|
||||
A list or a list of list of :obj:`dict`: Each result comes as a dictionary with the following keys:
|
||||
|
||||
- **generated_text** (:obj:`str`, present when ``return_text=True``) -- The generated text.
|
||||
- **generated_token_ids** (:obj:`torch.Tensor` or :obj:`tf.Tensor`, present when ``return_tensors=True``)
|
||||
-- The token ids of the generated text.
|
||||
"""
|
||||
assert return_tensors or return_text, "You must specify return_tensors=True or return_text=True"
|
||||
|
||||
prefix = self.model.config.prefix if self.model.config.prefix is not None else ""
|
||||
if isinstance(args[0], list):
|
||||
assert (
|
||||
self.tokenizer.pad_token_id is not None
|
||||
), "Please make sure that the tokenizer has a pad_token_id when using a batch input"
|
||||
args = ([prefix + arg for arg in args[0]],)
|
||||
padding = True
|
||||
|
||||
elif isinstance(args[0], str):
|
||||
args = (prefix + args[0],)
|
||||
padding = False
|
||||
else:
|
||||
raise ValueError(
|
||||
" `args[0]`: {} have the wrong format. The should be either of type `str` or type `list`".format(
|
||||
args[0]
|
||||
)
|
||||
)
|
||||
|
||||
with self.device_placement():
|
||||
inputs = self._parse_and_tokenize(*args, padding=padding, **generate_kwargs)
|
||||
|
||||
if self.framework == "pt":
|
||||
inputs = self.ensure_tensor_on_device(**inputs)
|
||||
input_length = inputs["input_ids"].shape[-1]
|
||||
elif self.framework == "tf":
|
||||
input_length = tf.shape(inputs["input_ids"])[-1].numpy()
|
||||
|
||||
min_length = generate_kwargs.get("min_length", self.model.config.min_length)
|
||||
max_length = generate_kwargs.get("max_length", self.model.config.max_length)
|
||||
self.check_inputs(input_length, min_length, max_length)
|
||||
|
||||
# truncation should be used by _parse_and_tokenize
|
||||
generate_kwargs.pop("truncation", None)
|
||||
|
||||
generations = self.model.generate(
|
||||
inputs["input_ids"],
|
||||
attention_mask=inputs["attention_mask"],
|
||||
**generate_kwargs,
|
||||
)
|
||||
results = []
|
||||
for generation in generations:
|
||||
record = {}
|
||||
if return_tensors:
|
||||
record[f"{self.return_name}_token_ids"] = generation
|
||||
if return_text:
|
||||
record[f"{self.return_name}_text"] = self.tokenizer.decode(
|
||||
generation,
|
||||
skip_special_tokens=True,
|
||||
clean_up_tokenization_spaces=clean_up_tokenization_spaces,
|
||||
)
|
||||
results.append(record)
|
||||
return results
|
||||
|
||||
|
||||
@add_end_docstrings(PIPELINE_INIT_ARGS)
|
||||
class SummarizationPipeline(Text2TextGenerationPipeline):
|
||||
"""
|
||||
Summarize news articles and other documents.
|
||||
|
||||
This summarizing pipeline can currently be loaded from :func:`~transformers.pipeline` using the following task
|
||||
identifier: :obj:`"summarization"`.
|
||||
|
||||
The models that this pipeline can use are models that have been fine-tuned on a summarization task, which is
|
||||
currently, '`bart-large-cnn`', '`t5-small`', '`t5-base`', '`t5-large`', '`t5-3b`', '`t5-11b`'. See the up-to-date
|
||||
list of available models on `huggingface.co/models <https://huggingface.co/models?filter=summarization>`__.
|
||||
|
||||
Usage::
|
||||
|
||||
# use bart in pytorch
|
||||
summarizer = pipeline("summarization")
|
||||
summarizer("Sam Shleifer writes the best docstring examples in the whole world.", min_length=5, max_length=20)
|
||||
|
||||
# use t5 in tf
|
||||
summarizer = pipeline("summarization", model="t5-base", tokenizer="t5-base", framework="tf")
|
||||
summarizer("Sam Shleifer writes the best docstring examples in the whole world.", min_length=5, max_length=20)
|
||||
"""
|
||||
|
||||
return_name = "summary"
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
r"""
|
||||
Summarize the text(s) given as inputs.
|
||||
|
||||
Args:
|
||||
documents (`str` or :obj:`List[str]`):
|
||||
One or several articles (or one list of articles) to summarize.
|
||||
return_text (:obj:`bool`, `optional`, defaults to :obj:`True`):
|
||||
Whether or not to include the decoded texts in the outputs
|
||||
return_tensors (:obj:`bool`, `optional`, defaults to :obj:`False`):
|
||||
Whether or not to include the tensors of predictions (as token indices) in the outputs.
|
||||
clean_up_tokenization_spaces (:obj:`bool`, `optional`, defaults to :obj:`False`):
|
||||
Whether or not to clean up the potential extra spaces in the text output.
|
||||
generate_kwargs:
|
||||
Additional keyword arguments to pass along to the generate method of the model (see the generate method
|
||||
corresponding to your framework `here <./model.html#generative-models>`__).
|
||||
|
||||
Return:
|
||||
A list or a list of list of :obj:`dict`: Each result comes as a dictionary with the following keys:
|
||||
|
||||
- **summary_text** (:obj:`str`, present when ``return_text=True``) -- The summary of the corresponding
|
||||
input.
|
||||
- **summary_token_ids** (:obj:`torch.Tensor` or :obj:`tf.Tensor`, present when ``return_tensors=True``) --
|
||||
The token ids of the summary.
|
||||
"""
|
||||
return super().__call__(*args, **kwargs)
|
||||
|
||||
def check_inputs(self, input_length: int, min_length: int, max_length: int) -> bool:
|
||||
"""
|
||||
Checks wether there might be something wrong with given input with regard to the model.
|
||||
"""
|
||||
if input_length < min_length // 2:
|
||||
logger.warning(
|
||||
"Your min_length is set to {}, but you input_length is only {}. You might consider decreasing min_length manually, e.g. summarizer('...', min_length=10)".format(
|
||||
min_length, input_length
|
||||
)
|
||||
)
|
||||
|
||||
if input_length < max_length:
|
||||
logger.warning(
|
||||
"Your max_length is set to {}, but you input_length is only {}. You might consider decreasing max_length manually, e.g. summarizer('...', max_length=50)".format(
|
||||
max_length, input_length
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@add_end_docstrings(PIPELINE_INIT_ARGS)
|
||||
class TranslationPipeline(Text2TextGenerationPipeline):
|
||||
"""
|
||||
Translates from one language to another.
|
||||
|
||||
This translation pipeline can currently be loaded from :func:`~transformers.pipeline` using the following task
|
||||
identifier: :obj:`"translation_xx_to_yy"`.
|
||||
|
||||
The models that this pipeline can use are models that have been fine-tuned on a translation task. See the
|
||||
up-to-date list of available models on `huggingface.co/models
|
||||
<https://huggingface.co/models?filter=translation>`__.
|
||||
|
||||
Usage::
|
||||
en_fr_translator = pipeline("translation_en_to_fr")
|
||||
en_fr_translator("How old are you?")
|
||||
"""
|
||||
|
||||
return_name = "translation"
|
||||
|
||||
def check_inputs(self, input_length: int, min_length: int, max_length: int):
|
||||
if input_length > 0.9 * max_length:
|
||||
logger.warning(
|
||||
"Your input_length: {} is bigger than 0.9 * max_length: {}. You might consider increasing your max_length manually, e.g. translator('...', max_length=400)".format(
|
||||
input_length, max_length
|
||||
)
|
||||
)
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
r"""
|
||||
Translate the text(s) given as inputs.
|
||||
|
||||
Args:
|
||||
args (:obj:`str` or :obj:`List[str]`):
|
||||
Texts to be translated.
|
||||
return_tensors (:obj:`bool`, `optional`, defaults to :obj:`False`):
|
||||
Whether or not to include the tensors of predictions (as token indices) in the outputs.
|
||||
return_text (:obj:`bool`, `optional`, defaults to :obj:`True`):
|
||||
Whether or not to include the decoded texts in the outputs.
|
||||
clean_up_tokenization_spaces (:obj:`bool`, `optional`, defaults to :obj:`False`):
|
||||
Whether or not to clean up the potential extra spaces in the text output.
|
||||
generate_kwargs:
|
||||
Additional keyword arguments to pass along to the generate method of the model (see the generate method
|
||||
corresponding to your framework `here <./model.html#generative-models>`__).
|
||||
|
||||
Return:
|
||||
A list or a list of list of :obj:`dict`: Each result comes as a dictionary with the following keys:
|
||||
|
||||
- **translation_text** (:obj:`str`, present when ``return_text=True``) -- The translation.
|
||||
- **translation_token_ids** (:obj:`torch.Tensor` or :obj:`tf.Tensor`, present when ``return_tensors=True``)
|
||||
-- The token ids of the translation.
|
||||
"""
|
||||
return super().__call__(*args, **kwargs)
|
||||
@@ -1,15 +1,14 @@
|
||||
import numpy as np
|
||||
|
||||
from transformers.file_utils import add_end_docstrings, is_tf_available, is_torch_available
|
||||
|
||||
from ..file_utils import add_end_docstrings, is_tf_available, is_torch_available
|
||||
from .base import PIPELINE_INIT_ARGS, Pipeline
|
||||
|
||||
|
||||
if is_tf_available():
|
||||
from transformers.models.auto.modeling_tf_auto import TF_MODEL_FOR_SEQUENCE_CLASSIFICATION_MAPPING
|
||||
from ..models.auto.modeling_tf_auto import TF_MODEL_FOR_SEQUENCE_CLASSIFICATION_MAPPING
|
||||
|
||||
if is_torch_available():
|
||||
from transformers.models.auto.modeling_auto import MODEL_FOR_SEQUENCE_CLASSIFICATION_MAPPING
|
||||
from ..models.auto.modeling_auto import MODEL_FOR_SEQUENCE_CLASSIFICATION_MAPPING
|
||||
|
||||
|
||||
@add_end_docstrings(
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from transformers.file_utils import add_end_docstrings
|
||||
|
||||
from ..file_utils import add_end_docstrings
|
||||
from .base import PIPELINE_INIT_ARGS, Pipeline
|
||||
|
||||
|
||||
@@ -51,25 +50,15 @@ class TextGenerationPipeline(Pipeline):
|
||||
self.check_model_type(self.ALLOWED_MODELS)
|
||||
|
||||
# overriding _parse_and_tokenize to allow for unusual language-modeling tokenizer arguments
|
||||
|
||||
def _parse_and_tokenize(self, inputs, padding=True, add_special_tokens=True, **kwargs):
|
||||
def _parse_and_tokenize(self, *args, **kwargs):
|
||||
"""
|
||||
Parse arguments and tokenize
|
||||
"""
|
||||
# Parse arguments
|
||||
if self.model.__class__.__name__ in ["TransfoXLLMHeadModel"]:
|
||||
tokenizer_kwargs = {"add_space_before_punct_symbol": True}
|
||||
else:
|
||||
tokenizer_kwargs = {}
|
||||
inputs = self.tokenizer(
|
||||
inputs,
|
||||
add_special_tokens=add_special_tokens,
|
||||
return_tensors=self.framework,
|
||||
padding=padding,
|
||||
**tokenizer_kwargs,
|
||||
)
|
||||
kwargs.update({"add_space_before_punct_symbol": True})
|
||||
|
||||
return inputs
|
||||
return super()._parse_and_tokenize(*args, **kwargs)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
from typing import TYPE_CHECKING, List, Optional, Union
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ..file_utils import add_end_docstrings, is_tf_available, is_torch_available
|
||||
from ..modelcard import ModelCard
|
||||
from ..models.bert.tokenization_bert import BasicTokenizer
|
||||
from ..tokenization_utils import PreTrainedTokenizer
|
||||
from .base import PIPELINE_INIT_ARGS, ArgumentHandler, Pipeline
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..modeling_tf_utils import TFPreTrainedModel
|
||||
from ..modeling_utils import PreTrainedModel
|
||||
|
||||
if is_tf_available():
|
||||
|
||||
from ..models.auto.modeling_tf_auto import TF_MODEL_FOR_TOKEN_CLASSIFICATION_MAPPING
|
||||
|
||||
if is_torch_available():
|
||||
import torch
|
||||
|
||||
from ..models.auto.modeling_auto import MODEL_FOR_TOKEN_CLASSIFICATION_MAPPING
|
||||
|
||||
|
||||
class TokenClassificationArgumentHandler(ArgumentHandler):
|
||||
"""
|
||||
Handles arguments for token classification.
|
||||
"""
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
|
||||
if args is not None and len(args) > 0:
|
||||
inputs = list(args)
|
||||
batch_size = len(inputs)
|
||||
else:
|
||||
raise ValueError("At least one input is required.")
|
||||
|
||||
offset_mapping = kwargs.get("offset_mapping")
|
||||
if offset_mapping:
|
||||
if isinstance(offset_mapping, list) and isinstance(offset_mapping[0], tuple):
|
||||
offset_mapping = [offset_mapping]
|
||||
if len(offset_mapping) != batch_size:
|
||||
raise ValueError("offset_mapping should have the same batch size as the input")
|
||||
return inputs, offset_mapping
|
||||
|
||||
|
||||
@add_end_docstrings(
|
||||
PIPELINE_INIT_ARGS,
|
||||
r"""
|
||||
ignore_labels (:obj:`List[str]`, defaults to :obj:`["O"]`):
|
||||
A list of labels to ignore.
|
||||
grouped_entities (:obj:`bool`, `optional`, defaults to :obj:`False`):
|
||||
Whether or not to group the tokens corresponding to the same entity together in the predictions or not.
|
||||
""",
|
||||
)
|
||||
class TokenClassificationPipeline(Pipeline):
|
||||
"""
|
||||
Named Entity Recognition pipeline using any :obj:`ModelForTokenClassification`. See the `named entity recognition
|
||||
examples <../task_summary.html#named-entity-recognition>`__ for more information.
|
||||
|
||||
This token recognition pipeline can currently be loaded from :func:`~transformers.pipeline` using the following
|
||||
task identifier: :obj:`"ner"` (for predicting the classes of tokens in a sequence: person, organisation, location
|
||||
or miscellaneous).
|
||||
|
||||
The models that this pipeline can use are models that have been fine-tuned on a token classification task. See the
|
||||
up-to-date list of available models on `huggingface.co/models
|
||||
<https://huggingface.co/models?filter=token-classification>`__.
|
||||
"""
|
||||
|
||||
default_input_names = "sequences"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model: Union["PreTrainedModel", "TFPreTrainedModel"],
|
||||
tokenizer: PreTrainedTokenizer,
|
||||
modelcard: Optional[ModelCard] = None,
|
||||
framework: Optional[str] = None,
|
||||
args_parser: ArgumentHandler = TokenClassificationArgumentHandler(),
|
||||
device: int = -1,
|
||||
binary_output: bool = False,
|
||||
ignore_labels=["O"],
|
||||
task: str = "",
|
||||
grouped_entities: bool = False,
|
||||
ignore_subwords: bool = False,
|
||||
):
|
||||
super().__init__(
|
||||
model=model,
|
||||
tokenizer=tokenizer,
|
||||
modelcard=modelcard,
|
||||
framework=framework,
|
||||
device=device,
|
||||
binary_output=binary_output,
|
||||
task=task,
|
||||
)
|
||||
|
||||
self.check_model_type(
|
||||
TF_MODEL_FOR_TOKEN_CLASSIFICATION_MAPPING
|
||||
if self.framework == "tf"
|
||||
else MODEL_FOR_TOKEN_CLASSIFICATION_MAPPING
|
||||
)
|
||||
|
||||
self._basic_tokenizer = BasicTokenizer(do_lower_case=False)
|
||||
self._args_parser = args_parser
|
||||
self.ignore_labels = ignore_labels
|
||||
self.grouped_entities = grouped_entities
|
||||
self.ignore_subwords = ignore_subwords
|
||||
|
||||
if self.ignore_subwords and not self.tokenizer.is_fast:
|
||||
raise ValueError(
|
||||
"Slow tokenizers cannot ignore subwords. Please set the `ignore_subwords` option"
|
||||
"to `False` or use a fast tokenizer."
|
||||
)
|
||||
|
||||
def __call__(self, inputs: Union[str, List[str]], **kwargs):
|
||||
"""
|
||||
Classify each token of the text(s) given as inputs.
|
||||
|
||||
Args:
|
||||
inputs (:obj:`str` or :obj:`List[str]`):
|
||||
One or several texts (or one list of texts) for token classification.
|
||||
|
||||
Return:
|
||||
A list or a list of list of :obj:`dict`: Each result comes as a list of dictionaries (one for each token in
|
||||
the corresponding input, or each entity if this pipeline was instantiated with
|
||||
:obj:`grouped_entities=True`) with the following keys:
|
||||
|
||||
- **word** (:obj:`str`) -- The token/word classified.
|
||||
- **score** (:obj:`float`) -- The corresponding probability for :obj:`entity`.
|
||||
- **entity** (:obj:`str`) -- The entity predicted for that token/word (it is named `entity_group` when
|
||||
`grouped_entities` is set to True.
|
||||
- **index** (:obj:`int`, only present when ``self.grouped_entities=False``) -- The index of the
|
||||
corresponding token in the sentence.
|
||||
- **start** (:obj:`int`, `optional`) -- The index of the start of the corresponding entity in the sentence.
|
||||
Only exists if the offsets are available within the tokenizer
|
||||
- **end** (:obj:`int`, `optional`) -- The index of the end of the corresponding entity in the sentence.
|
||||
Only exists if the offsets are available within the tokenizer
|
||||
"""
|
||||
|
||||
inputs, offset_mappings = self._args_parser(inputs, **kwargs)
|
||||
|
||||
answers = []
|
||||
|
||||
for i, sentence in enumerate(inputs):
|
||||
|
||||
# Manage correct placement of the tensors
|
||||
with self.device_placement():
|
||||
|
||||
tokens = self.tokenizer(
|
||||
sentence,
|
||||
return_attention_mask=False,
|
||||
return_tensors=self.framework,
|
||||
truncation=True,
|
||||
return_special_tokens_mask=True,
|
||||
return_offsets_mapping=self.tokenizer.is_fast,
|
||||
)
|
||||
if self.tokenizer.is_fast:
|
||||
offset_mapping = tokens.pop("offset_mapping").cpu().numpy()[0]
|
||||
elif offset_mappings:
|
||||
offset_mapping = offset_mappings[i]
|
||||
else:
|
||||
offset_mapping = None
|
||||
|
||||
special_tokens_mask = tokens.pop("special_tokens_mask").cpu().numpy()[0]
|
||||
|
||||
# Forward
|
||||
if self.framework == "tf":
|
||||
entities = self.model(tokens.data)[0][0].numpy()
|
||||
input_ids = tokens["input_ids"].numpy()[0]
|
||||
else:
|
||||
with torch.no_grad():
|
||||
tokens = self.ensure_tensor_on_device(**tokens)
|
||||
entities = self.model(**tokens)[0][0].cpu().numpy()
|
||||
input_ids = tokens["input_ids"].cpu().numpy()[0]
|
||||
|
||||
score = np.exp(entities) / np.exp(entities).sum(-1, keepdims=True)
|
||||
labels_idx = score.argmax(axis=-1)
|
||||
|
||||
entities = []
|
||||
# Filter to labels not in `self.ignore_labels`
|
||||
# Filter special_tokens
|
||||
filtered_labels_idx = [
|
||||
(idx, label_idx)
|
||||
for idx, label_idx in enumerate(labels_idx)
|
||||
if (self.model.config.id2label[label_idx] not in self.ignore_labels) and not special_tokens_mask[idx]
|
||||
]
|
||||
|
||||
for idx, label_idx in filtered_labels_idx:
|
||||
if offset_mapping is not None:
|
||||
start_ind, end_ind = offset_mapping[idx]
|
||||
word_ref = sentence[start_ind:end_ind]
|
||||
word = self.tokenizer.convert_ids_to_tokens([int(input_ids[idx])])[0]
|
||||
is_subword = len(word_ref) != len(word)
|
||||
|
||||
if int(input_ids[idx]) == self.tokenizer.unk_token_id:
|
||||
word = word_ref
|
||||
is_subword = False
|
||||
else:
|
||||
word = self.tokenizer.convert_ids_to_tokens(int(input_ids[idx]))
|
||||
|
||||
start_ind = None
|
||||
end_ind = None
|
||||
|
||||
entity = {
|
||||
"word": word,
|
||||
"score": score[idx][label_idx].item(),
|
||||
"entity": self.model.config.id2label[label_idx],
|
||||
"index": idx,
|
||||
"start": start_ind,
|
||||
"end": end_ind,
|
||||
}
|
||||
|
||||
if self.grouped_entities and self.ignore_subwords:
|
||||
entity["is_subword"] = is_subword
|
||||
|
||||
entities += [entity]
|
||||
|
||||
if self.grouped_entities:
|
||||
answers += [self.group_entities(entities)]
|
||||
# Append ungrouped entities
|
||||
else:
|
||||
answers += [entities]
|
||||
|
||||
if len(answers) == 1:
|
||||
return answers[0]
|
||||
return answers
|
||||
|
||||
def group_sub_entities(self, entities: List[dict]) -> dict:
|
||||
"""
|
||||
Group together the adjacent tokens with the same entity predicted.
|
||||
|
||||
Args:
|
||||
entities (:obj:`dict`): The entities predicted by the pipeline.
|
||||
"""
|
||||
# Get the first entity in the entity group
|
||||
entity = entities[0]["entity"].split("-")[-1]
|
||||
scores = np.nanmean([entity["score"] for entity in entities])
|
||||
tokens = [entity["word"] for entity in entities]
|
||||
|
||||
entity_group = {
|
||||
"entity_group": entity,
|
||||
"score": np.mean(scores),
|
||||
"word": self.tokenizer.convert_tokens_to_string(tokens),
|
||||
"start": entities[0]["start"],
|
||||
"end": entities[-1]["end"],
|
||||
}
|
||||
return entity_group
|
||||
|
||||
def group_entities(self, entities: List[dict]) -> List[dict]:
|
||||
"""
|
||||
Find and group together the adjacent tokens with the same entity predicted.
|
||||
|
||||
Args:
|
||||
entities (:obj:`dict`): The entities predicted by the pipeline.
|
||||
"""
|
||||
|
||||
entity_groups = []
|
||||
entity_group_disagg = []
|
||||
|
||||
if entities:
|
||||
last_idx = entities[-1]["index"]
|
||||
|
||||
for entity in entities:
|
||||
|
||||
is_last_idx = entity["index"] == last_idx
|
||||
is_subword = self.ignore_subwords and entity["is_subword"]
|
||||
if not entity_group_disagg:
|
||||
entity_group_disagg += [entity]
|
||||
if is_last_idx:
|
||||
entity_groups += [self.group_sub_entities(entity_group_disagg)]
|
||||
continue
|
||||
|
||||
# If the current entity is similar and adjacent to the previous entity, append it to the disaggregated entity group
|
||||
# The split is meant to account for the "B" and "I" suffixes
|
||||
# Shouldn't merge if both entities are B-type
|
||||
if (
|
||||
(
|
||||
entity["entity"].split("-")[-1] == entity_group_disagg[-1]["entity"].split("-")[-1]
|
||||
and entity["entity"].split("-")[0] != "B"
|
||||
)
|
||||
and entity["index"] == entity_group_disagg[-1]["index"] + 1
|
||||
) or is_subword:
|
||||
# Modify subword type to be previous_type
|
||||
if is_subword:
|
||||
entity["entity"] = entity_group_disagg[-1]["entity"].split("-")[-1]
|
||||
entity["score"] = np.nan # set ignored scores to nan and use np.nanmean
|
||||
|
||||
entity_group_disagg += [entity]
|
||||
# Group the entities at the last entity
|
||||
if is_last_idx:
|
||||
entity_groups += [self.group_sub_entities(entity_group_disagg)]
|
||||
# If the current entity is different from the previous entity, aggregate the disaggregated entity group
|
||||
else:
|
||||
entity_groups += [self.group_sub_entities(entity_group_disagg)]
|
||||
entity_group_disagg = [entity]
|
||||
# If it's the last entity, add it to the entity groups
|
||||
if is_last_idx:
|
||||
entity_groups += [self.group_sub_entities(entity_group_disagg)]
|
||||
|
||||
return entity_groups
|
||||
|
||||
|
||||
NerPipeline = TokenClassificationPipeline
|
||||
@@ -2,9 +2,9 @@ from typing import List, Union
|
||||
|
||||
import numpy as np
|
||||
|
||||
from transformers.file_utils import add_end_docstrings
|
||||
from transformers.utils import logging
|
||||
|
||||
from ..file_utils import add_end_docstrings
|
||||
from ..tokenization_utils import TruncationStrategy
|
||||
from ..utils import logging
|
||||
from .base import PIPELINE_INIT_ARGS, ArgumentHandler, Pipeline
|
||||
|
||||
|
||||
@@ -79,7 +79,14 @@ class ZeroShotClassificationPipeline(Pipeline):
|
||||
return -1
|
||||
|
||||
def _parse_and_tokenize(
|
||||
self, sequences, candidate_labels, hypothesis_template, padding=True, add_special_tokens=True, **kwargs
|
||||
self,
|
||||
sequences,
|
||||
candidate_labels,
|
||||
hypothesis_template,
|
||||
padding=True,
|
||||
add_special_tokens=True,
|
||||
truncation=TruncationStrategy.ONLY_FIRST,
|
||||
**kwargs
|
||||
):
|
||||
"""
|
||||
Parse arguments and tokenize only_first so that hypothesis (label) is not truncated
|
||||
@@ -90,7 +97,7 @@ class ZeroShotClassificationPipeline(Pipeline):
|
||||
add_special_tokens=add_special_tokens,
|
||||
return_tensors=self.framework,
|
||||
padding=padding,
|
||||
truncation="only_first",
|
||||
truncation=truncation,
|
||||
)
|
||||
|
||||
return inputs
|
||||
|
||||
@@ -12,15 +12,21 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from string import ascii_lowercase
|
||||
from typing import List, Optional
|
||||
from unittest import mock
|
||||
|
||||
from transformers import is_tf_available, is_torch_available, pipeline
|
||||
from transformers.pipelines import Pipeline
|
||||
from transformers.testing_utils import _run_slow_tests, is_pipeline_test, require_tf, require_torch, slow
|
||||
from transformers.tokenization_utils import TruncationStrategy
|
||||
from transformers.tokenization_utils_base import to_py_obj
|
||||
|
||||
|
||||
if is_torch_available():
|
||||
import torch
|
||||
|
||||
|
||||
VALID_INPUTS = ["A simple string", ["list of strings"]]
|
||||
|
||||
|
||||
@@ -242,3 +248,67 @@ class MonoInputPipelineCommonMixin(CustomInputPipelineCommonMixin):
|
||||
self.assertIn(key, result)
|
||||
|
||||
self.assertRaises(Exception, nlp, self.invalid_inputs)
|
||||
|
||||
|
||||
class DummyTok:
|
||||
pad_token_id = 0
|
||||
eos_token_id = 0
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
for name, v in kwargs.items():
|
||||
setattr(self, name, v)
|
||||
self.index = 0
|
||||
|
||||
def __call__(self, inputs, **kwargs):
|
||||
if kwargs.get("return_tensors", "") == "pt":
|
||||
return self.encode_pt(inputs, **kwargs)
|
||||
else:
|
||||
return self.encode_list(inputs, **kwargs)
|
||||
|
||||
def encode_list(self, inputs, **kwargs):
|
||||
unwrap = False
|
||||
if isinstance(inputs, str):
|
||||
unwrap = True
|
||||
inputs = [inputs]
|
||||
|
||||
assert isinstance(inputs, list)
|
||||
input_ids = [self.encode(input_) for input_ in inputs]
|
||||
|
||||
if unwrap:
|
||||
input_ids = input_ids[0]
|
||||
|
||||
return {"input_ids": input_ids}
|
||||
|
||||
def encode_pt(self, inputs, **kwargs):
|
||||
if isinstance(inputs, str):
|
||||
input_ids = torch.LongTensor(self.encode(inputs)).unsqueeze(0)
|
||||
else:
|
||||
input_ids = self._pad([self.encode(input_) for input_ in inputs])
|
||||
return self.finalize_pt(input_ids, **kwargs)
|
||||
|
||||
def finalize_pt(self, input_ids, **kwargs):
|
||||
if kwargs.get("truncation", TruncationStrategy.DO_NOT_TRUNCATE) == TruncationStrategy.ONLY_FIRST:
|
||||
input_ids = input_ids[:, : self.model_max_length]
|
||||
attention_mask = torch.zeros_like(input_ids).long() + 1
|
||||
return {"input_ids": input_ids, "attention_mask": attention_mask}
|
||||
|
||||
def _pad(self, inputs):
|
||||
return torch.nn.utils.rnn.pad_sequence(
|
||||
[torch.LongTensor(input_) for input_ in inputs],
|
||||
padding_value=self.pad_token_id,
|
||||
).transpose(1, 0)
|
||||
|
||||
def pad(self, inputs, **kwargs):
|
||||
input_ids = self._pad(inputs["input_ids"])
|
||||
return self.finalize_pt(input_ids, **kwargs)
|
||||
|
||||
def encode(self, input_):
|
||||
return list(input_.encode("utf-8"))
|
||||
|
||||
def decode(self, sequence, **kwargs):
|
||||
string = ""
|
||||
for i in range(len(sequence)):
|
||||
string += ascii_lowercase[self.index]
|
||||
self.index += 1
|
||||
self.index %= len(ascii_lowercase)
|
||||
return string
|
||||
@@ -14,15 +14,138 @@
|
||||
|
||||
import unittest
|
||||
|
||||
from transformers import AutoModelForSeq2SeqLM, AutoTokenizer, Conversation, ConversationalPipeline, pipeline
|
||||
from transformers import (
|
||||
AutoModelForSeq2SeqLM,
|
||||
AutoTokenizer,
|
||||
Conversation,
|
||||
ConversationalPipeline,
|
||||
is_torch_available,
|
||||
pipeline,
|
||||
)
|
||||
from transformers.testing_utils import require_torch, slow, torch_device
|
||||
|
||||
from .test_pipelines_common import MonoInputPipelineCommonMixin
|
||||
from .test_pipelines_common import DummyTok, MonoInputPipelineCommonMixin
|
||||
|
||||
|
||||
if is_torch_available():
|
||||
from transformers.models.gpt2 import GPT2Config, GPT2LMHeadModel
|
||||
|
||||
DEFAULT_DEVICE_NUM = -1 if torch_device == "cpu" else 0
|
||||
|
||||
|
||||
class SimpleConversationPipelineTests(unittest.TestCase):
|
||||
def get_pipeline(self):
|
||||
# When
|
||||
config = GPT2Config(
|
||||
vocab_size=257,
|
||||
n_ctx=64,
|
||||
max_length=64,
|
||||
n_embd=64,
|
||||
n_layer=1,
|
||||
n_head=8,
|
||||
eos_token_id=0,
|
||||
bos_token_id=0,
|
||||
pad_token_id=0,
|
||||
)
|
||||
model = GPT2LMHeadModel(config)
|
||||
tokenizer = DummyTok()
|
||||
nlp = pipeline(task="conversational", device=DEFAULT_DEVICE_NUM, model=model, tokenizer=tokenizer)
|
||||
return nlp
|
||||
|
||||
@require_torch
|
||||
def test_integration_torch_conversation(self):
|
||||
nlp = self.get_pipeline()
|
||||
conversation_1 = Conversation("Going to the movies tonight - any suggestions?")
|
||||
conversation_2 = Conversation("What's the last book you have read?")
|
||||
# Then
|
||||
self.assertEqual(len(conversation_1.past_user_inputs), 0)
|
||||
self.assertEqual(len(conversation_2.past_user_inputs), 0)
|
||||
# When
|
||||
result = nlp([conversation_1, conversation_2], max_length=48)
|
||||
# Then
|
||||
self.assertEqual(result, [conversation_1, conversation_2])
|
||||
self.assertEqual(
|
||||
result,
|
||||
[
|
||||
Conversation(
|
||||
None,
|
||||
past_user_inputs=["Going to the movies tonight - any suggestions?"],
|
||||
generated_responses=["a"],
|
||||
),
|
||||
Conversation(
|
||||
None, past_user_inputs=["What's the last book you have read?"], generated_responses=["b"]
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
# When
|
||||
conversation_2.add_user_input("Why do you recommend it?")
|
||||
result = nlp(conversation_2, max_length=49)
|
||||
# Then
|
||||
self.assertEqual(result, conversation_2)
|
||||
self.assertEqual(
|
||||
result,
|
||||
Conversation(
|
||||
None,
|
||||
past_user_inputs=["What's the last book you have read?", "Why do you recommend it?"],
|
||||
generated_responses=["b", "c"],
|
||||
),
|
||||
)
|
||||
|
||||
def test_history_cache(self):
|
||||
nlp = self.get_pipeline()
|
||||
conversation = Conversation(
|
||||
"Why do you recommend it?",
|
||||
past_user_inputs=["What's the last book you have read?"],
|
||||
generated_responses=["b"],
|
||||
)
|
||||
_ = nlp(conversation, max_length=26)
|
||||
self.assertEqual(conversation._index, 1)
|
||||
self.assertEqual(
|
||||
conversation._history,
|
||||
[
|
||||
87,
|
||||
104,
|
||||
97,
|
||||
116,
|
||||
39,
|
||||
115,
|
||||
32,
|
||||
116,
|
||||
104,
|
||||
101,
|
||||
32,
|
||||
108,
|
||||
97,
|
||||
115,
|
||||
116,
|
||||
32,
|
||||
98,
|
||||
111,
|
||||
111,
|
||||
107,
|
||||
32,
|
||||
121,
|
||||
111,
|
||||
117,
|
||||
32,
|
||||
104,
|
||||
97,
|
||||
118,
|
||||
101,
|
||||
32,
|
||||
114,
|
||||
101,
|
||||
97,
|
||||
100,
|
||||
63,
|
||||
0,
|
||||
98,
|
||||
0,
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class ConversationalPipelineTests(MonoInputPipelineCommonMixin, unittest.TestCase):
|
||||
pipeline_task = "conversational"
|
||||
small_models = [] # Models tested without the @slow decorator
|
||||
|
||||
@@ -15,14 +15,45 @@
|
||||
import unittest
|
||||
|
||||
from transformers import pipeline
|
||||
from transformers.models.bart import BartConfig, BartForConditionalGeneration
|
||||
from transformers.testing_utils import require_torch, slow, torch_device
|
||||
from transformers.tokenization_utils import TruncationStrategy
|
||||
|
||||
from .test_pipelines_common import MonoInputPipelineCommonMixin
|
||||
from .test_pipelines_common import DummyTok, MonoInputPipelineCommonMixin
|
||||
|
||||
|
||||
DEFAULT_DEVICE_NUM = -1 if torch_device == "cpu" else 0
|
||||
|
||||
|
||||
class SimpleSummarizationPipelineTests(unittest.TestCase):
|
||||
@require_torch
|
||||
def test_input_too_long(self):
|
||||
config = BartConfig(
|
||||
vocab_size=257,
|
||||
d_model=32,
|
||||
encoder_layers=1,
|
||||
decoder_layers=1,
|
||||
encoder_ffn_dim=32,
|
||||
decoder_ffn_dim=32,
|
||||
# So any text > 4 should raise an exception
|
||||
max_position_embeddings=4,
|
||||
encoder_attention_heads=1,
|
||||
decoder_attention_heads=1,
|
||||
max_length=4,
|
||||
min_length=1,
|
||||
)
|
||||
model = BartForConditionalGeneration(config)
|
||||
tokenizer = DummyTok(model_max_length=4)
|
||||
nlp = pipeline(task="summarization", model=model, tokenizer=tokenizer)
|
||||
|
||||
with self.assertRaises(IndexError):
|
||||
_ = nlp("This is a test")
|
||||
|
||||
output = nlp("This is a test", truncation=TruncationStrategy.ONLY_FIRST)
|
||||
|
||||
self.assertEqual(output, [{"summary_text": "abcd"}])
|
||||
|
||||
|
||||
class SummarizationPipelineTests(MonoInputPipelineCommonMixin, unittest.TestCase):
|
||||
pipeline_task = "summarization"
|
||||
pipeline_running_kwargs = {"num_beams": 2, "min_length": 2, "max_length": 5}
|
||||
|
||||
Reference in new issue
Block a user