sklearn r2_score与PyTorch MSELoss计算R2得分存在差异的原因?
发现sklearn的r2_score()函数返回的R²得分,与通过PyTorch的MSELoss()结合statistics.variance()计算得到的结果存在细微差异,且sklearn的结果始终略低。以下是复现差异的代码:
from sklearn.metrics import r2_score from torch.nn import MSELoss import statistics import random import torch import numpy as np actuals = random.sample(range(1, 50), 40) preds = [] for value in actuals: pred = value * 0.70 preds.append(pred) loss = MSELoss() mse = loss(torch.tensor(preds), torch.tensor(actuals)) r2 = 1 - mse / statistics.variance(actuals) score = r2_score(actuals, preds) print(f'R2 Score using (PyTorch) MSELoss: {r2}') print(f'R2 Score using (sklearn) r2_score: {score}')
示例输出:
R2 Score using (PyTorch) MSELoss: 0.6261289715766907 R2 Score using (sklearn) r2_score: 0.6165425269729996
差异原因
这和张量输入无关,核心是样本方差与总体方差的计算区别:
statistics.variance()计算的是样本方差,分母为n-1(n是样本量);- PyTorch的
MSELoss默认计算的是平均MSE,分母为n; - sklearn的
r2_score严格遵循R²的定义:$R^2 = 1 - \frac{残差平方和}{总平方和}$,其中总平方和等于n * 总体方差(分母为n),残差平方和等于n * 平均MSE。
你的计算中,用了样本方差(分母n-1)除以平均MSE(分母n),导致最终结果被放大,公式推导如下:
$$
R^2_{你的计算} = 1 - \frac{\frac{1}{n}\sum(y-\hat{y})2}{\frac{1}{n-1}\sum(y-\bar{y})2} = 1 - \frac{n-1}{n} \cdot \frac{\sum(y-\hat{y})2}{\sum(y-\bar{y})2}
$$
而sklearn的计算是:
$$
R^2_{sklearn} = 1 - \frac{\sum(y-\hat{y})2}{\sum(y-\bar{y})2}
$$
两者的差异正好是$\frac{n-1}{n}$的比例,用你的示例数值验证:$(1 - 0.6261) * \frac{40}{39} ≈ 0.3835$,$1 - 0.3835 = 0.6165$,完全匹配sklearn的结果。
解决方法
只需让MSE和方差的计算分母保持一致即可:
方法1:使用总体方差代替样本方差
用np.var(actuals, ddof=0)计算总体方差(分母为n),和MSELoss的平均损失匹配:
from sklearn.metrics import r2_score from torch.nn import MSELoss import numpy as np import random import torch actuals = random.sample(range(1, 50), 40) preds = [val * 0.70 for val in actuals] loss = MSELoss() mse = loss(torch.tensor(preds), torch.tensor(actuals)) # 使用总体方差(ddof=0) r2 = 1 - mse / np.var(actuals, ddof=0) score = r2_score(actuals, preds) print(f'R2 Score using (PyTorch) MSELoss + 总体方差: {r2}') print(f'R2 Score using (sklearn) r2_score: {score}')
方法2:调整MSE计算为除以n-1
将PyTorch的MSELoss设置为求和模式,再除以n-1,和样本方差匹配:
from sklearn.metrics import r2_score from torch.nn import MSELoss import statistics import random import torch actuals = random.sample(range(1, 50), 40) preds = [val * 0.70 for val in actuals] loss = MSELoss(reduction='sum') mse_sum = loss(torch.tensor(preds), torch.tensor(actuals)) # MSE除以n-1 mse = mse_sum / (len(actuals)-1) r2 = 1 - mse / statistics.variance(actuals) score = r2_score(actuals, preds) print(f'R2 Score using (PyTorch) MSELoss(求和)+样本方差: {r2}') print(f'R2 Score using (sklearn) r2_score: {score}')
两种方法都能让PyTorch计算的R²和sklearn结果完全一致。
内容的提问来源于stack exchange,提问作者Pau Vila Soler

