懐かしい曲

90年代にミュージック・スクエアで聞いていた曲達

www.youtube.com

www.youtube.com

www.youtube.com

www.youtube.com

www.youtube.com

www.youtube.com

www.youtube.com

www.youtube.com

www.youtube.com

www.youtube.com

www.youtube.com

www.youtube.com

www.youtube.com

www.youtube.com

www.youtube.com

www.youtube.com

www.youtube.com

www.youtube.com

www.youtube.com

www.youtube.com

www.youtube.com

www.nicovideo.jp

www.youtube.com

www.nicovideo.jp

PyTorchの基本的なところ

テンソルの添字

>>> t = torch.zeros(2, 3, 4)

>>> t
tensor([[[0., 0., 0., 0.],
         [0., 0., 0., 0.],
         [0., 0., 0., 0.]],

        [[0., 0., 0., 0.],
         [0., 0., 0., 0.],
         [0., 0., 0., 0.]]])

>>> t.shape
torch.Size([2, 3, 4])

>>> t[0][1][2] = 1
>>> t
tensor([[[0., 0., 0., 0.],
         [0., 0., 1., 0.],
         [0., 0., 0., 0.]],

        [[0., 0., 0., 0.],
         [0., 0., 0., 0.],
         [0., 0., 0., 0.]]])

t.shape が [2, 3, 4] であるということは、4つのまとまりが3つあるまとまりが2つある、みたいな意味

view による変形

>>> t = torch.Tensor([[[1,2,3], [4, 5, 6]], [[7, 8, 9], [10, 11, 12]]])
>>> t
tensor([[[ 1.,  2.,  3.],
         [ 4.,  5.,  6.]],

        [[ 7.,  8.,  9.],
         [10., 11., 12.]]])

>>> t.shape
torch.Size([2, 2, 3])

>>> t2 = t.view(6, 2) # 2個のまとまりを6個に変換
>>> t2
tensor([[ 1.,  2.],
        [ 3.,  4.],
        [ 5.,  6.],
        [ 7.,  8.],
        [ 9., 10.],
        [11., 12.]])
>>> t2.shape
torch.Size([6, 2])

>>> t3 = t.view(4, -1) # X個のまとまりを4個に変換
>>> t3
tensor([[ 1.,  2.,  3.],
        [ 4.,  5.,  6.],
        [ 7.,  8.,  9.],
        [10., 11., 12.]])
>>> t3.shape
torch.Size([4, 3])

>>> t.view(9, -1)
RuntimeError: shape '[9, -1]' is invalid for input of size 12

テンソルの形を要素数が同じという条件下で変形する -1を使うと他に指定された数値からかってに埋めてくれる

squeeze

素数が1の軸を削除してくれる

>>> t = torch.zeros(2, 1, 3, 1)
>>> t
tensor([[[[0.],
          [0.],
          [0.]]],

        [[[0.],
          [0.],
          [0.]]]])

>>> t.squeeze() # [2, 1, 3, 1] を [2, 3] に整形してくれる
tensor([[0., 0., 0.],
        [0., 0., 0.]])

>>> # 引数(dim)を指定すると dim番めの軸のみに着目
>>> t.squeeze(0) # 0番目は2なのでsqueezeされない
tensor([[[[0.],
          [0.],
          [0.]]],


        [[[0.],
          [0.],
          [0.]]]]) 

>>> t.squeeze(1) # 1番目は1なので [2, 3, 1] に整形
tensor([[[0.],
         [0.],
         [0.]],

        [[0.],
         [0.],
         [0.]]])

>>> t.squeeze(3) # 3番目は1なので[2, 1, 3]に整形
tensor([[[0., 0., 0.]],

        [[0., 0., 0.]]])

unsqueeze

squeezeの逆バージョン 要素数が1の軸を足してくれる dimが必須

>>> t = torch.zeros(2, 3)
>>> t
tensor([[0., 0., 0.],
        [0., 0., 0.]])
>>> t.unsqueeze(1)
tensor([[[0., 0., 0.]],

        [[0., 0., 0.]]])

>>> t.unsqueeze(2)
tensor([[[0.],
         [0.],
         [0.]],

        [[0.],
         [0.],
         [0.]]])

permute

>>> t = torch.Tensor([[[1,2,3], [4, 5, 6]], [[7, 8, 9], [10, 11, 12]]])
>>> t
tensor([[[ 1.,  2.,  3.],
         [ 4.,  5.,  6.]],

        [[ 7.,  8.,  9.],
         [10., 11., 12.]]])

>>> t2 = t.permute(2, 0, 1)
>>> t2
tensor([[[ 1.,  4.],
         [ 7., 10.]],

        [[ 2.,  5.],
         [ 8., 11.]],

        [[ 3.,  6.],
         [ 9., 12.]]])

>>> t3 = t.permute(2, 1, 0)
>>> t3
tensor([[[ 1.,  7.],
         [ 4., 10.]],

        [[ 2.,  8.],
         [ 5., 11.]],

        [[ 3.,  9.],
         [ 6., 12.]]])

>>> t[1][0][2]
tensor(9.)

>>> t2[2][1][0]
tensor(9.)

>>> t3[2][0][1]
tensor(9.)

spacy と Multi30k

from torchtext.datasets import TranslationDataset, Multi30k
from torchtext.data import Field, BucketIterator
import spacy

!python -m spacy download en

spacy_en = spacy.load('en')

!python -m spacy download de

spacy_de = spacy.load('de')

l = spacy_en.tokenizer('This is a pen!')
type(l) # => spacy.tokens.doc.Doc
type(l[0]) # => spacy.tokens.token.Token
l[0].text # => 'This'

def tokenize_de(text):
    """
    Tokenizes German text from a string into a list of strings (tokens) and reverses it
    """
    return [tok.text for tok in spacy_de.tokenizer(text)]

def tokenize_en(text):
    """
    Tokenizes English text from a string into a list of strings (tokens)
    """
    return [tok.text for tok in spacy_en.tokenizer(text)]

SRC = Field(tokenize = tokenize_de, 
            init_token = '<sos>', 
            eos_token = '<eos>', 
            lower = True)

TRG = Field(tokenize = tokenize_en, 
            init_token = '<sos>', 
            eos_token = '<eos>', 
            lower = True)


train_data, valid_data, test_data = Multi30k.splits(exts = ('.de', '.en'), 
                                                    fields = (SRC, TRG))

type(train_data) # => torchtext.datasets.translation.Multi30k

print(vars(train_data.examples[0]))
"""
=> {
  'src': [
    'zwei',
    'junge',
    'weiße',
    'männer',
    'sind',
    'im',
    'freien',
    'in',
    'der',
    'nähe',
    'vieler',
    'büsche',
    '.'
  ],
  'trg': [
    'two',
    'young',
    ',',
    'white',
    'males',
    'are',
    'outside',
    'near',
    'many',
    'bushes',
    '.'
  ]
}
"""

SRC.build_vocab(train_data, min_freq = 2)
TRG.build_vocab(train_data, min_freq = 2)


BATCH_SIZE = 128

train_iterator, valid_iterator, test_iterator = BucketIterator.splits(
    (train_data, valid_data, test_data), 
    batch_size = BATCH_SIZE, 
    device = device)

for b in train_iterator:
  break

type(b) # => torchtext.data.batch.Batch
type(b.src) # => torch.Tensor
type(b.trg) # => torch.Tensor

b.src.shape # => [27, 128]
b.trg.shape # => [27, 128]
b.trg
"""
tensor([[   2,    2,    2,  ...,    2,    2,    2],
        [   4,   16,    4,  ...,    4,    4,    4],
        [   9,   50, 4224,  ...,   14,    9,   61],
        ...,
        [   1,    1,    1,  ...,    1,    1,    1],
        [   1,    1,    1,  ...,    1,    1,    1],
        [   1,    1,    1,  ...,    1,    1,    1]], device='cuda:0')
"""
# b.trg[:, n] が n文目のはず
b.trg[:, 0]
"""
tensor([  2,   4,   9,  11,  74, 589,  17,   6,  43,  12,   4,  59,  77,   5,
          3,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1],
       device='cuda:0')
"""

for t in b.trg[:, 0]:
  print(TRG.vocab.itos[t])

"""
<sos>
a
man
and
some
bicycles
are
in
front
of
a
large
building
.
<eos>
<pad>
<pad>
<pad>
<pad>
<pad>
<pad>
<pad>
<pad>
<pad>
<pad>
<pad>
<pad>
"""

Bashのプロンプトをカスタマイズする

やりたいこと

カレントディレクトリと時刻とgitのブランチとgcloudに設定されているプロジェクトを表示したいです

そもそもどこで設定されている?

環境変数のPS1に設定されています

echo $PS1
${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$

f:id:uhiaha888:20200228215406p:plain

意味がわからない

カスタマイズする前に、現状を把握しておきたいが意味がわからないです

Bash/プロンプトのカスタマイズ - ArchWiki を参考に読んで見ましょう

debian_chroot ???
${debian_chroot:+($debian_chroot)}

早速謎の debian_chroot なるなにかが現れました

Ubuntuのデフォルト設定から学ぶbashプロンプト - Qiita にある通り chroot でルートディレクトリを変えている場合にそれを表示してくれるらしいです

普段 chroot を使うことがないのでこの部分はとりあえず無視しておきます

ユーザー名@ホスト名
\[\033[01;32m\]\u@\h

\[ と \] はエスケープに使われる記号で、この間に書かれた文字は文字数をカウントしないとかそんな感じみたいです

\033[01;32m

\033 は \e と書いてもいいみたいです \033[ と m の間に書体や文字色等の設定ができるみたいです 複数同時に指定したい場合は ; で区切るようです

コード 意味
00 装飾なし
01 太字
02 細字
03 イタリック
04 下線
30 文字色 黒
31 文字色 赤
32 文字色 緑
33 文字色 黄色
34 文字色 青
35 文字色 マゼンタ
36 文字色 シアン
37 文字色 白
40 背景色 黒
41 背景色 赤
42 背景色 緑
43 背景色 黄色
44 背景色 青
45 背景色 マゼンタ
46 背景色 シアン
47 背景色 白

なので、 [\033[01;32m]\ は ここからは 太字 かつ 文字色 緑 で表示します という意味になるようです

\u はユーザー名, @はただの文字列で \h がホスト名になります

コロン
\[\033[00m\]:

装飾をすべてなしにして : を表示します

カレントディレクト
\[\033[01;34m\]\w

\w はカレントディレクトリ(フルパス) さきほどの表によると、01は太字、34は文字色 青 です

$
\[\033[00m\]\$

再度装飾をすべてなしにしています \$ は一般ユーザーのときは $, root のときは # を表示してくれるようです

読めたね!

ここからはやりたいことを実現していきます

日時を表示

\D を使えば良いみたいです

\D{%Y/%m/%d %T}

ちょっと表示してみましょう

export PS1="[\D{%Y/%m/%d %T}]${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ "

f:id:uhiaha888:20200228215731p:plain

日付が表示できましたね!!

git ブランチ

プロンプトをカスタマイズしてgitブランチを表示する - Qiita を参考に、以下でブランチ名が取得できるようです

git branch --no-color 2> /dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/\1/'

ということでこれを.bashrcに記載していきましょう

function branch () {
    git branch --no-color 2> /dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/\1/'
}

if [ "$color_prompt" = yes ]; then
    PS1='$(branch)${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ '
else
    PS1='$(branch)${debian_chroot:+($debian_chroot)}\u@\h:\w\$ '
fi

f:id:uhiaha888:20200228221317p:plain

表示できた!!

gcloud のプロジェクト名

zsh-gcloud-prompt/gcloud.zsh at master · ocadaruma/zsh-gcloud-prompt · GitHub を参考に、以下でプロジェクト名が取得できます

gcloud config get-value project 2>/dev/null

ということでこれも.bashrcに記載します

f:id:uhiaha888:20200228221822p:plain

f:id:uhiaha888:20200228221856p:plain

YES!!

レイアウト

欲しい情報は一通り取得できましたが、プロンプトが長くなりすぎて使いにくそうです 2行に分けたり右端に置いたりして整理しましょう

改行

\n でいけるみたいです

右寄せ

ややこしいんですが

Bash/プロンプトのカスタマイズ - ArchWiki を参考にしたらできました

f:id:uhiaha888:20200228225442p:plain

まとめ

意外とできましたね

.bashrc の関連しそうな部分は最終的に下記のような形になりました

function branch () {
    git branch --no-color 2> /dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/\1/'
}

function project () {
    gcloud config get-value project 2>/dev/null
}

function right () {
    printf "%*s" $COLUMNS "$(branch) $(project) "
}

if [ "$color_prompt" = yes ]; then
    PS1='\[$(tput sc; right; tput rc)\]${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\n\D{%Y/%m/%d %T}\[\033[00m\] \$ '
else
    PS1='${debian_chroot:+($debian_chroot)}\u@\h:\w\$ '
fi
unset color_prompt force_color_prompt

Keras に入門する 文書生成 Part.1 サンプルを動かしてみる

文書生成のサンプルを動かしてみる

keras/lstm_text_generation.py at master · keras-team/keras · GitHub

Colabはこれ

https://colab.research.google.com/drive/1lbSMhWEUxenIXVPSCl8MaYcz_rJbuB6o

もとの文書は

https://s3.amazonaws.com/text-datasets/nietzsche.txt

まず1~40文字, 4~43文字, 7~46文字 のように、3字ずつずらしながら40文字ずつに切り分けていく & 41文字目、44文字目、47文字目 のように それぞれの40文字の次の1文字をnext_charsに格納していく

maxlen = 40
step = 3
sentences = []
next_chars = []
for i in range(0, len(text) - maxlen, step):
    sentences.append(text[i: i + maxlen])
    next_chars.append(text[i + maxlen])
print('nb sequences:', len(sentences))

つぎにベクトル化をしているらしい

print('Vectorization...')
x = np.zeros((len(sentences), maxlen, len(chars)), dtype=np.bool)
y = np.zeros((len(sentences), len(chars)), dtype=np.bool)
for i, sentence in enumerate(sentences):
    for t, char in enumerate(sentence):
        x[i, t, char_indices[char]] = 1
    y[i, char_indices[next_chars[i]]] = 1

各文字のindexは indices_char に格納されている

{0: '\n',
 1: ' ',
 2: '!',
 3: '"',
 4: "'",
 5: '(',
 6: ')',
 7: ',',
 8: '-',
 9: '.',
 10: '0',
 11: '1',
 12: '2',
 13: '3',
 14: '4',
 15: '5',
 16: '6',
 17: '7',
 18: '8',
 19: '9',
 20: ':',
 21: ';',
 22: '=',
 23: '?',
 24: '[',
 25: ']',
 26: '_',
 27: 'a',
 28: 'b',
 29: 'c',
 30: 'd',
 31: 'e',
 32: 'f',
 33: 'g',
 34: 'h',
 35: 'i',
 36: 'j',
 37: 'k',
 38: 'l',
 39: 'm',
 40: 'n',
 41: 'o',
 42: 'p',
 43: 'q',
 44: 'r',
 45: 's',
 46: 't',
 47: 'u',
 48: 'v',
 49: 'w',
 50: 'x',
 51: 'y',
 52: 'z',
 53: 'ä',
 54: 'æ',
 55: 'é',
 56: 'ë'}

x は (len(sentences), maxlen, len(chars)) の形になっている

x[i][j][k] は i文目 の j文字目 が id=k の文字なら True, そうじゃなければ False となっている (便宜上、"文"と書いたけど、先程40文字ずつに分けた1まとまりを文としている)

たとえば

x[0][0]

→ array([False, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False,  True, False, False,
       False, False, False, False, False, False, False, False, False,
       False, False, False])

これは最初の文 (preface\n\n\nsupposing that truth is a woma) の 1文字目が、id:42(=p)であることを表している

つぎにLSTMのモデルを構築する

# build the model: a single LSTM
print('Build model...')
model = Sequential()
model.add(LSTM(128, input_shape=(maxlen, len(chars))))
model.add(Dense(len(chars), activation='softmax'))

optimizer = RMSprop(lr=0.01)
model.compile(loss='categorical_crossentropy', optimizer=optimizer)

学習途中の経過表示用


def sample(preds, temperature=1.0):
    # helper function to sample an index from a probability array
    preds = np.asarray(preds).astype('float64')
    preds = np.log(preds) / temperature
    exp_preds = np.exp(preds)
    preds = exp_preds / np.sum(exp_preds)
    probas = np.random.multinomial(1, preds, 1)
    return np.argmax(probas)


def on_epoch_end(epoch, _):
    # Function invoked at end of each epoch. Prints generated text.
    print()
    print('----- Generating text after Epoch: %d' % epoch)

    start_index = random.randint(0, len(text) - maxlen - 1)
    for diversity in [0.2, 0.5, 1.0, 1.2]:
        print('----- diversity:', diversity)

        generated = ''
        sentence = text[start_index: start_index + maxlen]
        generated += sentence
        print('----- Generating with seed: "' + sentence + '"')
        sys.stdout.write(generated)

        for i in range(400):
            x_pred = np.zeros((1, maxlen, len(chars)))
            for t, char in enumerate(sentence):
                x_pred[0, t, char_indices[char]] = 1.

            preds = model.predict(x_pred, verbose=0)[0]
            next_index = sample(preds, diversity)
            next_char = indices_char[next_index]

            generated += next_char
            sentence = sentence[1:] + next_char

            sys.stdout.write(next_char)
            sys.stdout.flush()
        print()

学習の実行

print_callback = LambdaCallback(on_epoch_end=on_epoch_end)

model.fit(x, y,
          batch_size=128,
          epochs=60,
          callbacks=[print_callback])

学習結果は以下

多分変な文が生成されてるけど、英語が苦手だからどのくらい不自然なのか判別できない...

Epoch 60/60
200285/200285 [==============================] - 148s 741us/step - loss: 1.3291

----- Generating text after Epoch: 59
----- diversity: 0.2
----- Generating with seed: "ar as it is desired and demanded by wome"
ar as it is desired and demanded by women and and all the sense and subject of the problems of the power the sense to the most sense of the present the former the strength, the strength of the sense of the power the sense of the sense of the sense of the power of the participated to the sense and account the consciousness of the power of the power of the problemse--and the problemse--he wishes to the problemse--he who would not the prob
----- diversity: 0.5
----- Generating with seed: "ar as it is desired and demanded by wome"
ar as it is desired and demanded by women and believe to the unhered in the stands and entirely but all in his learned to extent as a standard new and affation of a power the result of the instances, and fatherly, which is the contempt in the person the power and a consciousness of the standate of a participation of the
man and entire power, which may be present is he was personal of the self-sake and struggle and our acts and instinct 
----- diversity: 1.0
----- Generating with seed: "ar as it is desired and demanded by wome"
ar as it is desired and demanded by women; the counter" in pitts. the
latter hand, in. a notainly ragulour, in the goodan; ard dogmes really who
experience by the "made of a liuitest power--from the cosgrable oh, an true.--we really, indeed and outicated of every such a book of its "me the one's act of worl! for hthe personal
grancts, to position--have will, well to cause about  self-my tereomtic grow where the lightness, with its choie
----- diversity: 1.2
----- Generating with seed: "ar as it is desired and demanded by wome"
ar as it is desired and demanded by women, he some good utiesnely it cleards who gif and purtant, numblinariation, ourselves. the secret curioe,
in emess,--above
spiritually tuld
insiestiaing make. this kanishons this kind of at personne--it
not "let peraportly aginives ever also, thanc
easists of delusion fseeses: suth on wind nothin! herguimed portagh highle
eighlesing, any
kundod
systems the svenses its power overwary acvedges? w) as

Keras に入門する 画像分類 Part.3 独自の分類器を作ってみる

前回準備した画像で学習してみる

https://colab.research.google.com/drive/1NQhm5hCOFqv1v-QZM-AuGr1bDWbz2Mgh

学習結果は

WARNING:tensorflow:Variable *= will be deprecated. Use `var.assign(var * other)` if you want assignment to the variable value or `x = x * y` if you want a new python Tensor object.
Train on 7722 samples, validate on 200 samples
Epoch 1/100
7722/7722 [==============================] - 6s 765us/step - loss: 2.2591 - acc: 0.1444 - val_loss: 2.1802 - val_acc: 0.2200
Epoch 2/100
7722/7722 [==============================] - 4s 508us/step - loss: 2.0826 - acc: 0.2370 - val_loss: 2.0145 - val_acc: 0.2700
Epoch 3/100

...

Epoch 99/100
7722/7722 [==============================] - 4s 500us/step - loss: 0.6649 - acc: 0.7762 - val_loss: 1.5615 - val_acc: 0.5650
Epoch 100/100
7722/7722 [==============================] - 4s 502us/step - loss: 0.6505 - acc: 0.7835 - val_loss: 1.3909 - val_acc: 0.5450
<keras.callbacks.History at 0x7f75c9ba2780>

テストデータの分類精度は55%程度までしかあがらなかった

検証用に2個ずつ残しておいた画像を分類してみる

model.predict_classes(x_valid)
→ array([5, 0, 1, 7, 6, 5, 3, 3, 4, 4, 5, 9, 6, 6, 5, 6, 8, 8, 9, 1])

12/20で60%の正解だった

(すべて正解していれば [0, 0, 1, 1, 2, 2, 3, 3, ... , 9, 9] となるはず)

外れたやつをいくつか見てみる

f:id:uhiaha888:20180930024459p:plain

1枚目はカレーの画像だけど、キャラ弁的に何かが乗っかっているみたいなので分類は難しいのかも

2枚目は餃子の画像だけど32x32に縮小すると何かわからない

f:id:uhiaha888:20180930024512p:plain

3枚目と4枚目はハンバーグの画像のはずだけど僕がみても何かよくわからない

逆に当たっていたのも見てみると

f:id:uhiaha888:20180930025112p:plain

オムライスは黄色が特徴的だからかどちらも正解

f:id:uhiaha888:20180930025238p:plain

カレーやラーメンもそれっぽい画像は当てられていた

ということで、自分で集めた画像データでもそれなりの分類をできた

やったね

Keras に入門する 画像分類 Part.2 画像を用意する

32x32のカラー画像とそのラベルがあれば、前回の方法でそれらを判別するシステムを作れそうなので、今回は32x32の画像を用意してみよう

テーマは料理の画像を判別するシステム、とした

個人的にこのテーマは割と良いものが選べたんじゃないかと自負していて、その理由は

  1. 学習データとして得られる画像が均質になりやすい(大体料理のちょっと上から皿全体が映る写真になる)
  2. 画像の数を集めやすい
  3. 同一料理の画像はそこそこ似てるけど、違う料理の画像はそこそこ違う
  4. 分類できるとそこそこ意味がある

最初は、もっと荒唐無稽なテーマの分類(鉛筆と豚と火星とか)をしようと思っていたけど、料理の画像分類ならなんとなくレシピサイトとかで役立ちそう(少なくともなんかそういうサービスの技術検証とかでどこかがやっててもおかしくなさそう)なので随分マシなテーマが設定できたと思う。

ということで、やることは以下

  1. 料理の種類ごとの画像をあつめる
  2. 画像を正方形にトリミング
  3. 32x32に縮小
  4. nparrayとして保存

早速やっていく

  1. 料理の種類ごとの画像をあつめる

"Google画像検索をスクレイピングしたらいいんでしょ"と思っていたけど

Yahoo、Bing、Googleでの画像収集事情まとめ - Qiita とかみてみるとそんなに簡単でもないみたい

そもそものサンプルは、"10種類のカテゴリに対して、訓練データで50000枚、テストデータで10000枚 だったので各カテゴリで大体6000枚使っていることになる

Googleの画像検索でとりあえず"カレーライス"で検索して"結果をもっと表示"を押して最大限表示させると700枚くらいの画像が表示できた

6000枚には遠く及ばないが、まあまあ現実的な数じゃないかと思うのでこれを使うことにする

具体的な手順は、

  1. Google画像検索で料理の名前を入力
  2. 下にスクロール & "結果をもっと表示"をクリック でできるだけ画像を表示する
  3. Chromeの開発ツールでElementを表示
  4. 全体をコピー
  5. テキストファイルとして保存
  6. 保存したファイルから imgタグ かつ srcが "https://encrypted-tbn0" で始まるものを抜き出す
  7. そのimgタグのsrcを画像ファイルとしてダウンロードする

1~5が手作業になるけど10種類くらいなら対して問題にならないのでこれを実行できるコードを書く

import sys
import urllib.request

from bs4 import BeautifulSoup
from time import sleep
import os

def main(file_path, save_dir):
    html_str = None
    with open(file_path, 'r') as f:
        html_str = f.read()
    soup = BeautifulSoup(html_str, features="html.parser")
    imgs = soup.find_all('img', src=lambda s : s and s.startswith('https://encrypted-tbn0'))
    imgs = [ i['src'] for i in imgs ]

    for idx, url in enumerate(imgs):
        print('{}/{}'.format(idx+1, len(imgs)))
        download(url, './{}/{}'.format(save_dir, idx))

def download(url, dest):
    data = urllib.request.urlopen(url).read()
    with open(dest, mode='wb') as f:
        f.write(data)
        sleep(1)

if __name__ == '__main__':
    menus = [
        ('ramen.txt', 'ramen'),
        ('curry.txt', 'curry'),
        ('spaghetti.txt', 'spaghetti'),
        ('gyoza.txt', 'gyoza'),
        ('omlette-rice.txt', 'omlette-rice'),
        ('sandwich.txt', 'sandwitch'),
        ('sushi.txt', 'sushi'),
        ('hamburg.txt', 'hamburg'),
        ('steak.txt', 'steak'),
        ('miso-soup.txt', 'miso-soup')
    ]
    for menu in menus:
        os.mkdir(menu[1])
        main(menu[0], menu[1])

1~5の手順の結果をramen.txt とかの名前で保存しておくと、ramen とかのフォルダの下に連番で画像を保存してくれる

ちなみに今回検索したのは

ラーメン カレーライス スパゲッティ 餃子 オムライス サンドイッチ 寿司 ハンバーグ ステーキ 味噌汁

の10種類

  1. 画像のトリミング

note.nkmk.me

に画像の中心を最大の正方形でトリミングする方法が書いてあるのでこれをそのまま使う

(例えば100x200の画像から中心の100x100を抜き出すことにしたけど、余白部分を白とか黒とかでパディングして200x200にするという方法もあるとは思う。面倒なので検証はしていないけど精度が上がる可能性はあると思う)

全部で8000枚くらい集められた

cifar10では学習用と検査用に分けてあったけど、最後の確認用に各カテゴリ2枚ずつ残しておこうと思う

というので書いたコードが以下

# coding: utf-8
from PIL import Image
import glob
import numpy as np
import pickle

def crop_center(pil_img, crop_width, crop_height):
    img_width, img_height = pil_img.size
    return pil_img.crop(((img_width - crop_width) // 2,
                         (img_height - crop_height) // 2,
                         (img_width + crop_width) // 2,
                         (img_height + crop_height) // 2))

def crop_max_square(pil_img):
    return crop_center(pil_img, min(pil_img.size), min(pil_img.size))

def to_ary(d):
    files = glob.glob('./{}/*'.format(d))
    train = []
    test = []
    valid = []
    for i, f in enumerate(files):
        im = Image.open(f)
        # トリミング
        sq_im = crop_max_square(im)
        # 32x32へ変形
        im32 = sq_im.resize((32, 32))
        # numpy化
        im_ary = np.asarray(im32)
        if im_ary.shape != (32, 32, 3):
            continue
        if i < 20:
            test.append(im_ary)
        elif i < 22:
            valid.append(im_ary)
        else:
            train.append(im_ary)
    return train, test, valid

if __name__ == '__main__':
    X_train, X_test, X_valid = [], [], []
    Y_train, Y_test, Y_valid = [], [], []
    dirs = [
      'curry',
      'gyoza',
      'hamburg',
      'miso-soup',
      'omlette-rice',
      'ramen',
      'sandwitch',
      'spaghetti',
      'steak',
      'sushi'
    ]
    for idx, d in enumerate(dirs):
        print(d)
        x_train, x_test, x_valid = to_ary(d)
        X_train = X_train + x_train
        X_test = X_test + x_test
        X_valid = X_valid + x_valid
        Y_train = Y_train + [[idx]] * len(x_train)
        Y_test = Y_test + [[idx]] * len(x_test)
        Y_valid = Y_valid + [[idx]] * len(x_valid)
    with open('img32.pickle', mode='wb') as f:
        pickle.dump((np.array(X_train), np.array(Y_train), np.array(X_test), np.array(Y_test), np.array(X_valid), np.array(Y_valid)), f)

次回このデータで分類器を作ってみよう

Keras に入門する 画像分類 Part.1 サンプルを動かしてみる

前回サンプルで扱うデータを眺めてみたので、今回は実際にサンプルを動かして画像の分類をしてみたい

サンプルはこれ

keras/cifar10_cnn.py at master · keras-team/keras · GitHub

colab

https://colab.research.google.com/drive/1oCHanNltHykuY2R89lUFYY90cLyIKBrv

基本的にサンプルをそのまま動かしていくだけ コメントもcolabの方に書いていく

一層目は

# 一層目を追加
model.add(Conv2D(32, (3, 3), padding='same',
                 input_shape=x_train.shape[1:]))
model.add(Activation('relu'))

Conv2Dのドキュメントを見ると

keras.layers.Conv2D(filters, kernel_size, strides=(1, 1), padding='valid', data_format=None, dilation_rate=(1, 1), activation=None, use_bias=True, kernel_initializer='glorot_uniform', bias_initializer='zeros', kernel_regularizer=None, bias_regularizer=None, activity_regularizer=None, kernel_constraint=None, bias_constraint=None)

32(filters)は出力される画像の枚数、(3, 3) (kernel_size) はフィルタのサイズとのこと

また

x_train.shape[1:]

→ (32, 32, 3)

でinput_shape には画像1枚のデータの形を渡している

あと、現在の出力のshapeはmodel.output_shape で見れるみたい

model.output_shape
→ (None, 32, 32, 32)

2層目もほぼ同じ

# 二層目を追加
model.add(Conv2D(32, (3, 3)))
model.add(Activation('relu'))

このあとMaxPooling2Dで画像のサイズを小さくしている

# pooling で画像を縮小
model.add(MaxPooling2D(pool_size=(2, 2)))

model.output_shape
→ (None, 16, 16, 32)

Dropoutは指定した割合のノードを非活性化させて過学習を抑えるやつ

# Dropout 
model.add(Dropout(0.25))

3層目(と数えるのか? poolingが3層目?)以降も1~2層目と同じ感じ

model.add(Conv2D(64, (3, 3), padding='same'))
model.add(Activation('relu'))
model.add(Conv2D(64, (3, 3)))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))

つぎにFlattenを挟んでいる これは (50000, 32, 32, 3) とかを (50000, 32323) みたいな形に変形するやつ

model.add(Flatten())

フラットにした出力を512個のノードを持つ層に全連結する

model.add(Dense(512))
model.add(Activation('relu'))
model.add(Dropout(0.5))

最後に、10種類の分類問題なので10個のノードを持つ層に全連結、活性化関数はsoftmax

model.add(Dense(num_classes))
model.add(Activation('softmax'))

ここまででレイヤの設定は終了

次にオプティマイザの設定

# パラメータの更新に使うオプティマイザを指定
opt = keras.optimizers.rmsprop(lr=0.0001, decay=1e-6)

SGDとかAdagradとかは知ってたけど RMSPropって初めて知った

損失関数とかを設定してコンパイル

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

画像データは0~255の整数値だけど 0~1.0の実数にしたいので変換

# データの準備
x_train = x_train.astype('float32')
x_test = x_test.astype('float32')
x_train /= 255
x_test /= 255

ImageDataGenerator が何かよくわかっていないけど、学習データにノイズをのせたりちょっと変形させたりして学習データを増やすやつ?? あとで調べる

データの拡張をしないなら

model.fit(x_train, y_train,
              batch_size=batch_size,
              epochs=epochs,
              validation_data=(x_test, y_test),
              shuffle=True)

で学習できます

今回は拡張しないでやりました

Google Colaboratory の GPU ランタイムで、およそ1時間ほどで学習が完了しました (結構早い段階でloss, accは変わらなくなってたみたいです)

Train on 50000 samples, validate on 10000 samples
Epoch 1/100
50000/50000 [==============================] - 34s 678us/step - loss: 1.7920 - acc: 0.3452 - val_loss: 1.5589 - val_acc: 0.4344
Epoch 2/100
50000/50000 [==============================] - 33s 669us/step - loss: 1.4828 - acc: 0.4602 - val_loss: 1.3297 - val_acc: 0.5235
Epoch 3/100

...

Epoch 99/100
50000/50000 [==============================] - 33s 667us/step - loss: 0.6290 - acc: 0.7919 - val_loss: 0.7397 - val_acc: 0.7645
Epoch 100/100
50000/50000 [==============================] - 33s 667us/step - loss: 0.6290 - acc: 0.7928 - val_loss: 0.6674 - val_acc: 0.7836
<keras.callbacks.History at 0x7f5460e23a20>

最終的に80%程度の精度で判別できるようになったみたいです

本当に判別できるようになっているのか、確認してみます

model.predict(x_train[0:1])
→ array([[5.1839565e-05, 9.5230262e-06, 4.2473815e-02, 1.3438360e-01,
        1.5156801e-02, 4.1686233e-02, 7.6447803e-01, 1.6840984e-03,
        4.3788212e-05, 3.2197495e-05]], dtype=float32)

y_train[0]
→ array([0., 0., 0., 0., 0., 0., 1., 0., 0., 0.], dtype=float32)

学習データに使ったデータで確認するべきではないとは思いますが、とりあえず最初の画像は 7番目のラベルが 0.764で一番大きな値になっていて、y_train[0]と一致しているようです

model.predict(x_train[1:2])
→ array([[3.8416937e-07, 4.2660031e-05, 4.9953406e-09, 4.7760366e-08,
        1.3378058e-10, 6.4784986e-09, 4.5471082e-11, 1.5476447e-08,
        1.4176493e-06, 9.9995542e-01]], dtype=float32)

y_train[1]
→ array([0., 0., 0., 0., 0., 0., 0., 0., 0., 1.], dtype=float32)

2つ目の画像も10番目のラベルが99.99でy_train[1]と一致していますね

ということでサンプルを実際に動かしてみることができました

次回はこれを応用して独自の画像分類器を作って見たいと思っています。