Tất cả các bài tập trong bài viết này có thể được thực hiện trực tiếp trên trình duyệt qua trang web FundaML Show
2.0. Mảng nhiều chiềuTrong Numpy, người ta thường dùng mảng numpy hai chiều để thể hiện một ma trận. Mảng hai chiều có thể coi là một mảng của các mảng một chiều. Trong đó, mỗi mảng nhỏ một chiều tương ứng với một hàng của ma trận. Nói cách khác, ma trận có thể được coi là mảng của các vector hàng - mỗi vector hàng được biểu diễn bằng một mảng numpy một chiều. Ví dụ, nếu một mảng numpy hai chiều
8 mô tả ma trận: \(\left[ \begin{matrix} 1 & 2 \\ 3 & 4 \end{matrix} \right] \), khi được in ra nó sẽ có dạng: Ở đây chúng ta có thể nhìn thấy ba mảng, mỗi mảng được thể hiện bằng một cặp đóng mở ngoặc vuông
9:
Theo quy ước của Numpy, chúng ta cần đi từ mảng ngoài cùng tới các mảng trong:
(Xem thêm hình vẽ bên.) Chú ý:
Việc này hơi ngược với cách xây dựng toán học của các thuật toán, nơi mà mỗi điểm dữ liệu thường được coi là một vector cột - tức mỗi cột của ma trận là một điểm dữ liệu. Khi đọc các tài liệu và làm việc với các thư viện, bạn đọc cần chú ý. Giống như bài “Cơ bản về vector”, trong bài học này, chúng ta sẽ cùng làm quen với các cách xử lý ma trận trong Numpy: Khởi tạo, truy cập, thay đổi, ma trận đặc biệt, … 2.1. Khởi tạo một ma trận2.1.1. Khởi tạo một ma trậnCách đơn giản nhất để khởi tạo một ma trận là nhập vào từng phần tử của ma trận đó. Cách làm này, tất nhiên, chỉ phù hợp với các ma trận nhỏ.
Nếu bạn mới chuyển từ Matlab qua Python, bạn sẽ thấy cách khai báo của Matlab dễ chịu hơn rất nhiều. Chúng ta sẽ phải quen dần thôi :). Khi khai báo một mảng numpy nói chung, nếu ít nhất một phần tử của mảng là
2,
3 của mọi phần tử trong mảng sẽ được coi là
4 (số thực 64 bit). Ngược lại, nếu toàn bộ các phần tử là số nguyên (không có dấu
5 xuất hiện),
3 của mọi phần tử trong mảng sẽ được coi là
7 (số nguyên 64 bit). Nếu muốn chỉ định
3 của các phần tử trong mảng, ta cần đặt giá trị cho
9. Ví dụ:
Bài tập: Khai báo một mảng numpy hai chiều
0 mô tả ma trận: \[\mathbf{A} = \left[ \begin{matrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9 \end{matrix} \right] \] 2.2. Ma trận đơn vị và ma trận đường chéo2.2.1. Ma trận đơn vịĐể tạo một ma trận đơn vị có số chiều bằng
1 (ma trận đơn vị là một ma trận vuông có tất cả các phần tử trên đường chéo bằng 1), chúng ta sử dụng hàm
2:
Hàm
2 cũng được dùng để tạo các ma trận toàn 1 ở một đường chéo phụ nào đó, các thành phần còn lại bằng 0. Ví dụ:
4 sẽ tương ứng với đường chéo phụ ngay trên đường chéo chíh.
5 sẽ tương ứng với đường chéo phụ thứ hai bên dưới đường chéo chính. Bạn đọc có thể đọc thêm về cách sử dụng hàm ‘np.eye()’ tại đây. Xin nhắc lại rằng bạn đọc luôn có thể xem cách sử dụng một hàm trên terminal bằng cách gõ
6 trong đó
7 là tên hàm bạn muốn tra cứu. Ví dụ,
8. 2.2.2. Ma trận đường chéoĐể khai báo một ma trận đường chéo, hoặc muốn trích xuất đường chéo của một ma trận, ta dùng hàm
9.
Đường chéo phụ của một ma trận cũng có thể được lấy bằng cách sử dụng hàm này và chỉ ra giá trị của
5:
Bài tập: Với một số tự nhiên
1, hãy viết hàm trả về ma trận có dạng: \[ \left[ \begin{matrix} 0 & 0 & 0 & 0 & \dots & 0 & 0 \\ 1 & 0 & 0 & 0 & \dots & 0 & 0 \\ 0 & 2 & 0 & 0 & \dots & 0 & 0 \\ \dots & \dots & \dots & \dots & \ddots & \dots \\ 0 & 0 & 0 & 0 & \dots & 0 & 0 \\ 0 & 0 & 0 & 0 & \dots & n & 0 \end{matrix} \right] \] tức đường chéo phụ ngay dưới đường chéo chính nhận các giá trị từ 1 đến \(n\). Các thành phần là kiểu số nguyên. 2.3. Kích thước của ma trậnGiống như cách tìm kích thước của mảng một chiểu, để tìm kích thước của mảng hai chiều, ta cũng sử dụng thuộc tính
7:
Ở đây, kết quả trả về là một
8. Số phần tử của tuple này chính là số chiều của mảng. Nếu coi mảng hai chiều như ma trận, số hàng và số cột của ma trận có thể được tính bằng:
Với mảng numpy nhiều chiều, ta cũng dùng thuộc tính
7 để tìm kích thước của mỗi chiều. 2.4. Truy cập vào từng phần tử của ma trận2.4.1. Truy cập vào từng phần tửCó hai cách để truy cập vào mỗi phần tử của mảng hai chiều: 2.4.1.1. Cách 1: giống với listĐể truy cập vào phần tử ở hàng thứ
0, cột thứ
1 của ma trận (chỉ số bắt đầu từ 0), ta có thể coi phần tử đó là phần tử thứ
1 của mảng
0 trong mảng hai chiều ban đầu. Ví dụ:
ở đây
4 chính lả mảng một chiều
5, trong mảng này, ta lấy phần tử có chỉ số là
6, phần tử đó có giá trị là
7. Vậy
8 = 6. 2.4.1.2. Cách 2: giống như MatlabTrong Matlab, để truy cập vào phần tử ở hàng đầu tiên, cột đầu tiên của một ma trận
0, ta sử dụng
0. Trong Numpy, có một chút thay đổi:
Ví dụ 2.4.2. Truy cập vào hàng/cộtĐể truy cập vào hàng có chỉ số
0 của một ma trận
0, ta chỉ cần dùng
4 hoặc
5 hoặc
6:
Để truy cập vào cột có chỉ số
1, ta dùng
8:
0 Chú ý:
Bài tập: Cho một ma trận
0, viết hàm
09 tính tổng các phần tử trên các cột có chỉ số chẵn (
1 Giải thích: cột có chỉ số
11 của ma trận là mảng
12, tổng các phần tử của mảng này là 4. 2.5. Truy cập vào nhiều phần tử của ma trận2.5.1. Nhiều phần tử trong cùng một hàngViệc truy cập vào nhiều phần tử trong một hàng tương tự như với mảng một chiều:
2 trong đó,
13 tạo ra một
14 các phần tử là cấp số cộng với công sai là
6, bắt đầu từ
11 và kết thúc tại số lớn nhất có thể không vượt quá số cột của
0. Số cột của
0 chính là
19. 2.5.2. Nhiều phần tử trong cùng một cộtTương tự với nhiều phần tử trong cùng một cột:
3 2.5.3. Nhiều hàng, nhiều cộtNếu muốn trích một ma trận con từ ma trận ban đầu, giả sử lấy ma trận được tạo bởi hàng có chỉ số
00 và
6, cột có chỉ số
11 và
23, ta làm như sau:
4 Chú ý: Một cách tự nhiên, bạn đọc có thể suy ra rằng câu lệnh nên là
24 (giống như cách làm trong Matlab). Tuy nhiên, câu lệnh này sẽ cho ra một kết quả khác (xem mục 4).
25 có thể hiểu được là: đầu tiên lấy hai hàng có chỉ số
00 và
6 bằng
28, ta được một ma trận, sau đó lấy hai cột có chỉ số
11 và
23 của ma trận mới này. 2.5.4. Cặp các toạ độXét câu lệnh:
5 Câu lệnh này sẽ trả về một mảng một chiều gồm các phần tử:
31 và
32, tức
0 và
34 là
14 các toạ độ theo mỗi chiều. Hai
14 này phải có độ dài bằng nhau hoặc một
14 có độ dài bằng 1. Khi một
14 có độ dài bằng 1, nó sẽ được cặp với mọi phần tử của
14 còn lại. Ví dụ:
6 Bài tập: Viết hàm
09 tính tổng tất cả các phần tử có cả hai chỉ số đều chẵn của một ma trận
0 bất kỳ. Ví dụ:
7 Gợi ý: bạn đọc tìm đọc trước cách sử dụng
42 cho mảng nhiều chiều. 2.6. np.sum, np.min, np.max, np.mean cho mảng nhiều chiềuXin nhắc lại về cách quy ước
0 của ma trận.
4 là tính theo chiều từ trên xuống dưới, nghĩa là phương của nó cùng với phương của các cột. Tương tự
9 sẽ có phương cùng với phương của các hàng. Hãy quan sát hình dưới đây và ghi nhớ cách quy ước quan trọng này. Xét một ma trận:
8 Và các hàm
46 tác động lên
0 theo
4 (tức các cột của
0), kết quả sẽ là:
9 Các giá trị theo các hàm trê lần lượt là tổng, giá trị nhỏ nhất, giá trị lớn nhất, trung bình theo mỗi cột. Kết quả trả về là các mảng một chiều có số phần tử bằng số cột của
0. Tương tự như thế khi thay
9:
0 Kết quả trả về được tính theo hàng. Kết quả trả về cũng là các mảng một chiều có số phần tử bằng với số hàng của A. Khi không đề cập tới
0, kết quả được tính trên toàn bộ ma trận:
1
7 Đôi khi, để thuận tiện cho việc tính toán về sau, chúng ta muốn kết quả trả về khi
4 là các vector hàng thực sự, khi
9 là các vector cột thực sự. Để làm được việc đó, Numpy cung cấp thuộc tính
7 (mặc định là
57). Khi
7, nếu sử dụng
4, kết quả sẽ là một mảng hai chiều có chiều thứ nhất bằng 1 (coi như ma trận một hàng). Tương tự, nếu sử dụng
9, kết quả sẽ là một mảng hai chiều có chiều thứ hai bằng 1 (một ma trận có số cột bằng 1). Việc này, về sau chúng ta sẽ thấy, quan trọng trong nhiều trường hợp đặc biệt:
2 Bài tập: Cho một ma trận
0 bất kỳ. Trong mỗi hàng, ta định nghĩa độ biến động của nó là sự khác nhau giữa giá trị lớn nhất và nhỏ nhất của các phần tử trong hàng đó. Hãy viết hàm
09 trả về tổng độ biến động của tất cả các hàng trong ma trận đó. Ví dụ với ma trận
0 trong bài học, độ biến động của mỗi hàng lần lượt là
64. Vậy
65. 2.7. Các phép toán tác động đến mọi phần tử của ma trận2.7.1. Tính toán giữa một mảng hai chiều và một số vô hướngKhi tính toán giữa một số vô hướng và một mảng hai chiều, ví dụ:
3 Ta nhận thấy rằng từng phần tử của mảng sẽ được kết hợp với số vô hướng bằng các phép toán tương ứng để tạo ra một mảng mới cùng kích thước. Việc này, như cũng đã trình bày trong khi làm việc với mảng một chiều, đúng với các mảng numpy với số chiều bất kỳ. 2.7.2. np.abs, np.sin, np.exp, …Bạn đọc cũng có thể dự đoán được rằng các hàm số này cũng tác động lên từng phần tử của mảng và trả về một mảng cùng kích thước với mảng ban đầu.
4 Bài tập: Frobenious norm của một ma trận được định nghĩa là căn bậc hai của tổng bình phương các phần tử của ma trận. Frobenius norm được sử dụng rất nhiều trong các thuật toán Machine Learning vì các tính chất toán học đẹp của nó, trong đó quan trọng nhất là việc đạo hàm của bình phương của nó rất đơn giản. Frobenius norm của một ma trận \(\mathbf{A}\) được ký hiệu là \(||\mathbf{A}||_F\) Numpy có sẵn hàm tính toán norm này, tuy nhiên, chúng ta nên học cách tự tính nó trước. Viết hàm
66 tính Frobenius norm của một ma trận bất kỳ. Ví dụ:
5 thì
67 \(= \sqrt{1^2 + 3^2 + 2^2 + 5^2}\). Gợi ý: Sử dụng hàm
68. 2.8. Các phép toán giữa hai ma trận ICác phép toán cộng, trừ, nhân, chia, luỹ thừa (
6 Chú ý: tích của hai ma trận như định nghĩa trong Đại số tuyến tính được thực hiện dựa trên hàm số khác. Cách viết
70 được thực hiện trên từng cặp phần tử của
0 và
72 Bài tập: Trong khi làm việc với Machine Learning, chúng ta thường xuyên phải so sánh hai ma trận. Xem xem liệu chúng có gần giống nhau không. Một cách phổ biến để làm việc này là tính bình phương của Frobineous norm của hiệu hai ma trận đó. Cụ thể, để xem ma trận \(\mathbf{A}\) có gần ma trận \(\mathbf{B}\) hay không, người ta thường tính \(||\mathbf{A} - \mathbf{B}||_F^2\). Cho hai mảng hai chiều có cùng kích thước
0 và
72. Viết hàm
75 tính bình phương Frobenious norm của hiệu hai ma trận được mô tả bởi hai mảng đó. 2.9. Chuyện vị ma trận, Reshape ma trận2.9.1 Chuyển vị ma trậnCó hai cách để lấy chuyển vị của một ma trận: dùng thuộc tính
76 hoặc dùng hàm
77:
7 2.9.2. ReshapeKhi làm việc với ma trận, chúng ta sẽ phải thường xuyên làm việc với các phép biến đổi kích thước của ma trận. Phép biến đổi kích thước có thể coi là việc sắp xếp lại các phần tử của một ma trận vào một ma trận khác có tổng số phần tử như nhau. Trong numpy, để làm được việc này chúng ta dùng phương thức
78 hoặc hàm
79. Cùng xem ví dụ:
8 Số chiều của mảng mới không nhất thiết phải bằng 2, nó có thể bằng bất kỳ giá trị nào (lớn hơn hoặc bằng 1) nhưng phải đảm bảo tổng số phần tử của hai mảng là như nhau. Khi biến thành mảng một chiều, ta không dùng tuple (như
9 Ta có thể nhận thấy rằng nếu biến thành một mảng hai chiều mới, ta không nhất thiết phải biết kích thước của mỗi chiều mà chỉ cần kích thước của một chiều. Kích thước còn lại được suy ra từ việc tổng số phần tử của hai mảng là như nhau. Tương tự, nếu biến thành một mảng ba chiều mới, ta chỉ cần biết hai trong ba kích thước. Kích thước còn lại sẽ được python tự tính ra, và ta chỉ cần gán nó bằng
81:
0 2.9.3. Thứ tự của phép toán reshapeCó một điểm quan trọng cần nhớ là thứ tự của phép toán reshape: các phần tử trong mảng mới được sắp xếp như thế nào. Có hai cách sắp xếp chúng ta cần lưu ý: mặc định là
82, và một cách khác là
83 (xem hình). Trong
82, các thành phần của mảng nguồn được quét từ
0 trong ra ngoài (
9 rồi mới tới
4 trong mảng hai chiều, tức từng hàng một), sau đó chúng được xếp vào mảng đích cũng theo thứ tự đó. Trong
88 (Fortran) các thành phần của mảng nguồn được quét từ
0 ngoài vào trong (trong mảng hai chiều là từng cột một), sau đó chúng được sắp xếp vào mảng đích cũng theo thứ tự đó - từng cột một.
1 (Đọc thêm
90.) Bài tập: Hãy tạo ma trận
0 sau một cách nhanh nhất, không dùng cách thủ công ghi từng phần tử ra. \[ \left[ \begin{matrix} 1 &5&9&2\\6&10&3&7 \\11&4&8&12 \end{matrix} \right] \] Gợi ý:
Bạn có thể nhận được phản hồi ‘Kết quả thành công’ nhưng hãy thử cố nghĩ quy luật của ma trận này rồi dùng các phép
93 thích hợp. 2.10. Các phép toán giữa ma trận và vectorChúng ta đã qua các bài về phép toán giữa một mảng hai chiều và một số vô hướng, giữa hai mảng hai chiều cùng kích thước. Trong bài này, chúng ta cùng làm quen với các phép toán giữa một mảng hai chiều và một mảng một chiều. Trước tiên, hãy thử vài ví dụ:
2 Nhận thấy rằng kết quả của phép toán
94 thu được bằng cách lấy từng hàng của
0 cộng với
96. Kết quả của
97 thu được bằng cách lấy tích của từng hàng của
0 và
96 - tích ở đây là tích theo từng phần tử của hai mảng một chiều, không phải tích vô hướng của hai vector. Nói cách khác, kết quả của
97 thu được bằng cách lấy từng cột của
0 nhân với phần tử tương ứng của
96. Quy luật tương tự xảy ra với cả phép
03,
04 và
05:
3 Bài tập Giả sử tập dữ liệu bao gồm nhiều điểm dữ liệu có cùng chiều, được sắp xếp thành một mảng hai chiều mô tả một ma trận - được gọi là ma trận dữ liệu. Mỗi hàng của ma trận này là một điểm dữ liệu. Một trong các kỹ thuật quan trọng trước khi áp dụng các thuật toán Machine Learning lên dữ liệu là . Trong các phương pháp chuẩn hoá dữ liệu, một phương pháp thường được sử dụng là đưa dữ liệu về dạng zero-mean, tức trung bình cộng của toàn bộ dữ liệu là một vector có toàn bộ các thành phần bằng 0. Cách chuẩn hoá này có thể được thực hiện bằng cách trước tiên tính vector trung bình của toàn bộ dữ liệu (ở đây là vector trung bình của toàn bộ các hàng), sau đó lấy từng điểm dữ liệu trừ đi vector trung bình. Khi đó, ma trận mới sẽ có trung bình cộng các hàng bằng vector 0, và ta nói rằng ma trận dữ liệu mới này là zero-mean. Cho một mảng hai chiều
06 mô tả dữ liệu, trong đó
07 là một mảng một chiều mô tả dữ liệu có chỉ số
0. Hãy viết hàm
09 trả về ma trận dữ liệu đã chuẩn hoá theo zero-mean. 2.11. Tích giữa hai ma trận, tích giữa ma trận và vector2.11.1. Tích giữa hai ma trậnTrong Đại Số Tuyến Tính (ĐSTT), tích của hai ma trận \(\mathbf{A} \in \mathbb{R}{m\times n}\) và \(\mathbf{B} \in \mathbb{R}{n \times p}\) được ký hiệu là \(\mathbf{C = AB} \in \mathbb{R}{m \times p}\) trong đó phần tử ở hàng thứ \(i\) cột thứ \(j\) (tính từ \(0\)) của \(\mathbf{C}\) được tính theo công thức: \[ c_{ij} = \sum_{k=0}{n-1}a_{ik}b_{kj} \] Chú ý rằng để phép nhân thực hiện được, số cột của ma trận thứ nhất phải bằng với số hàng của ma trận thứ hai (ở đây đều bằng \(n\)). Và phép nhân ma trận không có tính chất giao hoán, nhưng có tính chất kết hợp, tức: \[ \mathbf{ABC} = \mathbf{(AB)C} = \mathbf{A}(\mathbf{BC}) \] Trong numpy, ký hiệu
10 không thực sự để chỉ tích hai ma trận theo nghĩa này mà là tích theo từng cặp phần tử (element-wise). Phép toán
10 trong numpy yêu cầu hai mảng phải có cùng kích thước, và phép toán này có tính chất giao hoán vì phép nhân của hai số vô hướng có tính chất giao hoán. Cho hai mảng numpy hai chiều
12 trong đó
13 (đừng quên điều kiện này). Nếu hai mảng này mô tả hai ma trận thì tích của hai ma trận (theo ĐSTT) có thể được thực hiện bằng thuộc tính
14 hoặc hàm
15:
4 2.11.2. Tích giữa một ma trận và một vectorTrong ĐSTT, tích giữa một ma trận và một vector cột được coi là một trường hợp đặc biệt của tích giữa một ma trận và một ma trận có số cột bằng một. Khi làm việc với numpy, ma trận được mô tả bởi mảng hai chiều, vector được mô tả bởi các mảng một chiều. Xem ví dụ dưới đây:
5 Tích của mảng hai chiều
0 và mảng một chiều
96 với
18 theo ĐSTT được thực hiện bằng phương thức
19 của mảng numpy
0. Kết quả trả về là một mảng một chiều có
21. Chúng ta cần chú ý một chút ở đây là kết quả trả về là một mảng một chiều chứ không phải một vector cột (được biểu diễn bởi một mảng hai chiều có
97 cũng được chỉ ra để nhắc các bạn phân biệt hai phép nhân này. Tiếp tục quan sát:
6 ta thấy rằng nếu đặt
96 lên trước
0 thì có lỗi xảy ra vì xung đột chiều. Tuy nhiên nếu mảng một chiều
26 có kích thước bằng
27 thì lại có thể nhân với mảng hai chiều
0 được. Kết quả thu được chính là vector hàng
26 nhân với ma trận
0. (Bạn có thể tự kiểm tra lại). Có một chút cần lưu ý ở đây: Nếu mảng một chiều được nhân vào sau một mảng hai chiều, nó được coi như một vector cột. Nếu nó được nhân vào trước một mảng hai chiều, nó lại được coi là một vector hàng. Dù sao thì nó vẫn là một vector, và vẫn được lưu bởi một mảng một chiều :). Đây cũng chính là một trong những lý o mà những người ban đầu làm quen với numpy gặp nhiều khó khăn. Bài tập: Quay lại với Frobineus norm. Có một cách khác để tính bình phương của Frobineus norm của một ma trận dựa trên công thức: \[ ||\mathbf{A}||_F^2 = \text{trace}(\mathbf{AA}^T) = \text{trace}(\mathbf{A}^T\mathbf{A}) \] trong đó \(\text{trace}()\) là hàm tính tổng các phần tử trên đường chéo của một ma trận vuông. Cho một mảng hai chiều
0, hãy viết hàm
32 tính bình phương của Frobineus norm của ma trận này dựa vào công thức trên. Gợi ý:
Hy vọng các bạn gặp khó khăn chút với Compiler và nhận ra lý do của việc đó ;). 2.12. Softmax III - Phiên bản tổng quátChúng ta đã làm quen với Phiên bản ổn định của hàm Softmax với một một mảng một chiều \(\mathbf{z}\): \[\frac{\exp(z_i)}{\sum_{j=0}{C-1} \exp(z_j)} = \frac{\exp(-b)\exp(z_i)}{\exp(-b)\sum_{j=0}{C-1} \exp(z_j)} = \frac{\exp(z_i-b)}{\sum_{j=0}^{C-1} \exp(z_j-b)}\] Bây giờ, chúng ta tiếp tục tổng quát hàm số này để áp dụng cho nhiều phần tử cùng lúc. Giả sử ma trận \(\mathbf{Z}\) là ma trận scores của \(N\) điểm dữ liệu, mỗi hàng \(\mathbf{z}_i\) của ma trận này ứng với score của một điểm dữ liệu. Hãy viết một hàm số trên python để tính softmax cho từng hàng của \(\mathbf{Z}\). Kết quả thu được là một ma trận \(\mathbf{A}\) cùng chiều với \(\mathbf{Z}\) mà mỗi hàng của \(\mathbf{A}\) là kết quả khi áp dụng hàm Softmax lên một hàng tương ứng của \(\mathbf{Z}\). |