Crie um mecanismo de pesquisa de imagens com Python e MongoDB
Mark Smith8 min read • Published Jan 31, 2024 • Updated Sep 18, 2024
APLICATIVO COMPLETO
Avalie esse Tutorial
Ainda me recordo quando a pesquisa começou a funcionar no Google fotos — a plataforma onde armazeno todas as fotos que tirava no meu celular. Pareceu-me mágico que algum tipo de técnica de aprendizado de máquina pudesse me permitir descrever uma imagem em minha vasta coleção de fotos e fazer com que a plataforma retornasse essa imagem para mim, junto com quaisquer imagens semelhantes.
Uma das técnicas usadas para isso é a classificação de imagens, onde uma rede causal é usada para identificar objetos e até mesmo pessoas em uma imagem, e a imagem é marcada com esses dados. Outra técnica — que é, no mínimo, mais poderosa — é a capacidade de gerar uma incorporação vetorial para a imagem usando um modelo de incorporação que funciona com texto e imagens.
Usar um modelo de incorporação multimodal como esse permite gerar um vetor que pode ser armazenado e indexado com eficiência no MongoDB Atlas e, quando você desejar recuperar uma imagem, o mesmo modelo de incorporação pode ser usado para gerar um vetor que é então usado para procurar imagens semelhantes à descrição. É quase como mágica.
Um modelo de incorporação multimodal é um modelo de aprendizado de máquina que codifica informações de vários tipos de dados, como texto e imagens, em um espaço vetorial comum. Ele ajuda a vincular diferentes tipos de dados para tarefas como correspondência de texto para imagem ou tradução entre modalidades.
A vantagem disso é que o texto e as imagens podem ser indexados da mesma forma, permitindo que as imagens sejam pesquisadas fornecendo texto ou outra imagem. Você pode até pesquisar um item de texto com uma imagem, mas não consigo pensar em um motivo pelo qual você gostaria de fazer isso. A desvantagem dos modelos multimodais é que eles são muito complexos de produzir e, portanto, não são tão "inteligentes" quanto alguns dos modelos monomodo que estão sendo produzidos atualmente.
Neste tutorial, mostrarei como usar o modeloclic-ViT-L-14, que codifica texto e imagens no mesmo espaço vetorial. Como estamos usando Python, instalarei o modelo diretamente em meu ambiente Python para executar localmente. Na produção, você provavelmente não gostaria de ter seu modelo de incorporação executado diretamente dentro de seu aplicativo web, pois ele acopla muito fortemente seu modelo, que requer uma CPU poderosa, ao restante do aplicativo, que geralmente será vinculado a IO. Nesse caso, você pode hospedar um modelo apropriado no Abraços Face ou em uma plataforma semelhante.
Esse exemplo de mecanismo de busca será uma prova de conceito. Todo o código está disponível em um Notebook Jupyter, e estou armazenando todas as minhas imagens localmente no disco. Na produção, você gostaria de usar um serviço de armazenamento de objetos como o S3 da Amazon.
Da mesma forma, em produção, você gostaria de hospedar o modelo usando um serviço especializado ou alguma configuração dedicada no hardware apropriado, enquanto eu tentaria baixar e executar o modelo localmente.
Se você tiver uma máquina mais antiga, pode demorar um pouco para gerar os vetores, mas descobrir que, em um Intel Macbook Pro de quatro anos, poderia gerar cerca de 1,000 incorporações em 30 minutos, ou o meu Macbook Air M2 faz o mesmo em cerca de cinco minutos! De qualquer forma, talvez você Go e faça uma chávena de café para si mesmo quando o bloco de anotações chegar a essa etapa.
O mecanismo de busca usará o mesmo modelo vetorial para codificar consultas (que são texto) no mesmo espaço vetorial usado para codificar dados de imagem, o que significa que uma frase descrevendo uma imagem deve aparecer em um local semelhante à localização da imagem no espaço vetorial. Essa é a mágica dos modelos vetoriais multimodais!
A primeira coisa que você vai querer fazer é criar um ambiente virtual usando sua técnica favorita.Costumo usarvenv, que vem com o Python.
Depois de fazer isso, instale dependências com:
1 pip install -r requirements.txt
Em seguida, você precisará definir uma variável de ambiente,
MONGODB_URI
, contendo a connection string para seu cluster MongoDB.1 # Set the value below to your cluster: 2 export MONGODB_URI="mongodb+srv://image_search_demo:my_password_not_yours@sandbox.abcde.mongodb.net/image_search_demo?retryWrites=true&w=majority"
Outra coisa que você precisará é de um diretório "images" que contenha algumas imagens para indexar! Fiz o download do conjunto de dados ImageNet 1000 (mini) da Kaggle, que contém muitas imagens com cerca de 4GB, mas você pode usar um conjunto de dados diferente, se preferir. O notebook pesquisa o diretório "images" recursivamente, portanto, não é necessário ter tudo no nível superior.
Então, você pode iniciar o notebook com:
1 jupyter notebook "Image Search.ipynb"
Se você configurou o notebook conforme descrito acima, você deve ser capaz de executá-lo e seguir as explicações no caderno. Neste tutorial, vou destacar o código mais importante, mas não vou reproduzi-lo todo aqui, pois trabalhei duro para tornar o notebook compreensível sozinho.
Primeiro, vamos configurar uma coleção com um índice de pesquisa vetorial apropriado. No Atlas, se você se conectar a um cluster, poderá configurar índices de pesquisa vetorial na guia Atlas Search, mas eu preferia configurar índices em meu código para manter tudo autônomo.
O código a seguir pode ser executado várias vezes, mas só criará a coleção e o índice de pesquisa associado na primeira execução. Isto é útil se você quiser executar o notebook várias vezes!
1 client = MongoClient(MONGODB_URI) 2 db = client.get_database(DATABASE_NAME) 3 4 # Ensure the collection exists, because otherwise you can't add a search index to it. 5 try: 6 db.create_collection(IMAGE_COLLECTION_NAME) 7 except CollectionInvalid: 8 # This is raised when the collection already exists. 9 print("Images collection already exists") 10 11 # Add a search index (if it doesn't already exist): 12 collection = db.get_collection(IMAGE_COLLECTION_NAME) 13 if len(list(collection.list_search_indexes(name="default"))) == 0: 14 print("Creating search index...") 15 collection.create_search_index( 16 SearchIndexModel( 17 { 18 "mappings": { 19 "dynamic": True, 20 "fields": { 21 "embedding": { 22 "dimensions": 768, 23 "similarity": "cosine", 24 "type": "knnVector", 25 } 26 }, 27 } 28 }, 29 name="default", 30 ) 31 ) 32 print("Done.") 33 else: 34 print("Vector search index already exists")
A parte mais importante do código acima é a configuração que está sendo passada para
create_search_index
:1 { 2 "mappings": { 3 "dynamic": True, 4 "fields": { 5 "embedding": { 6 "dimensions": 768, 7 "similarity": "cosine", 8 "type": "knnVector", 9 } 10 }, 11 } 12 }
Isso especifica que o índice indexará todos os campos no documento (porque "dynamic" está definido como "true") e que o campo "embedding" deve ser indexado como um vetor de incorporação, usando similaridade de cosseno. Atualmente, "knnVector" é o único tipo suportado pelo Atlas. A dimensão do vetor é definida como 768 porque esse é o número de dimensões vetoriais usadas pelo modelo CLIP.
A linha de código a seguir pode não parecer muito, mas na primeira vez que você a executar, ela fará o download do modelo Clip-Vit-L-14, que tem cerca de 2GB:
1 # Load CLIP model. 2 # This may print out warnings, which can be ignored. 3 model = SentenceTransformer("clip-ViT-L-14")
Dado um caminho para um arquivo de imagem, uma incorporação para essa imagem pode ser gerada com o seguinte código:
1 emb = model.encode(Image.open(path))
Nesta linha de código,
model
é o SentenceTransformer que criei acima e Image
vem da bibliotecaPillow e é usado para carregar os dados da imagem.Com o vetor de incorporação, um novo documento pode ser criado com o código abaixo:
1 collection.insert_one( 2 { 3 "_id": re.sub("images/", "", path), 4 "embedding": emb.tolist(), 5 } 6 )
Estou armazenando apenas o caminho para a imagem (como um identificador exclusivo) e o vetor de incorporação. Em um aplicativo do mundo real, eu armazenaria todos os metadados de imagem que meu aplicativo exigisse e provavelmente um URL para um objeto S3 contendo os próprios dados da imagem.
Observação: lembre-se de que queries vetoriais podem ser combinadas com qualquer outra técnica de query que você normalmente usaria no MongoDB! Essa é a grande vantagem que você obtém usando o Atlas Vector Search — ele faz parte do MongoDB Atlas, para que você possa executar query e transformar seus dados da maneira que quiser e até mesmo combiná-los com o poder do Atlas Search para consultas de texto livre.
O Notebook Jupyter carrega imagens em um loop — por padrão, ele carrega 10 imagens — mas isso não é suficiente para ver os benefícios de um mecanismo de pesquisa de imagens, então você provavelmente vai querer alterar
NUMBER_OF_IMAGES_TO_LOAD
para 1000 e execute o bloco de código de carregamento da imagem novamente.Depois de indexar um bom número de imagens, é hora de testar se elas funcionam bem. Eu defini duas funções que podem ser usadas para isso. A primeira função,
display_images
, pega uma lista de documentos e exibe as imagens associadas em uma grade. Não estou incluindo o código aqui porque é uma função utilitária.A segunda função,
image_search
, pega uma frase de texto, codifica-a como uma incorporação de vetor e, em seguida, usa o estágio de agregação$vectorSearch
do MongoDB para procurar imagens mais próximas desse local do vetor, limitando o resultado aos nove documentos mais próximos:1 def image_search(search_phrase): 2 """ 3 Use MongoDB Vector Search to search for a matching image. 4 5 The search_phrase is first converted to a vector embedding using 6 the model loaded earlier in the Jupyter notebook. The vector is then used 7 to search MongoDB for matching images. 8 """ 9 emb = model.encode(search_phrase) 10 cursor = collection.aggregate( 11 [ 12 { 13 "$vectorSearch": { 14 "index": "default", 15 "path": "embedding", 16 "queryVector": emb.tolist(), 17 "numCandidates": 100, 18 "limit": 9, 19 } 20 }, 21 {"$project": {"_id": 1, "score": {"$meta": "vectorSearchScore"}}}, 22 ] 23 ) 24 25 return list(cursor)
O estágio
$project
adiciona um campo "score" que mostra a semelhança de cada documento com o vetor de consulta original. 1.0 significa "exatamente o mesmo," enquanto 0.0 significaria que a imagem retornada era totalmente diferente.Com a função exibição_imagens e a função imagem_search, posso pesquisar imagens de "sharks in the pool":
1 display_images(image_search("sharks in the water"))
No meu laptop, recebo a seguinte grade de nove imagens, o que é muito bom!
Quando tentei pela primeira vez a pesquisa acima, não tinha imagens suficientes carregadas, então a consulta acima incluía a foto de um corgi em pé sobre ladrilhos cinza. Essa não foi uma partida muito disputada! Depois de carregar mais algumas imagens para corrigir os resultados da consulta sobre tubarões, ainda consegui encontrar a imagem do corgi pesquisando por " corgi on snow " — é a segunda imagem abaixo. Observe que nenhuma das imagens corresponde exatamente à consulta, mas algumas são definitivamente corgis e várias estão paradas na neve.
1 display_images(image_search("corgi in the snow"))
Uma das coisas que realmenteadoro na pesquisa vetorial é que ela é "semântica", então posso pesquisar por algo bastante obscuro, como "infância".
1 display_images(image_search("childhood"))
Meu resultado favorito foi quando pesquisei "ennui" (um sentido de apatia e preocupação decorrente da falta de atividade ou euforia) que retornou fotos de bichos entediados (e de um menor)!
1 display_images(image_search("ennui"))
Esperemos que você tenha achado este tutorial tão legal de ler quanto eu deixei de escrever!
Se você quisesse executar este modelo em produção, provavelmente desejaria usar um serviço de hospedagem como o Hugging Face, mas eu realmente gosto da capacidade de instalar e testar um modelo em meu laptop com uma única linha de código. Depois que a geração de incorporação, que exige muito do processador e, portanto, é uma tarefa de bloqueio, é delegada a uma chamada de API, seria mais fácil construir um wrapper FastAPI em torno da funcionalidade desse código. Então, você poderia construir uma interface web poderosa em torno dele e implantar seu próprio mecanismo de pesquisa de imagens personalizado.
Este exemplo também não demonstra muitos dos recursos de query do MongoDB. O poder do vetor Atlas Search com o MongoDB Atlas é a capacidade de combiná-lo com todo o poder da estrutura de agregação do MongoDB para consultar e agregar seus dados. Se eu tiver algum tempo, posso estender este exemplo para filtrar por critérios como a data de cada imagem e talvez permitir que as fotos sejam marcadas manualmente ou agrupadas automaticamente em coleções.
Principais comentários nos fóruns
Ainda não há comentários sobre este artigo.