Mô hình Linear Regression
1. Giới thiệu
Hồi quy tuyến tính (Linear Regression) là một bài toán phổ biến nhất đối với tất cả những ai học về Máy học, Khoa học dữ liệu hay về các ngành kinh tế. Để hiểu được khái niệm về hồi quy tuyến tính, ta cần biết rằng ứng dụng của Hồi quy tuyến tính trong thực tế là rất lớn, ví dụ ta có khái niệm về Phân tích hồi quy, là một phương pháp thống kê được sử dụng phổ biến trong lĩnh vực đầu tư và tài chính... Bây giờ, hãy thử đến với một bài toán như sau: chúng ta dự tính mua một căn nhà nào đó, chúng ta cần dự đoán xem giá của nó khoảng bao nhiêu để so sánh với giá mà chủ nhà yêu cầu là cao hay thấp sau đó mới quyết định mua?
Vậy làm sao để dự đoán giá của căn nhà này khi có rất nhiều yếu tố tác động lên giá như: diện tích, số phòng, số tầng lầu, vị trí (nội thành hay ngoại thành),... và còn nhiều yếu tố khác biết rằng ta đã sở hữu một bộ dữ liệu về giá nhà đất ở quá khứ?
Trên thực tế, ta có rất nhiều những thuật toán có thể giúp ta giải quyết được bài toán này và Hồi quy tuyến tính chính là một trong những phương pháp đấy.
Quay lại với bài toán dự đoán giá nhà, để đơn giản hơn chúng ta sẽ giả định giá trị của căn nhà chỉ phụ thuộc vào diện tích, bảng dữ liệu của chúng ta sẽ có dạng như sau:
Diện tích (m2) | Giá nhà (triệu VNĐ) |
---|---|
31 | 342 |
42 | 452 |
26 | 269 |
34 | 378 |
23 | 243 |
27 | 270 |
40 | 395 |
46 | 379 |
Trực quan hóa dữ liệu |
Như vậy, với câu hỏi như sau: "một căn nhà có diện tích là 30 mét vuông có giá bao nhiêu?" Khi ta sử dụng Hồi quy tuyến tính, ta sẽ thực hiện các bước sau đây:
- Dựa trên dữ liệu cho trước, ta tìm một đường thẳng , với là các hệ số, là diện tích và là giá của căn nhà được dự đoán sao cho đường thẳng này có thể khớp với nhiều điểm dữ liệu nhất có thể (tức khoảng cách giữa đường thẳng này với các điểm dữ liệu trong đồ thị là nhỏ nhất).
- Sau khi tìm được đường thẳng này, tính giá nhà ở có diện tích là 30 mét vuông.
Dự đoán giá nhà bằng một đường thẳng |
2. Phương pháp
Bài toán hồi quy tuyến tính là một bài toán lâu đời, vì vậy hiện nay các nhà khoa học đã tìm và phát triển đa dạng các phương pháp giải khác nhau. Trong phần này, ta sẽ cùng điểm qua một số những phương pháp phổ biến thường được sử dụng trong lĩnh vực Đại số tuyến tính cũng như Máy học từ đó chỉ ra những ưu, nhược điểm của từng phương pháp.
Bạn đọc cũng cần chú ý rằng, tất cả các phương pháp sắp được giới thiệu dù cách giải khác nhau nhưng đều có chung mục tiêu là tìm được một phương trình đường thẳng khớp với bộ dữ liệu cho trước nhất. Khi ta bám vào mục tiêu này, việc đọc hiểu sẽ trở nên dễ dàng hơn.
2.1. Gradient Descent
Đây là phương pháp quan trọng trong lĩnh vực Máy học, là nền tảng cho các thuật toán phức tạp khác cũng như cho ta một cái nhìn trực quan hơn về việc "học" của các mô hình trong Máy học. Vì vậy, bạn đọc cần thật sự chú tâm và nắm thật chắc các lý thuyết xoay quanh phương pháp này.
2.1.1. Hàm giả thuyết - Hypothesis function
Thông thường, chúng ta sẽ biểu diễn phương trình đường thẳng tổng quát như sau: (phương trình Hồi quy tuyến tính đơn biến).
Đường thẳng |
Bây giờ, ta thay đổi ký hiệu hai tham số và thành và , gọi chúng là cặp trọng số, và phương trình ban đầu trở thành: .
Trong Máy học, dạng phương trình trên còn được gọi là Hàm giả thuyết (Hypothesis function). Mục tiêu của hàm giả thuyết là chuyển đổi giá trị X (biến độc lập) sang giá trị y (biến phụ thuộc) sao cho các giá trị y này gần đúng so với giá trị thực tế của nó nhất. Các giá trị X, y này là các điểm dữ liệu (data points, samples...) trong một tập dữ liệu (dataset) nào đó.
Ta có thể coi hàm giả thuyết này như là hàm dự đoán giá trị dựa trên các trường thông tin cho trước. Xét tập dữ liệu ở đầu bài, ta có thể sử dụng hàm giả thuyết Hồi quy tuyến tính để dự đoán giá nhà dựa trên trường thông tin là diện tích của căn nhà đó.
Tổng quát, ta có phương trình giả thuyết cho bài toán Hồi quy tuyến tính đơn biến như sau:
Như đã nói ở trên, mục tiêu của hàm giả thuyết là cố gắng đưa ra dự đoán giá trị y nào đó gần đúng với thực tế nhất dựa trên các trường thông tin X trong một bộ dữ liệu. Khi đó, nếu hàm giả thuyết là một hàm tuyến tính, mục tiêu đó trở thành bài toán tìm được đường thẳng có thể đi qua được tất cả các điểm dữ liệu có trong bộ dữ liệu:
Nhận thấy rằng, để có thể tìm được đường thẳng này, nhiệm vụ của ta lúc này lại trở thành tìm được cặp trọng số tối ưu nhất cho hàm giả thuyết vì chỉ còn cặp trọng số là thành phần mà ta có thể tác động lên được (khi biến x trong phương trình là trường thông tin đã biết trước).
2.1.2. Hàm mất mát - Loss Function
Để tìm ra phương trình đường thẳng phù hợp với dữ liệu, chúng ta cần tìm ra bộ trọng số và tối ưu nhất.
Trước hết, ta giả định sẽ khởi tạo đường thẳng ngẫu nhiên bằng cách cho và một giá trị ngẫu nhiên. Cũng chính vì đường thẳng được tạo ngẫu nhiên nên khi dự đoán thì kết quả dự đoán sai lệch với giá trị thực tế rất lớn.
Độ chênh lệch giữa giá trị dự đoán và thực tế tại điểm |
Vậy để đánh giá một đường thẳng hay mô hình với bộ trọng số có tốt hay không? Ta sẽ đánh giá thông qua độ lệch giữa giá trị dự đoán mà giá trị thực tế.
Độ lệch tại một điểm dữ liệu sẽ được tính bằng cách sử dụng các công thức tính khoảng cách giữa 2 điểm dữ liệu trong đồ thị. Các công thức khoảng cách được sử dụng phổ biến trong bài Hồi quy tuyến tính bao gồm:
- Khoảng cách L1 (L1 distance):
- Khoảng cách L2 (L2 distance):
Khi xét trong ngữ cảnh Máy học, ta gọi sự chênh lệch giá trị giữa giá trị dự đoán và giá trị thực tế là giá trị mất mát (loss). Vì thế, các công thức tính độ lệch sử dụng công thức tính khoảng cách khi đó được gọi là các hàm mất mát (loss function).
Trong bài này, chúng ta sẽ chọn hàm mất mát L2 (hàm mất mát sử dụng công thức tính khoảng cách L2) để làm hàm mất mát cho mô hình hồi quy tuyến tính của chúng ta, công thức của L2 Loss được diễn đạt như sau:
.
Và độ lệch cho tất cả các điểm dữ liệu sẽ là:
Với được gọi là hàm mất mát, ta có một số các tính chất sau:
- không âm.
- càng nhỏ đường thẳng càng gần các điểm dữ liệu, mô hình càng tốt.
- = 0 khi đường thằng đi qua tất cả dữ liệu, mô hình lý tưởng.
Có thể thấy, khi độ lệch giữa giá trị dự đoán và giá trị thực tế càng thấp, hàm giả thuyết của chúng ta lúc này đã cho kết quả dự đoán tốt hơn. Với cấu tạo của hàm mất mát, ta có thể thấy rõ sự tương quan giữa cặp trọng số và giá trị mất mát. Tức nếu giá trị mất mát lớn, bộ trọng số khi đó đã làm cho hàm giả thuyết cho kết quả dự đoán quá khác xa so với thực tế và ngược lại.
Điều này dẫn chúng ta đến một manh mối, rằng nếu ta có thể làm cho hàm mất mát đạt giá trị nhỏ nhất, ta sẽ tìm được bộ trọng số tối ưu nhất cho hàm giả thuyết.
2.1.3. Thuật toán Gradient Descent
Để có thể tìm giá trị lớn nhất, nhỏ nhất của một hàm số, ta có thể nghĩ ngay đến việc đạo hàm. Và thật vậy, thuật toán Gradient Descent, với nồng cốt là đạo hàm, có thể giúp ta tìm bộ trọng số tối ưu nhất để làm cho hàm mất mát đạt giá trị nhỏ nhất.
Trước khi tìm hiểu về Gradient Descent, chúng ta sẽ cùng nhìn lại một chút về đạo hàm. Chúng ta đã học rất nhiều về đạo hàm ở cấp 3 và ứng dụng của đạo hàm trong bài toàn tìm điểm cực trị của hàm số.
Chúng ta sẽ lưu ý 2 tính chất sau của đạo hàm:
- Đạo hàm dương (+) thì hàm số tăng và ngược lại.
- Giá trị đạo hàm thể hiện độ dốc của đồ thị.
Ví dụ: Ta có hàm số
- Kết quả đạo hàm là vậy hàm số này luôn tăng .
- Độ dốc tức với tăng một lượng thì sẽ tăng hai lần tương ứng.
Ý nghĩa đạo hàm |
Ứng dụng đạo hàm trong thuật toán Gradient Descent để tìm giá trị nhỏ nhất.
- Bước 1: Khởi tạo giá trị tùy ý.
- Bước 2: Gán x = x - \text{learning_rate} * f'(x). Với \text{learning_rate} là một hằng số không âm.
- Bước 3: Lặp lại bước 2 cho đến khi đủ nhỏ.
Ví dụ: Tìm giá trị nhỏ nhất của hàm số .
Tìm giá trị nhỏ nhất bằng Gradient Descent |
- Bước 1: Khởi tạo ngẫu nhiên. Giả sử tại điểm A: .
- Bước 2: Vì đồ thị giảm . Khi đó x = x - \text{learning_rate} * f'(x) tức là sẽ được cộng thêm một giá trị dương Tiến dần về (giá trị nhỏ nhất). (*)
- Bước 3: Lặp lại bước 2 cho đến khi đạt giá trị nhỏ nhất hoặc gần giá trị nhỏ nhất.
(*) Ngược lại, nếu giá trị khởi tạo nằm bên phải đồ thị (Điểm E). Lúc này đồ thì tăng . Khi đó giá trị sẽ được trừ đi một số dương Vẫn tiến dần về giá trị nhỏ nhất.
Lưu ý: Việc lựa chọn hằng số learning rate sẽ có 3 trường hợp:
- Learning rate quá nhỏ: Thuật toán cần lặp lại rất nhiều lần để tìm được giá trị nhỏ nhất Tốn nhiều thời gian và chi phí tính toán.
- Learning rate quá lớn: Việc giá trị tăng (hoặc giảm) một cách nhanh chóng sẽ lướt qua điểm tối ưu, còn gọi là hiện tượng overshoot.
- Learning rate tốt: sẽ giúp tối ưu về thời gian và chi phí tính toán cũng như giá trị sẽ bằng hoặc gần bằng với giá trị nhỏ nhất sau mốt số ít lần lặp.
Các giá trị learning rate khác nhau |
Để kiểm tra xem tham số learning rate có phù hợp hay chưa, chúng ta chỉ cần vẽ đồ thị của hàm lên và quan sát giá trị của hàm mất mát.
Giá trị hàm mất mát của các learning rate khác nhau |
Như vậy, khi áp dụng thuật toán Gradient Descent cho hàm mất mát L2 để tìm ra bộ trọng số tối ưu nhất cho hàm giả thuyết, ta có được các bước như sau:
- Bước 1: Tính đạo hàm hàm mất mát L2 theo , việc giải đạo hàm riêng này có thể được thực hiện thông qua quy tắc chuỗi (chain rule):
- Bước 2: Cập nhật trọng số bằng cách trừ đi tích của giá trị đạo hàm riêng vừa tìm được và learning rate: \theta = \theta - \frac{\partial L}{\partial \theta}.\text{learning_rate}
Khi thực hiện các bước trên lần đầu, ta dễ dàng nhận thấy bộ trọng số vừa tìm được chưa phải là tối ưu nhất. Và khi cứ tiếp tục lặp lại các bước tính toán trên, ta lại tìm ra được một bộ trọng số mới (có thể tối ưu hơn hoặc tệ đi). Việc tìm được sự "bao nhiêu là đủ" cho thuật toán Gradient Descent này là một trong những khó khăn vẫn đang được nghiên cứu trong lĩnh vực Máy học.
2.1.4. Huấn luyện "mô hình" Hồi quy tuyến tính
Sau khi điểm qua các khái niệm liên quan đến Gradient Descent, ta sẽ kết nối các thành phần kiến thức đó thành một chuỗi các bước xử lý để giải quyết bài toán Hồi quy tuyến tính dưới góc nhìn của Máy học.
Trong Máy học, quy trình tính toán của phương pháp Gradient Desent này còn được gọi là quá trình huấn luyện "mô hình" (training). Như vậy, có thể hiểu huấn luyện là việc ta lặp đi lặp lại quá trình tính gradient (tính đạo hàm) theo một số lần lặp (epochs) từ đó tìm ra bộ trọng số tối ưu nhất cho "mô hình" và giá trị hàm mất mát đạt nhỏ nhất. Và với từ "mô hình" (model), ta tạm thời có thể hiểu khái này trong Máy học như là một hàm giả thuyết với bộ trọng số tối ưu.
Mô hình với hàm dự đoán là một phương trình tuyến tính và cho kết quả là một giá trị liên tục được gọi là một mô hình Hồi quy tuyến tính (Linear Regression model).
2.2. Phương trình chuẩn - Normal Equation
Thuật toán Gradient Descent sẽ phải lặp nhiều lần để tìm ra cặp tham số để giá trị của hàm số bằng hoặc gần bằng giá trị nhỏ nhất. Tuy nhiên có một giải pháp không cần phải chạy nhiều lần mà có thể trực tiếp tìm được đó là Normal Equation.
Như cách chúng ta đã từng làm trong cấp 3 đó là:
- Tính đạo hàm.
- Cho đạo hàm bằng , tìm .
Giá trị làm cho đạo hàm bằng là cực trị. Và ý tưởng của phương pháp này cũng vậy.
Như phía trên chúng ta đã định nghĩ về hàm mất mát, ta viết lại như sau:
Tiếp theo chúng ta sẽ tìm đạo hàm cho hàm mất mát này.
(*)
Cho đạo hàm bằng để tìm .
Đây là đường thẳng của chúng ta: .
(*) Chi tiết tìm đạo hàm
Cách 1: Biến đổi:
=
=
Đạo hàm từng số hạng sau đó cộng lại:
=
=
= $
=
=
Cách 2: Tham khảo cách tính đạo hàm cho ma trận Matrix Calculus.
2.3. Phân tích giá trị kỳ dị - Singular Value Decomposition
Ngoài cách sử dụng phương trình chuẩn Normal Equation để tìm ra thì SVD-Singular Value Decomposition cũng có khả năng tương tự.
SVD là một phương pháp phân tích một ma trận A có kích thước thành tích của ba ma trận trong đó:
- : ma trận có kích thước sao cho các cột tạo thành một hệ trực chuẩn.
- : ma trận đường chéo có kích thước với r là hạng của ma trận A.
- : ma trận có kích thước sao cho các cột tạo thành một hệ trực chuẩn.
Mục tiêu chính của chúng ta vẫn là tìm sao cho đạt giá trị nhỏ nhất. SVD sẽ phải giải một bất phương trình thay vì phương tình đạo hàm bằng như Normal Equation. Cụ thể như sau:
Phân tích SVD cho thành :
Đặt , ta có:
Với thì nên:
Dấu bằng xảy ra khi và chỉ khi: hay
2.4. So sánh các phương pháp
Sau đây là một số so sánh giữa các phương pháp dựa trên cơ sở lý thuyết của các phương pháp trên. Qua phần 4. Đánh giá, chúng ta sẽ làm rõ một số nhận định này hơn thông qua việc so sánh kết quả thực nghiệm trên một tập dữ liệu cụ thể.
Gradient Descrent | Normal Equation | Singular Value Decomposition | |
---|---|---|---|
Learning rate | Cần chọn learning_rate phù hợp | Không cần chọn | Không cần chọn |
Lặp lại | Cần lặp lại nhiều lần | Chỉ tính một lần | Chỉ tính một lần |
Tốc độ | Chậm | Chậm nếu dữ liệu lớn | Nhanh |
- | Tính toán chậm. Nếu không tính được không giải quyết được bài toán | - | |
Giá trị nhỏ nhất? | Có thể không phải là nhỏ nhất toàn cục | Giá trị gần như tối ưu nếu tính toán được | Là giải pháp tối ưu được sử dụng trong Scikit-learn, Scipy, Numpy |
3. Cài đặt chương trình
Trong phần này, chúng ta sẽ cùng nhau tìm hiểu về cách giải một bài toán Hồi quy tuyến tính thông qua việc cài đặt chương trình, từ bước đọc dữ liệu cho đến bước triển khai các phương pháp giải bài toán hồi quy tuyến tính kể trên sử dụng Python sử dụng thư viện NumPy. Bên cạnh đó, để thuận tiện cho người đọc cũng như để thể hiện các kết quả một cách trực quan, ta sẽ chọn bộ dữ liệu insurance.csv (đường dẫn của bộ dữ liệu được đính kèm ở cuối bài viết) làm bộ dữ liệu thực nghiệm cũng như làm ví dụ.
3.1. Tiền xử lý dữ liệu
Khi giải quyết một bài toán về máy học, một trong những công việc cực kỳ quan trọng cần được ưu tiên giải quyết đầu tiên đó là tiền xử lý dữ liệu. Việc tiền xử lý dữ liệu không chỉ giúp cho chúng ta có thể đưa dữ liệu vào mô hình một cách phù hợp, không gặp lỗi mà còn có thể giúp cải thiện độ hiệu quả của mô hình. Trong bài này, chúng ta sẽ cùng điểm qua một vài bước tiền xử lý dữ liệu cơ bản như mã hóa one-hot, chuẩn hóa...
3.1.1. Trực quan hóa dữ liệu
Để có một cái nhìn rõ ràng về dữ liệu chuẩn bị xử lý, ta cần biểu diễn dữ liệu theo một cách logic nào đó (bảng, đồ thị...) để thuận tiện trong việc quan sát cũng như đưa ra nhận xét.
Bộ dữ liệu insurance.csv là một dạng dữ liệu có cấu trúc (dạng bảng), vì vậy đã phần nào giúp ta có một cái nhìn trực quan về những thuộc tính có trong bộ dữ liệu.
index | age | sex | bmi | children | smoker | region | charges |
---|---|---|---|---|---|---|---|
0 | 19 | female | 27.9 | 0 | yes | southwest | 16884.924 |
1 | 18 | male | 33.77 | 1 | no | southeast | 1725.5523 |
2 | 28 | male | 33.0 | 3 | no | southeast | 4449.462 |
3 | 33 | male | 22.705 | 0 | no | northwest | 21984.47061 |
4 | 32 | male | 28.88 | 0 | no | northwest | 3866.8552 |
Tuy nhiên, ta cũng có thể thực hiện một số thống kê với bộ dữ liệu này và vẽ lên khung đồ thị như sau:
Ngoài ra vẫn còn rất nhiều các kĩ thuật trực quan hóa dữ liệu (Data Visualization) cho ta một cái nhìn trực quan hơn. Bạn đọc có hứng thú với chủ đề này có thể tìm đọc các đầu sách có liên quan cũng như tìm trên Google với từ khóa tương ứng.
3.1.2. Mã hóa one-hot - One-hot encoding
Hầu hết ở các tập dữ liệu, đặc biệt là các tập dữ liệu thực tế, ta sẽ luôn bắt gặp các biến không phải là các con số với giá trị liên tục mà là các văn bản (stirng) dùng để biểu diễn một thể loại (category) của một thuộc tính nào đó trong dữ liệu. Ví dụ, thuộc tính giới tính của một bộ dữ liệu về thông tin sinh viên sẽ có các 2 giá trị ('nam', 'nữ').
Đối với bộ dữ liệu insurance.csv, ta cũng có các trường thuộc tính thuộc dạng "category" này:
index | sex | smoker | region |
---|---|---|---|
0 | female | yes | southwest |
1 | male | no | southeast |
2 | male | no | southeast |
3 | male | no | northwest |
4 | male | no | northwest |
Rõ ràng, ta không thể đưa các giá trị với kiểu dữ liệu chuỗi này để tính toán. Vì vậy, ta cần một cách chuyển các giá trị "category" này thành một dạng biểu diễn số học nào đó. Một trong những thuật toán có thể giúp ta giải quyết được vấn đề này là thuật toán mã hóa One-hot.
Mã hóa One-hot (One-hot encoding) là một thuật toán giúp ta chuyển một trường thuộc tính có giá trị là các "category" thành một vector với các phần tử trong vector này chính là các "category". Hiểu một cách đơn giản, ta sẽ coi mỗi giá trị trong trường thuộc tính là một trường thuộc tính mới riêng biệt (một cột mới trong bảng dữ liệu). Từ đây, ta xét nếu mẫu dữ liệu nào có giá trị "category" nào thì tại cột "category" vừa tạo đó sẽ có giá trị bằng 1, ngược lại sẽ bằng 0. Hình dưới đây sẽ minh họa rõ hơn ý tưởng này:
Trong ví dụ này, thuộc tính "Color" có 3 giá trị bao gồm ('Red', 'Yellow', 'Green'), tức thuộc kiểu giá trị "category". Sau khi qua bước One-hot encoding, ta sẽ được một bảng dữ liệu mới gồm 3 cột chính là 3 giá trị "category" từ thuộc tính "Color" trước đó. Lưu ý, cột thuộc tính "Color" ban đầu sẽ được bỏ đi.
3.1.3. Chuẩn hóa - Normalization
Trước khi bàn về Chuẩn hóa, ta thử xem qua một bộ dữ liệu về thống kê số tiền chi tiêu dựa trên độ tuổi và thu nhập của họ trong 1 tháng (tính theo Việt Nam đồng):
Tuổi | Thu nhập | Số tiền tiêu |
---|---|---|
21 | 3000000 | 2000000 |
27 | 21500000 | 12500000 |
17 | 1250000 | 500000 |
42 | 42000000 | 15000000 |
33 | 29000000 | 10000000 |
Về lý thuyết, ta hoàn toàn có thể đưa trường thuộc tính "Tuổi" và "Thu nhập" vào mô hình hồi quy tuyến tính từ đó dự đoán được "Số tiền tiêu" mỗi tháng. Tuy nhiên, nếu nhìn kĩ vào giá trị của 2 thuộc tính đầu tiên, ta có thể nhận ra có sự cách biệt rất lớn về mặt miền giá trị giữa chúng (ví dụ "Tuổi" và "Thu nhập" ) mà nếu không giải quyết điều này có thể sẽ làm cho mô hình của chúng ta có kết quả rất tệ.
Việc triệt tiêu sự chênh lệch giá trị giữa các trường thuộc tính có thể cho ta một số lợi điểm sau:
- Mô hình không bị nhạy khi gặp phải các mẫu dữ liệu có giá trị quá lớn.
- Giảm số dữ liệu trùng lặp.
- Các trường thuộc tính có sự ảnh hưởng đến kết quả là như nhau.
- Tăng tốc độ hội tụ trong thuật toán Gradient Descent.
Để dễ hình dung hơn, ta thử xem qua một ví dụ trực quan mô tả cho lợi điểm số 3, với một mô hình tuyến tính đơn biến bất kì khi ta vẽ cặp giá trị trọng số (, ) của mô hình lên đồ thị. Có thể dễ dàng nhận thấy việc chuẩn hóa dữ liệu làm cho các vòng tròn giá trị của cặp trọng số này nhỏ lại cũng như đều ra, giúp cho việc tìm ra vùng cục trực toàn cục trở nên dễ hơn (đường màu đỏ) trong thuật toán Gradient Descent.
Chuẩn hóa (Normalization) là một kĩ thuật chuyển đổi các giá trị của các thuộc tính trong bộ dữ liệu về cùng một miền giá trị. Các miền giá trị được chuyển về phổ biến thường là các đoạn , ...
Có rất nhiều cách để có thể chuẩn hóa một bộ dữ liệu, trong đó phổ biến nhất là 2 công thức dưới đây:
- Min-max scaling:
- Mean normalization:
Như vậy, với bộ dữ liệu thống kê thu nhập trước đó khi ta áp dụng Chuẩn hóa, cụ thể là Mean Normalization thì sẽ được kết quả như sau:
Tuổi | Thu nhập | Số tiền tiêu |
---|---|---|
-0.6610981055933052 | -0.4561077315723939 | -1.0428869991764973 |
-0.6610976956096872 | 0.8080084237029789 | 0.782165249382373 |
-0.6610983789157171 | -0.5756862868011454 | -1.3036087489706216 |
-0.6610966706506424 | 2.2087857849540677 | 1.2167014990392468 |
-0.6610972856260693 | 1.320487946111914 | 0.3476289997254991 |
*Lưu ý, ta không nhất thiết phải chuẩn hóa dữ liệu (biến phụ thuộc).
Như vậy, ta đã đi qua các bước tiền xử lý dữ liệu cơ bản nhất khi làm việc với dữ liệu bảng trong bài toán hồi quy tuyến tính. Bên cạnh các kĩ thuật được nêu trên, vẫn còn rất nhiều các kĩ thuật tiền xử lý khác để giúp cho mô hình của chúng ta trở nên tốt hơn... và tùy vào tình huống, tình trạng của dữ liệu mà ta sẽ áp dụng cho phù hợp nhất.
Dưới đây là phần code tham khảo cho toàn bộ quá trình từ đọc dữ liệu đến tiền xử lý (one-hone encoding, normalization) cho bộ dữ liệu insurance.csv:
import numpy as np # khai báo thư viện numpy
import pandas as pd # khai báo thư viện pandas
from sklearn.model_selection import train_test_split # khai báo hàm train_test_split() từ thư viện sklearn
file_insurance_path = "/path/to/insurance.csv" # khai báo đường dẫn đến file insurance.csv
df = pd.read_csv(file_insurance_path) # đọc file csv
# one-hot encoding
category_names = ['sex', 'smoker', 'region'] # khai báo danh sách tên cách trường thuộc tính thuộc dạng "category"
for category in category_names: # với mỗi trường thuộc tính dạng "category" sẽ:
one_hot = pd.get_dummies(df[category], prefix=category) # chuyển về dạng vector one hot
df = df.drop(category, axis = 1) # xóa cột thuộc tính category gốc
df = pd.concat([one_hot, df], axis=1) # nối vector one-hot (các cột thuộc tính là các giá trị "category" mới)
# chia bộ dữ liệu thành 2 tập huấn luyện (training) và kiểm nghiệm (test)
data = df.to_numpy().astype(np.float64) # chuyển kiểu dữ liệu của các giá trị trong bộ dữ liệu về dạng số thực
X, y = data[:, :-1], data[:, -1] # tách biến độc lập và biến phụ thuộc
y = np.expand_dims(y, axis=1) # đưa chiều không gian của biến phụ thuộc y về cùng chiều không gian với biến độc lập X
bias = np.ones((X.shape[0], 1)) # khai báo biến chứa giá trị bias cho từng điểm dữ liệu
X = np.concatenate((X, bias), axis=1) # nối bias vào biến độc lập X
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=1, shuffle=False # chia X, y thành 2 tập train, test (80% training và 20% test)
# mean normalization
mean_X = np.mean(X_train) # tính mean của tập X_train
std_X = np.std(X_train) # tính std của tập X_train
X_train = (X_train - mean_X) / std_X # mean normalize tập X_train
X_test = (X_test - mean_X) / std_X # mean normalize tập X_test
mean_y = np.mean(y_train) # tính mean của tập y_train
std_y = np.std(y_train) # tính std của tập y_train
y_train = (y_train - mean_y) / std_y # mean normalize tập y_train
y_test = (y_test - mean_y) / std_y # mean normalize tập y_test
3.2. Triển khai các phương pháp giải bài toán hồi quy tuyến tính
Trong phần này, ta sẽ cùng điểm qua các bước triển khai lại các phương pháp đã nêu trong phần lý thuyết sử dụng thư viện NumPy trong Python.
3.2.1. Gradient Descent
import numpy as np # khai báo thư viện numpy
losses_train = []
losses_test = []
theta = np.random.rand(1, 3) # khai báo biến dùng để lưu lại trọng số mô hình
lr = 0.001 # khai báo giá trị learning rate
epochs = 1000 # khai báo số epochs
# khai báo hàm dự đoán sử dụng công thức hồi quy tuyến tính
def predict(X, theta):
return X @ theta.T
# khai báo hàm tính giá trị mất mát giữa giá trị dự đoán và giá trị thực tế sử dụng l2 loss
def loss(y, y_hat):
return (1 / (2 * len(y))) * np.sum((y - y_hat) ** 2)
# khai báo hàm tính gradient descent sử dụng công thức đạo hàm d_L2/d_theta
def gradient(X, y, theta):
return np.sum(-1 / len(y) * X * (y - X @ theta.T))
# khai báo hàm cập nhật trọng số mô hình theta sử dụng kết quả gradient tính được
def update_theta(theta, d_theta, lr=0.01):
return theta - d_theta * lr
# khởi tạo vòng lặp huấn luyện mô hình
for epoch in range(epochs): # với mỗi vòng lặp ta sẽ:
y_hat = predict(X, theta) # tính giá trị dự đoán tập train
l = loss(y, y_hat) # tính giá trị mất mát tập train
losses.append(l) # đưa giá trị mất mát vừa tính được vào danh sách các giá trị mất mát của tập train
d_theta = gradient(X, y, theta) # tính giá trị gradient
theta = update_theta(theta, d_theta, lr) # cập nhật trọng số theta
y_test_pred = predict(X_test, theta) # tính giá trị dự đoán tập test
y_test_loss = loss(y_test, y_test_pred) # tính giá trị mất mát với tập test
losses_test.append(y_test_loss) # đưa giá trị mất mát vừa tính được vào danh sách các giá trị mất mát của tập test
print(f"update epoch {epoch}: train loss = {l}, test loss = {y_test_loss}") # in kết quả trọng số theta vừa cập nhật và giá trị mất mát tại epoch thứ epoch
# in các giá trị về giá trị mất mát, hệ số xác định của mô hình sau khi huấn luyện trên tập dữ liệu huấn luyện và kiểm nghiệm
l_gd_train = loss(y_train, predict(X_train, theta))
l_gd_test = loss(y_test, predict(X_test, theta))
print(f"train min loss = {l_gd_train}")
print(f"train r2 score = {r2_score(y_train, predict(X_train, theta))}")
print(f"test min loss = {l_gd_test}")
print(f"test r2 score = {r2_score(y_test, predict(X_test, theta))}")
Sau khi huấn luyện mô hình với Gradient Descent, ta có thể vẽ đồ thị sự thay đổi của giá trị hàm mất mát sau mỗi epoch:
Việc vẽ biểu đồ giá trị hàm mất mát này khi sử dụng Gradient Descent là rất cần thiết, bởi nó cho ta một góc nhìn về quá trình học của mô hình từ đó đưa ra những nhận xét. Ví dụ như hình trên, có thể thấy mô hình của chúng ta học tương đối tương tốt, không gặp những cản trở nào, thể hiện thông qua sự giảm đều của giá trị hàm mất mát sau mỗi epoch.
3.2.2. Normal Equation
import numpy as np # khai báo thư viện numpy
theta_best = np.linalg.inv(X_train.T.dot(X_train)).dot(X_train.T).dot(y_train) # tính bộ trọng số tối ưu bằng công thức của phương trình chuẩn với bộ dữ liệu huấn luyện
y_hat_train = X_train @ theta_best # tính toán giá trị dự đoán trên tập huấn luyện
l_norm_equation_train = (1 / (2 * len(y_train))) * np.sum((y_train - y_hat_train) ** 2) # tính giá trị mất mát trên tập huấn luyện
y_hat_test = X_test @ theta_best # tính giá trị dự đoán trên tập kiểm nghiệm
l_norm_equation_test = (1 / (2 * len(y_test))) * np.sum((y_test - y_hat_test) ** 2) # tính giá trị mất trên tập kiểm nghiệm
# in các giá trị về giá trị mất mát, hệ số xác định của mô hình sau khi huấn luyện trên tập dữ liệu huấn luyện và kiểm nghiệm
print(f"train loss = {l_norm_equation_train}")
print(f"train r2 score = {r2_score(y_train, y_hat_train)}")
print(f"test loss = {l_norm_equation_test}")
print(f"test r2 score = {r2_score(y_test, y_hat_test)}")
3.2.3. Singular Value Decomposition
import numpy as np # khai báo thư viện numpy
U,S,Vt = np.linalg.svd(X_train, full_matrices=False) # tính ba ma trận U S Vt
X_hat = Vt.T @ np.linalg.inv(np.diag(S)) @ U.T @ y_train # tính bộ trọng số tối ưu dựa trên ba ma trận U S Vt vừa tìm được trên tập dữ liệu huấn luyện
y_hat_train = X_train @ X_hat # tính giá trị dự đoán trên tập huấn luyện
l_svd_train = (1 / (2 * len(y_train))) * np.sum((y_train - y_hat_train) ** 2) # tính giá trị mất mát trên tập huấn luyện
y_hat_test = X_test @ X_hat # tính giá trị dự đoán trên tập kiểm nghiệm
l_svd_test = (1 / (2 * len(y_test))) * np.sum((y_test - y_hat_test) ** 2) # tính giá trị mất mát trên tập kiểm nghiệm
# in các giá trị về giá trị mất mát, hệ số xác định của mô hình sau khi huấn luyện trên tập dữ liệu huấn luyện và kiểm nghiệm
print(f"train loss = {l_svd_train}")
print(f"train r2 score = {r2_score(y_train, y_hat_train)}")
print(f"test loss = {l_svd_test}")
print(f"test r2 score = {r2_score(y_test, y_hat_test)}")
4. Đánh giá
Thông qua việc cài đặt và thực nghiệm trên một bộ dữ liệu cụ thể (benchmark), ta có thể đánh giá được sự hiệu quả (performance) giữa các phương pháp khác nhau từ đó tìm được kĩ thuật tối ưu nhất cho bài toán.
4.1. Độ đo đánh giá
Đầu tiên, để đánh giá được độ hiệu quả giữa các phương pháp khác nhau, ta cần sử dụng một độ đo tính toán về khả năng giải quyết bài toán hồi quy tuyến tính của một phương pháp.
4.1.1. Giá trị mất mát
Đối với các mô hình mô hình máy học, ta hoàn toàn có thể sử dụng giá trị hàm mất mát đã được giới thiệu trong phần Gradient Descent làm thước đo đánh giá giữa các mô hình. Mô hình nào cho kết quả hàm mất mát thấp nhất sẽ được coi là mô hình tốt nhất. Ở trong bài viết này, chúng ta sẽ chọn giá trị hàm mất L2 làm độ đo đánh giá, định nghĩa của hàm mất mát L2 như sau:
=
Trong đó:
- : tổng số mẫu dữ liệu (samples)
- : giá trị dự đoán (prediction) với mẫu dữ liệu
- : giá trị nhãn thực tế (label) của mẫu dữ liệu
4.1.2. Hệ số xác định
Bên cạnh giá trị mất mát, các nhà khoa học cũng thường sử dụng hệ số xác định để đánh giá mô hình hồi quy tuyến tính.
Hệ số xác định (Coefficient of Determination), được ký hiệu là . Trong thống kê, đây là một độ đo dùng để chỉ ra tỉ lệ phương sai của biến phụ thuộc (dependent variable) có thể được dự đoán thông qua biến độc lập. Trong ngữ cảnh của bài toán hồi quy tuyến tính, có thể nói hệ số xác định cho thấy sự khớp (fit) của dữ liệu đối với mô hình hồi quy.
Công thức của hệ số xác định được miêu tả như sau:
Trong đó:
- tổng bình phương của hiệu giữa giá trị thực tế so với giá trị dự đoán. Công thức:
- tổng bình phương của hiệu giữa giá trị thực tế so với trung bình cộng các giá trị thực tế. Công thức:
Để có một cái nhìn trực quan hơn về hệ số xác định cũng như cách tính toán giá trị này, ta hãy xét một bộ dữ liệu nhỏ gồm 5 điểm dữ liệu như sau:
Dựa theo công thức đã bàn luận ở trên, từ đây ta có thể tính được giá trị trung bình của các giá trị thực tế là và cuối cùng tìm được các :
1 | 2 | 4 |
2 | 4 | 0 |
3 | 5 | 1 |
4 | 4 | 0 |
5 | 5 | 1 |
Giả sử ta đã tìm được bộ trọng số cho mô hình và có được một phương trình tuyến tính như sau:
Với hàm tuyến tính này, ta dễ dàng tìm được giá trị dự đoán với mỗi điểm dữ liệu và từ đây tìm được các :
1 | 2 | 2.8 | 0.64 |
2 | 4 | 3.4 | 0.36 |
3 | 5 | 4 | 1 |
4 | 4 | 4.6 | 0.36 |
5 | 5 | 5.2 | 0.04 |
Tổng hợp các dữ kiện đã tính toán được, ta sẽ tìm được và và từ đó tìm được của mô hình tuyến tính với bộ dữ liệu đang xét:
Giá trị đầu ra của nằm trong khoảng từ . Vì vậy, với giá trị của mô hình trên, có thể nói mô hình tuyến tính ta tìm được không thật sự quá tệ (). Các mô hình cho kết quả có thể được coi là một mô hình hồi quy hoạt động tốt.
Trong một số trường hợp, ta có thể bắt gặp các mô hình cho ra kết quả . Điều này xảy ra bởi vì mô hình hồi quy tìm được cho ra một đường tuyến tính quá lệch so với bộ dữ liệu. Hình dưới đây sẽ phần nào minh họa giúp chúng ta điều đó:
4.2. Kết quả thực nghiệm
Trong phần thực nghiệm, ta sẽ sử dụng bộ dữ liệu insurance.csv để đánh giá độ hiệu quả giữa các phương pháp giải bài toán hồi quy tuyến tính thông qua hai độ đo chính đã được giới thiệu ở phần trên gồm điểm và điểm . Bộ dữ liệu đã được tiền xử lý trước khi đưa vào huấn luyện và đánh giá (sử dụng các kỹ thuật đã đề cập ở phần 3.1. Tiền xử lý dữ liệu).
Kết quả đánh giá trên tập dữ liệu huấn luyện (training):
Phương pháp | Điểm | Điểm | Thời gian thực thi |
---|---|---|---|
Gradient Descent (epochs=100, lr=0.01) | -0.142 | 0.571 | 0.0947 |
Normal Equation | 0.759 | 0.126 | 0.0282 |
Singular Value Decomposition | 0.759 | 0.126 | 0.0105 |
Kết quả đánh giá trên tập dữ liệu kiểm nghiệm (test):
Phương pháp | Điểm | Điểm |
---|---|---|
Gradient Descent (epochs=100, lr=0.01) | -0.227 | 0.614 |
Normal Equation | 0.758 | 0.121 |
Singular Value Decomposition | 0.758 | 0.121 |
Dựa trên hai bảng kết quả thực nghiệm trên, ta có thể rút ra một số nhận xét như sau:
- Thời gian tìm ra bộ trọng số tối ưu của Gradient Descent với bộ dữ liệu nhỏ sẽ lâu hơn so với 2 phương pháp còn lại do mất thời gian huấn luyện.
- Kết quả hàm mất mát và hệ số xác định của Normal Equation và Singular Value Decomposition cho ra tốt hơn rất nhiều so với Gradient Descent trong tập dữ liệu huấn luyện và kiểm nghiệm.
- Singular Value Decomposition là phương pháp hiệu quả nhất với bộ dữ liệu insurance.csv cả về độ xác lẫn thời gian xử lý.
Thông qua các nhận định trên, ta phần nào hiểu rõ hơn về việc tại sao các hàm về Hồi quy tuyến tính trong các thư viện lớn như sklearn, scipy... lại sử dụng phương pháp SVD (đã đề cập trong mục 2.4. So sánh các phương pháp).
Mặc dù vậy, phương pháp Gradient Descent dù không thể có được một kết quả đo đạc thật sự hoàn hảo trong bài toán Hồi quy tuyến tính song đây lại là một công cụ rất đắc lực trong các bài toán phức tạp hơn của Máy học đặc biệt là Học sâu, vì các bài toán ấy sẽ không có một phương trình toán học cụ thể nào để giải trực tiếp ra bộ trọng số tối ưu của mô hình như bài Hồi quy tuyến tình này.
5. Bài tập
Một nghiên cứu của WHO về yếu tố ảnh hưởng đến tuổi thọ như các yếu tố về tiêm chủng, yếu tố xã hội,... được thống kê trong bộ dữ liệu Life Expectancy.
Để giúp bạn đọc có thể nắm rõ hơn các phần kiến thức trong bài viết, chúng mình đề xuất các bạn hãy thực hành một bài tập về dự đoán tuổi thọ của một người dựa trên các thông tin cá nhân của họ với một số yêu cầu như sau:
Yêu cầu:
- Sử dụng phương pháp hồi quy tuyến tính để xây dựng mô hình dự đoán tuổi thọ của một người dựa trên các các kê trong dữ liệu trên. Lưu ý: cài đặt chương trình Hồi quy tuyến tính bằng thư viện PyTorch.
- Tìm hiểu và sử dụng thêm một số phương pháp tiền xử lý dữ liệu khác như: Missing Data Imputation, Principal Component Analysis,... để tối ưu mô hình cũng như thời gian tính toán.
Bài giải
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import mean_squared_error, r2_score
class linearRegression(torch.nn.Module):
def __init__(self, inputSize, outputSize):
super(linearRegression, self).__init__()
self.linear = torch.nn.Linear(inputSize, outputSize)
def forward(self, x):
out = self.linear(x)
return out
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
def load_data(path):
df = pd.read_csv(path)
#Thay các khoảng trắng trong tên cột thành "_"
orig_cols = list(df.columns)
new_cols = []
for col in orig_cols:
new_cols.append(col.strip().replace(' ', ' ').replace(' ', '_').replace('-', '_').lower())
df.columns = new_cols
return df
#Xóa khoảng trắng
def remove_whitespace(x):
try:
x = "".join(x.split())
except:
pass
return x
path = "/content/Life Expectancy Data.csv"
df = load_data(path)
df.country = df.country.apply(remove_whitespace)
#Thay bằng giá trị trung bình cho các vị trí null
df.fillna(df.mean(), inplace=True)
#Onehot cho cột status
dmap = {'Developed':1,'Developing':0}
df['status'] = df['status'].map(dmap)
y = df['life_expectancy'].values #Giá trị dự đoán
X = df.drop(['country','life_expectancy'],axis=1).values #Xóa cột country và cột predict tạo dữ liệu train.
X_train, X_test, y_train, y_test = train_test_split(X, y, 0.2) #Chia tập train và test
mean_X = np.mean(X_train) # tính mean của tập X_train
std_X = np.std(X_train) # tính std của tập X_train
X_train = (X_train - mean_X) / std_X # mean normalize tập X_train
X_test = (X_test - mean_X) / std_X # mean normalize tập X_test
mean_y = np.mean(y_train) # tính mean của tập y_train
std_y = np.std(y_train) # tính std của tập y_train
y_train = (y_train - mean_y) / std_y # mean normalize tập y_train
y_test = (y_test - mean_y) / std_y # mean normalize tập y_test
#Chuyển từ numpy_array sang Variable
X_train = Variable( torch.Tensor(X_train))
X_test = Variable(torch.Tensor(X_test))
y_train = Variable(torch.Tensor(y_train))
y_test = Variable(torch.Tensor(y_test))
def model_train(X_train, X_test, y_train, y_test):
inputDim = X_train.shape[1]
outputDim = 1
model = linearRegression(inputDim, outputDim).to(device)
learningRate = 0.07
mse = torch.nn.MSELoss() #Hàm mất mát
optimizer = torch.optim.SGD(model.parameters(), lr=learningRate) #Gradient Descent
epochs = 2000 #lặp lại 2000 lần để tìm theta
for epoch in range(epochs):
outputs = model(X_train)
train_loss = mse(outputs, y_train)
optimizer.zero_grad()
train_loss.backward()
optimizer.step()
with torch.no_grad():
predicted = model(X_test)
test_loss = mse(predicted, y_test)
if(epoch % 50 == 0):
print('epoch {}, train_loss {}, test_loss {}'.format(epoch, train_loss.data, test_loss.data))
return model
model_train(X_train, X_test, y_train, y_test)
6. Kết luận
Bài toán hồi quy tuyến tính là một bài toán cơ bản nhất trong lĩnh vực Máy học cũng như trong một số lĩnh vực khác song lại đóng vai trò là một nền tảng vững chắc cho người mới bắt đầu học Máy học có thể hiểu được những khái niệm quan trọng, mang tính xuyên suốt khi tiếp xúc với các bài toán Máy học.
Trong bài viết này, ta đã cùng nhau bàn luận về khái niệm Hồi quy tuyến tính, một số phương pháp giải bài toán Hồi quy tuyến tính bao gồm Gradient Descent, Normal Equation và cuối cùng là Singular Value Decomposition. Đặc biệt, ta đã cùng tìm hiểu qua về quy trình cơ bản trong việc giải quyết một bài toán Máy học, từ các bước đọc và tiền xử lý dữ liệu cho đến việc huấn luyện mô hình. Cuối cùng, sử dụng các kết quả đạt được từ chương trình đã cài đặt để đánh giá các phương pháp giải Hồi quy tuyến tính ở phần lý thuyết bằng các độ đo đánh giá phổ biến như giá trị mất mát , hệ số xác định .
Mặc dù vẫn còn một số những chủ đề, khái niệm quan trọng khác xoay quanh bài toán Hồi quy tuyến tính này mà trong bài viết chưa thể giải bày hết được. Tuy nhiên, hy vọng bài viết này sẽ phần nào giúp các bạn có một cái nhìn chi tiết, rõ ràng hơn về bài toán Hồi quy tuyến tính cũng như phương pháp giải một bài toán Máy học cơ bản.
Cảm ơn các bạn đọc đã quan tâm theo dõi tới cuối bài viết này, hẹn gặp các bạn trong các số bài viết tiếp theo.
Tài liệu tham khảo
- Bộ dữ liệu insurance.csv: https://raw.githubusercontent.com/stedy/Machine-Learning-with-R-datasets/master/insurance.csv
- Bộ dữ liệu Life Expectancy (WHO): https://www.kaggle.com/datasets/kumarajarshi/life-expectancy-who
- Toán cho Machine Learning - machinelearningcoban: https://machinelearningcoban.com/math/
- Normal Equation - ML Wiki: http://mlwiki.org/index.php/Normal_Equation
- Singular Values Decomposition - viblo: https://viblo.asia/p/handbook-singular-values-decomposition-va-mot-so-ung-dung-yMnKMOoml7P
- Optimization - Pauli Miettinen: https://www.mpi-inf.mpg.de/fileadmin/inf/d5/teaching/ss17_dmm/lectures/2017-05-22-optimization.pdf
Tác giả:
- Dương Đình Thắng
- Dương Nguyễn Thuận