[PDF 기반 퀴즈 생성기] 구현하기 (feat. Streamlit, RAG)

2025. 5. 7. 00:40·ML_DL/MUJAKJUNG (무작정 시리즈)
728x90
반응형

1. PDF 파일 업로드

  • streamlit의 'file_uploader'를 통해 파일 업로드
  • type 지정으로, pdf 파일만 업로드 할 수 있도록 제한
uploaded_file = st.file_uploader(label="PDF 업로드", type=["pdf"])

2. OpenAI API KEY 입력

  • streamlit의 'text_input'으로 사용자의 OpenAI API Key 입력
  • models.list()를 호출하여, 입력한 API Key의 유효성 검사
user_api_key = st.text_input(label="Groq API키 입력", type="password", placeholder="sk-******")

def api_key_check(self):
    try:
        OpenAI(api_key=self.api_key).models.list()
        return True
    except AuthenticationError as e:
        return False
    except OpenAIError as e:
        return False

3. 퀴즈 생성

  • pdf 텍스트 추출
    • PyMuPdf 라이브러리를 통해 페이지 별 텍스트 추출 후, 문장으로 구분하여 리스트 생성
class PDFTextExtractor:
    def extract_text(self, file_obj):
        doc = fitz.open(stream=file_obj.read(), filetype="pdf")
        return "\n".join(page.get_text() for page in doc)

    def split_sentences(self, text):
        return [s.strip() for s in re.split(r'(?<=[.!?])\s+', text) if s.strip()]

 

  • 입력한 주제와 관련한 내용 추출
    • pdf 텍스트를 임베딩 모델로 임베딩 생성
    • tiktoken 라이브러리로 openai에서 사용하는 tokenizer로 token 계산
    • max_token을 설정하고, 이 token의 크기를 넘어가는 경우 chunk로 구분
    • 추후 faiss 활용을 위해, np.array 형태로 반환
    • 입력한 주제가 pdf의 내용과 관련이 없는 경우 재입력 요구

class EmbeddingGenerator:
    def __init__(self, api_key, model="text-embedding-3-small", max_tokens=8192):
        self.client = OpenAI(api_key=api_key)
        self.model = model
        self.max_tokens = max_tokens
        self.tokenizer = tiktoken.encoding_for_model(model)

    def get_token_length(self, text):
        return len(self.tokenizer.encode(text))

    def chunk_text(self, text):
        tokens = self.tokenizer.encode(text)
        chunks = [tokens[i:i+self.max_tokens] for i in range(0, len(tokens), self.max_tokens)]
        return [self.tokenizer.decode(chunk) for chunk in chunks]

    def embed_text(self, text):
        if self.get_token_length(text) <= self.max_tokens:
            response = self.client.embeddings.create(input=text, model=self.model)
            return np.array(response.data[0].embedding, dtype=np.float32)
        else:
            chunks = self.chunk_text(text)
            embeddings = [np.array(self.client.embeddings.create(input=c, model=self.model).data[0].embedding, dtype=np.float32) for c in chunks]
            return np.mean(embeddings, axis=0)

    def embed_texts(self, texts):
        embeddings = []
        for text in texts:
            try:
                emb = self.embed_text(text)
                embeddings.append(emb)
            except Exception as e:
                print(f"[오류] '{text[:30]}...': {e}")
                embeddings.append(np.zeros(1536, dtype=np.float32))
        return np.array(embeddings, dtype=np.float32)

 

  • 퀴즈 생성
    • 사용자가 입력한 주제를 pdf 텍스트와 동일하게 임베딩
    • 임베딩 결과를 '퀴즈생성 프롬프트'에 넣어 퀴즈 생성
    • 반환 결과를 json 형식으로 load하고, json 형식이 아닐 경우 에러 반환
try:
    response = client.chat.completions.create(
        model = MODEL_NAME,
        messages = [{"role":"user", "content":prompt}],
        max_tokens=2048,
    )
    content = response.choices[0].message.content
    return json.loads(content)
except AuthenticationError:
    return "유효하지 않은 API 키 입니다. API 키를 확인해주세요!"
except OpenAIError as e:
    return {f'result':'실패 이유 {e}'}

4. 퀴즈 화면 구성 및 정답 확인

  • 퀴즈 출력
    • question, options, answer, explanation 으로 구분되어 반환한 결과를 radio 버튼과 함께 출력

st.session_state['user_selected'] = {}
st.session_state['answers'] = {}
print(st.session_state['quiz'])
for i, q in enumerate(st.session_state['quiz']):
    st.subheader(f"{i+1}번. {q['question']}")
    selected = st.radio(
        label="보기",
        options=q['options'],
        label_visibility="collapsed",
        key=f"q_{i}"
    )
    st.session_state['user_selected'][i] = selected
    st.session_state['answers'][i] = [q['answer'], q['explanation']]

submit = st.button(label="정답제출")
if submit:
    answer_check(st.session_state['user_selected'], st.session_state['answers'])

 

  • 정답확인
    • st.dialog로 정답 확인 화면 제공
    • 세션에 저장해둔 answer와 입력한 radio 버튼의 값을 비교하여 정답률 계산 및 해설 제공

@st.dialog("정답확인", width="large")
def answer_check(user_answer, answer):
    score = sum([u == a[0] for u, a in zip(user_answer.values(), answer.values())])
    total = len(answer)
    st.header(f"정답확인 - {score} / {total} 문제 정답 ({int(score / total * 100)}점)")
    for i in range(len(answer)):
        st.write("내 정답")
        st.write(f"{user_answer[i]}")
        st.write("정답")
        st.write(f"{answer[i][0]}")
        st.write("해설")
        st.write(f"{answer[i][1]}")

5. 초기화

  • 퀴즈 생성시 세션에 저장해준 퀴즈 내용, 주제 등 내용 삭제
def clear_session(keys):
    for key in keys:
        st.session_state.pop(key, None)

if clear_button:
    clear_session(['quiz_make_button', 'quiz', 'user_selected', 'answers', 'context', 'category'])

추가 고민 사항

  • 문제 수 증가
  • LangChain을 활용해, 동일한 코드로 다양한 모델 지원 추가
  • 현재 pdf 텍스트를 문장 부호 기반의 구분에서, 의미 기반의 문장 구분을 통한 RAG 성능 개선
저작자표시 (새창열림)

'ML_DL > MUJAKJUNG (무작정 시리즈)' 카테고리의 다른 글

[PDF 기반 퀴즈 생성기] 개선하기 (시간 단축)  (0) 2025.05.10
텍스트 분할 (Text Spliter)  (0) 2025.05.08
[PDF 기반 퀴즈 생성기] 구성하기  (0) 2025.05.02
[LangChain] 모델 파라미터 설정  (0) 2025.04.09
[LangChain] 챗봇 구성하기  (0) 2025.04.03
'ML_DL/MUJAKJUNG (무작정 시리즈)' 카테고리의 다른 글
  • [PDF 기반 퀴즈 생성기] 개선하기 (시간 단축)
  • 텍스트 분할 (Text Spliter)
  • [PDF 기반 퀴즈 생성기] 구성하기
  • [LangChain] 모델 파라미터 설정
swwho
swwho
일상을 데이터화하다
  • swwho
    하루한장
    swwho
  • 전체
    오늘
    어제
    • 분류 전체보기 (188)
      • ML_DL (39)
        • MUJAKJUNG (무작정 시리즈) (18)
        • 딥러닝 공부하기 (21)
      • 데이터사이언스 (1)
        • EDA (1)
        • 데이터과학을 위한 통계 (0)
      • 데이터엔지니어링 (2)
      • 논문리뷰 (2)
        • Computer Vision (2)
      • Python 활용하기 (12)
      • 코딩테스트 (127)
        • Python (109)
        • MySQL (14)
      • Git (3)
      • MySQL 활용하기 (0)
      • 일상 이야기 (1)
  • 블로그 메뉴

    • 홈
    • 태그
  • 최근 글

  • 250x250
  • hELLO· Designed By정상우.v4.10.3
swwho
[PDF 기반 퀴즈 생성기] 구현하기 (feat. Streamlit, RAG)
상단으로

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.