PyTorch从入门到实践 | (2) Tensor

本文深入探讨PyTorch中的张量概念,包括基础操作、与NumPy的关系、内部结构等,通过实例演示如何利用张量进行高效计算,适合初学者和进阶者阅读。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Tensor,又名张量,它不仅在PyTorch中出现过,它也是Theano、TensorFlow、 Torch和MxNet中重要的数据结构。关于张量的本质不乏深度的剖析,但从工程角度来讲,可简单地认为它就是一个数组,且支持高效的科学计算。它可以是一个数(标量)、一维数组(向量)、二维数组(矩阵)和更高维的数组(高阶数据)。Tensor和Numpy的ndarrays类似,但PyTorch的tensor支持GPU加速。

本篇博客将就一些常用的内容做一些介绍,更多用法可以通过在Notebook中使用函数名?的形式查看或查看PyTorch1.0官方文档:英文官方文档中文官方文档

目录

1. 基础操作

2. Tensor和NumPy

3. 内部结构

4. 其他有关tensor的话题

5. 线性回归


1. 基础操作

tensor的接口和numpy数组的接口非常类似:

从接口的角度来讲,对tensor的操作可分为两类:

1)torch.function,如torch.save等。

2)另一类是tensor.function,如tensor.view等。

为方便使用,对tensor的大部分操作同时支持这两类接口,在本博客中不做具体区分,如torch.add(a,b)与a.add(b)功能等价。

而从存储的角度来讲,对tensor的操作又可分为两类:

1) 不会修改自身的数据,如 a.add(b), 加法的结果会返回一个新的tensor。

2) 会修改自身的数据,如 a.add_(b), 加法的结果仍存储在a中,a被修改了。函数名以_结尾的都是inplace方式, 即会修改调用者自己的数据,在实际应用中需加以区分。

# Let's begin
from __future__ import print_function
import torch  
torch.__version__
  • 创建tensor

常用创建tensor的方法:

这些创建方法都可以在创建的时候指定数据类型dtype和在什么device上创建(cpu/gpu).

其中使用Tensor函数新建tensor是最复杂多变的方式,它既可以接收一个list,并根据list的数据新建tensor,也能根据指定的形状新建tensor,还能传入其他的tensor,下面举几个例子。

# 指定tensor的形状
a = torch.Tensor(2, 3)
print(a.type())   #Tensor默认是FloatTensor
a # 数值取决于内存空间的状态,print时候可能overflow

# 指定tensor的形状
a = torch.Tensor(1)
print(a.type())   #Tensor默认是FloatTensor
a # 数值取决于内存空间的状态,print时候可能overflow

# 用list的数据创建tensor
b = torch.Tensor([[1,2,3],[4,5,6]])
print(b)
b = torch.Tensor([1])
print(b)
print(b.tolist()) # 把tensor转为list

tensor.size()返回torch.Size对象,它是tuple的子类,但其使用方式与tuple略有区别:

b = torch.Tensor([[1,2,3],[4,5,6]])
b_size = b.size()
print(b_size) #shape
print(b_size[1]) #列数
print(b.size(1)) #列数
print(b.size()[1]) #列数
print(b.numel()) #b中的元素个数
print(b.nelement())#b中的元素个数

# 创建一个和b形状一样的tensor
c = torch.Tensor(b_size)
# 创建一个元素为2和3的tensor
d = torch.Tensor((2, 3))
# 创建一个形状为2,3的tensor
e = torch.Tensor(2,3)
c,d,e

除了tensor.size(),还可以利用tensor.shape直接查看tensor的形状,tensor.shape等价于tensor.size():

print(c.size()) #方法
print(c.shape) #属性
print(c.shape[1])

需要注意的是,torch.Tensor(*sizes)创建tensor时,系统不会马上分配空间,只是会计算剩余的内存是否足够使用,使用到tensor时才会分配,而其它操作都是在创建完tensor之后马上进行空间分配。其它常用的创建tensor的方法举例如下。

print(torch.ones(2,3))
print(torch.zeros(2,3))
print(torch.arange(1,8,2))
print(torch.linspace(1,100,3))
print(torch.randn(2,3,device=torch.device('cpu'))) #在cpu上创建 randn是标准正态分布 均值0,标准差1
print(torch.randperm(5)) #长度为5的随机排列
print(torch.eye(2,3,dtype=torch.int)) #对角线为1 其余为0 不必行列数相同

torch.tensor是在0.4版本之后新增加的一个新版本的创建tensor方法,使用的方法,和参数几乎和np.array完全一致:

scalar = torch.tensor(3.14) #标量转换为tensor
print(scalar)
print(scalar.shape) #0-dim

vector = torch.tensor([1,2])
print(vector)
print(vector.shape)  #1-dim
a = torch.Tensor(1,2)  #注意区别 1,2表示形状
print(a.shape)

matrix = torch.tensor([[0.1,0.2],[2.3,3.3],[4.5,5.5]])
print(matrix,matrix.shape)  #2-dim

a = torch.tensor([[1.111,2.222,3.333]],dtype=torch.float64,device=torch.device("cpu"))
a.shape,a

empty_tensor = torch.tensor([])
empty_tensor.shape

  • 常用tensor操作

通过tensor.view方法可以调整tensor的形状,但必须保证调整前后元素总数一致。view不会修改自身的数据,返回的新tensor与源tensor共享内存,也即更改其中的一个,另外一个也会跟着改变。在实际应用中可能经常需要添加或减少某一维度,这时候squeezeunsqueeze两个函数就派上用场了。

a = torch.arange(0,8)
print(a.view(2,4))
b = a.view(-1,4) #-1会自动计算大小
print(b.shape)

print(b.shape)
print(b.unsqueeze(1).shape) # 注意形状,在第1维(下标从0开始)上增加“1” 
#等价于 b[:,None]
print(b[:, None].shape)
print(b)
print(b.unsqueeze(-2)) #倒数第二个维度 添加1维

print(b.shape)
c = b.view(1,1,2,4)
print(c.shape,c)
print(c.squeeze(0).shape,c.squeeze(0)) #压缩第0维的 “1“
print(c.squeeze().shape,c.squeeze()) #把所有维度为"1"的进行压缩

print(a)
print(b)
a[0] = 10000
print(a)  #a,b共享内存
print(b) 

 

resize是另一种可用来调整size的方法,但与view不同,它可以修改tensor的大小。如果新大小超过了原大小,会自动分配新的内存空间,而如果新大小小于原大小,则之前的数据依旧会被保存,看一个例子。

print(b)
print(b.shape)
b.resize_(1,4) #函数后加_ 意味着对调用者本身进行修改 新大小小于原大小
print(b) 
b.resize_(3,4) #旧的数据依旧保存着,多出的大小会分配新空间
b

  • 索引操作

Tensor支持与numpy.ndarray类似的索引操作,语法上也类似,下面通过一些例子,讲解常用的索引操作。如无特殊说明,索引出来的结果与原tensor共享内存,也即修改一个,另一个会跟着修改。

a = torch.randn(3,4)
print(a)
print(a[0]) #第1行
print(a[:,0]) #第一列
print(a[0,2]) #第1行第3个元素
print(a[0][2]) #第1行第3个元素
print(a[0,-1]) #第1行最后一个元素
print(a[:2]) #前2行
print(a[:2,0:2]) #前两行两列交汇

# 第0行,前两列 
print(a[0:1, :2]) 
print(a[0:1,:2].shape)
print(a[0, :2]) # 注意两者的区别:形状不同 用切片索引会保持形状 整数索引不会
print(a[0,:2].shape)

print(a.shape)
# None类似于np.newaxis, 为a新增了一个轴
# 等价于a.view(1, a.shape[0], a.shape[1]) a.unsqueeze(0)
a[None].shape #等价于 a[None,:,:]

print(a.shape)
print(a[:,None,:].shape)
print(a[:,None,:,None,None].shape)

print(a)
print(a>1)  
print(a[a>1]) #等价于a.masked_select(a>1)  #这种布尔型索引选择的结果和原tensor不共享内存

print(a)
print(a[torch.LongTensor([0,1])]) #第1,2行
print(a[[0,1]])

常用选择函数:

gather是一个比较复杂的操作,对一个2维tensor,输出的每个元素如下:

三维tensor的gather操作同理,下面举几个例子。

a = torch.arange(0,16).view(4,4)
print(a)
#选取对角线元素
index = torch.LongTensor([[0,1,2,3]])
print(a.gather(0,index))
#选取反对角线
index = torch.LongTensor([[3,2,1,0]]).t() #1,4 -> 4,1
print(index)
print(a.gather(1,index)) 
# 选取反对角线上的元素,注意与上面的不同
index = torch.LongTensor([[3,2,1,0]])
print(a.gather(0, index))
# 选取两个对角线上的元素
index = torch.LongTensor([[0,1,2,3],[3,2,1,0]]).t()
b = a.gather(1, index)
print(b)


gather相对应的逆操作是scatter_gather把数据从input中按index取出,而scatter_是把取出的数据再放回去。注意scatter_函数是inplace操作。

# 把两个对角线元素放回去到指定位置
c = torch.zeros(4,4)
c.scatter_(1, index, b)


对tensor的任何索引操作仍是一个tensor,想要获取标准的python对象数值,需要调用tensor.item(), 这个方法只对包含一个元素的tensor适用.

print(a)
print(a[0,0])
print(a[0,0].item())
d = a[0:1,0:1,None]
print(d.shape)
print(d)
print(d.item())# 只包含一个元素的tensor即可调用tensor.item,与形状无关

  • 高级索引

PyTorch完善了索引操作,目前已经支持绝大多数numpy的高级索引。高级索引可以看成是普通索引操作的扩展,但是高级索引操作的结果一般不和原始的Tensor共享内存。

x = torch.arange(27).view(3,3,3)
print(x)
print(x[[1, 2], [1, 2], [2, 0]]) # x[1,1,2]和x[2,2,0]
print(x[[2, 1, 0], [0], [1]]) # x[2,0,1],x[1,0,1],x[0,0,1]
print(x[[0, 2], ...]) # x[0] 和 x[2]

  • tensor类型

Tensor有不同的数据类型,如下所示,每种类型分别对应有CPU和GPU版本(HalfTensor除外)。默认的tensor是FloatTensor,可通过torch.set_default_tensor_type 来修改默认tensor类型(如果默认类型为GPU tensor,则所有操作都将在GPU上进行)。Tensor的类型对分析内存占用很有帮助。例如对于一个size为(1000, 1000, 1000)的FloatTensor,它有1000*1000*1000=10^9个元素,每个元素占32bit/8 = 4Byte内存,所以共占大约4GB内存/显存。HalfTensor是专门为GPU版本设计的,同样的元素个数,显存占用只有FloatTensor的一半,所以可以极大缓解GPU显存不足的问题,但由于HalfTensor所能表示的数值大小和精度有限https://stackoverflow.com/questions/872544/what-range-of-numbers-can-be-represented-in-a-16-32-and-64-bit-ieee-754-syste↩,所以可能出现溢出等问题。

各数据类型之间可以互相转换,type(new_type)是通用的做法,同时还有floatlonghalf等快捷方法。CPU tensor与GPU tensor之间的互相转换通过tensor.cudatensor.cpu方法实现,此外还可以使用tensor.to(device)。Tensor还有一个new方法,用法与torch.Tensor一样,会调用该tensor对应类型的构造函数,生成与当前tensor类型一致的tensor。torch.*_like(tensora)可以生成和tensora拥有同样属性(类型,形状,cpu/gpu)的新tensor。 tensor.new_*(new_shape) 新建一个不同形状的tensor。

# 设置默认tensor,注意参数是字符串
torch.set_default_tensor_type('torch.DoubleTensor')

a = torch.Tensor(2,3)
print(a.dtype) # 现在a是DoubleTensor,dtype是float64

# 恢复之前的默认设置
torch.set_default_tensor_type('torch.FloatTensor')
# 把a转成FloatTensor,等价于b=a.type(torch.FloatTensor)
b = a.float() 
print(b.dtype)

c = a.type_as(b) #把a赋给c 类型是b的类型
c,c.dtype

print(a.dtype)
print(a.new(2,3)) #生成与a类型一致的tensor 等价于torch.DoubleTensor(2,3),建议使用a.new_tensor

print(torch.zeros_like(a)) #创建与a 形状 类型 device一样的值为0的tensor 
#等价于t.zeros(a.shape,dtype=a.dtype,device=a.device)
print(torch.zeros_like(a, dtype=torch.int16)) #可以修改某些属性

print(torch.rand_like(a)) #创建与a 形状 类型 device一样的值为0-1随机数的tensor

print(a.new_ones(4,5, dtype=torch.int) )#新建一个不同形状的值全为1 dtype为int的tensor
print(a.new_tensor([3,4]))#新建值为[3,4]的tensor dtype、device等属性和a一样

  • 逐元素操作

这部分操作会对tensor的每一个元素(point-wise,又名element-wise)进行操作,此类操作的输入与输出形状一致。

对于很多操作,例如div、mul、pow、fmod等,PyTorch都实现了运算符重载,所以可以直接使用运算符。如a ** 2 等价于torch.pow(a,2)或a.pow(2)a * 2等价于torch.mul(a,2)或a.mul(2)

其中clamp(x, min, max)的输出满足以下公式:

clamp常用在某些需要比较大小的地方,如取一个tensor的每个元素与另一个数的较大值。

a = torch.rand(6).view(2,3)   #rand返回0-1之间均匀分布的随机数
#cos的输入类型是浮点型
print(torch.cos(a))
print(a.cos())

print(a%3)   #torch.fmod(a,3)    a.fmod(3) 
print(a**2)  #torch.pow(a,2)     a.pow(2)

print(a)
#取a中的每一个元素与3相比较大的一个 (小于3的截断成3)
print(torch.clamp(a,min=3))
print(a.clamp(min=3))
print(a.clamp_(min=3))#加_后 对自身修改
print(a) 

b = a.sin_()     #等价于 a = a.sin() b=a  效率更高
a
  • 聚合操作

此类操作会使输出形状小于输入形状,并可以沿着某一维度进行指定操作。如加法sum,既可以计算整个tensor的和,也可以计算tensor中每一行或每一列的和。常用的归并操作如下表所示。

常用聚合操作:

以上大多数函数都有一个参数dim,用来指定这些操作是在哪个维度上执行的。关于dim(对应于Numpy中的axis)的解释众说纷纭,这里提供一个简单的记忆方式:

假设输入的形状是(m, n, k)

size中是否有"1",取决于参数keepdimkeepdim=True会保留维度1()。注意,以上只是经验总结,并非所有函数都符合这种形状变化方式,如cumsum

b = torch.ones(2,3)
print(b.sum(dim=0,keepdim=True))
print(b.sum(dim=0,keepdim=False))
print(b.sum(dim=1)) #默认keepdim=False

a = torch.arange(6).view(2,3)
print(a)
print(a.cumsum(dim=1)) #沿着行进行累加 x轴方向

  • 比较

比较函数中有一些是逐元素比较,操作类似于逐元素操作,还有一些则类似于聚合操作。

常用比较函数:

表中第一行的比较操作已经实现了运算符重载,因此可以使用a>=ba>ba!=ba==b,其返回结果是一个ByteTensor,可用来选取元素。max/min这两个操作比较特殊,以max来说,它有以下三种使用情况:

1)torch.max(tensor):返回tensor中最大的一个数

2)torch.max(tensor,dim):指定维上最大的数,返回tensor和下标

3)torch.max(tensor1, tensor2): 比较两个tensor相比较大的元素

至于比较一个tensor和一个数,可以使用clamp函数。下面举例说明。

a = torch.linspace(0, 15, 6).view(2, 3)
print(a)
b = torch.linspace(15, 0, 6).view(2, 3)
print(b)
print(a>b) #torch.gt(a,b)  a.gt(b)
print(a[a>b]) #取a中大于b的元素

print(torch.max(a)) #a中最大的元素 a.max()
print(torch.max(b,dim=1)) #b中每行最大的元素以及所在位置的索引 b.max(dim=1)
# 第一个返回值的15和6分别表示第0行和第1行最大的元素
# 第二个返回值的0和0表示上述最大的数是该行第0个元素

print(torch.max(a,b)) #a.max(b) 返回a,b同位置最大的元素
print(torch.clamp(a,min=10))  #比较a中元素和10 <10的赋为10 a.clamp(min=10)

  • 线性代数

PyTorch的线性函数主要封装了Blas和Lapack,其用法和接口都与之类似。

常用的线性代数函数:

具体使用说明请参见官方文档,需要注意的是,矩阵的转置会导致存储空间不连续,需调用它的.contiguous方法将其转为连续。

print(a)
print(a.shape)
b = a.t()  #torch.t(a)
print(b.is_contiguous())
print(b)
print(b.shape)
print(b.contiguous())
b = b.contiguous()
print(b.is_contiguous())

 

2. Tensor和NumPy

Tensor和Numpy数组之间具有很高的相似性,彼此之间的互操作也非常简单高效。需要注意的是,Numpy和Tensor共享内存。由于Numpy历史悠久,支持丰富的操作,所以当遇到Tensor不支持的操作时,可先转成Numpy数组,处理后再转回tensor,其转换开销很小。

import numpy as np
a = np.ones((2, 3),dtype=np.float32)
print(a)
b = torch.from_numpy(a) #数组转tensor
print(b)
#也可以直接把numpy对象 传给tensor
b = torch.Tensor(a)
print(b)

a[0,1] = 100
print(b) #共享内存
c = b.numpy() #tensor 转为numpy
print(c)  #a,b,c共享内存

当numpy的数据类型和Tensor的类型不一样的时候,数据会被复制,不会共享内存。

a = np.ones((2,3))
print(a.dtype)
b = torch.Tensor(a)  #Tensor默认FloatTensor float32  此处进行拷贝,不共享内存
print(b.dtype)
c = torch.from_numpy(a) #注意c的类型 Doubletensor
print(c.dtype)

a[0,1] = 100
print(b)  # b与a不共享内存,所以即使a改变了,b也不变
print(c)  # c与a共享内存

不论输入的类型是什么,torch.tensor都会进行数据拷贝,不会共享内存

tensor = torch.tensor(a) #tensor的dtype和a的一样
print(tensor)
tensor[0,0] = -99
print(a)

广播法则(broadcast)是科学运算中经常使用的一个技巧,它在快速执行向量化的同时不会占用额外的内存/显存。 Numpy的广播法则定义如下:

PyTorch当前已经支持了自动广播法则,但是笔者还是建议读者通过以下两个函数的组合手动实现广播法则,这样更直观,更不易出错:

注意,repeat实现与expand相类似的功能,但是repeat会把相同数据复制多份,因此会占用额外的空间。

a = torch.ones(3, 2)
b = torch.zeros(2, 3,1)
# 自动广播法则
# 第一步:a是2维,b是3维,所以先在较小的a前面补1 ,
#               即:a.unsqueeze(0),a的形状变成(1,3,2),b的形状是(2,3,1),
# 第二步:   a和b在第一维和第三维形状不一样,其中一个为1 ,
#               可以利用广播法则扩展,两个形状都变成了(2,3,2)
a+b

# 手动广播法则
# 或者 a.view(1,3,2).expand(2,3,2)+b.expand(2,3,2)
# 或者 a.unsqueeze(0).expand(2,3,2)+b.expand(2,3,2)
a[None].expand(2, 3, 2) + b.expand(2,3,2)

# expand不会占用额外空间,只会在需要的时候才扩充,可极大节省内存
e = a.unsqueeze(0).expand(10000000000000, 3,2)

3. 内部结构

tensor的数据结构如下图所示。tensor分为头信息区(Tensor)和存储区(Storage),信息区主要保存着tensor的形状(size)、步长(stride)、数据类型(type)等信息,而真正的数据则保存成连续数组。由于数据动辄成千上万,因此信息区元素占用内存较少,主要内存占用则取决于tensor中元素的数目,也即存储区的大小。

一般来说一个tensor有着与之相对应的storage, storage是在data之上封装的接口,便于使用,而不同tensor的头信息一般不同,但却可能使用相同的数据。下面看两个例子。

a = torch.arange(6) #默认长整数 int64
print(a.storage())
b = a.view(2,3)
print(b.storage())
# 一个对象的id值可以看作它在内存中的地址
# storage的内存地址一样,即是同一个storage
id(b.storage()) == id(a.storage())
# a改变,b也随之改变,因为他们共享storage
a[1] = 100
print(b)

c = a[2:]
print(a.dtype)
print(c)
print(c.storage())
print(c.data_ptr(), a.data_ptr())# data_ptr返回tensor首元素的内存地址
# 可以看出相差16,这是因为2*8=16--相差两个元素,每个元素占8个字节(int64)
c[0] = -100
print(a) # c[0]的内存地址对应a[2]的内存地址

e = b[::2,::2]# 隔2行/列取一个元素
print(b)
print(e)
# 下面4个tensor共享storage
print(id(a.storage()) == id(b.storage()) == id(c.storage()) == id(e.storage()))
print(a.storage_offset(), c.storage_offset(), e.storage_offset())
print(b.stride(), e.stride())
print(e.is_contiguous())

可见绝大多数操作并不修改tensor的数据,而只是修改了tensor的头信息。这种做法更节省内存,同时提升了处理速度。在使用中需要注意。 此外有些操作会导致tensor不连续,这时需调用tensor.contiguous方法将它们变成连续的数据,该方法会使数据复制一份,不再与原来的数据共享storage。 另外读者可以思考一下,之前说过的高级索引一般不共享stroage,而普通索引共享storage,这是为什么?(提示:普通索引可以通过只修改tensor的offset,stride和size,而不修改storage来实现)。

 

4. 其他有关tensor的话题

  • GPU/CPU

tensor可以很随意的在gpu/cpu上传输。使用tensor.cuda(device_id)或者tensor.cpu()。另外一个更通用的方法是tensor.to(device)

a = torch.randn(3,4)
print(a.device)


if torch.cuda.is_available():
    a = torch.randn(3,4, device=torch.device('cuda:1'))
    # 等价于
    # a.torch.randn(3,4).cuda(1)
    # 但是前者更快
    a.device
device = torch.device('cpu')
a.to(device)

尽量使用tensor.to(device), 将device设为一个可配置的参数,这样可以很轻松的使程序同时兼容GPU和CPU

数据在GPU之中传输的速度要远快于内存(CPU)到显存(GPU), 所以尽量避免频繁的在内存和显存中传输数据。

  • 持久化

Tensor的保存和加载十分的简单,使用torch.save和torch.load即可完成相应的功能。在save/load时可指定使用的pickle模块,在load时还可将GPU tensor映射到CPU或其它GPU上。

if torch.cuda.is_available():
    a = a.cuda(1) # 把a转为GPU1上的tensor,
    torch.save(a,'a.pth')

    # 加载为b, 存储于GPU1上(因为保存时tensor就在GPU1上)
    b = torch.load('a.pth')
    # 加载为c, 存储于CPU
    c = torch.load('a.pth', map_location=lambda storage, loc: storage)
    # 加载为d, 存储于GPU0上
    d = torch.load('a.pth', map_location={'cuda:1':'cuda:0'})
  • 向量化

向量化计算是一种特殊的并行计算方式,相对于一般程序在同一时间只执行一个操作的方式,它可在同一时间执行多个操作,通常是对不同的数据执行同样的一个或一批指令,或者说把指令应用于一个数组/向量上。向量化可极大提高科学运算的效率,Python本身是一门高级语言,使用很方便,但这也意味着很多操作很低效,尤其是for循环。在科学计算程序中应当极力避免使用Python原生的for循环

def for_loop_add(x, y):
    result = []
    for i,j in zip(x, y):
        result.append(i + j)
    return torch.Tensor(result)
x = torch.zeros(10000)
y = torch.ones(10000)
%timeit -n 10 for_loop_add(x,y)
%timeit -n 10 x+y

可见二者有超过几十倍的速度差距,因此在实际使用中应尽量调用内建函数(buildin-function),这些函数底层由C/C++实现,能通过执行底层优化实现高效计算。因此在平时写代码时,就应养成向量化的思维习惯,千万避免对较大的tensor进行逐元素遍历。

此外还有以下几点需要注意:

1) 大多数torch.function都有一个参数out,这时候产生的结果将保存在out指定tensor之中。

2) torch.set_num_threads可以设置PyTorch进行CPU多线程并行计算时候所占用的线程数,这个可以用来限制PyTorch所占用的CPU数目。

3) torch.set_printoptions可以用来设置打印tensor时的数值精度和格式。 下面举例说明。

a = torch.arange(0, 20000000)
print(a.dtype)  #现在arange默认是int64 LongTensor
print(a[-1], a[-2]) #之前是32bit的IntTensor 会精度有限导致溢出
b = torch.LongTensor()
torch.arange(0, 20000000, out=b) # 64bit的LongTensor不会溢出
b[-1],b[-2]

a = torch.randn(2,3)
print(a)
torch.set_printoptions(precision=10)
print(a)

 

5. 线性回归

线性回归是机器学习入门知识,应用十分广泛。线性回归利用数理统计中回归分析,来确定两种或两种以上变量间相互依赖的定量关系的,其表达形式为y=wx+b+e,e为误差服从均值为0的正态分布。首先让我们来确认线性回归的损失函数:

然后利用随机梯度下降法更新参数w和b来最小化损失函数,最终学得w和b的数值.

import torch 
%matplotlib inline
from matplotlib import pyplot as plt
from IPython import display


device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

# 设置随机数种子,保证在不同电脑上运行时下面的输出一致
torch.manual_seed(1000) 

def get_fake_data(batch_size=8):
    ''' 产生随机数据:y=x*2+3,加上了一些噪声'''
    x = torch.rand(batch_size, 1, device=device) * 5
    y = x * 2 + 3 +  torch.randn(batch_size, 1, device=device)
    return x, y
# 来看看产生的x-y分布
x, y = get_fake_data(batch_size=16)
plt.scatter(x.squeeze().cpu().numpy(), y.squeeze().cpu().numpy())

# 随机初始化参数
w = torch.rand(1, 1).to(device) #w = torch.rand(1,1,device=device)
b = torch.zeros(1, 1).to(device)

lr =0.02 # 学习率

for ii in range(500):
    x, y = get_fake_data(batch_size=4) #x (4,1) y(4,1)
    
    # forward:计算loss
    y_pred = x.mm(w) + b.expand_as(y) # x@W等价于x.mm(w);for python3 only
    loss = 0.5 * (y_pred - y) ** 2 # 均方误差
    loss = loss.mean()
    
    # backward:手动计算梯度
    dloss = 1
    dy_pred = dloss * (y_pred - y)
    
    dw = x.t().mm(dy_pred)
    db = dy_pred.sum()
    
    # 更新参数
    w.sub_(lr * dw)
    b.sub_(lr * db)
    
    if ii%50 ==0:
       
        # 画图
        display.clear_output(wait=True)
        x = torch.arange(0, 6,dtype=torch.float32).view(-1, 1)
        y = x.mm(w) + b.expand_as(x)
        plt.plot(x.cpu().numpy(), y.cpu().numpy()) # predicted
        
        x2, y2 = get_fake_data(batch_size=32) 
        plt.scatter(x2.numpy(), y2.numpy()) # true data
        
        plt.xlim(0, 5)
        plt.ylim(0, 13)
        plt.show()
        plt.pause(0.5)
        
print('w: ', w.item(), 'b: ', b.item())

可见程序已经基本学出w=2、b=3,并且图中直线和数据已经实现较好的拟合。

虽然上面提到了许多操作,但是只要掌握了这个例子基本上就可以了,其他的知识,读者日后遇到的时候,可以再看看这部份的内容或者查找对应文档。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值