FSRS核心字段

无聊看了下 FSRS (Free Spaced Repetition Scheduler) ,想看它怎么存数据的

Card — 卡片当前状态

表结构:

字段 类型 说明
due Date 下次复习时间
stability float 记忆稳定性(R=90% 时的间隔天数)
difficulty float 难度,范围 [1, 10]
state int 状态机:New=0, Learning=1, Review=2, Relearning=3
learning_steps int 当前学习步骤 index
scheduled_days int 本次调度天数
reps int 总复习次数
lapses int 遗忘次数
last_review Date? 上次复习时间

核心字段详解

1. state

状态机,取值

         Again          Good/Hard/Easy
New ──────────→ Learning ──────────────→ Review
                  ↑  │                      │
                  │  │ Again (learning步内)  │
                  │  └──────────────────────┘
                  │                           │
                  │      Again (遗忘)         │
                  └─────── Relearning ←──────┘

state 决定调度公式分支,不能从 stability/difficulty 推导。同一个 stability/difficulty 的卡片,Review 和 Relearning 走完全不同的公式。

2. learning_steps

learning_steps 字段记录的是当前走到第几步(0-based index)。

新卡片第一次看到时,你不可能直接让它 10 天后再复习。所以先用预设的短间隔反复巩固:

默认 learning_steps = ['1m', '10m']

某些测试用例里能看到 ['1m', '10m', '30m', '1h', '6h', '12h']

New ──[首次复习]──→ Learning (step=0)
                        │
                    [Good]│→ Learning (step=1)
                        │       │
                    [Good]│→ Review (毕业,FSRS接管)
                        │
                    [Again]│→ 回到 step=0
Review ──[Again]──→ Relearning (step=0)
                        │
                    [Good]│→ Review (重新毕业)

3. difficulty

一般取值1-10

3.1 初始化(New 卡片首次复习)

init_difficulty(g: Grade): number {
  const d = w[4] - Math.exp((g - 1) * w[5]) + 1
  return clamp(roundTo(d, 8), 1, 10)
}

从 grade(1-4)计算初始难度。grade 越大(记得越好),难度越低。

3.2 更新(已有卡片复习)

next_difficulty(d: number, g: Grade): number {
  const delta_d = -w[6] * (g - 3)          // grade>3 降难度,grade<3 升难度
  const next_d = d + linear_damping(delta_d, d)  // 线性阻尼防止越界
  return clamp(mean_reversion(init_easy, next_d), 1, 10)  // 均值回归
}

从当前 difficulty 和 本次 grade 推算下一个 difficulty。
关键结论
difficulty 是纯计算值,只依赖
- 上一次的值
- 本次 grade(1-4)
- 权重参数 w4, w5, w6, w7

不需要额外存储输入参数。每次复习时算法读 difficulty → 算新的 → 写回去

3. last_review

上一次 review 时间

4. stability

FSRS的核心指标,是个 float,看定义

export const S_MIN = 0.001
export const S_MAX = 36500.0

极限精简字段

FSRS 最终算 interval 时是 round(s * modifier) 取整到天

之前最先学习的,到期那一天就先复习

考虑到db检索的方便性,直接存 4byte due_at 代替 last_review,然后1byte due_days 用来反推 last_review

为什么 due_days 0-255 ?需要连续 Good 评分 7-8 次才能突破 256 天。要么你彻底记住,要么就忘干净了。无所谓了。

┌──────────┬───────────┬──────────────┐
│ 2 bit │ 4 bit │ 16 bit │
│ step │ difficulty│ stability │
│ 0,1,2,3 │ 0-11→1-10 │ FP16 │
└──────────┴───────────┴──────────────┘


更新

实际上 due_days 都不用存

min(max_interval, max(1, round(stability * interval_modifier)))

直接可以 stability 反推。囧。突然有点领会FSRS精华在哪里了。它丫的其实没啥调度算法。

甚至都不是给每张卡训练一条曲线。就是用数据集训练一组全局参数 w,设定为标准遗忘曲线长什么样,然后每次复习后就去更新 stability difficulty,也就是说S D这两个参数去映射这个曲线。

值钱的是这个曲线。。

Comments