k 近邻法

k 近邻法

  • k 近邻算法
  • k 近邻模型
  • k 近邻法的实现:kd 树
  • 搜索 kd 树

k 近邻模型实现

  • k 近邻模型实现
  • kd 树搜索单个近邻点
  • kd 树搜索 k 个近邻点

k 近邻法(k-nearest neighbor,k-NN)是一种基本分类与回归方法。k 近邻法假设给定一个训练数据集,其中的实例类别已定。分类时,对新的实例,根据其 k 个最近邻的训练实例的类别,通过多数表决等方式进行预测。因此,k近邻法不具有显式的学习过程。k 近邻法实际上利用训练数据集对特征向量空间进行划分,并作为其分类的“模型”。k 值的选择、距离度量及分类决策规则是 k 近邻法的三个基本要素。

k 近邻算法

  1. 给定数据集 T=\{(x_1,y_1), (x_2,y_2),...,(x_N,y_N)\},其中, x_i \in X \subseteq R^n 为实例的特征向量, y_i \in Y = \{c_1, c_2, ..., c_K\} 为实例的类别, i=1,2,...,N;实例的特征向量 x
    1>> 根据给定的距离度量,在训练集 T 中找出与 x 最邻近的 k 个点,涵盖这 k 个点的 x 的邻域记作 N_k(x)
    2>> 在 N_k(x) 中根据分类决策规则(如多数表决)决定 x 的类别 y
    y = arg\ max_{c_j} \sum_{x_i \in N_i(x)}I(y_i=c_j),\ \ \ \ \ i=1,2,..,N; j=1,2,...,K
    式中I 为指示函数。

  2. k 近邻法的特殊情况是 k=1 的情形,称为最近邻算法。对于输入的实例点(特征向量)x,最近邻法将训练数据集中与 x 最邻近点的类作为 x 的类。

k 近邻模型

  1. 模型由三个基本要素——距离度量、k 值的选择和分类决策规则决定。

  2. 特征空间中,对每个训练实例点 x_i,距离该点比其他点更近的所有点组成一个区域,叫作单元(cell)。


  1. 特征空间中两个实例点的距离是两个实例点相似程度的反映。(欧氏距离、L_p距离、Minkowski 距离)

  2. 设特征空间 Xn 维实数向量空间 R^nx_i,x_j \in Xx_i=(x_i^{(1)}, x_i^{(2)},...,x_i^{{n}})^Tx_j=(x_j^{(1)}, x_j^{(2)},...,x_j^{{n}})^Tx_i,x_jL_p 距离定义为
    L_p(x_i,x_j) = (\sum_{I=1}^n \mid x_i^{(I)} - x_j^{(I)} \mid ^p)^{\frac{1}{p}}
    这里 p \ge 1。当 p=2 时, 称为欧氏距离,即
    L_2(x_i,x_j) = (\sum_{I=1}^n \mid x_i^{(I)} - x_j^{(I)} \mid ^2)^{\frac{1}{2}}
    p=1 时, 称为哈曼顿距离,即
    L_1(x_i,x_j) = \sum_{I=1}^n \mid x_i^{(I)} - x_j^{(I)} \mid
    p=\infty,它是各个坐标距离的最大值,即
    L_\infty(x_i,x_j) = max \mid x_i^{(I)} - x_j^{(I)} \mid

  3. k 值的选择会对 k 近邻法的结果产生重大影响。如果选择较小的 k 值,就相当于用较小的邻域中的训练实例进行预测,“学习”的近似误差(approximation error)会减小,只有与输入实例较近的(相似的)训练实例才会对预测结果起作用。但缺点是“学习”的估计误差(estimation error)会增大,预测结果会对近邻的实例点非常敏感。如果邻近的实例点恰巧是噪声,预测就会出错。换句话说,k 值的减小就意味着整体模型变得复杂,容易发生过拟合。
    如果选择较大的 k 值,就相当于用较大邻域中的训练实例进行预测。其优点是可以减少学习的估计误差。但缺点是学习的近似误差会增大。这时与输入实例较远的(不相似的)训练实例也会对预测起作用,使预测发生错误。k 值的增大就意味着整体的模型变得简单。
    如果 k=N,那么无论输入实例是什么,都将简单地预测它属于在训练实例中最多的类。这时,模型过于简单,完全忽略训练实例中的大量有用信息,是不可取的。
    在应用中,k 值一般取一个比较小的数值。通常采用交叉验证法来选取最优的 k 值。

  4. k 近邻法中的分类决策规则往往是多数表决,即由输入实例的 k 个邻近的训练实例中的多数类决定输入实例的类。

k 近邻法的实现:kd 树

  1. 实现 k 近邻法时,主要考虑的问题是如何对训练数据进行快速 k 近邻搜索。k 近邻法最简单的实现方法是线性扫描(linear scan)。这时要计算输入实例与每一个训练实例的距离。当训练集很大时,计算非常耗时,这种方法是不可行的。

  2. kd 树是一种对 k 维空间中的实例点进行存储以便对其进行快速检索的树形数据结构。kd 树是二叉树,表示对 k 维空间的一个划分(partition)。构造 kd 树相当于不断地用垂直于坐标轴的超平面将 k 维空间切分,构成一系列的 k 维超矩形区域。kd 树的每个结点对应于一个 k 维超矩形区域。

  3. 构造 kd 树的方法如下:构造根结点,使根结点对应于 k 维空间中包含所有实例点的超矩形区域;通过下面的递归方法,不断地对 k 维空间进行切分,生成子结点。在超矩形区域(结点)上选择一个坐标轴和在此坐标轴上的一个切分点,确定一个超平面,这个超平面通过选定的切分点并垂直于选定的坐标轴,将当前超矩形区域切分为左右两个子区域(子结点);这时,实例被分到两个子区域。这个过程直到子区域内没有实例时终止(终止时的结点为叶结点)。在此过程中,将实例保存在相应的结点上。

  4. 通常,依次选择坐标轴对空间切分,选择训练实例点在选定坐标轴上的中位数(median)为切分点,这样得到的 kd 树是平衡的。注意,平衡的 kd 树搜索时的效率未必是最优的。

  5. 构造平衡 kd 树:
    给定 k 维空间数据集 T=\{x_1,x_2,...,x_N\},其中 x_i=(x_i^{(1)}, x_i^{(2)},...,x_i^{(k)})^Ti=1,2,...,N
    1>> 开始:构造根结点,根结点对应于包含 T 的 k 维空间的超矩形区域。选择 x^{(1)} 为坐标轴,以 T 中所有实例的 x^{(1)} 坐标的中位数为切分点,将根结点对应的超矩形区域切分为两个子区域。切分由通过切分点并与坐标轴 x^{(1)} 垂直的超平面实现。
    由根结点生成深度为 1 的左、右子结点:左子结点对应坐标 x^{(1)} 小于切分点的子区域,右子结点对应于坐标 x^{(1)} 大于切分点的子区域。
    将落在切分超平面上的实例点保存在根结点。
    2>> 重复:对深度为 j 的结点,选择 x^{(p)} 为切分的坐标轴,p=j(mod\ k)+1,以该结点的区域中所有实例的 x^{(p)} 坐标的中位数为切分点,将该结点对应的超矩形区域切分为两个子区域。切分由通过切分点并与坐标轴 x^{(p)} 垂直的超平面实现。
    由该结点生成深度为 j+1 的左、右子结点:左子结点对应坐标 x^{(p)} 小于切分点的子区域,右子结点对应坐标 x^{(p)} 大于切分点的子区域。
    将落在切分超平面上的实例点保存在该结点。
    3>> 直到两个子区域没有实例存在时停止。从而形成kd树的区域划分。

  6. kd 树示例:
    给定一个二维空间的数据集:T=\{(2,3)^T,(5,4)^T,(9,6)^T,(4,7)^T,(8,1)^T,(7,2)^T\}
    根结点对应包含数据集 T 的矩形,选择 x^{(1)} 轴,6 个数据点的 x^{(1)} 坐标的中位数是 7,以平面 x^{(1)}=7 将空间分为左、右两个子矩形(子结点);接着,左矩形以 x^{(2)}=4 分为两个子矩形,右矩形以 x^{(2)}=6 分为两个子矩形,如此递归,得到如下特征空间划分图及 kd 树图。

搜索 kd 树

  1. 在 kd 树中找出包含目标点 x 的叶结点:从根结点出发,递归地向下访问 kd 树。若目标点 x 当前维的坐标小于切分点的坐标,则移动到左子结点,否则移动到右子结点。直到子结点为叶结点为止。

  2. 以此叶结点为“当前最近点”。

  3. 递归地向上回退,在每个结点进行以下操作:
    1>> 如果该结点保存的实例点比当前最近点距离目标点更近,则以该实例点为“当前最近点”。
    2>> 当前最近点一定存在于该结点一个子结点对应的区域。检查该子结点的父结点的另一子结点对应的区域是否有更近的点。具体地,检查另一子结点对应的区域是否与以目标点为球心、以目标点与“当前最近点”间的距离为半径的超球体相交。如果相交,可能在另一个子结点对应的区域内存在距目标点更近的点,移动到另一个子结点。接着,递归地进行最近邻搜索;如果不相交,向上回退。

  4. 当回退到根结点时,搜索结束。最后的“当前最近点”即为 x 的最近邻点。

  5. kd 树更适用于训练实例数远大于空间维数时的 k 近邻搜索。当空间维数接近训练实例数时,它的效率会迅速下降,几乎接近线性扫描。

  6. kd 树搜索示例:
    给定一个如下图所示的 kd 树,根结点为 A,其子结点为 B,C 等。树上共存储 7 个实例点;另有一个输入目标实例点 S,求 S 的最近邻。
    解:首先在 kd 树中找到包含点 S 的叶结点 D(图中的右下区域),以点 D 作为近似最近邻。真正最近邻一定在以点 S 为中心通过点 D 的圆的内部。然后返回结点 D 的父结点 B,在结点 B 的另一子结点 F 的区域内搜索最近邻。结点 F 的区域与圆不相交,不可能有最近邻点。继续返回上一级父结点 A,在结点 A 的另一子结点 C 的区域内搜索最近邻。结点 C 的区域与圆相交;该区域在圆内的实例点有点 E,点 E 比点 D 更近,成为新的最近邻近似。最后得到点 E 是点 S 的最近邻。

k 近邻模型实现

k 近邻模型实现

import math
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from collections import Counter


class Knn(object):
    def __init__(self, x_train, y_train, k=3, p=2):
        self.k = k
        self.p = p
        self.x_train = x_train
        self.y_train = y_train
    
    # 计算 Lp 距离
    def lp(self, x, y, p=2):
        if len(x) == len(y) and len(x) > 1:
            _sum = sum([math.pow(abs(x[i] - y[i]), p) for i in range(len(x))])
            return math.pow(_sum, 1/p)
        else:
            return 0
    
    # 预测
    def predict(self, x):
        # 计算 x 到其他节点的距离
        dists = [(np.linalg.norm(x - self.x_train[i], ord=self.p), self.y_train[i])
                 for i in range(len(x_train))]
        # 排序筛选出最近的 k 个节点
        nodes = sorted(dists, key=lambda x: x[0])[:self.k]
        # 对最近的 k 个节点的所属类别进行计数
        counter = Counter([node[-1] for node in nodes])
        # 返回计数最多的一个类别
        return counter.most_common(1)[0][0]
    
    # 计算模型的准确率
    def score(self, x_test, y_test):
        rights = 0
        for x, y in zip(x_test, y_test):
            label = self.predict(x)
            if label == y:
                rights += 1
        return rights / len(x_test)


if __name__ == '__main__':
    # 获取鸢尾花数据集
    iris = load_iris()
    df = pd.DataFrame(iris.data, columns=iris.feature_names)
    df['label'] = iris.target
    df.columns = ['sepal length', 'sepal width', 'petal length', 'petal width', 'label']
    # 生成训练样本,只取 sepal length,sepal width 作为样本特征
    train = np.array(df.iloc[:100, [0, 1, -1]])
    x, y = train[:, :-1], train[:, -1]
    # 划分训练集与测试集
    x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.2)
    # 生成 knn 模型,并使用测试集进行验证
    knn = Knn(x_train, y_train)
    print('测试集验证模型准确率为:%s' % knn.score(x_test, y_test))
    # 对点 [6.0, 3.0] 进行分类
    print('点 [6.0, 3.0] 分类结果为: %s' % knn.predict([6.0, 3.0]))
    # 绘图
    plt.scatter(df[:50]['sepal length'], df[:50]['sepal width'], label='0')
    plt.scatter(df[50:100]['sepal length'], df[50:100]['sepal width'], label='1')
    plt.plot(6.0, 3.0, 'bo', label='[6.0, 3.0]')
    plt.xlabel('sepal length')
    plt.ylabel('sepal width')
    plt.legend()

运行结果

也可以使用 sklearn 实现 knn 模型

from sklearn.neighbors import KNeighborsClassifier
# KNeighborsClassifier 参数
# n_neighbors:临近点个数
# p:距离度量
# algorithm:近邻算法,可选{'auto', 'ball_tree', 'kd_tree', 'brute'}
# weights:近邻的权重
knn_sk = KNeighborsClassifier()
knn_sk.fit(x_train, y_train)
knn_sk.score(x_test, y_test)

kd 树搜索单个近邻点

from math import sqrt
from collections import namedtuple


class KdNode(object):
    # 节点数据结构
    def __init__(self, node, split, left, right):
        self.node = node # k 维向量节点
        self.split = split # 分割维度序号
        self.left = left # 左树
        self.right = right # 右树


class KdTree(object):
    def __init__(self, data):
        self.k = len(data[0]) # 数据维度
        self.root = self.create_kdnode(0, data) # 生成 kd 树
        
    def create_kdnode(self, split, data):
        if not data: return None
        data.sort(key=lambda x: x[split])
        split_index = len(data) // 2
        median = data[split_index] # 中位数分割点 
        split_next = (split + 1) % self.k
        return KdNode(
            median,
            split,
            self.create_kdnode(split_next, data[:split_index]),
            self.create_kdnode(split_next, data[split_index + 1:])
        )
    
    # 对于构建好的 kd 树 tree,寻找离 node 最近的节点
    def nearest(self, node):
        k = len(node)
        nearest_tuple = namedtuple("nearest_tuple", "nearest_node nearest_dist visited")
        
        def travel(kd_node, target, nearest_dist):
            # python中用float("inf")和float("-inf")表示正负无穷
            if kd_node is None: return nearest_tuple([0] * k, float("inf"), 0)
            
            visited = 1
            # 分割维度
            split = kd_node.split
            # 分割节点
            node = kd_node.node

            # 如果目标点第 split 维小于分割节点的对应值,则目标离左子树更近,否则离右子树更近
            if target[split] <= node[split]:
                nearer_node = kd_node.left
                further_node = kd_node.right
            else:
                nearer_node = kd_node.right
                further_node = kd_node.left

            # 进行遍历找到包含目标点的区域
            nearest_tuple_1 = travel(nearer_node, target, nearest_dist)
            nearest = nearest_tuple_1.nearest_node
            dist = nearest_tuple_1.nearest_dist
            visited += nearest_tuple_1.visited
            # 更新最近距离
            if dist < nearest_dist:
                nearest_dist = dist
            # 第 split 维上目标点与分割超平面的距离
            temp_dist = abs(node[split] - target[split])
            # 判断超球体是否与超平面相交
            if  temp_dist > nearest_dist:
                # 相交则可以直接返回,不用继续判断
                return nearest_tuple(nearest, dist, visited)
            
            # 计算目标点与分割点的欧氏距离
            temp_dist = sqrt(sum((p1 - p2) ** 2 for p1, p2 in zip(node, target)))
            # 如果更近,更新最近距离
            if temp_dist < nearest_dist: 
                nearest = node
                nearest_dist = temp_dist

            # 检查另一个子结点对应的区域是否有更近的点
            nearest_tuple_2 = travel(further_node, target, nearest_dist) 
            visited += nearest_tuple_2.visited
            if nearest_tuple_2.nearest_dist < nearest_dist:  
                nearest = nearest_tuple_2.nearest_node
                nearest_dist = nearest_tuple_2.nearest_dist
            return nearest_tuple(nearest, nearest_dist, visited)
        # 从根节点开始递归
        return travel(self.root, node, float("inf"))


if __name__ == '__main__':
    data = [[2,3],[5,4],[9,6],[4,7],[8,1],[7,2]]
    kd = KdTree(data)
    print(kd.nearest([3, 5]))

运行结果

nearest_tuple(nearest_node=[4, 7], nearest_dist=2.23606797749979, visited=4)

kd 树搜索 k 个近邻点

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from math import sqrt
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from collections import Counter


class KdNode(object):
    def __init__(self, data, depth=0, lchild=None, rchild=None):
        self.data = data
        self.depth = depth
        self.lchild = lchild
        self.rchild = rchild


class KdTree(object):
    def __init__(self):
        self.n = 0
        self.tree = None
        self.nearest = None

    def create(self, data_set, depth=0):
        if len(data_set) > 0:
            m, n = np.shape(data_set)
            self.n, axis, mid = n - 1, depth % (n - 1), int(m / 2)
            data_set_sorted = sorted(data_set, key=lambda x: x[axis])
            node = KdNode(data_set_sorted[mid], depth)
            if depth == 0: self.tree = node
            node.lchild = self.create(data_set_sorted[:mid], depth+1)
            node.rchild = self.create(data_set_sorted[mid+1:], depth+1)
            return node
        return None

    # 搜索 kdtree 的前 k 个最近点
    def search(self, x, k=1):
        nearest = []
        for i in range(k):
            nearest.append([-1, None])
        # 初始化 n 个点,nearest 是按照距离递减的方式
        self.nearest = np.array(nearest)

        def travel(node):
            if node is not None:
                # 当前点的维度 axis
                axis = node.depth % self.n
                # x 点和当前点在 axis 维度上的差
                daxis = x[axis] - node.data[axis]
                # 如果小于进左子树,大于进右子树
                if daxis < 0:
                    travel(node.lchild)
                else:
                    travel(node.rchild)

                # 计算 x 点到当前点的距离 dist
                dist = sqrt(sum((p1 - p2) ** 2 for p1, p2 in zip(x, node.data)))
                for i, d in enumerate(self.nearest):
                    # 如果有比现在最近的 n 个点更近的点,更新最近的点
                    if d[0] < 0 or dist < d[0]:
                        # 插入第 i 个位置的点
                        self.nearest = np.insert(self.nearest, i, [dist, node], axis=0)
                        # 删除最后一个多出来的点
                        self.nearest = self.nearest[:-1]
                        break
                
                # 统计距离为 -1 的个数 n
                n = list(self.nearest[:, 0]).count(-1)
                # self.nearest[-n-1, 0] 是当前 nearest 中已经有的最近点中,距离最大的点
                # self.nearest[-n-1, 0] > abs(daxis) 代表以 x 点为圆心,self.nearest[-n-1, 0]为半径的圆
                # 与 axis 相交,说明在左右子树里面可能有比 self.nearest[-n-1, 0] 更近的点
                if self.nearest[-n-1, 0] > abs(daxis):
                    if daxis < 0:
                        travel(node.rchild)
                    else:
                        travel(node.lchild)

        travel(self.tree)
        # nodes 就是最近 k 个点
        nodes = self.nearest[:, 1]
        counter = Counter([node.data[-1] for node in nodes])
        return self.nearest, counter.most_common(1)[0][0]


if __name__ == '__main__':
    # 获取鸢尾花数据集

    iris = load_iris()
    df = pd.DataFrame(iris.data, columns=iris.feature_names)
    df['label'] = iris.target
    df.columns = ['sepal length', 'sepal width', 'petal length', 'petal width', 'label']
    # 生成训练样本,只取 sepal length,sepal width 作为样本特征
    data = np.array(df.iloc[:100, [0, 1, -1]])
    # 划分训练集与测试集
    train, test = train_test_split(data, test_size=0.1)
    x0 = np.array([x0 for i, x0 in enumerate(train) if train[i][-1] == 0])
    x1 = np.array([x1 for i, x1 in enumerate(train) if train[i][-1] == 1])
        
    # 生成 knn 模型,并使用测试集进行验证
    kdt = KdTree()
    kdt.create(train)

    score = 0
    for x in test:
        # 绘制训练点
        plt.scatter(x0[:, 0], x0[:, 1], c='pink', label='[0]')
        plt.scatter(x1[:, 0], x1[:, 1], c='orange', label='[1]')
        plt.xlabel('sepal length')
        plt.ylabel('sepal width')
         # 绘制测试点
        plt.scatter(x[0], x[1], c='red', marker='x')
        # 设置临近点的个数
        nearest, belong = kdt.search(x[:-1], 5)
        if belong == x[-1]: score += 1
        print("test:", x, "predict:", belong)
        print("nearest:", nearest)
        for near in nearest:
            # k 个近邻点
            plt.scatter(near[1].data[0], near[1].data[1], c='green', marker='+')
        plt.legend()
        plt.show()

    score /= len(test)
    print("score:", score)

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