이번 강의에서는 WeGo LIMO에서 사용하는 TensorFlow 기반 트랙 주행 모델의 학습 과정을 자세히 살펴보겠습니다.
사용하는 소스는 다음 Jupyter Notebook입니다.
train_tensorflow_track_detection_docker.ipynb
이 소스의 이름에는 Track Detection이 들어 있지만, 코드를 자세히 살펴보면 일반적인 객체 검출 Object Detection 모델과는 구조가 다릅니다.
이 모델은 카메라 영상에서 트랙의 특정 정보를 찾아 2개의 연속적인 숫자값을 출력하는 Regression, 즉 회귀 모델입니다.
전체 흐름은 다음과 같습니다.
LIMO Camera로 이미지 수집
↓
이미지 파일 저장
↓
파일명에서 2개의 Label 추출
↓
트랙이 있는 ROI 영역 Crop
↓
TensorFlow CNN 입력
↓
Feature Extraction
↓
2개의 값 예측
↓
dl_drive.h5 저장
↓
실제 LIMO 주행 프로그램에서 사용
여기서 매우 중요한 부분이 있습니다.
이 Notebook 자체는 LIMO 모터를 직접 움직이지 않습니다.
이 소스의 역할은
LIMO가 트랙을 주행할 때 사용할 딥러닝 모델을 학습하고
dl_drive.h5파일로 저장하는 것
입니다.
즉 전체 LIMO 시스템에서 다음 부분을 담당합니다.
데이터 수집
↓
★
TensorFlow Training
↓
dl_drive.h5
★
↓
ROS 2 추론 Node
↓
주행 제어
↓
/cmd_vel
↓
LIMO
이번 강의에서는 별표로 표시한 Training Model 생성 부분을 공부합니다.
1. 전체 Notebook의 실행 순서
소스에서는 작업 순서를 다음과 같이 구성하고 있습니다.
1. Docker 환경 및 경로 확인
2. Dataset 준비 및 압축 해제
3. TensorFlow / OpenCV 등의 Package 확인
4. Dataset 이미지 목록 생성
5. 이미지와 Label 전처리
6. CNN Model 생성
7. Optimizer와 Callback 설정
8. Model Training
9. Validation Loss가 가장 좋은 Model 저장
10. dl_drive.h5 생성
Notebook의 기본 경로는 다음과 같습니다.
작업 공간
/workspace
Dataset
/workspace/dataset/dl_images
Model 저장
/workspace/outputs/dl_drive.h5
따라서 Docker Container의 /workspace가 이 학습 프로젝트의 기준 Directory가 됩니다.
2. Docker 환경을 사용하는 이유
딥러닝 개발에서 가장 자주 발생하는 문제 중 하나가 Package 버전 충돌입니다.
대표적으로 다음 Package들이 서로 영향을 줍니다.
Python
TensorFlow
NumPy
ml_dtypes
CUDA
cuDNN
OpenCV
예를 들어 TensorFlow 버전은 새로운데 ml_dtypes가 너무 오래되었다면 다음과 같은 문제가 발생할 수 있습니다.
TensorFlow
↓
새로운 Data Type 필요
↓
ml_dtypes
↓
해당 Data Type 없음
↓
Import Error
이 Notebook은 이런 문제를 줄이기 위해 실행 중에 TensorFlow 등을 무작정 다시 설치하지 않습니다.
# Docker 컨테이너 실행 중 TensorFlow / NumPy /
# ml_dtypes / JAX를 강제로 uninstall/install 하지 않습니다.
즉 Docker Image를 만들 때 정상적인 Package 조합을 만들어 놓고 Notebook에서는 그 환경을 그대로 사용하는 방식입니다.
이것은 실무에서도 좋은 방법입니다.
개발자 A 컴퓨터
개발자 B 컴퓨터
강의실 PC
Notebook PC
GPU Workstation
환경이 서로 달라도 Docker Image를 같게 사용하면 상당히 동일한 실행환경을 유지할 수 있기 때문입니다.
3. 작업 Directory 설정
첫 번째 주요 코드를 살펴보겠습니다.
WORKSPACE_DIR = Path(
os.environ.get(
"WORKSPACE_DIR",
"/workspace"
)
).resolve()
환경변수 WORKSPACE_DIR이 있으면 그 값을 사용합니다.
없다면 기본적으로
/workspace
를 사용합니다.
같은 방법으로 Dataset Directory를 설정합니다.
DATASET_DIR = Path(
os.environ.get(
"DATASET_DIR",
WORKSPACE_DIR / "dataset"
)
).resolve()
따라서 기본 경로는
/workspace/dataset
입니다.
Model 출력 Directory는
OUTPUT_DIR = Path(
os.environ.get(
"OUTPUT_DIR",
WORKSPACE_DIR / "outputs"
)
).resolve()
입니다.
기본적으로
/workspace/outputs
가 됩니다.
4. 최종 Directory 구조
소스가 기대하는 기본적인 Folder 구조는 다음과 같습니다.
/workspace
│
├── dataset
│ └── dl_images
│ ├── image001.jpg
│ ├── image002.jpg
│ ├── image003.jpg
│ └── ...
│
├── outputs
│ └── dl_drive.h5
│
├── dataset.zip
│
└── train_tensorflow_track_detection_docker.ipynb
이미 Dataset 압축을 풀어 놓았다면
/workspace/dataset/dl_images
안에 JPG 파일이 존재하면 됩니다.
5. 출력 Directory 자동 생성
다음 코드가 있습니다.
OUTPUT_DIR.mkdir(
parents=True,
exist_ok=True
)
이 코드는 Model을 저장할 Directory가 없으면 자동으로 생성합니다.
parents=True는 상위 Directory까지 필요한 경우 생성하겠다는 의미입니다.
exist_ok=True는 이미 Directory가 존재하더라도 Error를 발생시키지 않는다는 의미입니다.
따라서
/workspace/outputs
가 이미 있어도 문제가 없습니다.

6. Dataset 자동 압축 해제
Dataset Directory가 없으면 다음 과정을 수행합니다.
if not IMAGE_DIR.exists():
if DATASET_ZIP.exists():
...
즉
/workspace/dataset/dl_images
가 존재하지 않지만
/workspace/dataset.zip
이 존재한다면 ZIP 파일을 자동으로 압축 해제합니다.
실제 코드는 다음과 같습니다.
with zipfile.ZipFile(DATASET_ZIP, "r") as zf:
zf.extractall(WORKSPACE_DIR)
결과적으로 다음 구조가 만들어져야 합니다.
dataset.zip
↓
압축 해제
↓
/workspace/dataset/dl_images/*.jpg
7. Dataset이 없으면 즉시 중단한다
Dataset이 전혀 없다면 소스에서는 그냥 계속 진행하지 않습니다.
raise FileNotFoundError(...)
를 발생시킵니다.
이것은 좋은 방어 코드입니다.
Dataset이 없는데 Training까지 진행하면 뒤에서 이해하기 어려운 Error가 발생하기 때문입니다.
따라서 가능한 한 문제 발생 위치와 가까운 곳에서 프로그램을 중단합니다.

8. Package 버전을 먼저 검사한다
다음으로 중요한 부분입니다.
packages = [
"tensorflow",
"numpy",
"ml-dtypes",
"opencv-python-headless",
"tqdm",
]
다음 Package의 설치 상태를 검사합니다.
TensorFlow
NumPy
ml-dtypes
OpenCV
tqdm
각 역할은 다음과 같습니다.
| Package | 역할 |
|---|---|
| TensorFlow | 딥러닝 Model 생성과 학습 |
| NumPy | 이미지와 Label 배열 처리 |
| ml-dtypes | TensorFlow의 Machine Learning Data Type 지원 |
| OpenCV | 이미지 파일 읽기 |
| tqdm | Dataset 전처리 진행률 표시 |

9. 실제 사용하는 Python Module
다음 Module들을 Import합니다.
import glob
import os
import re
import time
import random
import cv2
import numpy as np
import ml_dtypes
import tensorflow as tf
from tensorflow import keras
그리고 Callback과 Optimizer를 Import합니다.
from tensorflow.keras.callbacks import (
EarlyStopping,
LearningRateScheduler,
ModelCheckpoint
)
from tensorflow.keras.optimizers import Adam
각 기능은 뒤에서 자세히 설명하겠습니다.
10. TensorFlow 환경 검사가 중요한 이유
소스에서는 실제 TensorFlow와 관련 Library의 버전을 출력합니다.
print("TensorFlow :", tf.__version__)
print("NumPy :", np.__version__)
print("ml_dtypes :", ml_dtypes.__version__)
print("OpenCV :", cv2.__version__)
딥러닝 프로그램에서 Package 버전을 기록해 두는 것은 매우 중요합니다.
왜냐하면
어제는 됐는데 오늘은 안 된다
라는 문제가 발생했을 때 상당수가 Package Version 차이에서 발생하기 때문입니다.
강의에서도 정상적으로 실행된 환경의 Version을 기록해 두는 것을 권장합니다.
11. ml_dtypes 호환성을 직접 검사한다
특히 이 Notebook에는 다음 검사가 추가되어 있습니다.
if not hasattr(
ml_dtypes,
"float4_e2m1fn"
):
raise RuntimeError(...)
이 코드는 현재 ml_dtypes가 TensorFlow가 필요로 하는 Data Type을 지원하는지 확인합니다.
즉 다음 구조입니다.
ml_dtypes
↓
float4_e2m1fn 존재?
↓
YES
↓
계속 실행
NO
↓
RuntimeError
이 검사가 필요한 이유는 TensorFlow와 ml_dtypes Version이 서로 맞지 않으면 TensorFlow가 Import 단계에서 실패할 수 있기 때문입니다.
12. GPU가 있는지 확인한다
다음 코드는 TensorFlow가 GPU를 인식하고 있는지 확인합니다.
gpus = tf.config.list_physical_devices("GPU")
GPU가 있다면
GPU devices:
[PhysicalDevice(...)]
같은 결과가 나타납니다.
없으면
CPU only
가 출력됩니다.
딥러닝 Model은 CPU에서도 학습할 수 있지만 CNN Training은 GPU를 사용하는 편이 훨씬 유리합니다.
전체 관계는 다음과 같습니다.
TensorFlow
↓
CUDA / cuDNN
↓
NVIDIA GPU
↓
CNN Training
13. GPU Memory Growth 설정
다음 코드도 중요합니다.
tf.config.experimental.set_memory_growth(
gpu,
True
)
TensorFlow가 시작할 때 GPU Memory를 필요 이상으로 한 번에 점유하지 않고 실제 필요에 따라 증가시키도록 설정합니다.
개념적으로는
Memory Growth OFF
TensorFlow 시작
↓
GPU Memory를 크게 확보
대신
Memory Growth ON
TensorFlow 시작
↓
필요한 만큼 사용
↓
필요하면 추가 확보
방식입니다.
강의실이나 개발 PC에서 GPU를 다른 프로그램과 함께 사용할 때 특히 유용할 수 있습니다.


14. 학습 이미지 파일 목록 만들기
Dataset을 읽는 부분입니다.
file_list = sorted(
glob.glob(
str(IMAGE_DIR / "*.jpg")
)
)
glob.glob()은 특정 Pattern에 해당하는 파일 목록을 찾아줍니다.
여기에서는
*.jpg
이므로 모든 JPG 이미지를 찾습니다.
예:
001_123_150.jpg
002_125_148.jpg
003_130_145.jpg
...
그리고
sorted()
를 사용하여 파일 목록을 정렬합니다.
15. 이미지 개수 확인
다음 정보를 출력합니다.
print("First image :", file_list[0])
print("Last image :", file_list[-1])
print("Image count :", len(file_list))
이를 통해
첫 번째 이미지
마지막 이미지
전체 이미지 수
를 확인할 수 있습니다.
Dataset Training을 시작하기 전에 반드시 확인해야 하는 정보입니다.

16. 이 모델에서 가장 중요한 특징은 Label이 파일명에 있다는 것이다
이 Notebook의 매우 중요한 특징입니다.
별도의 CSV Label 파일을 읽지 않습니다.
대신 JPG 파일명에서 숫자를 추출하여 Label로 사용합니다.
다음 코드입니다.
filename = os.path.basename(file)
name, _ = os.path.splitext(filename)
먼저 Directory를 제거하고 순수 파일 이름만 가져옵니다.
예를 들어
/workspace/dataset/dl_images/120_180.jpg
라면
120_180.jpg
가 됩니다.
확장자도 제거합니다.
120_180
가 됩니다.
17. 정규표현식으로 숫자 추출
핵심 코드입니다.
numbers = re.findall(
r"-?\d+(?:\.\d+)?",
name
)
이 정규표현식은 파일 이름에서 숫자를 찾습니다.
다음 값을 처리할 수 있습니다.
100
250
-30
125.5
-42.7
구조를 나누어 보면
-?
는 음수 부호가 있을 수도 있다는 뜻입니다.
\d+
는 한 개 이상의 숫자를 의미합니다.
(?:\.\d+)?
는 소수점 이하 숫자가 존재할 수도 있다는 뜻입니다.
따라서 Integer뿐 아니라 Floating Point Label도 처리할 수 있습니다.
18. 파일명에서 앞의 2개 숫자를 Label로 사용한다
코드는 다음과 같습니다.
value1 = float(numbers[0])
value2 = float(numbers[1])
즉 파일 이름에서 발견한 첫 번째 숫자와 두 번째 숫자를 하나의 Label로 사용합니다.
예를 들어 파일명이
125_238.jpg
와 같은 형식이라면
value1 = 125
value2 = 238
이 됩니다.
Label은 최종적으로
label.append([
value1,
value2
])
형태로 저장됩니다.
따라서 Model이 출력해야 하는 값도 정확하게 2개입니다.
주의할 점은 이 Notebook 자체에서는 value1, value2가 물리적으로 무엇을 의미하는지 별도 설명하지 않는다는 것입니다.
코드상 확실하게 알 수 있는 내용은
이미지 파일명에서 추출한 첫 번째와 두 번째 숫자를 연속형 정답값으로 학습한다
는 것입니다.
실제 데이터 수집 프로그램에서 이 두 숫자를 어떤 트랙 좌표나 Target 정보로 파일명에 저장했는지는 해당 데이터 생성 코드와 함께 확인해야 합니다.
19. Label이 없는 파일은 버린다
만약 숫자가 2개 미만이라면
if len(numbers) < 2:
print("라벨 추출 실패:", filename)
continue
로 처리합니다.
즉 해당 이미지는 Training Dataset에서 제외합니다.
예를 들어
image.jpg
처럼 숫자가 전혀 없는 파일은 사용할 수 없습니다.
이 모델에서는 파일 이름 자체가 Label 정보를 가지고 있기 때문입니다.
20. OpenCV로 이미지 읽기
다음 코드로 이미지를 읽습니다.
X = cv2.imread(
file,
cv2.IMREAD_COLOR
)
cv2.IMREAD_COLOR이므로 Color Image로 읽습니다.
OpenCV에서 일반적으로 Color Image Channel 순서는
B
G
R
입니다.
즉 TensorFlow 모델에 들어가는 Training Image도 이 코드 기준에서는 BGR 순서입니다.
이 부분은 나중에 LIMO 실시간 Camera에서 추론할 때 매우 중요합니다.
Training 때 BGR로 학습했다면 실제 추론에서도 같은 Channel 순서를 사용하는 것이 안전합니다.
21. 너무 작은 이미지는 제외한다
다음 검사를 수행합니다.
if X.shape[0] < 300 or X.shape[1] < 300:
...
continue
즉 최소한
Height >= 300
Width >= 300
인 이미지가 필요합니다.
왜 300 Pixel이 필요한지는 바로 다음 Crop 코드에서 알 수 있습니다.
22. 이 모델의 핵심 전처리 ROI
가장 중요한 코드 중 하나입니다.
crop_img = X[190:300, :300]
OpenCV와 NumPy Image의 배열은
image[y, x]
순서입니다.
따라서
X[190:300, :300]
은
Y = 190 ~ 299
X = 0 ~ 299
영역을 가져옵니다.
결과 Image Size는
Height = 110
Width = 300
Channel = 3
입니다.
즉
110 × 300 × 3
Image가 Model 입력으로 들어갑니다.
23. 왜 전체 영상을 사용하지 않고 ROI를 사용하는가
트랙 주행에서는 영상 전체가 모두 중요하지 않습니다.
예를 들어 Camera 화면이 다음과 같다고 생각해 보겠습니다.

로봇의 주행 판단에는 화면 아래쪽 Track 영역이 중요합니다.
따라서 필요 없는 위쪽 영역을 제거하면
불필요한 Background 감소
입력 데이터 감소
연산량 감소
Track 정보 집중
효과를 얻을 수 있습니다.
이 Notebook에서는 그 ROI를 정확하게
Y = 190 ~ 299
X = 0 ~ 299
로 고정하고 있습니다.
24. 실시간 추론에서도 반드시 같은 ROI를 사용해야 한다
이 부분은 매우 중요합니다.
Training에서
crop_img = X[190:300, :300]
를 사용했다면 실제 LIMO 주행에서도 같은 전처리가 필요합니다.
즉
Training
Camera Image
↓
[190:300, :300]
↓
Model
이었다면 실제 주행도
Inference
LIMO Camera
↓
[190:300, :300]
↓
dl_drive.h5
로 만들어야 합니다.
Training과 Inference의 전처리가 달라지면 Model 성능이 크게 떨어질 수 있습니다.
25. 이미지 Resize를 하지 않는다는 것도 중요하다
이 코드는 cv2.resize()를 사용하지 않습니다.
따라서 원본 Image에서 바로
X[190:300, :300]
을 잘라냅니다.
즉 Camera Resolution과 위치가 바뀌면 동일한 영역이 완전히 다른 장면을 포함할 수 있습니다.
예를 들어 Training Camera가
300 × 300
기준인데 실제 Camera가
640 × 480
으로 바뀌었다면
:300
은 화면 전체가 아니라 왼쪽 300 Pixel만 사용합니다.
따라서 실제 주행에서 Camera Resolution과 Crop 기준을 Training 환경과 일치시키는 것이 중요합니다.
26. Dataset을 NumPy 배열로 변환한다
전처리가 끝난 후 다음 코드가 실행됩니다.
label = np.asarray(
label,
dtype=np.float32
)
Label은 float32로 변환됩니다.
Deep Learning에서 가장 일반적으로 사용하는 실수형 Data Type 중 하나입니다.
Image는
img = np.ascontiguousarray(
np.asarray(
img,
dtype=np.uint8
)
)
로 변환합니다.
Image Pixel은
0 ~ 255
범위이므로 uint8을 사용합니다.
27. ascontiguousarray는 왜 사용하는가
NumPy 배열은 메모리에 항상 연속적으로 배치된다고 보장할 수 없습니다.
np.ascontiguousarray()를 사용하면 배열을 C-Style의 연속된 Memory Layout으로 확보합니다.
개념적으로
Memory
[Image1][Image2][Image3][Image4]...
처럼 연속된 형태로 정리합니다.
TensorFlow 등의 Library에 큰 배열을 전달할 때 안정적인 Memory Layout을 확보하는 데 도움이 됩니다.
28. Label을 300으로 나눈다
매우 중요한 전처리입니다.
label = label / 300.0
예를 들어 원래 Label이
150
240
이라면
150 / 300 = 0.5
240 / 300 = 0.8
이 됩니다.
즉
[150, 240]
이라는 Label이
[0.5, 0.8]
로 변환됩니다.
이것을 Normalization, 정규화라고 합니다.
29. 왜 Label을 정규화하는가
Neural Network는 지나치게 큰 값보다 일정한 범위의 값을 다루는 편이 일반적으로 학습 안정성에 유리합니다.
이 Model의 마지막 Layer도 다음과 같습니다.
Dense(
2,
activation="tanh"
)
tanh의 출력 범위는
-1 ~ +1
입니다.
Label을 300으로 나누면 좌표 성격의 값이 0~300 범위라고 가정할 경우
0 ~ 1
범위가 되어 tanh 출력 범위 안에 들어갑니다.
음수 Label을 사용하는 Dataset이라면 정규화 결과 역시 음수가 될 수 있습니다.
30. 중요한 특징: Image Pixel은 0~1로 정규화하지 않는다
소스를 자세히 보면
img = img / 255.0
코드가 없습니다.
또한 Model 안에도
Rescaling(1.0 / 255.0)
Layer가 없습니다.
따라서 이 Notebook은 Image를
uint8
0 ~ 255
상태로 Model에 전달합니다.
이것이 반드시 잘못되었다는 뜻은 아닙니다.
중요한 것은 Training과 실제 Inference의 입력 조건을 동일하게 유지하는 것입니다.
즉 이 모델을 그대로 사용할 경우 실시간 주행 코드에서 임의로
image = image / 255.0
를 추가하면 Training과 다른 Input Distribution이 되어 성능이 달라질 수 있습니다.
31. 전처리 결과 Shape
최종적으로 다음 형태가 됩니다.
img shape
(N, 110, 300, 3)
여기서 N은 사용할 수 있는 이미지 개수입니다.
Label은
label shape
(N, 2)
입니다.
즉 Dataset 한 개는 다음과 같은 구조입니다.
Input
110 × 300 × 3 Image
+
Output
2개의 Float 값
입니다.


32. 이 문제는 Classification이 아니라 Regression이다
Model 마지막을 보면
output = keras.layers.Dense(
2,
activation="tanh",
name="track_output"
)(norm12)
입니다.
출력값이
LEFT
RIGHT
STRAIGHT
같은 Class가 아닙니다.
다음처럼 두 개의 연속적인 숫자를 출력합니다.
[0.42, 0.73]
따라서 이 Model은 Regression Model입니다.
즉
Image
↓
CNN
↓
2개의 연속적인 수치 예측
구조입니다.
33. Model의 전체 구조
Model 구조를 간략하게 정리하면 다음과 같습니다.
Input
110 × 300 × 3
↓
Conv2D 16
↓
BatchNormalization
↓
MaxPooling
↓
Conv2D 32
↓
BatchNormalization
↓
Conv2D 32
↓
BatchNormalization
↓
Residual Add
↓
Conv2D 64
↓
Conv2D 64
↓
Residual Add
↓
Conv2D 128
↓
Conv2D 128
↓
Residual Add
↓
Conv2D 256
↓
Conv2D 512
↓
Flatten
↓
Dense 128
↓
Dense 64
↓
Dense 64
↓
Dense 2
↓
Track Output
34. 첫 번째 Input Layer
input1 = keras.layers.Input(
shape=(110, 300, 3),
name="image"
)
정확하게 앞에서 Crop한 Image 크기와 같습니다.
Height = 110
Width = 300
Channel = 3
따라서 전처리와 Model Input Shape가 정확하게 연결됩니다.
X[190:300, :300]
↓
110 × 300 × 3
↓
Input Layer
35. 첫 번째 Convolution Layer
conv1 = keras.layers.Conv2D(
16,
(3, 3),
strides=(2, 2),
padding="same",
activation="swish"
)(input1)
여기에는 중요한 Parameter가 여러 개 있습니다.
Filter = 16
Kernel = 3 × 3
Stride = 2 × 2
Padding = same
Activation = Swish
3×3 Kernel이 Image 위를 이동하면서 Feature를 추출합니다.
Stride = 2×2 는 필터가 가로·세로로 2칸씩 이동하는 것을 의미합니다.
Padding = same은 출력 크기를 ceil(입력 / stride) 규칙으로 맞추도록 가장자리를 padding 합니다.
따라서 Stride=2 + Same Padding 을 수행하면 이미지 가로·세로 크기가 대략 절반입니다.
초기 Layer에서는 주로
Edge
Line
Corner
Color 변화
와 같은 비교적 단순한 특징을 학습합니다.
36. Stride 2를 사용하는 이유
Stride가
2 × 2
라는 것은 Kernel이 한 Pixel씩 움직이지 않고 두 Pixel 단위로 이동한다는 의미입니다.
동시에 Feature Map 크기도 줄어듭니다.
입력
110 × 300 × 3
에서 첫 Conv2D를 지나면 대략
55 × 150 × 16
이 됩니다.
즉 Feature는 늘어나지만 공간 크기는 감소합니다.
37. BatchNormalization
Conv Layer 뒤에 다음 코드가 반복됩니다.
keras.layers.BatchNormalization()
Batch Normalization은 학습 중 Layer의 값 분포를 조정하여 Training을 보다 안정적으로 만드는 데 사용됩니다.
전체 흐름은
Convolution
↓
Batch Normalization
↓
다음 Layer
입니다.
이 Model에서는 CNN뿐 아니라 Dense Layer 사이에도 BatchNormalization을 적극적으로 사용하고 있습니다.
38. MaxPooling
첫 번째 Block에는
pool1 = keras.layers.MaxPooling2D(
pool_size=(3, 3),
strides=(2, 2)
)(norm1)
이 있습니다.
3×3 영역에서 큰 값을 중심으로 Feature를 압축합니다.
이를 통해
Feature 유지
공간 크기 감소
계산량 감소
효과를 얻습니다.
첫 Conv 이후 약
55 × 150 × 16
이었다면 Pooling 이후에는 약
27 × 74 × 16
이 됩니다.
39. 두 번째 Convolution Block
다음 부분입니다.
conv2 = Conv2D(
32,
(3, 3),
strides=(2, 2),
padding="same",
activation="swish"
)(pool1)
Feature Channel이
16 → 32
로 증가합니다.
그리고 다음 Conv Layer에서는 공간 크기를 유지합니다.
conv3 = Conv2D(
32,
(3, 3),
strides=(1, 1),
padding="same",
activation="swish"
)(norm2)
40. Residual Connection
이 모델의 중요한 특징 중 하나입니다.
add1 = keras.layers.Add()([
norm2,
norm3
])
즉

구조입니다.
이러한 구조를 흔히 Residual Connection 또는 Skip Connection이라고 부릅니다.
다음 Layer를 무조건 거쳐야 하는 것이 아니라 이전 Feature를 뒤쪽 결과와 직접 더합니다.
개념적으로

형태입니다.
Deep Network의 Training을 보다 안정적으로 만드는 데 널리 사용하는 구조입니다.
41. 64 Channel Block
다음 단계에서는
Conv2D(64)
를 사용합니다.
32 Channel
↓
64 Channel
로 Feature 수를 증가시킵니다.
그리고 다시
conv5 = Conv2D(64)
를 통과시킨 후
add2 = Add()([
norm4,
norm5
])
로 Residual Connection을 구성합니다.
42. 128 Channel Block
그 다음은
64
↓
128
입니다.
코드는
conv6 = Conv2D(
128,
(3, 3),
strides=(2, 2),
...
)
이고 다시
conv7 = Conv2D(
128,
...
)
를 거쳐
add3 = Add()([
norm6,
norm7
])
로 연결합니다.
따라서 Model 앞부분은 작은 Residual CNN 구조를 가지고 있다고 볼 수 있습니다.
43. Feature Channel이 점점 증가한다
전체적으로 Channel 수는 다음과 같이 증가합니다.
3
↓
16
↓
32
↓
64
↓
128
↓
256
↓
512
반대로 공간 크기는 계속 작아집니다.
대략적으로 보면
110 × 300 × 3
55 × 150 × 16
27 × 74 × 16
14 × 37 × 32
7 × 19 × 64
4 × 10 × 128
2 × 5 × 256
1 × 3 × 512
형태로 변합니다.
CNN의 중요한 특징입니다.
공간 정보
크고 상세함
↓
점점 압축
↓
고수준 Feature
로 변환됩니다.
44. 마지막 CNN Feature
마지막 Convolution은
conv9 = keras.layers.Conv2D(
512,
(3, 3),
strides=(2, 2),
padding="same",
activation="swish"
)(norm8)
입니다.
여기까지 오면 원본 Pixel을 직접 보고 있다기보다 여러 CNN Layer에서 추출한 고차원 Feature를 사용하게 됩니다.
초기 Layer가
선
Edge
밝기 변화
등을 학습한다면 뒤쪽 Layer는 학습 결과에 따라 더 복합적인 Track 형태를 표현할 수 있습니다.
45. Flatten Layer
CNN Feature를 Dense Layer로 보내기 위해
flat1 = keras.layers.Flatten()(norm9)
을 사용합니다.
마지막 Feature Map이 대략
1 × 3 × 512
라면 Flatten 결과는
1 × 1536
형태의 Vector가 됩니다.
1 × 3 × 512
↓
Flatten
↓
1536
입니다.
46. Dense Layer
Flatten 이후 다음 Fully Connected Layer가 연결됩니다.
dense1 = Dense(
128,
activation="swish"
)(flat1)
다음은
dense2 = Dense(
64,
activation="swish"
)(norm10)
그리고
dense3 = Dense(
64,
activation="swish"
)(norm11)
입니다.
구조는
1536
↓
128
↓
64
↓
64
↓
2
입니다.
CNN이 영상 Feature를 추출했다면 Dense Layer는 그 Feature들을 조합하여 최종 두 값을 계산하는 역할을 합니다.
47. Swish Activation Function
이 Model은 대부분의 Conv Layer와 Dense Layer에서
activation="swish"
를 사용합니다.
Swish는 단순한 ReLU 대신 사용할 수 있는 Activation Function입니다.
개념적으로 Activation Function은 Neural Network에 비선형성을 추가합니다.
Activation이 없다면 여러 Layer를 깊게 쌓아도 복잡한 Pattern을 표현하는 데 한계가 생깁니다.
따라서
Convolution
↓
Swish
↓
다음 Feature
형태로 사용됩니다.
48. 마지막 출력은 2개
최종 Layer입니다.
output = keras.layers.Dense(
2,
activation="tanh",
name="track_output"
)(norm12)
핵심은
Dense(2)
입니다.
즉 출력은 항상 두 개입니다.
Prediction
[
value1,
value2
]
형태입니다.
Training Dataset의 Label도
[
value1,
value2
]
였으므로 정확하게 대응합니다.
49. tanh를 사용하는 이유
tanh Activation의 출력 범위는
-1 ≤ output ≤ +1
입니다.
그리고 Training Label은
label = label / 300.0
으로 정규화했습니다.
따라서 Model이 너무 큰 값을 출력하지 않고 정규화된 범위의 두 값을 예측하도록 구성되어 있습니다.
전체 관계는
원래 Label
↓
/ 300
↓
정규화 Label
↓
CNN Training
↓
tanh
↓
정규화 Prediction
입니다.
50. Model 이름
Model 생성 코드는
model = keras.models.Model(
inputs=input1,
outputs=output,
name="limo_track_detector"
)
입니다.
따라서 Model의 논리적인 이름은
limo_track_detector
입니다.
model.summary()를 실행하면 전체 Layer, Output Shape, Parameter 등을 확인할 수 있습니다.
51. 이 모델은 End-to-End에 가까운 구조다
전통적인 차선인식이라면 다음과 같은 과정을 사용할 수 있습니다.
Image
↓
HSV
↓
Threshold
↓
Edge
↓
Contour
↓
Lane Line
↓
Target 계산
하지만 이 Model에서는 직접적인 Line Detection Algorithm이 없습니다.
대신
ROI Image
↓
CNN
↓
두 개의 주행 관련 값
을 바로 학습합니다.
즉 사람이 모든 Track Feature를 직접 정의하지 않고 CNN이 Dataset에서 필요한 Feature를 학습하도록 구성되어 있습니다.


