본문 바로가기
C.W.K.
Stream
Lesson 02 of 05 · published

반감기 달린 선호도, 그리고 딱 떨어지는 숫자를 비껴간 문턱

~13 min · ranking, decay, tuning, bugs

Level 0미분류
0 XP0/36 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete

관심은 누적이 아니라 현재야

순진한 프로필은 지금까지 한 행위를 전부 더해. 구체적이고 영구적인 방식으로 틀렸지. 한 주제를 몰아 읽은 두 주가 고정물이 되고, 그 뒤 모든 신호가 그걸 이기려고 소리를 질러야 해. 프로필이 누가 뭘 신경 쓰는지 추적하길 그만두고 한때 뭘 제일 신경 썼는지를 기록하기 시작하는 거야.

지수 감쇠가 곱셈 하나로 이걸 고쳐. 각 기여에 이분의 일의 (반감기 단위 나이) 제곱을 곱해. 두 주 된 저장은 절반, 한 달 된 저장은 사분의 일. 아무것도 안 지워지고 넘는 문턱도 없어. 옛 관심이 연속적으로 바래고, 그건 관심이 옮겨가면 선반이 알아서 회복된다는 뜻이야. 초기화 버튼도, 왜 네 이력을 버렸는지 독자한테 설명할 일도 없이.

서로 다른 일을 하는 반감기 둘

프로필 반감기는 취향이 얼마나 빨리 바뀌어도 되는지를 다스려. 느려야 해. 두어 주쯤. 취향은 진짜로 안정적이고, 팔랑거리는 프로필은 잡음을 쫓아다니거든.

신선도는 훨씬 짧은 반감기를 가진 별개 항이고 다른 질문에 답해. "이 주제를 신경 쓰나"가 아니라 "이게 아직 뉴스인가". 둘을 숫자 하나로 접는 게 흔한 지름길이고, 그러면 이틀 만에 네 관심을 잊거나 지난주 헤드라인을 보여주는 선반이 나와. 독립된 축이고 독립된 상수를 받을 자격이 있어.

행위는 그 비용대로 가중치를 줘

기사를 여는 건 약한 증거야. 탭 한 번이면 되고 후회하는 경우도 많지. 저장은 강해. 미래 가치에 대한 의도적 주장이니까. 공유도 비슷하고. 뭔가를 발행하는 건 공개적 약속이거든. 명시적 싫어함은 구할 수 있는 제일 강한 신호고 거기 맞는 크기를 받을 자격이 있어. 굳이 그걸 누르는 독자는 어떤 추론된 행동보다 정확한 걸 말해주는 거니까.

정확한 숫자보다 순서가 중요해. 하면 안 되는 건 신호를 모으기 쉬운 정도로 가중치를 주는 거야. 그게 시스템이 탭을 최적화하면서 그걸 인게이지먼트라고 부르게 되는 경로고.

딱 떨어지는 숫자가 잡아먹은 오후

크기보다 값어치 있는 버그 하나. 음소거는 가중치가 문턱 아래로 떨어지면 발동하는데, 문턱이 싫어함 가중치랑 정확히 같은 딱 떨어지는 숫자에 주차돼 있었어. 한 번도 발동 안 했지.

이유는 감쇠야. 기여가 명목값 그대로인 건 기록되는 그 순간뿐이고, 백만분의 일 초 뒤엔 곱셈이 이미 깎았거든. 그래서 문턱의 정확히 음수만큼 가중된 싫어함 하나는 의도가 아무리 뻔해도 영원히 바보다 미세하게 위인 가중치를 만들어. 문턱을 자연스러운 계단 위가 아니라 사이에 놓으니까 고쳐졌어. 일반형은 이래. 연속 감쇠가 있는 시스템에선 시스템이 만들어낼 수 있는 값 위에 문턱을 절대 놓지 마. 감쇠된 양과의 비교는 네가 적어둔 것보다 엄격히 작은 것과의 비교니까.

감쇠는 튜닝 손잡이가 아니라 네 데이터가 무슨 뜻인지에 대한 진술이야. 안 감쇠하는 가중치는 관심이 영구하다고 주장하고, 너무 빨리 감쇠하는 건 순간적이라고 주장해. 반감기는 그게 재는 게 실제로 얼마나 오래 참인지 물어서 골라.

Code

질문 둘에 반감기 둘, 그리고 딱 떨어지는 숫자를 비껴 놓은 문턱·python
PROFILE_HALF_LIFE_DAYS = 14.0     # how fast taste may change
FRESHNESS_HALF_LIFE_DAYS = 2.0    # how fast news stops being news


def _decay(stamp: str, now, half_life_days: float) -> float:
    age_days = (now - parse(stamp)).total_seconds() / 86400.0
    return 0.5 ** (age_days / half_life_days)


def build_profile(con) -> dict[str, float]:
    """Sum decayed contributions over the CURRENT state projection --
    the reversal-applied view of the log, so an unsaved article stops
    contributing without anything being deleted."""
    now, profile = _now(), {}
    for row in con.execute(_PROFILE_SELECT):
        article = dict(row)
        feats = features(article)
        for state_col, weight in EVENT_WEIGHTS.items():
            stamp = article.get(state_col)
            if not stamp:
                continue
            contribution = weight * _decay(stamp, now, PROFILE_HALF_LIFE_DAYS)
            for key, scale in feats.items():
                profile[key] = profile.get(key, 0.0) + contribution * scale
    return profile


# The afternoon a round number cost.
#
# EVENT_WEIGHTS["disliked_at"] is -4.0, so a threshold of -4.0 looks
# like "one dislike is enough". It never fires: decay shaves every
# weight the instant it is recorded, so a single dislike is -3.9998...
# and rising. The bar must sit BETWEEN the steps the system can
# actually produce, never ON one.
MUTE_THRESHOLD = -3.5

External links

Exercise

네 코드에서 한쪽 상수가 반대쪽 값을 만들 때 쓰인 상수랑 같은 문턱 비교를 찾아봐. 감쇠든 반올림이든 스케일링이든 부동소수점 합이든, 그 사이에 있는 뭔가가 만들어진 값을 모자라게 만들 수 있는지 따져봐. 마지막으로 문턱을 옮길지, 비교를 이상/이하로 바꿀지 정해.
Hint
부동소수점 누적은 감쇠 없이도 같은 부류의 빗나감을 내. 1.1 세 개를 더해서 3.3 이랑 비교하는 건 안정적으로 참이 아니거든. 바가 '이거 세 개'를 뜻해야 한다면 정확한 값에서 비껴 놓든가 허용 오차로 비교하든가 해. 근데 뭘 골랐는지 주석에 적어. 다음 독자는 그 딱 떨어지는 숫자가 의도적이었다고 넘겨짚을 테니까.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.