栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Python

PyTorch学习笔记(四)

Python 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

PyTorch学习笔记(四)

模型构建

PyTorch中神经网络构造一般是基于Module类的模型来完成的,它让模型构造更加灵活。

神经网络的构造

Module类是nn模块里提供的一个模型构造类,是所有神经网络模块的基类,可以继承它来定义想要的模型。这里定义的MLP类重载了Module类的init函数和forward函数,分别用于创建模型参数和定义前向计算(正向传播)。

import torch
from torch import nn
class MLP(nn.Module):
    #声明带有模型参数的层
    def __init__(self, **kwargs):
        #调用MLP父类Block的构造函数来进行必要的初始化,这样在构造实例时还可以指定其他函数
        super(MLP, self).__init__(**kwargs)
        self.hidden = nn.Linear(784,256)
        self.act = nn.ReLU()
        self.output = nn.Linear(256,10)
    
    #定义模型的前向计算,即如何根据输入x计算返回所需要的模型输出
    def forward(self, x):
        o = self.act(self.hidden(x))
        return self.ouput(o)    

以上的MLP类中无须定义反向传播函数,系统将通过自动求梯度而自动生成反向传播所需的backward函数。

X = torch.rand(2, 784)
net = MLP() # MLP实例化,初始化net
print(net)
net(X) # 传入输入数据X做一次前向计算
#net(X)会调用MLP继承自Module类的call函数,这个函数将调用用MLP类定义的forward函数来完成前向计算。

注意:Module类的子类既可以是一个层(比如Linear类),又可以是一个模型(如MLP类)或者是模型的一个部分。

神经网络中常见的层

接下来会介绍如何使用Module来自定义层,从而可以被反复调用。

  • 不含模型参数的层
    下面构造的MyLayer类通过继承Module类自定义了一个将输入减掉均值后输出的层,并将层的计算定义在了forward函数里。这个层里不含模型参数。
import torch
from torch import nn
class MyLayer(nn.Module):
    def __init__(self, **kwargs):
        super(MyLayer, self).__init__(**kwargs)
    def forward(self, x):
        return x - x.mean

#测试,实例化该层,然后做前向计算
layer = MyLayer()
layer(torch.tensor([1, 2, 3, 4, 5], dtype=torch.float)
  • 含模型参数的层

Parameter类其实是Tensor的子类,如果一个Tensor是Parameter,那么它会自动被添加到模型的参数列表里。所以在自定义含模型参数的层时,应该将参数定义成Parameter,除了直接定义成Parameter类外,还可以使用ParameterList和ParameterDict分别定义参数的列表和字典。

class MyListDense(nn.Module):
    def __init__(self):
        super(MyListDense, self).__init__()
        self.params = nn.ParameterList([nn.Parameter(torch.randn(4, 4)) for i in range(3)])
        self.params.append(nn.Parameter(torch.randn(4, 1)))
    def forward(self, x):
        for i in range(len(self.params)):
            x = torch.mm(x, self.params[i])

net = MyListDense()
print(net)
class MyDictDense(nn.Module):
	def __init__(self):
		super(MyDictDense, self).__init__()
		self.params = nn.ParameterDict({
			'linear1':nn.Parameter(torch.randn(4, 4)),
			'linear2':nn.Parameter(torch.randn(4, 1))
			})
		self.params.update({'linear3':nn.Parameter(torch.randn(4,2))})
	
	def forward(self, x, choice='linear1'):
		return torch.mm(x, self.params[choice])

net = MyDictDense()
print(net)
  • 二维卷积层
    二维卷积层将输入和卷积核做互相关运算,并加上一个标量偏差来得到输出。卷积层的模型参数包括了卷积核和标量偏差。在训练模型的时候,通常先对卷积核随机初始化,然后不断迭代卷积核和偏差。
import torch
from torch import nn

# 卷积运算 二维互相关
def corr2d(X, K):
	h, w = K.shape
	X, K = X.float(), K.float()
	Y = torch.zeros((X.shape[0] - h + 1, X.shape[1] - w + 1))
	for i in range(Y.shape[0]):
		for j in range(Y.shape[1]):
			Y[i, j] = (X[i: i + h, j:j + w] * K).sum()
	return Y

# 二维卷积层
class Conv2D(nn.Module):
	def __init__(self, kernel_size):
		super(Conv2D, self).__init__()
		self.weight = nn.Parameter(torch.randn(kernel_size))
		self.bias = nn.Parameter(torch.randn(1))
	
	def forward(self, x):
		return corr2d(x, self.weight) + self.bias

卷积窗口形状为p * q的卷积层称为p * q卷积层。同样,p * q卷积或p * q卷积核说明卷积核的高和宽分别为p和q。
填充(padding)是指在输入高和宽的两侧填充元素(通常是0元素)。
下面的例子里创建一个高和宽为3的二维卷积层,然后设输入高和宽两侧的填充数分别为1。给定一个高和宽为8的输入,则输出的高和宽是8。

import torch
from torch import nn
#定义一个函数来计算卷积层,它对输入和输出做相应的升维和降维
def comp_conv2d(conv2d, X):
	X = X.view((1, 1) + X.shape) # (1,1)代表批量大小和通道数
	Y = conv2d(X)
	return Y.view(Y.shape[2:]) # 排除不关心的前两维:批量和通道

#注意这里是两侧分别填充1行或列,所以在两侧一共填充2行或列
conv2d = nn.Conv2d(in_channels=1, out_channels=1, kernel_size=3, padding=1)

X = torch.rand(8, 8)
comp_conv2d(conv2d, X).shape
torch.Size([8, 8])

当卷积核的高和宽不同时,也可以通过设置高和宽上不同的填充数使输出和输入具有相同的高和宽。

#使用高为5、宽为3的卷积核,在高和宽两侧的填充数分别为2和1
conv2d = nn.Conv2d(in_channels=1, out_channels=1, kernel_size=(5,3), padding=(2,1))
comp_conv2d(conv2d, X).shape
torch.Size([8, 8])

当卷积核的高和宽不同时,也可以通过设置高和宽上不同的填充数使输出和输入具有相同的高和宽。

# 使用高为5、宽为3的卷积核。在⾼和宽两侧的填充数分别为2和1
conv2d = nn.Conv2d(in_channels=1, out_channels=1, kernel_size=(5, 3), padding=(2, 1))
comp_conv2d(conv2d, X).shape

在二维互相关运算中,卷积窗口从输入数组的最左上方开始,按从左往右、从上往下的顺序,一次在输入数组上滑动。每次滑动的行数和列数称为步幅(stride)。

conv2d = nn.Conv2d(1, 1, kernel_size=(3,5), padding=(0,1), stride=(3,4))
comp_conv2d(conv2d, X).shape
torch.Size([2, 2])

填充可以增加输出的高和宽,这常用来使输出与输入具有相同的高度。
步幅可以减小输出的高和宽。

  • 池化层
    池化层每次对输入数据的一个固定形状窗口中的元素计算输出。不同于卷积层里计算输入和核的互相关性,池化层直接计算池化窗口内元素的最大值或平均值。
import torch
from torch import nn
def pool2d(X, pool_size, mode='max'):
	p_h, p_w = pool_size
	Y = torch.zeros((X.shape[0] - p_h + 1, X.shape[1] - p_w + 1))
	for i in range(Y.shape[0]):
		for j in range(Y.shape[1]):
			if mode == 'max':
				Y[i, j] = X[i: i + p_h, j: j + p_w].max()
			elif mode == 'avg':
				Y[i, j] = X[i: i + p_h, j: j+ p_w].mean()
	return Y
X = torch.tensor([[0, 1, 2],[3, 4, 5],[6, 7, 8]], dtype=torch.float)
pool2d(X, (2,2))
tensor([[4., 5.],
	[7., 8.]])
pool2d(X, (2, 2), 'avg')
tensor([[2., 3.],
	[5., 6.]])
模型示例
  • LeNet

    神经网络的典型训练过程如下:
  1. 定义包含一些可学习参数(即权重)的神经网络
  2. 在输入数据集上迭代
  3. 通过网络处理输入
  4. 计算loss(输出和正确答案的距离)
  5. 将梯度反向传播给网络的参数
  6. 更新网络的权重,一般是用一个简单的规则:weight = weight - learning_rate * gradient
import torch
import torch.nn as nn
import torch.nn.functional as F

class Net(nn.Module):
	def __init__(self):
		super(Net, self).__init__()
		# 输入图像channel:1;输出channel:6;5*5卷积核
		self.conv1 = nn.Conv2d(1, 6, 5)
		self.conv2 = nn.Conv2d(6, 16, 5)
		# an affline operation: y = wx + b
		self.fc1 = nn.Linear(16 * 5 * 5, 120)
		self.fc2 = nn.Linear(120, 84)
		self.fc3 = nn.Linear(84, 10)
	
	def forward(self, x):
		# 2*2 max pooling
		x = F.max_pool2d(F.relu(self.conv1(x)), (2, 2))
		# 如果是方阵,则可以只使用一个数字进行定义
		x = F.max_pool2d(F.relu(self.conv2(x)), 2)
		x = x.view(-1, self.num_flat_features(x))
		x = F.relu(self.fc1(x))
		x = F.relu(self.fc2(x))
		x = self.fc3(x)
		return x
	
	def num_flat_features(self, x):
		size = x.size()[1:] # 除去批处理维度的其他所有维度
		num_features = 1
		for s in size:
			num_features *= s
		return num_features

net = Net()
print(net)
Net(
  (conv1): Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1))
  (conv2): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1))
  (fc1): Linear(in_features=400, out_features=120, bias=True)
  (fc2): Linear(in_features=120, out_features=84, bias=True)
  (fc3): Linear(in_features=84, out_features=10, bias=True)
)

只需要定义forward函数,backward函数在使用autograd时自动定义,backward函数用来计算导数。可以在forward函数中使用任何针对张量的操作和计算。

一个模型的可学习参数可以通过net.parameters()返回

params = list(net.parameters())
print(len(params))
print(paramas[0].size()) # conv1的权重
10
torch.size([6, 1, 5, 5])

尝试一个随机的3232的输入,即LeNet的输入是3232的张量。如果使用MNIST数据集来训练这个网络,要把图片大小重新调整到32*32。

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

清零所有参数的梯度缓存,然后进行随机梯度的反向传播:

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

注意:torch.nn只支持小批量处理(mini-batches),且不支持单个样本的输入。比如,nn.Conv2d接受一个4维的张量,即nSample * nChannels * Height * Width,如果是单个样本,需使用input.unsqueeze(0)来添加一个“假的”批大小维度。

  • torch.Tensor 一个多维数组,支持诸如backward()等的自动求导操作,同时也保存了张量的梯度。
  • nn.Module 神经网络模块,是一种方便封装参数的方式,具有将参数移动到GPU、导出、加载等功能。
  • nn.Parameter 张量的一种,当它作为一个属性分配给一个Module时,它会被自动注册为一个参数。
  • autograd.Function 实现了自动求导前向和反向传播的定义,每个Tensor至少创建一个Function节点,该节点连接到创建Tensor的函数并对其历史进行编码。
模型初始化

在深度学习模型的训练中,权重的初始值极为重要。一个好的权重值,会使模型收敛速度提高,使模型准确率更精确。为了利于训练和减少收敛时间,需要对模型进行合理的初始化。
torch.nn.init 官方文档 https://pytorch.org/docs/stable/nn.init.html
通常会根据实际模型来使用torch.nn.init进行初始化,使用isinstance来进行判断模块属于什么类型。

import torch
import torch.nn as nn

conv = nn.Conv2d(1, 3, 3)
linear = nn.Linear(10, 1)

isinstance(conv, nn.Conv2d)
isinstance(linear, nn.Conv2d)

对于不同的类型层,可以设置不同的权值初始化的方法

# 查看随机初始化的conv参数
conv.weight.data
# 查看linear的参数
linear.weight.data
# 对conv进行kaiming初始化
torch.nn.init.kaiming_normal_(conv.weight.data)
conv.weight.data
# 对linear进行常数初始化
torch.nn.init.constant_(linear.weight.data, 0.3)
linear.weight.data
初始化函数的封装

将各种初始化方法定义为一个initialize_weights()的函数并在模型初始后进行使用。

def initialize_weights(self):
	for m in self.modules():
		#判断是否属于Conv2d
		if isinstance(m, nn.Conv2d):
			torch.nn.init.xavier_normal_(m.weight.data)
			#判断是否有偏置
			if m.bias is not None:
				torch.nn.init.constant_(m.bias.data, 0.3)
		elif isinstance(m, nn.Linear):
			torch.nn.init.normal_(m.weight.data, 0.1)
			if m.bias is not None:
				torch.nn.init.zeros_(m.bias.data)
		elif isinstance(m, nn.BatchNorm2d):
			m.weight.data.fill_(1)
			m.bias.data.zeros_()

这段代码流程是遍历当前模型的每一层,然后判断各层属于什么类型,然后根据不同类型层,设定不同的权值初始化方法。

class MLP(nn.Module):
	def __init__(self, **kwargs):
		super(MLP, self).__init__(**kwargs)
		self.hidden = nn.Conv2d(1, 1, 3)
		self.act = nn.ReLU()
		self.output = nn.Linear(10, 1)
	
	def forward(self, x):
		o = self.act(self.hidden(x))
		return self.output(o)

mlp = MLP()
print(list(mlp.parameters())
print("--------初始化--------")

initialize_weights(mlp)
print(list(mlp.parameters()))
损失函数

一个模型想要达到很好的效果需要学习(即训练),一个好的训练离不开优质的负反馈,损失函数就是模型的负反馈。所以在PyTorch中,损失函数是必不可少的,它是数据输入到模型当中,产生的结果与真实标签的评价指标,模型也是根据损失函数的目标来改进。

二分类交叉熵损失函数
torch.nn.BCELoss(weight=None, size_average=None, reduce=None, reduction='mean')

功能:计算二分类任务时的交叉熵(cross entropy)函数。在二分类中,label是{0,1}。input为概率分布的形式,一般来说是sigmoid激活层的输出或者softmax的输出。
官方链接:https://pytorch.org/docs/stable/generated/torch.nn.BCELoss.html

m = nn.Sigmoid()
loss = nn.BCELoss()
input = torch.randn(3, requires_grad=True)
target = torch.empty(3).random_(2)
output = loss(m(input), target)
output.backward()
交叉熵损失函数
torch.nn.CrossEntropyLoss(weight=None, size_average=None, ignore
_index=-100, reduce=None, reduction='mean')

功能:计算交叉熵函数
官方链接:https://pytorch.org/docs/stable/generated/torch.nn.CrossEntropyLoss.html

loss = nn.CrossEntropyLoss()
input = torch.randn(3, 5, requires_grad=True)
target = torch.empty(3, dtype=torch.long).random_(5)
output = loss(input, target)
output.backward()
L1损失函数
torch.nn.L1Loss(size_average=None, reduce=None, reduction='mean')

功能:计算输出y和真实标签target之间的差值的绝对值
官方链接:https://pytorch.org/docs/stable/generated/torch.nn.L1Loss.html

loss = nn.L1Loss()
input = torch.randn(3, 5, requires_grad=True)
target = torch.randn(3, 5)
output = loss(input, target)
output.backward()
MSE损失函数
torch.nn.MSELoss(size_average=None, reduce=None, reduction='mean')

功能:计算输出y和真实标签target之差的平方
官方链接:https://pytorch.org/docs/stable/generated/torch.nn.MSELoss.html

loss = nn.MSELoss()
input = torch.randn(3, 5, requires_grad = True)
target = torch.randn(3, 5)
output = loss(input, target)
output.backward()
平滑L1(Smooth L1)损失函数
torch.nn.SmoothL1Loss(size_average=None, reduce=None, reduction='mean', beta=1.0)

功能:L1的平滑输出,其功能是减轻离群点带来的影响
官方链接:https://pytorch.org/docs/stable/generated/torch.nn.SmoothL1Loss.html

loss = nn.SmoothL1Loss()
input = torch.randn(3, 5, requires_grad=True)
target = torch.randn(3, 5)
output = loss(input, target)
output.backward()

平滑L1与L1的对比
通过可视化两种损失函数曲线来对比平滑L1和L1两种损失函数的区别。

inputs = torch.linspace(-10, 10, steps=5000)
target = torch.zeros_like(inputs)

loss_f_smooth = nn.SmoothL1Loss(reduction='none')
loss_smooth = loss_f_smooth(inputs, target)
loss_f_l1 = nn.L1Loss(reduction='none')
loss_l1 = loss_f_l1(inputs, target)

plt.plot(inputs.numpy(), loss_smooth.numpy(), label='Smooth L1 Loss')
plt.plot(inputs.numpy(), loss_l1, label='L1 loss')
plt.xlabel('x_i - y_i')
plt.ylabel('loss value')
plt.legend()
plt.grid()
plt.show()


对于smoothL1来说,在0这个尖端处,过渡更为平滑。

目标泊松分布的负对数似然损失
torch.nn.PoissonNLLLoss(log_input=True, full=False, size_average=None, eps=1e-08, reduce=None, reduction='mean')

功能:泊松分布的负对数似然损失函数
官方链接:https://pytorch.org/docs/stable/generated/torch.nn.PoissonNLLLoss.html

loss = nn.PoissonNLLLoss()
log_input = torch.randn(5, 2, requires_grad=True)
target = torch.randn(5, 2)
output = loss(log_input, target)
output.backward()
KL散度
torch.nn.KLDivLoss(size_average=None, reduce=None, reduction='mean', log_target=False)

功能:计算KL散度,即相对熵。用于连续分布的距离度量,并且对离散采用的连续输出空间分布进行回归通常很有用。
官方链接:https://pytorch.org/docs/stable/generated/torch.nn.KLDivLoss.html

inputs = torch.tensor([[0.5, 0.3, 0.2], [0.2, 0.3, 0.5]])
target = torch.tensor([[0.9, 0.05, 0.05], [0.1, 0.7, 0.2]], dtype=torch.float)
loss  = nn.KLDivLoss()
output = loss(inputs, target)
print('KLDivLoss损失函数的计算结果为', output)
MarginRankingLoss
torch.nn.MarginRankingLoss(margin=0.0, size
_average=None, reduce=None, reduction='mean')

功能:计算两个向量之间的相似度,用于排序任务。该方法用于计算两组数据之间的差异。
官方链接:https://pytorch.org/docs/stable/generated/torch.nn.MarginRankingLoss.html

loss = nn.MarginRankingLoss()
input1 = torch.randn(3, requires_grad=True)
input2 = torch.randn(3, requires_grad=True)
target = torch.randn(3).sign()
output = loss(input1, input2, target)
output.backward()
print('MarginRankingLoss损失函数的计算结果为',output)
多标签边界损失函数
torch.nn.MultiLabelMarginLoss(size_average=None, reduce=None, reduction='mean')

功能:对于多标签分类问题计算损失函数。
官方链接:https://pytorch.org/docs/stable/generated/torch.nn.MultiLabelMarginLoss.html

loss = nn.MultiLabelMarginLoss()
x = torch.FloatTensor([[0.9, 0.2, 0.4, 0.8]])
# for target y, only consider labels 3 and 0, not after label -1
y = torch.LongTensor([[3, 0, -1, 1]]) #真实的分类是第3类和第0类
output = loss(x,y)

print('MultiLabelMarginLoss损失函数的计算结果为',output)
二分类损失函数
torch.nn.SoftMarginLoss(size_average=None, reduce=None, reduction='mean')

功能:计算二分类的logistic损失
官方链接:https://pytorch.org/docs/stable/generated/torch.nn.SoftMarginLoss.html

inputs = torch.tensor([[0.3, 0.7], [0.5, 0.5]])
target = torch.tensor([[-1, 1], [1, -1]], dtype=torch.float)
loss_f = nn.SoftMarginLoss()
output = loss_f(inputs, target)

print('SoftMarginLoss损失函数的计算结果为',output)
多分类的折页损失
torch.nn.MultiMarginLoss(p=1, margin=1.0, weight=None, size_average=None, reduce=None, reduction='mean')

功能:计算多分类的折页损失
官方链接:https://pytorch.org/docs/stable/generated/torch.nn.MultiMarginLoss.html

inputs = torch.tensor([[0.3, 0.7],[0.5, 0.5]])
target = torch.tensor([0, 1], dtype=torch.long)
loss_f = nn.MultiMarginLoss()
output = loss_f(inputs, target)
print('MultiMarginLoss损失函数的计算结果为',output)
三元组损失
torch.nn.TripletMarginLoss(margin=1.0, p=2.0, eps=1e-06, swap=False, size_average=None, reduce=None, reduction='mean')

功能:计算三元组损失
官方链接:https://pytorch.org/docs/stable/generated/torch.nn.TripletMarginLoss.html

triplet_loss = nn.TripleMarginLoss(margin=1.0, p=2)
anchor = torch.randn(100, 128, requires_grad=True)
positive = torch.randn(100, 128, requires_grad=True)
negative = torch.randn(100, 128, requires_grad=True)
output = triplet_loss(anchor, positive, negative)
output.backward()
print('TripletMarginLoss损失函数的计算结果为',output)
HingeEmbeddingLoss
torch.nn.HingeEmbeddingLoss(margin=1.0, size_average=None, reduce=None, reduction='mean')

功能:对输出的embedding结果做Hing损失计算
官方链接:https://pytorch.org/docs/stable/generated/torch.nn.HingeEmbeddingLoss.html

loss_f = nn.HingeEmbeddingLoss()
inputs = torch.tensor([[1., 0.8, 0.5]])
target = torch.tensor([[1, 1, -1]])
output = loss_f(inputs, target)

print('HingeEmbeddingLoss损失函数的计算结果为',output)
余弦相似度
torch.nn.CosineEmbeddingLoss(margin=0.0, size_average=None, reduce=None, reduction='mean')

功能:对两个向量做余弦相似度
官方链接:https://pytorch.org/docs/stable/generated/torch.nn.CosineEmbeddingLoss.html

loss_f = nn.CosineEmbeddingLoss()
inputs_1 = torch.tensor([[0.3, 0.5, 0.7],[0.3, 0.5, 0.7]])
inputs_2 = torch.tensor([[0.1, 0.3, 0.5],[0.1, 0.3, 0.5]])
target = torch.tensor([[1, -1]], dtype=torch.float)
output = loss_f(inputs_1, inputs_2, target)

print('CosineEmbeddingLoss损失函数的计算结果为',output)
CTC损失函数
torch.nn.CTCLoss(blank=0, reduction='mean',zero_infinity=False)

功能:用于解决时序类数据的分类
官方链接:https://pytorch.org/docs/stable/generated/torch.nn.CTCLoss.html

# Target are to be padded
T = 50      # Input sequence length
C = 20      # Number of classes (including blank)
N = 16      # Batch size
S = 30      # Target sequence length of longest target in batch (padding length)
S_min = 10  # Minimum target length, for demonstration purposes

# Initialize random batch of input vectors, for *size = (T,N,C)
input = torch.randn(T, N, C).log_softmax(2).detach().requires_grad_()

# Initialize random batch of targets (0 = blank, 1:C = classes)
target = torch.randint(low=1, high=C, size=(N, S), dtype=torch.long)

input_lengths = torch.full(size=(N,), fill_value=T, dtype=torch.long)
target_lengths = torch.randint(low=S_min, high=S, size=(N,), dtype=torch.long)
ctc_loss = nn.CTCLoss()
loss = ctc_loss(input, target, input_lengths, target_lengths)
loss.backward()


# Target are to be un-padded
T = 50      # Input sequence length
C = 20      # Number of classes (including blank)
N = 16      # Batch size

# Initialize random batch of input vectors, for *size = (T,N,C)
input = torch.randn(T, N, C).log_softmax(2).detach().requires_grad_()
input_lengths = torch.full(size=(N,), fill_value=T, dtype=torch.long)

# Initialize random batch of targets (0 = blank, 1:C = classes)
target_lengths = torch.randint(low=1, high=T, size=(N,), dtype=torch.long)
target = torch.randint(low=1, high=C, size=(sum(target_lengths),), dtype=torch.long)
ctc_loss = nn.CTCLoss()
loss = ctc_loss(input, target, input_lengths, target_lengths)
loss.backward()

print('CTCLoss损失函数的计算结果为',loss)

参考链接
https://datawhalechina.github.io/thorough-pytorch/第三章/index.html#

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/1038379.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号