从零掌握k近邻算法:原理、手写实现与scikit-learn实战
1. 项目概述:从零到一掌握k近邻算法
如果你刚开始接触机器学习,面对一堆听起来高大上的算法名字感到无从下手,那我建议你从k近邻(k-Nearest Neighbors,简称kNN)开始。它不像神经网络那样像个黑盒子,也不像支持向量机那样有复杂的数学推导。kNN的核心思想简单到可以用一句话概括:物以类聚,人以群分。判断一个新样本的类别,就看它在特征空间里离哪些已知样本最近,然后“少数服从多数”。这种直觉式的逻辑,让kNN成为了机器学习入门的最佳拍档,也是你工具箱里不可或缺的“瑞士军刀”——简单、直观,且在特定场景下异常有效。
Python作为数据科学和机器学习领域的事实标准语言,为我们提供了绝佳的实现环境。尤其是NumPy和scikit-learn这两个库,一个负责底层高效的数值计算,一个提供开箱即用的高级接口。学习kNN,不仅仅是调用
sklearn.neighbors.KNeighborsClassifier
那一行代码,更重要的是理解数据如何被表示、距离如何被计算、以及“k”这个关键参数背后的权衡。本篇文章将带你走完这个完整的旅程:从算法原理的直观理解与数学拆解,到手写NumPy实现以夯实基础,最后到利用scikit-learn进行高效实战。无论你是想彻底搞懂一个经典算法,还是急需一个分类或回归工具来解决手头问题,这里都有你需要的答案。
2. kNN算法核心思想与特性拆解
2.1 “物以类聚”的直觉化理解
让我们暂时忘掉所有数学公式。想象一下,你搬进了一个新小区,想知道这个小区整体上是否安静。你会怎么做?最直接的办法,可能就是去问问你的左邻右舍。如果隔壁、对门、楼上的邻居都说晚上很安静,那么你大概率可以推断这个小区环境不错。kNN算法做的就是这件事,只不过它处理的是数据。
在机器学习中,每个数据点(比如一条用户记录、一张图片、一段音频)都被转换为一组特征(Feature),这组特征定义了该数据点在多维空间中的一个具体位置。kNN假设,在特征空间中相互靠近的点,更可能具有相同的性质或标签。当一个新数据点(我们不知道它的标签)出现时,算法就在这个空间中找到离它最近的k个“老邻居”(训练数据),然后观察这些邻居们大多数是什么身份,并将这个多数派身份赋予新数据点。对于回归问题,则是取这k个邻居标签值的平均值作为预测值。
这种方法的优势极其明显: 无需训练模型 。传统的模型如逻辑回归、决策树,都需要一个“训练”阶段来调整内部参数(如权重、树结构)。而kNN是一种“惰性学习”(Lazy Learning)算法,它没有显式的训练过程,或者说,它的训练过程仅仅是把所有数据“记住”(存储起来)。所有计算都推迟到预测时才发生。这带来了两个直接好处:一是模型永远不需要更新,新数据来了直接存入记忆库即可;二是理论上可以拟合非常复杂的决策边界,因为它的预测完全依赖于局部数据,而非全局假设。
2.2 算法背后的数学:距离度量
直觉需要数学来精确描述。kNN的核心数学概念是“距离”。如何定义两个数据点之间的“近”与“远”?最常用的是
欧几里得距离
,也就是我们中学学的两点间直线距离。对于两个n维特征向量
x = (x1, x2, ..., xn)
和
y = (y1, y2, ..., yn)
,其欧氏距离为:
distance = sqrt((x1 - y1)^2 + (x2 - y2)^2 + ... + (xn - yn)^2)
这个公式非常直观,但在使用前必须注意一个关键步骤: 特征标准化 。如果特征A的取值范围是0-10000(比如工资),而特征B的取值范围是0-1(比如考试得分),那么计算距离时,特征A的微小波动就会完全主导结果,使得特征B的作用被忽略。因此,在实际应用kNN前,通常需要对所有特征进行标准化(如Z-score标准化)或归一化(缩放到[0,1]区间),确保每个特征在距离计算中具有同等的重要性。
除了欧氏距离,还有其他度量方式:
-
曼哈顿距离
:
distance = |x1 - y1| + |x2 - y2| + ... + |xn - yn|。想象在城市棋盘状街道上行走,不能斜穿,只能沿网格线走,这个距离就是曼哈顿距离。它对异常值不如欧氏距离敏感。 - 闵可夫斯基距离 :欧氏距离和曼哈顿距离的泛化形式。当参数p=2时,就是欧氏距离;p=1时,就是曼哈顿距离。
- 余弦相似度 :衡量两个向量方向上的差异,而非绝对距离。在文本分类(如文档TF-IDF向量)和高维稀疏数据中特别有用。
选择哪种距离度量,没有绝对标准,需要根据数据特性和业务场景来定。一个实用的建议是:从欧氏距离开始,如果效果不佳或数据有明确特性(如文本、分类特征),再尝试其他度量。
2.3 关键参数k:偏差与方差的权衡
参数k是kNN中唯一的“超参数”,它的选择直接决定了模型的性格。
- 当k值很小(例如k=1)时 :模型变得非常“敏感”和“复杂”。它只听取最近的一个邻居的意见,这会导致决策边界非常崎岖,能够捕捉到数据中细微的局部模式。但这也意味着模型对噪声数据点异常敏感(容易过拟合),因为一个错误的邻居就会导致预测错误。此时模型具有 低偏差、高方差 的特点。
- 当k值很大(例如k=训练集样本数)时 :模型变得非常“平滑”和“简单”。预测结果将由绝大多数样本决定,这能有效抑制噪声的影响,但也会忽略数据中合理的局部特征。极端情况下,无论输入什么,模型都会预测为整个数据集中最多的那个类别(分类)或全局平均值(回归)。此时模型具有 高偏差、低方差 的特点。
所以,选择k是一个在偏差和方差之间寻找平衡的艺术。k太小,模型不稳定,容易学到噪声;k太大,模型过于粗糙,可能忽略有用信息。通常,k值通过交叉验证来确定。一个经验法则是设置k为样本数量的平方根,但这只是一个粗糙的起点。
注意 :k值通常选择奇数(对于二分类问题),以避免平票情况。对于多分类问题,奇偶性不能完全避免平票,但奇数仍是一个好习惯。
3. 实战准备:从零开始手写kNN
理解了原理,最好的巩固方式就是亲手实现它。我们将使用NumPy,不借助任何现成的机器学习库,从头构建一个kNN分类器。这个过程会让你对算法的每一个细节都了然于胸。
3.1 数据准备与距离计算
我们使用一个经典的鸢尾花数据集作为示例。但为了演示,我们先自己构造一个简单的二维数据集,便于可视化理解。
import numpy as np
import matplotlib.pyplot as plt
# 构造一个简单的模拟数据集:两类点
np.random.seed(42) # 确保结果可复现
# 类别0的数据点,围绕中心(0,0)
class0 = np.random.randn(20, 2) * 0.6 + np.array([0, 0])
# 类别1的数据点,围绕中心(2,2)
class1 = np.random.randn(20, 2) * 0.6 + np.array([2, 2])
X_train = np.vstack([class0, class1]) # 特征矩阵 (40, 2)
y_train = np.array([0]*20 + [1]*20) # 标签向量 (40,)
# 可视化
plt.scatter(class0[:, 0], class0[:, 1], c='blue', label='Class 0')
plt.scatter(class1[:, 0], class1[:, 1], c='red', label='Class 1')
plt.xlabel('Feature 1')
plt.ylabel('Feature 2')
plt.legend()
plt.title('Training Data')
plt.show()
现在,我们来实现kNN类的骨架和核心的距离计算函数。我们将使用欧氏距离。
class KNNFromScratch:
def __init__(self, k=3):
"""
初始化kNN分类器。
参数:
k (int): 考虑的最近邻居数量。
"""
self.k = k
self.X_train = None
self.y_train = None
def fit(self, X, y):
"""
“训练”模型。对于kNN,就是存储训练数据。
参数:
X (np.ndarray): 训练特征,形状 (n_samples, n_features)
y (np.ndarray): 训练标签,形状 (n_samples,)
"""
# 简单起见,这里不做特征标准化。实际应用中务必先标准化!
self.X_train = X
self.y_train = y
return self
def _compute_distances(self, X):
"""
计算输入样本X与所有训练样本之间的欧氏距离。
使用向量化操作避免低效循环。
参数:
X (np.ndarray): 需要预测的样本,形状 (n_queries, n_features)
返回:
distances (np.ndarray): 距离矩阵,形状 (n_queries, n_train_samples)
"""
# 利用公式 (a-b)^2 = a^2 - 2ab + b^2 进行向量化计算
# 计算 X 中每个样本的平方和 (n_queries, 1)
X_sq = np.sum(X**2, axis=1, keepdims=True)
# 计算训练集每个样本的平方和 (1, n_train)
X_train_sq = np.sum(self.X_train**2, axis=1, keepdims=True).T
# 计算点积 -2 * X * X_train.T
dot_product = -2 * np.dot(X, self.X_train.T)
# 计算欧氏距离的平方(避免开方,节省计算,因为距离排序关系不变)
distances_sq = X_sq + dot_product + X_train_sq
# 防止因数值误差导致的极小负数
distances_sq = np.maximum(distances_sq, 0)
return distances_sq
这里有一个重要的
实操心得
:在
_compute_distances
函数中,我们计算的是距离的平方,而不是真正的欧氏距离。因为开方运算
np.sqrt
计算成本较高,而距离平方和距离本身的排序是完全一致的(对于正数,若 a^2 < b^2,则 a < b)。在只需要找出最近邻居而不需要精确距离值的场景下,省略开方步骤可以显著提升计算效率,尤其是在数据量大的时候。
3.2 邻居选取与多数表决预测
有了距离矩阵,下一步就是为每个待预测样本找出k个最近的邻居,并根据这些邻居的标签进行投票。
class KNNFromScratch(KNNFromScratch): # 接上文的类定义
def predict(self, X):
"""
预测输入样本X的类别标签。
参数:
X (np.ndarray): 需要预测的样本,形状 (n_queries, n_features)
返回:
y_pred (np.ndarray): 预测的标签,形状 (n_queries,)
"""
if self.X_train is None:
raise ValueError("Model must be fitted before prediction!")
# 1. 计算距离
distances_sq = self._compute_distances(X) # (n_queries, n_train)
# 2. 为每个查询样本找出k个最近邻居的索引
# argsort沿着最后一个轴(训练样本轴)进行排序,返回的是索引
nearest_indices = np.argsort(distances_sq, axis=1)[:, :self.k] # (n_queries, k)
# 3. 获取这些邻居的标签
nearest_labels = self.y_train[nearest_indices] # (n_queries, k)
# 4. 进行多数投票
y_pred = np.empty(X.shape[0], dtype=self.y_train.dtype)
for i in range(X.shape[0]):
# 使用np.bincount统计每个标签出现的次数,适用于标签为整数的情况
counts = np.bincount(nearest_labels[i])
# argmax返回出现次数最多的标签。如果平票,返回第一个最大值(可通过随机选择改进)
y_pred[i] = np.argmax(counts)
return y_pred
def predict_proba(self, X):
"""
预测输入样本X属于各个类别的概率。
概率 = 邻居中属于该类的数量 / k
参数:
X (np.ndarray): 需要预测的样本,形状 (n_queries, n_features)
返回:
proba (np.ndarray): 概率矩阵,形状 (n_queries, n_classes)
"""
distances_sq = self._compute_distances(X)
nearest_indices = np.argsort(distances_sq, axis=1)[:, :self.k]
nearest_labels = self.y_train[nearest_indices]
n_classes = len(np.unique(self.y_train))
proba = np.zeros((X.shape[0], n_classes))
for i in range(X.shape[0]):
counts = np.bincount(nearest_labels[i], minlength=n_classes)
proba[i] = counts / self.k
return proba
现在,让我们用自己写的类来测试一下:
# 实例化并训练模型
my_knn = KNNFromScratch(k=3)
my_knn.fit(X_train, y_train)
# 创建几个测试点
X_test = np.array([[0.5, 0.5], [1.5, 1.5], [2.5, 2.5]])
print("Test points:", X_test)
print("Predictions:", my_knn.predict(X_test))
print("Prediction Probabilities:\n", my_knn.predict_proba(X_test))
# 可视化决策边界(可选,帮助理解)
def plot_decision_boundary(knn, X, y, title):
x_min, x_max = X[:, 0].min() - 0.5, X[:, 0].max() + 0.5
y_min, y_max = X[:, 1].min() - 0.5, X[:, 1].max() + 0.5
xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.02),
np.arange(y_min, y_max, 0.02))
Z = knn.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
plt.contourf(xx, yy, Z, alpha=0.4, cmap=plt.cm.RdYlBu)
plt.scatter(X[:, 0], X[:, 1], c=y, edgecolors='k', cmap=plt.cm.RdYlBu)
plt.title(title)
plt.show()
plot_decision_boundary(my_knn, X_train, y_train, f'Decision Boundary (k={my_knn.k})')
通过手写实现,你不仅理解了算法流程,更深刻体会到了向量化计算对于性能的重要性,以及k值如何影响决策边界的平滑程度。你可以尝试修改k值(比如改为1或10),重新运行可视化代码,直观感受决策边界从崎岖到平滑的变化。
4. 工业级实践:拥抱scikit-learn
虽然手写实现有助于学习,但在实际项目中,我们几乎总是使用经过高度优化和严格测试的库,比如scikit-learn。它能让我们用极少的代码实现强大的功能,并集成到完整的数据流水线中。
4.1 数据加载、预处理与模型训练
我们使用真实的鸢尾花数据集,并展示一个标准的工作流程。
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 classification_report, confusion_matrix, accuracy_score
import seaborn as sns
# 1. 加载数据
iris = load_iris()
X, y = iris.data, iris.target
print(f"Dataset shape: {X.shape}")
print(f"Target names: {iris.target_names}")
print(f"Feature names: {iris.feature_names}")
# 2. 划分训练集和测试集
# 永远不要在用于训练的数据上评估模型,这会导致过于乐观的估计。
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42, stratify=y)
print(f"Training set size: {X_train.shape}, Test set size: {X_test.shape}")
# 3. 特征标准化 (至关重要!)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train) # 在训练集上拟合scaler,并转换训练集
X_test_scaled = scaler.transform(X_test) # 使用训练集的scaler来转换测试集
# 注意:绝对不能对测试集使用fit_transform,这会引入数据泄露。
# 4. 创建并训练kNN模型
knn = KNeighborsClassifier(n_neighbors=5, metric='euclidean') # 默认就是欧氏距离
knn.fit(X_train_scaled, y_train)
# 5. 在训练集和测试集上进行预测
y_train_pred = knn.predict(X_train_scaled)
y_test_pred = knn.predict(X_test_scaled)
# 6. 评估模型
print("\n--- Training Set Performance ---")
print(f"Accuracy: {accuracy_score(y_train, y_train_pred):.4f}")
print(classification_report(y_train, y_train_pred, target_names=iris.target_names))
print("\n--- Test Set Performance ---")
print(f"Accuracy: {accuracy_score(y_test, y_test_pred):.4f}")
print(classification_report(y_test, y_test_pred, target_names=iris.target_names))
# 可视化混淆矩阵
cm = confusion_matrix(y_test, y_test_pred)
plt.figure(figsize=(8,6))
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
xticklabels=iris.target_names,
yticklabels=iris.target_names)
plt.ylabel('True Label')
plt.xlabel('Predicted Label')
plt.title('Confusion Matrix on Test Set')
plt.show()
这段代码展示了一个完整的机器学习建模流程。其中 特征标准化 和 数据集划分 是两个极易出错的关键点。标准化必须在划分之后进行,且测试集的标准化参数(均值和标准差)必须来自训练集,否则就相当于让模型在训练时“偷看”了测试集的信息,评估结果将毫无意义。
4.2 超参数调优与交叉验证
如何找到最优的k值?我们不能凭感觉,而需要通过系统性的搜索和验证。这里我们使用网格搜索结合交叉验证。
from sklearn.model_selection import GridSearchCV
# 定义参数网格
param_grid = {
'n_neighbors': list(range(1, 31)), # 尝试k从1到30
'weights': ['uniform', 'distance'], # 'uniform':所有邻居权重相等;'distance':权重与距离成反比
'metric': ['euclidean', 'manhattan', 'minkowski'] # 尝试不同距离度量
}
# 创建一个基础的kNN分类器
base_knn = KNeighborsClassifier()
# 创建GridSearchCV对象
# cv=5 表示5折交叉验证, scoring='accuracy' 以准确率作为评估指标
grid_search = GridSearchCV(estimator=base_knn,
param_grid=param_grid,
cv=5,
scoring='accuracy',
n_jobs=-1, # 使用所有CPU核心并行计算
verbose=1) # 输出详细进度
# 在标准化后的训练集上进行搜索
print("Starting grid search...")
grid_search.fit(X_train_scaled, y_train)
# 输出最佳参数和最佳得分
print(f"\nBest parameters found: {grid_search.best_params_}")
print(f"Best cross-validation accuracy: {grid_search.best_score_:.4f}")
# 使用最佳模型在测试集上做最终评估
best_knn = grid_search.best_estimator_
y_test_pred_best = best_knn.predict(X_test_scaled)
test_accuracy = accuracy_score(y_test, y_test_pred_best)
print(f"Test set accuracy with best model: {test_accuracy:.4f}")
# 可视化k值与交叉验证得分的关系(固定其他参数为最佳值)
# 为了清晰,我们固定 weights='uniform', metric='euclidean', 只观察k的影响
cv_scores = []
k_range = range(1, 31)
for k in k_range:
knn = KNeighborsClassifier(n_neighbors=k, weights='uniform', metric='euclidean')
# 使用简单的交叉验证计算平均分
from sklearn.model_selection import cross_val_score
scores = cross_val_score(knn, X_train_scaled, y_train, cv=5, scoring='accuracy')
cv_scores.append(scores.mean())
plt.figure(figsize=(10,6))
plt.plot(k_range, cv_scores, marker='o', linestyle='-')
plt.xlabel('Value of k for KNN')
plt.ylabel('Cross-Validated Accuracy')
plt.title('kNN Performance vs. k Value')
plt.grid(True)
plt.axvline(x=grid_search.best_params_['n_neighbors'], color='red', linestyle='--', label=f'Best k={grid_search.best_params_["n_neighbors"]}')
plt.legend()
plt.show()
通过网格搜索,我们不仅找到了最佳的k值,还可能发现
weights='distance'
(给更近的邻居更高的投票权重)或曼哈顿距离在某些数据集上表现更好。交叉验证曲线图能直观展示模型性能随k值变化的趋势:通常随着k增大,准确率先上升后下降,形成一个“肘部”,帮助我们理解模型的偏差-方差权衡。
5. kNN的优缺点与实战避坑指南
经过原理学习和实战编码,我们现在可以更全面地审视kNN算法,并总结出一套实用的“避坑”经验。
5.1 算法优势与局限性分析
优势:
- 原理简单,易于理解和实现 :这是kNN最大的优点,使其成为教学和原型设计的首选。
- 无需训练阶段 :新数据可以即时加入“记忆库”,适用于数据流不断更新的场景。
- 对异常值有一定鲁棒性(当k较大时) :少数异常点不会对整体预测造成决定性影响。
- 天然支持多分类问题 :无需像逻辑回归那样进行改造。
- 决策边界非线性 :可以拟合非常复杂的模式,只要局部数据足够密集。
局限性:
- 计算成本高 :预测时需要计算新样本与所有训练样本的距离,时间复杂度为O(N),其中N是训练集大小。对于大规模数据集,预测速度极慢。虽然有KD-Tree、Ball Tree等数据结构可以加速近邻搜索,但在高维空间下其优势也会减弱。
- 维度灾难的受害者 :随着特征维度增加,数据点在空间中的分布会变得极其稀疏,任何两点间的距离都趋于相等,使得“最近邻”的概念失去意义。kNN在成百上千维的特征空间(如文本TF-IDF)中效果往往很差。
- 对不平衡数据敏感 :如果某个类别的样本数量远多于其他类别,那么新样本的k个邻居很可能被这个大类别“垄断”,导致对小类别的预测精度极差。
- 对特征尺度敏感 :必须进行特征标准化/归一化,否则量纲大的特征会主导距离计算。
- 需要大量内存 :必须存储整个训练集,内存消耗大。
5.2 常见问题与解决方案实录
在实际项目中,你会遇到各种各样的问题。以下是我踩过坑后总结的经验:
问题1:模型在训练集上准确率接近100%,但在测试集上很差。
- 可能原因 :过拟合,k值太小(如k=1)。
-
排查与解决
:
- 绘制交叉验证曲线,观察不同k值下的验证集性能。
- 使用网格搜索寻找最优k值。
-
考虑增加k值,或使用带权重的投票(
weights='distance'),让更近的邻居拥有更大话语权,而不是简单的一票否决。
问题2:模型运行速度太慢,尤其是预测时。
- 可能原因 :训练集样本数(N)或特征数(D)过大。
-
排查与解决
:
-
算法层面
:在实例化
KNeighborsClassifier时,指定algorithm参数。默认是'auto',scikit-learn会自动选择。对于低维数据(D<20),'kd_tree'效率很高;对于高维数据或稀疏数据,'ball_tree'可能更好;对于非常高的维度,'brute'(暴力搜索)有时反而是唯一选择。可以尝试不同选项并对比速度。 -
数据层面
:
- 降维 :使用PCA、t-SNE等降维技术减少特征数量D。
-
样本缩减
:使用聚类方法(如K-Means)找出代表性样本,只用这些样本来构建kNN的“记忆库”。或者使用
NearestNeighbors进行近似最近邻搜索。
- 工程层面 :考虑使用更快的近邻搜索库,如Facebook的Faiss(针对稠密向量)或Annoy(针对高维向量)。
-
算法层面
:在实例化
问题3:面对类别不平衡的数据集,模型总是预测多数类。
- 可能原因 :kNN的多数表决机制在不平衡数据上失效。
-
排查与解决
:
-
调整类别权重
:scikit-learn的kNN不支持直接的
class_weight参数,但可以通过以下方式变通:-
使用带权重的距离
:
weights='distance',让近邻的投票权重大于远邻,有时能缓解问题。 - 对少数类样本进行过采样 :使用SMOTE等算法生成合成的少数类样本,平衡数据集后再训练。
- 对多数类样本进行欠采样 :随机丢弃一部分多数类样本,但可能会丢失信息。
-
使用带权重的距离
:
- 改变评估指标 :不要只看准确率(Accuracy)。对于不平衡数据,准确率是欺骗性的。关注精确率(Precision)、召回率(Recall)、F1-score,尤其是少数类的召回率。使用混淆矩阵和分类报告来全面评估。
-
调整类别权重
:scikit-learn的kNN不支持直接的
问题4:不知道如何选择距离度量。
- 可能原因 :对数据特性不了解。
-
排查与解决
:
- 数值型特征 :从欧氏距离开始。如果数据有异常值,尝试曼哈顿距离(对异常值更不敏感)。
- 分类特征 :欧氏距离不适用。需要使用汉明距离等专门处理分类数据的度量。在scikit-learn中,可以自定义距离度量函数,但需确保其满足距离公理。
- 文本或高维稀疏数据 :首选余弦相似度,因为它只关心向量的方向而非大小,适合TF-IDF向量。
- 万能方法 :通过网格搜索将距离度量作为超参数进行优化,让数据说话。
问题5:如何处理既有数值型又有类别型的混合特征?
- 可能原因 :原始数据复杂。
-
排查与解决
:
- 特征工程 :将类别型特征进行独热编码(One-Hot Encoding)或标签编码(Label Encoding,需谨慎,因为会引入大小关系)。然后对所有数值特征(包括编码后的)进行标准化。
- 使用专门的距离度量 :设计或寻找能够同时处理混合类型数据的距离度量,如Gower距离。这通常需要自定义距离函数传入kNN模型。
- 考虑其他算法 :如果特征类型非常复杂,树模型(如随机森林、梯度提升树)通常能更好地原生处理混合类型数据,可能是更合适的选择。
最后,一个最重要的心得: kNN是一个优秀的基准模型和探索性工具,但在生产环境中部署需要谨慎评估其计算和存储成本 。对于实时性要求高或数据量巨大的场景,你可能需要更轻量级的模型(如线性模型)或更高效的近似最近邻算法。理解它的原理和局限,能让你在正确的场景下做出正确的技术选型。
更多推荐



所有评论(0)