前回記事の多層パーセプトロンに続いて、
今回は畳み込みニューラルネットワークを
ゼロから作っていきましょう!
いっぺんに書くと長くなってしまうので、
この記事では特に畳み込み層にフォーカスします。
畳み込み層の概要
まずは畳み込み層の確認をしておきましょう。
素直に実装してありますので、
計算の流れを細かいところまで理解してあれば、
簡単にコードは落とし込めると思います。
畳み込み層は入力に対して、
フィルタ(カーネル)を掛けていきます。
入力とフィルタを1マスづつ順に掛けて総和をとります。
「掛けて総和をとる」という処理単位は、
フィルタサイズ分だけ行い、その後フィルタ自体を少しズラします。
このズラす幅のことをストライドと言い、
画像いっぱいまでズラしたら計算終了です。
もしここでフィルタをズラしていったときに、
画像サイズと合わずに中途半端なところで終わる場合は、
計算の前にパッディングしておきます。
フィルタの計算を終えたら、
最後にバイアスを足して出力を得ます。
畳み込み層のイメージはこんな感じです。

入力には画像が例に使われますが、
その画像がグレースケールではなく、
もしRGBで表現されていた場合、
入力はRGB×縦×横の3次元になります。
ここで実装において注意が必要なのが、
用意するフィルタはRGBそれぞれについて考えます。
例えばフィルタ”群”を3つ用意するとした場合、
実際には9つのフィルタを作る必要があります。
これは1つのフィルタ”群”にそれぞれ、
RGBに対応するフィルタがあるからです。
そもそもフィルタは特徴に反応してほしいものですので、
RGBのRだけに反応してほしい場合だってあるわけです。
「エッジに反応するフィルタ群があって、
その中でもRのエッジに反応してほしい」
フィルタを通した出力(値)は、
Rだけ0以外の値でGBの値はゼロ、
というようなイメージです。
以下がコードの説明になります。
畳み込み層をGo言語でゼロから作ってみる
コードは説明のために分けてありますが、
全てコピペして単純に1つにまとめれば動きます。
畳み込み層への入力はレナさんを使います。

まずは定義まわりのコードです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
package main import ( "errors" "fmt" "image" "image/color" "image/jpeg" "image/png" "log" "math" "math/rand" "os" "path/filepath" "strconv" "strings" ) const ( name = "000.jpg" ) type Tensor3 struct { index int row int column int ten []float64 } type Matrix struct { row int column int mat []float64 } type Kernel struct { f Tensor3 //filter b Tensor3 //bias st int //stride } |
3階テンソル、行列、カーネルの構造体を定義しています。
カーネル構造体で、畳み込み層の肝をまとめています。
次はmain文です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
func main() { imgData, err := OpenImg(name) if err != nil { log.Fatal("err") } numFilter := 3 f := GetFilters(numFilter*imgData.index, 5, 5) //the number of filters * h * w b := GetBias(numFilter, 0, 10) //min to max ker := KernelInit(f, b, 2) output := ker.Convol(ker.DoPadding(imgData)) output.Activate("LeaklyReLu") fmt.Println(output) } |
チャネルごとにフィルターを用意するのは、
rgb別の細かいところで特徴を抽出するためです。
続いて入力画像を取得します。
レナさんを入力出来るように
画像データを配列に落とし込みます。
標準のimageパッケージを用います。
画像はpngもしくはjpegのみ受け付けます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
func OpenImg(name string) (*Tensor3, error) { f, err := os.Open(name) if err != nil { return &Tensor3{}, err } defer f.Close() var img image.Image switch strings.ToLower(filepath.Ext(name)) { case ".png": img, err = png.Decode(f) if err != nil { return &Tensor3{}, err } case ".jpg", ".jpeg": img, err = jpeg.Decode(f) if err != nil { log.Println(err) return &Tensor3{}, err } default: return &Tensor3{}, errors.New("Wrong Extention!") } return imgToPixels(img), nil } func imgToPixels(img image.Image) *Tensor3 { b := img.Bounds() height := b.Max.Y width := b.Max.X rgbs := tensor3Init(3, height, width, 0) for y := 0; y < height; y++ { for x := 0; x < width; x++ { r, g, b, _ := img.At(x, y).RGBA() rgbs.ten[y*width+x] = float64(r >> 8) rgbs.ten[height*width+y*width+x] = float64(g >> 8) rgbs.ten[2*height*width+y*width+x] = float64(b >> 8) } } return &rgbs } |
画像をデコードして得られるimage型を
imgToPixels関数で3次元配列に変えています。
img.At(x, y).RGBA()で取得できる値はuint32で、
8バイトRGB値を各々得るには、
コードのようにシフト演算します。
続いてカーネル構造体の設定です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
func GetFilters(num, h, w int) Tensor3 { return tensor3InitRand(num, h, w) } func GetBias(nf int, min, max float64) Tensor3 { return tensor3Init(nf, 1, 1, random(min, max)) } func KernelInit(filter, bias Tensor3, stride int) *Kernel { return &Kernel{ f: filter, b: bias, st: stride, } } |
続いて数学関連の関数です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
func matrixInit(m, n int, v float64) Matrix { var matrix = Matrix{ row: m, column: n, mat: make([]float64, n*m), } for i := 0; i < len(matrix.mat); i++ { matrix.mat[i] = v } return matrix } func tensor3Init(l, m, n int, v float64) Tensor3 { var tensor = Tensor3{ index: l, row: m, column: n, ten: make([]float64, l*m*n), } for i := 0; i < len(tensor.ten); i++ { tensor.ten[i] = v } return tensor } func tensor3InitRand(l, m, n int) Tensor3 { var tensor = Tensor3{ index: l, row: m, column: n, ten: make([]float64, l*m*n), } for i := 0; i < len(tensor.ten); i++ { tensor.ten[i] = random(0, 1) } return tensor } func random(a, b float64) float64 { return (b-a)*rand.Float64() + a } |
続いてパディングするためのメソッドです。
最初にパディングする必要があるかを判断し、
必要なければそのまま戻り、
必要があれば最小限のパディングを行います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
func (k *Kernel) DoPadding(data *Tensor3) *Tensor3 { imgH := data.row imgW := data.column padH := (imgH - k.f.row) % k.st padW := (imgW - k.f.column) % k.st if padH == 0 && padW == 0 { return data } else { pData := tensor3Init(data.index, imgH+padH, imgW+padW, 0) startH := int(math.Floor(float64(padH) / 2)) startW := int(math.Floor(float64(padW) / 2)) for i := 0; i < pData.index; i++ { for y := startH; y < data.row+startH; y++ { for x := startW; x < data.column+startW; x++ { pData.ten[i*pData.row*pData.column+y*pData.column+x] = data.ten[i*data.row*data.column+(y-startH)*data.column+(x-startW)] } } } return &pData } } |
img縦(横)とフィルタ縦(横)の差を
ストライドで割り切れなければパディングします。
パディングを左右(上下)均等にセット出来れば、
左右(上下)に均一のサイズだけパディングし、
もし不均一になるようであれば、
右(下)側がより多くなるようにセットします。
続いて実際の畳み込み計算のメソッドです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
func (k *Kernel) Convol(data *Tensor3) *Tensor3 { outH := (data.row-k.f.row)/k.st + 1 outW := (data.column-k.f.column)/k.st + 1 output := tensor3Init(k.f.index/data.index, outH, outW, 0) fmt.Println(outH) fmt.Println(outW) oneKernelSize := k.f.row * k.f.column oneOutputSize := outH * outW var tmp float64 for nf := 0; nf < k.f.index/data.index; nf++ { //the index of filters for rgb := 0; rgb < data.index; rgb++ { for y := 0; y < data.row-k.f.row; y += k.st { for x := 0; x < data.column-k.f.column; x += k.st { tmp = 0 for m := 0; m < k.f.row; m++ { for n := 0; n < k.f.column; n++ { tmp += data.ten[rgb*data.row*data.column+(y+m)*data.column+x+n] * k.f.ten[nf*data.index*oneKernelSize+rgb*oneKernelSize+m*k.f.column+n] } } output.ten[nf*oneOutputSize+y/k.st*output.column+x/k.st] += tmp + k.b.ten[nf] //rgbの各チャネルにおいて、同一のポジションは和をとる。 } } } } return &output } |
フィルタを掛ける前にパディング済みなので、
計算は画像の隅から隅まで出来るはずです。
最後に活性化関数に関するメソッドです。
とりあえずReLu、LeaklyReLu、Tanhだけ用意しました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
func (t *Tensor3) Activate(funcName string) { switch funcName { case "Relu": for i := 0; i < len(t.ten); i++ { t.ten[i] = reLu(t.ten[i]) } case "LeaklyReLu": for i := 0; i < len(t.ten); i++ { t.ten[i] = reLu(t.ten[i]) } case "Tanh": for i := 0; i < len(t.ten); i++ { t.ten[i] = math.Tanh(t.ten[i]) } case "none": default: } } func reLu(x float64) float64 { if x < 0 { return 0 } return x } func leaklyReLu(x float64) float64 { if x < 0 { return 0.01 * x } return x } |
最後まで読んでいただきありがとうございます。