本文章的内容和知识为作者学习期间总结且部分内容借助AI理解,可能存在一定错误与误区,期待各位读者指正。
本文章中的部分例子仅用于生动解释相关概念,切勿结合实际过度解读。
语雀链接:《初识神经网络代码》
部分内容来源:B站:李哥考研
目前内容仍处于更新阶段,部分章节存在缺失内容,望读者理解
根据前两节课学习的知识,我们这次来模拟一下神经网络
还记得我们之前机器学习的宏观流程吗

根据这个步骤,让我们开始吧。
在开始定义模型之前,我们首先需要创造一些数据,代替真实的数据,以便我们此次练习使用
# 生成500条数据
num = 500
# 设置真正的w,一共4个权重系数,也就是说存在4个特征,一个特征对应着一个权重系数
true_w = torch.tensor([8.1, 2, 2, 4])
# 设置真正的b
true_b = torch.tensor(1.1)
def create_data(w, b, data_num): # 生成真实数据
"""
随机生成数据,以代表真实数据
:param w: 参数w
:param b: 参数b
:param data_num: 数据量
:return: 张量x 以及对应的 y
"""
# 生成数据 500*4 的张量 (4个权重系数 代表有4个特征)
x = torch.normal(0, 1, (data_num, len(w)))
# 生成真实的y = wx + b
y = torch.matmul(x, w) + b
# 生成噪音,使得y更为真实
noise = torch.normal(0, 0.01, y.shape)
# 生成理论的输出值y
y += noise
return x, y
# 生成真实数据X 以及对应的Y
X, Y = create_data(true_w, true_b, num)
根据代码所示,我们本次生成了500条数据(num),并生成4个权重值w,以及偏置b。从而我们可以根据 y = w x + b y=wx+b y=wx+b得到x所对应的y,由于真实情况下,数据不可能完全拟合于某个函数,我们为了让输出值y更“真实”一点,生成一些“噪音”与输出值y相加,这样数据就更贴近实际情况了。至此 我们得到了 用于此次实验的真实数据 x 以及对应的 y。
w为1行4列的张量,意味着存在4个特征,这也对应了x的列数,也就是说w的列数与X的列数相等。
x = torch.normal(0, 1, (data_num, len(w)))
,这行代码使用torch.normal
函数从均值为 0、标准差为 1 的正态分布中随机采样来生成输入数据 x。生成的张量 x 的形状为(data_num, len(w))
,也就是 (500, 4)(因为data_num
为 500,len(w)
为 4,对应 4 个特征)。
y = torch.matmul(x, w) + b
,通过torch.matmul
函数进行矩阵乘法操作,将输入数据 x 与权重 w 相乘,然后再加上偏置 b,得到理论上的输出值 y,此时的y是无噪声的,也就是没加上任何随机值。
noise = torch.normal(0, 0.01, y.shape)
,再次使用torch.normal
函数生成噪声数据,该正态分布的均值为 0,标准差为 0.01,形状与 y 相同,确保噪声能逐元素对应地添加到 y 上。然后通过y += noise
将生成的噪声添加到之前计算得到的 y 上,这样得到的 y 就是带有一定随机噪声的,模拟了在实际数据收集过程中可能存在的测量误差等情况,使得生成的数据更加贴近真。
准备工作做完了,接下来我们进行第一步,定义一个模型
# 随机生成一组初始权重w 这个w需要计算梯度
w_0 = torch.normal(0, 0.01, true_w.shape, requires_grad=True)
# 生成一个初始偏置b 同样需要计算梯度
b_0 = torch.tensor(0.01, requires_grad=True)
def fun(x, w, b):
"""
计算获取预测的Y
"""
pred_y = torch.matmul(x, w) + b # 根据y = wx+b,生成一个预测函数
return pred_y
这样我们便可以得到一个简单线性回归模型 y = w 0 x + b 0 y = w_{0} x +b_{0} y=w0x+b0
使用
torch.normal
函数从均值为 0、标准差为 0.01 的正态分布中随机采样来生成初始权重 w 0 w_0 w0。这里指定了其形状与true_w
的形状相同,也就是4(因为 true_w 代表真实的权重系数,有 4 个元素,对应 4 个特征),确保生成的初始权重 w_0 维度与实际问题中的特征数量匹配.
requires_grad
属性设置为 True,使其具备可求导的特性,以便后续参与到模型训练过程中的梯度计算和参数更新环节。
第二步,定义一个损失函数,我们需要根据损失函数来预测数据的损失程度。
def maeLoss(pre_y, y):
"""
计算平均绝对误差
:param pre_y: 预测值y
:param y: 真实值y
:return: 平均绝对误差
"""
return torch.sum(abs(pre_y - y)) / len(y)
输入值分别为预测值
pre_y
,以及真实值y
,选择使用平均绝对误差算法 L ( w , b ) = 1 n ∑ i = 1 n ∣ y 0 − y ∣ L(w,b)=\frac{1}{n}\sum_{i=1}^{n}|y_{0}-y| L(w,b)=n1∑i=1n∣y0−y∣,计算Loss值
第三步,根据算出的Loss值,对模型进行优化
# 设置学习率
lr = 0.03
# 随机梯度下降,更新参数
def sgd(paras, lr):
# 属于这句代码的部分,不计算梯度 在这个语句块内的操作不会被记录到计算图中,也就不会进行自动求导计算。
with torch.no_grad():
for para in paras:
# para.grad 表示该参数对应的梯度,这个梯度是在之前计算损失函数关于该参数的导数时得到的
para -= para.grad * lr # 不能写成 para = para - para.grad*lr
# 使用过的梯度,归0
para.grad.zero_()
我们设置了学习率为0.03,并根据给定的学习率和参数的梯度信息来更新参数。
学习率lr如果设置过小,可能导致训练过慢,几乎没什么效果,读者可以尝试调小lr数值,感受一下差别。
with torch.no_grad():
,这是一个用于控制是否进行自动求导计算的上下文管理语句。在这个语句块内的操作不会被记录到计算图中,也就不会进行自动求导操作。其原因在于,当执行参数更新操作时,本身并不需要对这个更新过程再次求导。
for para in paras:
paras 包含了需要更新的模型参数,也就是权重参数 w 和偏置参数 b 组成的列表
para -= para.grad * lr
这一操作实现了参数更新。其中para.grad
表示该参数对应的梯度,这个梯度是在之前计算损失函数关于该参数的导数时得到的。
para.grad.zero_()
对该参数的梯度进行清零操作。这是因为在每次计算损失函数对参数的梯度时,梯度是会累积的,而我们在每次基于当前梯度完成参数更新后,下一次重新计算梯度前,需要将之前的梯度清零,确保下一次计算得到的梯度是基于新的输入数据和当前最新的参数状态的,避免梯度的错误累积影响后续参数更新的准确性。
接下来 我们就可以正式开始了
def data_provider(data, label, batchsize):
"""
每次访问这个函数, 就能提供一批数据(数量与batchsize相等)
:param data: 数据源X
:param label: 标签,就是X所对应的Y
:param batchsize: 步长,代表着取多少条数据
:return: 随机的data和对应的label
"""
length = len(label) # 获取数据的总条数 labek = 500条
indices = list(range(length)) # 生成0到499的列表,用来代表数据的下标
random.shuffle(indices) # 按顺序取数据不具有随机性,因此打乱下标
for each in range(0, length, batchsize): # 从0取到499 步长为batchSize = 16
get_indices = indices[each: each + batchsize] # 获取之前已经打乱的下标,连续取16个
get_data = data[get_indices] # 根据下标,获取对应的data
get_label = label[get_indices] # 根据下标,获取对应label
yield get_data, get_label # 返回data和label yield是有存档点的return
# 步长
batchsize = 16
# 训练次数
epochs = 50
# 开始训练 训练50次
for epoch in range(epochs):
data_loss = 0 # 初始loss为0
for batch_x, batch_y in data_provider(X, Y, batchsize): # 从随机获取 16个X和对应Y
pred_y = fun(batch_x, w_0, b_0) # 根据真实的X 获取预测的Y
loss = maeLoss(pred_y, batch_y) # 根据预测的Y 和 真实的Y 计算损失值
loss.backward() # loss回归 执行反向传播(Backpropagation)的关键代码 触发这个反向传播计算梯度过程的操作。
sgd([w_0, b_0], lr) # 更新w 和 b
data_loss += loss # 累计loss
print("epoch %03d: loss: %.6f" % (epoch, data_loss)) # 输出epoch(轮次),loss(损失值)
print("真实的函数值是", true_w, true_b)
print("训练得到的参数值是", w_0, b_0)
我们先忽略data_provider
这个方法,从下面的代码开始分析。我们设置了训练次数epochs
,通过data_provider()
方法,从真实的x
和y
中获取数据,并根据第一步定义的函数,计算预测的y。得到预测值y
后,我们使用第二步编写的损失函数,计算出偏差loss
。随后对w
和b
进行更新,并累计loss
。完成50次训练后,我们输出真实的w
与b
,与我们训练得到的参数进行对比。
首先设定了
batchsize 为 16
,表示每次获取的数据批量大小为 16 个样本;epochs 为 50
,意味着整个训练过程会进行 50 轮。for epoch in range(epochs):
控制了训练的轮次,在每一轮中进行一系列的操作来更新模型参数并计算损失情况。
在每一轮训练中,将data_loss
初始化为 0,用于累计本轮训练中每个批次产生的损失值。然后通过for batch_x, batch_y in data_provider(X, Y, batchsize)
循环从data_provider()
函数获取批量的输入数据batch_x
和对应的标签数据batch_y
。
对于每个批次的数据,调用fun
函数,传入当前批次的输入数据batch_x
以及当前的模型参数w_0
和b_0
,计算得到预测值pred_y
。调用maeLoss()
函数,传入预测值pred_y
和真实标签batch_y
,计算出当前批次数据的平均绝对误差损失值loss
。
执行loss.backward()
触发反向传播过程,自动计算出损失函数关于模型参数(这里是 w_0 和 b_0)的梯度,存储在w_0.grad
和b_0.grad
中。接着调用sgd
函数,传入当前的模型参数列表[w_0, b_0]
和学习率lr
,按照随机梯度下降算法更新模型参数。
最后将当前批次计算得到的损失值loss
累加到data_loss
中,这样在本轮训练结束时,data_loss
就代表了这一轮训练中所有批次数据的损失值总和,用于衡量本轮训练的整体效果。
data_provider()
将给定的数据集(真实x,y),按照指定的批量大小(batchsize)进行分批处理,并以随机打乱的顺序逐批返回数据及其对应的标签。该方法首先获取数据长度并创建索引列表,打乱索引顺序,以模拟数据的下标,并按下标获取数据并返回。
yield
关键字:当调用一个包含yield
的函数时,函数的执行不会像普通函数那样一直运行到函数结束,而是会暂停在yield
语句处,并返回yield
后面表达式的值。当下次调用时,会保存记忆),函数会返回当前批次的get_data
和get_label
,同时函数的执行状态(包括局部变量的值、执行位置等)会被保存起来。如果第一次each
的值为0,那么下次就从16开始,虽然值已经返回了,但就像有一个存档点一样,并不会重新开始。