[Nhập môn Machine Learning] Bài 10: Cài đặt Linear Regression
Ở bài này, chúng ta sẽ sử dụng thư viện Numpy để tiến hành cài đặt thuật toán Linear Regression. Không giống những lần trước, khi chương trình của tôi chỉ xử lý được dữ liệu một chiều. Lần này, chúng ta có thể mở rộng ra cho dữ liệu nhiều chiều bằng cách vector hóa chúng. (Nếu bạn không biết vector hóa, bạn có thể xem lại bài viết này).
Trước khi bắt đầu, ta cần phải quy ước trước kích thước của các tham số trong thuật toán Linear Regression. Để cho thuận tiện, tôi sẽ sử dụng quy ước dưới đây:
Trong code, ta làm như sau
Ở đây, sau khi nhân với , kết quả ta có được là một ma trận kích thước là kết quả của phép toán . Tuy nhiên ta vẫn cần phải cộng thêm (là biến ) cho kết quả này. Quả thật, ta chỉ cần cộng thẳng b vô thôi, các ndarray sẽ tự động cộng cho tất cả các phần tử thông qua cơ chế broadcasting.
hay
Với code, ta làm như sau
Có một hàm các bạn có thể thấy không quen đó là
sẽ thỏa mãn phương trình
còn với , ta chỉ cần cộng các chênh lệch lại rồi chia cho . Ta sẽ code như sau
Hàm này trả về giá trị là gradient của và đạo hàm của . Đến đây, ta có thể bắt đầu cài đặt hàm cuối cùng là thuật toán gradient descent.
Bạn có thể tự hỏi tại sao lần trước tôi lấy điều kiện dừng là khi đủ nhỏ nhưng lần này lại có điều kiện khác. Trên thực tế, có nhiều cách dừng thuật toán này lại tùy theo yêu cầu bài toán. Điều kiện lý tưởng nhất là khi mỗi đạo hàm có giá trị tuyệt đối cực nhỏ, nhưng cho đến khi đó sẽ tốn rất nhiều lần lặp của chúng ta nhưng hiệu quả (hàm Cost Function) của chúng ta chẳng giảm được bao nhiêu. Vậy nên đôi khi ta chỉ cần đáp số đủ tốt là ổn rồi và điều này phản ánh qua mức chênh lệch của Cost Function.
Và để chắc chắn hơn, tôi cũng giới hạn số lần lặp của Gradient Descent để không phải chờ quá lâu, bạn có quyền nâng giá trị
Khi đã có dữ liệu, ta bắt đầu đưa chúng vào thuật toán và nhận lại các tham số sau khi chạy Gradient Descent cũng như lịch sử giá trị hàm Cost.
Cuối cùng, để dễ thấy rằng hàm Cost của ta giảm theo thời gian, tốt nhất là nên đưa chúng lên một biểu đồ.
Đây là kết biểu đồ tôi nhận được sau cùng.
Tất nhiên các bạn không cần lấy dữ liệu một chiều này mà bất cứ dữ liệu nào, 2, 3 feature tùy ý.
Trước khi bắt đầu, ta cần phải quy ước trước kích thước của các tham số trong thuật toán Linear Regression. Để cho thuận tiện, tôi sẽ sử dụng quy ước dưới đây:
- sẽ là ma trận chứa các mẫu dữ liệu. Ma trận này có kích thước tức là mỗi dòng sẽ là một điểm dữ liệu, còn mỗi dòng sẽ là một feature.
- sẽ là một vector chứa các nhãn có độ dài .
- sẽ là vector chứa các tham số của ta có độ dài . Tới đây bạn có thể thắc mắc tại sao lại là mà không phải vì ta cần một chỗ cho và . Lý do sẽ được giải thích ngay sau đây.
- Khác với lần trước là ta phải thêm một cột các giá trị vào trước mà ta hay gọi là để nhân được với tham số thì bây giờ ta không cần làm vậy nữa. Cơ chế broadcasting sẽ giúp chúng ta trong việc này nên tham số ta có thể để riêng, tôi sẽ gọi tham số này là . Việc này có lợi trên thực nghiệm vì ta không phải thay đổi ma trận .
import
thư viện Numpy vào đầu file để có thể sử dụng.Hypothesis Function
Tiếp theo, ta sẽ tiến hành cài đặt hàm tính hypothesis function của Linear Regression. Như đã biết ở phần vector hóa, ta có công thứcTrong code, ta làm như sau
def hypothesis_function(X, theta, b):
""" Hàm tính dự đoán của mô hình"""
return np.matmul(X, theta) + b # shape (m,)
Cost Function
Đây là hàm quan trọng thứ hai trong Linear Regression, công thức mà lúc trước ta đã biết được làhay
Với code, ta làm như sau
def cost_function(X, y, theta, b):
"""Hàm tính toán chi phí"""
m = X.shape[0]
# Lấy dự đoán của mô hình
y_hat = hypothesis_function(X, theta, b) # shape (m,)
# Tính độ chênh lệch giữa dự đoán và nhãn
z = y_hat - y # shape (m,)
return 1/(2*m) * np.matmul(z.T, z).squeeze()
np.squeeze
. np.squeeze
giúp giảm số chiều của ndarray đi. Có thể nói là nó sẽ bỏ hết tất cả các số trong shape
đi. Ví dụ một ndarray có shape (m,1,1)
hay (1,m)
sẽ chỉ còn là (m)
sau khi squeeze
. Trong trường hợp của ta trong đoạn code trên, sau khi nhân, ta sẽ có một ndarray shape (1,1)
, tức là nó vẫn là một ma trận hai chiều và điều này rất thiếu tự nhiên. squeeze
giúp bỏ hết những chiều thừa đi, khiến giá trị chỉ còn là một số thực.Tính toán Gradient
Với hàm tính gradient, ta cósẽ thỏa mãn phương trình
còn với , ta chỉ cần cộng các chênh lệch lại rồi chia cho . Ta sẽ code như sau
def calc_gradient(X, y, theta, b):
"""Hàm trả về tính toán gradient của biến theta và b"""
m = X.shape[0]
# Lấy dự đoán của mô hình
y_hat = hypothesis_function(X, theta, b) # shape (m,)
# Tính độ chênh lệch giữa dự đoán và nhãn
z = y_hat - y # shape (m,)
return 1/m*np.matmul(X.T, z).squeeze(), 1/m*np.sum(z)
Gradient Descent
Hàm này sẽ nhận vào 2 biếnX
và y
và trả về các tham số tối ưu cùng một mảng chứa các giá trị của Cost Function qua từng vòng lặp. Với các giá trị ban đầu của các tham số, ta có thể khởi tạo chúng với giá trị ngẫu nhiên vì gradient descent sẽ tìm được tham số tối ưu dù tham số ban đầu như thế nào.def gradient_descent(X, y, alpha=0.01, epsilon=3e-6, max_iter=500):
"""Thực hiện thuật toán gradient descent trên tập dữ liệu X, y"""
m, n = X.shape
# Khởi tạo các giá trị
theta = np.random.rand(n) # Khởi tạo mảng n phần tử ngẫu nhiên
b = 0
history = []
while True:
# Tính gradient
grad_theta, grad_b = calc_gradient(X, y, theta, b)
# Thực hiện update theo ngược chiều gradient
theta -= alpha* grad_theta
b -= alpha* grad_b
#Tính giá trị của cost function
j_cost = cost_function(X, y, theta, b)
history.append(j_cost)
# Ta dừng khi sự khác biệt trong cost đủ nhỏ hoặc số vòng lặp đủ lớn
if abs(history[-2] - history[-1]) < epsilon:
break
elif len(history)> max_iter:
break
return theta, b, history
Và để chắc chắn hơn, tôi cũng giới hạn số lần lặp của Gradient Descent để không phải chờ quá lâu, bạn có quyền nâng giá trị
max_iter
nếu muốn nhiều vòng lặp hơn.Thử nghiệm
Để chắc chắn là thuật toán chạy đúng, ta cần phải thử lại lần nữa. Ở đây, (vì lười) nên tôi sẽ lấy lại bộ dữ liệu lúc trước. Tuy nhiên cũng biến đổi đôi chút để phù hợp với quy ước ban đầu.X = np.array([[3],
[5],
[3.25],
[1.5]]) # shape (4, 1)
y = np.array([1.5, 2.25, 1.625, 1.0]) # shape (4,)
Cuối cùng, để dễ thấy rằng hàm Cost của ta giảm theo thời gian, tốt nhất là nên đưa chúng lên một biểu đồ.
Đây là kết biểu đồ tôi nhận được sau cùng.
Cột y là giá trị hàm Cost, cột x là số lần lặp |
Tất nhiên các bạn không cần lấy dữ liệu một chiều này mà bất cứ dữ liệu nào, 2, 3 feature tùy ý.
các bài viết của bạn rất hay và dễ hiểu , cảm ơn bạn rất nhiều
Trả lờiXóaCảm ơn bạn đã quan tâm tới những bài viết của mình
Xóacác bài viết của tác giả rất hay , mà đợi hơi lâu chưa thấy tg ra thêm bài
Trả lờiXóaBài viết thật sự rất hay ạ
Trả lờiXóa