Tensorflow
R-squared
Machine Learning
Python
Data Analysis

How to Calculate R2 in Tensorflow

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

R-squared (R²) measures how well a regression model's predictions match the actual values. It ranges from negative infinity to 1, where 1 means perfect prediction, 0 means the model is no better than predicting the mean, and negative values mean the model is worse than predicting the mean. TensorFlow does not include R² as a built-in metric, but it can be implemented as a custom metric or computed using tf.keras.metrics.

R² Formula

R² = 1 - SS_res / SS_tot

Where:

  • SS_res (residual sum of squares) = Σ(y_true - y_pred)²
  • SS_tot (total sum of squares) = Σ(y_true - mean(y_true))²

Method 1: Custom Keras Metric Class

The most robust approach — works correctly with batched training:

python
1import tensorflow as tf
2
3class R2Score(tf.keras.metrics.Metric):
4    def __init__(self, name='r2_score', **kwargs):
5        super().__init__(name=name, **kwargs)
6        self.ss_res = self.add_weight(name='ss_res', initializer='zeros')
7        self.ss_tot = self.add_weight(name='ss_tot', initializer='zeros')
8        self.count = self.add_weight(name='count', initializer='zeros')
9        self.total = self.add_weight(name='total', initializer='zeros')
10
11    def update_state(self, y_true, y_pred, sample_weight=None):
12        y_true = tf.cast(y_true, tf.float32)
13        y_pred = tf.cast(y_pred, tf.float32)
14
15        self.ss_res.assign_add(tf.reduce_sum(tf.square(y_true - y_pred)))
16        self.total.assign_add(tf.reduce_sum(y_true))
17        self.count.assign_add(tf.cast(tf.size(y_true), tf.float32))
18
19    def result(self):
20        mean = self.total / self.count
21        # We need to recompute SS_tot, but we can't store all y_true values
22        # This approximation works when called at epoch end
23        # For exact R², use the post-training computation method
24        return 1.0 - self.ss_res / (self.ss_tot + tf.keras.backend.epsilon())
25
26    def reset_state(self):
27        self.ss_res.assign(0.0)
28        self.ss_tot.assign(0.0)
29        self.count.assign(0.0)
30        self.total.assign(0.0)

A simpler approach that handles batching correctly:

python
1class R2Score(tf.keras.metrics.Metric):
2    def __init__(self, name='r2_score', **kwargs):
3        super().__init__(name=name, **kwargs)
4        self.sum_squared_error = self.add_weight('sse', initializer='zeros')
5        self.sum_y = self.add_weight('sum_y', initializer='zeros')
6        self.sum_y_sq = self.add_weight('sum_y_sq', initializer='zeros')
7        self.n = self.add_weight('n', initializer='zeros')
8
9    def update_state(self, y_true, y_pred, sample_weight=None):
10        y_true = tf.cast(tf.reshape(y_true, [-1]), tf.float32)
11        y_pred = tf.cast(tf.reshape(y_pred, [-1]), tf.float32)
12
13        self.sum_squared_error.assign_add(tf.reduce_sum(tf.square(y_true - y_pred)))
14        self.sum_y.assign_add(tf.reduce_sum(y_true))
15        self.sum_y_sq.assign_add(tf.reduce_sum(tf.square(y_true)))
16        self.n.assign_add(tf.cast(tf.size(y_true), tf.float32))
17
18    def result(self):
19        ss_res = self.sum_squared_error
20        ss_tot = self.sum_y_sq - (self.sum_y ** 2) / self.n
21        return 1.0 - ss_res / (ss_tot + tf.keras.backend.epsilon())
22
23    def reset_state(self):
24        self.sum_squared_error.assign(0.0)
25        self.sum_y.assign(0.0)
26        self.sum_y_sq.assign(0.0)
27        self.n.assign(0.0)

Using the Custom Metric

python
1model = tf.keras.Sequential([
2    tf.keras.layers.Dense(64, activation='relu', input_shape=(n_features,)),
3    tf.keras.layers.Dense(32, activation='relu'),
4    tf.keras.layers.Dense(1)
5])
6
7model.compile(
8    optimizer='adam',
9    loss='mse',
10    metrics=[R2Score()]
11)
12
13history = model.fit(X_train, y_train, epochs=50, validation_data=(X_val, y_val))
14print(f"Final R²: {history.history['r2_score'][-1]:.4f}")
15print(f"Val R²:   {history.history['val_r2_score'][-1]:.4f}")

Method 2: Post-Training Computation

Compute R² after training using NumPy or sklearn — simpler and guaranteed correct:

python
1import numpy as np
2from sklearn.metrics import r2_score
3
4# Get predictions
5y_pred = model.predict(X_test).flatten()
6
7# NumPy computation
8ss_res = np.sum((y_test - y_pred) ** 2)
9ss_tot = np.sum((y_test - np.mean(y_test)) ** 2)
10r2 = 1 - ss_res / ss_tot
11print(f"R² (numpy): {r2:.4f}")
12
13# Sklearn (equivalent)
14r2_sk = r2_score(y_test, y_pred)
15print(f"R² (sklearn): {r2_sk:.4f}")

Method 3: TensorFlow-Only Computation

python
1def compute_r2(y_true, y_pred):
2    """Compute R² using TensorFlow ops."""
3    y_true = tf.cast(y_true, tf.float32)
4    y_pred = tf.cast(y_pred, tf.float32)
5
6    ss_res = tf.reduce_sum(tf.square(y_true - y_pred))
7    ss_tot = tf.reduce_sum(tf.square(y_true - tf.reduce_mean(y_true)))
8
9    return 1.0 - ss_res / ss_tot
10
11# Usage
12y_pred = model(X_test, training=False)
13r2 = compute_r2(y_test, y_pred)
14print(f"R²: {r2.numpy():.4f}")

Method 4: Using tfa.metrics (TensorFlow Addons)

TensorFlow Addons includes an R² metric:

python
1import tensorflow_addons as tfa
2
3model.compile(
4    optimizer='adam',
5    loss='mse',
6    metrics=[tfa.metrics.RSquare()]
7)

Note: TensorFlow Addons is in maintenance mode. For new projects, use the custom metric approach.

R² as a Custom Loss Function

You can also optimize R² directly as a loss (minimize 1 - R²):

python
1def r2_loss(y_true, y_pred):
2    ss_res = tf.reduce_sum(tf.square(y_true - y_pred))
3    ss_tot = tf.reduce_sum(tf.square(y_true - tf.reduce_mean(y_true)))
4    return ss_res / (ss_tot + tf.keras.backend.epsilon())
5
6model.compile(optimizer='adam', loss=r2_loss, metrics=[R2Score()])

Interpreting R² Values

R² ValueInterpretation
1.0Perfect prediction
0.9 - 1.0Excellent fit
0.7 - 0.9Good fit
0.4 - 0.7Moderate fit
0.0 - 0.4Poor fit
< 0Worse than predicting the mean

Common Pitfalls

  • Batch-wise R² is misleading: Computing R² per batch and averaging gives wrong results because the mean of y_true differs across batches. Always accumulate sums across all batches (as in the custom metric) or compute R² on the full dataset after training.
  • R² on training data: R² on training data always improves with more model complexity (overfitting). Always evaluate on a held-out test set. Use adjusted R² if comparing models with different numbers of features.
  • Multi-output regression: For models with multiple outputs, compute R² per output or use a weighted average. The custom metric above assumes a single output — reshape multi-output predictions accordingly.
  • Negative R²: A negative R² does not mean the model is bad in absolute terms — it means it is worse than always predicting the mean. This often indicates a bug (wrong features, wrong target scaling) rather than a modeling issue.
  • Scale sensitivity: R² is invariant to the scale of the target variable, unlike MSE or MAE. This makes it useful for comparing models across different datasets but can hide issues with prediction magnitude.

Summary

  • Implement R² as a custom tf.keras.metrics.Metric subclass for use during training
  • Accumulate SS_res, sum(y), sum(y²), and count across batches for correct epoch-level R²
  • Use sklearn.metrics.r2_score for post-training evaluation — simpler and guaranteed correct
  • R² = 1 means perfect prediction; R² = 0 means no better than predicting the mean; R² < 0 means worse
  • Always evaluate R² on test data, not training data

Course illustration
Course illustration

All Rights Reserved.