Skip to content

Commit 352c7a2

Browse files
authored
FSRS 6 (#104)
* update scheduler to fsrs6 * slight update to optimizer code for fsrs6 compatability * update tests * update minimum stability value * update tests * remove Card.get_retrievability() method in favor of Scheduler.get_card_retrievability() method * bump major 5.1.4 -> 6.0.0 * update README * update short term stability update code * fix test_memo_state unit test * update optimizer unit tests
1 parent d9008db commit 352c7a2

7 files changed

Lines changed: 204 additions & 135 deletions

File tree

README.md

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -97,26 +97,28 @@ from datetime import timedelta
9797
# note: the following arguments are also the defaults
9898
scheduler = Scheduler(
9999
parameters = (
100-
0.40255,
101-
1.18385,
102-
3.173,
103-
15.69105,
104-
7.1949,
105-
0.5345,
106-
1.4604,
107-
0.0046,
108-
1.54575,
109-
0.1192,
110-
1.01925,
111-
1.9395,
112-
0.11,
113-
0.29605,
114-
2.2698,
115-
0.2315,
116-
2.9898,
117-
0.51655,
118-
0.6621,
119-
),
100+
0.2172,
101+
1.1771,
102+
3.2602,
103+
16.1507,
104+
7.0114,
105+
0.57,
106+
2.0966,
107+
0.0069,
108+
1.5261,
109+
0.112,
110+
1.0178,
111+
1.849,
112+
0.1133,
113+
0.3127,
114+
2.2934,
115+
0.2191,
116+
3.0004,
117+
0.7536,
118+
0.3332,
119+
0.1437,
120+
0.2,
121+
),
120122
desired_retention = 0.9,
121123
learning_steps = (timedelta(minutes=1), timedelta(minutes=10)),
122124
relearning_steps = (timedelta(minutes=10),),
@@ -127,7 +129,7 @@ scheduler = Scheduler(
127129

128130
#### Explanation of parameters
129131

130-
`parameters` are a set of 19 model weights that affect how the FSRS scheduler will schedule future reviews. If you're not familiar with optimizing FSRS, it is best not to modify these default values.
132+
`parameters` are a set of 21 model weights that affect how the FSRS scheduler will schedule future reviews. If you're not familiar with optimizing FSRS, it is best not to modify these default values.
131133

132134
`desired_retention` is a value between 0 and 1 that sets the desired minimum retention rate for cards when scheduled with the scheduler. For example, with the default value of `desired_retention=0.9`, a card will be scheduled at a time in the future when the predicted probability of the user correctly recalling that card falls to 90%. A higher `desired_retention` rate will lead to more reviews and a lower rate will lead to fewer reviews.
133135

@@ -150,7 +152,7 @@ You can still specify custom datetimes, but they must use the UTC timezone.
150152
You can calculate the current probability of correctly recalling a card (its 'retrievability') with
151153

152154
```python
153-
retrievability = card.get_retrievability()
155+
retrievability = scheduler.get_card_retrievability(card)
154156

155157
print(f"There is a {retrievability} probability that this card is remembered.")
156158
# > There is a 0.94 probability that this card is remembered.

fsrs/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
ReviewLog,
1313
State,
1414
DEFAULT_PARAMETERS,
15+
STABILITY_MIN,
1516
)
1617

1718
from .optimizer import Optimizer
@@ -24,4 +25,5 @@
2425
"State",
2526
"Optimizer",
2627
"DEFAULT_PARAMETERS",
28+
"STABILITY_MIN",
2729
]

fsrs/fsrs.py

Lines changed: 76 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -20,31 +20,32 @@
2020
from random import random
2121
import time
2222

23+
STABILITY_MIN = 0.001
24+
2325
DEFAULT_PARAMETERS = (
24-
0.40255,
25-
1.18385,
26-
3.173,
27-
15.69105,
28-
7.1949,
29-
0.5345,
30-
1.4604,
31-
0.0046,
32-
1.54575,
33-
0.1192,
34-
1.01925,
35-
1.9395,
36-
0.11,
37-
0.29605,
38-
2.2698,
39-
0.2315,
40-
2.9898,
41-
0.51655,
42-
0.6621,
26+
0.2172,
27+
1.1771,
28+
3.2602,
29+
16.1507,
30+
7.0114,
31+
0.57,
32+
2.0966,
33+
0.0069,
34+
1.5261,
35+
0.112,
36+
1.0178,
37+
1.849,
38+
0.1133,
39+
0.3127,
40+
2.2934,
41+
0.2191,
42+
3.0004,
43+
0.7536,
44+
0.3332,
45+
0.1437,
46+
0.2,
4347
)
4448

45-
DECAY = -0.5
46-
FACTOR = 0.9 ** (1 / DECAY) - 1
47-
4849
FUZZ_RANGES = [
4950
{
5051
"start": 2.5,
@@ -211,29 +212,6 @@ def from_dict(source_dict: dict[str, int | float | str | None]) -> Card:
211212
last_review=last_review,
212213
)
213214

214-
def get_retrievability(self, current_datetime: datetime | None = None) -> float:
215-
"""
216-
Calculates the Card object's current retrievability for a given date and time.
217-
218-
The retrievability of a card is the predicted probability that the card is correctly recalled at the provided datetime.
219-
220-
Args:
221-
current_datetime (datetime): The current date and time
222-
223-
Returns:
224-
float: The retrievability of the Card object.
225-
"""
226-
227-
if self.last_review is None:
228-
return 0
229-
230-
if current_datetime is None:
231-
current_datetime = datetime.now(timezone.utc)
232-
233-
elapsed_days = max(0, (current_datetime - self.last_review).days)
234-
235-
return (1 + FACTOR * elapsed_days / self.stability) ** DECAY
236-
237215

238216
class ReviewLog:
239217
"""
@@ -363,6 +341,9 @@ def __init__(
363341
self.maximum_interval = maximum_interval
364342
self.enable_fuzzing = enable_fuzzing
365343

344+
self._DECAY = -self.parameters[20]
345+
self._FACTOR = 0.9 ** (1 / self._DECAY) - 1
346+
366347
def __repr__(self) -> str:
367348
return (
368349
f"{self.__class__.__name__}("
@@ -374,6 +355,32 @@ def __repr__(self) -> str:
374355
f"enable_fuzzing={self.enable_fuzzing})"
375356
)
376357

358+
def get_card_retrievability(
359+
self, card: Card, current_datetime: datetime | None = None
360+
) -> float:
361+
"""
362+
Calculates a Card object's current retrievability for a given date and time.
363+
364+
The retrievability of a card is the predicted probability that the card is correctly recalled at the provided datetime.
365+
366+
Args:
367+
card (Card): The card whose retriebility is to be calculated
368+
current_datetime (datetime): The current date and time
369+
370+
Returns:
371+
float: The retrievability of the Card object.
372+
"""
373+
374+
if card.last_review is None:
375+
return 0
376+
377+
if current_datetime is None:
378+
current_datetime = datetime.now(timezone.utc)
379+
380+
elapsed_days = max(0, (current_datetime - card.last_review).days)
381+
382+
return (1 + self._FACTOR * elapsed_days / card.stability) ** self._DECAY
383+
377384
def review_card(
378385
self,
379386
card: Card,
@@ -436,8 +443,9 @@ def review_card(
436443
card.stability = self._next_stability(
437444
difficulty=card.difficulty,
438445
stability=card.stability,
439-
retrievability=card.get_retrievability(
440-
current_datetime=review_datetime
446+
retrievability=self.get_card_retrievability(
447+
card,
448+
current_datetime=review_datetime,
441449
),
442450
rating=rating,
443451
)
@@ -510,8 +518,9 @@ def review_card(
510518
card.stability = self._next_stability(
511519
difficulty=card.difficulty,
512520
stability=card.stability,
513-
retrievability=card.get_retrievability(
514-
current_datetime=review_datetime
521+
retrievability=self.get_card_retrievability(
522+
card,
523+
current_datetime=review_datetime,
515524
),
516525
rating=rating,
517526
)
@@ -551,7 +560,8 @@ def review_card(
551560
difficulty=card.difficulty,
552561
stability=card.stability,
553562
retrievability=card.get_retrievability(
554-
current_datetime=review_datetime
563+
scheduler_parameters=self.parameters,
564+
current_datetime=review_datetime,
555565
),
556566
rating=rating,
557567
)
@@ -691,9 +701,9 @@ def _clamp_difficulty(self, difficulty: float) -> float:
691701

692702
def _clamp_stability(self, stability: float) -> float:
693703
if isinstance(stability, (float, int)):
694-
stability = max(stability, 0.01)
704+
stability = max(stability, STABILITY_MIN)
695705
else: # type(stability) is torch.Tensor
696-
stability = stability.clamp(min=0.01)
706+
stability = stability.clamp(min=STABILITY_MIN)
697707

698708
return stability
699709

@@ -714,8 +724,8 @@ def _initial_difficulty(self, rating: Rating) -> float:
714724
return initial_difficulty
715725

716726
def _next_interval(self, stability: float) -> int:
717-
next_interval = (stability / FACTOR) * (
718-
(self.desired_retention ** (1 / DECAY)) - 1
727+
next_interval = (stability / self._FACTOR) * (
728+
(self.desired_retention ** (1 / self._DECAY)) - 1
719729
)
720730

721731
next_interval = round(float(next_interval)) # intervals are full days
@@ -729,9 +739,19 @@ def _next_interval(self, stability: float) -> int:
729739
return next_interval
730740

731741
def _short_term_stability(self, stability: float, rating: Rating) -> float:
732-
short_term_stability = stability * (
742+
short_term_stability_increase = (
733743
math.e ** (self.parameters[17] * (rating - 3 + self.parameters[18]))
734-
)
744+
) * (stability ** -self.parameters[19])
745+
746+
if rating in (Rating.Good, Rating.Easy):
747+
if isinstance(short_term_stability_increase, (float, int)):
748+
short_term_stability_increase = max(short_term_stability_increase, 1.0)
749+
else: # type(short_term_stability_increase) is torch.Tensor
750+
short_term_stability_increase = short_term_stability_increase.clamp(
751+
min=1.0
752+
)
753+
754+
short_term_stability = stability * short_term_stability_increase
735755

736756
short_term_stability = self._clamp_stability(short_term_stability)
737757

fsrs/optimizer.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
This module defines the optional Optimizer class.
66
"""
77

8-
from .fsrs import Card, ReviewLog, Scheduler, Rating, DEFAULT_PARAMETERS
8+
from .fsrs import Card, ReviewLog, Scheduler, Rating, DEFAULT_PARAMETERS, STABILITY_MIN
99
import math
1010
from datetime import datetime, timezone
1111
from copy import deepcopy
@@ -19,14 +19,13 @@
1919
import pandas as pd
2020

2121
# weight clipping
22-
S_MIN = 0.01
2322
INIT_S_MAX = 100.0
2423
lower_bounds = torch.tensor(
2524
[
26-
S_MIN,
27-
S_MIN,
28-
S_MIN,
29-
S_MIN,
25+
STABILITY_MIN,
26+
STABILITY_MIN,
27+
STABILITY_MIN,
28+
STABILITY_MIN,
3029
1.0,
3130
0.1,
3231
0.1,
@@ -42,6 +41,8 @@
4241
1.0,
4342
0.0,
4443
0.0,
44+
0.0,
45+
0.1,
4546
],
4647
dtype=torch.float64,
4748
)
@@ -66,6 +67,8 @@
6667
6.0,
6768
2.0,
6869
2.0,
70+
0.8,
71+
0.8,
6972
],
7073
dtype=torch.float64,
7174
)
@@ -162,7 +165,9 @@ def _compute_batch_loss(self, parameters: list[float]) -> float:
162165
if i == 0:
163166
card = Card(card_id=card_id, due=x_date)
164167

165-
y_pred_retrievability = card.get_retrievability(x_date)
168+
y_pred_retrievability = scheduler.get_card_retrievability(
169+
card=card, current_datetime=x_date
170+
)
166171
y_retrievability = torch.tensor(
167172
y_retrievability, dtype=torch.float64
168173
)
@@ -322,7 +327,9 @@ def _update_parameters(
322327
card = Card(card_id=card_id, due=x_date)
323328

324329
# predicted target
325-
y_pred_retrievability = card.get_retrievability(x_date)
330+
y_pred_retrievability = scheduler.get_card_retrievability(
331+
card=card, current_datetime=x_date
332+
)
326333
y_retrievability = torch.tensor(
327334
y_retrievability, dtype=torch.float64
328335
)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "fsrs"
7-
version = "5.1.4"
7+
version = "6.0.0"
88
description = "Free Spaced Repetition Scheduler"
99
readme = "README.md"
1010
authors = [

0 commit comments

Comments
 (0)