Overfitting, underfitting e o trade-off viés-variância

Explorando as armadilhas em modelos de machine learning

Edson Junior
Data Hackers
13 min readAug 15, 2023

--

Photo by Caleb Jones on Unsplash

Imagine um caminho estreito onde de um lado está a simplificação excessiva e do outro, a adaptação excessiva. Navegar por esse caminho é essencial na construção de modelos de machine learning eficazes. Neste artigo, serão explorados os conceitos de underfitting, overfitting e o delicado trade-off viés-variância. Com uma abordagem prática, será esclarecido o que são estes esses fenômenos, como identificá-los e seus efeitos nos modelos desenvolvidos.

Suponha que seja construída uma relação cúbica entre a concentração de nutrientes por quilo de solo (mg/kg) e o crescimento de pés de algodão em centímetros.

Em Python, esta relação pode ser construída da seguinte maneira:

# BIBLIOTECAS
import numpy as np
import matplotlib.pyplot as plt

# Definindo a semente para o gerador de números aleatórios
np.random.seed(35)

# Eixo X: Geração de 100 valores aleatórios de uma distribuição uniforme
# entre 0 e 2
X = 2 * np.random.rand(100, 1)

# Eixo Y: relação cúbica com ruído
y = 4 * X**3 - 0.45 * X**2 + X -10 + np.random.randn(100, 1)
# Obs: O ruído "np.random.randn(100, 1)" é composto por 100 valores aleatórios
# de uma distribuição normal com média 0 e desvio padrão 1.

# AJUSTANDO O TAMANHO DA FIGURA
plt.figure(figsize = (10,6))

# SCATTER PLOT
plt.scatter(X, y, color = 'red')

# LABELS
plt.xlabel('Concentração de Nutriente (mg/kg)')
plt.ylabel('Crescimento do algodão (cm)')
plt.title('Crescimento do algodão em função de nutrientes', fontweight = 'bold')
plt.show()

Com este código acima é obtido o seguinte gráfico.

Fonte: Próprio autor

Com o gráfico em mãos e supondo que não se tenha conhecimento da natureza da relação entre as variáveis, é decidido gerar um modelo de machine learning que melhor se adapte aos dados disponíveis. Sendo assim, o primeiro passo a se tomar é a divisão dos dados em conjunto de treino (utilizados para treinar o modelo) e teste (utilizados para testar o modelo), conforme é apresentado a seguir:

# Importando a função que divide os dados em treino e teste
from sklearn.model_selection import train_test_split

# Dividindo o conjunto de dados em treinamento e teste
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)

Para esta análise, foi definido que o conjunto de teste equivale a 30% do dataset (test_size = 0.3) e o conjunto de treino corresponde a 70%.

Em seguida, é escolhido testar uma regressão linear para modelar os dados disponíveis. Para isso, o modelo é instanciado, treinado e são obtidas as previsões considerando o próprio conjunto de treino.

# Importando o modelo de regressão linear
from sklearn.linear_model import LinearRegression

# Instanciando o modelo de regressão linear
linear_model = LinearRegression()

# treinando o modelo
linear_model.fit(X_train, y_train)

# previsões do modelo aplicando nos dados de treino
y_train_pred = linear_model.predict(X_train)

Para a plotagem do gráfico, são exibidos os dados do conjunto de treino, os do conjunto de teste e a reta encontrada como melhor opção que se ajusta aos dados de treino.

# Aqui os valores de X_train são ordenados de forma crescente, 
# para o modelo predito ser uma linha contínua
idx_train = np.argsort(X_train, axis=0).ravel()

# PLOTAGEM DOS DADOS

plt.figure(figsize = (10,6))

# IDENTIFICANDO OS DADOS DE TREINO
plt.scatter(X_train, y_train, color='blue', label='Dados de Treino')

# IDENTIFICANDO OS DADOS DE TESTE
plt.scatter(X_test, y_test, color='green', label='Dados de Teste')

# IDENTIFICANDO O MODELO PREDITO
plt.plot(X_train[idx_train], y_train_pred[idx_train], color='red', label='Modelo Predito')
plt.xlabel('Concentração de Nutriente (mg/kg)')
plt.ylabel('Crescimento do algodão (cm)')
plt.title('Modelo Linear', fontweight = 'bold')
plt.legend()
plt.show()

Deste modo, obtém-se a visualização a seguir.

Fonte: Próprio autor

Observando o gráfico acima, é possível notar que trata-se de um exemplo fácil de Underfitting.

OK, mas o que é underfitting??

Underfitting

O conceito de underfitting está ligado a uma situação em que o modelo de machine learning não possui a habilidade de relacionar as variáveis X e Y de maneira eficaz, não consegue aprender muito bem o padrão dos dados de treinamento. Isso acontece quando o modelo é muito simples para capturar as relações complexas dos dados. Ou seja, no caso acima, a reta de regressão é muito simples em relação ao comportamento dos dados (polinômio de terceiro grau). Uma métrica capaz de ajudar a identificar o underfitting é o Erro Quadrático Médio, descrita a seguir.

Erro Quadrático Médio

O Erro Quadrático Médio (MSE, do inglês Mean Square Error) é uma métrica utilizada para avaliar modelos de regressão. Nela é realizada a média do quadrado das diferenças entre os valores previstos pelo modelo e os valores reais. Pode ser representada da seguinte forma:

Fonte: Próprio autor

Na qual:

  • n: número de instâncias;
  • y_i: valor real da i-ésima instância; e
  • ŷ_i: valor previsto pelo modelo para a i-ésima instância.

Ao calcular esta métrica, o objetivo é verificar o quão bem o modelo se ajusta aos dados, de acordo com a magnitude dos erros, fornecendo informação sobre sua capacidade de generalização.

Mas como fazer isto em Python?

# Importando a função que calcula o Erro quadrático médio
from sklearn.metrics import mean_squared_error

# previsões do modelo aplicando nos dados de treino
y_train_pred = linear_model.predict(X_train)

# previsões do modelo aplicando nos dados de teste
y_test_pred = linear_model.predict(X_test)

# MSE para os dados de treino
mean_squared_error(y_train, y_train_pred)

# MSE para os dados de teste
mean_squared_error(y_test, y_test_pred)

Para esta análise, o MSE tanto para os dados de treino quanto para os de teste resultaram em um valor relativamente alto:

Fonte: Próprio autor
Fonte: Próprio autor

Assim, o resultado alto obtido é um indicativo de underfitting, mostrando a incapacidade do modelo de generalizar, de se ajustar aos dados de treino. Uma outra forma de verificar o fenômeno de underfitting é através da curva de aprendizado do modelo, descrita a seguir.

Curva de aprendizado

Esta curva nada mais é do que um indicador que mostra como a performance do modelo se comporta à medida que o tamanho do conjunto de treino vai aumentando. Existe basicamente para nos ajudar a responder a seguinte pergunta: “Se eu utilizar mais dados de treinamento, o que acontece com o desempenho?” Assim, esta forma de avaliação também consegue informar sobre o quão boa é a capacidade de generalização do modelo.

No caso desta análise, a performance do modelo (indicada pelo eixo Y da curva de aprendizado) é medida pelo MSE e são plotados tanto o desempenho para o conjunto de treinamento quanto para o conjunto de teste.

Mas como fazer isto em Python?

# Importando a função que define a curva de aprendizado
from sklearn.model_selection import learning_curve
# Importando a função que calcula o Erro quadrático médio
from sklearn.metrics import mean_squared_error

# Aqui train_sizes são os percentuais do conjunto de treinamento em que serão
# avaliados o MSE para a curva de aprendizado
train_sizes = np.linspace(0.1, 1.0, 10)

# Obtendo os MSE para o conjunto de treino e teste
train_sizes, train_scores, test_scores = learning_curve(linear_model, X_train, y_train, train_sizes=train_sizes, cv=5, scoring='neg_mean_squared_error')

# Calculando a média dos scores de treinamento e teste
train_scores_mean = -np.mean(train_scores, axis=1)
test_scores_mean = -np.mean(test_scores, axis=1)

A função “learning_curve” retorna três arrays:

  • train_sizes: Contém os tamanhos do conjunto de treinamento (em valor numérico, não percentual) que foram usados para determinar o MSE tanto para o conjunto de treino quanto para o de teste. Equivale ao eixo X da curva de aprendizado. Neste caso, são utilizados 10 tamanhos diferentes;
Fonte: Próprio autor
  • train_scores e test_scores: São respectivamente os MSE do conjunto de treino e teste, obtidos para cada tamanho indicado em train_sizes. É necessário lembrar que, utilizando a função “learning_curve”, o conjunto de dados é dividido em K partes iguais (folds) e o modelo é treinado K vezes. Assim, são obtidas K métricas de avaliação para cada tamanho em train_sizes. Esta técnica é chamada de K-Fold Cross Validation e o valor de K é indicado no parâmetro “cv” da função “learning_curve”. Neste caso, foi utilizado cv = 5.
Fonte: Próprio autor

Como para cada tamanho do conjunto de treino tem-se 5 valores para a métrica MSE, são realizadas as médias destes 5 valores, obtendo:

  • train_scores_mean (MSE para o conjunto de treino)
Fonte: Próprio autor
  • test_scores_mean (MSE para o conjunto de teste)
Fonte: Próprio autor

Com estas informações é possível configurar o plot.

# Plotando a curva de aprendizado
plt.figure(figsize=(10, 6))
plt.plot(train_sizes, train_scores_mean, 'o-', color="r", label="Treinamento")
plt.plot(train_sizes, test_scores_mean, 'o-', color="g", label="Teste")
plt.xlabel('Tamanho do Conjunto de Treinamento')
plt.ylabel('MSE (Erro Médio Quadrático)')
plt.title('Curva de Aprendizado')
plt.legend(loc="best")
plt.show()

O que resulta na seguinte curva de aprendizado.

Fonte: Próprio autor

Pela Figura, analisando o MSE conforme o tamanho do conjunto de treinamento aumenta, percebe-se que tanto para os dados de treino quanto para os de teste, o MSE se estabiliza em um nível alto. Isso significa que o modelo não consegue se ajustar bem aos dados, um modelo simples demais, um forte indicativo de underfitting.

Como o modelo anterior era muito simples em relação aos dados de treinamento, é decidido utilizar um polinômio de grau 35 para modelar os dados disponíveis.

# Aqui é instanciada a classe PolynomialFeatures que ajustará 
# um modelo polinomial de grau 35 aos dados
poly = PolynomialFeatures(degree=35)

# A variável X_train é expandida para X_poly_train, que são potencias
# de X_train, do grau 0 até o grau 35
X_poly_train = poly.fit_transform(X_train)

# A variável X_test é expandida para X_poly_test, que são potencias
# de X_test, do grau 0 até o grau 35
X_poly_test = poly.transform(X_test)

# Instanciando o modelo de regressão linear
model = LinearRegression()

# Ajustando o modelo regressão linear para os dados expandidos de X_poly_train
model.fit(X_poly_train, y_train)

# Encontrando valores preditos para dados de treino e teste
y_train_pred = model.predict(X_poly_train)
y_test_pred = model.predict(X_poly_test)

Em seguida, é escrito o código para plotagem dos dados de treino e teste juntamente com o polinômio de grau 35.

# Aqui os valores de X_train são ordenados de forma crescente, 
# para garantir que o modelo predito ser uma linha contínua
idx_train = np.argsort(X_train, axis=0).ravel()

# PLOTAGEM DOS DADOS

plt.figure(figsize = (10,6))

# IDENTIFICANDO OS DADOS DE TREINO
plt.scatter(X_train, y_train, color='blue', label='Dados de Treino')

# IDENTIFICANDO OS DADOS DE TESTE
plt.scatter(X_test, y_test, color='green', label='Dados de Teste')

# IDENTIFICANDO O MODELO PREDITO
plt.plot(X_train[idx_train], y_train_pred[idx_train], color='red', label='Modelo Predito')
#plt.plot(X_train, y_train_pred, color='red', label='Modelo Predito')
plt.xlabel('Concentração de Nutriente (mg/kg)')
plt.ylabel('Crescimento do algodão (cm)')
plt.title('Polinômio de Grau 35', fontweight = 'bold')
plt.legend()
plt.show()
Fonte: Próprio autor

Observando o gráfico acima, é possível notar que trata-se de um exemplo fácil de Overfitting.

Ok, mas o que é Overfitting??

Overfitting

Ao contrário do que se caracteriza o underfitting, o conceito de overfitting está ligado a uma situação em que o modelo de machine learning se adequou tão excessivamente ao padrão dos dados de treinamento que não consegue generalizar para os dados de teste, para os dados não vistos. Isso significa que o desempenho do modelo para os dados de treino pode ser bom, mas para os dados de teste é muito abaixo, deixa a desejar. Em uma analogia com o ambiente acadêmico, é como se o aluno decorasse toda a lista de exercícios e fosse fazer a prova. Mas sendo a prova composta por exercícios não iguais aos da lista, o aluno não consegue implementar ou generalizar o seu raciocínio. Em machine learning, isso acontece quando o modelo é muito complexo em relação à complexidade dos dados de treinamento, ou até mesmo pelo fato do conjunto de treino ser muito pequeno.

Em Python, o MSE também foi calculado para os conjuntos de treino e teste:

# Importando a função que calcula o Erro quadrático médio
from sklearn.metrics import mean_squared_error

# Encontrando valores preditos para dados de treino e teste
y_train_pred = model.predict(X_poly_train)
y_test_pred = model.predict(X_poly_test)

# MSE para os dados de treino
mean_squared_error(y_train, y_train_pred)

# MSE para os dados de teste
mean_squared_error(y_test, y_test_pred)

Obtendo-se os seguintes resultados:

Fonte: Próprio autor
Fonte: Próprio autor

Pelos MSE obtidos, percebe-se que o modelo se ajusta muito bem para os dados de treino, mas nem tão bem para os dados de teste (ainda que o resultado obtido seja baixo), o que é um indicativo de overfitting.

Assim como foi avaliada a curva de aprendizado para o caso de underfitting, para situações em que houver overfitting este recurso também pode ser utilizado.

from sklearn.metrics import mean_squared_error

# Definindo diferentes tamanhos de conjunto de treinamento para a
# curva de aprendizado
train_sizes = np.linspace(0.1, 1.0, 10)

# Calculando a curva de aprendizado usando a função learning_curve
from sklearn.model_selection import learning_curve

train_sizes, train_scores, test_scores = learning_curve(model, X_poly_train, y_train, train_sizes=train_sizes, cv=5, scoring='neg_mean_squared_error')

# Calculando a média dos scores de treinamento e teste
train_scores_mean = -np.mean(train_scores, axis=1)
test_scores_mean = -np.mean(test_scores, axis=1)

Da mesma forma que no caso de underfitting, obtemos:

  • train_scores_mean (MSE para o conjunto de treino)
Fonte: Próprio autor

Pelo resultado acima, os valores de MSE para os dados de treino apresentam uma dimensão excessivamente reduzida, que sugere um cenário de overfitting aos dados de treinamento.

  • test_scores_mean (MSE para o conjunto de teste)
Fonte: Próprio autor

Percebe-se que os valores de MSE para o conjunto de teste demonstram um nível de magnitude excessivamente elevado, muito maiores que os de treino, e que seu valor vem a diminuir com o aumento do tamanho contidos em train_sizes. Em outras palavras, à medida que mais dados de treinamento são utilizados para treinar o modelo, a qualidade das previsões do modelo melhora, resultando em um MSE de teste menor.

Neste caso, para a construção da curva de aprendizado foi utilizado um gráfico com dois eixos y, devido a grande diferença de escala dos MSE para os conjuntos de treino e teste.

# Plotando a curva de aprendizado


# Criando a figura e o primeiro eixo y
fig, ax1 = plt.subplots(figsize=(10, 6))

# Plotando a primeira série de dados no primeiro eixo y
ax1.plot(train_sizes, train_scores_mean, 'o-', color="r", label="Treinamento")
ax1.set_xlabel('Tamanho do Conjunto de Treinamento')
ax1.set_ylabel('MSE (Erro Médio Quadrático)', color='r')
ax1.tick_params(axis='y', labelcolor='r')

# Criando o segundo eixo y
ax2 = ax1.twinx()

# Plotando a segunda série de dados no segundo eixo y
ax2.plot(train_sizes, test_scores_mean, 'o-', color="g", label="Teste")
ax2.set_ylabel('MSE (Erro Médio Quadrático)', color='g')
ax2.tick_params(axis='y', labelcolor='g')

# Posicionando as legendas de forma que não se sobreponham
ax1.legend(loc='upper left', bbox_to_anchor=(0.01, 0.95))
ax2.legend(loc='upper left', bbox_to_anchor=(0.01, 0.88))

# Título do gráfico
plt.title('Curva de Aprendizado')

# Mostrar o gráfico
plt.show()

Obtendo a seguinte curva de aprendizado.

Fonte: Próprio autor

Pela Figura, é possível perceber de forma mais visual a presença de overfitting, com o modelo se ajustando excessivamente bem aos dados de treino, obtendo MSE de níveis muito baixo. Em contrapartida, evidenciando os altos MSE do conjunto de teste, que tem seu valor reduzido conforme o tamanho do conjunto de treino aumenta.

Resumindo:

Para combater o underfitting…

  1. Aumentar a complexidade do modelo, com parâmetros mais otimizados.
  2. Aumentar a quantidade de dados do conjunto de treino, contribuindo para o modelo capturar relações significativas entre variáveis.
  3. Uso de técnicas de regularização, ajudando o modelo a melhor capturar os padrões dos dados.

Para combater o overfitting…

  1. Aumentar a quantidade de dados do conjunto de treino. Assim, o modelo terá mais dados para capturar os padrões necessários.
  2. Utilizar a técnica de Cross Validation. Assim, ao invés de limitar o treinamento e avaliação o modelo a apenas uma vez, esta técnica permite que sejam realizadas várias etapas de treino e teste, tornando o modelo mais confiável.
  3. Uso de técnicas de regularização, por exemplo a L1 (Lasso) e a L2 (Ridge), que melhoram a capacidade de generalização do modelo.
  4. Aplicar técnicas de pré-processamento (como feature selection, por exemplo), reduzindo a complexidade do modelo.
  5. Utilizar modelos ensemble, na qual a combinação dos vários modelos individuais podem suavizar as previsões mais extremas, reduzindo a sensibilidade aos dados não vistos.

Trade-off viés variância

O trade-off viés variância é um dos conceitos fundamentais de machine learning e consiste no equilíbrio entre dois dos erros que um modelo pode cometer: o erro de viés (também chamado de bias) e o erro de variância.

  • O erro de viés está relacionado com o conceito de underfitting. Isso porque tende a ser maior quando o modelo é muito simples para capturar as relações entre as variáveis, o que pode ocasionar suposições erradas sobre os dados e consequentemente causar previsões incorretas. Ou seja, um alto erro de víes subestima a complexidade dos dados e leva ao underfitting.
  • O erro de variância está relacionado com o conceito de overfitting. Isso porque tende a ser maior quando o modelo é muito complexo em relação aos dados de treinamento, se ajusta excepcionalmente bem ao conjunto de treino. Isso faz com que o modelo seja muito sensível a pequenas variações dos dados de treino. Ou seja, um alto erro de variância subestima a simplicidade dos dados e leva ao overfitting.

O grande objetivo é obter um modelo que seja complexo ao ponto de conseguir reconhecer as relações entre as variáveis (que não seja muito simples), mas também que não seja muito complexo ao ponto de se ajustar excessivamente aos dados de treino. É desta necessidade de equilíbrio que vem a expressão “trade-off”, reduzindo o erro do viés aumenta-se o da variância e reduzindo o erro variância aumenta-se o do viés. Encontrar este equilíbrio é uma tarefa necessária para tornar o modelo confiável e eficaz.

Referências

  1. Clube de Assinatura do Anwar Hermuche
  2. Clube de Assinatura do Andre Yukio

--

--