Data science (108)

반응형

 

 

RAG 를 사용하지 않은 기본적인 API Call 방법

 

1. 기본 Openai API

from openai import OpenAI
import os
os.environ["OPENAI_API_KEY"] = "sk..."

client = OpenAI()
completion = client.chat.completions.create(
    model="gpt-4o",
    temperature=0.1,
    messages=[
        {"role":"system", "content":system_prompt},
        {"role":"user", "content":user_query}
    ],
    response_format={"type":"json_object"}
)
response = completion.choices[0].message.content
return(response)

2. Llama_index 활용

from llama_index.llms.openai import OpenAI
from llama_index.core.llms import ChatMessage
import os
os.environ["OPENAI_API_KEY"] = "sk..."

# OpenAI 모델 설정
llm = OpenAI(model="gpt-4o", temperature=0.1)

messages = [
    ChatMessage(role="system", content=system_prompt),
    ChatMessage(role="user", content="Hi"),
]

# Response 를 json 형식으로 받고 싶은 경우 
resp = llm.chat(messages, response_format={"type": "json_object"})
반응형
반응형

 

RAG Framework

RAG (Retrieval Augmented Generation) 은 LLM 이 정보를 검색하고 이를 활용하여 결과 생성할 수 있도록 하는 방법이다. 

 

RAG 의 4가지 컴포넌트

 

유명한 프레임워크로 LangChain 과 LlamaIndex 가 있다. 이 포스트에서는 두 프레임워크에 어떤 차이점이 있는지를 살펴보려고 한다. RAG 에는 4가지 컴포넌트가 있다. Loaders, Splitters, Indexing, Chains 이 그것이다. 각각 LangChain과 LlamaIndex 가 이를 어떻게 구현하고 있는지 살펴보자. 

 

1. Loaders

Loader 는 API, document, DB 와 같은 다양한 소스들을 로드하는 객체이다. LangChain, LlamaIndex 모두 흔하게 사용되는 소스들을 로드할 수 있는 built-in 함수들을 제공한다. 로더를 통해 파일 위치를 입력해주면 Directory 객체가 반환되며, 이를 다시 한 번 더 로드해주면 텍스트를 로드할 수 있다. 

 

LangChain: Text 파일 로드하기

from langchain.document_loaders import TextLoader
# Load a text document
loader = TextLoader("sample.txt")
documents = loader.load()
print(documents[0].page_content)

 

LlamaIndex: 특정 디렉토리안에 있는 텍스트 파일 로드하기 

from llama_index.core import SimpleDirectoryReader
# Load a text document from a directory
loader = SimpleDirectoryReader('path/to/docs')
documents = loader.load_data()
print(documents[0].text)

 

2. Splitters

Splitter 는 도큐먼트를 작은 Chunk 단위로 분해해서 GPT 나 BERT 의 token limit 을 넘기지 않도록 만들어주는 역할을 한다. 

 

LangChain

LangChain 의 `TextSplitter` 는 텍스트를 Character, Word, Sentence 중 어떤 단위로 분할할지를 선택할 수 있다. 아래 코드는 Character 단위로 텍스트를 분할하며, chunk size 는 1000이고, chunk 간에 200개의 중복 두는 방식으로 텍스트를 분할한다. 

 

from langchain.text_splitter import CharacterTextSplitter

# Define a character splitter
splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
chunks = splitter.split_documents(documents)
print(chunks[0].page_content)

 

LlamaIndex 

LlamaIndex 에서는 아래와 같이 똑같은 작업을 수행할 수 있다. 

from llama_index import TokenTextSplitter

# Define a token splitter
splitter = TokenTextSplitter(chunk_size=1000)
chunks = splitter.split(documents)
print(chunks[0].text)

 

3. Indexing

Indexing 은 RAG system 에서 가장 핵심 부분이라고 할 수 있다. 유저 쿼리와 가장 관련있는 청크를 빠르고 효율적으로 검색할 수 있어야한다. VectorStoreIndex 라는 말이 자주 등장하는데, 이는 VectorStore 를 빠르게 검색해서 유저 쿼리와 연관된 청크를 반환해주는 색인 시스템을 의미한다고 이해하면 된다. 

 

LangChain

LangChain 의 VectorStoreIndex 는 Splitter 를 통해 만들어진 청크들을 통해 인덱스를 생성한다. 그리고 Similarity search 를 기반으로 동작하게 된다.

from langchain.vectorstores import FAISS
from langchain.embeddings.openai import OpenAIEmbeddings

# Create embeddings
embedding_model = OpenAIEmbeddings()

# Create FAISS index
index = FAISS.from_documents(chunks, embedding_model)

 

LlamaIndex

LlamaIndex 에서는  아래와 같이 Document 로부터 바로 VectorStoreIndex 를 생성할 수 있고, Splitter 를 바로 인자로 주어 생성할 수 있다. 

from llama_index.core import VectorStoreIndex
embed_model = OpenAIEmbedding(model="text-embedding-3-small")

index = VectorStoreIndex.from_documents(
    documents,
    embed_model=embed_model,
    transformations=[SentenceSplitter(chunk_size=16000)]
)

 

4. Chain

RAG 에서 Chain 이란 Retrieval 과 Generation 의 요소들이 결합된 일련의 작업들이라고 보면 된다. LangChain 과 LlamaIndex 모두 이러한 RAG 의 컴포넌트를 엮어서 하나의 체인으로 만드는 기능을 제공한다. 

 

LangChain

LangChain 에서는 아래와 같이 `RetrievalQA` 를 활용해서 LLM 과 Retrieval 가 결합된 체인을 만들 수 있다. 이를 통해 RAG workflow 를 좀 더 간결하게 표현할 수 있다. 

from langchain.chains import RetrievalQA
from langchain.llms import OpenAI

# Combine FAISS index and OpenAI for RAG
llm = OpenAI()
qa_chain = RetrievalQA(llm=llm, retriever=index.as_retriever())
result = qa_chain.run("What is the content of the document?")
print(result)

 

LlamaIndex

LlamaIndex 에서는 VectorStoreIndex 에 as_query_engine 또는 as_chat_engine 을 이용해서 쉽게 Index 와 LLM 을 연결할 수 있다. 해당 Index 를 활용하는 LLM 을 만들라는 의미이다.  query engine 과 chat engine 의 차이점은 chat engine 은 컨텍스트를 기억한다. as_chat_engine 을 보면, memory 인자를 전달함으로써, 과거 기억을 얼마나 전달할지를 정할 수 있다. token_limit 이 작을수록 과거 컨텍스트를 모델에 더 적게 전달한다. 

# Chat Engine 생성하기
memory = ChatMemoryBuffer.from_defaults(token_limit=16000)
chat_engine = index.as_chat_engine(
    chat_mode="context",
    memory=memory,
    system_prompt=system_prompt
)
response = chat_engine.query("What is the content of the document?")
print(response)

 

LangChain 과 LlamaIndex 중 무엇을 선택해야할까?

1. Customization

LangChain 의 장점은 다양한 소스를 기반으로 RAG 를 구현해야할 때 더욱 유연하다는 것이다. 예를 들어, Multi-modal RAG 시스템을 구축한다고 해보자. PDF, API 문서, Web 문서 등을 각각 Load 해서 VectorStoreIndex 를 구축한다고 해보자. LangChain 에서는 `+` operation 을 통해 쉽게 로더를 결합할 수 있다.  

from langchain.chains import SequentialChain
from langchain.llms import OpenAI
from langchain.document_loaders import WebPageLoader, TextLoader
from langchain.vectorstores import FAISS

# Step 1: Load data from multiple sources
pdf_loader = TextLoader("docs/sample.pdf")
web_loader = WebPageLoader(url="https://example.com")
documents = pdf_loader.load() + web_loader.load()

# Step 2: Create embeddings and index
embedding_model = OpenAIEmbeddings()
index = FAISS.from_documents(documents, embedding_model)

# Step 3: Build a chain that retrieves and generates responses
llm = OpenAI()
qa_chain = RetrievalQA(llm=llm, retriever=index.as_retriever())

# Run the chain
response = qa_chain.run("What are the key points in the PDF and website?")
print(response)

 

LlamaIndex 의 장점은 더욱 직관적이며, 심플하게 RAG 를 구현할 수 있다는 것이다. 예를 들어 LlamaIndex 의 경우, 법 문서나 medical report 등을 기반으로 RAG 를 만들 때, 매우 빠르게 최소한의 코드로 구축해볼 수 있다. 

from llama_index.core import VectorStoreIndex, SimpleDirectoryReader

# Load documents from a directory
loader = SimpleDirectoryReader('docs/')
documents = loader.load_data()

# Build a tree index and query
index = VectorStoreIndex(documents)
query_engine = index.as_query_engine()

# Query the system
response = query_engine.query("Summarize the legal document.")
print(response)

 

2. Ecosystem 통합

LangChain 은 여러 AI Ecosystem 들을 통합할 때 더욱 유용하다. LangChain 은 Multi-LLM, Multi-Retrieval 를 지원한다고 볼 수 있다. 

- Vector Database : FAISS, Pinecone, Chroma

- Language Model : OpenAI, GPT-4, Anthropic's Claude

- API : Hugging Face, Cohere

 

위와 같은 요소들을 자유롭게 결합할 수 있다는 것이 LangChain 의 장점이라고 볼 수 있다. 예를 들어, OpenAI 의 GPT-4 와 HuggingFace 의 BERT 를 태스크에 따라 자유롭게 스위치 할 수 있다.  

from langchain.chains import LLMChain
from langchain.chains import SimpleSequentialChain
from langchain.llms import OpenAI, HuggingFaceHub
from langchain_core.prompts import PromptTemplate

prompt_template = "Tell me a {adjective} joke"
prompt = PromptTemplate(
    input_variables=["adjective"], template=prompt_template
)

gpt_chain = LLMChain(llm=OpenAI(), prompt=prompt)
gpt_chain.invoke("scary")

bert_chain = LLMChain(llm=HuggingFaceHub(model="bert-large-uncased"), prompt=prompt)
bert_chain.invoke("scary")

 

반면 LlamaIndex 의 경우 LangChain 과 같은 유연성은 떨어지지만, 앞서 언급한 것처럼 빠르게 document 기반의 RAG 를 만들 때 유용하다고 할 수 있다. 

 

3. Multi-retrieval

LangChain 의 경우 Multi-retrieval 를 구현할 수 있다. 예를 들어, 유저 쿼리를 받아, 법과 관련된 쿼리라면, 법 문서에 적용된 key-based retrieval 를 호출하고, 과학 관련 쿼리라면, 과학 논문에 적용된 embedding-based retrieval 를 호출하는 multi-retrieval 를 구현해보자. 

from langchain.retrievers.multi_retriever import MultiRetriever
from langchain.retrievers import FAISSRetriever, KeywordRetriever

# Define keyword and embedding-based retrieval systems
keyword_retriever = KeywordRetriever(documents=legal_documents)
embedding_retriever = FAISSRetriever(index=scientific_index)

# Combine them in a MultiRetriever
retriever = MultiRetriever(retrievers={
    'legal': keyword_retriever,
    'science': embedding_retriever
})

# Query the retriever with a legal question
response = retriever.retrieve("What are the recent changes in contract law?")
print(response)

 

4. Community Support 측면

LangChain 은 위와 같은 advanced 된 기능들 때문에 최근 빠르게 관련 커뮤니티가 성장하고 있으며, Llama Index 와 비교하여 좀 더 성장된 ecosystem 을 갖고 있다. LlamaIndex 의 경우 비교적 최근에 등장했지만 마찬가지로 빠르게 관심을 얻고 있으며, 앞서 언급한 것처럼 좀 더 간단한 작업에 적합하다. 

 

결로: 결론적으로 어떤 상황에서 어떤 프레임워크를 선택해야할까?

LangChain 을 선택하면 좋은 상황
- 다양한 구성 요소들을 자유롭게 커스텀하고 싶을 때 (Retrieval, LLM, OpenAI, HuggingFace, FAISS, Chroma...)
- VectorStore 를 다양한 타입의 소스로 구성하고 싶을 때 (Text, PDF, API 등)
- 다양한 Retrieval 전략을 사용하고 싶을 때 (Keyworkd search, post-retrieval ranking 등)

LlamaIndex 를 선택하면 좋은 상황 
- 간단한 RAG System 을 구축하고 싶을 때 (예를 들어, document 를 요약해주는 AI 를 구현)
- 특별한 커스텀이 필요없을 때 
- 구조적인 문서 (법 관련 문서 등) 을 기반으로 RAG 를 구축할 때 잘 작동함 

 

 

 

 

 

출처

- https://medium.com/@tam.tamanna18/langchain-vs-llamaindex-a-comprehensive-comparison-for-retrieval-augmented-generation-rag-0adc119363fe

반응형
반응형

 

 

기본적으로 가상환경을 새롭게 생성하고 거기에 설치하는 것이 좋다.

 

가상환경 생성

virtualenv openai
python3 -m ipykernel install --user --name=openai

# 가상환경 activate
source ./activate

 

pysqlite3 설치

pip install pysqlite3-binary

# 내장된 pysqlite3 대신 pysqlite3 을 사용하도록 설정한다. 
import sys
import pysqlite3
sys.modules["sqlite3"] = pysqlite3

import sqlite3
print(sqlite3.sqlite_version)
>> 3.46.1

 

3.35 이상 버전에서 chromadb 를 사용할 수 있다.

import chromadb

 

 

반응형
반응형

 

기본적인 RAG 를 좀 더 개선해보기 (w/ langchain)

 

기본 RAG 의 개념 : 기본 RAG (Retrieval Augmented Generation) 는 query 를 입력으로 받아 vector store 로부터 relevant 한 chunk 를 검색해서 가져온 후, 이를 prompt 에 추가해서 최종적인 답변을 출력한다.

 

사용된 개념

1. Relevance Check : vector store 에서 retrive 해온 chunk 가 relevant 한지 LLM 을 활용해서 검증한다.

2. Hallucination Check  : retrieved chunk 를 참고해서 생성한 최종 답변이 hallucination 인지 여부를 LLM 을 활용해서 검증한다. 

 

RAG 를 사용하기 위한 프레임워크로 langchain 을 사용한다. (비슷한 역할을 하는 Llamaindex 라는 프레임워크도 있다.)

 

목적: 3가지 웹문서를 vector store 로 저장하고, 이에 기반해 사용자 질문에 답변하는 RAG 를 만든다.

 

1. 필요한 라이브러리 설치

%pip install langchain langchain-openai langchain-openai langchain_chroma langchain-text-splitters langchain_community

 

2. LLM 객체 생성

import getpass
import os

os.environ["OPENAI_API_KEY"] = "API_KEY 를 입력하세요."
from langchain_openai import ChatOpenAI

# openai의 gpt-4o-mini 모델 사용
llm = ChatOpenAI(model="gpt-4o-mini")

 

3. 문서를 로드한다.

- langchain 의 WebBaseLoader를 활용하며, beautiful soup 라이브러리를 활용해 html 태그 등을 parsing 해 본문 내용만 가져온다고 이해하면된다. 

import bs4
from langchain import hub
from langchain_chroma import Chroma
from langchain_community.document_loaders import WebBaseLoader
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter

urls = [
    "https://lilianweng.github.io/posts/2023-06-23-agent/",
    "https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/",
    "https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/",
]

# Load, chunk and index the contents of the blog.
loader = WebBaseLoader(
    web_paths=urls,
    bs_kwargs=dict(
        parse_only=bs4.SoupStrainer(
            class_=("post-content", "post-title", "post-header")
        )
    ),
)
docs = loader.load()

 

4. 로드한 문서를 Split 해서 Vector store 로 저장한다.

- chunk size 는 1000  토큰이며, 200의 오버랩을 허용한다. 각 청크간에 약간의 중복을 준다는 의미이다. 

- chunk 를 vector 로 저장할 때, embedding 이 필요한데, openai 의 text-embedding-3-small 모델을 사용했다.

- retrieval 의 세팅으로 similarity 를 기준으로 6개의 chunk 를 가져오는 retrieval 객체를 정의한다. 

text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
splits = text_splitter.split_documents(docs)
vectorstore = Chroma.from_documents(documents=splits, embedding=OpenAIEmbeddings(model="text-embedding-3-small"))
# Retrieve and generate using the relevant snippets of the blog.
retriever = vectorstore.as_retriever(search_type="similarity", search_kwargs={'k':6})

 

5. user query 를 입력으로 받아 관련도가 높은 chunk 를 retrieve 해온다.

- 가져온 chunk 6개를 1번 chunk : ... 2번 chunk : ... 이런 형식으로 텍스트 형식으로 저장한다. 

from langchain_core.output_parsers import JsonOutputParser
from langchain_core.prompts import PromptTemplate
from langchain import hub
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import JsonOutputParser
from langchain_core.prompts import PromptTemplate
from langchain_openai import ChatOpenAI

prompt = hub.pull("rlm/rag-prompt")

user_query = "What is agent memory?"
retrieved_docs = retriever.invoke("what is agent memory?")
retrieved_docs_text = ""
for i in range(0,6) :
    doc_content = retrieved_docs[i].page_content
    retrieved_docs_text += f'{i+1}번 chunk: {doc_content}\n'

 

6. 목적에 맞는 최종 prompt 를 작성한다.

- relevance check 와 hallucination check 를 하여 기본 RAG 를 좀 더 개선하는 것이 목적이므로, 이에 대한 로직을 추가한 프롬프트를 작성한다. 전체적인 답변 생성 로직과 함께 필요한 정보 - 유저 query 와 retrieved chunk 를 제공해주어야한다. 

query = {"question": {f"""
     너는 유저로부터 query 를 입력 받아서 최종적인 답변을 해주는 시스템이야.

     그런데, retrieval 된 결과를 본 후에, relevance 가 있는 retrieved chunk 만을 참고해서 최종적인 답변을 해줄거야.
     아래 step 대로 수행한 후에 최종적인 답변을 출력해.

     Step1) retrieved chunk 를 보고 관련이 있으면 각각의 청크 번호별로 relevance: yes 관련이 없으면 relevance: no 의 형식으로 json 방식으로 출력해.\n
     Step2) 만약 모든 chunk 가 relevance 가 no 인 경우에, relevant chunk 를 찾을 수 없었습니다. 라는 메시지와 함께 프로그램을 종료해.
     Step3) 만약 하나 이상의 chunk 가 relevance 가 yes 인 경우에, 그 chunk들만 참고해서 답변을 생성해.
     Step4) 최종 답변에 hallucination 이 있었는지를 평가하고 만약, 있었다면 hallucination : yes 라고 출력해. 없다면 hallucination : no 라고 출력해.
     Step5) 만약 hallucination : no 인 경우에, Step3로 돌아가서 답변을 다시 생성해. 이 과정은 딱 1번만 실행해 무한루프가 돌지 않도록.
     Step6) 지금까지의 정보를 종합해서 최종적인 답변을 생성해

     답변은 각 스텝별로 상세하게 출력해

     아래는 user query 와 retrieved chunk 에 대한 정보야.

     query : {user_query}\n
     retrieved chunks: {retrieved_docs_text}"""}}

 

7. Prompt Template 을 기반으로 RAG Chain 을 호출해 최종 답변을 json 형식으로 출력한다.

- prompt template 은 프롬프트를 구조화된 형식으로 작성할 수 있게 도와주는 클래스이다. 

- rag chain 은 query-> prompt -> llm -> output 파이프라인을 하나의 함수로 실행할 수 있도록 해준다. 

parser = JsonOutputParser()

prompt = PromptTemplate(
    template="Answer the user query.\n{format_instructions}\n{query}\n",
    input_variables=["query"],
    partial_variables={"format_instructions": parser.get_format_instructions()},
)

rag_chain = (
    prompt
    | llm
    | StrOutputParser()
)

rag_chain.invoke({"query": query})

 

최종적으로 생성된 답변

json\n{\n  "step1": {\n    "1": "relevance: yes",\n    "2": "relevance: yes",\n    "3": "relevance: yes",\n    "4": "relevance: yes",\n    "5": "relevance: no",\n    "6": "relevance: no"\n  },\n  "step2": "All retrieved chunks have relevance.",\n  "step3": "The relevant chunks provide information about agent memory, which consists of both long-term and short-term memory. Agent memory is a long-term memory module that records agents\' experiences in natural language. Observations can trigger new statements, and the retrieval model assists in informing the agent’s behavior based on relevance, recency, and importance. Additionally, the reflection mechanism synthesizes these memories into higher-level inferences over time.",\n  "step4": "hallucination: no",\n  "step5": "Since hallucination is no, I will generate the answer again.",\n  "step6": {\n    "final_answer": "Agent memory refers to a long-term memory module that captures a comprehensive list of an agent\'s experiences in natural language, which can be used to inform the agent\'s behavior. It includes short-term aspects, such as in-context learning, as well as long-term memory that retains information over extended periods. The retrieval model helps prioritize memories based on relevance, recency, and importance, while the reflection mechanism synthesizes past events into higher-level insights that guide future behavior."\n  }\n}\n

반응형
반응형

 

벤 다이어그램에서 구할 수 있는 다양한 유사도 지표들

벤다이어그램에서 A, B 그룹의 유사도를 구해보자

 

분석을 하다보면 두 집합이 얼마나 교차하는가? 를 이야기하고 싶을 때가 있다. A 그룹 유저는 B 그룹 유저와 N% 겹치고, B 그룹 유저는 A 그룹 유저와 M% 겹칩니다! 라고 말할 수 있다. 하지만 대량으로 이러한 교차성에 대한 값을 구해서 비교해 본다고 하자. 좀 더 심플하고 요약된 지표가 있다면 비교가 용이할 것이다. 교차성에 대한 정보를 0~1 사이의 값으로 나타내는 지표에는 어떤 것들이 있을까? 그리고 그 지표들 간에는 어떤 차이가 있을까? 

 

우선, 벤다이어그램으로 표현한 데이터는 아래와 같이 binary vector 로도 표현할 수 있다. 예를 들어, 어떤 유저들이 A 서비스를 사용하는지 B 서비스를 사용하는지 여부를 binary vector 로 아래와 같이 나타낼 수 있다. 

Sample A서비스사용 B서비스사용
1 1 1
2 1 0
3 0 1
4 1 0
5 1 0

 

유사도란 A, B 두 벡터가 얼마나 유사한지를 보는 것으로 이해할 수 있다. 또는, 벤다이어그램에서 겹치는 부분이 얼마나 되는지를 의미하는 지표라고 해석할 수 있다. 

 

1. Jaccard Index

 

Jaccard index 는 가장 기본적으로 생각할 수 있는 유사도 지표이다. 이것은 벤다이어그램에서 교집합 크기를 합집합 크기로 나눈 것으로 정의된다. 벤다이어그램에서 교차되는 부분의 면적을 전체 원의 면적으로 나눈 것이다. 

 

$$ J(A,B) = \frac{{|A \cap B|}}{{|A \cup B|}} $$

$$ J(A,B) = 100 / 1300 = 0.08 $$

 

Jaccard index 전통적으로 두 문서의 유사도를 비교할 때 쓰이기도 한다. A 문서와 B 문서의 유사도를 비교할 때, 두 문서에 등장하는 단어들을 하나의 샘플로해서 A,B 문서에 속했는지 여부를 이진 벡터로 만들 수 있을 것이다. 최종적으로 A문서와 B문서의 이진벡터를 만들 수 있고, 이를 통해 Jaccard index 를 구할 수 있다. 

 

2. Dice coefficient

 

Dice coefficient는 두 집합의 교집합 크기의 두 배를 두 집합 크기의 합으로 나눈 값이다. 

 

$$ D(A,B) = \frac{{2|A \cap B|}}{{|A| + |B|}} $$

$$ D(A,B) = \frac{100*2}{1000+400} = 0.14$$

 

3. Kulczynski similarity index

 

A 의 크기가 1만, B의 크기가 10, 그리고 A교집합B가 9라고 하자. B의 입장에서는 90%가 A와의 교집합인데 Jaccard index 나 Dice coefficient 는 낮게 계산된다. Kulczynski similarity 는 A 와 B 각각에서 교집합의 비율을 평균낸 값이기 때문에, B 입장에서 90%인 교집합의 비율이 동등하게 지표에 반영된다. Kulczynski similarity 는 두 집합의 크기 차이가 클 때, 이를 보정하는 지표로 볼 수 있다. 

 

$$ K(A,B) = 0.5 * \left(\frac{|A \cap B|}{|A|} + \frac{|A \cap B|}{|B|}\right) $$

$$ K(A,B) = 0.5*(100/1000 + 100/400) = 0.18 $$

 

즉, Kulczynski similarity  두 데이터 셋 간의 크기 차이가 결과에 미치는 영향을 감소시킨다. 반면, Jaccard index 나 Dice coefficient 의 경우, A,B의 크기의 차이가 결과에 영향을 미친다.

 

4. Cosine similarity (코사인 유사도)

 

Cosine similarity 는 두 벡터의 코사인 값을 통해 유사도를 구하는 개념이다. 코사인 유사도는 이진 벡터가 아닌 데이터에서도 광범위 하게 사용되는 measure 이다. 예를 들어, 자연어 처리에서 두 워드 임베딩 값의 유사도를 볼 때 쓰이기도 한다. 벤다이어그램에서는 두 벡터가 이진 벡터이기 때문에 두 벡터의 내적은 (1,1) 인 행의 숫자, 즉 교집합의 크기를 의미한다. 또한, 각 벡터의 크기는 집합의 크기의 제곱근이다. 

중고등학교 시절 배우는 공식

 

$$ \cos(\theta) = \frac{A \cdot B}{|A| |B|} $$

$$ \cos(\theta) = \frac{A \cap B}{\sqrt{|A|} \sqrt{|B|}} $$

$$ \cos(\theta) = \frac{100}{\sqrt{1000} \sqrt{400}} = 0.16 $$ 

 

반응형
반응형

기본적인 2x2 테이블 

이진 분류 모델의 최종 예측 값은 일반적으로 0~1사이로 나오게된다. 특정 임계치를 기준으로 테스트 양성과 테스트 음성을 분류한다. 예를 들어, 임계치를 0.5로 잡는다면, 0.5 이상인 경우를 테스트 양성, 0.5 미만인 경우를 테스트 음성으로 정의한다. 이 방법을 통해 아래와 같은 2x2 테이블을 만들 수 있다. 

  양성 (Disease) 실제 음성 (No Disease)
테스트 양성 (Positive) 50 10
테스트 음성 (Negative) 5 100
  • True Positive (TP): 50
  • False Positive (FP): 10
  • False Negative (FN): 5
  • True Negative (TN): 100

 

Sensitivity = Recall (민감도)

sensitivity 는 실제 질병인 사람 중에 테스트 양성인 사람의 비율이다. 

-> 50/55 = 0.909

 

Specificity = Negative Recall (특이도)

specificity 는 실제 질병이 아닌 사람 중에 테스트 음성인 사람의 비율이다. 

-> 100/110 = 0.909

 

Positive Predictive Value = Precision (양성 예측도, PPV) 

ppv 는 양성으로 예측한 사람 중에 실제 질병인 사람의 비율이다.

-> 50/60 = 0.833

 

Negative Predictive Value (음성 예측도, NPV) 

npv 는 음성으로 예측한 사람 중에 실제 질병이 아닌 사람의 비율이다. 

-> 100/105 = 0.952 

 

ROC 커브와 AUC (Area under curve)  

임계치를 변화시키면서 1-specificity, sensitivity 그래프를 그린 것이 ROC 커브이다. 위 두 지표를 통해 그래프를 그리는 이유는 sensitivity 와 specificity 간에 트레이드오프관계가 있기 때문에, 이 관계를 시각적으로 표현하여 모델의 성능을 평가하기 위해서이다. 

 

1-specificity = False Positive Rate (FPR)

sensitivity = True Positive Rate (TPR)

 

임계치가 낮아지면 모델은 더 많은 사례를 양성으로 분류하게 되어 True Positive RateFalse Positive Rate가 모두 증가한다. 반대로, 임계치가 높아지면 모델은 더 적은 사례를 양성으로 분류하게 되어 True Positive RateFalse Positive Rate가 모두 감소한다. 

 

반응형
반응형

회귀 모델의 선택

 

1) 두 변수가 nested 관계에 있을 때

 

--> Likelihood ratio test 를 통해 유의미하게 좋은 모델을 선택한다. 

 

두변수가 nested 관계에 있다는 것은 full model, reduced model 관계에 있다는 것을 의미한다. 

 

$$ Model1: g(\pi_i) = \beta_0 + \beta_1x_{1i} $$

$$ Model2: g(\pi_i) = \beta_0 + \beta_1x_{1i} + \beta_2x_{2i} $$  

 

만약 모델 2에서 beta2가 0인 경우, 모델1 이 된다. 따라서 두 모델은 nested 관계에 있다. 

이 경우 변수가 많은 모델2가 무조건 likelihood 가 높게 된다. 

 

이 때, Likelihood ratio 가 카이제곱분포를 따르게된다. 자유도는 두 모델의 모수 개수의 차이가 된다. 여기서 L0가 간소한 reduced 모델이고, L1이 full model 이다. 이는 full model 과 reduce model 의 log likelihood 의 차이의 2배이다. 

 

$$ -2ln(\frac{L_0}{L_1}) = -2(lnL_0 - lnL_1) \sim \chi(1)$$

 

만약 위 통계량이 유의미한 카이제곱 값을 가지면, full model 이 reduced model 보다 좋은 것이다. 따라서 full model 을 채택한다. 만약 카이제곱값의 p-value 가 0.05보다 크다면, full model 이 reduced model 보다 좋지 않은 것이므로, reduced 모델을 채택한다. 

 

2) 두 변수가 unnested 관계 일 때

 

--> AIC 가 작은 모델을 선택한다.

 

$$ Model1: g(\pi_i) = \beta_0 + \beta_1x_{1i} $$

$$ Model3: g(\pi_i) = \beta_0 + \beta_1x_{2i} + \beta_2x_{3i} $$  

 

이러한 경우 AIC 를 통해 두 모델 중 어떤 모델이 좋은지를 판단할 수 있다. p는 변수의 숫자로 패널티텀이다. unnested 관계일 때, likelihood ratio test 를 적용할 수 없는 이유는 unnested 관계일 때는 likelihood ratio 가 카이제곱분포를 따르지 않기 때문이다. 

 

$$ AIC = -2(Log L_1 - p) $$ 

 

만약 모델1 의 로그 우도가 -120 이고, 모델3의 로그 우도가 -115 라고하자. 모델3의 로그 우도가 더 높다.  

 

모델1의 AIC = -2(-120 - 2) = 244

모델3의 AIC = -2(-115 - 3) = 236 

 

만약 모델3의 추정 파라미터 수가 2개였다면, AIC 는 234였을 것이다. 파라미터로 인한 패널티 2점이 들어갔음을 알 수 있다. 이 경우 패널티를 고려해도 모델3의 AIC가 낮기 때문에 모델3을 채택한다. 

 

 

반응형
반응형

 

 

일반화 선형 모형의 개념 (Generalized Linear Model)

 

일반화 선형 모형의 식은 아래와 같다. 

 

$$ g(\mu) = \beta_0 + \beta_1x_1 + \beta_2x_2 + ... \beta_px_p $$ 

 

x1, x2 ... 가 주어졌을 때, Y를 예측하고 싶다. 근데 특정 조건하에서 Y 는 정해진 값이 아니라 어떤 분포를 따른다고 가정하고, 그 평균을 예측하고 싶을 때, 일반화 선형 모형을 활용한다. 기본적인 회귀분석에서는 반응변수가 정규분포를 따른다고 가정하고 모델링하는데, 일반화 선형 모형은 Y가 다른 분포를 따르는 경우에도 활용할 수 있는 모델링 방법이라고 볼 수 있다. 

 

일반화 선형모형에서는 반응변수가 어떤 분포를 따른다고 가정하기 때문에 랜덤성분 (random component) 이라고 부르고, 반응변수의 평균을 설명하기 위한 설명 변수들의 함수 (위 식에서 우측부분) 를 체계적 성분 (systematic component) 이라고 부른다. 랜덤성분과 체계정 성분을 연결하는 함수를 연결함수(link function) 라고 부른다.

 

Y가 정규분포를 따른다면, 평균값이 -무한대~+무한대일 수 있고, Y 가 베르누이 분포를 따르면 Y의 평균이 0~1사이의 값이다. 따라서 적당한 연결함수를 통해 값의 범위를 변환하는 것이 필요하다. 

 

또한, 일반화 선형 모형에서는 Y 가 지수족 분포를 따른다고 가정한다. 지수족 분포에는 정규분포, 이항분포, 포아송분포, 감마분포 등이 있다. Y가 따른다고 가정한 분포에 따라 알맞는 연결함수를 적용해준다. GLM 에서 지수족 분포가 중요한 개념이지만, 다소 심플하게 내용을 설명하기 위해 지수족 관련 내용은 설명하지 않겠다. 

 

만약, Y가 정규분포를 따르는 경우에 가장 기본적으로 항등함수를 이용할 수 있다. 연결함수가 항등함수인 경우, 일반 선형 모형이라고 한다. (general liner model) (generalied linear model 과 다르다.). 

 

$$ \mu = \alpha + \beta x $$ 

 

연결함수가 항등함수인 경우 beta 값의 해석은 매우 쉽다. "X가 1단위 증가했을 때 반응 변수가 beta 만큼 증가한다" 고 해석한다. 

 

Y가 베르누이분포를 따르는 경우 0~1의 값을 무한대로 변환하는 연결함수로 여러가지를 이용할 수 있다. 가장 대표적인 것이 로짓함수이다. 로짓함수를 사용한 변수가 1개인 일반화 선형모형은 아래와 같이 정의된다. 이를 로지스틱 회귀분석 (logistic regression) 이라고 부른다.  

 

$$ log(\frac{\mu}{1-\mu}) = \alpha + \beta x $$ 

 

좌측을 살펴보면 log odds 라는 것을 알 수 있다. (=> log(성공확률/실패확률) 이므로) 즉, 로지스틱 회귀분석은 log odds 를 설명변수들의 조합으로 예측하는 것을 의미한다. odds 가 아닌 확률(평균) 의 관점에서 로지스틱 회귀분석은 아래와 같이 써볼 수 있다. 

 

$$ \mu = \frac{exp(\alpha + \beta x)}{1+exp(\alpha+\beta x)} $$

 

또한 로지스틱 회귀 분석에서 중요한 것은 beta 값의 해석이다. 만약 x가 연속형인 경우 x+1과 x의 odds 를 구해서 odds ratio 를 구해보자. 위 식에 넣어 계산해보면, OR = exp(beta) 가 나온다. 양변에 log 를 취해주면 log(OR) = beta 라는 것을 알 수 있다. 즉, x가 1단위 증가했을 때의 log(OR) 값이 beta 라는 것을 알 수 있다.

 

한편, Y가 베르누이 분포를 따르는 경우에 사용할 수 있는 다른 연결함수로는 프로빗 연결함수가 있다. 프로빗 연결함수를 사용한 일반화 선형 모형을 프로빗 모형이라고 부른다. 프로빗 모형은 표준정규분포의 누적분포함수의 역함수를 연결함수로 사용한다. 누적분포함수의 역함수를 연결함수로 사용한다는 의미가 무엇일까? 누적분포함수는 0~1사이의 값을 갖는다. 즉, 어떤 -무한대~무한대에 있는 X라고하는 값을 0~1 사이로 변환하는 함수이다. 이에 역함수이기 때문에 0~1사이의 값을 -무한대~무한대로 바꾸어주는 함수가 된다. 

 

도수 자료의 경우에는 일반화 선형모형중 포아송 회귀분석을 해볼 수 있다. 도수 자료란 반응 변수가 도수 (count)로 이루어진 자료를 의미한다 (예를 들어, 교통사고 수, 고장 수 등...). 도수자료는 양의 방향에서만 존재한다. 교통사고수가 마이너스일 수는 없다. 반면, 설명변수의 조합인 체계적 성분은 -무한대~무한대의 범위를 갖는다. 이를 변환하기 위해서, 포아송 회귀분석에서는 연결함수로 log 를 활용하여 좌변이 -무한대~무한대의 값을 갖도록 변환한다. 포아송 회귀분석 식은 아래와 같다. 

 

$$ log(\mu) = \alpha + \beta x $$ 

 

이는 평균의 관점에서는 아래와 같이 쓸 수 있다.

 

$$ \mu = exp(\alpha + \beta x) $$

 

x가 t일 때와 t+1일때의 mu 값을 비교해보자. 위 수식에 대입하면 x가 t+1 일 때의 mu 와 t 일 때의 mu 의 ratio 는 exp(beta) 가 됨을 알 수 있다. 즉, 포아송 회귀분석과 같은 log linear regression 에서 beta 를 해석하는 방법은 "x 가 1단위 증가했을 때 Y값의 평균이 exp(beta)배 증가한다." 이다. 

 

포아송 회귀 관련해서는 종종 이런 문제가 발생할 수 있다. 만약, X가 차량 사고수에 미치는 영향을 포아송 회귀로 모델링을 하려고하는데, 지역별로 데이터가 수집 되었고, 지역별로 기본적인 차량의 개수가 달라 사고수가 이에 영향을 받는다고 해보자. 이 때, "사고율" 을 반응 변수로해서 모델링할 수 있다. 차량의 개수를 t라고 하자.

 

$$ \log(\mu / t) = \alpha+\beta x $$

 

사고수의 관점에서 아래와 같은 수식으로 변환할 수 있다. 이 때, log(t) 를 offset 이라고 한다. 

 

$$ \log(\mu) = \log(t)+\alpha+\beta x $$

$$ \mu = texp(\alpha + \beta x) $$ 

 

반응형
반응형

 

 

선택편향

 

선택편향은 특정 그룹을 선택해서 분석했을 때, 다른 그룹 또는 전체를 대상으로 분석했을 때와 다른 결론이 나오는 것을 의미한다. 아래와 같이 왼쪽 그림에서는 X,Y 의 연관성이 없지만, X+Y가 1.2 이상인 그룹만 선택해서 봤을 때는 X,Y의 음의 상관성이 생기는 것을 알 수 있다. 이러한 선택 편향은 우리의 실생활에서도 많이 발생한다. 

 

 

 

Collider bias

 

 

Collider bias는 X와 Y가 모두 영향을 미치는 Z라고 하는 변수가 있을 때, Z를 고정시켜 놓고 보면, X (exposure) 과 Y (outcome) 에 연관성에 편향이 생기는 현상을 의미한다. 

 

왜 Collider bias 가 발생할까? 이에 대해 사고적으로 이해하는 방법에는 "explaining away" 라고 하는 개념이 있다. 예를 들어, X 를 통계학 실력이라고 하고, Y를 아첨 능력이라고 하자. 그리고 X,Y 가 모두 승진 (Z) 에 영향을 준다고 해보자. 이 때, 승진 대상자만을 놓고 통계학 실력과 아첨 능력의 관계를 보면 둘 사이에는 음의 상관성을 확인할 수 있다. (이는 정확히 위 selection bias 에서 설명하는 그림과 같다.) 

 

이처럼 실제로는 통계 실력과 아첨 능력에는 아무런 상관성이 없으며, 승진에 영향을 주는 원인 변수일 뿐인데, 승진 대상자를 놓고 봤을 때는 둘 사이에 연관성이 생긴다 (false association). 승진한 어떤 사람이 아첨능력이 매우 좋다고 했을 때, 이것이 승진의 이유를 explain 해주므로, 이 사람의 통계학 실력은 좋지 않을 것이라고 '추정' 할 수 있을 것이다. 또한, 어떤 사람이 통계 실력이 매우 좋지 않음에도 불구하고 승진했을 때, 이 사람은 아첨 능력이 뛰어날 것이라고 추정할 수 있다. 이처럼 둘 사이에 음의 상관성이 존재하는 것을 직관적으로 이해할 수 있다.

 

 

반응형
반응형

 

CMH 검정과 통계량 계산 방법

 

범주형 자료 분석에서 코크란-멘텔-헨젤(Cochran-Mantel-Haenszel) 검정의 목표는 Z 가 주어질 때, X와 Y가 조건부 독립인지를 검정하는 것이다. 즉, Z를 고려했을 때, X-Y의 연관성이 존재하는지를 판단하는 검정이라고 할 수 있다. 이는 인과추론에서 말하는 X,Y가 조건부 독립 (conditional independence) 인지를 확인하는 검정이라고 할 수 있다. 보통 Z는 confounder 로 설정하는 경우가 많다. 만약, conditional independence 가 아니라고 한다면, Z 를 고려함에도 X-Y 연관성이 존재하는 것이고, 이는 X,Y 의 인과성에 대해 조금 더 근거를 더해준다고 할 수 있다.  CMH 검정은 2 X 2 X K 표에 대해서 활용할 수 있다. (K 는 Z의 수준 개수)  

 

그룹 i 에서의 흡연과 폐암의 연관성

  폐암X 폐암O
흡연X a b
흡연O c d

 

주요 지표

n = a+b+c+d

p1 = (a+b)/n (흡연X 비율)

p2 = (a+c)/n (폐암X 비율) 

m = n*p1*p2

 

CMH 통계량의 계산

그룹 i 에서의 CMH 통계량은 아래와 같다. 

 

$$ \frac{(a-m)^2}{m(1-p_1)(1-p_2)} $$

 

최종적인 CMH 통계량은 모든 그룹 i에서 위 값을 다 구해서 더한 것이다. 이 값은 자유도가 1인 카이제곱분포를 따른다는 것을 이용해 검정한다. 만약, 충분히 이 값이 큰 경우 그룹을 고려했을 때, 흡연과 폐암에 연관성이 있다고 결론을 낼 수 있다. 

 

위 수식에서 a-m 은 관측값에서 기대값 (평균) 을 빼준 것이고, 분모는 a의 분산을 의미한다. 이 분산은 초기하분포의 분산이다. 즉, cmh 통계량에서는 a가 초기하분포를 따른다고 가정한다. 즉, 수식은 a 에서 평균을 빼주고 표준편차로 나눈 값에 제곱이라고 할 수 있다. 

 

MH 공통 오즈비

 

그룹1

  X O
X 10 20
O 30 40

 

=> OR = 10*40 / 20*30 = 2/3

 

 

그룹2 

  X O
X 4 1
O 1 4

 

=> OR = 4*4 = 16 

 

1) 두 그룹의 공통 오즈비를 구하는 방법에는 단순히 두 그룹의 오즈비의 평균을 구하는 방법이 있을 수 있다. 이 경우 그룹2의 샘플수가 적음에도 불구하고 평균 오즈비는 8에 가깝게 높게 나온다. 

 

2) a*d 의 값을 모두 더한 값을 b*c 를 모두 더한 값으로 나누어주는 방법이 있다. 이러면 (10*40 + 4*4) / (20*30+1) = 0.69 가 나오게 된다. 이 값은 샘플수가 많은 그룹의 값으로 지나치게 치우친다. 

 

3) MH 공통 오즈비는 중도적인 방법으로 두 방법의 단점을 보완한다. 2) 방법에서 샘플수의 역수로 가중치를 줌으로써, 샘플수가 많은 그룹이 계산에 미치는 영향력을 의도적으로 줄여준다. 

 

(10*40/100 + 4*4/10) / (20*30/100 + 1/10) = 0.91 

 

즉, MH 공통 오즈비를 사용하면, 지나치게 그룹1에 치우치지 않으면서 적당한 공통 오즈비가 추정된다. 또한, 로그 MH 공통 오즈비의 분산을 계산할 수 있기 때문에, 공통 오즈비의 신뢰구간 및 오즈비가 유의미한지를 추론할 수 있다는 장점이 있따. 

 

예를 들어, 공통 오즈비가 0.91인 경우 로그 공통 오즈비는 -0.094이다. 그리고, 로그 공통 오즈비의 표준편차를 예를 들어 0.02라고 하자. 그러면 공통 오즈비의 95% 신뢰구간은 아래와 같이 계산된다. 

 

[exp(-0.094-1.96*0.02), exp(-0.094+1.96*0.02) ] = [0.88, 0.95] 

 

 

반응형