| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687 |
- # =====================================================================
- # 用「梯度下降」拟合一条直线 —— 神经网络最基础的原理演示
- # ---------------------------------------------------------------------
- # 场景:已知 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}')
|