電子工作 4作目・Corne Cherry

f:id:sugyan:20181214120545j:plain

HelixPico、Mint60、ErgoDash に続いて、自作キーボード 4作目。

今回作ったのは Corne Cherry。 この記事はCorne Cherryを使って書いています。

booth.pm

なぜ Corne Cherry を選んだか

これまで3種類ほど作ってきて、Ortholinear(格子配列)、Row-Staggered(横にズレている)、Column-Staggered(縦にズレている) とそれぞれ使ってみて やはりColumn-Staggeredが良さそう、と思い しかしErgoDashはちょっとキーが多すぎて絶対に使わないものも存在していてアレだし、あとGateron Brownのキースイッチ使ってみたけどちょっと打鍵音があって職場とかで使いづらいかな…と思って家での使用のみにしていた。

その点 Corne Cherryは

  • Column-Staggered 配列で キーが42個と少なめでコンパクト
    • 自分的には理想のキー数 & 配置
  • Kailh PCBソケットでキースイッチのホットスワップが可能

というあたりが魅力。

組み立て

ひたすら Build Guide とにらめっこしながら間違わないよう気をつけて丁寧に。

f:id:sugyan:20181206184131j:plain f:id:sugyan:20181211191441j:plain f:id:sugyan:20181214015009j:plain

ダイオードがチップ型で 表面実装と呼ぶもの? は初体験だったので難しかった。というか部品が小さすぎてすごい神経つかうし おじさんにはつらい…。 やっぱりこのへんになってくると フラックスというものが必要になってきそう。

あとは LEDもオプションながらパーツ買ってしまっていたので折角だから挑戦してみた。 が、見事に敗北。

つけたり外したりしながら何度かチャレンジして ようやく3つだけ光らせることができた……

4番目の場所のを付け直そうとして外してるときにランドの方まで剥がれてしまい 修復が困難そうだったので まぁ別に光らなくてもいいし… ってことで裏面の3つだけで終了とした。

キースイッチ・キーキャップ

今回は キースイッチは "Gateron MX Silent Switch Brown" という静かなタイプのもの (勿論あとから付け替えることも出来るのだけど)、キーキャップはErgoDashでDSAのものを使っていたけどそれほど好みでもなかったので XDAのものにしてみた。

talpkeyboard.stores.jp

talpkeyboard.stores.jp

キーマップ

これは以前から考えていた通りに自分にとって最適のものを設定。

--- keyboards/crkbd/keymaps/default/keymap.c    2018-12-14 02:21:01.000000000 +0900
+++ keyboards/crkbd/keymaps/sugyan/keymap.c     2018-12-15 17:24:59.000000000 +0900
@@ -60,37 +60,37 @@
 const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
   [_QWERTY] = LAYOUT_kc( \
   //,-----------------------------------------.                ,-----------------------------------------.
-        ESC,     Q,     W,     E,     R,     T,                      Y,     U,     I,     O,     P,  BSPC,\
+        TAB,     Q,     W,     E,     R,     T,                      Y,     U,     I,     O,     P,  BSLS,\
   //|------+------+------+------+------+------|                |------+------+------+------+------+------|
-      CTLTB,     A,     S,     D,     F,     G,                      H,     J,     K,     L,  SCLN,  QUOT,\
+       LCTL,     A,     S,     D,     F,     G,                      H,     J,     K,     L,  SCLN,  QUOT,\
   //|------+------+------+------+------+------|                |------+------+------+------+------+------|
-       LSFT,     Z,     X,     C,     V,     B,                      N,     M,  COMM,   DOT,  SLSH,  RSFT,\
+       LSFT,     Z,     X,     C,     V,     B,                      N,     M,  COMM,   DOT,  SLSH,   ENT,\
   //|------+------+------+------+------+------+------|  |------+------+------+------+------+------+------|
-                                  GUIEI, LOWER,   SPC,      ENT, RAISE, ALTKN \
+                                   LALT,  LGUI,   SPC,     RSFT, LOWER, RAISE \
                               //`--------------------'  `--------------------'
   ),

   [_LOWER] = LAYOUT_kc( \
   //,-----------------------------------------.                ,-----------------------------------------.
-        ESC,     1,     2,     3,     4,     5,                      6,     7,     8,     9,     0,  BSPC,\
+        ESC,  EXLM,    AT,  HASH,   DLR,  PERC,                   CIRC,  AMPR,  ASTR,  LPRN,  RPRN,   DEL,\
   //|------+------+------+------+------+------|                |------+------+------+------+------+------|
-      CTLTB,    F1,    F2,    F3,    F4,    F5,                     F6,    F7,    F8,    F9,   F10, XXXXX,\
+       LCTL,  UNDS,  PLUS,  LCBR,  RCBR,  TILD,                    GRV,  MINS,   EQL,  LBRC,  RBRC, XXXXX,\
   //|------+------+------+------+------+------|                |------+------+------+------+------+------|
-       LSFT,   F11,   F12,   F13,   F14,   F15,                    F16,   F17,   F18,   F19,   F20, XXXXX,\
+       LSFT,     1,     2,     3,     4,     5,                      6,     7,     8,     9,     0, XXXXX,\
   //|------+------+------+------+------+------+------|  |------+------+------+------+------+------+------|
-                                  GUIEI, LOWER,   SPC,      ENT, RAISE, ALTKN \
+                                   LALT,  LGUI,   SPC,     RSFT, LOWER, RAISE \
                               //`--------------------'  `--------------------'
   ),

   [_RAISE] = LAYOUT_kc( \
   //,-----------------------------------------.                ,-----------------------------------------.
-        ESC,  EXLM,    AT,  HASH,   DLR,  PERC,                   CIRC,  AMPR,  ASTR,  LPRN,  RPRN,  BSPC,\
+      XXXXX,    F1,    F2,    F3,    F4,    F5,                     F6,    F7,    F8,    F9,   F10, XXXXX,\
   //|------+------+------+------+------+------|                |------+------+------+------+------+------|
-      CTLTB, XXXXX, XXXXX, XXXXX, XXXXX, XXXXX,                   MINS,   EQL,  LCBR,  RCBR,  PIPE,   GRV,\
+       LTOG, XXXXX, XXXXX, XXXXX, XXXXX, XXXXX,                   LEFT,  DOWN,    UP,  RGHT, XXXXX, XXXXX,\
   //|------+------+------+------+------+------|                |------+------+------+------+------+------|
-       LSFT, XXXXX, XXXXX, XXXXX, XXXXX, XXXXX,                   UNDS,  PLUS,  LBRC,  RBRC,  BSLS,  TILD,\
+      XXXXX, XXXXX, XXXXX, XXXXX, XXXXX, XXXXX,                   HOME,  PGDN,  PGUP,   END, XXXXX, XXXXX,\
   //|------+------+------+------+------+------+------|  |------+------+------+------+------+------+------|
-                                  GUIEI, LOWER,   SPC,      ENT, RAISE, ALTKN \
+                                  XXXXX, XXXXX, XXXXX,    XXXXX, XXXXX, XXXXX \
                               //`--------------------'  `--------------------'
   ),

使用感

良いと思って選んだだけあって 形状・サイズ・配置・打鍵感 など どれもほぼ良い。

ほぼほぼ文句無しなんだけど、あえて挙げると、親指の位置がちょっと不満。 親指用のキーは下部に3つ 押しやすい位置に斜めに並んでいるのだけど、 どうもそれでも最内側のキーがまだ少し遠くて押しづらい…。 ほぼ同じ形状ながらErgoDashの方がこの親指のキーたちが押しやすいな… というのがちょっと使ってみての感想でした。

まとめ

だいたい自分の理想のキーボードってのが分かってきた気がする。 ここからさらにやるとしたら このCorne Cherryをベースに 親指のあたりだけ微調整したものを自分で作るか…? というところか

Mix Leap Study #29 で話をしてきた #mixleap

大阪で開催された「Mix Leap Study #29 - Step up for TensorFlow」に、縁あってお声がけいただき 20分ほどお話をさせていただきました。

yahoo-osaka.connpass.com

TensorFlow関連で、ってことで ここ数年〜最近で取り組んでいる趣味の画像認識について、データセットを集め管理するために取り組んできたことについてデモを混じえて紹介させていただきました。

メインゲストのLaurence氏にも良かったと声をかけていただいたりトークの中で例として引き合いに出して言及してくれたりして 嬉しかったです。 大阪開催のこの規模の勉強会で話すのも始めてで緊張しましたが 懇親会でも多くの方々に声をかけていただき交流できて とても良い時間になりました。 今後もこういうイベントに積極的に出ていけるよう引き続き頑張っていこうと思います。ありがとうございました!

ぼくのかんがえたさいきょうのキーマップ への挑戦

せっかく自作キーボードを使い始めてキーレイアウトが今までと変わったんだし、キーマップも変えて色々試してみよう、と思い 挑戦中。

以前までの使い方

今までの自分はMBP持ち歩いているとき以外はキーボードはHHKB(Professional? 英語配列のもの)を外付けして使用してきた。 使い方の特徴としては、

  • CtrlキーがAの左、Emacs派だったので当然多用している
    • 以前はKarabinerで 今はHammerspoonを使用して、遠い位置にあるキーをカスタマイズ
      • Ctrl + JReturn
      • Ctrl + HDelete
      • Ctrl + [Escape
      • など
  • 日本語入力にはAquaSKKを使用、 ; キーによる Sticky-Shiftを利用 (やはり現在はHammerspoonで)
  • 左手親指が キー、右手親指が Space キーを担当

という感じ。 十分慣れていたしそれほど不便を感じることは無かったけど、やはり数字キーとかが遠くて打ち間違えることが多かった、と思う。

40%キーボードでの挑戦

HelixPico を使い始めて、初めて 「数字キーの無いキーボード」という世界を経験。 慣れないとやはり大変だったけど、無くてもどうにかなる、というのを実感した。

むしろホームポジションからほぼ移動せずに何でも出来ればそれはラクだし最高なのでは?!

ということで、せっかくだから少ないキーで上手く使いこなしていけるようキーマップを考えて練習してみることにした。

やることとしては

  • 人差し指〜小指 の4本は 上下左右1個分までしか移動しない
    • 3行×6列の範囲
  • 親指は基本的に2キー(以上)使えるのでコンビネーションでカバーする
  • できるだけ疲労しないよう 指への負担を分散させる

ということ。 最低で20キーずつで 基本的な入力を出来るようにする。 片手4行6列(+1)のHelixPicoはちょうど良くて、これで問題なく使えればどんなキーボードでもだいたい大丈夫なはず。

キーマップ

設定したのは以下のような感じ。

  /* Qwerty
   * ,-----------------------------------------.             ,-----------------------------------------.
   * | Tab  |   Q  |   W  |   E  |   R  |   T  |             |   Y  |   U  |   I  |   O  |   P  |  \   |
   * |------+------+------+------+------+------|             |------+------+------+------+------+------|
   * | Ctrl |   A  |   S  |   D  |   F  |   G  |             |   H  |   J  |   K  |   L  |   ;  |  '   |
   * |------+------+------+------+------+------|             |------+------+------+------+------+------|
   * | Shift|   Z  |   X  |   C  |   V  |   B  |             |   N  |   M  |   ,  |   .  |   /  |Enter |
   * |------+------+------+------+------+------+-------------+------+------+------+------+------+------|
   * |Adjust| Esc  | Alt  |Lower | GUI  |Space |EISU  |KANA  |Shift |Raise | Left | Down |  Up  |Right |
   * `-------------------------------------------------------------------------------------------------'
   */
[_QWERTY] = LAYOUT( \
      KC_TAB,  KC_Q,    KC_W,    KC_E,    KC_R,    KC_T,                      KC_Y,    KC_U,    KC_I,    KC_O,    KC_P,    KC_BSLS, \
      KC_LCTL, KC_A,    KC_S,    KC_D,    KC_F,    KC_G,                      KC_H,    KC_J,    KC_K,    KC_L,    KC_SCLN, KC_QUOT, \
      KC_LSFT, KC_Z,    KC_X,    KC_C,    KC_V,    KC_B,                      KC_N,    KC_M,    KC_COMM, KC_DOT,  KC_SLSH, KC_ENT , \
      ADJUST,  KC_ESC,  KC_LALT, LOWER,   KC_LGUI, KC_SPC,  EISU,    KANA,    KC_RSFT, RAISE,   KC_LEFT, KC_DOWN, KC_UP,   KC_RGHT \
      ),

/* Raise
   * ,-----------------------------------------.             ,-----------------------------------------.
   * | Esc  |   !  |   @  |   #  |   $  |   %  |             |   ^  |   &  |   *  |   (  |   )  | Del  |
   * |------+------+------+------+------+------|             |------+------+------+------+------+------|
   * |      |   _  |   +  |   {  |   }  |   ~  |             |   `  |   -  |   =  |   [  |   ]  |      |
   * |------+------+------+------+------+------|             |------+------+------+------+------+------|
   * |      |   1  |   2  |   3  |   4  |   5  |             |   6  |   7  |   8  |   9  |   0  |      |
   * |------+------+------+------+------+------+-------------+------+------+------+------+------+------|
   * |      |      |      |      |      |      |      |      |      |      | Home |PageDn|PageUp| End  |
   * `-------------------------------------------------------------------------------------------------'
   */
  [_RAISE] = LAYOUT( \
      KC_ESC,  KC_EXLM, KC_AT,   KC_HASH, KC_DLR,  KC_PERC,                   KC_CIRC, KC_AMPR, KC_ASTR, KC_LPRN, KC_RPRN, KC_DEL, \
      _______, KC_UNDS, KC_PLUS, KC_LCBR, KC_RCBR, KC_TILD,                   KC_GRV,  KC_MINS, KC_EQL,  KC_LBRC, KC_RBRC, _______, \
      _______, KC_1,    KC_2,    KC_3,    KC_4,    KC_5,                      KC_6,    KC_7,    KC_8,    KC_9,    KC_0,    _______, \
      _______, _______, _______, _______, _______, _______, _______, _______, _______, _______, KC_HOME, KC_PGDN, KC_PGUP, KC_END  \
      ),
  • 通常のQWERTY と もう一層のレイヤー を用意し 2層の切り替えで入力を行う
  • 親指は
    • 左手は GUI (⌘)Space
    • 右手は Raise (レイヤー=切り替え) と Shift

という感じ。

Modifierキーたち

Ctrl, は従来通り 左手の小指と親指で担う。ここを変えるのは負担が大きすぎるので。 ただし Shift キーは 従来左手小指で使っていのを右手親指に移動している。

右手の親指でレイヤー切り替えを1個だけ使うことにする。 QMK Firmwareの設定では複数のレイヤーを作って複雑に切り替えることが出来るようだけど、そこまでは必要ないはず、ということで1個だけのレイヤーを用意して使うことにした。

Raise レイヤー

いわゆる zxcvb, nm,./ の列、ホームポジションの1段下の部分を数字キーとして利用する。 遠くない上に横並びになっていて分かりやすく打ち間違える確率が減る。

!@#$%, ^&*()などの、通常 Shift + 数字キーを使って入力する記号類は 数字とは逆の ホームポジションの1段上の位置に並べる。 例えば * なんかは 従来だったら Shift + 8 と押していたところを Raise + I で打つようになる。

-, =, [, ], ` などの 通常のキーボードで微妙に遠い位置にあるキーを 右手のホームポジションに、それらと Shift で組み合わせるキーを 左手のホームポジションの位置に配置した。 RaiseShift を横並びに配置しているので、基本的に Raise + Shift + 何か というキーの組み合わせは存在せず 通常 Shift + 何か で打っていたキーはすべて Raise + 何か で代用できるようになっている。

Space を 左手親指に、 Shift を右手親指に

今までは右手親指は space しか打っていなくてだいぶサボっていたのに対し、 前述の通りレイヤー切り替えを割り当てたことで忙しくなってきた。 左手の親指は でしか使っておらずヒマなはずなので こっちに Space を任せるように変更した。

ターミナルでコマンドを打つときの ls -l みたいに Space 直後に - があるときに 右手に Space があると Raise + J で打つ - と相性があまりに悪い、というのもある。

+ 何か で動かすショートカットなどがあったときに Raise キーが と並んでいると親指が足りなくなってしまうが、 Raise が右手親指であれば多少複雑なショートカットも問題なく押せる。たぶん。

色々考えながら試していたら 左手小指で CtrlShift も押しているのが疲れるよな… と感じ、右手親指は Raise に使っているものの Space からは解放されてまだもう一つキー押せるはず、ということで Shift をここで行うようにしてみた。 多少右手親指が忙しくなるが、左手小指を酷使するよりは良さそう。 これによって 今までAquaSKK; の Sticky-Shiftを利用していたのが 別に使わなくても良さそうだな、と思い 設定をオフってみることにした。

矯正

自分がずっと馴染んでいた 右手親指での Space と 左手小指での Shift をそれぞれ逆の手に移動したのはすごい戸惑いがあって まだまだ慣れない。 1週間くらい続けてようやく いちいち「えぇと、これを打ちたいときはこっちを押してからこっち…」と脳内で考えなくても多少自然に指が動くようになってきたかな、という感じ。

考えてみると ; のSticky-ShiftでSKK入力を始めた頃だって しばらくは全然上手く打てていなかったはずなので 慣れるまである程度時間がかかるのは当然なはず。

現時点では何を書くのにしても今までの2〜3倍の時間がかかってしまう感じだけど、ちゃんと慣れてくればほぼ遜色なく早く打てるようになるのでは…?とは思っている。 入力が遅いのは 単純にキーマップの配置が良くないのか 慣れていないからなのか を切り分けるためにも、まずはとにかく慣れていくしかない。

一つ問題があると感じているのは () => {} みたいなのを入力するときの => の記号。 これは Raise + K= を入力した直後に 今度は Shift + . を押す必要があるので 右手親指がとても忙しくなる。つらい。 とはいえこの => って従来のキーボードで入力するときだって 左手小指で Shift 押したまま 右手の小指?薬指? を頑張って伸ばして = を入力して そっから戻って右下の . を押しにいって、と移動が多く大変だったはずなので 多少押しづらい組み合わせのものがあるのは当然なのかな、とも思っている。

今後

とにかく練習して馴染んでみないとまだ分からない部分もあるので、この配置に慣れるよう頑張って文章やコードをたくさん書く。もちろんこの記事も頑張って上述のキーマップでHelixPicoを使って書いている。

ある程度慣れても打ちづらい、などまた課題が見えてきたら配置を考え直せば良さそう。

左手と比較して右手の小指はまだ遊んでいるし 端で余っているキーも少しあるので そこらへんまた何か割り当てを考えた方が良いかもしれない。

電子工作 3作目・ErgoDash

f:id:sugyan:20181110124333j:plain

HelixPico、Mint60 に続いて、自作キーボード 3作目。

今回作ったのは ErgoDash。 この記事はErgoDashを使って書いています。

booth.pm

本当はMint60より先に購入していたのだけど、色々と手違いがあったりして 遅くなってしまった。

組み立て

キー数が多い分 大変っていう以外はそれほど難しいことなかった、と思う。 ダイオードが隣り合わせで配置されている箇所が多くて繋がってしまわないよう気をつける必要があった、くらいかな

使用感

配列

最初に買ったHelixPicoはOrtholinear(格子配列)、次のMint60はRow-Staggered(横にズレている 標準キーボードによくあるやつ)、そして今度のErgoDashはColumn-Staggered(縦にズレて配置されているタイプ)、ということでそれぞれ違う種類のものが揃った。 まだ慣れていないけど、なんとなく感覚ではこのColumn-Staggeredが一番理にかなっていて良いと思っているので とにかく練習してみようと思う。

キースイッチ

前回まではLinearなスイッチだったけど、今回はTactileなものにチャレンジしてみた。

talpkeyboard.stores.jp

押し心地は良いかなー。元々あんまりこだわりないのでどうでもいいっちゃどうでもいいんだけどw ただやっぱりちょっとうるさい気がするので 職場とかで使うにはちょっと気になるかも… Silentタイプだったら問題なかっただろうか。

キーマップ

やっぱりキーが多くて戸惑うw 数字キーの行があるのは少し安心感はあるが やっぱり遠いのであまり積極的に使いたいとは思わない。Modifier Keyでうまく解決させたいところ。 最内側の列もなかなか使いどころが難しい。滅多に使わないけど一発で打てると嬉しいキーがあったら指を伸ばして使う、くらいのが良いのだろうか。実際自分はreturnキーはほぼ押さないので そういのを置いておくと良いのかもしれない

最下段は内側に押しやすい感じの位置で複数あるのが嬉しい。HelixPicoほぼ不満なかったけどちょっと親指が押しづらいかな、と思っていたので この形状この位置で親指の選択肢が3〜4個 あるといいなと思っている。

現時点ではちょっと default から書き換えた程度。

--- keymaps/default/keymap.c    2018-10-20 11:10:29.000000000 +0900
+++ keymaps/sugyan/keymap.c     2018-11-09 00:34:47.000000000 +0900
@@ -37,11 +37,11 @@
    * ,----------------------------------------------------------------------------------------------------------------------.
    */
   [_QWERTY] = LAYOUT( \
-    KC_ESC,  KC_1,    KC_2,    KC_3,    KC_4,    KC_5,    KC_LBRC,                        KC_RBRC, KC_6,    KC_7,    KC_8,    KC_9,    KC_0,    KC_PSCR, \
-    KC_GRV,  KC_Q,    KC_W,    KC_E,    KC_R,    KC_T,    KC_MINS,                        KC_EQL , KC_Y,    KC_U,    KC_I,    KC_O,    KC_P,    KC_BSLS, \
-    KC_TAB,  KC_A,    KC_S,    KC_D,    KC_F,    KC_G,    KC_DEL ,                        KC_BSPC, KC_H,    KC_J,    KC_K,    KC_L,    KC_SCLN, KC_QUOT, \
+    KC_GRV,  KC_1,    KC_2,    KC_3,    KC_4,    KC_5,    KC_LBRC,                        KC_RBRC, KC_6,    KC_7,    KC_8,    KC_9,    KC_0,    KC_PSCR, \
+    KC_TAB,  KC_Q,    KC_W,    KC_E,    KC_R,    KC_T,    KC_MINS,                        KC_EQL , KC_Y,    KC_U,    KC_I,    KC_O,    KC_P,    KC_BSLS, \
+    KC_LCTL, KC_A,    KC_S,    KC_D,    KC_F,    KC_G,    KC_DEL ,                        KC_BSPC, KC_H,    KC_J,    KC_K,    KC_L,    KC_SCLN, KC_QUOT, \
     KC_LSFT, KC_Z,    KC_X,    KC_C,    KC_V,    KC_B,    KC_SPC ,                        KC_ENT , KC_N,    KC_M,    KC_COMM, KC_DOT,  KC_SLSH, KC_RSFT, \
-    KC_LCTL, KC_LGUI, KC_LALT, EISU,             LOWER,   KC_SPC ,_______,        _______,KC_ENT , RAISE,            KC_LEFT, KC_DOWN, KC_UP,   KC_RGHT  \
+    KC_LCTL, KC_LGUI, KC_LALT, KC_LGUI,          LOWER,   KC_SPC , KC_SPC,        KC_SPC ,KC_SPC , RAISE,            KC_LEFT, KC_DOWN, KC_UP,   KC_RGHT  \
   ),

   /* Lower
@@ -81,7 +81,7 @@
   [_RAISE] = LAYOUT(
     KC_F11,  KC_F1,   KC_F2,   KC_F3,   KC_F4,   KC_F5,   KC_LCBR,                        KC_RCBR, KC_F6,   KC_F7,   KC_F8,   KC_F9,   KC_F10,  KC_F12,  \
     KC_TILD, KC_EXLM, KC_AT,   KC_HASH, KC_DLR,  KC_PERC, KC_UNDS,                        KC_PLUS, KC_CIRC, KC_AMPR, KC_ASTR, KC_LPRN, KC_RPRN, KC_PIPE, \
-    KC_TAB,  KC_1,    KC_2,    KC_3,    KC_4,    KC_5,    KC_DEL ,                        KC_BSPC, KC_H,    KC_J,    KC_K,    KC_L,    KC_COLN, KC_DQT , \
+    KC_TAB,  KC_1,    KC_2,    KC_3,    KC_4,    KC_5,    KC_DEL ,                        KC_BSPC, KC_H,    KC_MINS, KC_EQL,  KC_LBRC, KC_RBRC, KC_DQT , \
     KC_LSFT, KC_6,    KC_7,    KC_8,    KC_9,    KC_0,    KC_SPC ,                        KC_ENT , KC_N,    KC_M,    KC_LT,   KC_GT,   KC_QUES, KC_RSFT, \
     KC_LCTL, KC_LGUI, KC_LALT, EISU,             LOWER,   KC_SPC ,_______,        _______,KC_ENT , RAISE,            KC_HOME, KC_PGDN, KC_PGUP, KC_END   \
   ),

まとめ

とか色々触ってみての感想としては、

  • キーは別に少なくて良い (所謂40%ってやつで良い)
  • Column-Staggered良さそう
  • 親指は押しやすい位置に複数欲しい

ってことで次はCorne Cherryを入荷待ちで狙っており、これが本命になりそうな予感がしている。

booth.pm

f:id:sugyan:20181110183859j:plain

f:id:sugyan:20181108233153j:plain

f:id:sugyan:20181108233120j:plain

電子工作 2作目・Mint60

f:id:sugyan:20181103214334j:plain

HelixPicoに続いて、自作キーボード 2作目。

memo.sugyan.com

今回作ったのは、ゆかり(id:eucalyn)さんの「Mint60」。 この記事はMint60を使って書いています。

eucalyn.hatenadiary.jp

少しお値段はするけど、必要なものが全部揃ってるスターターキットを購入。 (Silent Red - Hawaii - マットクリア)

eucalyn.booth.pm

紹介記事に書かれている通り、初心者でも組み立てやすい設計で かつ 丁寧な解説記事があるので 本当にこれに書いてある通りにはんだ付けして組み立てるだけで良い。 非常に分かりやすくて有り難かったです。

eucalyn.hatenadiary.jp

組み立て

簡単といっても HelixPicoよりはちょっと難しかった…かな? HelixPicoが簡単すぎたのはあるかもしれないけど。 キースイッチの取り付けが どうしてもちょっと浮いてしまって 一度仮止めしてから指で押さえながらはんだ溶かすとか悪銭苦闘した。

スイッチのピンやピンヘッダを切り取るのも初体験だったけど 解説記事のおかげでどうにか。

右手最下段のレイアウトがどうして良いかわからず適当に配置したら 後でキーキャップはまらないことに気付いてスイッチを付け直した。スタビライザをつけたところからは離さないといけなかった…

アクリルケースがすごい良く出来ていて ちょっと感動した。よくこんなの設計できるなぁ…。

LED取り付け部分が盛り上がらないようには出来るだけ頑張ったけど やっぱり少し出っ張ってしまった。これはちょっと難しい

自分の作業ミスが多くて後半〜完成までが時間かかってしまった。 キーキャップまでだいたい付けて「よーし!」って思った段階で繋げて試打してみたら「A」キー、「return」キーが効かない。 組み立てたアクリルケース外して確認して はんだ付けが足りてなかったことに気付き修正… その後 今度こそ完成だ、と思ってキーマップを変更しようとして左手最下段のキーが一つまた効いていないことに気付き… なんと そこのダイオードが逆向きだった………

これは本当に目の前が真っ暗になるつらさw スイッチを付け直すくらいならいいけど、ダイオードはかなり初期段階の作業で その後キースイッチとアクリル板に挟まれてしまうので スイッチを全部抜かないと触れなくて。

幸いなことに 最下段で隙間も大きいところだったのでギリギリ ピンセットで触ることができて、吸取線で外したのち 頑張ってつまんで取り出して逆方向にセットしなおして どうにか付け直すことが出来た…。一生分の集中力と器用さを使い果たしたかもしれん。

キーマップ

とりあえずは普段使っている感じで まずは CapsLock と Ctrl を交換。 default は 右手最下段に Backspace と Enter が配置されているけど、元々自分は Ctrl+H と Ctrl+J で代用していて右側のキーは使わないし とりあえず Space と Layer切り替えに配置した。

あとHelixPicoの default を使って感じた、Layer切り替え+右手ホームポジション周辺で記号が打てたりするの便利だったのでそれを倣ってみた。

--- keyboards/mint60/keymaps/default/keymap.c   2018-10-20 11:10:30.000000000 +0900
+++ keyboards/mint60/keymaps/sugyan/keymap.c    2018-11-03 21:01:22.000000000 +0900
@@ -34,20 +34,19 @@
   RGBRST = SAFE_RANGE
 };

-
 const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
   [0] = LAYOUT( \
     KC_GRV,  KC_1,    KC_2,    KC_3,    KC_4,    KC_5,      KC_6,    KC_7,    KC_8,    KC_9,    KC_0,    KC_MINS, KC_EQL,    KC_BSPC,  \
     KC_TAB,    KC_Q,    KC_W,    KC_E,    KC_R,    KC_T,      KC_Y,    KC_U,    KC_I,    KC_O,    KC_P,    KC_LBRC, KC_RBRC, KC_BSLS,  \
-    KC_CAPS,     KC_A,    KC_S,    KC_D,    KC_F,    KC_G,      KC_H,    KC_J,    KC_K,    KC_L,    KC_SCLN, KC_QUOT,        KC_ENT,   \
+    KC_LCTL,     KC_A,    KC_S,    KC_D,    KC_F,    KC_G,      KC_H,    KC_J,    KC_K,    KC_L,    KC_SCLN, KC_QUOT,        KC_ENT,   \
     KC_LSFT,       KC_Z,    KC_X,    KC_C,    KC_V,    KC_B,      KC_N,    KC_M,    KC_COMM, KC_DOT,  KC_SLSH, KC_RSFT,   KC_UP,  MO(1),   \
-    KC_ESC,    KC_LCTL,    KC_LGUI,    KC_LALT,    KC_SPC,        KC_BSPC, KC_ENT,  LALT(KC_GRV),               KC_LEFT,KC_DOWN,KC_RGHT \
+    KC_ESC,    KC_CAPS,    KC_LALT,    KC_LGUI,    KC_SPC,         KC_SPC,  MO(1),  LALT(KC_GRV),               KC_LEFT,KC_DOWN,KC_RGHT \
   ),
   [1] = LAYOUT( \
     KC_ESC,  KC_F1,   KC_F2,   KC_F3,   KC_F4,   KC_F5,     KC_F6,   KC_F7,   KC_F8,   KC_F9,   KC_F10,  KC_F11,  KC_F12,    KC_DEL, \
     RGB_TOG,   RGBRST,  RGB_HUI, RGB_SAI, RGB_VAI, XXXXXXX,   XXXXXXX, XXXXXXX, XXXXXXX, XXXXXXX, XXXXXXX, XXXXXXX, XXXXXXX, XXXXXXX, \
-    XXXXXXX,     RGB_MOD, RGB_HUD, RGB_SAD, RGB_VAD, XXXXXXX,   XXXXXXX, XXXXXXX, XXXXXXX, XXXXXXX, XXXXXXX, XXXXXXX,        XXXXXXX, \
-    _______,       XXXXXXX, XXXXXXX, XXXXXXX, XXXXXXX, XXXXXXX,   XXXXXXX, XXXXXXX, XXXXXXX, XXXXXXX, XXXXXXX, _______, KC_PGUP, _______, \
+    XXXXXXX,     RGB_MOD, RGB_HUD, RGB_SAD, RGB_VAD, XXXXXXX,   XXXXXXX, KC_MINS,  KC_EQL, KC_LBRC, KC_RBRC, KC_BSLS,        XXXXXXX, \
+    _______,       XXXXXXX, XXXXXXX, XXXXXXX, XXXXXXX, XXXXXXX,   XXXXXXX, XXXXXXX, XXXXXXX, KC_PGDN, KC_PGUP, _______, KC_PGUP, _______, \
     XXXXXXX,   _______,    _______,    _______,    XXXXXXX,       XXXXXXX, XXXXXXX, XXXXXXX,                   KC_HOME, KC_PGDN, KC_END \
   )
 };

使用感

とにかく見た目が可愛い。Hawaiiキーキャップの見た目のインパクトやばい

LEDも初体験で、組み立てる前は正直「別にキーボード光らなくても良くない…?」って思ってたけど、実際光ってるのを見るとテンション上がる…w

Silent Redのキースイッチの押し心地はなかなか。 本当に静かな感じがして良い。 ただ、スタビライザ付けた大きなキーのところはどうしてもうるさくなってしまう。スタビライザ付けない、っていう選択肢もあったのかな…?

HelixPicoで格子配列には慣れてきてたので、Row-Staggeredについての善し悪しの判断は難しい。本当に使いやすいのはどっちなんだろう…と悩み続ける。

スターターキット同梱でUSBケーブルとオーディオケーブルあったけど 右手側のMBPから伸ばして 肩を自然に開いて使うにはどちらもちょっと長さが足りない感じがした。 HelixPico用に自分で買ったやつが今のところお気に入り

Aux ケーブル,CableCreation 3.5mmオーディオケーブル 90度角度付き(両側L型)L型-L型ステレオ・オーディオ・ケーブル 0.9M/ブラック&ホワイト

とりあえずこれがあればHHKBはもう必要なくなりそう、という感じにはなっている。

はじめての電子工作: HelixPico

HelixPico キーボードキット | 遊舎工房

なんとなく興味は持っていたものの第一歩を踏み込めずにいたのだけど、Twitterに書いたら一気に物事が進んだ。

その日のうちに キーボードキット、はんだごて、ニッパー、などなど必要になりそうなもの一式をポチっと全部注文した。

数日で全部揃って、始められる。

素人なりにゆっくりと進めていったら、なんとか出来上がった

f:id:sugyan:20181017012011j:plain

f:id:sugyan:20181018010002j:plain

f:id:sugyan:20181018010027j:plain

すごい。かわいい。

今までずっとMBPかHHKBしか使ってなかったから左右分離型も格子状配列も初体験で全然慣れないけど、使っているうちにイイカンジになりそうな予感はある。 早速これを使ってこのブログを書いているけど、ようやく慣れて今までの60%くらいの速度で打てるようになったかな、くらいw コード書くと記号とかで戸惑ってさらに遅くなりやすい。まぁ慣れだとは思うけど…

小さくコンパクトなので持ち運びやすそう、っていうところが利点かなー。あとかわいい。やっぱり自分で作った、ってなると愛着はわく。

とにかく思い立ってゼロからスタートして1週間くらいで出来上がるお手軽さは思ってた以上で、もっと早くから手を出してみればよかったw という感想。

せっかく工具類を買ったわけだし もう何種類か組み立ててみるつもり。

学習済みMobileNetV2モデルによる推論をTensorFlow.jsとWebWorkerを使ってブラウザ上で実行

将棋駒画像分類の話の続き。

memo.sugyan.com

学習させたモデルでの分類結果を実際に試すときに Web上でもインタラクティブに出来ると便利そう、と思ってやってみた。

学習済みモデルの変換

まずは学習済みのモデルをTensorFlow.js用に変換する必要がある。ここまではPythonの領域。

普通に tf.train.Saver を使っていると、checkpoint形式でパラメータが保存される。

./logdir
├── checkpoint
├── events.out.tfevents.1537850055.****.local
├── graph.pbtxt
├── model.ckpt-1500.data-00000-of-00001
├── model.ckpt-1500.index
└── model.ckpt-1500.meta

このままではダメなので Frozen Model の形式に変換したいところ。 TensorFlow の freeze_graph を使って

$ freeze_graph --input_checkpoint logdir/model.ckpt-1500 --output_node_names 'MobilenetV2/Logits/output' --input_graph logdir/graph.pbtxt --output_graph output_graph.pb

のようにすれば Frozen Model の形式にはなるのだけど、前回の記事のように学習時に作ったモデルそのままだとダメらしい。

import tensorflow.contrib.slim as slim
from nets.mobilenet import mobilenet_v2


def train():

    ...

    inputs = ...
    class_count = ...
    with slim.arg_scope(mobilenet_v2.training_scope(is_training=True)):
        logits, _ = mobilenet_v2.mobilenet(inputs, num_classes=class_count)

のように training_scope() 上で作られたモデルは dropout や batch normalization などが入った training mode として動作するものになるので、このまま freeze_graph しても挙動は training mode のままになってしまうっぽい。

なので、一度 non-training mode として復元してから freeze するようにする。 freeze_graph コマンドも中では tensorflow.python.framework.graph_util を使って変数を定数に変換しているだけなので、これを利用する。

import tensorflow as tf
from tensorflow.python.framework import graph_util
from nets.mobilenet import mobilenet_v2

FLAGS = tf.app.flags.FLAGS
tf.app.flags.DEFINE_string('checkpoint_path', 'logdir/model.ckpt',
                           '''Path to checkpoint file''')
tf.app.flags.DEFINE_string("output_graph", 'output_graph.pb',
                           """Path to write the frozen 'GraphDef'""")


def main(argv=None):
    class_count = ...
    placeholder = tf.placeholder(tf.float32, shape=(None, 96, 96, 3))
    logits, _ = mobilenet_v2.mobilenet(placeholder, class_count)
    saver = tf.train.Saver()
    with tf.Session() as sess:
        saver.restore(sess, FLAGS.checkpoint_path)
        output = graph_util.convert_variables_to_constants(
            sess, tf.get_default_graph().as_graph_def(), ['MobilenetV2/Logits/output'])
    with open(FLAGS.output_graph, 'wb') as f:
        f.write(output.SerializeToString())


if __name__ == '__main__':
    tf.app.run(main)

training_scope() をつけずに呼ぶことで(もしくは明示的に training_scope(is_training=False) のscopeにしてもよい) 、 non-training mode でのモデルが出来る。 そこから tr.train.Saver で restore した上で graph_util.convert_variables_to_constants() を呼んで、serializeすれば目的の Frozen Model が出来上がる。

label情報も含める

上記の Frozen Model で一応「入力画像に対する推論結果」を得られるが、その出力は単に計算結果の logits としての数値列で、「最も数値の高かったindexは どのlabelに対応するか」という情報が無い。 別ファイルで保存してあればそれを読んで照らし合わせてやればいいけど、ここから変換してJavaScriptの世界で使おうとするところでそれは面倒。 label情報も Frozen Model に含めてやりたい。

labels.txt にlabel名が羅列してあるとしたら、それを "," 区切りで繋げた文字列とかを定数として定義して freeze 時に加えてやれば良い。

def main(argv=None):
    with tf.gfile.Open(FLAGS.labels) as f:
        labels = [line.strip() for line in f.readlines()]
    labels_str = tf.constant(list(','.join(labels).encode()), dtype=tf.int32, name='labels')

    placeholder = tf.placeholder(tf.float32, shape=(None, 96, 96, 3))
    logits, _ = mobilenet_v2.mobilenet(placeholder, len(labels))
    saver = tf.train.Saver()
    with tf.Session() as sess:
        saver.restore(sess, FLAGS.checkpoint_path)
        output = graph_util.convert_variables_to_constants(
            sess, tf.get_default_graph().as_graph_def(), ['MobilenetV2/Logits/output', 'labels'])
    with open(FLAGS.output_graph, 'wb') as f:
        f.write(output.SerializeToString())

tf.string の型にしてやれば良いだけかと思ったけど、次のconvert時に tf.string はsupportされていない、とエラーになってしまうので、仕方ないのでバイト列に変換したものを tf.int32 の1階Tensorとして無理矢理使う。 graph_util.convert_variables_to_constants() の第3引数 output_node_names は複数を指定できるので、 'MobilenetV2/Logits/output''labels' と2つ指定するよう変更。

Web format に変換

ここまで出来れば、あとは tensorflowjs_converter で変換するだけ。

$ pip install tensorflowjs
$ mkdir js
$ tensorflowjs_converter --input_format tf_frozen_model --output_node_names 'MobilenetV2/Logits/output,labels' output_graph.pb ./js
$ ls js
group1-shard1of3
group1-shard2of3
group1-shard3of3
tensorflowjs_model.pb
weights_manifest.json

といった感じで コマンド一発で変換されたファイルが生成される。 あとはこれらを静的に配信するようにしておけば、JavaScriptの世界で利用できるようになる。はず。

TensorFlow.jsで推論

$ npm install @tensorflow/tfjs

必要なのは上記のみ。 前節で生成したファイルを配信しているURLを指定して loadFrozenModel() を呼ぶことでモデルが読み込まれる。

import * as tf from "@tensorflow/tfjs";
import { loadFrozenModel } from "@tensorflow/tfjs-converter";

const MODEL_URL = "****/tensorflowjs_model.pb";
const WEIGHTS_URL = "****/weights_manifest.json";

loadFrozenModel(
    MODEL_URL, WEIGHTS_URL,
).then((model: tf.FrozenModel) => {
    const labelsTensor: tf.Tensor = tf.tidy(() => {
        return model.execute(tf.tensor([], [0, 96, 96, 3]), "labels") as tf.Tensor;
    });
    const labels: string[] = String.fromCharCode(...labelsTensor.dataSync()).split(",");
    labelsTensor.dispose();

    ...
});

計算して出力する tensor の名前を model.execute() の第2引数で指定できるので、まずは適当な空の入力(定数を取り出すだけなので入力は何でも良い) と "labels" を上記のように指定することで、label情報を取り出せる。

tf.Tensor まわりはちょっとクセがあるので注意する。

不要になった tensordispose() で明示的に破棄する、もしくは tf.tidy() の中で処理を書いてGPUが余分なメモリ消費を消費しないよう気をつける必要がある、ということらしい。 計算結果の中身は data()dataSync()TypedArray 形式で得ることができる。

ということで実際に画像データ(ここでは ImageData[])を受け取って推論した softmax 出力の上位3つの結果を得るのは以下のような感じ。 こっちは "MobilenetV2/Logits/output" を出力する tensor として指定。 tf.topk() というAPIもあるし それを使っても良かったかも…

loadFrozenModel(
    MODEL_URL, WEIGHTS_URL,
).then((model: tf.FrozenModel) => {

    ...

    const data: ImageData[] = ...
    const softmax: tf.Tensor = tf.tidy(() => {
        const tensors: tf.Tensor[] = data.map((d: ImageData) => tf.fromPixels(d));
        const inputs: tf.Tensor = tf.stack(tensors).toFloat().div(tf.scalar(255.0));
        const logits: tf.Tensor = model.execute(inputs, "MobilenetV2/Logits/output") as tf.Tensor;
        return tf.softmax(logits);
    });
    const resultData: Float32Array = softmax.dataSync() as Float32Array;
    softmax.dispose();

    // sort and get top-k
    const results = [];
    for (let i: number = 0; i < resultData.length / labels.length; i++) {
        const values: Iscored[] = [];
        resultData.slice(labels.length * i, labels.length * (i + 1)).forEach((score: number, index: number) => {
            values.push({ index, score });
        });
        values.sort((a: Iscored, b: Iscored) => b.score - a.score);
        results.push(values.slice(0, 3).map((value: Iscored) => {
            const label: string = labels[value.index];
            return { score: value.score, label };
        }));
    }

    ...
});

WebWorker経由で結果を得る

で、上記のようなのを実際に画像を入力して試してみると。 TensorFlow.jsはデフォルトで WebGL backend を利用して高速に計算してくれるのだけど、 どうしても初回の呼び出し時には 2000-3000ms ほどかかってしまう。 以下のような理由らしい。

  1. Why is the predict() method for inference so much slower on the first call than the subsequent calls?

The time of first call also includes the compilation time of WebGL shader programs for the model. After the first call the shader programs are cached, which makes the subsequent calls much faster. You can warm up the cache by calling the predict method with an all zero inputs, right after the completion of the model loading.

https://github.com/tensorflow/tfjs-converter/blob/master/README.md#faq

また、これは自分の使い方が悪いのかもしれないけど 1回の入力に使う画像数が 76枚くらいを超えると極端にそれが遅くなる… どっかでメモリ使い過ぎてるのかな。要調査。 [75, 96, 96, 3] くらいまでの入力なら大丈夫だけど それより多くなると 5000-6000ms とか一気に遅くなる。

ともかく、例えば実行ボタンを押してから結果を表示させようとすると 数秒かかってしまうことがあるわけで その間 UIが止まってしまう。それは出来れば避けたい。

というわけで 計算のロジックを Web Workers に移すことを考えた。

worker-loader

今回はサーバサイドとフロントエンドを分離して開発していたので、できれば単一のJSでまとめてしまいたい。 ってことで webpack の worker-loader を利用。 これを使って { inline: true } を指定することで別ファイルでWorker用のJSを用意しなくてもイイカンジにやってくれるようだ。

$ npm install worker-loader

webpack.config.js は以下のような感じ。

module.exports = {
    entry: './src/index.tsx',
    module: {
        rules: [
            {
                test: /\.?worker\.ts$/,
                use: {
                    loader: 'worker-loader',
                    options: { inline: true }
                }
            },
            {
                test: /\.tsx?$/,
                use: 'ts-loader',
                exclude: /node_modules/
            }
        ]
    },
    resolve: {
        extensions: ['.tsx', '.ts', '.js']
    },
    ...
};

ts-loader と併用して使う場合は rules の順番に注意しないといけないようだ。

worker.ts に先程のような処理を以下のように書ける。

import * as tf from "@tensorflow/tfjs";
import { loadFrozenModel } from "@tensorflow/tfjs-converter";

const ctx: Worker = self as any;

...

loadFrozenModel(
    MODEL_URL, WEIGHTS_URL,
).then((model: tf.FrozenModel) => {
    ...

    ctx.addEventListener("message", (message: MessageEvent) => {
        ...

        ctx.postMessage(...);
    });
});

export default null as any;

入力データを addEventListener() で待ち受け、結果を postMessage() で返せば良い。

使う側(UI側)は、この Worker に対して postMessage() で入力画像を投げて addEventListener() で結果を待ち受ければ良い。 とはいえ連続で投げてしまうことも出来てしまうので、投げたものに対する結果を正しく得る必要がある。 単一のWorkerインスタンスを持つSingletonクラスのようなものを使って Promise を返す関数を提供するようにしてみた。

import Worker from "./worker";

export interface IpredictResult {
    score: number;
    label: string;
}

interface Iresponse {
    key: string;
    results: IpredictResult[][];
}

export default class WorkerProxy {
    public static predict(inputs: ImageData[]): Promise<IpredictResult[][]> {
        const worker: Worker = WorkerProxy.getInstance().worker;
        const key: string = Math.random().toString(36).slice(-8);
        return new Promise<IpredictResult[][]>((resolve) => {
            const listener = (ev: MessageEvent) => {
                const data: Iresponse = ev.data;
                if (data.key === key) {
                    resolve(data.results);
                    worker.removeEventListener("message", listener);
                }
            };
            worker.addEventListener("message", listener);
            worker.postMessage({ key, inputs });
        });
    }
    private static instance: WorkerProxy;
    private static getInstance(): WorkerProxy {
        if (!this.instance) {
            WorkerProxy.instance = new WorkerProxy();
        }
        return WorkerProxy.instance;
    }
    private worker: Worker;
    private constructor() {
        this.worker = new Worker();
    }
}

これによって、UI側では複数の画像も非同期にリクエストして結果を得ることができる。

import WorkerProxy, { IpredictResult } from "./worker-proxy";

...

const inputs: ImageData[] = ...
inputs.forEach((data: ImageData, i: number) => {
    WorkerProxy.predict([data]).then((results: IpredictResult[][]) => {
        ...
    });
});     

これで、冒頭のTweetみたいな感じにUIを止めずに バックグラウンドでのWebWorkerによる計算を使って推論結果を順次表示していける。

問題点

…ということで実現できたけど、これ、すごい遅いぞ…? 1枚の画像に対する処理でも 300ms前後かかるし、複数枚をまとめて渡すと線形に処理時間が倍増していく。

なんと、 WebWorker 内でのTensorFlow.jsの計算は、GPUを使ってくれないらしい。 確認したら WebWorker 内では tf.getBackend() の結果が cpu になっていた。

今現在、まさに進行中の話のようで 近いうちに解決してくれるのかもしれない。

warm up と 分割?

と、こういう内容を調べながらこの記事を書いていて思ったけど、結局 webgl backend を使えれば、「ある程度の決まったサイズの入力なら 初回以外は高速に処理できる」ということが分かったので、

  • [10, 96, 96, 3] くらいの入力を受け取るよう固定
    • 1個の入力([1, 96, 96, 3]) に対しては残り [9, 96, 96, 3] のダミーデータを連結して埋める
    • 11個以上の入力は分割して処理する
  • modelのload直後にはやっぱり空の [10, 96, 96, 3] を入力してwarm upして それから使うようにする

というようにすれば、この程度の計算なら もしかしてわざわざWebWorkerを使って非同期に処理しようとしなくても十分に高速に(UIを止めることなく)結果を得ることが出来るのでは… と思ったのでした。

結論

ぜんぜんわかってねぇ

追記

試しに WebWorker を使わずにフォアグラウンドで webgl を使って固定長入力に分割して warm up して処理時間を計測してみた。 入力画像は 81。

input [1 * 96 * 96 * 3] * 81 loop => warm up: 2088ms, execute: 1520ms
input [2 * 96 * 96 * 3] * 41 loop => warm up: 2086ms, execute: 960ms
input [3 * 96 * 96 * 3] * 27 loop => warm up: 2093ms, execute: 761ms
input [4 * 96 * 96 * 3] * 21 loop => warm up: 2135ms, execute: 640ms
input [5 * 96 * 96 * 3] * 17 loop => warm up: 2149ms, execute: 557ms
input [6 * 96 * 96 * 3] * 14 loop => warm up: 2270ms, execute: 527ms
input [7 * 96 * 96 * 3] * 12 loop => warm up: 2299ms, execute: 496ms
input [8 * 96 * 96 * 3] * 11 loop => warm up: 2146ms, execute: 444ms
input [9 * 96 * 96 * 3] *  9 loop => warm up: 2271ms, execute: 394ms
input [10 * 96 * 96 * 3] * 9 loop => warm up: 2204ms, execute: 452ms
input [11 * 96 * 96 * 3] * 8 loop => warm up: 2256ms, execute: 381ms
input [12 * 96 * 96 * 3] * 7 loop => warm up: 2203ms, execute: 399ms
input [14 * 96 * 96 * 3] * 6 loop => warm up: 2373ms, execute: 359ms
input [17 * 96 * 96 * 3] * 5 loop => warm up: 2310ms, execute: 332ms
input [21 * 96 * 96 * 3] * 4 loop => warm up: 2383ms, execute: 328ms
input [27 * 96 * 96 * 3] * 3 loop => warm up: 2410ms, execute: 316ms
input [42 * 96 * 96 * 3] * 2 loop => warm up: 2410ms, execute: 395ms

なるほど適切な数に分割して処理することで 初回実行のWarmUpではどうしても約2秒ほど止まってしまうけど、それ以降はかなり高速に計算が終わる感じにはなる。