SorryToPerson logo
返回
算法2026-04-29·11 分钟

算法知识库:推荐系统算法实现

JavaScript/TypeScript 实现推荐系统基础算法,如协同过滤、矩阵分解、内容推荐等。

推荐系统算法实现

1. 用户-物品评分矩阵

ts
class RatingMatrix {
  private ratings: Map<string, Map<string, number>> = new Map();
  private users: Set<string> = new Set();
  private items: Set<string> = new Set();

  addRating(userId: string, itemId: string, rating: number): void {
    if (!this.ratings.has(userId)) {
      this.ratings.set(userId, new Map());
    }
    this.ratings.get(userId)!.set(itemId, rating);
    this.users.add(userId);
    this.items.add(itemId);
  }

  getRating(userId: string, itemId: string): number | null {
    const userRatings = this.ratings.get(userId);
    return userRatings ? userRatings.get(itemId) || null : null;
  }

  getUserRatings(userId: string): Map<string, number> {
    return this.ratings.get(userId) || new Map();
  }

  getItemRatings(itemId: string): Map<string, number> {
    const itemRatings: Map<string, number> = new Map();
    for (const [userId, userRatings] of this.ratings) {
      const rating = userRatings.get(itemId);
      if (rating !== undefined) {
        itemRatings.set(userId, rating);
      }
    }
    return itemRatings;
  }

  getUsers(): string[] {
    return Array.from(this.users);
  }

  getItems(): string[] {
    return Array.from(this.items);
  }

  getAverageRating(): number {
    let total = 0;
    let count = 0;

    for (const userRatings of this.ratings.values()) {
      for (const rating of userRatings.values()) {
        total += rating;
        count += 1;
      }
    }

    return count > 0 ? total / count : 0;
  }
}

2. 基于用户的协同过滤

ts
class UserBasedCollaborativeFiltering {
  private matrix: RatingMatrix;
  private similarityMatrix: Map<string, Map<string, number>> = new Map();

  constructor(matrix: RatingMatrix) {
    this.matrix = matrix;
    this.computeSimilarities();
  }

  private computeSimilarities(): void {
    const users = this.matrix.getUsers();

    for (let i = 0; i < users.length; i += 1) {
      for (let j = i + 1; j < users.length; j += 1) {
        const similarity = this.cosineSimilarity(users[i], users[j]);

        if (!this.similarityMatrix.has(users[i])) {
          this.similarityMatrix.set(users[i], new Map());
        }
        if (!this.similarityMatrix.has(users[j])) {
          this.similarityMatrix.set(users[j], new Map());
        }

        this.similarityMatrix.get(users[i])!.set(users[j], similarity);
        this.similarityMatrix.get(users[j])!.set(users[i], similarity);
      }
    }
  }

  private cosineSimilarity(user1: string, user2: string): number {
    const ratings1 = this.matrix.getUserRatings(user1);
    const ratings2 = this.matrix.getUserRatings(user2);

    const commonItems = new Set([...ratings1.keys()].filter((item) => ratings2.has(item)));

    if (commonItems.size === 0) return 0;

    let dotProduct = 0;
    let norm1 = 0;
    let norm2 = 0;

    for (const item of commonItems) {
      const r1 = ratings1.get(item)!;
      const r2 = ratings2.get(item)!;
      dotProduct += r1 * r2;
      norm1 += r1 * r1;
      norm2 += r2 * r2;
    }

    norm1 = Math.sqrt(norm1);
    norm2 = Math.sqrt(norm2);

    return norm1 > 0 && norm2 > 0 ? dotProduct / (norm1 * norm2) : 0;
  }

  predictRating(userId: string, itemId: string, k: number = 5): number {
    const userRatings = this.matrix.getUserRatings(userId);
    if (userRatings.has(itemId)) {
      return userRatings.get(itemId)!;
    }

    const similarities: Array<{ user: string; similarity: number }> = [];
    for (const [otherUser, similarity] of this.similarityMatrix.get(userId) || []) {
      const otherUserRatings = this.matrix.getUserRatings(otherUser);
      if (otherUserRatings.has(itemId)) {
        similarities.push({ user: otherUser, similarity });
      }
    }

    similarities.sort((a, b) => b.similarity - a.similarity);
    const topK = similarities.slice(0, k);

    if (topK.length === 0) return this.matrix.getAverageRating();

    let weightedSum = 0;
    let totalWeight = 0;

    for (const { user, similarity } of topK) {
      const rating = this.matrix.getUserRatings(user).get(itemId)!;
      weightedSum += similarity * rating;
      totalWeight += Math.abs(similarity);
    }

    return totalWeight > 0 ? weightedSum / totalWeight : this.matrix.getAverageRating();
  }

  recommendItems(userId: string, n: number = 10): Array<{ itemId: string; predictedRating: number }> {
    const userRatings = this.matrix.getUserRatings(userId);
    const allItems = this.matrix.getItems();
    const unratedItems = allItems.filter((item) => !userRatings.has(item));

    const predictions: Array<{ itemId: string; predictedRating: number }> = [];

    for (const itemId of unratedItems) {
      const predictedRating = this.predictRating(userId, itemId);
      predictions.push({ itemId, predictedRating });
    }

    predictions.sort((a, b) => b.predictedRating - a.predictedRating);
    return predictions.slice(0, n);
  }
}

3. 基于物品的协同过滤

ts
class ItemBasedCollaborativeFiltering {
  private matrix: RatingMatrix;
  private similarityMatrix: Map<string, Map<string, number>> = new Map();

  constructor(matrix: RatingMatrix) {
    this.matrix = matrix;
    this.computeSimilarities();
  }

  private computeSimilarities(): void {
    const items = this.matrix.getItems();

    for (let i = 0; i < items.length; i += 1) {
      for (let j = i + 1; j < items.length; j += 1) {
        const similarity = this.cosineSimilarity(items[i], items[j]);

        if (!this.similarityMatrix.has(items[i])) {
          this.similarityMatrix.set(items[i], new Map());
        }
        if (!this.similarityMatrix.has(items[j])) {
          this.similarityMatrix.set(items[j], new Map());
        }

        this.similarityMatrix.get(items[i])!.set(items[j], similarity);
        this.similarityMatrix.get(items[j])!.set(items[i], similarity);
      }
    }
  }

  private cosineSimilarity(item1: string, item2: string): number {
    const ratings1 = this.matrix.getItemRatings(item1);
    const ratings2 = this.matrix.getItemRatings(item2);

    const commonUsers = new Set([...ratings1.keys()].filter((user) => ratings2.has(user)));

    if (commonUsers.size === 0) return 0;

    let dotProduct = 0;
    let norm1 = 0;
    let norm2 = 0;

    for (const user of commonUsers) {
      const r1 = ratings1.get(user)!;
      const r2 = ratings2.get(user)!;
      dotProduct += r1 * r2;
      norm1 += r1 * r1;
      norm2 += r2 * r2;
    }

    norm1 = Math.sqrt(norm1);
    norm2 = Math.sqrt(norm2);

    return norm1 > 0 && norm2 > 0 ? dotProduct / (norm1 * norm2) : 0;
  }

  predictRating(userId: string, itemId: string, k: number = 5): number {
    const userRatings = this.matrix.getUserRatings(userId);
    if (userRatings.has(itemId)) {
      return userRatings.get(itemId)!;
    }

    const similarities: Array<{ item: string; similarity: number }> = [];
    for (const [otherItem, similarity] of this.similarityMatrix.get(itemId) || []) {
      if (userRatings.has(otherItem)) {
        similarities.push({ item: otherItem, similarity });
      }
    }

    similarities.sort((a, b) => b.similarity - a.similarity);
    const topK = similarities.slice(0, k);

    if (topK.length === 0) return this.matrix.getAverageRating();

    let weightedSum = 0;
    let totalWeight = 0;

    for (const { item, similarity } of topK) {
      const rating = userRatings.get(item)!;
      weightedSum += similarity * rating;
      totalWeight += Math.abs(similarity);
    }

    return totalWeight > 0 ? weightedSum / totalWeight : this.matrix.getAverageRating();
  }

  recommendItems(userId: string, n: number = 10): Array<{ itemId: string; predictedRating: number }> {
    const userRatings = this.matrix.getUserRatings(userId);
    const allItems = this.matrix.getItems();
    const unratedItems = allItems.filter((item) => !userRatings.has(item));

    const predictions: Array<{ itemId: string; predictedRating: number }> = [];

    for (const itemId of unratedItems) {
      const predictedRating = this.predictRating(userId, itemId);
      predictions.push({ itemId, predictedRating });
    }

    predictions.sort((a, b) => b.predictedRating - a.predictedRating);
    return predictions.slice(0, n);
  }
}

4. 矩阵分解 (SVD 简化版)

ts
class MatrixFactorization {
  private matrix: RatingMatrix;
  private userFactors: Map<string, number[]> = new Map();
  private itemFactors: Map<string, number[]> = new Map();
  private k: number; // 隐因子维度
  private learningRate: number;
  private regularization: number;
  private epochs: number;

  constructor(matrix: RatingMatrix, k: number = 10, learningRate: number = 0.01, regularization: number = 0.1, epochs: number = 100) {
    this.matrix = matrix;
    this.k = k;
    this.learningRate = learningRate;
    this.regularization = regularization;
    this.epochs = epochs;
    this.initializeFactors();
  }

  private initializeFactors(): void {
    const users = this.matrix.getUsers();
    const items = this.matrix.getItems();

    for (const user of users) {
      this.userFactors.set(
        user,
        Array.from({ length: this.k }, () => Math.random()),
      );
    }

    for (const item of items) {
      this.itemFactors.set(
        item,
        Array.from({ length: this.k }, () => Math.random()),
      );
    }
  }

  train(): void {
    for (let epoch = 0; epoch < this.epochs; epoch += 1) {
      this.trainEpoch();
    }
  }

  private trainEpoch(): void {
    const users = this.matrix.getUsers();
    const items = this.matrix.getItems();

    for (const user of users) {
      for (const item of items) {
        const actualRating = this.matrix.getRating(user, item);
        if (actualRating !== null) {
          const predictedRating = this.predictRating(user, item);
          const error = actualRating - predictedRating;

          const userFactor = this.userFactors.get(user)!;
          const itemFactor = this.itemFactors.get(item)!;

          for (let i = 0; i < this.k; i += 1) {
            const userGrad = error * itemFactor[i] - this.regularization * userFactor[i];
            const itemGrad = error * userFactor[i] - this.regularization * itemFactor[i];

            userFactor[i] += this.learningRate * userGrad;
            itemFactor[i] += this.learningRate * itemGrad;
          }
        }
      }
    }
  }

  predictRating(userId: string, itemId: string): number {
    const userFactor = this.userFactors.get(userId);
    const itemFactor = this.itemFactors.get(itemId);

    if (!userFactor || !itemFactor) return this.matrix.getAverageRating();

    let prediction = 0;
    for (let i = 0; i < this.k; i += 1) {
      prediction += userFactor[i] * itemFactor[i];
    }

    return prediction;
  }

  recommendItems(userId: string, n: number = 10): Array<{ itemId: string; predictedRating: number }> {
    const userRatings = this.matrix.getUserRatings(userId);
    const allItems = this.matrix.getItems();
    const unratedItems = allItems.filter((item) => !userRatings.has(item));

    const predictions: Array<{ itemId: string; predictedRating: number }> = [];

    for (const itemId of unratedItems) {
      const predictedRating = this.predictRating(userId, itemId);
      predictions.push({ itemId, predictedRating });
    }

    predictions.sort((a, b) => b.predictedRating - a.predictedRating);
    return predictions.slice(0, n);
  }
}

5. 内容推荐 (基于标签)

ts
class ContentBasedRecommender {
  private itemTags: Map<string, Set<string>> = new Map();
  private userPreferences: Map<string, Map<string, number>> = new Map();

  addItemTags(itemId: string, tags: string[]): void {
    this.itemTags.set(itemId, new Set(tags));
  }

  addUserRating(userId: string, itemId: string, rating: number): void {
    if (!this.userPreferences.has(userId)) {
      this.userPreferences.set(userId, new Map());
    }

    const preferences = this.userPreferences.get(userId)!;
    const tags = this.itemTags.get(itemId);

    if (tags) {
      for (const tag of tags) {
        const current = preferences.get(tag) || 0;
        preferences.set(tag, current + rating);
      }
    }
  }

  recommendItems(userId: string, items: string[], n: number = 10): Array<{ itemId: string; score: number }> {
    const preferences = this.userPreferences.get(userId);
    if (!preferences) return [];

    const scoredItems: Array<{ itemId: string; score: number }> = [];

    for (const itemId of items) {
      const tags = this.itemTags.get(itemId);
      if (!tags) continue;

      let score = 0;
      for (const tag of tags) {
        score += preferences.get(tag) || 0;
      }

      scoredItems.push({ itemId, score });
    }

    scoredItems.sort((a, b) => b.score - a.score);
    return scoredItems.slice(0, n);
  }
}

6. 实现要点

  • 协同过滤基于用户或物品相似性。
  • 矩阵分解学习隐因子表示。
  • 内容推荐基于物品特征。
  • 实际系统结合多种方法。
算法推荐系统JavaScript