HW4: Scenario1 source code and report added

This commit is contained in:
2025-10-26 20:36:08 +02:00
parent 8ced0fd58d
commit 613b21e059
32 changed files with 665 additions and 1 deletions
+50
View File
@@ -0,0 +1,50 @@
function results = evaluate_classification(yTrue, yPred, classLabels)
% EVALUATE_CLASSIFICATION Compute OA, PA, UA, Kappa and confusion matrix
%
% results = evaluate_classification(yTrue, yPred, classLabels)
%
% yTrue, yPred : true & predicted class labels
% classLabels : vector with all class IDs (optional)
%
% results struct:
% .confMat
% .OA
% .PA
% .UA
% .Kappa
if nargin < 3
classLabels = unique([yTrue; yPred]);
end
% Ensure column vectors
yTrue = yTrue(:);
yPred = yPred(:);
% Build confusion matrix
confMat = confusionmat(yTrue, yPred, 'Order', classLabels);
% Overall Accuracy
OA = trace(confMat) / sum(confMat(:));
% Producer's & User's Accuracy (per class)
PA = diag(confMat) ./ sum(confMat,2); % Recall per class
UA = diag(confMat) ./ sum(confMat,1)'; % Precision per class
% Cohen's Kappa
n = sum(confMat(:));
po = OA;
pe = sum(sum(confMat,1) .* sum(confMat,2)) / n^2;
Kappa = (po - pe) / (1 - pe);
% Pack results
results.confMat = confMat;
results.OA = OA;
results.PA = PA;
results.UA = UA;
results.Kappa = Kappa;
% Optional display
fprintf('Overall Accuracy: %.2f%%\n', 100*OA);
fprintf('Cohen''s Kappa : %.3f\n', Kappa);
end
Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 566 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

@@ -0,0 +1,5 @@
Run,Mode,Radius,Rules,OA,Kappa
1,class-independent,0.2,32,0.688524590163934,0.412569690826153
2,class-independent,0.8,3,0.737704918032787,0.616502946954813
3,class-dependent,0.2,51,0.60655737704918,0.296492071119654
4,class-dependent,0.8,4,0.721311475409836,0.604349484929416
1 Run Mode Radius Rules OA Kappa
2 1 class-independent 0.2 32 0.688524590163934 0.412569690826153
3 2 class-independent 0.8 3 0.737704918032787 0.616502946954813
4 3 class-dependent 0.2 51 0.60655737704918 0.296492071119654
5 4 class-dependent 0.8 4 0.721311475409836 0.604349484929416
+165
View File
@@ -0,0 +1,165 @@
function plot_results1(results, classLabels, cfg)
% PLOT_RESULTS1 Scenario 1 plotting suite (Classification)
% Generates and saves:
% (A) Confusion matrix per model
% (B) PA/UA bars per model
% (C) Membership functions BEFORE/AFTER training per model
% (D) Learning curves (train/val error) per model
% (E) Predictions vs Truth (test set) per model
% (F) OA and Kappa across models (bars)
% (G) Rules vs Accuracy scatter
%
% All PNGs saved under cfg.outDir.
outDir = cfg.outDir;
if ~exist(outDir,'dir'), mkdir(outDir); end
nRuns = numel(results);
OA = zeros(nRuns,1);
Kap = zeros(nRuns,1);
nRules = zeros(nRuns,1);
modes = strings(nRuns,1);
radii = zeros(nRuns,1);
for i = 1:nRuns
OA(i) = results(i).metrics.OA;
Kap(i) = results(i).metrics.Kappa;
nRules(i) = results(i).nRules;
modes(i) = string(results(i).mode);
radii(i) = results(i).radius;
end
% -------- Per-model plots --------
for i = 1:nRuns
tag = sprintf('run%02d_%s_r%.2f_rules%d', ...
i, results(i).mode, results(i).radius, results(i).nRules);
% (A) Confusion matrix
fig = figure('Color','w');
confusionchart(results(i).metrics.confMat, string(classLabels), ...
'Title', sprintf('Confusion %s (r=%.2f, rules=%d)', ...
results(i).mode, results(i).radius, results(i).nRules));
exportgraphics(fig, fullfile(outDir, ['cm_' tag '.png']), 'Resolution', 200);
close(fig);
% (B) PA / UA bars
fig = figure('Color','w');
t = tiledlayout(2,1,'TileSpacing','compact','Padding','compact');
nexttile;
bar(results(i).metrics.PA); ylim([0 1]);
xticks(1:numel(classLabels)); xticklabels(string(classLabels));
ylabel('PA (Recall)');
title(sprintf('Producer''s Accuracy %s (r=%.2f)', results(i).mode, results(i).radius));
nexttile;
bar(results(i).metrics.UA); ylim([0 1]);
xticks(1:numel(classLabels)); xticklabels(string(classLabels));
ylabel('UA (Precision)');
title(sprintf('User''s Accuracy %s (r=%.2f)', results(i).mode, results(i).radius));
exportgraphics(fig, fullfile(outDir, ['pa_ua_' tag '.png']), 'Resolution', 200);
close(fig);
% (C) Membership functions BEFORE/AFTER
% Layout: 2 rows (Before/After) x D columns (inputs)
try
plot_mfs_before_after(results(i).initFis, results(i).fis, ...
sprintf('%s (r=%.2f, %s)', results(i).mode, results(i).radius, tag), ...
fullfile(outDir, ['mfs_' tag '.png']));
catch ME
warning('MF plot failed for %s: %s', tag, ME.message);
end
% (D) Learning curves (ANFIS)
trErr = results(i).trError; % may be vector
ckErr = results(i).ckError; % may be vector
if ~isempty(trErr)
fig = figure('Color','w');
plot(1:numel(trErr), trErr, 'LineWidth', 1.2); hold on;
if ~isempty(ckErr)
plot(1:numel(ckErr), ckErr, '--', 'LineWidth', 1.2);
legend('Training Error','Validation Error','Location','best');
else
legend('Training Error','Location','best');
end
xlabel('Epoch'); ylabel('Error'); grid on;
title(sprintf('Learning Curve %s (r=%.2f, rules=%d)', ...
results(i).mode, results(i).radius, results(i).nRules));
exportgraphics(fig, fullfile(outDir, ['learning_' tag '.png']), 'Resolution', 200);
close(fig);
end
% (E) Predictions vs Truth (test set) like report example
if isfield(results(i),'yhat') && isfield(results(i),'ytrue')
yhat = results(i).yhat(:);
ytrue = results(i).ytrue(:);
fig = figure('Color','w');
plot(ytrue, 'LineWidth', 1.0); hold on;
plot(yhat, '--', 'LineWidth', 1.0);
xlabel('Test sample index'); ylabel('Class label');
title(sprintf('Truth vs Prediction %s (r=%.2f)', results(i).mode, results(i).radius));
legend('Truth','Prediction','Location','best'); grid on;
exportgraphics(fig, fullfile(outDir, ['pred_vs_truth_' tag '.png']), 'Resolution', 200);
close(fig);
end
end
% -------- Across-model summaries --------
[~, idxSort] = sortrows([double(modes=='class-independent'), radii], [1 2]);
OA_s = OA(idxSort);
Kap_s = Kap(idxSort);
modes_s = modes(idxSort);
radii_s = radii(idxSort);
labels = arrayfun(@(j) sprintf('%s\nr=%.2f', modes_s(j), radii_s(j)), 1:nRuns, 'uni', 0);
fig = figure('Color','w');
bar(OA_s*100); xticks(1:nRuns); xticklabels(labels); xtickangle(30);
ylabel('Overall Accuracy (%)'); title('Overall Accuracy across models'); grid on;
exportgraphics(fig, fullfile(outDir, 'overall_accuracy_across_models.png'), 'Resolution', 200);
close(fig);
fig = figure('Color','w');
bar(Kap_s); xticks(1:nRuns); xticklabels(labels); xtickangle(30);
ylabel('Cohen''s \kappa'); title('Kappa across models'); grid on;
exportgraphics(fig, fullfile(outDir, 'kappa_across_models.png'), 'Resolution', 200);
close(fig);
fig = figure('Color','w');
gscatter(nRules, OA*100, modes, [], [], 8);
xlabel('#Rules'); ylabel('OA (%)'); title('Rules vs Accuracy'); grid on; legend('Location','best');
exportgraphics(fig, fullfile(outDir, 'rules_vs_accuracy.png'), 'Resolution', 200);
close(fig);
% CSV summary
T = table((1:nRuns)', modes, radii, nRules, OA, Kap, ...
'VariableNames', {'Run','Mode','Radius','Rules','OA','Kappa'});
writetable(T, fullfile(outDir, 'summary_scn1.csv'));
end
% ------------------ helpers ------------------
function plot_mfs_before_after(fisBefore, fisAfter, suptitleStr, outPng)
% Plot input MFs before/after in a 2xD tiled layout.
D = numel(fisAfter.Inputs); % assume same D
fig = figure('Color','w','Position',[100 100 1200 420]);
t = tiledlayout(2, D, 'TileSpacing','compact','Padding','compact');
for d = 1:D
nexttile(d); hold on;
try
[xB, yB] = plotmf(fisBefore, 'input', d);
plot(xB, yB, 'LineWidth', 1.0);
catch
% fallback: skip before if not available
end
title(sprintf('Input %d BEFORE', d));
ylim([0 1]); grid on;
nexttile(D + d); hold on;
[xA, yA] = plotmf(fisAfter, 'input', d);
plot(xA, yA, 'LineWidth', 1.0);
title(sprintf('Input %d AFTER', d));
ylim([0 1]); grid on;
end
sgtitle(['MFs ' suptitleStr]);
exportgraphics(fig, outPng, 'Resolution', 200);
close(fig);
end
+17
View File
@@ -0,0 +1,17 @@
function [Xn, mu, sigma] = preprocess_data(X, mu, sigma)
% PREPROCESS Normalize feature matrix using z-score scaling
%
% [Xn, mu, sigma] = preprocess(X)
% [Xn, mu, sigma] = preprocess(X, mu, sigma)
%
% Applies feature-wise standardization (zero mean, unit variance)
% to the predictors only labels Y remain unchanged.
if nargin < 2 || isempty(mu)
mu = mean(X,1);
sigma = std(X,[],1);
end
sigma(sigma==0) = 1; % avoid division by zero
Xn = (X - mu) ./ sigma;
end
Binary file not shown.
+222
View File
@@ -0,0 +1,222 @@
% scenario1.m Assignment 4 (Classification), Scenario 1 (Haberman)
% TSK classification with Subtractive Clustering (SC)
% Modes: (A) class-independent SC, (B) class-dependent SC)
% Uses: split_data, preprocess_data, evaluate_classification, plot_results1
%
% Dataset: ./Datasets/haberman.data
% Columns: [age, op_year, axillary_nodes, class] with class in {1,2}
close all; clear; clc;
% ============================ CONFIGURATION ================================
cfg = struct();
rng(42, 'twister'); % reproducibility
% Data handling
cfg.split = [0.6 0.2 0.2]; % train / val / test (stratified in split_data)
cfg.standardize = true; % z-score features
% SC radii sweep
cfg.radii = [0.20 0.80];
% ANFIS options
cfg.maxEpochs = 100;
cfg.errorGoal = 0;
cfg.initialStep = 0.01;
cfg.stepDecrease = 0.9;
cfg.stepIncrease = 1.1;
cfg.displayANFIS = 0; % quiet
% Modes
cfg.modes = {'class-independent','class-dependent'};
% Output
cfg.outDir = 'figures_scn1';
if ~exist(cfg.outDir,'dir'), mkdir(cfg.outDir); end
% =============================== DATA =====================================
dataPath = './Datasets/haberman.data';
assert(isfile(dataPath), 'Dataset not found at: %s', dataPath);
%
raw = load(dataPath);
assert(size(raw,2) == 4, 'Expected 4 columns in haberman.data');
X = raw(:,1:3);
Y = raw(:,4);
Y = Y(:);
classLabels = unique(Y);
minLabel = min(classLabels); maxLabel = max(classLabels);
% =========================== SPLIT & PREPROCESS ===========================
[trainX, valX, testX, trainY, valY, testY] = split_data(X, Y, cfg.split);
if cfg.standardize
[trainX, mu, sigma] = preprocess_data(trainX);
valX = preprocess_data(valX, mu, sigma);
testX = preprocess_data(testX, mu, sigma);
else
mu = []; sigma = [];
end
% For manual sugfis construction
inRanges = [min(trainX,[],1); max(trainX,[],1)];
% ============================== TRAINING ==================================
results = []; runId = 0;
for m = 1:numel(cfg.modes)
modeName = cfg.modes{m};
for r = 1:numel(cfg.radii)
radius = cfg.radii(r);
runId = runId + 1;
fprintf('\n=== Run %d: mode=%s, radius=%.2f ===\n', runId, modeName, radius);
% ----- Initial FIS -----
switch modeName
case 'class-independent'
% Use new-style API like your colleague
opt = genfisOptions('SubtractiveClustering', ...
'ClusterInfluenceRange', radius);
initFis = genfis(trainX, double(trainY), opt);
% genfis(Subtractive) already builds Sugeno with constant consequents.
case 'class-dependent'
% Our custom builder (fixes colleague's bug: feed only features to subclust)
initFis = build_classdep_fis(trainX, trainY, classLabels, radius, inRanges);
otherwise
error('Unknown mode: %s', modeName);
end
% ----- ANFIS training -----
trData = [trainX double(trainY)];
ckData = [valX double(valY)];
anfisOpts = [cfg.maxEpochs cfg.errorGoal cfg.initialStep cfg.stepDecrease cfg.stepIncrease];
if cfg.displayANFIS
[fisTrained, trError, ~, ~, ckError] = anfis(trData, initFis, anfisOpts, [], ckData);
else
[fisTrained, trError, ~, ~, ckError] = anfis(trData, initFis, anfisOpts, [0 0 0 0], ckData);
end
% ----- Evaluate on test set -----
yhat_cont = evalfis(testX, fisTrained);
yhat = round(yhat_cont);
% clip into valid label range (important for small rulebases)
yhat(yhat < minLabel) = minLabel;
yhat(yhat > maxLabel) = maxLabel;
% Metrics (note: our evaluate expects (yTrue, yPred))
R = evaluate_classification(testY, yhat, classLabels);
% Collect
res = struct();
res.runId = runId;
res.mode = modeName;
res.radius = radius;
res.fis = fisTrained;
res.nRules = numel(fisTrained.rule);
res.metrics = R;
res.trError = trError;
res.ckError = ckError;
res.mu = mu;
res.sigma = sigma;
res.initFis = initFis;
res.yhat = yhat;
res.ytrue = testY;
results = [results; res];
fprintf('Rules: %d | OA: %.2f%% | Kappa: %.3f\n', res.nRules, 100*R.OA, R.Kappa);
end
end
% ============================== PLOTTING ==================================
plot_results1(results, classLabels, cfg);
% =============================== SAVE ALL =================================
save('results_scn1.mat','results','cfg','classLabels','mu','sigma', ...
'trainX','valX','testX','trainY','valY','testY');
fprintf('\nDone. Figures saved in: %s\n', cfg.outDir);
% ============================ LOCAL FUNCTIONS =============================
function fis = build_classdep_fis(X, Y, classLabels, radius, inRanges)
% BUILD_CLASSDEP_FIS class-dependent SC for Sugeno FIS (ANFIS-ready)
% Creates ONE constant output MF PER RULE (required by ANFIS).
% Runs SUBCLUST on FEATURES ONLY for each class.
D = size(X,2);
fis = sugfis('Name','TSK_ClassDependent');
% Inputs with ranges from training data
for d = 1:D
fis = addInput(fis, [inRanges(1,d) inRanges(2,d)], 'Name', sprintf('x%d', d));
end
% Single scalar output y (range just spans label space)
outRange = [min(classLabels) max(classLabels)];
fis = addOutput(fis, outRange, 'Name', 'y');
ruleList = [];
% Build rules class-by-class
for k = 1:numel(classLabels)
c = classLabels(k);
Xi = X(Y==c, :);
if isempty(Xi), continue; end
% Subtractive clustering on class features
[centers, sigmas] = subclust(Xi, radius);
nCl = size(centers,1);
% ---- robust sigma broadcasting to M×D ----
if isscalar(sigmas)
S = repmat(sigmas, nCl, D);
elseif size(sigmas,1) == 1 && size(sigmas,2) == D
S = repmat(sigmas, nCl, 1);
elseif size(sigmas,1) == nCl && size(sigmas,2) == D
S = sigmas;
else
S = repmat(0.5*(inRanges(2,:)-inRanges(1,:)), nCl, 1);
end
% -----------------------------------------
% For each cluster: add one Gaussian MF per input, one constant MF for output,
% and one rule that ties those together (AND=prod).
for i = 1:nCl
antecedentIdx = zeros(1,D);
% Add input MFs for this cluster (and remember their indices)
for d = 1:D
mfName = sprintf('c%d_r%d_x%d', c, i, d);
params = [S(i,d) centers(i,d)]; % [sigma center]
fis = addMF(fis, sprintf('x%d', d), 'gaussmf', params, 'Name', mfName);
antecedentIdx(d) = numel(fis.Inputs(d).MembershipFunctions);
end
% Add ONE output MF (constant) for THIS rule (ANFIS requirement)
outMfName = sprintf('const_c%d_r%d', c, i);
fis = addMF(fis, 'y', 'constant', double(c), 'Name', outMfName);
outIdx = numel(fis.Outputs(1).MembershipFunctions);
% Rule row: [inMFs outMF weight AND=1]
rule = [antecedentIdx, outIdx, 1, 1];
ruleList = [ruleList; rule]; %#ok<AGROW>
end
end
if ~isempty(ruleList)
fis = addRule(fis, ruleList);
end
% Standard TSK operators
fis.AndMethod = 'prod';
fis.OrMethod = 'probor';
fis.ImplicationMethod = 'prod';
fis.AggregationMethod = 'sum';
fis.DefuzzificationMethod = 'wtaver';
end
+39
View File
@@ -0,0 +1,39 @@
function [trainX, valX, testX, trainY, valY, testY] = split_data(X, Y, ratios)
% SPLIT Split dataset into train/validation/test sets (stratified)
%
% [trainX, valX, testX, trainY, valY, testY] = split(X, Y, ratios)
%
% ratios : [trainRatio, valRatio, testRatio] (e.g. [0.6 0.2 0.2])
%
% Stratified split ensures class proportions remain consistent.
if nargin < 3
ratios = [0.6 0.2 0.2];
end
assert(abs(sum(ratios) - 1) < 1e-6, 'Ratios must sum to 1.');
n = size(X,1);
classes = unique(Y);
idxTrain = []; idxVal = []; idxTest = [];
for c = classes'
idx = find(Y == c);
idx = idx(randperm(length(idx))); % randomize within class
nTrain = round(ratios(1)*length(idx));
nVal = round(ratios(2)*length(idx));
idxTrain = [idxTrain; idx(1:nTrain)];
idxVal = [idxVal; idx(nTrain+1:nTrain+nVal)];
idxTest = [idxTest; idx(nTrain+nVal+1:end)];
end
% Shuffle within each subset to mix classes
idxTrain = idxTrain(randperm(length(idxTrain)));
idxVal = idxVal(randperm(length(idxVal)));
idxTest = idxTest(randperm(length(idxTest)));
trainX = X(idxTrain,:); trainY = Y(idxTrain);
valX = X(idxVal,:); valY = Y(idxVal);
testX = X(idxTest,:); testY = Y(idxTest);
end