FSRS核心字段
Posted | stdout
无聊看了下 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
状态机,取值
- New=0
- Learning=1
- Review=2
- Relearning=3
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']
- 第1次看 → 1分钟后复习
- 第2次看 → 10分钟后复习
- 第3次看 → 毕业,进入 Review,走 FSRS 长期间隔(几天→几周→几月)
某些测试用例里能看到 ['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
极限精简字段
- stability==0 默认 state=0。
- step给四个值0,1,2,然后为3表示走review。也代替 state
- difficulty 给 16 状态够了
- fp16 拿来存 stability
- 32bit 拿来存 due_at
- 1byte 拿来存 due_days
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