데보션앱 소개페이지 바로가기
로그인 선택

신고하기

CLOSE
신고사유 (대표 사유 1개)
상세내용 (선택)
0/200
  • 신고한 게시글은 더 이상 보이지 않습니다.
  • 이용약관과 운영정책에 따라 신고사유에 해당하는지 검토 후 조치됩니다.
  • 허위 신고인 경우, 신고자의 서비스 이용이 제한될 수 있으니 유의하시어 신중하게 신고해 주세요.
(이 회원이 작성한 모든 댓글과 커뮤니티 게시물이 보이지 않고, 알림도 오지 않습니다.)

미리보기

커뮤니티

      1,234

      badge 23.06.15

      글 등록

      카테고리를 선택해주세요.

      DEVOTEE를 활성화 시키면
      지금 작성한 커뮤니티 글에 대해 1개의 댓글을 달아줍니다.

      버튼을 누르면 글 수정 시 ChatGPT가 작성한 댓글이 수정됩니다.

      임시저장함에 저장되었습니다. 저장일시 : 2022.5.17 14:29:08

      임시저장함

      제목을 선택하시면 이어서 작성이 가능하며,
      최대 20건까지 저장합니다.
      컨텐츠 유형, 제목, 저장일시, 삭제로 이뤄진 임시저장 목록
      컨텐츠 유형 제목 저장일 삭제

      데보션 블로그 게재 요청

      CLOSE
      • *
      • *

      본인인증

      효율적인 데보션 서비스 이용 및
      고객님의 소중한 개인정보보호를 위해
      본인인증을 진행해주세요. 본인인증 미 진행 시 로그인이 제한됩니다.
      본인인증 실패

      본인인증 로그인에 실패하였습니다.
      회원이 아니시거나 본인인증 등록이
      완료되지 않은 사용자입니다.

      회원정보 연결

      FGD 데이터를 분석해보자(Feat.RAG)

      bykhb 24.07.04
      3,075 8 1
      DEVOTEE 요약
      SK텔레콤에서 고객 행동 데이터를 분석하는 중, FGD 문서의 양이 많아 AI를 활용해 요약하려다 토큰 제한 문제로 RAG 방법을 사용하게 되었습니다. RAG는 고객 인터뷰 텍스트를 잘게 나누어 벡터 DB에 저장하고, 관련 질문에 대해 LLM 모델이 답변을 생성하도록 했으며, 이를 통해 요약과 주요 주제 도출을 성공적으로 수행했습니다. AI 기술의 빠른 발전을 경험하며, 향후 더 깊이 있는 인사이트 도출에 대한 기대감을 갖게 되었습니다.
      DEVOTEE 추천 블로그

      고객 행동의 원인은...!?

      저는 SK텔레콤에서 주로 고객 행동 데이터를(Customer Behavior Data) 분석합니다.

      무선 회선에서 나오는 다양한 고객 데이터를 들여다 본 뒤에, 인사이트를 도출하거나 고객 행동을 예측하거나 마케팅/프로모션이 잘(?) 되었는지 판단하지요.


      고객이 남긴 흔적들은 데이터로 남는다 → 데이터를 들여다본다 → 현황을 파악하고, 원인을 추론하고, 결론을 내린다

      가장 어려운 질문은 고객 행동의 원인을 묻는 것입니다. "이 고객은 왜 이탈 했을까", "요금 상품을 변경한 이유가 뭐지?", "SK텔레콤을 계속 이용하는 이유는?" 등.


      이런 인과관계 물음에 대답하기 위한 여러 분석 방법론들이 있지만(RCT, Casual Graph, DID 등), 정교한 실험 설계는 현실적으로 어려울 수 있고,

      현재 쌓아놓은 데이터에는 생존 편향 등의 Bias가 이미 있는 경우가 많기에 모두가 원하는 명쾌한 해답을 말하기에는 껄끄러운 부분이 있습니다.


      이유를 추론하기 어렵게 하는 대표적인 예시. 교란변수(confounder)와 허위 상관관계(spurious correlation)


      고객에게 직접 물어보자! --> FGD

      가장 깔끔한 해답은 직접 고객에게 물어보는 것입니다. 고객 설문은 문항을 구성하여 설문조사를 하기도 하고, 직접 고객을 초대하여 인터뷰하는 방식도 있습니다.

      직접 인터뷰하는 방식을 FGI 혹은 FGD(Focus Group Interview/Discussion)이라고 부르는데, SK텔레콤에서도 고객에 대한 다양한 궁금증에 대해 FGD를 진행하고 있습니다.

      FGD는 특히 최근의 트렌드나 솔직한 감정/뉘앙스를 들여다봐야 할 때 도움이 됩니다.


      실제 참여해보면 사례금도 받고 꽤 재밌습니다

      이번에 하게 된 분석도 위와 같은 흐름이었습니다. 쿠팡, 해외직구 등을 통한 자급제 단말 구매(OMD) 증가 추세의 이유와 SKT 유지/해지의 관련성을 찾는 것이었지요.

      이를 위해 먼저, DB에 있는 고객 행동 데이터로 OMD 고객에 대해 대략적인 그림을 그려본 뒤에(프로파일링),

      좀 더 직접적으로 고객의 행동을 유추하기 위해 고객 설문을 진행하고, 그동안 진행했던 FGD 중에 관련 있는 조사도 찾아 보았습니다.

      넵, 맞습니다. 여기까진 순조로웠지요~


      다양한 데이터의 조합으로 고객 행동의 원인 유추하기


      도와줘 AI!!

      문제는 읽어야 할 FGD 문서가 너무 많다는 것이었습니다. FGD 문서는 word 파일로, 사회자와 고객간의 대화가 모두 STT(Speech to Text)로 정리 되어 있었고,

      대략 60 페이지의 word 파일이 12개가 있었습니다. 하나씩 읽으면서 정리하자니 시간 낭비 같았고, '대 AI' 시대에 어울리는 업무 방식도 아니지요.

      '그래 이런 업무 시키라고 GPT씨가 있는 것 아니곘어!?'라고 생각하며,

      가장 쉽게 할 수 있는 방법인 chatGPT-GPTS에 word 파일들을 올리고, 간단한 description을 작성한 이후 대화를 해봤습니다.


      도와줘 AI!!

      12개 중 하나의 문서는 확인을 위해 제가 직접 읽었고, 이 문서에 대해 GPTS에 여러 질문을 해보면서 정확도를 확인하였습니다.

      FGD 문서에 등장하는 고객의 수와 진행자의 질문 리스트 등을 물어보니 나쁘지 않은 요약 능력에 '됐다! 쉽게 해결할 수 있겠다'고 생각했으나....

      문서 뒤에 있는 내용으로 갈수록 전혀 대답하지 못함을 알게 되었습니다.


      앞부분의 참여자에 대한 요약은 옳게 한 반면, 인터뷰 뒤쪽에 있었던 멤버십에 대한 내용은 없었다고 하는 GPT씨. 실망이야

      또한 "평상시에 통신사 멤버십 혜택을 얼마나 자주 이용하고 계신가요?" 같은 명시적인 질문에는 쉽게 요약을 하지만,

      "멤버십 사용에 대한 각 고객의 의견을 요약해줘" 같이 전체 요약에 대해서도 전혀 다른 이야기를 생성하는 등 할루시네이션 현상이 나타났습니다.


      openai의 tokenizer를 활용해서 확인한 문서의 token, character 개수 (openai.tokenizer)

      이런 현상에 대한 이유는 아마도 모델의 토큰 제한 떄문으로 보입니다. 하나의 문서에만 36,200의 토큰이 포함되어 있으니까요.

      GPTS에 활용되는 GPT 버전이 무엇인지는 정확히 모르겠지만, 아마 문서의 토큰 개수가 context window의 범위 이상이지 않을까 싶어요.


      여러 문서들을 LLM이 잘 이해하게 할 수는 없을까? --> RAG

      "이래선 안되겠다!!" 라고 외치며(속으로), 다른 방법을 찾아 보았고 RAG(Retrieval Augmented Generation)을 활용하기로 하였습니다.

      RAG은 주로 LLM이 학습하지 못한 최신 정보나 특정 분야에 대한 세부적인 내용을 생성하고자 할 때 활용하는 기술로,

      2020년 Meta의 논문("Retrieval-Augmented Geneartion for Knowledge-Intensive NLP Tasks")에서 발표된 이후 여러 LLM 프로젝트에서 활용 되고 있습니다.


      RAG의 발전, Naive, Advanced, Modular RAG

      RAG에 대해 찾아보니 고급 방법론이 많더군요. Naive RAG, Advanced RAG, Modular RAG 등 패러다임이 빠르게 변하는 것 같은데,

      제게 필요한건 word 문서들을 잘 이해하여 답변하는 LLM. 서비스를 위한 Production Level이나 실시간으로 변하는 정보에 대한 고민은 필요하지 않으니, 최대한 간결하게 구현해보았습니다.


      RAG Work Flow

      ① 기본적인 RAG

      기본적인 RAG을 구현하기 위해 우선 여러 word 파일들을 합쳐주고, 간단한 전처리를 해줍니다.

      Word 파일 합치기 + 텍스트 만들기

      import os
      from docx import Document
      import glob
      
      # 검색할 디렉토리 경로
      directory_path = '...'
      
      # 지정된 디렉토리에서 모든 .docx 파일 찾기
      docx_files = glob.glob(os.path.join(directory_path, '*.docx'))
      
      # 새로운 Document 객체 생성 (모든 문서를 병합할 객체)
      merged_document = Document()
      
      # 각 DOCX 파일에 대해 반복
      for file_path in docx_files:
          # 문서 열기
          document = Document(file_path)
      
          # 현재 문서의 모든 문단에 대해 반복
          for para in document.paragraphs:
              # 새 문서에 문단 추가
              merged_document.add_paragraph(para.text)
      
      # 병합된 문서 저장
      merged_document.save("/merged_document.docx")
      print(f"해당 텍스트는 {len(merged_document.paragraphs)} paragraphs가 있습니다.")
      # 문서의 각 문단을 순회하며 텍스트 출력
      import tiktoken
      import pandas as pd
      from tqdm import tqdm
      
      fgd_text = ''
      for para in tqdm(merged_document.paragraphs):
          text = para.text
          fgd_text += text
      print('문서를 모두 읽어왔습니다!')


      텍스트 만들기 성공

      만들어 놓은 텍스트를 더 작은 조각으로(chunk) 나누는 chunking 작업을 해야 됩니다.

      이유는 언어 모델은 처리할 수 있는 맥락의 양에 한계가 있기 때문에 작은 텍스트 단위인 chunk를 생성하는 것이 필요합니다.

      어떤 방법을(spliter) 선택하고 적절한 chunk size/overlap 찾는 것이 필요한데, 저는 Recursive Chunk 방법을 선택했고, chunk size는 200, chunk overlap은 10으로 지정했습니다.

      여러가지 방법을 시도해보고 결정한 것은 아니고, 인터뷰 문서 특성상 모든 질문에 동일한 길이의 답변이 있는 것이 아니라서 Recursive 방법론이 유용하다고 생각했습니다.

      Chunk 종류 설명

      chunk 종류 설명

      from langchain.text_splitter import RecursiveCharacterTextSplitter
      
      text_splitter = RecursiveCharacterTextSplitter(
            chunk_size = 512
            ,chunk_overlap = 100
      )
      
      documents = text_splitter.split_text(fgd_text)
      documents_content = text_splitter.create_documents(documents)


      chunk로 나누고, Document형태로 만들어준 모습

      이제 chunk로 나뉜 텍스트 뭉치들을 임베딩 모델을 통과시키고, 결과물(임베딩 벡터)을 vector DB에 저장할 차례입니다.

      여기서도 선택의 순간들이 기다리고 있습니다.(네, 인생은 선택의 연속입니다) 어떤 임베딩 모델을 쓸지, vector DB는 무엇을 쓸지 정해야 됩니다.

      임베딩 모델은 다양한 언어를 처리할 수 있는 'text-embedding-3-large' 를, vector DB는 langchain에서 활용 가능하고 무료인 chroma DB를 사용했습니다.

      from langchain_openai import OpenAIEmbeddings
      from langchain.vectorstores import Chroma
      Chroma().delete_collection() # 메모리에 로드된 기존 데이터 삭제
      
      embedding = OpenAIEmbeddings(model='text-embedding-3-large')
      # vectorstore = Chroma.from_texts(documents, embedding = embedding,persist_directory = 'db')
      vectorstore = Chroma.from_documents(documents_content, embedding = embedding,persist_directory = 'db')
      vectorstore.persist()

      *임베딩이나 vector DB에 대해서는 아래 링크가 정리가 잘되어 있는 것 같아요. 예쁜 그림도 있구요. 영어지만, 우리에겐 번역기가 있음을....!

      • https://www.pinecone.io/learn/vector-embeddings/

      • https://www.pinecone.io/learn/vector-database/

      자, 여기까지 기본적인 RAG의 설정은 다 되었습니다. 이제 답변을 생성할 LLM 모델을 결정하고 langchain을 활용해 prompt를 작성하면 됩니다.

      결과를 호가인하면 아래와 같습니다.

      from langchain.prompts import ChatPromptTemplate
      from langchain_openai import ChatOpenAI
      from langchain import hub
      
      from langchain.schema.runnable import RunnablePassthrough
      from langchain.schema.output_parser import StrOutputParser
      
      prompt = hub.pull("rlm/rag-prompt")
      
      llm = ChatOpenAI(model_name='gpt-4-turbo-preview', temperature=0.1)
      
      def format_docs(docs):
          return "\n---\n".join(doc.page_content for doc in docs)
          # join : 구분자를 기준으로 스트링 리스트를 하나의 스트링으로 연결
      
      rag_chain = (
          {"context": retriever | format_docs, "question": RunnablePassthrough()}
          # retriever : question을 받아서 context 검색: document 반환
          # format_docs : document 형태를 받아서 텍스트로 변환
          # RunnablePassthrough(): 체인의 입력을 그대로 저장
          | prompt
          | llm
          | StrOutputParser()
      )




      > 굉장히 지엽적인 정보도 알고 있는 것으로 보아, RAG이 아니었으면 몰랐을 SKT에 대한 정보를 제대로 생성했음을 알 수 있습니다


      ② 조금 더 괜찮은 RAG(Advanced RAG)



      > RAG의 발전 모습 (Retrieval-Augmented Generation for Large Language Models: A Survey)

      기존의 RAG의 검색 정확도를 높이고 높은 품질의 답변을 만들기 위해 여러가지 방법이 제안되었습니다.

      문장 단위로 임베딩을 검색하고 window 형태로 문맥을 활용하거나(sentence-window retriever), 검색 우선순위를 조정하고(Rerank), Context 없이 예상 답변을 먼저 생상하고 임베딩 검색을 하는 등(HyDE).

      이 중에서 저는 langchain으로 가장 구현하기 편한 parent-child retriever 방법론과 BM25를 활용한 Ensemble Retriver를 활용해봤습니다.

      from langchain.storage import InMemoryStore
      from langchain.schema import Document
      from langchain.retrievers import ParentDocumentRetriever
      from langchain.text_splitter import RecursiveCharacterTextSplitter
      
      embedding = OpenAIEmbeddings(model='text-embedding-3-large')
      
      # 큰 청크와 작은 청크 분리
      child_splitter = RecursiveCharacterTextSplitter(chunk_size=150, chunk_overlap=20)
      parent_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=100)
      
      Chroma().delete_collection() # 메모리에 로드된 기존 데이터 삭제
      
      vectorstore = Chroma(embedding_function = embedding)
      store = InMemoryStore()
      
      parent_child_retriever = ParentDocumentRetriever(
          vectorstore = vectorstore,
          docstore = store,
          child_splitter = child_splitter,
          parent_splitter = parent_splitter
      )
      
      rag_chain = (
          {"context": parent_child_retriever | format_docs, "question": RunnablePassthrough()}
          | prompt
          | llm
          | StrOutputParser()
      )
      
      questions = [
          "FGD에 참석한 고객들은 자급제 단말에 대해 어떻게 생각하나요?",
          "자급제 단말은 SKT 유지/해지에 어떤 영향을 주나요?",
          "멤버십 혜택은 SKT 유지/해지에 어떤 영향을 주나요?"
      
      ]
      result = rag_chain.batch(questions)
      for i, ans in enumerate(result):
          ans = ans.replace('.','.\n')
          print(f"Question: {questions[i]}")
          print(f"Answer: {ans}")
          print('---')
      
      from langchain.retrievers import BM25Retriever, EnsembleRetriever
      
      
      bm25_retriever = BM25Retriever.from_documents(pages)
      bm25_retriever.k = 5
      
      vectorstore = Chroma.from_documents(pages, embedding)
      retriever = vectorstore.as_retriever()
      
      ensemble_retriever = EnsembleRetriever(
          retrievers=[bm25_retriever, retriever], weights=[0.5, 0.5]
      )


      Parent-Child Retriver와 Ensamble Retriever의 답변.

      답변의 품질과 내용이 모두 놀라웠습니다. GPTS가 답변을 못했던 멤버십에 대한 답변은 물론 기본 RAG 보다 풍부한 답변을 만들어내었습니다.


      ③ [FGD 데이터 x RAG] output


      RAG을 활용한 FGD 분석 Ouput을 정리해보면 아래와 같습니다.

      <1> 요약 작업


      Langchain에서 제공하는 요약(Summrization) 방법론.

      요약의 방법론은 크게 stuff, map-reduce, refine 방법론이 있는데,

      이미 여러 FGD 파일들이 있는 데이터의 특성상 Map Reduce 방법을 활용하여 전체 텍스트를 요약하였습니다.

      ## 요약
      
      from langchain.chains import MapReduceDocumentsChain, ReduceDocumentsChain
      from langchain_text_splitters import CharacterTextSplitter
      from langchain.chains.combine_documents.stuff import StuffDocumentsChain
      from langchain.chains.llm import LLMChain
      from langchain_core.prompts import PromptTemplate
      from langchain import hub
      
      llm = ChatOpenAI(temperature=0, model = 'gpt-4')
      
      # Map
      map_template = """다음의 문서들이 있다
      {docs}
      이 문서들을 바탕으로, 주요 주제들을 파악하라
      
      답변:"""
      map_prompt = PromptTemplate.from_template(map_template)
      map_chain = LLMChain(llm=llm, prompt=map_prompt)
      
      # Reduce
      reduce_template = """다음은 요약문들이다:
      {docs}
      이를 바탕으로 주요 주제에 대한 최종 통합 요약으로 정제하라.
      
      답변:"""
      reduce_prompt = PromptTemplate.from_template(reduce_template)
      # Run chain
      reduce_chain = LLMChain(llm=llm, prompt=reduce_prompt)
      
      # Takes a list of documents, combines them into a single string, and passes this to an LLMChain
      combine_documents_chain = StuffDocumentsChain(
          llm_chain=reduce_chain, document_variable_name="docs"
      )
      
      # Combines and iteratively reduces the mapped documents
      reduce_documents_chain = ReduceDocumentsChain(
          # This is final chain that is called.
          combine_documents_chain=combine_documents_chain,
          # If documents exceed context for `StuffDocumentsChain`
          collapse_documents_chain=combine_documents_chain,
          # The maximum number of tokens to group documents into.
          token_max=4000,
      )
      
      combine_documents_chain = StuffDocumentsChain(
          llm_chain=reduce_chain, document_variable_name="docs"
      )
      
      reduce_documents_chain = ReduceDocumentsChain(
          combine_documents_chain=combine_documents_chain,
          collapse_documents_chain=combine_documents_chain,
          token_max=4000,
      )
      
      from langchain.text_splitter import RecursiveCharacterTextSplitter
      map_reduce_chain = MapReduceDocumentsChain(
          llm_chain=map_chain,
          reduce_documents_chain=reduce_documents_chain,
          document_variable_name="docs",
          return_intermediate_steps=False,
      )
      
      text_splitter = CharacterTextSplitter.from_tiktoken_encoder(
          chunk_size = 2000, chunk_overlap = 100
          ,separator = '.'
      )
      
      split_docs = text_splitter.split_text(fgd_text)
      documents_content = text_splitter.create_documents(split_docs)


      Map-Reduce 방법으로 FGD 파일 전체를 요약한 결과물(정보의 중요도와 특성상 blur 처리하였습니다)

      결국, 처음 생각했던 대로 FGD 파일들을 AI Tech.를 활용하여 요약하는 것에 성공했습니다. 내용의 퀄리티도 마음에 들었고, 활용할 가치가 매우 높은 자료가 되었습니다.



      <2> SKT 유지/해지 연관 단어 그래프

      추가로, FGD내에서 SKT 유지/해지와 연관 있는 단어의 네트워크도 만들어 보았습니다. langchain의 GraphQAChain을 활용하였고, 정리하면 아래와 같습니다.


      python의 Networkx 라이브러리를 활용하면 network형태로 그릴 수 있습니다.

      자, 이렇게 해서 FGD 데이터와 AI 기술들을 통한 분석은 마무리 되었습니다.

      위 내용말고도 숨겨진 주제 찾기(bertopic)와 자급제 단말 특화 Fine-Tuning 등 다른 범주로도 접근하였는데, 나중에 시간이 되면 정리해놓을 계획입니다.


      저는 앞서도 밝혔듯, RAG이나 언어 모델에 대한 전문가도 아니고, 그저 현재 공개되어 있는 정보들을 활용한 것 뿐인데요,

      그래도 분석하면서 '오..앞으로는 AI를 활용하면 정말 별의 별것들을 할 수 있지 않을까'하는 기대를 갖게 되는 시간이었어요.

      사내에 있는 데이터들을 AI 기술과 함께 여러가지 방향으로 조합해보면 깊이 있는 인사이트를 도출할 수 있을 것 같습니다.


      후기 - AI와 업무


      AI가 향하는 곳은? (이 그림 역시 GPT씨가 그려준 것이라는...~_~)

      사실 위 작업은 3월~4월에 했었던 업무입니다. 네, 가장 최신의 GPT 모델(GPT-4o)가 나오기 이전이지요.

      이 글을 작성하면서, 혹시나해서 FGD 파일을 다시 긁어모아 업로드 하고 GPT-4o와 대화해봤습니다. 결과는 놀라웠습니다.

      GPT-4o가 이미 있었다면 RAG을 활용하지 않았을 정도로요.


      AI 기술의 발전이 빨라도 너무 빠르다는 생각이 듭니다. RAG에 대한 자료를 찾다보니 작년 12월에 나온 survey paper조차 트렌드를 담지 못해 이미 올드해보이는 면이 있더군요(RAFT).

      이렇게 일일이 따라가기 보다는, AI의 헤게모니가 어느 정도 결정된 뒤에 배우고 익히면 좋겠는데, 그때 되면 이미 제가 할 업무는 없을지도 모르겠네요.....(씁쓸)


      Reference

      1. 인과관계 방법론 - the book of why(judear pearl)

      2. FGD/FGI 개념 - https://blog.opensurvey.co.kr/research-tips/textbook-focus-groups-2/

      3. RAG 개념 - https://www.marktechpost.com/2024/04/01/evolution-of-rags-naive-rag-advanced-rag-and-modular-rag-architectures/

      4. chunk 설명 - https://www.pinecone.io/learn/chunking-strategies/

      5. langchain - https://python.langchain.com/v0.2/docs/tutorials/rag/

      6. 대학일기 이미지 - https://series.naver.com/comic/detail.series?productNo=4379566

      댓글 0

      DEVOTEE를 활성화 시키면
      지금 작성한 댓글에 AI가 댓글을 달아줍니다.

      bykhb 님의 최신 블로그

      더보기

      DEVOTEE 추천 블로그

      동영상 기고하기