1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

Group251.png

Leapcell: ベストオブサーバレスウェブホスティング

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

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

Ⅰ.ニューラルネットワークの基本原理と構造

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

+-----------+     +-----------+     +-----------+
| 入力層     |     | 中間層     |     | 出力層     |
|   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パッケージを用いて行列演算を行い、ネットワークの構造とパラメータを定義します:

// 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. 活性化関数とその微分

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

// 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 = 誤差 * σ'(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種類の品種(セトサ、バージカラ、バージニカ)が含まれています。データ前処理のステップ:

  • 品種ラベルをワンホットエンコーディングに変換。例:セトサは[1, 0, 0]、バージニカは[0, 1, 0]、バージカラは[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の読み取りに失敗しまし")
    }
    
    // データを入力特徴量(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入力、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)
}

モデル精度のテスト

// テストデータを読み取り予測
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: ベストオブサーバレスウェブホスティング

最後に、Goサービスのデプロイに最適なプラットフォーム**Leapcell** をご紹介します。

brandpic7.png

🚀 好きな言語で開発

JavaScript、Python、Go、Rustで気軽に開発できます。

🌍 無料で無制限のプロジェクトをデプロイ

使用分のみ課金(リクエストなしで無料)のため、無駄な支出がありません。

⚡ 使った分だけ請求、隠れた料金なし

アイドル料金は一切なく、シームレスにスケーリングできます。

Frame3-withpadding2x.png

📖 ドキュメントを確認する

🔹 Twitterでフォローして最新情報をゲット:@LeapcellHQ

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?