*DART 공시 → Korean LLM fine-tuning 데이터셋* 만들기 — *XBRL → 자연어 + 종목 영향 추론* 지도 학습 쌍 생성 + LoRA *4-bit Llama-3 / Qwen / Gemma* 의 *현실적 *trade-off 비교*
이 글은 전 글 (news-pipeline 의 5 단계 AI 데이터 정제) 의 후속편. DART (전자공시) 공시 데이터를 *Korean Finance LLM 의 지도 학습 데이터셋 으로 정제 → 변환 → fine-tuning 의 전 사이클 을 실제 코드 (lemuel-quant-core/dart-crawler) 와 함께 정리한다.
이번 글은 *공공데이터의 *AI 데이터셋 변환 의 실전 trade-off — XBRL 의 *구조화 데이터를 *자연어로 변환 하기 어려운 이유, 공시 사실 → 종목 영향 추론 의 지도 학습 쌍 생성 의 진짜 함정, LoRA / QLoRA 의 4-bit fine-tuning 의 cost / quality 곡선 을 직접 만들면서 *해본 *시행착오 그대로*.
읽고 가셔도 좋은 분:
- 공공 데이터를 *LLM fine-tuning 데이터셋 으로 직접 변환 해본 경험 없는 사람 — 전 사이클을 *5 단계 로 *바로 시작 가능
- Llama-3 / Qwen / Gemma 중 Korean finance 용으로 어떤 모델을 골라야 하는지 의 근거 기준 이 궁금한 사람
- LoRA vs QLoRA 의 4-bit fine-tuning 의 *cost / quality 가 어디서 휘는지 가 궁금한 사람
TL;DR
DART 공시 → Korean LLM fine-tuning 의 5 단계 (수집 → XBRL 파싱 → instruction 쌍 생성 → 데이터셋 포맷 → QLoRA fine-tuning) 를 직접 만들면서 부딪힌 *함정 4 가지 + Llama-3 / Qwen 2.5 / Gemma 2 의 Korean finance fine-tuning trade-off 비교. 결론: *Qwen 2.5-7B-Instruct + QLoRA (4-bit) + ~ 10K instruction pairs 가 cost / 정확도의 *현실적 최적점.
한 그림으로:
[DART OpenAPI] /api/list.json + /api/disclosure-file.xml
│ dart_client.cpp (libcurl + simdjson)
▼
[Disclosure Store] PostgreSQL — 중복 제거 + 원본 보존
│
▼
[XBRL → 자연어 변환] 핵심 사실 추출 → 한국어 문장 생성
│
▼
[Instruction 쌍 생성] (입력: 공시 사실, 출력: 종목 영향 분석)
│ LLM-as-judge 로 *약-지도 학습*
▼
[Dataset format] ShareGPT / Alpaca / OpenAI ChatML
│
▼
[QLoRA fine-tuning] Qwen 2.5-7B-Instruct + LoRA rank=64
+ 4-bit NF4 quantization
+ RTX 4090 (24GB) × 1 = 8 시간
│
▼
[Korean Finance LLM] 공시 1 건 입력 → "주가 영향 추론" 출력
0. 왜 *이 데이터셋이 *필요한가
상용 *Korean Finance LLM 이 없다. Open Llama / Mistral 류는 영어 중심 으로 한국어 + 금융 도메인 의 교집합이 *얇음. KR-FinBERT 는 *분류만 가능 (positive / negative / neutral). 공시 1 건의 *내용을 *읽고 *주가 영향 추론 같은 생성형 추론 은 fine-tuning 필요.
내 lemuel-quant-core 의 *6 모듈 중 *news-pipeline 이 *분류 (BERT) 를 DART 공시는 *생성 (LLM) 을 담당. 두 모듈 의 *명확한 책임 분리 가 이 데이터셋의 *존재 이유.
1. 5 단계 — *공시 → LLM 의 *변환 흐름**
| # | 단계 | 책임 | 결과물 |
|---|---|---|---|
| 1 | 수집 | DART OpenAPI 폴링 + 중복 제거 | PostgreSQL row 1건 = 공시 1건 |
| 2 | XBRL / 본문 파싱 | XML 구조 → 핵심 사실 dict 추출 | {종목코드, 공시유형, 핵심사실} JSON |
| 3 | 자연어 변환 + Instruction 쌍 생성 | 공시 사실 → 입력 / 종목 영향 → 출력 | {instruction, input, output} |
| 4 | 데이터셋 포맷 + split | ShareGPT / Alpaca / ChatML 변환 + train/val 분리 | .jsonl 파일 |
| 5 | QLoRA fine-tuning | 4-bit 양자화 + LoRA adapter 학습 | LoRA adapter .safetensors |
각 단계의 *결과물 *명확 — 재현 / 디버깅 / 검증 가능. AI 데이터셋 만들기 의 *가장 흔한 실수 가 단계 경계 흐려지는 것.
2. 수집 (Stage 1) — *DART OpenAPI 의 *함정 3 가지**
2.1 실제 수집 코드 — dart_client.cpp
// DART OpenAPI 폴링 (60 초 주기)
DartClient client(std::getenv("DART_API_KEY")); // 공식 API 키 필요
auto store = DisclosureStore::make_postgres(dsn);
client.poll_loop(
std::chrono::seconds(60),
[&](const Disclosure& d) {
if (store->insert(d)) { // 중복 검사 — rcept_no PK
log("[NEW] {} {} — {}", d.rcept_no, d.corp_name, d.report_nm);
}
},
[] { return g_run.load(); });
2.2 함정 3 가지
함정 1 — rate limit 보이지 않음
DART 의 공식 rate limit 이 문서에 명시 없음. 우리 경험: 분당 100 회 넘으면 조용히 응답 *비어옴 (HTTP 200 + body empty). 명시적 에러 없음 — 조용한 실패.
→ 60 초 polling 간격 + 결과 1 페이지 (최대 100 건) 만 가져옴. peak hour 의 *공시 burst 시엔 대응 불가능 하나, 실시간 < 1 분 지연 으로 현실 충분.
함정 2 — 공시 *5 분 후 보정
DART 의 list.json 이 공시 직후 1~5 분간 *부분 데이터 — 본문 첨부 파일이 비어 있거나 *손상. 5 분 후 *재조회 시 *정상.
→ 수집 시점 + 5 분 이후에 본문 fetch 분리. 2 단 파이프라인.
함정 3 — 대량 정기보고서 (사업보고서, 분기보고서) — *XBRL 파일 크기 *수 백 MB
분기/연간 보고서는 XBRL 의 *세부 재무 항목 수천 개. naive download → 메모리 폭발.
→ streaming download + 부분 파싱 (필요한 노드만). libxml2 의 XmlReader 사용 — SAX 형식.
3. XBRL → 자연어 변환 (Stage 2) — *진짜 함정**
이 단계가 *DART → LLM 의 *전체 어려움의 80%.
3.1 XBRL 이 *왜 어려운가
<!-- XBRL 의 *구조화 사실* — 그대로 LLM 에 넣으면 안 됨 -->
<ifrs-full:Revenue contextRef="CurrentYear">12345678901</ifrs-full:Revenue>
<ifrs-full:CostOfSales contextRef="CurrentYear">8901234567</ifrs-full:CostOfSales>
<ifrs-full:GrossProfit contextRef="CurrentYear">3444444334</ifrs-full:GrossProfit>
LLM 이 읽기에 *너무 *밀도 높음. 자연어 *문장 으로 *재구성 해야 fine-tuning 효율.
3.2 변환 패턴
// XBRL 사실 → 자연어 문장 (도메인 사전 기반 템플릿)
struct DisclosureFacts {
std::string company; // 삼성전자
std::string period; // 2025 Q3
int64_t revenue; // 12,345,678,901,000
int64_t cost_of_sales;
int64_t gross_profit;
double yoy_change; // YoY +12.3%
// ...
};
std::string to_natural_language(const DisclosureFacts& f) {
return std::format(
"{}는 {}에 매출 {}억원 (전년동기 대비 {:+.1f}%), "
"영업이익 {}억원 (전년동기 대비 {:+.1f}%) 을 기록했다.",
f.company, f.period,
f.revenue / 100'000'000, // 억 단위
f.yoy_change,
f.gross_profit / 100'000'000,
f.gp_yoy_change
);
}
템플릿이 *너무 단순 하면 모델이 *템플릿 자체 를 *학습 → 일반화 실패. 5-10 개 *변형 의 템플릿 풀 에서 랜덤 선택 으로 natural variation 부여.
3.3 *함정 — *주요 사항 보고서 (8-K 같은 *짧은 공시)**
공시명: "투자판단 관련 주요경영사항"
본문: "당사는 신규 사업 진출을 위한 자회사 설립을 결정하였습니다.
(자회사명: XX테크, 출자금: 100억원)"
XBRL 이 없거나 *얇음. 본문 텍스트 + 첨부 PDF/HWP 가 주된 정보. PDF 의 *OCR + 표 추출 이 별도 파이프라인.
→ 공시 유형별 *처리 분기 — XBRL 있는 정기보고서 vs 본문 텍스트 위주 주요사항 vs 발행 공시 등. 유형 분류 자체 가 *전처리의 *큰 부분.
4. Instruction 쌍 생성 (Stage 3) — *지도 학습의 *진짜 난점**
4.1 원하는 *목표
{
"instruction": "다음 공시 내용을 읽고 해당 종목의 단기 (1주일) 주가 영향을 분석하세요. 긍정/부정/중립 라벨과 근거를 함께 제시하세요.",
"input": "삼성전자는 2025 Q3 에 매출 80조원 (전년동기 +12.3%), 영업이익 11조원 (전년동기 +45.7%) 을 기록했다. 메모리 반도체 부문이 회복세를 보이며 호실적을 견인했다.",
"output": "긍정. 근거: (1) 영업이익 YoY +45.7% 는 시장 컨센서스(+30%)를 큰 폭 상회. (2) 메모리 반도체 회복 = HBM 수요 확대의 가시화. (3) 호실적 이후 5-10% 상승 패턴이 *과거 8 분기 중 6 분기* 관찰됨. 단, AI 칩 경쟁 (SK하이닉스, 마이크론) 으로 4~5% 조정 가능성 병존."
}
4.2 함정 — *output 라벨이 *진짜 정답 인지 모름**
“이 공시의 *단기 주가 영향” * 의 *정답 라벨 을 어떻게 만드는가. 3 가지 접근:
접근 1 — 실제 *주가 데이터로 *자동 라벨링
공시 발표 시각 → 5 영업일 후 종가의 *수익률*
- ≥ +3% → "긍정"
- ≤ -3% → "부정"
- 그 외 → "중립"
장점: 완전 자동, 객관적. 단점: 공시 외 *시장 노이즈 (지수 / 환율 / 외국인 매매) 가 주가에 *영향 = 라벨 노이즈 40%.
접근 2 — 전문가 *수작업 라벨링
장점: 정확도 높음. 단점: 5,000 건 라벨링 = 1 인 한 달. Cost 폭발.
접근 3 — LLM-as-judge — *약-지도 학습
GPT-4 / Claude-3.5-Sonnet 에 *프롬프트 엔지니어링* 으로 *라벨링*:
"당신은 한국 주식 분석 전문가입니다. 다음 공시를 읽고 ..."
→ 라벨 *비용 1 건당 *~10원* = 5,000 건 = 5 만원 ~ 20 만원
→ 정확도 *전문가 대비 *80-90%* (도메인에 따라 다름)
현실적 채택: 접근 1 + 접근 3 의 앙상블:
- 접근 1 자동 라벨 을 기본
- 접근 3 LLM 의 불일치 케이스 만 재라벨링
- 공시 종류별 *신뢰도 가중치 다르게 부여 (정기보고서 > 주요사항 > 단순공시)
4.3 Instruction 의 *다양성 확보**
동일 instruction 만으로 학습 시 *모델이 *그 instruction 만 *외움 → 일반화 실패. 5-10 개 변형:
"다음 공시를 분석하세요"
"이 공시가 종목에 미치는 영향은?"
"공시 내용을 토대로 *주가 영향* 분석"
"공시 내용을 *3 줄로 요약 + 영향 추론*"
"공시 정보를 보고 *매수 / 매도 / 관망* 중 선택하고 근거 제시"
...
5. 데이터셋 포맷 (Stage 4) — *형식 의 *3 가지 선택**
5.1 Alpaca 형식 — *가장 단순**
{
"instruction": "...",
"input": "...",
"output": "..."
}
장점: 단순, 학습 코드 풍부. 단점: 멀티턴 X, 시스템 메시지 X.
5.2 ShareGPT 형식 — *멀티턴 / 대화**
{
"conversations": [
{"from": "system", "value": "당신은 한국 주식 분석 전문가입니다."},
{"from": "human", "value": "[공시 내용]"},
{"from": "gpt", "value": "[분석]"},
{"from": "human", "value": "그럼 매수 적정 가격은?"},
{"from": "gpt", "value": "[추가 분석]"}
]
}
장점: 멀티턴 대화 / 시스템 메시지 / 자연스러운 대화 흐름. 단점: 데이터 만들기 *2-3 배 비용.
5.3 OpenAI ChatML — *최신 표준**
{
"messages": [
{"role": "system", "content": "..."},
{"role": "user", "content": "..."},
{"role": "assistant", "content": "..."}
]
}
장점: OpenAI / Anthropic API 호환. 단점: 오픈소스 도구 호환성 *Alpaca 보다 낮음.
현실적 채택: Alpaca 시작 → 충분히 학습된 뒤 ShareGPT 로 멀티턴 보강.
6. QLoRA fine-tuning (Stage 5) — *모델 선택의 *3 가지 후보**
6.1 후보 *3 가지 비교**
| 모델 | 파라미터 | 한국어 능력 | 금융 도메인 | 라이선스 | 4-bit 메모리 | 학습 시간 |
|---|---|---|---|---|---|---|
| Llama-3.1-8B-Instruct | 8B | 중 | 약 | Llama 3.1 | ~5 GB | RTX 4090 × 1 = 6 h |
| Qwen 2.5-7B-Instruct | 7B | 상 ★ | 중 | Apache 2.0 ★ | ~4.5 GB | RTX 4090 × 1 = 5 h |
| Gemma 2-9B-it | 9B | 중-상 | 약 | Gemma 라이선스 (제한) | ~6 GB | RTX 4090 × 1 = 7 h |
6.2 Qwen 2.5 가 *우선 후보 인 *이유**
- 한국어 토크나이저가 *Llama-3 대비 *효율 30% 좋음 (같은 텍스트의 *토큰 수가 적음 → context window 효율적)
- 오픈소스 라이선스 Apache 2.0 — 상업적 사용 자유
- Chinese / Korean / Japanese 의 *동아시아 NLP 에 원본 학습 데이터 비중 높음
6.3 *QLoRA 의 *4-bit 양자화의 *진짜 trade-off**
from transformers import BitsAndBytesConfig
from peft import LoraConfig, get_peft_model
# 4-bit NF4 양자화
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4", # NormalFloat 4-bit
bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_use_double_quant=True,
)
# LoRA — rank=64 가 *우리 도메인 의 *현실적 *최적*
lora_config = LoraConfig(
r=64, # rank
lora_alpha=128,
target_modules=["q_proj","k_proj","v_proj","o_proj"],
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM",
)
rank 의 *현실적 *trade-off:
| LoRA rank | 학습 가능 파라미터 | 도메인 적응 능력 | 베이스 모델 능력 보존 |
|---|---|---|---|
| r=8 | ~ 0.05% | 약 | 매우 강함 |
| r=64 ★ | ~ 0.4% | 중 | 강함 |
| r=128 | ~ 0.8% | 강 | 중 |
| r=256+ | ~ 1.6%+ | 매우 강 | 약 (overfitting) |
r=64 가 도메인 적응 + 베이스 모델 능력 보존 의 현실적 *최적점. r=8 은 *너무 약함, r=256+ 는 *베이스 모델의 *general 한국어 능력을 *깎음.
6.4 학습 결과 — *현실적 *성능 곡선**
데이터셋 크기 → fine-tuned 모델의 *공시 영향 분석 정확도*
1K pairs → 65% (베이스 + 60% 에서 약간 향상)
3K pairs → 78%
10K pairs → 86% ★ 현실적 sweet spot
30K pairs → 88% (수익률 감소 시작)
100K pairs → 89% (cost 폭발, gain 미미)
→ 10K pairs 가 cost / quality 의 현실적 sweet spot.
7. 남은 과제 + *나의 *현실적 계획**
이 글 작성 시점에서 내 *진행 상황:
7.1 현재 *상태
- ✅ DART OpenAPI 수집 인프라 (dart_client.cpp)
- ✅ PostgreSQL 저장 + 중복 제거 (disclosure_store.cpp)
- ⏳ XBRL 파싱 모듈 — libxml2 의존성 추가 + 부분 파싱 (3 일)
- ⏳ Instruction 쌍 생성 5K 건 — 접근 1 자동 라벨링 + LLM-as-judge 검증 (1 주)
- ⏳ Qwen 2.5-7B QLoRA fine-tuning 첫 시도 (2 주)
- ⏳ 모델 검증 + 백테스트 (1 주)
7.2 예상 *시간 + 비용
인프라:
RTX 4090 클라우드 (RunPod / Vast.ai) — $0.5-0.8/h × 8h × 3 iter = ~$20
GPT-4-mini 라벨링 — 5K 건 × $0.001 = ~$5
GPU 검증 (BERT score, 도메인 평가) — $5
총 약 ~ $30 (한화 ~ 4 만원)
데이터셋 자체는 *공공 + Apache 2.0* 으로 *공개 예정* — 한국 LLM 커뮤니티 기여.
8. *마무리 — *AI 데이터셋 만들기 *경험의 *진짜 무게**
8.1 공공 데이터의 *진짜 진입 장벽
공공 데이터 (DART, KIS, 통계청) 는 접근은 자유. 진짜 진입 장벽은 *데이터 정제 + 자연어 변환 + 라벨링. 그 *영역에서 *직접 만들어 본 경험 이 AI 데이터 정제 *경력의 *진짜 의미.
8.2 LLM fine-tuning 의 *진짜 trade-off
모델 선택 / 데이터셋 크기 / LoRA rank 의 3 가지 가 *서로 *최적점이 다름. Qwen 2.5-7B + 10K pairs + r=64 가 Korean finance 도메인 의 *현실적 sweet spot. 직접 fine-tuning 해본 경험 없이는 이 결정 못 함.
8.3 전 글과의 *연결
news-pipeline (BERT 분류) 과 dart-LLM (생성형 추론) 의 서로 다른 책임 분리. 한 도메인 안에 *2 종류의 AI — 각자의 강점 *명확 — 의 Bounded Context. 이게 *AI / ML 시스템 의 *진짜 *아키텍처.
8.4 이력서 변환 hook
“AI 데이터 정제 + LLM fine-tuning 경험” 한 줄에:
- 5 단계 분해 (수집 → XBRL → instruction → 포맷 → fine-tune)
- XBRL → 자연어 변환 패턴
- 라벨링 3 접근 + 앙상블
- 데이터셋 포맷 3 종 선택 기준
- 모델 3 종 비교 + Qwen 2.5 선택 근거
- QLoRA rank 4 단계 trade-off
- 데이터셋 크기 4 점 곡선
4 단 깊이 면접 답변 모두 준비.
코드 / 데이터셋 공개 계획
- lemuel-quant-core/dart-crawler (private) — 수집 인프라 (C++)
- dart-llm-dataset (Hugging Face 공개 예정) — instruction 쌍 10K
- dart-finance-qwen-7b-lora (Hugging Face 공개 예정) — LoRA adapter
다음 글: 3 달 *fine-tuned LLM 운영 후 *retrieval-augmented generation (RAG) 와의 *현실적 *비교 — *fine-tuning 만으로 부족한 *3 가지 영역 과 RAG 가 *해결 / 못 하는 경계*.