のんびりしているエンジニアの日記

ソフトウェアなどのエンジニア的な何かを書きます。

画像のDataAugmentationで最速のライブラリを探してみた

皆さんこんにちは
お元気ですか。年末に向けて燃え尽きようとしています。

OpenCVアドベントカレンダー第22日です。

qiita.com

今回、PythonのDataAugmentationで最速の方法を探してみます。
画像処理のDeepLearningでは、頻繁にDataAugmentationが使われています。

このData Augmentationは画像に結果がない程度に変換を加え、
ニューラルネットワークに入力する手法です。

以前、こんな資料を作りました。

www.slideshare.net

DataAugmentationにも様々な手法がありますが、次の処理する場合が非常に多いです。

  1. 画像をファイルから取得する。
  2. リサイズ
  3. アフィン変換
  4. 左右反転

この処理ですが、入力時に一定の確率で実行されます。
そのため、この処理が遅い場合には性能のボトルネックになります。
そのため、実装を高速化することは学習時間の短縮に非常に有用となります。

GPUが関わる処理で学習を高速化したい場合はCPU部も高速化することが必要でしょう。

速度を計測するにあたり、使用したライブラリをご紹介

OpenCV

当記事のアドベントカレンダーの掲載先となるフレームワーク
かつ、最も画像処理で有名なライブラリである言わずもがなOpenCVです。
Wikipediaの概要では、次のように記載されています。

画像処理・画像解析および機械学習等の機能を持つC/C++JavaPythonMATLAB用ライブラリ

opencv.org

Scikit-image

私個人はこれを推しています。
Pythonで完結するため、非常にインストールが楽です。
こちらも相当の人が使っており、サンプルも非常に多いです。

scikit-image: Image processing in Python — scikit-image

Pillow

このライブラリのサンプルを見たことある人はかなりいるのではないでしょうか?
Pythonの画像処理ライブラリで、かつて存在したPython Imaging Library (PIL)の fork プロジェクトです。

普段、Scikit-imageを利用している私は殆ど使ったことがありませんが、今回試しに使ってみました。

python-pillow.org

ぶっちゃけ全部画像処理ライブラリとしかいっていない

計測

では、早速計測を行います。
本手法における計測はtimeitを利用して実施します。

対象となる画像は皆さん大好きLennaさん(高225x幅225)です。
f:id:tereka:20171004005907j:plain

※ライブラリによって実装方法が異なるので、結果が多少ことなることがあるかもしれません。
 その点は確認して、利用してください。

画像読み込み

画像読み込みとすると非常に曖昧のため、今回は次の定義とします。

  1. 画像をファイルから読み込む
  2. 0-1へ画像を正規化
  3. float32に変換する
OpenCV

ソースコード

%timeit cv2.imread("./lenna.jpeg").astype(np.float32) / 255.0
2.06 ms ± 39.3 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
Scikit-Image

ソースコード

import skimage.io
%timeit skimage.io.imread("./lenna.jpeg")
2.1 ms ± 40.6 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
PIL

ソースコード

from PIL import Image
def load_img(path):
    with open(path, 'rb') as f:
        with Image.open(f) as img:
            return img.convert('RGB')

%timeit load_img("./lenna.jpeg")
2.02 ms ± 61.5 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

画像のリサイズ

続いて画像のリサイズです。
ニューラルネットワークの入力は固定長になる場合が多いので、基本的にはどこかでリサイズ処理が必要になり、
ImageNetを利用する場合は必須に近い技術となるでしょう。
そのため、この部分の速度を改善できるとDataAugmentationに非常に有用な手法となります。

f:id:tereka:20171221233331j:plain

OpenCV

ソースコード

%timeit cv2.resize(img,(128,128))
271 µs ± 4.26 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
Scikit-image

ソースコード

%timeit skimage.transform.resize(img, (128,128))
4.32 ms ± 142 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
PIL

ソースコード

%timeit p_img.resize((128,128))
23.2 µs ± 850 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

アフィン変換を行う

アフィン変換を実施します。
回転、平行移動、スケール変化、引き伸ばしをこのアフィン変換で実現できます。
DataAugmentationにおいても非常にメジャーな技術と言えるでしょう。
時間の都合もあり、条件が若干ばらばらですが、アフィン変換の計測をしました。

全て微妙に違う画像ですが、こんな感じで変換を行っています。

f:id:tereka:20171222005403j:plain

OpenCV
import cv2
def transform_opencv(img):
    size = int(img.shape[0] / 2)
    affine = cv2.getRotationMatrix2D((size, size), 45,  0.8)
    affine[0][2] += 10
    affine[1][2] += 15
    img_afn = cv2.warpAffine(img, affine, img.shape[0:2],flags=cv2.INTER_LINEAR)

%timeit transform_opencv(img)
771 µs ± 139 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
Scikit-Image
def tranform_skimage(img):
    rotation = 45
    rotation = np.deg2rad(rotation)
    tform = AffineTransform(scale=(1.3, 1.3), rotation=rotation,
                            shear=1.0,
                            translation=(3, 3))
    warped_img = warp(img, tform)

%timeit tranform_skimage(img)
4.69 ms ± 72.9 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
PIL
from PIL import Image

def load_img(path):
    with open(path, 'rb') as f:
        with Image.open(f) as img:
            return img.convert('RGB')

def ScaleRotateTranslate(image, angle, center = None, new_center = None, scale = None,expand=False):
    if center is None:
        return image.rotate(angle)
    angle = -angle/180.0*math.pi
    nx,ny = x,y = center
    sx=sy=1.0
    if new_center:
        (nx,ny) = new_center
    if scale:
        (sx,sy) = scale
    cosine = math.cos(angle)
    sine = math.sin(angle)
    a = cosine/sx
    b = sine/sx
    c = x-nx*a-ny*b
    d = -sine/sy
    e = cosine/sy
    f = y-nx*d-ny*e
    return image.transform(image.size, Image.AFFINE, (a,b,c,d,e,f), resample=Image.BICUBIC)

def scale():
    image =load_img("./lenna.jpeg")
    transformed = ScaleRotateTranslate(image, 15, None, (0, 0), scale=0.9)

%timeit scale()
3.03 ms ± 83 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

左右反転

左右反転を行います。処理後の画像は次の通り

f:id:tereka:20171221232633j:plain

OpenCV
%timeit cv2.flip(img,0)
13 µs ± 162 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
Numpyの方式
%timeit img[:, ::-1]
404 ns ± 12.3 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
PIL
%timeit img.transpose(p_img.FLIP_LEFT_RIGHT)
56.5 µs ± 4.97 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

まとめ

OpenCV強いと書こうと思ったらPILの方が速い場面があった。
OpenCVの方が使いやすいのですが・・・

ライブラリ 画像の読み込み リサイズ アフィン変換 フリップ
OpenCV 2.06ms 271 µs 771 µs 13 µs
Scikit-image 2.1 ms 4.32 ms 4.69 ms なし(numpyで実施)
PIL 2.02ms 23.2 µs 3.03 ms 56.5 µs
Numpy(一部) なし なし なし 404 ns

最後に

OpenCVが想像以上にScikit-imageより速いがPILの方が速い。そのため、もしこの処理で困っているならば
OpenCV、もしくは、PILを利用すると高速化できそうです。

もちろん、状況次第によって変化するかもしれませんが、
この結果を参考に良い実装をしてみてください。

日本語自然言語処理のData Augmentation

皆さんこんにちは
お元気ですか。週末の発表資料も作っています。

本記事は自然言語処理アドベントカレンダー第13日です。

qiita.com

画像処理ではデータをアフィン変換などで変形して、
画像を拡張する処理(=Data Augmentation)が知られています。
変換などをかけて過学習を防ぐ役割が1つとしてあります。

今回は、自然言語処理でもData Augmentationを実現したいと思います。

テキストのData Augmentationの意義

自分でデータセットを作る場合に、全てを網羅して1つ1つの単語が変わった場合を
作り上げるのは非常に困難で、時間がかかります。

そのため、一定の問題ない範囲(=結果が変わらない範囲)で
データを拡張する試みをすることで精度やロバストな学習ができる可能性があります。

WordNetの説明

Wikipediaより

WordNet(わーどねっと)は英語の概念辞書(意味辞書)である。WordNetでは英単語がsynsetと呼ばれる同義語のグループに分類され、簡単な定義や、他の同義語のグループとの関係が記述されている。 WordNetの目的は直感的に使うことのできる辞書とシソーラスが組み合わされた成果物を作ること、および自動的文書解析や人工知能のアプリケーションの実現を支援することにある。WordNetのデータベースやソフトウェアはBSDライセンスによって公開され、自由にダウンロードして用いることができる。データベースはオンラインで参照することもできる。

WordNetは日本語にもあり、国立研究開発法人情報通信研究機構NICT)が開発を進めています。
単語と単語の関係性、例えば、類義語や上位語などがこのWordNetからわかります。

類義語は病気と風邪、上位語の関係が牛乳と乳製品のような関係になります。

WordNetの環境を構築する。

まずは、WordNet(日本語)をダウンロードします。
http://nlpwww.nict.go.jp/wn-ja/

ダウンロードしたファイルを展開すると「wnjpn.db」が展開されるので
このファイルを使ってWordNetの中身を確認します。

WordNetを使ってデータを拡大する。

さて、ここからが本題です。WordNetを使ってデータセットを人工的に生成します。

文書拡張アルゴリズムの試み

まずはサンプルを用意します。元々のボットを拡張用に少し修正を加えている文章です。

そんな、とてもきつい風邪だから仕方ないよ。
名詞の類義語置換

まずは形態素解析で区切り、解析することを目指します。
そこで得られた単語をベースに他の単語で置換します。

そんな、とてもきつい風邪だから仕方ないよ。
そんな	連体詞,*,*,*,*,*,そんな,ソンナ,ソンナ
、	記号,読点,*,*,*,*,、,、,、
とても	副詞,助詞類接続,*,*,*,*,とても,トテモ,トテモ
きつい	形容詞,自立,*,*,形容詞・アウオ段,基本形,きつい,キツイ,キツイ
風邪	名詞,一般,*,*,*,*,風邪,カゼ,カゼ
だ	助動詞,*,*,*,特殊・ダ,基本形,だ,ダ,ダ
から	助詞,接続助詞,*,*,*,*,から,カラ,カラ
仕方	名詞,ナイ形容詞語幹,*,*,*,*,仕方,シカタ,シカタ
ない	助動詞,*,*,*,特殊・ナイ,基本形,ない,ナイ,ナイ
よ	助詞,終助詞,*,*,*,*,よ,ヨ,ヨ
。	記号,句点,*,*,*,*,。,。,。
EOS
類義語

上記データを類義語を使って置換します。
ただ、単純に全ての類義語を検索することに意味はなさそうなので絞ります。
名詞は非常に有効そうなので、名詞に絞込み別の単語に置き換えます。

WordNetから類義語を取得します。類義語は以下のコードを用いれば取得可能です。
wordテーブルからwordのidを取得します。
そして、次にsenseからwordが所属するsynset(類義語のネットワーク)を検索します。
最後に、senseから同じネットワークに存在する単語を取得し、類義語一覧を獲得しています。

def get_synonym_word(word):
    cur = conn.execute("select * from word where lemma=?", (word,))
    word_list = [row for row in cur]
    synonym_list = []
    for word in word_list:
        cur = conn.execute("select * from sense where wordid=?", (word[0],))
        synnet_list = [row for row in cur]
        for synnet in synnet_list:
            cur = conn.execute("select * from sense, word where synset = ? and word.lang = 'jpn' and sense.wordid = word.wordid;", (synnet[0],))
            synonym_list += [row for row in cur]
    return synonym_list

最初に取り上げた文章には「病気」の名詞があります。
その類義語は次のようになります。

感冒
風邪ひき
風邪
咳気
上位語への置換

WordNetは上位の言葉への言葉も登録されているため、その言葉を使う置換方式も可能そうに見えます。
上位語は次の方式で検索できます。因みに病気だと、'病み煩い', '疾病', '杜撰'が検出されます。難しい。
こちらの内容を参考にし、実装しています。

A frontend of WordNet-Ja database file (sqlite3 format) which is available on http://nlpwww.nict.go.jp/wn-ja/ · GitHub

from collections import namedtuple

Word = namedtuple('Word', 'wordid lang lemma pron pos')

def getWords(lemma):
  words = []
  cur = conn.execute("select * from word where lemma=?", (lemma,))
  row = cur.fetchone()
  while row:
    words.append(Word(*row))
    row = cur.fetchone()
  return words

def getWord(wordid):
  cur = conn.execute("select * from word where wordid=?", (wordid,))
  return Word(*cur.fetchone())
 
Sense = namedtuple('Sense', 'synset wordid lang rank lexid freq src')

def getSenses(word):
  senses = []
  cur = conn.execute("select * from sense where wordid=?", (word.wordid,))
  row = cur.fetchone()
  while row:
    senses.append(Sense(*row))
    row = cur.fetchone()
  return senses

def getSense(synset, lang='jpn'):
  cur = conn.execute("select * from sense where synset=? and lang=?",
      (synset,lang))
  row = cur.fetchone()
  if row:
    return Sense(*row)
  else:
    return None

SynLink = namedtuple('SynLink', 'synset1 synset2 link src')

def getSynLinks(sense, link):
  synLinks = []
  cur = conn.execute("select * from synlink where synset1=? and link=?", (sense.synset, link))
  row = cur.fetchone()
  while row:
    synLinks.append(SynLink(*row))
    row = cur.fetchone()
  return synLinks

def abstract_word(lemma):
    result = []
    for word in getWords(lemma):
        for sense in getSenses(word):
            if sense.src != 'hand': continue
            for synlink in getSynLinks(sense, 'hype'):
                abst_sense = getSense(synlink.synset2)
                if abst_sense and word.wordid != abst_sense.wordid:
                    result.append(getWord(abst_sense.wordid).lemma)
    return result
副詞・形容詞の削除

副詞と形容詞を削除します。
副詞と形容詞は強調する表現になるため削除してもあまり意味はなさそうです。
文章内だと、「とても」と「きつい」になります。

http://www.anlp.jp/proceedings/annual_meeting/2017/pdf_dir/P10-4.pdf

※自動的に付け加える方法は要検討ですね。

実際にどの程度データが増えるのかやってみた。

さて、実際に作ります。コードとしての手順は次の通りです。

  1. 形態素解析を行う。
  2. 品詞を抽出し適切な処理を行う。(これまでの処理)
  3. 全てのパタンを列挙する。

これらを使ったDataAugmentationのコードは次の通りです。

def data_augmentation_for_text(sentence):
    sentence_list_origin = []
    nodes = word_and_class(sentence)
    get_word(nodes,sentence_list=sentence_list_origin)
    return sentence_list_origin

def word_and_class(doc):
    doc_ex = doc
    doc_ex = doc
    print (doc_ex)

    # Execute class analysis
    
    result = tagger.parseToNode(doc_ex)

    # Extract word and class
    word_class = []
    while result:
        word = result.surface
        clazz = result.feature.split(',')[0]
        if clazz != u'BOS/EOS':
            word_class.append((word, clazz))
        result = result.next

    return word_class

def get_word(nodes, index=0,sentence="", sentence_list=[]):
    if len(nodes) == index:
        sentence_list.append(sentence)
        return None
    
    next_index = index + 1
    if nodes[index][1] == "副詞" or nodes[index][1] == "形容詞":
        get_word(nodes, index=next_index, sentence=sentence, sentence_list=sentence_list)
        get_word(nodes, index=next_index, sentence=sentence + nodes[index][0], sentence_list=sentence_list)
    
    elif nodes[index][1] == "名詞":
        candidate_words = get_synonym(nodes[index][0] )
        candidate_words += abstract_word(nodes[index][0] )
        get_word(nodes, next_index, sentence + nodes[index][0], sentence_list=sentence_list)
        for candidate_word in candidate_words:
            get_word(nodes, next_index, sentence + candidate_word, sentence_list=sentence_list)
    else:
        get_word(nodes, next_index, sentence + nodes[index][0], sentence_list=sentence_list)

結果

そんな、とてもきつい風邪だから仕方ないよ。
そんな、風邪だから仕方ないよ。
そんな、風邪だから流儀ないよ。
そんな、風邪だから遣り口ないよ。
そんな、風邪だから手ないよ。
そんな、風邪だから仕様ないよ。
そんな、風邪だから筆法ないよ。
そんな、風邪だからスタイルないよ。
そんな、風邪だから仕樣ないよ。
そんな、風邪だから様式ないよ。
そんな、風邪だからやり口ないよ。

なんか意味変わっていないか!?といった部分もありますね。

あなたのことが好き
あなたのことが好き
あなたのことが好み
あなたのことがお好
あなたのことが御好み
あなたのことがお好み
あなたのことが御好
あなたのことが好き
あなたのことが温かさ
あなたのことが慈しみ

少し近い気がします。何か日本語的にピンとこない内容もありますが、
若干誤字っぽければこうなるのでしょうか。

考察

意味が変わっている単語があり、もう少し検討の余地はありそうです。
もう少し単語の形を捉えることが必要でしょうか。

WebDNNのサンプルとコードの解説に挑戦

皆さんこんにちは。
お元気ですか。師走の12月らしく締め切りに追われています。

この記事は「Deep Learning フレームワークざっくり紹介 Advent Calendar 2017」第7日目です。

qiita.com

DeepLearningフレームワークの1つ、WebDNNのご紹介をします。
このフレームワークを調べたきっかけとして、「MakeGirls.moe」があります。
MakeGirls.moeは萌画像生成のWebサービスで、WebDNNを使用して提供しているようです。

make.girls.moe

WebDNN

WebDNNは東京大学 原田・牛久研究室が作成した深層学習モデル(DNN)を
ウェブブラウザ上で高速実行するためのオープンソースフレームワークです。

次のサイトのアワードに掲載されるほど、良いソフトウェアでもあります。

Open source software Award 2017 for WebDNN at ACM Multimedia 2017

DNNは顕著な成果を様々なタスクであげていますが、度々、計算コストが問題になります。
サーバ上でDNNの全ての計算を行う場合、様々なユーザが送信するリクエストは多数あります。
これを処理しきるにはそれなりのスペックのマシン及び台数が求められます。(多分、個人では無理)

WebDNNはウェブブラウザ上での実行を前提とし、様々な計算の最適化がされているようです。

github.com

WebDNNでWebサイトを構築するまでの手順

WebDNNはいくつかの有名なフレームワークに対応しています。
公式で確認ができているのはChainer, Kerasでしょうか。

KerasからWebDNN用のオブジェクトに変換してそれを利用し、Webサイトで動かすまでやってみます。

KerasからWebDNNへ変換する

まずは、Pretrainingで有名なResNet50を取得します。
KerasからResNet50を取得し、まずはディスクに保存します。
下記の実行は「example/resnet/」配下で行ってください。
(短いのでインタプリタでの実行で良いと思います)

from keras.applications import resnet50
model = resnet50.ResNet50(include_top=True, weights='imagenet')
model.save("resnet50.h5")

次にWebDNNで実行するモデル形式へ変換します。実行コマンドは次の通りです。

python ../../bin/convert_keras.py resnet50.h5 --input_shape '(1,224,224,3)' --out output_keras

Kerasを使い、Web上で実行する。

さて、ここからモデルを画面上で使います。
サンプルのサーバを立ち上げてみます。
サーバを起動します。setup.pyと同じ箇所で次のコマンドを実行します。

python -m http.server

実行後に次のURLにアクセスします。
(README.mdと異なることに注意、パスはexample配下をチェックして適切なパスを使いましょう)

http://localhost:8000/example/resnet/

アクセス後に次の画面が出てきます。
この画面でRunボタンを押すことで、WebDNNで変換されたモデルの実行が可能です。

f:id:tereka:20171203002625p:plain

さて、ここから先はWebDNNのjavascriptを見てみます。

ソースコード

Python(bin/convert_keras.py)

Kerasによる変換の解説になります。
まずは、KerasConverterを使います。このKerasConverterにより、
入力したモデルをWebDNN IR Graphへと変換します。

そして、この変換したWebDNN IR Graphとバックエンド(webgpu,webgl,webassembly,fallback)
とencodingを入力して与えると変換されたデータの出力を行います。
WebDNNのREADME曰く「--encoding eightbit」と引数に与えるとモデルが1/5サイズになるとのこと。

    backends = args.backend.split(",")
    for i, backend in enumerate(backends):
        console.stderr(f"[{path.basename(__file__)}] BackendName: {console.colorize(backend, console.Color.Cyan)}")
        try:
            graph_exec_data = generate_descriptor(backend, graph, constant_encoder_name=args.encoding)
            graph_exec_data.save(output_dir)
        except Exception as ex:
            if flags.DEBUG:
                raise ex

Javascript

ヘッダー

まずは、index.htmlを確認します。
ヘッダーとしてインポートされているスクリプトで必要なのは、
「../../dist/webdnn.js」と「script.js」です。
独自で開発する場合には「../../dist/webdnn.js」をインポートする必要があります。

<head>
    <title>ResNet-50 conversion WebDNN example</title>
    <meta charset="utf-8">
    <script src="../../lib/inflate.min.js"></script>
    <script src="../../dist/webdnn.js"></script>
    <script src="../data/imagenet_labels.js"></script>
    <script src="script.js"></script>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css">
</head>
scrpit.js

まず、実行時には次のコードが実行されます。
ここではモデルと画像を読み込み計算結果を返します。

prepare_runでモデルをファイルから読み込むような挙動をしています。
次にrunner#getInputViewsで計算する画像をセットします。

最後にrunner.run();で予測をする計算を行い、getOutputViewsで計算結果を取り出しています。
Chainerとその他でOrderを変化させているのは、
フレームワークによってCHW(Channel, Height,Width)とHWCの順番が異なることが理由です。

async function run() {
    let runner = await prepare_run();

    runner.getInputViews()[0].set(await WebDNN.Image.getImageArray(document.getElementById('input_image'), {
        order: getFrameworkName() === 'chainer' ? WebDNN.Image.Order.CHW : WebDNN.Image.Order.HWC,
        color: WebDNN.Image.Color.BGR,
        bias: [123.68, 116.779, 103.939]
    }));

    let start = performance.now();
    await runner.run();
    let elapsed_time = performance.now() - start;

    let out_vec = runner.getOutputViews()[0].toActual();
    let top_labels = WebDNN.Math.argmax(out_vec, 5);

    let predicted_str = 'Predicted:';
    for (let j = 0; j < top_labels.length; j++) {
        predicted_str += ` ${top_labels[j]}(${imagenet_labels[top_labels[j]]})`;
    }
    log(predicted_str);

    console.log('output vector: ', out_vec);
    log(`Total Elapsed Time[ms/image]: ${elapsed_time.toFixed(2)}`);
}

prepare_runでは、モデルをロードします。
backendとframeworkの名前で識別できるので、その名前を指定して読み込みます。
下記のコードでは非同期にモデルを読み込み、
既にモデルが存在する場合にはその読み込まれているモデルを返します。

async function prepare_run() {
    let backend_name = document.querySelector('input[name=backend]:checked').value;
    let framework_name = getFrameworkName();
    let backend_key = backend_name + framework_name;
    if (!(backend_key in runners)) {
        log('Initializing and loading model');
        let runner = await WebDNN.load(`./output_${framework_name}`, {backendOrder: backend_name});
        log(`Loaded backend: ${runner.backendName}, model converted from ${framework_name}`);

        runners[backend_key] = runner;
    } else {
        log('Model is already loaded');
    }
    return runners[backend_key];
}

最後にloadImageを紹介します。getImageArrayを使うことで、画像をurl(path)から読み込み、
画像データとして保持します。これによりCanvasに画像を埋め込みます。

async function loadImage() {
    let imageData = await WebDNN.Image.getImageArray(document.getElementById("image_url").value, {dstW: 224, dstH: 224});
    WebDNN.Image.setImageArrayToCanvas(imageData, 224, 224, document.getElementById('input_image'));

    document.getElementById('run_button').disabled = false;
    log('Image loaded to canvas');
}

最後に

これを使うとWebページでjavascriptを使い、モデルの実行ができます。
サーバ上で動作させるよりもユーザに提供しやすいので非常におすすめです。
これを使えばユーザにモデルの提供がしやすくなり、遊びで作ったツールを試してもらうことができます。

いつか、提供しよう。そうしよう

Pythonで自然言語処理のタスクをやってみる。

皆さんこんにちは
お元気ですか。アドベントカレンダー真っ盛りですね。
本日は「python Advent Calendar 2017」のアドベントカレンダー第5日です。

qiita.com

自然言語処理には様々なライブラリ(NLTKやCoreNLP)があります。
せっかくの機会として、本記事では紹介が少ないspaCyを紹介します。

spaCy

spaCyとは

spaCyはPythonの発展的な自然言語処理のライブラリです。
実際に使われていることを想定しており、英語、ドイツ語、フランス語、スペイン語に対応しています。
トークナイザーは日本語もあるとのこと(確かJanomeで動作します)。

github.com

次のリンク先には他の自然言語処理ライブラリの
アルゴリズムの観点や精度(Dependency parsing、Named entity comparison)が
載っており、他のライブラリとどのような点が違うのかを確認できます。

spacy.io

インストール

pip install spacy

spaCyで遊んでみる

spaCyですが、日本語対応が用意されています。
ただ、残念ながら日本語に対応したモデルがないため、
まだ本格的な日本語解析は難しいのかなと感じています。

それは置いておいて、本日はspaCyの機能紹介をしたいので、
既に準備されている英語のモデルを使います。

まずは、英語解析の準備をします。

import spacy
nlp = spacy.load('en')
doc1 = nlp(u"this is a pen.")

文章を単語ごとに区切る

文章を単語ごとに区切ることが可能です。

for token in doc1:
    print (token)

結果はスペースで区切られている次の通りです。

this
is
a
pen
.

文章を文ごとに分ける

簡単そうな例ですが、文章を文ごとに分解します。
英語だと、ピリオドによる分割になるでしょうか。

doc2 = nlp(u"This is a pen. That is an apple")
for sent in doc2.sents:
    print (sent)

出力結果です。「.」による分割ができています。

This is a pen.
That is an apple

固有表現抽出(Named Entity Recognizer)

固有表現抽出は文章の中から固有の表現を発見する技術です。

ent_doc = nlp("Rami Eid is studying at Stony Brook University in New York")
for ent in ent_doc.ents:
    print (ent, ent.label, ent.label_)

残念ながらNew Yorkが出力できていません。
固有表現が何かだけではなく、どんな固有表現(人なのか、組織なのか)かも抽出できています。

Rami Eid 377 PERSON
Stony Brook University 380 ORG

名詞フレーズの抽出

フレーズの抽出も可能です。

noun_chunk_test = nlp(u"Natural language processing (NLP) deals with the application of computational models to text or speech data.")
for noun_chunk in noun_chunk_test.noun_chunks:
    print (noun_chunk)

出力結果です。これもそれっぽいですね。

Natural language processing (NLP) deals
the application
computational models
text or speech data

文章間の距離を計算する。

文書のベクトルを計算します。
word2vecに類似したアルゴリズムです。実装は非常にシンプルで
apple.vectorをすれば、文書のベクトルを取得できます。
ベクトルを表示すると標準出力が長くなるので、代わりにshapeを実行してみます。

apple.vector.shape

また、2ベクトル間を計算し、類似度を算出できます。

udon = nlp(u"Udon and oranges are not similar.")
apple = nlp(u"Apples and oranges are similar. Boots and hippos aren't.")
udon.similarity(apple)

最後に

今回は自然言語処理のライブラリを紹介しました。
まだ、日本人向けには難しい状態でありますが、開発が進んでいるので、試しに使ってみてはいかがでしょうか。

TensorFlowにdefine by run(TensorFlow Eager)がやってきた

皆さんこんにちは
お元気ですか。今回の三連休は二郎食べて満足しました。

Chainerにはじまり、PyTorchなどdefine by runで
ニューラルネットワークを計算するフレームワークがあります。
このdefine by runは非常にRNN系のニューラルネットワークを書く際に重宝しています。

そして、10月末にTensorFlowからもdefine by run用の
インターフェースが試験的に提供されました。それがTensorFlow Eagerです。

※define by runが不明な方はこちらへ
s0sem0y.hatenablog.com

TensorFlow Eager

TensorFlow Eagerは次の公式の記事で紹介されています。
ただし、この機能はPreview段階です。
通常使う場面では問題ないと思いますが、念のため何か起こっても問題ない環境で利用してください。

research.googleblog.com

一言で言うなれば、TensorFlowのdefine by run用のインターフェースです。
次の特徴があるそうです。

  1. 高速なデバッグ
  2. Pythonを利用した動的なモデル
  3. TensorFlowの処理が大体実行可能

インストール

pipに含まれるTensorFlowに含まれていないため、別途インストールが必要です。
1.4.0ではimportをできますが、内部の実装がありません。
そのため、お試しで使うには次のコマンドを実行してください。

pip install tf-nightly

Example集

公式のサンプル集は次に掲載されています。とりあえず、MNISTを使います。
github.com

MNIST サンプルのご紹介

公式MNISTを紹介します。簡単に実装を理解するならばMNISTがわかりやすい。

準備

tensorflow eagerでは最初に次のコードを実行する必要があります。
このコードの実行によりtensorflow eagerでの実行ができます。所謂おまじないのようなコードです。

  tfe.enable_eager_execution()

そして、モデル、最適関数、データセットの準備をします。
MNISTModelの内部のコードに動かすニューラルネットワークを定義します。
実装方式は次で紹介します。

  # Load the datasets
  (train_ds, test_ds) = load_data(FLAGS.data_dir)
  train_ds = train_ds.shuffle(60000).batch(FLAGS.batch_size)

  # Create the model and optimizer
  model = MNISTModel(data_format)
  optimizer = tf.train.MomentumOptimizer(FLAGS.lr, FLAGS.momentum)

モデル

モデル部分のコードです。ChainerのChainやPyTorchのModuleと殆ど似ています。
__init__側に各モジュールを定義します。
Convolutionの計算や全結合層の計算モジュールを定義し、インスタンス変数として定義します。
__init__側で定義するのは、更新するパラメータを持つモジュールです。

計算の仕方はcall関数に定義します。ここで変数に対してどう計算するかを定義します。

class MNISTModel(tfe.Network):
  def __init__(self, data_format):
    super(MNISTModel, self).__init__(name='')
    if data_format == 'channels_first':
      self._input_shape = [-1, 1, 28, 28]
    else:
      assert data_format == 'channels_last'
      self._input_shape = [-1, 28, 28, 1]
    self.conv1 = self.track_layer(
        tf.layers.Conv2D(32, 5, data_format=data_format, activation=tf.nn.relu))
    self.conv2 = self.track_layer(
        tf.layers.Conv2D(64, 5, data_format=data_format, activation=tf.nn.relu))
    self.fc1 = self.track_layer(tf.layers.Dense(1024, activation=tf.nn.relu))
    self.fc2 = self.track_layer(tf.layers.Dense(10))
    self.dropout = self.track_layer(tf.layers.Dropout(0.5))
    self.max_pool2d = self.track_layer(
        tf.layers.MaxPooling2D(
            (2, 2), (2, 2), padding='SAME', data_format=data_format))

  def call(self, inputs, training):
    x = tf.reshape(inputs, self._input_shape)
    x = self.conv1(x)
    x = self.max_pool2d(x)
    x = self.conv2(x)
    x = self.max_pool2d(x)
    x = tf.layers.flatten(x)
    x = self.fc1(x)
    if training:
      x = self.dropout(x)
    x = self.fc2(x)
    return x

メインループ

サンプルのメインとなる処理です。
次のコードでは、1poch分の学習を行い、1epoch分学習が完了したモデルを保存しています。

  with tf.device(device):
    for epoch in range(1, 11):
      with tfe.restore_variables_on_create(
          tf.train.latest_checkpoint(FLAGS.checkpoint_dir)):
        global_step = tf.train.get_or_create_global_step()
        start = time.time()
        with summary_writer.as_default():
          train_one_epoch(model, optimizer, train_ds, FLAGS.log_interval)
        end = time.time()
        print('\nTrain time for epoch #%d (global step %d): %f' % (
            epoch, global_step.numpy(), end - start))
      with test_summary_writer.as_default():
        test(model, test_ds)
      all_variables = (
          model.variables
          + optimizer.variables()
          + [global_step])
      tfe.Saver(all_variables).save(
          checkpoint_prefix, global_step=global_step)

学習

次のコードは学習するコードです。
モデル 部分で定義したmodelを使います。全体の流れは次の通りです。

  1. バッチごとのデータセットiteratorで取得する。(tfe.iterator(dataset))
  2. 誤差を計算する(model loss)
  3. 最適化関数で最小化する(optimizer.minimize)

定義したモデルはmodel()で実行できます。

def train_one_epoch(model, optimizer, dataset, log_interval=None):
  """Trains model on `dataset` using `optimizer`."""

  tf.train.get_or_create_global_step()

  def model_loss(labels, images):
    prediction = model(images, training=True)
    loss_value = loss(prediction, labels)
    tf.contrib.summary.scalar('loss', loss_value)
    tf.contrib.summary.scalar('accuracy',
                              compute_accuracy(prediction, labels))
    return loss_value

  for (batch, (images, labels)) in enumerate(tfe.Iterator(dataset)):
    with tf.contrib.summary.record_summaries_every_n_global_steps(10):
      batch_model_loss = functools.partial(model_loss, labels, images)
      optimizer.minimize(
          batch_model_loss, global_step=tf.train.get_global_step())
      if log_interval and batch % log_interval == 0:
        print('Batch #%d\tLoss: %.6f' % (batch, batch_model_loss()))

判定

判定部は次のとおりです。モデルを使って出力し、誤差を計算しているなど
学習時に見られるコードのため、目立って新しい箇所はないかと思います。

def test(model, dataset):
  """Perform an evaluation of `model` on the examples from `dataset`."""
  avg_loss = tfe.metrics.Mean('loss')
  accuracy = tfe.metrics.Accuracy('accuracy')

  for (images, labels) in tfe.Iterator(dataset):
    predictions = model(images, training=False)
    avg_loss(loss(predictions, labels))
    accuracy(tf.argmax(predictions, axis=1, output_type=tf.int64),
             tf.argmax(labels, axis=1, output_type=tf.int64))
  print('Test set: Average loss: %.4f, Accuracy: %4f%%\n' %
        (avg_loss.result(), 100 * accuracy.result()))
  with tf.contrib.summary.always_record_summaries():
    tf.contrib.summary.scalar('loss', avg_loss.result())
    tf.contrib.summary.scalar('accuracy', accuracy.result())

標準出力

MNISTのサンプルを動作させると次の出力になります。

$ python mnist.py
/Users/Tereka/anaconda3/lib/python3.6/importlib/_bootstrap.py:205: RuntimeWarning: compiletime version 3.5 of module 'tensorflow.python.framework.fast_tensor_util' does not match runtime version 3.6
  return f(*args, **kwds)
2017-11-05 23:14:38.631714: I tensorflow/core/platform/cpu_feature_guard.cc:137] Your CPU supports instructions that this TensorFlow binary was not compiled to use: SSE4.2 AVX AVX2 FMA
Using device /cpu:0, and data format channels_last.
Extracting /tmp/tensorflow/mnist/input_data/train-images-idx3-ubyte.gz
Extracting /tmp/tensorflow/mnist/input_data/train-labels-idx1-ubyte.gz
Extracting /tmp/tensorflow/mnist/input_data/t10k-images-idx3-ubyte.gz
Extracting /tmp/tensorflow/mnist/input_data/t10k-labels-idx1-ubyte.gz
Batch #0	Loss: 2.295565
Batch #10	Loss: 2.279653
Batch #20	Loss: 2.263355
Batch #30	Loss: 2.227737
Batch #40	Loss: 2.176997
Batch #50	Loss: 2.159155
Batch #60	Loss: 1.995388
Batch #70	Loss: 1.832986
Batch #80	Loss: 1.613709
Batch #90	Loss: 1.197411

仮にTypeErrorが発生した場合は、該当する引数を消してください。私の場合、次の箇所で発生しました。

TypeError: create_summary_file_writer() got an unexpected keyword argument 'flush_secs'

最後に

MNISTの公式サンプルをご紹介しました。
RNNを書く時に非常に使いやすくなりそうです。また、フレームワーク戦争が熾烈になりそう。