회사 안에 쌓인 자료의 대부분은 표가 아니라 글이다. 계약서, 발주 메일, 상담 기록, 진료 노트, 사고 보고서. 사람은 읽으면 되지만 시스템은 못 읽는다. 금액으로 정렬하려면 금액이 숫자 칸에 들어 있어야 하고, 만기 30일 전에 알림을 보내려면 만기일이 날짜 칸에 있어야 한다.
정보 추출(information extraction)은 그 간극을 메우는 일이다. 자유롭게 쓰인 글에서 미리 정한 칸에 들어갈 값을 찾아내 채운다. 요약이나 분류와 달리 결과가 사람이 읽는 문장이 아니라 기계가 쓰는 자료라는 점이 이 일의 성격을 전부 정한다.
세 층위로 쌓인다
이 분야는 난이도 순으로 세 층이 있고, 실무에서 필요한 것은 대개 맨 위층이다.
- 개체명 인식. 글 안에서 사람 · 회사 · 날짜 · 금액에 해당하는 조각을 찾아 표시한다. 개체명 인식에서 다룬 그 문제다. "㈜누리테크"가 회사라는 것까지가 이 층의 일이다
- 관계 추출. 찾아낸 조각들 사이의 관계를 붙인다. 팔딘㈜과 ㈜누리테크가 둘 다 회사인 것까지는 1층이 알려 주는데, 누가 발주했고 누가 수주했는지는 관계다
- 사건 추출 · 스키마 채우기. "계약 체결"이라는 사건 하나를 잡고 그에 딸린 칸을 전부 채운다. 발주사 · 수주사 · 계약일 · 금액 · 기간이 한 줄로 묶여야 쓸모가 있다
1층만 해 놓고 멈추면 쓸 데가 거의 없다. 한 문서에 회사 이름이 다섯 개 나오는데 그중 무엇이 우리 칸의 값인지를 모르면, 결국 사람이 다시 읽는다. 목표는 언제나 3층이고, 그래서 스키마를 먼저 정의하는 것이 이 일의 출발점이다.
값을 뽑는 것보다 값을 다듬는 것이 오래 걸린다
원문에서 "4,800만 원"이라는 글자를 찾는 것은 어렵지 않다. 어려운 것은 그것을 48000000이라는 정수로 바꾸는 일이고, 이 정규화(normalization) 단계가 실무 작업량의 절반쯤을 차지한다.
- 수. "4,800만", "사천팔백만", "48백만", "USD 36,000". 단위와 통화가 섞이고, 부가세 포함 여부가 문장에 따로 적혀 있다
- 날짜. "다음 주 화요일", "익월 말일", "계약일로부터 30일 이내". 상대 표현은 기준일이 있어야 풀린다. 그 기준일이 문서 안에 있을 때도 있고 메일 발신 시각일 때도 있다
- 이름. "㈜누리테크", "누리테크", "누리테크 주식회사", "NuriTech Co., Ltd."가 같은 회사다. 이것을 하나의 식별자로 모으는 일을 개체 연결(entity linking)이라 부른다 — 뽑아낸 이름을 사내 거래처 대장의 어느 행에 붙일지 정하는 단계다
- 분류값. 계약 유형이 자유 문장으로 적혀 있는데 우리 시스템은 코드값 열두 개 중 하나만 받는다
여기서 흔한 실수가 뽑기와 다듬기를 한 단계로 시키는 것이다. 모델에게 "금액을 정수로 뽑아 줘"라고 하면 원문에 없는 계산을 하다가 조용히 틀린다. 원문 표현을 그대로 뽑는 단계와 그것을 자료형으로 바꾸는 단계를 나누면, 뒷단계는 대부분 코드로 확정적으로 처리할 수 있고 틀렸을 때 어디가 틀렸는지도 보인다.
근거 위치를 반드시 함께 남긴다
값만 내는 추출기는 실무에 쓰기 어렵다. 계약 금액이 48000000으로 채워져 있을 때, 그것이 맞는지 확인하려면 사람이 문서를 처음부터 다시 읽어야 하기 때문이다.
그래서 칸마다 근거 스팬(evidence span)을 함께 낸다 — 그 값이 원문의 몇 번째 글자에서 몇 번째 글자까지에서 나왔는지다. 이것 하나로 세 가지가 생긴다.
- 검수 화면. 값을 누르면 원문의 그 자리가 강조된다. 확인 시간이 문서당 몇 분에서 몇 초로 준다
- 자동 검증. 뽑았다는 글자가 원문의 그 위치에 실제로 있는지 코드로 확인할 수 있다. 없으면 모델이 지어낸 것이므로 그 칸을 버린다. 이 검사 하나가 환각을 막는 가장 값싼 방어다
- 감사 기록. 나중에 값이 틀렸다는 문의가 오면 왜 그렇게 뽑혔는지를 되짚을 수 있다
언어 모델로 추출할 때는 스팬 대신 원문 인용문을 함께 받는 편이 다루기 쉽다. 위치 숫자를 정확히 세는 것은 모델이 잘 못하지만, 원문 문구를 그대로 옮기는 것은 잘한다. 받은 인용문을 원문에서 검색해 위치를 코드가 찾으면 된다.
세 가지 방법
| 규칙 · 정규식 | 전용 모델 미세조정 | 언어 모델 + 스키마 | |
|---|---|---|---|
| 시작 비용 | 낮다 | 라벨 수천 건이 필요 | 거의 없다 |
| 양식이 고정된 문서 | 아주 잘 맞는다 | 잘 맞는다 | 잘 맞는다 |
| 처음 보는 양식 | 무너진다 | 성능이 떨어진다 | 그런대로 버틴다 |
| 칸을 하나 추가 | 규칙을 새로 쓴다 | 라벨을 다시 모아 재학습 | 스키마 한 줄 추가 |
| 건당 비용 | 무시할 수준 | 낮다 | 높다 |
| 틀린 이유 | 명확하다 | 안 보인다 | 안 보인다 |
| 없는 값 처리 | 확실하다 | 대체로 확실하다 | 지어낼 수 있다 |
셋 중 하나를 고르는 문제가 아니라 어디에 무엇을 쓸지의 문제다. 실무에서 잘 도는 구성은 대개 이렇게 섞여 있다.
- 사업자등록번호 · 계좌번호 · 날짜처럼 형식이 정해진 것은 정규식으로 잡는다. 언어 모델에 맡길 이유가 없고 더 정확하다
- 문서 종류를 가르는 일은 값싼 분류기에 맡긴다
- 문맥을 읽어야 하는 칸만 언어 모델에 보낸다. "이 금액이 총액인가 월 단가인가" 같은 것
- 물량이 크고 양식이 안정되면 언어 모델이 만든 결과로 전용 모델을 학습시켜 옮겨 태운다. 건당 비용이 크게 떨어진다
언어 모델로 할 때 지킬 것
가장 흔한 출발점이 이 방식이므로 실패하는 자리를 따로 적어 둔다.
스키마를 먼저 못 박는다. 자유 형식 JSON을 받아 파싱하면 필드 이름이 흔들리고 자료형이 매번 달라진다. 구조화 출력의 스키마 강제를 쓰면 모양이 어긋나는 문제는 대부분 사라진다.
없는 값을 표현할 자리를 만든다. 스키마에 있는 칸은 채워야 한다고 모델이 판단하면 그럴듯한 값을 지어낸다. 모든 칸을 null 허용으로 두고 "원문에 없으면 반드시 null"이라고 명시한다. 이 한 줄이 없어서 생기는 오류가 실전에서 가장 많다.
뽑은 값이 원문에 있는지 확인한다. 위에서 말한 인용문 검사다. 통과 못 한 칸은 값을 버리고 미확인으로 표시한다.
한 번에 다 시키지 않는다. 칸이 서른 개인 스키마를 한 번에 채우게 하면 뒤쪽 칸의 품질이 눈에 띄게 떨어진다. 성격이 비슷한 칸끼리 묶어 서너 번에 나눠 부르는 편이 정확도도 비용도 낫다 — 각 호출에 필요한 문서 부분만 넣으면 되기 때문이다.
긴 문서는 잘라 넣되, 칸마다 갈 곳이 다르다. 계약 금액은 대개 앞쪽에, 특약은 뒤쪽에 있다. 문서 전체를 매번 넣지 말고 필드 묶음마다 관련 구간을 찾아 넣는다. 스캔본이나 표가 섞인 문서라면 그 앞에 문서 파싱 단계가 먼저 필요하다.
{
"type": "object",
"properties": {
"contract_date": { "type": ["string", "null"], "format": "date" },
"buyer": { "type": ["string", "null"] },
"supplier": { "type": ["string", "null"] },
"amount_krw": { "type": ["integer", "null"] },
"term_months": { "type": ["integer", "null"] },
"evidence": {
"type": "object",
"description": "필드명마다 원문에서 그대로 옮긴 인용문",
"additionalProperties": { "type": "string" }
}
},
"required": ["contract_date", "buyer", "supplier", "amount_krw",
"term_months", "evidence"],
"additionalProperties": false
}
평가는 칸 단위로 한다
문서 하나가 통째로 맞았는지를 세면 숫자가 너무 비관적이고, 어디를 고쳐야 할지도 안 보인다. 칸마다 따로 재는 것이 기본이다.
- 정밀도(뽑은 값 중 맞은 비율)와 재현율(있어야 할 값 중 뽑아낸 비율)을 칸마다 낸다. 금액은 정밀도가, 특약 조항은 재현율이 중요한 식으로 칸마다 무게가 다르다
- 완전 일치와 부분 일치를 나눈다. 회사명에서 "㈜"가 빠진 것과 다른 회사를 뽑은 것은 전혀 다른 오류다. 정규화 후의 값으로 비교하면 이 구분이 대체로 정리된다
- 비어 있어야 하는 칸을 따로 센다. 없는 값을
null로 잘 두는지가 이 일의 핵심 지표 중 하나인데, 보통의 정밀도 · 재현율 계산에서는 이게 잘 안 드러난다 - 사람 검수 비용으로도 재 본다. 실제 운영 지표는 "몇 %가 검수 없이 통과했는가"다. 모델이 낸 신뢰도로 정렬해 위험한 것만 사람에게 보내는 구조라면, 그 정렬이 잘 되는지가 정확도만큼 중요하다
정답 자료는 골든 데이터셋으로 따로 관리한다. 양식이 새로 들어올 때마다 몇 건씩 보태 두면, 스키마를 고칠 때 어디가 망가졌는지 바로 보인다.
한국어에서 더 걸리는 자리
- 조사가 붙는다. "팔딘㈜은"에서 회사명은 "팔딘㈜"까지다. 스팬 경계가 한 글자씩 어긋나는 오류가 꾸준히 나온다. 정규화 단계에서 끝의 조사를 떼는 후처리를 두는 편이 낫다
- 띄어쓰기가 흔들린다. 같은 회사명이 문서마다 붙었다 떨어졌다 한다. 개체 연결에서 공백을 지우고 비교하는 정도의 정리는 기본으로 깐다
- 한자 · 영문 표기가 섞인다. 사명과 사람 이름이 문서 안에서 표기를 바꿔 가며 나온다
- 문서 양식이 기관마다 다르다. 공공 문서의 별지 서식은 같은 이름의 칸이 위치와 표현을 바꿔 가며 존재한다
한국어 처리의 형태소 분석기를 앞에 두면 조사 문제는 상당 부분 정리된다. 다만 언어 모델을 쓰는 구성에서는 형태소 분석 없이도 대체로 맞고, 틀리는 것은 위 후처리로 잡는 편이 단순하다.
정리
- 정보 추출은 글에서 값을 뽑아 정해진 칸에 채우는 일이다. 결과가 사람이 읽는 문장이 아니라 기계가 쓰는 자료라는 점이 다른 모든 것을 정한다
- 개체명 · 관계 · 사건의 세 층 중 실무에 필요한 것은 언제나 맨 위층이다. 스키마를 먼저 정의하는 데서 시작한다
- 값을 찾는 것보다 자료형으로 다듬는 일이 절반이다. 뽑기와 다듬기를 한 단계로 시키지 않는다
- 칸마다 근거를 함께 남긴다. 검수 화면 · 자동 검증 · 감사 기록이 여기서 나오고, 인용문이 원문에 있는지 확인하는 검사가 환각을 막는 가장 값싼 방어다
- 형식이 정해진 것은 정규식, 문맥이 필요한 칸만 언어 모델. 물량이 커지면 전용 모델로 옮겨 태운다
- 언어 모델을 쓸 때는 스키마를 강제하고, 모든 칸에
null을 허용하고, "없으면 null"을 명시하고, 칸이 많으면 나눠 부른다 - 평가는 칸 단위로 정밀도와 재현율을 낸다. 비어 있어야 하는 칸을 따로 세는 것을 빠뜨리지 않는다
- 한국어에서는 조사 경계와 띄어쓰기 흔들림이 꾸준한 오류원이다. 정규화 후처리에서 잡는다
읽어주셔서 감사합니다. 😊

