https://books.google.co.kr/books?id=C-u5DgAAQBAJ&printsec=frontcover&redir_esc=y#v=onepage&q&f=false
https://github.com/myeong01/neuralNet
위에 참조한 책은 Golang 과 전혀 무관하며 Python 을 사용하여 설명합니다.
하지만 Python 에서는 기본 패키지들조차 너무 편리한 기능을 제공해줘서 직접 하나하나 구현해보자 라는 생각은 무슨 사실 딱히 이유는 없고 재밌을 거 같아서 Golang 으로 구현합니다.
function/sigmoid.goSigmoid 함수가 복잡하진 않아 라이브러리 검색해보고 코드가 맞는지 확인하는 것보다 직접 구현하는 것이 나을 것 같아서 직접 구현
package function
import "math"
func Sigmoid(x float64) float64 {
return 1 / (1 + math.Exp(-x))
}
matrix/reverse.goTranspose 라는 용어가 더 보편적으로 사용되나 해당 코드를 작성하는 시점에는 용어에 대해 잘 몰랐음
package matrix
func Reverse(a [][]float64) [][]float64 {
b := make([][]float64, len(a[0]))
for yIndex := range b {
b[yIndex] = make([]float64, len(a))
for xIndex := range b[yIndex] {
b[yIndex][xIndex] = a[xIndex][yIndex]
}
}
return b
}
matrix/multiply.go와우! 행렬곱도 직접 구현
package matrix
import "fmt"
func Multiply(a [][]float64, b [][]float64) ([][]float64, error) {
if len(a) == 0 || len(a[0]) == 0 || len(b) == 0 || len(b[0]) == 0 {
return nil, fmt.Errorf("invalid array input")
}
if len(a[0]) != len(b) {
return nil, fmt.Errorf("row length of first elem must be same as column length of second elem")
}
newMatrix := make([][]float64, len(a))
for yIndex := range newMatrix {
newMatrix[yIndex] = make([]float64, len(b[0]))
for xIndex := range newMatrix[yIndex] {
for iter := range b {
newMatrix[yIndex][xIndex] += a[yIndex][iter] * b[iter][xIndex]
}
}
}
return newMatrix, nil
}
neuralnet/net.go드디어 본격적으로 신경망을 구현합니다!!!
먼저 신경망 구조체를 정의해줍니다.
(확장성보다는 ML 학습의 차원에서 작성한거라 히든 레이어는 한 개로 하드코딩하여 정의하였습니다.)
// 신경망 구조체
type Net struct {
// 입력 노드 갯수
inputNodes int
// 히든 노드 갯수
hiddenNodes int
// 결과 노드 갯수
outputNodes int
learningRate float64
activeFunction func(float64) float64
wih [][]float64
who [][]float64
}
랜덤 Weight 초기 값 생성 함수
import "math/rand"
func newRandomWeightValue() float64 {
var i float64
for i = rand.NormFloat64() * 0.3; i >= 1; i = rand.NormFloat64() * 0.3 {
}
return i
}
신경망 구조체 초기화 함수
import "neuralNet/utils/function"
func New(inputNodes, hiddenNodes, outputNodes int, learningRate float64, activeFunction func(float64) float64) *Net {
if activeFunction == nil {
activeFunction = function.Sigmoid
}
return &Net{
inputNodes: inputNodes,
hiddenNodes: hiddenNodes,
outputNodes: outputNodes,
learningRate: learningRate,
activeFunction: activeFunction,
// 행렬곱의 경우 곱하기 연산자 기준 좌측의 행렬의 i 번째 행의 각 성분과 우측 행렬의 j 번째 열의 각 성분을 순서대로 곱하여 더한 것이 결과의 (i, j) 의 값이 되기 때문에 행렬의 길이는 아래와 같이 적용됨
// (m x k) x (k x n) => (m x n)
// 우리는 사용자의 input 값을 (inputNodes x 1) 로 정의한다면
// <weight> x <input> => <result> 를
// (m x n) x (inputNodes x 1) => (hiddenNodes x 1) 임을 알 수 있고
// 곱하기 연산자 기준 좌측 행렬의 열의 수와 우측 행렬의 행의 수가 같아야되기 때문에 n은 inputNodes 임을 알 수 있다.
// 또한 결과 값의 행의 수가 곱하기 연산자 좌측 행렬의 행의 수가 같아야되기 때문에 m은 hiddenNodes 임을 알 수 있다.
// 따라서 위 수식은 (hiddenNodes x inputNode) x (inputNodes x 1) => (hiddenNodes x 1) 임을 알 수 있고
// inputNodes -> hiddenNodes 사이의 Weight 행렬의 크기는 hiddenNodes x inputNodes 임을 알 수 있다.
wih: newWeight(hiddenNodes, inputNodes),
who: newWeight(outputNodes, hiddenNodes),
}
}
신경망 학습시키기
import "neuralNet/utils/matrix"
func (n *Net) Train(rawData []float64, rawTargets []float64) error {
data := [][]float64{rawData}
// 처음 data 값이 (1 x inputNodes) 기 때문에 행렬을 뒤집어 (inputNodes x 1) 로 만들어 준다.
data = matrix.Reverse(data)
target := [][]float64{rawTargets}
// 처음 target 값이 (1 x inputNodes) 기 때문에 행렬을 뒤집어 (inputNodes x 1) 로 만들어 준다.
target = matrix.Reverse(target)
// hiddenValue 를 계산하기 위해 weight x input 을 해줌 (행렬의 길이는 위에서 설명)
hiddenValue, err := matrix.Multiply(n.wih, data)
if err != nil {
return err
}
// 값이 0과 1 사이에 들어와야되기 때문에 활성화 함수를 통하여 이를 맞춰줌
for yIndex := range hiddenValue {
for xIndex := range hiddenValue[yIndex] {
hiddenValue[yIndex][xIndex] = n.activeFunction(hiddenValue[yIndex][xIndex])
}
}
// finalValue 를 계산하기 위해 weight x hiddenValue 를 해줌
finalValue, err := matrix.Multiply(n.who, hiddenValue)
if err != nil {
return err
}
// 값이 0과 1 사이에 들어와야되기 때문에 활성화 함수를 통하여 이를 맞춰줌
for yIndex := range finalValue {
for xIndex := range finalValue[yIndex] {
finalValue[yIndex][xIndex] = n.activeFunction(finalValue[yIndex][xIndex])
}
}
// target 값에서 우리가 추측한 값을 빼어 에러 값을 계산
errors := make([][]float64, len(target))
for yIndex := range errors {
errors[yIndex] = make([]float64, len(target[yIndex]))
for xIndex := range errors[yIndex] {
errors[yIndex][xIndex] = target[yIndex][xIndex] - finalValue[yIndex][xIndex]
}
}
// hiddenNode 의 에러의 경우 최종 output 값에 영향을 준만큼(weight) error 를 적용
hiddenErrors, err := matrix.Multiply(matrix.Reverse(n.who), errors)
if err != nil {
return err
}
// error 값과 기대 값(finalValue), 영향을 미친 값(hiddenValue) 를 통하여 오차 수정 값을 계산
temp := make([][]float64, len(errors))
for yIndex := range temp {
temp[yIndex] = make([]float64, len(errors[yIndex]))
for xIndex := range temp[yIndex] {
temp[yIndex][xIndex] = errors[yIndex][xIndex] * finalValue[yIndex][xIndex] * (1 - finalValue[yIndex][xIndex])
}
}
delta, err := matrix.Multiply(temp, matrix.Reverse(hiddenValue))
if err != nil {
return err
}
// 계산된 오차 수정 값을 weight 에 적용
for yIndex := range n.who {
for xIndex := range n.who[yIndex] {
n.who[yIndex][xIndex] += delta[yIndex][xIndex]
}
}
// hiddenErrors 값과 기대 값(hiddenValue), 영향을 미친 값(input) 를 통하여 오차 수정 값을 계산
temp = make([][]float64, len(hiddenErrors))
for yIndex := range temp {
temp[yIndex] = make([]float64, len(hiddenErrors[yIndex]))
for xIndex := range temp[yIndex] {
temp[yIndex][xIndex] = hiddenErrors[yIndex][xIndex] * hiddenValue[yIndex][xIndex] * (1 - hiddenValue[yIndex][xIndex])
}
}
delta, err = matrix.Multiply(temp, matrix.Reverse(data))
if err != nil {
return err
}
// 계산된 오차 수정 값을 weight 에 적용
for yIndex := range n.wih {
for xIndex := range n.wih[yIndex] {
n.wih[yIndex][xIndex] += delta[yIndex][xIndex]
}
}
return nil
}
학습된 신경망으로 인퍼런스
import "neuralNet/utils/matrix"
func (n *Net) Query(rawData []float64) ([]float64, error) {
data := [][]float64{rawData}
// 처음 input 값이 (1 x inputNodes) 기 때문에 행렬을 뒤집어 (inputNodes x 1) 로 만들어 준다.
data = matrix.Reverse(data)
// hiddenValue 를 계산하기 위해 weight x input 을 해줌 (행렬의 길이는 위에서 설명)
hiddenValue, err := matrix.Multiply(n.wih, data)
if err != nil {
return nil, err
}
// 값이 0과 1 사이에 들어와야되기 때문에 활성화 함수를 통하여 이를 맞춰줌
for yIndex := range hiddenValue {
for xIndex := range hiddenValue[yIndex] {
hiddenValue[yIndex][xIndex] = n.activeFunction(hiddenValue[yIndex][xIndex])
}
}
// finalValue 를 계산하기 위해 weight x hiddenValue 를 해줌
finalValue, err := matrix.Multiply(n.who, hiddenValue)
if err != nil {
return nil, err
}
// 값이 0과 1 사이에 들어와야되기 때문에 활성화 함수를 통하여 이를 맞춰줌
for yIndex := range finalValue {
for xIndex := range finalValue[yIndex] {
finalValue[yIndex][xIndex] = n.activeFunction(finalValue[yIndex][xIndex])
}
}
// 최종 값이 (ouputNodes x 1) 기 때문에 행렬을 뒤집어 (1 x outputNodes) 로 만들어주고 일차원 배열로 반환해줌
// 어차피 y 축의 길이가 1이기 때문에 손실되는 데이터는 없음
result := matrix.Reverse(finalValue)
return result[0], nil
}
main.go길고 길었던 구현이 끝났으니 이제 사용해봅시다!
데이터는 mnist 를 사용했습니다.
추정 값들 중 가장 높은(값이 높은) 값(인덱스)가 추정하는 답이기 때문에 최대 값 Index 를 찾아야합니다.
func findMaxIndex(l []float64) int {
maxValue := l[0]
maxIndex := 0
for index, value := range l {
if maxValue < value {
maxValue = value
maxIndex = index
}
}
return maxIndex
}
초기화, 학습과 성능 측정까지 하는 main 함수
import (
"encoding/csv"
"fmt"
"neuralNet/neuralnet"
"os"
"strconv"
)
func main() {
// mnist 데이터를 사용하여 inputNodes 는 784 이고 결과 값이 0 ~ 9 이기 때문에 outputNodes 는 10이며 hiddenNodes 는 임의로 100으로 설정
n := neuralnet.New(784, 100, 10, 0.1, nil)
// 학습 데이터셋 읽어오고 파싱하기
trainFile, err := os.Open("./mnist_train.csv")
if err != nil {
panic(err)
}
defer trainFile.Close()
trainSetReader := csv.NewReader(trainFile)
rawTrainSet, err := trainSetReader.ReadAll()
if err != nil {
panic(err)
}
trainSet := make([][]float64, len(rawTrainSet))
for yIndex := range trainSet {
trainSet[yIndex] = make([]float64, len(rawTrainSet[yIndex]))
for xIndex := range trainSet[yIndex] {
trainSet[yIndex][xIndex], err = strconv.ParseFloat(rawTrainSet[yIndex][xIndex], 64)
if err != nil {
panic(err)
}
if xIndex != 0 {
// 학습 값이 0으로 시작한다면 해당 학습에서 해당 Node는 어떠한 영향도 끼치지 않는 죽은 노드가 되기 때문에 이를 방지하고자 0과 가까운 값으로 설정
// 0번째 인덱스는 학습 데이터가 아닌 정답 데이터기 때문에 해당 계산 부분에서 제외
trainSet[yIndex][xIndex] = (trainSet[yIndex][xIndex] / 255 * 0.99) + 0.01
}
}
}
// 학습 시작
fmt.Println("Start Train")
// 에폭은 5로 주어 동일한 데이터셋으로 5회 반복 학습 진행
epoch := 5
for i := 0; i < epoch; i++ {
fmt.Println("epoch", i, "start")
for _, train := range trainSet {
target := make([]float64, 10)
// 활성화 함수 특성상 무한히 0으로 다가갈 뿐 0이 될 수 없음
// 때문에 0으로 설정할 시 과도하게 피팅될 가능성이 있기 때문에 0이 아닌 0에 가까운 값으로 설정
for index := range target {
target[index] = 0.01
}
// 이 것도 위와 동일한 이유로 1에 가까운 값으로 설정
target[int(train[0])] = 0.99
// 첫 번째 인덱스는 정답(목표 값)이기 때문에 제거하고 학습 진행
err := n.Train(train[1:], target)
if err != nil {
panic(err)
}
}
}
fmt.Println("Finish Train")
// 테스트 시작
// 테스트 데이터셋 읽어오고 파싱하기
testFile, err := os.Open("./mnist_test.csv")
if err != nil {
panic(err)
}
defer testFile.Close()
testSetReader := csv.NewReader(testFile)
rawTestSet, err := testSetReader.ReadAll()
if err != nil {
panic(err)
}
testSet := make([][]float64, len(rawTestSet))
for yIndex := range testSet {
testSet[yIndex] = make([]float64, len(rawTestSet[yIndex]))
for xIndex := range testSet[yIndex] {
testSet[yIndex][xIndex], err = strconv.ParseFloat(rawTestSet[yIndex][xIndex], 64)
if err != nil {
panic(err)
}
if xIndex != 0 {
// 테스트 값이 0으로 시작한다면 해당 테스트에서 해당 Node는 어떠한 영향도 끼치지 않는 죽은 노드가 되기 때문에 이를 방지하고자 0과 가까운 값으로 설정
// 0번째 인덱스는 학습 데이터가 아닌 정답 데이터기 때문에 해당 계산 부분에서 제외
testSet[yIndex][xIndex] = (testSet[yIndex][xIndex] / 255 * 0.99) + 0.01
}
}
}
var correctCnt int64 = 0
for _, test := range testSet {
answer, err := n.Query(test[1:])
if err != nil {
panic(err)
}
if int(test[0]) == findMaxIndex(answer) {
correctCnt++
}
}
fmt.Println("Accuracy :", float64(correctCnt)/float64(len(testSet)))
}
실수로 테스트를 할 때 input 값을 0 ~ 1 사이로 계산하기 않고 기존 값을 바로 사용하여 성능이 0.1 정도 나왔어서 포기하고 싶었으나 이유를 발견하고 너무나도 허탈했습니다...ㅋㅋㅋㅋ 그래도 해당 부분 수정하니 성능이 0.8 중후반으로 올라서 기분이 좋았고 뿌듯했습니다 :)
잘 읽었습니다. 좋은 정보 감사드립니다.