1 特征工程简介
1.1 特征工程的评估步骤
(1) 在应用任何特征工程之前,得到机器学习模型的基准性能;
(2) 应用一种或多种特征工程;
(3) 对于每种特征工程,获取一个性能指标,并与基准性能进行对比;
(4) 如果性能的增量(变化)大于某个阈值(一般由我们定义),则认为这种特征工程是有益
的,并在机器学习流水线上应用;
(5) 性能的改变一般以百分比计算(如果基准性能从 40%的准确率提升到 76%的准确率,那
么改变是 90%)。
1.2 评估监督学习
分类问题:准确率
回归问题:MSE
1.3 评估无监督学习
轮廓系数,-1和1之间
2 特征理解
2.1 数据结构的有无
拿到一个新的数据集后,首要任务是确认数据是结构化还是非结构化的。
-
结构化(有组织)数据 :==可以分成观察值和特征的数据,一般以表格的形式组织(行是观察值,列是特征)。
-
非结构化(无组织)数据 :==作为自由流动的实体,不遵循标准组织结构(例如表格)的数据。通常,非结构化数据在我们看来是一团 数据,或只有一个特征(列)
2.2 定量数据和定性数据
-
定量数据本质上是数值,应该是衡量某样东西的数量。
-
定性数据本质上是类别,应该是描述某样东西的性质。
基本示例:
- 以华氏度或摄氏度表示的气温是定量的;
- 阴天或晴天是定性的;
- 白宫参观者的名字是定性的;
- 献血的血量是定量的。
2.3 数据的四个等级
- 定类等级(nominal level )
- 定序等级(ordinal level )
- 定距等级(interval level )
- 定比等级(ratio level )
2.3.1 定类等级
定类等级是数据的第一个等级,其结构最弱。这个等级的数据只按名称分类。例如,血型(A 、B 、O 和AB 型)、动物物种和人名。这些数据都是定性的。
可以执行的数学操作
- 不能执行任何定量数学操作,例如加法或除法
- 可以用Pandas中的value_counts进行计数
例如:
salary_ranges['Grade'].value_counts().head()
0000 61
07450 12
07170 9
07420 9
06870 9
Name: Grade, dtype: int64
绘制条形图:
salary_ranges['Grade'].value_counts().sort_values(ascending=False).head(20).plot(kind='bar')
2.3.2 定序等级
定序等级继承了定类等级的所有属性,而且有重要的附加属性:
-
定序等级的数据可以自然排序 ;
-
这意味着,可以认为列中的某些数据比其他数据更好或更大。
-
和定类等级一样,定序等级的天然数据属性仍然是类别,即使用数来表示类别也是如此。
例子包括: -
使用李克特量表① (比如1 ~10 的评分)
-
考试的成绩(F 、D 、C 、B 、A )
==可以执行的数学操作
- 可以像定类等级那样进行计数
- 也可以引入比较和排序
- 能计算中位数和百分位数
- 对于中位数和百分位数,我们可以绘制茎叶图和箱线图
2.3.3 定距等级
例子:
定距等级的一个经典例子是温度。如果美国得克萨斯州的温度是32℃ ,阿拉斯加州的温度是4℃ ,那么可以计算出32 - 4 = 28℃ 的温差。这个例子看上去简单,但是回首之前的两个等级,我们从未对数据执行过这种操作。
可以执行的数学操作
- 加减运算
- 算术平均数
- 标准差
2.3.4 定比等级
在这个等级上,可以说我们拥有最高程度的控制和数学运算能力。和定距等级一样,我们在定比等级上处理的也是定量数据。这里不仅继承了定距等级的加减运算,而且有了一个绝对零点 的概念,可以做乘除运算。
==可以执行的数学操作
- 乘除
例子:
当处理金融数据时,我们几乎肯定要计算一些货币的值。货币处于定比等级,因为“零资金”这个概念可以存在。那么我们就可以说:
-
$100 是$50 的两倍 ,因为100 / 50 = 2 ;
-
10 mg 青霉素是20 mg 青霉素的一半 ,因为10 / 20 = 0.5 。
2.4 数据等级总结
![[Pasted image 20220921111155.png]]
基本的工作流程:
(1) 数据有没有组织?数据是以表格形式存在、有不同的行列,还是以非结构化的文本格式存在?
(2) 每列的数据是定量的还是定性的?单元格中的数代表的是数值还是字符串?
(3) 每列处于哪个等级?是定类、定序、定距,还是定比?
(4) 我可以用什么图表?条形图、饼图、茎叶图、箱线图、直方图,还是其他?
基本逻辑
![[Pasted image 20220921111344.png]]
3 特征增强:清洗数据
3.1 识别数据中的缺失值
需要的包:
import pandas as pd # 存储表格数据
import numpy as np # 数学计算包
import matplotlib.pyplot as plt # 流行的数据可视化工具
import seaborn as sns # 另一个流行的数据可视化工具
%matplotlib inline
plt.style.use('fivethirtyeight') # 流行的数据可视化主题
基本的统计:
pima['onset_diabetes'].value_counts(normalize=True)
热力图:
# 数据集相关矩阵的热力图
sns.heatmap(pima.corr()) # pima是一个dataframe
输出相关性大小:
pima.corr()['onset_diabetes'] # 选取相关矩阵的某一列
Dataframe数据是否为空:
pima.isnull().sum()
数据的行数和列数
pima.shape # (行数, 列数)
(768, 9)
数据描述性统计
pima.describe() # 基本的描述性统计,通过观察min值,看数据是不是用0填充了缺失值
3.2 处理数据中的缺失值
最主要处理方法:
- 删除缺少值的行
- 填充缺失值
统计缺失值
pima['serum_insulin'].isnull().sum() # Dataframe的某一列
用None手动替换0
pima['serum_insulin'] = pima['serum_insulin'].map(lambda x:x if x != 0 else None)
也可以用inplace方法
pima['serum_insulin'].replace([0], [None], inplace=True)
3.2.1 删除有害的行
Pandas删除缺失值所在的行
# 删除存在缺失值的行
pima_dropped = pima.dropna()
3.2.2 填充缺失值
填充指的是利用现有知识/数据来确定缺失的数量值并填充的行为。我们有几个选择,最常见的是用此列其余部分的均值填充缺失值
查看缺失值
empty_plasma_index = pima[pima['plasma_glucose_concentration'].isnull()].index # 获取index
pima.loc[empty_plasma_index]['plasma_glucose_concentration'] # 通过index得到数据
75 None
182 None
342 None
349 None
502 None
Name: plasma_glucose_concentration, dtype: object
现在可以用内置的fillna 方法,将所有的None 填充为plasma_glucose_concentration列其余数据的均值:
pima['plasma_glucose_concentration'].fillna(pima['plasma_glucose_concentration'].mean(), inplace=True)
重新查看:
pima.loc[empty_plasma_index]['plasma_glucose_concentration']
75 121.686763
182 121.686763
342 121.686763
349 121.686763
502 121.686763
Name: plasma_glucose_concentration, dtype: float64
也可以用 scikit-learn 预处理类的Imputer 模块进行填充:
from sklearn.preprocessing import Imputer
我们有几个新的参数可以调节,但是主要关注strategy 。这个参数可以调节如何填充缺失值。对于定量值,可以使用内置的均值或中位数策略来填充值。
为了使用Imputer ,必须先实例化对象,方法如下:
imputer = Imputer(strategy='mean')
然后调用fit_transform 方法创建新对象:
pima_imputed = imputer.fit_transform(pima)
我们有个小问题要处理。Imputer 的输出值不是Pandas 的DataFrame ,而是NumPy 数组:
type(pima_imputed) # 是数组
numpy.ndarray
解决方法很简单,因为我们可以将任何数组直接变成DataFrame ,方法如下:
pima_imputed = pd.DataFrame(pima_imputed, columns=pima_column_names) # 将数组转换为Dataframe
3.2.3 在机器学习流水线中填充值
不可以用整个数据集的均值作为缺失值的填充,这会造成dataleakge
正确的做法是在训练集和测试集划分之后,使用训练集的均值填充测试集的缺失值
数据划分
from sklearn.model_selection import train_test_split
X = pima[['serum_insulin']].copy()
y = pima['onset_diabetes'].copy()
X.isnull().sum()
serum_insulin 374
dtype: int64
错误的方法:
# 不恰当的做法:在划分前填充值
entire_data_set_mean = X.mean() # 取整个数据集的均值
X = X.fillna(entire_data_set_mean) # 填充缺失值
print(entire_data_set_mean)
>>>serum_insulin 155.548223
>>>dtype: float64
# 使用一个随机状态,使每次检查的划分都一样
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=99)
正确的做法:
from sklearn.model_selection import train_test_split
X = pima[['serum_insulin']].copy()
y = pima['onset_diabetes'].copy()
# 使用相同的随机状态,保证划分不变
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=99)
training_mean = X_train.mean()
X_train = X_train.fillna(training_mean)
X_test = X_test.fillna(training_mean)
结合使用sklearn中的Pipeline和Imputer,可以使机器学习流程更容易。
from sklearn.pipeline import Pipeline
knn_params = {'classify__n_neighbors':[1, 2, 3, 4, 5, 6, 7]}
# 必须重新定义参数以符合流水线
knn = KNeighborsClassifier() # 实例化KNN 模型
mean_impute = Pipeline([('imputer', Imputer(strategy='mean')), ('classify', knn)])
X = pima.drop('onset_diabetes', axis=1)
y = pima['onset_diabetes']
grid = GridSearchCV(mean_impute, knn_params)
grid.fit(X, y)
print(grid.best_score_, grid.best_params_)
>>> 0.731770833333 {'classify__n_neighbors': 6}
要注意的是:
第一,我们的Pipeline 分两步:
- 1. 拥有strategy=‘mean’ 的Imputer ;
- 2. KNN 类型的分类器。
第二,要为网格搜索重新定义param 字典,因为必须明确n_neighbors 参数所属的步骤:
knn_params = {'classify__n_neighbors':[1, 2, 3, 4, 5, 6, 7]}
3.3 标准化和归一化
查看数据分布
impute = Imputer(strategy='mean')
# 填充所有的缺失值
pima_imputed_mean = pd.DataFrame(impute.fit_transform(pima),
columns=pima_column_names)
pima_imputed_mean.hist(figsize=(15, 15)) # 标准直方图查看所有列的分布情况
![[Pasted image 20220921141123.png]]
数据归一化方法:
-
z 分数标准化;
-
min-max 标准化;
-
行归一化。
3.3.1 Z分数标准化
公式为:
z
=
(
x
−
μ
)
/
σ
z=(x-\mu)/\sigma
z=(x−μ)/σ
-
z 是新的值(z 分数);
-
x 是单元格原来的值;
-
μ 是该列的均值;
-
σ 是列的标准差。
scikit-learn内置的归一化方法:
# 导入包
from sklearn.preprocessing import StandardScaler
# 处理前的分布情况
ax = pima['plasma_glucose_concentration'].hist()
ax.set_title('Distribution of plasma_glucose_concentration')
# 处理后的情况
scaler = StandardScaler()
glucose_z_score_standardized = scaler.fit_transform(pima[['plasma_glucose_concentration']])# 注意我们用双方括号,因为转换需要一个DataFrame
ax = pd.Series(glucose_z_score_standardized.reshape(-1,)).hist()
ax.set_title('Distribution of plasma_glucose_concentration after Z Score Scaling')
也可以对所有列应用标准化,StandardScaler 会对每列单独计算均值和标准差:
scale = StandardScaler() # 初始化一个z-scaler 对象
pima_imputed_mean_scaled = pd.DataFrame(scale.fit_transform(pima_imputed_mean),
columns=pima_column_names)
pima_imputed_mean_scaled.hist(figsize=(15, 15), sharex=True)
机器学习pipeline
knn_params = {'imputer__strategy':['mean', 'median'], 'classify__n_neighbors':[1, 2,
3, 4, 5, 6, 7]}
mean_impute_standardize = Pipeline([('imputer', Imputer()), ('standardize',
StandardScaler()), ('classify', knn)])
X = pima.drop('onset_diabetes', axis=1)
y = pima['onset_diabetes']
grid = GridSearchCV(mean_impute_standardize, knn_params)
grid.fit(X, y)
print(grid.best_score_, grid.best_params_)
0.7421875 {'classify__n_neighbors': 7, 'imputer__strategy': 'median'}
3.3.2 min-max标准化
m = ( x − x m i n ) / ( x m a x − x m i n ) m=(x-x_{min})/(x_{max}-x_{min}) m=(x−xmin)/(xmax−xmin)
- m 是新的值;
- x 是单元格原来的值;
- xmin 是该列的最小值;
- xmax 是该列的最大值。
使用scikit-learn内置模块:
# 导入sklearn 模块
from sklearn.preprocessing import MinMaxScaler
# 实例化
min_max = MinMaxScaler()
# 使用min-max 标准化
pima_min_maxed = pd.DataFrame(min_max.fit_transform(pima_imputed),
columns=pima_column_names)
# 得到描述性统计
pima_min_maxed.describe()
Pipeline:
knn_params = {'imputer__strategy': ['mean', 'median'], 'classify__n_neighbors':[1, 2,3, 4, 5, 6, 7]}
mean_impute_standardize = Pipeline([('imputer', Imputer()), ('standardize',
MinMaxScaler()), ('classify', knn)])
X = pima.drop('onset_diabetes', axis=1)
y = pima['onset_diabetes']
grid = GridSearchCV(mean_impute_standardize, knn_params)
grid.fit(X, y)
print(grid.best_score_, grid.best_params_)
0.74609375 {'classify__n_neighbors': 4, 'imputer__strategy': 'mean'}
3.3.3 行归一化
- 行归一化不是计算每列的统计值(均值、最小值、最大值等),而是会保证每行有单位范数 (unit norm ),意味着每行的向量长度相同
# 均值填充后平均范数
np.sqrt((pima_imputed**2).sum(axis=1)).mean()
>>> 223.36222025823744
使用sklearn中的包归一化:
from sklearn.preprocessing import Normalizer # 行归一化
normalize = Normalizer()
pima_normalized = pd.DataFrame(normalize.fit_transform(pima_imputed),
columns=pima_column_names)
np.sqrt((pima_normalized**2).sum(axis=1)).mean()
>>> 1.0
4 特征构建
4.1 检查数据集
import pandas as pd
X = pd.DataFrame({'city':['tokyo', None, 'london', 'seattle', 'san francisco', 'tokyo'],
'boolean':['yes', 'no', None, 'no', 'no', 'yes'],
'ordinal_column':['somewhat like', 'like', 'somewhat like', 'like',
'somewhat like', 'dislike'],
'quantitative_column':[1, 11, -.5, 10, None, 20]})
![[Pasted image 20220921143158.png]]
识别每列的类型和等级:
- boolean(布尔值):此列是二元分类数据(是/否),定类等级。
- city(城市):此列是分类数据,也是定类等级。
- ordinal_column(顺序列):顾名思义,此列是顺序数据,定序等级
- quantitative_column(定量列):此列是整数,定比等级。
4.2 填充分类特征
X.isnull().sum()
- scikit-learn 的Imputer 类有一个most_frequent 方法可以用在定性数据上,但是只能处理整数型的分类数据
- 先从定性列city 开始。对于数值数据,可以通过计算均值的方法填充缺失值;而对于分类数据,我们也有类似的处理方法:计算出最常见的类别用于填充
[ 注意,要对这个列使用value_counts 方法。这样会返回一个对象,由高到低包含列中的各个元素—— 第一个元素就是最常出现的]
# 寻找city 列中最常见的元素
X['city'].value_counts().index[0]
>>>
'tokyo'
找到最频繁的城市之后,开始填充:
# 用最常见的值填充city 列
X['city'].fillna(X['city'].value_counts().index[0])
4.2.1 自定义填充器
回顾机器学习Pipeline:
- 用流水线按顺序应用转换和最终的预测器
- 流水线的中间步骤只能是转换 ,这意味着它们必须实现fit 和transform 方法;
- 最终的预测器只需要实现fit 方法
4.2.2 自定义分类填充器
首先,用scikit-learn 的TransformerMixin 基类创建我们的自定义分类填充器。
from sklearn.base import TransformerMixin
class CustomCategoryImputer(TransformerMixin):
def __init__(self, cols=None):
self.cols = cols
def transform(self, df):
X = df.copy()
for col in self.cols:
X[col].fillna(X[col].value_counts().index[0], inplace=True)
return X
def fit(self, *_):
return self
在两列分类数据city 和boolean 上试验这个自定义方法:
# 在列上应用自定义分类填充器
cci = CustomCategoryImputer(cols=['city', 'boolean'])
调用:
cci.fit_transform(X)
4.2.3 自定义定量填充器
# 按名称对列进行转换的填充器
from sklearn.preprocessing import Imputer
class CustomQuantitativeImputer(TransformerMixin):
def __init__(self, cols=None, strategy='mean'):
self.cols = cols
self.strategy = strategy
def transform(self, df):
X = df.copy()
impute = Imputer(strategy=self.strategy)
for col in self.cols:
X[col] = impute.fit_transform(X[[col]])
return X
def fit(self, *_):
return self
试验这个自定义方法:
cqi = CustomQuantitativeImputer(cols=['quantitative_column'], strategy='mean')
调用:
cqi.fit_transform(X)
4.3 编码分类向量
4.3.1 定类等级的编码
两种编码方式:
- 用Pandas 自动找到分类变量并进行编码;
- 创建自定义虚拟变量编码器,在流水线中工作
用第一种选择将分类数据编码成虚拟变量。Pandas 有个很方便的get_dummies方法,可以找到所有的分类变量,并将其转换为虚拟变量
pd.get_dummies(X, columns = ['city', 'boolean'], # 要虚拟化的列
prefix_sep='__') # 前缀(列名)和单元格值之间的分隔符
另一种选择是创建一个自定义虚拟化器,从而在流水线中一口气转换整个数据集
# 自定义虚拟变量编码器
class CustomDummifier(TransformerMixin):
def __init__(self, cols=None):
self.cols = cols
def transform(self, X):
return pd.get_dummies(X, columns=self.cols)
def fit(self, *_):
return self
自定义虚拟化器模仿了scikit-learn 的OneHotEncoding ,但是可以在整个DataFrame上运行
实例:
cd = CustomDummifier(cols=['boolean', 'city'])
cd.fit_transform(X)
4.3.2 定序等级的编码
-
在定序等级,由于数据的顺序有含义,使用虚拟变量是没有意义的。为了保持顺序,我们使用标签编码器。
-
标签编码器是指,顺序数据的每个标签都会有一个相关数值。在我们的例子中,这意味着顺序列的值(dislike 、somewhat like 和like )会用0 、1 、2 来表示
最简单的编码方式:
# 创建一个列表,顺序数据对应于列表索引
ordering = ['dislike', 'somewhat like', 'like'] # 0 是dislike,1 是somewhat like,2是like
将列表的索引ordering 分配到各个元素上
# 将ordering 映射到顺序列
print(X['ordinal_column'].map(lambda x: ordering.index(x)))
放入Pipeline:
class CustomEncoder(TransformerMixin):
def __init__(self, col, ordering=None):
self.ordering = ordering
self.col = col
def transform(self, df):
X = df.copy()
X[self.col] = X[self.col].map(lambda x: self.ordering.index(x))
return X
def fit(self, *_):
return self
关键参数是ordering ,它会指定将标签编码成什么数值
实例:
ce = CustomEncoder(col='ordinal_column', ordering = ['dislike', 'somewhat like', 'like'])
ce.fit_transform(X)
得到:
![[Pasted image 20220921151456.png]]
4.3.3 将连续特征分箱
- 如果数值数据是连续的,那么将其转换为分类变量可能是有意义的。例如你的手上有年龄,但是年龄段可能会更有用
- Pandas 有一个有用的函数叫作cut,可以将数据分箱( binning),亦称为分桶( bucketing)。意思就是,它会创建数据的范围。
实例:
# 默认的类别名就是分箱
pd.cut(X['quantitative_column'], bins=3)
输出
0 (-0.52, 6.333]
1 (6.333, 13.167]
2 (-0.52, 6.333]
3 (6.333, 13.167]
4 NaN
5 (13.167, 20.0]
Name: quantitative_column, dtype: category
Categories (3, interval[float64]): [(-0.52, 6.333] < (6.333, 13.167] < (13.167, 20.0]]
当指定的bins 为整数的时候(bins = 3 ),会定义X 范围内的等宽分箱数。然而在本例中,X 的范围向两边分别扩展了0.1% ,以包括最小值和最大值。
也可以将标签设置为False,这将返回分箱的整数指示器:
# 不使用标签
pd.cut(X['quantitative_column'], bins=3, labels=False)
输出
quantitative_column 列的整数指示器如下:
0 0.0
1 1.0
2 0.0
3 1.0
4 NaN
5 2.0
Name: quantitative_column, dtype: float64
定义自己的CustomCutter
class CustomCutter(TransformerMixin):
def __init__(self, col, bins, labels=False):
self.labels = labels
self.bins = bins
self.col = col
def transform(self, df):
X = df.copy()
X[self.col] = pd.cut(X[self.col], bins=self.bins,labels=self.labels)
return X
def fit(self, *_):
return self
实例:
cc = CustomCutter(col='quantitative_column', bins=3)
cc.fit_transform(X)
4.3.4 创建Pipeline
from sklearn.pipeline import Pipeline
把每列的自定义转换器放在一起。我们流水线的顺序是:
(1) 用imputer 填充缺失值;
(2) 用虚拟变量填充分类列;
(3) 对ordinal_column 进行编码;
(4) 将quantitative_column 分箱。
这样设置流水线:
pipe = Pipeline([("imputer", imputer), ('dummify', cd), ('encode', ce), ('cut', cc)])
# 先是imputer
# 然后是虚拟变量
# 接着编码顺序列
# 最后分箱定量列
4.4 扩展数值特征
4.4.1 根据加速度识别动作的数据集
from sklearn.neighbors import KNeighborsClassifier
from sklearn.model_selection import GridSearchCV
X = df[['x', 'y', 'z']]
# 删除响应变量,建立特征矩阵
y = df['activity']
# 需要试验的KNN 模型参数
knn_params = {'n_neighbors':[3, 4, 5, 6]}
knn = KNeighborsClassifier()
grid = GridSearchCV(knn, knn_params)
grid.fit(X, y)
print(grid.best_score_, grid.best_params_)
0.720752487676999 {'n_neighbors': 5}
4.4.2 多项式特征
- 在处理数值数据、创建更多特征时,一个关键方法是使用scikit-learn 的Polynomial-
Features 类。这个构造函数会创建新的列,它们是原有列的乘积,用于捕获特征交互 - 更具体地说,这个类会生成一个新的特征矩阵,里面是原始数据各个特征的多项式组合,阶数小于或等于指定的阶数。意思是,如果输入是二维的,例如 [ a , b ] [a, b] [a,b] ,那么二阶的多项式特征就是 [ 1 , a , b , a 2 , a b , b 2 ] [1, a, b, a^2, ab, b^2] [1,a,b,a2,ab,b2]
- 参数
在实例化多项式特征时,需要了解3 个参数:
- degree
- degree 是多项式特征的阶数,默认值是2
- interaction_only
- interaction_only 是布尔值:如果为真,表示只生成互相影响/交互的特征,也就是不同阶数特征的乘积。interaction_only 默认为false
- include_bias
- include_bias 也是布尔值:如果为真(默认),会生成一列阶数为0 的偏差列,也就是说列中全是数字1。
from sklearn.preprocessing import PolynomialFeatures
poly = PolynomialFeatures(degree=2, include_bias=False, interaction_only=False)
X_poly = poly.fit_transform(X)
得到:
![[Pasted image 20220921154705.png]]
- 探索性数据分析
- 多项式特征的目的是更好地理解原始数据的特征交互情况,所以最好的可视化办法是关联热图
导入可视化工具
%matplotlib inline
import seaborn as sns
创建关联热图
sns.heatmap(pd.DataFrame(X_poly, columns=poly.get_feature_names()).corr())
同样可以使用Pipeline:
# 1. 先设置流水线参数
pipe_params = {'poly_features__degree':[1, 2, 3], 'poly_features__interaction_only':[True, False], 'classify__n_neighbors':[3, 4, 5, 6]}
# 2. 然后实例化流水线
from sklearn.pipeline import Pipeline
pipe = Pipeline([('poly_features', poly), ('classify', knn)])
# 3. 最后设置网格搜索,打印最佳准确率和学习到的参数:
grid = GridSearchCV(pipe, pipe_params)
grid.fit(X, y)
print(grid.best_score_, grid.best_params_)
>>>0.7211894080651812 {'classify__n_neighbors': 5, 'poly_features__degree': 2, 'poly_features__interaction_only': True}
4.5 针对文本的特征构建
4.5.1 词袋法
- 将文本数据称为语料库(corpus ),尤其是指文本内容或文档的集合
- 词袋(bag of words), 其背后的基本思想是:通过单词的出现来描述文档,完全忽略单词在文档中的位置。在它最简单的形式中,用一个袋子表示文本,不考虑语法和词序,并将这个袋子视作一个集合,其中重复度高的单词更重要
- 词袋的三个步骤:
- 分词tokenizing
- 分词过程是用空白和标点将单词分开,将其变为词项。每个可能出现的词项都有一个整数ID
- 计数counting
- 简单地计算文档中词项的出现次数
- 归一化normalizing
- 将词项在大多数文档中的重要性按逆序排列
- 分词tokenizing
4.5.2 CountVectorizer
- CountVectorizer 是将文本数据转换为其向量表示的最常用办法
- CountVectorizer 将文本列转换为矩阵,其中的列是词项,单元值是每个文档中每个词项的出现次数
- 这个矩阵叫文档-词矩阵 (document-term matrix ),因为每行代表一个文档 (在本例中是一条推文),每列代表一个词 (一个单词)。
实例用的数据:tweets
![[Pasted image 20220921160750.png]]
我们只关注Sentiment 和SentimentText 列,所以删除ItemID 列:
del tweets['ItemID']
得到数据:
![[Pasted image 20220921160908.png]]
导入包:
from sklearn.feature_extraction.text import CountVectorizer
设置X 和y :
X = tweets['SentimentText']
y = tweets['Sentiment']
使用CountVectorizer
vect = CountVectorizer()
_ = vect.fit_transform(X)
print(_.shape)
>>>(99989, 105849)
用CountVectorizer 转换后,数据有99 989 行和105 849 列。
CountVectorizer 的参数
-
stop_words
-
min_df
-
max_df
-
ngram_range
-
analyzer
- stop_words 参数很常用。如果向其传入字符串english,那么CountVectorizer 会使用内置的英语停用词列表。你也可以自定义停用词列表。这些词会从词项中删除,不会表示为特征。
vect = CountVectorizer(stop_words='english') # 删除英语停用词(if、a、the, 等等)
_ = vect.fit_transform(X)
print(_.shape)
>>> (99989, 105545)
停用词的意义在于消除特征的噪声,去掉在模型中意义不大的常用词
- min_d 通过忽略在文档中出现频率低于阈值的词,减少特征的数量
vect = CountVectorizer(min_df=.05) # 只保留至少在5%文档中出现的单词
# 减少特征数
_ = vect.fit_transform(X)
print(_.shape)
>>> (99989, 31)
- max_df 类似于试图理解文档中有哪些停用词
vect = CountVectorizer(max_df=.8) # 只保留至多在80%文档中出现的单词
# “推断”停用词
_ = vect.fit_transform(X)
print(_.shape)
>>> (99989, 105849)
- ngram_range, 这个参数接收一个元组,表示n 值的范围(代表要提取的不同n-gram 的数量)上下界。 n-gram代表短语:若 n = 1,则其是一个词项;若 n = 2,则其代表相邻的两个词项。 这个方法会显著地增加特征
vect = CountVectorizer(ngram_range=(1, 5)) # 包括最多5 个单词的短语
_ = vect.fit_transform(X)
print(_.shape) # 特征数爆炸
>>> (99989, 3219557)
因为短语可能有其他含义,所以调整这个参数会对建模有帮助。
- CountVectorizer 还可以设置分析器作为参数, 以判断特征是单词还是短语。默认是单词:
vect = CountVectorizer(analyzer='word') # 默认分析器,划分为单词
_ = vect.fit_transform(X)
print(_.shape)
>>> (99989, 105849)
默认就是划分为单词,所以特征列结果变化不大。我们还可以创建自定义分析器
4.5.3 TF-IDF向量化器
- TF-IDF 向量化器由两部分组成:表示词频的 TF 部分,以及表示逆文档频率的 IDF 部分。
- TF-IDF 是一个用于信息检索和聚类的词加权方法
- TF( term frequency,词频):衡量词在文档中出现的频率。由于文档的长度不同,词在长文中的出现次数有可能比在短文中出现的次数多得多。因此,一般会对词频进行归一化,用其除以文档长度或文档的总词数
- IDF( inverse document frequency,逆文档频率):衡量词的重要性。在计算词频时,我们认为所有的词都同等重要。但是某些词(如is、of 和that)有可能出现很多次,但这些词并不重要。因此,我们需要减少常见词的权重,加大稀有词的权重
*TfidfVectorizer 和CountVectorizer 相同,都从词项构造了特征,但是TfidfVectorizer 进一步将词项计数按照在语料库中出现的频率进行了归一化。
两者对比:
导入包:
from sklearn.feature_extraction.text import TfidfVectorizer
CountVectorizer 生成文档-词矩阵
vect = CountVectorizer()
_ = vect.fit_transform(X)
print(_.shape, _[0,:].mean())
>>> (99989, 105849) 6.613194267305311e-05
TfidfVectorizer生成文档-词矩阵
vect = TfidfVectorizer()
_ = vect.fit_transform(X)
print(_.shape, _[0,:].mean()) # 行列数相同,内容不同
>>> (99989, 105849) 2.1863060975751192e-05
两个向量化器输出的行列数相同, 但是填充单元值的方法不同
4.5.4 在机器学习Pipeline中使用文本
以使用朴素贝叶斯分类器为例,导入模型:
from sklearn.naive_bayes import MultinomialNB # 特征数多时更快
空准确率:
y.value_counts(normalize=True)
1 0.564632
0 0.435368
Name: Sentiment, dtype: float64
分两步创建Pipeline:
- 用CountVectorizer 将推文变成特征;
- 用朴素贝叶斯模型MultiNomialNB 进行正负面情绪的分类
首先设置流水线的参数,然后实例化网格搜索:
# 设置流水线参数
pipe_params = {'vect__ngram_range':[(1, 1), (1, 2)], 'vect__max_features':[1000,
10000], 'vect__stop_words':[None, 'english']}
实例化Pipeline
pipe = Pipeline([('vect', CountVectorizer()), ('classify', MultinomialNB())])
实例化网格搜索
grid = GridSearchCV(pipe, pipe_params)
拟合网格搜索对象
grid.fit(X, y)
去除结果
print(grid.best_score_, grid.best_params_)
>>> 0.7557531328446129 {'vect__max_features': 10000, 'vect__ngram_range': (1, 2), 'vect__stop_words': None}
- 进一步地,加入TfidfVectorizer。scikit-learn 有一个FeatureUnion 模块,可以水平(并排)排列特征。这样,在一个流水线中可以使用多种类型的文本特征构建器。
- 例如,可以构建一个featurizer 对象,在推文上使用TfidfVectorizer 和CountVectorizer ,并且并排排列推文(行数相同,增加列数):
from sklearn.pipeline import FeatureUnion
# 单独的特征构建器对象
featurizer = FeatureUnion([('tfidf_vect', TfidfVectorizer()), ('count_vect',
CountVectorizer())])
可以看见数据的变化情况:
_ = featurizer.fit_transform(X)
print(_.shape) # 行数相同,但列数为2 倍
>>> (99989, 211698)
查看效果:
featurizer.set_params(tfidf_vect__max_features=100, count_vect__ngram_range=(1, 2), count_vect__max_features=300)
# TfidfVectorizer 只保留100 个单词,而CountVectorizer 保留300 个1~2 个单词的短语
_ = featurizer.fit_transform(X)
print(_.shape) # 行数相同,但列数为2 倍
>>> (99989, 400)
建立Pipeline:
pipe_params = {'featurizer__count_vect__ngram_range':[(1, 1), (1, 2)],
'featurizer__count_vect__max_features':[1000, 10000],
'featurizer__count_vect__stop_words':[None, 'english'],
'featurizer__tfidf_vect__ngram_range':[(1, 1), (1, 2)],
'featurizer__tfidf_vect__max_features':[1000, 10000],
'featurizer__tfidf_vect__stop_words':[None, 'english']}
pipe = Pipeline([('featurizer', featurizer), ('classify', MultinomialNB())])
grid = GridSearchCV(pipe, pipe_params)
grid.fit(X, y)
print(grid.best_score_, grid.best_params_)
0.758433427677 {'featurizer__tfidf_vect__max_features': 10000,
'featurizer__tfidf_vect__stop_words': 'english',
'featurizer__count_vect__stop_words': None,
'featurizer__count_vect__ngram_range': (1, 2),
'featurizer__count_vect__max_features': 10000,
'featurizer__tfidf_vect__ngram_range': (1, 1)}
5 特征选择
- 特征选择是从原始数据中选择对于预测流水线而言最好的特征的过程
- 更正式地说,给定n个特征,我们搜索其中包括k (k < n )个特征的子集来改善机器学习流水线的性能
5.1 模型评价指标
分类任务可以使用如下指标:
- 真阳性率和假阳性率;
- 灵敏度(真阳性率)和特异性;
- 假阴性率和假阳性率
回归任务则可以使用:
- 平均绝对误差;
- R2
元指标:
- 模型拟合/ 训练所需的时间;
- 拟合后的模型预测新实例的时间;
- 需要持久化(永久保存)的数据大小
自定义模型评价函数:
# 导入网格搜索模块
from sklearn.model_selection import GridSearchCV
def get_best_model_and_accuracy(model, params, X, y):
grid = GridSearchCV(model, # 要搜索的模型
params, # 要尝试的参数
error_score=0.) # 如果报错,结果是0
grid.fit(X, y) # 拟合模型和参数
# 经典的性能指标
print("Best Accuracy: {}".format(grid.best_score_))
# 得到最佳准确率的最佳参数
print("Best Parameters: {}".format(grid.best_params_))
# 拟合的平均时间(秒)
print("Average Time to Fit (s):{}".format(round(grid.cv_results_['mean_fit_time'].mean(), 3)))
# 预测的平均时间(秒)# 从该指标可以看出模型在真实世界的性能
print("Average Time to Score (s):{}".format(round(grid.cv_results_['mean_score_time'].mean(), 3)))
5.2 创建基准的Pipeline
介绍数据集:
信用卡逾期数据集credit_card_default。我们使用23 个特征和一个响应变量。这个变量是一个布尔值,可以是True (真)或False(假)。
寻找最符合我们需求的机器学习模型
先导入4种模型:
- 逻辑回归
- K最近邻(KNN)
- 决策树
- 随机森林
# 导入4 种模型
from sklearn.linear_model import LogisticRegression
from sklearn.neighbors import KNeighborsClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
设置参数:
# 为网格搜索设置变量
# 先设置机器学习模型的参数
# 逻辑回归
lr_params = {'C':[1e-1, 1e0, 1e1, 1e2], 'penalty':['l1', 'l2']}
# KNN
knn_params = {'n_neighbors': [1, 3, 5, 7]}
# 决策树
tree_params = {'max_depth':[None, 1, 3, 5, 7]}
# 随机森林
forest_params = {'n_estimators': [10, 50, 100], 'max_depth': [None, 1, 3, 5, 7]}
实例化机器学习模型:
lr = LogisticRegression()
knn = KNeighborsClassifier()
d_tree = DecisionTreeClassifier()
forest = RandomForestClassifier()
逻辑回归:
get_best_model_and_accuracy(lr, lr_params, X, y)
>>> Best Accuracy: 0.809566666667
>>> Best Parameters: {'penalty': 'l1', 'C': 0.1}
>>> Average Time to Fit (s): 0.602
>>> Average Time to Score (s): 0.002
KNN:
get_best_model_and_accuracy(knn, knn_params, X, y)
>>> Best Accuracy: 0.760233333333
>>> Best Parameters: {'n_neighbors': 7}
>>> Average Time to Fit (s): 0.035
>>> Average Time to Score (s): 0.88
KNN 是基于距离的模型,使用空间的紧密度衡量,假定所有的特征尺度相同,但是数据并不是这样,因此需要缩放数据。
# 导入所需的包
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
# 为流水线设置KNN 参数
knn_pipe_params = {'classifier__{}'.format(k): v for k, v in knn_params.items()}
# KNN 需要标准化的参数
knn_pipe = Pipeline([('scale', StandardScaler()), ('classifier', knn)])
# 拟合快,预测慢
get_best_model_and_accuracy(knn_pipe, knn_pipe_params, X, y)
print(knn_pipe_params) # {'classifier__n_neighbors': [1, 3, 5, 7]}
>>> Best Accuracy: 0.8008
>>> Best Parameters: {'classifier__n_neighbors': 7}
>>> Average Time to Fit (s): 0.035
>>> Average Time to Score (s): 6.723
决策树:
get_best_model_and_accuracy(d_tree, tree_params, X, y)
>>> Best Accuracy: 0.820266666667
>>> Best Parameters: {'max_depth': 3}
>>> Average Time to Fit (s): 0.158
>>> Average Time to Score (s): 0.002
随机森林:
get_best_model_and_accuracy(forest, forest_params, X, y)
>>> Best Accuracy: 0.819566666667
>>> Best Parameters: {'n_estimators': 50, 'max_depth': 7}
>>> Average Time to Fit (s): 1.107
>>> Average Time to Score (s): 0.044
![[Pasted image 20220921172102.png]]
决策树不要求数据归一化。
5.3 特征选择的类型
从原始特征中选择一个子集,减少数据大小,只留下预测能力最高的特征
- 基于统计的特征选择
- 依赖于机器学习模型之外的统计测试,以便在流水线的训练阶段选择特征。
- 基于模型的特征选择
- 依赖于一个预处理步骤,需要训练一个辅助的机器学习模型,并利用其预测能力来选择特征。
5.3.1 基于统计的特征选择
本章会使用两个新概念帮我们选择特征:
- 皮尔逊相关系数(Pearson correlations );
- 假设检验。
这两个方法都是单变量方法。意思是,如果为了提高机器学习流水线性能而每次选择单一 特征以创建更好的数据集,这种方法最简便。
- 使用皮尔逊相关系数
计算相关系数:
credit_card_default.corr()
就可以得到各个列之间的相关性,形如:
![[Pasted image 20220922093241.png]]
- 皮尔逊相关系数(是Pandas 默认的)会测量列之间的线性 关系。该系数在-1 ~1 变化,0 代表没有线性关系。相关性接近-1 或1 代表线性关系很强。
- 值得注意的是,皮尔逊相关系数要求每列是正态分布的(根据中心极限定理,当数据量足够大时,可以认为数据是近似正态分布的)
- 绘制相关系数的热图
# 用Seaborn 生成热图
import seaborn as sns
import matplotlib.style as style
# 选用一个干净的主题
style.use('fivethirtyeight')
sns.heatmap(credit_card_default.corr())
使用相关性筛选特征的逻辑是:和最后要预测的变量越相关,这个特征就越有用。
只关注响应变量:
credit_card_default.corr()['default payment next month']
输出结果:
LIMIT_BAL -0.153520
SEX -0.039961
EDUCATION 0.028006
MARRIAGE -0.024339
AGE 0.013890
PAY_0 0.324794
PAY_2 0.263551
PAY_3 0.235253
PAY_4 0.216614
PAY_5 0.204149
PAY_6 0.186866
BILL_AMT1 -0.019644
BILL_AMT2 -0.014193
BILL_AMT3 -0.014076
BILL_AMT4 -0.010156
BILL_AMT5 -0.006760
BILL_AMT6 -0.005372
PAY_AMT1 -0.072929
PAY_AMT2 -0.058579
PAY_AMT3 -0.056250
PAY_AMT4 -0.056827
PAY_AMT5 -0.055124
PAY_AMT6 -0.053183
default payment next month 1.000000
Name: default payment next month, dtype: float64
定义一个Pandas mask 作为过滤器:
# 只留下相关系数超过正负0.2 的特征
credit_card_default.corr()['default payment next month'].abs() > .2
得到:
LIMIT_BAL False
SEX False
EDUCATION False
MARRIAGE False
AGE False
PAY_0 True
PAY_2 True
PAY_3 True
PAY_4 True
PAY_5 True
PAY_6 False
BILL_AMT1 False
BILL_AMT2 False
BILL_AMT3 False
BILL_AMT4 False
BILL_AMT5 False
BILL_AMT6 False
PAY_AMT1 False
PAY_AMT2 False
保存特征:
# 存储特征
highly_correlated_features = credit_card_default.columns[credit_card_default.corr()['default payment next month'].abs() > .2]
>>> highly_correlated_features
>>> Index(['PAY_0', 'PAY_2', 'PAY_3', 'PAY_4', 'PAY_5', 'default payment next month'], dtype='object')
自定义转换器:实现一个拟合逻辑和一个转换逻辑
- 拟合逻辑: 从特征矩阵中选择相关性高于阈值的列
- 转换逻辑: 对数据集取子集,只包含重要的列
from sklearn.base import TransformerMixin, BaseEstimator
class CustomCorrelationChooser(TransformerMixin, BaseEstimator):
def __init__(self, response, cols_to_keep=[], threshold=None):
# 保存响应变量
self.response = response
# 保存阈值
self.threshold = threshold
# 初始化一个变量,存放要保留的特征名
self.cols_to_keep = cols_to_keep
def transform(self, X):
# 转换会选择合适的列
return X[self.cols_to_keep]
def fit(self, X, *_):
# 创建新的DataFrame,存放特征和响应
df = pd.concat([X, self.response], axis=1)
# 保存高于阈值的列的名称
self.cols_to_keep = df.columns[df.corr()[df.columns[-1]].abs() > self.threshold]
# 只保留X 的列,删掉响应变量
self.cols_to_keep = [c for c in self.cols_to_keep if c in X.columns]
return self
运行结果:
# 实例化特征选择器
ccc = CustomCorrelationChooser(threshold=.2, response=y)
ccc.fit(X)
>>> ccc.cols_to_keep
>>> ['PAY_0', 'PAY_2', 'PAY_3', 'PAY_4', 'PAY_5']
Pipeline:
from copy import deepcopy
# 使用响应变量初始化特征选择器
ccc = CustomCorrelationChooser(response=y)
# 创建流水线,包括选择器
ccc_pipe = Pipeline([('correlation_select', ccc), ('classifier', d_tree)])
tree_pipe_params = {'classifier__max_depth': [None, 1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21]}
# 复制决策树的参数
ccc_pipe_params = deepcopy(tree_pipe_params)
# 更新决策树的参数选择
ccc_pipe_params.update({'correlation_select__threshold':[0, .1, .2, .3]})
print(ccc_pipe_params) #{'correlation_select__threshold': [0, 0.1, 0.2, 0.3],
'classifier__max_depth': [None, 1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21]}
# 比原来好一点,而且很快
get_best_model_and_accuracy(ccc_pipe, ccc_pipe_params, X, y)
>>> Best Accuracy: 0.8206
>>> Best Parameters: {'correlation_select__threshold': 0.1, 'classifier__max_depth': 5}
>>> Average Time to Fit (s): 0.105
>>> Average Time to Score (s): 0.003
2. 使用假设检验
- 假设检验用于在给定数据样本时确定可否在整个数据集上应用某种条件
- 在特征选择中,假设测试的原则是:“特征与响应变量没有关系”(零假设)为真还是假
使用假设检验之前,需要定义新模块SelectKBest 和f_classif
# SelectKBest 在给定目标函数后选择k 个最高分
from sklearn.feature_selection import SelectKBest
# ANOVA 测试
from sklearn.feature_selection import f_classif
# f_classif 可以使用负数,但不是所有类都支持
# chi2(卡方)也很常用,但只支持正数
# 回归分析有自己的假设检验
- *f_classif 函数在每个特征上单独(单变量测试由此得名)执行一次ANOVA测试(一种假设检验类型),并分配一个p 值。
- *SelectKBest 基本上就是包装了一定数量的特征,而这些特征是根据某个标准保留的前几名。SelectKBest 会将特征按p值排列(越小越好), 只保留我们指定的k个最佳特征
筛选规则: p 值是介于0 和1 的小数,代表在假设检验下,给定数据偶然出现的概率。p 值越低,拒绝零假设的概率越大。在特征选择中,p 值越低,这个特征与响应变量有关联的概率就越大,我们应该保留这个特征。
实验:
- 首先实例化一个SelectKBest 模块。我们手动设定k 是5 ,代表只希望保留5 个最佳的特征:
# 只保留最佳的5 个特征
k_best = SelectKBest(f_classif, k=5)
拟合并转化X矩阵
# 选择最佳特征后的矩阵
k_best.fit_transform(X, y)
得到输出结果:
# 30 000 列 °¡ 5 行
>>> array([[ 2, 2, -1, -1, -2],
[-1, 2, 0, 0, 0],
[ 0, 0, 0, 0, 0],
...,
[ 4, 3, 2, -1, 0],
[ 1, -1, 0, 0, 0],
[ 0, 0, 0, 0, 0]])
查看p值
# 取列的p 值
k_best.pvalues_
# 特征和p 值组成DataFrame
# 按p 值排列
p_values = pd.DataFrame({'column': X.columns, 'p_value': k_best.pvalues_}).sort_values('p_value')
# 前5 个特征
p_values.head()
得到结果:
![[Pasted image 20220922134419.png]]
要注意的是:p值不是越小越好。且不能相互比较。
p值的一个常见阈值是0.05 ,意思是可以认为p 值小于0.05 的特征是显著的。对于我们的测试,这些列是极其重要的。我们可以用Pandas 的过滤方法,查看所有p值小于0.05 的特征:
# 低p 值的特征
p_values[p_values['p_value'] < .05]
5.3.2 基于模型的特征选择
统计单变量方法在大量特征(例如从文本向量化中获取的特征)上表现不佳。
- 使用机器学习选择特征
特征选择指标——针对基于树的模型
默认情况下,scikit-learn 每步都会优化基尼指数 (gini metric )
拟合一个决策树,并输出特征重要性
# 创建新的决策树分类器
tree = DecisionTreeClassifier()
tree.fit(X, y)
拟合后,可以用feature_importances_ 属性展示特征对于拟合树的重要性:
# 注意:还有其他特征
importances = pd.DataFrame({'importance': tree.feature_importances_,
'feature':X.columns}).sort_values('importance', ascending=False)
importances.head()
得到结果:
![[Pasted image 20220922142642.png]]
- *scikit-learn 内置的包装器SelectFromModel, 和SelectKBest一样选取最重要的前k个特征,但是它会使用机器学习模型的内部指标来评估特征的重要性
- SelectFromModel 和SelectKBest 相比最大的不同之处在于不使用k(需要保留的特征数):SelectFromModel 使用阈值,代表重要性的最低限度
导入SelectFromModel
# 和SelectKBest 相似,但不是统计测试
from sklearn.feature_selection import SelectFromModel
实例化SelectFromModel
# 实例化一个类,按照决策树分类器的内部指标排序重要性,选择特征
select_from_model = SelectFromModel(DecisionTreeClassifier(), threshold=.05)
然后在SelectFromModel 上拟合数据,调用transform方法,观察数据选择后的数据子集:
selected_X = select_from_model.fit_transform(X, y)
selected_X.shape
>>> (30000, 9)
Pipeline
# 为后面加速
tree_pipe_params = {'classifier__max_depth': [1, 3, 5, 7]}
from sklearn.pipeline import Pipeline
# 创建基于DecisionTreeClassifier 的SelectFromModel
select = SelectFromModel(DecisionTreeClassifier())
select_from_pipe = Pipeline([('select', select), ('classifier', d_tree)])
select_from_pipe_params = deepcopy(tree_pipe_params)
select_from_pipe_params.update({ 'select__threshold': [.01, .05, .1, .2, .25, .3, .4, .5, .6, "mean", "median", "2.*mean"], 'select__estimator__max_depth': [None, 1, 3, 5, 7] })
print(select_from_pipe_params) # {'select__threshold': [0.01, 0.05, 0.1, 'mean', 'median', '2.*mean'], 'select__estimator__max_depth': [None, 1, 3, 5, 7],
>>> 'classifier__max_depth': [1, 3, 5, 7]}
get_best_model_and_accuracy(select_from_pipe, select_from_pipe_params, X, y)
>>> Best Accuracy: 0.820266666667
>>> Best Parameters: {'select__threshold': 0.01, 'select__estimator__max_depth': None, 'classifier__max_depth': 3}
>>> Average Time to Fit (s): 0.192
>>> Average Time to Score (s): 0.002
*mean 的阈值只选择比均值更重要的特征,median 的阈值只选择比中位数更重要的特征。我们还可以用这些保留字的倍数,2.mean 代表比均值重要两倍的特征
查看基于决策树的选择器选出了哪些特征,可以使用SelectFromModel 的get_support()方法。
- 这个方法会返回一个数组,其中的每个布尔值代表一个特征,从而告诉我们保留了哪些特征
# 设置流水线最佳参数
select_from_pipe.set_params(**{'select__threshold': 0.01, 'select__estimator__max_depth': None, 'classifier__max_depth': 3})
# 拟合数据
select_from_pipe.steps[0][1].fit(X, y)
# 列出选择的列
X.columns[select_from_pipe.steps[0][1].get_support()]
>>> [u'LIMIT_BAL', u'SEX', u'EDUCATION', u'MARRIAGE', u'AGE', u'PAY_0', u'PAY_2', u'PAY_3', u'PAY_6', u'BILL_AMT1', u'BILL_AMT2', u'BILL_AMT3', u'BILL_AMT4', u'BILL_AMT5', u'BILL_AMT6', u'PAY_AMT1', u'PAY_AMT2', u'PAY_AMT3', u'PAY_AMT4', u'PAY_AMT5', u'PAY_AMT6']
可以继续尝试几种基于树的模型,例如RandomForest (随机森林)和ExtraTrees-Classifier (极限随机树)等,但是效果应该比不基于树的方法差。
- 线性模型和正则化
正则化:也算是一种特征选择的方法
- L1正则化: 也称为lasso 正则化 ,会使用L1 范数(参见上面的公式)将向量条目绝对值的和加以限制,使系数可以完全消失。如果系数降为0 ,那么这个特征在预测时就没有任何意义,而且肯定不会被SelectFromModel 选择
- L2正则化: 也称为岭正则化 ,施加惩罚L2 范数(向量条目的平方和),让系数不会变成0 ,但是会非常小
线性模型参数: 用逻辑回归模型作为选择器,在L1 和L2 范数上进行网格搜索
# 用正则化后的逻辑回归进行选择
logistic_selector = SelectFromModel(LogisticRegression())
# 新流水线,用LogistisRegression 的参数进行排列
regularization_pipe = Pipeline([('select', logistic_selector),('classifier', tree)])
regularization_pipe_params = deepcopy(tree_pipe_params)
# L1 和L2 正则化
regularization_pipe_params.update({'select__threshold': [.01, .05, .1, "mean", "median", "2.*mean"], 'select__estimator__penalty': ['l1', 'l2']})
print(regularization_pipe_params) # {'select__threshold': [0.01, 0.05, 0.1, 'mean', 'median', '2.*mean'], 'classifier__max_depth': [1, 3, 5, 7], 'select__estimator__penalty': ['l1', 'l2']}
get_best_model_and_accuracy(regularization_pipe, regularization_pipe_params, X, y)
>>> Best Accuracy: 0.821166666667
>>> Best Parameters: {'select__threshold': 0.01, 'classifier__max_depth': 5, 'select__estimator__penalty': 'l1'}
>>> Average Time to Fit (s): 0.51
>>> Average Time to Score (s): 0.001
使用SelectFromModel 的get_support()方法,列出选择的特征
# 设置流水线最佳参数
regularization_pipe.set_params(**{'select__threshold': 0.01, 'classifier__max_depth': 5, 'select__estimator__penalty': 'l1'})
拟合数据:
# 拟合数据
regularization_pipe.steps[0][1].fit(X, y)
列出选择的列
X.columns[regularization_pipe.steps[0][1].get_support()]
Index(['SEX', 'EDUCATION', 'MARRIAGE', 'PAY_0', 'PAY_2', 'PAY_3', 'PAY_4', 'PAY_5'], dtype='object')
Pipeline实现SVC
# SVC 是线性模型,用线性支持在欧几里得空间内分割数据
# 只能分割二分数据
from sklearn.svm import LinearSVC
# 用SVC 取参数
svc_selector = SelectFromModel(LinearSVC())
svc_pipe = Pipeline([('select', svc_selector), ('classifier', tree)])
svc_pipe_params = deepcopy(tree_pipe_params)
svc_pipe_params.update({ 'select__threshold': [.01, .05, .1, "mean", "median", "2.*mean"], 'select__estimator__penalty': ['l1', 'l2'], 'select__estimator__loss': ['squared_hinge', 'hinge'], 'select__estimator__dual':[True, False]})
print(svc_pipe_params) # 'select__estimator__loss': ['squared_hinge', 'hinge'],'select__threshold': [0.01, 0.05, 0.1, 'mean', 'median', '2.*mean'],'select__estimator__penalty': ['l1', 'l2'], 'classifier__max_depth': [1, 3, 5, 7], 'select__estimator__dual': [True, False]}
get_best_model_and_accuracy(svc_pipe, svc_pipe_params, X, y)
得到结果:
Best Accuracy: 0.821233333333
Best Parameters: {'select__estimator__loss': 'squared_hinge', 'select__threshold': 0.01, 'select__estimator__penalty': 'l1', 'classifier__max_depth': 5, 'select__estimator__dual': False}
Average Time to Fit (s): 0.989
Average Time to Score (s): 0.001
5.4 选用正确的特征选择方法
- 如果特征是分类的,那么从SelectKBest 开始,用卡方或基于树的选择器。
- 如果特征基本是定量的(例如本例),用线性模型和基于相关性的选择器一般效果更好。
- 如果是二元分类问题,考虑使用SelectFromModel 和SVC ,因为SVC 会查找优化二元分类任务的系数。
- 在手动选择前,探索性数据分析会很有益处。不能低估领域知识的重要性
如果从很多特征(超过100 种)中选择,本章的方法不是很合适,在尝试优化CountVectorizer 时,对每个特征进行单变量测试的时间是个天文数字
6 特征转换
6.1 主成分分析
- 主成分分析(PCA ,principal components analysis )是将有多个相关特征的数据集投影到相关特征较少的坐标系上
- 主成分会产生新的特征,最大化数据的方差
6.1.1 PCA工作原理
- PCA 利用了协方差矩阵的特征值分解
- PCA 也可以在相关矩阵上使用。如果特征的尺度类似,那么可以使用相关矩阵;尺度不同时,应该使用协方差矩阵
- 步骤:
-
- 创建数据集的协方差矩阵;
-
- 计算协方差矩阵的特征值;
-
- 保留前k 个特征值(按特征值降序排列);
-
- 用保留的特征向量转换新的数据点。
-
6.1.2 鸢尾花数据集的PCA——手动处理
鸢尾花数据集(iris)有150 行和4 列。每行(观察值)代表一朵花,每列(特征)代表花的4 种定量特点
- 加载模块
# 从scikit-learn 中导入数据集
from sklearn.datasets import load_iris
# 导入画图模块
import matplotlib.pyplot as plt
%matplotlib inline
# 加载数据集
iris = load_iris()
- 将数据矩阵和响应变量存储到iris_X 和iris_y 中:
# 创建X 和y 变量,存储特征和响应列
iris_X, iris_y = iris.data, iris.target
- 看一下要预测的花的名称
# 要预测的花的名称
iris.target_names
>>> array(['setosa', 'versicolor', 'virginica'], dtype='<U10')
- 查看用于预测的特征名称
# 特征名称
iris.feature_names
>>> ['sepal length (cm)', 'sepal width (cm)', 'petal length (cm)', 'petal width (cm)']
- 查看数据特征
# {0: 'setosa', 1: 'versicolor', 2: 'virginica'}
label_dict = {i: k for i, k in enumerate(iris.target_names)}
def plot(X, y, title, x_label, y_label):
ax = plt.subplot(111)
for label,marker,color in zip(range(3),('^', 's', 'o'),('blue', 'red', 'green')):
plt.scatter(x=X[:,0].real[y == label], y=X[:,1].real[y == label], color=color, alpha=0.5, label=label_dict[label])
plt.xlabel(x_label)
plt.ylabel(y_label)
leg = plt.legend(loc='upper right', fancybox=True)
leg.get_frame().set_alpha(0.5)
plt.title(title)
plt.show()
plot(iris_X, iris_y, "Original Iris Data", "sepal length (cm)", "sepal width (cm)")
- 创建数据集的协方差矩阵
- 先计算特征的均值向量
- 用Numpy计算协方差矩阵
# 手动计算PCA
# 导入NumPy
import numpy as np
# 计算均值向量
mean_vector = iris_X.mean(axis=0)
print(mean_vector)
# 计算协方差矩阵
cov_mat = np.cov((iris_X).T)
print(cov_mat.shape)
得到输出:
>>>[5.84333333 3.054 3.75866667 1.19866667]
>>>(4, 4)
- 计算协方差矩阵的特征值
# 计算鸢尾花数据集的特征向量和特征值
eig_val_cov, eig_vec_cov = np.linalg.eig(cov_mat)
# 按降序打印特征向量和相应的特征值
for i in range(len(eig_val_cov)):
eigvec_cov = eig_vec_cov[:,i]
print('Eigenvector {}: \n{}'.format(i+1, eigvec_cov))
print('Eigenvalue {} from covariance matrix: {}'.format(i+1, eig_val_cov[i]))
print(30 * '-')
得到输出:
Eigenvector 1:
[ 0.36158968 -0.08226889 0.85657211 0.35884393]
Eigenvalue 1 from covariance matrix: 4.224840768320107
------------------------------
Eigenvector 2:
[-0.65653988 -0.72971237 0.1757674 0.07470647]
Eigenvalue 2 from covariance matrix: 0.242243571627515
------------------------------
Eigenvector 3:
[-0.58099728 0.59641809 0.07252408 0.54906091]
Eigenvalue 3 from covariance matrix: 0.07852390809415474
------------------------------
Eigenvector 4:
[ 0.31725455 -0.32409435 -0.47971899 0.75112056]
Eigenvalue 4 from covariance matrix: 0.023683027126001163
------------------------------
- 按降序保留前k个特征值
- 碎石图是一种简单的折线图,显示每个主成分解释数据总方差的百分比
- 绘制碎石图,需要对特征值进行降序排列,绘制每个主成分和之前所有主成分方差的和
- 在鸢尾花数据集上,我们的碎石图有4 个点,每个点代表一个主成分。每个主成分解释了总方差的某个百分比,相加后,所有主成分应该解释了数据集中总方差的100%
取每个特征向量(主成分)的特征值,将其除以所有特征值的和,计算每个特征向量解释方差的百分比:
# 每个主成分解释的百分比是特征值除以特征值之和
explained_variance_ratio = eig_val_cov/eig_val_cov.sum()
explained_variance_ratio
得到输出:
array([0.92461621, 0.05301557, 0.01718514, 0.00518309])
对碎石图进行可视化, 图中的x 轴上有4 个主成分,y 轴是累积方差。 每个数据点代表到这个主成分为止可以解释的方差百分比:
# 碎石图
plt.plot(np.cumsum(explained_variance_ratio))
plt.title('Scree Plot')
plt.xlabel('Principal Component (k)')
plt.ylabel('% of Variance Explained <= k')
得到输出:
![[Pasted image 20220922181545.png]]
上图告诉我们,前两个主成分就占了原始方差的近98% ,意味着几乎可以只用前两个特征向量作为新的主成分
- 使用保留的特征向量转换新的数据点
- 存储要保留的特征向量:
# 保存两个特征向量
top_2_eigenvectors = eig_vec_cov[:,:2].T
- 将原始数据投影
示意图:
![[Pasted image 20220922182200.png]]
代码表示投影:
# 将数据集从150 °¡ 4 转换到150 °¡ 2
# 将数据矩阵和特征向量相乘
np.dot(iris_X, top_2_eigenvectors.T)[:5,]
>>>array([[ 2.82713597, -5.64133105],
[ 2.79595248, -5.14516688],
[ 2.62152356, -5.17737812],
[ 2.7649059 , -5.00359942],
[ 2.78275012, -5.64864829]])
通过这种方式,就将四维的鸢尾花数据集转换为了只有两列的新矩阵,这个新矩阵可以在机器学习中代替原始的数据集。
6.1.3 scikit-learn的PCA
scikit-learn 在转换器中实现了这个过程,所以不必手动编写
(1) 导入PCA包
# scikit-learn 的PCA
from sklearn.decomposition import PCA
(2) 实例化对象
# 和其他scikit 模块一样,先实例化
pca = PCA(n_components=2)
(3) 拟合数据
# 在数据上使用PCA
pca.fit(iris_X)
(4) 查看PCA对象的属性
pca.components_
>>> array([[ 0.36158968, -0.08226889, 0.85657211, 0.35884393], [ 0.65653988, 0.72971237, -0.1757674 , -0.07470647]])
# 第二列是手动过程的负数,因为特征向量可以为正也可以为负
# 对机器学习流水线几乎没有影响
(5) 将原始数据投影
pca.transform(iris_X)[:5,]
>>> array([[-2.68420713, 0.32660731],
[-2.71539062, -0.16955685],
[-2.88981954, -0.13734561],
[-2.7464372 , -0.31112432],
[-2.72859298, 0.33392456]])
# scikit-learn 的PCA 会将数据中心化,所以和手动过程的数据不一样
(6) 提取每个主成分解释的方差量
# 每个主成分解释的方差量
# 和之前的一样
pca.explained_variance_ratio_
6.1.4 标准化化和缩放对PCA的影响
- scikit-learn 的 PCA 在预测阶段会将数据进行中心化( centering)
- 使用StandardScaler标准化不影响PCA的结果
- 两个矩阵的协方差矩阵相同,那么它们的特征值分解也相同
- 使用z-score影响PCA的结果
- 对数据进行缩放后,列与列的协方差会更加一致,而且每个主成分解释的方差会变得分散,而不是集中在一个主成分中
- 在实践和生产环境下,我们会建议进行缩放,但应该在缩放和未缩放的数据上都进行性能测试
6.2 线性判别分析
- 和PCA 一样,LDA 的目标是提取一个新的坐标系,将原始数据集投影到一个低维空间中
- 和PCA 的主要区别在于,LDA 不会专注于数据的方差,而是优化低维空间,以获得最佳的类别可分性
- 非常适合于分类任务的场景
- LDA 极为有用的原因在于,基于类别可分性的分类有助于避免机器学习流水线的过拟合,也叫防止维度诅咒。
- LDA 也会降低计算成本。
6.2.1 LDA的工作原理
LDA需要计算类内(within-class )和类间(between-class )散布矩阵的特征值和特征向量
LDA 分为5 个步骤:
-
计算每个类别的均值向量;
-
计算类内和类间的散布矩阵
-
计算 S W − 1 S B S_{W}^{-1}S_{B} SW−1SB的特征值和特征向量
-
降序排列特征值,保留前k 个特征向量;
-
使用前几个特征向量将数据投影到新空间
-
计算每个类的均值向量
首先计算每个类别中每列的均值向量,分别是setosa 、versicolor 和virginica :
# 每个类别的均值向量
# 将鸢尾花数据集分成3 块
# 每块代表一种鸢尾花,计算均值
mean_vectors = []
for cl in [0, 1, 2]:
class_mean_vector = np.mean(iris_X[iris_y==cl], axis=0)
mean_vectors.append(class_mean_vector)
print(label_dict[cl], class_mean_vector)
得到结果:
setosa [5.006 3.418 1.464 0.244]
versicolor [5.936 2.77 4.26 1.326]
virginica [6.588 2.974 5.552 2.026]
- 计算类内和类间的散度矩阵
类内散度矩阵:
S w = Σ i = 1 c S i S_w=\Sigma_{i=1}^{c}S_i Sw=Σi=1cSi
其中 S i S_i Si的定义是: S i = Σ x ∈ D i n ( x − m i ) ( x − m i ) T S_i=\Sigma_{x\in D_i}^{n}(x-m_i)(x-m_i)^T Si=Σx∈Din(x−mi)(x−mi)T
其中 m i m_i mi代表第 i i i个类别的均值向量
类间散度矩阵:
S
B
=
Σ
i
=
1
c
N
i
(
m
i
−
m
)
(
m
i
−
m
)
T
S_B=\Sigma_{i=1}^{c}N_i(m_i-m)(m_i-m)^T
SB=Σi=1cNi(mi−m)(mi−m)T
其中
m
m
m是数据集的总体均值,
m
i
m_i
mi是每个类别的样本均值,
N
i
N_i
Ni是每个类别的样本大小(观察值数量)
示例:
# 类内散布矩阵
S_W = np.zeros((4,4))
# 对于每种鸢尾花
for cl,mv in zip([0, 1, 2], mean_vectors):
# 从0 开始,每个类别的散布矩阵
class_sc_mat = np.zeros((4,4))
# 对于每个样本
for row in iris_X[iris_y == cl]:
# 列向量
row, mv = row.reshape(4,1), mv.reshape(4,1)
# 4 °¡ 4 的矩阵
class_sc_mat += (row-mv).dot((row-mv).T)
# 散布矩阵的和
S_W += class_sc_mat
S_W
>>> array([[38.9562, 13.683 , 24.614 , 5.6556],
[13.683 , 17.035 , 8.12 , 4.9132],
[24.614 , 8.12 , 27.22 , 6.2536],
[ 5.6556, 4.9132, 6.2536, 6.1756]])
类间散度矩阵
# 数据集的均值
overall_mean = np.mean(iris_X, axis=0).reshape(4,1)
# 会变成散布矩阵
S_B = np.zeros((4,4))
for i,mean_vec in enumerate(mean_vectors):
# 每种花的数量
n = iris_X[iris_y==i,:].shape[0]
# 每种花的列向量
mean_vec = mean_vec.reshape(4,1)
S_B += n * (mean_vec - overall_mean).dot((mean_vec - overall_mean).T)
S_B
>>> array([[ 63.21213333, -19.534 , 165.16466667, 71.36306667],
[-19.534 , 10.9776 , -56.0552 , -22.4924 ],
[165.16466667, -56.0552 , 436.64373333, 186.90813333],
[ 71.36306667, -22.4924 , 186.90813333, 80.60413333]])
- 计算 S W − 1 S B S_{W}^{-1}S_{B} SW−1SB的特征值和特征向量
# 计算矩阵的特征值和特征向量
eig_vals, eig_vecs = np.linalg.eig(np.dot(np.linalg.inv(S_W), S_B))
eig_vecs = eig_vecs.real
eig_vals = eig_vals.real
for i in range(len(eig_vals)):
eigvec_sc = eig_vecs[:,i]
print('Eigenvector {}: {}'.format(i+1, eigvec_sc))
print('Eigenvalue {:}: {}'.format(i+1, eig_vals[i]))
得到结果:
>>> Eigenvector 1: [ 0.20490976 0.38714331 -0.54648218 -0.71378517]
>>> Eigenvalue 1: 32.27195779972981
>>> Eigenvector 2: [-0.00898234 -0.58899857 0.25428655 -0.76703217]
>>> Eigenvalue 2: 0.27756686384004514
>>> Eigenvector 3: [ 0.26284129 -0.36351406 -0.41271318 0.62287111]
>>> Eigenvalue 3: -2.170668690724263e-15
>>> Eigenvector 4: [ 0.26284129 -0.36351406 -0.41271318 0.62287111]
>>> Eigenvalue 4: -2.170668690724263e-15
*注意第三个和第四个特征值几乎是0 ,这是因为LDA 的工作方式是在类间划分决策边界。考虑到鸢尾花数据中只有3 个类别,我们可能只需要2 个决策边界。通常来说,用LDA 拟合n个类别的数据集,最多只需要n - 1 次切割
- 降序排列特征值,保留前k个特征向量
和PCA 一样,只保留最有用的特征向量:
# 保留最好的两个线性判别式
linear_discriminants = eig_vecs.T[:2]
linear_discriminants
>>> array([[ 0.20490976, 0.38714331, -0.54648218, -0.71378517],
[-0.00898234, -0.58899857, 0.25428655, -0.76703217]])
用每个特征值除以特征值的和,可以查看每个类别(线性判别式)解释总方差的比例:
# 解释总方差的比例
eig_vals / eig_vals.sum()
>>> array([ 9.91472476e-01, 8.52752434e-03, -6.66881840e-17, -6.66881840e-17])
*第一个判别式拥有超过99% 的信息
- 使用前几个特征向量投影到新空间
# LDA 投影数据
lda_iris_projection = np.dot(iris_X, linear_discriminants.T)
plot(lda_iris_projection, iris_y, "LDA Projection", "LDA1", "LDA2")
得到结果:
![[Pasted image 20220922222753.png]]
6.2.2 在scikit-learn中使用LDA
导入包:
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
- LDA 其实是伪装成特征转换算法的分类器
- 和PCA 的无监督计算(不需要响应变量)不同,LDA 会尝试用响应变量查找最佳坐标系,尽可能优化类别可分性
- 这意味着,LDA 只在响应变量存在时才可以使用。使用时,我们把响应变量作为第二个参数输入fit
# 实例化LDA 模块
lda = LinearDiscriminantAnalysis(n_components=2)
# 拟合并转换鸢尾花数据
X_lda_iris = lda.fit_transform(iris_X, iris_y)
# 绘制投影数据
plot(X_lda_iris, iris_y, "LDA Projection", "LDA1", "LDA2")
得到结果:
![[Pasted image 20220922230939.png]]
LDA有一个scalings_属性,没有components_,但是二者的行为基本相同
# 和pca.components_基本一样,但是转置了(4 °¡ 2,而不是2 °¡ 4)
lda.scalings_
>>> array([[ 0.81926852, 0.03285975],
[ 1.5478732 , 2.15471106],
[-2.18494056, -0.93024679],
[-2.85385002, 2.8060046 ]])
# 和手动计算一样
lda.explained_variance_ratio_
>>> array([0.99147248, 0.00852752])
==LDA 和PCA 都不改变数据尺度,所以缩放非常重要
LDA 等有监督特征转换的局限性在于,不能像 PCA 那样处理聚类任务。这是因为聚类是无监督的任务,没有 LDA 需要的响应变量。
6.3 LDA与PCA:使用鸢尾花数据集
- 导入包
from sklearn.neighbors import KNeighborsClassifier
from sklearn.pipeline import Pipeline
from sklearn.model_selection import cross_val_score
- 创建三个变量,分别是LDA、PCA和KNN模型
# 创建有一个主成分的PCA 模块
single_pca = PCA(n_components=1)
# 创建有一个判别式的LDA 模块
single_lda = LinearDiscriminantAnalysis(n_components=1)
# 实例化KNN 模型
knn = KNeighborsClassifier(n_neighbors=3)
- 调用KNN模型,求一个基线准确率
# 不用特征转换,用KNN 进行交叉验证
knn_average = cross_val_score(knn, iris_X, iris_y).mean()
# 这是基线准确率。如果什么也不做,KNN 的准确率是98%
knn_average
>>> 0.9803921568627452
- LDA,只保留最好的线性判别式
lda_pipeline = Pipeline([('lda', single_lda), ('knn', knn)])
lda_average = cross_val_score(lda_pipeline, iris_X, iris_y).mean()
# 比PCA 好,比原始的差
lda_average
>>> 0.9673202614379085
- PCA
# 创建执行PCA 的流水线
pca_pipeline = Pipeline([('pca', single_pca), ('knn', knn)])
pca_average = cross_val_score(pca_pipeline, iris_X, iris_y).mean()
pca_average
>>> 0.8941993464052288
- 添加一个LDA判别式
# 试试有两个判别式的LDA
lda_pipeline = Pipeline([('lda', LinearDiscriminantAnalysis(n_components=2)), ('knn', knn)])
lda_average = cross_val_score(lda_pipeline, iris_X, iris_y).mean()
# 和原来的一样
lda_average
>>> 0.9803921568627452
- 加入特征选择模块
# 用特征选择工具和特征转换工具做对比
from sklearn.feature_selection import SelectKBest
# 尝试所有的k值,但是不包括全部保留
for k in [1, 2, 3]:
# 构建流水线
select_pipeline = Pipeline([('select', SelectKBest(k=k)), ('knn', knn)])
# 交叉验证流水线
select_average = cross_val_score(select_pipeline, iris_X, iris_y).mean()
print(k, "best feature has accuracy:", select_average)
- 联合使用有监督和无监督的特征转换
设置一个GridSearch模块,找参数的最佳组合
- 缩放数据(用或不用均值/ 标准差)
- PCA 主成分;
- LDA 判别式;
- KNN 邻居。
建立一个get_best_model_and_accuracy 函数,向其传入一个模型(scikit-learn 或其他模型)、一个字典形式的参数集合,以及X 和y 数据集,会输出网格搜索模块的结果,包括模型的最佳表现(准确率)、获得最佳表现时的最好参数、平均拟合时间,以及平均预测时间:
def get_best_model_and_accuracy(model, params, X, y):
grid = GridSearchCV(model, # 网格搜索的模型
params, # 试验的参数
error_score=0.) # 如果出错,当作结果是0
grid.fit(X, y) # 拟合模型和参数
# 传统的性能指标
print("Best Accuracy: {}".format(grid.best_score_))
# 最好参数
print("Best Parameters: {}".format(grid.best_params_))
# 平均拟合时间(秒)
print("Average Time to Fit (s):{}".format(round(grid.cv_results_['mean_fit_time'].mean(), 3)))
# 平均预测时间(秒)
# 显示模型在实时分析中的性能
print("Average Time to Score (s):{}".format(round(grid.cv_results_['mean_score_time'].mean(), 3)))
组合使用缩放、PCA、LDA和KNN进行测试:
from sklearn.model_selection import GridSearchCV
iris_params = { 'preprocessing__scale__with_std': [True, False],
'preprocessing__scale__with_mean': [True, False],
'preprocessing__pca__n_components':[1, 2, 3, 4],
# 根据scikit-learn 文档,LDA 的最大n_components 是类别数减1
'preprocessing__lda__n_components':[1, 2],
'clf__n_neighbors': range(1, 9)
}
# Pipeline
preprocessing = Pipeline([('scale', StandardScaler()), ('pca', PCA()), ('lda',
LinearDiscriminantAnalysis())])
iris_pipeline = Pipeline(steps=[('preprocessing', preprocessing), ('clf',
KNeighborsClassifier())])
get_best_model_and_accuracy(iris_pipeline, iris_params, iris_X, iris_y)
>>> Best Accuracy: 0.9866666666666667
>>> Best Parameters: {'clf__n_neighbors': 3, 'preprocessing__lda__n_components': 2, 'preprocessing__pca__n_components': 3, 'preprocessing__scale__with_mean': True, 'preprocessing__scale__with_std': False}
>>> Average Time to Fit (s): 0.002
>>> Average Time to Score (s): 0.001
7 特征学习
![[Pasted image 20220923093111.png]]
7.1 受限玻尔兹曼机(RBM)
- RBM 是一组无监督的特征学习算法,使用概率模型学习新特征
- 在RBM 提取特征之后使用线性模型(线性回归、逻辑回归、感知机等)往往效果最佳
- RBM 的无监督性质很重要,所以它和 PCA 的相似性高于和 LDA 的相似性。 RBM 和 PCA算法在提取新特征时都不需要真实值,可以用于更多的机器学习问题
- RBM 是一个浅层(两层)的神经网络, 隐藏层的节点数是人为选取的,代表我们想学习的特征数。
7.1.1 不一定降维
- PCA 和 LDA 对可以提取的特征数量有严格的限制
- PCA只能使用等于或小于原始特征数的输出
- LDA只能输出类别的数量减1
- RBM可以学习的特征数量取决于要解决的问题,可以进行网格搜索
7.1.2 受限玻尔兹曼机的图
![[Pasted image 20220923093744.png]]
可见层有4 个节点,代表原始数据的4 列
计算过程:
import numpy as np
import math
# S 形函数
def activation(x):
return 1 / (1 + math.exp(-x))
inputs = np.array([1, 2, 3, 4])
weights = np.array([0.2, 0.324, 0.1, .001])
bias = 1.5
a = activation(np.dot(inputs.T, weights) + bias)
print(a)
>>> 0.9341341524806636
7.1.3 玻尔兹曼机的限制
RBM 的限制是,不允许任何层内通信。这样,节点可以独立地创造权重和偏差,最终成为(希望是)独立的特征。
7.2 伯努利受限玻尔兹曼机
scikit-learn 中唯一的 RBM 实现是BernoulliRBM, 伯努利分布要求数据的值为0 ~1 。
7.2.1 从MNIST中提取RBM特征
# 使用MNIST数据集
# 从CSV 中创建NumPy 数组
images = np.genfromtxt('mnist_train.csv', delimiter=',')
# 提取X 和y 变量
images_X, images_y = images[:,1:], images[:,0]
# 进行缩放
# 把images_X 缩放到0~1
images_X = images_X / 255.
# 二分像素(白或黑)
images_X = (images_X > 0.5).astype(float)
# 实例化BernoulliRBM
# 设置random_state,初始化权重和偏差
# verbose 是True,观看训练
# n_iter 是前后向传导次数
# n_components 与PCA 和LDA 一样,代表我们希望创建的特征数
# n_components 可以是任意整数,小于、等于或大于原始特征数均可
rbm = BernoulliRBM(random_state=0, verbose=True, n_iter=20, n_components=100)
rbm.fit(images_X)
查看训练结果
# RBM 也有components_
len(rbm.components_)
>>> 100
特征可视化
# 绘制RBM 特征(新特征集的表示)
plt.figure(figsize=(10, 10))
for i, comp in enumerate(rbm.components_):
plt.subplot(10, 10, i + 1)
plt.imshow(comp.reshape((28, 28)), cmap=plt.cm.gray_r)
plt.xticks(())
plt.yticks(())
plt.suptitle('100 components extracted by RBM')
plt.show()
得到结果:
![[Pasted image 20220923095722.png]]
检查是否有一样的特征:
# 好像有些特征一样
# 但是其实所有的特征都不一样(虽然有的很类似)
np.unique(rbm.components_.mean(axis=1)).shape
>>> (100,)
用特征转换数据:
# 用玻尔兹曼机转换数字5
image_new_features = rbm.transform(images_X[:1]).reshape(100,)
7.3 在Pipeline中应用RBM
创建三个Pipeline并对比效果:
- 原始像素强度上的逻辑回归模型;
- PCA 主成分上的逻辑回归;
- RBM 特征上的逻辑回归
7.3.1 线性模型
# 导入逻辑回归和网格搜索模块
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import GridSearchCV
# 创建逻辑回归
lr = LogisticRegression()
params = {'C':[1e-2, 1e-1, 1e0, 1e1, 1e2]}
# 实例化网格搜索类
grid = GridSearchCV(lr, params)
# 拟合数据
grid.fit(images_X, images_y)
# 最佳参数
grid.best_params_, grid.best_score_
>>> ({'C': 0.1}, 0.8908333333333334)
7.3.2 对提取的PCA 主成分应用线性模型
# 用PCA 提取特征
lr = LogisticRegression()
pca = PCA()
# 设置流水线的参数
params = {'clf__C':[1e-1, 1e0, 1e1], 'pca__n_components': [10, 100, 200]}
# 创建流水线
pipeline = Pipeline([('pca', pca), ('clf', lr)])
# 实例化网格搜索类
grid = GridSearchCV(pipeline, params)
# 拟合数据
grid.fit(images_X, images_y)
# 最佳参数
grid.best_params_, grid.best_score_
>>> ({'clf__C': 10.0, 'pca__n_components': 100}, 0.8876666666666667)
7.3.3 对提取的RBM 特征应用线性模型
# 用RBM 学习新特征
rbm = BernoulliRBM(random_state=0)
# 设置流水线的参数
params = {'clf__C':[1e-1, 1e0, 1e1], 'rbm__n_components': [100, 200]}
# 创建流水线
pipeline = Pipeline([('rbm', rbm), ('clf', lr)])
# 实例化网格搜索类
grid = GridSearchCV(pipeline, params)
# 拟合数据
grid.fit(images_X, images_y)
# 最佳参数
grid.best_params_, grid.best_score_
>>> ({'clf__C': 1.0, 'rbm__n_components': 200}, 0.9156666666666666)
上面的例子证明了,面对非常复杂的任务(例如图像识别、音频处理和自然语言处理),特征学习算法很有效。这些大数据集有很多隐藏的特征,难以通过线性变换(如PCA 或LDA )提取,但是非参数算法(如RBM )可以。
7.4 学习文本特征:词向量
7.4.1 词嵌入
词嵌入 是单词在n 维特征空间中的向量化,其中n 代表单词潜在特征的数量。
例如,我们提取每个单词的n = 5 个特征,那么词汇表中的每个单词都会变成1 * 5 的向量
向量化的结果有可能是:
# 词嵌入的例子
king = np.array([.2, -.5, .7, .2, -.9])
man = np.array([-.5, .2, -.2, .3, 0.])
woman = np.array([.7, -.3, .3, .6, .1])
queen = np.array([ 1.4, -1. , 1.2, 0.5, -0.8])
7.4.2 两种词嵌入方法:Word2vec 和GloVe
主要的区别在于(出自斯坦福大学的)GloVe 算法通过一系列矩阵统计进行学习,而(出自Google 的)Word2vec 通过深度学习进行学习。
7.4.3 Word2vec:另一个浅层神经网络
![[Pasted image 20220923103312.png]]
- 输入层和希望学习的词汇长度相同。如果语料库有几百万词,但是我们只需要学习其中一小部分,那么这种设定很有用。在上图中,我们希望学习5000个单词的上下文。
- 图中的隐藏层代表对于每个单词要学习的特征数。本例要将词嵌入300维的空间中
- 这个神经网络和之前RBM 神经网络的主要区别在于存在输出层,输出层和输入层的节点数量一样
在这个结构上训练,通过传入单词的独热向量,提取隐藏层的输出向量并将其作为潜在结构,最终学习300 维的单词表示。在生产中,因为输出节点非常多,所以上图结构的效率极低。
7.4.4 创建Word2vec 词嵌入的gensim 包
导入包:
# 导入gensim 包
import gensim
设置一个日志记录器,以便查看详细训练过程:
import logging
logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)
创建语料库:
from gensim.models import word2vec, Word2vec
sentences = word2vec.Text8Corpus('../data/text8')
==gensim 需要可迭代对象(列表、生成器、元组等),里面是切分好的句子
训练gensim
# 实例化gensim 模块
# min-count 忽略出现次数小于该值的单词
# size 是要学习的单词维数
model = gensim.models.Word2vec(sentences, min_count=1, size=20)
查看词嵌入:
# 单个单词的嵌入
model.wv['king']
>>> array([-0.48768288, 0.66667134, 2.33743191, 2.71835423, 4.17330408, 2.30985498, 1.92848825, 1.43448424, 3.91518641, -0.01281452, 3.82612252, 0.60087812, 6.15167284, 4.70150518, -1.65476751, 4.85853577, 3.45778084, 5.02583361, -2.98040175, 2.37563372], dtype=float32)
查找相似的单词
==词嵌入会受限于选择的语料库,可以导入预训练的词嵌入,结局这个问题
model = gensim.models.KeyedVectors.load_word2vec_format('../data/GoogleNews-vectorsnegative300.bin', binary=True)
# 300万单词
len(model.wv.vocab)
3000000
示例:
# 女 + 国王 - 男 = 女王
model.wv.most_similar(positive=['woman', 'king'], negative=['man'], topn=1)
>>> [('queen', 0.7118192911148071)]
# 伦敦之于英国相当于巴黎之于?
model.wv.most_similar(positive=['Paris', 'England'], negative=['London'], topn=1)
>>> [('France', 0.667637825012207)]
- most_similar 方法会返回词汇表中与所提供单词最相似的词项
- 正列表(positive)中的单词是要相加的向量
- 负列表(negative)中的单词是要从结果中减去的
gensim中的doesnt_match
- 这个方法会选出不属于列表的单词,做法是将和其他单词平均值最不接近的单词分离开来
示例:
# 选出不属于同一类别的单词
model.wv.doesnt_match("duck bear cat tree".split())
>>> 'tree'
- 这个包也可以计算单词间的相似性(分数为0 ~1 ),用于动态比较单词
# 0~1 的相似性分数
# “女人”和“男人”的相似度,比较相似
model.wv.similarity('woman', 'man')
>>> 0.766401223
# “树”和“男人”的相似度,不大相似
model.wv.similarity('tree', 'man')
>>> 0.22937459
7.4.5 词嵌入的应用:信息检索
# 查找词嵌入,没有就返回None
def get_embedding(string):
try:
return model.wv[string]
except:
return None
创建三个文章标题:
# 原创标题
sentences = ["this is about a dog", "this is about a cat", "this is about nothing"]
目标是输入接近dog 或cat 的参考词,获取相关标题
先对每个句子创建一个 3 × 300 3\times 300 3×300的向量化矩阵。具体做法是对句子中的每个单词取均值,作为句子的均值向量。在向量化后,我们就可以对句子和参考词求点积,进行比较。最接近的向量,点积最大.
import numpy as np
from functools import reduce
# 3 * 300 的零矩阵
vectorized_sentences = np.zeros((len(sentences),300))
# 对于每个句子
for i, sentence in enumerate(sentences):
# 分词
words = sentence.split(' ')
# 进行词嵌入
embedded_words = [get_embedding(w) for w in words]
embedded_words = filter(lambda x:x is not None, embedded_words)
# 对标题进行向量化,取均值
vectorized_sentence = reduce(lambda x,y:x+y, embedded_words)/ len(list(embedded_words))
# 改成向量
vectorized_sentences[i:] = vectorized_sentence
vectorized_sentences.shape
>>> (3, 300)
检验效果:
# 和“狗”最接近的句子
reference_word = 'dog'
# 词嵌入和向量化矩阵的点积
best_sentence_idx = np.dot(vectorized_sentences, get_embedding(reference_word)).argsort()[-1]
# 最相关的句子
sentences[best_sentence_idx]
>>> 'this is about a dog'
还有一个例子:
sentences = """ How to Sound Like a Data Scientist
Types of Data
The Five Steps of Data Science
Basic Mathematics
A Gentle Introduction to Probability
Advanced Probability
Basic Statistics
Advanced Statistics
Communicating Data
Machine Learning Essentials
Beyond the Essentials
Case Studies
""".split('\n')
目标是给定主题,用参考词提供3 个最相关的标题。例如,给定“数学”,我们有可能建议阅读关于基础数学、统计学和概率的章节
# 12 * 300 的零矩阵
vectorized_sentences = np.zeros((len(sentences),300))
# 对于每个句子
for i, sentence in enumerate(sentences):
# 分词
words = sentence.split(' ')
# 进行词嵌入
embedded_words = [get_embedding(w) for w in words]
embedded_words = filter(lambda x:x is not None, embedded_words)
# 对标题进行向量化,取均值
vectorized_sentence = reduce(lambda x,y:x+y, embedded_words)/ len(list(embedded_words))
# 改成向量
vectorized_sentences[i:] = vectorized_sentence
vectorized_sentences.shape
>>> (12, 300)
寻找和math 最相关的章节:
# 和“数学”最相关的章节
reference_word = 'math'
best_sentence_idx = np.dot(vectorized_sentences, get_embedding(reference_word)).argsort()[-3:][::-1]
[sentences[b] for b in best_sentence_idx]
>> ['Basic Mathematics', 'Basic Statistics', 'Advanced Probability ']
词嵌入可以从文本中检索上下文相关的信息
特征选择的步骤: