0
点赞
收藏
分享

微信扫一扫

PyTorch Tutorial

本文作为博客“Transformer - Attention is all you need 论文阅读”的补充内容,阅读的内容来自于

https://pytorch.org/tutorials/intermediate/char_rnn_classification_tutorial.html#recommended-preparation

建议的准备流程。

Deep Learning with PyTorch: A 60 Minute Blitz — PyTorch Tutorials 2.2.0+cu121 documentation

页面中的YouTube链接只是一个overview,没办法科学上网不看也没关系。

Tensors — PyTorch Tutorials 2.2.0+cu121 documentation

Tensor是一个特殊的数据结构,类似于数组和矩阵。在PyTorch中我们使用Tensor来encode模型的输入、输出以及参数。Tensor类似于NumPy 中的ndarray,区别在于Tensor可以在GPU上或者其他能够加速运算的硬件上运行。

引入相关库

import torch
import numpy as np

Tensor初始化

直接由数据生成Tensor

可以直接使用list来生成Tensor:

data = [[1, 2], [3, 4]]
x_data = torch.tensor(data)
NumPy数组生成Tensor
np_array = np.array(data)
x_np = torch.from_numpy(np_array)

这里的data还是上面list中的,先把list转换成numpy array,再把这个numpy array转换到Tensor。

使用其他Tensor生成Tensor
x_ones = torch.ones_like(x_data) 
# retains the properties of x_data
print(f"Ones Tensor: \n {x_ones} \n")

x_rand = torch.rand_like(x_data, dtype=torch.float) 
# overrides the datatype of x_data
print(f"Random Tensor: \n {x_rand} \n")

这里x_data是来自于直接从list生成的Tensor,torch.ones_like表示返回用标量1填充出来的size和输入的Tensor相同的Tensor。torch.rand_like则是返回用[0,1)的数字填充出来的size与输入的Tensor相同的Tensor。

使用随机或固定值生成Tensor
shape = (2, 3,)
rand_tensor = torch.rand(shape)
ones_tensor = torch.ones(shape)
zeros_tensor = torch.zeros(shape)

print(f"Random Tensor: \n {rand_tensor} \n")
print(f"Ones Tensor: \n {ones_tensor} \n")
print(f"Zeros Tensor: \n {zeros_tensor}")

类似之前的操作,这里是指定Tensor的size,填充数据,分别是用[0,1)中的随机值填充,用1填充和用0填充。

Tensor属性

tensor = torch.rand(3, 4)

print(f"Shape of tensor: {tensor.shape}")
print(f"Datatype of tensor: {tensor.dtype}")
print(f"Device tensor is stored on: {tensor.device}")

Tensor运算

指定在GPU运算
# We move our tensor to the GPU if available
if torch.cuda.is_available():
  tensor = tensor.to('cuda')
  print(f"Device tensor is stored on: {tensor.device}")

如果在colab上,可以直接改notebook设置

类numpy索引和切片
tensor = torch.ones(4, 4)
tensor[:,1] = 0
print(tensor)

可以按index访问,也可以切片

按行/列方向拼接Tensor
>>> x = torch.randn(2, 3)
>>> x
tensor([[ 0.6580, -1.0969, -0.4614],
        [-0.1034, -0.5790,  0.1497]])
>>> torch.cat((x, x, x), 0)
tensor([[ 0.6580, -1.0969, -0.4614],
        [-0.1034, -0.5790,  0.1497],
        [ 0.6580, -1.0969, -0.4614],
        [-0.1034, -0.5790,  0.1497],
        [ 0.6580, -1.0969, -0.4614],
        [-0.1034, -0.5790,  0.1497]])
>>> torch.cat((x, x, x), 1)
tensor([[ 0.6580, -1.0969, -0.4614,  0.6580, -1.0969, -0.4614,  0.6580,
         -1.0969, -0.4614],
        [-0.1034, -0.5790,  0.1497, -0.1034, -0.5790,  0.1497, -0.1034,
         -0.5790,  0.1497]])
Tensor乘法
# This computes the element-wise product
print(f"tensor.mul(tensor) \n {tensor.mul(tensor)} \n")
# Alternative syntax:
print(f"tensor * tensor \n {tensor * tensor}")

乘法,但是是对应元素和对应元素相乘。

print(f"tensor.matmul(tensor.T) \n {tensor.matmul(tensor.T)} \n")
# Alternative syntax:
print(f"tensor @ tensor.T \n {tensor @ tensor.T}")

乘法,但是矩阵乘法

Tensor加法
print(tensor, "\n")
tensor.add_(5)
print(tensor)

A Gentle Introduction to torch.autograd — PyTorch Tutorials 2.2.0+cu121 documentation

autograd是PyTorch的自动微分引擎,为神经网络训练提供动力,autograd如何帮助神经网络训练?

神经网络是一组嵌套函数的集合,这些函数在一些输入数据上执行。这些函数由参数(由权重和偏置组成)定义,这些参数在PyTorch中存储在张量中。

训练一个神经网络分为两个步骤:
前向传播:在前向传播中,神经网络对正确的输出做出最好的猜测。它通过每个函数运行输入数据来进行猜测。
反向传播:在反向传播中,神经网络根据其猜测中的误差调整其参数。它通过从输出向后遍历,收集误差相对于函数参数(梯度)的导数,并使用梯度下降优化参数来实现这一点。

(个人经验来看,这样说不好理解,前向传播就是给输入之后,输入通过网络走到输出,而反向传播就是输出的结果计算出的loss逆着网络方向走到输入位置,目的是为了调整参数的大小,希望下一次输入走过来能够得到更好的结果)

这里推荐了3Blue1Brown的视频,不过这里给的是YouTube链接,B站上有up搬运,链接(p1)如下:

3blue1brown-深度学习(英文搬运)_哔哩哔哩_bilibili

PyTorch使用

本例中从torchvision加载一个预训练的resnet18模型。我们创建了一个random data tensor来表示具有3个通道的单个图像,高度和宽度为64,并将其相应的标签初始化为一些随机值。预训练模型中的标签具有形状(1,1000)。且只能使用在CPU上。

换言之,图像实际上只是随机生成的有着固定size的Tensor,这些Tensor的标签也是随机生成的。

import torch
from torchvision.models import resnet18, ResNet18_Weights
model = resnet18(weights=ResNet18_Weights.DEFAULT)
data = torch.rand(1, 3, 64, 64)
labels = torch.rand(1, 1000)

这里,从torchvision里面直接获得resnet18的参数,生成的图像是64*64*3,图像的数量是1,labels是1*1000的大小在[0,1)的Tensor。

prediction = model(data) # forward pass

这个就是前向传播,直接把图片放到model里面(因为直接把参数拉下来了,所以当然可以直接predict)

loss = (prediction - labels).sum()
loss.backward() # backward pass

使用模型的预测和相应的标签来计算误差(损失)。下一步是通过网络反向传播这个错误。当我们在loss(Tensor)上调用. backward()时,反向传播被启动。然后autograd计算并存储每个模型参数的梯度在参数的.grad属性中。注意这里使用了sum,这是计算predict的结果对于1000个分类的loss的和。

optim = torch.optim.SGD(model.parameters(), lr=1e-2, momentum=0.9)

加载一个优化器,在本例中是学习率为0.01,momentum为0.9的SGD

optim.step() #gradient descent

最后,我们调用.step()来启动梯度下降。优化器根据存储在.grad中的梯度调整每个参数

Neural Networks — PyTorch Tutorials 2.2.0+cu121 documentation

一个简单的前馈网络。它接受输入,将其通过几个层一个接一个地传递,然后最后给出输出。

定义神经网络的训练过程步骤如下:

1. 定义网络
2. 在输入部分循环放dataset里面的data进去
3. 网络处理输入
4. 计算损失
5. 将梯度传回网络参数
6. 更新网络的weight

可以明显看出,不同的gradient给weight带来的变化是不一样的,比如某个neuron的gradient是1000,另一个和这个neuron差不多位置(层数,output链接的neuron)的neuron的gradient是1,那么在loss回来的时候,那个gradient是1000的显然会更新的更多(受的影响更大)。

定义网络

import torch
import torch.nn as nn
import torch.nn.functional as F


class Net(nn.Module):

    def __init__(self):
        super(Net, self).__init__()
        # 1 input image channel, 6 output channels, 5x5 square convolution
        # kernel
        self.conv1 = nn.Conv2d(1, 6, 5)
        self.conv2 = nn.Conv2d(6, 16, 5)
        # an affine operation: y = Wx + b
        self.fc1 = nn.Linear(16 * 5 * 5, 120)  # 5*5 from image dimension
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, 10)

    def forward(self, x):
        # Max pooling over a (2, 2) window
        x = F.max_pool2d(F.relu(self.conv1(x)), (2, 2))
        # If the size is a square, you can specify with a single number
        x = F.max_pool2d(F.relu(self.conv2(x)), 2)
        x = torch.flatten(x, 1) # flatten all dimensions except the batch dimension
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x


net = Net()
print(net)

简单解释一下,这是一个简单的卷积神经网络,首先x是一个二维的Tensor(比如图像),这个图像的channel是1(可能是黑白的,当然也可能是彩色图像但是只使用其中的某个通道),对这个Tensor使用6个kernel大小为5*5,步长为1,padding为0的卷积(使用6个就代表输出会有6个channel),然后接入ReLU(激活函数,提供非线性),接着是max pooling(2*2,stride=kernel size=2)

重复上面的步骤,但是此时的输入channel为6,输出channel为16

把得到的二维的Tensor拍扁,变成向量。

接入全连接层,这里假定经过两层conv的结果是一个5*5*16的feature map。也就可以反推最初的image的大小。

input -> conv2d -> relu -> maxpool2d -> conv2d -> relu -> maxpool2d
      -> flatten -> linear -> relu -> linear -> relu -> linear

进入flatten之前为5*5*16(16为channel大小),这也是第二个max pooling的结果
在进入第二个max pooling之前为10((x-2)/2+1=5)*10*16,这是第二个conv的结果
在进入第二个conv之前为14((x-5)/1+1=10)*14*6,这是第一个max pooling的结果
在进入第一个max pooling之前为28((x-2)/2+1=14)*28*6,这是第一个conv的结果
在进入第一个conv之前为32((x-5)/1+1=28)*32*1,这就是image的尺寸

只需要定义前向函数,后向函数(计算梯度)会使用autograd自动为你定义,模型的可学习参数由net.parameters()返回。

params = list(net.parameters())
print(len(params))
print(params[0].size())  # conv1's .weight

现在,我们尝试一个随机的32x32输入。注:此网(LeNet)的预期输入大小为32x32。注意使用任何数据集都要把图片尺寸调整成合适的大小(经常常用的比如裁剪,缩放)。

input = torch.randn(1, 1, 32, 32)
out = net(input)
print(out)

输出的长度是10,一位之前定义了输出的长度。

net.zero_grad()
out.backward(torch.randn(1, 10))

将所有参数和随机梯度backprops的梯度缓冲区归零。

Loss Function

损失函数接受(输出,目标)对输入,并计算一个值来估计输出与目标的距离。常用的比如均方误差。

output = net(input)
target = torch.randn(10)  # a dummy target, for example
target = target.view(1, -1)  # make it the same shape as output
criterion = nn.MSELoss()

loss = criterion(output, target)
print(loss)

现在我们能看到这样的计算图:

input -> conv2d -> relu -> maxpool2d -> conv2d -> relu -> maxpool2d
      -> flatten -> linear -> relu -> linear -> relu -> linear
      -> MSELoss
      -> loss

print(loss.grad_fn)  # MSELoss
print(loss.grad_fn.next_functions[0][0])  # Linear
print(loss.grad_fn.next_functions[0][0].next_functions[0][0])  # ReLU

Backprop

要反向传播错误,我们所要做的就是使用loss.backward()。你需要清除现有的梯度,否则梯度会累积到现有的梯度上。

net.zero_grad()     # zeroes the gradient buffers of all parameters

print('conv1.bias.grad before backward')
print(net.conv1.bias.grad)

loss.backward()

print('conv1.bias.grad after backward')
print(net.conv1.bias.grad)

更新参数

之前提到了参数更新的策略:

weight = weight - learning_rate * gradient
learning_rate = 0.01
for f in net.parameters():
    f.data.sub_(f.grad.data * learning_rate)

当你使用神经网络时,你想要使用各种不同的更新规则,如SGD、Nesterov-SGD、Adam、RMSProp等。因此可以使用torch.optim工具来做这件事。

import torch.optim as optim

# create your optimizer
optimizer = optim.SGD(net.parameters(), lr=0.01)

# in your training loop:
optimizer.zero_grad()   # zero the gradient buffers
output = net(input)
loss = criterion(output, target)
loss.backward()
optimizer.step()    # Does the update

Training a Classifier — PyTorch Tutorials 2.2.0+cu121 documentation

数据

这里使用的是CIFAR10数据集,即图像和他们的分类,总共10类。

载入和正则化CIFAR10数据集

import torch
import torchvision
import torchvision.transforms as transforms

transform = transforms.Compose(
    [transforms.ToTensor(),
     transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])

batch_size = 4

trainset = torchvision.datasets.CIFAR10(root='./data', train=True,
                                        download=True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=batch_size,
                                          shuffle=True, num_workers=2)

testset = torchvision.datasets.CIFAR10(root='./data', train=False,
                                       download=True, transform=transform)
testloader = torch.utils.data.DataLoader(testset, batch_size=batch_size,
                                         shuffle=False, num_workers=2)

classes = ('plane', 'car', 'bird', 'cat',
           'deer', 'dog', 'frog', 'horse', 'ship', 'truck')

torchvision.datasets.CIFAR10:返回(image, target) where target is index of the target class.
注意,这里的transform若为None,不经过任何转换,则trainset中保存的图片的格式是<class 'PIL.Image.Image'>类型。这里是添加了transform,一方面是转变格式为Tensor,另一方面就是对图像做正则化。

展示数据集

(不一定要做,不过一般大家都有自己的熟悉数据集的方法,我个人会做一些基础的统计之类的)

import matplotlib.pyplot as plt
import numpy as np

# functions to show an image


def imshow(img):
    img = img / 2 + 0.5     # unnormalize
    npimg = img.numpy()
    plt.imshow(np.transpose(npimg, (1, 2, 0)))
    plt.show()


# get some random training images
dataiter = iter(trainloader)
images, labels = next(dataiter)

# show images
imshow(torchvision.utils.make_grid(images))
# print labels
print(' '.join(f'{classes[labels[j]]:5s}' for j in range(batch_size)))

定义CNN网络

import torch.nn as nn
import torch.nn.functional as F


class Net(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(3, 6, 5)
        self.pool = nn.MaxPool2d(2, 2)
        self.conv2 = nn.Conv2d(6, 16, 5)
        self.fc1 = nn.Linear(16 * 5 * 5, 120)
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, 10)

    def forward(self, x):
        x = self.pool(F.relu(self.conv1(x)))
        x = self.pool(F.relu(self.conv2(x)))
        x = torch.flatten(x, 1) # flatten all dimensions except batch
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x


net = Net()

这一网络同之前的网络相同。

定义Loss function和optimizer

import torch.optim as optim

criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)

训练

for epoch in range(2):  # loop over the dataset multiple times

    running_loss = 0.0
    for i, data in enumerate(trainloader, 0):
        # get the inputs; data is a list of [inputs, labels]
        inputs, labels = data

        # zero the parameter gradients
        optimizer.zero_grad()

        # forward + backward + optimize
        outputs = net(inputs)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()

        # print statistics
        running_loss += loss.item()
        if i % 2000 == 1999:    # print every 2000 mini-batches
            print(f'[{epoch + 1}, {i + 1:5d}] loss: {running_loss / 2000:.3f}')
            running_loss = 0.0

print('Finished Training')

两个epoch,每次都只加载进来一个batch。首先把梯度归零,前向传播,得到当前的输出,计算loss,反向传播,更新参数。

每2000个mini-batch,输出一个平均损失。

PATH = './cifar_net.pth'
torch.save(net.state_dict(), PATH)

保存训练好的模型。

测试

net = Net()
net.load_state_dict(torch.load(PATH))

重新加载保存的模型(注意:这里不需要保存和重新加载模型,只是说明可以这么做)

correct = 0
total = 0
# since we're not training, we don't need to calculate the gradients for our outputs
with torch.no_grad():
    for data in testloader:
        images, labels = data
        # calculate outputs by running images through the network
        outputs = net(images)
        # the class with the highest energy is what we choose as prediction
        _, predicted = torch.max(outputs.data, 1)
        total += labels.size(0)
        correct += (predicted == labels).sum().item()

print(f'Accuracy of the network on the 10000 test images: {100 * correct // total} %')

整体的正确率。

# prepare to count predictions for each class
correct_pred = {classname: 0 for classname in classes}
total_pred = {classname: 0 for classname in classes}

# again no gradients needed
with torch.no_grad():
    for data in testloader:
        images, labels = data
        outputs = net(images)
        _, predictions = torch.max(outputs, 1)
        # collect the correct predictions for each class
        for label, prediction in zip(labels, predictions):
            if label == prediction:
                correct_pred[classes[label]] += 1
            total_pred[classes[label]] += 1


# print accuracy for each class
for classname, correct_count in correct_pred.items():
    accuracy = 100 * float(correct_count) / total_pred[classname]
    print(f'Accuracy for class: {classname:5s} is {accuracy:.1f} %')

查看不同的种类的accuracy。

Learning PyTorch with Examples — PyTorch Tutorials 2.2.0+cu121 documentation

这是一个比较老的tutorial,所以主要是用比较from scratch的方法,可以看一看。

The Unreasonable Effectiveness of Recurrent Neural Networks (karpathy.github.io)

Recurrent Neural Networks

Sequences

循环网络允许我们对向量序列进行操作,无论是输入的sequence还是输出的sequence。

对上面图像从左到右依次举例为图像分类,图像字幕(获取图像并输出一个句子),情感分析(将给定的句子分类为表达积极或消极的情绪),翻译,视频分类。

RNN将输入向量与状态向量结合起来,并使用一个固定的(但可以学习的)函数来产生一个新的状态向量。在编程术语中,这可以解释为运行具有特定输入和一些内部变量的固定程序。从这个角度来看,RNN本质上是描述程序。

(我其实没太看明白这里是什么意思,大概猜测一下,要说的就是不进关注当前的输入也关注状态,这个状态很有可能和HMM是相关的,也就是在出现词A的前提下跳转到词B的概率这种)

RNN computation

接受一个输入向量x,并给你一个输出向量y。然而,关键的是,这个输出向量的内容不仅受到你刚刚输入的输入的影响,还受到你过去输入的整个历史的影响。

RNN类有一些内部状态,每次调用时都会更新这些状态。在最简单的情况下,这个状态由一个隐藏向量h组成。

class RNN:
  # ...
  def step(self, x):
    # update the hidden state
    self.h = np.tanh(np.dot(self.W_hh, self.h) + np.dot(self.W_xh, x))
    # compute the output vector
    y = np.dot(self.W_hy, self.h)
    return y

假设我们只有四个可能的字母“hello”的词汇表,并且想在训练序列“hello”上训练一个RNN。这个训练序列实际上是4个独立训练示例的来源:1。在“h”的背景下,“e”的概率应该是可能的。3. " l "应该出现在"he"的上下文中。考虑到“hel”的上下文,“l”也应该是可能的,最后4。考虑到“hell”的上下文,“o”应该是可能的。

例如,我们看到,在第一个时间节点,当RNN看到字符“h”时,它为下一个字母“h”分配置信度为1.0,为字母“e”分配置信度为2.2,为“l”分配置信度为-3.0,为“o”分配置信度为4.1。由于在我们的训练数据(字符串“hello”)中,下一个正确的字符是“e”,我们希望增加它的置信度(绿色)并降低所有其他字母的置信度(红色)。

大概看了一下后续的内容都是举例了,感兴趣可以自己去看看。

Understanding LSTM Networks -- colah's blog

在上图中,循环允许信息从网络的一个步骤传递到下一个步骤。
虽然看起来不太一样,但是实际上与普通的神经网络并没有什么不同。递归神经网络可以被认为是同一网络的多个副本,每个副本向后继网络传递一条消息。如果展开循环会得到:

有时,我们只需要查看最近的信息来执行当前的任务。例如,考虑一个语言模型,它试图根据前面的单词预测下一个单词。如果我们要预测“the clouds are in the ___”,我们不需要更多的上下文了,很明显下一个单词是sky。在这种情况下,相关信息与需要信息的地方之间的差距很小,rnn可以学习使用过去的信息。

但也有一些情况我们需要更多的背景。试着预测“I grew up in France... I speak fluent _____”这篇文章的最后一个单词。最近的信息表明,下一个词可能是一种语言的名称,但如果我们想要缩小范围,我们需要法国的背景,从更远的地方。相关信息与需要信息的点之间的差距完全有可能变得非常大。
不幸的是,随着差距的扩大,rnn变得无法学习连接信息。

->LSTM

Long Short Term Memory Networks是一种特殊的RNN,能够学习长期依赖关系,被明确设计为避免长期依赖问题。长时间记住信息实际上是他们的默认行为,并不是学习所得。

所有的递归神经网络都具有神经网络重复模块链的形式。在标准的rnn中,这个重复模块将具有非常简单的结构,例如单个tanh层。

LSTM也具有这种链式结构,但重复模块具有不同的结构。

剩下的内容基本与课程中的类似,在此不做赘述。

举报

相关推荐

0 条评论