HarmonyOS开发:协同过滤推荐引擎构建

举报
Jack20 发表于 2026/06/21 14:08:34 2026/06/21
【摘要】 HarmonyOS开发:协同过滤推荐引擎构建核心要点:协同过滤是推荐系统中最经典的算法族,本文深入讲解UserCF和ItemCF两种核心算法的数学原理,并在HarmonyOS端侧实现完整的协同过滤引擎,涵盖相似度计算、邻居选择、评分预测等关键环节,同时解决稀疏矩阵存储和冷启动等端侧特有挑战。项目说明开发语言ArkTS关键能力稀疏矩阵运算、相似度计算、邻居聚合 一、背景与动机“喜欢这个的人也...

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的核心思想:和你品味相似的人喜欢的东西,你也可能喜欢

数学表达:

r^u,i=vN(u)sim(u,v)rv,ivN(u)sim(u,v)\hat{r}_{u,i} = \frac{\sum_{v \in N(u)} sim(u,v) \cdot r_{v,i}}{\sum_{v \in N(u)} |sim(u,v)|}

其中:

  • r^u,i\hat{r}_{u,i}:用户u对物品i的预测评分
  • N(u)N(u):用户u的邻居集合
  • sim(u,v)sim(u,v):用户u和v的相似度
  • rv,ir_{v,i}:用户v对物品i的实际评分

端侧困境:单个设备只有一个用户的数据,无法计算用户间相似度。

解决方案

  1. 云端预计算用户相似度,端侧只做邻居聚合
  2. 利用HarmonyOS分布式能力,跨设备共享匿名行为模式
  3. 端侧用"隐式用户画像"替代显式用户相似度

2.3 ItemCF:物以类聚

ItemCF的核心思想:和你之前喜欢的东西相似的东西,你也可能喜欢

数学表达:

r^u,i=jI(u)sim(i,j)ru,jjI(u)sim(i,j)\hat{r}_{u,i} = \frac{\sum_{j \in I(u)} sim(i,j) \cdot r_{u,j}}{\sum_{j \in I(u)} |sim(i,j)|}

其中:

  • I(u)I(u):用户u交互过的物品集合
  • sim(i,j)sim(i,j):物品i和j的相似度

端侧优势:物品相似度可以云端预计算,端侧只需查表+加权求和,计算量可控!

2.4 相似度计算方法

方法 公式 特点
余弦相似度 cos(a,b)=abab\cos(\vec{a},\vec{b}) = \frac{\vec{a} \cdot \vec{b}}{|\vec{a}| \cdot |\vec{b}|} 不考虑评分均值差异
皮尔逊相关系数 r=(aiaˉ)(bibˉ)(aiaˉ)2(bibˉ)2r = \frac{\sum(a_i-\bar{a})(b_i-\bar{b})}{\sqrt{\sum(a_i-\bar{a})^2 \cdot \sum(b_i-\bar{b})^2}} 消除评分偏差
Jaccard系数 $J(A,B) = \frac{ A \cap B

2.5 稀疏矩阵存储策略

用户-物品评分矩阵通常是极稀疏的(稀疏度>99%),端侧必须使用压缩存储:

  • COO格式(坐标存储):记录非零元素的(行,列,值)三元组
  • CSR格式(压缩稀疏行):按行压缩,适合行遍历
  • 自定义Map嵌套Map<userId, Map<itemId, score>>,最灵活

端侧推荐我们选择Map嵌套,因为:

  1. 查询效率O(1)
  2. 天然只存储非零元素
  3. 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 云端预计算+端侧缓存

核心要点回顾

  1. 🎯 ItemCF比UserCF更适合端侧,物品相似度可预计算,端侧只需查表+加权
  2. 📐 相似度选择:隐式反馈用Jaccard,显式评分用皮尔逊,混合场景用改进余弦
  3. 🧊 稀疏数据惩罚:共同交互数太少时相似度不可靠,必须加置信度惩罚
  4. 🔥 热门物品偏差:热门物品会"污染"相似度,需要逆频率加权
  5. 💾 内存管理:TopK剪枝+定期清理,物品超5000必须云端计算
  6. 📱 HarmonyOS 6:Worker计算+内存压力回调+MindSpore端侧推理

下一篇我们将深入内容推荐与特征工程,讲解如何从物品内容中提取特征并实现精准推荐。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。