pytorch框架 学习笔记
张量Tensors
概念:可以理解为是一个多维的数组,和Numpy的多维数组挺像的,但是张量可以在GPU上或者说硬件加速器上运行
1.tensor的创建
- 列表中创建张量
a = [1,2,3.] #type(a)的结果是list
b = torch.tensor(a) #b的类型就是torch.Tensor
- 从numpy数组中创建张量
a = np.random.normal((2,3)) #生成一个正态分布的数组array
torch.tensor(a) #变成tensor
- 从另一个张量中初始化一个新的张量
b #比如b是一个tensor([1.84,1.66])
c = torch.ones_like(b) #c就变成了tensor([1.,1.]),如果是rand_like(b),那就是随机数的样子
- 根据形状生成tensor
torch.rand([1,2]) #一行两列,元素内容是随机的
torch.rand((1,2)) #和上面的含义一样
#或者直接定义tensor里面的内容
a = torch.tensor([1,2],[2,3]) #2*2的张量
2.tensor的API
属性
tensor的一些属性:shape,dtype,device(在哪个设备上CPU or GPU)
range和arange
- torch.range(start,end,step),是[start,end],也可以只写一个数字,就是从0开始range(start=0,end=3)
- torch.arange()比上面的range短一个单位
eye生成矩阵
- torch.eye(3),生成一个三阶方阵,对角线全是1
full填满元素
- torch.full(size,fill_value),size规定几行几列,fill_size说明矩阵的元素值是多少
cat连接
- torch.cat(tensors),连接张量
a = torch.rand([2,2])
b = torch.rand([2,3])
torch.cat([a,b],dim=1) #意思是把两个张量的列拼在一起,因为他们只有行数是一样的
chunk和spilt拆分张量
- torch.chunk(tensor,拆分个数),拆分张量
b = torch.rand([3.2])
c,d = torch.chunk(b,chunks=2,dim=0) #c是2行2列,d是1行2列,因为3行没法整除2,默认是dim=0,沿着行分割
- torch.spilt(),可以不用均分,不像chunk只能均分,spilt可以自定义划分大小
a = torch.arange(10).reshape(5,2)
torch.split(a, 2) #以2为单位划分
'''
(tensor([[0, 1],
[2, 3]]),
tensor([[4, 5],
[6, 7]]),
tensor([[8, 9]]))
'''
torch.split(a, [1,4])#以列表中的元素去划分,默认dim=0就是按行划分
'''
(tensor([[0, 1]]),
tensor([[2, 3],
[4, 5],
[6, 7],
[8, 9]]))
'''
squeeze和unsqueeze维度变化
- torch.squeeze(input,dim=1),就是把大小维度为1的那些向量移除掉,比如输入的是A * 1 * B,那么最后就是A×B,dim就是指定消除为1的维数
- torch.unsqueeze(input,dim),就是根据dim的数值对input的这个向量,的dim位置增加一个维度
#比如a的维度就是[2,3]
torch.unsqueeze(a,dim=0).shape #torch.size([1,2,3])
torch.unsqueeze(a,dim=1).shape #torch.size([2,1,3])
torch.unsqueeze(a,dim=-1).shape #torch.size([2,3,1]),-1就是在最后一个维度加
take寻找元素
- torch.take(input,index),就是根据index去input这个张量里寻找元素,在寻找的时候会相当于把input这个张量铺平,铺成一维的再去寻找
src = torch.tensor([[4, 3, 5],
[6, 7, 8]])
torch.take(src, torch.tensor([0, 2, 5])) #结果是tensor([ 4, 5, 8])
tile复制张量
- torch.tile(input,dims),复制的作用,dims规定第几维度复制几份
x = torch.tensor([1, 2, 3])
x.tile((2,)) #结果是tensor([1, 2, 3, 1, 2, 3])
y = torch.tensor([[1, 2], [3, 4]])
torch.tile(y, (2, 2))
'''结果是tensor([[1, 2, 1, 2],
[3, 4, 3, 4],
[1, 2, 1, 2],
[3, 4, 3, 4]]),(2,2)指第一维度行复制两份,第二维度列也复制2份'''
transpose维度交换
- torch.transpose(input,dim0,dim1),dim0和dim1就是即将交换的维度
unbind维度拆分
- torch.unbind(),对于某一个维度,拆分成不同的小张量
a = torch.tensor([[1, 2, 3],[4, 5, 6],[7, 8, 9]]) #是一个3×3
torch.unbind(a,dim=0)
#结果是(tensor([1, 2, 3]), tensor([4, 5, 6]), tensor([7, 8, 9]))
#如果dim=1, (tensor([1, 4, 7]), tensor([2, 5, 8]), tensor([3, 6, 9]))
where判断
- torch.where(),有点像是判断语句
随机采样函数
-
还有随机的采样函数:
伯努利函数,normal正态分布函数
randperm(4):传一个上界,适合建立随机的数据集,比如这个就是tensor([2,1,0,3]),且每次都不一样
Datesets & DateLoaders类
1、Datesets 类
概念:用来处理单个训练样本,如何从磁盘中读取训练数据集,包括特征标签等,并且可以做一些简单的变形和数据的预处理,映射出来X和y
预处理:如果想要自定义一个Datesets则需要先去继承官方的Datesets的这个类,必须先实现三个函数,_ init _, _ len _ ,
_ getitem _三个函数。
import os
import pandas as pd
from torchvision.io import read_image
#自定义一个自己的datasets的类
class CustomImageDataset(Dataset):
def __init__(self, annotations_file, img_dir, transform=None, target_transform=None):
self.img_labels = pd.read_csv(annotations_file) #annotations_file是文件目录路径
self.img_dir = img_dir #img_dir存储照片的目录
self.transform = transform
self.target_transform = target_transform
def __len__(self): #返回数据集的大小,
return len(self.img_labels)
def __getitem__(self, idx): #通过idx索引返回一个样本,注意是只有一个样本,一个样本的image还有label
#通过img_labels的第idx行第0列,获得一个文件名称
img_path = os.path.join(self.img_dir, self.img_labels.iloc[idx, 0])
image = read_image(img_path) #将文件中的照片加载到内存中,得到image
label = self.img_labels.iloc[idx, 1]
if self.transform: #可能需要对image进行一系列的变化,比如归一化,对他通道处理等等,就是一些预处理
image = self.transform(image)
if self.target_transform:
label = self.target_transform(label) #如果对label处理,就用target_transform,跟上面含义一样
return image, label
再来个简单点的初始化数据集Datasets的代码
# 创建数据集对象
class text_dataset(Dataset): #需要继承Dataset类
def __init__(self, words, labels):
self.words = words
self.labels = labels
def __len__(self):
return len(self.labels)
def __getitem__(self, idx):
label = self.labels[idx]
word = self.words[idx]
return word, label
2、DateLoaders类
概念:是对于多个样本而言,通过Datasets获得单个训练样本之后,用DateLoaders变成我们随机梯度下降算法所以要的minibatches 的形式,就是小样本批量处理需要的数据。也可以在每个周期之后对数据进行打乱,也可以对数据固定的保存在GPU中,就是对数据进行的一系列操作
from torch.utils.data import DataLoader
train_dataloader = DataLoader(training_data, batch_size=64, shuffle=True)
test_dataloader = DataLoader(test_data, batch_size=64, shuffle=True)
#第一个参数就是Datasets的实例化,batch_size就是步长,
#shuffle就是每个训练周期之后是不是需要对这个数据进行一个打乱,
#Sampler就是采样方式,可以用默认的也可以自己实现一个,如果自定义了一个Sampler就不需要shuffle了
#batch_sampler就是batch级别的采样,想要以多大的batch构成一个minibatch,构建一批批样本
#num_workers就是默认值为0,默认使用主进程加载数据,也可以改成一个更大的数,通常取决于CPU的个数
#pin_memory就是可以把Tensor保存在GPU中,不需要重复保存
#drop_last:如果不是整数倍的,可以把最后剩下的扔掉
3、举个例子
from torch.utils.data import DataLoader
#先进行初始化训练集和测试集
train_dataloader = DataLoader(training_data, batch_size=64, shuffle=True)
test_dataloader = DataLoader(test_data, batch_size=64, shuffle=True)
#开始显示图像和标签
#_ _iter_ _是dataloader里面的一个迭代器,返回一个迭代
#_ _next_ _,里面调用了next_data(),就得到了data,返回了一个minibatch的数据
train_features, train_labels = next(iter(train_dataloader))
print(f"Feature batch shape: {train_features.size()}")
print(f"Labels batch shape: {train_labels.size()}")
img = train_features[0].squeeze()
label = train_labels[0]
plt.imshow(img, cmap="gray")
plt.show()
print(f"Label: {label}")
'''结果展示:
Feature batch shape: torch.Size([64, 1, 28, 28])
Labels batch shape: torch.Size([64]) 返回两个值train_features是图片也就是特征就是X,而train_labels就是y
Label: 5,表示y值是5
'''
4、一个学习链接:
https://zhuanlan.zhihu.com/p/544362848
5 、Transforms
概念:在读取图像之后,有时候在喂入神经网络之前可能还需要对图片的大小,图片的通道,图片的像素值进行一些修整变化,使其满足神经网络的要求。就是Transforms的作用。
Build Model实例
在pytorch中构建一个神经网络的实例,主要关于torch.nn,本质上神经网络上用到的模型都是继承于nn.Module。
1、获取训练用到的设备
device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"Using {device} device") #这两行代码可有可无?
2、初始化模块
通过对nn进行子类继承,并在__ init__中初始化神经网络层。在forward方法中实现对输入数据的操作
class NeuralNetwork(nn.Module):
def __init__(self): #在这里需要定义好各个模块
super(NeuralNetwork, self).__init__() #调用父类的创建模块,也可以这样写super().__init__()
self.flatten = nn.Flatten()
self.linear_relu_stack = nn.Sequential(
nn.Linear(28*28, 512), #线性层,第一个参数是输入特征的特征维度,第二个是隐含层的大小
nn.ReLU(), #非线性的激活函数
nn.Linear(512, 512),
nn.ReLU(),
nn.Linear(512, 10), #最后输出一个10个类别的输出
)
def forward(self, x): #这个其实就是一个前向传播,用起来上面定义好的各个模块
x = self.flatten(x) #对x的维度进行展开铺平
logits = self.linear_relu_stack(x)
return logits
3、初始化模块
model = NeuralNetwork().to(device) #to函数是module中方法,意思就是将各种优化及一系列的附属量都放在上面运算
print(model)
4、使用模块,输入数据
X = torch.rand(1, 28, 28, device=device) #初始化一个数据
logits = model(X) #开始完成前向运算
pred_probab = nn.Softmax(dim=1)(logits) #dim表示对哪一个维度进行计算
y_pred = pred_probab.argmax(1)
print(f"Predicted class: {y_pred}")
5、讲解模块的模型层
input_image = torch.rand(3,28,28)
print(input_image.size()) #torch.Size([3, 28, 28])
#nn.Flatten
flatten = nn.Flatten() #就是将后面的维度,除了第0维,也就是从第一维到最后一维度合并到一起
flat_image = flatten(input_image)
print(flat_image.size()) #torch.Size([3, 784])
#nn.Linear
#in_features就是线性层所接受的输入的特征大小,out_features就是输出的隐含层大小
layer1 = nn.Linear(in_features=28*28, out_features=20)
hidden1 = layer1(flat_image)
print(hidden1.size()) #torch.Size([3, 20])
#nn.Sequential
#这个是一个关于模块的有序的容器
seq_modules = nn.Sequential(
flatten,
layer1,
nn.ReLU(),
nn.Linear(20, 10)
)
input_image = torch.rand(3,28,28)
logits = seq_modules(input_image) #最后size是(3,10),第一个维度是batchsize
#因为我们要做的是分类任务,所以之后还需要传给softmax
softmax = nn.Softmax(dim=1)
pred_probab = softmax(logits) #关于十个类的一个概率分布
Autograd自动微分
概念:训练神经网络的时候怎么使用他的自动微分机制,让他去帮我们算模型中每一个梯度,而我们一般又是基于loss去计算梯度的,pytorch中有一个内置的微分引擎,Autograd。他支持对任意计算图的梯度计算
下图一时正向传播和反向传播的一个图示
图二是一个正向传播的运算分解,各个元操作,在求梯度的公式写好,根据链式法则就可以把所有的节点的梯度写出来
并且注意梯度是具有累计的,不能自动清零,平常需要注意清零 grad.zero_()
1、举个小例子
input x
, parameters w
and b
, and some loss function。一层的神经网络小例子
import torch
x = torch.ones(5) # input tensor(1,5)
y = torch.zeros(3) # expected output
w = torch.randn(5, 3, requires_grad=True) #requires_grad=True表示在做前向运算时,自动微分会自动记录参数的微分公式
b = torch.randn(3, requires_grad=True) #(1,3)
z = torch.matmul(x, w)+b
loss = torch.nn.functional.binary_cross_entropy_with_logits(z, y) #是一个二分类的交叉熵
#开始计算梯度
loss.backward() #需要注意这里的loss必须是一个标量,如果是一个张量,就需要在backward里面加一个和loss的shape一样格式的向量,这时候就会对可以计算的父节点,都会计算出来一个梯度,比如各个参数或者说复合函数中间的函数名字,什么雅克比向量乘积
print(w.grad)
print(b.grad)
总结:写好前向传播,然后最后让loss变成一个标量,就可以调用backward()去求得梯度了
2、不需要计算梯度
分两种情况:
(1)将神经网络中的某些参数标记为冻结参数。这是微调预处理网络的常见场景
(2)当模型训练好之后只需要测试或者推理的时候,只进行正向传递时,为了加快计算速度,因为在不跟踪梯度的张量上进行计算会更有效
#有的时候是不需要计算梯度的,这时候就可以这么处理
z = torch.matmul(x, w)+b
print(z.requires_grad) #true
with torch.no_grad(): #也可以换种方式处理:z_det = z.detach(),下面打印z_det.requires_grad也是false
z = torch.matmul(x, w)+b
print(z.requires_grad) #false
训练模型
为模型喂入一些数据,根据目标函数,不断迭代去调节参数,使得模型的预测值达到最好的效果
1、首先完成准备工作
import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets
from torchvision.transforms import ToTensor
#创建训练集和测试集,如果是自己的数据集,就需要自己按照上面讲述的datasets去创建类,然后实现那三个方法
training_data = datasets.FashionMNIST(
root="data",
train=True,
download=True,
transform=ToTensor()
)
test_data = datasets.FashionMNIST(
root="data",
train=False,
download=True,
transform=ToTensor()
)
#构建好datasets就扔给dataloader
train_dataloader = DataLoader(training_data, batch_size=64)
test_dataloader = DataLoader(test_data, batch_size=64)
#构建模型,一般写模型的时候都需要先继承nn.Module
class NeuralNetwork(nn.Module):
def __init__(self):
super(NeuralNetwork, self).__init__() #首先调用父类方法,初始化方法
self.flatten = nn.Flatten()
self.linear_relu_stack = nn.Sequential(
nn.Linear(28*28, 512),
nn.ReLU(),
nn.Linear(512, 512),
nn.ReLU(),
nn.Linear(512, 10),
)
def forward(self, x):
x = self.flatten(x)
logits = self.linear_relu_stack(x)
return logits
model = NeuralNetwork()
2、初始化一些超参数
概念:不需要去优化,但是会影响训练效果的一些参数
learning_rate = 1e-3
batch_size = 64
epochs = 5
3、设置目标函数
# 如果是分类的话就选择交叉熵函数
# 回归的话nn.MSELoss,nn.NLLLoss,或者 nn.CrossEntropyLoss
loss_fn = nn.CrossEntropyLoss()
4、构建优化器
优化器里面对参数进行更新,训练的时候需要注意:
(1)每一步优化之前都要对优化器中的参数梯度置零: optimizer.zero_grad()
(2)一旦loss是个标量,使用了 loss.backward()
,就可以使用 optimizer.step() 对模型中所有的参数进行更新
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate) #model.parameters()默认优化所有参数
5、完整的实现
def train_loop(dataloader, model, loss_fn, optimizer):
size = len(dataloader.dataset)
for batch, (X, y) in enumerate(dataloader): #枚举,每一次都是得到一个小批次
#enumerate会给每一个(x,y)添加一个序号batch,用来表明训练到了第几组小批次样本
pred = model(X) #这个就是那个logits,通过模型训练出来的结果
loss = loss_fn(pred, y) #得到loss的标量
#反向传播
optimizer.zero_grad() #要记得置零
loss.backward()
optimizer.step()
if batch % 100 == 0: #每训练100次就看一下结果
loss, current = loss.item(), batch * len(X)
print(f"loss: {loss:>7f} [{current:>5d}/{size:>5d}]")
def test_loop(dataloader, model, loss_fn):
size = len(dataloader.dataset)
num_batches = len(dataloader)
test_loss, correct = 0, 0
with torch.no_grad(): #会提高效率,因为是测试集
for X, y in dataloader:
pred = model(X)
#item()返回的是一个浮点型数据,所以我们在求loss或者accuracy时,一般使用item(),而不是直接取它对应的元素
test_loss += loss_fn(pred, y).item()
correct += (pred.argmax(1) == y).type(torch.float).sum().item()
test_loss /= num_batches
correct /= size
print(f"Test Error: \n Accuracy: {(100*correct):>0.1f}%, Avg loss: {test_loss:>8f} \n")
上述训练是单步训练和单步测试,但是我们不仅只是完成一次
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)
#迭代次数
epochs = 10
for t in range(epochs):
print(f"Epoch {t+1}\n-------------------------------")
train_loop(train_dataloader, model, loss_fn, optimizer)
test_loop(test_dataloader, model, loss_fn)
print("Done!")
保存和加载模型
官网地址:https://pytorch.org/tutorials/recipes/recipes/saving_and_loading_models_for_inference.html
两种保存和加载方式:可以只保存state_dict,存储了模型的参数和buffer;或者把整个模型保存下来
首先初始化数据,然后定义模型,初始化模型,优化参数…代码同上,就不重复了,然后开始使用state_dict保存
1、state_dict保存和加载模型
#写一个文件名字
PATH = "state_dict_model.pt"
#保存:其中net是实例化模型的对象名称,net.state_dict()就获取了net的所有参数和buffer缓冲量
torch.save(net.state_dict(), PATH) #但是上述只是保存了模型的参数,并没有保存模型的图
#导入:导入的时候需要保存模型的图,首先需要把模型的图建立起来,再去导入参数和缓存什么的
model = Net() #Net()就是自己定义的模型的类名
model.load_state_dict(torch.load(PATH))
model.eval() #就是主要是针对model 在训练时和评价时不同的 Batch Normalization 和 Dropout 方法模式
#其实上面这个用在预测的时候,因为预测的时候,网络参数是不能发生变化的
上面的model.eval()讲解链接:https://blog.csdn.net/sazass/article/details/116616664
2、对整个模型进行保存和加载
# Specify a path
PATH = "entire_model.pt"
# Save
torch.save(net, PATH)
# Load
model = torch.load(PATH) #和上面的方法相比,这种方法不需要再重新构建一个图结构了
model.eval()
3、保存和加载checkpoint
怎么去保存和加载一个一般的checkpoint
这个的作用是可以做一个暂缓的空间,可以方便暂停一下,方便之后继续训练
然后开始举例子,首先还是初始化数据,然后定义模型,初始化模型,优化参数…代码同上,就不重复了
先是保存:
# Additional information
EPOCH = 5 #因为在训练的时候外面肯定有个epoch,我们这里就举例每五次保存一次checkpoint
PATH = "model.pt"
LOSS = 0.4
torch.save({
'epoch': EPOCH, #当前训练多少个周期
'model_state_dict': net.state_dict(), #保存模型的参数
'optimizer_state_dict': optimizer.state_dict(), #保存优化器内部的状态
'loss': LOSS, #保存当前的训练loss,或者是验证loss
}, PATH)
下述是加载:
model = Net()
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)
checkpoint = torch.load(PATH)
model.load_state_dict(checkpoint['model_state_dict'])
optimizer.load_state_dict(checkpoint['optimizer_state_dict'])
epoch = checkpoint['epoch']
loss = checkpoint['loss']
model.eval() #做推理的时候用这个,因为预测或者说推理的时候,网络参数是不能发生变化的
# - or -
model.train() #做训练的时候用这个
4、保存加载多个模型
还是老样子,同样的流程只不过这次我们用两个模型举例,因为我们是展示保存加载多个模型
保存:
#现在是初始化两个模型,和优化算法
netA = Net()
netB = Net()
optimizerA = optim.SGD(netA.parameters(), lr=0.001, momentum=0.9)
optimizerB = optim.SGD(netB.parameters(), lr=0.001, momentum=0.9)
#开始保存
PATH = "model.pt"
#用一个字典保存起来
torch.save({
'modelA_state_dict': netA.state_dict(),
'modelB_state_dict': netB.state_dict(),
'optimizerA_state_dict': optimizerA.state_dict(),
'optimizerB_state_dict': optimizerB.state_dict(),
}, PATH)
加载:
modelA = Net()
modelB = Net()
optimModelA = optim.SGD(modelA.parameters(), lr=0.001, momentum=0.9)
optimModelB = optim.SGD(modelB.parameters(), lr=0.001, momentum=0.9)
checkpoint = torch.load(PATH)
modelA.load_state_dict(checkpoint['modelA_state_dict'])
modelB.load_state_dict(checkpoint['modelB_state_dict'])
optimizerA.load_state_dict(checkpoint['optimizerA_state_dict'])
optimizerB.load_state_dict(checkpoint['optimizerB_state_dict'])
modelA.eval()
modelB.eval()
# - or -
modelA.train()
modelB.train()
李沐深度学习模块讲解
1、层和块
(1)Sequential类
仔细地看看Sequential
类是如何工作的, 回想一下Sequential
的设计是为了把其他模块串起来。 为了构建我们自己的简化的MySequential
, 我们只需要定义两个关键函数:
- 一种将块逐个追加到列表中的函数。
- 一种前向传播函数,用于将输入按追加块的顺序传递给块组成的“链条”。
下面的MySequential
类提供了与默认Sequential
类相同的功能。就是初始化的时候,里面的一个个layer放进_module中,然后再前向运算,也就是返回的时候,再从modules.values()中一个个的取出来
class MySequential(nn.Module):
def __init__(self, *args):
super().__init__()
for idx, module in enumerate(args):
# 这里,module是Module子类的一个实例。我们把它保存在'Module'类的成员
# 变量_modules中。_module的类型是OrderedDict
self._modules[str(idx)] = module
def forward(self, X):
# OrderedDict保证了按照成员添加的顺序遍历它们
for block in self._modules.values():
X = block(X)
return X
#下面就是调用
net = MySequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10))
net(X)
(2)前向传播函数中执行代码
Sequential类使模型构造变得简单, 允许我们组合新的架构,而不必定义自己的类。 然而,并不是所有的架构都是简单的顺序架构。 当需要更强的灵活性时,我们需要定义自己的块。 例如,我们可能希望在前向传播函数中执行Python的控制流。 此外,我们可能希望执行任意的数学运算, 而不是简单地依赖预定义的神经网络层。
在这个FixedHiddenMLP
模型中,我们实现了一个隐藏层, 其权重(self.rand_weight
)在实例化时被随机初始化,之后为常量。 这个权重不是一个模型参数,因此它永远不会被反向传播更新。 然后,神经网络将这个固定层的输出通过一个全连接层。注意,在返回输出之前,模型做了一些不寻常的事情: 它运行了一个while循环,在L1范数大于1的条件下, 将输出向量除以2,直到它满足条件为止。 最后,模型返回了X
中所有项的和。 注意,此操作可能不会常用于在任何实际任务中, 我们只是向你展示如何将任意代码集成到神经网络计算的流程中。
class FixedHiddenMLP(nn.Module):
def __init__(self):
super().__init__()
# 不计算梯度的随机权重参数。因此其在训练期间保持不变
self.rand_weight = torch.rand((20, 20), requires_grad=False)
self.linear = nn.Linear(20, 20)
def forward(self, X):
X = self.linear(X)
# 使用创建的常量参数以及relu和mm函数,mm就是矩阵相乘
X = F.relu(torch.mm(X, self.rand_weight) + 1)
# 复用全连接层。这相当于两个全连接层共享参数
X = self.linear(X)
# 控制流
while X.abs().sum() > 1:
X /= 2
return X.sum()
上面主要想讲的其实就是,我们可以自己定义一下模型里面的运算,不只是一层一层的叠加,我们可以进行适当的修改,添加一些我们需要的参数,或者运算,或是变形。
2、参数管理
(1)参数访问
其中net就是实例化的模型,[0]、[1]这些东西就是代指的是 Sequential类定义的模型里面的层。下面代码以及运行结是:
print(net[2].state_dict())
OrderedDict([('weight', tensor([[ 0.0251, -0.2952, -0.1204, 0.3436, -0.3450, -0.0372, 0.0462, 0.2307]])), ('bias', tensor([0.2871]))])
所以我们可以通过以下语句去访问模型中的参数:
print(type(net[2].bias)) #<class 'torch.nn.parameter.Parameter'>
print(net[2].bias) #tensor([0.2871], requires_grad=True)
print(net[2].bias.data) #tensor([0.2871])
(2)参数初始化
其中normal_这种后面带有一个下划线的表示是,替代模型中参数的值,而不是调用此函数有个返回值
下面代码展示的是,首先调用内置的初始化器。 下面的代码将所有权重参数初始化为标准差为0.01的高斯随机变量, 且将偏置参数设置为0
def init_normal(m): #m指的就是模块
if type(m) == nn.Linear:
nn.init.normal_(m.weight, mean=0, std=0.01) #之所以可以直接用m.weight 是因为模型中的参数名,看上面第一部分参数访问的结果中就是weight和bias
nn.init.zeros_(m.bias)
net.apply(init_normal) #意思就是将init_normal这个初始化函数放到模块net中
net[0].weight.data[0], net[0].bias.data[0] #(tensor([1., 1., 1., 1.]), tensor(0.))
我们还可以对某些块应用不同的初始化方法。 例如,下面我们使用Xavier初始化方法初始化第一个神经网络层, 然后将第三个神经网络层初始化为常量值42。
def init_xavier(m):
if type(m) == nn.Linear:
nn.init.xavier_uniform_(m.weight) #就是固定的初始化xavier的方法,这个xavier是一种追求数值稳定性的初始化方法,就是之后梯度下降的时候尽可能的不会梯度爆炸也不会梯度消失
def init_42(m):
if type(m) == nn.Linear:
nn.init.constant_(m.weight, 42)
net[0].apply(init_xavier)
net[2].apply(init_42)
print(net[0].weight.data[0]) #tensor([-0.4792, 0.4968, 0.6094, 0.3063])
print(net[2].weight.data) #tensor([[42., 42., 42., 42., 42., 42., 42., 42.]])
(3)参数绑定
有时我们希望在多个层间共享参数: 我们可以定义一个稠密层,然后使用它的参数来设置另一个层的参数
# 我们需要给共享层一个名称,以便可以引用它的参数
shared = nn.Linear(8, 8)
net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(),
shared, nn.ReLU(),
shared, nn.ReLU(),
nn.Linear(8, 1))
net(X)
# 检查参数是否相同
print(net[2].weight.data[0] == net[4].weight.data[0]) #tensor([True, True, True, True, True, True, True, True])
net[2].weight.data[0, 0] = 100
# 确保它们实际上是同一个对象,而不只是有相同的值
print(net[2].weight.data[0] == net[4].weight.data[0]) #tensor([True, True, True, True, True, True, True, True])
这个例子表明第三个和第五个神经网络层的参数是绑定的。 它们不仅值相等,而且由相同的张量表示。 因此,如果我们改变其中一个参数,另一个参数也会改变。 你可能会思考:当参数绑定时,梯度会发生什么情况?
答案是由于模型参数包含梯度,因此在反向传播期间第二个隐藏层 (即第三个神经网络层)和第三个隐藏层(即第五个神经网络层)的梯度会加在一起。
3、自定义层
定义具有参数的层, 这些参数可以通过训练进行调整。 我们可以使用内置函数来创建参数,这些函数提供一些基本的管理功能。 比如管理访问、初始化、共享、保存和加载模型参数。 这样做的好处之一是:我们不需要为每个自定义层编写自定义的序列化程序。
现在,让我们实现自定义版本的全连接层。 回想一下该层需要两个参数,一个用于表示权重另一个用于表示偏置项。 在此实现中,我们使用修正线性单元作为激活函数。 该层需要输入参数:in_units
和units
,分别表示输入数和输出数。
class MyLinear(nn.Module):
def __init__(self, in_units, units):
super().__init__()
self.weight = nn.Parameter(torch.randn(in_units, units)) #nn.Parameter()添加的参数会被添加到Parameters列表中,会被送入优化器中随训练一起学习更新
# torch.nn.parameter.Parameter(data=None,requires_grad=True)
self.bias = nn.Parameter(torch.randn(units,))
def forward(self, X):
linear = torch.matmul(X, self.weight.data) + self.bias.data
return F.relu(linear)
linear = MyLinear(5, 3)
linear.weight
#打印的结果:tensor([[-1.4779, -0.6027, -0.2225],
# [ 1.1270, -0.6127, -0.2008],
# [-2.1864, -1.0548, 0.2558],
# [ 0.0225, 0.0553, 0.4876],
# [ 0.3558, 1.1427, 1.0245]], requires_grad=True)
GPU & CPU
1、计算设备
在PyTorch中,CPU和GPU可以用torch.device('cpu')
和torch.device('cuda')
表示。 应该注意的是,cpu
设备意味着所有物理CPU和内存, 这意味着PyTorch的计算将尝试使用所有CPU核心。 然而,gpu
设备只代表一个卡和相应的显存。 如果有多个GPU,我们使用torch.device(f'cuda:{i}')
来表示第i块GPU(i从0开始)。 另外,cuda:0
和cuda
是等价的。
import torch
from torch import nn
torch.device('cpu'), torch.device('cuda'), torch.device('cuda:1')
#(device(type='cpu'), device(type='cuda'), device(type='cuda', index=1)) index=1的意思是在第二块GPU上
#我们可以查询可用gpu的数量
torch.cuda.device_count()
现在我们定义了两个方便的函数, 这两个函数允许我们在不存在所需所有GPU的情况下运行代码。
注意之后下面讲别的内容的时候会用到这两个关键函数
def try_gpu(i=0): #比如,try_gpu(1),如果存在超过两块的GPU,就返回第二块GPU,方便之后设置在哪块gpu上存储运行数据
"""如果存在,则返回gpu(i),否则返回cpu()"""
if torch.cuda.device_count() >= i + 1:
return torch.device(f'cuda:{i}')
return torch.device('cpu') #如果参数i不合理或者没有gpu的话,就继续使用cpu
def try_all_gpus(): #意思是返回所有可用的gpu
"""返回所有可用的GPU,如果没有GPU,则返回[cpu(),]"""
devices = [torch.device(f'cuda:{i}')
for i in range(torch.cuda.device_count())]
return devices if devices else [torch.device('cpu')]
2、张量与GPU
以查询张量所在的设备。 默认情况下,张量是在CPU上创建的。
x = torch.tensor([1, 2, 3])
x.device #运行结果:device(type='cpu')
需要注意的是,无论何时我们要对多个项进行操作, 它们都必须在同一个设备上。 例如,如果我们对两个张量求和, 我们需要确保两个张量都位于同一个设备上, 否则框架将不知道在哪里存储结果,甚至不知道在哪里执行计算
3、存储在GPU上
有几种方法可以在GPU上存储张量。 例如,我们可以在创建张量时指定存储设备。接 下来,我们在第一个gpu
上创建张量变量X
。 在GPU上创建的张量只消耗这个GPU的显存。 我们可以使用nvidia-smi
命令查看显存使用情况。 一般来说,我们需要确保不创建超过GPU显存限制的数据
X = torch.ones(2, 3, device=try_gpu()) #这里就用到了上面定义的函数try_gpu(),没有参数的时候就是使用第一块GPU,也就是'cuda:0'
X
#运行结果:tensor([[1., 1., 1.],
# [1., 1., 1.]], device='cuda:0')
假设你至少有两个GPU,下面的代码将在第二个GPU上创建一个随机张量
Y = torch.rand(2, 3, device=try_gpu(1))
Y #tensor([[0.1379, 0.4532, 0.9869],
# [0.4779, 0.7426, 0.9622]], device='cuda:1')
4、复制
如果我们要计算X + Y
,我们需要决定在哪里执行这个操作。 如下图所以, 我们可以将X
传输到第二个GPU并在那里执行操作。 不要简单地X
加上Y
,因为这会导致异常, 运行时引擎不知道该怎么做:它在同一设备上找不到数据会导致失败。 由于Y
位于第二个GPU上,所以我们需要将X
移到那里, 然后才能执行相加运算
Z = X.cuda(1)
print(X)
print(Z)
'''运行结果:
tensor([[1., 1., 1.],
[1., 1., 1.]], device='cuda:0')
tensor([[1., 1., 1.],
[1., 1., 1.]], device='cuda:1')
'''
#现在数据在同一个GPU上(Z和Y都在),我们可以将它们相加
Y + Z
'''运行结果:
tensor([[1.1379, 1.4532, 1.9869],
[1.4779, 1.7426, 1.9622]], device='cuda:1')
'''
假设变量Z
已经存在于第二个GPU上。 如果我们还是调用Z.cuda(1)
会发生什么?它将返回Z而不会复制并分配新内存。
Z.cuda(1) is Z #结果是:True
5、神经网络与GPU
类似地,神经网络模型可以指定设备。 下面的代码将模型参数放在GPU上, 当输入为GPU上的张量时,模型将在同一GPU上计算结果
net = nn.Sequential(nn.Linear(3, 1))
net = net.to(device=try_gpu()) #.to()就是放在gpu上的函数
让我们确认模型参数存储在同一个GPU上
net[0].weight.data.device # device(type='cuda', index=0)
一设备上找不到数据会导致失败。 由于Y
位于第二个GPU上,所以我们需要将X
移到那里, 然后才能执行相加运算
Z = X.cuda(1)
print(X)
print(Z)
'''运行结果:
tensor([[1., 1., 1.],
[1., 1., 1.]], device='cuda:0')
tensor([[1., 1., 1.],
[1., 1., 1.]], device='cuda:1')
'''
#现在数据在同一个GPU上(Z和Y都在),我们可以将它们相加
Y + Z
'''运行结果:
tensor([[1.1379, 1.4532, 1.9869],
[1.4779, 1.7426, 1.9622]], device='cuda:1')
'''
假设变量Z
已经存在于第二个GPU上。 如果我们还是调用Z.cuda(1)
会发生什么?它将返回Z而不会复制并分配新内存。
Z.cuda(1) is Z #结果是:True
5、神经网络与GPU
类似地,神经网络模型可以指定设备。 下面的代码将模型参数放在GPU上, 当输入为GPU上的张量时,模型将在同一GPU上计算结果
net = nn.Sequential(nn.Linear(3, 1))
net = net.to(device=try_gpu()) #.to()就是放在gpu上的函数
让我们确认模型参数存储在同一个GPU上
net[0].weight.data.device # device(type='cuda', index=0)