17

I work on a project where Spaced Repetition is essential, however I am not a specialist on the subject and I am afraid to reinvent the square wheel. My research pointed me two different systems, namely the Leitner system and the SM family of algorithms.
I haven't decided yet which system would best fit into my project. If I was to take a SM orientation, I guess I would try to implement something similar to what Anki uses.

My best option would be to use an existing Java library. It could be quite simple, all I need is to compute the time for the next repetition.
Has anyone heard of such an initiative ?

Mikael Couzic
  • 12,283
  • 5
  • 22
  • 16

4 Answers4

12

I did reinvent the square wheel in my own flashcard app. The algorithm is quite simple: The weight of an item is the product of an age component, a progress component, and an effort component.

Age component

The formula is A(x) = Cn^x, where

  • x is the time in days since the item was last tested,
  • C is the value you want when x is zero, and
  • n is a constant based on how fast you want the value to increase as x increases.

For example, if you want the value to double every five days, n = e^(ln(2/C)/5).

Progress component

The formula is P(x) = Cn^-x, where

  • x is a number that corresponds to how successful you've been with the item,
  • C is the value you want when x is zero, and
  • n is a constant based on how fast you want the value to decay as x increases.

For example, if you want the value to halve every five consecutive successes, n = e^(ln(1/2)/-5).

Effort component

This takes on one of two values:

  • 10 if you found your last recall of the item to be "hard", or
  • 1 otherwise.

The progress is adjusted thus:

  • New entries start with progress 0.
  • If you find an answer easy, the item's progress is increased by 1.
  • If you find an answer hard, the item's progress goes to min(int(previous / 2), previous - 1).
  • If you get an answer wrong, the item's progress goes to min(-1, previous - 1).

Yes, values can go negative. :)

The app selects the next item to test by making a random selection from all the items, with the probability of selection varying directly with an item's weight.

The specific numbers in the algorithm are tweakable. I've been using my current values for about a year, leading to great success in accumulating and retaining vocabulary for Spanish, German, and Latin.

(Sorry for potato quality of the math expressions. LaTeX isn't allowed here.)

Tony
  • 1,221
  • 2
  • 12
  • 25
12

I haven't looked at Anki's implementation but have you seen this one? quiz-me an SRS in Java.

Basically it goes like this

public static void calcuateInterval(Card card) {
  if (card.getEFactor() < 3) {
      card.setCount(1);
  }
  int count = card.getCount();
  int interval = 1;
  if (count == 2) {
      interval = 6;
  } else if (count > 2) {
     interval =  Math.round(card.getInterval() * card.getEFactor());
  }
  card.setInterval(interval);
}

If you really want Anki's algorithm, look through the source of Anki in Android available in Github. It is GPL though so you might need to buy a license.

Tsundoku
  • 2,455
  • 1
  • 19
  • 31
Eugene Ramirez
  • 3,053
  • 2
  • 23
  • 17
8

Anki uses the SM2 algorithm. However, SM2 as described by that article has a number of serious flaws. Fortunately, they are simple to fix.

Explaining how to do so would be too lengthy of a subject for this post, so I've written a blog post about it here. There is no need to use an open-source library to do this, as the actual implementation is incredibly simple.

BlueRaja - Danny Pflughoeft
  • 84,206
  • 33
  • 197
  • 283
-1

Here is a spaced repetition algorithm that is well documented and easy to understand.

Features

  • Introduces sub-decks for efficiently learning large decks (Super useful!)
  • Intuitive variable names and algorithm parameters. Fully open-source with human-readable examples.
  • Easily configurable parameters to accommodate for different users' memorization abilities.
  • Computationally cheap to compute next card. No need to run a computation on every card in the deck.

https://github.com/Jakobovski/SaneMemo.

Disclaimer: I am the author of SaneMemo.

import random
import datetime

# The number of times needed for the user to get the card correct(EASY) consecutively before removing the card from
# the current sub_deck.
CONSECUTIVE_CORRECT_TO_REMOVE_FROM_SUBDECK_WHEN_KNOWN = 2
CONSECUTIVE_CORRECT_TO_REMOVE_FROM_SUBDECK_WHEN_WILL_FORGET = 3

# The number of cards in the sub-deck
SUBDECK_SIZE = 15
REMINDER_RATE = 1.6

class Deck(object):

    def __init__(self):
        self.cards = []

        # Used to make sure we don't display the same card twice
        self.last_card = None

    def add_card(self, card):
        self.cards.append(card)

    def get_next_card(self):
        self.cards = sorted(self.cards)  # Sorted by next_practice_time
        sub_deck = self.cards[0:min(SUBDECK_SIZE, len(self.cards))]
        card = random.choice(sub_deck)

        # In case card == last card lets select again. We don't want to show the same card two times in a row.
        while card == self.last_card:
            card = random.choice(sub_deck)

        self.last_card = card
        return card


class Card(object):

    def __init__(self, front, back):
        self.front = front
        self.back = back

        self.next_practice_time = datetime.utc.now()
        self.consecutive_correct_answer = 0
        self.last_time_easy = datetime.utc.now()

    def update(self, performance_str):
        """ Updates the card after the user has seen it and answered how difficult it was. The user can provide one of
        three options: [I_KNOW, KNOW_BUT_WILL_FORGET, DONT_KNOW].
        """

        if performance_str == "KNOW_IT":
            self.consecutive_correct_answer += 1

            if self.consecutive_correct_answer >= CONSECUTIVE_CORRECT_TO_REMOVE_FROM_SUBDECK_WHEN_KNOWN:
                days_since_last_easy = (datetime.utc.now() - self.last_time_easy).days
                days_to_next_review = (days_since_last_easy + 2) * REMINDER_RATE
                self.next_practice_time = datetime.utc.now() + datetime.time(days=days_to_next_review)
                self.last_time_easy = datetime.utc.now()
            else:
                self.next_practice_time = datetime.utc.now()

        elif performance_str == "KNOW_BUT_WILL_FORGET":
            self.consecutive_correct_answer += 1

            if self.consecutive_correct_answer >= CONSECUTIVE_CORRECT_TO_REMOVE_FROM_SUBDECK_WHEN_WILL_FORGET:
                self.next_practice_time = datetime.utc.now() + datetime.time(days=1)
            else:
                self.next_practice_time = datetime.utc.now()

        elif performance_str == "DONT_KNOW":
            self.consecutive_correct_answer = 0
            self.next_practice_time = datetime.utc.now()

    def __cmp__(self, other):
        """Comparator or sorting cards by next_practice_time"""
        if hasattr(other, 'next_practice_time'):
            return self.number.__cmp__(other.next_practice_time)
Jakobovski
  • 3,203
  • 1
  • 31
  • 38