HarmonyOS开发:协同过滤推荐引擎构建
HarmonyOS开发:协同过滤推荐引擎构建
核心要点:协同过滤是推荐系统中最经典的算法族,本文深入讲解UserCF和ItemCF两种核心算法的数学原理,并在HarmonyOS端侧实现完整的协同过滤引擎,涵盖相似度计算、邻居选择、评分预测等关键环节,同时解决稀疏矩阵存储和冷启动等端侧特有挑战。
| 项目 | 说明 |
|---|---|
| 开发语言 | ArkTS |
| 关键能力 | 稀疏矩阵运算、相似度计算、邻居聚合 |
一、背景与动机
“喜欢这个的人也喜欢……”——这句话你一定不陌生。它背后就是协同过滤(Collaborative Filtering)的思想:找到和你品味相似的人,把他们喜欢的东西推荐给你。
协同过滤的魅力在于它不依赖物品内容,纯粹从用户行为中挖掘规律。你不需要知道一部电影是什么类型、谁导演的,只要知道"看过A的人也看了B",就能做出推荐。这种"涌现式"的推荐能力,让协同过滤成为推荐系统的基石算法。
但在HarmonyOS端侧实现协同过滤,挑战不小:
- 用户-物品矩阵稀疏:端侧只有单个用户的数据,如何做UserCF?
- 相似度计算量大:全量物品对的相似度计算,端侧能扛住吗?
- 存储空间有限:相似度矩阵怎么存才能不爆内存?
别急,我们一个个来解决。
二、核心原理
2.1 协同过滤的分类
协同过滤主要分为两大流派:
flowchart TB
classDef primary fill:#4A90D9,stroke:#2C5F8A,color:#fff,font-weight:bold
classDef warning fill:#F5A623,stroke:#C7841A,color:#fff,font-weight:bold
classDef error fill:#D0021B,stroke:#9B0214,color:#fff,font-weight:bold
classDef info fill:#7B68EE,stroke:#5B48CE,color:#fff,font-weight:bold
classDef purple fill:#9B59B6,stroke:#7D3C98,color:#fff,font-weight:bold
CF[协同过滤]:::primary --> UserCF[基于用户的协同过滤]:::info
CF --> ItemCF[基于物品的协同过滤]:::warning
CF --> ModelCF[基于模型的协同过滤]:::purple
UserCF --> U1[找相似用户]:::info
UserCF --> U2[聚合邻居偏好]:::info
UserCF --> U3[推荐邻居喜欢物品]:::info
ItemCF --> I1[计算物品相似度]:::warning
ItemCF --> I2[找相似物品]:::warning
ItemCF --> I3[推荐相似物品]:::warning
ModelCF --> M1[矩阵分解 SVD]:::purple
ModelCF --> M2[隐语义模型 LFM]:::purple
ModelCF --> M3[深度学习模型]:::purple
2.2 UserCF:找到你的"同好"
UserCF的核心思想:和你品味相似的人喜欢的东西,你也可能喜欢。
数学表达:
其中:
- :用户u对物品i的预测评分
- :用户u的邻居集合
- :用户u和v的相似度
- :用户v对物品i的实际评分
端侧困境:单个设备只有一个用户的数据,无法计算用户间相似度。
解决方案:
- 云端预计算用户相似度,端侧只做邻居聚合
- 利用HarmonyOS分布式能力,跨设备共享匿名行为模式
- 端侧用"隐式用户画像"替代显式用户相似度
2.3 ItemCF:物以类聚
ItemCF的核心思想:和你之前喜欢的东西相似的东西,你也可能喜欢。
数学表达:
其中:
- :用户u交互过的物品集合
- :物品i和j的相似度
端侧优势:物品相似度可以云端预计算,端侧只需查表+加权求和,计算量可控!
2.4 相似度计算方法
| 方法 | 公式 | 特点 |
|---|---|---|
| 余弦相似度 | 不考虑评分均值差异 | |
| 皮尔逊相关系数 | 消除评分偏差 | |
| Jaccard系数 | $J(A,B) = \frac{ | A \cap B |
2.5 稀疏矩阵存储策略
用户-物品评分矩阵通常是极稀疏的(稀疏度>99%),端侧必须使用压缩存储:
- COO格式(坐标存储):记录非零元素的(行,列,值)三元组
- CSR格式(压缩稀疏行):按行压缩,适合行遍历
- 自定义Map嵌套:
Map<userId, Map<itemId, score>>,最灵活
端侧推荐我们选择Map嵌套,因为:
- 查询效率O(1)
- 天然只存储非零元素
- ArkTS的Map操作原生支持
三、代码实战
3.1 相似度计算工具
先实现核心的相似度计算工具类。
// SimilarityCalculator.ets - 相似度计算工具
/**
* 向量相似度计算器
* 支持余弦相似度、皮尔逊相关系数、Jaccard系数
*/
export class SimilarityCalculator {
/**
* 余弦相似度
* 衡量两个向量方向的相似程度,不考虑大小
* 适用于:评分向量、特征向量
*/
static cosineSimilarity(vecA: Map<string, number>, vecB: Map<string, number>): number {
let dotProduct = 0
let normA = 0
let normB = 0
// 计算点积和模长
vecA.forEach((valueA, key) => {
const valueB = vecB.get(key)
if (valueB !== undefined) {
dotProduct += valueA * valueB
}
normA += valueA * valueA
})
vecB.forEach((valueB) => {
normB += valueB * valueB
})
const denominator = Math.sqrt(normA) * Math.sqrt(normB)
if (denominator === 0) return 0
return dotProduct / denominator
}
/**
* 皮尔逊相关系数
* 消除用户评分偏差的影响
* 适用于:显式评分数据
*/
static pearsonCorrelation(vecA: Map<string, number>, vecB: Map<string, number>): number {
// 找出共同评分的物品
const commonKeys: string[] = []
vecA.forEach((_, key) => {
if (vecB.has(key)) {
commonKeys.push(key)
}
})
// 至少需要2个共同评分项
if (commonKeys.length < 2) return 0
// 计算均值
let sumA = 0, sumB = 0
for (const key of commonKeys) {
sumA += vecA.get(key)!
sumB += vecB.get(key)!
}
const meanA = sumA / commonKeys.length
const meanB = sumB / commonKeys.length
// 计算协方差和标准差
let covariance = 0
let varianceA = 0
let varianceB = 0
for (const key of commonKeys) {
const diffA = vecA.get(key)! - meanA
const diffB = vecB.get(key)! - meanB
covariance += diffA * diffB
varianceA += diffA * diffA
varianceB += diffB * diffB
}
const denominator = Math.sqrt(varianceA) * Math.sqrt(varianceB)
if (denominator === 0) return 0
return covariance / denominator
}
/**
* Jaccard相似系数
* 衡量两个集合的重叠程度
* 适用于:隐式反馈(浏览、点击等)
*/
static jaccardSimilarity(setA: Set<string>, setB: Set<string>): number {
if (setA.size === 0 && setB.size === 0) return 0
let intersectionSize = 0
setA.forEach(item => {
if (setB.has(item)) intersectionSize++
})
const unionSize = setA.size + setB.size - intersectionSize
if (unionSize === 0) return 0
return intersectionSize / unionSize
}
/**
* 改进的余弦相似度(考虑评分均值)
* 结合了余弦相似度和皮尔逊系数的优点
*/
static adjustedCosineSimilarity(
vecA: Map<string, number>,
vecB: Map<string, number>,
globalMean: number
): number {
let dotProduct = 0
let normA = 0
let normB = 0
vecA.forEach((valueA, key) => {
const valueB = vecB.get(key)
if (valueB !== undefined) {
const adjustedA = valueA - globalMean
const adjustedB = valueB - globalMean
dotProduct += adjustedA * adjustedB
}
const adjA = valueA - globalMean
normA += adjA * adjA
})
vecB.forEach((valueB) => {
const adjB = valueB - globalMean
normB += adjB * adjB
})
const denominator = Math.sqrt(normA) * Math.sqrt(normB)
if (denominator === 0) return 0
return dotProduct / denominator
}
/**
* 计算物品间的相似度矩阵
* 使用Jaccard系数(隐式反馈场景)
*/
static buildItemSimilarityMatrix(
itemUserMap: Map<string, Set<string>>,
topK: number = 50
): Map<string, Map<string, number>> {
const similarityMatrix: Map<string, Map<string, number>> = new Map()
const items = Array.from(itemUserMap.keys())
console.info(`[Similarity] 开始计算 ${items.length} 个物品的相似度矩阵`)
for (let i = 0; i < items.length; i++) {
const itemA = items[i]
const usersA = itemUserMap.get(itemA)!
const neighbors: Map<string, number> = new Map()
for (let j = 0; j < items.length; j++) {
if (i === j) continue
const itemB = items[j]
const usersB = itemUserMap.get(itemB)!
const sim = SimilarityCalculator.jaccardSimilarity(usersA, usersB)
if (sim > 0) {
neighbors.set(itemB, sim)
}
}
// 只保留TopK最相似的邻居
const sortedNeighbors = Array.from(neighbors.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, topK)
similarityMatrix.set(itemA, new Map(sortedNeighbors))
}
console.info(`[Similarity] 相似度矩阵计算完成`)
return similarityMatrix
}
}
3.2 ItemCF推荐引擎
核心的基于物品的协同过滤推荐引擎。
// ItemCFEngine.ets - 基于物品的协同过滤引擎
import { SimilarityCalculator } from './SimilarityCalculator'
/**
* 评分记录
*/
export interface RatingRecord {
userId: string
itemId: string
rating: number // 评分值(显式评分)或权重值(隐式反馈)
timestamp: number
}
/**
* ItemCF推荐结果
*/
export interface ItemCFRecommendation {
itemId: string
score: number
similarItems: string[] // 推荐依据的相似物品
confidence: number // 推荐置信度
}
/**
* 基于物品的协同过滤推荐引擎
*
* 核心思路:
* 1. 构建用户-物品评分矩阵(稀疏存储)
* 2. 计算物品间相似度(可云端预计算)
* 3. 根据用户历史行为,推荐相似物品
*/
export class ItemCFEngine {
// 用户-物品评分矩阵:userId -> (itemId -> rating)
private userItemMatrix: Map<string, Map<string, number>> = new Map()
// 物品-用户倒排索引:itemId -> Set<userId>
private itemUserIndex: Map<string, Set<string>> = new Map()
// 物品相似度矩阵:itemId -> (itemId -> similarity)
private itemSimilarityMatrix: Map<string, Map<string, number>> = new Map()
// 物品平均评分
private itemMeanRating: Map<string, number> = new Map()
// 全局平均评分
private globalMeanRating: number = 0
// 是否已计算相似度
private isSimilarityComputed: boolean = false
/**
* 加载评分数据
* 支持增量添加,无需一次性加载全部数据
*/
loadRatings(ratings: RatingRecord[]): void {
let totalRating = 0
let ratingCount = 0
for (const record of ratings) {
// 填充用户-物品矩阵
if (!this.userItemMatrix.has(record.userId)) {
this.userItemMatrix.set(record.userId, new Map())
}
this.userItemMatrix.get(record.userId)!.set(record.itemId, record.rating)
// 填充物品-用户倒排索引
if (!this.itemUserIndex.has(record.itemId)) {
this.itemUserIndex.set(record.itemId, new Set())
}
this.itemUserIndex.get(record.itemId)!.add(record.userId)
totalRating += record.rating
ratingCount++
}
this.globalMeanRating = ratingCount > 0 ? totalRating / ratingCount : 0
// 计算物品平均评分
this.computeItemMeanRatings()
// 标记相似度需要重新计算
this.isSimilarityComputed = false
console.info(`[ItemCF] 加载 ${ratings.length} 条评分,${this.userItemMatrix.size} 个用户,${this.itemUserIndex.size} 个物品`)
}
/**
* 计算物品平均评分
*/
private computeItemMeanRatings(): void {
this.itemMeanRating.clear()
this.itemUserIndex.forEach((users, itemId) => {
let sum = 0
let count = 0
users.forEach(userId => {
const rating = this.userItemMatrix.get(userId)?.get(itemId)
if (rating !== undefined) {
sum += rating
count++
}
})
this.itemMeanRating.set(itemId, count > 0 ? sum / count : 0)
})
}
/**
* 计算物品相似度矩阵
* 这是ItemCF的核心步骤,计算量较大
* 建议在空闲时或云端预计算
*/
computeItemSimilarities(topK: number = 50): void {
console.info('[ItemCF] 开始计算物品相似度...')
const startTime = Date.now()
this.itemSimilarityMatrix = SimilarityCalculator.buildItemSimilarityMatrix(
this.itemUserIndex,
topK
)
this.isSimilarityComputed = true
const elapsed = Date.now() - startTime
console.info(`[ItemCF] 相似度计算完成,耗时 ${elapsed}ms`)
}
/**
* 加载预计算的相似度矩阵(从云端或本地缓存)
*/
loadSimilarityMatrix(matrix: Map<string, Map<string, number>>): void {
this.itemSimilarityMatrix = matrix
this.isSimilarityComputed = true
console.info(`[ItemCF] 加载预计算相似度矩阵,${matrix.size} 个物品`)
}
/**
* 为指定用户生成推荐
* ItemCF核心推荐逻辑
*/
recommend(
userId: string,
topK: number = 20,
neighborCount: number = 10
): ItemCFRecommendation[] {
if (!this.isSimilarityComputed) {
console.warn('[ItemCF] 相似度矩阵未计算,请先调用computeItemSimilarities')
return []
}
const userRatings = this.userItemMatrix.get(userId)
if (!userRatings || userRatings.size === 0) {
console.warn(`[ItemCF] 用户 ${userId} 无评分记录`)
return []
}
// 候选物品得分累加器
const candidateScores: Map<string, number> = new Map()
// 候选物品相似度累加器(用于归一化)
const candidateSimSums: Map<string, number> = new Map()
// 推荐依据追踪
const candidateReasons: Map<string, string[]> = new Map()
// 遍历用户已评分的物品
userRatings.forEach((rating, ratedItemId) => {
// 找到该物品的相似物品
const similarItems = this.itemSimilarityMatrix.get(ratedItemId)
if (!similarItems) return
// 取TopN相似物品
const topNeighbors = Array.from(similarItems.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, neighborCount)
for (const [simItemId, similarity] of topNeighbors) {
// 跳过用户已评分的物品
if (userRatings.has(simItemId)) continue
// 加权累加得分
const currentScore = candidateScores.get(simItemId) || 0
const weightedScore = similarity * rating
candidateScores.set(simItemId, currentScore + weightedScore)
// 累加相似度(用于归一化)
const currentSimSum = candidateSimSums.get(simItemId) || 0
candidateSimSums.set(simItemId, currentSimSum + Math.abs(similarity))
// 记录推荐依据
if (!candidateReasons.has(simItemId)) {
candidateReasons.set(simItemId, [])
}
candidateReasons.get(simItemId)!.push(ratedItemId)
}
})
// 归一化得分并生成推荐结果
const results: ItemCFRecommendation[] = []
candidateScores.forEach((score, itemId) => {
const simSum = candidateSimSums.get(itemId) || 1
const normalizedScore = score / simSum
const similarItems = candidateReasons.get(itemId) || []
// 计算置信度:基于相似物品数量和相似度
const confidence = Math.min(1.0, similarItems.length / 5)
results.push({
itemId,
score: normalizedScore,
similarItems,
confidence,
})
})
// 按得分降序排列
results.sort((a, b) => b.score - a.score)
console.info(`[ItemCF] 为用户 ${userId} 生成 ${results.length} 个推荐`)
return results.slice(0, topK)
}
/**
* 预测用户对指定物品的评分
*/
predictRating(userId: string, itemId: string): number {
if (!this.isSimilarityComputed) return 0
const userRatings = this.userItemMatrix.get(userId)
if (!userRatings) return this.itemMeanRating.get(itemId) || this.globalMeanRating
// 如果用户已评分,直接返回
if (userRatings.has(itemId)) {
return userRatings.get(itemId)!
}
// 找到目标物品的相似物品中,用户已评分的
const similarItems = this.itemSimilarityMatrix.get(itemId)
if (!similarItems) return this.itemMeanRating.get(itemId) || this.globalMeanRating
let weightedSum = 0
let simSum = 0
similarItems.forEach((similarity, simItemId) => {
const rating = userRatings.get(simItemId)
if (rating !== undefined) {
weightedSum += similarity * (rating - (this.itemMeanRating.get(simItemId) || 0))
simSum += Math.abs(similarity)
}
})
if (simSum === 0) return this.itemMeanRating.get(itemId) || this.globalMeanRating
// 基线评分 + 邻居偏差加权
const itemMean = this.itemMeanRating.get(itemId) || this.globalMeanRating
return itemMean + weightedSum / simSum
}
/**
* 导出相似度矩阵(用于缓存或云端同步)
*/
exportSimilarityMatrix(): string {
const data: Record<string, Record<string, number>> = {}
this.itemSimilarityMatrix.forEach((neighbors, itemId) => {
const neighborObj: Record<string, number> = {}
neighbors.forEach((sim, neighborId) => {
neighborObj[neighborId] = sim
})
data[itemId] = neighborObj
})
return JSON.stringify(data)
}
/**
* 导入相似度矩阵
*/
importSimilarityMatrix(json: string): void {
const data = JSON.parse(json) as Record<string, Record<string, number>>
this.itemSimilarityMatrix.clear()
for (const [itemId, neighbors] of Object.entries(data)) {
const neighborMap: Map<string, number> = new Map()
for (const [neighborId, sim] of Object.entries(neighbors)) {
neighborMap.set(neighborId, sim)
}
this.itemSimilarityMatrix.set(itemId, neighborMap)
}
this.isSimilarityComputed = true
console.info(`[ItemCF] 导入相似度矩阵,${this.itemSimilarityMatrix.size} 个物品`)
}
/**
* 获取引擎统计信息
*/
getStats(): Record<string, number> {
return {
userCount: this.userItemMatrix.size,
itemCount: this.itemUserIndex.size,
ratingCount: Array.from(this.userItemMatrix.values())
.reduce((sum, ratings) => sum + ratings.size, 0),
similarityComputed: this.isSimilarityComputed ? 1 : 0,
globalMeanRating: this.globalMeanRating,
}
}
}
3.3 协同过滤推荐页面与可视化
将协同过滤引擎集成到页面中,并展示推荐依据。
// CollaborativeFilteringPage.ets - 协同过滤推荐页面
import { ItemCFEngine, RatingRecord, ItemCFRecommendation } from './ItemCFEngine'
@Entry
@Component
struct CollaborativeFilteringPage {
private engine: ItemCFEngine = new ItemCFEngine()
private currentUserId: string = 'user_001'
@State recommendations: ItemCFRecommendation[] = []
@State isComputing: boolean = false
@State computeProgress: number = 0
@State statsInfo: string = ''
@State showDetail: boolean = false
@State selectedItem: ItemCFRecommendation | null = null
// 模拟评分数据
private mockRatings: RatingRecord[] = [
// 用户1的评分
{ userId: 'user_001', itemId: 'item_A', rating: 5.0, timestamp: Date.now() - 86400000 },
{ userId: 'user_001', itemId: 'item_B', rating: 4.0, timestamp: Date.now() - 72000000 },
{ userId: 'user_001', itemId: 'item_C', rating: 3.5, timestamp: Date.now() - 36000000 },
{ userId: 'user_001', itemId: 'item_D', rating: 4.5, timestamp: Date.now() - 18000000 },
// 用户2的评分(和用户1有重叠偏好)
{ userId: 'user_002', itemId: 'item_A', rating: 4.5, timestamp: Date.now() - 86400000 },
{ userId: 'user_002', itemId: 'item_B', rating: 4.0, timestamp: Date.now() - 72000000 },
{ userId: 'user_002', itemId: 'item_E', rating: 5.0, timestamp: Date.now() - 50000000 },
{ userId: 'user_002', itemId: 'item_F', rating: 3.0, timestamp: Date.now() - 30000000 },
// 用户3的评分
{ userId: 'user_003', itemId: 'item_A', rating: 3.0, timestamp: Date.now() - 86400000 },
{ userId: 'user_003', itemId: 'item_C', rating: 4.0, timestamp: Date.now() - 60000000 },
{ userId: 'user_003', itemId: 'item_G', rating: 5.0, timestamp: Date.now() - 40000000 },
{ userId: 'user_003', itemId: 'item_H', rating: 4.5, timestamp: Date.now() - 20000000 },
// 用户4的评分
{ userId: 'user_004', itemId: 'item_B', rating: 3.5, timestamp: Date.now() - 86400000 },
{ userId: 'user_004', itemId: 'item_D', rating: 5.0, timestamp: Date.now() - 70000000 },
{ userId: 'user_004', itemId: 'item_E', rating: 4.0, timestamp: Date.now() - 50000000 },
{ userId: 'user_004', itemId: 'item_I', rating: 4.5, timestamp: Date.now() - 10000000 },
// 用户5的评分
{ userId: 'user_005', itemId: 'item_A', rating: 4.0, timestamp: Date.now() - 86400000 },
{ userId: 'user_005', itemId: 'item_D', rating: 4.0, timestamp: Date.now() - 60000000 },
{ userId: 'user_005', itemId: 'item_F', rating: 5.0, timestamp: Date.now() - 40000000 },
{ userId: 'user_005', itemId: 'item_J', rating: 3.5, timestamp: Date.now() - 5000000 },
]
aboutToAppear(): void {
this.initEngine()
}
/**
* 初始化推荐引擎
*/
private initEngine(): void {
this.engine.loadRatings(this.mockRatings)
this.updateStats()
}
/**
* 更新统计信息
*/
private updateStats(): void {
const stats = this.engine.getStats()
this.statsInfo = `用户: ${stats.userCount} | 物品: ${stats.itemCount} | 评分: ${stats.ratingCount}`
}
/**
* 执行推荐计算
*/
private doRecommend(): void {
this.isComputing = true
this.computeProgress = 0
// 模拟计算进度
const progressTimer = setInterval(() => {
this.computeProgress = Math.min(this.computeProgress + 10, 90)
}, 100)
setTimeout(() => {
// 计算相似度(首次)
this.engine.computeItemSimilarities(30)
// 生成推荐
this.recommendations = this.engine.recommend(this.currentUserId, 10, 8)
clearInterval(progressTimer)
this.computeProgress = 100
this.isComputing = false
}, 1500)
}
build() {
Navigation() {
Column() {
// 统计信息栏
this.StatsBar()
// 操作按钮
this.ActionBar()
// 计算进度
if (this.isComputing) {
this.ProgressView()
}
// 推荐结果列表
if (this.recommendations.length > 0 && !this.isComputing) {
this.RecommendResultList()
}
// 推荐详情弹窗
if (this.showDetail && this.selectedItem) {
this.DetailPanel()
}
}
.width('100%')
.height('100%')
.backgroundColor('#0f0f1a')
}
.title('协同过滤推荐')
.titleMode(NavigationTitleMode.Mini)
.navBarStyle(NavigationBarStyle.Constant)
}
// ======== 子组件 ========
@Builder
StatsBar() {
Row() {
Text(this.statsInfo)
.fontSize(12)
.fontColor('#888888')
}
.width('100%')
.padding({ left: 16, right: 16, top: 8, bottom: 4 })
}
@Builder
ActionBar() {
Row() {
Button('计算相似度 & 推荐')
.fontSize(14)
.fontColor('#FFFFFF')
.backgroundColor('#7B68EE')
.borderRadius(20)
.padding({ left: 20, right: 20, top: 8, bottom: 8 })
.enabled(!this.isComputing)
.onClick(() => this.doRecommend())
Button('重置')
.fontSize(14)
.fontColor('#7B68EE')
.backgroundColor('rgba(123,104,238,0.15)')
.borderRadius(20)
.padding({ left: 20, right: 20, top: 8, bottom: 8 })
.margin({ left: 12 })
.onClick(() => {
this.recommendations = []
this.engine = new ItemCFEngine()
this.initEngine()
})
}
.width('100%')
.justifyContent(FlexAlign.Center)
.padding({ top: 12, bottom: 12 })
}
@Builder
ProgressView() {
Column() {
Progress({ value: this.computeProgress, total: 100, type: ProgressType.Linear })
.width('80%')
.color('#7B68EE')
.backgroundColor('rgba(123,104,238,0.2)')
Text(`正在计算物品相似度... ${this.computeProgress}%`)
.fontSize(13)
.fontColor('#999999')
.margin({ top: 8 })
}
.width('100%')
.padding(16)
}
@Builder
RecommendResultList() {
List({ space: 10 }) {
ForEach(this.recommendations, (item: ItemCFRecommendation, index: number) => {
ListItem() {
this.RecommendCard(item, index + 1)
}
})
}
.width('100%')
.layoutWeight(1)
.padding({ left: 16, right: 16, top: 4 })
}
@Builder
RecommendCard(item: ItemCFRecommendation, rank: number) {
Column() {
Row() {
// 排名
Text(`#${rank}`)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(rank <= 3 ? '#F5A623' : '#666666')
.width(40)
// 物品信息
Column() {
Text(item.itemId)
.fontSize(16)
.fontColor('#E0E0E0')
.fontWeight(FontWeight.Medium)
// 推荐依据
Text(`因为喜欢: ${item.similarItems.join(', ')}`)
.fontSize(12)
.fontColor('#999999')
.margin({ top: 4 })
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
.margin({ left: 8 })
// 得分与置信度
Column() {
Text(item.score.toFixed(2))
.fontSize(16)
.fontColor('#F5A623')
.fontWeight(FontWeight.Bold)
Text(`置信度 ${(item.confidence * 100).toFixed(0)}%`)
.fontSize(11)
.fontColor('#7B68EE')
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
}
.width('100%')
.padding(14)
.borderRadius(12)
.backgroundColor('rgba(255,255,255,0.06)')
.backdropBlur(20)
.onClick(() => {
this.selectedItem = item
this.showDetail = true
})
}
@Builder
DetailPanel() {
Column() {
// 半透明遮罩
Column() {
// 详情卡片
Column() {
Text('推荐详情')
.fontSize(18)
.fontColor('#E0E0E0')
.fontWeight(FontWeight.Bold)
.margin({ bottom: 16 })
if (this.selectedItem) {
Text(`推荐物品: ${this.selectedItem.itemId}`)
.fontSize(16)
.fontColor('#E0E0E0')
.margin({ bottom: 8 })
Text(`推荐得分: ${this.selectedItem.score.toFixed(4)}`)
.fontSize(14)
.fontColor('#F5A623')
.margin({ bottom: 8 })
Text(`置信度: ${(this.selectedItem.confidence * 100).toFixed(1)}%`)
.fontSize(14)
.fontColor('#7B68EE')
.margin({ bottom: 16 })
// 推荐依据
Text('推荐依据(相似物品):')
.fontSize(14)
.fontColor('#999999')
.margin({ bottom: 8 })
ForEach(this.selectedItem.similarItems, (sourceItem: string) => {
Row() {
Text('●')
.fontSize(10)
.fontColor('#7B68EE')
.margin({ right: 8 })
Text(sourceItem)
.fontSize(14)
.fontColor('#CCCCCC')
}
.margin({ bottom: 4 })
})
}
Button('关闭')
.fontSize(14)
.fontColor('#FFFFFF')
.backgroundColor('#7B68EE')
.borderRadius(20)
.margin({ top: 20 })
.onClick(() => {
this.showDetail = false
})
}
.width('85%')
.padding(24)
.borderRadius(16)
.backgroundColor('rgba(30,30,50,0.95)')
.backdropBlur(30)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor('rgba(0,0,0,0.5)')
.onClick(() => {
this.showDetail = false
})
}
.width('100%')
.height('100%')
.position({ x: 0, y: 0 })
.zIndex(100)
}
}
四、踩坑与注意事项
4.1 相似度矩阵的计算时机
坑:在用户每次请求推荐时都重新计算相似度矩阵,导致严重卡顿。
解:
- 相似度矩阵是"全局"的,不需要每次推荐都重新计算
- 推荐策略:云端预计算 + 端侧缓存
- 端侧只在数据量变化超过阈值时重新计算
- 使用
importSimilarityMatrix/exportSimilarityMatrix做持久化缓存
// 推荐的计算策略
const CACHE_KEY = 'item_similarity_cache'
const CACHE_EXPIRY = 24 * 3600 * 1000 // 24小时过期
async function getOrComputeSimilarity(engine: ItemCFEngine): Promise<void> {
// 先尝试从缓存加载
const cached = await loadFromPreferences(CACHE_KEY)
if (cached) {
engine.importSimilarityMatrix(cached)
return
}
// 缓存未命中,执行计算
engine.computeItemSimilarities(50)
// 保存到缓存
await saveToPreferences(CACHE_KEY, engine.exportSimilarityMatrix())
}
4.2 稀疏数据下的相似度偏差
坑:两个物品只有1个共同用户,Jaccard系数可能很高,但不可靠。
解:引入置信度惩罚,共同用户数越少,相似度折扣越大:
// 带置信度惩罚的相似度
static penalizedJaccard(
setA: Set<string>,
setB: Set<string>,
minCommon: number = 3
): number {
const rawSim = SimilarityCalculator.jaccardSimilarity(setA, setB)
// 计算共同用户数
let common = 0
setA.forEach(item => { if (setB.has(item)) common++ })
// 置信度惩罚因子
const penalty = common < minCommon ? common / minCommon : 1.0
return rawSim * penalty
}
4.3 热门物品偏差
坑:热门物品和所有物品的Jaccard系数都很高,导致推荐结果被热门物品"污染"。
解:使用**逆物品频率(IIF)**加权:
// 逆物品频率加权
static iifWeightedJaccard(
setA: Set<string>,
setB: Set<string>,
userCount: number
): number {
let weightedIntersection = 0
let weightedUnion = 0
// 对共同用户施加逆频率权重
setA.forEach(user => {
if (setB.has(user)) {
// 用户越活跃,权重越低(减少热门用户的影响)
// 这里简化处理,实际需要用户的总交互数
weightedIntersection += 1
}
})
// 计算并集
const unionSet = new Set([...setA, ...setB])
weightedUnion = unionSet.size
return weightedUnion > 0 ? weightedIntersection / weightedUnion : 0
}
4.4 端侧内存管理
坑:物品数量超过1000时,相似度矩阵占用内存过大。
解:
- 限制每个物品只保留TopK=30个最相似邻居
- 使用
Map<string, number>而非对象,减少内存开销 - 定期清理不活跃物品的相似度数据
- 物品数量超过5000时,必须使用云端预计算
4.5 评分预测的边界情况
坑:当用户评分极少(如只有1条)时,评分预测可能产生极端值。
解:
- 设置评分预测的上下界(如1-5分)
- 当邻居数不足时,回退到物品平均评分
- 引入全局均值作为平滑项
// 评分预测加平滑
const predictedScore = this.predictRating(userId, itemId)
// 限制在合理范围内
const clampedScore = Math.max(1.0, Math.min(5.0, predictedScore))
// 邻居不足时回退
if (neighborCount < 2) {
return this.itemMeanRating.get(itemId) || this.globalMeanRating
}
五、HarmonyOS 6适配
5.1 版本差异
| 特性 | HarmonyOS 5.0 | HarmonyOS 6 |
|---|---|---|
| Worker线程 | worker 基础支持 |
worker 增强调度 |
| 关系型数据库 | 单进程访问 | 支持多进程共享 |
| 内存管理 | 手动管理 | 新增内存压力回调 |
| 端侧ML | 无 | MindSpore Lite集成 |
5.2 迁移指南
1. 使用Worker进行相似度计算
相似度计算是CPU密集型任务,在HarmonyOS 6中应使用Worker避免UI卡顿:
// HarmonyOS 6 Worker计算相似度
// SimilarityWorker.ets
const worker = new worker.ThreadWorker('entry/ets/workers/SimilarityWorker.ets')
// 主线程发送计算请求
worker.postMessage({
type: 'computeSimilarity',
data: serializedItemUserMap,
})
// 接收计算结果
worker.onmessage = (event) => {
const result = event.data
this.engine.importSimilarityMatrix(result.similarityMatrix)
}
2. 内存压力回调
HarmonyOS 6新增内存压力监听,在内存紧张时释放相似度缓存:
// HarmonyOS 6 内存压力监听
import { memory } from '@kit.BasicServicesKit'
memory.on('pressure', (level: memory.MemoryPressureLevel) => {
if (level >= memory.MemoryPressureLevel.MODERATE) {
// 释放相似度矩阵缓存
this.itemSimilarityMatrix.clear()
this.isSimilarityComputed = false
console.warn('[ItemCF] 内存压力,释放相似度缓存')
}
})
3. MindSpore端侧矩阵分解
HarmonyOS 6可使用MindSpore在端侧运行轻量级矩阵分解模型:
// HarmonyOS 6 端侧SVD推荐(概念代码)
import { mindSpore } from '@kit.MindSporeKit'
async function svdRecommend(userId: string): Promise<Map<string, number>> {
const model = await mindSpore.loadModel('svd_model.ms')
const userEmbedding = getUserEmbedding(userId) // 用户隐向量
const predictions = await model.predict(userEmbedding)
return parsePredictions(predictions)
}
六、总结
本文完整实现了HarmonyOS端侧的协同过滤推荐引擎,核心知识点回顾:
| 模块 | 核心功能 | 关键算法 |
|---|---|---|
| 相似度计算 | 余弦/皮尔逊/Jaccard | 稀疏向量运算 |
| ItemCF引擎 | 物品相似度推荐 | 邻居聚合、评分预测 |
| 稀疏存储 | Map嵌套存储评分矩阵 | 只存非零元素 |
| 相似度缓存 | 导入/导出JSON | 云端预计算+端侧缓存 |
核心要点回顾:
- 🎯 ItemCF比UserCF更适合端侧,物品相似度可预计算,端侧只需查表+加权
- 📐 相似度选择:隐式反馈用Jaccard,显式评分用皮尔逊,混合场景用改进余弦
- 🧊 稀疏数据惩罚:共同交互数太少时相似度不可靠,必须加置信度惩罚
- 🔥 热门物品偏差:热门物品会"污染"相似度,需要逆频率加权
- 💾 内存管理:TopK剪枝+定期清理,物品超5000必须云端计算
- 📱 HarmonyOS 6:Worker计算+内存压力回调+MindSpore端侧推理
下一篇我们将深入内容推荐与特征工程,讲解如何从物品内容中提取特征并实现精准推荐。
- 点赞
- 收藏
- 关注作者
评论(0)