LLM과 추천 시스템을 결합해 설명가능성(Explainability) 제공하기(Feat. LangChain, GPT-4o)
포스팅 개요
최근 OpenAI에서 GPT-4o 등이 나오는 등 LLM(Large Language Models)의 발전은 계속 진행되고 있습니다. 그러면서 동시에 LLM과 다양한 application, 다양한 domain, 다양한 downstream task와 어떻게 연계할 수 있는가도 지속적으로 연구되고 있는데요. 본 포스팅은 추천 시스템(Recommendation System) 영역에서 LLM을 어떻게 연결시킬 수 있는지를 고민합니다. 그리고 추천 시스템 연구에서 가장 중요하게 고민되고 있는 설명가능성(Explainbility)를 해결하기 위해 LLM과 결합해하여 설명가능성을 부여하는 방법에 대해 알아보고 파이썬(Python) 코드로 예제(example)를 구현해보겠습니다.
본 포스팅 외에도 저는 이전에 OpenAI ChatGPT API를 활용한 추천 시스템 포스팅을 작성한 적이 있습니다. 비록 시간이 꽤나 지난 글이지만, 해당 글의 확장 버전이라고 생각해주시면 감사하겠습니다.
본 포스팅에서 참고한 자료는 다음과 같습니다.
- https://grouplens.org/datasets/movielens/
- https://github.com/langchain-ai/langchain
- https://platform.openai.com/docs/models/gpt-4o
본 포스팅에서 사용한 코드(code)는 아래 github에 10번으로 올려두었습니다.
포스팅 본문
이번 포스팅은 제가 생각하는 LLM과 추천 시스템을 결합하는 연구 방법에 대해서 간단히 알아보고 LLM이 추천 시스템에 활용될 수 있는 다양한 방법 중 추천 시스템에 설명가능성(Explainability of the recommender system)을 부여하는 방법을 파이썬(Python) 코드 예제(example)로 구현해보고자 합니다.
LLM과 추천 시스템을 결합하는 연구 방향
최근 LLM과 추천 시스템을 같이 활용하는 논문들을 읽으면서, 관련 연구의 추세는 크게 다음 2가지로 분류되고 있다고 생각됩니다.
1. LLM(Large Language Models)를 추천 시스템으로 사용하는 방법
이 방법은 LLM을 추천 시스템 모델로써 기능을 수행하도록 하는 방법입니다. 자연어 기반 쿼리를 활용해서 LLM에 직접 추천을 요청하는 방법이죠. LLM을 파인튜닝(Fine-tuning)해서 사용하기도 합니다. 사용자와 아이템간의 interaction을 텍스트 prompt로 변환해 LLM을 학습시키는 방법이죠.
2. LLM이 가지고 있는 추론 능력과 방대한 지식으로 추천 시스템을 강화하는 방법
이 방법은 LLM이 가지고 있는 reasoning ability(추론 능력)과 모델이 가지고 있는 방대언 지식을 활용해 추천 시스템을 강화하는 방법입니다. 예를 들어서, 메타 데이터를 더 다양하게 만들어주거나, 요약 정보를 만들어주거나, LLM에서 나오는 embedding을 활용한다던가 등 다양하게 활용하는 것이죠.
추천 시스템과 설명가능성(Recommender System and Explainability)
추천 시스템에서 설명가능성(Explainability)는 사용자가 왜 이 아이템이 나에게 추천 되었는지를 이해할 수 있도록 도와주는 방법입니다. 이는 추천 시스템과 추천 시스템을 제공하는 서비스 입장에서 사용자에게 어떤 신뢰(Trust)를 주기도 합니다. 사용자가 이 추천을 받았던 것에서 reasonable하게 이해한다면, 서로간의 신뢰감을 쌓을 수 있기 때문이죠.
하지만, 추천 시스템에서 설명가능성을 제공하는 것은 쉬운 문제가 아닙니다. 어떻게 설명을 제공할 것인가? 우리의 설명이 사용자가 이해할 수 있는가? 등 여러가지 고려해야 할 것들이 많기 때문이죠.
반면, LLM은 방대한 양의 텍스트 데이터를 학습한 모델입니다. 그렇기에 다양한 자연어(NLP) 문제들을 해결할 수 있으며 생성형(Generation) 모델이기 때문에 언어를 이해하고 생성할 수 있죠. 특히, 요약, 정리 등의 task는 우수하다고 익히 알려져 있습니다. 즉, LLM과 추천 시스템(recommender system with LLM)을 활용하면 LLM이 가지고 있는 지식을 활용해서 추천 시스템의 설명력을 제공할 수 있을 것입니다. 또한, RAG(Retrieval-Augmented Generation)을 활용하면 LLM의 할루시네이션을 방지하면서도 설명력을 제공할 수 있을 것입니다. 즉, 추천 시스템과 LLM을 결합하는 방법 중 2번 방법에 해당된다고 할 수 있습니다.
LLM을 활용한 추천 시스템 설명가능성 Python으로 구현하기
(Implementing the explainability of recommener system using LLM in Python)
그럼 본격적으로 LLM을 활용해 추천 시스템의 설명가능성을 부여할 수 있도록 Python 코드로 간단히 구현해보겠습니다. 앞서 소개에서 말씀드렸듯, 본 코드는 github에 올려두었으니 참고해주시면 감사하겠습니다.
파이썬으로 LLM기반 추천 시스템 설명가능성(LLM based explainability recommender system)을 구현하기 위해서 본 포스팅에서는 총 6단계를 거쳐서 진행하게 됩니다.
1. 활용 데이터 셋팅
2. 훈련된 사전 랭킹 모델(NCF 모델) 가져오기
3. 사용자에게 제공되는 추천 셋 구성(model prediction)
4. 사용자 이력 기반의 text prompt 작성
5. 사용자 요약 정보와 페르소나 설정
6. LLM 기반 추천 시스템 결과 설명 가능성 제공
이제 본격적으로 하나씩 순서대로 살펴보겠습니다.
1. 활용 데이터 셋팅(Set data)
가장 먼저, 사용할 데이터를 셋팅합니다. 본 포스팅에서 사용하는 추천 시스템 데이터는 MovieLens1M을 사용합니다. MovieLens1M 데이터는 추천 시스템 데이터에서 널리 알려진 데이터입니다. MovieLens 데이터는 사용자가 영화를 시청하고 평가한 데이터가 저장되어 있는데요. 저는 사용자가 평가를 진행했으면, 상호작용(Interaction) 했다고 가정하고 1로 셋팅해두었습니다. 그리고 사용자가 상호작용 하지 않은 데이터 중에서 negative sampling을 수행했습니다.
데이터 예시는 다음과 같습니다.
2. 훈련된 추천 시스템 모델 셋팅(Set recommender system)
사용자에게 최종 추천을 ranking을 수행하는 추천 모델을 셋팅합니다. 본 포스팅에서는 NCF(Neural Collaborative Filtering) 모델을 활용하는데요. 저는 지면상의 이유로 미리 MovieLens1M 데이터로 학습된 NCF 모델을 가져와서 사용합니다.
config = {
'num_users': 6040,
'num_items': 3706,
'latent_dim_mf': 8,
'latent_dim_mlp': 16,
'layers': [32, 16, 8]
}
model = NeuMF(config)
model.load_state_dict(torch.load('./model/ncf_mlm'))
사전 학습된 NCF 모델은 MovieLens1M 데이터를 사용해 학습한 모델입니다. 해당 모델을 MovieLens1M 데이터로 학습하는 과정은 공개되어 있는 자료가 많으니, 참고해주시면 되겠습니다. 본 포스팅에서는 내용이 길어지므로 해당 과정은 생략하겠습니다.
또한, 저는 NCF 모델을 사용했지만, 다른 추천 시스템 모델을 사용하셔도 상관 없습니다. Ranker로써 동작할 수 있는 모델과 방법은 전부 사용가능하니 여러분들이 편하시고 익숙한 것을 사용하시길 바랍니다.
3. 사용자에게 제공할 추천 데이터 셋팅(Set item list of recommender system prediction output, Ranking result)
1번과 2번 과정에서 사용자 이력 정보와 추천 모델을 가져왔으니 이제 추천 모델의 예측 결과를 셋팅하겠습니다. 추천 모델의 예측 결과라는 것은 결국 사용자에게 추천 될 아이템 추천 리스트(recommended item list)라고 보시면 될 것 같습니다. 저는 테스트 셋 기준으로 사용자의 추천 셋을 구성하였습니다.
for user, data_info in tqdm(ncf_user_pred_info.items(), total=len(ncf_user_pred_info), position=0, leave=True):
# sorted by high prop and slice by top(10)
ranklist = sorted(data_info, key=lambda s : s[1], reverse=True)[:top]
# to list
ranklist = list(dict.fromkeys([r[0] for r in ranklist]))
user_pred_info[str(user)] = ranklist
이렇게 추천 리스트를 저장하면 아래와 같이 파이썬 딕셔너리(Python dictionary) 형태로 데이터가 저장되게 됩니다.
4. 사용자 이력 기반의 Text Prompt template 작성(Set user interaction history based Text Prompt template)
이제 본격적으로 LLM과 추천 시스템을 결합해 설명가능성을 추출하는 작업을 시작합니다. 가장 먼저, 사용자 이력을 기준으로 사용자의 특징(페르소나, persona) 정보를 추출하고 요약하려고 합니다. 이 과정을 수행하는 이유는 사용자의 페르소나와 요약 정보를 기반으로 LLM이 추천 된 결과와 비교해서 타당(reasonable)한지 타당하다면 이유가 무엇인지를 설명하도록 하려고 합니다.
저는 크게 2가지 정보를 활용해서 사용자 페르소나 및 요약 정보를 추출하려고 하는데요. 이를 위해서 각각 text prompt template을 사용자 이력 기반으로 구성합니다.
1. 사용자 최근 이력 기반의 text prompt template
# Recent user info
recent_ratio = int(sample_user_history.shape[0] * 0.1)
user_data = movielens_rcmm_origin[movielens_rcmm_origin['user_id'] == random_user[0]].fillna('non data')[['movie_decade', 'movie_year', 'rating_year', 'rating_decade', 'genre1', 'genre2', 'gender', 'age', 'zip']].values[:recent_ratio]
recent_user_hist_info = "#### Item interaction information\n\n- (item) : metadata information of items \n- (user) : metadata information of users"
for cnt, rows in enumerate(user_data):
recent_user_hist_info += f"\n\n{cnt+1}th.\n- (Item) Movie Release Decade (ex. 1990s movies): {rows[0]}\n- (Item) Movie Release Year: {rows[1]}\n- (User) Rating Year: {rows[2]}\n- (User) Rating Decade (e.g., 1990s ratings): {rows[3]}\n- (Item) Genre 1: {rows[4]}\n- (Item) Genre 2: {rows[5]}\n- (User) Gender: {rows[6]}\n- (User) Age: {rows[7]}\n- (User) Address Information (zipcode): {rows[8]}\n##### End of {cnt+1}th item interaction information"
위 코드는 사용자의 최근 이력을 활용해서 텍스트 프롬프트를 구성하는 코드입니다. 이를 위해 최근 10%정도의 이력만 가져와서 text prompt를 구성합니다. text prompt에 들어가는 내용은 다음과 같습니다.
- Movie Release Decade : 00년대 영화와 같은 정보입니다. 예를 들어 1990년대 영화 형태이죠.
- Movie Release Year : 영화가 개봉한 년도입니다. 1994년과 같이 구체적인 년도 정보를 보여줍니다.
- Rating decade : 사용자가 평점을 남긴 년대입니다. 1990년대에 평점을 남겼다 이런 정보를 의미합니다.
- Rating year : 사용자가 평가를 남긴 년도입니다.
- Genre 1 : 영화가 가지고 있는 장르 정보입니다. MovieLens1M 데이터에서 영화가 가지고 있는 장르 정보 중 첫 번째 장르입니다.
- Genre 2 : Genre 1과 마찬가지로 장르 정보이며, MovieLens1M 데이터에서 영화가 가지고 있는 장르 정보 중 두 번째 장르입니다.
- Gender : 사용자의 성별입니다.
- Age : 사용자의 연령 정보입니다.
- zipcode : 사용자의 주소 정보입니다.
이 정보를 text prompt에 구성해서 사용자의 최근 관심사 정보를 저장합니다.
2. 사용자의 전체 이력 기반의 text prompt template
이번에는 사용자의 전체 이력을 기준으로 text prompt template을 구성합니다. prompt에 들어가는 내용은 바로 위 1번과 동일합니다. 다만, 이력이 전체 이력 정보인 것만 다릅니다.
# Entire user history information
user_data = movielens_rcmm_origin[movielens_rcmm_origin['user_id'] == random_user[0]].fillna('non data')[['movie_decade', 'movie_year', 'rating_year', 'rating_decade', 'genre1', 'genre2', 'gender', 'age', 'zip']].values
user_all_hist_info = "#### Item interaction information\n\n- (item) : metadata information of items \n- (user) : metadata information of users"
for cnt, rows in enumerate(user_data):
user_all_hist_info += f"\n\n{cnt+1}th.\n- (Item) Movie Release Decade (ex. 1990s movies): {rows[0]}\n- (Item) Movie Release Year: {rows[1]}\n- (User) Rating Year: {rows[2]}\n- (User) Rating Decade (e.g., 1990s ratings): {rows[3]}\n- (Item) Genre 1: {rows[4]}\n- (Item) Genre 2: {rows[5]}\n- (User) Gender: {rows[6]}\n- (User) Age: {rows[7]}\n- (User) Address Information (zipcode): {rows[8]}\n##### End of {cnt+1}th item interaction information"
아래 사진은 이렇게 구성된 텍스트 프롬프트 예시입니다. 이렇게 구성된 prompt template은 LLM의 입력으로 들어가게 되며 사용자의 요약 정보와 페르소나 정보를 추출하도록 LLM에게 지시하여 LLM이 관련 데이터를 구성하고 만들도록 합니다.
5. 사용자 요약 정보와 페르소나 셋팅(Set user summary information and persona)
사용자의 interaction 이력 기반의 text prompt template 구성이 완료되었다면 이제 LLM에게 입력 데이터로 제공하여 사용자 요약 정보와 페르소나(persona) 정보를 생성하도록 지시합니다. 요약 정보를 구성하기 위한 데이터는 사용자 전체 이력 기반 text prompt를 사용합니다. 사용자의 전체 이력 기반으로 구성된 text prompt는 구성된 내용이 전체 이력 기반이므로 내용이 길 수 밖에 없습니다. 또한, 전체 이력이므로 전반적인 사용자의 선호 정보를 구성할 수 있을거라 가정하고 전체 이력 기반 text prompt template을 활용해 사용자 요약 정보를 생성합니다.
docs = []
text_splitter = RecursiveCharacterTextSplitter(chunk_size=550, chunk_overlap=100)
texts = text_splitter.split_text(user_all_hist_info)
docs += [Document(page_content=t) for t in texts]
template = '''Below is the user's past history information. Considering the user's main characteristics, persona, preferences, and meaningful patterns, please summarize the user information within 700 characters.\n\n##### User history information: {text}.'''
prompt = PromptTemplate(template=template, input_variables=['text'])
llm = ChatOpenAI(temperature=0, model='gpt-4o')
chain = load_summarize_chain(llm,
chain_type='map_reduce',
map_prompt=prompt, combine_prompt=prompt,
verbose=False)
summary = chain.run(docs)
위 코드는 사용자의 요약 정보를 생성하기 위한 langchain 코드입니다. 텍스트 내용이 길기 때문에 RecursiveCharacterTextSpliter를 사용해서 텍스트를 chunk 단위로 잘라줍니다. 저는 chunk_size를 550, overlap을 100으로 설정하였습니다. 요약(summary)을 수행하기 위한 text prompt template에는 사용자의 주된 특징, 페르소나 선호도 등을 고려해서 700글자 내로 요약을 해달라고 지시하는 템플릿을 구성하였습니다. 이때 요약을 수행하는 LLM 모델로 이번에 새로 나온 GPT-4o를 활용하였으며 langchain에서 제공해주는 load_summarize_chain 함수를 사용해서 map_reduce 방법으로 사용자 정보 요약을 진행하였습니다. 요약을 수행한 결과는 아래와 같이 나오게 됩니다.
또한, 최근 이력 기반으로도 사용자의 선호 정보, 메인 특징, 페르소나를 추출하도록 합니다. 해당 데이터는 최근 이력 기반이라서 내용이 짧기 때문에 langchain의 LLMChain을 사용해서 실행하였습니다. LLM에게 수행하도록 하는 prompt template은 앞선 템플릿과 거의 동일합니다. 마찬가지로 사용자 정보를 추출할 때 사용하는 LLM은 gpt-4o를 사용하였으며 아래와 같이 chain.invoke 함수를 실행하면 LLM의 실행 결과를 받아볼 수 있습니다.
template = """Below is the user's item interaction history information. Using this data, please derive the user's main characteristics, persona, preferences, and meaningful patterns.
# User history information
{user_hist}
Please output in the following format:
- Main characteristics of the user: string
- User persona: string
- User preferences: string
- Meaningful patterns of the user: string
"""
prompt = PromptTemplate(template=template, input_variables=['user_hist'])
llm = ChatOpenAI(temperature=0, model='gpt-4o')
chain = LLMChain(llm=llm, prompt=prompt)
user_recent_summary = chain.invoke({'user_hist': recent_user_hist_info})
아래 사진은 LLMChain으로 실행된 최근 사용자 이력 기반의 특징 및 페르소나 정보입니다. 이제 이렇게 구성된 데이터를 활용해서 추천 시스템과 LLM을 결합한 설명가능성을 생성해보도록 하겠습니다.
6. LLM기반 추천 시스템 설명가능성 제공(Provide LLM-based recommendation system explainability)
드디어 본 포스팅의 마지막 단계인 LLM 기반 추천 시스템 설명가능성을 생성하는 단계입니다. 저희는 앞서
- 데이터 셋팅
- 사용자에게 추천 되는 추천 리스트 생성
- 사용자 이력 기반의 텍스트 프롬프트 구성 및 사용자 요약, 페르소나 정보 생성
과정을 수행했습니다. 이제 사용자에게 추천되는 추천 리스트와 사용자의 요약, 페르소나 정보를 사용해서 recommender system이 사용자에게 아이템을 추천한 explainabiltiy를 생성해보도록 하겠습니다.
먼저, 사용자에게 추천 된 정보를 text prompt template으로 다시 변환해줍니다. 사용자의 추천 리스트가 무엇이고, 추천 된 item의 정보가 무엇인지 text로 구성해두는 것입니다. 이때 아이템의 정보는 제목, 장르, 개봉 년도와 같은 데이터입니다. 해당 작업을 진행하는 코드는 아래와 같습니다.
user_data = user_recom_result[['title', 'movie_decade', 'genre']].values
user_recom_info = "#### User Recommendation List\n\n"
for cnt, rows in enumerate(user_data):
user_recom_info += f"\n\nRecommendation {cnt+1}:\n- Item Title: {rows[0]}\n- (Item) Movie Release Decade (e.g., 1990s movie): {rows[1]}\n- Item Genre (Category): {rows[2]}\n##### End of Recommendation {cnt+1} Information"
이제 사용자의 persona 정보와 사용자에게 추천 된 item들로 만든 text template을 사용해 LLM에게 왜 추천이 되었는지를 설명하도록 유도하는 prompt template을 구성합니다. 이 template은 LLM이 보기에 아이템이 사용자에게 적합한 추천이라면 추천 사유를 말해주도록 합니다. 만약, LLM이 판단하기에 이 아이템은 사용자에게 적합한 추천이 아니다라고 판단하면 적합한 추천이 아니라고 말하도록 합니다. prompt template 내용이 길어서 자세한 것은 코드를 참고해주시길 바랍니다. prompt template의 핵심은 다음과 같습니다.
- LLM 너의 역할은 사용자의 정보와 추천 시스템에서 제공하는 추천 결과를 비교하여 추천 이유를 작성하는 것이다.
- 추천 사유가 적절하지 않다고 판단되면 적절하지 않은 추천이라고 말해주고 그 이유도 말해줘라.
- 추천 사유가 적절하다고 판단되면 추천이 왜 적절한지 말해줘라.
- 이때, 사용자의 전체 요약 정보, 최근 요약 정보, 추천 리스트를 같이 제공합니다.
이렇게 text prompte를 작성하고 나서 Langchain의 PromptTemplate와 매칭시켜 LLM에게 해당 작업을 수행하도록 지시합니다. 그리고 그 결과는 아래와 같습니다.
LLM이 판단하기에(여기서 LLM은 gpt-4o) 해당 추천 아이템이 적합하다면 적합하다고 말해줍니다. 그리고 추천이 적합하다는 이유도 말해주죠. 예를 들어서, 사용자의 decade를 기반으로 선호도가 있을 것이고 장르 기반으로도 봤을 때 사용자 선호도가 있을 것이라 판단된다고 말해줍니다.
반면, 적합하지 않다면 LLM은 does not aligh with the user's preference라고 말해줍니다. 또한, less suitable 또는 less fitting 이라고 하면서 그 추천에 적합하지 않은 이유를 말해주는 것을 확인할 수 있습니다.
마무리
이번 포스팅은 추천 시스템 연구에서 자주 다뤄지는 설명가능성(Explainability of recommender system)을 LLM으로 시도해보는 간단한 방법과 example을 정리한 포스팅입니다.
LLM과 추천 시스템을 결합할 수 있는 방법에 대해서 조금이라도 도움이 되시길 바랍니다.
긴 글 읽어주셔서 감사합니다.
저에게 연락을 주시고 싶으신 것이 있으시다면
- Linkedin : https://www.linkedin.com/in/lsjsj92/
- github : https://github.com/lsjsj92
- 블로그 댓글 또는 방명록
으로 연락주세요!