TensorFlow 实战Google深度学习框架(第2版)第十章读书笔记

第十章:TensorFlow高层封装

* 10.1TensorFlow高层封装总览
* 10.2Keras介绍
       * 10.2.1Keras基本用法
       * 10.2.2Keras高级用法
* 10.3Estimator介绍
       * 10.3.1Estimator基本用法
       * 10.3.2Estimator自定义模型
       * 10.3.3使用数据集(DataSet)作为Estimator输入

虽然原生态的TensorFlow API可以很灵活地支持不同的神经网络结构,但是其代码相对冗长,写起来比较麻烦。为了让用户更方便快捷地实现常用的神经网络结构,不同的组织和个人为TensorFlow提供了多种高层封装。在第6章中己经简单介绍了一种TensorFlow的高层封装,并使用它实现了卷积神经网络。在这一章中将更加详细地介绍儿种最常用的TensorFlow高层封装。因为TensorFlow的高层封装有很多,所以在10.1节中将先列举一些相对常用的高层API,并介绍它们各自的特点和共性。在后面的10.2和10.3节中将重点介绍使用最广的高层封装Keras和Google官方推荐的Estimatoror。

-10.1- TensorFlow高层封装总览

目前比较主流的TensorFlow高层封装主要有4个,分别是TensorFlow-SlimTFLearnKerasEstimator

TensorFlow-Slim

TensorFlow-Slim是Google官方给出的相对较早的TensorFlow高层封装,Google通过TensorFlow-Slim开源了一些己经训练好的图像分析的模型,所以目前在图像识别问题中TensorFlow-Slim仍被较多地使用。
以下代码给出了一个简单的样例,介绍了如何使用TensorFlow-Slim在MNIST数据集上实现LeNet-5模型:

import tensorflow as tf 
import tensorflow.contrib.slim as slim
import numpy as np 

from tensorflow.examples.tutorials.mnist import input_data 

# 通过TensorFlow-Slim来定义LeNet-5的网络结构。
def lenet5(inputs):
    # 将输入数据转化为一个4维数组,
    # 其中第一维表示batch大小,另三维表示一张图片。 
    inputs = tf.reshape(inputs, [-1, 28, 28, 1])
    # 定义第一层卷积层。
    # 从下面的代码可以看到通过TensorFlow-Slim定义的网络结构并不需要用户关心如何声明和初始化变量,而只需要定义网络结构即可。

    # 下一行代码中定了一个卷积层,该卷积层的深度为32,过滤器的大小为5*5,使用全0填充。
    net = slim.conv2d(inputs, 32, [5, 5], padding='SAME', scope='layer1-conv')

    # 定义一个最大池化层,其过滤器大小为2*2,步长为2。
    net = slim.max_pool2d(net, 2, stride=2, scope='layer2-max-pool')

    # 类似地定义其他网络层结构。
    net = slim.conv2d(net, 64, [5, 5], padding='SAME', scope='layer3-conv')
    net = slim.max_pool2d(net, 2, stride=2, scope='layer4-max-pool')

    # 直接使用TensorFlow-Slim封装好的flatten函数将4维矩阵转化为2维,这样可以方便后面的全连接层的计算。
    # 通过封装好的函数,用户不再需要自己计算通过卷积层之后矩阵的大小。
    net = slim.flatten(net, scope='flatten')

    # 通过TensorFlow-Slim定义全连接层,该全连接层有500个隐藏节点。
    net = slim.fully_connected(net, 500, scope='layer5')
    net = slim.fully_connected(net, 10, scope='output')
    return net 

# 通过TensorFlow-Slim定义网络结构,并使用之前章节中给出的方式训练呢定义好的模型。
def train(mnist):
    # 定义输入。
    x = tf.placholder(tf.float32, [None, 784], name='x-input')
    y_ = tf.placeholder(tf.float32, [None, 10], name='y-input')

    # 使用TensorFlow-Slim定义网络结构。
    y = lenet5(x)

    # 定义损失函数和训练方法。
    cross_entropy = tf.nn.sparse_softmax_cross_entropy_with_logits(
            logits=y, 
            labels=tf.argmax(y_, 1)
        )
    loss = tf.reduce_mean(cross_entropy)
    train_op = tf.train.GradientDescentOptimizer(0.01).minimize(loss)

    # 训练过程
    with tf.Session() as sess:
        tf.global_variables_initializer().run()
        for i in range(10000):
            xs, ys = mnist.train.next_batch(100)
            _, loss_value = sess.run(
                    [train_op, loss], 
                    feed_dict={x:xs, y_:ys}
                )
            for i % 1000 == 0:
                print("After %d training step(s), loss on training batch is %g." % (i, loss_value))

def main(argv=None):
    mnist=input_data.read_data_sets("/path/to/MNIST_data", one_hot=True)
    train(mnist)

if __name__ == "__main__":
    main()

"""
运行以上代码可以得到类似以下的结果:
After 0 training step(s), loss on training batch is 2.30562.
After 1000 training step(s), loss on training batch is 0.783361.
After 2000 training step(s), loss on training batch is 0.737067.
After 3000 training step(s), loss on training batch is 0.767944.
After 4000 training step(s), loss on training batch is 0.553031.
...
After 8000 training step(s), loss on training batch is 0.195572.
After 9000 training step(s), loss on training batch is 0.276471.
"""

从以上代码可以看出,TensorFlow-Slim主要的作用是使模型定义更加简洁,基本上每一层网络可以通过一句话来实现。除了对单层网络结构,TensorFlow-Slim还对数据预处理、损失函数、学习过程、测试过程等都提供了高层封装。不过因为TensorFlow-Slim的这些封装使用得并不广泛,所以本书不做详细介绍,感兴趣的读者可以参考GitHub上TensorFlow-Slim的代码库。TensorFlow-Slim最特别的一个地方是它对一些标准的神经网络模型进行了封装,比如VGG、Inception以及ResNet,而且Google开源的训练好的图像分类模型基本都是通过TensorFlow-Slim实现的。第6章介绍迁移学习时已经使用过通过TensorFlow-Slim定义的Inception-v3模型了。更加详细的、通过TensorFlow-Slim开源的训练好的模型列表可以在GitHub上找到。

TFLearn

与TensorFlow-Slim相比,TFLearn是一个更加简洁的TensorFlow高层封装。通过TFLearn可以更加容易地完成模型定义、模型训练以及模型评测的全过程。TFLearn没有集成在TensorFlow的安装包中,故需要单独安装。通过以下命令就可以安装TFLearn:

pip install tflearn

安装完TFLearn之后就可以通过TFLearn来实现神经网络了。以下代码展示了如何使用TFLearn在MNIST数据集上实现LeNet-5模型。

# -*- coding: utf-8 -*- 

import tflearn
from tflearn.layers.core import input_data, fully_connected
from tflearn.layers.conv import conv_2d, max_pool_2d
from tflearn.layers.estimator import regression
import tflearn.dataset.mnist as mnist

# 获取MNIST数据。
trainX, trainY, testX, testY = mnist.load_data(
        data_dir="/path/to/MNIST_data", 
        one_hot=True
    ) 

# 将图像数据reshape成卷积神经网络输入的格式。
trainX = train.reshape([-1, 28, 28, 1])
testX = testX.reshape([-1, 28, 28, 1])

# 构建神经网络,这个过程和TensorFlow-Slim比较类似。
# input_data定义了一个placeholder来接入数据。
net = input_data(shape=[None, 28, 28, 1], name='input')
# 通过TFLearn封装好的API定义一个深度为5,过滤器为5*5,激活函数为ReLU的卷积层。
net = conv_2d(net, 32, 5, activation='relu')

# 定义一个过滤器为2*2的最大池化层。
net = max_pool_2d(net, 2)

# 类似地,定义其他的网络层结构。
net = conv_2d(net, 64, 5, activation='relu')
net = max_pool_2d(net, 2)
net = fully_connected(net, 500, activation='relu')
net = fully_connected(net, 10, activation='softmax')

# 使用TFLearn封装好的函数定义学习任务。
# 指定优化器为SGD,学习率为0.01,损失函数为交叉熵。
net = regression(net, 
        optimizer='sgd', 
        learning_rate=0.01, 
        loss='categorical_crossentropy'
    )

# 通过定义网络结构训练模型,并在指定的验证数据上验证模型的效果。
# TFLearn将模型的训练过程封装到了一个类中,这样可以减少非常多的冗余代码。
model = tflearn.DNN(net, tensor board_verbose=0)
model.fit(
        trainX, trainY, 
        n_epoch=20, 
        validation_set=([testX, testY]), 
        show_metric=True
)

"""
运行以上代码可以得到类似以下的结果:
--------------------------------------------------- 
Run id: 61VRFQ
Log directory: /tmp/tflearn_logs/
--------------------------------------------------- 
Training samples: 55000
Validation samples: 10000
-- 
Training Step: 860 | total loss: 0.27329 | times: 185.113s | SGD | epoch: 001 | loss: 0.27329 - acc: 0.9316 | val_loss: 0.26857 - val_acc: 0.9163 -- iter: 55000/55000
--- 
Training Step: 1720 | total loss: 0.20611 | times: 170.847s | SGD | epoch: 002 | loss: 0.20611 - acc: 0.9475 | val_loss: 0.15714 - val_acc: 0.9527 -- iter: 55000/55000
...
--
Training Step: 17200 | total loss: 0.44106 | times: 189.344s | SGD | epoch: 020 | loss: 0.44106 - acc: 0.9689 | val_loss: 0.03203 - val_acc: 0.9898 -- iter: 55000/55000
"""

从以上代码可以看出,使用TFLearn训练神经网络的流程也是一样的:先定义神经网络的结构,再使用训练数据来训练模型。与原生态TensorFlow不同的地方在于,TFLearn不仅使神经网络结构定义更加简洁,还将模型训练的过程也进行了封装。另外,在定义神经网络的前向传播过程之后,TFLearn可以通过regression()函数来指定损失函数和优化方法。更方便的是,不仅TFLearn能很好地封装模型定义,tflearn.DNN()也能很好地封装模型训练的过程。通过fit()函数可以指定训练中使用的数据和训练的轮数。这样避免了大量的冗余代码。

因为篇幅关系,本章不再详细介绍TFLearn的复杂使用方法,感兴趣的读者可以参考TFLearn官网(http://tflearn.org/)上的相关内容。与TFLearn类似,KerasEstimator在封装的方式上基本和TFLearn一致,主要也是针对模型定义和模型训练两个部分。与TFLearn不同的是,Keras和Estimator都己经加入了TensorFlow代码库,而且它们是使用最为广泛的TensorFlow高层封装。下面两节将更加详细地介绍Keras和Estimator的使用方法。

-10.2- Keras介绍

Keras是目前使用最为广泛的深度学习工具之一,它的底层可以支持TensorFlow、MXNet、CNTK和Theano。如今,Keras更是被直接引入了TensorFlow的核心代码库,成为TensorFlow官方提供的高层封装之一。

-10.2.1- Keras基本用法

本小节中将首先介绍最基本的KerasAPI,并给出一个简单的样例。

和TFLearnAPI类似,KerasAPI也对模型定义、损失函数、训练过程等进行了封装,而且封装之后的整个训练过程和TFLearn是基本一致的,可以分为数据处理、模型定义和模型训练三个部分。

使用原生态的KerasAPI需要先安装Keras包,安装的方法如下:

pip install keras

以下代码展示了如何使用原生态Keras在MNIST数据集上实现LeNet-5模型。

# -*- coding:utf-8 -*-

import keras 
from keras.datasets import mnist 
from keras.model import Sequential
from keras.layers import Dense, Flatten, Con2D, MaxPooling2D
from keras import backend as K

num_class = 10 
img_rows, img_cols = 28, 28 

# 通过Keras封装好的API加载MNIST数据。
# 其中trainX就是一个60000*28*28的数组,trainY是每一张图片对应的数字。
(trainX, trainY), (testX, testY) = mnist.load_data()

# 因为不同的底层(TennsorFlow或者MXNet)对输入的要求不一样,所以这里需要根据对图像编码的格式要求来设置输入输入层的格式。
if K.image_data_format() == 'channels_first':
    trainX = trainX.reshape(trainX.shape[0], 1, img_rows, img_cols)
    testX = testX.reshape(testX.shape[0], 1, img_rows, img_cols)
    # 因为MNIST中的图片是黑白的,所以第一维的取值为1。
    input_shape = (1, img_rows, img_cols)
else:
    trainX = trainX.reshape(trainX.shape[0], img_rows, img_cols, 1)
    testX = testX.reshape(testX.shape[0], img_rows, img_cols, 1)

# 将图像像素转化为0到1之间的实数。
trainX = trainX.astype('float32')
testX = testX.astype('float32')
trainX /= 255.0 
testX /= 255.0

# 将标准答案转化为需要的格式(One-hot编码)。
trainY = keras.utils.to_categorical(trainY, num_classes)
testY = keras.utils.to_categorical(testY, num_classes)

# 使用Keras API定义模型。
model = Sequential()

# 一层深度为32, 过滤器大小为5*5的卷积层。
model.add(
    Conv2D(32, kernel_size=(5, 5), activation='relu', input_shape=input_shape)
)

# 一层过滤器大小为2*2的最大池化层。
model.add(
    MaxPooling2D(pool_size=(2, 2))
)

# 一层深度为64, 过滤器大小为5*5的卷积层。
model.add(
    Conv2D(64, (5, 5), activation='relu')
)

# 一层过滤器大小为2*2的最大池化层。
model.add(
    MaxPooling2D(pool_size=(2, 2))
)

# 将卷积层的输出拉直后作为下面全连接层的输入。
model.add(
    Flatten()
)

# 全连接层,有500个节点。
model.add(
    Dense(500, activation='relu')
)

# 全连接层,得到最后的输出。
model.add(
    Dense(num_classes, activation='softmax')
)


# 定义损失函数、优化函数、测评方法。
model.compile(
        loss=keras.losses.categorical_crossentropy,
        optimizer=keras.optimizers.SGD(), 
        metrics=['accuracy']
    )

# 类似TFLearn中的训练过程,给出训练数据、batch大小、训练轮数和验证数据,Keras可以自动完成模型训练过程。
model.fit(
        trainX, 
        trainY, 
        batch_size=128,
        epoch=20,
        validation_data=(testX, testY)
    )

# 在测试数据上计算准确率。
score = model.evaluate(testX, testY)
print('Test loss: ', score[0])
print('Test accuracy: ', score[1])

"""
运行以上代码可以得到类似以下的结果:

Using TensorFlow backend.

Epoch 1/20
60000/60000 [==============================] -91s 2ms/step - loss: 1.0321 - acc: 0.7458 - val_loss: 0.3143 - val_acc: 0.9061
Epoch 2/20
60000/60000 [==============================] -97s 2ms/step - loss: 0.2609 - acc: 0.9232 - val_loss: 0.1919 - val_acc: 0.9443
Epoch 3/20
60000/60000 [==============================] -101s 2ms/step - loss: 0.1854 - acc: 0.9450 - val_loss: 0.1413 - val_acc: 0.9608
...
Epoch 20/20
60000/60000 [==============================] -81s 1ms/step - loss: 0.0456 - acc: 0.9860 - val_loss: 0.0488 - val_acc: 0.9840

10000/10000 [==============================] -5s 481us/step 
{'Test loss: ', 0.048786141543649138}
{'Test accuracy: ', 0.98399999999999999}
"""

从以上代码可以看出使用Keras API训练模型可以先定义一个Sequential类,然后在Sequential实例中通过add()函数添加网络层。Keras把卷积层、池化层、RNN结构(LSTM、GRN)、全连接层等常用的神经网络结构都做了封装,可以很方便地实现深层神经网络。在神经网络结构定义好之后,Sequential实例可以通过compile()函数,指定优化函数、损失函数以及训练过程中需要监控的指标等。Keras对优化函数、损失函数以及监控指标都有封装,同时也支持使用自定义的方式,在Keras的API文档中有详细的介绍,这里不再赘述。最后在网络结构、损失函数和优化函数都定义好之后,Sequential实例可以通过fit()函数来训练模型。类似TFLearn中的fit()函数,Keras的fit()函数只须给出训练数据、batch大小和训练轮数,Keras就可以自动完成模型训练的整个过程。

除了能够很方便地处理图像问题,Keras对于循环神经网络的支持也是非常出色的。有了KerasAPI,循环神经网络的循环体结构也可以通过简单的一句命令完成。以下代码给出了如何通过Keras实现自然语言 情感分类问题。使用循环神经网络判断语言的情感(比如在以下例子中需要判断的一个评价是好评还是差评)和自然语言建模问题类似,唯一的区别在于除了最后一个时间点的输出是有意义的,其他时间点的输出都可以忽略。图10-1展示了使用循环神经网络处理情感分析问题的模型结构。

图10-1

# -*- coding: utf-8 -*- 

from keras.preprocessing import sequence
from keras.models import Sequential
from keras.layers import Dense, Embedding
from keras.layers import LSTM
from keras.datasets import imdv

# 最多使用的单词数。
max_features = 20000
# 循环神经网络的截断长度。
maxlen = 80
batch_size = 32 

# 加载数据并将单词转化为ID,max_features给出了使用最多的单词数。
# 和自然语言模型相似,会将出现频率较低的单词替换为统一的ID。
# 通过Keras封装的API会生成25000条训练数据和25000条测试数据,每一条数据可以被看成一段话,并且每段话都有一个好评或者差评的标签。
(trainX, trainY), (testX, testY) = imdb.load_data(
        num_words=max_features
    )

print(len(trainX), 'train sequences')
print(len(testX), 'test sequences')

# 在自然语言中,每一段话的长度是不一样的,但循环神经网络的循环长度是固定的,所以这里需要先将所有段落统一成固定的长度。
# 对于长度不够的段落,要是用默认值0来填充,对于超过长度的段落则直接忽略掉超过的部分。
trainX = sequence.pad_sequeces(trainX, maxlen=maxlen)
testX = sequence.pad_sequence(testX, maxlen=maxlen)

"""
# 输出统一长度之后的数据维度:
# ('x_train shape:', (25000, 80))
# ('x_test shape:', (25000, 80))

"""

print('trainX shape:', trainX.shape)
print('testX shape:', testX.shape)

# 在完成数据预处理之后构建模型
model = Sequential()

# 构建embedding层。128代表了embedding层的向量维度。
model.add(Embedding(max_features, 128))

# 构建LSTM层。
model.add(LSTM(128, dropout=0.2, recurrent_dropout=0.2))

# 构建最后的全连接层。
# 注意上面构建LSTM层时只会得到最后一个节点的输出,如果需要输出每个时间点的结果,那么可以将recurrent_sequence参数设为True。
model.add(Dense(1, activation='sigmoid'))

# 与MNIST样例类似地,指定损失函数、优化函数、测评指标。
model.compile(
        loss='binary_crossentropy', 
        optimizer='adam', 
        metrics=['accuracy']
    )

# 与MNIST样例类似地,指定训练数据、训练轮数、batch大小、验证数据。
model.fit(
        trainX, 
        trainY, 
        batch_size=batch_size, 
        epochs=15, 
        validation_data=(testX, testY)
    )

# 在测试数据上评测模型。
score = model.evaluate(
        testX, 
        testY, 
        batch_size=batch_size
    )
print('Test loss: ', score[0])
print('Test accuracy: ', score[1])

"""
运行上面代码可以得到类似以下的输出:
Train on 25000 samples, validate on 25000 samples
Epoch 1/15
25000/25000[==============================] - 247s 10ms/step - loss: 0.4645 - acc:0.7778 - val_loss: 0.3881 - val_acc: 0.8267
Epoch 1/15
25000/25000[==============================] - 231s 9ms/step - loss: 0.2980 - acc:0.8780 - val_loss: 0.3904 - val_acc: 0.8348
...
Epoch 1/15
25000/25000[==============================] - 167s 7ms/step - loss: 0.0126 - acc:0.9964 - val_loss: 1.0459 - val_acc: 0.8098

25000/25000[==============================] - 23s 921us/step 
('Test score: ', 1.0458565827691555)
('Test accuracy: ', 0.80984)
"""

以上两个样例针对Keras的基本用法做了详细的介绍。虽然通过Keras的封装,很多经典的神经网络结构能够很快被实现,不过要实现一些更加灵活的网络结构、损失函数或者数据输入方法,就需要对Keras的高级用法有更多的了解。10.2.2小节中将对这些话题做更加深入的介绍。

-10.2.2- Keras高级用法

本小节中将介绍如何使用Keras定义更加复杂的模型以及如何将Keras和原生态TensorFlow结合起来。

在10.2.1小节中一个最重要的封装就是Sequential类,所有的神经网络模型定义和训练都是通过Sequential实例来实现的。然而,从这个类的名称可以看出,它只支持顺序模型的定义。类似Inception这样的模型结构,通过Sequential类就不容易直接实现了。为了支持更加灵活的模型定义方法,Keras支持以返回值的形式定义网络层结构。以下代码展示了如何使用这种方式定义模型。

# -*- coding: utf-8 -*- 

import keras 
from keras.datasets import mnist
from keras.layers import Input, Dense
from keras.models import Model 

# 使用10.2.1小节中介绍的类似方法生成trainX、trainY、testX、testY,唯一的不同是这里只用了全连接层,所以不需要将输入数据整理成三维矩阵。
...

# 定义输入,这里指定的维度不用考虑batch大小。
inputs = Input(shape=(784,))

# 定义一层全连接层,该层有500隐藏节点,使用ReLU激活函数。
# 这一层的输入为inputs。
x = Dense(500, activation='relu')(inputs)

# 定义输出层。
# 注意因为Keras封装的categorical_crossentropy并没有将神经网络的输出再经过一层softmax,所以这里需要指定softmax作为激活函数。
predictions = Dense(10, activation='softmax')(x)

# 通过与10.2.1中类似的方法定义损失函数、优化函数、测评方法。
model.compile(
        loss=keras.losses.categorical_crossentropy,
        optimizer=keras.optimizer.SGD(), 
        metrics=['accuracy']
    )

# 使用与10.2.1中类似的方法训练模型。
model.fit(
        trainX, 
        trainY, 
        batch_size=128, 
        epochs=20, 
        validation_data=(testX, testY)
    )

通过这样的方式,Keras就可以实现类似Inception这样的模型结构。以下代码展示了如何通过Keras实现Inception结构。

from keras.layers import Conv2D, MaxPooling2D, Input

# 定义输入图像尺寸。
input_img = Input(shape=(256, 256, 3))

# 定义第一个分支。
tower_1 = Conv2D(64, (1, 1), padding='same', activation='relu')(input_img)
tower_1 = Conv2D(64, (3, 3), padding='same', activation='relu')(tower_1)

# 定义第二个分支。
# 与顺序模型不同,第二个分支的输入使用的是input_img,而不是第一个分支的输出。
tower_2 = Conv2D(64, (1, 1), padding='same', activation='relu')(input_img)
tower_2 = Conv2D(64, (5, 5), padding='same', activation='relu')(tower_2)

# 定义第三个分支。
# 类似地,第三个分支的输入也是inout_img。
tower_3 = MaxPooling2D((3, 3), strides=(1, 1), padding='same')(input_img)
tower_3 = Conv2D(64, (1, 1), padding='same', activation='relu')(tower_3)

# 将三个分支通过concatenate的方式拼接在一起。
output = keras.layers.concatenate([tower_1, tower_2, tower_3], axis=1)

图10-2

除了可以支持非顺序模型,Keras也可以支持有多个输入或输出的模型。以下代码实现了如图10-2所示的网络结构。图10-2是一个多输入、多输出的网络结构。输入层1含有784个节点,代表MNIST图片中784个像素。输入层2含有10个节点,代表该图片所对应的数字。输出层在预测时仅仅依赖维度为1的隐藏层,因此预测的准确度比较低;而输出层2的输入中直接包含了正确答案,因此预测的准确度很高。

# -*- coding: utf-8 -*- 

import keras
from tflearn.layers.core import fully_connected
from keras.datasets import mnist
from keras.layers import Input, Dense
from keras.model import Model 

# 类似10.2.1中的方式生成trainX、trainY、testX、testY。

# 定义两个输入,一个输入为原始的图片信息,另一个输入为正确答案。
input1 = Input(shape=(784, ), name='input1')
input2 = Input(shape=(10, ), name='input2')

# 定义一个只有一个隐藏节点的全连接网络。
x = Dense(1, activation='relu')(input)
# 定义只使用了一个隐藏节点的网络结构的输出层。
output1 = Dense(10, activation='softmax', name='output1')(x)

# 将一个隐藏节点的输出和正确答案拼接在一起,这个将作为第二个输出层的输入。
y = keras.layers.concatenate([x, input2])
# 定义第二个输出层。
output2 = Dense(10, activation='softmax', name='output2')(y)

# 定义一个有多个输入和多个输出的模型。
# 这里只需要将所有的输入和输出给出即可。
model = Model(input=[input1, input2], output=[output1, output2])

# 定义损失函数、优化函数、评测方法。
# 若多个输出的损失函数相同,可以只指定一个损失函数。
# 如果多个输出的损失函数不同,则可以通过一个列表或一个字典来指定每一个输出的损失函数。
# 比如可以使用:loss = {'output1':'binary_crossentropy', 'output2':'binary_crossentropy'} 来为不同的输出指定不同的损失函数。
# 类似地,Keras也支持为不同输出产生的损失指定权重,这可以通过loss_weight参数来完成。
# 在下面的定义中,输出output1的权重为1,output2的权重为0.1。所以这个模型会更加偏向于优化第一个输出。
model.compile(
        loss=keras.losses.categorical_crossentropy, 
        optimizer=keras.optimizer.SGD(),
        loss_weights=[1, 0.1], 
        metrics=['accuracy']
    )

# 训练模型过程。
# 因为有两个输入和输出,所以这里提供的数据也需要有两个输入和两个期待的正确答案输出。
# 通过列表的方式提供数据时,Keras会假设数据给出的顺序和定义Model类时输入输出的顺序是对应的。
# 为了避免顺序不一致导致的问题,本书更推荐使用字典形式给出:
# model.fit(
#         {'inout1': trainX, 'input2': trainY}, 
#         {'output1': trianY, 'output2': trainY}, 
#         ... )
model.fit(
        [trainX, trainY],
        [trainY, trainY],
        batch_size=128,
        epoch=20,
        validation_data=([testX, testY], [testY, testY])
    )

"""
运行以上代码可以得到类似以下的输出:
Train on 60000 samples, validate on  10000 samples
Epoch 1/20
60000/60000 [==============================] - 2s 28us/step - loss: 2.4584 - output1_loss: 2.2457 - output2_loss: 2.1266 - output1_acc: 0.1372 - output2_acc: 0.3172 - val_loss: 2.3903  - val_output1_loss: 2.1786 - val_output2_loss: 2.1168 - val_output1_acc: 0.1650 - val_output2_acc: 0.3778
Epoch 2/20
60000/60000 [==============================] - 1s 20us/step - loss: 2.3587 - output1_loss: 2.1488 - output2_loss: 2.0986 - output1_acc: 0.1714 - output2_acc: 0.3684 - val_loss: 2.3126  - val_output1_loss: 2.1056 - val_output2_loss: 2.0697 - val_output1_acc: 0.1833 - val_output2_acc: 0.3742
...
Epoch 20/20
60000/60000 [==============================] - 1s 20us/step - loss: 1.9393 - output1_loss: 1.8139 - output2_loss: 1.2537 - output1_acc: 0.2971 - output2_acc: 0.9149 - val_loss: 1.9138  - val_output1_loss: 1.7910 - val_output2_loss: 1.2288 - val_output1_acc: 0.2985 - val_output2_acc: 0.9210

"""

从以上输出可以看出Keras在训练过程中会显示每个输出层的loss和accuracy。因为输出层output1只使用了一个维度为1的隐藏节点,所以正确率只有29.85%。虽然输出层output2使用了正确答案作为输入,但是因为在损失函数中权重较低(只有0.1),所以它的收敛速度较慢,在第20个epoch时准确率也只有92.1%。如果将两个输出层的损失权重设为一样,那么输出层output1在第20个epoch时的准确率将只有27%,而输出层output2的准确率可以达到99.9%。

虽然通过返回值的方式已经可以实现大部分的神经网络模型,然而Keras API还存在两大问题。第一,原生态Keras API对训练数据的处理流程支持得不太好,基本上需要一次性将数据全部加载到内存。第二,原生态Keras API无法支持分布式训练。为了解决这两个问题,Keras提供了一种与原生态TensorFlow结合得更加紧密的方式。以下代码显示了如何将Keras和原生态TensorFlow API联合起来解决MNIST问题。

# -*- coding: utf-8 -*- 

import tensorflow as tf 
from tensorflow.examplpes.tutorials.mnist import input_data

mnist_data = input_data.read_data_sets(
        '/path/to/MNIST_data', 
        one_hot=True
    )

# 通过TensorFlow中的placeholder定义输入。
# 类似地,Keras封装的网络层结构也可以支持使用第7章中介绍的输入队列。
# 这样可以有效避免一次性加载所有数据的问题。
x = tf.placholder(tf.float32, shape=[None, 784])
y_ = tf.placholder(tf.float32, shape=[None, 10])

# 直接使用TensorFlow中提供的Keras API定义网络层结构。
net = tf.keras.layers.Dense(500, activationn='relu')(x)
y = tf.keras.layers.Dense(10, activation='relu')(net)

# 定义损失函数、优化方法。
# 注意这里可以混用Keras的API和原生态TensorFlow的API。
loss = tf.reduce_mean(tf.keras.losses.caategorical_crossentropy(y_, y))
train_step = tf.train.GradientDescentOptimizer(0.5).minimize(loss)

# 定义预测的正确率作为评价指标。
acc_value = tf.reduce_mean(tf.keras.metrics.categorical_accuracy(y_, y))

# 使用原生态TensorFlow的方式训练模型。
# 这样可以有效地实现分布式。
with tf.Session() as sess:
    tf.global_variables_initializer().run()

    for i in range(10000):
        xs, ys = mnist_data.train.nextbatch(100)
        _, loss_value = sess.run(
                [train_step, loss], 
                feed_dict={x:s, y_:ys}
            )
        if i%1000 == 0:
            print("After %d training step(s), loss on training batch is %g." % (i, loss_value))

    print(acc_value.eval(
            feed_dict={x: mnist_data.test.images, y_: mnist_data.test.labels}
        ))

"""
运行以上代码可以得到类似以下的输出:
After 0 training step(s), loss on training batch is 2.42256.
After 1000 training step(s), loss on training batch is 0.0550451.
...
After 9000 training step(s), loss on training batch is 0.00107198. 

0.9843
""'

通过和原生态TensorFlow更紧密地结合,可以使建模的灵活性进一步提高,但是同时也会损失一部分封装带来的易用性。所以在实际问题中读者可以根据需求合理地选择封装的程度。

-10.3- Estimator介绍

本节中将重点介绍广泛使用的Google官方推荐的Estimator。
除了第三方提供的TensorFlow高层封装API,TensorFlow从1.3版本开始也推出了官方支持的高层封装tf.estimator。为了引用方便,本书将tf.estimator简称为Estimator。因为Estimator是TensorFlow官方提供的高层API,所以它更好地整合了原生态TensorFlow提供的功能。

-10.3.1- Estimator基本用法

本小节将介绍如何通过TensorFlow预先定义好的Estimator来实现深层全连接神经网络,以解决MNIST问题。
类似其他的高层封装,本节先给出在MNIST数据集上,通过Estimator实现全连接神经网络的代码。

# -*- coding: utf-8 -*- 

import numpy as np 
import tensorflow as tf 
from tensorflow.examples.tutorials.mnist import input_data

# 将TensorFlow日志信息输出到屏幕。
tf.logging.set_verbosity(tf.logging.INFO)
mnist = input_data.read_data_sets("/path/to/mnist_data", one_hot=False)

# 指定神经网络的输入层。
# 所有这里指定的输入都会拼接在一起作为整个神经网络的输入。
feature_columns = [tf.feature_column.numeric_column("image", shape=[784])]

# 通过TensorFlow提供的封装好的Estimatoer定义神经网络模型。
# feature_columns参数给出了神经网络输入层需要用到的数据,
# hidden_units参数给出了神经网络的结构。
# 注意,这DNNClassifier只能定义多层全连接神经网络,而hidden_units列表中给出了每一层隐藏层的节点个数。
# n_classes给出了总共类目的数量,
# optimizer给出了使用的优化函数
# Estimator会将模型训练过程中的loss变化以及一些其他指标保存到model_dir目录下,通过TensorBoard可以可视化这些指标的变化过程。
# 图10-3展示了通过TensorBoard可视化的监控指标的结果。
estimator = tf.estimator.DNNClassifier(
        feature_columns=feature_columns,
        hidden_units=[500], 
        n_classes=10,
        optimizer=tf.train.AdamOptimizer(),
        model_dir="/path/to/log"
    )

# 定义数据输入。
# 这里x中需要给出所有的输入数据。
# 因为上面feature_columns只定义了一组输入,所以这里只需要指定一个就好。
# 如果feature_columns中指定了多个,那么这里也需要对每一个指标的输入提供数据。
# y中需要提供每一个x对应的正确答案,这里要求分类的结果是一个正整数。
# num_epoches指定了数据循环使用的轮数,比如在测试时可以将这个参数指定为1。
# batch_size指定了一个batch的大小。
# shuffle指定了是否需要对数据进行随机打乱。
train_input_fn = tf.estimator.inputs.numpy_input_fn(
        x={"image": mnist.train.images}, 
        y=mnist.train.labels.astype(np.int32), 
        num_epochs=None,
        batch_size=128, 
        shuffle=True
    )

# 训练模型。
# 注意这里没有指定损失函数,通过DNNClassifier定义的模型会使用交叉熵作为损失函数。
estimator.train(input_fn=train_input_fn, step=10000)

# 定义测试时的数据输入。
# 指定的形式和训练时的数据输入基本一致。
test_input_fn = tf.estimator.inputs.numpy_input_fn(
        x={"image": mnist.test.images}, 
        y=mnist.test.labels.astype(np.int32), 
        num_epochs=1, 
        batch_size=128, 
        shuffle=False
    )

# 通过evaluate评测训练好的模型的效果。
accuracy_score = estimator.evaluate(input_fn=test_input_fn)("accuracy")
print("\nTest accuracy : %g %%" % (accuracy_score * 100))

"""
运行以上代码可以得到类似以下的输出:
INFO: tensorflow: Using default config.
INFO: tensorflow: Using config: {
    '_save_checkpoints_secs': 600, 
    '_session_config': None, 
    '_keep_checkpoint_max': 5, 
    '_task_type': 'worker', 
    '_is_chief': True, 
    '_cluster_spec': <tensorflow.python.training.server_lib.ClusterSpec object at 0x115e03b50>,
    '_save_checkpoints_steps': None, 
    '_keep_checkpoint_every_n_hours': 10000,
    '_service': None, 
    '_num_ps_replicas': 0, 
    '_tf_random_seed': None, 
    '_master': '', 
    '_num_worker_replicas': 1, 
    '_task_id': 0, 
    '_log_step_count_steps': 100,
    '_model_dir': 'log', 
    '_save_summary_steps': 100
}
INFO: tensorflow: Saving checkpoints for 1 into log/model.ckpt.
INFO: tensorflow: loss = 301.948, step = 1 
INFO: tensorflow: global_step/sec = 85.0254 
INFO: tensorflow: loss = 21.2997, step = 101 (1.176 sec)
INFO: tensorflow: global_step/sec = 87.2444
... 
INFO: tensorflow: global_step/sec: 83.644
INFO: tensorflow: loss = 0.00975741, step = 9901 (1.196 sec)
INFO: tensorflow: Saving checkpoints for 10000 into log/model.ckpt. 
INFO: tensorflow: Loss for final step: 0.922314. 
INFO: tensorflow: Starting evaluation at 2017-11-13-19:04:57
INFO: tensorflow: Restoring parameters from log/model.ckpt-10000
INFO: tensorflow: Finished evaluation at 2017-11-13-19:04:58
INFO: tensorflow: Saving dict for global step 10000: accuracy = 0.9814, average_loss = 0.0839367, global_step = 10000, loss = 10.6249

Test accuracy: 98.14%

"""
图10-3

从以上代码可以看出,使用预先定义好的Estimator可以更加深层次地封装神经网络的定义和训练过程。在这个过程中,用户只需要关注模型的输入以及模型的结构,其他的工作都可以通过Estimator自动完成。然而预先定义好的Estimator功能有限,比如目前无法很好地实现卷积神经网络或者循环神经网络,也没有办法支持自定义的损失函数,所以为了更好地使用 Estimator,下一小节将介绍如何使用Estimator自定义模型。

-10.3.2- Estimator自定义模型

本小节中将进一步介绍如何使用自定义的Estimator模型。

若使用预先定义好的模型,除了不能灵活选择模型的结构,模型使用的损失函数和每一层使用的激活函数等也都是预先定义好的。为了更加灵活地构建模型,Estimator支持使用自定义的模型结构。以下代码展示了如何通过自定义的方式使用卷积神经网络解决MNIST问题。

# -*- coding: utf-8 -*- 

import numpy as np 
import tensorflow as tf 
from tensorflow.examples.tutorials.mnist import input_data

tf.logging.set_verbosity(tf.logging.INFO)

# 通过tf.layers来定义模型结构。
# 这里可以使用原生态TensorFlow API或者任何TensorFlow的高层封装。
# X给出了输入层张量,is_training指明了是否为训练。
# 该函数返回前向传播的结果。
def lenet(x, is_training):
    # 将输入转化为卷积层需要的形状。
    x = tf.reshape(x, shape=[-1, 28, 28, 1])

    net = tf.layers.conv2d(x, 32, 5, activation=tf.nn.relu)
    net = tf.layers.max_pooling2d(net, 2, 2)
    net = tf.layers.conv2d(net, 64, 3, activation=tf.nn.relu)
    net = tf.layers.max_pooling2d(net, 2, 2)
    net = tf.contrib.layers.flatten(net)
    net = tf.layers.dense(net, 1024)
    net = tf.layers.dropout(net, rate=0.4, training=is_training)

    return tf.layers.dense(net, 10)

# 自定义Estimator中使用的模型。
# 定义的函数有4个输入,
# features给出了在输入函数中会提供的输入层张量。
# 注意这是一个字典,字典里的内容是通过 tf.estimator.input.numpy_input_fn中x参数的内容指定的。
# labels是正确答案,这个字段的内容是通过numpy_input_fn中y参数给出的。
# mode的取值有3种可能,分别对应Estimator类的train、evaluate、predict这3个函数。
# 通过这个参数可以判断当前是否是训练过程。
# 最后params参数是一个字典,这个字典中可以给出模型相关的任何超参数(hyper-paramerter)。
# 比如学习率就可以放在params中。
def model_fn(features, labels, mode, params):
    # 定义神经网络的结构,并通过输入到前向传播的结果。
    predict = lenet(
            features["image"], 
            mode==tf.estimator.ModeKeys.TRAIN
        )

    # 如果在预测模式,那么只需要将结果返回即可。
    if mode == tf.estimator.ModeKeys.PREDICT:
        # 使用EstimatorSpec传递返回值,并通过predictions参数指定返回的结果。
        return tf.estimator.EstimatorSpec(
                mode=mode, 
                predictions=["result": tf.argmax(predict, 1)]
            )

    # 定义损失函数。
    loss = tf.reduce_mean(
            tf.nn.sparse_softmax_cross_entropy_with_logits(
                    logits=predict, 
                    labels=labels
                )
        )

    # 定义优化函数。
    optimizer = tf.train.GradientDescentOptimizer(
            learning_rate=params["learning_rate"]
        )

    # 定义训练过程。
    train_op = optimizer.minimize(
            loss=loss,
            global_step=tf.train.get_global_step()
        )

    # 定义评测标准,
    # 在运行evaluate时会计算这里定义的所有评测标准。
    eval_metric_ops = {
            "my_metric": tf.metrics.accuracy(tf.argmax(predict, 1), labels)
        }

    # 返回模型训练过程需要使用的损失函数、损失函数、评测方法。
    return tf.estimator.EstimatorSpec(   
            mode=mode, 
            loss=loss, 
            train_op=train_op, 
            eval_metric_ops=eval_metric_ops
        )

mnist = input_data.read_data_sets("/path/to/MNIST_data", one_hot=False)

# 通过自定义的方式生成Estimator类。
# 这里需要提供模型定义的函数,并通过params参数指定模型定义时使用的超参数。
model_params = {"learning_rate": 0.01}
estimator = tf.estimator.Estimator(
        model_fn=model_fn, 
        params=model_params
    )

# 和10.3.1小节中类似,训练和评测模型。
train_input_fn = tf.estimator.input.numpy_input_fn(
        x={"image": mnist.train.images}, 
        y=mnist.train.labels.astype(np.int32), 
        num_epochs=None,
        batch_size=128, 
        shuffle=True
    )
estimator.train(input_fn=train_input_fn, steps=30000)
test_input_fn = tf.estimator.inputs.numpy_input_fn(
        x={"image": mnist.test.images}, 
        y=mnist.test.labels.astype(np.int32), 
        num_epochs=1, 
        batch_size=128, 
        shuffle=False
    )
test_results = estimator.evaluate(input_fn=test_innput_fn)

# 这里使用的my_metric中的内容就是model_fn中eval_metric_ops定义的测评指标。
accuracy_score = test.result["my_metric"]
print("\nTest accuracy: %g %%" % (accuracy_score*100))

# 使用训练好的模型在新数据上测试结果。
predict_input_fn = tf.estimator.inputs.numpy_input_fn(
        x={"image": mnist.test.images[:10]}, 
        num_epochs=1, 
        shuffle=False
    )
prediction = estimator.predict(input_fn=predict_input_fn)

for i, p in enumerate(predictions):
    # 这里result就是tf.estimator.EstimatorSpec的参数predictions中指定的内容。
    # 因为这个内容是一个字典,所以Estimator可以很容易支持多输入。
    print("Prediction %s: %s" % (i+1, p["result"]))

"""
运行以上代码可以得到类似以下的输出:
INFO: tensorflow: loss = 2.30326, step=1
INFO: tensorflow: global_step/sec: 5.3973
INFO: tensorflow: loss = 1.94311, step = 101 (18.523 sec)
INFO: tensorflow: global_step/sec: 4.50934
... 
INFO: tensorflow: loss = 0.0121069, step = 29901 (15.266 sec)
INFO: tensorflow: Saving checkpoints for 30000 into /var/***/model.ckpt. 
INFO: tensorflow: Loss for final step: 0.0316249. 
INFO: tensorflow: Starting evaluation at 2017-11-14-01:49:12
INFO: tensorflow: Restoring parameters from /var/***/model.ckpt-30000
INFO: tensorflow: Finished evaluation at 2017-11-14-01:49:12
INFO: tensorflow: Saving dict for global step 30000: accuracy = 0.9899, global_step = 30000, loss = 0.030221 

Test accuracy: 98.99%
INFO: tensorflow: Restoring parameters from /var/***/model.ckpt-30000
Prediction 1: 7
Prediction 2: 2
Prediction 3: 1
Prediction 4: 0
Prediction 5: 4
Prediction 6: 1
Prediction 7: 4
Prediction 8: 9
Prediction 9: 5
Prediction 10: 9
"""

从以上代码可以看出,Estimator能非常好地支持自定义模型,而且模型结构的定义过程中也可以使用其他的TensorFlow高层封装(比如代码中使用到的tf.layers)。Estimator在支持自定义模型结构的同时,并不影响它对训练过程的封装。

-10.3.3- 使用数据集(DataSet)作为Estimator输入

本小节中将介绍如何使用Dataset作为Estimator的输入来实现输入队列。

Estimator作为TensorFlow官方推荐的高层封装,它可以原生地支持TensorFlow中数据处理流程的接口。为了更加方便地介绍数据集(Dataset)Estimator的结合方法,本节将使用iris分类数据集。iris数据集需要通过4个特征(feature)来分辨3种类型的植物。iris数据集中总共包含了150个样本,其中包括120条训练数据、30条测试数据。这些数据都存储在csv文件中。以下代码介绍了如何通过Estimator和数据集相结合的方式完成整个数据读取和模型训练的过程。

# -*- coding: utf-8 -*- 

import tensorflow as tf 

tf.logging.set_verbosity(tf.logging.INFO)

# Estimator的自定义输入函数需要每一次被调用时可以得到一个batch的数据(包括所有的输入层数据和期待的正确答案标注),通过数据集可以很自然地实现这个过程。
# 虽然Estimator要求的自定义输入函数不能有参数,但通过python提供的lambda表达式可以快速降下面的函数转化为不带参数的函数。
def my_input_fn(file_path, perform_shuffle=False, repeat_count=1):
    # 定义解析csv文件中一行的方法。
    def decode_csv(line):
        # 将一行中的数据解析出来。
        # 注意iris数据中最后一列为正确答案,前面4列为特征。
        parsed_line = tf.decode_csv(line, [[0.], [0.], [0.], [0]])

        # Estimator的输入函数要求特征是一个字典,所以这里返回的也需要是一个字典。
        # 字典中key的定义需要和DNNClassifier中feature_columns的定义匹配。
        return {"x": parsed_line[:-1], parsed_line[-1:]}

    # 使用数据集处理输入数据。
    # 数据集的具体使用方法可以参考第7章。
    dataset = (tf.contrib.data.TextLineDataset(file_path).skip(1).map(decode_csv))
    if perform_shuffle:
        dataset = dataset.shuffle(buffer_size=256)

    dataset = dataset.repeat(repeat_count)
    dataset = dataset.batch(32)
    iterator = dataset.make_one_shot_iterator() 

    # 通过定义的数据集得到一个batch的输入数据,这个就是整个自定义的输入过程的返回结果。
    batch_features, batch_labels = iterator.get_next()

    # 如果是为预测过程提供输入数据,那么batch_labels可以直接使用None。
    return batch_features, batch_labels

# 与10.3.1小节中类似地定义Estimator。
feature_columns = [tf.feature_column.nerumeric_column("x", shape=[4])]
classifier = tf.estimator.DNNClassifier(
        feature_columns=feature_columns, 
        hidden_units=[10, 10], 
        n_classes=3
    )

# 使用lambda表达式将训练相关的信息传入自定义输入数据处理函数,并生成Estimator需要的输入函数。
classifier.train(
        input_fn=lambda: my_input_fn("path/to/iris_training.csv", True, 10)
    )

# 使用lambda表达式将测试相关的信息传入自定义输入数据处理函数,并生成Estimator需要的输入函数。
# 通过lambda表达式的方式可以大大减少冗余代码。
test_result = classifier.evaluate(
        input_fn=lambda: my_input_fn("path/to/iris_training.csv", False, 1)
    )
print("\nTest accuracy: %g %%" % (test_results["accuracy"]*100))

"""
运行以上代码可以得到类似以下的输出:
INFO: tensorflow: Saving checkpoints for 1 into /var/***/model.ckpt.
INFO: tensorflow: loss = 82.5525, step = 1
...
INFO: tensorflow: loss = 2.26899, step = 301 (0.291 sec)
INFO: tensorflow: Saving checkpoint for 375 into /var/***/model.ckpt.
INFO: tensorflow: Loss for final step: 2.0514. 
INFO: tensorflow: Starting evaluation at 2017-11-14-18:01:45
INFO: tensorflow: Restoring parameters from /var/***/model.ckpt-375
INFO: tensorflow: Finished evaluation at 2017-11-14-18:01:45
INFO: tensorflow: Saving dict for global step 375: loss = 2.30858

Test accuracy: 96.667% 
"""

通过以上代码可以看出Estimator可以非常好地和数据集结合,这样就能够很容易地支持海量数据读入或者复杂的数据预处理流程。

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

推荐阅读更多精彩内容