728x90

Ensemble Retriever란?

  • 앙상블 검색기는 Retriever의 list를 받아서 각각의 Retriever로 get_relevant_documents 방법으로 검색한 결과를 종합한 뒤 Reciprocal Rank Fusion 알고리즘으로 결과를 rerank한다.
  • Similarity search 만으로 성능이 잘 나오지 않을 때 사용하기 좋은 방법론이다.
  • 가장 흔히 사용하는 방식은 sparse retriever(BM25 같이 키워드 기반)와 dense retriever(embedding similarity)를 함께 사용하는 것이다.
  • Hybrid Search 라고도 알려져있고 sparse retriever는 keyword 기반으로 문서를 찾는데 특화되어 있고 dense retriever는 의미론적 유사도 기반으로 문서를 찾는데 특화되어 있으므로 두 종류를 합쳐서 사용하면 효과가 좋다.관련 문서 링크
  • Reciprocal Rank Fusion에 대한 자세한 설명은 👉여기
  • Ensemble Retriever 관련 Langchain 문서 👉여기
  • BM25 Retriever 관련 Langchain 문서 👉여기

Dependency 설치

pip install --upgrade --quiet  rank_bm25 > /dev/null
pip install rank_bm25
pip install faiss-cpu
from langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever
from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings

실습

Retriever 정의

문자열이 저장된 리스트 2개를 선언한다. 이후 bm25_retriever와 faiss_retriever를 정의한다.
metadatas에 source를 정해주고 가져올 문서의 개수는 k로 설정한다.

이후 ensemble_retriever를 구성하고 weights로 어떤 retriever의 결과를 더 반영할 지 정한다.

doc_list_1 = [
    "I like apples",
    "I like oranges",
    "Apples and oranges are fruits",
]

# initialize the bm25 retriever and faiss retriever
bm25_retriever = BM25Retriever.from_texts(
    doc_list_1, metadatas=[{"source": 1}] * len(doc_list_1)
)
bm25_retriever.k = 2

doc_list_2 = [
    "You like apples",
    "You like oranges",
]

embedding = OpenAIEmbeddings()
faiss_vectorstore = FAISS.from_texts(
    doc_list_2, embedding, metadatas=[{"source": 2}] * len(doc_list_2)
)
faiss_retriever = faiss_vectorstore.as_retriever(search_kwargs={"k": 2})

# initialize the ensemble retriever
ensemble_retriever = EnsembleRetriever(
    retrievers=[bm25_retriever, faiss_retriever], weights=[0.5, 0.5]

이제 결과를 살펴보자

docs = ensemble_retriever.invoke("apples")
docs

결과

[Document(page_content='You like apples', metadata={'source': 2}),
 Document(page_content='I like apples', metadata={'source': 1}),
 Document(page_content='You like oranges', metadata={'source': 2}),
 Document(page_content='Apples and oranges are fruits', metadata={'source': 1})]

Runtime Configuration

개발자는 runtime에 retriever들을 구성할 수 있다. 이를 위해서는 우선 field들을 configurable 할 수 있게 표시해야한다.

 from langchain_core.runnables import ConfigurableField

아래 과정을 통해서 개발자는 runtime에 faiss_retriever가 몇개의 문서를 반환할 지 결정할 수 있다.

 faiss_retriever = faiss_vectorstore.as_retriever(
    search_kwargs={"k": 2}
).configurable_fields(
    search_kwargs=ConfigurableField(
        id="search_kwargs_faiss",
        name="Search Kwargs",
        description="The search kwargs to use",
    )
)
ensemble_retriever = EnsembleRetriever(
    retrievers=[bm25_retriever, faiss_retriever], weights=[0.5, 0.5]
)
config = {"configurable": {"search_kwargs_faiss": {"k": 1}}}
docs = ensemble_retriever.invoke("apples", config=config)
docs

'인공지능 > RAG' 카테고리의 다른 글

LangChain (10) Gemini로 RAG 구현  (0) 2024.06.04
What is Elastic Search?  (0) 2024.06.03
Langchain - Hybrid Search 구현  (0) 2024.05.31
MongoDB와 Gemma를 사용한 RAG 실습(LangChain X)  (0) 2024.05.28
Langchain - MessagesPlaceholder  (0) 2024.05.24
728x90

전제 조건

Vector Store에서 Hybrid Search를 지원해야 하기 때문에 이를 지원하는 vector store를 선정한다.

본 예제에서는 Astra DB를 사용한다.

Dependency 설치

우선 관련 Dependency를 설치한다. 터미널에 접속후 아래 명령어를 실행한다.

!pip install "cassio>=0.1.7"

Astra DB 회원 가입 및 DB 생성

이후 링크에 접속해서 회원가입을 하고 연결 정보를 생성한다. 우리에게 필요한건 Database ID, application token, keyspace 이다.

사실 위 링크에서 하라는대로 하면 된다

회원가입

링크에서 회원가입을 진행한다.

DB 생성

  1. 링크에서 좌측에 Database를 누른다.
  2. 우하단의 검은색 버튼 Create Database를 클릭한다.


3. 여기서 Serverless(Vector)를 선택하고 DB 이름을 설정한다. 이름을 한번 설정하면 변경이 불가하니 신중하게 생성한다.
Provider마다 조금씩 region이 다르긴 한데 Lock 되어있는건 비용을 지불해야한다. Asia Region이 없으니 어쩔 수 없이 US east를 선택한다. 설정이 완료되면 Create Database를 클릭한다.
4. Database가 active 상태가 된 이후 우측에 Application Tokens 탭의 Create Token을 눌러서 토큰을 생성한다. Token이 생성되면 다시는 확인할 길이 없기 때문에 clipboard에 복사 후 안전한 곳에 저장하자. 해당 토큰은 자동으로 데이터베이스 관리자 역할을 부여받는다. 자세한 사항은 여기
5. 이후 Overview 옆의 Data Explorer를 클릭한다. 현재 Namespace는 "default_keyspace"인데 원하는 Namespace를 생성하면 된다. 생성하는데 시간이 조금 걸린다.

실습

앞서 얻은 정보를 토대로 아래 코드에 정보를 삽입후 실행한다.

import cassio

cassio.init(
    database_id="Your database ID",
    token="Your application token",
    keyspace="Your key space",
)

이후 Cassandra vector store를 Standard index analyzer로 생성한다. 자세한 내용은 이 링크

Vector Store 실험

from cassio.table.cql import STANDARD_ANALYZER
from langchain_community.vectorstores import Cassandra
from langchain_openai import OpenAIEmbeddings

embeddings = OpenAIEmbeddings()
vectorstore = Cassandra(
    embedding=embeddings,
    table_name="test_hybrid",
    body_index_options=[STANDARD_ANALYZER],
    session=None,
    keyspace=None,
)

vectorstore.add_texts(
    [
        "In 2023, I visited Paris",
        "In 2022, I visited New York",
        "In 2021, I visited New Orleans",
    ]
)

생성 후 결과는 아래와 같다

 'ee2a6192e7524ff49c95bd7f82e73d36',
 '0ef8aa454fe24569a6363436f8bddca3']
['271c225aaace4586b1c53e32f170013b',
 'ee2a6192e7524ff49c95bd7f82e73d36',
 '0ef8aa454fe24569a6363436f8bddca3']

이제 일반적인 유사도 검색을 하면 모든 문서가 검색되는 것을 확인할 수 있다.

vectorstore.as_retriever().invoke("What city did I visit last?")

결과는 아래와 같다.

[Document(page_content='In 2022, I visited New York'),
 Document(page_content='In 2023, I visited Paris'),
 Document(page_content='In 2021, I visited New Orleans')]

근데 Astra DB vector store의 body search argument를 넣으면 검색 결과를 필터링 할 수 있다.

가령, New라는 keyword가 들어간 도시 만을 찾고 싶다면 아래와 같이 적용할 수 있다.

vectorstore.as_retriever(search_kwargs={"body_search": "new"}).invoke(
    "What city did I visit last?"
)

결과는 다음과 같다.

[Document(page_content='In 2022, I visited New York'),
 Document(page_content='In 2021, I visited New Orleans')]

QA chain 생성

from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import(
    ConfigurableField,
    RunnablePassthrough,
)
from langchain_openai import ChatOpenAI
  • Prompt Template, model, retriever를 정의한다.
    template = """Answer the question based only on the following context:
    {context}
    Question: {question}
    """
    

prompt = ChatPromptTemplate.from_template(template)

model = ChatOpenAI()

retriever = vectorstore.as_retriever()


- 여기서 Configurable field를 추가한 retriever를 생성한다. 모든 Vector store retriever들은 `search_kwargs`를 field로 가진다.
    Vector store 특정 field가 저장된 딕셔너리 타입이다.

```python
configurable_retriever = retriever.configurable_fields(
    search_kwargs=ConfigurableField(
        id='search_kwargs',
        name='Search Kwargs',
        description='The search Kwargs to use',
    )
)

이제 chain을 구성한다.

chain = (
    {"context" : configurable_retriever, "question" : RunnablePassthrough()}
    | prompt
    | model
    | StrOutputParser()
)

실험

cofigurable field에 값을 넣지 않고 invoke 하는 경우 결과

chain.invoke("What city did I visit last?")

결과

'Paris'

하지만 configurable field에 값을 넣은 후에는

chain.invoke(
    "What city did I visit last?",
    config={"configurable": {"search_kwargs":{"body_search":"new"}}}
)

결과

'New Orleans'

'인공지능 > RAG' 카테고리의 다른 글

What is Elastic Search?  (0) 2024.06.03
Langchain - Ensemble Retriever  (1) 2024.05.31
MongoDB와 Gemma를 사용한 RAG 실습(LangChain X)  (0) 2024.05.28
Langchain - MessagesPlaceholder  (0) 2024.05.24
Langchain - ChatPromptTemplate  (0) 2024.05.23
728x90

설명

  • 사용 모델 : gemma
  • 목적 : Langchain 프레임워크 기반으로 오픈소스 LLM과 문서 임베딩을 직접 수행하여 vectore db에 저장한 후 RAG 시스템을 구현하는 것
  • 기술 스택
    • Langchian
    • mongo db
    • RAG
    • gemma

STEP 1 : 라이브러리 설치

  • PyMongo: MongoDB와 상호 작용하는 Python 라이브러리로, 클러스터에 연결하고 컬렉션 및 문서에 저장된 데이터를 쿼리하는 기능을 제공합니다.
  • Pandas: 효율적인 데이터 처리 및 분석을 위해 Python을 사용하는 데이터 구조를 제공합니다.
  • Hugging Face datasets: 오디오, 비전, 텍스트 데이터셋을 보유하고 있습니다.
  • Hugging Face Accelerate: GPU와 같은 하드웨어 가속기를 사용하는 코드 작성의 복잡성을 추상화합니다. Accelerate는 GPU 리소스에서 Gemma 모델을 활용하기 위해 사용됩니다.
  • Hugging Face Transformers: 사전 훈련된 모델 컬렉션에 접근을 제공합니다.
  • Hugging Face Sentence Transformers: 문장, 텍스트, 이미지 임베딩에 접근을 제공합니다.
!pip install datasets pandas pymongo sentence_transformers -q
!pip install -U transformers -q
# 아래 라이브러리는 GPU 사용시 설치
!pip install accelerate -q

STEP 2 : 데이터 준비

#데이터셋 로드
from datasets import load_dataset
import pandas as pd

dataset = load_dataset('AIatMongoDB/embedded_movies')

dataset_df = pd.DataFrame(dataset['train'])
dataset_df.head()
  • 우선 dropna를 사용해서 데이터의 "fullplot" 속성이 비어있지 않게 설정한다.
  • 두번째로 "plot_embedding" 속성을 제거한다. 그리고 gte-large 모델로 새로 임베딩을 생성해서 저장한다
#비어있는 데이터를 제거한다
dataset_df = dataset_df.dropna(subset=['fullplot'])
print('\nNumber of missing values in each column after removal')
print(dataset_df.isnull().sum())

#plot_embedding 제거
dataset_df = dataset_df.drop(columns = ["plot_embedding"])
dataset_df.head(5)

STEP 3 : Embedding 생성

  1. 임베딩 모델에 접근하기 위해서 SentenceTransformers import
  2. SentenceTransformers를 사용해서 gte_large 임베딩 모델 로드
  3. get_embedding 함수 정의
    • 텍스트 문자열을 입력으로 받아서 임베딩을 출력하는 함수
    • 우선 입력 텍스트가 비어있는지 확인 후에 비어있으면 빈 리스트를 반환하고 아니면 임베디
  4. "fullplot" 열의 값을 get_embedding 함수에 넣어서 각 영화마다 임베딩을 생성한다. 새로 생성된 임베딩값들은 새로운 열에 할당된다.
from sentence_transformers import SentenceTransformer
embedding_model = SentenceTransformer("thenlper/gte-large")

def get_embedding(text:str) -> list[float]:
    if not text.strip():
        print("Attempted to get embedding for empty text.")
        return []

    embedding = embedding_model.encode(text)

    return embedding.tolist()

dataset_df["embedding"] = dataset_df["fullplot"].apply(get_embedding)

dataset_df.head()

STEP 4 : 데이터베이스 셋업 및 연결

  • mngo db는 일반적인 디비와 벡터 디비 역할 모두 수행. 효율적으로 저장하고 query하고 벡터임베딩을 검색 -> 데이터베이스 관리, 유지, 비용
  • 새로운 MongoDB 데이터베이스를 생성하기 위해서 데이터베이스 클러스터를 셋업해야한다.
  1. 다음 링크로 들어가서 무료 MongoDB 회원가입을 한다. 👉 링크
  2. 'Database' 옵션을 선택하면 현존하는 클러스터들의 배포 정보가 있는 데이터베이스 배포 페이지로 이동한다. 'build cluster' 버튼을 눌러서 새로운 데이터베이스 클러스터를 생성한다
  3. 데이터 베이스에 적용 가능한 모든 configuration을 선택하고 모든 configuration 옵션을 선택했으면 'Create Cluster' 버튼을 클릭해서 새로 생성한 클러스터를 배포한다. MongoDB는 shared tab을 통해서 무료 클러스터 생성도 지원한다.

Concept를 생성할때 Python host의 ip를 whitelist에 추가하거나(열어두거나), 0.0.0.0/0으로 ip를 설정해야한다.

  1. 클러스터 생성 및 배포 후 데이터베이스 배포 페이지에서 클러스터를 접근할 수 있다.
  2. 클러스터의 'connect' 버튼을 눌러서 다양한 언어 driver를 통해 연결하기 위한 옵션을 볼 수 있다. 여기서 driver를 선택하면 URI를 확인할 수 있다.
  3. 이 실습은 cluster의 URI만 필요하다. 이를 google colab 환경변수로 설정해주면 된다. 변수명은 MONGO_URI

step 4.1 데이터베이스 및 콜렉션 셋업

사전 준비할 사항을 미리 확인하자

  • MongoDB Atlas를 위한 Database cluster 설정
  • 클러스터 URI 확보
    클러스터를 생성한 후에는 MongoDB Atlas cluster 내부에 데이터베이스와 콜렉션을 만들어야한다.
    좌측에서 데이터베이스 클릭 > browse collection > create 를 눌러서 생성할 수 있다. Database : movies, collection : movie_collection_2이다.

STEP 5 : 벡터 검색 인덱스 생성

  • 이 과정에서는 MongoDB Atlas에 벡터 인덱스가 생성이 되어야한다.

  • 다음 단계는 정확하고 효율적인 벡터 기반 검색을 위한 필수 과정이다.

  1. database collection에 들어가서 search index를 누른다
  2. Atlas Vector Search의 JSON Editor를 누른다
  3. 아래 json 형식대로 생성한다.
  4. 기다리면 status가 Not Ready에서 Active로 바뀐다
  • 벡터 검색 인덱스를 만들면 문서를 효율적으로 탐색해서 벡터 유사도 기반으로 쿼리 임베딩과 가장 유사도가 높은 임베딩을 가진 문서를 검색한다.
    벡터 검색 인덱스에 대한 자세한 정보는 여기
    {
    "fields": [{
       "numDimensions": 1024,
       "path": "embedding",
       "similarity": "cosine",
       "type": "vector"
     }]
    }
  • numDimension 필드의 값 1024는 gte-large 임베딩 모델이 생성하는 차원에 해당합니다. gte-basegte-small 모델을 사용하게 되면 numDimension 값이 각각 768이나 384가 된다.

STEP 6 : 데이터 연결 생성

아래 코드를 통해서 PyMongo를 활용해서 MongoDB 클라이언트 객체를 생성한다. 이를 통해 클러스터와 연결하고 데이터베이스와 콜렉션에 접근할 수 있다.

import pymongo
from google.colab import userdata

def get_mongo_client(mongo_url):
    """ Establish conncetion to the Mongo DB"""
    try:
        client = pymongo.MongoClient(mongo_uri)
        print("Connection to MongoDB successful")

        return client
    except pymongo.errors.ConnectionFailure as e:
        print(f"Connection failed {e}")
        return None

mongo_uri = userdata.get("MONGO_URI")
if not mongo_uri:
    print("MONGO URI not set in environment variables")

mongo_client = get_mongo_client(mongo_uri)

#ingest data into Mongo DB
db = mongo_client["movies"]
collection = db["movie_collection_2"]
#Delete any existing records in the collection
collection.delete_many({})
documents = dataset_df.to_dict("records")
collection.insert_many(documents)

print("Data ingestion into MongoDB completed")

STEP 7 : 사용자의 질문에 벡터 검색 수행

다음 단계는 쿼리 임베딩을 생성하고 MongoDB 검색 파이프라인을 정의해서 벡터 검색 결과를 반환하는 함수를 구현합니다.

파이프라인은 Vector Search 단계와 Project 단계로 구성되어 있고 생성된 벡터로 쿼리를 실행하면서 결과에 대한 검색 점수를 통합하는 동시에 줄거리, 제목, 장르와 같이 필요한 정보 만을 포함하도록 결과 형식을 지정합니다.

def vector_search(user_query, collection):
    """
    Perform a vector search in the MongoDB collection based on the user query.

    Args:
    user_query (str): The user's query string.
    collection (MongoCollection): The MongoDB collection to search.

    Returns:
    list: A list of matching documents.
    """

    # Generate embedding for the user query
    query_embedding = get_embedding(user_query)

    if query_embedding is None:
        return "Invalid query or embedding generation failed"

    # Define the vector search pipeline
    pipeline = [
        {
            "$vectorSearch": {
                "index": "vector_index",
                "queryVector": query_embedding,
                "path": "embedding",
                "numCandidates": 150,  # Number of candidate matches to consider
                "limit": 4,  # Return top 4 matches
            }
        },
        {
            "$project": {
                "_id": 0,  # Exclude the _id field
                "fullplot": 1,  # Include the plot field
                "title": 1,  # Include the title field
                "genres": 1,  # Include the genres field
                "score": {"$meta": "vectorSearchScore"},  # Include the search score
            }
        },
    ]

    # Excute the search
    results = collection.aggregate(pipeline)
    return list(results)

STEP 8 : 사용자 쿼리 처리 및 Gemma 로드

  • 구글 gemma를 사용하려면 huggingface 사이트에서 인가를 받아야한다. 👉링크
  • 인가를 받은 후 huggingface의 access token을 코랩 환경변수로 설정 한 후 모델 로드시 매개변수로 전달한다.
def get_search_result(query, collection):
    get_knowledge = vector_search(query, collection)

    search_result = ""
    for result in get_knowledge:
        search_result += f"Title: {result.get('title', 'N/A')}, Plot: {result.get('fullplot','N/A')}\n"

        return search_result
# Conduct query with retival of sources
query = "What is the best romantic movie to watch and why?"
source_information = get_search_result(query,collection)
combined_information = f"Query: {query}\nContinue to answer the query by using the Search Results:\n{source_information}."

print(combined_information)
import os
from google.colab import userdata
HUGGINGFACE_API_KEY = userdata.get('HUGGINGFACE_API_KEY')
from transformers import AutoTokenizer, AutoModelForCausalLM

tokenizer = AutoTokenizer.from_pretrained("google/gemma-2b-it", token = HUGGINGFACE_API_KEY)
#CPU를 사용하는 경우 아래 주석을 해제
# model = AutoModelForCausalLM.from_pretrained("google/gemma-2b-it")
#GPU를 사용하는 경우 아래 주석을 해제
model = AutoModelForCausalLM.from_pretrained("google/gemma-2b-it",token = HUGGINGFACE_API_KEY, device_map = "auto")

결과

Query: What is the best romantic movie to watch and why?
Continue to answer the query by using the Search Results:
Title: Shut Up and Kiss Me!, Plot: Ryan and Pete are 27-year old best friends in Miami, born on the same day and each searching for the perfect woman. Ryan is a rookie stockbroker living with his psychic Mom. Pete is a slick surfer dude yet to find commitment. Each meets the women of their dreams on the same day. Ryan knocks heads in an elevator with the gorgeous Jessica, passing out before getting her number. Pete falls for the insatiable Tiara, but Tiara's uncle is mob boss Vincent Bublione, charged with her protection. This high-energy romantic comedy asks to what extent will you go for true love?
# GPU로 텐서 이동
input_ids= tokenizer(combined_information, return_tensors="pt").to("cuda")
response = model.generate(**input_ids, max_new_tokens=500)
print(tokenizer.decode(response[0]))

결과

<bos>Query: What is the best romantic movie to watch and why?
Continue to answer the query by using the Search Results:
Title: Shut Up and Kiss Me!, Plot: Ryan and Pete are 27-year old best friends in Miami, born on the same day and each searching for the perfect woman. Ryan is a rookie stockbroker living with his psychic Mom. Pete is a slick surfer dude yet to find commitment. Each meets the women of their dreams on the same day. Ryan knocks heads in an elevator with the gorgeous Jessica, passing out before getting her number. Pete falls for the insatiable Tiara, but Tiara's uncle is mob boss Vincent Bublione, charged with her protection. This high-energy romantic comedy asks to what extent will you go for true love?
.

The best romantic movie to watch is **Shut Up and Kiss Me!** because it perfectly captures the essence of romantic comedies. The movie perfectly balances humor, heart, and romance, making it a delightful and unforgettable viewing experience.<eos>

REFERENCE https://github.com/huggingface/cookbook/blob/main/notebooks/en/rag_with_hugging_face_gemma_mongodb.ipynb

'인공지능 > RAG' 카테고리의 다른 글

Langchain - Ensemble Retriever  (1) 2024.05.31
Langchain - Hybrid Search 구현  (0) 2024.05.31
Langchain - MessagesPlaceholder  (0) 2024.05.24
Langchain - ChatPromptTemplate  (0) 2024.05.23
LangChain (9) Retrieval - Retriever  (0) 2024.04.08
728x90

Messages Placeholder

  • Messages Placeholder를 통해서 이미 존재하는 메시지를 전달할 수 있다.
  • 특정 key를 통해서 맵핑 시킨다.
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder

prompt = ChatPromptTemplate.from_messages(
    [
        ("system", "You are a helpful assistant."),
        MessagesPlaceholder("history"),
        ("human", "{question}")
    ]
)
prompt.invoke(
   {
       "history": [("human", "what's 5 + 2"), ("ai", "5 + 2 is 7")],
       "question": "now multiply that by 4"
   }
)

위 경우 invoke 시에 "history"라는 key에 메시지의 리스트가 value로 들어가있는 걸 확인할 수 있다.

prompt Template을 찍어보면 그 값은 아래와 같다

ChatPromptValue(messages=[
    SystemMessage(content="You are a helpful assistant."),
    HumanMessage(content="what's 5 + 2"),
    AIMessage(content="5 + 2 is 7"),
    HumanMessage(content="now multiply that by 4"),
])

Reference

https://api.python.langchain.com/en/latest/prompts/langchain_core.prompts.chat.MessagesPlaceholder.html

728x90

2024년 05월 23일 기준

ChatPromptTemplate이란?

  • Chat model 들을 위한 Prompt Template
  • Prompt Template을 사용하면 LLM에게 전달할때 system message, human message, ai message를 구분해서 제공할 수 있다.
  • 이러한 구분은 LLM에 chat history를 제공할때 편리하고 다양한 변수들을 추가할 수 있어서 편리하다.
  • 재사용성이 좋아서 반복적인 작업을 할 떄 편리하다.

예시 코드

from langchain_core.prompts import ChatPromptTemplate

template = ChatPromptTemplate.from_messages([
    ("system", "You are a helpful AI bot. Your name is {name}."),
    ("human", "Hello, how are you doing?"),
    ("ai", "I'm doing well, thanks!"),
    ("human", "{user_input}"),
])

prompt_value = template.invoke(
    {
        "name": "Bob",
        "user_input": "What is your name?"
    }
)

위 Prompt의 결과는 아래와 같다

Output:
ChatPromptValue(
   messages=[
       SystemMessage(content='You are a helpful AI bot. Your name is Bob.'),
       HumanMessage(content='Hello, how are you doing?'),
       AIMessage(content="I'm doing well, thanks!"),
       HumanMessage(content='What is your name?')
   ]
)

Messages Placeholder

  • "place holder"라는 키를 사용해서 Prompt 중간에 내용을 추가할 수 있다.
  • MessagesPlaceholder라는 class를 사용해도 되고 튜플형식으로 전달해도 된다.
  • 아래의 경우 invoke 시에 conversation이라는 키로 Prompt Template에 대화 내용을 전달해서 LLM의 답변을 받는다
  • template 아래에 ("human", "{input}") 이런식으로 사용자의 input도 입력받을 수 있다.
template = ChatPromptTemplate.from_messages([
    ("system", "You are a helpful AI bot."),
    ("placeholder", "{conversation}")
    # 이런 코드로 대체 가능 MessagesPlaceholder(variable_name="conversation", optional=True)
])

prompt_value = template.invoke(
    {
        "conversation": [
            ("human", "Hi!"),
            ("ai", "How can I assist you today?"),
            ("human", "Can you make me an ice cream sundae?"),
            ("ai", "No.")
        ]
    }
)

Prompt Template의 형태

Output:
ChatPromptValue(
   messages=[
       SystemMessage(content='You are a helpful AI bot.'),
       HumanMessage(content='Hi!'),
       AIMessage(content='How can I assist you today?'),
       HumanMessage(content='Can you make me an ice cream sundae?'),
       AIMessage(content='No.'),
   ]
)

Reference

https://api.python.langchain.com/en/latest/prompts/langchain_core.prompts.chat.ChatPromptTemplate.html

728x90

SSH란?

SSH는 Secure Shell의 줄임말로, 원격 호스트에 접속하기 위해 사용되는 보안 프로토콜

*Shell(쉘): 명령어와 프로그램을 사용할 때 쓰는 인터페이스로 사용자로부터 명령을 받아 그것을 해석하고 실행

기존 원격 접속은 ‘텔넷(Telnet)’이라는 방식을 사용했는데, 암호화를 제공하지 않기 때문에 보안상 취약했다.

실제로 WireShark같은 패킷 분석 프로그램을 이용하면 누구나 쉽게 원격 접속 과정에서 옮겨지는 비밀번호나 파일 내용 등의 데이터를 탈취할 수 있다. 이러한 보안 문제를 해결하기 위해서 암호화 기술인 SSH가 등장했고 현재 원격 접속을 위해서 필수적이다.

클라우드 환경에 접속하기 위해서는 원격접속이 필수이므로 SSH도 그만큼 많이 사용하게된다.

SSH 작동 원리

SSH를 구성하는 가장 핵심적인 키워드는 ‘KEY(키, 열쇠)’입니다. 사용자(클라이언트)와 서버(호스트)는 각각의 키를 보유하고 있으며, 이 키를 이용해 연결 상대를 인증하고 안전하게 데이터를 주고 받게 된다. 키를 생성하는 방식은 두가지로 ‘대칭키’와 ‘비대칭키(또는 공개 키)’ 방식이다.

비대칭키 방식

https://medium.com/@jamessoun93/ssh%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80%EC%9A%94-87b58c521d6f

SSH 연결에서는 클라이언트와 서버가 서로를 증명해야하는데 이 시점에서 비대칭키가 사용된다. 서버나 클라이언트가 공개 키와 개인 키를 가지는 key pair를 만든다.

사용자가 키 페어를 생성한 경우 공개 키를 서버에 전송한다. 이때 공개 키는 누구나 가져도 상관이 없다.

서버는 수신한 공개키로 랜덤한 숫자를 생성하고 이 값은 클라이언트가 올바른 개인키를 가지고 있는지 확인하는 용도로 사용된다. 사용자는 개인키로 이 값을 풀고 서버에 전송한다.

서버는 수신한 값을 원래 본인이 생성한 값과 비교해서 두 값이 같으면 인증이 완료된다.

 

대칭키 방식

https://medium.com/@jamessoun93/ssh%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80%EC%9A%94-87b58c521d6f

서로 클라이언트와 서버라는 사실을 확인했으니 정보를 주고받을 수 있다.

정보를 전송할때 정보 유출을 막기 위해서 대칭키를 이용하여 암호화를 수행한다.

대칭키 방식에서는 비대칭키 방식과 달리 한 개의 키만을 사용한다.

사용자 또는 서버는 하나의 대칭 키를 만들어 서로 공유하고 공유된 대칭 키를 이용해 정보를 암호화하면, 받은 쪽에서 동일한 대칭 키로 암호를 풀어 정보를 습득하게 된다. 대칭키는 정보 교환이 완료되는 대로 폐기하고 접속시마다 새로운 대칭키를 생성해서 사용한다.

Reference

https://library.gabia.com/contents/infrahosting/9002/

728x90

Support Vector Machine

support vector machine은 클래스 간의 경계면(boundary)를 찾는 ML 기법이다.

Maximum Margin

SVM은 경계면을 야무지게 찾기 위해서 Maximum Margin을 갖는 Linear Function을 찾는다.

우선 classifier를 하나 잡고 margin을 넓히다 sample(data)와 처음 만나는 순간 멈춘다.

그래서 모든 가능한 linear function중에서 margin이 최대가 되는 classifier를 찾는다.

이렇게 찾는 이유는 이렇게 해야 이상치에 Robust하고 강한 일반화 능력을 갖게 된다. 즉 성능이 좋아진다.

Support Vectors

Support Vector는 decision surface y = 0에 가장 가까운 데이터 혹은 점이다

📌decision surface를 결정하는 건 support vector들 뿐이다. 나머지 값들은 상관이 없다.

boundary의 식은 $(y=)W_1 x_1 + W_2 x_2 = 0$이다. 따라서, W1, W2를 찾아야한다.

샘플에는 2가지 그룹이 있는데 $x^-$ : negative 그룹과 $x^+$ : positive 그룹이다.

그리고 가운데 y = 0 직선 즉, classifier equation(Linear)은 $W^Tx+b=0$이고 SVM 학습의 목표는 이 수식의 W와 b값을 찾는 것이다.

($W^Tx + b = 0$인 이유는 W가 2차원 벡터라고 했을때

$W = \begin{bmatrix} W_{1} \\ W_{2} \end{bmatrix}$ $x = \begin{bmatrix} x_{1} \\ x_{2} \end{bmatrix}$이기 때문에 벡터 연산을 위해서는 Transpose 또는 전치를 해줘야한다.

Classifier 식에 positive sample을 넣으면 $W^Tx^+ + b > 0$이 되고 negative sample을 넣으면 $W^Tx^+ + b < 0$이 된다.

그리고 support vector를 넣었을때는 Positive Support Vector의 경우 $W^Tx^+ + b = 1$이 되고 Negative Support Vector를 넣으면 $W^Tx^+ + b = -1$이 된다.

그렇기 때문에 Margin의 크기를 구하면 $\frac{2}{||w||_2}$가 된다. ($||w||_2=\sqrt{w^Tw}$ 이고 평행한 두 직선 사이 거리 $d= \frac{|c-c'|}{||w||_2}$)

이를 수식으로 증명해보자.

d를 margin의 길이라고 하자.

그러면 y = 1 위의 점 $x^+$는 다음과 같이 표현할 수 있다. $x^+ = x^- + d \frac{W}{||W||_2}$

(여기서 $ \frac{W}{||W||_2}$는 classifier의 기울기와 수직인 벡터이다. 그렇기 때문에 y = -1위의 점 $x^-$에서 $ \frac{W}{||W||_2}$방향으로 d만큼 가면 y = 1 위의 점 $x^+$에 갈 수 있다)

앞서 말했듯이 SVM 학습의 목표는 최대의 Margin을 갖게 하는 것이다.

Margin의 값은 $\frac{2}{||w||_2}$이므로 제약조건 $W^Tx^+ + b \geq 1$과 $W^Tx^+ + b \leq -1$을 만족하면서 반대로 $\frac{||w||_2}{2}$를 최소화 하면 된다.

Nonlinear Classification

Nonlinear classification도 Kernel을 통해서 가능하다. 예를 들어서 RBF kernel의 경우 아래와 같은 형태를 가지고 있다.

원래 차원에서 선형적으로 SVM을 구현하기 힘들때 고차원으로 맵핑 시킨후 decision surface를 구하는 방법이다.

위의 이미지에서 보이는 것처럼 3차원의 데이터에 대해서 decision surface를 평면으로하는 SVM을 구현할 수 있다.

여기서 mapping function을 $\phi$라 하면 $R^2$의 데이터를 입력받아서 $R^6$의 데이터로 변환합니다.

https://ratsgo.github.io/machine%20learning/2017/05/30/SVM3/

이런 변환을 사용하면 XOR 데이터가 선형 분리가 가능한 데이터로 바뀐다.

근데 고차원 맵핑을 하게되는 경우 그 연산량이 매우 크다. 이런 경우 다항식 커널을 사용하면, 고차원 매핑 없이 내적을 계산할 수 있다.

고차원 매핑의 직접 계산

높은 차원으로의 데이터 매핑은 데이터를 복잡한 형태의 결정 경계를 가질 수 있는 더 높은 차원의 공간으로 변환한다.

예를 들어, 두 개의 특성을 가진 데이터 $x = (x_1,x_2)$가 있을 때, 이 데이터를 다항식 커널을 사용하여 3차원 공간으로 매핑하려고 한다고 가정해 보자.

매핑 함수 $\phi(x)$는 다음과 같이 될 수 있다:

$$ \Phi(x) = (1, \sqrt{2}x_1, \sqrt{2}x_2, x_1^2, x_2^2, \sqrt{2}x_1x_2)$$

이제 두 데이터 포인트 x와 z의 내적은 다음과 같이 높은 차원에서 계산된다:

$$\Phi(x)^T \Phi(z) = (1, \sqrt{2}x_1, \sqrt{2}x_2, x_1^2, x_2^2, \sqrt{2}x_1x_2) \cdot (1, \sqrt{2}z_1, \sqrt{2}z_2, z_1^2, z_2^2, \sqrt{2}z_1z_2)$$

이것은 고차원 공간에서의 복잡한 계산을 요구하고, 차원이 클수록 계산량이 기하급수적으로 증가한다.

원래 입력 공간에서의 연산 (커널 트릭)

커널 트릭은 고차원 공간에서의 복잡한 계산을 피할 수 있는 방법을 제공한다.

같은 예에서 다항식 커널을 사용하면, 높은 차원 매핑 없이 내적을 계산할 수 있다.

2차 다항식 커널은 다음과 같이 정의된다:

$$K(x,z)=(x \cdot z + c)^2$$

여기서 c는 커널 함수의 매개변수다. 이제 원래의 특성 공간에서 두 데이터 포인트 x와 z의 커널은 다음과 같이 계산된다:

$$K(x,z) = ((x_1 z_1 + x_2 z_2)+c)^2$$

이렇게 하면 높은 차원으로의 실제 매핑을 계산할 필요 없이 원래의 차원에서 고차원 매핑의 효과를 얻을 수 있다.

결과적으로, 커널 함수는 복잡한 변환을 수행하는 것과 동일한 결과를 더 적은 계산으로 얻을 수 있게 해 준다.

Kernel 함수들

  • 선형 커널 (Linear Kernel): $K(x_i,x_j) = x_i^Tx_j$
  • 다항식 커널 (Polynomial Kernel): $K(x_i,x_j) = (x_i^Tx_j+c)^d$
  • 가우시안 RBF 커널 (Gaussian Radial Basis Function Kernel): $K(x_i,x_j) = exp (−γ∥x_i − x_j∥^2)$
  • 시그모이드 커널 (Sigmoid Kernel): $K(x_i,x_j) = tanh(αx_i^T x_j+c)$

SVM summary

svm 장점

  • 효율적이다
    • 다른 데이터 다 무시하고 Support Vector만 사용
    • 계산량이 적다. 그래서 고차원 데이터를 다루는데 유리하다
  • 강력하다
    • 사람이 수행하는 전처리 과정을 거치면 좋은 분류 성능을 보인다.
    • Kernel 방법을 사용하면 nonlinear classification 문제도 해결할 수 있다.
    • 딥러닝이 발전하기 이전에 거의 모든 분야에서 SOTA였다.
728x90

K Nearest Neighbors

KNN 알고리즘은 간단한 분류 알고리즘으로 거리에 따라 새로운 데이터의 class를 분류한다.

별 모양의 새로운 데이터가 들어오면 하이퍼파라미터 k를 기준으로 새로운 데이터에서 가장 가까운 k개의 데이터를 찾고 해당 데이터중에서 가장 많은 데이터를 가진 Class로 분류를 한다.

아래 그림의 경우에서 k = 3일때 3개의 데이터 중에서 class 1이 1개, class 2가 2개이므로 class 2로 분류한다.

반면에 k = 5일때 5개의 데이터 중에서 class 1이 3개, class 2가 2개이므로 class 1으로 분류한다.

KNN 알고리즘의 과정은 다음과 같다.

  1. 데이터를 분석하고
  2. 기존 데이터와 새로운 데이터의 거리를 계산
  3. k개의 가장 가까운 샘플을 선정하고
  4. 가장 많은 class를 선정한다.

KNN 알고리즘에서 일반적으로 k값은 동률인 경우를 막기 위해서 홀수를 사용한다.

Decision boundary


Decision boundary란 두 개의 서로 다른 클래스의 기준이 되는 선이다.
하지만 decision boudary는 이상치(outlier)에 의해 이상한 부분에 존재할 수 있다.

이러한 것처럼 경계면을 부드럽게 만들기 위해서는 k의 값을 키우는 것이 하나의 방법이 될  수 있다.

 

k = 1로 하면 이론상 훈련 데이터에 완벽하게 동작한다. 하지만 실제 데이터에서는 아마 제대로 동작을 안할 것이다.

Train Test Split

따라서, 우리는 전체 훈련 데이터를 Train Validation Test 데이터로 나눠서 Train data로 훈련을 하고 validation으로 검증을 해서 hyperparameter 값을 최적화한다. 이 경우에는 k값을 수정한다. 이후에 Test data로 최종 검증을 한다.

Cross Validation

Cross validation은 전체 데이터에서 test 데이터를 제한 나머지를 fold로 나눈 후 학습시마다 각 fold를 돌아가면서 validation으로 사용하고 결과의 평균을 사용한다. 

KNN 장단점

KNN의 장점은 모델 자체가 단순하는 것이다. 그리고 학습이 필요 없다. 왜냐하면 label 단 데이터만 있으면 되니까.

반대로 단점은 메모리를 많이 사용하고 연산이 많다는 점이다. 학습시마다 모든 데이터와 거리 계산이 필요하기 때문이다.

728x90

https://medium.com/@indirakrigan/vector-norms-cc12b80f5369

Data science에서 vector의 크기는 중요한 역할을 한다. Vector의 크기를 구하는 구하는 식을 Norm이라 한다.

Norm이란 벡터를 하나의 음이 아닌 수로 맵핑하는 함수이다.

L2 Norm (Euclidean Norm)

p = 2일때 Norm을 L2 norm이라 하고 다른 이름으로는 Euclidean Norm이라고도 한다.

L2 Distance

원소 별로 차의 제곱을 다 합해주고 이를 마지막에 1/2 제곱하면 즉, 루트로 감싸면 L2 distance이다.

즉, 일반적으로 생각하는 두 점 사이의 거리이다.

L1 Norm (Manhattan Norm)

p = 1일때 Norm을 L1 Norm이라 하고 다른 이름으로 Manhattan이라 한다.

위 식에서는 각 원소별로 절대값을 씌운 값의 총 합이 L1 Norm이 된다.

L1 Distance

L1 distance를 구하는 경우 식은 아래와 같다.

두 점사이 각 원소의 차이를 구한 후 절대값을 씌워서 총 합을 구하면 L1 Norm이다.

Max Norm(P = $\infty$)

p = $\infty$일때 각 좌표중에서 max의 절댓값을 가져온다.
$$ ||x||_{\infty} = max(|x_i|)

Cosine similarity

마지막으로는 코사인 유사도를 살펴보자. 코사인 유사도는 텍스트 분류, 벡터 검색등 다양한 분야에서 활용된다.

https://m.blog.naver.com/sjc02183/221866765335
https://m.blog.naver.com/sjc02183/221866765335

각도에 기반한 유사도 측정 방식으로 방향이 같으면 유사도가 높다고 판단한다.

L1 Loss vs L2 Loss

L1 norm과 L2 norm을 이용해서 Loss Function을 구성한 것이다.

Target Label : y 이고 Output vector : f(x)일때 Error : y - f(x)이고 Loss는 error의 크기이다(norm 형태)

따라서, L1-Loss와 L2-Loss를 식으로 나타내면 다음과 같다.
L1-Loss
$$ L = \sum^n_{i=1} |y_i - f(x_i)| $$
L2-Loss
$$ L = \sum^n_{i=1} ((y_i - f(x_i))^2 $$

L2-Loss 식에서 루트가 없는 이유는 미분을 용이하게 하기 위해서이다.(Gradient Descent시에 미분하는데 루트 있으면 계산량 늘어나니까)

728x90

Gradient Descent

이는 현실적으로 Loss Function의 최소값을 구하기 위해 고안된 방법으로 아래의 식으로 구한다.

$$W^{(n+1)}=W^{(n)}-\gamma_W \frac{\delta_{Loss}}{\delta_{W}}$$

$$b^{(n+1)}=b^{(n)}-\gamma_b \frac{\delta_{Loss}}{\delta_{b}}$$

여기서 $\gamma_W$는 learning rate로 각 epoch마다 기울기를 얼마나 변화시킬지, 즉 밑에 나오는 그래프에서 얼마나 이동할지 거리를 결정하는 매개변수이다.

그리고 $-\frac{\delta_{Loss}}{\delta_{b}}$는 기울기와 반대방향으로 진행한다는 의미이다. 이게 무슨소리인가하면

위의 그림을 살펴보면 최소값으로 향하기 위해서는 기울기의 반대방향으로 진행해야한다.

  1. 기울기가 음인 경우에는 양의 방향으로 가야 극소점으로 이동한다.
  2. 기울기가 양인 경우에는 음의 방향으로 가야 극소점으로 이동한다.

Learning rate

Learning rate에 대해 조금더 알아보면 lr이 너무 작으면 학습 속도가 느리고 너무 크면 최소점을 지나버린다.

따라서, 적절한 lr을 선택하는 것이 모델 훈련에 중요한 요소이다.

Local Minima

머신러닝 분야의 고질적인 문제인데 Loss Function을 평면이나 공간에 표현했을때 여러개의 극소점이 존재하게 되는데
그 중에서 전역적으로 최소인 점은 1개이다. 하지만 모델이 학습과정에서 위의 파란 선과같은 과정을 통해 지역국소점에 빠지게 되면
이를 빠져나올 방법이 사실 없다. 왜냐하면 복잡한 모델의 경우 위 그림처럼 나타내는것 자체가 불가능하기 때문이다.

아래는 GD 식이다.

GD 병렬 계산

GD를 계산할때 W, b를 아래와 같이 병렬적으로 계산할 수 있다.

Loss(W,b)에 대해서 기울기 값 Gradient는 아래와 같이 표현한다.

높은 차원의 Loss(W,b)에 대해서도 GD로 동일하게 계산한다.

오른쪽은 W,b에 대해서 그린 등고선이다. 최솟값은 가운데 점이므로 기울기의 반대 방향을 따라서 가운데 점으로 이동한다.

가중치가 여러 개인 일반적인 경우 $f(x)=Wx$ 식이 있을때 가중치 W와 x는 아래와 같은 행렬이다.

그리고 Gradient Descent 값은 $$ W^{(n+1)}=W^{(n)}-\gamma \Delta_{W} Loss(W^{(n)}) $$

Stochastic Gradient Descent

기존의 GD는 Loss Function의 값을 구할때 모든 데이터에 대해서 Loss값을 구한후 다 더해서 데이터의 총 수로 나눠줬다.

하지만 이러한 방식은 계산량이 많고 오래 걸린다.

전체 데이터셋에 대해서 Gradient를 평가하는 기존 방식에서 벗어나서 SGD는 데이터를 여러 개의 부분집합(mini-batch)으로 나누고 gradient를 계산하고 가중치를 업데이트한다.

SGD는 GD에 비해 연산량이 적고 빠르지만 더 "oscillate" 또는 "진동"한다. 진동한다는 의미는 목적지를 향해 갈때 좌우로 더 움직이는 것을 뜻한다.

+ Recent posts