Các phương pháp học máy
Trong số các phương pháp tích hợp sẵn của ma trận và vector, có một số phương pháp được sử dụng phổ biến trong các tác vụ học máy, đặc biệt là trong việc triển khai mạng nơ-ron.
Như tên gọi của nó, mạng nơ-ron là tập hợp của nhiều nơ-ron, vốn là các đơn vị tính toán cơ bản. Chúng được gọi là "cơ bản" vì chúng thực hiện các phép tính khá đơn giản: thông thường, một nơ-ron có một tập hợp các hệ số trọng số được áp dụng cho các tín hiệu đầu vào nhất định, sau đó tổng trọng số của các tín hiệu được đưa vào một hàm, thường là một bộ chuyển đổi phi tuyến tính.
Việc sử dụng hàm kích hoạt giúp khuếch đại các tín hiệu yếu và giới hạn những tín hiệu quá mạnh, ngăn chặn việc chuyển sang trạng thái bão hòa (tràn số trong tính toán thực). Tuy nhiên, điều quan trọng nhất là tính phi tuyến tính mang lại cho mạng những khả năng tính toán mới, cho phép giải quyết các vấn đề phức tạp hơn.
Mạng nơ-ron cơ bản
Sức mạnh của mạng nơ-ron được thể hiện qua việc kết hợp một số lượng lớn các nơ-ron và thiết lập các kết nối giữa chúng. Thông thường, các nơ-ron được tổ chức thành các tầng (có thể so sánh với ma trận hoặc vector), bao gồm cả những kết nối lặp (recurrent), và cũng có thể có các hàm kích hoạt khác nhau về hiệu quả. Điều này cho phép phân tích dữ liệu khối lượng lớn bằng nhiều thuật toán khác nhau, đặc biệt là bằng cách tìm kiếm các mẫu ẩn trong chúng.
Lưu ý rằng nếu không có tính phi tuyến tính trong mỗi nơ-ron, một mạng nơ-ron nhiều tầng có thể được biểu diễn dưới dạng tương đương như một tầng duy nhất, với các hệ số được tính bằng tích ma trận của tất cả các tầng (W_total = W_1 * W_2 * ... * W_L
, trong đó 1..L là số thứ tự các tầng). Và điều này chỉ là một bộ cộng tuyến tính đơn giản. Do đó, tầm quan trọng của các hàm kích hoạt được chứng minh một cách toán học.
Một số hàm kích hoạt nổi tiếng nhất
Một trong những cách phân loại chính của mạng nơ-ron là chia chúng theo thuật toán học được sử dụng: mạng học có giám sát và mạng học không giám sát. Các mạng có giám sát yêu cầu một chuyên gia con người cung cấp đầu ra mong muốn cho tập dữ liệu ban đầu (ví dụ: các đánh dấu rời rạc về trạng thái của một hệ thống giao dịch, hoặc các chỉ số số về mức tăng giá ngụ ý). Các mạng không giám sát tự xác định các cụm trong dữ liệu.
Dù trong trường hợp nào, nhiệm vụ huấn luyện một mạng nơ-ron là tìm ra các tham số giúp giảm thiểu sai số trên các mẫu huấn luyện và kiểm tra, sử dụng hàm mất mát: nó cung cấp một đánh giá định tính hoặc định lượng về sai số giữa mục tiêu và phản hồi nhận được từ mạng.
Các khía cạnh quan trọng nhất để áp dụng thành công mạng nơ-ron bao gồm việc lựa chọn các yếu tố dự đoán thông tin và độc lập lẫn nhau (các đặc điểm được phân tích), chuyển đổi dữ liệu (chuẩn hóa và làm sạch) theo đặc thù của thuật toán học, cũng như tối ưu hóa kiến trúc và kích thước mạng. Xin lưu ý rằng việc sử dụng các thuật toán học máy không đảm bảo thành công.
Ở đây, chúng ta sẽ không đi sâu vào lý thuyết về mạng nơ-ron, cách phân loại của chúng, và các tác vụ điển hình cần giải quyết. Chủ đề này quá rộng. Những ai quan tâm có thể tìm thấy các bài viết trên trang web mql5.com và các nguồn khác.
MQL5 cung cấp ba phương pháp học máy đã trở thành một phần của API ma trận và vector:
Activation
: tính toán giá trị của hàm kích hoạtDerivative
: tính toán giá trị của đạo hàm của hàm kích hoạtLoss
: tính toán giá trị của hàm mất mát
Các đạo hàm của hàm kích hoạt cho phép cập nhật hiệu quả các tham số mô hình dựa trên sai số của mô hình, vốn thay đổi trong quá trình học.
Hai phương pháp đầu tiên ghi kết quả vào vector/ma trận được truyền vào và trả về một chỉ báo thành công (true
hoặc false
), còn hàm mất mát trả về một số. Dưới đây là nguyên mẫu của chúng (dưới kiểu object<T>
, chúng ta đánh dấu cả matrix<T>
và vector<T>
):
bool object<T>::Activation(object<T> &out, ENUM_ACTIVATION_FUNCTION activation)
bool object<T>::Derivative(object<T> &out, ENUM_ACTIVATION_FUNCTION loss)
T object<T>::Loss(const object<T> &target, ENUM_LOSS_FUNCTION loss)
2
3
Một số hàm kích hoạt cho phép thiết lập tham số bằng đối số thứ ba, tùy chọn.
Vui lòng tham khảo Tài liệu MQL5 để biết danh sách các hàm kích hoạt được hỗ trợ trong bảng liệt kê ENUM_ACTIVATION_FUNCTION
và các hàm mất mát trong bảng liệt kê ENUM_LOSS_FUNCTION
.
Để làm ví dụ giới thiệu, hãy xem xét vấn đề phân tích luồng tick thực tế. Một số nhà giao dịch coi tick là nhiễu rác, trong khi những người khác thực hành giao dịch tần số cao dựa trên tick. Có giả thuyết rằng các thuật toán tần số cao thường mang lại lợi thế cho các nhà chơi lớn và chỉ dựa trên việc xử lý phần mềm thông tin giá cả. Dựa trên điều này, chúng ta sẽ đưa ra một giả thuyết rằng trong luồng tick có hiệu ứng bộ nhớ ngắn hạn, do các robot của những nhà tạo lập thị trường hiện đang hoạt động. Sau đó, một phương pháp học máy có thể được sử dụng để tìm ra mối phụ thuộc này và dự đoán một vài tick tiếp theo.
Học máy luôn liên quan đến việc đưa ra giả thuyết, tổng hợp một mô hình cho chúng, và thử nghiệm chúng trong thực tế. Rõ ràng, không phải lúc nào cũng thu được các giả thuyết hiệu quả. Đây là một quá trình dài của thử và sai, trong đó thất bại là nguồn cải tiến và ý tưởng mới.
Chúng ta sẽ sử dụng một trong những loại mạng nơ-ron đơn giản nhất: Bộ nhớ Liên kết Hai chiều (Bidirectional Associative Memory - BAM
). Mạng này chỉ có hai tầng: đầu vào và đầu ra. Một phản hồi nhất định (liên kết) được hình thành ở đầu ra để đáp ứng tín hiệu đầu vào. Kích thước các tầng có thể thay đổi. Khi kích thước giống nhau, kết quả là một mạng Hopfield.
Bộ nhớ liên kết hai chiều kết nối đầy đủ
Sử dụng mạng này, chúng ta sẽ so sánh N
tick gần đây trước đó và M
tick dự đoán tiếp theo, tạo thành một mẫu huấn luyện từ quá khứ gần đến một độ sâu nhất định. Các tick sẽ được đưa vào mạng dưới dạng mức tăng hoặc giảm giá được chuyển đổi thành giá trị nhị phân [+1, -1]
(tín hiệu nhị phân là dạng mã hóa chuẩn trong mạng BAM và Hopfield).
Ưu điểm quan trọng nhất của BAM là quá trình học gần như tức thời (so với hầu hết các phương pháp lặp khác), bao gồm việc tính toán ma trận trọng số. Chúng ta sẽ đưa ra công thức dưới đây.
Tuy nhiên, sự đơn giản này cũng có nhược điểm: dung lượng của BAM (số lượng hình ảnh mà nó có thể nhớ) bị giới hạn bởi kích thước tầng nhỏ nhất, với điều kiện phân bố đặc biệt của +1
và -1
trong các vector mẫu huấn luyện được đáp ứng.
Do đó, trong trường hợp của chúng ta, mạng sẽ tổng quát hóa tất cả các chuỗi tick trong mẫu huấn luyện, và sau đó, trong quá trình hoạt động bình thường, nó sẽ chuyển sang một hình ảnh đã lưu trữ nào đó, tùy thuộc vào chuỗi tick mới được trình bày. Việc này hiệu quả đến đâu trong thực tế phụ thuộc vào rất nhiều yếu tố, bao gồm kích thước và cài đặt mạng, đặc điểm của luồng tick hiện tại, và các yếu tố khác.
Vì giả định rằng luồng tick chỉ có bộ nhớ ngắn hạn, nên mong muốn huấn luyện lại mạng trong thời gian thực hoặc gần thời gian thực, vì quá trình huấn luyện thực chất chỉ giảm xuống một vài phép toán ma trận.
Vậy, để mạng ghi nhớ các hình ảnh liên kết (trong trường hợp của chúng ta, quá khứ và tương lai của luồng tick), cần phương trình sau:
W = Σ_i(A_i^T * B_i)
Trong đó W
là ma trận trọng số của mạng. Phép tổng được thực hiện trên tất cả các tích cặp của vector đầu vào A_i
và vector đầu ra tương ứng B_i
.
Sau đó, khi mạng hoạt động, chúng ta đưa hình ảnh đầu vào vào tầng đầu tiên, áp dụng ma trận W
vào nó, và do đó kích hoạt tầng thứ hai, nơi hàm kích hoạt cho mỗi nơ-ron được tính toán. Sau đó, sử dụng ma trận chuyển vị W^T
, tín hiệu lan truyền ngược lại tầng đầu tiên, nơi các hàm kích hoạt cũng được áp dụng trong các nơ-ron. Tại thời điểm này, hình ảnh đầu vào không còn đến tầng đầu tiên nữa, tức là quá trình dao động tự do tiếp tục trong mạng. Nó tiếp tục cho đến khi các thay đổi trong tín hiệu của các nơ-ron mạng ổn định (tức là nhỏ hơn một giá trị định trước nhất định).
Ở trạng thái này, tầng thứ hai của mạng chứa hình ảnh đầu ra liên kết được tìm thấy — dự đoán.
Hãy triển khai kịch bản học máy này trong script MatrixMachineLearning.mq5
.
Trong các tham số đầu vào, bạn có thể thiết lập tổng số tick cuối cùng (TicksToLoad
) được yêu cầu từ lịch sử, và bao nhiêu trong số đó được phân bổ cho việc kiểm tra (TicksToTest
). Theo đó, mô hình (trọng số) sẽ dựa trên (TicksToLoad - TicksToTest
) tick.
input int TicksToLoad = 100;
input int TicksToTest = 50;
input int PredictorSize = 20;
input int ForecastSize = 10;
2
3
4
Ngoài ra, trong các biến đầu vào, kích thước của vector đầu vào (số tick đã biết PredictorSize
) và vector đầu ra (số tick tương lai ForecastSize
) được chọn.
Các tick được yêu cầu ở đầu hàm OnStart
. Trong trường hợp này, chúng ta chỉ làm việc với giá Ask
. Tuy nhiên, bạn cũng có thể thêm quá trình xử lý Bid
và Last
, cùng với khối lượng.
void OnStart()
{
vector ticks;
ticks.CopyTicks(_Symbol, COPY_TICKS_ALL | COPY_TICKS_ASK, 0, TicksToLoad);
...
2
3
4
5
Hãy chia các tick thành tập huấn luyện và tập kiểm tra.
vector ask1(n - TicksToTest);
for(int i = 0; i < n - TicksToTest; ++i)
{
ask1[i] = ticks[i];
}
vector ask2(TicksToTest);
for(int i = 0; i < TicksToTest; ++i)
{
ask2[i] = ticks[i + TicksToLoad - TicksToTest];
}
...
2
3
4
5
6
7
8
9
10
11
12
Để tính toán mức tăng giá, chúng ta sử dụng phương thức Convolve
với vector bổ sung {+1, -1}
. Lưu ý rằng vector chứa mức tăng sẽ ngắn hơn vector gốc 1 phần tử.
vector differentiator = {+1, -1};
vector deltas = ask1.Convolve(differentiator, VECTOR_CONVOLVE_VALID);
...
2
3
Phép chập theo thuật toán VECTOR_CONVOLVE_VALID
có nghĩa là chỉ tính các lần chồng lấp đầy đủ của các vector (tức là vector nhỏ hơn được dịch chuyển tuần tự dọc theo vector lớn hơn mà không vượt ra ngoài ranh giới của nó). Các loại chập khác cho phép các vector chồng lấp chỉ với một phần tử, hoặc một nửa số phần tử (trong trường hợp này, các phần tử còn lại nằm ngoài vector tương ứng và các giá trị chập thể hiện hiệu ứng biên).
Để chuyển đổi các giá trị liên tục của mức tăng thành các xung đơn vị (dương và âm tùy thuộc vào dấu của phần tử ban đầu của vector), chúng ta sẽ sử dụng hàm phụ trợ Binary
(không được hiển thị ở đây): nó trả về một bản sao mới của vector trong đó mỗi phần tử là +1
hoặc -1
.
vector inputs = Binary(deltas);
Dựa trên chuỗi đầu vào nhận được, chúng ta sử dụng hàm TrainWeights
để tính toán ma trận trọng số W
của mạng nơ-ron. Chúng ta sẽ xem xét cấu trúc của hàm này sau. Hiện tại, hãy chú ý rằng các tham số PredictorSize
và ForecastSize
được truyền vào nó, cho phép chia một chuỗi tick liên tục thành các tập vector đầu vào và đầu ra ghép đôi theo kích thước của các tầng BAM đầu vào và đầu ra, tương ứng.
matrix W = TrainWeights(inputs, PredictorSize, ForecastSize);
Print("Check training on backtest:");
CheckWeights(W, inputs);
...
2
3
4
Ngay sau khi huấn luyện mạng, chúng ta kiểm tra độ chính xác của nó trên tập huấn luyện: chỉ để đảm bảo rằng mạng đã được huấn luyện. Điều này được thực hiện bởi hàm CheckWeights
.
Tuy nhiên, điều quan trọng hơn là kiểm tra xem mạng hoạt động như thế nào trên dữ liệu kiểm tra chưa biết. Để làm điều này, hãy phân biệt và nhị phân hóa vector thứ hai ask2
và sau đó cũng gửi nó đến CheckWeights
.
vector test = Binary(ask2.Convolve(differentiator, VECTOR_CONVOLVE_VALID));
Print("Check training on forwardtest:");
CheckWeights(W, test);
...
}
2
3
4
5
Đã đến lúc làm quen với hàm TrainWeights
, trong đó chúng ta xác định các ma trận A
và B
để "cắt" các vector từ chuỗi đầu vào được truyền vào, tức là từ vector data
.
template<typename T>
matrix<T> TrainWeights(const vector<T> &data, const uint predictor, const uint responce,
const uint start = 0, const uint _stop = 0, const uint step = 1)
{
const uint sample = predictor + responce;
const uint stop = _stop <= start ? (uint)data.Size() : _stop;
const uint n = (stop - sample + 1 - start) / step;
matrix<T> A(n, predictor), B(n, responce);
ulong k = 0;
for(ulong i = start; i < stop - sample + 1; i += step)
{
for(ulong j = 0; j < predictor; ++j)
{
A[k][j] = data[start + i * step + j];
}
for(ulong j = 0; j < responce; ++j)
{
B[k][j] = data[start + i * step + j + predictor];
}
}
const matrix<T> w = W.Transpose();
...
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Các ma trận A
và B
trong trường hợp này không được hình thành để tính toán W
mà đóng vai trò là "nhà cung cấp" các vector để thử nghiệm. Chúng ta cũng cần một bản sao chuyển vị của W
để tính toán các tín hiệu trả về từ tầng thứ hai của mạng về tầng đầu tiên.
Số lần lặp mà các quá trình chuyển tiếp được phép trong mạng, cho đến khi hội tụ, được giới hạn bởi hằng số limit
.
const uint limit = 100;
int positive = 0;
int negative = 0;
int average = 0;
2
3
4
5
Các biến positive
, negative
, và average
cần thiết để tính toán thống kê về các dự đoán thành công và không thành công nhằm đánh giá chất lượng huấn luyện.
Tiếp theo, mạng được kích hoạt trong một vòng lặp qua các cặp mẫu thử nghiệm và phản hồi cuối cùng của nó được lấy. Mỗi vector đầu vào tiếp theo được ghi vào vector a
, và tầng đầu ra b
được điền bằng số không. Sau đó, các lần lặp được khởi chạy để truyền tín hiệu từ a
đến b
bằng ma trận W
và áp dụng hàm kích hoạt AF_TANH
, cũng như tín hiệu phản hồi từ b
đến a
, cũng sử dụng AF_TANH
. Quá trình tiếp tục cho đến khi đạt limit
vòng lặp (điều này khó xảy ra) hoặc cho đến khi điều kiện hội tụ được đáp ứng, dưới đó các vector trạng thái nơ-ron a
và b
hầu như không thay đổi (ở đây chúng ta sử dụng phương thức Compare
và các bản sao phụ của vector x
và y
từ lần lặp trước).
for(ulong i = 0; i < k; ++i)
{
vector a = A.Row(i);
vector b = vector::Zeros(responce);
vector x, y;
uint j = 0;
for(; j < limit; ++j)
{
x = a;
y = b;
a.MatMul(W).Activation(b, AF_TANH);
b.MatMul(w).Activation(a, AF_TANH);
if(!a.Compare(x, 0.00001) && !b.Compare(y, 0.00001)) break;
}
Binarize(a);
Binarize(b);
...
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Sau khi đạt trạng thái ổn định, chúng ta chuyển trạng thái của các nơ-ron từ liên tục (số thực) sang nhị phân +1
và -1
bằng hàm Binarize
(nó tương tự như hàm Binary
đã đề cập trước đó, nhưng thay đổi trạng thái của vector tại chỗ).
Bây giờ, chúng ta chỉ cần đếm số lần khớp ở tầng đầu ra với vector mục tiêu. Để làm điều này, thực hiện phép nhân vô hướng của các vector. Kết quả dương có nghĩa là số lượng tick đoán đúng vượt quá số lượng không đúng. Tổng số lần khớp được tích lũy trong average
.
const int match = (int)(b.Dot(B.Row(i)));
if(match > 0) positive++;
else if(match < 0) negative++;
average += match; // 0 trong match nghĩa là độ chính xác 50/50 (tức là đoán ngẫu nhiên)
}
2
3
4
5
6
Sau khi vòng lặp hoàn tất cho tất cả các mẫu thử nghiệm, chúng ta hiển thị thống kê.
float skew = (float)average / k; // số lần khớp trung bình trên mỗi vector
PrintFormat("Count=%d Positive=%d Negative=%d Accuracy=%.2f%%",
k, positive, negative, ((skew + responce) / 2 / responce) * 100);
}
2
3
4
5
Script cũng bao gồm hàm RunWeights
, đại diện cho lần chạy hoạt động của mạng nơ-ron (bằng ma trận trọng số W
) cho vector trực tuyến từ predictor
tick cuối cùng. Hàm sẽ trả về một vector với các tick tương lai ước tính.
template<typename T>
vector<T> RunWeights(const matrix<T> &W, const vector<T> &data)
{
const uint predictor = (uint)W.Rows();
const uint responce = (uint)W.Cols();
vector a = data;
vector b = vector::Zeros(responce);
vector x, y;
uint j = 0;
const uint limit = LIMIT;
const matrix<T> w = W.Transpose();
for(; j < limit; ++j)
{
x = a;
y = b;
a.MatMul(W).Activation(b, AF_TANH);
b.MatMul(w).Activation(a, AF_TANH);
if(!a.Compare(x, 0.00001) && !b.Compare(y, 0.00001)) break;
}
Binarize(b);
return b;
}
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
Ở cuối hàm OnStart
, chúng ta tạm dừng thực thi trong 1 giây (để chờ các tick mới với một mức độ xác suất nhất định), yêu cầu PredictorSize + 1
tick cuối cùng (đừng quên +1 để phân biệt), và thực hiện dự đoán cho chúng trực tuyến.
void OnStart()
{
...
Sleep(1000);
vector ask3;
ask3.CopyTicks(_Symbol, COPY_TICKS_ALL | COPY_TICKS_ASK, 0, PredictorSize + 1);
vector online = Binary(ask3.Convolve(differentiator, VECTOR_CONVOLVE_VALID));
Print("Online:", online);
vector forecast = RunWeights(W, online);
Print("Forecast:", forecast);
}
2
3
4
5
6
7
8
9
10
11
Chạy script với cài đặt mặc định trên EURUSD vào tối thứ Sáu cho kết quả sau.
Check training on backtest:
Count=20 Positive=20 Negative=0 Accuracy=85.50%
Check training on forwardtest:
Count=20 Positive=12 Negative=2 Accuracy=58.50%
Online: [1,1,1,1,-1,-1,-1,1,-1,1,1,-1,1,1,-1,-1,1,1,-1,-1]
Forecast: [-1,1,-1,1,-1,-1,1,1,-1,1]
2
3
4
5
6
Ký hiệu và thời gian không được đề cập vì tình hình thị trường có thể ảnh hưởng đáng kể đến khả năng áp dụng của thuật toán và cấu hình mạng cụ thể. Khi thị trường mở, mỗi lần bạn chạy script, bạn sẽ nhận được kết quả mới khi ngày càng có nhiều tick đến. Đây là hành vi dự kiến phù hợp với giả thuyết hình thành bộ nhớ ngắn.
Như chúng ta thấy, độ chính xác huấn luyện là chấp nhận được, nhưng nó giảm đáng kể trên dữ liệu kiểm tra và có thể rơi xuống dưới 50%.
Tại thời điểm này, chúng ta chuyển dần từ lập trình sang lĩnh vực nghiên cứu khoa học. Bộ công cụ học máy tích hợp trong MQL5 cho phép bạn triển khai nhiều cấu hình khác nhau của mạng nơ-ron và bộ phân tích, với các chiến lược giao dịch và nguyên tắc chuẩn bị dữ liệu ban đầu khác nhau.