用Java手写kNN和朴素贝叶斯:从鸢尾花数据集到电影推荐,一次搞定两个经典算法
Java实战从kNN到朴素贝叶斯双算法实现分类与推荐系统在机器学习领域k近邻kNN和朴素贝叶斯NB是两个经典且实用的算法。本文将带你用Java实现这两个算法并应用于鸢尾花分类和电影推荐两个不同场景。通过对比实现你会发现虽然两者都基于相似性概念但在思想和代码实现上有着显著差异。1. 环境准备与数据加载在开始编码前我们需要准备好开发环境和数据集。这里使用Weka库来处理ARFF格式的数据文件它提供了方便的API来操作机器学习数据集。首先创建Maven项目并添加Weka依赖dependency groupIdnz.ac.waikato.cms.weka/groupId artifactIdweka-stable/artifactId version3.8.6/version /dependency对于鸢尾花数据集我们可以直接从网络获取public KnnClassification(String paraFilename) { try { FileReader fileReader new FileReader(paraFilename); dataset new Instances(fileReader); dataset.setClassIndex(dataset.numAttributes() - 1); fileReader.close(); } catch (Exception ee) { System.out.println(Error reading file: ee); System.exit(0); } }电影评分数据则需要特殊处理因为它是用户-物品-评分的三元组形式public MBR(String paraFilename, int paraNumUsers, int paraNumItems) throws Exception { // 初始化数组 BufferedReader tempBufReader new BufferedReader(new FileReader(tempFile)); String tempString; while ((tempString tempBufReader.readLine()) ! null) { String[] tempStrArray tempString.split(,); // 处理每行数据 } }2. kNN算法实现与优化kNN算法的核心思想是物以类聚——通过计算待分类样本与训练集中各样本的距离找出最近的k个邻居然后根据这些邻居的类别进行投票决定待分类样本的类别。距离度量是kNN的关键常见的有曼哈顿距离各维度绝对差之和欧氏距离平方差之和开方public double distance(int paraI, int paraJ) { double resultDistance 0; switch (distanceMeasure) { case MANHATTAN: // 曼哈顿距离计算 break; case EUCLIDEAN: // 欧氏距离计算 break; } return resultDistance; }邻居查找的原始实现需要多次扫描训练集效率较低。我们可以优化为单次扫描public int[] computeNearests(int paraCurrent) { int[] resultNearests new int[numNeighbors]; double[] tempDistances new double[trainingSet.length]; // 一次计算所有距离 for (int i 0; i trainingSet.length; i) { tempDistances[i] distance(paraCurrent, trainingSet[i]); } // 然后找出最小的k个 }kNN在鸢尾花分类上的典型准确率能达到95%左右但计算复杂度随数据量线性增长这是其主要缺点。3. 基于M-distance的推荐系统推荐系统与分类问题看似不同但核心思想也是找相似。这里我们实现基于物品的协同过滤推荐使用M-distance衡量物品相似度。M-distance认为两个物品的相似度取决于它们的平均评分是否接近if (Math.abs(itemAvgRating1 - itemAvgRating2) radius) { // 视为邻居物品 }预测用户对某物品的评分时取所有相似物品评分的平均值public double predictRating(int userId, int itemId) { double sum 0; int count 0; for (Item neighbor : findSimilarItems(itemId)) { if (userRatedItem(userId, neighbor.id)) { sum getUserRating(userId, neighbor.id); count; } } return count 0 ? sum / count : DEFAULT_RATING; }这种方法的MAE平均绝对误差通常在0.7-0.8之间调整radius参数可以平衡推荐覆盖率与准确率。4. 朴素贝叶斯分类器实现朴素贝叶斯基于贝叶斯定理假设特征之间相互独立。我们先实现处理符号型数据的版本public void calculateConditionalProbabilities() { // 初始化三维数组 conditionalProbabilitiesLaplacian new double[numClasses][numConditions][]; // 统计每个特征值在每个类别下的出现次数 for (Instance instance : dataset) { int cls (int)instance.classValue(); for (int attr 0; attr numConditions; attr) { int val (int)instance.value(attr); conditionalCounts[cls][attr][val]; } } // 计算拉普拉斯平滑后的概率 for (int cls 0; cls numClasses; cls) { for (int attr 0; attr numConditions; attr) { int numValues dataset.attribute(attr).numValues(); for (int val 0; val numValues; val) { conditionalProbabilitiesLaplacian[cls][attr][val] (conditionalCounts[cls][attr][val] 1) / (classCounts[cls] numValues); } } } }分类时计算后验概率的对数避免浮点数下溢public int classifyNominal(Instance instance) { double maxLogProbability Double.NEGATIVE_INFINITY; int bestClass -1; for (int cls 0; cls numClasses; cls) { double logProb Math.log(classDistributionLaplacian[cls]); for (int attr 0; attr numConditions; attr) { int val (int)instance.value(attr); logProb Math.log(conditionalProbabilitiesLaplacian[cls][attr][val]); } if (logProb maxLogProbability) { maxLogProbability logProb; bestClass cls; } } return bestClass; }5. 数值型数据的朴素贝叶斯对于数值型特征我们通常假设其服从高斯分布。需要为每个特征在每个类别下计算均值和标准差public void calculateGaussianParameters() { gaussianParameters new GaussianParamters[numClasses][numConditions]; for (int cls 0; cls numClasses; cls) { for (int attr 0; attr numConditions; attr) { double sum 0, sumSquares 0; int count 0; for (Instance instance : dataset) { if ((int)instance.classValue() cls) { double val instance.value(attr); sum val; sumSquares val * val; count; } } double mean sum / count; double stddev Math.sqrt((sumSquares - sum*sum/count) / count); gaussianParameters[cls][attr] new GaussianParamters(mean, stddev); } } }分类时使用高斯概率密度函数public int classifyNumerical(Instance instance) { double maxLogProbability Double.NEGATIVE_INFINITY; int bestClass -1; for (int cls 0; cls numClasses; cls) { double logProb Math.log(classDistributionLaplacian[cls]); for (int attr 0; attr numConditions; attr) { double x instance.value(attr); double mu gaussianParameters[cls][attr].mu; double sigma gaussianParameters[cls][attr].sigma; // 高斯分布的概率密度对数 logProb -Math.log(sigma) - (x-mu)*(x-mu)/(2*sigma*sigma); } if (logProb maxLogProbability) { maxLogProbability logProb; bestClass cls; } } return bestClass; }6. 算法对比与应用场景虽然kNN和朴素贝叶斯都可用于分类但它们的适用场景有所不同特性kNN朴素贝叶斯训练速度快(惰性学习)快预测速度慢快内存需求高(存储全部数据)低(存储参数)特征相关性可处理相关特征假设特征独立数据规模适合中小规模适合大规模特征类型数值型表现好数值型、类别型均可在推荐系统场景中基于用户的协同过滤(UserCF)类似于kNN而基于物品的协同过滤(ItemCF)更接近朴素贝叶斯的思想。选择哪种算法取决于具体需求和数据特性。7. 工程实践建议在实际项目中应用这些算法时有几个关键点需要注意数据预处理归一化kNN对特征尺度敏感需做归一化缺失值处理朴素贝叶斯需要处理缺失值// 归一化示例 public void normalize() { for (int attr 0; attr numConditions; attr) { double min Double.MAX_VALUE; double max Double.MIN_VALUE; // 找出最小最大值 // 归一化每个特征值 } }参数调优kNN中的k值M-distance中的radius阈值朴素贝叶斯的平滑参数性能优化kNN可以使用KD树等数据结构加速搜索对于大规模数据可以考虑近似算法评估指标分类问题准确率、精确率、召回率、F1值推荐系统MAE、RMSE、覆盖率、多样性// 评估指标计算示例 public void evaluate() { int[][] confusionMatrix new int[numClasses][numClasses]; for (int i 0; i numInstances; i) { int actual (int)dataset.instance(i).classValue(); int predicted predicts[i]; confusionMatrix[actual][predicted]; } // 计算各项指标... }8. 扩展与进阶掌握了基础实现后可以考虑以下扩展方向加权kNN给更近的邻居更高权重// 加权投票 public int weightedVoting(int[] neighbors) { double[] weights new double[numClasses]; for (int i 0; i neighbors.length; i) { double distance distances[i]; double weight 1.0 / (distance 1e-5); // 避免除零 int cls (int)dataset.instance(neighbors[i]).classValue(); weights[cls] weight; } // 返回权重最大的类别 }核密度估计改进朴素贝叶斯对数值型特征的建模混合型数据同时处理数值型和类别型特征增量学习支持在线更新模型分布式实现使用Spark等框架处理大数据通过本项目的实践你不仅学会了两种重要算法的实现还掌握了如何将机器学习应用于不同场景的关键技能。建议尝试将这些算法应用到自己的项目中或者参加Kaggle等数据科学竞赛来进一步磨练技能。