强化学习

一个强化学习的入门者,仅用于自己学习的记录

强化学习

Q-learining 算法

Pytorch 强化学习算法实现

https://github.com/p-christ/Deep-Reinforcement-Learning-Algorithms-with-PyTorch

# 讲的很详细, 循序渐进,含有代码,很适合入门

DQN算法

一些概念

  • 当前累计奖励增量更新:
    \begin{aligned} Q_{k} & =\frac{1}{k} \sum_{i=1}^{k} r_{i} \\ & =\frac{1}{k}\left(r_{k}+\sum_{i=1}^{k-1} r_{i}\right) \\ & =\frac{1}{k}\left(r_{k}+(k-1) Q_{k-1}\right) \\ & =\frac{1}{k}\left(r_{k}+k Q_{k-1}-Q_{k-1}\right) \\ & =Q_{k-1}+\frac{1}{k}\left[r_{k}-Q_{k-1}\right] \end{aligned}

  • 占用度量: 归一化的占用度量用于衡量在一个智能体决策与一个动态环境的交互过程中,采样到一个具体的状态动作对(state-action pair)的概率分布

    给定两个策略及其与一个动态环境交互得到的两个占用度量,那么当且仅当这个两个占用度量相同时,这两个策略相同。

  • 策略: 指的是智能体在不同的状态下如何选择动作。 强化学习的策略在训练过程中不断的更新, 一个策略对应的价值就是一个占用度量下对应的奖励的期望。 因此寻找平最优策略就是寻找最优占用度量。

  • 强化学习最终的目标就是寻找最优策略,使智能体和环境交互中的价值最大化。

马尔可夫过程

  • 状态之间的转移, 用元祖(S, P)表示,其中S 表示状态的集合, P表示状态转移的概率矩阵
  • 给定一个马尔可夫的过程, 我们可以从某个状态出发,根据它的状态转移矩阵生成一个状态序列(episode),这个步骤叫做采样(sampling)

马尔可夫奖励

  • 在马尔可夫过程中加入奖励因子r和折扣因子\gamma, 就得到马尔可夫奖励过程(MRP)
  • 在一个马尔可夫奖励过程中,从t时刻的状态s_t开始直到终止状态时,所有奖励的衰减之和称为回报。
    G_{t}=R_{t}+\gamma R_{t+1}+\gamma^{2} R_{t+2}+\cdots=\sum_{k=0}^{\infty} \gamma^{k} R_{t+k}
    其中R_t表示t时刻获取的奖励。

这里我的理解是,因为从某个状态s_{strat}开始, 按照概率转移矩阵就可以获取下一个状态,假设可以走到终止状态。这时候形成的序列中,有些状态会出现多次,有些出现一次,有些甚至不出现。假设采样了一个的状态序列为[s_1, s_8, s_3, s_4, s_3, s_8, s_1...,..s_{end}], 那么在第0时刻(本质上就是采样的顺序)s_1的期望回报G_0就是按照上面的公式计算整个序列,序列中也可能会包含同样的状态, 比如第6时刻的s_1

价值函数

  • 在马尔科夫奖励中,一个状态的期望回报(即从这个状态出发的未来累积奖励的期望),称为这个状态的价值。 所有状态的价值就组成了价值函数,价值函数的输入为某个状态,输出为这个状态的价值:
    V(s) = \mathbb{E}[G_t|S_t=s] = \mathbb{E}[R_{t}+\gamma R_{t+1}+\gamma^{2} R_{t+2}+\cdots] =\mathbb{E}[R_t+\gamma V(S_{t+1}) | S_t= s]

  • 利用状态转移矩阵,可得到贝尔曼方程计算状态价值函数
    V(s) = r(s) + \gamma \sum_{s^{\prime} \in S} p(s^{\prime}| s) V(s^{\prime})
    r(s)s 状态获取的即时奖励。

  • 将上式子写成矩阵的方式:
    V = R + \gamma PV

马尔可夫决策

  • 在马尔科夫奖励过程中加上动作,就称为马尔可夫决策过程。

  • 智能体的策略\pi表示,策略函数\pi(a \mid s)=P\left(A_{t}=a \mid S_{t}=s\right) 表示输入状态为s时,采用动作a的概率。

  • 基于策略\pi状态价值函数为:
    V^{\pi}(s)=\mathbb{E}_{\pi}\left[G_{t} \mid S_{t}=s\right]

  • 动作价值函数表示马尔可夫决策过程遵循决策\pi时, 对当前状态s执行动作a得到的期望回报:
    Q^{\pi}(s, a)=\mathbb{E}_{\pi}\left[G_{t} \mid S_{t}=s, A_{t}=a\right] = r(s, a)+\gamma \sum_{s^{\prime} \in S} P\left(s^{\prime} \mid s, a\right) V^{\pi}\left(s^{\prime}\right)

  • 状态价值函数和动作价值函数的关系:
    V^{\pi}(s)=\sum_{a \in A} \pi(a \mid s) Q^{\pi}(s, a)

综上得到状态价值函数和动作价值函数的贝尔曼期望方程:
\begin{aligned} V^{\pi}(s) & =\mathbb{E}_{\pi}\left[R_{t}+\gamma V^{\pi}\left(S_{t+1}\right) \mid S_{t}=s\right] \\ & =\sum_{a \in A} \pi(a \mid s)\left(r(s, a)+\gamma \sum_{s^{\prime} \in S} p\left(s^{\prime} \mid s, a\right) V^{\pi}\left(s^{\prime}\right)\right) \\ Q^{\pi}(s, a) & =\mathbb{E}_{\pi}\left[R_{t}+\gamma Q^{\pi}\left(S_{t+1}, A_{t+1}\right) \mid S_{t}=s, A_{t}=a\right] \\ & =r(s, a)+\gamma \sum_{s^{\prime} \in S} p\left(s^{\prime} \mid s, a\right) \sum_{a^{\prime} \in A} \pi\left(a^{\prime} \mid s^{\prime}\right) Q^{\pi}\left(s^{\prime}, a^{\prime}\right) \end{aligned}

  • 马尔可夫决策中状态的奖励,通过动作边缘化:
    r^{\prime}(s) = \sum_{a \in A} \pi (a|s)r(s,a)
  • 马尔可夫决策中状态的转移概率,通过动作边缘化:
    P^{\prime}(s^{\prime}|s) = \sum_{a \in A} \pi (a|s)P(s^{\prime}|s,a)
  • 这是马尔卡夫决策过程就转换为马尔可夫奖励

蒙特卡洛方法(要先完成序列采样)

  • 使用蒙特卡洛方法来估计一个策略在一个马尔可夫奖励过程中的状态价值函数
    V^{\pi}(s)=\mathbb{E}_{\pi}\left[G_{t} \mid S_{t}=s\right] \approx \frac{1}{N} \sum_{i=1}^{N} G_{t}^{(i)}
  • 根据上述公式,当计算一个状态的期望回报时,可以使用策略在MDP上采样很多条序列,计算从这个状态出发的回报再求期望。
  • 简单的说,就是不断的进行序列采样,计算采样到状态的累计奖励。
  1. 使用策略 \pi 采样若干条序列:

s_{0}^{(i)} \stackrel{a_{0}^{(i)}}{\longrightarrow} r_{0}^{(i)}, s_{1}^{(i)} \stackrel{a_{1}^{(i)}}{\longrightarrow} r_{1}^{(i)}, s_{2}^{(i)} \stackrel{a_{2}^{(i)}}{\longrightarrow} \cdots \stackrel{a_{T-1}^{(i)}}{\longrightarrow} r_{T-1}^{(i)}, s_{T}^{(i)}

  1. 对每一条序列中的每一时间步 t的状态 s进行以下操作:
  • 更新状态 s 的计数器 N(s) \leftarrow N(s)+1 ;
  • 更新状态 s 的总回报 M(s) \leftarrow M(s)+G_{t} ;
  1. 每一个状态的价值被估计为回报的平均值 V(s)=M(s) / N(s)
  • 状态s在序列中出现一次,就计算一次期望回报,当采样很多次序列时,假设状态s出现了N次,求均值就行。
    根据大数定律,当 N(s) \rightarrow \infty ,有 V(s) \rightarrow V^{\pi}(s) 。计算回报的期望时,除了可以把所有的回报加起来除以次数,还有一种增量更新的方法。对于每个状态 s 和对应回报 G ,进行如下计算:

    • N(s) \leftarrow N(s)+1
    • V(s) \leftarrow V(s)+\frac{1}{N(s)}(G-V(S))
  • 核心代码 (在这里是按照逆序列更新的)

def MC(episodes, V, N, gamma):
    # episodes 采样的序列列表【[s0---> s2--->s4--->....s_结束], [s0---> s3--->s8--->....s_结束],...】
    for episode in episodes:
    # episode  一个序列[s0---> s2--->s4--->....s_结束]
        G = 0
        for i in range(len(episode) - 1, -1, -1):  #一个序列从后往前计算
            (s, a, r, s_next) = episode[i]
            # 即使奖励+折扣回报
            G = r + gamma * G
            # 计数器
            N[s] = N[s] + 1
            # 更新状态s的价值
            V[s] = V[s] + (G - V[s]) / N[s]
  • 不同策略访问到的状态的概率分布是不同的,因此策略的状态价值函数是不同的。

动态规划(必须知道转移概率矩阵)

策略迭代
  • 策略评估: 使用贝尔曼期望方程来得到一个策略的状态价值函数, 当我们知道奖励函数状态转移函数,根据下一个状态的价值来计算当前状态。根据动态规划的思想,把计算下一个可能状态的价值当做一个子问题, 把计算当前状态的价值看作当前问题。 在得知子问题的解后,就可以求解当前问题。更一般的,考虑所有的状态,就变成了用上一轮的状态价值函数来计算当前这一轮的状态价值函数,即 V^{k+1}(s)= \sum_{a \in A} \pi (a|s) (r(s,a)+ \gamma \sum_{s^{\prime} \in S}P(s^{\prime} |s,a)V^k (s^{\prime} ) )
    k \rightarrow \infty\{V^{k}\}会收敛到V^{\pi}. 由于不断需要做贝尔曼期望方程的迭代,策略评估需要很大的计算代价,实际中,某轮max_{s \in S} | V^{k+1}(s)- V^{k}(s)|的值非常小,则可以结束。
  • 这里完全没看懂。。。很模糊。贝尔曼方程是根据下一个状态计算当前状态,怎么就成了使用上一轮状态计算当前这一轮的了? k代表的是每一轮。
  • 虽然看懂代码的过程了,但是还不明白为啥这么算? 代码的过程是,每轮遍历所有的状态和动作,然后利用上一轮计算的状态价值,使用贝尔曼来更新当前这一轮的状态价值,然后执行很多轮。。。也就是不断使用贝尔曼方程计算状态价值,一直到收敛。
  • 突然又想明白了,在计算当前状态时,无法知道下一个状态的值,因此,就是用上一轮获取的下一个状态的值来计算。 另一个问题是,为何要一轮一轮的计算? 每一轮代表的是什么?
  • 策略提升: 假设智能体在某一个状态s采取动作a之后的动作依旧遵循策略\pi, 此时得到的期望回报就是动作价值Q^{\pi}(s,a), 如果有Q^{\pi}(s,a) > V_{\pi}(s), 说明在该状态下存在一个更好的策略回报更高。现在假设存在一个策略\pi^{\prime}, 在任意一个状态s下,都满足Q^{\pi}(s,a) \geqslant V^{\pi}(s) , 则任意状态下有:V^{\pi^{\prime}}(s) \geqslant V^{\pi}(s)

  • 我们可以直接贪心地在每一个状态选择动作价值最大的动作,也就是在状态s中,选择动作价值最大的那些a
    \pi^{\prime}(s)=\arg \max _{a} Q^{\pi}(s, a)=\arg \max _{a}\left\{r(s, a)+\gamma \sum_{s^{\prime}} P\left(s^{\prime} \mid s, a\right) V^{\pi}\left(s^{\prime}\right)\right\}

  • 策略迭代和价值迭代通常只用于有限的马尔科夫决策,即状态空间和动作空间是离散的。

策略迭代是 策略评估和策略提升不断循环交替,直至最后得带最优策略的过程。 对当前测策略进行策略评估,得到其状态价值函数,然后根据该状态价值函数进行策略提升得到一个更好的新策略,持续进行。。。直到收敛。

价值迭代 : 使用贝尔曼最优方程进行动态规划,得到最终的最优状态价值
  • 如果只在策略评估中进行一轮价值更新,然后直接根据更新后的价值进行策略提升,这样是否可以呢?答案是肯定的

为啥是肯定的?

  • 价值迭代的更新方式, 就是始终选择最大的动作价值
    V^{k+1}(s)=\max _{a \in \mathcal{A}}\left\{r(s, a)+\gamma \sum_{s^{\prime} \in \mathcal{S}} P\left(s^{\prime} \mid s, a\right) V^{k}\left(s^{\prime}\right)\right\}
  • 在策略迭代中:策略评估---策略提升---策略评估---策略提升---策略评估---策略提升---策略评估。。。。一直进行下午,直到收敛, 这时就可以获取策略了, 其中策略评估需要很多轮
  • 在价值迭代在中: 价值评估----获取策略, 就结束了, 其中价值评估也是很多轮(明显少于策略评估的轮数)。
  • 我的理解是,价值评估中, 每次选择的都是最优价值进行评估,加快了收敛速度

时序差分

  • 无模型的强化学习(model-free reinforcement learning):马尔可夫决策过程的状态转移矩矩阵无法直接获取,也不知道奖励函数,智能体只能和环境交互采样数据进行学习。
  • 在线策略学习: 使用在当前策略采样得到的样本进行学习, 一旦策略更新,样本被抛弃
  • 离线策略学习: 使用经验回放池将之前采样得到的样本手机起来再次利用
  • 蒙塔卡洛的价值增量更新为:V(s_t) \leftarrow V(s_t)+ \alpha (G_t-V(S_t)) 。 需要采样很多序列,序列中每个状态可能出现很多次,按照顺序记为\{0,1....t\}时刻,s_t表示第t时刻的状态。蒙特卡洛需要采样整个序列结束后计算回报。
  • 时序差分,用当前获取的奖励加上下一个状态的奖励的价值估计来作为当前状态的回报:
    V(s_t) \leftarrow V(s_t)+ \alpha [r_t + \gamma V(s_{t+1}) - V(s_t)],
    其中[r_t + \gamma V(s_{t+1}) - V(s_t)]被称为时序差分, \alpha是更新步长。本质上, G_t = r_t + \gamma V(s_{t+1})

Sarsas算法, 在线策略

  • 在不知道奖励函数和状态转移函数的情况下,如何进行策略提升?使用时序差分来估计动作价值函数
    Q(s_t, a_t) \leftarrow Q(s_t, a_t) + \alpha [r_t + \gamma Q(s_{t+1}, a_{t+1}) - Q(s_t, a_t)]
  • Sarsa 的具体算法如下:


    Saras
  • 核心代码
class Sarsa:
    """ Sarsa算法 """
    def __init__(self, ncol, nrow, epsilon, alpha, gamma, n_action=4):
        self.Q_table = np.zeros([nrow * ncol, n_action])  # 初始化Q(s,a)表格
        self.n_action = n_action  # 动作个数
        self.alpha = alpha  # 学习率
        self.gamma = gamma  # 折扣因子
        self.epsilon = epsilon  # epsilon-贪婪策略中的参数

    def take_action(self, state):  # 选取下一步的操作,具体实现为epsilon-贪婪
        if np.random.random() < self.epsilon:
            action = np.random.randint(self.n_action)
        else:
            action = np.argmax(self.Q_table[state])
        return action

    def update(self, s0, a0, r, s1, a1):
        td_error = r + self.gamma * self.Q_table[s1, a1] - self.Q_table[s0, a0]
        self.Q_table[s0, a0] += self.alpha * td_error
  • 训练核心
            # 根据state先拿到action 
            action = agent.take_action(state)
            done = False
            while not done:
              # 根据当前的action,拿到下一个状态和即时奖励
                next_state, reward, done = env.step(action)
                # 采样下一个action
                next_action = agent.take_action(next_state)
                episode_return += reward  # 这里回报的计算不进行折扣因子衰减
                agent.update(state, action, reward, next_state, next_action, done)
                state = next_state
                action = next_action

Q-learning 算法 ,离线策略

  • Q-learning的价值更新:
    Q(s_t, a_t) \leftarrow Q(s_t, a_t) + \alpha [r_t + \gamma \max_a Q(s_{t+1}, a) - Q(s_t, a_t)]
  • 具体流程;


    Q-learning
  • 核心代码
class QLearning:
    """ Q-learning算法 """
    def __init__(self, ncol, nrow, epsilon, alpha, gamma, n_action=4):
        self.Q_table = np.zeros([nrow * ncol, n_action])  # 初始化Q(s,a)表格
        self.n_action = n_action  # 动作个数
        self.alpha = alpha  # 学习率
        self.gamma = gamma  # 折扣因子
        self.epsilon = epsilon  # epsilon-贪婪策略中的参数

    def take_action(self, state):  #选取下一步的操作
        if np.random.random() < self.epsilon:
            action = np.random.randint(self.n_action)
        else:
            action = np.argmax(self.Q_table[state])
        return action

    def update(self, s0, a0, r, s1):
        td_error = r + self.gamma * self.Q_table[s1].max() - self.Q_table[s0, a0]
        self.Q_table[s0, a0] += self.alpha * td_error
  • 训练核心
         while not done:
                # 直接根据state获取action
                action = agent.take_action(state)
                # 根据action 获取下一个状态和即时奖励
                next_state, reward, done = env.step(action)
                episode_return += reward  # 这里回报的计算不进行折扣因子衰减
                agent.update(state, action, reward, next_state)
                state = next_state

离线和在线策略的差异

  • 我们称采样数据的策略为行为策略, 用这些数据来更新的策略为目标策略
  • 在线策略(on-policy)算法表示行为策略和目标策略是同一个策略
  • 离线策略(off-policy)算法表示行为策略和目标策略不是同一个策略
  • Sarsa 是典型的在线策略算法,而 Q-learning 是典型的离线策略算法。判断二者类别的一个重要手段是看计算时序差分的价值目标的数据是否来自当前的策略
  • 对于Sarsa , 更新涉及的五元组( s, a, r, s^{\prime}, a^{\prime}) 来自当前策略的采样
  • 对于Q-learning , 更新用到的四元组 ( s, a, r, s^{\prime}), 其中sa 是给定的,rs^{\prime} 不一定来自于当前策略。
    Sarsa 和 Q-learning 的对比

DQN算法(使用神经网络实现Q-learning算法)

  • 写的很好
  • Q-learning 需要维护状态-动作表,因此只适用有限状态-动作。
  • 因此,使用神经网络来学习一个拟合函数,输入为状态s, 输出每一个动作的Q值,也就是输出一个向量\left[Q\left(s, a_{1}\right), Q\left(s, a_{2}\right), Q\left(s, a_{3}\right), \ldots, Q\left(s, a_{n}\right)\right], 其中每个值代表了状态S下执行动作aQ值。

现在最大的问题是,如何训练Q网络? 神经网络需要损失函数来提供信号进行参数更新,如何设置损失?也就是如何为Q网络提供 有标签的样本训练网络?

  • 由于Q-learning的更新过程为:
    Q(s, a) \leftarrow Q(s, a) + \alpha [r + \gamma \max_a Q(s^{\prime}, a^{\prime}) - Q(s, a)]
    它的目标就是使r + \gamma \max_a Q(s^{\prime}, a^{\prime})不断逼近Q(s, a)
    因此,损失函数定义为(w为神经网络的参数):
    L(w)=\mathbb{E}\left[(\underbrace{r+\gamma \max _{a^{\prime}} Q\left(s^{\prime}, a^{\prime}, w\right)}_{\text {Target }}-Q(s, a, w))^{2}\right]

经验回放

  • 维护经验回放池, 将每个采样的的四元组数据(状态、动作、奖励、下一状态)存储到回放缓冲区中,训练 Q 网络的时候再从回放缓冲区中随机采样若干数据来进行训练。
  1. 使样本满足独立假设。
  2. 提高样本效率。

目标网络

  • 更新目标时, r+\gamma \max _{a^{\prime}} Q\left(s^{\prime}, a^{\prime}, w\right) 不断的逼近Q(s, a, w),由于时序差分的误差目标本身就包含神经网络的输出,因此,网络参数的变化导致目标也不短变化。 因此DQN使用了目标网络: 在训练中暂时将时序差分目标中的Q网络固定。因此,DQN使用两套Q网络:
    (1) 训练网络计算 Q(s, a, w)
    (2)目标网络计算 r+\gamma \max _{a^{\prime}} Q\left(s^{\prime}, a^{\prime}, w^{\prime} \right)

训练网络正常更新,每隔C个step就将训练网络的参数同步到目标网络。

DDQN(Double DQN)

  • 出现的原因: Q-learning会对Q(s,a)高估。

Dueling DQN

  • Q(s, a) 比较大时, 是因为S比较好还是a比较好? 所以定义一个优势函数A_{\pi}(s,a) = Q_{\pi}(s,a)-V_{\pi}(s)
    可以理解为: 对于\pi中某一步行动的改变(做出了动作a), 所带来的变化。简单理解,就是在一个状态S上进行了一个a所带来的变化。
  • 由于 V_{\pi}(s)=\Sigma_{a} \pi(a \mid s) Q_{\pi}(s, a)=E_{a \sim \pi}\left(Q_{\pi}(s, a)\right)
    我们不难推出:

\Sigma_{a} \pi(a \mid s) A(s, a)=E_{a \sim \pi}\left(A_{\pi}(s, a)\right)=0

状态价值来自于所有动作的动作价值函数的期望。

策略梯度算法

又看不懂了。。。。

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

推荐阅读更多精彩内容