PyTorch框架深度学习笔记02(KNN分类机)

做好所有前置准备工作后,就要正式开始学习PyTorch了,我们以简单的KNN分类机为起点,本章主要以PyTorch的代码练习为中心。

(本次笔记全代码戳此)

什么是KNN?

最近邻居法K-Nearest Neighbor,又译K-近邻算法),是机器学习中最简单的算法之一。是一种可以用于分类(Classification)和回归(Regression)的非参数统计方法。

image-bmhh.png

KNN算法的原理并不复杂,简单来说就是通过计算测试数据周围K个最临近的样本数据(也就是所谓的邻居)来对测试数据进行归类的算法。这样就可以依赖样本数据对测试数据进行分类,比如上图,如果K=3,也就是考虑最临近的三个数据,那么测试样本应该被归类进红色,但是如果K=5,及考虑五个数据,那么就会被归类进蓝色。由此,我们也可以发现**K值的选定**是该算法较为核心的步骤。

在KNN算法的距离计算中,我们通常运用两种距离公式:

  • 欧式距离

    Distances = \sqrt{\left ( x_{1}-x_{2} \right )^{2} + \left ( y_{1}-y_{2} \right )^{2} }
    distances = torch.sqrt(torch.sum((train_point - test_point)**2, dim=1))
    
  • 曼哈顿距离

    Distances = \left | x_{1}-x_{2} \right | + \left | y_{1}-y_{2} \right |
    distances = torch.sum(torch.abs(train_point - test_point),dim=1)
    

构造数据集

上一篇笔记中我们已经学过如何用PyTorch进行随机数据的构建,我们此次可以通过 torch.rand() 方法进行随机点集的生成,然后指定一个线性函数对数据进行分类,当然,我们还可以用Matplotlib处理让数据可视化。

def generate_data(a, b, num_train=4000, num_test=1000):
    """生成线性分类数据集"""
    # 生成随机点
    x_train = torch.rand(num_train) * 20 - 10  # [-10, 10]
    y_train = torch.rand(num_train) * 20 - 10
  
    x_test = torch.rand(num_test) * 20 - 10
    y_test = torch.rand(num_test) * 20 - 10
  
    # 确定颜色 (直线上方为红色,标签记为1,下方或正好在线上为蓝色,标签记为0)
    # 通过zip(x_train, y_train)方式把合成(x,y)
    train_colors = torch.tensor([1 if y > a*x + b else 0 for x, y in zip(x_train, y_train)])
    test_colors = torch.tensor([1 if y > a*x + b else 0 for x, y in zip(x_test, y_test)])
  
    # 组合训练集和测试集
    train_data = torch.stack([x_train, y_train], dim=1)
    test_data = torch.stack([x_test, y_test], dim=1)
  
    return train_data, train_colors, test_data, test_colors

定义了一个生成训练集和测试集的方法,其实在KNN中,我们并没有显式的训练过程,而是直接依赖于我们的样本空间,不过鉴于习惯,还是将样本空间命名为数据集。

train_data, train_labels, test_data, test_labels = generate_data(a, b)

接下来我们调用这个方法,传入我们斜率和截距,进行数据的生成。

#上文调用函数的a和b分别是斜率和截距
a = 1.14  # 斜率
b = -2.713   # 截距

绘制图像

在初学分类算法、回归算法时,数据的可视化可以极大程度上助于我们更好的学习和理解这些算法,那么我们自然要使用强大的 Matplotlib 进行图像绘制**。**

def plot_data(a, b, data, labels, show_line=True):
    #设置图表宽和高
    plt.figure(figsize=(10, 8))
  
    # 绘制数据点
    #布尔索引操作:选择所有标签为1(即直线上方)的点(通过索引对应选取)
    red_points = data[labels == 1]
    blue_points = data[labels == 0]
  
    # red_points[:, 0] 取所有行的第一列,即所有红色点的x坐标,结果是一个一维数组
    plt.scatter(red_points[:, 0], red_points[:, 1], color='red', alpha=0.55, label='Above Line')
    plt.scatter(blue_points[:, 0], blue_points[:, 1], color='blue', alpha=0.55, label='Below Line')
  
    # 绘制答案直线
    if show_line:
        #通过torch.linspace在区间[-10,10]直接平分出100个x点,带入计算得到plot所需的x,y点集
        #因为我们生成的训练集就是[-10,10]范围的,所以我们的图当然也在这个范围
        x_line = torch.linspace(-10, 10, 100)
        y_line = a * x_line + b
        plt.plot(x_line, y_line, 'g-', linewidth=3, label=f'Answer: y={a}x+{b}')
  
    plt.title(f"Data Points ({'with' if show_line else 'without'} Answer Line)")
    plt.xlabel('X')
    plt.ylabel('Y')
    plt.xlim(-10, 10)
    plt.ylim(-10, 10)
    plt.grid(True, linestyle='--', alpha=0.7)
    plt.legend()
    plt.show()

接下来我们调用这个方法,进行图像的绘制。注意到我在定义这个方法时设置了一个参数 show_line ,这个参数可以决定我们绘制图像的时候是否绘制分割线。

plot_data(a, b, train_data, train_labels, show_line=False)

image-vrwa.png

plot_data(a, b, train_data, train_labels, show_line=True)

image-asdx.png

KNN分类机的创建

到这里就是我们的重头戏了,创建一个KNN分类器。

这里我用到了python的面向对象,这有助于我们创建出一个标准化的易于维护和改造的KNN分类器。

class KNNClassifier:
    def __init__(self, k=5):
        self.k = k
        self.X_train = None
        self.Y_train = None
  
    def fit(self, X, Y):
        """存储训练数据"""
        self.X_train = X #点的坐标张量
        self.Y_train = Y #点的标签(0或1)张量
  
    def predict(self, X):
        """预测输入数据的类别"""
        predictions = []
  
        # 转换为张量以确保兼容性
        if not torch.is_tensor(X):
            X = torch.tensor(X, dtype=torch.float32)
  
        # 批量计算距离以提高效率
        for test_point in X:
            # 计算欧氏距离
            #distances = torch.sqrt(torch.sum((self.X_train - test_point)**2, dim=1))
            # 计算曼哈顿距离
            distances = torch.sum(torch.abs(self.X_train - test_point),dim=1)
      
            '''KNN算法的核心步骤 找到最近的k个邻居'''
            # 获取最近的k个样本(largest=False: 表示选择最小而非最大的值)
            # 第一个返回值: 最小的k个距离值(用_忽略)
            _, indexes = torch.topk(distances, self.k, largest=False)
            k_nearest_labels = self.Y_train[indexes]
      
            # 统计整数张量中每个值出现的次数,投票决定类别
            counts = torch.bincount(k_nearest_labels)
            # 用torch.argmax返回张量中最大值所在的索引,item()将单元素张量转换为Python标量
            predictions.append(torch.argmax(counts).item())
  
        return torch.tensor(predictions)

这里我们用到了PyTorch提供的非常好用的方法

_, indexes = torch.topk(distances, self.k, largest=False)
counts = torch.bincount(k_nearest_labels)
predictions.append(torch.argmax(counts).item())

注意到这三行出现了三个新面孔,分别是torch.topk()torch.bincount()torch.argmax()

我们可以通过IDE自带的查询功能来阅读这三个方法的作用。

image-bdgq.png

比如 torch.topk() 就可以在传入的张量中挑选出最大或者最小个k个,然后把数据和索引返回给我们。

torch.bincount() 函数用于统计离散值的出现次数,而 torch.argmax() 可以把最大值的索引返回给我们.

在我们构建好KNN模型后,就可以开始使用并且评估了

def train_and_evaluate(K, train_data, train_labels, test_data, test_labels):
    """训练KNN模型并评估性能"""
    # 创建并训练KNN分类器
    knn = KNNClassifier(k=K)
    knn.fit(train_data, train_labels)
  
    # 预测测试集
    start_time = time.time()
    test_preds = knn.predict(test_data)
    inference_time = time.time() - start_time
  
    # 计算准确率
    if test_labels.device != test_preds.device:
        test_preds = test_preds.to(test_labels.device)
    accuracy = (test_labels == test_preds).float().mean().item()
    print(f"K={K} | 测试准确率: {accuracy:.4f} | 推理时间: {inference_time:.4f}秒")
  
    return knn, test_preds, accuracy
k_values = [1,2,3,4,5,7,10,13,15, 20,25]
results = {}

for k in k_values:
    knn, test_preds, accuracy = train_and_evaluate(k, train_data, train_labels, test_data, test_labels)
    results[k] = {
        'model': knn,
        'predictions': test_preds,
        'accuracy': accuracy
    }

根据输出结果来看

image-rsez.png

其实不太好看

那么怎么办?

当然还是使用 Matplotlib 了!

plt.figure(figsize=(10, 6))
accuracies = [results[k]['accuracy'] for k in k_values]
plt.plot(k_values, accuracies, 'bo-', linewidth=2, markersize=8)
plt.title('KNN')
plt.xlabel('K')
plt.ylabel('Accuracy')
plt.grid(True, linestyle='--', alpha=0.7)
plt.xticks(k_values)
plt.show()

image-phgr.png

所以我们可以得到在K=7时候有较好结果,而K>20的时候又会出现更好的结果,但是这并不意味着K越大越好!在考虑到计算量大小,数据集大小等因素的情况下,我们可以说这里K=7就是理想情况了,而且对于别的数据分类或回归时,可能并不会出现多个峰值。

决策边界

def plot_decision_boundary(a, b, knn, data, labels):
    """可视化决策边界"""
    plt.figure(figsize=(12, 10))
  
    # 绘制数据点
    red_points = data[labels == 1]
    blue_points = data[labels == 0]
  
    plt.scatter(red_points[:, 0], red_points[:, 1], color='red', alpha=0.5, label='Above Line')
    plt.scatter(blue_points[:, 0], blue_points[:, 1], color='blue', alpha=0.5, label='Below Line')
  
    # 绘制答案直线
    x_line = torch.linspace(-10, 10, 100)
    y_line = a * x_line + b
    plt.plot(x_line, y_line, 'g-', linewidth=3, label=f'Answer: y={a}x+{b}')
  
    # 绘制KNN决策边界
    resolution = 100
    xx, yy = torch.meshgrid(torch.linspace(-10, 10, resolution), 
                           torch.linspace(-10, 10, resolution))
    grid_points = torch.stack((xx.flatten(), yy.flatten()), dim=1)
  
    # 预测网格点的类别
    start_time = time.time()
    grid_preds = knn.predict(grid_points)
    print(f"决策边界计算时间: {time.time() - start_time:.2f}秒")
  
    # 创建决策边界图
    grid_preds = grid_preds.reshape(xx.shape)
    plt.contourf(xx, yy, grid_preds, alpha=0.2, levels=[-0.5, 0.5, 1.5], 
                colors=['blue', 'red'])
  
    # 绘制KNN近似的决策边界
    boundary_line = []
    for x in torch.linspace(-10, 10, 50):
        # 创建垂直线上的点
        y_values = torch.linspace(-10, 10, 100)
        test_points = torch.stack([torch.full_like(y_values, x), y_values], dim=1)
  
        # 预测这些点的类别
        preds = knn.predict(test_points)
  
        # 找到决策边界的位置
        diff = torch.abs(torch.diff(preds.float()))
        if torch.any(diff > 0):
            idx = torch.argmax(diff).item()
            y_boundary = (y_values[idx] + y_values[idx+1]) / 2
            boundary_line.append([x.item(), y_boundary.item()])
  
    if boundary_line:
        boundary_line = np.array(boundary_line)
        plt.plot(boundary_line[:, 0], boundary_line[:, 1], 'm--', linewidth=3, 
                label=f'KNN Boundary (k={knn.k})')
  
    plt.title(f"KNN Decision Boundary (k={knn.k})")
    plt.xlabel('X')
    plt.ylabel('Y')
    plt.xlim(-10, 10)
    plt.ylim(-10, 10)
    plt.grid(True, linestyle='--', alpha=0.3)
    plt.legend()
    plt.show()

image-xojf.png

测试集结果可视化

# 可视化测试集结果
plt.figure(figsize=(10, 8))

# 绘制测试点,使用不同形状表示预测是否正确
correct_indices = (test_preds == test_labels)
incorrect_indices = ~correct_indices

# 正确分类的点
correct_points = test_data[correct_indices]
correct_labels = test_labels[correct_indices]
correct_red = correct_points[correct_labels == 1]
correct_blue = correct_points[correct_labels == 0]

# 错误分类的点
incorrect_points = test_data[incorrect_indices]
incorrect_labels = test_labels[incorrect_indices]
incorrect_red = incorrect_points[incorrect_labels == 1]  # 实际是红色但被错分
incorrect_blue = incorrect_points[incorrect_labels == 0]  # 实际是蓝色但被错分

# 绘制正确分类的点
plt.scatter(correct_red[:, 0], correct_red[:, 1], color='red', marker='o', alpha=0.7, label='Correct: Above')
plt.scatter(correct_blue[:, 0], correct_blue[:, 1], color='blue', marker='o', alpha=0.7, label='Correct: Below')

# 绘制错误分类的点
plt.scatter(incorrect_red[:, 0], incorrect_red[:, 1], color='red', marker='x', s=100, linewidth=2, label='Misclassified: Above')
plt.scatter(incorrect_blue[:, 0], incorrect_blue[:, 1], color='blue', marker='x', s=100, linewidth=2, label='Misclassified: Below')

# 绘制答案直线
x_line = torch.linspace(-10, 10, 100)
y_line = a * x_line + b
plt.plot(x_line, y_line, 'g-', linewidth=3, label=f'Answer: y={a}x+{b}')

plt.xlabel('X')
plt.ylabel('Y')
plt.xlim(-10, 10)
plt.ylim(-10, 10)
plt.grid(True, linestyle='--', alpha=0.3)
plt.legend()
plt.show()

image-rkle.png

为梦想创造现实!