Comparação de técnicas de NLP para Atlas Search de produtos escaláveis
como
VH
NC
MM
Adit Shrimal, Varun Hande, Neil Chitre, Manav Middha8 min read • Published Sep 23, 2024 • Updated Sep 23, 2024
Avalie esse Artigo
No setor de varejo online em rápida expansão, os dados estão sendo gerados em uma escala sem precedentes. Para fornecer uma experiência de compra perfeita aos clientes, é crucial criar uma arquitetura eficiente e escalável do Atlas Search para armazenar e processar dados. Neste artigo, compararemos quatro técnicas populares de processamento de linguagem natural (NLP) - BM25, TF-IDF, Word2Vec e BERT - para encontrar empiricamente a solução mais ideal para recuperar os resultados mais relevantes para uma query do Atlas Search a partir de um grande corpus de produtos.
Nossos dados são obtidos a partir das APIs de dados de produtos em tempo real da Asos (um revendedor on-line de roupas, calçados, acessórios etc.). Utilizamos Apache Spark, GCP Storage (GSS) e MongoDB para armazenar e processar dados orquestrados pelo Apache Airflow.
Nosso pipeline de processamento de dados consiste em três partes principais:
Primeiro buscamos as categorias de produtos. Em seguida, obtemos os produtos em cada categoria e armazenamos esses dados no GCP Storage. O trecho do Python abaixo ilustra como buscamos os dados usando o RapidAPI e armazenamos os dados no GCS:
1 import json 2 import requests 3 4 url = "https://asos2.p.rapidapi.com/categories/list" 5 headers = { 6 "x-rapidapi-key": "YOUR_RAPIDAPI_KEY", 7 "x-rapidapi-host": "asos2.p.rapidapi.com", 8 } 9 response = requests.request("GET", url, headers=headers) 10 categories = json.loads(response.text) 11 for category in categories:a 12 url = f"https://asos2.p.rapidapi.com/products/v2/list/{category['id']}" 13 response = requests.request("GET", url, headers=headers) 14 products = json.loads(response.text) 15 # Store products in GCS 16 bucket = os.environ.get('GS_BUCKET_NAME') 17 product_filename = f"products_{datetime.now().strftime('%H%m%S')}.json" 18 write(bucket, str(date.today()) + "/" + product_filename, products)
A próxima etapa é o pré-processamento. Como os nomes dos produtos geralmente não são limpos, tokenizamos os dados e removemos as palavras vazias. Para isso, usamos o
RegexTokenizer
e StopWordsRemover
do SparkML.1 from pyspark.ml.feature import RegexTokenizer, StopWordsRemover 2 3 # Instantiate and set input and output column parameters 4 regexTokenizer = RegexTokenizer( 5 inputCol="product_name", outputCol="words", pattern="\\W" 6 ) 7 remover = StopWordsRemover(inputCol="words", outputCol="filtered") 8 9 # Transform the data 10 regexTokenized = regexTokenizer.transform(df) 11 removedStopWords = remover.transform(regexTokenized)
A última etapa é armazenar os documentos transformados em uma coleção MongoDB. MongoDBO do document model, a capacidade de lidar com eficiência com grandes volumes de dados, a funcionalidade de texto completo do Atlas Search e a fácil integração com ferramentas de big data, como o PySpark, fazem dele uma opção adequada para nossa pipeline de processamento de dados. O seguinte trecho de código mostra como escrever dados no MongoDB:
1 import json 2 3 from pymongo import MongoClient 4 5 client = MongoClient( 6 "mongodb://<username>:<password>@cluster0.mongodb.net/test?retryWrites=true&w=majority" 7 ) 8 db = client.get_database("product_db") 9 products = db.products 10 11 # Fetch data from GCS 12 for product in gcs_data: 13 products.insert_one(product)
Nosso objetivo principal era encontrar os produtos mais semelhantes para uma determinada query do Atlas Search. Experimentamos várias metodologias:
TF-IDF, abreviação de Term Frequency – Inverse Document Frequency , é uma medida da importância de uma palavra para um documento em um corpus . É um método amplamente usado para codificar texto. Dada uma query de usuário, primeiro a codificamos usando TF-IDF e, em seguida, usamos a similaridade de cosseno para encontrar os produtos mais semelhantes em nosso catálogo.
1 from pyspark.ml.feature import IDF, HashingTF 2 3 hashingTF = HashingTF(inputCol="filtered", outputCol="rawFeatures", numFeatures=20) 4 featurizedData = hashingTF.transform(removedStopWords) 5 6 idf = IDF(inputCol="rawFeatures", outputCol="features") 7 idfModel = idf.fit(featurizedData) 8 rescaledData = idfModel.transform(featurizedData)
Palavra2Vec é uma família de arquiteturas de modelo para aprender incorporações de palavras a partir de grandes conjuntos de dados. Aqui, usamos a implementação do Word2Vec no SparkML para incorporar a consulta do usuário e, em seguida, usamos a similaridade do cosseno para encontrar os produtos mais semelhantes em nosso catálogo.
1 from pyspark.ml.feature import Word2Vec 2 3 word2Vec = Word2Vec( 4 vectorSize=300, minCount=0, inputCol="filtered", outputCol="features" 5 ) 6 model = word2Vec.fit(removedStopWords) 7 result = model.transform(removedStopWords)
BM25, abreviação de "Best Match 25, ", é uma função de classificação usada em sistemas de recuperação de informações para classificar documentos com base em sua relevância para uma determinada query do Atlas Search. É um modelo de classificação probabilístico e é conhecido por sua capacidade de equilibrar a frequência de termo (TF) e a frequência inversa de documentos (IDF), tornando-o uma versão mais sofisticada do TF-IDF.
Em nosso projeto, implementamos o BM25 do zero no PySpark devido à falta de uma implementação direta na biblioteca. Aqui está uma visão mais detalhada de nossa abordagem e código:
Primeiro, calculamos os valores de IDF para cada termo, que é uma medida da quantidade de informações que a palavra fornece, ou seja, se é comum ou rara em todos os documentos.
Em segundo lugar, calculamos a frequência do termo em cada documento. A frequência do termo é simplesmente o número de vezes que uma palavra aparece em um documento.
Com essas duas métricas, poderíamos calcular a pontuação BM25 para cada documento em relação a uma query. Abaixo está um pseudocódigo simplificado para o processo:
1 # Compute IDF values for each term 2 # Compute term frequency in each document for each term 3 # For each query term, do: 4 # For each document containing the term do: 5 # Compute BM25 Score 6 # Sort documents by score
Para aumentar a eficiência de tempo do cálculo de TF e IDF todas as vezes para um produto, criamos um índice invertido. Esse índice invertido salva esses valores importantes antecipadamente para melhorar o tempo de recuperação quando alguém pesquisa um produto.
A primeira parte do pré-processamento envolve a derivação, que é o processo de reduzir palavras flexionadas (ou às vezes derivadas) à sua forma de derivação, base ou raiz. É feito para chegar à parte básica de uma palavra que carrega o significado. Por exemplo, a palavra matriz de “running” é “run.. Neste trabalho, usamos o algoritmo dePorter Stemming, um algoritmo de derivação popular e robusto.
1 def stemming(words): 2 ps = PorterStemmer() 3 result = [] 4 5 for w in words: 6 root_word = ps.stem(w) 7 result.append(root_word) 8 9 return result 10 11 stemming_udf = udf(lambda x: stemming(x), ArrayType(elementType=StringType())) 12 preprocessed_data = preprocessed_data.withColumn( 13 "tokens", stemming_udf("filtered_words") 14 )
Em seguida, nivelamos a lista de tokens em palavras individuais e contamos o número total de palavras únicas e palavras totais.
1 words = preprocessed_data.select(explode("tokens").alias("word")) 2 total_unique_words = words.select("word").distinct().count() 3 total_words = words.count()
Quando tivermos uma lista de palavras, começaremos a construir o índice invertido. Para cada palavra em nosso conjunto de dados, criamos uma entrada em nosso índice, onde cada entrada aponta para uma lista de documentos que contêm a palavra, juntamente com a frequência da palavra em cada documento.
1 id_str = concat_ws("", col("_id").cast(StringType())) 2 3 word_count_df = ( 4 preprocessed_data.select("_id", explode("tokens").alias("word")) 5 .groupBy("word", "_id") 6 .agg(size(collect_list("word")).alias("occurrence")) 7 .groupBy("word") 8 .agg( 9 map_from_entries(collect_list(struct(id_str.alias("id"), "occurrence"))).alias( 10 "documents" 11 ) 12 ) 13 .withColumnRenamed("word", "_id") 14 ).rdd.map(lambda row: (row["_id"], row["documents"])) 15 16 inverted_index = word_count_df.collectAsMap()
Por fim, também calculamos o comprimento de todos os documentos e a contagem exclusiva de palavras para cada documento. Esses valores são usados posteriormente na fórmula BM25 .
1 doc_length_df = ( 2 preprocessed_data.select("_id", size("tokens").alias("length")) 3 .groupBy("_id") 4 .agg(sum(col("length")).alias("doc_length")) 5 .withColumn("_id", concat_ws("", col("_id").cast(StringType()))) 6 .rdd.map(lambda row: (row["_id"], row["doc_length"])) 7 ) 8 doc_length = doc_length_df.collectAsMap() 9 10 doc_length["word_counter"] = total_unique_words 11 doc_length["doc_counter"] = total_words 12 13 unique_word_count_df = preprocessed_data.select( 14 "_id", 15 size(array_distinct("tokens")).alias("num_unique_words"), 16 ).withColumn("_id", concat_ws("", col("_id").cast(StringType()))).rdd.map(lambda row: (row["_id"], row["num_unique_words"])) 17 unique_word = unique_word_count_df.collectAsMap()
Isso conclui o pré-processamento e a etapa de construção de índice invertido. O índice invertido é um componente essencial da função de classificação do BM25 , pois permite a recuperação rápida de documentos relevantes para uma determinada query.
Agora, vamos mergulhar de cabeça na implementação do algoritmo BM25 .
A fórmula matemática para BM25 é a seguinte:
Onde:
- f(q_i, D): frequência do termo de query q_i no documento D
- |D|: contagem de palavras do documento D
- avgdl: contagem média de palavras em todos os documentos
- k_1 e b: Parâmetros de ajuste, geralmente definidos como k_1 em [1.2, 2.0] e b=0.75
- IDF(q_i): peso do termo de query q_i com base em sua raiz em todos os documentos
Veja como implementamos o algoritmo acima no Python:
1 def bm25(doc_length, 2 inverted_index, 3 unique_word, 4 word_counter, 5 doc_counter, 6 query): 7 k1 = 1.2 8 b = 0.75 9 k2 = 100 10 N = len(doc_length) - 2 11 avgdl = doc_length["doc_counter"] / N 12 13 score = defaultdict(int) 14 for word in query: 15 if word in inverted_index: 16 n = len(inverted_index[word]) 17 idf = math.log((N - n + 0.5) / (n + 0.5)) 18 for doc, freq in inverted_index[word].items(): 19 f = freq 20 qf = query.count(word) 21 dl = doc_length[doc] 22 K = k1 * ((1 - b) + b * (float(dl) / float(avgdl))) 23 R1 = idf * ((f * (k1 + 1)) / (f + K)) 24 R2 = (qf * (k2 + 1)) / (qf + k2) 25 R = R1 * R2 26 score[doc] += R 27 else: 28 Continue 29 30 return score
Essa função usa o dicionário de comprimento do documento, o índice invertido, o dicionário de contagem exclusiva de palavras, o contador total de palavras, o contador de documentos e a query do Atlas Search. Ele calcula a pontuação BM25 para cada documento em relação à query e retorna um dicionário em que as chaves são os IDs do documento e os valores são as pontuações BM25 correspondentes. Os documentos com as pontuações BM25 mais altas são os mais relevantes para a query. Neste trabalho, escolhemos valores para k1, b e k2 com base em outros estudos existentes.
O BERT, abreviação de Bidirecional Encoder Representations from Transformers, foi um dos primeiros exemplos de modelos de linguagem grandes (LLMs) e usa uma arquitetura de codificador de transformação para representar texto como vetores. Usamos um modelo BERT pré-treinado para mapear a query do usuário em um espaço vetorial de alta dimensão e, em seguida, usar a similaridade do cosseno para encontrar os produtos mais semelhantes em nosso catálogo.
1 from sentence_transformers import SentenceTransformer 2 3 model = SentenceTransformer('bert-base-nli-mean-tokens') 4 sentence_embeddings = model.encode(data['product_name'])
Cada um dos modelos tinha seus pontos fortes e fracos. Embora o TF-IDF seja o mais fácil de interpretar, ele tem dificuldade em capturar o significado semântica dos dados. A palavra2Vec estava limitada a lidar com queries presentes no corpus de treinamento. O BM25 foi muito rápido, mas faltava compreensão semântica. Entre os algoritmos que testamos, o melhor modelo foi o BERT, pois ele poderia incorporar o significado semântica dos dados e lidar com queries em vários idiomas.
Para nosso projeto, usamos uma instância de computação acelerada por CPU no Amazon Web Services. Abaixo estão os detalhes das especificações que usamos:
- Tipos de trabalhador/driver: g4dn.xlarge
- Número de trabalhadores: mínimo 2, máximo 5
- Recursos por trabalhador: 32–80 GB de memória, 8–20 Núcleos
- Recursos do driver: 16 GB de memória, 4 núcleos
Comparamos o desempenho dos quatro algoritmos do Atlas Search (BM25, TF-IDF, palavra2vec e BERT) em três queries distintas: "sneakers", "t-camiseta" e "chappals" (que significa sandlias/chinelas em Hindu). Os resultados variaram em termos de velocidade e relevância para cada combinação de query e algoritmo. Um resumo dos resultados é o seguinte:
- Query do Sneakers: o BM25 foi o mais rápido a retornar resultados relevantes. Tanto o TF-IDF quanto o Word2Vec foram mais lentos em comparação com o BM25, enquanto o BERT oferece um equilíbrio entre velocidade e relevância.
- Consulte25 Palavra2Vec teve um desempenho ruim em termos de velocidade e relevância, semelhante ao TF-IDF. O BERT foi moderadamente lento, mas forneceu resultados altamente relevantes.
- Querydo Appals : Appall é uma palavra em Hindu para sandalias de cabeda. Mais uma vez, BM25 se superou em termos de velocidade, mas forneceu resultados irrelevantes devido a limitações de linguagem no corpus. A palavra2Vec e o TF-IDF tiveram métricas de desempenho semelhantes às da query "t-camiseta". O BERT demonstrado novamente sua eficácia ao fornecer resultados altamente relevantes, mesmo com a complexidade de lidar com vários idiomas.
Os resultados do TF-IDF para a query "chappals " mostram a luta do algoritmo com queries de vários idiomas.
Os resultados do Word2Vec demonstram suas limitações ao lidar com diversos idiomas.
BM25 responde rapidamente, mas produz resultados irrelevantes devido a limitações de linguagem no corpus.
O BERT recupera com sucesso resultados relevantes, mostrando sua força no tratamento de queries em vários idiomas.
Neste artigo, comparamos quatro técnicas diferentes de processamento de linguagem natural (NLP) - BM25, TF-IDF, Word2Vec e BERT - para encontrar empiricamente a solução mais ideal para recuperar os resultados mais relevantes para um Query do Atlas Search de um grande corpus de produtos.
Nossos experimentos primários mostram que o algoritmo BM25 é o mais eficiente em termos de tempo, enquanto o BERT fornece a melhor compensação entre velocidade e relevância. Essa observação está alinhada com o que vemos na prática no mundo real, com o BM25 sendo um algoritmo popular para a Atlas Search lexical, e a Atlas Search vetorial sendo a escolha preferida para a Atlas Search semântica. A Atlas Search híbrida, uma técnica que combina os pontos fortes da palavra-chave e da semântica Atlas Search, está se tornando cada vez mais comum.
O MongoDB Atlas oferece suporte ao Atlas Search lexical, vetorial e híbrida. Veja alguns recursos para começar:
Principais comentários nos fóruns
Ainda não há comentários sobre este artigo.