본문 바로가기
Bigdata

RNN - Python numpy 기초 코드 실습

by 올엠 2024. 6. 8.
반응형

딥러닝을 공부한다면, 시작은 RNN부터 하는것이 전체적인 흐름을 이해하는데 큰 도움이 된다.

여기에서는 RNN을 파이썬 코드로 작성하는 방식으로 이해해보도록 하겠다.

RNN은 Recurrent Neural Network의 약자로, 시퀀스 데이터를 처리하는 데 유용한 인공 신경망의 한 종류이다.

RNN은 자연어 처리, 음성 인식, 이미지 캡셔닝 등 다양한 분야에 적용될 수 있으며, RNN은 시간 단계의 입력으로 사용하여, 이전 시간 단계의 출력을 현재 시간적인 의존성을 모델링할 수 있다.

RNN을 파이썬으로 구현하는 방법은 여러 가지가 있고, 가장 간단한 방법은 numpy 라이브러리를 사용하는 것이다.

numpy는 다차원 배열과 행렬 연산을 지원하는 파이썬 패키지로, numpy를 사용하면 RNN의 순전파와 역전파를 직접 구현할 수 있습니다.

RNN을 파이썬으로 구현할 때, 다음과 같은 변수들을 먼저 정의해야 한다.

- n_input: 입력 벡터의 차원

- n_hidden: 은닉층의 유닛 수

- n_output: 출력 벡터의 차원

- learning_rate: 학습률

- n_iterations: 반복 횟수

 

- U: 입력층과 은닉층 사이의 가중치 행렬

- W: 은닉층과 은닉층 사이의 가중치 행렬 (Whh)

- V: 은닉층과 출력층 사이의 가중치 행렬

- b: 은닉층의 편향 벡터

- c: 출력층의 편향 벡터

 

이때, U, W, V, b, c는 임의의 작은 값으로 초기화한다. 예를 들어, 다음과 같이 numpy의 random.uniform 함수를 사용할 수 있다.

U = np.random.uniform(0, 1, (n_hidden, n_input))

W = np.random.uniform(0, 1, (n_hidden, n_hidden))

V = np.random.uniform(0, 1, (n_output, n_hidden))

b = np.random.uniform(0, 1, (n_hidden,))

c = np.random.uniform(0, 1, (n_output,))

 

그리고, 활성화 함수와 그 미분을 정의해야 하는데, 은닉층의 활성화 함수로는 하이퍼볼릭 탄젠트 함수를, 출력층의 활성화 함수로는 시그모이드 함수를 사용할 수 있다.

이때, numpy의 tanh, exp, sum 함수를 사용하여 다음과 같이 정의할 수 있다.

def sigmoid(x):
    return 1 / (1 + np.exp(-x))

def sigmoid_prime(x):
    return sigmoid(x) * (1 - sigmoid(x))

def tanh(x):
    return np.tanh(x)

def tanh_prime(x):
    return 1 - np.tanh(x) ** 2

이제 실제적으로 학습에 사용되는 RNN의 순전파와 역전파를 구현해야 한다.

순전파는 입력 시퀀스를 받아서 출력 시퀀스와 은닉 상태를 반환하는 함수이고,

역전파는 입력 시퀀스와 출력 시퀀스와 목표 시퀀스를 받아서 가중치 행렬들의 기울기를 반환하는 함수이다.

이때, numpy의 dot, outer, zeros, arange 함수를 사용할 수 있다. 

예를 들어, 다음과 같이 구현이 가능하다.

#순전파 함수

def forward_propagation(inputs):
    # 입력 시퀀스의 길이
    T = len(inputs)

    # 은닉 상태와 출력을 저장할 배열 초기화
    s = np.zeros((T + 1, n_hidden)) # 은닉 상태
    s[-1] = np.zeros(n_hidden) # 초기 은닉 상태
    o = np.zeros((T, n_output)) # 출력


    # 각 시간 단계별로 순전파 계산
    for t in np.arange(T):
        # 입력층과 은닉층 사이의 선형 결합
        s[t] = np.dot(U, inputs[t]) + np.dot(W, s[t-1]) + b
        # 은닉층의 활성화 함수 적용
        s[t] = tanh(s[t])
        # 은닉층과 출력층 사이의 선형 결합
        o[t] = np.dot(V, s[t]) + c
        # 출력층의 활성화 함수 적용
        o[t] = sigmoid(o[t])

    return [o, s] # 출력과 은닉 상태 반환

    

#역전파 함수
def backward_propagation(inputs, outputs, actual_outputs):
    # 입력 시퀀스의 길이
    T = len(inputs)

    # 기울기를 저장할 배열 초기화
    dU = np.zeros(U.shape) # 입력층과 은닉층 사이의 가중치 기울기
    dW = np.zeros(W.shape) # 은닉층과 은닉층 사이의 가중치 기울기 (dWhh)
    dV = np.zeros(V.shape) # 은닉층과 출력층 사이의 가중치 기울기
    db = np.zeros(b.shape) # 은닉층의 편향 벡터 기울기
    dc = np.zeros(c.shape) # 출력층의 편향 벡터 기울기

    # 순전파 결과 가져오기
    [o, s] = forward_propagation(inputs)

    # 델타 초기화
    delta_o = np.zeros(o.shape) # 출력층의 델타
    delta_t = np.zeros(n_hidden) # 은닉층의 델타

    # 각 시간 단계별로 역전파 계산
    for t in np.arange(T)[::-1]:
        # 출력층의 델타 계산
        delta_o[t] = actual_outputs[t] - o[t]
        # 출력층과 은닉층 사이의 가중치 기울기 계산
        dV += np.outer(delta_o[t], s[t].T)
        # 출력층의 편향 벡터 기울기 계산
        dc += delta_o[t]
        # 은닉층의 델타 계산
        delta_t = np.dot(V.T, delta_o[t]) * tanh_prime(s[t])
        # 은닉층과 은닉층 사이의 가중치 기울기 계산
        dW += np.outer(delta_t, s[t-1])
        # 입력층과 은닉층 사이의 가중치 기울기 계산
        dU += np.outer(delta_t, inputs[t])
        # 은닉층의 편향 벡터 기울기 계산
        db += delta_t

    return [dU, dW, dV, db, dc] # 기울기 반환

 

마지막으로, RNN의 학습을 구현해야 하면 된다. 학습은 입력 시퀀스와 목표 시퀀스를 받아서 가중치 행렬들을 업데이트하는 함수로써, 이때 numpy의 sum, square 함수를 사용할 수 있다.

def train(inputs, outputs, actual_outputs, learning_rate):
    # 역전파 결과 가져오기
    [dU, dW, dV] = backward_propagation(inputs, outputs, actual_outputs)

    # 가중치 업데이트
    U += learning_rate * dU
    W += learning_rate * dW
    V += learning_rate * dV

 

전체 코드는 다음과 같다.

import numpy as np

# 파라미터 설정
n_input = 3 # 입력 벡터의 차원
n_hidden = 5 # 은닉층의 유닛 수
n_output = 2 # 출력 벡터의 차원
learning_rate = 0.01 # 학습률
n_iterations = 100 # 반복 횟수


# 가중치 초기화
U = np.random.uniform(0, 1, (n_hidden, n_input)) # 입력층과 은닉층 사이의 가중치
W = np.random.uniform(0, 1, (n_hidden, n_hidden)) # 은닉층과 은닉층 사이의 가중치
V = np.random.uniform(0, 1, (n_output, n_hidden)) # 은닉층과 출력층 사이의 가중치


# 활성화 함수와 그 미분 정의
def sigmoid(x):
    return 1 / (1 + np.exp(-x))

def sigmoid_prime(x):
    return sigmoid(x) * (1 - sigmoid(x))

def tanh(x):
    return np.tanh(x)

def tanh_prime(x):
    return 1 - np.tanh(x) ** 2

# RNN 순전파 함수 정의
def forward_propagation(inputs):
    # 입력 시퀀스의 길이
    T = len(inputs)

    # 은닉 상태와 출력을 저장할 배열 초기화
    s = np.zeros((T + 1, n_hidden)) # 은닉 상태
    s[-1] = np.zeros(n_hidden) # 초기 은닉 상태
    o = np.zeros((T, n_output)) # 출력

   
    # 각 시간 단계별로 순전파 계산
    for t in np.arange(T):
        # 입력층과 은닉층 사이의 선형 결합
        s[t] = np.dot(U, inputs[t]) + np.dot(W, s[t-1])
        # 은닉층의 활성화 함수 적용
        s[t] = tanh(s[t])
        # 은닉층과 출력층 사이의 선형 결합
        o[t] = np.dot(V, s[t])
        # 출력층의 활성화 함수 적용
        o[t] = sigmoid(o[t])

    return [o, s] # 출력과 은닉 상태 반환



# RNN 역전파 함수 정의
def backward_propagation(inputs, outputs, actual_outputs):
    # 입력 시퀀스의 길이
    T = len(inputs)

    # 기울기를 저장할 배열 초기화
    dU = np.zeros(U.shape) # 입력층과 은닉층 사이의 가중치 기울기
    dW = np.zeros(W.shape) # 은닉층과 은닉층 사이의 가중치 기울기
    dV = np.zeros(V.shape) # 은닉층과 출력층 사이의 가중치 기울기

    # 순전파 결과 가져오기
    [o, s] = forward_propagation(inputs)


    # 델타 초기화
    delta_o = np.zeros(o.shape) # 출력층의 델타
    delta_t = np.zeros(n_hidden) # 은닉층의 델타

    
    # 각 시간 단계별로 역전파 계산
    for t in np.arange(T)[::-1]:
        # 출력층의 델타 계산
        delta_o[t] = actual_outputs[t] - o[t]
        # 출력층과 은닉층 사이의 가중치 기울기 계산
        dV += np.outer(delta_o[t], s[t].T)
        # 은닉층의 델타 계산
        delta_t = np.dot(V.T, delta_o[t]) * tanh_prime(s[t])
        # 은닉층과 은닉층 사이의 가중치 기울기 계산
        dW += np.outer(delta_t, s[t-1])
        # 입력층과 은닉층 사이의 가중치 기울기 계산
        dU += np.outer(delta_t, inputs[t])

    
    return [dU, dW, dV] # 기울기 반환


# RNN 학습 함수 정의
def train(inputs, outputs, actual_outputs, learning_rate):
    # 역전파 결과 가져오기
    [dU, dW, dV] = backward_propagation(inputs, outputs, actual_outputs)

    
    # 가중치 업데이트
    U += learning_rate * dU
    W += learning_rate * dW
    V += learning_rate * dV


# 임의의 입력 시퀀스와 목표 시퀀스 생성
inputs = np.array([np.random.rand(n_input) for _ in range(10)])
actual_outputs = np.array([np.random.rand(n_output) for _ in range(10)])

# RNN 학습
for i in range(n_iterations):
    # 순전파 결과 가져오기
    [outputs, _] = forward_propagation(inputs)
    
    # 오차 계산
    error = np.sum(0.5 * (actual_outputs - outputs) ** 2)
    
    # 오차 출력
    print(f'Iteration {i}, Error: {error}')

    # 역전파 및 가중치 업데이트
    train(inputs, outputs, actual_outputs, learning_rate)

 

 

반응형