TensorFlow高階API Estimator自定義模型解決圖像分類問題

在之前的文章中,我們利用silm工具和谷歌訓練好的inception-v3模型完成了一個花朵圖像分類問題,但代碼還是比較繁瑣。為了更精簡的代碼和提高可讀性,這一次我們利用TensorFlow提供的高階API Estimator來解決同樣的問題。同時,在最后,我們會把訓練過程中的參數變化通過TensorBoard展示出來。

Estimator

Estimator是TensorFlow官方提供的一個高層API,它更好的整合了原生態TensorFlow提供的功能。它可以極大簡化機器學習編程。下面來看一下TensorFlow API結構:


API Architecture

在官方文檔中,有這么一句話:

We strongly recommend writing TensorFlow programs with the following APIs:

  • Estimators, which represent a complete model. The Estimator API provides methods to train the model, to judge the model's accuracy, and to generate predictions.
  • Datasets for Estimators, which build a data input pipeline. The Dataset API has methods to load and manipulate data, and feed it into your model. The Dataset API meshes well with the Estimators API.

可以看到Estimator和Dataset這兩個API是官方強烈推薦的。Estimator提供了預創建的DNN模型,使用起來非常方便。具體怎么使用Estimator預創建模型,官方文檔里面也有寫,有興趣的可以去看Estimator官方
但是預先定義的Estimator功能有限,比如目前無法很好的實現卷積神經網絡和循環神經網絡,也沒有辦法支持自定義的損失函數,所以為了更好的使用Estimator,這篇文章會教大家怎么用Estimator自定義CNN模型,以及如何配合Dataset讀取圖片數據。

數據準備

在這里我們可以使用之前的谷歌提供的花朵分類數據集,也可以使用其它的。為了區分上次結果這次我們使用新的數據集。在這里我使用百度挑桃分類數據集。下載解壓后可以看到是這樣的目錄:
數據集

數據集已經幫我們劃分好了是訓練還是測試。每一個文件夾代表一種桃子,總共有4種桃子(這個數據集肉眼很難辨別,可能是因為我不夠專業-_-)。

數據預處理

我們還是像之前一樣對數據預處理。在工程目錄下新建select_peach_data.py文件。跟之前處理花朵分類的時候一樣所以這里直接粘貼代碼:

import glob
import os.path
import numpy as np
import tensorflow as tf

from tensorflow.python.platform import gfile

#輸入圖片地址
INPUT_ALL_DATA = './select_peach'
INPUT_TRAIN_DATA = './select_peach/train'
INPUT_TEST_DATA = './select_peach/test'
OUTPUT_TRAIN_FILE = './path/to/output_train.tfrecords'
OUTPUT_TEST_FILE = './path/to/output_test.tfrecords'

def _int64_feature(value):
    return tf.train.Feature(int64_list=tf.train.Int64List(value=[value]))

#生成字符串的屬性
def _bytes_feature(value):
    return tf.train.Feature(bytes_list=tf.train.BytesList(value=[value]))

#檢索目錄并提取目錄圖片文件生成TFRecords
def get_img_data(sub_dirs,writer,INPUT_DATA,sess):
    current_label = 0
    is_root_dir = True
    print("文件地址: "+INPUT_DATA)
    for sub_dir in sub_dirs:
        if is_root_dir:
            is_root_dir = False
            continue
        file_list = []
        dir_name = os.path.basename(sub_dir)

        file_glob = os.path.join(INPUT_DATA, dir_name, '*.' + "png")
        # extend合并兩個數組
        # glob模塊的主要方法就是glob,該方法返回所有匹配的文件路徑列表(list)
        # 比如:glob.glob(r’c:*.txt’) 這里就是獲得C盤下的所有txt文件
        file_list.extend(glob.glob(file_glob))
        if not file_list: continue
        # print('file_list',current_label)
        # 處理圖片數據
        index = 0
        for file_name in file_list:
            # 讀取并解析圖片 講圖片轉化成299*299方便模型處理
            image_raw_data = gfile.FastGFile(file_name, 'rb').read()
            image = tf.image.decode_png(image_raw_data)
            if image.dtype != tf.float32:
                image = tf.image.convert_image_dtype(image, dtype=tf.float32)
            image = tf.image.resize_images(image, [299, 299])
            image_value = sess.run(image)
            pixels = image_value.shape[1]
            image_raw = image_value.tostring()
            # 存到features
            example = tf.train.Example(features=tf.train.Features(feature={
                'pixels': _int64_feature(pixels),
                'label': _int64_feature(current_label),
                'image_raw': _bytes_feature(image_raw)
            }))
            chance = np.random.randint(100)
            # 寫入訓練集
            writer.write(example.SerializeToString())
            index = index + 1
            if index == 400:
                break
            print("處理文件索引%d index%d"%(current_label,index))
        current_label += 1
#讀取數據并將數據分割成訓練數據、驗證數據和測試數據
def create_image_lists(sess):

    #首先處理訓練數據集
    sub_dirs = [x[0] for x in os.walk(INPUT_TRAIN_DATA)]
    writer_train = tf.python_io.TFRecordWriter(OUTPUT_TRAIN_FILE)
    get_img_data(sub_dirs,writer_train,INPUT_TRAIN_DATA,sess)

    sub_test_dirs = [x[0] for x in os.walk(INPUT_TEST_DATA)]
    writer_test = tf.python_io.TFRecordWriter(OUTPUT_TEST_FILE)
    get_img_data(sub_test_dirs,writer_test,INPUT_TEST_DATA,sess)

    writer_train.close()
    writer_test.close()

def main():
    with tf.Session() as sess:
        create_image_lists(sess)
        print('success')

if __name__ == '__main__':
    main()

這里因為test和train已經在文件夾上作了區分,所以這里我利用兩個TFRecordWriter來把數據分別寫入兩個TFRecord。為了節省時間在這里我并沒有利用全部的訓練數據,只是加載了其中的400份。當然在真實的訓練場景下你是需要加載全部的數據的。
代碼沒有詳盡的注釋,因為和之前的處理大部分都是一樣的,不清楚的可以去看我之前的文章。inception-v3

自定義Estimator

下面我們開始步入主題。先看一張Estimator類組成圖。

Estimator

以下源自官方文檔的一段話:
Pre-made Estimators are fully baked. Sometimes though, you need more control over an Estimator's behavior. That's where custom Estimators come in. You can create a custom Estimator to do just about anything. If you want hidden layers connected in some unusual fashion, write a custom Estimator. If you want to calculate a unique metric for your model, write a custom Estimator. Basically, if you want an Estimator optimized for your specific problem, write a custom Estimator.

A model function (or model_fn) implements the ML algorithm. The only difference between working with pre-made Estimators and custom Estimators is:

  • With pre-made Estimators, someone already wrote the model function for you.
  • With custom Estimators, you must write the model function.

Your model function could implement a wide range of algorithms, defining all sorts of hidden layers and metrics. Like input functions, all model functions must accept a standard group of input parameters and return a standard group of output values. Just as input functions can leverage the Dataset API, model functions can leverage the Layers API and the Metrics API.

大概意思是:預創建的 Estimator 是 tf.estimator.Estimator 基類的子類,而自定義 Estimator 是 tf.estimator.Estimator 的實例。
Pre-made Estimators和custom Estimators差異主要在于tensorflow中是否有它們可以直接使用的模型函數(model function or model_fn)的實現。對于前者,tensorflow中已經有寫好的model function,因而直接調用即可;而后者的model function需要自己編寫。因此,Pre-made Estimators使用方便,但使用范圍小,靈活性差;custom Estimators則正好相反。

總體來說,模型是由三部分構成:Input functions、Model functions 和Estimators(評估控制器,main function)。

  • Input functions:主要是由Dataset API組成,可以分為train_input_fn和eval_input_fn。前者的任務(行為)是接受參數,輸出數據訓練數據,后者的任務(行為)是接受參數,并輸出驗證數據和測試數據。
  • Model functions:是由模型(the Layers API )和監控模塊( the Metrics API)組成,主要是實現模型的訓練、測試(驗證)和監控顯示模型參數狀況的功能。
  • Estimators:在模型中的作用類似于計算機中的操作系統。它將各個部分“粘合”起來,控制數據在模型中的流動與變換,同時控制模型的的各種行為(運算)。

在得知以上知識以后,我們可以開始動手編碼起來。通過以上內容得知,首先我們需要先創建自定義的Model functions。下面新建my_estimator文件。
由于我們這里是實現自定義的model_fn函數,而model_fn主要功能是定義模型的結構,損失函數以及優化器。還會對預測和評測進行處理。綜上我們來完成model_fn的編寫。

自定義model_fn

#導入相關庫
import numpy as np
import tensorflow as tf
import tensorflow.contrib.slim as slim
# 加載通過TensorFlow-Silm定義好的 inception_v3模型
import tensorflow.contrib.slim.python.slim.nets.inception_v3 as inception_v3

#圖片數據地址
TRAIN_DATA = './path/to/output_train.tfrecords'
TEST_DATA = './path/to/output_test.tfrecords'

shuffle_buffer = 10000
BATCH = 64
#打開 estimator 日志
tf.logging.set_verbosity(tf.logging.INFO)

#自定義模型
#這里我們提供了兩種方案。一種是直接通過slim工具定義已有模型
#另一種是通過tf.layer更加靈活地定義神經網絡結構
def inception_v3_model(image,is_training):
    with slim.arg_scope(inception_v3.inception_v3_arg_scope()):
        predictions,_ = inception_v3.inception_v3(image,num_classes=5)
        return predictions
#定義lenet5模型
def lenet5(x,is_training):
    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,5)
#自定義Estimator中使用的模型。定義的函數有4個收入,
#features給出在輸入函數中會提供的輸入層張量。這是個字典
#字典通過input_fn提供。如果是系統的輸入
#系統會提供tf.estimator.inputs.numpy_input_fn中的x參數指定內容
#labels是正確答案,通過numpy_input_fn的y參數給出
#在這里我們用dataset來自定義輸入函數。
#mode取值有3種可能,分別對應Estimator的train,evaluate,predict這三個函數
#mode參數可以判斷當前是訓練,預測還是驗證模式。
#最有一個參數param也是字典,里面是有關于這個模型的相關任何超參數(學習率)
def model_fn(features,labels,mode,params):
    predict = lenet5(features,mode == tf.estimator.ModeKeys.TRAIN)
    #如果是預測模式,直接返回結果
    if mode == tf.estimator.ModeKeys.PREDICT:
        return tf.estimator.EstimatorSpec(
            mode=mode,
            predictions={"result":tf.argmax(predict,1)}
        )
  #定義損失函數,這里使用tf.losses可以直接從tf.losses.get_total_loss()拿到損失
    tf.losses.softmax_cross_entropy(tf.one_hot(labels, 5), predict, weights=1.0)

    #優化器
    optimizer = tf.train.GradientDescentOptimizer(learning_rate=params["learning_rate"])
    #定義訓練過程。傳入global_step的目的,為了在TensorBoard中顯示圖像的橫坐標
    train_op = optimizer.minimize(
        loss=tf.losses.get_total_loss(),
        global_step=tf.train.get_global_step()
    )

    #定義評測標準
    #這個函數會在調用Estimator.evaluate的時候調用
    accuracy = tf.metrics.accuracy(
            predictions=tf.argmax(predict,1),
            labels=labels,
            name="acc_op"
    )
    eval_metric_ops = {
        "my_metric":accuracy
    }
    #用于向TensorBoard輸出準確率圖像
    #如果你不需要使用TensorBoard可以不添加這行代碼
    tf.summary.scalar('accuracy', accuracy[1])
    #model_fn會返回一個EstimatorSpec
    #EstimatorSpec必須包含模型損失,訓練函數。其它為可選項
    #eval_metric_ops用于定義調用Estimator.evaluate()時候所指定的函數
    return tf.estimator.EstimatorSpec(
        mode=mode,
        loss=tf.losses.get_total_loss(),
        train_op=train_op,
        eval_metric_ops=eval_metric_ops
    )

自定義Input functions

定義完了model functions接下來我們通過Dataset API來定義input functions:

#解析tfrecords
def parse(record):
    features = tf.parse_single_example(
        record,
        features={
            'image_raw': tf.FixedLenFeature([], tf.string),
            'label': tf.FixedLenFeature([], tf.int64),
            'pixels': tf.FixedLenFeature([], tf.int64)
        }
    )
    decoded_image = tf.decode_raw(features['image_raw'], tf.float16)
    label = features['label']
    return decoded_image, label
#從dataset中讀取訓練數據,這里和之前處理花朵分類的時候一樣
def my_input_fn(file):
    dataset = tf.data.TFRecordDataset([file])
    dataset = dataset.map(parse)
    dataset = dataset.shuffle(shuffle_buffer).batch(BATCH)
    dataset = dataset.repeat(10)
    iterator = dataset.make_one_shot_iterator()
    batch_img,batch_labels = iterator.get_next()
    with tf.Session() as sess:
        batch_sess_img,batch_sess_labels = sess.run([batch_img,batch_labels])
        #這里需要特別注意 由于batch_sess_img這里是轉成了string后在原有長度上增加了8倍
        #所以在這里我們要先轉成numpy然后再reshape要不然會報錯
        batch_sess_img = np.fromstring(batch_sess_img, dtype=np.float32)
        #numpy轉換成Tensor
        batch_sess_img = tf.reshape(batch_sess_img, [BATCH, 299, 299, 3])
    return batch_sess_img,batch_sess_labels

在這里要注意,Estimator輸入函數要求每次被調用可以得到一個batch的數據,包括所有的輸入層數據和正確答案標注。而且my_input_fn函數并不能帶有參數。稍后我們會用lambda表達式解決這個問題。

最后我們通過main函數來啟動訓練過程:

def main():
    #定義超參數
    model_params = {"learning_rate":0.001}
    #定義訓練的相關配置參數
    #keep_checkpoint_max=1表示在只在目錄下保存一份模型文件
    #log_step_count_steps=50表示每訓練50次輸出一次損失的值
    run_config = tf.estimator.RunConfig(keep_checkpoint_max=1,log_step_count_steps=50)
    #通過tf.estimator.Estimator來生成自定義模型
    #把我們自定義的model_fn和超參數傳進去
    #這里我們還傳入了持久化模型的目錄
    #estimator會自動幫我們把模型持久化到這個目錄下
    estimator = tf.estimator.Estimator(model_fn=model_fn,params=model_params,model_dir="./path/model",config=run_config)
    #開始訓練模型,這里說一下lambda表達式
    #lambda表達式會把函數原本的輸入參數變成0個或它指定的參數。可以理解為函數的默認值
    #這里傳入自定義輸入函數,和訓練的輪數
    estimator.train(input_fn=lambda :my_input_fn(TRAIN_DATA),steps=300)
    #訓練完后進行驗證,這里傳入我們的測試數據
    test_result = estimator.evaluate(input_fn=lambda :my_input_fn(TEST_DATA))
    #輸出測試驗證結果
    accuracy_score = test_result["my_metric"]
    print("\nTest accuracy:%g %%"%(accuracy_score*100))

if __name__ == '__main__':
    main()

運行程序,可以看到如下輸出。因為我這里是從367步以后繼續訓練,所以我們在日志中看到我這里是直接加載了第367步保存的模型。
每隔一定時間,Estimator會自動創建模型文件。另外如果訓練中斷,下一次再啟動訓練的話,Estimator會自動從模型目錄下加載最新的模型并且用于訓練,非常方便。這就是為什么谷歌推薦我們用Estimator來訓練模型,因為它封裝了很多開發者并不需要關心的操作,大大提升了我們的開發效率。

INFO:tensorflow:Done calling model_fn.
INFO:tensorflow:Create CheckpointSaverHook.
INFO:tensorflow:Graph was finalized.
INFO:tensorflow:Restoring parameters from ./path/model/model.ckpt-367
INFO:tensorflow:Running local_init_op.
INFO:tensorflow:Done running local_init_op.
INFO:tensorflow:Saving checkpoints for 368 into ./path/model/model.ckpt.
INFO:tensorflow:loss = 0.2994086, step = 368
INFO:tensorflow:global_step/sec: 0.116191
INFO:tensorflow:loss = 0.2086069, step = 418 (430.326 sec)
INFO:tensorflow:Saving checkpoints for 438 into ./path/model/model.ckpt.
INFO:tensorflow:global_step/sec: 0.115405
INFO:tensorflow:loss = 0.17857286, step = 468 (433.259 sec)
INFO:tensorflow:Saving checkpoints for 506 into ./path/model/model.ckpt.
INFO:tensorflow:global_step/sec: 0.111342
INFO:tensorflow:loss = 0.107850984, step = 518 (449.065 sec)
INFO:tensorflow:global_step/sec: 0.115999
INFO:tensorflow:loss = 0.08592671, step = 568 (431.040 sec)
INFO:tensorflow:Saving checkpoints for 575 into ./path/model/model.ckpt.
INFO:tensorflow:global_step/sec: 0.112465
INFO:tensorflow:loss = 0.05861471, step = 618 (444.587 sec)
INFO:tensorflow:Saving checkpoints for 643 into ./path/model/model.ckpt.

TensorBoard

為了更加直觀的看到訓練過程,接下來我們將使用谷歌提供的一個工具TensorBoard來可視化我們的訓練過程。
要啟動TensorBoard,執行下面的命令:

#PATH替換為你模型保存的目錄。要注意在這里用的是絕對路徑。
tensorboard --logdir=PATH

執行命令后可以看到如下信息,說明TensorBoard已經跑起來了。

TensorBoard 1.8.0 at http://bogon:6006 (Press CTRL+C to quit)
W0817 16:14:27.129659 Reloader tf_logging.py:121] Found more than one graph event per run, or there was a metagraph containing a graph_def, as well as one or more graph events.  Overwriting the graph with the newest event.
W0817 16:14:27.650306 Reloader tf_lo

所有預創建的 Estimator 都會自動將大量信息記錄到 TensorBoard 上。不過,對于自定義 Estimator,TensorBoard 只提供一個默認日志(損失圖)以及您明確告知 TensorBoard 要記錄的信息。對于我們剛剛創建的自定義 Estimator,并且明確說明要繪制正確率的圖,所以TensorBoard 會生成以下內容:


TensorBoard.png

TensorBoard生成了三個圖。分別表示正確率,訓練處理的批次,訓練輪數所對應的損失值

簡而言之,下面是三張圖顯示的內容:

  • global_step/sec:這是一個性能指標,顯示我們在進行模型訓練時每秒處理的批次數(梯度更新)。
  • loss:所報告的損失。
  • accuracy:準確率由下列兩行記錄:
    • eval_metric_ops={'my_accuracy': accuracy}(評估期間)。
    • tf.summary.scalar('accuracy', accuracy[1])(訓練期間)。
      這些 Tensorboard 圖是務必要將 global_step 傳遞給優化器的 minimize 方法的主要原因之一。如果沒有它,模型就無法記錄這些圖的 x 坐標。

我們來看下TensorBoard的輸出。可以看到隨著訓練步驟的增加,loss在相應的減少,accuracy也在慢慢增加。這是一個健康的訓練過程。可以看到LeNet5在這個數據集上的正確率達到了95%左右。

eval

因為我自定義的Estimator在訓練結束之后并沒有輸出正確率(暫時沒找到原因),所以這里我們另外寫一個程序來測試這個模型的正確率。這里我們命名為eval.py。

import tensorflow as tf
import Estimator1
import numpy as np

TEST_DATA = './path/to/output_test.tfrecords'
CKPT_PATH = './path/model'
EVAL_BATCH = 20
def getValidationData():
   dataset = tf.data.TFRecordDataset([TEST_DATA])
   dataset = dataset.map(Estimator1.parse)
   dataset = dataset.batch(EVAL_BATCH)
   iterator = dataset.make_one_shot_iterator()
   batch_img, batch_labels = iterator.get_next()

   # batch_img作處理
   return batch_img, batch_labels
def my_eval():
   #estimator的eval方法不好使 用傳統方法試試
   batch_img,batch_labels = getValidationData()

   x = tf.placeholder(tf.float32, [None, 299,299,3], name='x-input')
   y_ = tf.placeholder(tf.int64, [None], name='y-input')
   y = Estimator1.lenet5(x, False)
   correct_prediction = tf.equal(tf.argmax(y, 1), y_)
   accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
   saver = tf.train.Saver()
   with tf.Session() as sess:
       while True:
           try:
               ckpt = tf.train.get_checkpoint_state(CKPT_PATH)
               if ckpt and ckpt.model_checkpoint_path:
                   saver.restore(sess,ckpt.model_checkpoint_path)
                   #通過文件名得到模型保存時迭代的輪數
                   global_step = ckpt.model_checkpoint_path.split('/')[-1].split('-')[-1]
                   batch_sess_img, batch_sess_labels = sess.run([batch_img, batch_labels])
                   batch_sess_img = np.fromstring(batch_sess_img, dtype=np.float32)
                   batch_sess_img = tf.reshape(batch_sess_img, [EVAL_BATCH, 299, 299, 3])
                   batch_sess_img = sess.run(batch_sess_img)
                   print(sess.run([tf.argmax(y,1),y_],feed_dict={x:batch_sess_img,y_:batch_sess_labels}))
                   accuracy_score = sess.run(accuracy,feed_dict={x:batch_sess_img,y_:batch_sess_labels})
                   print("After %s training step(s),validation accuracy = %g"%(global_step,accuracy_score))
               else:
                   print('No checkpoint file found')
                   return
           except tf.errors.OutOfRangeError:
               break
def main():
   my_eval()

if __name__ == '__main__':
   main()

這個程序大概的作用是:
1.讀取測試數據,把測試數據打包成batch。然后定義神經網絡輸入變量x和正確答案的標簽y_。
2.把x通過神經網絡得到的前向傳播結果y和y_作比較來計算正確率。
3.讀取之前訓練好的模型。
4.用一個while循環來輸出在訓練好的模型上每一個batch的正確率,直到數據讀取完畢。
運行這個程序可以得到以下輸出:

INFO:tensorflow:Restoring parameters from ./path/model/model.ckpt-643
[array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1]), array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1])]
After 643 training step(s),validation accuracy = 1
INFO:tensorflow:Restoring parameters from ./path/model/model.ckpt-643
[array([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2]), array([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2])]
After 643 training step(s),validation accuracy = 1
INFO:tensorflow:Restoring parameters from ./path/model/model.ckpt-643
[array([2, 2, 2, 2, 2, 2, 2, 3, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3]), array([2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3])]
After 643 training step(s),validation accuracy = 0.95
INFO:tensorflow:Restoring parameters from ./path/model/model.ckpt-643

嗯。60個數據中只有1個判斷錯誤,也符合我們之前得到的正確率。

寫在最后

Estimator是TensorFlow官方強烈推薦的API,通過上述程序大家也能看到相比傳統的TensorFlow API,Estimator封裝了大部分與業務邏輯無關的操作,然而通過Custom Estimator,Estimator也不失靈活性。

我們之前還通過slim定義了一個inception-v3模型,但是由于inception-v3結構比較復雜,訓練的時間比較久所以這里我們就以LeNet-5作演示了。但是在復雜的圖像分類問題上,比如ImageNet數據集中,LeNet-5的分類效果就不是很好。如果是復雜的圖像分類問題,就要選擇更加復雜的神經網絡模型來訓練才能達到較高的準確率。

另外這篇文章主要是以使用Estimator為主,對于其中的一些細節沒有很好的闡述。之后的文章會對一些技術細節做探究。

歡迎廣大喜歡AI的開發者互相交流,有問題也可以在評論區里留言,大家互相討論,一起進步。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,739評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,634評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,653評論 0 377
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,063評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,835評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,235評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,315評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,459評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,000評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,819評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,004評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,560評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,257評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,676評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,937評論 1 288
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,717評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,003評論 2 374

推薦閱讀更多精彩內容