プログラミング素人のはてなブログ

プログラミングも電気回路も専門外の技術屋の末端が勉強したことや作品をアウトプットするブログ。コードに間違いなど見つけられたら、気軽にコメントください。 C#、Python3、ラズパイなど。

ニューラルネットワークでQRコードを解読する

QRコードとは2次元バーコードの一種で、スマホなどで読み取ることで、文字列やURLなどを取得できるものです。
www.keyence.co.jp

QRコードは無料でweb上などのサービスで、好きな内容を書き込んだものを作ることもできます。
一方、その内容を読み取るのは専用のアプリ以外では簡単にはできません。
そんな、簡単ではないことに挑戦しているのが↓
nlab.itmedia.co.jp

仕組みさえわかれば肉眼でもって…。

そこで肉眼ではなく、我々人類の強い味方、「ニューラルネットワーク」で解読ができるか?ということに挑戦してみました。
ここではweb用のURLをQRコードに書き込んで、ニューラルネットワークで解読してみます。

ニューラルネットワークの構想

ニューラルネットワークとしてはモノクロの画像に、URLの文字列のn番目、という情報を付加して分類問題として評価します。(もっとスマートな方法があったら教えてください。)
f:id:s51517765:20181111201051p:plain

ニューラルネットワークに入力するデータはNumpyのListなので、x_train

[[ 1.  1.  1. ...  1.  1.  0.]
 [ 1.  1.  1. ...  1.  1.  1.]
 [ 1.  1.  1. ...  1.  1.  2.]
 ...
 [ 1.  1.  1. ...  1.  1. 27.]
 [ 1.  1.  1. ...  1.  1. 28.]
 [ 1.  1.  1. ...  1.  1. 29.]]

のようなイメージになります。(最後の要素が文字列のIndex)
y_train

[104. 116. 116. 112.  58.  47.  47. 115.  53.  49.  53.  49.  55.  55.
  54.  53.  46. 104.  97. 116. 101. 110.  97. 100. 105.  97. 114. 121.
  46. 106. 112.  47.]

↑本ブログのアドレスをバイナリに変換したもの

URLは最大で30文字とし、30文字未満のときはスペースをいれてpaddingします。

レーニング、テストデータ作成

これもpythonで作成します。
最大30文字のURLっぽい文字列をランダムに文字列を出力して作成します。
URLに使える文字はlist = "abcdefghijklmnopqrstuvwxyz0123456789:/.-http.www.//.co.jp//" としてこのなかからランダムに選びます。よく使われる文字は2回登録し選択される確率を上げるようにしました。
これをpython

$ pip install qrcode

QRコードを作成します。

import random
import random

max_url_length = 30
min_url_length = 25

def dec2chr(num):
    text = ""
    for i in num:
        i = int(i)
        chr(i)
        text += chr(i)
    return text

def makeQR(url, cnt):
    qr = qrcode.QRCode(version=12, error_correction=qrcode.constants.ERROR_CORRECT_H, box_size=1, border=4)
    print(url)
    qr.add_data(url)
    qr.make()
    img = qr.make_image(fill_color="black", back_color="white")
    img.save(cnt + '.jpg')

def make_dataset(count):
    os.chdir(folder + "/data")
    for cnt in range(count):
        y_bin = np.zeros((max_url_length))
        url_length = random.randint(min_url_length, max_url_length)
        try:
            for i in range(max_url_length):
                if i < url_length:
                    rand = random.randint(0, len(list) - 1)
                    ii = ord(list[rand])  # chr2dec
                    y_bin[i] = ii  # 10進数バイナリ
                elif i == url_length:
                    url = dec2chr(y_bin)
                    y_bin[i] = 32  # スペースで埋める
                else:
                    y_bin[i] = 32  # スペースで埋める
            cnt = cnt.zfill(5)  # zero padding
            makeQR(url, cnt)
            file = open('list.txt', 'a', encoding='utf')  # 書き込みモードでオープン
            file.write(url + "\n")
        except:
            print("Error= ", url)

if __name__ == "__main__":
    make_dataset(400)

レーニング用、テスト用として400個作成しました。
URL(正解)はテキストファイルに同時に出力しておきます。

ここで、最初ハマってしまったのが、数字のファイル名の読み込み順(ソート順)です。
数字の順番に読み込まれると思い込んでいましたが、たとえば…18、19、20…ではなく18、19、100となってしまいます。
そこで、

 cnt = cnt.zfill(5)  # zero padding

このようにゼロで埋めます。
これで…00018、00019、00020…とすることができます。

ニューラルネットワークで学習

from keras.utils.np_utils import to_categorical
from keras.layers import Dense
from keras.models import Sequential
import numpy as np
import os
import glob
from PIL import Image

train_size = 360 
test_size = 40
list_size = 30
max_num = 122 + 1  # 正解に使われる最大が122だから0~122で123分類?
size = 73

model_weight = 'cnn_qr.hdf5'  # モデルを保存する名前

np.set_printoptions(precision=2, suppress=True)  # 指数表示禁止、少数表示
np.set_printoptions(threshold=300000)  # 要素の省略禁止
folder = "C:/Users/***/Projects/QRcode"

def np_chr2dec(text): #文字列をnumpy配列に
    y = np.array([])
    for j in text:
        i = ord(j)
        y = np.append(y, i)
    return y

def model_build():
    x_train = []  # 普通のリスト
    x_test = []
    y_train = []
    y_test = []

    file = open('list.txt', 'r', encoding='utf')  # ファイルオープン
    url_list = []
    while (True):
        Line = file.readline()
        if Line == "":
            break
        print(Line)
        Line = Line.replace("\n", "")
        url_list.append(np_chr2dec(Line))  # 10進数に変換して追加

    # Trainデータ作成
    image_count = 0
    os.chdir(folder + "/data")
    filelist = glob.glob("./*")
    print(type(url_list))
    for picture in filelist:
        if ".jpg" in picture:
            print("picture = " + picture)
            image = np.array(Image.open(folder + "/data/" + picture).convert('L'))  # モノクロ 'L'
            # reshapeを使って開かれた配列を1次元配列に変換する
            image_risize = image.reshape(image.size)
            image_risize = image_risize.astype('float32') / 255
            if len(url_list[image_count]) == 0:
                break
            for i in range(max_url_length):
                image_risize_plus = np.append(image_risize, i)  # 画素 + 答えのインデックス
                if image_count <= train_size:
                    # バッチリストに追加していく
                    x_train.append(image_risize_plus)
                    y_train.append(url_list[image_count][i])
                elif image_count <= train_size + test_size:
                    x_test.append(image_risize_plus)
                    y_test.append(url_list[image_count][i])
                elif image_count > train_size + test_size:
                    break
        image_count += 1

    # arrayに変換
    x_train = np.asarray(x_train)
    x_test = np.asarray(x_test)
    y_train = np.asarray(y_train)
    y_test = np.asarray(y_test)

    # 正解ラベルをone-hot-encoding
    y_train = to_categorical(y_train, max_num)
    y_test = to_categorical(y_test, max_num)

    model = model_read()
    model.compile(optimizer='rmsprop', loss='categorical_crossentropy', metrics=['accuracy'])
    model.fit(x_train, y_train, batch_size=1460, epochs=60, verbose=1)  # データを学習

    model.save_weights(model_weight)  # 学習結果を保存
    model.evaluate(x_test, y_test)

def model_read():
    os.chdir(folder)

    model = Sequential()
    model.add(Dense(64, activation='relu', input_dim=size * size + 1))
    model.add(Dense(64, activation='relu', input_dim=64))
    model.add(Dense(64, activation='relu', input_dim=64))
    model.add(Dense(64, activation='relu', input_dim=64))
    model.add(Dense(max_num, activation='softmax'))  # softmax
    model.compile(optimizer='rmsprop', loss='categorical_crossentropy', metrics=['accuracy'])
    print("") #改行が足りない
    # 学習済みモデルの重みのデータを読み込み
    if os.path.exists(model_weight):
        model.load_weights(model_weight)  # 最初は重みデータファイルがないとErrorになる
    else:
        print("Fail to read model weigth!")
    return model

def check():
    check_folder = "C:/Users/***/Projects/QRcode/check"
    model = model_read()
    os.chdir(check_folder)
    filelist = glob.glob("./*")

    for picture in filelist:
        if ".jpg" in picture:
            image = np.array(Image.open(check_folder + "/" + picture).convert('L'))  # モノクロ 'L'
            # reshapeを使って開かれた配列を1次元配列に変換する
            image_risize = image.reshape(image.size)
            image_risize = image_risize.astype('float32') / 255
            answer = ""  # 復元されたURL
            bin = ""
            for i in range(max_url_length):
                image_risize_plus = np.append(image_risize, i)  # 画素 + 答えのインデックス
                image_risize_plus = image_risize_plus.reshape(1, size * size + 1)  # 1次元化
                # 判定
                res = model.predict([image_risize_plus])
                y = res.argmax()  # 値の中で最も値が大きいものが答え
                bin += str(y)
                answer += chr(y)
            print(picture)
            print(bin)
            print(answer)

if __name__ == "__main__":
    model_build()

結果

いくつかの一流サイトのアドレスをテストとして投入してみます。
f:id:s51517765:20181111195212j:plain
"https://www.google.co.jp/"
→ pppppooooooooooo
gooooooooooogleに近い!!

f:id:s51517765:20181111195225j:plain
"https://www.yahoo.co.jp/"
→ /////////////////////////
オールスラッシュ!?

f:id:s51517765:20181111195236j:plain
"https://keras.io/ja/"
→ pppppppppppp..........oo
"オー"と”ドット”はあってるかも?

f:id:s51517765:20181111195246j:plain
"https://s51517765.hatenadiary.jp/"
→ pppppooooooooooooo
pはhttpsのpか??

これは、失敗といっていいでしょう。
訓練を重ねても学習率も20%ぐらいまでしかいかないので、複雑すぎるのか?中間層をもっと複雑にすればいいのか?