赞
踩
上来不多说先上预测精度图,所有的预测都是基于未知的数据进行预测并不是训练预测!MAE的误差大概在0.4左右,数据是基于过去一年的进行模型的训练,输入的数据有许多列后面我也会进行讲解如何使用该模型进行预测你们自己的数据。
首先我们要对时间序列概念有一个基本的了解
时间序列预测大致分为两种一种是单元时间序列预测另一种是多元时间序列预测
单元时间序列预测是指只考虑一个时间序列的预测模型。它通常用于预测单一变量的未来值,例如股票价格、销售量等。在单元时间序列预测中,我们需要对历史数据进行分析,确定趋势、季节性和周期性等因素,并使用这些因素来预测未来的值。
常见的单元时间序列预测模型有
多元时间序列预测是指考虑多个时间序列的预测模型。它通常用于预测多个相关变量的未来值,例如股票价格和市场指数等。在多元时间序列预测中,我们需要对每个变量的历史数据进行分析,并确定它们之间的关系。然后,我们可以使用这些关系来预测未来的值。
常见的多元时间序列预测模型有
总的来说,单元时间序列预测和多元时间序列预测都是非常有用的预测模型,可以帮助我们更好地了解和预测未来的趋势。
但是如今多元时间序列预测在各种场景中越来越受欢迎,如电力预测、天气预测和交通流量估计。随着计算资源和模型架构的扩展,深度学习技术,包括基于rnn的模型和基于cnn的模型,已经取得了比传统统计方法更好的预测性能。由于捕获长期依赖关系,Transformer最近被用于捕获时间序列预测中的长期时间相关性,并显示出有希望的结果。
MTS-Mixers的代码包含许多个模块和许多个部分组成,所以我下面的讲解只会讲解其中的关键部分也会修改其中的一些BUG地方,代码地址推荐大家到Github上面进行下载,下面附上超链接地址点击即可跳转
(提示:加载进去以后点击右上角的code下面的download zip 即可下载到本地)
如果点击不开多点击几遍即可如果实在点击不开我后期会发布百度网盘版本下载链接方式(我自己已经调试好bug之后的代码点击即可运行结果)
提供给大家CSDN的下载地址
时间序列预测模型实战案例深度学习华为MTS-Mixers模型资源-CSDN文库
首先先对其运行文件(程序的入口)进行一个讲解包括其中的参数含义,如果你想要提高预测精度就要对这些参数要有一个具体的理解,否则光运用模型而不去调节其中的超参数效果不一定会好!
- parser.add_argument('--is_training', type=int, default=1, help='status')
- parser.add_argument('--model', type=str, default='Transformer',
- help='model name, options: [Transformer, Linear, NLinear, DLinear, SCINet, ConvFC, MTSMixer, MTSMatrix, FNet]')
-
- # data loader
- parser.add_argument('--data', type=str, default='custom', help='dataset type')
- parser.add_argument('--root_path', type=str, default='./', help='root path of the data file')
- parser.add_argument('--data_path', type=str, default='sum14.csv', help='data file')
- parser.add_argument('--features', type=str, default='MS',
- help='forecasting task, options:[M, S, MS]; M:multivariate predict multivariate, S:univariate predict univariate, MS:multivariate predict univariate')
- parser.add_argument('--target', type=str, default='sl', help='target feature in S or MS task')
- parser.add_argument('--freq', type=str, default='t',
- help='freq for time features encoding, options:[s:secondly, t:minutely, h:hourly, d:daily, b:business days, w:weekly, m:monthly], you can also use more detailed freq like 15min or 3h')
- parser.add_argument('--checkpoints', type=str, default='./checkpoints/', help='location of model checkpoints')
-
- # forecasting task
- parser.add_argument('--seq_len', type=int, default=128, help='input sequence length')
- parser.add_argument('--label_len', type=int, default=64, help='start token length')
- parser.add_argument('--pred_len', type=int, default=4, help='prediction sequence length')
-
- parser.add_argument('--individual', action='store_true', default=False,
- help='DLinear: a linear layer for each variate(channel) individually')
- parser.add_argument('--seg', type=int, default=20, help='prediction plot segments')
- parser.add_argument('--rev', action='store_true', default=False, help='whether to apply RevIN')
- parser.add_argument('--norm', action='store_false', default=True, help='whether to apply LayerNorm')
- parser.add_argument('--fac_T', action='store_true', default=False,
- help='whether to apply factorized temporal interaction')
- parser.add_argument('--sampling', type=int, default=2,
- help='the number of downsampling in factorized temporal interaction')
- parser.add_argument('--fac_C', action='store_true', default=False,
- help='whether to apply factorized channel interaction')
- parser.add_argument('--refine', action='store_true', default=False, help='whether to refine the linear prediction')
- parser.add_argument('--mat', type=int, default=0, help='option: [0-random, 1-identity]')
-
- # model
- parser.add_argument('--embed_type', type=int, default=0,
- help='0: default 1: value embedding + temporal embedding + positional embedding 2: value embedding + positional embedding')
- parser.add_argument('--enc_in', type=int, default=5, help='encoder input size')
- parser.add_argument('--dec_in', type=int, default=5, help='decoder input size')
- parser.add_argument('--c_out', type=int, default=1, help='output size')
- parser.add_argument('--d_model', type=int, default=512, help='dimension of model')
- parser.add_argument('--n_heads', type=int, default=1, help='num of heads')
- parser.add_argument('--e_layers', type=int, default=2, help='num of encoder layers')
- parser.add_argument('--d_layers', type=int, default=1, help='num of decoder layers')
- parser.add_argument('--d_ff', type=int, default=2048, help='dimension of fcn')
- parser.add_argument('--moving_avg', type=int, default=24, help='window size of moving average')
- parser.add_argument('--factor', type=int, default=1, help='attn factor')
- parser.add_argument('--dropout', type=float, default=0.05, help='dropout')
- parser.add_argument('--embed', type=str, default='timeF',
- help='time features encoding, options:[timeF, fixed, learned]')
- parser.add_argument('--activation', type=str, default='gelu', help='activation')
- parser.add_argument('--output_attention', action='store_true', help='whether to output attention in ecoder')
- parser.add_argument('--do_predict', action='store_true',default='True', help='whether to predict unseen future data')
-
- # optimization
- parser.add_argument('--num_workers', type=int, default=0, help='data loader num workers')
- parser.add_argument('--itr', type=int, default=1, help='experiments times')
- parser.add_argument('--train_epochs', type=int, default=10, help='train epochs')
- parser.add_argument('--batch_size', type=int, default=16, help='batch size of train input data')
- parser.add_argument('--patience', type=int, default=3, help='early stopping patience')
- parser.add_argument('--learning_rate', type=float, default=0.001, help='optimizer learning rate')
- parser.add_argument('--loss', type=str, default='mse', help='loss function')
- parser.add_argument('--lradj', type=str, default='type1', help='adjust learning rate')
- parser.add_argument('--use_amp', action='store_true', help='use automatic mixed precision training', default=False)
-
- # GPU
- parser.add_argument('--use_gpu', type=bool, default=True, help='use gpu')
- parser.add_argument('--gpu', type=int, default=0, help='gpu')
- parser.add_argument('--use_multi_gpu', action='store_true', help='use multiple gpus', default=False)
- parser.add_argument('--devices', type=str, default='0,1,2,3', help='device ids of multile gpus')
- parser.add_argument('--test_flop', action='store_true', default=False, help='See utils/tools for usage')
首先其中的参数部分分为几个部分分别为
(dataloader,forecasting task,model,optimization, gpu)
分为以上五个部分,我会选择其中的一些关键参数进行讲解
parser.add_argument('--is_training', type=int, default=1, help='status')
这就是决定是否训练,输入的类型必须是整数类型,则会进行训练!
- parser.add_argument('--model', type=str, default='Transformer',
- help='model name, options: [Transformer, Linear, NLinear, DLinear, SCINet, ConvFC, MTSMixer, MTSMatrix, FNet]')
这一部分是模型的选择,首先这个模型是由一遍论文发布出来的,所以他肯定要有一些对比的模型来衬托自己模型的优点,所以这一部分是进行一个模型的选择,其中Transformer就是本文所提到的模型MTS-Mixers,后面的模型分别为
Linear模型:是一个线性模型,适用于简单的线性回归问题。
NLinear模型:是一个非线性模型,可以处理非线性问题,例如多项式回归。
DLinear模型:是一个动态线性模型,可以处理随时间变化的线性关系。
SCINet模型:是一个基于自注意力机制的神经网络模型,可以用于时间序列预测和其他相关任务。
ConvFC模型:是一个卷积神经网络和全连接神经网络的结合体,可以处理时间序列数据。
MTSMixer模型:是一个基于多头自注意力机制的神经网络模型,可以用于时间序列预测和其他相关任务。
MTSMatrix模型:是一个基于矩阵分解的神经网络模型,可以处理多变量时间序列数据。
FNet模型:是一个基于傅里叶变换的神经网络模型,可以用于时间序列预测和其他相关任务。
注意这些模型呢其实都各有各自的优点如果本文的模型并没有给你一个好的精度对于你的数据你可以换着其它的模型进行尝试,总之时间序列预测并不是越复杂的模型越好,所有的选择都要基于你的数据进行选择。
parser.add_argument('--data', type=str, default='custom', help='dataset type')
这一部分是重点
如果你想要运行你自己的数据进行预测,这里需要根据你自己的数据进行输入!
首先我们来看下面的图!
如果你想要预测自己的数据则需要在参数部分的default位置后面输入custom,其中的ETTh1 ETTh2 ETTm1 ETTm2 是官方的数据集进行预测的选项,当大家没有数据的时候可以用来进行预测,我也会把官方数据提供给大家进行预测,
所以如果你用的是ETTh1数据集就可以填写ETTh1即可,如果你用的是自己的数据集进行预测就填入custom即可!!
这里我放一下官方的数据集标题给大家先看一下
官方的数据集就是这个形式是用前七列数据进行预测第八列数据OT,当然也可以将每一列的结果全部预测出来!
- parser.add_argument('--root_path', type=str, default='./', help='root path of the data file')
- parser.add_argument('--data_path', type=str, default='sum14.csv', help='data file')
这两个参数没什么好讲的就是你文件的目录所在,你的输入必须是csv格式的文件,如果你的是excel文件格式可以用Pandas转一下或者用Excel自带的功能转存一下, 像我的文件就存在和当前运行文件的同级目录下面!
- parser.add_argument('--features', type=str, default='MS',
- help='forecasting task, options:[M, S, MS]; )
重点参数!
这一个参数代表着你预测的方式,其中选项有三个分别是[M, S, MS],
情况一:假如你是做单变量预测,就是你的时间序列数据只包含两列一列是时间一列是你预测的数据, 那就填写S!
情况二:假如你的数据是多元时间序列预测,那么就可以有两个选项一个是M另一个是MS
我先介绍一下英文就是Multi-single和Multi-Multi,其中M和MS就是他们的首字母,所以大家应该理解是什么意思了,M就是多元变量预测多元变量,拿官方的数据举例,假设有7个变量你想要预测其中的几列数据就是多元预测多元,
情况三:MS就是多元预测单元数据,假设你输入的数据是7列输出的数据是一列数据就是多元预测单元也就是MS(Multi-single)
举例:我将以上图片所看到的数据全部输入的模型内部,得到OT一列的未来情况就填写MS!
parser.add_argument('--target', type=str, default='OT', help='target feature in S or MS task')
这一参数,就是代表你在上一个参数所讲的时候填写的是MS或者M就需要填写,我们这里还拿官方的数据举例,类似于假设你输入的是MS在上一个features参数的位置,那么这一参数就需要填写你数据当中需要被预测出来的列明标题,类似于官方的数据我们想要预测的是OT这一列这里我们就填写OT,假设你想要预测HUFL这一列数据就填写HUFL即可!
- parser.add_argument('--freq', type=str, default='t',
- help='freq for time features encoding, options:[s:secondly, t:minutely, h:hourly, d:daily, b:business days, w:weekly, m:monthly],
这一参数就代表你时间信息那一列,因为大家做的是时间序列模型如果没有时间的一列怎么能叫时间序列预测呢?所以这一列可选的参数有[s:secondly, t:minutely, h:hourly, d:daily, b:business days, w:weekly, m:monthly]
其中的含义就像英文所述的那样我就不在赘述了,下面来讲一下怎么填写
假设你的两列数据当中的间隔是分钟你就填写(t)如果是小时你就填写(h)以此类推,假设你的间隔不是一分钟或者一小时你也可以在他们前面加上系数,类似于15t这种,2h这种
- parser.add_argument('--seq_len', type=int, default=128, help='input sequence length')
- parser.add_argument('--label_len', type=int, default=64, help='start token length')
- parser.add_argument('--pred_len', type=int, default=4, help='prediction sequence length')
提高精度的重点!
这三个参数拿到一起来讲,其中seq_len,label_len,pred_len是相互之间有影响的,
其中seq_len就是你输入到模型内部一个长度,类似于你用过去128条数据去预测未来一个数据,那么这里就填写128!
当然这个数字不能够随便填写,往往和你模型的周期性和季节性有一定的关系,类似于sin或者cos函数一样有周期性,我们如果想预测未来的一个数据当然最好选够一个周期的长度数据来预测,这样就能包含所有的特征预测的往往也就更准,
当然如果你不知道怎么看你数据的周期性对数据进行一个建模的操作去分析可以在评论区留言后期我会教大家用EXCEL或者Matplotlib或者PoewBi进行一些简单的建模操作从而分析其中的周期性季节性等因素!
label_len就代表一个预测的预热性质你也可以理解为权重更大的部分这样更好的理解一点,可以这样想对于一个时间序列来说,肯定越新的数据越有价值,所以这个数据可以理解为更注重那一部分的数据(这只是为了让大家更好的理解这么解释),如果不理解可以看一下下面的图。
pred_len就好理解了你想预测未来多少个时间段的数据就填写几即可,
在我的数据当中我填写的是seq_len=128,label_len=64,pred_len=4,可以供大家参考,其中每一个参数都要相互影响如果大家想得到一个好的精度就要多做实验多尝试!
Model参数部分
- parser.add_argument('--enc_in', type=int, default=7, help='encoder input size')
- parser.add_argument('--dec_in', type=int, default=7, help='decoder input size')
- parser.add_argument('--c_out', type=int, default=1, help='output size')
这三个参数就代表着编码器和解码器的部分,
我们拿MS来举例,假设你输入的数据就是官方的数据,其中OT是你需要预测的列数,那么你的特征就是其余的7列数据,目标列就是OT列,那么enc_in和dec_in就填写7,c_out就填写1
如果你填写的是M那么就输入到少列数据就填写多少数字即可,S的话就都是1即可
参数部分我就讲到这里,因为参数实在是太多了,用文字的形式很难全部叙述出来如果大家有需要想更深入的了解,可以评论区留言,我也会进行回复解答!
这里修复一个小BUG(算是吧)
parser.add_argument('--do_predict', action='store_true', help='whether to predict unseen future data')
这一个参数就是进行是否进行预测的选项,需要在action后面增加一个参数,让其进行预测否则光训练模型在验证集和测试集评估没有意义,default='True'在action参数后面添加即可预测未知的数据!
这个官方模型最大的问题在于他输入的数据进行了归一化处理,而对于预测出来的数据并没有归一化处理,假设我们输入的数据是1000,那经过StandardScaler()处理以后,变为一个深度学习模型比较喜欢的数字范围(-1,-1)之间,那么他在预测输出的时候也是(-1,-1)之间的一个数字,并没有反转化为我们1000附近的数据,所以这算一个很大的bug,类似于下图。
如何修复这一个bug呢,
在data_provider文件目录下面的data_loader文件下面我们需要修改一部分内容,
在Class类名为class Dataset_Pred(Dataset):下面将下面这段代码复制粘贴替代他后面的所有代码,(注意只替换方法read_data以后的代码)
- def __read_data__(self):
- self.scaler = StandardScaler()
- self.y_scaler = StandardScaler()
- df_raw = pd.read_csv(os.path.join(self.root_path,
- self.data_path)) # 因为进行预测的时候代码会自动帮我们取最后的数据我们只需做好拼接即可
-
- '''
- df_raw.columns: ['date', ...(other features), target feature]
- '''
- if self.cols:
- cols = self.cols.copy()
- cols.remove(self.target)
- else:
- cols = list(df_raw.columns)
- cols.remove(self.target)
- cols.remove('date')
- df_raw = df_raw[['date'] + cols + [self.target]]
- border1 = len(df_raw) - self.seq_len
- border2 = len(df_raw)
-
- if self.features == 'M' or self.features == 'MS':
- cols_data = df_raw.columns[1:]
- df_data = df_raw[cols_data]
- elif self.features == 'S':
- df_data = df_raw[[self.target]]
-
- if self.scale:
- self.scaler.fit(df_data.values[:, :4])
- self.y_scaler.fit(df_data.values[:, 4:5])
- data1 = self.scaler.transform(df_data.values[:, :4])
- data2 = self.y_scaler.transform(df_data.values[:, 4:5])
- data = np.concatenate((data1,data2), axis=1)
-
- else:
- data = df_data.values
-
- tmp_stamp = df_raw[['date']][border1:border2]
- tmp_stamp['date'] = pd.to_datetime(tmp_stamp.date)
- pred_dates = pd.date_range(tmp_stamp.date.values[-1], periods=self.pred_len + 1, freq=self.freq)
-
- df_stamp = pd.DataFrame(columns=['date'])
- df_stamp.date = list(tmp_stamp.date.values) + list(pred_dates[1:])
- if self.timeenc == 0:
- df_stamp['month'] = df_stamp.date.apply(lambda row: row.month, 1)
- df_stamp['day'] = df_stamp.date.apply(lambda row: row.day, 1)
- df_stamp['weekday'] = df_stamp.date.apply(lambda row: row.weekday(), 1)
- df_stamp['hour'] = df_stamp.date.apply(lambda row: row.hour, 1)
- df_stamp['minute'] = df_stamp.date.apply(lambda row: row.minute, 1)
- df_stamp['minute'] = df_stamp.minute.map(lambda x: x // 15)
- data_stamp = df_stamp.drop(['date'], 1).values
- elif self.timeenc == 1:
- data_stamp = time_features(pd.to_datetime(df_stamp['date'].values), freq=self.freq)
- data_stamp = data_stamp.transpose(1, 0)
-
- self.data_x = data[border1:border2]
- if self.inverse:
- self.data_y = df_data.values[border1:border2]
- else:
- self.data_y = data[border1:border2]
- self.data_stamp = data_stamp
-
- def __getitem__(self, index):
- s_begin = index
- s_end = s_begin + self.seq_len
- r_begin = s_end - self.label_len
- r_end = r_begin + self.label_len + self.pred_len
-
- seq_x = self.data_x[s_begin:s_end]
- if self.inverse:
- seq_y = self.data_x[r_begin:r_begin + self.label_len]
- else:
- seq_y = self.data_y[r_begin:r_begin + self.label_len]
- seq_x_mark = self.data_stamp[s_begin:s_end]
- seq_y_mark = self.data_stamp[r_begin:r_end]
-
- return seq_x, seq_y, seq_x_mark, seq_y_mark
-
- def __len__(self):
- return len(self.data_x) - self.seq_len + 1
-
- def inverse_transform(self, data):
- return self.y_scaler.inverse_transform(data.detach().cpu().numpy())
在上面的代码部分主要我将两部分的标准化分开处理了,
- if self.scale:
- self.scaler.fit(df_data.values[:, :4])
- self.y_scaler.fit(df_data.values[:, 4:5])
- data1 = self.scaler.transform(df_data.values[:, :4])
- data2 = self.y_scaler.transform(df_data.values[:, 4:5])
- data = np.concatenate((data1,data2), axis=1)
其中主要修改了这两部分,因为假设前面的features部分输入的是MS这里就可以这么修改,
其中的4就是你输入数据的特征数我们还拿官方的数据举例,
其中前七列是特征数,OT列是要预测的列,那么在
- self.scaler.fit(df_data.values[:, :4])
- self.y_scaler.fit(df_data.values[:, 4:5])
其中fit将数字替换为6,后面的替换为6:7即可,因为我们的索引数字从0开始所以这么替换,所以这里其实就是和你前面的编码器和解码器部分的数字是相关的,前面在
- data1 = self.scaler.transform(df_data.values[:, :4])
- data2 = self.y_scaler.transform(df_data.values[:, 4:5])
同理按上面进行替换, 即可,
修改完上面之后我们就只需修改最后一部分即可
在exp_main.py文件当中我们添加一行代码即可
我们在最后一个方法predict下面,大约在280行左右进行一个替换
- outputs = outputs[0]
- outputs = data_set.inverse_transform(outputs)
- preds.append(outputs)
- print(preds)
- preds = np.array(preds)
- preds = preds.reshape(-1, preds.shape[-2], preds.shape[-1])
将这段代码进行一个替换即可替换之后的代码长下面这个样子
- with torch.no_grad():
- for _, (batch_x, batch_y, batch_x_mark, batch_y_mark) in enumerate(pred_loader):
- batch_x = batch_x.float().to(self.device)
- batch_y = batch_y.float()
- batch_x_mark = batch_x_mark.float().to(self.device)
- batch_y_mark = batch_y_mark.float().to(self.device)
-
- dec_inp = torch.zeros([batch_y.shape[0], self.args.pred_len, batch_y.shape[2]]).float().to(batch_y.device)
- dec_inp = torch.cat([batch_y[:, :self.args.label_len, :], dec_inp], dim=1).float().to(self.device)
-
- if self.args.use_amp:
- with torch.cuda.amp.autocast():
- if self.args.model in non_transformer:
- outputs = self.model(batch_x)
- else:
- outputs = self.model(batch_x, batch_x_mark, dec_inp, batch_y_mark)[0] if self.args.output_attention else \
- self.model(batch_x, batch_x_mark, dec_inp, batch_y_mark)
- else:
- if self.args.model in non_transformer:
- outputs = self.model(batch_x)
- else:
- outputs = self.model(batch_x, batch_x_mark, dec_inp, batch_y_mark)[0] if self.args.output_attention else \
- self.model(batch_x, batch_x_mark, dec_inp, batch_y_mark)
- outputs = outputs[0]
- outputs = data_set.inverse_transform(outputs)
- preds.append(outputs)
- print(preds)
- results.append(outputs[0][0])
- preds = np.array(preds)
- preds = preds.reshape(-1, preds.shape[-2], preds.shape[-1])
-
- # result save
- folder_path = './results/' + setting + '/'
- if not os.path.exists(folder_path):
- os.makedirs(folder_path)
-
- np.save(folder_path + 'real_prediction.npy', preds)
经过上面的处理之后我们的模型已经可以正常的开始训练和预测和测试了,
我们将所有的文件配置好之后就可以运行run.py文件
运行之后稍等控制台就可以看到模型已经开始训练了,等待时间跟你数据有关,其中有一部分代码变更部分我并没有讲因为涉及到公司的业务逻辑!但是并不影响你们进行正常的预测。
当训练完成之后模型就会自动进行预测,控制台会打印输出结果,并且预测结果会保存到同级目录下面
在同级目录result下面会保存每次预测的结果,因为其保存的结果是npy的格式文件需要numpy进行一个转化才可以读取其内容,可以另建一个py文件输入如下代码即可打印结果!
- import numpy as np
-
- forecast = np.load('results/Transformer_sum14_ftMS_sl64_ll32_pl4_dm512_nh1_el2_dl1_df2048_fc1_ebtimeF_0/real_prediction.npy')
-
- print(forecast)
其中的地址要换成你自己的即可,其实也可以将结果转化为外部的excel或者csv格式通过Dataframe格式然后进行转化即可,有需求可以评论区留言我也会进行更新,
我是通过转到csv文件通过excel作了一些数据可视化的工作,下面展示一下结果
预测值和真实值折线图
这是我做得两个预测不同的目标的预测结果大家可以参考一下,
MAE损失的截图
后期我也会讲一些最新的预测模型包括Informer,TPA-LSTM,ARIMA,XGBOOST,Holt-winter,移动平均法等等一系列关于时间序列预测的模型,包括深度学习和机器学习方向的模型我都会讲,你可以根据需求选取适合你自己的模型进行预测,如果有需要可以+个关注,包括本次模型我自己的代码大家有需要我也会放出百度网盘下载链接!!
其它时间序列预测模型讲解
----------------------------------------------------------Holt-Winters------------------------------------------------------
时间序列预测模型实战案例(二)(Holt-Winter)(Python)结合K-折交叉验证进行时间序列预测实现企业级预测精度(包括运行代码以及代码讲解)
----------------------------------------------------------LSTM---------------------------------------------------------------
时间序列预测模型实战案例(三)(LSTM)(Python)(深度学习)时间序列预测(包括运行代码以及代码讲解)
如果大家有不懂的也可以评论区留言一些报错什么的大家可以讨论讨论看到我也会给大家解答如何解决!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。