Transformer导论之——Transformer


description: >-
传统的RNN,GRU,LSTM他们都有一个问题,就是不能并行计算。同时虽然LSTM解决了长期依赖性的问题,但如果我们有一个很长很长的上万字的文本要处理,这个时候LSTM还是显得力不从心。Transformer模型很好的解决了上面的两个问题,它在2017年于论文Attention
is All you Need [1]发表,之后用于Bert,GPT2,GPT3等模型中。


😀 Transformer

Self-Attention 自注意力机制

Self-attention是一种机器学习中常用的技术,通常用于序列到序列的任务,如机器翻译、文本摘要、问答等。Self-attention 的目的是根据输入序列中各个位置之间的依赖关系,计算出每个位置的特征向量表示,从而得到一个表示整个序列的向量。在自然语言处理领域,self-attention被广泛应用于Transformer模型中,取得了很好的效果。

Self-Attention的定义

Self-attention机制是一种将输入序列的不同部分关联起来的方法,可以在不引入循环或卷积结构的情况下学习到序列中的全局依赖关系。在self-attention中,每个输入元素都会计算一个注意力得分,该得分衡量它与其他元素之间的相对重要性,并用这些得分来计算一个加权和。

Self-Attention的过程概览

Self-attention的过程可以分为三个步骤:计算注意力得分、计算加权和、输出。

  1. 计算注意力得分:对于输入序列中的每个元素,都需要计算它与其他元素之间的相似度得分,从而确定其在计算加权和时的权重。这个相似度得分可以使用点积、加性或其他方式来计算。计算出的注意力得分可以看作是一个权重向量,它确定了每个输入元素在计算加权和时的贡献。
  2. 计算加权和:使用注意力得分来计算每个元素的加权和,以获得对输入序列的综合表示。这个加权和可以表示为输入序列中所有元素的线性组合,每个元素的权重由对应的注意力得分确定。
  3. 输出:将加权和作为输出,这个输出可以作为后续层的输入或输出给到其他任务。

自注意力的计算公式

在自注意力模型(self-attention)中,给定一个输入序列,我们需要计算该序列中每个元素与其他元素的相关度,以此来推断其重要性。假设我们有一个输入序列 x_1, x_2, ..., x_n,那么它的自注意力表示为A \in \mathbb{R}^{n \times n},其中 A_{i,j} 表示 x_ix_j的相关度。那么,自注意力的计算公式可以表示为:

A_{i,j}={\frac{\exp(e_{i,j})}{\sum_{k=1}^{n}\exp(e_{i,k})}},

其中, e_{i,j}是一个表示 x_ix_j 相关度的能量值。在使用自注意力时,通常会采用点积(dot-product)或加性(additive)的方式来计算 e_{i,j}

在点积的情况下, e_{i,j} 的计算公式为:

e_{i,j}=x_{i}^{T}x_{j}

在加性的情况下, e_{i,j} 的计算公式为:

e_{i,j}=w_{2}^{T}\operatorname{tanh}(w_{1}x_{i}+w_{2}x_{j}),

其中 w_1w_2 是需要学习的权重矩阵。

自注意力计算的详细过程

在自注意力机制中,每个输入元素都可以被视为一个向量。对于每个向量,都可以通过一个矩阵变换来生成三个新向量:查询向量、键向量和值向量。这些新向量可以表示不同的信息,例如查询向量可以表示要查询的内容,键向量可以表示文本中的单词,值向量可以表示单词的嵌入表示。

在计算自注意力时,我们首先将查询向量与所有键向量进行点积运算,然后将结果除以一个可学习的缩放因子,得到一组分数。这些分数可以视为查询向量与不同键向量之间的相似度分数,用于衡量它们之间的相关性。接下来,我们可以使用分数对值向量进行加权汇聚,以获得对查询向量的响应表示。

在自注意力机制中,每个输入元素都可以作为查询向量、键向量和值向量的来源,因此每个元素都可以被视为自身与序列中所有其他元素之间的关系的表示。通过这种方式,自注意力可以有效地捕捉序列中元素之间的长程依赖关系,从而在各种自然语言处理任务中取得了很好的效果。

假设我们有一个输入序列 x = {x_1, x_2, ..., x_n},其中每个 x_i 都是一个向量,维度为 d。我们可以通过一个线性变换来将每个向量映射到三个不同的向量,即查询向量 q_i、键向量 k_i和值向量 v_i

q_i = W_q x_i, \ k_i = W_k x_i, \ v_i = W_v x_i

其中 W_q, W_k, W_v \in \mathbb{R}^{d \times d} 是可学习的权重矩阵。接下来,我们计算每对查询向量和键向量之间的点积得分,然后对值向量进行加权求和,以得到对查询向量的响应表示:

\mathrm{Attention}(Q, K, V) = \mathrm{softmax}(\frac{QK^T}{\sqrt{d}})V

其中 Q= [q_1, q_2, ..., q_n] \in \mathbb{R}^{d \times n}是查询矩阵, K = [k_1, k_2, ..., k_n] \in \mathbb{R}^{d \times n}是键矩阵,V = [v_1, v_2, ..., v_n] \in \mathbb{R}^{d \times n}是值矩阵, \mathrm{softmax}是对每行进行 softmax 操作, \sqrt{d}是缩放因子,用于平衡点积得分的量级。

最终,对于输入序列中的每个元素 x_i,我们都可以通过自注意力机制它和序列中其他元素的注意力。

多头注意力

在自然语言处理中,多头注意力通常用于机器翻译和文本生成任务中。具体来说,输入序列被分成多个头部,每个头部可以关注序列中的不同部分,并计算出一个对每个位置的表示。然后,这些表示被合并成一个整体表示,以便于后续的模型处理。

在计算机视觉中,多头注意力可以用于图像分类和目标检测等任务中。具体来说,输入图像被分成多个头部,每个头部可以关注图像中的不同区域,并计算出一个对每个区域的表示。然后,这些表示被合并成一个整体表示,以便于后续的模型处理。

总之,多头注意力是一种强大的注意力机制,可以帮助模型更好地理解和表示输入数据。

多头注意力的计算过程

在多头注意力中,输入序列首先被分成若干个子序列,每个子序列都会经过一个独立的注意力机制来计算其注意力权重。然后,每个子序列的输出向量将被拼接在一起,形成最终的多头注意力输出向量。

下面是多头注意力的计算过程:

将输入序列 X \in \mathbb{R}^{n \times d}通过 h 个线性变换(称为“头”)转换为 h个查询 Q_1, Q_2, ..., Q_hh个键 K_1, K_2, ..., K_hh 个值 V_1, V_2, ..., V_h,其中每个头的维度为 d/h

Q_i = XW_i^Q,K_i = XW_i^K, V_i = XW_i^V, i=1,2,...,h

这里 W_i^Q \in \mathbb{R}^{d \times d/h}W_i^K \in \mathbb{R}^{d \times d/h}W_i^V \in \mathbb{R}^{d \times d/h}分别是用于将输入序列 X 转换为查询 Q_i、键 K_i 和值 V_i 的线性变换矩阵, d 是输入序列的维度, h是头的数量。每个头的维度为 d/h ,因此每个头可以关注输入序列中的不同部分。

接下来,对于每个头 i,计算其注意力权重 A_i,该权重表示该头在输入序列中关注的重要程度。这里采用前面说的点积注意力机制:

A_i = \text{softmax}(\frac{Q_iK_i^T}{\sqrt{d/h}})
\sqrt{d/h}是用于缩放点积的常数,旨在避免点积过大或过小而导致的梯度问题。然后,将注意力权重 A_i与值 V_i 相乘并相加,得到头 i 的输出向量 O_i

O_i = A_iV_i

最后,将所有头的输出向量拼接在一起,得到多头注意力的输出向量 O \in \mathbb{R}^{n \times d}

O = \text{Concat}(O_1, O_2, ..., O_h)

多头注意力的输出向量 O 可以作为下一层模型的输入,例如 Transformer 模型中的前馈神经网络。多头注意力机制可以帮助模型更好地理解序列数据中的信息,从而提高模型的性能。

image.png

Transformer

Transformer 是一种基于自注意力机制(self-attention mechanism)的深度神经网络,它是自然语言处理领域中的一项重要技术。Transformer 最早由 Google 提出,已经被广泛应用于机器翻译、文本生成、语言模型等任务中。

Transformer 的核心思想是使用自注意力机制来实现对输入序列的编码和对输出序列的解码。自注意力机制可以让模型对输入序列中的不同位置进行关注,并将不同位置的信息整合起来。这种关注机制可以看作是一种在序列中进行“跨步”连接(skip connection)的方式,使得模型可以更好地捕捉序列中的长程依赖关系。

Transformer 包含两个主要模块:Encoder 和 Decoder。Encoder 模块将输入序列映射到一个高维空间中,而 Decoder 模块则根据 Encoder 模块生成的编码信息,逐步生成目标序列。

Encoder 模块由多个相同的层级组成,每个层级包含两个子层级:多头自注意力层(Multi-Head Self-Attention Layer)和全连接前馈层(Fully Connected Feedforward Layer)。多头自注意力层用于对输入序列进行编码,全连接前馈层用于对编码后的序列进行进一步处理。Decoder 模块也由多个相同的层级组成,每个层级包含三个子层级:多头自注意力层、编码器-解码器注意力层(Encoder-Decoder Attention Layer)和全连接前馈层。

在多头自注意力层中,模型通过计算每个输入序列元素与所有元素的相似度,得到每个元素对于其他元素的权重,然后使用这些权重进行加权平均,得到每个元素的向量表示。这样,模型可以在不同层级中对输入序列的不同部分进行关注。

在编码器-解码器注意力层中,模型使用编码器层级生成的信息来计算每个目标序列元素与输入序列元素的相似度,然后使用这些相似度进行加权平均,得到目标序列元素的向量表示。这样,模型可以将输入序列中相关的信息与目标序列中的信息相结合,从而更好地生成目标序列。

总之,Transformer 通过自注意力机制实现对序列的编码和解码,使得模型能够更好地捕捉序列中的依赖关系,进而提高自然语言处理等任务的效果。

Encoder

Transformer 的 Encoder 模块是由多个相同的层级组成的,每个层级包含两个子层级:多头自注意力层(Multi-Head Self-Attention Layer)和全连接前馈层(Fully Connected Feedforward Layer)。下面详细介绍这两个子层级的原理:

多头自注意力层

多头自注意力层是 Transformer 的核心部分,它通过对输入序列中每个元素与所有元素的相似度进行计算,得到每个元素对于其他元素的权重,并使用这些权重进行加权平均,得到每个元素的向量表示。这个过程可以看做是将序列中的每个元素与其他元素进行“跨步”连接(skip connection),从而更好地捕捉序列中的长程依赖关系。 具体地,多头自注意力层包含以下几个步骤:

首先,将输入序列经过三个线性变换(即,分别进行投影)得到 Q(Query)、K(Key)、V(Value)三个矩阵。

接着,计算 Q 和 K 的点积得到相似度矩阵,然后进行 softmax 归一化,得到每个元素对于其他元素的权重。 将权重和 V 矩阵相乘并加权求和,得到每个元素的向量表示。

最后,将每个元素的向量表示通过一个线性变换得到最终输出。

需要注意的是,多头自注意力层中的 Q、K、V 矩阵都是由输入序列经过不同的线性变换得到的,这些不同的线性变换分别对应不同的“头”(Head),每个头学习到不同的表示,从而可以捕捉序列中不同的特征。在计算相似度矩阵时,使用的是所有头学习到的表示的点积,从而综合考虑了不同头的信息。更通俗的说,我们把每次计算注意力看作是一次循环,每次循环就是一个头。

全连接前馈层

全连接前馈层是多头自注意力层的一个补充,它用于对编码后的序列进行进一步处理,增强模型的表示能力。具体地,全连接前馈层包含两个线性变换,中间使用激活函数(如 ReLU)进行非线性变换,从而生成更加复杂的特征表示。

总之,Transformer 的 Encoder 模块通过多头自注意力层和全连接前馈层对输入序列进行编码,从而捕捉序列中的依赖关系和特征表示。这些编码信息可以传递给 Decoder 模块,用于生成目标序列。

Add&Norm

Transformer中的Add & Norm是指在每个Multi-Head Attention和Feedforward层之后进行的一种规范化技术,目的是加快模型收敛速度并提高模型性能。这种想法来自于ResNet。

在Multi-Head Attention和Feedforward层中,模型进行一些线性变换和非线性变换,这些变换可能会导致梯度消失或梯度爆炸问题。为了解决这个问题,Transformer在每个层后添加了一个残差连接(residual connection),将输入和输出相加。在残差连接后,使用Layer Normalization对结果进行规范化。Layer Normalization是一种对数据进行归一化的方法,通过对每个特征维度上的数据进行标准化,使得不同特征维度上的数据具有相同的分布。最后,将归一化的结果与残差连接相加,得到该层的最终输出。

Add & Norm技术能够有效地减轻梯度消失和梯度爆炸问题,同时也有助于加速模型的收敛速度。

Decoder

decoder 的输入通常包括两个部分:

  1. 前一个时间步的输出:在解码器中,前一个时间步的输出是当前时间步的输入。因此,Transformer decoder 的输入包括前一个时间步的输出向量,即上一次解码器输出的结果。
  2. 编码器输出的表示:编码器会将输入序列转换成一个表示,该表示包含输入序列中每个位置的信息。解码器需要利用这个表示来指导自己生成正确的输出。因此,Transformer decoder 的输入还包括编码器输出的表示,通常是编码器输出的所有位置的向量的加权平均值(注意到这里权重的计算是通过注意力机制实现的)。

Masked Mult-Head Attention

Decoder的第一个子层是一个“masked”多头自注意力层,这意味着在计算注意力时,只允许当前位置之前的位置作为查询进行注意力计算,不允许当前位置之后的位置参与计算。这是因为在解码器中,我们需要逐步生成输出,而不是一次性生成所有输出。如果允许当前位置之后的位置参与计算,那么就相当于我们在生成当前位置的输出时使用了后面位置的信息,这会导致模型泄露未来信息,使得模型在生成输出时产生错误。

具体来说,如果我们允许当前位置使用后面位置的信息进行计算,那么解码器在生成当前位置的输出时,就会知道未来位置的信息,这违背了解码器的设计原则,也就是要根据之前生成的输出来生成后续的输出,而不是利用未来信息来影响当前输出。

这种未来信息泄露会导致模型在生成输出时产生错误,因为模型会过度依赖未来信息,而忽略当前位置及之前的信息,从而导致模型对输入的理解出现偏差,输出的结果也就不准确了。

因此,为了避免这种情况,解码器中使用“masked”多头自注意力层来限制只使用当前位置及其之前的信息进行计算,保证每个位置的输出只受前面位置的影响,从而避免了未来信息的泄露。

image.png

位置编码

在Transformer模型中,为了将序列的位置信息引入模型中,需要对输入序列的每个位置进行编码。这是通过在输入序列中添加一个位置编码向量来实现的。位置编码向量可以被看作是一个与词向量同样维度的向量,其中每个元素的值是基于该位置以及每个维度的信息计算得到的。具体地,对于位置 pos和维度 i,位置编码向量 PE_{pos, i} 的计算方式如下:

PE_{\mathrm{pos,}i}=\begin{cases}\sin\left(\frac{px}{10000^{2/4}\mathrm{medel}}\right)&i\text{is even}\\ \cos\left(\frac{pg^{2/4}\mathrm{med}}{10000^{2(i-1)/d}\mathrm{med}}\right)&i\text{is odd}\end{cases}

其中, PE_{pos, i} 表示位置编码矩阵中位置 pos上的第 i 维元素, d_{\text{model}} 是词向量和位置编码向量的维度, pos 是当前位置的索引。公式中的 sin 和 $cos$$ 函数分别代表正弦函数和余弦函数。它们能够给每个位置编码向量赋予一个独特的模式,从而区分不同位置的输入。在计算中,位置编码向量会被加到对应的词向量中,从而产生最终的输入向量。

需要注意的是,由于位置编码向量是通过正弦和余弦函数进行计算的,所以在计算中不需要额外的训练,也不需要对每个位置编码向量进行更新。位置编码向量只需要在模型的初始化阶段计算一次,然后在每次输入序列的编码中使用即可。

Transformer的pytorch实现

首先,我们需要导入所需的库和模块:

import torch
import torch.nn as nn
import torch.nn.functional as F
import math

然后,我们定义Transformer模型的主要组件,包括编码器、解码器和整个Transformer模型本身。

在编码器和解码器中,我们实现了多头自注意力机制(multi-head self-attention)和前馈神经网络(feed-forward network)这两个核心组件。

在Transformer模型中,我们将编码器和解码器组合在一起,并添加一些额外的组件,如嵌入层(embedding layer)、位置编码器(position encoding)和输出层(output layer)。

class MultiHeadAttention(nn.Module):
    def __init__(self, d_model, n_heads):
        super(MultiHeadAttention, self).__init__()
        self.d_model = d_model  # 模型的维度
        self.n_heads = n_heads  # 多头注意力的头数
        self.d_k = d_model // n_heads  # 每个头的维度,保证能够整除
        
        # 创建权重矩阵
        self.W_Q = nn.Linear(d_model, d_model)  # 查询向量的权重矩阵
        self.W_K = nn.Linear(d_model, d_model)  # 键向量的权重矩阵
        self.W_V = nn.Linear(d_model, d_model)  # 值向量的权重矩阵
        
        # 最后的线性层
        self.W_O = nn.Linear(d_model, d_model)  # 输出向量的权重矩阵
        
    def forward(self, Q, K, V, mask=None):
        batch_size = Q.size(0)  # 获取输入数据的批次大小
        
        # 通过线性层,分别计算 Q、K、V 的投影向量
        Q = self.W_Q(Q)
        K = self.W_K(K)
        V = self.W_V(V)
        
        # 将 Q、K、V 投影向量分裂为多个头
        Q = Q.view(batch_size, -1, self.n_heads, self.d_k)
        K = K.view(batch_size, -1, self.n_heads, self.d_k)
        V = V.view(batch_size, -1, self.n_heads, self.d_k)
        
        # 计算注意力得分
        scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k)
        
        # ...


MultiHeadAttention类中,我们实现了多头自注意力机制,其中包含了以下主要部分:

  1. __init__ 方法:初始化函数,定义了模型的维度、多头注意力的头数、每个头的维度,并创建了权重矩阵(查询、键、值、输出)。
  2. forward 方法:前向传播函数,用于计算多头自注意力机制的输出。在此函数中,我们首先通过线性层,将输入的 Q、K、V 分别投影到 d_model 维度空间上。
  3. 接着,我们将 Q、K、V 投影向量分裂为多个头,以便进行并行计算。
  4. 然后,我们计算注意力得分 scores,通过将 Q 与 K 转置后相乘,再除以 math.sqrt(self.d_k)。注意力得分用于计算每个值向量的权重,以便对值向量进行加权求和。

在这里,我们只是计算了注意力得分,并没有进行权重的计算。接下来,我们将使用 softmax 函数将注意力得分转换为权重:

# 对 scores 进行缩放和掩码操作
if mask is not None:
mask = mask.unsqueeze(1)
scores = scores.masked_fill(mask == 0, -1e9)

# 将注意力得分进行 softmax 计算
    attn_weights = F.softmax(scores, dim=-1)
    
    # 将权重与 V 向量相乘
    attn_output = torch.matmul(attn_weights, V)
    
    # 将多头注意力向量拼接在一起
    attn_output = attn_output.view(batch_size, -1, self.d_model)
    
    # 通过最后的线性层,得到最终的多头注意力向量
    attn_output = self.W_O(attn_output)
    
    return attn_output, attn_weights
class FeedForward(nn.Module):
def init(self, d_model, d_ff):
super(FeedForward, self).init()
 # 创建两个线性层
    self.linear_1 = nn.Linear(d_model, d_ff)
    self.linear_2 = nn.Linear(d_ff, d_model)
    
def forward(self, x):
    # 通过 ReLU 激活函数
    x = F.relu(self.linear_1(x))
    x = self.linear_2(x)
    return x
class EncoderLayer(nn.Module):
def init(self, d_model, n_heads, d_ff, dropout=0.1):
super(EncoderLayer, self).init()
self.multihead_attn = MultiHeadAttention(d_model, n_heads)
self.ff = FeedForward(d_model, d_ff)
self.norm1 = nn.LayerNorm(d_model)
self.norm2 = nn.LayerNorm(d_model)
self.dropout1 = nn.Dropout(dropout)
self.dropout2 = nn.Dropout(dropout)

以上的代码是 Transformer 模型的一部分,包括了 MultiHeadAttention、FeedForward 和 EncoderLayer 三个类。下面是代码解释:

  • MultiHeadAttention:多头注意力机制,将输入的 Q、K、V 矩阵分别通过线性变换得到 Q、K、V 的查询矩阵、键矩阵和值矩阵,然后计算注意力得分,并通过 softmax 函数将注意力得分转换为权重,最后将权重与 V 向量相乘得到多头注意力向量。
  • FeedForward:前馈神经网络,通过两个线性层和 ReLU 激活函数对输入进行变换。
  • EncoderLayer:编码器层,包括多头注意力机制、前馈神经网络、LayerNormalization 和 Dropout 层,其中 LayerNormalization 是为了减少训练过程中的内部协变量偏移,Dropout 是为了防止过拟合。

这三个类是 Transformer 模型的重要组成部分,可以用于语言建模、机器翻译等任务中。其中 MultiHeadAttention 的思想也被广泛应用于其他领域的深度学习模型中。

Refernce 引用

  1. Vaswani, Ashish et al. “Attention is All you Need.” ArXiv abs/1706.03762 (2017): n. pag.
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容