mirror of
https://github.com/deepset-ai/haystack.git
synced 2026-10-12 02:12:50 +00:00
feat: MultiModalRetriever (#2891)
* Adding Data2VecVision and Data2VecText to the supported models and adapt Tokenizers accordingly * content_types * Splitting classes into respective folders * small changes * Fix EOF * eof * black * API * EOF * whitespace * api * improve multimodal similarity processor * tokenizer -> feature extractor * Making feature vectors come out of the feature extractor in the similarity head * embed_queries is now self-sufficient * couple trivial errors * Implemented separate language model classes for multimodal inference * Document embedding seems to work * removing batch_encode_plus, is deprecated anyway * Realized the base Data2Vec models are not trained on retrieval tasks * Issue with the generated embeddings * Add batching * Try to fit CLIP in * Stub of CLIP integration * Retrieval goes through but returns noise only * Still working on the scores * Introduce temporary adapter for CLIP models * Image retrieval now works with sentence-transformers * Tidying up the code * Refactoring is now functional * Add MPNet to the supported sentence transformers models * Remove unused classes * pylint * docs * docs * Remove the method renaming * mpyp first pass * docs * tutorial * schema * mypy * Move devices setup into get_model * more mypy * mypy * pylint * Move a few params in HaystackModel's init * make feature extractor work with squadprocessor * fix feature_extractor_kwargs forwarding * Forgotten part of the fix * Revert unrelated ES change * Revert unrelated memdocstore changes * comment * Small corrections * mypy and pylint * mypy * typo * mypy * Refactor the call * mypy * Do not make FARMReader use the new FeatureExtractor * mypy * Detach DPR tests from FeatureExtractor too * Detach processor tests too * Add end2end marker * extract end2end feature extractor tests * temporary disable feature extraction tests * Introduce end2end tests for tokenizer tests * pylint * Fix model loading from folder in FeatureExtractor * working o n end2end * end2end keeps failing * Restructuring retriever tests * Restructuring retriever tests * remove covert_dataset_to_dataloader * remove comment * Better check sentence-transformers models * Use embed_meta_fields properly * rename passage into document * Embedding dims can't be found * Add check for models that support it * pylint * Split all retriever tests into suites, running mostly on InMemory only * fix mypy * fix tfidf test * fix weaviate tests * Parallelize on every docstore * Fix schema and specify modality in base retriever suite * tests * Add first image tests * remove comment * Revert to simpler tests * Update docs/_src/api/api/primitives.md Co-authored-by: Agnieszka Marzec <[email protected]> * Apply suggestions from code review Co-authored-by: Agnieszka Marzec <[email protected]> * Apply suggestions from code review Co-authored-by: Agnieszka Marzec <[email protected]> * Update haystack/modeling/model/multimodal/__init__.py Co-authored-by: Agnieszka Marzec <[email protected]> * Apply suggestions from code review Co-authored-by: Agnieszka Marzec <[email protected]> * Apply suggestions from code review Co-authored-by: Agnieszka Marzec <[email protected]> * Apply suggestions from code review Co-authored-by: Agnieszka Marzec <[email protected]> * Apply suggestions from code review Co-authored-by: Agnieszka Marzec <[email protected]> * Apply suggestions from code review * Apply suggestions from code review Co-authored-by: Agnieszka Marzec <[email protected]> * get_args * mypy * Update haystack/modeling/model/multimodal/__init__.py * Update haystack/modeling/model/multimodal/base.py * Update haystack/modeling/model/multimodal/base.py Co-authored-by: Agnieszka Marzec <[email protected]> * Update haystack/modeling/model/multimodal/sentence_transformers.py * Update haystack/modeling/model/multimodal/sentence_transformers.py Co-authored-by: Agnieszka Marzec <[email protected]> * Update haystack/modeling/model/multimodal/transformers.py * Update haystack/modeling/model/multimodal/transformers.py Co-authored-by: Agnieszka Marzec <[email protected]> * Update haystack/modeling/model/multimodal/transformers.py Co-authored-by: Agnieszka Marzec <[email protected]> * Apply suggestions from code review Co-authored-by: Agnieszka Marzec <[email protected]> * Apply suggestions from code review Co-authored-by: Agnieszka Marzec <[email protected]> * Apply suggestions from code review Co-authored-by: Agnieszka Marzec <[email protected]> * Update haystack/nodes/retriever/multimodal/retriever.py Co-authored-by: Agnieszka Marzec <[email protected]> * mypy * mypy * removing more ContentTypes * more contentypes * pylint * add to __init__ * revert end2end workflow for now * missing integration markers * Update haystack/nodes/retriever/multimodal/embedder.py Co-authored-by: bogdankostic <[email protected]> * review feedback, removing HaystackImageTransformerModel * review feedback part 2 * mypy & pylint * mypy * mypy * fix multimodal docs also for Pinecone * add note on internal constants * Fix pinecone write_documents * schemas * keep support for sentence-transformers only * fix pinecone test * schemas * fix pinecone again * temporarily disable some tests, need to understand if they're still relevant Co-authored-by: Agnieszka Marzec <[email protected]> Co-authored-by: bogdankostic <[email protected]>
This commit is contained in:
co-authored by
Agnieszka Marzec
bogdankostic
parent
ca2a1e1792
commit
101d2bc86c
@@ -798,4 +798,4 @@ jobs:
|
||||
with:
|
||||
status: ${{ job.status }}
|
||||
channel: '#haystack'
|
||||
if: failure() && github.repository_owner == 'deepset-ai' && github.ref == 'refs/heads/main'
|
||||
if: failure() && github.repository_owner == 'deepset-ai' && github.ref == 'refs/heads/main'
|
||||
@@ -2,6 +2,12 @@
|
||||
|
||||
# Module schema
|
||||
|
||||
<a id="schema.ContentTypes"></a>
|
||||
|
||||
#### ContentTypes
|
||||
|
||||
Types of supported content_types.
|
||||
|
||||
<a id="schema.Document"></a>
|
||||
|
||||
## Document
|
||||
@@ -17,7 +23,7 @@ class Document()
|
||||
|
||||
```python
|
||||
def __init__(content: Union[str, pd.DataFrame],
|
||||
content_type: Literal["text", "table", "image", "audio"] = "text",
|
||||
content_type: ContentTypes = "text",
|
||||
id: Optional[str] = None,
|
||||
score: Optional[float] = None,
|
||||
meta: Optional[Dict[str, Any]] = None,
|
||||
@@ -38,7 +44,7 @@ There's an easy option to convert from/to dicts via `from_dict()` and `to_dict`.
|
||||
**Arguments**:
|
||||
|
||||
- `content`: Content of the document. For most cases, this will be text, but it can be a table or image.
|
||||
- `content_type`: One of "text", "table" or "image". Haystack components can use this to adjust their
|
||||
- `content_type`: One of "text", "table", "image" or "audio". Haystack components can use this to adjust their
|
||||
handling of Documents and check compatibility.
|
||||
- `id`: Unique ID for the document. If not supplied by the user, we'll generate one automatically by
|
||||
creating a hash from the supplied text. This behaviour can be further adjusted by `id_hash_keys`.
|
||||
|
||||
@@ -383,7 +383,9 @@ class InMemoryDocumentStore(BaseDocumentStore):
|
||||
candidate_docs = []
|
||||
for doc, score in zip(document_to_search, scores):
|
||||
curr_meta = deepcopy(doc.meta)
|
||||
new_document = Document(id=doc.id, content=doc.content, meta=curr_meta, embedding=doc.embedding)
|
||||
new_document = Document(
|
||||
id=doc.id, content=doc.content, content_type=doc.content_type, meta=curr_meta, embedding=doc.embedding
|
||||
)
|
||||
new_document.embedding = doc.embedding if return_embedding is True else None
|
||||
|
||||
new_document.embedding = doc.embedding if return_embedding is True else None
|
||||
@@ -705,7 +707,7 @@ class InMemoryDocumentStore(BaseDocumentStore):
|
||||
raise NotImplementedError("InMemoryDocumentStore does not support headers.")
|
||||
|
||||
logger.warning(
|
||||
"""DEPRECATION WARNINGS:
|
||||
"""DEPRECATION WARNINGS:
|
||||
1. delete_all_documents() method is deprecated, please use delete_documents method
|
||||
For more details, please refer to the issue: https://github.com/deepset-ai/haystack/issues/1045
|
||||
"""
|
||||
|
||||
@@ -392,7 +392,10 @@ class PineconeDocumentStore(BaseDocumentStore):
|
||||
elif duplicate_documents == "fail":
|
||||
# Otherwise, we raise an error
|
||||
raise DuplicateDocumentError(f"Duplicate document IDs found in batch: {ids}")
|
||||
metadata = [{"content": doc.content, **doc.meta} for doc in document_objects[i : i + batch_size]]
|
||||
metadata = [
|
||||
{"content": doc.content, "content_type": doc.content_type, **doc.meta}
|
||||
for doc in document_objects[i : i + batch_size]
|
||||
]
|
||||
if add_vectors:
|
||||
embeddings = [doc.embedding for doc in document_objects[i : i + batch_size]]
|
||||
embeddings_to_index = np.array(embeddings, dtype="float32")
|
||||
@@ -498,7 +501,7 @@ class PineconeDocumentStore(BaseDocumentStore):
|
||||
metadata = []
|
||||
ids = []
|
||||
for doc in document_batch:
|
||||
metadata.append({"content": doc.content, **doc.meta})
|
||||
metadata.append({"content": doc.content, "content_type": doc.content_type, **doc.meta})
|
||||
ids.append(doc.id)
|
||||
# Update existing vectors in pinecone index
|
||||
self.pinecone_indexes[index].upsert(
|
||||
@@ -892,7 +895,7 @@ class PineconeDocumentStore(BaseDocumentStore):
|
||||
|
||||
doc = self.get_documents_by_id(ids=[id], index=index, return_embedding=True)[0]
|
||||
if doc.embedding is not None:
|
||||
meta = {"content": doc.content, **meta}
|
||||
meta = {"content": doc.content, "content_type": doc.content_type, **meta}
|
||||
self.pinecone_indexes[index].upsert(vectors=[(id, doc.embedding.tolist(), meta)], namespace=namespace)
|
||||
|
||||
def delete_documents(
|
||||
@@ -1177,7 +1180,8 @@ class PineconeDocumentStore(BaseDocumentStore):
|
||||
documents = []
|
||||
for _id, meta in zip(ids, metadata):
|
||||
content = meta.pop("content")
|
||||
doc = Document(id=_id, content=content, meta=meta)
|
||||
content_type = meta.pop("content_type")
|
||||
doc = Document(id=_id, content=content, content_type=content_type, meta=meta)
|
||||
documents.append(doc)
|
||||
if return_embedding:
|
||||
if values is None:
|
||||
|
||||
@@ -628,7 +628,7 @@ class SQLDocumentStore(BaseDocumentStore):
|
||||
raise NotImplementedError("SQLDocumentStore does not support headers.")
|
||||
|
||||
logger.warning(
|
||||
"""DEPRECATION WARNINGS:
|
||||
"""DEPRECATION WARNINGS:
|
||||
1. delete_all_documents() method is deprecated, please use delete_documents method
|
||||
For more details, please refer to the issue: https://github.com/deepset-ai/haystack/issues/1045
|
||||
"""
|
||||
|
||||
@@ -2572,6 +2572,165 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"MultiModalRetrieverComponent": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "Custom name for the component. Helpful for visualization and debugging.",
|
||||
"title": "Name",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"additionalProperties": false,
|
||||
"description": "Each parameter can reference other components defined in the same YAML file.",
|
||||
"properties": {
|
||||
"batch_size": {
|
||||
"default": 16,
|
||||
"title": "Batch Size",
|
||||
"type": "integer"
|
||||
},
|
||||
"devices": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Devices"
|
||||
},
|
||||
"document_embedding_models": {
|
||||
"additionalProperties": {
|
||||
"anyOf": [
|
||||
{
|
||||
"format": "path",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"title": "Document Embedding Models",
|
||||
"type": "object"
|
||||
},
|
||||
"document_feature_extractors_params": {
|
||||
"additionalProperties": {
|
||||
"type": "object"
|
||||
},
|
||||
"default": {
|
||||
"text": {
|
||||
"max_length": 256
|
||||
}
|
||||
},
|
||||
"title": "Document Feature Extractors Params",
|
||||
"type": "object"
|
||||
},
|
||||
"document_store": {
|
||||
"title": "Document Store",
|
||||
"type": "string"
|
||||
},
|
||||
"embed_meta_fields": {
|
||||
"default": [
|
||||
"name"
|
||||
],
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": "Embed Meta Fields",
|
||||
"type": "array"
|
||||
},
|
||||
"progress_bar": {
|
||||
"default": true,
|
||||
"title": "Progress Bar",
|
||||
"type": "boolean"
|
||||
},
|
||||
"query_embedding_model": {
|
||||
"anyOf": [
|
||||
{
|
||||
"format": "path",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"title": "Query Embedding Model"
|
||||
},
|
||||
"query_feature_extractor_params": {
|
||||
"default": {
|
||||
"max_length": 64
|
||||
},
|
||||
"title": "Query Feature Extractor Params",
|
||||
"type": "object"
|
||||
},
|
||||
"query_type": {
|
||||
"default": "text",
|
||||
"title": "Query Type",
|
||||
"type": "string"
|
||||
},
|
||||
"scale_score": {
|
||||
"default": true,
|
||||
"title": "Scale Score",
|
||||
"type": "boolean"
|
||||
},
|
||||
"similarity_function": {
|
||||
"default": "dot_product",
|
||||
"title": "Similarity Function",
|
||||
"type": "string"
|
||||
},
|
||||
"top_k": {
|
||||
"default": 10,
|
||||
"title": "Top K",
|
||||
"type": "integer"
|
||||
},
|
||||
"use_auth_token": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Use Auth Token"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"document_store",
|
||||
"query_embedding_model",
|
||||
"document_embedding_models"
|
||||
],
|
||||
"title": "Parameters",
|
||||
"type": "object"
|
||||
},
|
||||
"type": {
|
||||
"const": "MultiModalRetriever",
|
||||
"description": "Haystack Class name for the component.",
|
||||
"title": "Type",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"name"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"MultihopEmbeddingRetrieverComponent": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
@@ -6241,6 +6400,9 @@
|
||||
{
|
||||
"$ref": "#/definitions/MarkdownConverterComponent"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/MultiModalRetrieverComponent"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/MultihopEmbeddingRetrieverComponent"
|
||||
},
|
||||
|
||||
@@ -2572,6 +2572,165 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"MultiModalRetrieverComponent": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "Custom name for the component. Helpful for visualization and debugging.",
|
||||
"title": "Name",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"additionalProperties": false,
|
||||
"description": "Each parameter can reference other components defined in the same YAML file.",
|
||||
"properties": {
|
||||
"batch_size": {
|
||||
"default": 16,
|
||||
"title": "Batch Size",
|
||||
"type": "integer"
|
||||
},
|
||||
"devices": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Devices"
|
||||
},
|
||||
"document_embedding_models": {
|
||||
"additionalProperties": {
|
||||
"anyOf": [
|
||||
{
|
||||
"format": "path",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"title": "Document Embedding Models",
|
||||
"type": "object"
|
||||
},
|
||||
"document_feature_extractors_params": {
|
||||
"additionalProperties": {
|
||||
"type": "object"
|
||||
},
|
||||
"default": {
|
||||
"text": {
|
||||
"max_length": 256
|
||||
}
|
||||
},
|
||||
"title": "Document Feature Extractors Params",
|
||||
"type": "object"
|
||||
},
|
||||
"document_store": {
|
||||
"title": "Document Store",
|
||||
"type": "string"
|
||||
},
|
||||
"embed_meta_fields": {
|
||||
"default": [
|
||||
"name"
|
||||
],
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": "Embed Meta Fields",
|
||||
"type": "array"
|
||||
},
|
||||
"progress_bar": {
|
||||
"default": true,
|
||||
"title": "Progress Bar",
|
||||
"type": "boolean"
|
||||
},
|
||||
"query_embedding_model": {
|
||||
"anyOf": [
|
||||
{
|
||||
"format": "path",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"title": "Query Embedding Model"
|
||||
},
|
||||
"query_feature_extractor_params": {
|
||||
"default": {
|
||||
"max_length": 64
|
||||
},
|
||||
"title": "Query Feature Extractor Params",
|
||||
"type": "object"
|
||||
},
|
||||
"query_type": {
|
||||
"default": "text",
|
||||
"title": "Query Type",
|
||||
"type": "string"
|
||||
},
|
||||
"scale_score": {
|
||||
"default": true,
|
||||
"title": "Scale Score",
|
||||
"type": "boolean"
|
||||
},
|
||||
"similarity_function": {
|
||||
"default": "dot_product",
|
||||
"title": "Similarity Function",
|
||||
"type": "string"
|
||||
},
|
||||
"top_k": {
|
||||
"default": 10,
|
||||
"title": "Top K",
|
||||
"type": "integer"
|
||||
},
|
||||
"use_auth_token": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Use Auth Token"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"document_store",
|
||||
"query_embedding_model",
|
||||
"document_embedding_models"
|
||||
],
|
||||
"title": "Parameters",
|
||||
"type": "object"
|
||||
},
|
||||
"type": {
|
||||
"const": "MultiModalRetriever",
|
||||
"description": "Haystack Class name for the component.",
|
||||
"title": "Type",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"name"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"MultihopEmbeddingRetrieverComponent": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
@@ -6241,6 +6400,9 @@
|
||||
{
|
||||
"$ref": "#/definitions/MarkdownConverterComponent"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/MultiModalRetrieverComponent"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/MultihopEmbeddingRetrieverComponent"
|
||||
},
|
||||
|
||||
@@ -1,28 +1,33 @@
|
||||
from typing import List
|
||||
|
||||
from math import ceil
|
||||
|
||||
import torch
|
||||
from torch.utils.data import DataLoader
|
||||
from torch.utils.data import DataLoader, Dataset, Sampler
|
||||
|
||||
from haystack.errors import ModelingError
|
||||
|
||||
|
||||
class NamedDataLoader(DataLoader):
|
||||
"""
|
||||
A modified version of the PyTorch DataLoader that returns a dictionary where the key is
|
||||
the name of the tensor and the value is the tensor itself.
|
||||
"""
|
||||
|
||||
def __init__(self, dataset, batch_size, sampler=None, tensor_names=None, num_workers=0, pin_memory=False):
|
||||
def __init__(
|
||||
self,
|
||||
dataset: Dataset,
|
||||
batch_size: int,
|
||||
sampler: Sampler = None,
|
||||
tensor_names: List[str] = None,
|
||||
num_workers: int = 0,
|
||||
pin_memory: bool = False,
|
||||
):
|
||||
"""
|
||||
A modified version of the PyTorch DataLoader that returns a dictionary where the key is
|
||||
the name of the tensor and the value is the tensor itself.
|
||||
|
||||
:param dataset: The dataset that will be wrapped by this NamedDataLoader
|
||||
:type dataset: Dataset
|
||||
:param sampler: The sampler used by the NamedDataLoader to choose which samples to include in the batch
|
||||
:type sampler: Sampler
|
||||
:param batch_size: The size of the batch to be returned by the NamedDataLoader
|
||||
:type batch_size: int
|
||||
:param tensor_names: The names of the tensor, in the order that the dataset returns them in.
|
||||
:type tensor_names: list
|
||||
:param num_workers: number of workers to use for the DataLoader
|
||||
:type num_workers: int
|
||||
:param pin_memory: argument for Data Loader to use page-locked memory for faster transfer of data to GPU
|
||||
:type pin_memory: bool
|
||||
"""
|
||||
|
||||
def collate_fn(batch):
|
||||
@@ -38,11 +43,10 @@ class NamedDataLoader(DataLoader):
|
||||
if type(batch[0]) == list:
|
||||
batch = batch[0]
|
||||
|
||||
assert len(batch[0]) == len(
|
||||
_tensor_names
|
||||
), "Dataset contains {} tensors while there are {} tensor names supplied: {}".format(
|
||||
len(batch[0]), len(_tensor_names), _tensor_names
|
||||
)
|
||||
if len(batch[0]) != len(_tensor_names):
|
||||
raise ModelingError(
|
||||
f"Dataset contains {len(batch[0])} tensors while there are {len(_tensor_names)} tensor names supplied: {_tensor_names}"
|
||||
)
|
||||
lists_temp = [[] for _ in range(len(_tensor_names))]
|
||||
ret = dict(zip(_tensor_names, lists_temp))
|
||||
|
||||
@@ -55,7 +59,7 @@ class NamedDataLoader(DataLoader):
|
||||
|
||||
return ret
|
||||
|
||||
super(NamedDataLoader, self).__init__(
|
||||
super().__init__(
|
||||
dataset=dataset,
|
||||
sampler=sampler,
|
||||
batch_size=batch_size,
|
||||
@@ -71,19 +75,3 @@ class NamedDataLoader(DataLoader):
|
||||
return num_batches
|
||||
else:
|
||||
return super().__len__()
|
||||
|
||||
|
||||
def covert_dataset_to_dataloader(dataset, sampler, batch_size):
|
||||
"""
|
||||
Wraps a PyTorch Dataset with a DataLoader.
|
||||
|
||||
:param dataset: Dataset to be wrapped.
|
||||
:type dataset: Dataset
|
||||
:param sampler: PyTorch sampler used to pick samples in a batch.
|
||||
:type sampler: Sampler
|
||||
:param batch_size: Number of samples in the batch.
|
||||
:return: A DataLoader that wraps the input Dataset.
|
||||
"""
|
||||
sampler_initialized = sampler(dataset)
|
||||
data_loader = DataLoader(dataset, sampler=sampler_initialized, batch_size=batch_size)
|
||||
return data_loader
|
||||
|
||||
@@ -19,10 +19,9 @@ import requests
|
||||
from tqdm import tqdm
|
||||
from torch.utils.data import TensorDataset
|
||||
import transformers
|
||||
from transformers import PreTrainedTokenizer
|
||||
from transformers import PreTrainedTokenizer, AutoTokenizer
|
||||
|
||||
from haystack.modeling.model.tokenization import (
|
||||
get_tokenizer,
|
||||
from haystack.modeling.model.feature_extraction import (
|
||||
tokenize_batch_question_answering,
|
||||
tokenize_with_metadata,
|
||||
truncate_sequences,
|
||||
@@ -180,9 +179,11 @@ class Processor(ABC):
|
||||
"Loading tokenizer from deprecated config. "
|
||||
"If you used `custom_vocab` or `never_split_chars`, this won't work anymore."
|
||||
)
|
||||
tokenizer = get_tokenizer(load_dir, tokenizer_class=config["tokenizer"], do_lower_case=config["lower_case"])
|
||||
tokenizer = AutoTokenizer.from_pretrained(
|
||||
load_dir, tokenizer_class=config["tokenizer"], do_lower_case=config["lower_case"]
|
||||
)
|
||||
else:
|
||||
tokenizer = get_tokenizer(load_dir, tokenizer_class=config["tokenizer"])
|
||||
tokenizer = AutoTokenizer.from_pretrained(load_dir, tokenizer_class=config["tokenizer"])
|
||||
|
||||
# we have to delete the tokenizer string from config, because we pass it as Object
|
||||
del config["tokenizer"]
|
||||
@@ -218,7 +219,7 @@ class Processor(ABC):
|
||||
**kwargs,
|
||||
):
|
||||
tokenizer_args = tokenizer_args or {}
|
||||
tokenizer = get_tokenizer(
|
||||
tokenizer = AutoTokenizer.from_pretrained(
|
||||
tokenizer_name_or_path,
|
||||
tokenizer_class=tokenizer_class,
|
||||
use_fast=use_fast,
|
||||
@@ -337,17 +338,7 @@ class Processor(ABC):
|
||||
|
||||
:return: True if all the samples in the basket has computed its features, False otherwise
|
||||
"""
|
||||
if basket.samples is None:
|
||||
return False
|
||||
elif len(basket.samples) == 0:
|
||||
return False
|
||||
if basket.samples is None:
|
||||
return False
|
||||
else:
|
||||
for sample in basket.samples:
|
||||
if sample.features is None:
|
||||
return False
|
||||
return True
|
||||
return basket.samples and not any(sample.features is None for sample in basket.samples)
|
||||
|
||||
def _log_samples(self, n_samples: int, baskets: List[SampleBasket]):
|
||||
logger.debug("*** Show %s random examples ***", n_samples)
|
||||
@@ -1350,9 +1341,15 @@ class TableTextSimilarityProcessor(Processor):
|
||||
processor_config_file = Path(load_dir) / "processor_config.json"
|
||||
config = json.load(open(processor_config_file))
|
||||
# init tokenizer
|
||||
query_tokenizer = get_tokenizer(load_dir, tokenizer_class=config["query_tokenizer"], subfolder="query")
|
||||
passage_tokenizer = get_tokenizer(load_dir, tokenizer_class=config["passage_tokenizer"], subfolder="passage")
|
||||
table_tokenizer = get_tokenizer(load_dir, tokenizer_class=config["table_tokenizer"], subfolder="table")
|
||||
query_tokenizer = AutoTokenizer.from_pretrained(
|
||||
load_dir, tokenizer_class=config["query_tokenizer"], subfolder="query"
|
||||
)
|
||||
passage_tokenizer = AutoTokenizer.from_pretrained(
|
||||
load_dir, tokenizer_class=config["passage_tokenizer"], subfolder="passage"
|
||||
)
|
||||
table_tokenizer = AutoTokenizer.from_pretrained(
|
||||
load_dir, tokenizer_class=config["table_tokenizer"], subfolder="table"
|
||||
)
|
||||
|
||||
# we have to delete the tokenizer string from config, because we pass it as Object
|
||||
del config["query_tokenizer"]
|
||||
@@ -1978,7 +1975,7 @@ class InferenceProcessor(TextClassificationProcessor):
|
||||
processor_config_file = Path(load_dir) / "processor_config.json"
|
||||
config = json.load(open(processor_config_file))
|
||||
# init tokenizer
|
||||
tokenizer = get_tokenizer(load_dir, tokenizer_class=config["tokenizer"])
|
||||
tokenizer = AutoTokenizer.from_pretrained(load_dir, tokenizer_class=config["tokenizer"])
|
||||
# we have to delete the tokenizer string from config, because we pass it as Object
|
||||
del config["tokenizer"]
|
||||
|
||||
@@ -1999,37 +1996,6 @@ class InferenceProcessor(TextClassificationProcessor):
|
||||
ret: Dict = {}
|
||||
return ret
|
||||
|
||||
def dataset_from_dicts(
|
||||
self, dicts: List[Dict], indices: List[int] = [], return_baskets: bool = False, debug: bool = False
|
||||
):
|
||||
"""
|
||||
Function to convert input dictionaries containing text into a torch dataset.
|
||||
For normal operation with Language Models it calls the superclass' TextClassification.dataset_from_dicts method.
|
||||
For slow tokenizers, s3e or wordembedding tokenizers the function works on _dict_to_samples and _sample_to_features
|
||||
"""
|
||||
# TODO remove this sections once tokenizers work the same way for slow/fast and our special tokenizers
|
||||
if not self.tokenizer.is_fast:
|
||||
self.baskets = []
|
||||
for d in dicts:
|
||||
sample = self._dict_to_samples(dictionary=d)
|
||||
features = self._sample_to_features(sample)
|
||||
sample.features = features
|
||||
basket = SampleBasket(id_internal=None, raw=d, id_external=None, samples=[sample])
|
||||
self.baskets.append(basket)
|
||||
if indices and 0 not in indices:
|
||||
pass
|
||||
else:
|
||||
self._log_samples(n_samples=1, baskets=self.baskets)
|
||||
|
||||
problematic_ids: set = set()
|
||||
dataset, tensornames = self._create_dataset()
|
||||
ret = [dataset, tensornames, problematic_ids]
|
||||
if return_baskets:
|
||||
ret.append(self.baskets)
|
||||
return ret
|
||||
else:
|
||||
return super().dataset_from_dicts(dicts=dicts, indices=indices, return_baskets=return_baskets, debug=debug)
|
||||
|
||||
# Private method to keep s3e pooling and embedding extraction working
|
||||
def _dict_to_samples(self, dictionary: Dict, **kwargs) -> Sample:
|
||||
# this tokenization also stores offsets
|
||||
|
||||
+107
-36
@@ -16,9 +16,18 @@
|
||||
from typing import Dict, Any, Union, Tuple, Optional, List
|
||||
|
||||
import re
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
from transformers import AutoTokenizer, PreTrainedTokenizer, RobertaTokenizer
|
||||
import transformers
|
||||
from transformers import PreTrainedTokenizer, RobertaTokenizer, AutoConfig, AutoFeatureExtractor, AutoTokenizer
|
||||
|
||||
# NOTE: These two constants are internals of HF. Keep in mind that they might be renamed or removed at any time.
|
||||
from transformers.models.auto.feature_extraction_auto import FEATURE_EXTRACTOR_MAPPING_NAMES
|
||||
from transformers.models.auto.tokenization_auto import TOKENIZER_MAPPING_NAMES
|
||||
|
||||
from haystack.errors import ModelingError
|
||||
from haystack.modeling.data_handler.samples import SampleBasket
|
||||
@@ -31,45 +40,103 @@ logger = logging.getLogger(__name__)
|
||||
SPECIAL_TOKENIZER_CHARS = r"^(##|Ġ|▁)"
|
||||
|
||||
|
||||
def get_tokenizer(
|
||||
pretrained_model_name_or_path: str,
|
||||
revision: str = None,
|
||||
use_fast: bool = True,
|
||||
use_auth_token: Optional[Union[str, bool]] = None,
|
||||
**kwargs,
|
||||
) -> PreTrainedTokenizer:
|
||||
"""
|
||||
Enables loading of different Tokenizer classes with a uniform interface.
|
||||
Right now it always returns an instance of `AutoTokenizer`.
|
||||
FEATURE_EXTRACTORS = {
|
||||
**{key: AutoTokenizer for key in TOKENIZER_MAPPING_NAMES.keys()},
|
||||
**{key: AutoFeatureExtractor for key in FEATURE_EXTRACTOR_MAPPING_NAMES.keys()},
|
||||
}
|
||||
|
||||
:param pretrained_model_name_or_path: The path of the saved pretrained model or its name (e.g. `bert-base-uncased`)
|
||||
:param revision: The version of model to use from the HuggingFace model hub. Can be tag name, branch name, or commit hash.
|
||||
:param use_fast: Indicate if Haystack should try to load the fast version of the tokenizer (True) or use the Python one (False). Defaults to True.
|
||||
:param use_auth_token: The API token used to download private models from Huggingface.
|
||||
If this parameter is set to `True`, then the token generated when running
|
||||
`transformers-cli login` (stored in ~/.huggingface) will be used.
|
||||
Additional information can be found here
|
||||
https://huggingface.co/transformers/main_classes/model.html#transformers.PreTrainedModel.from_pretrained
|
||||
:param kwargs: other kwargs to pass on to `PretrainedTokenizer.from_pretrained()`
|
||||
:return: AutoTokenizer instance
|
||||
"""
|
||||
model_name_or_path = str(pretrained_model_name_or_path)
|
||||
|
||||
if "mlm" in model_name_or_path.lower():
|
||||
logging.error("MLM part of codebert is currently not supported in Haystack. Proceed at your own risk.")
|
||||
DEFAULT_EXTRACTION_PARAMS = {
|
||||
AutoTokenizer: {
|
||||
"max_length": 256,
|
||||
"add_special_tokens": True,
|
||||
"truncation": True,
|
||||
"truncation_strategy": "longest_first",
|
||||
"padding": "max_length",
|
||||
"return_token_type_ids": True,
|
||||
},
|
||||
AutoFeatureExtractor: {"return_tensors": "pt"},
|
||||
}
|
||||
|
||||
params = {}
|
||||
if any(tokenizer_type in model_name_or_path for tokenizer_type in ["albert", "xlnet"]):
|
||||
params["keep_accents"] = True
|
||||
|
||||
return AutoTokenizer.from_pretrained(
|
||||
pretrained_model_name_or_path=model_name_or_path,
|
||||
revision=revision,
|
||||
use_fast=use_fast,
|
||||
use_auth_token=use_auth_token,
|
||||
**params,
|
||||
class FeatureExtractor:
|
||||
def __init__(
|
||||
self,
|
||||
pretrained_model_name_or_path: Union[str, Path],
|
||||
revision: str = None,
|
||||
use_fast: bool = True,
|
||||
use_auth_token: Optional[Union[str, bool]] = None,
|
||||
**kwargs,
|
||||
)
|
||||
):
|
||||
"""
|
||||
Enables loading of different feature extractors, including tokenizers, with a uniform interface.
|
||||
|
||||
Use `FeatureExtractor.extract_features()` to convert your input queries, documents, images, and tables
|
||||
into vectors that you can pass to the language model.
|
||||
|
||||
:param pretrained_model_name_or_path: The path of the saved pretrained model or its name (for example, `bert-base-uncased`)
|
||||
:param revision: The version of the model to use from the Hugging Face model hub. It can be tag name, branch name, or commit hash.
|
||||
:param use_fast: Indicate if Haystack should try to load the fast version of the tokenizer (True) or use the Python one (False). Defaults to True.
|
||||
:param use_auth_token: The API token used to download private models from Hugging Face.
|
||||
If this parameter is set to `True`, then the token generated when running
|
||||
`transformers-cli login` (stored in ~/.huggingface) is used.
|
||||
For more information, see
|
||||
[Hugging Face documentation](https://huggingface.co/transformers/main_classes/model.html#transformers.PreTrainedModel.from_pretrained)
|
||||
:param kwargs: Other kwargs you want to pass on to `PretrainedTokenizer.from_pretrained()`
|
||||
"""
|
||||
model_name_or_path = str(pretrained_model_name_or_path)
|
||||
model_type = None
|
||||
|
||||
config_file = Path(pretrained_model_name_or_path) / "tokenizer_config.json"
|
||||
if os.path.exists(config_file):
|
||||
# it's a local directory
|
||||
config = json.load(open(config_file))
|
||||
feature_extractor_classname = config["tokenizer_class"]
|
||||
logger.debug(f"⛏️ Selected feature extractor: {feature_extractor_classname} (from {config_file})")
|
||||
# Use FastTokenizers as much as possible
|
||||
try:
|
||||
feature_extractor_class = getattr(transformers, feature_extractor_classname + "Fast")
|
||||
logger.debug(
|
||||
"Fast version of this tokenizer exists. Loaded class: %s",
|
||||
feature_extractor_class.__class__.__name__,
|
||||
)
|
||||
except AttributeError:
|
||||
logger.debug("Fast version could not be loaded. Falling back to base version.")
|
||||
feature_extractor_class = getattr(transformers, feature_extractor_classname)
|
||||
|
||||
else:
|
||||
# it's a HF Hub identifier
|
||||
config = AutoConfig.from_pretrained(
|
||||
pretrained_model_name_or_path=model_name_or_path, use_auth_token=use_auth_token, revision=revision
|
||||
)
|
||||
model_type = config.model_type
|
||||
try:
|
||||
feature_extractor_class = FEATURE_EXTRACTORS[model_type]
|
||||
except KeyError as e:
|
||||
raise ModelingError(
|
||||
f"'{pretrained_model_name_or_path}' has no known feature extractor. "
|
||||
"Haystack can assign tokenizers to the following model types: "
|
||||
# Using chr(10) instead of \n due to f-string limitation
|
||||
# https://peps.python.org/pep-0498/#specification: "Backslashes may not appear anywhere within expressions"
|
||||
f"\n- {f'{chr(10)}- '.join(FEATURE_EXTRACTORS.keys())}"
|
||||
) from e
|
||||
logger.debug(
|
||||
f"⛏️ Selected feature extractor: {feature_extractor_class.__name__} (for model type '{model_type}')"
|
||||
)
|
||||
|
||||
self.default_params = DEFAULT_EXTRACTION_PARAMS.get(feature_extractor_class, {})
|
||||
|
||||
self.feature_extractor = feature_extractor_class.from_pretrained(
|
||||
pretrained_model_name_or_path=model_name_or_path,
|
||||
revision=revision,
|
||||
use_fast=use_fast,
|
||||
use_auth_token=use_auth_token,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def __call__(self, **kwargs):
|
||||
params = {**self.default_params, **(kwargs or {})}
|
||||
return self.feature_extractor(**params)
|
||||
|
||||
|
||||
def tokenize_batch_question_answering(
|
||||
@@ -101,7 +168,11 @@ def tokenize_batch_question_answering(
|
||||
# # Tokenize texts in batch mode
|
||||
texts = [d["context"] for d in pre_baskets]
|
||||
tokenized_docs_batch = tokenizer(
|
||||
texts, return_offsets_mapping=True, return_special_tokens_mask=True, add_special_tokens=False, verbose=False
|
||||
text=texts,
|
||||
return_offsets_mapping=True,
|
||||
return_special_tokens_mask=True,
|
||||
add_special_tokens=False,
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
# Extract relevant data
|
||||
@@ -13,9 +13,9 @@
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""
|
||||
"""
|
||||
Acknowledgements: Many of the modeling parts here come from the great transformers repository: https://github.com/huggingface/transformers.
|
||||
Thanks for the great work!
|
||||
Thanks for the great work!
|
||||
"""
|
||||
|
||||
from typing import Type, Optional, Dict, Any, Union, List
|
||||
@@ -26,7 +26,6 @@ import logging
|
||||
import os
|
||||
from abc import ABC, abstractmethod
|
||||
from pathlib import Path
|
||||
from functools import wraps
|
||||
import numpy as np
|
||||
import torch
|
||||
from torch import nn
|
||||
@@ -36,6 +35,7 @@ from transformers import AutoModel, AutoConfig
|
||||
from transformers.modeling_utils import SequenceSummary
|
||||
|
||||
from haystack.errors import ModelingError
|
||||
from haystack.modeling.utils import silence_transformers_logs
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -58,30 +58,6 @@ LANGUAGE_HINTS = (
|
||||
OUTPUT_DIM_NAMES = ["dim", "hidden_size", "d_model"]
|
||||
|
||||
|
||||
def silence_transformers_logs(from_pretrained_func):
|
||||
"""
|
||||
A wrapper that raises the log level of Transformers to
|
||||
ERROR to hide some unnecessary warnings.
|
||||
"""
|
||||
|
||||
@wraps(from_pretrained_func)
|
||||
def quiet_from_pretrained_func(cls, *args, **kwargs):
|
||||
|
||||
# Raise the log level of Transformers
|
||||
t_logger = logging.getLogger("transformers")
|
||||
original_log_level = t_logger.level
|
||||
t_logger.setLevel(logging.ERROR)
|
||||
|
||||
result = from_pretrained_func(cls, *args, **kwargs)
|
||||
|
||||
# Restore the log level
|
||||
t_logger.setLevel(original_log_level)
|
||||
|
||||
return result
|
||||
|
||||
return quiet_from_pretrained_func
|
||||
|
||||
|
||||
# TODO analyse if LMs can be completely used through HF transformers
|
||||
class LanguageModel(nn.Module, ABC):
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
from typing import Optional, Union, Dict, Any, Type, List
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from transformers import AutoConfig
|
||||
import torch
|
||||
from huggingface_hub import hf_hub_download
|
||||
|
||||
from haystack.modeling.model.multimodal.base import HaystackModel
|
||||
from haystack.modeling.model.multimodal.sentence_transformers import HaystackSentenceTransformerModel
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
#: Match the name of the HuggingFace Model class to the corresponding Haystack wrapper
|
||||
HUGGINGFACE_TO_HAYSTACK: Dict[str, Type[HaystackModel]] = {
|
||||
"CLIP": HaystackSentenceTransformerModel,
|
||||
"MPNet": HaystackSentenceTransformerModel,
|
||||
"DPRContextEncoder": HaystackSentenceTransformerModel,
|
||||
"DPRQuestionEncoder": HaystackSentenceTransformerModel,
|
||||
}
|
||||
|
||||
|
||||
#: HF Capitalization pairs. Contains alternative capitalizations.
|
||||
HUGGINGFACE_CAPITALIZE = {
|
||||
"big-bird": "BigBird",
|
||||
"deberta-v2": "DebertaV2",
|
||||
"xlm-roberta": "XLMRoberta",
|
||||
**{k.lower(): k for k in HUGGINGFACE_TO_HAYSTACK.keys()},
|
||||
}
|
||||
|
||||
|
||||
def get_model(
|
||||
pretrained_model_name_or_path: Union[Path, str],
|
||||
content_type: str, # change to ContentTypes starting Python3.8
|
||||
devices: Optional[List[torch.device]] = None,
|
||||
autoconfig_kwargs: Optional[Dict[str, Any]] = None,
|
||||
model_kwargs: Optional[Dict[str, Any]] = None,
|
||||
feature_extractor_kwargs: Optional[Dict[str, Any]] = None,
|
||||
pooler_kwargs: Optional[Dict[str, Any]] = None,
|
||||
) -> HaystackModel:
|
||||
"""
|
||||
Load a pretrained language model by specifying its name and either downloading the model from the Hugging Face hub
|
||||
(if it's given a model identifier from Hugging Face Hub) or loading it from disk (if it's given a local path).
|
||||
|
||||
For all supported model variations, see [Models](https://huggingface.co/models).
|
||||
The appropriate language model class is inferred automatically from the model's configuration or its name.
|
||||
|
||||
:param pretrained_model_name_or_path: The path of the saved pretrained model or its name.
|
||||
:param content_type: The type (text, image, ...) of content the model should handle.
|
||||
:param autoconfig_kwargs: Additional keyword arguments to pass to AutoConfig, like the revision or the auth key.
|
||||
:param model_kwargs: Additional keyword arguments to pass to the language model constructor.
|
||||
Haystack applies some default parameters to some models. You can override them by specifying the
|
||||
desired value in this parameter. See `DEFAULT_MODEL_PARAMS`.
|
||||
:param feature_extractor_kwargs: A dictionary of parameters to pass to the feature extractor's initialization (revision, use_auth_key, etc...)
|
||||
Haystack applies some default parameters to some models. You can override them by specifying the
|
||||
desired value in this parameter. See `DEFAULT_MODEL_PARAMS`.
|
||||
:param pooler_kwargs: A dictionary of parameters to pass to the pooler's initialization (summary_last_dropout, summary_activation, etc...)
|
||||
Haystack applies some default parameters to some models. You can override them by specifying the
|
||||
desired value in this parameter. See `POOLER_PARAMETERS`.
|
||||
"""
|
||||
autoconfig_kwargs = autoconfig_kwargs or {}
|
||||
model_kwargs = model_kwargs or {}
|
||||
feature_extractor_kwargs = feature_extractor_kwargs or {}
|
||||
pooler_kwargs = pooler_kwargs or {}
|
||||
|
||||
if not pretrained_model_name_or_path or not isinstance(pretrained_model_name_or_path, (str, Path)):
|
||||
raise ValueError(
|
||||
f"{pretrained_model_name_or_path} is not a valid 'pretrained_model_name_or_path' value. "
|
||||
"Please provide a string or a Path object."
|
||||
)
|
||||
model_name = str(pretrained_model_name_or_path)
|
||||
model_type: Optional[str] = ""
|
||||
model_wrapper_class: Type[HaystackModel]
|
||||
|
||||
# Prepare the kwargs the model wrapper expects (see each wrapper's init for details)
|
||||
wrapper_kwarg_groups = {}
|
||||
wrapper_kwarg_groups["model_kwargs"] = model_kwargs
|
||||
|
||||
# SentenceTransformers are much faster, so use them whenever possible
|
||||
if _is_sentence_transformers_model(
|
||||
pretrained_model_name_or_path, use_auth_token=autoconfig_kwargs.get("use_auth_token", False)
|
||||
):
|
||||
model_wrapper_class = HaystackSentenceTransformerModel
|
||||
try:
|
||||
# Use AutoConfig to log some more info about the model class
|
||||
config = AutoConfig.from_pretrained(pretrained_model_name_or_path=model_name, **autoconfig_kwargs)
|
||||
model_type = config.model_type
|
||||
except Exception as e:
|
||||
logger.debug(f"Can't find model type for {pretrained_model_name_or_path}: {e}")
|
||||
|
||||
if feature_extractor_kwargs is not None:
|
||||
logger.debug(
|
||||
"Can't forward feature_extractor_kwargs to a SentenceTransformers model. "
|
||||
"These kwargs are being dropped. "
|
||||
f"Content of feature_extractor_kwargs: {feature_extractor_kwargs}"
|
||||
)
|
||||
|
||||
else:
|
||||
# Use AutoConfig to understand the model class
|
||||
config = AutoConfig.from_pretrained(pretrained_model_name_or_path=model_name, **autoconfig_kwargs)
|
||||
if not config.model_type:
|
||||
logger.error(
|
||||
f"Model type not understood for '{pretrained_model_name_or_path}'. Please provide the name of "
|
||||
"a model that can be downloaded from the Model Hub.\nUsing the AutoModel class. "
|
||||
"THIS CAN CAUSE CRASHES and won't work for models that are not working with text."
|
||||
)
|
||||
model_type = None
|
||||
else:
|
||||
try:
|
||||
model_type = HUGGINGFACE_CAPITALIZE[config.model_type.lower()]
|
||||
except KeyError as e:
|
||||
logger.error(
|
||||
f"Haystack doesn't support model '{pretrained_model_name_or_path}' (type '{config.model_type.lower()}') "
|
||||
"We'll use the AutoModel class for it. "
|
||||
"THIS CAN CAUSE CRASHES and won't work for models that are not working with text. "
|
||||
f"Supported model types: {', '.join(HUGGINGFACE_CAPITALIZE.keys())}"
|
||||
)
|
||||
model_type = None
|
||||
|
||||
# Find the HF class corresponding to this model type
|
||||
try:
|
||||
model_wrapper_class = HUGGINGFACE_TO_HAYSTACK[model_type or "AutoModel"]
|
||||
except KeyError as e:
|
||||
raise ValueError(
|
||||
f"The type of the given model (name/path: {pretrained_model_name_or_path}, detected type: {model_type or config.model_type}) "
|
||||
"is not supported by Haystack or was not correctly identified. Please use supported models only. "
|
||||
f"Supported model types: {', '.join(HUGGINGFACE_TO_HAYSTACK.keys())}"
|
||||
) from e
|
||||
|
||||
if feature_extractor_kwargs:
|
||||
wrapper_kwarg_groups["feature_extractor_kwargs"] = feature_extractor_kwargs
|
||||
if pooler_kwargs:
|
||||
wrapper_kwarg_groups["pooler_kwargs"] = pooler_kwargs
|
||||
|
||||
# Instantiate the model's wrapper
|
||||
model_wrapper = model_wrapper_class(
|
||||
pretrained_model_name_or_path=pretrained_model_name_or_path,
|
||||
model_type=model_type,
|
||||
content_type=content_type,
|
||||
**wrapper_kwarg_groups,
|
||||
)
|
||||
model_wrapper.to(devices)
|
||||
|
||||
return model_wrapper
|
||||
|
||||
|
||||
def _is_sentence_transformers_model(pretrained_model_name_or_path: Union[Path, str], use_auth_token: Union[bool, str]):
|
||||
|
||||
# Check if sentence transformers config file is in local path
|
||||
if Path(pretrained_model_name_or_path).exists():
|
||||
if (Path(pretrained_model_name_or_path) / "config_sentence_transformers.json").exists():
|
||||
return True
|
||||
|
||||
# Check if sentence transformers config file is in model hub
|
||||
try:
|
||||
hf_hub_download(
|
||||
repo_id=str(pretrained_model_name_or_path),
|
||||
filename="config_sentence_transformers.json",
|
||||
use_auth_token=use_auth_token,
|
||||
)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.debug("%s not found in model hub: an error occurred. Error: %s", pretrained_model_name_or_path, e)
|
||||
|
||||
# Pattern matching the name as a last resort
|
||||
if str(pretrained_model_name_or_path).startswith("sentence-transformers"):
|
||||
logger.debug(
|
||||
"The model name starts with 'sentence-transformers': assuming this is a Sentence Transformers model."
|
||||
)
|
||||
return True
|
||||
|
||||
logger.debug(
|
||||
"The model name doesn't start with 'sentence-transformers': assuming this is NOT a Sentence Transformers model."
|
||||
)
|
||||
return False
|
||||
@@ -0,0 +1,56 @@
|
||||
from typing import Any, List, Union, Optional
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
import torch
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HaystackModel(ABC):
|
||||
"""
|
||||
The interface on top of HaystackTransformer and HaystackSentenceTransformer.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, pretrained_model_name_or_path: Union[str, Path], model_type: Optional[str], content_type: str
|
||||
): # replace the type of content_type with ContentTypes starting Python3.8
|
||||
"""
|
||||
:param pretrained_model_name_or_path: The name of the model to load
|
||||
:param model_type: the value of `model_type` from the model's `Config` class.
|
||||
:param content_type: The type of data (such as "text", "image" and so on) the model should process.
|
||||
See the values of `haystack.schema.ContentTypes`.
|
||||
"""
|
||||
logger.info(
|
||||
f" 🤖 Loading '{pretrained_model_name_or_path}' "
|
||||
f"({self.__class__.__name__} of type '{model_type if model_type else '<unknown>'}' "
|
||||
f"for {content_type} data)"
|
||||
)
|
||||
self.model_name_or_path = pretrained_model_name_or_path
|
||||
self.model_type = model_type
|
||||
self.content_type = content_type
|
||||
|
||||
@abstractmethod
|
||||
def encode(self, data: List[Any], **kwargs) -> torch.Tensor:
|
||||
"""
|
||||
Run the model on the input data to obtain output vectors.
|
||||
"""
|
||||
raise NotImplementedError("Abstract method, use a subclass.")
|
||||
|
||||
@abstractmethod
|
||||
def to(self, devices: Optional[List[torch.device]]) -> None:
|
||||
"""
|
||||
Send the model to the specified PyTorch device(s)
|
||||
"""
|
||||
raise NotImplementedError("Abstract method, use a subclass.")
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def embedding_dim(self) -> int:
|
||||
"""
|
||||
The output embedding size.
|
||||
"""
|
||||
raise NotImplementedError("Abstract method, use a subclass.")
|
||||
@@ -0,0 +1,92 @@
|
||||
from typing import Optional, Dict, Any, List, Union
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
from sentence_transformers import SentenceTransformer
|
||||
|
||||
from haystack.modeling.model.multimodal.base import HaystackModel
|
||||
from haystack.schema import ContentTypes
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HaystackSentenceTransformerModel(HaystackModel):
|
||||
"""
|
||||
Parent class for `sentence-transformers` models.
|
||||
|
||||
These models read raw data (text, image), internally extract features, and return vectors that capture the meaning
|
||||
of the original data.
|
||||
|
||||
Models inheriting from `HaystackSentenceTransformerModel` are designed to be used in parallel one with the other
|
||||
in multimodal retrieval settings, for example image retrieval from a text query, mixed table/text retrieval, and so on.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
pretrained_model_name_or_path: Union[str, Path],
|
||||
model_type: str,
|
||||
content_type: ContentTypes,
|
||||
model_kwargs: Optional[Dict[str, Any]] = None,
|
||||
):
|
||||
"""
|
||||
:param pretrained_model_name_or_path: The name of the model to load.
|
||||
:param model_type: the value of `model_type` from the model's `Config` class
|
||||
:param content_type: The type of data (such as "text", "image", and so on) the model should process.
|
||||
See the values of `haystack.schema.ContentTypes`.
|
||||
:param model_kwargs: A dictionary of parameters to pass to the model's initialization.
|
||||
(revision, use_auth_key, and so on)
|
||||
Haystack applies some default parameters to some models. You can override them by specifying the
|
||||
desired value in this parameter. See `DEFAULT_MODEL_PARAMS`.
|
||||
"""
|
||||
super().__init__(
|
||||
pretrained_model_name_or_path=pretrained_model_name_or_path,
|
||||
model_type=model_type,
|
||||
content_type=content_type,
|
||||
)
|
||||
try:
|
||||
self.model = SentenceTransformer(pretrained_model_name_or_path, **(model_kwargs or {}))
|
||||
except Exception as e:
|
||||
logger.exception(
|
||||
"Models of type '%s' like %s "
|
||||
"are only supported through sentence-transformers. Make sure this "
|
||||
"model is compatible with sentence-transformers or use an alternative, compatible "
|
||||
"implementation of this model.",
|
||||
model_type,
|
||||
pretrained_model_name_or_path,
|
||||
)
|
||||
|
||||
@property
|
||||
def embedding_dim(self) -> int:
|
||||
"""
|
||||
Finds out the output embedding dim by running the model on a minimal amount of mock data.
|
||||
"""
|
||||
emedding_dim = self.model.get_sentence_embedding_dimension()
|
||||
if not emedding_dim:
|
||||
logger.warning(
|
||||
"Can't find the output embedding dimensions for '%s'. Some checks will not run as intended.",
|
||||
self.model_name_or_path,
|
||||
)
|
||||
return emedding_dim
|
||||
|
||||
def to(self, devices: Optional[List[torch.device]]) -> None:
|
||||
"""
|
||||
Send the model to the specified PyTorch devices.
|
||||
"""
|
||||
if devices:
|
||||
if len(devices) > 1:
|
||||
self.model = nn.DataParallel(self.model, device_ids=devices)
|
||||
else:
|
||||
self.model.to(devices[0])
|
||||
|
||||
def encode(self, data: List[Any], **kwargs) -> torch.Tensor:
|
||||
"""
|
||||
Generate the tensors representing the input data.
|
||||
|
||||
Validates the inputs according to what the subclass declared in the `expected_inputs` property.
|
||||
Then passes the vectors to the `_forward()` method and returns its output untouched.
|
||||
"""
|
||||
return self.model.encode(data, convert_to_tensor=True, **kwargs)
|
||||
@@ -5,8 +5,10 @@ import os
|
||||
import pickle
|
||||
import random
|
||||
import signal
|
||||
from functools import wraps
|
||||
from copy import deepcopy
|
||||
from itertools import islice
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.distributed as dist
|
||||
@@ -28,6 +30,30 @@ class GracefulKiller:
|
||||
self.kill_now = True
|
||||
|
||||
|
||||
def silence_transformers_logs(from_pretrained_func):
|
||||
"""
|
||||
A wrapper that raises the log level of Transformers to
|
||||
ERROR to hide some unnecessary warnings.
|
||||
"""
|
||||
|
||||
@wraps(from_pretrained_func)
|
||||
def quiet_from_pretrained_func(cls, *args, **kwargs):
|
||||
|
||||
# Raise the log level of Transformers
|
||||
t_logger = logging.getLogger("transformers")
|
||||
original_log_level = t_logger.level
|
||||
t_logger.setLevel(logging.ERROR)
|
||||
|
||||
result = from_pretrained_func(cls, *args, **kwargs)
|
||||
|
||||
# Restore the log level
|
||||
t_logger.setLevel(original_log_level)
|
||||
|
||||
return result
|
||||
|
||||
return quiet_from_pretrained_func
|
||||
|
||||
|
||||
def set_all_seeds(seed: int, deterministic_cudnn: bool = False) -> None:
|
||||
"""
|
||||
Setting multiple seeds to make runs reproducible.
|
||||
|
||||
@@ -40,6 +40,7 @@ from haystack.nodes.retriever import (
|
||||
TfidfRetriever,
|
||||
Text2SparqlRetriever,
|
||||
TableTextRetriever,
|
||||
MultiModalRetriever,
|
||||
)
|
||||
from haystack.nodes.summarizer import BaseSummarizer, TransformersSummarizer
|
||||
from haystack.nodes.translator import BaseTranslator, TransformersTranslator
|
||||
|
||||
@@ -14,3 +14,4 @@ from haystack.nodes.retriever.sparse import (
|
||||
TfidfRetriever,
|
||||
)
|
||||
from haystack.nodes.retriever.text2sparql import Text2SparqlRetriever
|
||||
from haystack.nodes.retriever.multimodal import MultiModalRetriever
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
from haystack.nodes.retriever.multimodal.retriever import MultiModalRetriever
|
||||
from haystack.nodes.retriever.multimodal.embedder import MultiModalEmbedder
|
||||
@@ -0,0 +1,207 @@
|
||||
from typing import Union, Optional, Dict, List, Any
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
import torch
|
||||
from tqdm import tqdm
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
from haystack.modeling.model.multimodal import get_model
|
||||
from haystack.errors import NodeError, ModelingError
|
||||
from haystack.modeling.model.multimodal.base import HaystackModel
|
||||
from haystack.schema import Document
|
||||
from haystack.utils.torch_utils import get_devices
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MultiModalRetrieverError(NodeError):
|
||||
pass
|
||||
|
||||
|
||||
FilterType = Dict[str, Union[Dict[str, Any], List[Any], str, int, float, bool]]
|
||||
|
||||
|
||||
# TODO the keys should match with ContentTypes (currently 'audio' is missing)
|
||||
DOCUMENT_CONVERTERS = {
|
||||
# NOTE: Keep this '?' cleaning step, it needs to be double-checked for impact on the inference results.
|
||||
"text": lambda doc: doc.content[:-1] if doc.content[-1] == "?" else doc.content,
|
||||
"table": lambda doc: " ".join(
|
||||
doc.content.columns.tolist() + [cell for row in doc.content.values.tolist() for cell in row]
|
||||
),
|
||||
"image": lambda doc: Image.open(doc.content),
|
||||
}
|
||||
|
||||
CAN_EMBED_META = ["text", "table"]
|
||||
|
||||
|
||||
class MultiModalEmbedder:
|
||||
def __init__(
|
||||
self,
|
||||
embedding_models: Dict[str, Union[Path, str]], # replace str with ContentTypes starting from Python3.8
|
||||
feature_extractors_params: Dict[str, Dict[str, Any]] = None,
|
||||
batch_size: int = 16,
|
||||
embed_meta_fields: List[str] = ["name"],
|
||||
progress_bar: bool = True,
|
||||
devices: Optional[List[Union[str, torch.device]]] = None,
|
||||
use_auth_token: Optional[Union[str, bool]] = None,
|
||||
):
|
||||
"""
|
||||
Init the Retriever and all its models from a local or remote model checkpoint.
|
||||
The checkpoint format matches the Hugging Face transformers' model format.
|
||||
|
||||
:param embedding_models: A dictionary matching a local path or remote name of encoder checkpoint with
|
||||
the content type it should handle ("text", "table", "image", etc...).
|
||||
The format is the one that Hugging Face Hub models use.
|
||||
Expected input format: `{'text': 'name_or_path_to_text_model', 'image': 'name_or_path_to_image_model', ... }`
|
||||
Keep in mind that the models should output in the same embedding space for this retriever to work.
|
||||
:param feature_extractors_params: A dictionary matching a content type ("text", "table", "image" and so on) with the
|
||||
parameters of its own feature extractor if the model requires one.
|
||||
Expected input format: `{'text': {'param_name': 'param_value', ...}, 'image': {'param_name': 'param_value', ...}, ...}`
|
||||
:param batch_size: Number of questions or passages to encode at once. In case of multiple GPUs, this will be the total batch size.
|
||||
:param embed_meta_fields: Concatenate the provided meta fields and text passage / image to a text pair that is
|
||||
then used to create the embedding.
|
||||
This is the approach used in the original paper and is likely to improve
|
||||
performance if your titles contain meaningful information for retrieval
|
||||
(topic, entities etc.).
|
||||
:param progress_bar: Whether to show a tqdm progress bar or not.
|
||||
Can be helpful to disable in production deployments to keep the logs clean.
|
||||
:param devices: List of GPU (or CPU) devices to limit inference to certain GPUs and not use all available ones.
|
||||
These strings are converted into pytorch devices, so use the string notation described
|
||||
[in the pytorch documentation](https://pytorch.org/docs/simage/tensor_attributes.html?highlight=torch%20device#torch.torch.device)
|
||||
(for example, ["cuda:0"]). Note: as multi-GPU training is currently not implemented for TableTextRetriever,
|
||||
training only uses the first device provided in this list.
|
||||
:param use_auth_token: API token used to download private models from Hugging Face. If this parameter is set to `True`,
|
||||
the local token is used, which must be previously created using `transformer-cli login`.
|
||||
For more information, see [Hugging Face documentation](https://huggingface.co/transformers/main_classes/model.html#transformers.PreTrainedModel.from_pretrained)
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
self.devices = get_devices(devices)
|
||||
if batch_size < len(self.devices):
|
||||
logger.warning("Batch size is lower than the number of devices. Not all GPUs will be utilized.")
|
||||
|
||||
self.batch_size = batch_size
|
||||
self.progress_bar = progress_bar
|
||||
self.embed_meta_fields = embed_meta_fields
|
||||
|
||||
feature_extractors_params = {
|
||||
content_type: {"max_length": 256, **(feature_extractors_params or {}).get(content_type, {})}
|
||||
for content_type in ["text", "table", "image", "audio"] # FIXME get_args(ContentTypes) from Python3.8 on
|
||||
}
|
||||
|
||||
self.models: Dict[str, HaystackModel] = {} # replace str with ContentTypes starting from Python3.8
|
||||
for content_type, embedding_model in embedding_models.items():
|
||||
self.models[content_type] = get_model(
|
||||
pretrained_model_name_or_path=embedding_model,
|
||||
content_type=content_type,
|
||||
devices=self.devices,
|
||||
autoconfig_kwargs={"use_auth_token": use_auth_token},
|
||||
model_kwargs={"use_auth_token": use_auth_token},
|
||||
feature_extractor_kwargs=feature_extractors_params[content_type],
|
||||
)
|
||||
|
||||
# Check embedding sizes for models: they must all match
|
||||
if len(self.models) > 1:
|
||||
sizes = {model.embedding_dim for model in self.models.values()}
|
||||
if None in sizes:
|
||||
logger.warning(
|
||||
"Haystack could not find the output embedding dimensions for '%s'. "
|
||||
"Dimensions won't be checked before computing the embeddings.",
|
||||
", ".join(
|
||||
{str(model.model_name_or_path) for model in self.models.values() if model.embedding_dim is None}
|
||||
),
|
||||
)
|
||||
elif len(sizes) > 1:
|
||||
embedding_sizes: Dict[int, List[str]] = {}
|
||||
for model in self.models.values():
|
||||
embedding_sizes[model.embedding_dim] = embedding_sizes.get(model.embedding_dim, []) + [
|
||||
str(model.model_name_or_path)
|
||||
]
|
||||
raise ValueError(f"Not all models have the same embedding size: {embedding_sizes}")
|
||||
|
||||
def embed(self, documents: List[Document], batch_size: Optional[int] = None) -> np.ndarray:
|
||||
"""
|
||||
Create embeddings for a list of documents using the relevant encoder for their content type.
|
||||
|
||||
:param documents: Documents to embed.
|
||||
:return: Embeddings, one per document, in the form of a np.array
|
||||
"""
|
||||
batch_size = batch_size if batch_size is not None else self.batch_size
|
||||
|
||||
all_embeddings = []
|
||||
for batch_index in tqdm(
|
||||
iterable=range(0, len(documents), batch_size),
|
||||
unit=" Docs",
|
||||
desc=f"Create embeddings",
|
||||
position=1,
|
||||
leave=False,
|
||||
disable=not self.progress_bar,
|
||||
):
|
||||
docs_batch = documents[batch_index : batch_index + batch_size]
|
||||
data_by_type = self._docs_to_data(documents=docs_batch)
|
||||
|
||||
# Get output for each model
|
||||
outputs_by_type: Dict[str, torch.Tensor] = {} # replace str with ContentTypes starting Python3.8
|
||||
for data_type, data in data_by_type.items():
|
||||
|
||||
model = self.models.get(data_type)
|
||||
if not model:
|
||||
raise ModelingError(
|
||||
f"Some data of type {data_type} was passed, but no model capable of handling such data was "
|
||||
f"initialized. Initialized models: {', '.join(self.models.keys())}"
|
||||
)
|
||||
outputs_by_type[data_type] = model.encode(data=data)
|
||||
|
||||
# Check the output sizes
|
||||
embedding_sizes = [output.shape[-1] for output in outputs_by_type.values()]
|
||||
|
||||
if not all(embedding_size == embedding_sizes[0] for embedding_size in embedding_sizes):
|
||||
raise ModelingError(
|
||||
"Some of the models are using a different embedding size. They should all match. "
|
||||
f"Embedding sizes by model: "
|
||||
f"{ {name: output.shape[-1] for name, output in outputs_by_type.items()} }"
|
||||
)
|
||||
|
||||
# Combine the outputs in a single matrix
|
||||
outputs = torch.stack(list(outputs_by_type.values()))
|
||||
embeddings = outputs.view(-1, embedding_sizes[0])
|
||||
embeddings = embeddings.cpu()
|
||||
all_embeddings.append(embeddings)
|
||||
|
||||
return np.concatenate(all_embeddings)
|
||||
|
||||
def _docs_to_data(
|
||||
self, documents: List[Document]
|
||||
) -> Dict[str, List[Any]]: # FIXME replace str to ContentTypes from Python3.8
|
||||
"""
|
||||
Extract the data to embed from each document and return them classified by content type.
|
||||
|
||||
:param documents: The documents to prepare fur multimodal embedding.
|
||||
:return: A dictionary containing one key for each content type, and a list of data extracted
|
||||
from each document, ready to be passed to the feature extractor (for example the content
|
||||
of a text document, a linearized table, a PIL image object, and so on)
|
||||
"""
|
||||
docs_data: Dict[str, List[Any]] = { # FIXME replace str to ContentTypes from Python3.8
|
||||
key: [] for key in ["text", "table", "image", "audio"]
|
||||
} # FIXME get_args(ContentTypes) from Python3.8 on
|
||||
for doc in documents:
|
||||
try:
|
||||
document_converter = DOCUMENT_CONVERTERS[doc.content_type]
|
||||
except KeyError as e:
|
||||
raise MultiModalRetrieverError(
|
||||
f"Unknown content type '{doc.content_type}'. Known types: 'text', 'table', 'image'." # FIXME {', '.join(get_args(ContentTypes))}" from Python3.8 on
|
||||
) from e
|
||||
|
||||
data = document_converter(doc)
|
||||
|
||||
if doc.content_type in CAN_EMBED_META:
|
||||
meta = [v for k, v in (doc.meta or {}).items() if k in self.embed_meta_fields]
|
||||
data = f"{' '.join(meta)} {data}" if meta else data
|
||||
|
||||
docs_data[doc.content_type].append(data)
|
||||
|
||||
return {key: values for key, values in docs_data.items() if values}
|
||||
@@ -0,0 +1,215 @@
|
||||
from typing import Union, Optional, Dict, List, Any
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
import torch
|
||||
import numpy as np
|
||||
|
||||
from haystack.nodes.retriever import BaseRetriever
|
||||
from haystack.document_stores import BaseDocumentStore
|
||||
from haystack.schema import ContentTypes, Document
|
||||
from haystack.nodes.retriever.multimodal.embedder import MultiModalEmbedder, MultiModalRetrieverError
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
FilterType = Optional[Dict[str, Union[Dict[str, Any], List[Any], str, int, float, bool]]]
|
||||
|
||||
|
||||
class MultiModalRetriever(BaseRetriever):
|
||||
def __init__(
|
||||
self,
|
||||
document_store: BaseDocumentStore,
|
||||
query_embedding_model: Union[Path, str],
|
||||
document_embedding_models: Dict[str, Union[Path, str]], # Replace str with ContentTypes starting Python3.8
|
||||
query_type: str = "text", # Replace str with ContentTypes starting Python3.8
|
||||
query_feature_extractor_params: Dict[str, Any] = {"max_length": 64},
|
||||
document_feature_extractors_params: Dict[str, Dict[str, Any]] = {"text": {"max_length": 256}},
|
||||
top_k: int = 10,
|
||||
batch_size: int = 16,
|
||||
embed_meta_fields: List[str] = ["name"],
|
||||
similarity_function: str = "dot_product",
|
||||
progress_bar: bool = True,
|
||||
devices: Optional[List[Union[str, torch.device]]] = None,
|
||||
use_auth_token: Optional[Union[str, bool]] = None,
|
||||
scale_score: bool = True,
|
||||
):
|
||||
"""
|
||||
Retriever that uses a multiple encoder to jointly retrieve among a database consisting of different
|
||||
data types. See the original paper for more details:
|
||||
Kostić, Bogdan, et al. (2021): "Multi-modal Retrieval of Tables and Texts Using Tri-encoder Models"
|
||||
(https://arxiv.org/abs/2108.04049),
|
||||
|
||||
:param document_store: An instance of DocumentStore from which to retrieve documents.
|
||||
:param query_embedding_model: Local path or remote name of question encoder checkpoint. The format equals the
|
||||
one used by Hugging Face transformers' modelhub models.
|
||||
:param document_embedding_models: Dictionary matching a local path or remote name of document encoder
|
||||
checkpoint with the content type it should handle ("text", "table", "image", and so on).
|
||||
The format equals the one used by Hugging Face transformers' modelhub models.
|
||||
:param query_type: The content type of the query ("text", "image" and so on)
|
||||
:param query_feature_extraction_params: The parameters to pass to the feature extractor of the query.
|
||||
:param document_feature_extraction_params: The parameters to pass to the feature extractor of the documents.
|
||||
:param top_k: How many documents to return per query.
|
||||
:param batch_size: Number of questions or documents to encode at once. In case of multiple GPUs, this will be
|
||||
the total batch size.
|
||||
:param embed_meta_fields: Concatenate the provided meta fields to a (text) pair that is then used to create
|
||||
the embedding. This is likely to improve performance if your titles contain meaningful information
|
||||
for retrieval (topic, entities, and so on). Note that only text and table documents support this feature.
|
||||
:param similarity_function: Which function to apply for calculating the similarity of query and document
|
||||
embeddings during training. Options: `dot_product` (default) or `cosine`.
|
||||
:param progress_bar: Whether to show a tqdm progress bar or not.
|
||||
Can be helpful to disable in production deployments to keep the logs clean.
|
||||
:param devices: List of GPU (or CPU) devices to limit inference to certain GPUs and not use all available ones.
|
||||
These strings will be converted into pytorch devices, so use the string notation described in [Tensor Attributes]
|
||||
(https://pytorch.org/docs/simage/tensor_attributes.html?highlight=torch%20device#torch.torch.device)
|
||||
(e.g. ["cuda:0"]). Note: As multi-GPU training is currently not implemented for TableTextRetriever,
|
||||
training only uses the first device provided in this list.
|
||||
:param use_auth_token: API token used to download private models from Hugging Face. If this parameter is set
|
||||
to `True`, the local token is used, which must be previously created using `transformer-cli login`.
|
||||
For more information, see
|
||||
[Hugging Face documentation](https://huggingface.co/transformers/main_classes/model.html#transformers.PreTrainedModel.from_pretrained)
|
||||
:param scale_score: Whether to scale the similarity score to the unit interval (range of [0,1]).
|
||||
If true (default) similarity scores (e.g. cosine or dot_product) which naturally have a different value
|
||||
range are scaled to a range of [0,1], where 1 means extremely relevant.
|
||||
Otherwise raw similarity scores (for example, cosine or dot_product) are used.
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
self.similarity_function = similarity_function
|
||||
self.progress_bar = progress_bar
|
||||
self.top_k = top_k
|
||||
self.scale_score = scale_score
|
||||
|
||||
self.document_embedder = MultiModalEmbedder(
|
||||
embedding_models=document_embedding_models,
|
||||
feature_extractors_params=document_feature_extractors_params,
|
||||
batch_size=batch_size,
|
||||
embed_meta_fields=embed_meta_fields,
|
||||
progress_bar=progress_bar,
|
||||
devices=devices,
|
||||
use_auth_token=use_auth_token,
|
||||
)
|
||||
|
||||
# Try to reuse the same embedder for queries if there is overlap
|
||||
if document_embedding_models.get(query_type, None) == query_embedding_model:
|
||||
self.query_embedder = self.document_embedder
|
||||
else:
|
||||
self.query_embedder = MultiModalEmbedder(
|
||||
embedding_models={query_type: query_embedding_model},
|
||||
feature_extractors_params={query_type: query_feature_extractor_params},
|
||||
batch_size=batch_size,
|
||||
embed_meta_fields=embed_meta_fields,
|
||||
progress_bar=progress_bar,
|
||||
devices=devices,
|
||||
use_auth_token=use_auth_token,
|
||||
)
|
||||
|
||||
self.document_store = document_store
|
||||
|
||||
def retrieve( # type: ignore
|
||||
self,
|
||||
query: Any,
|
||||
query_type: ContentTypes = "text",
|
||||
filters: Optional[FilterType] = None,
|
||||
top_k: Optional[int] = None,
|
||||
index: str = None,
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
scale_score: bool = None,
|
||||
) -> List[Document]:
|
||||
"""
|
||||
Scan through documents in DocumentStore and return a small number of documents that are most relevant to the
|
||||
supplied query. Returns a list of Documents.
|
||||
|
||||
:param query: Query value. It might be text, a path, a table, and so on.
|
||||
:param query_type: Type of the query ("text", "table", "image" and so on).
|
||||
:param filters: Optional filters to narrow down the search space to documents whose metadata fulfill certain
|
||||
conditions. It can be a single filter applied to each query or a list of filters
|
||||
(one filter per query).
|
||||
:param top_k: How many documents to return per query. Must be > 0.
|
||||
:param index: The name of the index in the DocumentStore from which to retrieve documents.
|
||||
:param batch_size: Number of queries to embed at a time. Must be > 0.
|
||||
:param scale_score: Whether to scale the similarity score to the unit interval (range of [0,1]).
|
||||
If true, similarity scores (for example, cosine or dot_product) which naturally have a different
|
||||
value range is scaled to a range of [0,1], where 1 means extremely relevant.
|
||||
Otherwise raw similarity scores (for example, cosine or dot_product) are used.
|
||||
"""
|
||||
return self.retrieve_batch(
|
||||
queries=[query],
|
||||
queries_type=query_type,
|
||||
filters=[filters],
|
||||
top_k=top_k,
|
||||
index=index,
|
||||
headers=headers,
|
||||
batch_size=1,
|
||||
scale_score=scale_score,
|
||||
)[0]
|
||||
|
||||
def retrieve_batch( # type: ignore
|
||||
self,
|
||||
queries: List[Any],
|
||||
queries_type: ContentTypes = "text",
|
||||
filters: Union[None, FilterType, List[FilterType]] = None,
|
||||
top_k: Optional[int] = None,
|
||||
index: str = None,
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
batch_size: Optional[int] = None,
|
||||
scale_score: bool = None,
|
||||
) -> List[List[Document]]:
|
||||
"""
|
||||
Scan through documents in DocumentStore and return a small number of documents that are most relevant to the
|
||||
supplied queries. Returns a list of lists of Documents (one list per query).
|
||||
|
||||
This method assumes all queries are of the same data type. Mixed-type query batches (for example one image and one text)
|
||||
are currently not supported. Group the queries by type and call `retrieve()` on uniform batches only.
|
||||
|
||||
:param queries: List of query values. They might be text, paths, tables, and so on.
|
||||
:param queries_type: Type of the query ("text", "table", "image" and so on)
|
||||
:param filters: Optional filters to narrow down the search space to documents whose metadata fulfill certain
|
||||
conditions. It can be a single filter that will be applied to each query or a list of filters
|
||||
(one filter per query).
|
||||
:param top_k: How many documents to return per query. Must be > 0.
|
||||
:param index: The name of the index in the DocumentStore from which to retrieve documents.
|
||||
:param batch_size: Number of queries to embed at a time. Must be > 0.
|
||||
:param scale_score: Whether to scale the similarity score to the unit interval (range of [0,1]).
|
||||
If True, similarity scores (for example, cosine or dot_product) which naturally have a different
|
||||
value range are scaled to a range of [0,1], where 1 means extremely relevant.
|
||||
Otherwise raw similarity scores (for example, cosine or dot_product) are used.
|
||||
"""
|
||||
filters_list: List[FilterType]
|
||||
if not isinstance(filters, list):
|
||||
filters_list = [filters] * len(queries)
|
||||
else:
|
||||
if len(filters) != len(queries):
|
||||
raise MultiModalRetrieverError(
|
||||
"The number of filters does not match the number of queries. Provide as many filters "
|
||||
"as queries, or a single filter that will be applied to all queries."
|
||||
)
|
||||
filters_list = filters
|
||||
|
||||
top_k = top_k or self.top_k
|
||||
index = index or self.document_store.index
|
||||
scale_score = scale_score or self.scale_score
|
||||
|
||||
# Embed the queries - we need them into Document format to leverage MultiModalEmbedder.embed()
|
||||
query_docs = [Document(content=query, content_type=queries_type) for query in queries]
|
||||
query_embeddings = self.query_embedder.embed(documents=query_docs, batch_size=batch_size)
|
||||
|
||||
# Query documents by embedding (the actual retrieval step)
|
||||
documents = []
|
||||
for query_embedding, query_filters in zip(query_embeddings, filters_list):
|
||||
docs = self.document_store.query_by_embedding(
|
||||
query_emb=query_embedding,
|
||||
top_k=top_k,
|
||||
filters=query_filters,
|
||||
index=index,
|
||||
headers=headers,
|
||||
scale_score=scale_score,
|
||||
)
|
||||
|
||||
documents.append(docs)
|
||||
return documents
|
||||
|
||||
def embed_documents(self, docs: List[Document]) -> np.ndarray:
|
||||
return self.document_embedder.embed(documents=docs)
|
||||
+8
-5
@@ -34,12 +34,15 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
BaseConfig.arbitrary_types_allowed = True
|
||||
|
||||
#: Types of content_types supported
|
||||
ContentTypes = Literal["text", "table", "image", "audio"]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Document:
|
||||
id: str
|
||||
content: Union[str, pd.DataFrame]
|
||||
content_type: Literal["text", "table", "image", "audio"] = Field(default="text")
|
||||
content_type: ContentTypes = Field(default="text")
|
||||
meta: Dict[str, Any] = Field(default={})
|
||||
score: Optional[float] = None
|
||||
embedding: Optional[np.ndarray] = None
|
||||
@@ -52,7 +55,7 @@ class Document:
|
||||
def __init__(
|
||||
self,
|
||||
content: Union[str, pd.DataFrame],
|
||||
content_type: Literal["text", "table", "image", "audio"] = "text",
|
||||
content_type: ContentTypes = "text",
|
||||
id: Optional[str] = None,
|
||||
score: Optional[float] = None,
|
||||
meta: Optional[Dict[str, Any]] = None,
|
||||
@@ -69,7 +72,7 @@ class Document:
|
||||
It's particularly helpful for handling of duplicates and referencing documents in other objects (e.g. Labels)
|
||||
There's an easy option to convert from/to dicts via `from_dict()` and `to_dict`.
|
||||
:param content: Content of the document. For most cases, this will be text, but it can be a table or image.
|
||||
:param content_type: One of "text", "table" or "image". Haystack components can use this to adjust their
|
||||
:param content_type: One of "text", "table", "image" or "audio". Haystack components can use this to adjust their
|
||||
handling of Documents and check compatibility.
|
||||
:param id: Unique ID for the document. If not supplied by the user, we'll generate one automatically by
|
||||
creating a hash from the supplied text. This behaviour can be further adjusted by `id_hash_keys`.
|
||||
@@ -226,8 +229,8 @@ class Document:
|
||||
|
||||
def __repr__(self):
|
||||
values = self.to_dict()
|
||||
if values.get("embedding", False):
|
||||
values["embedding"] = f"<embedding of shape {values['embedding'].get('shape', '[no shape]')}>"
|
||||
if values.get("embedding", None) is not None:
|
||||
values["embedding"] = f"<embedding of shape {getattr(values['embedding'], 'shape', '[no shape]')}>"
|
||||
return f"<Document: {str(self.to_dict())}>"
|
||||
|
||||
def __str__(self):
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
from typing import Optional, List, Union
|
||||
|
||||
import torch
|
||||
from torch.utils.data import Dataset
|
||||
|
||||
|
||||
@@ -10,3 +13,15 @@ class ListDataset(Dataset):
|
||||
|
||||
def __getitem__(self, i):
|
||||
return self.original_list[i]
|
||||
|
||||
|
||||
def get_devices(devices: Optional[List[Union[str, torch.device]]]) -> List[torch.device]:
|
||||
"""
|
||||
Convert a list of device names into a list of Torch devices,
|
||||
depending on the system's configuration and hardware.
|
||||
"""
|
||||
if devices is not None:
|
||||
return [torch.device(device) for device in devices]
|
||||
elif torch.cuda.is_available():
|
||||
return [torch.device(device) for device in range(torch.cuda.device_count())]
|
||||
return [torch.device("cpu")]
|
||||
|
||||
+2
-2
@@ -327,9 +327,9 @@ min-similarity-lines=6
|
||||
minversion = "6.0"
|
||||
addopts = "--strict-markers"
|
||||
markers = [
|
||||
"integration: integration tests",
|
||||
"unit: unit tests",
|
||||
|
||||
"integration: integration tests",
|
||||
|
||||
"generator: generator tests",
|
||||
"summarizer: summarizer tests",
|
||||
"embedding_dim: uses a document store with non-default embedding dimension (e.g @pytest.mark.embedding_dim(128))",
|
||||
|
||||
+16
-16
@@ -9,7 +9,7 @@ from unittest.mock import MagicMock
|
||||
from tokenizers.pre_tokenizers import WhitespaceSplit
|
||||
|
||||
import haystack
|
||||
from haystack.modeling.model.tokenization import get_tokenizer
|
||||
from haystack.modeling.model.feature_extraction import FeatureExtractor
|
||||
|
||||
|
||||
BERT = "bert-base-cased"
|
||||
@@ -28,8 +28,8 @@ SENTENCE_WITH_ELLIPSIS = "This is a sentence..."
|
||||
SENTENCE_WITH_LINEBREAK_1 = "and another one\n\n\nwithout space"
|
||||
SENTENCE_WITH_LINEBREAK_2 = """This is a sentence.
|
||||
With linebreak"""
|
||||
SENTENCE_WITH_LINEBREAKS = """Sentence
|
||||
with
|
||||
SENTENCE_WITH_LINEBREAKS = """Sentence
|
||||
with
|
||||
multiple
|
||||
newlines
|
||||
"""
|
||||
@@ -75,21 +75,21 @@ def convert_offset_from_word_reference_to_text_reference(offsets, words, word_sp
|
||||
|
||||
|
||||
def test_get_tokenizer_str():
|
||||
tokenizer = get_tokenizer(pretrained_model_name_or_path="test-model-name")
|
||||
tokenizer = FeatureExtractor(pretrained_model_name_or_path="test-model-name")
|
||||
tokenizer.mocker.from_pretrained.assert_called_with(
|
||||
pretrained_model_name_or_path="test-model-name", revision=None, use_fast=True, use_auth_token=None
|
||||
)
|
||||
|
||||
|
||||
def test_get_tokenizer_path(tmp_path):
|
||||
tokenizer = get_tokenizer(pretrained_model_name_or_path=tmp_path / "test-path")
|
||||
tokenizer = FeatureExtractor(pretrained_model_name_or_path=tmp_path / "test-path")
|
||||
tokenizer.mocker.from_pretrained.assert_called_with(
|
||||
pretrained_model_name_or_path=str(tmp_path / "test-path"), revision=None, use_fast=True, use_auth_token=None
|
||||
)
|
||||
|
||||
|
||||
def test_get_tokenizer_keep_accents():
|
||||
tokenizer = get_tokenizer(pretrained_model_name_or_path="test-model-name-albert")
|
||||
tokenizer = FeatureExtractor(pretrained_model_name_or_path="test-model-name-albert")
|
||||
tokenizer.mocker.from_pretrained.assert_called_with(
|
||||
pretrained_model_name_or_path="test-model-name-albert",
|
||||
revision=None,
|
||||
@@ -100,7 +100,7 @@ def test_get_tokenizer_keep_accents():
|
||||
|
||||
|
||||
def test_get_tokenizer_mlm_warning(caplog):
|
||||
tokenizer = get_tokenizer(pretrained_model_name_or_path="test-model-name-mlm")
|
||||
tokenizer = FeatureExtractor(pretrained_model_name_or_path="test-model-name-mlm")
|
||||
tokenizer.mocker.from_pretrained.assert_called_with(
|
||||
pretrained_model_name_or_path="test-model-name-mlm", revision=None, use_fast=True, use_auth_token=None
|
||||
)
|
||||
@@ -115,7 +115,7 @@ def test_get_tokenizer_mlm_warning(caplog):
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.parametrize("model_name", TOKENIZERS_TO_TEST)
|
||||
def test_save_load(tmp_path, model_name: str):
|
||||
tokenizer = get_tokenizer(pretrained_model_name_or_path=model_name, do_lower_case=False)
|
||||
tokenizer = FeatureExtractor(pretrained_model_name_or_path=model_name, do_lower_case=False)
|
||||
text = "Some Text with neverseentokens plus !215?#. and a combined-token_with/chars"
|
||||
|
||||
tokenizer.add_tokens(new_tokens=["neverseentokens"])
|
||||
@@ -124,7 +124,7 @@ def test_save_load(tmp_path, model_name: str):
|
||||
save_dir = tmp_path / "saved_tokenizer"
|
||||
tokenizer.save_pretrained(save_dir)
|
||||
|
||||
tokenizer_loaded = get_tokenizer(pretrained_model_name_or_path=save_dir)
|
||||
tokenizer_loaded = FeatureExtractor(pretrained_model_name_or_path=save_dir)
|
||||
new_encoding = tokenizer_loaded(text)
|
||||
|
||||
assert original_encoding == new_encoding
|
||||
@@ -132,7 +132,7 @@ def test_save_load(tmp_path, model_name: str):
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_tokenize_custom_vocab_bert():
|
||||
tokenizer = get_tokenizer(pretrained_model_name_or_path=BERT, do_lower_case=False)
|
||||
tokenizer = FeatureExtractor(pretrained_model_name_or_path=BERT, do_lower_case=False)
|
||||
tokenizer.add_tokens(new_tokens=["neverseentokens"])
|
||||
text = "Some Text with neverseentokens plus !215?#. and a combined-token_with/chars"
|
||||
|
||||
@@ -162,7 +162,7 @@ def test_tokenization_on_edge_cases_full_sequence_tokenization(model_name: str,
|
||||
"""
|
||||
Verify that tokenization on full sequence is the same as the one on "whitespace tokenized words"
|
||||
"""
|
||||
tokenizer = get_tokenizer(pretrained_model_name_or_path=model_name, do_lower_case=False, add_prefix_space=True)
|
||||
tokenizer = FeatureExtractor(pretrained_model_name_or_path=model_name, do_lower_case=False, add_prefix_space=True)
|
||||
|
||||
pre_tokenizer = WhitespaceSplit()
|
||||
words_and_spans = pre_tokenizer.pre_tokenize_str(edge_case)
|
||||
@@ -182,7 +182,7 @@ def test_tokenization_on_edge_cases_full_sequence_tokenization_roberta_exception
|
||||
Verify that tokenization on full sequence is the same as the one on "whitespace tokenized words".
|
||||
These test cases work for all tokenizers under test except for RoBERTa.
|
||||
"""
|
||||
tokenizer = get_tokenizer(pretrained_model_name_or_path=model_name, do_lower_case=False, add_prefix_space=True)
|
||||
tokenizer = FeatureExtractor(pretrained_model_name_or_path=model_name, do_lower_case=False, add_prefix_space=True)
|
||||
|
||||
pre_tokenizer = WhitespaceSplit()
|
||||
words_and_spans = pre_tokenizer.pre_tokenize_str(edge_case)
|
||||
@@ -211,7 +211,7 @@ def test_tokenization_on_edge_cases_full_sequence_tokenization_roberta_exception
|
||||
)
|
||||
@pytest.mark.parametrize("model_name,marker", TOKENIZERS_TO_TEST_WITH_TOKEN_MARKER)
|
||||
def test_tokenization_on_edge_cases_full_sequence_verify_spans(model_name: str, marker: str, edge_case: str):
|
||||
tokenizer = get_tokenizer(pretrained_model_name_or_path=model_name, do_lower_case=False, add_prefix_space=True)
|
||||
tokenizer = FeatureExtractor(pretrained_model_name_or_path=model_name, do_lower_case=False, add_prefix_space=True)
|
||||
|
||||
pre_tokenizer = WhitespaceSplit()
|
||||
words_and_spans = pre_tokenizer.pre_tokenize_str(edge_case)
|
||||
@@ -246,7 +246,7 @@ def test_tokenization_on_edge_cases_full_sequence_verify_spans(model_name: str,
|
||||
],
|
||||
)
|
||||
def test_detokenization_for_bert(edge_case):
|
||||
tokenizer = get_tokenizer(pretrained_model_name_or_path=BERT, do_lower_case=False)
|
||||
tokenizer = FeatureExtractor(pretrained_model_name_or_path=BERT, do_lower_case=False)
|
||||
|
||||
encoded = tokenizer(edge_case, add_special_tokens=False).encodings[0]
|
||||
|
||||
@@ -261,7 +261,7 @@ def test_detokenization_for_bert(edge_case):
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_encode_plus_for_bert():
|
||||
tokenizer = get_tokenizer(pretrained_model_name_or_path=BERT, do_lower_case=False)
|
||||
tokenizer = FeatureExtractor(pretrained_model_name_or_path=BERT, do_lower_case=False)
|
||||
text = "Some Text with neverseentokens plus !215?#. and a combined-token_with/chars"
|
||||
|
||||
encoded_batch = tokenizer(text)
|
||||
@@ -309,7 +309,7 @@ def test_encode_plus_for_bert():
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_tokenize_custom_vocab_bert():
|
||||
tokenizer = get_tokenizer(pretrained_model_name_or_path=BERT, do_lower_case=False)
|
||||
tokenizer = FeatureExtractor(pretrained_model_name_or_path=BERT, do_lower_case=False)
|
||||
|
||||
tokenizer.add_tokens(new_tokens=["neverseentokens"])
|
||||
text = "Some Text with neverseentokens plus !215?#. and a combined-token_with/chars"
|
||||
+25
-22
@@ -9,15 +9,16 @@ import pytest
|
||||
import torch
|
||||
from torch.utils.data import SequentialSampler
|
||||
from tqdm import tqdm
|
||||
from transformers import DPRQuestionEncoder
|
||||
from transformers import DPRQuestionEncoder, AutoTokenizer
|
||||
|
||||
from haystack.modeling.data_handler.dataloader import NamedDataLoader
|
||||
from haystack.modeling.data_handler.processor import TextSimilarityProcessor
|
||||
from haystack.modeling.model.biadaptive_model import BiAdaptiveModel
|
||||
from haystack.modeling.model.language_model import get_language_model, DPREncoder
|
||||
from haystack.modeling.model.prediction_head import TextSimilarityHead
|
||||
from haystack.modeling.model.tokenization import get_tokenizer
|
||||
|
||||
from haystack.nodes.retriever.dense import DensePassageRetriever
|
||||
|
||||
from haystack.modeling.utils import set_all_seeds, initialize_device_settings
|
||||
|
||||
from ..conftest import SAMPLES_PATH
|
||||
@@ -31,10 +32,10 @@ def test_dpr_modules(caplog=None):
|
||||
devices, n_gpu = initialize_device_settings(use_cuda=True)
|
||||
|
||||
# 1.Create question and passage tokenizers
|
||||
query_tokenizer = get_tokenizer(
|
||||
query_tokenizer = AutoTokenizer.from_pretrained(
|
||||
pretrained_model_name_or_path="facebook/dpr-question_encoder-single-nq-base", do_lower_case=True, use_fast=True
|
||||
)
|
||||
passage_tokenizer = get_tokenizer(
|
||||
passage_tokenizer = AutoTokenizer.from_pretrained(
|
||||
pretrained_model_name_or_path="facebook/dpr-ctx_encoder-single-nq-base", do_lower_case=True, use_fast=True
|
||||
)
|
||||
|
||||
@@ -363,9 +364,9 @@ def test_dpr_processor(embed_title, passage_ids, passage_attns, use_fast, num_ha
|
||||
]
|
||||
|
||||
query_tok = "facebook/dpr-question_encoder-single-nq-base"
|
||||
query_tokenizer = get_tokenizer(query_tok, use_fast=use_fast)
|
||||
query_tokenizer = AutoTokenizer.from_pretrained(query_tok, use_fast=use_fast)
|
||||
passage_tok = "facebook/dpr-ctx_encoder-single-nq-base"
|
||||
passage_tokenizer = get_tokenizer(passage_tok, use_fast=use_fast)
|
||||
passage_tokenizer = AutoTokenizer.from_pretrained(passage_tok, use_fast=use_fast)
|
||||
processor = TextSimilarityProcessor(
|
||||
query_tokenizer=query_tokenizer,
|
||||
passage_tokenizer=passage_tokenizer,
|
||||
@@ -420,9 +421,9 @@ def test_dpr_processor_empty_title(use_fast, embed_title):
|
||||
}
|
||||
|
||||
query_tok = "facebook/dpr-question_encoder-single-nq-base"
|
||||
query_tokenizer = get_tokenizer(query_tok, use_fast=use_fast)
|
||||
query_tokenizer = AutoTokenizer.from_pretrained(query_tok, use_fast=use_fast)
|
||||
passage_tok = "facebook/dpr-ctx_encoder-single-nq-base"
|
||||
passage_tokenizer = get_tokenizer(passage_tok, use_fast=use_fast)
|
||||
passage_tokenizer = AutoTokenizer.from_pretrained(passage_tok, use_fast=use_fast)
|
||||
processor = TextSimilarityProcessor(
|
||||
query_tokenizer=query_tokenizer,
|
||||
passage_tokenizer=passage_tokenizer,
|
||||
@@ -505,9 +506,9 @@ def test_dpr_problematic():
|
||||
]
|
||||
|
||||
query_tok = "facebook/dpr-question_encoder-single-nq-base"
|
||||
query_tokenizer = get_tokenizer(query_tok)
|
||||
query_tokenizer = AutoTokenizer.from_pretrained(query_tok)
|
||||
passage_tok = "facebook/dpr-ctx_encoder-single-nq-base"
|
||||
passage_tokenizer = get_tokenizer(passage_tok)
|
||||
passage_tokenizer = AutoTokenizer.from_pretrained(passage_tok)
|
||||
processor = TextSimilarityProcessor(
|
||||
query_tokenizer=query_tokenizer,
|
||||
passage_tokenizer=passage_tokenizer,
|
||||
@@ -536,9 +537,9 @@ def test_dpr_query_only():
|
||||
]
|
||||
|
||||
query_tok = "facebook/dpr-question_encoder-single-nq-base"
|
||||
query_tokenizer = get_tokenizer(query_tok)
|
||||
query_tokenizer = AutoTokenizer.from_pretrained(query_tok)
|
||||
passage_tok = "facebook/dpr-ctx_encoder-single-nq-base"
|
||||
passage_tokenizer = get_tokenizer(passage_tok)
|
||||
passage_tokenizer = AutoTokenizer.from_pretrained(passage_tok)
|
||||
processor = TextSimilarityProcessor(
|
||||
query_tokenizer=query_tokenizer,
|
||||
passage_tokenizer=passage_tokenizer,
|
||||
@@ -598,9 +599,9 @@ def test_dpr_context_only():
|
||||
]
|
||||
|
||||
query_tok = "facebook/dpr-question_encoder-single-nq-base"
|
||||
query_tokenizer = get_tokenizer(query_tok)
|
||||
query_tokenizer = AutoTokenizer.from_pretrained(query_tok)
|
||||
passage_tok = "facebook/dpr-ctx_encoder-single-nq-base"
|
||||
passage_tokenizer = get_tokenizer(passage_tok)
|
||||
passage_tokenizer = AutoTokenizer.from_pretrained(passage_tok)
|
||||
processor = TextSimilarityProcessor(
|
||||
query_tokenizer=query_tokenizer,
|
||||
passage_tokenizer=passage_tokenizer,
|
||||
@@ -649,9 +650,9 @@ def test_dpr_processor_save_load(tmp_path):
|
||||
}
|
||||
|
||||
query_tok = "facebook/dpr-question_encoder-single-nq-base"
|
||||
query_tokenizer = get_tokenizer(query_tok)
|
||||
query_tokenizer = AutoTokenizer.from_pretrained(query_tok)
|
||||
passage_tok = "facebook/dpr-ctx_encoder-single-nq-base"
|
||||
passage_tokenizer = get_tokenizer(passage_tok)
|
||||
passage_tokenizer = AutoTokenizer.from_pretrained(passage_tok)
|
||||
processor = TextSimilarityProcessor(
|
||||
query_tokenizer=query_tokenizer,
|
||||
passage_tokenizer=passage_tokenizer,
|
||||
@@ -727,11 +728,11 @@ def test_dpr_processor_save_load_non_bert_tokenizer(tmp_path: Path, query_and_pa
|
||||
# load model from model hub
|
||||
query_embedding_model = query_and_passage_model["query"]
|
||||
passage_embedding_model = query_and_passage_model["passage"]
|
||||
query_tokenizer = get_tokenizer(
|
||||
query_tokenizer = AutoTokenizer.from_pretrained(
|
||||
pretrained_model_name_or_path=query_embedding_model
|
||||
) # tokenizer class is inferred automatically
|
||||
query_encoder = get_language_model(pretrained_model_name_or_path=query_embedding_model)
|
||||
passage_tokenizer = get_tokenizer(pretrained_model_name_or_path=passage_embedding_model)
|
||||
passage_tokenizer = AutoTokenizer.from_pretrained(pretrained_model_name_or_path=passage_embedding_model)
|
||||
passage_encoder = get_language_model(pretrained_model_name_or_path=passage_embedding_model)
|
||||
|
||||
processor = TextSimilarityProcessor(
|
||||
@@ -771,11 +772,11 @@ def test_dpr_processor_save_load_non_bert_tokenizer(tmp_path: Path, query_and_pa
|
||||
passage_tokenizer.save_pretrained(save_dir + f"/{passage_encoder_dir}")
|
||||
|
||||
# load model from disk
|
||||
loaded_query_tokenizer = get_tokenizer(
|
||||
loaded_query_tokenizer = AutoTokenizer.from_pretrained(
|
||||
pretrained_model_name_or_path=Path(save_dir) / query_encoder_dir, use_fast=True
|
||||
) # tokenizer class is inferred automatically
|
||||
loaded_query_encoder = get_language_model(pretrained_model_name_or_path=Path(save_dir) / query_encoder_dir)
|
||||
loaded_passage_tokenizer = get_tokenizer(
|
||||
loaded_passage_tokenizer = AutoTokenizer.from_pretrained(
|
||||
pretrained_model_name_or_path=Path(save_dir) / passage_encoder_dir, use_fast=True
|
||||
)
|
||||
loaded_passage_encoder = get_language_model(pretrained_model_name_or_path=Path(save_dir) / passage_encoder_dir)
|
||||
@@ -893,11 +894,13 @@ def test_dpr_processor_save_load_non_bert_tokenizer(tmp_path: Path, query_and_pa
|
||||
loaded_passage_tokenizer.save_pretrained(save_dir + f"/{passage_encoder_dir}")
|
||||
|
||||
# load model from disk
|
||||
query_tokenizer = get_tokenizer(
|
||||
query_tokenizer = AutoTokenizer.from_pretrained(
|
||||
pretrained_model_name_or_path=Path(save_dir) / query_encoder_dir
|
||||
) # tokenizer class is inferred automatically
|
||||
query_encoder = get_language_model(pretrained_model_name_or_path=Path(save_dir) / query_encoder_dir)
|
||||
passage_tokenizer = get_tokenizer(pretrained_model_name_or_path=Path(save_dir) / passage_encoder_dir)
|
||||
passage_tokenizer = AutoTokenizer.from_pretrained(
|
||||
pretrained_model_name_or_path=Path(save_dir) / passage_encoder_dir
|
||||
)
|
||||
passage_encoder = get_language_model(pretrained_model_name_or_path=Path(save_dir) / passage_encoder_dir)
|
||||
|
||||
processor = TextSimilarityProcessor(
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import haystack
|
||||
from haystack.modeling.model.feature_extraction import FeatureExtractor, FEATURE_EXTRACTORS
|
||||
|
||||
|
||||
class MockedAutoTokenizer:
|
||||
mocker: MagicMock = MagicMock()
|
||||
|
||||
@classmethod
|
||||
def from_pretrained(cls, *args, **kwargs):
|
||||
cls.mocker.from_pretrained(*args, **kwargs)
|
||||
return cls()
|
||||
|
||||
|
||||
class MockedAutoConfig:
|
||||
mocker: MagicMock = MagicMock()
|
||||
model_type: str = "mocked"
|
||||
|
||||
@classmethod
|
||||
def from_pretrained(cls, *args, **kwargs):
|
||||
cls.mocker.from_pretrained(*args, **kwargs)
|
||||
return cls()
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_autotokenizer(request, monkeypatch):
|
||||
# Do not patch integration tests
|
||||
if "integration" in request.keywords:
|
||||
return
|
||||
monkeypatch.setattr(
|
||||
haystack.modeling.model.feature_extraction, "FEATURE_EXTRACTORS", {"mocked": MockedAutoTokenizer}
|
||||
)
|
||||
monkeypatch.setattr(haystack.modeling.model.feature_extraction, "AutoConfig", MockedAutoConfig)
|
||||
monkeypatch.setattr(haystack.modeling.model.feature_extraction, "AutoTokenizer", MockedAutoTokenizer)
|
||||
|
||||
|
||||
#
|
||||
# Unit tests
|
||||
#
|
||||
|
||||
|
||||
def test_init_str():
|
||||
tokenizer = FeatureExtractor(pretrained_model_name_or_path="test-model-name")
|
||||
|
||||
tokenizer.feature_extractor.mocker.from_pretrained.assert_called_with(
|
||||
pretrained_model_name_or_path="test-model-name", revision=None, use_fast=True, use_auth_token=None
|
||||
)
|
||||
|
||||
|
||||
def test_init_path(tmp_path):
|
||||
tokenizer = FeatureExtractor(pretrained_model_name_or_path=tmp_path / "test-path")
|
||||
|
||||
tokenizer.feature_extractor.mocker.from_pretrained.assert_called_with(
|
||||
pretrained_model_name_or_path=str(tmp_path / "test-path"), revision=None, use_fast=True, use_auth_token=None
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Integration tests
|
||||
#
|
||||
|
||||
FEATURE_EXTRACTORS_TO_TEST = ["bert-base-cased"]
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.parametrize("model_name", FEATURE_EXTRACTORS_TO_TEST)
|
||||
def test_load_modify_save_load(tmp_path, model_name: str):
|
||||
|
||||
# Load base tokenizer
|
||||
feature_extractor = FeatureExtractor(pretrained_model_name_or_path=model_name, do_lower_case=False)
|
||||
|
||||
# Add new tokens
|
||||
feature_extractor.feature_extractor.add_tokens(new_tokens=["neverseentokens"])
|
||||
|
||||
# Save modified tokenizer
|
||||
save_dir = tmp_path / "saved_tokenizer"
|
||||
feature_extractor.feature_extractor.save_pretrained(save_dir)
|
||||
|
||||
# Load modified tokenizer
|
||||
new_feature_extractor = FeatureExtractor(pretrained_model_name_or_path=save_dir)
|
||||
|
||||
# Assert the new tokenizer still has the added tokens
|
||||
assert len(new_feature_extractor.feature_extractor) == len(feature_extractor.feature_extractor)
|
||||
@@ -1,10 +1,8 @@
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from transformers import AutoTokenizer
|
||||
|
||||
from haystack.modeling.data_handler.processor import SquadProcessor
|
||||
from haystack.modeling.model.tokenization import get_tokenizer
|
||||
|
||||
from ..conftest import SAMPLES_PATH
|
||||
|
||||
@@ -24,7 +22,7 @@ def test_dataset_from_dicts_qa_inference(caplog=None):
|
||||
sample_types = ["answer-wrong", "answer-offset-wrong", "noanswer", "vanilla"]
|
||||
|
||||
for model in models:
|
||||
tokenizer = get_tokenizer(pretrained_model_name_or_path=model)
|
||||
tokenizer = AutoTokenizer.from_pretrained(pretrained_model_name_or_path=model)
|
||||
processor = SquadProcessor(tokenizer, max_seq_len=256, data_dir=None)
|
||||
|
||||
for sample_type in sample_types:
|
||||
@@ -251,7 +249,7 @@ def test_dataset_from_dicts_qa_labelconversion(caplog=None):
|
||||
sample_types = ["answer-wrong", "answer-offset-wrong", "noanswer", "vanilla"]
|
||||
|
||||
for model in models:
|
||||
tokenizer = get_tokenizer(pretrained_model_name_or_path=model)
|
||||
tokenizer = AutoTokenizer.from_pretrained(pretrained_model_name_or_path=model)
|
||||
processor = SquadProcessor(tokenizer, max_seq_len=256, data_dir=None)
|
||||
|
||||
for sample_type in sample_types:
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from transformers import AutoTokenizer
|
||||
|
||||
from haystack.modeling.data_handler.processor import SquadProcessor
|
||||
from haystack.modeling.model.tokenization import get_tokenizer
|
||||
from haystack.modeling.utils import set_all_seeds
|
||||
import torch
|
||||
|
||||
@@ -16,7 +17,7 @@ def test_processor_saving_loading(tmp_path, caplog):
|
||||
set_all_seeds(seed=42)
|
||||
lang_model = "roberta-base"
|
||||
|
||||
tokenizer = get_tokenizer(pretrained_model_name_or_path=lang_model, do_lower_case=False)
|
||||
tokenizer = AutoTokenizer.from_pretrained(pretrained_model_name_or_path=lang_model, do_lower_case=False)
|
||||
|
||||
processor = SquadProcessor(
|
||||
tokenizer=tokenizer,
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
from typing import List
|
||||
|
||||
import os
|
||||
import logging
|
||||
import os
|
||||
from math import isclose
|
||||
|
||||
import pytest
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from pandas.testing import assert_frame_equal
|
||||
from elasticsearch import Elasticsearch
|
||||
from transformers import DPRContextEncoderTokenizerFast, DPRQuestionEncoderTokenizerFast
|
||||
|
||||
from haystack.document_stores.base import BaseDocumentStore
|
||||
from haystack.document_stores.memory import InMemoryDocumentStore
|
||||
import pytest
|
||||
from elasticsearch import Elasticsearch
|
||||
|
||||
from haystack.document_stores import WeaviateDocumentStore
|
||||
from haystack.nodes.retriever.base import BaseRetriever
|
||||
from haystack.pipelines import DocumentSearchPipeline
|
||||
@@ -18,7 +23,7 @@ from haystack.document_stores.faiss import FAISSDocumentStore
|
||||
from haystack.document_stores import MilvusDocumentStore
|
||||
from haystack.nodes.retriever.dense import DensePassageRetriever, EmbeddingRetriever, TableTextRetriever
|
||||
from haystack.nodes.retriever.sparse import BM25Retriever, FilterRetriever, TfidfRetriever
|
||||
from transformers import DPRContextEncoderTokenizerFast, DPRQuestionEncoderTokenizerFast
|
||||
from haystack.nodes.retriever.multimodal import MultiModalRetriever
|
||||
|
||||
from ..conftest import SAMPLES_PATH
|
||||
|
||||
@@ -623,3 +628,179 @@ def test_es_filter_only(document_store, retriever):
|
||||
document_store.write_documents(docs)
|
||||
retrieved_docs = retriever.retrieve(query="", filters={"f1": ["0"]})
|
||||
assert len(retrieved_docs) == 11
|
||||
|
||||
|
||||
#
|
||||
# MultiModal
|
||||
#
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def text_docs() -> List[Document]:
|
||||
return [
|
||||
Document(
|
||||
content="My name is Paul and I live in New York",
|
||||
meta={
|
||||
"meta_field": "test2",
|
||||
"name": "filename2",
|
||||
"date_field": "2019-10-01",
|
||||
"numeric_field": 5.0,
|
||||
"odd_field": 0,
|
||||
},
|
||||
),
|
||||
Document(
|
||||
content="My name is Carla and I live in Berlin",
|
||||
meta={
|
||||
"meta_field": "test1",
|
||||
"name": "filename1",
|
||||
"date_field": "2020-03-01",
|
||||
"numeric_field": 5.5,
|
||||
"odd_field": 1,
|
||||
},
|
||||
),
|
||||
Document(
|
||||
content="My name is Christelle and I live in Paris",
|
||||
meta={
|
||||
"meta_field": "test3",
|
||||
"name": "filename3",
|
||||
"date_field": "2018-10-01",
|
||||
"numeric_field": 4.5,
|
||||
"odd_field": 1,
|
||||
},
|
||||
),
|
||||
Document(
|
||||
content="My name is Camila and I live in Madrid",
|
||||
meta={
|
||||
"meta_field": "test4",
|
||||
"name": "filename4",
|
||||
"date_field": "2021-02-01",
|
||||
"numeric_field": 3.0,
|
||||
"odd_field": 0,
|
||||
},
|
||||
),
|
||||
Document(
|
||||
content="My name is Matteo and I live in Rome",
|
||||
meta={
|
||||
"meta_field": "test5",
|
||||
"name": "filename5",
|
||||
"date_field": "2019-01-01",
|
||||
"numeric_field": 0.0,
|
||||
"odd_field": 1,
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def table_docs() -> List[Document]:
|
||||
return [
|
||||
Document(
|
||||
content=pd.DataFrame(
|
||||
{
|
||||
"Mountain": ["Mount Everest", "K2", "Kangchenjunga", "Lhotse", "Makalu"],
|
||||
"Height": ["8848m", "8,611 m", "8 586m", "8 516 m", "8,485m"],
|
||||
}
|
||||
),
|
||||
content_type="table",
|
||||
),
|
||||
Document(
|
||||
content=pd.DataFrame(
|
||||
{
|
||||
"City": ["Paris", "Lyon", "Marseille", "Lille", "Toulouse", "Bordeaux"],
|
||||
"Population": ["13,114,718", "2,280,845", "1,873,270 ", "1,510,079", "1,454,158", "1,363,711"],
|
||||
}
|
||||
),
|
||||
content_type="table",
|
||||
),
|
||||
Document(
|
||||
content=pd.DataFrame(
|
||||
{
|
||||
"City": ["Berlin", "Hamburg", "Munich", "Cologne"],
|
||||
"Population": ["3,644,826", "1,841,179", "1,471,508", "1,085,664"],
|
||||
}
|
||||
),
|
||||
content_type="table",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def image_docs() -> List[Document]:
|
||||
return [
|
||||
Document(content=str(SAMPLES_PATH / "images" / imagefile), content_type="image")
|
||||
for imagefile in os.listdir(SAMPLES_PATH / "images")
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_multimodal_text_retrieval(text_docs: List[Document]):
|
||||
retriever = MultiModalRetriever(
|
||||
document_store=InMemoryDocumentStore(return_embedding=True),
|
||||
query_embedding_model="sentence-transformers/multi-qa-mpnet-base-dot-v1",
|
||||
document_embedding_models={"text": "sentence-transformers/multi-qa-mpnet-base-dot-v1"},
|
||||
)
|
||||
retriever.document_store.write_documents(text_docs)
|
||||
retriever.document_store.update_embeddings(retriever=retriever)
|
||||
|
||||
results = retriever.retrieve(query="Who lives in Paris?")
|
||||
assert results[0].content == "My name is Christelle and I live in Paris"
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_multimodal_table_retrieval(table_docs: List[Document]):
|
||||
retriever = MultiModalRetriever(
|
||||
document_store=InMemoryDocumentStore(return_embedding=True),
|
||||
query_embedding_model="deepset/all-mpnet-base-v2-table",
|
||||
document_embedding_models={"table": "deepset/all-mpnet-base-v2-table"},
|
||||
)
|
||||
retriever.document_store.write_documents(table_docs)
|
||||
retriever.document_store.update_embeddings(retriever=retriever)
|
||||
|
||||
results = retriever.retrieve(query="How many people live in Hamburg?")
|
||||
assert_frame_equal(
|
||||
results[0].content,
|
||||
pd.DataFrame(
|
||||
{
|
||||
"City": ["Berlin", "Hamburg", "Munich", "Cologne"],
|
||||
"Population": ["3,644,826", "1,841,179", "1,471,508", "1,085,664"],
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_multimodal_image_retrieval(image_docs: List[Document]):
|
||||
retriever = MultiModalRetriever(
|
||||
document_store=InMemoryDocumentStore(return_embedding=True, embedding_dim=512),
|
||||
query_embedding_model="sentence-transformers/clip-ViT-B-32",
|
||||
document_embedding_models={"image": "sentence-transformers/clip-ViT-B-32"},
|
||||
)
|
||||
retriever.document_store.write_documents(image_docs)
|
||||
retriever.document_store.update_embeddings(retriever=retriever)
|
||||
|
||||
results = retriever.retrieve(query="What's a cat?")
|
||||
assert str(results[0].content) == str(SAMPLES_PATH / "images" / "cat.jpg")
|
||||
|
||||
|
||||
@pytest.mark.skip("Not working yet as intended")
|
||||
@pytest.mark.integration
|
||||
def test_multimodal_text_image_retrieval(text_docs: List[Document], image_docs: List[Document]):
|
||||
retriever = MultiModalRetriever(
|
||||
document_store=InMemoryDocumentStore(return_embedding=True, embedding_dim=512),
|
||||
query_embedding_model="sentence-transformers/clip-ViT-B-32",
|
||||
document_embedding_models={
|
||||
"text": "sentence-transformers/clip-ViT-B-32",
|
||||
"image": "sentence-transformers/clip-ViT-B-32",
|
||||
},
|
||||
)
|
||||
retriever.document_store.write_documents(image_docs)
|
||||
retriever.document_store.write_documents(text_docs)
|
||||
retriever.document_store.update_embeddings(retriever=retriever)
|
||||
|
||||
results = retriever.retrieve(query="What's Paris?")
|
||||
|
||||
text_results = [result for result in results if result.content_type == "text"]
|
||||
image_results = [result for result in results if result.content_type == "image"]
|
||||
|
||||
assert str(image_results[0].content) == str(SAMPLES_PATH / "images" / "paris.jpg")
|
||||
assert text_results[0].content == "My name is Christelle and I live in Paris"
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.1 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 620 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.7 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 8.1 MiB |
Reference in New Issue
Block a user