Skip to content

Commit a9f7bb6

Browse files
committed
refactor(evaluation): extract helper methods
1 parent b455ad2 commit a9f7bb6

3 files changed

Lines changed: 64 additions & 48 deletions

File tree

src/main/java/sentiment/evaluation/CrossDomainEvaluator.java

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424
*/
2525
public class CrossDomainEvaluator {
2626

27+
private static final ObjectMapper MAPPER = new ObjectMapper()
28+
.enable(SerializationFeature.INDENT_OUTPUT);
29+
2730
private static final String[] TRAIN_DOMAINS = {"imdb_50k", "amazon_polarity", "yelp"};
2831
private static final String[] TEST_DOMAINS = {"imdb_50k", "amazon_polarity", "yelp"};
2932
private static final String[] ALGORITHMS = {"svm", "naive_bayes", "random_forest", "logistic_regression"};
@@ -196,7 +199,7 @@ public String generateReport() {
196199
var best = getBestGeneralizingModel();
197200
if (best != null) {
198201
sb.append("═══════════════════════════════════════════════════════════════\n");
199-
sb.append("🏆 BEST GENERALIZING MODEL\n");
202+
sb.append("BEST GENERALIZING MODEL\n");
200203
sb.append("═══════════════════════════════════════════════════════════════\n");
201204
sb.append(String.format("Model: %s%n", best.getKey()));
202205
sb.append(String.format("Cross-Domain Avg Accuracy: %.3f%n", best.getValue()));
@@ -214,9 +217,6 @@ public String generateReport() {
214217
* Export to JSON
215218
*/
216219
public void exportToJson(Path outputPath) throws IOException {
217-
ObjectMapper mapper = new ObjectMapper();
218-
mapper.enable(SerializationFeature.INDENT_OUTPUT);
219-
220220
Map<String, Object> export = new LinkedHashMap<>();
221221
export.put("evaluated_at", evaluatedAt.toString());
222222
export.put("domains", Arrays.asList(TRAIN_DOMAINS));
@@ -261,8 +261,8 @@ public void exportToJson(Path outputPath) throws IOException {
261261
export.put("best_generalizing_model", bestModel);
262262
}
263263

264-
mapper.writeValue(outputPath.toFile(), export);
265-
System.out.println("Cross-domain evaluation exported to: " + outputPath);
264+
MAPPER.writeValue(outputPath.toFile(), export);
265+
System.out.println("Cross-domain evaluation exported to: " + outputPath);
266266
}
267267
}
268268

@@ -285,25 +285,25 @@ public static CrossDomainMatrix evaluateAll(
285285
for (String algo : ALGORITHMS) {
286286
Map<String, SentimentClassifier> algoModels = models.get(algo);
287287
if (algoModels == null) {
288-
System.out.println("No models found for algorithm: " + algo);
288+
System.out.println("No models found for algorithm: " + algo);
289289
continue;
290290
}
291291

292292
for (String trainDomain : TRAIN_DOMAINS) {
293293
SentimentClassifier model = algoModels.get(trainDomain);
294294
if (model == null) {
295-
System.out.println("No model found for " + algo + " trained on " + trainDomain);
295+
System.out.println("No model found for " + algo + " trained on " + trainDomain);
296296
continue;
297297
}
298298

299299
for (String testDomain : TEST_DOMAINS) {
300300
currentEval++;
301-
System.out.printf("[%d/%d] Evaluating %s (trained on %s) testing on %s%n",
301+
System.out.printf("[%d/%d] Evaluating %s (trained on %s) -> testing on %s%n",
302302
currentEval, totalEvaluations, algo, trainDomain, testDomain);
303303

304304
List<Dataset> testData = testDatasets.get(testDomain);
305305
if (testData == null) {
306-
System.out.println(" No test data found for " + testDomain);
306+
System.out.println(" No test data found for " + testDomain);
307307
continue;
308308
}
309309

@@ -372,7 +372,7 @@ public static CrossDomainMatrix evaluateAll(
372372
);
373373

374374
matrix.addResult(result);
375-
System.out.printf(" Accuracy: %.3f | Brier: %.3f%s%n",
375+
System.out.printf(" Accuracy: %.3f | Brier: %.3f%s%n",
376376
result.accuracy, result.brierScore,
377377
result.isInDomain ? " (in-domain)" : "");
378378
}
@@ -388,9 +388,6 @@ public static CrossDomainMatrix evaluateAll(
388388
* Uses TrainingMetadata class to ensure correct JSON structure.
389389
*/
390390
private static void persistToModelMetadata(CrossDomainMatrix matrix, Path modelsDir) {
391-
ObjectMapper mapper = new ObjectMapper();
392-
mapper.enable(SerializationFeature.INDENT_OUTPUT);
393-
394391
for (String algo : ALGORITHMS) {
395392
for (String trainDomain : TRAIN_DOMAINS) {
396393
// Build metadata file path
@@ -487,14 +484,14 @@ public static void main(String[] args) {
487484
if (fullData.size() > maxSamplesPerDomain) {
488485
Collections.shuffle(fullData, new Random(42)); // Reproducible sampling
489486
sampledData = fullData.subList(0, maxSamplesPerDomain);
490-
System.out.println("Loaded " + domain + ": " + sampledData.size() + " samples (sampled from " + fullData.size() + ") from " + testFile);
487+
System.out.println("Loaded " + domain + ": " + sampledData.size() + " samples (sampled from " + fullData.size() + ") from " + testFile);
491488
} else {
492-
System.out.println("Loaded " + domain + ": " + sampledData.size() + " samples from " + testFile);
489+
System.out.println("Loaded " + domain + ": " + sampledData.size() + " samples from " + testFile);
493490
}
494491

495492
testDatasets.put(domain, sampledData);
496493
} else {
497-
System.out.println("Test file not found in either:");
494+
System.out.println("Test file not found in either:");
498495
System.out.println(" " + processedTestFile);
499496
System.out.println(" " + rawTestFile);
500497
}
@@ -509,15 +506,15 @@ public static void main(String[] args) {
509506
Map<String, SentimentClassifier> algoModels = sentiment.models.ModelLoader.loadAllForAlgorithm(algo);
510507

511508
if (algoModels.isEmpty()) {
512-
System.out.println("No models found for " + algo);
509+
System.out.println("No models found for " + algo);
513510
} else {
514511
models.put(algo, algoModels);
515-
System.out.println("Loaded " + algoModels.size() + " " + algo + " model(s)");
512+
System.out.println("Loaded " + algoModels.size() + " " + algo + " model(s)");
516513
}
517514
}
518515

519516
if (models.isEmpty()) {
520-
System.err.println("\n✗ No models found in " + modelsDir);
517+
System.err.println("\nNo models found in " + modelsDir);
521518
System.err.println("Train models first using: ./scripts/train_all_models.sh");
522519
System.exit(1);
523520
}

src/main/java/sentiment/evaluation/FeatureImportanceAnalyzer.java

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,22 @@ public FeatureImportanceResult analyzeFeatureImportance(
9393
}
9494
}
9595

96+
/**
97+
* Falls back to perturbation-based feature importance extraction.
98+
* This method is called when direct coefficient extraction fails for linear SVMs.
99+
*
100+
* @param reason the reason for falling back (logged as a warning)
101+
* @param trainedData the training instances
102+
* @param classifier the classifier to analyze
103+
* @return feature importance map from perturbation analysis
104+
*/
105+
private Map<String, Double> fallbackToPerturbation(
106+
String reason, Instances trainedData, Classifier classifier) {
107+
logger.warn("{}", reason);
108+
logger.warn("Falling back to perturbation method");
109+
return extractFeatureImportance(trainedData, classifier);
110+
}
111+
96112
/**
97113
* Detects if the classifier is a linear SVM (SMO with linear kernel or normalized poly kernel).
98114
*
@@ -160,15 +176,13 @@ private Map<String, Double> extractLinearSVMWeights(Instances trainedData, SMO s
160176
double[][][] sparseWeights = smo.sparseWeights();
161177

162178
if (sparseIndices == null || sparseWeights == null) {
163-
logger.warn("sparseIndices() or sparseWeights() returned null");
164-
logger.warn("Falling back to perturbation method");
165-
return extractFeatureImportance(trainedData, smo);
179+
return fallbackToPerturbation(
180+
"sparseIndices() or sparseWeights() returned null", trainedData, smo);
166181
}
167182

168183
if (sparseIndices.length == 0 || sparseWeights.length == 0) {
169-
logger.warn("Empty sparse arrays - no classifiers found");
170-
logger.warn("Falling back to perturbation method");
171-
return extractFeatureImportance(trainedData, smo);
184+
return fallbackToPerturbation(
185+
"Empty sparse arrays - no classifiers found", trainedData, smo);
172186
}
173187

174188
logger.info("Found {} classifier(s) in SMO", sparseIndices.length);
@@ -186,9 +200,8 @@ private Map<String, Double> extractLinearSVMWeights(Instances trainedData, SMO s
186200
double[][] weightsForClassifier = sparseWeights[0];
187201

188202
if (indicesForClassifier == null || weightsForClassifier == null) {
189-
logger.warn("No sparse data for classifier 0");
190-
logger.warn("Falling back to perturbation method");
191-
return extractFeatureImportance(trainedData, smo);
203+
return fallbackToPerturbation(
204+
"No sparse data for classifier 0", trainedData, smo);
192205
}
193206

194207
logger.info("Classifier 0 has {} class weight vectors", indicesForClassifier.length);
@@ -214,9 +227,8 @@ private Map<String, Double> extractLinearSVMWeights(Instances trainedData, SMO s
214227
}
215228

216229
if (featureIndices == null || featureWeights == null) {
217-
logger.warn("No valid weight vectors found in any class");
218-
logger.warn("Falling back to perturbation method");
219-
return extractFeatureImportance(trainedData, smo);
230+
return fallbackToPerturbation(
231+
"No valid weight vectors found in any class", trainedData, smo);
220232
}
221233

222234
// Extract weights from the sparse representation
@@ -244,17 +256,17 @@ private Map<String, Double> extractLinearSVMWeights(Instances trainedData, SMO s
244256
logger.info("Non-zero feature weights: {} out of {}", nonZeroCount, numAttributes - 1);
245257

246258
if (nonZeroCount == 0) {
247-
logger.warn("All extracted weights are zero! This indicates a problem with coefficient extraction.");
248-
logger.warn("Falling back to perturbation method");
249-
return extractFeatureImportance(trainedData, smo);
259+
return fallbackToPerturbation(
260+
"All extracted weights are zero! This indicates a problem with coefficient extraction.",
261+
trainedData, smo);
250262
}
251263

252264
return weights;
253265

254266
} catch (Exception e) {
255267
logger.error("Failed to extract linear SVM weights: {}", e.getMessage(), e);
256-
logger.warn("Falling back to perturbation method");
257-
return extractFeatureImportance(trainedData, smo);
268+
return fallbackToPerturbation(
269+
"Exception during coefficient extraction", trainedData, smo);
258270
}
259271
}
260272

src/main/java/sentiment/evaluation/StratifiedDataSplitter.java

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,20 @@ public static List<DataSplit> stratifiedKFold(
194194
return cvSplits;
195195
}
196196

197+
/**
198+
* Counts the number of samples per class in the given dataset.
199+
*
200+
* @param data the dataset to count
201+
* @return a map from sentiment label to count
202+
*/
203+
private static Map<Dataset.SentimentLabel, Long> countByClass(List<Dataset> data) {
204+
if (data.isEmpty()) {
205+
return Map.of();
206+
}
207+
return data.stream()
208+
.collect(Collectors.groupingBy(Dataset::getSentiment, Collectors.counting()));
209+
}
210+
197211
/**
198212
* Verifies and logs that stratification preserved class distribution across splits.
199213
*
@@ -208,17 +222,10 @@ private static void verifyStratification(
208222
List<Dataset> val,
209223
List<Dataset> test) {
210224

211-
Map<Dataset.SentimentLabel, Long> origDist = original.stream()
212-
.collect(Collectors.groupingBy(Dataset::getSentiment, Collectors.counting()));
213-
214-
Map<Dataset.SentimentLabel, Long> trainDist = train.stream()
215-
.collect(Collectors.groupingBy(Dataset::getSentiment, Collectors.counting()));
216-
217-
Map<Dataset.SentimentLabel, Long> valDist = val.isEmpty() ? Map.of() : val.stream()
218-
.collect(Collectors.groupingBy(Dataset::getSentiment, Collectors.counting()));
219-
220-
Map<Dataset.SentimentLabel, Long> testDist = test.stream()
221-
.collect(Collectors.groupingBy(Dataset::getSentiment, Collectors.counting()));
225+
Map<Dataset.SentimentLabel, Long> origDist = countByClass(original);
226+
Map<Dataset.SentimentLabel, Long> trainDist = countByClass(train);
227+
Map<Dataset.SentimentLabel, Long> valDist = countByClass(val);
228+
Map<Dataset.SentimentLabel, Long> testDist = countByClass(test);
222229

223230
logger.info("=== Class Distribution Verification ===");
224231
for (Dataset.SentimentLabel label : origDist.keySet()) {

0 commit comments

Comments
 (0)