When building corpora for large language models, deduplicating the text data is considered an important step for improving performance and reducing privacy risks. For this reason, corpus construction pipelines for large language models often include a De-duplication step (see the figure below).
Recent corpora for large language models range from hundreds of gigabytes to several terabytes, which makes deduplication computationally expensive. MinHash is therefore commonly used as an efficient deduplication method. MinHash is a probabilistic method for efficiently estimating the similarity (the Jaccard index) between pairs of texts.
This post explains the basic idea and method of MinHash, and how to reduce the number of comparisons.
Defining Duplication Between Text Pairs
When deduplicating the texts in a corpus, how you define a duplicate matters. The simplest approach is to treat only exact matches as duplicates. In real text data, however, the same content is often expressed differently, so exact matching alone may not be enough. A common approach is therefore to compute the similarity between texts and treat texts whose similarity is above a certain threshold as duplicates.
Text similarity can be measured with the Jaccard index, edit distance, cosine similarity, and so on. Since MinHash, the topic of this post, is a similarity method based on the Jaccard index, we focus on the Jaccard index here.
The Jaccard Index
The Jaccard index is a measure of similarity between two sets , defined as follows.
A nice property of the Jaccard index is the following.
- , where it is 0 when the intersection of is empty and 1 when
In other words, when the elements of the two sets match exactly, the Jaccard index is 1. Conversely, when the two sets have no elements in common, the Jaccard index is 0.
To use the Jaccard index for the similarity of a text pair, we first convert each text into a set. Ways to do this include splitting the string into n-grams (e.g. “I have a pen” -> {“I hav”, “have a”, “ve a ”, “e a p”, ” a pe”, “a pen”}) and splitting it into words (e.g. “I have a pen” -> {“I”, “have”, “a”, “pen”}).
As an example, let’s compute the Jaccard index between “I have a pen” and “I have an apple”. Here we convert each text into a set by splitting it into words.
A = {”I”, “have”, “a”, “pen”}, B = {”I”, “have”, ”an”, ”apple”}
The Jaccard index is then
Computational Cost of the Jaccard Index
The Jaccard index can be implemented in Python as follows.
def jaccard_similarity(A: set, B: set) -> float:
intersection = len(A & B)
union = len(A | B)
return intersection / union
According to the CPython documentation, intersection has an average time complexity of and a worst-case time complexity of , while union has both an average and worst-case time complexity of . The average time complexity of the Jaccard index is therefore , which is proportional to the length of the texts. This makes the computation expensive for long texts, so computing a large number of Jaccard indices, as in deduplicating a large corpus, takes a very long time.
MinHash, a probabilistic method for efficiently estimating the Jaccard index, solves this problem.
MinHash
MinHash is a probabilistic method for efficiently estimating the Jaccard index of sets . The MinHash algorithm is as follows.
- Convert each element of sets into a hash value with a hash function .
- Take the minimum (the minhash) of the hash values of each of .
- Then holds.
Let’s consider why the probability that the minhashes of match equals the Jaccard index.
Let . Then .
Now we compute the hash value of each element with the hash function : . Here, there are as many distinct hash values as there are elements in .
Therefore, for , the probability that the hash value is the minimum of is .
Moreover, holds only when the minimum among the elements of belongs to . The probability of this is .
Therefore, holds.
From the above, we see that the probability that the minhashes of sets match equals the Jaccard index. This means that we can estimate the Jaccard index by computing the minhash of each set in advance and comparing them.
However, a single minhash pair is not enough to estimate a probability, so we use multiple hash functions, compute the fraction of minhashes that match, and use that as the estimate of the Jaccard index. That is, using hash functions, we set . The larger is, the more accurate the estimate.
Computational Cost of MinHash
If minhashes have been computed in advance, the time complexity of computing the Jaccard index of two sets is .
Computing the minhash itself (), however, takes . Using minhashes just to compute a single similarity is therefore not efficient.
On the other hand, suppose there are many texts and queries whose similarity to them we want to compute. If we compute the minhashes of in advance, the time complexity of computing the similarity between a query and is . When there are many queries, the cost of precomputing the minhashes of becomes negligible, so MinHash lets us compute similarities quickly. Deduplicating a large corpus requires computing the similarity of every pair of texts, which makes MinHash very useful.
Reducing the Number of Minhash Comparisons
MinHash estimates the Jaccard index by comparing minhashes. Comparing minhashes, however, requires comparisons. Since an accurate estimate of the Jaccard index requires a large , the number of comparisons grows. To reduce the number of comparisons at the expense of accuracy, the minhashes are divided into buckets. The bucketing method is described below.
Let the number of minhashes be , and divide the minhashes into buckets. Each bucket holds minhashes. We then concatenate the minhashes within each bucket.
Next, after dividing the minhashes of the two sets into buckets, we compare whether each pair of corresponding buckets matches. This reduces the number of comparisons to .
Then, for sets with , the probability that at least one of the buckets matches is as follows.
Derivation: The probability that all minhashes in a bucket match is . So the probability that a bucket does not match is . The probability that none of the buckets match is . So the probability that at least one of the buckets matches is . ■
In this way, by comparing in coarse units called buckets and treating a pair as duplicates if at least one bucket matches, we can reduce the number of comparisons. However, there is a chance of missing a duplicate (a false negative) and of wrongly judging a pair to match (a false positive). By tuning the bucket size and the number of buckets, we can adjust the trade-off between computational cost and accuracy.
Below is an example of the MinHash parameters used for the corpus FineWeb. You can see that the larger the similarity , the higher the probability that a bucket matches.
Thus, dividing the minhashes into buckets reduces the number of comparisons.
Summary and Thoughts
MinHash is built on the interesting idea that the Jaccard index can be estimated by comparing minhashes, and the algorithm itself turned out to be very simple.
While looking into MinHash, I found that Okanohara-san and Akiba-san had published an article and slides on MinHash about ten years ago, which shows that it is a long-established method. It is interesting that it is attracting a lot of attention again now.
Also, in developing large language models, attention tends to go to pretraining and fine-tuning, but corpus construction, which has to handle large-scale data, relies on parallel processing over many CPU cores and on classical algorithms such as MinHash and clustering. I found it an interesting field where knowledge from computer science can be applied broadly.
References
This is the original MinHash paper.
- Leskovec et al., Mining of Massive Datasets - Chapter 3 Finding Similar Items
- Lee et al., Deduplicating Training Data Makes Language Models Better
- Kandpal et al., Deduplicating Training Data Mitigates Privacy Risks in Language Models
- FineWeb: decanting the web for the finest text data at scale
A blog post by the Hugging Face team packed with insights on building large-scale corpora. It includes extensive discussion of de-duplication.