欢迎光临
我们一直在努力

基于Python的最小AI 实践

本文最小任务  Hello AI

我们用最简单的 BP 神经网络,基于  输入层(1) → 隐藏层(4) → 输出层(1)  ,以6个神经元实现学习  1是不是1,0是不是0;  

函数及参数

最简单的BP神经网络有如下参数:

符号(LaTeX)含义本案例数值
n_{in} 输入层神经元数量 1
n_{h} 隐藏层神经元数量 4
n_{out} 输出层神经元数量 1
x_{i} 输入层第 i 个神经元输入值 x = 0 或 1
h_{j} 隐藏层第 j 个神经元输出值   h 的输出是对这个输入的“激活”
o_{k} 输出层第 k 个神经元输出值
W_{hij} 输入层 i→ 隐藏层 j 的权重 在本程序中以w1表示
W_{ojk} 隐藏层 j→ 输出层 k 的权重 在本程序中以w2表示
b_{j} 隐藏层第 j 个神经元偏置 在本程序中以b1表示
b_{k} 输出层第 k 个神经元偏置 在本程序中以b2表示
f(⋅) 隐藏层激活函数 在本程序中采用ReLU函数
g(⋅) 输出层激活函数 在本程序中采用Sigmoid函数
d_{k} 输出层第 k 个神经元期望输出 正确答案,是个常数
E 单个样本损失函数 MSE  均方差 mean-square error
η 学习率 0.1
m 样本数 80

激活函数

激活函数是为神经网络引入非线性特性的关键组件,激活函数(如 ReLU、Sigmoid)通过非线性变换(如 h = ReLU(X·W1 + b1)),让网络能学习到输入和输出之间的非线性映射关系

网络类型线性拟合能力非线性拟合能力
无激活函数的纯线性网络 ✅ 可以 ❌ 不可以
带激活函数的非线性网络 ✅ 可以 ✅ 可以

ReLU

线性修正函数 Rectified Linear Unit, ReLU 又称整流函数,其函数本体及导数为:

ReLU(x)=max(0,x)=\\left\\{ \\begin{aligned} x,x>0\\\\ 0, x \\leqslant 0 \\\\ \\end{aligned} \\right.

ReLU(x)'=\\left\\{ \\begin{aligned} x & =1,x>0\\\\ x & =0, x \\leqslant 0 \\\\ \\end{aligned} \\right.

Sigmoid

Sigmoid函数常被用作神经网络的激活函数,将变量映射到0,1之间

\\sigma(x) = \\frac{1}{1 + e^{-x}}

\\sigma(x)'=\\sigma(x)(1-\\sigma(x))

链式法则:

若函数 y 是 u 的函数,而 u 又是 x 的函数(即 y=f(u), u=g(x)),则 y 对 x 的导数等于 y 对 u 的导数 乘以 u 对 x 的导数

FNN 前馈神经网络(feedforward neural network,FNN)

FNN是一种无环的神经网络结构,信号从输入层开始,依次经过隐藏层,最终传递到输出层,结构是 “单向流动” 的,数据只能向前传播,不能向后传递,没有任何反馈连接(即信号不会从输出层返回输入层)。目标是通过调整网络参数(权重、偏置),让模型对输入数据做出准确预测或分类。

BP(Backpropagation)神经网络是最基础的神经网络,其输出结果采用前向传播,误差采用反向传播方式进行。

FNN 是“被训练的对象”,BP 是“训练 FNN 的工具”,二者是“载体” 与 “方法”的关系,缺一不可。

前向传播(Forward Propagation)

核心逻辑:输入信号逐层加权求和 + 激活函数变换,最终输出预测值,计算损失。

1. 隐藏层计算

  • 隐藏层加权输入(输入与权重的点积 + 偏置, 在下方python代码中h用1表示):

    Z_1= x \\cdot W_1+B_1  ​     

    其中 ⋅ 表示矩阵乘法,偏置会自动广播到 m 个样本。

  • 隐藏层输出(经过激活函数引入非线性):

    H=f (Z_1)=ReLU(Z_1) 

2. 输出层计算

  • 输出层加权输入(隐藏层与权重的点积 + 偏置, 在下方python代码中o用2表示):

    Z_o=H \\cdot W_o+B_o 

  • 输出层预测值:

    o=g(\\sum Z_o)=Sigmoid(\\sum Z_o)=Sigmoid(z2_1+z2_2+z2_3+z2_4+b2)​ 

* 由于B1 在运算过程中对结果有影响,而B2 终归会加和在一起才产生影响,所以B2 可以退化成单个常数b2

输出对输出层输入求导:

\\frac{\\partial o}{\\partial z_2}=\\sigma(\\sum z_2)(1-\\sigma(\\sum z_2))

隐藏层输出对隐藏层输入求导

\\frac{\\partial z_2}{\\partial z_1}=w_2* ReLU(z_1)'

3. 损失函数计算

以均方误差 MSE 为例,适用于回归任务,单样本损失扩展到批量样本:

MSE 多样本:

E = \\frac{1}{2m} \\sum_{i=1}^m \\sum_{k=1}^{n_{out}} \\left(D_{\\text{true}}^{(i,k)} - O_{\\text{pred}}^{(i,k)}\\right)^2

MSE 单样本:

E = \\frac{1}{2} \\sum_{k=1}^{n_{out}} (d_k - o_k)^2

在输出伸进元只有一个时,即 n_{out}=1 

E = \\frac{1}{2} (d - o)^2   其中d是正确答案,是个常数

损失对输出的偏导:

\\frac{\\partial E}{\\partial o} = o-d

误差表示好之后,我们就要求导数了。这里的参数包括权重和偏置。所以要求损失函数对权重和偏置的导数。

反向传播(Backward Propagation)

核心逻辑:利用链式法则,从输出层反向计算损失对权重与偏置的导数,再用梯度下降更新参数。

1.误差求导

符号代码对应数学含义维度
\\delta _2 delta2 输出层误差项 \\frac{\\partial E}{\\partial z_2} (80, 1)
\\delta_1 delta1 隐藏层误差项 \\frac{\\partial E}{\\partial z_1}  (80, 4)
W2​ self.W2 隐藏层→输出层的权重矩阵 (4, 1)
W2T​ self.W2.T W2​ 的转置矩阵 (1, 4)
z1​ self.z1 隐藏层加权和(z1​=x⋅W1​+b1​) (80, 4)
ReLU′(z1​) self.relu_deriv(self.z1) ReLU 激活函数的导数 (80, 4)

输出层误差

delta_2 = \\frac{\\partial E}{\\partial z_2}=\\frac{\\partial E}{\\partial o}\\frac{\\partial o}{\\partial z_2}=(o-d)* \\sigma(\\sum z_2)(1-\\sigma(\\sum z_2))

隐藏层误差

delta_1 = \\frac{\\partial E}{\\partial z_1}=\\frac{\\partial E}{\\partial o}\\frac{\\partial o}{\\partial z_2}\\frac{\\partial z_2}{\\partial z_1}= delta2*w_2* ReLU(z_1)'

2. 输出层梯度计算

代码中的梯度变量数学含义(梯度)维度作用
dW1 \\frac{\\partial E}{\\partial w_1} (1, 4) 损失对 W1 的梯度
db1 \\frac{\\partial E}{\\partial b_1} (1, 4) 损失对 b1 的梯度
dW2 \\frac{\\partial E}{\\partial w_2} (4, 1) 损失对 W2 的梯度
db2 \\frac{\\partial E}{\\partial b_2} (1, 1) 损失对 b2 的梯度

梯度可以理解为:在参数空间中,让损失函数 “下降最快” 的方向 + 下降的 “速度”(大小)。

用生活中的例子类比,你站在一座山上(损失函数的 “地形”),目标是走到山脚(损失最小),梯度就是 “你当前位置,往哪个方向走,下山最快?每走一步能下降多少?”

神经网络的参数(W1、b1、W2、b2)就是 “你在山上的坐标”,梯度就是 “调整坐标的方向和幅度”,让你一步步走到山脚。

这就是梯度下降法,核心逻辑是:

参数 = 参数 – 学习率 × 梯度

训练结果如下

计算模型的参数量与内存占用

现在AI 领域比较火的7B 大模型,是指70亿个参数,那我们这段代码是多大呢,这是个超轻量级模型,只有 13 个可训练参数

参数名代码定义维度参数数量(个数)计算逻辑
W1 self.W1 = np.random.randn(1, 4) * 0.1 (1, 4) 1×4 = 4 1 个输入神经元→4 个隐藏神经元,每个连接对应 1 个权重
b1 self.b1 = np.zeros((1, 4)) (1, 4) 1×4 = 4 4 个隐藏神经元,每个对应 1 个偏置
W2 self.W2 = np.random.randn(4, 1) * 0.1 (4, 1) 4×1 = 4 4 个隐藏神经元→1 个输出神经元,每个连接对应 1 个权重
b2 self.b2 = np.zeros((1, 1)) (1, 1) 1×1 = 1 1 个输出神经元,对应 1 个偏置
总计 13 个 4+4+4+1 = 13

算模型的内存占用(多少 B)

NumPy 中默认的浮点数类型是 float64(双精度浮点数),每个 float64 占用 8 字节(B);如果是 float32(单精度),每个占用 4B。我们分两种情况计算:

按默认的 float64 计算(代码中实际使用的类型)

参数名参数数量每个参数字节数总字节数(B)
W1 4 8 4×8 = 32
b1 4 8 4×8 = 32
W2 4 8 4×8 = 32
b2 1 8 1×8 = 8
总计 13 104 B

Python 代码

代码如下

import numpy as np
import matplotlib.pyplot as plt
import tkinter as tk
from tkinter import simpledialog, messagebox

# ==================== 设置Matplotlib中文显示 ====================
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号

# 1. 定义神经网络类(三层:输入层1→隐藏层4→输出层1)
class ThreeLayerNN:
def __init__(self):
# 初始化权重
self.W1 = np.random.randn(1, 4) * 0.1
self.b1 = np.zeros((1, 4))
self.W2 = np.random.randn(4, 1) * 0.1
self.b2 = np.zeros((1, 1))

# 记录权重和损失变化
self.W1_history = []
self.W2_history = []
self.loss_history = []

# 新增:统计参数数量和内存大小
def get_params_info(self):
# 计算参数数量
w1_num = self.W1.size
b1_num = self.b1.size
w2_num = self.W2.size
b2_num = self.b2.size
total_params = w1_num + b1_num + w2_num + b2_num

# 计算内存大小
param_dtype = self.W1.dtype
bytes_per_param = param_dtype.itemsize
total_bytes = total_params * bytes_per_param
total_kb = total_bytes / 1024

return {
"total_params": total_params,
"bytes_per_param": bytes_per_param,
"total_bytes": total_bytes,
"total_kb": round(total_kb, 4)
}

# ReLU激活函数
def relu(self, x):
return np.maximum(0, x)

# ReLU导数
def relu_deriv(self, x):
return (x > 0).astype(np.float32)

# Sigmoid激活函数
def sigmoid(self, x):
return 1 / (1 + np.exp(-x))

# Sigmoid导数
def sigmoid_deriv(self, x):
return x * (1 – x)

# 前向传播
def forward(self, x):
self.z1 = np.dot(x, self.W1) + self.b1
self.h1 = self.relu(self.z1)
self.z2 = np.dot(self.h1, self.W2) + self.b2
self.o = self.sigmoid(self.z2)
return self.o

# 反向传播(梯度下降)
def backward(self, x, y, output, lr=0.2):
# 计算输出层误差
delta2 = (output – y) * self.sigmoid_deriv(output)
# 计算隐藏层误差
delta1 = np.dot(delta2, self.W2.T) * self.relu_deriv(self.z1)

# 计算梯度(按批次均值)
batch_size = x.shape[0]
dW2 = np.dot(self.h1.T, delta2) / batch_size
db2 = np.sum(delta2, axis=0, keepdims=True) / batch_size
dW1 = np.dot(x.T, delta1) / batch_size
db1 = np.sum(delta1, axis=0, keepdims=True) / batch_size

# 更新权重和偏置
self.W1 -= lr * dW1
self.b1 -= lr * db1
self.W2 -= lr * dW2
self.b2 -= lr * db2

# 记录权重均值
self.W1_history.append(np.mean(self.W1))
self.W2_history.append(np.mean(self.W2))

# 生成80组0/1数据(各40个,打乱顺序)
def generate_data(num_samples=80):
X = np.vstack([np.zeros((num_samples // 2, 1)), np.ones((num_samples // 2, 1))])
y = X.copy()
# 打乱顺序
indices = np.arange(num_samples)
np.random.shuffle(indices)
X = X[indices]
y = y[indices]
return X, y

# 训练神经网络
def train_nn():
nn = ThreeLayerNN()
X_train, y_train = generate_data(80)
epochs = 1000
learning_rate = 0.2

for epoch in range(epochs):
output = nn.forward(X_train)
nn.backward(X_train, y_train, output, learning_rate)

# 计算并记录损失
loss = np.mean((output – y_train) ** 2)
nn.loss_history.append(loss)

# 每50轮打印一次
if epoch % 50 == 0:
print(f"Epoch {epoch}, Loss: {loss:.6f}, W1均值: {np.mean(nn.W1):.4f}, W2均值: {np.mean(nn.W2):.4f}")

return nn

# 可视化(优化参数信息显示位置,无遮挡)
def plot_results(nn):
params_info = nn.get_params_info()
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 10))

# ==================== 1. 权重变化曲线(参数信息整合到标题,无遮挡) ====================
weight_title = (
f"神经网络权重随训练次数变化 | 模型参数:{params_info['total_params']}个 | "
f"内存占用:{params_info['total_bytes']}字节({params_info['total_kb']}KB)"
)
ax1.plot(nn.W1_history, label='隐藏层W1均值', color='blue')
ax1.plot(nn.W2_history, label='输出层W2均值', color='red')
ax1.set_xlabel('训练轮数', fontsize=10)
ax1.set_ylabel('权重均值', fontsize=10)
ax1.set_title(weight_title, fontsize=11, fontweight='bold', pad=15) # pad增加标题间距
ax1.legend(loc='upper right', fontsize=9) # 图例移到右上角
ax1.grid(True, alpha=0.3)

# ==================== 2. 损失变化曲线(最终损失移到右上角,无遮挡) ====================
ax2.plot(nn.loss_history, label='均方误差损失', color='green')
ax2.set_xlabel('训练轮数', fontsize=10)
ax2.set_ylabel('损失值', fontsize=10)
ax2.set_title('损失随训练次数变化', fontsize=12, fontweight='bold')
ax2.legend(loc='upper right', fontsize=9)
ax2.grid(True, alpha=0.3)

# 最终损失标注移到右上角(y轴0.8位置)
final_loss = nn.loss_history[-1]
ax2.text(0.98, 0.8, f"最终损失值:{final_loss:.6f}",
transform=ax2.transAxes,
fontsize=9,
horizontalalignment='right',
verticalalignment='center',
bbox=dict(boxstyle='round', facecolor='lightgreen', alpha=0.8))

plt.tight_layout()
plt.show()

# 弹窗交互预测
def predict_interactive(nn):
root = tk.Tk()
root.withdraw()

while True:
user_input = simpledialog.askstring("输入", "请输入0或1(输入q退出):")
if user_input is None or user_input.lower() == 'q':
messagebox.showinfo("提示", "退出预测程序!")
break
if user_input not in ['0', '1']:
messagebox.showerror("错误", "请输入有效的0或1!")
continue

x = np.array([[int(user_input)]])
prediction = nn.forward(x)
result = 1 if prediction > 0.5 else 0

messagebox.showinfo("识别结果",
f"输入:{user_input}\\n"
f"预测概率值:{prediction[0][0]:.4f}\\n"
f"识别结果:{result}")

root.destroy()

# 主程序执行
if __name__ == "__main__":
print("开始训练神经网络…")
trained_nn = train_nn()
print("训练完成!")

print("绘制权重和损失变化曲线(含参数信息)…")
plot_results(trained_nn)

print("启动交互预测窗口…")
predict_interactive(trained_nn)

赞(0)
未经允许不得转载:171主机测评 » 基于Python的最小AI 实践
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址