10scikit-learn 机器学习基础入门

机器学习和 scikit-learn 介绍

监督学习介绍

机器学习中,我们通常会接触到:监督学习、非监督学习、半监督学习,强化学习等不同的应用类型。其中,监督学习(英语:Supervised learning)是最为常见,且应用最为广泛的分支之一。
监督学习的目标是从已知训练数据中学习一个预测模型,使得这个模型对于其他输入数据产生一个预测输出。其中,监督学习的「监督」是相对与「非监督」的一种表达,二者的区别在于,监督学习的训练数据经过了人工进行标注,而非监督学习则没有这个过程。

image.png

如同上面的两个简单的数据集。左边的数据集明显没有经过标注。而右边数据集则进行了颜色标注,也就是人为给数据样本打上了橙色、绿色和蓝色的标签。
监督学习的类型
监督学习中,所面对的问题大致分为两类:分类和回归。
其中,分类问题可以简单概括为:已有了一些数据样本及明确的样本分类。现在从这些样本的特征中总结规律,再用于判断新输入样本到底属于哪一类别。例如下图展示了一个分类过程,使用监督学习算法对水果进行类别区分。
image.png

面对一个新问题,判断它是属于分类还是回归,就需要根据这个问题具备的特征来判断。
其中,回归问题与分类问题的最大区别(特征)在于,输出变量的类型不同。详细说来:
分类问题,输出为有限个离散变量,布尔值或者定类变量。
回归问题,输出为连续变量,一般为实数,也就是一个确切值。

非监督学习介绍

在监督学习的过程中,我们需要对训练数据打上标签,这是必不可少的一步。而非监督学习面对的数据是没有标签的。
比如我们现在有一堆动物的照片。在监督学习中,我们需要提前对每张照片代表的动物进行标记。这一张是狗,那一张是猫,然后再进行训练。最后,模型对于新输入的照片,就能分清楚动物的类别。
当进行非监督学习时,照片并未进行标注。我们需要将所有的训练样本照片「喂」给算法即可。注意,这个时候和监督学习有一些不同,非监督学习只能识别出训练样本里包含了几种类别的动物,而并不能直接告诉你这只是猫,那一只是狗。但是,这里的类别数量一般都不会太大,你可以手动对类别进行标记,再将数据用于其他用途。
非监督学习识别出样本包含几种类别,就是我们通常所说的「聚类」。
实际上非监督学习还包括主成分分析等更多的应用方面。机器学习中,当我们使用到的数据没有特定标签时,基本都可以被归为非监督学习问题。

scikit-learn 介绍

机器学习常用的方法有很多,例如:线性回归、支持向量机、k 近邻、决策树、朴素贝叶斯、逻辑回归等。scikit-learn 还提供了围绕机器学习核心算法的一套工具,包括数据预处理,模型评估,超参数优化等。

线性回归和感知机分类

线性回归模型

线性模型有最小二乘回归、感知机、逻辑回归、岭回归,贝叶斯回归等,由 sklearn.linear_model 模块导入。对于线性模型而言,即通过拟合线性函数(下图)去完成样本分类或回归预测。


image.png

其中,最小二乘回归、岭回归、贝叶斯回归等是用于解决回归问题。而感知机、逻辑回归被用于解决分类问题。
最小二乘法是线性回归中最经典的方法之一,最小二乘的取名即来自于其选择了平方损失函数。在 scikit-learn 中,最小二乘法的实现方法如下:


image.png

使用 scikit-learn 去解决一个机器学习相关的问题时,我们的代码都大同小异,主要是由几个部分组成:
调用一个机器学习方法构建相应的模型 model,并设置模型参数。

使用该机器学习模型提供的 model.fit() 方法训练模型。
使用该机器学习模型提供的 model.predict() 方法用于预测。

通过最小二乘回归去拟合二维平面上的一些点。首先,执行第一步,载入方法并构建模型。

import warnings
from sklearn.linear_model import LinearRegression

# 忽略代码警告,仅供教学方便,自行书写代码时警告也很重要,不建议忽略
warnings.filterwarnings('ignore')

model = LinearRegression()  # 调用最小二乘法线性回归(第 1 步)
model

model 模型输出的参数,即为相应算法类的默认参数。

LinearRegression(copy_X=True, fit_intercept=True, n_jobs=1, normalize=False)

使用模型带有的 fit() 方法去拟合 3 个点。三个点的特征向量分别为[0,0], [1,1],[2,2],对应的目标值为[1,2,3]。

model.fit([[0, 0], [1, 1], [2, 2]], [1, 2, 3])  # 模型训练(第 2 步)

训练时,选择的[0,0],[1,1],[2,2] 这三个点恰好在一条直线上,再结合目标值想象一下它们的空间位置关系。我们可以使用下面的方法,输出拟合直线 ww 项和常数项值。

model.coef_, model.intercept_

当我们输入新的数值,例如[3,3] 时,根据上面的函数,因变量的值为 44。那么,我们使用模型来预测,看一看结果是否为 4。

model.predict([[3, 3]])  # 模型预测(第 3 步)

下面我们导入 scikit-learn 内置的 diabetes 糖尿病数据集来训练一个复杂一点的最小二乘回归模型。
第一步:导入数据,并将其划分为 70% 的训练集和 30% 的测试集。机器学习中,我们习惯采用这样的比例来划分训练集和测试集。其中训练集用来训练模型,而测试集则用来评估模型的质量。测试集的数据不会出现在训练数据中,这也就类似我们使用了新的数据对训练好的模型进行预测和评估,以保证模型质量真实可靠。


image.png
from sklearn import datasets  # 导入内置数据集模块
from sklearn.model_selection import train_test_split  # 导入数据集切分模块
import numpy as np  # 导入数值计算模块

diabetes = datasets.load_diabetes()  # 载入糖尿病数据集

diabetes_feature = diabetes.data[:, np.newaxis, 2]  # 该数据集的特征较多,这里只选取其中一个
diabetes_target = diabetes.target  # 设定目标值

# 切分数据集为 70% 的训练集和 30% 的预测集
# random_state 随机数种子用于保证每次执行结果一致
train_feature, test_feature, train_target, test_target = train_test_split(
    diabetes_feature, diabetes_target, test_size=0.3, random_state=56)

第二步:载入最小二乘回归模型,并训练数据。

model = LinearRegression()  # 构建最小二乘线性回归模型
model.fit(train_feature, train_target)  # 使用训练集数据训练模型

第三步:使用测试集进行预测,并将结果绘图。

import matplotlib.pyplot as plt  # 导入 matplotlib 绘图模块
%matplotlib inline

# 绘图
plt.scatter(train_feature, train_target,  color='black')  # 绘制训练集散点图
plt.scatter(test_feature, test_target,  color='red')  # 绘制测试集散点图
plt.plot(test_feature, model.predict(test_feature),
         color='blue', linewidth=3)  # 绘制拟合直线

# 绘制图例
plt.legend(('Fit line', 'Train Set', 'Test Set'), loc='lower right')
plt.title('LinearRegression Example')

最后,我们可以通过绘制的图像,更加直观地看出采用最小二乘回归模型进行线性拟合的结果。


image.png

对于其他常见的线性回归模型,它们和最小二乘线性回归模型非常相似,只是采用了不同的损失函数。
例如,岭回归采用了带 L2 惩罚项的平方和损失函数。
而另一种常见的 Lasso 回归,同样采用了带 L1 惩罚项的平方损失函数。
一些常见的广义线性回归模型,及它们在 scikit-learn 中对应的方法。


image.png

这些方法相对于普通最小二乘回归模型而言,均增加了一些惩罚项。这样会提高模型的泛化能力,在实际应用中效果可能会好一些。

线性分类模型

感知机 是一个经典的二分类方法,它是神经网络和支持向量机的基础。感知机模型非常简单,输入为一些特征向量,输出则由正类和负类组成。而输入和输出之间,则是由符号函数连接。
感知机的损失函数是错误分类点到分离超平面之间的距离总和,其学习策略同样也是损失函数最小化。

image.png

实现感知机通过调用 sklearn.linear_model.Perceptron()方法完成。
首先,使用 scikit-learn 提供的 make_classification 方法生成一组可被二分类的二维数组作为数据集。

from sklearn.datasets import make_classification  # 导入分类数据生成模块

# 随机生成一组可以被二分类的数据
X, y = make_classification(n_features=2, n_redundant=0,
                           n_informative=1, n_clusters_per_class=1, random_state=1)

X.shape, y.shape  # 查看数组形状

我们可以使用 Matplotlib 将该数据集绘制出来。

plt.scatter(X[:, 0], X[:, 1], marker='o', c=y)  # 绘制数据集散点图

其中,y 即相当于人为给数据添加的标签。
数据集分为 2 种颜色的样本点,并呈现出明显的线性界线。接下来,我们使用感知机对该数据集进行分类训练。

from sklearn.linear_model import Perceptron

# 将数据集划分为 70% 训练集和 30% 测试集
train_feature, test_feature, train_target, test_target = train_test_split(
    X, y, test_size=0.3, random_state=56)
# 建立感知机模型,使用默认参数
model = Perceptron()
# 使用训练集训练模型
model.fit(train_feature, train_target)

训练结束后,我们用测试数据进行预测。请注意,由于测试数据与训练数据完全不同,也是算法之前完全没有见过的数据。我们后续可以通过模型对测试数据的预测结果,然后与真实的结果进行比较,从而得到模型的分类准确度。

preds = model.predict(test_feature)  # 使用测试集预测

准确度表示正确预测的样本占全部样本的比例,是用于评估分类模型的常用指标之一,我们得到的 preds 是模型的预测结果,而真实结果为 test_target。接下来,可以通过 scikit-learn 提供的 accuracy_score 计算出分类准确度。

from sklearn.metrics import accuracy_score

accuracy_score(test_target, preds)  # 先传入真实值,再传入预测值

返回的结果即是测试集预测分类准确度,如果为 1.0 则表示预测全部正确,分类准确度为 100%。
我们使用 Matplotlib 将训练数据和测试数据绘制在原图上,并绘制出感知机分类时的决策边界。

# 创建一个绘图矩阵方便显示决策边界线
x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.02),
                     np.arange(y_min, y_max, 0.02))

fig, ax = plt.subplots()
# 绘制决策边界
Z = model.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
ax.contourf(xx, yy, Z, cmap=plt.cm.Paired)
# 绘制训练和测试数据
ax.scatter(train_feature[:, 0], train_feature[:, 1])
ax.scatter(test_feature[:, 0], test_feature[:, 1])

支持向量机分类预测

线性支持向量机

感知机的学习过程由误分类驱动,即当感知机寻找到没有实例被错误分类时,就确定了分割超平面。这样虽然可以解决一些二分类问题,但是训练出来的模型往往容易出现过拟合。


image.png

如上图所示,当感知机在进行分类时,为了照顾左下角的两个红色标记样本,分割线会呈现出如图所示的走向。你应该通过观察就能发现,这条分割线不是特别合理。
支持向量机也被看成是感知机的延伸。简单来讲,支持向量机就是通过找出一个最大间隔超平面来完成分类。


image.png

如图所示,中间的实线是我们找到的分割超平面。这个超平面并不是随手一画,它必须满足两个类别中距离直线最近的样本点,与实线的距离一样且最大。这里的最大,也就是上面提到的最大间隔超平面。
支持向量机中的「支持向量」指的是上图中,距离分割超平面最近的样本点,即两条虚线上的一个实心点和两个空心点。

首先,我们需要先导入数据文件

import pandas as pd  # 导入 pandas 模块
import warnings
warnings.filterwarnings('ignore')

# 读取 csv 数据文件
df = pd.read_csv(
    "https://labfile.oss.aliyuncs.com/courses/866/data.csv", header=0)
df.head()

可以直接通过 df.head() 语句查看一下这个数据集头部,对里面的数据组成初步熟悉一下。
可以看到,有两个类别。其中 0 即表示上面图中的蓝色样本点,1 对应着红色样本点。
将整个数据集划分为训练集和测试集两部分,其中训练集占 70%。

from sklearn.model_selection import train_test_split  # 导入数据集划分模块

# 读取特征值及目标值
feature = df[["x", "y"]]
target = df["class"]

# 对数据集进行分割
train_feature, test_feature, train_target, test_target = train_test_split(
    feature, target, test_size=0.3)

导入线性支持向量机分类器

from sklearn.svm import LinearSVC  # 导入线性支持向量机分类器

# 构建线性支持向量机分类模型
model_svc = LinearSVC()
model_svc.fit(train_feature, train_target)

对模型在测试集上的准确度进行评估,之前使用了 accuracy_score ,这里使用模型带有的 score 方法效果是一样的

# 支持向量机分类准确度
model_svc.score(test_feature, test_target)

绘制出分类器的决策边界

import numpy as np
from matplotlib import pyplot as plt
%matplotlib inline

# 创建一个绘图矩阵方便显示决策边界线
X = feature.values
x_min, x_max = X[:, 0].min() - 0.1, X[:, 0].max() + 0.1
y_min, y_max = X[:, 1].min() - 0.1, X[:, 1].max() + 0.1
xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.02),
                     np.arange(y_min, y_max, 0.02))

fig, ax = plt.subplots()
# 绘制决策边界
Z = model_svc.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
ax.contourf(xx, yy, Z, cmap=plt.cm.Paired)
# 绘制训练和测试数据
ax.scatter(train_feature.values[:, 0], train_feature.values[:, 1], c=train_target)
ax.scatter(test_feature.values[:, 0], test_feature.values[:, 1], c=test_target)

非线性支持向量机

在实际生活中,我们大部分情况面对的却是非线性分类问题,因为实际数据往往都不会让你通过一个水平超平面就能完美分类。


image.png

上图展现的就是一个非线性分类问题,而支持向量机就是解决非线性分类的有力武器。
支持向量机引入了核函数来解决非线性分类的问题。简单来讲,通过核函数,我们可以将特征向量映射到高维空间中,然后再高维空间中找到最大间隔分割超平面完成分类。而映射到高维空间这一步骤也相当于将非线性分类问题转化为线性分类问题。


image.png

第一张图中,红蓝球无法进行线性分类。
使用核函数将特征映射到高维空间,类似于在桌子上拍一巴掌使小球都飞起来了。

在高维空间完成线性分类后,再将超平面重新投影到原空间。

在将特征映射到高维空间的过程中,我们常常会用到多种核函数,包括:线性核函数、多项式核函数、高斯径向基核函数等。其中,最常用的就算是高斯径向基核函数了,也简称为 RBF 核。
我们选择了 digits 手写数字数据集。digits 数据集无需通过外部下载,可以直接由 scikit-learn 提供的 datasets.load_digits() 方法导入。该数据集的详细信息如下:


image.png

第一步,导入数据并进行初步观察。

from sklearn import datasets  # 导入数据集模块

# 载入数据集
digits = datasets.load_digits()

# 绘制数据集前 5 个手写数字的灰度图
for index, image in enumerate(digits.images[:5]):
    plt.subplot(2, 5, index+1)
    plt.imshow(image, cmap=plt.cm.gray_r, interpolation='nearest')

可以用 digits.target[:5] 查看前五张手写数字对应的实际标签

digits.target[:5]

通常,我们在处理图像问题时,都是将图像的每一个像素转换为灰度值或按比例缩放的灰度值。有了数值,就可以构建和图像像素大小相同的矩阵了。在这里,digits 已经预置了每一张图像对应的矩阵,并包含在 digits.images 方法中。
我们可以通过 digits.images[1] 输出第 1 张手写数字对应的 8x8 矩阵。很方便地,scikit-learn 已经将 8x8 矩阵转换成了方便作为特征变量输入 64x1 的矩阵,并放在了 digits.data 中。你可以使用 digits.data[1]查看。

digits.data[1]

划分训练集和测试集,然后针对测试集进行预测并评估预测精准度

from sklearn.svm import SVC  # 导入非线性支持向量机分类器
from sklearn.metrics import accuracy_score  # 导入评估模块

feature = digits.data  # 指定特征
target = digits.target  # 指定目标值

# 划分数据集,将其中 70% 划为训练集,另 30% 作为测试集
train_feature, test_feature, train_target, test_target = train_test_split(
    feature, target, test_size=0.33)

model = SVC()  # 建立模型
model.fit(train_feature, train_target)  # 模型训练
results = model.predict(test_feature)  # 模型预测

scores = accuracy_score(test_target, results)  # 评估预测精准度
scores

最后,模型预测准确度为 44.6%。由于每一次运行时,数据集都会被重新划分,所以你训练的准确度甚至会低于 44.6%。
我们在建立模型的时候使用的是默认参数
该模型的确使用了最常用的 RBF 高斯径向基核函数,这没有问题。问题出在了 gamma 参数,gamma 是核函数的因数,这里选择了 auto 自动。自动即表示 gamma 的取值为 1 / 特征数量,这里为 1/64。
尝试将 gamma 参数的值改的更小一些,比如 0.001。重新建立模型

model = SVC(gamma=0.001)  # 重新建立模型
model.fit(train_feature, train_target)  # 模型训练
results = model.predict(test_feature)  # 模型预测

scores = accuracy_score(test_target, results)  # 评估预测精准度
scores

可以看到,这一次的预测准确度已经达到 98% 了,结果非常理想。所以说,会用 scikit-learn 建立模型只是机器学习过程中最基础的一步,更加重要的是理解模型的参数,并学会调参使得模型的预测性能更优。

监督学习算法对比评估

K 近邻

K 近邻是一种十分常用的监督学习算法。简单来讲,K 近邻就是假设一个给定的数据集,且数据的类别已经确定。这些数据的特征所构成的特征向量可以映射到对应的特征空间中。现在,假设一个输入实例,我们可以计算该输入和其他数据点之间的距离,再通过多数表决的方式,来确定新输入实例的类别,最后完成分类。
其中,K 近邻中的「近邻」代表原有特征空间中与新输入实例距离最近的那些样本。而 K 代表距离最近的 K 个样本。所以,对于 K 近邻而言。K 值的大小和距离的度量方式(欧式距离或者曼哈顿距离)是其构成的两个关键因素。


image.png

如上图所示,原数据集由红、绿两类组成。现在,我们新输入一个橙色实例,图中表示了样本对应在特征空间的位置。现在,我们确定 K = 3,然后可以圈定出距离橙色实例最近的 3 个样本点。其中,红色样本为 1 个,绿色 2 个。根据多数表决的规则,最终确定新输入的橙色样本数据被判定为 B 类别。而当我们指定 K = 7 时,红色样本 4 个,绿色样本 3 个,则橙色样本被判定为 A 类别。

决策树和随机森林

决策树也是一种十分常见的监督学习方法。它是一种特殊的树形结构,一般由节点和有向边组成。其中,节点表示特征、属性或者一个类。而有向边包含有判断条件。


image.png

如图所示,决策树从根节点开始延伸,经过不同的判断条件后,到达不同的子节点。而上层子节点又可以作为父节点被进一步划分为下层子节点。一般情况下,我们从根节点输入数据,经过多次判断后,这些数据就会被分为不同的类别。这就构成了一颗简单的分类决策树。
当我们使用决策树分类时,对于已有训练集只建立一颗决策树。而随机森林的概念是,对于一个训练集随机建立多颗决策树。而建立这些决策树时,会采取一种叫 Bootstrap 的取样方式,即每一次从数据集中又放回的取出一部分数据,再用这部分数据去建立小决策树。对于随机森林而言,最终的分类结果由众多小决策树输出类别的众数确定。下图展示了一个由 3 颗决策树构成的随机森林过程。


image.png

由于随机森林的特点,有效地降低过拟合程度,具有较好的泛化误差。另外,训练速度也非常快,模型的表现往往都比较好,是十分受欢迎的一种机器学习方法。

常见监督学习方法

通过同一个示例数据集来对常见的监督学习算法分类性能做一个比较。
为了更方便可视化,这里选用了一个随机生成的二分类数据集。总共包含 300 条数据,类别为 0 和 1。

import pandas as pd  # 加载 pandas 模块
import warnings
warnings.filterwarnings('ignore')

# 读取 csv 文件, 并将第一行设为表头
data = pd.read_csv(
    "https://labfile.oss.aliyuncs.com/courses/866/class_data.csv", header=0)

data.head()  # 输出数据预览

通常情况下,可视化是直观认识陌生数据的很好方法。这里,我们通过 Matplotlib 来可视化这些数据。画出数据集的散点图。

from matplotlib import pyplot as plt  # 加载绘图模块
%matplotlib inline

plt.scatter(data["X"], data['Y'], c=data['CLASS'])  # 绘制散点图

我们用 c=data['CLASS'] 参数来控制散点的颜色。
接下来,加载本次实验需要的模块,以及 scikit-learn 中常见的分类器。

# 集成方法分类器
from sklearn.ensemble import AdaBoostClassifier
from sklearn.ensemble import BaggingClassifier
from sklearn.ensemble import ExtraTreesClassifier
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.ensemble import RandomForestClassifier
# 高斯过程分类器
from sklearn.gaussian_process import GaussianProcessClassifier
# 广义线性分类器
from sklearn.linear_model import PassiveAggressiveClassifier
from sklearn.linear_model import RidgeClassifier
from sklearn.linear_model import SGDClassifier
# K近邻分类器
from sklearn.neighbors import KNeighborsClassifier
# 朴素贝叶斯分类器
from sklearn.naive_bayes import GaussianNB
# 神经网络分类器
from sklearn.neural_network import MLPClassifier
# 决策树分类器
from sklearn.tree import DecisionTreeClassifier
from sklearn.tree import ExtraTreeClassifier
# 支持向量机分类器
from sklearn.svm import SVC
from sklearn.svm import LinearSVC

接下来,建立预测模型,采用默认参数即可。由于方法较多,所有这里就不再依次单独定义模型,而是用列表形式管理。

# 建立模型
models = [
    AdaBoostClassifier(),
    BaggingClassifier(),
    ExtraTreesClassifier(),
    GradientBoostingClassifier(),
    RandomForestClassifier(),
    GaussianProcessClassifier(),
    PassiveAggressiveClassifier(),
    RidgeClassifier(),
    SGDClassifier(),
    KNeighborsClassifier(),
    GaussianNB(),
    MLPClassifier(),
    DecisionTreeClassifier(),
    ExtraTreeClassifier(),
    SVC(),
    LinearSVC()
]

# 依次为模型命名
classifier_Names = ['AdaBoost', 'Bagging', 'ExtraTrees',
                    'GradientBoosting', 'RandomForest', 'GaussianProcess',
                    'PassiveAggressive', 'Ridge', 'SGD',
                    'KNeighbors', 'GaussianNB', 'MLP',
                    'DecisionTree', 'ExtraTree', 'SVC', 'LinearSVC']

然后,划分数据集。70% 用于训练,另外 30% 用于测试。

from sklearn.model_selection import train_test_split  # 导入数据集切分模块

feature = data[['X', 'Y']]  # 指定特征变量
target = data['CLASS']  # 指定标签变量
X_train, X_test, y_train, y_test = train_test_split(
    feature, target, test_size=.3)  # 切分数据集

准备好数据之后,就可以开始模型训练和测试了。

from sklearn.metrics import accuracy_score  # 导入准确度评估模块

# 遍历所有模型
for name, model in zip(classifier_Names, models):
    model.fit(X_train, y_train)  # 训练模型
    pre_labels = model.predict(X_test)  # 模型预测
    score = accuracy_score(y_test, pre_labels)  # 计算预测准确度
    print('%s: %.2f' % (name, score))  # 输出模型准确度

这 16 个分类器最终的准确度均在 80% ~ 90% 之间,差距不是很大。对于这种现象,主要有两个原因。首先,本次使用的是一个非常规范整洁的线性分类数据集。其次,所有的分类器均采用了默认参数,而 scikit-learn 提供的默认参数一般已经较优。
我们通过可视化的方法将每一个模型在分类时的决策边界展示出来,这样能更加直观的感受到机器学习模型在执行分类预测时发生的变化。

from matplotlib.colors import ListedColormap  # 加载色彩模块
import numpy as np  # 导入数值计算模块
from tqdm.notebook import tqdm

# 绘制数据集
i = 1  # 为绘制子图设置的初始编号参数
cm = plt.cm.Reds  # 为绘制等高线选择的样式
cm_color = ListedColormap(['red', 'yellow'])  # 为绘制训练集和测试集选择的样式

# 栅格化
x_min, x_max = data['X'].min() - .5, data['X'].max() + .5
y_min, y_max = data['Y'].min() - .5, data['Y'].max() + .5

xx, yy = np.meshgrid(np.arange(x_min, x_max, .1),
                     np.arange(y_min, y_max, .1))

# 模型迭代
plt.figure(figsize=(20, 10))

for name, model in tqdm(zip(classifier_Names, models)):
    ax = plt.subplot(4, 4, i)  # 绘制 4x4 子图

    model.fit(X_train, y_train)  # 模型训练
    pre_labels = model.predict(X_test)  # 模型测试
    score = accuracy_score(y_test, pre_labels)  # 模型准确度

    # 根据类的不同选择决策边界计算方法
    if hasattr(model, "decision_function"):
        Z = model.decision_function(np.c_[xx.ravel(), yy.ravel()])
    else:
        Z = model.predict_proba(np.c_[xx.ravel(), yy.ravel()])[:, 1]

    # 绘制决策边界等高线
    Z = Z.reshape(xx.shape)
    ax.contourf(xx, yy, Z, cmap=cm, alpha=.6)

    # 绘制训练集和测试集
    ax.scatter(X_train['X'], X_train['Y'], c=y_train, cmap=cm_color)
    ax.scatter(X_test['X'], X_test['Y'], c=y_test,
               cmap=cm_color, edgecolors='black')

    # 图形样式设定
    ax.set_xlim(xx.min(), xx.max())
    ax.set_ylim(yy.min(), yy.max())
    ax.set_xticks(())
    ax.set_yticks(())
    ax.set_title('%s | %.2f' % (name, score))
    i += 1

按照上面的步骤执行,就能看到如下图所示的对比图了。


image.png

上面将决策边界绘制出来,并用等高线图显示。其中,颜色越深表示偏向于黄色散点分类的概率越高,而颜色越浅,则表示偏向红色散点的概率越高。
为了进一步探索各分类器对于不同特征分布的数据集的适用情况。接下来,我们将原有数据集做一些变换。
sklearn.datasets 这个模块可以导入一些预设的数据集。其实,不仅如此,这个模块开提供了一些数据集的生成方法。比如:
sklearn.datasets.make_circles 方法可以生成大圆环包小圆环样式的数据集。
sklearn.datasets.make_moons 方法可以生成两个交织间隔圆环样式的数据集。

from sklearn import datasets

# 生成 200 个包含噪声的环状样本
circles = datasets.make_circles(n_samples=200, noise=.1)
plt.scatter(circles[0][:, 0], circles[0][:, 1], c=circles[1])

# 生成 300 个包含噪声的月牙状样本
moons = datasets.make_moons(n_samples=300, noise=.2, random_state=1)
plt.scatter(moons[0][:, 0], moons[0][:, 1], c=moons[1])

这两组数据都是无法进行线性分类,所以如果是非线性分类器,其结果应该会好很多。

# 对月牙状样本进行分割测试
X_train, X_test, y_train, y_test = train_test_split(
    moons[0], moons[1], test_size=0.3)

for name, model in zip(classifier_Names, models):
    model.fit(X_train, y_train)  # 训练模型
    pre_labels = model.predict(X_test)  # 模型预测
    score = accuracy_score(y_test, pre_labels)  # 计算预测准确度
    print('%s: %.2f' % (name, score))  # 输出模型准确度

K-Means 数据聚类算法应用

K-Means 聚类

监督学习被用于解决分类和回归问题,而非监督学习主要是用于解决聚类问题。聚类,顾名思义就是将具有相似属性或特征的数据聚合在一起。聚类算法有很多,最简单和最常用的就算是 K-Means 算法了。
K-Means,中文译作 K - 均值算法。从它的名字来讲,K 代表最终将样本数据聚合为 K 个类别。而「均值」代表在聚类的过程中,我们计算聚类中心点的特征向量时,需要采用求相邻样本点特征向量均值的方式进行。例如:


image.png

K-Means 算法在应用时,相对来上面的例子要复杂一些。现在,假设有如下图所示的一组二维数据。接下来,我们就一步一步演示 K-Means 的聚类过程。
首先,正如我们前面介绍非监督学习时所说,非监督学习面对的数据都是没有标签的。如果我们把下方示例数据的颜色看作是标签,那么只有一种颜色。


image.png

第一步,确定要聚为几类?也就是 K 值。假设,这里我们想将样本聚为 3 类。当然,你也完全可以将其聚为 2 类或 4 类,不要受到视觉上的误导。
这里,我们以 3 类为例。当确定聚为 3 类之后,我们在特征空间上,随机初始化 3 个类别中心。这里的 3 也就对应着 K 值的大小。
image.png

第二步,我们依据这三个随机初始化的中心,将现有样本按照与最近中心点之间的距离进行归类。图中绿线将全部样本划分为三个类别。(中间小三角形是参考线,可以忽略。)


image.png

这样,我们的样本被划为为三个区域。现在,我们就要用到上面提到的均值来重新求解 3 个区域对应的新的样本中心。
image.png

如上图所示,假设我们求解的新样本中心为三个绿点所示的位置。然后,又重新回到上一步,根据这三个中心重新划分样本,再求解中心。
image.png

依次迭代,直到样本中心变化非常小时终止。最终,就可以将全部样本聚类为三类。

首先,我们导入 Pandas 数据处理模块,用来解析 CSV 数据文件,并查看文件的组成结构。

import pandas as pd  # 导入数据处理模块
import warnings
warnings.filterwarnings('ignore')

df = pd.read_csv(
    "http://labfile.oss.aliyuncs.com/courses/866/cluster_data.csv", header=0)  # 导入数据文件
df.head()

可以看到,文件包含两列,也就是对应在特征空间的 x, y 坐标。接下来,我们用 Matplotlib 将数据绘制成散点图。

from matplotlib import pyplot as plt  # 导入绘图模块
%matplotlib inline

X = df['x']  # 定义横坐标数据
y = df['y']  # 定义纵坐标数据

plt.scatter(X, y)  # 绘制散点图

接下来,开始导入 K-Means 方法进行聚类。scikit-learn 中的聚类算法都包含在 sklearn.cluster 方法下。

from sklearn.cluster import k_means  # 导入 K-Means 方法

model = k_means(df, n_clusters=3)  # 建立聚类模型
model

model 输出的结果包含三个数组。其中,第一个数组表示三个聚类中心点坐标。第二个数组表示样本聚类后类别,第三个数组表示样本距最近聚类中心的距离总和。
接下来,我们就将聚类的结果绘制出来。

cluster_centers = model[0]  # 聚类中心数组
cluster_labels = model[1]  # 聚类标签数组

plt.scatter(X, y, c=cluster_labels)  # 绘制样本并按聚类标签标注颜色

# 绘制聚类中心点,标记成五角星样式,以及红色边框
for center in cluster_centers:
    plt.scatter(center[0], center[1], marker="p", edgecolors="red")

可以看到,聚类的结果已经显示出来了,聚类中心也做了相应标记。

K 值选择

我们为什么要聚成 3 类?为什么不可以将数据聚成 10 类?
聚成 10 类当然是可以的,只需要将 n_clusters=10 即可。

model = k_means(df, n_clusters=10)  # 建立聚类模型

cluster_centers = model[0]  # 聚类中心数组
cluster_labels = model[1]  # 聚类标签数组
plt.scatter(X, y, c=cluster_labels)  # 绘制样本并按聚类标签标注颜色

# 绘制聚类中心点,标记成五角星样式,以及红色边框
for center in cluster_centers:
    plt.scatter(center[0], center[1], marker="p", edgecolors="red")

那我们为什么会选择 K=3 呢?当我们遇到肉眼看起来不太好确定类别,或者是高维数据时怎么办呢?
所以,这一系列问题最终可以归结为一个问题,那就是:当我们在使用 K-Means 聚类时,我们该如何确定 K 值?
启发式学习算法,被称之为肘部法则。在上文谈到用 print(model) 语句时,它会输出三个数组。其中,前两个数组在进行聚类绘图时已经用到了,但是我们一直没有使用第三个数组。

model[2]  # 打印第三个数组

第三个数组,准确说来只是一个数值。它代表着样本与最近中心点距离的平方和。当我们的 K 值增加时,也就是类别增加时,这个数值应该是会降低的。直到聚类类别的数量和样本的总数相同时,也就是说一个样本就代表一个类别时,这个数值会变成 0。

index = []  # 横坐标数组
inertia = []  # 纵坐标数组

# K 从 1~ 10 聚类
for i in range(9):
    model = k_means(df, n_clusters=i + 1)
    index.append(i + 1)
    inertia.append(model[2])

# 绘制折线图
plt.plot(index, inertia, "-o")

我们可以看到,和预想的一样,样本距离最近中心点距离的总和会随着 K 值的增大而降低。其中,畸变程度最大的点被称之为「肘部」。我们可以看到,这里的「肘部」明显是 K = 3。这也说明,将样本聚为 3 类的确是最佳选择。
当我们完成一项聚类任务之后,我们需要对聚类效果进行评估。其实,上面提到的肘部法则也算是一种评估方法,它让我们知道当 K 值为多少时,整体聚类的结果更理想。
聚类中还有一种评估方法,叫 轮廓系数。轮廓系数综合了聚类后的两项因素:内聚度和分离度。内聚度就指一个样本在簇内的不相似度,而分离度就指一个样本在簇间的不相似度。
在 scikit-learn 种,同样也提供了直接计算轮廓系数的方法。下面,绘制出轮廓系数与聚类 K 值变化的折线图。

from sklearn.metrics import silhouette_score  # 导入轮廓系数计算模块

index2 = []  # 横坐标
silhouette = []  # 轮廓系数列表

# K 从 2 ~ 10 聚类
for i in range(8):
    model = k_means(df, n_clusters=i + 2)
    index2.append(i + 2)
    silhouette.append(silhouette_score(df, model[1]))

print(silhouette)  # 输出不同聚类下的轮廓系数

# 绘制折线图
plt.plot(index2, silhouette, "-o")

轮廓系数越接近于 1,代表聚类的效果越好。我们可以很清楚的看出,K=3 对应的轮廓系数数组最大,也更接近于 1 。

PCA 主成分分析应用

主成分分析

主成分分析 是多元线性统计里面的概念,它的英文是 Principal Components Analysis,简称 PCA。主成分分析旨在降低数据的维数,通过保留数据集中的主要成分来简化数据集。简化数据集在很多时候是非常必要的,因为复杂往往就意味着计算资源的大量消耗。通过对数据进行降维,我们就能在结果不受影响或受略微影响的同时,减少模型学习时间。
本次实验所说的降维,不是指降低 NumPy 数组的维度,而是特指减少样本的特征数量。
主成分分析的数学基原理比较简单,通过对协方差矩阵进行特征分解,从而得出主成分(特征向量)与对应的权值(特征值)。然后剔除那些较小特征值(较小权值)对应的特征向量,从而达到降低数据维数的目的。

image.png

上图展示了把三维空间的数据降维到二维空间的过程。

sklearn.decomposition.PCA(n_components=None, copy=True, whiten=False, svd_solver='auto')

n_components= 表示需要保留主成分(特征)的数量。
copy= 表示针对原始数据降维还是针对原始数据副本降维。当参数为 False 时,降维后的原始数据会发生改变,这里默认为 True。
whiten= 白化表示将特征之间的相关性降低,并使得每个特征具有相同的方差。
svd_solver= 表示奇异值分解 SVD 的方法。有 4 参数,分别是:auto, full, arpack, randomized。
在使用 PCA 降维时,我们也会使用到 PCA.fit() 方法。.fit() 是 scikit-learn 训练模型的通用方法,但是该方法本身返回的是模型的参数。所以,通常我们会使用 PCA.fit_transform() 方法直接返回降维后的数据结果。

import numpy as np  # 导入数值计算模块
from sklearn.decomposition import PCA  # 导入 PCA 模块
import warnings
warnings.filterwarnings('ignore')

data = np.array([[1, 2], [3, 4], [5, 6], [7, 8]])  # 新建一个 2 维数组
new_data = PCA(n_components=1).fit_transform(data)  # 降维成 1 维并返回值

print(data)  # 输出原数据
print(new_data)  # 输出降维后的数据

我们可以看到,原先包含 2 个特征的数组已经降维为 1 组特征。

手写数字识别聚类

手写数字数据集一共由 1797 张 8x8 的影像组成。该数据集可以直接通过 scikit-learn 导入,无需到外部下载。
先输出前 5 张图像预览一下

from sklearn import datasets  # 导入数据集模块
import matplotlib.pyplot as plt  # 导入绘图模块
%matplotlib inline

# 载入数据集
digits_data = datasets.load_digits()

# 绘制数据集前 5 个手写数字的灰度图
for index, image in enumerate(digits_data.images[:5]):
    plt.subplot(2, 5, index+1)
    plt.imshow(image, cmap=plt.cm.gray_r, interpolation='nearest')

上面的 5 张数字图像依次为 0,1,2,3,4。之前,我们用支持向量机完成了分类,即预测哪一张图像代表哪一个数字。现在,我们采用相同的数据集完成聚类分析,即将全部数据集聚为 10 个类别。
首先,我们导入常用的 NumPy 数值计算模块和 Matplotlib 绘图模块。由于原数据集维度达到 16,所以这里要进行 PCA 降维。

from sklearn import decomposition
from sklearn.cluster import KMeans

# 导入数据集
digits_data = datasets.load_digits()
X = digits_data.data
y = digits_data.target

# PCA 将数据降为 2 维
estimator = decomposition.PCA(n_components=2)
reduce_data = estimator.fit_transform(X)
reduce_data.shape

接下来,将降维后的数据聚为 10 类,并将聚类后的结果、聚类中心点、聚类决策边界绘制出来。

# 建立 K-Means 并输入数据
model = KMeans(n_clusters=10)
model.fit(reduce_data)

# 计算聚类过程中的决策边界
x_min, x_max = reduce_data[:, 0].min() - 1, reduce_data[:, 0].max() + 1
y_min, y_max = reduce_data[:, 1].min() - 1, reduce_data[:, 1].max() + 1
xx, yy = np.meshgrid(np.arange(x_min, x_max, .05),
                     np.arange(y_min, y_max, .05))

result = model.predict(np.c_[xx.ravel(), yy.ravel()])

# 将决策边界绘制绘制出来
result = result.reshape(xx.shape)
plt.figure(figsize=(10, 5))
plt.contourf(xx, yy, result, cmap=plt.cm.Greys)
plt.scatter(reduce_data[:, 0], reduce_data[:, 1], c=y, s=15)

# 绘制聚类中心点
center = model.cluster_centers_
plt.scatter(center[:, 0], center[:, 1], marker='p',
            linewidths=2, color='b', edgecolors='w', zorder=20)

# 图像参数设置
plt.xlim(x_min, x_max)
plt.ylim(y_min, y_max)

图中,不同的色块区域代表一类。这里色块的颜色没有意义,只表示类别。散点代表数据,散点的颜色表示数据原始类别。我们可以看出,虽然原始数据已经从 16 维降维 2 维,但某几个数字的依旧有明显的成团现象。
除此之外,我们还可以使用分类方法来对比降维前后的数据表现。我们使用随机森林方法对数据进行分类,并通过交叉验证得到准确度结果。

from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import cross_val_score

model = RandomForestClassifier()
cross_val_score(model, X, y, cv=5).mean()  # 5 折交叉验证平均准确度

然后,我们再使用降维后的数据做一次一样的分类实验。

estimator = decomposition.PCA(n_components=5) # 从 10 个特征缩减为 5 个特征
X_pca = estimator.fit_transform(X)

model = RandomForestClassifier()
cross_val_score(model, X_pca, y, cv=5).mean()  # 5 折交叉验证平均准确度

特征减少 1 半,实际上准确度减少并不多。也就是说,在客观条件限制下,我们往往可以以更少维度的数据训练出准确度可以勉强接受的模型。

聚类学习算法对比评估

更多聚类算法

Mini Batch K-Means
实现方法:sklearn.cluster.MiniBatchKMeans
Mini Batch K-Means 整体上和 K-Means 很相似,它是 K-Means 的一个变种形式。与 K-Means 不同的地方在于,其每次从全部数据集中抽样小数据集进行迭代。Mini Batch K-Means 算法在不对聚类效果造成较大影响的前提下,大大缩短了计算时间。
Affinity Propagation
实现方法:sklearn.cluster.AffinityPropagation
Affinity Propagation 又被称为亲和传播聚类。Affinity Propagation 是基于数据点进行消息传递的理念设计的。与 K-Means 等聚类算法不同的地方在于,亲和传播聚类不需要提前确定聚类的数量,即 K 值。但是运行效率较低。
Mean Shift
实现方法:sklearn.cluster.MeanShift
MeanShift 又被称为均值漂移聚类。Mean Shift 聚类的目的是找出最密集的区域, 同样也是一个迭代过程。在聚类过程中,首先算出初始中心点的偏移均值,将该点移动到此偏移均值,然后以此为新的起始点,继续移动,直到满足最终的条件。Mean Shift 也引入了核函数,用于改善聚类效果。除此之外,Mean Shift 在图像分割,视频跟踪等领域也有较好的应用。
Spectral Clustering
实现方法:sklearn.cluster.SpectralClustering
Spectral Clustering 又被称为谱聚类。谱聚类同样也是一种比较常见的聚类方法,它是从图论中演化而来的。谱聚类一开始将特征空间中的点用边连接起来。其中,两个点距离越远,那么边所对应的权值越低。同样,距离越近,那么边对应的权值越高。最后,通过对所有特征点组成的网络进行切分,让切分后的子图互相连接的边权重之和尽可能的低,而各子图内部边组成的权值和经可能高,从而达到聚类的效果。谱聚类的好处是能够识别任意形状的样本空间,并且可以得到全局最优解。
Agglomerative Clustering
实现方法:sklearn.cluster.AgglomerativeClustering
Agglomerative Clustering 又被称为层次聚类。层次聚类算法是将所有的样本点自下而上合并组成一棵树的过程,它不再产生单一聚类,而是产生一个聚类层次。层次聚类通过计算各样本数据之间的距离来确定它们的相似性关系,一般情况下,距离越小就代表相似度越高。最后,将相似度越高的样本归为一类,依次迭代,直到生成一棵树。由于层次聚类涉及到循环计算,所以时间复杂度比较高,运行速度较慢。
Birch 聚类
实现方法:sklearn.cluster.Birch
Birch 是英文 Balanced Iterative Reducing and Clustering Using Hierarchies 的简称,它的中文译名为「基于层次方法的平衡迭代规约和聚类」,名字实在太长。
Birch 引入了聚类特征树(CF 树),先通过其他的聚类方法将其聚类成小的簇,然后再在簇间采用 CF 树对簇聚类。Birch 的优点是,只需要单次扫描数据集即可完成聚类,运行速度较快,特别适合大数据集。
DBSCAN
实现方法:sklearn.cluster.DBSCAN
DBSCAN 是英文 Density-based spatial clustering of applications with noise 的简称,它的中文译名为「基于空间密度与噪声应用的聚类方法」,名字同样很长。
DBSCAN 基于密度概念,要求聚类空间中的一定区域内所包含的样本数目不小于某一给定阈值。该算法运行速度快,且能够有效处理特征空间中存在的噪声点。但是对于密度分布不均匀的样本集合,DBSCAN 的表现较差。

聚类算法对比

首先,我们从 sklearn.cluster 模块中,导入各聚类方法。如 K-Means 等方法需要提前确定类别数量,也就是 K 值。判断的方法很简单,如果聚类方法中包含 n_clusters= 参数,即代表需要提前指定。这里我们统一确定 K=3。

from sklearn import cluster  # 导入聚类模块

# 对聚类方法依次命名
cluster_names = ['KMeans', 'MiniBatchKMeans', 'AffinityPropagation', 'MeanShift',
                 'SpectralClustering', 'AgglomerativeClustering', 'Birch', 'DBSCAN']

# 确定聚类方法相应参数
cluster_estimators = [
    cluster.KMeans(n_clusters=3),
    cluster.MiniBatchKMeans(n_clusters=3),
    cluster.AffinityPropagation(),
    cluster.MeanShift(),
    cluster.SpectralClustering(n_clusters=3),
    cluster.AgglomerativeClustering(n_clusters=3),
    cluster.Birch(n_clusters=3),
    cluster.DBSCAN()
]

接下来,我们对上面提到的 8 种常见的聚类算法做一个对比。这里选择了一个空间分布由三个团状图案组成的示例数据集。

import pandas as pd  # 导入数据处理模块
from matplotlib import pyplot as plt  # 导入绘图模块
%matplotlib inline

# 读取数据集 csv 文件
data = pd.read_csv(
    "https://labfile.oss.aliyuncs.com/courses/866/data_blobs.csv", header=0)
X = data[['x', 'y']]
plt.scatter(data['x'], data['y'])

然后,我们分别应用 8 种聚类方法对数据进行聚类,并将最终的聚类结果绘制出来。

import numpy as np  # 导入数值计算模块

plot_num = 1  # 为绘制子图准备
plt.figure(figsize=(20, 10))
# 不同的聚类方法依次运行
for name, algorithm in zip(cluster_names, cluster_estimators):
    algorithm.fit(X)  # 聚类
    # 判断方法中是否由 labels_ 参数,并执行不同的命令
    if hasattr(algorithm, 'labels_'):
        algorithm.labels_.astype(np.int)
    else:
        algorithm.predict(X)
    # 绘制子图
    plt.subplot(2, len(cluster_estimators) / 2, plot_num)
    plt.scatter(data['x'], data['y'], c=algorithm.labels_)
    # 判断方法中是否由 cluster_centers_ 参数,并执行不同的命令
    if hasattr(algorithm, 'cluster_centers_'):
        centers = algorithm.cluster_centers_
        plt.scatter(centers[:, 0], centers[:, 1], marker="p", edgecolors="red")
    # 绘制图标题
    plt.title(name)
    plot_num += 1

在我们指定 n_clusters=3 的方法中,除了 SpectralClustering 出现了三个样本点漂移,其他几种方法的结果几乎是一致的。除此之外,在没有指定 n_clusters 的聚类方法中,Mean Shift 对于此数据集的适应性较好,而亲和传播聚类方法在默认参数下表现最差。
scikit-learn 提供了一张选择判断图供大家参考。


image.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容