오픈 소스 인도 주소 파서(Address Parser) 구축하기: 가공되지 않은 MCA/은행 데이터에서 미세 조정된 LLM까지
요약
비구조화된 인도 주소 데이터를 정형화된 JSON 형식으로 변환하기 위한 LLM 미세 조정 파이프라인 구축 과정을 다룹니다. 규칙 기반 태깅과 LLM을 결합한 계층적 레이블링 전략을 통해 대규모 데이터를 효율적으로 처리하는 방법을 소개합니다.
핵심 포인트
- 규칙 기반 태깅과 LLM을 결합한 하이브리드 레이블링 파이프라인 구축
- LLM의 환각을 방지하기 위해 추출된 값이 원문과 일치하는지 검증하는 프롬프트 전략
- 도메인 특유의 데이터 특성(예: Unclassified)을 처리하기 위한 프롬프트 엔지니어링
- 모델 학습보다 정교한 필드 분류 체계(Taxonomy) 설계의 중요성 강조
전체 파이프라인 — 데이터 레이블링 (data labeling), LoRA 미세 조정 (fine-tuning), 프레임워크 간 변환, 그리고 기존 NER 모델과의 벤치마크 — 을 교차 게시합니다. 왜냐하면 흥미로운 버그 대부분은 ML 자체에 있지 않았기 때문입니다.
문제 (The problem)
인도 주소는 구조화되지 않기로 악명이 높습니다. 단 한 줄이 다음과 같이 보일 수 있습니다:
FLAT NO.32, UTTARA TOWERS, MG ROAD GUWAHATI , Kamrup Unclassified AS 781029
집 번호, 건물 이름, 거리, 지역, 구, 주, 그리고 우편번호(pincode)가 일관된 형식 없이 모두 하나의 자유 텍스트 문자열로 뭉쳐져 있습니다. 만약 여러분이 인도의 기업 등록 데이터, 은행 KYC 기록, 또는 배송 물류 관련 업무를 해보셨다면, 이미 이 고통을 알고 계실 것입니다.
저는 위와 같은 문자열을 다음과 같이 변환하는 무언가를 만들기 위해 시작했습니다:
{
"houseNumber": "FLAT NO.32",
"houseName": "UTTARA TOWERS",
...
13개의 필드로 구성되며, 항상 존재하되, 없을 경우 null로 처리됩니다. 결함까지 포함된 전체 파이프라인을 소개합니다.
레이블링 예산 없이 레이블링된 데이터 확보하기
시작점: 서로 매우 다른 형태를 가진 두 가지 소스 — 인도의 MCA (Ministry of Corporate Affairs, 기업부) 기업 등록 데이터와 은행/비즈니스 코레스폰던트(business-correspondent) 지점 기록 — 에서 추출한 437만 개의 가공되지 않은 주소입니다. 레이블은 없습니다.
수동 레이블링은 그 정도 규모를 감당할 수 없으므로, 파이프라인은 계층적으로 구성됩니다:
- 규칙 기반 태깅 (Rule-based tagging) — 정규 표현식 (regex) + 가제티어 (gazetteer) 교차 확인 (India Post의 공식 우편번호 CSV를 통한 pincode → 구/주 조회)을 통해 모든 레코드에 신뢰도 점수를 부여합니다. 신뢰도가 높은 레코드는 "실버 (silver)" 레이블로 자동 수락됩니다.
- 나머지에 대한 LLM 지원 레이블링 — OpenRouter를 통해 LLM에 배치 호출을 수행하며, 추출된 모든 값이 소스 텍스트에서 있는 그대로 (verbatim) 복사되도록 요구하는 시스템 프롬프트를 사용합니다. 만약 모델의 필드 값이 입력값의 부분 문자열 (substring)이 아니라면, 신뢰하는 대신 버립니다. 이것만으로도 환각 (hallucination)의 한 부류를 완전히 제거할 수 있습니다.
- 규모를 키우기 전, LLM 자체의 정확도를 점검하기 위한 소량의 사람 검토 슬라이스 (human-reviewed slice).
실제로 중요했던 미묘한 차이 하나가 있었습니다. MCA 주소에는 "...Kamrup Unclassified AS 781029"와 같이 기계가 생성한 접미사(tail)가 붙어 있는데, 여기서 "Unclassified"는 지명이 아니라 "기록된 하위 행정 구역 분류 없음"을 의미하는 고정된 플레이스홀더(placeholder)입니다. 초기 실행 시 LLM은 "Unclassified"를 subDistrict 값으로 태깅했습니다. 프롬프트(prompt)를 통해 모델에게 이 관례를 명시적으로 가르침으로써 해결했습니다. 사소한 부분이지만, 일반적인 주소 파서(address parser)라면 알 수 없는 도메인 특유의 기묘한 특성(domain quirk)입니다.
또한 강조할 점은 필드 분류 체계(field taxonomy) 설계가 모델 학습보다 더 어렵다는 것입니다. 첫 번째 스키마(Google Maps의 전체 지오코딩 구성 요소 분류 체계, 35개 유형)는 사람이나 LLM 모두 일관되게 라벨링하기에는 너무 세분화되어 있었습니다. 이를 사람이 예외 사례(edge cases)를 고민하며 괴로워하지 않고도 실제로 적용할 수 있는 수준인 13개 필드로 축소했습니다.
미세 조정 (Fine-tuning)
M4 Mac에서 MLX를 통해 학습된 Qwen/Qwen3-0.6B에 대한 LoRA (mlx-lm의 lora 명령 사용 — Apple Silicon에서 작업하기 정말 쾌적하며, CUDA나 bitsandbytes를 씨름할 필요가 없습니다).
rank=16, alpha=32, dropout=0.05
target_modules: q_proj, k_proj, v_proj, o_proj, gate_proj, up_proj, down_proj
28개 레이어 중 16개 미세 조정, 2000회 반복, 약 1.8시간
237개 예시로 구성된 홀드아웃 골드 테스트 세트(held-out gold test set) 결과:
| 지표 (Metric) | 값 (Value) |
|---|---|
| JSON 파싱률 (JSON parse rate) | 100% |
| ... |
필드별 정확도(per-field accuracy)와 완전 일치(exact-match) 사이의 격차가 흥미로운 부분입니다. 불일치 사례를 파헤쳐 보니, 대부분은 모델이 틀린 것이 아니라 스키마의 모호성 (schema ambiguity) 때문이었습니다. locality/subLocality/subsubLocality/village는 모두 "이름이 있는 지역, 다른 세분성"이라는 동일한 개념을 나타내며, 심지어 골드 라벨(gold labels)조차 특정 지명이 어느 범주에 속하는지에 대해 때때로 일관성이 없었습니다 (동일한 문자열이 동시에 locality와 village로 라벨링된 골드 레코드를 발견하기도 했습니다). 이는 모델의 문제가 아니라 분류 체계(taxonomy)의 문제이며, 더 확고한 라벨링 관례 없이는 추가 학습을 아무리 많이 해도 해결되지 않습니다.
MLX 외부에서 실행하기
이 과정은 실제 디버깅 시간의 대부분을 차지한 부분이었으며, 그중 ML(머신러닝)과 관련된 것은 전혀 없었습니다.
mlx-lm은 PEFT와 호환되지 않는 자체 어댑터 (adapter) 형식을 생성합니다. 모델을 (Apple Silicon뿐만 아니라) CUDA/CPU에서 사용할 수 있도록 만들기 위해, 저는 가중치 변환 (weight conversion) 과정을 직접 유도해야 했습니다:
# mlx-lm: lora_a [in_features, r], lora_b [r, out_features], x @ A @ B로 사용됨
# PEFT: lora_A.weight [r, in_features], lora_B.weight [out_features, r]
# 따라서: peft_A = mlx_a.T, peft_B = mlx_b.T
저는 제 유도 과정을 그대로 믿기보다 mlx-lm 자체의 fuse() 소스 코드(delta = (scale * lora_b.T) @ lora_a.T)를 통해 이를 검증했으며, 그 후 수치적으로 확인했습니다. 동일한 15개의 주소를 원래의 MLX 어댑터와 변환된 PEFT 버전 모두에 실행해 보았습니다. 15개 중 13개의 출력이 동일했습니다. 2개의 불일치는 이미 모호하다고 알려진 필드에서 정확히 발생했는데, 이는 변환 버그라기보다는 거의 동등한 softmax 결정 과정에서 백엔드 간의 부동 소수점 (floating-point) 차이로 인한 결과였습니다.
배포, 그리고 의존성 하한선(dependency-floor)과의 두더지 잡기
모델을 Hugging Face에 배포하였고 (두 형식 모두 — 루트에는 PEFT, 하위 폴더에는 MLX), 그 다음 pip install이 가능한 패키지로 래핑(wrap)했습니다: PyPI의 indian-address-parser, GitHub의 소스 코드입니다.
그 후 실제 사용자들이 기존 환경(특히 Anaconda base 환경)에 이를 설치하려고 시도했고, 다음과 같은 문제들이 순차적으로 발생했습니다:
-
peft가transformers.BloomPreTrainedModel을 임포트(import)할 때, 해당 지연 로딩(lazy-loading) 체인이 무조건적으로import tensorflow를 수행합니다. TF/numpy/h5py 설치 버전이 맞지 않는 conda 환경에서는 TensorFlow 기능을 사용하기도 전에 전체 프로세스가 충돌했습니다. 해결책:transformers나peft를 임포트하기 전에os.environ["USE_TF"] = "0"을 설정하여transformers의 TF 감지(TF-detection) 과정을 단축(short-circuit)시킵니다. -
qwen3모델 유형이 인식되지 않음. 확인 결과,transformers는 정확히4.51.0버전부터 Qwen3 지원을 추가했습니다. 이는 실제 PyPI 릴리스를 이분 탐색(bisecting)하여 검증했습니다 (4.50.0: 지원 안 함,4.51.0: 지원함). 제가 설정한 의존성 하한선(>=4.45.0)이 너무 느슨하여, pip가transformers를 업그레이드하는 대신 기존의 오래된 버전을 그대로 남겨두었습니다. -
hf_hub_download() got an unexpected keyword argument 'use_auth_token'.peft<0.18.0버전은 호출자가 요청했는지 여부와 관계없이hf_hub_download에 무조건적으로use_auth_token=None을 전달합니다. 최신huggingface_hub(1.x) 버전에서는 이 폐기된(deprecated) 키워드 인자(kwarg)를 완전히 제거했습니다. 정확한 수정 경계 지점을 찾기 위해 10개 버전에 걸쳐peft의 소스 코드를 이분 탐색했습니다 (0.17.1: 무조건 전달, 0.18.0: 월러스 연산자(walrus operator)를 통한 조건부 전달).
각 해결책은 단순히 그럴듯해 보이는 것이 아니라, _실제로 보고된 실패 사례_를 바탕으로 검증되었습니다. 저는 버그 리포트에 명시된 오래된 의존성 3종 세트가 고정된 가상 환경(venv)을 구축하고, 패치된 패키지를 설치한 뒤, pip가 모든 것을 자동으로 업그레이드하는 것을 확인하고, 실제 추론(inference)을 실행하여 해결되었음을 확정했습니다.
여기서 얻을 수 있는 교훈은 다음과 같습니다: **>=X.Y.Z 하한선은
상황이 안정된 후, 저는 Shiprocket의 open-tinybert-indian-address-ner와 비교했습니다. 이는 BIO 태깅된 토큰 분류 (token classification)를 수행하는 6개 레이어의 TinyBERT로, JSON을 생성하는 0.6B 인과적 언어 모델 (causal LM)과는 근본적으로 다른 아키텍처 (그리고 다른 필드 분류 체계 (field taxonomy))를 가지고 있습니다.
개념적으로 중첩되는 9개의 필드(그들의 house_details ↔ 저의 houseNumber, road ↔ street 등)를 포괄하는 명시적인 필드 매핑을 구축하고, 동일한 237개 예시의 홀드아웃 세트 (held-out set)를 사용하여 두 모델을 평가했습니다:
| 필드 (Field) | 나의 모델 | Shiprocket 모델 |
|---|---|---|
| city | 91.3% | 17.4% |
| ... | ... | ... |
모든 공유 필드에서 더 높은 정확도를 보였지만, Shiprocket의 모델은 주소당 약 240배 더 빠릅니다 (19ms 대 4.6s). 이는 품질의 차이가 아니라 아키텍처의 차이입니다. 즉, 자기회귀 생성 (autoregressive generation) 방식과 대비되는, 단일 순전파 (forward pass)를 수행하는 6개 레이어의 분류기 (classifier)이기 때문입니다. 만약 귀하의 유스케이스 (use case)가 완벽한 정확도보다 높은 처리량 (high-throughput) 및 낮은 지연 시간 (low-latency) 파싱을 필요로 한다면, 다른 모델을 선택하는 것은 정당한 이유가 됩니다. 저는 비교 결과가 한쪽 방향으로만 나타나는 것처럼 꾸미기보다, 이러한 트레이드오프 (tradeoff)를 정직하게 공개하고 싶었습니다.
데이터도 함께 공개합니다
또한 기반이 되는 데이터를 두 개의 HF 데이터셋으로 배포했습니다:
indian-addresses-raw— 437만 개의 레코드로 구성된 전체 미라벨링 코퍼스 (unlabeled corpus)indian-addresses-gold— 4,834개의 스팬 라벨링 (span-labeled)된 학습 예시
원시 코퍼스(raw corpus)를 공개하기 전에 언급할 만한 가치가 있는 사항을 발견했습니다. 은행/BC 주소 기록은 KYC(Know Your Customer) 방식의 데이터이며, 그중 일부에는 실제 고객의 전화번호와 관계 명칭 마커(S/O/D/O/W/O/C/O — 인도 주소 양식의 표준인 "son of"/
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기