打鍵ダイナミクスに基づく集中力リアルタイム推定アルゴリズムの数学的仕様と処理パイプライン(第二部)
打鍵ダイナミクスから集中強度を算出する数理モデルの詳細仕様と、処理パイプラインの各計算ステップについて解説します。
注意: 本記事は、Gamita Novelist 開発チームによるホワイトペーパー「打鍵ダイナミクスに基づく集中力リアルタイム推定アルゴリズム」の内容を分割して紹介する第二部です。 アルゴリズムの概要および論文PDFのダウンロードにつきましては、以下の紹介記事をご覧ください: 打鍵パターンから集中度をリアルタイム推定するアルゴリズム:ホワイトペーパー公開
本稿および記載されているアルゴリズムは、クリエイティブ・コモンズ 表示 - 非営利 4.0 国際 (CC BY-NC 4.0) ライセンスの下で提供されています。商用利用を希望される場合は、お問い合わせよりご連絡ください。
入力と前提
各ティックの入力:
t:現在のタイムスタンプ(秒、単調増加)K_t:前回ティック以降の打鍵数(非負整数)
セッション継続関数:
S(t_prev, t) = true (if t > t_prev 且つ Δt_raw ≦ Δt_max)
false (otherwise / 起動直後・スリープ復帰等)
状態変数と初期条件
| 変数 | 意味 | 型 | 初期値 | 永続 |
|---|---|---|---|---|
t_prev | 前回タイムスタンプ | float | 未定義 | No |
λ | レート移動平均 | float | 0 | No |
λ^slow | 閾値学習用低速平均 | float | 0 | Yes |
X̄ | 密度移動平均 | float | 0 | Yes |
μ | 対数密度の平均 | float | 0 | Yes |
v | 対数密度の分散 | float | 1.0 | Yes |
t_total | 加重有効活動時間 | float | 0 | Yes |
t_last_session | 前回セッション終了時刻 | float | t_0 | Yes |
T_burst | バースト持続時間の学習値 | float | 30 | Yes |
n_burst | バーストイベント観測回数 | uint | 0 | Yes |
T_pause | 休止持続時間の学習値 | float | 20 | Yes |
n_pause | 休止イベント観測回数 | uint | 0 | Yes |
Δ^idle | 累積沈黙時間 | float | 0 | No |
Δt_since_resume | 再開からの経過時間 | float | ∞ | No |
t_since_depart | 出発からの経過時間 | float | ∞ | No |
A_prev | 前回活動度 | float | 0 | No |
S | 平滑化スコア | float | 50 | No |
F | 最終表示スコア | float | 50 | No |
v_0 = 1.0の理由:初期値を 0 にするとσ ≈ √εとなり、最初の数ティックで Z 値が異常に大きくなる。v_0 = 1.0は「対数密度は標準偏差 ± 1 程度のばらつき」という穏やかな事前仮定であり、信頼度γ_tと組み合わせて初期安定化を担う。T_burst,0 = 30s,T_pause,0 = 20s の理由:学習前の安全なデフォルト。これらは数回の活動サイクルで実測値に収束する。実測が 1 回蓄積されれば既に部分的に自分に合った値となる。
起動時フック(パイプライン外)
メインループ開始前に一度だけ実行する。
t_total <- t_total * exp(-(t_now - t_last_session) / τ_decay)
前回セッションからの経過時間に応じて t_total を減衰させる。長期離脱後は統計感度が部分的に復活し、再開後の再較正が速くなる。
自動算出される時定数の設計
本節では、従来の固定値パラメータを自動算出に置き換える核心部分を説明する。
動機:固定値の問題
τ_rise や τ_fall を固定した場合、以下の問題が生じた。
- 速いタイポリズム(10秒バースト)の人には過敏または過鈍な反応
- 遅いタイポリズム(3分バースト)の人には不適切な反応速度
- チューニングに開発者の主観が混入する
解決策:バースト・休止時間の観測学習(ハイブリッド Welford→EMA)
アルゴリズムは活動状態の遷移を観測し、各ユーザーの「呼吸のリズム」を学習する。
- バースト持続時間
T_burst:活動中(A_t ≧ 0.5)が続く時間の学習値。「出発イベント」(Aが0.5を下回る瞬間)に更新される。 - 休止持続時間
T_pause:非活動(A_t < 0.5)が続く時間の学習値。「再開イベント」(Aが0.5を上回る瞬間)に更新される。
更新方式:ハイブリッド Welford → EMA
最初の N_switch イベントは Welford の逐次平均(1/n 更新)を使用し、以降は EMA(α_event 更新)に切り替える。
α*(n) = 1 / n (n ≦ N_switch の場合)
α_event (n > N_switch の場合)
n <- n + 1
T <- T + α*(n) * (x_new - T)
(T ∈ {T_burst, T_pause}、x_new は今回の観測値、n ∈ {n_burst, n_pause})
設計根拠:
- Welford フェーズ(
n ≦ N_switch):初期値T_0(デフォルト値)の影響がn回で1/nに薄まり、素直に真の平均へ収束する。 - EMA フェーズ(
n > N_switch):学習率がα_eventで固定されるため、ユーザーのリズムが変化しても追従し続ける。純 Welford ではn → ∞で更新量がゼロになり、長期の変化に追従できなくなるためこの切り替え必要。
適応的時定数の算出式
上昇時定数 τ_rise,t
バーストの典型的な長さに比例して、速く検出する。
τ_rise,t = clip(T_burst / 20, τ_rise,min, τ_rise,max)
- 係数
1/20の意味:バースト開始からT_burst / 20秒でλはX_tに1 - e^(-1) ≈ 63%追従する。バーストの 5% 未満の時間で検出が始まる。 - 例:
T_burst = 30s ⇒τ_rise = 1.5s(3秒で 86% 追従)
下降時定数 τ_fall,t
通常の休止より少し長い間は、レートを高いまま保つ。
τ_fall,t = clip(T_pause * 0.6, τ_fall,min, τ_fall,max)
- 係数
0.6の意味:典型的な休止時間T_pauseの 60% が経過した時点でλはe^(-T_pause / τ_fall) = e^(-1 / 0.6) ≈ 0.19倍になる。閾値λ_th = 0.3 * λ^slowに対し、λ_0 = λ^slowから始まると0.19 < 0.3つまり通常の休止の 60% 後に非活動状態に入る。これにより「いつもより少し長い沈黙」でようやく非活動と判定される。 - 例:
T_pause = 20s ⇒τ_fall = 12s(12秒休止で 63% 低下)
シグモイド c_t
新しい状態変数なしで算出できる。σ_t(打鍵密度の対数標準偏差)はすでに統計量として計算されており、ユーザーの打鍵パターンのばらつきを直接反映している。
c_t = clip(σ_t / 2, c_min, c_max)
設計根拠: σ_t が大きい(ばらつきの大きいバースト型ユーザー)ほど c_t が大きくなり、活動度の遷移がよりなめらかになる。σ_t が小さい(安定持続型ユーザー)ほど c_t が小さくなり、活動・非活動の境界がより明確に引かれる。適応的な個人内較正が自動で行われる。
- 例:
σ_t = 1.0(中程度のばらつき) ⇒c_t = 0.5(本アルゴリズムのデフォルト値と一致)
固定値として残るパラメータとその理由
| パラメータ | 理由 |
|---|---|
r_th = 0.3 | 「活動の定義」は設計思想。データから一意に導けない。 |
D_max = 8 | 安全弁(上限クランプ)。動的にすると不安定化リスクがある。 |
λ_th,min = 1.0 | コールドスタート時の数値安全弁。 |
τ_slow, τ_density | 統計の時間スケール設定。設計的選択。 |
T^sil_floor, T^sil_ceil | スコアの語義定義。変えると表示意味が変わる。 |
処理パイプライン
- 時間制御(
Δt = 0なら即 return) - 打鍵密度(
X_t, Y_t) - 適応的レート推定(
τ_rise,tとτ_fall,tを自動算出してからλ_tを更新) - 活動閾値(
λ^slow_t, λ_th,t) - 適応的活動判定(
c_tを自動算出してからA_tを計算) - 遷移イベント処理(出発 / 再開イベントを判定し
T_burst,T_pause,Sを更新) - 個人内相対密度(
X̄_t, D_t) - 統計量更新(
t_total, μ_t, v_t, σ_t) - 信頼度(
γ_t) - 瞬間強度(
Z_t, T_t) - 沈黙時間・ターゲット(
Δ^idle_t, T^silence_t) - ダイナミクス(
S_t、グレース適用) - 最終出力(
F_t) - 状態保存
各ステップ詳細
Step 1: 時間制御
Δt = min(Δt_raw, Δt_max) (S = true の場合)
0 (otherwise)
Δt = 0 の場合、以降すべてスキップし return する。
Step 2: 打鍵密度
X_t = K_t / Δt Y_t = ln(X_t + 1)
Step 3: 適応的レート推定
まず自動算出された時定数を求める:
τ_rise,t = clip(T_burst / 20, τ_rise,min, τ_rise,max)
τ_fall,t = clip(T_pause * 0.6, τ_fall,min, τ_fall,max)
τ^rate_t = τ_rise,t (X_t ≧ λ_{t-1} の場合)
τ_fall,t (X_t < λ_{t-1} の場合)
κ^rate_t = 1 - exp(-Δt / τ^rate_t)
λ_t = λ_{t-1} + κ^rate_t * (X_t - λ_{t-1})
Step 4: 活動閾値
κ^slow_t = 1 - exp(-Δt / τ_slow)
λ^slow_t = λ^slow_{t-1} + κ^slow_t * (λ_t - λ^slow_{t-1})
λ_th,t = max(r_th * λ^slow_t, λ_th,min)
Step 5: 適応的活動判定
シグモイドを自動算出:
c_t = clip(σ_{t-1} / 2, c_min, c_max)
(σ_{t-1} は前ティックの統計量を使用。初回は σ_0 = √v_0 = 1.0)
A_t = 1 / (1 + exp(-(λ_t - λ_th,t) / (c_t * λ_th,t + ε)))
Step 6: 遷移イベント処理
出発イベント(活動 → 非活動)のトリガー:
A_prev ≧ 0.5 且つ A_t < 0.5
処理(ハイブリッド Welford → EMA):
T^obs_burst = Δt_since_resume
if T_min ≦ T^obs_burst ≦ T_max (有効域ガード):
n_burst <- n_burst + 1
α*_burst = 1 / n_burst (n_burst ≦ N_switch の場合)
α_event (otherwise)
T_burst <- T_burst + α*_burst * (T^obs_burst - T_burst)
t_since_depart <- 0
- 有効域ガードの意味と設計根拠:
T^obs_burst < T_min(デフォルト 5s)の除外:A_tが0.5付近で振動する「チャタリング」により生じる偽の短時間イベントを排除する。T^obs_burst > T_max(デフォルト 600s)の除外:セッション境界とリズムの混同を防ぐ。T_burstとT_pauseはユーザーの「打鍵のリズム(呼吸)」を学習するものであり、セッション全体の長さ(例:20分のまとまった執筆)を学習するものではない。600s(10分)を超えるバースト・休止は「ワーキングスタイル」であって「リズム」ではないため、ガードによりT_burst/T_pauseの更新対象から除外する。初期化直後のΔt_since_resume = ∞の除外も兼ねる。
再開イベント(非活動 → 活動)のトリガー:
A_prev < 0.5 且つ A_t ≧ 0.5
処理(ハイブリッド Welford → EMA):
T^obs_pause = t_since_depart
if T_min ≦ T^obs_pause ≦ T_max (有効域ガード):
n_pause <- n_pause + 1
α*_pause = 1 / n_pause (n_pause ≦ N_switch の場合)
α_event (otherwise)
T_pause <- T_pause + α*_pause * (T^obs_pause - T_pause)
w = 1 - exp(-Δ^idle / τ_session)
S <- w * 50 + (1 - w) * S
Δ^idle <- 0
Δt_since_resume <- 0
Step 6 が更新した S を、同フレーム of Step 12 が S_{t-1} として使用する。
Step 7: 個人内相対密度
κ^den_t = 1 - exp(-Δt / τ_density)
X̄_t = X̄_{t-1} + κ^den_t * A_t * (X_t - X̄_{t-1})
D_t = min((X_t + ε) / (X̄_{t-1} + ε), D_max)
Step 8: 統計量更新
t_total <- t_total * exp(-Δt / τ_decay) + A_t * Δt
τ_stat,t = τ_stat,0 + (τ_stat,∞ - τ_stat,0) * (1 - exp(-t_total / τ_stat,grow))
α_t = 1 - exp(-Δt / τ_stat,t)
μ_t = μ_{t-1} + α_t * A_t * (Y_t - μ_{t-1})
v_raw = v_{t-1} + α_t * A_t * ((Y_t - μ_{t-1})^2 - v_{t-1})
v_t = max(v_raw, v_min)
σ_t = √v_t
Step 9: 信頼度
γ_t = 1 - exp(-t_total / τ_confidence)
Step 10: 瞬間強度
Z_t = (Y_t - μ_{t-1}) / σ_t
T_t = 50 + γ_t * (10 * clip(Z_t, -3, 3) + 8 * tanh(ln(D_t)))
Step 11: 沈黙時間とターゲット
Δ^idle_t = Δ^idle_{t-1} + (1 - A_t)^2 * Δt
T^silence_t = T^sil_floor + (T^sil_ceil - T^sil_floor) * exp(-Δ^idle_t / τ_silence)
Step 12: ダイナミクス(グレース付き)
g_t = exp(-Δt_since_resume / τ_grace)
τ_active = τ_warmup + g_t * (τ_grace_dur - τ_warmup)
τ_t = A_t * τ_active + (1 - A_t) * τ_silence
Target_t = A_t * T_t + (1 - A_t) * T^silence_t
S_t = S_{t-1} + (1 - exp(-Δt / τ_t)) * (Target_t - S_{t-1})
Step 13: 最終出力
β_t = 1 - exp(-Δt / τ_perception)
F_t = F_{t-1} + β_t * (S_t - F_{t-1})
Step 14: 状態保存
A_prev <- A_t t_prev <- t t_last_session <- t Δt_since_resume <- Δt_since_resume + Δt t_since_depart <- t_since_depart + Δt
実装上の注意
- 浮動小数点精度:全演算を 64 ビット(f64 / double)で行うこと。
T_burst/T_pauseの外れ値対策:Δt_since_resumeやt_since_departがΔt_maxを超える区間(スリープ復帰等)を含む場合、その値はT_burst/T_pauseの更新に使用しないこと(外れ値による誤学習を防ぐ)。スリープ復帰はS = falseでΔt = 0になるため、Step 6 が実行されず保護される。