[Deep-Learning-with-Python]計算機視覺中的深度學習

包括:

  • 理解卷積神經網絡
  • 使用數據增強緩解過擬合
  • 使用預訓練卷積網絡做特征提取
  • 微調預訓練網絡模型
  • 可視化卷積網絡學習結果以及分類決策過程
    介紹卷積神經網絡,convnets,深度學習在計算機視覺方面廣泛應用的一個網絡模型。

卷積網絡介紹

在介紹卷積神經網絡理論以及神經網絡在計算機視覺方面應用廣泛的原因之前,先介紹一個卷積網絡的實例,整體了解卷積網絡模型。用卷積網絡識別MNIST數據集。

from keras import layers
from keras import models

model = models.Sequential()

model.add(layers.Conv2D(32, (3, 3), activation='relu', input_shape=(28, 28, 1)))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(64, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(64, (3, 3), activation='relu'))

卷積網絡接收(image_height,image_width,image_channels)形狀的張量作為輸入(不包括batch size)。MNIST中,將圖片轉換成(28,28,1)形狀,然后在第一層傳遞input_shape參數。
顯示網絡架構

model.summary()

________________________________________________________________
Layer (type)        Output Shape        Param #
================================================================
conv2d_1 (Conv2D)   (None, 26, 26, 32)  320
________________________________________________________________
maxpooling2d_1 (MaxPooling2D) (None, 13, 13, 32) 0
________________________________________________________________
conv2d_2 (Conv2D)   (None, 11, 11, 64)  18496
________________________________________________________________
maxpooling2d_2 (MaxPooling2D)   (None, 5, 5, 64) 0
________________________________________________________________
conv2d_3 (Conv2D)   (None, 3, 3, 64)    36928
================================================================
Total params: 55,744
Trainable params: 55,744
Non-trainable params: 0

可以看到每個Conv2D和MaxPooling2D網絡層輸出都是3D張量,形狀為(height,width,channels).隨著網絡層的加深,長度和寬度逐漸減小;通道數通過Conv2D層的參數控制。
下一步連接Dense層,但當前輸出為3D張量,需要將3D張量平鋪成1D,然后添加Dense層。

model.add(layers.Flatten())
model.add(layers.Dense(64,activation='relu'))
model.add(layers.Dense(10,activation='softmax'))

因為是10分類,最后一層為10個神經元,激活函數為softmax。
最后的網絡架構

>>> model.summary()
Layer (type)        Output Shape        Param #
================================================================
conv2d_1 (Conv2D)   (None, 26, 26, 32)  320
________________________________________________________________
maxpooling2d_1 (MaxPooling2D) (None, 13, 13, 32) 0
________________________________________________________________
conv2d_2 (Conv2D)   (None, 11, 11, 64)  18496
________________________________________________________________
maxpooling2d_2 (MaxPooling2D)   (None, 5, 5, 64) 0
________________________________________________________________
conv2d_3 (Conv2D)   (None, 3, 3, 64)    36928
________________________________________________________________
flatten_1 (Flatten) (None, 576)     0
________________________________________________________________
dense_1 (Dense)     (None, 64)      36928
________________________________________________________________
dense_2 (Dense)     (None, 10)      650
================================================================
Total params: 93,322
Trainable params: 93,322
Non-trainable params: 0

(3,3,64)輸出平攤成(576,)向量。
網絡訓練

from keras.datasets import mnist
from keras.utils import to_categorical

(train_images, train_labels), (test_images, test_labels) = mnist.load_data()

train_images = train_images.reshape((60000, 28, 28, 1))
train_images = train_images.astype('float32') / 255
test_images = test_images.reshape((10000, 28, 28, 1))
test_images = test_images.astype('float32') / 255

train_labels = to_categorical(train_labels)
test_labels = to_categorical(test_labels)

model.compile(optimizer='rmsprop',loss='categorical_crossentropy',metrics=['accuracy'])
model.fit(train_images, train_labels, epochs=5, batch_size=64)

測試集上模型評估:

>>> test_loss, test_acc = model.evaluate(test_images, test_labels)
>>> test_acc
0.99080000000000001

在Dense網絡上準確率為97.8%,基本卷積網絡上準確率到99%.為什么簡單的卷積網絡工作效果這么好?回答之前,先了解Conv2D和MaxPooling2D層。

卷積操作

全連接網絡和卷積網絡的區別在于Dense全連接層學習輸入特征空間的全局模式特征,而卷積神經網絡學習輸入特征空間的局部模式特征。
卷積網絡的兩個關鍵特性:

  • 學習具有平移不變性的模式特征:一旦學習到圖片左上角的模式特征,可以在任何地方識別,如右下角,這種特性使得圖片處理更加有效,需要的樣本相對減少(實際生活中具有平移不變性)
  • 學習模式的空間層次結構:第一個卷積層將學習小的局部模式,如邊緣,第二個卷積層將學習由第一層特征構成的更大圖案,等等。這使得卷積網絡能夠有效地學習越來越復雜和抽象的視覺概念。(現實生活中許多都是分級的)。


    image

卷積在3D張量上運算,稱為特征映射,具有兩個空間軸(高度和寬度)以及深度軸(也稱為通道軸).對RGB三原色圖片來說,通道數為3--紅、綠、藍;MNIST數據集中圖片通道數為1--灰度圖。卷積操作在輸入特征圖上小分片上,然后將多個操作結果生成最后的特征圖。輸出的特征圖仍然是3D張量:width、height,深度可以是任意值,因為深度是網絡層的一個參數,而且深度值不再代表紅綠藍顏色通道,表示過濾器的個數。過濾器對輸入數據的特定方面進行編碼:比如在高級別,單個過濾器可以編碼“輸入中存在面部”的概念。
卷積定義的兩個參數:

  • 卷積核大小:通常為3x3,5x5.
  • 卷積核個數:卷積核個數等于本層網絡輸出層的深度。

Keras中,Conv2D網絡層定義:Conv2D(output_depth, (window_height, window_width)) .
卷積:卷積核在上一層的特征圖的全通道進行滑動,然后抽取形狀為(window_height,window_width,input_depth)形狀的3D片特征。每個3D片特征最后轉換成1D向量(卷積運算--張量點積),形狀(output_depth,),所有的結果向量整合形成最后的3D特征(height,width,output_depth).


image

輸出結果的寬度和高度可能和輸入寬度高度不同,由于:

  • Padding項;
  • Strides 步長

最大池化 MaxPooling

最大池化層的作用在于對特征圖進行下采樣。最大池化在特征圖中選擇window,然后每個通道的在窗口內求最大值。概念上與卷積操作類似,卷積操作在小patch 中做線性轉換,最大池化是求最大值,通過tensor的max張量操作。最大池化通常采用2x2窗口,步長為2,特征圖減半。卷積通常卷積核大小為3x3,步長為1。

下采樣的目的在于減少要處理特征圖的參數量,通過使連續的卷積層看到越來越大的窗口(就它們所涵蓋的原始輸入的比例而言)來促使空間濾波器層次結構。
最大池化并不是唯一的下采樣方法。可以使用帶步長卷積、或平均池化,但是最大池化的工作效果更好。

小數據集上訓練卷積網絡

計算機視覺中進場會遇到使用很少的數據集去訓練一個圖像分類模型。“小樣本”意味著樣本量在幾百到幾萬張. 比如貓狗分類,共4000張圖片,貓2000張,狗2000張。用2000張圖片來訓練--1000張驗證集,1000張測試集。
首先不做任何正則化處理,直接訓練,得到一個baseline模型,準確率為71%。主要問題在于模型過擬合。之后介紹data augmentation數據增強,減緩過擬合。訓練后為82%。更有效的方法是用已訓練好的模型最特征提取---準確率90%~96%,或者微調已訓練好的網絡做特征提取(97%)。這三種方法有助于在小數據集上的模型訓練。

深度學習與小數據問題的相關性

可能經常聽說:深度學習只能工作在大數據集上。這種說法部分正確:深度學習的一個重要特性在于深度學習能自己在訓練數據中尋找特征,而不需要人工干預,而這個特性只有在大數據樣本量上才有效,特別是輸入數據維度特別高時,eg圖片。
但是,對于初學者來說,構成大量樣本的內容與嘗試訓練的網絡的大小和深度是相對的。用幾十張圖片訓練卷積網絡來解決一個十分復雜的問題是不可能的,但如果模型比較簡單經過正則化處理,同時任務比較簡單,幾百張圖片也能解決問題。因為卷積網絡學習局部的、具有平移不變性的特征,它們在感知問題上具有很高的數據效率。 盡管相對缺乏數據,但無需額外的特征工程,即使在非常小的圖像數據集上從頭開始訓練,卷積網絡仍然會產生合理的結果。

更重要的是,深度學習模型本質上是高度可再利用的:例如,可以采用在大規模數據集上訓練的圖像分類或語音到文本模型,只需進行微小的更改,就可以重新用于顯著不同的問題上。具體而言,以計算機視覺為例,許多預先訓練好的模型(通常在ImageNet數據集上訓練)提供公開下載,當樣本量少時,可以用在模型中(做特征提取使用)提升工作效果。

數據下載

Keras中沒有包括Dogs vs. Cats數據集。可以在Kaggle上下載。
圖片格式為JPEGs.數據集包含25000張貓狗圖片(一半一半)。下載解壓縮后,創建一個新數據集包括3個文件夾:每類1000張的訓練集、每類500張的驗證集和每類500張的測試集。

import os,shutil

#原始數據
original_dataset_dir = '/Users/fchollet/Downloads/kaggle_original_data'
#新數據集目錄
base_dir = '/Users/fchollet/Downloads/cats_and_dogs_small'
os.mkdir(base_dir)
#創建訓練集、驗證集、測試集目錄
train_dir = os.path.join(base_dir, 'train')
os.mkdir(train_dir)
validation_dir = os.path.join(base_dir, 'validation')
os.mkdir(validation_dir)
test_dir = os.path.join(base_dir, 'test')
os.mkdir(test_dir)
#創建對應數據集下不同類別的目錄
train_cats_dir = os.path.join(train_dir, 'cats')
os.mkdir(train_cats_dir) 
train_dogs_dir = os.path.join(train_dir, 'dogs')
os.mkdir(train_dogs_dir) 
validation_cats_dir = os.path.join(validation_dir, 'cats')
os.mkdir(validation_cats_dir) 
validation_dogs_dir = os.path.join(validation_dir, 'dogs')
os.mkdir(validation_dogs_dir) 
test_cats_dir = os.path.join(test_dir, 'cats')
os.mkdir(test_cats_dir) 
test_dogs_dir = os.path.join(test_dir, 'dogs')
os.mkdir(test_dogs_dir) 
fnames = ['cat.{}.jpg'.format(i) for i in range(1000)]#取前1000張貓圖片
for fname in fnames:#將前一千張貓圖片復制到新數據集目錄下
    src = os.path.join(original_dataset_dir, fname)
    dst = os.path.join(train_cats_dir, fname)
    shutil.copyfile(src, dst)

fnames = ['cat.{}.jpg'.format(i) for i in range(1000, 1500)]#取500張貓圖片
for fname in fnames:#500張貓圖片復制到驗證集
    src = os.path.join(original_dataset_dir, fname)
    dst = os.path.join(validation_cats_dir, fname)
    shutil.copyfile(src, dst)

fnames = ['cat.{}.jpg'.format(i) for i in range(1500, 2000)]#取500張貓圖片
for fname in fnames:#500張貓圖片做測試集
    src = os.path.join(original_dataset_dir, fname)
    dst = os.path.join(test_cats_dir, fname)
    shutil.copyfile(src, dst)
#狗圖片
fnames = ['dog.{}.jpg'.format(i) for i in range(1000)]#1000張狗圖片做訓練集
for fname in fnames:
    src = os.path.join(original_dataset_dir, fname)
    dst = os.path.join(train_dogs_dir, fname)
    shutil.copyfile(src, dst)

fnames = ['dog.{}.jpg'.format(i) for i in range(1000, 1500)]
for fname in fnames:#500張狗圖片做驗證集
    src = os.path.join(original_dataset_dir, fname)
    dst = os.path.join(validation_dogs_dir, fname)
    shutil.copyfile(src, dst) Copies the next 500

fnames = ['dog.{}.jpg'.format(i) for i in range(1500, 2000)]
for fname in fnames:#500張狗圖片做測試集
    src = os.path.join(original_dataset_dir, fname)
    dst = os.path.join(test_dogs_dir, fname)
    shutil.copyfile(src, dst)

構建模型

from keras import layers
from keras import models

model = models.Sequential()

model.add(layers.Conv2D(32, (3, 3), activation='relu',input_shape=(150, 150, 3)))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(64, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(128, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(128, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Flatten())
model.add(layers.Dense(512, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))

模型架構:

>>> model.summary()
Layer (type)            Output Shape                    Param #
================================================================
conv2d_1 (Conv2D)       (None, 148, 148, 32)            896
________________________________________________________________
maxpooling2d_1 (MaxPooling2D)   (None, 74, 74, 32)      0
________________________________________________________________
conv2d_2 (Conv2D)       (None, 72, 72, 64)              18496
________________________________________________________________
maxpooling2d_2 (MaxPooling2D)   (None, 36, 36, 64)      0
________________________________________________________________
conv2d_3 (Conv2D)       (None, 34, 34, 128)             73856
________________________________________________________________
maxpooling2d_3 (MaxPooling2D)   (None, 17, 17, 128)     0
________________________________________________________________
conv2d_4 (Conv2D)       (None, 15, 15, 128)             147584
________________________________________________________________
maxpooling2d_4 (MaxPooling2D)   (None, 7, 7, 128)       0
________________________________________________________________
flatten_1 (Flatten)     (None, 6272)                    0
________________________________________________________________
dense_1 (Dense)         (None, 512)                     3211776
________________________________________________________________
dense_2 (Dense)         (None, 1)                       513
================================================================
Total params: 3,453,121
Trainable params: 3,453,121
Non-trainable params: 0

編譯階段,使用RMSProp優化算法,binary crossentropy為損失函數。

from keras import optimizers

model.compile(loss='binary_crossentropy',optimizer=optimizers.RMSprop(lr=1e-4),metrics=['acc'])

數據預處理

數據在送到網絡模型之前應該轉換成浮點類型的張量。目前數據集中數據格式為JPEG,所以處理步驟大致為:

  1. 讀取圖片文件;
  2. 將JPEG格式轉換為RGB像素值;
  3. 轉換成浮點類型張量;
  4. 將像素值(0~255)縮放到[0,1]之間。

針對上述步驟,Keras中有自動化處理方法。Keras中有一個圖像處理模塊,keras.preprocessing.image. 其中包括一個ImageDataGenerator類,可以將磁盤上的圖片文件自動轉換成預處理的張量batch批量。使用方法:

from keras.preprocessing.image import ImageDataGenerator

train_datagen = ImageDataGenerator(rescale=1./255)
test_datagen = ImageDataGenerator(rescale=1./255)
#將圖片轉換成150x150,類別為2;class_mode 確定返回標簽的類型binary二分類 1D類型
train_generator=train_datagen.flow_from_directory(train_dir,\
        target_size=(150,150),batch_size=20,class_mode='binary')

validation_generator = test_datagen.flow_from_directory(
validation_dir,target_size=(150, 150),batch_size=20,class_mode='binary')

生成器generator的數據結果為150x150 RGB批量圖片,尺寸為(20,150,150,3),二進制標簽形狀(20,)。每個批量大小為20個樣本(batch_size為20). 注意-生成器無限期地生成這些批次:它在目標文件夾的圖像上無休止地循環。

使用generator數據生成器對模型進行訓練。使用fit_generator方法,對于數據生成器來說,相當于fit方法。fit_generator第一個參數是Python生成器類型,能不斷地生成輸入和標簽批量。因為數據不斷生成,Keras模型需要知道在聲明一個epoch之前從發生器中抽取多少批量;steps_per_epoch參數:從生成器中生成 steps_per_epoch個批量數據;在經過steps_per_epoch次梯度下降后,在下一個epoch上進行訓練。在這里,批量大小為20,一個epoch有100個批量,生成2000張圖片樣本。
使用fit_generator方法,可以傳遞validataion_data參數,和fit方法相似。值得注意的是,這個參數可以賦值為數據生成器,也可以是numpy數組的元組。如果validation_data參數是數據生成器,生成器能不斷地生成數據,所以需要設置validation_steps參數,確定從生成器中生成多少驗證集批量。

history = model.fit_generator(train_generator,steps_per_epoch=100,epoch=30,validation_data=validation_generator,validation_steps=50)

模型保存:

model.save('cats_and_dogs_small_1.h5')

訓練集驗證集準確率、損失值變化:


image

可以發現模型發生過擬合現象。訓練準確率隨著時間線性增加,直到100%,而驗證集準確率在70-72%波動。驗證集損失在5個epoch之后達到最小值,之后開始波動;訓練集損失線性減少直到為0

因為訓練集只有2000張圖片,遇到的第一個問題就是模型過擬合。Dropout、權重衰減可以減緩過擬合,還有一個計算機視覺任務中,經常使用的處理方法:數據增強data augmentation。

數據增強

過度擬合是由于樣本太少而無法學習,導致無法訓練可以推廣到新數據的模型。給定無限的數據,模型可以學習到手頭數據分布的每個可能方面:永遠不會過擬合。數據增強采用從現有訓練樣本生成更多訓練數據的方法,通過大量隨機變換來增加樣本,從而產生新的可靠的圖像樣本。
目標是在訓練時,模型將永遠不會看到兩張完全相同的圖片。這有助于模型觀察數據的更多方面并更好地概括數據。
Keras中,可以通過實例化ImageDataGenerator實例,確定圖片轉換方法,從而實現數據增強。

datagen = ImageDataGenerator(
    rotation_range=40,#最大旋轉角度
    width_shift_range=0.2,#水平隨機平移圖片的范圍,比例
    height_shift_range=0.2,#垂直隨機平移圖片的范圍
    shear_range=0.2,#隨機應用剪切變換
    zoom_range=0.2,#隨機縮放圖片
    horizontal_flip=True,#隨機翻轉圖片
    fill_mode='nearest')#用于填充新創建的像素的策略,在旋轉或寬度/高度偏移后出現

如果使用這樣的數據增強配置訓練新網絡,網絡將永遠不會看到兩張相同的輸入圖片。但它看到的輸入仍然是嚴重相互關聯的,因為它們來自少量原始圖像 - 無法生成新信息,只能重新混合現有信息。因此,這不可能完全擺脫過擬合。為了進一步減緩過擬合,需要增加Dropout層,在全連接層之前。
新網絡模型:

model = models.Sequential()

model.add(layers.Conv2D(32, (3, 3), activation='relu',
input_shape=(150, 150, 3)))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(64, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(128, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(128, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Flatten())
model.add(layers.Dropout(0.5))
model.add(layers.Dense(512, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))
model.compile(loss='binary_crossentropy',optimizer=optimizers.RMSprop(lr=1e-4),metrics=['acc'])

使用數據增強和Dropout訓練網絡。

train_datagen = ImageDataGenerator(
rescale=1./255,
rotation_range=40,
width_shift_range=0.2,
height_shift_range=0.2,
shear_range=0.2,
zoom_range=0.2,
horizontal_flip=True,)

test_datagen = ImageDataGenerator(rescale=1./255)

train_generator = train_datagen.flow_from_directory(train_dir,
target_size=(150, 150),batch_size=32,class_mode='binary')

validation_generator = test_datagen.flow_from_directory(validation_dir,
target_size=(150, 150),batch_size=32,class_mode='binary')

history = model.fit_generator(train_generator,steps_per_epoch=100,
epochs=100,validation_data=validation_generator,validation_steps=50)

model.save('cats_and_dogs_small_2.h5')#模型保存

使用數據增強和Dropout后,訓練集、驗證集準確率和損失函數變化。


image

模型不再過擬合:訓練集曲線和驗證集曲線幾乎相互吻合。準確率82%,提高了15%左右。使用正則化技術,微調網絡超參數,模型準確率會進一步提高,到86%~87%.但是很難繼續提高,因為訓練數據有限,樣本量太少。另一種方法,可以采用預先訓練好的網絡模型,做特征提取,提高準確率。

使用預訓練卷積網絡

在小圖像數據集上使用深度學習的一種常見且高效的方法是使用預訓練網絡。預訓練網絡是先前在大型數據集上訓練的已保存網絡,通常是處理大規模圖像分類任務。如果這個原始數據集足夠大且代表性強,則預訓練網絡學習的特征的空間層次結構可以有效地充當視覺世界的通用模型,因此其特征可以證明對許多不同的計算機視覺問題都有用,甚至這些新問題可能涉及與原始任務完全不同。例如,可以在ImageNet上訓練網絡(其中類主要是動物和日常物品),然后將這個訓練好的網絡重新用于識別圖像中的家具物品任務中。與許多較舊的淺學習方法(傳統機器學習方法)相比,學習特征在不同問題中的這種可移植性是深度學習的關鍵優勢,并且它使得深度學習對于小數據問題非常有效。

比如在ImageNet數據集上訓練的網絡模型(140萬個標記圖像和1,000個不同類)。ImageNet包含許多動物類別,包括不同種類的貓和狗,因此可以期望在狗與貓的分類問題上表現良好。

使用VGG16網絡架構,它是ImageNet的簡單且廣泛使用的convnet架構。
使用預訓練網絡有兩種方法:特征提取和微調。

特征提取

特征提取包括使用先前網絡學習的表示從新樣本中提取有趣特征。然后,這些功能將通過一個新的分類器運行,該分類器從頭開始訓練。

如前所述,用于圖像分類的網絡包含兩部分:它們以一系列池化和卷積層開始,并以密集連接的分類器結束。第一部分稱為模型的卷積基礎。在卷積網絡中,特征提取包括獲取先前訓練的網絡的卷積基礎,通過它運行新數據,以及在輸出之上訓練新的分類器。

image

為什么只重用卷積網絡?是否可以重復使用全連接分類器?一般來說,應該避免這樣做。原因是卷積網絡學習的表示可能更通用,因此更可重復使用:特征網絡的特征圖是圖片上一般概念的存在圖,無論處理的計算機視覺問題是什么,都可能是有用的。但是,分類器學習的表示必然特定于訓練模型的類集 - 它們將僅包含關于整個圖像中該類或該類的存在概率的信息。此外,在全連接網絡層的輸出表示不再包含有關對象在輸入圖像中的位置信息:這些表示消除了空間的概念,而卷積特征圖還可以描述對象的位置信息。對于對象位置很重要的問題,全連接的特征表示在很大程度上是無用的。

注意,由特定卷積層提取的表示的一般性(以及因此可重用性)的級別取決于模型中網絡層的深度。模型中較早出現的圖層會提取局部的,高度通用的特征貼圖(例如可視邊緣,顏色和紋理),而較高層的圖層會提取更抽象的概念(例如“貓耳朵”或“狗眼”) 。因此,如果訓練數據集與訓練原始模型的數據集有很大差異,那么最好只使用模型的前幾層來進行特征提取,而不是使用整個卷積網絡的輸出。

在這種情況下,因為ImageNet類集包含多個dog和cat類,所以重用原始模型的全連接層中包含的信息可能是有益的。但是我們會選擇不這樣做,以便涵蓋新問題的類集不與原始模型的類集重疊的更一般情況。通過使用在ImageNet上訓練的VGG16網絡的卷積網絡來實現這一點,從貓和狗圖像中提取有趣的特征,然后在這些特征之上訓練狗與貓的分類器。
Keras中可以直接獲取VGG16模型,包含在keras.applications模塊中。其中還包括其他模型:

  • Xception
  • Inception V3
  • ResNet50
  • VGG16
  • VGG19
  • MobileNet

實例化VGG16模型:

from keras.application import vgg16

conv_base = VGG16(weights='imagenet',include_top=False,input_shape=(150, 150, 3))

構造器的3個參數:

  • weights:讀取權重保存點文件,初始化模型;
  • include_top:是否包含網絡的全連接層。模型,全連接層分類類別在ImageNet上的1000類。因為要使用自己創建的全連接分類器,可以不使用原來的全連接層;
  • input_shape:送到模型中圖片張量的形狀;參數是可選的:如果不傳遞參數,網絡可以處理任意形狀的輸入。
    VGG16網絡模型架構:
>>> conv_base.summary()
Layer (type)                    Output Shape                Param #
================================================================
input_1 (InputLayer)            (None, 150, 150, 3)         0
________________________________________________________________
block1_conv1 (Convolution2D)    (None, 150, 150, 64)        1792
________________________________________________________________
block1_conv2 (Convolution2D)    (None, 150, 150, 64)        36928
________________________________________________________________
block1_pool (MaxPooling2D)      (None, 75, 75, 64)          0
________________________________________________________________
block2_conv1 (Convolution2D)    (None, 75, 75, 128)         73856
________________________________________________________________
block2_conv2 (Convolution2D)    (None, 75, 75, 128)         147584
________________________________________________________________
block2_pool (MaxPooling2D)      (None, 37, 37, 128)         0
________________________________________________________________
block3_conv1 (Convolution2D)    (None, 37, 37, 256)         295168
________________________________________________________________
block3_conv2 (Convolution2D)    (None, 37, 37, 256)         590080
________________________________________________________________
block3_conv3 (Convolution2D)    (None, 37, 37, 256)         590080
________________________________________________________________
block3_pool (MaxPooling2D)      (None, 18, 18, 256)         0
________________________________________________________________
block4_conv1 (Convolution2D)    (None, 18, 18, 512)         1180160
________________________________________________________________
block4_conv2 (Convolution2D)    (None, 18, 18, 512)         2359808
________________________________________________________________
block4_conv3 (Convolution2D)    (None, 18, 18, 512)         2359808
________________________________________________________________
block4_pool (MaxPooling2D)      (None, 9, 9, 512)           0
________________________________________________________________
block5_conv1 (Convolution2D)    (None, 9, 9, 512)           2359808
________________________________________________________________
block5_conv2 (Convolution2D)    (None, 9, 9, 512)           2359808
________________________________________________________________
block5_conv3 (Convolution2D)    (None, 9, 9, 512)           2359808
________________________________________________________________
block5_pool (MaxPooling2D)      (None, 4, 4, 512)           0
================================================================
Total params: 14,714,688
Trainable params: 14,714,688
Non-trainable params: 0

最后一層的特征圖形狀為(4,4,512).之后連接到全連接分類器上。有兩種處理方法:

  • 訓練卷積網絡模型部分,將輸出結果保存在磁盤上,之后讀取磁盤上的數據送到全連接分類器中。優點在于運行高效、快速,因為卷積網絡部分針對每張輸入圖片只運行一次,而卷積部分是最耗時、耗費運算能力資源的;但同時不能使用數據增強;
  • 將全連接分類器和卷積部分整合到一起,在輸入數據上端到端的運行;可以使用數據增強,因為每次輸入模型的圖像都會通過模型經過卷積部分。

不使用數據增強的特征提取
使用ImageDataGenerator將磁盤文件和標簽讀取成張量形式,運行卷積部分的predict提取圖片特征。

import os
import numpy as np
from keras.preprocessing.image import ImageDataGenerator

base_dir = '/Users/fchollet/Downloads/cats_and_dogs_small'
train_dir = os.path.join(base_dir, 'train')#訓練數據
validation_dir = os.path.join(base_dir, 'validation')#驗證數據
test_dir = os.path.join(base_dir, 'test')#測試數據

datagen = ImageDataGenerator(rescale=1./255)#
batch_size = 20

def extract_features(directory, sample_count):#讀取文件,轉換成張量形式;
    features = np.zeros(shape=(sample_count, 4, 4, 512))
    labels = np.zeros(shape=(sample_count))
    generator = datagen.flow_from_directory(directory,
                    target_size=(150, 150),
                    batch_size=batch_size,
                    class_mode='binary')
    i = 0
    for inputs_batch, labels_batch in generator:#生成對應批量數據
        features_batch = conv_base.predict(inputs_batch)#卷積特征提取結果
        features[i * batch_size : (i + 1) * batch_size] = features_batch
        labels[i * batch_size : (i + 1) * batch_size] = labels_batch
        i += 1
        if i * batch_size >= sample_count:
            break
    return features, labels
    
train_features, train_labels = extract_features(train_dir, 2000)
validation_features,validation_labels=extract_features(validation_dir, 1000)
test_features, test_labels = extract_features(test_dir, 1000)

當前提取特征形狀為(samples,4,4,512),在送到全連接層之前,需要先平鋪成(samples,8192),。

train_features = np.reshape(train_features, (2000, 4 * 4 * 512))
validation_features=np.reshape(validation_features, (1000, 4 * 4 * 512))
test_features = np.reshape(test_features, (1000, 4 * 4 * 512))

定義全連接分類器,將特征數據送到分類器中訓練。

from keras import models
from keras import layers
from keras import optimizers

model = models.Sequential()

model.add(layers.Dense(256, activation='relu', input_dim=4 * 4 * 512))
model.add(layers.Dropout(0.5))
model.add(layers.Dense(1, activation='sigmoid'))
model.compile(optimizer=optimizers.RMSprop(lr=2e-5),
            loss='binary_crossentropy',metrics=['acc'])
history = model.fit(train_features, train_labels,epochs=30,
    batch_size=20,
    validation_data=(validation_features, validation_labels))

驗證集、訓練集上損失值和準確率變化情況:

image

驗證集準確率達到90%.但圖示顯示模型從開始就過擬合了。使用數據正增強可以緩解一下。
使用數據增強的特征提取
和第一種方法相比,運算速度更慢、耗費運算資源更多,通常需要GPU。如果GPU上速度還慢,最好使用第一種方法。

from keras import models
from keras import layers

model = models.Sequential()
model.add(conv_base)
model.add(layers.Flatten())
model.add(layers.Dense(256, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))

模型架構為:

>>> model.summary()
Layer (type)                Output Shape                Param #
================================================================
vgg16 (Model)               (None, 4, 4, 512)           14714688
________________________________________________________________
flatten_1 (Flatten)         (None, 8192)                0
________________________________________________________________
dense_1 (Dense)             (None, 256)                 2097408
________________________________________________________________
dense_2 (Dense)             (None, 1)                   257
================================================================
Total params: 16,812,353
Trainable params: 16,812,353
Non-trainable params: 0

在模型訓練之前,需要對卷積部分進行freeze‘凍住’。Freezing網絡層意味著避免在訓練過程網絡層的參數被更新。如果不做‘freeze’處理,訓練過程中卷積部分提取的特征會逐漸改變。
在Keras中,可以通過設置trainable參數為False進行Freeze處理。

conv_base.trainable = False

注意,為了使這些更改生效,必須首先編譯模型。如果在編譯后修改了權重可訓練性,則應重新編譯模型,否則將忽略這些更改。

from keras.preprocessing.image import ImageDataGenerator
from keras import optimizers

train_datagen = ImageDataGenerator(rescale=1./255,
            rotation_range=40,
            width_shift_range=0.2,
            height_shift_range=0.2,
            shear_range=0.2,
            zoom_range=0.2,
            horizontal_flip=True,
            fill_mode='nearest')

test_datagen = ImageDataGenerator(rescale=1./255)

train_generator=train_datagen.flow_from_directory(train_dir,
            target_size=(150, 150), batch_size=20,class_mode='binary')

validation_generator = test_datagen.flow_from_directory(validation_dir,
    target_size=(150, 150),batch_size=20,class_mode='binary')

model.compile(loss='binary_crossentropy',optimizer=optimizers.RMSprop(lr=2e-5),metrics=['acc'])

history = model.fit_generator(train_generator,steps_per_epoch=100,
epochs=30,validation_data=validation_generator,validation_steps=50)

損失值和準確率變化:


image

驗證集上準確率達到96%.

模型微調Fine-tuning

另一種廣泛使用的模型重用技術,對特征提取的補充,就是模型參數微調。微調包括解凍用于特征提取的凍結模型基礎的一些頂層,并聯合訓練模型的新添加部分(在這種情況下,全連接的分類器)和這些頂層。這稱為微調,因為它稍微調整了重復使用的模型的抽象表示,以使它們與手頭的問題更相關。

image

微調網絡模型步驟:

  1. 在已經訓練好的網絡模型上添加自定義網絡模型;
  2. Freeze”凍住“訓練好的模型;
  3. 訓練添加部分網絡;
  4. Unfreeze”解凍“部分base 網絡;
  5. 重新訓練解凍部分和添加部分。

base部分網絡模型:

>>> conv_base.summary()
Layer (type)                Output Shape                Param #
================================================================
input_1 (InputLayer)        (None, 150, 150, 3)         0
________________________________________________________________
block1_conv1 (Convolution2D)(None, 150, 150, 64)        1792
________________________________________________________________
block1_conv2 (Convolution2D)(None, 150, 150, 64)        36928
________________________________________________________________
block1_pool (MaxPooling2D)  (None, 75, 75, 64)          0
________________________________________________________________
block2_conv1 (Convolution2D)(None, 75, 75, 128)         73856
________________________________________________________________
block2_conv2 (Convolution2D)(None, 75, 75, 128)         147584
________________________________________________________________
block2_pool (MaxPooling2D)  (None, 37, 37, 128)         0
________________________________________________________________
block3_conv1 (Convolution2D)(None, 37, 37, 256)         295168
________________________________________________________________
block3_conv2 (Convolution2D)(None, 37, 37, 256)         590080
________________________________________________________________
block3_conv3 (Convolution2D)(None, 37, 37, 256)         590080
________________________________________________________________
block3_pool (MaxPooling2D)  (None, 18, 18, 256)         0
________________________________________________________________
block4_conv1 (Convolution2D)(None, 18, 18, 512)         1180160
________________________________________________________________
block4_conv2 (Convolution2D)(None, 18, 18, 512)         2359808
________________________________________________________________
block4_conv3 (Convolution2D)(None, 18, 18, 512)         2359808
________________________________________________________________
block4_pool (MaxPooling2D)  (None, 9, 9, 512)           0
________________________________________________________________
block5_conv1 (Convolution2D)(None, 9, 9, 512)           2359808
________________________________________________________________
block5_conv2 (Convolution2D)(None, 9, 9, 512)           2359808
________________________________________________________________
block5_conv3 (Convolution2D)(None, 9, 9, 512)           2359808
________________________________________________________________
block5_pool (MaxPooling2D)  (None, 4, 4, 512)           0
================================================================
Total params: 14714688

微調模型的最后3個卷積層,意味著到block4_pool之前都被”凍住“,網絡層block5_conv1,block5_conv2和block5_conv3都是可訓練的。
為什么不微調更多層?為什么不微調整個卷積網絡?可以這么做。但是你需要考慮以下幾點:

  • 卷積網絡中的前幾層編碼更通用,可重用的特征,而更高層的編碼更專業的特征。微調更專業的功能更有用,因為這些功能需要重新用于新問題。微調下層會有快速下降的回報。
  • 訓練的參數越多,越有可能過度擬合。卷積網絡模型有1500萬個參數,因此嘗試在小數據集上訓練它會有風險。

一個很好的策略是只微調卷積基礎中的前兩個或三個層。

conv_base.trainable = True
set_trainable = False
for layer in conv_base.layers:
    if layer.name == 'block5_conv1':#block5_conv1可訓練
        set_trainable = True#flag可訓練
    if set_trainable:
        layer.trainable = True#block5_conv1網絡層設置為可訓練;
    else:
        layer.trainable = False#其它層不可訓練

現在可以開始微調網絡了。使用RMSProp優化器以非常低的學習速率執行此操作。使用低學習率的原因是希望限制對正在微調的三個網絡層的表示所做的修改的幅度。太大的更新可能會損害這些表示。

model.compile(loss='binary_crossentropy',optimizer=optimizers.RMSprop(lr=1e-5),metrics=['acc'])
history = model.fit_generator(train_generator,steps_per_epoch=100,
epochs=100,validation_data=validation_generator,validation_steps=50)

驗證集、測試集上損失函數和準確率變化:


image

請注意,損失曲線沒有顯示任何真正的改善(事實上,它正在惡化)。如果損失沒有減少,準確度如何保持穩定或改善?答案很簡單:展示的是指數損失值的平均值;但是對于準確性而言重要的是損失值的分布,而不是它們的平均值,因為精度是模型預測的類概率的二元閾值的結果。即使沒有反映在平均損失中,該模型仍可能會有所改善。
在測試集上評估:

test_generator = test_datagen.flow_from_directory(test_dir,
    target_size=(150, 150),batch_size=20,class_mode='binary')
test_loss, test_acc = model.evaluate_generator(test_generator,steps=50)
print('test acc:', test_acc)
#97%

小結

  • Convnets是用于計算機視覺任務的最佳機器學習模型。即使在非常小的數據集上也可以從頭開始訓練,并獲得不錯的結果。
  • 在小型數據集上,過度擬合將是主要問題。在處理圖像數據時,數據增強是對抗過度擬合的有效方法;
  • 通過重用現有的卷積網絡模型可以在新數據集上做特征提取;這是處理小圖像數據集的有用技術。
  • 作為特征提取的補充,可以使用模型微調,讓模型適應新問題---以前現有模型可以學習新問題的特征表示,能進一步推動性能。

卷積學習結果可視化

人們常說,深度學習模型是“黑匣子”:學習表示難以提取以及很難以人類可讀的形式呈現。雖然對于某些類型的深度學習模型來說這是部分正確的,但對于convnets來說絕對不是這樣。由convnet學習的表示非常適合可視化,這在很大程度上是因為它們是視覺概念的表示。三種常見的可視化方法:

  • 可視化中間信號輸出(中間激活)--有助于了解連續的convnet層如何轉換輸入數據,以及了解各個convnet過濾器的含義;
  • 可視化convnets過濾器---有助于準確理解convnet中每個過濾器可接受的視覺模式或概念;
  • 可視化圖像中類激活的熱圖---有助于了解圖像的哪些部分被識別為屬于給定的類,從而可以在圖像中本地化對象。

可視化中間激活值

可視化中間激活包括在給定特定輸入的情況下顯示由網絡中的各種卷積和池化層輸出的特征映射(層的輸出通常稱為其激活,激活函數的輸出)。這給出了如何將輸入分解為網絡學習的不同過濾器的視圖。希望從三個維度:寬度,高度和深度(通道)可視化特征圖。每個通道編碼相對獨立的特征,因此可視化這些特征圖的正確方法是通過將每個通道的內容獨立地繪制為2D圖像。

加載保存的模型

from keras.models import load_model
model = load_model('cats_and_dogs_small_2.h5')

img_path = './cats_and_dogs_small/test/cats/cat.1700.jpg'#給定一張圖片
from keras.preprocessing import image
import numpy as np

img = image.load_img(img_path, target_size=(150, 150))
img_tensor = image.img_to_array(img)
img_tensor = np.expand_dims(img_tensor, axis=0)
img_tensor /= 255.

查看所有網絡層的輸出結果:

from keras import models

layer_outputs = [layer.output for layer in model.layers[:8]]
activation_model=models.Model(inputs=model.input,outputs=layer_outputs)

輸入圖像輸入時,此模型返回原始模型中網絡層激活的值。一個多輸出模型:到目前為止,看到的模型只有一個輸入和一個輸出。在一般情況下,模型可以具有任意數量的輸入和輸出。這個有一個輸入和八個輸出:每層激活一個輸出。
模型運行:

activations = activation_model.predict(img_tensor)#輸出:每層激活值一個數組

第一個卷積層結果:

first_layer_activation = activations[0]
print(first_layer_activation.shape)
#(1, 148, 148, 32)
import matplotlib.pyplot as plt
plt.matshow(first_layer_activation[0, :, :, 4], cmap='viridis')#第4通道可視化
image

網絡中所有激活函數值可視化,將8個網絡層激活函數值的所有通道結果顯示出來。

layer_names = []
for layer in model.layers[:8]:
    layer_names.append(layer.name)
images_per_row = 16

for layer_name, layer_activation in zip(layer_names, activations):
    n_features = layer_activation.shape[-1]
    size = layer_activation.shape[1]
    
    n_cols = n_features // images_per_row
    display_grid = np.zeros((size * n_cols, images_per_row * size))

    for col in range(n_cols):
        for row in range(images_per_row):
            channel_image=layer_activation[0,:,:,col*images_per_row+row]
            channel_image -= channel_image.mean()
            channel_image /= channel_image.std()
            channel_image *= 64
            channel_image += 128
            channel_image=np.clip(channel_image,0, 255).astype('uint8')
            display_grid[col*size:(col+1)*size,row*size:(row+1)*size]=channel_image
    scale = 1. / size
    plt.figure(figsize=(scale * display_grid.shape[1],
    scale * display_grid.shape[0]))
    plt.title(layer_name)
    plt.grid(False)
    plt.imshow(display_grid, aspect='auto', cmap='viridis')

[圖片上傳失敗...(image-49e53e-1532251766723)]
值得注意的是:

  • 第一層充當各種邊緣檢測器的集合。在那個階段,激活值幾乎保留了初始圖片中的所有信息;
  • 隨著網絡層的增加,激活變得越來越抽象,在視覺上也不那么容易理解;開始編碼更高級別的概念,如“貓耳”和“貓眼。”更高級別的表示關于圖像的視覺內容越來越少,關于圖像類型的信息越來越多;
  • 激活的稀疏性隨著層的深度而增加:在第一層中,所有濾波器都由輸入圖像激活;但在以下圖層中,越來越多的過濾器為空白。這意味著在輸入圖像中找不到濾鏡編碼的圖案。
    剛剛證明了深度神經網絡所學習的表征的一個重要的普遍特征:由層提取的特征隨著層的深度而變得越來越抽象。更高層的激活越來越少地顯示關于所看到的特定輸入的信息,越來越多關于目標的信息. 深度神經網絡有效地充當信息蒸餾管道,原始數據進入(在這種情況下為RGB圖像)并被重復變換以便過濾掉無關信息(例如,圖像的特定視覺外觀),以及有用的信息被放大和細化(例如,圖像的類)。

可視化卷積核

另一種檢查由convnet學習的過濾器的簡單方法是顯示每個過濾器要響應的視覺模式。這可以通過輸入空間中的漸變上升來完成:將漸變下降應用于convnet的輸入圖像的值空間上;從空白輸入圖像開始,最大化特定過濾器的響應。得到的輸入圖像將是所選濾波器最大響應的圖像。

過程很簡單:您將構建一個損失函數,使給定卷積層中給定濾波器的值最大化,然后您將使用隨機梯度下降來調整輸入圖像的值,以便最大化此激活值。例如,這是在VGG16的block3_conv1中激活過濾器0的損失.

from keras.applications import VGG16
from keras import backend as K

model = VGG16(weights='imagenet',include_top=False)
layer_name = 'block3_conv1'
filter_index = 0
layer_output=model.get_layer(layer_name).output#得到block3_conv1的激活值
loss = K.mean(layer_output[:, :, :, filter_index])

要實現梯度下降,需要相對于模型輸入求損失的梯度。

grads = K.gradients(loss, model.input)[0]

使用梯度正則化平滑梯度值

grads /= (K.sqrt(K.mean(K.square(grads))) + 1e-5)

計算損失張量和梯度張量。使用keras的iterate函數,接收numpy張量,返回關于損失和梯度的張量列表。

iterate = K.function([model.input], [loss, grads])
import numpy as np
loss_value, grads_value = iterate([np.zeros((1, 150, 150, 3))])

將張量轉換為圖片格式:

def deprocess_image(x):
    x -= x.mean()
    x /= (x.std() + 1e-5)
    x *= 0.1
    x += 0.5
    x = np.clip(x, 0, 1)
    x *= 255
    x = np.clip(x, 0, 255).astype('uint8')
    return x

整合卷積核可視化函數:

def generate_pattern(layer_name, filter_index, size=150):
    layer_output = model.get_layer(layer_name).output
    loss = K.mean(layer_output[:, :, :, filter_index])
    grads = K.gradients(loss, model.input)[0]
    grads /= (K.sqrt(K.mean(K.square(grads))) + 1e-5)
    iterate = K.function([model.input], [loss, grads])

    input_img_data = np.random.random((1, size, size, 3)) * 20 + 128.

    step = 1.
    for i in range(40):
        loss_value, grads_value = iterate([input_img_data])
        input_img_data += grads_value * step
  
    img = input_img_data[0]
    return deprocess_image(img)

這些過濾器可視化展示了很多關于如何使用數字網絡層來查看世界:網絡中的每個層都學習了一組過濾器,以便它們的輸入可以表示為過濾器的組合。

類別激活值heatmap可視化

一種可視化技術:有助于理解給定圖像的哪些部分引導其進行最終分類決策的可視化技術。
這種通用類別的技術稱為類激活圖(CAM)可視化,它包括在輸入圖像上生成類激活的熱圖。類激活熱圖是與特定輸出類相關聯的分數的2D網格,針對任何輸入圖像中的每個位置計算,指示每個位置相對于所考慮的類的重要程度。

image

小結

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

推薦閱讀更多精彩內容