LSTM là gì? Ứng dụng dự đoán giá cổ phiếu


Chào mọi người, như đã đề cập trong bài viết trước, hôm nay mình sẽ giới thiệu đến mọi người một mạng RNN đặc biệt - mạng LSTM. Đã có rất nhiều bài viết về LSTM và RNN, tuy nhiên mình muốn trình bày lại các kiến thức này với cách tiếp cận từ một người mới, vừa học vừa ứng dụng. Sau khi nắm được các đặc điểm và cấu trúc LSTM, chúng ta sẽ thử xây dựng ứng dụng dự đoán giá cổ phiếu đơn giản.

Mạng thần kinh hồi quy

Mỗi suy đoán hoặc quyết định của chúng ta được liên kết từ các thông tin phức tạp được tìm hiểu trước. Vấn đề càng phức tạp, lượng thông tin được tổng hợp để giải quyết càng lớn. Mạng thần kinh hồi quy (RNN) ưu việt hơn các mạng thần kinh truyền thống ở đặc điểm này, chúng đưa ra các dự đoán dựa trên các thông tin đã có trong các vòng lặp chứa thông tin phía trước.


Trong mô hình trên, có thể thấy rõ kết quả của đầu ra H1 nhận dữ liệu từ đầu vào X1 và kết quả của nút phía trước hay nói cách khác đầu ra của nút này chính là đầu vào của nút ngay phía sau. Điều này đã mang lại sự thành công cho mạng thần kinh hồi quy nhờ vào khả năng suy luận có cơ sở của nó. Tuy nhiên, khả năng này tỏ ra không hiệu quả đối với các trường hợp phức tạp, đòi hỏi lượng thông tin cần tổng hợp cho suy đoán nhiều hơn. Vấn đề ở đây là gì?

Ngữ cảnh và phụ thuộc xa

Trong quá trình tiếp nhận thông tin, đôi khi chúng ta không hoàn toàn tiếp nhận hết tất cả lượng thông tin được gửi đến. Tuy nhiên, chúng ta có thể dự đoán nội dung thông tin một cách chính xác. Mình lấy một ví dụ khi nghe câu "Tôi đang học trí tuệ nhân ..." thì chúng ta dễ dàng dự đoán từ tiếp theo là "tạo" dựa vào từ "trí tuệ nhân" phía trước. Nhưng trong một số trường hợp ví dụ như "Tôi là người Việt Nam. Tôi nói tiếng ..." thì việc xác định được từ còn thiếu phụ thuộc vào nội dung phía trước. Giữa nội dung cần đưa ra dự đoán và nội dung cần cho quá trình này đã có khoảng cách nhất định, vì thế mạng RNN khó có thể đưa ra dự đoán chính xác. Để giải quyết được vấn đề này, cần một mạng RNN có khả năng học được các ngữ cảnh hoặc nói chính xác hơn là kiểm soát được các phụ thuộc xa (long-term dependency).

LSTM và khả năng dự đoán dựa vào ngữ cảnh

Để giải quyết vấn đề nêu trên, một mạng thần kinh mới hơn được công bố với khả năng học các phụ thuộc xa - LSTM (Long Short Term Memory networks).


Kiến trúc dạng chuỗi các module của LSTM tương tự RNN tuy nhiên trong các module LSTM có cấu trúc phức tạp hơn, bao gồm 4 tầng với các cổng Forget Gate (ft), Input Gate (it), Output Gate (ot). Các cổng này quyết định việc thông tin nào sẽ được lưu trữ, xoá, chỉnh sửa và truyền đi. Các bạn có thể tìm hiểu thêm về cấu trúc này tại blog của blogger Colah.


Trong các module này, có 2 output là Cell state (Ct) và Hidden state (ht), với t là thời điểm đang xét. Cell state (Ct) đóng vai trò như một băng tải đưa thông tin đi xuyên suốt qua các module, nhờ vậy thông tin được lưu trữ cho các quá trình xử lý phía sau mà không mất đi. Đồng thời, các khối xử lý khác có thể tác động vào nội dung của ct để quyết định thông tin cần truyền tại các thời điểm. Các phép biến đổi trong modlue bao gồm sigma, tanh (chuyển về giá trị trong khoảng [0;1]), phép nhân và phép cộng.

Theo đó, giả sử hàm kích hoạt là hàm tanh, theo sơ đồ dễ thấy có 2 đầu vào qua hàm tanh:


Với các cổng ft, it, ot qua các sigma:




Khi đó, ct sẽ trở thành:



Nhận xét: ct chịu tác động bởi Forget gate nhằm loại bỏ các thông tin không cần thiết, đồng thời thêm các thông tin mới từ Input gate và từ Hidden layer của module trước. Trong khi đó, ht thừa hưởng thông tin từ Output gate và ct. Do tính kế thừa này, mà LSTM phù hợp với các bài toán xử lý thông tin dạng chuỗi, ứng dụng trong các mô hình dự đoán giá trị liền kề, sáng tạo nội dung,...

Ứng dụng dự đoán cổ phiếu

Mình sẽ dùng LSTM để thử dự đoán giá trị cổ phiếu trong ngày tiếp theo, tính theo ngày cuối cùng trong bộ dữ liệu mình tải về từ Yahoo! Finance. Trong bài viết này, mình đơn giản chỉ hướng dẫn cách để ứng dụng LSTM vào một bài toán dự đoán và không cố gắng để cải thiện tính chính xác của mô hình. Mình sử dụng giá đóng cửa để làm cơ sở dự đoán.

Chuẩn bị dữ liệu

Giá cổ phiếu là một chuỗi các giá trị thay đổi theo thời gian N, gọi P là giá đóng cửa trong ngày i thì 0 < i < N. Mình sẽ tạo ra một cửa sổ trượt w và cho trượt theo trục thời gian, kích thước của w là cố định và step mỗi lần trượt đúng bằng kích thước này để không xảy ra sự chồng chéo dữ liệu. Ý tưởng là dùng dữ liệu trong cửa sổ w(t) để dự đoán cho w(t+1).


Dữ liệu sẽ được nhóm lại trong 7 ngày liên tiếp. Một vòng lặp sẽ chạy từ điểm đầu đến điểm cuối của chiều dài bộ dữ liệu. Biến được truyền vào sẽ là bộ dữ liệu (data) và bước nhảy look back (lb) của window slide. Các giá trị này được lưu vào các list.

#Function to process the data into 7 day look back slices
def processData(data,lb):
X,Y = [],[]
for i in range(len(data)-lb-1):
X.append(data[i:(i+lb),0])
Y.append(data[(i+lb),0])
return np.array(X),np.array(Y)

Biến data và lb sẽ được truyền vào khi function processdata() được gọi trong stockpredict phía dưới. Data sẽ có dạng .csv, các bạn có thể tải xuống bộ data trong 5 năm (từ 2013 đến 2018) đã chuẩn bị sẵn tại link này.

Xử lý và dự đoán

Mình sẽ chia bộ dữ liệu thành 2 tệp: 80% cho tệp train và 20% cho tệp test. Sau đó, dùng tệp train để huấn luyện và mang tệp test ra để dự đoán, nếu kết quả dự đoán bám tốt trên tệp test thì cơ bản thành công. Đây là function xử lý chính.
def stockpredict(stockName):
data = pd.read_csv('dataset/all_stocks_5yr.csv')
cl = data[data['Name']==stockName].close
path = os.getcwd()
os.mkdir(path+"/static/stocks/"+stockName)

# Scaling using MinMaxScaler
scl = MinMaxScaler()
cl = cl.values.reshape(cl.shape[0],1)
cl = scl.fit_transform(cl)

X,Y = processData(cl,7)
X_train,X_test = X[:int(X.shape[0]*0.80)],X[int(X.shape[0]*0.80):]
Y_train,Y_test = Y[:int(Y.shape[0]*0.80)],Y[int(Y.shape[0]*0.80):]

# building the RNN LSTM model
model = Sequential()
model.add(LSTM(32,input_shape=(7,1)))
model.add(Dropout(0.5))
model.add(Dense(1))
model.compile(optimizer='adam',loss='mse')
    #Reshape data for (Sample,Timestep,Features)
X_train = X_train.reshape((X_train.shape[0],X_train.shape[1],1))
X_test = X_test.reshape((X_test.shape[0],X_test.shape[1],1))

hist = model.fit(X_train,Y_train,epochs=50,validation_data=(X_test,Y_test),shuffle=False)

plt.plot(hist.history['loss'])
plt.plot(hist.history['val_loss'])
plt.legend(['Loss', 'Validation Loss'], loc='upper right')
plt.savefig('static/stocks/'+stockName+'/'+stockName+'2.png')
plt.clf()
plt.close()

i=249
Xt = model.predict(X_test[i].reshape(1,7,1))
pprice=scl.inverse_transform(Xt).copy()
pprice=round(float(pprice.tolist()[0][0]),2)

Xt = model.predict(X_test)
rval = scl.inverse_transform(Y_test.reshape(-1,1))
pval = scl.inverse_transform(Xt)
ploss=0
for i in range(len(rval)):
ploss += abs((rval[i] - pval[i])/rval[i])*100
ploss = round(float(ploss / len(rval)), 2)
acr = 100-ploss

plt.plot(rval)
plt.plot(pval)
plt.ylabel('Price')
plt.xlabel('Days')
plt.legend(['Real', 'Prediction'], loc='upper left')
plt.savefig('static/stocks/'+stockName+'/'+stockName+'1.png')
plt.clf()
plt.close()

filepathtosave = path+"/static/stocks/"+stockName+"/"+stockName+".txt"
tostore = [str(pprice),str(acr)]
with open(filepathtosave, 'w') as filehandle:
for listitem in tostore:
filehandle.write('%s\n' % listitem)

return tostore

Giá trị của cột close (giá đóng cửa) được mình tách ra từ bộ dữ liệu và lưu vào biến cl.

    data = pd.read_csv('dataset/all_stocks_5yr.csv')
    cl = data[data['Name']==stockName].close
    path = os.getcwd()
    os.mkdir(path+"/static/stocks/"+stockName)

Mình dùng MinMaxScale(), trong khoảng giá trị được đưa vào, MinMaxScale() sẽ chọn ra giá trị nhỏ nhất tương ứng với 0 và giá trị lớn nhất tương ứng với 1 để chuyển dữ liệu về dạng [0;1] . Sau đó chia tỉ lệ cho các giá trị còn lại.

    scl = MinMaxScaler()
cl = cl.values.reshape(cl.shape[0],1)
cl = scl.fit_transform(cl)

Dữ liệu và biến look back (lb) sẽ được truyền vào function processData(). Đồng thời, mình chia tỉ lệ 80 - 20 cho tệp train và test. Vì mình muốn dự đoán tương lai nên tệp train mình sẽ lấy các giá trị cuối cùng trong tập dữ liệu, cách này để khi chạy dự đoán trên tệp test kết quả sẽ thuộc khoảng thời gian cuối cùng.

    X,Y = processData(cl,7)
X_train,X_test = X[:int(X.shape[0]*0.80)],X[int(X.shape[0]*0.80):]
Y_train,Y_test = Y[:int(Y.shape[0]*0.80)],Y[int(Y.shape[0]*0.80):]

Một số đoạn code dùng để vẽ và lưu biểu đồ hàm loss và loss valid sau khi training model.

    plt.plot(hist.history['loss'])
plt.plot(hist.history['val_loss'])
plt.legend(['Loss', 'Validation Loss'], loc='upper right')
plt.savefig('static/stocks/'+stockName+'/'+stockName+'2.png')
plt.clf()
plt.close()

Một biến i được khai báo chứa giá trình bằng số phần tử trong tệp test, giá trị dự đoán của phần tử thứ i sẽ được mình lưu lại. Các giá trị trước đó thì đưa vào biểu đồ so sánh giá trị thực - dự đoán.

    i=249
Xt = model.predict(X_test[i].reshape(1,7,1))
pprice=scl.inverse_transform(Xt).copy()
pprice=round(float(pprice.tolist()[0][0]),2)

Một số đoạn code để tính độ chính xác của dự đoán.

    Xt = model.predict(X_test)
rval = scl.inverse_transform(Y_test.reshape(-1,1))
pval = scl.inverse_transform(Xt)
ploss=0
for i in range(len(rval)):
ploss += abs((rval[i] - pval[i])/rval[i])*100
ploss = round(float(ploss / len(rval)), 2)
acr = 100-ploss

Một số đoạn code dùng để vẽ và lưu biểu đồ so sánh giữa giá trị thực và giá trị sau dự đoán.

    plt.plot(rval)
plt.plot(pval)
plt.ylabel('Price')
plt.xlabel('Days')
plt.legend(['Real', 'Prediction'], loc='upper left')
plt.savefig('static/stocks/'+stockName+'/'+stockName+'1.png')
plt.clf()
plt.close()

Trong model.compile() mình chọn phương pháp tối ưu hoá adam, Trong model.fit() mình chọn training trong 50 epochs. Sau quá trình training và predict kết quả sẽ lưu lại trong file .txt đồng thời vẽ đồ thị Train - Loss và đồ thị Giá trị thực tế - Giá trị dự đoán.

Kết quả

Kết quả sau khi dự đoán giá cổ phiếu của Apple trên tệp test vào ngày 9/7/2021:


Có thể thấy mô hình LSTM dự đoán khá chính xác xu hướng hình dạng biểu đồ cổ phiếu, tuy nhiên xuất hiện chênh lệch giá theo thời gian ngày càng nhiều. Điều này vì các giá trị mình đưa vào từ dữ liệu đều được làm tròn chỉ giữ lại vài con số sau dấu chấm nên xuất hiện sai số ngày càng lớn. Đồng thời, khoảng cách thời gian dự đoán cũng ảnh hưởng đến độ chính xác. Độ chính xác của mô hình này đạt khoảng 89.1% trên tệp test.

Các bạn có thể tải xuống code tham khảo của mình trên github tại đường dẫn này.

Để chạy chương trình, các bạn cần tải bộ dữ liệu S&P 500 phía trên và bỏ file .csv và thư mục dataset. Theo hướng dẫn trong mục README gồm tạo thư mục stocks trong thư mục static, cài đặt các thư viện cần thiết trong file requirements.txt và chạy file app.py.