밑바닥부터 시작하는 딥러닝 - 2 (신경망)

앞에서 퍼셉트론을 배울 때 좋은 소식과 나쁜 소식이 있었다. 좋은 소식은 퍼셉트론으로도 복잡한 함수를 표현할 수 있다는 것이다. 나쁜 소식은 가중치를 설정하는 작업은 여전히 사람이 수동으로 한다는 것이다.
신경망은 이 나쁜 소식을 해결해준다. 신경망은 가중치 매개변수의 적절한 값을 데이터로부터 자동으로 학습하는 능력을 갖추고 있다.
퍼셉트론에서 신경망으로
신경망은 앞 장에서 설명한 퍼셉트론과 공통점이 많다.
신경망의 예
신경망을 그림으로 나타내면 아래와 같다.

가장 왼쪽 줄을 입력층, 맨 오른쪽 줄을 출력층, 중간 줄을 은닉층이라고 한다. 은닉충의 뉴런은 입력층이나 출력층과 달리 사람 눈에는 보이지 않는다. 그래서 은닉이다.
위 그림은 앞에서 본 퍼셉트론과 특별히 달라 보이지 않는다. 실제로 뉴런이 연결되는 방식은 퍼셉트론과 달라진 것이 없다. 그럼 신경망에서는 신호를 어떻게 전달할까?
퍼셉트론 복습
신경망의 신호 전달 방법을 보기 전에 퍼셉트론을 복습해보자.

위 그림은 x1과 x2라는 두 신호를 입력받아 y를 출력하는 퍼셉트론이다. 이 퍼셉트론을 수식으로 나타내면 아래 식이 된다.

여기서 b는 편향을 나타내는 매개변수로, 뉴런이 얼마나 쉽게 활성화 되느냐를 제어한다. 한편 w1과 w2는 각 신호의 가중치를 나타내는 매개변수로, 각 신호의 영향력을 제어한다.
위 퍼셉트론 그림에서는 편향 b가 보이지 않는다. 여기에 편향을 명시한다면 아래와 같이 나타낼 수 있다.

위 그림에서는 가중치가 b이고 입력이 1인 뉴런이 추가되었다. 이 퍼셉트론의 동작은 x1, x2, 1이라는 3개의 신호가 뉴런에 입력되어, 각 신호에 가중치를 곱한 후 다음 뉴런에 전달된다. 다음 뉴런에서는 이 신호들의 값을 더하여, 그 합이 0을 넘으면 1을 출력하고 그렇지 않으면 0을 출력한다. 참고로 편향의 입력 신호는 항상 1이다.
그럼 아까의 식을 더 간결한 형태로 다시 작성해보자. 이를 위해서 조건 분기의 동작을 하나의 함수로 나타낸다. 이 함수를 h(X)라 하면 아래와 같이 표현할 수 있다.

위 식은 입력 신호의 총합이 h(x)라는 함수를 거쳐 변환되어, 그 변환된 값이 y의 출력이 됨을 보여준다. 그리고 식의 h(x) 함수는 입력이 0을 넘으면 1을 돌려주고 그렇지 않으면 0을 돌려준다.
활성화 함수의 등장
조금 전 h(x)라는 함수가 등장했는데, 이처럼 입력 신호의 총합을 출력 신호로 변환하는 함수를 활성화 함수라 한다. 활성화라는 이름이 말해주듯 활성화 함수는 입력 신호의 총합이 활성화를 일으키는지를 정하는 역할을 한다.
위 식에서는 가중치가 곱해진 입력 신호의 총합을 계산하고, 그 합을 활성화 함수에 입력해 결과를 내는 2단계로 처리되었었다. 그래서 이 식은 아래와 같이 2개로 나눌 수 있다.

뉴런을 큰 원으로 그려보면 식은 아래 그림처럼 나타낼 수 있다.

보다시피 위 그림에서는 기존 뉴런의 원을 키우고, 그 안에 활성화 함수의 처리 과정을 명시적으로 그려 넣었다. 즉 가중치 신호를 조합한 결과가 a라는 노드가 되고, 활성화 함수 h()를 통과하여 y라는 노드로 변환되는 과정이 분명하게 나타나 있다.
활성화 함수
활성화 함수는 임곗값을 경계로 출력이 바뀌는데 이런 함수를 계단 함수라 한다. 그래서 퍼셉트론에서는 활성화 함수로 계단 함수를 이용한다고 할 수 있다. 즉, 활성화 함수로 쓸 수 있는 여러 후보 중에서 퍼셉트론은 계단 함수를 채용하고 있다. 그렇다면 계단 함수 이외의 함수를 사용하면 어떻게 될까?
시그모이드 함수

위는 신경망에서 자주 이용하는 활성화 함수인 시그모이드 함수의 식이다. exp(-x)는 e^-x를 뜻하며, e는 자연상수로 2.7182...의 값을 갖는 실수이다. 위 식으로 표현되는 시그모이드 함수는 얼핏 복잡해보이지만 이 역시 단순한 함수일 뿐이다. 함수는 입력을 주면 출력을 돌려주는 변환기이다. 예를 들어 시그모이드 함수에 1.0과 2.0을 입력하면 h(1.0) = 0.731... H(2.0) = 0.880.. 처럼 특정 값을 출력한다. 신경망에서는 활성화 함수로 시그모이드 함수를 이용하여 신호를 변환하고, 그 변환된 신호를 다음 뉴런에 전달한다. 퍼셉트론과 신경망의 주된 차이는 이 활성화 함수 뿐이다.
계단 함수 구현하기
계단 함수는 입력이 0을 넘으면 1을 출력하고 그 외에는 0을 출력하는 함수이다. 아래는 이러한 계단 함수를 단순하게 구현한 코드이다.

이 구현은 단순하고 쉽지만, 인수 x는 실수만 받아들인다. 즉 stepFunction(3.0)은 되지만 넘파이 배열을 인수로 넣을 수는 없다. 만약 넘파이 배열도 지원하도록 수정하고 싶으면 아래와 같은 구현을 생각할 수 있다.

두 줄뿐이라 엉성해보이겠지만, 이는 넘파이의 편리한 트릭을 사용한 덕분이다.

넘파이 배열에 부동호 연산을 수행하면 배열의 원소 각각에 부등호 연산을 수행한 bool 배열이 생성된다. y는 bool 배열이다. 그런데 우리가 원하는 계단 함수는 0이나 1의 int형을 출력하는 함수이므로 y의 원소를 astype을 사용해서 int형으로 바꿔줄 수 있다.
이처럼 넘파이 배열의 자료형을 변환할 때는 astype() 메서드를 이용한다. 파이썬에서 bool을 int로 변환하면 True는 1로 False는 0으로 변환된다.
계단 함수의 그래프
계단 함수를 그래프로 그려보자.

np.arange(-5.0, 5.0, 0.1)은 -5.0에서 5.0까지 0.1 간격으로 넘파이 배열을 생성한다. stepFunction()은 인수로 받은 넘파이 배열의 원소 각각을 인수로 받아 계단 함수를 실행하고, 그 결과를 다시 배열로 만들어 돌려준다. 이 x, y 배열을 그래프로 그리면 아래와 같이 된다.

위 그림에서 보듯 계단 함수는 0을 경계로 출력이 0에서 1로 바뀐다.
시그모이드 함수 구현하기
시그모이드 함수는 아래와 같이 작성할 수 있다.

여기서 np.exp(-x)는 exp(-x) 수식에 해당한다.
이 함수가 넘파이 배열도 훌륭하게 처리할 수 있는 비밀은 브로드캐스트에 있다. 브로드캐스트란 넘파이 배열과 스칼라값의 연산을 넘파이 배열의 원소 각각과 스칼라값의 연산으로 바꿔 수행하는 기능이다.
그럼 시그모이드 함수를 그래프로 그려보자. 그래프를 그리는 코드 자체는 계단 함수와 비슷하다.

이 코드를 실행하면 아래와 같은 그래프를 확인할 수 있다.

시그모이드 함수와 계단 함수 비교
시그모이드 함수와 계단 함수를 비교해보자.

위 그림에 두 함수를 같이 그렸다. 가장 먼저 느껴지는 차이는 매끄러움의 차이이다. 시그모이드 함수는 부드러운 곡선이며 입력에 따라 출력이 연속적으로 변화한다. 한편, 계단 함수는 0을 경계로 출력이 갑자기 바뀌어버린다. 시그모이드 함수의 이 매끈함이 신경망 학습에서 아주 중요한 역할을 하게 된다.
계단 함수는 0과 1 중 하나의 값만 돌려주는 반면, 시그모이드 함수는 실수를 돌려준다는 점도 다르다. 다시 말해, 퍼셉트론에서는 뉴런 사이에 0과 1이 흐르지만 신경망에서는 연속적인 실수가 흐른다.
두 함수의 공통점을 살펴보면, 큰 관점에서는 둘이 비슷한 모양이다. 둘 다 입력이 작을 때의 출력은 0에 가깝고, 입력이 커지면 출력이 1에 가까워지는 구조이다. 즉 계단 함수와 시그모이드 함수는 입력이 중요하면 큰 값을 출력하고, 입력이 중요하지 않으면 작은 값을 출력한다. 그리고 입력이 아무리 작거나 커도 출력은 0에서 1사이라는 점도 둘의 공통점이다.
비선형 함수
계단 함수와 시그모이드 함수의 공통점은 그 밖에도 있다. 중요한 공통점으로는 둘 모두 비선형 함수라는 것이다. 시그모이드 함수는 곡선, 계단 함수는 구부러진 직선으로 나타나며, 동시에 비선형 함수로 분류된다.
신경망에서는 활성화 함수로 비선형 함수를 사용해야 한다. 달리 말하면 선형 함수를 사용해서는 안된다. 선형 함수를 이용하면 신경망의 층을 깊게 하는 의미가 없어지기 때문이다.
선형 함수의 문제는 층을 아무리 깊게 해도 은닉층이 없는 네트워크로도 똑같은 기능을 할 수 있다는 데 있다. 선형 함수인 h(x) = cx를 활성화 함수로 사용한 3층 네트워크를 떠올려보자. 이를 식으로 나타내면 y(x) = h(h(h(x)))가 된다. 이 계산은 곱셈을 세번 수행하지만 실은 y(x) = ax와 똑같인 식이다. a를 c^3이라고만 하면 끝이다. 즉, 은닉층이 없는 네트워크로 표현할 수 있다. 이 예처럼, 선형 함수를 이용해서는 여러 층으로 구성하는 이점을 살릴 수 없다.
ReLU 함수
지금까지 활성화 함수로서 계단 함수와 시그모이드 함수를 소개했다. 시그모이드 함수는 신경망 분야에서 오래전부터 이용해왔으나, 최근에는 ReLU 함수를 주로 이용한다.
ReLU는 입력이 0을 넘으면 그 입력을 그대로 출력하고, 0 이하면 0을 출력하는 함수이다.

식으로는 아래처럼 쓸 수 있다.


코드 구현도 위와 같이 가능하다. 여기서 넘파이의 maximum 함수를 사용했다. maximum은 두 입력 중 큰 값을 선택해 반환하는 함수이다.
다차원 배열의 계산
넘파이의 다차원 배열을 사용한 계산법을 숙달하면 신경망을 효율적으로 구현할 수 있다.
다차원 배열
다차원 배열도 그 기본은 숫자의 집합이다. 숫자가 한줄로 늘어선 것이나 직삭각형으로 늘어놓은 것, 3차원으로 늘어놓은 것이나 N차원으로 나열하는 것을 통틀어 다차원 배열이라고 한다. 넘파이를 사용해 다차원 배열을 착성해보자. 먼저 지금까지 보아온 1차원 배열을 작성해보자.

이와 같이 배열의 차수는 np.ndim() 함수로 확인할 수 있다. 또 배열의 형상은 shape으로 알 수 있다. A.shape은 튜플을 반환한다. 이는 1차원 배열이라도 다차원 배열일 때와 통일된 형태로 결과를 반환하기 위함이다. 이제 2차원 배열을 작성해보자.

여기에서는 3x2 배열인 B를 작성했다. 3x2 배열은 처음 차원에는 원소가 3개, 다음 차원에는 원소가 2개 있다는 의미이다. 이때 처음 차원은 0번째 차원, 다음 차원은 1번째 차원에 대응한다. 2차원 배열은 특히 행열이라고 부르고 아래 그림과 같이 배열의 가로 방향을 행 세로 방향을 열이라 한다.

행렬의 곱
2x2 행렬의 곱은 아래 그림처럼 계산한다.

그림에서처럼 행렬 곱은 왼쪽 행렬의 행과 오른쪽 행렬의 열을 원소별로 곱하고 그 값들을 더해서 계산한다. 그리고 그 계산 결과가 새로운 다차원 배열의 원소가 된다.

이 코드에서 A와 B는 2x2 행렬이며, 두 행렬의 곱은 넘파이 함수 np.dot()로 계산한다. np.dot()는 입력이 1차원 배열이면 벡터를 2차원 배열이면 행렬 곱을 계산한다. 여기서 한가지 주의할 점은 np.dot(A, B)와 np.dot(B, A)는 다른 값이 될 수 있다는 점이다.
앞에서는 2x2 행렬을 곱하는 예를 들었지만, 형상이 다른 행렬의 곱도 마찬가지 방법으로 계산할 수 있다.

2x3 행렬 A와 3x2 행렬 B의 곱은 이와 같이 구현할 수 있다. 이때 행렬의 shape에 주의해야 한다. 구체적으로 말하면 행렬 A의 1번째 차원의 원소 수와 행렬 B의 0번째 차원의 원소 수가 같아야 한다.
그렇기 때문에 2x3 행렬과 3x2 행렬이 아닌 2x3 행렬과 2x2 행렬을 곱하면 오류가 나타난다.
이 오류는 행렬 A의 1번째 차원가 행렬 C의 0번째 차원의 원소 수가 다르다고 말한다. 즉, 다차원 배열을 곱하려면 두 행렬의 대응하는 차원의 원소 수를 일치시켜야 한다.

3x2 행렬과 2x4 행렬을 곱해 3x4 행렬을 만드는 예이다. 이 그림과 같이 행렬의 대응하는 차원의 원소 수가 같아야 한다.

A가 2차원 행렬이고 B가 1차원 배열일 때도 대응하는 차원의 원소 수를 일치시킨다는 원칙이 똑같이 적용된다.
신경망에서의 행렬 곱
그럼 넘파이 행렬을 써서 신경망을 구현해보자.

위 그림의 간단한 신경망을 가정해보자. 이 신경망은 편향과 활성화 함수를 생략하고 가중치만 갖는다.
이 구현에서도 X, W, Y의 shape을 주의해서 보아야 한다. 특히 X와 W의 대응하는 차원의 원소 수 W의 대응하는 차원의 원소 수가 같아야 한다는 걸 잊지 말아야 한다.

다차원 배열의 스칼라곱을 구해주는 np.dot 함수를 사용하면 이처럼 단번에 결과 Y를 계산할 수 있다. Y의 원소가 100개든, 1000개든 한번의 연산으로 계산할 수 있다.
3층 신경망 구현하기

이번에는 위 그림의 3층 신경망에서 수행되는 입력부터 출력까지의 처리를 구현하겠다.
각 층의 신호 전달 구현하기

a1(1)을 수식으로 나타내보자. a1(1)은 가중치를 곱한 신호 2개와 편향을 합해서 아래와 같이 계산한다.

여기에서 행렬의 곱을 이용하면 1층의 가중치 부분을 아래 식처럼 간소화 할 수 있다.

이때 각 A, X, B, W는 아래와 같다.


이 계산은 앞에서 했던 행렬곱과 같다. W1은 2x3행렬 X는 원소가 2개인 1차원 배열이고 여기에서도 역시 대응하는 차원의 원소 수가 일치한다.
이어서 1층의 활성화 함수에서의 처리를 살펴보자. 이 활성화 함수의 처리를 그림으로 나타내면 아래처럼 된다.

위 그림과 같이 은닉층에서의 가중치 합을 a로 표기하고 활성화 함수 h()로 변환된 신호를 z로 표기한다. 여기에서는 활성화 함수로 시그모이드 함수를 사용하기로 한다. 이를 파이썬으로 구현하면 다음과 같다.

이 sigmoid 함수는 앞에서 정의한 함수이다. 이 함수는 넘파이 배열을 받아 같은 수의 원소로 구성된 넘파이 배열을 반환한다.
이어서 1층에서 2층으로 가는 과정을 살펴보자.


이 구현은 1층의 출력 Z1이 2층의 입력이 된다는 점을 제외하면 조금 전의 구현과 똑같다.
마지막으로 2층에서 출력층으로의 신호전달이다. 이 구현도 거의 같지만 활성화 함수만 지금까지의 은닉층과 다르다.


여기에서는 항등 함수인 identityFunction을 정의하고 이를 출력층의 활성화 함수로 이용했다. 항등함수는 입력을 그대로 출력하는 함수이다. 따라서 굳이 정의할 필요는 없지만, 그동안의 흐름과 통일하기 위해 이렇게 구현했다.
구현 정리
지금까지의 구현을 정리해보자. 신경망 구현의 관례의 따라 가중치만 W1과 같이 대문자로 쓰고 그 외 편향과 중간 결과 등은 모두 소문자로 쓰자.

여기에서는 init_network()와 forward()라는 함수를 정의했다. init_network() 함수는 가중치와 편향을 초기화 하고 이들을 딕셔너리 변수인 network에 저장한다. 이 딕셔너리 변수 network에는 각 층에 필요한 매개변수를 저장한다. 그리고 forward() 함수는 입력 신호를 출력으로 변환하는 처리 과정을 모두 구현하고 있다.
함수 이름을 forward라 한 이유는 신호가 순방향으로 전달되기 때문이다. 역방향인 backward도 존재한다.
출력층 설계하기
신경망은 분류와 회귀 모두에 이용할 수 있다. 다만 둘 중 어떤 문제냐에 따라 출력층에서 사용하는 활성화 함수가 달라진다. 일반적으로 회귀에는 항등 함수를, 분류에는 소프트맥스 함수를 사용한다.
항등함수와 softmax 구현하기
항등함수는 입력을 그대로 출력한다. 입력과 출력이 항상 같다는 뜻의 항등이다. 그래서 출력층에서 항등 함수를 사용하면 입력 신호가 그대로 출력 신호가 된다. 항등 함수의 처리를 신경망 그림으로 나타내면 아래처럼 된다.

반면 분류에서 사용하는 소프트맥스 함수의 식은 다음과 같다.

exp(x)는 e^x를 뜻하는 지수함수 이다. n은 출력층의 뉴런 수, Yk는 그중 k번째 출력임을 뜻한다. 위 식과 같이 소프트맥스 함수의 분자는 입력 신호 ak의 지수 함수, 분모는 모든 입력 신호의 지수 함수의 합으로 구성된다.
이 소프트맥스 함수를 그림으로 나타내면 아래처럼 된다.

그림과 같이 소프트맥스의 출력은 모든 입력 신호로부터 화살표를 받는다. 식의 분모에서 보듯, 출력층이 각 뉴런이 모든 입력 신호에서 영향을 받기 때문이다.
그럼 소프트맥스 함수를 구현해보자.

이제 이 논리 흐름을 함수로 정의하여 앞으로 필요할 때 사용할 수 있도록 해놓자.

소프트맥스 함수 구현 시 주의점
앞에서 구현한 softmax() 함수의 코드는 식을 제대로 표현하고 있지만, 컴퓨터로 계산할 때는 결함이 있다. 바로 오버플로 문제이다. 소프트맥스 함수는 지수 함수를 사용하는데, 지수 함수란 것이 쉽게 아주 큰 ㄱ밧을 내뱉는다. 가령 e^10은 20000이 넘고 e^100은 무한대를 뜻하는 inf가 되어 돌아온다. 그리고 이런 큰 값끼리 나눗셈을 하면 결과값이 불안정해진다.
이 문제를 해결하도록 소프트맥스 함수 구현을 개선해보자.

위 식의 전개 과정을 살펴보자. 첫 번째 변형에서는 C라는 임의의 정수를 분자와 분모 양쪽에 곱했다. 그 다음으로는 C를 지수 함수 exp() 안으로 옮겨 logC로 만든다. 마지막으로 logC를 C'라는 새로운 기호로 바꾼다.
위 식이 의미하는 바는 소프트맥스의 지수 함수를 계산할 때 어떤 정수를 더해도 결과가 바뀌지 않는다는 것이다. 여기서 C'에 어떤 값을 대입해도 상관없지만, 오버플로를 막을 목적으로는 입력 신호 중 최댓값을 이용하는 것이 일반적이다.

이 예에서 보는 것처럼 아무런 조치 없이 그냥 계산하면 nan이 출력된다. 하지만 입력 신호 중 최댓값을 빼주면 올바르게 계산할 수 있다. 이를 바탕으로 소프트맥스 함수를 다시 구현하면 다음과 같다.

소프트맥스 함수의 특징
softmax()함수를 사용하면 신경망의 출력은 다음과 같이 계산할 수 있다.

소프트맥스 함수의 출력은 0에서 1.0 사이의 실수이다. 또 소프트맥스 함수 출력의 총합은 1이다. 출력 총합이 1이 된다는 점은 소프트맥스 함수의 중요한 성질이다.
이 성질 덕분에 소프트맥스 함수의 출력을 확률로 해석할 수 있다.
가령 앞의 예에서 y[0]의 확률은 0.018, y[1]의 확률은 0.245, y[2]의 확률은 0.737로 해석할 수 있다. 그리고 이 결과 확률들로부터 2번째 원소의 확률이 가장 높으니, 답은 2번째 클래스다 라고 할 수 있다. 혹은 74%의 확률로 2번째 클래스 25%의 확률로 1번째 클래스, 1%의 학률로 0번째 클래스다와 같이 확률적인 결론도 낼 수 있다. 즉, 소프트맥스 함수를 이용함으로써 문제를 확률적으로 대응할 수 있게 된다.
여기서 주의할 점은 소프트맥스 함수를 적용해도 각 원소의 대소 관계는 변하지 않는다는 사실이다. 이는 지수 함수가 단조 증가 함수이기 때문이다. 실제로 앞의 예에서는 a의 원소들 사이의 대소 관계가 y의 원소들 사이의 대소 관계 그대로 이어진다.
신경망을 이용한 분류에서는 일반적으로 가장 큰 출력을 내는 뉴런에 해당하는 클래스로만 인식한다. 그리고 소프트맥스 함수를 적용해도 출력이 가장 큰 뉴런의 위치는 달라지지 않는다. 결과적으로 신경망으로 분류할 때는 소프트맥스 함수를 생략해도 된다.
출력층의 뉴련 수 정하기
출력층의 뉴런 수는 풀려는 문제에 맞게 적절히 정해야 한다. 분류에서는 분류하고 싶은 클래스 수로 설정하는 것이 일반적이다. 예를 들어 입력 이미지를 숫자 0부터 9중 하나로 분류하는 문제라면 출력층의 뉴런을 10개로 설정한다.
손글씨 숫자 인식
신경망의 구조를 배웠으니 실전 예에 적용해보자. 바로 손글씨 숫자 분류이다. 여기서는 추론 과정만 구현할 것인데 이 추론 과정을 신경망의 순전파라고도 한다.
MNIST 데이터셋
이번 예에서 사용하는 데이터셋은 MNIST라는 손글씨 숫자 이미지 집합이다. MNIST 데이터셋은 0부터 9까지 숫자 이미지로 구성된다.

MNIST의 이미지 데이터는 28x28 크기의 회색조 이미지(1채널)이며, 각 픽셀은 0에서 255까지의 값을 취한다.
신경망의 추론 처리
이 신경망은 입력층 뉴런을 784개 출력층 뉴런을 10개로 구성한다. 입력층 뉴런이 784인 이유는 이미지 크기가 28x28=784 이기 때문이고, 출력층 뉴런이 10개인 이유는 이 문제가 0에서 9까지 숫자를 구분하는 문제이기 때문이다. 한편 은닉층은 총 2개이며 첫 번째 은닉층에는 50개의 뉴런을 두 번째 은닉층에는 100개의 뉴런을 배치할 것이다.
이제 순서대로 작업을 처리해줄 세 함수인 get_data(), init_network(), predict()를 정의해보자.

init_network()에서는 pickle 파일인 sample.weight.pkl에 저장된 학습된 가중치 매개변수를 읽는다. 이 파일에는 가중치와 편향이 딕셔너리 변수로 저장되어있다.
그러면 이 세 함수를 사용해 신경망에 의한 추론을 수행해보고 정확도를 평가해보자.

가장 먼저 MNIST 데이터셋을 얻고 네트워크를 생성한다. 이어서 for 문을 돌며 x에 저장된 이미지 데이터를 1장씩 꺼내 predict함수로 분류한다. predict 함수는 각 레이블의 확률을 넘파이 배열로 반환한다. 그런 다음 np.argmax() 함수로 이 배열에서 값이 가장 큰 원소의 인덱스를 구하고 이 값과 정답 레이블을 비교하여 맞힌 숫자를 세고 이를 전체 이미지 숫자로 나눠 정확도를 구한다.
이 코드를 실행하면 정확도는 0.9352가 나온다. 올바르게 분류한 비율이 93.52% 라는 것이다.
또한, 이 예에서는 load_mnist 함수의 인수인 normalize를 True로 설정했다. 이 값을 True로 설정하면 0~255 범위인 각 픽셀의 값을 0.0~1.0 범위로 변환한다. (단순히 255로 나눈 것) 이 처럼 데이터를 특정 범위로 변환하는 처리를 정규화라 하고, 신경망의 입력 데이터에 특정 변환을 가하는 작업을 전처리라 한다. 여기에서는 입력 이미지 데이터에 대한 전처리 작업으로 정규화를 수행한 셈이다.
배치 처리
조금 전의 구현을 다시 살펴보자. 우선 앞서 구현한 신경망 각 층의 가중치 shape을 출력해보자.

이 결과에서 다차원 배열의 대응하는 차원의 원소수가 일치함을 확인할 수 있다. 또한 최종 결과로는 원소가 10개인 1차원 배열 y가 출력되는 점도 확인할 수 있다.

위는 원소 784개로 구성된 1차원 배열이 입력되어 마지막에는 원소가 10개인 1차원 배열이 출력되는 흐름을 나타내고 있다. 이는 이미지 데이터를 1장만 입력했을 때의 처리 흐름이다.
그렇다면 이미지 100개를 묶어 predict 함수를 한번에 넘기면 어떻게 될까? x의 shape을 100x784로 바꿔서 100장 분량의 데이터를 하나의 입력 데이터로 표현하면 될 것이다.

위 그림과 같이 입력 데이터의 형상은 100x784 출력 데이터의 형상은 100x10이 된다. 이는 100장 분량 입력 데이터의 결과가 한번에 출력됨을 나타낸다. 가령 x[0]과 y[0]에는 0번째 이미지와 그 추론 결과가 x[1]과 y[1]에는 1번째 이미지와 그 결과가 저장되는 식이다.
이처럼 하나로 묶은 입력 데이터를 batch라고 한다.
이제 배치 처리를 구현해보자.

range() 함수가 반환하는 반복자를 바탕으로 x[i : i + batch_size]에서 입력 데이터를 묶는다. x[i : i + batch_size]는 입력 데이터의 i번째부터 i + batch_size번째까지의 데이터를 묶는다는 의미이다. 이 예에서는 batch_size가 100이므로 100장씩 묶어 꺼내게 된다.
그리고 앞에서도 나온 argmax()는 최댓값의 인덱스를 가져온다. 다만 여기에서는 axis = 1이라는 인수를 추가했다. 이는 100x10 배열 중 1번째 차원을 구성하는 각 원소에서 최댓값의 인덱스를 찾도록 한 것이다.