8.1. Mô hình chuỗi

Hãy tưởng tượng rằng bạn đang xem phim trên Netflix. Là một người dùng Netflix tốt, bạn quyết định đánh giá từng bộ phim một cách cẩn thận. Xét cho cùng, bạn muốn xem thêm nhiều bộ phim hay phải không? Nhưng hóa ra, mọi thứ không hề đơn giản như vậy. Đánh giá của mỗi người về một bộ phim có thể thay đổi đáng kể theo thời gian. Trên thực tế, các nhà tâm lý học thậm chí còn đặt tên cho một số hiệu ứng:

  • Hiệu ứng mỏ neo: dựa trên ý kiến của người khác. Ví dụ, xếp hạng của một bộ phim sẽ tăng lên sau khi nó thắng giải Oscar, mặc dù đoàn làm phim này không có bất kỳ tác động nào về mặt quảng bá đến bộ phim. Hiệu ứng này kéo dài trong vòng một vài tháng cho đến khi giải thưởng bị lãng quên. [Wu et al., 2017] chỉ ra rằng hiệu ứng này tăng chỉ số xếp hạng thêm hơn nửa điểm.
  • Hiệu ứng vòng xoáy khoái lạc: con người nhanh chóng thích nghi để chấp nhận một tình huống tốt hơn (hoặc xấu đi) như một điều bình thường mới. Chẳng hạn, sau khi xem nhiều bộ phim hay, sự kỳ vọng rằng bộ phim tiếp theo sẽ hay tương đương hoặc thậm chí phải hay hơn trở nên khá cao, do đó ngay cả một bộ phim trung bình cũng có thể bị coi là một bộ phim tồi.
  • Tính thời vụ: rất ít khán giả thích xem một bộ phim về ông già Noel vào tháng 8.
  • Trong một số trường hợp, các bộ phim trở nên không được ưa chuộng do những hành động sai trái của các đạo diễn hoặc diễn viên tham gia vào quá trình sản xuất phim.
  • Một số phim trở thành “phim cult” vì chúng gần như tệ đến mức phát cười. Plan 9 from Outer SpaceTroll 2 là hai ví dụ nổi tiếng.

Tóm lại, thứ bậc xếp hạng không hề cố định. Sử dụng các động lực dựa trên thời gian đã giúp [Koren, 2009] đề xuất phim chính xác hơn. Tuy nhiên, vấn đề không chỉ là về phim ảnh.

  • Nhiều người dùng có thói quen rất đặc biệt liên quan tới thời gian mở ứng dụng. Chẳng hạn, học sinh sử dụng các ứng dụng mạng xã hội nhiều hơn hẳn sau giờ học. Các ứng dụng giao dịch chứng khoán được sử dụng nhiều khi thị trường mở cửa.
  • Việc dự đoán giá cổ phiếu ngày mai khó hơn nhiều so với việc dự đoán giá cổ phiếu bị bỏ lỡ ngày hôm qua, mặc dù cả hai đều là bài toán ước tính một con số. Rốt cuộc, nhìn lại quá khứ dễ hơn nhiều so với dự đoán tương lai. Trong thống kê, bài toán đầu tiên được gọi là ngoại suy và bài toán sau được gọi là nội suy.
  • Âm nhạc, giọng nói, văn bản, phim ảnh, bước đi, v.v … đều có tính chất tuần tự. Nếu chúng ta hoán vị chúng, chúng sẽ không còn nhiều ý nghĩa. Dòng tiêu đề chó cắn người ít gây ngạc nhiên hơn nhiều so với người cắn chó, mặc dù các từ giống hệt nhau.
  • Các trận động đất có mối tương quan mạnh mẽ, tức sau một trận động đất lớn, rất có thể sẽ có một số dư chấn nhỏ hơn và xác suất xảy ra dư chấn cao hơn nhiều so với trường hợp trận động đất lớn không xảy ra trước đó. Trên thực tế, các trận động đất có mối tương quan về mặt không-thời gian, tức các dư chấn thường xảy ra trong một khoảng thời gian ngắn và ở gần nhau.
  • Con người tương tác với nhau một cách tuần tự, điều này có thể được thấy trong các cuộc tranh cãi trên Twitter, các điệu nhảy và các cuộc tranh luận.

8.1.1. Các công cụ thống kê

Tóm lại, ta cần các công cụ thống kê và các kiến trúc mạng nơ-ron sâu mới để xử lý dữ liệu chuỗi. Để đơn giản hóa mọi việc, ta sẽ sử dụng giá cổ phiếu được minh họa trong Fig. 8.1.1 để làm ví dụ.

../_images/ftse100.png

Fig. 8.1.1 Giá cổ phiếu FTSE 100 trong vòng 30 năm

Ta sẽ gọi giá cổ phiếu là \(x_t \geq 0\), tức tại thời điểm \(t \in \mathbb{N}\) ta thấy giá cổ phiếu bằng \(x_t\). Để có thể kiếm lời trên thị trường chứng khoán vào ngày \(t\), một nhà giao dịch sẽ muốn dự đoán \(x_t\) thông qua

(8.1.1)\[x_t \sim p(x_t \mid x_{t-1}, \ldots, x_1).\]

8.1.1.1. Mô hình Tự hồi quy

Để dự đoán giá cổ phiếu, các nhà giao dịch có thể sử dụng một mô hình hồi quy, chẳng hạn như mô hình mà ta đã huấn luyện trong Section 3.3. Chỉ có một vấn đề lớn ở đây, đó là số lượng đầu vào, \(x_{t-1}, \ldots, x_1\) thay đổi tùy thuộc vào \(t\). Cụ thể, số lượng đầu vào sẽ tăng cùng với lượng dữ liệu thu được và ta sẽ cần một phép tính xấp xỉ để làm cho giải pháp này khả thi về mặt tính toán. Phần lớn nội dung tiếp theo trong chương này sẽ xoay quanh việc làm thế nào để ước lượng \(p(x_t \mid x_{t-1}, \ldots, x_1)\) một cách hiệu quả. Nói ngắn gọn, ta có hai chiến lược:

  1. Giả sử rằng việc sử dụng một chuỗi có thể rất dài \(x_{t-1}, \ldots, x_1\) là không thực sự cần thiết. Trong trường hợp này, ta có thể hài lòng với một khoảng thời gian \(\tau\) và chỉ sử dụng các quan sát \(x_{t-1}, \ldots, x_{t-\tau}\). Lợi ích trước mắt là bây giờ số lượng đối số luôn bằng nhau, ít nhất là với \(t > \tau\). Điều này sẽ cho phép ta huấn luyện một mạng sâu như được đề cập ở bên trên. Các mô hình như vậy được gọi là các mô hình tự hồi quy (autoregressive), vì chúng tự thực hiện hồi quy trên chính mình.
  2. Một chiến lược khác, được minh họa trong Fig. 8.1.2, là giữ một giá trị \(h_t\) để tóm tắt các quan sát trong quá khứ, đồng thời cập nhật \(h_t\) bên cạnh việc dự đoán \(\hat{x}_t\). Kết quả là mô hình sẽ ước tính \(x_t\) với \(\hat{x}_t = p(x_t \mid x_{t-1}, h_{t})\) và cập nhật \(h_t = g(h_{t-1}, x_{t-1})\). Do \(h_t\) không bao giờ được quan sát nên các mô hình này còn được gọi là các mô hình tự hồi quy tiềm ẩn (latent autoregressive model). LSTM và GRU là hai ví dụ cho kiểu mô hình này.
../_images/sequence-model.svg

Fig. 8.1.2 Một mô hình tự hồi quy tiềm ẩn.

Cả hai trường hợp đều đặt ra câu hỏi về cách tạo ra dữ liệu huấn luyện. Người ta thường sử dụng các quan sát từ quá khứ cho đến hiện tại để dự đoán các quan sát xảy ra trong tương lai. Rõ ràng chúng ta không thể trông đợi thời gian sẽ đứng yên. Tuy nhiên, một giả định phổ biến là: tuy các giá trị cụ thể của \(x_t\) có thể thay đổi, ít ra động lực của chuỗi thời gian sẽ không đổi. Điều này khá hợp lý, vì nếu động lực thay đổi thì ta sẽ không thể dự đoán được nó bằng cách sử dụng dữ liệu mà ta đang có. Các nhà thống kê gọi các động lực không thay đổi này là cố định (stationary). Dù có làm gì đi chăng nữa, chúng ta vẫn sẽ tìm được ước lượng của toàn bộ chuỗi thời gian thông qua

(8.1.2)\[p(x_1, \ldots, x_T) = \prod_{t=1}^T p(x_t \mid x_{t-1}, \ldots, x_1).\]

Lưu ý rằng các xem xét trên vẫn đúng trong trường hợp chúng ta làm việc với các đối tượng rời rạc, chẳng hạn như từ ngữ thay vì số. Sự khác biệt duy nhất trong trường hợp này là chúng ta cần sử dụng một bộ phân loại thay vì một bộ hồi quy để ước lượng \(p(x_t \mid x_{t-1}, \ldots, x_1)\).

8.1.1.2. Mô hình Markov

Nhắc lại phép xấp xỉ trong một mô hình tự hồi quy, chúng ta chỉ sử dụng \((x_{t-1}, \ldots, x_{t-\tau})\) thay vì \((x_{t-1}, \ldots, x_1)\) để ước lượng \(x_t\). Bất cứ khi nào phép xấp xỉ này là chính xác, chúng ta nói rằng chuỗi thỏa mãn điều kiện Markov. Cụ thể, nếu \(\tau = 1\), chúng ta có mô hình Markov bậc một\(p(x)\) như sau

(8.1.3)\[p(x_1, \ldots, x_T) = \prod_{t=1}^T p(x_t \mid x_{t-1}).\]

Các mô hình như trên rất hữu dụng bất cứ khi nào \(x_t\) chỉ là các giá trị rời rạc, vì trong trường hợp này, quy hoạch động có thể được sử dụng để tính toán chính xác các giá trị theo chuỗi. Ví dụ, chúng ta có thể tính toán \(p(x_{t+1} \mid x_{t-1})\) một cách hiệu quả bằng cách chỉ sử dụng các quan sát trong một khoảng thời gian ngắn tại quá khứ:

(8.1.4)\[p(x_{t+1} \mid x_{t-1}) = \sum_{x_t} p(x_{t+1} \mid x_t) p(x_t \mid x_{t-1}).\]

Chi tiết về quy hoạch động nằm ngoài phạm vi của phần này, nhưng chúng tôi sẽ giới thiệu nó trong Section 9.4. Các công cụ trên được sử dụng rất phổ biến trong các thuật toán điều khiển và học tăng cường.

8.1.1.3. Quan hệ Nhân quả

Về nguyên tắc, không có gì sai khi trải (unfolding) \(p(x_1, \ldots, x_T)\) theo thứ tự ngược lại. Bằng cách đặt điều kiện như vậy, chúng ta luôn có thể viết chúng như sau

(8.1.5)\[p(x_1, \ldots, x_T) = \prod_{t=T}^1 p(x_t \mid x_{t+1}, \ldots, x_T).\]

Trên thực tế, nếu có một mô hình Markov, chúng ta cũng có thể thu được một phân phối xác suất có điều kiện ngược. Tuy nhiên trong nhiều trường hợp vẫn tồn tại một trật tự tự nhiên cho dữ liệu, cụ thể đó là chiều thuận theo thời gian. Rõ ràng là các sự kiện trong tương lai không thể ảnh hưởng đến quá khứ. Do đó, nếu thay đổi \(x_t\) thì ta có thể ảnh hưởng đến những gì xảy ra tại \(x_{t+1}\) trong tương lai, nhưng lại không thể ảnh hưởng tới quá khứ theo chiều ngược lại. Nếu chúng ta thay đổi \(x_t\), phân phối trên các sự kiện trong quá khứ sẽ không thay đổi. Do đó, việc giải thích \(p(x_{t+1} \mid x_t)\) sẽ đơn giản hơn là \(p(x_t \mid x_{t+1})\). Ví dụ: [Hoyer et al., 2009] chỉ ra rằng trong một số trường hợp chúng ta có thể tìm \(x_{t+1} = f(x_t) + \epsilon\) khi có thêm nhiễu, trong khi điều ngược lại thì không đúng. Đây là một tin tuyệt vời vì chúng ta thường quan tâm tới việc ước lượng theo chiều thuận hơn. Để tìm hiểu thêm về chủ đề này, có thể tìm đọc cuốn sách [Peters et al., 2017a]. Chúng ta sẽ chỉ tìm hiểu sơ qua trong phần này.

8.1.2. Một ví dụ đơn giản

Sau khi đề cập nhiều về lý thuyết, bây giờ chúng ta hãy thử lập trình minh họa. Đầu tiên, hãy khởi tạo một vài dữ liệu như sau. Để đơn giản, chúng ta tạo chuỗi thời gian bằng cách sử dụng hàm sin cộng thêm một chút nhiễu.

%matplotlib inline
from d2l import mxnet as d2l
from mxnet import autograd, np, npx, gluon, init
from mxnet.gluon import nn
npx.set_np()

T = 1000  # Generate a total of 1000 points
time = np.arange(0, T)
x = np.sin(0.01 * time) + 0.2 * np.random.normal(size=T)
d2l.plot(time, [x])
../_images/output_sequence_vn_c0eed1_1_0.svg

Tiếp theo, chúng ta cần biến chuỗi thời gian này thành các đặc trưng và nhãn có thể được sử dụng để huấn luyện mạng. Dựa trên kích thước embedding \(\tau\), chúng ta ánh xạ dữ liệu thành các cặp \(y_t = x_t\)\(\mathbf{z}_t = (x_{t-1}, \ldots, x_{t-\tau})\). Để ý kĩ, có thể thấy rằng ta sẽ mất \(\tau\) điểm dữ liệu đầu tiên, vì chúng ta không có đủ \(\tau\) điểm dữ liệu trong quá khứ để làm đặc trưng cho chúng. Một cách đơn giản để khắc phục điều này, đặc biệt là khi chuỗi thời gian rất dài, là loại bỏ đi số ít các phần tử đó. Một cách khác là đệm giá trị 0 vào chuỗi thời gian. Mã nguồn dưới đây về cơ bản là giống hệt với mã nguồn huấn luyện trong các phần trước. Chúng tôi cố gắng giữ cho kiến trúc đơn giản với vài tầng kết nối đầy đủ, hàm kích hoạt ReLU và hàm mất mát \(\ell_2\). Do việc mô hình hóa phần lớn là giống với khi ta xây dựng các bộ ước lượng hồi quy viết bằng Gluon trong các phần trước, nên chúng ta sẽ không đi sâu vào chi tiết trong phần này.

tau = 4
features = np.zeros((T-tau, tau))
for i in range(tau):
    features[:, i] = x[i: T-tau+i]
labels = x[tau:]

batch_size, n_train = 16, 600
train_iter = d2l.load_array((features[:n_train], labels[:n_train]),
                            batch_size, is_train=True)
test_iter = d2l.load_array((features[:n_train], labels[:n_train]),
                           batch_size, is_train=False)

# Vanilla MLP architecture
def get_net():
    net = nn.Sequential()
    net.add(nn.Dense(10, activation='relu'),
            nn.Dense(1))
    net.initialize(init.Xavier())
    return net

# Least mean squares loss
loss = gluon.loss.L2Loss()

Bây giờ chúng ta đã sẵn sàng để huấn luyện.

def train_net(net, train_iter, loss, epochs, lr):
    trainer = gluon.Trainer(net.collect_params(), 'adam',
                            {'learning_rate': lr})
    for epoch in range(1, epochs + 1):
        for X, y in train_iter:
            with autograd.record():
                l = loss(net(X), y)
            l.backward()
            trainer.step(batch_size)
        print('epoch %d, loss: %f' % (
            epoch, d2l.evaluate_loss(net, train_iter, loss)))

net = get_net()
train_net(net, train_iter, loss, 10, 0.01)
epoch 1, loss: 0.045946
epoch 2, loss: 0.031594
epoch 3, loss: 0.028504
epoch 4, loss: 0.028767
epoch 5, loss: 0.027109
epoch 6, loss: 0.026090
epoch 7, loss: 0.028275
epoch 8, loss: 0.025843
epoch 9, loss: 0.026051
epoch 10, loss: 0.026025

8.1.3. Dự đoán của Mô hình

Vì cả hai giá trị mất mát trên tập huấn luyện và kiểm tra đều nhỏ, chúng ta kỳ vọng mô hình trên sẽ hoạt động tốt. Hãy cùng xác nhận điều này trên thực tế. Điều đầu tiên cần kiểm tra là mô hình có thể dự đoán những gì sẽ xảy ra trong bước thời gian kế tiếp tốt như thế nào.

estimates = net(features)
d2l.plot([time, time[tau:]], [x, estimates],
         legend=['data', 'estimate'])
../_images/output_sequence_vn_c0eed1_7_0.svg

Kết quả khá tốt, đúng như những gì chúng ta mong đợi. Thậm chí sau hơn 600 mẫu quan sát, phép ước lượng vẫn trông khá tin cậy. Chỉ có một chút vấn đề: nếu chúng ta quan sát dữ liệu tới bước thời gian thứ 600, chúng ta không thể hy vọng sẽ nhận được nhãn gốc cho tất cả các dự đoán tương lai. Thay vào đó, chúng ta cần tiến lên từng bước một:

(8.1.6)\[\begin{split}\begin{aligned} x_{601} & = f(x_{600}, \ldots, x_{597}), \\ x_{602} & = f(x_{601}, \ldots, x_{598}), \\ x_{603} & = f(x_{602}, \ldots, x_{599}). \end{aligned}\end{split}\]

Nói cách khác, chúng ta sẽ phải sử dụng những dự đoán của mình để đưa ra dự đoán trong tương lai. Hãy cùng xem cách này có ổn không.

predictions = np.zeros(T)
predictions[:n_train] = x[:n_train]
for i in range(n_train, T):
    predictions[i] = net(
        predictions[(i-tau):i].reshape(1, -1)).reshape(1)
d2l.plot([time, time[tau:], time[n_train:]],
         [x, estimates, predictions[n_train:]],
         legend=['data', 'estimate', 'multistep'], figsize=(4.5, 2.5))
../_images/output_sequence_vn_c0eed1_9_0.svg

Ví dụ trên cho thấy, cách này đã thất bại thảm hại. Các giá trị ước lượng rất nhanh chóng suy giảm thành một hằng số chỉ sau một vài bước. Tại sao thuật toán trên hoạt động tệ đến thế? Suy cho cùng, lý do là trên thực tế các sai số dự đoán bị chồng chất qua các bước thời gian. Cụ thể, sau bước thời gian 1 chúng ta có nhận được sai số \(\epsilon_1 = \bar\epsilon\). Tiếp theo, đầu vào cho bước thời gian 2 bị nhiễu loạn bởi \(\epsilon_1\), do đó chúng ta nhận được sai số dự đoán \(\epsilon_2 = \bar\epsilon + L \epsilon_1\). Tương tự như thế cho các bước thời gian tiếp theo. Sai số có thể phân kỳ khá nhanh khỏi các quan sát đúng. Đây là một hiện tượng phổ biến. Ví dụ, dự báo thời tiết trong 24 giờ tới có độ chính xác khá cao nhưng nó giảm đi nhanh chóng với những dự báo xa hơn quãng thời gian đó. Chúng ta sẽ thảo luận về các phương pháp để cải thiện vấn đề trên trong chương này và những chương tiếp theo.

Chúng ta hãy kiểm chứng quan sát trên bằng cách tính toán dự đoán \(k\) bước thời gian trên toàn bộ chuỗi.

k = 33  # Look up to k - tau steps ahead

features = np.zeros((k, T-k))
for i in range(tau):  # Copy the first tau features from x
    features[i] = x[i:T-k+i]

for i in range(tau, k):  # Predict the (i-tau)-th step
    features[i] = net(features[(i-tau):i].T).T

steps = (4, 8, 16, 32)
d2l.plot([time[i:T-k+i] for i in steps], [features[i] for i in steps],
         legend=['step %d' % i for i in steps], figsize=(4.5, 2.5))
../_images/output_sequence_vn_c0eed1_11_0.svg

Điều này minh họa rõ ràng chất lượng của các ước lượng thay đổi như thế nào khi chúng ta cố gắng dự đoán xa hơn trong tương lai. Mặc dù những dự đoán có độ dài là 8 bước vẫn còn khá tốt, bất cứ kết quả dự đoán nào vượt ra ngoài khoảng đó thì khá là vô dụng.

8.1.4. Tóm tắt

  • Các mô hình chuỗi thường yêu cầu các công cụ thống kê chuyên biệt để ước lượng. Hai lựa chọn phổ biến đó là các mô hình tự hồi quy và mô hình tự hồi quy biến tiềm ẩn.
  • Sai số bị tích lũy và chất lượng của phép ước lượng suy giảm đáng kể khi mô hình dự đoán các bước thời gian xa hơn.
  • Khó khăn trong phép nội suy và ngoại suy khá khác biệt. Do đó, nếu bạn có một kiểu dữ liệu chuỗi thời gian, hãy luôn để ý trình tự thời gian của dữ liệu khi huấn luyện, hay nói cách khác, không bao giờ huấn luyện trên dữ liệu thuộc về bước thời gian trong tương lai.
  • Đối với các mô hình nhân quả (ví dụ, ở đó thời gian đi về phía trước), ước lượng theo chiều xuôi thường dễ dàng hơn rất nhiều so với chiều ngược lại.

8.1.5. Bài tập

  1. Hãy cải thiện mô hình nói trên bằng cách
    • Kết hợp nhiều hơn 4 mẫu quan sát trong quá khứ? Bao nhiêu mẫu quan sát là thực sự cần thiết?
    • Bạn sẽ cần bao nhiêu mẫu nếu dữ liệu không có nhiễu? Gợi ý: bạn có thể viết \(\sin\)\(\cos\) dưới dạng phương trình vi phân.
    • Có thể kết hợp các đặc trưng cũ hơn trong khi đảm bảo tổng số đặc trưng là không đổi không? Điều này có cải thiện độ chính xác không? Tại sao?
    • Thay đổi cấu trúc mạng nơ-ron và quan sát tác động của nó.
  2. Nếu một nhà đầu tư muốn tìm một mã chứng khoán tốt để mua. Cô ta sẽ nhìn vào lợi nhuận trong quá khứ để quyết định mã nào có khả năng sinh lời. Điều gì có thể khiến chiến lược này trở thành sai lầm?
  3. Liệu có thể áp dụng quan hệ nhân quả cho dữ liệu văn bản được không? Nếu có thì ở mức độ nào?
  4. Hãy cho một ví dụ khi mô hình tự hồi quy tiềm ẩn có thể cần được dùng để nắm bắt động lực của dữ liệu.

8.1.6. Thảo luận

8.1.7. Những người thực hiện

Bản dịch trong trang này được thực hiện bởi:

  • Đoàn Võ Duy Thanh
  • Nguyễn Duy Du
  • Nguyễn Cảnh Thướng
  • Phạm Minh Đức
  • Nguyễn Lê Quang Nhật
  • Nguyễn Văn Quang
  • Nguyễn Văn Cường
  • Lê Khắc Hồng Phúc
  • Phạm Hồng Vinh