Python 机器学习 基础 之 数据表示与特征工程 【分类变量】的简单说明
目录
Python 机器学习 基础 之 数据表示与特征工程 【分类变量】的简单说明
一、简单介绍
二、数据表示与特征工程
数据表示
特征工程
三、分类变量
1、One-Hot编码(虚拟变量)
附录
一、参考文献
一、简单介绍
Python是一种跨平台的计算机程序设计语言。是一种面向对象的动态类型语言,最初被设计用于编写自动化脚本(shell),随着版本的不断更新和语言新功能的添加,越多被用于独立的、大型项目的开发。Python是一种解释型脚本语言,可以应用于以下领域: Web 和 Internet开发、科学计算和统计、人工智能、教育、桌面界面开发、软件开发、后端开发、网络爬虫。
Python 机器学习是利用 Python 编程语言中的各种工具和库来实现机器学习算法和技术的过程。Python 是一种功能强大且易于学习和使用的编程语言,因此成为了机器学习领域的首选语言之一。Python 提供了丰富的机器学习库,如Scikit-learn、TensorFlow、Keras、PyTorch等,这些库包含了许多常用的机器学习算法和深度学习框架,使得开发者能够快速实现、测试和部署各种机器学习模型。
Python 机器学习涵盖了许多任务和技术,包括但不限于:
- 监督学习:包括分类、回归等任务。
- 无监督学习:如聚类、降维等。
- 半监督学习:结合了有监督和无监督学习的技术。
- 强化学习:通过与环境的交互学习来优化决策策略。
- 深度学习:利用深度神经网络进行学习和预测。
通过 Python 进行机器学习,开发者可以利用其丰富的工具和库来处理数据、构建模型、评估模型性能,并将模型部署到实际应用中。Python 的易用性和庞大的社区支持使得机器学习在各个领域都得到了广泛的应用和发展。
二、数据表示与特征工程
之前我们一直假设数据是由浮点数组成的二维数组,其中每一列是描述数据点的 连续特征 (continuous feature)。对于许多应用而言,数据的收集方式并不是这样。一种特别常见的特征类型就是分类特征 (categorical feature),也叫离散特征 (discrete feature)。这种特征通常并不是数值。分类特征与连续特征之间的区别类似于分类和回归之间的区别,只是前者在输入端而不是输出端。我们已经见过的连续特征的例子包括像素明暗程度和花的尺寸测量。分类特征的例子包括产品的品牌、产品的颜色或产品的销售部门(图书、服装、硬件)。这些都是描述一件产品的属性,但它们不以连续的方式变化。一件产品要么属于服装部门,要么属于图书部门。在图书和服装之间没有中间部门,不同的分类之间也没有顺序(图书不大于服装也不小于服装,硬件不在图书和服装之间,等等)。
无论你的数据包含哪种类型的特征,数据表示方式都会对机器学习模型的性能产生巨大影响。我们在之前案例可以中看到,数据缩放非常重要。换句话说,如果你没有缩放数据(比如,缩放到单位方差),那么你用厘米还是英寸表示测量数据的结果将会不同。我们在之前的案例中还看到,用额外的特征扩充 (augment)数据也很有帮助,比如添加特征的交互项(乘积)或更一般的多项式。
对于某个特定应用来说,如何找到最佳数据表示,这个问题被称为特征工程 (feature engineering),它是数据科学家和机器学习从业者在尝试解决现实世界问题时的主要任务之一。用正确的方式表示数据,对监督模型性能的影响比所选择的精确参数还要大。
机器学习中的数据表示与特征工程是模型训练的基础环节。
- 数据表示指的是如何将原始数据转换为机器学习算法可以处理的形式。
- 特征工程是指从原始数据中提取或创建特征,以便更好地进行模型训练。
以下是关于数据表示和特征工程的详细介绍:
数据表示
数值数据: 最常见的数据类型,可以直接用于大多数机器学习算法。包括整数和浮点数。
- 示例:年龄、收入、评分等。
分类数据: 包含离散的类别或标签,需要转换为数值形式才能用于算法中。
- 标签编码(Label Encoding): 将每个类别映射为一个整数。
- 独热编码(One-Hot Encoding): 使用二进制向量表示每个类别。
- 示例:性别(男、女)、城市(纽约、伦敦、巴黎)等。
文本数据: 需要转换为数值表示,常用方法有:
- 词袋模型(Bag of Words): 统计每个单词在文本中出现的次数。
- TF-IDF(Term Frequency-Inverse Document Frequency): 考虑单词在文档和整个语料库中的频率。
- 词向量(Word Embeddings): 使用预训练的词向量模型(如Word2Vec、GloVe)将单词表示为向量。
时间序列数据: 包含时间戳和相应的值。常用的方法有:
- 时间特征提取: 提取如年、月、日、小时、星期几等特征。
- 滑动窗口: 创建时间窗口来捕捉短期趋势。
- 差分处理: 计算当前值与前一个值的差异。
特征工程
特征选择: 从原始数据中选择对预测任务最有用的特征。
- 过滤法(Filter Method): 基于统计特性选择特征,如方差选择、互信息选择等。
- 包裹法(Wrapper Method): 使用特定的机器学习模型选择特征,如递归特征消除(RFE)。
- 嵌入法(Embedded Method): 在模型训练过程中选择特征,如LASSO回归中的L1正则化。
特征提取: 从原始数据中提取新的特征。
- 主成分分析(PCA): 降维技术,通过线性变换将数据映射到新的特征空间。
- 独立成分分析(ICA): 类似于PCA,但假设特征是独立的。
- 特征聚合: 将多个特征组合在一起,如求和、平均等。
特征构造: 基于现有特征创建新的特征。
- 多项式特征: 创建特征的多项式组合,如𝑥12,𝑥1𝑥2x12,x1x2等。
- 交互特征: 计算两个或多个特征的交互作用,如乘积、比值等。
特征缩放: 将特征值缩放到相同范围,以提高模型的稳定性和收敛速度。
- 标准化(Standardization): 将特征缩放到均值为0,方差为1的分布。
- 归一化(Normalization): 将特征缩放到指定范围(通常是0到1)。
三、分类变量
作为例子,我们将使用美国成年人收入的数据集,该数据集是从 1994 年的普查数据库中导出的。adult
数据集的任务是预测一名工人的收入是高于 50 000 美元还是低于 50 000 美元。这个数据集的特征包括工人的年龄、雇用方式(独立经营、私营企业员工、政府职员等)、教育水平、性别、每周工作时长、职业,等等。表 4-1 给出了该数据集中的前几个条目。
age | workclass | education | gender | hours-per-week | occupation | income | |
0 | 39 | State-gov | Bachelors | Male | 40 | Adm-clerical | <=50K |
1 | 50 | Self-emp-not-inc | Bachelors | Male | 13 | Exec-managerial | <=50K |
2 | 38 | Private | HS-grad | Male | 40 | Handlers-cleaners | <=50K |
3 | 53 | Private | 11th | Male | 40 | Handlers-cleaners | <=50K |
4 | 28 | Private | Bachelors | Female | 40 | Prof-specialty | <=50K |
5 | 37 | Private | Masters | Female | 40 | Exec-managerial | <=50K |
6 | 49 | Private | 9th | Female | 16 | Other-service | <=50K |
7 | 52 | Self-emp-not-inc | HS-grad | Male | 45 | Exec-managerial | >50K |
8 | 31 | Private | Masters | Female | 50 | Prof-specialty | >50K |
9 | 42 | Private | Bachelors | Male | 40 | Exec-managerial | >50K |
10 | 37 | Private | Some-college | Male | 80 | Exec-managerial | >50K |
这个任务属于分类任务,两个类别是收入 <=50k
和 >50k
。也可以预测具体收入,那样就变成了一个回归任务。但那样问题将变得更加困难,而理解 50K 的分界线本身也很有趣。
在这个数据集中,age
(年龄)和 hours-per-week
(每周工作时长)是连续特征,我们知道如何处理这种特征。但 workclass
(工作类型)、education
(教育程度)、gender
(性别)、occupation
(职业)都是分类特征。它们都来自一系列固定的可能取值(而不是一个范围),表示的是定性属性(而不是数量)。
首先,假设我们想要在这个数据上学习一个 Logistic 回归分类器。我们在第 2 章学过,Logistic 回归利用下列公式进行预测,预测值为 :
其中 w [i ] 和 b 是从训练集中学到的系数,x [i ] 是输入特征。当 x [i ] 是数字时这个公式才有意义,但如果 x [2] 是 "Masters"
或 "Bachelors"
的话,这个公式则没有意义。显然,在应用 Logistic 回归时,我们需要换一种方式来表示数据。下一节将会说明我们如何解决这一问题。
1、One-Hot编码(虚拟变量)
到目前为止,表示分类变量最常用的方法就是使用 one-hot 编码 (one-hot-encoding)或 N 取一编码 (one-out-of-N encoding),也叫虚拟变量 (dummy variable)。虚拟变量背后的思想是将一个分类变量替换为一个或多个新特征,新特征取值为 0 和 1。对于线性二分类(以及 scikit-learn
中其他所有模型)的公式而言,0 和 1 这两个值是有意义的,我们可以像这样对每个类别引入一个新特征,从而表示任意数量的类别。
比如说,workclass
特征的可能取值包括 "Government Employee"
、"Private Employee"
、"Self Employed"
和 "Self Employed Incorporated"
。为了编码这 4 个可能的取值,我们创建了 4 个新特征,分别叫作 "Government Employee"
、"Private Employee"
、"Self Employed"
和 "Self Employed Incorporated"
。如果一个人的 workclass
取某个值,那么对应的特征取值为 1,其他特征均取值为 0。因此,对每个数据点来说,4 个新特征中只有一个的取值为 1。这就是它叫作 one-hot 编码或 N 取一编码的原因。
其原理如表 4-2 所示。利用 4 个新特征对一个特征进行编码。在机器学习算法中使用此数据时,我们将会删除原始的 workclass
特征,仅保留 0-1 特征。
workclass | Government Employee | Private Employee | Self Employed | Self Employed Incorporated |
Government Employee | 1 | 0 | 0 | 0 |
Private Employee | 0 | 1 | 0 | 0 |
Self Employed | 0 | 0 | 1 | 0 |
Self Employed Incorporated | 0 | 0 | 0 | 1 |
我们使用的 one-hot 编码与统计学中使用的虚拟编码(dummy encoding)非常相似,但并不完全相同。为简单起见,我们将每个类别编码为不同的二元特征。在统计学中,通常将具有 k 个可能取值的分类特征编码为 k - 1 个特征(都等于零表示最后一个可能取值)。这么做是为了简化分析(更专业的说法是,这可以避免使数据矩阵秩亏)。
将数据转换为分类变量的 one-hot 编码有两种方法:一种是使用 pandas
,一种是使用 scikit-learn
。在写作本书时,使用 pandas
要稍微简单一些,所以我们选择这种方法。首先,我们使用 pandas
从逗号分隔值(CSV)文件中加载数据:
import pandas as pd
from IPython.display import display
# 文件中没有包含列名称的表头,因此我们传入header=None
# 然后在"names"中显式地提供列名称
data = pd.read_csv(
"data/adult.data", 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'])
# 为了便于说明,我们只选了其中几列
data = data[['age', 'workclass', 'education', 'gender', 'hours-per-week',
'occupation', 'income']]
# IPython.display可以在Jupyter notebook中输出漂亮的格式
display(data.head())
运行结果见表 4-3
age | workclass | education | gender | hours-per-week | occupation | income | |
0 | 39 | State-gov | Bachelors | Male | 40 | Adm-clerical | <=50K |
1 | 50 | Self-emp-not-inc | Bachelors | Male | 13 | Exec-managerial | <=50K |
2 | 38 | Private | HS-grad | Male | 40 | Handlers-cleaners | <=50K |
3 | 53 | Private | 11th | Male | 40 | Handlers-cleaners | <=50K |
4 | 28 | Private | Bachelors | Female | 40 | Prof-specialty | <=50K |
1)检查字符串编码的分类数据
读取完这样的数据集之后,最好先检查每一列是否包含有意义的分类数据。在处理人工(比如网站用户)输入的数据时,可能没有固定的类别,拼写和大小写也存在差异,因此可能需要预处理。举个例子,有人可能将性别填为“male”(男性),有人可能填为“man”(男人),而我们希望能用同一个类别来表示这两种输入。检查列的内容有一个好方法,就是使用 pandas Series
(Series
是 DataFrame
中单列对应的数据类型)的 value_counts
函数,以显示唯一值及其出现次数:
print(data.gender.value_counts())
gender Male 21790 Female 10771 Name: count, dtype: int64
可以看到,在这个数据集中性别刚好有两个值:Male
和 Female
,这说明数据格式已经很好,可以用 one-hot 编码来表示。在实际的应用中,你应该查看并检查所有列的值。为简洁起见,这里我们将跳过这一步。
用 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
没有发生变化,而分类特征的每个可能取值都被扩展为一个新特征:
data_dummies.head()
运行结果如下表:
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 | ... | 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 | |
0 | 39 | 40 | FALSE | FALSE | FALSE | FALSE | FALSE | FALSE | FALSE | TRUE | ... | FALSE | FALSE | FALSE | FALSE | FALSE | FALSE | FALSE | FALSE | TRUE | FALSE |
1 | 50 | 13 | FALSE | FALSE | FALSE | FALSE | FALSE | FALSE | TRUE | FALSE | ... | FALSE | FALSE | FALSE | FALSE | FALSE | FALSE | FALSE | FALSE | TRUE | FALSE |
2 | 38 | 40 | FALSE | FALSE | FALSE | FALSE | TRUE | FALSE | FALSE | FALSE | ... | FALSE | FALSE | FALSE | FALSE | FALSE | FALSE | FALSE | FALSE | TRUE | FALSE |
3 | 53 | 40 | FALSE | FALSE | FALSE | FALSE | TRUE | FALSE | FALSE | FALSE | ... | FALSE | FALSE | FALSE | FALSE | FALSE | FALSE | FALSE | FALSE | TRUE | FALSE |
4 | 28 | 40 | FALSE | FALSE | FALSE | FALSE | TRUE | FALSE | FALSE | FALSE | ... | FALSE | FALSE | FALSE | TRUE | FALSE | FALSE | FALSE | FALSE | TRUE | FALSE |
5 rows×46 columns
下面我们可以使用 values
属性将 data_dummies
数据框(DataFrame
)转换为 NumPy 数组,然后在其上训练一个机器学习模型。在训练模型之前,注意要把目标变量(现在被编码为两个 income
列)从数据中分离出来。将输出变量或输出变量的一些导出属性包含在特征表示中,这是构建监督机器学习模型时一个非常常见的错误。
注意:
pandas
中的列索引包括范围的结尾,因此'age':'occupation_Transport-moving'
中包括occupation_Transport-moving
。这与 NumPy 数组的切片不同,后者不包括范围的结尾,例如np.arange(11)[0:10]
不包括索引编号为 10 的元素。
在这个例子中,我们仅提取包含特征的列,也就是从 age
到 occupation_ Transport-moving
的所有列。这一范围包含所有特征,但不包含目标:
features = data_dummies.loc[:, 'age':'occupation_ Transport-moving']
# 提取NumPy数组
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,)
现在数据的表示方式可以被 scikit-learn
处理,我们可以像之前一样继续下一步:
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
在这个例子中,我们对同时包含训练数据和测试数据的数据框调用
get_dummies
。这一点很重要,可以确保训练集和测试集中分类变量的表示方式相同。假设我们的训练集和测试集位于两个不同的数据框中。如果
workclass
特征的"Private Employee"
取值没有出现在测试集中,那么pandas
会认为这个特征只有 3 个可能的取值,因此只会创建 3 个新的虚拟特征。现在训练集和测试集的特征个数不相同,我们就无法将在训练集上学到的模型应用到测试集上。更糟糕的是,假设workclass
特征在训练集中有"Government Employee"
和"Private Employee"
两个值,而在测试集中有"Self Employed"
和"Self Employed Incorporated"
两个值。在两种情况下,pandas
都会创建两个新的虚拟特征,所以编码后的数据框的特征个数相同。但在训练集和测试集中的两个虚拟特征含义完全不同。训练集中表示"Government Employee"
的那一列在测试集中对应的是"Self Employed"
。如果我们在这个数据上构建机器学习模型,那么它的表现会很差,因为它认为每一列表示的是相同的内容(因为位置相同),而实际上表示的却是非常不同的内容。要想解决这个问题,可以在同时包含训练数据点和测试数据点的数据框上调用
get_dummies
,也可以确保调用get_dummies
后训练集和测试集的列名称相同,以保证它们具有相同的语义。
在 adult
数据集的例子中,分类变量被编码为字符串。一方面,可能会有拼写错误;但另一方面,它明确地将一个变量标记为分类变量。无论是为了便于存储还是因为数据的收集方式,分类变量通常被编码为整数。例如,假设adult
数据集中的人口普查数据是利用问卷收集的,workclass
的回答被记录为 0(在第一个框打勾)、1(在第二个框打勾)、2(在第三个框打勾),等等。现在该列包含数字 0 到 8,而不是像 "Private"
这样的字符串。如果有人观察表示数据集的表格,很难一眼看出这个变量应该被视为连续变量还是分类变量。但是,如果知道这些数字表示的是就业状况,那么很明显它们是不同的状态,不应该用单个连续变量来建模。
分类特征通常用整数进行编码。它们是数字并不意味着它们必须被视为连续特征。一个整数特征应该被视为连续的还是离散的(one-hot 编码的),有时并不明确。如果在被编码的语义之间没有顺序关系(比如
workclass
的例子),那么特征必须被视为离散特征。对于其他情况(比如五星评分),哪种编码更好取决于具体的任务和数据,以及使用哪种机器学习算法。
pandas
的 get_dummies
函数将所有数字看作是连续的,不会为其创建虚拟变量。为了解决这个问题,你可以使用 scikit-learn
的 OneHotEncoder
,指定哪些变量是连续的、哪些变量是离散的,你也可以将数据框中的数值列转换为字符串。为了说明这一点,我们创建一个两列的 DataFrame
对象,其中一列包含字符串,另一列包含整数:
# 创建一个DataFrame,包含一个整数特征和一个分类字符串特征
demo_df = pd.DataFrame({'Integer Feature': [0, 1, 2, 1],
'Categorical Feature': ['socks', 'fox', 'socks', 'box']})
display(demo_df)
运行结果见表 4-4
Integer Feature | Categorical Feature | |
0 | 0 | socks |
1 | 1 | fox |
2 | 2 | socks |
3 | 1 | box |
使用 get_dummies
只会编码字符串特征,不会改变整数特征,正如表 4-5 所示。
pd.get_dummies(demo_df)
Integer Feature | Categorical Feature_box | Categorical Feature_fox | Categorical Feature_socks | |
0 | 0 | FALSE | FALSE | TRUE |
1 | 1 | FALSE | TRUE | FALSE |
2 | 2 | FALSE | FALSE | TRUE |
3 | 1 | TRUE | FALSE | FALSE |
如果你想为“Integer Feature”这一列创建虚拟变量,可以使用 columns
参数显式地给出想要编码的列。于是两个特征都会被当作分类特征处理(见表 4-6):
demo_df['Integer Feature'] = demo_df['Integer Feature'].astype(str)
pd.get_dummies(demo_df, columns=['Integer Feature', 'Categorical Feature'])
Integer Feature_0 | Integer Feature_1 | Integer Feature_2 | Categorical Feature_box | Categorical Feature_fox | Categorical Feature_socks | |
0 | TRUE | FALSE | FALSE | FALSE | FALSE | TRUE |
1 | FALSE | TRUE | FALSE | FALSE | TRUE | FALSE |
2 | FALSE | FALSE | TRUE | FALSE | FALSE | TRUE |
3 | FALSE | TRUE | FALSE | TRUE | FALSE | FALSE |
附录
一、参考文献
参考文献:[德] Andreas C. Müller [美] Sarah Guido 《Python Machine Learning Basics Tutorial》