오늘 소개해 드릴 재미있는 AI 기술은 embedchain입니다. 실제 사용 예시 코드를 코랩으로 준비하였습니다. 또한, 배포했던 이미지도 함께 스샷으로 찍어두었으니, 다들 직접 실습해보시고, 배포까지 해보시면 좋을 거 같습니다. LLM을 사용하여 RAG(검색 증강 생성) 애플리케이션을 구축하는 방법을 알아보고자 합니다. 지금까지 RAG 애플리케이션은 AI 혁명의 가장 유용한 결과입니다. 아마 내년쯤에는 훨씬 더 큰 발전을 볼 수 있을 것입니다. 한때 prompt engineer가 대세였다면, 이제 RAG engineer가 대세가 되지 않을까 조심스럽게 추측해봅니다.
우리는 RAG DB를 구축하여도 DB내의 정보가 업데이트가 되기 때문에 RAG용 DB를 지속적으로 업데이트 해야한다는 이슈가 있었습니다. 이건 여간 번거로운 일이 아닙니다. 왜냐하면, 굳이 업데이트가 필요하지 않는 부분까지 모두 업데이트를 해야하기 때문에 시간과 리소스가 낭비되기 때문입니다. 오늘 소개드린 embedchain은 이러한 문제점을 쉽게 해결할 수 있게 도와줍니다. 어떻게 그게 가능할까요? 바로, 이슈가 발생된 데이터만 추가할 수 있도록 add function이 있습니다.
또한, 무엇보다 쉽게 배포할 수 있도록 설계되어 있습니다.
그 외 장점들
다양한 소스의 데이터를 통합하고 색인화합니다.
각 소스에 대한 최적의 데이터 청킹 방법을 결정합니다.
RAG 파이프라인을 정기적으로 업데이트되는 데이터 소스와 동기화합니다.
벡터 저장소에 효율적인 데이터 저장소를 구현합니다.
문서 청크에 메타데이터를 포함할지 여부를 결정합니다.
권한 관리를 처리합니다.
LLM(대형 언어 모델) 구성.
효과적인 프롬프트를 선택합니다.
적절한 검색 전략을 선택합니다.
RAG 파이프라인의 성능을 평가합니다.
무엇보다도 파이프라인을 프로덕션 환경에 배포합니다.
Embedchain이란 무엇입니까?
Embedchain은 생산 준비가 완료된 오픈 소스 RAG 프레임워크입니다. 구조화되지 않은 데이터(인터넷 페이지 등)를 로드, 인덱싱, 검색 및 동기화합니다.
Embedchain은 RAG 애플리케이션 생성을 간소화하여 다양한 유형의 비정형 데이터를 관리하기 위한 원활한 프로세스를 제공합니다. 데이터를 관리 가능한 단위로 효율적으로 분할하고, 관련 임베딩을 생성하며, 최적화된 검색을 위해 벡터 데이터베이스에 저장합니다. 다양한 API 제품군을 통해 사용자는 상황에 맞는 정보를 추출하고, 정확한 답변을 찾고, 대화형 채팅 대화에 참여할 수 있으며 모두 자신의 데이터에 맞춰 조정됩니다.
Embedchain은 데이터 과학자, 기계 학습 엔지니어와 같은 AI 전문가부터 대학생, 독립 개발자, 취미 활동가를 포함하여 AI 여정을 막 시작한 사람들에 이르기까지 다양한 범위의 사용자를 위해 설계되었습니다. 기본적으로 이는 전문 지식 수준에 관계없이 AI에 관심이 있는 모든 사람을 위한 것입니다.
우리의 API는 사용자 친화적이면서도 적응성이 뛰어나 초보자가 단 4줄의 코드만으로 LLM 기반 애플리케이션을 쉽게 만들 수 있습니다. 동시에 우리는 RAG 파이프라인의 모든 측면에 대해 광범위한 사용자 정의 옵션을 제공합니다. 여기에는 LLM, 벡터 데이터베이스, 로더 및 청커 선택, 검색 전략, 순위 재지정 등이 포함됩니다.
우리 플랫폼의 명확하고 구조화된 추상화 계층은 사용자가 간단한 프로젝트를 만들든, 복잡하고 미묘한 AI 애플리케이션을 만들든 상관없이 특정 요구 사항에 맞게 시스템을 맞춤화할 수 있도록 보장합니다.
Embedchain을 사용하면 다음과 같은 간단한 단계를 통해 RAG 파이프라인에 데이터를 쉽게 추가할 수 있습니다.
자동 데이터 처리: 자동으로 데이터 유형을 인식하여 로드합니다.
효율적인 데이터 처리: 시스템은 데이터의 주요 부분에 대한 임베딩을 생성합니다.
유연한 데이터 저장: 벡터 데이터베이스에서 처리된 데이터를 저장할 위치를 선택할 수 있습니다.
사용자가 채팅, 검색, 쿼리 등 질문을 하면 Embedchain은 응답 프로세스를 단순화합니다.
쿼리 처리: 사용자의 질문을 임베딩으로 변환합니다.
문서 검색: 이러한 임베딩은 데이터베이스에서 관련 문서를 찾는 데 사용됩니다.
답변 생성: 관련 문서는 LLM에서 정확한 답변을 작성하는 데 사용됩니다.
Embedchain을 사용하면 RAG 파이프라인 구축의 복잡성에 대해 걱정할 필요가 없습니다. 모든 종류의 데이터로 애플리케이션을 개발할 수 있는 사용하기 쉬운 인터페이스를 제공합니다.
단점
첫째, 아직 한국어로 된 영상의 경우, 정상적으로 작동하지 않는 이슈가 있습니다.
두번째, 유튜브에서 자막 기능을 제공하지 않는 영상의 경우 작동되지 않습니다.
셋째, web_page dtype의 경우 bs4(beautiful soup)으로 데이터를 불러오기 때문에 bs4가 막혀있는 곳은 아래 이미지처럼 데이터가 로드되지 않는 오류를 볼 수 있습니다.
이처럼 다양한 RAG 서비스가 출시되고 있다. 현 시점에서 우리가 할 수 있는 것이 무엇이고, 어떻게 RAG를 적용해야하는지에 대한 고민이 필요한 시점이라고 생각이 든다. 빠르게 변화되고 있는 현 시점에서 좀 더 좋은 소식과 재미있는 소식을 가져올 수 있도록 노력해보겠습니다.
오늘은 Gemini(제미나이)에 대해서 소개해드리는 시간입니다. Gemini가 무엇이고, 누가 만들었고, 어떤 기능을 할 수 있는지에 대해서 알아보도록 하겠습니다. 텍스트 뿐 아니라 음성과 이미지 등을 이해할 수 있는 멀티모달 방식의 ai로 만들져서 채팅 수준을 넘어 말하거나 들을 수 있고, 그림도 이해할 수 있습니다. 수학 문제를 풀거나 데이터를 분석하는 높은 추론 능력까지 갖추어 프로그램 코딩까지 할 수 있다고 설명하였습니다. 제미나이(Gemini)는 Ultra, Pro, Nano 3가지 모델로 구성되어 있습니다. 구글은 제미나이(Gemini) Ultra가 GPT4보다 성능이 좋다고 주장합니다. 그러나, 개발자들 사이에서는 정말 좋다고 말할 수 있는 것인가? 에 대한 논란이 붉어지고 있습니다. 왜 논란이 되었는지에 대해서 살펴본 후 제미나이(Gemini)가 가지고 있는 장점들 그리고 어떻게 활용할 수 있는지에 대해서도 함께 소개해드리고자 합니다.
이 이미지는 언어이해도(MMLU) 성능을 제미나이와 GPT4를 비교해놓았습니다. 자세하게 보시면, 하얀색 조그마한 글씨로 GPT-4는 5-shot이고, gemini는 CoT32가 적혀 있습니다. 일반인들께서는 이게 먼데? 라고 말하실 수 있지만, 이건 엄청난 차이가 있는 것입니다.
일반인분들을 위해 조금 쉽게 설명하자면, 인간의 지능을 89.8%라고 했을 때 GPT4는 5번을 시도하여 86.4%가 된 것이고, 제미나이는 32번을 시도하여 90.0%가 된 것입니다. 그런데, 그냥 32번을 시도한 것이 아닙니다. 논문 appendix를 살펴보면 아래와 같은 figure7 표를 찾으실 수 있습니다.
일단, CoT(Chain-of-Thought : 여러 단계의 추론 과정을 생성하도록 유도하여 언어 모델의 추론 능력을 향상시키는 기법)로 32번을 GPT4와 비교하여도 GPT4가 앞섭니다.
구글에서 Uncertainty-Routed라는 새로운 CoT32방법을 사용하여야 겨우 GPT4의 성능을 넘어서는 것을 볼 수 있습니다. 그렇다면 Uncertainty-Routed 방식이 무엇인지 궁금하실텐데요. 쉽게 설명드리자면, 인공지능에게 명백하게 일관성이 없는 정보 물어보는 방식입니다. 이 방식이 중요한 의미를 갖는 이유는 언어모델의 경우 정치 이야기를 하다가 갑자기 물리 이야기를 하면 응? 어떻게 말하지? 를 고민하면서 이상한 말을 생성하거나, 틀린 답을 생성하는 경우가 발생됩니다. 이러한 현상을 할루시네이션이라고 하는데, 언어 모델에서는 이 부분이 아직까지 숙제로 남아 있습니다. 구글은 사용자들의 엉뚱함을 대처하기 위해 노력한 것으로 본 저자는 생각을 합니다. 왜 이렇게 생각을 하였냐면 실제로 서비스를 만들더라도 사용자들의 엉뚱함을 대처하기가 상당히 까롭다고 느끼고, 제미나이를 소개하는 영상에서도 상당히 엉뚱한 질문들을 하는데, 제미나이가 잘 대처하는 것을 볼 수 있습니다. 바로 이 영상입니다.
여러분이 보는 것은 연속된 이미지를 보여주고 추론을 한 것이라고 이야기합니다. 그런데, 영상을 보면 영상을 넣어 실시간으로 인공지능이 대답하는 것처럼 보입니다. 영상을 넣어서 실행한게 아니라 이미지를 넣어서 인공지능이 대답한 것을 영상으로 편집한 것입니다.
머, 그럴 수 있지? 라고 생각할 수 있지만 트위터에서는 뜨거운 감자입니다.
수 많은 구독자를 가지고 계신 @svpio님은 당황스럽다고 이야기를 하시네요!
그래서 Gemini가 먼데?
제미나이란?
text, code, image, video, audio를 이해할 수 있는 multimodal AI 입니다. gemini의 철학은 사람이 생각하는 방식으로 우리 주변 세계를 이해 하려고 노력한다고 이야기하고 있습니다. 즉, 사람이 세상을 이해하는 것과 비슷하게 인공지능이 세상을 이해하게 만들고 싶다고 합니다.
GPT4랑 무엇이 다른건데?
근본적으로 GPT4와 다릅니다. GPT4는 언어모델이고, 그 위에 이미지를 이해하는 모델 등을 추가하면서 복합적으로 작용하는 것이고, gemini는 모델 하나로 이 모든 것을 해보겠다는 것입니다.
제미나이 종류는?
gemini는 크게 3가지로 구성되어 있습니다. 서두에도 말씀드렸듯 울트라, 프로, 나노로 구성되어 있는데 각각의 모델은 목적이 조금 다른 것 같습니다. 확실히 구글은 전세계에 자신들의 상품이 많이 깔려있기에 그 제품을 업그레이드 할 수 있는 쪽으로 생각을 하는 것 같습니다. 저자는 구글이 가정 안으로 들어오고 싶어 한다고 오래전부터 생각을 해왔는데요. 역시 제미나이에도 그러한 노력이 보입니다. 이렇게 생각하는 이유는 디바이스에 넣을 수 있도록 나노라는 버전이 나와 있고, 데미스 하사비스(Demis Hassabis) 구글 딥마인드 CEO 인터뷰에서 향후 계획을 로봇 관련된 쪽으로 나아가겠다고 이야기하고 있기 때문입니다. 또한, 제미나이는 숙제를 도와주는 기능도 탑재되어 있습니다.
gemini pro는 현재 bard(바드)에 적용이 되어 있기에 지금 bard를 쓰면 gemini-pro를 쓰고 있는 거라고 보면 됩니다. 지금 공개되어 있습니다.
Nano의 경우, 인터넷 없이 작은 device에 실제로 탑재하는 것을 목표로 하고 있다고 합니다. 픽셀 8부터 탑재된다고 하네요. 픽셀 8 프로는 제미나이 나노를 실행하도록 설계된 최초의 스마트폰입니다. 제미나이 나노는 녹음 앱 상의 요약하기와 같은 새로운 기능을 지원하며, 오늘 왓츠앱(WhatsApp)을 시작으로 내년에는 더 많은 메시징 앱과 함께 지보드(Gboard)의 스마트 답장에 적용될 예정입니다. 자세한 내용은 픽셀 블로그에서 확인해 보세요.
구글은 확실히 안정성을 매우 중요하게 생각하기에 Ultra는 조금 시간이 걸릴거 같습니다.
현재 제미나이 울트라는 출시를 앞두고 신뢰할 수 있는 외부 기관 소속 레드팀의 리뷰 등 광범위한 신뢰성 및 안전 점검을 완료한 후, 미세 조정과 사람의 피드백을 통한 강화 학습(Reinforcement Learning from Human Feedback: RLHF)을 거쳐 모델을 더욱 개선하는 작업을 진행 중입니다.
이 과정의 일환으로 구글은 일부 고객, 개발자, 파트너, 안전 및 책임 전문가에게 제미나이 울트라를 공개해 초기 실험을 진행하고 피드백을 받은 후 내년 초에 개발자와 기업 고객을 대상으로 광범위하게 제공할 예정입니다.
또한, 가장 뛰어난 성능의 최대 규모 모델인 제미나이 울트라를 적용해 새롭고 최첨단의 AI경험을 제공하게 될 바드 어드밴스드(Bard Advanced)를 내년 초에 선보일 예정입니다.
장점은 무엇일까?
저자가 생각하는 제미나이의 장점은
논문을 일괄적으로 찾아주는 것
숙제채점를 도와주는 것
코드를 작성하는 것
입니다. 코드는 정말 정말 작성을 잘합니다. 심지어 코드테스트는 pro로만 진행하였습니다. 울트라로 적용을 한다면 지금보다 훨씬 더 좋은 성능을 낼 것으로 기대가 됩니다. 점점 인공지능이 개발자의 영역까지 들어오고 있는 현 시점 우리는 어떻게 미래를 개척해야할지 진지한 고민이 필요할 때라고 생각합니다.
Attention Sinks로 더 많이 소개되고 있는 논문 : Efficient Streaming Language Models with Attention Sinks(feat. text of infinite length without fine-tuning)에 대해서 오늘은 한번 알아보고자 해요. 어떤 문제를 해결하고 싶었고, 어떻게 아이디어 발견하였고, 수식과 코드까지 어떻게 연결되었는지 한 큐에 설명드릴테니 잘 따라오셔요!
기존 문제점
mit-han-lab의 영상 중 왼쪽 영상처럼 KV Cache보다 긴 문장을 생성하다보면, 모델이 튀어 이상한 문장을 생성하는 것을 발견할 수 있습니다. 원 저자는 이 문제를 논문에서 아래와 같이 말하고 있습니다. (여기서 한 가지 오해할 수 있는 사실이 엄청 긴 입력문장을 넣는 다는 것이 아니고, 영상처럼 모델이 무한한 생성을 하는 것을 기준으로 생각을 하셔야합니다.)
Window attention, where only the most recent KVs are cached, is a natural approach — but we show that it fails when the text length surpasses the cache size.
It is very challenging for LLM to generalize to longer sequence lengths than they have been pretrained Llama-2. The reason is that LLms are constrained by the attention window during pre-training. The acceptable sequence length remains finite, which doesn't allow persistent deployments.
StreamingLLM 나온 배경과 아이디어
Dense Attention, Window Attention, Sliding Window with Re-computation 등 다양한 방법으로 연구를 하였지만, 이러한 문제를 해결하지 못했습니다.
그러던 중 저자들은 특이한 점을 하나 발견하게 됩니다.
Layer2의 initial token에 score가 몰려있는 것을 관찰하게 됩니다. 그리고 이러한 이유를 softmax 때문이라고 생각한다고 이야기합니다. softmax과 하면 가장 중요한 토큰을 찾고, 모든 토큰의 합을 1로 만들어야 합니다. token별 중요도를 계산하게 되는데, 진짜 중요한 것을 찾고나면 나머지 점수들을 어디에 배정해야할지 모른다는 것이죠. 그래서 현재 토큰과 의미적으로는 큰 관계는 없지만, 그래도 initial token에 중요도를 부여하게 된다고 저자들은 이야기 하고 있습니다.
To understand the failure of window attention, we find an interesting pehnomenon of autoregressive LLMs: a superrisingly large amount of attention score is allocated to the initial tokens, irrespective of their relevance to the language modeling task.
We attribute the reason to the Softmax operation, which requires attnetion scores to sum up to one for all contextual tokens. Thus, even when the current query does not have a strong match in many previous tokens, the model still needs to allocate these unneeded attention values somewhere so it sums up to one.
저자들은 이러한 insight를 활용하여 StreamingLLM을 제안하게 됩니다. 이것은 단순하고 효율적인 프레임워크라고 그들은 이야기하고 있습니다. 즉, softmax를 계산할 때, 수식을 조금 변경하여 최초 4개의 initial token을 계속 가지고 가면서 생성을 이어갑니다.
이를 수식적으로도 함께 살펴보겠습니다.
이처럼 분모에 시작 토큰을 고정을 가져가게 되므로, 문맥의 전반적인 흐름을 놓치지 않고, streaming으로 text를 생성할 수 있는 프레임워크를 제안하게 됩니다. 논문에서는 아래와 같이 언급하고 있습니다.
Based on the above insights, we propose StreamingLLM, a simlpe and efficient framework that enalbles LLMs trained with a finite attention winodw to work on text of infinite length without fine-tuning.
Therefore, StreamingLLM simply keeps the attention sink tokens'KV (with just 4 initial tokens sufficing) together with the sliding window's KV to anchor the attention computation and stabilize the model's performance.
이러한 수식 코드로는 어떻게 반영되고 있는지에 대해서 한번 살펴보도록 하겠습니다.
이를 코드로 구현하는 방법은 크게 두가지가 있습니다. 원 저자가 작성한 방법과 허깅페이스로 구현한 방법이 있습니다.
어떤 분이 친절하게 pip로 설치할 수 있도록 코드를 만들어주셨어요! 이 글을 그 분이 직접 보시진 않겠지만, 그래도 고맙습니다!
여기서는 코드를 직접 구현하지 않고, huggingface에 구현된 streamer라는 class를 이용하여 구현하셨더라구요.
streamer도 위 코드와 작동하는 방식은 비슷합니다. 단, 여기서는 min_new_token과 max_new_token을 파라미터 값을 넣어주어야합니다. 1번처럼 무한하게 생성을 하는 것이 아니라 max_new_token까지만 생성을 진행하게 됩니다.
import torch
from transformers import AutoTokenizer, TextStreamer, GenerationConfig
from attention_sinks import AutoModelForCausalLM
# model_id = "meta-llama/Llama-2-7b-hf"
# model_id = "mistralai/Mistral-7B-v0.1"
model_id = "mosaicml/mpt-7b"
# model_id = "tiiuae/falcon-7b"
# model_id = "EleutherAI/pythia-6.9b-deduped"
# Note: instruct or chat models also work.
# Load the chosen model and corresponding tokenizer
model = AutoModelForCausalLM.from_pretrained(
model_id,
# for efficiency:
device_map="auto",
torch_dtype=torch.float16,
# `attention_sinks`-specific arguments:
attention_sink_size=4,
attention_sink_window_size=252, # <- Low for the sake of faster generation
)
model.eval()
tokenizer = AutoTokenizer.from_pretrained(model_id)
tokenizer.pad_token_id = tokenizer.eos_token_id
# Our input text
text = "Vaswani et al. (2017) introduced the Transformers"
# Encode the text
input_ids = tokenizer.encode(text, return_tensors="pt").to(model.device)
with torch.no_grad():
# A TextStreamer prints tokens as they're being generated
streamer = TextStreamer(tokenizer)
generated_tokens = model.generate(
input_ids,
generation_config=GenerationConfig(
# use_cache=True is required, the rest can be changed up.
use_cache=True,
min_new_tokens=100_000,
max_new_tokens=1_000_000,
penalty_alpha=0.6,
top_k=5,
pad_token_id=tokenizer.pad_token_id,
eos_token_id=tokenizer.eos_token_id,
),
streamer=streamer,
)
# Decode the final generated text
output_text = tokenizer.decode(generated_tokens[0], skip_special_tokens=True)
Gradient Accumulation과 GradientCheckPointing이 무엇을까요? LLM을 fine-tuning 할 때, 메모리 부족 문제는 많은 연구자와 개발자들이 직면하는 고질적인 문제 중 하나입니다. 한정된 리소스 자원으로 대규모 모델을 훈련시키는 동안 메모리 부족으로 학습이 실패하는 상황은 흔히 발생하는 문제입니다. 그러나 걱정하지 마세요! 오늘은 여러분이 겪고 있는 이러한 어려움을 극복하는 두 가지 중요한 기술을 소개하려고 합니다. Gradient Accumulation과 Gradient CheckPointing은 메모리 부족 문제를 해결하는 데 필수적인 도구입니다. 이러한 기술을 익히고 적용함으로써, LLM 학습 중 메모리 문제로 인한 실패를 더 이상 겪지 않을 수 있습니다. 이 글에서는 이러한 기술들을 자세히 살펴보고, LLM fine-tuning을 원활하게 진행하는 방법에 대해 알아보겠습니다.
Gradient Accumulation
Batch size를 작게 하여 LLM을 돌리게 되면, 모델의 성능이 좋지 않게 되는데, 이를 극복하기 위해 나온 기술로 1step을 돌고 바로 가중치를 업데이트 하는 것이 아니라 4, 8, 16, 32 등 사용자가 설정한 step만큼 기다렸다가 일괄적으로 가중치를 업데이트 하는 방식입니다.
Gradient Accumulation(그래디언트 누적)은 딥 러닝 모델의 학습 과정 중에 그래디언트(gradient)를 여러 미니배치(mini-batch)에 대해 누적하고, 이 누적된 그래디언트를 사용하여 가중치 업데이트를 수행하는 기술입니다. 이 기술은 주로 큰 모델을 학습하거나 메모리가 제한된 환경에서 모델 학습을 수행할 때 유용합니다. Gradient Accumulation을 사용하는 주요 이유는 다음과 같습니다:
메모리 관리: 대규모 딥 러닝 모델을 학습할 때, 하나의 미니배치에서 그래디언트를 계산할 때 발생하는 메모리 부담이 큽니다. Gradient Accumulation을 사용하면 미니배치 크기를 작게 유지하면서도 여러 미니배치의 그래디언트를 누적하므로 메모리 사용량을 줄일 수 있습니다
더 정확한 그래디언트 추정: Gradient Accumulation은 그래디언트를 여러 미니배치에 걸쳐 누적하기 때문에, 단일 미니배치에 대한 그래디언트 추정보다 더 정확한 추정을 제공할 수 있습니다. 이는 모델 학습의 안정성을 향상시킬 수 있습니다.
Gradient Accumulation을 구현하려면 다음 단계를 따를 수 있습니다:
미니배치 크기 선택: 미니배치 크기를 선택하고, 각 미니배치에서 그래디언트를 계산합니다.
그래디언트 누적: 각 미니배치에서 계산한 그래디언트를 누적합니다. 이를 위해 미니배치마다 그래디언트를 기존 그래디언트에 더해주거나, 누적 변수에 더해주는 방식을 사용합니다.
업데이트: 그래디언트를 누적한 후, 가중치 업데이트를 수행합니다. 보통은 역전파(backpropagation) 및 경사 하강법(gradient descent)을 사용하여 가중치를 조정합니다. Gradient Accumulation을 통해 메모리 효율성을 높일 수 있지만, 학습 시간은 조금 더 오래 걸릴 수 있습니다. 따라서 미니배치 크기와 누적 스텝 수를 적절하게 조정하여 모델 학습의 메모리와 계산 비용을 조절해야 합니다. 이를 통해 모델 학습을 원활하게 진행할 수 있습니다.
이렇게 Gradient Accumulation을 사용한다고 하더라도, 메모리가 터지는 현상이 발생될 수 있습니다.
이럴때 사용할 수 있는 것이 gradient checkpointing입니다.
Gradient Checkpointing
Gradient Accumulation을 통해서 step을 모아서 가중치를 업데이트 한다고 해도, 역전파 과정에서 메모리가 터져버리는 현상이 발생될 수 있습니다. 이런 상황을 막고자 역전파 도중 가중치를 저장하여 터지는 현상을 방지하는 기술입니다.
"Gradient Checkpointing"은 딥 러닝 모델에서 역전파(backpropagation) 과정 중에 발생하는 메모리 부담을 줄이기 위한 기술입니다. 딥 러닝 모델의 역전파는 모델의 파라미터에 대한 그래디언트(gradient)를 계산하고, 이를 사용하여 가중치를 업데이트하는 과정입니다. 대규모 모델이나 시퀀스 길이가 긴 모델의 경우, 그래디언트 계산에 많은 메모리가 필요할 수 있습니다.
Gradient Checkpointing은 이러한 문제를 해결하기 위해 중간 계산 결과를 저장하고 나중에 필요할 때 계산을 다시 수행하는 방식으로 동작합니다. 이를 통해 메모리 사용량을 크게 줄일 수 있으며, 대규모 모델의 학습을 가능하게 합니다.
Gradient Checkpointing은 PyTorch에서 "torch.utils.checkpoint.checkpoint" 함수를 사용하여 구현할 수 있으며, 이 함수는 역전파 과정에서 중간 계산 결과를 저장하고 관리합니다.
예를 들어, 다음과 같이 사용할 수 있습니다:
pythonCopy code
import torch
from torch.utils.checkpoint import checkpoint
# 모델 정의
model = YourDeepLearningModel()
# 입력 데이터
input_data = torch.randn(1, 3, 64, 64)
# Gradient Checkpointing을 사용하여 역전파 계산
output = checkpoint(model, input_data)
이렇게 하면 역전파 과정에서 중간 계산 결과를 저장하고 메모리를 효율적으로 관리할 수 있습니다. Gradient Checkpointing은 메모리 사용량을 줄이면서 모델 학습을 가능하게 하므로 대규모 모델을 학습할 때 유용합니다.
그러나, 실제로 코드를 나중에 사용해보시면 아시겠지만, GradientCheckPointing은 Deepspeed:zero3에 더 많이 사용하시는 것을 보실 수 있습니다. 분산컴퓨팅할 때 반드시 필요하거든요!
git release를 활용하여 대용량 데이터 파일을 주고 받을 수 있다는 사실을 알고 계셨나요? 프로젝트를 하다보면, 다른 분들에게 대용량 파일을 공유하기 위해 드라이브를 사용하여 공유하곤 합니다. 그러나, 이는 파일을 다운로드/업로드할때 매우 번거롭습니다. 어떻게 하면 서로 쉽게 주고 받을 수 있는지에 대해서 오늘은 알려드리고자 합니다.
Git Release는 프로젝트의 특정 시점을 명확하게 표시하고 배포하는 데 유용한 도구입니다.
-
-
-
Git-Release: Tag
1. 태그(Tag)
Git에서 Release를 만들기 위해서는 먼저 태그를 이해해야 합니다. 태그는 특정 commit에 대한 참조로, 보통 중요한 시점이나 배포 버전을 표시하는 데 사용됩니다.
Lightweight 태그: 단순한 참조로, 어떤 메시지도 포함되지 않습니다.
Annotated 태그: 메시지, 태거 이름, 이메일 등의 정보와 함께 commit을 참조합니다. GPG로 서명할 수 있어 보안에 더 용이합니다.
태그 생성 예시:
bashCopy code
# Annotated 태그 생성 git tag -a v1.0.0 -m "Release version 1.0.0" # Lightweight 태그 생성 git tag v1.0.0
2. Semantic Versioning (SemVer)
Semantic Versioning은 소프트웨어 버전 관리에 대한 규약입니다. "MAJOR.MINOR.PATCH" 형식을 가지며, 각 숫자는 다음과 같은 의미를 가집니다:
MAJOR: 호환되지 않는 API 변경이 있을 때 증가
MINOR: 기능이 추가되면서 이전 버전과 호환될 때 증가
PATCH: 버그 수정과 같은 이전 버전과 호환되는 수정이 있을 때 증가
3. Git Hooks
Git Hook은 특정 이벤트가 발생할 때 자동으로 실행되는 스크립트입니다. 예를 들어, pre-commit Hook은 commit이 되기 전에 코드 포맷팅 검사를 수행할 수 있습니다. Release 전에 특정 검사나 빌드 과정을 자동화하려면 Hook을 사용할 수 있습니다.
오늘은 RAG, Retrieval-augmented Knowledge-intensive Task라는 논문을 알아보도록 하겠습니다.
제목에서 알 수 있는 Knowledge-intensive Task를 해결하기 위해 검색된 내용(retrieval)을 조합(augmented)하여 생성하는 모델이다 라고 볼 수 있습니다. 해당 논문은 2021년 facebook에서 발표한 논문입니다. 이 논문을 보게 된 이유는 RAG는 facebook Blenderbot의 backbone이 되기 때문에 이를 이해하는 것이 블랜더봇에 대한 이해를 높여줄거라고 생각하여 깊게 논문을 리뷰하게 되었습니다. 이 포스팅을 이해하기 위해서는
ODQA와 Know-Intensive Task가 무엇인지 모르는 분들을 위해 잠깐 개념을 잡고 넘어가겠습니다.
ODQA란, 특정한 도메인에 국한을 두지 않고, 질문(Q, Question)이 들어왔을 때 DB에서 관련된 문서(doc or passage)를 찾고, 찾은 문서에서 정답(A, Answer)을 찾아주는 2 단계로 구성되어 있습니다. 1)DB에서 관련된 문서를 찾아주는 모델과 2)찾은 문서에서 정답을 찾는 모델 2개가 필요합니다. 여기서 중요한 것은 정답은 연속된 토큰(Continuous Span)으로 정답이 존재하는 경우가 많다는 것이다. 즉, 이 말은 일반적인 ODQA task에서는 연속된 토큰으로 답이 존재하지 않을 경우, 성능이 떨어질 수 있고, Task를 타는 경우가 있습니다.
Know-intensive task란,AI 연구원이 광범위한 작업을 수행하기 위해 실제 지식을 더 잘 활용할 수 있는 모델을 구축하는 데 도움이 되는 새로운 통합 벤치마크입니다. Know-intensive task는 크게fact-checking, open-domain question answering, slot filling, entity linking, and dialog generation으로 구성되어 있으며, 보다 자세한 내용은 facebook의KILT를 참고해보세요.
Follow-up ODQA Papers (ODQA 논문 흐름 파악)
이제 대략적으로 ODQA와 Know-Intensive Task가 무엇인지 알았으니, ODQA 관련 논문의 흐름을 살펴볼까 합니다.
ODQA의 논문의 흐름을 보면서, 각 논문이 가지는 의의와 목적을 살펴보고 RAG가 왜 중요한지 체감해보도록 하겠습니다.
사실 ODQA의 역사는 그리 길지 않습니다.
각 논문별로 특징을 간략하게 조사해왔습니다. 함께 살펴보시죠~
DrQA : OpenDomain-QA를 제안한 논문
-. 모델의 weight에 이 세상 모든 정보를 담는 것은 논리적으로 말이 되지 않아!
-. 질문에 대한 정답을 외부 지식 데이터 베이스에서 찾아오자
OrQA : retriever 학습에 필요한 신호를 처음으로 제안한 논문
-. 정답을 찾긴 찾는데, 잘 못찾네.. 우리 그럼 Retriever를 훈련시켜보자!
REALM : 처음으로 ODQA Framework 제안한 논문
-. Retriever만 학습시키는게 아니라 Reader도 함께 End-to-End로 학습 시켜보면 좋을거 같아!
RAG : ODQA와 Know-Intensive task를 연결한 논문
-. Retriever만 학습시키는게 아니라 Generator도 함께 End-to-End로 학습 시켜는데 생성까지 하면 더 좋을거 같아
-. 즉, 찾은 정보를 활용하여 말을 하는 영역으로 넘어오게 됨
FiD-Distill
-. 수 백개의 문서를 동시에 처리할 수 있도록 RAG에 FiD를 사용하는데, 너무 무겁잖아. 경량화를 해보자
Atlas
-. 공부중..
Open-Domain QA 어떤 흐름으로 진행되는지 간략하게 살펴보았는데, 대략적으로 어떤 흐름을 가지고 발전했는지 느낌이 오시나요? 이제는 본격적으로 RAG논문에 대해서 리뷰를 진행해보도록 하겠습니다.
RAG paper review
1. Introduction
-. 기존의 모델들은 Implicit parametric knowledge으로 제작하다보니 확장성과 저장 정보 수정을 할 수 없다라고 이야기하고 있습니다. 또한, 모델은 그럴듯한 오답인 생성하는 hallucination 현상이 일어난다고 이야기합니다. 이러한 단점 때문에non-parametric memories를 결합한 모델이 나왔습니다.
-.여기서 모르는 용어가 있을실거 같아서 잠깐 정리를 해보자면, Implicit parametricmemories는 모델 파라미터에 저장된 정보를 이야기하며,Explicit non-parametric memories는 모델 외부에 있는 정보(일반적으로 DB)를 이야기합니다. Implicit parametricmemories과Explicit non-parametric memories를 결합하여 만든 대표적인 모델이 REALM입니다.
-. 저자들은 REALM의 연장선으로 parametricmemories과 non-parametric memories를 결합한 Generation Model을 만들었고, 이는 RAG(Retrieval-augmented Generation)라고 부르며, parametricmemories는 seq2seq transformer를 사용하였고, non-parametric memories로는 dense vector index of Wikipedia를 사용했다고 합니다. 이렇게 만든 RAG 모델은 QA의 open Natural Questions, WebQuestions and CuratedTrec에서 SOTA를 달성하였고, intensive-task(fact verification)에서도 SOTA를 달성하였다고 합니다.
2. Methods
-. RAG의 작동 방식은 input sequence $x$가 들어왔을 때 text document(non-parametric memories) 에서 관련된 documents $z$를 찾고 이를 활용하여 target sequence $y$를 생성합니다.
-. rag는 Retriever와 Generator라는 2개의 요소로 구성되어 있습니다.
-. 앞서 언급 드렸듯, Retriever에서는 input sequence $x$가 들어왔을 때 text document(non-parametric memories) 에서 관련된 documents $z$를 찾는 역할을 담당합니다.
-. Generator는 input sequence $x$와 documents $z$ 그리고 1부터 i-1까지 token(y)가 주어졌을 때 $y_{i}$가 나올 확률을 계산하며 생성을 진행하게 됩니다.
1) 첫 번째가 RAG-Sequence이고,RAG-Sequence는 동일한 문서를 사용하여 다음에 올 토큰을 예측합니다.
2) 두번째가 RAG-Token이며, RAG-Token는 여러 문서를 이용하여 다음에 올 토큰을 예측하게 됩니다.
좀 더 깊이 들어가 보겠습니다.
2.1 Models
먼저, RAG-Sequence에 대해서 조금 더 깊게 알아보도록 하겠습니다.
RAG-Sequence는 동일한 문서를 사용하여 다음에 올 토큰을 예측한다고 말씀 드렸습니다.
이를 조금 더 풀어서 설명을 드리겠습니다. 우리는 retriever를 사용하여 여러 개의 doc를 가져오게 됩니다. 그 중에서 한 개의 doc를 일단 선택하고 text generation을 진행하게 됩니다. 만약 5개의 doc가 있었다고 하면 5개의 answer가 생성이 되겠지요. RAG-Sequence에서는 여러 개의 doc를 검색하더라도 한 개의 doc에서 한 개의 answer를 생성하기 때문에 single latent variable를 이용한다고 볼 수 있습니다. 이를 수식으로 쓰면 이렇게 쓸 수 있습니다.
위 식은 이렇게 나누어서 생각할 수 있습니다.
$ p_{η} (z|x)$는 retriever에 입력 x가 들어갔을 때 z가 나올 확률입니다.
$ p_{\theta}(y|x,z)$는 x와 z가 주어졌을 때 정답 y가 나올 확률입니다.
이 두 식을 곱하므로써 두개의 다른 모델에서 각각 개별적으로 일어나는 task들을 end-to-end로 학습 할 수 있게 됩니다.
이제는 RAG-Token에 대해서 알아보도록 하겠습니다.
RAG-Token 모델에서는 Generator가 각 대상 토큰에 대해 다른 latent variable documents에서 token을 선택하여 Answer를 생성할 수 있도록 합니다.
2.2 Retriever: DPR
먼저 DPR이란, Training 단계에서는 연관된 question-passage끼리 가까운 공간에 위치하는 vector space를 구축 한 후 FAISS 형식으로 저장하여 새로운 질문이 입력으로 들어왔을 때 연관된 passage를 빠르게 검색하는 방법입니다.
FAISS는 시맨틱 검색을 이용하는 벡터 검색 라이브러리로써 기존에 사용되는 키워드 매칭이 아닌 문장의 의미에 초점을 맞춘 정보 검색 시스템으로 이해하시면 될 거 같습니다. RAG는 Retriever로는 DPR를 사용하였으며, DPR은 bi-encoder architecture를 사용하였습니다. 말 그대로 2개의 encoder를 사용하였다는 건데요, encoder로는 Bert_base모델을 사용하였으며, 하나는 query encoder로 사용하고 하나는 document encoder로 사용합니다. 여기서 중요한 것은 input $x$가 들어왔을 때 모든 Documents를 인덱싱와의 내적을 구하여 내적값이 최고로 높은 document을 찾아가는 방법을 MIPS (Maxium Inner Product Search)라고 합니다. 매번 인덱싱하여 유사도가 높은 문서를 top-k를 찾는 것은 너무 비효율적이기 때문에 모든 Documents에 대해 미리 indexing을 진행해 둡니다. 이런 구조를 만들기 위해 bi-encoder를 사용하게 됩니다. 이 해당 DPR retriever는 TriviaQA question과 Natural Question 으로 미리 fine-tuning 되어 있는 모델입니다. 앞에서 언급한 Non-parametric memory가 이 모든 Document의 index를 언급한 것입니다.
그럼 정리 해보겠습니다.
Retriever :
DB(Wiki) 등에서 관련된 정보를 검색해주는 모델 ( 방대한 정보량을 줄여주는 역할 )
passage와 question Encoder가 존재하며 내적값으로 Score를 구하게 됨
Answer 산출 시, Passage의 weight로 작용하게 됨
2.3 Generator:BART
해당 논문에서는 Generator로 BART-large 모델을 사용하였고 각각의 generator 방식에 따른 구조는 아래와 같습니다.
논문에서는 RAG-Sequence방식이 생성을 할 때 검색된 모든 문서를 참고하면서 생성하지 않기 때문에 일부 생성되지 않는 문장들이 있을 수 있다고 합니다. 그래서 추가적으로 forward를 돌려서 생성되지 않는 문장을 생성할 수 있도록 추가적인 연산을 진행하게 되는데 이를 논문에서는 철저한 디코딩 방식(Thorough Decoding)이라고 언급되어 있습니다. 또한, 논문에서는 매번 이렇게 추가적인 연산을 들여가며 문장을 생성할 수 없기 때문에 발견되지 않는 확률 값들은 0으로 처리하여 연산량을 줄인다고 언급하고 있습니다. 이러한 문제점 때문에 RAG-Token 생성방식이 나오게 되었습니다.
2.4 Training
이제 Training에 대해서 알아보도록 하겠습니다.
논문에서는 retriever와 generator components를 jointly하여 direct supervision 없이 훈련을 하였다고 합니다.
과연 어떻게 query encoder의 가중치를 업데이트 할 수 있었을까? 매우 궁금해하셨을텐데요.
이는 huggingface의 loss 부분 코드를 살펴보아야 조금 이해를 할 수 있습니다. 논문에는 query encoder 가중치 업데이트에 대한 별다른 언급이 없습니다(링크)
이곳에 가서 보시면, RagTokenGeneration class에서 get_nll이라는 loss 함수가 있는데, 이를 천천히 살펴보시면 document score loss에 들어가는 것을 볼 수 있습니다. 들어갈 때 log_softmax값을 취한 후 -(마이너스)가 붙어서 들어가게 됩니다. 이는 encoder가 유사도가 높은 document를 뽑으면 잘할 것이니 -값을 주어 더 많은 loss가 감소하는 원리로 코드가 구성되어 있으니 꼭 코드를 보시면서 참고해보시면 좋을 거 같습니다.
앞에서도 언급 드렸다시피 document encoder(BERT)는 freeze 시켜놓은 상태로 query encoder와 bart만 fine-tuning 하게 됩니다. 이러한 이유를 논문에서 짧게 언급하고 있는데요. 같이 훈련을 시켜봐도 좋은 성능을 관찰 할 수 없었다 라고 이야기하고 있습니다.
위에 녹색으로 표기된 <> code를 누르면, 위와 같은 창이 하나 뜹니다. 저기서 복사 버튼을 클릭하면 됩니다.
그리고 커맨트 창으로 가서 위에 기재한 코드를 입력하면 폴더가 하나 생기는 것을 볼 수 있습니다.
그럼, 해당 폴더로 들어갑니다.
cd ParlAI_SearchEngine
그럼 깃허브에서 보이는 것처럼 파일들이 보이게 됩니다.
그 상태에서 parlai 라이브러리를 설치하지 않으셨다면, conda 환경을 하나 생성합니다.
# 기존에 활성화된 conda를 끕니다.
conda deactivate
# 새로운 conda 환경을 생성합니다.
conda create -n parlai python=3.8 -y
# parlai라는 이름을 가진 conda를 만들것이고
# 이것의 파이썬 버전은 3.8를 사용할 것이고
# 설치 중간에 yes를 입력하는 것을 바로 하기 위해 -y 옵션을 넣어주는 것입니다.
# 새로운 conda를 실행합니다.
conda activate parlai
그후 바로 pip install requirements.txt를 하면 됩니다.
pip install -r requirements.txt
이렇게 하면 설치는 모두 끝납니다.
2. 어떻게 사용해야하는지?
터미널 창을 하나 더 켜주어서 같은 conda 환겨으로 접속하고, 아래 명령어를 입력하여 서버를 실행 시켜줍니다.
python search_server.py serve --host 0.0.0.0:8080
이 명령어를 설명드리겠습니다.
python search_server.py는 파이썬 파일을 실행하겠다라는 이야기입니다.
여러분이 간단한 파이썬 파일을 만들고 그 안에 프린터 명령어를 기재하고 python test.py 라고 하면 프린터 되는 것을 볼 수 있습니다.
그 뒤에 serve라는 것은 search_server.py 파일에서 Application에 있는 serve입니다.
그리고 --host는 우리가 실행할 서버를 어디에 올려야하는지 명기해주는 것입니다.
그리고 원래 실행되고 있던 터미널 창에는 아래와 같이 입력을 하게 되면 맨 처음 보신 이미지와 같이 검색이 되는 것을 확인할 수 있습니다.
curl -X POST "http://0.0.0.0:8080" -d "q=손흥민&n=5"
curl에 대한 설명은 별도의 포스팅을 해놓았습니다. 해당 글을 참고해주시면 감사드리겠습니다.
그럼 실행하는 방법도 모두 알아보았습니다. 그럼 이 서버가 어떻게 작동하는지를 알아보도록 하겠습니다.
3. 어떻게 작동하는지?
코드를 쭉 읽어보시면, 아래 순서와 같이 작동을 하는지는 쉽게 알 수 있습니다.
class Application : serve
↓
class GoogleSearchServer
그런데, 이 후 do_POST라는 함수가 작동이 되는데 디버그를 해보면 실제로 그냥 건너뛰게 됩니다.
왜 이렇게 작동을 하는지 몰라서 한참을 고민하던 끝에 이유를 발견하였습니다.
먼저, 다시 한번 실행되는 순서를 확인하기 위해 import traceback을 하여 call 되는 순서를 출력해보았습니다.
class SearchABC(http.server.BaseHTTPRequestHandler):
def do_POST(self):
""" Handle POST requests from the client. (All requests are POST) """
#######################################################################
# Prepare and Parse
#######################################################################
for line in traceback.format_stack():
print(line.strip())
위와 같이 for문을 추가하면, 아래와 같은 결과가 출력이 됩니다.
Host: 0.0.0.0:8080
File "/home/.conda/envs/parlai/lib/python3.8/threading.py", line 890, in _bootstrap
self._bootstrap_inner()
File "/home/.conda/envs/parlai/lib/python3.8/threading.py", line 932, in _bootstrap_inner
self.run()
File "/home/.conda/envs/parlai/lib/python3.8/threading.py", line 870, in run
self._target(*self._args, **self._kwargs)
File "/home/.conda/envs/parlai/lib/python3.8/socketserver.py", line 683, in process_request_thread
self.finish_request(request, client_address)
File "/home/.conda/envs/parlai/lib/python3.8/socketserver.py", line 360, in finish_request
self.RequestHandlerClass(request, client_address, self)
File "/home/.conda/envs/parlai/lib/python3.8/socketserver.py", line 747, in __init__
self.handle()
File "/home/.conda/envs/parlai/lib/python3.8/http/server.py", line 427, in handle
self.handle_one_request()
File "/home/.conda/envs/parlai/lib/python3.8/http/server.py", line 415, in handle_one_request
method()
File "search_server.py", line 92, in do_POST
for line in traceback.format_stack():
아! threading.py라는 파일로 넘어가는구나를 확인할 수 있고, 거기에서 handle_one_request라는 코드를 보면 되겠구나를 확인하였습니다.
def handle_one_request(self):
"""Handle a single HTTP request.
You normally don't need to override this method; see the class
__doc__ string for information on how to handle specific HTTP
commands such as GET and POST.
"""
try:
self.raw_requestline = self.rfile.readline(65537)
if len(self.raw_requestline) > 65536:
self.requestline = ''
self.request_version = ''
self.command = ''
self.send_error(HTTPStatus.REQUEST_URI_TOO_LONG)
return
if not self.raw_requestline:
self.close_connection = True
return
if not self.parse_request():
# An error code has been sent, just exit
return
mname = 'do_' + self.command
if not hasattr(self, mname):
self.send_error(
HTTPStatus.NOT_IMPLEMENTED,
"Unsupported method (%r)" % self.command)
return
method = getattr(self, mname)
method()
self.wfile.flush() #actually send the response if not already done.
except socket.timeout as e:
#a read or a write timed out. Discard this connection
self.log_error("Request timed out: %r", e)
self.close_connection = True
return
위 코드를 천천히 읽어보시면, command 라인에 입력된 command를 가지고 와서 앞에 do_를 붙히는 것을 볼 수 있습니다.
그런 후 getattr로 해당 텍스트의 속성 값을 호출하게 되고, 그 호출된 것을 method로 정의한 후 실행하는 코드를 볼 수 있습니다.
참 오래오래 디버깅한 끝에 알아냈습니다. 인공지능만 잘하면 되는 것이 아니라 이런 부분도 빠르게 읽을 수 있다면 연구하는데 속도가 날 것이라고 생각이 드는 하루였습니다.
이렇게 출력된 값이 오게 되니 이후 커스터마이징하면 제가 원하는 값을 처리 할 수 있을 듯 싶습니다.
SeeKeR를 사용하시는 분들을 위해 SeeKeR 샘플 코드도 함께 기재 드립니다.
# SeeKeR
python -m parlai.scripts.interactive -mf zoo:seeker/seeker_dialogue_400M/model \
-o /Users/kds/Documents/openchat/workspace/ko_parlai/parlai/opt_presets/gen/seeker_dialogue \
--search-server 0.0.0.0:8080
# 이렇게 --search-server라는 명령어를 입력하여 사용하시면 됩니다.