Goでゼロから作るニューラルネットワーク：原理・構造・実装


[![Group251.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4075813/b7597efa-631d-487e-ac2b-13c6c71475de.png)](https://leapcell.io/)


**[Leapcell: ベストオブサーバレスウェブホスティング](https://leapcell.io/)**

# Go言語でニューラルネットワークをスクラッチから構築する：原理、構造、実装  

本稿では、Goプログラミング言語を用いて、シンプルなニューラルネットワークをスクラッチから構築する方法を紹介し、アイリス分類タスクを通じてその動作フローを実演します。原理説明、コード実装、視覚的な構造表示を組み合わせることで、読者がニューラルネットワークの核心メカニズムを理解できるよう支援します。  


## Ⅰ．ニューラルネットワークの基本原理と構造  
ニューラルネットワークは、生物のニューロンを模倣した計算モデルで、多層にわたるノードの接続によって非線形写像を実現します。典型的な3層ニューラルネットワークの構造には、入力層、中間層（隠れ層）、出力層が含まれます。各層のノードは重みとバイアスによって接続され、層間の伝達は活性化関数を介して処理されます。以下は、シンプルな3層ニューラルネットワークの構造模式図（ASCIIキャラクタで描いたもの）です：  

```bash
+-----------+     +-----------+     +-----------+
| 入力層     |     | 中間層     |     | 出力層     |
|   4ノード   |     |     3ノード     |     |     3ノード     |
+-----------+     +-----------+     +-----------+
    ↑              ↑              ↑
    │    重み     │    重み     │    重み     │
    ├───────────────┼───────────────┼───────────────┤
    ↓              ↓              ↓
+-----------+  +-----------+  +-----------+
|   バイアス  |  |   バイアス  |  |   バイアス  |
+-----------+  +-----------+  +-----------+
    ↓              ↓              ↓
+-----------+  +-----------+  +-----------+
| 活性化関数|  | 活性化関数|  | 活性化関数|
|    (σ)    |  |    (σ)    |  |    (σ)    |
+-----------+  +-----------+  +-----------+
```  

### 核心概念：  
1. **順伝播（Forward Propagation）**  
   入力データは、重み行列を用いた線形変換（`入力 × 重み + バイアス`）を経た後、活性化関数によって非線形性が導入され、層ごとに出力層まで伝播します。  
   **式例**：  
   - 中間層入力：\( 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) \)  

2. **逆伝播（Backpropagation）**  
   予測値と真値の誤差（例：二乗平均誤差）を計算し、連鎖律を用いて各層の重みとバイアスを逆方向に更新することで、モデルパラメータを最適化します。  
   **キーステップ**：  
   - 出力誤差計算：\( \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`パッケージを用いて行列演算を行い、ネットワークの構造とパラメータを定義します：  

```go
// neuralNetは学習済みのニューラルネットワークパラメータを格納する
type neuralNet struct {
    config   neuralNetConfig // ネットワーク構成
    wHidden  *mat.Dense       // 中間層の重み行列
    bHidden  *mat.Dense       // 中間層のバイアスベクトル
    wOut     *mat.Dense       // 出力層の重み行列
    bOut     *mat.Dense       // 出力層のバイアスベクトル
}

// neuralNetConfigはネットワークのアーキテクチャと学習パラメータを定義する
type neuralNetConfig struct {
    inputNeurons  int     // 入力層のノード数（例：アイリスの4つの特徴量）
    outputNeurons int     // 出力層のノード数（例：アイリスの3種類）
    hiddenNeurons int     // 中間層のノード数（チューニング可能なハイパーパラメータ）
    numEpochs     int     // 学習エポック数
    learningRate  float64 // 学習率
}
```  

### 2. 活性化関数とその微分  
活性化関数にシグモイド関数を採用します。この関数の微分は関数値に基づいて高速に計算できるため、逆伝播に適しています：  

```go
// 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. 逆伝播学習ロジック  
#### パラメータの初期化  
重みとバイアスを乱数で初期化し、ネットワークが学習できるようにします：  

```go
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`メソッドを用いて活性化関数と微分をバッチ処理しています：  

```go
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 = 誤差 * σ'(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. 順伝播予測関数  
学習後、学習済みの重みとバイアスを用いて順伝播し、予測結果を出力します：  

```go
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種類の品種（セトサ、バージカラ、バージニカ）が含まれています。データ前処理のステップ：  
- 品種ラベルをワンホットエンコーディングに変換。例：セトサは`[1, 0, 0]`、バージニカは`[0, 1, 0]`、バージカラは`[0, 0, 1]`に対応します。  
- データの80%を訓練セット、20%をテストセットに分割し、学習の難易度を上げるために小さなランダムノイズを追加します。  
- サンプルデータ（`train.csv`の抜粋）：  
  ```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. メインプログラムのフロー  
#### データの読み取りと行列への変換  
```go
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の読み取りに失敗しまし")
    }
    
    // データを入力特徴量(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)
}
```

#### ネットワークパラメータの設定と学習
```go
// ネットワーク構造の定義: 4入力、5中間ノード、3出力
config := neuralNetConfig{
    inputNeurons:  4,
    outputNeurons: 3,
    hiddenNeurons: 5, // 中間層ノード数を5に設定
    numEpochs:     8000, // 8000エポック学習
    learningRate:  0.2,  // 学習率
}

network := newNetwork(config)
if err := network.train(inputs, labels); err != nil {
    log.Fatalf("学習に失敗しました: %v", err)
}
```

#### モデル精度のテスト
```go
// テストデータを読み取り予測
predictions, err := network.predict(testInputs) // 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) // 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%%\n", float64(trueCount)/float64(numPreds)*100)
```


## Ⅳ．実験結果とまとめ  
8000回の学習エポック後、モデルはテストセットで約**98%**の分類精度を達成しました（乱数初期化の影響により若干のばらつきがあります）。これは、シンプルな3層ニューラルネットワークでも非線形分類問題を効果的に解決できることを示しています。  

### 核心的なメリット：  
- **純粋Go実装**：C拡張（`cgo`）に依存せず、静的バイナリファイルにコンパイル可能で、クロスプラットフォームデプロイに適しています。  
- **行列抽象化**：`gonum/mat`パッケージに基づく数値計算で、コード構造が明瞭で拡張性に優れています。  

### 改良の方向性：  
- 異なる活性化関数（例：ReLU）や最適化手法（例：Adam）を実験する。  
- 正則化（例：L2正則化）を追加して過学習を防ぐ。  
- 複数の中間層をサポートし、より深いニューラルネットワークを構築する。  


# [Leapcell: ベストオブサーバレスウェブホスティング](https://leapcell.io/)  

最後に、Goサービスのデプロイに最適なプラットフォーム**[Leapcell](https://leapcell.io/)** をご紹介します。  

[![brandpic7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4075813/d399865c-6330-4008-a2b0-28569ba4d86e.png)](https://leapcell.io/)


### 🚀 好きな言語で開発  
JavaScript、Python、Go、Rustで気軽に開発できます。  

### 🌍 無料で無制限のプロジェクトをデプロイ  
使用分のみ課金（リクエストなしで無料）のため、無駄な支出がありません。  

### ⚡ 使った分だけ請求、隠れた料金なし  
アイドル料金は一切なく、シームレスにスケーリングできます。  

[![Frame3-withpadding2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4075813/14e1f5a7-3794-4fa1-b017-f4e2c0ca3398.png)](https://leapcell.io/)


[📖 ドキュメントを確認する](https://docs.leapcell.io/)  

🔹 Twitterでフォローして最新情報をゲット：[@LeapcellHQ](https://x.com/LeapcellHQ)