PyTorch for Deep Learning & Machine Learning – Full Course(1) - NoTe0x006

Baobao0824

原链接:
https://www.youtube.com/watch?v=V_xro1bcAuA

写在前面

这一系列是我在学习他人的优质课程(博客文章、视频课程等)时所做的学习笔记,根据我自身的水平来进行学习,同时会进行思维的发散,补充原文中没有提到或者有错误的地方。同时也会进行经常性的更新和整理,让它和我的当下的状态更加契合。

最近在弄百度的比赛,所以必须把我的AI技能提上日程了。我必须要好好掌握机器学习的主要流程,能够自己不借助AI,从头训练一个模型,我觉得这才是达到目的了。

基本概念介绍

Welcome and “what is deep learning?”

机器学习是将数据变成数,然后去寻找这些数中的模式。那么如何找到这些模式呢,就是各种算法了。

而深度学习是机器学习的一个子集,pytorch经常用来写深度学习代码,但是它也可以写一些机器学习的代码。作者用做烤鸡举例:传统编程是给你原材料和规则,让你输出一个烤鸡;而机器学习算法是给你原材料和烤鸡,让你发现做烤鸡的规则。

Why use machine/deep learning?

为什么我们要用机器学习或者深度学习呢?对于一个过于复杂的问题,你能想出所有的规则吗?例如我们想造一辆自动驾驶的汽车,让你写出所有驾驶规则,即使你开了很长时间的车,你还是列举不完,毕竟规则实在是太多了。视频作者用了一个用户的评论:它可以量化宇宙中的一切事物。他还分享了一本叫做《Google’s Machine Learning Handbook》的书,等到我以后有机会就拿去读吧。

The number one rule of ML

什么时候要用机器学习呢?那首先就是传统方法失效的时候,深度学习就可能有帮助。还有就是环境持续变化,它可以学习新的场景,未来根据需要进行调整。或者你有很大的数据集,

什么场景是典型的不适合使用机器学习的地方呢?首先是你需要可解释性的地方;其次是传统方法更好的时候;还有就是当错误不可接受的时候;最后就是当时没有足够数据的时候。

Machine learning vs deep learning

机器学习适合成列的表格数据,也就是结构化数据;而深度学习适合非标准化的数据,也就是各种不同的数据例如图片、自然语言之类的。

一些适合结构化数据的典型算法有(在深度学习出现之后,这些都被称作浅层算法):

  • 随机森林
  • 梯度提升模型
  • 朴素贝叶斯
  • 最近邻
  • 支持向量机

深度学习的典型方法有下面这些:

  • 神经网络
  • 全连接神经网络
  • 卷积神经网络CNN
  • 循环神经网络RNN
  • Transformer

Anatomy of neural networks

作者没直接下定义,而是让你google一下到底什么是神经网络,他还推荐了3b1b的视频,有很多种自己的定义。

数据在进入神经网络之前,会被转化成各种数,即数值编码;然后将他们传入神经网络。各种不同的神经网络都遵循输入、操作、输出这三部分,而神经网络会自己学会这些表征。最终输出一个权重,我们可以将这个权重转化为人类能够理解的格式。

神经网络分为三部分:输入层、隐藏层和输出层。隐藏层的数量不一,例如Resnet152,总共有152个隐藏层。每一层的神经元数量也可以不一样。例如视频作者给出的网络,输入层有2个神经元,隐藏层有3个而输出层有一个。一般我们谈论神经网络的架构,指的就是这个层数和神经元数量。每一层之间都用线性或(和)非线性的函数相结合。

可以忽略那晃动的鼠标指针
可以忽略那晃动的鼠标指针

Different learning paradigms

深度学习有不同的范式。

首先是监督学习。当你有数据和标签的时候可以用监督学习,例如做烤鸡的例子,你有大量的原材料(数据),还有很多正确的输出示例。再比如猫狗大战,你有很多张猫狗的照片,而且你知道一张照片是猫还是狗。

然后是无监督/自监督学习,是你拥有数据本身,但是没有标签,例如猫狗大战中,你只有猫狗的照片,而不知道他们哪张是猫哪张是狗。

最后是迁移学习,借鉴一个模型中学到的范式,然后将数据集转移到另一个模式。例如我们有一个猫狗大战的监督学习模型,我可以把这个转移到我们自己的模型中。

还有一个作者没提到的范式就是强化学习,是agent和环境交互,环境根据agent的动作给出奖赏让agent继续学习。

本视频主要关注的两个内容就是监督学习和迁移学习。

What can deep learning be used for?

深度学习会用在什么方面?这个问题简直是太水了。我只知道transformer是深度学习,那么这就说明只要想碰transformer都要先弄明白深度学习。不过作者还是给了我们一些使用深度学习的场合。

  • 推荐系统(不得不说油管的推荐系统确实很烂)
  • 机器翻译
  • 语音识别
  • 计算机视觉(YOLO是吧)
  • NLP

深层次来讲,机器翻译和语言识别是seq2seq,因为你输出一个序列,然后输出一个序列。而计算机视觉和NLP是分类回归,他预测了一个问题,例如判断一个邮件是否是垃圾文件。

What is/why PyTorch?

什么是pytorch?他直接给出了一张图

  • 最受欢迎的研究型深度学习框架
  • 用 Python 编写高效的深度学习代码(可在单个 GPU/多个 GPU 上运行)
  • 能调用众多预训练深度学习模型(Torch Hub / torchvision.models)
  • 全流程覆盖:数据预处理、模型构建、在应用/云端部署模型
  • 最初由 Facebook/Meta 内部设计并使用(现已开源,特斯拉、微软、OpenAI 等企业也在采用)

视频作者还推荐了一个网站,叫做code with papers,这是一个用来追踪机器学习论文代码的网站,你可以用它来看各个论文中给出的代码。说实话现在有这么好的深度学习框架,完全不需要从头开始搭建,很多东西你都可以先放一放,例如transformer到底为什么这么做之类的(我感觉)。

他还介绍了一下GPU和TPU,GPU我们都知道这里不赘述了;而TPU是Tensor Processing Unit,在现在的通用场景中显然GPU更好用,毕竟个人用户不可能搞TPU过来。

What are tensors?

什么是Tensor,让我先去问一下AI。从数学的本职上来说,他就是一个n维的数组,有多个轴(只有2个轴的你才能称之为矩阵),在pytorch中它给多维数组套了一层功能壳,除了存储原始数值,还会存储数据类型、存储位置和计算属性等。本视频的作者给出的定义,它就是任何数据的数字表述形式。

同时视频作者给出了一个典型的pytorch工作流程。

我懒得翻译了,应该能看得懂
我懒得翻译了,应该能看得懂

  1. 准备数据
  2. 构建或选择一个预训练模型;选择损失函数和优化器;构建训练循环
  3. 将模型拟合到数据并做出预测
  4. 评估模型
  5. 通过实验改进
  6. 保存并重新加载训练好的模型

Important resources

在正式开始代码之前,作者也介绍了一些重要的基本资源

如果要写代码的话,可以直接从资源中下载对应的jupyter notebook。

Getting setup

他介绍了 google colab网站,这是一个在线使用Jupyter notebook的地方,同时会给你免费的GPU和NPU来使用。由于我不需要在线使用,那么我就在本地使用,毕竟我手里有conda环境,这个还是在毕设的时候弄得,如果这个模型不支持mps的话,我就会在那上面跑。项目中的SETUP.md也给了安装的方法。不过我就不需要看了,这些环境还是不会难倒我的。

1
2
3
4
5
import torch
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
print(torch.__version__)

我们当前环境的torch版本是2.5.1,远大于视频的1.10.0,不过大多数的基础操作应该还是没问题的。如果有问题的话就再去看看最新的代码怎么弄,毕竟现在也不需要AI来帮我生成代码了,这说明我根本没理解,也没看懂训练代码应该怎么做。

张量的基础操作

Introduction to tensors

Tensor是深度学习的基本模块和数据的基础。首先我们可以创建一个标量(scalar)也就是没有任何维度的数字。

1
scalar = torch.tensor(7)

直接输出scalar,可以发现他是tensor(7)也就是一个标量7。

同时视频作者把Tensor的官方文档放在这里了,如果你需要查阅的话点击这个链接就可以了

那么一个标量有哪些属性呢?在本系列的文档中,尤其是涉及到jupyter notebook的,如果碰见一个单个的变量,那么下一行注释就是他的值。

1
2
scalar.ndim
# 0

item方法可以把一个Tensor转换为普通的python类型,例如这个标量中的int。

1
2
scalar.item()
# 7

接下来我们创建一个向量。

1
2
3
vector = torch.tensor([7,7])
vector
# tensor([7, 7])

看一下这个向量的维度

1
2
vector.ndim
# 1

为什么只有一个维度呢?这是因为他是一个一维的数组,和0相比较而言,他不是一个点而是一条线,这解释了为什么维度是1。

shape方法可以看它的形状。

1
2
vector.shape
# torch.Size([2])

看维度有一个更简单的方法就是看方括号的对数,而这里的形状是2*1,所以是2。

接下来我们再尝试创建一个矩阵

1
2
3
4
5
6
7
MATRIX = torch.tensor([
[7, 8],
[9, 10]
])
MATRIX
# tensor([[ 7, 8],
# [ 9, 10]])

根据方括号的对数,我们很容易发现他是二维的,真的是这样吗?

1
2
MATRIX.ndim
# 2

事实上还真是这样,那么假如我给他一个一维的索引会输出什么呢?

1
2
MATRIX[0]
# tensor([7, 8])

那么这个矩阵的形状我们也应该非常清楚了,会是2*2。

1
2
MATRIX.shape
# torch.Size([2, 2])

虽然刚才的那些变量维数各不相同,但是全都是Tensor类型的变量。接下来我们再看看真正的3维的Tenser。

1
2
3
4
5
6
7
TENSOR = torch.tensor([[[1, 2, 3],
[3, 6, 9],
[2, 4, 5]]])
TENSOR
# tensor([[[1, 2, 3],
# [3, 6, 9],
# [2, 4, 5]]])

很显然,这个张量的维度是3维。

1
2
TENSOR.ndim
# 3

那么他的形状是怎么样的?是$3 \times 3 \times 1$还是$1 \times 3 \times 3$?

1
2
TENSOR.shape
# torch.Size([1, 3, 3])

很显然,最外面一层中括号里面只有一个中括号,所以第一维的大小是1,而不是3,那么他的第一维就应该是那个矩阵,真的是这样的吗?

1
2
3
4
TENSOR[0]
# tensor([[1, 2, 3],
# [3, 6, 9],
# [2, 4, 5]])

还真的是这样。这里有一张图片来解释这件事。

这个图还挺不错的
这个图还挺不错的

你可以这么理解,维度数越大,取的方括号的层数越深。我觉得其实还是挺好理解的。只要你想的话,你甚至还可以这么操作

1
2
TENSOR[0][1][2]
# tensor(9)

取的是0层矩阵的第1个行和第2个列,那么就是9(在本网站中当我用阿拉伯数字的时候是0开始,用汉字数字的时候是从一开始)

在推荐的标准中,矩阵和张量变量的命名最好是大写,而标量和向量是小写的则最好。

Creating tensors

接下来我们介绍一下随机张量。为什么要用随机张量?因为许多神经网络的学习方式是从随机张量开始,然后调整这些随机数来更好的表示数据。随机数->查看数据->更新随机数->查看数据->更新随机数…

随机张量的文档在这里

那么如何创建一个随机张量呢?假设我们要创建一个3乘4大小的随机张量。

1
2
3
4
5
random_tensor = torch.rand(3,4)
random_tensor
# tensor([[0.4545, 0.1568, 0.5251, 0.7801],
# [0.8649, 0.8840, 0.5487, 0.4744],
# [0.3544, 0.5987, 0.8096, 0.3671]])

接下来我们可以尝试创建一个形状类似于图像张量的随机张量,例如

1
2
3
4
random_image_size_tensor = torch.rand(size=(3, 224, 224))
# 大小是224*224,3是颜色通道RGB
random_image_size_tensor.shape, random_image_size_tensor.ndim
# (torch.Size([3, 224, 224]), 3)

关于图像张量的直观展示就像下面的图片。

每个像素都有三个数,分别表示RGB的值
每个像素都有三个数,分别表示RGB的值

接下来我们看全0和全1张量。

1
2
3
4
5
6
7
8
9
10
11
zeros = torch.zeros(size=(3, 4))
zeros
# tensor([[0., 0., 0., 0.],
# [0., 0., 0., 0.],
# [0., 0., 0., 0.]])

ones = torch.ones(size=(3, 4))
ones
# tensor([[1., 1., 1., 1.],
# [1., 1., 1., 1.],
# [1., 1., 1., 1.]])

还有一个重要的参数我们还没看,就是数据类型。

1
2
3
4
ones.dtype
# torch.float32
random_tensor.dtype
# torch.float32

除非我们指定,否则默认创建的张量数据类型就是torch.float32

接下来我们如何创建一个范围张量呢?

1
2
3
one_to_ten = torch.arange(start=1, end=11, step=1)
one_to_ten
# tensor([ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])

注意到,它的区间是左闭右开,这些参数都很常见,这里不再赘述了。

假如说我们有一个特定的张量形状,想在别的地方复制,但是你不记得到底是什么形状,就可以使用这种like方法。

1
2
3
ten_zeros = torch.zeros_like(input=one_to_ten)
ten_zeros
# tensor([0, 0, 0, 0, 0, 0, 0, 0, 0, 0])

Tensor datatypes

假如说我们让张量的类型为None,会发生什么事情?可以尝试一下。

1
2
3
float_32_tensor = torch.tensor([3.0, 6.0, 9.0],dtype=None)
float_32_tensor.dtype
# torch.float32

结果还是torch.float32,这是因为float32是pytorch的默认数据类型,即使指定为None,也会将其设为float32。那么除了float32,别的是不是可以的?假设我们给换成了float16可以吗,显然是可以的。

1
2
3
float_32_tensor = torch.tensor([3.0, 6.0, 9.0],dtype=torch.float16)
float_32_tensor.dtype
# torch.float16

作者说初学pytorch和深度学习最常见的三个错误就是:

  • 张量数据类型不对
  • 张量形状错误
  • 张量不在正确的设备中

除了dtype参数,我们还有两个最重要的参数,分别是devicerequires_grad

  • device代表你要使用这个张量的设备。默认情况下,设备是cpu如果你有CUDA环境就可以把设备改成cuda
  • requires_grad参数代表你是否需要进行梯度追踪。

我们如何改变已有张量的数据类型呢。假设我们要把float32转为float16,可以这样做:

1
2
3
float_16_tensor = float_32_tensor.type(torch.float16)
float_16_tensor.dtype
# torch.float16

当然,数据类型除了不同的float,还有整数类型。

1
2
3
int_64_tensor = torch.tensor([3, 6, 9], dtype=torch.long)
int_64_tensor.dtype
# torch.int64

如果我们把两个不同类型的张量相乘,他们的结果是什么类型呢?我们可以测试一下:

1
2
3
4
5
result_a = float_32_tensor * int_64_tensor
result_b = float_32_tensor * float_16_tensor
result_c = int_64_tensor * float_16_tensor
result_a.dtype, result_b.dtype, result_c.dtype
# (torch.float32, torch.float32, torch.float16)

这样看来,规则大抵就是全部转成两边精度最高的那个类型(颇有以前学习C语言时候的感觉了)不过在模型训练的时候,我们最好还是保证数据类型的一致性。

Tensor attributes (information about tensors)

针对刚才的三个问题,我们在检查张量的属性的时候就有三个重要的属性可以来看。

  • 张量数据类型不对:dtype
  • 张量形状错误:shape
  • 张量不在正确的设备中:device

例如我们随便找个张量。

1
2
3
some_tensor = torch.rand(3, 4)
some_tensor.dtype, some_tensor.shape, some_tensor.device
# (torch.float32, torch.Size([3, 4]), device(type='cpu'))

Manipulating tensors

张量的运算包括:

  • 按元素数乘
  • 矩阵乘法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
tensor = torch.tensor([1, 2, 3])
tensor + 10
# tensor([11, 12, 13])
tensor * 10
# tensor([10, 20, 30])
tensor - 10
# tensor([-9, -8, -7])
tensor / 2
# tensor([0.5000, 1.0000, 1.5000])

# 也可以使用pytorch的内置方法
torch.mul(tensor, 10)
# tensor([10, 20, 30])
torch.add(tensor, 10)
# tensor([11, 12, 13])
torch.sub(tensor, 10)
# tensor([-9, -8, -7])
torch.div(tensor, 2)
# tensor([0.5000, 1.0000, 1.5000])

作者更推荐我们直接使用运算符号而不是方法,因为这样看起来更加的直观。

Matrix multiplication

在这一节中它会教我们很多矩阵的运算。矩阵的点乘我是不需要他来教的。

首先,我们有矩阵的按元素乘法,就是让两个同样大小的矩阵的对应元素相乘得出结果。

1
2
tensor * tensor
# tensor([1, 4, 9])

对于矩阵乘法来说,有一个专门的运算符@,不过我们也可以用torch.matmul方法。

1
2
3
4
torch.matmul(tensor, tensor)
# tensor(14)
tensor@tensor
# tensor(14)

在pytorch中,一维张量相乘的结果其实是向量的点积,没有行向量与列向量之分。如果你想要达到真正的矩阵乘法,你需要让两个张量都是二维的。

1
2
3
4
5
6
7
tensor_a = torch.randn(size=(3,1))
(tensor_a @ tensor_a.T).shape
# torch.Size([3, 3])
(tensor_a.T @ tensor_a).shape
# torch.Size([1, 1])
(tensor_a.T @ tensor_a.T).shape
# RuntimeError: mat1 and mat2 shapes cannot be multiplied (1x3 and 1x3)

这个东西才是真正的矩阵乘法。

作者同样给我们演示了一下,pytorch自带的方法要比自己手动实现要快得多,那是肯定的毕竟python本身是C语言写的,直接用C语言写的方法肯定是更快的。

1
2
3
4
5
6
7
8
9
10
%%time
value = 0
for i in range(len(tensor)):
value += tensor[i] * tensor[i]
# CPU times: user 905 μs, sys: 753 μs, total: 1.66 ms
# Wall time: 1.49 ms
%%time
torch.matmul(tensor, tensor)
# CPU times: user 87 μs, sys: 78 μs, total: 165 μs
# Wall time: 121 μs

torch.mm也可以用作矩阵乘法的简写,不过一维张量是不能用的(那我用@岂不是更简单)。

1
2
3
4
torch.mm(tensor,tensor)
# RuntimeError: self must be a matrix
torch.mm(tensor_a,tensor_a.T).shape
# torch.Size([3, 3])

作者提醒我们注意矩阵乘法中的维度,也就是行和列,对于学过考研数学线性代数的人来说,这个根本就不是问题。同时他也为我们推荐了一个网站,可以方便的计算矩阵乘法。

同时作者在这里介绍了矩阵的转置,我们已经用过了,不再需要他介绍了,直接跳到下一个片段。

Finding the min, max, mean & sum

先创建本节需要用到的张量。

1
2
3
x = torch.arange(1, 100, 10)
x, x.dtype
# (tensor([ 1, 11, 21, 31, 41, 51, 61, 71, 81, 91]), torch.int64)

最大值和最小值都可以通过两种方式,一个是torch下面的方法,一个是张量下面的方法

1
2
3
4
torch.min(x), x.min()
# (tensor(1), tensor(1))
torch.max(x), x.max()
# (tensor(91), tensor(91))

平均值应该也是遵循一样的原则,我们就试试看。

1
2
torch.mean(x), x.mean()
# RuntimeError: mean(): could not infer output dtype. Input dtype must be either a floating point or complex dtype. Got: Long

居然不行哎,他必须要float或者复数才能求平均值,因此我们需要转一下数据类型。

1
2
torch.mean(x.type(torch.float32)), x.type(torch.float32).mean()
# (tensor(46.), tensor(46.))

最后是求和。

1
2
torch.sum(x), x.sum()
# (tensor(460), tensor(460))

那么我们想知道,到底最小值和最大值出现在哪个下标中呢?,我们需要借助argminargmax方法。

1
2
3
4
x.argmin(), x.argmax()
# (tensor(0), tensor(9))
x[0], x[9]
# (tensor(1), tensor(91))

的确是最小值和最大值,这个东西感觉比min和max要更加的好用。为了保持API的一致性,pytorch几乎所有的操作返回的都是tensor,这样可以方便进行运算,这也是为什么这两个方法不返回int。

Reshaping, viewing and stacking

这一节我们会学习和形状相关的方法,包括:

  • reshape,将输入张量调整为指定的形状。
  • view,返回一个具有特定形状的输入张量的视图,但该视图与原张量共享内存。
  • stacking,将多个张量在垂直方向(vstack)或水平方向(hstack)上进行组合。
  • squeeze,移除张量中所有维度为1的维度。
  • unsqueeze,在目标张量中增加一个维度为1的维度。
  • permute,返回输入张量,其维度按照特定方式进行了重新排列(交换)

首先我们先定义这一节需要用到的张量x

1
2
3
x = torch.arange(1., 10.)
x, x.shape
# (tensor([1., 2., 3., 4., 5., 6., 7., 8., 9.]), torch.Size([9]))

我们先尝试使用reshape,最简单的使用方式是给张量添加一个维度,或者也可以将它变成其他的形状。

1
2
3
4
5
6
7
8
9
10
11
12
x_reshaped = x.reshape(1, 9)
x_reshaped, x_reshaped.shape
# (tensor([[1., 2., 3., 4., 5., 6., 7., 8., 9.]]), torch.Size([1, 9]))
x_reshaped = x.reshape(3, 3)
x_reshaped, x_reshaped.shape
# (tensor([[1., 2., 3.],
# [4., 5., 6.],
# [7., 8., 9.]]),
# torch.Size([3, 3]))
x_reshaped = x.reshape(3, 4)
x_reshaped, x_reshaped.shape
# RuntimeError: shape '[3, 4]' is invalid for input of size 9

只要保证它的尺寸能够符合最后有9个元素就可以,不管是1乘9还是3乘3都可以办得到,注意到reshape方法不改变原来变量的值。

接下来我们尝试使用view函数。

1
2
3
4
5
6
7
8
9
z = x.view(1, 9)
z, z.shape
# (tensor([[1., 2., 3., 4., 5., 6., 7., 8., 9.]]), torch.Size([1, 9]))
z = x.view(3, 3)
z, z.shape
# (tensor([[1., 2., 3.],
# [4., 5., 6.],
# [7., 8., 9.]]),
# torch.Size([3, 3]))

看起来似乎都差不多啊,到底有什么区别呢?区别就在于reshape可能会隐式的复制数据,而view要求张量在内存上连续。在实践中还是推荐首先使用reshape(毕竟你都搞深度学习了,还管内存干嘛)。

作者使用了一个例子来说明共享内存。现在我们改变z中的元素,再看看x的元素有没有变化。

1
2
3
4
5
6
z[:, 0] = 5
z, x
# (tensor([[5., 2., 3.],
# [5., 5., 6.],
# [5., 8., 9.]]),
# tensor([5., 2., 3., 5., 5., 6., 5., 8., 9.]))

这个变化是我没想到的,改变了z的元素,x中对应的元素也会改变,即使他们形状不同。

接下来我们看stack方法是如何使用的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
x_stacked = torch.stack([x, x, x, x], dim=0)
x_stacked
# tensor([[5., 2., 3., 5., 5., 6., 5., 8., 9.],
# [5., 2., 3., 5., 5., 6., 5., 8., 9.],
# [5., 2., 3., 5., 5., 6., 5., 8., 9.],
# [5., 2., 3., 5., 5., 6., 5., 8., 9.]])
x_stacked = torch.stack([x, x, x, x], dim=1)
x_stacked
# tensor([[5., 5., 5., 5.],
# [2., 2., 2., 2.],
# [3., 3., 3., 3.],
# [5., 5., 5., 5.],
# [5., 5., 5., 5.],
# [6., 6., 6., 6.],
# [5., 5., 5., 5.],
# [8., 8., 8., 8.],
# [9., 9., 9., 9.]])
x_stacked = torch.stack([x, x, x, x], dim=2)
x_stacked
# IndexError: Dimension out of range (expected to be in range of [-2, 1], but got 2)

在第0个维度上相加,竖着叠出了4行的x张量,在第1个维度上相加,那就是把x竖过来放了。对于向量之间的叠加,就只能在0和1维上进行。可能你以为vstackhstack只是stack方法在dim取0或1时的特例罢了吗,但是事实真的是这样吗?

1
2
3
4
5
6
7
8
x_stacked = torch.vstack([x,x,x])
x_stacked
# tensor([[5., 2., 3., 5., 5., 6., 5., 8., 9.],
# [5., 2., 3., 5., 5., 6., 5., 8., 9.],
# [5., 2., 3., 5., 5., 6., 5., 8., 9.]])
x_stacked = torch.hstack([x,x,x])
x_stacked
# tensor([5., 2., 3., 5., 5., 6., 5., 8., 9., 5., 2., 3., 5., 5., 6., 5., 8., 9.,5., 2., 3., 5., 5., 6., 5., 8., 9.])

注意到,竖直的叠放其实是从上往下叠放,而hstack则是从左往右叠放,在向量中不会出现类似于上面dim=1时的stack方法。实际上,stack方法要求所有的输入张量必须是完全相同的形状,而且输出维度比输入维度多一维。使用stack方法,当dim=1时,要将原来的形状为(9,)的张量的第1维插入4,最终的张量形状就变成了(9,4)

我们再使用一个2维的矩阵,来感受一下。

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
t_a = torch.arange(0,6).reshape(2,3)
t_a
# tensor([[0, 1, 2],
# [3, 4, 5]])
torch.stack([t_a,t_a],dim=0)
# tensor([[[0, 1, 2],
# [3, 4, 5]],
#
# [[0, 1, 2],
# [3, 4, 5]]])
# 形状变成了(2,2,3)
torch.stack([t_a,t_a],dim=1)
# tensor([[[0, 1, 2],
# [0, 1, 2]],
#
# [[3, 4, 5],
# [3, 4, 5]]])
# 形状还是(2,2,3),把新元素的元素插入到每个第1维元素中去、
torch.stack([t_a,t_a],dim=2)
# tensor([[[0, 0],
# [1, 1],
# [2, 2]],
#
# [[3, 3],
# [4, 4],
# [5, 5]]])
# 形状是(2,3,2)

下面这两张图分别是dim等于1和2时候的效果:

第1维的时候就是列,shape中的3被踹到第2维了
第1维的时候就是列,shape中的3被踹到第2维了

反而是dim=2的那个图片看起来更好懂一些
反而是dim=2的那个图片看起来更好懂一些

这些图片都是用TorchCat生成的。

Squeezing, unsqueezing and permuting

接下来我们来看剩下的方法。squeeze方法可以清除掉张量中所有的单维空间。例如

1
2
3
4
5
x_reshaped, x_reshaped.shape
# (tensor([[1., 2., 3., 4., 5., 6., 7., 8., 9.]]), torch.Size([1, 9]))
x_squeezed = x_reshaped.squeeze()
x_squeezed, x_squeezed.shape
# (tensor([1., 2., 3., 4., 5., 6., 7., 8., 9.]), torch.Size([9]))

对应的,unsqueeze方法会在对应的位置添加一个单维。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
x_squeezed, x_squeezed.shape
# (tensor([1., 2., 3., 4., 5., 6., 7., 8., 9.]), torch.Size([9]))
x_unsqueezed = x_squeezed.unsqueeze(dim=0)
x_unsqueezed, x_unsqueezed.shape
# (tensor([[1., 2., 3., 4., 5., 6., 7., 8., 9.]]), torch.Size([1, 9]))
x_unsqueezed = x_squeezed.unsqueeze(dim=1)
x_unsqueezed, x_unsqueezed.shape
# (tensor([[1.],
# [2.],
# [3.],
# [4.],
# [5.],
# [6.],
# [7.],
# [8.],
# [9.]]),
# torch.Size([9, 1]))

最后是permute方法,他能够将张量按照你想要的维度来重排。同时这只是一种视角变换,两个张量是共享内存的。例如:

1
2
3
4
5
6
7
x_original = torch.rand(size=(5, 4, 3))
x_permuted = x_original.permute(2, 0, 1)
x_permuted.shape
# torch.Size([3, 5, 4])
x_original[0, 0, 0] = 728218
x_original[0, 0, 0], x_permuted[0, 0, 0]
# (tensor(728218.), tensor(728218.))

Selecting data (indexing)

这一节中我们学习如何选取数据,就是如何正确的使用pytorch的下标。如果你用过numpy的话,你会发现这二者使用下标其实是差不多的。先介绍一下我们本节要用到的张量。

1
2
3
4
5
6
x = torch.arange(1, 10).reshape(1, 3, 3)
x, x.shape
# (tensor([[[1, 2, 3],
# [4, 5, 6],
# [7, 8, 9]]]),
# torch.Size([1, 3, 3]))

根据你中括号的多少,决定了你是在选择第0维度、第1维还是第2维。

1
2
3
4
5
6
7
8
x[0]
# tensor([[1, 2, 3],
# [4, 5, 6],
# [7, 8, 9]])
x[0][0]
# tensor([1, 2, 3])
x[0][1][1]
# tensor(5)

你可以用:来选择一个维度里面所有的元素。

1
2
3
4
5
x[0,:, 1]
# tensor([2, 5, 8])
# 注意,写x[0][:][1]就和x[0][1]是一样的,解释器会把[:]忽略掉
x[:, 1, 1]
# tensor([5])

其他基础操作

PyTorch and NumPy

pytorch和Numpy可以进行很好的互动,二者之间的数据可以互转。

  • 想把numpy中的数据转成torch,使用torch.from_numpy()
  • 想把torch张量转成numpy数据,使用torch.Tensor.numpy()
1
2
3
4
5
6
7
import torch
import numpy as np

array = np.arange(1.0, 8.0)
tensor = torch.from_numpy(array)
array, tensor
# (array([1., 2., 3., 4., 5., 6., 7.]),tensor([1., 2., 3., 4., 5., 6., 7.], dtype=torch.float64))

注意到转换之后的张量数据类型是float64,因为这是numpy的默认数据类型。

把torch张量转成numpy数组也是同样的。注意到转换之后的数组数据类型是float32,这说明二者互转的时候都不会变动对面的数据类型,原先的数据类型是啥转过来之后还是啥。

1
2
3
4
tensor = torch.ones(7)
numpy_tensor = tensor.numpy()
tensor, numpy_tensor
# (tensor([1., 1., 1., 1., 1., 1., 1.]),array([1., 1., 1., 1., 1., 1., 1.], dtype=float32))

同时,这两个变量并不共享同一块内存。例如:

1
2
3
array = array + 1
array, tensor
# (array([2., 3., 4., 5., 6., 7., 8.]),tensor([1., 2., 3., 4., 5., 6., 7.], dtype=torch.float64))

Reproducibility

为了保证实验可复现,我们需要降低随机性。他在介绍随机种子。神经网络的工作模式是:从随机数开始->张量操作->更新这些数让他们更好的代表数据->继续…

1
2
3
4
5
6
random_tensor_A = torch.rand(3, 4)
random_tensor_B = torch.rand(3, 4)
random_tensor_A == random_tensor_B
# tensor([[False, False, False, False],
# [False, False, False, False],
# [False, False, False, False]])

因此我们需要自己设置随机种子。

1
2
3
4
5
6
7
8
9
RANDOM_SEED = 42
torch.manual_seed(RANDOM_SEED)
random_tensor_C = torch.rand(3, 4)
torch.manual_seed(RANDOM_SEED)
random_tensor_D = torch.rand(3, 4)
random_tensor_C == random_tensor_D
# tensor([[True, True, True, True],
# [True, True, True, True],
# - [True, True, True, True]])

当然了,如果你忘了更新随机种子的话,同一个种子产生的两个不同变量依旧是不相等的,我怀疑是你每次十三薪设置种子就会将随机数生成器的历史清空,这样他生成的永远是这个种子的第一个随机数。

1
2
3
4
5
6
7
8
RANDOM_SEED = 42
torch.manual_seed(RANDOM_SEED)
random_tensor_C = torch.rand(3, 4)
random_tensor_D = torch.rand(3, 4)
random_tensor_C == random_tensor_D
# tensor([[False, False, False, False],
# [False, False, False, False],
# [False, False, False, False]])

Accessing a GPU

在这一节我们将学习如何在GPU上运行pytorch以加快运行速度。在没有CUDA出现之前可以说神经网络远没有现在这么发达,用显卡计算神经网络真是一个天才的想法。

首先先看看怎么获取GPU。有以下几种方法:

  • 最简单:使用Google Colab以获取免费GPU,这是本视频作者使用的方式。
  • 用自己的显卡:需要一些设置,并且你需要掏钱,有很多选项,如果我以后有钱我可能真的会掏钱买一个本地跑的大模型(突然想起来视频发布的年代还没有大模型,还是挺令人感叹的,时间过得真快,我已经不知道在没有大模型之前到底是如何工作和学习的了)。
  • 使用云计算:例如GCP,AWS,Azure。可惜我只知道autodl。

在本系列教程中我应该就直接用mps了,毕竟也不跑很大的模型,直接这么用就行了。

在命令行中,查看你的英伟达显卡配置使用的是nvidia-smi命令,想要在jupyter notebook中使用命令行的话,只需要在命令前面加一个!即可。

1
!nvidia-smi

作者使用的是 Tesla T100,这个显卡说实话我都没听过。我也好奇那些算力集群里用的是公版显卡还是厂商自定的显卡。

查看你的环境中是否能使用CUDA(或者mps),只需要下面这个方法:

1
2
3
4
5
6
7
torch.cuda.is_available()
# False
torch.mps.is_available()
# True
torch.cpu.is_available()
# True
# 我也很好奇这个会有返回false的时候吗。

在日常写的时候,我们总是希望能够写出设备无关的代码,所以标准的设备无关变量就是这样的。

1
2
3
device = "cuda" if torch.cuda.is_available()  else "cpu"
device = "cuda" if torch.cuda.is_available() else "mps" if torch.mps.is_available() else "cpu"
# 添加上mps也可以,但是如果mps也能运行的话才这么填,毕竟大多数算子都是为CUDA打造的。

你也可以通过方法来查看自己当前环境下的GPU数量。

1
2
torch.cuda.device_count()
# 当然我现在的环境肯定是0了。

pytorch官网在这里给出了一份最佳实践,非常适合单文件脚本运行的时候使用。

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

parser = argparse.ArgumentParser(description='PyTorch Example')
# 这里的description会在--help中显示
parser.add_argument('--disable-cuda', action='store_true',
help='Disable CUDA')
# 出现就设为true,不出现就是false
args = parser.parse_args()
args.device = None
if not args.disable_cuda and torch.cuda.is_available():
args.device = torch.device('cuda')
else:
args.device = torch.device('cpu')

Setting up device agnostic code

在这节我们来看如何实际使用这块显卡。在默认创建变量的时候,张量都是在CPU中的,我们可以通过查看device字段来查看它在哪个设备上运算。

1
2
3
tensor = torch.tensor([1, 2, 3])
tensor, tensor.device
# (tensor([1, 2, 3]), device(type='cpu'))

那么如何将张量移动到对应的设备上呢?也非常的简单,直接用to方法即可,如果设备不存在的话它就会直接报错了。

1
2
3
4
5
tensor_on_gpu = tensor.to(device)
tensor_on_gpu
# tensor([1, 2, 3], device='mps:0')
tensor_on_gpu = tensor.to("cuda")
# AssertionError: Torch not compiled with CUDA enabled

当张量在GPU上时,是不能将其转为numpy数组的,因为它存在了显存中,而numpy变量是进不了显存的,都存在了内存中。当出现这种情况的时候,只需要将其复制到cpu就好了,使用cpu()方法即可。

1
2
3
4
5
6
7
8
tensor_on_gpu.numpy()
# TypeError: can't convert mps:0 device type tensor to numpy. Use Tensor.cpu() to copy the tensor to host memory first.
tensor_back_on_cpu = tensor_on_gpu.cpu().numpy()
tensor_back_on_cpu
# array([1, 2, 3])
tensor_on_gpu
# tensor([1, 2, 3], device='mps:0')
# 注意到cpu方法不会把已经在显存中的张量移走,只有to方法会这么做。

总结

这一部分是视频的Chapter 0,讲了很多的基础操作,为接下来的正式流程做了铺垫。但是视频上讲的东西都有,同时视频节奏又太慢,所以接下来我就不看视频了,直接看对应的notebook了。

  • 标题: PyTorch for Deep Learning & Machine Learning – Full Course(1) - NoTe0x006
  • 作者: Baobao0824
  • 创建于 : 2026-06-02 14:32:30
  • 更新于 : 2026-06-02 14:32:30
  • 链接: https://blog.baobao0824.top//学习笔记NoTe/NT-0x006/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论