今天来分享一下我的数据结构与算法课设中所做的应用题目。
- (系数1)很多人都喜爱听歌,以前我们用 MP3 听歌,现在直接通过音乐 App 在线就能听歌。而且各种音乐 App 的功能越来越强大,不仅可以自己选歌听,还可以根据你听歌的口味偏好,给你推荐可能会喜爱的音乐,而且有时候推荐的音乐还非常适合你的口味,甚至会惊艳到你!如此智能的一个功能,你知道它是怎么实现的吗?
实际上要解决这个问题,并不需要特别高深的理论。解决思路的核心思想非常简单、直白,用两句话就能总结出来。找到跟你口味偏好相似的用户,把他们爱听的歌曲推荐给你;找出跟你喜爱的歌曲特征相似的歌曲,把这些歌曲推荐给你。
基于相似用户做推荐如何找到跟你口味偏好相似的用户呢?或者说如何定义口味偏好相似呢?实际上,思路也很简单,我们把跟你听类似歌曲的人,看做口味相似的用户。你可以看我下面画的这个图。我用“1”表示“喜爱”,用“0”笼统地表示“不发表意见”。从下图中我们可以看出,你跟小明共同喜爱的歌曲最多,有 5 首。于是,我们就可以说,小明跟你的口味非常相似。
我们只需要遍历所有的用户,对比每个用户跟你共同喜爱的歌曲个数,并且设置一个阈值,如果你和某个用户共同喜爱的歌曲个数超过这个阈值,我们就把这个用户看作跟你口味相似的用户,把这个用户喜爱但你还没听过的歌曲推荐给你。不过刚刚的这个解决方案中有一个问题,我们如何知道用户喜爱哪首歌曲呢?也就是说,如何定义用户对某首歌曲的喜爱程度呢?实际上,我们可以通过用户的行为,来定义这个喜爱程度。我们给每个行为定义一个得分,得分越高表示喜爱程度越高。
还是刚刚那个例子,我们如果把每个人对每首歌曲的喜爱程度表示出来,就是下面这个样子。图中,某个人对某首歌曲是否喜爱,我们不再用“1”或者“0”来表示,而是对应一个具体的分值。
有了这样一个用户对歌曲的喜爱程度的对应表之后,如何来判断两个用户是否口味相似呢?显然,我们不能再像之前那样,采用简单的计数来统计两个用户之间的相似度。还记得我们之前讲字符串相似度度量时,提到的编辑距离吗?这里的相似度度量,我们可以使用另外一个距离,那就是欧几里得距离(Euclidean distance)。这个概念中有两个关键词:向量和距离。一维空间是一条线,我们用 1,2,3……这样单个的数,来表示一维空间中的某个位置;二维空间是一个面,我们用(1,3)(4,2)(2,2)……这样的两个数,来表示二维空间中的某个位置;三维空间是一个立体空间,我们用(1,3,5)(3,1,7)(2,4,3)……这样的三个数,来表示三维空间中的某个位置。一维、二维、三维应该都不难理解,那更高维中的某个位置该如何表示呢?类比一维、二维、三维的表示方法,K 维空间中的某个位置,我们可以写作(X1,X2,X3,…,XK)。这种表示方法就是向量(vector)。二维、三维空间中,两个位置之间有距离的概念,类比到高纬空间,同样也有距离的概念,这就是我们说的两个向量之间的距离。那如何计算两个向量之间的距离呢?我们还是可以类比到二维、三维空间中距离的计算方法。通过类比,我们就可以得到两个向量之间距离的计算公式。这个计算公式就是欧几里得距离的计算公式:
我们把每个用户对所有歌曲的喜爱程度,都用一个向量表示。我们计算出两个向量之间的欧几里得距离,作为两个用户的口味相似程度的度量。从图中的计算可以看出,小明与你的欧几里得距离距离最小,也就是说,你俩在高维空间中靠得最近,所以,我们就断定,小明跟你的口味最相似。
请设计上述算法,实现一个简单的推荐系统。
以上是学校中给我的算法提示。
但是细想这样是不对的
所有计算出来的一个数值他们的范数都是不统一的,这就会导致,如果相似度最高即欧几里得距离最小的用户无法进行用户所需要的音乐推荐时,我们应该考虑去找相似度次高的用户让其去推荐主用户的所需要的歌曲。所以可以通过计算内积来得到一个0-1之间的数,这时候可以设定一个阈值Therhold,大于这个阈值的判定为和这个用户的口味比较相似,能够进行音乐的推送,所以可以进行如下改动。如图3.2
图3.2
此时我们把每一个用户看成一个向量,进行向量的乘法运算记录在dotProduct中即dotProduct += UserMusic[i][j] * UserReference[j],可以看出是主用户和每个用户的乘积的加和。然后主用户和其他用户的自身进行内积,存在UserNorm和MusicNorm数组中,再进行除法运算如公式1.1
0i主用户 « 其他用户主用户2+(其他用户)2
图1.1
这样就会得到一个0~1的数字Similarity记作用户的相似度。接下来就需要来进行排名比较来进行用户的歌曲推荐。
所以理论就有了,实践搞起。
void RecommondMu(int UserReference[], int UserMusic[NoUser][NoSongs], float ThreHold, int Reference[], int RecommendIndex[], int requiredSongs) {
// 计算用户之间的相似度
float similarities[NoUser] = { 0.0 };
for (int i = 0; i < NoUser; i++) {
int dotProduct = 0;
int userNorm = 0;
int preNorm = 0;
for (int j = 0; j < NoSongs; j++) {
// 内积计算存储在dotProduct中 两个向量元素相乘后累加
// UserMusic的二维向量和UserReference的一维向量进行内积再进行累加
// 第i个用户的similaritie计算在similarities[i]中
dotProduct += UserMusic[i][j] * UserReference[j];
// 向量自身进行内积
userNorm += UserMusic[i][j] * UserMusic[i][j];
preNorm += UserReference[j] * UserReference[j];
}
// 计算相似度
similarities[i] = dotProduct / (sqrt(userNorm) * sqrt(preNorm));
}
/* for (int i = 0; i < NoUser; i++) {
printf("用户%d相似度为%f\t",i+1, similarities[i]);
}*/
int recommendCount = 0;
int recommendIndex = 0;
int userIndex = 0;
while (recommendCount < requiredSongs && userIndex < NoUser) {
float currentMaxSimilarity = -1.0;
int currentMaxSimilarityIndex = -1;
// 找到相似度最高的用户
for (int i = 0; i < NoUser; i++) {
if (similarities[i] > currentMaxSimilarity) {
// 检查该用户是否有音乐可以推荐且主用户还未喜欢
for (int j = 0; j < NoSongs; j++) {
if (UserMusic[i][j] > 0 && UserReference[j] == 0) {
currentMaxSimilarityIndex = i;
currentMaxSimilarity = similarities[i];
break;
}
}
}
}
// 如果找到了相似度最高的用户
if (currentMaxSimilarityIndex >= 0) {
// 推荐该用户的音乐
for (int j = 0; j < NoSongs; j++) {
if (UserMusic[currentMaxSimilarityIndex][j] > 0 && UserReference[j] == 0) {
Reference[recommendCount] = j;
RecommendIndex[recommendCount] = currentMaxSimilarityIndex + 1;
recommendCount++;
similarities[currentMaxSimilarityIndex] = -1;//将该用户的相似度置为-1避免重复推荐
if (recommendCount == requiredSongs) {
break; // 推荐足够数量的音乐
}
}
}
}
userIndex++;
}
if (recommendCount == 0) {
printf("无法推荐所需音乐。\n");
}
else if (recommendCount < requiredSongs) {
printf("只能为您推荐 %d 首音乐。\n", recommendCount);
}
}
这是推荐音乐处的代码,我带了详细的注解。
以下是带数据测试的完整代码
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<math.h>
#include<time.h>
#include<ctime>
#include<iostream>
#define NoUser 6
#define NoSongs 1000
using namespace std;
/*
在这段代码中,通过计算用户i的行为得分向量userMusicMatrix的内积(userNorm)和用户喜好程度向量userPreference的内积(prefNorm),可以得到这两个向量的范数的乘积。
然后,通过将用户i与userPreference之间的内积(dotProduct)除以这两个向量范数的乘积,可以获得一个在0到1之间的相似度值。
这样做的原因是,在计算相似度时,除法操作可以对不同用户之间的度量进行归一化。这是因为不同用户的行为得分和喜好程度的范围可能不一样,有可能存在数量级上的差异。通过进行归一化操作,可以消除这些数量级的差异,使得相似度值更加可比较和可解释。
此外,通过将相似度值限制在0到1之间,可以将其解释为两个向量之间的度量或相似程度。更高的相似度值表示两个向量在喜好程度上更加相似。
*/
//采用内积算法
//其中 UserReference为主用户的喜好情况,UserMusic是其他用户的音乐喜好情况为一个矩阵,为内积的判断值,Reference为最终的喜好
int IsSongRecommended(int song, int Reference[], int count) {
for (int i = 0; i < count; i++) {
if (Reference[i] == song) {
return 1;//歌曲已经被推荐
}
}
return 0;
}
void RecommondMu(int UserReference[], int UserMusic[NoUser][NoSongs], float ThreHold, int Reference[], int RecommendIndex[], int requiredSongs) {
// 计算用户之间的相似度
float similarities[NoUser] = { 0.0 };
for (int i = 0; i < NoUser; i++) {
int dotProduct = 0;
int userNorm = 0;
int preNorm = 0;
for (int j = 0; j < NoSongs; j++) {
// 内积计算存储在dotProduct中 两个向量元素相乘后累加
// UserMusic的二维向量和UserReference的一维向量进行内积再进行累加
// 第i个用户的similaritie计算在similarities[i]中
dotProduct += UserMusic[i][j] * UserReference[j];
// 向量自身进行内积
userNorm += UserMusic[i][j] * UserMusic[i][j];
preNorm += UserReference[j] * UserReference[j];
}
// 计算相似度
similarities[i] = dotProduct / (sqrt(userNorm) * sqrt(preNorm));
}
/* for (int i = 0; i < NoUser; i++) {
printf("用户%d相似度为%f\t",i+1, similarities[i]);
}*/
int recommendCount = 0;
int recommendIndex = 0;
int userIndex = 0;
while (recommendCount < requiredSongs && userIndex < NoUser) {
float currentMaxSimilarity = -1.0;
int currentMaxSimilarityIndex = -1;
// 找到相似度最高的用户
for (int i = 0; i < NoUser; i++) {
if (similarities[i] > currentMaxSimilarity) {
// 检查该用户是否有音乐可以推荐且主用户还未喜欢
for (int j = 0; j < NoSongs; j++) {
if (UserMusic[i][j] > 0 && UserReference[j] == 0) {
currentMaxSimilarityIndex = i;
currentMaxSimilarity = similarities[i];
break;
}
}
}
}
// 如果找到了相似度最高的用户
if (currentMaxSimilarityIndex >= 0) {
// 推荐该用户的音乐
for (int j = 0; j < NoSongs; j++) {
if (UserMusic[currentMaxSimilarityIndex][j] > 0 && UserReference[j] == 0) {
Reference[recommendCount] = j;
RecommendIndex[recommendCount] = currentMaxSimilarityIndex + 1;
recommendCount++;
similarities[currentMaxSimilarityIndex] = -1;//将该用户的相似度置为-1避免重复推荐
if (recommendCount == requiredSongs) {
break; // 推荐足够数量的音乐
}
}
}
}
userIndex++;
}
if (recommendCount == 0) {
printf("无法推荐所需音乐。\n");
}
else if (recommendCount < requiredSongs) {
printf("只能为您推荐 %d 首音乐。\n", recommendCount);
}
}
//测试数据
void generateRandomData(int UserReference[], int UserMusic[][NoSongs]) {
srand(time(NULL));
printf("随机生成的用户喜好:\n");
printf("主用户:");
for (int j = 0; j < NoSongs; j++) {
UserReference[j] = rand() % 7 - 1;
printf("%d ", UserReference[j]);
}
printf("\n");
printf("其他用户:\n");
for (int i = 0; i < NoUser; i++) {
printf("User %d: ", i + 1);
for (int j = 0; j < NoSongs; j++) {
UserMusic[i][j] = rand() % 7 - 1;
printf("%d ", UserMusic[i][j]);
}
printf("\n");
}
}
//计算需要多少首音乐
int getRequired(int userReference[NoSongs]) {
int count = 0;
for (int i = 0; i < NoSongs; i++) {
if (userReference[i] == 0) {
count++;
}
}
return count;
}
void main() {
int userReference[NoUser];
int UserMusic[NoUser][NoSongs];
generateRandomData(userReference, UserMusic);
float threshold = 0.6;
//推荐结果数组
int recommendMusic[NoSongs] = { 0 };
int requireSongs = 0;
int RecommendIndex[NoUser] = { 0 };
requireSongs = getRequired(userReference);
clock_t start = clock(); // 开始计时
RecommondMu(userReference, UserMusic, threshold, recommendMusic, RecommendIndex, requireSongs);
clock_t end = clock(); // 结束计时
double elapsedTime = double(end - start) / CLOCKS_PER_SEC; // 计算运行时间
for (int i = 0; i < requireSongs; i++) {
printf("音乐%d,推荐者:User%d\n", recommendMusic[i] + 1, RecommendIndex[i]);
}
cout << "用户数: " << NoUser << "音乐数:" << NoSongs << endl;
cout << "运行时间:" << elapsedTime << " seconds" << endl;
}
可见,算法的时间复杂度为O(NoUser * NoSongs + NoUser * NoUser)
但是有一个问题就是测试数据为1000时就会堆内存溢出,希望有人可以解决