GPUを使って無線LANをクラックする話・読解編・序

<VTuber語法>
はいどうもー!実在する架空のバーチャルプログラマの平藤でーす。のじゃのじゃー。

世の中世知辛い!

あの〜本職(?)が〜データセンターおじさんなんですけど〜、
無線LANのクラックやってもお金にならない!やってもあんま意味ない!(社会的に)

え〜そんなことよりも〜はるかに〜あの〜
データセンターとかで〜
重いサーバー二人掛かりで設置して腰痛めたり〜
あの〜機嫌が悪いとパケットが通らなくなるルータくんの設定したり〜
あのぉ…新しいサーバくんにSSDを刺したのに無視されたりすることなどのほうがぁ…
人生においてぇ…重要なことぉ…なのじゃあ。のじゃーーーーー。
</VTuber語法>

えー、何の話でしたっけ。あーそうだ。WPA2のパスワードをクラックしてたんでした。今回はその続きをやって、実際にクラックするのを目指していきましょう。

写真は今年の紫陽花です。

そういえば。そろそろ紫陽花の季節も終わりですけど、紫陽花の「花」って散らないんですよ。知ってました?…「フォトジェニック」ではないからか、街中の紫陽花の「花」はだいたい人間に刈り取られちゃうんですが。せちがらーーーーい!

WPA2でこっそり堂々と鍵を交換するヒミツ:有料

前回がんばってスマホが無線LANルータと「つながる」までのパケットをキャプチャーしたんでしたね。

今回は、このパケットのうち、「認証」をするために送りあっているKeyメッセージの部分をWPA2の仕様に沿ってもう少し詳しく読み解きましょう。

…うーん…。

…うーん…??

よーし、パパ気の効いた解説でも書いちゃうぞー、と思っていたのですが、ネット上に参考になる記事がありません。しょうがないにゃあ・・、(元の仕様書読むから)いいよ。

…と思ったところ、WPA2はIEEE802.11iという規格が元になっているらしいというところまでは突き止めたですが、IEEE802.11iの仕様書はなんと有料らしいのです。あと最近出たWPA3の仕様書ダウンロードフォームを発見したのですが(WPA2はどこだよ??)、なんか簡単にダウンロードできる気配じゃない!

世の中WiFiデバイスばっかなのに仕様書が地味にどこにあるのかわからない!あっても有料!

世の中、世知辛いのじゃー!

秘伝のソースからWPA2を理解するプログラマーの会

…えー。というわけで、作戦を変更して、逆にPyritのソースコードを解読してWPA2の「パスワード認証」に迫っていこうと思います。

えっ、そんな場当たり的でいいの?なんて声が聞こえてきたような気がしましたが、いいんです。無線LANのハックそのものが目的ではないですし(「目的」は強いて言うならGPUと遊ぶベンチマーク取る事だったんだよなぁ)、Pyritのソースで遊んでいるわけですから、WPA2の仕様書よりPyritのソースと仲良くなる方がきっとこの先楽しいと思います。たぶん。

雑に状況を要約:ゼアイズパスワードオンリー、合言葉しかない

こうなると我々には頼るべきものはもう何もありません。我々に残されたのは、パケットダンプとPyritのソース、そしてWiFiデバイスのみ。…まぁ、よくあるシチュエーションだなぁ。

無線LANルータに接続するために必要な情報は、WPA2-PSKを使っている場合、パスワードだけです。それ以外には正確に何もいりません。これが漏れるとオシマイです。FreeWiFiになります。

何を今更、当たり前なことを何度も繰り返しやがって、と思われるかもしれませんが、これはもう一度じっくり観察しておいてもよい事実だと思います。

WiFiの仕様を決めているWiFiアライアンスはいわゆるライセンス商法をやっておりまして、WiFiアライアンスの言う通りに製品を作って、WiFiアライアンスの試験に合格しないと「WiFi」は名乗れないようになっています。そうやって縛ることで互換性を高めて「とりあえず『WiFi』ってシール貼ってあるやつ買えば無線でネット繋がるんでしょ?」って消費者に安心して買ってもらおうとしてるわけですね。USBもまぁだいたい似たような感じなんですが、USBは最近USB Type-C™ Authenticationというのが入りまして、USBでお互いに通信する前に、相手の機械が本当にUSBの業界団体から認証をもらっているのかどうかのチェックをする事が可能になったらしいのです。今までは「USBのロゴを使ってよい」という商標の仕組みを使って許可してれば十分だったのが、それを守らない人が出てきたからそうなったんでしょうね〜。

さて。もし仮に、WiFiにもUSB Authenticationのような仕組みが導入されていた場合、Pyritはクラックする時に、パスワードだけでなく、WiFiアライアンスからもらった証明書も考慮してクラックしないといけなかったりするかもしれません(そうじゃないかもしれません)。そういうややこしい仕組みはない、パスワードだけ考えればいい、そういう事を確認したかったのです。なんで仕様書もソースも読まずにそんな事を断言できるのかというと、…WiFiモジュールの一切入っていないGPUサーバでPyritが動いてクラックできているからです。えぇ、我々にはもはや…こんな推論しか、できる事はないのですよ。

言い換えると、WPA2-PSKで認証できたかどうかのチェックをする関数は、次のような関数シグネチャに集約される事が予想されます:

// あくまで予想。
bool checkWPA2Password(byte[] captureData, string password);

うーん、とってもかんたん。もちろん、パケットキャプチャデータの中からどのデータをどう使うかが大事なわけですし、もっと言えば並列化してるのでもっともっと複雑になっていて、関数1つに纏まっているはずはないと思います。が、こういうのを多少なりとも頭に浮かべながら読むのって大事だと思うんよ。

とりあえず正解のデータを突っ込んで「解けた」と言ってもらう

Pyritのソースもそれなりにデカいので、まず一旦「解けた」と言うコードパスを走ってもらいましょう。言い換えると、正解を突っ込んで「クラックできました!」とPyritに叫んでもらいましょう。マッチポンプ…はちょっと違うか。

えーと。パスワードの正解をズバリ言ってしまうと「xxxxxxxxx」です。今すぐダウンロー
。データセンターに置いてあるWiFiのパスワードもこれです。

(ブログを検閲する偉い人によって文章の一部が改変されました)

…あーっ、ちょっと、悪用しないでくださいよ。まぁ「おらけんま」ができなくなった今、わざわざオランダヒルズの面倒な警備を乗り越えてオフィスにFreeWiFiしにやってくる輩も居ないだろうと思って書いてるわけですが。FreeWiFiなんて今日び、駅からファミレスからコンビニ、スタバまで、そこらじゅうにあるからね。…あとは隣のオフィスの人がこれを読んで無いのを祈るだけだな。

さて。この正解のパスワードをPyritに教えて「解けた!!!」って叫んでもらうには次の呪文を唱えればよいです(zoi.capはリポジトリに入っています):

~/src/Pyrit$ pyrit -r zoi.cap -i passwords.txt -b '00:1a:eb:ac:51:30' --all-handshakes attack_passthrough
Pyrit 0.5.1 (C) 2008-2011 Lukas Lueg - 2015 John Mora
https://github.com/JPaulMora/Pyrit
This code is distributed under the GNU General Public License v3+

Parsing file 'zoi.cap' (1/1)...
Parsed 4055 packets (4055 802.11-packets), got 104 AP(s)

Attacking 4 handshake(s).
Tried 1 PMKs so far; 0 PMKs per second. xxxxxxxxx

The password is 'xxxxxxxxx'.

passwords.txtにパスワードの羅列が書いてあります。今回は正解の1行だけを入力しました:

$ cat passwords.txt
xxxxxxxxx

しかし。そんな雑なパスワードでいいのかよ!という声がいかにも聞こえてきそうですが、Pyritも使っている辞書攻撃はだいたい英語を前提にしているので、ローマ字の日本語と英語を混ぜたこのパスワードは意外と当てられないんじゃ無いかなぁというのが個人的な見解です。

Pyritがデフォルトで持ってる辞書がtest/dict.gz以下にあるのですが、例えばそれでは破れません:

$ pyrit -r zoi.cap -i test/dict.gz -b '00:1a:eb:ac:51:30' --all-handshakes attack_passthrough
Pyrit 0.5.1 (C) 2008-2011 Lukas Lueg - 2015 John Mora
https://github.com/JPaulMora/Pyrit
This code is distributed under the GNU General Public License v3+

Parsing file 'zoi.cap' (1/1)...
Parsed 4055 packets (4055 802.11-packets), got 104 AP(s)

Attacking 4 handshake(s).
Tried 4094 PMKs so far; 1552 PMKs per second. abrogate

Password was not found.

あとWiFiって近くに居ないと繋げないですから、攻撃を受ける回数はネット越しで攻撃できるsshと比べれば段違いに少ないはずですし、「ネットをつなぐための鍵」なのでできるだけ覚えやすい方が嬉しいかなぁと思ってこれに設定してます。スマホの暗証番号が4桁なのとだいたい同じ理論武装です。

嘘です。ジャバがすき(きらい)だからで、残りは全部後付け。Javaとは何で、なぜ必要ですか。

…はい。

WiFiクラックって(今まさにやってるような)スクリプトキディ的ハッキングの題材としてはありがちで、ネット上でも雑誌上でもよく見るから、みんな好きなんでしょうけど(Pyritクラスター作ってるやつらもおるぞ)、「WiFiのパスワードってそこまでクラックする価値あるの?sshとかTLSの穴探した方が『コスパ』いいんじゃない?」みたいなのが正直な感想ですな。己の記事全否定か〜?って感じですが、いや、まぁ、今回やってるのはベンチマークであって、そういう「価値」とはまた違う話ですから。…それに、公開IPにやってくる大量のsshログイン試行ログを見ていると、明らかに「業務」「ビジネス」としてやってる感じが見て取れて、それはそれで、気が滅入るのですよ。その点、Pyritクラスターは見てるだけでも楽しそうでしょ?

ソースコードの海に飛び込むのです

だいぶ話が逸れました。Pyritがパスワードを発見するのに成功した場合と失敗した場合のメッセージを得たので、これを足がかりにソースコードを読解していきます。Pyritもそれなりに大きいので、全部読むのは正直大変だなーという判断です。

するとサクッとヒットします。二箇所あるんですが、コマンド名と関数が同じこちらでしょう。

    def attack_passthrough(self, infile, capturefile, essid=None, \
                           bssid=None, outfile=None, all_handshakes=False, \
                           use_aes=False):

# ...

        if not all_handshakes:
            crackers.append(cpyrit.pckttools.AuthCracker(auths[0], use_aes))
        else:
            self.tell("Attacking %i handshake(s)." % (len(auths),))
            for auth in auths:
                crackers.append(cpyrit.pckttools.AuthCracker(auth, use_aes))
        with cpyrit.util.FileWrapper(infile) as reader:
            with cpyrit.cpyrit.PassthroughIterator(essid, reader) as rstiter:
                for results in rstiter:
                    for cracker in crackers:
                        cracker.enqueue(results)
# ...
        for cracker in crackers:
            cracker.join()
            if cracker.solution is not None:
                self.tell("\nThe password is '%s'.\n" % cracker.solution)
                if outfile is not None:
                    with cpyrit.util.FileWrapper(outfile, 'w') as writer:
                        writer.write(cracker.solution)
                break
        else:
            errmsg = "\nPassword was not found."

これの上の方を手繰っていくと、cpyrit.pckttools.AuthCrackerというのがクラッカーをまとめてる親玉らしいことがわかります:

class AuthCracker(object):

    def __init__(self, authentication, use_aes=False):
        self.queue = Queue.Queue(10)
        self.workers = []
        self.solution = None
        if authentication.version == "HMAC_SHA1_AES" \
         and authentication.ccmpframe is not None \
         and use_aes:
            self.cracker = CCMPCrackerThread
        else:
            self.cracker = EAPOLCrackerThread
        for i in xrange(util.ncpus):
            self.workers.append(self.cracker(self.queue, authentication))

    def _getSolution(self):
        if self.solution is None:
            for worker in self.workers:
                if worker.solution is not None:
                    self.solution = worker.solution
                    break

    def enqueue(self, results):
        self.queue.put(results)
        self._getSolution()

    def join(self):
        self.queue.join()
        for worker in self.workers:
            worker.shallStop = True
        self._getSolution()

早速CCMPCrackerThreadとEAPOLCrackerThreadという二種類がいることがわかるわけですが、これをprintfデバッグしてどっちなのか確かめると(こういうのを何の躊躇いもなく「さっ」とやるために前回わざわざ自前でビルドしたんやぞ)、EAPOLCrackerThreadを使っているらしいです。が、–aesってオプションつけてやるとCCMPCrackerThreadを使うようになります。でも、どっちでもちゃんとパスワードを解読できます。…ど、どういう事じゃ…?

ちなみに実装はすぐ上にありまして…

class CrackerThread(threading.Thread):

    def __init__(self, workqueue):
        threading.Thread.__init__(self)
        self.workqueue = workqueue
        self.shallStop = False
        self.solution = None
        self.numSolved = 0
        self.setDaemon(True)
        self.start()

    def run(self):
        while not self.shallStop:
            try:
                results = self.workqueue.get(block=True, timeout=0.5)
            except Queue.Empty:
                pass
            else:
                solution = self.solve(results)
                self.numSolved += len(results)
                if solution:
                    self.solution = solution[0]
                self.workqueue.task_done()


class EAPOLCrackerThread(CrackerThread, _cpyrit_cpu.EAPOLCracker):

    def __init__(self, workqueue, auth):
        CrackerThread.__init__(self, workqueue)
        _cpyrit_cpu.EAPOLCracker.__init__(self, auth.version, auth.pke,
                                          auth.keymic, auth.keymic_frame)


class CCMPCrackerThread(CrackerThread, _cpyrit_cpu.CCMPCracker):

    def __init__(self, workqueue, auth):
        if auth.version != "HMAC_SHA1_AES":
            raise RuntimeError("CCMP-Attack is only possible for " \
                               "HMAC_SHA1_AES-authentications.")
        CrackerThread.__init__(self, workqueue)
        s = str(auth.ccmpframe[scapy.layers.dot11.Dot11WEP])
        msg = s[8:8+6]
        counter = (s[0:2] + s[4:8])[::-1]
        mac = scapy.utils.mac2str(auth.ccmpframe.addr2)
        _cpyrit_cpu.CCMPCracker.__init__(self, auth.pke, msg, mac, counter)

_cpyrit_cpuパッケージは、C言語で書かれたモジュールです。あれっ、CUDAは?まぁいいや。とりあえずここでPythonの外に出る事が確認できました。そして、initに渡されている情報が、それぞれのCrackerで必要な情報らしいことがわかります(そしてそれはEAPoLとCCMPで違うこともわかります)。

それぞれのCrackerはsolve()という(C言語で書かれた)メソッドを持ってて、ここへresultsというのが降ってくるのがわかりますが、このresultsというのは具体的に何なのだろう…と思って”The password is “のところまで戻ってもっかい手繰っていくと、cpyrit.cpyrit.PassThroughIteratorというところへ行き着き、これはさらにその名もCPyritというクラスに処理を投げていることが見て取れます。…そして、こちらにはCUDAとかなんとか書いてありますね。

class CPyrit(object):
    """Enumerates and manages all available hardware resources provided in
       the module and does most of the scheduling-magic.
       The class provides FIFO-scheduling of workunits towards the 'host'
       which can use .enqueue() and corresponding calls to .dequeue().
       Scheduling towards the hardware is provided by _gather(), _scatter() and
       _revoke().
    """

    def __init__(self):
        """Create a new instance that blocks calls to .enqueue() when more than
           the given amount of passwords are currently waiting to be scheduled
           to the hardware.
        """
        self.inqueue = []
        self.outqueue = {}
        self.workunits = []
        self.slices = {}
        self.in_idx = self.out_idx = 0
        self.cores = []
        self.CUDAs = []
        self.OpCL = []
        self.all = []
        self.cv = threading.Condition()

        # CUDA
        if config.cfg['use_CUDA'] == 'true' and 'cpyrit._cpyrit_cuda' in sys.modules and config.cfg['use_OpenCL'] == 'false':

            CUDA = _cpyrit_cuda.listDevices()

            for dev_idx, device in enumerate(CUDA):
                self.CUDAs.append(CUDACore(queue=self, dev_idx=dev_idx))

# ...

        # CPUs
        for i in xrange(util.ncpus):
            self.cores.append(CPUCore(queue=self))

# ...

むっ、じゃあさっきのCrackerThreadっていういかにも「クラックしてます!!!!!」的な空気を纏わせてた連中はCUDAでは走らないってこと?

・・・今日のところは、この辺で勘弁しといてやりますか・・・。

ソース読解のまとめ

まぁとりあえず。CrackerとIteratorの2つの「重い処理」があって、IteratorはCUDAでも処理されているが、CrackerはCPUにしか移譲されてなさそうな所は見えてきました。そしてパスワードのリストを読んでIteratorが作ったresultsをCrackerがsolveして「パスワードあたり!」って叫んだり叫ばなかったりすることも。

これはすごい雑な仮説ですが、もしCUDAのカードをいっぱいぶち込んで5000兆フロップス並列に処理できたとしても、Iteratorは速くなるけど、CPUで動くCrackerがボトルネックになってGPUが遊んでしまうでしょう。すると、Teslaでも大して速くなかった前回の結果を擁護できるかもしれません。Teslaくんは悪く無い、CPUで動いてるCrackerが遅いのが悪いんだ!って。ありそうなボトルネックとしてテキストファイルを読む所や、GPUへパスワードを運ぶメモリ帯域を想像していたんですが、実際にソースを読む事でもう一つ見つけられたわけです。もちろん、ベンチマークとか取ったり実験してみないと、どこがボトルネックかなんてわからんわけですが。

<VTuber語法>
よくわかん事、Pyrit…CUDA…よくわからん…のじゃー(突然戻ってくる狐)

…でも、でもでもでも!
こうやって、こう ちょっとつ、ステップアップする所のぉー

この…CUDAとかC言語とPythonが組み合わさったやつのプロファイルもどうやって取ればいいのかわからないし。
ほんとはこう…いろいろやりたいけど。
(まだ)できないっ!

世知辛い!
世の中世知辛い!
世の中世知辛いのじゃーーー!!!!のじゃー!
ふーんふーんふーん(元ネタを知らないがとりあえず踊っておく狐)
</VTuber語法>

プロファイルの取り方を調べるのは次回までの宿題にしておきます。

データの打ち込みを回避するために手書き文字をスキャンして認識する

今回の目的

こんにちは。

Link-Uの町屋敷です。

今回は下のような手書き文字をパソコンにスキャンして認識するプログラムを作ろうと思います。

テストではどこに文字があるかの情報は全く与えず、文字の場所がどこにあるのかから機械に算出させます。(訓練データもいちいち座標与えるのがめんどくさかったのでそうなりましたが…)

普通のアルファベットと微妙に違うのがわかるでしょうか。

実はこれらは全部cに線を書き足してabcdeにしています。

何のためにこんなものの認識をしたいかというと、外部に出さないようなデータのアノテーション(ラベル付け)をするときに、項目とそれに対応するcが書かれた紙を用意して、項目に該当する部分だけcをaとかeなどに変更してあとAIに任せれば作業効率が上がるかなって。

自分でスキャンされた手書き文字認識を実装してみる

さっそく始めましょう。

画像中の物体を認識するための手法としてR-CNNを参考にします。

画像からの物体認識では大きく二つのパートがあります。画像の中から文字が書かれている領域の候補を選択するパートとその領域内に書かれた文字が何なのかを認識するパートです。

Kerasで一度にR-CNNをできるものもある(より新しいMask-RCNNですら存在する)ようですが、地道に領域をから候補を選択してCNNをするという方法でやります。

Selective Search

画像の中から文字が書かれている領域の候補を選択する方法としてR-CNNの元論文ではSelective searchが使われています。Selective searchにはpythonのパッケージが公開されているのでサンプルを改造して試してみましょう。入力画像は屋上から撮った写真を400*300にリサイズしたものです。

import selectivesearch
from matplotlib import pyplot as plt
import matplotlib.patches as mpatches

def search(img):
    #img = img[0:300,200:800]
    # perform selective search
    img_lbl, regions = selectivesearch.selective_search(
    #    img, scale=5, sigma=0.25, min_size=20)
        img, scale=5, sigma=5, min_size=30)
    candidates = set()
    for r in regions:
        #excluding same rectangle (with different segments)
        if r['rect'] in candidates:
            continue
        # excluding regions smaller than 2000 pixels
        if r['size'] < 30:
            continue
        # distorted rects
        x, y, w, h = r['rect']
        if h == 0 or w == 0:
            continue
        if w / h > 8 or h / w > 8:
            continue
        candidates.add(r['rect'])

    #draw rectangles on the original image
    fig, ax = plt.subplots(ncols=1, nrows=1, figsize=(6, 6))
    ax.imshow(img)
    for x, y, w, h in candidates:
        rect = mpatches.Rectangle(
            (x, y), w, h, fill=False, edgecolor='red', linewidth=1)
        ax.add_patch(rect)
  
    plt.show()
    return candidates

def test_selective_search():
    im = imread("tower.png")
    search(im[:,:,0:3]) #アルファチャンネルを消す

if __name__ == "__main__":
    test_selective_search()
    learn()
    test()

こんな感じで領域の候補が出力されれば成功です。同じ物体に複数の領域が重なっていますがこれが正しい出力結果です。

Selective searchのパッケージが正しく動作していることが分かったので、さっそく訓練データを入れていきましょう。今回の訓練データは、フォントの大きさが違うcが書かれた紙を4枚印刷し、線を付け加えて作ったa,b,d,eの紙をスキャナーで取り込んで縮小したものです。(縮小したものを使わないとSelective Searchの処理が重い)

実際のものがこれです(aのデータ)、折れとか汚れ(スキャン時にできた)とか雑音はいりまくりです。フォントサイズが変わってもAIが対応できるように微妙にフォントの大きさを変えてますが意味があるかは対称を取ってないのでわかりません。

さてこのままさっきのSelective searchに入れてみましょう。

どえらい結果になります。雑音のせいなので取り除きましょう。

今回は背景が白で黒い文字のみの検出を目指しているのでべたに2値化。

    gray = rgb2gray(im)       # グレイスケールに変換
    whiteRegion = gray > 0.8  # 白部分の雑音除去
    gray[whiteRegion] = 1
    whiteRegion = gray <= 0.8  # 黒部分の雑音除去
    gray[whiteRegion] = 0
    im = gray2rgb(gray)

さてこのままさっきのSelective searchに入れてみましょう。<img class=”alignnone wp-image-337 size-full” src=”https://tech.link-u.co.jp/wp-content/
また、Selective searchの設定も変えましょう。

1文字の認識には長方形の領域はいらないので消去、

また上のほうの小さい文字が認識できてないのでomegaを変更して感度も上げ、画像全体が領域として選択されているのでこれも弾きます。

img_lbl, regions = selectivesearch.selective_search(
img, scale=5, sigma=0.25, min_size=20)
#img, scale=5, sigma=8, min_size=30)
candidates = set()
for r in regions:
#excluding same rectangle (with different segments)
if r['rect'] in candidates:
continue
#excluding regions smaller than 2000 pixels
if r['size'] < 20 or r['size'] > 3000:
continue
# distorted rects
x, y, w, h = r['rect']
if h == 0 or w == 0:
continue
if w / h > 2 or h / w > 2:
continue
candidates.add(r['rect'])

まともになりました。

しかしよく見て見ると一つの文字に複数の領域が重なっています。

もとのR-CNNの論文ではgreedy non-maximum suppressionなどを駆使して除去していますが。今回は汚れなどを除いて背景がなく、文字が重なることもなく、また各文字の大きさも一定なので、単純にある領域に近い領域か存在した場合、領域の中心座標だけ残して領域は全部消し、その点から上下左右16pixを新しい領域にしました。

def get_candidate(img_path):
    def is_checked(checked, test_point,r):
        for c in checked:
            if np.linalg.norm(c - test_point) < r:
                return True
        return False

    im = imread(img_path)
    gray = rgb2gray(im)       # グレイスケールに変換
    whiteRegion = gray > 0.8  # 白部分の雑音除去
    gray[whiteRegion] = 1
    whiteRegion = gray <= 0.8  # 黒部分の雑音除去
    gray[whiteRegion] = 0
    # = median(gray, disk(1))
    #gray = denoise_wavelet(gray) #ごましお雑音除去
    im = gray2rgb(gray)
    cand = search(im)
    print(np.shape(im))
    g_img = np.asarray(gray)
    checked_points = []
    images = []
    ren = 16
    for c in cand:
        xc = int(c[0] + c[2]/2)
        yc = int(c[1] + c[3]/2)
        if len(checked_points) is not 0:
            if is_checked(checked_points, np.array([xc,yc]), 10):
                continue
        image = resize(g_img[yc-ren:yc+ren,xc-ren:xc+ren], (28,28))
        images.append(image)
        #X = np.array([image])

        #plt.imshow(X[0], cmap='gray')
        checked_points.append(np.array([xc,yc]))
        #plt.show()
    return images, checked_points

これをa,b,c,d,eの5枚に行います。これで教師データとして画像データを取り出すことができます。

しかし、ラベルデータがないので

CNN

作ったデータセットでCNNの識別器を作ります。

スクリプトは以下の通り。

def learn():
    import glob
    img_pathes = glob.glob("..\\..\\Documents\\dataset\\abcde\\learn\\scan\\*.*")

    x = np.array([])
    y = np.array([])
    for ip in img_pathes:
        print(ip)
        if "a_" in ip:
            i = 0
        elif "b_" in ip:
            i = 1
        elif "c_" in ip:
            i = 2
        elif "d_" in ip:
            i = 3
        elif "e_" in ip:
            i = 4
        else:
            raise("Unexpected File is Loaded")


        imgs, _ =  get_candidate(ip)
        nimgs = np.array(imgs)
        x = np.append(x, nimgs)
        print(len(x))
        y = np.append(y, np.array(len(imgs) * [i]))
        print(len(y))
    x = np.reshape(x, (-1,28,28))
    seed = 123
    (X_train, X_valid, Y_train, Y_valid) = train_test_split(x, y, test_size=0.15, random_state=seed)
    learn_cnn(X_train, X_valid, Y_train, Y_valid)
    py = load_predict(x)
    for a,b,c in zip(x,y,py):
        if not b == np.argmax(c):
            print((b,c))
            plt.imshow(a, cmap='gray')
            plt.show()

def learn_cnn(X_train, X_test, Y_train, Y_test):
    X_train = X_train.reshape(X_train.shape[0], 28, 28, 1).astype('float32')
    X_test  = X_test.reshape(X_test.shape[0], 28, 28, 1).astype('float32')

    #X_train = X_train / 255
    #X_test  = X_test / 255
    # one hot encode outputs
    Y_train = np_utils.to_categorical(Y_train)
    Y_test  = np_utils.to_categorical(Y_test)

    num_classes = Y_test.shape[1]


    # create model
    model = Sequential()
    model.add(Conv2D(64, (5, 5), input_shape=(28, 28, 1), activation='relu'))
    model.add(MaxPooling2D(pool_size=(4, 4)))
    model.add(Conv2D(32, (3, 3), activation='relu'))
    model.add(MaxPooling2D(pool_size=(2, 2)))
    model.add(Dropout(0.2))
    model.add(Flatten())
    model.add(Dense(128, activation='relu'))
    model.add(Dense(5, activation='softmax'))

    # Compile model
    model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
    model.fit(X_train, Y_train, validation_data=(X_test, Y_test), epochs=500, batch_size=200, verbose=2)

    # Final evaluation of the model
    scores = model.evaluate(X_test,Y_test, verbose=0)
    print("CNN Error: %.2f%%" % (100-scores[1]*100))

    model.save('weights.model')

Kerasのサンプルに畳み込み層とプーリング層を追加したものです。多分これが最善ではないです。

パラメータを変えるなど各種改造をするときは、testデータを使わずtrainデータを分割して評価して最善のものを目指します。

このような結果が出力されます。テストデータのうち2パーセント間違えたことがわかります。

また、このスクリプトは、学習後に間違えたデータを表示します。

この2つのデータを間違えたようです。(実際は左がa右がd)

試行錯誤繰り返し、満足したところでtestデータを使って認識結果を出力してみましょう。

教師データを分割していましたが、もったいないのですべてを教師データとしてもう一度CNNを学習させます。

learn_cnn(x, X_valid, y, Y_valid)

テストデータは最初に貼ったデータと

このスパースなデータです。

いつものGPUのideapadS720のスピードの差はこちら

GPUのほうが6倍くらい速いです。

一回大量データかつモデルが複雑な場合もやらねばなあ。

で、文字の識別結果は以下の通り。

画像の中の?はCNNの出力を見てAIに自信がなさそうな時に付けています。自信度がなぜわかるか具体的に書くと今回CNNの出力は5クラスの識別問題なので5次元のベクトルで、それぞれが0から1の値を持っています。5つの次元は[a,b,c,d,e]を表します。

例えば、わかりやすいaの画像が入力された場合5次元のベクトルは[1, 0 , 0, 0, 0]に近い値を出力します。少しdとまぎらわしいaが入力された場合[0.8, 0, 0, 0.2, 0]のような値を出します。このベクトルの数値を見ることによってAIの自信度がわかるということです。

1つめは4ミスでしたが2つ目はノーミスでした。もう少し教師データを増やせば2つぐらいは減らせると思います。丁寧に書けば使えるかなといった感じです。

まとめ

今回はスキャンした手書き文字を認識するプログラムを作りました。

字をきれいに書けば使える程度のものは作れたと思います。

プログラム全文

from keras.models import Sequential
from keras.layers import Dense
from keras.layers import Dropout
from keras.layers import Flatten
from keras.layers.convolutional import Conv2D
from keras.layers.convolutional import MaxPooling2D
from keras import backend as K
from keras.utils import np_utils
from sklearn.model_selection import train_test_split
import numpy as np
from matplotlib import pyplot as plt
import matplotlib.patches as mpatches
from decimal import *
import skimage
from skimage.filters.rank import median
from skimage.morphology import disk
from skimage.transform import resize
from skimage.restoration import denoise_wavelet
from skimage.io import imread
from skimage.color import rgb2gray, gray2rgb
from PIL import Image, ImageDraw, ImageFont
import selectivesearch
import time

def load_predict(X):
    X  = X.reshape(X.shape[0], 28, 28, 1).astype('float32')
    #X = X / 255
    # create model
    model = Sequential()
    model.add(Conv2D(64, (5, 5), input_shape=(28, 28, 1), activation='relu'))
    model.add(MaxPooling2D(pool_size=(4, 4)))
    model.add(Conv2D(32, (3, 3), activation='relu'))
    model.add(MaxPooling2D(pool_size=(2, 2)))
    model.add(Dropout(0.2))
    model.add(Flatten())
    model.add(Dense(128, activation='relu'))
    model.add(Dense(5, activation='softmax'))

    # Compile model
    model.compile(loss='sparse_categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
    model.load_weights('weights.model')
    return model.predict(X)

def search(img):
    #img = img[0:300,200:800]
    # perform selective search
    img_lbl, regions = selectivesearch.selective_search(
        img, scale=5, sigma=0.25, min_size=20)
        #img, scale=5, sigma=8, min_size=30)
    candidates = set()
    for r in regions:
        #excluding same rectangle (with different segments)
        if r['rect'] in candidates:
            continue
        # excluding regions smaller than 2000 pixels
        if r['size'] < 20 or r['size'] > 3000:
            continue
        # distorted rects
        x, y, w, h = r['rect']
        if h == 0 or w == 0:
            continue
        if w / h > 2 or h / w > 2:
            continue
        candidates.add(r['rect'])

    #draw rectangles on the original image
    #fig, ax = plt.subplots(ncols=1, nrows=1, figsize=(6, 6))
    #ax.imshow(img)
    #===========================================================================
    # for x, y, w, h in candidates:
    #     rect = mpatches.Rectangle(
    #         (x, y), w, h, fill=False, edgecolor='red', linewidth=1)
    #     ax.add_patch(rect)
    #===========================================================================

    #plt.show()
    return candidates

def test_selective_search():
    im = imread("a_scan.png")
    search(im[:,:,0:3])

def learn():
    import glob
    #img_pathes = glob.glob("..\\..\\Documents\\dataset\\abcde\\learn\\*.*")
    #img_pathes += glob.glob("..\\..\\Documents\\dataset\\abcde\\learn\\scan\\*.*")
    img_pathes = glob.glob("..\\..\\Documents\\dataset\\abcde\\learn\\scan\\*.*")

    x = np.array([])
    y = np.array([])
    for ip in img_pathes:
        print(ip)
        if "a_" in ip:
            i = 0
        elif "b_" in ip:
            i = 1
        elif "c_" in ip:
            i = 2
        elif "d_" in ip:
            i = 3
        elif "e_" in ip:
            i = 4
        else:
            raise("Unexpected File is Loaded")


        imgs, _ =  get_candidate(ip)
        nimgs = np.array(imgs)
        x = np.append(x, nimgs)
        print(len(x))
        y = np.append(y, np.array(len(imgs) * [i]))
        print(len(y))
    x = np.reshape(x, (-1,28,28))
    seed = 123
    (X_train, X_valid, Y_train, Y_valid) = train_test_split(x, y, test_size=0.15, random_state=seed)
    #learn_cnn(x, X_valid, y, Y_valid)
    learn_cnn(x, X_valid, y, Y_valid)
    py = load_predict(x)
    for a,b,c in zip(x,y,py):
        if not b == np.argmax(c):
            print((b,c))
            plt.imshow(a, cmap='gray')
            plt.show()

def learn_cnn(X_train, X_test, Y_train, Y_test):
    t = time.time()
    X_train = X_train.reshape(X_train.shape[0], 28, 28, 1).astype('float32')
    X_test  = X_test.reshape(X_test.shape[0], 28, 28, 1).astype('float32')

    #X_train = X_train / 255
    #X_test  = X_test / 255
    # one hot encode outputs
    Y_train = np_utils.to_categorical(Y_train)
    Y_test  = np_utils.to_categorical(Y_test)

    num_classes = Y_test.shape[1]


    # create model
    model = Sequential()
    model.add(Conv2D(64, (5, 5), input_shape=(28, 28, 1), activation='relu'))
    model.add(MaxPooling2D(pool_size=(4, 4)))
    model.add(Conv2D(32, (3, 3), activation='relu'))
    model.add(MaxPooling2D(pool_size=(2, 2)))
    model.add(Dropout(0.2))
    model.add(Flatten())
    model.add(Dense(128, activation='relu'))
    model.add(Dense(5, activation='softmax'))

    # Compile model
    model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
    model.fit(X_train, Y_train, validation_data=(X_test, Y_test), epochs=500, batch_size=200, verbose=2)

    # Final evaluation of the model
    scores = model.evaluate(X_test,Y_test, verbose=0)
    print("CNN Error: %.2f%%" % (100-scores[1]*100))

    model.save('weights.model')
    print(time.time()-t)

def get_candidate(img_path):
    def is_checked(checked, test_point,r):
        for c in checked:
            if np.linalg.norm(c - test_point) < r:
                return True
        return False

    im = imread(img_path)
    gray = rgb2gray(im)       # グレイスケールに変換
    whiteRegion = gray > 0.8  # 白部分の雑音除去
    gray[whiteRegion] = 1
    whiteRegion = gray <= 0.8  # 黒部分の雑音除去
    gray[whiteRegion] = 0
    # = median(gray, disk(1))
    #gray = denoise_wavelet(gray) #ごましお雑音除去
    im = gray2rgb(gray)
    cand = search(im)
    print(np.shape(im))
    g_img = np.asarray(gray)
    checked_points = []
    images = []
    ren = 16
    for c in cand:
        xc = int(c[0] + c[2]/2)
        yc = int(c[1] + c[3]/2)
        if len(checked_points) is not 0:
            if is_checked(checked_points, np.array([xc,yc]), 10):
                continue
        image = resize(g_img[yc-ren:yc+ren,xc-ren:xc+ren], (28,28))
        images.append(image)
        #X = np.array([image])

        #plt.imshow(X[0], cmap='gray')
        checked_points.append(np.array([xc,yc]))
        #plt.show()
    return images, checked_points

def test():
    import glob
    img_pathes = glob.glob("..\\..\\Documents\\dataset\\abcde\\test\\*")

    label_to_char = ["a", "b", "c", "d", "e"]
    i = -1
    for ip in img_pathes:
        x = np.array([])
        y = np.array([])
        print(ip)
        imgs, points =  get_candidate(ip)
        nimgs = np.array(imgs)
        x = np.append(x, nimgs)
        print(len(x))
        y = np.append(y, np.array(len(imgs) * [i]))
        print(len(y))

        x = np.reshape(x, (-1,28,28))
        seed = 123
        pre_y = load_predict(x)
        im = Image.open(ip)
        draw = ImageDraw.Draw(im)
        font = ImageFont.truetype("C:\\Windows\\Fonts\\meiryob.ttc", 16)
        for py, p in zip(pre_y, points):
            pst = np.argsort(py)
            max_val = np.max(py)
            am1, am2 = pst[-1], pst[-2]
            if   max_val > 0.95:
                draw.text((p[0]+16, p[1]), label_to_char[am1], fill=(255, 0, 0), font=font)
            elif max_val > 0.8:
                draw.text((p[0]+16, p[1]), label_to_char[am1] +"," + label_to_char[am2] + "?", fill=(255, 0, 0), font=font)
            elif max_val > 0.4:
                draw.text((p[0]+16, p[1]), label_to_char[am1] +"," + label_to_char[am2] + "??", fill=(255, 0, 0), font=font)
            else:
                draw.text((p[0]+16, p[1]), "?", fill=(255, 0, 0), font=font)
        im.save("hand_only_sparse.png")
        im.show()

if __name__ == "__main__":
    #test_selective_search()
    learn()
    test()

GPUを使って無線LANをクラックする話・準備編

<Youtuber語法>ブンブン・ハロー、Link-U。どうも、平藤です。

えーそれでは今日はね、無線LANのパスワードをクラックするPyrit、これのほうを、CPUとかGPUで動かしてみようと思いまーす。</Youtuber語法>

はい。というわけで、弊社の業務用無線LANルータ、SSIDがその名も「zoi」のWPA2-PSKのパスワードをCPUやGPUでクラックすることで、機械学習とはまた違った「アプリケーション」でのパフォーマンスを実測してみたいと思います。

パケットをキャプチャする

ちなみに写真はオフィスにある会議室の机の下です。いつもと見慣れない風景で、ちょっとかっこよくない?…そうでもないか。

なにはなくとも、まずは攻撃するために、ルータがスマホをパスワードで認証して通信を開始する「ハンドシェイク」の瞬間のパケットをキャプチャしましょう。以下、わたしの使っているMacBook Pro + OSX HighSierraでWPA2-PSKのパケットをキャプチャする方法を説明します。Linuxでの方法は他の人に譲ることにします。餅は餅屋なので。

まずは次のコマンドで、どんな電波が飛んでるかをチェックします:

sudo /System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport -s

結果はこんな感じです:

% sudo
/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport -s
Password:
SSID BSSID RSSI CHANNEL HT CC SECURITY (auth/unicast/group)
LU-MOBILE 00:1a:eb:b2:c3:b6 -66 112,-1 Y JP WPA2(PSK/AES,TKIP/TKIP)
PAI-AP2.4G1 34:76:c5:4d:a7:c8 -76 10,-1 Y JP WPA(PSK/AES,TKIP/TKIP) WPA2(PSK/AES,TKIP/TKIP)
PAI-AP2.4G2 06:76:c5:4d:a7:c8 -75 10,-1 Y JP NONE zoi 00:1a:eb:ac:51:20 -44 9 Y JP WPA2(PSK/AES/AES)
LU-MOBILE 00:1a:eb:b2:c3:b5 -54 6 Y JP WPA2(PSK/AES,TKIP/TKIP)
RADIX01 4c:e6:76:ae:00:1e -86 6 Y -- WPA(PSK/AES,TKIP/TKIP) WPA2(PSK/AES,TKIP/TKIP)
Eviry24G c0:25:a2:89:b2:72 -61 7 Y JP WPA2(PSK/AES/AES)
GW-office_G1 34:3d:c4:86:83:d0 -90 3 Y -- WPA2(PSK/AES/AES)
zoi 00:1a:eb:ac:51:30 -54 40 Y JP WPA2(PSK/AES/AES)

まぁ色々と出てきますが、今回注目なのはこの一番最後の行です:

zoi 00:1a:eb:ac:51:30 -54  40      Y  JP WPA2(PSK/AES/AES)

ここから、SSID「zoi」のBSSID(MACアドレス)が「00:1a:eb:ac:51:30」なこと、チャンネルが40chなことが読み取れますので、これをメモっておきます。

おっと、ここで質問をいただきました。

「zoiは上にもチャンネル9のやつがあるけど?」

良い質問です。おっしゃる通りです。いやまぁ今わたしが考えた質問なんですが。

チャンネルは13番までがいわゆる「2.4GHz」帯で、それ以降は「5GHz」帯になります。 2.4GHz帯の方をキャプチャしても、もちろん構いません。当たり前ですが、パスワードはどの周波数でも共有なので、クラックできればどちらも同じパスワードが得られます。ただ、これも当たり前ですが、ハンドシェイクが行われている場面をキャプチャできなければクラックには使えません。見てない家政婦は事件も解決できないってやつです。手元にあるどんな一般的なご家庭にもありそうな最近のスマホは5GHz帯をデフォルトで使うようになっているようでしたので、これからは40chだけに着目していきます。

で、次に、実際にキャプチャするコマンドがこちらになります(en0は内臓無線LANアダプタのデバイス名です):

% sudo /System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport en0 sniff 40
Password:
Capturing 802.11 frames on en0.
^CSession saved to /tmp/airportSniffGUDQbe.cap.

最後に表示されたのがキャプチャされた結果のファイルです。これをPyritに解析してもらう事になります。

ああ、一つ注意があります。これを使うとMacの無線LANが一切使えなくなって、ネットも使えなくなります。ダウロードとかsshの接続が切れても困らない時にやってください。あと、この理由があるので、MacBook Proだけではキャプチャはできません。スマホか何か他のデバイスが必要です。

あともう一つ注意としては、スマホの「設定」画面でWiFiをon-offしただけではハンドシェイクは行われません。再起動してください。今回は4回ほど再起動しました。…回数に「おまじない」以上の意味はありませんので、ハンドスピナー的に飽きるまで再起動してください。

これももう一つ注意ですが。tcpdumpはWPA2のハンドシェイクが終わった後のパケットしか見れません。つまり、クラックには利用できません。なんでかというと、それはそもそもtcpdumpのキャプチャするレイヤーがWPA2より一つ上なのと、tcpdumpの使ってBarkley Packet Filterの制限もあるような気がするのですが、OSXのカーネルに立ち入るのは今日は流石に勘弁しといてやりましょうぜ。でもフォーマットはtcpdumpの読み書きできるpcapフォーマットです。なんかちょっと不思議な気分。

WPA2-PSKで「おててつなぐ」ところを観察

さて。この無線LANルータ(厳密にはブリッジ)「zoi」は、WPA2-PSKによって隣のオフィスの人や、たまにやってくる「おらけんま」ことテイルゲーターから守られております。このWPA2-PSKを倒すには、まずこの敵のことをよく観察して理解しなければなりません。これはゲームの攻略だけでなく無線LANクラックにおいても同じです。たぶん。というわけで、さきほどキャプチャした実際のやりとりを眺めながら、WPA2-PSKでパスワード認証する仕組みを実際に見てみましょう。

すぐ上に書いた通り、キャプチャしたファイルはtcpdumpで読めますが、細かい情報に関しては表示してくれないようでしたので、GUIでパケットの情報が見れるWiresharkを使います。鮫好きにもやさしいブログなのです。嘘です。

ビーコン

WiFiに接続しようって時に、無線LANのルーターのリストがスマホに表示されますよね?そのためにルーターが虚空に向かって己の存在をシャウトしている様子がこちらです:

上でハイライトされてる行を見ると、SSID=zoiの「ビーコン」フレームであることや、「zoi」がアライド・テレシス製な事も書いてあります。だいたい1/10秒ごとに射出されているようなので、人間の時間スケール的には「常時叫びっぱなし」といったところでしょうか。ちなみに、さらにProbe-requestというメッセージをスマホから叫ぶと、「誰かいるか〜!?」と聞くことができます。機械にとっては、1/10秒っていうのは「永遠」なんでしょうね。

さて、中身を見ていきましょう。300バイトの中に密に詰まったたくさんの情報があって情報の大洪水に流されそうになりますが、淡々と情報を見ていくと「RSN Information」というところにAESと書いてあります。もっというと、PSKというのも下の方に書いてあります。ここで「zoiでつなぐ時はAESで暗号化してPSKで認証するんだぞい!」と叫んでいるわけです。秒間10回も。ええ、無線LANルータの仕事も大変なのですよ。

アソシエーション手続き

無線LANルータを見つけたら「繋がせていただけないでしょうか?」とまずは声を掛ける「アソシエーション手続き」が始まります。ただ、ここでは「繋ぎたいんですが」と申し出るだけです。玄関で言うならピンポンを押して「佐川急便でーす」という行為に近いでしょうか。

秒間10回ビーコンが叫んでいることもあって、これを手動で探すのは難しいです。そんな時は、上の「表示フィルタ」ってところに「wlan.fc.type_subtype eq 11」と打ち込んでみてください

Authentication Request

「佐川急便でーす」に相当するように、SONYのMACアドレス(スマホ)からアライド・テレシスのMACアドレス(zoi)へのメッセージである旨が記載されています。

Authenticationリクエストは、Authenticationリクエストといいつつ、単に名乗るだけです。パスワードに関係する内容をやりとりしたりはしません。もっというと、歴史的事情があって今は形骸化しているらしいです。まさに「あいさつ」ですね。

パスワードをクラックするのには関係ないので一切無視してもいいんですが、この辺も「そういえばそもそも無線LANってどうやって繋がってんだ!?」という見学気分で流れるように見ていきまます。こういう経験の積み重ねがいざという時のネットワークデバッグに…役立つといいね。

Association Request

形骸化したAuthenticationリクエストの代わりに色々な情報が詰まっているのが、Associationリクエストです。パケットを眺めてみると、図のようにデータレートが書かれています。「zoi」は54MBit/secまでしか対応してないらしいです。知らなかった…。

ここには接続するために必要な細々とした情報はたくさん乗っていますが、パスワードとか認証に関係する情報は乗っていません。なので、これも流していきますよ。

Keyメッセージ

次に続く「Key」メッセージがパスワードで認証するために使われるメッセージです。4つあるので、「4-way handshake」と言います。玄関の例えでいうなら、窓から見える佐川の制服、声(先週と同じ人だ!とか)、手元にある再配達表なんかを見て、本当にドアの向こうにいるのが佐川のお兄さんであって、全然関係ない消火器を売る人とかでないことを確信してドアを開けるためのやりとりです。

今はざっくり見ていただくだけで良いのですが、「パスワード」を直接送っているわけではないことを観察しましょう。当たり前です。パスワード、つまり「あいことば」を直接喋ってしまったら、こうやってずっと「聞いて」いれば、すぐ「あいことば」がバレてしまいます。もっと「ふしぎな方法」で、こっそり、ひっそり、でも堂々と、こうやってやりとりしています。

これに関しては今後の記事でもっとくわしく書く予定ですが、お互いに事前に共有したパスワード(PSK)を共有している同士で、この”Nonce”とか”IV”とかなんとかついてる、4つのメッセージを使って「ある計算」をすれば、「あいことば」を直接喋らなくとも、「我々は同じ合言葉を共有しあった仲間だ!」と確信しあえる…らしいことは覚えておきましょう。まぁもっとも、我々はこれからその「連帯感」「信頼感」を全力でぶち壊す側に回るわけですが。ふふっ、いい気味だ。

Pyritをビルドする

ここから話は手元のMacBook Proから、リモートのGPUやCPUのいっぱい積まれたUbuntuサーバへと話が変わります。

次はPyritのビルドです。無線LANクラック道は、長く険しい坂なのです。打ち切りにならない程度に頑張っていきましょう。

Pyritは実はビルドなんかしなくてもaptで入ります。

$ apt search pyrit
ソート中... 完了
全文検索... 完了
pyrit/xenial 0.4.0-5 amd64
  GPGPU-driven WPA/WPA2-PSK key cracker

pyrit-opencl/xenial 0.4.0-1 amd64
  OpenCL extension module for Pyrit

pyrite-publisher/xenial 2.1.1-11 amd64
  Convert html and text documents to palm DOCformat

wifite/xenial,xenial 2.0r85-1 all
  Python script to automate wireless auditing using aircrack-ng tools

が、見てもらえばわかるとおり、OpenCLバージョンは入ってもCUDAバージョンは入らないのと、これから改造して遊ぼう〜とか考えるとビルドする方がいいので、ビルドしていきます。

必要なライブラリのインストール

粛々とインストールしましょう。環境によっては足りないやつとかもあるかもしれませんが、その辺は柔軟にお願いします:

sudo apt install clang libpcap-dev openssl-dev python-virtualenv libpython-all-dev

ソースのclone

今後改造することもあるかもしれないのでわたしの個人リポジトリにforkしてしまいましたので、そこからcloneしてください。

git clone https://github.com/ledyba/Pyrit
cd Pyrit

CPU版のビルド

CPUだけで解析するやつを一旦ビルドしましょう。

そのための呪文は次に置いておくので唱えてください。

# venv環境を作って有効にします。Pyritは2.7じゃないと動かない…。
$ virtualenv .env --python=/usr/bin/python2.7
$ . .env/bin/activate

# Pythonからcapファイルを見るためのライブラリです。
# 古い2.3.3でないと動かないので、バージョンを指定してインストール。
$ pip install "scapy==2.3.3"

# ビルド。
$ python setup.py build

# 己の作ったvenv環境にいることを確認(しないとたまに間違えるので…)
$ which python
/home/psi/src/Pyrit/.env/bin/python

# インストール
$ python setup.py install

…「ビルド方法」って出てくる結果は単純なのにどうしてこうも試行錯誤が必要なんだろう?

ちなみに上記の呪文でMacでもビルドして実行できるはずなのですが、「scapyが見つからない」という謎のエラーメッセージがでるのでMacの人は注意してください(うそつけちゃんとバージョン指定して入れたやんけ)。わたしは今日のところは諦めました。

なにはなくともビルドできたはずですので、次の呪文を唱えてCPUが48個あることを確認します。

$ pyrit list_cores
Pyrit 0.5.1 (C) 2008-2011 Lukas Lueg - 2015 John Mora
https://github.com/JPaulMora/Pyrit
This code is distributed under the GNU General Public License v3+

The following cores seem available...
#1:  'CPU-Core (SSE2/AES)'
#2:  'CPU-Core (SSE2/AES)'
#3:  'CPU-Core (SSE2/AES)'
#4:  'CPU-Core (SSE2/AES)'
#5:  'CPU-Core (SSE2/AES)'
#6:  'CPU-Core (SSE2/AES)'
#7:  'CPU-Core (SSE2/AES)'
#8:  'CPU-Core (SSE2/AES)'
#9:  'CPU-Core (SSE2/AES)'
#10:  'CPU-Core (SSE2/AES)'
#11:  'CPU-Core (SSE2/AES)'
#12:  'CPU-Core (SSE2/AES)'
#13:  'CPU-Core (SSE2/AES)'
#14:  'CPU-Core (SSE2/AES)'
#15:  'CPU-Core (SSE2/AES)'
#16:  'CPU-Core (SSE2/AES)'
#17:  'CPU-Core (SSE2/AES)'
#18:  'CPU-Core (SSE2/AES)'
#19:  'CPU-Core (SSE2/AES)'
#20:  'CPU-Core (SSE2/AES)'
#21:  'CPU-Core (SSE2/AES)'
#22:  'CPU-Core (SSE2/AES)'
#23:  'CPU-Core (SSE2/AES)'
#24:  'CPU-Core (SSE2/AES)'
#25:  'CPU-Core (SSE2/AES)'
#26:  'CPU-Core (SSE2/AES)'
#27:  'CPU-Core (SSE2/AES)'
#28:  'CPU-Core (SSE2/AES)'
#29:  'CPU-Core (SSE2/AES)'
#30:  'CPU-Core (SSE2/AES)'
#31:  'CPU-Core (SSE2/AES)'
#32:  'CPU-Core (SSE2/AES)'
#33:  'CPU-Core (SSE2/AES)'
#34:  'CPU-Core (SSE2/AES)'
#35:  'CPU-Core (SSE2/AES)'
#36:  'CPU-Core (SSE2/AES)'
#37:  'CPU-Core (SSE2/AES)'
#38:  'CPU-Core (SSE2/AES)'
#39:  'CPU-Core (SSE2/AES)'
#40:  'CPU-Core (SSE2/AES)'
#41:  'CPU-Core (SSE2/AES)'
#42:  'CPU-Core (SSE2/AES)'
#43:  'CPU-Core (SSE2/AES)'
#44:  'CPU-Core (SSE2/AES)'
#45:  'CPU-Core (SSE2/AES)'
#46:  'CPU-Core (SSE2/AES)'
#47:  'CPU-Core (SSE2/AES)'
#48:  'CPU-Core (SSE2/AES)'

うーん。壮観かな。

PyritはGPGPUで解析できるのがウリなのですが、CPUの時もご覧の通り、SSE2とAES系の命令を使ってアクセラレーションしてくれるらしいです。

GPU版のビルド

CUDA版とOpenCL版があるのですが、今日はCUDA版をインストールします。

$ cd modules/cpyrit_cuda/
$ python build install

手元の環境では一発でうまくいったのでよかったのですが、うまく行かなかった人は、この何語かもよくわからない本家のIssueを参考に頑張ってください。コマンドが読めればきっとなんとかなる。はず。

GPU付きで実行する

以上でインストールは完了ですが、設定ファイルでCUDAをONにしないとCUDAは有効になりません

$ cat ~/.pyrit/config
default_storage = file://
limit_ncpus = 0
rpc_announce = true
rpc_announce_broadcast = false
rpc_knownclients =
rpc_server = false
use_CUDA = true
use_OpenCL = false
workunit_size = 75000

CUDAを使うために、use_CUDA = trueとしてください。

virtualenv使ってるのに、これだけ$HOME/.pyritにあるのすごい変な感じがするんですが、まぁ、置いておきましょう。

以上で有効にしたら、以前と同じコマンドを打った時にCUDAデバイスが表示されるようになるはずです:

$ pyrit list_cores
Pyrit 0.5.1 (C) 2008-2011 Lukas Lueg - 2015 John Mora
https://github.com/JPaulMora/Pyrit
This code is distributed under the GNU General Public License v3+

The following cores seem available...
#1:  'CPU-Core (SSE2/AES)'
(略)
#48:  'CPU-Core (SSE2/AES)'

The following CUDA GPUs seem aviable...
#1:  'CUDA-Device #1 'Tesla V100-PCIE-16GB''
#2:  'CUDA-Device #2 'Tesla V100-PCIE-16GB''
#3:  'CUDA-Device #3 'GeForce GTX 1080 Ti''
#4:  'CUDA-Device #4 'GeForce GTX 1080 Ti''

いぇーい。

実測する前にベンチマークをする

ここまでだいぶ長かったので、いちばん美味しい解析はまた今度にとっておくことにして、Pyrit付属の「ベンチマーク」を実行して遊んで終わりとしましょう。…美味しいものを最初に食べる派のみなさん、すいません。

$ pyrit benchmark
Pyrit 0.5.1 (C) 2008-2011 Lukas Lueg - 2015 John Mora
https://github.com/JPaulMora/Pyrit
This code is distributed under the GNU General Public License v3+

Running benchmark (134716.6 PMKs/s)... /

Computed 134716.61 PMKs/s total.
#1: 'CPU-Core (SSE2/AES)': 558.2 PMKs/s (RTT 2.8)
#2: 'CPU-Core (SSE2/AES)': 525.5 PMKs/s (RTT 3.1)
#3: 'CPU-Core (SSE2/AES)': 576.1 PMKs/s (RTT 2.8)
#4: 'CPU-Core (SSE2/AES)': 582.1 PMKs/s (RTT 2.9)
#5: 'CPU-Core (SSE2/AES)': 516.2 PMKs/s (RTT 2.9)
#6: 'CPU-Core (SSE2/AES)': 575.0 PMKs/s (RTT 2.9)
#7: 'CPU-Core (SSE2/AES)': 635.2 PMKs/s (RTT 2.8)
#8: 'CPU-Core (SSE2/AES)': 513.1 PMKs/s (RTT 2.9)
#9: 'CPU-Core (SSE2/AES)': 500.3 PMKs/s (RTT 2.8)
#10: 'CPU-Core (SSE2/AES)': 546.0 PMKs/s (RTT 2.7)
#11: 'CPU-Core (SSE2/AES)': 502.4 PMKs/s (RTT 3.1)
#12: 'CPU-Core (SSE2/AES)': 549.2 PMKs/s (RTT 3.1)
#13: 'CPU-Core (SSE2/AES)': 588.1 PMKs/s (RTT 3.0)
#14: 'CPU-Core (SSE2/AES)': 487.7 PMKs/s (RTT 2.9)
#15: 'CPU-Core (SSE2/AES)': 612.9 PMKs/s (RTT 2.9)
#16: 'CPU-Core (SSE2/AES)': 615.7 PMKs/s (RTT 2.7)
#17: 'CPU-Core (SSE2/AES)': 548.0 PMKs/s (RTT 2.9)
#18: 'CPU-Core (SSE2/AES)': 572.3 PMKs/s (RTT 2.9)
#19: 'CPU-Core (SSE2/AES)': 568.8 PMKs/s (RTT 2.9)
#20: 'CPU-Core (SSE2/AES)': 589.1 PMKs/s (RTT 2.8)
#21: 'CPU-Core (SSE2/AES)': 546.5 PMKs/s (RTT 2.9)
#22: 'CPU-Core (SSE2/AES)': 570.1 PMKs/s (RTT 2.9)
#23: 'CPU-Core (SSE2/AES)': 552.7 PMKs/s (RTT 3.1)
#24: 'CPU-Core (SSE2/AES)': 571.3 PMKs/s (RTT 3.0)
#25: 'CPU-Core (SSE2/AES)': 518.7 PMKs/s (RTT 2.8)
#26: 'CPU-Core (SSE2/AES)': 544.2 PMKs/s (RTT 2.8)
#27: 'CPU-Core (SSE2/AES)': 560.4 PMKs/s (RTT 2.8)
#28: 'CPU-Core (SSE2/AES)': 569.5 PMKs/s (RTT 2.9)
#29: 'CPU-Core (SSE2/AES)': 566.7 PMKs/s (RTT 3.0)
#30: 'CPU-Core (SSE2/AES)': 552.7 PMKs/s (RTT 2.9)
#31: 'CPU-Core (SSE2/AES)': 518.4 PMKs/s (RTT 3.0)
#32: 'CPU-Core (SSE2/AES)': 613.1 PMKs/s (RTT 3.1)
#33: 'CPU-Core (SSE2/AES)': 597.8 PMKs/s (RTT 2.6)
#34: 'CPU-Core (SSE2/AES)': 572.2 PMKs/s (RTT 3.1)
#35: 'CPU-Core (SSE2/AES)': 577.3 PMKs/s (RTT 2.9)
#36: 'CPU-Core (SSE2/AES)': 597.6 PMKs/s (RTT 3.0)
#37: 'CPU-Core (SSE2/AES)': 558.5 PMKs/s (RTT 3.0)
#38: 'CPU-Core (SSE2/AES)': 560.2 PMKs/s (RTT 3.0)
#39: 'CPU-Core (SSE2/AES)': 523.3 PMKs/s (RTT 2.8)
#40: 'CPU-Core (SSE2/AES)': 566.2 PMKs/s (RTT 3.0)
#41: 'CPU-Core (SSE2/AES)': 594.0 PMKs/s (RTT 2.8)
#42: 'CPU-Core (SSE2/AES)': 544.6 PMKs/s (RTT 3.0)
#43: 'CPU-Core (SSE2/AES)': 512.6 PMKs/s (RTT 3.0)
#44: 'CPU-Core (SSE2/AES)': 558.7 PMKs/s (RTT 3.2)
#45: 'CPU-Core (SSE2/AES)': 596.8 PMKs/s (RTT 2.9)
#46: 'CPU-Core (SSE2/AES)': 579.3 PMKs/s (RTT 2.8)
#47: 'CPU-Core (SSE2/AES)': 562.6 PMKs/s (RTT 2.8)
#48: 'CPU-Core (SSE2/AES)': 605.8 PMKs/s (RTT 3.1)
CUDA:
#1: 'CUDA-Device #1 'Tesla V100-PCIE-16GB'': 125707.5 PMKs/s (RTT 0.4)
#2: 'CUDA-Device #2 'Tesla V100-PCIE-16GB'': 118158.6 PMKs/s (RTT 0.4)
#3: 'CUDA-Device #3 'GeForce GTX 1080 Ti'': 113215.9 PMKs/s (RTT 0.4)
#4: 'CUDA-Device #4 'GeForce GTX 1080 Ti'': 97661.2 PMKs/s (RTT 0.4)

PMKs/sっていうのが、「パスワードを一秒間に何回試したか」です。Pyritは、パスワードを総当たり(ブルートフォース)でクラックします。銀行の暗証番号とかロッカーや自転車の鍵で例えると、0000から9999まで全部ためす感じです。アホな戦略を計算力で全力でごり押していくのが、Pyritスタイル。たぶん。

じっと数字を眺めていると、こんなところに気が付きます:

  • CPU48コアを全部束るとだいたい3万パスワード/secで、12万パスワード/secのTesla1台の1/4くらい

まぁCPUはかなり頑張った方なんじゃないでしょうか?「みんなで同じこと(クラック)をデータ(パスワード)だけ差し替えて計算する」のはGPUのもっとも得意とするところなので、健闘していると思います。

  • GTXとTeslaでほとんど差がない

これ結構ショックなんですが。Teslaはとってもたかいんだぞー。ただ、ひょっとすると、これは「最適化のしがいがある」と捉える事も可能かもしれません。Teslaの方が潜在的な能力があるのは確かなはずなので、ここまで差がないのは、なんか違和感があります。もう少し詳しくプロファイリングする甲斐があるというものでしょう。技術ブログってやつにはこういうのが必要なんですよ。たぶん。

無論、単純にベンチマークが実態と乖離してるだけで、実データを使うとそうでもない可能性もあります。

  • このサーバが全力を出すと、zoiのパスワード([a-z]が11文字)を総当たりクラックするのに200年くらい掛かる

まぁ200年も業務データがクラックされなければ十分なんじゃないでしょうか。誰もクラックされたデータがなんだったのか、思い出せなくなってる頃だと思います。

ただPyritも一番アホな「総当たり攻撃」だけではなく「辞書攻撃」もするので、場合によってはすぐクラックできる可能性はあります。

ああ。あと。最初の方に出てる「Computed 134716.61 PMKs/s total.」ってのがなんか変な気がするので、次までにその辺はデバッグしておきますね。

<Youtuber語法>ご静聴、ありがとう!
気に入ってくれたみんなはRSSリーダーに登録したり↓↓のソーシャルボタンとかで拡散してね</Youtuber語法>

機械学習の入門編 とりあえずライブラリを使ってデータを分類してみる

今回は前回設定した環境を使って、何らかのデータを分類する方法を書きます。

とりあえず機械学習に触ってみたい人向けにPythonプログラムを書いて結果が出るまでを解説します。アルゴリズムの詳細やハイパーパラメータの意味は専門書を読んだほうがいいので書きません。(間違っていたら怖いし)プログラムの全文は最後に書いてます。

今回扱う問題

今回は教師あり学習の中の分類問題を扱います。教師あり学習は、すでに知っている入力Xと出力Yの情報からいい感じのXとYの関係を推定して、新しい入力X’が来た時にその関係性を使って正しいY’を求めることができるプログラム[学習器]を生成することが目標になります。画像に何が写ってるかを当てるみたいな問題を分類問題、立地とか家の素材、周辺地域の平均年収とかから家の値段を推定するみたいな問題を回帰問題といいます。今回扱うのは分類問題です。でも画像の分類みたいな楽しそうなやつではなく地味なデータでやります。

-SVMを使って分類してみる-

データ例はこれです。

この点を生成するスクリプトはこちら。

    SIZE = 1000
d = [[0,0,0,0,0,0,0,0,1,0],
[0,1,2,2,2,2,2,2,1,0],
[0,1,2,0,0,0,0,0,1,0],
[0,1,2,0,2,2,2,0,1,0],
[0,1,2,0,0,0,2,0,1,0],
[0,1,2,2,2,2,2,0,1,0],
[0,1,1,1,1,1,1,0,1,0],
[0,1,0,0,0,0,0,0,1,0],
[0,1,0,0,0,0,0,0,1,0],
[0,1,1,1,1,1,1,1,1,0]]
x = []
y = []
ud = np.random.rand #alias
for _ in range(SIZE):
a = ud()*10
b = ud()*10
x.append([a, b])
y.append(d[int(a)][int(b)])
x = np.array(x)
x = np.reshape(x,(-1,2))
y = np.array(y)

1000点生成して訓練用、テスト用にそれぞれ500点ずつ分割します。

ここで、numpyのappendはpythonネイティブの[].appendと比べて滅茶苦茶遅いので使わないようにします。

本来はもう一つ本当のテスト用にデータを分割しないといけないんですが、ややこしいので今回はしません。

今回の機械学習の目標は1つ1つの点が3つのうちどのクラスから生成されたかを推定することです。

Xとyの具体的な数値はこのようになっています。例えばXとyの最初の要素は[12.13, 7.37]の座標に点があってそれは1の領域にあることを表しています。

さっそくですが問題を解くためのアルゴリズムを適当に決めてみましょう。それらはディープラーニングを含めてたくさんの種類があります。一般的なものを使用せる場合はライブラリが存在しているので自分でコードを書く必要はありません。

今回はSVMとニューラルネット(階層が深くなるとディープラーニング)をそれぞれ実装してみます。

各アルゴリズムが何をしてるのかは、ここではとても説明できないので必要に応じてパターン学習と機械学習(PRML)とかの参考書で。

まずscikit-learnライブラリを用いてSVMを実装します。scikit-learnにはSVCとSVRの関数がありますが、今回は分類問題なのでSVCを使用します。回帰問題の場合はSVRです。

from sklearn.svm import SVC
clf = SVC(C=30, kernel="rbf", gamma=0.01,class_weight = "balanced")
clf.fit(X_train, y_train)
y_predict = clf.predict(X_test)

このy_predictに予測したラベルが入っています。

SVCにはハイパーパラメータとしてC,カーネル,ガンマが存在します。これらの変数は自動的に決まるものではなく自分で与えてあげるもので、SVMに限らずほぼすべてのアルゴリズムに存在します。

これらの最適値はデータごとに変わるので、通常はグリッドサーチという作業を行い最適なものを選択します。C, ガンマ, カーネルに様々な値を入力したときの一例は以下の通りです。

linearカーネルは境界線が直線なのに対してrbfカーネルは境界線が曲線なのが分かります。またrbfカーネルのガンマが大きいほうがより境界線が複雑になっています。Cは各領域内にノイズが含まれないので300のほうが結果がよくなるようです。

-ニューラルネットも使ってみる-

先ほどはSVMを用いてデータを分類してみました。次はニューラルネットを用いて分類してみます。

データセットは全く同じです。

Kerasを使ってニューラルネットもモデルを作ります。今回は入力データが2次元、分類するクラスは3クラスなので間に隠れ層としてノード数が5のモデルは以下のスクリプトで作ることができます。

from keras.layers import Input, Dense, Activation, Dropout
from keras.models import Model, Sequential
from keras.wrappers.scikit_learn import KerasClassifier

def make_model():
model = Sequential()
model.add(Dense(5, input_dim=2, activation='relu'))
model.add(Dense(3, activation='softmax'))
adam = optimizers.Adam(lr = 0.001, decay = 0)
model.compile(loss='sparse_categorical_crossentropy',
optimizer=adam,
metrics=['accuracy'],
)
return model

X_train = X_train.reshape((len(X_train), np.prod(X_train.shape[1:])))
y_train = np.reshape(y_train, (np.shape(X_train)[0],1))
clf = KerasClassifier(make_model, batch_size=100)
clf.fit(X_train, y_train, epochs=10000, validation_data=(X_test, y_test),)

このプログラムで入力層のノード数が2,隠れ層のノード数が5,出力層で3クラスの分類を行うモデルが作られます。表にするとこんなモデルです。

早速ですが結果を見てみましょう。

ぜんぜんですね。それでは次は隠れ層をものすごく増やして同じことをやってみましょう。

def make_model():
model = Sequential()
model.add(Dense(200, input_dim=2, activation='relu'))
model.add(Dense(200, activation='relu'))
model.add(Dense(200, activation='relu'))
model.add(Dense(200, activation='relu'))
model.add(Dense(200, activation='relu'))
model.add(Dense(200, activation='relu'))
model.add(Dense(200, activation='relu'))
model.add(Dense(3, activation='softmax'))
adam = optimizers.Adam(lr = 0.001, decay = 0)
model.compile(loss='sparse_categorical_crossentropy',
optimizer=adam,
metrics=['accuracy'],
)
return model

結果はこうなりました。

先ほどよりはよくなりましたが、もっと良くするにはどうすればいいでしょうか。

原因を探るためにAccuracyとLossのepochごとの経過を見てみましょう。

ニューラルネットでは各ノードの適切な重みを計算するために何回も訓練データを使って学習します。

その回数がepoch数で,訓練データとテストデータそれぞれ見ます。

まずは層の少ないほう。

trainのAccuracyがそもそも低いのでネットワークの表現力が足りないことがわかります。

つまりノード数が少なすぎるということです。

次にノードの多いほうを見てみましょう。

trainのほうはaccuracy,lossともに回数を重ねるにつれて改善されてますが、testのほうは悪化しています。これは典型的な過学習の症状です。これを解消するにはノードを減らす、Dropoutやepoch数を減らす、trainデータを増やすことが考えられます。

実際Dropoutを行うコードを追加すると。まだ過学習気味ですが、結果はよくなります。

def make_model():
model = Sequential()
model.add(Dense(200, input_dim=2, activation='relu'))
model.add(Dense(200, activation='relu'))
model.add(Dense(200, activation='relu'))
model.add(Dense(200, activation='relu'))
model.add(Dense(200, activation='relu'))
model.add(Dense(200, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(200, activation='relu'))
model.add(Dense(3, activation='softmax'))
adam = optimizers.Adam(lr = 0.001, decay = 0)
model.compile(loss='sparse_categorical_crossentropy',
optimizer=adam,
metrics=['accuracy'],
)
return model

結果はこうなりました。

緑の領域の左側で改善が見られます。

また、入力データ数を50倍にするとより正確な結果が出ます。

隠れ層の数を増やしていくとニューラルネットの表現力は増加しますが、計算にかかる時間は増加します。教師データ数25000,ノード数200の隠れ層の数が1から10の時の所要時間は前回使用したGPUサーバとノートパソコンでそれぞれ計測すると次のようになりました。一応スペックをもう一回。

手元のノートパソコン(ideapad 720S)GPUサーバー
CPUCore i7 8550U 1.8Ghz 4コア8スレッドXeon Silver 4116 × 2 2.1Ghz 24コア48スレッド
メモリ8GB128GB
GPUなしTesla V100 × 2 + 1080Ti × 2
OSUbuntu 16.04 DesktopUbuntu 16.04 Server

層が増えるにしたがって時間が増加していますが、増加率はノートパソコンのほうが多い結果になりました。CNNの時はもっと差があったのでモデルが複雑になるほど差が出るのかも?

今回はこれで終了します。本当はここからどのアルゴリズムを使うかや、適切なハイパーパラメータをチューニング、データの前処理をして正しい方法で評価し、一番使えるものを探さなければなりません。

まとめ

今回は本当に使ってみただけで雰囲気程度でした。

次回からは実際の業務に使えそうなものを作ることに挑戦しようと思います。

プログラム全文

import numpy as np
from matplotlib import pyplot as plt
import joblib

def make_data(SIZE):

d = [[0,0,0,0,0,0,0,0,1,0],
[0,1,2,2,2,2,2,2,1,0],
[0,1,2,0,0,0,0,0,1,0],
[0,1,2,0,2,2,2,0,1,0],
[0,1,2,0,0,0,2,0,1,0],
[0,1,2,2,2,2,2,0,1,0],
[0,1,1,1,1,1,1,0,1,0],
[0,1,0,0,0,0,0,0,1,0],
[0,1,0,0,0,0,0,0,1,0],
[0,1,1,1,1,1,1,1,1,0]]


x = []
y = []
ud = np.random.rand #alias
for _ in range(SIZE):
a = ud()*10
b = ud()*10
x.append([a, b])
y.append(d[int(a)][int(b)])
x = np.array(x)
x = np.reshape(x,(-1,2))
y = np.array(y)
joblib.dump(x,"mx.pkl")
joblib.dump(y,"my.pkl")

def main():
x = joblib.load("mx.pkl")
y = joblib.load("my.pkl")
dx = x
#データの一部を表示
fig = plt.figure()
ax = fig.add_subplot(111)
colors = ["#ff0000", "#00ff00", "#0000ff"]
print("Drawing Images")
if len(dx) > 3000:
px = dx[0:3000]
else:
px = dx
for i ,v in enumerate(px):
ax.scatter(v[0], v[1], c=colors[int(y[i])-1], marker='o', alpha = 0.3)
ax.set_title('Dataset')
print("Finish")
plt.show()
plt.close()

from sklearn.svm import SVC
from sklearn.metrics import accuracy_score
from sklearn.metrics import f1_score
from sklearn.model_selection import train_test_split

seed = 12345
(X_train, X_test, y_train, y_test) = train_test_split(x, y, test_size=0.50, random_state=seed)
for k in ["rbf", "linear"]:
for c in [30,300]:
for g in [0.01, 1]:
clf = SVC(C=c, kernel=k, gamma=g,class_weight = "balanced")
clf.fit(X_train, y_train)
y_predict = clf.predict(X_test)
ac = accuracy_score(y_test, y_predict)
f1 = f1_score(y_test, y_predict, average="macro")
fig = plt.figure()
ax = fig.add_subplot(111)
if len(X_test) > 3000:
px = X_test[0:3000]
else:
px = X_test
colors = ["#ff8800", "#00ff88", "#8800ff"]
print("Data plotting")
for i ,v in enumerate(px):
ax.scatter(v[0], v[1], c=colors[int(y_predict[i])-1], marker='o', alpha = 0.3)
ax.set_title('')
print("Finish")
print([k,c,g])
plt.show()
plt.close()
np.set_printoptions(suppress=True)
np.set_printoptions(threshold=np.inf, precision=2, floatmode='maxprec')
print(ac)
print(f1)

from keras.layers import Input, Dense, Activation, Dropout
from keras.models import Model, Sequential
from keras.wrappers.scikit_learn import KerasClassifier
from keras import optimizers

layers = 0

def make_model():
model = Sequential()
model.add(Dense(200, input_dim=2, activation='relu'))
for _ in range(layers):
model.add(Dense(200, activation='relu'))
model.add(Dense(3, activation='softmax'))
adam = optimizers.Adam(lr = 0.001, decay = 0)
model.compile(loss='sparse_categorical_crossentropy',
optimizer=adam,
metrics=['accuracy'],
)

#plot_model(model, to_file='model.png', show_shapes=True)
return model

X_train = X_train.reshape((len(X_train), np.prod(X_train.shape[1:])))
y_train = np.reshape(y_train, (np.shape(X_train)[0],1))

for i in range(10):
layers = i
clf = KerasClassifier(make_model, batch_size=100)
history = clf.fit(X_train, y_train, epochs=100, verbose = 0, validation_data=(X_test, y_test))

np.set_printoptions(suppress=True)
np.set_printoptions(threshold=np.inf, precision=2, floatmode='maxprec')
ac = accuracy_score(y_test, y_predict)
f1 = f1_score(y_test, y_predict, average="macro")
print(ac)
print(f1)

# summarize history for accuracy
plt.plot(history.history['acc'])
plt.plot(history.history['val_acc'])
plt.title('model accuracy')
plt.ylabel('accuracy')
plt.xlabel('epoch')
plt.legend(['train', 'test'], loc='upper left')
plt.show()
# summarize history for loss
plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.title('model loss')
plt.ylabel('loss')
plt.xlabel('epoch')
plt.legend(['train', 'test'], loc='upper left')
plt.show()

y_predict = clf.predict(X_test)

fig = plt.figure()
ax = fig.add_subplot(111)
if len(X_test) > 3000:
px = X_test[0:3000]
else:
px = X_test
colors = ["#ff8800", "#00ff88", "#8800ff"]
for i ,v in enumerate(px):
ax.scatter(v[0], v[1], c=colors[int(y_predict[i])-1], marker='o', alpha = 0.3)
ax.set_title('Test plot')
print("Finish")
plt.show()
plt.close()


if __name__ =="__main__":
make_data(50000)
main()

CPUとGPUの比較

はじめまして、Link-Uの山田です。

社内ではサーバーなど、ハードウェアの調達担当です。

GPUサーバーはお高い買い物になりがちですので、GPUサーバーを買う理由を考えてみましょう。

*社内への言い訳記事ではありません、多分。

まずは性能を比較してみましょう

とりあえず、CPUメーカーの長たるIntelの最上位モデルとNVIDIAのゲーム向け、サーバー向けの最上位モデルの浮動小数点演算能力を比較してみましょう。

Xeon Platinum 8180GeForce 1080TiTesla V100
倍精度(64bit)1.12 TFLOPS0.33TFLOPS6.38TFLOPS
単精度(32bit)2.24 TFLOPS10.61TFLOPS12.75TFLOPS
半精度(16bit)2.24 TFLOPS10.61TFLOPS25.5TFLOPS
消費電力205W250W250W
お値段$10009$699$8000~$9000

*この他V100にはTensor Coreという行列演算のアクセラレーターもあります。
*定格の能力で比較しています。実際には温度等の環境によって性能が上下します。

やはり最上位というだけあってお値段は中々ですね。特にXeonの場合はモデルのバリエーションも多いので、最上位モデルを買う意味はあまりないかと思います。

各種演算の使い道

倍精度浮動小数点演算

主に科学技術計算、シミュレーション等の昔ながらの伝統的なエンタープライズ用途での計算に用います。

ゲーム用途のGeForceではバッサリ切られています。

完全に切らなかったのは、一応エンタープライズのGPU向けに書いたプログラムが「動かないわけではない」状態にしたかったからではないかと思います。

単精度浮動小数点演算

一般的に利用する浮動小数点の命令になります。ゲームも単精度浮動小数点演算になります。

ゲーム用途になった時のGeForceのパフォーマンスの良さが光ります。

もっともこんな話もあり、GeForceを商用利用するには注意が必要です。

半精度浮動小数点演算

最近までは注目されていませんでしたが、Deep Learningでよく使います。

Deep Learningは精度があまり必要なく、とにかく計算量をこなしたいので、使われます。

最近は半精度に飽き足らず、半々精度の活用まで行われつつあります。

Xeon、GeForceは特に半精度のことを意識していないので、単精度と同じになっています。

というか半精度であっても単精度で演算してるだけですね。

一方でTeslaは半精度をうまく扱えるようになっており、きれいに単精度の倍出るようになっています。

これはTeslaの大きなメリットであると言えます。

CPUとGPUの構造の比較

CPUは科学技術計算や機械学習、ゲームの他にもさまざまなアプリケーションを動作させます。

その中には処理を複数のコアに分散しにくいものや、複雑な条件分岐を含むものもあります。

それらを処理するため、CPUはコア1つ1つが極めて強力かつ巨大にできています。

逆にGPUはCPUに比べて単純な計算機が大量に載せられています。

さきほど出した浮動小数点演算の能力の計算式ですが、

コア数 × クロック(コアの動作速度) × IPC(クロック当たりの命令実行数)

で計算できます。この計算式に従ってGeForceとTeslaを比較してみましょう。

Xeon Platinum 8180GeForce 1080TiTesla V100
コア数2835845120
クロック2.5GHz1.48GHz1.245Ghz
IPC3222
単精度演算能力2.24TFLOPS10.61TFLOPS12.75TFLOPS

Tesla V100には1枚のカードになんと5120コアも載っています。一方でIPCが高くなかったり、クロックがCPUに比べると抑えられていたりするのもわかります。

CPUでGPUに対抗してみる

Xeon Platinumは8 CPUの構成まで取ることができます。8個搭載した場合の演算能力は

2.24TFLOPS × 8 = 17.92TFLOPS

これはTesla V100の12.75TFLOPSを超えます。

こう考えてみると、消費電力の問題さえ考えなければ、8 CPUのサーバーを持っている人はGPUを買わないでCPUで押し切るというのも1つの選択肢であることがわかります。

勿論、既存のサーバーのない環境下でDeep LearningをしたいのであればGPUを買ったほうがいいことは言うまでもありません。
そしてデータセンターに設置するのでなければGeForceのほうがコストパフォーマンスが良いこともまた、言うまでもありません。

*もっとも、Teslaの方が長時間稼働を前提とした設計となっているので、Deep Learningを長時間回すのであればTeslaも選択肢に上がると思います。

消費電力とラック

実際にGPUサーバーを購入する場合、2Uの筐体に8枚までGPUを挿せるサーバーが発売されています。

このサーバーを弊社の契約しているラックに入れることを考えてみましょう。

まず、8 GPUサーバーの消費電力は、

250W(GPU) × 8 + 150W(CPU) × 2 + その他 + 予備

で、大体3000Wぐらい見ておけば大丈夫だと思います。

Link-Uの契約しているラックは42U(フルラック)で4KVA ≒ 4000Wです。

このラックに何台収容できるかというと・・・

1台ですね!

何ということでしょう。42Uのラックに2Uのサーバーというさみしい光景が浮かびます。

いやいやいや、データセンターはお金を払えば契約電力を引き上げることができます。

Link-Uの利用しているデータセンターはお金さえ払えば6KVA≒6000Wまで引き上げることができます。

お金を払えば何台まで引き上げれるかというと・・・

2台ですね!

何ということでしょう。やはり42Uのラックに2Uのサーバーが2台というさみしい光景が浮かびます。

ちなみに弊社の利用しているデータセンターは2009年竣工のデータセンターで、

特に新しいわけではありませんが、特に古いわけでもありません。

日本のデータセンターは電力供給能力、冷却能力が低いため、GPUを大量に搭載したサーバーは場所に余裕があっても設置できないことがあります。

まとめ

非商業用途でDeep Learningを行うのであればGeForceのコスパが良いですが、利用制限があったり絶対的な性能はTeslaに劣ったりします。

いざGPUサーバーを購入する際は電力供給能力、冷却能力に気を付けましょう。

[初心者用]Pythonの環境構築とSSHとCUDAを使ってGPUサーバーで機械学習をする方法

こんにちは、はじめまして。Link-Uの町屋敷です。

技術ブログを立ち上げるとのことで、ちょうど機械学習の環境を整える必要があったのでその詳細を書きます。環境構築って初投稿っぽいですし。

次にやる人のためのときのメモも兼ねてるからできるだけ色んなサイトに行かなくていいようにしたら結構長くなった。どこか間違ってたらごめんなさい。

折角なので最後に手元のノートパソコンとGPUサーバーの両方で実行して手書き文字認識のベンチマークを取ってみます。

環境

手元のノートパソコン(ideapad 720S)GPUサーバー
CPUCore i7 8550U 1.8Ghz 4コア8スレッドXeon Silver 4116 × 2 2.1Ghz 24コア48スレッド
メモリ8GB128GB
GPUなしTesla V100 × 2 + 1080Ti × 2
OSUbuntu 16.04 DesktopUbuntu 16.04 Server

Pythonを使う準備

今回はIDEとしてEclipse、環境構築用にAnacondaを使います。PythonのIDEでEclipse使う人は少数派らしいけど。

Eclipseの設定

公式サイトのダウンロードボタンを押す。

ファイルを保存を選択して、ダウンロードしたフォルダに行き右クリックをして「ここに展開する」を選択。
出来上がったeclipse-installerフォルダ内のeclipse-instをダブルクリックする。

今回はjreがないと怒られたから取りに行きます。
Oracleの公式サイトからjreをダウンロード

規約の同意(Accept License Agreement)をチェックしないとダウンロードできないので注意

先ほどと同様にダウンロードしてきたファイルを展開して、生成されたjreから始まるフォルダをjreにリネームしてeclipse-installerの中に置けばeclipse-instが通るようになる。

pythonが選択肢にないのでとりあえずEclipse IDE for java Developpersを選択、フォルダを選択してインストール。インストールが完了したらLaunchボタンを押して起動。
ここでWorkspaceの選択をする。選択したフォルダ内にpythonのプロジェクトを置いていくことになる。

起動したら上のHelpタブからEclipse Marketplaceを選択し、pydevを検索。

installボタンからインストールする。インストールが完了すると、Eclipseを再起動するか聞かれるので再起動
初めはWelcomeタブが出ていると思うのでそれをxを押して消す。右上のOpenPerspectiveボタンを押して表示されるウィンドウにPydevが含まれていれば成功。

Anacondaの設定

仮想環境を使わないとpythonのバージョンが変わった時やバグった時に最悪OSから入れなおしになる事件が発生するかもしれないので、Anacondaを使って仮想環境を使えるようにする。
Anacondaを公式サイトからダウンロード。
よほどのことがない限りpython3.6バージョンでいい。
ターミナル(Ctr+Alt+T)に行って以下を実行

sudo sh '/home/user/ダウンロード/Anaconda3-5.1.0-Linux-x86_64.sh'

*userは人によって違う

途中利用規約やインストールパスを聞かれる。

 Do you wish the installer to prepend the Anaconda3 install location
to PATH in your /home/m/.bashrc

はyes Microsoft VSCodeのインストールはEclipseを使うならいらない。
インストールが終わったら一旦再起動
再起動したらターミナルを開いて仮想環境を作る

conda create -n python3.6-env

-n の後ろに環境名をかく

pythonのバージョンを環境名に入れておくと複数の環境を作った時にバージョンを間違えないから便利

source activate python3.6-env

で環境に入る。するとターミナルのパスの前に環境名がつく

which pythonと打つと現在使用しているpython.exeの場所がわかる。

機械学習によく使うパッケージをインストール

conda install numpy scipy sikit-learn keras pandas matplotlib

その後

python
import numpy, scipy, pandas, sklearn, matplotlib, keras

と打ってエラーが出なければ成功。Ctrl+Dで抜けます。

パッケージをインストールをインストールする方法にはpipとconda (とgit)があるけど基本的にpipとconda両方あるパッケージはcondaを使ったほうが良さそう。
pipはソースからコンパイルするものもあるからハマった時はCやFORTRANなどの闇と戦わなければならんらしい。

EclipseでAnacondaの仮想環境でインストールしたパッケージを使う

先にインストールしたEclipseのpydevで普通にpythonスクリプトを書いても反映されない。

反映させるには、まずeclipseを開き、penPerspectiveボタンを押して表示されるウィンドウにのPydevを選択してopenする。すると右上にpythonマークが出てくるのでこれを選択し、上のFile->New->PydevProjectを選択すると以下のようなウィンドウが出てくるのでプロジェクト名を適当に決めて真ん中付近のplease configure an interpreter before proceedingをクリックしManual Configを選択

その後右のNewからInterpreterNameを適当に決め、Browseを選択し、Anacondaの仮想環境内でwhich pythonを打った時に出てくるpythonを登録する。あとはOKを2回押してApply.Interpreterが変更したものになってることを確認してFinishを押す。

できたか確認しよう。
File→New→Fileから作ったプロジェクトを選択し、ファイルを名前の最後が.pyになるように生成。
短い名前はライブラリにすでに使われているファイル名と被った時めんどくさいことになるので避ける。

GPUサーバーの設定

このままでも機械学習を始められるが、ノートパソコンなどの普通の性能のパソコンだとビッグデータを使おうとすると処理にかなり時間がかかってその間パソコンが使えなくなったり、そもそもメモリが足りなくて動かないことのないよう小さいデータセットでテストしたら処理を別のものにやらせたほうがいい。今回はGPUサーバーがあるのでそれを使う。

サーバーといってもOSを入れれば普通のパソコンと同様に設定できる。OSはUSBからUbuntu16.04をインストールした。SSHをつなげるまではサーバーに画面とキーボードを直接つなげて設定した。繋げずにやる方法もあるらしいが試してない。

ログイン画面でAlt+Ctr+F1でCUIに入れる。OSにUbuntu Serverを選んだなら元々CUI。

SSHでサーバーと接続する

GPUサーバーは起動時に100dB超えるくらいの爆音であまり近づいて作業したくないのでSSHで全部やってしまう。

ここからサーバー側とクライアント側の操作がどっちがどっちかややこしくなるので行の先頭にサーバー側なら[S],クライアント側なら[C]をつける。

まずサーバーのIPをチェック

[S]ifconfig

inetアドレス:192.168.101.48 と書かれているこれがサーバーのIPアドレス。

長く使うならIPは固定したほうがいい。

[C]sudo apt-get install ssh
[C]ssh 192.168.101.48 -l [サーバー名]

SSHをインストールして、サーバーと接続。パスワードを聞かれるので、サーバーのユーザーのログイン時のパスワードを入れる。

こうなったらサーバーとの接続は成功。このターミナルともう一つターミナルを開くことでクライアント側のパソコンだけで処理が全てできる。

CUDAの設定

デフォルトのドライバではパフォーマンスが出ないので、NVIDIAから搭載しているGPU専用のドライバを取ってくる。

基本NVIDIAの公式インストールガイドに沿っていくがこの工程で結構バグが出て戦ったので最終的にできたやつを書いておく。公式の存在を知ったのが結構戦ったあとだったのが長引いた原因かも。

下手すると最悪OS再インストールになるので大事なデータなどは退避させたほうがいい。

sudo apt-get purge cuda*
sudo apt-get purge nvidia*
dpkg -l | grep nvidia
sudo wget http://developer.download.nvidia.com/compute/cuda/repos/ubuntu1604/x86_64/cuda-repo-ubuntu1604_8.0.61-1_amd64.deb
sudo dpkg -i cuda-repo-ubuntu1604_8.0.61-1_amd64.deb
sudo apt-get update
sudo apt-get upgrade
sudo apt-get install cuda
sudo nvidia-xconfig
export CUDA_HOME=/usr/local/cuda-9.1
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:${CUDA_HOME}/lib64
export PATH=$PATH:{$CUDA_HOME}/bin
sudo reboot
nvidia-smi

4, 5行目は人によって変える必要あり

最後にこんなのが出てきたらドライバのインストールはできている。

cuDNNの設定

これも公式サイトを参考にする。

最初にcuDNNの公式サイトからcuDNNをダウンロード。画面が必要なのでクライアント側でダウンロードしてサーバーに送りつけることにする。Nvidia Developer Programに登録しないとダウンロードできない。

[C] sudo scp '/home/[クライアントユーザー名]/Downloads/cudnn-9.1-linux-x64-v7.1.tgz' [サーバー名]@[サーバーIPアドレス]:/home/[サーバーユーザー名]
[S] sudo tar -xzvf ../../home/[サーバーユーザー名]/cudnn-9.1-linux-x64-v7.1.tgz
[S] sudo cp -P cuda/include/cudnn.h /usr/local/cuda-9.1/include
[S] sudo cp -P cuda/lib64/libcudnn* /usr/local/cuda-9.1/lib64/
[S] sudo chmod a+r /usr/local/cuda-9.1/lib64/libcudnn*

scpでファイルを送りつけてその後は公式サイトをなぞるだけの作業。cudaとかcuDNNのバージョンだけ注意

これでサーバーでプログラムを動かせるが、まだanacondaのライブラリたちが使えない。

‘/home/user/Downloads/Anaconda3-5.1.0-Linux-x86_64.sh’をサーバーに送りつけて先ほどと同じことをすれば使えるようになる。

[C] sudo scp '/home/[クライアントユーザー名]/Downloads/Anaconda3-5.1.0-Linux-x86_64.sh' [サーバー名]@[サーバーIPアドレス]:/home/[サーバーユーザー名
[S] sudo sh ./Anaconda3-5.1.0-Linux-x86_64.sh 以下同様

パッケージが増えたらAnacondaの環境を複製する方法を使うと楽そう。

サーバー側ではGPUを使うのでKeras-GPUとtensorflow-GPUもインストールする。

テスト

ついに環境ができたのでテスト。

使用したのはKerasライブラリのCNNを使った数字識別のサンプルコード mnist_cnn.pyに時間を計るコードだけを追加したもの。

試しに手元のノートパソコンideapadの結果はこうなった。

accuracyが正解率でtimeがかかった秒数、大体46分かかっている。

次にサーバーでやってみよう、サンプルをサーバーに送り、走らせる、

[S] source activate [環境名]
[S] python [ファイルを追加したフォルダのパス]mnist_cnn.py

結果はこれ。

手元のノートパソコン(ideapad 720S)GPUサーバー
Time2753.96s100.28s
Accuracy0.99130.9917

ちょうど100秒で終わった。速度は27.5倍くらい速くなっているだけでなくaccuracyも若干上がっている。1回しかテストしてないのでたまたまかもしれないが、Teslaのほうが倍精度をより正確に計算できるので、それが影響してるのかもしれない。ちなみにTeslaは2基搭載されているが、簡単のためそのうち1つしか使っていない。

まとめ

今回はGPUサーバーでPythonを使った機械学習を行う準備方法の解説でした。

次回からの自分の担当分はいろんな機械学習の手法を手元のデータに使っていって、そのやり方や結果を書いていきたいです。