第2部分:创建数据

def create_data(w, b, data_num):

x = torch.normal(0, 1, (data_num, len(w)))

y = torch.matmul(x, w) + b

noise = torch.normal(0, 0.01, y.shape)

y += noise

return x, y

```

1. `def create_data(w, b, data_num):`    定义一个名为`create_data`的函数,它接受三个参数:权重`w`、偏置`b`和数据数量`data_num`。

2. `x = torch.normal(0, 1, (data_num, len(w)))`    使用PyTorch的`normal`函数生成一个形状为`(data_num, len(w))`的张量`x`,其中`data_num`是数据点的数量,`len(w)`是特征的数量。这个张量包含从标准正态分布(均值为0,标准差为1)中随机抽取的数值。

3. `y = torch.matmul(x, w) + b`    计算目标值`y`。首先,使用`torch.matmul(x, w)`计算输入数据`x`和权重`w`之间的矩阵乘法,然后加上偏置`b`。这模拟了线性回归模型的真实输出。

4. `noise = torch.normal(0, 0.01, y.shape)`    生成一个与`y`形状相同的张量`noise`,其值来自均值为0、标准差为0.01的正态分布。这个张量代表添加到目标值`y`上的噪声。

5. `y += noise`    将噪声`noise`加到目标值`y`上,以模拟真实世界中的数据噪声。

6. `return x, y`    返回生成的输入数据`x`和对应的目标值`y`。

第3部分:生成真实权重和偏置,并创建数据

num = 500

true_w = torch.tensor([8.1, 2, 2, 4])

true_b = torch.tensor(1.1)

X, Y = create_data(true_w, true_b, num)

1. `num = 500`    设置变量`num`为500,表示要生成500个数据点。

2. `true_w = torch.tensor([8.1, 2, 2, 4])`    创建一个PyTorch张量`true_w`,并赋值为`[8.1, 2, 2, 4]`。这个张量代表了真实的权重值,用于之后的模型训练和比较。

3. `true_b = torch.tensor(1.1)`    创建一个PyTorch张量`true_b`,并赋值为`1.1`。这个张量代表了真实的偏置值。

4. `X, Y = create_data(true_w, true_b, num)`    调用之前定义的`create_data`函数,使用真实的权重`true_w`、偏置`true_b`和数据点数量`num`来生成输入数据`X`和对应的目标值`Y`。

这两部分代码的主要目的是生成一个线性回归问题的数据集,其中包括带有噪声的输入特征`X`和对应的目标值`Y`。这些数据将用于之后的模型训练和评估。

Q:在第二部分的步骤3中,数据x和权重w之间的矩阵乘法后加上b是否可以被我理解为不停的循环计算w*x+b?实际上这里的步骤是将每一组x与权重相乘再加上一个偏置,从而得到该x对应的y,我的理解正确吗?

A:您的理解是正确的。在矩阵乘法`torch.matmul(x, w)`中,实际上是对每一行`x`(代表一个数据点)和权重向量`w`进行点积运算。点积运算的结果是一个标量,代表该数据点在权重向量方向上的投影长度。然后,这个标量再加上偏置`b`,得到目标值`y`。

具体来说,如果我们有`data_num`个数据点,每个数据点有`len(w)`个特征,那么`x`的形状是`(data_num, len(w))`,`w`的形状是`(len(w), 1)`(因为`w`是一个权重向量)。矩阵乘法`torch.matmul(x, w)`的结果是一个`(data_num, 1)`形状的张量,其中每一行都是对应数据点与权重向量的点积。

然后,再加上偏置`b`,`b`是一个标量或者形状为`(data_num, 1)`的张量(在这种情况下,它会被广播到每个数据点)。因此,这个加法操作是将偏置加到每一个数据点对应的点积结果上。

所以,您的理解是正确的,这个过程可以被看作是对每一组`x`与权重`w`相乘再加上偏置`b`,从而得到该`x`对应的`y`。这确实是线性回归模型的基本计算过程。

Q:好的。接下来,在第三部分中,true_w的建立是否意味着我们的回归函数可以被理解为w1x1+w2x2+w3x3+b?将w和b张量化后输入create_data函数,我们便为该回归函数确定了偏置值和对于每一个xi的权重,因此只要在函数中生成随机的x并进行计算得到y后再返回,我们便得到了一组关于这个线性回归模型的模拟数据。请问我的理解正确吗?

A:

您的理解完全正确。在第三部分中,`true_w` 的建立确实表示了一个线性回归函数的形式,其中 `w1`、`w2`、`w3` 等是各个特征的权重,`b` 是偏置项。对于线性回归模型,预测的 `y` 值是根据每个特征 `xi` 与其对应权重 `wi` 的乘积之和再加上偏置 `b` 来计算的。数学表达式可以写作:

\[ y = w_1 x_1 + w_2 x_2 + w_3 x_3 + \ldots + w_n x_n + b \]

其中 `n` 是特征的数量。在这个例子中,我们有4个特征,所以线性回归函数是:

\[ y = w_1 x_1 + w_2 x_2 + w_3 x_3 + w_4 x_4 + b \]

通过将 `true_w` 和 `true_b` 作为参数传递给 `create_data` 函数,我们实际上是在为这个线性回归函数设置真实的权重和偏置值。然后,函数生成随机的输入数据 `x`,并根据这些权重和偏置值计算对应的目标值 `y`。这样,我们就得到了一组模拟数据,这些数据是根据我们定义的线性回归模型生成的,并包含了随机噪声以模拟真实世界中的不确定性。

总结来说,您的理解是正确的:通过设置权重和偏置,并使用这些值在 `create_data` 函数中生成数据和目标值,我们得到了一个线性回归模型的模拟数据集。这个数据集可以用于后续的模型训练和评估。

第4部分:散点图

plt.scatter(X[:, 3], Y, 1) # 画图

plt.show()

Q:你的意思是,这两行代码其实只绘制了某一个特征对应的y值,而我们的回归函数,刚才提到过,应该是有四个权重及其对应的特征。也就是说,这幅散点图只能表示一种特征对于y的影响,对吗?

A:

是的,您理解得对。在给出的代码示例中,`plt.scatter(X[:, 0], Y, color='blue')` 这行代码仅仅绘制了数据集中的一个特征(`X` 的第一列,即 `X[:, 0]`)与对应的目标值 `Y` 之间的散点图。

由于线性回归模型通常涉及多个特征,每个特征都有自己的权重,因此仅通过一个二维散点图无法展示所有特征与目标值之间的关系。这幅散点图只能揭示第一个特征与目标值之间的潜在线性关系,而忽略了其他特征的影响。

要完整地理解所有特征如何共同影响目标值 `Y`,通常需要更复杂的可视化方法。例如,可以使用成对散点图(pairplot)来展示每对特征与目标值之间的关系,或者利用降维技术(如主成分分析PCA)来降低数据维度,并在较低维度上可视化数据点。这些方法能够帮助我们更全面地理解数据的结构和特征之间的相互作用。

然而,在某些情况下,即使只能观察一个特征的散点图,也可能足够用来理解该特征与目标值之间的基本关系,并为后续的模型训练提供指导。例如,如果散点图显示出一个清晰的线性趋势,那么线性回归模型可能是一个合适的选择。如果关系看起来是非线性的,那么可能需要考虑使用其他类型的模型。

第5部分:数据提供器

def data_provider(data, label, 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

文心一言解释:

首先,我们有一个名为 `data_provider` 的函数定义,该函数用于提供数据批次。函数接受三个参数:`data`(特征数据)、`label`(标签数据)和 `batchsize`(每个批次的大小)。

1

length = len(label)

    * 这行代码计算标签数据的长度,并将其存储在变量 `length` 中。 2. 

indices = list(range(length))

    * 这行代码创建一个从 0 到 `length-1` 的整数列表,表示数据的索引。 3. 

random.shuffle(indices)

    * 这行代码将 `indices` 列表中的元素随机打乱。这样,在后续的数据提取过程中,数据的顺序将被随机化,有助于模型的泛化。 4. 

for each in range(0, length, batchsize):

    * 这是一个循环,从 0 开始,以 `batchsize` 为步长,遍历到 `length`。这样,每次循环都会得到一个起始索引 `each`,用于从数据中提取一个批次。 5. 

get_indices = indices[each: each+batchsize]

    * 根据当前的起始索引 `each` 和批次大小 `batchsize`,从 `indices` 列表中提取一个批次的索引。 6. 

get_data = data[get_indices]

    * 使用上一步得到的索引 `get_indices`,从 `data` 中提取相应的特征数据。 7. 

get_label = label[get_indices]

    * 同样地,使用 `get_indices` 从 `label` 中提取相应的标签数据。 8. 

yield get_data, get_label

    * `yield` 关键字表示这是一个生成器函数。每次调用该函数时,它会返回一个批次的数据和标签,然后暂停,等待下一次调用。这允许我们按需生成数据批次,而不是一次性加载所有数据到内存中。

接下来,我们设置了批次大小并调用 `data_provider` 函数:

python

batchsize = 16

for batch_x, batch_y in data_provider(X, Y, batchsize):

    print(batch_x, batch_y)

    break

1. `batchsize = 16`

    * 设置每个数据批次的大小为 16。 2. `for batch_x, batch_y in data_provider(X, Y, batchsize):`

    * 调用 `data_provider` 函数,并将 `X`(特征数据)、`Y`(标签数据)和 `batchsize` 作为参数传递。然后,函数会返回一个生成器,我们在这个循环中迭代这个生成器。 3. `print(batch_x, batch_y)`

    * 在循环内部,我们打印当前批次的特征数据 `batch_x` 和标签数据 `batch_y`。 4. `break`

    * 这行代码会终止循环,所以我们只会看到第一个数据批次。

总的来说,这段代码实现了一个简单的数据提供器,它能够将数据随机打乱并分成指定大小的批次,然后按需生成这些批次。这对于训练深度学习模型时的小批量梯度下降非常有用。

Q:每一次执行yield时,我们就给batch_x和batch_y返回了对应一个批次的x和y值,对吗?而yield会使我们下次执行data_provider时,可以从上次结束的地方继续运行,从而提取了下一组批次的数据,对吗?

A:

是的,您理解得完全正确。在data_provider函数中,yield关键字的使用确实起到了这样的作用。

每次执行yield时,函数会暂停执行,并返回yield后面的值,也就是当前批次的数据和标签get_data, get_label。在这个上下文中,yield返回的是一个生成器对象,它允许我们在每次迭代时按需生成数据批次,而不是一次性加载所有数据到内存中。

当我们在循环中迭代data_provider函数时,每次循环都会从上次yield暂停的地方继续执行,直到再次遇到yield。这样,我们可以逐个提取数据批次,直到处理完所有的数据。

Q:第一行的计算标签长度,和计算数据的长度会有什么区别吗? 第二就是,indices列表我可不可以理解为一个装有每一组x和y值的结构体的指针?这样一来下面的get_data和get_label就和访问一个结构体的成员差不多意思了

A:没有区别。 可以这样理解。

第6部分:模型函数,损失函数及训练主体

当然,我可以帮您将这些代码分为几个部分,并解释每个部分的作用。以下是代码的分组和每个部分的解释:

### 1. 定义模型函数

python

def fun(x, w, b):

    pred_y = torch.matmul(x, w) + b

    return pred_y

**作用**: 这个函数定义了模型的预测过程。它接受输入数据`x`、权重`w`和偏置`b`作为参数,并通过矩阵乘法和加法操作计算预测值`pred_y`。

### 2. 定义损失函数

python

def maeLoss(pred_y, y):

    loss = torch.sum(abs(pred_y - y)) / len(y)

    return loss

**作用**: 这个函数定义了平均绝对误差(Mean Absolute Error, MAE)损失函数。它接受预测值`pred_y`和真实值`y`作为参数,并计算两者之间的平均绝对差值作为损失。

### 3. 定义优化器函数

def sgd(paras, lr):

    with torch.no_grad():    # 接下来的计算 不算梯度

        for para in paras:

            para -= para.grad * lr        # A -= 3  A = A-3

            para.grad.zero_()

**作用**: 这个函数实现了随机梯度下降(Stochastic Gradient Descent, SGD)优化算法。它接受参数列表`paras`和学习率`lr`作为参数,并通过循环遍历每个参数,使用其梯度进行更新,并清零梯度。

### 4. 初始化参数和设置学习率

lr = 0.03

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

**作用**: 这部分代码初始化了学习率`lr`、权重`w_0`和偏置`b_0`。其中,`w_0`和`b_0`是从正态分布和常数中随机初始化的,并设置为需要计算梯度(`requires_grad=True`)。同时,设置了训练轮数`epochs`。

### 5. 训练模型

for epoch in range(epochs):

    data_loss = 0

    for batch_x, batch_y in data_provider(X, Y, batchsize):

        pred = fun(batch_x, w_0, b_0)

        loss = maeLoss(pred, batch_y)

        loss.backward()

        sgd([w_0, b_0], lr)

        data_loss += loss

        print("epoch %03d: loss: %.6f"%(epoch, data_loss))

**作用**: 这部分代码是模型训练的主体部分。它使用`data_provider`函数按批次获取数据,并通过`fun`函数进行预测,然后计算损失并使用`loss.backward()`进行反向传播。接着,使用`sgd`函数进行参数更新。每个epoch结束后,打印当前epoch的损失值。

### 6. 输出结果和可视化 ```python

print("原来的函数值:", true_w, true_b)

print(w_0, b_0)

idx = 1

plt.plot(X[:, idx].detach().numpy(), X[:, idx].detach().numpy() * w_0[idx].detach().numpy() + b_0.detach().numpy(), label="pred")

plt.scatter(X[:, idx], Y, 1, label="true")

plt.show()

**作用**: 这部分代码首先输出了真实的权重和偏置值(`true_w`, `true_b`)以及训练后的权重和偏置值(`w_0`, `b_0`)。然后,使用matplotlib库对其中一个特征进行可视化,展示了真实的数据点(scatter plot)和模型预测的线(plot)。

关于优化器:  

当然,第3部分的优化器函数是实现了随机梯度下降(Stochastic Gradient Descent, SGD)的一个简化版本。在深度学习中,优化器用于更新模型的参数(如权重和偏置)以最小化损失函数。这里我们逐步解释sgd函数的作用:

def sgd(paras, lr):

with torch.no_grad(): # 接下来的计算 不算梯度

for para in paras:

para -= para.grad * lr # A -= 3 A = A-3

para.grad.zero_() # 把梯度归0

with torch.no_grad()::这个上下文管理器的作用是告诉PyTorch,在这个块里面的所有操作都不需要计算梯度。因为在优化步骤中,我们不需要计算梯度来更新参数,而是直接使用已经计算好的梯度。这也有助于节省内存,因为不需要存储中间操作的梯度。 for para in paras::这个循环遍历了所有的模型参数(权重和偏置),这些参数在paras列表中。 para -= para.grad * lr:这行代码实现了参数的更新。para.grad存储了参数para关于损失函数的梯度,lr是学习率。这个操作实质上是按照梯度的方向和大小,以及学习率的大小来更新参数。具体来说,就是沿着梯度的反方向(因为我们要最小化损失函数,而梯度指向的是函数增长最快的方向)移动参数,移动的距离由学习率控制。 para.grad.zero_():这行代码将参数的梯度归零。这是因为在PyTorch中,梯度是累积的,也就是说,如果你不手动清零,那么下一次反向传播时,新计算的梯度会累加到现有的梯度上。但在SGD中,我们希望每次更新参数后都使用新的梯度,因此需要在每次参数更新后将梯度清零。

总的来说,sgd函数的作用是:在给定学习率的情况下,根据已经计算好的梯度来更新模型的参数,并在每次更新后将梯度清零,以便进行下一次的参数更新。这是训练神经网络时常见的参数更新策略之一。

Q:为什么优化器中无需计算梯度?每次运行优化器后清零当前梯度是为什么? A:

在深度学习中,当我们说“不需要计算梯度来更新参数”时,实际上是指在优化步骤中不需要再次进行前向和反向传播来计算梯度。这里的梯度已经在之前的前向和反向传播过程中计算出来了,并且存储在了参数的.grad属性中。

让我们回顾一下梯度是如何计算的:

前向传播:首先,我们将输入数据传递给模型(在这个例子中是fun函数),模型根据当前的参数(权重和偏置)计算出预测值。这个过程中,PyTorch会跟踪所有涉及到的运算,以便后续计算梯度。 损失计算:然后,我们使用损失函数(在这个例子中是maeLoss函数)来计算预测值与真实值之间的损失。 反向传播:接着,我们调用损失值的.backward()方法。这一步会触发PyTorch的自动微分系统,根据之前前向传播中跟踪的运算,自动计算出损失函数关于模型参数的梯度,并将这些梯度存储在相应参数的.grad属性中。 参数更新:在梯度计算完成后,我们进入优化步骤。在这个步骤中,我们使用优化器(在这个例子中是sgd函数)来根据计算好的梯度更新模型的参数。这就是为什么我们说“不需要计算梯度来更新参数”,因为梯度已经在之前的反向传播过程中计算好了。 梯度清零:在参数更新之后,为了防止梯度累积(即下一次反向传播时新计算的梯度会累加到现有梯度上),我们需要将参数的梯度清零。这就是为什么在sgd函数中会有para.grad.zero_()这一行代码。

综上所述,已经计算好的梯度是在之前的前向传播和反向传播过程中计算出来的,并存储在参数的.grad属性中。在优化步骤中,我们直接使用这些已经计算好的梯度来更新参数,而不需要再次计算梯度。

至于为什么每次计算时都需要重置参数的梯度,这是因为在神经网络的训练过程中,我们通常使用梯度下降算法来更新模型的参数。在每一次迭代中,我们都会计算损失函数关于模型参数的梯度,并使用这些梯度来更新参数。如果我们不清零梯度,那么梯度就会累积起来,导致下一次迭代时计算的梯度是上一次迭代梯度的累加,而不是当前迭代的梯度。这样就会破坏梯度下降算法的正确性,导致模型训练出现问题。因此,在每次迭代更新参数之后,我们都需要将参数的梯度清零,以便进行下一次迭代的梯度计算。

参考链接

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: