Luis Henrique 2024-09-02
Quando pensamos em regressões lineares, o uso mais comum e conceitual é para previsões de acontecimentos futuros. Nessa abordagem, o objetivo principal é encontrar a função \(g(x)\) que minimize a função de risco \(R(g(x))\) e que generalize bem para novos dados.
Contudo, enfrentamos um problema conceitual com a MSE (Mean Squared Error), que é a função de custo mais utilizada. A MSE tende a reduzir-se quando a complexidade do modelo aumenta, o que pode levar a um problema de overfitting. Overfitting ocorre quando o modelo se ajusta excessivamente aos dados de treinamento, perdendo a capacidade de generalizar para novos dados.
Para enfrentar o overfitting, aplicamos técnicas como Data Splitting e Cross Validation. Essas abordagens ajudam a avaliar o modelo de forma mais robusta, separando os dados em conjuntos de treinamento e teste e validando o modelo em diferentes subconjuntos dos dados. No entanto, além dessas técnicas, é importante lidar com o trade-off entre viés e variância.
O trade-off entre viés e variância refere-se à troca entre a complexidade do modelo e sua capacidade de generalização. Modelos mais complexos podem ter alta variância (ou seja, ajustam-se muito bem aos dados de treinamento) e baixo viés (ou seja, não generalizam bem para novos dados). Para mitigar isso, aplicamos penalidades para reduzir a complexidade do modelo e melhorar sua capacidade de generalização.
Quando lidamos com conjuntos de dados de alta dimensionalidade, ou seja, com muitas variáveis de entrada, a complexidade dos modelos pode causar overfitting devido à grande quantidade de features. Nesse contexto, adicionamos parâmetros de ajuste, conhecidos como penalidades, às funções de regressão. As duas principais penalidades são Lasso (L1) e Ridge (L2).
A penalidade Lasso adiciona um termo de regularização à função de custo que penaliza a soma dos valores absolutos dos coeficientes:
\[ \text{Lasso} \; R(g(x)) = \text{MSE} + \lambda \sum_{j=1}^p |\beta_j| \]
Onde \(\lambda\) é o parâmetro de regularização que controla a intensidade da penalidade. O efeito principal da penalidade Lasso é forçar alguns coeficientes a serem exatamente zero, efetivamente realizando seleção de variáveis e reduzindo a complexidade do modelo.
A penalidade Ridge adiciona um termo de regularização que penaliza a soma dos quadrados dos coeficientes:
\[ \text{Ridge} \; R(g(x)) = \text{MSE} + \lambda \sum_{j=1}^p \beta_j^2 \]
Aqui, \(\lambda\) também controla a intensidade da penalidade, mas o Ridge tende a reduzir todos os coeficientes de forma mais uniforme, sem forçá-los a zero. Isso ajuda a reduzir a variância do modelo sem eliminar completamente variáveis.
Ambas as técnicas ajudam a controlar a complexidade do modelo e a melhorar a capacidade de generalização ao lidar com dados de alta dimensionalidade. A escolha entre Lasso e Ridge depende das características do problema e dos objetivos do modelo.
Seguindo o exemplo do livro “Aprendizado de Máquina - Uma abordagem estatística” de Rafael Izbicki (que inclusive recomendo a leitura) ele trabalha, no cap. 3 parte 3.8 no exemplo 3.3 com dados da Amazon Fine Food Reviews do link https://www.kaggle.com/datasets/snap/amazon-fine-food-reviews
O código começa carregando um conjunto de dados de avaliações a partir de um arquivo CSV e seleciona uma amostra aleatória de 20.000 linhas para análise, garantindo a reprodutibilidade ao definir uma semente para a amostragem. Em seguida, ele cria um corpus de texto a partir da coluna de texto das avaliações e constrói uma matriz de documentos e termos (DTM) que representa a frequência das palavras em cada documento. Essa matriz é então convertida em uma forma esparsa para otimizar o armazenamento, considerando que muitas entradas são zero.
Após a preparação dos dados, o código divide aleatoriamente os dados em conjuntos de treinamento e teste, selecionando 10.000 índices para treinamento. Em relação ao tratamento de dados, o código calcula a média dos escores de avaliação, ignorando os valores ausentes (NAs), e substitui os NAs nos escores de teste por essa média. Esse tratamento é essencial para evitar problemas durante a validação do modelo, assegurando que o modelo possa ser avaliado de forma eficaz e sem distorções causadas por valores ausentes.
Obs: Lembrando que essa parte é dos créditos do professor Rafael
# Ler os dados da amostra
dados <- read.csv('Reviews.csv', header = TRUE)
# Selecionar uma amostra dos dados
set.seed(1)
selecao <- sample(nrow(dados), 20000) #selecionando uma amostra dos dados
dados <- dados[selecao,]
# Criar a matriz de preditoras (Document-Term Matrix)
corp <- VCorpus(VectorSource(dados$Text))
dtm <- DocumentTermMatrix(corp,
control = list(tolower = TRUE,
stemming = FALSE,
removeNumbers = TRUE,
removePunctuation = TRUE,
removeStripwhitespace = TRUE,
weighting = weightTf))
dtmMatrix <- sparseMatrix(i = dtm$i, j = dtm$j, x = dtm$v,
dimnames = list(NULL, dtm$dimnames[[2]]),
dims = c(dtm$nrow, dtm$ncol))
dim(dtmMatrix)## [1] 20000 35514
tr <- sample.int(length(selecao), 10000, replace = FALSE)
# Calcular a média ignorando os NAs -> Esses valors atrapalham validações
media_score <- mean(dados$Score[-tr], na.rm = TRUE)
# Substituir os NAs pela média calculada
dados$Score[-tr][is.na(dados$Score[-tr])] <- media_scoreVeja a matriz criada, tendo mais de 35mil features (termos) e 20mil instâncias
Aqui \(d > n\) mostra a imensa dimensionalidade causada de forma proposital.
Neste segmento, o autor desenvolve três modelos de regressão: um modelo baseado em Mínimos Quadrados Ordinários (MQO), um modelo de Ridge e um modelo de Lasso.
Primeiramente, é criado um modelo de regressão linear simples utilizando MQO. Em seguida, o autor adapta o modelo para incorporar penalidades Ridge e Lasso. A aplicação da penalidade Ridge envolve a adição de um termo de regularização baseado na soma dos quadrados dos coeficientes, enquanto a penalidade Lasso adiciona um termo de regularização baseado na soma dos valores absolutos dos coeficientes.
Após a construção dos modelos, realiza-se a previsão para cada um
deles. É importante notar que o pacote glmnet utiliza
validação cruzada para determinar automaticamente o valor ideal do
parâmetro de ajuste, \(\lambda\). No
entanto, em seções subsequentes, abordaremos a metodologia para
selecionar esse parâmetro de forma manual, permitindo uma compreensão
mais profunda do processo de ajuste dos modelos
# Ajuste dos modelos de regressão
# Modelo MQO (Mínimos Quadrados Ordinários)
ajuste_mq <- glmnet(dtmMatrix[tr,], dados$Score[tr],
alpha = 0, lambda = 0)
predito_mq <- predict(ajuste_mq, newx = dtmMatrix[-tr,])
# Modelo Ridge
ridge <- cv.glmnet(dtmMatrix[tr,], dados$Score[tr],
alpha = 0)
predito_ridge <- predict(ridge,
s = ridge$lambda.min, newx = dtmMatrix[-tr,])
# Modelo Lasso
lasso <- cv.glmnet(dtmMatrix[tr,], dados$Score[tr],
alpha = 1)
predito_lasso <- predict(lasso,
s = lasso$lambda.min, newx = dtmMatrix[-tr,])Agora vamos montar uma tabela cruzando dados previstos com dados reais para encontrarmos os resultados resíduais de todos os três modelos.
A partir daqui eu apenas me inspirei no livro, porém os códigos são de autoria própria
# Função para calcular o EQM e o erro padrão com tratamento de NA
calcular_metrica_eficiente <- function(predito, real) {
# Remover valores NA
predito <- na.omit(predito)
real <- na.omit(real)
# Verificar se os vetores têm o mesmo comprimento
if (length(predito) != length(real)) {
stop("Os vetores 'predito' e 'real' têm comprimentos diferentes.")
}
# Calcular o erro e as métricas
erro <- (predito - real)^2
eqm <- mean(erro)
erro_padrao <- sd(erro) / sqrt(length(erro))
return(c(eqm, erro_padrao))
}
# Calcular métricas para cada modelo
metrica_mqo <- calcular_metrica_eficiente(
as.vector(predito_mq), dados$Score[-tr])
metrica_lasso <- calcular_metrica_eficiente(
as.vector(predito_lasso), dados$Score[-tr])
metrica_ridge <- calcular_metrica_eficiente(
as.vector(predito_ridge), dados$Score[-tr])
# Criar a tabela de métricas
tabela_metricas <- data.frame(
Modelo = c("MQO", "Lasso", "Ridge"),
EQM = c(metrica_mqo[1], metrica_lasso[1], metrica_ridge[1]),
Erro_Padrao = c(metrica_mqo[2], metrica_lasso[2], metrica_ridge[2])
)
tabela_metricas## Modelo EQM Erro_Padrao
## 1 MQO 7.722554 0.22488391
## 2 Lasso 1.101487 0.02063685
## 3 Ridge 1.161883 0.02113314
Desempenho do Modelo Lasso
O modelo Lasso demonstrou ser o mais eficaz entre os três analisados. O Lasso aplica uma penalidade à função de custo que é proporcional à soma das magnitudes dos coeficientes multiplicados pelo parâmetro de ajuste \(\lambda\). Esta abordagem é particularmente vantajosa em cenários de alta dimensionalidade, pois tem a capacidade de eliminar features irrelevantes de maneira mais assertiva.
Embora o modelo Ridge também tenha mostrado um desempenho próximo, o Lasso apresentou uma vantagem significativa ao reduzir mais efetivamente o número de features. No entanto, essa eliminação excessiva de features pode, às vezes, impactar negativamente a capacidade de generalização do modelo para novos dados, já que o Lasso tende a promover um maior número de coeficientes zero comparado ao Ridge.
Portanto, enquanto o Lasso é eficaz na seleção de features e na redução da dimensionalidade, é crucial balancear essa seleção com a necessidade de manter a capacidade preditiva do modelo.
# Transformar a tabela em formato longo
tabela_metricas_long <- melt(tabela_metricas,
id.vars = "Modelo",
variable.name = "Métrica",
value.name = "Valor")
# Garantir que os modelos apareçam na ordem desejada
tabela_metricas_long$Modelo <- factor(
tabela_metricas_long$Modelo,
levels = c("MQO", "Lasso", "Ridge"))
# Plotar o gráfico de barras com valores
ggplot(tabela_metricas_long,
aes(x = Modelo, y = Valor, fill = Métrica)) +
geom_bar(stat = "identity", position = "dodge") +
geom_text(aes(label = round(Valor, 2)),
position = position_dodge(width = 0.9),
vjust = -0.5, size = 4) +
labs(title = "Comparação de Métricas dos Modelos",
x = "Modelo", y = "Valor") +
scale_fill_manual(values =
c("EQM" = "cornflowerblue",
"Erro_Padrao" = "tomato2")) +
theme_classic()Vamos dar uma breve olhada para quais palavras apresentam maior importância para a previsão no nosso modelo Lasso. Quanto maior o coeficiente melhor e mais importante ele se torna à previsão
# Obtenha os coeficientes do Lasso e remova o intercepto
coeficientes_lasso <- as.matrix(coef(lasso, s = lasso$lambda.min))
coeficientes_lasso <- coeficientes_lasso[-1, ,
drop = FALSE] # Remover o intercepto
# Converter em dataframe e adicionar os nomes das palavras
coeficientes_lasso_df <- data.frame(
Palavra = rownames(coeficientes_lasso),
Coeficiente = coeficientes_lasso[, 1]
)
# Separar em positivos e negativos
coeficientes_positivos <- coeficientes_lasso_df %>%
filter(Coeficiente > 0) %>%
arrange(desc(Coeficiente))
coeficientes_negativos <- coeficientes_lasso_df %>%
filter(Coeficiente < 0) %>%
arrange((Coeficiente))
# Gerar gráfico para coeficientes positivos
grafico_positivos <- ggplot(coeficientes_positivos[1:20, ],
aes(x = Coeficiente,
y = reorder(Palavra, Coeficiente))) +
geom_col(fill = "cornflowerblue") +
labs(x = "Coeficiente Lasso", title = "Coeficientes Positivos") +
theme_classic()
# Gerar gráfico para coeficientes negativos
grafico_negativos <- ggplot(coeficientes_negativos[1:20, ],
aes(x = Coeficiente,
y = reorder(Palavra, Coeficiente))) +
geom_col(fill = "tomato") +
labs(x = "Coeficiente Lasso", title = "Coeficientes Negativos") +
theme_classic()
# Colocar os gráficos lado a lado
grafico_positivos + grafico_negativosVeja como temos boas features mas a partir da décima quinta ou vigésima feature seu coef se aproxima de 0. Aqui mostramos apenas 20, mas lembre-se que são muito mais de 20, veja que quanto mais features acrescentamos, menos importância algumas tem, considerar modelos de regressão com as mesmas infla e gera overfitting no modelo piorando sua previsão.
Poderíamos considerar isso em regressões inferenciais, visto que a ideia não é predizer valores futuros mas avaliar uma regressão “real” do modelo e verificar o cruzamento das features com seus rótulos.
Por fim, como prometido, vou te mostrar um pouco de como selecionar bons valores de tuning params ou o lambda
Para isso, vamos criar uma lista de diversos valores de lambda e calcular, para cada, a função de risco. Desses resultados, vamos plotar uma curva para representar os resultados.
# Definir a sequência de valores de lambda
lambda_seq <- seq(0, 1, by = 0.01)
# Ajustar o modelo com glmnet para cada valor de lambda
lasso_model <- glmnet(dtmMatrix[tr,], dados$Score[tr],
alpha = 1, lambda = lambda_seq)
# Realizar a validação cruzada e calcular o erro de previsão e penalidade
mse_penalidade <- numeric(length(lambda_seq))
for (i in 1:length(lambda_seq)) {
# Prever usando o valor de lambda
predito <- predict(lasso_model,
s = lambda_seq[i], newx = dtmMatrix[-tr,])
# Calcular o erro de previsão (MSE)
erro <- mean((dados$Score[-tr] - predito)^2)
# Calcular a penalidade
penalidade <- lambda_seq[i] * sum(coef(lasso_model,
s = lambda_seq[i])[-1] != 0)
# Calcular a função de risco (MSE + Penalidade)
mse_penalidade[i] <- erro + penalidade
}
# Criar um data frame para plotagem
resultados <- data.frame(
Lambda = lambda_seq,
Funcao_Risco = mse_penalidade
)
# Encontrar o valor mínimo da função de risco e o correspondente lambda
indice_min <- which.min(resultados$Funcao_Risco)
lambda_min <- resultados$Lambda[indice_min]
risco_min <- resultados$Funcao_Risco[indice_min]
# Plotar o gráfico com linha azul e suavização
ggplot(resultados, aes(x = Lambda, y = Funcao_Risco)) +
geom_line(color = "lightblue", size = 1) + # Linha azul
geom_vline(xintercept = lambda_min,
linetype = "dashed",
color = "tomato") + # Linha vertical tracejada
geom_text(aes(x = lambda_min+0.15,
y = risco_min, label =
sprintf("Lambda = %.3f", lambda_min)),
vjust = -0.5, hjust = 1.1, color = "tomato", size = 3) + # Rótulo
labs(x = "Lambda",
y = "Função de Risco (MSE)",
title = "Função de Risco vs Lambda para Lasso") +
theme_classic()## Warning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.
## ℹ Please use `linewidth` instead.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning in geom_text(aes(x = lambda_min + 0.15, y = risco_min, label = sprintf("Lambda = %.3f", : All aesthetics have length 1, but the data has 101 rows.
## ℹ Please consider using `annotate()` or provide this layer with data containing
## a single row.
Sendo que o Lasso utilizado pelo glmnet foi:
# Obtém o lambda que minimiza o erro de validação cruzada
lambda_min <- lasso$lambda.min
# Obtém o lambda com o menor erro de validação cruzada mais um desvio padrão
lambda_1se <- lasso$lambda.1se
# Imprime os valores de lambda
cat("Lambda que minimiza o erro de validação cruzada:", lambda_min, "\n")## Lambda que minimiza o erro de validação cruzada: 0.02041104
## Lambda com o menor erro de validação cruzada mais um desvio padrão: 0.02826697
Assim, podemos ver o quão benéfico é as regularizações de Lasso e Ridget para datasets de alta dimensão.
Comumente é recomendado o uso da Regressão de Ridge, porém caso acredite que tenham features que não são importantes, a Lasso ou Elastic Net (uma soma das regularizações de Lasso e Ridget) são as mais recomendadas, sendo a Elastic Net mais recomendado.
Para um bônus vou te mostrar uma forma interessante de plotar esses gráficos de Tuning (lambda) x MSE
Basta usar o plot(modelo_lasso ou modelo_ridge). Veja que legal: