当前位置:   article > 正文

【全网最详细】使用PyTorch实现循环神经网络_循环神经网络 pytorch

循环神经网络 pytorch

目录

1. 什么是循环神经网络

2. PyTorch中的循环神经网络

3. 创建循环神经网络模型

小结

4.训练循环神经网络模型

5.评估循环神经网络模型


欢迎来到这篇使用PyTorch实现循环神经网络的教程!在这里,我将向您展示如何使用PyTorch创建、训练和评估一个循环神经网络(RNN),并将其应用于文本生成任务。这篇教程将涵盖以下主题:

1.什么是循环神经网络

2.PyTorch中的循环神经网络

3.创建循环神经网络模型

4.训练循环神经网络模型

5.评估循环神经网络模型

6.应用循环神经网络模型于文本生成

让我们开始吧!

1. 什么是循环神经网络

循环神经网络(RNN)是一种用于序列数据的神经网络,常用于语言建模、翻译和音乐生成等任务。RNN是基于前一个时间步的输入和当前时间步的状态来预测下一个时间步的输出。这使得RNN在处理连续数据时非常有效,因为它可以利用上一个时间步的信息来更新其状态并生成新的输出。

RNN模型的核心是“循环”结构。在传统的神经网络中,每个输入都是独立地处理的,而在RNN中,每个输入都与前一个时间步的输出相关联。在每个时间步,RNN将当前输入和前一个时间步的输出传递到一个称为“隐藏状态”的向量中。该隐藏状态捕获了当前输入和过去输入的信息,并将其用于预测下一个输出。

2. PyTorch中的循环神经网络

PyTorch是一个非常流行的深度学习框架,提供了许多构建和训练神经网络的工具。在PyTorch中,我们可以使用torch.nn模块中的RNN类和LSTM类来构建RNN模型。

RNN类是最基本的RNN类型。它接收一个输入张量和一个初始隐藏状态张量,并输出一个输出张量和一个最终隐藏状态张量。LSTM类是一种改进的RNN类型,它使用一种称为长短期记忆(Long Short-Term Memory,LSTM)的结构来记忆和管理状态。LSTM比RNN更适合处理长序列数据,因为它可以更好地处理梯度消失和梯度爆炸的问题。

在接下来的部分中,我们将使用PyTorch中的RNN类来构建一个简单的循环神经网络模型,并将其用于文本生成任务。

3. 创建循环神经网络模型

在这个示例中,我们将创建一个字符级别的文本生成模型,它将一个或多个字符作为输入,并生成下一个字符作为输出。我们将使用莎士比亚的一些作品作为我们的训练数据,以生成新的莎士比亚风格的文本。

首先,我们需要对文本进行预处理。我们将把所有的字符转换成小写,并将其表示为数字。我们还将创建一个字典,将每个字符映射到一个唯一的数字,并将这些数字用于训练模型。

  1. import string
  2. def preprocess(text):
  3. # 将文本转换为小写
  4. text = text.lower()
  5. # 删除所有标点符号
  6. text = text.translate(str.maketrans('', '', string.punctuation))
  7. # 创建一个字符到数字的映射表
  8. char_to_index = {char: i for i, char in enumerate(set(text))}
  9. index_to_char = {i: char for char, i in char_to_index.items()}
  10. # 将文本表示为数字
  11. text = [char_to_index[char] for char in text]
  12. return text, char_to_index, index_to_char

接下来,我们将创建一个TextDataset类,用于加载和处理训练数据。该类将接收一个预处理过的文本列表,并将其转换为PyTorch张量。

  1. import torch
  2. from torch.utils.data import Dataset
  3. class TextDataset(Dataset):
  4. def __init__(self, text):
  5. self.text = torch.tensor(text)
  6. def __len__(self):
  7. return len(self.text) - 1
  8. def __getitem__(self, idx):
  9. x = self.text[idx]
  10. y = self.text[idx+1]
  11. return x, y

接下来,我们将创建一个RNN类来实现我们的循环神经网络模型。该模型将包含一个嵌入层、一个RNN层和一个全连接层。嵌入层将输入字符的数字表示转换为向量表示,RNN层将处理序列数据并生成隐藏状态,全连接层将根据隐藏状态生成输出。

  1. import torch.nn as nn
  2. class RNN(nn.Module):
  3. def __init__(self, input_size, hidden_size, output_size):
  4. super(RNN, self).__init__()
  5. self.embedding = nn.Embedding(input_size, hidden_size)
  6. self.rnn = nn.RNN(hidden_size, hidden_size, batch_first=True)
  7. self.fc = nn.Linear(hidden_size, output_size)
  8. def forward(self, x, h):
  9. x = self.embedding(x)
  10. out, h = self.rnn(x, h)
  11. out = self.fc(out)
  12. return out, h

我们还需要定义一些超参数,例如序列长度、批大小、隐藏层大小和训练迭代次数。

  1. # 定义超参数
  2. sequence_length = 100
  3. batch_size = 128
  4. hidden_size = 256
  5. num_layers = 2
  6. num_epochs = 50
  7. learning_rate = 0.001

我们还需要定义一个函数来生成输入和目标序列。该函数将根据指定的序列长度将文本数据分割成多个序列,并将每个序列转换为PyTorch张量。

  1. def generate_sequence_data(text, sequence_length, batch_size):
  2. num_sequences = len(text) // sequence_length
  3. text = text[:num_sequences * sequence_length]
  4. x_data = text[:-1]
  5. y_data = text[1:]
  6. x_batches = torch.split(x_data, batch_size)
  7. y_batches = torch.split(y_data, batch_size)
  8. sequence_data = []
  9. for i in range(num_sequences // batch_size):
  10. x = x_batches[i * batch_size:(i + 1) * batch_size]
  11. y = y_batches[i * batch_size:(i + 1) * batch_size]
  12. sequence_data.append((torch.stack(x), torch.stack(y)))
  13. return sequence_data

接下来,我们将加载并预处理我们的训练数据,并将其转换为PyTorch数据集。

  1. with open('shakespeare.txt', 'r', encoding='utf-8') as f:
  2. text = f.read()
  3. text, char_to_index, index_to_char = preprocess(text)
  4. dataset = TextDataset(text)

现在,我们可以创建一个DataLoader对象,用于加载数据并将其分成批次。

dataloader = torch.utils.data.DataLoader(dataset, batch_size=batch_size, shuffle=True)

接下来,我们将创建一个RNN对象,并定义我们的损失函数和优化器。

  1. model = RNN(len(char_to_index), hidden_size, len(char_to_index)).to(device)
  2. criterion = nn.CrossEntropyLoss()
  3. optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)

现在我们已经准备好训练我们的模型了。在每个训练迭代中,我们将向模型提供一个输入序列并计算模型的输出。我们将使用损失函数来计算模型输出和目标序列之间的差异,并使用反向传播算法更新模型参数。

  1. for epoch in range(num_epochs):
  2. total_loss = 0
  3. h = torch.zeros(num_layers, batch_size, hidden_size).to(device)
  4. for i, (x, y) in enumerate(dataloader):
  5. x = x.to(device)
  6. y = y.to(device)
  7. optimizer.zero_grad()
  8. out, h = model(x, h)
  9. loss = criterion(out.view(-1, len(char_to_index)), y.view(-1))
  10. loss.backward()
  11. optimizer.step()
  12. total_loss += loss.item()
  13. if (i+1) % 100 == 0:
  14. print('Epoch [{}/{}], Step [{}/{}], Loss: {:.4f}'
  15. .format(epoch+1, num_epochs, i+1, len(dataset)//batch_size, total_loss / (i+1)))

在训练完成后,我们可以使用我们的训练好的模型来生成文本。我们可以通过提供一个起始字符并循环多次生成文本。在每次生成文本时,我们将向模型提供当前字符并获取下一个字符的预测

  1. def generate_text(model, char_to_index, index_to_char, sequence_length, seed_text, num_chars):
  2. with torch.no_grad():
  3. h = torch.zeros(num_layers, 1, hidden_size).to(device)
  4. seed = [char_to_index[char] for char in seed_text]
  5. input = torch.tensor(seed).unsqueeze(1).to(device)
  6. output = []
  7. for i in range(num_chars):
  8. out, h = model(input, h)
  9. out = out[-1]
  10. prob = nn.functional.softmax(out, dim=0).cpu().numpy()
  11. index = np.random.choice(len(char_to_index), p=prob)
  12. output.append(index_to_char[index])
  13. input = torch.tensor([index]).unsqueeze(1).to(device)
  14. return ''.join(output)

现在,我们可以使用以下代码段训练我们的模型并生成文本。

  1. # 定义超参数
  2. sequence_length = 100
  3. batch_size = 128
  4. hidden_size = 256
  5. num_layers = 2
  6. num_epochs = 50
  7. learning_rate = 0.001
  8. # 加载数据集
  9. with open('shakespeare.txt', 'r', encoding='utf-8') as f:
  10. text = f.read()
  11. text, char_to_index, index_to_char = preprocess(text)
  12. dataset = TextDataset(text)
  13. # 创建数据加载器
  14. dataloader = torch.utils.data.DataLoader(dataset, batch_size=batch_size, shuffle=True)
  15. # 创建模型,损失函数和优化器
  16. model = RNN(len(char_to_index), hidden_size, len(char_to_index)).to(device)
  17. criterion = nn.CrossEntropyLoss()
  18. optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
  19. # 训练模型
  20. for epoch in range(num_epochs):
  21. total_loss = 0
  22. h = torch.zeros(num_layers, batch_size, hidden_size).to(device)
  23. for i, (x, y) in enumerate(dataloader):
  24. x = x.to(device)
  25. y = y.to(device)
  26. optimizer.zero_grad()
  27. out, h = model(x, h)
  28. loss = criterion(out.view(-1, len(char_to_index)), y.view(-1))
  29. loss.backward()
  30. optimizer.step()
  31. total_loss += loss.item()
  32. if (i+1) % 100 == 0:
  33. print('Epoch [{}/{}], Step [{}/{}], Loss: {:.4f}'
  34. .format(epoch+1, num_epochs, i+1, len(dataset)//batch_size, total_loss / (i+1)))
  35. # 生成文本
  36. generated_text = generate_text(model, char_to_index, index_to_char, sequence_length, 'shall i compare thee to a summer\'s day?\n', 1000)
  37. print(generated_text)

这将训练我们的循环神经网络,并使用它生成一段长度为1000个字符的文本。您可以尝试不同的超参数和训练数据来创建不同的模型和生成文本。

小结

在本教程中,我们介绍了循环神经网络(RNN)及其应用,使用 PyTorch 实现了一个基本的字符级 RNN 模型,以生成文本为例子。我们首先预处理文本数据集,然后定义了一个 RNN 模型。我们使用 Cross Entropy 损失函数和 Adam 优化器来训练我们的模型,并通过生成一些示例文本来验证它是否有效。

总的来说,RNN 在处理序列数据时非常有用,如自然语言处理、时间序列预测等。PyTorch 为开发人员提供了非常方便的工具和接口,使其能够轻松地构建和训练深度学习模型。同时,使用 GPU 可以加速模型的训练和推理过程。因此,我们希望本教程能够为您提供有关 PyTorch 和 RNN 的基础知识,并启发您开始尝试更复杂的深度学习任务。

接下来,我们将介绍如何训练循环神经网络模型、评估循环神经网络模型,并将循环神经网络模型应用于文本生成。

4.训练循环神经网络模型

我们的模型已经定义好了,接下来就是要训练模型。我们需要为模型提供训练数据,然后通过计算损失和反向传播更新模型参数。在此之前,我们需要设置一些训练的超参数,例如学习率、迭代次数等等。

  1. learning_rate = 0.005
  2. n_epochs = 2000
  3. print_every = 100
  4. plot_every = 10
  5. hidden_size = 100
  6. # Create RNN model and set loss and optimizer
  7. rnn = RNN(n_characters, hidden_size, n_characters)
  8. loss_fn = nn.CrossEntropyLoss()
  9. optimizer = torch.optim.Adam(rnn.parameters(), lr=learning_rate)
  10. # Initialize loss and time variables
  11. current_loss = 0
  12. all_losses = []
  13. start_time = time.time()

然后我们开始训练循环神经网络模型:

  1. for epoch in range(1, n_epochs + 1):
  2. # Get input and target tensors from random batch
  3. input_tensor, target_tensor = random_training_example(all_characters, n_characters,
  4. data, chunk_len) # Initialize hidden state hidden = rnn.init_hidden()
  5. # Zero the gradients
  6. optimizer.zero_grad()
  7. # Forward pass
  8. loss, hidden = rnn(input_tensor, target_tensor, hidden)
  9. # Backward pass
  10. loss.backward()
  11. # Update parameters
  12. optimizer.step()
  13. # Update loss
  14. current_loss += loss.item()
  15. # Print progress
  16. if epoch % print_every == 0:
  17. print(f'Epoch [{epoch}/{n_epochs}], Loss: {current_loss/print_every:.4f}')
  18. current_loss = 0
  19. # Save the model and the optimizer every 500 epochs
  20. if epoch % 500 == 0:
  21. torch.save(rnn.state_dict(), f'./models/rnn_{epoch}.pth')
  22. torch.save(optimizer.state_dict(), f'./models/optimizer_{epoch}.pth')
  23. # Record the average loss every `plot_every` epochs
  24. if epoch % plot_every == 0:
  25. all_losses.append(current_loss / plot_every)
  26. current_loss = 0

在训练过程中,我们首先从数据集中随机获取一批训练样本。然后,我们初始化隐藏状态,将梯度归零,通过前向传播计算损失,然后通过反向传播更新模型参数。最后,我们记录每个时期的损失,并在训练完成后将损失可视化。在训练过程中,我们还可以保存模型和优化器的状态,以便稍后恢复模型。

5.评估循环神经网络模型

一旦我们训练好了模型,我们就需要评估模型的表现。我们可以使用一些指标来衡量模型的表现,例如准确率、精度、召回率、F1 值等等。对于文本生成任务,我们通常使用困惑度(perplexity)作为评估指标。困惑度表示模型对于给定序列的预测能力,计算方法为:

perplexity=e1Ni=1Nlogp(xi)

其中,$N$ 是序列的长度,$p(x_i)$ 是模型对于第 $i$ 个字符的预测概率。

下面是一个评估模型的示例代码:

  1. def evaluate(rnn, data, prime_str='A', predict_len=100, temperature=0.8):
  2. # Initialize hidden state
  3. hidden = rnn.init_hidden()
  4. # Initialize input sequence
  5. prime_input = char_tensor(prime_str)
  6. # Generate prime sequence
  7. for p in range(len(prime_str) - 1):
  8. _, hidden = rnn(prime_input[p].unsqueeze(0), hidden)
  9. # Initialize predicted sequence
  10. predicted = prime_str
  11. # Generate predicted sequence
  12. for p in range(predict_len):
  13. # Forward pass
  14. output, hidden = rnn(prime_input[-1].unsqueeze(0), hidden)
  15. # Sample from the network as a multinomial distribution
  16. output_dist = output.data.view(-1).div(temperature).exp() top_i =
  17. torch.multinomial(output_dist, 1)[0]
  18. # Add predicted character to string and use as next input
  19. predicted_char = all_characters[top_i]
  20. predicted += predicted_char
  21. prime_input = char_tensor(predicted_char)
  22. # Calculate perplexity
  23. perplexity = calc_perplexity(rnn, data)
  24. return predicted, perplexity
  25. def calc_perplexity(rnn, data):
  26. # Set loss function
  27. loss_fn = nn.CrossEntropyLoss()
  28. # Initialize variables
  29. loss = 0
  30. n_chars = 0
  31. # Iterate over all chunks in data
  32. for chunk in data:
  33. # Get input and target tensors
  34. input_tensor = char_tensor(chunk[:-1])
  35. target_tensor = char_tensor(chunk[1:])
  36. # Initialize hidden state
  37. hidden = rnn.init_hidden()
  38. # Forward pass
  39. output, hidden = rnn(input_tensor, target_tensor, hidden)
  40. # Calculate loss
  41. loss += loss_fn(output.view(-1, n_characters), target_tensor.view(-1)).item() * len(chunk)
  42. # Update number of characters
  43. n_chars += len(chunk) - 1
  44. # Calculate perplexity
  45. perplexity = np.exp(loss / n_chars)
  46. return perplexity

我们首先定义了一个 evaluate 函数,该函数接受一个已经训练好的模型、一个初始字符串、一个生成的序列长度和一个温度参数。我们使用初始字符串生成一个起始序列,并利用模型生成一个长度为 predict_len 的序列。然后我们计算该序列的困惑度,并返回生成的序列和困惑度。

计算困惑度的过程在 calc_perplexity 函数中。我们使用交叉熵损失函数计算每个序列的损失,然后累加损失和字符数。最后,我们将损失除以字符数,并使用指数函数计算 perplexity 值。

最后,我们来看一下如何将循环神经网络应用于文本生成任务。对于文本生成任务,我们的目标是根据前面的一段文本生成下一个字符或单词。我们可以使用训练好的循环神经网络模型来预测下一个字符或单词的概率分布,并根据该分布来进行采样。具体来说,我们可以执行以下步骤:

1.给定一个初始字符串和循环神经网络模型。

2.使用初始字符串生成一个起始序列,将其输入到循环神经网络模型中。

3.在模型的输出中,选择一个概率最高的字符作为预测的下一个字符,并将该字符添加到序列的末尾。

4.将预测的字符作为输入,重复步骤 3 直到生成所需长度的序列。

在生成文本时,我们通常会添加一个温度参数来控制生成文本的多样性。温度参数越高,生成的文本越随机;温度参数越低,生成的文本越保守。以下是一个生成文本的示例代码:

  1. def generate_text(rnn, prime_str='A', predict_len=100, temperature=0.8):
  2. # Initialize hidden state
  3. hidden = rnn.init_hidden()
  4. # Initialize input sequence
  5. prime_input = char_tensor(prime_str)
  6. # Generate prime sequence
  7. for p in range(len(prime_str) - 1):
  8. _, hidden = rnn(prime_input[p].unsqueeze(0), hidden)
  9. # Initialize predicted sequence
  10. predicted = prime_str
  11. # Generate predicted sequence
  12. for p in range(predict_len):
  13. # Forward pass
  14. output, hidden = rnn(prime_input[-1].unsqueeze(0), hidden)
  15. # Sample from the network as a multinomial distribution
  16. output_dist = output.data.view(-1).div(temperature).exp() top_i =
  17. torch.multinomial(output_dist, 1)[0]
  18. # Add predicted character to string and use as next input
  19. predicted_char = all_characters[top_i]
  20. predicted += predicted_char
  21. prime_input = char_tensor(predicted_char)
  22. return predicted

我们可以使用该函数来生成指定长度的文本。我们首先使用初始字符串生成一个起始序列,并利用模型生成一个长度为 predict_len 的序列。然后我们使用温度参数对生成的序列进行采样,并返回生成的文本。

在这个过程中,我们还需要一些辅助函数来将字符和单词转换为数字表示。以下是将字符转换为数字表示的辅助函数:

  1. def char_tensor(string):
  2. tensor = torch.zeros(len(string)).long()
  3. for c in range(len(string)):
  4. tensor[c] = all_characters.index(string[c])
  5. return tensor

该函数接受一个字符串作为输入,并将其转换为一个长为字符串长度的整数张量。每个元素都是对应字符在字符表中的索引。

对于单词级别的循环神经网络,我们需要一个不同的辅助函数来将单词转换为数字表示。以下是将单词转换为数字表示的辅助函数:

  1. def word_tensor(word, word_to_ix):
  2. tensor = torch.zeros(len(word)).long()
  3. for w in range(len(word)):
  4. tensor[w] = word_to_ix[word[w]]
  5. return tensor

该函数接受一个单词和单词表 word_to_ix 作为输入,并将单词转换为一个长为单词长度的整数张量。每个元素都是对应单词在单词表中的索引。

接下来,我们需要一些训练和评估循环神经网络模型的函数。以下是训练函数的代码:

  1. def train(rnn, criterion, optimizer, train_data, n_epochs=200, print_every=100, plot_every=10):
  2. # Initialize losses and perplexities
  3. current_loss = 0
  4. all_losses = []
  5. current_perplexity = 0
  6. all_perplexities = []
  7. # Train loop
  8. for epoch in range(1, n_epochs + 1):
  9. # Initialize hidden state
  10. hidden = rnn.init_hidden()
  11. # Shuffle training data
  12. random.shuffle(train_data)
  13. # Train on each example
  14. for i, (input_seq, target_seq) in enumerate(train_data):
  15. # Clear gradients
  16. optimizer.zero_grad()
  17. # Convert input and target sequences to tensors
  18. input_tensor = input_seq.unsqueeze(0)
  19. target_tensor = target_seq.unsqueeze(0)
  20. # Forward pass
  21. output, hidden = rnn(input_tensor, hidden)
  22. # Calculate loss and perplexity
  23. loss = criterion(output.view(-1, n_categories), target_tensor.view(-1))
  24. perplexity = torch.exp(loss)
  25. # Backward pass and optimization step
  26. loss.backward()
  27. optimizer.step()
  28. # Update current loss and perplexity
  29. current_loss += loss.item()
  30. current_perplexity += perplexity.item()
  31. # Print progress
  32. if (i + 1) % print_every == 0:
  33. print('Epoch [{}/{}], Step [{}/{}], Loss: {:.4f}, Perplexity: {:.4f}'
  34. .format(epoch, n_epochs, i + 1, len(train_data),
  35. current_loss / print_every, current_perplexity / print_every))
  36. current_loss = 0
  37. current_perplexity = 0
  38. # Add loss and perplexity to list
  39. all_losses.append(current_loss)
  40. all_perplexities.append(current_perplexity)
  41. # Plot losses and perplexities
  42. if epoch % plot_every == 0:
  43. plot_loss(all_losses, epoch)
  44. plot_perplexity(all_perplexities, epoch)

该函数接受循环神经网络模型 rnn、损失函数 criterion、优化器 optimizer、训练数据 train_data、训练周期数 n_epochs、每隔多少步打印一次进度 print_every、每隔多少周期绘制一次损失和困惑度图表 plot_every 作为输入。

在训练过程中,我们首先初始化损失和困惑度的变量 current_loss 和 current_perplexity,然后进行训练循环。在每个训练周期中,我们首先初始化隐藏状态 hidden,然后将训练数据随机打乱。接着,我们对每个训练样本进行训练,具体步骤如下:

1.清除梯度。

2.将输入序列和目标序列转换为张量。

3.前向传播,计算输出序列和新的隐藏状态。

4.计算损失和困惑度。

5.反向传播和优化。

6.更新当前损失和困惑度。

7.如果达到了打印进度的步数,则打印进度,并重置当前损失和困惑度。

在每个训练周期结束后,我们将当前损失和困惑度添加到列表中,并在每隔一定周期数时绘制损失和困惑度图表。

以下是评估函数的代码:

  1. def evaluate(rnn, criterion, data):
  2. # Initialize loss and perplexity
  3. total_loss = 0 total_perplexity = 0
  4. # Loop through data
  5. with torch.no_grad():
  6. for input_seq, target_seq in data:
  7. # Initialize hidden state
  8. hidden = rnn.init_hidden()
  9. # Convert input and target sequences to tensors
  10. input_tensor = torch.tensor(input_seq, dtype=torch.long).view(-1, 1)
  11. target_tensor = torch.tensor(target_seq, dtype=torch.long).view(-1, 1)
  12. # Forward pass
  13. output, hidden = rnn(input_tensor, hidden)
  14. loss = criterion(output, target_tensor)
  15. # Update total loss and perplexity
  16. total_loss += loss.item()
  17. total_perplexity += np.exp(loss.item())
  18. # Calculate average loss and perplexity
  19. avg_loss = total_loss / len(data)
  20. avg_perplexity = total_perplexity / len(data)
  21. return avg_loss, avg_perplexity

该函数接受循环神经网络模型 `rnn`、损失函数 `criterion` 和测试数据 `data` 作为输入。在评估过程中,我们初始化损失和困惑度变量 `total_loss` 和 `total_perplexity`,然后对测试数据进行循环。对于每个测试样本,我们首先初始化隐藏状态 `hidden`,然后将输入序列和目标序列转换为张量。接着,我们进行前向传播,计算输出序列和新的隐藏状态,并计算损失和困惑度。最后,我们将总损失和困惑度除以测试样本数,计算平均损失和困惑度。 最后,我们来看一下如何使用训练好的循环神经网络模型进行文本生成。以下是文本生成函数的代码:未完待续。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/繁依Fanyi0/article/detail/697741
推荐阅读
相关标签
  

闽ICP备14008679号