動(dòng)手學(xué)深度學(xué)習(xí)PyTorch版-機(jī)器翻譯

1. 詞嵌入(word2vec)

自然語言是一套用來表達(dá)含義的復(fù)雜系統(tǒng)栏笆。在這套系統(tǒng)中垃你,詞是表義的基本單元。顧名思義缭嫡,詞向量是用來表示詞的向量缔御,也可被認(rèn)為是詞的特征向量或表征。把詞映射為實(shí)數(shù)域向量的技術(shù)也叫詞嵌入(word embedding)妇蛀。

* 為何不采用One-short

雖然one-hot詞向量構(gòu)造起來很容易耕突,但通常并不是一個(gè)好選擇。一個(gè)主要的原因是评架,one-hot詞向量無法準(zhǔn)確表達(dá)不同詞之間的相似度眷茁,如我們常常使用的余弦相似度。對(duì)于向量 x,y∈?d 纵诞,它們的余弦相似度是它們之間夾角的余弦值上祈。
兩個(gè)向量間的余弦值可以通過使用歐幾里得點(diǎn)積公式求出:
x\cdot y=\left \|x\right\|\left \|y\right\|cos\theta
similarity = cos\theta = {x^Ty\over\left \|x\right\|\left \|y\right\|}\in[-1,1]
由于任何兩個(gè)不同詞的one-hot向量的余弦相似度都為0,多個(gè)不同詞之間的相似度難以通過one-hot向量準(zhǔn)確地體現(xiàn)出來。相似性范圍從-1到1:-1意味著兩個(gè)向量指向的方向正好截然相反雇逞,1表示它們的指向是完全相同的,0通常表示它們之間是獨(dú)立的茁裙,而在這之間的值則表示中間的相似性或相異性塘砸。
對(duì)于文本匹配,屬性向量x和y通常是文檔中的詞頻向量晤锥。余弦相似性掉蔬,可以被看作是在比較過程中把文件長(zhǎng)度正規(guī)化的方法。

word2vec工具的提出正是為了解決上面這個(gè)問題矾瘾。它將每個(gè)詞表示成一個(gè)定長(zhǎng)的向量女轿,并使得這些向量能較好地表達(dá)不同詞之間的相似和類比關(guān)系。word2vec工具包含了兩個(gè)模型壕翩,即跳字模型(skip-gram)和連續(xù)詞袋模型(continuous bag of words蛉迹,CBOW)。CBOW對(duì)小型數(shù)據(jù)庫(kù)比較合適放妈,而Skip-Gram在大型語料中表現(xiàn)更好北救。

  1. Skip-Gram 跳字模型:假設(shè)背景詞由中心詞生成,即建模 P(w_o\mid w_c)芜抒,其中 w_c 為中心詞珍策,w_o 為任一背景詞;

  2. CBOW (continuous bag-of-words) 連續(xù)詞袋模型:假設(shè)中心詞由背景詞生成宅倒,即建模 P(w_c\mid \mathcal{W}_o)攘宙,其中 \mathcal{W}_o 為背景詞的集合。

Skip-Gram 模型的實(shí)現(xiàn)與CBOW 實(shí)現(xiàn)類似拐迁,大致從以下四個(gè)部分展開:

  1. PTB 數(shù)據(jù)集
  2. Skip-Gram 跳字模型
  3. 負(fù)采樣近似
  4. 訓(xùn)練模型

這篇文章對(duì)Word2Vec的數(shù)學(xué)原理講解的不錯(cuò)

1. PTB 數(shù)據(jù)集

簡(jiǎn)單來說误窖,Word2Vec 能從語料中學(xué)到如何將離散的詞映射為連續(xù)空間中的向量痘番,并保留其語義上的相似關(guān)系。那么為了訓(xùn)練 Word2Vec 模型,我們就需要一個(gè)自然語言語料庫(kù)蔓同,模型將從中學(xué)習(xí)各個(gè)單詞間的關(guān)系,這里我們使用經(jīng)典的 PTB 語料庫(kù)進(jìn)行訓(xùn)練沧侥。PTB (Penn Tree Bank) 是一個(gè)常用的小型語料庫(kù)瞎疼,它采樣自《華爾街日?qǐng)?bào)》的文章,包括訓(xùn)練集割卖、驗(yàn)證集和測(cè)試集前酿。我們將在PTB訓(xùn)練集上訓(xùn)練詞嵌入模型。

載入數(shù)據(jù)集

數(shù)據(jù)集訓(xùn)練文件 ptb.train.txt 示例:

aer banknote berlitz calloway centrust cluett fromstein gitano guterman ...
pierre  N years old will join the board as a nonexecutive director nov. N 
mr.  is chairman of  n.v. the dutch publishing group 
with open('/home/kesci/input/ptb_train1020/ptb.train.txt', 'r') as f:
    lines = f.readlines() # 該數(shù)據(jù)集中句子以換行符為分割
    raw_dataset = [st.split() for st in lines] # st是sentence的縮寫鹏溯,單詞以空格為分割
print('# sentences: %d' % len(raw_dataset))

# 對(duì)于數(shù)據(jù)集的前3個(gè)句子罢维,打印每個(gè)句子的詞數(shù)和前5個(gè)詞
# 句尾符為 '' ,生僻詞全用 '' 表示丙挽,數(shù)字則被替換成了 'N'
for st in raw_dataset[:3]:
    print('# tokens:', len(st), st[:5])
建立詞語索引
counter = collections.Counter([tk for st in raw_dataset for tk in st]) # tk是token的縮寫肺孵,連續(xù)兩個(gè)for匀借,先執(zhí)行st in raw_dataset,去除sentence平窘,再執(zhí)行tk in st吓肋,取出sentence中的token詞。counter用來存儲(chǔ)
counter = dict(filter(lambda x: x[1] >= 5, counter.items())) # 只保留在數(shù)據(jù)集中至少出現(xiàn)5次的詞
idx_to_token = [tk for tk, _ in counter.items()]#返回詞典對(duì)象
token_to_idx = {tk: idx for idx, tk in enumerate(idx_to_token)}#對(duì)于一個(gè)可迭代的(iterable)/可遍歷的對(duì)象(如列表瑰艘、字符串)是鬼,enumerate將其組成一個(gè)索引序列,利用它可以同時(shí)獲得索引和值紫新。Counter(計(jì)數(shù)器)是對(duì)字典的補(bǔ)充均蜜,用于追蹤值的出現(xiàn)次數(shù)。
dataset = [[token_to_idx[tk] for tk in st if tk in token_to_idx]
           for st in raw_dataset] # raw_dataset中的單詞在這一步被轉(zhuǎn)換為對(duì)應(yīng)的idx
num_tokens = sum([len(st) for st in dataset])
'# tokens: %d' % num_tokens
二次采樣

文本數(shù)據(jù)中一般會(huì)出現(xiàn)一些高頻詞芒率,如英文中的“the”“a”和“in”囤耳。通常來說,在一個(gè)背景窗口中敲董,一個(gè)詞(如“chip”)和較低頻詞(如“microprocessor”)同時(shí)出現(xiàn)比和較高頻詞(如“the”)同時(shí)出現(xiàn)對(duì)訓(xùn)練詞嵌入模型更有益紫皇。因此,訓(xùn)練詞嵌入模型時(shí)可以對(duì)詞進(jìn)行二次采樣腋寨。 具體來說聪铺,數(shù)據(jù)集中每個(gè)被索引詞 w_i 將有一定概率被丟棄,該丟棄概率為
P(w_i)=\max(1-\sqrt{\frac{t}{f(w_i)}},0).

其中 f(w_i) 是數(shù)據(jù)集中詞 w_i 的個(gè)數(shù)與總詞數(shù)之比萄窜,常數(shù) t 是一個(gè)超參數(shù)(實(shí)驗(yàn)中設(shè)為 10^{?4})铃剔。可見查刻,只有當(dāng) f(w_i)>t 時(shí)键兜,我們才有可能在二次采樣中丟棄詞 w_i,并且越高頻的詞被丟棄的概率越大穗泵。具體的代碼如下:

def discard(idx):
    '''
    @params:
        idx: 單詞的下標(biāo)
    @return: True/False 表示是否丟棄該單詞
    '''
    return random.uniform(0, 1) < 1 - math.sqrt(
        1e-4 / counter[idx_to_token[idx]] * num_tokens)

subsampled_dataset = [[tk for tk in st if not discard(tk)] for st in dataset]
print('# tokens: %d' % sum([len(st) for st in subsampled_dataset]))

def compare_counts(token):
    return '# %s: before=%d, after=%d' % (token, sum(
        [st.count(token_to_idx[token]) for st in dataset]), sum(
        [st.count(token_to_idx[token]) for st in subsampled_dataset]))

print(compare_counts('the'))
print(compare_counts('join'))
提取中心詞和背景詞
def get_centers_and_contexts(dataset, max_window_size):
    '''
    @params:
        dataset: 數(shù)據(jù)集為句子的集合普气,每個(gè)句子則為單詞的集合,此時(shí)單詞已經(jīng)被轉(zhuǎn)換為相應(yīng)數(shù)字下標(biāo)
        max_window_size: 背景詞的詞窗大小的最大值
    @return:
        centers: 中心詞的集合
        contexts: 背景詞窗的集合佃延,與中心詞對(duì)應(yīng)现诀,每個(gè)背景詞窗則為背景詞的集合
    '''
    centers, contexts = [], []
    for st in dataset:
        if len(st) < 2:  # 每個(gè)句子至少要有2個(gè)詞才可能組成一對(duì)“中心詞-背景詞”
            continue
        centers += st
        for center_i in range(len(st)):
            window_size = random.randint(1, max_window_size) # 隨機(jī)選取背景詞窗大小
            indices = list(range(max(0, center_i - window_size),
                                 min(len(st), center_i + 1 + window_size)))
            indices.remove(center_i)  # 將中心詞排除在背景詞之外
            contexts.append([st[idx] for idx in indices])
    return centers, contexts

all_centers, all_contexts = get_centers_and_contexts(subsampled_dataset, 5)

tiny_dataset = [list(range(7)), list(range(7, 10))]
print('dataset', tiny_dataset)
for center, context in zip(*get_centers_and_contexts(tiny_dataset, 2)):
    print('center', center, 'has contexts', context)
Skip-Gram 跳字模型

在跳字模型中,每個(gè)詞被表示成兩個(gè) d 維向量履肃,用來計(jì)算條件概率仔沿。假設(shè)這個(gè)詞在詞典中索引為 i ,當(dāng)它為中心詞時(shí)向量表示為 \boldsymbol{v}_i\in\mathbb{R}^d尺棋,而為背景詞時(shí)向量表示為 \boldsymbol{u}_i\in\mathbb{R}^d 封锉。設(shè)中心詞 w_c 在詞典中索引為 c,背景詞 w_o 在詞典中索引為 o,我們假設(shè)給定中心詞生成背景詞的條件概率滿足下式:

P(w_o\mid w_c)=\frac{\exp(\boldsymbol{u}_o^\top \boldsymbol{v}_c)}{\sum_{i\in\mathcal{V}}\exp(\boldsymbol{u}_i^\top \boldsymbol{v}_c)}

PyTorch 預(yù)置的 Embedding 層
embed = nn.Embedding(num_embeddings=10, embedding_dim=4)
print(embed.weight)

x = torch.tensor([[1, 2, 3], [4, 5, 6]], dtype=torch.long)
print(embed(x))
PyTorch 預(yù)置的批量乘法
X = torch.ones((2, 1, 4))
Y = torch.ones((2, 4, 6))
print(torch.bmm(X, Y).shape)
Skip-Gram 模型的前向計(jì)算
def skip_gram(center, contexts_and_negatives, embed_v, embed_u):
    '''
    @params:
        center: 中心詞下標(biāo)成福,形狀為 (n, 1) 的整數(shù)張量
        contexts_and_negatives: 背景詞和噪音詞下標(biāo)碾局,形狀為 (n, m) 的整數(shù)張量
        embed_v: 中心詞的 embedding 層
        embed_u: 背景詞的 embedding 層
    @return:
        pred: 中心詞與背景詞(或噪音詞)的內(nèi)積,之后可用于計(jì)算概率 p(w_o|w_c)
    '''
    v = embed_v(center) # shape of (n, 1, d)
    u = embed_u(contexts_and_negatives) # shape of (n, m, d)
    pred = torch.bmm(v, u.permute(0, 2, 1)) # bmm((n, 1, d), (n, d, m)) => shape of (n, 1, m)
    return pred
負(fù)采樣近似

由于 softmax 運(yùn)算考慮了背景詞可能是詞典 \mathcal{V} 中的任一詞奴艾,對(duì)于含幾十萬或上百萬詞的較大詞典擦俐,就可能導(dǎo)致計(jì)算的開銷過大。我們將以 skip-gram 模型為例握侧,介紹負(fù)采樣 (negative sampling) 的實(shí)現(xiàn)來嘗試解決這個(gè)問題。

負(fù)采樣方法用以下公式來近似條件概率 P(w_o\mid w_c)=\frac{\exp(\boldsymbol{u}_o^\top \boldsymbol{v}_c)}{\sum_{i\in\mathcal{V}}\exp(\boldsymbol{u}_i^\top \boldsymbol{v}_c)}

P(w_o\mid w_c)=P(D=1\mid w_c,w_o)\prod_{k=1,w_k\sim P(w)}^K P(D=0\mid w_c,w_k)

其中 P(D=1\mid w_c,w_o)=\sigma(\boldsymbol{u}_o^\top\boldsymbol{v}_c)嘿期,\sigma(\cdot) 為 sigmoid 函數(shù)品擎。對(duì)于一對(duì)中心詞和背景詞,我們從詞典中隨機(jī)采樣 K 個(gè)噪聲詞(實(shí)驗(yàn)中設(shè) K=5)备徐。根據(jù) Word2Vec 論文的建議萄传,噪聲詞采樣概率 P(w) 設(shè)為 w 詞頻與總詞頻之比的 0.75 次方。
*注:除負(fù)采樣方法外蜜猾,還有層序 softmax (hiererarchical softmax) 方法也可以用來解決計(jì)算量過大的問題

批量讀取數(shù)據(jù)
class MyDataset(torch.utils.data.Dataset):
    def __init__(self, centers, contexts, negatives):
        assert len(centers) == len(contexts) == len(negatives)
        self.centers = centers
        self.contexts = contexts
        self.negatives = negatives
        
    def __getitem__(self, index):
        return (self.centers[index], self.contexts[index], self.negatives[index])

    def __len__(self):
        return len(self.centers)
    
def batchify(data):
    '''
    用作DataLoader的參數(shù)collate_fn
    @params:
        data: 長(zhǎng)為batch_size的列表秀菱,列表中的每個(gè)元素都是__getitem__得到的結(jié)果
    @outputs:
        batch: 批量化后得到 (centers, contexts_negatives, masks, labels) 元組
            centers: 中心詞下標(biāo),形狀為 (n, 1) 的整數(shù)張量
            contexts_negatives: 背景詞和噪聲詞的下標(biāo)蹭睡,形狀為 (n, m) 的整數(shù)張量
            masks: 與補(bǔ)齊相對(duì)應(yīng)的掩碼衍菱,形狀為 (n, m) 的0/1整數(shù)張量
            labels: 指示中心詞的標(biāo)簽,形狀為 (n, m) 的0/1整數(shù)張量
    '''
    max_len = max(len(c) + len(n) for _, c, n in data)
    centers, contexts_negatives, masks, labels = [], [], [], []
    for center, context, negative in data:
        cur_len = len(context) + len(negative)
        centers += [center]
        contexts_negatives += [context + negative + [0] * (max_len - cur_len)]
        masks += [[1] * cur_len + [0] * (max_len - cur_len)] # 使用掩碼變量mask來避免填充項(xiàng)對(duì)損失函數(shù)計(jì)算的影響
        labels += [[1] * len(context) + [0] * (max_len - len(context))]
        batch = (torch.tensor(centers).view(-1, 1), torch.tensor(contexts_negatives),
            torch.tensor(masks), torch.tensor(labels))
    return batch

batch_size = 512
num_workers = 0 if sys.platform.startswith('win32') else 4

dataset = MyDataset(all_centers, all_contexts, all_negatives)
data_iter = Data.DataLoader(dataset, batch_size, shuffle=True,
                            collate_fn=batchify, 
                            num_workers=num_workers)
for batch in data_iter:
    for name, data in zip(['centers', 'contexts_negatives', 'masks',
                           'labels'], batch):
        print(name, 'shape:', data.shape)
    break
訓(xùn)練模型
損失函數(shù)

應(yīng)用負(fù)采樣方法后肩豁,我們可利用最大似然估計(jì)的對(duì)數(shù)等價(jià)形式將損失函數(shù)定義為如下

\sum_{t=1}^T\sum_{-m\le j\le m,j\ne 0} [-\log P(D=1\mid w^{(t)},w^{(t+j)})-\sum_{k=1,w_k\sim P(w)^K}\log P(D=0\mid w^{(t)},w_k)]

根據(jù)這個(gè)損失函數(shù)的定義脊串,我們可以直接使用二元交叉熵?fù)p失函數(shù)進(jìn)行計(jì)算:

class SigmoidBinaryCrossEntropyLoss(nn.Module):
    def __init__(self):
        super(SigmoidBinaryCrossEntropyLoss, self).__init__()
    def forward(self, inputs, targets, mask=None):
        '''
        @params:
            inputs: 經(jīng)過sigmoid層后為預(yù)測(cè)D=1的概率
            targets: 0/1向量,1代表背景詞清钥,0代表噪音詞
        @return:
            res: 平均到每個(gè)label的loss
        '''
        inputs, targets, mask = inputs.float(), targets.float(), mask.float()
        res = nn.functional.binary_cross_entropy_with_logits(inputs, targets, reduction="none", weight=mask)
        res = res.sum(dim=1) / mask.float().sum(dim=1)
        return res

loss = SigmoidBinaryCrossEntropyLoss()

pred = torch.tensor([[1.5, 0.3, -1, 2], [1.1, -0.6, 2.2, 0.4]])
label = torch.tensor([[1, 0, 0, 0], [1, 1, 0, 0]]) # 標(biāo)簽變量label中的1和0分別代表背景詞和噪聲詞
mask = torch.tensor([[1, 1, 1, 1], [1, 1, 1, 0]])  # 掩碼變量
print(loss(pred, label, mask))

def sigmd(x):
    return - math.log(1 / (1 + math.exp(-x)))
print('%.4f' % ((sigmd(1.5) + sigmd(-0.3) + sigmd(1) + sigmd(-2)) / 4)) # 注意1-sigmoid(x) = sigmoid(-x)
print('%.4f' % ((sigmd(1.1) + sigmd(-0.6) + sigmd(-2.2)) / 3))
模型初始化
embed_size = 100
net = nn.Sequential(nn.Embedding(num_embeddings=len(idx_to_token), embedding_dim=embed_size),
                    nn.Embedding(num_embeddings=len(idx_to_token), embedding_dim=embed_size))
訓(xùn)練模型
def train(net, lr, num_epochs):
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    print("train on", device)
    net = net.to(device)
    optimizer = torch.optim.Adam(net.parameters(), lr=lr)
    for epoch in range(num_epochs):
        start, l_sum, n = time.time(), 0.0, 0
        for batch in data_iter:
            center, context_negative, mask, label = [d.to(device) for d in batch]
            
            pred = skip_gram(center, context_negative, net[0], net[1])
            
            l = loss(pred.view(label.shape), label, mask).mean() # 一個(gè)batch的平均loss
            optimizer.zero_grad()
            l.backward()
            optimizer.step()
            l_sum += l.cpu().item()
            n += 1
        print('epoch %d, loss %.2f, time %.2fs'
              % (epoch + 1, l_sum / n, time.time() - start))

train(net, 0.01, 5)
測(cè)試模型
def get_similar_tokens(query_token, k, embed):
    '''
    @params:
        query_token: 給定的詞語
        k: 近義詞的個(gè)數(shù)
        embed: 預(yù)訓(xùn)練詞向量
    '''
    W = embed.weight.data
    x = W[token_to_idx[query_token]]
    # 添加的1e-9是為了數(shù)值穩(wěn)定性
    cos = torch.matmul(W, x) / (torch.sum(W * W, dim=1) * torch.sum(x * x) + 1e-9).sqrt()
    _, topk = torch.topk(cos, k=k+1)
    topk = topk.cpu().numpy()
    for i in topk[1:]:  # 除去輸入詞
        print('cosine sim=%.3f: %s' % (cos[i], (idx_to_token[i])))
        
get_similar_tokens('chip', 3, net[0])

參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末琼锋,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子祟昭,更是在濱河造成了極大的恐慌缕坎,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,454評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件篡悟,死亡現(xiàn)場(chǎng)離奇詭異谜叹,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)恰力,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門叉谜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人踩萎,你說我怎么就攤上這事停局。” “怎么了?”我有些...
    開封第一講書人閱讀 157,921評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵董栽,是天一觀的道長(zhǎng)码倦。 經(jīng)常有香客問我,道長(zhǎng)锭碳,這世上最難降的妖魔是什么袁稽? 我笑而不...
    開封第一講書人閱讀 56,648評(píng)論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮擒抛,結(jié)果婚禮上推汽,老公的妹妹穿的比我還像新娘。我一直安慰自己歧沪,他們只是感情好歹撒,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,770評(píng)論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著诊胞,像睡著了一般暖夭。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上撵孤,一...
    開封第一講書人閱讀 49,950評(píng)論 1 291
  • 那天迈着,我揣著相機(jī)與錄音,去河邊找鬼邪码。 笑死裕菠,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的闭专。 我是一名探鬼主播糕韧,決...
    沈念sama閱讀 39,090評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼喻圃!你這毒婦竟也來了萤彩?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,817評(píng)論 0 268
  • 序言:老撾萬榮一對(duì)情侶失蹤斧拍,失蹤者是張志新(化名)和其女友劉穎雀扶,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體肆汹,經(jīng)...
    沈念sama閱讀 44,275評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡愚墓,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,592評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了昂勉。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片浪册。...
    茶點(diǎn)故事閱讀 38,724評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖岗照,靈堂內(nèi)的尸體忽然破棺而出村象,到底是詐尸還是另有隱情笆环,我是刑警寧澤,帶...
    沈念sama閱讀 34,409評(píng)論 4 333
  • 正文 年R本政府宣布厚者,位于F島的核電站躁劣,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏库菲。R本人自食惡果不足惜账忘,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,052評(píng)論 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望熙宇。 院中可真熱鬧鳖擒,春花似錦、人聲如沸烫止。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,815評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽烈拒。三九已至,卻和暖如春广鳍,著一層夾襖步出監(jiān)牢的瞬間荆几,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,043評(píng)論 1 266
  • 我被黑心中介騙來泰國(guó)打工赊时, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留吨铸,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,503評(píng)論 2 361
  • 正文 我出身青樓祖秒,卻偏偏與公主長(zhǎng)得像诞吱,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子竭缝,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,627評(píng)論 2 350