21장. AI 거버넌스와 보안, 온프레미스 인프라
실습 3 — 개인정보 찾기
상담 메모 40건(cs_records.csv, 지어낸 연습용 데이터)에서 전화번호, 이메일, 주민등록번호, 카드, 계좌를 정규식으로 찾고, 심어 둔 정답과 견줘 정밀도와 재현율을 잽니다. 단순한 패턴과 다듬은 패턴을 견줍니다. 메모에는 주문번호와 날짜처럼 개인정보와 닮은 숫자도 섞여 있습니다.
1단계. 메모와 정답 보기
import json
import re
from pathlib import Path
import pandas as pd
records = pd.read_csv("cs_records.csv")
records["정답"] = records["정답"].map(json.loads)
truth = {(r.번호, p["종류"], p["글"]) for r in records.itertuples() for p in r.정답}
# 결과: 메모 수, 종류마다 심어 둔 개인정보 수, 메모 둘
print(len(records), "건 ·", pd.Series([k for _, k, _ in truth]).value_counts().to_dict())
for m in records["메모"].head(3):
print(" -", m)40 건 · {'전화': 16, '이메일': 16, '계좌': 8, '카드': 8, '주민번호': 8}
- 김하늘 고객 문의. 연락처 010-0000-4487 로 배송 지연 안내 요청. 주문번호 20261019-1835668.
- 이도담 고객이 영수증을 user77@example.com 로 다시 보내 달라고 함. 20261018-2273469 건.
- 본인 확인용으로 주민등록번호 720402-2345678 를 불러 줌(받으면 안 되는 정보). 연락처 010-0000-3681.2단계. 단순한 패턴으로 찾고 채점하기
NAIVE = {
"전화": r"010-\d{4}-\d{4}",
"이메일": r"[\w.]+@[\w.]+",
"주민번호": r"\d{6}-\d{7}",
"카드": r"\d{4}-\d{4}-\d{4}-\d{4}",
"계좌": r"\d+-\d+-\d+",
}
def find_pii(patterns, text):
"""(종류, 찾은 글) 목록"""
return [(kind, m.group(0)) for kind, p in patterns.items() for m in re.finditer(p, text)]
def evaluate(patterns):
found = {(r.번호, k, g) for r in records.itertuples() for k, g in find_pii(patterns, r.메모)}
rows = []
for kind in NAIVE:
t = {x for x in truth if x[1] == kind}
f = {x for x in found if x[1] == kind}
rows.append({"종류": kind, "정답": len(t), "찾음": len(f), "맞음": len(t & f),
"정밀도": len(t & f) / len(f) if f else 1.0, "재현율": len(t & f) / len(t) if t else 1.0})
return pd.DataFrame(rows), found
table, found = evaluate(NAIVE)
# 결과: 종류마다 정밀도(찾은 것 가운데 진짜)와 재현율(진짜 가운데 찾은 것)
print(table.round(3).to_string(index=False)) 종류 정답 찾음 맞음 정밀도 재현율
전화 16 7 7 1.000 0.438
이메일 16 16 8 0.500 0.500
주민번호 8 32 8 0.250 1.000
카드 8 5 5 1.000 0.625
계좌 8 28 8 0.286 1.0003단계. 틀린 것 보고 패턴 다듬기
# 단순 패턴이 잘못 찾은 것(오탐)과 놓친 것
print("오탐:", sorted({g for _, k, g in found - truth})[:6])
print("놓침:", sorted({g for _, k, g in truth - found})[:6])
CAREFUL = {
"전화": r"(?<!\d)010[- ]?\d{4}[- ]?\d{4}(?!\d)", # 하이픈, 빈칸, 붙여 쓴 것 모두
"이메일": r"[\w.]+@[\w-]+(?:\.[\w-]+)+", # 문장 끝의 마침표는 빼고
"주민번호": r"(?<!\d)\d{6}-[1-4]\d{6}(?!\d)", # 앞뒤에 숫자가 붙지 않게
"카드": r"(?<!\d)\d{4}[- ]\d{4}[- ]\d{4}[- ]\d{4}(?!\d)",
"계좌": r"(?<![\d-])\d{3}-\d{2}-\d{6}(?![\d-])", # 이 회사 메모의 계좌 꼴(3-2-6)
}
table2, found2 = evaluate(CAREFUL)
print(table2.round(3).to_string(index=False))
print("남은 오탐:", sorted({g for _, k, g in found2 - truth}), "· 남은 놓침:", sorted({g for _, k, g in truth - found2}))오탐: ['010-0000-2314', '010-0000-2747', '010-0000-3113', '010-0000-3662', '010-0000-3681', '010-0000-4487']
놓침: ['010 0000 2929', '010 0000 3011', '010 0000 3096', '010 0000 5523', '010 0000 6206', '010 0000 6456']
종류 정답 찾음 맞음 정밀도 재현율
전화 16 16 16 1.0 1.0
이메일 16 16 16 1.0 1.0
주민번호 8 8 8 1.0 1.0
카드 8 8 8 1.0 1.0
계좌 8 8 8 1.0 1.0
남은 오탐: [] · 남은 놓침: []단순한 패턴은 다섯 종류 모두에서 문제가 있었습니다. 전화는 하이픈 없는 꼴과 띄어 쓴 꼴을 놓쳐 재현율이 0.438 이었습니다. 주민번호는 주문번호(20261019-1835668)의 뒷부분까지 잡아 32개를 찾았는데 진짜는 8개(정밀도 0.25)였습니다. 계좌는 \d+-\d+-\d+ 가 주민번호와 주문번호, 날짜 조각까지 잡아 정밀도 0.286 이었습니다. 이메일은 문장 끝의 마침표까지 삼켜 user37@example.com. 처럼 찾아서, 글자가 정답과 달라 절반만 맞았습니다. 앞뒤 경계와 꼴의 변형을 넣은 패턴은 다섯 종류 모두 정밀도와 재현율 1.0 이었습니다. 다만 이것은 이 메모들의 꼴에 맞춘 결과입니다. 새로운 꼴의 메모가 들어오면 다시 잽니다.
4단계. 정규식이 못 잡는 것
# 이름은 정답에 넣지 않았지만 개인정보다. 다듬은 패턴으로 찾은 뒤에도 메모에 남은 이름을 센다
names = ["김하늘", "이도담", "박새솔", "최온유", "정가온", "한여름", "윤바다", "서나래"]
left = sum(any(n in m for n in names) for m in records["메모"])
print(f"이름이 들어 있는 메모 {left}/{len(records)}건 · 정규식으로는 못 잡는다")
# 패턴을 저장해 실습 4와 프로젝트가 쓴다
Path("out_pii_patterns.json").write_text(json.dumps(CAREFUL, ensure_ascii=False, indent=1), encoding="utf-8")
print("저장:", Path("out_pii_patterns.json").stat().st_size, "바이트")이름이 들어 있는 메모 24/40건 · 정규식으로는 못 잡는다
저장: 287 바이트이 실습이 뜻하는 것
이 실습은 견습생에게 형광펜 규칙을 주고 서류 더미에서 개인정보를 칠하게 한 뒤 정답지와 견주는 일과 같습니다. 서류와 정답지를 보고(1단계), 단순한 규칙으로 칠해 채점하고(2단계), 잘못 칠한 것과 놓친 것을 보고 규칙을 다듬은 뒤(3단계), 규칙으로는 칠할 수 없는 것을 셉니다(4단계). 여기서 형광펜 규칙이 정규식(regular expression, 규칙적인 글자 꼴을 적는 식), 정밀도(precision)가 "칠한 것 가운데 진짜 개인정보의 비율", 재현율(recall)이 "진짜 개인정보 가운데 칠한 것의 비율"입니다. 메모와 개인정보는 모두 지어낸 가짜 꼴입니다.
| 단계 | 출력 | 뜻 |
|---|---|---|
| 1. 메모와 정답 | 40건, 전화 16, 이메일 16, 계좌 8, 카드 8, 주민번호 8 | 상담 메모 40건에 개인정보 56개를 심어 두었습니다 |
| 2. 전화 | 찾음 7, 재현율 0.438 | 찾은 7개는 모두 진짜였지만(정밀도 1.000) 16개 가운데 7개만 찾았습니다 |
| 2. 이메일 | 맞음 8, 0.500 | 문장 끝의 마침표까지 붙여 잡아, 글자가 정답과 달라 절반만 맞았습니다 |
| 2. 주민번호 | 찾음 32, 정밀도 0.250 | 32개를 칠했는데 진짜는 8개였습니다. 주문번호의 뒷부분까지 주민번호로 칠했습니다 |
| 2. 계좌 | 찾음 28, 정밀도 0.286 | 28개 가운데 8개만 진짜입니다. 숫자-숫자-숫자 꼴이면 다 잡았습니다 |
| 3. 오탐과 놓침 | 오탐 010-0000-2314 등, 놓침 010 0000 2929 등 | 놓친 전화는 띄어 쓴 꼴입니다. 오탐에 전화번호가 보이는 것은 계좌 규칙이 전화번호까지 계좌로 잡았기 때문입니다 |
| 3. 다듬은 패턴 | 다섯 종류 모두 1.0, 남은 오탐: [] · 남은 놓침: [] | 앞뒤 경계와 띄어쓰기 변형을 넣자 56개를 빠짐없이, 틀림없이 찾았습니다 |
| 4. 이름 | 24/40건 | 메모 40건 가운데 24건에 이름이 남아 있습니다. 이름은 꼴이 정해져 있지 않아 정규식으로 못 잡습니다 |
| 4. 저장 | 287 바이트 | 다듬은 패턴을 파일로 남겨 실습 4와 프로젝트가 씁니다 |
한 줄로 줄이면, 개인정보 찾기는 "빠짐없이 잡기"(재현율)와 "엉뚱한 것을 잡지 않기"(정밀도)를 함께 재야 하고, 꼴의 변형을 알아야 둘 다 1.0 에 이른다는 것을 보인 실습입니다. 다만 1.0 은 이 메모 40건의 꼴에 맞춘 결과라, 새로운 꼴이 들어오면 다시 재야 합니다. 이름처럼 정규식이 못 잡는 개인정보도 남습니다. 찾은 개인정보를 가리고 가명으로 바꾸는 일은 실습 4에서 합니다.
코드 설명
- 정답은
(메모 번호, 종류, 글)의 집합입니다. 찾은 것도 같은 꼴로 만들어 교집합(&)과 차집합(-)으로 맞음, 오탐, 놓침을 셉니다. (?<!\d)와(?!\d)는 앞뒤에 숫자가 붙어 있지 않을 때만 맞추는 경계입니다. 주문번호20261019-1835668의 뒷부분이 주민번호로 잡히는 것을 막습니다.010[- ]?\d{4}[- ]?\d{4}의[- ]?는 하이픈, 빈칸, 아무것도 없음을 모두 받습니다.- 계좌 패턴은 이 메모들의 꼴(3-2-6 자리)에 맞춘 것입니다. 실제로는 은행마다 꼴이 달라 목록을 따로 둡니다.
- 이름처럼 정해진 꼴이 없는 개인정보는 정규식으로 잡지 못합니다. 개체명 인식 모델이나 상담 화면에서 받지 않는 설계가 필요합니다.