Skip to content

基本运算

乘法

import torch
x1 = torch.tensor([1,2]).cuda()#x1就在gpu上了
w1 = torch.tensor([2,0](2,0)).cuda()#w1就在gpu上了
x2 = torch.matmul(w1,x1)#等价于w1 @ x1.这个也是生成在gpu上.注意,@才是线性代数意义上的矩阵乘法.

自动微分

x1 = torch.tensor([2.0])#输入是一维的list就是一维的tensor.当然,实际上输入的类型远远不止list.注意,要改成浮点数才能进行计算.
w1 = torch.tensor([3.0],requires_grad = True)#表示这是需要求梯度的变量.

x2 = x1 ** w1
#x2对w1的梯度
x2.backward()
#dx2 / dw1.注意,dx2就应该是上面callbackward的位置.dw1就是想要求偏导的变量.
dx2_dw1 = w1.grad
#打印出dx2_dw1应该就是x2对w1求偏导的值.

如果把神经网络弄的更深一点.

x1 = torch.tensor([2.0])
w1 = torch.tensor([3.0], requires_grad = True)
w2 = torch.tensor([4.0]),requires_grad = True)
w3 = torch.tensor([5.0]),requires_grad = True)
target = torch.tensor([1.0])

x2 = w1 * x1
x3 = torch.functional.relu(w2 * x2)#添加relu激活函数的方法是这样的,sigmoid激活函数的方法是torch.sigmoid(w2 * x2)
x4 = w3 * x3
loss = (x4 - target) ** 2
loss.backward()
print(w1.grad)#dloss / dw1
print(w2.grad)#dloss / dw2
print(w3.grad)#dloss / dw3

优化器

x1 = torch.tensor([2.0])
w1 = torch.tensor([3.0], requires_grad = True)
w2 = torch.tensor([4.0]),requires_grad = True)
w3 = torch.tensor([5.0]),requires_grad = True)
target = torch.tensor([1.0])

x2 = w1 * x1
x3 = torch.functional.relu(w2 * x2)#添加relu激活函数的方法是这样的,sigmoid激活函数的方法是torch.sigmoid(w2 * x2)
x4 = w3 * x3

loss = (x4 - target) ** 2
loss.backward()

dloss_dw1 = w1.grad
dloss_dw2 = w2.grad
dloss_dw3 = w3.grad

learning_rate = 0.01
w1.data -= learning_rate * dloss_dw1
w2.data -= learning_rate * dloss_dw2
w3.data -= learning_rate * dloss_dw3
为什么不能直接用 w1? 当你创建一个张量并设置 requires_grad=True(例如 w1),PyTorch 的 autograd 引擎就会开始“监视”它。任何涉及到 w1 的计算(如 x2 = w1 * x1)都会被记录下来,形成一个计算图。这个图是计算梯度的基础。

更新也是一种操作:w1 -= learning_rate * dloss_dw1 这个更新步骤本身也是一个计算操作。

如果你直接对 w1 执行这个更新操作,autograd 引擎会尝试将这个操作也加入到计算图中。这意味着,你的权重 w1 不再是计算图的“叶子节点”,它本身也依赖于之前的梯度,这会把整个梯度计算体系搞乱。在下一次调用 loss.backward() 时,PyTorch 不知道该如何正确地回溯,因为用于前向传播的权重本身就有了梯度历史。这通常会导致运行时错误或意想不到的梯度结果。

方案一:旧方法 w1.data (不推荐使用) w1.data 是一个历史遗留的用法。它的作用是直接获取 w1 底层的数据张量,但绕过了 autograd 的跟踪系统。

方案二:现代最佳实践 with torch.no_grad(): (强烈推荐) with torch.no_grad(): 创建了一个上下文环境,告诉 PyTorch 在这个代码块内部,临时禁用所有梯度计算和跟踪。 因此,w1 -= learning_rate * dloss_dw1 就变成了一个纯粹的、不会被跟踪的数值更新操作。 或者说:只要不属于"model"的部分(这肯定不属于模型了,因为这里模型肯定是不会根据学习率来定义并且随着时间变化而变化的) 上面的代码还可以变得更加简洁:

x1 = torch.tensor([2.0])
w1 = torch.tensor([3.0], requires_grad = True)
w2 = torch.tensor([4.0]),requires_grad = True)
w3 = torch.tensor([5.0]),requires_grad = True)
target = torch.tensor([1.0])

parameters = [w1,w2,w3]#这里就是直接合成一个列表了

x2 = w1 * x1
x3 = torch.functional.relu(w2 * x2)#添加relu激活函数的方法是这样的,sigmoid激活函数的方法是torch.sigmoid(w2 * x2)
x4 = w3 * x3

loss = (x4 - target) ** 2
loss.backward()

dloss_dw1 = w1.grad
dloss_dw2 = w2.grad
dloss_dw3 = w3.grad

learning_rate = 0.01
for p in parameters:
    p.data -= 0.01 * p.grad
可以设置动态的学习率:(当然,这种设置是不对的)
x1 = torch.tensor([2.0])
w1 = torch.tensor([3.0], requires_grad = True)
w2 = torch.tensor([4.0]),requires_grad = True)
w3 = torch.tensor([5.0]),requires_grad = True)
target = torch.tensor([1.0])

parameters = [w1,w2,w3]#这里就是直接合成一个列表了

x2 = w1 * x1
x3 = torch.functional.relu(w2 * x2)#添加relu激活函数的方法是这样的,sigmoid激活函数的方法是torch.sigmoid(w2 * x2)
x4 = w3 * x3

loss = (x4 - target) ** 2
loss.backward()

dloss_dw1 = w1.grad
dloss_dw2 = w2.grad
dloss_dw3 = w3.grad

learning_rate = 0.01
for p in parameters:
    learning_rate = p.grad**2 
    p.data -= learning_rate * p.grad
可以使用"优化器"进行封装:
```python
x1 = torch.tensor([2.0])
w1 = torch.tensor([3.0], requires_grad = True)
w2 = torch.tensor([4.0]),requires_grad = True)
w3 = torch.tensor([5.0]),requires_grad = True)
target = torch.tensor([1.0])

optimizer = torch.optim.SGD([w1,w2,w3],lr=0.01)#优化器相当于是封装了上面的for p inparameters

x2 = w1 * x1
x3 = torch.functional.relu(w2 * x2)#添加relu激活函数的方法是这样的,sigmoid激活函数的方法是torch.sigmoid(w2 * x2)
x4 = w3 * x3

loss = (x4 - target) ** 2
loss.backward()

dloss_dw1 = w1.grad
dloss_dw2 = w2.grad
dloss_dw3 = w3.grad

optimizer.step()#这里就替代了原本的"一次梯度下降"
当然,除了SGD,还有Adam等. 我们的计算还可能有很多次循环,也就是多次的下降.
x1 = torch.tensor([2.0])
w1 = torch.tensor([3.0], requires_grad = True)
w2 = torch.tensor([4.0]),requires_grad = True)
w3 = torch.tensor([5.0]),requires_grad = True)
target = torch.tensor([1.0])

optimizer = torch.optim.SGD([w1,w2,w3],lr=0.01)#优化器相当于是封装了上面的for p inparameters
# ps:实际上,这个就是"随机梯度下降优化器"本身就是随机挑选batch出来了

for i in range(100):
    x2 = w1 * x1
    x3 = torch.functional.relu(w2 * x2)
    x4 = w3 * x3

    loss = (x4 - target) ** 2
    loss.backward()

    optimizer.step()#这里就替代了原本的"一次梯度下降"
    # 由于梯度会累加(也就是w1.grad+=这次算的梯度),因此这里需要清空一下
    optimizer.zero_grad()#其实等价于w1.grad.fill_(0),w2.grad.fill_(0)....

模型

上面的代码这里还是不够简洁:

    x2 = w1 * x1
    x3 = torch.functional.relu(w2 * x2)
    x4 = w3 * x3
这里不光是重复的问题.假如后面我还想评估模型,上面的逻辑又要重写一遍.因此我需要抽象出来:
x1 = torch.tensor([2.0])
w1 = torch.tensor([3.0], requires_grad = True)
w2 = torch.tensor([4.0]),requires_grad = True)
w3 = torch.tensor([5.0]),requires_grad = True)
target = torch.tensor([1.0])

optimizer = torch.optim.SGD([w1,w2,w3],lr=0.01)#优化器相当于是封装了上面的for p inparameters

def model_predict()
    x2 = w1 * x1
    x3 = torch.functional.relu(w2 * x2)
    x4 = w3 * x3
    return x4

for i in range(100):

    x4 = model_predict()

    loss = (x4 - target) ** 2
    loss.backward()

    optimizer.step()#这里就替代了原本的"一次梯度下降"
    # 由于梯度会累加(也就是w1.grad+=这次算的梯度),因此这里需要清空一下
    optimizer.zero_grad()#其实等价于w1.grad.fill_(0),w2.grad.fill_(0)....
可以抽象出面向对象,其实就是把一些涉及到的内容全部加到类里.
x1 = torch.tensor([2.0])

class MyModel(torch.nn.Module)
    def __init__(self):#替代原本暴漏出来的参数
        super(MyModel,self).__init__()
        self.w1 = torch.tensor([3.0], requires_grad = True)
        self.w2 = torch.tensor([4.0]),requires_grad = True)
        self.w3 = torch.tensor([5.0]),requires_grad = True)

    def forward(self,x)#替代上面的model_predict
        x2 = w1 * x
        x3 = torch.functional.relu(w2 * x2)
        x4 = w3 * x3
        return x4

my_model = MyModel()

target = torch.tensor([1.0])

optimizer = torch.optim.SGD(my_model.parameters(),lr=0.01)#这里会自动找到my_model里定义的这些参数,相当于原本的[w1,w2,w3]



for i in range(100):

    x4 = my_model.forward(x1)

    loss = (x4 - target) ** 2
    loss.backward()

    optimizer.step()#这里就替代了原本的"一次梯度下降"
    # 由于梯度会累加(也就是w1.grad+=这次算的梯度),因此这里需要清空一下
    optimizer.zero_grad()#其实等价于w1.grad.fill_(0),w2.grad.fill_(0)...
后面预测的时候,也只需要forward就行:
x4 = my_model.forward(x1)
而且这样可以很方便地切换model.

我们继续处理和抽象.我们现在需要抽象出input target output:(另外得改一下一些bug)

input_ = torch.tensor([2.0])

class MyModel(torch.nn.Module)
    def __init__(self):#替代原本暴漏出来的参数
        super(MyModel,self).__init__()
        self.w1 = torch.nn.Parameter(torch.tensor([3.0]))#由于统一交给model管理,我们不再用requires_grad表示这个是个变量了
        self.w2 = torch.nn.Parameter(torch.tensor([4.0]))
        self.w3 = torch.nn.Parameter(torch.tensor([5.0]))

    def forward(self,x)#替代上面的model_predict,这里也优化了一下.
        x = self.w1 * x
        x = torch.functional.relu(self.w2 * x)
        x = self.w3 * x
        return x

my_model = MyModel()

target_ = torch.tensor([1.0])

optimizer = torch.optim.SGD(my_model.parameters(),lr=0.01)#这里会自动找到my_model里定义的这些参数,相当于原本的[w1,w2,w3]

for i in range(100):

    output_ = my_model.forward(x1)

    loss = (output_ - target_) ** 2
    loss.backward()

    optimizer.step()#这里就替代了原本的"一次梯度下降"
    # 由于梯度会累加(也就是w1.grad+=这次算的梯度),因此这里需要清空一下
    optimizer.zero_grad()#其实等价于w1.grad.fill_(0),w2.grad.fill_(0)...

代码练习

# 任务2:只允许w2(输出层参数)进行改变

class MyModelW2Only(nn.Module):
    def __init__(self):
        super(MyModelW2Only, self).__init__()
        self.w1 = torch.tensor([1.0])  # 不设为Parameter,这样就不会被优化器更新
        self.w2 = nn.Parameter(torch.tensor([1.0])) 

    def forward(self, x):
        self.hidden_activation = x * self.w1  # w1固定不变
        output = self.hidden_activation * self.w2
        return output

input_ = torch.tensor([1.0])
target = torch.tensor([2.0])

my_model_w2 = MyModelW2Only()

optimizer_w2 = optim.SGD([my_model_w2.w2], lr=0.01)# 这样设置就可以只优化w2

epochs = 20
activations_history_w2 = []

print("任务2:只允许w2改变的训练过程")
print("=" * 50)

for epoch in range(epochs):
    output = my_model_w2.forward(input_)
    loss = (output - target) ** 2
    loss.backward()
    optimizer_w2.step()
    optimizer_w2.zero_grad()

    input_activation = input_.item()
    hidden_activation = my_model_w2.hidden_activation.item()
    output_activation = output.item()
    activations_history_w2.append([input_activation, hidden_activation, output_activation])

    print(f'Epoch [{epoch+1}/{epochs}], Loss: {loss.item():.4f}, w1: {my_model_w2.w1.item():.4f} (固定), w2: {my_model_w2.w2.item():.4f}')

# 任务2的可视化
plt.figure(figsize=(10, 6))

neuron_indices = [1, 2, 3]
neuron_labels = ['Input Neuron', 'Hidden Neuron', 'Output Neuron']

for i, activations in enumerate(activations_history_w2):
    # 为了让线条在图例中不那么拥挤,我们只显示部分epoch的标签
    if (i + 1) % 5 == 0 or i == 0:
        label = f'Epoch {i + 1}'
    else:
        label = None
    plt.plot(neuron_indices, activations, marker='o', linestyle='-', label=label)

plt.xlabel("神经元 (Neuron)")
plt.ylabel("激活值 (Activation Value)")
plt.title("任务2:只允许w2改变时神经元的激活值 (w1固定=1.0)")
plt.xticks(neuron_indices, neuron_labels)
plt.legend()
plt.grid(True)
plt.savefig('neuron_activations_task2_w2_only.png', dpi=300, bbox_inches='tight')
plt.show()

batch_size

一次性从数据集取多少数据,batch_size就是多大.比如设置成64个. 实际上是从数据集随机64个位置取64个点,组成一个batch.在batch上,总共的误差是"每个点"上误差的和.(然后取平均) 这样很方便进行并行化,同时更加稳定.

.detach()

把变量从计算图中去除/切断