Tensorflow Layer指南-創建卷積神經網絡

我們都知道,神經網絡是由一層一層的神經元組合而成的,每個層之間可以通過不同的方式來連接起來以構成不同結構的神經網絡。Tensorflow的layer模塊為我們提供了一組抽象層級很高的API,讓我們可以輕松地構建一個我們想要的神經網絡。我們可以通過layer對象的方法來很方便的實現我們常見的一些對神經網絡層操作,例如添加激活函數,應用dropout regularization減少過擬合等。在本教程中,您將學習如何使用layer對象構建卷積神經網絡模型來識別MNIST數據集中的手寫數字。

可能學過機器學習和神經網絡教程的同學們對于MNIST數據集中手寫數字識別這個例子應該很熟悉了,但是為了讓沒有學過的小白們能看懂這篇教程,還是有必要介紹一下什么是MNIST數據集:

MNIST數據集包含60,000個訓練樣例和10,000個手寫數字0-9的測試示例,格式為28x28像素單色圖像。


MINIST數據集

卷積神經網絡的介紹

卷積神經網絡——Convolutional neural networks (通常縮寫為 CNN)是當前用于執行圖像分類任務的最先進也是最常用的神經網絡結構。 CNN將一系列濾波器應用于圖像的原始像素數據以提取和學習更高級別的特征,使得該模型能夠將這些特征用于分類。 CNN包含三個組件:

  1. 卷積層(Convolutional layers),將特定數量的卷積濾鏡(convolution filters)應用于圖像。 對于每個子區域,圖層執行一組數學運算以在輸出特征映射中生成單個值。 卷積層通常將ReLU激活函數應用于輸出以將非線性引入到模型中。
  2. 合并層(Pooling layers),負責對由卷積層提取的圖像數據進行下采樣以減少特征映射的維度以提高處理效率。 常用的池化算法是最大池化(max polling),其提取特征地圖的子區域(例如,2×2像素的塊),保持它們的最大值并丟棄所有其他值。
  3. 密集層(Dence layers),對由卷積圖層提取的特征并由共用圖層進行下采樣(downsampled)執行分類。 密集層是全連接的神經網絡,在密集層中,圖層中的每個節點都連接到前一圖層中的每個節點。

通常,CNN由執行特征提取的一組卷積模塊組成,每個模塊又由一個卷積層和一個合并層組成。 最后的卷積模塊之后是一個或多個執行分類的密集層。 CNN中的最終密集層的節點數量是與所有目標類型的數量一致的,即模型可能預測的所有可能的目標類型,使用softmax激活函數為每個節點生成0-1之間的值(全部 這些softmax值等于1), 我們可以將給定圖像的softmax值解釋為圖像落入每個目標類別的可能性的相對測量值。

用Tensorflow創建基于CNN的MNIST數據分類器

讓我們建立一個擁有以下結構的CNN,來對MNIST數據集中的圖像進行分類:

  1. 卷積層#1:包含32個5x5濾波器(提取5x5像素子區域),使用ReLU激活函數。
  2. 合并層#1:包含32個2x2濾鏡,并按照最大池化的策略提取的數據執行步幅為2的池化操作。(其指定池區域不重疊)
  3. 卷積層#2:包含64個5x5濾波器,使用ReLU激活函數。
  4. 合并層#2:同樣,使用2x2濾波器和2步幅進行最大池化。
  5. 密集層#1:包含1,024個神經元,dropout regularization的比率為0.4。
  6. 密集層#2(Logits Layer):10個神經元,每個數字目標類別(0-9)一個。

下面讓我們隆重請出今天的主角tf.layer,該模塊包含創建上述三種圖層類型的方法:

  • conv2d() ,構造一個二維卷積層。 采用過濾器數量,過濾內核大小,填充和激活函數作為參數。
  • max_pooling2d() ,使用max-pooling算法構造一個二維池化層。 參數為過濾器大小和步幅。
  • dense() 構建一個密集層。 以神經元數量和激活函數作為參數。

這些方法中的每一個都接受tensor作為輸入,并將變換后的tensor作為輸出返回。 這樣可以很容易地將一個神經層連接到另一個神經層:只需從一個神經層創建方法中獲取輸出并將其作為輸入提供給另一個神經層。
現在我們添加以下cnn_model_fn函數,該函數符合TensorFlow的Estimator API預期的接口。 cnn_mnist.py將MNIST特征數據,標簽和模型模式(TRAIN,EVAL,PREDICT)作為參數; 配置CNN; 并返回預測,損失和培訓操作:

def cnn_model_fn(features, labels, mode):
  """Model function for CNN."""
  # Input Layer
  input_layer = tf.reshape(features["x"], [-1, 28, 28, 1])

  # Convolutional Layer #1
  conv1 = tf.layers.conv2d(
      inputs=input_layer,
      filters=32,
      kernel_size=[5, 5],
      padding="same",
      activation=tf.nn.relu)

  # Pooling Layer #1
  pool1 = tf.layers.max_pooling2d(inputs=conv1, pool_size=[2, 2], strides=2)

  # Convolutional Layer #2 and Pooling Layer #2
  conv2 = tf.layers.conv2d(
      inputs=pool1,
      filters=64,
      kernel_size=[5, 5],
      padding="same",
      activation=tf.nn.relu)
  pool2 = tf.layers.max_pooling2d(inputs=conv2, pool_size=[2, 2], strides=2)

  # Dense Layer
  pool2_flat = tf.reshape(pool2, [-1, 7 * 7 * 64])
  dense = tf.layers.dense(inputs=pool2_flat, units=1024, activation=tf.nn.relu)
  dropout = tf.layers.dropout(
      inputs=dense, rate=0.4, training=mode == tf.estimator.ModeKeys.TRAIN)

  # Logits Layer
  logits = tf.layers.dense(inputs=dropout, units=10)

  predictions = {
      # Generate predictions (for PREDICT and EVAL mode)
      "classes": tf.argmax(input=logits, axis=1),
      # Add `softmax_tensor` to the graph. It is used for PREDICT and by the
      # `logging_hook`.
      "probabilities": tf.nn.softmax(logits, name="softmax_tensor")
  }

  if mode == tf.estimator.ModeKeys.PREDICT:
    return tf.estimator.EstimatorSpec(mode=mode, predictions=predictions)

  # Calculate Loss (for both TRAIN and EVAL modes)
  loss = tf.losses.sparse_softmax_cross_entropy(labels=labels, logits=logits)

  # Configure the Training Op (for TRAIN mode)
  if mode == tf.estimator.ModeKeys.TRAIN:
    optimizer = tf.train.GradientDescentOptimizer(learning_rate=0.001)
    train_op = optimizer.minimize(
        loss=loss,
        global_step=tf.train.get_global_step())
    return tf.estimator.EstimatorSpec(mode=mode, loss=loss, train_op=train_op)

  # Add evaluation metrics (for EVAL mode)
  eval_metric_ops = {
      "accuracy": tf.metrics.accuracy(
          labels=labels, predictions=predictions["classes"])}
  return tf.estimator.EstimatorSpec(
      mode=mode, loss=loss, eval_metric_ops=eval_metric_ops)

以下部分(與上面每個代碼塊對應的標題)深入介紹用于創建每個神經層的tf.layers代碼,以及如何計算損失,配置訓練操作并生成預測。

輸入層

對于處理2D圖像數據的CNN,Tensorflow的Layer對象中用于創建卷積層和合并層的方法需要輸入一個結構為[batch_size, image_width, image_height, channels]的4維tensor,各個參數定義如下:

  • batch_size: 在訓練期間執行梯度下降時要使用的示例子集的大小。
  • image_width:示例圖像寬度。
  • image_height: 示例圖像高度。
  • channels: 示例圖像中的顏色通道數量。 對于彩色圖像,通道數量是3(紅色,綠色,藍色),對于單色圖像,只有1個通道(黑色)。

這里,我們的MNIST數據集由單色的28x28像素圖像組成,因此我們輸入圖層的所需形狀為[batch_size,28,28,1]。
為了將我們的輸入特征數據映射(特征)轉換為這種形狀,我們可以執行下面的整形操作:

input_layer = tf.reshape(features["x"], [-1, 28, 28, 1])

值得注意的是,我們已經為batch_size賦值為-1,意味著此維度大小應該根據feature["x"]中輸入值的數量動態計算,并保持所有其他維度的大小不變。 這使我們可以將batch_size視為我們可以調整的超參數。 例如,如果我們將示例以5批次的形式提供給我們的模型,則feature["x"]將包含3,920個值(每個圖像中每個像素的一個值),并且input_layer將具有[5,28,28,1]. 同樣,如果我們以100個批次的形式提供示例,則feature["x"]將包含78,400個值,而input_layer將具有[100,28,28,1]的形狀。

卷積層 #1

在我們的第一個卷積層中,我們希望將32個5x5濾波器應用于輸入層,并使用ReLU作為激活函數。 我們可以在圖層模塊中使用conv2d()方法來創建該圖層,如下所示:

conv1 = tf.layers.conv2d(
    inputs=input_layer,
    filters=32,
    kernel_size=[5, 5],
    padding="same",
    activation=tf.nn.relu)
  • filter參數表示filter的數量(這里是32)。
  • kernel_size表示filter的維度(這里是[5,5])。
  • padding參數為兩個枚舉值中的一個(不區分大小寫):valid(缺省值)或+ same。 我們在此處設置padding=same,表示輸出tensor應該與輸入tensor具有相同的寬度和高度值。此時TensorFlow將0值添加到輸入tensor的邊緣以保持寬度和高度為28。(如果沒有設置padding屬性,則將在28x28tensor上進行5x5卷積生成24x24tensor,因為有24x24個位置從28x28網格中提取5x5瓦片。)
  • activation參數表示用于卷積層輸出的激活函數。這里我們選取了Relu函數 tf.nn.relu

我們的用于輸出的tensor由conv2d()函數生成,tensor的結構是[batch_size, 28, 28, 32]。這里前三個維度大小和input_layer輸出的大小一致,最后一個32表示了有32個通道保存每個過濾器的輸出。

匯聚層 #1

接下來,我們將第一個匯聚層與剛才創建的卷積層連接起來。我們可以利用layermax_pooling2d()方法構建層執行max_pooling策略的一個2x2的過濾器:

pool1 = tf.layers.max_pooling2d(inputs=conv1, pool_size=[2, 2], strides=2)

這里同樣,inputs表示輸入的結構為[batch_size, image_width, image_height, channels]的tensor。這里,我們的輸入tensor是conv1,它是我們的第一個卷積層的輸出,它的結構為[batch_size, 28, 28, 32]

  • pool_size表示這個max pooling filter的大小為[width,height](這里是[2,2])。
  • strides參數表示步幅大小,在這里,我們設置了長度為2的步幅,這表明由濾波器提取的子區域應該在寬和高上間隔2個像素(對于2x2濾波器,這意味著沒有提取的區域將重疊), 如果要為寬度和高度設置不同的跨度值,則可以改為指定元組或列表(例如,stride = [3,6])。

我們的輸出tensor是由max_pooling2d()方法生成的,#pool1輸出格式為:[batch_size,14,14,32],2X2的filter使原始數據的長度和高度都減少50%。

卷積層#2以及合并層#2

我們可以繼續將第二個卷積層和合并層的組合連接到我們的CNN中,這里我們依然使用conv2d()max_pooling2d()方法。第二個卷積層中,我們將filter的數量增加到64個,依然使用ReLU函數作為激活函數,而對于第二個合并層,我們將采用和一個合并層相同的結構(一個長寬和步幅都為2的max pooling filter):

conv2 = tf.layers.conv2d(
    inputs=pool1,
    filters=64,
    kernel_size=[5, 5],
    padding="same",
    activation=tf.nn.relu)
pool2 = tf.layers.max_pooling2d(inputs=conv2, pool_size=[2, 2], strides=2)

值得注意的是,卷積層#2使用的是合并層#1的輸出作為輸入tensor,并通過conv2d()方法生成結構為[batch_size,14,14,64]的tensor作為輸出。其中widthheight由于設置了參數padding="same",和pool1的輸出寬高是一致的,channels 則表示64個filter輸出的64個channel。
合并層#2采用conv2作為輸入,并以pool2作為輸出,輸出格式為:[batch_size, 7, 7, 64],可以看出,數據寬高大小再一次減半。

密集層

接下來,我們需要添加一個由1024個采用ReLU激活函數的神經元組成的密集層到我們的神經網絡中,來為我們從前面的卷積層和合并層中提取出來的圖像特征做分類。在我們將這個神經層連接到神經網絡之前,我們需要將我們的pool2輸出的tensor扁平化(flatten)一下,讓其結構變成[batch_size,features]只有兩個維度,代碼如下:

pool2_flat = tf.reshape(pool2, [-1, 7 * 7 * 64])

在上面的reshape()操作中,-1表示batch_size由輸入數據的實例數量動態計算。每個實例具有7x7x64=3136個特征,這里每個數字分別對應pool2的寬、高以及通道數量,所以我們的pool2_flat的被“壓扁”后的大小為:[batch_size, 3136]
Now, we can use the dense() method in layers to connect our dense layer as follows:
現在 ,我們可以使用layerdense()方法去將我們的dense層創建出來:

dense = tf.layers.dense(inputs=pool2_flat, units=1024, activation=tf.nn.relu)

其中inputsactivation參數的意義和tf.layers.conv2d()方法中一樣,分別表示輸入tensor和激活函數,而units參數則表示該層中神經元的數量。

為了防止過擬合,我們可以使用tf.layers.dropout()方法,在我們的dense層輸出后面加上dropout regularization:

dropout = tf.layers.dropout(
    inputs=dense, rate=0.4, training=mode == tf.estimator.ModeKeys.TRAIN)

inputs參數不用多說,表示輸入的tensor,rate參數表示我們droout的比率,這里我們使用0.4,意味著在訓練時,40%的數據會被隨機丟棄。 train參數是個布爾值,用于控制dropout是否啟用,這里我們將只在TRAIN模式中采用dropout,dropout的大小為:[batch_size, 1024]

Logits層

我們的神經網絡中的最后一層是logits層,它會返回我們預測的原始值。 最終我們創建了一個結構為[batch_size, 10]包含10個神經元(分別對應0-9這10個目標類)的密集層,并使用線性激活函數(默認值):

logits = tf.layers.dense(inputs=dropout, units=10)

生成預測值

我們的模型為我們返回的[batch_size, 10]-維的tensor中包含預測結果的原始值,讓我們將這些原始值轉換成一些比較直觀的格式來作為我們模型的返回值,例如:

  • 每個示例的預測類別:直接根據預測值返回一個0-9的數字。
  • 每個示例的每個可能目標類的概率:返回預測值為0,為1,為2...的概率。

回到我們的代碼,我們采用了一個tf.argmax()方法來找到返回的tensor中每一行數據中的最大值的下標:

tf.argmax(input=logits, axis=1)

input參數表示輸入tensor,axis參數表示我們是對哪個維度求最大值下標,這里給1表示對行求最大值下標,而我們的輸入logit的結構是[batch_size,10],所以我們這里表示是對數字10所代表的維度求最大值下標,而這10個下標分別對應我們所預測的0-9這9個數字,而最大值所對應的下標就是我們預測的結果。這樣講可能有點抽象,我們來舉個例子:

[...[1,1,1,1,1000,1,1,1,1,1]...]

這里可以看到,我們輸出tensor的某一行中最大值1000所對應的下標為4(從0開始),表示我們對于這一組數據的預測結果為4,即這幅圖片上面寫的是阿拉伯數字4。
然后,我們將我們的預測值轉換成兩種輸出格式再組合成一個dict后輸出一個EstimatorSpec對象:

predictions = {
    "classes": tf.argmax(input=logits, axis=1),
    "probabilities": tf.nn.softmax(logits, name="softmax_tensor")
}
if mode == tf.estimator.ModeKeys.PREDICT:
  return tf.estimator.EstimatorSpec(mode=mode, predictions=predictions)

計算損失

對于培訓和評估,我們需要定義一個損失函數來衡量模型的預測與目標類別的匹配程度。 對于像MNIST這樣的多類分類問題,通常使用交叉熵來度量損失。 以下代碼計算模型在TRAIN或EVAL模式下運行時的交叉熵:

onehot_labels = tf.one_hot(indices=tf.cast(labels, tf.int32), depth=10)
loss = tf.losses.softmax_cross_entropy(
    onehot_labels=onehot_labels, logits=logits)

先看第一行代碼,label這個tensor中包含了我們用于訓練的預測值列表,例如, [1,9,...]。 為了計算交叉熵,首先我們需要將標簽轉換為相應的單熱編碼 (one-hot encoding)

[[0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
 ...]

我們使用tf.one_hot()函數來執行此轉換,這個函數有兩個必需的參數:

  • indices: 單熱tensor中"on value"所處的位置,即上述tensor中的"1"值的位置。
  • depth: 單熱tensor的深度,即目標類別的數量。 這里,深度是10(0-9)。

經過這一步驟之后我們的label的值從[0,1...]這樣的由0-9數字組成的列表變為onehot_labels這樣由[1,0,0,0,0,0,0,0,0],[0,1,0,0,0,0,0,0,0]...等單熱編碼所組成的列表,數字1所在的下標表示原來的數值。

接下來在看第二行代碼,我們利用tf.losses.softmax_cross_entropy()方法來計算onehot_labelslogits層輸出預測值的交叉熵。 在計算時,會在logits上執行softmax激活,再將onehot_labels和softmax激活后的logits作為參數計算交叉熵,并將loss作為一個標量tensor返回。

配置訓練操作

在我們將CNN的損失定義為logits層和我們label的softmax交叉熵后,我們將配置我們的模型以在訓練期間優化這個損失值。 我們將使用學習率為0.001的隨機梯度下降作為優化算法:

if mode == tf.estimator.ModeKeys.TRAIN:
  optimizer = tf.train.GradientDescentOptimizer(learning_rate=0.001)
  train_op = optimizer.minimize(
      loss=loss,
      global_step=tf.train.get_global_step())
  return tf.estimator.EstimatorSpec(mode=mode, loss=loss, train_op=train_op)

添加評估指標

要評估我們模型的預測準確性,我們需要在EVAL模式中定義eval_metric_ops字典,如下所示:

eval_metric_ops = {
    "accuracy": tf.metrics.accuracy(
        labels=labels, predictions=predictions["classes"])
}
return tf.estimator.EstimatorSpec(
    mode=mode, loss=loss, eval_metric_ops=eval_metric_ops)

訓練和評估我們的 CNN MNIST 分類器

我們已經完成 CNN MNIST 模型的創建,現在我們要訓練并評估它

加載訓練集和測試集

First, let's load our training and test data. Add a main() function to cnn_mnist.py with the following code:

首先,讓我們加載我們的訓練集和測試集。首先為我們的工程添加main()函數:

def main(unused_argv):
  # Load training and eval data
  mnist = tf.contrib.learn.datasets.load_dataset("mnist")
  train_data = mnist.train.images # Returns np.array
  train_labels = np.asarray(mnist.train.labels, dtype=np.int32)
  eval_data = mnist.test.images # Returns np.array
  eval_labels = np.asarray(mnist.test.labels, dtype=np.int32)

我們將train_data和train_labels中的訓練特征數據(手繪數字的55,000個圖像的原始像素值)和訓練label(每個圖像的0-9的對應值)分別存儲為numpy數組。 同樣,我們將評估特征數據(10,000個圖像)和評估label分別存儲在eval_data和eval_labels中。

創建Estimator

接下來,讓我們為我們的模型創建一個Estimator(一個TensorFlow類,用于執行高級模型訓練,評估和推理)。 將下面的代碼添加到main()中:

# Create the Estimator
mnist_classifier = tf.estimator.Estimator(
    model_fn=cnn_model_fn, model_dir="/tmp/mnist_convnet_model")

model_fun就是我們前面所編寫的創建模型的cnn_model_fnmodel_dir表示我們保存模型數據的路徑。

設置日志鉤子

由于CNN的訓練需要一段時間,因此我們需要在訓練期間建立一些日志記錄,以便跟蹤訓練進度。 我們可以使用TensorFlow的tf.train.SessionRunHook創建一個tf.train.LoggingTensorHook,它將記錄來自CNN的softmax層的概率值:

  # Set up logging for predictions
  tensors_to_log = {"probabilities": "softmax_tensor"}
  logging_hook = tf.train.LoggingTensorHook(
      tensors=tensors_to_log, every_n_iter=50)

我們在tensors_to_log字典中存儲了我們想要進行日志跟蹤的tensor,字典的key是我們輸出日志的標簽,而對應的value是我們的tensor在tensorflow的graph中的名稱。在這里,我們的概率可以在softmax_tensor中找到,這是我們在cnn_model_fn中生成概率時早先給出的softmax操作的名稱。
接下來,我們創建LoggingTensorHook,將tensors_to_log傳遞給tensors參數,我們設置every_n_iter = 50,表示每50步記錄一次日志。

訓練我們的模型

現在我們準備訓練我們的模型,我們可以通過在mnist_classifier上創建train_input_fn并調用train()來完成這個模型的訓練。 將以下內容添加到main()

# Train the model
train_input_fn = tf.estimator.inputs.numpy_input_fn(
    x={"x": train_data},
    y=train_labels,
    batch_size=100,
    num_epochs=None,
    shuffle=True)
mnist_classifier.train(
    input_fn=train_input_fn,
    steps=20000,
    hooks=[logging_hook])

在numpy_input_fn調用中,我們將訓練特征數據和label分別傳遞給x和y,并設置了100的batch_size(這意味著模型將在每個步驟以數量為100的minibatches進行訓練)。num_epochs = None表示模型將訓練到達到指定的步數。 我們還設置shuffle = True來洗牌訓練數據。 在調用train()時,我們設置了steps= 20000(這意味著模型將訓練總共20000步)。 我們將logging_hook傳遞給hooks參數,以便在訓練過程中觸發它。

評估我們的模型

一旦訓練完成,我們要評估我們的模型以確定其在MNIST測試集上的準確性。 我們將測試集中的eval_dataeval_label傳入numpy_input_fn的x和y參數,并調用evaluate()方法來評估我們在model_fn中的eval_metric_ops參數中指定指標:

# Evaluate the model and print results
eval_input_fn = tf.estimator.inputs.numpy_input_fn(
    x={"x": eval_data},
    y=eval_labels,
    num_epochs=1,
    shuffle=False)
eval_results = mnist_classifier.evaluate(input_fn=eval_input_fn)
print(eval_results)

為了創建eval_input_fn,我們設置num_epochs = 1,以便模型評估一個歷元數據上的度量并返回結果。 我們還設置shuffle = False來循環遍歷數據。

Run the Model

我們編寫了CNN模型函數、Estimator和訓練/評估邏輯; 現在讓我們看看結果。 運行cnn_mnist.py得到以下輸出:

INFO:tensorflow:loss = 2.36026, step = 1
INFO:tensorflow:probabilities = [[ 0.07722801  0.08618255  0.09256398, ...]]
...
INFO:tensorflow:loss = 2.13119, step = 101
INFO:tensorflow:global_step/sec: 5.44132
...
INFO:tensorflow:Loss for final step: 0.553216.

INFO:tensorflow:Restored model from /tmp/mnist_convnet_model
INFO:tensorflow:Eval steps [0,inf) for training step 20000.
INFO:tensorflow:Input iterator is exhausted.
INFO:tensorflow:Saving evaluation summary for step 20000: accuracy = 0.9733, loss = 0.0902271
{'loss': 0.090227105, 'global_step': 20000, 'accuracy': 0.97329998}

可以看到我們的模型擁有高達97.3%的準確率,是不是很酷?

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