构建基于用户评分的图书推荐应用,核心是通过分析不同用户的评分数据,找到兴趣相似的用户群体,再基于相似用户的评分记录为目标用户推荐尚未阅读的书籍。整个流程需要设计合理的数据存储结构,实现评分数据的读写,以及核心的推荐算法逻辑。

核心数据结构设计
首先需要定义存储图书信息、用户信息和评分记录的结构体,方便后续的数据操作。我们可以用内存存储的方式简化实现,实际生产中可以替换为数据库存储。
package main
import (
"fmt"
"sort"
)
// 图书结构体,存储图书基础信息
type Book struct {
ID int // 图书唯一ID
Name string // 图书名称
Score float64 // 图书平均评分,后续计算用
}
// 用户结构体,存储用户基础信息
type User struct {
ID int // 用户唯一ID
Name string // 用户名称
}
// 评分记录结构体,存储用户对图书的评分
type Rating struct {
UserID int // 评分用户ID
BookID int // 被评分图书ID
Score float64 // 评分值,范围1-5
}
评分数据初始化
我们可以先初始化一批测试数据,包含多本图书、多个用户以及对应的评分记录,方便后续测试推荐逻辑。
// 初始化图书数据
func initBooks() map[int]Book {
books := make(map[int]Book)
books[1] = Book{ID: 1, Name: "Go语言编程", Score: 0}
books[2] = Book{ID: 2, Name: "算法导论", Score: 0}
books[3] = Book{ID: 3, Name: "人类简史", Score: 0}
books[4] = Book{ID: 4, Name: "三体", Score: 0}
books[5] = Book{ID: 5, Name: "代码整洁之道", Score: 0}
return books
}
// 初始化用户数据
func initUsers() map[int]User {
users := make(map[int]User)
users[1] = User{ID: 1, Name: "张三"}
users[2] = User{ID: 2, Name: "李四"}
users[3] = User{ID: 3, Name: "王五"}
return users
}
// 初始化评分数据
func initRatings() []Rating {
ratings := []Rating{
{UserID: 1, BookID: 1, Score: 4.5},
{UserID: 1, BookID: 2, Score: 5.0},
{UserID: 1, BookID: 3, Score: 3.0},
{UserID: 2, BookID: 1, Score: 4.0},
{UserID: 2, BookID: 2, Score: 4.5},
{UserID: 2, BookID: 4, Score: 5.0},
{UserID: 3, BookID: 3, Score: 4.0},
{UserID: 3, BookID: 4, Score: 4.5},
{UserID: 3, BookID: 5, Score: 5.0},
}
return ratings
}
计算用户相似度
基于用户评分的推荐常用皮尔逊相关系数计算用户之间的相似度,系数越接近1说明两个用户兴趣越相似。我们需要先整理每个用户的评分映射,再计算两两用户之间的相似度。
// 整理用户评分映射,key为用户ID,value为图书ID到评分的映射
func buildUserRatingMap(ratings []Rating) map[int]map[int]float64 {
userRatingMap := make(map[int]map[int]float64)
for _, r := range ratings {
if _, ok := userRatingMap[r.UserID]; !ok {
userRatingMap[r.UserID] = make(map[int]float64)
}
userRatingMap[r.UserID][r.BookID] = r.Score
}
return userRatingMap
}
// 计算两个用户的皮尔逊相关系数
func calcPearsonCorrelation(user1Ratings, user2Ratings map[int]float64) float64 {
// 找到两个用户共同评分的图书
commonBooks := make([]int, 0)
for bookID := range user1Ratings {
if _, ok := user2Ratings[bookID]; ok {
commonBooks = append(commonBooks, bookID)
}
}
// 没有共同评分则相似度为0
if len(commonBooks) == 0 {
return 0
}
// 计算评分和、平方和、乘积和
sum1 := 0.0
sum2 := 0.0
sum1Sq := 0.0
sum2Sq := 0.0
sumProduct := 0.0
for _, bookID := range commonBooks {
score1 := user1Ratings[bookID]
score2 := user2Ratings[bookID]
sum1 += score1
sum2 += score2
sum1Sq += score1 * score1
sum2Sq += score2 * score2
sumProduct += score1 * score2
}
n := float64(len(commonBooks))
// 计算分子分母
numerator := sumProduct - (sum1 * sum2 / n)
denominator := (sum1Sq - sum1*sum1/n) * (sum2Sq - sum2*sum2/n)
if denominator == 0 {
return 0
}
return numerator / denominator
}
生成推荐列表
得到用户相似度后,我们可以为目标用户找到最相似的K个用户,再收集这些相似用户评分较高但目标用户未读过的图书,按加权评分排序后输出推荐结果。
// 为用户生成推荐图书列表
func recommendBooks(targetUserID int, ratings []Rating, books map[int]Book, topK int) []Book {
userRatingMap := buildUserRatingMap(ratings)
targetRatings, ok := userRatingMap[targetUserID]
if !ok {
fmt.Println("目标用户不存在评分记录")
return nil
}
// 计算目标用户和其他所有用户的相似度
type userSim struct {
UserID int
Sim float64
}
simList := make([]userSim, 0)
for userID, ratings := range userRatingMap {
if userID == targetUserID {
continue
}
sim := calcPearsonCorrelation(targetRatings, ratings)
simList = append(simList, userSim{UserID: userID, Sim: sim})
}
// 按相似度降序排序,取前topK个用户
sort.Slice(simList, func(i, j int) bool {
return simList[i].Sim > simList[j].Sim
})
if len(simList) > topK {
simList = simList[:topK]
}
// 收集候选图书,计算加权评分
candidateBooks := make(map[int]float64)
for _, us := range simList {
similarUserRatings := userRatingMap[us.UserID]
for bookID, score := range similarUserRatings {
// 跳过目标用户已经评分过的图书
if _, ok := targetRatings[bookID]; ok {
continue
}
// 加权评分:相似度 * 评分
candidateBooks[bookID] += us.Sim * score
}
}
// 按加权评分降序排序,生成推荐列表
type bookScore struct {
BookID int
Score float64
}
bsList := make([]bookScore, 0)
for bookID, score := range candidateBooks {
bsList = append(bsList, bookScore{BookID: bookID, Score: score})
}
sort.Slice(bsList, func(i, j int) bool {
return bsList[i].Score > bsList[j].Score
})
// 转换为图书结构体列表
result := make([]Book, 0)
for _, bs := range bsList {
book := books[bs.BookID]
book.Score = bs.Score
result = append(result, book)
}
return result
}
完整调用示例
最后我们可以编写主函数,初始化数据后为目标用户生成推荐列表,验证推荐逻辑的正确性。
func main() {
books := initBooks()
users := initUsers()
ratings := initRatings()
// 为目标用户1生成推荐,取最相似的2个用户
targetUserID := 1
recommendList := recommendBooks(targetUserID, ratings, books, 2)
fmt.Printf("为用户 %s 推荐的图书:n", users[targetUserID].Name)
if len(recommendList) == 0 {
fmt.Println("暂无推荐图书")
return
}
for _, book := range recommendList {
fmt.Printf("图书名称:%s,推荐评分:%.2fn", book.Name, book.Score)
}
}
优化方向
上述实现是基础的内存版本,实际落地时可以做更多优化。比如将评分数据存储到MySQL或者Redis中,提升数据读写性能;可以引入更多特征,比如图书的分类、标签信息,优化相似度计算逻辑;还可以增加评分数据的实时更新逻辑,让推荐结果更及时反映用户的最新偏好。如果推荐数据量较大,也可以考虑使用Go的并发特性优化相似度计算的速度,提升推荐效率。