AICurious Logo
Published on
Monday, January 20, 2020

Linear Regression - Hồi quy tuyến tính cơ bản

3390 words17 min read
Authors

Linear Regression (hồi quy tuyến tính) là một trong những thuật toán cơ bản nhất của Machine Learning. Ở bài viết này, tôi sẽ giới thiệu đến các bạn khái niệm về thuật toán này, lý thuyết toán học và cách triển khai thuật toán trên Python. Bài viết này được viết bằng Jupyter Lab.

Trước hết hãy import và setup các thư viện cần thiết.

%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np

I. Lý thuyết toán học

1. Bài toán tìm phương trình đường thẳng

Giả sử ta có phân bố ở một mặt phẳng dưới dạng một đường thẳng, nhiệm vụ của chúng ta là tìm ra phương trình đường thẳng khớp nhất với dữ liệu đó. Phương trình đường thẳng cho bài toán này khá đơn giản, hầu hết chúng ta đã quen thuộc với nó từ trung học:

y=ax+by = ax + b

trong đó aa có thể được coi là độ dốc hay slope, quyết định đến độ nghiêng của đường thẳng; còn bb có thể hiểu là chặn hay intercept của đường thẳng, quyết định sự dịch chuyển của đường thẳng so với gốc toạ độ. Với b=0b = 0, đường thẳng sẽ đi qua gốc toạ độ.

Hãy xem xét một dữ liệu có dạng đường thẳng với a=3a = 3b=5b = -5 (là đường thẳng màu đỏ trong hình bên dưới). Ở đây tôi sẽ dùng np.random để tạo ra các điểm phân bố theo một đường thẳng (các chấm xanh ở trong hình vẽ). Ở phần sau của bài viết này, ta sẽ cùng tìm hiểu cách dùng Linear Regression để tìm phương trình đường thẳng ứng với các điểm dữ liệu rời rạc đó.

rng = np.random.RandomState(1)
x = 10 * rng.rand(25)
y = 3 * x - 5 + rng.randn(25)
plt.scatter(x, y)
plt.plot(x, 3 * x - 5, linestyle='solid', color='red')

2. Bài toán tổng quát

Tìm phương trình đường thẳng là một trường hợp hết sức đơn giản của có thể áp dụng Linear Regression. Trên thực tế, dữ liệu của bài toán Linear Regression có thể nằm trong không gian nhiều hơn 2 chiều. Khi đó, ta có thể hiểu một cách trực quan là output y^\hat{y} của bài toán chính là sự kết hợp các input xix_i theo một tỷ lệ nào đó. Tỷ lệ này là các hệ số wiw_i, và thường được gọi là trọng số của mô hình. Các giá trị xix_i có thể được viết thành vector X\mathbf X, các trọng số wiw_i có thể được viết thành vector W\mathbf W. Việc tối ưu mô hình Linear Regression là tìm ra vector W\mathbf{W} sao cho từ input X\mathbf{X} ta có thể tính ra được output y^\hat{y} của bài toán.

Hãy cùng xem xét biểu diễn bài toán trong không gian nn chiều. Ví dụ bài toán xác định gía nhà dựa trên nn thuộc tính của căn nhà như diện tích, số phòng, khoảng cách đến trung tâm thành phố... Khi đó input của bài toán sẽ là vector X=[x1,x2,x3,,xn]\mathbf{X} = [x_1, x_2, x_3, \dots, x_n] thể hiện các thuộc tính của căn nhà dưới dạng các số thực và output sẽ là y^=f(X)y\hat{y} = f(\mathbf{X}) \approx y, với yy là giá trị thật của căn nhà. f(X)f(\mathbf{X}) có thể được tính bằng công thức sau:

f(X)=w0+w1x1+w2x2+w3x3++wnxnf(\mathbf{X}) = w_0 + w_1 x_1 + w_2 x_2 + w_3 x_3 + \dots + w_n x_n

Nhìn chung giá trị dự đoán y^\hat{y} và giá trị thật yy thường là 2 giá trị khác nhau do sai số mô hình. Nhiệm vụ của bài toán này là đi tìm các tham số tối ưu {w0,w1,w2,w3,,wn}\{w_0, w_1, w_2, w_3, \dots, w_n \} sao cho sự khác nhau này là nhỏ nhất. Bài toán xác định phương trình đường thẳng trong mục 1 chính là trường hợp n=2n = 2, w1w_1aaw0w_0bb.

Ta có thể biểu diễn tham số dưới dạng một vector cột W=[w0,w1,w2,w3,,wn]T\mathbf{W} = [w_0, w_1, w_2, w_3, \dots, w_n]^T và input X\mathbf X mở rộng dưới dạng Xˉ=[1,x1,x2,x3,,xn]\mathbf{\bar{X}} = [1, x_1, x_2, x_3, \dots, x_n]. Ở đây, ta thêm số 1 vào Xˉ\mathbf{\bar{X}} để không cần phải xử lý riêng một trường hợp tham số tự do w0w_0.

Việc tính toán giá trị output dự đoán trở thành:

y^=XˉW\hat{y} = \mathbf{\bar{X}}\mathbf{W}

Sai số dự đoán e=yy^e = y - \hat{y} được gọi là sai số dự đoán. Giá trị này sẽ được tối ưu sao cho gần 0 nhất.

3. Hàm mất mát (loss function)

Chúng ta cần ước lượng xem mô hình của chúng ta bị sai, bị lỗi đến đâu, để từ đó tối ưu mô hình để có thể hoạt động ít lỗi nhất. Việc tính lỗi này không thể dựa vào cảm tính, mà phải dựa vào tính toán số học. Đó là lý do định nghĩa hàm mất mát (loss function) ra đời. Ta có thể hiểu hàm này được thiết kế để đánh giá độ lỗi, độ sai của mô hình. Kết quả của hàm này có giá trị càng lớn thì mô hình của chúng ta càng sai. Việc tối ưu bài toán được đưa về việc tối ưu các giá trị trọng số để hàm mất mát có giá trị nhỏ nhất. Ở bài toán này, dữ liệu huấn luyện gồm nn mẫu - nn cặp giá trị (X_i,yi)(\mathbf{X}\_i, y_i) với i=1,2,,ni = 1, 2, \dots, n. Hàm mất mát có thể được định nghĩa là:

L(W)=12i=1n(yiXˉiW)2=12yXˉW22\mathcal{L}(\mathbf{W}) = \frac{1}{2}\sum_{i=1}^n (y_i - \mathbf{\bar{X}_i}\mathbf{W})^2 = \frac{1}{2} \|\mathbf{y} - \mathbf{\bar{X}}\mathbf{W} \|_2^2

Ở công thức trên, chúng ta lấy giá trị output thực tế ở trong tập dữ liệu huấn luyện y\mathbf{y} trừ đi giá trị dự đoán y^=Xˉw\hat{y} = \mathbf{\bar{X}}\mathbf{w} sau đó lấy bình phương của kết quả đó để ước lượng sai số của 1 điểm dữ liệu. Tại sao không phải lấy trị tuyệt đối, vì chỉ cần lấy giá trị tuyệt đối là ta đã có thể có một dạng sai số rồi? Câu trả lời là do chúng ta cần một hàm để dễ tính toán đạo hàm ở bước tìm nghiệm cho bài toán. Hàm bình phương có đạo hàm tại mọi điểm, còn trị tuyệt đối có đạo hàm bị đứt tại điểm 0. Số 12\frac{1}{2} trong công thức trên chỉ có ý nghĩa làm đẹp kết quả của đạo hàm.

Giá trị tối ưu của ww được ký hiệu là:

w=argminwL(w)\mathbf{w}^* = \arg\min_{\mathbf{w}} \mathcal{L}(\mathbf{w})

4. Tìm nghiệm cho bài toán

Để tìm nghiệm cho bài toán Linear Regression, chúng ta có thể giải phương trình đạo hàm của hàm loss bằng 0. Đạo hàm theo ww của hàm loss có dạng:

L(w)w=XˉT(Xˉwy)\frac{\partial{\mathcal{L}(\mathbf{w})}}{\partial{\mathbf{w}}} = \mathbf{\bar{X}}^T(\mathbf{\bar{X}}\mathbf{w} - \mathbf{y})

Nghiệm tối ưu cho bài toán này có dạng như sau (xem thêm tại đây).

w=(XˉTXˉ)XˉTy\mathbf{w} = (\mathbf{\bar{X}}^T\mathbf{\bar{X}})^{\dagger} \mathbf{\bar{X}}^T\mathbf{y}

II. Cài đặt với Python

Ở phần này, chúng ta sẽ giải một bài toán đơn giản, đó là việc tìm phương trình đường thẳng với dữ liệu tự sinh ra ở mục I.1. Việc mở rộng mã nguồn để giải các bài toán khác như dự đoán giá nhà, dự đoán chất lượng rượu, dự đoán giá xe... bạn đọc có thể tự tìm tòi và mở rộng mã nguồn bên dưới. Dữ liệu thử nghiệm cho các bài toán khác có thể được tải về từ đây. Việc mở rộng này sẽ giúp các bạn hiểu sâu hơn về Linear Regression, đống thời có được kĩ năng áp dụng Linear Regression cho các bài toán khác nhau. Đó cũng chính là mục đích cuối cùng của một kĩ sư AI.

Quay lại bài toán tìm phương trình đường thẳng. Bạn có thể sử dụng Jupyter Lab để cài đặt thuật toán, hay đơn giản là viết tất cả vào một file Python. Trước hết hãy đảm bảo là bạn đã import hết các thư viện cần thiết bằng phần code ở đầu bài viết này:

%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np

Sau đấy là việc sinh ra các dữ liệu ngẫu nhiên, phân bố dưới hình dạng một đường thẳng.

rng = np.random.RandomState(1)
x = 10 * rng.rand(25)
y = 3 * x - 5 + rng.randn(25)
plt.scatter(x, y);
# plt.plot(x, 3 * x - 5, linestyle='solid', color='red')

1. Sử dụng công thức trong phần lý thuyết

Để cho tiện việc ký hiệu, ta viết lại phương trình đường thẳng ở mục I.1 như sau:

y=ax+b=w1x+w0y = ax + b = w_1 x + w_0

Ở phần lý thuyết, ta đã biết được nghiệm tối ưu của bài toán có dạng như sau:

w=(XˉTXˉ)XˉTy\mathbf{w} = (\mathbf{\bar{X}}^T\mathbf{\bar{X}})^{\dagger} \mathbf{\bar{X}}^T\mathbf{y}

Ta sẽ thử cài đặt thuật toán bằng công thức này để tìm ra phương trình đường thẳng ở trên.

# Hiện tại, x đang có dạng một array kích thước (25,), tương ứng với 25 giá trị của x vừa được sinh ra.
# Ta cần chuyển x về dạng ma trận 2D có kích thước (25, 1) - ứng với 25 hàng và 1 cột.
# Tiếp đó ta sẽ nối giá trị 1 vào đầu mỗi mẫu dữ liệu để tạo thành ma trận có kích thước (25, 2) -
# ứng với 25 hàng và 2 cột, trong đó cột đầu toàn là số 1. Bạn đọc có thể dùng thêm lệnh print(Xbar) để xem các
# giá trị trong ma trận được sinh ra.
x_reshaped = x.reshape((x.shape[0], 1))
one = np.ones((x_reshaped.shape[0], 1))
Xbar = np.concatenate((one, x_reshaped), axis = 1)

# Tính toán giá trị tối ưu của w theo công thức như phần trên
A = np.dot(Xbar.T, Xbar) # Tính phần trong ngoặc của công thức
b = np.dot(Xbar.T, y) # Tính phần bên ngoài ngoặc
w = np.dot(np.linalg.pinv(A), b) # Tính giá trị tối ưu của w, trong đó hàm np.linalg.pinv() được dùng để tính giả nghịch đảo

np.set_printoptions(precision=3)
print("Phương trình đường thẳng: y = {:0.2f}x + ({:0.3f})".format(float(w[1]), w[0]))

Output:

Phương trình đường thẳng: y = 3.00x + (-5.018)

Vậy là chúng ta đã có thể tìm ra một phương trình đường thẳng cho dữ liệu vừa tạo y=3.00x+(5.018)y = 3.00x + (-5.018). Phương trình này khá gần với phương trình gốc mà chúng ta dùng để tạo ra đường thẳng đó (y=3x5y = 3x - 5). Có thể nói thuật toán của chúng ta đã hoạt động khá tốt. Hãy cùng vẽ đường thẳng chúng ta tìm được cùng với dữ liệu ban đầu:

rng = np.random.RandomState(1)
x = 10 * rng.rand(25)
y = 3 * x - 5 + rng.randn(25)
plt.scatter(x, y);
plt.plot(x, w[1] * x + w[0], linestyle='solid', color='red')

2. Sử dụng thư viện scikit-learn

scikit-learn là thư viện rất phổ biến trong Machine Learning. Hãy dùng xem cách cài đặt thuật toán tương tự như trên trong thư viện này.

from sklearn import datasets, linear_model

regr = linear_model.LinearRegression(fit_intercept=False)
regr.fit(Xbar, y)
w = regr.coef_

print("Phương trình đường thẳng: y = {:0.2f}x + ({:0.3f})".format(float(w[1]), w[0]))

Output:

Phương trình đường thẳng: y = 3.00x + (-5.018)

Vậy là phương trình đường thẳng chúng ta có được cũng tương tự với cách tính bằng Python thuần phía trên. Trên thực tế, bạn nên dùng thư viện scikit-learn thay vì tự tính theo công thức mình khai triển vì các thuật toán trong thư viện scikit-learn đã được đánh giá kĩ càng bởi các chuyên gia và được tối ưu về mặt tốc độ.

III. Thảo luận

Linear Regression là một thuật toán đơn giản và dễ cài đặt cho một bài toán với quan hệ tuyến tính giữa input và output. Tuy vậy Linear Regression có một nhược điểm lớn là nhạy cảm với nhiễu. Hãy cùng thử nghiệm bằng cách thêm một điểm nhiễu (12.5,5)(12.5, 5) vào dữ liệu sinh ra.

# Sinh dữ liệu ngẫu nhiên tương tự như trước
rng = np.random.RandomState(1)
x = 10 * rng.rand(25)
y = 3 * x - 5 + rng.randn(25)

# Thêm điểm nhiễu (12.5, 5)
x = np.append(x, 12.5)
y = np.append(y, 5)

# Chuẩn bị Xbar
x_reshaped = x.reshape((x.shape[0], 1))
one = np.ones((x_reshaped.shape[0], 1))
Xbar = np.concatenate((one, x_reshaped), axis = 1)

# Tối ưu mô hình
regr = linear_model.LinearRegression(fit_intercept=False)
regr.fit(Xbar, y)
w = regr.coef_

# Vẽ kết quả
plt.scatter(x, y);
plt.plot(x, w[1] * x + w[0], linestyle='solid', color='red')

Có thể thấy, chỉ một điểm nhiễu (12.5,5)(12.5, 5) đã có thể kéo lệch đường thẳng được tìm ra bởi Linear Regression khá nhiều. Điều này cho thấy thuật toán rất nhạy cảm với nhiễu.

Linear Regression cho bài toán phi tuyến: Ta vẫn có thể sử dụng Linear Regression cho một bài toán phi tuyến tính bằng cách biến đổi nó một chút. Hãy bắt đầu với phương trình cơ bản cho Linear Regression:

y=f(x)=w0+w1x1+w2x2+w3x3++wnxny = f(\mathbf{x}) = w_0 + w_1 x_1 + w_2 x_2 + w_3 x_3 + \dots + w_n x_n

Ta đặt xn=fn(x)x_n = f_n(x) với fn()f_n() là một hàm dùng để biến đổi dữ liệu đầu vào. Ví dụ với fn(x)=xnf_n(x) = x^n, bài toán được chuyển thành hồi quy đa thức.

y=f(x)=w0+w1x1+w2x22+w3x33++wnxnny = f(\mathbf{x}) = w_0 + w_1 x_1 + w_2 x_2^2 + w_3 x_3^3 + \dots + w_n x_n^n

Cách làm này giúp chúng ta dùng Linear Regression với các quan hệ phức tạp hơn giữa xxyy. Tuy nhiên, nó lại dế gây ra hiện tượng overfitting, dẫn đến việc phải có thêm các phương pháp regularization.

Tham khảo