diff --git a/src/main/java/com/kamco/cd/training/filemanager/service/FileManagerService.java b/src/main/java/com/kamco/cd/training/filemanager/service/FileManagerService.java index 4e92f67..2bdb020 100644 --- a/src/main/java/com/kamco/cd/training/filemanager/service/FileManagerService.java +++ b/src/main/java/com/kamco/cd/training/filemanager/service/FileManagerService.java @@ -622,7 +622,6 @@ public class FileManagerService { final int[] fileCount = {0}; final int[] directoryCount = {0}; - log.info("[StorageSpace] walkFileTree 시작 - path={}", directoryPath); try { Files.walkFileTree( directory, @@ -642,12 +641,6 @@ public class FileManagerService { return FileVisitResult.CONTINUE; } }); - log.info( - "[StorageSpace] walkFileTree 완료 - fileCount={}, dirCount={}, usedSize={} bytes ({})", - fileCount[0], - directoryCount[0], - usedSize[0], - formatFileSize(usedSize[0])); } catch (IOException e) { log.error("디렉토리 용량 계산 중 오류 발생: {}", directoryPath, e); return FileManagerDto.StorageSpaceRes.builder() @@ -666,7 +659,7 @@ public class FileManagerService { .build(); } - // 디스크 공간 정보 조회 (FileStore 사용) — basePath가 속한 파티션/NFS 마운트 전체 기준 + // 디스크 공간 정보 조회 (FileStore 사용) long totalDiskSpace = 0; long freeSpace = 0; long usableSpace = 0; @@ -674,28 +667,17 @@ public class FileManagerService { try { java.nio.file.FileStore fileStore = Files.getFileStore(directory); - totalDiskSpace = fileStore.getTotalSpace(); - freeSpace = fileStore.getUnallocatedSpace(); // OS 미할당 공간 (root 예약 블록 포함 가능) - usableSpace = fileStore.getUsableSpace(); // 실제 프로세스가 사용 가능한 공간 - log.info( - "[StorageSpace] FileStore - name={}, type={}, total={} bytes ({}), unallocated={} bytes ({}), usable={} bytes ({})", - fileStore.name(), - fileStore.type(), - totalDiskSpace, - formatFileSize(totalDiskSpace), - freeSpace, - formatFileSize(freeSpace), - usableSpace, - formatFileSize(usableSpace)); + totalDiskSpace = fileStore.getTotalSpace(); // 전체 디스크 용량 + freeSpace = fileStore.getUnallocatedSpace(); // 남은 저장공간 (할당되지 않은 공간) + usableSpace = fileStore.getUsableSpace(); // 사용 가능한 공간 (실제 사용 가능) - // 사용률은 usableSpace 기준으로 계산 (더 정확한 실사용 가능 공간 반영) + // 디스크 사용률 계산 if (totalDiskSpace > 0) { - long usedDiskSpace = totalDiskSpace - usableSpace; + long usedDiskSpace = totalDiskSpace - freeSpace; usagePercentage = (usedDiskSpace * 100.0) / totalDiskSpace; } - log.info("[StorageSpace] usagePercentage={}%", Math.round(usagePercentage * 100.0) / 100.0); } catch (IOException e) { - log.warn("[StorageSpace] 디스크 공간 정보 조회 실패: {}", directoryPath, e); + log.debug("디스크 공간 정보 조회 중 오류 발생: {}", directoryPath, e); // 디스크 정보 조회 실패 시에도 디렉토리 용량은 반환 } diff --git a/src/main/java/com/kamco/cd/training/postgres/repository/train/ModelTestMetricsJobRepositoryImpl.java b/src/main/java/com/kamco/cd/training/postgres/repository/train/ModelTestMetricsJobRepositoryImpl.java index 4a30413..ec3685c 100644 --- a/src/main/java/com/kamco/cd/training/postgres/repository/train/ModelTestMetricsJobRepositoryImpl.java +++ b/src/main/java/com/kamco/cd/training/postgres/repository/train/ModelTestMetricsJobRepositoryImpl.java @@ -220,7 +220,20 @@ public class ModelTestMetricsJobRepositoryImpl extends QuerydslRepositorySupport // Train 메트릭 (Loss, LR, Duration) modelMetricsTrainEntity.loss, modelMetricsTrainEntity.lr, - modelMetricsTrainEntity.durationTime))) + modelMetricsTrainEntity.durationTime), + Projections.constructor( + com.kamco.cd.training.train.dto.ModelTrainMetricsDto.TestMetrics.class, + modelMetricsTestEntity.model1, + modelMetricsTestEntity.tp, + modelMetricsTestEntity.fp, + modelMetricsTestEntity.fn, + modelMetricsTestEntity.precisions, + modelMetricsTestEntity.recall, + modelMetricsTestEntity.f1Score, + modelMetricsTestEntity.accuracy, + modelMetricsTestEntity.iou, + modelMetricsTestEntity.detectionCount, + modelMetricsTestEntity.gtCount))) .from(modelMetricsValidationEntity) .innerJoin(modelMasterEntity) .on(modelMetricsValidationEntity.model.id.eq(modelMasterEntity.id)) @@ -228,6 +241,8 @@ public class ModelTestMetricsJobRepositoryImpl extends QuerydslRepositorySupport .on( modelMetricsTrainEntity.model.id.eq(modelMasterEntity.id), modelMetricsTrainEntity.epoch.eq(epoch)) + .leftJoin(modelMetricsTestEntity) + .on(modelMetricsTestEntity.model.id.eq(modelId)) .where( modelMetricsValidationEntity.model.id.eq(modelId), modelMetricsValidationEntity.epoch.eq(epoch)) diff --git a/src/main/java/com/kamco/cd/training/train/dto/ModelTrainMetricsDto.java b/src/main/java/com/kamco/cd/training/train/dto/ModelTrainMetricsDto.java index 05efdef..a659f96 100644 --- a/src/main/java/com/kamco/cd/training/train/dto/ModelTrainMetricsDto.java +++ b/src/main/java/com/kamco/cd/training/train/dto/ModelTrainMetricsDto.java @@ -3,7 +3,6 @@ package com.kamco.cd.training.train.dto; import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; import java.nio.file.Path; -import java.util.Properties; import java.util.UUID; import lombok.AllArgsConstructor; import lombok.Getter; @@ -51,6 +50,53 @@ public class ModelTrainMetricsDto { private Integer epoch; // epoch 번호 private Properties properties; + + @JsonProperty("test_metrics") + private TestMetrics testMetrics; // 테스트 메트릭 추가 + + // 기존 방식용 생성자 (metricType, epoch, testMetrics 없음) + public ModelMetricJsonDto(String cdModelType, String modelVersion, Properties properties) { + this.cdModelType = cdModelType; + this.modelVersion = modelVersion; + this.metricType = null; + this.epoch = null; + this.properties = properties; + this.testMetrics = null; + } + } + + @Getter + @AllArgsConstructor + @NoArgsConstructor + public static class TestMetrics { + + private String model; // 모델명 + + @JsonProperty("true_positive") + private Long tp; // True Positive + + @JsonProperty("false_positive") + private Long fp; // False Positive + + @JsonProperty("false_negative") + private Long fn; // False Negative + + private Float precisions; // Precision + + private Float recall; // Recall + + @JsonProperty("f1_score") + private Float f1Score; // F1 Score + + private Float accuracy; // Accuracy + + private Float iou; // IoU + + @JsonProperty("detection_count") + private Long detectionCount; // Detection Count + + @JsonProperty("gt_count") + private Long gtCount; // Ground Truth Count } @Getter @@ -106,6 +152,25 @@ public class ModelTrainMetricsDto { @JsonProperty("duration_time") private Float durationTime; + + // 기존 방식용 생성자 (getTestMetricPackingInfo에서 사용) + public Properties(Float f1Score, Float precisions, Float recall, Float iou, Double loss) { + this.changedFscore = f1Score; + this.changedPrecision = precisions; + this.changedRecall = recall; + this.unchangedFscore = null; + this.unchangedPrecision = null; + this.unchangedRecall = null; + this.mFscore = null; + this.mPrecision = null; + this.mRecall = null; + this.mIou = iou; + this.mAcc = null; + this.aAcc = null; + this.loss = loss; + this.lr = null; + this.durationTime = null; + } } @Getter diff --git a/src/main/java/com/kamco/cd/training/train/service/ModelTestMetricsJobService.java b/src/main/java/com/kamco/cd/training/train/service/ModelTestMetricsJobService.java index 152e1d2..69de8ef 100644 --- a/src/main/java/com/kamco/cd/training/train/service/ModelTestMetricsJobService.java +++ b/src/main/java/com/kamco/cd/training/train/service/ModelTestMetricsJobService.java @@ -6,6 +6,7 @@ import com.kamco.cd.training.common.enums.TrainStatusType; import com.kamco.cd.training.postgres.core.ModelTestMetricsJobCoreService; import com.kamco.cd.training.train.dto.ModelTrainMetricsDto.BestPthInfo; import com.kamco.cd.training.train.dto.ModelTrainMetricsDto.ModelMetricJsonDto; +import com.kamco.cd.training.train.dto.ModelTrainMetricsDto.ModelTestFileName; import com.kamco.cd.training.train.dto.ModelTrainMetricsDto.ResponsePathDto; import java.io.BufferedReader; import java.io.IOException; @@ -18,6 +19,7 @@ import java.nio.file.StandardOpenOption; import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.List; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -156,14 +158,19 @@ public class ModelTestMetricsJobService { modelInfo.getModelId(), bestPthFiles.stream().map(BestPthInfo::getFileName).collect(Collectors.toList())); - // 2. 각 Best PTH별로 개별 ZIP 생성 + // 2. 기존 방식: F-Score 기준 통합 ZIP 생성 (1개) + createLegacyZipFile(modelInfo, responsePath); + + // 3. 신규 방식: 각 Best PTH별로 개별 ZIP 생성 (3개) createIndividualZipFiles(modelInfo, bestPthFiles, responsePath); modelTestMetricsJobCoreService.updatePackingEnd( modelInfo.getModelId(), ZonedDateTime.now(), TrainStatusType.COMPLETED.getId()); log.info( - "모든 ZIP 파일 생성 완료: modelId={}, zipCount={}", modelInfo.getModelId(), bestPthFiles.size()); + "모든 ZIP 파일 생성 완료: modelId={}, 기존방식=1개, 개별방식={}개", + modelInfo.getModelId(), + bestPthFiles.size()); } catch (Exception e) { log.error("ZIP 파일 생성 실패: modelId={}", modelInfo.getModelId(), e); @@ -213,14 +220,109 @@ public class ModelTestMetricsJobService { } /** - * Response 폴더에서 모든 best_changed_*.pth 파일 찾기 + * 기존 방식: F-Score 기준 통합 ZIP 파일 생성 파일명: {modelNo}.{modelVer}.{uuid}.zip 포함 파일: model_config.py, + * best_changed_fscore_epoch_N.pth, {modelVersion}.json, yolov8_6th-6m.pt + * + * @param modelInfo 모델 정보 + * @param responsePath Response 디렉토리 경로 + */ + private void createLegacyZipFile(ResponsePathDto modelInfo, Path responsePath) { + try { + log.info("기존 방식 ZIP 파일 생성 시작: modelId={}", modelInfo.getModelId()); + + // 1. Test 메트릭 기반 JSON 생성 (기존 getTestMetricPackingInfo 사용) + ModelMetricJsonDto jsonDto = + modelTestMetricsJobCoreService.getTestMetricPackingInfo(modelInfo.getModelId()); + + if (jsonDto == null) { + log.warn("Test 메트릭 정보를 찾을 수 없습니다: modelId={}", modelInfo.getModelId()); + return; + } + + // 2. JSON 파일명: {modelVersion}.json (예: G4_000001.json) + String legacyJsonFileName = jsonDto.getModelVersion() + ".json"; + Path legacyJsonPath = responsePath.resolve(legacyJsonFileName); + writeJsonFile(jsonDto, legacyJsonPath); + + log.info("JSON 파일 생성 완료: {}", legacyJsonFileName); + + // 3. Best Epoch 파일명 찾기 (F-Score 기준) + ModelTestFileName fileInfo = + modelTestMetricsJobCoreService.findModelTestFileNames(modelInfo.getModelId()); + + if (fileInfo == null || fileInfo.getBestEpochFileName() == null) { + log.warn("Best Epoch 파일명을 찾을 수 없습니다: modelId={}", modelInfo.getModelId()); + return; + } + + log.info("Best Epoch 파일명: {}.pth", fileInfo.getBestEpochFileName()); + + // 4. ZIP에 포함할 파일 리스트 + Set targetNames = + Set.of( + "model_config.py", + fileInfo.getBestEpochFileName() + ".pth", // best_changed_fscore_epoch_N.pth + legacyJsonFileName); // {modelVersion}.json + + List filesToZip = new ArrayList<>(); + + // Response 폴더에서 파일 수집 + try (Stream stream = Files.list(responsePath)) { + filesToZip.addAll( + stream + .filter(Files::isRegularFile) + .filter(p -> targetNames.contains(p.getFileName().toString())) + .collect(Collectors.toList())); + } + + log.info("Response 폴더에서 수집한 파일 {}개", filesToZip.size()); + + // PT 파일 추가 (사전학습 모델) + Path ptPath = findPretrainedModel(); + if (ptPath != null) { + filesToZip.add(ptPath); + log.info("사전학습 모델 추가: {}", ptPath.getFileName()); + } else { + log.warn("사전학습 모델(.pt) 파일을 찾을 수 없습니다."); + } + + // 5. ZIP 파일명 생성: {modelNo}.{modelVer}.{uuid}.zip + String legacyZipFileName = + String.format( + "%s.zip", jsonDto.getModelVersion() // G4.G4_000001.uuid + ); + + Path legacyZipPath = responsePath.resolve(legacyZipFileName); + + // 6. ZIP 압축 + zipFiles(filesToZip, legacyZipPath); + + long zipSize = Files.size(legacyZipPath); + log.info("기존 방식 ZIP 파일 생성 완료: fileName={}, size={} bytes", legacyZipFileName, zipSize); + + } catch (Exception e) { + log.error("기존 방식 ZIP 파일 생성 실패: modelId={}", modelInfo.getModelId(), e); + // 에러가 발생해도 신규 방식 ZIP은 계속 생성되도록 throw 하지 않음 + } + } + + /** + * Response 폴더에서 모든 best_changed_*.pth 파일 찾기 패턴: best_changed_{metricType}_epoch_{N}.pth 예: + * best_changed_fscore_epoch_3.pth, best_changed_precision_epoch_2.pth, + * best_changed_accuracy_epoch_5.pth 등 * * @param responsePath Response 디렉토리 경로 * @return Best PTH 파일 정보 리스트 */ private List findAllBestPthFiles(Path responsePath) throws IOException { List bestFiles = new ArrayList<>(); - Pattern pattern = Pattern.compile("best_changed_(fscore|precision|recall)_epoch_(\\d+)\\.pth"); + + // 개선: 모든 메트릭 타입을 자동으로 감지하는 유연한 패턴 + // best_changed_{어떤문자든}_epoch_{숫자}.pth 형식의 모든 파일 검색 + Pattern pattern = Pattern.compile("best_changed_(.+?)_epoch_(\\d+)\\.pth"); + + log.info("📂 Best PTH 파일 검색 시작: path={}", responsePath); + log.info("🔍 검색 패턴: best_changed_{{metricType}}_epoch_{{N}}.pth"); try (Stream stream = Files.list(responsePath)) { stream @@ -230,14 +332,47 @@ public class ModelTestMetricsJobService { String fileName = file.getFileName().toString(); Matcher matcher = pattern.matcher(fileName); if (matcher.matches()) { - String metricType = matcher.group(1); // fscore, precision, recall + String metricType = + matcher.group(1); // 메트릭 타입 (fscore, precision, recall, accuracy 등) Integer epoch = Integer.parseInt(matcher.group(2)); + log.info( + "Best PTH 파일 발견: file={}, metricType={}, epoch={}", + fileName, + metricType, + epoch); + bestFiles.add(new BestPthInfo(fileName, metricType, epoch, file)); } }); } + log.info("Best PTH 파일 검색 완료: 총 {}개 발견", bestFiles.size()); + + if (bestFiles.isEmpty()) { + log.warn(" Best PTH 파일이 하나도 발견되지 않았습니다. 파일명 패턴을 확인하세요."); + log.warn(" 예상 패턴: best_changed_{{metricType}}_epoch_{{N}}.pth"); + + // 디버깅: response 폴더의 모든 .pth 파일 출력 + try (Stream debugStream = Files.list(responsePath)) { + List allPthFiles = + debugStream + .filter(Files::isRegularFile) + .map(p -> p.getFileName().toString()) + .filter(name -> name.endsWith(".pth")) + .collect(Collectors.toList()); + + if (!allPthFiles.isEmpty()) { + log.info(" Response 폴더의 .pth 파일 목록:"); + allPthFiles.forEach(name -> log.info(" - {}", name)); + } else { + log.warn(" Response 폴더에 .pth 파일이 전혀 없습니다."); + } + } catch (IOException e) { + log.error("디버깅 중 에러 발생", e); + } + } + return bestFiles; } @@ -268,6 +403,13 @@ public class ModelTestMetricsJobService { for (BestPthInfo bestPth : bestPthFiles) { try { + log.info( + "ZIP 파일 생성 시작: modelId={}, metricType={}, epoch={}, pthFile={}", + modelInfo.getModelId(), + bestPth.getMetricType(), + bestPth.getEpoch(), + bestPth.getFileName()); + // 1. 메트릭 JSON 생성 ModelMetricJsonDto metricJson = modelTestMetricsJobCoreService.getMetricsByEpoch( @@ -282,10 +424,20 @@ public class ModelTestMetricsJobService { continue; } + log.info( + "메트릭 JSON 조회 성공: modelId={}, metricType={}, epoch={}, cdModelType={}, modelVersion={}", + modelInfo.getModelId(), + metricJson.getMetricType(), + metricJson.getEpoch(), + metricJson.getCdModelType(), + metricJson.getModelVersion()); + String jsonFileName = bestPth.getMetricType() + "_metrics.json"; Path jsonPath = responsePath.resolve(jsonFileName); writeJsonFile(metricJson, jsonPath); + log.info("JSON 파일 생성 완료: {}", jsonFileName); + // 2. ZIP에 포함할 파일 리스트 List filesToZip = new ArrayList<>(); filesToZip.add(modelConfigPath); // model_config.py @@ -293,25 +445,32 @@ public class ModelTestMetricsJobService { filesToZip.add(jsonPath); // {type}_metrics.json filesToZip.add(ptPath); // yolov8_6th-6m.pt - // 3. ZIP 파일명 생성 + // 3. ZIP 파일명 생성 (하이퍼파라미터별 구분) String zipFileName = String.format( "%s.%s.zip", - metricJson.getModelVersion(), // ex) G1.G1_000001.dfdaj-dlks-fjad-dkdajfl - bestPth.getMetricType()); // ex) fscore/precision/recall + metricJson.getModelVersion(), // G1.G1_000001.{uuid} + bestPth.getMetricType()); // fscore/precision/recall Path zipPath = responsePath.resolve(zipFileName); // 4. ZIP 압축 zipFiles(filesToZip, zipPath); - log.info("ZIP 파일 생성 완료: {}", zipPath); + long zipSize = Files.size(zipPath); + log.info( + "ZIP 파일 생성 완료: path={}, size={} bytes, metricType={}, epoch={}", + zipPath, + zipSize, + bestPth.getMetricType(), + bestPth.getEpoch()); } catch (Exception e) { log.error( - "ZIP 파일 생성 실패: metricType={}, epoch={}", + "ZIP 파일 생성 실패: metricType={}, epoch={}, pthFile={}", bestPth.getMetricType(), bestPth.getEpoch(), + bestPth.getFileName(), e); throw new RuntimeException(e); }