From 0d0f305e03215e1372cba99eeb387354c552c489 Mon Sep 17 00:00:00 2001 From: rlaope Date: Sat, 18 Oct 2025 16:03:14 +0900 Subject: [PATCH 1/3] =?UTF-8?q?=EC=A0=95=EA=B7=9C=ED=99=94=20=EB=AA=A8?= =?UTF-8?q?=EB=93=88=20=EB=B6=84=EB=A6=AC=20=EB=B0=8F=20=EC=9D=B4=EC=83=81?= =?UTF-8?q?=ED=83=90=EC=A7=80=20=EB=B9=84=EC=A7=80=EB=8F=84=20=EB=AA=A8?= =?UTF-8?q?=EB=8D=B8=EB=8F=84=20=EC=B6=94=EA=B0=80=ED=95=98=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ai/build.gradle.kts | 2 + .../kotlin/lab/ai-model/NormalizationUtil.kt | 16 +++++ .../lab/ai-model/gc/GcAnomalyDetector.kt | 70 +++++++++++++++++++ ...iner.kt => GcLogisticRegressionTrainer.kt} | 25 +++---- ai/src/main/kotlin/lab/api/ApiController.kt | 6 +- 5 files changed, 99 insertions(+), 20 deletions(-) create mode 100644 ai/src/main/kotlin/lab/ai-model/NormalizationUtil.kt create mode 100644 ai/src/main/kotlin/lab/ai-model/gc/GcAnomalyDetector.kt rename ai/src/main/kotlin/lab/ai-model/gc/{GcTrainer.kt => GcLogisticRegressionTrainer.kt} (84%) diff --git a/ai/build.gradle.kts b/ai/build.gradle.kts index dbcf10f..b3bec73 100644 --- a/ai/build.gradle.kts +++ b/ai/build.gradle.kts @@ -24,6 +24,8 @@ dependencies { implementation("com.github.haifengl:smile-core:2.6.0") implementation("com.github.haifengl:smile-plot:2.6.0") + implementation("com.github.haifengl:smile-data:2.6.0") + implementation("com.github.haifengl:smile-anomaly:2.6.0") testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") diff --git a/ai/src/main/kotlin/lab/ai-model/NormalizationUtil.kt b/ai/src/main/kotlin/lab/ai-model/NormalizationUtil.kt new file mode 100644 index 0000000..f6f11d2 --- /dev/null +++ b/ai/src/main/kotlin/lab/ai-model/NormalizationUtil.kt @@ -0,0 +1,16 @@ +package lab.`ai-model` + +object NormalizationUtil { + fun normalize(features: Array): Array { + val numFeatures = features.first().size + val minVals = DoubleArray(numFeatures) { idx -> features.minOf { it[idx] } } + val maxVals = DoubleArray(numFeatures) { idx -> features.maxOf { it[idx] } } + + return features.map { f -> + DoubleArray(numFeatures) { i -> + if (maxVals[i] == minVals[i]) 0.0 + else (f[i] - minVals[i]) / (maxVals[i] - minVals[i]) + } + }.toTypedArray() + } +} diff --git a/ai/src/main/kotlin/lab/ai-model/gc/GcAnomalyDetector.kt b/ai/src/main/kotlin/lab/ai-model/gc/GcAnomalyDetector.kt new file mode 100644 index 0000000..bdf2fad --- /dev/null +++ b/ai/src/main/kotlin/lab/ai-model/gc/GcAnomalyDetector.kt @@ -0,0 +1,70 @@ +package lab.`ai-model`.gc + +import lab.`ai-model`.NormalizationUtil +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Component +import smile.clustering.KMeans +import smile.math.distance.EuclideanDistance +import java.io.File +import java.io.ObjectOutputStream + +@Component +class GcAnomalyDetectorKMeans { + + private val extractor: GcFeatureExtractor by lazy { GcFeatureExtractor } + private val normalizationUtil: NormalizationUtil by lazy { NormalizationUtil } + + private val projectRootDir: String = System.getProperty("user.dir") + private val modelDir = File("$projectRootDir/ai-models/gc-anomaly").apply { mkdirs() } + + private lateinit var model: KMeans + + private val log = LoggerFactory.getLogger(GcAnomalyDetectorKMeans::class.java) + + fun train(k: Int = 3) { + log.info("Start GcAnomalyDetector(KMeans) training...") + + val dataList = getDataList() + if (dataList.isEmpty()) { + log.warn("No data available for training.") + return + } + + val features = dataList.map { extractor.extract(it) }.toTypedArray() + val normalized = normalizationUtil.normalize(features) + + // KMeans ํ•™์Šต + model = KMeans.fit(normalized, k) + log.info("โœ… KMeans training completed with $k clusters.") + saveModel() + } + + fun predict(data: GcTrainData): Double { + val dist = EuclideanDistance() + val features = extractor.extract(data) + val cluster = model.predict(features) + val centroid = model.centroids[cluster] + val distance = dist.d(features, centroid) + log.info("๐Ÿ”Ž Distance to cluster center: %.4f".format(distance)) + return distance + } + + private fun saveModel() { + val file = File(modelDir, "gc_anomaly_kmeans.model") + ObjectOutputStream(file.outputStream().buffered()).use { oos -> + oos.writeObject(model) + } + log.info("๐Ÿ’พ Saved KMeans anomaly model โ†’ ${file.absolutePath}") + } + + private fun getDataList(): List { + // TODO - ์‹ค์ œ exporter ๋ฐ์ดํ„ฐ ์—ฐ๊ฒฐ + return listOf( + GcTrainData(120, 500, 40, 1.2, 400_000, "G1", label = 1), + GcTrainData(200, 800, 350, 3.8, 1_200_000, "G1", label = 1), + GcTrainData(85, 260, 12, 0.9, 250_000, "Parallel", label = 1), + GcTrainData(600, 1500, 900, 6.8, 2_200_000, "G1", label = 1), + GcTrainData(95, 340, 18, 1.4, 370_000, "Serial", label = 1) + ) + } +} diff --git a/ai/src/main/kotlin/lab/ai-model/gc/GcTrainer.kt b/ai/src/main/kotlin/lab/ai-model/gc/GcLogisticRegressionTrainer.kt similarity index 84% rename from ai/src/main/kotlin/lab/ai-model/gc/GcTrainer.kt rename to ai/src/main/kotlin/lab/ai-model/gc/GcLogisticRegressionTrainer.kt index 2f16777..0786063 100644 --- a/ai/src/main/kotlin/lab/ai-model/gc/GcTrainer.kt +++ b/ai/src/main/kotlin/lab/ai-model/gc/GcLogisticRegressionTrainer.kt @@ -1,5 +1,6 @@ package lab.`ai-model`.gc +import lab.`ai-model`.NormalizationUtil import org.slf4j.LoggerFactory import org.springframework.stereotype.Component import smile.classification.LogisticRegression @@ -12,14 +13,17 @@ import java.io.ObjectOutputStream import java.util.* @Component -class GcTrainer { +class GcLogisticRegressionTrainer { private val extractor: GcFeatureExtractor by lazy { GcFeatureExtractor } - private lateinit var model: LogisticRegression - private val log = LoggerFactory.getLogger(GcTrainer::class.java) + private val normalizationUtil: NormalizationUtil by lazy { NormalizationUtil } private val projectRootDir: String = System.getProperty("user.dir") private val modelDir = File("$projectRootDir/ai-models/gc-model").apply { mkdirs() } + private lateinit var model: LogisticRegression + + private val log = LoggerFactory.getLogger(GcLogisticRegressionTrainer::class.java) + fun train() { log.info("Start GcTrainer training...") val dataList = getDataList() @@ -37,7 +41,7 @@ class GcTrainer { log.info("Sample training data: ${dataList.take(3)}") val features = dataList.map { extractor.extract(it) }.toTypedArray() - val normalizedFeatures = normalize(features) + val normalizedFeatures = normalizationUtil.normalize(features) val labels = dataList.map { it.label }.toIntArray() val df = DataFrame.of( @@ -68,19 +72,6 @@ class GcTrainer { saveModel("test") } - private fun normalize(features: Array): Array { - val numFeatures = features.first().size - val minVals = DoubleArray(numFeatures) { idx -> features.minOf { it[idx] } } - val maxVals = DoubleArray(numFeatures) { idx -> features.maxOf { it[idx] } } - - return features.map { f -> - DoubleArray(numFeatures) { i -> - if (maxVals[i] == minVals[i]) 0.0 - else (f[i] - minVals[i]) / (maxVals[i] - minVals[i]) - } - }.toTypedArray() - } - private fun saveModel(key: String) { val m = model ?: run { log.error("Model not trained. Cannot save [$key].") diff --git a/ai/src/main/kotlin/lab/api/ApiController.kt b/ai/src/main/kotlin/lab/api/ApiController.kt index 7ced8c9..b23c85e 100644 --- a/ai/src/main/kotlin/lab/api/ApiController.kt +++ b/ai/src/main/kotlin/lab/api/ApiController.kt @@ -1,16 +1,16 @@ package lab.api -import lab.`ai-model`.gc.GcTrainer +import lab.`ai-model`.gc.GcLogisticRegressionTrainer import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController @RestController class ApiController( - private val gcTrainer: GcTrainer + private val gcLogisticRegressionTrainer: GcLogisticRegressionTrainer ) { @GetMapping("/api/train") fun train() { - gcTrainer.train() + gcLogisticRegressionTrainer.train() } } From c3009d39a9cfa6b41afc9dd92ab10cb64d26ba27 Mon Sep 17 00:00:00 2001 From: rlaope Date: Sat, 18 Oct 2025 16:06:35 +0900 Subject: [PATCH 2/3] =?UTF-8?q?paramter=EB=B3=84=EB=A1=9C=20=EB=AA=A8?= =?UTF-8?q?=EB=8D=B8=20=ED=95=99=EC=8A=B5=20=ED=8A=B8=EB=A6=AC=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ai/src/main/kotlin/lab/Application.kt | 4 +++- .../kotlin/lab/ai-model/gc/GcAnomalyDetector.kt | 4 ++-- ai/src/main/kotlin/lab/api/ApiController.kt | 13 ++++++++++--- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/ai/src/main/kotlin/lab/Application.kt b/ai/src/main/kotlin/lab/Application.kt index 8a7b033..b442ad4 100644 --- a/ai/src/main/kotlin/lab/Application.kt +++ b/ai/src/main/kotlin/lab/Application.kt @@ -3,7 +3,9 @@ package lab import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication -@SpringBootApplication +@SpringBootApplication( + scanBasePackages = ["lab.api", "lab.`ai-model`"] +) class Application fun main(args: Array) { diff --git a/ai/src/main/kotlin/lab/ai-model/gc/GcAnomalyDetector.kt b/ai/src/main/kotlin/lab/ai-model/gc/GcAnomalyDetector.kt index bdf2fad..a93af1c 100644 --- a/ai/src/main/kotlin/lab/ai-model/gc/GcAnomalyDetector.kt +++ b/ai/src/main/kotlin/lab/ai-model/gc/GcAnomalyDetector.kt @@ -9,7 +9,7 @@ import java.io.File import java.io.ObjectOutputStream @Component -class GcAnomalyDetectorKMeans { +class GcAnomalyDetector { private val extractor: GcFeatureExtractor by lazy { GcFeatureExtractor } private val normalizationUtil: NormalizationUtil by lazy { NormalizationUtil } @@ -19,7 +19,7 @@ class GcAnomalyDetectorKMeans { private lateinit var model: KMeans - private val log = LoggerFactory.getLogger(GcAnomalyDetectorKMeans::class.java) + private val log = LoggerFactory.getLogger(GcAnomalyDetector::class.java) fun train(k: Int = 3) { log.info("Start GcAnomalyDetector(KMeans) training...") diff --git a/ai/src/main/kotlin/lab/api/ApiController.kt b/ai/src/main/kotlin/lab/api/ApiController.kt index b23c85e..1855e0a 100644 --- a/ai/src/main/kotlin/lab/api/ApiController.kt +++ b/ai/src/main/kotlin/lab/api/ApiController.kt @@ -1,16 +1,23 @@ package lab.api +import lab.`ai-model`.gc.GcAnomalyDetector import lab.`ai-model`.gc.GcLogisticRegressionTrainer import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController @RestController class ApiController( - private val gcLogisticRegressionTrainer: GcLogisticRegressionTrainer + private val gcLogisticRegressionTrainer: GcLogisticRegressionTrainer, + private val gcAnomalyDetector: GcAnomalyDetector, ) { @GetMapping("/api/train") - fun train() { - gcLogisticRegressionTrainer.train() + fun train(@RequestParam trainModelName: String?) { + when(trainModelName) { + "gc_logistic_regression" -> gcLogisticRegressionTrainer.train() + "gc_anomaly_detector" -> gcAnomalyDetector.train() + else -> throw IllegalArgumentException("Unknown model name: $trainModelName") + } } } From 02eefc49a3ead0d0034a1305f92fab54f46758f9 Mon Sep 17 00:00:00 2001 From: rlaope Date: Sat, 18 Oct 2025 16:07:43 +0900 Subject: [PATCH 3/3] =?UTF-8?q?test=20set=20and=20label=EB=A1=9C=20feature?= =?UTF-8?q?=20toggle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lab/ai-model/gc/GcAnomalyDetector.kt | 22 +++++++++++-------- .../gc/GcLogisticRegressionTrainer.kt | 22 +++++++++++-------- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/ai/src/main/kotlin/lab/ai-model/gc/GcAnomalyDetector.kt b/ai/src/main/kotlin/lab/ai-model/gc/GcAnomalyDetector.kt index a93af1c..7786ec2 100644 --- a/ai/src/main/kotlin/lab/ai-model/gc/GcAnomalyDetector.kt +++ b/ai/src/main/kotlin/lab/ai-model/gc/GcAnomalyDetector.kt @@ -57,14 +57,18 @@ class GcAnomalyDetector { log.info("๐Ÿ’พ Saved KMeans anomaly model โ†’ ${file.absolutePath}") } - private fun getDataList(): List { - // TODO - ์‹ค์ œ exporter ๋ฐ์ดํ„ฐ ์—ฐ๊ฒฐ - return listOf( - GcTrainData(120, 500, 40, 1.2, 400_000, "G1", label = 1), - GcTrainData(200, 800, 350, 3.8, 1_200_000, "G1", label = 1), - GcTrainData(85, 260, 12, 0.9, 250_000, "Parallel", label = 1), - GcTrainData(600, 1500, 900, 6.8, 2_200_000, "G1", label = 1), - GcTrainData(95, 340, 18, 1.4, 370_000, "Serial", label = 1) - ) + private fun getDataList(isTestSet: Boolean = true): List { + return if(isTestSet) { + listOf( + GcTrainData(120, 500, 40, 1.2, 400_000, "G1", label = 1), + GcTrainData(200, 800, 350, 3.8, 1_200_000, "G1", label = 1), + GcTrainData(85, 260, 12, 0.9, 250_000, "Parallel", label = 1), + GcTrainData(600, 1500, 900, 6.8, 2_200_000, "G1", label = 1), + GcTrainData(95, 340, 18, 1.4, 370_000, "Serial", label = 1) + ) + } else { + // TODO heesung feature + listOf() + } } } diff --git a/ai/src/main/kotlin/lab/ai-model/gc/GcLogisticRegressionTrainer.kt b/ai/src/main/kotlin/lab/ai-model/gc/GcLogisticRegressionTrainer.kt index 0786063..0813228 100644 --- a/ai/src/main/kotlin/lab/ai-model/gc/GcLogisticRegressionTrainer.kt +++ b/ai/src/main/kotlin/lab/ai-model/gc/GcLogisticRegressionTrainer.kt @@ -87,14 +87,18 @@ class GcLogisticRegressionTrainer { log.info("๐Ÿ’พ Saved model [$key] โ†’ ${file.absolutePath}") } - private fun getDataList(): List { - // TODO - khope heesung์ด ๋งŒ๋“ค์–ด์ค€ data get์—์„œ ๊ฐ€์ ธ์™€์“ฐ๋Š”๊ฑธ๋กœ ์ˆ˜์ • - return listOf( - GcTrainData(100, 400, 30, 1.2, 300_000, "G1", label = 1), - GcTrainData(150, 700, 300, 3.8, 1_000_000, "G1", label = 0), - GcTrainData(80, 250, 15, 0.8, 200_000, "Parallel", label = 1), - GcTrainData(400, 1200, 700, 6.2, 2_000_000, "G1", label = 0), - GcTrainData(90, 320, 20, 1.5, 350_000, "Serial", label = 1) - ) + private fun getDataList(isTestSet: Boolean = true): List { + return if(isTestSet) { + listOf( + GcTrainData(120, 500, 40, 1.2, 400_000, "G1", label = 1), + GcTrainData(200, 800, 350, 3.8, 1_200_000, "G1", label = 1), + GcTrainData(85, 260, 12, 0.9, 250_000, "Parallel", label = 1), + GcTrainData(600, 1500, 900, 6.8, 2_200_000, "G1", label = 1), + GcTrainData(95, 340, 18, 1.4, 370_000, "Serial", label = 1) + ) + } else { + // TODO heesung feature + listOf() + } } }