18.AI大模型开发:机器学习----KNN算法

写在前面

今天咱们开始学第一个真正意义上的分类算法------KNN(K近邻算法)

KNN大概是所有机器学习算法里最直观、最好理解的一个,不需要复杂的数学推导,核心思想就是"物以类聚,人以群分"。

全程不用课件里的电影分类案例 ,我换一个更贴近生活的场景------"相亲匹配预测",帮你看懂KNN到底在干什么。

第一部分:KNN算法的核心思想

1.1 先讲个故事:相亲角的大爷大妈

你去人民公园的相亲角,想帮朋友找个对象。朋友的条件是:28岁、身高178cm、程序员、年收入35万

你该怎么判断"这个人大概是什么类型"?

聪明的做法是:找已经配好对的样本------看看这个圈子里的历史数据,和这个条件最像的几个人,最后都找了什么类型的对象。如果离得最近的5个人里,有4个找了"温柔顾家型",1个找了"事业拼搏型",那你可以大胆预测:这个人大概率也适合"温柔顾家型"。

这就是KNN的思想。

1.2 KNN的官方定义(翻译成人话版)

KNN算法 :如果一个样本在特征空间中的 K个 最相似的样本中的大多数属于某一个类别,那么这个样本也属于这个类别。

一句话总结:你邻居是啥样,你就是啥样。

1.3 KNN能解决什么问题?

问题类型 说明 举例
分类问题 预测"是哪一类" 预测相亲结果是"成"还是"不成"
回归问题 预测"是多少" 预测约会后"感情评分"是几分(连续值)

KNN是少数既能做分类又能做回归的算法,这点很硬核。


第二部分:KNN的"相似性"怎么算?(核心公式)

KNN里最重要的一步就是判断"谁和我最像" 。怎么判断?算距离。距离越近,越相似。

2.1 欧氏距离(最常用的距离公式)

这就是你初中就学过的两点间距离公式,只不过从二维推广到了多维。

二维空间(两个特征)

d=(x1​−x2​)2+(y1​−y2​)2​

三维空间(三个特征)

d=(x1​−x2​)2+(y1​−y2​)2+(z1​−z2​)2​

N维空间(N个特征,这是最通用的版本)

d=k=1∑n​(x1k​−x2k​)2​

2.2 拿"相亲匹配"算一遍距离

假设我们把相亲对象用3个特征表示:

对象 年龄 年收入(万) 身高(cm) 匹配结果
A 26 20 175 成功
B 30 50 180 成功
C 28 15 170 失败
D 25 60 185 成功
E 35 25 172 失败
新来的X 28 35 178

现在我们要判断 X 属于"成功"还是"失败"。

第一步:计算 X 到每一个已知样本的距离

以 X 到 A 为例(三维欧氏距离):

d=(28−26)2+(35−20)2+(178−175)2=4+225+9=238≈15.43dXA​=(28−26)2+(35−20)2+(178−175)2​=4+225+9​=238​≈15.43

同样的方法算出 X 到 B、C、D、E 的距离。

第二步:按距离从小到大排序

排名 对象 距离 匹配结果
1 B ? 成功
2 D ? 成功
3 A ? 成功
4 E ? 失败
5 C ? 失败

第三步:选K个最近的邻居,投票决定

假设 K=3,取前3个最近的:B(成功)、D(成功)、A(成功)→ 3票成功,0票失败。

结论 :KNN预测 X 的匹配结果是 "成功"


第三部分:KNN的分类流程和回归流程(完整版)

3.1 分类流程(5步走)

  1. 计算距离:计算未知样本到每一个训练样本的距离(欧氏距离)

  2. 排序:将训练样本按距离从小到大排序

  3. 取K个:取出距离最近的 K 个训练样本

  4. 投票表决:统计这 K 个样本中,哪个类别出现次数最多

  5. 得出结论:将未知样本归到出现次数最多的类别

3.2 回归流程(5步走)

  1. 计算距离:计算未知样本到每一个训练样本的距离

  2. 排序:将训练样本按距离从小到大排序

  3. 取K个:取出距离最近的 K 个训练样本

  4. 取平均值:把这 K 个样本的目标值(标签)求平均

  5. 得出结论:这个平均值就是未知样本的预测值

分类 vs 回归:分类是"少数服从多数"投票,回归是"邻居取平均"。


第四部分:K值怎么选?(决定模型生死的关键)

K值选得好,模型就好;K值选不好,模型就废。

4.1 K值过小(比如 K=1)

  • 只找最近的一个邻居

  • 风险:如果那个邻居是异常点(噪音),你就跟着跑偏了

  • 后果 :模型变得复杂,容易过拟合

4.2 K值过大(比如 K=N,N是全部样本数)

  • 所有邻居都算上,看全体的意见

  • 风险:如果数据里"成功"占多数,永远预测"成功",忽略了局部信息

  • 后果 :模型变得太简单,容易欠拟合

4.3 实际工作中怎么选K?

  • 一般 K取较小的值(3、5、7、9、11等奇数),避免平票

  • 交叉验证来确定最优K值(后面会细讲)


第五部分:手写KNN代码(从0开始)

光说不练假把式。咱们用纯Python手写一个KNN分类器,方便你理解底层原理。

python 复制代码
import numpy as np
from collections import Counter

class KNNClassifier:
    """手写一个KNN分类器,帮你理解底层原理"""
    
    def __init__(self, k=5):
        self.k = k
        self.X_train = None
        self.y_train = None
    
    def fit(self, X, y):
        """训练:其实就是把数据存下来"""
        self.X_train = np.array(X)
        self.y_train = np.array(y)
    
    def _euclidean_distance(self, x1, x2):
        """计算欧氏距离:√(∑(x1-x2)²)"""
        return np.sqrt(np.sum((x1 - x2) ** 2))
    
    def predict_one(self, x):
        """预测单个样本的分类"""
        # 1. 计算x到所有训练样本的距离
        distances = [self._euclidean_distance(x, x_train) for x_train in self.X_train]
        
        # 2. 按距离从小到大排序,取前k个的索引
        k_indices = np.argsort(distances)[:self.k]
        
        # 3. 取这k个样本的标签
        k_labels = [self.y_train[i] for i in k_indices]
        
        # 4. 投票表决:多数取胜
        most_common = Counter(k_labels).most_common(1)[0][0]
        return most_common
    
    def predict(self, X):
        """批量预测"""
        return [self.predict_one(x) for x in np.array(X)]


# ====== 测试我们的手写KNN ======

# 造一个简单的数据集(特征:年龄,年收入,身高)
X_train = [[26, 20, 175], [30, 50, 180], [28, 15, 170], [25, 60, 185], [35, 25, 172]]
y_train = ['成功', '成功', '失败', '成功', '失败']

# 新来的一个相亲对象
X_test = [[28, 35, 178]]

# 用K=3训练和预测
knn = KNNClassifier(k=3)
knn.fit(X_train, y_train)
result = knn.predict(X_test)

print(f"预测结果:{result[0]}")  # 输出:成功

这段代码不依赖任何第三方库,完完整整展示了KNN的5个步骤。你先把它跑通,体会一下"距离→排序→取K个→投票"的逻辑。


第六部分:KNN算法的API用法

实际开发中你不需要手写,直接用 sklearn 库,方便太多了。

6.1 KNN分类(KNeighborsClassifier)

python 复制代码
from sklearn.neighbors import KNeighborsClassifier

# 1. 准备数据
X = [[0], [1], [2], [3]]    # 特征(比如:学习时长)
y = [0, 0, 1, 1]            # 标签(0=未通过,1=通过)

# 2. 创建模型(K=1)
model = KNeighborsClassifier(n_neighbors=1)

# 3. 训练
model.fit(X, y)

# 4. 预测
pred = model.predict([[4]])   # 学习4小时,能通过吗?
print(pred)  # 输出:[1](通过)

6.2 KNN回归(KNeighborsRegressor)

python 复制代码
from sklearn.neighbors import KNeighborsRegressor

# 1. 准备数据(特征3个,标签是连续值)
X = [[0, 0, 1], [1, 1, 0], [3, 10, 10], [4, 11, 12]]
y = [0.1, 0.2, 0.3, 0.4]    # 连续值

# 2. 创建模型(K=2)
model = KNeighborsRegressor(n_neighbors=2)

# 3. 训练
model.fit(X, y)

# 4. 预测
pred = model.predict([[3, 11, 10]])
print(pred)  # 输出:取两个最近邻居的平均值

6.3 分类和回归的API对比总结

任务类型 API K值参数
分类 KNeighborsClassifier(n_neighbors=5) n_neighbors
回归 KNeighborsRegressor(n_neighbors=5) n_neighbors

第七部分:特征预处理(归一化 vs 标准化)

7.1 为什么需要特征预处理?

假设你判断一个人"健康风险",用两个特征:

  • 身高:1.6m ~ 1.9m

  • 年收入:5万 ~ 100万

年收入的数值范围(5~100)比身高(1.6~1.9)大得多。在计算欧氏距离时,年收入会"霸凌"身高------距离几乎由年收入决定,身高的影响被淹没了。

解决方法:把所有特征都"压缩"到差不多的数值范围内。

7.2 归一化(MinMaxScaler)

公式

X′=(x-min)/(max-min)

把数据映射到 0, 1 区间。

python 复制代码
from sklearn.preprocessing import MinMaxScaler

data = [[90, 2, 10, 40], [60, 4, 15, 45], [75, 3, 13, 46]]
scaler = MinMaxScaler()
data_scaled = scaler.fit_transform(data)
print(data_scaled)

缺点 :如果数据里有异常值(比如某个特征有个1000),最大值被拉大,其他所有值都被压到特别小。鲁棒性差

7.3 标准化(StandardScaler)------ 推荐使用

公式

X′=(x−mean)/标准差​

把数据转换成 均值为0,标准差为1 的标准正态分布。

复制代码
from sklearn.preprocessing import StandardScaler
python 复制代码
data = [[90, 2, 10, 40], [60, 4, 15, 45], [75, 3, 13, 46]]
scaler = StandardScaler()
data_scaled = scaler.fit_transform(data)
print(data_scaled)
print("均值:", scaler.mean_)
print("方差:", scaler.var_)

优点 :即使有异常值,因为数据量足够大,对均值和标准差的影响也很有限。鲁棒性强

7.4 归一化 vs 标准化 怎么选?

对比维度 归一化 标准化
鲁棒性(抗异常值) ❌ 差 ✅ 好
适用场景 传统精确小数据 现代嘈杂大数据(推荐)
是否改变数据分布 会压缩成0,1 变成标准正态分布

实战建议无脑选标准化(StandardScaler),除非你有特殊理由。

7.5 小练习

课件里有个填空,我帮你填上:

归一化度量值容易受到样本中最大值和最小值的影响,鲁棒性差。

样本数量较大的情况下,异常值对样本的均值和标准差的影响可以忽略不计。


第八部分:完整实战------鸢尾花分类(KNN标准流程)

终于到了一个完整的机器学习项目了!咱们用KNN做鸢尾花分类,走一遍完整建模流程

8.1 数据长什么样?

鸢尾花数据集有150个样本,3个品种(山鸢尾、变色鸢尾、维吉尼亚鸢尾),每个样本有4个特征:

  • 花萼长度、花萼宽度、花瓣长度、花瓣宽度

标签是花的品种(0、1、2)。

8.2 代码实现

python 复制代码
# 导入所有需要的库
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import accuracy_score

# ===== 第1步:获取数据 =====
iris = load_iris()
print(f"特征数量: {iris.data.shape[1]}, 样本数量: {iris.data.shape[0]}")
print(f"目标类别: {iris.target_names}")  # ['setosa' 'versicolor' 'virginica']

# ===== 第2步:数据基本处理(划分训练集和测试集) =====
X_train, X_test, y_train, y_test = train_test_split(
    iris.data, iris.target, test_size=0.2, random_state=22
)
print(f"训练集: {len(X_train)}个, 测试集: {len(X_test)}个")

# ===== 第3步:特征工程 ------ 标准化(重要!) =====
scaler = StandardScaler()
# 用训练集拟合scaler,然后转换训练集
X_train_scaled = scaler.fit_transform(X_train)
# 用同样的scaler转换测试集(注意:这里只用transform,不用fit)
X_test_scaled = scaler.transform(X_test)

# ===== 第4步:机器学习(模型训练) =====
# 创建KNN分类器,K=3
model = KNeighborsClassifier(n_neighbors=3)
model.fit(X_train_scaled, y_train)

# ===== 第5步:模型评估 =====
# 方法1:直接用score方法算准确率
accuracy = model.score(X_test_scaled, y_test)
print(f"模型准确率: {accuracy * 100:.2f}%")

# 方法2:先预测再算准确率(结果一样)
y_pred = model.predict(X_test_scaled)
acc = accuracy_score(y_test, y_pred)
print(f"准确率(第二种方法): {acc * 100:.2f}%")

# ===== 第6步:模型预测(新数据) =====
# 假设来了两朵新花,测量了4个特征值
new_flowers = [[5.1, 3.5, 1.4, 0.2], [6.7, 3.0, 5.2, 2.3]]
# 注意:新数据也要做同样的标准化处理!
new_flowers_scaled = scaler.transform(new_flowers)

pred = model.predict(new_flowers_scaled)
print(f"预测类别: {pred}")  # 输出:[0 2] 分别对应山鸢尾和维吉尼亚鸢尾

# 看预测概率(每个类别的概率)
prob = model.predict_proba(new_flowers_scaled)
print(f"预测概率: {prob}")

8.3 运行结果分析

python 复制代码
特征数量: 4, 样本数量: 150
目标类别: ['setosa' 'versicolor' 'virginica']
训练集: 120个, 测试集: 30个
模型准确率: 100.00%
准确率(第二种方法): 100.00%
预测类别: [0 2]
预测概率: [[1.  0.  0. ] [0.  0.  1. ]]

第一个新花100%概率是类别0(山鸢尾),第二个100%概率是类别2(维吉尼亚鸢尾)。


第九部分:交叉验证(Cross Validation)------ 模型评估更靠谱

9.1 为什么需要交叉验证?

你之前是一次性划分训练集和测试集,这样有个风险:如果这次划分恰好把难的数据都分到了测试集,评估结果就偏低;如果恰好把简单的都分到了测试集,评估结果就虚高。

交叉验证:把数据分成N份,每份轮流做一次测试集,其他N-1份做训练集,训练N次,取平均分。

9.2 交叉验证的流程

5折交叉验证 为例:

  • 把训练集分成5份(每份叫一折)

  • 第1次:第1份做验证集,其他4份做训练集 → 得一个分数

  • 第2次:第2份做验证集,其他4份做训练集 → 得一个分数

  • ...以此类推,共5次

  • 取5次得分的平均值作为最终分数

好处:模型评分更稳定、更可信,不容易被"运气好的一次划分"忽悠。


第十部分:网格搜索(Grid Search)------ 自动找最优K值

10.1 为什么需要网格搜索?

KNN里的 K值(邻居个数)是超参数 ------它不是从数据里学出来的,是你人为设定的

你可能会想:K=3好还是K=5好?K=7会不会更好?一个个手动试太累了。

网格搜索:你告诉它"我要试哪些K值",它自动帮你全部试完,交叉验证打分,返回最好的那个。

10.2 网格搜索 + 交叉验证 代码

python 复制代码
from sklearn.model_selection import GridSearchCV

# ===== 准备数据(和之前一样) =====
iris = load_iris()
X_train, X_test, y_train, y_test = train_test_split(
    iris.data, iris.target, test_size=0.2, random_state=22
)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

# ===== 创建KNN模型 =====
knn = KNeighborsClassifier()

# ===== 设置要搜索的超参数列表 =====
param_grid = {'n_neighbors': [1, 3, 5, 7, 9, 11]}

# ===== 网格搜索 + 5折交叉验证 =====
grid_search = GridSearchCV(
    estimator=knn,           # 要调参的模型
    param_grid=param_grid,   # 要搜索的参数组合
    cv=5,                    # 5折交叉验证
    n_jobs=-1                # 用所有CPU核加速
)
grid_search.fit(X_train_scaled, y_train)

# ===== 查看结果 =====
print(f"最佳K值: {grid_search.best_params_}")
print(f"交叉验证最佳得分: {grid_search.best_score_:.4f}")
print(f"最优模型: {grid_search.best_estimator_}")
print(f"测试集准确率: {grid_search.score(X_test_scaled, y_test):.4f}")

# ===== 查看所有搜索结果(转成DataFrame方便看) =====
import pandas as pd
cv_results = pd.DataFrame(grid_search.cv_results_)
print(cv_results[['param_n_neighbors', 'mean_test_score', 'std_test_score']])

10.3 运行结果

python 复制代码
最佳K值: {'n_neighbors': 3}
交叉验证最佳得分: 0.9667
最优模型: KNeighborsClassifier(n_neighbors=3)
测试集准确率: 1.0000

   param_n_neighbors  mean_test_score  std_test_score
0                  1          0.966667        0.033333
1                  3          0.966667        0.033333
2                  5          0.958333        0.030000
3                  7          0.958333        0.030000
4                  9          0.958333        0.030000
5                 11          0.958333        0.030000

可以看到,K=1和K=3在交叉验证上得分相同,但K=3可能更稳健(不易受异常值影响),所以选K=3。


第十一部分:分类评估方法(混淆矩阵、精确率、召回率、F1)

11.1 为什么只看准确率不够?

课件里用了一个特别好的例子:癌症检测

假设10000个人里只有10个癌症患者(正例)。如果一个模型把所有10000个人都预测为"健康",它的准确率是:

999010000=99.9%100009990​=99.9%

准确率99.9%!看起来牛逼炸了,但实际上一个癌症患者都没检测出来,这模型就是个废物。

所以,对于类别不平衡的数据,需要更多指标来评估。

11.2 混淆矩阵(Confusion Matrix)

混淆矩阵是一个 2x2 的表格,帮我们看清楚"预测对了多少,错在哪"。

二分类问题(正例 = 癌症患者,反例 = 健康人):

预测为正例 预测为反例
真实为正例 TP(真正例) 正确识别患者 FN(假反例) 漏诊了患者
真实为反例 FP(假正例) 误把健康人当患者 TN(真反例) 正确识别健康人
  • TP(True Positive):真实是正例,预测也是正例 ✅

  • FN(False Negative):真实是正例,预测是反例 ❌(漏诊,最可怕)

  • FP(False Positive):真实是反例,预测是正例 ❌(误诊)

  • TN(True Negative):真实是反例,预测也是反例 ✅

11.3 精确率(Precision)------ 查准率

精确率=TP+FPTP​

翻译 :模型说"你是癌症患者"的人里面,真正是患者的比例。

如果你不想把健康人误诊为癌症(误诊造成恐慌),你就追求高精确率

11.4 召回率(Recall)------ 查全率 / 敏感度

召回率=TP+FNTP​

翻译 :所有真正的癌症患者里面,模型找出了多少比例

如果你不想漏掉任何一个癌症患者(漏诊耽误病情),你就追求高召回率

11.5 精确率 vs 召回率 ------ 一个经典的矛盾

模型 TP FN FP TN 精确率 召回率
模型A(保守) 3 3 0 4 100% 50%
模型B(激进) 6 0 3 1 67% 100%
  • 模型A:宁可少预测(只预测了3个阳性),但预测的都对(精确率100%)。缺点是漏掉了3个患者(召回率低)。

  • 模型B:把所有10个样本都预测为阳性,6个真正的患者全找到了(召回率100%)。缺点是误伤了4个健康人(精确率低)。

没有绝对的好坏 ,看业务需求。癌症筛查宁可误诊也不能漏诊,所以优先保证召回率

11.6 F1-score ------ 精确率和召回率的"调和平均"

F1=精确率+召回率2×精确率×召回率​

  • F1-score 是一个综合指标,同时考虑了精确率和召回率。

  • 如果精确率和召回率都很高,F1才高;任何一个拉胯,F1就掉下来了。

模型A(精确率100%,召回率50%)

F1=1.0+0.52×1.0×0.5​=1.51.0​≈0.67

模型B(精确率67%,召回率100%)

F1=0.67+1.02×0.67×1.0​=1.671.34​≈0.80

模型B的F1更高,说明综合考虑下模型B更好。

11.7 代码:算混淆矩阵、精确率、召回率、F1

python 复制代码
from sklearn.metrics import confusion_matrix, precision_score, recall_score, f1_score

# 真实的10个样本标签(6个恶性=正例,4个良性=反例)
y_true = ['恶性', '恶性', '恶性', '恶性', '恶性', '恶性', 
          '良性', '良性', '良性', '良性']

# 模型A的预测(对了3个恶性,4个良性)
y_pred_A = ['恶性', '恶性', '恶性', '良性', '良性', '良性', 
            '良性', '良性', '良性', '良性']

print("========== 模型A ==========")
print("混淆矩阵:")
print(confusion_matrix(y_true, y_pred_A, labels=['恶性', '良性']))
print(f"精确率: {precision_score(y_true, y_pred_A, pos_label='恶性'):.2f}")
print(f"召回率: {recall_score(y_true, y_pred_A, pos_label='恶性'):.2f}")
print(f"F1-score: {f1_score(y_true, y_pred_A, pos_label='恶性'):.2f}")

# 模型B的预测(对了6个恶性,1个良性)
y_pred_B = ['恶性', '恶性', '恶性', '恶性', '恶性', '恶性', 
            '恶性', '恶性', '良性', '良性']

print("\n========== 模型B ==========")
print("混淆矩阵:")
print(confusion_matrix(y_true, y_pred_B, labels=['恶性', '良性']))
print(f"精确率: {precision_score(y_true, y_pred_B, pos_label='恶性'):.2f}")
print(f"召回率: {recall_score(y_true, y_pred_B, pos_label='恶性'):.2f}")
print(f"F1-score: {f1_score(y_true, y_pred_B, pos_label='恶性'):.2f}")

输出

python 复制代码
========== 模型A ==========
混淆矩阵:
[[3 3]
 [0 4]]
精确率: 1.00
召回率: 0.50
F1-score: 0.67

========== 模型B ==========
混淆矩阵:
[[6 0]
 [3 1]]
精确率: 0.67
召回率: 1.00
F1-score: 0.80

第十二部分:总结

模块 核心内容
算法思想 "近朱者赤,近墨者黑"------看最近的K个邻居
相似度度量 欧氏距离 d=∑(x1​−x2​)²
K值 太小过拟合,太大欠拟合,用交叉验证选最优
分类 K个邻居投票,多数取胜
回归 K个邻居取平均
特征预处理 必须做标准化!让特征在同一尺度
交叉验证 把数据分N份,轮流做验证集,取平均分
网格搜索 自动尝试多组超参数,返回最优组合
混淆矩阵 TP、FN、FP、TN四个指标看清模型
精确率 预测为正例里面真正为正例的比例
召回率 真正为正例里面被预测出来的比例
F1-score 精确率和召回率的调和平均,综合评价

KNN的代码写起来就几行,但背后的逻辑------距离怎么算、K怎么选、特征要不要标准化、怎么评估模型好不好------这些才是真正的功力。今天把这些问题都搞懂了,以后学其他算法就会轻松很多。明天咱们继续!