합동회사 코무라소프트
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 습함」 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에서는 명료함을 위해 규칙별로 한 줄씩 풀어 적었습니다. JavaScript의 루프 내 지역 변수 muC의 aggregated[x](크기 101의 double 배열의 각 요소)에 대응합니다. 양쪽 모두 「같은 x에 대한 집약 후 높이」를 나타냅니다.

같은 로직의 참고 구현을 fc101-reference.c로도 함께 담아 두었습니다. 링크 끊김에 대비해 본 페이지의 C 코드는 페이지 내에서 자기완결되도록 작성했습니다.

구현을 읽을 때 주목할 점

소속 함수는 경계에서 0이나 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. 그런 다음에야 라벨 수나 경계 값을 조정합니다.