机器学习理论

背景:
  机器学习可行性分析,这个机器学习的基石已经卡了2,3天了,缕一缕思路,抓了抓头发,是时候一决高下了。本文脉络如此:有没有一个好算法,千秋万载,一统江湖?当我们拿到一个数据集时,我们分析数据、提取特征、确定模型,为什么ML(Machine Learning)可以做到大概率预测结果?
学习问题:
  假如我们想学习一个垃圾邮件分类的模型, 通过上帝视角,我们知道必然存在一个目标函数(target function):

f:X\rightarrow Y

和一个样本集:

D:{(x_1,y_1),(x_2,y_2)\cdots (x_N,y_N)},x_i\in X,y_i\in Y

  其中,x_i可看作特征,在垃圾邮件分类下,也许是各种单词:大润发、促销、电话、抢购等,y_i可看作该特征下输出的结果。问题是,我们是否可以通过样本集,让学习算法在假设函数集合中选择比较好的假设函数:

g:X \rightarrow Y (g\approx f)

  在此,要有一个约束,我们仅仅对二分类(dichotomy)问题进行讨论,符号描述:

h:f\left \{ x_1,x_2,\cdots,x_N\right \} \rightarrow \left\{ -1,+1\right\}

  其中,\left\{ x_1,x_2,\cdots,x_N \right\}称为输入空间,其x_i是一个n维向量,n就是特征数,在这儿可以看作是用于垃圾邮件分类的特征单词,\left\{ -1,+1\right\}叫做输出空间。这儿-1可以表示SpamEmail+1可以表示NonSpamEmail
  上面就是一个学习任务,有一个样本集,那么学习算法能否从假设函数集合H中选择一个好的假设函数g(g\approx f)
  如果学习算法成功找到一个g,我们说这个学习是可行的,否则就是不可行的。
  如果用图描述下学习的过程,也许会更清晰一点:

选自Learning From Data

注意:本文仅仅对二分类的学习问题可行性进行讨论,但是并非所有的机器学习输出空间都是二值的,其他类型的需要更深入的学习了。

第一部分:NFL(No Free Lunch)理论

  有没有一个算法可以千秋万载,一统江湖,来我们一起做个选择题:

  好的,你选什么?对于这样的一个没有具体情境的题目,不存在一个好答案,他们作为答案的期望水平都是一样的,也就是f_i\sim U。而对于特定情境下的问题,往往某些算法表现很不错。这也就是NFL理论,下面是一段引用:

A number of "no free lunch" (NFL) theorems are presented that establish that for any algorithm, any elevated performance over one class of problems is exactly paid for in performance over another class
———— Wolpert and Macready (1997)

  在具体情况下,学习算法对f_i的“偏见”,我们称之为学习算法的归纳偏好。

第二部分:

  这一部分主要是引入一个经典的模型,并将其与学习问题联系起来,讨论有限个假设函数的时候,机器学习的可行性。

一、罐子模型

  考虑这样一个里面存放红、绿色球的罐子,有如下假设:

P[从罐子里取一个红球]=\mu

P[从罐子里取一个绿球]=1-\mu

  假如罐子的原因,\mu对于我们是不得而知,那我们怎么估计\mu。机智的你是否想到了统计学知识?好的,我们对罐子里小球进行取样,保证其独立同分布,并且一次取N个。
P[从抽样中取一个红球]=\nu

  那么\nu\mu有什么关系?让我们来看看。此时,我们引入Hoeffding不等式,得到下述式子,可以这么理解:在一次取样数N比较大时,在一个容忍度\varepsilon下,\nu是在概率上接近\mu的。

P[\left | \nu-\mu \right |\geqslant \varepsilon ]\leqslant 2e^{-2\varepsilon^2N}

二、联系学习问题

  对于罐子,我们未知的是\mu。对于学习问题,我们未知的是f:X\rightarrow Y,每一个球就是一个样本点,罐子里所有的球形成了样本空间\chi

  学习算法获得一个假设函数h,作用于样本空间里每一个样本点x\in X,罐中样本就被上色,其绿色代表:h(x)=f(x),其红色代表:h(x)\neq f(x)

  对于我们从罐中取N个样本(独立同分布),我们定义作如下定义:
  样本内错误率:
E_{in}=\frac{1}{N}\sum_{i=1}^{N}I\left \{ h(x_i) \neq f(x_i) \right \}

  其中I(x)叫指示函数,如x为真返回1,如x为假返回0
  样本外错误率:
E_{out}=P\left \{ h(x) \neq f(x) \right \}

  可以看出,其实E_{in}就是\nuE_{out}就是\mu,于是Hoeffding不等式就变为:
P[\left | E_{in}(h)-E_{out}(h) \right |\geqslant \varepsilon ]\leqslant 2e^{-2\varepsilon^2N}

  从上,Hoeffding不等式告诉我们,对于一条假设,随着N变大,样本上的错误率与样本外错误率是不大的!
  但是,它这儿是一条假设,如果固定一条假设函数,那都不需要学习算法了,且这儿更像是验证一个假设函数的预测能力,由于只有一条,没有选择余地,学习算法并不一定能获得一个E_{in}较小的,尽管其保证了E_{in}很接近E_{out}。我们希望的是多条假设以供学习算法选择,而不是一条固定的假设函数。

三、有限条假设函数

  为了解决上面一条假设函数带来的问题,我们尝试加入有限的假设函数,希望在一个样本集下,学习算法能从有限假设函数集H=\left \{ h_1,h_2,\cdots,h_M\right \}选择一条h作为g(g\approx f),此时让我们看看E_{in}(g)E_{out}(g)会不会差很多?
P\left \{|E_{in}(g)-E_{out}(g)|\geq\varepsilon \right \}\leq P\left \{|E_{in}(h_{1})-E_{out}(h_{1})|\geq\varepsilon \\ \ or\ |E_{in}(h_{2})-E_{out}(h_{2})|\geq\varepsilon \\ \cdots \\ \ or\ |E_{in}(h_{M})-E_{out}(h_{M})|\geq\varepsilon \right \}

P\left \{|E_{in}(g)-E_{out}(g)|\geq\varepsilon \right \}\leq \sum_{i=1}^{M}P\left \{|E_{in}(h_i)-E_{out}(h_i)|\geq\varepsilon \right \}\leq2Me^{-2\varepsilon^2N}

  解释下式子,因为g是从H里面选择出来的,因此g上发生E_{in}偏离E_{out}的概率必然是小于等于某一个h_i,必然是小于等于所有h上发生的概率。
  从上面的公式可以看出:在一个容量为N样本集下,只要N足够大,面对M(有限)个假设函数,学习算法可以从假设集合中选择一个E_{in}(h)比较小的假设函数作为g(g\approx f),也就是我们证明了其学习的可行性。

四、小结

我们的问题是:学习算法能否,在假设函数集合H中寻找合适的h作为g,使得g \approx f
从上文看到,我们要保证两个要点:
1、E_{in}(g) \approx E_{out}(g)
2、为了想让E_{out}(g) \approx 0 ,希望E_{in}(g)尽可能小。
其中第一个要点可以通过Hoeffding不等式保证,第二个要点可以通过加入更多的M保证。然后我们用概率上一个简单的缩放(Union Bound)搞定了M有限的时候。

第三部分:

  由上,我们得知M有限的时候,学习是可行的。但是,现实中,我们面对的常常是M无限的时候,这部分主要讨论M无穷大的时候。明显如果M真的无穷大,事情将会变得非常复杂,我们希望可以用一个有限的数代替M。为此将引入二分类(dichotomies)、成长函数(growth function)、断点(break point)等概念来寻找这个替代值。
  第一节我们将从二分类入手,引入二分类的最大数目成长函数。希望成长函数是一个多项式级别,引入了BreakPoint等理论。
  第二节解决成长函数和M的关系,从一个直观的角度说明成长函数代替M

一、基本概念

  让我们分析下为什么M会变得无穷大?为了表达方便,我们定义Bad events B_m
|E_{in}(h_{m})-E_{out}(h_{m})|\geq\varepsilon

  那么Union Bound告诉我们:
P[B_1 \ or\ B_2 \cdots or\ B_m] \leq P[B_1] +P[B_2] +\cdots+P[B_m]

  原来是这边产生了M,那么这个坏事情(Bad events)发生会不会重叠呢?Union Bound财大气粗,造成了Over estimate。我们是不是只要不用Union Bound呢?

1、坏事情发生是会重叠的

  让我们来看个PLA(感知器)的例子,下图目标函数f,蓝色部分、紫色部分分别是被f划分的两类,绿色、红色线分别是我们的假设函数h_1h_2,显然他们非常接近。我们先看他们的E_{in},如果h_1在某一个样本集D上出现了E_{in}偏离E_{out}。对于h_2,由于h_1h_2非常接近,面对同一个D(这里是理解的关键,他们面对的是同一个!),所以h_2D上出现了E_{in}偏离E_{out}情况的概率是非常非常大的。由于h_1h_2非常接近,他们的E_{out}也是很接近的,可以看到黄色部分是他们E_{out}的差。由于两者E_{in}接近,E_{out}接近,因此坏事情发生很大概率是存在重叠的,何况这里只是两个接近的线,面对无穷多的假设函数,坏事情发生是存在重叠的,Union Bound是Over estimate!

2、二分类

  A dichotomy:对一个假设函数h: \left \{ x_1,x_2,\cdots x_N\right \} \rightarrow \left \{ -1,+1 \right \}
  number of dichotomies|H|,其中
H(x_1,x_2,\cdots,x_N)=\left \{h(x_1),h(x_2)\cdots ,h(x_N) |h\in H \right \}

  让我们解释一下,A dichotomy是什么意思?就是将h带入(x_1,x_2,\cdots x_N)得到(-1,+1,\cdots,+1)这就叫一个二分类。但是我们不关注某一个二分类,我们更加关注的是对于某一个假设函数集合H上有多少种这样的二分类。于是引入了|H|,就是将H中每一个h作用到(x_1,x_2,\cdots x_N)得到若干不同的二分类,得到一个二分类集合,其集合中不同二分类数:|H|
  注意一点:这个二分类的个数是随(x_1,x_2,\cdots x_N)变化而变化的。H在不同的样本集上的二分类结果是不同的。比如

其二分类数目分别是6、8
3、成长函数

  由于不同D,可得到不同二分类数量,人们想知道H(x_1,x_2,\cdots x_N)到底多少二分类,终于引入了成长函数,m_{H}(N)=\max_{(x_1,x_2,\cdots x_N)\in \chi}|H(x_1,x_2,\cdots,x_N)|
显然成长函数的天花板:2^N
  成长函数什么意思呢?H在任意D上最大的二分类个数。

4、打散

  如\exists DHD上产生了2^ {|D|}个二分类,那么H可以打散D
  PLA的例子:此时,m_{H}(3)=8,表明其可以产生所有的二分类。如果不能打散,说明对于任意的DHD上至少有一种二分类没法实现。

5、断点

  人们希望成长函数是多项式级别的,于是引入了断点的概念。
  如果大小为k的数据集不可以被H打散,那么k就是一个断点(Break Point)。
  PLA例子:其m_{H}(4)=14 \neq 2^4=16,所以4就是断点。
  注意:如果4是断点,5同样也是断点,因为如果5上面可以组成所有二分类,遮掉一个点,4必然是可以组成所有二分类。


  断点给我们的最重要的结论是:如果没有断点,。如果有断点, is polynomial in N,这是一个不错的结论,因为这样的话,如果我想知道你在学习上的预算,我不需要知道、、学习算法,只要你告诉我它的断点是多少。同样,你只要告诉我它的断点存在,我就知道它是可以学习的。

6、一道题目

  让我们用一道题目检测下是否理解了上面的概念。我们有三个点,对这个假设集合,其断点是2,因此任意两个点不能产生所有的二分类。在这个限制下,问总共可以产生多少个二分类?

  答案是4个,如果没有断点这个约束,就是8个,但是因为有断点这个约束,导致你在添加点的时候,任取两列,其两列不能形成所有的二分类,也就是不能有oo,ox,xo,xx。下图是其中的一种答案:

7、小结:

  考虑二分类的学习问题中,为了寻找M合适的替代物,我们提出了Dichotomies,为了确定在某一个D上Dichotomies的最大值。引入了成长函数,为了说明成长函数是多项式级别, 引入了断点,下一部分我们将说明两个问题:
1、为什么有断点的,成长函数是多项式级?
2、为什么可以用成长函数代替M?

二、泛化理论

  不得不说,这部分是很理论了,如yaser Abu-Mostafa在caltech课程中所述,勒紧安全带,我们要开始了。

1、有断点的成长函数是多项式级:

  为了说明有断点的m_H(N)是多项式类型,我们希望让m_H(N) \leq \cdots \leq \cdots \leq a\ polynomial,于是定义了B(N,K):在N个点上最大的二分类数量,包含了k个断点。回忆下成长函数的定义:对某个H而言,任意D,其最大的二分类数量。在我看来,不就是加入了断点限制的成长函数么?显然如果有断点,下式显然成立:
m_H(N) \leq B(N,K) \leq 2^N

  下面求解具体的B(N,K)的值。
  有N个点(x_1,x_2,\cdots x_N),其断点为K,下表是B(N,K)的所有情况,可以看出B(N,K)所有的组合从列上被分成了三类,行上被分为两部分。

结构化分析

  表中每一行代表(h(x_1),h(x_2),\cdots,h(x_N)),其中第一类指(h(x_1),h(x_2),\cdots h(x_{N-1}))部分(第一部分)出现过二分类向量仅仅一次,记一类:S_1,其数目记作\alpha。第二、三类:(h(x_1),h(x_2),\cdots h(x_{N-1}))出现二分类向量两次且h(x_N)=1 \ or \ -1(第二部分)分别记两类:S_2^+S_2^-,其数目是相等的,所以记作β,此时:
B(N,K)=α+2β

  接下来,我们想用递归的方式对其进行估计:在S_1 \cup S_2^+的第一部分,任取K列,不可能可以组成所有的二分类,如果可以就与B(N,K)矛盾了,所以,易得下面式子:
α+β \leq B(N-1,K)

  同样,对于S_2^-的第一部分,任取K-1列,不可能组成所有的二分类,于是得:
β \leq B(N-1,K-1)

  举个例子:试想K-1=2,其组成了所有的二分类组合:oo、ox、xo、xx,当你S_2^+ \cup S_2^-并且添加第二部分的时候,就组成了ooo、oox、oxo、oxx、xoo、xox、xxo、xxx,如果可以就与B(N,K)矛盾了。
  综上,不难得到:
B(N,K) \leq B(N-1,K)+B(N-1,K-1)

  最终得到:
B(N,K) \leq \sum_{i=0}^{k-1}\binom{N}{i}

  使用归纳法对上公式进行一个证明:
  当N=1,2时,上式成立;
  假设N < N_0上式子成立,我们尝试证明N =N_0时,
B(N,K) \leq \sum_{i=0}^{k-1}\binom{N}{i} \\ =\sum_{i=0}^{k-1}\binom{N-1}{i}+\sum_{i=0}^{k-2}\binom{N-1}{i} \\ =1+\sum_{i=1}^{k-1}\binom{N-1}{i}+\sum_{i=1}^{k-1}\binom{N-1}{i-1} \\ =1+\sum_{i=1}^{k-1}\left [\binom{N-1}{i}+\binom{N-1}{i-1} \right ]\ \\ =1+\sum_{i=1}^{k-1}\binom{N}{i}=\sum_{i=0}^{k-1}\binom{N}{i}

  BinGo!我们终于证明了有断点的成长函数是一个多项式级别的。
  注意:上述证明倒数第二步的两个组合的合并需要一点技巧,可以利用组合的定义,但是利用组合的意义会快很多。

2、对成长函数代替M的简单证明:

  此处,我们分成三部分,分别:成长函数如何描述重叠部分、E_{out}的处理、最后将它们放一起。
2.1、成长函数如何描述重叠部分:
  我们缕一缕两点:第一、坏事情(在某一个h上,E_{in}偏离E_{out}很大)发生是有重叠的,并不是独立的。这一点在Union Bound 为什么Over Estimate 那有详细的解释。
  第二点:成长函数是如何描述其重叠部分的呢?成长函数告诉我们,H在任意D上最大的二分类数目。什么意思呢?对于二分类问题,拿PLA说好了,尽管假设函数无数条,但是面对同一个样本集,很多假设函数返回的是一个完全一样的二分类,因此有很多假设函数在D上表现是一致的。因此,成长函数是以一个二分类(尽管很多问题不是二分类,但是可以通过技术手段推广的)的角度告诉我们,坏事情发生不是独立的,是有重叠区域的。
2.2、E_{out}的处理:
  为什么要处理E_{out},我们知道E_{out}取决于整个输入空间。于是用E_{in}^{'}替换掉E_{out},其中E_{in}^{'}E_{in}^{}是基于两个大小为N的样本。那为什么可以替换?因为E_{in}^{}是和E_{out}相互影响的,E_{in}^{'}是和E_{out}相互影响的,所以E_{in}^{'}E_{in}^{}也是相互有关的。
2.3、综合:
  千呼万唤始出来。最后,我们终于终于得到了著名的:VC不等式。
P[\left | E_{in}(g)-E_{out}(g) \right |> \varepsilon ]\leqslant 4m_{H}(2N)e^{-\frac{1}{8}\varepsilon^2N}

  注意:在使用的时候,已知BreakPoint存在,我们一般用多项式代替成长函数(d_{vc}(H)后面会解释)。
m_H(2N) \leq B(2N,K) \leq \sum_{i=0}^{K-1}\binom{2N}{i}\leq \sum_{i=0}^{d_{vc}(H)}\binom{2N}{i} \leq (2N)^{d_{vc}(H)}

第四部分:

  我们已经解决了机器学习的可行性问题,虽然是基于二分类目标函数的,但是推广是可以的,只是技术上的手段。下面主要是一些关于VC的知识,以及VC分析的一些现实意义。

一、VC维

  一个假设空间H的VC维d_{vc}(H),是满足m_{H}(N)=2^N的最大整数。换言之:H可以打散的最大点数;再言之:H可以在某一个最大点数的D上实现所有的二分类组合,这个最大点数就是VC维;再或者VC维就是BreakPoint点-1。
  两点补充:
N\leq d_{vc}(H)\Rightarrow H\ can \ shatter\ N\ points

k> d_{vc}(H)\Rightarrow k \ is \ a \ break \ point \ for \ H

二、VC维的解释与泛化边界:

  由于VC分析不依赖于学习算法、具体分布、目标函数,加之成长函数使用比较松弛的多项式:d_{vc}(H),造成VC泛化边界时是一个非常宽松的边界。由于其松弛的边界,我们一般可以用其评估模型好坏。

1、d_{vc}与有效参数

  h上的参数w=(w_0,w_1,\cdots,w_d)创建了自由度。d_{vc}代表了二分类下的自由度。

2、d_{vc}与需要的数据量

  我们来看下VC不等式:
P[\left | E_{in}(g)-E_{out}(g) \right |> \varepsilon ]\leqslant \underset{\delta }{\underbrace{4m_{H}(2N)e^{-\frac{1}{8}\varepsilon^2N}}}

  定义了ε、δ,可以理解ε为精度,δ为概率上的估计。给定一个δ>0,假设我们期望泛化误差(E_{in}E_{out}距离)不超过ε,则
N \geq \frac{8}{\varepsilon^2}ln(\frac{4m_h(2N)}{\delta}) \geq \frac{8}{\varepsilon^2}ln(\frac{4((2N)^{d_{vc}}+1)}{\delta})

  因此,只要确定了实践中,样本数N至少\geq 10d_{vc}

3、泛化边界

  在VC不等式中,我们用δ表示ε\delta =4m_{H}(2N)e^{-\frac{1}{8}\varepsilon^{2}N},于是可以得到:
\varepsilon =\underset{\Omega}{\underbrace{ \sqrt{\frac{8}{N}ln\frac{4m_{H}(2N)}{\delta}}}}
  联系上式子与E_{in}、E_{out},得下述解释:我们至少有1-\delta的把握,使得|E_{out}-E_{in}| \leq \Omega(N,H,\delta)
  由于E_{out}通常要大于E_{in},于是上式子摇身一变就变成了泛化边界
  我们至少有1-\delta的把握,E_{out}-E_{in} \leq \Omega \Rightarrow E_{out} \leq \Omega+E_{in}
  泛化边界告诉我们,不是模型越复杂越好,虽然E_{in}降低了,但是\Omega(可以视之为惩罚)增加了。所以,好的模型要综合考虑这两项(泛化边界也为后面正则化带来了启发)。

后记:

  本文是笔者在学习加州理工学院公开课-机器学习与数据挖掘时的笔记。要感谢Yaser Abu-Mostafa、林轩田老师的公开课视频资源,感谢博客园viredery、知乎八汰等人的笔记资料。

参考资料:
[1] Learning From Data(网易公开课).
[2] 林轩田机器学习基石(B站公开课).
[3] No Free Lunch Theorems.
[4] 机器学习为什么可行的上、中、下(知乎八汰).
[5] 机器学习 - 学习理论(博客园viredery).
[6]如何通俗的理解机器学习中的VC维、shatter和break point?(知乎).
[7]台湾大学林轩田机器学习基石课程学习笔记(红色石头).

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

推荐阅读更多精彩内容