Avaliando a qualidade do modelo de aprendizado de máquina mais simples do mundo
Acurácia, precisão, revocação e F1
Há alguns textos atrás, escrevemos sobre o modelo de aprendizado de máquina mais simples do mundo. Encerramos o texto questionando o leitor sobre a maneira que utilizamos para avaliar o desempenho do modelo; que, no texto, era bem ingenua.
Nesse texto, vamos aprofundar um pouco sobre algumas abordagens que são amplamente utilizadas para avaliar a qualidade de modelos de aprendizado de máquina.
Todos os modelos são errados, mas alguns são úteis
Algoritmos de aprendizado de máquina implementam mecanismos de aleatoriedade em diferentes etapas do processo de aprendizado, como na inicialização de parâmetros, na amostragem de dados e na divisão de conjuntos de treinamento e teste.
Além disso, a qualidade da predição do modelo está também intimamente relacionada a qualidade dos dados utilizados no processo de treino. Se estes modelos forem treinados com dados insuficientes, não representativos, com ruído, etc, é possível que a qualidade da sua predição fique comprometida.
Embora existam técnicas para para minimizar alguns desses riscos, permitindo que os resultados tenham bom desempenho e sejam reproduzíveis e consistentes, seria ingênuo esperar que um modelo faça sempre predições corretas. Dessa forma:
aplicações de software que fazem uso de modelos de aprendizado de máquina precisam ser desenvolvidas de forma a tolerar um certo grau de resultados incorretos.
Como o estatístico George Box indicou, não faz sentido perguntar se o modelo é verdadeiro, pois ele nunca será; devemos perguntar se o modelo é bom o suficiente; “All models are wrong, but some are useful”.
Métricas para avaliação de qualidade de modelos
Quando avaliamos a qualidade modelos de aprendizado de maquinas, estamos interessados em saber se estes modelos se adequam a um determinado problema. O grau de adequação varia de problema para problema, e é um assunto extenso e subjetivo, que abordaremos em outro momento.
Por hora, focaremos na pergunta: como avaliar a qualidade de um modelo?
Acurácia
A acurácia talvez seja a técnica mais simples de se avaliar a qualidade de um modelo.
Para um determinado conjunto de dados, a acurácia mede o percentual de predições que corresponde ao factual número de respostas esperadas.
Mais objetivamente, a acurácia pode ser calculada usando a seguinte formula:
Um dos principais benefícios da acurácia é que ela é fácil de interpretar.
No entanto, é importante entender algumas de suas limitações.
A primeira é que precisamos saber como calcular a variável total_de_predicoes_corretas
, ou seja, precisamos conhecer quais são as respostas corretas. No entanto, só teríamos como saber a resposta correta se tivessemos uma especie de gabarito de resposta. Então, poderíamos comparar se a resposta do modelo é igual a resposta do gabarito.
Em aprendizado de máquina, este gabarito é chamado de rótulo.
Em particular, os algoritmos de aprendizado de máquina que utilizam rótulos são chamados de algoritmos supervisionados. Algoritmos não supervisionados, por sua vez, não utilizam rótulos no processo de aprendizado.
Suponha que queremos criar um modelo que diga se um código tem ou não um bug. Usando um algorimto supervisionado, precisaríamos ter rótulos para indicar quando o código tem um bug.
No processo de treino, os pares de entrada e saída (o código que queremos avaliar se tem bug, e o rótulo, respectivamente) ajudam o algoritmo a aprender a mapear os padrões presentes nos dados para que, em seguida, faça previsões mais precisas para exemplos não rotulados.
Vamos considerar que nosso objetivo agora é saber se um código está ou não está correto (ou se tem ou não um bug).
Para fins de simplificação, poderíamos considerar que um código correto tem, no máximo, 999 linhas de código. Ou seja, todo código com 1000 linhas de código ou mais, não estaria correto, segundo nossa heurística de classifição.
O código a seguir exemplifica nossos dados e nossos rótulos.
# Dados de entrada (quantidade de linhas de código)
dados = [[1000],
[500],
[2000],
[1500],
[300],
[1200],
[800],
[2500],
[600],
[1800]]
# Rótulos correspondentes (0 = Está correto, 1 = Não está correto)
rotulos = [1, 0, 1, 1, 0, 1, 0, 1, 0, 1]
Primeiro, fornecemos nossos dados
e os rótulos
para cada item do dado.
Sem seguida, precisamos treinar nosso algoritmo para que ele possa fazer esse tipo de classificação. Como estamos usando variáveis que representam categorias, ou seja, variáveis categóricas, precisamos usar um algoritmo de classificação, supervisionado, que opere com variáveis categóricas.
Existem vários algoritmos com essas características, mas por hoje utilizaremos uma árvore de decisão, que, resumidamente, divide recursivamente os dados com base em suas características (variáveis categóricas) até que cada subconjunto de dados pertença a uma única classe. Há uma implementação de árvore de decisão disponível na biblioteca sklearn, na classe DecisionTreeClassifier
.
from sklearn.tree import DecisionTreeClassifier
# Divisão dos dados em conjunto de treinamento e conjunto de teste
dados_train = dados[:8] # Primeiros 8 exemplos para treinamento
rotulos_train = rotulos[:8]
dados_test = dados[8:] # Últimos 2 exemplos para teste
rotulos_test = rotulos[8:]
# Criando o modelo de Árvore de Decisão
modelo = DecisionTreeClassifier()
# Treinando o modelo com os dados de treinamento
modelo.fit(dados_train, rotulos_train)
# Fazendo previsões com o conjunto de teste
rotulos_pred = modelo.predict(dados_test)
Inicialmente, dividimos nossos dados em 80% para treinamento e 20% para testes.
Depois de criar e treinar o modelo (usando ao função fit()
), pedimos para que ele faça sua classificação para o conjunto de teste (usando a função predict()
), dados estes desconhecidos do modelo. O retorno da função predict()
é a classificação das variáveis armazenadas em dados_test.
Por fim, basta apenas comparar se a classificação do algoritmo é a mesma da classificação que já havíamos rotulado anteriormente.
def acuracia(rotulos_test, rotulos_pred):
num_previsoes_corretas = 0
for i in range(len(rotulos_test)):
if rotulos_test[i] == rotulos_pred[i]:
num_previsoes_corretas += 1
# Calcular a acurácia
acuracia = num_previsoes_corretas / len(rotulos_test)
print("Acurácia:", acuracia)
acuracia(rotulos_pred, rotulos_test)
Neste exemplo, o algoritmo desempenhou muito bem, retornando a saída Acurácia: 1.0
; ou seja, acertou todos os casos.
Perceba que, para calcularmos a acurácia, precisamos obrigatoriamente ter os rótulos dos dados. Isso implica em duas limitações:
não podemos usar a acurácia para algoritmos não-supervisionados, como já discutido anteriormente, mas também
não podemos usá-la para dados desconhecidos, que não estão em nosso conjunto de dados, uma vez que não temos os rótulos verdadeiros para comparar com as previsões do modelo.
Além disso, a acurácia conta ainda com duas outras limitações importantes:
Um outro problema da acurácia é quando os dados estão desbalanceados, ou seja, quando uma classe tem uma presença muito maior do que a outra. Nesses casos, um modelo que preveja a classe majoritária pode ter uma alta acurácia, mas não será eficaz na identificação dos exemplos da classe minoritária. Isso pode levar a uma falsa impressão de bom desempenho.
A acurácia não leva em consideração os diferentes tipos de erros. Por exemplo, e se tivéssemos uma classe com mais de 1000 linhas de código, mas que estivesse correto (falso positivo), ou uma classe com menos de 1000 linhas de código, mas que não estivesse correto (falso negativo). A acurácia não faz essa distinção e trata todos os erros da mesma maneira.
Precisão e revocação
Em problemas de classificação, em particular, classificação binária (como no exemplo que usamos para identificar código está ou não correto), há outras métricas que podem ser utilizadas, que são particularmente úteis para lidar com falsos positivos e falsos negativos.
Vamos discutir esses tipos de erro mais pra frente, mas, por hora, vamos apenas adicionar um novo item nos dados (e observar a manifestação desses erros):
# Dados de entrada (quantidade de linhas de código)
dados = [[1000],
[500],
[2000],
[1500],
[300],
[1200],
[800],
[2500],
[600],
[1800],
[999]]
# Rótulos de classe correspondentes (0 = Está correto, 1 = Não está correto)
rotulos = [1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0]
No caso, adicionamos o item [999]
na variável dados
, e o rótulo 0
, indicando que esse item está ou não correto. Se rodarmos novamente nosso algoritmo, teríamos uma saída diferente:
Acurácia: 0.6666666666666666
Nossa acurácia diminuiu pois o modelo errou a última previsão, e chutou [999]
como incorreto (quando na verdade está correto).
Ou seja, o modelo construiu o seguinte entendimento:
Rótulos verdadeiros: [0, 1, 0]
Previsões do modelo: [0, 1, 1]
Esse é um exemplo de um falso negativo, pois o modelo classificou um item que está correto (verdadeiro positivo) como incorreto. É tipo um alarme perdido, que deveria tocar mas não tocou?
Por outro lado, falsos positivos ocorrem quando o modelo classifica incorretamente uma um item incorreto como correto. Esse parece um alarme falso, que tocou, mas não deveria ter tocado?
Tanto falsos positivos quanto falsos negativos são importantes mas, dependendo do contexto, eles podem ter pesos diferentes.
Considere por um instante que você trabalha para uma instituição bancária e, como parte do seu trabalho, você está desenvolvendo um linter para avaliação de código seguro. O que seria mais importante pra o seu linter?
Reportar todos as possíveis brechas de segurança, mesmo que algumas delas não sejam de fato confirmadas. Nesse caso, toleraríamos falsos negativos pois sabemos que uma brecha de segurança é algo muito importante.
Reportar somente as brechas de segurança que sejam 100% confirmadas, ao custo de não reportar algumas potenciais brechas de segurança não confirmadas. Neste outro caso, não toleraríamos falsos negativos pois estes poderiam diminuir a confiança do time com a ferramenta.
Outros cenários podem ter prioridades diferentes. Considere, por exemplo, um modelo que tenta prever se um paciente tem câncer. É melhor optimiza-lo pensando em falsos negativos ou falsos positivos?
Sabendo da existência, e da diferença entre, falsos positivos e negativos, podemos calcular duas métricas: precisão e revocação.
A precisão avalia a taxa das previsões positivas do modelo, focando em minimizar os falsos positivos. É uma métrica relevante quando o objetivo é evitar classificar incorretamente instâncias negativas como positivas, garantindo que as previsões positivas sejam confiáveis. Precisão se calcula da seguinte forma:
Por outro lado, a revocação mede a capacidade do modelo de capturar corretamente os exemplos positivos, evitando falsos negativos. É uma métrica importante quando o foco está na minimização de falsos negativos e na garantia de que as instâncias positivas sejam adequadamente identificadas pelo modelo. Revocação se calcula da seguinte forma:
Voltando ao nosso exemplo do começo do texto, podemos calcular a precisão e revocação do nosso modelo da seguinte forma:
Rótulos verdadeiros: [0, 1, 0]
Previsões do modelo: [0, 1, 1]
Precisão = VP / (VP + FP)
VP = 2 (casos em que o modelo previu corretamente a presença de um bug)
FP = 1 (caso em que o modelo previu incorretamente a presença de um bug)
Precisão = 2 / (2 + 1) = 2 / 3 ≈ 0.666 (ou 66.6%)
Revocação = VP / (VP + FN)
VP = 2 (casos em que o modelo previu corretamente a presença de um bug)
FN = 0 (caso em que o modelo previu incorretamente a ausência de um bug)
Revocação = 2 / (2 + 0) = 10 / 10 ≈ 1.00 (ou 100%)
F1
Embora a precisão e a revocação sejam métricas úteis para avaliar o desempenho de um modelo de classificação, elas se concentram em aspectos diferentes da classificação e podem fornecer informações complementares. Em resumo:
A precisão é importante quando se deseja minimizar os falsos positivos, ou seja, quando é crucial evitar classificar erroneamente exemplos negativos como positivos.
A revocação é importante quando se deseja minimizar os falsos negativos, ou seja, quando é crucial evitar que exemplos positivos sejam erroneamente classificados como negativos.
Mas, e quando precisamos minimizar tanto os falsos positivos quanto os falsos negativos?
Por exemplo, considere que você trabalha numa instituição bancária, e está trabalhando na detecção de fraudes de cartão de crédito. Nesse contexto, é desejável minimizar os falsos positivos, ou seja, evitar que transações legítimas sejam erroneamente classificadas como fraudulentas. Uma alta precisão significa que a maioria das transações identificadas como fraudulentas realmente são fraudes, reduzindo os inconvenientes para os usuários legítimos.
Por outro lado, a revocação é crucial para minimizar os falsos negativos, ou seja, garantir que a maioria das transações fraudulentas seja detectada. Uma alta revocação significa que a maioria das transações fraudulentas é corretamente identificada, evitando perdas financeiras significativas para os usuários e para a instituição financeira.
Quando o objetivo é levar otimizar tanto precisão e revocação, se utiliza a métrica F1, que calcula uma medida geral do desempenho do modelo, considerando tanto a precisão quanto a revocação. A fórmula para o cálculo do F1 é:
O F1 varia de 0 a 1, sendo 1 o melhor desempenho possível.
Nosso calculo de F1 seria o seguinte:
Precisão = 0.66
Revocação = 1.0
F1 = 2 * (P * R) / (P + R)
F1 = 2 * (0.66 * 1.0) / (0.66 + 1.0)
F1 = 0.749
Saber das limitações da acurácia, e entender como precisão e revocação podem ser utilizados (e priorizados) já é um bom avanço para que possamos criar melhores formas de avaliar nossos modelos. Há ainda diversas outras métricas para o leitor curioso se aprofundar, cada uma, porém com suas limitações e compensações.
Ademais, algoritmos mais modernos, como aqueles que popularizaram as técnicas de aprendizado profundo demandam de novas estratégias de avaliação de qualidade, como a BLEU, que compara o quão próximo uma tradução, por exemplo, está do texto que era esperado.
Por fim, vale ressaltar que não podemos esperar que nosso modelo seja correto, no sentido de que uma acurácia de 99% indique uma falha no modelo. Todavida, a performance dos modelos devem ser avaliadas, também, levando em consideração o contexto do problema que o modelo tenta ajudar a resolver. Por exemplo, uma acurácia de 95% pode ser excepcionalmente boa para alguns problemas, mas terrivel para outros problemas.
A performance ideal do modelo depende exclusivamente do problema proposto.
O código apresentado nesse texto está disponível neste link, licenciado sob MIT.