去年我曾發表過幾篇有關使用神經網絡進行金融價格預測的教程,我認爲其中有一部分結果至少還挺有意思,並且值得在實際交易中加以應用。如果你閱讀過這些文章,你一定注意到一個現象:當你試圖將一些機器學習模型應用於「隨機」數據並希望從中找到隱藏規律的時候,訓練過程往往會產生嚴重的過擬合。我們曾使用不同的正則化技術和附加數據應對這個問題,但是這不僅很費時,還有種盲目搜索的感覺。
今天,我想介紹一個略微有些不同的方法對同樣的算法進行擬合。使用概率的觀點看待這個問題能夠讓我們從數據本身學習正則化、估計預測結果的確定性、使用更少的數據進行訓練,還能在模型中引入額外的概率依賴關係。我不會過多深入貝葉斯模型或變分原理的數學、技術細節,而是會給出一些概述,也更多地將討論集中在應用場景當中。文中所用的代碼可以在以下鏈接中找到:https://github.com/Rachnog/Deep-Trading/tree/master/bayesian
與此同時,我也推薦大家查閱我此前發佈的基於神經網絡的財務預測教程:
1. 簡單時間序列預測(錯誤糾正完畢)
2. 正確一維時間序列預測+回測
3. 多元時間序列預測
4. 波動預測和自定義損失
5. 多任務和多模式學習
6. 超參數優化
爲了更深入地瞭解概率規劃、貝葉斯模型以及它們的應用,我推薦你在以下資源網站中查看:
模式識別和機器學習
黑客貝葉斯方法
下面即將提到的庫文件
另外,你還可能會用到下列 Python 庫:
PyMC3 (https://github.com/pymc-devs/pymc3)
Edward (http://edwardlib.org/)
Pyro (http://pyro.ai/)
概率編程
這個「概率」指的是什麼?我們爲什麼稱其爲「編程」呢?首先,讓我們回憶一下我們所謂「正常的」神經網絡指的是什麼、以及我們能從中得到什麼。神經網絡有着以矩陣形式表達的參數(權重),而其輸出通常是一些標量或者向量(例如在分類問題的情況下)。當我們用諸如 SGD 的方法訓練這個模型後,這些矩陣會獲得固定值。與此同時,對於同一個輸入樣本,輸出向量應該相同,就是這樣!但是,如果我們將所有的參數和輸出視爲相互依賴的分佈,會發生什麼?神經網絡的權重將與輸出一樣,是一個來自網絡並取決於參數的樣本——如果是這樣,它能爲我們帶來什麼?
讓我們從基礎講起。如果我們認爲網絡是一個取決於其他分佈的數集,這首先就構成了聯合概率分佈 p(y, z|x),其中有着輸出 y 和一些模型 z 的「內部」隱變量,它們都取決於輸入 x(這與常規的神經網絡完全相同)。我們感興趣的是找到這樣神經網絡的分佈,這樣一來就可以對 y ~ p(y|x) 進行採樣,並獲得一個形式爲分佈的輸出,該分佈中抽取的樣本的期望通常是輸出,和標準差(對不確定性的估計)——尾部越大,則輸出置信度越小。
這種設定可能不是很明確,但我們只需要記住:現在開始,模型中所有的參數、輸入及輸出都是分佈,並且在訓練時對這些分佈進行擬合,以便在實際應用中獲得更高的準確率。我們也需要注意自己設定的參數分佈的形狀(例如,所有的初識權重 w 服從正態分佈 Normal(0,1),之後我們將學習正確的均值和方差)。初始分佈即所謂的先驗知識,在訓練集上訓練過的分佈即爲後驗知識。我們使用後者進行抽樣並得出結果。
圖源:http://www.indiana.edu/~kruschke/BMLR/
模型要擬合到什麼程度纔有用?通用結構被稱爲變分推理(variational inference)。無需細想,我們可以假設,我們希望找到一個可以得到最大對數似然函數 p_w(z | x)的模型,其中 w 是模型的參數(分佈參數),z 是我們的隱變量(隱藏層的神經元輸出,從參數 w 的分佈採樣得到),x 是輸入數據樣本。這就是我們的模型了。我們在 Pyro 中引入了一個實例來介紹這個模型,該簡單實例包含所有隱變量 q_(z)的一些分佈,其中 ф 被稱爲變分參數。這種分佈必須近似於訓練最好的模型參數的「實際」分佈。
訓練目標是使得 [log(p_w(z|x))—log(q_ф(z))] 的期望值相對於有指導的輸入數據和樣本最小化。在這裏我們不探討訓練的細節,因爲這裏面的知識量太大了,此處就先當它是一個可以優化的黑箱吧。
對了,爲什麼需要編程呢?因爲我們通常將這種概率模型(如神經網絡)定義爲變量相互關聯的有向圖,這樣我們就可以直接顯示變量間的依賴關係:
圖源:http://kentonmurray.com/
而且,概率編程語言起初就被用於定義此類模型並在模型上做推理。
爲什麼選擇概率編程?
不同於在模型中使用 dropout 或 L1 正則化,你可以把它當作你數據中的隱變量。考慮到所有的權重其實是分佈,你可以從中抽樣 N 次得到輸出的分佈,通過計算該分佈的標準差,你就知道能模型有多靠譜。作爲成果,我們可以只用少量的數據來訓練這些模型,而且我們可以靈活地在變量之間添加不同的依賴關係。
概率編程的不足
我還沒有太多關於貝葉斯建模的經驗,但是我從 Pyro 和 PyMC3 中瞭解到,這類模型的訓練過程十分漫長且很難定義正確的先驗分佈。而且,處理從分佈中抽取的樣本會導致誤解和歧義。
數據準備
我已經從 http://bitinfocharts.com/ 抓取了每日 Ethereum(以太坊)的價格數據。其中包括典型的 OHLCV(高開低走),另外還有關於 Ethereum 的每日推特量。我們將使用七日的價格、開盤及推特量數據來預測次日的價格變動情況。
價格、推特數、大盤變化
上圖是一些數據樣本——藍線對應價格變化,黃線對應推特數變化,綠色對應大盤變化。它們之間存在某種正相關(0.1—0.2)。因此我們希望能利用好這些數據中的模式對模型進行訓練。
貝葉斯線性迴歸
首先,我想驗證簡單線性分類器在任務中的表現結果(並且我想直接使用 Pyro tutorial——http://pyro.ai/examples/bayesian_regression.html——的結果)。我們按照以下操作在 PyTorch 上定義我們的模型(詳情參閱官方指南:http://pyro.ai/examples/bayesian_regression.html)。
class RegressionModel(nn.Module):
def __init__(self, p):
super(RegressionModel, self).__init__()
self.linear = nn.Linear(p, 1)
def forward(self, x):
# x * w + b
return self.linear(x)
以上是我們以前用過的簡單確定性模型,下面是用 Pyro 定義的概率模型:
def model(data):
# Create unit normal priors over the parameters
mu = Variable(torch.zeros(1, p)).type_as(data)
sigma = Variable(torch.ones(1, p)).type_as(data)
bias_mu = Variable(torch.zeros(1)).type_as(data)
bias_sigma = Variable(torch.ones(1)).type_as(data)
w_prior, b_prior = Normal(mu, sigma), Normal(bias_mu, bias_sigma)
priors = {'linear.weight': w_prior, 'linear.bias': b_prior}
lifted_module = pyro.random_module("module", regression_model, priors)
lifted_reg_model = lifted_module()
with pyro.iarange("map", N, subsample=data):
x_data = data[:, :-1]
y_data = data[:, -1]
# run the regressor forward conditioned on inputs
prediction_mean = lifted_reg_model(x_data).squeeze()
pyro.sample("obs",
Normal(prediction_mean, Variable(torch.ones(data.size(0))).type_as(data)),
obs=y_data.squeeze())
從上面的代碼可知,參數 W 和 b 均定義爲一般線性迴歸模型分佈,兩者都服從正態分佈 Normal(0,1)。我們稱之爲先驗,創建 Pyro 的隨機函數(在我們的例子中是 PyTorch 中的 RegressionModel),爲它添加先驗 ({『linear.weight』: w_prior, 『linear.bias』: b_prior}),並根據輸入數據 x 從這個模型 p(y|x) 中抽樣。
這個模型的 guide 部分可能像下面這樣:
def guide(data):
w_mu = Variable(torch.randn(1, p).type_as(data.data), requires_grad=True)
w_log_sig = Variable(0.1 * torch.ones(1, p).type_as(data.data), requires_grad=True)
b_mu = Variable(torch.randn(1).type_as(data.data), requires_grad=True)
b_log_sig = Variable(0.1 * torch.ones(1).type_as(data.data), requires_grad=True)
mw_param = pyro.param("guide_mean_weight", w_mu)
sw_param = softplus(pyro.param("guide_log_sigma_weight", w_log_sig))
mb_param = pyro.param("guide_mean_bias", b_mu)
sb_param = softplus(pyro.param("guide_log_sigma_bias", b_log_sig))
w_dist = Normal(mw_param, sw_param)
b_dist = Normal(mb_param, sb_param)
dists = {'linear.weight': w_dist, 'linear.bias': b_dist}
lifted_module = pyro.random_module("module", regression_model, dists)
return lifted_module()
我們定義了想要「訓練」的分佈的可變分佈。如你所見,我們爲 W 和 b 定義了相同的分佈,目的是讓它們更接近實際情況(據我們假設)。這個例子中,我讓分佈圖更窄一些(服從正態分佈 Normal(0, 0.1))
然後,我們用這種方式對模型進行訓練:
for j in range(3000):
epoch_loss = 0.0
perm = torch.randperm(N)
# shuffle data
data = data[perm]
# get indices of each batch
all_batches = get_batch_indices(N, 64)
for ix, batch_start in enumerate(all_batches[:-1]):
batch_end = all_batches[ix + 1]
batch_data = data[batch_start: batch_end]
epoch_loss += svi.step(batch_data)
在模型擬合後,我們想從中抽樣出 y。我們循環 100 次並計算每一步的預測值的均值和標準差(標準差越高,預測置信度就越低)。
preds = []
for i in range(100):
sampled_reg_model = guide(X_test)
pred = sampled_reg_model(X_test).data.numpy().flatten()
preds.append(pred)
現在有很多經典的經濟預測度量方法,例如 MSE、MAE 或 MAPE,它們都可能會讓人困惑——錯誤率低並不意味着你的模型表現得好,驗證它在測試集上的表現也十分重要,而這就是我們做的工作。
使用貝葉斯模型進行爲期 30 天的預測
從圖中我們可以看到,預測效果並不夠好。但是預測圖中最後的幾個跳變的形狀很不錯,這給了我們一線希望。繼續加油!
常規神經網絡
在這個非常簡單的模型進行實驗後,我們想要嘗試一些更有趣的神經網絡。首先讓我們利用 25 個帶有線性激活的神經元的單隱層網絡訓練一個簡單 MLP:
def get_model(input_size):
main_input = Input(shape=(input_size, ), name='main_input')
x = Dense(25, activation='linear')(main_input)
output = Dense(1, activation = "linear", name = "out")(x)
final_model = Model(inputs=[main_input], outputs=[output])
final_model.compile(optimizer='adam', loss='mse')
return final_model
訓練 100 個 epoch:
model = get_model(len(X_train[0]))
history = model.fit(X_train, Y_train,
epochs = 100,
batch_size = 64,
verbose=1,
validation_data=(X_test, Y_test),
callbacks=[reduce_lr, checkpointer],
shuffle=True)
其結果如下:
使用 Keras 神經網絡進行爲期 30 天的預測
我覺得這比簡單的貝葉斯迴歸效果更差,此外這個模型不能得到確定性的估計,更重要的是,這個模型甚至沒有正則化。
貝葉斯神經網絡
現在我們用 PyTorch 來定義上文在 Keras 上訓練的模型:
class Net(torch.nn.Module):
def __init__(self, n_feature, n_hidden):
super(Net, self).__init__()
self.hidden = torch.nn.Linear(n_feature, n_hidden) # hidden layer
self.predict = torch.nn.Linear(n_hidden, 1) # output layer
def forward(self, x):
x = self.hidden(x)
x = self.predict(x)
return x
相比於貝葉斯迴歸模型,我們現在有兩個參數集(從輸入層到隱藏層的參數和隱藏層到輸出層的參數),所以我們需要對分佈和先驗知識稍加改動,以適應我們的模型:
priors = {'hidden.weight': w_prior,
'hidden.bias': b_prior,
'predict.weight': w_prior2,
'predict.bias': b_prior2}
以及 guide 部分:
dists = {'hidden.weight': w_dist,
'hidden.bias': b_dist,
'predict.weight': w_dist2,
'predict.bias': b_dist2}
請不要忘記爲模型中的每一個分佈起一個不同的名字,因爲模型中不應存在任何歧義和重複。更多代碼細節請參見源代碼:https://github.com/Rachnog/Deep-Trading/tree/master/bayesian
訓練之後,讓我們看看最後的結果:
使用 Pyro 神經網絡進行爲期 30 天的預測
它看起來比之前的結果都好得多!
比起常規貝葉斯模型,考慮到貝葉斯模型所中習得的權重特徵或正則化,我還希望看到權重的數據。我按照以下方法查看 Pyro 模型的參數:
for name in pyro.get_param_store().get_all_param_names():
print name, pyro.param(name).data.numpy()
這是我在 Keras 模型中所寫的代碼:
import tensorflow as tf
sess = tf.Session()
with sess.as_default():
tf.global_variables_initializer().run()
dense_weights, out_weights = None, None
with sess.as_default():
for layer in model.layers:
if len(layer.weights) > 0:
weights = layer.get_weights()
if 'dense' in layer.name:
dense_weights = layer.weights[0].eval()
if 'out' in layer.name:
out_weights = layer.weights[0].eval()
例如,Keras 模型最後一層的權重的均值和標準差分別爲 -0.0025901748 和 0.30395043,Pyro 模型對應值爲 0.0005974418 和 0.0005974418。數字小了很多,但效果真的不錯!其實這就是 L2 或 Dropout 這種正則化算法要做的——把參數逼近到零,而我們可以用變分推理來實現它!隱藏層的權重變化更有趣。我們將一些權重向量繪製成圖,藍線是 Keras 模型的權重,橙線是 Pyro 模型的權重:
輸入層與隱藏層之間的部分權重
真正有意思的不止是權重的均值與標準差變得小,還有一點是權重變得稀疏,所以基本上在訓練中完成了第一個權重集的稀疏表示,以及第二個權重集的 L2 正則化,多麼神奇!別忘了自己跑跑代碼感受一下:https://github.com/Rachnog/Deep-Trading/tree/master/bayesian
小結
我們在文中使用了新穎的方法對神經網絡進行訓練。不同於順序更新靜態權重,我們是更新的是權重的分佈。因此,我們可能獲得有趣又有用的結果。我想強調的是,貝葉斯方法讓我們在調整神經網絡時不需要手動添加正則化,瞭解模型的不確定性,並儘可能使用更少的數據來獲得更好的結果。感謝閱讀:)
原文鏈接:https://medium.com/@alexrachnog/financial-forecasting-with-probabilistic-programming-and-pyro-db68ab1a1dba