使用Tensorflow实现线性回归

欢迎来我的个人Blog获得更好的阅读体验。

Tensorflow基础

Tensor

1.正如名称所示,TensorFlow 这一框架定义和运行涉及张量的计算。张量是对矢量和矩阵向潜在的更高维度的泛化。TensorFlow 在内部将张量表示为基本数据类型的 n 维数组。

2.tf.Tensor 具有以下属性:

  • 数据类型(例如 float32int32string

  • 形状

3.张量中的每个元素都具有相同的数据类型,且该数据类型一定是已知的。形状,即张量的维数和每个维度的大小,可能只有部分已知。如果其输入的形状也完全已知,则大多数操作会生成形状完全已知的张量,但在某些情况下,只能在执行图时获得张量的形状。

4.如何按索引取多个值?

使用gather_nd方法


a = tf.constant([[1,2],[3,4],[5,6]])

b = tf.Variable([[1],[2]])

tf.gather_nd(a, b)

OUT:


<tf.Tensor: id=11677, shape=(2, 2), dtype=int32, numpy=

array([[3, 4],

[5, 6]])>

Eager Execution

1.如何在enable_eager_execution下求梯度?

导入import tensorflow.contrib.eager as tfe,然后使用tfe.gradients_function()进行求梯度。


def square(x, y):

return tf.multiply(x, x) + y

grad = tfe.gradients_function(square)

grad(2., 2.)

OUT:


[<tf.Tensor: id=595913, shape=(), dtype=float32, numpy=4.0>,

<tf.Tensor: id=595910, shape=(), dtype=float32, numpy=1.0>]

2.如何使用batch数据集?


batched_dataset = tf.data.Dataset.from_tensor_slices((X, y)).batch(batch_size)

iterator = batched_dataset.make_one_shot_iterator()

for X, y in iterator:

do_something

线性回归

  1. 模型训练三要素:训练数据、损失函数、优化算法。

  2. 在线性回归中,o的计算依赖于 x_1x_2。也就是说,输出层中的神经元和输入层中各个输入完全连接。因此,这里的输出层又叫全连接层(fully-connected layer)或稠密层(dense layer)。

  3. 在训练模型的时候,我们需要遍历数据集并不断读取小批量数据样本。这里我们定义一个函数:它每次返回batch_size(批量大小)个随机样本的特征和标签。也就是说batch_size即为mini梯度下降的每次的数据量。

导包


import numpy as np

import tensorflow as tf

import tensorflow.contrib.eager as tfe

%matplotlib inline

from IPython import display

from matplotlib import pyplot as plt

import random

tf.enable_eager_execution() # 开启Eager Execution模式

手撸线性回归

构建数据集

我们构造一个简单的人工训练数据集,它可以使我们能够直观比较学到的参数和真实的模型参数的区别。设训练数据集样本数为1000,输入个数(特征数)为2。给定随机生成的批量样本特征\boldsymbol{X} \in \mathbb{R}^{1000 \times 2},我们使用线性回归模型真实权重\boldsymbol{w} = [2, -3.4]^\top和偏差b=4.2,以及一个随机噪声项ϵ来生成标签

\boldsymbol{y} = \boldsymbol{X}\boldsymbol{w} + b + \epsilon

其中噪声项ϵ服从均值为0、标准差为0.01的正态分布。噪声代表了数据集中无意义的干扰。下面,让我们生成数据集。


num_inputs = 2

num_examples = 1000

true_w = [2, -3.4]

true_b = 4.2

features = tf.random.normal(shape=(num_examples, num_inputs), stddev = 1)

labels = true_w[0] * features[:,0] + true_w[1] * features[:,1] + true_b

labels += tf.random.normal(stddev=0.01, shape=labels.shape) # 添加噪音

注意,features的每一行是一个长度为2的向量,而labels的每一行是一个长度为1的向量(标量)。


features[0], labels[0]

OUT:


(<tf.Tensor: id=10879, shape=(2,), dtype=float32, numpy=array([-0.7999102 ,  0.21825652], dtype=float32)>,

<tf.Tensor: id=10883, shape=(), dtype=float32, numpy=1.8640134>)

接下来我们可以画图来更直观的看出他们的线性关系


def use_svg_display():

    # 用矢量图显示

    display.set_matplotlib_formats('svg')

def set_figsize(figsize=(3.5, 2.5)):

    use_svg_display()

    # 设置图的尺寸

    plt.rcParams['figure.figsize'] = figsize


set_figsize()

plt.scatter(features[:, 1].numpy(), labels.numpy(), 1);  # 加分号只显示图

1.png

读取数据

在训练模型的时候,我们需要遍历数据集并不断读取小批量数据样本。这里我们定义一个函数:它每次返回batch_size(批量大小)个随机样本的特征和标签。


def data_iter(batch_size, features, labels):

    num_examples = len(features)

    indices = list(range(num_examples))

    random.shuffle(indices)  # 样本的读取顺序是随机的

    for i in range(0, num_examples, batch_size):

        j = tf.Variable(indices[i: min(i + batch_size, num_examples)])

        j = tf.reshape(j, [-1,1])

        yield tf.gather_nd(features, j), tf.gather_nd(labels, j)

让我们读取第一个小批量数据样本并打印。每个批量的特征形状为(10, 2),分别对应批量大小和输入个数;标签形状为批量大小。


batch_size = 10

for X, y in data_iter(batch_size, features, labels):

    print(X, y)

    break

OUT:


tf.Tensor(

[[-0.227834    0.9596326 ]

[ 0.34192654 -0.66422266]

[ 0.3691396  -0.18605368]

[-1.1413608  0.8678634 ]

[ 0.58045554  0.3434902 ]

[-0.5410006  -0.42822808]

[ 0.9343072  1.4792926 ]

[-0.46756336 -0.6927819 ]

[ 0.08125348 -1.2476276 ]

[ 0.14274137 -0.79248554]], shape=(10, 2), dtype=float32) tf.Tensor(

[ 0.48526025  7.1483665  5.5596623  -1.0285498  4.194294    4.563314

  1.0244439  5.611564    8.604012    7.180201  ], shape=(10,), dtype=float32)

初始化模型参数

我们将权重初始化成均值为0、标准差为0.01的正态随机数,偏差则初始化成0。


w = tf.random.normal(stddev=0.01, shape=(num_inputs, 1))

b = tf.zeros(shape=(1,))

定义损失函数

我们使用平方误差作为损失函数


def squared_loss(X, w, b, y):

    y_pred = tf.matmul(X, w) + b

    return tf.reduce_sum((y_pred - tf.reshape(y, shape=y_pred.shape)) ** 2) / 2

定义优化算法

我们使用小批量随机梯度下降来定义优化算法,其实就是除了一个batch_size


def sgd(param, grad, lr, batch_size):

    param = param - lr * grad / batch_size

    return param

训练模型

我们训练三个epoch,定义学习率为0.03batch_size10,使用tfe.gradients_function来求取梯度


num_epochs = 3

lr = 0.03

batch_size = 10

print("Initial loss: {:.3f}".format(squared_loss(X, w, b, y)))

for epoch in range(num_epochs):  # 训练模型一共需要num_epochs个迭代周期

    for X, y in data_iter(batch_size, features, labels):

        grad = tfe.gradients_function(squared_loss)

        grad_result = grad(X, w, b, y)

        w = sgd(w, grad_result[1], lr, batch_size)

        b = sgd(b, grad_result[2], lr, batch_size)

    train_l = squared_loss(X, w, b, y)

    print('epoch %d, loss %f' % (epoch + 1, train_l.numpy()))

OUT:


Initial loss: 170.409

epoch 1, loss 0.397634

epoch 2, loss 0.000867

epoch 3, loss 0.000623

我们来观察一下训练后的参数,可以发现他们是很接近的


true_w, w


([2, -3.4], <tf.Tensor: id=2451472, shape=(2, 1), dtype=float32, numpy=

array([[ 1.9991914],

        [-3.399644 ]], dtype=float32)>)


true_b, b


(4.2,

<tf.Tensor: id=2451477, shape=(1,), dtype=float32, numpy=array([4.1997643], dtype=float32)>)

线性回归更简洁的实现

同样,我们定义好参数


num_epochs = 3

lr = 0.02

batch_size = 10

定义模型,这里注意要把wb都要定义成Variable,不然后面的梯度下降优化无法更新权值。


class Model(tf.keras.Model):

    def __init__(self):

        super(Model, self).__init__()

        self.W = tf.Variable(tf.random.normal(stddev=0.01, shape=(num_inputs, 1)), name="weight")

        self.B = tf.Variable(0., name='bias')

    def call(self, inputs):

        return tf.matmul(inputs, self.W) + self.B

定义损失函数,这里注意y_pred的维度要与targets的维度相同,不然相减可能会出问题,影响后面的结果,可能导致算法无法收敛。


def loss(model, inputs, targets):

    y_pred = model(inputs)

    error = y_pred - tf.reshape(targets, shape=y_pred.shape)

    return tf.reduce_mean(tf.pow(error, 2)) / 2

定义梯度


def grad(model, inputs, targets):

    with tf.GradientTape() as tape:

        loss_value = loss(model, inputs, targets)

    return tape.gradient(loss_value, [model.W, model.B])

初始化模型与优化方法,这里我们就用普通的梯度下降优化器


model = Model()

optimizer = tf.train.GradientDescentOptimizer(learning_rate=lr)

下面是用两种方法来做实验,一种是带batch_size的小批量随机梯度下降,一种是不带batch_size的批量梯度下降。

小批量:

这里使用到了Dataset,可以直接读取数据,然后使用batch方法生成一个批处理后的数据集,然后使用make_one_shot_iterator生成一个迭代器,通过此对象,可以一次访问数据集中的一个元素。


print("Initial loss: {:.3f}".format(loss(model, features, labels)))

batched_dataset = tf.data.Dataset.from_tensor_slices((features, labels)).batch(batch_size)

for epoch in range(1, num_epochs + 1):

    iterator = batched_dataset.make_one_shot_iterator()

    for X, y in iterator:

        grads = grad(model, X, y)

        optimizer.apply_gradients(zip(grads, [model.W, model.B]))

#    if epoch % 20 == 0:

    print("Loss at epoch {:03d}: {:.8f}".format(epoch, loss(model, features, labels)))

print("Final loss: {:.3f}".format(loss(model, features, labels)))

print("W = {}, B = {}".format(model.W.numpy(), model.B.numpy()))

OUT:


Initial loss: 16.771

Loss at epoch 001: 0.28207895

Loss at epoch 002: 0.00484521

Loss at epoch 003: 0.00013049

Final loss: 0.000

W = [[ 1.9972771]

[-3.3913243]], B = 4.190994739532471

批量:


print("Initial loss: {:.3f}".format(loss(model, features, labels)))

for i in range(200):

    grads = grad(model, features, labels)

    optimizer.apply_gradients(zip(grads, [model.W, model.B]),

                            global_step=tf.train.get_or_create_global_step())

    if i % 20 == 0:

        print("Loss at step {:03d}: {:.8f}".format(i, loss(model, features, labels)))

print("Final loss: {:.3f}".format(loss(model, features, labels)))

print("W = {}, B = {}".format(model.W.numpy(), model.B.numpy()))

OUT:


Initial loss: 16.791

Loss at step 000: 16.11963463

Loss at step 020: 7.13295412

Loss at step 040: 3.15785861

Loss at step 060: 1.39871728

Loss at step 080: 0.61985403

Loss at step 100: 0.27484268

Loss at step 120: 0.12194006

Loss at step 140: 0.05414332

Loss at step 160: 0.02406745

Loss at step 180: 0.01071854

Final loss: 0.005

W = [[ 1.9749271]

[-3.3363655]], B = 4.128057956695557

线性回归更更简洁的实现

这里我们使用到Tensorflow的最新科技Estimator,一种可极大地简化机器学习编程的高阶 TensorFlow API。

从官方给的图我们就可以看出有多么的高阶,可以说是顶峰了。

[图片上传失败...(image-1f118e-1557193297372)]

编写一个或多个数据集导入函数

例如,您可以创建一个函数来导入训练集,并创建另一个函数来导入测试集。每个数据集导入函数都必须返回两个对象:

  • 一个字典,其中键是特征名称,值是包含相应特征数据的张量(或 SparseTensor)

  • 一个包含一个或多个标签的张量

例如,以下代码展示了输入函数的基本框架:


f0 = features[:,0].numpy()

f1 = features[:,1].numpy()

这里为什么要分开写呢?我到现在也不知道,希望知道的大神可以滴我一下。


def input_fn():

    feature_dict = {}

    feature_dict["w0"] = f0

    feature_dict["w1"] = f1

    return feature_dict, labels.numpy()

输入函数可以以您需要的任何方式生成 features 字典和 label 列表。不过,我们建议使用 TensorFlow 的 Dataset API,它可以解析各种数据。概括来讲,Dataset API 包含下列类:


def dataset_input_fn():

    feature_dict = {}

    feature_dict["w0"] = f0

    feature_dict["w1"] = f1

    dataset = tf.data.Dataset.from_tensors((feature_dict, labels.numpy()))

    return dataset.shuffle(2000).repeat()

dataset_input_fn是用了没有切片的结果


def dataset_slices_input_fn():

    feature_dict = {}

    feature_dict["w0"] = f0

    feature_dict["w1"] = f1

    dataset = tf.data.Dataset.from_tensor_slices((feature_dict, labels.numpy()))

    return dataset.shuffle(100).repeat().batch(batch_size)

dataset_slices_input_fn是用了切片的结果

这两种的区别就是dataset_slices_input_fn函数会利用 tf.data.Dataset.from_tensor_slices函数创建一个代表数组切片的 tf.data.Dataset。系统会在第一个维度内对该数组进行切片。例如,一个包含MNIST训练数据的数组的形状为 (60000, 28, 28)。将该数组传递给from_tensor_slices 会返回一个包含 60000 个切片的 Dataset对象,其中每个切片都是一个 28x28 的图像。而这里就是每一行。

这里有个坑,就是使用了from_tensor_slices,如果不用batch直接训练会导致


ValueError: Feature (key: w0) cannot have rank 0. Give: Tensor("IteratorGetNext:0", shape=(), dtype=float32)

报错。

上面两个函数用到了几个对Dataset的操作:

  • shuffle 方法使用一个固定大小的缓冲区,在条目经过时随机化处理条目。在这种情况下,buffer_size 大于 Dataset 中样本的数量,确保数据完全被随机化处理。

  • repeat 方法会在结束时重启 Dataset。要限制周期数量,请设置 count 参数。

  • batch 方法会收集大量样本并将它们堆叠起来以创建批次。这为批次的形状增加了一个维度。新的维度将添加为第一个维度。

定义特征列

每个 tf.feature_column 都标识了特征名称、特征类型和任何输入预处理操作。例如,这里就要按照上面input_fn的返回值的feature_dict的键名来命名特征名。


w0 = tf.feature_column.numeric_column('w0')

w1 = tf.feature_column.numeric_column('w1')

实例化相关的预创建的 Estimator

例如,下面是对名为 LinearRegressor的预创建 Estimator 进行实例化的示例代码:


estimator = tf.estimator.LinearRegressor(

      feature_columns=[w0, w1],

      optimizer=tf.train.GradientDescentOptimizer(

          learning_rate=0.0001,

    ))

调用训练、评估或推理方法。

1.使用input_fn()函数


estimator.train(input_fn=lambda:input_fn(), steps=200)

OUT:


INFO:tensorflow:Calling model_fn.

INFO:tensorflow:Done calling model_fn.

INFO:tensorflow:Create CheckpointSaverHook.

INFO:tensorflow:Graph was finalized.

INFO:tensorflow:Running local_init_op.

INFO:tensorflow:Done running local_init_op.

INFO:tensorflow:Saving checkpoints for 0 into C:\Users\ADMINI~1\AppData\Local\Temp\2\tmpix5nedss\model.ckpt.

INFO:tensorflow:loss = 33480.66, step = 1

INFO:tensorflow:global_step/sec: 534.187

INFO:tensorflow:loss = 0.09687384, step = 101 (0.187 sec)

INFO:tensorflow:Saving checkpoints for 200 into C:\Users\ADMINI~1\AppData\Local\Temp\2\tmpix5nedss\model.ckpt.

INFO:tensorflow:Loss for final step: 0.09687384.

最终loss为0.09687384.

2.使用dataset_input_fn()函数


estimator.train(input_fn=lambda:dataset_input_fn(), steps=200)

OUT:


INFO:tensorflow:Calling model_fn.

INFO:tensorflow:Done calling model_fn.

INFO:tensorflow:Create CheckpointSaverHook.

INFO:tensorflow:Graph was finalized.

INFO:tensorflow:Running local_init_op.

INFO:tensorflow:Done running local_init_op.

INFO:tensorflow:Saving checkpoints for 0 into C:\Users\ADMINI~1\AppData\Local\Temp\2\tmpt_lj1x_h\model.ckpt.

INFO:tensorflow:loss = 33480.66, step = 1

INFO:tensorflow:global_step/sec: 534.187

INFO:tensorflow:loss = 0.09687384, step = 101 (0.187 sec)

INFO:tensorflow:Saving checkpoints for 200 into C:\Users\ADMINI~1\AppData\Local\Temp\2\tmpt_lj1x_h\model.ckpt.

INFO:tensorflow:Loss for final step: 0.09687384.

最终loss为0.09687384.

input_fn()函数结果一样,因为我们都没有做任何改变,只是用了官方建议的DatasetAPI去封装了我们的数据集。

3.使用dataset_slices_input_fn()函数

这里我们要调整学习率,因为使用的是小批量梯度下降,所以学习率要高一点,不然收敛很慢。

重新定义estimator


estimator = tf.estimator.LinearRegressor(

      feature_columns=[w0, w1],

      optimizer=tf.train.GradientDescentOptimizer(

          learning_rate=0.02,

    ))

训练


estimator.train(input_fn=lambda:dataset_slices_input_fn(), steps=200)

OUT:


INFO:tensorflow:Calling model_fn.

INFO:tensorflow:Done calling model_fn.

INFO:tensorflow:Create CheckpointSaverHook.

INFO:tensorflow:Graph was finalized.

INFO:tensorflow:Running local_init_op.

INFO:tensorflow:Done running local_init_op.

INFO:tensorflow:Saving checkpoints for 0 into C:\Users\ADMINI~1\AppData\Local\Temp\2\tmpli_pz5wh\model.ckpt.

INFO:tensorflow:loss = 207.14127, step = 1

INFO:tensorflow:global_step/sec: 493.095

INFO:tensorflow:loss = 0.0006978577, step = 101 (0.218 sec)

INFO:tensorflow:Saving checkpoints for 200 into C:\Users\ADMINI~1\AppData\Local\Temp\2\tmpli_pz5wh\model.ckpt.

INFO:tensorflow:Loss for final step: 0.000740177.

最终loss为0.000740177.

效果果然好了很多

小trick

无意中发现,estimator有个牛逼的功能,就是能够断点续训,当然他不叫断点,官方的说法是Checkpoints,翻译过来叫做检查点,这真的太秀了,拿上面的dataset_slices_input_fn()举个例子:

我故意把学习率调低


estimator = tf.estimator.LinearRegressor(

      feature_columns=[w0, w1],

      optimizer=tf.train.GradientDescentOptimizer(

          learning_rate=0.0001,

    ))

然后训练


estimator.train(input_fn=lambda:dataset_slices_input_fn(), steps=200)

OUT:


INFO:tensorflow:Calling model_fn.

INFO:tensorflow:Done calling model_fn.

INFO:tensorflow:Create CheckpointSaverHook.

INFO:tensorflow:Graph was finalized.

INFO:tensorflow:Running local_init_op.

INFO:tensorflow:Done running local_init_op.

INFO:tensorflow:Saving checkpoints for 0 into C:\Users\ADMINI~1\AppData\Local\Temp\2\tmplxsxz4hw\model.ckpt.

INFO:tensorflow:loss = 232.43555, step = 1

INFO:tensorflow:global_step/sec: 493.095

INFO:tensorflow:loss = 132.51915, step = 101 (0.218 sec)

INFO:tensorflow:Saving checkpoints for 200 into C:\Users\ADMINI~1\AppData\Local\Temp\2\tmplxsxz4hw\model.ckpt.

INFO:tensorflow:Loss for final step: 191.9462.

loss为191.9462.很明显,没有收敛,于是我继续执行:


estimator.train(input_fn=lambda:dataset_slices_input_fn(), steps=200)

OUT:


INFO:tensorflow:Calling model_fn.

INFO:tensorflow:Done calling model_fn.

INFO:tensorflow:Create CheckpointSaverHook.

INFO:tensorflow:Graph was finalized.

INFO:tensorflow:Restoring parameters from C:\Users\ADMINI~1\AppData\Local\Temp\2\tmplxsxz4hw\model.ckpt-200

INFO:tensorflow:Running local_init_op.

INFO:tensorflow:Done running local_init_op.

INFO:tensorflow:Saving checkpoints for 200 into C:\Users\ADMINI~1\AppData\Local\Temp\2\tmplxsxz4hw\model.ckpt.

INFO:tensorflow:loss = 78.7848, step = 201

INFO:tensorflow:global_step/sec: 457.875

INFO:tensorflow:loss = 128.2986, step = 301 (0.203 sec)

INFO:tensorflow:Saving checkpoints for 400 into C:\Users\ADMINI~1\AppData\Local\Temp\2\tmplxsxz4hw\model.ckpt.

INFO:tensorflow:Loss for final step: 70.70934.

他读取的缓存文件,继续训练了,这真的很优秀,再也不用重新训练了。

参考

《动手学深度学习》

Eager Execution

Datasets

Estimator

Premade Estimators

Checkpoints

Datasets for Estimators

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