이미지를 위한 인공 신경망
합성곱 신경망의 시각화
가중치 시각화
합성곱 층의 가중치를 이미지로 출력하는 것을 말함
합성곱 신경망은 주로 이미지를 다루기 때문에 가중치가 시각적인 패턴을 학습하는지 알아볼 수 있음
특성 맵 시각화
합성곱 층의 활성화 출력을 이미지로 그리는 것을 말함
가중치 시각화와 함께 비교하여 각 필터가 이미지의 어느 부분을 활성화시키는지 확인할 수 있음
함수형 API
케라스에서 신경망 모델을 만드는 방법 중 하나
Model 클래스에 모델의 입력과 출력을 지정함
전형적으로 입력은 Input() 함수를 사용하여 정의하고 출력은 마지막 층의 출력으로 정의
가중치 시각화
from tensorflow import keras
!wget https://github.com/rickiepark/hg-mldl/raw/master/best-cnn-model.h5
model = keras.models.load_model('best-cnn-model.h5')
model.layers
[<Conv2D name=conv2d, built=True>,
<MaxPooling2D name=max_pooling2d, built=True>,
<Conv2D name=conv2d_1, built=True>,
<MaxPooling2D name=max_pooling2d_1, built=True>,
<Flatten name=flatten, built=True>,
<Dense name=dense, built=True>,
<Dropout name=dropout, built=True>,
<Dense name=dense_1, built=True>]
conv = model.layers[0]
print(conv.weights[0].shape, conv.weights[1].shape)
# (3, 3, 1, 32) (32,)
conv_weights = conv.weights[0].numpy()
print(conv_weights.mean(), conv_weights.std())
# -0.02494116 0.24951957
import matplotlib.pyplot as plt
plt.hist(conv_weights.reshape(-1, 1))
plt.xlabel('weight')
plt.ylabel('count')
plt.show()
fig, axs = plt.subplots(2, 16, figsize=(15,2))
for i in range(2):
for j in range(16):
axs[i, j].imshow(conv_weights[:,:,0,i*16 + j], vmin=-0.5, vmax=0.5)
axs[i, j].axis('off')
plt.show()
no_training_model = keras.Sequential()
no_training_model.add(keras.layers.Conv2D(32, kernel_size=3, activation='relu',
padding='same', input_shape=(28,28,1)))
no_training_conv = no_training_model.layers[0]
print(no_training_conv.weights[0].shape)
# (3, 3, 1, 32)
no_training_weights = no_training_conv.weights[0].numpy()
print(no_training_weights.mean(), no_training_weights.std())
# 0.00031903095 0.07969899
plt.hist(no_training_weights.reshape(-1, 1))
plt.xlabel('weight')
plt.ylabel('count')
plt.show()
fig, axs = plt.subplots(2, 16, figsize=(15,2))
for i in range(2):
for j in range(16):
axs[i, j].imshow(no_training_weights[:,:,0,i*16 + j], vmin=-0.5, vmax=0.5)
axs[i, j].axis('off')
plt.show()




함수형 API
inputs = keras.Input(shape=(784,))
dense1 = keras.layers.Dense(100, activation='relu')
dense2 = keras.layers.Dense(10, activation='softmax')
hidden = dense1(inputs)
outputs = dense2(hidden)
func_model = keras.Model(inputs, outputs)
print(model.inputs)
# [<KerasTensor shape=(None, 28, 28, 1), dtype=float32, sparse=False, ragged=False, name=conv2d_input>]
conv_acti = keras.Model(model.inputs[0], model.layers[0].output)
특성 맵 시각화
(train_input, train_target), (test_input, test_target) = keras.datasets.fashion_mnist.load_data()
plt.imshow(train_input[0], cmap='gray_r')
plt.show()
inputs = train_input[0:1].reshape(-1, 28, 28, 1)/255.0
feature_maps = conv_acti.predict(inputs)
print(feature_maps.shape)
# (1, 28, 28, 32)
fig, axs = plt.subplots(4, 8, figsize=(15,8))
for i in range(4):
for j in range(8):
axs[i, j].imshow(feature_maps[0,:,:,i*8 + j])
axs[i, j].axis('off')
plt.show()
conv2_acti = keras.Model(model.inputs, model.layers[2].output)
feature_maps = conv2_acti.predict(train_input[0:1].reshape(-1, 28, 28, 1)/255.0)
print(feature_maps.shape)
# (1, 14, 14, 64)
fig, axs = plt.subplots(8, 8, figsize=(12,12))
for i in range(8):
for j in range(8):
axs[i, j].imshow(feature_maps[0,:,:,i*8 + j])
axs[i, j].axis('off')
plt.show()



실습 - 손글씨 이미지 분류하기
MNIST: 0~9 손글씨 숫자 이미지 70,000장으로 구성된 딥러닝 입문용 데이터셋
데이터: 훈련 60,000장 / 테스트 10,000장 / 이미지 크기 28×28 흑백
CNN은 입력으로 (샘플 수, 높이, 너비, 채널 수) 형태를 요구하기 때문에 흑백 채널(1)을 추가, 픽셀값을 0~255에서 0.0~1.0으로 정규화해 학습 안정화
총 6개의 합성곱(Conv)층을 3개 블럭으로 나눠 쌓기
| 블록필터 수 | 출력 | 크기 | 감지하는 특징 |
| Conv 블록 1 | 32 | 14×14 | 엣지, 직선 등 저수준 |
| Conv 블록 2 | 64 | 7×7 | 곡선, 반복 패턴 등 중간 수준 |
| Conv 블록 3 | 128 | 7×7 | 숫자 전체 형태 등 고수준 |
| Dense (분류기) | — | 10 | 0~9 확률 출력 |
총 파라미터: 약 190만 개 (7.24MB)
각 블록에 BatchNormalization 을 적용해 학습 중 값 분포를 안정화
Dropout 으로 특정 뉴런에 과도하게 의존하는 과적합을 방지
MaxPooling 으로 블록을 거칠수록 이미지 크기를 절반씩 줄여 연산량을 낮추면서 핵심 특징만 남
학습 전략
단순 에포크 고정X
3가지 콜백 조합해 학습 자동 제어
| EarlyStopping | val_loss가 5 에포크 연속 개선되지 않으면 학습 중단, 최적 가중치 자동 복원 |
| ModelCheckpoint | val_loss가 개선될 때만 모델 파일 저장 |
| ReduceLROnPlateau | val_loss가 3 에포크 개선 없으면 학습률을 절반으로 감소 |
# 1. 데이터 가져오기 및 확인
from keras.datasets import mnist
(train_input, train_target), (test_input, test_target) = mnist.load_data()
# 2. 학습에 필요한 형태로 변환하기
# 3. 모델 생성, 컴파일, 학습, 평가까지 (조기종료, 모델저장 콜백까지 포함해서 학습하기)
# 4. 합성곱 층을 몇개 썼는지와 무관하게, 하나의 합성곱으로부터 나온 피처맵을 시각화해보기
데이터 불러오기
from keras.datasets import mnist
(train_input, train_target), (test_input, test_target) = mnist.load_data()
train_input.shape
# Downloading data from https://storage.googleapis.com/tensorflow/tf-keras-datasets/mnist.npz
# 11490434/11490434 ━━━━━━━━━━━━━━━━━━━━ 2s 0us/step
# (60000, 28, 28)
import matplotlib.pyplot as plt
plt.imshow(train_input[0], cmap='gray')
plt.show()

전처리
# CNN 입력 형태: (샘플수, 높이, 너비, 채널수)
# MNIST는 흑백 -> 채널=1, 픽셀값 0~255 -> 0.0~1.0 정규화
train_input = train_input.reshape(-1, 28, 28, 1) / 255.0
test_input = test_input.reshape(-1, 28, 28, 1) / 255.0
print(train_input.shape) # (60000, 28, 28, 1)
print(test_input.shape) # (10000, 28, 28, 1)
# (60000, 28, 28, 1)
# (10000, 28, 28, 1)
모델 생성
import tensorflow as tf
from tensorflow import keras
model = keras.Sequential([
# Conv 블록 1: 기본 엣지·선 등 저수준 특징 감지
keras.layers.Conv2D(32, (3,3), activation='relu',
input_shape=(28,28,1), padding='same'),
keras.layers.BatchNormalization(), # 학습 안정화 (값 분포 정규화)
keras.layers.Conv2D(32, (3,3), activation='relu', padding='same'),
keras.layers.MaxPooling2D((2,2)), # 14×14로 축소
keras.layers.Dropout(0.25),
# Conv 블록 2: 곡선·패턴 등 중간 수준 특징 감지
keras.layers.Conv2D(64, (3,3), activation='relu', padding='same'),
keras.layers.BatchNormalization(),
keras.layers.Conv2D(64, (3,3), activation='relu', padding='same'),
keras.layers.MaxPooling2D((2,2)), # 7×7로 축소
keras.layers.Dropout(0.25),
# Conv 블록 3: 숫자 전체 형태 등 고수준 특징 감지
keras.layers.Conv2D(128, (3,3), activation='relu', padding='same'),
keras.layers.BatchNormalization(),
keras.layers.Conv2D(128, (3,3), activation='relu', padding='same'),
keras.layers.Dropout(0.25),
# 분류기
keras.layers.Flatten(),
keras.layers.Dense(256, activation='relu'),
keras.layers.BatchNormalization(),
keras.layers.Dropout(0.5),
keras.layers.Dense(10, activation='softmax') # 0~9 → 10개 클래스
])
model.summary()
/usr/local/lib/python3.12/dist-packages/keras/src/layers/convolutional/base_conv.py:113: UserWarning: Do not pass an `input_shape`/`input_dim` argument to a layer. When using Sequential models, prefer using an `Input(shape)` object as the first layer in the model instead.
super().__init__(activity_regularizer=activity_regularizer, **kwargs)
Model: "sequential"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Layer (type) ┃ Output Shape ┃ Param # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ conv2d (Conv2D) │ (None, 28, 28, 32) │ 320 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ batch_normalization │ (None, 28, 28, 32) │ 128 │
│ (BatchNormalization) │ │ │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ conv2d_1 (Conv2D) │ (None, 28, 28, 32) │ 9,248 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ max_pooling2d (MaxPooling2D) │ (None, 14, 14, 32) │ 0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dropout (Dropout) │ (None, 14, 14, 32) │ 0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ conv2d_2 (Conv2D) │ (None, 14, 14, 64) │ 18,496 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ batch_normalization_1 │ (None, 14, 14, 64) │ 256 │
│ (BatchNormalization) │ │ │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ conv2d_3 (Conv2D) │ (None, 14, 14, 64) │ 36,928 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ max_pooling2d_1 (MaxPooling2D) │ (None, 7, 7, 64) │ 0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dropout_1 (Dropout) │ (None, 7, 7, 64) │ 0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ conv2d_4 (Conv2D) │ (None, 7, 7, 128) │ 73,856 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ batch_normalization_2 │ (None, 7, 7, 128) │ 512 │
│ (BatchNormalization) │ │ │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ conv2d_5 (Conv2D) │ (None, 7, 7, 128) │ 147,584 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dropout_2 (Dropout) │ (None, 7, 7, 128) │ 0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ flatten (Flatten) │ (None, 6272) │ 0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense (Dense) │ (None, 256) │ 1,605,888 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ batch_normalization_3 │ (None, 256) │ 1,024 │
│ (BatchNormalization) │ │ │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dropout_3 (Dropout) │ (None, 256) │ 0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense_1 (Dense) │ (None, 10) │ 2,570 │
└─────────────────────────────────┴────────────────────────┴───────────────┘
Total params: 1,896,810 (7.24 MB)
Trainable params: 1,895,850 (7.23 MB)
Non-trainable params: 960 (3.75 KB)
컴파일
model.compile(
optimizer=keras.optimizers.Adam(learning_rate=1e-3),
loss='sparse_categorical_crossentropy',
metrics=['accuracy']
)
콜백
callbacks = [
# val_loss 기준 5에포크 연속 개선 없으면 조기 종료, 최적 가중치 복원
keras.callbacks.EarlyStopping(
monitor='val_loss', patience=5, restore_best_weights=True
),
# val_loss 개선 시에만 모델 저장
keras.callbacks.ModelCheckpoint(
filepath='best_mnist_model.keras',
monitor='val_loss', save_best_only=True
),
# val_loss가 3에포크 동안 개선 없으면 학습률을 절반으로 줄임
keras.callbacks.ReduceLROnPlateau(
monitor='val_loss', factor=0.5, patience=3, verbose=1
)
]
학습
# 학습
history = model.fit(
train_input, train_target,
epochs=50,
batch_size=64,
validation_split=0.2, # 훈련 데이터 20%를 검증용으로 분리
callbacks=callbacks
)
Epoch 1/50
750/750 ━━━━━━━━━━━━━━━━━━━━ 20s 11ms/step - accuracy: 0.9443 - loss: 0.1784 - val_accuracy: 0.9732 - val_loss: 0.0903 - learning_rate: 0.0010
Epoch 2/50
750/750 ━━━━━━━━━━━━━━━━━━━━ 6s 8ms/step - accuracy: 0.9800 - loss: 0.0639 - val_accuracy: 0.9865 - val_loss: 0.0432 - learning_rate: 0.0010
Epoch 3/50
750/750 ━━━━━━━━━━━━━━━━━━━━ 6s 8ms/step - accuracy: 0.9849 - loss: 0.0491 - val_accuracy: 0.9865 - val_loss: 0.0410 - learning_rate: 0.0010
Epoch 4/50
750/750 ━━━━━━━━━━━━━━━━━━━━ 6s 8ms/step - accuracy: 0.9874 - loss: 0.0411 - val_accuracy: 0.9895 - val_loss: 0.0376 - learning_rate: 0.0010
Epoch 5/50
750/750 ━━━━━━━━━━━━━━━━━━━━ 6s 9ms/step - accuracy: 0.9874 - loss: 0.0382 - val_accuracy: 0.9915 - val_loss: 0.0268 - learning_rate: 0.0010
Epoch 6/50
750/750 ━━━━━━━━━━━━━━━━━━━━ 6s 8ms/step - accuracy: 0.9899 - loss: 0.0326 - val_accuracy: 0.9877 - val_loss: 0.0440 - learning_rate: 0.0010
Epoch 7/50
750/750 ━━━━━━━━━━━━━━━━━━━━ 6s 8ms/step - accuracy: 0.9903 - loss: 0.0320 - val_accuracy: 0.9933 - val_loss: 0.0226 - learning_rate: 0.0010
Epoch 8/50
750/750 ━━━━━━━━━━━━━━━━━━━━ 6s 8ms/step - accuracy: 0.9913 - loss: 0.0278 - val_accuracy: 0.9898 - val_loss: 0.0338 - learning_rate: 0.0010
Epoch 9/50
750/750 ━━━━━━━━━━━━━━━━━━━━ 6s 8ms/step - accuracy: 0.9920 - loss: 0.0254 - val_accuracy: 0.9912 - val_loss: 0.0294 - learning_rate: 0.0010
Epoch 10/50
743/750 ━━━━━━━━━━━━━━━━━━━━ 0s 7ms/step - accuracy: 0.9930 - loss: 0.0223
Epoch 10: ReduceLROnPlateau reducing learning rate to 0.0005000000237487257.
750/750 ━━━━━━━━━━━━━━━━━━━━ 6s 8ms/step - accuracy: 0.9924 - loss: 0.0241 - val_accuracy: 0.9928 - val_loss: 0.0259 - learning_rate: 0.0010
Epoch 11/50
750/750 ━━━━━━━━━━━━━━━━━━━━ 6s 8ms/step - accuracy: 0.9956 - loss: 0.0135 - val_accuracy: 0.9947 - val_loss: 0.0195 - learning_rate: 5.0000e-04
Epoch 12/50
750/750 ━━━━━━━━━━━━━━━━━━━━ 6s 8ms/step - accuracy: 0.9961 - loss: 0.0115 - val_accuracy: 0.9950 - val_loss: 0.0220 - learning_rate: 5.0000e-04
Epoch 13/50
750/750 ━━━━━━━━━━━━━━━━━━━━ 6s 8ms/step - accuracy: 0.9960 - loss: 0.0114 - val_accuracy: 0.9950 - val_loss: 0.0196 - learning_rate: 5.0000e-04
Epoch 14/50
745/750 ━━━━━━━━━━━━━━━━━━━━ 0s 7ms/step - accuracy: 0.9969 - loss: 0.0099
Epoch 14: ReduceLROnPlateau reducing learning rate to 0.0002500000118743628.
750/750 ━━━━━━━━━━━━━━━━━━━━ 6s 8ms/step - accuracy: 0.9968 - loss: 0.0100 - val_accuracy: 0.9947 - val_loss: 0.0242 - learning_rate: 5.0000e-04
Epoch 15/50
750/750 ━━━━━━━━━━━━━━━━━━━━ 6s 8ms/step - accuracy: 0.9979 - loss: 0.0062 - val_accuracy: 0.9952 - val_loss: 0.0197 - learning_rate: 2.5000e-04
Epoch 16/50
750/750 ━━━━━━━━━━━━━━━━━━━━ 6s 8ms/step - accuracy: 0.9979 - loss: 0.0066 - val_accuracy: 0.9950 - val_loss: 0.0196 - learning_rate: 2.5000e-04
평가
test_loss, test_acc = model.evaluate(test_input, test_target)
print(f"\n테스트 정확도: {test_acc:.4f} / 테스트 손실: {test_loss:.4f}")
# 313/313 ━━━━━━━━━━━━━━━━━━━━ 2s 5ms/step - accuracy: 0.9960 - loss: 0.0131
# 테스트 정확도: 0.9960 / 테스트 손실: 0.0131
피처맵 시각화
import matplotlib.pyplot as plt
import numpy as np
TARGET_LAYER = 'conv2d'
# 서브 모델 생성
feature_map_model = keras.Model(
inputs=model.layers[0].input,
outputs=model.get_layer(TARGET_LAYER).output
)
# 테스트 이미지 1장으로 피처맵 추출
sample = test_input[0:1] # shape: (1, 28, 28, 1)
feature_maps = feature_map_model.predict(sample) # shape: (1, H, W, 필터수)
n_filters = feature_maps.shape[-1]
print(f"피처맵 shape: {feature_maps.shape}")
# 1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 213ms/step
# 피처맵 shape: (1, 28, 28, 32)
# 원본 이미지 출력
!pip install koreanize-matplotlib -q
import koreanize_matplotlib
plt.figure(figsize=(3, 3))
plt.imshow(test_input[0].squeeze(), cmap='gray')
plt.title(f"원본 이미지 (정답: {test_target[0]})")
plt.axis('off')
plt.tight_layout()
plt.show()
# 피처맵 격자 시각화
cols = 8
rows = int(np.ceil(n_filters / cols))
fig, axes = plt.subplots(rows, cols, figsize=(cols * 1.8, rows * 1.8))
fig.suptitle(f"'{TARGET_LAYER}' 층의 피처맵 ({n_filters}개 필터)", fontsize=13)
for i, ax in enumerate(axes.flat):
if i < n_filters:
fmap = feature_maps[0, :, :, i]
ax.imshow(fmap, cmap='viridis')
ax.set_title(f"f{i}", fontsize=7)
ax.axis('off')
plt.tight_layout()
plt.show()


결과 해석
| Epoch 1 | 정확도 94.43% — 첫 에포크부터 빠르게 패턴 학습 |
| Epoch 2~9 | 98% -> 99%대로 빠르게 수렴 |
| Epoch 10 | ReduceLROnPlateau 작동 -> 학습률 0.001 -> 0.0005 |
| Epoch 14 | 재작동 -> 학습률 0.0005 -> 0.00025 |
| Epoch 15~ | 99.79%에서 수렴 안정화 후 EarlyStopping으로 종료 |
학습률 감소가 두 차례 작동한 것이 핵심
학습 초반에는 큰 보폭으로 빠르게 수렴
정체 구간에서 학습률을 줄여 더 세밀하게 최적점을 찾아 높은 정확도 도달
테스트 정확도: 99.60% / 테스트 손실: 0.0131
피처맵
첫 번째 Conv 층의 32개 필터 출력을 시각화한 결과, 같은 숫자 7 이미지를 입력했을 때 필터마다 서로 다른 반응 보임
밝게 반응하는 필터: 획의 윤곽선과 경계 강조
방향성 있는 필터: 7의 가로획(수평)이나 대각선 획에 선택적 반응
어둡게 반응하는 필터: 해당 입력에서 활성화되지 않아 이번 이미지와 무관한 특징 담당
정답 코드
# 1. 데이터 가져오기 및 확인
from keras.datasets import mnist
(train_input, train_target), (test_input, test_target) = mnist.load_data()
import matplotlib.pyplot as plt
plt.imshow(train_input[0], cmap="gray")
# 픽셀 값 모두 확인하기
import sys
for x in train_input[0]:
for i in x:
sys.stdout.write("%-3s" % i)
sys.stdout.write('\n')
# 2. 학습에 필요한 형태로 변환하기
train_input = train_input.reshape(-1, 28, 28, 1) / 255.0
test_input = test_input.reshape(-1, 28, 28, 1) / 255.0
# 3. 모델 생성, 컴파일, 학습, 평가까지 (조기종료, 모델저장 콜백까지 포함해서 학습하기)
model = keras.models.Sequential()
model.add(keras.layers.Conv2D(32, kernel_size=(3, 3), input_shape=(28, 28, 1), activation='relu', padding='same'))
model.add(keras.layers.Conv2D(64, (3, 3), activation='relu'))
model.add(keras.layers.MaxPooling2D(pool_size=(2,2)))
model.add(keras.layers.Dropout(0.25))
model.add(keras.layers.Flatten())
model.add(keras.layers.Dense(128, activation='relu'))
model.add(keras.layers.Dropout(0.5))
model.add(keras.layers.Dense(10, activation='softmax'))
model.compile(loss='sparse_categorical_crossentropy',
optimizer='adam',
metrics=['accuracy'])
cp = keras.callbacks.ModelCheckpoint("checkpoint.keras", monitor='val_loss', verbose=0, save_best_only=True)
es = keras.callbacks.EarlyStopping(monitor='val_loss', patience=3, restore_best_weights=True)
history = model.fit(train_input, train_target, validation_split=0.2, epochs=50, verbose=1, callbacks=[cp,es])
import matplotlib.pyplot as plt
plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.xlabel('epoch')
plt.ylabel('loss')
plt.legend(['train', 'val'])
plt.show()
# 4. 합성곱 층을 몇개 썼는지와 무관하게, 하나의 합성곱으로부터 나온 피처맵을 시각화해보기
conv_acti = keras.Model(model.inputs[0], model.layers[0].output)
idx = 99
digit = train_input[idx:idx+1].reshape(-1, 28, 28, 1) / 255.0
feature_maps = conv_acti.predict(digit)
fig, axs = plt.subplots(4, 8, figsize=(15,8))
for i in range(4):
for j in range(8):
axs[i, j].imshow(feature_maps[0,:,:,i*8 + j])
axs[i, j].axis('off')
plt.show()
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 3 18 18 18 12613617526 1662552471270 0 0 0
0 0 0 0 0 0 0 0 30 36 94 15417025325325325325322517225324219564 0 0 0 0
0 0 0 0 0 0 0 49 23825325325325325325325325325193 82 82 56 39 0 0 0 0 0
0 0 0 0 0 0 0 18 2192532532532532531981822472410 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 80 15610725325320511 0 43 1540 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 14 1 15425390 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 1392531902 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 11 19025370 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 35 2412251601081 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 81 24025325311925 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 45 18625325315027 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 16 93 2522531870 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 24925324964 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 46 1301832532532072 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 39 1482292532532532501820 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 24 11422125325325325320178 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 23 66 21325325325325319881 2 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 18 17121925325325325319580 9 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 55 17222625325325325324413311 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 13625325325321213513216 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
텍스트를 위한 인공 신경망
순차 데이터와 순환 신경망
순차 데이터
텍스트나 시계열 데이터와 같이 순서에 의미가 있는 데이터
대표적 순차 데이터: 글, 대화, 일자별 날씨, 일자별 판매 실적 등
순환 신경망
순차 데이터에 잘 맞는 인공 신경망의 한 종류
순차 데이터를 처리하기 위해 고안된 순환층을 1개 이상 사용한 신경망을 순환 신경망이라 부름
셀
순환 신경망에서는 종종 순환층을 셀이라 부름
일반적인 인공 신경망과 마찬가지로 하나의 셀은 여러 개의 뉴런으로 구성됨
ㄴ입출력ㅡ모델 파라미터 수 = Wx + Wb + 절편
은닉 상태
순환 신경망에선 셀의 출력을 특별히 은닉 상태라 부름
은닉 상태는 다음 층으로 전달될 뿐만 아니라 셀이 다음 타임스텝의 데이터를 처리할 때 재사용
텍스트 분석
1. 단어 단위로 토큰화
2. 토큰화한 데이터를 배열로 변환
3. 순환 뉴런의 개수를 지정하여 순환
4. 순환 뉴런의 출력을 처리하는 밀집층을 통화시키기
순환 신경망으로 IMDB 리뷰 분류하기
IMDB 리뷰 데이터셋
말뭉치
자연어 처리에 사용하는 텍스트 데이터의 모음
훈련 데이터셋
토큰
텍스트에서 공백으로 구분되는 단어 혹은 단어의 일부분
종종 소문자로 변환하고 구둣점은 삭제함
텍스트를 숫자로 변환하는 전처리 작업의 일
원-핫 인코딩
어떤 클래스에 해당하는 원소만 1이고 나머지는 모두 0인 벡터
정수로 변환된 토큰을 원-핫 인코딩으로 변환하려면 어휘 사전 크기의 벡터가 만들어짐
1이 하나만 포함된 1차원 배열로 변환하기
단어 임베딩
정수로 변환된 토큰을 비교적 작은 크기의 실수 밀집 벡터로 변환
이런 밀집 벡터는 단어 사이의 관계를 표현할 수 있기 때문에 자연어 처리에서 좋은 성능을 발휘함
실수를 포함한 고유한 배열로 변환하기(길이 줄어듦, 용량 줄어듦)
from keras.datasets import imdb
(train_input, train_target), (test_input, test_target) = imdb.load_data(
num_words=200)
print(train_input.shape, test_input.shape)
# (25000,) (25000,)
print(len(train_input[0]))
# 218
print(len(train_input[1]))
# 189
print(train_input[0])
# [1, 14, 22, 16, 43, 2, 2, 2, 2, 65, 2, 2, 66, 2, 4, 173, 36, 2, 5, 25, 100, 43, 2, 112, 50, 2, 2, 9, 35, 2, 2, 5, 150, 4, 172, 112, 167, 2, 2, 2, 39, 4, 172, 2, 2, 17, 2, 38, 13, 2, 4, 192, 50, 16, 6, 147, 2, 19, 14, 22, 4, 2, 2, 2, 4, 22, 71, 87, 12, 16, 43, 2, 38, 76, 15, 13, 2, 4, 22, 17, 2, 17, 12, 16, 2, 18, 2, 5, 62, 2, 12, 8, 2, 8, 106, 5, 4, 2, 2, 16, 2, 66, 2, 33, 4, 130, 12, 16, 38, 2, 5, 25, 124, 51, 36, 135, 48, 25, 2, 33, 6, 22, 12, 2, 28, 77, 52, 5, 14, 2, 16, 82, 2, 8, 4, 107, 117, 2, 15, 2, 4, 2, 7, 2, 5, 2, 36, 71, 43, 2, 2, 26, 2, 2, 46, 7, 4, 2, 2, 13, 104, 88, 4, 2, 15, 2, 98, 32, 2, 56, 26, 141, 6, 194, 2, 18, 4, 2, 22, 21, 134, 2, 26, 2, 5, 144, 30, 2, 18, 51, 36, 28, 2, 92, 25, 104, 4, 2, 65, 16, 38, 2, 88, 12, 16, 2, 5, 16, 2, 113, 103, 32, 15, 16, 2, 19, 178, 32]
print(train_target[:20])
# [1 0 0 1 0 0 1 0 1 0 1 0 0 0 0 0 1 1 0 1]
from sklearn.model_selection import train_test_split
train_input, val_input, train_target, val_target = train_test_split(
train_input, train_target, test_size=0.2, random_state=42)
import numpy as np
lengths = np.array([len(x) for x in train_input])
print(np.mean(lengths), np.median(lengths))
# 239.00925 178.0
import matplotlib.pyplot as plt
plt.hist(lengths)
plt.xlabel('length')
plt.ylabel('frequency')
plt.show()
from keras.preprocessing.sequence import pad_sequences
train_seq = pad_sequences(train_input, maxlen=100)
print(train_seq.shape)
# (20000, 100)
print(train_seq[0])
[ 10 4 20 9 2 2 2 5 45 6 2 2 33 2 8 2 142 2
5 2 17 73 17 2 5 2 19 55 2 2 92 66 104 14 20 93
76 2 151 33 4 58 12 188 2 151 12 2 69 2 142 73 2 6
2 7 2 2 188 2 103 14 31 10 10 2 7 2 5 2 80 91
2 30 2 34 14 20 151 50 26 131 49 2 84 46 50 37 80 79
6 2 46 7 14 20 10 10 2 158]
print(train_input[0][-10:])
# [6, 2, 46, 7, 14, 20, 10, 10, 2, 158]
print(train_seq[5])
[ 0 0 0 0 1 2 195 19 49 2 2 190 4 2 2 2 183 10
10 13 82 79 4 2 36 71 2 8 2 25 19 49 7 4 2 2
2 2 2 10 10 48 25 40 2 11 2 2 40 2 2 5 4 2
2 95 14 2 56 129 2 10 10 21 2 94 2 2 2 2 11 190
24 2 2 7 94 2 2 10 10 87 2 34 49 2 7 2 2 2
2 2 2 2 46 48 64 18 4 2]
val_seq = pad_sequences(val_input, maxlen=100)

순환 신경망 만들기
from tensorflow import keras
model = keras.Sequential()
model.add(keras.layers.SimpleRNN(8, input_shape=(100, 300)))
model.add(keras.layers.Dense(1, activation='sigmoid'))
train_oh = keras.utils.to_categorical(train_seq)
print(train_oh.shape)
# (20000, 100, 200)
print(train_oh[0][0][:12])
# [0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 1. 0.]
print(np.sum(train_oh[0][0]))
# 1.0
val_oh = keras.utils.to_categorical(val_seq)
model.summary()
Model: "sequential"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Layer (type) ┃ Output Shape ┃ Param # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ simple_rnn (SimpleRNN) │ (None, 8) │ 2,472 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense (Dense) │ (None, 1) │ 9 │
└─────────────────────────────────┴────────────────────────┴───────────────┘
Total params: 2,481 (9.69 KB)
Trainable params: 2,481 (9.69 KB)
Non-trainable params: 0 (0.00 B)
순환 신경망 훈련하기
model.compile(optimizer='adam', loss='binary_crossentropy',
metrics=['accuracy'])
checkpoint_cb = keras.callbacks.ModelCheckpoint('best-simplernn-model.keras',
save_best_only=True)
early_stopping_cb = keras.callbacks.EarlyStopping(patience=3,
restore_best_weights=True)
history = model.fit(train_oh, train_target, epochs=100, batch_size=64,
validation_data=(val_oh, val_target),
callbacks=[checkpoint_cb, early_stopping_cb])
Epoch 25/100
313/313 ━━━━━━━━━━━━━━━━━━━━ 9s 28ms/step - accuracy: 0.7261 - loss: 0.5532 - val_accuracy: 0.6958 - val_loss: 0.5891
Epoch 26/100
313/313 ━━━━━━━━━━━━━━━━━━━━ 9s 27ms/step - accuracy: 0.7313 - loss: 0.5467 - val_accuracy: 0.6942 - val_loss: 0.5970
Epoch 27/100
313/313 ━━━━━━━━━━━━━━━━━━━━ 7s 23ms/step - accuracy: 0.7161 - loss: 0.5635 - val_accuracy: 0.6978 - val_loss: 0.5899
Epoch 28/100
313/313 ━━━━━━━━━━━━━━━━━━━━ 10s 24ms/step - accuracy: 0.7310 - loss: 0.5439 - val_accuracy: 0.6998 - val_loss: 0.5974
plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.xlabel('epoch')
plt.ylabel('loss')
plt.legend(['train', 'val'])
plt.show()

단어 임베딩 사용
(train_input, train_target), (test_input, test_target) = imdb.load_data(
num_words=500)
train_input, val_input, train_target, val_target = train_test_split(
train_input, train_target, test_size=0.2, random_state=42)
train_seq = pad_sequences(train_input, maxlen=100)
val_seq = pad_sequences(val_input, maxlen=100)
model_emb = keras.Sequential()
model_emb.add(keras.layers.Input(shape=(100,)))
model_emb.add(keras.layers.Embedding(500, 16))
model_emb.add(keras.layers.SimpleRNN(8))
model_emb.add(keras.layers.Dense(1, activation='sigmoid'))
model_emb.summary()
Model: "sequential_2"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Layer (type) ┃ Output Shape ┃ Param # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ embedding_1 (Embedding) │ (None, 100, 16) │ 8,000 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ simple_rnn_2 (SimpleRNN) │ (None, 8) │ 200 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense_2 (Dense) │ (None, 1) │ 9 │
└─────────────────────────────────┴────────────────────────┴───────────────┘
Total params: 8,209 (32.07 KB)
Trainable params: 8,209 (32.07 KB)
Non-trainable params: 0 (0.00 B)
model_emb.compile(optimizer='adam', loss='binary_crossentropy',
metrics=['accuracy'])
checkpoint_cb = keras.callbacks.ModelCheckpoint('best-embedding-model.keras',
save_best_only=True)
early_stopping_cb = keras.callbacks.EarlyStopping(patience=3,
restore_best_weights=True)
history = model_emb.fit(train_seq, train_target, epochs=100, batch_size=64,
validation_data=(val_seq, val_target),
callbacks=[checkpoint_cb, early_stopping_cb])
Epoch 1/100
313/313 ━━━━━━━━━━━━━━━━━━━━ 10s 26ms/step - accuracy: 0.5399 - loss: 0.6881 - val_accuracy: 0.5756 - val_loss: 0.6762
Epoch 2/100
313/313 ━━━━━━━━━━━━━━━━━━━━ 6s 20ms/step - accuracy: 0.6479 - loss: 0.6371 - val_accuracy: 0.7204 - val_loss: 0.5799
Epoch 3/100
313/313 ━━━━━━━━━━━━━━━━━━━━ 10s 20ms/step - accuracy: 0.7384 - loss: 0.5440 - val_accuracy: 0.7284 - val_loss: 0.5557
Epoch 4/100
313/313 ━━━━━━━━━━━━━━━━━━━━ 13s 41ms/step - accuracy: 0.7771 - loss: 0.4894 - val_accuracy: 0.7610 - val_loss: 0.5081
Epoch 5/100
313/313 ━━━━━━━━━━━━━━━━━━━━ 15s 24ms/step - accuracy: 0.7865 - loss: 0.4693 - val_accuracy: 0.7584 - val_loss: 0.5089
Epoch 6/100
313/313 ━━━━━━━━━━━━━━━━━━━━ 8s 24ms/step - accuracy: 0.7980 - loss: 0.4507 - val_accuracy: 0.7752 - val_loss: 0.4913
Epoch 7/100
313/313 ━━━━━━━━━━━━━━━━━━━━ 6s 19ms/step - accuracy: 0.8049 - loss: 0.4394 - val_accuracy: 0.7682 - val_loss: 0.4931
Epoch 8/100
313/313 ━━━━━━━━━━━━━━━━━━━━ 10s 19ms/step - accuracy: 0.8090 - loss: 0.4307 - val_accuracy: 0.7734 - val_loss: 0.4969
Epoch 9/100
313/313 ━━━━━━━━━━━━━━━━━━━━ 8s 24ms/step - accuracy: 0.8120 - loss: 0.4228 - val_accuracy: 0.7648 - val_loss: 0.5124
plt.plot(history.history['loss'], label='train')
plt.plot(history.history['val_loss'], label='val')
plt.xlabel('epoch')
plt.ylabel('loss')
plt.legend()
plt.show()

from tensorflow.keras.preprocessing.text import Tokenizer
from tensorflow.keras.preprocessing.sequence import pad_sequences
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense,Flatten,Embedding
from tensorflow.keras.utils import to_categorical
from numpy import array
from tensorflow.keras.preprocessing.text import text_to_word_sequence
text = "매주 수요일은 닭볶음탕 나오는 날"
result = text_to_word_sequence(text)
print("\n원문:\n", text)
print("\n토큰화:\n", result)
# 전처리하려는 세 개의 문장을 정합니다.
docs = [
'먼저 텍스트의 각 단어를 나누어 토큰화 합니다.',
'텍스트의 단어로 토큰화해야 딥러닝에서 인식됩니다.',
'토큰화한 결과는 딥러닝에서 사용할 수 있습니다.',
]
# 토큰화 함수를 이용해 전처리 하는 과정입니다.
token = Tokenizer() # 토큰화 함수 지정
token.fit_on_texts(docs) # 토큰화 함수에 문장 적용
# 단어의 빈도수를 계산한 결과를 각 옵션에 맞추어 출력합니다.
# Tokenizer()의 word_counts 함수는 순서를 기억하는 OrderedDict 클래스를 사용합니다.
print("\n단어 카운트:\n", token.word_counts)
# 출력되는 순서는 랜덤입니다.
print("\n문장 카운트: ", token.document_count)
print("\n각 단어가 몇 개의 문장에 포함되어 있는가:\n", token.word_docs)
print("\n각 단어에 매겨진 인덱스 값:\n", token.word_index)
text = "매주 수요일은 닭볶음탕 나오는 날"
token = Tokenizer()
token.fit_on_texts([text])
print(token.word_index)
x = token.texts_to_sequences([text])
print(x)
# 단어의 시작을 알리는 정수가 필요하므로, 원래 단어수보다 1 더하기
x = to_categorical(x, num_classes=6)
print(x)
원문:
매주 수요일은 닭볶음탕 나오는 날
토큰화:
['매주', '수요일은', '닭볶음탕', '나오는', '날']
단어 카운트:
OrderedDict({'먼저': 1, '텍스트의': 2, '각': 1, '단어를': 1, '나누어': 1, '토큰화': 1, '합니다': 1, '단어로': 1, '토큰화해야': 1, '딥러닝에서': 2, '인식됩니다': 1, '토큰화한': 1, '결과는': 1, '사용할': 1, '수': 1, '있습니다': 1})
문장 카운트: 3
각 단어가 몇 개의 문장에 포함되어 있는가:
defaultdict(<class 'int'>, {'먼저': 1, '각': 1, '합니다': 1, '단어를': 1, '토큰화': 1, '텍스트의': 2, '나누어': 1, '토큰화해야': 1, '단어로': 1, '딥러닝에서': 2, '인식됩니다': 1, '사용할': 1, '수': 1, '있습니다': 1, '결과는': 1, '토큰화한': 1})
각 단어에 매겨진 인덱스 값:
{'텍스트의': 1, '딥러닝에서': 2, '먼저': 3, '각': 4, '단어를': 5, '나누어': 6, '토큰화': 7, '합니다': 8, '단어로': 9, '토큰화해야': 10, '인식됩니다': 11, '토큰화한': 12, '결과는': 13, '사용할': 14, '수': 15, '있습니다': 16}
{'매주': 1, '수요일은': 2, '닭볶음탕': 3, '나오는': 4, '날': 5}
[[1, 2, 3, 4, 5]]
[[[0. 1. 0. 0. 0. 0.]
[0. 0. 1. 0. 0. 0.]
[0. 0. 0. 1. 0. 0.]
[0. 0. 0. 0. 1. 0.]
[0. 0. 0. 0. 0. 1.]]]
# 1. 토큰화 : 단어 별로 쪼개고, 각 단어를 고유한 숫자로 만들자.
# 2. 원-핫 인코딩
# 3. 패딩
# 4. 순환신경망 생성, 컴파일, 그리고 학습
# 5. 예측 잘 되나 해보기
import numpy as np
from tensorflow.keras.preprocessing.text import Tokenizer
from tensorflow.keras.preprocessing.sequence import pad_sequences
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Embedding, LSTM, Dense
from tensorflow.keras.utils import to_categorical
# 직접 리뷰 10개 작성해서 리스트 채워 넣기! 긍정 다섯, 부정 다섯
docs = [
"정말 재밌어요",
"연출이 훌륭해요",
"배우 연기가 좋아요",
"몰입감 최고에요",
"다시 보고 싶어요",
"스토리가 아쉬워요",
"연기가 어색해요",
"전개가 뻔해요",
"너무 지루해요",
"시간이 아까워요"
]
target = [1, 1, 1, 1, 1, 0, 0, 0, 0, 0]
token = Tokenizer()
token.fit_on_texts(docs)
vocab_size = len(token.word_index) + 1 # 전체 단어 수 (+1: 패딩용 0 자리)
print("=" * 45)
print("[STEP 2] 토큰화")
print(f" 단어 사전 : {token.word_index}")
print(f" 단어 수(vocab): {vocab_size}")
# 문장을 숫자 시퀀스로 변환
sequences = token.texts_to_sequences(docs)
print(f"\n 숫자 시퀀스 :\n {sequences}")
=============================================
[STEP 2] 토큰화
단어 사전 : {'연기가': 1, '정말': 2, '재밌어요': 3, '연출이': 4, '훌륭해요': 5, '배우': 6, '좋아요': 7, '몰입감': 8, '최고에요': 9, '다시': 10, '보고': 11, '싶어요': 12, '스토리가': 13, '아쉬워요': 14, '어색해요': 15, '전개가': 16, '뻔해요': 17, '너무': 18, '지루해요': 19, '시간이': 20, '아까워요': 21}
단어 수(vocab): 22
숫자 시퀀스 :
[[2, 3], [4, 5], [6, 1, 7], [8, 9], [10, 11, 12], [13, 14], [1, 15], [16, 17], [18, 19], [20, 21]]
one_hot = [to_categorical(seq, num_classes=vocab_size) for seq in sequences]
print("\n" + "=" * 45)
print("[STEP 3] 원-핫 인코딩")
print(f" 첫 번째 문장 원-핫 shape: {np.array(one_hot[0]).shape}")
print(f" (단어 수) x (vocab_size) 형태")
=============================================
[STEP 3] 원-핫 인코딩
첫 번째 문장 원-핫 shape: (2, 22)
(단어 수) x (vocab_size) 형태
max_len = max(len(seq) for seq in sequences) # 가장 긴 문장 길이
sequences_padded = pad_sequences(sequences, maxlen=max_len, padding='pre')
print("\n" + "=" * 45)
print("[STEP 4] 패딩")
print(f" 최대 문장 길이: {max_len}")
print(f" 패딩 결과 shape: {sequences_padded.shape}")
print(f"\n 패딩된 시퀀스:\n{sequences_padded}")
# 라벨(정답)을 numpy 배열로 변환
X = sequences_padded
y = np.array(target)
=============================================
[STEP 4] 패딩
최대 문장 길이: 3
패딩 결과 shape: (10, 3)
패딩된 시퀀스:
[[ 0 2 3]
[ 0 4 5]
[ 6 1 7]
[ 0 8 9]
[10 11 12]
[ 0 13 14]
[ 0 1 15]
[ 0 16 17]
[ 0 18 19]
[ 0 20 21]]
model = Sequential([
Embedding(input_dim=vocab_size, output_dim=8, input_length=max_len),
LSTM(32),
Dense(1, activation='sigmoid'), # 이진 분류 -> sigmoid
])
model.compile(
optimizer='adam',
loss='binary_crossentropy', # 이진 분류 손실 함수
metrics=['accuracy']
)
print("\n" + "=" * 45)
print("[STEP 5] 모델 구조")
model.summary()
print("\n 학습 시작...")
history = model.fit(
X, y,
epochs=200,
verbose=0 # 200줄 출력 방지; 최종 결과만 표시
)
# 마지막 10 에폭 정확도 출력
final_acc = history.history['accuracy'][-1]
print(f" 최종 학습 정확도: {final_acc * 100:.1f}%")
=============================================
[STEP 5] 모델 구조
/usr/local/lib/python3.12/dist-packages/keras/src/layers/core/embedding.py:100: UserWarning: Argument `input_length` is deprecated. Just remove it.
warnings.warn(
Model: "sequential_3"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Layer (type) ┃ Output Shape ┃ Param # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ embedding_2 (Embedding) │ ? │ 0 (unbuilt) │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ lstm (LSTM) │ ? │ 0 (unbuilt) │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense_3 (Dense) │ ? │ 0 (unbuilt) │
└─────────────────────────────────┴────────────────────────┴───────────────┘
Total params: 0 (0.00 B)
Trainable params: 0 (0.00 B)
Non-trainable params: 0 (0.00 B)
학습 시작...
최종 학습 정확도: 100.0%
def predict_sentiment(sentences: list[str]) -> None:
"""
새 문장을 입력하면 긍정/부정을 출력하는 함수.
학습 때 보지 못한 단어는 자동으로 무시됨.
"""
seqs = token.texts_to_sequences(sentences)
padded = pad_sequences(seqs, maxlen=max_len, padding='pre')
preds = model.predict(padded, verbose=0)
print("\n" + "=" * 45)
print("[STEP 6] 예측 결과")
print(f" {'문장':<18} {'확률':>6} {'결과'}")
print(" " + "-" * 38)
for sent, prob in zip(sentences, preds):
label = "긍정" if prob[0] >= 0.5 else "부정"
print(f" {sent:<18} {prob[0]:>6.3f} {label}")
# 테스트 문장
test_sentences = [
"정말 재밌어요", # 학습 데이터와 동일, 긍정 기대
"너무 지루해요", # 학습 데이터와 동일, 부정 기대
"연기가 좋아요", # 혼합 단어, 긍정 기대
"전개가 아쉬워요", # 혼합 단어, 부정 기대
]
predict_sentiment(test_sentences)
=============================================
[STEP 6] 예측 결과
문장 확률 결과
--------------------------------------
정말 재밌어요 0.992 긍정
너무 지루해요 0.004 부정
연기가 좋아요 0.113 부정
전개가 아쉬워요 0.004 부정
# 6. (선택사항) : 다 했으면, 리뷰의 개수 더 채워넣어서 또 해보기
# 데이터 77문장 확장 버전
import numpy as np
from tensorflow.keras.preprocessing.text import Tokenizer
from tensorflow.keras.preprocessing.sequence import pad_sequences
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Embedding, LSTM, Dense
from tensorflow.keras.utils import to_categorical
# STEP 1. 데이터 준비 (기존 10 + 추가 67 = 총 77문장)
docs = [
# 기존 긍정 (5개)
"정말 재밌어요",
"연출이 훌륭해요",
"배우 연기가 좋아요",
"몰입감 최고에요",
"다시 보고 싶어요",
# 기존 부정 (5개)
"스토리가 아쉬워요",
"연기가 어색해요",
"전개가 뻔해요",
"너무 지루해요",
"시간이 아까워요",
# 추가 긍정 (30개)
"스토리가 탄탄해요",
"영상미가 아름다워요",
"음악이 감동적이에요",
"연출이 세련됐어요",
"캐릭터가 매력적이에요",
"대사가 인상적이에요",
"감동이 넘쳐요",
"완성도가 높아요",
"기대 이상이에요",
"강력 추천해요",
"명작이에요",
"눈물이 났어요",
"웃음이 멈추질 않아요",
"긴장감이 넘쳐요",
"몰입이 잘 돼요",
"마지막 장면이 최고에요",
"배우들이 훌륭해요",
"연기력이 뛰어나요",
"촬영이 아름다워요",
"내용이 알차요",
"전개가 흥미로워요",
"시간 가는 줄 몰랐어요",
"정말 좋았어요",
"여운이 길어요",
"OST가 좋아요",
"분위기가 좋아요",
"설정이 독창적이에요",
"구성이 탁월해요",
"완벽한 영화에요",
"두고두고 생각나요",
# 추가 긍정 (7개) - '연기'의 부정 가중치 개선용
"연기가 좋아요",
"연기가 자연스러워요",
"연기가 인상적이에요",
"연기가 훌륭해요",
"연기가 감동적이에요",
"연기가 완벽해요",
"연기가 탁월해요",
# 추가 부정 (30개)
"스토리가 엉망이에요",
"연기가 실망스러워요",
"내용이 빈약해요",
"결말이 허무해요",
"캐릭터가 매력이 없어요",
"대사가 어색해요",
"연출이 조잡해요",
"편집이 엉성해요",
"음악이 별로에요",
"전혀 재미없어요",
"기대 이하에요",
"돈이 아까워요",
"다시는 안 볼 것 같아요",
"너무 억지스러워요",
"개연성이 없어요",
"지루하기만 해요",
"최악이에요",
"별로에요",
"실망이에요",
"졸렸어요",
"집중이 안 돼요",
"내용이 없어요",
"이해가 안 가요",
"마무리가 이상해요",
"흥미가 없어요",
"주인공이 답답해요",
"불필요한 장면이 많아요",
"과장이 심해요",
"몰입이 안 돼요",
"추천하기 싫어요",
]
target = (
[1] * 5 + # 기존 긍정
[0] * 5 + # 기존 부정
[1] * 30 + # 추가 긍정
[1] * 7 + # '연기' 긍정 추가
[0] * 30 # 추가 부정
)
print("=" * 50)
print(f" 총 문장 수 : {len(docs)}개")
print(f" 긍정 : {sum(target)}개")
print(f" 부정 : {len(target) - sum(target)}개")
# STEP 2. 토큰화
token = Tokenizer()
token.fit_on_texts(docs)
vocab_size = len(token.word_index) + 1
sequences = token.texts_to_sequences(docs)
print("\n" + "=" * 50)
print(f"[STEP 2] 토큰화 완료")
print(f" 전체 단어 수(vocab_size): {vocab_size}")
# STEP 3. 원-핫 인코딩
# 각 단어 인덱스를 vocab_size 길이의 벡터로 변환
one_hot = [to_categorical(seq, num_classes=vocab_size) for seq in sequences]
print("\n" + "=" * 50)
print(f"[STEP 3] 원-핫 인코딩 완료")
print(f" 첫 번째 문장 shape: {np.array(one_hot[0]).shape}")
# STEP 4. 패딩
max_len = max(len(seq) for seq in sequences)
X = pad_sequences(sequences, maxlen=max_len, padding='pre')
y = np.array(target)
print("\n" + "=" * 50)
print(f"[STEP 4] 패딩 완료")
print(f" 최대 문장 길이 : {max_len}")
print(f" X shape : {X.shape}")
# STEP 5. LSTM 모델 생성 & 컴파일 & 학습
model = Sequential([
Embedding(input_dim=vocab_size, output_dim=16, input_length=max_len),
LSTM(64),
Dense(1, activation='sigmoid'),
])
model.compile(
optimizer='adam',
loss='binary_crossentropy',
metrics=['accuracy']
)
print("\n" + "=" * 50)
print("[STEP 5] 모델 구조")
model.summary()
print("\n 학습 시작...")
history = model.fit(
X, y,
epochs=200,
verbose=0
)
final_acc = history.history['accuracy'][-1]
print(f" 최종 학습 정확도: {final_acc * 100:.1f}%")
# STEP 6. 예측
def predict_sentiment(sentences: list[str]) -> None:
seqs = token.texts_to_sequences(sentences)
padded = pad_sequences(seqs, maxlen=max_len, padding='pre')
preds = model.predict(padded, verbose=0)
print("\n" + "=" * 50)
print("[STEP 6] 예측 결과")
print(f" {'문장':<20} {'확률':>6} 결과")
print(" " + "-" * 40)
for sent, prob in zip(sentences, preds):
label = "긍정" if prob[0] >= 0.5 else "부정"
print(f" {sent:<20} {prob[0]:>6.3f} {label}")
# 이전에 틀렸던 문장 + 추가 테스트
test_sentences = [
"정말 재밌어요", # 기존 긍정, 긍정 기대
"너무 지루해요", # 기존 부정, 부정 기대
"연기가 좋아요", # 이전에 틀린 문장, 긍정 기대
"연기가 별로에요", # 이전에 틀린 문장, 부정 기대
"전개가 아쉬워요", # 혼합 단어, 부정 기대
"음악이 좋아요", # 새로운 조합, 긍정 기대
"내용이 실망이에요", # 새로운 조합, 부정 기대
]
predict_sentiment(test_sentences)
==================================================
총 문장 수 : 77개
긍정 : 42개
부정 : 35개
==================================================
[STEP 2] 토큰화 완료
전체 단어 수(vocab_size): 119
==================================================
[STEP 3] 원-핫 인코딩 완료
첫 번째 문장 shape: (2, 119)
==================================================
[STEP 4] 패딩 완료
최대 문장 길이 : 5
X shape : (77, 5)
==================================================
[STEP 5] 모델 구조
Model: "sequential_6"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Layer (type) ┃ Output Shape ┃ Param # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ embedding_5 (Embedding) │ ? │ 0 (unbuilt) │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ lstm_3 (LSTM) │ ? │ 0 (unbuilt) │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense_6 (Dense) │ ? │ 0 (unbuilt) │
└─────────────────────────────────┴────────────────────────┴───────────────┘
Total params: 0 (0.00 B)
Trainable params: 0 (0.00 B)
Non-trainable params: 0 (0.00 B)
학습 시작...
최종 학습 정확도: 100.0%
==================================================
[STEP 6] 예측 결과
문장 확률 결과
----------------------------------------
정말 재밌어요 1.000 긍정
너무 지루해요 0.000 부정
연기가 좋아요 1.000 긍정
연기가 별로에요 0.002 부정
전개가 아쉬워요 0.003 부정
음악이 좋아요 0.999 긍정
내용이 실망이에요 0.001 부정
정답 코드
from tensorflow.keras.preprocessing.text import Tokenizer
from tensorflow.keras.preprocessing.sequence import pad_sequences
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Input, Embedding, SimpleRNN, Dense
import numpy as np
# 직접 리뷰 10개 작성해서 리스트 채워 넣기! 긍정 다섯, 부정 다섯
docs = [
"너무 재미있어요",
"최고의 영화에요",
"끝내주는 영화에요",
"다시 또 보고싶어요",
"추천합니다",
"별로에요",
"너무 지루했어요",
"연기가 별로에요",
"재미없어요",
"시간이 아까워요"
]
target = np.array([1, 1, 1, 1, 1, 0, 0, 0, 0, 0])
# 1. 토큰화 : 단어 별로 쪼개고, 각 단어를 고유한 숫자로 만들자.
token = Tokenizer()
token.fit_on_texts(docs)
print(token.word_index)
train_input = token.texts_to_sequences(docs)
print("\n리뷰 텍스트, 토큰화 결과:\n", train_input)
# 2. (선택사항) : 원-핫 인코딩
# 3. 패딩
train_pad = pad_sequences(train_input, 4)
print("\n패딩 결과:\n", train_pad)
# 4. 순환신경망 생성, 컴파일, 그리고 학습
model = Sequential()
model.add(Input(shape=(4,)))
model.add(Embedding(input_dim=len(token.word_index)+1, output_dim=4))
model.add(SimpleRNN(8))
model.add(Dense(1, activation='sigmoid'))
model.compile(optimizer="adam", loss="binary_crossentropy", metrics=["accuracy"])
model.fit(train_pad, target, epochs=20)
# 5. 예측 잘 되나 해보기
print("\n Accuracy: %.4f" % (model.evaluate(train_pad, target)[1]))
# 6. (선택사항) : 다 했으면, 리뷰의 개수 더 채워넣어서 또 해보기
'로보테크AI' 카테고리의 다른 글
| 융합_로보테크 AI 자율주행 로봇 개발자 과정-26/03/24 (0) | 2026.03.24 |
|---|---|
| 융합_로보테크 AI 자율주행 로봇 개발자 과정-26/03/23 (0) | 2026.03.23 |
| 융합_로보테크 AI 자율주행 로봇 개발자 과정-26/03/20 (0) | 2026.03.20 |
| 융합_로보테크 AI 자율주행 로봇 개발자 과정-26/03/19 (0) | 2026.03.19 |
| 융합_로보테크 AI 자율주행 로봇 개발자 과정-26/03/18[ML, DL] (0) | 2026.03.18 |