智慧型控制 Intelligent Control

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 層歸屬度第 2 層規則強度第 3 層正規化第 4 層規則輸出第 5 層加總hu水位低水位中水位高閥小閥大ΠNw̄1·f1ΠNw̄2·f2ΠNw̄3·f3ΠNw̄4·f4ΠNw̄5·f5ΠNw̄6·f6ΣΔh學:中心 c、寬度 σ(梯度法)學:f = p·h + q·u + s(最小平方一次解出)固定運算,沒有參數
層運算參數
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\)),不必慢慢迭代。所以每一輪的做法是:

  1. 前向:前提參數固定,用最小平方解出最好的結論參數。
  2. 反向:結論參數固定,用梯度法調整前提參數。由連鎖律, $$\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
線性 ARX50.25 cm13.2 cm
NARX 神經網路1,2490.19 cm1.47 cm
ANFIS,固定歸屬函數(只學結論)180.23 cm0.76 cm
ANFIS,混合學習280.015 cm0.041 cm
0100200300400500600-4-2024時間(s)自由模擬誤差(cm)NARX(1,249 個參數):1.47 cmANFIS 固定歸屬函數(18):0.76 cmANFIS 混合學習(28):0.041 cm同一段 10 分鐘測試資料(與上一頁相同);縱軸為「模型 − 真實水位」,線性 ARX 的誤差達 ±30 cm,超出圖外

ANFIS 用少得多的參數,自由模擬的誤差卻比 NARX 小一個數量級以上。但這個比較對 NARX 不完全公平:

換一個輸入很多、或關係很不規則的系統,結果可能相反。

精度與可解讀性 Accuracy vs. Interpretability

ANFIS 常被宣傳成「可以解讀的神經網路」。看看學習前後的歸屬函數:

00.511.5200.51水位 h(m)歸屬度00.20.40.600.51閥開度 u虛線:學習前(等距) 實線:混合學習後;水位「低」的中心移到 -1.32 m、閥「小」移到 -0.87,已在範圍外中高

混合學習為了把誤差降到最低,把水位的 3 個歸屬函數都擠到 0.6~0.7 m 附近與範圍外(中心 −1.32 m),閥開度「小」的中心也移到 −0.87。學出來的規則雖然更準,「水位低、中、高」已經沒有原本的意思了。固定歸屬函數的版本保留了語意,但讀它的規則時也要小心:結論參數是讓整體誤差最小的值,相鄰規則會互相補償,不一定等於該水位的物理線性化。

實務上常見的折衷是限制前提參數的變化範圍(例如中心不能越過鄰居、寬度有上下限),或在損失函數裡加上懲罰項,用一點精度換回可解讀性。

限制與應用 Limits and Uses