pseong

mnist 이용한 손글씨 인식 cnn 딥러닝 구현하기 본문

인공지능/딥러닝

mnist 이용한 손글씨 인식 cnn 딥러닝 구현하기

pseong 2022. 1. 25. 13:33

밑바닥부터 시작하는 딥러닝 1 책을 읽고 난 후
 
책 내용을 전부 요약해서 정리해 봅니다.
 

mnist 손글씨 파이썬 딥러닝 구현

 
 

신경망은 입력층, 은닉층, 출력층이 존재한다.
 
각 각의 층에서 다음 층으로 넘어갈 때 가중치(w)를 곱하고 편향(b)을 더해준다.
 
그리고 계산된 값을 활성화 함수에 넣어서 활성화를 시키는 것 까지 수행하면 된다.
 

y = h(xw+b)

xw+b의 작업을 Affine 레이어라고 부른다.
 
h(a)의 작업을 활성화 함수라고 부르는데 Relu 또는 Sigmoid 등이 있다. 여기서는 ReLU레이어를 사용한다.

이것을 여러 노드들에게 적용시키면 다음과 같다.

이러한 연결을 완전 연결 계층이라고 하는데 필요한 변수는 각 각의 가중치(w)들과 각 층의 편향(b)이다.
 
흐름은 이미지 입력 -> Affine -> ReLU -> Affine -> ReLu -> Affine -> Softmax이다.

마지막 Softmax는 출력된 모든 값의 e의 제곱으로 만들어 준 후 그 값을 전부 더한 값으로 나눈다.
 
실제로 구현할 때는 오버플로우를 방지하기 위해 최댓값을 빼준 코드를 사용한다.

def softmax(x):
    if x.ndim == 2:
        x = x.T
        x = x - np.max(x, axis=0)
        y = np.exp(x) / np.sum(np.exp(x), axis=0)
        return y.T 

    x = x - np.max(x)
    return np.exp(x) / np.sum(np.exp(x))

numpy를 사용하면 엄청 빠르고 편리하기 때문에 numpy를 사용하여 전부 계산한다.
 
numpy의 내적을 이용 하여 가중치 계산을 다음과 같이 빠르게 값을 도출할 수 있다.

 
아래는 입력 이미지를 100장을 사용했을 때다. (이것을 미니 배치 사이즈라고 한다. 미니 배치 사이즈를 100으로 한다면)

출력은 100x10의 행렬이 된다.
 
학습을 하기 위해 이 신경망이 얼마나 잘 학습되어있는지 점수를 매겨주는 손실 함수가 있다.
 
손실 함수 오차 제곱합과 교차 엔트로피 오차 2가지를 알아보자
 

오차 제곱합

 

오차제곱합

교차 엔트로피 오차

교차 엔트로피 오차

 
t는 정답 레이블이고 y는 출력 값이다.
 
ex) t = [0, 0, 0, 0, 1, 0, 0, 0, 0]
 
오차 제곱합은 모든 정답 레이블과 출력 값의 차이의 제곱을 더해주고
 
교차 엔트로피 오차는 정답 레이블인 값과의 오차만 확인한다.
 
이제 중요한 것은 가중치(w)와 편향(b)을 어떻게 조절해 줄지가 문제인데, 2가지 방법이 있다.
 
수치적 방법과 해석적 방법이 있다.
 
수치적 방법은 기울기를 알고 싶은 가중치만 약간 올려서 오차를 계산해주고
 
약간 내려서 오차를 계산해서 기울기가 양수인지, 음수인지, 그리고 그 크기가 얼마나 되는지 알 수 있다.
 
(f(w+h)-f(w-h)) / 2h로 계산해준다. 단점은 엄청나게 느리다.
 
해석적 방법은 오차 역전파법을 이용하여 출력 값을 반대로 넘기면서 계산해주는 방식이다.
 
앞에서 넘어온 값을 현재 계산에서 미분해준 값을 곱해주면 기울기가 나온다.
 
또 이 기울기를 다시 이전으로 넘겨주고 반복하면 모든 변수에서의 기울기를 구할 수 있다.

오차 역전파법 연쇄법칙

덧셈은 미분 값이 1이므로 역전파 할 때 1을 곱해준다. 따라서 앞에서 넘어온 값 그대로 넘겨준다.
 
곱셉은 편미분에 대해 값을 생각하면 된다.
 
이제 기본적인 덧셈, 곱셈 역전파에 대해 알아봤으니 실제 레이어에 대해 적용해보자.
 

ReLu 계층

ReLu 계층은 0보다 크면 그대로 출력하고 0보다 작으면 0으로 출력하므로 미분 값은 다음과 같다.

따라서 역전파 할 때는 0보다 작은 값은 0으로 바꿔주기만 하면 된다.
 
앞에서 넘어온 값에 1을 곱하면 그 값이 그대로 나오기 때문이다.
 

Sigmoid  계층

Sigmoid 함수
Sigmoid 역전파

1/x를 미분하면 -1/x^2이고 x가 1+exp(-x)이므로 -y^2이다.
 
x+1을 미분하면 1이다. -y^2 X 1 = -y^2
 
exp(x) 미분하면 exp(x)이다. -y^2 X exp(-x) = -exp(-x) y^2
 
x * (-1) 미분하면 -1이다. -exp(-x) y^2 X (-1) = exp(-x) y^2
 
따라서 답은 exp(-x) y^2에다가 앞에서 넘어온 L에 대한 y변화량을 곱하면 된다.
 
이 식을 정리하면 다음과 같다.

 

Affine 계층

X가 입력 행렬이고 W가 가중치 행렬 그리고 Y가 출력 행렬이다.
 
이것도 중간 과정이 많이 생략되었지만
 
결과만 보자면 X에 대한 편미분을 알고 싶다면 앞에서 넘어온 값과 가중치 행렬의 전치 행렬을 내적 해주면 되고,
 
W에 대한 편미분을 알고 싶다면 앞에서 넘어온 값과 입력 행렬의 전치 행렬을 내 해주면 된다.
 
편향(b)에 대한 편미분은 덧셈이므로 1이다.
 
배치 사이즈가 N일 때 Affine 계층의 역전 파는 다음과 같다.

Softmax 계층

위에 식을 하나씩 앞에서부터 따라가면 잘 이해가 된다.
 
결과만 보자면 y1-t1 이 기울기다.
 
y는 softmax만 통과한 출력 값이고 t는 정답 레이블이다.
 
결국 정답 레이블과 출력 값과 에 오차가 바로 미분 값이 된다.
 

더보기
더보기
class Relu:
    def __init__(self):
        self.mask = None

    def forward(self, x):
        self.mask = (x <= 0)
        out = x.copy()
        out[self.mask] = 0

        return out

    def backward(self, dout):
        dout[self.mask] = 0
        dx = dout

        return dx


class Sigmoid:
    def __init__(self):
        self.out = None

    def forward(self, x):
        out = sigmoid(x)
        self.out = out
        return out

    def backward(self, dout):
        dx = dout * (1.0 - self.out) * self.out

        return dx


class Affine:
    def __init__(self, W, b):
        self.W = W
        self.b = b
        
        self.x = None
        self.original_x_shape = None
        # 가중치와 편향 매개변수의 미분
        self.dW = None
        self.db = None

    def forward(self, x):
        # 텐서 대응
        self.original_x_shape = x.shape
        x = x.reshape(x.shape[0], -1)
        self.x = x

        out = np.dot(self.x, self.W) + self.b

        return out

    def backward(self, dout):
        dx = np.dot(dout, self.W.T)
        self.dW = np.dot(self.x.T, dout)
        self.db = np.sum(dout, axis=0)
        
        dx = dx.reshape(*self.original_x_shape)  # 입력 데이터 모양 변경(텐서 대응)
        return dx


class SoftmaxWithLoss:
    def __init__(self):
        self.loss = None # 손실함수
        self.y = None    # softmax의 출력
        self.t = None    # 정답 레이블(원-핫 인코딩 형태)
        
    def forward(self, x, t):
        self.t = t
        self.y = softmax(x)
        self.loss = cross_entropy_error(self.y, self.t)
        
        return self.loss

    def backward(self, dout=1):
        batch_size = self.t.shape[0]
        if self.t.size == self.y.size: # 정답 레이블이 원-핫 인코딩 형태일 때
            dx = (self.y - self.t) / batch_size
        else:
            dx = self.y.copy()
            dx[np.arange(batch_size), self.t] -= 1
            dx = dx / batch_size
        
        return dx

 
기울기를 구했다면 각 각의 가중치와 편향에 대해 기울기가 낮아지는 쪽으로 값을 조정해 줘야 한다.
 

1. 확률적 경사 하강법(SGD)

확률적 경사 하강법은 가중치(w)에 대한 손실 함수의 기울기에다가 학습률 n을 곱한 값을 빼주는 방법이다.
 
학습률 n는 0.01이나 0.001과 같이 미리 자신이 정해야 한다. (learning rate라고 부른다.)

SGD의 움직임은 기울기가 낮은 쪽으로 기울기 크기에 learning rate를 곱해서 무조건 움직이기 때문에
 
움직임이 굉장히 비효율 적이다.
 

2. 모멘텀

 

모멘텀은 가중치에 속도를 학습할 때마다 더한다.
 
그리고 그 속도를 학습할 때마다 SGD랑 똑같은 값을 계속 더해가는 방식이다.
 
잘 보면 기울기가 0 이어도 v에다가 알파를 곱하는데, 알파는 기본적으로 0.9를 설정해 준다.
 
이 의미는 공기 저항이나 마찰력을 표현한 것으로 기울기가 없더라도 매 학습마다 속도는 감소한다.
 

3. AdaGrad

 
이 방법은 그 가중치에 가해진 기울기를 계속 누적시켜서 학습률을 감소시키는 방법이다.
 
각 가중치의 기울기는 가중치마다 다르므로 이 방법은 각 각의 매개변수에 딱 알맞은 방법이다.
 
 

식을 잘 보면 h 에다가 매 학습 시 기울기 제곱을 계속 더해준다.
 
그리고 매 학습 시마다 가중치에 1/sqrt(h)를 학습률*기울기에 곱해준 값을 빼준다.
 

 

4. Adam

 
모멘텀과 AdaGrad와 합쳐진 게 Adam이다.

 
가중치 초깃값
 
가중치의 초깃값을 설정하는 방법도 여러 가지가 있다.
 
1. Xavier 초깃값 : 표준편차가 sqrt(1/n)인 정규 분포 사용 (n은 앞 층의 노드 수)
 
np.random.randn * sqrt(1.0/n)를 사용하면
 
표준편차가 1/sqrt(n)인 정규 분포를 따르는 랜덤 함수를 사용할 수 있다.
 
2. He 초깃값 : 표준편차가 sqrt(2/n)인 정규 분포 사용 (n은 앞 층의 노드 수)
 
ReLU를 사용할 때에는 초깃값을 He 초깃값으로 사용해야 하고
 
Sigmoid를 사용할 때에는 Xavier 초깃값을 사용해야 한다.
 
 

배치 정규화

 
배치 정규화를 사용하면 학습 속도를 상승시키고 오버 피팅 억제 초기값 의존 감소의 효과를 볼 수 있다.
 
 
 

첫째줄은 미니 배치 모든 입력 데이터에 대해 평균값을 구한다.
 
둘째 줄을 미니 배치 모든 입력 데이터에 대해 분산을 구한다.
 
셋째 줄은 입력 데이터를 평균이 0, 분산이 1이 되게 정규화한다.
 
그리고 이 정규화된 데이터에 고유한 확대와 이동 변환을 다음과 같이 수행하면 끝이다.

 
배치 정규화의 역전 파는 다음과 같다.

 
 
오버 피팅 억제 방법
 

1. 가중치 감소

 
모든 가중치 각 각의 손실 함수에 (감마 x가중치^2)/2를 더한다.
 
따라서 기울기를 구할 때 감마 x가중치를 더해준다.
 

2. 드롭아웃

 
랜덤으로 x프로의 입력 노드를 뽑고 나머지는 불활성화(0으로 바꿈) 시키는 방법이다.
 
훈련 시에는 삭제할 뉴런을 무작위로 선택하고
 
시험 때는 모든 뉴런에 신호를 보내는데 삭제 안 한 비율을 곱해서 보낸다.
 
 

합성곱 신경망 ( CNN )

 
CNN 네트워크는 새로운 합성곱 계층과 풀링 계층이 존재한다.
 

 
합성곱 연산의 간단한 예시

 
필터가 돌아다니면서 모든 값을 곱하고 더해서 값을 도출한다.
 
패딩 : 입력 데이터 주위에 0을 채운다.
 
스트라이드 : 필터를 적용하는 위치의 간격
 
채널 : 입력 데이터의 3차원 값
 
 
입력 데이터와 필터의 채널은 같아야 한다!
 

합성곱 계층

 
아래 그림은 채널이 3인 입력 데이터 1개가 필터를 거쳐서 출력 데이터로 변환되는 과정이다.
 

 
출력 데이터는 무조건 채널이 1인데 출력 데이터의 채널을 늘리고 싶다면
 
필터를 여러 개 그러니까 필터를 4차원 구조로 만들면 된다.

 
 
여기까지 입력 데이터 1개에 대한 처리였고
 
입력 데이터를 여러 개 하고 ( 미니 배치 사이즈를 늘리고 ) 싶다면 입력 데이터를 4차원으로 만들면 된다.
 
그러면 출력 데이터도 4차원으로 출력되게 된다.

그림을 잘 보면 편향이 존재하는데, 합성곱 계층에서도 편향이 존재한다.
 
풀링 계층
 
풀링은 세로 가로 방향의 공간을 줄이는 연산이다.

보통 풀링 계층은 필터 사이즈와 스트라이드 사이즈를 같게 해서 각  영역의 최댓값을 구한다.
 
풀링 계층의 특징은 입력 데이터가 조금 변해도 결과는 잘 변하지 않다는 특징이 있다.
 
 

합성곱 계층 계산 방법

 
im2 col로 데이터를 전개해서 numpy의 내적으로 한 번에 계산하는 방식을 사용하면 엄청 빠르다.

그림과 같이 입력 데이터와 필터 데이터를 2차원으로 펴서 출력한 다음
 
numpy의 reshape함수를 사용하여 출력 데이터를 복원한다.
 
내적을 사용해야 하기 때문에 입력 데이터는 가로로 펴주고 필터는 세로로 펴줘야 한다.
 
입력 데이터를 펴 줄 때에는 스트라이드를 고려해서 펴줘야 한다.
 
스트라이드가 작을 때에는 겹쳐지는 부분이 많기 때문에 입력 데이터보다 펴준 데이터가 훨씬 클 수가 있다.
 
하지만 numpy의 내이 빠르기 때문에 메모리를 소비하더라도 꼭 이렇게 사용해야 빠르다.
 
펼쳐진 출력 데이터를 다시 뭉쳐줄 때 단순히 reshape만 사용하면 안 된다.
 
이유는 행렬 내적을 사용하고 난 후 형상의 순서가 조금 바뀌기 때문이다.

out = out.reshape(N, out_h, out_w, -1).transpose(0, 3, 1, 2)

이렇게 transpose로 기존의 순서 (데이터 개수, 채널, 높이, 가로)를 맞춰서 출력해준다.
 
 

풀링 계층 계산 방법

 
풀링 계층도 입력 데이터를 풀어서 2차원으로 만든 다음 다시 합쳐주면 된다.
 
풀링 계층은 내적이 아니라 최댓값만 구함면 되기 때문에 다음 그림과 같이 수행해 주면 된다.

그림의 설명은 전부 입력 데이터가 1개인 3차원 입력 데이터지만
 
입력 데이터가 여러 개인 4차원 데이터도 동일하게 적용해주면 코드는 다음과 같다.

        arg_max = np.argmax(col, axis=1)
        out = np.max(col, axis=1)
        out = out.reshape(N, out_h, out_w, C).transpose(0, 3, 1, 2)

 
합성곱과 풀링의 역전파 미분 기울기는 다음과 같다.
 
합성곱

    def backward(self, dout):
        FN, C, FH, FW = self.W.shape
        dout = dout.transpose(0,2,3,1).reshape(-1, FN)

        self.db = np.sum(dout, axis=0)
        self.dW = np.dot(self.col.T, dout)
        self.dW = self.dW.transpose(1, 0).reshape(FN, C, FH, FW)

        dcol = np.dot(dout, self.col_W.T)
        dx = col2im(dcol, self.x.shape, FH, FW, self.stride, self.pad)

        return dx

 
풀링

    def backward(self, dout):
        dout = dout.transpose(0, 2, 3, 1)
        
        pool_size = self.pool_h * self.pool_w
        dmax = np.zeros((dout.size, pool_size))
        dmax[np.arange(self.arg_max.size), self.arg_max.flatten()] = dout.flatten()
        dmax = dmax.reshape(dout.shape + (pool_size,)) 
        
        dcol = dmax.reshape(dmax.shape[0] * dmax.shape[1] * dmax.shape[2], -1)
        dx = col2im(dcol, self.x.shape, self.pool_h, self.pool_w, self.stride, self.pad)
        
        return dx

 

Comments