From 8d221e4d82f03fc0f24ca8705a5b70438117ca3c Mon Sep 17 00:00:00 2001 From: Surya Date: Fri, 12 Apr 2024 16:24:59 -0700 Subject: [PATCH 1/2] add sentence-transformers uses MRL --- requirements.txt | 3 +- vlite/main.py | 30 ++++++++-------- vlite/model.py | 90 +++++++++++++++++++++--------------------------- 3 files changed, 56 insertions(+), 67 deletions(-) diff --git a/requirements.txt b/requirements.txt index 8226f39..602ca71 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,4 +8,5 @@ llama-cpp-python huggingface_hub tiktoken onnxruntime==1.17.1 -tokenizers==0.15.2 \ No newline at end of file +tokenizers==0.15.2 +sentence_transformers \ No newline at end of file diff --git a/vlite/main.py b/vlite/main.py index 6d3c456..70c8b60 100644 --- a/vlite/main.py +++ b/vlite/main.py @@ -86,16 +86,19 @@ def retrieve(self, text=None, top_k=5, metadata=None, return_scores=False): print("Retrieving similar texts...") if text: print(f"Retrieving top {top_k} similar texts for query: {text}") - query_chunks = chop_and_chunk(text, fast=True) - query_vectors = self.model.embed(query_chunks, device=self.device) + + # Embed and quantize the query text + query_vectors = self.model.embed(text, device=self.device) query_binary_vectors = self.model.quantize(query_vectors, precision="binary") + # Perform search on the query binary vectors results = [] for query_binary_vector in query_binary_vectors: - chunk_results = self.search(query_binary_vector, top_k, metadata) + chunk_results = self.rank_and_filter(query_binary_vector, top_k, metadata) results.extend(chunk_results) - results.sort(key=lambda x: x[1], reverse=True) + # Sort the results by similarity score + results.sort(key=lambda x: x[1]) results = results[:top_k] print("Retrieval completed.") @@ -103,15 +106,12 @@ def retrieve(self, text=None, top_k=5, metadata=None, return_scores=False): return [(idx, self.index[idx]['text'], self.index[idx]['metadata'], score) for idx, score in results] else: return [(idx, self.index[idx]['text'], self.index[idx]['metadata']) for idx, _ in results] - - def search(self, query_binary_vector, top_k, metadata=None): - # Reshape query_binary_vector to 1D array - query_binary_vector = query_binary_vector.reshape(-1) - - # Perform binary search - binary_vectors = np.array([item['binary_vector'] for item in self.index.values()]) - binary_similarities = np.einsum('i,ji->j', query_binary_vector, binary_vectors) - top_k_indices = np.argpartition(binary_similarities, -top_k)[-top_k:] + + def rank_and_filter(self, query_binary_vector, top_k, metadata=None): + query_binary_vector = np.array(query_binary_vector).reshape(-1) + + corpus_binary_vectors = np.array([item['binary_vector'] for item in self.index.values()]) + top_k_indices, top_k_scores = self.model.search(query_binary_vector, corpus_binary_vectors, top_k) top_k_ids = [list(self.index.keys())[idx] for idx in top_k_indices] # Apply metadata filter on the retrieved top_k items @@ -122,9 +122,7 @@ def search(self, query_binary_vector, top_k, metadata=None): if all(item_metadata.get(key) == value for key, value in metadata.items()): filtered_ids.append(chunk_id) top_k_ids = filtered_ids[:top_k] - - # Get the similarity scores for the top_k items - top_k_scores = binary_similarities[top_k_indices] + top_k_scores = top_k_scores[:len(top_k_ids)] return list(zip(top_k_ids, top_k_scores)) diff --git a/vlite/model.py b/vlite/model.py index 054ea13..64970e9 100644 --- a/vlite/model.py +++ b/vlite/model.py @@ -4,71 +4,61 @@ import numpy as np from typing import List from tokenizers import Tokenizer +import numpy as np +from typing import List +from sentence_transformers import SentenceTransformer + def normalize(v): - norm = np.linalg.norm(v, axis=1) + if v.ndim == 1: + v = v.reshape(1, -1) # Reshape v to 2D array if it is 1D + norm = np.linalg.norm(v, axis=1, keepdims=True) norm[norm == 0] = 1e-12 - return v / norm[:, np.newaxis] + return v / norm + + class EmbeddingModel: def __init__(self, model_name="mixedbread-ai/mxbai-embed-large-v1"): - tokenizer_path = hf_hub_download(repo_id=model_name, filename="tokenizer.json") - model_path = hf_hub_download(repo_id=model_name, filename="onnx/model.onnx") - - self.tokenizer = Tokenizer.from_file(tokenizer_path) - self.tokenizer.enable_truncation(max_length=512) - self.tokenizer.enable_padding(pad_id=0, pad_token="[PAD]", length=512) - - self.model = ort.InferenceSession(model_path) - print("[model]", self.model.get_modelmeta()) - + self.model = SentenceTransformer(model_name) self.model_metadata = { - "bert.embedding_length": 1024, + "bert.embedding_length": 512, "bert.context_length": 512 } self.embedding_size = self.model_metadata.get("bert.embedding_length", 1024) self.context_length = self.model_metadata.get("bert.context_length", 512) self.embedding_dtype = "float32" + + def embed(self, texts, max_seq_length=512, device="cpu", batch_size=32): + if isinstance(texts, str): + texts = [texts] # Ensure texts is always a list + embeddings = self.model.encode(texts, device=device, batch_size=batch_size, normalize_embeddings=True) + return embeddings - def embed(self, texts: List[str], max_seq_length=512, device="cpu", batch_size=32): - all_embeddings = [] - for i in range(0, len(texts), batch_size): - batch = texts[i:i + batch_size] - encoded = [self.tokenizer.encode(d) for d in batch] - input_ids = np.array([e.ids for e in encoded]) - attention_mask = np.array([e.attention_mask for e in encoded]) - token_type_ids = np.zeros_like(input_ids, dtype=np.int64) - - onnx_input = { - "input_ids": input_ids, - "attention_mask": attention_mask, - "token_type_ids": token_type_ids, - } - model_output = self.model.run(None, onnx_input) - last_hidden_state = model_output[0] - - input_mask_expanded = np.broadcast_to(np.expand_dims(attention_mask, -1), last_hidden_state.shape) - embeddings = np.sum(last_hidden_state * input_mask_expanded, 1) / np.clip(input_mask_expanded.sum(1), a_min=1e-9, a_max=None) - embeddings = normalize(embeddings).astype(np.float32) - all_embeddings.append(embeddings) - - return np.concatenate(all_embeddings) - - def token_count(self, texts): - tokens = 0 - for text in texts: - encoded = self.tokenizer.encode(text) - tokens += len(encoded.ids) - return tokens def quantize(self, embeddings, precision="binary"): - embeddings = np.array(embeddings) + # first normalize_embeddings to unit length + embeddings = normalize(embeddings) + # slice to get MRL embeddings + embeddings_slice = embeddings[..., :512] + if precision == "binary": - return np.packbits(embeddings > 0).reshape(embeddings.shape[0], -1) - elif precision == "int8": - return ((embeddings - np.min(embeddings, axis=0)) / (np.max(embeddings, axis=0) - np.min(embeddings, axis=0)) * 255).astype(np.uint8) + return self._binary_quantize(embeddings_slice) else: - raise ValueError(f"Unsupported precision: {precision}") + raise ValueError(f"Precision {precision} is not supported") - def rescore(self, query_vector, vectors): - return np.dot(query_vector, vectors.T).flatten() \ No newline at end of file + def _binary_quantize(self, embeddings): + return (np.packbits(embeddings > 0).reshape(embeddings.shape[0], -1) - 128).astype(np.int8) + + def hamming_distance(self, embedding1, embedding2): + # Ensure the embeddings are numpy arrays for the operation. + return np.count_nonzero(np.array(embedding1) != np.array(embedding2)) + + def search(self, query_embedding, embeddings, top_k): + # Convert embeddings to a numpy array for efficient operations if not already. + embeddings = np.array(embeddings) + distances = np.array([self.hamming_distance(query_embedding, emb) for emb in embeddings]) + + # Find the indices of the top_k smallest distances + top_k_indices = np.argsort(distances)[:top_k] + return top_k_indices, distances[top_k_indices] \ No newline at end of file From 8cc1ed2c659c34698fac87eb782e94d3d95ddbb5 Mon Sep 17 00:00:00 2001 From: Surya Date: Fri, 12 Apr 2024 17:10:37 -0700 Subject: [PATCH 2/2] implementing onnx --- requirements.txt | 4 +- tests/contexts/my_collection.ctx | Bin 0 -> 48604 bytes vlite/main.py | 4 +- vlite/model.py | 76 ++++++++++++++++++++++++++----- 4 files changed, 67 insertions(+), 17 deletions(-) create mode 100644 tests/contexts/my_collection.ctx diff --git a/requirements.txt b/requirements.txt index 602ca71..820f4c9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,9 +4,7 @@ docx2txt pandas Requests beautifulsoup4 -llama-cpp-python huggingface_hub tiktoken onnxruntime==1.17.1 -tokenizers==0.15.2 -sentence_transformers \ No newline at end of file +tokenizers==0.15.2 \ No newline at end of file diff --git a/tests/contexts/my_collection.ctx b/tests/contexts/my_collection.ctx new file mode 100644 index 0000000000000000000000000000000000000000..a58a7fcf123a469e9741767683f3c8750282bfc0 GIT binary patch literal 48604 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%0D56&v{dCagd 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 UH^