Semantic search and text generation in R. Part 1

Semantic search and text generation in R. Part 1

Introduction

For this task, I use LLM (Large Language Models – for example, chatGPT or opensource models) for internal tasks (a la search or question-matching system according to the required data).

I write in R and am also into NLP (hope I’m not the only one). But there are difficulties due to the fact that the main language for LLM is python. Accordingly, there are few examples and documentation on R, so you have to spend more time to “translate” from python, but on the other hand, I get pumped from it.

In order not to wall off your infrastructure, there are ready-made solutions to quickly and conveniently connect and use. These are LangChain and LlamaIndex. I usually use LangChain (it will be used later). I can’t say that it’s better that I use the first one. They are written in Python, but with the help of the reticulate library it also works in R.

Data and environment

We will use models from OpenAI and Hugging Face.

We install and connect libraries

library(reticulate)
library(data.table)
library(magrittr)
library(stringr)

py_install(c('torch', 
             'torchvision', 
             "torchaudio", 
             'langchain', 
             'docx2txt', 
             'huggingface_hub', 
             'sentence-transformers',
             'transformers',
             'faiss-cpu', 
             'openai==0.28.1', 
             'tiktoken'), pip = TRUE)


langchain <- import('langchain')
transformers <- import('transformers')

Constants

Immediately for two examples we define (chatGPT and HF models).

OPENAI_API_KEY <- '<YOUR OPENAI TOKEN>'
MODEL_NAME_GPT <- "gpt-4"

HUGGINGFACEHUB_API_TOKEN <- '<YOUR HUGGINGFACE TOKEN>'
EMBEDDINGS_MODEL_HF <- "distiluse-base-multilingual-cased-v2"
REPO_ID <- "mistralai/Mistral-7B-Instruct-v0.1"
MODEL_NAME_PIPE <- "cointegrated/rut5-base-multitask"

MAX_LEN <- 128L
TEMPERATURE <- 0.5

Data

To show the possibilities required data that language models had not seen before. For example, I compiled questions and answers from the Karachay-Balkar Nart epic.

data <- data.table(questions = c("Гемуда - чей конь?", "Как обычно выглядят эмегены?", "Как появился двухглавый Эльбрус?", "Где родился Сосурук?", "Как Сатанай спасла мужа?"),
                   answer = c("Судя по рассказам, Алауган был спокойным, скромным человеком огромного. Кроме Карашауая, у него детей не было. Он очень любил своего единственного сына и радовался, видя, что маленький Карашауай растет настоящим богатырем. Как-то однажды Алауган взял с собой Карашауая и повел его в подземелье, где находился Гемуда. Когда они открыли замок и вошли туда, он показал Гемуду и ушел, сказав: — Вот мой конь, на котором я езжу. Если ты будешь настоящим мужчиной, он всегда будет для тебя достойным конем. Он будет понимать все, что ты скажешь, и делать все то, что ты велишь. С этого дня вы будете неразлучны, познакомьтесь друг с другом.",
                              "Алауган стал разглядывать эмегеншу: рот ее был в одну сажень, нижняя губа свисала до грудей, на лбу у нее был один глаз, а на затылке ярко блестел, как утренняя звезда, другой глаз, величиной с человеческую голову. На красной, как медь, голове не было волос. Она была громадной, словно девять буйволов вместе. Руки и ноги ее были необъятной толщины. Ногти ее были длинными и острыми, как клюв орла.",
                              "— Не бей меня, — сказал Гемуда, — Если ты будешь достойным мужчиной, Я в море буду как рыба, обгоню даже рыб, Грудью ударив, подводные скалы могу разбить. В темном небе, как орел, могу летать. Только на земле не могу быть таким же быстрым, как Генжетай. А в морях и озерах я ему первенства не уступлю. Ведь Генжетай — из степных, а я — из морских коней. Вот потому я — первый на воде, А он в степи обгоняет всех коней. Молвив это, Гемуда к подножию Минги тау поднялся. Перепрыгнул через него на другую сторону. Его задние ноги задели его вершину и сделали гору двуглавой. Потом оттуда обратно перепрыгнул.", 
                              "Однажды, когда нарты возвращались из похода, на островке одной могучей реки они увидели маленького мальчика: на глазах нартов раскололся большой камень состар и этот мальчик появился из него. Нарты остановились на этом месте, чтобы дать отдых коням. Они стали препираться друг с другом: «Ты поплыви к острову за мальчиком». — «Нет, ты». Наконец один из нартов решился поплыть. Его звали Ёрюзмеком. Привязали
к поясу Ёрюзмека аркан и спустили его в реку. Ерюзмек доплыл до острова, снял аркан с пояса, привязал его к камню, а сам пошел к месту, где находился мальчик. Подошел он и увидел богатыря-младенца. Ёрюзмек протянул мальчику указательный палец. Мальчик тут же схватил его палец. Ёрюзмек пытался вытащить свой палец, но никак не мог, и, чтобы освободиться от мальчика, он так дернул руку, что вывихнул палец. Ёрюзмек вправил свой палец и доставил мальчика с острова к нартам.",
"Тогда взбешенный Ёрюзмек пошел к своей жене Сатанай за советом. Сатанай посоветовала ему идти на пир, но так как его хотят отравить там бузой, то
вставить в горло медную трубочку, через которую бы питье проливалось наружу. Она сама сделала это, и Ёрюзмек отправился на пир. Пир был громадный, Ёрюзмека чествовали больше всех, но он во рту вливал бузу вместо горла в медную трубку и остался совершенно трезвым и неотравленным, так как буза была подносима ему с отравой. "
                              ))

Search

To perform a search, the data must first be indexed, and for this it is vectorized through embeddings. In fact, embedding is a vector of a word (more precisely, a token) that reflects its characteristics. To get embeddings (turn them into vector words), you can use OpenAI solutions or based on open models. I use FAISS as a vector database – a quick solution for searching for similarity of vectors from Meta, but you can use other databases.

# эмбеддинги chatGPT 
embeddings_gpt = langchain$embeddings$OpenAIEmbeddings(openai_api_key=OPENAI_API_KEY)
# эмбеддинги SentenceTransformers 
# embeddings_hf = langchain$embeddings$HuggingFaceEmbeddings()
embeddings_hf = langchain$embeddings$HuggingFaceEmbeddings(model_name=EMBEDDINGS_MODEL_HF)

# загружаем датасет в лоадер, выделив колонку для векторизации
loader <- langchain$document_loaders$DataFrameLoader(data, page_content_column='questions')
# docx: loader <- langchain$document_loaders$Docx2txtLoader(data_path)
# pdf: loader <- langchain$document_loaders$PDFMinerLoader(data_path)
# txt: loader <- langchain$document_loaders$TextLoader(data_path)

documents <- loader$load()
documents[[1]]
### Document(page_content="Гемуда - чей конь?", metadata={'answer': 'Судя по рассказам, Алауган был спокойным, скромным человеком огромного. Кроме Карашауая, у него детей не было. Он очень любил своего единственного сына и радовался, видя, что маленький Карашауай растет настоящим богатырем. Как-то однажды Алауган взял с собой Карашауая и повел его в подземелье, где находился Гемуда. Когда они открыли замок и вошли туда, он показал Гемуду и ушел, сказав: — Вот мой конь, на котором я езжу. Если ты будешь настоящим мужчиной, он всегда будет для тебя достойным конем. Он будет понимать все, что ты скажешь, и делать все то, что ты велишь. С этого дня вы будете неразлучны, познакомьтесь друг с другом.'})

# создаем сплиттер документов, чтобы уложиться в лимит по токенам, в нашем случае это не очень полезный шаг
text_splitter<- langchain$text_splitter$RecursiveCharacterTextSplitter(chunk_size=400, chunk_overlap=0)
texts <- text_splitter$split_documents(documents)

# создаем хранилище
db_gpt <- langchain$vectorstores$FAISS$from_documents(texts, embeddings_gpt)
db_hf <- langchain$vectorstores$FAISS$from_documents(texts, embeddings_hf)

# db_hf$as_retriever()

# также можно сохранить хранилище локально
# db_hf$save_local(path_save_index)

# Загружаем индексы
# db = langchain$vectorstores$FAISS$load_local(path_save_index, embeddings_hf)
db_hf
### <langchain.vectorstores.faiss.FAISS object at 0x0000025D9ED4AD60>

# запрос
query <- "Какому нарту в конечном итоге принадлежит лошадь по имени Гемуда?"


# поиск. Чем меньше значение, тем ближе
# chatGPT
db_gpt$similarity_search_with_score(query)[[1]] %>% print
### [[1]]
### Document(page_content="Гемуда - чей конь?", metadata={'answer': 'Судя по рассказам, Алауган был спокойным, скромным человеком огромного. Кроме Карашауая, у него детей не было. Он очень любил своего единственного сына и радовался, видя, что маленький Карашауай растет настоящим богатырем. Как-то однажды Алауган взял с собой Карашауая и повел его в подземелье, где находился Гемуда. Когда они открыли замок и вошли туда, он показал Гемуду и ушел, сказав: — Вот мой конь, на котором я езжу. Если ты будешь настоящим мужчиной, он всегда будет для тебя достойным конем. Он будет понимать все, что ты скажешь, и делать все то, что ты велишь. С этого дня вы будете неразлучны, познакомьтесь друг с другом.'})
### 
### [[2]]
### [1] 0.2217191

# Hugging Face
db_hf$similarity_search_with_score(query)[[1]] %>% print

### [[1]]
### Document(page_content="Гемуда - чей конь?", metadata={'answer': 'Судя по рассказам, Алауган был спокойным, скромным человеком огромного. Кроме Карашауая, у него детей не было. Он очень любил своего единственного сына и радовался, видя, что маленький Карашауай растет настоящим богатырем. Как-то однажды Алауган взял с собой Карашауая и повел его в подземелье, где находился Гемуда. Когда они открыли замок и вошли туда, он показал Гемуду и ушел, сказав: — Вот мой конь, на котором я езжу. Если ты будешь настоящим мужчиной, он всегда будет для тебя достойным конем. Он будет понимать все, что ты скажешь, и делать все то, что ты велишь. С этого дня вы будете неразлучны, познакомьтесь друг с другом.'})
### 
### [[2]]
### [1] 0.4073001

Generating a response

Now let’s do not just a search, but so that the language model can answer questions. Actually RetrievalQA. I will not delve into how it works (you can learn on LangChain and DeepLearningAI (video in English), all in python, of course, but it can add insight). First, we will combine the question and the answer to search.

# загружаем датасет в лоадер, выделив колонку для векторизации
loader_n <- langchain$document_loaders$DataFrameLoader(copy(data)[, search := paste0("Question: ", questions, "\nAnswer: ", answer)], page_content_column = "search")
# docx: loader <- langchain$document_loaders$Docx2txtLoader(data_path)
# pdf: loader <- langchain$document_loaders$PDFMinerLoader(data_path)
# txt: loader <- langchain$document_loaders$TextLoader(data_path)

documents_n <- loader_n$load()
documents_n[[1]] %>% print()
### Document(page_content="Question: Гемуда - чей конь?\nAnswer: Судя по рассказам, Алауган был спокойным, скромным человеком огромного. Кроме Карашауая, у него детей не было. Он очень любил своего единственного сына и радовался, видя, что маленький Карашауай растет настоящим богатырем. Как-то однажды Алауган взял с собой Карашауая и повел его в подземелье, где находился Гемуда. Когда они открыли замок и вошли туда, он показал Гемуду и ушел, сказав: — Вот мой конь, на котором я езжу. Если ты будешь настоящим мужчиной, он всегда будет для тебя достойным конем. Он будет понимать все, что ты скажешь, и делать все то, что ты велишь. С этого дня вы будете неразлучны, познакомьтесь друг с другом.", metadata={'questions': 'Гемуда - чей конь?', 'answer': 'Судя по рассказам, Алауган был спокойным, скромным человеком огромного. Кроме Карашауая, у него детей не было. Он очень любил своего единственного сына и радовался, видя, что маленький Карашауай растет настоящим богатырем. Как-то однажды Алауган взял с собой Карашауая и повел его в подземелье, где находился Гемуда. Когда они открыли замок и вошли туда, он показал Гемуду и ушел, сказав: — Вот мой конь, на котором я езжу. Если ты будешь настоящим мужчиной, он всегда будет для тебя достойным конем. Он будет понимать все, что ты скажешь, и делать все то, что ты велишь. С этого дня вы будете неразлучны, познакомьтесь друг с другом.'})

# создаем хранилище
db_gpt_n <- langchain$vectorstores$FAISS$from_documents(documents_n, embeddings_gpt)
db_hf_n <- langchain$vectorstores$FAISS$from_documents(documents_n, embeddings_hf)

Now we use all the power of LLM.

# Open AI
llm_gpt <- langchain$chat_models$ChatOpenAI(temperature = TEMPERATURE, 
                                            openai_api_key = OPENAI_API_KEY, 
                                            model_name = MODEL_NAME_GPT, 
                                            max_tokens = MAX_LEN)

chain_gpt <- langchain$chains$RetrievalQA$from_chain_type(
  llm = llm_gpt,
  chain_type="stuff",
  retriever = db_gpt_n$as_retriever()
  )

# Hugging Face
llm_hf <- langchain$llms$HuggingFaceHub(repo_id = REPO_ID, 
                                        huggingfacehub_api_token = HUGGINGFACEHUB_API_TOKEN, 
                                        model_kwargs = list(temperature = TEMPERATURE, 
                                                            max_length = MAX_LEN), 
                                        # task = 'text2text-generation',
                                        task = 'text-generation'
                                        )

chain_hf <- langchain$chains$RetrievalQA$from_chain_type(
  llm = llm_hf,
  chain_type="stuff",
  retriever = db_hf_n$as_retriever()
  )

# Результат
chain_gpt$run(query) %>% cat()
### Лошадь Гемуда принадлежит Алаугану, который показал его своему сыну Карашауаю, предполагая, что если Карашауай вырастет настоящим мужчиной, Гемуда будет для него достойным конем.

chain_hf$run(paste0("[INST] ", query, " [/INST]")) %>% cat()
### Кроме Карашауая, у Алаугана детей не было. Он очень любил своего единственного сына и радовался, видя, что маленький Карашауай растет настоящим богатырем. Как-то однажды Алауган взял с собой Карашауая и повел его в под

The Mistral model did not do well, although it answered in English.

Generating the response by the local model

For example, you have your own model or want some model to work locally. This can be implemented in the following way. For variety, let’s change the model.

tokenizer <- transformers$T5Tokenizer$from_pretrained(MODEL_NAME_PIPE)
model <- transformers$T5ForConditionalGeneration$from_pretrained(MODEL_NAME_PIPE)


# загружаем датасет в лоадер, выделив колонку для векторизации
loader_pipeline <- langchain$document_loaders$DataFrameLoader(data, page_content_column = "answer")
documents_pipeline <- loader_pipeline$load()
documents_pipeline[[1]] %>% print()
### Document(page_content="Судя по рассказам, Алауган был спокойным, скромным человеком огромного. Кроме Карашауая, у него детей не было. Он очень любил своего единственного сына и радовался, видя, что маленький Карашауай растет настоящим богатырем. Как-то однажды Алауган взял с собой Карашауая и повел его в подземелье, где находился Гемуда. Когда они открыли замок и вошли туда, он показал Гемуду и ушел, сказав: — Вот мой конь, на котором я езжу. Если ты будешь настоящим мужчиной, он всегда будет для тебя достойным конем. Он будет понимать все, что ты скажешь, и делать все то, что ты велишь. С этого дня вы будете неразлучны, познакомьтесь друг с другом.", metadata={'questions': 'Гемуда - чей конь?'})

texts_pipeline <- text_splitter$split_documents(documents_pipeline)
db_hf_pipeline <- langchain$vectorstores$FAISS$from_documents(texts_pipeline, embeddings_hf)


# Pipeline
pipe <- transformers$pipeline(task = 'text2text-generation',
                              model = model, 
                              tokenizer = tokenizer)

llm_hf_pipeline = langchain$llms$huggingface_pipeline$HuggingFacePipeline(pipeline = pipe, 
                                                                          model_kwargs = list(temperature = TEMPERATURE))



chain_hf_pipeline <- langchain$chains$RetrievalQA$from_chain_type(
  llm = llm_hf_pipeline,
  chain_type="stuff",
  retriever = db_hf_pipeline$as_retriever()
  )

# Результат
chain_hf_pipeline$run(paste0("reply | ", query)) %>% cat()
### конь, на котором я езжу.

One sentence tried to squeeze out the answer.

texts_pipeline[[2]]
### Document(page_content="конь, на котором я езжу. Если ты будешь настоящим мужчиной, он всегда будет для тебя достойным конем. Он будет понимать все, что ты скажешь, и делать все то, что ты велишь. С этого дня вы будете неразлучны, познакомьтесь друг с другом.", metadata={'questions': 'Гемуда - чей конь?'})

Not the best model for this case, but relatively light and not bad for an example.

Conclusion

Current frameworks allow you to quickly enter the topic and implement solutions related to AI. And the implementations listed above are not the only ones, you can also add memory for communication and implement agents. About this in another article.

If you look events in Mykolaiv – https://city-afisha.com/afisha/

Related posts