Epwing辞書をtsvに変換するツールを作った

Mouse DictionaryでEpwing辞書を使いたいと以前から考えていて、年末年始の時間を使って、ひとまず動作するものを作ってみました。

ちなみにMouse Dictionaryについてはこちら

qiita.com

使い方

1. Epwing辞書を用意する

dessed にて変換可能なものが安価でよいです。

2. EBWin4をインストールする

ソフトウェア本体は使いませんが、同時にインストールされるmapファイルが必要です。 ebstudio.info

3. eb2tsvをダウンロードする

https://github.com/rubyu/eb2tsv/releases/download/v1.0.0/eb2tsv-1.0.0.jar

4. 次のようなコマンドでtsvを生成する

java  -Dfile.encoding=utf-8  -jar eb2tsv-1.0.0.jar --ebmap C:\Users\ユーザー名\AppData\Roaming\EBWin4\GAIJI\KENE7J5.map -d C:\Epwing\KENE7J5 > result.tsv

このとき、�のような文字が出力結果に含まれる場合、mapファイル内にその文字に対応する定義が不足しています。 mapファイルに定義を追加することで解消します。mapファイルはまだ完全ではないようなので、フィードバックをEBWin4に送るとみんな嬉しいかもしれません。

その他のソリューション

これ実装してから思いましたが、Epwing辞書は語義が長大すぎてこの用途に向いてないですね…。E-DIC2 英和辞書あたりが最適な気がします。

rubyu.hatenablog.com

買い物ログ振り返り 2020年版

パナソニック 食器洗い乾燥機「プチ食洗」(3人用・食器点数18点) NP-TCM4-W (ホワイト)

一人暮らしでも食器を洗ってる時間は無駄なので省いていきたいです。3人用とありますが、調理器具を入れるならこのぐらいのサイズが最低限必要という感じです。スペースが許せばもう少し大きいのを入れたかったですが…。

グリシン 1kg 睡眠 アミノ酸 純度100% 粉末 眠りサポート パウダー 付属スプーン付き

眠りが深くなってる実感があります。

SHARP ヘルシオ ホットクック 2.4L 電気無水鍋無線LAN/音声発話搭載) ホワイト系 KN-HW24E-W

煮込む系の料理を作る機会が多いなら買いです。僕はロールキャベツとおでんをよく作ります。材料を放り込んで放っておくだけなので時間がそこそこ節約できます。使用後にパーツを洗う手間はありますが、食洗機と組み合わせてなんとかしましょう。

全自動コーヒーメーカー マグニフィカS ミルク泡立て:手動 ブラック ECAM22112B

これは実家に導入したのですが、評判がよいです。僕はロングブラックかフラットホワイトを飲みたいので、スチームドミルクも作れるエスプレッソ機でまさに!という機種です。前職で会社の近くの美味しいコーヒー屋さんにみんなで通って舌が完全にオーストラリアンになってしまった…。

TOPPING DX7 Pro ヘッドホンアンプ Bluetooth5.0 DSD1024 32bit/768kHz LDAC/AAC/SBC/USB デジタルアンプ XLR dac(黒&リモコン)

それほど期待してなかったのですが、TEAC UD-505を処分して今はこれがメインのヘッドフォンアンプになってます。

beyerdynamic DT1990PRO 開放型モニターヘッドホン

開放型なので蒸れないし、音もとても自然でよいです。

www.soundhouse.co.jp

東プレ REALFORCE R2 TKL SA 静音/APC機能 日本語 静電容量方式 USB 荷重30g 昇華印刷(墨) かなナシ ブラック R2TLSA-JP3-BK-SHK

とにかく軽くて疲れない。ストローク長を調節するためのクッションシートを入れることができたり、行き届いています。僕は2枚買って左右に並べて使ってます。分割キーボードより面積を取りますが、大した問題ではありません。

α7R II ILCE-7RM2 ボディ デジタル一眼カメラ SONY 新品・送料無料(沖縄・離島除く) + タムロン 交換レンズ 28-75mm F/2.8 Di III RXD(Model A036)【ソニーEマウント】

カメラに詳しい友人に相談したらなんか想像の倍ぐらいのをポチることになったやつです。画質がよいし、シャッター音も気持ちよいのでよい買い物でした。

ふるさと納税】D-15 丸亀産 シャインマスカット 2kg

ふるさと納税ではこれが一番評判がよかったです。

ケンジントン 【正規品・1年保証】VeriMark 指紋認証キー Windows Hello 機能対応 FIDO U2F 準拠 2要素認証 K67977JP

デスクトップとか安いノートとかに指紋認証を後付けできるやつです。精度も悪くなくて便利に使っています。

45 x 200cm PVC Back Sticky Waterproof Movable Kid Graffiti Writing Board White Board Roll Up Reusable Message Board

これでクローゼットのドアをホワイトボードにしました。省スペースで便利なのですが、書いたまま放置すると線が消えないのでアルコールで落としてます。

www.aliexpress.com

2020 人気 おすすめ メンズ 対象 送料無料 男性用 浴衣 1枚 M L LLサイズ ゆかた 福袋 綿100% ゆかた レトロ kis 男物 男浴衣 紳士物 業務用 イベント用に 父の日 複数枚購入可能 大人 単品 商品単価:1,100円

僕は浴衣で寝てるのでパジャマ代わりです。今年は需要が低迷したせいか、1枚 1000円ほどと破格でした。ちなみにまだ売ってます。。

item.rakuten.co.jp

ICL手術

目の中にレンズを埋め込む、近視/乱視の矯正手術です。もうそろそろ半年になりますが、概ね快適です。 難点は薄暗いシーンで光がぼやけることと、目から20cmぐらいまでの距離にピントが合わないことぐらいです。その他のシーンでは眼鏡が不要になったことのメリットを十分感じています。

www.sannoclc.or.jp

Google Play Musicが終わってしまったのでプレイリストを移行するためのスクリプトを書いた

概要

タイトルの通り、Google Play Music(GPM)のサービス終了に伴ってプレイリストを移行する必要が生じたので、そのためのツールを簡単に実装しました。

元々flacで購入していたものはそっちを使いたかったので、flacファイルが手持ちにあればflacのパスに差し替えるような機能も加えました。

引数に対応してないため、実行に際しmain.pyを書き換える必要があります。ダサいですが、常用するものではないのでOKというジャッジです。

github.com

乗り換え先サービスは見つかっておらず、ひとまずfoobar2000で再生する予定です。

以下は実装時のメモ。

Dumped Files

ダンプされたファイルは次のような名前で複数個になることもある。そのまま同一のフォルダに展開して問題ないように見える。

takeout-20201011T062914Z-001.zip
takeout-20201011T062914Z-002.zip
...

メモ

Directory Tree

tree
└─Takeout
    └─Google Play Music
        ├─トラック
        ├─プレイリスト
        │  ├─#All-Thumbs-Up
        │  │  └─トラック
        │  ├─2019-01-07
        │  │  └─トラック
        │  ├─2019-02-01
        │  │  └─トラック
        │  └─高く評価した曲
        └─ラジオ ステーション
            └─最近利用したステーション

Playlist Directory

プレイリスト直下のメタデータ.csvは特に重要なデータを持ってなさそう。トラック以下のものをパースする必要がある。

├─2020-06-30
│  │  メタデータ.csv
│  │
│  └─トラック
│          XXXX.csv
│          YYYY.csv

ヘッダは次のようなもの。

タイトル,アルバム,作成者,時間(ミリ秒),評価,再生回数,削除済み,プレイリストのインデックス

Track Directory

Google Play Music直下のトラックフォルダ。ここにはmp3ファイルとそれに対応するcsvファイルが存在する。 csvファイルのヘッダは次のようなもの。

タイトル,アルバム,作成者,時間(ミリ秒),評価,再生回数,削除済み

プレイリスト -> トラック以下のcsvファイルとここのcsvファイルはTitile, Album, Artistで結合できるが、これらのcsvとMP3を結びつける方法がない。 そのため、mp3から直接タグを吸い出す実装にして、ここのcsvファイルのデータは使用しないことにした。

テキストストリーミングでAnime(POC)

先週末にサシシさん(@sashishi_EN)と🍣オフをしてたときに、「Spritz、Kindleのワードランナーのように視点を固定して動画の字幕を読めたら便利だと思うんですよ」という話をしてもらって、僕も興味があったのでPythonで実装してみた。

動いているところ

テキストストリーミングというらしい。概念は知ってたけど名前は知らなかった。

やってること

字幕のフォーマットについては動画コンテンツで英語学習をしているとなぜか詳しくなるので皆さんよくご存知だとは思うんですが、例えばassフォーマットでは次のようにスタイルとイベントで字幕が構成されています。

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

例えば次のようなイベントを

Dialogue: 0,0:00:10.40,0:00:13.96,Main,S,0000,0000,0000,,Tama, I want you to collect stones \Nthat are about as big as a core.

このように単語ごとに分割できればやりたいことはひとまず達成できます。

Dialogue: 0,0:00:10.40,0:00:10.63,Main,S,0,0,0,,Tama,
Dialogue: 0,0:00:10.63,0:00:10.87,Main,S,0,0,0,,I
Dialogue: 0,0:00:10.87,0:00:11.11,Main,S,0,0,0,,want
Dialogue: 0,0:00:11.11,0:00:11.34,Main,S,0,0,0,,you
Dialogue: 0,0:00:11.34,0:00:11.58,Main,S,0,0,0,,to
Dialogue: 0,0:00:11.58,0:00:11.82,Main,S,0,0,0,,collect
Dialogue: 0,0:00:11.82,0:00:12.06,Main,S,0,0,0,,stones
Dialogue: 0,0:00:12.06,0:00:12.29,Main,S,0,0,0,,that
Dialogue: 0,0:00:12.29,0:00:12.53,Main,S,0,0,0,,are
Dialogue: 0,0:00:12.53,0:00:12.77,Main,S,0,0,0,,about
Dialogue: 0,0:00:12.77,0:00:13.01,Main,S,0,0,0,,as
Dialogue: 0,0:00:13.01,0:00:13.24,Main,S,0,0,0,,big
Dialogue: 0,0:00:13.24,0:00:13.48,Main,S,0,0,0,,as
Dialogue: 0,0:00:13.48,0:00:13.72,Main,S,0,0,0,,a
Dialogue: 0,0:00:13.72,0:00:13.95,Main,S,0,0,0,,core.

データとして開始と終了のタイムスタンプがあるので、これも単語の数で割って等分に割り当てる必要があります。

[Events]
Format: Layer,Start,End,Style,Name,MarginL,MarginR,MarginV,Effect,Text

実装(ライブラリ調べたりしながら1時間)

!pip install ass
import ass
import re
import copy

def split_text_to_words(text):
  t = re.sub(r"(\\n|\\N)", " ", text)
  return re.split(r"\s+", t)

def split_event(ev):
  words = split_text_to_words(ev.text)
  if len(words) == 0: return []

  duration = ev.end - ev.start
  dpw = duration / len(words)
  
  split_events = []
  for i, word in enumerate(words):
    ev2 = copy.deepcopy(ev)
    ev2.text = word
    ev2.start = ev.start + (dpw * i)
    ev2.end = ev2.start + dpw
    split_events.append(ev2)
  return split_events

with open("in.ass", encoding='utf_8_sig') as in_f:
  doc = ass.parse(in_f)
  split_events = []
  for ev in doc.events:
    events = split_event(ev)
    split_events.extend(events)
  doc.events.clear()
  doc.events.extend(split_events)

  with open("out.ass", "w", encoding='utf_8_sig') as out_f:
    doc.dump_file(out_f)

ぱっと思いついた残タスク

  • 単語が表示されている時間は揃えたほうがよさそうな?
  • テキストストリーミングに変換すべきところとそうでないところがありそう
  • タグを残しつつ分割しなければいけない…?
  • サシシさんへの引き継ぎ😊

ICL手術を受けてきたのでメモ

ICL(Implantable Contact Lens)とは眼内コンタクトレンズのことです。 ICL手術ではその名の通り眼の中に超ちっちゃいコンタクトレンズを埋め込みます。

受けることに決めた理由はいろいろありますが、一番の理由としてはメガネが邪魔だということです。普段意識してないだけで、眼鏡の重さとか、視界に入るフレームとか絶対仕事や勉強などで集中の妨げになってますからね。あとコンタクトレンズは面倒。なので裸眼視力を上げられる手術があるならまあ受けてみるかなと。

手術までの通院は2回。

  • 初回: 簡単な検査&カウンセリング (2h)
  • 2回目: より高度な検査&レンズ度数決定&カウンセリング (3h)

手術は20分で日帰り。

  • 3回目: 手術 (入院~退院まで7h、手術自体は20分)

直後の検査で1.0+と、その日のうちにもう仕事ができるぐらいには視力が出ていました。ただし、この記事を書いてる時点(手術後10時間)で、いわゆるハロー、グレアはどちらも確認できます。ハローは光の輪っか。グレアは光の滲みです。ハローは綺麗でいいのですが、グレアはダメですね。 左目はハローだけ。右目はハロー、グレアどちらも。しばらくすれば落ち着くという話を事前に先生から聞いていますが、落ち着くというのが光学的に解消するのか、それとも人体の神秘!により補正されるようになるのかは気になるところです。 手術後は術後検査をだんだん間隔をバックオフしながらやっていきます。

  • 4回目: 術後1日検査

手術翌日ですが、左右とも1.5、両眼で2.0の視力が出てました。若干左眼の手術痕が疼く…感じがありますが経過は良好とのこと。 電車の窓から外の景色を眺めていてもものすごく遠くまで綺麗に見えて、しかも眼鏡のように違和感がないのが素晴らしいです。

  • 5回目: 術後1週間検査
  • 6回目: 術後1ヶ月検査

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