详解隐马尔可夫模型(HMM)中的维特比算法

笔记转载于GitHub项目https://github.com/NLP-LOVE/Introduction-NLP

4. 隐马尔可夫模型与序列标注

第3章的n元语法模型从词语接续的流畅度出发,为全切分词网中的二元接续打分,进而利用维特比算法求解似然概率最大的路径。这种词语级别的模型无法应对 OOV(Out of Vocabulary,即未登录词) 问题: 00V在最初的全切分阶段就已经不可能进人词网了,更何谈召回。

例如下面一句:

头上戴着束发嵌宝紫金冠,齐眉勒着二龙抢珠金抹额

加粗的就是相对陌生的新词,之前的分词算法识别不出,但人类确可以,是因为读者能够识别“戴着”,这些构词法能让人类拥有动态组词的能力。我们需要更细粒度的模型,比词语更细粒度的就是字符。

具体说来,只要将每个汉字组词时所处的位置(首尾等)作为标签,则中文分词就转化为给定汉字序列找出标签序列的问题。一般而言,由字构词是序列标注模型的一种应用。 在所有“序列标注”模型中,隐马尔可夫模型是最基础的一种。

4.1 序列标注问题

序列标注指的是给定一个序列 x=x_1x_2...x_n,找出序列中每个元素对应标签 y=y_1y_2...y_n 的问题。其中,y 所有可能的取值集合称为标注集。比如,输入一个自然数序列,输出它们的奇偶性。

image

求解序列标注问题的模型一般称为序列标注器,通常由模型从一个标注数据集 \{X,Y\}=\{(x^{(i)},y^{(i)})\},i=1,...,K 中学习相关知识后再进行预测。再NLP问题中,x 通常是字符或词语,而 y 则是待预测的组词角色或词性等标签。中文分词、词性标注以及命名实体识别,都可以转化为序列标注问题。

  1. 序列标注与中文分词

    考虑一个字符序列(字符串) x,想象切词器真的是在拿刀切割字符串,如此,中文分词转化为标注集{切,过}的序列标注问题。

    image

分词标注集并非只有一种,为了捕捉汉字分别作为词语收尾(Begin、End)、词中(Middle)以及单字成词(Single)时不同的成词概率,人们提出了{B,M,E,S}这种最流行的标注集。

image
  1. 序列标注与词性标注

    词性标注任务是一个天然的序列标注问题:x 是单词序列,y 是相应的词性序列。需要综合考虑前后的单词与词性才能决定当前单词的词性。
    image
  1. 序列标注与命名实体识别

    所谓命名实体,指的是现实存在的实体,比如人名、地名和机构名,命名实体是 OOV 的主要组成部分。

    考虑到字符级别中文分词和词语级别命名实体识别有着类似的特点,都是组合短单位形成长单位的问题。所以命名实体识别可以复用BMES标注集,并沿用中文分词的逻辑,只不过标注的对象由字符变为单词而已。唯一不同的是,命名实体识别还需要确定实体所属的类别。这个额外的要求依然是个标注问题,可以通过将命名实体类别附着到BMES标签来达到目的。比如,构成地名的单词标注为“B/M/E/S-地名”,以此类推。对于那些不构成命名实体的单词,则统-标注为O ( Outside), 即复合词之外。

    image

总之,序列标注问题是NLP中最常见的问题之一。许多应用任务都可以变换思路,转化为序列标注来解决。所以一个准确的序列标注模型非常重要,直接关系到NLP系统的准确率。机器学习领域为NLP提供了许多标注模型,本着循序渐进的原则,本章介绍其中最基础的一个隐马尔可夫模型。

4.2 隐马尔可夫模型

隐马尔可夫模型( Hidden Markov Model, HMM)是描述两个时序序列联合分布 p(x,y) 的概率模型: x 序列外界可见(外界指的是观测者),称为观测序列(obsevation sequence); y 序列外界不可见,称为状态序列(state sequence)。比如观测 x 为单词,状态 y 为词性,我们需要根据单词序列去猜测它们的词性。隐马尔可夫模型之所以称为“隐”,是因为从外界来看,状
态序列(例如词性)隐藏不可见,是待求的因变量。从这个角度来讲,人们也称状态为隐状态(hidden state),而称观测为显状态( visible state)。隐马尔可夫模型之所以称为“马尔可夫模型”,”是因为它满足马尔可夫假设

  1. 从马尔可夫假设到隐马尔可夫模型

    马尔可夫假设:每个事件的发生概率只取决于前一个事件。

    马尔可夫链:将满足马尔可夫假设的连续多个事件串联起来,就构成了马尔可夫链。

    如果把事件具象为单词,那么马尔可夫模型就具象为二元语法模型。

隐马尔可夫模型:它的马尔可夫假设作用于状态序列,

假设 ① 当前状态 Yt 仅仅依赖于前一个状态 Yt-1, 连续多个状态构成隐马尔可夫链 y。有了隐马尔可夫链,如何与观测序列 x 建立联系呢?

隐马尔可夫模型做了第二个假设: ② 任意时刻的观测 x 只依赖于该时刻的状态 Yt,与其他时刻的状态或观测独立无关。如果用箭头表示事件的依赖关系(箭头终点是结果,依赖于起点的因缘),则隐马尔可夫模型可以表示为下图所示

image

状态与观测之间的依赖关系确定之后,隐马尔可夫模型利用三个要素来模拟时序序列的发生过程----即初始状态概率向量、状态转移概率矩阵和发射概率矩阵

  1. 初始状态概率向量

    系统启动时进入的第一个状态 Y1 称为初始状态,假设 y 有 N 种可能的取值,那么 Y1 就是一个独立的离散型随机变量,由 P(y1 | π) 描述。其中
    \pi=\left(\pi_{1}, \cdots, \pi_{N}\right)^{\mathrm{T}}, 0 \leqslant \pi_{i} \leqslant 1, \sum_{i=1}^{N} \pi_{i}=1
    是概率分布的参数向量,称为初始状态概率向量

    image

给定 π ,初始状态 Y1 的取值分布就确定了,比如采用{B,M,E,S}标注集时概率如下:
p(y_1=B)=0.7\\ p(y_1=M)=0\\ p(y_1=E)=0\\ p(y_1=S)=0.3
那么此时隐马尔可夫模型的初始状态概率向量为 π=[0.7,0,0,0.3],注意,句子第一个词是单字的可能性要小一些。

  1. 状态转移矩阵

    Yt 如何转移到 Yt+1 呢?根据马尔可夫假设,t+1 时的状态仅仅取决于 t 时的状态,既然一共有 N 种状态,那么从状态 Si 到状态 Sj 的概率就构成了一个 NN 的方阵,称为状态转移矩阵 A
    A=\left[p\left(y_{t+1}=s_{j} | y_{t}=s_{i}\right)\right]_{N \times N}
    其中下标 i、j 分别表示状态的第 i、j 种取值。状态转移概率的存在有其实际意义,在中文分词中,标签 B 的后面不可能是 S,于是就有 P(Yt+1 = S | Yt = B) = 0。同样,词性标注中的“形容词->名词”“副词->动词”也可通过状态转移概率来模拟,这些
    概率分布参数不需要手动设置,而是通过语料库上的统计自动学习*。

  1. 发射概率矩阵

    有了状态 Yt 之后,如何确定观测 Xt 的概率分布呢?根据隐马尔可夫假设②,当前观测 Xt 仅仅取决于当前状态 Yt。也就是说,给定每种 y,x 都是一个独立的离散型随机变量,其参数对应一个向量。 假设观测 x 一共有 M 种可能的取值,则 x 的概率分布参数向量维度为 M。由于 y 共有 N 种,所以这些参数向量构成了 NM 的矩阵,称为发射概率矩阵B*。

    \boldsymbol{B}=\left[p\left(x_{t}=o_{i} | y_{t}=s_{j}\right)\right]_{N \times M}

    其中,第 i 行 j 列的元素下标 i 和 j 分别代表观测和状态的第 i 种和第 j 种取值。

  1. 隐马尔可夫模型的三个基本用法

    • 样本生成问题:给定模型,如何有效计算产生观测序列的概率?换言之,如何评估模型与观测序列之间的匹配程度?

    • 序列预测问题:给定模型和观测序列,如何找到与此观测序列最匹配的状态序列?换言之,如何根据观测序列推断出隐藏的模型状态?

    • 模型训练问题:给定观测序列,如何调整模型参数使得该序列出现的概率最大?换言之,如何训练模型使其能最好地描述观测数据?

    前两个问题是模式识别的问题:1) 根据隐马尔科夫模型得到一个可观察状态序列的概率(评价);2) 找到一个隐藏状态的序列使得这个序列产生一个可观察状态序列的概率最大(解码)。第三个问题就是根据一个可以观察到的状态序列集产生一个隐马尔科夫模型(学习)。

4.3 隐马尔可夫模型的训练

  1. 案例假设和模型构造

    设想如下案例:某医院招标开发“智能”医疗诊断系统,用来辅助感冒诊断。已知①来诊者只有两种状态:要么健康,要么发烧。②来诊者不确定自己到底是哪种状态,只能回答感觉头晕、体寒或正常。医院认为,③感冒这种病,只跟病人前一天的状态有关,并且,④当天的病情决定当天的身体感觉。有位来诊者的病历卡上完整地记录了最近 T 天的身体感受(头晕、体寒或正常),请预测这 T 天的身体状态(健康或发烧)。由于医疗数据属于机密隐私,医院无法提供训练数据,但根据医生经验,感冒发病的规律如下图所示(箭头上的数值表示概率):

    image

根据已知条件①②,病情状态(健康、发烧)可作为隐马尔可夫模型的隐状态(上图蓝色状态),而身体感受(头晕、体寒或正常)可作为隐马尔可夫模型的显状态(图中白色状态)。条件③符合隐马尔可夫模型假设一,条件④符 合隐马尔可夫模型假设二。这个案例其实描述了一个隐马尔可夫模型, 并且参数已经给定。构造模型代码见:

import numpy as np
from pyhanlp import *
from jpype import JArray, JFloat, JInt

to_str = JClass('java.util.Arrays').toString

## 隐马尔可夫模型描述
states = ('Healthy', 'Fever')
start_probability = {'Healthy': 0.6, 'Fever': 0.4}
transition_probability = {
    'Healthy': {'Healthy': 0.7, 'Fever': 0.3},
    'Fever': {'Healthy': 0.4, 'Fever': 0.6},
}
emission_probability = {
    'Healthy': {'normal': 0.5, 'cold': 0.4, 'dizzy': 0.1},
    'Fever': {'normal': 0.1, 'cold': 0.3, 'dizzy': 0.6},
}
observations = ('normal', 'cold', 'dizzy')


def generate_index_map(lables):
    index_label = {}
    label_index = {}
    i = 0
    for l in lables:
        index_label[i] = l
        label_index[l] = i
        i += 1
    return label_index, index_label


states_label_index, states_index_label = generate_index_map(states)
observations_label_index, observations_index_label = generate_index_map(observations)



def convert_map_to_matrix(map, label_index1, label_index2):
    m = np.empty((len(label_index1), len(label_index2)), dtype=float)
    for line in map:
        for col in map[line]:
            m[label_index1[line]][label_index2[col]] = map[line][col]
    return JArray(JFloat, m.ndim)(m.tolist())

def convert_observations_to_index(observations, label_index):
    list = []
    for o in observations:
        list.append(label_index[o])
    return list

def convert_map_to_vector(map, label_index):
    v = np.empty(len(map), dtype=float)
    for e in map:
        v[label_index[e]] = map[e]
    return JArray(JFloat, v.ndim)(v.tolist())  # 将numpy数组转为Java数组


## pi:初始状态概率向量
## A:状态转移概率矩阵
## B:发射概率矩阵
A = convert_map_to_matrix(transition_probability, states_label_index, states_label_index)
B = convert_map_to_matrix(emission_probability, states_label_index, observations_label_index)
observations_index = convert_observations_to_index(observations, observations_label_index)
pi = convert_map_to_vector(start_probability, states_label_index)

FirstOrderHiddenMarkovModel = JClass('com.hankcs.hanlp.model.hmm.FirstOrderHiddenMarkovModel')
given_model = FirstOrderHiddenMarkovModel(pi, A, B)
  1. 样本生成算法

    它的生成过程就是沿着隐马尔可夫链走 T 步:

    • 根据初始状态概率向量采样第一个时刻的状态 Y1 = Si,即 Y1 ~ π。
    • Yt 采样结束得到 Si 后,根据状态转移概率矩s阵第 i 行的概率向量,采样下一时刻的状态 Yt+1。
    • 对每个 Yt = Si,根据发射概率矩阵的第 i 行采样 Xt。
    • 重复步骤 2 共计 T-1 次,重复步骤 3 共计 T 次,输出序列 x 与 y。

    代码如下(接上),直接通过模型进行生成:

    ## 第一个参数:序列最低长度
    ## 第二个参数:序列最高长度
    ## 第三个参数:需要生成的样本数
    for O, S in given_model.generate(3, 5, 2):
        print(" ".join((observations_index_label[o] + '/' + states_index_label[s]) for o, s in zip(O, S)))
    
  1. 隐马尔可夫模型的训练

    样本生成后,我们就可以利用生成的数据重新训练,通过极大似然法来估计隐马尔可夫模型的参数。参数指的是三元组(π,A,B)。

    利用给定的隐马尔可夫模型 P生成十万个样本,在这十万个样本上训练新模型Q,比较新旧模型参数是否一致。

    trained_model = FirstOrderHiddenMarkovModel()
    
    ## 第一个参数:序列最低长度
    ## 第二个参数:序列最高长度
    ## 第三个参数:需要生成的样本数
    trained_model.train(given_model.generate(3, 10, 100000))
    print('新模型与旧模型是否相同:', trained_model.similar(given_model))
    

    输出:

    新模型与旧模型是否相同: True
    

    运行后一般都成立,由于随机数,仅有小概率发生失败。

4.4 隐马尔可夫模型的预测

隐马尔可夫模型最具实际意义的问题当属序列标注了:给定观测序列,求解最可能的状态序列及其概率。

  1. 概率计算的前向算法

    给定观测序列 x 和一个状态序列 y,就可以估计两者的联合概率 P(x,y),联合概率就是一种结果的概率,在这些结果当中找到最大的联合概率就是找到最有可能的结果预测。联合概率:P(x,y) = P(y) P(x|y),下面我们来分别求出P(y)和P(x|y)

  • 顺着隐马尔可夫链走,首先 t=1 时初始状态没有前驱状态,发生概率由 π 决定:

    P(y_1=s_i)=\pi_i

  • 接着对 t >= 2,状态 Yt 由前驱状态 Yt-1 转移而来,转移矩阵由矩阵 A 决定:

    P(y_t=s_j|y_{t-1}=s_i)=A_{i,j}

    所以状态序列的概率为上面式子的乘积:

    p(y)=p\left(y_{1}, \cdots, y_{r}\right)=p\left(y_{1}\right) \prod_{i=2}^{T} p\left(y_{i} | y_{i-1}\right)

  • P(y) 我们已经求出来了,下面要求 P(x|y)

    对于每个 Yt = Si,都会“发射”一个 Xt = Oj,发射概率由矩阵 B 决定:

    P(x_t=O_j|y_t=s_i)=B_{i,j}

  • 那么给定一个状态序列 Y,对应的 X 的概率累积形式:

    p(x | y)=\prod_{t=1}^{T} p\left(x_{t} | y_{t}\right)

  • 最后带入联合概率公式得:
    \begin{aligned} p(x, y) &=p(y) p(x | y) \\ &=p\left(y_{1}\right) \prod_{t=2}^{T} p\left(y_{t} | y_{t-1}\right) \prod_{t=1}^{T} p\left(x_{t} | y_{t}\right) \end{aligned}

将其中的每个 Xt、Yt 对应上实际发生序列的 Si、Oj,就能带入(π,A,B)中的相应元素,从而计算出任意序列的概率,最后找出这些概率的最大值就得到预测结果。找出概率最大值要用到维特比算法

  1. 搜索状态序列的维特比算法

    理解了前向算法之后,找寻最大概率所对应的状态序列无非是一个搜索问题。具体说来,将每个状态作为有向图中的一个节点, 节点间的距离由转移概率决定,节点本身的花费由发射概率决定。那么所有备选状态构成一幅有 向无环图,待求的概率最大的状态序列就是图中的最长路径,此时的搜索算法称为维特比算法,如图下图所示:

    image

上图从左往右时序递增,虚线由初始状态概率决定,实线则是转移概率。由于受到观测序列的约束,不同状态发射观测的概率不同,所以每个节点本身也必须计算自己的花费,由发射概率决定。又由于 Yt+1 仅依赖于 Yt,所以网状图可以动态规划的搜索,也就是维特比算法

  • 初始化,t=1 时初始最优路径的备选由 N 个状态组成,它们的前驱为空。
    \begin{aligned} \delta_{1, i}=\pi_{i} \boldsymbol{B}_{i, o_{1}}, & i=1, \cdots, N \\ \psi_{1, i}=0, & i=1, \cdots, N \end{aligned}
    其中,δ 存储在时刻 t 以 Si 结尾的所有局部路径的最大概率。ψ 存储局部最优路径末状态 Yt 的前驱状态。

  • 递推,t >= 2 时每条备选路径像贪吃蛇一样吃入一个新状态,长度增加一个单位,根据转移概率和发射概率计算花费。找出新的局部最优路径,更新 δ、ψ 两个数组。
    \begin{array}{ll} {\delta_{t, i}=\max _{1 \leqslant j \leqslant N}\left(\delta_{t-1, j} A_{j, i}\right) B_{i, o_{t}},} & {i=1, \cdots, N} \\ {\psi_{t, i}=\arg \max _{1 \leqslant j \leqslant N}\left(\delta_{t-1, j} A_{j, i}\right),} & {i=1, \cdots, N} \end{array}

  • 终止,找出最终时刻 δt,i 数组中的最大概率 P*,以及相应的结尾状态下标 i*t。
    \begin{aligned} &p^{*}=\max _{1 \leqslant i \leqslant N} \delta_{T, i}\\ &i_{T}^{*}=\arg \max _{1 \leqslant i \leqslant N} \delta_{T, i} \end{aligned}

  • 回溯,根据前驱数组 ψ 回溯前驱状态,取得最优路径状态下标。
    i_{t}^{*}=\psi_{t+1, i_{t+1}}, \quad t=T-1, T-2, \cdots, 1

预测代码如下(接上面代码):

pred = JArray(JInt, 1)([0, 0, 0])
prob = given_model.predict(observations_index, pred)
print(" ".join((observations_index_label[o] + '/' + states_index_label[s]) for o, s in
               zip(observations_index, pred)) + " {:.3f}".format(np.math.exp(prob)))

输出为:

normal/Healthy cold/Healthy dizzy/Fever 0.015

观察该结果,“/”隔开观测和状态,最后的 0.015 是序列的联合概率。

4.5 隐马尔可夫模型应用于中文分词

HanLP 已经实现了基于隐马尔可夫模型的中文分词器 HMMSegmenter,并且实现了训练接口。代码详见:

hmm_cws.py:https://github.com/NLP-LOVE/Introduction-NLP/tree/master/code/ch04/hmm_cws.py

4.6 性能评测

如果隐马尔可夫模型中每个状态仅依赖于前一个状态, 则称为一阶;如果依赖于前两个状态,则称为二阶。既然一阶隐马尔可夫模型过于简单,是否可以切换到二阶来提高分数呢?

答案是可以的,跟一阶类似,这里不再详细介绍二阶隐马尔可夫模型,详细请看原书。

这里我们使用 MSR语料库进行评测,结果如下表所示:

算法 P R F1 R(oov) R(IV)
最长匹配 89.41 94.64 91.95 2.58 97.14
二元语法 92.38 96.70 94.49 2.58 99.26
一阶HHM 78.49 80.38 79.42 41.11 81.44
二阶HHM 78.34 80.01 79.16 42.06 81.04

可以看到,二阶隐马尔可夫模型的 Roov 有少许提升,但综合 F1 反而下降了。这说明增加隐马尔可夫模型的阶数并不能提高分词器的准确率,单靠提高转移概率矩阵的复杂度并不能提高模型的拟合能力,我们需要从别的方面想办法。目前市面上一些开源分词器仍然停留在一阶隐马尔可夫模型的水平,比如著名的结巴分词,它们的准确率也只能达到80%左右。

4.7 总结

这一章我们想解决的问题是新词识别,为此从词语级模型切换到字符级模型,将中文分词任务转换为序列标注问题。作为新手起步,我们尝试了最简单的序列标注模型----隐马尔可夫模型。隐马尔可夫模型的基本问题有三个:样本生成、参数估计、序列预测

然而隐马尔可夫模型用于中文分词的效果并不理想,虽然召回了一半的 OOV,但综合 F1 甚至低于词典分词。哪怕升级到二阶隐马尔可夫模型, F1 值依然没有提升。 看来朴素的隐马尔可夫模型不适合中文分词,我们需要更高级的模型。

话说回来,隐马尔可夫模型作为入门模型,比较容易上手,同时也是许多高级模型的基础。打好基础,我们才能挑战高级模型。

4.8 GitHub项目

HanLP何晗--《自然语言处理入门》笔记:

https://github.com/NLP-LOVE/Introduction-NLP

项目持续更新中......

目录


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

推荐阅读更多精彩内容