Go言語でニューラルネットワークをスクラッチから構築する:原理、構造、実装
本稿では、Goプログラミング言語を用いて、シンプルなニューラルネットワークをスクラッチから構築する方法を紹介し、アイリス分類タスクを通じてその動作フローを実演します。原理説明、コード実装、視覚的な構造表示を組み合わせることで、読者がニューラルネットワークの核心メカニズムを理解できるよう支援します。
Ⅰ.ニューラルネットワークの基本原理と構造
ニューラルネットワークは、生物のニューロンを模倣した計算モデルで、多層にわたるノードの接続によって非線形写像を実現します。典型的な3層ニューラルネットワークの構造には、入力層、中間層(隠れ層)、出力層が含まれます。各層のノードは重みとバイアスによって接続され、層間の伝達は活性化関数を介して処理されます。以下は、シンプルな3層ニューラルネットワークの構造模式図(ASCIIキャラクタで描いたもの)です:
+-----------+ +-----------+ +-----------+
| 入力層 | | 中間層 | | 出力層 |
| 4ノード | | 3ノード | | 3ノード |
+-----------+ +-----------+ +-----------+
↑ ↑ ↑
│ 重み │ 重み │ 重み │
├───────────────┼───────────────┼───────────────┤
↓ ↓ ↓
+-----------+ +-----------+ +-----------+
| バイアス | | バイアス | | バイアス |
+-----------+ +-----------+ +-----------+
↓ ↓ ↓
+-----------+ +-----------+ +-----------+
| 活性化関数| | 活性化関数| | 活性化関数|
| (σ) | | (σ) | | (σ) |
+-----------+ +-----------+ +-----------+
核心概念:
-
順伝播(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) )
-
逆伝播(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** をご紹介します。
🚀 好きな言語で開発
JavaScript、Python、Go、Rustで気軽に開発できます。
🌍 無料で無制限のプロジェクトをデプロイ
使用分のみ課金(リクエストなしで無料)のため、無駄な支出がありません。
⚡ 使った分だけ請求、隠れた料金なし
アイドル料金は一切なく、シームレスにスケーリングできます。
🔹 Twitterでフォローして最新情報をゲット:@LeapcellHQ