2026.03.12 08:09
CNN見直し
・なんだかCNNの方にいろいろ手を入れていたらぐちゃぐちゃになってきたのでもう一回整理。フォワードのときとバックワードの時で共通して持っておくべきもの、持っておいたほうが良いものがあれこれあるというところ。とりあえず、行列の扱いもだいぶ慣れてきたかな。
・で、ちょっと書いたものを試しにGeminiに投げてみたら「コメントでこう書いてあるけど・・・」というご指摘。そこまで読むか。
・それにしてもこのLLMというのは一体どういう構造をしているのだろうな。
2026.03.11 19:39
トラ技の付録
・あの日からもう15年。そうだな。あまり備えはしていないけど、とりあえず先日、非常用のトイレはそこそこの回数分備えておくことにした。
・先日のことだけど、トラ技の今月号が届いた。今月の付録が「エンジニア手帳2026」これは結構便利そうだ。オペアンプを使った基本的なフィルタからよく使うモジュール類のピン配置、基板のパターン設計関係の資料などなど、とにかく盛りだくさん。
2026.03.10 17:05
CNN+全結合
・なんとなくできた感じなので、とりあえずCNNを2段、その後全結合層を2段という構成にして、まずはForward側。テストパターンを突っ込んで出力を眺めるとなんとなくそれっぽく動いていそうな感じもするけど。
・というところで、続きをやろうと思っていたのだけど、なんとなく眠気が勝ってしまって気がついたら日付が変わる寸前。ちゃんと寝よう。
2026.03.09 22:51
一応forward側は動いている・・・かな?
・で、プーリング層のバックプロパゲーション部分も書く。こちらはさほどややこしくはない。入力されたものをnp.repeatして引き伸ばしてフォワードのときに作ったマスクパターンと掛け算すれば良い。
・とりあえずテストしようということで、簡単な8x8の縦横パターンを作ってやってみたら変なところでエラー。
・なんかエラーメッセージがよくわからないので、一旦コメントアウトし削ったものを復活させながらチェックしたら、思わぬところで変なミスをしていた。
・というところで気を取り直して改めてテストパターンを入れてテスト。なんとなくそれっぽくなってきたけど、さすがに元画像が8x8だとプーリングしてしまうと小さくなりすぎる。
・仕方ないので、np.repeatして拡張してから実行。ついでなのでnp.padでパディングして畳み込みした後に小さくならないように拡張しておく。
・さて、なんとなくそれっぽい結果にもなったことだし。明日は2層にしてみるか。
2026.03.08 22:09
プーリング処理を書いてみる
・昨日、「ただ、全部ゼロだったらまずいので、そこは避けないとだめだな。
」なんて書いたけど、そもそも「最大値の位置が1」という条件なんだから、全部0ということはありえない。ということに、寝入りばなに気がついた。なんだかボケてるな。
・ということで、プーリング処理をコーディング。
2026.03.07 23:30
プーリング層のコーディング方針を整理
・プーリング層を考えることに。こちらのフォワード側は要するに2x2なり3x3といった領域に分割して、その領域を代表する値を作る。
・最もよく使われるのは最大値をとるMaxプーリングと呼ばれる手法らしい。
・たとえば2x2の領域ごとに分割してMaxプーリングするとき、入力が
1 2 3 4
4 8 6 7
7 3 9 2
4 8 3 7
という具合になっていれば出力は
8 7
8 9
となる。これはnp.maxを使うと割と簡単に抽出できる。
・入力は畳み込み層の出力なので、構造は[バッチサイズ,フィルタ数、 Xサイズ、Yサイズ]な4次元配列なので、ここからプーリングサイズ分切り出して
xmax=np.max(x,axis=(2,3),keepdims=True)
して4次元配列まま最大値位置を取り出す。
・で、バックプロパゲーションのためには最大値がどの場所だったのかも記録しないといけない。こちらは
mask=np.where(x==xmax,1,0)
で一発でいける。めったに無いけど、運悪く全く同じ値になる場所が複数あったときには責任を分け合ったほうが良いのだろうか。もしやるなら、
mask=np.where(x==xmax,1,0)/np.sum(xmax,axis=(2,3),keepdims=True)
で良さそうだ。
・ただ、全部ゼロだったらまずいので、そこは避けないとだめだな。
・あとはバックプロパゲーションするときには戻ってきた行列(仮にerr_inとする)との乗算、つまり
err_in*mask
をしてやれば、NumPyが勝手にブロードキャストしてくれるので、該当箇所だけ値が戻る形にしてくれる。
・…というのが、どうやらプーリング層の実装になるらしい。明日暇があったらコーディングして完成させよう。
・しかし、CNNってやたらと時間かかりそうだな。やはりもっとC/C++なりで作成された高水準ライブラリを使わないと駄目かもしれないな。
・とりあえず実際にMNIST認識をやらせてみてからPyTorchを使う方向も考えよう。
2026.03.06 23:46
バックプロパゲーションもなんとか書けた
・Chromeに脆弱性があったとかでアップデート
・いつものことながら自動ではうまくアップデートしてくれないので、.debパッケージをダウンロードしてsudo apt installでインストール。最後になんかエラーっぽいメッセージは出たけど、Chrome再起動したら「バージョン 145.0.7632.159(公式ビルド) (64 ビット)」となっていたので、たぶん良いのだろう。
・ということで、CNNの続きでバックプロパゲーションを書く。
・行数にすればわずか数行の演算なのだけど、やりたいことをどう書いたら良いのかという感じ。ウェイトやバイアスはまだ良いけど、入力に戻す誤差成分を算出するのはちょっと面倒だった。
・行列の形でいうと、バックプロパゲーションのときに後段から戻ってくる誤差データは[バッチサイズ、出力チャンネル数(フィルタ数)、Yサイズ、Xサイズ]な4次元配列データ。バッチサイズというのは、一度に複数の画像データをまとめて処理するときの画像の枚数。フィルタはたとえば3x3といった小さい領域に対する重みデータで、これが元画像から切り出した同じサイズ(3x3)のデータと積和演算されているわけだ。
・元画像側がカラー画像だと1つの画像がRGBの3枚で形成されている。これが入力チャンネル。
・で、ごちゃごちゃしたけど、1つのニューロンは
・入力:9つ(3x3)の画素データがチャンネル数分
フィルタ(3x3の重みデータ)がチャンネル数分
バイアス値がチャンネル数分
・出力:画素の各位置と対応するフィルタの値を掛けて全部足す、さらにバイアス値も足す
ということをやっていて、これが平面にズラーッと並んだものがあり、更にこれがフィルタ数分あるという感じ。
・重み(フィルタ)
weight[output_ch_num, input_ch_num, fil_xsize, fil_ysize]
・誤差データ
err_in[Bach_Size, output_ch_num, xsize, ysize]
ということで、こいつらの積和ってことなので、まずは掛け算するために次元を揃える。err_in側は1点ずつ(ニューロン1個分ずつ)切り出すので、i,jをy方向、x方向のスキャンとして、
weight_reshape = weight.reshape(1,output_ch_num, input_ch_num, fil_xsize, fil_ysize)
err_reshape = err_in[:,:,i:i+1,j:j+1].reshape(batch_size,output_ch_num,1,1,1)
で、あとは出力チャンネル数方向で積和をとってやる(複数のフィルタをかけたものを通したものからそれぞれ入力に誤差分が戻って合流する感じ)
np.sum(weight_reshape*err_reshape, axis=1)
入力データは[batch_size, input_ch_num, input_xsize, input_ysize]みたいな感じで4次元。ここからフィルタサイズ分切り出したところに足せば良いんだから、
d_input[:,:,i:i+fil_xsize, j:j+fil_ysize] = np.sum(weight_reshape*err_reshape, axis=1)
何ていう感じ。わずか数行だけど、ここにたどり着くのは少々しんどかったよと。
・他の部分もまとめてなんとかバックプロパゲーションは書けた(と思う)
・一応Gemini君は「完璧」と言ってくれたけど、本当にちゃんと動くのかな?
2026.03.05 18:13
CNNのバックプロパゲーションが見えてきた&全結合の修正で認識率96%
・というところで、バックプロパゲーションを・・・と思って書き出そうとしたらいきなりVSCode君がコードを吐き出してくれた。たぶんCoPilotが生成したんだろう。
・なんとなく面白いけどねと思いながら、それを下に置いて1行ずつ自分で改めて書きながら読むということをしていたのだけど、途中でわけがわからなくなってきた。
・ということで、困った時のいつものGCC・・・この手のはGeminiも得意だよねということで質問していたら、なんか話が噛み合わない。
・で、ある程度まとまったコードブロックを提示したら、「それ、変だよ」と指摘してきた。
・いちいち指摘が的確というのか、「なんでこの値を掛けてるんだ?」と疑問に思っていた部分がやはり間違っている。
・「CoPilotが自動生成したんだけどね」といったら「あはは・・そうでしたか」ときて、CoPilotに対してちょっと「あいつのコード生成はね・・・」的な指摘をしてくるのがまた可笑しい。
・というところで、じっと考えて整理できてきた。わかってしまえばそれほど難しくは無いのだけど、バッチサイズ分まとめてたり、フィルタ数分まとめて考えていたりというのを一度に考えると頭の中で行列がごちゃごちゃしてややこしいのだな。
・そんなこんなでやっていたら、全結合の方で軽い勘違いを見つけた。まぁ、2層程度なら問題ないんだけど、バックプロパゲーションで前段に戻すときにもLR(Learning Rate)を掛け算してしまっていたという凡ミス。
・これを修正してLRを更に小さくして(でないと、途中でオーバーフローしやすい)再度全結合でMNISTしてみたら正答率96%となった。うん、標準的な値とされる範囲になったな。
2026.03.04 17:46
とりあえず順方向は良いかな
・ぼちぼちとpythonで書く。四次元配列に混乱しそうになりながらとりあえずCNNクラスの__init__()と順方向はできたかな?
・バックプロパゲーションがちょっと難しい。というのか、4次元の配列を眺めているとどうも頭が混乱しそうになる。
・もう一回ちょっと整理しよう
2026.03.03 23:41
CNNの仕組みがだいぶ見えた・・・かな
・さて、GCC(Gemini/Chatwork/Copilot)君に書かせたコードを眺めていると、フィルタの枚数やX,Yのサイズなどの他にChannelというパラメータがある。何だこれ?ということでお尋ねしたら例えばカラー画像データのRGBのようなものとのこと。なるほどね。
・たとえばRGBであればそれぞれについてフィルタをかけてやって、RGBの三枚分のデータを全部足し算するという流れらしい。
・PyTorchなどのライブラリを使えば楽できるらしいけど、今回はNumPyだけでやるので地道にループさせる。
・で、もう一つ。出力側にもやはりチャンネルがある。画素ごとの重みをつけるフィルタの構造は
filter[出力チャンネル数、入力チャンネル数、Yサイズ、Xサイズ]
という4次元配列になる。たとえば、入力がRGBの3チャンネルあって、フィルタのサイズが5x5ならば
filter[出力チャンネル数,3,5,5]
つまり、入力チャンネル数×出力チャンネル数分のフィルタがあるということ。これはつまり、入力画像の1つのチャンネルの一つずつ(たとえばRGBならばRの画像など)についてN個のチャンネル(つまりN個のフィルタ)があるという具合。
・要するに演算の考えとしては1つのニューロンが元画像の3x3なり5x5なりといった狭い領域について全結合ニューラルネットと同じような積和演算をしていて、これがズラッと並んでいる(元画像の1ドットずつシフトしながら)。
・外から見るとちょうど元画像を底面としたピラミッドがたくさん重なり合いながら並んでいる感じで、これが入力チャンネル数×出力チャンネル数個あるわけだ。
・全結合のように、このニューロンすべてが独立したウェイトとバイアスをもたせるという手もあるけど、そこを節約して同じ出力チャンネルに属するニューロンのウェイトやバイアスは共通で使うことにした。それをフィルタと称している・・・とまぁそんな感じで解釈できるのだろう。
そんなことをすると要求されるメモリ量が膨大なものになってしまう。そこで同じチャンネルのニューロン(要するに一つのピラミッド群)
のウェイトやバイアスは同じものを使うことにしていると思えば良いのだろう。
・
3x3とか5x5とかのパターンを使いまわしているという点
全結合ニューラルネットの1層目なんかと同じようなものと思えば良いのかな?と
イメージ的には全結合のときに同じ入力に対してズラッとニューロンが並ぶようなのと似ていて、同じフィルタをかけたものを
・