计算机系统应用教程网站

网站首页 > 技术文章 正文

PyTorch入门与实战——必备基础知识(下)01

btikc 2024-10-14 08:50:00 技术文章 6 ℃ 0 评论

前言

我们在上篇PyTorch入门与实战——必备基础知识(上)01中了解向量、矩阵和张量的基本概念和代码实现,并且重点介绍了张量的基本操作。但是在实际的神经网络模型中往往会使用一些高阶的张量操作,如拼接,广播等。本章节我们来学习张量的高阶操作。

目录

  1. 张量的维度操作--squezee和unsquezee
  2. 调整张量的形状--reshape/view
  3. 维度扩展---expand
  4. 维度交换---transpose vs permute
  5. 转置操作--.t()
  6. t维度重复---repeat
  7. 张量拼接---cat & stack
  8. 缩小张量---narrow
  9. Tensor运行设备(GPU,CPU)转换
  10. 索引与切片


张量的维度操作--squezee & unsqueeze

squezee就是压缩(维度减少,降维)

b=torch.IntTensor([ [[1,2,3],[4,5,6]] ] )
#b这个tensor就是[  [[1,2,3],[4,5,6]]  ]
#即列表[ ]中只有一个2*3的二维向量,所以三维是1,2,3
#如果列表[ ]中有三个2*3的二维向量,那么三维就是3,2,3
print(b.shape)#输出得到:torch.Size([1,2,3])
 
#b这个tensor有三个维度,1*2*3,只有某个维度是1,这个维度才能被压缩
c=torch.squeeze(b,0)#表示在第0维上进行压缩
print(c)
#tensor([[1, 2, 3],
#        [4, 5, 6]], dtype=torch.int32)
print(c.shape)#输出得到:torch.Size([2, 3])
 
成功的将1*2*3变为2*3,实现了降维
import torch
"""
torch.squeeze(input, dim=None, out=None) → Tensor
除去输入张量input中数值为1的维度,并返回新的张量。如果输入张量的形状为( A × 1 × B × C × 1 × D ) ,
那么输出张量的形状为( A × B × C × D ) 

当通过dim参数指定维度时,维度压缩操作只会在指定的维度上进行。如果输入向量的形状为( A × 1 × B ) ,
squeeze(input, 0)会保持张量的维度不变,只有在执行squeeze(input, 1)时,输入张量的形状会被压缩至( A × B ) 

如果一个张量只有1个维度,那么它不会受到上述方法的影响。

输出的张量与原张量共享内存,如果改变其中的一个,另一个也会改变。
Note:只能除去输入张量input中数值为1的维度。如果指定的维度不是1,那么不改变形状
squeeze参数:

input (Tensor) – 输入张量
dim (int, optional) – 如果给定,则只会在给定维度压缩
out (Tensor, optional) – 输出张量
"""
x = torch.zeros(2, 1, 2, 1, 2)
print(x.size())
y = torch.squeeze(x)
print(y.size())
y = torch.squeeze(x, 0)
print(y.size())
y = torch.squeeze(x, 1)
print(y.size())
torch.Size([2, 1, 2, 1, 2])
torch.Size([2, 2, 2])
torch.Size([2, 1, 2, 1, 2])
torch.Size([2, 2, 1, 2])

unsqueeze就是扩充(维度增加,升维)

b=torch.IntTensor([ [[1,2,3],[4,5,6]] ] )
#b这个tensor就是[  [[1,2,3],[4,5,6]]  ]
#即列表[ ]中只有一个2*3的二维向量,所以三维是1,2,3
#如果列表[ ]中有三个2*3的二维向量,那么三维就是3,2,3
print(b.shape)#输出得到:torch.Size([1,2,3])
 
#b这个tensor有三个维度,1*2*3,只有某个维度是1,这个维度才能被压缩
c=torch.squeeze(b,0)#表示在第0维上进行压缩
print(c)
#tensor([[1, 2, 3],
#        [4, 5, 6]], dtype=torch.int32)
print(c.shape)#输出得到:torch.Size([2, 3])
 
成功的将1*2*3变为2*3,实现了降维

"""
torch.unsqueeze(input, dim) → Tensor
dim的范围[-a.dim()-1,a.dim()+1)    eg.a的维度=4,范围是[-5,5)
新增加的这一个维度不会改变数据本身,只是为数据新增加了一个组别,这个组别是什么由我们自己定义。
参数:
input(tensor) -输入张量
dim(int) -在给定维度上插入一维
"""
a=torch.rand(4,1,28,28)
print(a.unsqueeze(0).shape)         #torch.Size([1, 4, 1, 28, 28]

b=torch.tensor([1.2, 2.3])          #torch.Size([2])
print(b.unsqueeze(0)) #变成二维              #tensor([[1.2000, 2.3000]])   torch.Size([2, 1])
print(b.unsqueeze(-1))
#tensor([[1.2000],
#        [2.3000]])

x=torch.rand(3)
y=torch.rand(4,32,14,14)
print(x)
print(x.unsqueeze(1))
x=x.unsqueeze(1).unsqueeze(2).unsqueeze(0)      #[32]->[32,1]->[32,1,1]->[1,32,1,1]
print(x.shape)                                  #torch.Size([1, 32, 1, 1])) 再进行扩展即可计算x+y
torch.Size([1, 4, 1, 28, 28])
tensor([[1.2000, 2.3000]])
tensor([[1.2000],
[2.3000]])
tensor([0.0527, 0.8367, 0.1623])
tensor([[0.0527],
[0.8367],
[0.1623]])
torch.Size([1, 3, 1, 1])

reshape/view调整张量的形状,返回一个新的形状的张量

reshape

view

相同点

都可以重新调整tensor的形状

不同点

1、view只能用于内存中连续存储的tensor。如果对tensor做了transpose,permute等操作,

则tensor在内存中会不连续,此时不能调用view函数。此时先调用.contiguous()方法,使

tensor的元素在内存空间中连续,然后调用.view()

2、reshape连续与否都能用。

a = torch.rand(4,1,28,28)
print(a.view(4,2,-1).shape)
print(a.reshape(4,-1).shape)
torch.Size([4, 2, 392])
torch.Size([4, 784])

expand维度扩展

"""
torch.Tensor.expand(*sizes) → Tensor
将现有张量沿着值为1的维度扩展到新的维度。张量可以同时沿着任意一维或多维展开。
如果不想沿着一个特定的维度展开张量,可以设置它的参数值为-1。
参数:
sizes(torch.size or int....)--想要扩展的维度
"""
x = torch.Tensor([3])
print(x.size())
print(x.expand(3,2))

a = torch.tensor([[[1,2,3],[4,5,6]]])
print(a)
print(a.size()) 
print(a.expand(3,2,3)) #只能沿着1的维度扩展到新的维度
torch.Size([1])
tensor([[3., 3.],
[3., 3.],
[3., 3.]])


tensor([[[1, 2, 3],
[4, 5, 6]]])

torch.Size([1, 2, 3])

tensor([[[1, 2, 3],
[4, 5, 6]],

[[1, 2, 3],
[4, 5, 6]],

[[1, 2, 3],
[4, 5, 6]]])

注意

  1. 只能在第0维扩展一个维数,比如原来是是(1,3,4)-->(2,1,3,4),而在其他维度扩展不可以(1,3,4)==》(1,2,3,4)【错误
  2. 如果不增加维数,只是增加维度,要增加的原维度必须是1才可以在该维度增加维度,其他值均不可以.
import torch
#1
x = torch.randn(2, 1, 1)#为1可以扩展为3和4
x = x.expand(2, 3, 4)
print('x :', x.size())
>>> x : torch.Size([2, 3, 4])
#2
#扩展一个新的维度必须在最前面,否则会报错
x = x.expand(2, 3, 4, 6)
>>> RuntimeError: The expanded size of the tensor (3) must match the existing size (2) at non-singleton dimension 1.
x = x.expand(6, 2, 3, 4)
>>> x : torch.Size([6, 2, 3, 4])
#3
#某一个维度为-1表示不改变该维度的大小
x = x.expand(6, -1, -1, -1)
>>> x : torch.Size([6, 2, 1, 1])

维度交换--transpose vs permute

transpose和permute

相同点

都可以进行维度交换

不同的

transpose 只能交换两个维度 permute可以自由交换位置

"""
torch.transpose 只能交换两个维度 permute可以自由交换位置
"""
import torch
x = torch.rand(5,1,2,1)
print(x.size())
c = x.transpose(1,2) # 交换1和2维度
print(x.size())
b = x.permute(0,3,1,2) #可以直接变换
print(b.size())
print("***************")
"""
For example:
四个维度表示的[batch,channel,h,w] ,如果想把channel放到最后去,
形成[batch,h,w,channel],那么如果使用前面的维度交换,至少要交换两次(先13交换再12交换)。
而使用permute可以直接指定维度新的所处位置,更加方便。
"""
b=torch.rand(4,3,28,32)
print(b.transpose(1, 3).shape)                      #torch.Size([4, 32, 28, 3])
print(b.transpose(1, 3).transpose(1, 2).shape)      #torch.Size([4, 28, 32, 3])
 
print(b.permute(0,2,3,1).shape)                     #torch.Size([4, 28, 32, 3]
torch.Size([5, 1, 2, 1])
torch.Size([5, 1, 2, 1])
torch.Size([5, 1, 1, 2])
***************
torch.Size([4, 32, 28, 3])
torch.Size([4, 28, 32, 3])
torch.Size([4, 28, 32, 3])

转置操作

"""
.t操作指适用于矩阵
"""
a=torch.rand(3, 4)
print(a.t().shape)     
torch.Size([4, 3])

维度重复--repeat

repeat:会重新申请内存空间来增加数据。

"""
repeat会重新申请内存空间,repeat()参数表示各个维度指定的重复次数。
"""
a=torch.rand(1,32,1,1)
print(a.repeat(4,32,1,1).shape)                 #torch.Size([4, 1024, 1, 1])
print(a.repeat(4,1,1,1,1).shape)                  #torch.Size([4, 32, 1, 1])
torch.Size([4, 1024, 1, 1])
torch.Size([4, 1, 32, 1, 1])

张量拼接 cat & stack

cat:是在已有的张量上拼接,且不需要张量之间形状相同

cat 中示意图的例子,原来就是3维的,cat之后仍旧是3维的,而现在咱们是从⒉维变成了3维。

stack: 会生成行的维度,并且要求张量之间形状相同。为了让你加深理解,我们还是结合具体例子来看看。假设我们有两个二维矩阵Tensor,把它们“堆叠”放在一起,构成一个三维的Tensor,如下图:


这相当于原来的维度(秩)是2,现在变成了3,变成了一个立体的结构,增加了一个维度。

"""
torch.cat(a_tuple, dim)#tuple 是一个张量或者元组,在指定维度上进行拼接
torch.stack(a_tuple, dim)#与cat不同的在于,cat只能在原有的某一维度上进行连接,stack可以创建一个新的维度,
将原有维度在这个维度上进行顺序排列
#比如说,有2个4x4的张量,用cat就只能把它们变成一个8x4或4x8的张量,用stack可以变成2x4x4.
"""
x = torch.randn(2, 3)
print(x)
y = torch.cat((x, x, x), 0)#从行方向进行拼接
print(y)
y1 = torch.cat((x, x, x), 1)#从列方向进行拼接
print(y1)
print("***********")

"""
下面例子说明torch.cat()与torch.stack()区别。可以看出,stack()是增加新的维度来完成拼接,不改变原维度上的数据大小。
cat()是在现有维度上进行数据的增加(改变了现有维度大小),不增加新的维度。
"""
x = torch.rand(2,3)
y = torch.rand(2,3)
print(x)
print(y)
print(torch.stack((x,y),1))
print(torch.stack((x,y),1).size())
print(torch.cat((x,y),1))
print(torch.cat((x,y),1).size())
tensor([[-0.1622, -1.0951, -0.6887],
[ 0.5533, 0.2154, 0.0024]])
tensor([[-0.1622, -1.0951, -0.6887],
[ 0.5533, 0.2154, 0.0024],
[-0.1622, -1.0951, -0.6887],
[ 0.5533, 0.2154, 0.0024],
[-0.1622, -1.0951, -0.6887],
[ 0.5533, 0.2154, 0.0024]])
tensor([[-0.1622, -1.0951, -0.6887, -0.1622, -1.0951, -0.6887, -0.1622, -1.0951,
-0.6887],
[ 0.5533, 0.2154, 0.0024, 0.5533, 0.2154, 0.0024, 0.5533, 0.2154,
0.0024]])
***********
tensor([[0.3894, 0.8128, 0.9344],
[0.1456, 0.9882, 0.0165]])
tensor([[0.8347, 0.9807, 0.4423],
[0.8735, 0.7247, 0.8918]])
tensor([[[0.3894, 0.8128, 0.9344],
[0.8347, 0.9807, 0.4423]],

[[0.1456, 0.9882, 0.0165],
[0.8735, 0.7247, 0.8918]]])
torch.Size([2, 2, 3])
tensor([[0.3894, 0.8128, 0.9344, 0.8347, 0.9807, 0.4423],
[0.1456, 0.9882, 0.0165, 0.8735, 0.7247, 0.8918]])
torch.Size([2, 6])

缩小张量---narrow

torch.narrow()方法用于对 PyTorch 张量执行窄操作。它返回一个新的张量,它是原始输入张量的缩小版本。

例如,[4, 3] 的张量可以缩小为 [2, 3] 或 [4, 2] 大小的张量。我们可以一次缩小一个维度上的张量。在这里,我们不能将两个维度都缩小到 [2, 2] 的大小。我们也可以用来缩小张量的范围。

"""
torch.Tensor.narrow(dimension, start, length) → Tensor

返回一个经过缩小后的张量。操作的维度由dimension指定。缩小范围是从start开始到start+length。执行本方法的张量与返回的张量共享相同的底层内存。

参数:

dimension (int) – 要进行缩小的维度
start (int) – 开始维度索引
length (int) – 缩小持续的长度
"""
x = torch.Tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print(x)
m=x.narrow(0, 0, 2)#从行维度缩减,从0开始,缩小2的长度
print(m)
n=x.narrow(1, 1, 2)#从列维度缩减,从1开始,缩小2的长度
print(n)
tensor([[1., 2., 3.],
[4., 5., 6.],
[7., 8., 9.]])
tensor([[1., 2., 3.],
[4., 5., 6.]])
tensor([[2., 3.],
[5., 6.],
[8., 9.]])

思考:如何将[4, 3] 的张量缩小到 [2, 2] 的大小??评论区回答

Tensor运行设备(GPU,CPU)转换

PyTorch中的数据类型为Tensor,Tensor与Numpy中的ndarray类似,同样可以用于标量,向量,矩阵乃至更高维度上面的计算。PyTorch中的tensor又包括CPU上的数据类型和GPU上的数据类型,一般GPU上的Tensor是CPU上的Tensor加cuda()函数得到。系统默认的torch.Tensor是torch.FloatTensor类型。例如data = torch.Tensor(2,3)是一个2*3的张量,类型为FloatTensor; data.cuda()就将其转换为GPU的张量类型,torch.cuda.FloatTensor类型。

#查看当前gpu的信息
print(torch.cuda.current_device())
print(torch.cuda.device(0))
print(torch.cuda.device_count())
print(torch.cuda.get_device_name())
print(torch.cuda.is_available())
print("_____________________")

def get_device(ordinal):
    # Use GPU ?
    if ordinal < 0:
        print("Computation on CPU")
        device = torch.device('cpu')
    elif torch.cuda.is_available():
        print("Computation on CUDA GPU device {}".format(ordinal))
        device = torch.device('cuda:{}'.format(ordinal))
    else:
        print("/!\\ CUDA was requested but is not available! Computation will go on CPU. /!\\")
        device = torch.device('cpu')
    return device
"""
更改get_device()的参数即可,
0表示用GPU(由于本机只有一块gpu,只能为0时,才调用GPU), -1表示CPU .
"""
device = get_device(0) 
print(device)
x = torch.randn(5,6)
y = torch.randn(6,5)
x,y = x.to(device), y.to(device) #将数据转到GPU上
xy = torch.mm(x,y)
print(xy)
0
<torch.cuda.device object at 0x000001BD409C8EC8>
1
GeForce GTX 1660 Ti
True
_____________________
Computation on CUDA GPU device 0
cuda:0
tensor([[-1.2128, 0.7759, -4.3527, 2.9628, -3.7611],
[-0.1583, -0.4208, -2.4139, 0.5759, 0.5275],
[ 0.7142, -1.0377, -1.0579, -2.4379, 3.4897],
[-1.4674, 2.6131, 1.3632, -2.6275, 1.0764],
[-0.5082, 0.7873, 2.3805, 3.0700, -0.8430]], device='cuda:0')

直接指定device的值

"""
或者直接指定device的值
"""
x = torch.randn(2,3)
y = torch.randn(3,2)
x,y = x.to(device='cuda'), y.to(device='cuda') #将数据转到GPU上
xy = torch.mm(x,y)
print(xy)
x,y = x.to(device='cpu'), y.to(device='cpu') #将数据转到CPU上
xy = torch.mm(x,y)
print(xy)
tensor([[-2.0578, -1.2883],
[-3.9589, -1.3973]], device='cuda:0')
tensor([[-2.0578, -1.2883],
[-3.9589, -1.3973]])

Pytorch的tensor与numpy的NDArray转换

"""
pytorch的numpy()函数:tensor-->NDArray
pytorch的from_numpy()函数:NDArray-->tensor
"""
import numpy as np
x = torch.rand(2,3)
print(x)
ndarray = x.numpy() #x本身就是tensor,直接调用numpy()函数
print(ndarray)

print('___________________')
ndarray1 = np.random.randn(2,3)
print(ndarray1)
x1 = torch.from_numpy(ndarray1) #要使用torch.from_numpy()
print(x1)
tensor([[0.6719, 0.6100, 0.7454],
[0.4262, 0.8800, 0.9117]])
[[0.6718741 0.610046 0.74536425]
[0.42622328 0.87996227 0.9116586 ]]
___________________
[[ 0.31791228 -1.47390049 0.94522577]
[ 0.08259648 0.64698846 0.11206799]]
tensor([[ 0.3179, -1.4739, 0.9452],
[ 0.0826, 0.6470, 0.1121]], dtype=torch.float64)

索引与切片

切片:从序列中提取出子序列,用法为 变量名[lower:upper:steps],左到右不到。切分是连接的逆过程,有了刚才的经验,你很容易就会想到,切分的操作也应该有很多种,比如切片、切块等。没错,切分的操作主要分为三种类型: chunk、split、unbind。

chunk:作用就是将 Tensor 按照声明的 dim,进行尽可能平均的划分。比如说,我们有一个 32channel 的特征,需要将其按照 channel 均匀分成 4 组,每组 8 个 channel,这个切分就可以通过 chunk 函数来实现。具体函数如下:

"""
input表示要做 chunk 操作的 Tensor。
chunks代表将要被划分的块的数量,而不是每组的数量。请注意,chunks 必须是整型。
dim就是按照哪个维度来进行 chunk。
"""
torch.chunk(input, chunks, dim=0)
A=torch.tensor([1,2,3,4,5,6,7,8,9,10])
B = torch.chunk(A, 2, 0)
B
(tensor([1, 2, 3, 4, 5]), tensor([ 6, 7, 8, 9, 10]))

split 的函数定义如下,我们还是分别看看这里涉及的参数。

"""
tensor,也就是待切分的 Tensor。
split_size_or_sections 这个参数。当它为整数时,表示将 tensor 按照每块大小为这个整数的数值来切割;
当这个参数为列表时,则表示将此 tensor 切成和列表中元素一样大小的块
dim就是按照哪个维度来进行切分。
"""
torch.split(tensor, split_size_or_sections, dim=0)
A=torch.rand(4,4)
A
tensor([[0.6418, 0.4171, 0.7372, 0.0733],
[0.0935, 0.2372, 0.6912, 0.8677],
[0.5263, 0.4145, 0.9292, 0.5671],
[0.2284, 0.6938, 0.0956, 0.3823]])
 B=torch.split(A, 2, 0)
 B
(tensor([[0.6418, 0.4171, 0.7372, 0.0733],
[0.0935, 0.2372, 0.6912, 0.8677]]),
tensor([[0.5263, 0.4145, 0.9292, 0.5671],
[0.2284, 0.6938, 0.0956, 0.3823]]))

unbind的函数定义如下:

 torch.unbind(input, dim=0)

其中,input 表示待处理的 Tensor,dim 还是跟前面的函数一样,表示切片的方向。

 A=torch.arange(0,16).view(4,4)
 A
tensor([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11],
[12, 13, 14, 15]])

b=torch.unbind(A, 0)
b
(tensor([0, 1, 2, 3]),
tensor([4, 5, 6, 7]),
tensor([ 8, 9, 10, 11]),
tensor([12, 13, 14, 15]))

在这个例子中,我们首先创建了一个 4x4 的二维矩阵 Tensor,随后我们从第 0 维,也就 是“行”的方向进行切分 ,因为矩阵有 4 行,所以就会得到 4 个结果。


索引:通过索引index的方法来访问对应位置的值,一般索引值从0开始,例如索引0表示第1个元素。但是Python还有负索引值的用法,即从后向前开始计数,例如索引-1表示倒数第1个元素。

[开始索引:结束索引:步长]


索引操作有很多方式,其中最常用的两 个操作就是 index_select 和 masked_select,我们分别去看看用法。

index_select这个函数了,其定义如下:

torch.index_select(tensor, dim, index)

其中tensor、dim 跟前面函数里的一样,不再赘述。我们重点看一看 index,它表示从 dim 维度中的哪些位置选择数据,这里需要注意,index是 torch.Tensor 类型。

A=torch.arange(0,16).view(4,4)
A
tensor([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11],
[12, 13, 14, 15]])

B=torch.index_select(A,0,torch.tensor([1,3]))
B
tensor([[ 4, 5, 6, 7],
[12, 13, 14, 15]])

C=torch.index_select(A,1,torch.tensor([0,3]))
C
tensor([[ 0, 3],
[ 4, 7],
[ 8, 11],
[12, 15]])

在这个例子中,我们先创建了一个 4x4 大小的矩阵 Tensor A。然后,我们从第 0 维选择第1(行)和 3(行)的数据,并得到了最终的 Tensor B,其大小为 2x4。随后我们从 Tensor A 中选择第 0(列)和 3(列)的数据,得到了最终的 Tensor C,其大小为 4x2。

indexed_select,它是基于给定的索引来进行数据提取的。但有的时候,我们还想通过一些判断条件来进行选择,比如提取深度学习网络中某一层中数值大于 0 的参数呢?

masked_select它的定义:

torch.masked_select(input, mask, out=None)

其中input 表示待处理的 Tensor。mask 代表掩码张量,也就是满足条件的特征掩码。这里你 需要注意的是,mask 须跟 input 张量有相同数量的元素数目,但形状或维度不需要相同。

a=torch.rand(4,3,28,28) #生成四维数据
print(a[0].shape)               #torch.Size([3, 28, 28])
print(a[0,0].shape)             #torch.Size([28, 28])
print(a[0,0,2,4])               #tensor(0.7309)

print(a[:2,:1].shape)           #torch.Size([2, 1, 28, 28])  等价于a[:2,:1,:,:].shape
print(a[:,:,::2,::2].shape)     #torch.Size([4, 3, 14, 14])
print("________________")
"""
使用掩码索引masked_select,根据mask取值
"""
#select by mask
x=torch.randn(3,4)
print(x)
mask=x.ge(0.5)                  #greater equal大于等于
print(mask)
y = torch.masked_select(x, mask)    
print(y)
torch.Size([3, 28, 28])
torch.Size([28, 28])
tensor(0.1992)
torch.Size([2, 1, 28, 28])
torch.Size([4, 3, 14, 14])
________________
tensor([[-1.1685, -0.8040, -0.9153, 0.5629],
[ 0.6403, 0.8488, 0.1973, -1.8883],
[-1.2136, 1.1000, 0.6384, 0.9331]])
tensor([[False, False, False, True],
[ True, True, False, False],
[False, True, True, True]])
tensor([0.5629, 0.6403, 0.8488, 1.1000, 0.6384, 0.9331])


小结

本文主要介绍一些常用的张量操作,学会这些操作对模型的构建和数据处理很有帮助。如对某矩阵进行转置可以使用.t()和permute()。我们不要死板的学习这些张量操作,学会灵活使用和变通。


思考题

1、如何将张量形状为[3,2]变成张量形状[3,1,2]呢?

2、怎么将CPU中的张量转到GPU中呢?

3、将张量形状为[3,1,1,4]转为张量形状[3,4,1]呢?需要经过哪些操作呢?


欢迎大家评论区中讨论和交流

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表