为了掌握PyTorch(以及一般的深度学习),我开始通过一些基本的分类示例进行学习。其中一个示例是使用sklearn创建的非线性数据集进行分类(完整代码可作为笔记本在此处获取)
n_pts = 500X, y = datasets.make_circles(n_samples=n_pts, random_state=123, noise=0.1, factor=0.2)x_data = torch.FloatTensor(X)y_data = torch.FloatTensor(y.reshape(500, 1))
然后使用一个相当基本的神经网络准确地对其进行分类
class Model(nn.Module): def __init__(self, input_size, H1, output_size): super().__init__() self.linear = nn.Linear(input_size, H1) self.linear2 = nn.Linear(H1, output_size) def forward(self, x): x = torch.sigmoid(self.linear(x)) x = torch.sigmoid(self.linear2(x)) return x def predict(self, x): pred = self.forward(x) if pred >= 0.5: return 1 else: return 0
由于我对健康数据感兴趣,我决定尝试使用相同的网络结构来对一些基本的现实世界数据集进行分类。我从这里获取了一位患者的心率数据,并对其进行了修改,使所有大于91的值被标记为异常(例如,标记为1
,而所有小于或等于91的值标记为0
)。这完全是任意的,但我只是想看看分类效果如何。这个示例的完整笔记本在这里。
让我感到不直观的是,为什么第一个示例在1,000个epoch后达到0.0016的损失,而第二个示例在10,000个epoch后仅达到0.4296的损失
或许我天真地认为心率示例会更容易分类。任何能帮助我理解为什么我没有看到这种情况的见解都将非常有帮助!
回答:
TL;DR
你的输入数据未经归一化处理。
- 使用
x_data = (x_data - x_data.mean()) / x_data.std()
- 增加学习率
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
仅需1000次迭代即可收敛。
更多细节
你所提供的两个示例之间的关键区别在于,第一个示例中的数据x
围绕(0, 0)居中,并且方差非常低。
而第二个示例中的数据围绕92居中,并且方差相对较大。
在你随机初始化权重时,这种数据的初始偏差并未被考虑到,这是基于输入大致围绕零的正态分布的假设进行的。
优化过程几乎不可能补偿这种巨大的偏差 – 因此模型陷入了一个次优解中。
一旦你通过减去均值并除以标准差来归一化输入,优化过程再次变得稳定,并迅速收敛到一个好的解。
关于输入归一化和权重初始化的更多细节,你可以阅读He et al的深入研究整流器:在ImageNet分类上超越人类水平表现(ICCV 2015)中的第2.2节。
如果我无法归一化数据怎么办?
如果由于某些原因,你无法提前计算均值和标准差数据,你仍然可以使用nn.BatchNorm1d
来估计并在训练过程中归一化数据。例如
class Model(nn.Module): def __init__(self, input_size, H1, output_size): super().__init__() self.bn = nn.BatchNorm1d(input_size) # 添加批归一化 self.linear = nn.Linear(input_size, H1) self.linear2 = nn.Linear(H1, output_size) def forward(self, x): x = torch.sigmoid(self.linear(self.bn(x))) # 对输入x进行批归一化 x = torch.sigmoid(self.linear2(x)) return x
这种修改在不改变输入数据的情况下,仅在1000个epoch后就达到了类似的收敛:
一个小评论
为了数值稳定性,最好使用nn.BCEWithLogitsLoss
而不是nn.BCELoss
。为此,你需要从forward()
输出中移除torch.sigmoid
,sigmoid
将在损失内部计算。
例如,关于二元预测的相关sigmoid + 交叉熵损失,请参见这个线程。