"""
=============================================================================
第 6 讲 · 代码 1：策略的参数化与"得分函数" ∇ log π
=============================================================================

对应讲义：lectures/lecture06_policy_gradient.md 第 2、4 节

-----------------------------------------------------------------------------
【这个文件要解决什么问题？】
-----------------------------------------------------------------------------
策略梯度方法直接学习一个带参数 θ 的策略 π_θ(a|s)。在写任何学习算法之前，要先搞清楚两件事：

    1. 策略长什么样？——离散动作用 softmax，连续动作用高斯分布。
    2. 得分函数（score function）∇_θ log π_θ(a|s) 怎么算？
       后面你会看到，**策略梯度的全部计算都围绕这个量展开**（讲义 4.2 节）。

本文件：
    - 实现 softmax 策略和高斯策略
    - 手工推导它们的得分函数，并用"有限差分"数值验证推导是否正确
    - 验证得分函数的一个关键性质：E_{a~π}[∇ log π(a|s)] = 0（它是"基线"不引入偏差的原因）

-----------------------------------------------------------------------------
【运行方式】
-----------------------------------------------------------------------------
    python 01_policy_parameterization.py
"""

import numpy as np

np.set_printoptions(precision=4, suppress=True)


# =============================================================================
# 第 1 部分：softmax 策略（离散动作）
# =============================================================================
def softmax(h):
    """
    把一组"偏好值" h（任意实数）变成概率分布：π_i = exp(h_i) / Σ_j exp(h_j)。

    【数值稳定性——一个真实的坑】
        如果 h 里有很大的数（比如 1000），exp(1000) 会溢出成无穷大，结果变成 nan。
        技巧：先把所有 h 减去它们的最大值。因为 exp(h_i - c) / Σ exp(h_j - c) 与原式相等
        （分子分母同时除以 exp(c)），结果不变，但最大的指数变成了 exp(0) = 1，不会溢出。

    【参数】h : 一维数组。
    【返回】同长度的概率数组，非负且和为 1。
    """
    z = h - np.max(h)
    e = np.exp(z)
    return e / e.sum()


def softmax_policy_probs(theta, phi_s):
    """
    线性偏好的 softmax 策略：
        偏好 h(s, a) = θ · φ(s, a)
        π_θ(a|s) = exp(h(s, a)) / Σ_b exp(h(s, b))

    【参数】
        theta : 长度 d 的参数向量。
        phi_s : 形状 (A, d) 的矩阵，第 a 行是 φ(s, a)（状态 s 下动作 a 的特征）。

    【返回】长度 A 的概率向量。

    【直觉】θ·φ(s,a) 越大，动作 a 越可能被选中。θ 整体放大（比如乘 10），
        偏好之间的差距被放大，策略变得更"确定"；θ 趋于 0，策略趋于均匀随机。
    """
    return softmax(phi_s @ theta)


def softmax_score(theta, phi_s, a):
    """
    softmax 策略的得分函数：∇_θ log π_θ(a|s) = φ(s, a) - Σ_b π_θ(b|s) φ(s, b)

    【推导】（讲义 2.2 节）
        log π(a|s) = θ·φ(s,a) - log Σ_b exp(θ·φ(s,b))
        对 θ 求导：
            第一项的导数：φ(s, a)
            第二项的导数：Σ_b exp(θ·φ(s,b)) φ(s,b) / Σ_b exp(θ·φ(s,b)) = Σ_b π(b|s) φ(s,b)
        所以 ∇ log π(a|s) = φ(s,a) - E_{b~π}[φ(s,b)]

    【直觉】"这个动作的特征"减去"平均而言的特征"。
        沿着这个方向调整 θ，会让动作 a 的概率上升（因为 a 的特征相对于平均更"突出"了）。
    """
    probs = softmax_policy_probs(theta, phi_s)
    return phi_s[a] - probs @ phi_s


# =============================================================================
# 第 2 部分：高斯策略（连续动作）
# =============================================================================
def gaussian_log_prob(a, mu, log_std):
    """
    一维高斯分布的对数概率密度：log N(a; μ, σ²) = -(a-μ)²/(2σ²) - log σ - ½ log(2π)。

    【为什么用 log_std 而不是 std 作参数？】
        σ 必须为正。直接学 σ，梯度下降可能把它推成负数；学 log σ 则可以取任意实数，
        σ = exp(log σ) 自动为正。这是一个常用的"重新参数化"技巧。
    """
    std = np.exp(log_std)
    return -0.5 * ((a - mu) / std) ** 2 - log_std - 0.5 * np.log(2 * np.pi)


def gaussian_score(theta, log_std, phi_s, a):
    """
    高斯策略 a ~ N(μ_θ(s), σ²)，μ_θ(s) = θ · φ(s) 的得分函数：

        ∇_θ log π(a|s)     = (a - μ) / σ² · φ(s)
        ∂/∂(log σ) log π   = (a - μ)² / σ² - 1

    【直觉】（第一行）如果采样到的动作 a 比均值 μ 大，得分方向就是"增大 μ"；
        比均值小，就是"减小 μ"。偏离越多（相对于 σ），得分越大。
        （第二行）如果 |a - μ| 比 σ 大，就倾向于增大 σ（"这个分布太窄了"）；反之减小 σ。

    【返回】(对 θ 的得分, 对 log σ 的得分)
    """
    mu = theta @ phi_s
    var = np.exp(2 * log_std)
    return (a - mu) / var * phi_s, (a - mu) ** 2 / var - 1.0


# =============================================================================
# 第 3 部分：用有限差分检验梯度
# =============================================================================
def numerical_gradient(f, theta, eps=1e-6):
    """
    用中心差分数值计算 f 在 theta 处的梯度：∂f/∂θ_i ≈ [f(θ + ε e_i) - f(θ - ε e_i)] / (2ε)。

    【为什么要做"梯度检验"？】
        手推的梯度公式很容易出错（少个负号、漏一项），而梯度错了，学习算法照样能跑，
        只是学不好——非常难排查。用数值差分核对一次，是写任何梯度代码时的好习惯。

    【参数】
        f     : 输入参数向量、输出一个数的函数。
        theta : 在哪里求梯度。
        eps   : 差分步长。太大近似不准，太小会被浮点舍入误差淹没，1e-6 左右通常合适。
    """
    grad = np.zeros_like(theta)
    for i in range(len(theta)):
        e = np.zeros_like(theta)
        e[i] = eps
        grad[i] = (f(theta + e) - f(theta - e)) / (2 * eps)
    return grad


# =============================================================================
# 第 4 部分：演示
# =============================================================================
def main():
    rng = np.random.default_rng(0)

    # ---------------------------------------------------------------------
    print("=" * 78)
    print("演示 1：softmax 策略——θ 的大小决定了策略有多\"确定\"")
    print("=" * 78)
    # 一个状态，3 个动作，特征维度 3（这里用独热特征：φ(s,a) = e_a，于是 θ_a 就是动作 a 的偏好）
    phi_s = np.eye(3)
    for scale in [0.0, 0.5, 1.0, 3.0, 10.0]:
        theta = scale * np.array([1.0, 2.0, 0.5])
        print(f"  θ = {scale:4.1f} × [1, 2, 0.5]  →  π = {softmax_policy_probs(theta, phi_s)}")
    print("  → θ = 0 时均匀随机；θ 越大越接近\"总选偏好最大的动作 1\"（接近确定性策略）。")
    print("    策略梯度方法可以**自己学会**该有多随机——这是基于价值的 ε-贪心做不到的。")

    # ---------------------------------------------------------------------
    print("\n" + "=" * 78)
    print("演示 2：softmax 得分函数——手推公式 vs 数值差分")
    print("=" * 78)
    phi_s = rng.normal(size=(3, 4))            # 一般的特征：3 个动作，每个 4 维
    theta = rng.normal(size=4)
    print(f"  π(·|s) = {softmax_policy_probs(theta, phi_s)}")
    for a in range(3):
        analytic = softmax_score(theta, phi_s, a)
        numeric = numerical_gradient(lambda th: np.log(softmax_policy_probs(th, phi_s)[a]), theta)
        print(f"  动作 {a}：公式 {analytic}")
        print(f"          数值 {numeric}   最大差 {np.abs(analytic - numeric).max():.1e}")
    print("  → 两者一致（差异在 1e-9 量级，来自浮点误差），手推公式正确。")

    # ---------------------------------------------------------------------
    print("\n" + "=" * 78)
    print("演示 3：得分函数的期望为 0：E_{a~π}[∇ log π(a|s)] = 0")
    print("=" * 78)
    probs = softmax_policy_probs(theta, phi_s)
    exact = sum(probs[a] * softmax_score(theta, phi_s, a) for a in range(3))
    print(f"  精确期望 Σ_a π(a|s) ∇log π(a|s) = {exact}")
    samples = rng.choice(3, size=100_000, p=probs)
    mc = np.mean([softmax_score(theta, phi_s, a) for a in samples], axis=0)
    print(f"  10 万个样本的平均                = {mc}")
    print("  【为什么？】Σ_a π(a|s) ∇log π(a|s) = Σ_a ∇π(a|s) = ∇ Σ_a π(a|s) = ∇ 1 = 0。")
    print("  → 这个看似平凡的性质非常重要：它说明给回报减去任何只依赖状态的\"基线\" b(s)，")
    print("    都不会改变策略梯度的期望（因为 E[b(s) ∇log π] = b(s) · 0 = 0）。讲义第 7 节。")

    # ---------------------------------------------------------------------
    print("\n" + "=" * 78)
    print("演示 4：高斯策略的得分函数（连续动作）")
    print("=" * 78)
    phi = np.array([1.0, 0.5, -0.3])
    theta_g = np.array([0.2, -0.4, 0.1])
    log_std = np.log(0.5)
    mu = theta_g @ phi
    print(f"  μ(s) = θ·φ(s) = {mu:.3f}，σ = {np.exp(log_std):.2f}")
    for a in [mu - 1.0, mu, mu + 0.3]:
        g_theta, g_logstd = gaussian_score(theta_g, log_std, phi, a)
        num_theta = numerical_gradient(lambda th: gaussian_log_prob(a, th @ phi, log_std), theta_g)
        num_logstd = numerical_gradient(lambda ls: gaussian_log_prob(a, mu, ls[0]), np.array([log_std]))[0]
        print(f"  动作 a = {a:+.3f}：∇_θ = {g_theta}（数值 {num_theta}），∂/∂logσ = {g_logstd:+.3f}（数值 {num_logstd:+.3f}）")
    print("  → a < μ 时，得分让 μ 减小；a = μ 时 ∇_θ = 0，且倾向于缩小 σ（-1）；")
    print("    a 离 μ 超过 σ 时（如 a = μ - 1，偏离 2σ），倾向于增大 σ。")

    # ---------------------------------------------------------------------
    print("\n" + "=" * 78)
    print("演示 5：边界情况——softmax 的数值溢出")
    print("=" * 78)
    h = np.array([1000.0, 1001.0, 999.0])
    with np.errstate(over="ignore", invalid="ignore"):
        naive = np.exp(h) / np.exp(h).sum()
    print(f"  偏好 h = {h}")
    print(f"  直接计算 exp(h)/Σexp(h) = {naive}   ← 溢出成 nan")
    print(f"  减去最大值后再计算     = {softmax(h)}   ← 正确")


if __name__ == "__main__":
    main()
