本文共 9879 字,大约阅读时间需要 32 分钟。
我们福禄网络致力于为广大用户提供智能化充值服务,包括各类通信充值卡(比如移动、联通、电信的话费及流量充值)、游戏类充值卡(比如王者荣耀、吃鸡类点券、AppleStore充值、Q币、斗鱼币等)、生活服务类(比如肯德基、小鹿茶等),网娱类(比如QQ各类钻等),作为一个服务提供商,商品质量的稳定、持续及充值过程的便捷一直是我们在业内的口碑。
在整个商品流通过程中,如何做好库存的管理,以充分提高库存运转周期和资金使用效率,一直是个难题。基于此,我们提出了智能化的库存管理服务,根据订单数据及商品数据,来预测不同商品随着时间推移的日常消耗情况。目前成熟的时间序列预测算法很多,但商业领域性能优越的却不多,经过多种尝试,给大家推荐2种时间序列算法:facebook开源的Prophet算法和LSTM深度学习算法。
现将个人理解的2种算法特性予以简要说明:time,product,cnt2019-10-01 00,**充值,62019-10-01 00,***游戏,3682019-10-01 00,***,12019-10-01 00,***,112019-10-01 00,***游戏,172019-10-01 00,三网***,392019-10-01 00,**网,62019-10-01 00,***,2
字段说明:
时间序列一般不进行特征处理,当然可以根据具体情况进行归一化处理或是取对数处理等。
目前待选的算法主要有2种:
以上是理论上的调参步骤,但我们在实际情况下在建议使用grid_search(网格寻参)方式,直接简单效果好。当机器性能不佳时网格调参配合理论调参方法可以加快调参速度。建议初学者使用手动调参方式以理解每个参数对模型效果的影响。
holiday.csv
import pandas as pdimport numpy as npimport matplotlib.pyplot as pltfrom fbprophet import Prophetdata = pd.read_csv('../data/data2.csv', parse_dates=['time'], index_col='time')def get_product_data(name, rule=None): product = data[data['product'] == name][['cnt']] product.plot() if rule is not None: product = product.resample(rule).sum() product.reset_index(inplace=True) product.columns = ['ds', 'y'] return productholidays = pd.read_csv('holiday.csv', parse_dates=['ds'])holidays['lower_window'] = -1holidays = holidays.append(pd.DataFrame({ 'holiday': '双11', 'ds': pd.to_datetime(['2019-11-11', '2020-11-11']), 'lower_window': -1, 'upper_window': 1,})).append(pd.DataFrame({ 'holiday': '双12', 'ds': pd.to_datetime(['2019-12-12', '2020-12-12']), 'lower_window': -1, 'upper_window': 1,}))def predict(name, rule='1d', freq='d', periods=1, show=False): ds = get_product_data(name, rule=rule) if ds.shape[0] < 7: return None m = Prophet(holidays=holidays) m.fit(ds) future = m.make_future_dataframe(freq=freq, periods=periods) # 建立数据预测框架,数据粒度为天,预测步长为一年 forecast = m.predict(future) if show: m.plot(forecast).show() # 绘制预测效果图 m.plot_components(forecast).show() # 绘制成分趋势图 mse = forecast['yhat'].iloc[ds.shape[0]] - ds['y'].values mse = np.abs(mse) / (ds['y'].values + 1) return [name, mse.mean(), mse.max(), mse.min(), np.quantile(mse, 0.9), np.quantile(mse, 0.8), mse[-7:].mean(), ds['y'].iloc[-7:].mean()]if __name__ == '__main__': products = set(data['product']) p = [] for i in products: y = predict(i) if y is not None: p.append(y) df = pd.DataFrame(p, columns=['product', 'total_mean', 'total_max', 'total_min', '0.9', '0.8', '7_mean', '7_real_value_mean']) df.set_index('product', inplace=True) product_sum: pd.DataFrame = data.groupby('product').sum() df = df.join(product_sum) df.sort_values('cnt', ascending=False, inplace=True) df.to_csv('result.csv', index=False)
结果如下:由于行数较多这里只展示前1行
根据结果,对比原生数据,可以得出如下结论:
就算法与产品的匹配性可分为3个类型:A. 因素分解图
上图中主要分为3个部分,分别对应prophet 3大要素,趋势、节假日或特殊日期、周期性(包括年周期、月周期、week周期、天周期以及用户自定义的周期)下面依照上面因素分解图的顺序依次对图进行说明:LSTM(长短记忆网络)主要用于有先后顺序的序列类型的数据的深度学习网络。是RNN的优化版本。一般用于自然语言处理,也可用于时间序列的预测。
简单来说就是,LSTM一共有三个门,输入门,遗忘门,输出门, i 、o、 f 分别为三个门的程度参数, g 与RNN中的概念一致。公式里可以看到LSTM的输出有两个,细胞状态c 和隐状态 h,c是经输入、遗忘门的产物,也就是当前cell本身的内容,经过输出门得到h,就是想输出什么内容给下一单元。
import numpy as npimport pandas as pdimport matplotlib.pyplot as pltimport torchfrom torch import nnfrom sklearn.preprocessing import MinMaxScalerts_data = pd.read_csv('../data/data2.csv', parse_dates=['time'], index_col='time')def series_to_supervised(data, n_in=1, n_out=1, dropnan=True): n_vars = 1 if type(data) is list else data.shape[1] df = pd.DataFrame(data) cols, names = list(), list() # input sequence (t-n, ... t-1) for i in range(n_in, 0, -1): cols.append(df.shift(i)) names += [('var%d(t-%d)' % (j + 1, i)) for j in range(n_vars)] # forecast sequence (t, t+1, ... t+n) for i in range(0, n_out): cols.append(df.shift(-i)) if i == 0: names += [('var%d(t)' % (j + 1)) for j in range(n_vars)] else: names += [('var%d(t+%d)' % (j + 1, i)) for j in range(n_vars)] # put it all together agg = pd.concat(cols, axis=1) agg.columns = names # drop rows with NaN values if dropnan: agg.dropna(inplace=True) return aggdef transform_data(feature_cnt=2): yd = ts_data[ts_data['product'] == '移动话费'][['cnt']] scaler = MinMaxScaler(feature_range=(0, 1)) yd_scaled = scaler.fit_transform(yd.values) yd_renamed = series_to_supervised(yd_scaled, n_in=feature_cnt).values.astype('float32') n_row = yd_renamed.shape[0] n_train = int(n_row * 0.7) train_X, train_y = yd_renamed[:n_train, :-1], yd_renamed[:n_train, -1] test_X, test_y = yd_renamed[n_train:, :-1], yd_renamed[n_train:, -1] # 最后,我们需要将数据改变一下形状,因为 RNN 读入的数据维度是 (seq, batch, feature),所以要重新改变一下数据的维度,这里只有一个序列,所以 batch 是 1,而输入的 feature 就是我们希望依据的几天,这里我们定的是两个天,所以 feature 就是 2. train_X = train_X.reshape((-1, 1, feature_cnt)) test_X = test_X.reshape((-1, 1, feature_cnt)) print(train_X.shape, train_y.shape, test_X.shape, test_y.shape) # 转化成torch 的张量 train_x = torch.from_numpy(train_X) train_y = torch.from_numpy(train_y) test_x = torch.from_numpy(test_X) test_y = torch.from_numpy(test_y) return scaler, train_x, train_y, test_x, test_yscaler, train_x, train_y, test_x, test_y = transform_data(24)# lstm 网络class lstm_reg(nn.Module): # 括号中的是python的类继承语法,父类是nn.Module类 不是参数的意思 def __init__(self, input_size, hidden_size, output_size=1, num_layers=2): # 构造函数 # inpu_size 是输入的样本的特征维度, hidden_size 是LSTM层的神经元个数, # output_size是输出的特征维度 super(lstm_reg, self).__init__() # super用于多层继承使用,必须要有的操作 self.rnn = nn.LSTM(input_size, hidden_size, num_layers) # 两层LSTM网络, self.reg = nn.Linear(hidden_size, output_size) # 把上一层总共hidden_size个的神经元的输出向量作为输入向量,然后回归到output_size维度的输出向量中 def forward(self, x): # x是输入的数据 x, _ = self.rnn(x) # 单个下划线表示不在意的变量,这里是LSTM网络输出的两个隐藏层状态 s, b, h = x.shape x = x.view(s * b, h) x = self.reg(x) x = x.view(s, b, -1) # 使用-1表示第三个维度自动根据原来的shape 和已经定了的s,b来确定 return xdef train(feature_cnt, hidden_size, round, save_path='model.pkl'): # 我使用了GPU加速,如果不用的话需要把.cuda()给注释掉 net = lstm_reg(feature_cnt, hidden_size) criterion = nn.MSELoss() optimizer = torch.optim.Adam(net.parameters(), lr=1e-2) for e in range(round): # 新版本中可以不使用Variable了 # var_x = Variable(train_x).cuda() # var_y = Variable(train_y).cuda() # 将tensor放在GPU上面进行运算 var_x = train_x var_y = train_y out = net(var_x) loss = criterion(out, var_y) optimizer.zero_grad() loss.backward() optimizer.step() if (e + 1) % 100 == 0: print('Epoch: {}, Loss:{:.5f}'.format(e + 1, loss.item())) # 存储训练好的模型参数 torch.save(net.state_dict(), save_path) return netif __name__ == '__main__': net = train(24, 8, 5000) # criterion = nn.MSELoss() # optimizer = torch.optim.Adam(net.parameters(), lr=1e-2) pred_test = net(test_x) # 测试集的预测结果 pred_test = pred_test.view(-1).data.numpy() # 先转移到cpu上才能转换为numpy # 乘以原来归一化的刻度放缩回到原来的值域 origin_test_Y = scaler.inverse_transform(test_y.reshape((-1,1))) origin_pred_test = scaler.inverse_transform(pred_test.reshape((-1,1))) # 画图 plt.plot(origin_pred_test, 'r', label='prediction') plt.plot(origin_test_Y, 'b', label='real') plt.legend(loc='best') plt.show() # 计算MSE # loss = criterion(out, var_y)? true_data = origin_test_Y true_data = np.array(true_data) true_data = np.squeeze(true_data) # 从二维变成一维 MSE = true_data - origin_pred_test MSE = MSE * MSE MSE_loss = sum(MSE) / len(MSE) print(MSE_loss)
转载地址:http://gxsuz.baihongyu.com/