docs: 복습 알고리즘 상세 설명서 (ALGORITHM.md)
SM-2 하이브리드와 에빙하우스 망각곡선 두 경로의 수식, 상수, 의사결정 분기, 페르소나/강도 효과를 코드 레벨에서 설명. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
362
ALGORITHM.md
Normal file
362
ALGORITHM.md
Normal file
@@ -0,0 +1,362 @@
|
||||
# ReLoop v2 복습 알고리즘 상세 설명
|
||||
|
||||
> 이 문서는 ReLoop v2의 복습 스케줄링이 어떻게 작동하는지를 코드 레벨에서 설명합니다.
|
||||
|
||||
## 요약: 두 가지 경로
|
||||
|
||||
ReLoop은 **두 가지 복습 스케줄링 경로**를 가지고 있고, 사용자의 행동에 따라 자동으로 갈림:
|
||||
|
||||
| 경로 | 언제 사용? | 핵심 아이디어 |
|
||||
|------|-----------|-------------|
|
||||
| **SM-2 하이브리드** | 사용자가 "어려웠어/괜찮았어/쉬웠어"를 선택했을 때 | 체감 난이도로 다음 복습 간격 결정 |
|
||||
| **에빙하우스 망각곡선** | 난이도 선택 없이 결과만 기록했을 때 (legacy) | 기억 감쇄 시뮬레이션으로 복습 시점 계산 |
|
||||
|
||||
현재 앱의 주요 UI(복습 세션, e-book 뷰어, 처음 풀기)는 모두 `selfDifficulty`를 보내므로 **SM-2 경로가 기본**이다.
|
||||
|
||||
---
|
||||
|
||||
## 경로 1: SM-2 하이브리드 (메인)
|
||||
|
||||
### 1.1 개념
|
||||
|
||||
SM-2는 SuperMemo 2의 약자로, 1987년에 만들어진 간격 반복 알고리즘이다. 핵심 아이디어:
|
||||
|
||||
- 각 문제(태그)마다 **EF (Ease Factor, 쉬움 계수)**를 관리한다 (기본값 2.5)
|
||||
- 쉽다고 느끼면 EF가 올라가고, 어렵다고 느끼면 내려간다
|
||||
- 다음 복습 간격 = `이전 간격 × EF`
|
||||
- EF가 높을수록 간격이 빠르게 벌어진다
|
||||
|
||||
ReLoop은 여기에 **페르소나 배율**을 곱해서 학습자 유형별로 조정한다.
|
||||
|
||||
### 1.2 EF (Ease Factor) 업데이트
|
||||
|
||||
사용자가 "어려웠어/괜찮았어/쉬웠어"를 선택할 때마다:
|
||||
|
||||
```
|
||||
어려웠어 → EF = max(1.3, 현재EF - 0.3) // 급격히 하락
|
||||
괜찮았어 → EF = 현재EF // 변동 없음
|
||||
쉬웠어 → EF = max(1.3, 현재EF + 0.15) // 소폭 상승
|
||||
```
|
||||
|
||||
- 최소 EF는 **1.3** (이 이하로 내려가지 않음)
|
||||
- 초기 EF는 **2.5**
|
||||
- EF는 `SkillSnapshot.s0` 필드에 저장됨 (태그별로 하나)
|
||||
|
||||
**예시:**
|
||||
```
|
||||
시작: EF = 2.5
|
||||
어려웠어: EF = 2.5 - 0.3 = 2.2
|
||||
괜찮았어: EF = 2.2 (그대로)
|
||||
쉬웠어: EF = 2.2 + 0.15 = 2.35
|
||||
어려웠어: EF = 2.35 - 0.3 = 2.05
|
||||
어려웠어: EF = 2.05 - 0.3 = 1.75
|
||||
어려웠어: EF = 1.75 - 0.3 = 1.45
|
||||
어려웠어: EF = max(1.3, 1.45 - 0.3) = 1.3 ← 바닥
|
||||
```
|
||||
|
||||
### 1.3 복습 간격 계산
|
||||
|
||||
```
|
||||
페르소나 배율 (pf):
|
||||
상위권 (senior) = 1.3 → 간격이 30% 더 넓어짐
|
||||
중위권 (mid) = 1.0 → 기본
|
||||
하위권 (junior) = 0.8 → 간격이 20% 좁아짐
|
||||
벼락치기 (crammer)= 0.6 → 간격이 40% 좁아짐
|
||||
```
|
||||
|
||||
**어려웠어 → 무조건 1일 후** (어떤 상황이든):
|
||||
```
|
||||
간격 = 1일
|
||||
```
|
||||
|
||||
**처음 푸는 문제 (iteration = 0):**
|
||||
```
|
||||
괜찮았어 → 간격 = round(3 × pf)일
|
||||
쉬웠어 → 간격 = round(7 × pf)일
|
||||
```
|
||||
|
||||
| 체감 난이도 | senior (×1.3) | mid (×1.0) | junior (×0.8) | crammer (×0.6) |
|
||||
|------------|:---:|:---:|:---:|:---:|
|
||||
| 어려웠어 | 1일 | 1일 | 1일 | 1일 |
|
||||
| 괜찮았어 | 4일 | 3일 | 2일 | 2일 |
|
||||
| 쉬웠어 | 9일 | 7일 | 6일 | 4일 |
|
||||
|
||||
**첫 번째 복습 (iteration = 1):**
|
||||
```
|
||||
어려웠어 → 1일
|
||||
그 외 → 간격 = round(6 × pf)일
|
||||
```
|
||||
|
||||
**두 번째 이후 복습 (iteration >= 2):**
|
||||
```
|
||||
어려웠어 → 1일
|
||||
그 외 → 간격 = round(6 × EF^(iteration-1) × pf)일
|
||||
```
|
||||
|
||||
| iteration | EF=2.5, mid(×1.0) | EF=2.5, senior(×1.3) | EF=1.5, mid(×1.0) |
|
||||
|:---------:|:---:|:---:|:---:|
|
||||
| 0 (처음) | 3일 (괜찮음) | 4일 | 3일 |
|
||||
| 1 | 6일 | 8일 | 6일 |
|
||||
| 2 | 15일 | 20일 | 9일 |
|
||||
| 3 | 38일 | 49일 | 14일 |
|
||||
| 4 | 60일 (cap) | 60일 (cap) | 20일 |
|
||||
|
||||
**모든 간격은 최소 1일, 최대 60일로 제한된다.**
|
||||
|
||||
### 1.4 전체 흐름 (SM-2 경로)
|
||||
|
||||
```
|
||||
[사용자가 문제를 풀고 "어려웠어" 클릭]
|
||||
↓
|
||||
1. SkillSnapshot에서 현재 EF 로드 (없으면 2.5)
|
||||
↓
|
||||
2. EF 업데이트: EF = max(1.3, EF - 0.3)
|
||||
↓
|
||||
3. 간격 계산: 1일 (어려웠어는 항상 1일)
|
||||
↓
|
||||
4. 간격 × 페르소나 배율 (어려웠어는 1일 고정이라 배율 적용 안 됨)
|
||||
↓
|
||||
5. ReviewSchedule 생성 (scheduledAt = 지금 + 1일)
|
||||
↓
|
||||
6. SkillSnapshot.s0에 새 EF 저장
|
||||
↓
|
||||
[1일 후 복습 큐에 등장]
|
||||
↓
|
||||
[사용자가 "쉬웠어" 클릭]
|
||||
↓
|
||||
1. EF = max(1.3, 이전EF + 0.15)
|
||||
↓
|
||||
2. 간격 = round(6 × EF^(1-1) × pf) = round(6 × 1 × pf) = 6일 (mid 기준)
|
||||
↓
|
||||
3. 새 ReviewSchedule (scheduledAt = 지금 + 6일)
|
||||
↓
|
||||
[6일 후 다시 복습 큐에 등장]
|
||||
↓
|
||||
... 반복 (iteration 증가 → EF 지수승 → 간격 기하급수적 확대)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 경로 2: 에빙하우스 망각곡선 (Legacy)
|
||||
|
||||
> 이 경로는 `selfDifficulty`가 없을 때만 사용됨. 현재 UI에서는 거의 발생하지 않는다.
|
||||
|
||||
### 2.1 개념
|
||||
|
||||
에빙하우스의 망각곡선은 "시간이 지남에 따라 기억이 지수적으로 감쇄한다"는 모델:
|
||||
|
||||
```
|
||||
S(t) = S₀ × e^(-λ × t)
|
||||
```
|
||||
|
||||
- `S₀`: 현재 기억 강도 (0~1). 문제를 맞히면 올라가고, 틀리면 내려감
|
||||
- `λ` (람다): 망각 속도. 페르소나별로 다름
|
||||
- `t`: 마지막 학습으로부터 경과 일수
|
||||
|
||||
여기에 "정답 확률"을 시그모이드 함수로 변환:
|
||||
|
||||
```
|
||||
P(정답) = sigmoid(k × (S(t) - D))
|
||||
= 1 / (1 + e^(-k × (S(t) - D)))
|
||||
```
|
||||
|
||||
- `k`: 시그모이드 기울기 (고정값 4.0). 클수록 S(t)의 작은 변화에도 P가 급변
|
||||
- `D`: 문제 난이도 (0~1). 높을수록 어려운 문제
|
||||
|
||||
### 2.2 S₀ (기억 강도) 업데이트
|
||||
|
||||
문제를 풀 때마다 결과에 따라 S₀가 변경:
|
||||
|
||||
```
|
||||
맞음 (correct) → 새S₀ = clamp(이전S₀ × 0.5 + 0.6, 0, 1)
|
||||
부분 (partial) → 새S₀ = clamp(이전S₀ × 0.7 + 0.3, 0, 1)
|
||||
틀림 (incorrect) → 새S₀ = clamp(이전S₀ × 0.5, 0, 1)
|
||||
```
|
||||
|
||||
| 이전 S₀ | 맞음 | 부분 | 틀림 |
|
||||
|:-------:|:----:|:----:|:----:|
|
||||
| 0.3 (초기) | 0.75 | 0.51 | 0.15 |
|
||||
| 0.5 | 0.85 | 0.65 | 0.25 |
|
||||
| 0.8 | 1.0 | 0.86 | 0.40 |
|
||||
|
||||
### 2.3 다음 복습 시점 계산
|
||||
|
||||
"정답 확률이 기준선 아래로 떨어지는 시점"을 구한다:
|
||||
|
||||
```
|
||||
기준선 (P_threshold) — 학습 강도 설정:
|
||||
엄격 (strict) = 0.70 → 아직 70%일 때 미리 복습 (자주)
|
||||
균형 (moderate) = 0.50 → 반반일 때 복습 (기본)
|
||||
여유 (relaxed) = 0.35 → 많이 까먹은 후 복습 (드물게)
|
||||
```
|
||||
|
||||
수학적으로 풀면:
|
||||
```
|
||||
P_threshold = sigmoid(k × (S₀ × e^(-λ × t) - D))
|
||||
|
||||
이것을 t에 대해 풀면:
|
||||
|
||||
t = -(1/λ) × ln((D + logit(P_threshold)/k) / S₀)
|
||||
|
||||
여기서 logit(p) = ln(p / (1-p))
|
||||
```
|
||||
|
||||
### 2.4 예시 시나리오
|
||||
|
||||
**설정:** 중위권(mid, λ=0.2), 균형(moderate, threshold=0.5), S₀=0.75, D=0.5
|
||||
|
||||
```
|
||||
logit(0.5) = ln(0.5/0.5) = 0
|
||||
target = 0.5 + 0/4 = 0.5
|
||||
|
||||
t = -(1/0.2) × ln(0.5 / 0.75)
|
||||
= -5 × ln(0.667)
|
||||
= -5 × (-0.405)
|
||||
= 2.03일 → 약 2일 후 복습
|
||||
```
|
||||
|
||||
**같은 조건, 상위권(senior, λ=0.1):**
|
||||
```
|
||||
t = -(1/0.1) × ln(0.5 / 0.75)
|
||||
= -10 × (-0.405)
|
||||
= 4.05일 → 약 4일 후 복습 (느리게 잊으니까 더 늦게)
|
||||
```
|
||||
|
||||
**같은 조건, 벼락치기(crammer, λ=0.6):**
|
||||
```
|
||||
t = -(1/0.6) × ln(0.5 / 0.75)
|
||||
= -1.67 × (-0.405)
|
||||
= 0.68일 → 약 16시간 후 복습 (빨리 잊으니까 빨리)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 페르소나별 효과 비교
|
||||
|
||||
### SM-2 경로에서 (현재 메인)
|
||||
|
||||
페르소나는 **PERSONA_FACTOR 배율**로만 영향:
|
||||
|
||||
```
|
||||
상위권 ×1.3 → 모든 간격이 30% 넓어짐
|
||||
중위권 ×1.0 → 기본
|
||||
하위권 ×0.8 → 모든 간격이 20% 좁아짐
|
||||
벼락치기 ×0.6 → 모든 간격이 40% 좁아짐
|
||||
```
|
||||
|
||||
학습 강도(reviewIntensity)는 **SM-2에 영향 없음**.
|
||||
|
||||
### 망각곡선 경로에서 (legacy)
|
||||
|
||||
페르소나는 **PERSONA_LAMBDA (망각 속도)**로 영향:
|
||||
```
|
||||
상위권: λ=0.1 → 하루에 10%씩 감쇄 → 천천히 잊음 → 넓은 간격
|
||||
중위권: λ=0.2 → 하루에 20%씩 감쇄 → 보통
|
||||
하위권: λ=0.4 → 하루에 40%씩 감쇄 → 빨리 잊음 → 좁은 간격
|
||||
벼락치기: λ=0.6 → 하루에 60%씩 감쇄 → 매우 빨리 잊음 → 매우 좁은 간격
|
||||
```
|
||||
|
||||
학습 강도는 **P_threshold**로 영향:
|
||||
```
|
||||
엄격: P가 70% 아래로 떨어지기 전에 복습 → 자주 복습
|
||||
균형: P가 50% 아래로 떨어지기 전에 복습 → 보통
|
||||
여유: P가 35% 아래로 떨어지기 전에 복습 → 드물게 복습
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## SkillSnapshot의 이중 용도
|
||||
|
||||
`SkillSnapshot.s0` 필드는 **경로에 따라 다른 값을 저장**한다:
|
||||
|
||||
| 경로 | s0에 저장되는 값 | 범위 | 의미 |
|
||||
|------|---------------|------|------|
|
||||
| SM-2 | EF (Ease Factor) | 1.3 ~ 4.0+ | 높을수록 쉬운 문제 |
|
||||
| 망각곡선 | 기억 강도 | 0 ~ 1.0 | 높을수록 잘 기억하는 중 |
|
||||
|
||||
**구분 방법:** `s0 >= 1.3 && s0 <= 4.0`이면 SM-2 경로로 판단.
|
||||
|
||||
---
|
||||
|
||||
## 밀린 복습 자동 이월
|
||||
|
||||
복습 예정일에 안 풀면 **다음날 아침 9시(KST)로 자동 이월**:
|
||||
|
||||
```
|
||||
복습 큐 조회 시 (GET /reviews/queue):
|
||||
1. 오늘 00:00 KST 이전의 pending 상태 복습을 찾음
|
||||
2. 해당 복습의 scheduledAt을 "오늘 09:00 KST"로 변경
|
||||
3. 변경된 수를 rescheduledCount로 반환
|
||||
```
|
||||
|
||||
단, 수학 과목 + 사용자 직접 등록 문제만 이월됨 (기출 문제집 문제는 제외).
|
||||
|
||||
---
|
||||
|
||||
## 페르소나 변경 시 재계산
|
||||
|
||||
프로필에서 페르소나나 학습 강도를 바꾸면 **모든 pending 복습 일정이 재계산**된다:
|
||||
|
||||
1. 해당 유저의 pending 상태 ReviewSchedule 전부 로드
|
||||
2. 각 리뷰에 대해:
|
||||
- SM-2 경로: 새 페르소나의 PERSONA_FACTOR로 간격 재계산 (selfDifficulty='medium' 가정)
|
||||
- 망각곡선 경로: 새 λ, 새 threshold로 다음 복습 시점 재계산
|
||||
3. scheduledAt 업데이트
|
||||
|
||||
---
|
||||
|
||||
## 상수 요약 (빠른 참조)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ SM-2 상수 │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ DEFAULT_EF = 2.5 (초기 쉬움 계수) │
|
||||
│ MIN_EF = 1.3 (최소 EF) │
|
||||
│ MAX_INTERVAL = 60일 (최대 복습 간격) │
|
||||
│ Hard EF 패널티 = -0.3 │
|
||||
│ Easy EF 보상 = +0.15 │
|
||||
│ │
|
||||
│ 초기 간격 (iteration=0): │
|
||||
│ 어려웠어 = 1일 │
|
||||
│ 괜찮았어 = 3 × pf 일 │
|
||||
│ 쉬웠어 = 7 × pf 일 │
|
||||
│ │
|
||||
│ 이후 간격: │
|
||||
│ 어려웠어 = 1일 │
|
||||
│ 그 외 = 6 × EF^(iter-1) × pf 일 │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ PERSONA_FACTOR (SM-2 간격 배율) │
|
||||
│ 상위권 = 1.3 │
|
||||
│ 중위권 = 1.0 │
|
||||
│ 하위권 = 0.8 │
|
||||
│ 벼락치기 = 0.6 │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ 망각곡선 상수 │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ PERSONA_LAMBDA (망각 속도, day⁻¹) │
|
||||
│ 상위권 = 0.1 │
|
||||
│ 중위권 = 0.2 │
|
||||
│ 하위권 = 0.4 │
|
||||
│ 벼락치기 = 0.6 │
|
||||
│ │
|
||||
│ INTENSITY_THRESHOLD (복습 기준선) │
|
||||
│ 엄격 = 0.70 │
|
||||
│ 균형 = 0.50 │
|
||||
│ 여유 = 0.35 │
|
||||
│ │
|
||||
│ DEFAULT_K = 4.0 (시그모이드 기울기) │
|
||||
│ DEFAULT_INITIAL_S0 = 0.3 (초기 기억 강도) │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 코드 위치
|
||||
|
||||
| 파일 | 역할 |
|
||||
|------|------|
|
||||
| `backend/src/forget/persona-forget.service.ts` | 알고리즘 엔진 (모든 수식) |
|
||||
| `backend/src/study-logs/study-logs.service.ts` | 문제 풀 때 → 첫 복습 스케줄 생성 |
|
||||
| `backend/src/reviews/reviews.service.ts` | 복습 제출 → 다음 복습 스케줄 생성 |
|
||||
| `backend/src/me/me.service.ts` | 페르소나 변경 → 기존 스케줄 재계산 |
|
||||
Reference in New Issue
Block a user