什么是one-hot向量
还记得上一次线性回归实验的数据集吗?我们借助pandas库的工具查看了它的属性信息,并发现它的所有特征字段都是float浮点数类型,这种类型的数据可以直接传递到模型中。但是在现实世界中,特征并不都是连续的数值,大部分情况下,特征是离散的数据。比如动物类别这个特征,假设会有如下这些属性:
["猫","狗","鸟"]
很明显,这种分类特征并不能直接传入到我们的模型中,我们需要对其进行一定的数值化处理,怎么处理呢?这时候独热编码one-hot便派上了用场。
one-hot向量是一种用于表示离散数据的编码方式。它是一个仅由0和1构成的向量,并且只有一个元素是1,其余都是0,1的位置表示特定的类别或标签。
对于刚刚的动物类别特征,由于总计有3个取值,所以可以用一个长度为3的向量来表示各种可能的取值:
猫:[1, 0, 0]
狗:[0, 1, 0]
鸟:[0, 0, 1]
是不是很容易理解?但你可能想到了另一种更为简单的方式,对于上述数据,共计3个可能的取值,我们也可以用3个数字来表示它们,最简单地:猫=1,狗=2,鸟=3,这也实现了对离散属性的编码。然而,这样编码意味着模型可能会学习到“猫<狗<鸟”,但是这三个取值并无大小关系,只是不同的类别,而采用one-hot向量进行编码就不会出现这种问题,它们都是向量空间的基向量,模长相等,而且每两个向量之间的距离都是相同的,很好地解决了直接赋值所引入的问题。
使用for循环在列表中填充one-hot向量
注意!代码中自带输出结果,记得删除,否则会报错!
下面我们使用Python来基于列表实现离散特征到one-hot向量的转换,仅需一个for循环即可实现对所有的属性取值进行编码。
# 离散特征
animals = ['猫', '狗', '鸟']
# 属性的个数
num_classes = len(animals)
# 记录每一个取值的独热编码
one_hot = {}
for index, animal in enumerate(animals):
# 对于每一个取值,构建一个长度为取值个数的全0列表, 并在其索引的位置上取值为1
one_hot[animal] = [0 if i != index else 1 for i in range(num_classes)]
print(one_hot)
输出结果
{'猫': [1, 0, 0], '狗': [0, 1, 0], '鸟': [0, 0, 1]}
可以看到,我们成功地对动物类别这一离散特征实现了one-hot编码。
使用第三方库实现标签与one-hot向量的转换
虽然我们可以手写一个for循环来获取离散特征的one-hot向量,但是真实的数据集可能有多个特征都是离散取值,对于每一个特征都要用for循环手动构建one-hot向量费时费力,而且代码不够高效。这时候我们可以借助之前安装的Python库。以numpy为例:
import numpy as np
# 离散特征
animals = ['猫', '狗', '鸟']
# 属性的个数
num_classes = len(animals)
# 使用numpy生成one-hot向量
one_hot = np.eye(num_classes)
print(one_hot)
# 假如我们需要猫的独热编码
index = animals.index('猫')
print(one_hot[index])
输出结果:
[[1. 0. 0.]
[0. 1. 0.]
[0. 0. 1.]]
[1. 0. 0.]
使用eye(),可以生成一个单位矩阵,大小由传入参数决定。单位矩阵是对角线元素全部为1,其余元素为0的矩阵,如果我们需要某个特定的属性的独热编码,只需要根据其索引获得第index行的向量即可,这正是one-hot向量的形式。
如果不想按照索引的顺序进行编码,也可以手动指定:
# 定义属性的顺序
labels = [1, 0, 2]
one_hot = np.eye(num_classes)[labels]
print(one_hot)
输出结果:
[[0. 1. 0.]
[1. 0. 0.]
[0. 0. 1.]]
pandas也为我们提供了用于获取one-hot向量的工具:
import pandas as pd
# 创建 DataFrame存放数据
df = pd.DataFrame({'animal': animals})
# 使用 get_dummies 进行独热编码
one_hot = pd.get_dummies(df, columns=['animal'])
print(one_hot)
输出结果:
animal_狗 animal_猫 animal_鸟
0 False True False
1 True False False
2 False False True
pandas的get_dummies()默认将分类变量转换为布尔值,如果我们想看到数值,可以指定dtype为int类型。
# 使用 get_dummies 进行独热编码
one_hot = pd.get_dummies(df, columns=['animal'], dtype=int)
print(one_hot)
输出结果:
animal_狗 animal_猫 animal_鸟
0 0 1 0
1 1 0 0
2 0 0 1
sklearn也提供了转换为one-hot向量的方式:
from sklearn.preprocessing import OneHotEncoder
# 将类别转换为二维数组(Scikit-learn 需要输入为二维数组)
animals_2d = np.array(animals).reshape(-1, 1)
# 初始化 OneHotEncoder
encoder = OneHotEncoder(sparse_output=False)
# 对类别进行独热编码
one_hot = encoder.fit_transform(animals_2d)
print("Scikit-learn 独热编码结果:")
print(one_hot)
输出结果:
Scikit-learn 独热编码结果:
[[0. 1. 0.]
[1. 0. 0.]
[0. 0. 1.]]
最后,我们通过一个对真实数据集的处理,来加深对one-hot向量的理解。
我们选取Adults数据用于ont-hot向量转换的实战。该数据集来自美国1994年人口普查数据,因此也称作“人口普查收入”数据集,共包含48842条记录。该数据集有14个属性变量,其中有8个是类别离散型变量,可以很好地用于one-hot编码的实战。
首先,我们导入数据并查看基本信息。
from sklearn.datasets import fetch_openml
# 加载 Adult 数据集
adult = fetch_openml(name='adult', version=2, as_frame=True)
# 获取特征和标签
X = adult.data # 特征
y = adult.target # 标签
# 查看数据集的基本信息
print("特征名称:", adult.feature_names)
print("标签名称:", adult.target_names)
print(X.info())
print(y.info())
数据集的基本信息如下:
输出结果:
特征名称: ['age', 'workclass', 'fnlwgt', 'education', 'education-num', 'marital-status', 'occupation', 'relationship', 'race', 'sex', 'capital-gain', 'capital-loss', 'hours-per-week', 'native-country']
标签名称: ['class']
特征信息:
RangeIndex: 48842 entries, 0 to 48841
Data columns (total 14 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 age 48842 non-null int64
1 workclass 46043 non-null category
2 fnlwgt 48842 non-null int64
3 education 48842 non-null category
4 education-num 48842 non-null int64
5 marital-status 48842 non-null category
6 occupation 46033 non-null category
7 relationship 48842 non-null category
8 race 48842 non-null category
9 sex 48842 non-null category
10 capital-gain 48842 non-null int64
11 capital-loss 48842 non-null int64
12 hours-per-week 48842 non-null int64
13 native-country 47985 non-null category
dtypes: category(8), int64(6)
memory usage: 2.6 MB
标签信息:
RangeIndex: 48842 entries, 0 to 48841
Series name: class
Non-Null Count Dtype
-------------- -----
48842 non-null category
dtypes: category(1)
memory usage: 47.9 KB
由于部分数据存在缺失值,我们有必要进行数据的预处理,从上面输出结果中可以发现,workclass、occupation和native-country存在缺失值,但由于缺失的数据占比不大,我们可以去除这些缺失值,留下不存在缺失属性的数据。不过这并不是本次实验的重点,不必过多关注去除缺失值数据的代码。
import pandas as pd
# 去除掉存在缺失值的数据
X = X[X['workclass'] != ' ?']
X = X[X['occupation'] != ' ?']
X = X[X['native-country'] != ' ?']
# 获取分类数据
cat_cols = ['workclass', 'education', 'marital-status',
'occupation', 'relationship', 'race', 'sex', 'native-country']
cat_df = X[cat_cols]
# 利用pandas转换为one-hot向量
cat_one_hot = pd.get_dummies(cat_df, dtype=int)
data_all = pd.concat([cat_one_hot, X[['age', 'fnlwgt', 'education-num',
'capital-gain', 'capital-loss',
'hours-per-week']]], axis=1)
print(data_all.shape)
print(data_all.head())
这里我们以pandas的get_dummies()为例进行独热编码,对于非类别数据,我们不需要进行编码,所以我们取出所有数据的分类属性的部分,然后转换成独热编码表示。最后将数值特征与分类特征独热编码拼接,就得到了新的数据集,此数据集将所有分类数据进行了独热编码,便于后续传入模型进行训练。
我们可以打印编码后的数据集尺寸,并获取前5行查看:
输出结果:
(48842, 105)
workclass_Federal-gov workclass_Local-gov workclass_Never-worked \
0 0 0 0
1 0 0 0
2 0 1 0
3 0 0 0
4 0 0 0
workclass_Private workclass_Self-emp-inc workclass_Self-emp-not-inc \
0 1 0 0
1 1 0 0
2 0 0 0
3 1 0 0
4 0 0 0
workclass_State-gov workclass_Without-pay education_10th education_11th \
0 0 0 0 1
1 0 0 0 0
2 0 0 0 0
3 0 0 0 0
4 0 0 0 0
... native-country_Trinadad&Tobago native-country_United-States \
0 ... 0 1
1 ... 0 1
2 ... 0 1
3 ... 0 1
4 ... 0 1
native-country_Vietnam native-country_Yugoslavia age fnlwgt \
0 0 0 25 226802
1 0 0 38 89814
2 0 0 28 336951
3 0 0 44 160323
4 0 0 18 103497
education-num capital-gain capital-loss hours-per-week
0 7 0 0 40
1 9 0 0 50
2 12 0 0 40
3 10 7688 0 40
4 10 0 0 30
[5 rows x 105 columns]
实验总结与反思
这一小节,我们了解了什么是one-hot向量,为什么要有one-hot向量,完成了手动构建one-hot向量,并利用第三方库工具进行one-hot编码。最后对一个真实的数据集完成了one-hot编码,相信你对于one-hot独热编码现在有了更加深入的理解!
不过,one-hot编码并不是万能的,它仍然有着一定的缺陷。比如,我们最后对Adult数据集进行了独热编码,编码后的数据集维度来到了105维,这就是one-hot编码的最明显的一个问题,它会导致编码后的特征矩阵维度很高并且非稀疏。我们查看编码后的数据就会发现绝大部分的值都是0,这会导致训练模型时占用更多的内存,且降低计算的效率。独热编码遇到缺失值也会增加特征的维度,这无疑更进一步加大计算和存储开销,且意义不大,甚至可能影响模型最后的性能。
同时,独热编码不能捕捉到相关类别之间的关系,因为它将每个类别视为完全独立的。拿动物类别举例:“猫、狗、鸟”,按照我们的常识和直觉,猫和狗都不会飞,且有更多的相似特征,编码向量之间的距离应该要小一些,而鸟与猫、狗之间的距离应该要大一些,但是独热编码并不能体现出这种关系。要想解决这种问题,我们可以使用嵌入(Embedding)方法来捕捉语义之间的关系,大家感兴趣的可以自行搜索。

Comments NOTHING