Pytorch中新建Tensor以及其操作记录


前言

最近在尝试用Pytorch实现Transformer,这里记录下我在复现时遇到的bug以及Pytorch一些语法技巧等~


一、代码最顶上一般都写上import的包

import torch
import numpy as np
import torch.nn.functional as F
import matplotlib
matplotlib.use('agg')
import matplotlib.pyplot as plt

1.调用matplotlib时遇到no module name Tkinter的bug:

这里使用matplotlib.use(‘agg’)是因为服务器上直接调用matplotlib.pyplot会报no module named Tkinter的错误,而安装Tkinter对我这种非root用户很麻烦,因此使用 matplotlib.use(‘agg’) 语句可以避免这个错误,但美中不足是因此就不无法使用plt.show()函数,而是得用plt.savefig()来保存图片后再去看图片~

如果自己是root用户可以直接用yum或者pip来下载的话,推荐参考这篇博客自行安装Tkinter。

二、Pytorch中相关Tensor的技巧

1.新建一个Tensor

如果需要新建一个全是0的Tensor

pe = torch.zeros(5, 4) 

在这里插入图片描述
如果需要直接按自己想法初始化里面的数值

pe=torch.Tensor([[1,2,3,4,5],[4,5,6,7,8],[7,8,9,10,11]])

在这里插入图片描述
这是个二维的Tensor,当然,类似的也可以定义三维Tensor或者四维Tensor,比如

a = torch.randn(4, 3, 28, 28) #四维Tensor,其中维度分别为4,3,28,28

2.Tensor的数据选择及跳步

这里用这个pe为例子演示

pe=torch.Tensor([[1,2,3,4,5],[4,5,6,7,8],[7,8,9,10,11]])

在这里插入图片描述

按维度选择数据

pe0=pe[:,0:2] #这里第一个:表示第一个维度全选,0:2表示选择第二个维度的第0到第2列内容(不包括第2列)不同维度中用','隔开
# 这里pe0就是对pe的行全选,并只其中第0和第1列的数据

在这里插入图片描述
按维度进行跳步的选择数据

pe1=pe[:,0::2] #第一个:表示行数据全选,而0::2表示从第0列开始到最后一列,隔一个数选一个数据
# 这也可用来表示选择偶数列的数据

在这里插入图片描述

pe2=pe[:,1::2] #第一个:表示行数据全选,而1::2表示从第1列开始到最后一列,隔一个数选一个数据
# 这也可用来表示选择奇数列的数据

在这里插入图片描述

pe2=pe[:,0:3:2] #第一个:表示行数据全选,而0:3:2表示从第0列开始到第3列,隔一个数选一个数据
# 这样就可以自己定范围了

在这里插入图片描述

3. size()函数

继续使用pe为例子演示

在这里插入图片描述

pe.size() #直接输出pe的矩阵大小信息

在这里插入图片描述
当然,也可以在size()函数中输入来控制输出信息,比如

pe.size(0) #输出pe矩阵第0维信息,这里直接输出3
pe.size(1) #输出pe矩阵第1维信息,这里输出5
pe.size(-1) #输出pe矩阵最后一维的信息,这里输出5

4. Transpose()和Permute()

transpose函数和permute函数都是用来转置矩阵数据的,两者最大的不同就是transpose只能转置矩阵中的两维,permute可以转置多维,可以看以下例子:

这里使用一个随机生成的三维pe数据来为例子(使用randint函数,其中0表示最低值,10表示最高值(取不到),(2,3,4)是生成矩阵的大小)

pe = torch.randint(0,10,(2,3,4)) 
#randint(low=0, high, size, out=None, dtype=None)

在这里插入图片描述

pe.transpose(1,2)

在这里插入图片描述
这里transpose是转置第1,2维,第0维没变,并且pe.transpose(1,2)和pe.transpose(2,1)输出是一样的,所以输入值的位置对transpose函数输出无影响,transpose只是把输入值对应的维度相互转置,并且transpose函数只能传入两个维度信息。

permute函数则更能操作高维的矩阵转置,核心思想是:把输入值对应的维度信息转置到输入值的位置对应的维度上。比较拗口,继续用pe为例:

pe.permute(0,1,2)

在这里插入图片描述
可以看出,pe没变,因为permute(0,1,2),第0维用第0维数据,第1维用第1维数据,第2维用第2维数据,因此没变~

pe.permute(0,2,1)

在这里插入图片描述
permute(0,2,1),及第0维不变,第1维用第2维数据,第2维用第1维数据,实现了与transpose(2,1)一样的功能~

接下来继续看permute(1,2,0)

pe.permute(1,2,0)

在这里插入图片描述

这样看值可能有点不直观,我们直接看他的尺寸可以更容易理解

pe.permute(1,2,0).size()

在这里插入图片描述

原本pe尺寸为(2,3,4),经过permute(1,2,0)后尺寸变为(3,4,2),第0维和第1位换位了,第1维和第2维换了,第2维和第1维换了,这就很直白了,所以permute核心思想是:把输入值对应的维度信息转置到输入值的位置对应的维度上,因此permute函数能处理矩阵的高维转置。

由此,transpose函数只能转置两个维度,但permute能转置多个维度;transpose只需也只能传入两个值,而permute得把矩阵所有维度信息都传入才能运行~

5. view()

view函数的作用为重构张量的维度,在日常对Tensor处理中使用较多的用法有torch.view(参数a,参数b…)view(-1),以及 view(参数a,-1…) 的用法,这里直接用例子阐明:

pe = torch.randint(0,5,(1,2,3)) 
#randint(low=0, high, size, out=None, dtype=None)
>>>tensor([[[1, 0, 2],
            [1, 1, 0]]])
pe.view(1,1,6)
>>>tensor([[[1, 0, 2, 1, 1, 0]]])

所以view(1,1,6)其实是把原来(1,2,3)维的pe拉平了~

pe.view(-1)
>>>tensor([1, 0, 2, 1, 1, 0])

这里看到view(-1)也是把矩阵拉平,但是直接拉平成一维向量,维度为[6],之前view(1,1,6)的输出是一个三维矩阵,维度为[1,1,6];

pe.view(1,-1)
>>>tensor([[1, 0, 2, 1, 1, 0]])

这里view(1,-1)可以理解为,把pe变成两维矩阵,第0维的尺寸为1,第1维尺寸设为-1,-1可以看成是,确定了第0维后,把后续的值在第一维拉平,所以view(1,-1)之后得到的矩阵尺寸为[1,6];
这里再用几个例子做以阐明:

pe.view(2,-1)
>>>tensor([[1, 0, 2],
           [1, 1, 0]])
           
pe.view(2,-1,1)
>>>tensor([[[1],
            [0],
            [2]],

           [[1],
            [1],
            [0]]])

pe.view(3,-1)
>>>tensor([[1, 0],
           [2, 1],
           [1, 0]])

pe.view(3,-1,1)
>>>tensor([[[1],
            [0]],

           [[2],
            [1]],

           [[1],
            [0]]])

所以对view(参数a,-1,参数b…)来说,-1在哪就说明那一维度拉平,按别的确定维度(参数a,参数b等)来确定-1那一维度应该是多少维~

6. lambda 匿名函数

其实这个大家上网查查都有资料,但是都讲的很浅,比如如下:

a = lambda x,y : x*y
a(2,3)
>>>6

所以lambda是干嘛的呢,他其实本质是希望让函数更简单,比如上述例子实现的其实是:

def a(x,y):
	return x*y
a(2,3)
>>>6

好了,这就是lambda的基本解释了,然后网上就默认大家都弄懂了,但今天看代码看到一个很有趣的lambda用法,个人想了很久才想懂,这里用个简单示例来阐明并解释:
(这个例子是和Annotated Transformer对应的,用来解释为什么要用lambda函数)

import torch
import torch.nn as nn

class Top(nn.Module):
    def __init__(self):
        super(Top, self).__init__()
        self.self_attn = MultiHeadedAttention()
        self.sublayer = SublayerConnection()

    def forward(self, x, mask):
        x = self.sublayer(x, lambda x : self.self_attn(x, x, x, mask))
        return x

class SublayerConnection(nn.Module):
    def __init__(self):
        super(SublayerConnection, self).__init__()

    def forward(self, x, layer):
        return x + layer(x+1)

class MultiHeadedAttention(nn.Module): 
    def __init__(self): 
        super(MultiHeadedAttention, self).__init__() 
        self.attn = None

    def forward(self, query, key, value, mask=None): 
        x = attention(query, key, value, mask=mask) 
        return x

def attention(query, key, value, mask=None, dropout=None):
    return query*key*value - mask

#instance
top = Top()
top(2,1)
>>> 28

大家可以看下为什么这里等于28~
这里的有趣点是,当我们调用Top时,里面代码对应的

x = self.sublayer(x, lambda x : self.self_attn(x, x, x, mask))

这是在干嘛呢?以及这个最终结果28是怎么得来的呢?

这里我就开门见山的说了,在Top里,self_attn对应的MultiHeadedAttention的forward函数里得传入四个参数,分别为(query, key, value, mask),sublayer对应的SublayerConnection类的forward需要传入(x, layer),其中layer是个对象,但在SublayerConnection类的forward可以看到

return x + layer(x+1)

这里layer只有一个(x+1)这一个输入,当layer对应到MultiHeadedAttention这种需要四个输入的时候,是无法直接使用的,因此需要一个函数来对其进行操作,这就是需要这个lambda的时候了,这个lambda匿名函数的实际操作类似于如下

def lambda1(self, x, mask):
  return self.self_attn(x, x, x, mask) 

但对应到原代码中,这个匿名函数并不是把self.self_attn(x, x, x, mask) 运行完得到值了再把值传到sublayer中的,而是直接把这个self.self_attn(x, x, x, mask)这个对象传了进去,这个期间并没有运行!他是到了SublayerConnection类的forward函数里的return之后才一起运行的!
在例子里面就是:

x = self.sublayer(x, lambda x : self.self_attn(x, x, x, mask)) 
# 当程序到这后会先进行lambda匿名函数的操作
# 把self.self_attn(x, x, x, mask)这个对象直接传入sublayer()的第二项中,
# 与sublayer对应的SublayerConnection类中的forward的layer进行结合,
# 从原来的
return x + layer(x+1)
#变成
return x + self.self_attn(x+1,x+1,x+1,mask)
# 直到这一步,整个return才开始运行!
# 这样也完成了layer(x+1)的x+1直接对应到self.self_attn(x, x, x, mask)的x中
# 由此完成整体操作

因此在最后调用top(2,1)时,全局的计算应该就为

2+(2+1)*(2+1)*(2+1)-1=28

之前对此还有个疑问,就是,这里为什么要用lambda,不直接用下列就行了?不也是直接把self.self_attn这个对象传进去了?

x = self.sublayer(x, self.self_attn(x, x, x, mask))

就像我上面所述,这样直接传会报错,因为SublayerConnection类中的return x + layer(x+1),这个layer(x+1)只有这一个输入,self_attn有四个输入,对应不上,所以才需要另加一个函数进行对应操作才能正常运行,这里用匿名函数也正式为了让代码更加简洁,不用再另起几行写个新函数了~

三、 将print输出到txt或者csv文件中

1. 将print同时输出到终端以及指定txt文档中

import sys

class Logger(object):
    def __init__(self, fileN='Default.log'):
        self.terminal = sys.stdout
        self.log = open(fileN, 'a')

    def write(self, message):
        self.terminal.write(message)
        self.log.write(message)

    def flush(self):
        pass
        
sys.stdout = Logger('./test.txt') //

这样直接调用的print函数便可以直接在终端输出以及写在txt文档中。

2. 将指定内容输入到csv文件中

import csv

f = open('./test.csv', 'w')
csv_write = csv.writer(f)

a=1
csv_write.writerow(a)

这是将a的内容输入到csv文件中,也可以传入自己想要的内容~


总结

2022.03.23

以上就是今天记录的内容了,之后遇到了新技巧也会继续更新的~

对内容有疑问也可以联系博主,博主会找空回复的 fuyz@stu.pku.edu.cn

对了,这里分享下博主觉得写得很好的一篇讲Transformer的文章,博主也是跟着这个学习Pytorch的,希望可以帮到大家~

2022.03.27

今天更新了对size(),transpose(),以及permute()函数的考量,其实size(-1)这个用法在实际使用中还是挺常见的,嗯,继续努力吧~

2022.03.30
更新了view()函数

2022.03.31
更新了lambda 匿名函数,对代码中一个神秘用法给了一定解释~

2022.04.06
更新了print输出到txt文件以及csv文件中的代码

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值