基于M-distance的KNN算法
时间:2022/5/4
0.数据集分析
采用的是电影评分数据集movielens-943u1682m.txt,数据由三部分构成(用户ID、电影ID、评分)。数据采用的是压缩存储的方式。

1.M-distance
这是闵老师为我们介绍的一个之前的师姐在做推荐系统时发现的距离度量论文地址。结合具体的数据movielens-943u1682m.txt分析,
1.基于电影的方法中,M-distance就是在预测用户对于未知电影的评分时,计算其他用户对该电影评分的平均值A1,计算该用户其他已经评分的电影的平均评分A2。在给定的阈值R内选取离预测目标平均评分的距离(|A1-A2|)小于阈值的用户已评分电影作为邻居,计算邻居的平均评分作为该用户对预测目标电影的预测评分。(核心思想:找寻用户已经评分的与预测电影相似的电影,用用户对该目标的相似电影的评分来估计预测目标的评分)
2.基于用户的方法中,M-distance就是在预测用户对于未知电影的评分时,计算用户所有已经评分的电影的平均评分A1;计算对预测目标电影已经评分的其他用户的平均评分A2。在给定阈值R内找寻离预测用户平均评分的距离(|A1-A2|)小于阈值的其他用户作为邻居。计算其他用户对预测目标电影的平均评分作为预测用户对预测目标电影的预测评分。(核心思想:用其他和预测用户相似的用户对该电影的评分来估计预测用户对预测电影的评分)

2.算法流程
- 读入数据,从文本中一行一行的读取,构建用户评分矩阵。
- 采用leave-one-out的方法进行测试;
- 使用基于电影的方法进行预测:
- 计算其他用户对该电影评分的平均值A1;
- 计算该用户其他已经评分的电影的平均评分A2;
- 在给定的阈值R内选取离预测目标平均评分的距离(|A1-A2|)小于阈值的用户已评分电影作为邻居,不必选出来,只需要统计用户该电影的评分总和S1;
- 求平均评分 S1/邻居数量;得到对该电影的评分。
- 计算MAE、RSME评价指标;
3.代码部分
/**
* MDistance.java
*
* @author zjy
* @date 2022/5/3
* @Description:
* @version V1.0
*/
package swpu.zjy.ML.KNN;
import java.io.*;
public class MDistance {
//默认的评分,在对没有邻居的数据进行预测时,则填入默认的评分
public static final double DEFAULT_RATING = 3.0;
//用户数量
private int numUsers;
//Item数量
private int numItems;
//非零评分数量
private int numRatings;
//预测向量
private double[] predictions;
//评分矩阵:user-item-rating 用以存储数据
private int[][] compressedRatingMatrix;
/**
* 这里存在两种方法,一种是基于用户的,一种是基于Item的
*/
//用户评分数量,用以计算用户平均评分
private int[] userDegrees;
//当前用户的平均评分
private double[] userAverageRatings;
//电影非零评分数量,用以计算当前Item平均评分
private int[] itemDegrees;
//Item平均评分
private double[] itemAverageRatings;
//用户评分在评分矩阵开始下标
private int[] userStartingIndices;
//不存在邻居的对象数量
private int numNonNeighbors;
//用以确定邻居的半径,小于这个半径的元素均为邻居,不用K来限制
private double radius;
/**
* 构造方法,根据所传参数,初始算法所需要的矩阵
*
* @param dataSetFileName 数据集文件路径
* @param paraNumUsers 用户数量
* @param paraNumItems Item数量
* @param paraNumRatings 非零评分数量
*/
public MDistance(String dataSetFileName, int paraNumUsers, int paraNumItems, int paraNumRatings) {
//初始化 所需要的矩阵
this.numItems = paraNumItems;
this.numUsers = paraNumUsers;
this.numRatings = paraNumRatings;
userDegrees = new int[numUsers];
//+1为了在最后存放非零评分数量
userStartingIndices = new int[numUsers + 1];
userAverageRatings = new double[numUsers];
itemDegrees = new int[numItems];
itemAverageRatings = new double[numItems];
compressedRatingMatrix = new int[numRatings][3];
predictions = new double[numRatings];
/**
* 开始读入数据
*/
try {
readFile(dataSetFileName);
} catch (IOException e) {
e.printStackTrace();
}
/**
* 求取平均数
*/
getAverageValue();
}
/**
* 从数据集读入数据,分开填写,减少构造方法的复杂度,提高可读性
*
* @param dataSetFileName 数据集文件路径
* @throws IOException
*/
private void readFile(String dataSetFileName) throws IOException {
File tempfile = new File(dataSetFileName);
if (!tempfile.exists()) {
System.out.println("File " + dataSetFileName + " does not exists.");
System.exit(0);
}
BufferedReader tempBufferReader = new BufferedReader(new FileReader(tempfile));
//存放一行数据
String tempLine;
//拆分一行数据:user-item-rating
String[] tempValue;
int tempIndex = 0;
userStartingIndices[0] = 0;
userStartingIndices[numUsers] = numRatings;
while ((tempLine = tempBufferReader.readLine()) != null) {
tempValue = tempLine.split(",");
compressedRatingMatrix[tempIndex][0] = Integer.parseInt(tempValue[0]);//user
compressedRatingMatrix[tempIndex][1] = Integer.parseInt(tempValue[1]);//item
compressedRatingMatrix[tempIndex][2] = Integer.parseInt(tempValue[2]);//rating
userDegrees[compressedRatingMatrix[tempIndex][0]]++;//用户评分数量++
itemDegrees[compressedRatingMatrix[tempIndex][1]]++;//Item评分数量++
if (tempIndex > 0) {
//记录一个用户数据开始的下标,以便后续访问
if (compressedRatingMatrix[tempIndex][0] != compressedRatingMatrix[tempIndex - 1][0]) {
userStartingIndices[compressedRatingMatrix[tempIndex][0]] = tempIndex;
}
}
tempIndex++;
}
tempBufferReader.close();
}
/**
* 求取平均数
*/
public void getAverageValue() {
//记录总评分
double[] tempUserTotalScore = new double[numUsers];
double[] tempItemTotalScore = new double[numItems];
//求和
for (int i = 0; i < numRatings; i++) {
tempUserTotalScore[compressedRatingMatrix[i][0]] += compressedRatingMatrix[i][2];
tempItemTotalScore[compressedRatingMatrix[i][1]] += compressedRatingMatrix[i][2];
}
//求平均数
for (int i = 0; i < numUsers; i++) {
userAverageRatings[i] = tempUserTotalScore[i] / userDegrees[i];
}
for (int i = 0; i < numItems; i++) {
itemAverageRatings[i] = tempItemTotalScore[i] / itemDegrees[i];
}
}
/**
* 配置邻居半径
*
* @param radius 邻居半径
*/
public void setRadius(double radius) {
if (radius > 0) {
this.radius = radius;
} else {
this.radius = 0.1;
}
}
/**
* leave-one-out测试:一次测试数据集中的一个,将数据集中除了被测数据外的其他数据作为训练数据,
* 对数据集中的每一个数据都进行测试
*/
public void leaveOneOutPrediction() {
//统计测试评分所属Item的平均评分
double tempItemAverageRating;
//记录测试对象数据
int tempUser, tempItem, tempRating;
System.out.println("\r\nLeaveOneOutPrediction for radius " + radius);
//用以记录无邻居的对象
numNonNeighbors = 0;
//开始leave-one-out
for (int i = 0; i < numRatings; i++) {
//获取测试对象数据
tempUser = compressedRatingMatrix[i][0];
tempItem = compressedRatingMatrix[i][1];
tempRating = compressedRatingMatrix[i][2];
//计算测试对象所属Item平均评分(将测试对象排除在外)
tempItemAverageRating = (itemAverageRatings[tempItem] * itemDegrees[tempItem] - tempRating)
/ (itemDegrees[tempItem] - 1);
//记录邻居个数
int tempNeighbors = 0;
//记录邻居总评分
double tempTotalRating = 0;
int tempCompareItem;
for (int j = userStartingIndices[tempUser]; j < userStartingIndices[tempUser + 1]; j++) {
tempCompareItem = compressedRatingMatrix[j][1];
/**
* 排除测试对象
*/
if (tempCompareItem == tempItem)
continue;
/**
* 比较测试对象平均评分与其他该用户已经评分的Item的平均评分是否小于给定的邻居半径
* 统计邻居个数与总评分
*/
if (Math.abs(tempItemAverageRating - itemAverageRatings[tempCompareItem]) <= radius) {
tempTotalRating += compressedRatingMatrix[j][2];
tempNeighbors++;
}
}
/**
* 进行预测
*/
if (tempNeighbors > 0) {
predictions[i] = tempTotalRating / tempNeighbors;
} else {
predictions[i] = DEFAULT_RATING;
numNonNeighbors++;
}
}
}
/**
* 计算算法的MAE评价指标,即预测总误差/预测个数
*
* @return
* @throws Exception
*/
public double computeMAE() throws Exception {
double tempTotalError = 0;
for (int i = 0; i < predictions.length; i++) {
tempTotalError += Math.abs(predictions[i] - compressedRatingMatrix[i][2]);
}
return tempTotalError / predictions.length;
}
/**
* 计算算法的RSME评价指标,即RSME=sqrt((预测误差^2)/预测个数)
*
* @return
* @throws Exception
*/
public double computeRSME() throws Exception {
double tempTotalError = 0;
for (int i = 0; i < predictions.length; i++) {
tempTotalError += (predictions[i] - compressedRatingMatrix[i][2])
* (predictions[i] - compressedRatingMatrix[i][2]);
}
double tempAverage = tempTotalError / predictions.length;
return Math.sqrt(tempAverage);
}
public static void main(String[] args) {
MDistance tempRecommender = new MDistance("E:\\DataSet\\movielens-943u1682m.txt", 943, 1682, 100000);
//测试不同阈值半径
for (double tempRadius = 0.2; tempRadius < 0.6; tempRadius += 0.1) {
tempRecommender.setRadius(tempRadius);
tempRecommender.leaveOneOutPrediction();
double tempMAE = 0;
double tempRSME = 0;
try {
tempMAE = tempRecommender.computeMAE();
tempRSME = tempRecommender.computeRSME();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("Radius = " + tempRadius + ", MAE = " + tempMAE + ", RSME = " + tempRSME
+ ", numNonNeighbors = " + tempRecommender.numNonNeighbors);
}
}
}
4.运行结果

从图中可以看出半径太小,无邻居的对象便会增多;半径太大,预测误差也会增大;所以对半径的选取对算法效果的影响还是很大的。
学习总结
这次主要学习了KNN,M-distance算法、K-means算法、朴素贝叶斯算法,由于毕设的原因,这次只完成了前两个算法。这次听闵老师讲课主要收获了如何学习算法的方法,以及如何将算法的理论转化成实际的代码。同时学习了一下闵老师的代码风格,例如变量命名规则,注释规则等。同时也学习老师在代码中所使用的基于索引的间接寻址思维,这种方式具有很浓烈的面向过程的味道,是很精彩的。在实际的使用过程中也是体现了很好的效果。
2025

被折叠的 条评论
为什么被折叠?



