데이터셋 버전·문서화·라이선스
먼저 읽을 장 15.4 데이터 분할과 누수 방지
코드는 git으로 버전을 관리하는 것이 당연해졌지만, 데이터는 여전히 final_v2_real.csv 같은 이름과 공유 폴더에 맡겨지는 경우가 많다. 그러다 보면 석 달 전의 결과를 다시 만들 수 없게 된다. 코드는 그대로인데 누군가 데이터 파일을 덮어썼거나, 분할이 바뀌었거나, label 몇 개가 고쳐졌기 때문이다. 결과가 왜 달라졌는지조차 알 수 없다는 것이 더 큰 문제다.
이 장은 데이터를 코드만큼 추적 가능하게 만드는 방법을 다룬다. 내용 해시로 데이터와 분할의 버전을 고정하고, 여러 출처를 통합할 때의 규칙을 정하고, 데이터셋 문서로 데이터의 맥락과 한계를 전달하고, 라이선스와 민감정보를 관리하고, 데이터가 바뀌었을 때 어디까지 다시 실험할지 정한다. 15.1장의 데이터 사전과 15.4장의 분할이 이 장에서 기록과 문서로 고정된다.
학습 목표
- 데이터 버전과 분할을 추적한다
- 데이터셋 문서를 작성한다
- 라이선스와 민감정보를 관리한다
버전 관리
왜 필요한가. 실험 결과는 (코드, 데이터, 분할, 설정)의 함수다. 코드는 git 커밋으로, 설정은 설정 파일로(6.3장) 고정할 수 있다. 데이터와 분할도 같은 수준으로 고정해야 "이 숫자는 정확히 무엇에서 나왔는가"에 답할 수 있다.
내용 해시. 파일 이름과 수정 날짜는 내용을 보증하지 않는다. 대신 내용 해시(checksum)를 쓴다. sha256 같은 암호학적 해시 함수는 입력 바이트가 하나만 달라도 전혀 다른 출력을 내고, 서로 다른 두 입력이 같은 출력을 낼 가능성은 실용적으로 무시할 수 있다. 그래서 해시가 같으면 내용이 같다고 본다. 반대로 해시는 바이트를 비교하므로, 데이터의 의미는 같아도 표현이 다르면 해시가 다르다.
import hashlib
import io
import numpy as np
import pandas as pd
def sha256(b: bytes) -> str:
return hashlib.sha256(b).hexdigest()
df = pd.DataFrame({
"cell_id": np.repeat(["C00", "C01", "C02"], 4),
"cycle": np.tile(np.arange(1, 5), 3),
"capacity_ah": [2.0, 1.99, 1.985, 1.98, 2.01, 2.0, 1.995, 1.99, 1.97, 1.96, 1.955, 1.95],
}) # (12, 3)
variants = {
"원본 ": df.to_csv(index=False).encode(),
"줄 끝 CRLF ": df.to_csv(index=False, lineterminator="\r\n").encode(),
"행 순서 다름": df.sort_values("capacity_ah").to_csv(index=False).encode(),
}
for name, b in variants.items():
print(name, sha256(b)[:16])
def canonical(b: bytes) -> bytes: # 읽어서 정렬한 뒤 정해진 형식으로 다시 쓴다
d = pd.read_csv(io.BytesIO(b)).sort_values(["cell_id", "cycle"])
return d.to_csv(index=False, lineterminator="\n").encode()
print("정규화 후 해시가 모두 같은가:", len({sha256(canonical(b)) for b in variants.values()}) == 1)원본 73e015ec4f4cb4e6 줄 끝 CRLF fb1170f539e9ad76 행 순서 다름 358b7b0a0dfd76a2 정규화 후 해시가 모두 같은가: True
줄 끝 문자(\n과 \r\n)나 행 순서만 달라도 해시가 완전히 다르다. 이것은 결함이 아니라 해시가 하는 일이다. 두 가지 용도를 구분한다.
- 무결성 확인: raw 파일은 받은 바이트 그대로 해시한다. 파일이 조금이라도 바뀌면 알아챈다.
- 내용 비교: "같은 데이터인가"를 묻고 싶으면, 읽어서 정렬하고 정해진 형식으로 다시 쓴 정규화 표현을 해시한다.
manifest. 데이터셋은 대개 여러 파일이다. 파일마다 해시와 크기를 적은 목록을 manifest라 하고, manifest 자체를 다시 해시하면 데이터셋 전체를 대표하는 값 하나가 된다. 경로 순서로 정렬해 두어야 도구나 운영체제에 관계없이 같은 내용이 같은 해시를 낸다.
files = {f"raw/{c}.csv": df[df["cell_id"] == c].to_csv(index=False).encode()
for c in ["C00", "C01", "C02"]}
def manifest(files: dict) -> str:
return "".join(f"{sha256(files[p])} {len(files[p])} {p}\n" for p in sorted(files))
m1 = manifest(files)
for line in m1.splitlines():
h, size, path = line.split(" ")
print(h[:16], size, path) # 화면에는 해시 앞 16자리만
print("데이터셋 해시 v1:", sha256(m1.encode())[:16])
files_v2 = dict(files)
files_v2["raw/C01.csv"] += b"C01,5,1.985\n" # 누군가 조용히 한 줄을 추가했다
m2 = manifest(files_v2)
print("데이터셋 해시 v2:", sha256(m2.encode())[:16])
by_path = lambda m: {line.split(" ")[2]: line for line in m.splitlines()}
old, new = by_path(m1), by_path(m2)
print("바뀐 파일:", [p for p in new if old.get(p) != new[p]])55c9daa038a72d2b 70 raw/C00.csv 45ff24c419306bd4 70 raw/C01.csv 4f30704a7ebf566f 71 raw/C02.csv 데이터셋 해시 v1: 35c55f4efcea7225 데이터셋 해시 v2: b6fdda9bd3798a10 바뀐 파일: ['raw/C01.csv']
누군가 C01.csv에 한 줄을 조용히 추가하자 데이터셋 해시가 바뀌었고, manifest를 비교해 어느 파일이 바뀌었는지도 바로 찾았다. 실험을 실행할 때 이 데이터셋 해시를 로그와 결과 파일에 함께 기록하면, 나중에 결과가 달라졌을 때 데이터 때문인지 코드 때문인지를 가를 수 있다.
split manifest. 분할도 데이터의 일부로 버전을 관리한다. 분할 코드와 시드를 기록하는 것만으로는 부족하다. 데이터가 늘거나 순서가 바뀌면 같은 코드와 시드가 다른 분할을 만든다. 그래서 각 샘플(또는 그룹)이 어느 집합에 속하는지를 파일(split.csv: cell_id,split)로 저장하고, 그 파일의 해시를 기록한다. 데이터가 늘어날 것이 예상되면 그룹 ID의 해시로 소속을 정하는 방법도 있다.
def hash_split(group_id: str, test_frac: float = 0.2) -> str:
h = int(sha256(group_id.encode()), 16)
return "test" if h % 1000 < test_frac * 1000 else "train"
def perm_split(ids: list, seed: int = 0, test_frac: float = 0.2) -> dict:
perm = np.random.default_rng(seed).permutation(len(ids))
n_test = int(test_frac * len(ids))
return {ids[i]: ("test" if r < n_test else "train") for r, i in enumerate(perm)}
v1 = [f"C{i:03d}" for i in range(100)] # 셀 100개
v2 = v1 + [f"C{i:03d}" for i in range(100, 150)] # 셀 50개 추가
p1, p2 = perm_split(v1), perm_split(v2)
h1, h2 = {c: hash_split(c) for c in v1}, {c: hash_split(c) for c in v2}
print("무작위 순열: 기존 셀 중 분할이 바뀐 수", sum(p1[c] != p2[c] for c in v1))
print("해시 기반 : 기존 셀 중 분할이 바뀐 수", sum(h1[c] != h2[c] for c in v1))
n1, n2 = list(h1.values()).count("test"), list(h2.values()).count("test")
print(f"해시 기반 test 셀 수: v1 {n1}/100, v2 {n2}/150")무작위 순열: 기존 셀 중 분할이 바뀐 수 28 해시 기반 : 기존 셀 중 분할이 바뀐 수 0 해시 기반 test 셀 수: v1 13/100, v2 26/150
전체 목록을 무작위 순열로 섞어 나누면, 셀 50개가 추가되었을 때 기존 100개 중 28개의 소속이 바뀐다. 이전 실험의 test 셀이 새 실험의 train에 들어간 것이다. 해시 기반 분할에서는 셀의 소속이 자기 ID로만 정해지므로 기존 셀은 하나도 바뀌지 않는다. 대신 test 비율이 정확히 20%가 되지는 않는다(13%, 17%). 그룹 수가 적을 때는 이 오차가 크므로, 어느 방법을 쓰든 최종 분할은 split manifest로 고정한다.
도구. 큰 데이터 파일을 git에 직접 넣으면 저장소가 무거워진다. 데이터 버전 관리 도구는 파일 내용을 저장소 밖(원격 저장소, 공유 디스크)에 두고, git에는 내용 해시를 담은 작은 포인터 파일만 넣는다. DVC가 대표적이다. 아래는 DVC로 raw 데이터를 추적하는 흐름이다(집필 시점 예시. 명령과 옵션은 공식 문서에서 확인한다).
dvc init # 저장소에 DVC 설정 추가
dvc add data/raw # data/raw의 해시를 계산해 data/raw.dvc에 기록
git add data/raw.dvc data/.gitignore # 데이터 대신 포인터 파일을 커밋
git commit -m "data: raw v1 (셀 100개)"
dvc remote add -d storage /shared/dvc-store
dvc push # 내용을 원격 저장소로 올린다
# 다른 사람·다른 서버에서
git checkout <커밋> && dvc pull # 그 커밋이 가리키는 데이터 내용을 받는다
Git LFS도 큰 파일을 포인터로 바꾸어 관리하지만, 파이프라인 단계와 결과까지 추적하는 기능은 도구마다 다르다. 도구가 없어도 원리는 같다. raw는 읽기 전용으로 두고, manifest와 해시를 실험 기록에 남긴다.
데이터 통합
왜 필요한가. 과학 데이터는 여러 실험실, 여러 장비, 여러 공개 데이터셋에서 모인다. 출처마다 열 이름, 단위, 프로토콜, 결측 규칙이 다르다. 15.1장에서 본 단위 혼합은 통합 과정에서 가장 흔히 생기는 오류다.
통합 스키마. 여러 출처를 합칠 때는 먼저 목표 스키마(열 이름·단위·자료형·결측 규칙)를 정하고, 출처마다 그 스키마로 바꾸는 매핑 표를 만든다.
| 목표 열 | 단위 | 출처 A | 출처 B | 출처 C |
|---|---|---|---|---|
temp_c |
°C | T (°C) |
temperature_K (K, −273.15) |
temp (°F, 변환식) |
capacity_ah |
Ah | cap_mAh (÷1000) |
Q (Ah) |
없음(결측) |
protocol |
— | "CC-CV 1C" | 문서에서 수동 입력 | 열 없음, 논문 표 2 |
매핑 표는 코드로 구현하고, 변환 후 15.1장의 범위 검사와 15.3장의 출처별 분포 비교를 반드시 거친다. 통합된 데이터에는 출처 열(source)을 남겨 둔다. 출처별 차이가 남아 있는지 확인하고, 출처 단위로 분할하거나 외부 검증을 할 수 있게 하기 위해서다(15.4장).
프로토콜의 차이는 단위 변환으로 해결되지 않는다. 같은 "용량"이라도 방전 전류, 온도, 종료 전압이 다르면 다른 양이다. 이런 조건은 열로 남기고, 조건이 크게 다른 출처를 합칠지는 연구 질문에 따라 판단한다.
데이터셋 문서
왜 필요한가. 데이터를 만든 사람은 수집 조건과 한계를 알지만, 쓰는 사람은 모른다. 이 정보 차이 때문에 데이터가 만들어진 목적과 맞지 않는 곳에 쓰이고, 숨은 편향이 모델로 옮겨 간다. 전자 부품에 사양서(datasheet)가 따라오듯 데이터셋에도 표준화된 문서를 붙이자는 제안이 datasheets for datasets다(Gebru 외, 2021). 이 제안은 데이터셋의 동기, 구성, 수집 과정, 전처리, 용도, 배포, 유지 관리에 대한 질문 목록을 제시한다. 같은 목적의 짧은 요약 문서를 data card라 부르기도 한다.
무엇을 쓰는가. 과학 데이터에 맞춘 최소 항목은 다음과 같다.
| 항목 | 내용 |
|---|---|
| 동기 | 왜, 누가, 어떤 연구 질문을 위해 만들었는가 |
| 구성 | 단위별 개수(entity·group·sample), 열의 의미(15.1장 데이터 사전 링크), label의 생성 과정 |
| 수집 | 장비, 기간, 조건, 프로토콜, 선정 기준(무엇이 포함되고 무엇이 빠졌는가) |
| 전처리 | raw → processed의 단계와 코드 버전, 제외한 샘플과 그 이유 |
| 분할 | split manifest와 분할 단위, 그 근거 |
| 알려진 한계 | 다루지 않는 조건 범위, 알려진 편향, label 잡음의 크기 |
| 용도 | 권장 용도와 권장하지 않는 용도 |
| 배포·유지 | 라이선스, 인용 방법, 버전과 manifest 해시, 연락처, 변경 이력 |
"권장하지 않는 용도"와 "알려진 한계"는 쓰기 불편하지만 가장 중요한 항목이다. 예를 들어 상온에서만 측정한 배터리 데이터라면 "저온 성능 예측에는 쓰지 않는다"를 명시한다.
라이선스와 이용 조건
왜 필요한가. 데이터를 내려받을 수 있다는 것과 그 데이터를 쓰고, 고치고, 다시 배포할 수 있다는 것은 다르다. 라이선스를 확인하지 않고 만든 파생 데이터셋은 공개할 수 없거나, 공개한 뒤 내려야 할 수 있다.
확인할 세 가지.
- 상업적 이용: 연구 결과가 기업 과제나 특허로 이어질 수 있다면 비상업 조건(예: Creative Commons의 NC 조건)을 확인한다.
- 재배포와 파생물: 원 데이터나 전처리한 데이터를 다시 공개해도 되는가. 파생물에 같은 라이선스를 요구하는가(예: Creative Commons의 SA 조건).
- 인용 의무: 저작자 표시(예: CC BY의 BY 조건)와 데이터셋이 지정한 인용 형식.
라이선스가 없을 때. 표기가 없다는 것은 자유롭게 써도 된다는 뜻이 아니다. 이용 조건이 정해지지 않은 상태다. 제공자에게 문의해 조건을 서면으로 받는다. 재배포를 허락받지 못하면 원 데이터를 받는 방법과 전처리 코드만 공개해, 다른 사람이 같은 데이터를 스스로 만들 수 있게 한다.
자신의 데이터를 공개할 때. 데이터에 맞는 라이선스를 명시한다. 법적 판단이 필요한 부분(기관의 지식재산권 규정, 공동 연구 계약, 데이터 제공 계약)은 소속 기관의 담당 부서에 확인한다. 구체적인 라이선스 조건은 라이선스 원문에서 확인한다.
민감정보
왜 필요한가. 의료·생체 데이터, 설문, 위치 정보에는 개인정보가 있다. 산업 협력 데이터에는 기업의 기밀(공정 조건, 소재 조성)이 있다. 이런 정보가 유출되면 연구 윤리와 법, 계약을 모두 어기게 된다.
가명화와 익명화. 개인을 직접 식별하는 값(이름, ID)을 다른 값으로 바꾸는 것을 가명화(pseudonymization)라 한다. 흔한 실수는 ID를 해시하면 안전하다고 생각하는 것이다. 가능한 ID의 수가 적으면 모두 해시해 보는 것만으로 되찾을 수 있다.
import hmac
patient = "P0427"
leaked = sha256(patient.encode()) # 공개된 데이터의 "익명" ID
table = {sha256(f"P{i:04d}".encode()): f"P{i:04d}" for i in range(10_000)}
print("가능한 ID 1만 개를 해시해 되찾은 ID:", table[leaked])
key = b"store-this-key-outside-the-dataset" # 실제 키는 데이터와 따로 보관
pseudo = hmac.new(key, patient.encode(), hashlib.sha256).hexdigest()
print("키가 있는 HMAC 가명이 표에 있는가:", pseudo in table)가능한 ID 1만 개를 해시해 되찾은 ID: P0427 키가 있는 HMAC 가명이 표에 있는가: False
가능한 ID 1만 개를 모두 해시해 대조표를 만드는 일은 순식간에 끝나고, 그 표로 "익명" 해시에서 원래 ID를 되찾았다. 데이터와 따로 보관한 비밀 키를 쓰는 HMAC으로 만든 가명은 키가 없으면 같은 표로 찾을 수 없다. 키의 보관과 접근 통제가 곧 보호 수준이다.
가명화는 익명화가 아니다. 생년월일, 우편번호, 성별, 희귀 질환 같은 준식별자(quasi-identifier)의 조합만으로도 개인을 다시 식별할 수 있다. 공개 전에 준식별자를 범주로 묶거나(나이 → 연령대), 드문 조합을 제거하는 등의 처리를 검토한다.
접근 권한. 민감 데이터는 공개 여부가 아니라 누가 어디서 무엇을 할 수 있는가로 관리한다.
- 연구윤리위원회(IRB)의 승인 범위와 동의서의 이용 범위를 확인하고 기록한다.
- 원본은 접근이 통제된 서버에 두고, 분석은 그 안에서 한다. 개인 노트북이나 공용 클라우드 폴더로 복사하지 않는다.
- 데이터 문서에 접근 조건과 신청 절차를 쓴다. 데이터를 공개할 수 없어도 코드와 문서, 합성 예시 데이터(15.6장)는 공개할 수 있다.
- AI 도구에 데이터를 붙여 넣는 것도 외부 전송이다(4.3장). 기관의 정책을 확인한다.
업데이트와 재실험
왜 필요한가. 데이터는 고쳐지고 늘어난다. label 오류가 발견되고, 새 셀이 측정되고, 전처리 규칙이 바뀐다. 매번 모든 실험을 처음부터 다시 하는 것은 비싸고, 아무것도 다시 하지 않으면 결과가 서로 비교 불가능해진다.
무엇이 바뀌었는가로 범위를 정한다.
| 바뀐 것 | 다시 할 것 |
|---|---|
| test의 label만 고침 | 비교한 모든 모델의 재평가(재학습 불필요) |
| train의 label·샘플 | 영향받는 모든 모델의 재학습과 재평가 |
| 분할 | 전체 재학습·재평가. 이전 결과와 직접 비교하지 않는다 |
| 전처리 규칙 | 전처리 이후의 모든 단계 |
| 새 데이터 추가(분할 고정) | 새 데이터를 쓰는 실험만. 기존 test는 그대로 유지 |
원칙은 하나다. 한 표 안의 숫자는 같은 데이터 버전과 같은 분할에서 나와야 한다. 표마다 데이터셋 해시(또는 버전 이름)를 적고, 버전이 다른 결과를 섞지 않는다. 데이터셋의 변경 이력(무엇을, 왜, 언제 바꾸었는가)은 데이터 문서의 마지막 항목으로 남긴다.
실습
실험마다 사용한 데이터 버전·분할·checksum을 자동으로 기록하는 체계를 만들고, 데이터 카드를 작성한다.
- manifest 생성. raw 데이터 디렉터리의 manifest와 데이터셋 해시를 만드는 스크립트를 쓴다. 아래 연습문제의 함수가 그 핵심이다.
- split manifest. 현재 분할을
split.csv(그룹 ID, 집합)로 저장하고 해시를 기록한다. - 실험 기록. 학습 스크립트가 시작할 때 데이터셋 해시, split manifest 해시, 코드 커밋, 설정 파일을 결과 디렉터리에 함께 저장하게 한다. 데이터 해시가 기록된 값과 다르면 경고하게 한다.
- 데이터 카드. 위 표의 항목으로 자신의 데이터셋 카드를 쓴다. "알려진 한계"와 "권장하지 않는 용도"를 각각 두 개 이상 쓴다.
- 추적 확인. 지난 실험 하나를 골라, 기록만으로 그 실험이 쓴 원본·전처리·분할을 찾아 같은 숫자를 다시 만든다.
실패 사례
- 데이터가 조용히 바뀌어 이전 결과를 재현하지 못하는 것. 이름과 날짜는 그대로인데 내용이 바뀌었다. 실험마다 데이터셋 해시를 기록했다면 바로 알아챘다.
- 라이선스를 확인하지 않고 데이터를 공개하는 것. 파생 데이터셋을 내려야 하거나, 논문의 재현 자료를 공개하지 못한다. 수집할 때 이용 조건을 기록한다.
- 분할을 코드와 시드로만 기록하는 것. 데이터가 늘면 같은 시드가 다른 분할을 만든다. split manifest 파일을 저장한다.
- ID를 해시해 익명화했다고 믿는 것. 입력 공간이 작으면 전수 대입으로 되돌릴 수 있다. 키를 쓰는 가명화와 준식별자 검토를 함께 한다.
- 버전이 다른 데이터의 결과를 한 표에 섞는 것. 비교가 성립하지 않는다. 표마다 데이터 버전을 쓴다.
통과 기준
- 임의의 지난 실험에 대해 기록만으로 원본 데이터(manifest 해시)·전처리(코드 커밋)·분할(split manifest 해시)을 찾아 결과를 다시 만든다.
- 동기·구성·수집·한계·용도·라이선스를 포함한 데이터 카드를 작성하고, 라이선스와 민감정보의 처리 근거를 기록한다.
- 연습문제
dataset-manifest-hash의 테스트를 통과한다.
참고 문헌
- Gebru, Morgenstern, Vecchione, Vaughan, Wallach, Daumé III, Crawford (2021). Datasheets for datasets. Communications of the ACM, 64(12), 86–92. — 데이터셋 문서화를 위한 질문 목록