Add unlabeled-corpus extractor (B-2)
build_unlabeled_corpus.py pulls the 34,030 raw-source dialogues that never got speech_act/slot labels (found via the earlier QA cross-check) into their own unlabeled.jsonl, forward-filling the per-dialogue metadata that AI-Hub's CSV export only writes on each dialogue's first row. Verified against the actual data: 176,605 raw - 142,575 labeled = 34,030, matches exactly.
This commit is contained in:
parent
6d9738e153
commit
7307cb56bb
|
|
@ -123,6 +123,8 @@
|
||||||
- [x] PoC #1 기반 코퍼스 확보 및 전처리 파이프라인 — AI-Hub "한국어 SNS 멀티턴 대화" 14.2만 건
|
- [x] PoC #1 기반 코퍼스 확보 및 전처리 파이프라인 — AI-Hub "한국어 SNS 멀티턴 대화" 14.2만 건
|
||||||
확보, `poc/tone-corpus/prepare_dataset.py`로 학습/검증 JSONL 정제 완료 (개인화 검증 자체는
|
확보, `poc/tone-corpus/prepare_dataset.py`로 학습/검증 JSONL 정제 완료 (개인화 검증 자체는
|
||||||
아직 별개로 필요 — `poc-plan.md` "필요한 것" 참고)
|
아직 별개로 필요 — `poc-plan.md` "필요한 것" 참고)
|
||||||
|
- [x] 라벨 없는 원천 대화 34,030건 추가 확보 — `poc/tone-corpus/build_unlabeled_corpus.py`로
|
||||||
|
`unlabeled.jsonl` 정리 (화행/슬롯 라벨 없음, 순수 언어모델링용)
|
||||||
- [x] 응답 초안 생성기 프로토타입 — `poc/tone-corpus/generate_draft.py` (말투 예시 + 대화 맥락 →
|
- [x] 응답 초안 생성기 프로토타입 — `poc/tone-corpus/generate_draft.py` (말투 예시 + 대화 맥락 →
|
||||||
LLM 호출로 초안 생성, 에스컬레이션 케이스는 `[ESCALATE]`로 거절). API 키 없이 코퍼스 실제
|
LLM 호출로 초안 생성, 에스컬레이션 케이스는 `[ESCALATE]`로 거절). API 키 없이 코퍼스 실제
|
||||||
대화로 프롬프트 구성까지만 확인함 — 실제 자동 호출은 `ANTHROPIC_API_KEY` 설정 후 가능
|
대화로 프롬프트 구성까지만 확인함 — 실제 자동 호출은 `ANTHROPIC_API_KEY` 설정 후 가능
|
||||||
|
|
|
||||||
|
|
@ -24,9 +24,13 @@ python3 prepare_dataset.py --input <원본 zip이 있는 디렉토리> --output
|
||||||
- 주제 9종, 정치(3,242건)·경제및사회(4,257건)가 다른 주제(1.7만~2.2만건)보다 훨씬 적음 — 주제별
|
- 주제 9종, 정치(3,242건)·경제및사회(4,257건)가 다른 주제(1.7만~2.2만건)보다 훨씬 적음 — 주제별
|
||||||
균형이 필요하면 샘플링 시 감안할 것
|
균형이 필요하면 샘플링 시 감안할 것
|
||||||
- 화자 연령/성별 필드 노이즈 81건은 정제 과정에서 `null`로 치환 (버리지 않고 필드만 비움)
|
- 화자 연령/성별 필드 노이즈 81건은 정제 과정에서 `null`로 치환 (버리지 않고 필드만 비움)
|
||||||
- **QA 결과**: 원천(CSV) 쪽에는 라벨링된 142,575건 외에 **34,030건이 더 있음** (라벨 없이 텍스트만).
|
- **QA 결과**: 원천(CSV) 쪽에는 라벨링된 142,575건 외에 34,030건이 더 있었음 (라벨 없이 텍스트만).
|
||||||
화행/슬롯 라벨이 필요 없는 순수 언어모델링 목적이라면 이 34,030건도 추가로 끌어올 수 있다 —
|
`build_unlabeled_corpus.py`로 그 34,030건만 뽑아 `unlabeled.jsonl`(63MB)로 정리함 — 화행/슬롯
|
||||||
다만 이번 스크립트는 라벨 JSON만 다루므로 추가 파서가 필요함 (아직 안 만듦)
|
없이 화자·발화 텍스트만 있어서 순수 언어모델링(다음 문장 예측 등)용으로만 쓸 것
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 build_unlabeled_corpus.py --input <원본 zip 디렉토리> --output <출력 디렉토리>
|
||||||
|
```
|
||||||
|
|
||||||
## 응답 초안 생성기 (`generate_draft.py`)
|
## 응답 초안 생성기 (`generate_draft.py`)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Pull the dialogues that exist in the AI-Hub raw source (VS_/TS_ CSV) but
|
||||||
|
were never labeled (VL_/TL_ JSON) -- 34,030 of them, per the --qa check in
|
||||||
|
prepare_dataset.py. No speech_act/slot labels are available for these, so
|
||||||
|
they're only useful for plain language-modeling, not the labeled fields
|
||||||
|
`generate_draft.py`'s prompt doesn't use anyway.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 build_unlabeled_corpus.py --input <dir with the *.zip parts> --output <output dir>
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import csv
|
||||||
|
import glob
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import zipfile
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
VALID_SEX = {"남자", "여자"}
|
||||||
|
|
||||||
|
|
||||||
|
def is_valid_age(value):
|
||||||
|
return bool(value) and value.isdigit() and 10 <= int(value) <= 90 and int(value) % 10 == 0
|
||||||
|
|
||||||
|
|
||||||
|
def clean(value, kind):
|
||||||
|
if not value:
|
||||||
|
return None
|
||||||
|
if kind == "sex":
|
||||||
|
return value if value in VALID_SEX else None
|
||||||
|
return value if is_valid_age(value) else None
|
||||||
|
|
||||||
|
|
||||||
|
def labeled_ids(input_dir):
|
||||||
|
ids = set()
|
||||||
|
for path in glob.glob(os.path.join(input_dir, "*VL_*.zip")) + glob.glob(
|
||||||
|
os.path.join(input_dir, "*TL_*.zip")
|
||||||
|
):
|
||||||
|
with zipfile.ZipFile(path) as z:
|
||||||
|
for name in z.namelist():
|
||||||
|
if name.endswith(".json"):
|
||||||
|
with z.open(name) as f:
|
||||||
|
ids.add(json.load(f)["info"]["id"])
|
||||||
|
return ids
|
||||||
|
|
||||||
|
|
||||||
|
def read_csv_dialogues(input_dir):
|
||||||
|
dialogues = OrderedDict()
|
||||||
|
csv_paths = sorted(
|
||||||
|
glob.glob(os.path.join(input_dir, "*VS_*.zip")) + glob.glob(os.path.join(input_dir, "*TS_*.zip"))
|
||||||
|
)
|
||||||
|
for path in csv_paths:
|
||||||
|
with zipfile.ZipFile(path) as z:
|
||||||
|
for name in z.namelist():
|
||||||
|
if not name.endswith(".csv"):
|
||||||
|
continue
|
||||||
|
with z.open(name) as f:
|
||||||
|
text = io.TextIOWrapper(f, encoding="utf-8-sig")
|
||||||
|
current = None
|
||||||
|
for row in csv.DictReader(text):
|
||||||
|
if row.get("대화ID"):
|
||||||
|
did = row["대화ID"]
|
||||||
|
speakers = {}
|
||||||
|
for letter in ("A", "B", "C"):
|
||||||
|
sid = row.get(f"화자{letter} ID")
|
||||||
|
if not sid:
|
||||||
|
continue
|
||||||
|
speakers[letter] = {
|
||||||
|
"id": sid,
|
||||||
|
"sex": clean(row.get(f"화자{letter} 성별"), "sex"),
|
||||||
|
"age": clean(row.get(f"화자{letter} 연령대"), "age"),
|
||||||
|
}
|
||||||
|
current = {
|
||||||
|
"id": did,
|
||||||
|
"topic": row.get("주제"),
|
||||||
|
"keyword": row.get("키워드"),
|
||||||
|
"speakers": speakers,
|
||||||
|
"utterances": [],
|
||||||
|
}
|
||||||
|
dialogues[did] = current
|
||||||
|
if current is None:
|
||||||
|
continue
|
||||||
|
current["utterances"].append(
|
||||||
|
{"speaker": row.get("발화자", ""), "text": row.get("발화", "")}
|
||||||
|
)
|
||||||
|
return dialogues
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("--input", required=True)
|
||||||
|
ap.add_argument("--output", required=True)
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
os.makedirs(args.output, exist_ok=True)
|
||||||
|
already_labeled = labeled_ids(args.input)
|
||||||
|
all_dialogues = read_csv_dialogues(args.input)
|
||||||
|
|
||||||
|
out_path = os.path.join(args.output, "unlabeled.jsonl")
|
||||||
|
kept = 0
|
||||||
|
with open(out_path, "w", encoding="utf-8") as out:
|
||||||
|
for did, d in all_dialogues.items():
|
||||||
|
if did in already_labeled:
|
||||||
|
continue
|
||||||
|
d["turns"] = len(d["utterances"])
|
||||||
|
out.write(json.dumps(d, ensure_ascii=False) + "\n")
|
||||||
|
kept += 1
|
||||||
|
|
||||||
|
print(f"원천 전체: {len(all_dialogues)}건 / 라벨링됨(제외): {len(already_labeled)}건 / 출력: {kept}건 -> {out_path}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Loading…
Reference in New Issue