# =====================================================================
# 用「梯度下降」拟合一条直线 —— 神经网络最基础的原理演示
# ---------------------------------------------------------------------
# 场景：已知 100 套房子的「面积」和「成交价」，想自动学出一个估价公式：
#           价格 = w * 面积 + b
#   其中 w 是「每平米单价」（直线的斜率），b 是「基价」（直线的截距）。
#
# 神经网络学习一件事，永远只有三步，循环往复：
#   ① 预测：用当前的 w、b 算一个预测值 h
#   ② 算误差：拿预测值 h 和真实值 y 比较，得到一个「损失 loss」
#   ③ 调参：沿「让 loss 变小的方向」微调 w 和 b
# 下面每一行代码，你都能对应到这三步中的某一步。
# =====================================================================

import random
import matplotlib.pyplot as plt


# ---------- ① 初始化参数 ----------
# 一开始我们「随便猜」w 和 b，random.random() 会得到 [0,1) 之间的随机数。
# 注意：真实值是 w=3、b=10，我们故意从很远的 0~1 开始猜，
# 这样才能看出「梯度下降」是如何一步步把错误答案纠正过来的。
w = random.random()  # 猜一个「每平米单价」
b = random.random()  # 猜一个「基价」


# ---------- ② 造一批「带噪声」的训练数据 ----------
_x = [i / 100 for i in range(100)]                          # 面积：0.00 ~ 0.99，共 100 个点
_y = [3 * e + 10 + (random.random() - 0.5) for e in _x]     # 真实价格 = 3*面积 + 10 + 噪声

# 上面 _y 为什么要加 (random.random() - 0.5)？
#   现实数据永远有误差（同面积房子价格不会一模一样）。这个式子 = 噪声，范围 [-0.5, 0.5]，
#   均值恰好是 0，所以「整体上」数据仍然服从 y = 3x + 10，最后学出来 w≈3、b≈10。
#   如果只加 random.random()（均值 0.5），那真实截距会变成 10.5，初学者容易困惑。


# ---------- ③ 开始训练 ----------
# 原来的代码是 `while True:` —— 这是个「死循环」，程序永远停不下来，所以跑不动。
# 我们改成「固定轮数」：一轮(epoch) = 把 100 个数据点全部看一遍。
# 300 轮足够让 w、b 收敛了。
EPOCHS = 300        # 训练轮数
LR = 0.01           # 学习率（learning rate）：每次调参数的「步子大小」

plt.ion()           # 打开 matplotlib 的「交互模式」：画图时程序不卡住，能继续往下跑
                    # （这个只需要开一次，所以放在循环外面）

for epoch in range(EPOCHS):
    # 内层 for：把 100 个样本逐个过一遍，每看一个样本就微调一次 w 和 b。
    # （这种做法叫「随机梯度下降 SGD」，是训练神经网络的常见方式）
    for x, y in zip(_x, _y):
        # —— 第 1 步：预测 ——
        h = w * x + b                      # 用当前的 w、b 算出「预测价格」

        # —— 第 2 步：算误差（损失函数）——
        loss = (y - h) ** 2                # 差的平方：预测偏离真实越远，loss 越大
                                           # 平方有两点好处：① 正负误差都算数；② 好求导

        # —— 第 3 步：求「梯度」，也就是 loss 对 w、b 的偏导数 ——
        # 为什么是这两个式子？用链式法则对 loss=(y-h)^2 求导：
        #   h = w*x + b，所以 dh/dw = x，dh/db = 1
        #   d(loss)/dw = 2*(y-h) * d(y-h)/dw = 2*(y-h) * (-x) = -2*x*(y-h)
        #   d(loss)/db = 2*(y-h) * d(y-h)/db = 2*(y-h) * (-1) = -2*(y-h)
        # 梯度告诉我们：w、b 往哪个方向挪，loss 会「上升」。所以我们要「反着」挪。
        dw = -2 * x * (y - h)              # loss 对 w 的偏导数
        db = -2 * (y - h)                  # loss 对 b 的偏导数

        # —— 第 4 步：沿「让 loss 变小」的方向更新参数 ——
        # w -= dw * LR 的意思是：w 朝 dw 的反方向走一小步，步长 = LR * |dw|。
        # 直观理解（只看 b）：
        #   预测偏低时 h<y，(y-h)>0，于是 db<0，b -= 负数 → b 变大 → 预测抬高 ✅
        #   预测偏高时 h>y，(y-h)<0，于是 db>0，b -= 正数 → b 变小 → 预测压低 ✅
        w -= dw * LR                       # 学习率控制每步走多大
        b -= db * LR

    # —— 每结束一轮，画一次图、打印一次进度 ——
    # plt.cla() 清空上一帧，避免线条越叠越多；plt.pause() 让画面刷新出来。
    plt.cla()
    plt.plot(_x, _y, 'r', label='真实数据')                       # 红点：真实房价
    plt.plot(_x, [w * e + b for e in _x], label='拟合直线')        # 蓝线：我们学到的直线
    plt.legend()
    plt.pause(0.01)
    print(f'epoch:{epoch:3d}  w:{w:.3f}  b:{b:.3f}  loss:{loss:.4f}')

# 训练结束后：关掉交互模式，让窗口保留，方便观察最终结果
plt.ioff()
plt.show()
print(f'\n训练完成！真实值 w=3, b=10；学到的 w≈{w:.3f}, b≈{b:.3f}')
