�김이언
김이언
🏅 AI 마스터
🌈 지피터스금손

Langchain으로 만든 험한 말 번역기(+Streamlit)

와, RAG가 이렇게 어려운 것이었네요.🤣 소장하고 있는 PDF를 이용한 앱을 계획하였는데 원하는 내용을 정확히 추출하기까지 정말 쉽지 않았어요. 다음은 완성한 험한 말 번역기 “Raw & Real English” 앱의 사용 예시 영상입니다.

목차

  1. 험한 말 번역기 “Raw & Real English” 앱 기획

  2. 프로그램의 주요 기능
    1) 데이터 추출

    2) 리액트 에이전트 생성

    3) 외부 CSS Style 사용

    4) gTTS 사용

  3. 프로그램 작성
    1) 데이터 전처리 및 PDF에서 영어 문장과 한글 문장 추출 함수 정의

    2) 검색기 생성 및 쿼리와 검색기를 이용하여 PDF에서 문장을 검색하는 함수 정의

    3) 모델 설정, 쿼리를 정중한 영어 문장으로 변환하는 함수 정의, 툴 리스트 설정

    4) 에이전트 프롬프트 템플릿 설정, 에이전트 생성, 텍스트를 음성으로 변환하는 함수 정의

    5) Streamlit 앱 설정, 영어 표현 유형과 내용을 표시하는 함수 정의

    6) 'Translate' 버튼을 눌렀을 때 에이전트 실행, 화면에 출력

  4. 사용 화면

  5. 정리


1. 험한 말 번역기 “Raw & Real English” 앱 기획

RAG(Retrieval-Augmented Generation)는 LLM의 한계를 극복하기 위해 사용한다는데, 무엇을 하면 RAG를 이해할 수 있을지 고민하다가 “험한 말 번역기”라는 주제를 선택하였습니다. (재미삼아 정했는데 과정이 험난했어요.🥶)

험한 말 번역기 “Raw & Real English”는 한글 비속어를 영어로 번역하는 앱입니다. LLM은 비속어를 직역해주지 않으므로 Raw Expression에는 RAG를 활용하고, 공적이고 예의바른 Formal Expression에는 GPT4-o 모델을 사용한다는 아이디어입니다.

앱 UI는 Streamlit으로 구현하고, 번역한 영어 표현들은 gTTS(Google Text-to-Speach)를 이용해 오디오로 들어보도록 하였습니다.

RAG로 이용한 문서는 “싸가지 없는 영어책 - 욕으로 배우는 영어 회화”라는 책의 PDF 파일입니다. 저자의 채널에서 회원 전용으로 공유한 것으로 개인 사용만 가능합니다.

한국어로 된 책 표지

2. 프로그램 주요 기능

1) 데이터 추출

  • PyPDFLoader와 FAISS를 이용하는 방법으로 원하는 내용이 정확히 추출되지 않아 PyMuPDF(fitz)와 TF-IDF를 이용하는 것으로 변경했습니다.

  • PyPDFLoader + FAISS

    • 데이터 로딩: PyPDFLoader를 사용하여 PDF 전체 내용을 텍스트로 추출, RecursiveCharacterTextSplitter를 사용하여 긴 텍스트를 작은 청크로 분할

    • 임베딩 생성: OpenAIEmbeddings를 사용하여 각 텍스트 청크를 고차원 벡터로 변환

    • 인덱스 생성: FAISS를 사용하여 벡터들의 인덱스 생성, FAISS는 고차원 벡터의 빠른 유사도 검색을 위한 라이브러리

    • 검색 과정: 쿼리 텍스트도 같은 방식으로 벡터로 변환, FAISS 인덱스를 사용하여 쿼리 벡터와 가장 유사한 벡터들을 검색, 유사한 벡터에 해당하는 원본 텍스트 청크들을 반환

  • PyMuPDF + TF-IDF

    • 데이터 로딩: PyMuPDF(fitz)를 사용하여 PDF에서 영어 표현과 한국어 번역을 정확하게 추출, {"english": ..., "korean": ..., "page": ...} 형식의 구조화된 데이터로 저장

    • 텍스트 처리: 텍스트에 대해 전처리를 수행(특수문자 제거, 소문자화 등)

    • TF-IDF 벡터화: sklearn의 TfidfVectorizer를 사용하여 한국어 텍스트를 TF-IDF 벡터로 변환, TF-IDF는 단어의 중요도를 문서 내 빈도와 전체 문서에서 희소성으로 계산

    • 검색 과정: 쿼리 텍스트도 같은 TF-IDF 벡터라이저를 사용하여 벡터로 변환, 코사인 유사도를 사용하여 쿼리 벡터와 모든 문서 벡터 간의 유사도를 계산, 가장 유사도가 높은 문서들을 선택하여 반환

  • PyMuPDF + TF-IDF 데이터셋이 작을 경우 더 간단하고 직관적이며, 특정 PDF 구조에 맞춰 설계되어 정확할 수 있으나 유연하지 못합니다. PyPDFLoader + FAISS는 다양한 형태의 문서에 일반적으로 적용할 수 있으나 정확도가 떨어질 수 있습니다.

2) 리액트 에이전트 생성

  • 2개의 툴을 만들고, 사용자의 입력에 따라 적절한 툴을 선택하도록 하였습니다.

  • 툴 생성

    • PDF Search: search_pdf 함수를 사용하여 PDF에서 한국어 문장에 대응하는 영어 표현 검색

    • Formal Expression Generator: generate_formal_expression 함수를 사용하여 주어진 문장을 공적이고 예의바른 영어 표현으로 변환

  • 프롬프트 템플릿 정의

    • 에이전트의 행동 방식에 대한 상세한 지시사항으로 프롬프트 템플릿 정의

    • 에이전트가 도구를 사용하는 방법, 결과를 분석하는 방법, 최종 답변을 제시하는 형식 등을 지정

  • 리액트 에이전트 생성

    • create_react_agent 함수를 사용하여 ReAct(Reasoning and Acting) 방식의 에이전트를 생성

    • 이 함수에 언어 모델, 툴 목록, 프롬프트 템플릿을 제공

    • 생성된 에이전트는 사용자의 입력에 따라 적절한 툴을 선택하여 한글 문장을 번역해서 Raw Expression과 Formal Expression을 제공할 수 있습니다. 에이전트는 ReAct 방식을 따라 추론(Reasoning)과 행동(Acting)을 반복하며 최적의 결과를 도출합니다.

3) 외부 CSS Style 사용

  • Streamlit의 UI 디자인을 위해 파일 외부에 CSS 파일을 만들고 링크하여 사용했습니다.

4) gTTS(Google Text-to-Speach) 사용

  • 번역된 영어 표현을 들어볼 수 있도록 gTTS를 사용해 mp3 파일을 만들고, UI에 오디오 플레이어를 넣었습니다.


3. 프로그램 작성

1) 데이터 전처리 및 PDF에서 영어 문장과 한글 문장 추출 함수 정의

import streamlit as st
import fitz  # PyMuPDF
import re
import tempfile

from langchain_openai import ChatOpenAI
from langchain.agents import Tool, AgentExecutor, create_react_agent
from langchain.prompts import PromptTemplate
from typing import List
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
from gtts import gTTS

# OpenAI API 키 설정

# 데이터 전처리 함수
def preprocess_text(text: str) -> str:
    text = re.sub(r'[^\w\s]', '', text)  # 특수 문자를 제거하고 텍스트 정리
    return ' '.join(text.lower().split())  # 텍스트를 소문자로 변환하고 공백을 정리

# PDF에서 페이지 상단의 영어 문장과 한글 문장 추출 함수
def extract_text_from_pdf(pdf_path: str, start_page: int, end_page: int) -> List[Dict[str, str]]:
    document = fitz.open(pdf_path)  # PDF 파일 열기
    english_regex = re.compile(r'[a-zA-Z\s\-\'\!\?]+')  # 영어 문장을 찾기 위한 정규 표현식
    korean_regex = re.compile(r'[가-힣\s\(\)\.\!\?]+')  # 한글 문장을 찾기 위한 정규 표현식
    extracted_data = []  # 추출된 데이터를 저장할 리스트

    for page_number in range(start_page - 1, end_page):  # 시작 페이지부터 끝 페이지까지 반복
        page = document.load_page(page_number)  # 현재 페이지 로드
        text = page.get_text("text")  # 페이지의 텍스트 추출
        lines = text.split('\n')  # 텍스트를 줄 단위로 나누기
        english_line = ''  # 영어 문장을 저장할 변수
        korean_line = ''  # 한글 문장을 저장할 변수
        collecting_english = False  # 영어 문장을 수집 중인지 여부를 나타내는 플래그
        
        for line in lines:  # 각 줄을 반복
            if re.match(r'^Chapter\s\d+\s\d+\s\w+$', line):  # 챕터 헤더 라인을 건너뛰기
                continue
            
            if english_regex.match(line):  # 현재 줄이 영어 문장인지 확인
                if not collecting_english:  # 처음 영어 문장을 찾은 경우
                    english_line = line.strip()  # 영어 문장을 저장
                    collecting_english = True  # 영어 문장을 수집 중으로 설정
                else:  # 이미 영어 문장을 수집 중인 경우
                    english_line += ' ' + line.strip()  # 이전 영어 문장에 이어서 추가
            elif korean_regex.search(line):  # 현재 줄에 한글 문장이 있는지 확인
                korean_line = line.strip()  # 한글 문장을 저장
                collecting_english = False  # 영어 문장 수집 중지
        
        english_line = re.sub(r'[가-힣.,]', '', english_line).strip()  # 영어 문장에서 한글 및 특수 문자 제거
        
        if english_line and korean_line:  # 영어와 한글 문장이 모두 있는 경우
            extracted_data.append({  # 데이터를 딕셔너리로 저장
                "english": english_line,  # 영어 문장
                "korean": korean_line,  # 한글 문장
                "page": page_number + 1  # 페이지 번호
            })

    return extracted_data  # 추출된 데이터 리스트 반환

2) 검색기 생성 및 쿼리와 검색기를 이용하여 PDF에서 문장을 검색하는 함수 정의

# 주어진 PDF 파일 경로를 받아서 검색기를 생성하는 함수
def create_retriever(pdf_path: str):
    extracted_data = extract_text_from_pdf(pdf_path, start_page=10, end_page=184)  
                        # PDF(10~184페이지)에서 텍스트 데이터를 추출
    vectorizer = TfidfVectorizer()  # TF-IDF 벡터라이저 생성
    korean_texts = [preprocess_text(item['korean']) for item in extracted_data]  
                        # 추출된 데이터에서 한글 텍스트를 전처리하여 리스트 생성
    tfidf_matrix = vectorizer.fit_transform(korean_texts)  
                        # 한글 텍스트 리스트를 TF-IDF 매트릭스로 변환
    
    def search_similar(query, top_k=1):  # 주어진 쿼리에 가장 유사한 문장을 찾는 내부 함수 정의
        query_vec = vectorizer.transform([preprocess_text(query)])  
                        # 쿼리 텍스트를 전처리하여 벡터로 변환
        similarities = cosine_similarity(query_vec, tfidf_matrix)[0] 
                        # 쿼리 벡터와 TF-IDF 매트릭스 간의 코사인 유사도 계산
        top_indices = similarities.argsort()[-top_k:][::-1]  
                        # 유사도가 높은 순서대로 정렬하여 상위 인덱스 추출
        return [extracted_data[i] for i in top_indices if similarities[i] > 0.3]  
                        # 유사도가 0.3 이상인 상위 결과 반환
    
    return search_similar  # 검색기 함수를 반환

# 주어진 쿼리와 검색기를 이용하여 PDF에서 문장을 검색하는 함수
def search_pdf(query: str, retriever) -> str:  
    results = retriever(query)  # 검색기 함수를 이용하여 쿼리에 대한 결과를 얻음
    if results:  # 결과가 있는 경우
        doc = results[0]  # 첫 번째 결과를 선택
        return f"Raw Expression: {doc['english']}\nKorean: {doc['korean']}\nPage: {doc['page']}"  
                                           # 결과를 포맷팅하여 반환
    return "No matching expression found." # 결과가 없는 경우 메시지 반환

pdf_path = "assets/english_expressions.pdf" # PDF 파일 경로 설정
retriever = create_retriever(pdf_path)  # 검색기 생성

3) 모델 설정, 쿼리를 정중한 영어 문장으로 변환하는 함수 정의, 툴 리스트 설정

# GPT-4o 모델을 사용한 언어 모델 설정
llm = ChatOpenAI(model="gpt-4o", temperature=0)

# 쿼리를 정중한 영어 문장으로 변환하는 함수
def generate_formal_expression(query: str) -> str:  
    prompt = PromptTemplate(  # 프롬프트 템플릿 생성
        input_variables=["query"],  # 입력 변수로 "query" 설정
        template="Translate the following Korean sentence or English expression into a formal and polite English sentence, maintaining the original meaning: '{query}'"  # 프롬프트 템플릿 내용
    )
    chain = prompt | llm  # 프롬프트와 언어 모델을 체인으로 연결
    result = chain.invoke({"query": query})  # 체인을 호출하여 결과 생성
    return result.content  # 결과의 내용을 반환

# 툴 리스트 생성
tools = [  
    Tool(  # PDF 검색 툴 정의
        name="PDF Search",  # 툴 이름
        func=lambda q: search_pdf(q, retriever),  # 툴 함수
        description="Search for English expressions corresponding to Korean sentences in the PDF"  # 툴 설명
    ),
    Tool(  # 정중한 표현 생성기 툴 정의
        name="Formal Expression Generator",  # 툴 이름
        func=generate_formal_expression,  # 툴 함수
        description="Generate formal and polite English expressions"  # 툴 설명
    )
]

4) 에이전트 프롬프트 템플릿 설정, 에이전트 생성, 텍스트를 음성으로 변환하는 함수 정의

# 에이전트 프롬프트 템플릿 설정
prompt = PromptTemplate.from_template(
    """You are an AI assistant that helps translate Korean sentences to English, providing expressions based on user preferences.
    You have access to the following tools:

    {tools}

    Use the following format:

    Human: <한글 문장>
    Thought: 어떤 도구를 사용해야 할지 고민합니다. 사용자의 요청에 따라 도구를 선택합니다.
    Action: <도구 이름>
    Action Input: <도구 입력>
    Observation: <도구 출력>
    Thought: 결과를 분석하고 다음 단계를 결정합니다.
    ... (필요한 만큼 반복)
    Final Answer: 다음 형식으로 결과를 제시합니다:
    * Raw Expression: <PDF에서 찾은 원래의 영어 표현>
      예문: <영어로  사용 예시>
      <원래 표현의 의미와 사용 상황에 대한 한글 설명>
      
    * Formal Expression: <공식적인 상황에서 사용할  있는 영어 표현>
      예문: <영어로  사용 예시>    
      <공식적인 표현의 의미와 사용 상황에 대한 한글 설명>

    Raw Expression을 제시할 때는 PDF에서 찾은 표현을 그대로 사용하세요. 비속어나 강한 표현이 포함되어 있더라도 수정하지 마세요.
    설명은 반드시 한글로 제공하세요.

    사용자의 요청에 따라 Raw Expression만, Formal Expression만, 또는 둘 다 제공해야 합니다.

    The available tool names are: {tool_names}

    Begin!

    Human: {input}
    {agent_scratchpad}"""
)  # 사용자에게 제공할 프롬프트 템플릿 정의

# 에이전트 생성
agent = create_react_agent(llm, tools, prompt)  # 반응형 에이전트 생성
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True, handle_parsing_errors=True)  # 에이전트 실행기 생성, 디버깅을 위해 verbose 모드 활성화, 파싱 오류가 발생했을 때 다시 시도

# 텍스트를 음성으로 변환하는 함수
def text_to_speech(text: str) -> str: 
    tts = gTTS(text=text, lang='en')  # gTTS 모듈을 사용하여 텍스트를 음성으로 변환
    with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as fp:  # 임시 파일 생성
        tts.save(fp.name)  # 변환된 음성을 임시 파일에 저장
        return fp.name  # 임시 파일 반환

5) Streamlit 앱 설정, 영어 표현 유형과 내용을 표시하는 함수 정의

# Streamlit 앱 설정
st.set_page_config(layout="wide", page_title="Raw & Real English")  # 페이지 레이아웃과 제목 설정
st.markdown(f'<style>{open("mystyle.css").read()}</style>', unsafe_allow_html=True)  # 외부 CSS 파일을 읽어와 스타일 적용

# 사이드바에 앱 설명과 사용 방법 설정
st.sidebar.title("😁 Raw & Real English")  # 사이드바의 제목
st.sidebar.markdown("""
한글 문장을 두 가지 유형의 영어 표현으로 번역합니다.

> *Raw Expression*: 일상적이고 때로는 거친 표현을 포함한 실제 사용되는 표현
*Formal Expression*: 공식적인 상황에서 사용할 수 있는 정중한 표현
#                    
**사용 방법**
1. 입력창에 한글 문장을 입력하세요.
2. 원하는 표현 유형(Raw, Formal, 또는 둘 다)을 선택하세요.
3. 'Translate' 버튼을 클릭하세요.
4. 결과로 나온 영어 표현을 보고 들어보세요.

\* Raw Expression에는 비속어나 강한 표현이 포함될 수 있으니 주의하세요.
""")  

# 입력창 설정
korean_sentence = st.text_input("한글 문장을 입력하세요: ")  # 사용자가 한글 문장을 입력받는 텍스트 입력창

# 체크박스 설정
col1, col2 = st.columns(2)  # 두 개의 열로 나누기
with col1:
    use_pdf = st.checkbox("Raw Expression", value=True)  # 첫 번째 열에 Raw Expression 체크박스
with col2:
    use_formal = st.checkbox("Formal Expression", value=True)  # 두 번째 열에 Formal Expression 체크박스

# 영어 표현 유형과 내용을 표시하는 함수 정의
def display_expression(expression_type, content):  
    st.markdown(f"<div class='divider'>", unsafe_allow_html=True)  # 구분선 표시
    st.markdown(f"<div class='expression-title'>🗨️ {expression_type}</div>", unsafe_allow_html=True)                            
                                                # 표현 유형 제목 표시
    col1, col2 = st.columns([7, 3])  # 표현 내용을 표시할 두 개의 열 설정
    with col1:
        st.markdown(f'<span class="highlight">{content[0]}</span>', unsafe_allow_html=True) 
                                                # 첫 번째 열에 강조된 표현 내용 표시
    with col2:
        audio_file = text_to_speech(content[0])  # 첫 번째 표현 내용을 음성으로 변환
        st.audio(audio_file, format='audio/mp3')  # 음성 파일 재생
    for line in content[1:]:  # 나머지 내용을 반복하여 표시
        st.write(line)  # 나머지 내용 표시
    st.markdown("</div>", unsafe_allow_html=True)  # 구분선 닫기

6) 'Translate' 버튼을 눌렀을 때 에이전트 실행, 화면에 출력

# 'Translate' 버튼을 눌렀을 때
if st.button("Translate"): 
    if korean_sentence:  # 한글 문장이 입력되었는지 확인
        # 사용자 선택에 따라 에이전트에게 지시 생성
        if use_pdf and use_formal:  # Raw Expression과 Formal Expression 둘 다 선택된 경우
            user_instruction = f"'{korean_sentence}' 이 문장에 대해 Raw Expression과 Formal Expression을 모두 제공해주세요."
        elif use_pdf:  # Raw Expression만 선택된 경우
            user_instruction = f"'{korean_sentence}' 이 문장에 대해 Raw Expression만 제공해주세요."
        elif use_formal:  # Formal Expression만 선택된 경우
            user_instruction = f"'{korean_sentence}' 이 문장에 대해 Formal Expression만 제공해주세요."
        else:  # 아무 옵션도 선택되지 않은 경우
            st.warning("적어도 하나의 옵션을 선택해주세요.")  # 경고 메시지 표시
            st.stop()  # 실행 중지

        # 에이전트 실행: 지시를 전달하고 결과 받기
        result = agent_executor.invoke({"input": user_instruction})

        output_lines = result['output'].split('\n')  # 결과를 줄 단위로 분할
        current_expression = None  # 현재 표현 유형 초기화
        expression_content = []  # 표현 내용을 저장할 리스트 초기화

        for line in output_lines:  # 결과의 각 줄을 반복
            if line.startswith('* Raw Expression:') or line.startswith('* Formal Expression:'):  
                                        # 표현 유형을 나타내는 줄인지 확인
                if current_expression:  # 현재 표현 유형이 있는 경우
                    display_expression(current_expression, expression_content) 
                                        # 표현 내용을 화면에 표시
                current_expression, english_sentence = line.split(':', 1)  # 표현 유형과 영어 문장 분리
                current_expression = current_expression.strip('* ')  # 표현 유형 정리
                english_sentence = english_sentence.strip()  # 영어 문장 정리
                expression_content = [english_sentence]  # 표현 내용을 리스트로 초기화
            elif line.strip():  # 빈 줄이 아닌 경우
                expression_content.append(line)  # 표현 내용에 추가

        # 마지막 표현 출력
        if current_expression:  # 현재 표현 유형이 있는 경우
            display_expression(current_expression, expression_content)  # 표현 내용을 화면에 표시

    else:
        st.warning("한글 문장을 입력해주세요.")  # 한글 문장이 입력되지 않은 경우 경고 메시지 표시

4. 사용 화면

  • 시작 화면

한국사이트 스크린샷
  • ‘쓸모없는’을 입력하고 ‘Translate’ 버튼을 눌러 실행한 화면과 PDF 원본 페이지

한국어 텍스트가 있는 웹사이트의 스크린샷

5. 정리

  • 무엇보다도 데이터 전처리와 정규표현식의 중요성을 다시금 느꼈습니다. 데이터만 잘 정리되어 있다면 원하는 내용의 추출이 쉽고, 이후 작업의 효율이 증가합니다.

  • 사용한 PDF 문서의 구조와 원하는 추출 형태에 맞추어 PyMuPDF + TF-IDF를 사용했습니다. 그 과정이 매우 복잡하고 어려웠으나, 결과적으로 정확하게 데이터 추출을 할 수 있게 되어 앱 제작을 마쳤습니다. 그렇다면 RAG로 사용할 문서 구조와 추출 목적에 맞추어 매번 데이터 처리를 하는 것이 답일까요? 좀 더 유연한 방법이 있는 것이 아닐까요?

  • 사례를 작성할 때마다 이것이 최선인지 아닌지 모르는 상태에서 마무리를 하는 것이 아쉽습니다. 아직 모르는 부분이 너무 많기 때문이겠죠~ Langchain을 좀 더 깊이 알고 싶습니다. 다음엔 LangGraph를 이용해보려고요!

  • 재미로 시작한 주제였는데, 비속어가 많이 등장해 사용 예시 동영상을 만들거나 스크린샷을 찍기가 힘드네요.😅 재미로 봐주시기 바랍니다~


#11기랭체인

15
4개의 답글

👉 이 게시글도 읽어보세요