MaartenGr / MaartenGr/BERTopic

Suggestion: Batched cosine similarity for efficient memory handling of large documents

Open
#2,379 4 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
7.8k
Forks
920
Avg merge
22h 24m
Merged PRs (30d)
5

Description

Feature request

To help mitigate excessive memory consumption in cosine similarity for a large set of documents, allow a batched approach (when calculate_probabilities is False)

Motivation

Oftentimes the whole similarity matrix is brought into memory when it doesn't need to be (ie when we only take the assignment, assignment_val), which can cause memory issues when the document set is large

Your contribution

Something like

@staticmethod
def batched_cosine_similarity_argmax(
        embeddings: np.ndarray, zeroshot_embeddings: np.ndarray, batch_size: int = 1000
    ):
        """
        Assigns each embedding to the zeroshot embedding with the highest cosine similarity, processing in batches.
        Args:
            embeddings (np.ndarray): Array of shape (n_samples, n_features) containing the input embeddings to be assigned.
            zeroshot_embeddings (np.ndarray): Array of shape (n_zeroshot, n_features) containing the zeroshot embeddings to compare against.
            batch_size (int, optional): Number of embeddings to process per batch. Defaults to 1000.
        Returns:
            Tuple[np.ndarray, np.ndarray]:
                - assignment: Array of shape (n_samples,) with the index of the most similar zeroshot embedding for each input embedding.
                - assignment_vals: Array of shape (n_samples,) with the corresponding maximum cosine similarity values.
        """
        logger.info("Executing batched cosine similarity")

        assignment = []
        assignment_vals = []

        for i in range(0, embeddings.shape[0], batch_size):
            batch = embeddings[i : i + batch_size]
            sim = cosine_similarity(batch, zeroshot_embeddings)
            max_indices = np.argmax(sim, axis=1)
            max_vals = np.max(sim, axis=1)
            assignment.extend(max_indices)
            assignment_vals.extend(max_vals)

        return np.array(assignment), np.array(assignment_vals)

Then for illustration, zeroshot modelling could become

def _zeroshot_topic_modeling(
        self, documents: pd.DataFrame, embeddings: np.ndarray, batch_size: int = 1000
    ) -> Tuple[pd.DataFrame, np.array, pd.DataFrame, np.array]:
        """Find documents that could be assigned to either one of the topics in self.zeroshot_topic_list.

        We transform the topics in `self.zeroshot_topic_list` to embeddings and
        compare them through cosine similarity with the document embeddings.
        If they pass the `self.zeroshot_min_similarity` threshold, they are assigned.

        Arguments:
            documents: Dataframe with documents and their corresponding IDs
            embeddings: The document embeddings

        Returns:
            documents: The leftover documents that were not assigned to any topic
            embeddings: The leftover embeddings that were not assigned to any topic
        """
        logger.info(
            "Zeroshot Step 1 - Finding documents that could be assigned to either one"
            " of the zero-shot topics"
        )
        # Similarity between document and zero-shot topic embeddings
        zeroshot_embeddings = self._extract_embeddings(self.zeroshot_topic_list)

        assignment, assignment_vals = self.batched_cosine_similarity_argmax(
            embeddings, zeroshot_embeddings, batch_size=batch_size
        )

        assigned_ids = [
            index
            for index, value in enumerate(assignment_vals)
            if value >= self.zeroshot_min_similarity
        ]
        non_assigned_ids = [
            index
            for index, value in enumerate(assignment_vals)
            if value < self.zeroshot_min_similarity
        ]

        # Assign topics
        assigned_documents = documents.iloc[assigned_ids]
        assigned_documents["Topic"] = [topic for topic in assignment[assigned_ids]]

        assigned_documents["Old_ID"] = assigned_documents["ID"].copy()
        assigned_documents["ID"] = range(len(assigned_documents))
        assigned_embeddings = embeddings[assigned_ids]

        # Check that if a number of topics was specified, it exceeds the number of zeroshot topics matched
        num_zeroshot_topics = len(assigned_documents["Topic"].unique())
        if self.nr_topics != "auto":
            if self.nr_topics and not self.nr_topics > num_zeroshot_topics:
                raise ValueError(
                    f"The set nr_topics ({self.nr_topics}) must exceed the number of"
                    f" matched zero-shot topics ({num_zeroshot_topics}). Consider"
                    " raising nr_topics or raising the zeroshot_min_similarity"
                    f" ({self.zeroshot_min_similarity})."
                )

        # Select non-assigned topics to be clustered
        documents = documents.iloc[non_assigned_ids]
        documents["Old_ID"] = documents["ID"].copy()
        documents["ID"] = range(len(documents))
        embeddings = embeddings[non_assigned_ids]

        if len(documents) == 0:
            self.topics_ = assigned_documents["Topic"].values.tolist()
            self.topic_mapper_ = TopicMapper(self.topics_)

        logger.info("Zeroshot Step 1 - Completed \u2713")
        return documents, embeddings, assigned_documents, assigned_embeddings

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by locating the existing cosine-similarity path and the _zeroshot_topic_modeling entry point. Compare the proposed batched assignment behavior with the current similarity calculation, then verify that large document sets avoid retaining the full similarity matrix while assignments and similarity values remain equivalent.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
machine-learning, performance
Issue type
Feature
Difficulty
3/5
Estimated time
1-2 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.