【问题标题】:frequently and most recently used items ranking algorithm经常和最近使用的项目排名算法
【发布时间】:2017-06-02 15:09:36
【问题描述】:

我正在寻找一种排名算法,该算法将根据使用量和最近的使用量对项目(文件、应用程序、访问的网站...)进行排序。

例如在应用程序启动器中,用户键入应用程序名称的一些短前缀,满足条件的应用程序将被排名。 app A 是用户最喜欢的 app 并且经常使用,但现在他经常使用 app B,只是有时会使用 app A。app A 的启动次数比 app B 的启动次数多,但 B 上一次的使用次数比 A 多。

所以应用 B 排在应用 A 之前。

此外,如果应用C想要超过应用B,它必须(在最近的时间)更多地使用,但是对于应用A来说,它是第一个,它不需要这么多的使用,因为它是用户最喜欢的应用程序,过去的使用量比其他应用程序更多。

我不知道这是否是我想要的一个很好的解释,但我希望有人能理解。

【问题讨论】:

  • 这有点宽泛,但您可以存储每个应用程序的每次使用日期,然后计算这些日期的加权和,为最近的使用赋予更多权重。或者,只需存储应用的总使用次数和最近(或最近五次左右)使用的日期。
  • 按时间段(上周、过去 2..4 周、..)对应用程序的使用情况进行分组。为周期分配权重并计算加权分数。
  • 对所有使用情况进行一些对数加权会很棒(就像在 reddit 热门排名算法中一样),但这种按时间段对使用进行分组的想法也很棒。我会考虑的:)(也等待更多答案:P)

标签: algorithm ranking


【解决方案1】:

我认为您可以使用cache replacement policy 的实现来实现这一点。

这些算法可帮助计算机处理器 (CPU) 确定将主内存 (RAM) 的哪些部分(“页”)保存在缓存中(L1、L2 等)——CPU 可以访问哪些部分很多 比 RAM 快。但它们可以很容易地适应您的问题。

根据最近使用情况对项目进行排序的算法类似于缓存策略 LRU 过期/替换 Least-R ecently-U缓存已满时的页面。

按最频繁使用(此处“频繁”实际上是指“使用次数”)对项目进行排序的算法类似于缓存策略 LFU 过期/替换 Least-F频繁-U缓存已满时的页面。

有几种策略可以按照您要求的方式显式或隐式结合这两个概念。有些还涉及“时间”(根据实际的计算机时钟时间或简单的页面请求的递增计数器等)以获得更好的页面访问“年龄”或“频率”的感觉(而不是简单的使用次数)。

这些变得更加复杂和难以实现,特别是如果您需要一个非常有效的算法。但是对于与用户界面相关的大多数用途,即使是低效的算法也应该足够快,因为项目的数量很少,用户很少会修改列表。

一个可能适合您的算法示例是 LRFU(最近最少/经常使用)策略,该策略直接寻求结合 LRU 和 LFU 以根据结合了新近度的公式来过期页面和使用频率。您可以在上面列出的同一页面上找到对此的参考。你也可以看到被举报的scholarly article。

本文使用堆和链表数据结构的组合来实现它,并包含一些实现的伪代码。

对于您的使用,您可能可以相当容易地编写一个更简单但效率较低的算法。

例如,您可以简单地存储具有 2 个属性的对象数组:

  1. 价值(您关心的东西——例如网站或文件名等)
  2. 分数(下文讨论——在文章中称为“CRF”)

只要用户选择了一个值,您就可以按如下方式修改列表:

  1. 通过将每个分数乘以恒定加权因子USAGE_WEIGHT(介于 0.5 和 1.0 之间的数字,下文将详细讨论)来更新列表中所有现有项目的分数。
  2. 搜索列表以查看最近选择的项目是否已存在。如果是这样,只需将 1.0 添加到其现有分数。如果没有,则创建一个初始分数为 1.0 的新项目并将其添加到列表中。如果列表中没有更多空间(意味着它已经包含您要显示的最大 MRU 项目数),则首先删除列表中具有 最低 分数的项目(始终位于列表的末尾,由于下一步)。
  3. 现在按分数降序重新排序列表。 (最高分应首先出现在 MRU 列表中)。

USAGE_WEIGHT 因素决定了“新近度”与“频率”(即使用次数)相比的重要性。值 0.5 将导致列表具有完全 LRU 行为(只有最近才重要),而值 1.0 将导致列表具有完全 LFU 行为(只有使用计数很重要)。中间值(例如 0.9)将导致列表具有 LRU 和 LFU 行为的混合,如下面的示例输出所示。

在下面的每个场景中,“值”都是字母,它们按以下顺序添加:

A B C B A A D A C D A B D E C B A  

每次添加后,我都会在引号中列出添加的字母以及当前 MRU 列表(例如“DBA”)。最大 MRU 大小为 3。我还列出了更详细的列表表示形式,以 { Letter, Score } 的形式显示每个项目的 Value(字母)和 Score。

USAGE_WEIGHT = 1.0(完全 LFU - 只有使用次数很重要)

  1. (Added A) "A"      [ { A, 1.0 } ]
  2. (Added B) "AB"     [ { A, 1.0 } { B, 1.0 } ]
  3. (Added C) "ABC"    [ { A, 1.0 } { B, 1.0 } { C, 1.0 } ]
  4. (Added B) "BAC"    [ { B, 2.0 } { A, 1.0 } { C, 1.0 } ]
  5. (Added A) "BAC"    [ { B, 2.0 } { A, 2.0 } { C, 1.0 } ]
  6. (Added A) "ABC"    [ { A, 3.0 } { B, 2.0 } { C, 1.0 } ]
  7. (Added D) "ABD"    [ { A, 3.0 } { B, 2.0 } { D, 1.0 } ]
  8. (Added A) "ABD"    [ { A, 4.0 } { B, 2.0 } { D, 1.0 } ]
  9. (Added C) "ABC"    [ { A, 4.0 } { B, 2.0 } { C, 1.0 } ]
 10. (Added D) "ABD"    [ { A, 4.0 } { B, 2.0 } { D, 1.0 } ]
 11. (Added A) "ABD"    [ { A, 5.0 } { B, 2.0 } { D, 1.0 } ]
 12. (Added B) "ABD"    [ { A, 5.0 } { B, 3.0 } { D, 1.0 } ]
 13. (Added D) "ABD"    [ { A, 5.0 } { B, 3.0 } { D, 2.0 } ]
 14. (Added E) "ABE"    [ { A, 5.0 } { B, 3.0 } { E, 1.0 } ]
 15. (Added C) "ABC"    [ { A, 5.0 } { B, 3.0 } { C, 1.0 } ]
 16. (Added B) "ABC"    [ { A, 5.0 } { B, 4.0 } { C, 1.0 } ]
 17. (Added A) "ABC"    [ { A, 6.0 } { B, 4.0 } { C, 1.0 } ]

USAGE_WEIGHT = 0.5(完全 LRU - 只有最近才重要)

  1. (Added A) "A"      [ { A, 1.0 } ]
  2. (Added B) "BA"     [ { B, 1.0 } { A, 0.5 } ]
  3. (Added C) "CBA"    [ { C, 1.0 } { B, 0.5 } { A, 0.25 } ]
  4. (Added B) "BCA"    [ { B, 1.25 } { C, 0.5 } { A, 0.125 } ]
  5. (Added A) "ABC"    [ { A, 1.0625 } { B, 0.625 } { C, 0.25 } ]
  6. (Added A) "ABC"    [ { A, 1.5313 } { B, 0.3125 } { C, 0.125 } ]
  7. (Added D) "DAB"    [ { D, 1.0 } { A, 0.7656 } { B, 0.1563 } ]
  8. (Added A) "ADB"    [ { A, 1.3828 } { D, 0.5 } { B, 0.0781 } ]
  9. (Added C) "CAD"    [ { C, 1.0 } { A, 0.6914 } { D, 0.25 } ]
 10. (Added D) "DCA"    [ { D, 1.125 } { C, 0.5 } { A, 0.3457 } ]
 11. (Added A) "ADC"    [ { A, 1.1729 } { D, 0.5625 } { C, 0.25 } ]
 12. (Added B) "BAD"    [ { B, 1.0 } { A, 0.5864 } { D, 0.2813 } ]
 13. (Added D) "DBA"    [ { D, 1.1406 } { B, 0.5 } { A, 0.2932 } ]
 14. (Added E) "EDB"    [ { E, 1.0 } { D, 0.5703 } { B, 0.25 } ]
 15. (Added C) "CED"    [ { C, 1.0 } { E, 0.5 } { D, 0.2852 } ]
 16. (Added B) "BCE"    [ { B, 1.0 } { C, 0.5 } { E, 0.25 } ]
 17. (Added A) "ABC"    [ { A, 1.0 } { B, 0.5 } { C, 0.25 } ]

USAGE_WEIGHT = 0.9(LRFU -- LRU 和 LFU 的混合)

  1. (Added A) "A"  [ { A, 1.0 } ]
  2. (Added B) "BA" [ { B, 1.0 } { A, 0.9 } ]
  3. (Added C) "CBA"    [ { C, 1.0 } { B, 0.9 } { A, 0.81 } ]
  4. (Added B) "BCA"    [ { B, 1.81 } { C, 0.9 } { A, 0.729 } ]
  5. (Added A) "ABC"    [ { A, 1.6561 } { B, 1.629 } { C, 0.81 } ]
  6. (Added A) "ABC"    [ { A, 2.4905 } { B, 1.4661 } { C, 0.729 } ]
  7. (Added D) "ABD"    [ { A, 2.2414 } { B, 1.3195 } { D, 1.0 } ]
  8. (Added A) "ABD"    [ { A, 3.0173 } { B, 1.1875 } { D, 0.9 } ]
  9. (Added C) "ABC"    [ { A, 2.7156 } { B, 1.0688 } { C, 1.0 } ]
 10. (Added D) "ADB"    [ { A, 2.444 } { D, 1.0 } { B, 0.9619 } ]
 11. (Added A) "ADB"    [ { A, 3.1996 } { D, 0.9 } { B, 0.8657 } ]
 12. (Added B) "ABD"    [ { A, 2.8796 } { B, 1.7791 } { D, 0.81 } ]
 13. (Added D) "ADB"    [ { A, 2.5917 } { D, 1.729 } { B, 1.6012 } ]
 14. (Added E) "ADE"    [ { A, 2.3325 } { D, 1.5561 } { E, 1.0 } ]
 15. (Added C) "ADC"    [ { A, 2.0993 } { D, 1.4005 } { C, 1.0 } ]
 16. (Added B) "ADB"    [ { A, 1.8893 } { D, 1.2604 } { B, 1.0 } ]
 17. (Added A) "ADB"    [ { A, 2.7004 } { D, 1.1344 } { B, 0.9 } ]

在第一个示例中(USAGE_WEIGHT=1.0),现有项目的分数在添加新项目时不会改变(这是因为我们在每一步都乘以 1.0)。这导致每次使用后将分数简单地增加 1.0,因此分数直接表示使用计数。请注意,项目始终按使用次数递减的顺序列出。

在第二个示例中(USAGE_WEIGHT=0.5),每次添加一个项目时,现有项目的分数减半(这是因为我们每一步都乘以 0.5)。这导致列表中的所有分数都低于最近添加的项目(得分为 1.0)的属性,而且在第 N 步添加的项目总是比在第 N 步添加的项目具有更高的分数。 N 之前的任何步骤,无论该项目被重新添加多少次。这正是产生 LRU 策略的属性。

最后,当 (USAGE_WEIGHT=0.9) 我们可以看到这两种策略是如何混合的。第三个示例开始看起来像 LRU(即“新近度”很重要)。但是随着某些项目的使用次数开始增加,它们开始产生影响并改变行为。这可以从 LRU 列出“DAB”的第 7 步中看出,但由于 A 和 B 的使用率较高,示例 3 显示为“ABD”。然后示例 3 看起来更像 LFU 几个步骤,但有趣的事情发生在步骤10. 这是 LRFU 开始大放异彩的地方。至此,A 被添加了 4 次,而“B”、“C”和“D”分别被添加了 2 次。 LRU 显示“DCA”,因为“D”和“C”是最近添加的,但它忽略了用户选择“A”的可能性是“D”或“C”的两倍这一事实。 LFU 显示“ABD”,除了用户在选择“B”后两次选择“D”外,这表明“D”的使用正在“升温”,因此比“B”更有可能。 LRFU 通过显示“ADB”来正确处理。当然,这有点主观,其他读者可能不同意这是一个更好的选择。毕竟,我们试图根据之前的历史来预测用户未来的选择,所以没有完美的解决方案。但是使用 LRFU,您至少可以“调整” USAGE_WEIGHT 参数以在给定情况下找到 LRU 与 LFU 的正确平衡。

事实上,对于某些应用程序,甚至可能更可取的是动态随着程序的进展而更改 USAGE_WEIGHT 以改进基于历史数据的预测。这对于用户界面 MRU 列表可能没有意义,但对于预测大量或高频事件更有意义。

仅供参考,在本文讨论的 LRFU 算法中,Score 被称为“CRF”。他们讨论的算法还存储了计算每个分数的“时间”(或步数)。通过存储分数和时间,可以只更新正在添加的项目的分数以及项目的一小部分——而不是整个列表。此外,排序顺序由堆和链表数据结构组合维护,因此该算法比我在此处描述的使用简单数组并重新计算和重新计算的算法要高效得多每次添加后对列表进行排序。但这种实现方式简单明了、易于理解,并且适用于用户界面 MRU 列表。

这是一个非常基本的 Java 中 LRFU 列表的实现。在性能方面可以做很多改进,但它充分证明了 LRFU 的结果:

public static void main( String[] args ) {
    double[] weights = { 1.0, 0.5, 0.9 };
    for(double weight : weights) {
        System.out.println("USAGE_WEIGHT = " + weight);
        testMRU(weight);
        System.out.println();
    }
}

private static void testMRU(double weight) {
    PrintStream p = System.out;
    MRUList<String> list = new MRUList<>(3, weight);
    String[] lettersAdded = "A B C B A A D A C D A B D E C B A".split(" ");
    for(int i = 0; i < lettersAdded.length; i++) {
        String value  = lettersAdded[i];
        list.add(value);
        p.printf("%3s. (Added %s) \"", i, value);
        for(MRUItem<String> item : list.list)
            p.print(item.Value);
        p.print("\"\t[ ");
        for(MRUItem<String> item : list.list) {
            p.printf("{ %s, %.5s } ", item.Value, item.Score);
        }
        p.println("]");
    }
}

private static class MRUList<T> {
    public static final double SCORE_INIT = 1.0;

    private double usageWeight; // factor that balances LRU vs LFU
    private int maxSize; // maximum number of MRU items.
    public ArrayList<MRUItem<T>> list;

    public MRUList(int maxItemCount) { this(maxItemCount, 0.9); }
    public MRUList(int maxItemCount, double usageWeightFactor) {
        maxSize = maxItemCount;
        usageWeight = usageWeightFactor;
        list = new ArrayList<>(maxSize);
    }

    // Add an item each time the user chooses it.
    public void add(T value) {
        // Update the score of all existing items
        for(MRUItem<T> item : list)
            item.Score *= usageWeight; // age the items (this does not affect sort order)

        // Search for the item in the list.
        MRUItem<T> existing = find(value);
        if (existing==null) {
            existing = new MRUItem<>(value, SCORE_INIT);
            if (list.size()<maxSize) {
                // we have room -- add the item.
                list.add(existing);
            } else {
                // no more room -- replace last item.
                list.set(list.size() - 1, existing);
            }
        } else {
            // increment the score of the item if it already existed in the list.
            existing.Score += SCORE_INIT;
        }

        // Sort the items for display.
        // Collections.sort uses the Comparable interface of MRUItem.
        Collections.sort(list);
    }

    // Get a copy of the list of items, in the correct display order.
    public List<T> getItems() {
        ArrayList<T> copy = new ArrayList<>();
        for(MRUItem<T> item : list)
            copy.add(item.Value);
        return copy;
    }

    // return an item if it's Value is already present in the list.
    private MRUItem<T> find(T value) {
        for(MRUItem<T> item : list)
            if (Objects.equals(item.Value, value))
                return item;
        return null;
    }
}
private static class MRUItem<T> implements Comparable<MRUItem<T>> {
    public T Value;
    public double Score;
    public MRUItem(final T value, final double score) {
        Score = score;
        Value = value;
    }

    // Sorts by Score in descending order (due to - sign)
    @Override
    public int compareTo(final MRUItem<T> other) {
        return -Double.compare(Score, other.Score);
    }
}

【讨论】:

  • 我真的很喜欢这个解释。这值得被标记为已接受的答案。
猜你喜欢
  • 2019-09-07
  • 2010-09-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-09-22
  • 2011-06-03
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多