책에서는 RAG랑 Agents 두 가지 패턴을 알려주나, 내용이 너무 길어서 RAG만 스터디에서 발표함.
머리 터지는 줄 알았네.
- ML model 엔지니어링과 동일한 목표: "input에 대한 처리를 위한 필수 정보를 model에게 넘겨주자."
- 많은 이들이 model이 충분히 긴 context를 수용할 수 있으면 RAG의 시대도 끝날 거라 하지만, RAG는 죽지 않았다.
- context가 길어질 수록 중간의 내용이 손실되기도 하고, cost와 latency도 문제, 그리고 모든 지식을 model에 학습시킬 수도 없음.
- Retrieval Mechanism vs. Attention Mechanism
- Anthropic의 Introducting Contextual Retrieval에서 나온 것처럼, knowlege base가 20만 tokens 미만이라면 그냥 프롬프트에 모두 때려넣는 게 도움이 될 수도 있다. (model이 수용할 수 있다는 전제 하에)
- Attention is All you need(2017) 논문이 발표된 이후 많은 LLM은 transformer 아키텍처를 갖는데, 어지간하면 얘가 알아서 맥락 파악 잘 함. -> 자세히 들어가면 이게 또 엄청 재밌는 내용.
- retrieval은 model에 input을 넣기 전에 미리 관련된 문서를 조회하는 과정. -> model을 좀 더 효율적으로 써보자.
- RAG는 크게 2개 컴포넌트로 구성됨.
- retrieval: 외부 메모리 소스로부터 정보를 조회
- 검색(search)이 조회(retrieval)보다 넓은 개념이지만, 책에서는 동등한 개념으로 취급하고 진행
- generator: 조회한 정보를 기반으로 응답 생성
- retriever의 quality가 RAG 성공 여부를 결정하기도 하나, 어느정도 선을 넘어서면 model이 output token 생성하는 게 병목일 수도.
- retrieval: 외부 메모리 소스로부터 정보를 조회
- sparse vs. dense retrieval
- sparse retrival
- 각 dimension이 하나의 token의 의미를 표현 (해석 가능, 나머지 dimension은 모두 0)
- e.g. ["food", "banana", "slug"] vocabulary가 존재하고, 각각 [0, 1, 2]라는 값을 가지면, [[1, 0, 0], [0, 1, 0], [0, 0, 1]] 벡터로 표현
- TF-IDF, BM25 같은 retrival가 여기 속하는데, documents가 너무 많으면 dimension이 수조 차원으로 넘어갈 수도 있어서 기업 입장에서는 꺼려지는 부분.
- 각 dimension이 하나의 token의 의미를 표현 (해석 가능, 나머지 dimension은 모두 0)
- dense retrieval
- 모든 dimension이 함께 token의 의미를 표현 (해석 어려움, 0으로 채워진 dimension 거의 없음)
- e.g. "banana"라는 단어를 768-dim dense vector로 변환한다고 가정
=> [0.023, -0.156, 0.892, 0.034, -0.567, 0.123, ..., 0.045]
- e.g. "banana"라는 단어를 768-dim dense vector로 변환한다고 가정
- aboutness(~에 관한 것)을 포착해야 하는 semantic retrieval 시 사용.
- e.g. "banana" → [0.82, -0.15, 0.45, ...]
"apple" → [0.79, -0.12, 0.43, ...] ← 비슷한 과일이라 벡터가 유사
"car" → [-0.56, 0.88, -0.23, ...] ← 완전 다른 개념이라 벡터가 다름
- e.g. "banana" → [0.82, -0.15, 0.45, ...]
- 중요한 점
- embedding model이 매우 중요함. 비슷한 의미를 갖는 token을 비슷한 위치에 놓이도록 fine-tuning 필요할 수도 (heuristic한 영역) -> embedding model이 바뀌면 모든 데이터를 다시 re-embedding해야 하는 미라클 현상. (사실상 불가능)
- ANN 해도 documents 수가 너무 많으면 의미 없음. 따라서 nearest-neighbor search 어떻게 할 지도 고민 많이 해야 함.
- 대표적인 예시 BERT(Bidirectional Encoder Representations from Transformers)
- MLM(Masked Language Model)로 빈칸 채우기 문제 대량 학습 -> token의 meaning을 학습하고 embedding
- 모든 dimension이 함께 token의 의미를 표현 (해석 어려움, 0으로 채워진 dimension 거의 없음)
- SPLADE(Sparse Lexical and Expansion) = 학습된 sparse embedding
- 전통적인 sparse embedding의 정밀함과 dense embedding의 의미적 풍부함을 결합한 고급 embedding ("BERT의 지능 + Sparse의 효율성과 해석 가능성", 쉽게 말해서 BERT 기반인데 출력을 spare vector로 변환하는 것)
- BERT 같은 사전학습 언어 모델이 단어들 간의 연결을 파악
- 다른 용어들의 관련성에 가중치를 부여하고, term expansion을 가능하게 -> 원본 텍스트에 없던 관련 용어도 식별이 가능함.
- Efficient Inverted Indexes for Approximate Retrieval over Learned Sparse Representations(2024)에서 SPLADE는 BM25와 다른 통계적 특성을 가져서, 기존 역색인 최적화가 안 먹힌다고 함. -> "어차피 내적의 대부분은 상위 몇 개 차원이 결정한다"는 사실을 이용해, 클러스터링 + summary vector로 새로운 pruning 전략을 만듦
- sparse retrival
- Tokenization
- 문자 기반 토큰화: 텍스트를 개별 문자로 쪼개어 각 문자를 하나의 토큰으로 취급 (e.g. hello = 5 tokens) -> 비용 & 틀릴 확률 증가
- LLM은 먼저 나올 토큰 생성하면, 그 다음 나올 토큰을 확률적으로 계산하여 생성하는 로직을 가짐. 그런데 문자 기반 토큰화의 토큰이 많아서, 틀린 토큰을 생성할 가능성 증가.
- 단어 기반 토큰화: 텍스트를 단어 단위로 분리 (e.g. "hello, world!" -> ["hello", ",", "world", "!"]) -> 단어 사전 관리 비효율, 합성어가 의미를 잃어버릴 수 있음 (e.g. hot dog -> hot, dog)
- OOV (Out of Vocabulary) 문제: ["고양이", "강아지", "먹다", ...]를 학습했는데, "골든리트리버가 사료를 먹는다"가 입력으로 들어오면, "골든리트리버" 의미 해석 불가
- 근데 요새는 Vocabulary가 워낙 커서 OOV는 예전만큼 고려하지 않는 추세
- 모든 단어를 저장하는 사전을 만들면? 한 단어에서 나올 수 있는 모든 형태를 가지고 있어야 하는 공간 낭비
- n-gram을 사용하면? 텍스트를 n개 단위로 쪼갬. (e.g. if n = 2, "hello" -> ["he", "el", "ll", "lo"])
- 서브 워드 기반 토큰화: 작은 문자쌍의 빈도수를 계산하여, 자주 등장하는 쌍을 하나의 token으로 결합. (e.g. "Machine learning" -> ["Ma", "chine", "learn", "ing"])
- 합성어도 추론 가능 (e.g. "child" + "ish" -> "childish" = 어린아이같은)
- 문자 기반 토큰화: 텍스트를 개별 문자로 쪼개어 각 문자를 하나의 토큰으로 취급 (e.g. hello = 5 tokens) -> 비용 & 틀릴 확률 증가
- Term-based retrieval vs. Embedding-based retrieval
- Term-based retrieval
- 정확한 검색(Lexical retrieval)할 때 주로 사용.
- 문자 그대로 검색하는 lexical retrieval을 사용하면?
- 단점1: 많은 문서에 해당 terms가 포함되어 있을 가능성 높음 -> context overflow -> solution: TF(term frequency) "term가 문서에 많이 포함되어 있을 수록 관련성이 높을 것"
- 단점2: 모든 terms가 중요하진 않음. to, for, at 같은 전치사는 제외. -> solution: IDF(inverse document frequency) "term이 모든 문서에 등장한다면 유익한(informative) 정보는 아닐 것이다."
- TF-IDF = TF * IDF = \( \sum_{i+1}^q (IDF(t_i) * TF(f_i, D)) \)
- TF: 연관성, term이 문서에 얼마나 포함? (단순 계산, 불린 빈도, 로그 스케일 빈도, 증가 빈도) -> 무식하게 하면 문서 길이가 길거나, 단순히 단어 반복횟수만 많은 것들이 과하게 유리해지니 여러 수식이 존재함.
- IDF: 유익성, term이 전체 문서 중 얼마나 포함? (IDF = \( \log \frac{(전체 \; 문서 \; 수)}{(term이 \; 등장하는 \; 문서 \; 수)} \)) -> for, the, at 같은 무의미한 token은 걸러내는 작업.
- TF-IDF가 높다 = term이 문서에 많이 나오고, 다른 문서에는 많이 안 나옴 = 문서를 특징 지을 수 있는 term
- 두 가지 term-based retrieval solution이 유명
- Elasticsearch, Opensearch
- BM25: TF-IDF 개선 버전 (BM25+, BM25F 같은 변형도 존재)
- TF-IDF 문제점
- 문제1: TF가 무한 증가 (e.g. "고양이"가 100번 나오면 더 중요? 일정 횟수를 넘어서면 관련성이 크게 증가하지 않을 것이다.)
- 문제2: 문서 길이 미고려 (e.g. 100단어 문서에서 고양이 5번 등장 = 1,000단어 문서에서 고양이 등장??)
- 개선점
- TF 포화 (Saturation): 포화 구간을 넘어서면 증가량 억제
- \( \frac{tf(t, d) * (k_1 + 1)}{tf(t,d) + k_1} \)
- 문서 길이 정규화
- \( k_1 * (1 - b + b * \frac{문서길이}{평균문서길이}) \)
- b=0이면 문서 길이 무시, b=1이면 문서 길이 완전 반영(가중치). -> 문서 길이가 짧을 수록 패널티가 적어서 점수 유리
- k1(TF 포화 정도 조절), b(문서 길이 영향도 조절) parameters tuning 가능
- TF 포화 (Saturation): 포화 구간을 넘어서면 증가량 억제
- TF-IDF 문제점
- Embedding-based retrieval
- semantic retrieval 중점. 의미론적 유사도
- token을 embedding model 사용해서 vector 좌표계로 변환 (retrieve할 때도 query를 embedding해야 함.)
- embedding model이 구리면 노답임.
- 실제 semantic retrieval system은 `reranker`나 `cache`도 등장할 거임.
- word2vec: Efficient Estimation of Word Representations in Vector Space(2013)
- vector database
- vector 검색을 빠르고 효율적으로 하는 방향으로 인덱싱하고 저장 -> 굳이 vector DB여야 할까? 요샌 많은 DB나 Store들도 vector search 기능 확장하는 경우 많음.
- A nearest-neighbor search problem
- 어떻게 K개의 가장 가까운 이웃을 찾을 것인가?
- brute force (k-NN): query를 임베딩하고, DB의 모든 데이터와 유사도 비교 후 rank를 매겨서 k개를 반환
- ANN(Approximate Nearest Neighbor): "정확도를 조금 포기하고, 속도를 얻자"
- LSH(locality-sensitive hashing), HNSW(Hierarchical Navigable Small World), Product Ouantization, IVF(inverted file index), Annoy(Approximate Nearest Neighbors Oh Yeah) 등 -> 너무 많아서 그냥 패스..
- 벡터 검색 알고리즘들은, 유사한 벡터들이 서로 가까이 위치할 가능성을 높이기 위해 사용하는 휴리스틱에 따라 달라진다.
- e.g. HNSW는 그래프 기반으로 비슷한 벡터들을 그래프 이웃 노드로 연결, IVF는 클러스트 기반이라 비슷한 벡터들을 같은 클러스터에 묶음, LSH는 해시 기반이라 비슷한 벡터가 같은 bucket에 들어가도록 설계 -> 미리 가까이 모아두는 알고리즘이 heuristic한 부분.
- Term-based retrieval
- RAG System Evaluation
- retrieval
- 가장 대표적인 평가 요소
- Context precision : 찾은 문서 중 query와 관련된 비율
- Context recall: query와 관련된 모든 문서 중 찾아낸 비율
- 전체 RAG system이 가진 context 자체를 평가할 수도 있음.
- latency는 전체 system에서 retriever이 차지하는 비중이 그리 높이 않을 수 있음. 보통 output generation이 길어지는 게 latency에 큰 영향을 미치는 경우가 많음.
- trade-off: indexing vs. querying
- 많은 정보 저장 -> query 정확도 증가 & 탐색 시간 감소 but, indexing 시간과 memory 사용량 증가 & storage 용량 증가
- ANN-Benchmarks에서 Recall, QPS(Query per second), Build time, Index size로 ANN algorithm과 복합 datasets을 평가한 것을 참고.
- 가장 대표적인 평가 요소
- ranking
- NDCG, MAP, MRR 등
- embedding
- embedding만 따로 평가가 가능하고, 특수한 작업에 대해 얼마나 잘 처리하는지 평가 가능 (MTEB)
- embedding할 문서가 많아질 수록 cost는 높아지고, model이나 vector strage 바꿀 때마다 모두 다 다시 embedding하는 게 부담이 될 수 있다.
- 최소한 retrieval quality, RAG outputs, (vector 검색 하면)embedding은 평가해라.
- retrieval
- Combining retrieval algorithms
- hybrid search = term-based retrieval + embedding-based retrieval
- 방법1. 순차 실행 -> 보통 이거 씀..병렬 실행의 고질적 문제 때문.
- 방법2. 병렬 실행
- RRF (Reciprocal Rank Fusion)로 서로 다른 rank가 매겨진 documents를 결합하여 re-rank할 때 사용함.
- \(RRF(d) = \sum_{q \in Q}^1 {\frac{1}{k + rank_q(d)}}\)
- d = 점수를 계산할 문서
- Q: 쿼리 집합 (또는 검색 시스템 집합)
- k = 순위 상수 (평활화와 순위 간 차이 조절을 고려해 실험적으로 60으로 설정)
- 고질적인 문제...term-based retrieval와 embedding-based retrieval 각각 조회해서 RRF를 사용했다 치자, {A, B}문서가 각각 {1, 2}, {2, 1}로 rank가 매겨졌다면 둘은 같은 점수를 받는다. -> term-based retrieval과 embedding-based retrieval 중 무엇이 더 높은 평가를 받아야 하는가? 고려 불가능.
- hybrid search = term-based retrieval + embedding-based retrieval
- Chunking strategy
- 단순하게 고정 길이 chunk
- 특정 문서는 창의적으로 chunk해볼 수도? (e.g. Q&A 게시판이면 question - answer 쌍으로 나누고, 각 쌍을 chunk로)
- overlapping 안 하면 중요 정보 손실 가능성 있음. (e.g. "I left my wife a note"를 ["I left my wife", "a note"]로 분리하면, 두 chunk모두 핵심 정보 전달에 실패한다.)
- unit을 생성형 model가 사용하는 tokenizer의 token 기준으로 chunk를 나눌 수도
- 생성형 model을 바꿀 때, 해당 model이 사용하는 tokenizer 기준으로 모두 다시 reindex해야 하는 단점
- recursive chunking 전략 -> 나라면 이거 쓸 듯? overlapping 고려하는 것도 일임.
- e.g. section -> section이 너무 길면 paragraphs -> praragraphs도 너무 길면 sentence
- 관련된 text의 맥락이 끊길 우려를 줄일 수 있다. (text의 의미 구조를 존중하면서 분할하므로, 한 문장이나 한 단락이 두 chunk로 쪼개질 가능성이 줄어듦.)
- Anthropic의 Contextual retrieval
- chunk마다 metadata를 추가 (e.g. 이커머스면 상품 정보, 리뷰 등을 함께 저장, 이미지와 비디오면 제목과 캡션을 함께 저장)
- 기존 문제점: Traditional RAG의 Context 손실
- 원본 문서: ""ACME Corp의 2023년 Q2 실적 보고서...(중략)......회사의 매출은 전 분기 대비 3% 성장했다."
- Tranditional Chunking: "회사의 매출은 전 분기 대비 3% 성장했다."
- Query: "ACME Corp의 Q2 2023 매출 성장률은?" -> "회사 = ACME corp" 정보가 손실된 상태라 알 수 없음
- Solution: Chunk에 Context를 Prepend (overlap이나 recursive chunking과는 다르게, Contextual Retrieval은 의미적 맥락을 명시적으로 추가)
- Reranking
- 언제 사용? model's context size에 맞추기 위해 retrieved documents 수를 줄여야 할 때, input tokens 수를 줄여야 할 때
- Context reranking도 있는데 중요도는 떨어짐. 일단 documents가 포함되기만 한다면, 순서의 영향은 search ranking에 비해 덜 중요.
- query rewriting
- 사용자 query의 모호함을 해결하고 retrieve해야 함.
- e.g.
User: When was the last time John Doe bought something from us?
AI: John last bought a Fruity Fedora hat from us two weeks ago, on January 3, 2030.
User: How about Emily Doe?
- e.g.
- 마지막 질문을 “When was the last time Emily Doe bought something from us?”로 보정해주어야 제대로 검색할 것. (chat context를 기반으로 사용자 의도를 재해석)
- “How about his wife?” -> wife 정보 탐색 -> 없음 -> 해결 불가능한 지식이라는 정보를 포함 -> 환각 & 잘못된 답변을 방지할 수 있을 것.
- 사용자 query의 모호함을 해결하고 retrieve해야 함.
- Multimodal RAG
- multimodal: 텍스트, 이미지, 오디오, 비디오 등 여러 종류의 데이터(양식/모드)를 함께 이해하고 처리하는 기술
- query에 매칭되는 image를 조회할 때, image에 metadata를 추가할 수 있음.
- CLIP(Contrastive Language–Image Pre-training)같은 multimodal embedding model을 사용하여 text와 image를 embedding할 수도 있음.
- Learning Transferable Visual Models From Natural Language Supervision(2021)
- 인터넷에서 4억개 이미지와 해당 이미지에 대한 설명 Text를 pair로 두고 학습데이터셋으로 구성한 후, 각각을 인코더로 임베딩하여 같은 pair에 대해 거리를 가깝게하고 다른 pair에 대해 거리가 멀어지도록 텍스트/이미지 인코더를 학습
- 학습된 인코더로 Test 이미지와 Test 텍스트 간의 유사도를 계산
- 학습시에는 이미지와 텍스트간의 cosine similiary를 계산하고, 같은 Pair간에는 유사도를 최대화하고 다른 Pair간에는 최소화하도록 Cross Entropy Loss를 사용
- RAG with rabular data
- 테이블 조회해야 하면, User Query → [Text-to-SQL] → [SQL Execution] → [Generation] → Response 순서로 이루어져야 할 것