12장. 멀티모달 RAG 와 데이터셋 자동 생성
생성 데이터 거르기
문제집을 인쇄하기 전에는 검수를 합니다. 문제와 답이 교과서와 맞는지, 같은 문제가 두 번 나오지 않는지, 글자가 깨지지 않았는지 봅니다. 생성한 질문-답도 학습에 넣기 전에 같은 검수를 거칩니다.
네 카드는 이 장의 거르기 기준입니다. 꼴은 질문과 답이 비어 있지 않은지, 근거는 답의 숫자와 낱말이 원래 절에 있는지, 중복은 뜻이 거의 같은 질문이 이미 있는지, 글자는 한자나 러시아 문자처럼 섞이면 안 되는 글자가 있는지 봅니다. 여기서 교과서와 맞는지 보기가 근거 확인입니다.
기준과 거르는 법
| 기준 | 걸리는 예 | 거르는 법 |
|---|---|---|
| 꼴 | 답이 비었다, 질문이 너무 짧다 | 길이와 빈칸을 본다 |
| 근거 | 답의 "3,000원"이 원래 글에 없다 | 답의 숫자가 모두 원래 글에 있고, 낱말이 절반 넘게 겹치는지 |
| 중복 | "언제까지 교환해요?"와 "교환 기한이 언제예요?" | 질문 임베딩의 코사인 유사도가 0.92 이상이면 하나만 |
| 글자 | "팔ается가요", "꽃鉢" | 한자, 가나, 키릴 문자가 있으면 뺀다 |
근거 확인하기
import re
source = "방수 덮개는 2,000원에 따로 팝니다."
def grounded(answer, source):
"""답의 숫자가 모두 원래 글에 있고, 답의 두 글자 묶음이 절반 넘게 원래 글에 있으면 True"""
nums = re.findall(r"\d[\d,]*", answer)
if any(n.replace(",", "") not in source.replace(",", "") for n in nums):
return False
a = answer.replace(" ", "")
pairs = [a[i:i + 2] for i in range(len(a) - 1)]
return sum(p in source.replace(" ", "") for p in pairs) / max(len(pairs), 1) > 0.5
print(grounded("방수 덮개는 2,000원입니다.", source), grounded("방수 덮개는 3,000원입니다.", source))코드에 나오는 함수
| 코드 | 쉬운 뜻 |
|---|---|
re.findall(r"\d[\d,]*", answer) | \d 는 digit(숫자)이다. 숫자로 시작해 숫자나 쉼표가 이어지는 덩어리를 모두 찾는다 |
n.replace(",", "") | 쉼표를 지워 "2,000"과 "2000"을 같게 본다 |
any(...) | 하나라도 참이면 True 다. 원래 글에 없는 숫자가 하나라도 있으면 바로 False 를 돌려준다 |
a[i:i + 2] | i 번째부터 두 글자를 잘라 낸다. 두 글자 묶음(bigram)이라고 한다 |
sum(p in ... for p in pairs) | 원래 글에 있는 묶음을 1, 없는 묶음을 0 으로 세어 더한다 |
max(len(pairs), 1) | 묶음이 하나도 없을 때 0 으로 나누지 않게 한다 |
> 0.5 | 묶음의 절반을 넘게 원래 글에 있어야 통과한다 |
이 코드가 뜻하는 것
장을 보고 영수증을 확인할 때는 먼저 찍힌 값이 진열대 가격표와 같은지 보고, 그다음 품목 이름이 맞는지 훑어봅니다. 여기서 영수증이 생성한 답, 가격표가 원래 글, 값 확인이 숫자 검사, 품목 이름 훑기가 두 글자 묶음의 겹침 검사입니다. 다만 영수증은 하나라도 틀리면 다시 계산하지만, 이 함수의 글자 검사는 절반만 넘게 겹치면 통과시킵니다.
| 줄 | 하는 일 | 쉽게 말하면 |
|---|---|---|
nums = re.findall(...) | 답에 든 숫자를 모두 뽑는다 | 영수증의 값들 |
if any(...): return False | 원래 글에 없는 숫자가 있으면 바로 탈락 | 가격표에 없는 값이 찍혔다 |
a = answer.replace(" ", "") | 답의 띄어쓰기를 지운다 | 품목 이름의 빈칸 무시 |
pairs = [...] | 답을 두 글자 묶음으로 자른다 | 품목 이름을 조각내 본다 |
return sum(...) / ... > 0.5 | 원래 글에 있는 묶음의 비율이 절반을 넘는지 | 이름이 대체로 맞나 |
실행하면 True False 가 나옵니다. 첫 답은 숫자 2,000 이 원래 글에 있어 통과하고, 둘째 답은 3,000 이 원래 글에 없어 글자를 볼 것도 없이 탈락합니다.
작게 줄여서 눈으로 보기
설명을 위해 지어낸 글로 숫자 찾기와 두 글자 묶음이 어떻게 나오는지 봅니다.
import re
a = "덮개는3,000원"
print(re.findall(r"\d[\d,]*", "덮개는 3,000원, 우산은 12,500원"))
print([a[i:i + 2] for i in range(len(a) - 1)])['3,000', '12,500']
['덮개', '개는', '는3', '3,', ',0', '00', '00', '0원']| 결과 | 읽는 법 |
|---|---|
['3,000', '12,500'] | 쉼표가 든 큰 숫자도 한 덩어리로 찾는다. 숫자 뒤의 "원"은 숫자도 쉼표도 아니라서 덩어리가 거기서 끝난다 |
| 묶음 8개 | 띄어쓰기를 지운 9 글자를 한 칸씩 밀며 두 글자씩 잘랐다. 글자가 n 개면 묶음은 n-1 개다 |
'00' 두 번 | 같은 묶음이 여러 번 나오면 여러 번 센다 |
주의할 점
- 거르기는 틀린 데이터를 줄일 뿐 없애지는 못합니다. 걸러 낸 뒤에도 표본을 사람이 읽어 봅니다.
- 거르면 데이터가 줄고, 어떤 사실은 통째로 빠질 수 있습니다. 걸러 낸 뒤 "시험에 나올 사실이 데이터에 남아 있는지"를 세어 봅니다(실습 6).