PyTorch实现SGD的系数与OLS不匹配,问题出在哪?
问题:SGD拟合结果与OLS系数差距大的原因及修正
问题背景
业务场景中需要用SGD得到接近普通最小二乘法(OLS)的回归系数,但当前分别用OLS、未缩放数据的SGD、缩放数据的SGD拟合后,系数差异极大,需要定位原因并修正。
核心原因分析
1. 模型设定完全不匹配
OLS使用的是无截距模型(公式中-1),特征是x1到x16共16个变量;但原PyTorch代码中:
- 输入
X取的是df.iloc[:,2:],跳过了原始数据中的x1列,只用到x2到x16 - 模型
nn.Linear默认添加了截距(bias),相当于把OLS中的x1替换成了模型截距,其他特征对应错位,自然系数完全不对
2. 缩放后系数未做逆转换
用StandardScaler缩放特征后,SGD学到的系数是针对标准化后特征的,不能直接和OLS(基于原始特征)的系数对比,必须将系数转换回原始特征空间才能对齐。
3. SGD训练参数不合理
- 学习率
0.1过大:原始特征量级差异极大(比如x1是-0.2左右,x7可达几千),大学习率会导致参数更新震荡,无法收敛到最优解 - 迭代次数不足:5000次迭代对于这种病态条件数(OLS结果中Cond. No.为2.14e+07,说明特征高度共线性)的回归问题,可能不够收敛
- 未针对病态数据做优化:高条件数的回归问题,SGD需要更小的学习率或正则化辅助收敛
修正方案与代码
步骤1:对齐模型设定
去掉PyTorch模型的截距,输入包含全部16个特征(x1到x16),和OLS保持一致。
步骤2:处理缩放后系数的逆转换
对标准化后的特征系数,用Scaler的均值和标准差转换回原始特征的系数。
步骤3:优化SGD训练参数
降低学习率,增加迭代次数,添加学习率衰减辅助收敛。
以下是修正后的完整代码:
import pandas as pd from statsmodels.formula.api import ols import torch.optim as optim from sklearn.preprocessing import StandardScaler import torch import torch.nn as nn # 1. OLS拟合(保持原逻辑) df = pd.read_csv('https://gist.githubusercontent.com/alexhallam/820baf95a6f29f8d032dadd384dc4f6b/raw/b3e6a6829a8ce94349acfc775801d426fd05d08b/test_ols_sgd_data.csv') fit = ols('z ~ x1 + x2 + x3 + x4 + x5 + x6 + x7 + x8 + x9 + x10 + x11 + x12 + x13 + x14 + x15 + x16 - 1', data=df).fit() print("OLS系数:") print(fit.params) # 2. 修正后的PyTorch线性回归模型(无截距) class LinearRegressionNoBias(nn.Module): def __init__(self, input_dim): super().__init__() self.linear = nn.Linear(input_dim, 1, bias=False) # 无截距,和OLS对齐 self.linear.weight = nn.Parameter(self.linear.weight.to(torch.float64)) def forward(self, x): return self.linear(x) def fit_by_SGD(X, y, scaler=None, learning_rate=1e-5, num_epochs=50000, weight_decay=1e-4, debug=True): input_dim = X.shape[1] model = LinearRegressionNoBias(input_dim) criterion = nn.MSELoss() # 添加学习率衰减,优化收敛过程 optimizer = optim.SGD(model.parameters(), lr=learning_rate, weight_decay=weight_decay) scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=10000, gamma=0.5) prev_loss = float('inf') for epoch in range(num_epochs): outputs = model(X.to(torch.float64)) loss = criterion(outputs, y.view(-1, 1)) # 确保y的形状与输出匹配 optimizer.zero_grad() loss.backward() optimizer.step() scheduler.step() # 收敛判断 if abs(loss.item() - prev_loss) < 1e-10: print(f"已收敛,迭代次数:{epoch+1}") break prev_loss = loss.item() coeffs = model.linear.weight.data.view(-1).numpy() # 如果是缩放后的特征,转换回原始特征空间的系数 if scaler is not None: coeffs = coeffs / scaler.scale_ if debug: x_names = [f'x{i+1}' for i in range(input_dim)] print("\nSGD拟合系数:") print(pd.DataFrame({'特征': x_names, '系数': coeffs})) return coeffs # 3. 基于原始数据的SGD拟合 X_raw = torch.tensor(df.iloc[:, 1:17].values) # 取x1到x16,共16个特征 y = torch.tensor(df.iloc[:, 0].values, dtype=torch.float64) print("\n=== 原始数据SGD拟合 ===") sgd_raw_coeffs = fit_by_SGD(X_raw, y) # 4. 基于缩放数据的SGD拟合并转换系数 scaler = StandardScaler() X_scaled_np = scaler.fit_transform(df.iloc[:, 1:17].values) X_scaled = torch.tensor(X_scaled_np) print("\n=== 缩放数据SGD拟合(转换回原始系数) ===") sgd_scaled_coeffs = fit_by_SGD(X_scaled, y, scaler=scaler) # 5. 对比OLS和SGD系数 print("\n=== 系数对比 ===") compare_df = pd.DataFrame({ 'OLS系数': fit.params.values, '原始数据SGD系数': sgd_raw_coeffs, '缩放数据SGD系数': sgd_scaled_coeffs }, index=fit.params.index) print(compare_df)
修正后的效果
- 模型设定对齐后,SGD的系数会和OLS高度接近
- 缩放数据的系数经过逆转换后,也能匹配OLS结果
- 优化后的SGD参数(小学习率+学习率衰减+正则化)能有效处理高条件数的回归问题,收敛到最优解
内容的提问来源于stack exchange,提问作者Alex
相关产品推荐
相关产品推荐

