zoukankan      html  css  js  c++  java
  • 实验二 K-近邻算法及应用

    【实验目的】

    理解K-近邻算法原理,能实现算法K近邻算法;
    掌握常见的距离度量方法;
    掌握K近邻树实现算法;
    针对特定应用场景及数据,能应用K近邻解决实际问题。

    【实验内容】

    实现曼哈顿距离、欧氏距离、闵式距离算法,并测试算法正确性。
    实现K近邻树算法;
    针对iris数据集,应用sklearn的K近邻算法进行类别预测。
    针对iris数据集,编制程序使用K近邻树进行类别预测。

    【实验报告要求】

    对照实验内容,撰写实验过程、算法及测试结果;
    代码规范化:命名规则、注释;
    分析核心算法的复杂度;
    查阅文献,讨论K近邻的优缺点;
    举例说明K近邻的应用场景。

    k近邻法

    1.(k)近邻法是基本且简单的分类与回归方法。(k)近邻法的基本做法是:对给定的训练实例点和输入实例点,首先确定输入实例点的(k)个最近邻训练实例点,然后利用这(k)个训练实例点的类的多数来预测输入实例点的类。
    2.(k)近邻模型对应于基于训练数据集对特征空间的一个划分。(k)近邻法中,当训练集、距离度量、(k)值及分类决策规则确定后,其结果唯一确定。
    3.(k)近邻法三要素:距离度量、(k)值的选择和分类决策规则。常用的距离度量是欧氏距离及更一般的pL距离。(k)值小时,(k)近邻模型更复杂;(k)值大时,(k)近邻模型更简单。(k)值的选择反映了对近似误差与估计误差之间的权衡,通常由交叉验证选择最优的(k)
    常用的分类决策规则是多数表决,对应于经验风险最小化。
    4.(k)近邻法的实现需要考虑如何快速搜索k个最近邻点。kd树是一种便于对k维空间中的数据进行快速检索的数据结构。kd树是二叉树,表示对(k)维空间的一个划分,其每个结点对应于(k)维空间划分中的一个超矩形区域。利用kd树可以省去对大部分数据点的搜索, 从而减少搜索的计算量。

    距离度量

    设特征空间(x)(n)维实数向量空间 ,(x_{i}, x_{j} in mathcal{X}),(x_{i}=left(x_{i}^{(1)}, x_{i}^{(2)}, cdots, x_{i}^{(n)} ight)^{mathrm{T}}),(x_{j}=left(x_{j}^{(1)}, x_{j}^{(2)}, cdots, x_{j}^{(n)} ight)^{mathrm{T}})
    ,则:(x_i),(x_j)(L_p)距离定义为:
    (L_{p}left(x_{i}, x_{j} ight)=left(sum_{i=1}^{n}left|x_{i}^{(i)}-x_{j}^{(l)} ight|^{p} ight)^{frac{1}{p}})

    • (p= 1) 曼哈顿距离
    • (p= 2) 欧氏距离
    • (p= infty) 切比雪夫距离
    import math
    from itertools import combinations
    def L(x, y, p=2):
        # x1 = [1, 1], x2 = [5,1]
        if len(x) == len(y) and len(x) > 1:
            sum = 0
            for i in range(len(x)):
                sum += math.pow(abs(x[i] - y[i]), p)
            return math.pow(sum, 1 / p)
        else:
            return 0
    
    x1 = [1, 1]
    x2 = [5, 1]
    x3 = [4, 4]
    # x1, x2
    for i in range(1, 5):
        r = {'1-{}'.format(c): L(x1, c, p=i) for c in [x2, x3]}
        print(min(zip(r.values(), r.keys())))
    import numpy as np
    import pandas as pd
    import matplotlib.pyplot as plt
    %matplotlib inline
    
    from sklearn.datasets import load_iris
    from sklearn.model_selection import train_test_split
    from collections import Counter
    
    # data
    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']
    # data = np.array(df.iloc[:100, [0, 1, -1]])
    
    df
    
    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.xlabel('sepal length')
    plt.ylabel('sepal width')
    plt.legend()
    

    data = np.array(df.iloc[:100, [0, 1, -1]])
    X, y = data[:,:-1], data[:,-1]
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
    
    class KNN:
        def __init__(self, X_train, y_train, n_neighbors=3, p=2):
            """
            parameter: n_neighbors 临近点个数
            parameter: p 距离度量
            """
            self.n = n_neighbors
            self.p = p
            self.X_train = X_train
            self.y_train = y_train
    
        def predict(self, X):
            # 取出n个点
            knn_list = []
            for i in range(self.n):
                dist = np.linalg.norm(X - self.X_train[i], ord=self.p)
                knn_list.append((dist, self.y_train[i]))
    
            for i in range(self.n, len(self.X_train)):
                max_index = knn_list.index(max(knn_list, key=lambda x: x[0]))
                dist = np.linalg.norm(X - self.X_train[i], ord=self.p)
                if knn_list[max_index][0] > dist:
                    knn_list[max_index] = (dist, self.y_train[i])
    
            # 统计
            knn = [k[-1] for k in knn_list]
            count_pairs = Counter(knn)
    #         max_count = sorted(count_pairs, key=lambda x: x)[-1]
            max_count = sorted(count_pairs.items(), key=lambda x: x[1])[-1][0]
            return max_count
    
        def score(self, X_test, y_test):
            right_count = 0
            n = 10
            for X, y in zip(X_test, y_test):
                label = self.predict(X)
                if label == y:
                    right_count += 1
            return right_count / len(X_test)
    
    clf = KNN(X_train, y_train)
    clf.score(X_test, y_test)
    test_point = [6.0, 3.0]
    print('Test Point: {}'.format(clf.predict(test_point)))
    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(test_point[0], test_point[1], 'bo', label='test_point')
    plt.xlabel('sepal length')
    plt.ylabel('sepal width')
    plt.legend()
    

    scikit-learn实例

    from sklearn.neighbors import KNeighborsClassifier
    clf_sk = KNeighborsClassifier()
    clf_sk.fit(X_train, y_train)
    clf_sk.score(X_test, y_test)
    

    kd树

    kd树是一种对k维空间中的实例点进行存储以便对其进行快速检索的树形数据结构。

    kd树是二叉树,表示对(k)维空间的一个划分(partition)。构造kd树相当于不断地用垂直于坐标轴的超平面将(k)维空间切分,构成一系列的k维超矩形区域。kd树的每个结点对应于一个(k)维超矩形区域。

    构造kd树的方法如下:

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

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

    构造平衡kd树算法

    输入:(k)维空间数据集(T={x_1,x_2,…,x_N})

    其中(x_{i}=left(x_{i}^{(1)}, x_{i}^{(2)}, cdots, x_{i}^{(k)} ight)^{mathrm{T}})(i=1,2,…,N)

    输出:kd树。

    (1)开始:构造根结点,根结点对应于包含(T)(k)维空间的超矩形区域。

    选择(x^{(1)})为坐标轴,以T中所有实例的(x^{(1)})坐标的中位数为切分点,将根结点对应的超矩形区域切分为两个子区域。切分由通过切分点并与坐标轴(x^{(1)})垂直的超平面实现。

    由根结点生成深度为1的左、右子结点:左子结点对应坐标(x^{(1)})小于切分点的子区域, 右子结点对应于坐标(x^{(1)})大于切分点的子区域。

    将落在切分超平面上的实例点保存在根结点。

    (2)重复:对深度为(j)的结点,选择(x^{(1)})为切分的坐标轴,(l=j(modk)+1),以该结点的区域中所有实例的(x^{(1)})坐标的中位数为切分点,将该结点对应的超矩形区域切分为两个子区域。切分由通过切分点并与坐标轴(x^{(1)})垂直的超平面实现。

    由该结点生成深度为(j+1)的左、右子结点:左子结点对应坐标(x^{(1)})小于切分点的子区域,右子结点对应坐标(x^{(1)})大于切分点的子区域。

    将落在切分超平面上的实例点保存在该结点。

    (3)直到两个子区域没有实例存在时停止。从而形成kd树的区域划分。

    # kd-tree每个结点中主要包含的数据结构如下
    class KdNode(object):
        def __init__(self, dom_elt, split, left, right):
            self.dom_elt = dom_elt  # k维向量节点(k维空间中的一个样本点)
            self.split = split  # 整数(进行分割维度的序号)
            self.left = left  # 该结点分割超平面左子空间构成的kd-tree
            self.right = right  # 该结点分割超平面右子空间构成的kd-tree
    
    
    class KdTree(object):
        def __init__(self, data):
            k = len(data[0])  # 数据维度
    
            def CreateNode(split, data_set):  # 按第split维划分数据集exset创建KdNode
                if not data_set:  # 数据集为空
                    return None
                # key参数的值为一个函数,此函数只有一个参数且返回一个值用来进行比较
                # operator模块提供的itemgetter函数用于获取对象的哪些维的数据,参数为需要获取的数据在对象中的序号
                #data_set.sort(key=itemgetter(split)) # 按要进行分割的那一维数据排序
                data_set.sort(key=lambda x: x[split])
                split_pos = len(data_set) // 2  # //为Python中的整数除法
                median = data_set[split_pos]  # 中位数分割点
                split_next = (split + 1) % k  # cycle coordinates
    
                # 递归的创建kd树
                return KdNode(
                    median,
                    split,
                    CreateNode(split_next, data_set[:split_pos]),  # 创建左子树
                    CreateNode(split_next, data_set[split_pos + 1:]))  # 创建右子树
    
            self.root = CreateNode(0, data)  # 从第0维分量开始构建kd树,返回根节点
    
    
    # KDTree的前序遍历
    def preorder(root):
        print(root.dom_elt)
        if root.left:  # 节点不为空
            preorder(root.left)
        if root.right:
            preorder(root.right)
    
    # 对构建好的kd树进行搜索,寻找与目标点最近的样本点:
    from math import sqrt
    from collections import namedtuple
    
    # 定义一个namedtuple,分别存放最近坐标点、最近距离和访问过的节点数
    result = namedtuple("Result_tuple",
                        "nearest_point  nearest_dist  nodes_visited")
    
    
    def find_nearest(tree, point):
        k = len(point)  # 数据维度
    
        def travel(kd_node, target, max_dist):
            if kd_node is None:
                return result([0] * k, float("inf"),
                              0)  # python中用float("inf")和float("-inf")表示正负无穷
    
            nodes_visited = 1
    
            s = kd_node.split  # 进行分割的维度
            pivot = kd_node.dom_elt  # 进行分割的“轴”
    
            if target[s] <= pivot[s]:  # 如果目标点第s维小于分割轴的对应值(目标离左子树更近)
                nearer_node = kd_node.left  # 下一个访问节点为左子树根节点
                further_node = kd_node.right  # 同时记录下右子树
            else:  # 目标离右子树更近
                nearer_node = kd_node.right  # 下一个访问节点为右子树根节点
                further_node = kd_node.left
    
            temp1 = travel(nearer_node, target, max_dist)  # 进行遍历找到包含目标点的区域
    
            nearest = temp1.nearest_point  # 以此叶结点作为“当前最近点”
            dist = temp1.nearest_dist  # 更新最近距离
    
            nodes_visited += temp1.nodes_visited
    
            if dist < max_dist:
                max_dist = dist  # 最近点将在以目标点为球心,max_dist为半径的超球体内
    
            temp_dist = abs(pivot[s] - target[s])  # 第s维上目标点与分割超平面的距离
            if max_dist < temp_dist:  # 判断超球体是否与超平面相交
                return result(nearest, dist, nodes_visited)  # 不相交则可以直接返回,不用继续判断
    
            #----------------------------------------------------------------------
            # 计算目标点与分割点的欧氏距离
            temp_dist = sqrt(sum((p1 - p2)**2 for p1, p2 in zip(pivot, target)))
    
            if temp_dist < dist:  # 如果“更近”
                nearest = pivot  # 更新最近点
                dist = temp_dist  # 更新最近距离
                max_dist = dist  # 更新超球体半径
    
            # 检查另一个子结点对应的区域是否有更近的点
            temp2 = travel(further_node, target, max_dist)
    
            nodes_visited += temp2.nodes_visited
            if temp2.nearest_dist < dist:  # 如果另一个子结点内存在更近距离
                nearest = temp2.nearest_point  # 更新最近点
                dist = temp2.nearest_dist  # 更新最近距离
    
            return result(nearest, dist, nodes_visited)
    
        return travel(tree.root, point, float("inf"))  # 从根节点开始递归
    
    data = [[2,3],[5,4],[9,6],[4,7],[8,1],[7,2]]
    kd = KdTree(data)
    preorder(kd.root)
    from time import clock
    from random import random
    
    # 产生一个k维随机向量,每维分量值在0~1之间
    def random_point(k):
        return [random() for _ in range(k)]
     
    # 产生n个k维随机向量 
    def random_points(k, n):
        return [random_point(k) for _ in range(n)]     
    ret = find_nearest(kd, [3,4.5])
    print (ret)
    
    N = 400000
    t0 = clock()
    kd2 = KdTree(random_points(3, N))            # 构建包含四十万个3维空间样本点的kd树
    ret2 = find_nearest(kd2, [0.1,0.5,0.8])      # 四十万个样本点中寻找离目标最近的点
    t1 = clock()
    print ("time: ",t1-t0, "s")
    print (ret2)
    

    实验结果

    time:  6.945672 s
    Result_tuple(nearest_point=[0.097329902511892, 0.49823498137918865, 0.7964108251004665], nearest_dist=0.004809042294184305, nodes_visited=42)
    

    实验小结

    1.K近邻的优缺点
    优点:
    (1)简单,易于理解,易于实现,无需估计参数。
    (2)训练时间为零。它没有显示的训练,不像其它有监督的算法会用训练集train一个模型(也就是拟合一个函数),然后验证集或测试集用该模型分类。KNN只是把样本保存起来,收到测试数据时再处理,所以KNN训练时间为零。
    (3)KNN可以处理分类问题,同时天然可以处理多分类问题,适合对稀有事件进行分类。
    (4)特别适合于多分类问题(multi-modal,对象具有多个类别标签), KNN比SVM的表现要好。
    (5)KNN还可以处理回归问题,也就是预测。
    (6)和朴素贝叶斯之类的算法比,对数据没有假设,准确度高,对异常点不敏感。
    缺点:
    (1)计算量太大,尤其是特征数非常多的时候。每一个待分类文本都要计算它到全体已知样本的距离,才能得到它的第K个最近邻点。
    (2)可理解性差,无法给出像决策树那样的规则。
    (3)是慵懒散学习方法,基本上不学习,导致预测时速度比起逻辑回归之类的算法慢。
    (4)样本不平衡的时候,对稀有类别的预测准确率低。当样本不平衡时,如一个类的样本容量很大,而其他类样本容量很小时,有可能导致当输入一个新样本时,该样本的K个邻居中大容量类的样本占多数。
    (5)对训练数据依赖度特别大,对训练数据的容错性太差。如果训练数据集中,有一两个数据是错误的,刚刚好又在需要分类的数值的旁边,这样就会直接导致预测的数据的不准确。
    2.K近邻应用场合
    题目描述:海伦喜欢在在线约会网站寻找适合自己的对象,但是她不是喜欢每一个人。她发现交往过三种类型的人:
      1.不喜欢的人
      2.魅力一般的人
      3.极具魅力的人
    所以需要对网站的对象归入恰当的分类。她周一到周五喜欢魅力一般的人,而周末则更喜欢极具魅力的人。所以需要根据数据来分类

  • 相关阅读:
    geoserver发布地图服务WMTS
    geoserver发布地图服务WMS
    geoserver安装部署步骤
    arcgis api 3.x for js 入门开发系列十四最近设施点路径分析(附源码下载)
    arcgis api 3.x for js 入门开发系列十三地图最短路径分析(附源码下载)
    cesium 之自定义气泡窗口 infoWindow 后续优化篇(附源码下载)
    arcgis api 3.x for js 入门开发系列十二地图打印GP服务(附源码下载)
    arcgis api 3.x for js 入门开发系列十一地图统计图(附源码下载)
    arcgis api 3.x for js 入门开发系列十叠加 SHP 图层(附源码下载)
    arcgis api 3.x for js入门开发系列九热力图效果(附源码下载)
  • 原文地址:https://www.cnblogs.com/smallsung/p/14797631.html
Copyright © 2011-2022 走看看