ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Neural Network using TensorFlow | Advanced Learning Algorithm
    Machine Learning/Stanford ML Specialization 2024. 2. 9. 18:27

    Coursera Machine Learning Specialization > Supervised Machine Learning: Advanced Learning Algorithms > Neural Network Intuition

     

    실제로 Coffee Roasting한 원두에서 맛이 좋은 원두를 추론하는 것을 TensorFlow로 구현해보자.

     

    먼저, 사용하고자 하는 라이브러리를 import 해준다. 여기서, 몇 데이터는 Coursera 수업에 쓰이기 위해 custom하게 개발된것이기 때문에, public 하지는 않다. 또한, logging설정을 해줘서 로깅 레벨을 ERROR로 설정해주고, Tensorflow의 verbosity도 0으로 지정했다. 로그를 최대한 줄였다.

    import numpy as np
    import matplotlib.pyplot as plt
    plt.style.use('./deeplearning.mplstyle')
    import tensorflow as tf
    from tensorflow.keras.models import Sequential
    from tensorflow.keras.layers import Dense
    from lab_utils_common import dlc
    from lab_coffee_utils import load_coffee_data, plt_roast, plt_prob, plt_layer, plt_network, plt_output_unit
    import logging
    logging.getLogger("tensorflow").setLevel(logging.ERROR)
    tf.autograph.set_verbosity(0)

     

     

    Dataset

    이미 강의에서 만든 커피 데이터를 가져와 사용할것이다. Coffee Roasting at Home 홈페이지를 보면, 로스팅 할 때 시간을 12분에서 15분정도로 갖고, 온도를 175도에서 260도(Celcius)로 유지하는게 좋다고 말하고 있다. 물론, 온도가 올라가게 되면, 시간은 줄어들것이다.

     

    먼저, 사용할 데이터셋을 만들어보자. 이 데이터의 shape는 어떨까?

    X,Y = load_coffee_data();
    print(X.shape, Y.shape)
    (200, 2) (200, 1)

     

    우선 예시는 200개가 있고, X, 즉 인풋에는 2개의 feature가 있다. 온도와 시간이다. 이 데이터를 살펴보자. 하나하나 살펴보기보다는, 주어진 라이브러리를 이용해서 plot을 했다.

    지난 내용에서 예시로 사용했던 것 보다 훨씬 많은 볶은 원두 샘플 데이터가 있다. 200개의 원두 중, 빨간색 X로 표시된 원두들이 모두 적절한 온도와 시간에 로스팅된 예시들이다. 파란색 ㅇ들은 너무 적게/오래 볶았거나, 온도가 너무 낮거나 높아서 너무 타버리거나 맛이 없어진 예시들이다.

     

     

    Normalize Data

    온도와 시간 데이터는 숫자만 보았을 때 너무 큰 값의 차이가 난다. 그렇게 되면, 너무 큰 숫자에 의해서 가중치가 다르게 반영되어서 모델의 정확도가 떨어질 수 있다. 이럴 때를 대비해서 Normalization을 하게 된다. 데이터 연산은, Normalization이 되어있으면 훨씬 더 빠르게 연산 수행이 된다. 이 과정은 데이터의 각 특성이 유사한 범위를 가지도록 정규화합니다. 아래 절차는 케라스(Keras) Normalization Layer를 사용합니다. 다음과 같은 단계를 포함한다:

     

    • "Normalization Layer"를 생성한다. 이것은 모델의 레이어가 아니다.
    • 데이터에 '적응'한다. 데이터 세트의 평균과 분산을 학습하고 내부적으로 값을 저장합니다.
    • 데이터를 정규화(Normalize)한다.

    학습된 모델을 사용할 때, 어떤 데이터에도 정규화를 적용하는 것이 중요하다.

    print(f"Temperature Max, Min pre normalization: {np.max(X[:,0]):0.2f}, {np.min(X[:,0]):0.2f}")
    print(f"Duration    Max, Min pre normalization: {np.max(X[:,1]):0.2f}, {np.min(X[:,1]):0.2f}")
    norm_l = tf.keras.layers.Normalization(axis=-1)
    norm_l.adapt(X)  # learns mean, variance
    Xn = norm_l(X)
    print(f"Temperature Max, Min post normalization: {np.max(Xn[:,0]):0.2f}, {np.min(Xn[:,0]):0.2f}")
    print(f"Duration    Max, Min post normalization: {np.max(Xn[:,1]):0.2f}, {np.min(Xn[:,1]):0.2f}")
    Temperature Max, Min pre normalization: 284.99, 151.32
    Duration Max, Min pre normalization: 15.45, 11.51

    Temperature Max, Min post normalization: 1.66, -1.69
    Duration Max, Min post normalization: 1.79, -1.70

     

    결과값을 보면, 원래 284.99 에서 151.32 사이의 데이터였던 온도값이 1.66에서 -1.69로 조정되고, 15.45에서 11.51사이의 데이터가 1.79에서 -1.70사이로 조정되었다.

     

    이제, 데이터의 양을 증가시켜서 트레이닝 횟수(Epoch)를 줄여보자.

    Xt = np.tile(Xn,(1000,1))
    Yt= np.tile(Y,(1000,1))   
    print(Xt.shape, Yt.shape)
    (200000, 2) (200000, 1)

     

    200개였던 샘플들이 1000개가 넘게 생성되었다.

     

     

    Tensorflow Model

    이제, Coffee Rosting Network 모델을 만들어보자. Sigmoid activation을 가진 2개의 layer들이 있다.

     

    # 같은 결과를 갖기 위해 seed를 1234로 설정했다.
    tf.random.set_seed(1234)
    
    # Sequential하게 뉴런 모델을 생성했다. 입력부터의 간단한 흐름을 보여준다.
    # 여기서 shape=(2,)는 모델이 2개의 Feature를 가지고 있다는걸 말해준다.
    model = Sequential(
    	[
        	tf.keras.Input(shape=(2,)),
            Dense(3, activation='sigmoid', name='layer1'),
            Dense(1, activation='sigmoid', name='layer2')
        ]
    )

     

    아래 코드는 input의 shape를 나타낸다. 이것을 주어서 Tensorflow가 이 시점에서 가중치(weight, w)와 편향(bias, b) 매개변수의 크기를 결정할 수 있게 해준다. 이는 Tensorflow 모델을 탐색할 때 유용하다. 이 과정은 생략할 수 있는데, 이후에 fit 함수를 부를 때 parameter를 size할수 있다고 한다.

    tf.keras.Input(shape=(2,))

     

    또 한가지. sigmoid activation을 마지막 layer에 쓰는것은 best practice가 아니다. 이 내용에 대해서는 나중에 다루도록 한다. 이제 summary 함수를 이용해서 어떤 모델이 생성되었는지 보자.

     

    model.summary()
    
    Model: "sequential"
    _________________________________________________________________
     Layer (type)                Output Shape              Param #   
    =================================================================
     layer1 (Dense)              (None, 3)                 9         
                                                                     
     layer2 (Dense)              (None, 1)                 4         
                                                                     
    =================================================================
    Total params: 13
    Trainable params: 13
    Non-trainable params: 0
    _________________________________________________________________

     

     

    어떻게 해서 parameter 갯수가 9개, 4개가 나와서 총 13개가 된걸까? 계산을 해보자.

    L1_num_params = 2 * 3 + 3   # W1 parameters  + b1 parameters
    L2_num_params = 3 * 1 + 1   # W2 parameters  + b2 parameters
    print("L1 params = ", L1_num_params, ", L2 params = ", L2_num_params  )
    L1 params = 9 , L2 params = 4

     

    2개의 feature가 있기 때문에, w가 각 뉴런당 2개가 필요하다. 그리고 각 뉴런에 한개의 b가 있게 된다. 그러므로 3개의 뉴런이 있는 layer 1에서는 2 * 3 + 3, 즉 9개의 parameter가 생성된다. layer 2에서는 어떨까? layer1의 뉴런 갯수가 3개였으므로 3개의 인풋이 들어오고, 그렇게 되면 3개의 w값이 생성된다. 그리고 한개의 b가 필요하다. layer 2는 뉴런이 1개이므로, 3 + 1, 총 4개의 parameter가 필요하다.

     

    Updated Weights

    그렇다면 모델을 트레이닝 하기 전에 w와 b의 값들이 어떻게 초기화 되었는지 살펴보자.

    W1, b1 = model.get_layer("layer1").get_weights()
    W2, b2 = model.get_layer("layer2").get_weights()
    print(f"W1{W1.shape}:\n", W1, f"\nb1{b1.shape}:", b1)
    print(f"W2{W2.shape}:\n", W2, f"\nb2{b2.shape}:", b2)
    W1(2, 3): [[ 0.08 -0.3 0.18] [-0.56 -0.15 0.89]]
    b1(3,): [0. 0. 0.]
    W2(3, 1): [[-0.43] [-0.88] [ 0.36]]
    b2(1,): [0.]

     

    레이어 1의 w1은 2x3 matrix가, b1은 길이 3의 어레이이다. 두번째 레이어에서는 3x1의 matrix가, b2값에는 하나의 값이 들어가있다. 위에서 계산한대로 w1 6개, b1 3개, w2 3개, b2 1개, 총 13개의 parameter들이 잘 초기화 되어있다. 자, 이제 모델을 트레이닝 해보자.

    model.compile(
        loss = tf.keras.losses.BinaryCrossentropy(),
        optimizer = tf.keras.optimizers.Adam(learning_rate=0.01),
    )
    
    model.fit(
        Xt,Yt,            
        epochs=10,
    )

     

    아직 이 함수들에 대해 자세하게 설명하지 않았지만, 아래와 같은 기능을 통해서 모델을 트레이닝 했다.

    • model.compile 을 이용해서 loss function을 지정해주었고, compile optimization도 설정해주었다.
    • model.fit statement 은 gradient descent(경사 하강법) weights을 조정한다.

    fit 함수에서 epochs를 10으로 설정해 주었는데, 이것은 전체 데이터는 10번 트레이닝을 해야한다고 설정해준것이다. 이 코드를 돌리면 아래와 같이 각 Epoch마다 어떻게 트레이닝이 되고, loss가 얼마였는지를 보여준다.

    Epoch 1/10
    6250/6250 [==============================] - 5s 811us/step - loss: 0.1782
    Epoch 2/10
    6250/6250 [==============================] - 5s 821us/step - loss: 0.1165
    Epoch 3/10
    6250/6250 [==============================] - 5s 811us/step - loss: 0.0426
    Epoch 4/10
    6250/6250 [==============================] - 5s 791us/step - loss: 0.0160
    Epoch 5/10
    6250/6250 [==============================] - 5s 776us/step - loss: 0.0104
    Epoch 6/10
    6250/6250 [==============================] - 5s 807us/step - loss: 0.0073
    Epoch 7/10
    6250/6250 [==============================] - 5s 784us/step - loss: 0.0052
    Epoch 8/10
    6250/6250 [==============================] - 5s 786us/step - loss: 0.0037
    Epoch 9/10
    6250/6250 [==============================] - 5s 800us/step - loss: 0.0027
    Epoch 10/10
    6250/6250 [==============================] - 5s 795us/step - loss: 0.0020

     

    Epoch가 늘어나면서 loss 값이 점점 줄어드는것을 볼 수 있다. 효율적으로 연산하기 위해서, 트레이닝 데이터는 batches들로 쪼개져서 연산되게 된다. 보통 default는 32로 설정되어 있다. 자, 이제 업데이트된 weight 값들을 살펴보자.

    W1, b1 = model.get_layer("layer1").get_weights()
    W2, b2 = model.get_layer("layer2").get_weights()
    print("W1:\n", W1, "\nb1:", b1)
    print("W2:\n", W2, "\nb2:", b2)
    W1: [[-11.86 -0.18 15.61] [ -0.29 -9.45 12.95]]
    b1: [-12.82 -11.71 2.17]
    W2: [[-68.17] [-63.52] [-55.36]]
    b2: [37.49]

     

    TensorFlow를 사용한 신경망 모델 훈련의 과정에서 가중치(weight)의 변화는 매우 중요하다. 모델을 훈련시킨 후 model.fit()을 호출하기 전과 후의 가중치 값을 비교함으로써, 모델이 어떻게 학습을 통해 좋은 커피 로스트와 나쁜 커피 로스트를 구별하는 능력을 개발했는지 확인할 수 있다. 또한, 일관된 논의를 위해 이전 훈련 실행에서 저장한 가중치를 사용하는 것이 권장된다. 이는 TensorFlow가 시간에 따라 변하지만, 실험의 안정성을 유지하기 위함입니다. 서로 다른 훈련 실행은 다소 다른 결과를 낼 수 있으므로, 이전에 저장된 가중치를 사용하면 모델의 일관성을 유지할 수 있습니다. 다시 코드를 돌렸을 때, 낮은 손실을 기록한 경우 대부분 같은 결과를 얻을 것으로 예상할 수 있다. 이러한 접근 방식은 머신 러닝 실험에서의 일관성과 재현성을 보장하는 데 중요한 역할을 한다. 첫번째 실험이 끝나고 다시 실행해서 실제로 결과가 같은지 확인해보자.

     

    Prediction

    트레이닝된 모델을 사용해서 예측을 수행해보자. 여기서 트레이닝 한 모델의 출력은 확률값인데, 좋은 로스트였는지에 대한 확률이다. 이 로스팅이 좋았는지 아닌지, 좋은 커피를 만들어낼 볶은 원두가 되었는지 결정을 내리기 위해서는 이 확률에 임계값(threshold)을 적용해야 한다. 이 값을 0.5라고 해보자.

    우선 테스트를 해볼 입력 데이터를 생성해보자. 모델은 하나 이상의 예시를 받을 수 있는데, 이 예시들은 행렬의 행들이다. 커피 예시의 경우에는 두 가지 특성(온도, 시간)이 있으므로, 행렬은 (m, 2)의 사이즈가 도고, 여기서 m은 예시의 수 이다. 입력 특성(Feature)을 정규화(Normalization)했기 때문에 테스트 데이터도 마찬가지로 정규화를 해주어야한다. 예측을 하기 위해서는 predict 메소드를 적용한다.

    이러한 과정을 통해, 훈련된 모델은 새로운 데이터에 대한 예측을 제공하고, 이 예측은 주어진 데이터가 좋은 커피 로스트일 확률을 나타낸다. 예측값이 0.5 이상이면 좋은 로스트였고, 미만이면 아니라고 추론할 수 있다.

     

    # 예시를 생성했다. 첫번째는 좋은 로스팅을, 두번쨰는 좋지않은 예시이다.
    X_test = np.array([
        [200,13.9],  # positive example
        [200,17]])   # negative example
        
    # 이 값을 normalization 해주었다.
    X_testn = norm_l(X_test)
    
    # 이제 우리가 트레이닝 한 모델을 이용해서 예측을 해보자.
    predictions = model.predict(X_testn)
    
    # 예측된 값을 print했다.
    print("predictions = \n", predictions)
    predictions = [[9.98e-01] [1.69e-08]]

     

    예측된 값은 2 x 1 매트릭스를 리턴했다. 첫번째 예시는 0.998, 즉 거의 완벽한 로스팅이었다고 판단했고, 두번째 예시는 0.0000000169 로, 좋은 로스팅일 확률이 아주 낮았다. 즉 나쁘다고 예측했다. 이것을 우리 계산이 아닌, 코드가 하도록 해보자.

     

    # 예시를 이용해서 모두 0으로 채워진 배열을 생성한다. predictions와 같은 형태이다
    yhat = np.zeros_like(predictions)
    
    # 모든 예측값에 대해 임계값(threshold)하고 비교를 해서 좋은 로스팅이었는지 판단한다.
    for i in range(len(predictions)):
        if predictions[i] >= 0.5:
            yhat[i] = 1
        else:
            yhat[i] = 0
            
    # 결과값(1/0)이 포함된 배열을 프린트한다.
    print(f"decisions = \n{yhat}")
    decisions = [[1.] [0.]]

    위에서 계산한대로, 처음 예시는 1(True)가, 두번째 예시는 0(False)가 나왔다.

     

    Layer Functions

    입력값(duration, temp)의 모든 가능한 조합에 대해 각 노드의 출력이 어떤것인지 그려보자. 각 유닛은 0부터 1까지의 값을 출력할 수 있는 로지스틱 함수이다. 그래프에서 음영은 출력값을 나타낸다. 

    plt_layer(X,Y.reshape(-1,),W1,b1,norm_l)

     

    뉴런 3개에서 각각의 출력

    위 plot을 보면 각각의 뉴런이 '나쁜 로스트' 지역에 대해 어떻게 책임을 지는지 설명하고 있다. 첫번째, 유닛 0에서는 온도가 너무 낮을 때 더 큰 값을 가지고, 유닛 1은 로스팅 시간이 너무 짧을 때 더 큰 값을 가짐을 보여준다. 그리고 유닛 2는 시간과 온도의 나쁜 조합에 대해 더 큰 값을 가진다. 뉴런 네트워크가 경사 하강법을 통해 이러한 함수를 스스로 학습했다는 점이 정말 놀랍다. 이 함수들은 사람이 좋은/나쁜 로스팅에 대한 결정을 내리기 위해 선택할 수 있는 유형의 생각과 매우 비슷하다.

    우리가 생각했던 나쁜 구역과 매우 흡사했다.


    최종 레이어의 함수 플롯을 시각화하는 것은 어렵다.  첫 번째 레이어의 출력이 입력이 되는데, 첫 번째 레이어가 시그모이드 함수를 사용하여 그 출력 범위가 0과 1 사이이다. 세 입력의 모든 가능한 조합에 대한 출력을 계산하는 3차원 그래프를 생성해보자. 아래에 나타나 있는 그래프에서, 높은 출력값은 '나쁜 로스트' 영역에 해당합니다. 반면, 최대 출력은 세 입력이 모두 작은 값으로, '좋은 로스트' 영역에 해당하는 곳에서 나타납니다. 즉, layer 1에서 각 유닛은 '나쁜구역' 에 대한 값들을 내보냈다. 이 값들이 모두 작으면 자동적으로 '좋은 로스트'가 되는것이다.

     

    plt_output_unit(W2,b2)

    unit 0, 1, 2는 layer1에서 각 뉴런으로부터의 값들이다

     

     

    이제 신경망 전체가 작동하는 것을 보여주는 그래프를 그려보자. 왼쪽 그래프는 최종 레이어의 원시 출력을 파란색 음영으로 나타내고 있고, 이는 X와 O로 표현된 훈련 데이터 위에 중첩되어 있다. 오른쪽 그래프는 의사 결정 임계값(threshold)을 적용한 후의 네트워크 출력을 보여준다. 여기서 X와 O는 네트워크가 내린 결정이다. 즉, 왼쪽 그래프는 신경망이 어떻게 원시 데이터를 처리하는지, 오른쪽 그래프는 신경망이 최종적으로 어떤 결정을 내리는지를 보여준다.

    netf= lambda x : model.predict(norm_l(x))
    plt_network(X,Y,netf)

     

    Reference

    Coursera Machine Learning Specialization > Supervised Machine Learning: Advanced Learning Algorithms > Neural Network Intuition

     

    Advanced Learning Algorithms

    In the second course of the Machine Learning Specialization, you will: • Build and train a neural network with TensorFlow to perform multi-class ... 무료로 등록하십시오.

    www.coursera.org

     

     

     

     

     

    댓글

Designed by Tistory.