TensorFlow

신경망 첫걸음 , 06 MNIST 손글씨 데이터 인식하기

LEEHANDS 2022. 4. 14. 17:12
반응형

https://github.com/makeyourownneuralnetwork/makeyourownneuralnetwork

 

GitHub - makeyourownneuralnetwork/makeyourownneuralnetwork: Code for the Make Your Own Neural Network book

Code for the Make Your Own Neural Network book. Contribute to makeyourownneuralnetwork/makeyourownneuralnetwork development by creating an account on GitHub.

github.com

 

 

사람의 손글씨를 인식한다는 것은 컴퓨터에게는 어려운 문제이다.

역사적으로도 인공지능에서도 큰 도전 과제

 

컴퓨터가 이미지의 내용을 명확히 분류하는 작업을 이미 이미지 인식이라고했다.

 

MNIST 데이터 셋은 신경망 학자인 얀 르쿤 교수의 우베사이트에서 구할 수 있다.

http://yann.lecun.com/exdb/mnist/

 

MNIST handwritten digit database, Yann LeCun, Corinna Cortes and Chris Burges

 

yann.lecun.com

이 웹사이트에서는 그동안 등장한 다양한 기법이 이손으로 쓴 숫자를 얼마나 잘 인식하는지에 대한 정보도 함께 담고있다.

MNIST 데이터베이스의 원래 포멧은 다루기에 쉽지 않기때문에 많은 사람들이 사용의 편의르 ㄹ위해 보다 편리한 포멧으로 변경한 자료들이 있다.

http://pjreddie.com/projects/mnist-in-csv

 

MNIST in CSV

MNIST in CSV Here's the train set and test set. The format is: label, pix-11, pix-12, pix-13, ... where pix-ij is the pixel in the ith row and jth column. For the curious, this is the script to generate the csv files from the original data. def convert(img

pjreddie.com

 

이 자료는 CSV (comma seperated values) 라는 포멧을 사용하는데 이 포맷은 단순 텍스트 형태로 데이터 값을 쉼표로 구분해 놓은 것입니다.

CSV 포멧을 가지는 파일은 텍스트 편집기는 물론 엑셀과 같은 스프레드시트나 여러 데이터 분석 소프트웨어에서 쉽게 열어볼 수 있다.

즉 표준 포멧이다.

이 웹사이트에서는 2개의 CSV 파일을 제공한다.

FTP 다운로드 가능

( ftp://public.leehands.com/neuralnetwork/mnist )

 

학습데이터 (Training set ) 에서는 신경망의 학습에 이용 될 수 있도록 레이블이 붙어있는 60,000개의 데이터가 존재

레이블이 존재한다는 말은 입력 값과 함께 결과 (정답) 이 들어있다는 의미

테스트데이터(Test Data) 는 총 10,000개가 존재합니다. 테스트 데이터는 우리의 아이디어나 알고리즘이 얼마나 잘 동작하는지 확인하기 위해 사용한다.

 

학습데이터와 테스트 데이터를 별도로 두는 이유는 학습시키지 않은 데이터를 대상으로 테스트하기 위함

이렇게 하지 않고 컴퓨터가 고스란히 기억하고있는 학습 데이터에 대해 테스트를 진행하게 되면 우리는 왜곡된 점수를 얻게 됩니다.

이처럼 학습 데이터와 테스트 데이터를 분리하는 것은 머신러닝에서 매우 일반적인 개념

 

학습데이터를 보자

각 행은 레코드 (Record) 라고도 하고 이 내용물을 이해하자면

첫번째 값은 레이블 (Label) 입니다. 다시말해 5,0,4,1 등 그 이미지에 있는 숫자의 값을 나타냅니다.  이 값이 바로 신경망이 학습을 통해 맞히고자하는 정답

레이블 이후에 나오는 쉼표로 구분되는 값들은 손으로 쓴 숫자의 픽셀값들입니다. 픽셀 배열의 크기는 28x28이므로 총 784개의 값이 존재한다.

 

일단 작은 파일을 이용해 학습을 해보자

mnist_test_10.csv

mnist_train_100.csv

 

위에서 공유한 ftp 서버에서 다운받을 수 있다.

 

그러면 다운로드받은 파일은 어떻게 Python 에서 사용할 수 있을까?

 

data_file = open("mnist_dataset/mnist_train_100.csv",'r')
data_list = data_file.readlines()
data_file.close()

 

파이썬에서 len() 함수는 리스트의 길이를 알려줍니다.

이어서 data_list[0] 을 입력하여 첫번째 레코드 내용을 확인 했습니다.

첫번째 숫자가 5 이므로 이 이미지의 레이블은 5라는 것을 알 수 있다.

나머지 784 개의 숫자들은 이 이미지를 구성하는 픽셀의 색상 값입니다.

 

우리는 앞에서 imshow()함수를 이용해 행렬을 시각화하는 방법을 본바 있습니다.

이번에도 시각화 해볼 텐데 그러기 위해서는 우선 쉼표로 구분된 숫자들의 리스트를 적절한 행렬로 변환해줘야합니다.

단계를 구분해보면 다음과 같습니다.

 

  • 구분자로 쉼표를 이용해 긴 텍스트 문자열을 개별 값으로 분리합니다.
  • 레이블 값인 첫 번째 값은 무시하고, 나머지 28x28 = 784의 값을 추출한 후, 이를 28개 행과 열의 형태를 가지는 배열로 변환
  • 이 배열을 시각화

우선 배열의 이용과 시각화를 도와줄 파이썬 확장 라이브러리를 불러오겠습니다.

import numpy
import matplotlib.pyplot
%matplotlib inline

 

3행으로 이루어지 다음 코드를 봅시다.

 

all_values = data_list[0].split(',')
image_array = numpy.asfarrray(all_values[1:]).reshape((28,28))
matplotlib.pyplot.imshow(image_array, cmap='Greys', interpolation = 'None')

첫번째 항에서는 첫번째 레코드인 data_list[0]를 불러와서 이를 쉼표로 구분하여 분리합니다.

split()함수는 구분자로 사용할 기호를 그 매개변수로 가진다는 것을 볼 수 있다.

이 결과가 all_values 라는 변수로 저장 이변수가 실제로 785개의 값

 

다음행은 all_values 리스트가 있는데 [1:] 을 이용해 원소중 첫번째 원소인 레이블 값을 무시하고 나머지 784개의 값만 취할 수 있다.

numpy.asfarray() 는 numpy 함수로써 문자열을 숫자로 변환한 다음에 그 숫자로 구성된 배열을 생성합니다.

 

세번째 행에서 imshow()함수를 이용해 image_array를 시각화합니다.

손으로 쓴 숫자를 보다 잘 보이게 하기 위해 cmap='Greys'를 지정해 회색으로 표기

 

MNIST 학습데이터 준비하기

MNiST 데이터 파일로부터 데이터를 불러와서 이 데이터를 우리의 용도에 맞게 가공하여 시각화 까지했습니다.

이제 이 데이터를 이용해 신경망을 학습시켜야 겠죠

 

데이터를 바로 신경망에 적용하기 전에 데이터를 어떻게 준비해야할지 생각해볼점이 있다.

 

입력데이터와 출력데이터의 값들이 적절한 형태를 가져서 활성화함수의 수용범위 내에 있게 되면 신경망은 더 잘 동작하게 됩니다.

 

우리가 우선해야할일은 0~255 사이에 속하는 입력 색상값들을의 범위를 0.01 ~ 1.0 사이에 속하게 조정하는 것입니다.

입력값이 0을 가지면 가중치 업데이트를 없애버리므로 하안선은 0.01 로 선택합니다. 입력 값이 1.0 인것은 별 문제가 되지 않는다.

다만 출력값에 대해서는 1.0을 피해야 합니다.

 

0~255 사이의 값을 가지는 입력 값들을 255으로 나누면 0~1 범위를 가지게 될 것입니다. 그리고 여기에 0.99를 곱하면 그 범위는 0.0~0.99가 됩니다. 여기에 0.01을 더함으로써 원하는 범위인 0.01~1.00을 얻을 수 있다.

 

scaled_input = (numpy.asfarray(all_values[1:])/ 255.0 * 0.99) + 0.01
print(scaled_input)

이제 신경망의 결과 값에 대해서 생각해보자

결과 값이 활성화 함수가 출력할 수 있는 값의 범위 내에 있어야한다고 배웠다.

우리가 이용할 로지스틱 함수는 -2.0 이나 255 같은 값을 출력할 수 없다. 그 범위는 0.0 ~ 1.0 사이인데 0.0 과 1.0은 무한히 접근만 할 뿐 실제값은 될 수 없다.

여기에서 좀더 깊이있는 의문을 가져보자

정답의 이미지가 되어야할까? 그렇게한다면 28x28 = 784 개의 노드가 필요하다는 뜻

 

우리가 신경망에 바라는 점을 생각해보자

레이블은 0~9까지 10개의 숫자 중 하나입니다.

다시말해 10개의 노드로 구성된 출력 계층이 필요하고

각 노드는 가능한 결과 값, 즉 각 레이블에 해당할 것입니다.

결과 값이 0이라면 첫번째 노드가 활성화 되고, 나머지 노드는 활성화 되지 않을 것입니다.

출처 : 신경망 첫걸음

 

출력노드 예시

onodes = 10
targets = numpy.zeros(onodes)+0.01
targets[int(all_values[0])] = 0.99

첫번째 행은 출력 계층의 노드의 수를 10개로 설정

두번째 행은 0으로 채워진 행렬을 생성하기 위해 numpy.zeros()라는 함수 사용하고 이 함수는 매개변수로 행렬의 크기와 형태를 받는다.

여기에 최종 출력 계층의 노드수를 의미하는 onodes 변수를 받아 행렬의 길이로 사용, 그리고 0의 값을 피하기 위해서 0.01을 더해줍니다.

다음행에서는 학습의 결과 레이블인 MNIST 레코드의 첫번째 원소를 취한 다음 int함수를 이용해 문자열을 정수로 변환합니다.

레코드는 소스 파일로부터 숫자가 아니라 문자열로 읽혀진다는점 기억하자

"0" 의 레이블이 이 레이블에 대해 target[]에 들어가야할 정확한 인덱스인 정수 0 으로 변환되므로 깔끔하게 표현가능하다.

 

다른 예로 "9" 레이블은 정수 9로 변환되지면 결과 레이블을 이용해 결과 원소의 값을 0.99 으로 설정해줍니다.

 

학습과 질의를 위한 입력값과 학습을 위한 출력 값의 준비를 완료했다.

이제 파이썬 코드를 업데이트 해봅시다.

 

import numpy
# 시그모이드 함수 expit() 사용하기 위해 scipy.special 불러오기
import scipy.special
# 행렬을 시각화하기 위한 라이브러리
import matplotlib.pyplot
# 시각화가 외부 윈도우가 아닌 현재의 노트북 내에서 보이도록 설정
%matplotlib inline

# 신경망 클래스의 정의
class neuralNetwork:
    # 신경망 초기화 하기
    def __init__(self, inputnodes, hiddennodes, outputnodes, learningrate):
        # 입력 / 은닉 / 출력 계층의 노드 갯수 설정
        self.inodes = inputnodes
        self.hnodes = hiddennodes
        self.onodes = outputnodes
        # 가중치 행렬 wih & who
        # 배열 내 가중치는 w_i_j 로 표기. 노드 i 에서 다음 계층의 노드 j 로 연결됨을 의미
        # w11 w21
        # w12 w22 etc 
        self.wih = numpy.random.normal(0.0, pow(self.inodes, -0.5), (self.hnodes, self.inodes))
        self.who = numpy.random.normal(0.0, pow(self.hnodes, -0.5), (self.onodes, self.hnodes))
        # 학습률
        self.lr = learningrate
        # 활성화 함수로 시그모이드 함수를 이용
        self.activation_function = lambda x: scipy.special.expit(x)
        pass
    # 신경망 학습 시키기
    def train(self, inputs_list, targets_list):
        # 입력 리스트를 2차원의 행렬로 변환
        inputs = numpy.array(inputs_list, ndmin=2).T
        targets = numpy.array(targets_list, ndmin=2).T
        # 은닉 계층으로 들어오는 신호를 계산
        hidden_inputs = numpy.dot(self.wih, inputs)
        # 은닉 계층에서 나가는 신호를 계산
        hidden_outputs = self.activation_function(hidden_inputs)
        # 최종 출력 계층으로 들어오는 신호를 계산
        final_inputs = numpy.dot(self.who, hidden_outputs)
        # 최종 출력 계층에서 나가는 신호를 예산
        final_outputs = self.activation_function(final_inputs)
        # 출력 계층의 오차는 ( 실제 값 - 계산 값 )
        output_errors = targets - final_outputs
		# 은닉 계층의 오차는 가중치에 의해 나뉜 출력 계층의 오차들을 재조합해 계산
        hidden_errors = numpy.dot(self.who.T, output_errors) 
		# 은닉 계층과 출력 계층 간의 가중치 업데이트
        self.who += self.lr * numpy.dot((output_errors * final_outputs * (1.0 - final_outputs)), numpy.transpose(hidden_outputs))
		# 입력 계층과 은닉 계층 간의 가중치 업데이트
        self.wih += self.lr * numpy.dot((hidden_errors * hidden_outputs * (1.0 - hidden_outputs)), numpy.transpose(inputs))
        pass
    # 신경망에 질의 하기
    def query(self, inputs_list):
		# 입력 리스트를 2차원 행렬로 변환
        inputs = numpy.array(inputs_list, ndmin=2).T
		# 은닉 계층으로 들어오는 신호를 계산
        hidden_inputs = numpy.dot(self.wih, inputs)
		# 은닉 계층에서 나가는 신호를 계산
        hidden_outputs = self.activation_function(hidden_inputs)
        
		# 최종 출력 계층으로 들어오는 신호를 계산
        final_inputs = numpy.dot(self.who, hidden_outputs)
		# 최종 출력 계층에서 나가는 신호를 계산
        final_outputs = self.activation_function(final_inputs)
        
        return final_outputs
        
# 입력 , 은닉, 출력 노드의 수
input_nodes = 784
hidden_nodes = 200
output_nodes = 10

# 학습률
learning_rate = 0.1

# 신경망 인스턴스 생성
n = neuralNetwork(input_nodes,hidden_nodes,output_nodes, learning_rate)

# mnist 학습 데이터인 csv 파일을 리스트로 불러오기
training_data_file = open("mnist_dataset/mnist_train_100.csv", 'r')
training_data_list = training_data_file.readlines()
training_data_file.close()

# 신경망을 학습하기
# 전체 데이터셋을 면번 순환시켜서 학습 시킬 것인가?
epochs = 5

for e in range(epochs):
    for record in training_data_list:
        # 레코드를 쉼표에 의해 분리
        all_values = record.split(',')
		# 입력 값의 범위와 값 조정
        inputs = (numpy.asfarray(all_values[1:]) / 255.0 * 0.99) + 0.01
		# 결과 값 생성 ( 실제 값인 0.99 외에는 모두 0.01)
        targets = numpy.zeros(output_nodes) + 0.01
		# all_values[0] 은 이 레코드에 대한 결과 값
        targets[int(all_values[0])] = 0.99
        n.train(inputs, targets)
        pass
    pass
    
    
# mnist 테스트 데이터인 csv 파일을 리스트로 불러오기
test_data_file = open("mnist_dataset/mnist_test.csv", 'r')
test_data_list = test_data_file.readlines()
test_data_file.close()

# 신경망 테스트

# 신경망의 성능 지표가 되는 성적표를 아무 값도 가지지 않도록 초기화
scorecard = []

# 테스트 데이터 모음 내의 모든 레코드를 탑색
for record in test_data_list:
    # 레코드를 쉼표에 의해 분리
    all_values = record.split(',')
	# 정답은 첫번 째 값
    correct_label = int(all_values[0])
    print(correct_label, "correct label")
    # 입력 값의 범위와 값 조정
    inputs = (numpy.asfarray(all_values[1:]) / 255.0 * 0.99) + 0.01
	# 신경망에 질의
    outputs = n.query(inputs)
	# 가장 높은 값의 인덱스는 레이블의 인덱스와 일치
    label = numpy.argmax(outputs)
    print(label, "network's answer")
	# 정답 또는 오답을 리스트에 추가
    if (label == correct_label):
		# 정답인 경우 성적표에 1을 더함
        scorecard.append(1)
    else:
		# 오답인 경우에 성적표에 0을 더함
        scorecard.append(0)
        pass
    pass
    
# 정답의 비율인 성적을 계산해 출력
scorecard_array = numpy.asarray(scorecard)
print ("performance = ", scorecard_array.sum() / scorecard_array.size)

 

학습률은 0.3 을 이용했다 그러면 0.6 으로 적용해 전반적인 신경망의 학습에 도움될 지 봅시다.

그러면 오히려 정확도가 낮을 수 있다.

큰 학습률에 의해 경사 하강 과장에서 오버 슈팅이 일어나면서 튕김 현상이 발생하는 것같다.

그러면 0.1 으로 적용해보면 너무 작은 학습률은 경사 하강의 속도를 제한해 너무 작은 걸음을 걷는 셈이다.

 

epochs 여러번 수행을 통한 신경망 개선

많이하면할 수 록 좋다고생각하는데 너무 많이 하면 신경망이 학습 데이터에 오버피팅(과적합) 되어 이전에 보지 못한 신규 데이터에 대해서는 오히려 성능이 떨어질 수 있다.

 

반응형