类别数据
与数值特征不同,类别数据往往更难被计算机理解,主要分为序数和标称。
序数具有顺序,比如衣服尺码中有XL>L>M等
标称不含任何顺序,特征之间相互独立。
处理序数特征
为了让算法正确解读序数特征,我们需要用整数来表示。我们可以定义映射关系,训练后再反向映射,比如:
import pandas as pd
df = pd.DataFrame([['green', 'M', 10.1, 'class2'],
['red', 'L', 13.5, 'class1'],
['blue', 'XL', 15.3, 'class2']])
df.columns = ['color', 'size', 'price', 'classlabel']
size_mapping = {'XL': 3,
'L': 2,
'M': 1}
df['size'] = df['size'].map(size_mapping)
查看映射关系:
inv_size_mapping = {v: k for k, v in size_mapping.items()}
df['size'].map(inv_size_mapping)
处理标称数据
与上述不同的是,标称数据没有顺序,所以可以更方便地自动获取映射:
# 获取这列数据,用unique去除重复,再为每种数据映射到数字
class_mapping = {label: idx for idx, label in enumerate(np.unique(df['classlabel']))}
df['classlabel'] = df['classlabel'].map(class_mapping)
# 反向映射
inv_class_mapping = {v: k for k, v in class_mapping.items()}
df['classlabel'] = df['classlabel'].map(inv_class_mapping)
在ski-learn中也可以直接调用LabelEncoder类来实现:
from sklearn.preprocessing import LabelEncoder
class_le = LabelEncoder()
y = class_le.fit_transform(df['classlabel'].values)
# y = array([1, 0, 1])
# 反向映射
class_le.inverse_transform(y)
# >> array(['class2', 'class1', 'class2'], dtype=object)
独热编码
虽然我们将标称数据映射成了整数,并声称其没有顺序,但机器学习算法会认为1大于0,2大于1,从而影响算法的偏好。
解决这个问题的常用方案是独热编码。我们将每一个特征映射成同样大小的向量。比如:
array = [1, 0, 1, 2]
将会变成:
array2 = [ [0, 1, 0],
[1, 0, 0],
[0, 1, 0],
[0, 0, 1]]
注意
使用独热编码要小心多重共线性,即所有的特征呈现出了一个相关性(如果n种取值中前n-1个都预测为0,那第n个一定是1)。如果发生了特征高度相关,会导致矩阵求逆是很难的,且会降低数据稳定性。
所以,我们可以简单粗暴地删除一个特征列,不用担心丢失信息。
代码实现
最方便的是pandas:
pd.get_dummies(df[['price', 'color', 'size']], drop_first=True)
或者用sklearn中的OneHotEncoder:
from sklearn.preprocessing import OneHotEncoder
X = df[['color', 'size', 'price']].values
color_ohe = OneHotEncoder()
# 此处只转换单列
color_ohe.fit_transform(X[:, 0].reshape(-1, 1)).toarray()
如果想转化多列,ColumnTransformer支持同时对不同列使用不同的转化策略。
# 导入ColumnTransformer类,它允许对数据集的不同列应用不同的转换器
from sklearn.compose import ColumnTransformer
# 从数据帧df中选择'color', 'size', 'price'三列,并将它们转换为NumPy数组赋值给X
X = df[['color', 'size', 'price']].values
# 定义ColumnTransformer实例c_transf
c_transf = ColumnTransformer([
# 定义第一个转换器:对第0列('color'列)进行独热编码
('onehot', OneHotEncoder(), [0]),
# 定义第二个转换器:对于第1列和第2列('size'和'price'列)不进行任何转换,直接保留原数据
# 'nothing'是转换器的名称,'passthrough'是一个特殊的转换器,表示这些列的数据将被直接传递,不做任何修改
('nothing', 'passthrough', [1, 2])
])
# 使用X数据拟合c_transf,并对其进行转换
# fit_transform()方法首先会根据X数据拟合转换器,然后应用这些转换器对X数据进行转换
c_transf.fit_transform(X).astype(float)
降维
数据的维度直接决定了训练模型的难度和时间成本,维度过高也有过拟合的风险。主要有两种降维技术:特征选择和特征提取。
序列特征选择算法
经典的序列特征选择算法是序列后向选择(SBS),使用贪心的思想,每次选择一个性能损失最小的特征并删除,直到新的特征子空间包含了需要的特征数量。
(1)假设
d
d
d为特征空间
X
d
X_{d}
Xd的维数
(2)找到影响最小的特征
x
−
∈
X
d
x^{-}\in{X_{d}}
x−∈Xd,
x
−
=
a
r
g
m
a
x
J
(
X
d
−
x
)
x^{-}=argmaxJ(X_{d}-x)
x−=argmaxJ(Xd−x) ------max指找到最大值,arg指找到对应使J最大的
x
x
x。
(3)从特征集中去除特征
x
−
x^{-}
x−:
X
d
−
1
=
X
d
−
x
−
X_{d-1}=X_{d}-x^{-}
Xd−1=Xd−x−;
d
:
=
d
−
1
d:=d-1
d:=d−1。
(4)若已经减到期望的特征数,则停止,否则跳转步骤2。
实现
ski-learn并没有现成的SBS模块,但《python机器学习》帮我们写了一个类,以后直接拿来用就好了
# 导入必要的库和模块
from sklearn.base import clone # 用于深度克隆模型,避免原始模型被修改
from itertools import combinations # 用于生成组合
import numpy as np # 用于数学和数组操作
from sklearn.metrics import accuracy_score # 用于评估模型预测的准确性
from sklearn.model_selection import train_test_split # 用于数据集的划分
# 定义SBS类
class SBS():
def __init__(self, estimator, k_features, scoring=accuracy_score, test_size=0.25, random_state=1):
# 初始化方法
self.scoring = scoring # 评分函数,用于评估模型性能,默认为accuracy_score
self.estimator = clone(estimator) # 克隆所需的模型对象,用于避免原始模型被修改
self.k_features = k_features # 目标特征数量
self.test_size = test_size # 测试集大小
self.random_state = random_state # 随机状态,用于确保随机过程的可重复性
def fit(self, X, y):
# 模型拟合方法
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=self.test_size, random_state=self.random_state)
# 将数据集划分为训练集和测试集
dim = X_train.shape[1] # 初始特征数量
self.indices_ = tuple(range(dim)) # 初始特征索引
self.subsets_ = [self.indices_] # 存储所有特征子集
score = self._calc_score(X_train, y_train, X_test, y_test, self.indices_)
# 计算当前特征集的评分
self.scores_ = [score] # 存储每次迭代的评分
# 主循环,通过逐步移除特征来减少特征数量
while dim > self.k_features:
scores = []
subsets = []
# 遍历当前特征集的所有可能子集(每个子集少一个特征)
for p in combinations(self.indices_, r=dim - 1):
score = self._calc_score(X_train, y_train, X_test, y_test, p)
# 计算子集的评分
scores.append(score)
subsets.append(p)
# 将评分和子集保存
best = np.argmax(scores) # 找到评分最高的子集
self.indices_ = subsets[best]
# 更新最佳子集
self.subsets_.append(self.indices_)
# 保存当前最佳子集
dim -= 1 # 减少特征数量
# 保存当前迭代的评分
self.scores_.append(scores[best])
self.k_score_ = self.scores_[-1] # 最终特征集的评分
return self
def transform(self, X):
# 特征转换方法
return X[:, self.indices_] # 返回保留的特征列
def _calc_score(self, X_train, y_train, X_test, y_test, indices):
# 计算评分的辅助方法
self.estimator.fit(X_train[:, indices], y_train) # 在训练集上拟合模型
y_pred = self.estimator.predict(X_test[:, indices]) # 预测测试集标签
score = self.scoring(y_test, y_pred) # 评估预测结果
return score
import matplotlib.pyplot as plt # 导入matplotlib.pyplot模块,用于绘制图表
from sklearn.neighbors import KNeighborsClassifier # 导入K近邻分类器
# 创建一个K近邻分类器实例,设定k值为5
knn = KNeighborsClassifier(n_neighbors=5)
# 从上文的代码段中,我们已经创建了SBS实例并用训练数据训练了该实例
# 这里的sbs是已经使用X_train_std和y_train数据训练过的SBS实例
# 这个SBS实例用于选择特征,其中k_features设为1,这意味着SBS将递归地减少特征直到只剩1个特征
sbs = SBS(knn, k_features=1)
# 用训练集数据调用SBS实例的fit方法,对特征进行选择
sbs.fit(X_train_std, y_train)
# 创建一个列表,其中的元素是SBS在每轮迭代中选择的特征子集的长度
k_feat = [len(k) for k in sbs.subsets_]
# 绘制特征数量与模型准确率的关系图
plt.plot(k_feat, sbs.scores_, marker='o') # 以特征数量为x轴,准确率为y轴,绘制散点图
plt.ylim([0.7, 1.02]) # 设置y轴的范围为0.7到1.02
plt.ylabel('Accuracy') # 设置y轴的标签为'Accuracy'
plt.xlabel('Number of features') # 设置x轴的标签为'Number of features'
plt.grid() # 显示网格线
plt.tight_layout() # 自动调整子图参数,使之填充整个图像区域
# 保存图表为PNG文件,dpi参数指定图像分辨率,此行注释掉了,不会执行
# plt.savefig('images/04_08.png', dpi=300)
# 显示图表
plt.show()
准确率随维度变化:
使用随机森林评估特征的重要性
在随机森林或者决策树模型中,在构建每棵树的过程中,每个特征在节点上被用来分割数据集时,都会导致节点的“杂质度”(通常是基尼不纯度或熵)的减少。这个减少量可以被视为该特征对模型预测能力的贡献度。也称为决策树的平均杂质度衰减(Mean Decrease Impurity, MDI)。这是评价一个特征重要性的指标。
scikit-learn中提供的随机森林模型已经为我们收集好了特征的重要程度:
from sklearn.ensemble import RandomForestClassifier
# 定义一个列表,其中包含数据集中除目标变量以外的所有特征的标签
feat_labels = df_wine.columns[1:]
# 创建一个随机森林分类器实例,设置树的数量为500,随机种子为1以保证结果可复现
forest = RandomForestClassifier(n_estimators=500,
random_state=1)
# 训练
forest.fit(X_train, y_train)
# 计算随机森林中每个特征的重要性,返回一个数组,数组中的值表示每个特征的重要性得分
importances = forest.feature_importances_
# 对特征的重要性得分进行排序,生成排序后的特征索引数组,[::-1]表示按照从大到小的顺序排序
indices = np.argsort(importances)[::-1]
# 遍历训练数据集中所有特征
for f in range(X_train.shape[1]):
# 打印每个特征的排名、名称和重要性得分
print("%2d) %-*s %f" % (f + 1, 30,
feat_labels[indices[f]],
importances[indices[f]]))
# 可视化
plt.title('Feature Importance')
# 绘制特征重要性的柱状图,x轴为特征的索引,y轴为特征的重要性得分
plt.bar(range(X_train.shape[1]),
importances[indices],
align='center') # 柱状图居中对齐
# 设置x轴的刻度标签,使用特征的名称,刻度标签旋转90度以适应图表
plt.xticks(range(X_train.shape[1]),
feat_labels[indices], rotation=90)
# 设置x轴的范围,确保所有特征都被包含在图表中
plt.xlim([-1, X_train.shape[1]])
# 调整图表布局,使其紧凑且适合显示
plt.tight_layout()
# plt.savefig('images/04_09.png', dpi=300)
plt.show()
然后我们可以对特征进行挑选以降维:
from sklearn.feature_selection import SelectFromModel # 导入SelectFromModel类,用于基于模型的特征选择
# 创建SelectFromModel实例,模型为随机森林分类器,设置阈值为0.1,表示选择所有特征重要性大于等于0.1的特征
# prefit参数为True表示提供的模型estimator已经被预训练过,SelectFromModel不需要再进行训练
sfm = SelectFromModel(forest, threshold=0.1, prefit=True)
# 使用SelectFromModel实例对训练数据集X_train进行转换,返回一个只包含选定特征的新数据集
X_selected = sfm.transform(X_train)
# 打印满足阈值条件的特征数量,即特征重要性大于等于0.1的特征数量
print('Number of features that meet this threshold criterion:',
X_selected.shape[1])
# 遍历选择后的特征,打印其排名、名称和重要性得分。这里实际上是对所有特征进行遍历,
# 但由于X_selected只包含选择的特征,因此实际上只打印出满足条件的那些特征
for f in range(X_selected.shape[1]):
print("%2d) %-*s %f" % (f + 1, 30,
feat_labels[indices[f]],
importances[indices[f]]))
主成分分析实现降维
对于数学零基础的,强推这篇:
用通俗易懂的方式讲解:主成分分析(PCA)算法及案例
主成分分析(PCA),属于一种无监督线性变换技术。如下图,令
x
1
,
x
2
x_{1}, x_{2}
x1,x2为原始特征轴,而
P
C
1
,
P
C
2
PC1, PC2
PC1,PC2为主成分方向,可以理解为方差最大的方向。
降维目标:
(1)找到方差最大的方向,通过保留这个方向的信息,有助于最大化保留数据中的特征。
(2)选择主成分,将原始数据投影到新坐标系中,使各个维度正交,有助于减少特征的冗余,实现降维
步骤1: 标准化
与之前一样,不赘述:
import pandas as pd
df_wine = pd.read_csv('https://archive.ics.uci.edu/ml/'
'machine-learning-databases/wine/wine.data',
header=None)
from sklearn.model_selection import train_test_split
X, y = df_wine.iloc[:, 1:].values, df_wine.iloc[:, 0].values
X_train, X_test, y_train, y_test = \
train_test_split(X, y, test_size=0.3,
stratify=y,
random_state=0)
步骤2: 构造协方差矩阵
协方差定义:
协方差(Covariance)是统计学和概率论中一个重要的概念,用于度量两个随机变量的线性相关程度。它描述了两个变量如何一起变化,即一个变量的值相对于其平均值变化时,另一个变量的值相对于其平均值变化的趋势和程度。
对于两个随机变量
X
X
X和
Y
Y
Y,它们的协方差
C
o
v
(
X
,
Y
)
Cov(X, Y)
Cov(X,Y)定义为:
C o v ( X , Y ) = E [ ( X − μ x ) ( Y − μ y ) ] Cov(X,Y)=E[(X-\mu_{x})(Y-\mu_{y})] Cov(X,Y)=E[(X−μx)(Y−μy)]
= 1 n ∑ i = 1 n ( x i − x ˉ ) ( y i − y ˉ ) =\frac{1}{n}\sum_{i=1}^{n}(x_{i}-\bar{x})(y_{i}-\bar{y}) =n1∑i=1n(xi−xˉ)(yi−yˉ)
假设数据集维度为 d d d,则我们构造一个 d × d d\times d d×d的 对称协方差矩阵 Σ \Sigma Σ,来存储不同特征之间的协方差,例如,特征 x j x_{j} xj和 x k x_{k} xk的协方差可以计算成:
σ j k = 1 n ∑ i = 1 n ( x j ( i ) − μ j ) ( y k ( i ) − μ k ) \sigma _{jk}=\frac{1}{n}\sum_{i=1}^{n}(x_{j}^{(i)}-\mu _{j})(y_{k}^{(i)}-\mu _{k}) σjk=n1∑i=1n(xj(i)−μj)(yk(i)−μk)
然后我们找到它的特征向量 v ⃗ \vec{v} v和特征值(标量) λ \lambda λ,满足:
Σ v ⃗ = λ v ⃗ \Sigma \vec{v}=\lambda \vec{v} Σv=λv
这里的特征向量代表了协方差矩阵的主成分(最大方差的方向),而特征值定义了它们的大小。注意,一个
d
×
d
d \times d
d×d的矩阵最多有
d
d
d个特征向量。
我们使用
N
u
m
p
y
Numpy
Numpy的 linalg.eig 函数获取特征向量和特征值:
import numpy as np
# 假设X_train_std是一个二维NumPy数组,包含了经过预处理(例如,标准化)的训练数据集。
# 计算X_train_std的协方差矩阵。使用.T来转置数据集,这是因为np.cov()方法期望每一列代表一个变量,每一行代表一个观测值。当调用np.cov()时,我们通常转置数据,以确保每一行代表一个变量(特征),这在计算协方差矩阵时是必需的。
cov_mat = np.cov(X_train_std.T)
# 使用NumPy的线性代数子模块linalg的eig()函数来计算cov_mat的特征值和特征向量。
# eig()函数返回两个对象:eigen_vals是一个一维数组,包含了cov_mat的特征值;eigen_vecs是一个二维数组,其中的每一列是与特征值相对应的特征向量。
eigen_vals, eigen_vecs = np.linalg.eig(cov_mat)
print('\nEigenvalues \n%s' % eigen_vals) # 输出所有的特征值
步骤3: 特征变换
将协方差矩阵分解为特征对(一个特征值和一个特征向量)后,我们进行以下步骤完成特征变换,将原数据集变换到新的主成分轴:
(1)选出前
k
k
k大的特征值对应的特征向量,那么
k
k
k就是新特征子空间的维数。
(2)用这
k
k
k个特征向量构建投影矩阵
W
W
W
(3)用投影矩阵
W
W
W变换
d
d
d维的原数据集
X
X
X,获得新的
k
k
k维特征子空间。
我们先获取投影矩阵 W W W:
# eigen_vals是一个包含协方差矩阵特征值的一维数组,而eigen_vecs是一个包含协方差矩阵特征向量的二维数组。
# 创建一个列表,其中的每个元素都是一个元组,元组的第一个元素是特征值的绝对值(因为特征值可能为负),第二个元素是与该特征值对应的特征向量。
eigen_pairs = [(np.abs(eigen_vals[i]), eigen_vecs[:, i])
for i in range(len(eigen_vals))]
# 根据特征值的绝对值大小对eigen_pairs列表进行排序,特征值大的特征向量排在前面。
# 使用sort()方法,key参数指定排序依据,这里是一个lambda函数,返回元组的第一个元素(特征值的绝对值),reverse=True表示降序排序。
eigen_pairs.sort(key=lambda k: k[0], reverse=True)
# 选择排序后前两个主成分对应的特征向量,构建变换矩阵W。
# 通过np.hstack()函数水平堆叠(即按列堆叠)两个特征向量,形成一个二维数组。[:, np.newaxis]将一维特征向量转换为二维数组,便于水平堆叠。
w = np.hstack((eigen_pairs[0][1][:, np.newaxis],
eigen_pairs[1][1][:, np.newaxis]))
print('Matrix W:\n', w) # 构建好的投影矩阵W,它包含了两个主成分方向。
然后,我们通过计算矩阵点积将整个数据集变换成两个主成分:
X ′ = X W X^{'}=XW X′=XW
X_train_pca = X_train_std.dot(w)
colors = ['r', 'b', 'g']
markers = ['s', 'x', 'o']
for l, c, m in zip(np.unique(y_train), colors, markers):
plt.scatter(X_train_pca[y_train == l, 0],
X_train_pca[y_train == l, 1],
c=c, label=l, marker=m)
plt.xlabel('PC 1')
plt.ylabel('PC 2')
plt.legend(loc='lower left')
plt.tight_layout()
# plt.savefig('images/05_03.png', dpi=300)
plt.show()
到这里,主成分分析部分已经完成,接下来就可以用线性分类器等方法继续训练模型
使用scikit-learn实现主成分分析
scikit-learn中提供了转换器类PCA来实现主成分分析。
先在plot_decision_regions_script.py文件中实现一个决策区域可视化的脚本:
from matplotlib.colors import ListedColormap
def plot_decision_regions(X, y, classifier, resolution=0.02):
# setup marker generator and color map
markers = ('s', 'x', 'o', '^', 'v')
colors = ('red', 'blue', 'lightgreen', 'gray', 'cyan')
cmap = ListedColormap(colors[:len(np.unique(y))])
# plot the decision surface
x1_min, x1_max = X[:, 0].min() - 1, X[:, 0].max() + 1
x2_min, x2_max = X[:, 1].min() - 1, X[:, 1].max() + 1
xx1, xx2 = np.meshgrid(np.arange(x1_min, x1_max, resolution),
np.arange(x2_min, x2_max, resolution))
Z = classifier.predict(np.array([xx1.ravel(), xx2.ravel()]).T)
Z = Z.reshape(xx1.shape)
plt.contourf(xx1, xx2, Z, alpha=0.4, cmap=cmap)
plt.xlim(xx1.min(), xx1.max())
plt.ylim(xx2.min(), xx2.max())
# plot examples by class
for idx, cl in enumerate(np.unique(y)):
plt.scatter(x=X[y == cl, 0],
y=X[y == cl, 1],
alpha=0.6,
color=cmap(idx),
edgecolor='black',
marker=markers[idx],
label=cl)
然后写主程序进行主成分分析:
from sklearn.linear_model import LogisticRegression # 导入逻辑回归模型类。
from sklearn.decomposition import PCA # 导入PCA类,用于数据降维。
# 创建PCA实例,指定保留2个主成分。若n_components设置为None,则保留所有主成分
pca = PCA(n_components=2)
# 对训练集进行PCA变换,fit_transform()方法先拟合数据,再进行变换,返回降维后的数据。
X_train_pca = pca.fit_transform(X_train_std)
# >>pca.explained_variance_ratio_ # 这里可以输出一下解释方差比,决定要保留多少个维度
# 使用PCA模型对测试集进行变换,注意只使用transform()方法,因为模型已经在训练集上拟合好了。
X_test_pca = pca.transform(X_test_std)
# 创建逻辑回归分类器实例,设置分类策略为one-vs-rest,随机状态为1,求解器为L-BFGS-B。
lr = LogisticRegression(multi_class='ovr', random_state=1, solver='lbfgs')
# 使用训练集的降维数据训练逻辑回归模型。
lr = lr.fit(X_train_pca, y_train)
# 绘制决策边界
plot_decision_regions(X_train_pca, y_train, classifier=lr) # 即上一份代码
plt.xlabel('PC 1') # 设置x轴标签为“主成分1”。
plt.ylabel('PC 2') # 设置y轴标签为“主成分2”。
plt.legend(loc='lower left') # 添加图例,位置在左下角。
plt.tight_layout() # 调整图形布局,确保标签不被裁剪。
plt.show() # 显示图形。