与使用Adam优化器的相同Keras模型相比,PyTorch的误差高出400%。

44

简述:

一个使用PyTorch训练的简单(单层隐藏层)前馈模型,用于预测函数y = sin(X1) + sin(X2) + ... sin(X10),其性能明显不如使用Keras构建/训练的相同模型。为什么会这样,并且有什么方法可以缓解性能差异?


在训练回归模型时,我注意到PyTorch与使用Keras构建的相同模型相比性能明显下降。

此现象以前已经被观察和报告过:

以下解释和建议以前也已经提出过:

  1. 使用相同的小数位精度(32位与64位):12

  2. 使用CPU而非GPU: 12

  3. 在使用autograd.grad计算二阶导数时,将retain_graph=True更改为create_graph=True: 1

  4. 检查Keras是否以不同的方式使用了正则化器、约束器、偏差或损失函数: 12

  5. 确保以相同的方式计算验证损失: 1

  6. 使用相同的初始化方法:12

  7. 更长时间地训练PyTorch模型:1

  8. 尝试多个随机种子:1

  9. 确保在训练PyTorch模型时,在验证步骤中调用model.eval()1

  10. 主要问题出在Adam优化器上,而不是初始化:1

为了理解这个问题,我在Keras和PyTorch中使用了一个简单的两层神经网络(比我的原始模型要简单得多),使用相同的超参数和初始化例程,并遵循上面列出的所有建议。然而,PyTorch模型的均方误差(MSE)比Keras模型高了400%。
以下是我的代码:
0. 导入
import numpy as np
from scipy.stats import pearsonr

from sklearn.preprocessing import MinMaxScaler
from sklearn import metrics

from torch.utils.data import Dataset, DataLoader

import tensorflow as tf
from tensorflow.keras import layers
from tensorflow.keras.regularizers import L2
from tensorflow.keras.models import Model
from tensorflow.keras.optimizers import Adam

1. 生成可再现的数据集


def get_data():

    np.random.seed(0)
    Xtrain = np.random.normal(0, 1, size=(7000,10))
    Xval = np.random.normal(0, 1, size=(700,10))
    ytrain = np.sum(np.sin(Xtrain), axis=-1)
    yval = np.sum(np.sin(Xval), axis=-1)
    scaler = MinMaxScaler()
    ytrain = scaler.fit_transform(ytrain.reshape(-1,1)).reshape(-1)
    yval = scaler.transform(yval.reshape(-1,1)).reshape(-1) 

    return Xtrain, Xval, ytrain, yval



class XYData(Dataset):
    
    def __init__(self, X, y):
        
        super(XYData, self).__init__()
        self.X = torch.tensor(X, dtype=torch.float32)
        self.y = torch.tensor(y, dtype=torch.float32)
        self.len = len(y)
         
    def __getitem__(self, index):
        
        return (self.X[index], self.y[index])


    def __len__(self):

        return self.len

# Data, dataset, and dataloader
Xtrain, Xval, ytrain, yval = get_data()
traindata = XYData(Xtrain, ytrain)
valdata = XYData(Xval, yval)
trainloader = DataLoader(dataset=traindata, shuffle=True, batch_size=32, drop_last=False)
valloader = DataLoader(dataset=valdata, shuffle=True, batch_size=32, drop_last=False)

2. 使用相同的超参数和初始化方法构建Keras和PyTorch模型

class TorchLinearModel(nn.Module):
    
    def __init__(self, input_dim=10, random_seed=0):
        
        super(TorchLinearModel, self).__init__()
        _ = torch.manual_seed(random_seed)
        self.hidden_layer = nn.Linear(input_dim,100)
        self.initialize_layer(self.hidden_layer)        
        self.output_layer = nn.Linear(100, 1)
        self.initialize_layer(self.output_layer)

    def initialize_layer(self, layer):
        
        _ = torch.nn.init.xavier_normal_(layer.weight)
        #_ = torch.nn.init.xavier_uniform_(layer.weight)
        _ = torch.nn.init.constant(layer.bias,0)
        
    def forward(self, x):
        x = self.hidden_layer(x)
        x = self.output_layer(x)
        return x




def mean_squared_error(ytrue, ypred):
    
    return torch.mean(((ytrue - ypred) ** 2))


def build_torch_model():

    torch_model = TorchLinearModel()
    optimizer = optim.Adam(torch_model.parameters(), 
                           betas=(0.9,0.9999),
                           eps=1e-7,
                           lr=1e-3,
                           weight_decay=0)
    return torch_model, optimizer




def build_keras_model():
    
    x = layers.Input(shape=10)
    z = layers.Dense(units=100, activation=None, use_bias=True, kernel_regularizer=None, 
                     bias_regularizer=None)(x)
    y = layers.Dense(units=1, activation=None, use_bias=True, kernel_regularizer=None, 
                     bias_regularizer=None)(z)
    keras_model = Model(x, y, name='linear')
    optimizer = Adam(learning_rate=1e-3, beta_1=0.9, beta_2=0.9999, epsilon=1e-7, 
                     amsgrad=False)
    
    keras_model.compile(optimizer=optimizer, loss='mean_squared_error')
    
    return keras_model




# Instantiate models
torch_model, optimizer = build_torch_model()
keras_model = build_keras_model()


Great! How may I assist you today?

torch_trainlosses, torch_vallosses = [], []

for epoch in range(100):

    # Training
    losses = []
    _ = torch_model.train()
    
    for i, (x,y) in enumerate(trainloader):
        optimizer.zero_grad()                          
        ypred = torch_model(x)
        loss = mean_squared_error(y, ypred) 
        _ = loss.backward()
        _ = optimizer.step()
        losses.append(loss.item())
    torch_trainlosses.append(np.mean(losses))
    
    # Validation
    losses = []
    _ = torch_model.eval()

    with torch.no_grad():
        for i, (x, y) in enumerate(valloader):
            ypred = torch_model(x)
            loss = mean_squared_error(y, ypred) 
            losses.append(loss.item())
    torch_vallosses.append(np.mean(losses))
    
    print(f"epoch={epoch+1}, train_loss={torch_trainlosses[-1]:.4f}, val_loss={torch_vallosses[-1]:.4f}")
    

4. 训练 Keras 模型 100 个 epochs:

history = keras_model.fit(Xtrain, ytrain, sample_weight=None, batch_size=32, epochs=100, 
                    validation_data=(Xval, yval))

5. 训练历史中的损失

plt.plot(torch_trainlosses, color='blue', label='PyTorch Train')    
plt.plot(torch_vallosses, color='blue', linestyle='--', label='PyTorch Val')  
plt.plot(history.history['loss'], color='brown', label='Keras Train')
plt.plot(history.history['val_loss'], color='brown', linestyle='--', label='Keras Val')
plt.legend()

enter image description here

Keras在训练中记录了更低的误差。由于这可能是由于Keras计算损失的方式不同,我使用sklearn.metrics.mean_squared_error计算了验证集上的预测误差。

6. 训练后的验证误差

ypred_keras = keras_model.predict(Xval).reshape(-1)
ypred_torch = torch_model(torch.tensor(Xval, dtype=torch.float32))
ypred_torch = ypred_torch.detach().numpy().reshape(-1)


mse_keras = metrics.mean_squared_error(yval, ypred_keras)
mse_torch = metrics.mean_squared_error(yval, ypred_torch)
print('Percent error difference:', (mse_torch / mse_keras - 1) * 100) 

r_keras = pearsonr(yval, ypred_keras)[0] 
r_pytorch = pearsonr(yval, ypred_torch)[0]  
print("r_keras:", r_keras)
print("r_pytorch:", r_pytorch)

plt.scatter(ypred_keras, yval); plt.title('Keras'); plt.show(); plt.close()
plt.scatter(ypred_torch, yval); plt.title('Pytorch'); plt.show(); plt.close()

Percent error difference: 479.1312469426776
r_keras: 0.9115184443702814
r_pytorch: 0.21728812737220082

enter image description here enter image description here

Keras的预测值与实际值的相关性为0.912,而Pytorch的相关性仅为0.217,误差高出479%!

7.其他尝试 我还尝试过:

  • 降低Pytorch的学习率(lr = 1e-4),R从0.217增加到0.576,但仍远不如Keras(r = 0.912)。
  • 增加Pytorch的学习率(lr = 1e-2),R更糟糕,只有0.095
  • 使用不同的随机种子进行多次训练。总体表现大致相同
  • 训练时间超过100个epoch。没有观察到改善!
  • 在权重初始化时使用torch.nn.init.xavier_uniform_而不是torch.nn.init.xavier_normal_。R 从0.217提高到0.639,但仍不如Keras(0.912)。

有什么方法可以确保PyTorch模型收敛到与Keras模型相当的合理误差?



5
使用pytorchMSELoss时会发出警告:UserWarning: 使用目标大小(torch.Size([32, 1]))与输入大小(torch.Size([32]))不同。这可能会由于广播导致结果不正确。一些运行提供的[mre]所需的导入缺失(但很明显)。 - Michael Szczesny
4
非常棒的问题,写得很好,研究得也非常透彻!此外:这是第n个例子,“看似不可能的奇怪问题-->必然是代码中一个极其微不足道的问题,其他人只需要花不到5分钟就可以发现和修复”...这种情况经常发生。 - GACy20
1个回答

68
这里的问题是在PyTorch训练循环中无意中进行广播。
nn.Linear操作的结果始终具有形状[B, D],其中B是批处理大小,D是输出维度。因此,在您的mean_squared_error函数中,ypred的形状为[32,1],而ytrue的形状为[32]。根据NumPy和PyTorch使用的广播规则,这意味着ytrue - ypred的形状为[32,32]。您几乎肯定想要ypred的形状为[32]。可以通过多种方式实现这一点;可能最易读的方法是使用Tensor.flatten
class TorchLinearModel(nn.Module):
    ...
    def forward(self, x):
        x = self.hidden_layer(x)
        x = self.output_layer(x)
        return x.flatten()

这将生成以下训练/验证曲线:

enter image description here


1
当我看到像这样的答案时,我会嫉妒其他一些人有多么聪明。 - Contango

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接