코드는 GitHub에, 논문은 arXiv에서 볼 수 있다. 더 많은 글은 radarlog.kr에서.
1편에서 MemoryBank의 아키텍처와 활용법을 다뤘다. 이번 편은 그 심장부를 열어본다. 수식 하나. R = e^(−t/S). 이 수식이 어떻게 작동하는지, S값이 변하면 곡선이 어떻게 바뀌는지, 임계값은 어디에 잡아야 하는지를 숫자로 파고든다.
수식을 분해한다
R = e^(−t/S)
변수가 세 개다. R은 기억 보유율(0~1 사이), t는 학습 이후 경과 시간, S는 기억 강도. e는 자연상수 2.71828이다.
이 수식은 지수 감쇠(exponential decay) 모델이다. 물리학에서 방사성 붕괴를 표현하는 공식과 구조가 같다. 시간이 지날수록 값이 기하급수적으로 줄어든다. 처음에 급격히 떨어지고, 나중에는 천천히 떨어진다.
게임 개발자라면 익숙한 패턴이다. 파티클의 알파값 감쇠, 사운드의 페이드아웃, 데미지 오버 타임(DoT)의 틱 감소. 전부 지수 감쇠를 기반으로 한다. 망각 곡선도 같은 수학이다. 다만 대상이 데미지가 아니라 기억일 뿐이다.
핵심을 짚자. 이 수식에서 진짜 중요한 건 t/S 비율이다. t와 S가 각각 얼마인지보다, 둘의 비율이 R을 결정한다. t/S가 1이면 R은 약 0.368(36.8%). t/S가 2이면 R은 약 0.135(13.5%). t/S가 3이면 R은 약 0.050(5.0%). 비율이 1 올라갈 때마다 보유율이 대략 1/e(약 36.8%)로 줄어든다.
import math
# t/S 비율에 따른 보유율
for ratio in [0, 0.5, 1, 2, 3, 5]:
R = math.exp(-ratio)
print(f"t/S = {ratio:.1f} → R = {R:.4f} ({R*100:.1f}%)")
# t/S = 0.0 → R = 1.0000 (100.0%)
# t/S = 0.5 → R = 0.6065 (60.7%)
# t/S = 1.0 → R = 0.3679 (36.8%)
# t/S = 2.0 → R = 0.1353 (13.5%)
# t/S = 3.0 → R = 0.0498 (5.0%)
# t/S = 5.0 → R = 0.0067 (0.7%)이게 의미하는 바는 명확하다. S가 클수록 같은 시간이 지나도 t/S 비율이 작으니까 R이 높게 유지된다. S는 곡선의 기울기를 완만하게 만드는 역할이다.
S가 올라가면 곡선이 어떻게 변하나
MemoryBank에서 S는 정수다. 처음 언급되면 1, 한 번 회상되면 2, 또 회상되면 3. 단순하다. 이 단순한 변화가 곡선에 어떤 영향을 주는지 구체적으로 본다.
경과 시간 t를 "일(day)" 단위로 잡겠다. S=1인 기억이 하루가 지나면 R은 얼마인가.
# S값별, 경과일별 보유율
for S in [1, 2, 3, 5]:
print(f"\n--- S = {S} ---")
for t_days in [0, 0.5, 1, 2, 3, 5, 7]:
R = math.exp(-t_days / S)
print(f" t={t_days}일 → R = {R:.4f} ({R*100:.1f}%)")결과를 정리하면 이런 그림이 나온다.
S=1일 때: 0일 100% → 1일 36.8% → 2일 13.5% → 3일 5.0% → 7일 0.1%
S=2일 때: 0일 100% → 1일 60.7% → 2일 36.8% → 3일 22.3% → 7일 3.0%
S=3일 때: 0일 100% → 1일 71.7% → 2일 51.3% → 3일 36.8% → 7일 9.7%
S=5일 때: 0일 100% → 1일 81.9% → 2일 67.0% → 3일 54.9% → 7일 24.7%
S=1인 기억은 하루 만에 36.8%로 추락한다. 일주일이면 0.1%. 사실상 소멸이다.
S=2이면 같은 하루가 지나도 60.7%가 남는다. S=5이면 하루 후에도 81.9%. 일주일이 지나도 24.7%가 살아있다.
한 번 회상할 때마다 S가 1씩 오르니까, 3번 회상된 기억(S=4)은 일주일이 지나도 17.4%가 남는다. 5번 회상된 기억(S=6)은 일주일 후에도 31.1%. 곡선이 확연히 눕는다.
이걸 게임으로 비유하면 "경험치 누적에 따른 버프 지속시간 증가"와 비슷하다. 같은 버프를 여러 번 걸면 지속시간이 점점 길어지는 메커니즘. 스택이 쌓일수록 효과가 오래간다. MemoryBank의 S도 정확히 그렇다.
t 리셋의 수학적 의미
S가 올라가는 것보다 t가 0으로 리셋되는 것이 사실 더 극적인 효과를 만든다.
시나리오를 하나 그려보자. 어떤 기억이 S=1 상태로 3일이 지났다. R은 e^(−3/1) = 0.050, 즉 5.0%다. 거의 사라질 뻔한 기억이다.
이 시점에 이 기억이 대화 중 회상됐다. MemoryBank는 S를 2로 올리고, t를 0으로 리셋한다. 이 순간 R은 e^(−0/2) = 1.000, 즉 100%로 돌아간다.
회상 전: S=1, t=3일 → R = 5.0% (거의 소멸)
회상 후: S=2, t=0일 → R = 100% (완전 부활)
5%에서 100%로. 이 점프가 핵심이다.
여기서 한 가지 더. 부활한 기억은 이전보다 더 강하다. S가 1에서 2로 올랐으니까, 다음에 같은 3일이 지나면 R이 5.0%가 아니라 22.3%가 된다. 첫 번째 생애에서는 3일이면 거의 죽었는데, 두 번째 생애에서는 3일이 지나도 아직 살아있다.
이걸 반복하면 이런 패턴이 된다.
# 회상 시나리오 시뮬레이션
# 기억이 3일마다 회상되는 경우
memory = {'S': 1, 't': 0}
for cycle in range(5):
# 3일 경과
memory['t'] = 3
R_before = math.exp(-memory['t'] / memory['S'])
# 회상 발생
memory['S'] += 1
memory['t'] = 0
R_after = math.exp(-memory['t'] / memory['S'])
print(f"사이클 {cycle+1}: 회상 전 R={R_before:.4f} ({R_before*100:.1f}%)"
f" → 회상 후 S={memory['S']}, R=100%")
# 사이클 1: 회상 전 R=0.0498 (5.0%) → 회상 후 S=2, R=100%
# 사이클 2: 회상 전 R=0.2231 (22.3%) → 회상 후 S=3, R=100%
# 사이클 3: 회상 전 R=0.3679 (36.8%) → 회상 후 S=4, R=100%
# 사이클 4: 회상 전 R=0.4724 (47.2%) → 회상 후 S=5, R=100%
# 사이클 5: 회상 전 R=0.5488 (54.9%) → 회상 후 S=6, R=100%3일마다 회상되는 기억의 "회상 직전 보유율"이 5.0% → 22.3% → 36.8% → 47.2% → 54.9%로 올라간다. 같은 3일이 경과해도, 회상 횟수가 쌓일수록 기억이 점점 더 잘 유지된다.
에빙하우스가 발견한 **간격 효과(spacing effect)**가 수식 안에 자연스럽게 내장된 거다. 반복 복습하면 망각 곡선이 점점 완만해진다는 그 원리.
임계값은 어디에 잡아야 하나
MemoryBank 논문에서 구체적인 임계값 숫자를 명시하지는 않았다. 하지만 실제로 구현하려면 "R이 얼마 아래로 떨어지면 기억을 삭제할 것인가"를 결정해야 한다.
임계값을 정하려면 먼저 질문을 바꿔야 한다. "기억이 며칠 안 꺼내지면 잊혀야 하는가?"
S=1인 기억(한 번도 회상 안 된 기억)이 기준이다. 이 기억이 잊혀지기까지 걸리는 시간은 임계값에 따라 달라진다.
# S=1일 때, 임계값별 "잊혀지는 시간"
for threshold in [0.5, 0.3, 0.1, 0.05, 0.01]:
# R = e^(-t/S) → t = -S * ln(R)
t_forget = -1 * math.log(threshold)
print(f"임계값 {threshold} → S=1 기억이 {t_forget:.2f}일 후 소멸")
# 임계값 0.5 → S=1 기억이 0.69일(약 17시간) 후 소멸
# 임계값 0.3 → S=1 기억이 1.20일(약 29시간) 후 소멸
# 임계값 0.1 → S=1 기억이 2.30일 후 소멸
# 임계값 0.05 → S=1 기억이 3.00일 후 소멸
# 임계값 0.01 → S=1 기억이 4.61일 후 소멸임계값 0.5이면 17시간 만에 기억이 사라진다. 너무 공격적이다. 어제 한 대화가 오늘 이미 없다.
임계값 0.01이면 4.6일까지 버틴다. 좀 느슨하다. 의미 없는 대화가 거의 5일이나 남아 있으면 메모리가 비효율적이다.
임계값 0.05~0.1 사이가 실용적인 범위다. S=1 기억이 2~3일 안에 자연스럽게 사라지고, S=3 이상인 기억(2번 이상 회상)은 일주일 넘게 살아남는다.
이걸 역으로 쓸 수도 있다. "우리 서비스에서 중요한 기억은 최소 7일은 유지돼야 한다"는 요구사항이 있다면, S와 임계값을 역산할 수 있다.
# "7일 후에도 살아남으려면 S가 최소 얼마여야 하나?"
target_days = 7
threshold = 0.1
# R = e^(-t/S) ≥ threshold
# -t/S ≥ ln(threshold)
# S ≥ -t / ln(threshold)
S_min = -target_days / math.log(threshold)
print(f"7일 후 R ≥ 0.1 이려면 S ≥ {S_min:.2f}")
# 7일 후 R ≥ 0.1 이려면 S ≥ 3.04S가 최소 4(3번 회상)는 돼야 7일 후에도 임계값 0.1 위에 머문다. 서비스 특성에 맞춰서 이런 식으로 파라미터를 튜닝할 수 있다.
반감기로 바라보기
지수 감쇠에서 가장 직관적인 지표는 반감기다. 보유율이 50%로 떨어지는 데 걸리는 시간.
# R = e^(-t/S) = 0.5
# -t/S = ln(0.5)
# t_half = S * ln(2) ≈ S * 0.693
for S in [1, 2, 3, 5, 10]:
t_half = S * math.log(2)
print(f"S={S:2d} → 반감기 = {t_half:.2f}일")
# S= 1 → 반감기 = 0.69일 (약 17시간)
# S= 2 → 반감기 = 1.39일 (약 33시간)
# S= 3 → 반감기 = 2.08일 (약 50시간)
# S= 5 → 반감기 = 3.47일
# S=10 → 반감기 = 6.93일 (약 1주일)반감기가 S에 정비례한다. S가 2배가 되면 반감기도 2배. 선형 관계다.
한 번도 회상 안 된 기억(S=1)은 반감기가 17시간이다. 하루도 안 돼서 절반이 날아간다. 2번 회상된 기억(S=3)은 반감기가 2일. 9번 회상된 기억(S=10)은 반감기가 거의 일주일이다.
이 반감기 숫자를 보면 MemoryBank의 설계 의도가 선명해진다. 최근에 자주 나온 화제는 오래 기억하고, 한 번 스쳐 지나간 얘기는 빠르게 잊는다. 사람의 기억과 같은 패턴이다.
게임에서 유사한 시스템을 찾자면, 어그로(aggro) 감쇠가 있다. 플레이어가 보스한테 데미지를 안 넣으면 어그로가 시간에 따라 감쇠한다. 꾸준히 데미지를 넣으면 어그로가 유지되고 강화된다. 데미지 주입이 "회상"이고, 어그로 수치가 "기억 보유율"인 셈이다.
시뮬레이션: 10일간 5개 기억의 운명
실제 시나리오를 돌려본다. 5개의 기억이 서로 다른 패턴으로 회상되는 10일간의 시뮬레이션이다.
import math
THRESHOLD = 0.1
memories = {
"이직 고민": {"S": 1, "t": 0, "born": 0, "recalls": []},
"점심 메뉴": {"S": 1, "t": 0, "born": 0, "recalls": []},
"UE5 버그": {"S": 1, "t": 0, "born": 1, "recalls": []},
"주말 캠핑": {"S": 1, "t": 0, "born": 2, "recalls": []},
"연봉 협상": {"S": 1, "t": 0, "born": 3, "recalls": []},
}
# 회상 스케줄 (일 단위)
recall_schedule = {
"이직 고민": [1, 3, 5, 8], # 자주 회상
"점심 메뉴": [], # 한 번도 안 꺼냄
"UE5 버그": [2, 4], # 가끔 회상
"주말 캠핑": [3], # 한 번만 회상
"연봉 협상": [4, 6, 7, 8, 9], # 매우 자주 회상
}
print("=== 10일 시뮬레이션 ===\n")
for day in range(11):
print(f"--- Day {day} ---")
for name, mem in memories.items():
if day < mem['born']:
continue
# 경과 시간 계산
last_event = mem['recalls'][-1] if mem['recalls'] else mem['born']
mem['t'] = day - last_event
# 이 날 회상되는가?
if day in recall_schedule[name]:
mem['S'] += 1
mem['recalls'].append(day)
mem['t'] = 0
R = math.exp(-mem['t'] / mem['S']) if mem['S'] > 0 else 0
status = "ALIVE" if R >= THRESHOLD else "DEAD"
print(f" {name:8s}: S={mem['S']}, t={mem['t']}일, "
f"R={R:.3f} ({R*100:.1f}%) [{status}]")
print()결과에서 핵심만 뽑으면 이렇다.
"점심 메뉴"는 한 번도 회상되지 않았다. S=1, Day 2에서 이미 R이 13.5%로 떨어지고, Day 3이면 5.0%. 임계값 0.1 기준으로 Day 3에 소멸한다.
"이직 고민"은 Day 1, 3, 5, 8에 회상됐다. Day 10이 되면 S=5, 마지막 회상으로부터 2일 경과. R은 e^(−2/5) = 67.0%. 아직 건강하다.
"연봉 협상"은 Day 4부터 거의 매일 회상됐다. Day 10이면 S=6, 마지막 회상으로부터 1일 경과. R은 e^(−1/6) = 84.6%. 가장 강한 기억이다.
"주말 캠핑"은 Day 3에 딱 한 번 회상됐다. S=2로 올랐지만, 그 후 7일 동안 아무도 안 꺼냈다. Day 10에서 R은 e^(−7/2) = 3.0%. 소멸이다.
같은 10일이라도 회상 패턴에 따라 운명이 완전히 갈린다. 이게 MemoryBank의 핵심 메커니즘이 만들어내는 효과다.
MemoryBank 모델의 수학적 한계
이 수식이 깔끔한 만큼, 현실과의 괴리도 명확하다.
S의 선형 증가 문제. 회상할 때마다 S가 단순히 1씩 오른다. 하지만 실제 인간의 기억 강화는 비선형이다. 첫 번째 복습의 효과가 가장 크고, 이후 복습의 효과는 점점 줄어든다(수확 체감). S_new = S_old + 1보다 S_new = S_old + 1/log(S_old + 1) 같은 감쇠 증가가 더 현실적이다.
감정 가중치의 부재. 에빙하우스 원래 실험은 무의미한 음절(WID, ZOF 같은)을 대상으로 했다. 의미 있는 정보는 무의미한 정보보다 10배 느리게 잊힌다고 에빙하우스 자신도 인정했다. MemoryBank는 "이직 고민"과 "점심 메뉴"의 초기 S를 똑같이 1로 놓는다. 감정적 중요도가 반영되지 않는다. 의미 가중치를 S 초기값에 반영하는 확장이 가능하다. LLM으로 대화의 감정 강도를 판별해서 S_init을 1~3으로 차등 부여하는 식.
시간 단위의 모호성. 논문에서 t의 단위를 명시하지 않는다. 일? 시간? 분? 대화 세션 수? 단위에 따라 곡선의 형태가 완전히 달라진다. 실서비스에 적용할 때 가장 먼저 결정해야 할 파라미터다.
회상 판정 기준. "이 기억이 대화 중 회상됐다"의 판정이 FAISS 검색 결과 기반이다. top-k에 포함되면 회상으로 치는 건지, 실제로 응답에 반영돼야 회상인지에 따라 S 증가 빈도가 달라진다. 검색됐지만 응답에 안 쓰인 기억도 S가 올라간다면, 기억 강도가 과대평가될 수 있다.
이 한계들은 MemoryBank를 확장할 수 있는 방향이기도 하다. 논문 저자들이 "탐색적이고 단순화된 모델"이라고 명시한 건, 이 수식이 최종 답이 아니라 출발점이라는 의미다. 기본 공식 위에 감정 가중치, 비선형 S 증가, 맥락 기반 회상 판정 같은 레이어를 얹으면 훨씬 정교한 메모리 시스템을 만들 수 있다.
R = e^(−t/S). 수식 하나가 기억의 탄생, 강화, 소멸을 전부 설명한다. 복잡한 메모리 아키텍처가 아니라, 심리학에서 140년간 검증된 원리를 LLM에 올린 거다. 단순하지만 효과적이다. 그리고 단순하기 때문에 확장 가능하다.
"수식은 단순할수록 강하다. 복잡한 건 구현하기 쉽지만, 단순한 건 확장하기 쉽다."