Previsão de Preços de Imóveis em Boston com Gradiente Descendente

Este artigo detalha a implementação de um modelo de regressão linear para prever preços de imóveis em Boston, utilizando o algoritmo de gradiente descendente. Exploraremos o processo de carregamento e pré-processamento dos dados, a construção da rede neural, o treinamento do modelo e, finalmente, a salvaguarda e utilização do modelo treinado.

Conjunto de Dados e Pré-processamento

O conjunto de dados "housing.data" contém informações sobre imóveis e seus preços. Cada linha representa um imóvel, com as primeiras 13 colunas sendo características e a 14ª sendo o preço médio de venda (MEDV).

Carregamento e Normalização de Dados

A função load_data é responsável por carregar os dados de um arquivo e realizar a normalização. A normalização é crucial para que o gradiente descendente convirja eficientemente. Utilizamos a normalização Min-Max, onde cada característica é escalada para um intervalo entre 0 e 1 usando os valores mínimo e máximo do conjunto de treinamento. O conjunto de dados é dividido em treino (80%) e teste (20%).


import numpy as np

def load_data():
   datafile = './work/housing.data'
   data = np.fromfile(datafile, sep=' ')
   feature_names = [ 'CRIM', 'ZN', 'INDUS', 'CHAS', 'NOX', 'RM', 'AGE', 'DIS', 'RAD', 'TAX', 'PTRATIO', 'B', 'LSTAT', 'MEDV' ]
   feature_num = len(feature_names)
   data = data.reshape([data.shape[0] // feature_num, feature_num])

   ratio = 0.8
   offset = int(data.shape[0] * ratio)
   training_data_raw = data[:offset]

   maximums, minimums = training_data_raw.max(axis=0), training_data_raw.min(axis=0)

   # Normaliza todo o conjunto de dados usando os limites do conjunto de treino
   for i in range(feature_num):
       data[:, i] = (data[:, i] - minimums[i]) / (maximums[i] - minimums[i])

   training_data = data[:offset]
   test_data = data[offset:]
   return training_data, test_data, maximums, minimums
   

Implementação da Rede Neural

A classe Network encapsula a lógica do modelo de regressão linear:

  • __init__: Inicializa os pesos (w) e o viés (b) aleatoriamente. A semente aleatória é fixada para reprodutibilidade.
  • forward: Calcula a saída do modelo (z) dada uma entrada (x).
  • loss: Calcula o erro quadrático médio (MSE) entre a previsão (z) e o valor real (y).
  • gradient: Calcula os gradientes do erro em relação aos pesos e ao viés.
  • update: Atualiza os pesos e o viés usando a taxa de aprendizado (eta).
  • train: Executa o processo de treinamento iterativamente, calculando a perda e atualizando os parâmetros.
  • save_model: Salva os pesos, viés e os valores de máximo/mínimo usados na normalização.
  • load_model: Carrega os parâmetros do modelo salvos.
  • predict: Realiza uma previsão para novas entradas.

import numpy as np

class Network(object):
   def __init__(self, num_of_weights):
       np.random.seed(0)
       self.w = np.random.randn(num_of_weights, 1)
       self.b = 0.
       self.maximums = None
       self.minimums = None

   def forward(self, x):
       return np.dot(x, self.w) + self.b

   def loss(self, z, y):
       error = z - y
       num_samples = error.shape[0]
       cost = np.sum(error**2) / num_samples
       return cost

   def gradient(self, x, y):
       z = self.forward(x)
       error = z - y
       gradient_w = np.mean(error * x, axis=0, keepdims=True).T
       gradient_b = np.mean(error)
       return gradient_w, gradient_b

   def update(self, gradient_w, gradient_b, eta=0.01):
       self.w -= eta * gradient_w
       self.b -= eta * gradient_b

   def train(self, x, y, iterations=100, eta=0.01):
       losses = []
       for i in range(iterations):
           z = self.forward(x)
           L = self.loss(z, y)
           gradient_w, gradient_b = self.gradient(x, y)
           self.update(gradient_w, gradient_b, eta)
           losses.append(L)
           if (i + 1) % 100 == 0:
               print(f'Iteração {i+1}, Perda: {L:.4f}')
       return losses

   def save_model(self, filename='trained_model.npz', maximums=None, minimums=None):
       if maximums is None or minimums is None:
           raise ValueError("Valores de máximo e mínimo devem ser fornecidos para salvar o modelo.")
       np.savez(filename, weights=self.w, bias=self.b, maximums=maximums, minimums=minimums)

   def load_model(self, filename='trained_model.npz'):
       model_data = np.load(filename)
       self.w = model_data['weights']
       self.b = model_data['bias']
       self.maximums = model_data['maximums']
       self.minimums = model_data['minimums']

   def predict(self, x):
       return self.forward(x)
   

Treinamento e Salvamento do Modelo

O script train_and_save.py orquestra o treinamento e o salvamento:

  1. Carrega os dados normalizados e os divide em características (x) e alvo (y).
  2. Instancia a classe Network.
  3. Treina o modelo usando os dados de treinamennto.
  4. Salva os pesos, viés e os valores de normalização em um arquivo '.npz'.
  5. Plota o gráfico da função de perda ao longo das iterações para visualizar a convergência.

import matplotlib.pyplot as plt
from network import Network, load_data # Assumindo que Network e load_data estão em network.py

# Carrega os dados
train_data, _, maximums, minimums = load_data() # Ignora test_data aqui para o propósito de treino

x = train_data[:, :-1]
y = train_data[:, -1:]

# Inicializa e treina a rede
net = Network(num_of_weights=13)
num_iterations = 1000
learning_rate = 0.01
losses = net.train(x, y, iterations=num_iterations, eta=learning_rate)

# Salva o modelo treinado
net.save_model('trained_model.npz', maximums=maximums, minimums=minimums)

# Plota a perda
plot_x = np.arange(num_iterations)
plot_y = np.array(losses)
plt.figure(figsize=(10, 6))
plt.plot(plot_x, plot_y)
plt.title('Função de Perda Durante o Treinamento')
plt.xlabel('Iteração')
plt.ylabel('Perda (MSE)')
plt.grid(True)
plt.show()

print("Modelo treinado e salvo como 'trained_model.npz'")
   

Realizando Previsões

O script predict.py é responsável por carregar o modelo treinado e fazer previsões com base na entrada do usuário:

  1. Carrega o modelo (pesos, viés e parâmetros de normalização) usando load_model.
  2. Solicita ao usuário que insira 13 valores de características.
  3. Normaliza os dados de entrada usando os parâmetros maximums e minimums carregados.
  4. Realiza a previsão usando net.predict.
  5. Desnormaliza o resultado da previsão para obter o preço estimado no valor original.

import numpy as np
from network import Network # Assumindo que Network está em network.py

def load_and_predict(model_file='trained_model.npz'):
   net = Network(num_of_weights=13)
   try:
       net.load_model(model_file)
   except FileNotFoundError:
       print(f"Erro: Arquivo do modelo '{model_file}' não encontrado. Treine o modelo primeiro.")
       return
   except ValueError as e:
       print(f"Erro ao carregar modelo: {e}")
       return

   print("\n--- Previsor de Preços de Imóveis ---")
   print("Insira 13 valores de características separados por espaço (ou 'exit' para sair):")

   while True:
       user_input = input("> ").strip()
       if user_input.lower() == 'exit':
           print("Saindo do programa.")
           break

       try:
           features = np.array(user_input.split(), dtype=np.float32)
           if len(features) != 13:
               raise ValueError("Número incorreto de características.")

           # Normaliza as características de entrada usando os limites carregados do modelo
           # Certifica-se de que net.minimums e net.maximums não são None
           if net.minimums is None or net.maximums is None:
                raise ValueError("Parâmetros de normalização (mínimo/máximo) não carregados.")

           # Normaliza apenas as 13 características de entrada
           norm_features = (features - net.minimums[:-1]) / (net.maximums[:-1] - net.minimums[:-1])

           # Realiza a previsão
           predicted_norm = net.predict(norm_features.reshape(1, -1))

           # Desnormaliza o resultado usando os limites carregados para MEDV (a última coluna)
           real_price = predicted_norm * (net.maximums[-1] - net.minimums[-1]) + net.minimums[-1]

           print(f"Preço de Imóvel Previsto: ${real_price[0][0]:.2f} mil (MEDV)")

       except ValueError as e:
           print(f"Entrada inválida: {e}. Por favor, insira 13 números separados por espaço.")
       except Exception as e:
           print(f"Ocorreu um erro inesperado: {e}")

# Executa a função de predição
load_and_predict('trained_model.npz')
   

Considerações sobre a Normalização e Convergência

Normalização de Amostras para Previsão: É fundamental que os dados de teste ou de novas amostras sejam normalizados usando os mesmos valores mínimo e máximo do conjunto de treinamento. Isso garante que os pesos aprendidos pelo modelo sejam aplicados de forma consistente, pois eles foram treinados sob essa escala específica.

Gradiente Próximo de Zero: Quando os gradientes de todos os parâmetros se aproximam de zero, isso geralmente indica que o modelo convergiu para um mínimo (local ou global) da função de perda. No entanto, pode também sugerir que a taxa de aprendizado é muito pequena, ou que há problemas com a escala dos dados. Não significa necessariamente que o treinamento esteja completo se a perda ainda for alta; pode indicar um platô ou um mínimo local indesejado.

Tamanho do Batch (Batch Size):

  • Batch Size pequeno (ex: 1 - Gradiente Descendente Estocástico): Gera estimativas de gradiente ruidosas, o que pode ajudar a escapar de mínimos locais, mas torna a convergência mais instável.
  • Batch Size grande (ex: todo o dataset - Gradiente Desecndente em Batch): Resulta em atualizações de gradiente mais estáveis e precisas, mas pode levar mais tempo por iteração e pode convergir para mínimos locais mais agudos.
  • A escolha ideal geralmente fica entre 32 e 256, dependendo da arquitetura da rede, do tamanho do dataset e dos recursos computacionais.

Cálculo de Iterações de Treinamento: Para 1000 amostras, 5 épocas e um batch size de 20, o número total de atualizações de gradiente (ou "passos de mini-batch") seria: (1000 amostras / 20 amostras/batch) * 5 épocas = 250 atualizações.

Alternativa: Normalização Z-Score

Uma alternativa à normalização Min-Max é a normalização Z-score (padronização), que transforma os dados para terem média 0 e desvio padrão 1. O processo envolve calcular a média e o desvio padrão do conjunto de treinamento e aplicá-los para transformar os dados de treino, validação e teste.


import numpy as np
from scipy import stats

# ... (carregamento e divisão de dados como antes) ...

# Calcula média e desvio padrão do conjunto de treino
train_mean = np.mean(train_data, axis=0)
train_std = np.std(train_data, axis=0)

# Aplica Z-score a todos os conjuntos de dados
train_data_scaled = stats.zscore(train_data, axis=0)
eval_data_scaled = (eval_data - train_mean) / train_std
test_data_scaled = (test_data - train_mean) / test_std

# O restante do treinamento usaria esses dados escalados.
# A classe Network precisaria ser adaptada para salvar/carregar train_mean e train_std
# e para a função de inversão de transformação.
   

A implementação com Z-score também exigiria adaptações na classe Network para salvar/carregar a média e o desvio padrão, além de uma função para reverter a transformação Z-score durante a previsão.

Tags: regressão linear Gradiente Descendente machine learning Python Normalização de Dados

Publicado em 10-22 11:00