学习笔记

全连接神经网络完全指南:从零到PyTorch实战

一、从最简单的线性关系说起:y = wx + b

1.1 理解基本公式

想象你在预测地铁到站时间,发现一个规律:时间 = 速度 × 距离 + 基础时间。这就是最简单的神经网络!

1
2
3
4
5
6
7
# 最原始的预测函数
def forward(x):
return x * w # w是权重,决定斜率

# 例如:距离1km用时2分钟,距离2km用时4分钟
x_data = [1, 2, 3]
y_data = [2, 4, 6]

但问题来了:我怎么知道w应该是多少? 这就需要”学习”!

1.2 什么是损失(Loss)

损失就是”打分系统”,告诉你预测得有多烂:

1
2
3
4
5
def cost(x_in, y_in):
loss = 0
for x, y in zip(x_in, y_in):
loss += (y - forward(x))**2 # 预测值和真实值的差距平方
return loss / len(x_in)

核心思想:预测值和真实值差距越大,loss越大,说明你的w选得很糟糕。


二、如何调整参数:梯度下降

2.1 梯度是什么

梯度就是”指南针”,告诉你:

  • 往哪个方向调整w,loss会下降
  • 调整幅度应该多大
1
2
3
4
5
def gradient(x_in, y_in):
grad = 0
for x, y in zip(x_in, y_in):
grad += 2 * x * (w * x - y) # 损失函数对w求导
return grad / len(x_in)

数学本质:对损失函数求偏导数 → ∂Loss/∂w

2.2 手动梯度下降循环

1
2
3
4
5
6
w = 4  # 初始猜测
for epoch in range(100):
loss = cost(x_data, y_data)
grad = gradient(x_data, y_data)
w = w - 0.01 * grad # 朝梯度相反方向移动
print(f'训练轮数{epoch}, w={w}, loss={loss}')

结果:w从4逐渐收敛到2,loss从18降到0.00009 ✅


三、PyTorch自动梯度:告别手动求导

3.1 使用requires_grad自动追踪

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import torch

# 创建需要计算梯度的张量
x = torch.tensor([10.0], requires_grad=True)
lr = 0.1

for i in range(20):
y = x*x - 4*x # 前向计算
y.backward() # 自动计算梯度!

print(f'第{i}步x的梯度为{x.grad}')

# 更新参数(注意要用no_grad包裹)
with torch.no_grad():
x -= x.grad * lr
x.grad.zero_() # 清零梯度,准备下一轮

关键点

  • backward():自动计算所有梯度
  • no_grad():更新参数时不记录梯度(节省内存)
  • zero_():必须清零,否则梯度会累加

四、扩展到多维:矩阵运算

4.1 为什么需要矩阵

现实中,输入不是一个数字,而是很多特征(速度、距离、角度…):

1
2
3
4
5
6
7
8
9
# 输入是3维向量
x = torch.randn(1, 3) # [速度, 距离, 角度]

# 手工版:y = xW + b
W = torch.randn(3, 5, requires_grad=True) # 3个输入 → 5个输出
b = torch.randn(5, requires_grad=True)
y_manual = torch.matmul(x, W) + b

print(f"输出形状: {y_manual.shape}") # torch.Size([1, 5])

4.2 工业标准:nn.Linear

PyTorch封装好了线性层:

1
2
3
4
5
6
7
8
9
10
import torch.nn as nn

# 自动创建W和b
linear_layer = nn.Linear(in_features=3, out_features=5)

# 直接调用
y_auto = linear_layer(x)

print(f"权重形状: {linear_layer.weight.shape}") # [5, 3]
print(f"偏置形状: {linear_layer.bias.shape}") # [5]

五、构建多层神经网络

5.1 激活函数:打破线性

如果只堆叠线性层,相当于 y = W3(W2(W1x)) = 还是线性!

解决方案:加入非线性激活函数(ReLU)

1
2
3
relu = nn.ReLU()
print(f"ReLU(-5): {relu(torch.tensor([-5.0]))}") # 0
print(f"ReLU(5): {relu(torch.tensor([5.0]))}") # 5

ReLU规则:负数变0,正数保持不变。

5.2 完整网络定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ObstacleNet(nn.Module):
def __init__(self):
super().__init__()
self.layer1 = nn.Linear(784, 256) # 输入层
self.act = nn.ReLU() # 激活函数
self.layer2 = nn.Linear(256, 64) # 隐藏层
self.layer3 = nn.Linear(64, 10) # 输出层

def forward(self, x):
x = self.layer1(x)
x = self.act(x)
x = self.layer2(x)
x = self.act(x)
out = self.layer3(x)
return out

model = ObstacleNet().cuda() # 搬到GPU

网络结构:784 → 256 → 64 → 10(比如识别0-9数字)


六、训练五步法

6.1 准备工作

1
2
3
4
5
6
7
8
9
10
11
import torch.optim as optim

# 1. 定义损失函数(CrossEntropyLoss用于分类)
criterion = nn.CrossEntropyLoss()

# 2. 定义优化器(Adam是最常用的)
optimizer = optim.Adam(model.parameters(), lr=0.001)

# 3. 准备数据
inputs = torch.randn(8, 784).cuda() # 8张图
targets = torch.randint(0, 10, (8,)).cuda() # 8个标签(0-9)

6.2 训练循环(核心!)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
for i in range(100):
# Step 1: 梯度清零
optimizer.zero_grad()

# Step 2: 前向传播
outputs = model(inputs)

# Step 3: 计算损失
loss = criterion(outputs, targets)

# Step 4: 反向传播(计算梯度)
loss.backward()

# Step 5: 更新参数
optimizer.step()

print(f"第{i}轮 Loss: {loss.item():.4f}")

为什么要清零梯度? PyTorch默认梯度累加,不清零会导致梯度爆炸!


七、关于你提到的问题

7.1 Softmax vs Sigmoid

应用场景

  • Softmax:多分类(10个数字选1个)→ CrossEntropyLoss
  • Sigmoid:二分类(障碍物/正常)→ BCELoss
1
2
3
4
5
6
7
# Softmax:输出概率分布,和为1
softmax = nn.Softmax(dim=1)
probs = softmax(torch.randn(1, 10)) # [0.05, 0.12, ..., 0.08] 和=1

# Sigmoid:每个输出独立判断
sigmoid = nn.Sigmoid()
probs = sigmoid(torch.randn(1, 10)) # [0.3, 0.7, ..., 0.2] 各自独立

7.2 SGD vs Adam

优化器 特点 学习率 适用场景
SGD 简单粗暴 需手动调 简单任务、理论研究
Adam 自适应学习率 0.001(黄金值) 99%的深度学习任务
1
2
3
4
5
# SGD:需要仔细调lr和momentum
optimizer_sgd = optim.SGD(model.parameters(), lr=0.01, momentum=0.9)

# Adam:开箱即用
optimizer_adam = optim.Adam(model.parameters(), lr=0.001)

八、PyTorch张量操作总结

8.1 创建张量

1
2
3
4
5
6
7
8
9
10
11
12
13
# 创建全0/全1/随机张量
a = torch.zeros(2, 3)
b = torch.ones(2, 3)
c = torch.randn(2, 3)

# 从NumPy转换
import numpy as np
numpy_array = np.array([[1, 2], [3, 4]])
tensor = torch.from_numpy(numpy_array)

# 指定设备
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
d = torch.randn(2, 3, device=device)

8.2 张量运算

1
2
3
4
5
6
7
8
9
10
11
a = torch.randn(2, 3)
b = torch.randn(2, 3)

# 逐元素运算
print(a * b) # 逐元素乘
print(a + b) # 逐元素加

# 聚合操作
print(a.sum()) # 所有元素求和
print(a.max()) # 最大值
print(a[1, 2]) # 索引第2行第3列

8.3 设备转移(CPU ↔ GPU)

1
2
3
4
5
6
7
8
9
10
# 创建在CPU的张量
cpu_tensor = torch.randn(2, 3, 4, 4)

# 搬到GPU
if torch.cuda.is_available():
gpu_tensor = cpu_tensor.to('cuda')
result = gpu_tensor + 100

# 搬回CPU(画图/NumPy转换需要)
back_to_cpu = result.to('cpu')

重要:形状 [2, 3, 4, 4] 表示:

  • 2张图片
  • 每张3个通道(RGB)
  • 分辨率4×4(16个像素)

8.4 梯度相关操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 创建需要梯度的张量
x = torch.tensor([1.0], requires_grad=True)

# 前向计算
y = x * 2

# 反向传播
y.backward()
print(x.grad) # tensor([2.])

# 更新参数时禁用梯度追踪
with torch.no_grad():
x -= x.grad * 0.1
x.grad.zero_() # 清零梯度

8.5 数据加载(DataLoader)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from torch.utils.data import Dataset, DataLoader

class SubwayDataset(Dataset):
def __init__(self, num_samples=1000):
self.x = torch.randn(num_samples, 784)
self.y = torch.randint(0, 2, (num_samples,))

def __len__(self):
return len(self.x)

def __getitem__(self, idx):
return self.x[idx], self.y[idx]

# 使用DataLoader批量加载
train_dataset = SubwayDataset(1000)
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)

for batch_x, batch_y in train_loader:
# 自动分批,每次拿32个样本
pass

九、完整训练流程速查表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 1. 定义模型
model = ObstacleNet().cuda()

# 2. 定义损失和优化器
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

# 3. 训练循环
for epoch in range(num_epochs):
for inputs, targets in train_loader:
# 3.1 梯度清零
optimizer.zero_grad()

# 3.2 前向传播
outputs = model(inputs)

# 3.3 计算损失
loss = criterion(outputs, targets)

# 3.4 反向传播(计算梯度)
loss.backward()

# 3.5 更新参数
optimizer.step()

print(f"Epoch {epoch}, Loss: {loss.item()}")

# 4. 预测时关闭梯度
model.eval() # 切换到评估模式
with torch.no_grad():
predictions = model(test_inputs)

为什么要清零梯度? PyTorch默认梯度累加,不清零会导致梯度爆炸!


十、常见操作对比表

操作 代码 说明
创建中间层 self.layer1 = nn.Linear(784, 256) 784输入 → 256输出
梯度清零 optimizer.zero_grad() 每次训练前必须!
前向传播 outputs = model(inputs) 等价于 model.forward(inputs)
计算损失 loss = criterion(outputs, targets) 分类用CrossEntropyLoss
反向传播 loss.backward() 自动计算所有梯度
更新参数 optimizer.step() 根据梯度更新W和b
禁用梯度 with torch.no_grad(): 预测或参数更新时用
清空梯度 x.grad.zero_() 手动清零(优化器会自动)
切换设备 tensor.to('cuda') CPU ↔ GPU
评估模式 model.eval() 关闭Dropout等训练特性