基于RNN的Seq2Seq的基本假设:原始序列的最后一个隐含状态(一个向量)包含了该序列的全部信息。(这显然是不合理的)

Seg2Seg问题:记忆长序列能力不足

解决:当要生成一个目标语言单词的时候,不光考虑前一个时刻的状态和已经生成的单词,还要考虑当前要生成的单词和源句子中的哪些单词更加相关,即更关注源句子中的哪些词,这种做法就叫做注意力机制(Attention)

Attention

Luong等人在2015年发布的Effective Approaches to Attention-based Neural Machine Translation论文中,提出了attention技术,通过attention技术,seg2seg模型极大地提高了机器翻译的质量。

归其原因是:attention机制使得seg2seg模型可以有区分度、有重点地关注输入序列。

实例:

  1. 假设模型已经生成单词“我”之后,要生成下一个单词;
  2. 显然和源语言中“love”关系最大,因此将源语言句子中的“love”对应的状态乘以一个比较大的权重,如0.6,而其余词的权重则较小;
  3. 最终将源语言句子中每个单词对应的状态加权求和,并用作新状态更新一个额外输出。

结合Attention机制的Seg2Seg模型

  • 结合attention, seg2seg模型decoder每次更新状态的时候都会再看一遍encoder的所有状态(decoder会知道去关注哪里)

  • 在Encoder结束工作之后,Attention和Decoder同时开始工作

  • attention可以简单的理解为:一种有效的加权求和技术,关键点在于如何获得权重。

  • 计算权重:αi=aligh(hi,s0)\alpha_i=aligh(h_i,s_0)αi=aligh(hi,s0)
    (相当于计算hih_ihis0的相关性s_0的相关性s0
    hi为Encoder的隐藏层状态,s0为Encoder的最后一个隐藏层状态h_i为Encoder的隐藏层状态,s_0为Encoder的最后一个隐藏层状态hiEncoders0Encoder
    (权重为0-1之间的数,加起来等于1)
  • 计算方法:
  1. Linear maps(线性变换):
    ki=WK⋅hi,fori=1tomk_i=W_K·h_i,for i = 1 to mki=WKhifori=1tomq0=WQ⋅s0q_0=W_Q·s_0q0=WQs0
  2. Inner product(计算内积):
    αi~=kiTq0\tilde{\alpha_i} = \mathbf{k}^\mathrm{T}_iq_0αi~=kiTq0
  3. Normalization:
    [α1,⋅⋅⋅,αm]=softmax([α1~,⋅⋅⋅,αm~])[\alpha_1,···,\alpha_m] = softmax([\tilde{\alpha_1},···,\tilde{\alpha_m}])[α1,αm]=softmax([α1~,,αm~])

计算权重还有另一种方法,本文的代码中用到了,但现在更为主流的是第一种方法

  • 获得权重之后就是求取Context Vector:

c0=α1h1+⋅⋅⋅+αmhmc_0=\alpha_1h_1+···+\alpha_mh_mc0=α1h1++αmhm

  • 更新Decoder状态向量(s0=hms_0=h_ms0=hm)
  1. SimpleRNN
    s1=tanh(A′⋅[X1′s0]+b)s_1=tanh(A'·\begin{bmatrix}X_1'\\s_0\\ \end{bmatrix}+b)s1=tanh(A[X1s0]+b)
  2. SimpleRNN+Attention
    s1=tanh(A′⋅[X1′s0c0]+b)s_1=tanh(A'·\begin{bmatrix}X_1'\\s_0\\c_0\\ \end{bmatrix}+b)s1=tanh(AX1s0c0+b)
  • 缺点:计算量非常大
  • attention时间复杂度很高mtmtmt(encoder和decoder序列长度的乘积)

GRU

由于后面的代码用到了GRU,这里简单介绍一下。

GRU(Gate Recurrent Unit)是RNN的一种。和LSTM一样,也是为了解决长期记忆和反向传播中的梯度等问题而提出的。

GRU输入输出的结构和普通的RNN相似,其中内部思想与LSTM相似。

与LSTM相比,GRU内部少了一个Gate,参数相对更少,训练速度更快,但是也能达到和LSTM相当的功能。

GRU的输入输出

  • 和RNN一样,没什么好说的

GRU的内部结构

  1. 通过前一时刻状态ht−1h_{t-1}ht1和当前输入xtx_txt来获取两个门控状态
    重置门控:r=σ(Wr⋅[xtht−1])重置门控:r = \sigma(W^r·\begin{bmatrix}x_t\\h_{t-1}\\ \end{bmatrix})r=σ(Wr[xtht1])更新门控:z=σ(Wz⋅[xtht−1])更新门控:z = \sigma(W^z·\begin{bmatrix}x_t\\h_{t-1}\\ \end{bmatrix})z=σ(Wz[xtht1])

  2. 得到门控信息后,首先使用重置门控来得到重置之后的数据ht−1′=ht−1⋅rh_{t-1}^{'} = h_{t-1} \cdot rht1=ht1r再将ht−1′h_{t-1}^{'}ht1与输入xtx_txt进行拼接,再通过一个tanh函数来将数据缩放到-1~1之间h′=tanh(W[xtht−1′])h'=tanh(W\begin{bmatrix}x_t\\h_{t-1}^{'}\\ \end{bmatrix})h=tanh(W[xtht1])这里的h′h'h主要是包含了当前输入xtx_txt,有针对性的添加了上一时刻的隐藏层状态,类似与LSTM的forget gate。

  3. 更新记忆阶段
    在这个阶段,我们同时进行了遗忘和记忆两个步骤。
    先使用了之前得到的更新门控ht=(1−z)⋅ht−1+z⋅h′h_t = (1-z) \cdot h_{t-1} + z \cdot h'ht=(1z)ht1+zh
    GRU的关键点就在于此:使用更新门控zzz就可以同时进行遗忘和选择记忆

代码实现

任务:机器翻译
源语言:德语
目标语言:英语

  • 导入包
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as Ffrom torchtext.legacy import data,datasetsimport spacy
import numpy as npimport random
import mathimport de_core_news_sm

数据预处理

  • 设置种子
SEED = 1234random.seed(SEED)
np.random.seed(SEED)
torch.manual_seed(SEED)
torch.cuda.manual_seed(SEED)
torch.backends.cudnn.deterministic = True
  • 接下来,我们将加载spaCy模块,并为源语言和目标语言定义标记器。(这里比较麻烦,建议从spacy的github直接下载,然后安装)
spacy_de = spacy.load('de_core_news_sm')
spacy_en = spacy.load('en_core_web_sm')
  • 构建分词器
def tokenize_de(text):# Tokenizes German text from a string into a list of stringsreturn [tok.text for tok in spacy_de.tokenizer(text)]def tokenize_en(text):# Tokenizes English text from a string into a list of stringsreturn [tok.text for tok in spacy_en.tokenizer(text)]
  • 接下来,我们将设置决定如何处理数据的字段。我们附加了序列标记的开始和结束,并对所有文本进行小写。
SRC = data.Field(tokenize = tokenize_de, init_token = '<sos>', eos_token = '<eos>', lower = True)TRG = data.Field(tokenize = tokenize_en, init_token = '<sos>', eos_token = '<eos>', lower = True)

  • 我们加载训练集、验证集和测试集,生成dataset类。使用torchtext自带的Multi30k数据集,这是一个包含约30000个平行的英语、德语和法语句子的数据集,每个句子包含约12个单词。
train_data, valid_data, test_data = datasets.Multi30k.splits(exts = ('.de', '.en'),fields = (SRC, TRG))
  • 可以查看以下加载完的数据集
print(f"Number of training examples:{len(train_data.examples)}")
print(f"Number of validation examples:{len(valid_data.examples)}")
print(f"Number of testing examples:{len(test_data.examples)}")Number of training examples: 29000
Number of validation examples: 1014
Number of testing examples: 1000
  • 查看数据实例
vars(train_data.examples[0]){'src': ['zwei','junge','weiße','männer','sind','im','freien','in','der','nähe','vieler','büsche','.'],'trg': ['two','young',',','white','males','are','outside','near','many','bushes','.']}

  • 构建词汇表,将出现次数少于2次的任何标记转换为标记。
SRC.build_vocab(train_data, min_freq = 2)
TRG.build_vocab(train_data, min_freq = 2)
  • 查看一下生成的词表大小
print(f"Unique tokens in source (de) vocabulary:{len(SRC.vocab)}")
print(f"Unique tokens in target (en) vocabulary:{len(TRG.vocab)}")Unique tokens in source (de) vocabulary: 7855
Unique tokens in target (en) vocabulary: 5893
  • 查看词汇表中最常见的单词及其他们在数据集中出现的次数。
SRC.vocab.freqs.most_common(20)[('.', 28809),('ein', 18851),('einem', 13711),('in', 11895),('eine', 9909),(',', 8938),('und', 8925),('mit', 8843),('auf', 8745),('mann', 7805),('einer', 6765),('der', 4990),('frau', 4186),('die', 3949),('zwei', 3873),('einen', 3479),('im', 3107),('an', 3062),('von', 2363),('sich', 2273)]
  • 也可以使用 stoi (string to int) or itos (int to string) 方法,以下输出TRG-vocab的前10个词汇。
print(TRG.vocab.itos[:10])# ['<unk>', '<pad>', '<sos>', '<eos>', 'a', '.', 'in', 'the', 'on', 'man']

  • 设置GPU,构建迭代器
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')BATCH_SIZE = 64train_iterator, valid_iterator, test_iterator = data.BucketIterator.splits((train_data, valid_data, test_data), batch_size = BATCH_SIZE, device = device)
  • 查看一下生成的batch
batch = next(iter(train_iterator))
print(batch)[torchtext.legacy.data.batch.Batch of size 64 from MULTI30K][.src]:[torch.cuda.LongTensor of size 25x64 (GPU 0)][.trg]:[torch.cuda.LongTensor of size 28x64 (GPU 0)]

搭建模型

Encoder

Encoder用的是单层双向GRU

  • 双向GRU的隐藏层状态输出由两个向量拼接而成

    例如:h1=[h1→;hT←];h2=[h2→;hT−1←];..........h_1=[\overrightarrow{h_1};\overleftarrow{h_T}];h_2=[\overrightarrow{h_2};\overleftarrow{h_{T-1}}];..........h1=[h1

    ;hT

    ];h2=[h2

    ;hT1

    ];..........

    output={h1,h2,......hT}output=\{h_1,h_2,......h_T\}output={h1,h2,......hT}

  • 假设这是个m层的GRUhidden={[hT1→;h11←],[hT2→;h12←],......,[hTm→;h1m←]}hidden=\{[\overrightarrow{h_T^1};\overleftarrow{h_1^1}],[\overrightarrow{h_T^2};\overleftarrow{h_1^2}],......,[\overrightarrow{h_T^m};\overleftarrow{h_1^m}]\}hidden={[hT1

    ;h11

    ],[hT2

    ;h12

    ],......,[hTm

    ;h1m

    ]}

  • 我们需要的是hidden的最后一层输出(包括正向和反向),因此我们可以通过hidden[-2,:,:]hidden[-1,:,:]取出最后一层的hidden state,将他们拼接起来记作s0s_0s0

  • s0s_0s0的维度变换(我们需要将s0s_0s0维度变换到匹配decoder的初始隐藏状态)

    1. enc_hidden:[n_layersnum_directions, batch_size, hid_dim2]
    2. 由于是双向的,做concat:[batch_size, enc_hid_dim*x]
    3. 经过一个全连接层:[batch_size,dec_hid_dim]
    4. 剩下还需要进行unsqueeze 和 repeat,这将在下一步完成。
class Encoder(nn.Module):def __init__(self, input_dim, emb_dim, enc_hid_dim, dec_hid_dim, dropout):super().__init__()self.embedding = nn.Embedding(input_dim, emb_dim)self.rnn = nn.GRU(emb_dim, enc_hid_dim, bidirectional = True)self.fc = nn.Linear(enc_hid_dim * 2, dec_hid_dim)self.dropout = nn.Dropout(dropout)def forward(self, src): '''src = [src_len, batch_size]'''src = src.transpose(0, 1) # src = [batch_size, src_len]embedded = self.dropout(self.embedding(src)).transpose(0, 1) # embedded = [src_len, batch_size, emb_dim]# enc_output = [src_len, batch_size, hid_dim * num_directions]# enc_hidden = [n_layers * num_directions, batch_size, hid_dim]enc_output, enc_hidden = self.rnn(embedded) # if h_0 is not give, it will be set 0 acquiescently# enc_hidden is stacked [forward_1, backward_1, forward_2, backward_2, ...]# enc_output are always from the last layer# enc_hidden [-2, :, : ] is the last of the forwards RNN# enc_hidden [-1, :, : ] is the last of the backwards RNN# initial decoder hidden is final hidden state of the forwards and backwards# encoder RNNs fed through a linear layer# s = [batch_size, dec_hid_dim]s = torch.tanh(self.fc(torch.cat((enc_hidden[-2,:,:], enc_hidden[-1,:,:]), dim = 1)))# s就是隐藏层输出,之后作为decoder的初始隐藏状态# 由于维度不匹配,经历了一个全连接网络改变维度,以适应decoder的初始隐藏层维度。# 之后还需要unsqueeze(0)return enc_output, s

Attention


Et=tanh[attn(st−1,H)]E_t=tanh[attn(s_{t-1},H)]Et=tanh[attn(st1,H)]α~t=v⋅Et\tilde\alpha_t = v·E_tα~t=vEtαt=softmax(α~t)\alpha_t=softmax(\tilde\alpha_t)αt=softmax(α~t)

  1. st−1是指encoder的隐藏层输出s_{t-1}是指encoder的隐藏层输出st1encoder
  2. H指的是Encoder中的变量enc_ouputH指的是Encoder中的变量enc\_ouputHEncoderenc_ouput
  3. attn()其实就是一个简单的全连接神经网络。

维度变换:

  1. 首先将encoder传过来的s变换成[batch_size, src_len, dec_hid_dim]
  2. enc_output进行transpose–>[batch_sizr, src_len, enc_hid_dim*2]好让他们可以concat
  3. 运算第一个公式得到energy [batch_size,src_len, dec_hid_dim]
  4. 第二个公式,线性变换 [batch_size,src_len,1] 然后squeeze(2)去掉最后一个维度
  5. 最后就是softmax,不改变维度。

返回的就是权重值

class Attention(nn.Module):def __init__(self, enc_hid_dim, dec_hid_dim):super().__init__()self.attn = nn.Linear((enc_hid_dim * 2) + dec_hid_dim, dec_hid_dim, bias=False)self.v = nn.Linear(dec_hid_dim, 1, bias = False)def forward(self, s, enc_output):# s = [batch_size, dec_hid_dim]# enc_output = [src_len, batch_size, enc_hid_dim * 2]batch_size = enc_output.shape[1]src_len = enc_output.shape[0]# repeat decoder hidden state src_len times# s = [batch_size, src_len, dec_hid_dim]# enc_output = [batch_size, src_len, enc_hid_dim * 2]s = s.unsqueeze(1).repeat(1, src_len, 1)enc_output = enc_output.transpose(0, 1)# energy = [batch_size, src_len, dec_hid_dim]energy = torch.tanh(self.attn(torch.cat((s, enc_output), dim = 2)))# attention = [batch_size, src_len]attention = self.v(energy).squeeze(2)return F.softmax(attention, dim=1)

Decoder

  • Decoder用的是单向单层GRU

实际也就三个公式:

  1. 利用attention部分求得的权重计算context vectorc=αtHc=\alpha_tHc=αtH
  2. 更新decoder状态向量st=GRU(emb(yt),c,st−1)s_t=GRU(emb(y_t),c,s_{t-1})st=GRU(emb(yt),c,st1)
  3. yt^=f(emb(yt),c,st)\hat{y_t}=f(emb(y_t),c,s_t)yt^=f(emb(yt),c,st)
  • H指的是Encoder中的变量enc_ouputH指的是Encoder中的变量enc\_ouputHEncoderenc_ouput
  • emb(yt)指的是将dec_input经过WordEmbedding之后得到的结果emb(y_t)指的是将dec\_input经过WordEmbedding之后得到的结果emb(yt)dec_inputWordEmbedding
  • f()函数实际上就是为了转换维度f()函数实际上就是为了转换维度f()
  • torch.bmm 维度为3的两个tensor矩阵相乘

维度变换

  1. decoder中最开始先调用一次attention,得到权重αt\alpha_tαt,它的维度是[batch_size, src_len]
class Decoder(nn.Module):def __init__(self, output_dim, emb_dim, enc_hid_dim, dec_hid_dim, dropout, attention):super().__init__()self.output_dim = output_dimself.attention = attentionself.embedding = nn.Embedding(output_dim, emb_dim)self.rnn = nn.GRU((enc_hid_dim * 2) + emb_dim, dec_hid_dim)self.fc_out = nn.Linear((enc_hid_dim * 2) + dec_hid_dim + emb_dim, output_dim)self.dropout = nn.Dropout(dropout)def forward(self, dec_input, s, enc_output):# dec_input = [batch_size]# s = [batch_size, dec_hid_dim]# enc_output = [src_len, batch_size, enc_hid_dim * 2]dec_input = dec_input.unsqueeze(1) # dec_input = [batch_size, 1]embedded = self.dropout(self.embedding(dec_input)).transpose(0, 1) # embedded = [1, batch_size, emb_dim]# a = [batch_size, 1, src_len]a = self.attention(s, enc_output).unsqueeze(1)# enc_output = [batch_size, src_len, enc_hid_dim * 2]enc_output = enc_output.transpose(0, 1)# c = [1, batch_size, enc_hid_dim * 2]c = torch.bmm(a, enc_output).transpose(0, 1)# rnn_input = [1, batch_size, (enc_hid_dim * 2) + emb_dim]rnn_input = torch.cat((embedded, c), dim = 2)# dec_output = [src_len(=1), batch_size, dec_hid_dim]# dec_hidden = [n_layers * num_directions, batch_size, dec_hid_dim]dec_output, dec_hidden = self.rnn(rnn_input, s.unsqueeze(0))# embedded = [batch_size, emb_dim]# dec_output = [batch_size, dec_hid_dim]# c = [batch_size, enc_hid_dim * 2]embedded = embedded.squeeze(0)dec_output = dec_output.squeeze(0)c = c.squeeze(0)# pred = [batch_size, output_dim]pred = self.fc_out(torch.cat((dec_output, c, embedded), dim = 1))return pred, dec_hidden.squeeze(0)

Seg2Seg(with attention)

class Seq2Seq(nn.Module):def __init__(self, encoder, decoder, device):super().__init__()self.encoder = encoderself.decoder = decoderself.device = devicedef forward(self, src, trg, teacher_forcing_ratio = 0.5):# src = [src_len, batch_size]# trg = [trg_len, batch_size]# teacher_forcing_ratio is probability to use teacher forcingbatch_size = src.shape[1]trg_len = trg.shape[0]trg_vocab_size = self.decoder.output_dim# tensor to store decoder outputsoutputs = torch.zeros(trg_len, batch_size, trg_vocab_size).to(self.device)# enc_output is all hidden states of the input sequence, back and forwards# s is the final forward and backward hidden states, passed through a linear layerenc_output, s = self.encoder(src)# first input to the decoder is the <sos> tokensdec_input = trg[0,:]for t in range(1, trg_len):# insert dec_input token embedding, previous hidden state and all encoder hidden states# receive output tensor (predictions) and new hidden statedec_output, s = self.decoder(dec_input, s, enc_output)# place predictions in a tensor holding predictions for each tokenoutputs[t] = dec_output# decide if we are going to use teacher forcing or notteacher_force = random.random() < teacher_forcing_ratio# get the highest predicted token from our predictionstop1 = dec_output.argmax(1) # if teacher forcing, use actual next token as next input# if not, use predicted tokendec_input = trg[t] if teacher_force else top1return outputs

参考

  • RNN模型与NLP应用(8/9):Attention (注意力机制)
  • Seq2Seq (Attention) 的 PyTorch 实现
  • Datawhale(learning-nlp-with-transformers)
  • 人人都能看懂的GRU
  • Pytorch实现Seq2Seq模型:以机器翻译为例
  • 卷积序列到序列模型的学习(Convolutional Sequence to Sequence Learning)

Attention及其pytorch代码实现相关推荐

  1. 神经网络中的Attention机制 pytorch 代码

    注意力机制总述 引言 注意力分布 加权平均 注意力机制的变体 硬性注意力 键值对注意力 多头注意力 代码 引言 在计算能力有限的情况下,注意力机制作为一种资源分配方案,将有限的计算资源用来处理更为重要 ...

  2. 【NLP】Github标星7.7k+:常见NLP模型的PyTorch代码实现

    推荐github上的一个NLP代码教程:nlp-tutorial,教程中包含常见的NLP模型代码实现(基于Pytorch1.0+),而且教程中的大多数NLP模型都使用少于100行代码. 教程说明 这是 ...

  3. Vision Transformer(ViT)PyTorch代码全解析(附图解)

    Vision Transformer(ViT)PyTorch代码全解析 最近CV领域的Vision Transformer将在NLP领域的Transormer结果借鉴过来,屠杀了各大CV榜单.本文将根 ...

  4. AFM模型原理及Pytorch代码复现

    一.前言 该模型是和NFM模型结构上非常相似, 算是NFM模型的一个延伸,在NFM中, 不同特征域的特征embedding向量经过特征交叉池化层的交叉,将各个交叉特征向量进行"加和" ...

  5. 零基础入门--中文实体关系抽取(BiLSTM+attention,含代码)

    前面写过一片实体抽取的入门,实体关系抽取就是在实体抽取的基础上,找出两个实体之间的关系. 本文使用的是BiLSTM+attention模型,代码在这里,不定期对代码进行修改添加优化. 数据处理 其实数 ...

  6. 聊一聊计算机视觉中常用的注意力机制 附Pytorch代码实现

    聊一聊计算机视觉中常用的注意力机制以及Pytorch代码实现 注意力机制(Attention)是深度学习中常用的tricks,可以在模型原有的基础上直接插入,进一步增强你模型的性能.注意力机制起初是作 ...

  7. GAT:图注意力模型介绍及PyTorch代码分析

    文章目录 1.计算注意力系数 2.聚合 2.1 附录--GAT代码 2.2 附录--相关代码 3.完整实现 3.1 数据加载和预处理 3.2 模型训练 1.计算注意力系数 对于顶点 iii ,通过计算 ...

  8. 深度学习中一些注意力机制的介绍以及pytorch代码实现

    文章目录 前言 注意力机制 软注意力机制 代码实现 硬注意力机制 多头注意力机制 代码实现 参考 前言 因为最近看论文发现同一个模型用了不同的注意力机制计算方法,因此懵了好久,原来注意力机制也是多种多 ...

  9. Transformer Pytorch代码实现以及理解

    Transformer结构​​​​​​​ 论文:Attention is all you need Transformer模型是2017年Google公司在论文<Attention is All ...

最新文章

  1. Delphi程序员代码编写标准指南
  2. Linux内核很吊之 module_init解析 (下)【转】
  3. java redis缓存实例_spring项目整合ehcache和redis缓存实例
  4. idea将远程代码更新合并到本地_idea 本地调试远程服务器代码
  5. 一个在职的软件测试的日常工作是怎么样的?
  6. MySQL增强版命令行客户端连接工具(mycli)
  7. cesium加载批量模型
  8. x3850用uefi安装Linux7,X3850 X5在uEFI模式下无法安装Centos 6.2的解决办法
  9. 新手购买基金的买入策略
  10. 视频信息和信号的特点
  11. python处理grd格式文件_python基础
  12. 【_ 面試 】在单点登录中,如果 cookie 被禁用了怎么办?
  13. 保险资管需求多元化 壹资管平台赋能行业智能化转型
  14. 蓝牙耳机南卡和vivo哪个好用?南卡与vivo实际评测!
  15. Java发起GET请求的二三事
  16. 04 cefsharp谷歌浏览器多开页面的实现
  17. Android 10.0热点为Enhanced Open模式时不允许WiFI和热点同时开启代码流程梳理
  18. 嵌入式硬件学习之嵌入式软件和硬件的区别
  19. echarts 添加百分号
  20. D. Unusual Sequences (数论,质因子分解,dp)

热门文章

  1. 细说Java性能测试第四课 数据性能测试 结语
  2. 软件测试三阶段,你在哪一步?
  3. IntelliJ IDEA 的撤销和反撤销快捷键
  4. idea 的反撤销快捷键失效解决方案
  5. wireshark官网上下载最新及历史版本
  6. 7.21 封装和继承
  7. 机器学习实战2(有监督的机器学习)
  8. php 5.4 fastcgi,Windows Server 2012一键安装PHP环境(PHP5.4+FastCGI模式)_护卫神
  9. C/C++中 sizeof 详解
  10. 全民斩仙服务器维护,《全民斩仙2》停运公告