重要提示

您正在查看 NeMo 2.0 文档。此版本对 API 和新库 NeMo Run 进行了重大更改。我们目前正在将 NeMo 1.0 的所有功能移植到 2.0。有关先前版本或 2.0 中尚不可用的功能的文档,请参阅 NeMo 24.07 文档

GPU 加速的精确和模糊去重#

背景#

NeMo Curator 中的精确和模糊文档级去重模块旨在减少数据集中重复和近重复文档的出现。NeMo Curator 支持这两种功能,并使用 RAPIDS 加速。

主要动机是,对于语言模型,在随机选择的文档上训练多个 epoch 可能对下游性能而言并非最优。有关何时有害的更多信息,请参阅 Muennighoff et al., 2023Tirumala et al., 2023

精确去重#

精确去重是指从数据集中删除完全相同的文档(即,文档字符串相等)。

由于精确去重所需的计算量明显较少,我们通常会在模糊去重之前运行精确去重。此外,根据我们去重 Common Crawl 快照的经验,很大一部分重复项(高达约 40%)可能是完全重复项。

工作原理#

精确去重的工作原理是对每个文档进行哈希处理,并且每个哈希仅保留一个文档。运行精确去重可在基于 CPU 和 GPU 的后端上工作。

用法#

Python API#

注意

在运行精确去重之前,您需要确保数据集包含每个文档的唯一 ID。如果需要,您可以使用 NeMo Curator 中的 add_id 模块来完成此操作。

from nemo_curator import AddId
from nemo_curator.datasets import DocumentDataset

add_id = AddId(id_field="my_id", id_prefix="doc_prefix")
dataset = DocumentDataset.read_json("input_file_path")
id_dataset = add_id(dataset)
id_dataset.to_parquet("/path/to/parquet/data")

在确保您的数据集具有唯一 ID 字段(或使用上面的代码创建一个)后,您可以按如下方式执行精确去重

from nemo_curator import ExactDuplicates
from nemo_curator.datasets import DocumentDataset

# Initialize the deduplication object
exact_duplicates = ExactDuplicates(
  id_field="my_id",
  text_field="text",
  perform_removal=True,
  cache_dir="/path/to/dedup_outputs", # Recommended to specify a cache_dir if perform_removal=True
)

dataset = DocumentDataset.read_parquet(
    input_files="/path/to/parquet/data",
    backend="cudf",  # or "pandas" for CPU
)
# Users who have specified perform_removal=False can split as following
duplicate_docs = exact_duplicates.identify_duplicates(dataset)

"""
Sample output:
my_id                  _hashes
22   doc_prefix-37820  e7cb1e88a7a30ea101d33e0c4c8857ef
70   doc_prefix-56261  bfce4501b9caa93cb3daccd6db1f13af
75   doc_prefix-56271  bfce4501b9caa93cb3daccd6db1f13af
84   doc_prefix-52261  0f763a2937d57b9d96bf9f220e55f2bd
107  doc_prefix-52271  0f763a2937d57b9d96bf9f220e55f2bd
"""

deduplicated_dataset = exact_duplicates.remove(dataset, duplicate_docs)

# Users who have specified perform_removal=True can get the output deduplicated dataset directly as follows
# deduplicated_dataset = exact_duplicates(dataset)

提示

更全面的示例可以在 examples/exact_deduplication.py 中找到。

CLI 实用程序#

假设已为每个文档添加了唯一 ID,用户可以按如下方式继续查找完全重复项

  • 查找完全重复项
    1. 输入:数据目录

    2. 输出:_exact_duplicates.parquet。完全重复项列表和文档哈希。

# same as `python nemo_curator/scripts/find_exact_duplicates.py`
 gpu_exact_dups \
   --input-data-dirs /path/to/jsonl/dir1 /path/to/jsonl/dir2 \
   --output-dir /path/to/output_dir \
   --input-json-text-field text_column_name \
   --input-json-id-field id_column_name \
   --log-dir ./
   # --scheduler-file /path/to/file.json

所有 CLI 脚本都包含在 nemo_curator/scripts/ 子目录中。

注意

CLI 实用程序仅限于 JSONL 数据集,并且仅适用于基于 GPU 的后端。对于不同的数据集格式或后端,请使用 Python API

模糊去重#

当删除语料库中的近重复项时,我们在文档级别执行模糊去重,以便删除 Jaccard 相似度得分高的文档。我们的方法与 Smith et al., 2020 中描述的方法非常相似。

工作原理#

此方法基本上可以分为以下阶段

  1. 计算 Minhash:第一阶段涉及计算文档上的 MinHash 签名。NeMo Curator 当前仅支持基于字符的 n-gram 用于 MinHashing。可以使用每个单词约 4.5 个字符的近似度量来确定熟悉基于单词的 n-gram 的用户的 n-gram 大小。

  2. LSH (局部敏感哈希):执行 LSH 以查找候选重复项。

  3. 存储桶到边列表:如果不使用误报检查,我们将直接将 LSH 存储桶转换为连接组件计算的边。

  1. 误报检查 (存储桶到边列表的可选替代方案):由于通过 MinHash + LSH 进行存储桶划分的近似性质 (Leskovec et al., 2020),NeMo Curator 提供了进一步处理每个存储桶的选项,方法是计算每个存储桶中文档之间的一些成对 Jaccard 相似度得分,并过滤掉可能已哈希到同一存储桶中的误报。

  1. Jaccard 地图存储桶: 由于 LSH 生成的存储桶可能具有高基数,我们将多个 LSH 存储桶映射到更大的批次以实现高效处理。此外,我们为每个存储桶分配少量文档(通过 num_anchor_docs 控制),以作为该存储桶内成对 Jaccard 相似度计算的候选文档。

  2. Jaccard 混洗:将原始数据集中的文档存储到新目录和文件中,以便将同一批次(存储桶)中的所有文档存储在一起。这允许跨不同存储桶并行化成对 Jaccard 相似度计算。

  3. Jaccard 计算:计算每个存储桶中所有文档对候选锚文档之间的 Jaccard 相似度得分。

  1. 连接组件:由于 LSH 的近似性质,近重复文档可能会被分配到不同的存储桶中,这些存储桶之间有一些重叠文档。我们使用 GPU 加速的连接组件算法来查找由同一存储桶中文档之间边形成的图中的所有连接组件。

连接组件步骤的结果是文档 ID 列表及其所属组。同一组中的所有文档都被视为近重复项。这些结果可用于从语料库中删除近重复项。

用法#

Python API#

注意

在运行模糊去重之前,您需要确保数据集包含每个文档的唯一 ID。如果需要,您可以使用 NeMo Curator 中的 add_id 模块来完成此操作。

from nemo_curator import AddId
from nemo_curator.datasets import DocumentDataset

add_id = AddId(id_field="my_id", id_prefix="doc_prefix")
dataset = DocumentDataset.read_json("input_file_path")
id_dataset = add_id(dataset)
id_dataset.to_json("/path/to/jsonl/data")
  1. 配置

  1. 直接使用 API

from nemo_curator import FuzzyDuplicatesConfig

config = FuzzyDuplicatesConfig(
    cache_dir="/path/to/dedup_outputs", # must be cleared between runs
    id_field="my_id",
    text_field="text",
    perform_removal=False, # dictates if deduplicated dataset or IDs of duplicates are returned
    seed=42,
    char_ngrams=24,
    num_buckets=20,
    hashes_per_bucket=13,
    use_64_bit_hash=False,
    buckets_per_shuffle=2,
    false_positive_check=False,
)
  1. 使用 YAML 文件

cache_dir: /path/to/dedup_outputs
id_field: my_id
text_field: text
perform_removal: False
seed: 42
char_ngrams: 24
num_buckets: 20
hashes_per_bucket: 13
use_64_bit_hash: False
buckets_per_shuffle: 2
false_positive_check: False
from nemo_curator import FuzzyDuplicatesConfig

config = FuzzyDuplicatesConfig.from_yaml("/path/to/config.yaml")
  1. 配置后用法

from nemo_curator import FuzzyDuplicates
from nemo_curator.datasets import DocumentDataset

# Initialize the deduplication object
fuzzy_duplicates = FuzzyDuplicates(config=config, logger="./")

dataset = DocumentDataset.read_json(
    input_files="/path/to/jsonl/data",
    backend="cudf", # FuzzyDuplicates only supports datasets with the cuDF backend.
)

# Users who have specified perform_removal=False can split as following
duplicate_docs = fuzzy_duplicates.identify_duplicates(dataset)
"""
Sample output:
              my_id  group
0  doc_prefix-56151     32
1  doc_prefix-47071    590
2  doc_prefix-06840    305
3  doc_prefix-20910    305
4  doc_prefix-42050    154
"""

deduplicated_dataset = fuzzy_duplicates.remove(dataset, duplicate_docs)

# Users who have specified perform_removal=True can get the output deduplicated dataset directly as follows
# deduplicated_dataset = fuzzy_duplicates(dataset)

提示

  • 一个全面的示例可以在 examples/fuzzy_deduplication.py 中找到。

  • num_bucketshashes_per_bucket 的默认值设置为查找 Jaccard 相似度约为 0.8 或更高的文档。

  • 较高的 buckets_per_shuffle 值可以带来更好的性能,但可能会导致内存不足错误。

  • false_positive_check 标志设置为 False 是获得最佳性能的理想选择。

  • 当将 false_positive_check 标志设置为 True 时,请确保运行之间清空 cache_dir,以避免先前运行的数据干扰当前运行的结果。

CLI 实用程序#

注意

模糊去重 CLI 脚本仅适用于 add_id 脚本生成的特定 ID 格式。如果数据集不包含此格式的 ID,建议使用 add_id 脚本创建它们,如下所示

add_id \
  --id-field-name="my_id" \
  --input-data-dir=<Path to directory containing jsonl files> \
  --id-prefix="doc_prefix" \
  --log-dir=./log/add_id

这将在每个 JSON 文档中创建一个名为 my_id 的新字段,其形式为“doc_prefix-000001”。如果数据集已经具有唯一 ID,则可以跳过此步骤。

为每个文档添加唯一 ID 后,用户可以继续进行模糊去重,这大致需要以下步骤(所有脚本都包含在 nemo_curator/scripts/fuzzy_deduplication 子目录中)

  1. 计算 Minhash

  • 输入:数据目录

  • 输出:每个数据目录的 minhashes.parquet

  • 示例调用

    # same as `python compute_minhashes.py`
    gpu_compute_minhashes \
      --input-data-dirs /path/to/jsonl/dir1 /path/to/jsonl/dir2 \
      --output-minhash-dir /path/to/output_minhashes \
      --input-json-text-field text_column_name \
      --input-json-id-field id_column_name \
      --minhash-length number_of_hashes \
      --char-ngram char_ngram_size \
      --hash-bytes 4 `#or 8 byte hashes` \
      --seed 42 \
      --log-dir ./
      # --scheduler-file /path/to/file.json
    
  1. 存储桶(Minhash 存储桶)

  • 输入:Minhash 目录

  • 输出:_buckets.parquet

  • 示例调用

    # same as `python minhash_lsh.py`
    minhash_buckets \
      --input-data-dirs /path/to/output_minhashes/dir1 /path/to/output_minhashes/dir2 \
      --output-bucket-dir /path/to/dedup_output \
      --input-minhash-field _minhash_signature \
      --input-json-id-field id_column_name \
      --minhash-length number_of_hashes \
      --num-bands num_bands \
      --buckets-per-shuffle 1 `#Value between [1-num_bands]. Higher is better but might lead to OOM` \
      --log-dir ./
      # --false-positive-check `#Writes bucket ID's in a format required for the false positive check`
      # --scheduler-file /path/to/file.json
    
  1. 误报检查(可选):如果跳过此步骤,请继续 跳过误报检查部分

  1. Jaccard 地图存储桶

  • 输入:_buckets.parquet 和数据目录

  • 输出:anchor_docs_with_bk.parquet

  • 示例调用

    # same as `python map_buckets.py`
    jaccard_map_buckets \
      --input-data-dirs /path/to/jsonl/dir1 /path/to/jsonl/dir2 \
      --input-bucket-dir /path/to/dedup_output/_buckets.parquet \
      --output-dir /path/to/dedup_output \
      --input-json-text-field text_column_name \
      --input-json-id-field id_column_name
      # --scheduler-file /path/to/file.json
    
  1. Jaccard 混洗

  • 输入:anchor_docs_with_bk.parquet 和数据目录

  • 输出:shuffled_docs.parquet

  • 示例调用

    # same as `python jaccard_shuffle.py`
    jaccard_shuffle \
      --input-data-dirs /path/to/jsonl/dir1 /path/to/jsonl/dir2 \
      --input-bucket-mapping-dir /path/to/dedup_output/anchor_docs_with_bk.parquet \
      --output-dir /path/to/dedup_output \
      --input-json-text-field text_column_name \
      --input-json-id-field id_column_name
      # --scheduler-file /path/to/file.json
    
  1. Jaccard 计算

  • 输入:shuffled_docs.parquet

  • 输出:jaccard_similarity_results.parquet

  • 示例调用

    # same as `python jaccard_compute.py`
    jaccard_compute \
      --shuffled-docs-path /path/to/dedup_output/shuffled_docs.parquet \
      --output-dir /path/to/dedup_output \
      --ngram-size char_ngram_size_for_similarity \
      --input-json-id-field id_column_name
      # --scheduler-file /path/to/file.json
    
  1. 跳过误报检查(性能更高)。如果执行了误报检查,则不需要此步骤。

  1. 存储桶到边列表

  • 输入:_buckets.parquet

  • 输出:_edges.parquet

  • 示例调用

    # same as `python buckets_to_edges.py`
    buckets_to_edges \
      --input-bucket-dir /path/to/dedup_output/_buckets.parquet \
      --output-dir /path/to/dedup_output \
      --input-json-id-field id_column_name
      # --scheduler-file /path/to/file.json
    
  1. 连接组件

  • 输入:jaccard_similarity_results.parquet(如果您运行了误报检查)或 _edges.parquet(如果您跳过了误报检查)

  • 输出:connected_components.parquet

  • 示例调用

    # same as `python connected_components.py`
    gpu_connected_component \
      --jaccard-pairs-path /path/to/dedup_output/jaccard_similarity_results.parquet `#Or /path/to/dedup_output/_edges.parquet` \
      --output-dir /path/to/dedup_output \
      --cache-dir /path/to/cc_cache \
      --jaccard-threshold 0.8 \
      --input-json-id-field id_column_name
      # --scheduler-file /path/to/file.json
    

注意

CLI 实用程序仅限于 JSONL 数据集,并且仅适用于特定的 ID 格式。对于不同的数据集或 ID 格式,请使用 Python API

增量模糊去重#

  • 如果向语料库添加了任何新数据,您将需要增量执行去重。要增量执行模糊去重,我们不需要为已经计算了 Minhash 的数据集重新计算 Minhash。相反,您可以将增量数据集组织到单独的目录中,并将所有新目录的列表传递给 gpu_compute_minhashes

    • 输入(假设增量快照都位于 /input/ 下)

      /input/cc-2020-40
      /input/cc-2021-42
      /input/cc-2022-60
      
    • 输出(假设 --output-minhash-dir=/output

      /output/cc-2020-40/minhashes.parquet
      /output/cc-2021-42/minhashes.parquet
      /output/cc-2022-60/minhashes.parquet
      
    • 示例调用

      # same as `python compute_minhashes.py`
      gpu_compute_minhashes \
        --input-data-dirs /input/cc-2020-40 /input/cc-2020-42 /input/cc-2020-60 \
        --output-minhash-dir /output/ \
        --input-json-text-field text_column_name \
        --input-json-id-field id_column_name \
        --minhash-length number_of_hashes \
        --char-ngram char_ngram_size \
        --hash-bytes 4(or 8 byte hashes) \
        --seed 42 \
        --log-dir ./
        # --scheduler-file /path/to/file.json
      

所有后续步骤(从 存储桶 开始)都可以在所有数据(旧的和新的)上执行,如上所述,无需修改。