LITTLE BY LITTLE

[1]실전! 텐서플로2를 활용한 딥러닝 컴퓨터비전 (1장.컴퓨터 비전, 신경망 시작하기) 본문

4-2/이미지 딥러닝

[1]실전! 텐서플로2를 활용한 딥러닝 컴퓨터비전 (1장.컴퓨터 비전, 신경망 시작하기)

위나 2022. 9. 7. 09:14

목차

1부. 컴퓨터 비전에 적용한 텐서플로2와 딥러닝

01. 컴퓨터 비전과 신경망
02. 텐서플로 기초와 모델 훈련 (케라스, 텐서보드, 애드온, 확장, 라이트, TensorFlow.js, 모델 실행 장소)
03. 현대 신경망 (다차원 데이터를 이용한 신경망, CNN작업, 유효수용영역, 훈련 프로세스 개선, 정규화 기법)

더보기

2부. 전통적 인식 문제를 해결하는 최신 솔루션

04. 유력한 분류 도구 (고급 CNN아키텍처의 이해 - VGG, GoogleLeNet, Inception,ResNet[잔차 네트워크], 전이학습)

05. 객체 탐지 모델 (YOLO, Faste R-CNN, 텐서플로 객체 탐지 API)

06. 이미지 보강 및 분할 (인코더-디코더로 이미지 변환, 노이즈 제거, 객체 분할)

3부. 컴퓨터 비전의 고급 개념 및 새 지평

07. 복합적이고 불충분한 데이터셋에서 훈련시키기 (입력 파이프라인, 데이터셋 보강, VAE와 GAN)

08. 동영상과 순환 신경망 (RNN, 장단기 메모리 셀, LSTM으로 동영상 분류하기)

09. 모델 최적화 및 모바일 기기 배포 (계산 및 디스크 용량 최적화, 온디바이스 머신러닝, MobileNet)


01. 컴퓨터 비전과 신경망

컴퓨터 비전은 크게 4가지로 나뉨

  1. 콘텐츠 인식
    1. 컴퓨터 비전의 주요 목적은 이미지를 '이해'하는 것, 즉 픽셀로부터 유의미한, 의미론적 정보를 추출하는 것
    2. 하위 영역
      1. 객체 분류 - 사전 정의된 집합의 이미지에 적절한 레이블(클래스) 할당
      2. 객체 식별 - 클래스의 특정 인스턴스 인식, 데이터셋을 클러스터링 하는 절차
      3. 객체 탐지와 위치 측정
      4. 객체 및 인스턴스 분할 - 특정 클래스에 속한 '모든 픽셀에 레이블을 단 마스크를 반환'
      5. 자세 추정
  2. 동영상 분석
    1. 인스턴스 추적 - 움직임 연속성이 핵심속성
    2. 행동 인식
    3. 움직임 추정 - 실제 속도/궤도 추정
  3. 이미지 자체 개선
    1. 잡음 제거 denoising
    2. 흐릿한 부분제거 deblurring
    3. 변환 resolution enhancing
    4. 해상도 개선 super-resolution
  4. 장면 복원

신경망 시작하기

  1. 인공 뉴런은 몇 개의 입력을 취해서 합계를 내고, 마지막으로 활성화 함수를 적용해 네트워크의 다음 뉴런에 전달될 '출력' 신호를 얻는다.
    1. 입력은 일반적으로 가중 합으로 더해짐
    2. 각 입력은 특정 입력에 주어진 '가중치'에 따라 크거나 작아짐
      1. 가중치는 네트워크 훈련 단계에서 뉴런이 적절한 특징에 반응하게 하기 위해 조정되는 매개변수
      2. 뉴런의 '편향 값'은 단순히 오프 셋으로 가중합에 더해짐
    3. 입력 크기가 바뀌고 더해져 결과 z를 얻으면, 뉴런의 출력을 얻기 위해 그 결과에 '활성화 함수' 적용
      1. 활성화 함수는 y가 임곗값 t보다 크면 전기 자극(1)을 반환하고 그렇지 않으면 0(일반적으로 t=0)을 반환하는 이진함수
        1. 시그모이드 함수 
        2. 쌍곡 탄젠트
        3. 정류 선형 유닛 ReLu
        4. 비선형 활성화 함수를 사용하지 않고 뉴런을 연결하는 것은 단일 뉴런을 갖는 것이나 마찬가지, 복잡한 모델 생성시 비선형 활성화함수는 불가피함
  2. 이 인공 뉴런은 신호를 받아 처리해 다른 뉴런에 '전달'될 값을 출력으로 만들어서 네트워크 구성 

구현 ( At the Biginning : Neuron )

  1. Args
    1. num_inputs(int) : 입력 벡터 크기 / 입력 값 개수
    2. activation_fn (callable) : 활성화 함수
  2. Attributes
    1. W (ndarray) : 각 입력에 대한 가중치
    2. b (float) : 편향값, 가중합에 더해짐
    3. activation_fn (callable) : 활성화 함수
import numpy as np

class Neuron(object):
   def __init__(self, num_inputs, activation_function):
      super().__init__() 
    # 랜덤 값으로 '가중치 벡터'와 '편향 값'을 초기화함:
      self.W = np.random.uniform(size=num_inputs, low=-1., high=1.)
      self.b = np.random.uniform(size=1, low=-1, high=1.)
      self.activation_function=activation_function
    
   def forward(self, x):
    # 뉴런을 통해 입력 신호를 전달:
    z = np.dot(x, self.W) + self.b
    return self.activation_function(z)
퍼셉트론(활성화 기법으로 계단 함수를 사용하는 뉴런) 인스턴스화해서 임의의 입력값을 이 퍼셉트론을 통해 전달하자)
# 결과를 복제하기 위해 랜덤 숫자 생성기의 시드 값을 고정
np.random.seed(42)

# 3개의 랜덤입력을 칼럼으로 받을 수 있는 배열 (shape='(1,3)')
input_size = 3
print(x)

# 계단 함수
step_fn = lambda y: 0 if y<=0 else 1

# 퍼셉트론을 인스턴스화
perceptron = Neuron(num_inputs=input_size, activation_function = step_fn)
print("Perceptron's random weights = {}, and random bias = {}".format(perceptron.W, perceptron.b))​

[Out]

[[0.86617615 0.60111501 0.70807258]]

Perceptron's random weights = [-0.25091976  0.90142861  0.46398788], and random bias = [0.19731697]

 

We randomly generate a random input vector of 3 values

(i.e. a column-vector of (shape = (1, 3)), to be fed to our neuron:

x = np.random.rand(input_size).reshape(1, input_size)
print("Input vector : {}".format(x))

[Out]

Input vector : [[0.15601864 0.15599452 0.05808361]]

We can now feed our perceptron with this input and display the corresponding activation.

y = perceptron.forward(x)
print("Perceptron's output value given 'x':{}".format(y))

With this Neuron class, we implemented the mathematical model for neurons proposed by early A.I. scientists.


뉴런을 계층화하기

  1. 일반적으로 신경망은 '계층' (동일한 입력을 받고, 동일한 연산을 적용하는 뉴런 집합) 으로 구성
    1. ex. 동일한 활성화 함수를 적용해 입력을 받아 맨 처음 가중치를 사용해 합을 구함
  2. 수학 모델
    1. 네트워크에서 정보는 입력=>출력 계층으로, 그 사이에 하나 이상의 '은닉'계층을 두고 흐른다.
    2. 각 뉴런이 이전 계층에서 나온 모든 값에 연결되어 있는 층을 '완전 연결 계층(fully-connected layer)' 또는 '밀집 계층(dense layer)'이라 함

완전 연결 층 구현 ( Layering Neurons Together )

  1. Args
    1. num_inputs(int) : 입력 벡터 크기 / 입력 값 개수
    2. layer_size (int) : 출력 벡터 크기 / 뉴런의 개수
    3. activation_fn (callable) : 활성화 함수
  2. Attributes
    1. W (ndarray) : 각 입력에 대한 가중치
    2. b (float) : 편향값, 가중합에 더해짐
    3. size (int) : 계층의 크기 / 뉴런의 개수
    4. activation_fn (callable) : 활성화 함수
class FullyConnectedLayer(object):
    def __init__(self, num_inputs, layer_size, activation_function, derivated_activation_function = None):
        super().__init__()

        # 임의로 매개변수 초기화(여기서는 정규 분포 사용):
        self.W = np.random.standard_normal((num_inputs, layer_size))
        self.b = np.random.standard_normal(layer_size)
        self.size = layer_size

        self.activation_function = activation_function
        self.derivated_activation_function = derivated_activation_function
        self.x, self.y = None, None
        self.dL_dW, self.dL_db = None, None


    def forward(self, x):
        # 계층을 통해 입력 신호를 전달
        z = np.dot(x, self.W) + self.b
        self.y = self.activation_function(z)
        self.x = x # store the input & output values for BACK-PROPAGATION
        return self.y
    

    def backward(self,dL_dy):
        dy_dz = self.derivated_activation_function(self.y) 
        dL_dz = (dL_dy) 
        dz_dw = self.x.T
        dz_dx = self.W.T
        dz_db = np.ones(dL_dy.shape[0]) 

        
        # Compute derivatives with respect to the layer's parameters, and storing them for OPT.OPTIMIZATION:
        self.dL_dW = np.dot(dz_dw, dL_dz)
        self.dL_db = np.dot(dz_db, dL_dz)


        # Compute the derivative with respect to the input, to be passed to the previous layers (their 'dL_dy'):
        dL_dx = np.dot(dL_dz, dz_dx)
        return dL_dx
        
    def optimize(self, epsilon):
        self.W -= epsilon*self.dL_dW
        self.b -= epsilon*self.dL_db

완전 연결층 적용

input_size    = 2
num_neurons   = 3
relu_function = lambda y: np.maximum(y, 0)

layer = FullyConnectedLayer(num_inputs=input_size, layer_size=num_neurons, activation_function=relu_function)
x1 = np.random.uniform(-1, 1, 2).reshape(1, 2)
print("Input vector #1: {}".format(x1))
x2 = np.random.uniform(-1, 1, 2).reshape(1, 2)
print("Input vector #2: {}".format(x2))

can do it separately

y1 = layer.forward(x1)
print("Layer's output value given `x1` : {}".format(y1))
y2 = layer.forward(x2)
print("Layer's output value given `x2` : {}".format(y2))
x12 = np.concatenate((x1, x2))  # stack of input vectors, of shape `(2, 2)`
y12 = layer.forward(x12)
print("Layer's output value given `[x1, x2]` :\n{}".format(y12))

[Out]

Layers output value given [x1, x2] :
[[0.         0.4593046  1.61941647]
 [0.         0.73048436 1.05288999]]

=> 한 계층에 여러 뉴런을 반영할 수 있게 일부 변수의 '차원'을 바꾸기만 하면 된다.
=> 계층이 한번에 여러 개 입력을 처리할 수 있음
* 배치 : 일반적으로 입력 데이터의 스택을 칭함


분류를 위한 신경망 만들기

def sigmoid(x):             # 시그모이드 함수
    y = 1 / (1 + np.exp(-x))
    return y


def derivated_sigmoid(y):   # 시그모이드 미분 함수
    return y * (1 - y)

- L2 loss, Cross-entropy loss 사용

def loss_L2(pred, target):             # L2 손실 함수
    return np.sum(np.square(pred - target)) / pred.shape[0] # option. 배치 사이즈로 나누기


def derivated_loss_L2(pred, target):   # L2 미분 함수
    return 2 * (pred - target)


def binary_cross_entropy(pred, target):            # cross-entropy 손실 함수
    return -np.mean(np.multiply(np.log(pred), target) + np.multiply(np.log(1 - pred), (1 - target)))


def derivated_binary_cross_entropy(pred, target):  # cross-entropy 미분 함수
    return (pred - target) / (pred * (1 - pred))

As described in the book, we should now

  1. connect everything together,
  2. building a class able to connect multiple neural layers together, able to to feed-forward data through these layers
  3. and back-propagate the loss' gradients for training:
  1. Args
    1. num_inputs(int) : 입력 벡터 크기 / 입력 값 개수
    2. hidden_layers_size (list)
    3. layer_size (int) : 출력 벡터 크기 / 뉴런의 개수
    4. activation_fn (callable) : 활성화 함수
    5. loss_function (callable)
    6. derivated_loss_function (callable) : back-propagation을 위한 손실 함수
  2. Attributes
    1. W (ndarray) : 각 입력에 대한 가중치
    2. b (float) : 편향값, 가중합에 더해짐
    3. size (int) : 계층의 크기 / 뉴런의 개수
    4. activation_fn (callable) : 활성화 함수
    5. layers (list)

분류에 사용할 수 있는 전방 전달 신경망(feed forward neural network) SimpleNetwork 구현 (▼)

더보기
class SimpleNetwork(object):
    
    def __init__(self, num_inputs, num_outputs, hidden_layers_sizes=(64, 32),
                 activation_function=sigmoid, derivated_activation_function=derivated_sigmoid,
                 loss_function=loss_L2, derivated_loss_function=derivated_loss_L2):
        super().__init__()
        # We build the list of layers composing the network, according to the provided arguments:
        layer_sizes = [num_inputs, *hidden_layers_sizes, num_outputs]
        self.layers = [
            FullyConnectedLayer(layer_sizes[i], layer_sizes[i + 1], 
                                activation_function, derivated_activation_function)
            for i in range(len(layer_sizes) - 1)]

        self.loss_function = loss_function
        self.derivated_loss_function = derivated_loss_function

    def forward(self, x):
        """
        Forward the input vector through the layers, returning the output vector.
        Args:
            x (ndarray): The input vector, of shape `(batch_size, num_inputs)`.
        Returns:
            activation (ndarray): The output activation value, of shape `(batch_size, layer_size)`.
        """
        for layer in self.layers: # from the input layer to the output one
            x = layer.forward(x)
        return x

    def predict(self, x):
        """
        Compute the output corresponding to input `x`, and return the index of the largest 
        output value.
        Args:
            x (ndarray): The input vector, of shape `(1, num_inputs)`.
        Returns:
            best_class (int): The predicted class ID.
        """
        estimations = self.forward(x)
        best_class = np.argmax(estimations)
        return best_class

    def backward(self, dL_dy):
        """
        Back-propagate the loss hrough the layers (require `forward()` to be called before).
        Args:
            dL_dy (ndarray): The loss derivative w.r.t. the network's output (dL/dy).
        Returns:
            dL_dx (ndarray): The loss derivative w.r.t. the network's input (dL/dx).
        """
        for layer in reversed(self.layers): # from the output layer to the input one
            dL_dy = layer.backward(dL_dy)
        return dL_dy

    def optimize(self, epsilon):
        """
        Optimize the network parameters according to the stored gradients (require `backward()`
        to be called before).
        Args:
            epsilon (float): The learning rate.
        """
        for layer in self.layers:             # the order doesn't matter here
            layer.optimize(epsilon)

    def evaluate_accuracy(self, X_val, y_val):
        """
        Given a dataset and its ground-truth labels, evaluate the current accuracy of the network.
        Args:
            X_val (ndarray): The input validation dataset.
            y_val (ndarray): The corresponding ground-truth validation dataset.
        Returns:
            accuracy (float): The accuracy of the network 
                              (= number of correct predictions/dataset size).
        """
        num_corrects = 0
        for i in range(len(X_val)):
            pred_class = self.predict(X_val[i])
            if pred_class == y_val[i]:
                num_corrects += 1
        return num_corrects / len(X_val)

    def train(self, X_train, y_train, X_val=None, y_val=None, 
              batch_size=32, num_epochs=5, learning_rate=1e-3, print_frequency=20):
        """
        Given a dataset and its ground-truth labels, evaluate the current accuracy of the network.
        Args:
            X_train (ndarray): The input training dataset.
            y_train (ndarray): The corresponding ground-truth training dataset.
            X_val (ndarray): The input validation dataset.
            y_val (ndarray): The corresponding ground-truth validation dataset.
            batch_size (int): The mini-batch size.
            num_epochs (int): The number of training epochs i.e. iterations over the whole dataset.
            learning_rate (float): The learning rate to scale the derivatives.
            print_frequency (int): Frequency to print metrics (in epochs).
        Returns:
            losses (list): The list of training losses for each epoch.
            accuracies (list): The list of validation accuracy values for each epoch.
        """
        num_batches_per_epoch = len(X_train) // batch_size
        do_validation = X_val is not None and y_val is not None
        losses, accuracies = [], []
        for i in range(num_epochs): # for each training epoch
            epoch_loss = 0
            for b in range(num_batches_per_epoch):  # for each batch composing the dataset
                # Get batch:
                batch_index_begin = b * batch_size
                batch_index_end = batch_index_begin + batch_size
                x = X_train[batch_index_begin: batch_index_end]
                targets = y_train[batch_index_begin: batch_index_end]
                # Optimize on batch:
                predictions = y = self.forward(x)  # forward pass
                L = self.loss_function(predictions, targets)  # loss computation
                dL_dy = self.derivated_loss_function(predictions, targets)  # loss derivation
                self.backward(dL_dy)  # back-propagation pass
                self.optimize(learning_rate)  # optimization of the NN
                epoch_loss += L

            # Logging training loss and validation accuracy, to follow the training:
            epoch_loss /= num_batches_per_epoch
            losses.append(epoch_loss)
            if do_validation:
                accuracy = self.evaluate_accuracy(X_val, y_val)
                accuracies.append(accuracy)
            else:
                accuracy = np.NaN
            if i % print_frequency == 0 or i == (num_epochs - 1):
                print("Epoch {:4d}: training loss = {:.6f} | val accuracy = {:.2f}%".format(
                    i, epoch_loss, accuracy * 100))
        return losses, accuracies

예제 신경망을 분류에 적용하기

import numpy as np
%matplotlib inline
import matplotlib
import matplotlib.pyplot as plt
from keras.datasets import mnist
import tensorflow as tf
np.random.seed(42)
data_train, data_test = tf.keras.datasets.mnist.load_data()
(X_train, y_train) = data_train
(X_test, y_test) = data_test

num_classes = 10 # 클래스는 0부터9
X_train.shape

[Out]

(60000, 28, 28)

=> 60,000개의 training samples, with each sample an image of 28 x 28 pixels

X_test.shape

[Out]

(10000, 28, 28)

=> 10,000개의 test samples, with each sample an image of 28 x 28 pixels

matplotlib으로 데이터 확인

img_idx = np.random.randint(0, X_test.shape[0])
plt.imshow(X_test[img_idx], cmap=matplotlib.cm.binary)
plt.axis("off")
plt.show()

y_test[img_idx]

[Out]

6

=> 이미지가 ground-truth label과 일치하는 것을 확인할 수 있다.
네트워크는 컬럼 벡터만을 입력으로 받기 때문에, 이미지를 1D 벡터로 Flatten시켜주어야 한다.
(즉, 28 x 28 = 784이므로, shape이 (1,784)인 벡터로 펴줌 )

# flatten to 28 x 28 = 784
X_train, X_test = X_train.reshape(-1, 28*28), X_test.reshape(-1, 28*28)

사이즈뿐만 아니라. 각 데이터를 구성하는 픽셀 값도 확인해야한다.

# pixel value 확인
print("Pixel values between {} and {}".format(X_train.min(), X_train.max()))

[Out]

Pixel values between 0 and 255

=> (0, 255) are normal integer values for images with 8 bits per channel (unit8)
=> 하지만 입력 값이 너무 크면 시그모이드가 nan을 리턴하기 대문에, 0-1로 정규화 시켜주기(scale)

# 정규화
X_train, X_test = X_train / 255., X_test / 255.
print("Normalized pixel values between {} and {}".format(X_train.min(), X_train.max()))

[Out]

Normalized pixel values between 0.0 and 1.0

기존의 라벨을 후에 loss를 계산하기 위해 원-핫 인코딩 해주기

y_train = np.eye(num_classes)[y_train] # 원-핫 인코딩

SimpleNetwork class 사용, 2개의 은닉층을 가진 네트워크를 구현해보자.
- flattened image를 입력으로 하여 각각의 클래스로 분류된 이미지를 대표하는 10-value 벡터 반환하도록
- mnist image분류를 위한 네트워크, 그 안에 크기가 64, 32개인 2개의 은닉층이 있음

# instantiate the network
mnist_classifier = SimpleNetwork(num_inputs = X_train.shape[1],
                                 num_outputs = num_classes, hidden_layers_sizes=[64,32])

Check how the network performs (training set에서 loss를, test set에서 accuracy를 확인)

predictions = mnist_classifier.forward(X_train)  # Forward Pass
loss_untrained = mnist_classifier.loss_function(predictions, y_train)   # Loss Computation

accuracy_untrained = mnist_classifier.evaluate_accuracy(X_test, y_test) # Accuracy
print("Untrained : training loss = {:.6f} | val accuracy = {:.2f}%".format(
    loss_untrained, accuracy_untrained * 100))

[Out]

Untrained : training loss = 4.436700 | val accuracy = 12.19%

=> 정확도가 겨우 12.19%밖에 되지 않는다.
=> 매개변수를 임의의 값으로 정했기에 당연한 결과, 이 예제의 용도에 맞게 신경망을 훈련시켜야 한다.

오래 걸림, epoch줄여서 실행하기

losses, accuratcies = mnist_classifier.train(X_train, y_train, X_test, y_test,
                                             batch_size = 30, num_epochs = 500)

그래프 그리기

losses, accuracies = [loss_untrained] + losses, [accuracy_untrained] + accuracies
fig, ax_loss = plt.subplots()

color = 'red'
ax_loss.set_xlim([0,510])
ax_loss.set_xlabel('Epochs')
ax_loss.set_ylabel('Training Loss', color=color)
ax_loss.plot(losses, color=color)
ax_loss.tick_params(axis='y', labelcolor=color)

ax_acc = ax_loss.twinx() 
color = 'blue'
ax_acc.set_xlim([0,510])
ax_acc.set_ylim([0,1])
ax_acc.set_ylabel('Val Accuracy', color=color)
ax_acc.plot(accuracies, color=color)
ax_acc.tick_params(axis='y', labelcolor=color)

fig.tight_layout()
plt.show()

[Out]

이제 신경망이 랜덤 테스트 이미지를 분류할 수 있다.
- np.expand_dims(x,0) : Simulate a batch by transforming the image shape

predicted_class = mnist_classifier.predict(np.expand_dims(X_test[img_idx],0))
print('Predicted class: {}; Correct class: {}'.format(predicted_class, y_test[img_idx]))

[Out]

Predicted class: 1; Correct class: 1

 

Comments