Core ML框架详细解析(十三) —— 使用Keras和Core ML开始机器学习(一)

版本记录

版本号 时间
V1.0 2018.10.16 星期二

前言

目前世界上科技界的所有大佬一致认为人工智能是下一代科技革命,苹果作为科技界的巨头,当然也会紧跟新的科技革命的步伐,其中ios API 就新出了一个框架Core ML。ML是Machine Learning的缩写,也就是机器学习,这正是现在很火的一个技术,它也是人工智能最核心的内容。感兴趣的可以看我写的下面几篇。
1. Core ML框架详细解析(一) —— Core ML基本概览
2. Core ML框架详细解析(二) —— 获取模型并集成到APP中
3. Core ML框架详细解析(三) —— 利用Vision和Core ML对图像进行分类
4. Core ML框架详细解析(四) —— 将训练模型转化为Core ML
5. Core ML框架详细解析(五) —— 一个Core ML简单示例(一)
6. Core ML框架详细解析(六) —— 一个Core ML简单示例(二)
7. Core ML框架详细解析(七) —— 减少Core ML应用程序的大小(一)
8. Core ML框架详细解析(八) —— 在用户设备上下载和编译模型(一)
9. Core ML框架详细解析(九) —— 用一系列输入进行预测(一)
10. Core ML框架详细解析(十) —— 集成自定义图层(一)
11. Core ML框架详细解析(十一) —— 创建自定义图层(一)
12. Core ML框架详细解析(十二) —— 用scikit-learn开始机器学习(一)

开始

首先看一下本文的写作环境

Swift 4, iOS 11, Xcode 9

Apple的Core MLVision框架已经让开发人员进入了一个勇敢的机器学习新世界,并带来了令人兴奋的可能性。Vision允许您检测和跟踪面部,Apple的Machine Learning page提供检测对象和场景的即用型模型,以及用于自然语言处理的NSLinguisticTagger。如果您想构建自己的模型,请尝试使用Apple的新Turi Create来扩展其使用您的数据预先训练的模型之一。

但是,如果您想要做什么需要更加个性化的东西?然后,是时候进入机器学习(ML),使用谷歌,微软,亚马逊或伯克利的众多框架之一。而且,为了让生活更加精彩,您需要选择一种新的编程语言和一套新的开发工具。

在这个Keras机器学习教程中,您将学习如何训练深度学习卷积神经网络模型,将其转换为Core ML,并将其集成到iOS应用程序中。您将学习一些ML术语,使用一些新工具,并在此过程中学习一些Python。

示例项目使用ML的Hello-World示例 - 一种对手写数字进行分类的模型,在MNIST dataset上进行训练。


Why Use Keras? - 为什么使用Keras?

ML模型涉及许多复杂的代码,操纵数组和矩阵。但ML已经存在了很长时间,研究人员已经创建了库,使像我们这样的人更容易创建ML模型。其中许多是用Python编写的,尽管研究人员还使用R,SAS,MATLAB和其他软件。但您可能会在基于Python的工具中找到所需的一切:

  • scikit-learn提供了一种运行许多经典ML算法的简便方法,例如线性回归和支持向量机。
  • 另一方面是PyTorchGoogleTensorFlow,它可以让您更好地控制深度学习模型的内部工作。
  • 微软的CNTKBerkeleyCaffe是类似的深度学习框架,它们使用Python API来访问他们的C ++引擎。

那么Keras在哪里适合?这是TensorFlowCNTK的包装,亚马逊的MXN​​et即将推出。 (它也与Theano合作,但蒙特利尔大学于2017年9月停止了这项工作。)它提供了一个易于使用的API,用于构建模型,您可以在一个后端训练,并在另一个后端部署。

使用Keras而不是直接使用TensorFlow的另一个原因是coremltools包括Keras转换器,但不包括TensorFlow转换器 - 尽管存在TensorFlow to CoreML converterMXNet to CoreML converter。虽然Keras支持CNTK作为后端,但coremltools仅适用于Keras + TensorFlow

注意:在使用这些工具之前,您是否需要学习Python?好吧,我没有。当你完成本教程时,你会发现Python语法与Swift类似:更加简化,缩进是语法的重要部分。如果您感到紧张,请在浏览器选项卡中保持打开状态,以便快速参考:Crash Course in Python for Machine Learning Developers

另一个注意事项:研究人员同时使用Python 2Python 3,但coremltoolsPython 2.7中运行得更好。


开始进入正题

打开项目起始文件夹:它包含一个入门iOS应用程序,您将在其中添加ML模型和代码以使用它。 它还包含一个docker-keras文件夹,其中包含本教程的Jupyter笔记本。

1. Setting Up Docker - 设置Docker

Docker是一个容器平台,允许您在自定义环境中部署应用程序 - 有点像虚拟机,但different。 通过安装Docker,您可以访问大量的ML资源,这些资源主要作为Docker镜像中的交互式Jupyter notebooks分发。

注意:安装Docker并构建映像需要几分钟,因此请在等待时阅读ML in a Nutshell

下载,安装和启动Docker Community Edition for Mac。 在终端中,一次输入以下命令:

cd <where you unzipped starter>/starter/docker-keras
docker build -t keras-mnist .
docker run --rm -it -p 8888:8888 -v $(pwd)/notebook:/workspace/notebook keras-mnist

最后一个命令将Docker容器的notebook文件夹映射到本地notebook文件夹,因此即使在您注销Docker服务器之后,您也可以访问notebook所写的文件。

在命令输出的最后是包含token的URL。 它看起来像这样,但具有不同的标记值:

http://0.0.0.0:8888/?token=7b189c8e200f49dcc33845d39101e8a0ab257db5f3b539a7

将此URL粘贴到浏览器中以登录Docker容器的notebook服务器。

打开notebook文件夹,然后打开keras_mnist.ipynb。 点击Not Trusted按钮将其更改为Trusted:这样您就可以在notebook文件夹中保存对notebook以及模型文件所做的更改。

2. ML in a Nutshell - Nutshell中的ML

Arthur Samuel将机器学习定义为“研究领域,让计算机具有无需明确编程即可学习的能力”。 您有数据,它具有一些可用于对数据进行分类的功能,或者用它来进行一些预测,但是您没有用于这种计算的明确的公式,因此您无法编写程序来执行此操作。 如果您有“足够”的数据样本,则可以训练计算机模型以识别此数据中的模式,然后将其学习应用于新数据。 当您知道所有训练数据的正确结果时,它被称为监督学习:然后模型仅根据已知结果检查其预测,并调整自身以减少误差并提高准确性。 无监督学习超出了本教程的范围。

Weights & Threshold - 权重和阈值

假设您想和一群朋友一起选择一家餐厅共进晚餐。 有几个因素会影响您的决定:饮食限制,公共交通,价格范围,食物类型,儿童友好等。您为每个因素分配一个权重,以表明其对您决定的重要性。 然后,对于选项列表中的每个餐馆,您可以根据餐厅满足该因素的程度为每个因素分配一个值。 您将每个因子值乘以系数的权重,然后将它们相加以获得加权和。 结果最高的餐厅是最佳选择。 使用此模型的另一种方法是生成二进制输出:是或否。 您设置了一个阈值,并从列表中删除任何加权总和低于此阈值的餐馆。

Training an ML Model - 训练ML模型

提出权重并不是一件容易的事。 但幸运的是,您有很多以前的晚餐数据,包括选择了哪家餐厅,因此您可以训练ML模型来计算产生相同结果的权重,尽可能接近。 然后将这些计算出的权重应用于未来的决策。

要训练ML模型,首先要使用随机权重,将它们应用于训练数据,然后将计算出的输出与已知输出进行比较以计算误差。 这是一个具有最小值的多维函数,训练的目标是确定非常接近此最小值的权重。 权重也需要处理新数据:如果大量验证数据的误差高于训练数据的误差,那么模型就会过度拟合 - 权重对训练数据越适合,表明训练错误检测到一些不会推广到新数据的功能。

Stochastic Gradient Descent - 随机梯度下降

要计算减少误差的权重,可以在当前图形位置计算误差函数的梯度,然后调整权重以“降低”斜率。 这称为梯度下降,在训练期间多次发生。 对于大型数据集,使用所有数据计算梯度需要很长时间。 随机梯度下降(Stochastic gradient descent - SGD)从随机选择的小批量训练数据中估计梯度 - 例如在选举日之前对选民进行调查:如果您的样本代表整个数据集,则调查结果可准确预测最终结果。

Optimizers - 优化器

错误函数是块状的:你必须小心不要走得太远,否则你可能会错过最低限度。 你的步数也需要有足够的动力来推动你摆脱任何虚假的最低限度。 ML研究人员为设计优化算法付出了很多努力。 目前最受欢迎的是AdamAdaptive Moment estimation - 自适应力矩估计),它结合了以前最受欢迎的RMSpropRoot Mean Square propagation - 均方根传播)和AdaGradAdaptive Gradient algorithm - 自适应梯度算法)的特征。


Keras Code Time! - Keras代码时间!

好的,Docker容器现在应该准备就绪:返回并按照说明打开notebook。 是时候写一些Keras代码了!

在具有匹配标题的keras_mnist.ipynb单元格中输入以下代码。 在每个单元格中输入代码后,按Control-Enter运行它。 代码运行时,In []:标签中会出现一个星号,然后会出现一个数字,以显示运行单元格的顺序。 当您登录notebook时,所有内容都会保留在内存中。 每隔一段时间,点击Save and Checkpoint按钮。

注意:双击markdown单元格以添加自己的注释;按Control-Enter以呈现markdown并运行Python代码。 您还可以使用其他笔记本按钮添加或复制粘贴单元格,以及移动单元格。


Import Utilities & Dependencies - 导入实用项和依赖项

输入以下代码,然后运行它以检查Keras版本。

from __future__ import print_function
from matplotlib import pyplot as plt

import keras
from keras.datasets import mnist
from keras.models import Sequential
from keras.layers import Dense, Dropout, Flatten
from keras.layers import Conv2D, MaxPooling2D
from keras.utils import np_utils
from keras import backend as K

import coremltools
# coremltools supports Keras version 2.0.6
print('keras version ', keras.__version__)

__future__Python 2Python 3之间的兼容层:Python 2有一个print命令(没有括号),但Python 3需要print()函数。 导入print_function允许您在Python 2代码中使用print()语句。

Keras使用NumPy数学库来操作数组和矩阵。 MatplotlibNumPy的绘图库:您将使用它来检查训练数据项。

注意:由于due to NumPy 1.14,您可能会看到FutureWarning

导入keras后,打印其版本:coremltools支持2.0.6版,如果使用更高版本,将发出警告。 Keras已经拥有MNIST数据集,因此您可以导入它。 然后接下来的三行导入模型组件。 您导入NumPy实用程序,并为后端提供带有import backend as K的标签,您将使用它来检查image_data_format

最后,导入coremltools,您将在此notebook的末尾使用它。


Load & Pre-Process Data - 加载和预处理数据

1. Training & Validation Data Sets - 训练和验证数据集

首先,获取您的数据! 输入以下代码并运行它:下载数据需要一段时间。

(x_train, y_train), (x_val, y_val) = mnist.load_data()

这将从https://s3.amazonaws.com/img-datasets/mnist.npz下载数据,对数据项进行混洗,并在训练数据集和验证数据集之间进行拆分。 验证数据有助于检测模型过度拟合到训练数据的问题。 训练步骤使用训练的参数来计算验证数据的输出。 您将设置回调以监控验证丢失和准确性,以保存对验证数据执行最佳的模型,并且如果验证丢失或准确性未能在太多时期(重复)中提高,则可能提前停止。

2. Inspect x & y Data - Inspect x&y数据

下载完成后,在下一个单元格中输入以下代码,然后运行它以查看您获得的内容。

注意:您不必输入以#开头的行。 这些是注释,其中大部分都是为了向您展示运行单元格时notebook应显示的内容。

# Inspect x data
print('x_train shape: ', x_train.shape)
# Displays (60000, 28, 28)
print(x_train.shape[0], 'training samples')
# Displays 60000 train samples
print('x_val shape: ', x_val.shape)
# Displays (10000, 28, 28)
print(x_val.shape[0], 'validation samples')
# Displays 10000 validation samples

print('First x sample\n', x_train[0])
# Displays an array of 28 arrays, each containing 28 gray-scale values between 0 and 255
# Plot first x sample
plt.imshow(x_train[0])
plt.show()

# Inspect y data
print('y_train shape: ', y_train.shape)
# Displays (60000,)
print('First 10 y_train elements:', y_train[:10])
# Displays [5 0 4 1 9 2 1 3 1 4]

您有60,00028×28像素的训练样本和10,000个验证样本。 第一个训练样本是一个包含28个数组的数组,每个数组包含0到255之间的28个灰度值。查看非零值,您可以看到类似数字5的形状。

果然,plt代码显示第一个训练样本是手写的5:

y数据是一个60000元素的数组,包含训练样本的正确分类:第一个训练样本为5,下一个为0,依此类推。

3. Set Input & Output Dimensions - 设置输入和输出尺寸

输入这两行,然后运行单元格以设置x输入和y输出的基本尺寸。

img_rows, img_cols = x_train.shape[1], x_train.shape[2]
num_classes = 10

MNIST数据项是28×28像素的图像,您希望将每个数据分类为0到9之间的数字。

您可以使用x_train.shape值来设置图像行和列的数量。 x_train.shape是一个包含3个元素的数组:

  • 0) 数据样本数:60000
  • 1) 每个数据样本的行数:28
  • 2) 每个数据样本的列数:28

4. Reshape x Data & Set Input Shape

该模型需要略有不同的“shape”的数据。 输入以下代码,然后运行它。

# Set input_shape for channels_first or channels_last
if K.image_data_format() == 'channels_first':  
    x_train = x_train.reshape(x_train.shape[0], 1, img_rows, img_cols)
    x_val = x_val.reshape(x_val.shape[0], 1, img_rows, img_cols)
    input_shape = (1, img_rows, img_cols)
else:  
    x_train = x_train.reshape(x_train.shape[0], img_rows, img_cols, 1)
    x_val = x_val.reshape(x_val.shape[0], img_rows, img_cols, 1)
    input_shape = (img_rows, img_cols, 1)

卷积神经网络认为图像具有宽度,高度和深度。 深度维度称为通道,并包含颜色信息。 灰度图像有1个通道;RGB图像有3个通道。

TensorFlowCNTK这样的Keras后端,期望图像数据采用channels-last format (rows, columns, channels)channels-first format (channels, rows, columns)reshape函数将通道插入正确的位置。

您还可以在正确的末尾设置初始input_shape和通道。

5. Inspect Reshaped x Data

输入下面的代码,然后运行它以查看形状的变化情况。

print('x_train shape:', x_train.shape)
# x_train shape: (60000, 28, 28, 1)
print('x_val shape:', x_val.shape)
# x_val shape: (10000, 28, 28, 1)
print('input_shape:', input_shape)
# input_shape: (28, 28, 1)

TensorFlow图像数据格式是最后一个通道,因此x_train.shapex_val.shape现在最后有一个新元素1。

6. Convert Data Type & Normalize Values - 转换数据类型和规范化值

模型需要特定格式的数据值。 输入以下代码,然后运行它。

x_train = x_train.astype('float32')
x_val = x_val.astype('float32')
x_train /= 255
x_val /= 255

MNIST图像数据值的类型为uint8,范围为[0,255],但Keras需要float32类型的值,范围为[0,1]。

7. Inspect Normalized x Data

输入以下代码,然后运行它以查看对x数据的更改。

print('First x sample, normalized\n', x_train[0])
# An array of 28 arrays, each containing 28 arrays, each with one value between 0 and 1

现在每个值都是一个数组,值是浮点数,非零值介于0和1之间。

8. Reformat y Data

y数据是一个60000个元素的数组,包含训练样本的正确分类,但是只有10个类别并不明显。 输入以下代码,仅运行一次以重新格式化y数据。

print('y_train shape: ', y_train.shape)
# (60000,)
print('First 10 y_train elements:', y_train[:10])
# [5 0 4 1 9 2 1 3 1 4]
# Convert 1-dimensional class arrays to 10-dimensional class matrices
y_train = np_utils.to_categorical(y_train, num_classes)
y_val = np_utils.to_categorical(y_val, num_classes)
print('New y_train shape: ', y_train.shape)
# (60000, 10)

y_train是一维数组,但该模型需要一个60000 x 10矩阵来表示10个类别。 您还必须为10000元素的y_val数组进行相同的转换。

9. Inspect Reformatted y Data - 检查重新格式化的y数据

输入以下代码,然后运行它以查看y数据的更改方式。

print('New y_train shape: ', y_train.shape)
# (60000, 10)
print('First 10 y_train elements, reshaped:\n', y_train[:10])
# An array of 10 arrays, each with 10 elements, 
# all zeros except at index 5, 0, 4, 1, 9 etc.

y_train现在是一个包含10个元素数组的数组,每个数组都包含除了图像匹配的索引之外的所有零。


Define Model Architecture - 定义模型架构

模型架构是炼金术的一种形式,如完美烧烤酱或garam masala的秘密家庭食谱。 您可以从通用架构开始,然后调整它以利用输入数据中的对称性,或者生成具有特定特征的模型。

以下是来自两位研究人员的模型:Sri Raghu Malireddi和Keras作者的François CholletChollet是通用的,Malireddi's旨在生产适合移动应用的小模型。

输入以下代码,然后运行它以查看模型摘要。

1. Malireddi’s Architecture

model_m = Sequential()
model_m.add(Conv2D(32, (5, 5), input_shape=input_shape, activation='relu'))
model_m.add(MaxPooling2D(pool_size=(2, 2)))
model_m.add(Dropout(0.5))
model_m.add(Conv2D(64, (3, 3), activation='relu'))
model_m.add(MaxPooling2D(pool_size=(2, 2)))
model_m.add(Dropout(0.2))
model_m.add(Conv2D(128, (1, 1), activation='relu'))
model_m.add(MaxPooling2D(pool_size=(2, 2)))
model_m.add(Dropout(0.2))
model_m.add(Flatten())
model_m.add(Dense(128, activation='relu'))
model_m.add(Dense(num_classes, activation='softmax'))
# Inspect model's layers, output shapes, number of trainable parameters
print(model_m.summary())

2. Chollet’s Architecture

model_c = Sequential()
model_c.add(Conv2D(32, (3, 3), input_shape=input_shape, activation='relu'))
# Note: hwchong, elitedatascience use 32 for second Conv2D
model_c.add(Conv2D(64, (3, 3), activation='relu'))
model_c.add(MaxPooling2D(pool_size=(2, 2)))
model_c.add(Dropout(0.25))
model_c.add(Flatten())
model_c.add(Dense(128, activation='relu'))
model_c.add(Dropout(0.5))
model_c.add(Dense(num_classes, activation='softmax'))
# Inspect model's layers, output shapes, number of trainable parameters
print(model_c.summary())

虽然Malireddi的架构比Chollet有更多的卷积层(Conv2D),但它的运行速度要快得多,而且结果模型要小得多。

3. Model Summaries - 模型摘要

快速浏览这两个模型的模型摘要:

model_m

Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_6 (Conv2D)            (None, 24, 24, 32)        832       
_________________________________________________________________
max_pooling2d_5 (MaxPooling2 (None, 12, 12, 32)        0         
_________________________________________________________________
dropout_6 (Dropout)          (None, 12, 12, 32)        0         
_________________________________________________________________
conv2d_7 (Conv2D)            (None, 10, 10, 64)        18496     
_________________________________________________________________
max_pooling2d_6 (MaxPooling2 (None, 5, 5, 64)          0         
_________________________________________________________________
dropout_7 (Dropout)          (None, 5, 5, 64)          0         
_________________________________________________________________
conv2d_8 (Conv2D)            (None, 5, 5, 128)         8320      
_________________________________________________________________
max_pooling2d_7 (MaxPooling2 (None, 2, 2, 128)         0         
_________________________________________________________________
dropout_8 (Dropout)          (None, 2, 2, 128)         0         
_________________________________________________________________
flatten_3 (Flatten)          (None, 512)               0         
_________________________________________________________________
dense_5 (Dense)              (None, 128)               65664     
_________________________________________________________________
dense_6 (Dense)              (None, 10)                1290      
=================================================================
Total params: 94,602
Trainable params: 94,602
Non-trainable params: 0

model_c

Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_4 (Conv2D)            (None, 26, 26, 32)        320       
_________________________________________________________________
conv2d_5 (Conv2D)            (None, 24, 24, 64)        18496     
_________________________________________________________________
max_pooling2d_4 (MaxPooling2 (None, 12, 12, 64)        0         
_________________________________________________________________
dropout_4 (Dropout)          (None, 12, 12, 64)        0         
_________________________________________________________________
flatten_2 (Flatten)          (None, 9216)              0         
_________________________________________________________________
dense_3 (Dense)              (None, 128)               1179776   
_________________________________________________________________
dropout_5 (Dropout)          (None, 128)               0         
_________________________________________________________________
dense_4 (Dense)              (None, 10)                1290      
=================================================================
Total params: 1,199,882
Trainable params: 1,199,882
Non-trainable params: 0

底线Total params是尺寸差异的主要原因:Chollet的1,199,882比Malireddi的94,602多12.5倍。 而这正是模型尺寸的差异:4.8MB对380KB。

Malireddi的模型有三个Conv2D图层,每个图层后跟一个MaxPooling2D图层,它将图层的宽度和高度减半。 这使得第一个密集层的参数数量远远小于Chollet,并解释了为什么Malireddi的模型要小得多并且训练速度要快得多。 卷积层的实现是高度优化的,因此额外的卷积层提高了准确性,而不会增加训练时间。 但较小的致密层比Chollet的运行速度快得多。

在等待下一步完成运行时,我将在Explanations部分告诉您有关图层,输出形状和参数编号的信息。

后记

本篇主要讲述了使用Keras和Core ML开始机器学习,感兴趣的给个赞或者关注~~~

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

推荐阅读更多精彩内容