小村軟體有限公司
第 6 章

閱讀實作 — 最精簡的 Mamdani 控制器

將前面章節的圖示與手計算,原封不動地抄寫成 JavaScript 與 C 程式碼。

用兩種語言並列同一份邏輯的目的

本章把 JavaScript 與 C 並列呈現。目的是要確認「即使語法不同的兩種語言,相同的模糊控制管線結構並不會改變」。JavaScript 是直接驅動瀏覽器模擬器的版本,C 則是想把控制器移植到嵌入式機器時的參考實作。兩者的對應關係如下。

階段JavaScriptC
隸屬度getTemperatureDegrees(t) / getHumidityDegrees(h)temperature_degrees(t, &deg) / humidity_degrees(h, &deg)
觸發強度(AND)Math.min(a, b)fmin(a, b)
聚合(max)Math.max(acc, fire)fmax(acc, fire)
削平後的峰迴圈內局部變數 mu(length 101)陣列 double aggregated[101]
重心centroid(temperature, humidity)fc101_centroid(temperature, humidity)

JavaScript 的完整最小實作

下方程式碼是貼進瀏覽器主控台即可執行的完整版本。並非片段,所有函式內容皆已完整列出。

// ---- 隸屬函數(三角形・梯形)----
function trimf(x, a, b, c) {
  if (x <= a || x >= c) return 0;
  if (x === b) return 1;
  return x < b ? (x - a) / (b - a) : (c - x) / (c - b);
}
function trapmf(x, a, b, c, d) {
  if (x <= a || x >= d) return 0;
  if (x >= b && x <= c) return 1;
  return x < b ? (x - a) / (b - a) : (d - x) / (d - c);
}

// ---- 輸入標籤的隸屬度 ----
function getTemperatureDegrees(t) {
  return {
    cold:        trapmf(t, 16, 16, 19, 23),
    comfortable: trimf (t, 20, 24, 28),
    hot:         trapmf(t, 25, 29, 34, 34)
  };
}
function getHumidityDegrees(h) {
  return {
    dry:    trapmf(h, 20, 20, 30, 45),
    normal: trimf (h, 35, 55, 75),
    humid:  trapmf(h, 60, 75, 90, 90)
  };
}

// ---- 輸出標籤的隸屬度 ----
function getOutputDegrees(x) {
  return {
    low:    trimf(x,  0, 20, 40),
    medium: trimf(x, 30, 50, 70),
    high:   trimf(x, 65, 85, 100)
  };
}

// ---- 9 條規則的定義 ----
const RULES = [
  { temp: "cold",        humid: "dry",    out: "low"    },
  { temp: "cold",        humid: "normal", out: "low"    },
  { temp: "cold",        humid: "humid",  out: "medium" },
  { temp: "comfortable", humid: "dry",    out: "low"    },
  { temp: "comfortable", humid: "normal", out: "medium" },
  { temp: "comfortable", humid: "humid",  out: "medium" },
  { temp: "hot",         humid: "dry",    out: "medium" },
  { temp: "hot",         humid: "normal", out: "high"   },
  { temp: "hot",         humid: "humid",  out: "high"   }
];

// ---- 通用評估 9 條規則並得到聚合高度 ----
function evaluateRules(tDeg, hDeg) {
  const heights = { low: 0, medium: 0, high: 0 };
  for (const r of RULES) {
    const fire = Math.min(tDeg[r.temp], hDeg[r.humid]); // AND = min
    heights[r.out] = Math.max(heights[r.out], fire);    // 同標籤 = max
  }
  return heights;
}

// ---- 重心法(精確版)----
function centroid(temperature, humidityValue) {
  const tDeg = getTemperatureDegrees(temperature);
  const hDeg = getHumidityDegrees(humidityValue);
  const h    = evaluateRules(tDeg, hDeg);

  let area = 0, moment = 0;
  for (let x = 0; x <= 100; ++x) {
    const m = getOutputDegrees(x);
    const muLow  = Math.min(m.low,    h.low);
    const muMed  = Math.min(m.medium, h.medium);
    const muHigh = Math.min(m.high,   h.high);
    const mu     = Math.max(muLow, muMed, muHigh);
    area   += mu;
    moment += x * mu;
  }
  // 防禦式程式設計:避免任何規則都未觸發時的分母為 0
  if (area < 1e-9) return 0;
  return moment / area;
}

// ---- 動作確認 ----
console.log(centroid(26, 68));   // 約 62.05
console.log(centroid(26.8, 69)); // 約 70.13
console.log(centroid(24, 55));   // 約 50.00(僅「舒適×普通→中風」觸發)

9 條規則並非寫死於單一規則(「舒適 AND 悶熱」一條而已),而是由 evaluateRules 以迴圈逐條評估 RULES 陣列的定義。分母為 0 的檢查也已包含在內,因此即便調整標籤設計到所有規則都未觸發,控制器也不會崩潰。

C 的完整最小實作

下方程式碼可直接以 gcc fc101.c -o fc101 -lm 編譯。aggregated 陣列的大小、型別、初始化也全部明確指定。

#include <stdio.h>
#include <math.h>

#define OUT_LEN 101  /* x = 0,1,...,100 */

/* ---- 三角形・梯形隸屬函數 ---- */
static double trimf(double x, double a, double b, double c) {
  if (x <= a || x >= c) return 0.0;
  if (x == b) return 1.0;
  return (x < b) ? (x - a) / (b - a) : (c - x) / (c - b);
}
static double trapmf(double x, double a, double b, double c, double d) {
  if (x <= a || x >= d) return 0.0;
  if (x >= b && x <= c) return 1.0;
  return (x < b) ? (x - a) / (b - a) : (d - x) / (d - c);
}

/* ---- 輸入標籤的隸屬度 ---- */
typedef struct { double cold, comfortable, hot; } TempDeg;
typedef struct { double dry,  normal,      humid; } HumDeg;

static TempDeg temperature_degrees(double t) {
  TempDeg d;
  d.cold        = trapmf(t, 16, 16, 19, 23);
  d.comfortable = trimf (t, 20, 24, 28);
  d.hot         = trapmf(t, 25, 29, 34, 34);
  return d;
}
static HumDeg humidity_degrees(double h) {
  HumDeg d;
  d.dry    = trapmf(h, 20, 20, 30, 45);
  d.normal = trimf (h, 35, 55, 75);
  d.humid  = trapmf(h, 60, 75, 90, 90);
  return d;
}

/* ---- 輸出標籤的隸屬度 ---- */
static double mu_low   (double x) { return trimf(x,  0, 20,  40); }
static double mu_medium(double x) { return trimf(x, 30, 50,  70); }
static double mu_high  (double x) { return trimf(x, 65, 85, 100); }

/* ---- 9 條規則所得 low / medium / high 的聚合高度 ---- */
static void evaluate_rules(TempDeg t, HumDeg h, double *low, double *med, double *hi) {
  *low = *med = *hi = 0.0;
  /* cold */
  *low = fmax(*low, fmin(t.cold,        h.dry));
  *low = fmax(*low, fmin(t.cold,        h.normal));
  *med = fmax(*med, fmin(t.cold,        h.humid));
  /* comfortable */
  *low = fmax(*low, fmin(t.comfortable, h.dry));
  *med = fmax(*med, fmin(t.comfortable, h.normal));
  *med = fmax(*med, fmin(t.comfortable, h.humid));
  /* hot */
  *med = fmax(*med, fmin(t.hot,         h.dry));
  *hi  = fmax(*hi,  fmin(t.hot,         h.normal));
  *hi  = fmax(*hi,  fmin(t.hot,         h.humid));
}

/* ---- 重心法(精確版)---- */
double fc101_centroid(double temperature, double humidity) {
  TempDeg t = temperature_degrees(temperature);
  HumDeg  h = humidity_degrees(humidity);
  double low, med, hi;
  evaluate_rules(t, h, &low, &med, &hi);

  double aggregated[OUT_LEN] = {0};   /* 大小 101,double,0 初始化 */
  for (int x = 0; x < OUT_LEN; ++x) {
    double a = fmin(mu_low(x),    low);
    double b = fmin(mu_medium(x), med);
    double c = fmin(mu_high(x),   hi);
    double m = fmax(a, fmax(b, c));
    aggregated[x] = m;
  }

  double area = 0.0, moment = 0.0;
  for (int x = 0; x < OUT_LEN; ++x) {
    area   += aggregated[x];
    moment += x * aggregated[x];
  }
  if (area < 1e-9) return 0.0;        /* 分母為 0 時的防禦 */
  return moment / area;
}

int main(void) {
  printf("%.2f\n", fc101_centroid(26.0, 68.0));   /* 約 62.05 */
  printf("%.2f\n", fc101_centroid(26.8, 69.0));   /* 約 70.13 */
  printf("%.2f\n", fc101_centroid(24.0, 55.0));   /* 約 50.00 */
  return 0;
}

JavaScript 端的 evaluateRules 以迴圈走訪 RULES 陣列,C 版本為求清楚則為每條規則寫一行。JavaScript 迴圈內的局部變數 mu對應於 C 中 aggregated[x](大小 101 的 double 陣列之各元素),兩者都代表「同一個 x 上的聚合後高度」。

同一份邏輯的參考實作也以 fc101-reference.c 的形式附在課程中。本頁面的 C 程式碼刻意設計成在頁面內自我包含,以防附加檔案連結失效。

閱讀實作時要留意的重點

隸屬函數在邊界值是否落在 0 或 1?
三角形或梯形只要區間錯開 1 格,所有後續答案都會跟著變。
是否把 AND 與聚合搞混了?
組合條件用 min,整合同一輸出標籤的主張用 max
重心計算的分母會不會變成 0?
本課程的隸屬函數設計成必定至少有某條規則會觸發,但上方範例為了安全仍加入 if (area < 1e-9) return 0; 的防禦。當你調整標籤邊界時,請勿移除這個防禦。

理解檢核 6 — 把程式碼還原成數字

就地追蹤實作中出現的 min / max / 加權平均。

Q1. trimf(26, 20, 24, 28) 的回傳值是多少?

Q2. Math.min(0.25, 0.35) 的結果是多少?

Q3. Math.max(0.30, 0.45) 的結果是多少?

Q4. 以標籤中心近似法,當 medium = 0.30、high = 0.45 時,最終輸出是多少?

%

如何把這份實作移植到自己的問題上

  1. 首先把每個變數的標籤數先控制在 3 個左右。
  2. 選一個案例在紙上完成,把隸屬度與觸發強度手算一次。
  3. 確認程式碼對同一個案例會重現相同的數字。
  4. 接下來再調整標籤數量或邊界值。