推荐算法之协同过滤及其改进与实现
前言
因为我的项目和简历中都提及了推荐算法,也有几次面试官问到,现在想想回答的虽然没有问题,但是总感觉表述不是很清晰。今天抽空把推荐算法理一理。
协同过滤算法
因为我使用的推荐算法是基于协同过滤,所以我们先聊聊协同过滤算法吧。
协同过滤(Collaborative Filtering,简称CF)是一种最经典的推荐算法,这个算法的出现对于推荐系统具有划时代的意义。1992年第一次提出协同过滤算法。
协同过滤算法主要通过分析用户的历史数据,用以构建用户模型并进行推荐。协同过滤算法主要分为以下两类:一类是基于用户的协同过滤算法(User-Based Collaborative Filtering,简称UserCF),另一类是基于物品的协同过滤算法(Item-Based Collaborative Filtering,简称ItemCF)。
(1)基于用户的协同过滤算法
UserCF算法的核心是用户,思想是“人以群分”。算法的基本思路是:首先找到与目标用户喜好相似的邻居用户集体,然后以此为基础计算出目标用户对未操作物品的喜好预测评分,根据评分的高低推荐前N个给目标用户。
如图3-3所示,形象的显示了基于用户的协同过滤算法的一般概念:假设用户C为目标用户,根据图3-3可知,因为用户A和用户C由相同评分物品C和D,可知用户C的相似用户为用户A;又因为用户A已评分的集合物品A是用户C未操作的,所以将物品A推荐给目标用户C。
(2)基于物品的协同过滤算法
ItemCF算法的核心是物品,思想是“物以类聚”。算法的基本思路是:首先计算物品之间的相似度,然后根据目标用户的历史行为,将与之相似度较高的物品推荐给目标用户。
基于物品的协同过滤算法的理论如图3-4所示,假设用户A为目标用户,根据图3-4可知,因为物品A和物品C同时被用户B和用户C评分,因此物品C的相似邻居为物品A,又因为目标用户A对物品C已评分,所以将物品A推荐给目标用户A。
改进的协同过滤算法
传统的协同过滤推荐算法的思想如下。
(1)使用评分计算其他用户和目标用户的相似度;
(2)找出K个和目标用户相似度最高的邻居用户;
(3)向目标用户推荐邻居喜欢的物品且目标用户没有操作过的物品。
- 音乐推荐应用场景中,传统的协同过滤算法有如下几点可以改进。
(1)用户相似度计算的改进
传统的协同过滤算法在音乐推荐时实现的思想是,构建一个稀疏的用户-歌曲评分矩阵,根据用户对歌曲的评分(如收藏、分享、加心心等操作),计算出用户的相似度。
但对于音乐方面,我认为用户偏好并不是评分就能笼统表达的,仅利用评分的填充会使得填充后的矩阵出现一定偏差,比如两个用户都收藏过《海阔天空》,这并不能说明他们相似度高,因为听歌时还带有用户的情感,可能用户A感觉斗志昂扬,但用户B感觉情绪低落。如果两个用户的感受相似,则可以更好地说明他们的相似性。
因此,针对上述这个问题,考虑到用户听歌时带有情感性,引入情感相似度,以此来提高系统推荐的准确性,改善用户的体验。基于音乐互动模块对用户情感进行分析,将用户情感数值化,基于情感相似度结合协同过滤推荐算法找出与目标用户最相似的邻居用户。
此改进方法的具体思想如下。
首先,通过音乐互动模块将用户互动所得的用户情感相似性数值化后,使用欧几里德距离公式计算用户间的情感相似度。欧几里德度量公式是一种简单易懂的用以计算相似度的方法。它是将用户共同参与的互动作为坐标轴,然后,将参与互动的用户放置在坐标系中,并计算两个用户之间的直线距离。在二维坐标中,两用户的欧几里德距离如图3-5所示。
由此可推导到i维坐标中,两用户之间的欧几里德距离如公式(4.1)所示。
上述欧几里德距离公式计算出来的是一个大于或等于0的数,使用公式(4.2)将其规范到(0, 1]之间,以便更直观地反映用户之间的相似度。
可以将数据库中的数据构建出一个用户-互动分值矩阵,如图3-6所示,通过矩阵的构建和计算可以找到u1的情感相似用户u3和u5。
用户u和用户v的情感相似度计算公式如(4.3)所示。
其中,N(u)表示用户u已经参与过的互动集合,N(v)表示用户v已经参与过的互动集合。u(i)表示用户u对互动i的偏好,v(i)表示用户v对互动i的偏好。n表示用户u和用户v共同参与过的互动数。
(2)加权系数惩罚热门物品
在音乐场景下经常产生热门歌曲,如果热门歌曲出现次数较多,就会影响实际相似度的计算结果,从而导致推荐的歌曲都是热门歌曲,无法满足用户的实际需求。
为了减小这种影响,可以考虑加入一个加权系数用以惩罚热门歌曲的影响,即惩罚因子。
因此本文对相似度的计算公式加以改进,将歌曲出现次数的倒数作为惩罚因子。歌曲出现的次数越多,即改歌曲越热门,同时,该歌曲对用户喜好相似度的贡献则越少。带有惩罚因子的公式可减弱热门歌曲造成的影响,改进后的公式如(4.4)所示。
其中,N(i)表示歌曲i出现的次数,可以看出,该公式加入歌曲出现次数的倒数计算用户u和用户v的共同爱好列表中的相似度,从而惩罚了热门歌曲的影响。
改进后的算法的具体流程,如图3-7所示。
- 改进后的算法的具体步骤如下。
(1)用户情感分析信息、用户评分信息。
(2)在音乐互动分析的用户情感信息上,进行情感数值化构建用户情感矩阵,计算用户情感相似度。
(3)根据用户情感相似度找出目标用户的K个最近邻居集,并按递减顺序将这些结果值排序。
(4)根据用户听歌的历史行为,构建用户评分矩阵,并加入惩罚因子,计算用户评分相似度。
(5)将最终评分值由高到低排序,并将排序结果推荐给目标用户。
(6)输出目标用户的推荐用户和推荐歌曲集合。
具体实现
先贴下推荐算法时序图,如下。
如果熟悉的朋友可能看出来了,这是基于Mahout框架实现的。在架子里面实现自己逻辑即可。
这里主要贴出重点的代码块,详细点击文末阅读原文移步GitHub。
1、请求入口
请求入口和相应参数设置,很简单就不多解释了。
- RecommendController
@Controller
@RequestMapping("/recommendAction")
public class RecommendController {
private static final Logger log = LoggerFactory.getLogger(RecommendController.class);
final static int NEIGHBORHOOD_NUM = 3; //用户邻居数量
final static int RECOMMENDER_NUM = 3; //推荐结果个数
static DataModel dataModel = null; //Mahout提供的数据模型(用于将数据库的数据转为带构建的数据模型)
如下代码构建推荐系统,并调用相关的方法。
//基于用户的协同过滤算法,基于物品的协同过滤算法
UserSimilarity user = new EuclideanDistanceSimilarity(dataModel); //计算欧式距离,欧式距离来定义相似性,用s=1/(1+d)来表示,范围在[0,1]之间,值越大,表明d越小,距离越近,则表示相似性越大
//指定用户邻居数量
NearestNUserNeighborhood neighbor = new NearestNUserNeighborhood(NEIGHBORHOOD_NUM, user, dataModel);
//构建基于用户的推荐系统
Recommender r = new GenericUserBasedRecommender(dataModel, neighbor, user);
//获取目标用户的K个最近邻居集
long[] theNeighborhood = r.recommendUser(userID, RECOMMENDER_NUM);
//获取最终推荐结果
List<MyRec> myRecList = r.recommendSong(theNeighborhood, userID, RECOMMENDER_NUM);
2、推荐实现类(获取最近K邻居)
- GenericUserBasedRecommender
/**
* 推荐方法:获取目标用户的邻居用户id
* @param userID 需要推荐的目标用户id
* @param howMany 推荐结果个数
* @return 邻居用户id数组
* @throws TasteException
*/
@Override
public long[] recommendUser(long userID, int howMany) throws TasteException {
Preconditions.checkArgument(howMany >= 1, "howMany must be at least 1");
log.debug("Recommending items for user ID '{}'", userID);
long[] theNeighborhood = neighborhood.getUserNeighborhood(userID);
System.out.println(userID+"'s theNeighborhood:"+Arrays.toString(theNeighborhood));
return theNeighborhood;
}
/**
* 推荐方法:根据邻居用户id推荐最喜欢的歌曲
* @param theNeighborhood
* @param userID
* @param howMany
* @return 推荐歌曲集合
* @throws TasteException
*/
@Override
public List<MyRec> recommendSong(long[] theNeighborhood, long userID, int howMany) throws TasteException {
List<MyRec> myRecList = new ArrayList<>();
for (long oneNeighborhood : theNeighborhood) {
FastIDSet theItemIDs = getTheItems(oneNeighborhood, userID);
TopItems.Estimator<Long> estimator = new Estimator(userID, null , oneNeighborhood);
List<RecommendedItem> topItems = TopItems.getTopSongs(howMany, theItemIDs.iterator(),null, estimator);
log.debug("Recommendations are: {}", topItems);
System.out.println("Recommendations: userId:"+oneNeighborhood + " ,songs:"+topItems);
User user = new User();
user.setId((int)oneNeighborhood);
List<Song> songList = new ArrayList<>();
for (int i = 0; i<topItems.size(); i++){
Song song = new Song();
song.setId((int)topItems.get(i).getItemID());
songList.add(song);
}
MyRec myRec = new MyRec();
myRec.setUser(user);
myRec.setSongList(songList);
myRecList.add(myRec);
System.out.println("myRecList:"+ myRecList);
}
return myRecList;
}
- NearestNUserNeighborhood
/**
* 获取目标用户的邻居用户
* @param userID
* ID of user for which a neighborhood will be computed
* @return
* @throws TasteException
*/
@Override
public long[] getUserNeighborhood(long userID) throws TasteException {
DataModel dataModel = getDataModel();
UserSimilarity userSimilarityImpl = getUserSimilarity();
TopItems.Estimator<Long> estimator = new Estimator(userSimilarityImpl, userID, minSimilarity);
LongPrimitiveIterator userIDs = SamplingLongPrimitiveIterator.maybeWrapIterator(dataModel.getUserIDs(),
getSamplingRate());
return TopItems.getTopUsers(n, userIDs, null, estimator);
}
- TopItems.getTopUsers:这里就是通过优先队列获得K个最近邻居集(不懂的朋友可以参看我之前文章写的 Top K问题 )
/**
* 根据相似度排序邻居用户
* @param howMany
* @param allUserIDs
* @param rescorer
* @param estimator
* @return
* @throws TasteException
*/
public static long[] getTopUsers(int howMany,
LongPrimitiveIterator allUserIDs,
IDRescorer rescorer,
Estimator<Long> estimator) throws TasteException {
Queue<SimilarUser> topUsers = new PriorityQueue<SimilarUser>(howMany + 1, Collections.reverseOrder());
boolean full = false;
double lowestTopValue = Double.NEGATIVE_INFINITY;
while (allUserIDs.hasNext()) {
long userID = allUserIDs.next();
if (rescorer != null && rescorer.isFiltered(userID)) {
continue;
}
double similarity;
try {
similarity = estimator.estimate(userID);
} catch (NoSuchUserException nsue) {
continue;
}
double rescoredSimilarity = rescorer == null ? similarity : rescorer.rescore(userID, similarity);
if (!Double.isNaN(rescoredSimilarity) && (!full || rescoredSimilarity > lowestTopValue)) {
topUsers.add(new SimilarUser(userID, rescoredSimilarity));
if (full) {
topUsers.poll();
} else if (topUsers.size() > howMany) {
full = true;
topUsers.poll();
}
lowestTopValue = topUsers.peek().getSimilarity();
}
}
int size = topUsers.size();
if (size == 0) {
return NO_IDS;
}
List<SimilarUser> sorted = Lists.newArrayListWithCapacity(size);
sorted.addAll(topUsers);
Collections.sort(sorted);
long[] result = new long[size];
int i = 0;
for (SimilarUser similarUser : sorted) {
result[i++] = similarUser.getUserID();
}
return result;
}
- AbstractSimilarity. userSimilarity 市重点!这里就是将数据模型的数据提取出来并进行相关计算。
/**
* 估算目标用户和其他用户的相似度!!!
* @param userID1
* @param userID2
* @return
* @throws TasteException
*/
@Override
public double userSimilarity(long userID1, long userID2) throws TasteException {
DataModel dataModel = getDataModel();
PreferenceArray xPrefs = dataModel.getPreferencesFromUser(userID1);
PreferenceArray yPrefs = dataModel.getPreferencesFromUser(userID2);
int xLength = xPrefs.length();
int yLength = yPrefs.length();
if (xLength == 0 || yLength == 0) {
return Double.NaN;
}
long xIndex = xPrefs.getItemID(0);
long yIndex = yPrefs.getItemID(0);
int xPrefIndex = 0;
int yPrefIndex = 0;
double sumX = 0.0;
double sumX2 = 0.0;
double sumY = 0.0;
double sumY2 = 0.0;
double sumXY = 0.0;
double sumXYdiff2 = 0.0;
int count = 0;
boolean hasInferrer = inferrer != null;
boolean hasPrefTransform = prefTransform != null;
while (true) {
int compare = xIndex < yIndex ? -1 : xIndex > yIndex ? 1 : 0;
if (hasInferrer || compare == 0) {
double x;
double y;
if (xIndex == yIndex) {
// Both users expressed a preference for the item
if (hasPrefTransform) {
x = prefTransform.getTransformedValue(xPrefs.get(xPrefIndex));
y = prefTransform.getTransformedValue(yPrefs.get(yPrefIndex));
} else {
x = xPrefs.getValue(xPrefIndex);
y = yPrefs.getValue(yPrefIndex);
}
} else {
// Only one user expressed a preference, but infer the other one's preference and tally
// as if the other user expressed that preference
if (compare < 0) {
// X has a value; infer Y's
x = hasPrefTransform
? prefTransform.getTransformedValue(xPrefs.get(xPrefIndex))
: xPrefs.getValue(xPrefIndex);
y = inferrer.inferPreference(userID2, xIndex);
} else {
// compare > 0
// Y has a value; infer X's
x = inferrer.inferPreference(userID1, yIndex);
y = hasPrefTransform
? prefTransform.getTransformedValue(yPrefs.get(yPrefIndex))
: yPrefs.getValue(yPrefIndex);
}
}
sumXY += x * y;
sumX += x;
sumX2 += x * x;
sumY += y;
sumY2 += y * y;
double diff = x - y;
sumXYdiff2 += diff * diff;
count++;
}
if (compare <= 0) {
if (++xPrefIndex >= xLength) {
if (hasInferrer) {
// Must count other Ys; pretend next X is far away
if (yIndex == Long.MAX_VALUE) {
// ... but stop if both are done!
break;
}
xIndex = Long.MAX_VALUE;
} else {
break;
}
} else {
xIndex = xPrefs.getItemID(xPrefIndex);
}
}
if (compare >= 0) {
if (++yPrefIndex >= yLength) {
if (hasInferrer) {
// Must count other Xs; pretend next Y is far away
if (xIndex == Long.MAX_VALUE) {
// ... but stop if both are done!
break;
}
yIndex = Long.MAX_VALUE;
} else {
break;
}
} else {
yIndex = yPrefs.getItemID(yPrefIndex);
}
}
}
// "Center" the data. If my math is correct, this'll do it.
double result;
if (centerData) {
double meanX = sumX / count;
double meanY = sumY / count;
// double centeredSumXY = sumXY - meanY * sumX - meanX * sumY + n * meanX * meanY;
double centeredSumXY = sumXY - meanY * sumX;
// double centeredSumX2 = sumX2 - 2.0 * meanX * sumX + n * meanX * meanX;
double centeredSumX2 = sumX2 - meanX * sumX;
// double centeredSumY2 = sumY2 - 2.0 * meanY * sumY + n * meanY * meanY;
double centeredSumY2 = sumY2 - meanY * sumY;
result = computeResult(count, centeredSumXY, centeredSumX2, centeredSumY2, sumXYdiff2);
} else {
result = computeResult(count, sumXY, sumX2, sumY2, sumXYdiff2);
}
if (similarityTransform != null) {
result = similarityTransform.transformSimilarity(userID1, userID2, result);
}
if (!Double.isNaN(result)) {
result = normalizeWeightResult(result, count, cachedNumItems);
}
return result;
}
- EuclideanDistanceSimilarity:最终调用到这里的欧几里德距离计算公式
@Override
double computeResult(int n, double sumXY, double sumX2, double sumY2, double sumXYdiff2) {
return 1.0 / (1.0 + Math.sqrt(sumXYdiff2) / Math.sqrt(n));
}
以上就是获取K个最近邻居的整体流程。
注:传统的协同过滤推荐算法并不会直接返回最近K邻居,而是根据用户对歌曲的评分矩阵,返回推荐的歌曲。
而我因为业务需要,需要返回最近K邻居,并且在通过最近K邻居和歌曲评分矩阵,返回相应的推荐歌曲。
3、推荐实现类(获取相应的推荐歌曲)
- GenericUserBasedRecommender
/**
* 推荐方法:根据邻居用户id推荐最喜欢的歌曲
* @param theNeighborhood
* @param userID
* @param howMany
* @return 推荐歌曲集合
* @throws TasteException
*/
@Override
public List<MyRec> recommendSong(long[] theNeighborhood, long userID, int howMany) throws TasteException {
List<MyRec> myRecList = new ArrayList<>();
for (long oneNeighborhood : theNeighborhood) {
FastIDSet theItemIDs = getTheItems(oneNeighborhood, userID);
TopItems.Estimator<Long> estimator = new Estimator(userID, null , oneNeighborhood);
List<RecommendedItem> topItems = TopItems.getTopSongs(howMany, theItemIDs.iterator(),null, estimator);
log.debug("Recommendations are: {}", topItems);
System.out.println("Recommendations: userId:"+oneNeighborhood + " ,songs:"+topItems);
User user = new User();
user.setId((int)oneNeighborhood);
List<Song> songList = new ArrayList<>();
for (int i = 0; i<topItems.size(); i++){
Song song = new Song();
song.setId((int)topItems.get(i).getItemID());
songList.add(song);
}
MyRec myRec = new MyRec();
myRec.setUser(user);
myRec.setSongList(songList);
myRecList.add(myRec);
System.out.println("myRecList:"+ myRecList);
}
return myRecList;
}
- GenericUserBasedRecommender:注:这里有和传统的不同,我没有去除目标用户已接触过的物品
private FastIDSet getTheItems(long oneNeighborhood, long theUserID) throws TasteException {
DataModel dataModel = getDataModel();
FastIDSet possibleItemIDs = new FastIDSet();
// 添加所有邻居用户的物品
possibleItemIDs.addAll(dataModel.getItemIDsFromUser(oneNeighborhood));
// 去除目标用户已接触过的物品
// possibleItemIDs.removeAll(dataModel.getItemIDsFromUser(theUserID));
return possibleItemIDs;
}
- TopItems:同理获取推荐歌曲
public static List<RecommendedItem> getTopSongs(int howMany,
LongPrimitiveIterator possibleItemIDs,
IDRescorer rescorer,
Estimator<Long> estimator) throws TasteException {
Preconditions.checkArgument(possibleItemIDs != null, "argument is null");
Preconditions.checkArgument(estimator != null, "argument is null");
Queue<RecommendedItem> topItems = new PriorityQueue<RecommendedItem>(howMany + 1,
Collections.reverseOrder(ByValueRecommendedItemComparator.getInstance()));
boolean full = false;
double lowestTopValue = Double.NEGATIVE_INFINITY;
while (possibleItemIDs.hasNext()) {
long itemID = possibleItemIDs.next();
if (rescorer == null || !rescorer.isFiltered(itemID)) {
double preference;
try {
preference = estimator.estimateSong(itemID);
} catch (NoSuchItemException nsie) {
continue;
}
double rescoredPref = rescorer == null ? preference : rescorer.rescore(itemID, preference);
// if (Double.isNaN(rescoredPref)) {
// rescoredPref = 0;
// }
if (!Double.isNaN(rescoredPref) && (!full || rescoredPref > lowestTopValue)) {
topItems.add(new GenericRecommendedItem(itemID, (float) rescoredPref));
if (full) {
topItems.poll();
} else if (topItems.size() > howMany) {
full = true;
topItems.poll();
}
lowestTopValue = topItems.peek().getValue();
}
}
}
int size = topItems.size();
if (size == 0) {
return Collections.emptyList();
}
List<RecommendedItem> result = Lists.newArrayListWithCapacity(size);
result.addAll(topItems);
Collections.sort(result, ByValueRecommendedItemComparator.getInstance());
return result;
}
- GenericUserBasedRecommender.estimateSong:
@Override
public double estimateSong(Long itemID) throws TasteException {
return doEstimatePreferenceSong(theUserID, oneNeighborhood, itemID);
}
- GenericUserBasedRecommender.doEstimatePreferenceSong:评估歌曲(注:这里我也有所改动)
protected float doEstimatePreferenceSong(long theUserID, long oneNeighborhood, long itemID) throws TasteException {
DataModel dataModel = getDataModel();
double preference = 0.0;
double totalSimilarity = 0.0;
int count = 0;
if (oneNeighborhood != theUserID) {
// See GenericItemBasedRecommender.doEstimatePreference() too
Float pref = dataModel.getPreferenceValue(oneNeighborhood, itemID);
if (pref != null) {
double theSimilarity = similarity.userSimilarity(theUserID, oneNeighborhood);
if (!Double.isNaN(theSimilarity)) {
preference += theSimilarity * pref;
totalSimilarity += theSimilarity;
count++;
}else {
// preference += pref;
// totalSimilarity += theSimilarity;
// count++;
return pref;
}
}
}
// if (count <= 1) {
// return Float.NaN;
// }
float estimate = (float) (preference / totalSimilarity);
if (capper != null) {
estimate = capper.capEstimate(estimate);
}
return estimate;
}
4、 封装返回结果
- RecommendController:在controller层封装结果返回给前端
jsonResult jr = new jsonResult();
List<MyRec> myRecInfoList = getRecInfo(myRecList);
jr.add(myRecInfoList);
System.out.println("myRecommend json---:"+jr);
return jr;
- RecommendController:调用方法
public List<MyRec> getRecInfo(List<MyRec> myRecList) {
UserServiceDao userService = new UserServiceDao();
SongServiceDao songService = new SongServiceDao();
System.out.println("get myRecList:" + myRecList);
// 循环遍历到数据库中查询详细信息
List<MyRec> myRecListInfo = new ArrayList<>();
for (MyRec myRec : myRecList) {
System.out.println("myRec.getUser().getUserId():" + myRec.getUser().getId());
// 获取用户id
Integer userId = myRec.getUser().getId();
// 查询用户详情信息
User user = userService.getUserInfoById(userId);
System.out.println("user:" + user);
// 设置更新后的用户
myRec.setUser(user);
// myRecListInfo.add(myRec);
System.out.println("myRec.getSongList():" + myRec.getSongList());
List<Song> songList = new ArrayList<>();
for (Song song : myRec.getSongList()) {
// System.out.println("song.getSongId():"+song.getSongId());
Integer songId = song.getId();
// 获取歌曲详细信息
song = songService.getSongInfoById(songId);
// System.out.println("song:"+song);
// 如果本地曲库有这首歌则添加。否则加的是null影响体验
if (song.getId() != null){
songList.add(song);
}
}
myRec.setSongList(songList);
// 添加myRec到数组中
myRecListInfo.add(myRec);
}
System.out.println("myRecListInfo:" + myRecListInfo);
return myRecListInfo;
}
以上。大致就是推荐功能的重点代码块(注:惩罚因子目前并没实现)。如需完整代码,欢迎前往全球最大同性交友网站
GayHub哦不GitHub查看(由于时间和经验的限制,代码写得可能不是很好,但整体还是可供参考的)
- 哦!顺便推广一下实现推荐算法的这个作品 “寻找最佳音缘”,是一款微信小程序,正式版已经发布上线,可以直接在微信中搜索“寻找最佳音缘”或者扫下图小程序码进入。主要功能是音乐服务(希望不要有网易云音乐的伙伴给我发律师函,至于为什么你用了就知道啦hhh)这个作品完全是出于自己对音乐的喜爱和臆想,从设计到实现以及部署都是自己完全的,还是希望大家支持一下吧~ 给我加加用户量或者GitHub点小星星!说不定面试官会给我加分呢 😄
最后的总结和絮叨
总结一下,我实现的推荐算法是基于协同过滤算法。主要的流程如下:
- 通过音乐互动分析用户的情感;
- 使用情感计算其他用户和目标用户的相似度;
- 找出K个和目标用户相似度最高的邻居用户;
- 向目标用户推荐邻居喜欢的歌曲。
所以,这个项目中存在推荐系统常见的问题:冷启动问题(如果你是新用户或者未进行音乐互动相关操作,则无法对你进行推荐),针对这个问题,很多系统给出的解决方案是,新用户注册后需要选择一些感兴趣的领域,或者根据用户的注册信息进行推荐。但我目前还未优化,因为当时这个作品用作毕设答辩,改动的话增加麻烦,先把答辩过了再说啦 😄
关于推荐算法我其实不是很懂,因为毕设作品需要,我就借了本 《推荐系统实践》(作者:项亮) 学习, 这本书还是很值得推荐的,适合入门和上手。
最后,下图是个人的公众号,欢迎朋友萌学习交流~