Gradient Descent, SGD, Adam: từ đạo hàm cấp 3 đến Muon optimizer 2026

Giải thích Gradient Descent, SGD, Adam optimizer từ đạo hàm cấp 3, và vì sao Muon (Kimi 2025) sẽ là bước tiếp theo. Toán dễ hiểu, không code.

Gradient Descent, SGD, Adam: từ đạo hàm cấp 3 đến Muon optimizer 2026
Featured

Khi cả thế giới đang quay cuồng với LLM và AI Agent. Người thiệt nhất có lẽ là các bạn đang muốn học AI từ gốc, vì thế giới AI không chỉ xoay quanh LLM, có rất nhiều bài toán khác: phát hiện bất thường, phân tích tài chính, các bài xử lý ảnh (y tế, sản xuất...), Explainable AI và rất nhiều bài toán nữa. Có thể đây sẽ là một chuỗi bài (hoặc không, hãy cho mình biết bạn có quan tâm cái này không). Và đây sẽ là bài Optimization fundamental.

Bài toán: tìm điểm thấp nhất của một thung lũng

Mọi mô hình machine learning, từ linear regression cho tới GPT (và các kiến trúc transformer như Gemma 3), đều quy về một bài toán: tìm bộ tham số $\theta$ sao cho hàm loss $L(\theta)$ nhỏ nhất.

Hàm loss đo "model sai bao nhiêu". $\theta$ là các trọng số (weights) bên trong model. Nếu vẽ $L$ như một mặt cong trong không gian, thì việc training một model là đi tìm điểm thấp nhất của mặt cong đó, như thả một quả bóng vào thung lũng và xem nó dừng ở đâu.

Mình muốn dừng một chút ở chữ "loss" trước khi đi tiếp, vì đây là chỗ rất dễ bị lướt qua.

Hàm loss thực ra là một cách bạn dịch "mục tiêu" thành "một con số". Máy tính không hiểu khái niệm "model dự đoán đúng". Nó chỉ hiểu số. Nên việc đầu tiên trong mọi bài toán ML là: nghĩ ra một công thức sao cho con số đó càng nhỏ thì model càng tốt. Đó là toàn bộ ý nghĩa của loss.

Ví dụ cụ thể: bạn muốn model dự đoán giá nhà. Có một căn nhà giá thật là 5 tỷ, model đoán 4.5 tỷ. Sai 0.5 tỷ. Bạn có thể nói "loss của lần dự đoán này là 0.5". Tổng lại trên cả tập dữ liệu, lấy trung bình, được một con số duy nhất. Nhỏ là tốt. Lớn là tệ. Đơn giản vậy thôi.

Loss với ví dụ giá nhà

Trong hình trên, các chấm đen là dữ liệu thật (giá nhà theo diện tích), đường thẳng là dự đoán của model. Mỗi đoạn dọc chấm chấm là một sai số. Loss của model chính là trung bình bình phương của các đoạn đó. Việc training model là việc xoay/dịch đường thẳng để tổng các đoạn dọc đó nhỏ nhất.

Nhưng có hai ràng buộc bạn cần biết khi thiết kế một hàm loss:

  1. Càng nhỏ phải càng tốt. Nghe hiển nhiên nhưng dễ nhầm. Ví dụ "tỉ lệ đoán đúng" thì càng lớn mới càng tốt, nên không dùng được làm loss. Phải đảo lại thành "tỉ lệ đoán sai" mới đúng chiều.
  2. Phải lấy được đạo hàm. Lát nữa mình sẽ thấy thuật toán cần đạo hàm của loss để biết "đi đâu". Nên loss phải là hàm trơn, không có bậc thang đột ngột. Đây là lý do tại sao trong ví dụ giá nhà, người ta không dùng "đoán đúng/sai" (vì đó là hàm bậc thang, đạo hàm bằng 0 ở mọi nơi, vô dụng), mà dùng bình phương sai số $(5 - 4.5)^2 = 0.25$. Bình phương vừa luôn dương, vừa trơn, vừa phạt nặng các sai lệch lớn.

Vài hàm loss phổ biến bạn sẽ gặp đi gặp lại trong sách vở:

  • MSE (Mean Squared Error): trung bình bình phương sai số. Dùng cho bài toán dự đoán số (giá nhà, nhiệt độ, doanh thu).
  • Cross-Entropy: dùng cho bài toán phân loại (ảnh này là chó hay mèo). Đo khoảng cách giữa "xác suất model nghĩ" và "đáp án thật".
  • MAE (Mean Absolute Error): trung bình giá trị tuyệt đối sai số. Giống MSE nhưng không bình phương, ít nhạy với outlier hơn.
  • Hinge loss: dùng trong Support Vector Machine, phạt các mẫu nằm sai phía hoặc quá gần đường phân tách.

Tạm gác ở đây. Quan trọng nhất bạn cần nhớ: loss là một con số, càng nhỏ càng tốt, và phải lấy được đạo hàm. Phần còn lại của bài này là cách làm cho con số đó nhỏ đi. (Nếu bạn muốn xem một bài "fundamentals" khác cùng tinh thần này, xem Quantization FundamentalsTurboQuant: KV Cache Compression.)

Vấn đề: hàm $L$ thường có hàng triệu, hàng tỉ chiều (mỗi tham số là một chiều). Mình không thể "nhìn" thấy mặt cong đó. Mình cũng không thể giải phương trình $\nabla L = 0$ trực tiếp như hồi cấp ba, vì $L$ quá phức tạp, không có công thức đóng (closed-form solution).

Nên cách duy nhất là: đứng tại một điểm, nhìn xuống chân, đi một bước về phía dốc nhất. Lặp lại cho tới khi tới đáy.

Đó là toàn bộ ý tưởng của Gradient Descent. Phần còn lại của bài viết này chỉ là chi tiết hoá ý tưởng đó.

Đạo hàm: cái la bàn của một bước đi

Trước khi nói gradient, mình quay lại đạo hàm, thứ bạn đã học cấp ba.

Tangent slope

Cho hàm một biến $f(x)$, đạo hàm tại điểm $x_0$ là:

$$
f'(x_0) = \lim_{h \to 0} \frac{f(x_0 + h) - f(x_0)}{h}
$$

Đạo hàm chính là độ dốc của tiếp tuyến với đồ thị tại điểm $x_0$. Nếu $f'(x_0) > 0$, hàm đang đi lên khi $x$ tăng. Nếu $f'(x_0) < 0$, hàm đang đi xuống.

Hệ quả nhỏ nhưng quan trọng: nếu mình đang đứng ở $x_0$ và muốn $f$ giảm, mình nên đi về phía ngược dấu của đạo hàm. Đạo hàm dương, đi sang trái. Đạo hàm âm, đi sang phải. Cụ thể:

$$
x_{\text{mới}} = x_0 - \eta \cdot f'(x_0)
$$

với $\eta > 0$ là một hằng số nhỏ, gọi là learning rate (tốc độ học), quyết định mỗi bước đi xa cỡ nào.

Đây là Gradient Descent một chiều. Toàn bộ deep learning chỉ là phiên bản nhiều chiều của công thức này.

Gradient: đạo hàm của hàm nhiều biến

Khi hàm $L$ phụ thuộc vào nhiều biến $\theta_1, \theta_2, \ldots, \theta_n$, mình không có một con số đạo hàm nữa. Mình có đạo hàm riêng theo từng biến, và gom chúng lại thành một vector gọi là gradient:

$$
\nabla L(\theta) = \begin{pmatrix} \dfrac{\partial L}{\partial \theta_1} \ \dfrac{\partial L}{\partial \theta_2} \ \vdots \ \dfrac{\partial L}{\partial \theta_n} \end{pmatrix}
$$

Ký hiệu $\dfrac{\partial L}{\partial \theta_i}$ đọc là "đạo hàm riêng của $L$ theo $\theta_i$", tức là coi tất cả biến khác như hằng số, chỉ lấy đạo hàm theo một biến.

Vector gradient này có một tính chất rất đẹp mà mình thấy ai mới học cũng nên dừng lại nghĩ kỹ:

Gradient $\nabla L(\theta)$ chỉ đúng hướng mà $L$ tăng nhanh nhất tại điểm $\theta$.

Hệ quả: $-\nabla L(\theta)$ là hướng $L$ giảm nhanh nhất. Đây không phải định nghĩa, đây là một định lý chứng minh được từ định nghĩa derivative theo hướng (directional derivative) và bất đẳng thức Cauchy-Schwarz. Nếu bạn từng thắc mắc "vì sao đi theo gradient lại tối ưu", đó là vì Cauchy-Schwarz nói rằng tích vô hướng của hai vector lớn nhất khi chúng cùng hướng, và bạn muốn tích vô hướng giữa "hướng đi" và "gradient" cực đại (hoặc cực tiểu, để giảm).

Gradient direction

Gradient Descent (GD): phiên bản gốc

Bây giờ ghép lại. Quy tắc cập nhật của Gradient Descent là:

$$
\theta_{t+1} = \theta_t - \eta \cdot \nabla L(\theta_t)
$$

Đọc bằng tiếng người: "Tham số mới = tham số cũ trừ đi một bước nhỏ về phía gradient âm."

Lặp lại công thức này hàng nghìn, hàng triệu lần. Đó là toàn bộ training. Không có gì khác.

Nhưng có một chi tiết quan trọng mình bỏ qua: $\nabla L(\theta_t)$ tính như thế nào? $L$ trong thực tế là trung bình loss trên toàn bộ tập dữ liệu:

$$
L(\theta) = \frac{1}{N} \sum_{i=1}^{N} \ell(x_i, y_i; \theta)
$$

với $N$ là số mẫu (sample) trong dataset, $\ell$ là loss của một mẫu. Nên gradient cũng là trung bình:

$$
\nabla L(\theta) = \frac{1}{N} \sum_{i=1}^{N} \nabla \ell(x_i, y_i; \theta)
$$

Đây là chỗ Gradient Descent gốc gãy.

Vì sao GD vanilla không scale được

Giả sử bạn train trên ImageNet, 1.4 triệu ảnh. Để tính một gradient, bạn phải:

  1. Chạy forward pass cho cả 1.4 triệu ảnh
  2. Chạy backward pass cho cả 1.4 triệu ảnh
  3. Trung bình lại
  4. Cập nhật $\theta$ một lần

Một bước update tốn vài giờ. Và bạn cần hàng nghìn bước. Vậy là vài tháng chỉ để train một epoch.

Mình đã thử điều này một lần với một dataset 50k ảnh, không phải vì dataset quá lớn mà vì lúc đó chưa hiểu rõ. Hôm sau ngủ dậy thấy laptop vẫn đang ở step 3. Đó là khi mình bắt đầu hiểu vì sao cộng đồng cần SGD.

SGD: Stochastic Gradient Descent

SGD (Stochastic Gradient Descent) là biến thể của Gradient Descent, trong đó mỗi bước cập nhật chỉ dùng một mẫu (hoặc một mini-batch nhỏ) thay vì toàn bộ dataset. Nhờ vậy, SGD nhanh hơn GD hàng nghìn lần khi dataset lớn, và là nền tảng cho mọi optimizer hiện đại.

Ý tưởng của SGD đơn giản đến mức gần như là gian lận: thay vì tính gradient trên toàn bộ dataset, tính gradient trên một mẫu duy nhất (hoặc một nhóm nhỏ).

Công thức:

$$
\theta_{t+1} = \theta_t - \eta \cdot \nabla \ell(x_i, y_i; \theta_t)
$$

với $(x_i, y_i)$ là một mẫu được chọn ngẫu nhiên từ dataset. Chữ "Stochastic" trong SGD có nghĩa là ngẫu nhiên.

Trong thực tế ai cũng dùng mini-batch SGD: chọn một batch nhỏ gồm $B$ mẫu (thường $B = 32, 64, 128, 256$):

$$
\theta_{t+1} = \theta_t - \eta \cdot \frac{1}{B} \sum_{i \in \text{batch}} \nabla \ell(x_i, y_i; \theta_t)
$$

Vì sao điều này hoạt động? Vì trung bình của một mẫu ngẫu nhiên là một ước lượng không chệch (unbiased estimator) của gradient thật:

$$
\mathbb{E}[\nabla \ell(x_i, y_i; \theta)] = \nabla L(\theta)
$$

Tạm dịch: nếu bạn lấy đủ nhiều mẫu, trung bình các gradient mẫu sẽ tiến về gradient thật. Đây là Định lý Giới hạn Trung tâm (Central Limit Theorem), thứ bạn đã học trong môn xác suất thống kê đại học.

Đánh đổi: mỗi bước SGD không chính xác như GD. Nó nhiễu (noisy). Path đi của SGD trên mặt loss không mượt mà, nó zigzag, nhảy lung tung.

GD vs SGD path

Nhưng cái nhiễu đó hoá ra lại là tính năng, không phải bug. Trong các bài toán không lồi (non-convex), tức gần như mọi neural network, nhiễu giúp model thoát khỏi các local minimum nông và tìm được vùng phẳng tốt hơn. Có một dòng research lớn về việc vì sao SGD lại generalize tốt hơn GD; câu trả lời ngắn gọn (và mình tin chỉ đúng một phần) là vì noise của nó implicit-regularize model. Nhưng đó là một bài viết khác.

Vấn đề SGD chưa giải được

SGD cứu được vấn đề tốc độ, nhưng vẫn còn hai khúc mắc lớn:

Một là: chọn learning rate $\eta$ rất khó.

  • $\eta$ quá lớn, bước đi văng qua đáy, model không bao giờ hội tụ, loss nhảy múa.
  • $\eta$ quá nhỏ, đi rùa bò, training mất nhiều ngày không cần thiết.
  • Tệ hơn: cùng một $\eta$ tốt cho tham số $\theta_1$ có thể quá lớn cho tham số $\theta_2$. Mỗi tham số có "scale" riêng.

Hai là: SGD bị mắc kẹt ở các điểm yên ngựa (saddle points) và các vùng phẳng.

Tại vùng phẳng, gradient gần bằng $0$, bước đi gần bằng $0$, model đứng yên. Trong không gian cao chiều, các saddle points và vùng phẳng nhiều hơn local minima rất nhiều (kết quả từ paper "Identifying and attacking the saddle point problem in high-dimensional non-convex optimization" của Dauphin et al., 2014).

Để giải quyết hai vấn đề này, người ta lần lượt phát minh ra hai ý tưởng. Và Adam là phép cộng của cả hai.

Momentum: quán tính của một quả bóng

Momentum là kỹ thuật cộng "quán tính" vào Gradient Descent: thay vì chỉ đi theo gradient hiện tại, optimizer mang theo trung bình có trọng số của các gradient gần đây. Hiệu ứng: tăng tốc trên dốc thẳng, giảm dao động ở vùng zigzag, vượt qua được saddle points.

Hãy tưởng tượng quả bóng lăn xuống thung lũng. Trong vật lý thật, quả bóng không chỉ đi theo gradient hiện tại, nó còn mang theo quán tính từ các bước trước. Khi gặp một dốc dài liên tục, nó tăng tốc. Khi gặp một bump nhỏ, nó vẫn lăn qua được nhờ động năng.

Momentum ball

Momentum đưa ý tưởng đó vào optimizer. Thay vì cập nhật $\theta$ bằng gradient hiện tại, mình duy trì một vector "vận tốc" $v$, tích luỹ gradient theo thời gian:

$$
v_{t+1} = \beta \cdot v_t + (1 - \beta) \cdot \nabla L(\theta_t)
$$

$$
\theta_{t+1} = \theta_t - \eta \cdot v_{t+1}
$$

với $\beta \in [0, 1)$, thường $\beta = 0.9$, là hệ số quán tính.

Đọc lại công thức trên: $v_{t+1}$ là trung bình có trọng số mũ giảm dần (exponentially weighted moving average) của các gradient gần đây. Gradient cũ cách đây càng lâu thì trọng số càng nhỏ, vì $\beta^k \to 0$ khi $k \to \infty$.

Tác dụng:
- Tại các vùng gradient cùng hướng nhiều bước liên tiếp, $v$ tích luỹ lớn, bước đi nhanh hơn (tăng tốc).
- Tại các vùng gradient dao động (zigzag), các thành phần ngược dấu triệt tiêu lẫn nhau, đi mượt hơn.
- Tại các saddle points, $v$ vẫn còn từ bước trước, có thể "đẩy" model ra khỏi điểm yên.

Momentum giải quyết vấn đề vùng phẳng và saddle points, nhưng không giải quyết vấn đề learning rate per-parameter.

RMSProp: mỗi tham số một learning rate riêng

Ý tưởng RMSProp: nếu một tham số có gradient lớn liên tục, nên đi bước nhỏ hơn (vì đang ở dốc thẳng đứng, dễ bay qua đáy). Nếu một tham số có gradient bé liên tục, nên đi bước lớn hơn (vì đang ở vùng phẳng).

Cách đo "gradient lớn liên tục" cho từng tham số: lấy trung bình bình phương gradient (mean square) gần đây. Bình phương để bỏ dấu, cả gradient âm và dương cùng đóng góp vào "độ lớn".

$$
s_{t+1} = \gamma \cdot s_t + (1 - \gamma) \cdot \big(\nabla L(\theta_t)\big)^2
$$

(Bình phương ở đây là element-wise, bình phương từng thành phần của vector gradient, không phải tích vô hướng.)

Sau đó, chia bước đi cho căn bậc hai của $s$:

$$
\theta_{t+1} = \theta_t - \frac{\eta}{\sqrt{s_{t+1}} + \epsilon} \cdot \nabla L(\theta_t)
$$

$\epsilon$ (thường $10^{-8}$) là số bé chống chia cho 0.

Tham số nào có gradient lớn, $s$ lớn, bước nhỏ. Tham số nào gradient bé, $s$ bé, bước lớn. Mỗi tham số có learning rate hiệu dụng riêng, được điều chỉnh tự động.

Adam: phép cộng đẹp

Adam (Adaptive Moment Estimation) là optimizer kết hợp Momentum và RMSProp: vừa duy trì trung bình động của gradient (moment bậc 1) để có quán tính, vừa duy trì trung bình động bình phương gradient (moment bậc 2) để mỗi tham số có learning rate riêng. Đây là optimizer mặc định trong hầu hết deep learning frameworks hiện nay.

Adam là viết tắt của Adaptive Moment Estimation. Ý tưởng đơn giản: kết hợp Momentum và RMSProp.

Adam combination

Cụ thể, Adam giữ hai trung bình động:

Moment bậc nhất (mean, chính là Momentum):
$$
m_{t+1} = \beta_1 \cdot m_t + (1 - \beta_1) \cdot \nabla L(\theta_t)
$$

Moment bậc hai (mean of square, chính là RMSProp):
$$
v_{t+1} = \beta_2 \cdot v_t + (1 - \beta_2) \cdot \big(\nabla L(\theta_t)\big)^2
$$

Mặc định: $\beta_1 = 0.9$, $\beta_2 = 0.999$.

Có một tinh tế nhỏ. Ban đầu $m_0 = 0$ và $v_0 = 0$, nên ở các bước đầu tiên $m$ và $v$ bị lệch về 0 (biased toward zero). Tác giả Adam (Kingma & Ba, 2014) đưa ra bước hiệu chỉnh lệch (bias correction):

$$
\hat{m}{t+1} = \frac{m{t+1}}{1 - \beta_1^{t+1}}, \qquad \hat{v}{t+1} = \frac{v{t+1}}{1 - \beta_2^{t+1}}
$$

Khi $t$ nhỏ, mẫu số $1 - \beta^{t+1}$ bé, khuếch đại $\hat{m}, \hat{v}$ cho cân lại với 0 ban đầu. Khi $t$ lớn, mẫu số tiến về 1, bias correction biến mất tự nhiên.

Cuối cùng, cập nhật:

$$
\theta_{t+1} = \theta_t - \frac{\eta}{\sqrt{\hat{v}{t+1}} + \epsilon} \cdot \hat{m}{t+1}
$$

Đọc câu này thật chậm: bước đi tỉ lệ với gradient trung bình (Momentum), chia cho căn của bình phương gradient trung bình (RMSProp). Đó là Adam. Không có gì hơn.

Vì sao Adam trở thành mặc định

Adam có ba tính chất khiến nó gần như mặc định trong deep learning hiện đại:

  • Robust với learning rate: $\eta = 10^{-3}$ hoạt động tốt cho rất nhiều bài toán khác nhau, nên bạn không phải tune quá kỹ. (Đây là lý do paper hay viết lr=1e-3 mà không giải thích.)
  • Hoạt động trên gradient thưa (sparse): vì có per-parameter learning rate, các tham số ít gặp gradient (như embedding của các từ hiếm trong NLP) vẫn được cập nhật đủ.
  • Hội tụ nhanh ở giai đoạn đầu: kết hợp momentum + adaptive lr giúp loss giảm rất nhanh trong 10-20% epochs đầu.

Nhưng (và đây là chỗ mình muốn thừa nhận một bất đồng đang tồn tại trong cộng đồng), Adam không phải lúc nào cũng tốt nhất. Với vision tasks lớn (ImageNet, COCO), SGD với momentum và learning rate schedule cẩn thận thường generalize tốt hơn Adam. Có nhiều giả thuyết về vì sao, nhưng câu trả lời ngắn gọn: adaptive methods có thể converge tới vùng "sharp" của loss surface, trong khi SGD-momentum thường tới vùng "flat" hơn, và flat minima generalize tốt hơn (xem paper "The Marginal Value of Adaptive Gradient Methods in Machine Learning" của Wilson et al., 2017).

Nên không có optimizer nào "tốt nhất". Có optimizer phù hợp với từng bài toán.

Quay lại điểm xuất phát

Quay lại câu mở đầu: "Adam optimizer with learning rate 1e-4." Bây giờ câu này không còn là magic. Nó nói:

  • Adam: dùng phương pháp adaptive moment estimation, cập nhật mỗi tham số với learning rate riêng, có quán tính.
  • learning rate 1e-4: bước đi cơ sở là $0.0001$, thấp hơn mặc định một bậc, có thể vì model lớn hoặc gradient có scale lớn.

Một dòng cấu hình. Đằng sau là 60 năm research từ Cauchy (Gradient Descent, 1847), Robbins & Monro (Stochastic approximation, 1951), Polyak (Momentum, 1964), Hinton (RMSProp, 2012), Kingma & Ba (Adam, 2014).


Mình đã định viết phần kết bằng một câu kiểu "hãy thử implement Adam from scratch để hiểu sâu hơn". Nhưng nghĩ lại, đó không phải điều mình muốn nói nhất.

Điều mình thật sự muốn nói là: cảm giác đứng trước một công thức không hiểu, cảm giác mà mình có hai năm trước với câu "Adam optimizer with learning rate 1e-4", không phải là dấu hiệu bạn không đủ giỏi. Nó là dấu hiệu rằng người viết bài đó đã bỏ qua câu chuyện. Hầu hết các thứ trong machine learning, khi tháo dỡ tới gốc, đều bắt đầu từ thứ gì đó bạn đã học cấp ba: đạo hàm, trung bình, bình phương. Phần khó là xếp chúng lại đúng thứ tự.

Và thật ra bài này là bước đà vừa đẹp để mình kể bạn nghe về một thứ thú vị hơn nhiều: Muon, một optimizer mà đội Kimi (Moonshot AI) công bố tháng 2 năm 2025 trong tech report "Muon is Scalable for LLM Training" (arXiv:2502.16982). Muon không phải Adam phiên bản 2. Nó dựa trên một ý tưởng khác hẳn: trực giao hoá ma trận (matrix orthogonalization) trên gradient của các weight matrix trước khi cập nhật. Hệ quả: trên cùng một bài toán, Muon đạt chất lượng tương đương AdamW nhưng chỉ tốn khoảng 52% số FLOPs training, gần gấp đôi hiệu quả tính toán. Đội Kimi đã dùng nó (kết hợp với một biến thể tên MuonClip để giải quyết vấn đề attention score blow-up) để train Kimi K2, một model trillion-parameter, và họ release kèm Moonlight 16B MoE như proof-of-concept.

Vì sao điều này quan trọng? Vì AdamW đã là mặc định gần một thập kỷ. Một optimizer mới chứng minh được scale tới trillion-parameter và rẻ hơn một nửa, đó là sự kiện hiếm. Nhưng để hiểu Muon đang làm gì khác, bạn cần nắm chắc Adam đang làm gì trước. Đó là lý do mình viết bài này trước. Bài tiếp theo (nếu bạn quan tâm) sẽ là Muon, từ đại số tuyến tính cấp đại học cho tới vì sao Newton-Schulz iteration lại xuất hiện trong inner loop của một optimizer.


Nếu bài này hữu ích cho bạn, mình mừng. Nếu bạn thấy chỗ nào mình giải thích sai hoặc đơn giản hoá quá đà, mình rất muốn nghe, đó là cách bài viết tiếp theo sẽ tốt hơn.

alt text
Bài viết này có chút gió của Lâm Hà, Lâm Đồng ⛰️

Bình