- 1. Utilities (Các tiện ích)
- 2. Class Models
- 3. Class Data
- 4. Class Training
- [[#41-init|4.1. init]]
- 4.2. prepare_data(data)
- 4.3. prepare_model(model)
- 4.4. fit(model, data)
- 4.5. fit_epoch() (chưa triển khai)
- 5. Tóm tắt
- 6. Bài tập
Bài viết này tham khảo từ cuốn Dive into Deep Learning Chương 3 phần 2. Việc triển khai một mô hình học sâu thường liên quan đến nhiều thành phần như: dữ liệu, mô hình, hàm mất mát và thuật toán tối ưu. Mặc dù có thể bắt đầu với một ví dụ đơn giản, cấu trúc tổng thể của quá trình huấn luyện vẫn lặp lại ở hầu hết các mô hình, từ đơn giản đến phức tạp. Để đảm bảo cấu trúc mã rõ ràng, dễ mở rộng và dễ tái sử dụng, việc thiết kế theo hướng đối tượng là một chiến lược hiệu quả. Cách tiếp cận này xem mỗi phần trong pipeline học sâu như một đối tượng độc lập, có thể đóng gói dữ liệu và hành vi của chính nó. Thay vì viết các hàm rời rạc, chúng ta định nghĩa các lớp (classes) đại diện cho từng thành phần, từ đó mô tả rõ ràng cách các phần này tương tác với nhau. Cách tổ chức như vậy không chỉ giúp mã dễ bảo trì mà còn hỗ trợ việc chia sẻ hoặc kết hợp lại các thành phần trong các dự án khác nhau. Khi thiết kế tốt, bạn chỉ cần thay đổi một phần - như bộ tối ưu hoặc bộ dữ liệu - mà không ảnh hưởng đến phần còn lại của mô hình. Dựa trên cảm hứng từ các thư viện mã nguồn mở hiện đại, một cấu trúc tổng quát có thể bao gồm ba lớp chính:
Module: chứa mô hình, hàm mất mát và các phương pháp tối ưu;DataModule: cung cấp các bộ nạp dữ liệu (data loaders) phục vụ cho quá trình huấn luyện và kiểm tra;Trainer: kết hợp cả hai lớp trên và cho phép ta huấn luyện mô hình trên nhiều nền tảng phần cứng khác nhau. Trong hầu hết các ứng dụng, hai lớp đầu tiên là đủ để xây dựng và kiểm thử mô hình. LớpTrainerthường trở nên quan trọng khi cần hỗ trợ nâng cao như huấn luyện song song, tối ưu hiệu suất trên GPU/CPU, hoặc sử dụng kỹ thuật tối ưu hóa đặc biệt.
Import các thư viện cần thiết để cài đặt các class trong bài:
import time
import numpy as np
import torch
from torch import nn
from d2l import torch as d2l1. Utilities (Các tiện ích)
Khi viết chương trình hướng đối tượng trong Jupyter Notebook, chúng ta thường sẽ phải dùng những hàm hỗ trợ này để thiết kế tốt hơn.
1.1. Hàm add_to_class
Chúng ta gặp một vấn đề OOP là:
- Các định nghĩa class thường rất dài, nhưng notebook lại yêu cầu chia nhỏ code kèm giải thích để dễ đọc.
- Điều này không hợp với cách lập trình thông thường như trong các thư viện Python (nơi mà mọi thứ nằm trong cùng một khối code lớn). → Giải pháp: Ta cần một hàm tiện ích giúp thêm một phương thức (method) vào lớp đã khai báo từ trước, thậm chí ngay cả khi đối tượng đã được tạo ra.
def add_to_class(Class): #@save
"""Đăng ký một hàm như là phương thức của một class đã tạo."""
def wrapper(obj):
setattr(Class, obj.__name__, obj)
return wrapper- Hàm này nhận vào một lớp
Class - Bên trong, nó trả về một
wrapper, dùng để gắn (attach) bất kỳ hàm nào vàoClass - Dùng
setattrđể thêm hàm vào class như một phương thức thực thụ Cách sử dụng**@save**Bước 1: Khai báo class
class A:
def __init__(self):
self.b = 1
a = A()→ Ta đã có class A và tạo ra một instance a.
Bước 2: Định nghĩa method **do**, nhưng bên ngoài class
@add_to_class(A)
def do(self):
print('Class attribute "b" is', self.b)→ Bây giờ, do đã trở thành một method của class A, dù ta định nghĩa nó bên ngoài!
Kiểm tra hoạt động
a.do()
# In ra: Class attribute "b" is 1✅ Nó hoạt động như thể do() đã được viết bên trong class A ngay từ đầu.
1.2. Hàm hỗ trợ tự động lưu siêu tham số (hyperparameters)
Lớp tiện ích thứ hai là một class cơ sở (base class) có nhiệm vụ lưu trữ tất cả các đối số (argument) truyền vào phương thức __init__ dưới dạng thuộc tính của class.
→ Điều này giúp ta mở rộng hàm khởi tạo (constructor) một cách gọn gàng và linh hoạt, không cần viết thêm code lặp lại.
class HyperParameters: #@save
"""Lớp cơ sở cho việc lưu siêu tham số."""
def save_hyperparameters(self, ignore=[]):
"""Save function arguments into class attributes."""
frame = inspect.currentframe().f_back
_, _, _, local_vars = inspect.getargvalues(frame)
self.hparams = {k:v for k, v in local_vars.items()
if k not in set(ignore+['self']) and not k.startswith('_')}
for k, v in self.hparams.items():
setattr(self, k, v)Bài giải thích chi tiết ở đây: [Todo]
Cách sử dụng **HyperParameters**
Giả sử ta đã có phiên bản đầy đủ của HyperParameters được cài sẵn trong thư viện d2l.
Ta có thể viết như sau:
# Giả sử HyperParameters đã được định nghĩa đầy đủ trong d2l
class B(d2l.HyperParameters):
def __init__(self, a, b, c):
self.save_hyperparameters(ignore=['c'])
print('self.a =', self.a, 'self.b =', self.b)
print('There is no self.c =', not hasattr(self, 'c'))
b = B(a=1, b=2, c=3)Kết quả in ra:
self.a = 1 self.b = 2
There is no self.c = TrueDiễn giải chi tiết:
-
save_hyperparameters()sẽ tự động gán:self.a = a self.b = b -
Nhưng vì
cnằm trong danh sáchignore, nên**self.c**sẽ không được tạo ra. -
Điều này cực kỳ tiện khi class có nhiều tham số, giúp giảm lặp code và dễ mở rộng mô hình. So sánh khi không có tiện ích này: Nếu không dùng
save_hyperparameters(), bạn phải viết:
self.a = a
self.b = b
self.c = cNếu có 10–20 tham số thì việc này sẽ rất dài dòng và dễ nhầm lẫn.
1.3. Trực quan hóa tiến trình huấn luyện
Tiện ích thứ ba cho phép ta vẽ tiến trình của thí nghiệm (ví dụ: quá trình huấn luyện mô hình) theo thời gian thực, tức là trong lúc mô hình đang chạy.
Vì muốn tránh nhầm lẫn với công cụ chuyên sâu hơn như TensorBoard (vốn mạnh mẽ nhưng cũng phức tạp), ta đặt tên tiện ích này là ProgressBoard
@d2l.add_to_class(d2l.ProgressBoard) #@save
def draw(self, x, y, label, every_n=1):
Point = collections.namedtuple('Point', ['x', 'y'])
if not hasattr(self, 'raw_points'):
self.raw_points = collections.OrderedDict()
self.data = collections.OrderedDict()
if label not in self.raw_points:
self.raw_points[label] = []
self.data[label] = []
points = self.raw_points[label]
line = self.data[label]
points.append(Point(x, y))
if len(points) != every_n:
return
mean = lambda x: sum(x) / len(x)
line.append(Point(mean([p.x for p in points]),
mean([p.y for p in points])))
points.clear()
if not self.display:
return
d2l.use_svg_display()
if self.fig is None:
self.fig = d2l.plt.figure(figsize=self.figsize)
plt_lines, labels = [], []
for (k, v), ls, color in zip(self.data.items(), self.ls, self.colors):
plt_lines.append(d2l.plt.plot([p.x for p in v], [p.y for p in v],
linestyle=ls, color=color)[0])
labels.append(k)
axes = self.axes if self.axes else d2l.plt.gca()
if self.xlim: axes.set_xlim(self.xlim)
if self.ylim: axes.set_ylim(self.ylim)
if not self.xlabel: self.xlabel = self.x
axes.set_xlabel(self.xlabel)
axes.set_ylabel(self.ylabel)
axes.set_xscale(self.xscale)
axes.set_yscale(self.yscale)
axes.legend(plt_lines, labels)
display.display(self.fig)
display.clear_output(wait=True)1. Gắn phương thức vào class bằng **@d2l.add_to_class**
@d2l.add_to_class(d2l.ProgressBoard)
def draw(self, x, y, label, every_n=1):@d2l.add_to_class(...): dùng để thêm method**draw**vào class**ProgressBoard**, kể cả khi nó được định nghĩa ở nơi khác.x,y: tọa độ của điểm mớilabel: tên đường (ví dụ: “loss”, “accuracy”)every_n: vẽ một điểm sau mỗinlần (để làm mượt) 2. Khai báo cấu trúc dữ liệu để lưu điểm
Point = collections.namedtuple('Point', ['x', 'y'])- Tạo kiểu dữ liệu giống
tuplenhưng có tên:Point(x, y)3. Khởi tạo nếu chưa có dữ liệu
if not hasattr(self, 'raw_points'):
self.raw_points = collections.OrderedDict()
self.data = collections.OrderedDict()raw_points: danh sách các điểm chưa làm mượt (thô)data: danh sách các điểm đã được xử lý làm mượt để vẽ 4. Thêm điểm mới vào dữ liệu tương ứng label
if label not in self.raw_points:
self.raw_points[label] = []
self.data[label] = []- Nếu đây là label mới (ví dụ lần đầu vẽ “loss”), thì tạo mảng trống để lưu
points = self.raw_points[label]
line = self.data[label]
points.append(Point(x, y))5. Chỉ xử lý vẽ nếu đủ số lượng every_n
if len(points) != every_n:
return- Nếu chưa đủ
every_nđiểm thì thoát (để giảm tần suất vẽ) 6. Làm mượt: lấy trung bình x và y
mean = lambda x: sum(x) / len(x)
line.append(Point(mean([p.x for p in points]),
mean([p.y for p in points])))
points.clear()- Tính trung bình của các điểm
xvàyrồi thêm vàoline - Xóa danh sách
pointsđể bắt đầu nhóm tiếp theo 7. Không vẽ nếu**display=False**
if not self.display:
return8. Chuẩn bị vẽ hình
d2l.use_svg_display()
if self.fig is None:
self.fig = d2l.plt.figure(figsize=self.figsize)- Dùng SVG cho hình đẹp hơn
- Nếu chưa có figure (
self.fig), tạo mới 9. Vẽ các đường với nhãn, kiểu đường và màu
plt_lines, labels = [], []
for (k, v), ls, color in zip(self.data.items(), self.ls, self.colors):
plt_lines.append(d2l.plt.plot([p.x for p in v], [p.y for p in v],
linestyle=ls, color=color)[0])
labels.append(k)- Vẽ từng
labelvới kiểu đườnglsvà màucolor10. Cài đặt trục, nhãn, tỉ lệ
axes = self.axes if self.axes else d2l.plt.gca()
if self.xlim: axes.set_xlim(self.xlim)
if self.ylim: axes.set_ylim(self.ylim)
if not self.xlabel: self.xlabel = self.x
axes.set_xlabel(self.xlabel)
axes.set_ylabel(self.ylabel)
axes.set_xscale(self.xscale)
axes.set_yscale(self.yscale)
axes.legend(plt_lines, labels)- Tùy chọn giới hạn trục
- Gắn nhãn trục x, y
- Đặt kiểu tỉ lệ (thường là
linear, có thể làlog) - Hiển thị chú thích (legend) 11. Hiển thị biểu đồ động
display.display(self.fig)
display.clear_output(wait=True)- Hiển thị hình mới
- Xóa hình cũ → tạo hiệu ứng “animation”

2. Class Models
Class Module là lớp nền (base class) cho mọi mô hình mà chúng ta sẽ xây dựng trong cuốn sách này. Ít nhất nó cần có 3 phương thức:
__init__: khởi tạo mô hình và lưu các tham số có thể học được (learnable parameters)training_step: xử lý một batch dữ liệu huấn luyện, trả về giá trị mất mát (loss)configure_optimizers: trả về bộ tối ưu (optimizers) sẽ dùng để cập nhật tham số Ngoài ra còn có:
validation_step: đánh giá mô hình trên tập validation (không bắt buộc)forward: định nghĩa quá trình lan truyền đầu vào qua mạng (forward pass) Đọc lại bài: Giới thiệu thư viện PyTorch Cài đặt class**Module**
class Module(nn.Module, d2l.HyperParameters): #@save- Kế thừa từ
nn.Module(PyTorch) vàHyperParameters(lưu siêu tham số) - Cho phép xử lý mô hình học sâu thuận tiện
2.1. Khởi tạo Class Model
def __init__(self, plot_train_per_epoch=2, plot_valid_per_epoch=1):
super().__init__()
self.save_hyperparameters()
self.board = ProgressBoard()plot_train_per_epoch: số lần vẽ biểu đồ mỗi epoch huấn luyệnboard: bảng vẽ tiến trình (như ta định nghĩa trước đó)
2.2. Hàm loss: cần ghi đè (override)
def loss(self, y_hat, y):
raise NotImplementedError- Phải được viết cụ thể trong các lớp con (ví dụ dùng MSE, CrossEntropy,…)
2.3. Hàm forward: chạy đầu vào qua mạng
def forward(self, X):
assert hasattr(self, 'net'), 'Neural network is defined'
return self.net(X)self.netlà mạng neural đã được gán trong lớp con- Gọi
a(X)sẽ chạya.forward(X)nhờ PyTorch (__call__→forward)
2.4. Vẽ biểu đồ tiến trình
def plot(self, key, value, train):
"""Plot a point in animation."""
assert hasattr(self, 'trainer'), 'Trainer is not inited'
self.board.xlabel = 'epoch'
if train:
x = self.trainer.train_batch_idx / \
self.trainer.num_train_batches
n = self.trainer.num_train_batches / \
self.plot_train_per_epoch
else:
x = self.trainer.epoch + 1
n = self.trainer.num_val_batches / \
self.plot_valid_per_epoch
self.board.draw(x, value.to(d2l.cpu()).detach().numpy(),
('train_' if train else 'val_') + key,
every_n=int(n))- Vẽ biểu đồ loss cho từng batch hoặc epoch
key: tên chỉ số (vd:'loss')value: giá trị losstrain: True nếu là train, False nếu là validation
2.5. Bước huấn luyện từng batch
def training_step(self, batch):
l = self.loss(self(*batch[:-1]), batch[-1])
self.plot('loss', l, train=True)
return l- Lấy đầu vào từ
batch(trừ phần tử cuối là label) - Dự đoán → tính loss → vẽ biểu đồ → trả về loss
2.6. Bước kiểm tra (validation)
def validation_step(self, batch):
l = self.loss(self(*batch[:-1]), batch[-1])
self.plot('loss', l, train=False)2.7. Cấu hình thuật toán tối ưu
def configure_optimizers(self):
raise NotImplementedError- Phải được ghi đè trong lớp con (ví dụ:
torch.optim.SGD,Adam,…)
2.8. Ghi chú về nn.Module trong PyTorch
- Đây là lớp nền của tất cả các mô hình PyTorch.
- Nếu bạn định nghĩa hàm
forward(), bạn có thể gọi bằng cú phápmodel(X)vì__call__()đã được PyTorch xử lý sẵn để gọiforward(). Link: [TODO 6.1]
3. Class Data
Class DataModule là class nền (base class) cho mọi bộ xử lý dữ liệu trong dự án.
3.1. Mục đích Class DataModule
- Giúp quản lý việc tải dữ liệu, tiền xử lý, và cung cấp các batch dữ liệu cho mô hình huấn luyện.
- Tách phần dữ liệu ra khỏi phần mô hình → giúp mã gọn gàng và tái sử dụng.
3.2. Cách hoạt động
Code cụ thể:
class DataModule(d2l.HyperParameters): #@save
"""Lớp cơ sở cho xử lý dữ liệu."""
def __init__(self, root='../data', num_workers=4):
self.save_hyperparameters()
def get_dataloader(self, train):
raise NotImplementedError # Phải được ghi đè
def train_dataloader(self):
return self.get_dataloader(train=True)
def val_dataloader(self):
return self.get_dataloader(train=False)__init__():- Nơi chuẩn bị dữ liệu: tải xuống, giải nén, xử lý,…
- Lưu các siêu tham số như
root(đường dẫn lưu dữ liệu),num_workers(số luồng nạp dữ liệu song song)
get_dataloader(train=True/False):- Phải được ghi đè (override) trong lớp con.
- Trả về data loader — tức là một generator trả về các batch dữ liệu từng lần một.
train_dataloader():- Gọi
get_dataloader(train=True)→ trả về bộ dữ liệu huấn luyện
- Gọi
val_dataloader():- Gọi
get_dataloader(train=False)→ trả về bộ dữ liệu kiểm tra (validation)
- Gọi
3.3. Hình dung luồng dữ liệu trong mô hình
train_dataloader()→ lấy từngbatch- Gửi batch đó vào
Module.training_step(batch) - Tính
loss→ cập nhật trọng số - Nếu là validation →
val_dataloader()→validation_step(batch)
4. Class Training
Class Trainer dùng để huấn luyện các tham số học được (learnable parameters) trong lớp Module — sử dụng dữ liệu được cung cấp bởi **DataModule**.
Mục tiêu chính
Phương thức trung tâm của lớp này là:
fit(model, data)model: một instance (đối tượng) của lớpModuledata: một instance củaDataModulePhương thứcfit()sẽ lặp qua toàn bộ dữ liệu nhiều lần (theo sốmax_epochs) để huấn luyện mô hình.
❗ Việc triển khai chi tiết fit_epoch() được hoãn lại đến các chương sau (nơi cụ thể hóa cách lặp qua từng batch và cập nhật trọng số). Code:
class Trainer(d2l.HyperParameters): #@save
"""Lớp nền để huấn luyện mô hình với dữ liệu."""
def __init__(self, max_epochs, num_gpus=0, gradient_clip_val=0):
self.save_hyperparameters()
assert num_gpus == 0, 'No GPU support yet'
def prepare_data(self, data):
self.train_dataloader = data.train_dataloader()
self.val_dataloader = data.val_dataloader()
self.num_train_batches = len(self.train_dataloader)
self.num_val_batches = (len(self.val_dataloader)
if self.val_dataloader is not None else 0)
def prepare_model(self, model):
model.trainer = self
model.board.xlim = [0, self.max_epochs]
self.model = model
def fit(self, model, data):
self.prepare_data(data)
self.prepare_model(model)
self.optim = model.configure_optimizers()
self.epoch = 0
self.train_batch_idx = 0
self.val_batch_idx = 0
for self.epoch in range(self.max_epochs):
self.fit_epoch()
def fit_epoch(self):
raise NotImplementedError- Kế thừa
HyperParametersđể lưu các cấu hình (epoch, GPU,…)
4.1. __init__
def __init__(self, max_epochs, num_gpus=0, gradient_clip_val=0):
self.save_hyperparameters()
assert num_gpus == 0, 'No GPU support yet'max_epochs: số lần huấn luyện toàn bộ tập dữ liệunum_gpus: số lượng GPU (chưa hỗ trợ ở đây)gradient_clip_val: dùng để giới hạn gradient (chống bùng nổ gradient)
4.2. prepare_data(data)
def prepare_data(self, data):
self.train_dataloader = data.train_dataloader()
self.val_dataloader = data.val_dataloader()
self.num_train_batches = len(self.train_dataloader)
self.num_val_batches = (len(self.val_dataloader)
if self.val_dataloader is not None else 0)- Gọi phương thức của
DataModuleđể lấyDataLoader - Lưu số batch train & val để phục vụ cho việc tính toán/hiển thị
4.3. prepare_model(model)
def prepare_model(self, model):
model.trainer = self
model.board.xlim = [0, self.max_epochs]
self.model = model- Gán trainer vào mô hình (giúp model gọi lại
plot()biết mình đang ở epoch nào) - Gán giới hạn trục x (0 → số epoch)
- Lưu mô hình vào
self.model
4.4. fit(model, data)
def fit(self, model, data):
self.prepare_data(data)
self.prepare_model(model)
self.optim = model.configure_optimizers()
self.epoch = 0
self.train_batch_idx = 0
self.val_batch_idx = 0
for self.epoch in range(self.max_epochs):
self.fit_epoch()- Chuẩn bị dữ liệu & mô hình
- Gọi
model.configure_optimizers()để khởi tạo optimizer - Lặp qua số epoch → gọi
fit_epoch()ở mỗi vòng
4.5. fit_epoch() (chưa triển khai)
def fit_epoch(self):
raise NotImplementedError- Chức năng này sẽ được viết sau, để định nghĩa chi tiết cách huấn luyện từng epoch (duyệt từng batch, tính loss, backward,…) Đọc bài:
5. Tóm tắt
Để làm nổi bật thiết kế hướng đối tượng (OOP) trong việc triển khai các mô hình học sâu sau này, các lớp mà ta đã giới thiệu ở trên chủ yếu minh họa cách các đối tượng lưu dữ liệu và tương tác với nhau.
Ý tưởng cốt lõi
- Các lớp như
Module,DataModule,Trainer,ProgressBoard,… chỉ là khung nền ban đầu. - Trong phần còn lại của cuốn sách, ta sẽ tiếp tục mở rộng chúng (ví dụ: dùng
@add_to_classđể thêm phương thức sau khi đã định nghĩa class).
Lưu trữ trong thư viện D2L
Các lớp sau khi được triển khai đầy đủ đều sẽ được lưu trong thư viện **D2L** (Dive into Deep Learning):
- Đây là bộ công cụ nhẹ, có cấu trúc rõ ràng
- Giúp việc xây dựng mô hình học sâu trở nên dễ dàng và có thể tái sử dụng giữa các dự án
♻️ Mức độ modular – tái sử dụng cao
Bạn có thể thay thế từng phần riêng biệt mà không cần sửa cả hệ thống, ví dụ:
| Muốn thay đổi | Việc cần làm |
| Tối ưu | Thay optimizer |
| Mô hình | Thay Module |
| Dữ liệu | Thay DataModule |
| 👉 Thiết kế này cực kỳ có lợi khi: |
- Viết code ngắn gọn
- Dễ kiểm thử và mở rộng
- Dễ chia sẻ & tái sử dụng trong dự án cá nhân
6. Bài tập
1. Tìm phiên bản triển khai đầy đủ của các lớp trên trong thư viện D2L
Các lớp như Module, DataModule, Trainer, ProgressBoard,… đều đã được triển khai hoàn chỉnh trong thư viện D2L (Dive into Deep Learning). Yêu cầu: Hãy tìm các đoạn mã gốc đó trong thư viện (thường là trong repo GitHub hoặc file
.pyđi kèm). Lời khuyên: Sau khi bạn đã quen với khái niệm mô hình hóa trong deep learning, nên đọc kỹ cách các class đó được cài đặt. Điều này sẽ giúp bạn hiểu sâu hơn về cách tổ chức code chuyên nghiệp và dễ mở rộng.
2. Gỡ bỏ câu lệnh **save_hyperparameters()** khỏi class **B**
Giả sử class B được định nghĩa như sau:
class B(d2l.HyperParameters):
def __init__(self, a, b, c):
self.save_hyperparameters(ignore=['c'])
print('self.a =', self.a, 'self.b =', self.b)Yêu cầu:
-
Hãy xóa dòng self.save_hyperparameters(…)
-
Sau đó kiểm tra: bạn có còn in được
self.avàself.bkhông? Gợi ý: -
Nếu bạn không còn dùng
**save_hyperparameters**, thìself.a,self.bsẽ không được tự động gán. -
Khi chạy dòng
print('self.a =', self.a), Python sẽ báo lỗi:AttributeError: 'B' object has no attribute 'a' -
Giải:
Không – bạn sẽ không in được self.a và self.b nếu không có self.save_hyperparameters()
3. (Nâng cao) Giải thích tại sao lại như vậy
Nếu bạn đã xem toàn bộ phần cài đặt của HyperParameters, hãy giải thích tại sao save_hyperparameters lại cần thiết?
-
Gợi ý giải thích
Trong cài đặt đầy đủ của
HyperParameters.save_hyperparameters()(bạn có thể tra trong D2L), hàm này dùng:frame = inspect.currentframe().f_back self.__dict__.update({k: v for k, v in frame.f_locals.items() if k not in ignore and not k.startswith('_')})→ Nghĩa là: nó lấy tất cả tham số trong
**__init__**(trừ các biến**ignore**) và tự gán vào**self**.Nếu không gọi
save_hyperparameters, bạn phải tự viết:self.a = a self.b = b ....