手写数字图像识别各种算法的比较
1、准备工作
1.1、数据集介绍
使用到了两个手写数字的数据集:
-
scikit-learn
中的手写数字数据集;
-
mnist
中的手写数字数据集。
1.1.1、scikit-learn 中的手写数字数据集
首先来看一下 scikit-learn
中的手写数字数据集:
# scikit-learn 中的数据集
from sklearn.datasets import load_digits
digits = load_digits()
print(digits.keys())
dict_keys(['data', 'target', 'frame', 'feature_names', 'target_names', 'images', 'DESCR'])
包含在 scikit-learn 中的数据集通常被保存为 Bunch 对象,里面包含真实数据以及一些数据集信息。
print(digits.images.shape)
# (1797, 8, 8)
可以看到这份图像数据是一个三维矩阵:一共有 1797 个样本,每张图像都是 8 像素 × 8 像素。对前 100 张图片进行可视化:
# 对前 100 张图片进行可视化
import matplotlib.pyplot as plt
fig, axes = plt.subplots(10, 10, figsize=(8, 8),
subplot_kw={'xticks':[], 'yticks':[]},
gridspec_kw=dict(hspace=0.1, wspace=0.1))
for i, ax in enumerate(axes.flat):
ax.imshow(digits.images[i], cmap='binary', interpolation='nearest')
ax.text(0.05, 0.05, str(digits.target[i]),
transform=ax.transAxes, color='green')
plt.show()
为了在 scikit-learn
中使用数据,我们需要一个维度为 [n_samples, n_features] 的二维特征矩阵——可以将每个样本图像的所有像素都作为特征,也就是将每个 8 像素 × 8 像素平铺成长度为 64 的一维数组。另外,还需要一个目标数组,用来表示每个数字的真实值(标签)。这两份数据已经放在手写数字数据集的 data
和 target
属性中:
# 特征矩阵
X = digits.data
print(X.shape) # (1797, 64)
# 目标矩阵
y = digits.target
print(y.shape) # (1797,)
一共有 1797 个样本和 64 个特征。
1.1.2、mnist 中的手写数字数据集
再来看一下 mnist
中的手写数字数据集:
# mnist 中的数据集
from keras.datasets import mnist
(train_images, train_labels), (test_images, test_labels) = mnist.load_data()
print(train_images.shape, test_images.shape)
# (60000, 28, 28) (10000, 28, 28)
可以看到这份图像数据也是一个三维矩阵:一共有 70000 个样本,每张图片都是 28 像素 × 28 像素。其中训练样本 60000 个,测试样本 10000 个。
print(train_labels[0])
# 5
plt.imshow(train_images[0])
plt.show()
1.2、特征提取
1.2.1、主成分分析(PCA)
1、主成分分析(PCA
)是一种旋转数据集的方法,旋转后的特征在统计上不相关。在做完这种旋转后,通常是根据新特征对解释数据的重要性来选择它的一个子集。PCA
最常见的一个应用就是高维数据可视化。现在我们来使用 PCA
对两种手写数据数据集进行降维可视化:
# 使用 PCA 对 digits 可视化
from sklearn.decomposition import PCA
# 保留数据的前两个主成分
pca = PCA(n_components=2)
# 对 sklearn 中的手写数字数据集拟合 PCA 模型
pca.fit(digits.data)
# 将数据变换到前两个主成分的方向上
digits_pca = pca.transform(digits.data)
print("原始维度:{}".format(str(digits.data.shape)))
# 原始维度:(1797, 64)
print("降维后的维度:{}".format(str(digits_pca.shape)))
# 降维后的维度:(1797, 2)
现在我们对前两个主成分作图:
# 对第一个和第二个主成分作图,按类别着色
plt.scatter(digits_pca[:, 0], digits_pca[:, 1], c=digits.target,
edgecolors='none', alpha=5,
cmap=plt.cm.get_cmap('Spectral', 10))
plt.colorbar(label='digit label', ticks=range(10))
plt.clim(-0.5, 9.5)
plt.show()
我们还可以将数据实际绘制成文本,而不是散点:
# 构建一个 PCA 模型
pca = PCA(n_components=2)
pca.fit(digits.data)
# 将数据变换到前两个主成分的方向上
digits_pca = pca.transform(digits.data)
colors = ['#476A2A', '#7851B8', '#BD3430', '#4A2D4E', '#875525',
'#A83683', '#4E655E', '#853541', '#3A3120', '#535D8E']
plt.figure(figsize=(10, 10))
plt.xlim(digits_pca[:, 0].min(), digits_pca[:, 0].max())
plt.ylim(digits_pca[:, 1].min(), digits_pca[:, 1].max())
for i in range(len(digits.data)):
# 将数据实际绘制成文本,而不是散点图
plt.text(digits_pca[i, 0], digits_pca[i, 1], str(digits.target[i]),
color = colors[digits.target[i]],
fontdict={'weight':'bold','size':9})
plt.xlabel("First principal component")
plt.ylabel("Second principal component")
plt.show()
2、PCA
的另一个应用是特征提取。特征提取背后的思想是,可以找到一种数据表示,比给定的原始表示更适合于分析。特征提取很有用,它的一个很好的应用实例就是图像。图像由像素组成,通常存储为红绿蓝(RGB
)强度。图像中的对象通常由上千个像素组成,它们只有放在一起才有意义。
这里我们启用 PCA
的白化(whitening
)选项,它将主成分缩放到相同的尺度。变换后的结果与使用 StandardScaler
相同。白化不仅对应于旋转数据,还对应于缩放数据使其形状是圆形而不是椭圆:
from sklearn.decomposition import PCA
pca = PCA(n_components=40, whiten=True, random_state=0).fit(X_train) # 使用40个主成分表示原特征
X_train_pca = pca.transform(X_train)
X_test_pca = pca.transform(X_test)
print("X_train_pca.shape: {}".format(X_train_pca.shape))
# X_train_pca.shape: (1347, 40)
print("X_test_pca.shape: {}".format(X_test_pca.shape))
# X_test_pca.shape: (450, 40)
我们可以使用原数据(64 个特征)与特征提取后的数据分别进行拟合,查看模型的泛化效果。
1.2.2、流形学习
-
t-SNE
t-SNE
的思想是找到数据的一个二维表示,然后尝试让在原始特征空间中距离较近的点更加靠近,原始特征中相距较远的点更加远离。
# t-SNE
from sklearn.manifold import TSNE
tsne = TSNE(random_state=42)
# 使用 fit_transform 方法
digits_tsne = tsne.fit_transform(digits.data)
plt.figure(figsize=(10, 10))
plt.xlim(digits_tsne[:, 0].min(), digits_tsne[:, 0].max())
plt.ylim(digits_tsne[:, 1].min(), digits_tsne[:, 1].max())
for i in range(len(digits.data)):
# 将数据实际绘制成文本,而不是散点图
plt.text(digits_tsne[i, 0], digits_tsne[i, 1], str(digits.target[i]),
color = colors[digits.target[i]],
fontdict={'weight':'bold','size':9})
plt.xlabel("t-SNE feature 0")
plt.ylabel("t-SNE feature 1")
plt.show()
-
保距映射法(Isomap
)
Isomap
寻求一种低维表示,以保持点之间的 “距离”。距离是曲面距离的推广。因此,Isomap
不是用毕达哥拉斯定理导出的距离公式来测量纯欧几里德距离,而是沿着发现的流形优化距离。
# Isomap
from sklearn.manifold import Isomap
iso = Isomap(n_components=2)
iso.fit(digits.data)
digits_iso = iso.transform(digits.data)
plt.scatter(digits_iso[:, 0], digits_iso[:, 1], c=digits.target,
edgecolors='none', alpha=5,
cmap=plt.cm.get_cmap('Spectral', 10))
plt.colorbar(label='digit label', ticks=range(10))
plt.clim(-0.5, 9.5)
plt.show()
-
多维标度法(MDS
)
# MDS
from sklearn.manifold import MDS
mds = MDS(n_components=2)
# mds.fit(digits.data)
digits_mds = mds.fit_transform(digits.data)
plt.scatter(digits_mds[:, 0], digits_mds[:, 1], c=digits.target,
edgecolors='none', alpha=5,
cmap=plt.cm.get_cmap('Spectral', 10))
plt.colorbar(label='digit label', ticks=range(10))
plt.clim(-0.5, 9.5)
plt.show()
各个数字在参数空间中的分离程度还是令人满意的,这其实告诉我们:用一个简单的有监督分类算法就可以完成分类。
2、高斯朴素贝叶斯分类
2.1、加载数据集
数据集选择:
选择的是使用 scikit-learn
中自带的手写数字数据集。
# 加载并可视化手写数字
from sklearn.datasets import load_digits
digits = load_digits()
print(digits.images.shape)
# (1797, 8, 8)
# 特征矩阵
X = digits.data
print(X.shape) # (1797, 64)
# 目标矩阵
y = digits.target
print(y.shape) # (1797,)
2.2、数字分类
先将数据集分成训练集和测试集,然后用高斯朴素贝叶斯模型来拟合:
# 将数据分成训练集和测试集
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
# 用高斯朴素贝叶斯模型来拟合
from sklearn.naive_bayes import GaussianNB
model = GaussianNB()
model.fit(X_train, y_train) # 拟合数据,构建模型
y_model = model.predict(X_test) # 做出预测
2.3、评估模型
-
准确率
# 训练集的准确率
print(model.score(X_train, y_train)) # 0.8574610244988864
# 测试集的准确率
print(model.score(X_test, y_test)) # 0.8333333333333334
-
混淆矩阵
# 混淆矩阵
import seaborn as sns
from sklearn.metrics import confusion_matrix
mat = confusion_matrix(y_test, y_model)
sns.heatmap(mat, square=True, annot=True, cbar=False)
plt.xlabel('predicted value')
plt.ylabel('true value')
可以看到,误判的原因在于许多数字 2 被误判成了数字 1 或 8。
-
交叉验证
# 五轮交叉验证
from sklearn.model_selection import cross_val_score
print(cross_val_score(model, X, y, cv=5))
'''
[0.78055556 0.78333333 0.79387187 0.8718663 0.80501393]
'''
-
更加直观的方式
另一种显示模型特征的直观方式是将样本画出来,然后把预测标签放在左下角,用绿色表示预测正确,用红色表示预测错误:
# 更直观的方式
fig, axes = plt.subplots(10,10,figsize=(8,8),
subplot_kw={'xticks':[],'yticks':[]},
gridspec_kw=dict(hspace=0.1,wspace=0.1))
test_images = X_test.reshape(-1,8,8)
for i, ax in enumerate(axes.flat):
ax.imshow(test_images[i],cmap='binary',interpolation='nearest')
ax.text(0.05,0.05,str(y_model[i]),
transform=ax.transAxes,
color='green' if (y_test[i] == y_model[i]) else 'red')
2.4、使用特征提取后的数据进行分类
from sklearn.decomposition import PCA
pca = PCA(n_components=40, whiten=True, random_state=0).fit(X_train)
X_train_pca = pca.transform(X_train)
X_test_pca = pca.transform(X_test)
# 用高斯朴素贝叶斯模型来拟合
from sklearn.naive_bayes import GaussianNB
model = GaussianNB()
model.fit(X_train_pca, y_train) # 构建模型
y_model = model.predict(X_test_pca) # 做出预测
# 准确率
# 训练集的准确率
print(model.score(X_train_pca, y_train)) # 0.9621380846325167
# 测试集的准确率
print(model.score(X_test_pca, y_test)) # 0.94
可以看到,准确率有了很大的提升。这说明主成分可能提供了一种更好的数据表示。
2.5、总结
GaussianNB
主要用于高维数据。朴素贝叶斯模型的训练和预测速度都很快。该模型对高维稀疏数据的效果很好,对参数的鲁棒性也相对较好。朴素贝叶斯模型是很好的基准模型,常用于非常大的数据集。
附:完整代码
# 加载并可视化手写数字
from sklearn.datasets import load_digits
import matplotlib.pyplot as plt
digits = load_digits()
# 特征矩阵
X = digits.data
# 目标矩阵
y = digits.target
# 将数据分成训练集和测试集
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
# 用高斯朴素贝叶斯模型来拟合
from sklearn.naive_bayes import GaussianNB
model = GaussianNB()
model.fit(X_train, y_train) # 构建模型
y_model = model.predict(X_test) # 做出预测
# 评估模型
# 准确率
# 训练集的准确率
print(model.score(X_train, y_train))
# 测试集的准确率
print(model.score(X_test, y_test))
# 混淆矩阵
import seaborn as sns
from sklearn.metrics import confusion_matrix
mat = confusion_matrix(y_test,y_model)
sns.heatmap(mat, square=True,annot=True, cbar=False)
plt.xlabel('predicted value')
plt.ylabel('true value')
plt.show()
# 更直观的方式
fig, axes = plt.subplots(10, 10, figsize=(8, 8),
subplot_kw={'xticks':[], 'yticks':[]},
gridspec_kw=dict(hspace=0.1, wspace=0.1))
test_images = X_test.reshape(-1, 8, 8)
for i, ax in enumerate(axes.flat):
ax.imshow(test_images[i], cmap='binary', interpolation='nearest')
ax.text(0.05,0.05,str(y_model[i]),
transform=ax.transAxes,
color='green' if (y_test[i] == y_model[i]) else 'red')
plt.show()
# 五轮交叉验证
from sklearn.model_selection import cross_val_score
print(cross_val_score(model, X, y, cv=5))
# 两轮交叉验证
from sklearn.model_selection import cross_val_score
print(cross_val_score(model, X, y, cv=2))
'--------------------------------------------------------------------'
# 使用特征提取后的数据
from sklearn.decomposition import PCA
pca = PCA(n_components=40, whiten=True, random_state=0).fit(X_train)
X_train_pca = pca.transform(X_train)
X_test_pca = pca.transform(X_test)
# print("X_train_pca.shape: {}".format(X_train_pca.shape))
# print("X_test_pca.shape: {}".format(X_test_pca.shape))
# 用高斯朴素贝叶斯模型来拟合
from sklearn.naive_bayes import GaussianNB
model = GaussianNB()
model.fit(X_train_pca, y_train) # 构建模型
y_model = model.predict(X_test_pca) # 做出预测
# 准确率
# 训练集的准确率
print(model.score(X_train_pca, y_train))
# 测试集的准确率
print(model.score(X_test_pca, y_test))
3、KNN 算法
3.1、加载数据集
数据集选择:
选择的是使用 scikit-learn
中自带的手写数字数据集。
# 加载并可视化手写数字
from sklearn.datasets import load_digits
digits = load_digits()
3.2、使用 KNN 算法分类
# 使用 KNN 构建模型
from sklearn.model_selection import train_test_split
X = digits.data
y = digits.target
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
from sklearn.neighbors import KNeighborsClassifier
clf = KNeighborsClassifier(n_neighbors=3)
clf.fit(X_train, y_train) # 拟合数据
3.3、评估模型
# 评估模型
print("Accuracy on training set: {:.3f}".format(clf.score(X_train, y_train)))
# Accuracy on training set: 0.992
print("Accuracy on test set: {:.3f}".format(clf.score(X_test, y_test)))
# Accuracy on test set: 0.987
我们可以调整 “邻居” 的参数,来观察模型的效果:
# 调整参数
clf = KNeighborsClassifier(n_neighbors=5)
clf.fit(X_train, y_train)
print("Accuracy on training set: {:.3f}".format(clf.score(X_train, y_train)))
# Accuracy on training set: 0.991
print("Accuracy on test set: {:.3f}".format(clf.score(X_test, y_test)))
# Accuracy on test set: 0.980
clf = KNeighborsClassifier(n_neighbors=1)
clf.fit(X_train, y_train)
print("Accuracy on training set: {:.3f}".format(clf.score(X_train, y_train)))
# Accuracy on training set: 1.000
print("Accuracy on test set: {:.3f}".format(clf.score(X_test, y_test)))
# Accuracy on test set: 0.991
而距离默认使用的是欧式距离。
3.4、总结
-
参数
-
邻居个数
使用较小的邻居个数(比如 3 个 或 5 个)往往能得到不错的性能。
-
数据点之间距离的度量方法
默认使用欧氏距离。
-
优点
- 容易理解,在使用复杂的技术之前,尝试此算法是一种很好地基准方法;
- 构建最近邻模型的速度很快,但如果训练集很大(特征数很多或者样本数很大),预测速度可能会很慢。
-
缺点
- 使用 k-NN 算法时,对数据的预处理是很重要的;
- 对于很多特征的数据集往往效果不好,对于大多数特征取值为 0 的数据集(所谓的稀疏矩阵)来说,这一算法的效果尤其不好。
附:完整代码
# 加载并可视化手写数字
from sklearn.datasets import load_digits
digits = load_digits()
# 使用 KNN 构建模型
from sklearn.model_selection import train_test_split
X = digits.data
y = digits.target
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
from sklearn.neighbors import KNeighborsClassifier
clf = KNeighborsClassifier(n_neighbors=3)
clf.fit(X_train, y_train)
# 评估模型
print("Accuracy on training set: {:.3f}".format(clf.score(X_train, y_train)))
print("Accuracy on test set: {:.3f}".format(clf.score(X_test, y_test)))
# 调整参数
clf = KNeighborsClassifier(n_neighbors=5)
clf.fit(X_train, y_train)
print("Accuracy on training set: {:.3f}".format(clf.score(X_train, y_train)))
print("Accuracy on test set: {:.3f}".format(clf.score(X_test, y_test)))
clf = KNeighborsClassifier(n_neighbors=1)
clf.fit(X_train, y_train)
print("Accuracy on training set: {:.3f}".format(clf.score(X_train, y_train)))
print("Accuracy on test set: {:.3f}".format(clf.score(X_test, y_test)))
'----------------------------------------------------------------'
from sklearn.decomposition import PCA
pca = PCA(n_components=40, whiten=True, random_state=0).fit(X_train)
X_train_pca = pca.transform(X_train)
X_test_pca = pca.transform(X_test)
# 使用 SVM
from sklearn.neighbors import KNeighborsClassifier
clf = KNeighborsClassifier(n_neighbors=3)
clf.fit(X_train_pca, y_train)
clf.predict(X_test_pca)
# 准确率
# 训练集的准确率
print(clf.score(X_train_pca, y_train))
# 测试集的准确率
print(clf.score(X_test_pca, y_test))
4、用随机森林识别手写数字
4.1、加载数据集
数据集选择:
选择的是使用 scikit-learn
中自带的手写数字数据集。
# 加载并可视化手写数字
from sklearn.datasets import load_digits
digits = load_digits()
4.2、用随机森林分类
# 用随机森林分类
from sklearn.model_selection import train_test_split
X = digits.data
y = digits.target
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
from sklearn.ensemble import RandomForestClassifier
model = RandomForestClassifier(n_estimators=1000)
model.fit(X_train, y_train) # 拟合数据,构建模型
ypred = model.predict(X_test) # 做出预测
4.3、评估模型
-
查看分类器的分类结果报告
# 分类结果报告
from sklearn import metrics
print(metrics.classification_report(ypred, y_test))
'''
precision recall f1-score support
0 1.00 0.97 0.99 38
1 1.00 0.98 0.99 44
2 0.95 1.00 0.98 42
3 0.98 0.96 0.97 46
4 0.97 1.00 0.99 37
5 0.98 0.98 0.98 48
6 1.00 1.00 1.00 52
7 1.00 0.96 0.98 50
8 0.94 0.98 0.96 46
9 0.98 0.98 0.98 47
accuracy 0.98 450
macro avg 0.98 0.98 0.98 450
weighted avg 0.98 0.98 0.98 450
'''
# 准确率
print(model.score(X_train, y_train)) # 1.0
print(model.score(X_test, y_test)) # 0.98
在训练集上的准确率为 1.0,可能存在过拟合。但在测试集上的表现也还可以。
-
混淆矩阵
# 混淆矩阵
import seaborn as sns
from sklearn.metrics import confusion_matrix
mat = confusion_matrix(y_test, ypred)
sns.heatmap(mat.T, square=True, annot=True, fmt='d', cbar=False)
plt.xlabel('true label')
plt.ylabel('predicted label')
4.4、总结
-
参数
需要调节的重要参数有 n_estimators
和 max_features
,可能还包括预剪枝选项(如 max_depth
)。
-
n_estimators
总是越大越好。对更多的树取平均可以降低过拟合,从而得到鲁棒性更好的集成。不过收益是递减的,而且树越多需要的内存也越多,训练时间也越长。常用的经验法则就是“在你的时间 / 内存允许的情况下尽量多”。
-
max_features
决定每棵树的随机性大小,较小的 max_features
可以降低过拟合。一般来说,好的经验就是使用默认值:对于分类,默认值是 max_features=sqrt(n_features)
;对于回归,默认值是 max_features=n_features
。增大 max_features
或 max_leaf_nodes
有时也可以提高性能。它还可以大大降低用于训练和预测的时间和空间要求。
-
优点
用于回归和分类的随机森林是目前应用最广泛的机器学习方法之一。这种方法非常强大,通常不需要反复调节参数就可以给出很好的结果,也不需要对数据进行缩放。
从本质上看,随机森林拥有决策树的所有优点,同时弥补了决策树的一些缺陷。
-
缺点
仍然使用决策树的一个原因是需要决策过程的紧凑表示。基本上不可能对几十棵甚至上百棵树做出详细解释,随机森林中树的深度往往比决策树还要大(因为用到了特征子集)。因此,如果你需要以可视化的方式向非专家总结预测过程,那么选择单棵决策树可能更好。
虽然在大型数据集上构建随机森林可能比较费时间,但在一台计算机的多个 CPU 内核上并行计算也很容易。如果你用的是多核处理器(几乎所有的现代化计算机都是),你可以用 n_jobs
参数来调节使用的内核个数。使用更多的 CPU 内核,可以让速度线性增加(使用 2 个内核,随机森林的训练速度会加倍),但设置 n_jobs
大于内核个数是没有用的。你可以设置 n_jobs=-1
来使用计算机的所有内核。
你应该记住,随机森林本质上是随机的,设置不同的随机状态(或者不设置 random_state
参数)可以彻底改变构建的模型。森林中的树越多,它对随机状态选择的鲁棒性就越好。如果你希望结果可以重现,固定 random_state
是很重要的。
对于维度非常高的稀疏数据(比如文本数据),随机森林的表现往往不是很好。对于这种数据,使用线性模型可能更合适。即使是非常大的数据集,随机森林的表现通常也很好,训练过程很容易并行在功能强大的计算机的多个 CPU 内核上。不过,随机森林需要更大的内存,训练和预测的速度也比线性模型要慢。对一个应用来说,如果时间和内存很重要的话,那么换用线性模型可能更为明智。
附:完整代码
# 加载并可视化手写数字
import matplotlib.pyplot as plt
from sklearn.datasets import load_digits
digits = load_digits()
# 用随机森林分类
from sklearn.model_selection import train_test_split
X = digits.data
y = digits.target
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
from sklearn.ensemble import RandomForestClassifier
model = RandomForestClassifier(n_estimators=1000)
model.fit(X_train, y_train)
ypred = model.predict(X_test)
# 分类结果报告
from sklearn import metrics
print(metrics.classification_report(ypred, y_test))
# 混淆矩阵
import seaborn as sns
from sklearn.metrics import confusion_matrix
mat = confusion_matrix(y_test, ypred)
sns.heatmap(mat.T, square=True, annot=True, fmt='d', cbar=False)
plt.xlabel('true label')
plt.ylabel('predicted label')
plt.show()
5、梯度提升回归树(梯度提升机)
5.1、加载数据集
数据集选择:
选择的是使用 scikit-learn
中自带的手写数字数据集。
# 加载并可视化手写数字
from sklearn.datasets import load_digits
digits = load_digits()
5.2、用梯度提升回归树(梯度提升机)进行分类
# 用梯度提升回归树(梯度提升机)分类
from sklearn.model_selection import train_test_split
X = digits.data
y = digits.target
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
from sklearn.ensemble import GradientBoostingClassifier
gbrt = GradientBoostingClassifier(random_state=0)
gbrt.fit(X_train, y_train) # 拟合数据
5.3、评估模型
# 评估模型
print("Accuracy on training set: {:.3f}".format(gbrt.score(X_train, y_train)))
# Accuracy on training set: 1.000
print("Accuracy on test set: {:.3f}".format(gbrt.score(X_test, y_test)))
# Accuracy on test set: 0.956
由于训练精度达到 100%,所以很可能存在过拟合。为了降低过拟合,我们可以限制最大深度来加强预剪枝,也可以降低学习率:
# 限制最大深度来加强预剪枝
gbrt = GradientBoostingClassifier(random_state=0, max_depth=1)
gbrt.fit(X_train, y_train)
print("Accuracy on training set: {:.3f}".format(gbrt.score(X_train, y_train)))
# Accuracy on training set: 0.975
print("Accuracy on test set: {:.3f}".format(gbrt.score(X_test, y_test)))
# Accuracy on test set: 0.913
# 降低学习率
gbrt = GradientBoostingClassifier(random_state=0, learning_rate=0.01)
gbrt.fit(X_train, y_train)
print("Accuracy on training set: {:.3f}".format(gbrt.score(X_train, y_train)))
# Accuracy on training set: 0.965
print("Accuracy on test set: {:.3f}".format(gbrt.score(X_test, y_test)))
# Accuracy on test set: 0.891
5.4、总结
-
参数
梯度提升树模型的主要参数包括树的数量 n_estimators
和学习率 learning_rate
。learning_rate
用于控制每棵树对前一棵树的错误的纠正强度。这两个参数高度相关,因为 learning_rate
越低,就需要更多的树来构建具有相似复杂度的模型。随机森林的 n_estimators
值总是越大越好,但梯度提升不同,增大 n_estimators
会导致模型更加复杂,进而可能导致过拟合。通常的做法是根据时间和内存的预算选择合适的 n_estimators
,然后对不同的 learning_rate
进行遍历。
另一个重要参数是 max_depth
(或 max_leaf_nodes
),用于降低每棵树的复杂度。梯度提升模型的 max_depth
通常都设置得很小,一般不超过 5。
-
优点
梯度提成回归树是另一种集成方法,通过合并对个决策树来构建一个更为强大的模型。与随机森林方法不同,梯度提升采用连续的方式构造树,每棵树都试图纠正前一棵树的错误。默认情况下,梯度提升回归树没有随机化,而是采用了强预剪枝。梯度提升树通常使用深度很小(1 到 5 之间)的树,这样模型占用的内存更少,预测速度也更快。
梯度提升背后的主要思想是合并许多简单的模型(在这个语境中叫做弱学习器),比如深度很小的树。每棵树只能对部分数据做出好的预测,因此,添加的树越来越多,可以不断迭代提高性能。
-
缺点
其主要缺点是需要仔细调参,而且训练时间可能会比较长。与其他基于树的模型类似,这一算法不需要对数据进行缩放就可以表现得很好,而且也适用于二元特征与连续特征同时存在的数据集。与其他基于树的模型相同,它也通常不适用于高维稀疏数据。
附:完整代码
# 加载并可视化手写数字
from sklearn.datasets import load_digits
digits = load_digits()
# 用梯度提升回归树(梯度提升机)分类
from sklearn.model_selection import train_test_split
X = digits.data
y = digits.target
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
from sklearn.ensemble import GradientBoostingClassifier
gbrt = GradientBoostingClassifier(random_state=0)
gbrt.fit(X_train, y_train)
# 评估模型
print("Accuracy on training set: {:.3f}".format(gbrt.score(X_train, y_train)))
print("Accuracy on test set: {:.3f}".format(gbrt.score(X_test, y_test)))
# 限制最大深度来加强预剪枝
gbrt = GradientBoostingClassifier(random_state=0, max_depth=1)
gbrt.fit(X_train, y_train)
print("Accuracy on training set: {:.3f}".format(gbrt.score(X_train, y_train)))
print("Accuracy on test set: {:.3f}".format(gbrt.score(X_test, y_test)))
# 降低学习率
gbrt = GradientBoostingClassifier(random_state=0, learning_rate=0.1, n_estimators=5)
gbrt.fit(X_train, y_train)
print("Accuracy on training set: {:.3f}".format(gbrt.score(X_train, y_train)))
print("Accuracy on test set: {:.3f}".format(gbrt.score(X_test, y_test)))
6、SVM
6.1、加载数据集
数据集选择:
选择的是使用 scikit-learn
中自带的手写数字数据集。
# 加载并可视化手写数字
from sklearn.datasets import load_digits
digits = load_digits()
6.2、使用 SVM 进行分类
# 使用 SVM
from sklearn.model_selection import train_test_split
X = digits.data
y = digits.target
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
from sklearn.svm import SVC
svm = SVC(kernel='rbf', C=10, gamma=0.1) # 使用 “高斯核”,gamma 系数取 0.1
svm.fit(X_train, y_train)
6.3、评估数据
print("Accuracy on training set: {:.3f}".format(svm.score(X_train, y_train)))
# Accuracy on training set: 1.000
print("Accuracy on test set: {:.3f}".format(svm.score(X_test, y_test)))
# Accuracy on test set: 0.084
训练集准确率为 1.000,而测试集准确率很小,说明模型太过拟合。我们需要调整参数。
svm = SVC(kernel='rbf', C=100, gamma=0.001)
svm.fit(X_train, y_train)
print("Accuracy on training set: {:.3f}".format(svm.score(X_train, y_train)))
# Accuracy on training set: 1.000
print("Accuracy on test set: {:.3f}".format(svm.score(X_test, y_test)))
# Accuracy on test set: 0.993
虽然可能会存在一些过拟合,但在测试集上表现的也不错。
6.4、使用特征提取过的数据
from sklearn.decomposition import PCA
pca = PCA(n_components=40, whiten=True, random_state=0).fit(X_train)
X_train_pca = pca.transform(X_train)
X_test_pca = pca.transform(X_test)
# 使用 SVM
from sklearn.svm import SVC
svm = SVC(kernel='rbf', C=10, gamma=0.1)
svm.fit(X_train_pca, y_train)
# 准确率
# 训练集的准确率
print(svm.score(X_train_pca, y_train)) # # 1.0
# 测试集的准确率
print(svm.score(X_test_pca, y_test)) # 0.9222222222222223
相比如之前系数为 “kernel='rbf', C=10, gamma=0.1
” 时的准确率提高了更多。SVM 对数据的缩放十分敏感,这里我们使用白化(系数 “whiten=True
”)也起到了缩放的作用。
6.4、总结
-
参数
核 SVM 的重要参数是正则化参数 C
、核的选择以及与核相关的参数。虽然我们主要讲的是 RBF 核,但 scikit-learn
中还有其他选择。RBF 核只有一个参数 gamma
,它是高斯核宽度的倒数。gamma
和 C
控制的都是模型复杂度,较大的值都对应更为复杂的模型。因此,这两个参数的设定通常是强烈相关的,应该同时调节。
-
优点
核支持向量机是非常强大的模型,在各种数据集上的表现都很好。SVM 允许决策边界很复杂,即使数据只有几个特征。它在低维数据和高维数据(即很少特征和很多特征)上的表现都很好。
-
缺点
对样本个数的缩放表现不好。在有多达 10 000 个样本的数据上运行 SVM 可能表现良好,但如果数据量达到 100 000 甚至更大,在运行时间和内存使用方面可能会面临挑战。
SVM 的另一个缺点是,预处理数据和调参都需要非常小心。这也是为什么如今很多应用中用的都是基于树的模型,比如随机森林或梯度提升(需要很少的预处理,甚至不需要预处理)。此外,SVM 模型很难检查,可能很难理解为什么会这么预测,而且也难以将模型向非专家进行解释。
附:完整代码
# 加载并可视化手写数字
from sklearn.datasets import load_digits
digits = load_digits()
# 使用 SVM
from sklearn.model_selection import train_test_split
X = digits.data
y = digits.target
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
from sklearn.svm import SVC
svm = SVC(kernel='rbf', C=10, gamma=0.1)
svm.fit(X_train, y_train)
print("Accuracy on training set: {:.3f}".format(svm.score(X_train, y_train)))
print("Accuracy on test set: {:.3f}".format(svm.score(X_test, y_test)))
svm = SVC(kernel='rbf', C=100, gamma=0.001)
svm.fit(X_train, y_train)
print("Accuracy on training set: {:.3f}".format(svm.score(X_train, y_train)))
print("Accuracy on test set: {:.3f}".format(svm.score(X_test, y_test)))
'--------------------------------------------------------------------------'
# 使用特征提取后的数据
from sklearn.decomposition import PCA
pca = PCA(n_components=40, whiten=True, random_state=0).fit(X_train)
X_train_pca = pca.transform(X_train)
X_test_pca = pca.transform(X_test)
# 使用 SVM
from sklearn.svm import SVC
svm = SVC(kernel='rbf', C=10, gamma=0.1)
svm.fit(X_train_pca, y_train)
# 准确率
# 训练集的准确率
print(svm.score(X_train_pca, y_train))
# 测试集的准确率
print(svm.score(X_test_pca, y_test))
7、神经网络
7.1、加载数据集
数据集选择:
选择使用 minst
数据集中的手写数字数据集。
导入相应的库:
from keras.utils import to_categorical
from keras import models, layers, regularizers
from keras.optimizers import RMSprop
from keras.datasets import mnist
import matplotlib.pyplot as plt
# 加载数据集
(train_images, train_labels), (test_images, test_labels) = mnist.load_data()
print(train_images.shape, test_images.shape)
# (60000, 28, 28) (10000, 28, 28)
将图片由二维铺开成一维:
# 将图片由二维铺开成一维
train_images = train_images.reshape((60000, 28*28)).astype('float')
test_images = test_images.reshape((10000, 28*28)).astype('float')
train_labels = to_categorical(train_labels)
test_labels = to_categorical(test_labels)
7.2、搭建一个神经网络(示例)
# 搭建神经网络
network = models.Sequential()
network.add(layers.Dense(units=15, activation='relu', input_shape=(28*28, ),))
network.add(layers.Dense(units=10, activation='softmax'))
7.3、神经网络训练
7.3.1、编译确定优化器和损失函数
# 编译步骤
network.compile(optimizer=RMSprop(lr=0.001), loss='categorical_crossentropy', metrics=['accuracy'])
7.3.2、训练网络:确定训练的数据、训练的轮数和每次训练的样本数等
# 训练网络,用fit函数, epochs表示训练多少个回合, batch_size表示每次训练给多大的数据
network.fit(train_images, train_labels, epochs=20, batch_size=128, verbose=2)
7.4、用训练好的模型进行预测,并在测试集上做出评价
# 来在测试集上测试一下模型的性能吧
y_pre = network.predict(test_images[:5])
print(y_pre, test_labels[:5])
test_loss, test_accuracy = network.evaluate(test_images, test_labels)
print("test_loss:", test_loss, " test_accuracy:", test_accuracy)
7.5、完整的代码
-
使用全连接神经网络
from keras.utils import to_categorical
from keras import models, layers, regularizers
from keras.optimizers import RMSprop
from keras.datasets import mnist
import matplotlib.pyplot as plt
# 加载数据集
(train_images, train_labels), (test_images, test_labels) = mnist.load_data()
train_images = train_images.reshape((60000, 28*28)).astype('float')
test_images = test_images.reshape((10000, 28*28)).astype('float')
train_labels = to_categorical(train_labels)
test_labels = to_categorical(test_labels)
network = models.Sequential()
network.add(layers.Dense(units=128, activation='relu', input_shape=(28*28, ),
kernel_regularizer=regularizers.l1(0.0001)))
network.add(layers.Dropout(0.01))
network.add(layers.Dense(units=32, activation='relu',
kernel_regularizer=regularizers.l1(0.0001)))
network.add(layers.Dropout(0.01))
network.add(layers.Dense(units=10, activation='softmax'))
# 编译步骤
network.compile(optimizer=RMSprop(lr=0.001), loss='categorical_crossentropy', metrics=['accuracy'])
# 训练网络,用fit函数, epochs表示训练多少个回合, batch_size表示每次训练给多大的数据
network.fit(train_images, train_labels, epochs=20, batch_size=128, verbose=2)
# 来在测试集上测试一下模型的性能吧
test_loss, test_accuracy = network.evaluate(test_images, test_labels)
print("test_loss:", test_loss, " test_accuracy:", test_accuracy)
Epoch 1/20
469/469 - 1s - loss: 2.6369 - accuracy: 0.7405
Epoch 2/20
469/469 - 0s - loss: 0.7591 - accuracy: 0.8705
Epoch 3/20
469/469 - 0s - loss: 0.6083 - accuracy: 0.9010
Epoch 4/20
469/469 - 0s - loss: 0.5309 - accuracy: 0.9165
Epoch 5/20
469/469 - 0s - loss: 0.4678 - accuracy: 0.9299
Epoch 6/20
469/469 - 0s - loss: 0.4241 - accuracy: 0.9391
Epoch 7/20
469/469 - 0s - loss: 0.3930 - accuracy: 0.9429
Epoch 8/20
469/469 - 0s - loss: 0.3764 - accuracy: 0.9462
Epoch 9/20
469/469 - 0s - loss: 0.3455 - accuracy: 0.9530
Epoch 10/20
469/469 - 0s - loss: 0.3166 - accuracy: 0.9566
Epoch 11/20
469/469 - 0s - loss: 0.3156 - accuracy: 0.9560
Epoch 12/20
469/469 - 0s - loss: 0.2935 - accuracy: 0.9600
Epoch 13/20
469/469 - 0s - loss: 0.2822 - accuracy: 0.9618
Epoch 14/20
469/469 - 0s - loss: 0.2698 - accuracy: 0.9623
Epoch 15/20
469/469 - 0s - loss: 0.2666 - accuracy: 0.9636
Epoch 16/20
469/469 - 0s - loss: 0.2548 - accuracy: 0.9657
Epoch 17/20
469/469 - 0s - loss: 0.2482 - accuracy: 0.9661
Epoch 18/20
469/469 - 0s - loss: 0.2415 - accuracy: 0.9663
Epoch 19/20
469/469 - 0s - loss: 0.2359 - accuracy: 0.9687
Epoch 20/20
469/469 - 0s - loss: 0.2315 - accuracy: 0.9680
313/313 [==============================] - 0s 389us/step - loss: 0.2709 - accuracy: 0.9667
test_loss: 0.27094602584838867 test_accuracy: 0.96670001745224
-
使用卷积神经网络
from keras.utils import to_categorical
from keras import models, layers
from keras.optimizers import RMSprop
from keras.datasets import mnist
# 加载数据集
(train_images, train_labels), (test_images, test_labels) = mnist.load_data()
# 搭建LeNet网络
def LeNet():
network = models.Sequential()
network.add(layers.Conv2D(filters=6, kernel_size=(3, 3), activation='relu', input_shape=(28, 28, 1)))
network.add(layers.AveragePooling2D((2, 2)))
network.add(layers.Conv2D(filters=16, kernel_size=(3, 3), activation='relu'))
network.add(layers.AveragePooling2D((2, 2)))
network.add(layers.Conv2D(filters=120, kernel_size=(3, 3), activation='relu'))
network.add(layers.Flatten())
network.add(layers.Dense(84, activation='relu'))
network.add(layers.Dense(10, activation='softmax'))
return network
network = LeNet()
network.compile(optimizer=RMSprop(lr=0.001), loss='categorical_crossentropy', metrics=['accuracy'])
train_images = train_images.reshape((60000, 28, 28, 1)).astype('float') / 255
test_images = test_images.reshape((10000, 28, 28, 1)).astype('float') / 255
train_labels = to_categorical(train_labels)
test_labels = to_categorical(test_labels)
# 训练网络,用fit函数, epochs表示训练多少个回合, batch_size表示每次训练给多大的数据
network.fit(train_images, train_labels, epochs=10, batch_size=128, verbose=2)
test_loss, test_accuracy = network.evaluate(test_images, test_labels)
print("test_loss:", test_loss, " test_accuracy:", test_accuracy)
Epoch 1/10
469/469 - 4s - loss: 0.3614 - accuracy: 0.8931
Epoch 2/10
469/469 - 4s - loss: 0.0982 - accuracy: 0.9702
Epoch 3/10
469/469 - 4s - loss: 0.0617 - accuracy: 0.9811
Epoch 4/10
469/469 - 4s - loss: 0.0478 - accuracy: 0.9851
Epoch 5/10
469/469 - 4s - loss: 0.0380 - accuracy: 0.9883
Epoch 6/10
469/469 - 4s - loss: 0.0313 - accuracy: 0.9900
Epoch 7/10
469/469 - 4s - loss: 0.0267 - accuracy: 0.9916
Epoch 8/10
469/469 - 4s - loss: 0.0233 - accuracy: 0.9926
Epoch 9/10
469/469 - 4s - loss: 0.0196 - accuracy: 0.9938
Epoch 10/10
469/469 - 4s - loss: 0.0173 - accuracy: 0.9946
313/313 [==============================] - 0s 1ms/step - loss: 0.0325 - accuracy: 0.9897
test_loss: 0.03245333582162857 test_accuracy: 0.9897000193595886
7.6、总结
-
参数
估计神经网络的复杂度。最重要的参数是层数和每层的隐单元个数。你应该首先设置 1 个或 2 个隐层,然后可以逐步增加。每个隐层的结点个数通常与输入特征个数接近,但在几千个结点时很少会多于特征个数。
在考虑神经网络的模型复杂度时,一个有用的度量是学到的权重(或系数)的个数。
神经网络调参的常用方法是,首先创建一个大到足以过拟合的网络,确保这个网络可以对任务进行学习。知道训练数据可以被学习之后,要么缩小网络,要么增大 alpha
来增强正则化,这可以提高泛化性能。
在我们的实验中,主要关注模型的定义:层数、每层的结点个数、正则化和非线性。这些内容定义了我们想要学习的模型。还有一个问题是,如何学习模型或用来学习参数的算法,这一点由 solver
参数设定。solver
有两个好用的选项。默认选项是 'adam'
,在大多数情况下效果都很好,但对数据的缩放相当敏感(因此,始终将数据缩放为均值为 0、方差为 1 是很重要的)。另一个选项是 'lbfgs'
,其鲁棒性相当好,但在大型模型或大型数据集上的时间会比较长。还有更高级的 'sgd'
选项,许多深度学习研究人员都会用到。'sgd'
选项还有许多其他参数需要调节,以便获得最佳结果。你可以在用户指南中找到所有这些参数及其定义。当你开始使用 MLP 时,我们建议使用 'adam'
和 'lbfgs'
。
-
优点
它的主要优点之一是能够获取大量数据中包含的信息,并构建无比复杂的模型。给定足够的计算时间和数据,并且仔细调节参数,神经网络通常可以打败其他机器学习算法(无论是分类任务还是回归任务)。
-
缺点
神经网络——特别是功能强大的大型神经网络——通常需要很长的训练时间。它还需要仔细地预处理数据,正如我们这里所看到的。与 SVM 类似,神经网络在“均匀”数据上的性能最好,其中“均匀”是指所有特征都具有相似的含义。如果数据包含不同种类的特征,那么基于树的模型可能表现得更好。神经网络调参本身也是一门艺术。
8、对比
|
高斯朴素贝叶斯模型 |
KNN |
随机森林 |
梯度提升回归树 |
SVM |
神经网络 |
原数据测试集准确率 |
0.8333333333333334 |
0.991 |
0.98 |
0.956 |
0.084 |
0.9897000193595886 |
特征提取后的数据测试集准确率 |
0.94 |
|
|
|
0.9222222222222223 |
|
注:表中的准确率都是在默认参数下。