Go로 처음부터 신경망 구축하기: 원리, 구조 및 구현
James Reed
Infrastructure Engineer · Leapcell

Go로 처음부터 신경망 구축하기: 원리, 구조 및 구현
이 기사에서는 Go 프로그래밍 언어를 사용하여 간단한 신경망을 처음부터 구축하고 Iris 분류 작업을 통해 워크플로를 시연하는 방법을 소개합니다. 원리 설명, 코드 구현 및 시각적 구조 디스플레이를 결합하여 독자가 신경망의 핵심 메커니즘을 이해하도록 돕습니다.
Ⅰ. 신경망의 기본 원리 및 구조
신경망은 생물학적 뉴런을 시뮬레이션하는 계산 모델로, 여러 계층에서 노드의 연결을 통해 비선형 매핑을 달성합니다. 일반적인 3계층 신경망 구조에는 입력 계층, 은닉 계층 및 출력 계층이 포함됩니다. 각 계층의 노드는 가중치와 편향을 통해 연결되며 계층 간 전송은 활성화 함수를 통해 처리됩니다. 아래는 간단한 3계층 신경망 구조의 개략도입니다(ASCII 문자로 그림).
+-----------+ +-----------+ +-----------+ | Input Layer | | Hidden Layer | | Output Layer | | 4 Nodes | | 3 Nodes | | 3 Nodes | +-----------+ +-----------+ +-----------+ ↑ ↑ ↑ │ Weights │ Weights │ Weights │ ├───────────────┼───────────────┼───────────────┤ ↓ ↓ ↓ +-----------+ +-----------+ +-----------+ | Bias | | Bias | | Bias | +-----------+ +-----------+ +-----------+ ↓ ↓ ↓ +-----------+ +-----------+ +-----------+ | Activation| | Activation| | Activation| | Function | | Function | | Function | +-----------+ +-----------+ +-----------+
핵심 개념:
-
순방향 전파 입력 데이터는 가중치 행렬(
input × weights + bias
)을 통해 선형 변환을 거친 다음 활성화 함수를 통해 비선형성을 도입하여 계층별로 출력 계층으로 전파됩니다. 공식 예시:- 은닉 계층 입력: ( Z_1 = X \cdot W_1 + b_1 )
- 은닉 계층 출력: ( A_1 = \sigma(Z_1) ) ((\sigma)는 시그모이드 함수)
- 출력 계층 입력: ( Z_2 = A_1 \cdot W_2 + b_2 )
- 출력 계층 출력: ( A_2 = \sigma(Z_2) )
-
역전파 예측 값과 실제 값 사이의 오류(예: 평균 제곱 오차)를 계산하여 각 계층의 가중치와 편향이 체인 규칙을 사용하여 역순으로 업데이트되어 모델 매개변수를 최적화합니다. 주요 단계:
- 출력 오류 계산: ( \delta_2 = A_2 - Y )
- 은닉 계층 오류: ( \delta_1 = \delta_2 \cdot W_2^T \odot \sigma'(Z_1) ) ((\odot)은 요소별 곱셈을 나타냄)
- 가중치 업데이트: ( W_2 \leftarrow W_2 - \eta \cdot A_1^T \cdot \delta_2 ), ( W_1 \leftarrow W_1 - \eta \cdot X^T \cdot \delta_1 )
- 편향 업데이트: ( b_2 \leftarrow b_2 - \eta \cdot \sum \delta_2 ), ( b_1 \leftarrow b_1 - \eta \cdot \sum \delta_1 ) ((\eta)는 학습률, (\sigma')는 활성화 함수의 도함수)
Ⅱ. Go에서 신경망 구현의 주요 설계
1. 데이터 구조 정의
Go에서 행렬 연산을 위해 gonum.org/v1/gonum/mat
패키지를 사용하여 네트워크 구조 및 매개변수를 정의합니다.
// neuralNet은 훈련된 신경망 매개변수를 저장합니다. type neuralNet struct { config neuralNetConfig // 네트워크 구성 wHidden *mat.Dense // 은닉 계층 가중치 행렬 bHidden *mat.Dense // 은닉 계층 편향 벡터 wOut *mat.Dense // 출력 계층 가중치 행렬 bOut *mat.Dense // 출력 계층 편향 벡터 } // neuralNetConfig는 네트워크 아키텍처 및 훈련 매개변수를 정의합니다. type neuralNetConfig struct { inputNeurons int // 입력 계층 노드 수 (예: Iris의 4가지 특징) outputNeurons int // 출력 계층 노드 수 (예: 3가지 Iris 종) hiddenNeurons int // 은닉 계층 노드 수 (조정 가능한 하이퍼파라미터) numEpochs int // 훈련 에포크 수 learningRate float64 // 학습률 }
2. 활성화 함수 및 그 도함수
시그모이드 함수를 활성화 함수로 선택하고, 그 도함수는 함수 값을 기반으로 빠르게 계산할 수 있어 역전파에 적합합니다.
// sigmoid 시그모이드 활성화 함수 func sigmoid(x float64) float64 { return 1.0 / (1.0 + math.Exp(-x)) } // sigmoidPrime 시그모이드 함수의 도함수 func sigmoidPrime(x float64) float64 { s := sigmoid(x) return s * (1.0 - s) }
3. 역전파 훈련 로직
매개변수 초기화
네트워크가 학습할 수 있도록 가중치와 편향을 난수로 초기화합니다.
func (nn *neuralNet) train(x, y *mat.Dense) error { randGen := rand.New(rand.NewSource(time.Now().UnixNano())) // 난수 생성기 // 은닉 및 출력 계층에 대한 가중치 및 편향 초기화 wHidden := mat.NewDense(nn.config.inputNeurons, nn.config.hiddenNeurons, nil) bHidden := mat.NewDense(1, nn.config.hiddenNeurons, nil) wOut := mat.NewDense(nn.config.hiddenNeurons, nn.config.outputNeurons, nil) bOut := mat.NewDense(1, nn.config.outputNeurons, nil) // 매개변수 행렬을 난수로 채우기 for _, param := range [][]*mat.Dense{{wHidden, bHidden}, {wOut, bOut}} { for _, m := range param { raw := m.RawMatrix().Data for i := range raw { raw[i] = randGen.Float64() // [0, 1) 범위의 난수 값 } } } // 훈련을 위해 역전파 호출 return nn.backpropagate(x, y, wHidden, bHidden, wOut, bOut) }
핵심 역전파 로직
행렬 연산을 통해 오류 역전파 및 매개변수 업데이트를 구현합니다. 이 코드는 Apply
메서드를 사용하여 활성화 함수와 도함수를 일괄 처리합니다.
func (nn *neuralNet) backpropagate(x, y, wHidden, bHidden, wOut, bOut *mat.Dense) error { for epoch := 0; epoch < nn.config.numEpochs; epoch++ { // 각 계층의 출력을 계산하기 위한 순방향 전파 hiddenInput := new(mat.Dense).Mul(x, wHidden) // 은닉 계층 선형 입력: X·W_hidden hiddenInput.Apply(func(_, col int, v float64) float64 { // 편향 항 추가 return v + bHidden.At(0, col) }, hiddenInput) hiddenAct := new(mat.Dense).Apply(sigmoid, hiddenInput) // 활성화된 은닉 계층 출력 outputInput := new(mat.Dense).Mul(hiddenAct, wOut) // 출력 계층 선형 입력: A_hidden·W_out outputInput.Apply(func(_, col int, v float64) float64 { // 편향 항 추가 return v + bOut.At(0, col) }, outputInput) output := new(mat.Dense).Apply(sigmoid, outputInput) // 활성화된 출력 계층 출력 // 오류 및 기울기를 계산하기 위한 역전파 error := new(mat.Dense).Sub(y, output) // 출력 오류: Y - A_out // 출력 계층 기울기 계산 outputSlope := new(mat.Dense).Apply(sigmoidPrime, outputInput) // σ'(Z_out) dOutput := new(mat.Dense).MulElem(error, outputSlope) // δ_out = error * σ'(Z_out) // 은닉 계층 기울기 계산 hiddenError := new(mat.Dense).Mul(dOutput, wOut.T()) // 오류 역전파: δ_out·W_out^T hiddenSlope := new(mat.Dense).Apply(sigmoidPrime, hiddenInput) // σ'(Z_hidden) dHidden := new(mat.Dense).MulElem(hiddenError, hiddenSlope) // δ_hidden = δ_out·W_out^T * σ'(Z_hidden) // 가중치 및 편향 업데이트 (확률적 경사 하강법) wOut.Add(wOut, new(mat.Dense).Scale(nn.config.learningRate, new(mat.Dense).Mul(hiddenAct.T(), dOutput))) bOut.Add(bOut, new(mat.Dense).Scale(nn.config.learningRate, sumAlongAxis(0, dOutput))) wHidden.Add(wHidden, new(mat.Dense).Scale(nn.config.learningRate, new(mat.Dense).Mul(x.T(), dHidden))) bHidden.Add(bHidden, new(mat.Dense).Scale(nn.config.learningRate, sumAlongAxis(0, dHidden))) } // 훈련된 매개변수 저장 nn.wHidden, nn.bHidden, nn.wOut, nn.bOut = wHidden, bHidden, wOut, bOut return nil }
4. 순방향 예측 함수
훈련 후 훈련된 가중치와 편향을 사용하여 순방향 전파를 통해 예측을 출력합니다.
func (nn *neuralNet) predict(x *mat.Dense) (*mat.Dense, error) { // 매개변수가 있는지 확인 if nn.wHidden == nil || nn.wOut == nil { return nil, errors.New("신경망이 훈련되지 않았습니다.") } hiddenAct := new(mat.Dense).Mul(x, nn.wHidden).Apply(func(_, col int, v float64) float64 { return v + nn.bHidden.At(0, col) }, nil).Apply(sigmoid, nil) output := new(mat.Dense).Mul(hiddenAct, nn.wOut).Apply(func(_, col int, v float64) float64 { return v + nn.bOut.At(0, col) }, nil).Apply(sigmoid, nil) return output, nil }
Ⅲ. 데이터 처리 및 실험적 검증
1. 데이터세트 준비
4가지 특징(꽃받침 길이, 꽃받침 너비, 꽃잎 길이, 꽃잎 너비)과 3가지 종(Setosa, Versicolor, Virginica)을 포함하는 클래식 Iris 데이터세트를 사용합니다. 데이터 전처리 단계:
- 종 레이블을 원-핫 인코딩으로 변환합니다(예: Setosa는
[1, 0, 0]
에 해당하고 Versicolor는[0, 0, 1]
에 해당합니다). - 데이터의 80%를 훈련 세트로, 20%를 테스트 세트로 분할하고 훈련 난이도를 높이기 위해 작은 무작위 노이즈를 추가합니다.
- 샘플 데이터 (
train.csv
에서 발췌):sepal_length,sepal_width,petal_length,petal_width,setosa,virginica,versicolor 0.0873,0.6687,0.0,0.0417,1.0,0.0,0.0 0.7232,0.4533,0.6949,0.967,0.0,1.0,0.0 0.6617,0.4567,0.6580,0.6567,0.0,0.0,1.0
2. 주 프로그램 흐름
데이터 읽기 및 행렬로 변환
func main() { // 훈련 데이터 파일 읽기 f, err := os.Open("data/train.csv") if err != nil { log.Fatalf("파일을 여는 데 실패했습니다: %v", err) } defer f.Close() reader := csv.NewReader(f) reader.FieldsPerRecord = 7 // 4가지 특징 + 3가지 레이블 rawData, err := reader.ReadAll() if err != nil { log.Fatalf("CSV를 읽는 데 실패했습니다: %v", err) } // 데이터를 입력 특징(X) 및 레이블(Y)로 파싱 numSamples := len(rawData) - 1 // 헤더 건너뛰기 inputsData := make([]float64, 4*numSamples) labelsData := make([]float64, 3*numSamples) for i, record := range rawData { if i == 0 { continue // 헤더 건너뛰기 } for j, val := range record { fVal, err := strconv.ParseFloat(val, 64) if err != nil { log.Fatalf("잘못된 값: %v", val) } if j < 4 { inputsData[(i-1)*4+j] = fVal // 처음 4개 열은 특징입니다. } else { labelsData[(i-1)*3+(j-4)] = fVal // 마지막 3개 열은 레이블입니다. } } } inputs := mat.NewDense(numSamples, 4, inputsData) labels := mat.NewDense(numSamples, 3, labelsData) }
네트워크 매개변수 구성 및 훈련
// 네트워크 구조 정의: 4개의 입력, 3개의 은닉 노드, 3개의 출력 config := neuralNetConfig{ inputNeurons: 4, outputNeurons: 3, hiddenNeurons: 5, numEpochs: 8000, // 5000 에포크 동안 훈련 learningRate: 0.2, // 학습률 } network := newNetwork(config) if err := network.train(inputs, labels); err != nil { log.Fatalf("훈련 실패: %v", err) }
모델 정확도 테스트
// 테스트 데이터 읽기 및 예측 predictions, err := network.predict(testInputs) if err != nil { log.Fatalf("예측 실패: %v", err) } // 분류 정확도 계산 trueCount := 0 numPreds, _ := predictions.Dims() for i := 0; i < numPreds; i++ { // 실제 레이블 가져오기 (원-핫에서 인덱스로) trueLabel := mat.Row(nil, i, testLabels) trueClass := -1 for j, val := range trueLabel { if val == 1.0 { trueClass = j break } } // 예측에서 가장 높은 확률을 가진 클래스 가져오기 predRow := mat.Row(nil, i, predictions) maxVal := floats.Min(predRow) predClass := -1 for j, val := range predRow { if val > maxVal { maxVal = val predClass = j } } if trueClass == predClass { trueCount++ } } fmt.Printf("정확도: %.2f%% ", float64(trueCount)/float64(numPreds)*100)
Ⅳ. 실험 결과 및 요약
8000번의 훈련 에포크 후 모델은 테스트 세트에서 약 98%의 분류 정확도를 달성합니다(결과는 무작위 초기화로 인해 약간 다를 수 있음). 이는 간단한 3계층 신경망도 비선형 분류 문제를 효과적으로 해결할 수 있음을 보여줍니다.
핵심 장점:
- 순수 Go 구현: C 확장(cgo 없음)에 의존하지 않고 정적 바이너리 파일로 컴파일할 수 있어 크로스 플랫폼 배포에 적합합니다.
- 행렬 추상화:
gonum/mat
패키지를 기반으로 한 숫자 계산으로 코드 구조가 명확하고 확장성이 용이합니다.
개선 방향:
- 다양한 활성화 함수(예: ReLU) 또는 최적화 프로그램(예: Adam)을 실험해 봅니다.
- 과적합을 방지하기 위해 정규화(예: L2 정규화)를 추가합니다.
- 여러 개의 은닉 계층을 지원하여 더 깊은 신경망을 구축합니다.
Leapcell: 최고의 서버리스 웹 호스팅
마지막으로 Go 서비스를 배포하기 위한 최고의 플랫폼인 **Leapcell**을 추천합니다.
🚀 좋아하는 언어로 빌드
JavaScript, Python, Go 또는 Rust로 간편하게 개발하세요.
🌍 무제한 프로젝트를 무료로 배포
사용한 만큼만 지불하세요. 요청도 없고 요금도 없습니다.
⚡ 사용량에 따라 지불, 숨겨진 비용 없음
유휴 수수료 없이 원활한 확장성만 제공합니다.
🔹 Twitter에서 팔로우하세요: @LeapcellHQ