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/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/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..7786ec2 --- /dev/null +++ b/ai/src/main/kotlin/lab/ai-model/gc/GcAnomalyDetector.kt @@ -0,0 +1,74 @@ +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 GcAnomalyDetector { + + 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(GcAnomalyDetector::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(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/GcTrainer.kt b/ai/src/main/kotlin/lab/ai-model/gc/GcLogisticRegressionTrainer.kt similarity index 71% 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..0813228 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].") @@ -96,14 +87,18 @@ class GcTrainer { 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() + } } } diff --git a/ai/src/main/kotlin/lab/api/ApiController.kt b/ai/src/main/kotlin/lab/api/ApiController.kt index 7ced8c9..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.GcTrainer +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 gcTrainer: GcTrainer + private val gcLogisticRegressionTrainer: GcLogisticRegressionTrainer, + private val gcAnomalyDetector: GcAnomalyDetector, ) { @GetMapping("/api/train") - fun train() { - gcTrainer.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") + } } }