神经网络代码入门解析
python
import torch
import matplotlib.pyplot as plt
import random
def create_data(w, b, data_num): # 数据生成
x = torch.normal(0, 1, (data_num, len(w)))
y = torch.matmul(x, w) + b # 矩阵相乘再加b
noise = torch.normal(0, 0.01, y.shape) # 为y添加噪声
y += noise
return x, y
num = 500
true_w = torch.tensor([8.1, 2, 2, 4])
true_b = 1.1
X, Y = create_data(true_w, true_b, num)
# plt.scatter(X[:, 3], Y, 1) # 画散点图 对X取全部的行的第三列,标签Y,点大小
# plt.show()
def data_provider(data, label, batchsize): # 每次取batchsize个数据
length = len(label)
indices = list(range(length))
# 这里需要把数据打乱
random.shuffle(indices)
for each in range(0, length, batchsize):
get_indices = indices[each: each+batchsize]
get_data = data[get_indices]
get_label = label[get_indices]
yield get_data, get_label # 有存档点的return
batchsize = 16
# for batch_x, batch_y in data_provider(X, Y, batchsize):
# print(batch_x, batch_y)
# break
# 定义模型
def fun(x, w, b):
pred_y = torch.matmul(x, w) + b
return pred_y
# 定义loss
def maeLoss(pre_y, y):
return torch.sum(abs(pre_y-y))/len(y)
# sgd(梯度下降)
def sgd(paras, lr):
with torch.no_grad(): # 这部分代码不计算梯度
for para in paras:
para -= para.grad * lr # 不能写成 para = para - paras.grad * lr !!!! 这句相当于要创建一个新的para,会导致报错
para.grad.zero_() # 将使用过的梯度归零
lr = 0.01
w_0 = torch.normal(0, 0.01, true_w.shape, requires_grad=True)
b_0 = torch.tensor(0.01, requires_grad=True)
print(w_0, b_0)
epochs = 50
for epoch in range(epochs):
data_loss = 0
for batch_x, batch_y in data_provider(X, Y, batchsize):
pred_y = fun(batch_x, w_0, b_0)
loss = maeLoss(pred_y, batch_y)
loss.backward()
sgd([w_0, b_0], lr)
data_loss += loss
print("epoch %03d: loss: %.6f" % (epoch, data_loss))
print("真实函数值:", true_w, true_b)
print("训练得到的函数值:", w_0, b_0)
idx = 0
plt.plot(X[:, idx].detach().numpy(), X[:, idx].detach().numpy()*w_0[idx].detach().numpy()+b_0.detach().numpy())
plt.scatter(X[:, idx].detach().numpy(), Y, 1)
plt.show()
逐步分析代码
1.数据生成

首先设计一个函数create_data
,提供我们所需要的数据集的x与y
py
def create_data(w, b, data_num): # 数据生成
x = torch.normal(0, 1, (data_num, len(w))) # 生成特征数据,形状为 (data_num, len(w))
y = torch.matmul(x, w) + b # 计算目标值 y = x * w + b
noise = torch.normal(0, 0.01, y.shape) # 生成噪声,形状与 y 相同
y += noise # 为 y 添加噪声,模拟真实数据中的随机误差
return x, y
-
torch.normal() 生成一个张量
torch.normal(0, 1, (data_num, len(w)))
:生成一个形状为(data_num, len(w))
的张量,其中的元素是从均值为0
、标准差为1
的正态分布中随机采样的。
-
torch.matmul() 让矩阵相乘
matmul: matrix multiply
-
再使用torch.normal()生成一个张量,添加到y上,相当于为y添加了随机的噪声
噪声的引入是为了模拟真实数据中的随机误差,使生成的数据更接近现实场景。
2.设计一个数据加载器
py
def data_provider(data, label, batchsize): # 每次取 batchsize 个数据
length = len(label)
indices = list(range(length))
random.shuffle(indices) # 打乱数据顺序,避免模型学习到顺序特征
for each in range(0, length, batchsize):
get_indices = indices[each: each+batchsize] # 获取当前批次的索引
get_data = data[get_indices] # 获取当前批次的数据
get_label = label[get_indices] # 获取当前批次的标签
yield get_data, get_label # 返回当前批次的数据和标签
data_provider可以分批提供数据,并通过yield来返回已实现记忆功能
首先把list y顺序打乱,这样就相当于从生成的训练集y中随机读取,若不打乱数据,可能造成训练结果的不理想
打乱数据可以避免模型在训练过程中学习到数据的顺序特征,从而提高模型的泛化能力。
之后分段遍历打乱的y,返回对应的局部的数据集来给神经网络进行训练
3.定义模型函数

py
def fun(x, w, b):
pred_y = torch.matmul(x, w) + b # 计算预测值 y = x * w + b
return pred_y
fun(x, w, b)
是一个线性模型,形式为y = x * w + b
,其中x
是输入特征,w
是权重,b
是偏置。
4.定义Loss函数

py
def maeLoss(pre_y, y):
return torch.sum(abs(pre_y - y)) / len(y) # 计算平均绝对误差 (MAE)
maeLoss
是平均绝对误差(Mean Absolute Error, MAE),它计算预测值pre_y
和真实值y
之间的绝对误差的平均值。- 公式为:
MAE = (1/n) * Σ|pre_y - y|
,其中n
是样本数量。
5.梯度下降sgd函数
py
# sgd(梯度下降)
def sgd(paras, lr):
with torch.no_grad(): # 这部分代码不计算梯度
for para in paras:
para -= para.grad * lr # 不能写成 para = para - paras.grad * lr !!!! 这句相当于要创建一个新的para,会导致报错
para.grad.zero_() # 将使用过的梯度归零
这里需要使用torch.no_grad()来避免重复计算梯度

在前向过程中已经累计过一次梯度了,如果在梯度下降过程中又累计了梯度,那么就会造成不必要的麻烦
PyTorch 会累积梯度,如果不手动清零,梯度会不断累积,导致参数更新错误。
para -= para.grad * lr
就是将参数w修正的过程(w=w-(dy^/dw)*learningRate)
torch.no_grad()
是一个上下文管理器,用于禁用梯度计算。在参数更新时,禁用梯度计算可以避免不必要的计算和内存占用。
5.开始训练
py
epochs = 50
for epoch in range(epochs):
data_loss = 0
num_batches = len(Y) // batchsize # 计算批次数量
for batch_x, batch_y in data_provider(X, Y, batchsize):
pred_y = fun(batch_x, w_0, b_0) # 前向传播
loss = maeLoss(pred_y, batch_y) # 计算损失
loss.backward() # 反向传播
sgd([w_0, b_0], lr) # 更新参数
data_loss += loss.item() # 累积损失
print("epoch %03d: loss: %.6f" % (epoch, data_loss / num_batches)) # 打印平均损失
先定义一个训练轮次epochs=50,表示训练50轮
在每轮训练中将loss记录下来,以此评价训练的效果
首先用data_provider
来获取数据集中随机的一部分
接着传入相应数据给模型函数,通过前向传播获得预测y值pred_y
调用Loss计算函数,获取这次的loss,再通过反向传播loss.backward()
计算梯度
loss.backward()
是反向传播的核心步骤,用于计算损失函数对模型参数的梯度。
再通过梯度下降sgd([w_0, b_0], lr)
来更新模型的参数
最终将这组数据的loss累加到这轮数据的loss中
6.结果绘制
py
idx = 0
plt.plot(X[:, idx].detach().numpy(), X[:, idx].detach().numpy() * w_0[idx].detach().numpy() + b_0.detach().numpy()) # 绘制预测直线
plt.scatter(X[:, idx].detach().numpy(), Y, 1) # 绘制真实数据点
plt.show()