1
$\begingroup$

I built a neural network using PyTorch to predict y (a continuous variable) based on X consisting of m (=20) features. I found that the residuals (y_predicted – y_true) for the test data set show a clear linear trend with the true value ‘y_ture’ .

In addition, I can observe a linear relation between y_predicted and y_true, with R2 equal to about 0.5. Another observation is that the model tends to overestimate lower values of y but underestimate larger values of y, leading to the value range of the predicted y is evidently smaller than that of the true values.

I have tried the following ways to avoid the linear trend in the residuals: 1) increased the complexity of the model, by increasing the number of hidden layers and neurons for hidden layers, 2) added dropout scheme, early-stopping criterion, and regularization term to avoid overfitting. But they did not work, and the linear trend still exists. I also tried to design a network that incorporates an additional component to learn the linear trend and added the predicted error back to the predicted value of y, but I did not know how to design it effectively. Is there anyone who can help me to remove the linear trend in the residuals and further enhance the model?

    import torch
    import torch.nn as nn
    import torch.optim as optim
    from torch.utils.data import Dataset, DataLoader
    import numpy as np
    from sklearn.model_selection import train_test_split
    from sklearn.metrics import r2_score
    import matplotlib.pyplot as plt

    # Define the custom dataset
    class CustomDataset(Dataset):
        def __init__(self, X, y):
            self.X = torch.FloatTensor(X)
            self.y = torch.FloatTensor(y).reshape(-1, 1)

        def __len__(self):
            return len(self.y)

        def __getitem__(self, idx):
            return self.X[idx], self.y[idx]

    # Define the neural network
    class NeuralNetwork(nn.Module):
        def __init__(self, input_size):
            super(NeuralNetwork, self).__init__()
            self.layers = nn.Sequential(
                nn.Linear(input_size, 64),
                nn.ReLU(),
                nn.Dropout(0.2),
                nn.Linear(64, 32),
                nn.ReLU(),
                nn.Dropout(0.2),
                nn.Linear(32, 16),
                nn.ReLU(),
                nn.Dropout(0.2),
                nn.Linear(16, 1)
            )

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

    # Function to train the model
    def train_model(model, train_loader, val_loader, criterion, optimizer, num_epochs, patience):
        best_val_loss = float('inf')
        epochs_no_improve = 0

        for epoch in range(num_epochs):
            model.train()
            for inputs, targets in train_loader:
                optimizer.zero_grad()
                outputs = model(inputs)
                loss = criterion(outputs, targets)
                loss.backward()
                optimizer.step()

            # Validation
            model.eval()
            val_loss = 0
            with torch.no_grad():
                for inputs, targets in val_loader:
                    outputs = model(inputs)
                    val_loss += criterion(outputs, targets).item()

            val_loss /= len(val_loader)

            if val_loss < best_val_loss:
                best_val_loss = val_loss
                epochs_no_improve = 0
                torch.save(model.state_dict(), 'best_model.pth')
            else:
                epochs_no_improve += 1

            if epochs_no_improve == patience:
                print(f"Early stopping triggered after {epoch + 1} epochs")
                break

        model.load_state_dict(torch.load('best_model.pth'))
        return model

    # Function to evaluate the model
    def evaluate_model(model, test_loader):
        model.eval()
        predictions = []
        actuals = []
        with torch.no_grad():
            for inputs, targets in test_loader:
                outputs = model(inputs)
                predictions.extend(outputs.numpy().flatten())
                actuals.extend(targets.numpy().flatten())

        r2 = r2_score(actuals, predictions)
        return r2, predictions, actuals

    # Function to plot residuals
    def plot_residuals(y_true, y_pred):
        residuals = y_pred - y_true
        plt.figure(figsize=(10, 5))
    
        plt.subplot(1, 2, 1)
        plt.scatter(y_true, residuals)
        plt.xlabel('True values')
        plt.ylabel('Residuals')
        plt.title('Residuals vs True values')
    
        plt.subplot(1, 2, 2)
        plt.scatter(y_pred, residuals)
        plt.xlabel('Predicted values')
        plt.ylabel('Residuals')
        plt.title('Residuals vs Predicted values')
    
        plt.tight_layout()
        plt.show()

    # Main execution
    def main():
        # Assuming you have your data in X and y
        # X = ... # Your input data (n_samples, 20)
        # y = ... # Your target data (n_samples,)

        # Split the data
        X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
        X_train, X_val, y_train, y_val = train_test_split(X_train, y_train, test_size=0.2,             random_state=42)

        # Create datasets and dataloaders
        train_dataset = CustomDataset(X_train, y_train)
        val_dataset = CustomDataset(X_val, y_val)
        test_dataset = CustomDataset(X_test, y_test)

        train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
        val_loader = DataLoader(val_dataset, batch_size=32)
        test_loader = DataLoader(test_dataset, batch_size=32)

        # Initialize the model
        model = NeuralNetwork(input_size=20)

        # Define loss function and optimizer
        criterion = nn.MSELoss()
        optimizer = optim.Adam(model.parameters(), lr=0.001, weight_decay=1e-5)  # L2 regularization

        # Train the model
        model = train_model(model, train_loader, val_loader, criterion, optimizer, num_epochs=100, patience=10)

        # Evaluate the model
        r2, predictions, actuals = evaluate_model(model, test_loader)
        print(f"R2 score on test set: {r2}")

        # Plot residuals
        plot_residuals(np.array(actuals), np.array(predictions))

    if __name__ == "__main__":
        main()

This is the resulting figure:

enter image description here

$\endgroup$
1
  • 2
    $\begingroup$ Many posts on site address this issue, which (along with almost any other model worth discussing) occurs with ordinary linear regression. You should expect that it will happen this way. Incidentally, residuals are more typically $y - \hat{y}$ rather than the way you have them (what's left in the data after you remove the fit, not what's left in the fit after you remove the data), and the resulting correlation positive. $\endgroup$
    – Glen_b
    Commented Jun 25 at 3:50

1 Answer 1

5
$\begingroup$

You should have linear correlation between $y$ and $y-\hat y$. What you don't want is linear correlation between $\hat y$ and $y-\hat y$.

Suppose you could get the best possible prediction from a set of features $X$, so that $\hat y=E[Y|X]$. The error, $y-\hat y$, would be completely uncorrelated with any function of $X$; that's what makes your prediction the best possible one. In that scenario, $\mathrm{cov}[\hat y, y-\hat y]$ is always zero, but $$\mathrm{cov}[y, y-\hat y]=\mathrm{cov}[y, y] -\mathrm{cov}[y,\hat y]=\mathrm{var}[y, y] -\mathrm{cov}[y,\hat y]$$ This is only zero if the prediction is perfect, ie, if $\hat y=y$ exactly, which would be nice but is not a reasonable hope.

$\endgroup$
2
  • $\begingroup$ Is it possible to learn this linear relation and use it to correct the initial model response? $\endgroup$
    – tunar
    Commented Jun 25 at 4:13
  • $\begingroup$ In general, no. You can try to improve your model by learning $y-\hat y$, which is basically what boosting does, but that's not this linear relationship. $\endgroup$ Commented Jun 25 at 23:59

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.