PyTorch深度学习Day03:神经网络基础与实战

前言

本文将详细介绍PyTorch中神经网络的构建、激活函数的使用、参数初始化以及自定义神经网络的实现。

1. 广播机制

在深度学习中,我们经常需要对不同形状的张量进行运算。NumPy和PyTorch都支持广播机制,让我们先了解一下这个概念。

1.1 广播机制原理

广播机制允许不同形状的数组进行运算,系统会自动将较小的数组扩展到与较大数组相同的形状。

import numpy as np

def test01():
    a = np.array([0,1,2])
    print("a-->",a.shape)
    b = 2  # [2,2,2] (3,)
    print("a+b-->",a+b) # 可以相加
    # 解释:b会自动扩展形状为(3,)的数组

def test02():
    # 案例一
    a = np.array([[1,2,3],[4,5,6]])
    b = np.array([1,1,1])
    print("a-->\n",a,a.shape)
    print("b-->",b,b.shape)
    print("a+b-->",a+b)
    # 解释:b.shape在左边补1 ->(1,3),然后1进行复制匹配(2,3)

def test04():
    # 案例三
    a = np.array([1, 2, 3, 4])
    b = np.array([[1, 2, 3], [2, 3, 4], [3, 4, 5], [4, 5, 6]])
    print('a-->\n', a, a.shape)  # (4,)
    print('b-->', b, b.shape)  # (4, 3)
    print('a+b-->', a + b)  # 不可以相加
    # 不满足广播条件。

广播规则:

  1. 从右往左比较维度
  2. 如果维度大小相等或其中一个为1,则可以广播
  3. 如果维度缺失,则在该维度前补1

2. 线性回归实战案例

让我们通过一个完整的线性回归案例来理解PyTorch神经网络的构建和训练过程。

2.1 构建数据集

import matplotlib
import torch
from sklearn.datasets import make_regression
import matplotlib.pyplot as plt
from torch import nn, optim
from torch.utils.data import TensorDataset, DataLoader

# 过滤警告
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning, message=".*__array_wrap__.*")
plt.rcParams["font.sans-serif"] = ["SimHei"]  # 设置字体为黑体
plt.rcParams["axes.unicode_minus"] = False  # 正常显示负号
matplotlib.use("TkAgg")

def get_dataset():
    x, y, coef = make_regression(n_samples=100,
                                 n_features=1,
                                 bias=2,
                                 noise=10,
                                 coef=True,
                                 random_state=22)

    # 数值张量化
    tensor_x = torch.tensor(x, dtype=torch.float)
    tensor_y = torch.tensor(y, dtype=torch.float)
    print("tensor_x-->", tensor_x)
    print("tensor_y-->", tensor_y)
    return tensor_x, tensor_y, coef

2.2 数据可视化

def show_data():
    x, y, coef = get_dataset()
    # 画图:y = kx + b(k = coef,b=bias)
    plt.scatter(x, y)
    # 要么把tensor转换为numpy,要么把tensor前置
    plt.plot(x.numpy(), coef * x.numpy() + 2)
    plt.show()

2.3 构建模型

def make_model():
    # 模型
    model = nn.Linear(in_features=1, out_features=1)
    
    # 损失函数
    criterion = nn.MSELoss()
    
    # 优化器
    optimizer = optim.SGD(params=model.parameters(), lr=0.01)
    
    return model, criterion, optimizer

2.4 模型训练

def train_model():
    # 得到数据
    x, y, coef = get_dataset()
    x1, y1 = x, y
    my_dataset = TensorDataset(x, y)
    
    # 创建数据加载器
    my_dataloader = DataLoader(dataset=my_dataset, batch_size=8, shuffle=True)
    
    # 构建模型
    model, criterion, optimizer = make_model()
    
    # 准备轮次
    epochs = 100
    
    # 定义额外的参数,用于记录日志和画图
    epoch_loss = []             # 记录每一轮次的损失,用于画图
    total_loss = 0.0            # 总损失值
    total_sample = 0.0          # 总样本数
    
    # 模型训练
    for epoch in range(epochs):
        for x, y in my_dataloader:
            # 在进行损失函数计算时,最好保证预测结果和真实结果的shape是一样的
            y = y.reshape(-1, 1)
            
            # 给模型喂数据
            y_pred = model(x)
            
            # 计算损失
            loss = criterion(y_pred, y)
            
            # 梯度清零
            optimizer.zero_grad()
            # 反向传播
            loss.backward()
            # 梯度更新
            optimizer.step()
            
            # 其他损失相关的计算操作
            total_loss += loss.item()
            total_sample += len(y)
        
        # 记录每一轮的平均损失,用于画图
        epoch_loss.append(total_loss / total_sample)
        print("当前轮次:", epoch + 1, "当前平均损失:", total_loss / total_sample)
    
    # 绘制损失曲线图
    plt.plot(range(epochs), epoch_loss)
    plt.grid()
    plt.title("loss change line")
    plt.show()
    
    # 绘制拟合曲线图
    plt.scatter(x1, y1)
    # 绘制真实拟合曲线
    plt.plot(x1, x1 * coef + 2, label="true")
    # 绘制模型预测的拟合曲线
    # detach():因为模型参数是挂在计算流图上,需要使用时要使用detach()剥离出来
    plt.plot(x1, x1 * model.weight.detach().numpy() + model.bias.detach().numpy(), label="train")
    plt.grid()
    plt.title("train line")
    plt.show()

3. 激活函数详解

激活函数是神经网络中非常重要的组成部分,它决定了神经元的输出。让我们详细学习几种常用的激活函数。

3.1 Sigmoid激活函数

Sigmoid函数将输入压缩到(0,1)区间,常用于二分类问题。

def dm01_sigmoid():
    # 创建画布, 1行2列
    fig, axes = plt.subplots(1, 2)
    
    # 先画第一张图,sigmoid函数值域
    x = torch.linspace(-20, 20, 1000)
    y = torch.sigmoid(x)
    axes[0].plot(x, y)
    axes[0].grid()
    
    # 再画第二张图,sigmoid函数导数
    # 设置x可以求导
    x = torch.linspace(-20, 20, 1000, requires_grad=True)
    # .backward()创建梯度只能对标量使用,如果不是会报错
    torch.sigmoid(x).sum().backward()
    
    # 上面的报错是由于x被挂在了计算流图中,需要使用detach()取出
    axes[1].plot(x.detach(), x.grad)
    axes[1].grid()
    plt.show()

Sigmoid特点:

  • 输出范围:(0, 1)
  • 平滑可导
  • 容易产生梯度消失问题

3.2 Tanh激活函数

Tanh函数将输入压缩到(-1,1)区间,是Sigmoid的改进版本。

def dm02_tanh():
    fig, axes = plt.subplots(1, 2)
    
    # 先画第一张图,tanh函数值域
    x = torch.linspace(-20, 20, 1000)
    y = torch.tanh(x)
    axes[0].plot(x, y)
    axes[0].grid()
    
    # 再画第二张图,tanh函数导数
    x = torch.linspace(-20, 20, 1000, requires_grad=True)
    torch.tanh(x).sum().backward()
    
    axes[1].plot(x.detach(), x.grad)
    axes[1].grid()
    plt.show()

Tanh特点:

  • 输出范围:(-1, 1)
  • 零中心化
  • 梯度消失问题比Sigmoid轻

3.3 ReLU激活函数

ReLU是目前最常用的激活函数,计算简单且能有效缓解梯度消失问题。

def dm03_relu():
    fig, axes = plt.subplots(1, 2)
    
    # 先画第一张图,relu函数值域
    x = torch.linspace(-20, 20, 1000)
    y = torch.relu(x)
    axes[0].plot(x, y)
    axes[0].grid()
    
    # 再画第二张图,relu函数导数
    x = torch.linspace(-20, 20, 1000, requires_grad=True)
    torch.relu(x).sum().backward()
    
    axes[1].plot(x.detach(), x.grad)
    axes[1].grid()
    plt.show()

ReLU特点:

  • 计算简单,收敛快
  • 解决梯度消失问题
  • 可能产生"死神经元"问题

3.4 Softmax激活函数

Softmax函数将输入转换为概率分布,常用于多分类问题的输出层。

def dm04_softmax():
    # 准备logits/score
    scores = torch.tensor([[[0.2,0.02,0.15,0.15,1.3,0.5,0.06,1.1,0.05,3.75],
                           [0.3,0.02,0.15,0.15,1.3,0.5,0.06,1.1,0.05,3.75]],
                          [[0.2,0.02,0.15,0.15,1.3,0.5,0.06,1.1,0.05,3.75],
                           [0.3,0.02,0.15,0.15,1.3,0.5,0.06,1.1,0.05,3.75]]])
    
    # 让logits的结果经过softmax
    result = torch.softmax(scores, dim=1)
    print("result-->", result)
    
    # 结论:在计算softmax的时候,我们一般指定dim=-1

Softmax特点:

  • 输出和为1的概率分布
  • 常用于多分类输出层
  • 数值稳定性需要注意

4. 参数初始化

参数初始化对神经网络的训练效果有重要影响。好的初始化方法可以加速收敛并提高模型性能。

def dm01_initial():
    # 创建线性层
    linear = nn.Linear(5, 3)
    
    # 1.指定均匀初始化
    # nn.init.uniform_(linear.weight)
    
    # 2.指定正态分布初始化
    nn.init.normal_(linear.weight)
    
    # 3.指定全0初始化
    nn.init.zeros_(linear.weight)
    
    # 4.指定全1初始化
    nn.init.ones_(linear.weight)
    
    # 5.指定固定值初始化
    nn.init.constant_(linear.weight, 10)
    
    # 6.kaiming初始化
    # kaiming 均匀分布
    nn.init.kaiming_uniform_(linear.weight)
    # kaiming 正态分布
    nn.init.kaiming_normal_(linear.weight)
    
    # 7.xavier初始化
    # xavier均匀分布
    nn.init.xavier_uniform_(linear.weight)
    # xavier 正态分布
    nn.init.xavier_normal_(linear.weight)
    
    # 结论: 偏置一般初始化为0,权重要么选择kaiming初始化,要么选择xavier初始化
    print("linear.weight-->", linear.weight)

初始化方法选择:

  • 偏置:一般初始化为0
  • 权重:推荐使用Kaiming或Xavier初始化
  • Kaiming:适用于ReLU激活函数
  • Xavier:适用于Sigmoid和Tanh激活函数

5. 自定义神经网络

在实际项目中,我们经常需要自定义神经网络结构。PyTorch提供了灵活的模块化设计。

5.1 自定义神经网络步骤

# 自定义神经网络步骤:
# 步骤一:继承nn.Module类
# 步骤二:实现2个方法:__init__()和forward()

5.2 完整实现

import torch
import torch.nn as nn
from torchsummary import summary

class MyModel(nn.Module):
    # init方法
    def __init__(self):
        super().__init__()
        
        # 定义第一层隐藏层
        self.linear1 = nn.Linear(in_features=3, out_features=3)
        nn.init.xavier_uniform_(self.linear1.weight)
        nn.init.ones_(self.linear1.bias)
        
        # 定义第二层隐藏层
        self.linear2 = nn.Linear(in_features=3, out_features=2)
        nn.init.kaiming_normal_(self.linear2.weight)
        nn.init.ones_(self.linear2.bias)
        
        # 定义输出层
        self.out = nn.Linear(in_features=2, out_features=2)
    
    # forward方法,方法名是固定的,不能写错
    def forward(self, x):
        # 数据经过第一个线性层
        x = self.linear1(x)
        
        # 使用sigmoid激活函数
        x = torch.sigmoid(x)
        
        # 数据经过第二个线性层
        x = self.linear2(x)
        
        # 使用relu激活函数
        x = torch.relu(x)
        
        # 数据经过输出层
        x = self.out(x)
        
        # 使用softmax激活函数,让输出层的结果经过softmax
        x = torch.softmax(x, dim=-1)
        
        return x

def train_model():
    # 创建神经网络模型对象
    model = MyModel()
    print("model-->", model)
    
    # 准备数据
    input = torch.randn(3, 3)  # 第一个是样本数,第二个是特征数量
    print("input-->", input.shape)
    
    # 给模型喂数据:模型对象名()
    # 当我们给模型喂数据的时候,PyTorch框架会自动调用自定义模型的forward方法
    output = model(input)
    print("output-->", output.shape)
    
    # 统计模型参数
    summary(model=model, input_size=(3,))

5.3 模型结构分析

使用torchsummary可以查看模型的详细结构:

# 安装torchsummary
# pip install torchsummary

# 查看模型结构
summary(model=model, input_size=(3,))

6. 完整训练流程总结

通过今天的学习,我们掌握了PyTorch神经网络的完整构建和训练流程:

  1. 数据准备:使用TensorDatasetDataLoader处理数据
  2. 模型构建:继承nn.Module类自定义网络结构
  3. 激活函数:选择合适的激活函数
  4. 参数初始化:使用合适的初始化方法
  5. 损失函数:选择合适的损失函数
  6. 优化器:选择合适的优化算法
  7. 训练循环:前向传播、计算损失、反向传播、参数更新

7. 关键知识点总结

7.1 重要概念

  • 广播机制:不同形状张量的运算规则
  • 激活函数:Sigmoid、Tanh、ReLU、Softmax的特点和适用场景
  • 参数初始化:Kaiming和Xavier初始化的选择
  • 自定义网络:继承nn.Module的基本步骤

7.2 编程技巧

  • 使用detach()方法从计算图中分离张量
  • 使用requires_grad=True启用梯度计算
  • 使用zero_grad()清零梯度
  • 使用summary()查看模型结构

7.3 常见问题

  • 梯度消失问题:选择合适的激活函数和初始化方法
  • 形状不匹配:注意张量的维度对应关系
  • 内存管理:及时使用detach()释放不需要的梯度信息

参考资料

  • PyTorch官方文档
  • 深度学习课程讲义
  • 相关技术博客和论文

版权声明:本文仅供学习交流使用,转载请注明出处。

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐