一般的なRAGのworkflow, RAG series 5/n
RAGシリーズ5回目。
本稿にはあまり関係ないのですが、下の図がわかりやすくて良いなと感じたので、下図に則ってメモを残していこうと思いました。
RAGのフローを5つのStepに分割しています。
① Query Classification:RAGの要否判断
② Retrieval:情報源の取得
③ Reranking:取得情報の順序最適化
④ Repacking:構造化
⑤ Summarization:要約
この記事で紹介した、TOYOTAの出願特許の中で近年出願件数が増加している"Smart”や"Auto", "Management"などがkeywordとなるSystem関連topic群の特許をもとにフローを整理してみます。
概要
上記フローの②、⑤のみのフローで進めます。
実施内容
0. 環境
1. Retrieval Source
1-0. Data読込
# 特許Data読み込み(過去作成Dataframeを使用)
import pandas as pd
test_df = pd.read_pickle(f"{path}/test_df.pkl") #準備したデータ
# langchainのDataFrameLoaderでload
from langchain_community.document_loaders import DataFrameLoader
loader = DataFrameLoader(test_df, page_content_column="description") #明細文を使用
documents = loader.load()
1-1. Chunking
from langchain_text_splitters import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
# Set a really small chunk size, just to show.
separators=[
"\n\n",
"\n",
" ",
".",
",",
"\u200b", # Zero-width space
"\uff0c", # Fullwidth comma
"\u3001", # Ideographic comma
"\uff0e", # Fullwidth full stop
"\u3002", # Ideographic full stop
"",
],
chunk_size=500,
chunk_overlap=20,
length_function=len,
is_separator_regex=False,
)
docs = text_splitter.split_documents(documents)
1-2. Embedding
modelは性能そこそこ、sizeもコンパクトなBAAI/llm-embedderを使用
import langchain.embeddings
embedding = langchain.embeddings.HuggingFaceEmbeddings(
model_name="BAAI/llm-embedder"
)
1-3. Vector DataBase
冒頭で紹介した論文によればMilvusがよいのだそうです。使いなれたchromaを使用。
from langchain.vectorstores import Chroma
vectorstore = Chroma.from_documents(
documents=docs,
embedding=embedding
)
「電子制御ユニット(ECU)のOTA(Over-the-Air)ソフトウェアに関連する技術は何のために開発されたのか?」でhitするtextを確認します。
query = "What is the technology associated with electronic control unit (ECU) over-the-air (OTA) software developed for?"
docs_ = vectorstore.similarity_search(query=query, k=3)
for index, doc in enumerate(docs_):
print("%d:" % (index + 1))
print(doc.page_content)
2. LLM
modelの設定
from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline
import torch
import langchain.llms
model_id = "HODACHI/Llama-3.1-8B-EZO-1.1-it"
dtype = torch.bfloat16
tokenizer = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForCausalLM.from_pretrained(
model_id,
device_map="cuda",
torch_dtype=dtype,)
pipe = pipeline(
"text-generation",
model=model,
tokenizer=tokenizer,
max_new_tokens=500
)
llm = langchain.llms.HuggingFacePipeline(
pipeline=pipe
)
3. Retrieval
3-1. retriever : MMR(Maximal Marginal Relevance)
単純なretrieverを用いた場合の応答を確認します。
import langchain.chains
retriever=vectorstore.as_retriever(search_type="mmr", search_kwargs={"k": 5})
qa = langchain.chains.RetrievalQA.from_chain_type(
llm=llm,
retriever=retriever,
chain_type='stuff',
)
answer = qa.invoke(query)
3-2. +BM25 ensemble retriever
技術文書はkeywordの重要性が一般文書に比べて高いことが多く、keyword検索に強いBM25とのensemble, hybridで使用します。
from langchain.retrievers import BM25Retriever, EnsembleRetriever
bm25_retriever = BM25Retriever.from_documents(docs)
bm25_retriever.k = 5
ensemble_retriever = EnsembleRetriever(retrievers=[bm25_retriever, retriever],
weights=[0.4, 0.6])
qa = langchain.chains.RetrievalQA.from_chain_type(llm=llm, chain_type="stuff", retriever=ensemble_retriever)
answer = qa.invoke(query)
3-3. Query Rewriting, Decomposition
複雑な質問を 1 回の検索で正しく回答するのは困難であることが多いため、複数の質問に分割し、それぞれにcontextを探してそれらを統合し、微妙な違いのある出力を返すことで改善します。
最初の質問、queryを3つに分解します。
from langchain.prompts import ChatPromptTemplate
template = """You are a helpful assistant that generates multiple sub-questions related to an input question. \n
The goal is to break down the input into sub-questions that can be answers in isolation. \n
Do not do anything other than that.\n
Generate multiple search queries related to: {question} \n
Output (3 questions):"""
prompt_decomposition = ChatPromptTemplate.from_template(template)
prompt = prompt_decomposition.format(question=query)
questions = llm.invoke(prompt)
questions = questions.split("\n")
分割した質問を個別に回答を生成し、質問と回答のペアを作成します。
sequential_prompt = """You are a helpful assistant.Your response should be comprehensive and not contradicted with the following context if they are relevant. Otherwise, ignore them if they are not relevant.\n
Here is the question you need to answer:
\n --- \n {query} \n --- \n
Here is any available background question + answer pairs:
\n --- \n {q_a_pairs} \n --- \n
Here is additional context relevant to the question:
\n --- \n {context} \n --- \n
Use the above context and any background question + answer pairs to answer the question: \n {query}
"""
seq_decomposition_prompt = ChatPromptTemplate.from_template(sequential_prompt)
def format_qa_pair(query, answer):
"""Format Q and A pair"""
formatted_string = ""
formatted_string += f"Question: {query}\nAnswer: {answer}\n\n"
return formatted_string.strip()
q_a_pairs = ""
for q in questions:
context = ',.,'.join([i.page_content for i in ensemble_retriever.invoke(q)])
answer = qa.invoke(seq_decomposition_prompt.format(query=q,q_a_pairs=q_a_pairs,context=context))
q_a_pair = format_qa_pair(q,answer['result'][answer['result'].index('Helpful Answer')+len('Helpful Answer'):])
q_a_pairs = q_a_pairs + "\n---\n"+ q_a_pair
4. Reranking
5. Repacking
skip
6. Summarization
【3-3. Query Rewriting, Decomposition】で分割した3つの質問とその回答、それらと最初の質問から最終回答を生成します。
context = ',.,'.join([i.page_content for i in ensemble_retriever.invoke(query)])
answer = qa.invoke(seq_decomposition_prompt.format(query=query,q_a_pairs=q_a_pairs,context=context))
所感
この1年でRAGの工夫が大量に出てきています。
冒頭の図のようにフローを区分するとわかりやすくなりますね。
この記事が気に入ったらサポートをしてみませんか?