ANFIS:可學習的模糊推論系統
模糊控制章的規則與歸屬函數都是人憑經驗設計的。ANFIS(adaptive-network-based fuzzy inference system,Jang 1993)把一個 Sugeno 型模糊推論系統寫成多層網路,於是可以像類神經網路一樣從資料學習參數,學完後仍是一組「若…則…」的模糊規則。它是模糊理論與類神經網路結合(neuro-fuzzy)最具代表性的方法,MATLAB 的 Fuzzy Logic Toolbox 也內建了 anfis 指令。
五層架構 Architecture
以本頁的水槽為例:輸入是水位 \(h\) 與閥開度 \(u\),水位用「低、中、高」3 個歸屬函數、閥開度用「小、大」2 個,所有組合共 6 條一階 Sugeno 規則:
$$R_r:\ \text{若 } h \text{ 是 } A_i \text{ 且 } u \text{ 是 } B_j,\text{則 } f_r = p_r h + q_r u + s_r$$| 層 | 運算 | 參數 |
|---|---|---|
| 1 歸屬度 | \(\mu_{A_i}(h) = \exp\!\big(-(h - c_i)^2 / 2\sigma_i^2\big)\)(高斯歸屬函數) | 中心 \(c\)、寬度 \(\sigma\)(前提參數) |
| 2 規則強度 | \(w_r = \mu_{A_i}(h)\,\mu_{B_j}(u)\)(以乘積當作 AND) | 無 |
| 3 正規化 | \(\bar w_r = w_r / \sum_k w_k\) | 無 |
| 4 規則輸出 | \(\bar w_r f_r = \bar w_r (p_r h + q_r u + s_r)\) | \(p, q, s\)(結論參數) |
| 5 加總 | \(y = \sum_r \bar w_r f_r\) | 無 |
這正是 Sugeno 推論的加權平均,只是每一步都寫成可微分的運算。本例共有 \(2 \times 5 = 10\) 個前提參數、\(6 \times 3 = 18\) 個結論參數。
混合學習 Hybrid Learning
所有參數都可以用倒傳遞的梯度法來學,但 Jang 注意到一個性質:前提參數固定時,輸出對結論參數是線性的。把 \(N\) 筆資料寫成矩陣,
$$\mathbf y = \mathbf A \boldsymbol\theta, \qquad \mathbf A \text{ 的第 } n \text{ 列} = \big[\bar w_1 h_n,\ \bar w_1 u_n,\ \bar w_1,\ \ldots,\ \bar w_6 h_n,\ \bar w_6 u_n,\ \bar w_6\big]$$結論參數 \(\boldsymbol\theta\) 可以用最小平方一次解出(\(\boldsymbol\theta = (\mathbf A^\mathsf T \mathbf A)^{-1} \mathbf A^\mathsf T \mathbf t\)),不必慢慢迭代。所以每一輪的做法是:
- 前向:前提參數固定,用最小平方解出最好的結論參數。
- 反向:結論參數固定,用梯度法調整前提參數。由連鎖律, $$\frac{\partial y}{\partial w_r} = \frac{f_r - y}{\sum_k w_k}, \qquad \frac{\partial w_r}{\partial c_i} = w_r \frac{x - c_i}{\sigma_i^2}, \qquad \frac{\partial w_r}{\partial \sigma_i} = w_r \frac{(x - c_i)^2}{\sigma_i^3}$$
因為最小平方已把誤差壓得很低,前提參數的梯度很小(本頁水槽的例子最大只有約 \(2 \times 10^{-5}\)),用固定步長的梯度下降幾乎不會動。本頁改用 Adam:它依梯度的歷史大小自動調整步長。Jang 原文則用「誤差連續下降就加大步長、來回振盪就減小」的規則。
下方動畫在網頁裡即時執行混合學習,用一個輸入的 ANFIS 逼近一條曲線。只用 3 個歸屬函數時,只學結論的誤差停在 0.061;混合學習把歸屬函數移到曲線彎曲的地方,誤差降到 0.002:
範例:用 ANFIS 鑑別水槽 Example
使用與上一頁 NARX 完全相同的訓練與測試資料。ANFIS 學的是一秒後水位的變化量 \(\Delta h = h(k+1) - h(k)\),PyTorch 版本的前提參數梯度由自動微分算出:
# ANFIS 系統鑑別:用一階 Sugeno 模糊推論系統(寫成網路)學水槽(同模糊控制章)的動態
# 輸入:水位 h(k)、閥開度 u(k);輸出:一秒後水位的變化量 Δh = h(k+1) − h(k)
# 資料與 tank_narx.py 完全相同(同一個亂數種子),方便和 NARX、ARX 比較
import json
import math
from pathlib import Path
import numpy as np
import torch
torch.set_default_dtype(torch.float64)
def tank(u, h0=0.5):
"""真實系統(只用來產生資料):dh/dt = 0.5u − 0.05√(2gh),取樣 1 s,槽高 2 m"""
h, out = h0, [h0]
for uk in u:
for _ in range(10):
f = lambda x: 0.5 * uk - 0.05 * math.sqrt(2 * 9.8 * max(x, 0.0))
k1 = f(h); k2 = f(h + 0.05 * k1); k3 = f(h + 0.05 * k2); k4 = f(h + 0.1 * k3)
h = min(max(h + 0.1 / 6 * (k1 + 2 * k2 + 2 * k3 + k4), 0.0), 2.0)
out.append(h)
return np.array(out)
def steps_input(n, rng, hi=0.62):
u = []
while len(u) < n:
u += [rng.uniform(0, hi)] * int(rng.integers(10, 61))
return np.array(u[:n])
rng = np.random.default_rng(0)
u_tr = steps_input(3000, rng); y_tr = tank(u_tr)
u_te = steps_input(600, rng); y_te = tank(u_te)
X = torch.tensor(np.c_[y_tr[:-1], u_tr]) # [h(k), u(k)]
T = torch.tensor(y_tr[1:] - y_tr[:-1]) # Δh
class ANFIS:
"""水位 3 個、閥開度 2 個高斯歸屬函數 → 6 條規則;規則 r 的結論 f_r = p_r h + q_r u + s_r"""
def __init__(self):
self.c = [torch.tensor([0.0, 1.0, 2.0]), torch.tensor([0.0, 0.62])] # 中心:等距
w = lambda a, b, m: (b - a) / (m - 1) / (2 * math.sqrt(2 * math.log(2))) # 寬度:相鄰兩個交在 0.5
self.s = [torch.full((3,), w(0, 2, 3)), torch.full((2,), w(0, 0.62, 2))]
self.rules = [(i, j) for i in range(3) for j in range(2)]
self.P = torch.zeros(6, 3)
def norm_firing(self, X): # 第 1~3 層:歸屬度 → 相乘 → 正規化
mu = [torch.exp(-(X[:, d:d + 1] - self.c[d]) ** 2 / (2 * self.s[d] ** 2)) for d in range(2)]
W = torch.stack([mu[0][:, i] * mu[1][:, j] for i, j in self.rules], 1)
return W / W.sum(1, keepdim=True)
def __call__(self, X): # 第 4~5 層:加權平均各規則的線性結論
Xa = torch.cat([X, torch.ones(len(X), 1)], 1)
return (self.norm_firing(X) * (Xa @ self.P.T)).sum(1)
def lse(self, X, T):
"""前提固定時,輸出對 18 個結論參數是線性的 → 最小平方一次解出"""
with torch.no_grad():
Xa = torch.cat([X, torch.ones(len(X), 1)], 1)
A = (self.norm_firing(X)[:, :, None] * Xa[:, None, :]).reshape(len(X), -1)
self.P = torch.linalg.solve(A.T @ A + 1e-8 * torch.eye(18), A.T @ T).reshape(6, 3)
def train(net, epochs, lr):
"""混合學習:每輪先用最小平方解結論,再用自動微分+Adam 調歸屬函數的中心與寬度"""
prem = net.c + net.s
for p in prem:
p.requires_grad_(True)
opt = torch.optim.Adam(prem, lr=lr)
hist = []
for _ in range(epochs):
net.lse(X, T)
loss = torch.mean((net(X) - T) ** 2)
hist.append(math.sqrt(loss.item()))
opt.zero_grad(); loss.backward(); opt.step()
with torch.no_grad():
for s in net.s:
s.clamp_(min=0.05)
net.lse(X, T)
for p in prem:
p.requires_grad_(False)
return hist
def free_run(net, y, u):
"""自由模擬:只給初始水位,之後都用模型自己的預測"""
h = [y[0]]
with torch.no_grad():
for k in range(len(u)):
dh = float(net(torch.tensor([[h[-1], u[k]]]))[0])
h.append(min(max(h[-1] + dh, 0.0), 2.0))
return np.array(h)
rm = lambda a, b: float(np.sqrt(np.mean((a - b) ** 2)))
fixed, learned = ANFIS(), ANFIS()
fixed.lse(X, T) # 只學結論(歸屬函數固定在等距)
hist = train(learned, 300, 0.03) # 混合學習 300 輪
for name, net, n in (("固定歸屬函數(只學結論)", fixed, 18), ("混合學習(也調歸屬函數)", learned, 28)):
one = net(torch.tensor(np.c_[y_te[:-1], u_te])).detach().numpy() + y_te[:-1]
print(f"{name}:{n} 個參數,一步預測 RMSE {rm(one, y_te[1:]) * 100:.3f} cm,自由模擬 600 s 的 RMSE {rm(free_run(net, y_te, u_te), y_te) * 100:.3f} cm")
print("混合學習後的水位歸屬函數 中心", np.round(learned.c[0].numpy(), 3), "寬度", np.round(learned.s[0].numpy(), 3))
print(" 閥開度歸屬函數 中心", np.round(learned.c[1].numpy(), 3), "寬度", np.round(learned.s[1].numpy(), 3))
print("固定歸屬函數的規則(Δh = p·h + q·u + s):")
for (i, j), (p, q, s) in zip(fixed.rules, fixed.P.numpy()):
print(f" 若水位{'低中高'[i]}且閥開度{'小大'[j]},Δh = {p:+.3f}h {q:+.3f}u {s:+.3f}")
dump = lambda net: {"c": [c.tolist() for c in net.c], "s": [s.tolist() for s in net.s], "P": net.P.tolist()}
Path(__file__).with_name("tank_anfis.json").write_text(json.dumps({
"fixed": dump(fixed), "learned": dump(learned), "hist": hist,
"test": {"u": u_te.tolist(), "y": y_te.tolist(), "fixed": free_run(fixed, y_te, u_te).tolist(),
"learned": free_run(learned, y_te, u_te).tolist()}}), encoding="utf-8")⬇ 下載 tank_anfis.py(Python 3+PyTorch)
實際執行的輸出:
固定歸屬函數(只學結論):18 個參數,一步預測 RMSE 0.230 cm,自由模擬 600 s 的 RMSE 0.762 cm
混合學習(也調歸屬函數):28 個參數,一步預測 RMSE 0.015 cm,自由模擬 600 s 的 RMSE 0.041 cm
混合學習後的水位歸屬函數 中心 [-1.323 0.599 0.701] 寬度 [0.525 0.375 0.355]
閥開度歸屬函數 中心 [-0.866 0.253] 寬度 [0.361 0.145]
固定歸屬函數的規則(Δh = p·h + q·u + s):
若水位低且閥開度小,Δh = -0.387h +0.138u -0.017
若水位低且閥開度大,Δh = -0.131h +0.138u +0.136
若水位中且閥開度小,Δh = -0.266h +0.597u +0.049
若水位中且閥開度大,Δh = -0.075h +0.574u -0.195
若水位高且閥開度小,Δh = -0.395h +0.363u +0.436
若水位高且閥開度大,Δh = -0.036h +0.381u -0.157
| 模型(測試資料 600 s) | 參數個數 | 一步預測 RMSE | 自由模擬 RMSE |
|---|---|---|---|
| 線性 ARX | 5 | 0.25 cm | 13.2 cm |
| NARX 神經網路 | 1,249 | 0.19 cm | 1.47 cm |
| ANFIS,固定歸屬函數(只學結論) | 18 | 0.23 cm | 0.76 cm |
| ANFIS,混合學習 | 28 | 0.015 cm | 0.041 cm |
ANFIS 用少得多的參數,自由模擬的誤差卻比 NARX 小一個數量級以上。但這個比較對 NARX 不完全公平:
- ANFIS 的輸入選對了:水槽是一階系統,下一刻只由 \(h(k), u(k)\) 決定;NARX 多用了 \(y(k-1), u(k-1)\),網路要自己學會忽略它們。
- 水槽的 \(\Delta h\) 對 \(u\) 幾乎是線性、對 \(h\) 是平滑的曲線,正好適合「幾個局部線性模型的加權平均」這種形式。
- ANFIS 的結論參數用最小平方直接解出最佳值,NARX 只用梯度法訓練 3000 次。
換一個輸入很多、或關係很不規則的系統,結果可能相反。
精度與可解讀性 Accuracy vs. Interpretability
ANFIS 常被宣傳成「可以解讀的神經網路」。看看學習前後的歸屬函數:
混合學習為了把誤差降到最低,把水位的 3 個歸屬函數都擠到 0.6~0.7 m 附近與範圍外(中心 −1.32 m),閥開度「小」的中心也移到 −0.87。學出來的規則雖然更準,「水位低、中、高」已經沒有原本的意思了。固定歸屬函數的版本保留了語意,但讀它的規則時也要小心:結論參數是讓整體誤差最小的值,相鄰規則會互相補償,不一定等於該水位的物理線性化。
實務上常見的折衷是限制前提參數的變化範圍(例如中心不能越過鄰居、寬度有上下限),或在損失函數裡加上懲罰項,用一點精度換回可解讀性。
限制與應用 Limits and Uses
- 維度爆炸:格狀分割的規則數是 \(m^n\)。10 個輸入、每個 3 個歸屬函數就有 \(3^{10} = 59{,}049\) 條規則。輸入多時要改用分群(subtractive clustering、fuzzy c-means)產生規則,或直接改用一般的深度網路。
- Takagi–Sugeno 模型與控制:每條規則都是一個局部線性模型,整個模型是它們的平滑切換,和增益排程(gain scheduling)的概念相同。對這類模型可以為每條規則設計一個線性回授,再用線性矩陣不等式(LMI)證明整體穩定(parallel distributed compensation),這是模糊控制少數有完整穩定性理論的方法。
- 學控制器:除了鑑別受控體,也可以讓 ANFIS 模仿一個好的控制器(例如 MPC 或專家操作)的輸入輸出資料,得到一組可以檢查的模糊規則,計算量也比 MPC 小得多。