python基础教程四级查数据_《Python机器学习基础教程》四、数据表示与特征工程...

*本章讨论了如何处理不同的数据类型(特别是分类变量)。强调了使用适合机器学习算法的数据表示的重要性,例如one-hot编码过的分类变量。还讨论了通过特征工程生成新特征的重要性,以及利用专家知识从数据中创建到处特征的可能性。特别是线性模型,可能会从分箱、添加多项式和交互项而生成的新特性中大大受益。对于更加复杂的非线性模型(比如随机森林和SVM),在无需显式扩展特征空间的前提下就可以学习更加复杂的任务。在实践中,所使用的特征(以及特征与方法之间的匹配)通常是机器学习方法变现良好的最重要的因素。

如何找到最佳数据表示,这个问题被称为特征工程(feature engineering)

一、分类变量

1.One-Hot编码(虚拟变量)

表示分类变量最常用的方法就是使用one-hot编码(one-hot-encoding)或N取一编码(one-out-of-N encoding),也叫虚拟变量(dummy variable)。虚拟变量背后的思想是将一个分类变量替换为一个或多个新特征,新特征取值为0和1。

将数据转换为分类变量的one-dot编码有两种方法:一种是使用pandas,一种是使用scikit-learn。

(1)使用pandas从逗号分隔值(CSV)文件中加载数据:

import os

# The file has no headers naming the columns, so we pass header=None

# and provide the column names explicitly in "names"

adult_path = os.path.join(mglearn.datasets.DATA_PATH, "adult.data")

data = pd.read_csv(

adult_path, header=None, index_col=False,

names=['age', 'workclass', 'fnlwgt', 'education', 'education-num',

'marital-status', 'occupation', 'relationship', 'race', 'gender',

'capital-gain', 'capital-loss', 'hours-per-week', 'native-country',

'income'])

# For illustration purposes, we only select some of the columns:

data = data[['age', 'workclass', 'education', 'gender', 'hours-per-week',

'occupation', 'income']]

# IPython.display allows nice output formatting within the Jupyter notebook

display(data.head())

(2)检查字符串编码的分类数据

检查列的内容有一个好方法,就是使用pandas Series(Series是DataFrame中单列对应的数据内容)的value_counts函数,以显示唯一值及出现次数。

print(data.gender.value_counts())

Male 21790

Female 10771

Name: gender, dtype: int64

(3)用pandas编码数据有一种非常简单的方法,就是使用get_dummies函数。

get_dummies函数自动变换所有具有对象类型(比如字符串)的列或所有分类的列(这是pandas中的一个特殊概念)。

print("Original features:\n", list(data.columns), "\n")

data_dummies = pd.get_dummies(data)

print("Features after get_dummies:\n", list(data_dummies.columns))

Original features:

['age', 'workclass', 'education', 'gender', 'hours-per-week', 'occupation', 'income']

Features after get_dummies:

['age', 'hours-per-week', 'workclass_ ?', 'workclass_ Federal-gov', 'workclass_ Local-gov', 'workclass_ Never-worked', 'workclass_ Private', 'workclass_ Self-emp-inc', 'workclass_ Self-emp-not-inc', 'workclass_ State-gov', 'workclass_ Without-pay', 'education_ 10th', 'education_ 11th', 'education_ 12th', 'education_ 1st-4th', 'education_ 5th-6th', 'education_ 7th-8th', 'education_ 9th', 'education_ Assoc-acdm', 'education_ Assoc-voc', 'education_ Bachelors', 'education_ Doctorate', 'education_ HS-grad', 'education_ Masters', 'education_ Preschool', 'education_ Prof-school', 'education_ Some-college', 'gender_ Female', 'gender_ Male', 'occupation_ ?', 'occupation_ Adm-clerical', 'occupation_ Armed-Forces', 'occupation_ Craft-repair', 'occupation_ Exec-managerial', 'occupation_ Farming-fishing', 'occupation_ Handlers-cleaners', 'occupation_ Machine-op-inspct', 'occupation_ Other-service', 'occupation_ Priv-house-serv', 'occupation_ Prof-specialty', 'occupation_ Protective-serv', 'occupation_ Sales', 'occupation_ Tech-support', 'occupation_ Transport-moving', 'income_ <=50K', 'income_ >50K']

可以看到,连续特征age和hours-per-week没有发生变化,而分类特征的每个可能取值都被扩展为一个新特征:

display(data_dummies.head(n=10))

(4)使用values属性将data_dummies数据框(DataFrame)转换为NumPy数据

在训练模型之前,注意要把目标变量(现在被编码为两个income列)从数据中分离出来。

# Get only the columns containing features

# that is all columns from 'age' to 'occupation_ Transport-moving'

# This range contains all the features but not the target

features = data_dummies.loc[:, 'age':'occupation_ Transport-moving']

# extract NumPy arrays

X = features.values

y = data_dummies['income_ >50K'].values

print("X.shape:{}y.shape:{}".format(X.shape, y.shape))

X.shape: (32561, 44) y.shape: (32561,)

(5)在其上训练一个机器学习模型。

from sklearn.linear_model import LogisticRegression

from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)

logreg = LogisticRegression()

logreg.fit(X_train, y_train)

print("Test score:{:.2f}".format(logreg.score(X_test, y_test)))

Test score: 0.81

2.数字可以编码分类变量

分类特征通常用整数进行编码。它们是数字并不意味着它们必须被视为连续特征。一个整数特征应该被视为连续的还是离散的(one-hot编码的),有时并不明确。如果在被编码的语意之间没有顺序关系(比如workclass的例子),那么特征必须被视为离散特征。对于其他情况(比如五星评分),哪种编码更好取决于具体的任务和数据,以及使用哪种机器学习算法。

pandas的get_dummies函数将所有数字看做是连续的,不会为其创建虚拟变量。为了解决这个问题,你可以使用scikit-learn的OneHotEncoder,指定哪些变量是连续的、哪些变量是离散的,你也可以将数据框中的数值转换为字符串。为了说明这一点,我们创建一个两列的DataFrame对象,其中一列包含字符串,另一列包含整数:

# create a DataFrame with an integer feature and a categorical string feature

demo_df = pd.DataFrame({'Integer Feature': [0, 1, 2, 1],

'Categorical Feature': ['socks', 'fox', 'socks', 'box']})

display(demo_df)

使用get_dummies智慧编码字符串特征,不会改变整数特征:

display(pd.get_dummies(demo_df))

如果想为“Interger Feature”这一列创建虚拟变量,可以使用columns参数显式地给出想要编码的列,于是两个特征都会被当做分类特征处理:

demo_df['Integer Feature'] = demo_df['Integer Feature'].astype(str)

display(pd.get_dummies(demo_df, columns=['Integer Feature', 'Categorical Feature']))

二、分箱、离散化、线性模型与树

上图是在wave数据集上笔记线性回归和决策树。如你所知,线性模型只能对线性关系建模,对于单个特征的情况就是直线。决策树可以构建更为复杂的数据模型,但这强烈依赖于数据表示。有一种方法可以让线性模型在连续数据上变得更为强大,就是使用特征分箱(binning,也叫离散化,即discretization)将其划分为多个特征。

我们假设将特征输入范围(这里是从-3到3)划分成固定个数的箱子(bin),比如10个,那么数据点就可以用它所在的箱子来表示。

bins = np.linspace(-3, 3, 11)

print("bins:{}".format(bins))

bins: [-3. -2.4 -1.8 -1.2 -0.6 0. 0.6 1.2 1.8 2.4 3. ]

接下来,我们记录每个数据点所属的箱子,可以用np.digitize函数轻松计算出来:

which_bin = np.digitize(X, bins=bins)

print("\nData points:\n", X[:5])

print("\nBin membership for data points:\n", which_bin[:5])

Data points:

[[-0.753]

[ 2.704]

[ 1.392]

[ 0.592]

[-2.064]]

Bin membership for data points:

[[ 4]

[10]

[ 8]

[ 6]

[ 2]]

我们在这里做的是将wave数据集中单个连续输入特征变换为一个分类特征,用于表示数据点所在的箱子。要想在这个数据上使用scikit-learn模型,我们利用preprocessing模块的OneHotEncoder将这个离散特征变换为one-hot编码。OneHotEncoder实现的编码与pandas.get_dummies相同,但目前它只适用于值为整数的分类变量。

from sklearn.preprocessing import OneHotEncoder

# transform using the OneHotEncoder

encoder = OneHotEncoder(sparse=False)

# encoder.fit finds the unique values that appear in which_bin

encoder.fit(which_bin)

# transform creates the one-hot encoding

X_binned = encoder.transform(which_bin)

print(X_binned[:5])

[[0. 0. 0. 1. 0. 0. 0. 0. 0. 0.]

[0. 0. 0. 0. 0. 0. 0. 0. 0. 1.]

[0. 0. 0. 0. 0. 0. 0. 1. 0. 0.]

[0. 0. 0. 0. 0. 1. 0. 0. 0. 0.]

[0. 1. 0. 0. 0. 0. 0. 0. 0. 0.]]

由于我们指定了10个箱子,所以变换后的X_binned数据集现在包含10个特征:

print("X_binned.shape:{}".format(X_binned.shape))

X_binned.shape: (100, 10)

下面,我们在one-hot编码后的数据上构建新的线性模型和新的决策树模型(箱子的边界由黑色虚线表示):

line_binned = encoder.transform(np.digitize(line, bins=bins))

reg = LinearRegression().fit(X_binned, y)

plt.plot(line, reg.predict(line_binned), label='linear regression binned')

reg = DecisionTreeRegressor(min_samples_split=3).fit(X_binned, y)

plt.plot(line, reg.predict(line_binned), label='decision tree binned')

plt.plot(X[:, 0], y, 'o', c='k')

plt.vlines(bins, -3, 3, linewidth=1, alpha=.2)

plt.legend(loc="best")

plt.ylabel("Regression output")

plt.xlabel("Input feature")

箱子的边界由灰色虚线表示,可以看到线性回归和决策树做出了完全相同的预测(两条线完全重合)。对于每个箱子,二者都预测一个常数值。比较对特征进行分箱前后模型学到的内容,我们发现,线性模型变得更加灵活了,因为现在它对每个箱子具有不同的取值,而决策树模型的灵活性降低了。

分箱特征对基于树的模型通常不会产生更好地效果,因为这种模型可以学习在任何位置划分数据,从某种意义上来看,决策树可以学习如何分箱对预测这些数据最为有用,此外,决策树可以同时查看多个特征,而分箱通常针对的是单个特征。线性模型的表现力在数据变化后得到了极大的提高。

对于特定的数据集,如果有充分的理由使用线性模型——比如数据集很大、维度很高,但有些特征与输出的关系是非线性的——那么分箱是提高建模能力的好办法。

三、交互特征与多项式特征

1.向分箱数据上的线性模型添加斜率

一种方法是重新加入原始特征(图中的x轴)

X_combined = np.hstack([X, X_binned])

print(X_combined.shape)

(100, 11)

reg = LinearRegression().fit(X_combined, y)

line_combined = np.hstack([line, line_binned])

plt.plot(line, reg.predict(line_combined), label='linear regression combined')

for bin in bins:

plt.plot([bin, bin], [-3, 3], ':', c='k', linewidth=1)

plt.legend(loc="best")

plt.ylabel("Regression output")

plt.xlabel("Input feature")

plt.plot(X[:, 0], y, 'o', c='k')

np.vstack():在竖直方向上堆叠

np.hstack():在水平方向上平铺

例:

arr1=np.array([1,2,3])

arr2=np.array([4,5,6])

print np.vstack((arr1,arr2))

print np.hstack((arr1,arr2))

[[1 2 3]

[4 5 6]]

[1 2 3 4 5 6]

可以看到上图,模型在每个箱子中都学到了一个偏移,还学到了一个斜率。学到的斜率是向下的,并且在所有箱子中都相同——只有一个x轴特征,也就只有一个斜率。因为斜率在所有箱子中是相同的,所以它似乎不是很有用。

我们希望每个箱子都有一个不同的斜率,为了实现这一点,我们可以添加交互特征或者乘积特征,用来表示数据点所在箱子以及数据点在x轴上的位置。这个特征是箱子指示符与原始特征的乘积:

X_product = np.hstack([X_binned, X * X_binned])

print(X_product.shape)

(100, 20)

reg = LinearRegression().fit(X_product, y)

line_product = np.hstack([line_binned, line * line_binned])

plt.plot(line, reg.predict(line_product), label='linear regression product')

for bin in bins:

plt.plot([bin, bin], [-3, 3], ':', c='k', linewidth=1)

plt.plot(X[:, 0], y, 'o', c='k')

plt.ylabel("Regression output")

plt.xlabel("Input feature")

plt.legend(loc="best")

如你所见,现在这个模型中每个箱子都有自己的偏移和斜率。

2.使用分箱是扩展连续特征的一种方法。另一种方法是使用原始特征的多项式(polynomial)。

对于给定特征x,我们可以考虑x**2、x**3、x**4,等等。这在preprocessing模块的PolynomialFeatures中实现:

from sklearn.preprocessing import PolynomialFeatures

# include polynomials up to x ** 10:

# the default "include_bias=True" adds a feature that's constantly 1

poly = PolynomialFeatures(degree=10, include_bias=False)

poly.fit(X)

X_poly = poly.transform(X)

print("X_poly.shape:{}".format(X_poly.shape))

X_poly.shape: (100, 10)

print("Entries of X:\n{}".format(X[:5]))

print("Entries of X_poly:\n{}".format(X_poly[:5]))

Entries of X:

[[-0.753]

[ 2.704]

[ 1.392]

[ 0.592]

[-2.064]]

Entries of X_poly:

[[ -0.753 0.567 -0.427 0.321 -0.242 0.182 -0.137

0.103 -0.078 0.058]

[ 2.704 7.313 19.777 53.482 144.632 391.125 1057.714

2860.36 7735.232 20918.278]

[ 1.392 1.938 2.697 3.754 5.226 7.274 10.125

14.094 19.618 27.307]

[ 0.592 0.35 0.207 0.123 0.073 0.043 0.025

0.015 0.009 0.005]

[ -2.064 4.26 -8.791 18.144 -37.448 77.289 -159.516

329.222 -679.478 1402.367]]

print("Polynomial feature names:\n{}".format(poly.get_feature_names()))

Polynomial feature names:

['x0', 'x0^2', 'x0^3', 'x0^4', 'x0^5', 'x0^6', 'x0^7', 'x0^8', 'x0^9', 'x0^10']

将多项式特征与线性回归模型一起使用,可以得到经典的多项式回归模型。

reg = LinearRegression().fit(X_poly, y)

line_poly = poly.transform(line)

plt.plot(line, reg.predict(line_poly), label='polynomial linear regression')

plt.plot(X[:, 0], y, 'o', c='k')

plt.ylabel("Regression output")

plt.xlabel("Input feature")

plt.legend(loc="best")

可见,多项式特征在一个一维数据上得到了非常平滑的拟合。但高次多项式在边界上或数据很少的区域可能有极端的表现。

作为对比,下面是在原始数据上学到的核SVM模型,没有做任何变换:

from sklearn.svm import SVR

for gamma in [1, 10]:

svr = SVR(gamma=gamma).fit(X, y)

plt.plot(line, svr.predict(line), label='SVR gamma={}'.format(gamma))

plt.plot(X[:, 0], y, 'o', c='k')

plt.ylabel("Regression output")

plt.xlabel("Input feature")

plt.legend(loc="best")

可以看到使用更加复杂的模型(即核SVM),我们能够得到一个与多项式回归的复杂度类似的预测结果,且不需要进行显式的特征变换。

3.交互特征和多项式特征更加实际的应用

from sklearn.datasets import load_boston

from sklearn.model_selection import train_test_split

from sklearn.preprocessing import MinMaxScaler

boston = load_boston()

X_train, X_test, y_train, y_test = train_test_split(

boston.data, boston.target, random_state=0)

# rescale data

scaler = MinMaxScaler()

X_train_scaled = scaler.fit_transform(X_train)

X_test_scaled = scaler.transform(X_test)

poly = PolynomialFeatures(degree=2).fit(X_train_scaled)

X_train_poly = poly.transform(X_train_scaled)

X_test_poly = poly.transform(X_test_scaled)

print("X_train.shape:{}".format(X_train.shape))

print("X_train_poly.shape:{}".format(X_train_poly.shape))

X_train.shape: (379, 13)

X_train_poly.shape: (379, 105)

print("Polynomial feature names:\n{}".format(poly.get_feature_names()))

Polynomial feature names:

['1', 'x0', 'x1', 'x2', 'x3', 'x4', 'x5', 'x6', 'x7', 'x8', 'x9', 'x10', 'x11', 'x12', 'x0^2', 'x0 x1', 'x0 x2', 'x0 x3', 'x0 x4', 'x0 x5', 'x0 x6', 'x0 x7', 'x0 x8', 'x0 x9', 'x0 x10', 'x0 x11', 'x0 x12', 'x1^2', 'x1 x2', 'x1 x3', 'x1 x4', 'x1 x5', 'x1 x6', 'x1 x7', 'x1 x8', 'x1 x9', 'x1 x10', 'x1 x11', 'x1 x12', 'x2^2', 'x2 x3', 'x2 x4', 'x2 x5', 'x2 x6', 'x2 x7', 'x2 x8', 'x2 x9', 'x2 x10', 'x2 x11', 'x2 x12', 'x3^2', 'x3 x4', 'x3 x5', 'x3 x6', 'x3 x7', 'x3 x8', 'x3 x9', 'x3 x10', 'x3 x11', 'x3 x12', 'x4^2', 'x4 x5', 'x4 x6', 'x4 x7', 'x4 x8', 'x4 x9', 'x4 x10', 'x4 x11', 'x4 x12', 'x5^2', 'x5 x6', 'x5 x7', 'x5 x8', 'x5 x9', 'x5 x10', 'x5 x11', 'x5 x12', 'x6^2', 'x6 x7', 'x6 x8', 'x6 x9', 'x6 x10', 'x6 x11', 'x6 x12', 'x7^2', 'x7 x8', 'x7 x9', 'x7 x10', 'x7 x11', 'x7 x12', 'x8^2', 'x8 x9', 'x8 x10', 'x8 x11', 'x8 x12', 'x9^2', 'x9 x10', 'x9 x11', 'x9 x12', 'x10^2', 'x10 x11', 'x10 x12', 'x11^2', 'x11 x12', 'x12^2']

from sklearn.linear_model import Ridge

ridge = Ridge().fit(X_train_scaled, y_train)

print("Score without interactions:{:.3f}".format(

ridge.score(X_test_scaled, y_test)))

ridge = Ridge().fit(X_train_poly, y_train)

print("Score with interactions:{:.3f}".format(

ridge.score(X_test_poly, y_test)))

Score without interactions: 0.621

Score with interactions: 0.753

from sklearn.ensemble import RandomForestRegressor

rf = RandomForestRegressor(n_estimators=100).fit(X_train_scaled, y_train)

print("Score without interactions:{:.3f}".format(

rf.score(X_test_scaled, y_test)))

rf = RandomForestRegressor(n_estimators=100).fit(X_train_poly, y_train)

print("Score with interactions:{:.3f}".format(rf.score(X_test_poly, y_test)))

Score without interactions: 0.788

Score with interactions: 0.780

显然,在使用Ridge时,交互特征和多项式特征对性能有很大提升,但如果使用更加复杂的模型(比如随机森林),情况会稍有不同:即使没有额外的特征,随机森林的性能也要优于Ridge。添加交互特征和多项式特征实际上会略微降低其性能。

四、单变量非线性变换

大部分模型都在每个特征(在回归问题中还包括目标值)大致遵循高斯分布时表现最好,也就是说,每个特征的直方图应用该具有类似于熟悉的“钟形曲线”的形状。使用诸如log和exp之类的变换并不稀奇,但却是实现这一点的简单又有效的方法。在一种特别常见的情况下,这样的变换非常有用,就是处理整数计数数据时。计数数据是指类似“用户A多长时间登录一次?”这样的特征。计数不可能取负值,并且通常遵循特定的统计模式。下面我们使用一个模拟的计数数据集,其性质与在自然状态下能找到的数据集类似。特征全是整数值,而响应是连续的。

rnd = np.random.RandomState(0)

X_org = rnd.normal(size=(1000, 3)) # 生成高斯分布的概率函数密度,大小为1000行,3列

w = rnd.normal(size=3) # 生成高斯分布的概率函数密度,大小为3列

X = rnd.poisson(10 * np.exp(X_org)) # 返回e的幂次方,e是一个常数为2.71828

# numpy.random.poisson(lam=1.0, size=None)

# Draw samples from a Poisson distribution.

y = np.dot(X_org, w)

print("Number of feature appearances:\n{}".format(np.bincount(X[:, 0]))) # 计算每个值出现的次数

Number of feature appearances:

[28 38 68 48 61 59 45 56 37 40 35 34 36 26 23 26 27 21 23 23 18 21 10 9

17 9 7 14 12 7 3 8 4 5 5 3 4 2 4 1 1 3 2 5 3 8 2 5

2 1 2 3 3 2 2 3 3 0 1 2 1 0 0 3 1 0 0 0 1 3 0 1

0 2 0 1 1 0 0 0 0 1 0 0 2 2 0 1 1 0 0 0 0 1 1 0

0 0 0 0 0 0 1 0 0 0 0 0 1 1 0 0 1 0 0 0 0 0 0 0

1 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1]

bins = np.bincount(X[:, 0]) # 统计次数

plt.bar(range(len(bins)), bins, color='grey')

plt.ylabel("Number of appearances")

plt.xlabel("Value")

上图是X[:,0]特征取值的直方图

尝试拟合一个岭回归模型:

from sklearn.linear_model import Ridge

X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)

score = Ridge().fit(X_train, y_train).score(X_test, y_test)

print("Test score:{:.3f}".format(score))

Test score: 0.622

X_train_log = np.log(X_train + 1)

X_test_log = np.log(X_test + 1)

plt.hist(X_train_log[:, 0], bins=25, color='gray')

plt.ylabel("Number of appearances")

plt.xlabel("Value")

score = Ridge().fit(X_train_log, y_train).score(X_test_log, y_test)

print("Test score: {:.3f}".format(score))

Test score: 0.875

可以从处理过程看出:Ridge无法真正捕获x和y之间的关系。不过应用对数变换可能有用。由于数据取值中包括0(对数在0处没有定义),所以我们不能直接应用log,而是要计算log(X+1)。变换后,数据分布的不对称性变小,也不再有非常大的异常值。在新数据上构建一个岭回归模型,可以得到更好地拟合。

为数据集合模型的所有组合寻找最佳变换,这在某种程度上是一门艺术。在上例中,所有特征都具有相同的性质,这在实践中是非常少见的情况。通常来说,只有一部分特征应该进行变换,有时每个特征的变换方式也各不相同。前面提到过,对基于树的模型而言,这种变换并不重要,但对线性模型来说可能至关重要。对回归的目标变量y进行变换有时也是一个好主意。尝试预测计数(比如订单数量)是一项相当常见的任务,而且使用log(y+1)变换也往往有用。

从前面的例子中可以看出,分箱、多项式和交互项都对模型在给定数据集上的性能有很大影响,对于复杂度较低的模型更是这样,比如线性模型和朴素贝叶斯模型。与之相反,基于树的模型通常能够自己发现重要的交互项,大多数情况下不需要显式地变换数据。其他模型,比如SVM、最近邻和神经网络,有时可能会从使用分箱、交互项或多项式中受益,但其效果通常不如线性模型那么明显。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值