XGBoost é uma ferramenta essencial para muitas aplicações Learning to Rank, desde a previsão de taxas de cliques até o aprimoramento de sistemas de recomendação.

Usei muito ele quando era responsável pelo sistema de ranking de freelancers na Upwork.

Neste tutorial, vamos explorar o potencial do XGBoost para suas tarefas de LTR.

Vamos explorar várias funções objetivo, passos da preparação de dados e ilustrar como treinar seu modelo.

Ao final deste guia, você estará totalmente equipado para construir seus próprios modelos LTR usando XGBoost.

Então, vamos começar!

Funções Objetivo de Learning to Rank

Otimizar uma pontuação de ranking pode ser tão simples quanto tratar cada par consulta-documento como um problema de classificação ou tão complexo quanto otimizar uma função objetivo que leva em conta todos os candidatos para uma consulta.

Existem vários tipos de funções objetivo, cada uma com seus pontos fortes e fracos.

Vamos dar uma olhada em algumas delas:

Nota: Estou usando o termo “documento” para me referir aos itens que estão sendo classificados, mas pode ser qualquer coisa, desde páginas da web até produtos.

Uma “consulta” (query) pode ser uma busca ou o histórico de navegação de um usuário, por exemplo.

Pontual (Pointwise)

Esta abordagem trata cada par consulta-documento independentemente.

Você só precisa rotular cada par consulta-documento com uma pontuação de relevância e treinar um modelo para prever essa pontuação.

Por exemplo, digamos que temos um conjunto de dados de pares consulta-documento onde cada par é rotulado com uma pontuação de relevância de 1 a 5.

Podemos treinar um modelo de regressão para prever a pontuação de relevância para cada par.

Apesar de sua simplicidade, a abordagem pontual é surpreendentemente eficaz e difícil de superar, então é um bom ponto de partida.

No XGBoost, essa abordagem pode ser implementada usando qualquer uma das funções objetivo padrão de regressão ou classificação.

Lembre-se de fazer ajustes para quaisquer desequilíbrios de rótulos em seu conjunto de dados.

Pareada (Pairwise)

A abordagem pairwise considera pares de documentos e tenta minimizar o número de pares ordenados incorretamente. É usada em algoritmos como RankNet.

Por exemplo, considerando nosso conjunto de dados anterior, essa abordagem pegaria uma consulta e dois documentos de cada vez e ajustaria as pontuações de relevância previstas para que o documento mais relevante tenha uma pontuação mais alta do que o menos relevante.

É diferente da abordagem pontual, pois na primeira consideramos apenas a consulta e um único documento de cada vez.

Aqui estamos tentando modelar explicitamente a ordem relativa dos documentos para uma consulta.

Pode parecer confuso falar de pares de documentos após discutir a abordagem pontual, mas pense no seguinte ao imaginar as instâncias de treinamento:

pares_pontuais = [(consulta, documento1), (consulta, documento2), (consulta, documento3)]
pares_pareados = [(consulta, documento1, documento2), (consulta, documento2, documento3), (consulta, documento1, documento3)]

Como já podemos perceber acima, esta abordagem exigirá mais tempo de treinamento, pois estamos considerando mais instâncias ao criar pares de documentos em vez de considerar apenas um documento por vez.

O XGBoost fornece algumas funções objetivo para essa abordagem:

rank:pairwise

Também conhecido como LambdaMART, porque combina MART (Multiple Additive Regression Trees) e LambdaRank, esta é a versão original da função de perda pairwise (também conhecida como RankNet).

Mais detalhes sobre como funciona podem ser encontrados na documentação original.

rank:ndcg

NDCG significa Normalized Discounted Cumulative Gain (Ganho Cumulativo Descontado Normalizado).

É uma das medidas mais populares de qualidade de ranking usadas na indústria, pois leva em conta a ordem relativa dos documentos para uma consulta e a pontuação de relevância de cada documento.

Esta função objetivo usa um gradiente substituto derivado da métrica NDCG para otimizar o modelo.

É útil se você estiver usando NDCG como a métrica de avaliação, pois é uma otimização direta dela.

rank:map

MAP significa Mean Average Precision.

É uma medida mais simples de qualidade de ranking que é usada quando as pontuações de relevância são binárias (0 ou 1).

Em geral, se você estiver usando MAP como a métrica de avaliação, deve usar esta função objetivo.

Listwise

Embora não implementada no XGBoost, vale a pena mencionar a abordagem listwise por sua importância.

Ela considera toda a lista de documentos para uma consulta e tenta otimizar a ordem da lista inteira de uma vez.

É uma evolução das abordagens pontual e pairwise, e pode levar a melhores resultados porque leva em conta a ordem relativa de todos os documentos.

Um exemplo de uma função de perda listwise é o ListMLE.

Vamos ver um exemplo de como usar o XGBoost para Learning to Rank em Python.

Preparando os Dados

Nesta seção, estamos preparando nossos dados para a tarefa de Learning to Rank.

Estamos usando o conjunto de dados MSLR-WEB10K da Microsoft, que é um conjunto de dados de benchmark comumente usado na comunidade de Learning to Rank.

Ele tem pares de consultas e documentos, onde cada par é rotulado com uma pontuação de relevância de 0 a 4, do menos ao mais relevante.

Este é um conjunto de dados do mundo real que foi usado pela Microsoft para treinar seu mecanismo de busca (Bing).

Primeiro, importamos as bibliotecas necessárias: pandas para manipulação de dados, NumPy para operações numéricas e load_svmlight_file de sklearn.datasets para carregar nosso conjunto de dados.

O formato SVMLight é um formato baseado em texto para armazenar conjuntos de dados esparsos.

É comumente usado quando você tem muitas features e a maioria delas são zeros (esparsas), que é o caso para previsão de taxa de cliques e outras tarefas similares.

import pandas as pd
import numpy as np
from sklearn.datasets import load_svmlight_file

Em seguida, carregamos nossos conjuntos de dados de treinamento e validação.

A função load_svmlight_file retorna uma tupla contendo a matriz de features, o vetor alvo e os IDs de consulta.

Os IDs de consulta são usados para agrupar as instâncias por consulta.

Digamos que você pesquisou por “como usar XGBoost para ranking” em um buscador.

Todas as páginas que o buscador retorna para esta consulta seriam agrupadas juntas.

train = load_svmlight_file(str(data_path / 'vali.txt'), query_id=True)
valid = load_svmlight_file(str(data_path / 'test.txt'), query_id=True)
0 qid:1 1:3 2:0 3:2 4:2 … 135:0 136:0
2 qid:1 1:3 2:3 3:0 4:0 … 135:0 136:0

Exemplos extraídos do site original do MSLR.

Se você prestar atenção aos arquivos carregados, notará que estou usando os conjuntos originais de validação e teste de uma única divisão do MSLR.

Isso é apenas porque eles têm menos linhas e são mais fáceis de trabalhar para uma demonstração.

Todos os arquivos contêm o mesmo formato, então você pode usar qualquer um deles.

Em seguida, descompactamos as tuplas em variáveis separadas para uma manipulação mais fácil.

X_train, y_train, qid_train = train
X_valid, y_valid, qid_valid = valid

X_train e X_valid são as matrizes de features, y_train e y_valid são os vetores alvo (relevância), e qid_train e qid_valid são arrays indicando a consulta para cada instância.

Se tivermos 10 resultados para uma consulta, haverá 10 instâncias com o mesmo ID de consulta, mas diferentes vetores de features e pontuações de relevância.

No contexto do XGBoost, quando você vê o atributo group, pense nele como os IDs de consulta (qid_train e qid_valid).

Ele é usado para indicar quais instâncias pertencem à mesma consulta.

Originalmente, precisávamos especificar quantas instâncias pertencem a cada consulta, mas agora o XGBoost tem uma interface mais amigável onde podemos especificar as consultas às quais cada instância pertence diretamente como um array NumPy.

Treinamento do modelo XGBRanker

Agora que nossos dados estão preparados, podemos passar para o treinamento do nosso modelo.

Usaremos a classe XGBRanker da biblioteca XGBoost, que é especificamente projetada para tarefas de Learning to Rank.

Primeiro, importamos a biblioteca XGBoost.

import xgboost as xgb

Em seguida, criamos uma instância da classe XGBRanker.

Especificamos o tree_method como "hist" e o objective como "rank:ndcg".

O método "hist" é um método rápido baseado em histograma para construir as árvores de decisão, e "rank:ndcg" é a função objetivo que discutimos anteriormente.

Você pode usar tree_method="gpu_hist" se tiver uma GPU e quiser treinar seu modelo mais rápido.

model = xgb.XGBRanker(tree_method="hist", objective="rank:ndcg")

Finalmente, treinamos o modelo com nossos dados de treinamento.

Passamos nossa matriz de features (X_train), nosso vetor alvo (y_train) e nossos IDs de consulta (qid_train).

Os IDs de consulta são usados para agrupar as instâncias por consulta, o que é crucial para tarefas de Learning to Rank.

model.fit(X_train, y_train, qid=qid_train)

A única diferença entre esta e uma tarefa regular de classificação ou regressão é que precisamos passar os IDs de consulta para o método fit, pois ele agrupará as instâncias para otimizar as funções objetivo pairwise.

Vale notar que existe um recurso experimental no XGBoost para lidar com viés de posição, chamado lambdarank_unbiased.

Viés de posição é o nome dado ao fenômeno onde os primeiros resultados são mais propensos a serem clicados do que o resto, mesmo que não sejam os mais relevantes.

Eu nunca usei esse recurso experimental, mas estou mencionando aqui caso você queira experimentá-lo.

Prevendo Pontuações de Ranking

Uma vez que nosso modelo estiver treinado, podemos usá-lo para fazer previsões.

No contexto de Learning to Rank, normalmente queremos classificar os documentos para uma única consulta de cada vez.

Para fazer isso, podemos usar o método predict do nosso modelo e passar a matriz de features para uma única consulta.

Primeiro, digamos que queremos fazer previsões para a primeira consulta em nosso conjunto de validação.

Podemos obter a matriz de features para esta consulta assim:

X_query = X_valid[qid_valid == qids[0]]

X_query conterá todas as features para as linhas onde o ID da consulta é igual ao primeiro ID de consulta em nosso conjunto de validação.

Então, podemos usar o método predict para obter as pontuações de relevância previstas:

y_pred = model.predict(X_query)

O método predict retorna um array NumPy com as previsões.

Os documentos podem então ser classificados com base nessas pontuações. Quanto maior a pontuação, maior a relevância.

Cuidado! Como você pode ver, não há ID de consulta no método predict, então se você passar todo o conjunto de validação, ele retornará as pontuações de relevância previstas para todos os documentos de uma vez, pensando que eles pertencem à mesma consulta.

Avaliando o Desempenho do Ranking

Depois de fazer previsões, é importante avaliar o desempenho do nosso modelo.

Para tarefas de Learning to Rank, normalmente usamos medidas que levam em conta a relevância e a posição dos itens, como o Normalized Discounted Cumulative Gain (NDCG).

Como é uma das métricas offline mais populares na indústria, usarei o NDCG para avaliar o desempenho do nosso modelo.

Primeiro, vamos definir uma função para calculá-lo.

Peguei esta função emprestada do CatBoost.

Esta função recebe três argumentos: as pontuações previstas (y_score), as pontuações de relevância verdadeiras (y_true) e o número de documentos principais a serem considerados (k).

def ndcg(y_score, y_true, k):
    order = np.argsort(y_score)[::-1]
    y_true = np.take(y_true, order[:k])

    gain = 2 ** y_true - 1

    discounts = np.log2(np.arange(len(y_true)) + 2)
    return np.sum(gain / discounts)

Em seguida, obtemos os IDs de consulta únicos do nosso conjunto de validação.

qids = np.unique(qid_valid)

Então, fazemos um loop sobre cada ID de consulta, fazemos previsões para essa consulta e calculamos a pontuação NDCG para ela.

Adicionamos cada pontuação a uma lista.

ndcg_ = list()

for i, qid in enumerate(qids):
    y = y_valid[qid_valid == qid]
    
    if np.sum(y) == 0:
        continue
    
    p = model.predict(X_valid[qid_valid == qid])

    idcg = ndcg(y, y, k=10)
    ndcg_.append(ndcg(p, y, k=10) / idcg)

Finalmente, calculamos a pontuação NDCG média em todas as consultas.

Isso nos dá um único número que representa o desempenho geral do nosso modelo.

np.mean(ndcg_)

Agora você tem uma visão completa de como usar XGBoost para tarefas de Learning to Rank, desde a preparação dos dados até a avaliação do modelo.

Seja o primeiro a saber das novidades em Machine Learning. Me siga no LinkedIn.