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:
- Carrega os dados normalizados e os divide em características (
x) e alvo (y). - Instancia a classe
Network. - Treina o modelo usando os dados de treinamennto.
- Salva os pesos, viés e os valores de normalização em um arquivo '.npz'.
- 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:
- Carrega o modelo (pesos, viés e parâmetros de normalização) usando
load_model. - Solicita ao usuário que insira 13 valores de características.
- Normaliza os dados de entrada usando os parâmetros
maximumseminimumscarregados. - Realiza a previsão usando
net.predict. - 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.