Ver3.0 ๐ฑ ๋ชจ๋ฐ์ผ ๋จธ์ ๋ฌ๋๊ณผ TensorFlow Lite๋ก ๋ง๋๋ ์จ๋๋ฐ์ด์ค AI์ ์ธ๊ณ

๐ฑ ๋ชจ๋ฐ์ผ ๋จธ์ ๋ฌ๋๊ณผ TensorFlow Lite๋ก ๋ง๋๋ ์จ๋๋ฐ์ด์ค AI์ ์ธ๊ณ
์ค๋งํธํฐ ์์์ ํผ์ณ์ง๋ ์ธ๊ณต์ง๋ฅ์ ๋ง๋ฒ, ์ง๊ธ ๋ฐ๋ก ์์ํด๋ณด์ธ์!
์ฌ๋ฌ๋ถ, ์์ฆ ์ค๋งํธํฐ์ผ๋ก ์ฌ์ง ์ฐ์ผ๋ฉด ์๋์ผ๋ก ๋ฐฐ๊ฒฝ์ด ํ๋ ค์ง๊ณ , ์ผ๊ตด ์ธ์์ด ์์๊ฐ์ ๋๊ณ , ์์ฑ ๋ช
๋ น์ ๋ด๋ฆฌ๋ฉด ๋ฐ๋ก ๋ฐ์ํ๋ ๊ฑฐ ์ ๊ธฐํ์ง ์๋์? ๐ค
์ด ๋ชจ๋ ๊ฒ ๋ฐ๋ก ์จ๋๋ฐ์ด์ค AI ๋๋ถ์ด์์. ํด๋ผ์ฐ๋ ์๋ฒ์ ๋ฐ์ดํฐ๋ฅผ ๋ณด๋ด์ง ์๊ณ ๋, ์ฌ๋ฌ๋ถ์ ์์์ ์๋ ์์ ๊ธฐ๊ธฐ๊ฐ ์ค์ค๋ก ์๊ฐํ๊ณ ํ๋จํ๋ ๊ฑฐ์ฃ !
์ค๋์ ์ด ๋๋ผ์ด ๊ธฐ์ ์ ํต์ฌ์ธ TensorFlow Lite๋ฅผ ํ์ฉํ ๋ชจ๋ฐ์ผ ๋จธ์ ๋ฌ๋์ ๋ํด ์น๊ตฌ์ฒ๋ผ ํธํ๊ฒ ์ด์ผ๊ธฐํด๋ณผ๊ฒ์. ํ๋ก๊ทธ๋๋ฐ์ ๊ด์ฌ ์๋ ๋ถ๋ค์ด๋ผ๋ฉด, ์ด ๊ธ์ ์ฝ๊ณ ๋๋ฉด ๋ฐ๋ก ์ฌ๋ฌ๋ถ์ ์ฑ์ AI๋ฅผ ํ์ฌํ๊ณ ์ถ์ด์ง ๊ฑฐ์์! ๐
๐ฏ ์จ๋๋ฐ์ด์ค AI๊ฐ ๋ญ๊ธธ๋?
๋จผ์ ๊ธฐ๋ณธ๋ถํฐ ์ง๊ณ ๋์ด๊ฐ๊ฒ์. ์จ๋๋ฐ์ด์ค AI(On-Device AI)๋ ๋ง ๊ทธ๋๋ก '๊ธฐ๊ธฐ ์์์ ๋์๊ฐ๋ ์ธ๊ณต์ง๋ฅ'์ด์์.
์ ํต์ ์ธ AI ์๋น์ค๋ ์ด๋ ๊ฒ ์๋ํ์ด์:
1๏ธโฃ ์ฌ์ฉ์๊ฐ ์ค๋งํธํฐ์์ ์ฌ์ง์ ์ฐ์ด์
2๏ธโฃ ๊ทธ ์ฌ์ง์ด ์ธํฐ๋ท์ ํตํด ์๋ฒ๋ก ์ ์ก๋ผ์
3๏ธโฃ ์๋ฒ์ ๊ฐ๋ ฅํ ์ปดํจํฐ๊ฐ AI ๋ชจ๋ธ๋ก ๋ถ์ํด์
4๏ธโฃ ๊ฒฐ๊ณผ๋ฅผ ๋ค์ ์ค๋งํธํฐ์ผ๋ก ๋ณด๋ด์ค์
์ด ๋ฐฉ์์ ๊ฐ๋ ฅํ์ง๋ง ๋ช ๊ฐ์ง ๋ฌธ์ ๊ฐ ์์์ฃ . ์ธํฐ๋ท์ด ์์ผ๋ฉด ์๋ ์ ํ๊ณ , ๋ฐ์ดํฐ ์ ์ก์ ์๊ฐ์ด ๊ฑธ๋ฆฌ๊ณ , ๊ฐ์ธ์ ๋ณด๊ฐ ์ธ๋ถ๋ก ๋๊ฐ๋ ๊ฒ๋ ์ฐ์ฐํ๊ณ ์. ๐ฐ
์๋ฒ ์๋ณต ์๊ฐ์ด ์์ผ๋ ์๋ต์ด ๊ฑฐ์ ์ฆ๊ฐ์ ์ด๊ณ , ๋ฐ์ดํฐ๊ฐ ๊ธฐ๊ธฐ๋ฅผ ๋ฒ์ด๋์ง ์์ผ๋ ํ๋ผ์ด๋ฒ์๋ ์๋ฒฝํ๊ฒ ์ง์ผ์ ธ์. ๊ฒ๋ค๊ฐ ์ธํฐ๋ท ์ฐ๊ฒฐ ์์ด๋ ์๋ํ๋ ์งํ์ฒ ์ด๋ ๋นํ๊ธฐ์์๋ ๋ฌธ์ ์์ฃ !
ํ์ง๋ง ์ฌ๊ธฐ์ ์๋ฌธ์ด ์๊ธฐ์ฃ . "์ค๋งํธํฐ์ด ๊ทธ๋ ๊ฒ ๋๋ํด? ์๋ฒ๊ธ AI๋ฅผ ๋๋ฆด ์ ์์ด?" ๐คจ
์ ๋ต์... ๋ฐ์ ๋ง๊ณ ๋ฐ์ ํ๋ ค์! ์ค๋งํธํฐ์ ์๋ฒ๋งํผ ๊ฐ๋ ฅํ์ง ์์ง๋ง, ์ต๊ทผ ๋ชจ๋ฐ์ผ ํ๋ก์ธ์์ ๋ฐ์ ์ด ์ ๋ง ๋๋ผ์์. ํนํ NPU(Neural Processing Unit)๋ผ๋ AI ์ ์ฉ ์นฉ์ด ํ์ฌ๋๋ฉด์ ํจ์จ์ ์ธ AI ์ฐ์ฐ์ด ๊ฐ๋ฅํด์ก๊ฑฐ๋ ์.
๐ง TensorFlow Lite, ๋ ๋๊ตฌ๋?
์, ์ด์ ์ค๋์ ์ฃผ์ธ๊ณต TensorFlow Lite๋ฅผ ์๊ฐํ ์๊ฐ์ด์์! ๐
TensorFlow๋ ๊ตฌ๊ธ์ด ๋ง๋ ๋จธ์ ๋ฌ๋ ํ๋ ์์ํฌ์ธ๋ฐ์, ์๋๋ ์๋ฒ๋ ๋ฐ์คํฌํฑ์์ ์ฌ์ฉํ๋๋ก ์ค๊ณ๋์ด์. ๊ทธ๋ฐ๋ฐ ์ด๊ฑธ ๋ชจ๋ฐ์ผ์์ ๋๋ฆฌ๋ ค๋๊น ๋๋ฌด ๋ฌด๊ฒ๊ณ ๋ฐฐํฐ๋ฆฌ๋ ๋ง์ด ๋จน๋ ๊ฑฐ์์.
๊ทธ๋์ ๊ตฌ๊ธ์ด ๊ณ ๋ฏผ ๋์ ๋ง๋ ๊ฒ ๋ฐ๋ก TensorFlow Lite์์. ์ด๋ฆ์์ ์ ์ ์๋ฏ์ด '๊ฐ๋ฒผ์ด(Lite)' ๋ฒ์ ์ด์ฃ !
TensorFlow Lite์ ํน์ง
| ํน์ง | ์ค๋ช |
|---|---|
| ๊ฒฝ๋ํ | ๋ชจ๋ธ ํฌ๊ธฐ๋ฅผ ์ต๋ 75%๊น์ง ์ค์ผ ์ ์์ด์. 100MB ๋ชจ๋ธ์ด 25MB๋ก! |
| ์ต์ ํ | ๋ชจ๋ฐ์ผ CPU, GPU, NPU์ ๋ง์ถฐ ์ต์ ํ๋ ์ฐ์ฐ์ ์ํํด์ |
| ์ ์ ๋ ฅ | ๋ฐฐํฐ๋ฆฌ ์๋ชจ๋ฅผ ์ต์ํํ๋ ํจ์จ์ ์ธ ์คํ ๊ตฌ์กฐ |
| ํฌ๋ก์ค ํ๋ซํผ | Android, iOS, ์๋ฒ ๋๋ ๋ฆฌ๋ ์ค ๋ฑ ๋ค์ํ ํ๋ซํผ ์ง์ |
TensorFlow Lite๋ ํฌ๊ฒ ๋ ๊ฐ์ง ๊ตฌ์ฑ ์์๋ก ์ด๋ฃจ์ด์ ธ ์์ด์:
1. TensorFlow Lite Converter ๐
๊ธฐ์กด์ TensorFlow ๋ชจ๋ธ์ ๋ชจ๋ฐ์ผ์์ ์คํ ๊ฐ๋ฅํ .tflite ํ์์ผ๋ก ๋ณํํด์ฃผ๋ ๋๊ตฌ์์. ์ด ๊ณผ์ ์์ ๋ชจ๋ธ์ ์์ถํ๊ณ ์ต์ ํํ์ฃ .
2. TensorFlow Lite Interpreter โ๏ธ
๋ณํ๋ ๋ชจ๋ธ์ ์ค์ ๋ก ๋ชจ๋ฐ์ผ ๊ธฐ๊ธฐ์์ ์คํํ๋ ๋ฐํ์ ์์ง์ด์์. ๊ฐ ํ๋ซํผ์ ํ๋์จ์ด ๊ฐ์์ ํ์ฉํด์ ๋น ๋ฅด๊ฒ ์ถ๋ก (inference)์ ์ํํด์.
๐๏ธ TensorFlow Lite ๋ชจ๋ธ ๋ง๋ค๊ธฐ: ์ฒ์๋ถํฐ ๋๊น์ง
์ด์ ์ค์ ์ผ๋ก ๋ค์ด๊ฐ๋ณผ๊น์? TensorFlow Lite ๋ชจ๋ธ์ ๋ง๋๋ ์ ์ฒด ๊ณผ์ ์ ๋จ๊ณ๋ณ๋ก ์ดํด๋ณผ๊ฒ์. ๊ฑฑ์ ๋ง์ธ์, ํ๋ํ๋ ์น์ ํ๊ฒ ์ค๋ช ํด๋๋ฆด๊ฒ์! ๐
Step 1: ๊ธฐ๋ณธ ๋ชจ๋ธ ํ์ตํ๊ธฐ
๋จผ์ ์ผ๋ฐ์ ์ธ TensorFlow๋ก ๋ชจ๋ธ์ ํ์ต์์ผ์ผ ํด์. ๊ฐ๋จํ ์ด๋ฏธ์ง ๋ถ๋ฅ ๋ชจ๋ธ์ ์๋ก ๋ค์ด๋ณผ๊ฒ์.
import tensorflow as tf
from tensorflow import keras
import numpy as np
# ๋ฐ์ดํฐ ์ค๋น (์: MNIST ์๊ธ์จ ์ซ์)
(train_images, train_labels), (test_images, test_labels) = keras.datasets.mnist.load_data()
# ๋ฐ์ดํฐ ์ ์ฒ๋ฆฌ
train_images = train_images.reshape((60000, 28, 28, 1)).astype('float32') / 255
test_images = test_images.reshape((10000, 28, 28, 1)).astype('float32') / 255
# ๋ชจ๋ธ ๊ตฌ์ถ
model = keras.Sequential([
keras.layers.Conv2D(32, (3, 3), activation='relu', input_shape=(28, 28, 1)),
keras.layers.MaxPooling2D((2, 2)),
keras.layers.Conv2D(64, (3, 3), activation='relu'),
keras.layers.MaxPooling2D((2, 2)),
keras.layers.Flatten(),
keras.layers.Dense(64, activation='relu'),
keras.layers.Dense(10, activation='softmax')
])
# ๋ชจ๋ธ ์ปดํ์ผ ๋ฐ ํ์ต
model.compile(optimizer='adam',
loss='sparse_categorical_crossentropy',
metrics=['accuracy'])
model.fit(train_images, train_labels, epochs=5, validation_split=0.2)
# ๋ชจ๋ธ ์ ์ฅ
model.save('mnist_model.h5')
์ด ์ฝ๋๋ ์๊ธ์จ ์ซ์๋ฅผ ์ธ์ํ๋ CNN(Convolutional Neural Network) ๋ชจ๋ธ์ ๋ง๋ค์ด์. ์ค์ ๋ก ๋๋ ค๋ณด๋ฉด 98% ์ด์์ ์ ํ๋๋ฅผ ๋ณด์ฌ์ค ๊ฑฐ์์! ๐ฏ
Step 2: TensorFlow Lite๋ก ๋ณํํ๊ธฐ
์ด์ ํ์ต๋ ๋ชจ๋ธ์ ๋ชจ๋ฐ์ผ์์ ์ฌ์ฉํ ์ ์๋๋ก ๋ณํํด์ผ ํด์. ์ฌ๊ธฐ์ ๋ง๋ฒ์ด ์ผ์ด๋์ฃ ! โจ
# ๊ธฐ๋ณธ ๋ณํ
converter = tf.lite.TFLiteConverter.from_keras_model(model)
tflite_model = converter.convert()
# ๋ณํ๋ ๋ชจ๋ธ ์ ์ฅ
with open('mnist_model.tflite', 'wb') as f:
f.write(tflite_model)
print(f"์๋ณธ ๋ชจ๋ธ ํฌ๊ธฐ: {os.path.getsize('mnist_model.h5') / 1024:.2f} KB")
print(f"TFLite ๋ชจ๋ธ ํฌ๊ธฐ: {len(tflite_model) / 1024:.2f} KB")
TFLite ๋ชจ๋ธ ํฌ๊ธฐ: 401.2 KB
๊ธฐ๋ณธ ๋ณํ๋ง์ผ๋ก๋ ์ฝ 7% ์ ๋ ํฌ๊ธฐ๊ฐ ์ค์ด๋ค์๋ค์! ํ์ง๋ง ์ฐ๋ฆฌ๋ ๋ ๋์๊ฐ ์ ์์ด์.
Step 3: ์์ํ๋ก ๋ ๊ฐ๋ณ๊ฒ!
์์ํ(Quantization)๋ ๋ชจ๋ธ์ ๋์ฑ ์์ถํ๋ ํต์ฌ ๊ธฐ์ ์ด์์. 32๋นํธ ๋ถ๋์์์ ์ซ์๋ฅผ 8๋นํธ ์ ์๋ก ๋ฐ๊พธ๋ ๊ฑฐ์ฃ . ์ ํ๋๋ ์ฝ๊ฐ ๋จ์ด์ง์ง๋ง, ํฌ๊ธฐ์ ์๋ ๋ฉด์์ ์์ฒญ๋ ์ด๋์ ๋ด์! ๐
# ๋์ ๋ฒ์ ์์ํ (๊ฐ์ฅ ๊ฐ๋จ)
converter = tf.lite.TFLiteConverter.from_keras_model(model)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
tflite_quant_model = converter.convert()
with open('mnist_model_quant.tflite', 'wb') as f:
f.write(tflite_quant_model)
# ์ ์ฒด ์ ์ ์์ํ (๋ ๊ณต๊ฒฉ์ )
def representative_dataset():
for i in range(100):
yield [train_images[i:i+1]]
converter = tf.lite.TFLiteConverter.from_keras_model(model)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
converter.representative_dataset = representative_dataset
converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]
converter.inference_input_type = tf.uint8
converter.inference_output_type = tf.uint8
tflite_int8_model = converter.convert()
with open('mnist_model_int8.tflite', 'wb') as f:
f.write(tflite_int8_model)
โข ํฌ๊ธฐ: ์ฝ 75% ๊ฐ์
โข ์๋: 2-3๋ฐฐ ํฅ์
โข ์ ํ๋ ์์ค: ๊ฑฐ์ ์์
โข ๋์ด๋: โญ
2. ์ ์ฒด ์ ์ ์์ํ (Full Integer Quantization) โข ๊ฐ์ค์น์ ํ์ฑํ ๋ชจ๋ 8๋นํธ๋ก ๋ณํ
โข ํฌ๊ธฐ: ์ฝ 75% ๊ฐ์
โข ์๋: 3-4๋ฐฐ ํฅ์ (ํนํ NPU ์ฌ์ฉ ์)
โข ์ ํ๋ ์์ค: 1-2%
โข ๋์ด๋: โญโญโญ
3. Float16 ์์ํ โข 32๋นํธ๋ฅผ 16๋นํธ๋ก ๋ณํ
โข ํฌ๊ธฐ: ์ฝ 50% ๊ฐ์
โข ์๋: GPU์์ 2๋ฐฐ ํฅ์
โข ์ ํ๋ ์์ค: ๊ฑฐ์ ์์
โข ๋์ด๋: โญโญ
์ค์ ํ๋ก์ ํธ์์๋ ๋์ ๋ฒ์ ์์ํ๋ถํฐ ์์ํ๋ ๊ฑธ ์ถ์ฒํด์. ๊ตฌํ์ด ์ฝ๊ณ ์ ํ๋ ์์ค๋ ๊ฑฐ์ ์๊ฑฐ๋ ์. ๊ทธ๋ค์์ ์ฑ๋ฅ์ด ๋ ํ์ํ๋ฉด ์ ์ฒด ์ ์ ์์ํ๋ฅผ ์๋ํด๋ณด์ธ์!
๐ฑ Android์์ TensorFlow Lite ์ฌ์ฉํ๊ธฐ
์, ์ด์ ๋ณํ๋ ๋ชจ๋ธ์ ์ค์ Android ์ฑ์ ํตํฉํด๋ณผ ์๊ฐ์ด์์! Android Studio๋ฅผ ์ผ๊ณ ๋ฐ๋ผ์ ์ฃผ์ธ์. ๐ฎ
Step 1: ํ๋ก์ ํธ ์ค์
๋จผ์ build.gradle ํ์ผ์ TensorFlow Lite ์์กด์ฑ์ ์ถ๊ฐํด์ผ ํด์.
// app/build.gradle
android {
aaptOptions {
noCompress "tflite" // TFLite ๋ชจ๋ธ ์์ถ ๋ฐฉ์ง
}
}
dependencies {
// TensorFlow Lite
implementation 'org.tensorflow:tensorflow-lite:2.13.0'
// GPU ๊ฐ์ ์ง์ (์ ํ์ฌํญ)
implementation 'org.tensorflow:tensorflow-lite-gpu:2.13.0'
// Support Library (์ด๋ฏธ์ง ์ฒ๋ฆฌ ๋ฑ)
implementation 'org.tensorflow:tensorflow-lite-support:0.4.4'
}
๊ทธ๋ฆฌ๊ณ assets ํด๋๋ฅผ ๋ง๋ค์ด์ .tflite ๋ชจ๋ธ ํ์ผ์ ๋ฃ์ด์ฃผ์ธ์. ํ๋ก์ ํธ ๊ตฌ์กฐ๋ ์ด๋ ๊ฒ ๋ ๊ฑฐ์์:
๐ app
ย ย ๐ src
ย ย ย ย ๐ main
ย ย ย ย ย ย ๐ assets
ย ย ย ย ย ย ย ย ๐ mnist_model.tflite
ย ย ย ย ย ย ๐ java
ย ย ย ย ย ย ๐ res
Step 2: ๋ชจ๋ธ ๋ก๋ ๋ฐ ์ถ๋ก ํด๋์ค ๋ง๋ค๊ธฐ
์ด์ ์ค์ ๋ก ๋ชจ๋ธ์ ์ฌ์ฉํ๋ ์ฝ๋๋ฅผ ์์ฑํด๋ณผ๊ฒ์. Kotlin์ผ๋ก ๊น๋ํ๊ฒ ๋ง๋ค์ด๋ณผ๊ฒ์! ๐
import android.content.Context
import android.graphics.Bitmap
import org.tensorflow.lite.Interpreter
import org.tensorflow.lite.support.common.FileUtil
import org.tensorflow.lite.support.image.TensorImage
import org.tensorflow.lite.support.image.ops.ResizeOp
import java.nio.ByteBuffer
class DigitClassifier(private val context: Context) {
private var interpreter: Interpreter? = null
private val modelPath = "mnist_model.tflite"
// ๋ชจ๋ธ ์ด๊ธฐํ
fun initialize() {
val model = FileUtil.loadMappedFile(context, modelPath)
// Interpreter ์ต์
์ค์
val options = Interpreter.Options().apply {
// ์ค๋ ๋ ๊ฐ์ ์ค์ (๋ฉํฐ์ฝ์ด ํ์ฉ)
setNumThreads(4)
// GPU ๊ฐ์ ์ฌ์ฉ (์ ํ์ฌํญ)
// addDelegate(GpuDelegate())
}
interpreter = Interpreter(model, options)
}
// ์ด๋ฏธ์ง ๋ถ๋ฅ
fun classify(bitmap: Bitmap): Pair<Int, Float> {
// 1. ์ด๋ฏธ์ง ์ ์ฒ๋ฆฌ
val tensorImage = TensorImage.fromBitmap(bitmap)
val resizedImage = ResizeOp(28, 28, ResizeOp.ResizeMethod.BILINEAR)
.apply(tensorImage)
// 2. ์
๋ ฅ ๋ฒํผ ์ค๋น
val inputBuffer = resizedImage.buffer
// 3. ์ถ๋ ฅ ๋ฒํผ ์ค๋น
val outputBuffer = Array(1) { FloatArray(10) }
// 4. ์ถ๋ก ์คํ
interpreter?.run(inputBuffer, outputBuffer)
// 5. ๊ฒฐ๊ณผ ํด์
val probabilities = outputBuffer[0]
val maxIndex = probabilities.indices.maxByOrNull { probabilities[it] } ?: -1
val confidence = probabilities[maxIndex]
return Pair(maxIndex, confidence)
}
// ๋ฆฌ์์ค ์ ๋ฆฌ
fun close() {
interpreter?.close()
interpreter = null
}
}
2. ์ค๋ ๋ ๊ฐ์ ์กฐ์ : setNumThreads()๋ก CPU ์ฝ์ด๋ฅผ ์ต๋ํ ํ์ฉํ์ธ์. ๋ณดํต 4๊ฐ๊ฐ ์ ๋นํด์.
3. GPU ๊ฐ์: ์ด๋ฏธ์ง ์ฒ๋ฆฌ ๋ชจ๋ธ์ด๋ผ๋ฉด GPU ๊ฐ์์ ๊ผญ ๊ณ ๋ คํด๋ณด์ธ์. ์๋๊ฐ 5-10๋ฐฐ ๋นจ๋ผ์ง ์ ์์ด์!
4. ๋ฐฐ์น ์ฒ๋ฆฌ: ์ฌ๋ฌ ์ด๋ฏธ์ง๋ฅผ ํ ๋ฒ์ ์ฒ๋ฆฌํ ์ ์๋ค๋ฉด ๋ฐฐ์น๋ก ๋ฌถ์ด์ ์ฒ๋ฆฌํ๋ฉด ๋ ํจ์จ์ ์ด์์.
Step 3: Activity์์ ์ฌ์ฉํ๊ธฐ
์ด์ ์ค์ ํ๋ฉด์์ ์ฌ์ฉํด๋ณผ๊น์? ์นด๋ฉ๋ผ๋ก ์ฐ์ ์ฌ์ง์ด๋ ๊ฐค๋ฌ๋ฆฌ์์ ์ ํํ ์ด๋ฏธ์ง๋ฅผ ๋ถ๋ฅํ๋ ๊ฐ๋จํ ์์ ์์.
class MainActivity : AppCompatActivity() {
private lateinit var classifier: DigitClassifier
private lateinit var imageView: ImageView
private lateinit var resultText: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
imageView = findViewById(R.id.imageView)
resultText = findViewById(R.id.resultText)
// ๋ถ๋ฅ๊ธฐ ์ด๊ธฐํ
classifier = DigitClassifier(this)
classifier.initialize()
// ์ด๋ฏธ์ง ์ ํ ๋ฒํผ
findViewById<Button>(R.id.selectButton).setOnClickListener {
selectImage()
}
}
private fun selectImage() {
val intent = Intent(Intent.ACTION_PICK)
intent.type = "image/*"
startActivityForResult(intent, REQUEST_IMAGE)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_IMAGE && resultCode == RESULT_OK) {
data?.data?.let { uri ->
val bitmap = MediaStore.Images.Media.getBitmap(contentResolver, uri)
imageView.setImageBitmap(bitmap)
// ๋ถ๋ฅ ์คํ
val startTime = System.currentTimeMillis()
val (digit, confidence) = classifier.classify(bitmap)
val inferenceTime = System.currentTimeMillis() - startTime
// ๊ฒฐ๊ณผ ํ์
resultText.text = """
์์ธก ์ซ์: $digit
์ ๋ขฐ๋: ${(confidence * 100).toInt()}%
์ถ๋ก ์๊ฐ: ${inferenceTime}ms
""".trimIndent()
}
}
}
override fun onDestroy() {
super.onDestroy()
classifier.close()
}
companion object {
private const val REQUEST_IMAGE = 100
}
}
์ด๋ ๊ฒ ํ๋ฉด ์ฌ์ฉ์๊ฐ ์ด๋ฏธ์ง๋ฅผ ์ ํํ๋ฉด ์ฆ์ AI๊ฐ ๋ถ์ํด์ ๊ฒฐ๊ณผ๋ฅผ ๋ณด์ฌ์ค์. ์ค์ ๋ก ๋๋ ค๋ณด๋ฉด ์ถ๋ก ์๊ฐ์ด 10-30ms ์ ๋๋ฐ์ ์ ๊ฑธ๋ ค์. ์ ๋ง ๋น ๋ฅด์ฃ ? โก
๐ iOS์์ TensorFlow Lite ์ฌ์ฉํ๊ธฐ
Android ๊ฐ๋ฐ์๋ค์ ์ด์ ์ฌ์ด๋ ๋ผ์! iOS ๊ฐ๋ฐ์ ์ฌ๋ฌ๋ถ, ์ด์ ์ฌ๋ฌ๋ถ ์ฐจ๋ก์์. ๐
iOS์์๋ TensorFlow Lite๋ฅผ ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ์ Android์ ํฌ๊ฒ ๋ค๋ฅด์ง ์์์. Swift๋ก ๊น๋ํ๊ฒ ๊ตฌํํด๋ณผ๊ฒ์!
Step 1: CocoaPods๋ก ์ค์นํ๊ธฐ
๋จผ์ Podfile์ TensorFlow Lite๋ฅผ ์ถ๊ฐํด์ฃผ์ธ์.
# Podfile
platform :ios, '12.0'
target 'YourApp' do
use_frameworks!
# TensorFlow Lite
pod 'TensorFlowLiteSwift', '~> 2.13.0'
# Metal ๊ฐ์ ์ง์ (์ ํ์ฌํญ)
pod 'TensorFlowLiteSwift/Metal', '~> 2.13.0'
end
ํฐ๋ฏธ๋์์ pod install์ ์คํํ๊ณ , ์์ฑ๋ .xcworkspace ํ์ผ์ ์ด์ด์ฃผ์ธ์.
Step 2: ๋ชจ๋ธ ์ถ๊ฐ ๋ฐ ๋ถ๋ฅ๊ธฐ ๊ตฌํ
๋ชจ๋ธ ํ์ผ์ ํ๋ก์ ํธ์ ๋๋๊ทธ ์ค ๋๋กญ์ผ๋ก ์ถ๊ฐํ๊ณ , Swift๋ก ๋ถ๋ฅ๊ธฐ๋ฅผ ๋ง๋ค์ด๋ณผ๊ฒ์.
import UIKit
import TensorFlowLite
class DigitClassifier {
private var interpreter: Interpreter?
private let modelFileName = "mnist_model"
// ๋ชจ๋ธ ์ด๊ธฐํ
func initialize() throws {
guard let modelPath = Bundle.main.path(forResource: modelFileName,
ofType: "tflite") else {
throw ClassifierError.modelNotFound
}
// Interpreter ์ต์
์ค์
var options = Interpreter.Options()
options.threadCount = 4
// Metal ๊ฐ์ ์ฌ์ฉ (์ ํ์ฌํญ)
// let delegate = MetalDelegate()
// options.addDelegate(delegate)
interpreter = try Interpreter(modelPath: modelPath, options: options)
try interpreter?.allocateTensors()
}
// ์ด๋ฏธ์ง ๋ถ๋ฅ
func classify(image: UIImage) throws -> (digit: Int, confidence: Float) {
guard let interpreter = interpreter else {
throw ClassifierError.interpreterNotInitialized
}
// 1. ์ด๋ฏธ์ง ์ ์ฒ๋ฆฌ
guard let resizedImage = image.resize(to: CGSize(width: 28, height: 28)),
let pixelBuffer = resizedImage.toGrayscaleBuffer() else {
throw ClassifierError.imageProcessingFailed
}
// 2. ์
๋ ฅ ๋ฐ์ดํฐ ๋ณต์ฌ
try interpreter.copy(Data(bytes: pixelBuffer, count: 28 * 28),
toInputAt: 0)
// 3. ์ถ๋ก ์คํ
try interpreter.invoke()
// 4. ์ถ๋ ฅ ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ
let outputTensor = try interpreter.output(at: 0)
let results = [Float32](unsafeData: outputTensor.data) ?? []
// 5. ๊ฒฐ๊ณผ ํด์
guard let maxIndex = results.indices.max(by: { results[$0] < results[$1] }) else {
throw ClassifierError.resultProcessingFailed
}
return (digit: maxIndex, confidence: results[maxIndex])
}
enum ClassifierError: Error {
case modelNotFound
case interpreterNotInitialized
case imageProcessingFailed
case resultProcessingFailed
}
}
// UIImage ํ์ฅ - ์ ์ฒ๋ฆฌ ํฌํผ ํจ์
extension UIImage {
func resize(to size: CGSize) -> UIImage? {
UIGraphicsBeginImageContextWithOptions(size, false, 1.0)
defer { UIGraphicsEndImageContext() }
draw(in: CGRect(origin: .zero, size: size))
return UIGraphicsGetImageFromCurrentImageContext()
}
func toGrayscaleBuffer() -> [UInt8]? {
guard let cgImage = self.cgImage else { return nil }
let width = cgImage.width
let height = cgImage.height
var buffer = [UInt8](repeating: 0, count: width * height)
let colorSpace = CGColorSpaceCreateDeviceGray()
let context = CGContext(data: &buffer,
width: width,
height: height,
bitsPerComponent: 8,
bytesPerRow: width,
space: colorSpace,
bitmapInfo: CGImageAlphaInfo.none.rawValue)
context?.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height))
return buffer
}
}
// Array ํ์ฅ - ๋ฐ์ดํฐ ๋ณํ ํฌํผ
extension Array {
init?(unsafeData: Data) {
guard unsafeData.count % MemoryLayout<Element>.stride == 0 else { return nil }
self = unsafeData.withUnsafeBytes { .init($0.bindMemory(to: Element.self)) }
}
}
Step 3: ViewController์์ ์ฌ์ฉํ๊ธฐ
class ViewController: UIViewController, UIImagePickerControllerDelegate,
UINavigationControllerDelegate {
@IBOutlet weak var imageView: UIImageView!
@IBOutlet weak var resultLabel: UILabel!
private let classifier = DigitClassifier()
override func viewDidLoad() {
super.viewDidLoad()
do {
try classifier.initialize()
} catch {
print("๋ชจ๋ธ ์ด๊ธฐํ ์คํจ: \(error)")
}
}
@IBAction func selectImageTapped(_ sender: UIButton) {
let picker = UIImagePickerController()
picker.delegate = self
picker.sourceType = .photoLibrary
present(picker, animated: true)
}
func imagePickerController(_ picker: UIImagePickerController,
didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
picker.dismiss(animated: true)
guard let image = info[.originalImage] as? UIImage else { return }
imageView.image = image
// ๋ถ๋ฅ ์คํ
let startTime = Date()
do {
let result = try classifier.classify(image: image)
let inferenceTime = Date().timeIntervalSince(startTime) * 1000
resultLabel.text = """
์์ธก ์ซ์: \(result.digit)
์ ๋ขฐ๋: \(Int(result.confidence * 100))%
์ถ๋ก ์๊ฐ: \(Int(inferenceTime))ms
"""
} catch {
resultLabel.text = "๋ถ๋ฅ ์คํจ: \(error)"
}
}
}
Core ML ๋ณํ: TensorFlow Lite ๋์ Core ML๋ก ๋ณํํ๋ ๊ฒ๋ ๊ณ ๋ คํด๋ณด์ธ์. Apple ํ๋์จ์ด์ ๋ ์ต์ ํ๋์ด ์์ด์.
Neural Engine: A11 ์นฉ ์ด์์ ๊ธฐ๊ธฐ๋ Neural Engine์ ํ์ฌํ๊ณ ์์ด์. Core ML์ ์ฌ์ฉํ๋ฉด ์๋์ผ๋ก ํ์ฉ๋ผ์!
๐จ ์ค์ ํ์ฉ ์ฌ๋ก: ๋ค์ํ AI ๋ชจ๋ธ๋ค
์๊ธ์จ ์ธ์์ ์์์ ๋ถ๊ณผํด์! TensorFlow Lite๋ก ๊ตฌํํ ์ ์๋ ๋ฉ์ง ๊ธฐ๋ฅ๋ค์ ์๊ฐํ ๊ฒ์. ๐
1. ์ด๋ฏธ์ง ๋ถ๋ฅ (Image Classification)
๊ฐ์ฅ ๊ธฐ๋ณธ์ ์ด๋ฉด์๋ ๊ฐ๋ ฅํ ๊ธฐ๋ฅ์ด์์. ์ฌ์ง ์ ๋ฌผ์ฒด๊ฐ ๋ฌด์์ธ์ง ์์๋ด๋ ๊ฑฐ์ฃ .
โข ๋ชจ๋ธ ํฌ๊ธฐ: 4.3MB (MobileNetV2)
โข ์ถ๋ก ์๋: 20-40ms (๋ชจ๋ฐ์ผ CPU)
โข ์ ํ๋: Top-5 accuracy 90%
ํ์ฉ ์์:
โข ์์ ์ฌ์ง ์ฐ์ผ๋ฉด ์นผ๋ก๋ฆฌ ์๋ ๊ณ์ฐ
โข ์๋ฌผ ์ฌ์ง์ผ๋ก ์ข ๋ฅ ์๋ณ
โข ์ ํ ์ฌ์ง์ผ๋ก ์ผํ ๊ฒ์
โข ๋๋ฌผ ์ฌ์ง์ผ๋ก ํ์ข ํ์ธ
// MobileNet ์ฌ์ฉ ์์ (Kotlin)
class ImageClassifier(context: Context) {
private val labels = loadLabels(context, "labels.txt")
fun classifyImage(bitmap: Bitmap): List<Classification> {
// ์ ์ฒ๋ฆฌ: 224x224๋ก ๋ฆฌ์ฌ์ด์ฆ, ์ ๊ทํ
val input = preprocessImage(bitmap, 224, 224)
// ์ถ๋ก
val output = Array(1) { FloatArray(1000) }
interpreter.run(input, output)
// Top-5 ๊ฒฐ๊ณผ ๋ฐํ
return output[0]
.mapIndexed { index, score -> Classification(labels[index], score) }
.sortedByDescending { it.score }
.take(5)
}
}
data class Classification(val label: String, val score: Float)
2. ๊ฐ์ฒด ํ์ง (Object Detection)
์ด๋ฏธ์ง ๋ถ๋ฅ๋ณด๋ค ํ ๋จ๊ณ ๋ ๋์๊ฐ์, ์ฌ์ง ์ ์ฌ๋ฌ ๋ฌผ์ฒด์ ์์น๊น์ง ์ฐพ์๋ด์! ๐
โข ๋ชจ๋ธ ํฌ๊ธฐ: 27MB
โข ์ถ๋ก ์๋: 50-100ms (๋ชจ๋ฐ์ผ CPU)
โข ํ์ง ๊ฐ๋ฅ: ์ต๋ 90๊ฐ ์นดํ ๊ณ ๋ฆฌ
ํ์ฉ ์์:
โข AR ์ฑ์์ ์ค์๊ฐ ๋ฌผ์ฒด ์ธ์
โข ์์จ์ฃผํ ๋ณด์กฐ ์์คํ
โข ์๋งค์ ์ฌ๊ณ ์๋ ์นด์ดํ
โข ์คํฌ์ธ ๊ฒฝ๊ธฐ ์ ์ ์ถ์
// ๊ฐ์ฒด ํ์ง ๊ฒฐ๊ณผ ์ฒ๋ฆฌ
data class Detection(
val boundingBox: RectF,
val label: String,
val score: Float
)
fun detectObjects(bitmap: Bitmap): List<Detection> {
val detections = mutableListOf<Detection>()
// ๋ชจ๋ธ ์ถ๋ ฅ: [์์น, ํด๋์ค, ์ ์, ๊ฐ์]
val locations = Array(1) { Array(10) { FloatArray(4) } }
val classes = Array(1) { FloatArray(10) }
val scores = Array(1) { FloatArray(10) }
val numDetections = FloatArray(1)
val outputs = mapOf(
0 to locations,
1 to classes,
2 to scores,
3 to numDetections
)
interpreter.runForMultipleInputsOutputs(arrayOf(inputBuffer), outputs)
val count = numDetections[0].toInt()
for (i in 0 until count) {
if (scores[0][i] > 0.5f) { // ์ ๋ขฐ๋ 50% ์ด์๋ง
val box = RectF(
locations[0][i][1] * bitmap.width,
locations[0][i][0] * bitmap.height,
locations[0][i][3] * bitmap.width,
locations[0][i][2] * bitmap.height
)
detections.add(Detection(box, labels[classes[0][i].toInt()], scores[0][i]))
}
}
return detections
}
3. ํฌ์ฆ ์ถ์ (Pose Estimation)
์ฌ๋์ ์์ธ์ ๋์์ ์ค์๊ฐ์ผ๋ก ๋ถ์ํด์. ํผํธ๋์ค ์ฑ์ด๋ AR ๊ฒ์์ ์๋ฒฝํ์ฃ ! ๐โโ๏ธ
โข ๋ชจ๋ธ ํฌ๊ธฐ: 13MB (MoveNet Lightning)
โข ์ถ๋ก ์๋: 30-60ms (30fps ์ค์๊ฐ ๊ฐ๋ฅ)
โข ์ ํ๋: ์ค๋ด ํ๊ฒฝ์์ 95%+
ํ์ฉ ์์:
โข ํํธ๋ ์ด๋ ์์ธ ๊ต์ ์ฑ
โข ๋์ค ๊ฒ์ ๋์ ์ธ์
โข ์ฌํ ์น๋ฃ ๋ชจ๋ํฐ๋ง
โข ์คํฌ์ธ ํผ ๋ถ์
// ํฌ์ฆ ์ถ์ ๊ฒฐ๊ณผ
data class Keypoint(
val bodyPart: BodyPart,
val position: PointF,
val score: Float
)
enum class BodyPart {
NOSE, LEFT_EYE, RIGHT_EYE, LEFT_EAR, RIGHT_EAR,
LEFT_SHOULDER, RIGHT_SHOULDER, LEFT_ELBOW, RIGHT_ELBOW,
LEFT_WRIST, RIGHT_WRIST, LEFT_HIP, RIGHT_HIP,
LEFT_KNEE, RIGHT_KNEE, LEFT_ANKLE, RIGHT_ANKLE
}
class PoseDetector(context: Context) {
fun estimatePose(bitmap: Bitmap): List<Keypoint> {
// ์
๋ ฅ: 192x192 ์ด๋ฏธ์ง
val input = preprocessImage(bitmap, 192, 192)
// ์ถ๋ ฅ: [1, 17, 3] (17๊ฐ ๊ด์ , ๊ฐ๊ฐ y, x, confidence)
val output = Array(1) { Array(17) { FloatArray(3) } }
interpreter.run(input, output)
return output[0].mapIndexed { index, data ->
Keypoint(
bodyPart = BodyPart.values()[index],
position = PointF(
data[1] * bitmap.width,
data[0] * bitmap.height
),
score = data[2]
)
}.filter { it.score > 0.3f }
}
// ์ค์ฟผํธ ์์ธ ๋ถ์ ์์
fun analyzeSquat(keypoints: List<Keypoint>): String {
val leftKnee = keypoints.find { it.bodyPart == BodyPart.LEFT_KNEE }
val leftHip = keypoints.find { it.bodyPart == BodyPart.LEFT_HIP }
val leftAnkle = keypoints.find { it.bodyPart == BodyPart.LEFT_ANKLE }
if (leftKnee != null && leftHip != null && leftAnkle != null) {
val angle = calculateAngle(leftHip.position, leftKnee.position, leftAnkle.position)
return when {
angle < 90 -> "๋ ๊น๊ฒ ์์ผ์ธ์!"
angle > 120 -> "๋ฌด๋ฆ์ ๋ ๊ตฌ๋ถ๋ฆฌ์ธ์!"
else -> "์๋ฒฝํ ์์ธ์
๋๋ค! ๐"
}
}
return "์์ธ๋ฅผ ์ธ์ํ ์ ์์ต๋๋ค"
}
}
4. ํ ์คํธ ์ธ์ (OCR)
์ด๋ฏธ์ง ์ ํ ์คํธ๋ฅผ ์ฝ์ด๋ด๋ ๊ธฐ๋ฅ์ด์์. ๋ช ํจ, ๋ฌธ์, ๊ฐํ ๋ฑ์ ์ค์บํ ๋ ์ ์ฉํ์ฃ ! ๐
โข ๋ฌธ์ ๋์งํธํ (์ข ์ด ๋ฌธ์๋ฅผ ํ ์คํธ๋ก ๋ณํ)
โข ์ค์๊ฐ ๋ฒ์ญ (๊ฐํ์ด๋ ๋ฉ๋ดํ ๋ฒ์ญ)
โข ์์์ฆ ๊ด๋ฆฌ (๊ธ์ก, ๋ ์ง ์๋ ์ถ์ถ)
โข ์ฐจ๋ ๋ฒํธํ ์ธ์
5. ์์ฑ ์ธ์ (Speech Recognition)
์์ฑ์ ํ ์คํธ๋ก ๋ณํํ๊ฑฐ๋, ํน์ ๋ช ๋ น์ด๋ฅผ ์ธ์ํ ์ ์์ด์. ๐ค
โข ๋ชจ๋ธ ํฌ๊ธฐ: 1.5MB
โข ์ถ๋ก ์๋: ์ค์๊ฐ (10ms ์ดํ)
โข ์ธ์ ๋จ์ด: 10-20๊ฐ ์ปค์คํ ๊ฐ๋ฅ
ํ์ฉ ์์:
โข ํธ์ฆํ๋ฆฌ ์ฑ ์ ์ด
โข ์์ฑ ๋น์ (๊ฐ๋จํ ๋ช ๋ น)
โข ๊ฒ์ ์์ฑ ์ปจํธ๋กค
โข ์ ๊ทผ์ฑ ๊ธฐ๋ฅ (์ฅ์ ์ธ ์ง์)
6. ์คํ์ผ ์ ์ด (Style Transfer)
์ฌ์ง์ ์์ ์ํ์ฒ๋ผ ๋ณํํด์ฃผ๋ ์ฌ๋ฏธ์๋ ๊ธฐ๋ฅ์ด์์! ๐จ
ํด๊ฒฐ์ฑ : ์ด๋ฏธ์ง๋ฅผ 512x512 ์ ๋๋ก ๋ฆฌ์ฌ์ด์ฆํ๊ฑฐ๋, GPU ๊ฐ์์ ํ์ฉํ๋ฉด ์๋๊ฐ ํฌ๊ฒ ๊ฐ์ ๋ผ์.
โก ์ฑ๋ฅ ์ต์ ํ: ๋ ๋น ๋ฅด๊ณ ํจ์จ์ ์ผ๋ก!
๋ชจ๋ธ์ ๋ง๋ค๊ณ ํตํฉํ๋ ๊ฒ๋ ์ค์ํ์ง๋ง, ์ค์ ์ฌ์ฉ์ ๊ฒฝํ์ ์ํด์๋ ์ฑ๋ฅ ์ต์ ํ๊ฐ ํ์์์! ์ฌ๊ธฐ ๊ฒ์ฆ๋ ์ต์ ํ ๊ธฐ๋ฒ๋ค์ ์๊ฐํ ๊ฒ์. ๐
1. ํ๋์จ์ด ๊ฐ์ ํ์ฉํ๊ธฐ
ํ๋ ์ค๋งํธํฐ์๋ AI ์ฐ์ฐ์ ์ํ ํน์ ํ๋์จ์ด๊ฐ ํ์ฌ๋์ด ์์ด์. ์ด๊ฑธ ์ ๋๋ก ํ์ฉํ๋ฉด ์๋๊ฐ ๋ช ๋ฐฐ๋ ๋นจ๋ผ์ ธ์!
Android - NNAPI (Neural Networks API)
// NNAPI ์ฌ์ฉ (Android 8.1+)
val options = Interpreter.Options()
val nnApiDelegate = NnApiDelegate()
options.addDelegate(nnApiDelegate)
val interpreter = Interpreter(modelFile, options)
์ฅ์ : CPU, GPU, DSP, NPU ์ค ์ต์ ์ ํ๋์จ์ด ์๋ ์ ํ์๋ ํฅ์: 2-10๋ฐฐ
์ฃผ์: Android ๋ฒ์ ๊ณผ ๊ธฐ๊ธฐ์ ๋ฐ๋ผ ์ง์ ์ฌ๋ถ ๋ค๋ฆ
Android - GPU Delegate
// GPU ๊ฐ์ ์ฌ์ฉ
val options = Interpreter.Options()
val gpuDelegate = GpuDelegate()
options.addDelegate(gpuDelegate)
val interpreter = Interpreter(modelFile, options)
// ์ฌ์ฉ ํ ์ ๋ฆฌ
gpuDelegate.close()
์ฅ์ : ์ด๋ฏธ์ง/๋น๋์ค ์ฒ๋ฆฌ์ ํนํ ํจ๊ณผ์ ์๋ ํฅ์: 5-10๋ฐฐ
์ฃผ์: ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋ ์ฆ๊ฐ, ์ด๊ธฐํ ์๊ฐ ํ์
iOS - Core ML Delegate
// Core ML ๊ฐ์ ์ฌ์ฉ
var options = Interpreter.Options()
let coreMLDelegate = CoreMLDelegate()
options.addDelegate(coreMLDelegate)
let interpreter = try Interpreter(modelPath: modelPath, options: options)
์ฅ์ : Apple Neural Engine ์๋ ํ์ฉ์๋ ํฅ์: 3-8๋ฐฐ
์ฃผ์: ์ผ๋ถ ์ฐ์ฐ์ ๋ฏธ์ง์ ๊ฐ๋ฅ์ฑ
์ผ๋ฐ์ ์ธ ๊ฒฝ์ฐ: NNAPI (Android) ๋๋ Core ML (iOS)
์ ์ฌ์ ๊ธฐ๊ธฐ: CPU๋ง ์ฌ์ฉ (Delegate ์์)
์ต๊ณ ์ฑ๋ฅ: ์ฌ๋ฌ Delegate๋ฅผ ์์ฐจ์ ์ผ๋ก ์๋ํด๋ณด๊ณ ๊ฐ์ฅ ๋น ๋ฅธ ๊ฒ ์ ํ
2. ๋ชจ๋ธ ์ต์ ํ ๊ธฐ๋ฒ
๋ชจ๋ธ ์์ฒด๋ฅผ ์ต์ ํํ๋ฉด ๋ชจ๋ ๊ธฐ๊ธฐ์์ ์ฑ๋ฅ์ด ํฅ์๋ผ์!
๋์ ๋ฒ์ ์์ํ (์ถ์ฒ ์์์ )
converter = tf.lite.TFLiteConverter.from_keras_model(model)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
tflite_model = converter.convert()
โข ๊ตฌํ ๋์ด๋: โญโข ํฌ๊ธฐ ๊ฐ์: 75%
โข ์๋ ํฅ์: 2-3๋ฐฐ
โข ์ ํ๋ ์์ค: ๊ฑฐ์ ์์
Float16 ์์ํ (GPU ์ต์ ํ)
converter = tf.lite.TFLiteConverter.from_keras_model(model)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
converter.target_spec.supported_types = [tf.float16]
tflite_model = converter.convert()
โข ๊ตฌํ ๋์ด๋: โญโข ํฌ๊ธฐ ๊ฐ์: 50%
โข ์๋ ํฅ์: GPU์์ 2๋ฐฐ
โข ์ ํ๋ ์์ค: ๊ฑฐ์ ์์
์ ์ฒด ์ ์ ์์ํ (์ต๊ณ ์ฑ๋ฅ)
def representative_dataset():
for data in train_dataset.take(100):
yield [data]
converter = tf.lite.TFLiteConverter.from_keras_model(model)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
converter.representative_dataset = representative_dataset
converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]
converter.inference_input_type = tf.uint8
converter.inference_output_type = tf.uint8
tflite_model = converter.convert()
โข ๊ตฌํ ๋์ด๋: โญโญโญโข ํฌ๊ธฐ ๊ฐ์: 75%
โข ์๋ ํฅ์: 3-4๋ฐฐ (NPU ์ฌ์ฉ ์ ๋ ๋น ๋ฆ)
โข ์ ํ๋ ์์ค: 1-2%
3. ์ ๋ ฅ ๋ฐ์ดํฐ ์ต์ ํ
๋ชจ๋ธ์ ๋ค์ด๊ฐ๋ ๋ฐ์ดํฐ๋ฅผ ํจ์จ์ ์ผ๋ก ์ฒ๋ฆฌํ๋ ๊ฒ๋ ์ค์ํด์!
// ํจ์จ์ ์ธ ์ด๋ฏธ์ง ์ ์ฒ๋ฆฌ (Kotlin)
class ImagePreprocessor {
// โ ๋นํจ์จ์ ์ธ ๋ฐฉ๋ฒ
fun preprocessSlow(bitmap: Bitmap): ByteBuffer {
val resized = Bitmap.createScaledBitmap(bitmap, 224, 224, true)
val buffer = ByteBuffer.allocateDirect(224 * 224 * 3 * 4)
buffer.order(ByteOrder.nativeOrder())
for (y in 0 until 224) {
for (x in 0 until 224) {
val pixel = resized.getPixel(x, y)
buffer.putFloat(((pixel shr 16) and 0xFF) / 255.0f)
buffer.putFloat(((pixel shr 8) and 0xFF) / 255.0f)
buffer.putFloat((pixel and 0xFF) / 255.0f)
}
}
return buffer
}
// โ
ํจ์จ์ ์ธ ๋ฐฉ๋ฒ
fun preprocessFast(bitmap: Bitmap): ByteBuffer {
val resized = Bitmap.createScaledBitmap(bitmap, 224, 224, true)
val buffer = ByteBuffer.allocateDirect(224 * 224 * 3 * 4)
buffer.order(ByteOrder.nativeOrder())
val intValues = IntArray(224 * 224)
resized.getPixels(intValues, 0, 224, 0, 0, 224, 224)
var pixel = 0
for (i in 0 until 224) {
for (j in 0 until 224) {
val value = intValues[pixel++]
buffer.putFloat(((value shr 16) and 0xFF) / 255.0f)
buffer.putFloat(((value shr 8) and 0xFF) / 255.0f)
buffer.putFloat((value and 0xFF) / 255.0f)
}
}
buffer.rewind()
return buffer
}
}
preprocessFast(): ์ฝ 12ms
๊ฐ์ ์จ: 3.75๋ฐฐ ๋นจ๋ผ์ง! ๐
ํต์ฌ์ getPixels()๋ฅผ ์ฌ์ฉํด์ ํ ๋ฒ์ ๋ชจ๋ ํฝ์ ์ ๊ฐ์ ธ์ค๋ ๊ฑฐ์์. getPixel()์ ๋ฐ๋ณต ํธ์ถํ๋ ๊ฒ๋ณด๋ค ํจ์ฌ ๋น ๋ฅด์ฃ !
4. ๋ฐฐ์น ์ฒ๋ฆฌ์ ๋น๋๊ธฐ ์คํ
์ฌ๋ฌ ์ด๋ฏธ์ง๋ฅผ ์ฒ๋ฆฌํด์ผ ํ๋ค๋ฉด ๋ฐฐ์น๋ก ๋ฌถ์ด์ ์ฒ๋ฆฌํ๊ณ , UI ์ค๋ ๋๋ฅผ ๋ธ๋กํ์ง ์๋๋ก ๋น๋๊ธฐ๋ก ์คํํ์ธ์!
// Kotlin Coroutines๋ฅผ ํ์ฉํ ๋น๋๊ธฐ ์ถ๋ก
class AsyncClassifier(context: Context) {
private val classifier = ImageClassifier(context)
private val scope = CoroutineScope(Dispatchers.Default)
fun classifyAsync(bitmap: Bitmap, callback: (Result) -> Unit) {
scope.launch {
try {
val result = classifier.classify(bitmap)
withContext(Dispatchers.Main) {
callback(Result.Success(result))
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
callback(Result.Error(e))
}
}
}
}
// ๋ฐฐ์น ์ฒ๋ฆฌ
fun classifyBatch(bitmaps: List<Bitmap>): List<Classification> {
val batchSize = bitmaps.size
val inputBuffer = ByteBuffer.allocateDirect(batchSize * 224 * 224 * 3 * 4)
// ๋ชจ๋ ์ด๋ฏธ์ง๋ฅผ ํ๋์ ๋ฒํผ์ ๋ด๊ธฐ
bitmaps.forEach { bitmap ->
val processed = preprocessImage(bitmap)
inputBuffer.put(processed)
}
// ํ ๋ฒ์ ์ถ๋ก
val outputBuffer = Array(batchSize) { FloatArray(1000) }
interpreter.run(inputBuffer, outputBuffer)
return outputBuffer.map { scores ->
scores.indices.maxByOrNull { scores[it] }?.let { index ->
Classification(labels[index], scores[index])
} ?: Classification("Unknown", 0f)
}
}
}
sealed class Result {
data class Success(val classification: Classification) : Result()
data class Error(val exception: Exception) : Result()
}
5. ๋ฉ๋ชจ๋ฆฌ ๊ด๋ฆฌ
๋ชจ๋ฐ์ผ ๊ธฐ๊ธฐ๋ ๋ฉ๋ชจ๋ฆฌ๊ฐ ์ ํ์ ์ด์์. ํจ์จ์ ์ธ ๋ฉ๋ชจ๋ฆฌ ๊ด๋ฆฌ๊ฐ ํ์์ฃ !
2. Delegate ์ ๋ฆฌ: GPU Delegate ๋ฑ๋ ๋ช ์์ ์ผ๋ก ์ ๋ฆฌ ํ์
3. ByteBuffer ์ฌ์ฌ์ฉ: ๋งค๋ฒ ์๋ก ํ ๋นํ์ง ๋ง๊ณ ์ฌ์ฌ์ฉ
4. Bitmap ์ฌํ์ฉ: ํฐ ์ด๋ฏธ์ง๋ ์ฒ๋ฆฌ ํ ์ฆ์ recycle()
// ๋ฉ๋ชจ๋ฆฌ ํจ์จ์ ์ธ ๋ถ๋ฅ๊ธฐ
class MemoryEfficientClassifier(context: Context) : AutoCloseable {
private val interpreter: Interpreter
private val gpuDelegate: GpuDelegate?
private val inputBuffer: ByteBuffer
private val outputBuffer: Array<FloatArray>
init {
// GPU Delegate ์ด๊ธฐํ
gpuDelegate = try {
GpuDelegate()
} catch (e: Exception) {
null // GPU ๋ฏธ์ง์ ๊ธฐ๊ธฐ
}
// Interpreter ์ด๊ธฐํ
val options = Interpreter.Options().apply {
gpuDelegate?.let { addDelegate(it) }
setNumThreads(4)
}
interpreter = Interpreter(loadModelFile(context), options)
// ๋ฒํผ ๋ฏธ๋ฆฌ ํ ๋น (์ฌ์ฌ์ฉ)
inputBuffer = ByteBuffer.allocateDirect(224 * 224 * 3 * 4)
inputBuffer.order(ByteOrder.nativeOrder())
outputBuffer = Array(1) { FloatArray(1000) }
}
fun classify(bitmap: Bitmap): Classification {
// ๋ฒํผ ์ฌ์ฌ์ฉ
inputBuffer.rewind()
fillInputBuffer(bitmap, inputBuffer)
// ์ถ๋ก
interpreter.run(inputBuffer, outputBuffer)
// ๊ฒฐ๊ณผ ์ฒ๋ฆฌ
val scores = outputBuffer[0]
val maxIndex = scores.indices.maxByOrNull { scores[it] } ?: 0
return Classification(labels[maxIndex], scores[maxIndex])
}
override fun close() {
interpreter.close()
gpuDelegate?.close()
}
}
// Activity์์ ์ฌ์ฉ
class MainActivity : AppCompatActivity() {
private lateinit var classifier: MemoryEfficientClassifier
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
classifier = MemoryEfficientClassifier(this)
}
override fun onDestroy() {
super.onDestroy()
classifier.close() // ๋ฆฌ์์ค ์ ๋ฆฌ
}
}
๐ ๋๋ฒ๊น ๊ณผ ๋ฌธ์ ํด๊ฒฐ
๊ฐ๋ฐํ๋ค ๋ณด๋ฉด ์์์น ๋ชปํ ๋ฌธ์ ๋ค์ด ์๊ธฐ์ฃ . ์์ฃผ ๋ฐ์ํ๋ ๋ฌธ์ ๋ค๊ณผ ํด๊ฒฐ ๋ฐฉ๋ฒ์ ์ ๋ฆฌํด๋ดค์ด์! ๐
๋ฌธ์ 1: ๋ชจ๋ธ ๋ก๋ฉ ์คํจ
java.io.FileNotFoundException: model.tflite
โ ํด๊ฒฐ ๋ฐฉ๋ฒ
1. ๋ชจ๋ธ ํ์ผ์ด assets ํด๋์ ์๋์ง ํ์ธ
2. ํ์ผ ์ด๋ฆ๊ณผ ํ์ฅ์๊ฐ ์ ํํ์ง ํ์ธ (.tflite)
3. ๋น๋ ํ APK์ ํฌํจ๋์๋์ง ํ์ธ
// ์ฌ๋ฐ๋ฅธ ๋ชจ๋ธ ๋ก๋ฉ ๋ฐฉ๋ฒ
private fun loadModelFile(context: Context, modelName: String): MappedByteBuffer {
val fileDescriptor = context.assets.openFd(modelName)
val inputStream = FileInputStream(fileDescriptor.fileDescriptor)
val fileChannel = inputStream.channel
val startOffset = fileDescriptor.startOffset
val declaredLength = fileDescriptor.declaredLength
return fileChannel.map(FileChannel.MapMode.READ_ONLY, startOffset, declaredLength)
}
๋ฌธ์ 2: ์ ๋ ฅ ํ ์ ํฌ๊ธฐ ๋ถ์ผ์น
Input tensor has incorrect size
โ ํด๊ฒฐ ๋ฐฉ๋ฒ
๋ชจ๋ธ์ด ๊ธฐ๋ํ๋ ์ ๋ ฅ ํฌ๊ธฐ๋ฅผ ํ์ธํ๊ณ ๋ง์ถฐ์ฃผ์ธ์.
// ์
๋ ฅ ํ
์ ์ ๋ณด ํ์ธ
val inputTensor = interpreter.getInputTensor(0)
println("์
๋ ฅ shape: ${inputTensor.shape().contentToString()}")
println("์
๋ ฅ ํ์
: ${inputTensor.dataType()}")
// ์: [1, 224, 224, 3] -> 1์ฅ์ 224x224 RGB ์ด๋ฏธ์ง
// ์ฌ๋ฐ๋ฅธ ํฌ๊ธฐ๋ก ์ ์ฒ๋ฆฌ
fun preprocessImage(bitmap: Bitmap, width: Int, height: Int): ByteBuffer {
val resized = Bitmap.createScaledBitmap(bitmap, width, height, true)
// ... ๋๋จธ์ง ์ฒ๋ฆฌ
}
๋ฌธ์ 3: ์ ํ๋๊ฐ ๋๋ฌด ๋ฎ์
1. ์ ์ฒ๋ฆฌ ํ์ธ
โข ์ด๋ฏธ์ง ์ ๊ทํ๊ฐ ํ์ต ์์ ๋์ผํ๊ฐ์?
โข RGB ์์๊ฐ ๋ง๋์? (RGB vs BGR)
โข ํ๊ท /ํ์คํธ์ฐจ ๊ฐ์ด ์ ํํ๊ฐ์?
// ํ์ต ์ ์ ๊ทํ
train_images = train_images / 255.0 # [0, 1] ๋ฒ์
// ์ถ๋ก ์์๋ ๋์ผํ๊ฒ
val normalized = pixelValue / 255.0f
2. ์์ํ ์ ํ๋ ์์คโข ์ ์ฒด ์ ์ ์์ํ๋ 1-2% ์ ํ๋ ์์ค ๊ฐ๋ฅ
โข ๋ํ ๋ฐ์ดํฐ์ ์ด ์ถฉ๋ถํ๊ฐ์? (์ต์ 100๊ฐ ์ํ)
โข ๋์ ๋ฒ์ ์์ํ๋ก ๋จผ์ ํ ์คํธํด๋ณด์ธ์
3. ๋ชจ๋ธ ๋ณํ ๋ฌธ์
โข ์ผ๋ถ ์ฐ์ฐ์๊ฐ TFLite์์ ์ง์๋์ง ์์ ์ ์์ด์
โข ๋ณํ ์ ๊ฒฝ๊ณ ๋ฉ์์ง๋ฅผ ํ์ธํ์ธ์
# ๋ณํ ์ ์์ธ ๋ก๊ทธ ํ์ธ
converter = tf.lite.TFLiteConverter.from_keras_model(model)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
tflite_model = converter.convert()
# ๋ณํ๋ ๋ชจ๋ธ ๊ฒ์ฆ
interpreter = tf.lite.Interpreter(model_content=tflite_model)
interpreter.allocate_tensors()
# ํ
์คํธ ๋ฐ์ดํฐ๋ก ์ ํ๋ ๋น๊ต
original_accuracy = model.evaluate(test_data)
tflite_accuracy = evaluate_tflite_model(interpreter, test_data)
print(f"์ ํ๋ ์ฐจ์ด: {original_accuracy - tflite_accuracy:.2%}")
๋ฌธ์ 4: ์ถ๋ก ์๋๊ฐ ๋๋ฌด ๋๋ฆผ
1. ํ๋์จ์ด ๊ฐ์ ์ฌ์ฉ ์ฌ๋ถ
โข GPU Delegate ๋๋ NNAPI ์ ์ฉํ๋์?
โข ๊ธฐ๊ธฐ๊ฐ ํด๋น ๊ฐ์์ ์ง์ํ๋์?
2. ๋ชจ๋ธ ์ต์ ํ
โข ์์ํ๋ฅผ ์ ์ฉํ๋์?
โข ๋ชจ๋ธ ํฌ๊ธฐ๊ฐ ์ ์ ํ๊ฐ์? (๋ชจ๋ฐ์ผ์ ๋ณดํต 50MB ์ดํ)
3. ์ ๋ ฅ ํฌ๊ธฐ
โข ์ ๋ ฅ ์ด๋ฏธ์ง๊ฐ ๋๋ฌด ํฌ์ง ์๋์?
โข 224x224 ๋๋ 192x192 ์ ๋๊ฐ ์ ๋นํด์
4. ์ค๋ ๋ ๊ฐ์
val options = Interpreter.Options()
options.setNumThreads(4) // CPU ์ฝ์ด ์์ ๋ง๊ฒ ์กฐ์
5. ํ๋กํ์ผ๋ง// ๊ฐ ๋จ๊ณ๋ณ ์๊ฐ ์ธก์
val t1 = System.currentTimeMillis()
val preprocessed = preprocessImage(bitmap)
val t2 = System.currentTimeMillis()
interpreter.run(preprocessed, output)
val t3 = System.currentTimeMillis()
val result = postprocess(output)
val t4 = System.currentTimeMillis()
println("์ ์ฒ๋ฆฌ: ${t2-t1}ms, ์ถ๋ก : ${t3-t2}ms, ํ์ฒ๋ฆฌ: ${t4-t3}ms")
๋ฌธ์ 5: ๋ฉ๋ชจ๋ฆฌ ๋ถ์กฑ (OutOfMemoryError)
1. ์ด๋ฏธ์ง ๋ค์ด์ํ๋ง
// ํฐ ์ด๋ฏธ์ง๋ฅผ ๋ก๋ฉํ ๋
val options = BitmapFactory.Options()
options.inSampleSize = 2 // 1/2 ํฌ๊ธฐ๋ก ๋ก๋ฉ
val bitmap = BitmapFactory.decodeFile(path, options)
2. Bitmap ์ฌํ์ฉ// ์ฌ์ฉ ํ ์ฆ์ ๋ฉ๋ชจ๋ฆฌ ํด์
bitmap.recycle()
3. ๋ฒํผ ์ฌ์ฌ์ฉ// ๋งค๋ฒ ์๋ก ํ ๋นํ์ง ๋ง๊ณ ์ฌ์ฌ์ฉ
class Classifier {
private val inputBuffer = ByteBuffer.allocateDirect(224 * 224 * 3 * 4)
fun classify(bitmap: Bitmap): Result {
inputBuffer.rewind() // ์ฌ์ฌ์ฉ
// ...
}
}
4. ๋ชจ๋ธ ํฌ๊ธฐ ์ค์ด๊ธฐโข ์์ํ ์ ์ฉ (ํฌ๊ธฐ 75% ๊ฐ์)
โข ๋ถํ์ํ ๋ ์ด์ด ์ ๊ฑฐ
โข ๋ ์์ ๋ชจ๋ธ ์ํคํ ์ฒ ์ฌ์ฉ (MobileNetV2 โ MobileNetV3-Small)
๐ ์ค์ ํ๋ก์ ํธ: ์ค์๊ฐ ๊ฐ์ฒด ํ์ง ์ฑ ๋ง๋ค๊ธฐ
์ด๋ก ์ ์ถฉ๋ถํ ๋ฐฐ์ ์ผ๋, ์ด์ ์ค์ ํ๋ก์ ํธ๋ฅผ ๋ง๋ค์ด๋ณผ๊น์? ์นด๋ฉ๋ผ๋ก ์ค์๊ฐ์ผ๋ก ๋ฌผ์ฒด๋ฅผ ์ธ์ํ๋ ์ฑ์ ๋ง๋ค์ด๋ณผ๊ฒ์! ๐ธ
ํ๋ก์ ํธ ๊ฐ์
๐ ๏ธ ์ฌ์ฉ ๊ธฐ์
โข TensorFlow Lite (SSD MobileNet ๋ชจ๋ธ)
โข CameraX (์นด๋ฉ๋ผ ์ฒ๋ฆฌ)
โข Canvas (๋ฐ์ด๋ฉ ๋ฐ์ค ๊ทธ๋ฆฌ๊ธฐ)
โข Kotlin Coroutines (๋น๋๊ธฐ ์ฒ๋ฆฌ)
๐ ์ฃผ์ ๊ธฐ๋ฅ
โข ์ค์๊ฐ ๊ฐ์ฒด ํ์ง (30fps ๋ชฉํ)
โข ๋ค์ค ๊ฐ์ฒด ๋์ ์ธ์
โข ์ ๋ขฐ๋ ํ์
โข ์ฑ๋ฅ ๋ชจ๋ํฐ๋ง (FPS, ์ถ๋ก ์๊ฐ)
Step 1: ํ๋ก์ ํธ ์ค์
// build.gradle (app)
dependencies {
// TensorFlow Lite
implementation 'org.tensorflow:tensorflow-lite:2.13.0'
implementation 'org.tensorflow:tensorflow-lite-gpu:2.13.0'
implementation 'org.tensorflow:tensorflow-lite-support:0.4.4'
// CameraX
def camerax_version = "1.3.0"
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}"
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
implementation "androidx.camera:camera-view:${camerax_version}"
// Coroutines
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
}
<!-- AndroidManifest.xml -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" />
Step 2: ๊ฐ์ฒด ํ์ง ํด๋์ค ๊ตฌํ
import android.content.Context
import android.graphics.Bitmap
import android.graphics.RectF
import org.tensorflow.lite.Interpreter
import org.tensorflow.lite.gpu.GpuDelegate
import org.tensorflow.lite.support.common.FileUtil
import org.tensorflow.lite.support.image.TensorImage
import org.tensorflow.lite.support.image.ops.ResizeOp
import java.nio.ByteBuffer
data class Detection(
val boundingBox: RectF,
val label: String,
val score: Float
)
class ObjectDetector(private val context: Context) : AutoCloseable {
private var interpreter: Interpreter? = null
private var gpuDelegate: GpuDelegate? = null
private val labels: List<String>
private val inputSize = 300 // SSD MobileNet ์
๋ ฅ ํฌ๊ธฐ
private val maxDetections = 10
init {
// ๋ผ๋ฒจ ๋ก๋ฉ
labels = FileUtil.loadLabels(context, "labels.txt")
}
fun initialize() {
// GPU Delegate ์ด๊ธฐํ
gpuDelegate = GpuDelegate()
// Interpreter ์ต์
val options = Interpreter.Options().apply {
addDelegate(gpuDelegate!!)
setNumThreads(4)
}
// ๋ชจ๋ธ ๋ก๋ฉ
val model = FileUtil.loadMappedFile(context, "detect.tflite")
interpreter = Interpreter(model, options)
}
fun detect(bitmap: Bitmap): List<Detection> {
val interpreter = this.interpreter ?: return emptyList()
// 1. ์ด๋ฏธ์ง ์ ์ฒ๋ฆฌ
val tensorImage = TensorImage.fromBitmap(bitmap)
val resizedImage = ResizeOp(inputSize, inputSize, ResizeOp.ResizeMethod.BILINEAR)
.apply(tensorImage)
// 2. ์ถ๋ ฅ ๋ฒํผ ์ค๋น
val locations = Array(1) { Array(maxDetections) { FloatArray(4) } }
val classes = Array(1) { FloatArray(maxDetections) }
val scores = Array(1) { FloatArray(maxDetections) }
val numDetections = FloatArray(1)
val outputs = mapOf(
0 to locations,
1 to classes,
2 to scores,
3 to numDetections
)
// 3. ์ถ๋ก ์คํ
interpreter.runForMultipleInputsOutputs(
arrayOf(resizedImage.buffer),
outputs
)
// 4. ๊ฒฐ๊ณผ ํ์ฑ
val detections = mutableListOf<Detection>()
val count = numDetections[0].toInt().coerceAtMost(maxDetections)
for (i in 0 until count) {
val score = scores[0][i]
if (score > 0.5f) { // ์ ๋ขฐ๋ 50% ์ด์๋ง
val box = RectF(
locations[0][i][1] * bitmap.width,
locations[0][i][0] * bitmap.height,
locations[0][i][3] * bitmap.width,
locations[0][i][2] * bitmap.height
)
val classIndex = classes[0][i].toInt()
val label = if (classIndex < labels.size) {
labels[classIndex]
} else {
"Unknown"
}
detections.add(Detection(box, label, score))
}
}
return detections
}
override fun close() {
interpreter?.close()
gpuDelegate?.close()
interpreter = null
gpuDelegate = null
}
}
Step 3: ์นด๋ฉ๋ผ ํ๋ฆฌ๋ทฐ์ ํตํฉ
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var detector: ObjectDetector
private lateinit var cameraExecutor: ExecutorService
private var inferenceTime = 0L
private var frameCount = 0
private var lastFpsTime = System.currentTimeMillis()
private var fps = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// ๊ฐ์ฒด ํ์ง๊ธฐ ์ด๊ธฐํ
detector = ObjectDetector(this)
detector.initialize()
// ์นด๋ฉ๋ผ ๊ถํ ํ์ธ
if (allPermissionsGranted()) {
startCamera()
} else {
ActivityCompat.requestPermissions(
this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS
)
}
cameraExecutor = Executors.newSingleThreadExecutor()
}
private fun startCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener({
val cameraProvider = cameraProviderFuture.get()
// ํ๋ฆฌ๋ทฐ ์ค์
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(binding.viewFinder.surfaceProvider)
}
// ์ด๋ฏธ์ง ๋ถ์ ์ค์
val imageAnalyzer = ImageAnalysis.Builder()
.setTargetResolution(Size(640, 480))
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
.also {
it.setAnalyzer(cameraExecutor, ObjectDetectionAnalyzer())
}
// ์นด๋ฉ๋ผ ์ ํ (ํ๋ฉด ์นด๋ฉ๋ผ)
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
try {
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(
this, cameraSelector, preview, imageAnalyzer
)
} catch (exc: Exception) {
Log.e(TAG, "์นด๋ฉ๋ผ ๋ฐ์ธ๋ฉ ์คํจ", exc)
}
}, ContextCompat.getMainExecutor(this))
}
private inner class ObjectDetectionAnalyzer : ImageAnalysis.Analyzer {
override fun analyze(imageProxy: ImageProxy) {
val startTime = System.currentTimeMillis()
// ImageProxy๋ฅผ Bitmap์ผ๋ก ๋ณํ
val bitmap = imageProxy.toBitmap()
// ๊ฐ์ฒด ํ์ง
val detections = detector.detect(bitmap)
inferenceTime = System.currentTimeMillis() - startTime
// UI ์
๋ฐ์ดํธ
runOnUiThread {
binding.overlayView.setDetections(detections, bitmap.width, bitmap.height)
updatePerformanceInfo()
}
imageProxy.close()
}
}
private fun updatePerformanceInfo() {
frameCount++
val currentTime = System.currentTimeMillis()
if (currentTime - lastFpsTime >= 1000) {
fps = frameCount
frameCount = 0
lastFpsTime = currentTime
}
binding.performanceText.text = """
FPS: $fps
์ถ๋ก ์๊ฐ: ${inferenceTime}ms
""".trimIndent()
}
override fun onDestroy() {
super.onDestroy()
cameraExecutor.shutdown()
detector.close()
}
companion object {
private const val TAG = "ObjectDetection"
private const val REQUEST_CODE_PERMISSIONS = 10
private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA)
}
}
// ImageProxy๋ฅผ Bitmap์ผ๋ก ๋ณํํ๋ ํ์ฅ ํจ์
fun ImageProxy.toBitmap(): Bitmap {
val yBuffer = planes[0].buffer
val uBuffer = planes[1].buffer
val vBuffer = planes[2].buffer
val ySize = yBuffer.remaining()
val uSize = uBuffer.remaining()
val vSize = vBuffer.remaining()
val nv21 = ByteArray(ySize + uSize + vSize)
yBuffer.get(nv21, 0, ySize)
vBuffer.get(nv21, ySize, vSize)
uBuffer.get(nv21, ySize + vSize, uSize)
val yuvImage = YuvImage(nv21, ImageFormat.NV21, width, height, null)
val out = ByteArrayOutputStream()
yuvImage.compressToJpeg(Rect(0, 0, width, height), 100, out)
val imageBytes = out.toByteArray()
return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
}
Step 4: ๋ฐ์ด๋ฉ ๋ฐ์ค ๊ทธ๋ฆฌ๊ธฐ
class OverlayView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private var detections = listOf<Detection>()
private var imageWidth = 0
private var imageHeight = 0
private val boxPaint = Paint().apply {
color = Color.GREEN
style = Paint.Style.STROKE
strokeWidth = 4f
}
private val textPaint = Paint().apply {
color = Color.WHITE
textSize = 40f
style = Paint.Style.FILL
}
private val backgroundPaint = Paint().apply {
color = Color.GREEN
style = Paint.Style.FILL
alpha = 180
}
fun setDetections(detections: List<Detection>, imageWidth: Int, imageHeight: Int) {
this.detections = detections
this.imageWidth = imageWidth
this.imageHeight = imageHeight
invalidate()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (imageWidth == 0 || imageHeight == 0) return
val scaleX = width.toFloat() / imageWidth
val scaleY = height.toFloat() / imageHeight
detections.forEach { detection ->
// ๋ฐ์ด๋ฉ ๋ฐ์ค ๊ทธ๋ฆฌ๊ธฐ
val box = RectF(
detection.boundingBox.left * scaleX,
detection.boundingBox.top * scaleY,
detection.boundingBox.right * scaleX,
detection.boundingBox.bottom * scaleY
)
canvas.drawRect(box, boxPaint)
// ๋ผ๋ฒจ ๊ทธ๋ฆฌ๊ธฐ
val label = "${detection.label} ${(detection.score * 100).toInt()}%"
val textBounds = Rect()
textPaint.getTextBounds(label, 0, label.length, textBounds)
val textX = box.left
val textY = box.top - 10
// ๋ฐฐ๊ฒฝ ๊ทธ๋ฆฌ๊ธฐ
canvas.drawRect(
textX,
textY - textBounds.height() - 10,
textX + textBounds.width() + 20,
textY + 5,
backgroundPaint
)
// ํ
์คํธ ๊ทธ๋ฆฌ๊ธฐ
canvas.drawText(label, textX + 10, textY, textPaint)
}
}
}
์ฑ๋ฅ ํ:
โข GPU ๊ฐ์์ผ๋ก 30fps ์ด์ ๋ฌ์ฑ ๊ฐ๋ฅ
โข ํด์๋๋ฅผ ๋ฎ์ถ๋ฉด ๋ ๋น ๋ฆ (640x480 โ 320x240)
โข ์ ๋ขฐ๋ ์๊ณ๊ฐ์ ์กฐ์ ํด์ ์ ํ๋์ ์๋ ๊ท ํ ๋ง์ถ๊ธฐ
โข ๋ฐฐํฐ๋ฆฌ ์๋ชจ๋ฅผ ์ค์ด๋ ค๋ฉด ํ๋ ์ ์คํต ๊ณ ๋ ค
๐ ์ฌ๋ฅ๋ท์์ AI ๊ฐ๋ฐ ์ญ๋ ํค์ฐ๊ธฐ
์ฌ๊ธฐ๊น์ง ์ฝ์ผ์ ์ฌ๋ฌ๋ถ, ์ ๋ง ๋๋จํด์! ๐ TensorFlow Lite์ ๊ธฐ์ด๋ถํฐ ์ค์ ํ๋ก์ ํธ๊น์ง ํจ๊ป ๋ฌ๋ ค์๋ค์.
ํ์ง๋ง ํผ์ ๊ณต๋ถํ๋ค ๋ณด๋ฉด ๋งํ๋ ๋ถ๋ถ๋ ์๊ณ , ์ค๋ฌด ๊ฒฝํ์ด ์๋ ์ฌ๋์ ์กฐ์ธ์ด ํ์ํ ๋๊ฐ ์์ฃ . ๊ทธ๋ด ๋๋ ์ฌ๋ฅ๋ท(https://www.jaenung.net)์ ํ์ฉํด๋ณด์ธ์!
์ฌ๋ฅ๋ท์๋ ๋ชจ๋ฐ์ผ AI ๊ฐ๋ฐ ๊ฒฝํ์ด ํ๋ถํ ์ ๋ฌธ๊ฐ๋ค์ด ๋ง์ด ๊ณ์ธ์. 1:1 ๋ฉํ ๋ง์ ๋ฐ๊ฑฐ๋, ํ๋ก์ ํธ ์ฝ๋ ๋ฆฌ๋ทฐ๋ฅผ ๋ฐ์ ์๋ ์๊ณ , ์ค๋ฌด์์ ๋ฐ๋ก ์ธ ์ ์๋ ํ๋ค์ ๋ฐฐ์ธ ์ ์์ด์. ๐ก
๋ฐ๋๋ก ์ฌ๋ฌ๋ถ์ด ์ด ๋ถ์ผ์ ์ด๋ ์ ๋ ์ต์ํด์ง๋ฉด, ์ฌ๋ฅ๋ท์์ ์ฌ๋ฌ๋ถ์ ์ง์์ ๋๋๋ฉฐ ์์ต์ ์ฐฝ์ถํ ์๋ ์์ด์. ์ด๋ณด์๋ค์๊ฒ TensorFlow Lite๋ฅผ ๊ฐ๋ฅด์ณ์ฃผ๊ฑฐ๋, ๊ฐ๋จํ AI ๋ชจ๋ธ ํตํฉ์ ๋์์ฃผ๋ ๊ฒ๋ง์ผ๋ก๋ ์ถฉ๋ถํ ๊ฐ์น ์๋ ์ฌ๋ฅ์ด ๋ ์ ์๋ต๋๋ค!
๋ค์ ๋จ๊ณ: ๋ ๊น์ด ํ๊ณ ๋ค๊ธฐ
์ด ๊ธ์์ ๋ค๋ฃฌ ๋ด์ฉ์ ์์์ ๋ถ๊ณผํด์. ๋ ๊น์ด ๊ณต๋ถํ๊ณ ์ถ๋ค๋ฉด ์ด๋ฐ ์ฃผ์ ๋ค์ ํ๊ตฌํด๋ณด์ธ์:
โข ๋ชจ๋ธ ์ํคํ ์ฒ ์ค๊ณ ๋ฐ ์ต์ ํ
โข ๋ฐ์ดํฐ ์ฆ๊ฐ(Data Augmentation) ๊ธฐ๋ฒ
2. ์ฃ์ง TPU ํ์ฉ โข Google Coral ๋ณด๋๋ก ๋ ๋น ๋ฅธ ์ถ๋ก
โข ์๋ฒ ๋๋ ์์คํ ์์์ AI
โข IoT ๊ธฐ๊ธฐ์ ํตํฉ
3. ์ฐํฉ ํ์ต (Federated Learning) โข ํ๋ผ์ด๋ฒ์๋ฅผ ๋ณดํธํ๋ฉด์ ๋ชจ๋ธ ๊ฐ์
โข ์จ๋๋ฐ์ด์ค์์ ๋ชจ๋ธ ์ ๋ฐ์ดํธ
โข TensorFlow Federated ํ์ฉ
4. ๋ชจ๋ธ ์์ถ ๊ธฐ๋ฒ โข ํ๋ฃจ๋(Pruning): ๋ถํ์ํ ๊ฐ์ค์น ์ ๊ฑฐ
โข ์ง์ ์ฆ๋ฅ(Knowledge Distillation): ํฐ ๋ชจ๋ธ์ ์ง์์ ์์ ๋ชจ๋ธ๋ก
โข Neural Architecture Search: ์ต์ ์ ๋ชจ๋ธ ๊ตฌ์กฐ ์๋ ํ์
5. ๋ฉํฐ๋ชจ๋ฌ AI โข ์ด๋ฏธ์ง + ํ ์คํธ ๋์ ์ฒ๋ฆฌ
โข ์์ฑ + ๋น๋์ค ํตํฉ
โข ์ผ์ ๋ฐ์ดํฐ ์ตํฉ
์ ์ฉํ ๋ฆฌ์์ค
โข TensorFlow Lite ๊ณต์ ๊ฐ์ด๋: tensorflow.org/lite
โข TensorFlow Model Garden: ์ฌ์ ํ์ต๋ ๋ชจ๋ธ ๋ชจ์
โข ML Kit: Google์ ๋ชจ๋ฐ์ผ ML SDK
์ปค๋ฎค๋ํฐ:
โข TensorFlow Forum: ์ง๋ฌธ๊ณผ ๋ต๋ณ
โข GitHub Issues: ๋ฒ๊ทธ ๋ฆฌํฌํธ ๋ฐ ๊ธฐ๋ฅ ์์ฒญ
โข Stack Overflow: ์ค๋ฌด ๋ฌธ์ ํด๊ฒฐ
๋๊ตฌ:
โข Netron: ๋ชจ๋ธ ์๊ฐํ ๋๊ตฌ
โข TensorBoard: ํ์ต ๋ชจ๋ํฐ๋ง
โข Android Profiler: ์ฑ๋ฅ ๋ถ์
๐ฌ ๋ง๋ฌด๋ฆฌํ๋ฉฐ
์, ์ ๋ง ๊ธด ์ฌ์ ์ด์์ฃ ? ๐
ํ์ง๋ง ์ฌ๋ฌ๋ถ์ ์ด์ ๋ชจ๋ฐ์ผ AI ๊ฐ๋ฐ์ ํต์ฌ์ ์ดํดํ๊ณ ์์ด์!
TensorFlow Lite๋ฅผ ์ฌ์ฉํ๋ฉด ์๋ฒ ์์ด๋ ์ค๋งํธํฐ์์ ๊ฐ๋ ฅํ AI ๊ธฐ๋ฅ์ ๊ตฌํํ ์ ์์ด์. ๋น ๋ฅด๊ณ , ์์ ํ๊ณ , ํจ์จ์ ์ด์ฃ . ๊ฒ๋ค๊ฐ ์ฌ์ฉ์ ๊ฒฝํ๋ ํจ์ฌ ์ข์์ ธ์. ์ธํฐ๋ท ์ฐ๊ฒฐ ์์ด๋ ์๋ํ๊ณ , ์๋ต์ด ์ฆ๊ฐ์ ์ด๋๊น์! โก
์ฒ์์๋ ์ด๋ ต๊ฒ ๋๊ปด์ง ์ ์์ง๋ง, ํ๋์ฉ ๋ฐ๋ผํ๋ค ๋ณด๋ฉด ์ด๋์ ์ฌ๋ฌ๋ถ๋ง์ AI ์ฑ์ ๋ง๋ค๊ณ ์์ ๊ฑฐ์์. ์์ ํ๋ก์ ํธ๋ถํฐ ์์ํด์ ์ ์ ๋ณต์กํ ๊ธฐ๋ฅ์ ์ถ๊ฐํด๋ณด์ธ์.
๊ทธ๋ฆฌ๊ณ ๋งํ๋ ๋ถ๋ถ์ด ์๋ค๋ฉด ์ฃผ์ ํ์ง ๋ง๊ณ ์ปค๋ฎค๋ํฐ์ ์ง๋ฌธํ๊ฑฐ๋, ์ฌ๋ฅ๋ท์์ ์ ๋ฌธ๊ฐ์ ๋์์ ๋ฐ์๋ณด์ธ์. ํผ์ ๊ณ ๋ฏผํ๋ ๊ฒ๋ณด๋ค ํจ์ฌ ๋น ๋ฅด๊ฒ ์ฑ์ฅํ ์ ์์ด์! ๐ฑ
๐ ์ฌ๋ฌ๋ถ์ AI ์ฌ์ ์ ์์ํฉ๋๋ค!
๋ชจ๋ฐ์ผ AI๋ ๊ณ์ ๋ฐ์ ํ๊ณ ์์ด์.
์ค๋ ๋ฐฐ์ด ๋ด์ฉ์ด ๋ด์ผ์ ํ์ ์ ์ธ ์ฑ์ด ๋ ์ ์์ด์.
์ฌ๋ฌ๋ถ์ ์ฐฝ์๋ ฅ๊ณผ ๊ธฐ์ ์ด ๋ง๋๋ฉด ์ ๋ง ๋ฉ์ง ์ผ๋ค์ด ์ผ์ด๋ ๊ฑฐ์์!
Happy Coding! ๐ปโจ
์ด ๊ธ์ด ๋์์ด ๋์๋ค๋ฉด, ์ฌ๋ฌ๋ถ์ ํ๋ก์ ํธ์ ์ ์ฉํด๋ณด๊ณ ๊ฒฝํ์ ๊ณต์ ํด์ฃผ์ธ์!
๊ทธ๋ฆฌ๊ณ ๋ ๊ถ๊ธํ ์ ์ด ์๋ค๋ฉด ์ธ์ ๋ ์ฌ๋ฅ๋ท์์ ์ ๋ฌธ๊ฐ๋ค๊ณผ ์ํตํด๋ณด์ธ์. ๐
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.