Links da Aula:
Notebook da aula: Google Colab
Mistral AI LangChain Mistral-7B-Instruct-v0.2 ChromaAula 49 - Retrieval Augmented Generation com Mistral AI e Langchain
Mistral AI
A Mistral AI é uma empresa francesa de inteligência artificial fundada em 2023 por ex-funcionários da Meta e do Google DeepMind. A Mistral AI é comprometida com a ciência aberta, comunidade e software livre. Grande parte de seus modelos e ferramentas de implementação são lançados sob licenças permissivas, como Apache 2.0. Isso significa que:- O código está disponível para qualquer pessoa inspecionar, modificar e usar.
- A comunidade pode contribuir para o desenvolvimento e aprimoramento da tecnologia.
- A IA se torna mais democrática, transparente e acessível a todos.
LangChain
Framework de código aberto para:- Conectar LLMs a dados
- Criar pipelines de NLP
- Personalizar LLMs
- Implementar interfaces de usuário
Benefícios:
- Acelera o desenvolvimento
- Simplifica o uso de LLMs
- Promove a reutilização de código
- Amplia a comunidade de LLMs
O RAG
Começaremos inicializando o Modelo de Linguagem (LLM) tanto para a incorporação(embeddings) de texto quanto para a geração de resposta. Para os embeddings vamos fazer usar o all-mpnet-base-v2 e o distilbert-base-uncased Para gerar a resposta, utilizaremos o modelo "Mistral-7B-Instruct-v0.2", especificamente sua versão quantizada. Obs. Os parâmetros são os pesos, isto é, a quantidade de pesos que compõem o modelo, ou seja, os pesos ajustáveis da rede neural que compõe o modelo. Carregar esse modelo quantizado requer uma GPU com pelo menos 16 GB de RAM. Por favor, verifique se sua GPU atende a esse requisito. Precisaremos de uma configuração na qual possamos carregar o modelo em 4 bits, como na quantização FP4. Para isso, utilizamos uma conta Colab Pro equipada com uma GPU T4.Passo 1
Aqui nesse passo, importamos as bibliotecas necessárias. Bibliotecas usadas- chromadb: Biblioteca para armazenamento e recuperação eficiente de dados estruturados usando índices de proximidade. Estamos usando esta biblioteca para armazenar e recuperar os chunks de texto e suas embeddings, facilitando a busca durante o processo de geração de respostas.
- langchain: Uma biblioteca para processamento de linguagem natural (NLP) que fornece diversas ferramentas, como divisão de texto, embeddings de texto e geração de respostas. Estamos usando langchain para dividir o texto do documento PDF em partes gerenciáveis, processar as embeddings de texto e implementar a geração de respostas.
- pypdf: Uma biblioteca para processamento de arquivos PDF. Estamos utilizando esta biblioteca para carregar e extrair texto de documentos PDF fornecidos como entrada para o sistema.
- sentencepiece: Uma biblioteca para tokenização de texto, frequentemente usada em modelos de aprendizado de máquina para dividir texto em tokens.
- bitsandbytes: Uma biblioteca para manipulação eficiente de bits e bytes em Python.
- transformers: Uma biblioteca desenvolvida pela Hugging Face para implementação e uso de modelos de linguagem pré-treinados, como BERT, GPT, etc. Estamos usando esta biblioteca para carregar o modelo quantizado Mistral-7B-Instruct-v0.2 e seu tokenizer associado.
- peft: Biblioteca que oferece aceleração para pipelines da Hugging Face.
- accelerate: Outra biblioteca para aceleração de treinamento de modelos de aprendizado de máquina, desenvolvida pela Hugging Face.
- sentence-transformers: Uma biblioteca para criar embeddings de texto semântico de alta qualidade usando modelos pré-treinados. Estamos usando esta biblioteca para obter embeddings de texto que serão armazenadas no banco de dados.
Passo 2
Aqui é realizada a técnica de quantização e em seguida, carregamos o modelo quantizado usando o Pipeline da HuggingFace.# carrega a biblioteca necessária
import torch
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.embeddings import HuggingFaceEmbeddings
from langchain.vectorstores import Chroma
from langchain.llms.huggingface_pipeline import HuggingFacePipeline
from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline, BitsAndBytesConfig
from langchain.document_loaders import PyPDFLoader
from langchain.chains.question_answering import load_qa_chain
from langchain.prompts import PromptTemplate
quantization_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_compute_dtype=torch.bfloat16
)
model_kwargs = {'device': 'cuda'}
#embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-mpnet-base-v2", model_kwargs=model_kwargs)
embeddings = HuggingFaceEmbeddings(model_name="distilbert-base-uncased", model_kwargs=model_kwargs)
tokenizer = AutoTokenizer.from_pretrained("mistralai/Mistral-7B-Instruct-v0.2")
model = AutoModelForCausalLM.from_pretrained("mistralai/Mistral-7B-Instruct-v0.2", device_map='auto', quantization_config=quantization_config)
pipe = pipeline("text-generation", model=model, tokenizer=tokenizer, max_new_tokens=150)
llm = HuggingFacePipeline(pipeline=pipe)
O código que carrega várias bibliotecas e configurações necessárias para implementar a Retrieval Augmented Generation (RAG) usando o Langchain e o Hugging Face Transformers.
No quantization_config, a configuração prepara o modelo para usar uma quantização agressiva (redução para 4 bits) para armazenamento de dados e utiliza o tipo de dado bfloat16 nas computações, visando otimização de desempenho e eficiência de memória.
Na linha do embeddings, onde temos a criação de um objeto para gerar embeddings (representações numéricas) de texto, vamos testar dois modelos.
Primeiro o sentence-transformers/all-mpnet-base-v2 e depois o distilbert-base-uncased.
Na parte do model_kwargs é criado um dicionário chamado model_kwargs, o qual contém uma configuração para especificar o dispositivo em que um modelo de inteligência artificial deve ser executado.
Ao passar model_kwargs para um modelo ou função em bibliotecas que suportam operações em GPU, como PyTorch, você está instruindo o modelo a executar suas operações na GPU em vez de na CPU, buscando um processamento mais rápido.
Passo 3
Nesse passo o PDF é carregado, dividido em pedaços com alguma sobreposição por questões de contexto.
# Carrega o arquivo PDF
pdf_link = "/content/sample_data/CF.pdf"
loader = PyPDFLoader(pdf_link, extract_images=False)
pages = loader.load_and_split()
# Divida os dados em partes(chunks)
text_splitter = RecursiveCharacterTextSplitter(
chunk_size = 4000,
chunk_overlap = 20,
length_function = len,
add_start_index = True,
)
chunks = text_splitter.split_documents(pages)
Código que carrega um arquivo PDF e divide seu conteúdo em partes gerenciáveis.
- Carregamento do Arquivo PDF:
pdf_link = "/content/sample_data/CF.pdf": Especifica o caminho do arquivo PDF a ser carregado. Neste caso, o caminho é para o arquivo "CF.pdf" localizado em "/content/sample_data/".loader = PyPDFLoader(pdf_link, extract_images=False): Inicializa um objetoPyPDFLoaderpara carregar o arquivo PDF especificado. O parâmetroextract_images=Falseindica que as imagens presentes no PDF não serão extraídas durante o carregamento.pages = loader.load_and_split(): Carrega o PDF e divide seu conteúdo em páginas individuais.
- Divisão do Texto em Chunks:
text_splitter = RecursiveCharacterTextSplitter(...): Inicializa um objetoRecursiveCharacterTextSplitterpara dividir o texto em partes menores, chamadas de "chunks".chunk_size = 4000: Define o tamanho máximo de cada chunk em caracteres. Neste caso, cada chunk terá no máximo 4000 caracteres.chunk_overlap = 20: Define o número de caracteres de sobreposição entre os chunks adjacentes. Isso garante que não haja perda de informação entre os chunks.length_function = len: Define a função que será usada para calcular o comprimento do texto. Aqui, estamos usando a funçãolen()padrão do Python.add_start_index = True: Indica se deve ser adicionado um índice de início ao texto dividido.chunks = text_splitter.split_documents(pages): Divide o texto das páginas do PDF em chunks usando os parâmetros especificados.
Passo 4
Agora o código que armazena os dados divididos em chunks em um banco de dados usando a biblioteca Chroma.
# Armazena dados no banco de dados
db=Chroma.from_documents(chunks,embedding=embeddings, persist_directory="test_index")
db.persist()
- Armazenamento dos Dados no Banco de Dados:
db = Chroma.from_documents(chunks, embedding=embeddings, persist_directory="test_index"): Cria um objeto Chroma e adiciona os chunks de texto como documentos no banco de dados. O parâmetrochunkscontém a lista de chunks de texto que foram divididos anteriormente. O parâmetroembedding=embeddingsespecifica as embeddings de texto associadas aos chunks, que foram calculadas anteriormente. O parâmetropersist_directory="test_index"especifica o diretório onde os dados serão armazenados no disco.
- Persistência do Banco de Dados:
db.persist(): Salva o banco de dados no disco. Esta operação garante que os dados permaneçam armazenados mesmo após o encerramento do programa.
Passo 5
Carrega o banco de dados de vetores, configura um recuperador para buscar documentos relevantes no banco de dados e carrega uma cadeia de questionamento e resposta (QNA) usando o LangChain.
# Carrega o banco de dados de vetores
vectordb = Chroma(persist_directory="test_index", embedding_function=embeddings)
# Carrega o recuperador
retriever = vectordb.as_retriever(search_kwargs={"k": 3})
# Template de prompt para Pergunta e Resposta
qna_prompt_template = """### [INST] Instrução: Serão fornecidas perguntas e dados relacionados. Sua tarefa é encontrar as respostas para as perguntas usando os dados fornecidos. Se os dados não contiverem a resposta para a pergunta, então você deve retornar 'Informação insuficiente.'
{context}
### Pergunta: {question} [/INST]"""
# Cria o template de prompt
PROMPT = PromptTemplate(
template=qna_prompt_template, input_variables=["context", "question"]
)
# Carrega a cadeia de questionamento e resposta
chain = load_qa_chain(llm, chain_type="stuff", prompt=PROMPT)
Explicação do que cada parte faz:
- Carregamento do Banco de Dados de Vetores:
vectordb = Chroma(persist_directory="test_index", embedding_function=embeddings): Cria um objeto Chroma para carregar o banco de dados previamente criado e armazenado no diretório "test_index". O parâmetroembedding_function=embeddingsespecifica a função de embeddings a ser usada para recuperar as embeddings de texto associadas aos documentos no banco de dados.
- Configuração do Recuperador:
retriever = vectordb.as_retriever(search_kwargs={"k": 3}): Configura o banco de dados de vetores como um recuperador para buscar documentos relevantes com base nas embeddings de texto. O parâmetrosearch_kwargs={"k": 3}especifica que o recuperador buscará três partes, de pedaços do documento, que sejam mais relevantes para cada pergunta.
- Template de Prompt para Pergunta e Resposta:
qna_prompt_template: Define um template de prompt para perguntas e respostas, que será utilizado para formatar a entrada para a cadeia de perguntas e resposta (QNA). Este template inclui placeholders para o contexto e a pergunta.
- Criação do Template de Prompt:
PROMPT = PromptTemplate(template=qna_prompt_template, input_variables=["context", "question"]): Cria um objeto de template de prompt utilizando o template definido anteriormente e especificando as variáveis de entrada como "context" e "question".
- Carregamento da Cadeia de Pergunta e Resposta:
chain = load_qa_chain(llm, chain_type="stuff", prompt=PROMPT): Carrega a cadeia de pergunta e resposta (QNA) utilizando o modelo de linguagem pré-treinado (llm) e o template de prompt configurado anteriormente. O parâmetrochain_type="stuff"especifica o tipo de cadeia a ser carregado.
Passo 6
def ask(question):
max_length = 200
aviso_fora_de_contexto = "Aviso: A pergunta está fora do contexto dos documentos fornecidos. Fornecendo uma resposta com base no conhecimento geral do modelo."
prompt = "Responda em português: " + question
# Tenta encontrar documentos relevantes para a pergunta
context = retriever.get_relevant_documents(question)
try:
if not context:
print(aviso_fora_de_contexto)
full_response = llm.invoke(prompt, max_length=max_length)
else:
chain_response = chain.run({"input_documents": context, "question": question})
if "Informação insuficiente" in chain_response or chain_response.strip() == "":
print(aviso_fora_de_contexto)
full_response = llm.invoke(prompt, max_length=max_length)
else:
full_response = chain_response
# Encontra um ponto de corte lógico
response = full_response.strip().split("\n")[0]
if len(response) >= max_length - 1:
# Procura o último ponto final antes do limite
last_period = response.rfind('.')
response = response[:last_period + 1] if last_period != -1 else response
response += " [Resposta truncada. Consulte a fonte para mais detalhes.]"
if not context:
response = aviso_fora_de_contexto + "\n\nResposta: " + response
else:
response = "Resposta: " + response
except Exception as e:
response = f"Erro ao gerar resposta: {e}"
return response
A função ask(question) é responsável por receber uma pergunta do usuário, buscar documentos relevantes no banco de dados usando o recuperador configurado, e gerar uma resposta usando a cadeia de questionamento e resposta (QNA). Aqui está uma explicação do que cada parte da função faz:
max_length = 200: Define o comprimento máximo da resposta gerada.aviso_fora_de_contexto = "Aviso: A pergunta está fora do contexto dos documentos fornecidos. Fornecendo uma resposta com base no conhecimento geral do modelo.": Define uma mensagem de aviso para indicar que a pergunta está fora do contexto do documento fornecido.prompt = "Responda em português: " + question: Define o prompt que será usado para gerar a resposta, incluindo a pergunta feita pelo usuário.context = retriever.get_relevant_documents(question): Busca documentos relevantes no banco de dados com base na pergunta fornecida pelo usuário.if not context:: Verifica se não há documentos relevantes encontrados para a pergunta.- Se não houver contexto, a função tentará gerar uma resposta com base no conhecimento geral do modelo usando o modelo de linguagem pré-treinado (
llm.invoke()). - Se ocorrer um erro durante a geração da resposta, a função captura a exceção e retorna uma mensagem de erro.
- Se não houver contexto, a função tentará gerar uma resposta com base no conhecimento geral do modelo usando o modelo de linguagem pré-treinado (
else:: Se houver contexto (trechos relevantes do documento forma encontrados), a função executa a cadeia de questionamento e resposta (QNA) para gerar a resposta.- Se a resposta da cadeia for "Informação insuficiente" ou estiver vazia, a função tentará gerar uma resposta com base no conhecimento geral do modelo.
- Se ocorrer um erro durante a geração da resposta, a função captura a exceção e retorna uma mensagem de erro.
return response: Retorna a resposta gerada para o usuário.
Passo 7
# Pega a entrada do usuário e chama a função para gerar a saída
user_question = input("Usuário: ")
answer = ask(user_question)
print("Resposta:", answer)
Permite que o usuário faça uma pergunta e em seguida chama a função ask(user_question) para gerar uma resposta com base na pergunta fornecida. Aqui está uma explicação do que cada parte faz:
user_question = input("Usuário: "): Solicita ao usuário que insira uma pergunta através da entrada padrão (teclado) e armazena a pergunta na variáveluser_question.answer = ask(user_question): Chama a funçãoask(user_question)passando a pergunta do usuário como argumento e armazena a resposta gerada na variávelanswer.print("Resposta:", answer): Imprime a resposta gerada para a pergunta do usuário na tela, precedida pelo texto "Resposta:".