msra 面经

Q1:

要求用go语言进行设计

Q2:
Q2: 以太坊网络每隔一段时间就会产生一个新的区块(block)。 这个区块里面包含的所有交易(transactions)消耗的gas一般为一个零到八百万左右的整数。 我们需要构建一个服务预测下 k 块(k=1,2,4,8,16,32,64)的总gas消­耗量。 假设有个数据源能在以太坊产生一个新块时,把这个新块的信息传送给我们的服务程序。 我们的服务程序在每收到一个新块时,立即给出下 k 块的总gas消耗量的预测。 请尝试为这个服务设计一个预测算法或是训练一个预测模型,并在给定的数据集上评估你所设计算法的预测效果。 数据集为一万个整数的序列(见附件),对应连续一万块每块的gas的消耗量。 请注意,任意给出一种清晰的算法或模型即可,不需要追求效果最­优或者性能最优的算法或模型。 请把结果写成一个简要的实验报告(一页左右,不超过两页),报告要求如下: (1)请自己定义标准来衡量预测效果 (2)请自己定义模型、划分训练集以及测试集、模型参数等等 (3)适当总结可改进的方向

解答如下:

基于Keras的LSTM大窗口多变量预测

模型定义:

建立了一个lstm模型,将每次block消耗gas的行为看作时间序列,使用滑动窗口的模式判断下k次gas的总消耗量。

lstm模型参数:输入维度=3,输出维度=1,滑动窗口大小=128

输入数据:

[原数据,窗口均值,窗口方差]

我构造了一个新的三维数据集。举个例子,假如定义滑动窗口大小为为128,在第n(n>128)个索引处的内容为

[数据:csv文件的原数据,

窗口均值:前128个原数据的均值,

窗口方差:前128个原数据的方差

]n<128时窗口均值和方差置为平均数。

超参数设置:

滑动窗口大小:128

训练集测试集划分:7:3

epoches:训练次数为100

隐藏层为:50

输入维度:3

输出维度:1

time_stamp:等于滑动窗口大小

batch_size:等于滑动窗口大小

评价指标:

/Users/yangchengran/Desktop/Figure_1.png
损失函数

损失函数采用Mean Absolute Error(MAE)

优化算法采用Adam

模型评估指标:RMSE(均方根误差)

最终测试集RMSE:975905.750(还是太高)

预测方式:

根据当前窗口均值预测下一个窗口均值,将当前得到的数据作为输入,判断下k个gas的总消耗。

题目思路:

最开始观察数据规律,做回归和时序分析,结果不理想。

接下来考虑对数据进行清洗,但理解题意后我认为数据不应该被修改(不应该定义异常点)。

接下来调研预测方法。受到jdd2018赛题的启发,考虑到该行为类似于时序,计划使用arima和lstm模型求解。

分析两种模型的特点,因为lstm能学习长期依赖,选择用lstm模型。

查看文档,建模并调参。

最开始建立仅输入一维原数据的lstm时间序列模型,性能指标rmse太高(超过两百万),考虑调参调优。

调参无法解决问题,考虑修改模型。

受到exposure bias的启发,考虑如何减少由于模型无法识别新的分布带来的误差传播。

引入窗口的均值和方差数据,建立新的数据集合,对预测数据进行一定的修正。

编写代码,运行模型,对比之前的模型有一定的效果提升

难点:

理解lstm的原理

如何利用数据特征少的数据进行建模

编写多维数据进行窗口滑动的代码

改进方向:

1.在模型运行上花了太多时间,周末两天时间有点捉襟见肘,考虑对模型进行gpu加速。

2.该模型没有来得及用网格搜索或者贝叶斯方式调参优化,应该补上

3.应该使用传统数据处理方式建模进行效果对比,比如gbdt决策树等。

4.应该了解区块链gas消耗的原理,才能更好的对数据进行处理,找到更好的数据特征和选择更好的模型。

5.应该考虑模型的组合,比如arima和lstm模型的组合使用等。

6.精简代码

7.模型细节没有做好,没有对测试结果画图分析,代码没有模块化,没有提供k的输入选项等。

附录代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
import numpy
import matplotlib.pyplot as plt
import math
import pandas
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import LSTM
from sklearn.preprocessing import MinMaxScaler
from sklearn.metrics import mean_squared_error
from sklearn.metrics import explained_variance_score, mean_absolute_error, mean_squared_error, r2_score
import matplotlib
from pandas import read_csv
from datetime import datetime
from sklearn import preprocessing
from matplotlib import pyplot



#参数名
look_back=128
#窗口长度,建议大一点

n_out=1
#观测数据output(y)的步长, 范围为[0, len(data)-1], 默认为1

split=0.66
#划分训练集比例

timesteps=look_back
#timesteps

epochs=100
#训练次数

hidden=50
#隐藏层层数

n_features=3
#特征的维度


#思路,要增加一个特征,该窗口内的SUM,MIN和MA(后两者考虑删去),或者方差?
#先对DATAFRAME进行操作
def change_sum(dataset,look_back):
dataX = []
t = dataset.sum()
for i in range(look_back):
dataX.append(t/len(dataset))
sum = 0
for i in range(len(dataset) – look_back ): #
a = dataset[i:(i + look_back), 0]
for j in range(len(a)):
sum = a[j]+ sum
dataX.append(sum/look_back)
sum = 0
return list(dataX)


def change_max(dataset,look_back):
dataX = []
for i in range(look_back):
dataX.append(0)
for i in range(len(dataset) – look_back ): #
a = dataset[i:(i + look_back), 0]
aa = a.tolist()
dataX.append((max(aa)))
return list(dataX)

def change_min(dataset,look_back):
dataX = []
for i in range(look_back):
dataX.append(0)
for i in range(len(dataset) – look_back ): #
a = dataset[i:(i + look_back), 0]
aa = a.tolist()
dataX.append((min(aa)))
return list(dataX)

def change_var(dataset,look_back):
dataX = []
t=dataset.var()
for i in range(look_back):
dataX.append(t)
for i in range(len(dataset) – look_back ): #
a = dataset[i:(i + look_back), 0]
dataset_1 = numpy.mat(a)
dataX.append(dataset_1.var())
return list(dataX)

dataframe = pandas.read_csv(‘/Users/yangchengran/Desktop/data10000.csv’, usecols=[1], engine=‘python’, skipfooter=3)
dataset = dataframe.values #通过.values得到dataframe的值,返回shape是(10000, 1)的数组形式
dataset = dataset.astype(‘float32’) #把dataset里的所有数据从整型变为浮点型
#返回DATA list
data_1=[]
for i in range(len(dataset)):
data_a=dataset.tolist()
data_1.append(data_a[i][0])


#增加SUM维度
data_sum = change_sum(dataset,look_back)#SUM维度的LIST


#增加MAX维度
data_max = change_max(dataset,look_back)

#增加MIN维度,0这种极值会变多,会不会更影响模型的效果,把特殊值的影响扩大
data_min = change_min(dataset,look_back)

#增加方差维度
data_var=change_var(dataset,look_back)


#建立多维DATAFRAME

data_set = {
‘date’:data_1,
‘data_var’:data_var,
‘data_sum’:data_sum,
#LABEL略过
}
dataset = pandas.DataFrame(data = data_set)


# https://blog.csdn.net/qq_30219017/article/details/79539376
#从时间序列转换为监督学习的数据格式
def series_to_supervised(data, n_in, n_out=1, dropnan=True):
n_vars = 1 if type(data) is list else data.shape[1]
df = pandas.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 = pandas.concat(cols, axis=1)
agg.columns = names
# drop rows with NaN values
if dropnan:
agg.dropna(inplace=True)
return agg

# 存储
values = dataset.values

# 转换成浮点数
values = values.astype(‘float32’)

# 特征值转换
scaler = MinMaxScaler(feature_range=(0, 1))
scaled = scaler.fit_transform(values)

# 从时间序列转换为监督学习的数据格式,COPY如下:
# https://blog.csdn.net/qq_30219017/article/details/79539376
reframed = series_to_supervised(scaled, timesteps, n_out)


# 划分测试集和训练集,验证集待定,66%:34%
values = reframed.values
n_train = int(len(values)*split)
train = values[:n_train, :]
test = values[n_train:, :]

# 划分输入和输出,这里分别读前面所有列和最后一列,n_obs是为了进行多维的计算
n_obs = timesteps * n_features

train_X, train_y = train[:, :n_obs], train[:, –n_features]
test_X, test_y = test[:, :n_obs], test[:, –n_features]


# 将格式更改为三元,方便模型建立 [samples, timesteps, features]
train_X = train_X.reshape((train_X.shape[0], timesteps, n_features))
test_X = test_X.reshape((test_X.shape[0], timesteps, n_features))
print(train_X.shape, train_y.shape, test_X.shape, test_y.shape)

# LSTM模型中,隐藏层有50个神经元,输出层1个神经元(回归问题),
# 输入变量是N个时间步(t-1)的特征,损失函数采用Mean Absolute Error(MAE),
# 优化算法采用Adam,模型采用50个epochs并且每个batch的大小为72。
model = Sequential()
model.add(LSTM(hidden, input_shape=(train_X.shape[1], train_X.shape[2])))
model.add(Dense(1))
model.compile(loss=‘mae’, optimizer=‘adam’)
# fit
history = model.fit(train_X, train_y, epochs=epochs, batch_size=512, validation_data=(test_X, test_y), verbose=2, shuffle=False)
# plot
pyplot.plot(history.history[‘loss’], label=‘train’)
pyplot.plot(history.history[‘val_loss’], label=‘test’)
pyplot.legend()
pyplot.show()

#模型评估
# 进行预测
yhat = model.predict(test_X)
test_X = test_X.reshape((test_X.shape[0], timesteps*n_features))

# 对预测进行比例反转,因为最开始进行了归一化,所以需要反转回原来的数值
inv_yhat = numpy.concatenate((yhat, test_X[:, (1–n_features):]), axis=1)
inv_yhat = scaler.inverse_transform(inv_yhat)
inv_yhat = inv_yhat[:,0]
print(len(inv_yhat))

#对测试集数据反转,理由同上
test_y = test_y.reshape((len(test_y), 1))
inv_y = numpy.concatenate((test_y, test_X[:, (1–n_features):]), axis=1)
inv_y = scaler.inverse_transform(inv_y)
inv_y = inv_y[:,0]
print(‘\n‘)
print(len(inv_y))

# 计算 RMSE
rmse = numpy.sqrt(mean_squared_error(inv_y, inv_yhat))
print(‘Test RMSE: %.3f’ % rmse)

Go程序

1
package main   import (        // 可以把文本输出到命令行的包        “fmt”        // 数学计算的包        “math”        // 随机数的包        “math/rand”        // 文件系统的包        “os”        // 可以数字和文本转换的包        “strconv” )   // 程序主函数 func main() {        // 命令行输入的参数        args := os.Args[1:]        var T, P, M int        var err error        // 从命令行的参数里面获取 T P M        // 把文本转换成数字,成功就继续,失败就退出程序        if T, err = strconv.Atoi(args[0]); err != nil {               panic(err)        }        if P, err = strconv.Atoi(args[1]); err != nil {               panic(err)        }        if M, err = strconv.Atoi(args[2]); err != nil {               panic(err)        }          // 多线程之间通信的信道,里面传输新生成的随机位置,是否在园内        innerChan := make(chan bool, T*P)        // 开启 T 个线程        for i := 0; i < T; i++ {               // 开启线程               go func() {                      // 线程里面生成的位置数量                      number := 0                      // 循环生成随机位置,直到达到 P                      for {                             // 随机位置 x                             x := rand.Float64()                             // 随机位置 y                             y := rand.Float64()                             // x 与圆心的水平距离的差值                             diffX := x – 0.5                             // y 与圆心的垂直距离的差值                             diffY := y – 0.5                             // 随机位置 x y 与圆心的距离                             distance := math.Sqrt(diffX*diffX + diffY*diffY)                             // 距离小于半径 0.5 ,在园内                             if distance < 0.5 {                                    // 信道里面传进去一个数据,在圆内                                    innerChan <- true                             } else {                                    // 信道里面传进去一个数据,不在圆内                                    innerChan <- false                             }                             // 线程里面生成的位置的数量 + 1                             number++                             // 如果线程里生成的数量达到 P,退出循环,退出线程                             if number == P {                                    break                             }                      }               }()        }          // 总共接收到的数量        total := 0        // 在园内的数量        totalInner := 0        // 循环接收信道的数据        for {               // 接收数据               isInner := <-innerChan               // 总数 + 1               total++               // 在圆内               if isInner {                      // 在圆内的数量 + 1                      totalInner++               }               // 接收到的数量达到 M 的倍数,输出当前的 PI               if total > 0 && total%M == 0 {                      fmt.Println(“Total = “, total, ” PI = “, float64(totalInner)/float64(total)/0.25)               }               // 接收完毕,输出最终的 PI,退出               if total == T*P {                      fmt.Println(“Total = “, total, ” Final PI = “, float64(totalInner)/float64(total)/0.25)                      break               }        } }