IT業界に転職した話(2018-2019)

直近1年ほどで人生が大きく動いたので、忘れないうちに文章にしておこうと思います。 よくある転職の話です。

昔を振り返ってみると、僕はあまり学問に興味があるタイプの人間ではありませんでした。 ろくすっぽ勉強しないまま小中と進学し、高校受験に際しても無勉強で高専に挑んで当然のように落ちたりしてました。 中学の半ばあたりでCSに興味が芽生えたのですが、その興味を勉強という行動に結び付けることができず。単にコードを書いて遊んでただけ。 いい大学に行っていい会社に就職するんだというお話は、進路を決めるための折々の機会に、繰り返し学校の先生方から聞かされたのですが、そのお話の意図するところを掴めなかったのですね。 そうして僕は選択したわけです。いい会社に就職する…どころか、進学すらしないという選択を。

いい会社に就職できなかったケース 家業手伝い(農業)

僕は農家になりました。 農業は最高! 植物は勝手に育つ! 品種改良も進んでる! 次々いいやつが出てくるぞ! 機械もある! 農薬もある! 文明の力! …などと、いくらうそぶいたところで、無理があります。

結論からいって、農業はヤバいです。 収入はひどく不安定で、自然の影響を受けまくりです。大雨で農地は崩れ、台風で設備は半壊。長雨で品質低下、病気で品質低下、雑草が大量発生し品質低下…。 そして重要なのは、豊作でも収入が大して増えるわけじゃないということです。いつ生活に安寧が訪れるのか…😥 収入が土地の広さに比例するという問題もあります。 いくら機械の性能が向上したところで、所有している耕作地が狭いなら、そこをいくら早く耕しても意味がないのです。 結局は持てるものが利益を得るといういつもの図式です。お金をためて土地を借りる/買うという選択肢もありますが、借り受けた放棄地を頑張って耕したあと、やはり返してほしいという話になったり、先祖伝来の土地は売れない、あるいは大切な土地だからと相場を無視した…

ちょっとテンションが上ってしまいました。 このあたりのお話は本題ではないのでやめておきましょう。

そんなこんなで、僕は家業を手伝いながらCSの勉強をはじめました。 いままでろくすっぽ勉強してこなかった人間なので全てが手探りでしたが、時間だけはありました。 そのうち気づいたんですが、そもそも僕は勉強というものをうまくやれていなかったんですね。 やりたいことをやりたいようにしていると、頭がスッキリしてきます。雑念が生まれる余地がないというか、まさに寝食を忘れるという感じで。 不思議なもので、いったん要領を掴むと、他の勉強にも応用できました。

ずっと田舎でコードを書いていると誰かに見てほしくもなるというか、正直クローズドにしておく意味があまりないので、自分のために書いたものは大抵OSSとして公開しました。 勉強の過程で読んだOSSプロダクトにツッコミを入れたり、使用しているうちにバグに遭遇したら直したりもしました。様々なコードに触れることは重要です。独学では身につかない知見が得られます。 また、OSSにプルリクを送るのは超重要です。このことに気づいたのはずっと後になりますが、プルリクがマージされるということは、程度の差はあれ、そのプロダクトのコミッタが僕のコードを最終的に問題なしと判断してくれたということになります。

コードの他にもPCやサーバの日々のメンテナンスであるとか移行計画などもBlogで公開しました。これも単なる備忘録として以上の意味があったと思います。 インフラにも興味があったのでサーバを色々立てていたのですが、それに際して遭遇したエラーのトラブルシュートであったり、増えすぎたサーバを集約する必要から仮想化環境への移行を検討した件や、RAID環境のパフォーマンスチューニングなど、バリエーションに富んだ記事を残すことができました。

また、CS以外のサブの勉強として、2010年あたりから英語の勉強も並列して開始しました。 きっかけとしては、Googleのイベントに参加した際に同時通訳などがなく、すごいエンジニアが何か喋ってるんだけど何もわからん!!こういう世界か!!と驚愕した事件がありました。この出来事も非常に重要でした。 どうしたものかといろいろ学習方法を探りましたが「英語上達完全マップを10ヶ月やってみた」を参考に、発音、音読、文法、精読と順にやりました。 詳しい方は瞬間英作文が抜けてるじゃんと思われるかもしれません。これは僕の失敗でした。なぜか瞬間英作文だけスキップしてしまったんですよね…本は買ったのに…。瞬間英作文は重要です。ぜひ学習初期からやりましょう。 ある程度基本を学習した後は読み物として文法書を読んで、単語をフラッシュカードアプリで詰め込みました。

勉強を通じてですが、ついったーのお友達もできました。 特に英語に関して、僕は英語学習勢と呼んでいますが、日々すごい進捗を叩き出すおっかない人々がいるのです。英語が絡んだネタで楽しくワイワイやっているだけ…かと思いきや、彼らのとのつながりが後に重要な意味を持ちました。

さて、そんな生活を続けるうちに時は流れ2018年。 以前から温めていた計画を実行に移すときがやってきました。そう、上京です。人生の次のフェーズを、僕はITエンジニアとして東京でチャレンジすることに賭けました。…え、農業? 一生分やったのでもういいです。コンプ、完クリ、感動のエンディングですね(?)

このときの僕の手持ちのカードは次のようなものです:

  • いくつかの言語での複数のソフトウェア開発(OSSとして公開)
  • 著名なOSSへの数件のコミット
  • やってきたことをその時々で記事にまとめたBlog
  • Windows/Linuxの運用経験
  • TOEIC 800ぐらいの英語力
  • ついったーの人脈

単価50万のフリーランス ソフトウェアエンジニア

ある方に職務経歴書を送ったところ、とんとん拍子に話が進み、1社目でのお仕事が決まりました。 この"ある方"というのはフォロワーで、英語学習勢だったんです。つながりの重要性~~ですね😭

正社員採用ではなく、最初は業務委託としてジョインし、その後正社員への道を探るという形であれば、面接のためだけに上京する必要はないよというお話をご提案いただき、僕としてもありがたいものだったため、業務委託という形で働かせていただくことに。 リモート面談の中で、上京後の住居はどこがいいかという話まで出て、めちゃ即決~~という感じでお話を進めていただきました。

ただ、後でお話を聞いたところ、僕が持ってた手札で効いていなかったのはWindows/英語ぐらいで、他は全部重要だったとのこと。 また、僕を推薦してくれたフォロワーが社内で信用を築いていた部分が間違いなく大きかったです。 これら全てがうまく功を奏した感じでした。人生綱渡り…。

お仕事としてはGAE/Goでビッグデータの収集基盤を書くというものでした。 Goを書くのは初めてだったため、最初の1ヶ月ほどはキャッチアップに比重を置かせていただきましたが、その後は十分なパフォーマンスを発揮できたと思います。 DWHを乗っけているインフラとしてGCP, AWSについても見識を深めることができました。また、ETL処理やBigQueryを叩いてのデータ分析など、実務でしか経験できないシーンも体験できたことは非常に有意義でした。 評価は悪くなかったようで、途中で単価を60万円に上げていただきました。

スクラムでのアジャイル開発、20%ルール、勉強会の推奨、フレックス制、リモート勤務など、先進的な体制が整っていて素晴らしく働きやすい職場でした。 メンバーにも恵まれ、実に楽しくお仕事させていただきました。 IT畑での実務経験が無だった僕を拾ってくれた恩はいくら感謝してもしきれません。

まだまだやり残したことはあったのですが、問題は、僕が正社員採用プロセスの面接で緊張しすぎて大失敗してしまったことなんですよね…。 正社員になれないとすると、いつクビになってもおかしくはないわけで…。

本当に不満はなにもなかったのですが、その一点で悩んでいたとき、あるポジションのお話が舞い込んできました。

このときの僕の手持ちのカードは次のようなものです:

  • いくつかの言語での複数のソフトウェア開発(OSSとして公開)
  • 著名なOSSへの数件のコミット
  • やってきたことをその時々で記事にまとめたBlog
  • Windows/Linuxの運用経験
  • TOEIC 800ぐらいの英語力
  • ついったーの人脈
  • IT企業での実務経験(1年未満)

外資メガベンチャー サポートエンジニア

当初は開発ポジションじゃないのか~~と思ったものの、僕はそこまで開発に強いこだわりがあるわけではないと悩んでるうちに悟りました。少なくとも今はそこまで開発にこだわらなくていいかなと。 前職でビジネス的なポジションの方とお仕事で絡む機会が多く、そういうポジションに興味を惹かれたというのがあったり、それらと今自分ができるポジションの中間点としてサポートエンジニアはよさそうだという点に魅力を感じました。 加えて、サポートエンジニアについて経験者にお話を聞いたり、自分の体験として、OSSの開発をしているとたびたびサポートを行う機会があり、それが楽しいものであったことから興味が湧いてきました。

さて、いざ応募しようにも、まず書類が通るのかという点が問題になります。 僕の場合は学士カードがありません。募集要項に大卒と書いてあるポジションなので、まずもって状況は絶望的です。 しかし、ここで切れる唯一のカードがあります。ついったーの人脈、つまりリファラルです。 そう、このポジションもまた、英語学習勢からの紹介でした。 リファラルの場合、学歴フィルタがいったん無効化されて書類が通る可能性があります。

僕は賭けに勝ちました。 その後、カジュアル面談、ゲロむずいオンラインテスト、面接3回をパスしてこのポジションをゲットしました。 今回は前回よりもフィットがよく、全てのカードが効果を発揮したはずです。 特に今回は外資なので英語力は必須でした。CSはともかく英語は完全に趣味でやっていたものなので、まさかここで効いてくるとは…と感慨深くもありました。 また、前回はなかった実務経験がプラスされているのも大きな違いです。

選考について補足すると、カジュアル面談では社内文化、仕事、勤務体系、ドレスコード、福利厚生などについて本当にカジュアルに質問を投げさせていただきました。 オンラインテストに関しては、数理/地頭系の問題とプログラミング問題で時間配分は1時間と少し。難易度は前者がナイトメア、後者は10秒で解けるレベルでした。全て英語での出題で、正直かなりつらかったです。落とした問題も多かったと思いますが、なんとか基準はクリアできていたようです。 本選考では、3回の面接でそれぞれ「ソフトウェアエンジニアとしての能力」「システムアーキテクトクラウドエンジニアとしての能力」「サポートエンジニアとしての適性」を問われたように思います。 選考の過程で複数回「ソフトウェアエンジニアとしてコードを書く仕事ではないが大丈夫か」ということを尋ねられました。そこが不安点として挙げられていたのだと思います。それに関して僕は納得していたので、はっきりとYesと伝えました。 1次面接の冒頭では、憧れだったホワイトボードで問題を解くコーディング試験を体験できました。面接官がマーカーを手にとって、じゃあ早速ですが…と問題を書き始めたのを見て、思わずニヤニヤしてしまいました。 また、英語力のチェックもありました。このあたりは事前の準備が大切です。自分の語彙力、文法力で話せる英語で大まかなスクリプトを用意しておくことが大事だと思います。 2次面接では前職で設計したプロダクトのアーキテクチャを図に書いて説明しました。クラウドに関する知識を前職で上積みできていたのも助けになりました。 3次面接では適性の他にも人間性も見られていたように思います。たまたま振った話題が面接官の手掛けている仕事に絡んでいたため、話が盛り上がり、いい雰囲気で面接を終えることができました。

実際に自社プロダクトのサポートエンジニアを数ヶ月やった感想としては、想像していた以上に僕にはこのポジションへの適性がありそうだということです。 プロダクト自体が概ね.Netフレームワーク上に構築されているため、C#力がそのまま業務を遂行するための力になっています。 プロダクト周辺の知識についても、Windowsがまだポンコツだった時代から延々さわってきた経験値を活かせています。 文章能力についてもそれなりのものが要求されるのですが、昔から日本語の読み書きだけは苦労しなかったのと、その後ついったーで鍛えられたというのもあり、十分な水準にあるように思います。 その他、Web/デスクトップ開発経験、英語力、OSS活動で培ったコードを読み書きする能力、全てが活かせるポジションです。 まだまだフィットの途上ですが、手応えと楽しさを感じています。

フレックス制、リモート勤務があり、働きやすい職場です。 コアタイムの都合もあるのですが、サポートエンジニアという職種上、前職では可能だった昼過ぎに出勤するようなことはできません。ただ、これは前職が自由すぎだった気もします。 チームのメンバーは前職に比べて5倍ほどになったのですが、皆さん優秀かつ人格者揃いで、和気あいあいとした空気でお仕事ができています。 正直チームの空気感は入ってみるまで判断しようがないということもあって心配していましたが、全くの杞憂でした。

待遇として、ベースは前職の60万/月とほぼ同額。10-20%のパフォーマンスに応じたボーナスとRSU(ストックオプションみたいなもの。株がもらえる)が新たに加わった形になります。 え、なんか大台に乗りそうなんですけど。大丈夫なの??これはほんとに現実?? とお賃金が振り込まれるたびに思うのですが、どうやら現実のようです。

IT業界の職歴なし、異業種からの転職ということで苦戦を予想していましたが、蓋を開けてみれば選考パス率2/2。どうにかなりました。

しかしまだここがゴールではありません。人生後半、さらなる飛躍のためにこれからもやっていき!です💪💪

所感

f:id:ruby-U:20200101154722j:plain

ディープラーニングを用いたオブジェクトの傾き補正

これはなに?

紙の本を裁断してスキャンしたデータを、綺麗に見やすく整える必要があり、そのための傾き補正について、まずは基本的な考えが間違っていないかをトイデータで検証してみた記録です。

やりたいこと

紙の本はわりとアバウトな作りになっていて、製本時から既に、紙面の長方形に対して、文面が傾いていたりします。本を読むときに、我々はこの傾きを無意識に補正していて、ほとんどの場合は気にならないようです。

しかし、その本をスキャンし、データ化したものを電子端末でいざ読もうとすると、この傾きが暴力的なまでに効いてきます。非常に気持ち悪く、意識が散漫となり、とても読み進めることができません。

幸いに半手動でこの傾きを補正するようなツール*1があり、これを使って手動で補正していたのですが、電子書籍の普及とともに、この作業の価値が低下し…、ようするに面倒くさくなったのです。AIにこの作業をこなしてもらえれば非常に助かります。

Fashion-MNIST

手元にはそこそこの量のデータがあり、これを使って学習することもできますが、いきなり大量のデータを使ってディープラーニングを行うのは無謀です。私が学習に使えるGPUはGTX 1070が一枚。それだけです。ほぼ徒手空拳であり、これでサクサク学習が進むデータセットといえばMNISTぐらいです。

MNISTといえば、28x28ピクセルの手書きの数字画像が70000枚(訓練60000枚, テスト10000枚)…なのですが、今回の用途には適しません。なぜかというと、手書き文字には個々人の癖が反映されていて、傾きという要素もその中に織り込まれているからです。

MNISTの例(うまくいくようには見えない)

f:id:ruby-U:20181003215622p:plain

そこでFashion-MNISTです。MNISTと同じフォーマットでありながら、衣服などからなる10クラスで構成されていて、素晴らしいことに、傾きが補正されています。

Fashion-MNISTの例(綺麗に補正されている)

f:id:ruby-U:20181003215629p:plain

これを使って、まずは本来のラベル(商品のジャンルからなる10クラス)を捨てて、359度・359クラス・1度単位の角度をラベルとして与えたデータを考えます。 このとき、画像はラベルの角度で回転操作をされています。つまり、オブジェクトの回転角度を推定するタスクです。

0-359度、ランダムに回転したオブジェクト

f:id:ruby-U:20181004231232p:plain

多層パーセプトロン

359度・359クラス・1度単位

まずは単純な多層パーセプトロンで試してみます。GTX 1070でも、5並列ぐらいでパラメータの違う学習を進めることができて捗ります。

import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import backend as K

import numpy as np
import matplotlib.pyplot as plt

print("tf.version={0}".format(tf.__version__))

(train_images, train_labels), (test_images, test_labels) = keras.datasets.fashion_mnist.load_data()

num_classes = 359
classes = range(num_classes)

def generate_rotated_dataset(images):
    def _rotate(img, step):
        if step == 0: return img
        return rotate(img, angle=step, reshape=False, mode="constant", cval=0)
    new_images = []
    labels = []
    for img in images:
        label = np.random.choice(classes)
        new_images.append(_rotate(img, label))
        labels.append(label)
    return (np.array(new_images), np.array(labels))

train_images, train_labels = generate_rotated_dataset(train_images)
test_images, test_labels = generate_rotated_dataset(test_images)

train_images = train_images / 255.0
test_images = test_images / 255.0

from tensorflow.keras.layers import Flatten, Dense, Dropout, Activation
from tensorflow.keras.optimizers import Adam

def gpu_memory_lazy_allocate():
    config = tf.ConfigProto()
    config.gpu_options.allow_growth = True
    sess = tf.Session(config=config)
    K.set_session(sess)
gpu_memory_lazy_allocate()

def get_model():
    model = Sequential()
    model.add(Flatten(input_shape=(28, 28)))
    model.add(Dense(640))
    model.add(Activation('relu'))
    model.add(Dropout(0.2))
    model.add(Dense(640))
    model.add(Activation('relu'))
    model.add(Dropout(0.2))
    model.add(Dense(num_classes))
    model.add(Activation('softmax'))
    return model
model = get_model()

optimizer = tf.keras.optimizers.Adam(lr=0.0001)

model.compile(optimizer=optimizer, 
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])

reduce_lr = tf.keras.callbacks.ReduceLROnPlateau(
    monitor='val_loss', 
    factor=0.5, 
    patience=20, 
    verbose=1, 
    mode='auto', 
    min_delta=0.0001, 
    cooldown=10, 
    min_lr=0)
        
history = model.fit_generator(
    x=train_images, 
    y=train_labels,
    validation_data=(test_images, test_labels),
    callbacks=[reduce_lr],
    epochs=100000)

…うまくいきません。

f:id:ruby-U:20181003134250p:plain

これは訓練データの角度が常に一定であるからだと考えて、以下のように動的にデータを生成するようにします。 それに加えて、オーグメンテーションとして、左右反転を入れています。

from tensorflow.keras.models import Sequential

batch_size = 32

def generate_rotated_dataset(images):
    def _rotate(img, step):
        if step == 0: return img
        return rotate(img, angle=step, reshape=False, mode="constant", cval=0)
    new_images = []
    labels = []
    for img in images:
        label = np.random.choice(classes)
        new_images.append(_rotate(img, label))
        labels.append(label)
    return np.array(new_images), np.array(labels)

class TrainImageSequence(Sequence):
    def __init__(self, images, batch_size=1):
        self.images = images
        self.batch_size = batch_size
    
    def on_epoch_end(self):
        np.random.shuffle(self.images)
        
    def _horizontal_flip(self, img):
        if np.random.choice([True, False]):
            return cv2.flip(img, 1)
        return img
    
    def __getitem__(self, idx):
        start = idx * self.batch_size
        end = start + self.batch_size
        batch_x = self.images[start:end]
        batch_x = [self._horizontal_flip(x) for x in batch_x]
        return generate_rotated_dataset(batch_x)

    def __len__(self):
        return math.ceil(len(self.images) / self.batch_size)

class TestImageSequence(Sequence):
    def __init__(self, images, batch_size=1):
        self.x, self.y = generate_rotated_dataset(images)
        self.batch_size = batch_size
    
    def __getitem__(self, idx):
        start = idx * self.batch_size
        end = start + self.batch_size
        batch_x = self.x[start:end]
        batch_y = self.y[start:end]
        return batch_x, batch_y

    def __len__(self):
        return math.ceil(len(self.x) / self.batch_size)

train_gen = TrainImageSequence(train_images, batch_size=batch_size)
test_gen = TestImageSequence(test_images, batch_size=batch_size)
history = model.fit_generator(
    generator=train_gen, 
    validation_data=test_gen, 
    callbacks=[reduce_lr],
    epochs=100000)

f:id:ruby-U:20181003134057p:plain

先頭の6件

f:id:ruby-U:20181003134107p:plain

うまくいったようです。ちょっと見にくいですが、左から推定角度、真の角度、確信度です。

15度・600クラス・0.025単位。

さて、次のステップです。本題に戻って考えると、スキャンされたドキュメントというものは、おおよそ方向が揃っています。359度を考える必要はなく、紙面がこのぐらいは傾いてるかもしれない、という部分で解像度を高めてみます。

15度・600クラス・0.025単位を試します。この値は特に意味のあるものではなく、フィーリングで選びました。私が見分けられる傾きが0.1度ぐらいなので、0.025単位でうまく精度が出せるなら、素晴らしいのではないでしょうか。

参考: 上から順に1, 0.5, 0.25, 0.1 の傾き。

f:id:ruby-U:20181003140628p:plain f:id:ruby-U:20181003140635p:plain f:id:ruby-U:20181003140633p:plain f:id:ruby-U:20181003140631p:plain

以下の設定でデータを生成します。モデルは前と同じです。なお、ここからは画像のサイズを縦横それぞれ2倍にしています。多少ですが、実際のシーンに近づけるためです。

batch_size = 32
num_classes = 600
image_shape = (56, 56)

classes = range(num_classes)
class_values = np.linspace(-7.5, 7.5, num_classes)

def generate_rotated_dataset(images):
    def _resize(img, size):
        return cv2.resize(img, size)
    def _rotate(img, step):
        if step == 0: return img
        return rotate(img, angle=step, reshape=False, mode="constant", cval=0)
    new_images = []
    labels = []
    for img in images:
        label = np.random.choice(classes)
        img = _resize(img, image_shape)
        img = _rotate(img, class_values[label])
        img = img.reshape(*image_shape, 1)
        new_images.append(img)
        labels.append(label)
    return np.array(new_images), np.array(labels)
Top 1: 0.0795
Top 2: 0.1577
Top 3: 0.2344
Top 4: 0.3077
Top 5: 0.3801
Mean error: 0.1690

TopNの精度がビシッと出ておらず、ピークがぼやけている感じです。

先頭の6件

f:id:ruby-U:20181004231755p:plain

このモデルだと、必ず600クラスのうちから候補を選ぶことになるので、わからないときはわからないと判断してもらったほうがよいかもしれません。 600クラスに、-7.5 ~ 7.5 以外の角度からランダムにチョイスした1クラスを追加してみます。

15度・600 + 1クラス・0.025単位。

batch_size = 32
num_classes = 600
image_shape = (56, 56)

classes = range(num_classes)
class_values = np.linspace(-7.5, 7.5, num_classes)

def generate_rotated_dataset(images):
    def _resize(img, size):
        return cv2.resize(img, size)
    def _rotate(img, step):
        if step == 0: return img
        return rotate(img, angle=step, reshape=False, mode="constant", cval=0)
    new_images = []
    labels = []
    for img in images:
        if np.random.uniform(0, 1) > 0.1:
            label = np.random.choice(classes)
            angle = class_values[label]
        else:
            # 範囲外のクラス
            label = num_classes
            angle = np.random.uniform(7.5 + K.epsilon(), 352.5 - K.epsilon())
        img = _resize(img, image_shape)
        img = _rotate(img, angle)
        img = img.reshape(*image_shape, 1)
        new_images.append(img)
        labels.append(label)
    return np.array(new_images), np.array(labels)
Top 1: 0.0915
Top 2: 0.1821
Top 3: 0.2681
Top 4: 0.3505
Top 5: 0.4240
Mean error: 0.1394
False positive: 70 (0.0078)
False negative: 24 (0.0240)

15度内の画像を誤って外れクラスだと識別する確率が0.0078あり、その場合はTopNの計算からは省いています。 若干ですが、TopNの精度と平均誤差が改善しました。

VGG

さて、GPU負荷が軽い多層パーセプトロンでおおよその目星がついたので、やや重いVGGモデルで精度を高めていきます。オリジナルのVGGにバッチノーマリゼーションを加えています。  

from tensorflow.keras.models import Model
from tensorflow.keras.layers import Input, Flatten, Dense, Dropout, Activation, BatchNormalization
from tensorflow.keras.optimizers import Adam
from tensorflow.keras.applications import VGG16

def gpu_memory_lazy_allocate():
    config = tf.ConfigProto()
    config.gpu_options.allow_growth = True
    sess = tf.Session(config=config)
    K.set_session(sess)
gpu_memory_lazy_allocate()

def get_model():
    mnist_input = Input(shape=(*image_shape, 1), name='mnist_input')
    vgg16 = VGG16(input_shape=(*image_shape, 1), weights=None, include_top=False)
    vgg16.summary()
    vgg16_output = vgg16(mnist_input)
    x = Flatten(name='flatten')(vgg16_output)
    x = Dropout(0.8)(x)
    x = Dense(1024, activation='relu', name='fc1')(x)
    x = BatchNormalization()(x)
    x = Dropout(0.8)(x)
    x = Dense(1024, activation='relu', name='fc2')(x)
    x = BatchNormalization()(x)
    x = Dropout(0.8)(x)
    x = Dense(num_classes + 1, activation='softmax', name='predictions')(x)
    return Model(inputs=mnist_input, outputs=x)
model = get_model()

optimizer = tf.keras.optimizers.Adam(lr=0.001)

model.compile(optimizer=optimizer, 
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])

reduce_lr = tf.keras.callbacks.ReduceLROnPlateau(
    monitor='val_loss', 
    factor=0.5, 
    patience=20, 
    verbose=1, 
    mode='auto', 
    min_delta=0.0001, 
    min_lr=0)

history = model.fit_generator(
    generator=train_gen, 
    validation_data=test_gen, 
    max_queue_size=12*2, 
    workers=12, 
    use_multiprocessing=True,
    callbacks=[reduce_lr],
    epochs=100000)
Top 1: 0.4724
Top 2: 0.8104
Top 3: 0.9597
Top 4: 0.9906
Top 5: 0.9980
Mean error: 0.0148
False positive: 0 (0.0000)
False negative: 2 (0.1333)

途中で打ち切ったのですが、精度はやはり段違いです。 False-Positiveな要素がない(うまく識別できないものを外れクラスに追い出せていない)のが気になるので、この部分をロス関数で緩和してみます。

外れクラスへの誤答のペナルティを緩和したロス関数

def normalized_loss(y_true, y_pred):
    y_true_class = K.cast(y_true, dtype='float32')
    y_pred_class = K.cast(K.argmax(y_pred), dtype='float32')
    e = K.ones_like(y_true, dtype='float32') * num_classes
    a = K.equal(y_true_class, e) # y_true_class == error_class
    b = K.equal(y_pred_class, e) # y_pred_class == error_class
    a_eq_b = K.equal(a, b)
    a_ne_b = K.not_equal(a, b)
    
    case_0 = K.cast(a_eq_b, dtype='float32') # a == b
    case_1 = K.minimum(K.cast(a, dtype='float32'), K.cast(a_ne_b, dtype='float32')) # a AND (a != b)
    case_2 = K.minimum(K.cast(b, dtype='float32'), K.cast(a_ne_b, dtype='float32')) # b AND (a != b)
    
    w = 1 - y_pred[:, num_classes] * 0.2
    loss = K.sparse_categorical_crossentropy(y_true, y_pred)
    return loss * (case_0 + case_1 + case_2 * w)

model.compile(optimizer=optimizer, 
              loss=normalized_loss,
              metrics=['sparse_categorical_accuracy'])
Top 1: 0.7478
Top 2: 0.9483
Top 3: 0.9902
Top 4: 0.9972
Top 5: 0.9992
Mean error: 0.0066
False positive: 9 (0.0010)
False negative: 103 (0.1017)

誤差が0.1を超えたのは1個だけになりました。よさそうです。

先頭の6件

f:id:ruby-U:20181003145915p:plain

誤差の大きい順に6件

f:id:ruby-U:20181003145948p:plain

外れクラスだと誤識別された9件

f:id:ruby-U:20181003145953p:plain

一撃でウィンドウの内容をOCRする環境を構築した話

やったこと

もう大手各社のクラウドOCR APIの精度は十分実用的なレベルなので、単純にウィンドウの内容を投げればよい話なんだけれども、ウィンドウの内容全部をOCRしたかったり、あるいは矩形で選択したかったりするかもしれないので、スクリーンをキャプチャして、それら画像ファイルを生成するところまでは他のツールに任せることにして、「あるディレクトリ以下に画像ファイルが生成/移動されたら、それを自動でOCRする」作業をツールを書いて自動化しました。

成果物

github.com

インストール方法

zipファイルの中身を適当なところに解凍するだけです。

使い方

最小のコマンドは

cocr -d "PATH_TO_TARGET_DIRECTORY" -k "YOUR_GOOGLE_API_KEY"

-d で指定されたディレクトリを監視して、その中に画像ファイルが放り込まれたとき、自動で対応するjsonAPIを叩いたjsonレスポンスそのまま)、html(OCR結果を表示するHTMLファイル)を生成します。

-c オプションで結果をクリップボードに貼り付け。-s でhtmlファイルをブラウザで開く。-n で通知。他にもディレクトリ内の画像ファイルをまとめて処理する-b オプションなどがあります。

なお、使用しているGoogle Cloud Vision APIは月一定回数までは無料(2018/07 時点で最初の1000回は無料)ですが、-k オプションでのGoogle APIキーの指定は必須になります。APIキーを発行するときに、後で泣かないよう、適切な課金上限額を設定しておきましょう。

Authenticating to the Cloud Vision API  |  Cloud Vision API Documentation  |  Google Cloud

API キーの使用  |  ドキュメント  |  Google Cloud

イメージ

対象画像

f:id:ruby-U:20180713073812p:plain

クリップボード結果

f:id:ruby-U:20180713073741p:plain

HTMLを開いたところ

f:id:ruby-U:20180713074121p:plain

スクリーンキャプチャツールとの組み合わせ

SnapCrab www.fenrir-inc.com

GreenShot gigazine.net

などのツールの出力先を、cOCRの監視するディレクトリ以下に設定するだけです。あとはお好みに合わせて、スクリーンキャプチャツール側のキーバインドを設定すれば、「1ボタンで自動OCR、結果をクリップボードに保存」みたいなシステムが完成します。

PDFへの変換

cOCRは、Google Cloud Vision APIからのレスポンスをまるっとまるごとjsonファイルとして保存してるので、以下のツールとの組み合わせで、画像ファイルをOCRされたPDFに変換することも可能です。

github.com

github.com

Anki 2.1.0 (beta) で H.264 や MP3 の再生を可能にする方法

Anki のブラウザコンポーネントについて

まだ正式にリリースされていませんが、Anki 2.1.0 で Qt のバージョンが更新され、またブラウザ機能を提供するコンポーネントとして新しく QtWebEngine が導入されました。 Anki 2.1.0 beta42 では Qt, QtWebEngine のバージョンは 5.9.2 です。

Qt 5.9.2 Change Files - Qt Wiki

changes-5.9.2\dist - qt/qtwebengine.git - Qt WebEngine

 - Chromium Snapshot:
   * Security fixes from Chromium up to version 61.0.3163.79
    Including: CVE-2017-5092, CVE-2017-5093, CVE-2017-5095, CVE-2017-5097,
        CVE-2017-5099, CVE-2017-5102, CVE-2017-5103, CVE-2017-5107,
        CVE-2017-5112, CVE-2017-5114, CVE-2017-5117 and CVE-2017-5

とあるので、QtWebEngine 5.9.2 は Chromium 61 相当でしょうか。

一方で、Anki 2.0.x が使用しているブラウザコンポーネントは QtWebKit 2.2 で、これは 2011 年のリリース…。

QtWebKit 2.2.0 is released! - Qt Blog

QtWebKitFeatures22 – WebKit

QtWebKitRelease22 – WebKit

考えるのも嫌になるぐらい大昔の化石ですね。もう Anki 2.0.x のことは忘れてしまいましょう。

さて、Anki を最新のものにアップデートすれば、大抵のデッキはまず問題なくそのまま動作するのですが、以前から例えば Android 向けの実装である AnkiDroid では、より新しいブラウザコンポーネントが使用できていました。 極めて稀だとは思いますが、このような環境向けに、HTML5 Media API を使用する(Anki の [sound: xxx.mp3] タグではなく、JavaScriptでaudio/videoを直接操作している)デッキを作成し、運用している場合。または新しくこのようなデッキを導入する場合、最新のデスクトップ向け Anki でも、問題が生じるかもしれません。以下のような制限があるためです。

Qt WebEngine supports the MPEG-4 Part 14 (MP4) file format only if the required proprietary audio and video codecs, such as H.264 and MPEG layer-3 (MP3), have been enabled.

Qt WebEngine Features | Qt WebEngine 5.9

特定のメディア(H.264, MP3 など)について、ライセンス上の制限のため、デフォルトでは再生することができません。この問題を回避するには、-proprietary-codecs オプションを付加してコンパイルした QtWebEngine が必要です。

コンパイルしたものはこちら。商用利用にはライセンスが必要です

コンパイル

環境

>perl -v

This is perl 5, version 26, subversion 2 (v5.26.2) built for MSWin32-x64-multi-thread

~略~

>python --version
Python 2.7.13

ソース

http://download.qt.io/archive/qt/5.9/5.9.2/single/ から http://download.qt.io/archive/qt/5.9/5.9.2/single/qt-everywhere-opensource-src-5.9.2.tar.xz を拾ってきます。

依存ツール

手順

ソースを解凍後、生成された qtwebengine ディレクトリに、依存ツール(bison, flex, gperf)を放り込みます。 開発者コマンドプロンプト for VS2015 で qtwebengine を開き、-proprietary-codecs オプションを付けて qmake を実行します。結果は以下のようなものです。

C:\Qt\qt-everywhere-opensource-src-5.9.2\qtwebengine>"C:\Qt\5.9.2\msvc2015\bin\qmake.exe" -- -proprietary-codecs

Running configuration tests...
Done running configuration tests.

Configure summary:

Qt WebEngine:
  Embedded build ......................... no
  Pepper Plugins ......................... yes
  Printing and PDF ....................... yes
  Proprietary Codecs ..................... yes
  Spellchecker ........................... yes
  WebRTC ................................. yes
  Using system ninja ..................... no

Qt is now configured for building. Just run 'nmake'.
Once everything is built, Qt is installed.
You should NOT run 'nmake install'.
Note that this build cannot be deployed to other machines or devices.

Prior to reconfiguration, make sure you remove any leftovers from
the previous build.

Proprietary Codecs ..................... yes になっていることを確認してください。確認できたら nmake するだけです。数時間以上かかります。気長に待ちましょう。

Anki コンポーネントの上書き

C:\Program Files (x86)\AnkiQt5WebEngineCore.dll などがありますので、これらを成果物に置き換えます。

以上です。お疲れ様でした。

マウスジェスチャーツール Crevice 3.2.184 をリリースしました

カッコいいロゴを頂いたのでテンション上がってそのまま18時間コーディングしたらメジャーバージョンが1つ上がっていました。

f:id:ruby-U:20171223133526p:plain

どうですか、超クールなのでは?? イエーイ🎉🎉

Crevice 3.2.184 リリースノート

1月にリリースしたCrevice 2.5 から Crevice 3 へメジャーバージョンが1つ上がり、いくつかの大きな変更が加わっています。

  • 新しいクールなアイコン! アプリケーションの外観をブラッシュアップしました。

  • ユーザースクリプトのキャッシュ機構を追加。Crevice 2.5 と比較して起動速度が最大で50倍高速になりました。

  • コマンドラインインターフェイスの追加。ユーザースクリプトの場所、タスクトレイアイコン非表示、プロセス優先度、詳細なログ出力などの設定を引数として指定できるようになりました。

特に頑張ったところは起動速度の高速化です。CreviceではユーザースクリプトC# Scriptを使っていて、Crevice 2.5 ではRoslynで毎回コンパイルしていたのですが、このリソースが無駄だし起動時間は遅くなるしいいことなしだったので、ユーザースクリプトコンパイルした後のアセンブリをうまいことキャッシュできないかなーと試行錯誤してたら偶然うまく行きました。雰囲気でやっている。

技術資料

以下、できたてホヤホヤのソースから抜粋して、Roslynでユルい C# Script をコンパイルして、アセンブリからそれを評価する方法を書いておきます。アセンブリに GetType して、 GetMethod して Invoke という資料はよく見かけますが、 C# Script 特有のトップレベルに変数がくるようなコードでうまくそれをやる方法が見当たらなかったので貴重な資料だと思います(ホントか?)

Crevice 2 の実装(ユルいC# Script にglobalsを与えて評価する)

ユーザースクリプトから Script を作ります。

private Script ParseScript(string userScript)
{
    var stopwatch = new Stopwatch();
    Verbose.Print("Parsing UserScript...");
    stopwatch.Start();
    var script = CSharpScript.Create(
        userScript,
        ScriptOptions.Default
            .WithSourceResolver(ScriptSourceResolver.Default.WithBaseDirectory(UserDirectory))
            .WithMetadataResolver(ScriptMetadataResolver.Default.WithBaseDirectory(UserDirectory))
            .WithReferences("microlib")                   // microlib.dll
            .WithReferences("System")                     // System.dll
            .WithReferences("System.Core")                // System.Core.dll
            .WithReferences("Microsoft.CSharp")           // Microsoft.CSharp.dll
            .WithReferences(Assembly.GetEntryAssembly()), // CreviceApp.exe
                                                          // todo dynamic type
        globalsType: typeof(Core.UserScriptExecutionContext));
    stopwatch.Stop();
    Verbose.Print("UserScript parse finished. ({0})", stopwatch.Elapsed);
    return script;
}

そしてこの Script にコンテキスト ctx を globals として与えて評価します。ctxLeftButton, MiddleButton のような設定のためのトークン、 ジェスチャー定義に関する @when 関数や、Config のようなインスタンスTooltip, Balloon のようなユーティリティ関数などをメンバーとして持ち、それを globals として与えることで、ユーザースクリプト評価時のコンテキストとしています。

public class UserScriptExecutionContext
{
    public readonly DSL.Def.LeftButton   LeftButton   = DSL.Def.Constant.LeftButton;
    public readonly DSL.Def.MiddleButton MiddleButton = DSL.Def.Constant.MiddleButton;
// 略
    public Config.UserConfig Config
    {
      get { return Global.UserConfig; }
    }

    public DSL.WhenElement @when(DSL.Def.WhenFunc func)
    {
        return root.@when(func);
    }

    public void Tooltip(string text)
    {
        Tooltip(text, Global.UserConfig.UI.TooltipPositionBinding(WinAPI.Window.Window.GetPhysicalCursorPos()));
    }

    public void Balloon(string text)
    {
        Balloon(text, Global.UserConfig.UI.BalloonTimeout);
    }
// 略
}
private IEnumerable<Core.GestureDefinition> EvaluateUserScript(Script userScript, Core.UserScriptExecutionContext ctx)
{
    var stopwatch = new Stopwatch();
    Verbose.Print("Compiling UserScript...");
    stopwatch.Start();
    var diagnotstics = userScript.Compile();
    stopwatch.Stop();
    Verbose.Print("UserScript compilation finished. ({ 0})", stopwatch.Elapsed);
    foreach (var dg in diagnotstics.Select((v, i) => new { v, i }))
    {
        Verbose.Print("Diagnotstics[{0}]: {1}", dg.i, dg.v.ToString());
    }
    Verbose.Print("Evaluating UserScript...");
    stopwatch.Restart();
    userScript.RunAsync(ctx).Wait();
    stopwatch.Stop();
    Verbose.Print("UserScript evaluation finished. ({0})", stopwatch.Elapsed);
    return ctx.GetGestureDefinition();
}

評価後 ctx にはマウスジェスチャーの設定が保存されています。

Crevice 3 の実装(ユルい C# Script から生成したアセンブリをロードして globals を与えて評価する)

ユーザースクリプトから Script を作ります。

private Script ParseScript(string userScript)
{
    var stopwatch = new Stopwatch();
    Verbose.Print("Parsing UserScript...");
    stopwatch.Start();
    var script = CSharpScript.Create(
        userScript,
        ScriptOptions.Default
            .WithSourceResolver(ScriptSourceResolver.Default.WithBaseDirectory(UserDirectory))
            .WithMetadataResolver(ScriptMetadataResolver.Default.WithBaseDirectory(UserDirectory))
            .WithReferences("microlib")                   // microlib.dll
            .WithReferences("System")                     // System.dll
            .WithReferences("System.Core")                // System.Core.dll
            .WithReferences("Microsoft.CSharp")           // Microsoft.CSharp.dll
            .WithReferences(Assembly.GetEntryAssembly()), // CreviceApp.exe
                                                          // todo dynamic type
        globalsType: typeof(Core.UserScriptExecutionContext));
    stopwatch.Stop();
    Verbose.Print("UserScript parse finished. ({0})", stopwatch.Elapsed);
    return script;
}

ここまでは以前の実装と同じです。次にアセンブリのバイナリを作ります。次回起動時にアプリケーションのバージョンやユーザースクリプトに変更がなければ、これをキャッシュとして使うことで起動時間を大幅に短縮することができます。

private UserScriptAssembly.Cache CompileUserScript(UserScriptAssembly usa, string userScriptCode, Script userScript)
{
    var stopwatch = new Stopwatch();
    Verbose.Print("Compiling UserScript...");
    stopwatch.Start();
    var compilation = userScript.GetCompilation();
    stopwatch.Stop();
    Verbose.Print("UserScript compilation finished. ({0})", stopwatch.Elapsed);

    var peStream = new MemoryStream();
    var pdbStream = new MemoryStream();
    Verbose.Print("Genarating UserScriptAssembly...");
    stopwatch.Restart();
    compilation.Emit(peStream, pdbStream);
    stopwatch.Stop();
    Verbose.Print("UserScriptAssembly generation finished. ({0})", stopwatch.Elapsed);
    return usa.CreateCache(userScriptCode, peStream.GetBuffer(), pdbStream.GetBuffer());
}

キャッシュをロードして、以前と同様にコンテキスト ctx を globals として与えて評価します。アセンブリに GetType("Submission#0") して、 GetMethod("") して得られた関数を Invoke するのですが、この時に ctxnew object[] { new object[] { ctx, null } } のような形で2番めの引数として渡すのがポイントです。

private IEnumerable<Core.GestureDefinition> EvaluateUserScriptAssembly(UserScriptAssembly.Cache cache, Core.UserScriptExecutionContext ctx)
{
    var stopwatch = new Stopwatch();
    Verbose.Print("Loading UserScriptAssembly...");
    stopwatch.Start();
    var assembly = Assembly.Load(cache.pe, cache.pdb);
    stopwatch.Stop();
    Verbose.Print("UserScriptAssembly loading finished. ({0})", stopwatch.Elapsed);
    Verbose.Print("Evaluating UserScriptAssembly...");
    stopwatch.Restart();
    var type = assembly.GetType("Submission#0");
    var factory = type.GetMethod("<Factory>");
    var parameters = new object[] { new object[] { ctx, null } };
    var result = factory.Invoke(null, parameters);
    stopwatch.Stop();
    Verbose.Print("UserScriptAssembly evaluation finished. ({0})", stopwatch.Elapsed);
    return ctx.GetGestureDefinition();
}

評価後 ctx にはマウスジェスチャーの設定が保存されています。

仕組み的に Crevice 2 と Crevice 3 でのユーザースクリプトの実行は全く同様の結果になる…はずなのですが、いまいち確信が持てないので失敗時にはフォールバックするようには実装しています。

private IEnumerable<Core.GestureDefinition> GetGestureDef(Core.UserScriptExecutionContext ctx)
{
    var userScriptCode = GetUserScriptCode();
    var userScript = ParseScript(userScriptCode);
    if (!Global.CLIOption.NoCache)
    {
        try
        {
            var cache = GetUserScriptAssemblyCache(userScriptCode, userScript);
            return EvaluateUserScriptAssembly(cache, ctx);
        }
        catch (Exception ex)
        {
            Verbose.Print("Error occured when UserScript conpilation and save and restoration; fallback to the --nocache mode and continume");
            Verbose.Print(ex.ToString());
        }
    }
    return EvaluateUserScript(userScript, ctx);
}

うまく動かないよーという方がもしいらっしゃいましたらご連絡ください。

DisplayPortディスプレイには妖精が住んでいる(Windowsのディスプレイ構成をプロファイルごとに切り替える)

以前からWindowsのディスプレイ周りには困惑していて、特にDisplayPort(以下DP)が絡んだ場合には、DPの電源オフ時にはデバイス自体がOS上でオフ扱いになるという仕様からほんとにもうどうしようもない挙動をします。ディスプレイがオフ扱いになる、あるいは復帰するタイミング、またはデフォルトのモニター解像度の存在などが渾然一体となり、休憩から帰った後にウィンドウが元の位置にあることは決して保証されません。そのサイズすらもです。

この問題の対処法はいろいろあります。とりあえず簡単に紹介します。

DPディスプレイの電源を切らずに運用する

シンプルに因果律から導き出されるアイディアです。しばらく放っておくとDPディスプレイはスタンバイ状態に入り、この状態への移行、この状態からの復帰では特に問題は発生しません。 この解決策は良さそうに見えますが、席を立った後、あなたのディスプレイがまだ光を放っていることに気づいた親切な同僚の存在について考察してみましょう。

プログラムから即座にディスプレイをスタンバイ状態に移行する方法を以下に紹介します。あなたのディスプレイのインジケーターがスリープ時にモンスターたちを惹きつけないことを祈ります。

gist9167e2b52ab8033414fd70357079070e

モニターの解像度に関するレジストリ値を変更する

モニターを省電力から復帰させると解像度やウィンドウのサイズが小さくなるのを解決する : プログ 塵の雨日記: DisplayPort接続時のディスプレイのオンオフによるウィンドウの再配置について PC用4Kモニタでウインドウが小さく左上に偏る問題をレジストリ編集で解決

多くの方の直った!という声が寄せられていますが、この方法はマルチディスプレイ環境でウィンドウが別ウインドウに移動してしまう問題については効果がありません。

DPケーブルをハックする

DisplayPortの切断を回避するアダプター - Qiita DisplayPort接続でモニターの電源を切ると認識が外れるのを何とかできる? - Aquablews_apps.dump()

私はこの方法を試していませんが、コストと互換性問題について納得できれば試す価値はあるでしょう。

ウィンドウ位置を管理するツールを使う

あるディスプレイにウィンドウを集めるもの、ウィンドウの状態を保存・復元するものなどいろいろありますが、多くのツールが、異なるDPIを含むマルチディスプレイ環境で正常に動かず、ひとまず断念しました。いいツールがあったら教えてください。

ディスプレイ構成をプロファイルごとに切り替える

試行錯誤の後、最終的にこの方法にたどり着きました。要するに、悪いDPディスプレイは能動的に取り外して、DPディスプレイがオフ扱いになるタイミングを制御しようというものです。前述のレジストリ値を使う方法と組み合わせて、この問題を多少改善することができます。

1. Display Changer IIでWindowsのディスプレイ構成を保存・復元する

Display Changer II « 12noon

例えば以下のコマンドでProfile1.xmlにディスプレイの設定を保存し、復元することができます。

dc2.exe -create="Profile1.xml"
dc2.exe -configure="Profile1.xml"

このxmlファイルはその時点でのWindowsのディスプレイの設定を保存したものなので、グラフィックドライバの更新などでデバイス名などに大きな変更が加わった場合はその都度作り直す必要があります。

2. コマンドをタスクとして登録する

dc2.exe -configure="Profile1.xml" のようなコマンドを管理者権限のタスクとして、タスクスケジューラに登録しておきます。こうすることで、通常権限のAutoHotKeyアプリケーションからコマンドを実行できます。 ここでは例として、SwitchDisplayProfile1, SwitchDisplayProfile2, SwitchDisplayProfile3の3つのプロファイルを作成しました。

3. AutoHotKeyからディスプレイのプロファイルを切り替える

AutoHotKeyスクリプトには前述のDisplayStandBy.exeと管理者権限タスクSwitchDisplayProfile1, SwitchDisplayProfile2, SwitchDisplayProfile3を起動するフック、Ctrl+Shift+Alt+F9, Ctrl+Shift+Alt+F10, Ctrl+Shift+Alt+F11, Ctrl+Shift+Alt+F12を記述しました。

gist869012765c79d16d2604292c890c5f54

Amazonの過去の売れ筋商品データ(月間)を取得する

Amazonの商品コードを大量に集める必要があり、いろいろ方法を調べていると、次のようなものを見つけた。なかなか有用だと思うので取得方法を纏めておく。

過去の売れ筋商品データ(月間)

f:id:ruby-U:20171111085507p:plain

対象年月と商品カテゴリを選択してボタンを押せばデータへのリンクが表示される。

f:id:ruby-U:20171111085512p:plain

データのURLは固定されているようで、認証なども必要ない。好きなように取ってくればよさそうだ。

中身は普通のTSVで、1行のヘッダあり。なお、これは2011-01の本カテゴリのデータ。懐かしい…。 f:id:ruby-U:20171111090205p:plain

2011-01から2017-09までのデータを集めたものをダウンロードできるようにしておくので、面倒な人はこちら(Google Drive)

なお、注意点として、カメラカテゴリの他にも一部データに抜けがある。DVDカテゴリの2011-11, 2011-12など。恐らく元データが存在しない。

以下のスクリプトで作成した。

gist.github.com