学祭でバラ積みロボットを作った話
(一応)部の活動としてバラ積みロボットを作ったので、解説のようなものを書きます。バラ積みロボットというのは次の動画のようなものです。
上の動画のようなものを目指して、出来上がったものがこれです。
ぎてふくんと2人で作成したのですが、私は取得した画像からアームを降ろす座標を指定する部分のみを担当し、ロボット部分はすべてぎてふくんに任せたのでこの記事では私の担当箇所(学習部分)のみを解説します。ロボット部分は既にぎてふくんがこちらで書いています。
なにをやりたかったか
次のようにプレートを2つ左右に並べて消しゴムを雑に入れ、この消しゴムをいずれかのプレートからアームで取って別のプレートに移すことを繰り返します。
消しゴムの両面の中央付近には画鋲が刺さっており、アームの先端に付いた電磁石で消しゴムを取ります。画鋲はカバーの下に刺さっているので、外見からはどこアームを下ろせば取れるかは分かりません。また、消しゴムの側面には画鋲が付いていないので、側面にアームを下ろしても消しゴムは取れません。
消しゴムが取れるようにアームを動かす座標をDNNで指定するのが目標です。入力はプレートを真上から撮ったRGB画像です。なぜRGBDではなかったかというと、一重にdepthカメラが高いからです。なので、z軸は下ろせるところまで下ろすようになっています。
私は自分で勝手に学習していくのが好きなので、self-supervisedに学習できるようにします。つまり、学習用データを手作業でラベリングしたりせず、自動で集められるようにします。
前処理
上から撮った画像は次のようになっており、アームの可動域と一致していません。そこで、アームの可動域に矩形にマスキングテープを貼り、矩形を検出して画像をcropします。ついでに歪みも直します。
歪みを直したあとの画像サイズは1000x348にしました。これはアームに指定できる範囲を各辺4倍にしたものです。
手法1
次の流れでCNNを学習させます。
- ランダムな座標を指定し、アームを下ろす
- 指定した座標を中心とした129x129の正方形領域を切り抜いて、消しゴムが取れたかどうかでラベリングする
- その画像を教師データとしてCNNを学習させる(二値分類)
学習が終わったら入力画像の各画素を中心とした129x129の正方形領域を入力として、中心の取れる確率を出力し、最も確率の高かった座標を指定します。出力の結果を入力画像にオーバーレイしたものが次の画像です。緑が濃い領域ほど取れる確率が高いと判断した場所です。
ネットワーク
ネットワークは「AlexNetの表現力があれば十分やろ!」などと言って青い本に載っているAlexNetをそのままパクり、FC層のセルの数だけ変えました。本当は論文でよく見るあの図を貼りたいのですが、作り方を知らないので(調べるのもめんどうなので)コードだけ貼っておきます。(正直例の図より分かりやすくないですか?) 使用フレームワークはPytorchです。
class AlexNet(nn.Module):
# for 129 x 129
def __init__(self):
super(AlexNet, self).__init__()
self.conv1 = nn.Conv2d(3, 96, 11, 3)
self.pool1 = nn.MaxPool2d(3, 2)
self.norm1 = nn.LocalResponseNorm(5, k=2)
self.conv2 = nn.Conv2d(96, 256, 5, 1, padding=2)
self.pool2 = nn.MaxPool2d(3, 2)
self.norm2 = nn.LocalResponseNorm(5, k=1)
self.conv3 = nn.Conv2d(256, 384, 3, 1, padding=1)
self.conv4 = nn.Conv2d(384, 384, 3, 1, padding=1)
self.conv5 = nn.Conv2d(384, 256, 3, 1, padding=1)
self.pool5 = nn.MaxPool2d(3, 2)
self.fc6 = nn.Linear(4096, 4096)
self.fc7 = nn.Linear(4096, 1028)
self.fc8 = nn.Linear(1028, 1)
self.dropout = nn.Dropout(p=0.5)
def forward(self, x):
x = self.norm1(self.pool1(F.relu(self.conv1(x))))
x = self.norm2(self.pool2(F.relu(self.conv2(x))))
x = F.relu(self.conv3(x))
x = F.relu(self.conv4(x))
x = self.pool5(F.relu(self.conv5(x)))
x = x.view(-1, 4096)
x = self.dropout(F.relu(self.fc6(x)))
x = self.dropout(F.relu(self.fc7(x)))
x = self.fc8(x)
return x
結果
このアプローチは概ね上手くいって、消しゴムが取れる状態でプレートにある時は8割以上(体感)は取ることができていました。一応統計は取っていたのですが、一度消しゴムが取れないと入力が変わらないので同じところにアームをおろし続けたり、その対処法が悪く消しゴムが存在しないプレートから取ったりしていたので、良いデータにはなりませんでした。ランダムの時のデータはちゃんと取れていて、192/(192 + 1712)で、およそ10%だったのでかなり良くなったことがわかります。(ただし、難易度が盤面にかなり依存するのがあまりよろしくない)
しかし、この手法は入力の各画素を中心点としてそれぞれをDNNに入力にするので、画像の大きさ(またはアームの精度)に比例した時間がかかってしまいます。プレート一つに対するアームの駆動領域が(200/2)x87、バッチ200、GeForce 1050tiでの実行で20秒程かかります。このままでは遅すぎるので、これを改善しようとしたのが手法2です。手法2は学祭には間に合わなかったので、学祭では手法1のまま展示していました。(当日の朝4時に手法1が完成したので)
これは当日朝4時にテストが上手くいってDMで一人盛り上がってる私
これは当日朝10時にバグが発覚している様子(展示は13時から)(私は現地にいない)(アームも手元にない)
結局私は16時くらいから2時間だけ参加した。自転車で行ったので寒かった。
余談
学祭で使用したモデルの学習用データは2000枚弱です。しかもランダムに集めたデータなので、失敗に分類されたデータの殆どには消しゴムが写っていません。なので上の画像のように消しゴムが写っていれば、その消しゴムが横になっていても反応してしまうわけです。そこで学祭で使用したモデルを使い、追加で1000枚弱のデータを集めて再度学習しました。すると次の画像のように横になってる消しゴムには反応しなくなりました。
上から10, 20, 3380 epochs学習時の画像です。 10 epochsのモデルでは滑らかな確率の分布(分布ではない)になっているのですが、20 epochsからは格子のような模様が現れています。過学習のような気もするのですが、val lossは下がっていたのでどうなんだろうという気持ちです。誰か教えてください。maxpoolのストライドやフィルターサイズを大きくすると良い?
これはlossのグラフ。validationに使った画像は同じです。accは正解率。32が学祭当日に使ったモデル。day4_2が再度学習したモデル。
手法2
手法2では精度を多少犠牲にしてでも高速化を目指します。具体的には画像全体を一度NNに通せばそれぞれの画素の確率が出るようにします。
次の手順でFCNNを学習します:
- 手法1で作成したモデルを使い、画像の各画素についての確率を求める。
- 1.で作成したデータを目標出力、元の画像を入力としてFCNNを学習する。
ネットワーク
https://berkeleyautomation.github.io/fcgqcnn/ を参考にしました。コードはこれ。
class FullyConvNet(nn.Module):
def __init__(self):
super(FullyConvNet, self).__init__()
self.conv1 = nn.Conv2d(3, 16, 9, padding=4)
self.conv2 = nn.Conv2d(16, 16, 5, padding=2)
self.pool2 = nn.MaxPool2d(5, 1, 2)
self.conv3 = nn.Conv2d(16, 16, 5, padding=2)
self.conv4 = nn.Conv2d(16, 16, 5, padding=2)
self.pool4 = nn.MaxPool2d(5, 1, 2)
self.conv5 = nn.Conv2d(16, 128, 19, padding=9)
self.conv6 = nn.Conv2d(128, 128, 1)
self.conv7 = nn.Conv2d(128, 1, 1)
def forward(self, x):
x = F.relu(self.conv1(x))
x = self.pool2(F.relu(self.conv2(x)))
x = F.relu(self.conv3(x))
x = self.pool4(F.relu(self.conv4(x)))
x = F.relu(self.conv5(x))
x = F.relu(self.conv6(x))
x = self.conv7(x)
結果
これはうまく行きませんでした。loss関数をL1, L2, KLD, softmaxにして試しましたがダメでした。L1, L2はlossは下がるが画像にオーバーレイしてみると明らかにうまくいっておらず、KLDはlossが下がらない、softmaxはlossは下がるがval lossは下がりませんでした。(というか、KLDは使い方を間違っていそう)。
L1、L2で学習したモデルの出力結果です。
L1
L2
反省
- 手法2はFCNNだから上手く行かなかった?FC層追加したら上手く行くのかな→メモリが破滅した
- ネットワークを深くすると良い?
- ネットワーク設計なんも分からん
- アームの精度が低くて辛かった(指定した箇所に降ろすのに結構苦労した)(ぎてふくん曰く買ったサーボモータが良くなかったらしい)
- というか、画像のcropと整形の精度がかなり辛かった(画像上の座標を固定した方が良くない?(ぎてふくん曰く良くないらいしい))
- 取った消しゴムが場外にぶっ飛んでいくなど環境が悪くて結局あまり放置できなかった すべての消しゴムが横になると詰む
- シミュレータを使って学習させてみたい
- シミュレータなら綺麗なデータが取れるので、ネットワークが悪いのかデータが悪いのかが切り分けできる(しかもはやい)
- 3日ある学祭のうち2時間しか参加しなかった
リポジトリ
参考
深層学習でバラ積みロボットの0から学習 今回やったのはほぼこれです(たぶん)(私の解釈が間違っていなければ)
On-Policy Dataset Synthesis for Learning Robot Grasping Policies Using Fully Convolutional Deep Networks 手法2で使ったネットワークの構成の参考にしました 論文は読んでない