포스트

8월 2026 기준: 학습 데이터 큐레이션에서 Dedup이 ‘품질’로 이어지게 만드는 전처리 설계

학습 데이터 큐레이션에서 deduplication(중복 제거) 은 “디스크 절약”이 아니라 학습 신호의 왜곡을 줄이는 품질 공정입니다.

8월 2026 기준: 학습 데이터 큐레이션에서 Dedup이 ‘품질’로 이어지게 만드는 전처리 설계

들어가며

학습 데이터 큐레이션에서 deduplication(중복 제거) 은 “디스크 절약”이 아니라 학습 신호의 왜곡을 줄이는 품질 공정입니다. 웹 규모 코퍼스(예: CommonCrawl 계열)를 모으다 보면, 같은 문서/문장/템플릿이 수십 번씩 재등장하고(미러/스크랩/뉴스 재배포), 그 결과 모델이 특정 패턴을 과대학습하거나 평가 셋 오염(contamination) 으로 점수가 부풀어 오릅니다. FineWeb 계열은 이런 “웹 슬러지”를 정리하기 위해 cleaning + filtering + MinHash 기반 dedup을 핵심 공정으로 문서화했고, 설계 선택을 실험으로 비교합니다.1

언제 쓰면 좋은가:

  • 대규모 pretraining / continued pretraining: snapshot(월별/주별) 합칠 때 중복 폭발을 막아야 합니다.
  • SFT/Preference 데이터: 템플릿형 QA/대화가 반복되면 “다양성”이 무너지고 reward model도 편향됩니다.
  • RAG/평가용 데이터셋 구축: train/eval overlap 제거가 핵심입니다(성능 착시 방지).

언제 쓰면 안 되는가(또는 아주 조심해야 하는가):

  • “중복 = 나쁨”을 맹신해 과도하게 dedup하면, 오히려 분포가 바뀌어 저품질/이상한 텍스트가 상대적으로 ‘유니크’해서 살아남는 역효과가 납니다. FineWeb 쪽에서도 dedup/필터링 선택이 품질과 분포에 큰 영향을 준다는 점을 반복적으로 강조합니다.2
  • 법률/정책/표준처럼 의도적으로 반복되는 문구(정상 데이터)를 “중복”으로 잘못 판단하면 recall이 깨집니다.

🔧 핵심 개념

1) Dedup의 3계층: exact / near / semantic

  • Exact dedup: 문서 전체를 normalize한 뒤 hash(md5/sha256 등)로 동일성 판단. RedPajama-V2 파이프라인도 “quality signals 계산 → dedup”을 주요 단계로 잡고, exact dedup을 Bloom filter 기반으로 설명합니다.3
  • Near-duplicate(fuzzy) dedup: “거의 같은데 약간만 다른” 문서를 제거. 주류가 MinHash + LSH(Locality Sensitive Hashing) 입니다. NeMo Curator 문서도 문서 단위에서 Jaccard similarity 기반 fuzzy dedup을 수행하며, MinHash+LSH의 근사성 때문에 bucket 내에서 false positive check(선택적 재검증) 를 제공한다고 명시합니다.4
  • Semantic dedup: embedding 유사도(ANN index)로 의미 중복 제거. NeMo Curator는 “Semantic (GPU-accelerated)”를 기능으로 내세웁니다.5
    단, semantic dedup은 “정답이 없는” 영역이라 임계값/도메인별 정책이 중요합니다.

2) MinHash가 왜 표준이 됐나 (구조/흐름)

MinHash는 “문서를 n-gram(shingle) 집합으로 보고, 두 집합의 Jaccard similarity 를 근사”합니다. 실전 흐름은 대체로 이렇게 갑니다(NeMo Curator/FineWeb류 파이프라인에서 반복 등장):

  1. Normalize: HTML boilerplate 제거, whitespace/유니코드 정규화, 케이스 처리, 숫자/URL 처리 정책 결정
  2. Shingling: char n-gram 또는 token n-gram으로 문서를 조각냄
  3. MinHash signature 생성: k개의 hash로 “문서 지문”을 만듦
  4. LSH bucketing: signature를 band로 나눠 비슷한 것끼리 같은 bucket에 들어가게 함(근사 후보 생성)
  5. (옵션) 재검증: bucket 내 pair에 대해 실제 Jaccard를 다시 계산해 false positive를 제거(NeMo Curator가 이 옵션을 문서화)4
  6. Connected components: “A~B 유사, B~C 유사면 A,B,C를 한 덩어리”로 묶고 대표만 남김(NeMo Curator가 GPU connected components를 언급)4

여기서 중요한 차이점:

  • “pairwise 비교”는 O(N²)로 망하지만, LSH가 후보를 줄여서 웹 규모에서도 가능해집니다.
  • connected components는 “유사 관계의 전이”를 처리해 주지만, 임계값이 느슨하면 거대 컴포넌트(모든 게 연결) 가 생겨 과삭제 위험이 있습니다.

3) “Dedup = 품질”이 되려면: Quality signals와 결합

RedPajama-V2는 raw web 문서에 대해 quality signals/metadata를 제공해 필터링을 가능하게 하는 ‘풀(pool)’ 개념을 강조합니다.6
DataComp-LM(DCLM)도 “고정된 모델로 최고의 데이터를 찾는” 벤치마크로, baseline 분석에서 model-based filtering이 핵심이라고 요약합니다.7
즉 실무에서는:

  • dedup만 단독으로 하지 말고,
  • quality filter(heuristic + model-based) → dedup(정책적) → 최종 샘플링/믹싱 순서와 목적함수를 같이 설계해야 합니다.

💻 실전 코드

현실 시나리오: CommonCrawl/크롤링 텍스트 JSONL(수천만 문서) 를 “품질 필터 + MinHash near-dedup + (옵션) semantic dedup”으로 정제해서 학습용 JSONL로 내보내기.
아래 코드는 (1) CPU 환경에서 먼저 “재현 가능한 파이프라인”을 만들고, (2) 병목이 보이면 NeMo Curator 같은 GPU 경로로 갈 수 있게 설계합니다.

0) 의존성/실행

1
2
3
4
5
python -m venv .venv
source .venv/bin/activate

pip install orjson regex tqdm datasketch xxhash sentence-transformers faiss-cpu
# 대규모면: pip install duckdb pyarrow 도 추천

1) 초기 셋업: 스트리밍 품질 필터 + exact dedup

  • 중요: exact dedup은 “정규화가 80%”입니다. normalize 정책이 바뀌면 hash가 바뀌니, 버전/설정값을 메타데이터로 남기세요.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# curate_step1_exact_and_quality.py
import re, unicodedata, hashlib, orjson
from tqdm import tqdm

WS = re.compile(r"\s+")
CTRL = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f]")

def normalize_text(t: str) -> str:
    t = unicodedata.normalize("NFKC", t)
    t = CTRL.sub(" ", t)
    t = WS.sub(" ", t).strip()
    return t

def quality_gate(t: str) -> bool:
    # 현실적인 휴리스틱(예: 너무 짧거나, 링크/보일러플레이트 과다 등)
    if len(t) < 400:     # pretraining 문서 기준(프로젝트에 맞게 조정)
        return False
    alpha = sum(ch.isalpha() for ch in t)
    if alpha / max(1, len(t)) < 0.30:
        return False
    return True

def doc_hash(t: str) -> str:
    return hashlib.sha1(t.encode("utf-8")).hexdigest()

def run(in_jsonl: str, out_jsonl: str):
    seen = set()
    kept = 0
    total = 0
    with open(in_jsonl, "rb") as f_in, open(out_jsonl, "wb") as f_out:
        for line in tqdm(f_in, desc="step1"):
            total += 1
            obj = orjson.loads(line)
            text = normalize_text(obj["text"])
            if not quality_gate(text):
                continue
            h = doc_hash(text)
            if h in seen:
                continue
            seen.add(h)
            obj["text"] = text
            obj["norm_hash"] = h
            f_out.write(orjson.dumps(obj) + b"\n")
            kept += 1
    print(f"total={total} kept={kept} exact_dedup_kept_ratio={kept/max(1,total):.3f}")

if __name__ == "__main__":
    run("raw.jsonl", "step1_clean_exact.jsonl")

예상 출력(형태):

  • exact_dedup_kept_ratio가 너무 낮으면 normalize가 과격하거나 입력이 중복 투성이입니다.
  • 너무 높으면(거의 1.0) exact 중복은 없고, 문제는 near-dup/semantic 쪽일 가능성이 큽니다.

2) 기본 동작: MinHash(near-dup) + connected components 제거

대규모에서 핵심은 “모든 pair를 비교하지 않는 것”이며, MinHash+LSH는 이 목적에 맞습니다. NeMo Curator도 같은 구조(LSH bucketing 후 optional Jaccard 재검증, 그래프 컴포넌트)를 문서화합니다.4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# curate_step2_minhash_dedup.py
import orjson
from tqdm import tqdm
from datasketch import MinHash, MinHashLSH
import xxhash

def shingles(text: str, n: int = 5):
    # char n-gram(언어/도메인에 따라 token n-gram이 더 나을 수 있음)
    t = text
    for i in range(0, max(0, len(t) - n + 1)):
        yield t[i:i+n]

def make_minhash(text: str, num_perm: int = 128) -> MinHash:
    m = MinHash(num_perm=num_perm, hashfunc=lambda b: xxhash.xxh64(b).intdigest())
    for sh in shingles(text, 5):
        m.update(sh.encode("utf-8"))
    return m

def run(in_jsonl: str, out_jsonl: str, threshold: float = 0.85):
    # threshold는 “얼마나 공격적으로 near-dup을 제거할지”의 핵심 knob
    lsh = MinHashLSH(threshold=threshold, num_perm=128)
    kept = []
    with open(in_jsonl, "rb") as f:
        for idx, line in enumerate(tqdm(f, desc="minhash")):
            obj = orjson.loads(line)
            mh = make_minhash(obj["text"])
            dup_ids = lsh.query(mh)
            if dup_ids:
                # 이미 존재하는 near-dup이 있으면 스킵(가장 단순한 정책)
                continue
            doc_id = str(idx)
            lsh.insert(doc_id, mh)
            kept.append(obj)

    with open(out_jsonl, "wb") as f_out:
        for obj in kept:
            f_out.write(orjson.dumps(obj) + b"\n")
    print(f"kept={len(kept)}")

if __name__ == "__main__":
    run("step1_clean_exact.jsonl", "step2_near_dedup.jsonl", threshold=0.85)

현업에서는 여기서 더 나아가야 합니다:

  • “첫 등장만 남기기”는 간단하지만, 대표 선택 정책(길이, 품질 점수, 최신성 등)으로 바꾸면 품질이 확 올라갑니다.
  • connected components까지 구현하면 “전이 중복”까지 정리되지만, 그만큼 과삭제 위험도 커집니다(임계값 설계가 더 중요).

3) 확장: semantic dedup(옵션)으로 “의미상 반복” 제어

NeMo Curator/CuratorKIT 같은 프레임워크는 embedding 기반 dedup을 옵션으로 둡니다.5
아래는 “고품질 문서만 대상으로” semantic dedup을 얹는 패턴입니다(전체에 걸면 비용 폭발).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# curate_step3_semantic_dedup.py
import orjson, numpy as np
from tqdm import tqdm
from sentence_transformers import SentenceTransformer
import faiss

def run(in_jsonl: str, out_jsonl: str, sim_threshold: float = 0.92):
    model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")
    texts, objs = [], []
    with open(in_jsonl, "rb") as f:
        for line in f:
            obj = orjson.loads(line)
            objs.append(obj)
            texts.append(obj["text"][:2000])  # 비용 절감: 앞부분만(정책 필요)

    emb = model.encode(texts, batch_size=128, normalize_embeddings=True, show_progress_bar=True)
    emb = np.asarray(emb, dtype="float32")

    index = faiss.IndexFlatIP(emb.shape[1])  # cosine == inner product (normalized)
    index.add(emb)

    keep = np.ones(len(objs), dtype=bool)
    for i in tqdm(range(len(objs)), desc="semantic"):
        if not keep[i]:
            continue
        D, I = index.search(emb[i:i+1], 10)  # top-k 후보
        for score, j in zip(D[0], I[0]):
            if j == i:
                continue
            if score >= sim_threshold:
                keep[j] = False  # i를 대표로 남김(대표 정책은 프로젝트에 맞게)
    with open(out_jsonl, "wb") as f_out:
        for k, obj in zip(keep, objs):
            if k:
                f_out.write(orjson.dumps(obj) + b"\n")
    print(f"kept={int(keep.sum())} / {len(objs)}")

if __name__ == "__main__":
    run("step2_near_dedup.jsonl", "final.jsonl", sim_threshold=0.92)

⚡ 실전 팁 & 함정

Best Practice (바로 적용 가능한 것만)

1) “Dedup 임계값”을 고정값으로 박지 말고, 샘플링 기반으로 튜닝

  • threshold(예: 0.80/0.85/0.90)에 따라 과삭제/미삭제가 급변합니다.
  • FineWeb/RedPajama 같은 공개 파이프라인들도 설계 선택을 ablation으로 검증하는 방향을 취합니다.1

2) Quality filtering → Dedup 순서를 기본으로

  • 저품질(키워드 나열/광고/깨진 텍스트)은 “유니크”일 확률이 높습니다.
  • 먼저 quality gate로 바닥을 쳐주고, 그 다음에 dedup을 해야 “유니크한 쓰레기만 남는” 현상을 줄일 수 있습니다(실제 FineWeb 분석에서도 dedup/필터링 설계가 분포를 바꿀 수 있음을 강조).2

3) Compute budget과 curation을 같이 최적화

  • “필터를 세게 해서 더 좋은 데이터만”이 항상 이득은 아닙니다. DCLM/관련 연구 흐름에서는 모델/컴퓨트 스케일에 따라 최적 필터링이 달라진다는 문제의식을 강하게 제기합니다.8

흔한 함정/안티패턴

  • 전처리 버전 관리 부재: normalize 정책이 바뀌면 dedup 결과가 재현 불가. 결과적으로 “왜 성능이 올랐지/내렸지?”가 설명이 안 됩니다.
  • 전역(global) dedup만 맹신: snapshot 간 전역 dedup은 좋지만, 도메인별(뉴스/법률/코드/포럼) 분포가 다른데 같은 정책을 쓰면 손실이 큽니다.
  • semantic dedup을 무턱대고 전체에 적용: 비용 폭발 + 의미 유사도는 도메인마다 “중복” 정의가 달라 과삭제로 이어짐.

비용/성능/안정성 트레이드오프

  • CPU MinHash는 싸지만 규모가 커지면 느립니다. GPU 가속을 원하면 NeMo Curator는 GPU MinHash/connected components/false positive check까지 제공하며, cuDF 백엔드 제약 같은 운영 포인트가 있습니다.4
  • semantic dedup은 “적은 양의 고가치 데이터”에만 적용하면 ROI가 좋고, 웹 전체에는 보통 과합니다.

🚀 마무리

정리하면, 2026년 현재의 “정석”은 MinHash(near-dup) + quality filtering + (필요 시) semantic dedup을 하나의 파이프라인으로 보고, 임계값/대표 선택/도메인별 정책을 실험으로 결정하는 접근입니다. FineWeb은 dedup/필터링 설계를 공개적으로 문서화하며, RedPajama-V2는 quality signals를 제공해 “필터링 가능한 풀”을 만드는 방향을 제시합니다.1

도입 판단 기준(프로젝트 체크리스트):

  • 내 데이터가 “스냅샷/소스 병합” 구조인가? → near-dup는 거의 필수
  • 학습이 특정 템플릿/문장에 과적합하는가? → semantic/서브도큐먼트 dedup까지 검토
  • 필터를 강하게 했더니 오히려 이상한 문서가 늘었는가? → quality→dedup 순서/임계값 재튜닝

다음 학습 추천:

  • FineWeb 기술 리포트(설계/ablation)로 “어떤 필터가 실제로 효과가 있었는지” 감 잡기1
  • DCLM(DataComp-LM)로 “모델 고정 + 데이터만 바꿔서” 효과를 검증하는 사고방식 익히기7
  • 운영 관점에서 GPU 가속이 필요하면 NeMo Curator의 dedup 문서를 그대로 따라가며 병목을 없애기4
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.