買い物ログ振り返り 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ヶ月検査

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

これはなに?

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

やりたいこと

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

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

幸いに半手動でこの傾きを補正するようなツール*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 などがありますので、これらを成果物に置き換えます。

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