実装読解 — 最小の Mamdani 制御器を読む
ここまでの図と手計算を、そのまま JavaScript と C のコードへ写す。
2 つの言語で同じロジックを並べる狙い
本章では JavaScript と C を並列で示します。狙いは「同じファジィ制御のパイプラインを、文法の違う 2 言語で見ても、構造は変わらない」ことを確認するためです。JavaScript はブラウザ上のシミュレータでそのまま動く側、C は組み込み機器に持ち込みたいときの参考実装です。両者の対応関係は次のとおりです。
| 段階 | JavaScript | C |
|---|---|---|
| 所属度 | getTemperatureDegrees(t) / getHumidityDegrees(h) | temperature_degrees(t, °) / humidity_degrees(h, °) |
| 発火度 (AND) | Math.min(a, b) | fmin(a, b) |
| 集約 (max) | Math.max(acc, fire) | fmax(acc, fire) |
| クリップ済みの山 | 配列 aggregated[x] (length 101) | 配列 double aggregated[101] |
| 重心 | centroid(aggregated) | centroid(aggregated, 101) |
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 蒸し暑い" 1 本だけ) ではなく、RULES 配列に並べた定義を evaluateRules がループで評価します。分母 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 では明快さのためにルールごとに 1 行ずつ書き下しています。JavaScript の aggregated はループ内のローカル変数 mu、C の aggregated[x] はサイズ 101 の double 配列の各要素に対応します。どちらも「同じ x に対する集約後の高さ」を表します。
fc101-reference.c にも同じロジックの参考実装を同梱しています。リンク切れに備えて、本ページの C コードはページ内で完結するよう自己完結にしてあります。
実装を読むときの着眼点
min、同じ出力ラベルのまとめは max です。if (area < 1e-9) return 0; を入れて防御しています。ラベルの境界を調整して使うときは、この防御を外さないでください。理解チェック 6 — コードを数に戻す
実装に出てくる min / max / 加重平均を、その場で追います。
Q1. trimf(26, 20, 24, 28) の戻り値はいくつですか。
26 は右の斜面なので (28-26)/(28-24)=0.50 です。
Q2. Math.min(0.25, 0.35) の結果はいくつですか。
AND=min をそのまま書いているので、小さい方の 0.25 が発火度になります。
Q3. Math.max(0.30, 0.45) の結果はいくつですか。
同じ出力ラベルの主張をまとめるときは max を使うので 0.45 です。
Q4. ラベル中心近似で medium=0.30, high=0.45 のとき、最終出力はいくつですか。
分子は 50×0.30 + 85×0.45 = 53.25、分母は 0.75 なので 71.00% です。
次に自分の対象へ写すときの順序
- まずはラベルを 3 本ずつくらいに絞る
- 紙で所属度と発火度を 1 ケースだけ計算する
- そのケースがコードでも同じ数字になることを確認する
- 最後にラベル数や境界値を調整する