在分割训练和测试数据之前还是之后对数据进行规范化?

78

我想将我的数据分为训练集和测试集,是在拆分之前还是之后对数据进行归一化?对于构建预测模型是否有任何影响?

4个回答

136

首先,您需要将数据分为训练集和测试集(验证集也可能有用)。

不要忘记,测试数据点代表真实世界的数据。 对解释变量(或预测变量)进行特征归一化(或数据标准化)是一种技术,通过减去均值并除以方差来居中和规范化数据。如果您使用整个数据集的平均值和方差,将向训练解释变量(即平均值和方差)引入未来信息。

因此,您应该在训练数据上执行特征归一化。然后再次对测试实例进行归一化,但这次使用训练解释变量的均值和方差。通过这种方式,我们可以测试和评估模型是否能够很好地推广到新的、未知的数据点。

如需更全面的阅读,请阅读我的文章"Feature Scaling and Normalisation in a nutshell"


例如,假设我们有以下数据:

>>> import numpy as np
>>> 
>>> X, y = np.arange(10).reshape((5, 2)), range(5)

其中X代表我们的特征:

>>> X
[[0 1]
 [2 3]
 [4 5]
 [6 7]
 [8 9]]

并且Y包含对应的标签

>>> list(y)
>>> [0, 1, 2, 3, 4]

步骤1:创建训练/测试集

>>> X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33, random_state=42)

>>> X_train
[[4 5]
 [0 1]
 [6 7]]
>>>
>>> X_test
[[2 3]
 [8 9]]
>>>
>>> y_train
[2, 0, 3]
>>>
>>> y_test
[1, 4]

步骤 2:规范化训练数据

>>> from sklearn import preprocessing
>>> 
>>> normalizer = preprocessing.Normalizer()
>>> normalized_train_X = normalizer.fit_transform(X_train)
>>> normalized_train_X
array([[0.62469505, 0.78086881],
       [0.        , 1.        ],
       [0.65079137, 0.7592566 ]])

第三步:规范化测试数据

>>> normalized_test_X = normalizer.transform(X_test)
>>> normalized_test_X
array([[0.5547002 , 0.83205029],
       [0.66436384, 0.74740932]])

7
我仍对此感到困惑。在机器学习圣经《统计学习基础》中,它说在分离数据前进行任何形式的无监督预处理是可以的。其论点是,由于您没有使用标签,因此不会给您的估算器带来偏差。此外,任何ML模型的基本假设是,训练、验证和测试拆分样本都来自同一种族。因此,人口平均值(或方差或任何矩)是唯一的,无论我们使用整个可用数据集还是其中的子集来估算它,都只会影响我们的估算有多精确。 - Michael
2
但是我也理解另一个“实际”的观点,即在现实世界中我们没有访问测试集的权限,因此我们甚至不应该使用它来计算总体均值等。 - Michael
2
这个答案中的代码并没有执行它所描述的操作——减去平均值或除以方差。相反,它计算每行的长度(L2范数),并将行中的每个元素除以该长度。很容易检查:第一列的平均值为10/3,但转换后的数据在第一列的第二行中没有负数(0-10/3)/(某个正数)。但我们可以检查每一行,并看到平方元素的总和为1。文档也是如此说明的。https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.normalize.html - Sycorax
3
最后,由于L2范数仅逐行应用,因此训练/测试的区分是无关紧要的。在调用preprocessing.Normalizer()时,不使用任何有关训练数据的信息进行transform。它只需要提供给transform的数据。 - Sycorax
3
请注意,这个答案关于数据居中和缩放以及训练/测试集的说法基本上是正确的(尽管通常是除以标准差而不是方差);以这种方式进行预处理可以显著提高基于梯度的优化器的速度。但是所提供的代码并没有按照文本描述的方式对数据进行居中或缩放。 - Sycorax
显示剩余3条评论

8
在特定的训练/测试分离场景中,我们需要区分两种变换:
1. 根据特征(列)信息改变观察值(行)值的变换 2. 根据仅有该观察值本身的信息改变观察值的变换
(1)的两个常见例子是均值中心化(减去特征的平均值)或缩放到单位方差(除以标准差)。在Scikit-learn库中,这种变换是在sklearn.preprocessing.StandardScaler中实现的。重要的是,这与Normalizer不同,请参阅下面的详细信息。
(2)的一个例子是通过取对数或对每个值进行幂运算(如平方)来转换特征。
第一类变换最好应用于训练集数据上,并保留中心化和缩放值,然后应用于测试集数据上。这是因为使用测试集数据来训练模型可能会使模型比较指标过于乐观地偏向某个模型,从而导致过度拟合和选择虚假模型。
第二类变换可以不考虑训练/测试分离,因为每个观察值的修改值仅取决于观察值本身的数据,并不依赖于任何其他数据或观察值。
这个问题引起了一些误导性答案。接下来的回答专门解释了它们的误导性之处。在没有明确表述“正常化”意义的情况下,最好以最通用的方式看待问题。即使意图是询问Normalizer,答案也会误导,因为它们错误地描述了Normalizer的功能。即使在同一个库中,术语的使用也可能不一致。例如,PyTorch实现了normalize torchvision.transforms.Normalizetorch.nn.functional.normalize。其中之一可用于创建具有平均值为0和标准差为1的输出张量,而另一个则创建具有范数为1的输出。

Normalizer类的作用

Normalizer类是(2)的一个示例,因为它会对每个观测值(行)进行重新缩放,使得每行的平方和为1。如果一行的平方和等于0,则不进行重新缩放。在Normalizer文档的第一句话中说:

将样本单独归一化为单位范数。

这个简单的测试代码可以验证这个理解:

X = np.arange(10).reshape((5, 2))
normalizer = preprocessing.Normalizer()
normalized_all_X = normalizer.transform(X)
sum_of_squares = np.square(normalized_all_X).sum(1)
print(np.allclose(sum_of_squares,np.ones_like(sum_of_squares)))

这将打印True,因为结果是一组1,正如文档中所述。

即使其中一些方法只是“传递”方法,标准化程序实现了fittransformfit_transform方法。这是为了在预处理方法之间有一致的接口,而不是因为方法的行为需要区分不同的数据分区。


误导性演示 1

Normalizer类不会减去列平均值

另一个答案写道:

不要忘记测试数据点代表真实世界的数据。特征归一化(或数据标准化)是一种通过减去平均值并除以方差来居中和规范化数据的技术。

好的,让我们试试这个。使用答案中的代码片段,我们有

X = np.arange(10).reshape((5, 2))

X_train = X[:3]
X_test = X[3:]

normalizer = preprocessing.Normalizer()
normalized_train_X = normalizer.fit_transform(X_train)
column_means_train_X = normalized_train_X.mean(0)

这是“column_means_train_X”的值。它不为零!
[0.42516214 0.84670847]

如果从列中减去了列平均值,则居中的列平均值将为0.0(这很容易证明。 n 数字的和 x=[x1,x2,x3,...,xn]S,这些数字的平均值是 S / n。然后我们有 sum(x - S/n) = S - n * (S / n) = 0)。
我们可以编写类似的代码来表明列没有被除以方差。(也没有将列除以更常见的标准差)。
误导性展示2
Normalizer类应用于整个数据集不会改变结果。
如果你取整个数据集的平均值和方差,你将在训练解释变量(即均值和方差)中引入未来的信息。
这一说法只要是正确的就可以,但它与Normalizer类完全无关。事实上,Giorgos Myrianthous选择的例子实际上对他们所描述的效果是免疫的。
如果Normalizer类确实涉及特征的均值,那么我们预期归一化结果将根据包括在训练集中的哪些数据而发生变化。
例如,样本均值是样本中每个观察值的加权和。如果我们计算列平均值并将其减去,则将其应用于所有数据的结果将与仅应用于训练数据子集的结果不同。但是我们已经证明了Normalizer不会减去列平均值。
此外,这些测试表明将Normalizer应用于所有数据或仅应用于一些数据对结果没有影响。
如果我们分别应用这种方法,我们有
[[0.         1.        ]
 [0.5547002  0.83205029]
 [0.62469505 0.78086881]]

[[0.65079137 0.7592566 ]
 [0.66436384 0.74740932]]

如果我们结合应用,就会得到

[[0.         1.        ]
 [0.5547002  0.83205029]
 [0.62469505 0.78086881]
 [0.65079137 0.7592566 ]
 [0.66436384 0.74740932]]

唯一的区别是,在第一个情况下,我们有2个数组,这是由于分区造成的。让我们再次确认合并后的数组相同:

normalized_train_X = normalizer.fit_transform(X_train)
normalized_test_X = normalizer.transform(X_test)
normalized_all_X = normalizer.transform(X)
assert np.allclose(np.vstack((normalized_train_X, normalized_test_X)),normalized_all_X )

没有引发任何异常; 它们在数值上是相同的。

但是sklearn的转换器有时是有状态的,因此让我们创建一个新对象,只是为了确保这不是与状态相关的行为。

new_normalizer = preprocessing.Normalizer()
new_normalized_all_X = new_normalizer.fit_transform(X)
assert np.allclose(np.vstack((normalized_train_X, normalized_test_X)),new_normalized_all_X )

在第二种情况下,我们仍然没有引发任何异常。
我们可以得出结论,对于“规范化器”类,数据是否被分区并没有区别。

是的,此外还要补充一点的是,Normalizer.fit()方法是为了在Scikit-Learn中提供这些预处理器的统一API而提供的语法糖 - 如果你查看源代码,它只是一个传递方法。 - Greenstick
@Greenstick 是的,你说得对。在这个演示中,我决定专注于测试方法的输出,而不是审查代码本身,因为输出是立即可验证的。换句话说,我想建立预期行为,并使用可重复的示例进行验证,因为这种类型的结果对于初学者来说更容易理解,而深入源代码可能会让初学者感到害怕或难以理解。 - Sycorax

4
您可以使用“fit then transform”进行学习。
normalizer = preprocessing.Normalizer().fit(xtrain)

变换

xtrainnorm = normalizer.transform(xtrain) 
xtestnorm = normalizer.transform(Xtest) 

这种方法与此答案相符:https://datascience.stackexchange.com/a/54909/80221 - Kermit
还有sklearn预处理文档:https://scikit-learn.org/stable/modules/preprocessing.html#standardization-or-mean-removal-and-variance-scaling - Kermit
这个问题是在问“我应该在数据分割之前还是之后应用规范化?” 这个答案根本没有回答那个问题,它只提供了一小段代码片段。这段代码解决了什么问题?它与问题有什么关系?它如何处理你应该对不同的分区应用 fittransform 的问题?这个问题没有提到 Python、sklearn 或 Normalizer。为什么这个答案假定它是关于这个特定类的呢? - Sycorax
@Sycorax,代码确实是这样做的吗?从分割的数据xtrain创建了规范化器,然后使用该规范化器来转换训练和测试数据? - Kozlov
@Kozlov 这个问题问的是“在分割数据之前还是之后应该对数据进行归一化处理?这是否会对构建预测模型有任何影响?” 一个代码片段无法回答第一个问题,因为代码不知道代码是否犯了统计错误。它也无法回答第二个问题,因为它没有评论是否对模型有任何影响。 - Sycorax

3

问自己,如果在分割数据之前或之后进行转换,你的数据是否会有不同。如果正在进行log2转换,则顺序无关紧要,因为每个值都是独立转换的。如果正在对数据进行缩放和居中处理,则顺序很重要,因为异常值可能会极大地改变最终的分布。这将导致测试集"溢出"并影响训练集,从而可能导致过于乐观的性能指标。

对于R用户,caret包可很好处理测试/训练拆分。您可以向train函数添加参数preProcess = c("scale", "center"),它将自动将任何转换从训练数据应用到测试数据。

Tl;dr - 如果数据在归一化之前或之后产生了差异,请在拆分之前进行处理。


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