1. 从零开始理解kNN算法
kNN(k-Nearest Neighbors)是机器学习中最直观的分类算法之一。我第一次接触这个算法时,被它的"惰性学习"特性所吸引——它不需要像其他算法那样进行复杂的模型训练,而是直接把所有训练数据存储起来,等到需要预测时再临时计算。
这个算法的工作原理很像我们日常生活中的经验判断。比如你要判断一个水果是不是苹果,你会看看它周围最相似的几个水果是什么。如果周围5个水果中有4个是苹果,那它大概率也是苹果。kNN就是把这个过程数学化了,通过计算距离来确定"邻居"。
在Java中实现kNN时,有几个关键点需要注意:
- 距离度量:常用的有曼哈顿距离和欧式距离
- 邻居数量k的选择:通常取奇数以避免平票
- 数据归一化:不同属性的量纲可能不同,需要统一尺度
java复制// 计算两个实例之间的距离
public double distance(int paraI, int paraJ) {
double resultDistance = 0;
double tempDifference;
switch (distanceMeasure) {
case MANHATTAN: // 曼哈顿距离
for (int i = 0; i < dataset.numAttributes() - 1; i++) {
tempDifference = dataset.instance(paraI).value(i) - dataset.instance(paraJ).value(i);
resultDistance += Math.abs(tempDifference);
}
break;
case EUCLIDEAN: // 欧式距离
for (int i = 0; i < dataset.numAttributes() - 1; i++) {
tempDifference = dataset.instance(paraI).value(i) - dataset.instance(paraJ).value(i);
resultDistance += tempDifference * tempDifference;
}
resultDistance = Math.sqrt(resultDistance);
break;
}
return resultDistance;
}
在实际项目中,我发现kNN虽然简单,但在某些数据集上表现惊人地好。特别是在特征维度不高、数据分布不规则的情况下,kNN往往能取得不错的效果。不过它的计算复杂度确实是个问题,特别是当训练集很大时,每次预测都需要计算与所有训练样本的距离。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 朴素贝叶斯分类器的实现
朴素贝叶斯(Naive Bayes)是另一个经典的机器学习算法,它基于贝叶斯定理,并假设各特征之间相互独立。虽然这个"朴素"的假设在现实中很少成立,但实际应用中它的表现却经常出人意料地好。
我第一次实现朴素贝叶斯时,最大的困惑是如何处理连续型特征。后来了解到可以通过假设特征服从高斯分布,用概率密度函数来估计条件概率。对于离散型特征,则直接统计频率即可。
java复制// 处理数值型特征的高斯分布参数
public void calculateGausssianParameters() {
gaussianParameters = new GaussianParamters[numClasses][numConditions];
double[] tempValuesArray = new double[numInstances];
for (int i = 0; i < numClasses; i++) {
for (int j = 0; j < numConditions; j++) {
// 收集属于当前类的特征值
int tempNumValues = 0;
double tempSum = 0;
for (int k = 0; k < numInstances; k++) {
if ((int) dataset.instance(k).classValue() != i) continue;
tempValuesArray[tempNumValues] = dataset.instance(k).value(j);
tempSum += tempValuesArray[tempNumValues];
tempNumValues++;
}
// 计算均值和标准差
double tempMu = tempSum / tempNumValues;
double tempSigma = 0;
for (int k = 0; k < tempNumValues; k++) {
tempSigma += Math.pow(tempValuesArray[k] - tempMu, 2);
}
tempSigma = Math.sqrt(tempSigma / tempNumValues);
gaussianParameters[i][j] = new GaussianParamters(tempMu, tempSigma);
}
}
}
朴素贝叶斯的一个巨大优势是训练速度极快,因为它只需要统计各类别的先验概率和各特征的条件概率。在实际应用中,特别是文本分类领域,朴素贝叶斯常常是首选算法之一。
3. 数据预处理的关键步骤
在实现这两个分类器时,我深刻体会到数据预处理的重要性。原始数据往往不能直接用于机器学习算法,需要经过一系列处理:
- 数据清洗:处理缺失值、异常值
- 特征选择:选择有区分度的特征
- 数据归一化:特别是对kNN这样基于距离的算法
- 数据集划分:分为训练集和测试集
java复制// 数据集随机划分方法
public void splitTrainingTesting(double paraTrainingFraction) {
int tempSize = dataset.numInstances();
int[] tempIndices = getRandomIndices(tempSize);
int tempTrainingSize = (int) (tempSize * paraTrainingFraction);
trainingSet = new int[tempTrainingSize];
testingSet = new int[tempSize - tempTrainingSize];
for (int i = 0; i < tempTrainingSize; i++) {
trainingSet[i] = tempIndices[i];
}
for (int i = 0; i < tempSize - tempTrainingSize; i++) {
testingSet[i] = tempIndices[tempTrainingSize + i];
}
}
特别是在实现kNN时,如果不做归一化,数值范围大的特征会主导距离计算,导致算法性能下降。我通常使用min-max归一化或z-score标准化来处理数据。
4. 算法性能评估与比较
实现算法后,如何评估它们的性能是下一个关键问题。常用的评估指标包括:
- 准确率(Accuracy):正确分类的比例
- 精确率(Precision)和召回率(Recall):特别是对不平衡数据
- F1分数:精确率和召回率的调和平均
java复制// 计算分类器准确率
public double getAccuracy() {
double tempCorrect = 0;
for (int i = 0; i < predictions.length; i++) {
if (predictions[i] == dataset.instance(testingSet[i]).classValue()) {
tempCorrect++;
}
}
return tempCorrect / predictions.length;
}
通过比较kNN和朴素贝叶斯在相同数据集上的表现,我发现:
- kNN在特征间相关性强的数据集上表现更好
- 朴素贝叶斯在小数据集上更不容易过拟合
- kNN的计算成本随数据量线性增长,而朴素贝叶斯的预测速度很快
在实际项目中,我通常会尝试多种算法,选择最适合当前问题和数据特性的那一个。没有放之四海而皆准的最佳算法,只有最适合特定场景的算法。
