[DL] #3. 인공신경망

wonnie1224·2022년 10월 23일
0

ML / DL

목록 보기
2/5

1. 신경망이란?

1.1. 퍼셉트론의 단점을 보완하기 위한 인공신경망의 등장

  • 장점 : 퍼셉트론 이용하여 복잡한 함수 표현 가능
  • 단점 : 가중치 설정 - 사람이 수동으로 진행해야 함
    퍼셉트론의 단점을 보완하기 위해~
    => 신경망 : 가중치 매개변수의 적절한 값을 데이터로부터 자동으로 학습(결정)하는 알고리즘

1.2. 신경망의 구성

1) 입력층 (input layer)
: 데이터셋으로부터 입력 받음

  • 0층

2) 은닉층 (hidden layer)
: 사람 눈에 안 보임

  • 파라미터들의 의미 모름(왜 가중치가 크게 부여됐는지 모름)
  • 1층

3) 출력층 (output layer)
: 출력 데이터가 나오는 층

  • 2층으로 표현하기도 함

  • 실제 연산은 화살표에서 일어남

  • 가중치 갖는 층 2개 뿐 -> '2층 신경망'

  • 몇 층인지 : (입력층 + 은닉층 + 출력층의 개수 - 1)개
    => 인공신경망의 구조 = 퍼셉트론의 구조

1.3. 퍼셉트론 복습


  • b(편향)이 네트워크에 안 보이므로 명시적으로 적어서 표현하기 위해
    -> 입력이 1, 가중치가 b인 뉴런 추가 (주의 : 편향 입력 신호는 항상 1임)

  • 전체 동작 과정
    -- x1, x2, b 신호가 뉴런에 입력됨
    -- 각 신호에 가중치가 곱해짐 : w1x1, w2x2, b1
    -- 다음(층의) 뉴런에 전달
    -- 다음 뉴런에선 이 신호들을 더함 : w1
    x1 + w2x2 + b1
    -- 총합 > 0 => 1 출력
    -- 총합 < 0 => 0 출력

  • 간결한 수식 유도 : 조건 분기의 동장을 h(x)라는 함수로 표현하면

1.4. 신경망이란?

활성화 함수 (activation function)
: 입력신호의 총합 -> 출력 신호로 변환하는 함수

  • 입력신호의 총합이 활성화를 일으키는지를 정해줌
  • 활성화 = 출력신호 (y)가 1임

활성화 함수의 처리 과정

  • 가중치 신호를 조합한 결과 : a라는 뉴런이 됨 -> 활성화함수 h()를 통과해 y라는 뉴련이 됨
    (활성화 처리 과정을 명시하여 표현한 그림)

  • 단층 퍼셉트론 : 단층 네트워크에서 계단 함수(임계값을 경계로 출력이 바뀌는 함수)를 활성화 함수로 사용한 모델

  • 다층 퍼셉트론 = 신경망 : #층으로 구성, 시그모이드 함수 등 매끈한 활성화 함수를 사용


2. 활성화 함수 (activation function)

2.1. 활성화 함수 vs. 계단 함수

  • 활성화 함수 (activation function) : 입력신호의 총합 -> 출력 신호로 변환하는 함수
  • 계단 함수 (step function) : 임계값을 경계로 출력이 변하는 함수

=> 단순 퍼셉트론 : 계단 함수 사용
=> 계단 함수를 치환할 함수를 찾자!

2.2. 시그모이드 함수 (sigmoid function)

: 계단 함수를 스무딩 해줌

  • 신경망에 자주 활용되는 활성화 함수

2.3. python으로 계단 함수 구현

ver 1) np.array 배열 형태를 집어넣을 수 X

ver 2) np.array 배열 형태를 집어넣을 수 O

  • astype() : 자료형 변환할 때 사용
  • np.int : 정수형 뜻함
return y.astype(np.int)	# y를 bool 배열 -> int 배열로 바꾼 걸 리턴하삼
  • bool -> int : True = 1 / False = 0

나의 코드 해석 )

계단함수 그래프 작성 코드 해석 참고 링크

def step_function(x):
	return np.array(x > 0, dtype = np.int)
    # np.array로 새로운 넘파이 배열 생성
    # 매개변수로 들어온 x 넘파일 배열의 값이 0보다 클 경우 True = 1 / 작을 경우 False = 0이 돼서 새로운 넘파이 배열에 저장됨
    # 이 새로운 넘파이 배열이 결과로 반환됨
    
x = np.arrange(-5.0, 5.0, 0.1)	# 매개변수로 넣을 넘파이 배열을 -5.0에서 +5.0까지 증가폭 0.1로 생성함
# ylim으로 y축 범위 설정

💡 np.arange(시작점(생략 시 0), 끝점(미포함), step size(생략 시 1))
- 실수 단위도 표현 가능
- 실행 결과의 수열이 numpy array 형태의 자료형에 들어가 있음

2.4. python으로 시그모이드 함수 구현

def sigmoid(x):
	return 1 / (1 + np.exp(-x))
  • x가 np.array여도 계산 가능 - 브로드캐스트 기능 때문!

💡 브로드캐스트 기능 (Broadcasting)
: 서로 다른 차원의 배열 연산하기

  • 스칼라 값을 자동으로 행렬 크기를 맞춰 줘서 계산하는 기능
    - numpy 는 스칼라는 () 배열로 취급해서 행렬 크기만큼 늘려줘서 스칼라 & 행렬을 연산함

2.5. 시그모이드 함수 vs. 계단 함수

1) 차이점

(신경망의) 시그모이드 함수(퍼셉트론의) 계단 함수
부드러운 곡선 & 입력에 따라 출력이 연속적으로 변화급작스럽게 0 -> 1로 변화
출력으로 실수를 리턴출력으로 0 & 1만 리턴
연속적인 실수의 신호가 흐름0 & 1의 신호만 있음

2) 공통점

  • 0 ~ 1 사이의 값 리턴함
  • 비선형 함수임

💡 선형 vs. 비선형 함수

  • 선형 함수 : 함수에 무언가 입력했을 때 출력이 입력의 상수배만큼 변하는 함수
    - f(x) = ax + b
    • 1개의 직선으로 나타남
  • 비선형 함수(= 선형이 아님) : 직선 1개로 그릴 수 없는 함수

2.5. 활성화함수는 비선형 함수이다!

  • 선형 함수는 사용 불가능
    why) 선형 함수 - 몇 개를 쌓던지 결과가 동일함
    => 선형 함수 쓰면 신경망의 층 깊게 하는 의미 X
  • 선형 함수 사용하면 층 아무리 깊게 해도 '은닉층이 없는 네트워크'와 같은 기능함 (위에 말이랑 같은 말임)
    if) h(x) = cx를 3층 쌓으면 y(x) = h(h(h(x))) = ccc*x
    => 결국 y(x) = ax 꼴임
    => 층 쌓는다고 좋은 점 1도 없다 =.=

ReLU 함수 (연속적인 비선형 함수)

: 입력이 0 넘으면 입력 그대로 출력, 0 이하면 0 출력


3. 다차원 배열 계산

  • np.array : 파이썬의 리스트나 튜플과 같은 시퀀스 자료형을 Numpy arrays로 변환
  • np.ndim(A) : 배열의 차원수
  • A.shape : 배열 형태 확인 - 인스턴스 변수인 shape 이용
  • -> 리턴값 : (4,) : 1차원 배열임
  • np.dot() : 행렬곱 계산
  • A * B : 행렬 요소곱

2차원 배열(= 행렬)

단순 신경망 구현

: 가중치만 가진다고 가정하고 numpy 행렬 이용해서 구현해보자!


X : 입력값 행렬
-> X.shape : (2,) => 12 행렬
W : 가중치 행렬
np.array(X1의 화살표 3개 - 리스트 1개, X2의 화살표 3개 - 리스트 1개) => 2차원 행렬
-> W.shape : (2,3) => 2
3 행렬
Y : 출력값 행렬
Y = np.dot(X,W) # X x W 행렬곱


4. 3층 신경망 구현

< 가중치 변수 표기법 >

< 가중치 행렬 크기 >
: (앞층 뉴런 개수) by (다음층 뉴런 개수) 행렬

< 3층 신경망 구현 최종 코드 >

import numpy as np

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

def identify_func(x):
    return x

def init_network(): # init_network라는 함수 정의 : network 딕셔너리에 가중치&편향 값을 저장해서 초기화해주는 함수
    network = dict()  # 변수 network에 빈 딕셔너리 저장
    # 딕셔너리[키] = 값
    # network 딕셔너리의 키에 값으로 각층에 사용되는 가중치 행렬, 편향 행렬을 할당함
    network['W1'] = np.array([
        [0.1, 0.3, 0.5],
        [0.2, 0.4, 0.6]
    ])
    network['b1'] = np.array([0.1, 0.2, 0.3])
    network['W2'] = np.array([
        [0.1, 0.4],
        [0.2, 0.5],
        [0.3, 0.6]
    ])
    network['b2'] = np.array([0.1, 0.2])
    network['W3'] = np.array([
        [0.1, 0.3],
        [0.2, 0.4]
    ])
    network['b3'] = np.array([0.1, 0.2])

    return network

def forward(network, x):  # foward 함수 정의 : 진짜로 신경망 구현해주는 부분, 매개변수로 network 딕셔너리, x 행렬 입력 받음
    W1, W2, W3 = network['W1'], network['W2'], network['W3']  # 변수 W1, W2, W3에 딕셔너리 각 키의 값을 가리키게 함
    b1, b2, b3 = network['b1'], network['b2'], network['b3']

    a1 = np.dot(x, W1) + b1 # 은닉 1층 행렬곱으로 구현 : a1 = x*W1 + b1 
    z1 = sigmoid_func(a1) # 은닉 1층 활성화함수 처리
    a2 = np.dot(z1, W2) + b2  #은닉 2층 행렬곱으로 구현 : a2 = z*W2 + b2
    z2 = sigmoid_func(a2) # 은닉 2층 활성화함수 처리
    a3 = np.dot(z2, W3) + b3  # 출력층 구현
    y = identify_func(a3) # 출력층의 활성화 함수로 항등함수 사용

    return y  # 출력값 y 리턴함

network = init_network()  # 변수 network는 init_network를 이용해서 초기화해줌
x = np.array([1.0, 0.5])  # 입력변수 x 행렬 선언
y = forward(network, x) # foward 함수로 입력변수 & network로부터 출력변수 y 계산
print(y)	# 출력값 보여줌

*) forward 함수 명명; 입력 -> 출력 방향으로 신호 전달되는 것 의미함


5. 출력층 설계하기

5.1. 출력층 설계

어떤 문제를 푸느냐에 따라 출력층에서의 활성화 함수가 달라짐

기계학습 문제의 종류출력층의 활성화 함수
회귀항등함수
분류softmax 함수
  • 회귀 : 입력 데이터에서 연속적인 수치를 예측하는 문제
  • 분류 : 입력 데이터가 어느 클래스에 속하는지 판단

5.2. softmax 함수

  • 항등함수 (identity function) : 입력 그대로를 출력, 입력값 = 출력값
  • softmax 함수 : (입력 신호의 지수 함수값) / (모든 입력신호의 지수 함수값의 합)
    => 확률로 나타낼 수 있게 해줌

n : 출력층의 뉴런 개수
y_k : 그 중 k번째 출력

softmax 함수 구현

구현 시 주의점

  • 지수함수는 쉽게 큰 값이 됨 -> 큰 값끼리 나눗셈하면 결과 수치 불안정해짐 = 오버플로우 발생함

=> 오버플로우 방지 방법
: 입력신호 중 최댓값을 빼서 사용
C' : (- 최댓값)

def softmax(a):
  c = np.max(a)   # 입력 신호의 최댓값
  exp_a = np.exp(a - c) # 오버플로 방지위해 최댓값 빼줌
  sum_exp_a = np.sum(exp_a) # exp_a 배열의 요소들 총합
  y = exp_a / sum_exp_a   # 출력값 = 해당 입력 신호의 지수함수값 / 모든 입력 신호의 지수함수값의 총합

  return y  # 출력값 반환

softmax 함수의 특징

  • 출력 : 0 ~ 1 사이의 실수
  • 출력의 총합 = 1

=> 출력의 총합이 1이라는 성질로 인해서 softmax 함수의 출력을 '확률'로 해석 가능

=> 여기서 y[0]의 확률 = 1.8%, y[1] = 24.5%, y[2] = 73.7%로 해석 가능
=> 이 경우, "세 번째(인덱스 2) Class일 확률이 가장 높으니, 입력 데이터는 세 번째 클래스에 해당한다!"는 결론을 내릴 수 있다.

  • 신경망 이용한 분류에서 일반적으로 가장 출력값이 큰 뉴런에 해당하는 클래스로 인식함
  • softmax 함수 적용해도 출력이 가장 큰 뉴런의 위치는 안 변함
    ex) Softmax 함수에 다음 Array를 입력으로 주었다고 가정하자.
    [0.1 0.7 0.5]
    이 경우 Softmax 함수의 결괏값도 두 번째(인덱스 1) 요소가 가장 크게 나온다.

=> 신경망으로 분류할 때 빠른 결과처리 위해선 일반적으로 softmax 생략함

출력층 뉴런 수 정하기

  • 출력층 뉴런 수 = 분류하고 싶은 클래스의 수로 설정
    ex) 입력 이미지를 0~9 중 1개로 분류하는 문제 : 출력층 뉴런 10개로 설정
    아래 그림의 예시에선 이미지가 클래스 2로 분류됨


6. 손글씨 숫자인식

6.1. 손글씨 숫자 분류

  • 이미 학습된 매개변수 사용 - 손글씨 숫자를 분류하는 추론과정만 구현
    => 이런 추론과정 = "신경망의 순전파"라고 함

💡 기계학습 or 신경망의 문제 풀이 과정
1) 학습 : 훈련 데이터(학습 데이터) 사용해 가중치 매개변수를 학습
2) 추론 : 학습한 매개변수 사용해서 입력 데이터를 분류

6.2. MNIST 데이터셋

  • 손글씨 숫자 이미지 데이터셋, 기계학습의 실험용 데이터 (0~9까지)
  • 훈련 이미지 : 6만장 & 시험(테스트) 이미지 : 1만장

MNIST 데이터

  • 28X28 크기
  • 단일채널의 회색영상(회색조)
  • 각 픽셀의 값 : 0~255

📌 load_mnist 함수
: 읽은 MNIST 데이터를 "(훈련이미지, 훈련레이블), (테스트이미지, 테스트레이블)" 형식으로 변환
- bool 타입 인수 3가지 : normalize, flatten, one_hot_label 설정

  • normalize : 입력값을 0~1.0 사이의 값으로 정규화 할지 여부 결정
    - True : 정규화 O / False : 영상의 DN값을 유지
  • flatten : 입력 이미지를 평탄하게 = 1차원 배열로 만들지 여부 결정
    - True : 1차원으로 변환 / False : 기존 배열의 차원 유지
  • one_hot_label : 레이블(true label)를 원핫인코딩(one-hot encoding) 형태로 저장할지 여부 결정


    💡 원-핫 인코딩 (one-hot encoding)
    : [0,0,1,0,0,0,0,0,0]처럼 정답을 뜻하는 원소만 1, 나머진 모두 0인 배열을 의미함

+) 피클을 이용한 데이터 쓰기, 읽기

피클 : 프로그램 실행 중 특정 객체를 파일로 저장하는 기능

  • 텍스트 이외의 자료형(리스트 / 클래스)을 파일로 저장하기 위해 pickle 모듈 사용
  • load_mnist()함수에서도 2번쨰 이후 읽을 때부터 pickle 이용함


    특징

6.3. 신경망의 추론 처리

< MNIST 데이터셋 이용한 신경망 구현 >

  • 입력층 뉴런 : 784개 & 출력칭 뉴런 : 10개
    💡 why 입력층 뉴런 784개?
    : 영상(image) 크기가 28*28 = 784 라서
    💡 why 출력층 뉴런 10개?
    : 숫자가 0~9까지 10개라서

  • 은닉층 : 2개
    -- 1번째 은닉층 : 뉴런 50개
    -- 2번째 은닉층 : 뉴런 100개
    (뉴런 개수는 임의로 설정하는 거임)

< 코드 해석 >

import sys, os
sys.path.append(os.pardir)
import numpy as np
import pickle
from dataset.mnist import load_mnist
from common.functions import sigmoid, softmax


def get_data():
    (x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, flatten=True, one_hot_label=False)
    return x_test, t_test

def init_network():
    with open("sample_weight.pkl", 'rb') as f:
        network = pickle.load(f)
    return network

def predict(network, x):
    W1, W2, W3 = network['W1'], network['W2'], network['W3']
    b1, b2, b3 = network['b1'], network['b2'], network['b3']

    a1 = np.dot(x, W1) + b1
    z1 = sigmoid(a1)
    a2 = np.dot(z1, W2) + b2
    z2 = sigmoid(a2)
    a3 = np.dot(z2, W3) + b3
    y = softmax(a3)

    return y


x, t = get_data()
network = init_network()
accuracy_cnt = 0
for i in range(len(x)):
    y = predict(network, x[i])
    p = np.argmax(y)
    if p == t[i]:
        accuracy_cnt += 1

print("Accuracy:" + str(float(accuracy_cnt) / len(x)))

<각 구문의 역할>
1) get_data() : load_mnist() 함수를 호출해 MNIST 데이터셋(중 테스트 데이터셋)을 로드함

  • 이번엔 이미 학습된 모델의 가중치 & 편향 그대로 이용해서 신경망의 추론만 구현하는 거니까 ( 학습 필요 X -> training data 필요 X )

2) init_network() : 피클 파일인 sample_weight.pkl에 저장된 '이미 학습된 가중치 매개변수'를 읽어와서 매개변수(모델의 network)를 로딩함

  • pickle 파일엔 w, b가 딕셔너리 변수로 저장돼있음

3) predict(network, x)

  • neural network & 입력데이터(x)를 인자로 받아서 추론 처리 진행 (앞에서 신경망 구현한 코드임)
  • 출력층에서 softmax 함수 이용해서 각 레이블에 해당할 확률이 저장된 값 10개 들어있는 배열 리턴함 (출력층에서 10개 뉴런 가져서 배열에도 10개 값 들어있는 거임)

4) for문 : predict 함수로부터 출력값 얻음

  • predict함수의 리턴값인 배열 안 10개 값 중 가장 큰 원소 찾음
  • np.argmax(y) ; 배열 중 가장 큰 값 가지는 원소의 인덱스 리턴
  • if문 : 가장 큰 값의 원소 인덱스 == 데이터 레이블 t랑 같으면 accuracy_cnt 변수를 1 증가
  • 정확도 : accuracy_cnt 값 / 전체 x 개수

전처리 (pre-processing)

: 신경망의 입력 데이터에 특정 변환을 가하는 것

정규화

: 0~255 영상 DN(digital number)를 0~1의 값으로 정규화하는 과정, 전처리 중 하나임

6.4. 배치처리

1) 입력데이터 & 매개변수의 크기 확인

신경망 각층의 배열 크기 변화
(1) 영상(이미지) 1장 이용했을 때
X W1 W2 W3 => Y
(1,784) X (784, 50) X (50, 100) X (100, 10) = (1,10)

(2) 이미지 100장을 한 번에 신경망에 추론해라고 넘겼을 때
X W1 W2 W3 => Y
(100,784) X (784, 50) X (50, 100) X (100, 10) = (100,10)

=> 이렇게 되면 입력 데이터 크기 : 100x784, 출력 데이터 크기 : 100 x 10
=> 100장의 영상 동시 출력 가능

x[0], y[0] ; 각각 1번째 이미지의 flatten array / prediction 결과
...
x[84], y[84] ; 각각 85번째

배치 : #개의 입력 데이터를 1개로 묶은 덩어리

배치 처리의 장점 : 이미지 1장당 처리 시간을 대폭 줄여줌

  • 수치 계산 라이브러리 대부분이 큰 배열을 효율적으로 처리할 수 있도록 고도로 최적화되어 있기 때문
  • 큰 신경망에서는 데이터 전송이 병목으로 작용하는 경우가 자주 있는데, 배치 처리를 통해 버스에 주는 부하를 줄일 수 있음

배치 구현

for문으로 배치 사이즈만큼씩 입력데이터 range로 자르고,
아까 위의 함수에서 t[i] -> t[i:i+batch_size]로 바꾸면 됨

batch_size = 100 # 배치 사이즈
accuracy_cnt = 0

for i in range(0, len(x), batch_size): # 0 ~ len(x)까지 batch_size만큼씩 건너뛰어진 값을 내놓는 iterator 반환 
    y_batch = predict(network, x[i:i+batch_size])	# 입력 데이터 x를 배치 사이즈 만큼 덩어리로 잘라서 predict 함수에 넣음
    p = np.argmax(y_batch, axis=1)	# 100*10 행렬 중 1번째 차원을 구성하는 원소 중 최댓값의 인덱스 리턴 해서 p에 저장함
    accuracy_cnt += np.sum(p == t[i:i+batch_size])
    # ==연산자로 np.array인 p랑 t[i:i+batch_size] 끼리 비교해서 True / False로 구성된 bool 타입 배열 만들고
    # 여기서 np.sum으로 True가 몇 개인지 계산(true가 1이니까 1들의 합이 결국 true 개수임)

print("Accuracy:" + str(float(accuracy_cnt) / len(x)))
profile
안녕하세요😊 컴퓨터비전을 공부하고 있습니다 🙌

0개의 댓글