AI 팀에게 특정 훈련 문서가 어디서 왔는지 물어보면, 돌아오는 답은 대개 출처가 아니라 데이터셋 이름이다. 규제 당국, 고객, 법원은 이제 그 다음 답을 원한다. 어느 페이지를, 언제 가져왔고, 어떤 조건 하에서, 훈련 전에 어떤 처리를 거쳤는지에 관한 것이다. 웹 데이터로 모델을 구축하는 팀에게 출처 증명(provenance)은 모범 사례에서 반드시 증명 가능해야 하는 문제로 바뀌었다.
어려운 부분은 타이밍이다. 출처 기록에 필요한 대부분의 정보는 수집 시점에만 존재한다. 이 가이드는 이 문제가 왜 긴급해졌는지, 모든 문서에 대해 유지해야 할 기록은 무엇인지, 그리고 추출, 필터링, 중복 제거 과정에서 이 흐름을 어떻게 끊기지 않게 이어갈 수 있는지를 다룬다.
출처 증명이 요구 사항이 된 이유
세 가지 변화가 출처 증명을 연구 주제에서 규정 준수 과제로 바꿔놓았다.
EU AI Act는 공개 요약을 요구한다. 제53조(1)(d)는 범용 AI 모델 제공자에게 AI Office가 제공하는 템플릿을 사용하여 “훈련에 사용된 콘텐츠에 관한 충분히 상세한 요약”을 공개하도록 요구한다. 이 의무는 2025년 8월 2일부터 적용되며, 그 이전에 이미 시장에 출시된 모델은 2027년 8월 2일까지 유예된다. 집행위원회는 2025년 7월에 템플릿을 제시했으며, 이를 사용하는 것이 의무라고 명시하고 있다.
템플릿은 구체적인 사항을 요구한다. 스크래핑된 데이터의 경우, 사용된 크롤러의 이름, 수집 기간, 그리고 가장 관련성 높은 도메인 이름 목록을 요구한다. 즉 스크래핑된 콘텐츠 규모 기준 상위 10%의 도메인, 또는 중소기업의 경우 상위 5%나 1,000개 도메인 중 더 적은 쪽이다. 또한 제공자는 “데이터 수집 전과 중에” 텍스트 및 데이터 마이닝 옵트아웃을 어떻게 존중했는지 설명해야 한다. 제53조(1)(c)는 별도로 그러한 유보 사항을 식별하고 준수하는 저작권 정책을 요구한다.
공유 자원은 줄어들고 있고, 기록은 부실했다. Data Provenance Initiative는 2023년에 1,800개 이상의 텍스트 데이터셋을 감사했고, 널리 사용되는 호스팅 사이트에서 라이선스 누락률이 70%를 넘고 오류율이 50%를 넘는 것을 발견했다. 14,000개 웹 도메인에 대한 후속 감사에서는 단 1년 사이에 C4의 전체 토큰 중 약 5%, 그리고 가장 활발히 관리되는 출처의 28%가 robots.txt에 의해 완전히 제한되었다는 사실이 드러났다. 크롤링했을 당시에는 허용되던 데이터셋이 오늘 크롤링하기에는 허용되지 않을 수 있으며, 당시 상태를 기록해 두었을 때만 그 차이를 입증할 수 있다.
모든 문서에 대해 기록해야 할 것
출처 기록은 크롤러에 의해, 수집 시점에, 단 한 번 작성된다. 이후 모든 단계는 그것을 대체하는 것이 아니라 거기에 더해간다.
| 필드 | 중요한 이유 |
|---|---|
| 출처 URL | 도메인 목록 및 삭제 요청의 근거 |
| 수집 시각 (UTC) | 수집 기간과 해당 날짜의 옵트아웃 상태를 증명 |
| 응답 상태 | 실제 콘텐츠와 차단 페이지, 오류를 구분 |
| 원본 본문의 해시 | 이후의 모든 사본을 수신한 정확한 내용과 연결 |
| 관측 지점(vantage point) | 동일한 URL이 국가와 네트워크에 따라 다른 콘텐츠를 제공할 수 있음 |
| robots.txt 판단 | 크롤러가 수집 시점에 유효한 규칙을 준수했음을 보여줌 |
| 확인된 TDM 유보 | EU 저작권 규정과 템플릿이 중요시하는 옵트아웃 |
| 명시된 라이선스 또는 이용 약관 | 해당 페이지가 재사용에 관해 명시한 내용, 있다면 |
| 크롤러 이름 및 버전 | 템플릿은 크롤러 이름을 요구하며, 버전은 동작 변화를 설명함 |
| 상위(parent) 기록 | 정제되거나 중복 제거된 사본을 원본과 연결 |
두 가지 필드는 자주 누락된다. 첫 번째는 관측 지점이다. 한 국가나 네트워크에서 관측된 페이지는 다른 곳에서 본 동일한 URL과 다를 수 있으며, 이는 웹 데이터가 관측된 위치를 기록하는 것에 관한 우리 글의 논지다. 두 번째는 원본 본문 자체다. 해시는 수신한 바이트를 증명하지만, 예를 들어 페이지를 WARC 파일로 아카이빙하여 응답을 보관해 두면 이를 보여줄 수 있다.
코드로 작성한 출처 기록
아래 예제는 가져온 문서마다 하나의 기록을 작성하고, 수집 시점에 허용되었으며 유보되지 않은 문서만 유지하며, 추출된 각 사본을 해시로 원본과 연결한다. 또한 요약 템플릿이 요구하는 도메인 순위도 생성한다. 이 코드는 Python 3.9 이상에서 표준 라이브러리만으로 실행된다.
from __future__ import annotations
import hashlib
import json
from collections import Counter
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from urllib.parse import urlsplit
def sha256(data: bytes) -> str:
return hashlib.sha256(data).hexdigest()
@dataclass
class Provenance:
"""One record per fetched document, written at collection time."""
url: str
fetched_at: str # ISO 8601, UTC
status: int
content_sha256: str # hash of the raw response body
vantage: str # where it was observed from, e.g. "residential:de"
robots_allowed: bool # robots.txt decision for our crawler, at fetch time
tdm_reserved: bool # machine-readable TDM opt-out seen, at fetch time
license: str | None # licence or terms as stated on the page, if any
collector: str # crawler name and version
parents: list[str] = field(default_factory=list) # hashes this record derives from
step: str = "fetch" # fetch, extract, filter, dedup ...
def fetched(url, body, status, vantage, robots_allowed, tdm_reserved, license=None,
collector="corpus-crawler/1.4"):
return Provenance(
url=url,
fetched_at=datetime.now(timezone.utc).isoformat(timespec="seconds"),
status=status,
content_sha256=sha256(body),
vantage=vantage,
robots_allowed=robots_allowed,
tdm_reserved=tdm_reserved,
license=license,
collector=collector,
)
def derived(parent: Provenance, new_body: bytes, step: str) -> Provenance:
"""A transformed copy keeps the source fields and points back at its parent."""
rec = Provenance(**{**asdict(parent), "parents": [parent.content_sha256], "step": step})
rec.content_sha256 = sha256(new_body)
return rec
def eligible(rec: Provenance) -> bool:
"""Only documents that were allowed and not reserved at fetch time go further."""
return rec.status == 200 and rec.robots_allowed and not rec.tdm_reserved
def domain_summary(records, top_share=0.1):
"""Domains by document count, and the top share of them, for a training-data summary."""
counts = Counter(urlsplit(r.url).hostname for r in records)
ranked = [d for d, _ in counts.most_common()]
keep = max(1, round(len(ranked) * top_share))
return counts, ranked[:keep]
if __name__ == "__main__":
raw = [
fetched("https://news.example/a", b"<html>a</html>", 200, "residential:de", True, False),
fetched("https://news.example/b", b"<html>b</html>", 200, "residential:de", True, False),
fetched("https://shop.example/p/1", b"<html>p1</html>", 200, "residential:fr", True, True),
fetched("https://blog.example/x", b"<html>x</html>", 403, "residential:us", True, False),
fetched("https://forum.example/t/9", b"<html>t9</html>", 200, "residential:us", False, False),
]
kept = [r for r in raw if eligible(r)]
texts = [derived(r, r.url.encode() + b" text", "extract") for r in kept]
with open("manifest.jsonl", "w") as f:
for rec in raw + texts:
f.write(json.dumps(asdict(rec)) + "\n")
counts, top = domain_summary(raw)
print(f"fetched {len(raw)}, eligible {len(kept)}, extracted {len(texts)}")
print("domains:", dict(counts))
print("top domains:", top)
child = texts[0]
print("lineage ok:", child.parents == [kept[0].content_sha256], child.step, child.url)
출력:
fetched 5, eligible 2, extracted 2
domains: {'news.example': 2, 'shop.example': 1, 'blog.example': 1, 'forum.example': 1}
top domains: ['news.example']
lineage ok: True extract https://news.example/a
유보된 페이지, 차단된 페이지, robots.txt로 금지된 페이지 모두 매니페스트에 그대로 남아 있다. 이는 중요하다. 사용하지 않기로 선택한 것에 대한 기록도 증명의 일부이기 때문이다. 템플릿은 스크래핑된 콘텐츠의 크기로 도메인 순위를 매기므로, 실제 운영 환경에서는 문서 수가 아니라 바이트나 토큰 수를 세어야 한다.
파이프라인 전체에 출처 증명을 이어가기
수집 시점에 작성된 기록은 이후 모든 단계가 그 연결을 유지할 때만 유용하다.
- 추출. 훈련에 사용하는 텍스트는 가져온 HTML이 아니다. 위의
derived()가 하듯, 추출된 텍스트에 자체 해시를 부여하고 원본 기록을 가리키게 하라. - 필터링. 문서가 제외되었다는 사실뿐 아니라 왜 제외되었는지도 기록하라. “품질 기준 미달”과 “TDM 유보”는 규제 당국에게는 다른 답변이다.
- 중복 제거. 유사 중복 문서를 병합할 때는 살아남은 사본이 대표하는 모든 출처의 목록을 유지하라. 그렇지 않으면 특정 URL에 대한 삭제 요청이 그것이 결국 포함된 문서를 찾지 못한다.
- 혼합 및 샘플링. 각 훈련 실행이 사용한 매니페스트 버전을 기록하여, 요약이 모델이 실제로 본 데이터를 설명하도록 하라.
- 삭제. URL과 해시를 키로 하는 삭제 목록을 유지하라. 이후 어떤 사이트가 권리를 유보하거나 권리자가 삭제를 요청하면, 동일한 키로 파생된 모든 데이터셋에서 해당 문서를 찾을 수 있다.
옵트아웃 상태는 특히 주의가 필요하다. robots.txt 파일이나 텍스트 및 데이터 마이닝 유보는 크롤링 이후 어느 방향으로든 바뀔 수 있다. 수집 시점에 내린 판단을 기록하고, 작년의 답이 여전히 유효하다고 가정하기보다는 새로운 수집 전에 다시 확인하라.
참고할 만한 표준
처음부터 스키마를 새로 만들 필요는 없다.
- Datasheets for Datasets (Gebru et al., Communications of the ACM, 2021)는 모든 데이터셋이 그 동기, 구성, 수집 과정, 권장 용도를 문서화해야 한다고 제안한다. 데이터셋 수준 요약에 적합한 형식이다.
- Data & Trusted AI Alliance의 Data Provenance Standards는 세 그룹(출처, 출처 증명, 용도)으로 나뉜 22개의 메타데이터 필드를 정의하며, 여기에는 데이터 생성 기간, 방법, 사용 라이선스, 동의가 문서화된 위치가 포함된다. 버전 1.0.0은 2024년 7월에 발표되었다.
- C2PA Content Credentials는 서명된 출처 증명 매니페스트를 개별 미디어 자산에 첨부한다. 이는 자산 자체가 기록을 담을 수 있는 이미지, 오디오, 비디오에 가장 유용하다.
출처 증명이 흔히 무너지는 지점
- 사후에 추가된다. 문서별 기록이 없는 크롤링은 아무리 데이터셋 카드가 훌륭해도 나중에 감사할 수 없다.
- 라이선스를 데이터셋 카드에서 그대로 복사한다. 위의 감사는 바로 이 지점에서 오류가 집중된다는 것을 발견했다. 출처가 실제로 명시한 내용을 기록하고, 집계된 라벨은 검증이 필요한 주장으로 취급하라.
- 차단 페이지를 콘텐츠로 센다. 200 응답이라도 챌린지 페이지일 수 있다. 상태 코드와 콘텐츠 검사는 기록에 포함되어야 한다.
- 관측 지점을 알 수 없다. 각 시장을 로컬 레지덴셜 연결로 수집하는 것은, 각 수집이 어느 국가와 네트워크를 사용했는지 기록에 남아 있을 때만 출처 증명에 도움이 된다.
결론
출처 증명은 수집 시점에 작성하면 비용이 거의 들지 않지만, 나중에 복구하는 것은 거의 불가능에 가깝다. 문서마다 수집 시점에 하나의 기록을 작성하고, 모든 변환 과정에서 해시를 유지하며, 제외한 것과 포함한 것을 모두 기록하라. 그러면 AI Act가 요구하는 요약은 조사가 아니라 질의가 된다.
출처 및 참고 자료
- Regulation (EU) 2024/1689 (AI Act), Articles 53, 111 and 113: eur-lex.europa.eu
- European Commission, explanatory notice and template for the public summary of training content (July 2025): digital-strategy.ec.europa.eu
- Directive (EU) 2019/790, Article 4(3): eur-lex.europa.eu
- Longpre et al., The Data Provenance Initiative (2023): arxiv.org/abs/2310.16787
- Longpre et al., Consent in Crisis: The Rapid Decline of the AI Data Commons (2024): arxiv.org/abs/2407.14933
- Gebru et al., Datasheets for Datasets: doi.org/10.1145/3458723
- Data & Trusted AI Alliance, Data Provenance Standards v1.0.0: dtaalliance.org
- C2PA Specification: spec.c2pa.org