適逢其時,現在最火熱的AI,深度學習套件TensorFlow也是以python開發
既然都入門python了,怎麼能放過,自然是上網找了些範例來學學
只是TensorFlow的基礎概念不是那麼直觀
一開始就卡關了,加上工作繁忙,還是只能先以工作為主
去年從一個圖像文字識別範例完成後就放下了
直到今年開始接觸到了Keras
這個函式庫用起來直觀多啦
也才終於讓我入門到深度學習的大門
感謝Keras,讚嘆Keras
目前Keras已經納入TensorFlow的套件中,不用額外安裝
而且除了搭配TensorFlow使用之外
還自帶了一堆開源的神經網路可直接用
像是圖像識別用的RESNET50、VGG16等等
再搭配上TensorFlow內的開放訓練資料集
自學實作上真的不是大問題
不過學了一陣子都在玩圖像
最近開始對其它應用有興趣想試試
像是RNN(遞迴神經網路),這種對於有順序邏輯性的網路
可以處理翻譯,文章風格模仿產生與股票預測
「股票預測!!!」,這個我相當有興趣,就決定從這個方向來學
找了一下網路上相關的範例,就下面這個最詳細,而且針對單一股票處理
用深度學習幫你解析K線圖!
那就來試著玩玩看
程式環境上使用的python是3.6.9,tensorflow直接使用2.0.0
還需要額外安裝的函式庫有pandas,scikit-learn、numpy、matplotlib與requests
嗯?requests,上面那篇裡沒有提到網頁用的requests啊?
這是因為裡面訓練所需要的資料,要寫信請網誌主提供
先不要說我臉皮薄又懶得寫信
股票那麼多檔,總不能所有資料都靠別人給,終究還是要自己抓
所以在正式開始進入訓練前,先來想辦法撈一下我們想要的資料
而其實目前AI深度學習的第一步,也是最重要,最花時間的一步
還是從資料收集與處理做起
沒有正確地資料,也就訓練不出正確地答案
何況這資料也只要多打幾行程式就可以一直用下去
就先從這邊開始吧
目前從網路上找到的抓股市資料的範例大致上有兩種
一種是直接從公開資訊觀測站上面抓json下來
另一種是從股市網站抓網頁上的資料
JSON比較好處理,所以我是使用第一種辦法
根據找到的這個網站指示
只要在關鍵欄位加入年份、月份與股票代號,就可以抓那一整個月的股票資料
網站範例是直接存入MySQL,我們需要的是CSV檔,所以小改一下
另外,我們需要的資料的說明欄為了後續方便,一律轉英文處理
所以各欄對應如下:
日期(date),成交股數(day_shares),成交金額(day_price),開盤價(open),最高價(high),最低價(low),收盤價(close)
與漲跌價差(diff)跟成交筆數(volume)
我們以現在最火熱的台積電(2310)當訓練與第一驗證的目標好了
抓取的資料從2015年1月到2019年10月
def getStockCSV(date1, date2 ,stock_no): """產生範圍內的日期陣列:""" #製作空陣列 date_data = [] #取得起始日期,去掉不必要符號 d1 = re.split(',|-|/',date1) start_date = datetime.date(int(d1[0]),int(d1[1]),1) #取得結束日期,去掉不必要符號 d2 = re.split(',|-|/',date2) end_date = datetime.date(int(d2[0]),int(d2[1]),1) #利用雙迴圈產生日期陣列 for i in range(int(d1[0]), int(d2[0])+1): for j in range(1, 13): count_date = datetime.date(i,j,1) if (count_date >= start_date) & (count_date <= end_date): date_data.append(datetime.date(i,j,1)) """向股市網址要求資料""" url = 'http://www.twse.com.tw/exchangeReport/STOCK_DAY?date=%s&stockNo=%s' % ( start_date.strftime('%Y%m%d'), stock_no) r = requests.get(url) first_data = r.json() #如果資料欄中的stat是OK,代表有資料可以下載 if(first_data['stat'] == 'OK'): print('股票存在') #建立csv檔 csvfile1 = open(stock_no+'.csv','w', newline='\n', encoding = 'utf-8') writer1 = csv.writer(csvfile1) #轉英文後的欄位說明 title=['date','days_shares','days_price','open','high','low','close','diff','volume'] #建立CSV內的欄位 writer1.writerow(title) #先關閉檔案 csvfile1.close() print('檔案建立') """依日期正式下載股票資料""" for date in date_data: url = 'http://www.twse.com.tw/exchangeReport/STOCK_DAY?date=%s&stockNo=%s' % ( date.strftime('%Y%m%d'), stock_no) r = requests.get(url) stock_data = r.json() #開啟繼續寫檔模式 csvfile = open(stock_no+'.csv','a', newline='\n', encoding = 'utf-8') writer = csv.writer(csvfile) for row in stock_data['data']: writer.writerow(row) csvfile.close() time.sleep(5) #避免連續要求被距絕,下載後暫停5秒,再下載下一筆 else: print('找不到此股票,或日期錯誤') getData("2015-01", "2019-10", "2330")
拿著這程式試著抓了幾間公司,發現有公司抓不到...
查了一下,抓不到的是上櫃的公司
再查了一下,這邊說,上櫃要從這裡抓
嗯,不但網址不同,連年份的格式也不同,這邊是民國...真麻煩
不過到也還好,至少資料格式除了日期以外都類似
反正就上市的一種抓法,上櫃的一種抓法
重新統整一下程式碼
就變成下面這樣,餵入年月,還有股票代號
如果有在上市找到了,就繼續抓資料,轉成CSV檔存下來
如果在上市區找不到,就變換年月格式,往上櫃網頁找,一樣轉成CSV存檔。
當然連上櫃都沒資料,那大概是輸入錯誤了
import time import request import csv import datetime def getStockCSV(date1, date2 ,stock_no): """產生範圍內的日期陣列:""" #製作空陣列 date_data = [] #取得起始日期,去掉不必要符號 d1 = re.split(',|-|/',date1) start_date = datetime.date(int(d1[0]),int(d1[1]),1) #取得結束日期,去掉不必要符號 d2 = re.split(',|-|/',date2) end_date = datetime.date(int(d2[0]),int(d2[1]),1) #利用雙迴圈產生日期陣列 for i in range(int(d1[0]), int(d2[0])+1): for j in range(1, 13): count_date = datetime.date(i,j,1) if (count_date >= start_date) & (count_date <= end_date): date_data.append(datetime.date(i,j,1)) """向股市網址要求資料""" url = 'http://www.twse.com.tw/exchangeReport/STOCK_DAY?date=%s&stockNo=%s' % ( start_date.strftime('%Y%m%d'), stock_no) r = requests.get(url) first_data = r.json() #如果資料欄中的stat是OK,代表有資料可以下載 if(first_data['stat'] == 'OK'): print('股票存在') #建立csv檔 csvfile1 = open(stock_no+'.csv','w', newline='\n', encoding = 'utf-8') writer1 = csv.writer(csvfile1) #轉英文後的欄位說明 title=['date','days_shares','days_price','open','high','low','close','diff','volume'] #建立CSV內的欄位 writer1.writerow(title) #先關閉檔案 csvfile1.close() print('檔案建立') """依日期正式下載股票資料""" for date in date_data: url = 'http://www.twse.com.tw/exchangeReport/STOCK_DAY?date=%s&stockNo=%s' % ( date.strftime('%Y%m%d'), stock_no) r = requests.get(url) stock_data = r.json() #開啟繼續寫檔模式 csvfile = open(stock_no+'.csv','a', newline='\n', encoding = 'utf-8') writer = csv.writer(csvfile) for row in stock_data['data']: writer.writerow(row) csvfile.close() time.sleep(5) #避免連續要求被距絕,下載後暫停5秒,再下載下一筆 else: """取得上櫃股票資料""" #產生民國格式的日期 cstart_date = str(int(start_date.strftime('%Y'))-1911)+'/'+start_date.strftime('%m') """向上櫃股票網頁要求資料""" url = 'https://www.tpex.org.tw/web/stock/aftertrading/daily_trading_info/st43_result.php?d=%s&stkno=%s' % ( cstart_date, stock_no) r = requests.get(url) first_data = r.json() #如果資料欄中的iTotalRecards不是0,代表有資料可以下載 if first_data['iTotalRecords'] > 0: print('上櫃股票存在') #建立csv檔 csvfile1 = open(stock_no+'.csv','w', newline='\n', encoding = 'utf-8') #建立CSV內的欄位 writer1 = csv.writer(csvfile1) #轉英文後的欄位說明 title=['date','days_shares','days_price','open','high','low','close','diff','volume'] writer1.writerow(title) #先關閉檔案 csvfile1.close() print('上櫃檔案建立') """依日期正式下載股票資料""" for date in date_data: #產生民國格式的日期 cdate = str(int(date.strftime('%Y'))-1911)+'/'+date.strftime('%m') url = 'https://www.tpex.org.tw/web/stock/aftertrading/daily_trading_info/st43_result.php?d=%s&stkno=%s' % ( cdate, stock_no) r = requests.get(url) stock_data = r.json() #開啟繼續寫檔模式 csvfile = open(stock_no+'.csv','a', newline='\n', encoding = 'utf-8') writer = csv.writer(csvfile) for row in stock_data['aaData']: writer.writerow(row) csvfile.close() time.sleep(5) #避免連續要求被距絕,下載後暫停5秒,再下載下一筆 else: print('找不到此股票,或日期錯誤') getData("2015-01", "2019-10", "2330")上櫃這邊,我抓了威剛(3260)一樣是2015年1月到2019年10月的資料
準備當作第二驗證的目標
總於要開始進行實際的深度學習程式撰寫啦
但第一步,還是要先處理資料
範例裡面,是用pandas抓取csv的資料
import pandas as pd foxconndf= pd.read_csv('./2330.csv', index_col=0 , thousands=",") foxconndf.dropna(how='any',inplace=True) #去除CSV裡不要的資料 foxconndf.drop(['days_shares','days_price','diff'], axis=1, inplace=True)而這個變數名稱,是鴻海公司,想改也行
不過這是抄人家的東西,能不動就先不動吧
與原版不一樣的,有兩項
第一,讀檔時加入了thousands=","的引數
這是因為引入的資料裡有逗號,如果不使用這個參數
那麼用pandas讀入時就會當字串處理
到時還要再處理一次,不如就在讀入時一口氣處理
第二,去掉成交股數,成交金額與漲跌價差的資料
因為這些都不用到,避免運算上的麻煩,就乾脆不用比較好
沒辦法,我們不是用原作者的資料,還是要小處理一下
接下來就是正規化資料。
正規化簡單地說,就是把最大值壓在1,最小值控制在0的運算
做正規化最主要原因就是要讓輸入資料變化量都差不多。
不然像成交筆數熱門時通常都是股數的好幾倍
會讓訓練模型時,太偏重這些資料,最後導致失真
這邊使用scikit-learn的套件進行正規化
from sklearn import preprocessing def normalize(df): newdf= df.copy() min_max_scaler = preprocessing.MinMaxScaler() newdf['open'] = min_max_scaler.fit_transform(df.open.values.reshape(-1,1)) newdf['low'] = min_max_scaler.fit_transform(df.low.values.reshape(-1,1)) newdf['high'] = min_max_scaler.fit_transform(df.high.values.reshape(-1,1)) newdf['volume'] = min_max_scaler.fit_transform(df.volume.values.reshape(-1,1)) newdf['close'] = min_max_scaler.fit_transform(df.close.values.reshape(-1,1)) return newdf foxconndf_norm= normalize(foxconndf)
接著是產生訓練與驗證用的資料
原作者命名為Data_Helper的函數
裡面做的處理的概念很簡單
它要用n天的資料,當訓練集,用n+1天的收盤價當答案
所以舉例來說,我的n用10天
那就是抓1~10天的各項資料做第一筆訓練資料,第11天的收盤價當答案
然後抓2~11天的各項資料做第二筆訓練資料,第12天的收盤價當答案
依此類推,如果有60天的股票資料,用10天當區間,就會產生49筆的訓練資料
有這樣的結果後,再來抓9成的資料當訓練資料集,1成的資料做成驗證資料集
概念簡單歸簡單,實際上這運算要自己寫還是蠻複雜的
感謝原作者寫的範例,讓我們照著學習
只是範例終究還是參考用而已,這部分還是不能完全套用
最主要是因為我用的訓練資料是自己抓的
如果我們不做正規化,直接把資料送進原始的Data_Helper裡
看y_train與y_test的資料會發現,取得的其實是成交筆數(volume)的資料
而不是收盤價(close)
因為資料原始順序會是open,high,low,close,volume的排列
y_train = result[:int(number_train), -1][:,-1]
取得是資料是最後一欄volume
這時候要取得倒數第二欄close,要改成
y_train = result[:int(number_train), -1][:,-2]
y_test的部分也是一樣,要改成取-2,而不是-1
我一開始沒改程式照著跑時,出來的結果一點都不符合
亂改之下,甚至還有出現預測結果就是條水平線
輸入資料正確與否,真的很重要啊
修改好後要決定天數的部分,暫時就先以範例的20天為準
import numpy as np def data_helper(df, time_frame): # 資料維度: 開盤價、收盤價、最高價、最低價、成交量, 5維 number_features = len(df.columns) # 將dataframe 轉成 numpy array datavalue = df.as_matrix() result = [] # 若想要觀察的 time_frame 為20天, 需要多加一天做為驗證答案 for index in range( len(datavalue) - (time_frame+1) ): # 從 datavalue 的第0個跑到倒數第 time_frame+1 個 result.append(datavalue[index: index + (time_frame+1) ]) # 逐筆取出 time_frame+1 個K棒數值做為一筆 instance result = np.array(result) number_train = round(0.9 * result.shape[0]) # 取 result 的前90% instance做為訓練資料 x_train = result[:int(number_train), :-1] # 訓練資料中, 只取每一個 time_frame 中除了最後一筆的所有資料做為feature y_train = result[:int(number_train), -1][:,-2] # 訓練資料中, 取每一個 time_frame 中最後一筆資料的最後一個數值(收盤價)做為答案 # 測試資料 x_test = result[int(number_train):, :-1] y_test = result[int(number_train):, -1][:,-2] # 將資料組成變好看一點 x_train = np.reshape(x_train, (x_train.shape[0], x_train.shape[1], number_features)) x_test = np.reshape(x_test, (x_test.shape[0], x_test.shape[1], number_features)) return [x_train, y_train, x_test, y_test] # 以20天為一區間進行股價預測 X_train, y_train, X_test, y_test = data_helper(foxconndf_norm, 20)
接下來就不太有什麼大修改了
只是因為我用Tensorflow是2.0的版本
所以依新版的需求再改了一下import的部分
一樣還是使用keras建我們需要的模型
模型建了2個LSTM層與2個一般神經網路
中間加了drop參數,濾掉一點連結性
嗯~~剛接觸看不懂上面的說明對吧
先丟掉複雜的理論說明,簡單地說
LSTM全名(Logn Short-Term Memory)是一種RNN的架構
然後深度學習的一個重點就是它是一種多層的神經網路結合在一起
所以,在程式上的處理就像這個程式碼一樣
建構一層又一層自己設計的網路
然後給定loss function與optimize的參數,也就是決定尋找答案的方式
這樣就完成了模型的建立
最後把數值餵給模型進行訓練
當然訓練也有參數,一次進行訓練,總共訓練幾次等等
就可以等等等等,等個一陣子把結果算出來
有結果後就先存檔,等等還可以繼續用
#改成以tensorflow 2.0的導入方式 import tensorflow as tf from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Dense, Dropout, Activation from tensorflow.keras.layers import LSTM import tensorflow.keras as keras from tensorflow.keras import optimizers def build_model(input_length, input_dim): d = 0.3 model = Sequential() #建立神經網路物件 #建立一個LSTM網路層,256個神經元,輸入資料長度與輸入的維度,要連結下一個LSTM model.add(LSTM(256, input_shape=(input_length, input_dim), return_sequences=True)) model.add(Dropout(d)) #去除連結 #建立一個LSTM網路層,256個神經元,輸入資料長度與輸入的維度,不連結下一個LSTM model.add(LSTM(256, input_shape=(input_length, input_dim), return_sequences=False)) #建立一個一般神經網路層,16個神經元,建立初始化,激勵函數是relu model.add(Dense(16,kernel_initializer="uniform",activation='relu')) #建立一個一般神經網路層,1個神經元,建立初始化,激勵函數是線性函數,做為輸出用 model.add(Dense(1,kernel_initializer="uniform",activation='linear')) #決定模型的尋找答案方式,Loss函數:均方誤差(Mean Square Error,ADAM model.compile(loss='mse', optimizer='adam', metrics=['accuracy']) return model #建立20天期間,5組數據輸入的神經網路 model = build_model( 20, 5 ) #針對神經網路開始訓練,一次128組一起訓練,訓練50次,以訓練集的1成驗證,顯示訓練進度 model.fit( X_train, y_train, batch_size=128, epochs=50, validation_split=0.1, verbose=1 ) #把訓練後的結果連同整個模型存下來 model.save('LSTM_tf2_20.h5')
因為這模型實際輸出是正規化後的結果
所以還是要去正規化,還原成實際的數值才是我們想看的東西
原作者很貼心地寫了這樣的函數
把測試用的資料,餵給這個模型算出預測的結果
再用這樣的函數解出實際的大小
#去正規化轉換 def denormalize(df, norm_value): original_value = df['close'].values.reshape(-1,1) norm_value = norm_value.reshape(-1,1) min_max_scaler = preprocessing.MinMaxScaler() min_max_scaler.fit_transform(original_value) denorm_value = min_max_scaler.inverse_transform(norm_value) return denorm_value # 用訓練好的 LSTM 模型對測試資料集進行預測 pred = model.predict(X_test) # 將預測值與正確答案還原回原來的區間值 denorm_pred = denormalize(foxconndf, pred) denorm_ytest = denormalize(foxconndf, y_test)
最後用這個數據跟測試集的答案畫圖作比對
然後我們幫這個圖加個標題,就用2330 TSMC
import matplotlib.pyplot as plt #%matplotlib inline plt.plot(denorm_pred,color='red', label='Prediction') plt.plot(denorm_ytest,color='blue', label='Answer') plt.legend(loc='best') plt.title('2330 TSMC') #加個圖表標題 plt.show()出來就像這樣
耶,好像蠻符合的
我們抓了第二種股票,威鋼來試試的啦
重新修改程式碼,砍掉模型建立與訓練
直接載入剛剛訓練的結果
嗯,差不多,曲線是接近,但仍有相當差距
不過威剛與台積電算相近的產業
來試試其它的吧,再找兩個好了
一個和泰汽車,一個找上櫃的寶雅
和泰汽車OK,跑出來是這樣
然後寶雅就卡關了...
因為csv裡面插了一行帶有「-」的資料
pandas依這個資料,把整個資料都判定為文字而不是數字
自然就卡關了...
要靠寫程式把這東西處理掉有點麻煩(懶得再動腦)
直接打開notepad++,把這行幹掉再餵,這次OK了
兩個都還行, 不過終究上不了台面
我們改一下模型好了
LSTM層第一層改為512個神經元
一般神經網路層的第一層改神經元到256
dropout用0.2處理
然後修改訓練資料量,讓20天的資料縮減為10天
batch_size改成8,epochs改成100
重新用台積電的數據再來訓練一次
哇!更準了
一樣套用給威剛、和泰汽車與寶雅
也是很合,但無實用性...
先不講這條線與原本的線還是有相當偏差
抓了前10天的資料,只能預測第11天的結果
也就是再怎麼準,今天收盤後頂多就知道明天的收盤價
都收盤了,那就買不到,明天漲再多都沒用 XD
就算打定主意明天買,又猜不到後天,甚至未來的走勢
講講還真的蠻沒用的
不過對於學習上,當個入門學習範例沒問題
去學著怎麼調整模型與訓練,來達到想要的結果
還有確認訓練出來模型的泛用性與可靠性到底O不OK
之後再去思考,怎麼重新設計一個符合股票預測的新模型
這個只是個學習入門範例啦
沒有留言:
張貼留言