No post anterior, construímos nosso primeiro classificador de emojis com Regressão Logística em Java. Agora, vamos subir a um novo nível e encarar um problema mais prático e fascinante: a previsão de preço de Imóveis com Java utilizando Regressão Linear! Se você sempre quis saber como a Inteligência Artificial pode ser aplicada para analisar dados do mundo real e fazer previsões úteis, este post é para você!
Neste guia passo a passo, vamos utilizar um dataset real de preços de imóveis para construir um modelo preditivo completo em Java, desde o carregamento e análise dos dados até o treinamento, avaliação e visualização dos resultados. Prepare-se para mergulhar no mundo da Regressão Linear em Java e ver seus projetos de IA ganharem um toque de realidade! 🚀
Regressão linear: A base da previsão numérica 📈
Antes de codificarmos, vamos entender a Regressão Linear, um dos algoritmos mais fundamentais em Machine Learning para tarefas de previsão numérica. Diferente da Regressão Logística que usamos para classificar emojis (categorias), a Regressão Linear busca prever um valor contínuo, como o preço de um imóvel.
Imagine o seguinte: você quer prever o preço de um apartamento com base em sua área (metros quadrados). Intuitivamente, você sabe que quanto maior a área, maior tende a ser o preço, certo? A Regressão Linear modela essa relação linear entre a área e o preço.
Em termos mais formais
- Objetivo: Encontrar a melhor linha reta que descreve a relação entre uma ou mais variáveis de entrada (chamadas de features ou características) e uma variável de saída (chamada de target ou variável alvo). No nosso caso, as features serão características dos imóveis (área, número de quartos, localização…) e o target será o preço do imóvel.
- Equação Linear: A Regressão Linear assume que essa relação pode ser expressa por uma equação linear, na forma mais simples:
y = wx + b
Onde:- y é a variável target (preço do imóvel). x é a feature de entrada (área do imóvel). w é o peso ou coeficiente da feature x (inclinação da linha). Ele indica o quanto o preço do imóvel varia para cada unidade de aumento na área. b é o bias ou termo de viés (intercepto da linha no eixo y). É o preço base do imóvel quando a área é zero (na prática, um valor base para o modelo).
- Treinamento (Aprendizado): O objetivo do treinamento da Regressão Linear é encontrar os melhores valores para os pesos (w) e o bias (b) que minimizem o erro entre os preços preditos pelo modelo e os preços reais dos imóveis no dataset de treinamento.
- Função de Custo (Erro): Para medir o “erro” do modelo, utilizamos uma função de custo. Uma função de custo comum para Regressão Linear é o Erro Quadrático Médio (Mean Squared Error – MSE). O MSE calcula a média dos quadrados das diferenças entre os valores preditos e os valores reais. Quanto menor o MSE, melhor o modelo se ajusta aos dados.
- Algoritmo de Otimização (Gradiente Descendente): Para encontrar os valores de w e b que minimizam o MSE, utilizamos um algoritmo de otimização chamado Gradiente Descendente. O Gradiente Descendente ajusta iterativamente os pesos e bias na direção oposta ao gradiente da função de custo, buscando o ponto de mínimo (onde o erro é menor). Em termos mais simples, ele “desce a ladeira” da função de custo até encontrar o ponto mais baixo (menor erro).
Em resumo, a Regressão Linear é uma técnica poderosa para modelar relações lineares e fazer previsões numéricas. Vamos agora aplicá-la a um problema real de previsão de preços de imóveis em Java! 🏠💰
Obtendo e preparando dados reais de preços de imóveis 📊🏡
“Dados são a alma do negócio” – e também da Inteligência Artificial! Portanto, para construir nosso modelo de previsão de preços de imóveis, precisamos de um dataset real.
Existem diversas fontes de dados imobiliários disponíveis online. Para este post, utilizaremos um dataset simplificado que você pode baixar diretamente deste link.
Certifique-se de que o dataset esteja limpo, sem valores faltantes ou erros de formatação, e com a coluna de “Area” em uma escala consistente (por exemplo, todos os valores em metros quadrados – m²).
Exemplo do dataset
property_type,state,region,lat,lon,area_m2,price_brl
apartment,Pernambuco,Northeast,-8.134204,-34.906326,72,414222.98
apartment,Pernambuco,Northeast,-8.1266642,-34.903924,136,848408.53
apartment,Pernambuco,Northeast,-8.1255503,-34.907601,75,299438.28
Para este primeiro exemplo, vamos focar na feature area_m2 para prever o price_brl, simplificando o problema para Regressão Linear Simples (com apenas uma feature). Posteriormente, podemos expandir para Regressão Linear Múltipla utilizando mais features como “Quartos”, “Localização”, etc.
Além disso, para garantir um treinamento mais eficaz e evitar problemas numéricos devido a diferentes escalas de valores, aplicaremos a técnica de Padronização (Standardization ou Z-score) aos nossos dados. A padronização colocará tanto a feature “area_m2” quanto o target “price_brl” em uma escala similar, o que facilitará o aprendizado do modelo pelo Gradiente Descendente.
Implementando a regressão linear 💻✍️
Com os dados preparados e entendidos os conceitos de Regressão Linear, vamos implementar a classe LinearRegressionModel
em Java:
import org.nd4j.linalg.api.buffer.DataType;
import org.nd4j.linalg.api.ndarray.INDArray;
import org.nd4j.linalg.factory.Nd4j;
import org.nd4j.linalg.ops.transforms.Transforms;
public class LinearRegressionModel {
private INDArray weights; // Peso (coeficiente 'w')
private double bias; // Bias (termo de viés 'b')
public LinearRegressionModel(int numFeatures) {
// Inicializa pesos e bias (zeros inicialmente)
this.weights = Nd4j.zeros(DataType.DOUBLE, numFeatures, 1);
this.bias = 0.0;
}
public double predict(INDArray features) {
// y = (weights^T * x) + bias
return features.mmul(weights).getDouble(0) + bias;
}
public double costFunction(INDArray features, INDArray targets) {
// Função de Custo: Mean Squared Error (MSE)
int numSamples = features.rows();
INDArray predictions = features.mmul(weights).add(bias); // Previsões para todos os exemplos
INDArray errors = predictions.sub(targets); // Erros (diferença entre previsão e real)
INDArray squaredErrors = Transforms.pow(errors, 2); // Erros ao quadrado
double sumSquaredErrors = squaredErrors.sumNumber().doubleValue(); // Soma dos erros quadrados
return sumSquaredErrors / (2.0 * numSamples); // MSE (dividido por 2 para simplificar gradiente, comum em ML)
}
public void train(INDArray features, INDArray targets, double learningRate, int numIterations) {
int numSamples = features.rows();
for (int iteration = 0; iteration < numIterations; iteration++) {
// 1. Calcular Previsões
INDArray predictions = features.mmul(weights).add(bias);
// 2. Calcular Erros
INDArray errors = predictions.sub(targets);
// 3. Calcular Gradientes (de forma simplificada para este exemplo didático)
INDArray gradientsWeights = features.transpose().mmul(errors).div(numSamples); // Gradiente dos pesos
double gradientBias = errors.sumNumber().doubleValue() / numSamples; // Gradiente do bias
// 4. Atualizar Pesos e Bias (Gradiente Descendente)
weights.subi(gradientsWeights.mul(learningRate)); // Atualiza pesos (subtrai o gradiente multiplicado pela taxa de aprendizado)
bias -= learningRate * gradientBias; // Atualiza bias (subtrai o gradiente multiplicado pela taxa de aprendizado)
if (iteration % 100 == 0) {
// Print do custo a cada 100 iterações para acompanhar o treinamento
double cost = costFunction(features, targets);
System.out.println("Iteração " + iteration + ", Custo = " + String.format("%.4f", cost));
}
}
}
}
Na classe LinearRegressionModel:
- weights: Representa o peso (w) na equação da Regressão Linear. Para Regressão Linear Simples (1 feature), será um escalar (representado como INDArray de dimensão 1×1 no código). Para Regressão Linear Múltipla, será um vetor de pesos.
- bias: Representa o bias (b) na equação. Um valor escalar.
- Construtor LinearRegressionModel(int numFeatures): Inicializa weights e bias com zeros. numFeatures indica o número de features que o modelo irá receber (para este exemplo inicial, será 1 – a área). Importante: weights é inicializado como DataType.DOUBLE para garantir a precisão numérica das operações.
- predict(INDArray features): Realiza a previsão do preço para um dado input de features. Implementa a equação linear: y = (weights^T * x) + bias. Utiliza mmul para multiplicação de matrizes/vetores no ND4J (matrix multiply – multiplicação de matrizes).
- costFunction(INDArray features, INDArray targets): Calcula o Mean Squared Error (MSE) para avaliar o quão bem o modelo está ajustado aos dados. Recebe as features e os valores target reais e retorna o valor do MSE.
- train(INDArray features, INDArray targets, double learningRate, int numIterations): Implementa o algoritmo de Gradiente Descendente (de forma simplificada para este exemplo didático) para treinar o modelo, ajustando iterativamente os pesos e bias para minimizar o custo (MSE).
Carregando, normalizando e preparando os dados de imóveis 📝
Para carregar e preparar os dados, incluindo a normalização (padronização), utilizaremos a classe RealEstateData:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import org.nd4j.linalg.api.buffer.DataType;
import org.nd4j.linalg.api.ndarray.INDArray;
import org.nd4j.linalg.factory.Nd4j;
public class RealEstateData {
// Classe interna para retornar dados escalonados e parâmetros de escala
public static class ScaledData {
public final Pair<INDArray, INDArray> data;
public final double featureMean;
public final double featureStdDev;
public final double targetMean;
public final double targetStdDev;
public ScaledData(Pair<INDArray, INDArray> data, double featureMean, double featureStdDev, double targetMean, double targetStdDev) {
this.data = data;
this.featureMean = featureMean;
this.featureStdDev = featureStdDev;
this.targetMean = targetMean;
this.targetStdDev = targetStdDev;
}
}
public static ScaledData loadDataFromCSV(String csvFileName, String featureColumnName, String targetColumnName) throws IOException {
List<Double> featureList = new ArrayList<>();
List<Double> targetList = new ArrayList<>();
int featureColumnIndex = -1;
int targetColumnIndex = -1;
// Usando ClassLoader para acessar o recurso a partir de src/main/resources
ClassLoader classLoader = RealEstateData.class.getClassLoader();
InputStream inputStream = classLoader.getResourceAsStream(csvFileName);
if (inputStream == null) {
throw new IOException("Arquivo CSV não encontrado em src/main/resources: " + csvFileName);
}
try (BufferedReader br = new BufferedReader(new InputStreamReader(inputStream))) {
String line = br.readLine(); // Lê o cabeçalho
String[] headers = line.split(","); // Assume CSV com vírgula como separador
// Encontra os índices das colunas de feature e target no cabeçalho
for (int i = 0; i < headers.length; i++) {
if (headers[i].trim().equalsIgnoreCase(featureColumnName)) {
featureColumnIndex = i;
} else if (headers[i].trim().equalsIgnoreCase(targetColumnName)) {
targetColumnIndex = i;
}
}
if (featureColumnIndex == -1 || targetColumnIndex == -1) {
throw new IllegalArgumentException("Colunas de feature ou target não encontradas no CSV.");
}
while ((line = br.readLine()) != null) {
String[] values = line.split(",");
String area = values[featureColumnIndex].trim();
String price = values[targetColumnIndex].trim();
if (area.isEmpty() || price.isEmpty())
continue;
featureList.add(Double.parseDouble(area)); // Converte feature para double
targetList.add(Double.parseDouble(price)); // Converte target para double
}
}
// Converte List<Double> para INDArray (matrizes ND4J)
double[][] featuresArray = new double[featureList.size()][1]; // Matriz de features (uma coluna para a feature 'Area')
double[][] targetsArray = new double[targetList.size()][1]; // Matriz de targets (uma coluna para o 'Preco')
for (int i = 0; i < featureList.size(); i++) {
featuresArray[i][0] = featureList.get(i); // Preenche matriz de features
targetsArray[i][0] = targetList.get(i); // Preenche matriz de targets
}
INDArray featuresNDArray = Nd4j.create(featuresArray);
INDArray targetsNDArray = Nd4j.create(targetsArray);
// Padronização (feature scaling) - calcular média e desvio padrão
double featureMeanVal = Nd4j.mean(featuresNDArray).getDouble(0); // Média da feature 'Area'
double featureStdDevVal = Nd4j.std(featuresNDArray).getDouble(0); // Desvio padrão da feature 'Area'
double targetMeanVal = Nd4j.mean(targetsNDArray).getDouble(0); // Média dos targets 'Preço'
double targetStdDevVal = Nd4j.std(targetsNDArray).getDouble(0); // Desvio padrão dos targets 'Preço'
// Padronização (feature scaling) - aplicar a padronização (subtrair média e dividir por desvio padrão)
INDArray featuresNormalized = featuresNDArray.sub(featureMeanVal).div(featureStdDevVal); // Padroniza features
INDArray targetsNormalized = targetsNDArray.sub(targetMeanVal).div(targetStdDevVal); // Padroniza targets
// Retornando os dados normalizados (padronizados)
Pair<INDArray, INDArray> normalizedDataPair = new Pair<>(featuresNormalized, targetsNormalized);
return new ScaledData(normalizedDataPair, featureMeanVal, featureStdDevVal, targetMeanVal, targetStdDevVal);
}
public static class Pair<F, S> {
public final F first;
public final S second;
public Pair(F first, S second) {
this.first = first;
this.second = second;
}
}
}
Na classe RealEstateData (com padronização):
- Classe Interna ScaledData: Uma classe auxiliar para retornar tanto os dados normalizados (features e targets) quanto os parâmetros de escala (médias e desvios padrões) calculados durante a padronização.
- Carregamento de Dados de CSV (loadDataFromCSV): Carrega os dados de um arquivo CSV localizado na pasta src/main/resources. Copie o arquivo csv do dataset para esta pasta.
- Padronização (Feature Scaling): Calcula a média e o desvio padrão para a feature “Area” e o target “Preco” no dataset de treinamento. Em seguida, aplica a padronização (subtrair a média e dividir pelo desvio padrão) a ambas as features e targets, criando versões normalizadas dos dados.
- Retorno de ScaledData: Retorna um objeto ScaledData que contém os dados normalizados e os parâmetros de escala, essenciais para treinar o modelo com dados normalizados e denormalizar as previsões posteriormente. Importante: O DataType padrão do ND4J é temporariamente definido como DOUBLE para garantir a precisão numérica dos cálculos com ND4J, e restaurado ao final do método.
Treinando e avaliando o modelo 🚀📊
Finalmente, vamos utilizar as classes LinearRegressionModel
e RealEstateData
(com normalização) para treinar nosso modelo e prever preços de imóveis! Crie a classe Main.java
(ou utilize a sua classe Main existente dos Posts anteriores):
import org.nd4j.linalg.api.ndarray.INDArray;
import org.nd4j.linalg.factory.Nd4j;
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
// 1. Carregar e normaliza os dados de imóveis do CSV
String csvFilePath = "Brasile-real-estate-dataset.csv";
RealEstateData.ScaledData scaledData = RealEstateData.loadDataFromCSV(
csvFilePath, "area_m2", "price_brl");
INDArray featuresNormalized = scaledData.data.first; // Features normalizadas (Área)
INDArray targetsNormalized = scaledData.data.second; // Targets normalizados (Preço)
// Recuperar parâmetros de escala (para denormalizar previsões depois)
double featureMean = scaledData.featureMean;
double featureStdDev = scaledData.featureStdDev;
double targetMean = scaledData.targetMean;
double targetStdDev = scaledData.targetStdDev;
// 2. Criar Modelo de Regressão Linear (1 feature: Area - modelo treinará com dados normalizados)
LinearRegressionModel model = new LinearRegressionModel(1);
// 3. Treinar o Modelo (com dados normalizados)
System.out.println("--- Treinamento do Modelo ---");
double learningRate = 0.001; // Taxa de aprendizado (ajuste conforme necessidade)
int numIterations = 2000; // Número de iterações de treinamento (ajuste conforme necessidade)
model.train(featuresNormalized, targetsNormalized, learningRate, numIterations); // Treina com dados normalizados
// 4. Avaliar o Modelo e Fazer Previsões (com features normalizadas)
System.out.println("\n--- Avaliação e Previsões ---");
// Dados de exemplo para previsão (áreas de imóveis)
double[][] newHouseAreasRaw = {{30}, {85}, {65}, {130}}; // Áreas de novos imóveis (valores originais - não normalizados)
INDArray newHouseFeaturesRaw = Nd4j.create(newHouseAreasRaw);
// Normalizar as features de previsão usando a mesma média e desvio padrão do dataset de treinamento
INDArray newHouseFeaturesNormalized = newHouseFeaturesRaw.sub(featureMean).div(featureStdDev);
// Fazer previsões de preços (com features normalizadas) - modelo prevê em escala normalizada!!!
INDArray predictedPricesNormalized = Nd4j.zeros(newHouseFeaturesNormalized.rows(), 1);
for (int i = 0; i < newHouseFeaturesNormalized.rows(); i++) {
double predictedPriceNormalized = model.predict(newHouseFeaturesNormalized.getRow(i));
predictedPricesNormalized.putScalar(i, predictedPriceNormalized);
}
// Denormalizar as previsões para a escala original do preço!!! (inverso da normalização)
INDArray predictedPricesDenormalized = predictedPricesNormalized.mul(targetStdDev).add(targetMean);
// Imprimir Previsões
System.out.println("\n--- Previsões de Preços de Imóveis ---");
for (int i = 0; i < newHouseFeaturesRaw.rows(); i++) {
double area = newHouseFeaturesRaw.getDouble(i, 0);
double predictedPrice = predictedPricesDenormalized.getDouble(i, 0); // USAR PREÇOS DENORMALIZADOS!!!
System.out.println("Área: " + area + " m², Preço Previsto: R$" + String.format("%.2f", predictedPrice));
}
// Calcular e Imprimir o Custo Final (MSE) no dataset de treinamento (para ter uma ideia do ajuste do modelo)
// Calcular custo com dados normalizados (já que o treinamento foi feito com eles)
double finalCostNormalized = model.costFunction(featuresNormalized, targetsNormalized);
System.out.println("\nCusto Final (MSE) NORMALIZADO no Dataset de Treinamento: " + String.format("%.4f", finalCostNormalized));
}
}
Na classe Main.java (com normalização e denormalização):
- Carregar Dados Normalizados: Utiliza RealEstateData.loadDataFromCSV() para carregar o dataset, que agora retorna ScaledData contendo os dados já normalizados e os parâmetros de escala.
- Recuperar Parâmetros de Escala: Recupera as médias e desvios padrões para feature e target do objeto ScaledData.
- Treinar com Dados Normalizados: Instancia e treina o LinearRegressionModel utilizando os INDArrays de features e targets normalizados.
- Normalizar Features de Previsão: Para novos exemplos de áreas de imóveis a serem previstos, é crucial normalizar essas áreas utilizando os mesmos parâmetros de escala (média e desvio padrão da feature “Area” do dataset de treinamento), garantindo que as features de predição estejam na mesma escala dos dados de treinamento.
- Prever com Features Normalizadas: Realiza as previsões utilizando o modelo treinado e as features normalizadas. Importante: O modelo irá gerar previsões na escala normalizada.
- Denormalizar Previsões: Após obter as previsões normalizadas, denormalize-as para a escala original do preço, utilizando os parâmetros de escala do target “Preço” do dataset de treinamento. Isso garante que os preços previstos sejam retornados na escala de valor original (R$).
- Avaliação e Custo Final: Calcula e imprime o custo final (MSE) no dataset de treinamento (calculado com os dados normalizados).
Antes de executar Main.java
- Baixe o arquivo CSV do dataset com dados de imóveis na pasta src/main/resources do seu projeto Java.
- Mantenha as dependências ND4J e Deeplearning4j configuradas no pom.xml do seu projeto.
Execute Main.java e admire as previsões de preços de imóveis consistentes e o modelo de Regressão Linear em ação! 🚀🏠💰
--- Treinamento do Modelo ---
Iteração 0, Custo = 0,4997
Iteração 100, Custo = 0,4778
Iteração 200, Custo = 0,4598
Iteração 300, Custo = 0,4451
Iteração 400, Custo = 0,4330
Iteração 500, Custo = 0,4232
Iteração 600, Custo = 0,4151
Iteração 700, Custo = 0,4085
Iteração 800, Custo = 0,4031
Iteração 900, Custo = 0,3986
Iteração 1000, Custo = 0,3950
Iteração 1100, Custo = 0,3920
Iteração 1200, Custo = 0,3896
Iteração 1300, Custo = 0,3876
Iteração 1400, Custo = 0,3860
Iteração 1500, Custo = 0,3847
Iteração 1600, Custo = 0,3836
Iteração 1700, Custo = 0,3827
Iteração 1800, Custo = 0,3819
Iteração 1900, Custo = 0,3813
--- Avaliação e Previsões ---
--- Previsões de Preços de Imóveis ---
Área: 30.0 m², Preço Previsto: R$404309,94
Área: 85.0 m², Preço Previsto: R$576352,00
Área: 65.0 m², Preço Previsto: R$513791,25
Área: 130.0 m², Preço Previsto: R$717113,75
Custo Final (MSE) NORMALIZADO no Dataset de Treinamento: 0,3808
Parabéns! 🎉 Você construiu e treinou com sucesso um modelo de Regressão Linear em Java para prever preços de imóveis usando dados reais! Você viu o Gradiente Descendente em ação, minimizando o custo e aprendendo a relação linear entre a área e o preço dos imóveis!
Próximos passos e expandindo a previsão de imóveis com IA em Java… 🚀🏡
Neste artigo, demos um grande salto e construímos um modelo prático de previsão de preços de imóveis com Regressão Linear em Java, utilizando dados reais. Você aprendeu a carregar dados de CSV, implementar um modelo de Regressão Linear com Gradiente Descendente (simplificado), treinar o modelo e fazer previsões!
No próximo post, vamos explorar algoritmos de Clustering (Agrupamento) com KMeans em Java! Vamos descobrir como o KMeans pode ser usado para segmentar clientes, agrupar dados e encontrar padrões ocultos em datasets! Prepare-se para mais aprendizado prático e novos horizontes da Inteligência Artificial em Java! 🧙♂️📊
Gostou deste post? Compartilhe com seus amigos desenvolvedores Java que também querem dominar a Inteligência Artificial! Deixe seu comentário abaixo com dúvidas, sugestões e ideias para os próximos posts da série. Vamos construir juntos essa comunidade de IA em Java! 💪