23.06.15
DEVOTEE를 활성화 시키면
지금 작성한 커뮤니티 글에 대해 1개의 댓글을 달아줍니다.
버튼을 누르면 글 수정 시 ChatGPT가 작성한 댓글이 수정됩니다.
| 컨텐츠 유형 | 제목 | 저장일 | 삭제 |
|---|
본인인증 로그인에 실패하였습니다.
회원이 아니시거나 본인인증 등록이
완료되지 않은 사용자입니다.
저는 SK텔레콤에서 주로 고객 행동 데이터를(Customer Behavior Data) 분석합니다.
무선 회선에서 나오는 다양한 고객 데이터를 들여다 본 뒤에, 인사이트를 도출하거나 고객 행동을 예측하거나 마케팅/프로모션이 잘(?) 되었는지 판단하지요.
고객이 남긴 흔적들은 데이터로 남는다 → 데이터를 들여다본다 → 현황을 파악하고, 원인을 추론하고, 결론을 내린다
가장 어려운 질문은 고객 행동의 원인을 묻는 것입니다. "이 고객은 왜 이탈 했을까", "요금 상품을 변경한 이유가 뭐지?", "SK텔레콤을 계속 이용하는 이유는?" 등.
이런 인과관계 물음에 대답하기 위한 여러 분석 방법론들이 있지만(RCT, Casual Graph, DID 등), 정교한 실험 설계는 현실적으로 어려울 수 있고,
현재 쌓아놓은 데이터에는 생존 편향 등의 Bias가 이미 있는 경우가 많기에 모두가 원하는 명쾌한 해답을 말하기에는 껄끄러운 부분이 있습니다.
이유를 추론하기 어렵게 하는 대표적인 예시. 교란변수(confounder)와 허위 상관관계(spurious correlation)
가장 깔끔한 해답은 직접 고객에게 물어보는 것입니다. 고객 설문은 문항을 구성하여 설문조사를 하기도 하고, 직접 고객을 초대하여 인터뷰하는 방식도 있습니다.
직접 인터뷰하는 방식을 FGI 혹은 FGD(Focus Group Interview/Discussion)이라고 부르는데, SK텔레콤에서도 고객에 대한 다양한 궁금증에 대해 FGD를 진행하고 있습니다.
FGD는 특히 최근의 트렌드나 솔직한 감정/뉘앙스를 들여다봐야 할 때 도움이 됩니다.
실제 참여해보면 사례금도 받고 꽤 재밌습니다
이번에 하게 된 분석도 위와 같은 흐름이었습니다. 쿠팡, 해외직구 등을 통한 자급제 단말 구매(OMD) 증가 추세의 이유와 SKT 유지/해지의 관련성을 찾는 것이었지요.
이를 위해 먼저, DB에 있는 고객 행동 데이터로 OMD 고객에 대해 대략적인 그림을 그려본 뒤에(프로파일링),
좀 더 직접적으로 고객의 행동을 유추하기 위해 고객 설문을 진행하고, 그동안 진행했던 FGD 중에 관련 있는 조사도 찾아 보았습니다.
넵, 맞습니다. 여기까진 순조로웠지요~
다양한 데이터의 조합으로 고객 행동의 원인 유추하기
문제는 읽어야 할 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의 범위 이상이지 않을까 싶어요.
"이래선 안되겠다!!" 라고 외치며(속으로), 다른 방법을 찾아 보았고 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을 구현하기 위해 우선 여러 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 종류 설명
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의 발전 모습 (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 보다 풍부한 답변을 만들어내었습니다.
RAG을 활용한 FGD 분석 Ouput을 정리해보면 아래와 같습니다.
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.를 활용하여 요약하는 것에 성공했습니다. 내용의 퀄리티도 마음에 들었고, 활용할 가치가 매우 높은 자료가 되었습니다.
추가로, FGD내에서 SKT 유지/해지와 연관 있는 단어의 네트워크도 만들어 보았습니다. langchain의 GraphQAChain을 활용하였고, 정리하면 아래와 같습니다.
python의 Networkx 라이브러리를 활용하면 network형태로 그릴 수 있습니다.
자, 이렇게 해서 FGD 데이터와 AI 기술들을 통한 분석은 마무리 되었습니다.
위 내용말고도 숨겨진 주제 찾기(bertopic)와 자급제 단말 특화 Fine-Tuning 등 다른 범주로도 접근하였는데, 나중에 시간이 되면 정리해놓을 계획입니다.
저는 앞서도 밝혔듯, RAG이나 언어 모델에 대한 전문가도 아니고, 그저 현재 공개되어 있는 정보들을 활용한 것 뿐인데요,
그래도 분석하면서 '오..앞으로는 AI를 활용하면 정말 별의 별것들을 할 수 있지 않을까'하는 기대를 갖게 되는 시간이었어요.
사내에 있는 데이터들을 AI 기술과 함께 여러가지 방향으로 조합해보면 깊이 있는 인사이트를 도출할 수 있을 것 같습니다.
AI가 향하는 곳은? (이 그림 역시 GPT씨가 그려준 것이라는...~_~)
사실 위 작업은 3월~4월에 했었던 업무입니다. 네, 가장 최신의 GPT 모델(GPT-4o)가 나오기 이전이지요.
이 글을 작성하면서, 혹시나해서 FGD 파일을 다시 긁어모아 업로드 하고 GPT-4o와 대화해봤습니다. 결과는 놀라웠습니다.
GPT-4o가 이미 있었다면 RAG을 활용하지 않았을 정도로요.
AI 기술의 발전이 빨라도 너무 빠르다는 생각이 듭니다. RAG에 대한 자료를 찾다보니 작년 12월에 나온 survey paper조차 트렌드를 담지 못해 이미 올드해보이는 면이 있더군요(RAFT).
이렇게 일일이 따라가기 보다는, AI의 헤게모니가 어느 정도 결정된 뒤에 배우고 익히면 좋겠는데, 그때 되면 이미 제가 할 업무는 없을지도 모르겠네요.....(씁쓸)
인과관계 방법론 - the book of why(judear pearl)
FGD/FGI 개념 - https://blog.opensurvey.co.kr/research-tips/textbook-focus-groups-2/
chunk 설명 - https://www.pinecone.io/learn/chunking-strategies/
langchain - https://python.langchain.com/v0.2/docs/tutorials/rag/
대학일기 이미지 - https://series.naver.com/comic/detail.series?productNo=4379566
DEVOTEE를 활성화 시키면
지금 작성한 댓글에 AI가 댓글을 달아줍니다.