2018-07-22-tensorflow-dataset的使用

最近一段時間,在跑模型的過程中,發(fā)現(xiàn)數(shù)據(jù)量很大(1g以上)的時候宙刘,內(nèi)存很容易就爆表了,這是不能接受的牢酵。所幸Tensorflow中對輸入有著比較好的封裝悬包,因此抽時間學(xué)習一下dataset的概念和用法艺沼,這個玩意可以幫助我們用封裝好的方法 一行一行的讀數(shù)據(jù),但是不能幫我們完成batch的操作喲瑞佩。不過batch操作tensorflow中也有提供的晨川!特此用本文進行一下總結(jié)绑警。參考官方教程.

概述

?? tf.data 包提供的 API 就是用來幫助用戶快速的構(gòu)建輸入的管道pipeline的。以文本的輸入為例绞铃,tf.data提供的功能包括:從原始文本中抽取符號声畏;將文本符號轉(zhuǎn)化成查找表(embeddings)激率;把長度不同的輸入字符串轉(zhuǎn)化成規(guī)范的batch數(shù)據(jù)铁追〖韭欤總的來說茫船,這個接口能夠幫助用戶輕松的應(yīng)對大數(shù)據(jù)量的處理琅束,以及不同格式的歸一化處理。

?? tf.data 包主要提供了一下兩個主要的接口:

  • tf.data.Dataset可以用來表示一個序列的元素算谈,每個元素都是tensor或者tensor的集合涩禀。舉例來說,在圖像處理的管道中然眼,上文提到的元素就可以指一個訓(xùn)練樣本(包括輸入tensor和輸出標簽tensor)艾船。創(chuàng)建一個Dataset對象有兩種不同的方式:
    ?? * 從數(shù)據(jù)源構(gòu)造dataset。 (例如 Dataset.from_tensor_slices()) constructs a dataset from one or more tf.Tensor objects.
    ?? * 從其他dataset轉(zhuǎn)換得到新的dataset。 (例如Dataset.batch()) (https://www.tensorflow.org/api_docs/python/tf/data/Dataset) objects.

  • tf.data.Iterator 接口主要作用是獲取數(shù)據(jù)值屿岂。其中Iterator.get_next() 返回數(shù)據(jù)集的下一個元素践宴。很顯然,這個接口在數(shù)據(jù)集和模型之間充當了橋梁的作用爷怀。最簡單的Iterator是 "one-shot iterator"阻肩。這是一種和單一數(shù)據(jù)集綁定的iterator,并且只能遍歷一次运授。如果想要使用更加復(fù)雜的功能烤惊,就可以使用 Iterator.initializer操作重新初始化iterator,這個重新初始化包括了重新設(shè)定參數(shù)吁朦,甚至可以重新設(shè)定數(shù)據(jù)集柒室,這樣的話我們就能夠做到一個數(shù)據(jù)集遍歷多次。

基本原理

?? 本文的這一部分主要描述了構(gòu)造 DatasetIterator 對象的基本方法逗宜,以及如何利用這些對象獲取數(shù)據(jù)雄右。

?? 首先,想要啟動一個輸入數(shù)據(jù)管道纺讲,我們必須定義一個數(shù)據(jù)源(即source)不脯。數(shù)據(jù)源可以很多啦,拿內(nèi)存中tensor當數(shù)據(jù)源也是可以的刻诊,用這兩個方法就好tf.data.Dataset.from_tensors() 或者tf.data.Dataset.from_tensor_slices(). 再比如呢防楷,你的數(shù)據(jù)在磁盤上,并且是Tensorflow親兒子格式TFRecordDataset,那么我們就可以用 tf.data.TFRecordDataset.這個方法则涯。

?? 一旦有了Dataset對象复局,利用鏈式變換將它轉(zhuǎn)換成新的Dataset對象。舉例來說粟判,可以進行的變換包括:逐元素變換Dataset.map()亿昏;多元素變換 Dataset.batch(). See the documentation for tf.data.Dataset

?? 從Dataset中獲取數(shù)據(jù)的最主要方法還是前文提到的iterator的方法档礁。這個方法能夠一次調(diào)用為我們提供一個元素角钩。iterator包括兩個方法:Iterator.initializer主要用來初始化遍歷器;Iterator.get_next()主要用來返回下一個元素呻澜。同時呢递礼,iterator的品種口味有很多,用戶可以根據(jù)自己的需要使用不同的iterator詳情將會在下文中介紹羹幸。

Dataset 結(jié)構(gòu)

??dataset的每一個元素都是同構(gòu)的脊髓。每一個元素是一個或者多個 tf.Tensor 對象,這些對象被稱為組成元素. 每個組成元素都有一個tf.DType 屬性栅受,用來標識組成元素的類型将硝。還有tf.TensorShape](https://www.tensorflow.org/api_docs/python/tf/TensorShape) 對象用來標識組成元素的維度恭朗。

??而利用Dataset.output_typesDataset.output_shapes 這兩個屬性能夠檢查每個元素的輸出類型和輸出大小是否規(guī)范。同時呢依疼,嵌套的類型也是存在的痰腮。下面用例子來說明問題。

import tensorflow as tf

# 4行數(shù)據(jù)
dataset1 = tf.data.Dataset.from_tensor_slices(tf.random_uniform([4, 10]))

print(dataset1.output_shapes)  # (10,)=>1行1行的輸出數(shù)據(jù)
print(dataset1.output_types)

print()
print("##########################################")

# 1個tensor就1個輸出律罢,那多個tensor就多個輸出咯
dataset2 = tf.data.Dataset.from_tensor_slices(
    (tf.random_uniform([4]),
     tf.random_uniform([4, 100], maxval=100, dtype=tf.int32)))
print(dataset2.output_types)  # ==> "(tf.float32, tf.int32)"
print(dataset2.output_shapes)  # ==> "((), (100,))"

print()
print("##########################################")
# 有時候我們也可以先構(gòu)建不同的dataset然后再組合起來
dataset3 = tf.data.Dataset.zip((dataset1, dataset2))
print(dataset3.output_types)
print(dataset3.output_shapes)

print()
print("##########################################")
# 有時候我們也可以給這些tensor起個名字哇诽嘉,不然顯得多亂
named_dataset = tf.data.Dataset.from_tensor_slices({
    "single_value": tf.random_uniform([4]),
    "array": tf.random_uniform([4, 10])
})
print(named_dataset.output_shapes)
print(named_dataset.output_types)

??這里遺留了一個問題,就是如果采用這種拼接的方式對兩個tensor進行封裝弟翘,那么如果tensor的長度不一致可咋辦喲虫腋。

It is often convenient to give names to each component of an element, for example if they represent different features of a training example. In addition to tuples, you can use collections.namedtuple or a dictionary mapping strings to tensors to represent a single element of a Dataset.

    dataset1 = tf.data.Dataset.from_tensor_slices(tf.random_uniform([4, 10]))
    print(dataset1.output_types)  # ==> "tf.float32"
    print(dataset1.output_shapes)  # ==> "(10,)"

    dataset2 = tf.data.Dataset.from_tensor_slices(
       (tf.random_uniform([4]),
        tf.random_uniform([4, 100], maxval=100, dtype=tf.int32)))
    print(dataset2.output_types)  # ==> "(tf.float32, tf.int32)"
    print(dataset2.output_shapes)  # ==> "((), (100,))"

    dataset3 = tf.data.Dataset.zip((dataset1, dataset2))
    print(dataset3.output_types)  # ==> (tf.float32, (tf.float32, tf.int32))
    print(dataset3.output_shapes)  # ==> "(10, ((), (100,)))"

??Dataset的變換支持任何結(jié)構(gòu)的Dataset,使用 Dataset.map(), Dataset.flat_map(), 和 Dataset.filter()這三個變換時將會對每一個元素都進行相同的變化稀余,而元素結(jié)構(gòu)的變換就是Dataset變換的本質(zhì)悦冀。這些東西在后面的介紹中會用到,所以在這里只是給出了一個簡單的介紹睛琳,在后面的應(yīng)用場景中將會具體的介紹使用方法盒蟆。

dataset1 = dataset1.map(lambda x: ...)

dataset2 = dataset2.flat_map(lambda x, y: ...)

# Note: Argument destructuring is not available in Python 3.
dataset3 = dataset3.filter(lambda x, (y, z): ...)

創(chuàng)建iterator

??一旦有了Dataset對象,我們的下一步就是創(chuàng)造一個iterator來從dataset中獲取數(shù)據(jù)师骗。
tf.data 這個接口現(xiàn)在支持以下幾種iterator.

  • one-shot,
  • initializable,
  • reinitializable, and
  • feedable.

??這其中呢历等,one-shot iterator 是最簡單的一種啦。這種遍歷器只支持遍歷單一dataset辟癌,并且還不需要顯式的初始化寒屯。簡單但是有效!大部分的應(yīng)用場景這種遍歷器都是可以handle的黍少。但是它不支持參數(shù)化寡夹。下面給出一個例子:


print()
print("##########################################")
# one-shot iterator的使用
range_dataset = tf.data.Dataset.range(100)
iterator = range_dataset.make_one_shot_iterator()
next_elem = iterator.get_next()
with tf.Session() as sess:
    for i in range(100):
        num = sess.run(next_elem)
        print(num)


注意: 目前,封裝好的模型(Estimator)支持這種遍歷器厂置。至于其他三種iterator在這里就不給介紹啦菩掏,對于我等菜雞沒什么鬼用

從iterator獲取數(shù)據(jù)

??不瞎的話你就會發(fā)現(xiàn),前面其實已經(jīng)多次提到啦獲取數(shù)據(jù)的方法昵济,最簡單的就是使用iterator.get_next()方法來獲取下一個元素咯智绸。當然啦這個方法同樣是lazy_evaluation,也就是說只有在session中運行的時候才會打印出結(jié)果访忿,否則的話知識一個符號化的標記瞧栗。

??另外需要注意的是,當遍歷器遍歷到了dataset的地段的時候就會報錯 tf.errors.OutOfRangeError. 報完錯醉顽,這個遍歷器就瞎了不能用了沼溜,需要重新初始化平挑。所以說呀游添,常見的做法就是包裹一層try catch 咯:

sess.run(iterator.initializer)
while True:
  try:
    sess.run(result)
  except tf.errors.OutOfRangeError:
    break

??嵌套的dataset的使用方法也是很直觀的系草。不過呢,需要注意一點唆涝,就是下面代碼中找都,iterator.get_next()返回值是倆,這倆都是產(chǎn)生自同一個tensor廊酣,所以對其中的任何一個進行run操作都會直接導(dǎo)致iterator進入下一步的循環(huán)中能耻。因此我們常規(guī)操作是如果需要evaluate就把所有的下一個元素同時evaluate

print()
print("##########################################")
# 嵌套的dataset 利用iterator獲取數(shù)據(jù)

nested_iterator=dataset2.make_initializable_iterator()
next1,next2=nested_iterator.get_next()
with tf.Session() as sess:
    sess.run(nested_iterator.initializer)
    num1=sess.run(next1)
    print(num1)

保存iterator狀態(tài)

tf.contrib.data.make_saveable_from_iterator這個方法創(chuàng)建一個 SaveableObject 對象來保存iterator的狀態(tài)亡驰。存下來了之后就可以中斷然后改天再接著訓(xùn)練啦晓猛。,這個對象可以添加到tf.train.Saver 的變量列表中,或者 tf.GraphKeys.SAVEABLE_OBJECTS集合中凡辱,或者直接按照變量的方式進行保存戒职。具體的存儲過程自己看這個教程吧。 Saving and Restoring

# Create saveable object from iterator.
saveable = tf.contrib.data.make_saveable_from_iterator(iterator)

# Save the iterator state by adding it to the saveable objects collection.
tf.add_to_collection(tf.GraphKeys.SAVEABLE_OBJECTS, saveable)
saver = tf.train.Saver()

with tf.Session() as sess:

  if should_checkpoint:
    saver.save(path_to_checkpoint)

# Restore the iterator state.
with tf.Session() as sess:
  saver.restore(sess, path_to_checkpoint)

讀取數(shù)據(jù)

??前面把基本流程都講完啦透乾,但是:樵铩!乳乌!沒有用捧韵!我們想要的是大數(shù)據(jù)能夠讀進內(nèi)存!還是沒有解決汉操。如果原來的數(shù)據(jù)可以放進內(nèi)存再来,我們可以按照上面說的方法進行操作,但是那樣我們還廢了牛鼻子勁在這學(xué)這玩意干嘛磷瘤。所以在這里tensorflow支持使用Dataset對象管理文件其弊,包括TFRecord,txt,csv等等文件。從直接讀進內(nèi)存的那種我們就不講啦膀斋!只重點解剖txt這樣一種方法梭伐,殺雞儆猴!以儆效尤仰担!

Consuming NumPy arrays

Consuming TFRecord data

Consuming text data

??許許多多的數(shù)據(jù)集都是text文件組成的糊识。tf.data.TextLineDataset 接口提供了一種炒雞簡單的方法從這些數(shù)據(jù)文件中讀取。我們提供只需要提供文件名(1個或者好多個都可以)摔蓝。這個接口就會自動構(gòu)造一個dataset赂苗,這個dataset的每一個元素就是一行數(shù)據(jù),是一個string類型的tensor贮尉。

filenames = ["/var/data/file1.txt", "/var/data/file2.txt"]

dataset = tf.data.Dataset.from_tensor_slices(filenames)

# Use `Dataset.flat_map()` to transform each file as a separate nested dataset,
# and then concatenate their contents sequentially into a single "flat" dataset.
# * Skip the first line (header row).
# * Filter out lines beginning with "#" (comments).
dataset = dataset.flat_map(
    lambda filename: (
        tf.data.TextLineDataset(filename)
        .skip(1)
        .filter(lambda line: tf.not_equal(tf.substr(line, 0, 1), "#"))))

Consuming CSV data

使用Dataset.map()進行預(yù)處理

?? Dataset.map(f)方法的通過對每個元素進行f變換得到一個新的dataset使用的方法如下拌滋。

# Transforms a scalar string `example_proto` into a pair of a scalar string and
# a scalar integer, representing an image and its label, respectively.
def _parse_function(example_proto):
  features = {"image": tf.FixedLenFeature((), tf.string, default_value=""),
              "label": tf.FixedLenFeature((), tf.int32, default_value=0)}
  parsed_features = tf.parse_single_example(example_proto, features)
  return parsed_features["image"], parsed_features["label"]

# Creates a dataset that reads all of the examples from two files, and extracts
# the image and label features.
filenames = ["/var/data/file1.tfrecord", "/var/data/file2.tfrecord"]
dataset = tf.data.TFRecordDataset(filenames)
dataset = dataset.map(_parse_function)

在map函數(shù)中調(diào)用任意函數(shù)tf.py_func()

??處于性能的要求哇,我們建議你盡量使用Tensorflow內(nèi)部提供的函數(shù)進行數(shù)據(jù)的預(yù)處理猜谚。但是呢败砂,我們有時候不得不用一些外部的函數(shù)赌渣,要這么做就需要調(diào)用 tf.py_func()這個方法啦,具體的使用實例如下:

import cv2

# Use a custom OpenCV function to read the image, instead of the standard
# TensorFlow `tf.read_file()` operation.
def _read_py_function(filename, label):
  image_decoded = cv2.imread(filename.decode(), cv2.IMREAD_GRAYSCALE)
  return image_decoded, label

# Use standard TensorFlow operations to resize the image to a fixed shape.
def _resize_function(image_decoded, label):
  image_decoded.set_shape([None, None, None])
  image_resized = tf.image.resize_images(image_decoded, [28, 28])
  return image_resized, label

filenames = ["/var/data/image1.jpg", "/var/data/image2.jpg", ...]
labels = [0, 37, 29, 1, ...]

dataset = tf.data.Dataset.from_tensor_slices((filenames, labels))
dataset = dataset.map(
    lambda filename, label: tuple(tf.py_func(
        _read_py_function, [filename, label], [tf.uint8, label.dtype])))
dataset = dataset.map(_resize_function)

構(gòu)造batch數(shù)據(jù)

簡單batch

??最簡單的構(gòu)造方法就是把幾個輸入元素堆疊起來組成一個新的元素昌犹,這就構(gòu)造出來一個batch的數(shù)據(jù)啦坚芜。代碼如下
The simplest form of batching stacks n consecutive elements of a dataset into a single element. The Dataset.batch() transformation does exactly this, with the same constraints as the tf.stack() operator, applied to each component of the elements: i.e. for each component i, all elements must have a tensor of the exact same shape.

inc_dataset = tf.data.Dataset.range(100)
dec_dataset = tf.data.Dataset.range(0, -100, -1)
dataset = tf.data.Dataset.zip((inc_dataset, dec_dataset))
batched_dataset = dataset.batch(4)

iterator = batched_dataset.make_one_shot_iterator()
next_element = iterator.get_next()

print(sess.run(next_element))  # ==> ([0, 1, 2,   3],   [ 0, -1,  -2,  -3])
print(sess.run(next_element))  # ==> ([4, 5, 6,   7],   [-4, -5,  -6,  -7])
print(sess.run(next_element))  # ==> ([8, 9, 10, 11],   [-8, -9, -10, -11])

Batching tensors with padding

??不用說你也知道上面的這種情況實在是太簡單啦,它假設(shè)所有的tensor都是有相同長度的斜姥,但是這怎么可能呢鸿竖??铸敏?為了解決這個問題缚忧,,tensorflow中特地提出了Dataset.padded_batch()這種構(gòu)造方法杈笔。通過指定一個或多個需要補充(設(shè)置默認值的)維度 搔谴,tensorflow就會自動幫你完成這樣的填充。注意:填充是對每個元素填充成同樣的長度桩撮,而不是不夠一個batch 湊夠一個batch

dataset = tf.data.Dataset.range(100)
# 第一行0個0敦第;第二行1個1;第三行2個2店量;
dataset = dataset.map(lambda x: tf.fill([tf.cast(x, tf.int32)], x))
dataset = dataset.padded_batch(4, padded_shapes=[None])

iterator = dataset.make_one_shot_iterator()
next_element = iterator.get_next()

print(sess.run(next_element))  # ==> [[0, 0, 0], [1, 0, 0], [2, 2, 0], [3, 3, 3]]
print(sess.run(next_element))  # ==> [[4, 4, 4, 4, 0, 0, 0],
                               #      [5, 5, 5, 5, 5, 0, 0],
                               #      [6, 6, 6, 6, 6, 6, 0],
                               #      [7, 7, 7, 7, 7, 7, 7]]

??另外芜果,這個接口還有一些功能,包括:支持對不同的維度進行不同的padding策略融师;支持變長或者定長的padding策略右钾;支持自定義的默認值;

訓(xùn)練流程

多個epoch的訓(xùn)練

??tf.data 包提供兩種主要的方法用來進行多個epoch 的訓(xùn)練旱爆。

??最簡單的做法是使用 Dataset.repeat() 這個變換操作舀射。這個變換操作相當于疊加了幾次數(shù)據(jù)集,對后端毫無影響怀伦。具體代碼如下:

filenames = ["/var/data/file1.tfrecord", "/var/data/file2.tfrecord"]
dataset = tf.data.TFRecordDataset(filenames)
dataset = dataset.map(...)
dataset = dataset.repeat(10)
dataset = dataset.batch(32)

??簡單的方法好是好脆烟!但是有個問題,如果我們每個epoch后面想要打印一下信息或者計算一下錯誤率房待,這可腫么辦邢羔??所以還有一個不那么智能的操作桑孩“莺祝基本的邏輯就是,我們就一個epoch一個epoch的跑流椒,跑過了敏簿,就會報錯,報錯就說明一個epoch跑完了,就測測錯誤率惯裕,然后重新初始化一下遍歷器就好了温数。具體操作如下:

filenames = ["/var/data/file1.tfrecord", "/var/data/file2.tfrecord"]
dataset = tf.data.TFRecordDataset(filenames)
dataset = dataset.map(...)
dataset = dataset.batch(32)
iterator = dataset.make_initializable_iterator()
next_element = iterator.get_next()

# Compute for 100 epochs.
for _ in range(100):
  sess.run(iterator.initializer)
  while True:
    try:
      sess.run(next_element)
    except tf.errors.OutOfRangeError:
      break

  # [Perform end-of-epoch calculations here.]

總結(jié)

??總結(jié)一下,后面還有什么隨機化的輸入 什么的轻猖,我暫時用不到就先不寫了帆吻。但是也有一些用的到的卻沒在教程中涉及域那,比如說我們的單詞字典的構(gòu)造(初步想法是使用feature column)咙边,字母字典的構(gòu)造?還沒想好怎么寫次员,后續(xù)補上败许。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市淑蔚,隨后出現(xiàn)的幾起案子市殷,更是在濱河造成了極大的恐慌,老刑警劉巖刹衫,帶你破解...
    沈念sama閱讀 211,948評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件醋寝,死亡現(xiàn)場離奇詭異,居然都是意外死亡带迟,警方通過查閱死者的電腦和手機音羞,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,371評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來仓犬,“玉大人嗅绰,你說我怎么就攤上這事〔蠹蹋” “怎么了窘面?”我有些...
    開封第一講書人閱讀 157,490評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長叽躯。 經(jīng)常有香客問我财边,道長,這世上最難降的妖魔是什么点骑? 我笑而不...
    開封第一講書人閱讀 56,521評論 1 284
  • 正文 為了忘掉前任制圈,我火速辦了婚禮,結(jié)果婚禮上畔况,老公的妹妹穿的比我還像新娘鲸鹦。我一直安慰自己,他們只是感情好跷跪,可當我...
    茶點故事閱讀 65,627評論 6 386
  • 文/花漫 我一把揭開白布馋嗜。 她就那樣靜靜地躺著,像睡著了一般吵瞻。 火紅的嫁衣襯著肌膚如雪葛菇。 梳的紋絲不亂的頭發(fā)上甘磨,一...
    開封第一講書人閱讀 49,842評論 1 290
  • 那天,我揣著相機與錄音眯停,去河邊找鬼济舆。 笑死,一個胖子當著我的面吹牛莺债,可吹牛的內(nèi)容都是我干的滋觉。 我是一名探鬼主播,決...
    沈念sama閱讀 38,997評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼齐邦,長吁一口氣:“原來是場噩夢啊……” “哼椎侠!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起措拇,我...
    開封第一講書人閱讀 37,741評論 0 268
  • 序言:老撾萬榮一對情侶失蹤我纪,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后丐吓,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體浅悉,經(jīng)...
    沈念sama閱讀 44,203評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,534評論 2 327
  • 正文 我和宋清朗相戀三年券犁,在試婚紗的時候發(fā)現(xiàn)自己被綠了术健。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,673評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡族操,死狀恐怖苛坚,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情色难,我是刑警寧澤泼舱,帶...
    沈念sama閱讀 34,339評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站枷莉,受9級特大地震影響娇昙,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜笤妙,卻給世界環(huán)境...
    茶點故事閱讀 39,955評論 3 313
  • 文/蒙蒙 一冒掌、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蹲盘,春花似錦股毫、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,770評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春趣席,著一層夾襖步出監(jiān)牢的瞬間兵志,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,000評論 1 266
  • 我被黑心中介騙來泰國打工宣肚, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留想罕,地道東北人。 一個月前我還...
    沈念sama閱讀 46,394評論 2 360
  • 正文 我出身青樓霉涨,卻偏偏與公主長得像按价,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子嵌纲,可洞房花燭夜當晚...
    茶點故事閱讀 43,562評論 2 349

推薦閱讀更多精彩內(nèi)容