📜  K最近邻居的Python实现

📅  最后修改于: 2020-04-26 05:00:43             🧑  作者: Mango

先决条件:K临近算法

介绍

假设我们获得了一组数据,每个数据项均具有数值特征(例如身高,体重,年龄等)。如果特征数为n,则可以将这些项表示为n维网格中的点。给定一个新项目,我们可以计算出该项目到集合中所有其他项目的距离。我们选择k个最近的邻居,然后看到大多数这些邻居被归类在哪里。我们在那里对新项目进行分类。

因此,问题就变成了我们如何计算项目之间的距离。解决方案取决于数据集。如果值是实数,我们通常使用欧几里得距离。如果值是分类的或二进制的,则通常使用汉明距离Hamming distance。
算法:

对新的样本:

1.查找新项目和所有其他项目之间的距离

2.选择k个较短的距离

3.在这k个距离中选择最常见的类

4.该类是我们将新项目分类的地方

读取数据

让我们的输入文件采用以下格式:

Height, Weight, Age, Class
1.70, 65, 20, Programmer
1.90, 85, 33, Builder
1.78, 76, 31, Builder
1.73, 74, 24, Programmer
1.81, 75, 35, Builder
1.73, 70, 75, Scientist
1.80, 71, 63, Scientist
1.75, 69, 25, Programmer

每个项目都是一行,在“Class”下我们可以看到该项目的分类位置。要素名称(“Height”等)下的值是该项目对该要素具有的值。所有值和特征均以逗号分隔。
将这些数据文件放在工作目录data2data。选择一个并将其内容粘贴到名为data的文本文件中。
我们将从文件(名为“ data.txt”)中读取内容,并将输入内容按行划分:

f = open('data.txt', 'r');
lines = f.read().splitlines();
f.close();

文件的第一行包含要素名称,末尾带有关键字“ Class”。我们要将特征名称存储在列表中:

# 用逗号分隔第一行,删除第一个元素,将其余元素保存到列表中。 
# 列表现在包含数据集的功能名称.
features = lines[0].split(', ')[:-1];

然后,我们进入数据集本身。我们将这些项目保存到一个名为items的列表中,其元素为字典(每个项目一个)。这些项目字典的关键字是要素名称,外加用于保存项目类别的“类”。最后,我们要对列表中的项目进行洗牌(这是一项安全措施,以防这些项目处于怪异状态)。

items = [];
for i in range(1, len(lines)):
    line = lines[i].split(', ');
    itemFeatures = {"Class" : line[-1]};
    # 遍历特征
    for j in range(len(features)):
        # 在索引j处获取特征
        f = features[j];
        # 行中的第一项是class,请跳过它
        v = float(line[j]);
        # 向字典添加功能
        itemFeatures[f] = v;
    # 将临时命令附加到项目
    items.append(itemFeatures);
shuffle(items);

分类数据

将数据存储到item中后,我们现在开始构建分类器。对于分类器,我们将创建一个新函数Classify。我们将要分类的项目,项目列表和k(最接近的邻居数)作为输入。

如果k大于数据集的长度,则我们不进行分类,因为我们不能拥有比数据集中的项目总数更近的邻居。(或者,我们可以将k设置为项目长度,而不是返回错误消息)

if(k > len(Items)):
        # k大于列表的长度,中止
        return "k larger than list length";

我们要计算分类的项目与训练集中的所有项目之间的距离,最后保持k最短的距离。为了保持当前最近的邻居,我们使用一个列表,称为neighbors。最少的每个元素都具有两个值,一个是与要分类的项目的距离,另一个是与邻居所在类别的距离。我们将通过广义的欧几里得公式(对于n个维)计算距离。然后,我们将选择大多数情况下在邻居中出现的类别,这将是我们选择的类。在代码中:

def Classify(nItem, k, Items):
    if(k > len(Items)):
        # k大于列表的长度,中止
        return "k larger than list length";

    neighbors = [];
    for item in Items:
        # 查找欧几里得距离
        distance = EuclideanDistance(nItem, item);
        # 更新邻居,或者在邻居中添加当前项.
        neighbors = UpdateNeighbors(neighbors, item, distance, k);
    # 计算邻居中每个类的数量
    count = CalculateNeighborsClass(neighbors, k);
    # 找到最多的次数,也就是出现次数最多的类.
    return FindMax(count);

我们需要实现的外部函数是EuclideanDistanceUpdateNeighborsCalculateNeighborsClassFindMax

寻找欧几里得距离

两个向量x和y的广义欧几里得公式是这样的:

distance = sqrt{(x_{1}-y_{1})^2 + (x_{2}-y_{2})^2 + ... + (x_{n}-y_{n})^2}

在代码中:

def EuclideanDistance(x, y):
    # 元素的差平方的总和
    S = 0;
    for key in x.keys():
        S += math.pow(x[key]-y[key], 2);
    # 和的平方根
    return math.sqrt(S);

更新邻居

我们有邻居列表(其最大长度应为k),并且我们要向列表中添加给定距离的项目。首先,我们将检查邻居的长度是否为k。如果数量较少,则无论距离多长,我们都将其添加到该项目中(因为在开始拒绝该项目之前,我们需要将列表填满k个)。如果不是,我们将检查该数据的距离是否短于列表中具有最大距离的数据项,如果成立,我们将以最大距离替换新的数据。

为了更快地找到最大距离,我们将列表按升序排列。因此,列表中的最后一项将具有最大距离。我们将其替换为新项,然后再次排序。为了加快此过程,我们可以实现插入排序,在其中我们可以在列表中插入新项,而不必对整个列表进行排序。

def UpdateNeighbors(neighbors, item, distance, k):
    if(len(neighbors) > distance):
            # 如果是,请用新项目替换最后一个元素
            neighbors[-1] = [distance, item["Class"]];
            neighbors = sorted(neighbors);
    return neighbors;

计算相邻类别

在这里,我们将计算在邻居中最常出现的类。为此,我们将使用另一个称为count的字典,其中的键是出现在neighbors中的类名。如果键不存在,则将其添加,否则将增加其值。

def CalculateNeighborsClass(neighbors, k):
    count = {};
    for i in range(k):
        if(neighbors[i][1] not in count):
            # 第ith个索引处的类不在count dict中.
            # 初始化为1.
            count[neighbors[i][1]] = 1;
        else:
            # 找到了另一个班级#c [i]。增加计数器.
            count[neighbors[i][1]] += 1;
    return count;

FindMax

我们将在CalculateNeighborsClass中构建的字典计数输入到此函数,然后返回其最大值。

def FindMax(countList):
    # 储蓄最大值
    maximum = -1;
    # 储蓄类别
    classification = "";
    for key in countList.keys():
        if(countList[key] > maximum):
            maximum = countList[key];
            classification = key;
    return classification, maximum;

结论

这样,本kNN教程就完成了。
现在,您可以对新项进行分类,并根据需要设置k。通常对于k使用奇数,但这不是必需的。要对新项目进行分类,您需要创建一个字典,其中包含功能名称和表征该项目的值的键。分类示例:

newItem = {'Height' : 1.74, 'Weight' : 67, 'Age' : 22};
print Classify(newItem, 3, items);

上述方法的完整代码如下:

# Python程序说明KNN算法

import math
from random import shuffle
###_Reading_### def ReadData(fileName):
    # 读取文件,按行分割
    f = open(fileName, 'r')
    lines = f.read().splitlines()
    f.close()
    # 用逗号分隔第一行,删除第一个元素,然后将其余保存到列表中。
    # 列表保存数据集的要素名称。
    features = lines[0].split(', ')[:-1]
    items = []
    for i in range(1, len(lines)):
        line = lines[i].split(', ')
        itemFeatures = {'Class': line[-1]}
        for j in range(len(features)):
            # 在索引j处获取特征
            f = features[j]
            # 将要素值转换为浮点
            v = float(line[j])
             # 将特征值添加到字典
            itemFeatures[f] = v
        items.append(itemFeatures)
    shuffle(items)
    return items
###_Auxiliary Function_### def EuclideanDistance(x, y):
    # 元素差平方的总和
    S = 0
    for key in x.keys():
        S += math.pow(x[key] - y[key], 2)
    # 和的平方根
    return math.sqrt(S)
def CalculateNeighborsClass(neighbors, k):
    count = {}
    for i in range(k):
        if neighbors[i][1] not in count:
            # ith索引处的类不在dict中。 初始化为1。
            count[neighbors[i][1]] = 1
        else:
            # 找到了另一个类别c [i]。增加计数器.
            count[neighbors[i][1]] += 1
    return count
def FindMax(Dict):
    # 在字典中查找最大值,返回最大值和最大索引
    maximum = -1
    classification = ''
    for key in Dict.keys():
        if Dict[key] > maximum:
            maximum = Dict[key]
            classification = key
    return (classification, maximum)
###_Core Functions_### def Classify(nItem, k, Items):
    # 保持最近的邻居。第一项是距离,第二类
    neighbors = []
    for item in Items:
        # 查找欧几里得距离
        distance = EuclideanDistance(nItem, item)
        # 更新邻居,是否在邻居中添加当前项。
        neighbors = UpdateNeighbors(neighbors, item, distance, k)
    # 计算邻居中每个班级的数量
    count = CalculateNeighborsClass(neighbors, k)
    # 找到最多的次数,也就是出现次数最多的类
    return FindMax(count)
def UpdateNeighbors(neighbors, item, distance,
                                          k, ):
    if len(neighbors) < k:
        # 列表不完整,添加新项并排序
        neighbors.append([distance, item['Class']])
        neighbors = sorted(neighbors)
    else:
        # 列表已满检查是否应输入新的号
        if neighbors[-1][0] > distance:
            # 如果是,请用新项替换last元素
            neighbors[-1] = [distance, item['Class']]
            neighbors = sorted(neighbors)
    return neighbors
###_Evaluation Functions_### def K_FoldValidation(K, k, Items):
    if K > len(Items):
        return -1
    # 正确分类数
    correct = 0
    # 分类总数
    total = len(Items) * (K - 1)
    # 折的长度
    l = int(len(Items) / K)
    for i in range(K):
        # 将数据分为训练集和测试集
        trainingSet = Items[i * l:(i + 1) * l]
        testSet = Items[:i * l] + Items[(i + 1) * l:]
        for item in testSet:
            itemClass = item['Class']
            itemFeatures = {}
            # 获取特征值
            for key in item:
                if key != 'Class':
                    # 如果键不是“ Class",则将其添加到itemFeatures
                    itemFeatures[key] = item[key]
            # 根据其特征值对项目进行分类
            guess = Classify(itemFeatures, k, trainingSet)[0]
            if guess == itemClass:
                # 正确猜测
                correct += 1
    accuracy = correct / float(total)
    return accuracy
def Evaluate(K, k, items, iterations):
    # 运行算法迭代次数,选择平均值
    accuracy = 0
    for i in range(iterations):
        shuffle(items)
        accuracy += K_FoldValidation(K, k, items)
    print accuracy / float(iterations)
###_Main_### def main():
    items = ReadData('data.txt')
    Evaluate(5, 5, items, 100)
if __name__ == '__main__':
    main()

输出:

0.9375

输出因机器而异。该代码包含交叉验证功能,但与算法无关,可用于计算算法的准确性。