一、实验内容
1、pytorch 基础练习
首先,根据教程创建好 Colab 环境,每次使用时都要先挂载 Google Drive,从而访问 Google Drive 中的文件。
from google.colab import drive drive.mount('/content/drive/')(1)定义数据
一般定义数据使用 torch.Tensor,tensor的意思是张量,是数字各种形式的总称
import torch # 可以是一个数 x = torch.tensor(666) print(x)import torch # 可以是一维数组(向量) x = torch.tensor([1, 2, 3, 4, 5, 6]) print(x)import torch # 可以是二维数组(矩阵) x = torch.ones(2, 3) print(x)import torch # 可以是任意维度的数组(张量) x = torch.ones(2,3,4) print(x)Tensor支持各种各样类型的数据,包括:torch.float32,torch.float64, torch.float16, torch.uint8,torch.int8,torch.int16,torch.int32,torch.int64 。
创建Tensor有多种方法,包括:ones,zeros,eye,arange,linspace,rand,randn, normal,uniform,randperm,下面主要通过代码展示。
import torch # 创建一个空张量 x = torch.empty(5,3) print(x)import torch #创建一个随机初始化的张量 x = torch.rand(5,3) print(x)import torch #创建一个全0的张量,里面的数据类型为long x = torch.zeros(5,3,dtype=torch.long) print(x)import torch #基于现有的tensor,创建一个新的tensor #从而可以利用原有的tensor的的type,device,size之类的属性信息 x = torch.zeros(5,3,dtype=torch.long) y = x.new_ones(5,3) print(y) #利用原来的tensor的大小,但是重新定义了dtype z = torch.randn_like(x,dtype=torch.float) print(z)(2)定义操作
计算操作:
基本运算,加减乘除,求幂求余
布尔运算,大于小于,最大最小
线性运算,矩阵乘法,求模,求行列式
基本运算:abs /sqrt /div /exp /fmod /pow ,及一些三角函数 cos /sin /asin /atan2 /cosh,及 ceil /round /floor /trunc 等
布尔运算:gt /lt /ge /le /eq /ne,topk, sort, max /min
线性运算:trace, diag, mm /bmm,t,dot/cross,inverse,svd 等
下面为具体操作案例:
import torch #创建一个2*4的tensor m = torch.Tensor([[2,5,3,7], [4,2,1,9]]) print(m.size(0),m.size(1),m.size(),sep=' - ')size()用于获取张量的形状。m.size(0)表示第 0 维的大小,m.size()返回完整的形状信息
import torch m = torch.tensor([[2,5,3,7], [4,2,1,9]]) # 获取张量中元素的总数量 print(m.numel()) # 获取第 0 行、第 2 列的元素 print(m[0][2]) # 获取第 1 行的全部元素 print(m[1, :]) # 获取第 0 列的全部元素 print(m[:, 0])import torch #Create tensor of numbers from 1 to 5 #注意这里结果是1到4,没有5 v = torch.arange(1,5) print(v)import torch v = torch.arange(1,5,,dtype=torch.float32) m = torch.Tensor([[2,5,3,7], [4,2,1,9]]) # Scalar product # m 和 v 的类型一样才可以使用 @,否则会报错 m @ v# Calculated by 1*2 + 2*5 + 3*3 + 4*7 m[[0],:] @ v# Add a random tensor of size 2*4 to m m + torch.rand(2,4)#转置,由2*4变为4*2 print(m.t()) #使用transpose也可以达到相同效果 print(m.transpose(0,1))import torch #returns a 1D tensor of steps equally spaced points between start=3 end=8 and steps=20 #返回一个一维向量,从3开始,8结束,共20个 torch.linspace(3,8,20)from matplotlib import pyplot as plt # matlabplotlib只能显示numpy类型的数据,下面展示了转换数据类型,然后显示 # 注意randn是生成均值为0,方差为1的随机数 # 下面是生成1000个随机数,并按照100个bin统计直方图 plt.hist(torch.randn(1000).numpy(),100)# 当数据非常多的时候,正态分布会体现的非常明显 plt.hist(torch.randn(10**6).numpy(),100)import torch # 创建两个 1*4 的tensor a = torch.Tensor([[1,2,3,4]]) b = torch.Tensor([[5,6,7,8]]) # 在0方向拼接(即在Y方向上拼接),会得到 2*4 的矩阵 print(torch.cat((a,b),0)) # 在1方向拼接(即在X方向上拼接),会得到 1*8 的矩阵 print(torch.cat((a,b),1))torch.cat()用于沿指定维度拼接多个张量。并且参与拼接的张量除拼接维度外,其余维度的大小必须一致。
2、螺旋数据分类
(1)运行代码,下载绘图函数到本地:
!wget https://raw.githubusercontent.com/Atcold/NYU-DLSP21/refs/heads/master/res/plot_lib.py(2)引入基本的库,然后初始化重要参数
import random import torch from torch import nn, optim import math from IPython import display from plot_lib import plot_data, plot_model, set_default # 因为colab是支持GPU的,torch 将在 GPU 上运行 device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") print('device: ', device) # 初始化随机数种子。神经网络的参数都是随机初始化的, # 不同的初始化参数往往会导致不同的结果,当得到比较好的结果时我们通常希望这个结果是可以复现的, # 因此,在pytorch中,通过设置随机数种子也可以达到这个目的 seed = 12345 random.seed(seed) torch.manual_seed(seed) N = 1000 # 每类样本的数量 D = 2 # 每个样本的特征维度 C = 3 # 样本的类别 H = 100 # 神经网络里隐层单元的数量(3)初始化 X 和 Y
X 可以理解为特征矩阵,Y可以理解为样本标签。
结合代码可以看到,X的为一个 NxC 行, D 列的矩阵。C 类样本,每类样本是 N个,所以是 N*C 行。每个样本的特征维度是2,所以是 2列。
在 python 中,调用 zeros 类似的函数,第一个参数是 y方向的,即矩阵的行;第二个参数是 x方向的,即矩阵的列,大家得注意下,不要搞反了。下面结合代码看看 3000个样本的特征是如何初始化的。
X = torch.zeros(N * C, D).to(device) Y = torch.zeros(N * C, dtype=torch.long).to(device) for c in range(C): index = 0 t = torch.linspace(0, 1, N) # 在[0,1]间均匀的取10000个数,赋给t # 下面的代码不用理解太多,总之是根据公式计算出三类样本(可以构成螺旋形) # torch.randn(N) 是得到 N 个均值为0,方差为 1 的一组随机数,注意要和 rand 区分开 inner_var = torch.linspace( (2*math.pi/C)*c, (2*math.pi/C)*(2+c), N) + torch.randn(N) * 0.2 # 每个样本的(x,y)坐标都保存在 X 里 # Y 里存储的是样本的类别,分别为 [0, 1, 2] for ix in range(N * c, N * (c + 1)): X[ix] = t[index] * torch.FloatTensor((math.sin(inner_var[index]), math.cos(inner_var[index]))) Y[ix] = c index += 1 print("Shapes:") print("X:", X.size()) print("Y:", Y.size())# visualise the data plot_data(X,Y)(4)构建线性模型分类
learning_rate = 1e-3 lambda_l2 = 1e-5 # nn包用来创建线性模型 # 每一个线性模型都包含weight和bias model = nn.Sequential( nn.Linear(D, H), nn.Linear(H, C) ) #把模型放到GPU上 model.to(device) # nn包含多种不同的损失函数,这里使用的是交叉(cross entropy loss)损失函数 criterion = torch.nn.CrossEntropyLoss() #这里使用optim包进行随机梯度下降(stochastic gradient descent)优化 optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate, weight_decay=lambda_l2) #开始训练 for t in range(1000): #把数据输入模型,得到预测结果 y_pred = model(X) #计算损失和准确率 loss = criterion(y_pred,Y) score,predicted = torch.max(y_pred,1) acc = (Y == predicted).sum().float() / len(Y) print('[EPOCH]:%i, [LOSS]:%.6f, [ACCURACY]:%.3f' % (t, loss.item(), acc)) display.clear_output(wait=True) # 反向传播前把梯度置0 optimizer.zero_grad() # 反向传播优化 loss.backward() # 更新全部参数 optimizer.step()使用 print(y_pred.shape) 可以看到模型的预测结果,为[3000, 3]的矩阵。每个样本的预测结果为3个,保存在 y_pred 的一行里。值最大的一个,即为预测该样本属于的类别
score, predicted = torch.max(y_pred, 1) 是沿着第二个方向(即X方向)提取最大值。最大的那个值存在 score 中,所在的位置(即第几列的最大)保存在 predicted 中。
交叉熵损失函数用于衡量模型输出与真实类别之间的差异。随机梯度下降(SGD)优化器则根据反向传播计算得到的梯度更新模型参数。
由于 PyTorch 在每次反向传播时会把梯度累加到上一次的梯度上,不会自动清零。所以在每次反向传播前都要把梯度置0(即optimizer.zero_grad()),这样可以保证每次更新都只用当前批次的梯度。
下面代码把第10行的情况输出,供解释说明。
print(y_pred.shape) print(y_pred[10,:]) print(score[10]) print(predicted[10])# Plot trained model print(model) plot_model(X,Y,model)上面使用 print(model) 把模型输出,可以看到有两层:
第一层输入为 2(因为特征维度为主2),输出为 100;
第二层输入为 100 (上一层的输出),输出为 3(类别数)
该模型包含两层线性变换,但由于两层之间没有加入非线性激活函数,因此两个线性变换的组合仍然等价于一个线性变换。
而且从上面图示可以看出,线性模型的准确率最高只能达到 50% 左右,对于这样复杂的一个数据分布,线性模型难以实现准确分类。
(5)构建两层神经网络分类
learning_rate = 1e-3 lambda_l2 = 1e-5 # 这里可以看到,和上面模型不同的是,在两层之间加入了一个 ReLU 激活函数 model = nn.Sequential( nn.Linear(D, H), nn.ReLU(), nn.Linear(H, C) ) model.to(device) # 下面的代码和之前是完全一样的,这里不过多叙述 criterion = torch.nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate, weight_decay=lambda_l2) # built-in L2# 训练模型,和之前的代码是完全一样的 for t in range(1000): y_pred = model(X) loss = criterion(y_pred, Y) score, predicted = torch.max(y_pred, 1) acc = ((Y == predicted).sum().float() / len(Y)) print("[EPOCH]: %i, [LOSS]: %.6f, [ACCURACY]: %.3f" % (t, loss.item(), acc)) display.clear_output(wait=True) # zero the gradients before running the backward pass. optimizer.zero_grad() # Backward pass to compute the gradient loss.backward() # Update params optimizer.step()# Plot trained modelprint(model) plot_model(X, Y, model)加入 ReLU 激活函数以后,分类的准确率得到了显著提高。
说明面对具有复杂非线性结构的数据,仅使用线性变换往往难以获得理想的分类效果。通过在隐藏层中加入非线性激活函数,模型的表达能力得到增强,更容易学习螺旋形数据对应的复杂决策边界。
二、问题总结与体会
思考题
1、AlexNet有哪些特点?为什么可以比 LeNet 取得更好的性能?
特点:
网络结构更深、规模更大:相比 LeNet,AlexNet 拥有更多的卷积层和全连接层,能够提取更复杂的图像特征。
使用 ReLU 激活函数:相较于 Sigmoid 等传统激活函数,ReLU 能够缓解梯度消失问题,加快训练速度。
使用 Dropout:在训练过程中随机屏蔽部分神经元,降低神经元之间的过度依赖,缓解过拟合。
使用重叠最大池化:池化窗口大于步长,有助于提取显著特征。
利用 GPU 加速训练:能够处理更大规模的网络和数据。
使用数据增强:通过随机裁剪、翻转等方式扩充训练数据,提高模型的泛化能力。
AlexNet 比 LeNet 性能更好的主要原因是:它具有更强的特征提取能力,采用了 ReLU、Dropout 和数据增强等技术,并借助 GPU 提高了训练效率,因此能够更有效地处理复杂的大规模图像分类任务。
2、激活函数有哪些作⽤?
激活函数的主要作用是为神经网络引入非线性能力。如果网络中只有线性变换,那么无论叠加多少层,整体仍然可以表示为一个线性变换,难以学习复杂的数据关系。
激活函数不仅影响网络的表达能力,也会影响梯度传播和训练效率。
3、梯度消失现象是什么?
梯度消失是指在神经网络反向传播过程中,梯度随着层数增加而逐渐变小,甚至接近于 0的现象。
在反向传播中,梯度需要经过多层求导结果的连乘。如果激活函数的导数较小,多个较小的数相乘后就可能使梯度非常微弱,导致靠近输入端的网络层参数更新缓慢,难以有效学习特征。
4、神经网络是更宽好还是更深好?
神经网络的宽度指每层神经元的数量,深度指网络的层数。更宽和更深各有优势,不能简单地认为其中一种一定更好。
更宽的网络:单层可以容纳更多神经元,增强特征表示能力,但参数量和计算开销可能增加。
更深的网络:能够逐层组合特征,从简单特征逐步学习更抽象、复杂的特征,通常具有较强的特征表达能力,但训练难度也可能更高。
网络过深或过宽都可能增加计算成本和过拟合风险。因此,应根据任务复杂度、训练数据量、计算资源和模型效果选择合适的深度与宽度,而不是一味增加层数或神经元数量。
5、为什么要使⽤Softmax?
Softmax 常用于多分类神经网络的输出层,主要作用是将模型输出的原始得分(Logits)转换为一个概率分布。
转换为概率分布:每个类别的输出都在 0 到 1 之间,且所有类别的概率之和为 1。
便于进行多分类预测:通常选择概率最大的类别作为预测结果。
配合交叉熵损失函数训练模型:衡量预测结果与真实标签之间的差异,引导模型调整参数。
6、SGD 和 Adam 哪个更有效?
SGD 和 Adam 都是常见的梯度优化算法,各有优缺点,没有一种算法在所有任务中都一定更有效。
| 比较方面 | SGD | Adam |
|---|---|---|
| 基本原理 | 根据梯度更新参数 | 结合梯度的一阶矩和二阶矩估计自适应调整学习率 |
| 学习率 | 通常需要仔细调整 | 每个参数具有自适应的更新步长 |
| 收敛速度 | 可能较慢,对学习率较敏感 | 通常前期收敛较快 |
| 计算与存储 | 开销相对较小 | 需要额外存储梯度的统计量 |
| 泛化表现 | 在部分任务中可能获得更好的泛化效果 | 训练初期通常较方便,但最终泛化效果依赖任务和设置 |
如果希望训练过程更方便、较快地取得初步结果,Adam 通常是不错的选择。
如果追求特定任务上的泛化性能,并且能够调整学习率和训练策略,SGD 也可能取得更好的结果。
收获和体会
通过本次实验,我学习了 PyTorch 中张量的创建、索引、拼接、转置和矩阵乘法等基本操作,也熟悉了神经网络从数据构建、模型定义、损失计算到反向传播训练的完整流程,对神经网络的实际运行过程有了更具体的体会。
在螺旋数据分类实验中,线性模型的准确率只有 50% 左右,而加入 ReLU 激活函数后,分类效果显著提升。通过对比可以发现,线性模型虽然结构简单,但由于层与层之间没有非线性激活函数,最终仍然等价于一个线性变换,因此面对螺旋形这种复杂数据分布时,准确率只能停留在较低水平。而在加入 ReLU 激活函数后,模型的分类准确率明显提高,决策边界也能够更好地适应数据的非线性结构。这让我深刻体会到激活函数在神经网络中的关键作用:它不仅增强了模型的表达能力,也使神经网络能够拟合更加复杂的函数关系。
此外,通过思考题,我对 AlexNet、激活函数、梯度消失、网络宽度与深度、Softmax 以及 SGD 与 Adam 的区别有了更系统的认识。
总的来说,这次实验让我对神经网络的结构设计、优化方法及训练细节都有了更清晰的认识,为后续更深入的深度学习课程与项目打下了坚实基础。