C++从零实现KNN分类器
·
一、引言
在机器学习领域,K最近邻(K-Nearest Neighbors,KNN)算法以其简单直观的特性,成为了很多初学者入门的第一课。今天,我将手把手教你如何使用C++实现一个完整的KNN分类器,不仅理解算法原理,还能掌握实际的编程技巧。
什么是KNN算法?
KNN是一种基于实例的学习算法,它的核心思想可以用一句俗语概括:"近朱者赤,近墨者黑"。简单来说,一个样本的类别可以由它最接近的k个邻居的类别来决定。
算法工作原理:
距离计算:计算测试样本与所有训练样本的距离
寻找邻居:选择距离最近的k个训练样本
投票决策:根据这k个邻居的类别进行投票,票数最多的类别即为预测结果
二、C++实现核心代码
下面让我们一步步实现KNN分类器的核心部分。
2.1 类定义与基础结构:
class KNNClassifier {
private:
std::vector<std::vector<double>> trainFeatures; // 存储训练特征
std::vector<int> trainLabels; // 存储训练标签
int k; // K值参数
public:
// 构造函数,初始化K值
KNNClassifier(int k) : k(k) {}
// 训练方法:存储训练数据
void fit(const std::vector<std::vector<double>>& features,
const std::vector<int>& labels) {
trainFeatures = features;
trainLabels = labels;
}
};
2.2 距离计算:算法的核心
距离度量是KNN算法的关键,我们使用最常用的欧氏距离:
double euclideanDistance(const std::vector<double>& a,
const std::vector<double>& b) {
double sum = 0.0;
for (size_t i = 0; i < a.size(); ++i) {
sum += pow(a[i] - b[i], 2); // 计算平方差
}
return sqrt(sum); // 开方得到欧氏距离
}
性能提示:在实际应用中,我们可以省略开方运算,因为距离的大小关系不变,这样可以提升计算速度。
2.3 预测逻辑:实现KNN精髓
int predict(const std::vector<double>& sample) {
// 存储所有训练样本与测试样本的距离及其索引
std::vector<std::pair<double, int>> distances;
// 计算测试样本与每个训练样本的距离
for (size_t i = 0; i < trainFeatures.size(); ++i) {
double dist = euclideanDistance(sample, trainFeatures[i]);
distances.push_back({dist, i}); // 保存距离和对应的训练样本索引
}
// 按距离从小到大排序
std::sort(distances.begin(), distances.end());
// 统计前k个最近邻居的类别投票
std::map<int, int> voteCount;
for (int i = 0; i < k; ++i) {
int neighborIndex = distances[i].second;
int label = trainLabels[neighborIndex];
voteCount[label]++; // 对应类别票数加1
}
// 找出票数最多的类别
int predictedLabel = -1;
int maxVotes = 0;
for (const auto& vote : voteCount) {
if (vote.second > maxVotes) {
maxVotes = vote.second;
predictedLabel = vote.first;
}
}
return predictedLabel;
}
2.4 使用示例
让我们看看如何使用这个KNN分类器:
// 创建K=3的KNN分类器
KNNClassifier knn(3);
// 准备训练数据:特征和标签
std::vector<std::vector<double>> trainFeatures = {
{1.0, 2.0}, // 样本1
{1.5, 1.8}, // 样本2
{5.0, 8.0}, // 样本3
{8.0, 8.0} // 样本4
};
std::vector<int> trainLabels = {0, 0, 1, 1}; // 对应类别
// 训练模型
knn.fit(trainFeatures, trainLabels);
// 预测新样本
std::vector<double> testSample = {1.2, 2.0};
int result = knn.predict(testSample);
cout << "预测类别: " << result << endl;
三、重要注意事项
3.1 K值选择策略
K值的选择对模型性能有重大影响:
- K值太小:模型过于复杂,容易过拟合
- K值太大:模型过于简单,可能欠拟合
建议使用交叉验证来选择最优K值。
3.2 距离度量的选择
根据数据类型选择合适的距离度量:
- 欧氏距离:适用于连续特征
- 曼哈顿距离:适用于高维数据
- 余弦相似度:适用于文本数据
四、面试手撕
考虑到面试手撕一般写核心代码体现主要思路就行,下面给出一个稍微没那么严谨的作为参考。
#include <vector>
#include <algorithm>
#include <map>
// 假设数据格式:特征为vector<double>,标签为int
int knn(const std::vector<std::pair<std::vector<double>, int>>& train,
const std::vector<double>& test, int k) {
// 1. 计算距离并绑定标签
std::vector<std::pair<double, int>> dists; // (距离, 标签)
for (const auto& sample : train) { // C++11:通过sample访问pair成员
const std::vector<double>& feat = sample.first; // 特征
int label = sample.second; // 标签
double d = 0;
for (size_t i = 0; i < feat.size(); ++i) {
d += (feat[i] - test[i]) * (feat[i] - test[i]); // 欧氏距离平方
}
dists.emplace_back(d, label);
}
// 2. 按距离升序排序,取前k个
std::sort(dists.begin(), dists.end());
std::vector<int> topKLabels;
for (int i = 0; i < k; ++i) {
topKLabels.push_back(dists[i].second);
}
// 3. 投票选最多标签
std::map<int, int> count;
for (int l : topKLabels) {
count[l]++;
}
int bestLabel = -1;
int maxCnt = 0;
// C++11:通过迭代器访问map键值对(替代结构化绑定)
for (const auto& entry : count) { // entry是pair<const int, int>
int l = entry.first; // 标签
int c = entry.second; // 计数
if (c > maxCnt) {
maxCnt = c;
bestLabel = l;
}
}
return bestLabel;
}
更多推荐

所有评论(0)