如何逆转sklearn.OneHotEncoder的转换以恢复原始数据?
我用 sklearn.OneHotEncoder
对我的分类数据进行了编码,然后把这些数据输入到随机森林分类器中。看起来一切都正常,我得到了预测的结果。
有没有办法把编码反过来,恢复我的输出到最初的状态呢?
8 个回答
如果特征很密集,比如说 [1,2,4,5,6],中间有几个数字缺失。那么,我们可以把这些数字映射到对应的位置上。
>>> import numpy as np
>>> from scipy import sparse
>>> def _sparse_binary(y):
... # one-hot codes of y with scipy.sparse matrix.
... row = np.arange(len(y))
... col = y - y.min()
... data = np.ones(len(y))
... return sparse.csr_matrix((data, (row, col)))
...
>>> y = np.random.randint(-2,2, 8).reshape([4,2])
>>> y
array([[ 0, -2],
[-2, 1],
[ 1, 0],
[ 0, -2]])
>>> yc = [_sparse_binary(y[:,i]) for i in xrange(2)]
>>> for i in yc: print i.todense()
...
[[ 0. 0. 1. 0.]
[ 1. 0. 0. 0.]
[ 0. 0. 0. 1.]
[ 0. 0. 1. 0.]]
[[ 1. 0. 0. 0.]
[ 0. 0. 0. 1.]
[ 0. 0. 1. 0.]
[ 1. 0. 0. 0.]]
>>> [i.shape for i in yc]
[(4, 4), (4, 4)]
这是一种简单但有效的方法,而且通过 argmax() 很容易还原,比如:
>>> np.argmax(yc[0].todense(), 1) + y.min(0)[0]
matrix([[ 0],
[-2],
[ 1],
[ 0]])
从scikit-learn的0.20版本开始,OneHotEncoder
类的active_features_
属性已经不再推荐使用,所以我建议你使用categories_
属性。
下面这个函数可以帮助你从一个经过独热编码的矩阵中恢复原始数据:
def reverse_one_hot(X, y, encoder):
reversed_data = [{} for _ in range(len(y))]
all_categories = list(itertools.chain(*encoder.categories_))
category_names = ['category_{}'.format(i+1) for i in range(len(encoder.categories_))]
category_lengths = [len(encoder.categories_[i]) for i in range(len(encoder.categories_))]
for row_index, feature_index in zip(*X.nonzero()):
category_value = all_categories[feature_index]
category_name = get_category_name(feature_index, category_names, category_lengths)
reversed_data[row_index][category_name] = category_value
reversed_data[row_index]['target'] = y[row_index]
return reversed_data
def get_category_name(index, names, lengths):
counter = 0
for i in range(len(lengths)):
counter += lengths[i]
if index < counter:
return names[i]
raise ValueError('The index is higher than the number of categorical values')
为了测试这个函数,我创建了一个小数据集,里面包含了用户对其他用户的评分。
data = [
{'user_id': 'John', 'item_id': 'The Matrix', 'rating': 5},
{'user_id': 'John', 'item_id': 'Titanic', 'rating': 1},
{'user_id': 'John', 'item_id': 'Forrest Gump', 'rating': 2},
{'user_id': 'John', 'item_id': 'Wall-E', 'rating': 2},
{'user_id': 'Lucy', 'item_id': 'The Matrix', 'rating': 5},
{'user_id': 'Lucy', 'item_id': 'Titanic', 'rating': 1},
{'user_id': 'Lucy', 'item_id': 'Die Hard', 'rating': 5},
{'user_id': 'Lucy', 'item_id': 'Forrest Gump', 'rating': 2},
{'user_id': 'Lucy', 'item_id': 'Wall-E', 'rating': 2},
{'user_id': 'Eric', 'item_id': 'The Matrix', 'rating': 2},
{'user_id': 'Eric', 'item_id': 'Die Hard', 'rating': 3},
{'user_id': 'Eric', 'item_id': 'Forrest Gump', 'rating': 5},
{'user_id': 'Eric', 'item_id': 'Wall-E', 'rating': 4},
{'user_id': 'Diane', 'item_id': 'The Matrix', 'rating': 4},
{'user_id': 'Diane', 'item_id': 'Titanic', 'rating': 3},
{'user_id': 'Diane', 'item_id': 'Die Hard', 'rating': 5},
{'user_id': 'Diane', 'item_id': 'Forrest Gump', 'rating': 3},
]
data_frame = pandas.DataFrame(data)
data_frame = data_frame[['user_id', 'item_id', 'rating']]
ratings = data_frame['rating']
data_frame.drop(columns=['rating'], inplace=True)
如果我们要建立一个预测模型,记得在编码之前要把依赖变量(在这个例子中就是评分)从DataFrame
中删除。
ratings = data_frame['rating']
data_frame.drop(columns=['rating'], inplace=True)
然后我们就可以进行编码了。
ohc = OneHotEncoder()
encoded_data = ohc.fit_transform(data_frame)
print(encoded_data)
编码的结果是:
(0, 2) 1.0
(0, 6) 1.0
(1, 2) 1.0
(1, 7) 1.0
(2, 2) 1.0
(2, 5) 1.0
(3, 2) 1.0
(3, 8) 1.0
(4, 3) 1.0
(4, 6) 1.0
(5, 3) 1.0
(5, 7) 1.0
(6, 3) 1.0
(6, 4) 1.0
(7, 3) 1.0
(7, 5) 1.0
(8, 3) 1.0
(8, 8) 1.0
(9, 1) 1.0
(9, 6) 1.0
(10, 1) 1.0
(10, 4) 1.0
(11, 1) 1.0
(11, 5) 1.0
(12, 1) 1.0
(12, 8) 1.0
(13, 0) 1.0
(13, 6) 1.0
(14, 0) 1.0
(14, 7) 1.0
(15, 0) 1.0
(15, 4) 1.0
(16, 0) 1.0
(16, 5) 1.0
编码完成后,我们可以使用上面定义的reverse_one_hot
函数来反向操作,像这样:
reverse_data = reverse_one_hot(encoded_data, ratings, ohc)
print(pandas.DataFrame(reverse_data))
这样就得到了:
category_1 category_2 target
0 John The Matrix 5
1 John Titanic 1
2 John Forrest Gump 2
3 John Wall-E 2
4 Lucy The Matrix 5
5 Lucy Titanic 1
6 Lucy Die Hard 5
7 Lucy Forrest Gump 2
8 Lucy Wall-E 2
9 Eric The Matrix 2
10 Eric Die Hard 3
11 Eric Forrest Gump 5
12 Eric Wall-E 4
13 Diane The Matrix 4
14 Diane Titanic 3
15 Diane Die Hard 5
16 Diane Forrest Gump 3
使用 numpy.argmax()
函数,并设置参数 axis = 1
。
举个例子:
ohe_encoded = np.array([[0, 0, 1], [0, 1, 0], [0, 1, 0], [1, 0, 0]])
ohe_encoded
> array([[0, 0, 1],
[0, 1, 0],
[0, 1, 0],
[1, 0, 0]])
np.argmax(ohe_encoded, axis = 1)
> array([2, 1, 1, 0], dtype=int64)
只需要计算编码值和 ohe.active_features_
的点积就可以了。这种方法对稀疏和密集的表示方式都适用。举个例子:
from sklearn.preprocessing import OneHotEncoder
import numpy as np
orig = np.array([6, 9, 8, 2, 5, 4, 5, 3, 3, 6])
ohe = OneHotEncoder()
encoded = ohe.fit_transform(orig.reshape(-1, 1)) # input needs to be column-wise
decoded = encoded.dot(ohe.active_features_).astype(int)
assert np.allclose(orig, decoded)
关键在于,OHE模型的 active_features_
属性代表了每个二进制列的原始值。因此,我们可以通过简单地与 active_features_
计算点积来解码二进制编码的数字。对于每个数据点,只有一个 1
,它的位置对应于原始值。
一个比较系统的方法是先准备一些测试数据,然后用这些数据来理解一下sklearn.OneHotEncoder
的源代码。如果你不太关心具体的工作原理,只想要一个快速的答案,可以直接跳到最后。
X = np.array([
[3, 10, 15, 33, 54, 55, 78, 79, 80, 99],
[5, 1, 3, 7, 8, 12, 15, 19, 20, 8]
]).T
n_values_
第1763到1786行定义了n_values_
这个参数。如果你设置n_values='auto'
(这是默认设置),它会自动确定。你也可以为所有特征指定一个最大值(整数),或者为每个特征指定一个最大值(数组)。假设我们使用默认设置,那么接下来的代码就会执行:
n_samples, n_features = X.shape # 10, 2
n_values = np.max(X, axis=0) + 1 # [100, 21]
self.n_values_ = n_values
feature_indices_
接下来计算feature_indices_
参数。
n_values = np.hstack([[0], n_values]) # [0, 100, 21]
indices = np.cumsum(n_values) # [0, 100, 121]
self.feature_indices_ = indices
所以feature_indices_
其实就是n_values_
的累积和,前面加一个0。
稀疏矩阵构建
接下来,从数据构建一个scipy.sparse.coo_matrix
。它是通过三个数组初始化的:稀疏数据(全是1),行索引和列索引。
column_indices = (X + indices[:-1]).ravel()
# array([ 3, 105, 10, 101, 15, 103, 33, 107, 54, 108, 55, 112, 78, 115, 79, 119, 80, 120, 99, 108])
row_indices = np.repeat(np.arange(n_samples, dtype=np.int32), n_features)
# array([0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9], dtype=int32)
data = np.ones(n_samples * n_features)
# array([ 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.])
out = sparse.coo_matrix((data, (row_indices, column_indices)),
shape=(n_samples, indices[-1]),
dtype=self.dtype).tocsr()
# <10x121 sparse matrix of type '<type 'numpy.float64'>' with 20 stored elements in Compressed Sparse Row format>
注意,coo_matrix
会立即转换为scipy.sparse.csr_matrix
。使用coo_matrix
作为中间格式是因为它“方便在稀疏格式之间快速转换”。
active_features_
现在,如果n_values='auto'
,稀疏的csr矩阵会被压缩,只保留有活跃特征的列。如果sparse=True
,就返回稀疏的csr_matrix
,否则在返回之前会将其转为密集格式。
if self.n_values == 'auto':
mask = np.array(out.sum(axis=0)).ravel() != 0
active_features = np.where(mask)[0] # array([ 3, 10, 15, 33, 54, 55, 78, 79, 80, 99, 101, 103, 105, 107, 108, 112, 115, 119, 120])
out = out[:, active_features] # <10x19 sparse matrix of type '<type 'numpy.float64'>' with 20 stored elements in Compressed Sparse Row format>
self.active_features_ = active_features
return out if self.sparse else out.toarray()
解码
现在我们来反向操作。我们想知道如何根据返回的稀疏矩阵恢复X
,这个矩阵是和上面提到的OneHotEncoder
特征一起返回的。假设我们实际上运行了上面的代码,创建了一个新的OneHotEncoder
实例,并对我们的数据X
运行了fit_transform
。
from sklearn import preprocessing
ohc = preprocessing.OneHotEncoder() # all default params
out = ohc.fit_transform(X)
解决这个问题的关键是理解active_features_
和out.indices
之间的关系。对于csr_matrix
,索引数组包含每个数据点的列号。然而,这些列号并不一定是排序好的。为了排序,我们可以使用sorted_indices
方法。
out.indices # array([12, 0, 10, 1, 11, 2, 13, 3, 14, 4, 15, 5, 16, 6, 17, 7, 18, 8, 14, 9], dtype=int32)
out = out.sorted_indices()
out.indices # array([ 0, 12, 1, 10, 2, 11, 3, 13, 4, 14, 5, 15, 6, 16, 7, 17, 8, 18, 9, 14], dtype=int32)
我们可以看到,在排序之前,索引实际上是沿着行反向排列的。换句话说,它们的顺序是最后一列在前,第一列在后。从前两个元素可以看出:[12, 0]。0对应于X
第一列的3,因为3是最小元素,所以它被分配到第一个活跃列。12对应于X
第二列的5。由于第一行占用了10个不同的列,第二列的最小元素(1)得到了索引10。下一个最小的(3)得到了索引11,第三个最小的(5)得到了索引12。排序后,索引的顺序就如我们所期待的那样。
接下来我们看看active_features_
:
ohc.active_features_ # array([ 3, 10, 15, 33, 54, 55, 78, 79, 80, 99, 101, 103, 105, 107, 108, 112, 115, 119, 120])
注意这里有19个元素,这对应于我们数据中不同元素的数量(有一个元素8被重复了一次)。还要注意这些元素是按顺序排列的。X
第一列的特征是一样的,而第二列的特征只是加了100,这对应于ohc.feature_indices_[1]
。
回头看看out.indices
,我们可以看到最大列号是18,这比我们编码中的19个活跃特征少1。稍微想一下这个关系,就会发现ohc.active_features_
的索引对应于ohc.indices
中的列号。这样我们就可以解码了:
import numpy as np
decode_columns = np.vectorize(lambda col: ohc.active_features_[col])
decoded = decode_columns(out.indices).reshape(X.shape)
这给我们带来了:
array([[ 3, 105],
[ 10, 101],
[ 15, 103],
[ 33, 107],
[ 54, 108],
[ 55, 112],
[ 78, 115],
[ 79, 119],
[ 80, 120],
[ 99, 108]])
我们可以通过从ohc.feature_indices_
中减去偏移量来恢复原始特征值:
recovered_X = decoded - ohc.feature_indices_[:-1]
array([[ 3, 5],
[10, 1],
[15, 3],
[33, 7],
[54, 8],
[55, 12],
[78, 15],
[79, 19],
[80, 20],
[99, 8]])
注意,你需要知道X
的原始形状,它就是(n_samples, n_features)
。
总结
给定一个名为ohc
的sklearn.OneHotEncoder
实例,从ohc.fit_transform
或ohc.transform
输出的编码数据(scipy.sparse.csr_matrix
)称为out
,以及原始数据的形状(n_samples, n_feature)
,可以用以下方式恢复原始数据X
:
recovered_X = np.array([ohc.active_features_[col] for col in out.sorted_indices().indices])
.reshape(n_samples, n_features) - ohc.feature_indices_[:-1]