ナード戦隊データマン

データサイエンスを用いて悪と戦うぞ

補足: Webコンテンツ抽出モデルのために作成したモジュール

Webコンテンツ抽出のvision-based CNNという記事では、前処理段階で省略した部分がある・モジュールが若干汚い・url入力からコンテンツ抽出までのパイプラインが明確でない、という3つの点で読みにくいので、今回はその3つの点を補足します。

1.事前の前処理モジュール

dataprocessor.pyから、事前の前処理のための部分だけを切り出しました。

preprocess.py

#!/usr/bin/env python3
import os
from bs4 import BeautifulSoup
import pickle
from multiprocessing import Pool

def difficult_check(soup):
    for o in soup.find_all("object"):
        if int(o.find('difficult').text) == 1:
            return True
    return False


def extract_positions(soup, min_width=500, min_height=500):
    targets = {'ps':[], 'labels':[]}
    for o in soup.find_all("object"):
        xmax = int(o.find('xmax').text)
        xmin = int(o.find('xmin').text)
        ymax = int(o.find('ymax').text)
        ymin = int(o.find('ymin').text)
        width = xmax - xmin
        height = ymax - ymin
        if width < min_width or height < min_height:
            continue
        else:
            targets['ps'].append([xmin, ymin, xmax, ymax])
            targets['labels'].append(o.find('name').text == "content")
    return targets


def data_preparation(keyname, rootpath):
    path = os.path.join(rootpath,keyname)
    files = [f.split(".")[0] for f in os.listdir(path) if f.endswith("xml")]
    return path, files


def get_and_check_targets(soup):
    if difficult_check(soup):
        return False, None
    targets = extract_positions(soup)
    if sum(targets['labels']) != 1:
        return False, None
    return (True, targets)


def save_batch(d, keyname, path, outpath):
    i = d[0]
    file = d[1]
    print(i, end=" ", flush=True)
    pic = "{}.jpeg".format(file)
    xml = "{}.xml".format(file)
    outfile = "{}_{}".format(keyname, file)

    with open(os.path.join(path, xml)) as f:
        soup = BeautifulSoup(f.read(), "xml")

    check_flag, targets = get_and_check_targets(soup)

    if check_flag is False:
        return (i, False)
 
    with open(os.path.join(outpath, outfile+".pkl"), "wb") as f:
        pickle.dump(targets, f)
        return (i, True)
    

if __name__ == "__main__":
    from functools import partial
    
    path_length = 3
    datapath = "data"
    outpath = "batch"
    
    for i in range(path_length):
        idx = str(i+1)
        path, files = data_preparation(idx, datapath)
        fixed_data = list(enumerate(files))

        func = partial(save_batch, keyname=idx, path=path, outpath=outpath)
        with Pool(10) as pool:
            print(pool.map(func, fixed_data))

この前処理モジュールは、Pascal VOC形式のデータを読み込み、候補となる要素と正解ラベルを切り出してpklで保存するものです。

2. 画像読み込みの高速化、ジェネレータの簡素化

ジェネレータを使ってKerasを訓練しますが、画像読み込みを高速化したものが以下です。

bindetector.py

import numpy as np
import cv2


def load_image(image_file):
    return cv2.imread(image_file)


def processing(im, ps, size=(905, 905)):
    out = []
    for p in ps:
        alpha = np.zeros([im.shape[0], im.shape[1]])
        alpha[p[1]:p[3], p[0]:p[2]] = 255.
        target = np.dstack((im, alpha))
        target = cv2.resize(target, size)/255.0
        out.append(target)
    return out

skimageよりも、cv2を使ったほうが早いです。

次に、dataprocessor.pyを以下のように簡素化します。

dataprocessor.py

from bs4 import BeautifulSoup
import bindetector
import numpy as np
import pickle
import os
import random
from sklearn.utils import shuffle


def transform_img(index, ps, size=(905, 905), data_path="data", img_format="jpeg", batch_path="batch"):
    X = []
    keyname, fileid = index.split("_")[:2]
    pic = "{}.{}".format(fileid.split(".")[0], img_format)
    path = os.path.join(data_path, keyname)
    im = bindetector.load_image(os.path.join(path, pic))
    return bindetector.processing(im, ps, size)


def sampling(targets, sample_size=4, batch_path="batch"):
    assert len(targets['ps']) == len(targets['labels'])
    data = [p for p, label in zip(targets['ps'], targets['labels']) if label is True]
    assert len(data) == 1
    tmp_targets = shuffle(list(zip(targets['ps'], targets['labels'])))
    data += [p for p, label in tmp_targets if label is False][:sample_size]
    return data


def get_indices(batch_path="batch"):
    return [f for f in os.listdir(batch_path) if f.endswith("pkl")]
    

def generate_data(indices, data_path="data", batch_path="batch", sample_size=4, batch_size=5):
    while(True):
        X = []
        labels = []
        for index in shuffle(indices)[:batch_size]:
            with open(os.path.join(batch_path, index), "rb") as f:
                targets = pickle.load(f)
            data = sampling(targets, sample_size, batch_path=batch_path)
            label = [True] + [False for _ in data[1:]]
            labels += label
            assert len(data) == len(label)
            X += transform_img(index, data, data_path=data_path, batch_path=batch_path)
        yield np.array(X), np.array(labels)

以前は、画像に候補矩形のアルファ値を加えたものをnpyで保存したものにも対応していましたが、npyファイルのロードに時間がかかるため、結局、ジェネレータで逐一画像を読み込んだほうが楽なので、npyのコードを除外しました。また、preprocess.pyへコードを分離ししたため、不要な依存関係を排除しました。

3. 実行のパイプラインを定義するモジュール

どのようにコンテンツ抽出を実行するのかイメージがわかないと思うので、訓練したモデルを使ってURL入力->コンテンツ抽出の流れをモジュール化しました。

extractor.py

import os
import re
import copy
import tempfile
import time
import base64
import json
import numpy as np
import math
from subprocess import call
from keras.models import model_from_yaml
import sys
import bindetector as bd
import tensorflow as tf


def save_png(tmp_dir):
    with open(os.path.join(tmp_dir, "tmp.json")) as f:
        snapshot = json.load(f)
    target = snapshot['SnapshotResponse']['Page']['CaptureScreenshot']
    pngtxt = target['content']
    with open(os.path.join(tmp_dir,"screenshot.{}".format(target["format"])), "wb") as f:
        f.write(base64.b64decode(pngtxt))
        

def download_page(url, opts=" --full --delay=1 --timeout=5 "):
    tmp_dir = tempfile.mkdtemp()
    tmp_file = os.path.join(tmp_dir, "tmp.json")
    out_file = os.path.join(tmp_dir, "dom.json")
    commands = [
        ["cdp-utils", "dom-snapshot"] + opts.strip().split() + [url, tmp_file],
        ["cdp-utils", "dom-tree", tmp_file, out_file]
    ]
    for command in commands:
        try:
            call(command, shell=False)
        except Exception as e:
            print(e, "cdp-utils Error.")
            return False, None
    try:
        save_png(tmp_dir)
    except Exception as e:
        print(e)
        return False, None
    return True, tmp_dir


def search_positions(tmp_dir, min_width=500, min_height=500):
    with open(os.path.join(tmp_dir, "dom.json")) as f:
        parent = json.load(f)
    ps = []
    childs = []
    stack = [parent['childNodes']]
    while(True):
        if len(stack) == 0:
            break
        children = stack.pop(0)
        for child in children:
            if "position" in child:
                p = child["position"]
                if p[2]-p[0] > min_width and p[3]-p[1] > min_height:
                    ps.append(child['position'])
                    childs.append(child)
            if "childNodes" in child:
                stack.append(child['childNodes'])
        assert len(ps) == len(childs)
    return ps, childs


def load_model(model_path, weight_path):
    with open(model_path) as f:
        model = model_from_yaml(f.read())
    model.load_weights(weight_path)
    graph = tf.get_default_graph()
    return model, graph


def generator_from_array(X_test):
    while 1:
        for i in range(1000):
            yield X_test[i:i+1]


def extract(url, model, graph, opts=" --full --delay=3 --timeout=60 ", img_format="jpeg", min_width=500, min_height=500, pred_size=10):
    from functools import partial
    check_flag, tmp_dir = download_page(url, opts)
    if check_flag:
        ps, childs = search_positions(tmp_dir, min_width, min_height)
    im = bd.load_image(os.path.join(tmp_dir, "screenshot.{}".format(img_format)))
    candidates = np.array(bd.processing(im, ps))
    with graph.as_default():
        preds = model.predict_generator(generator_from_array(candidates), candidates.shape[0])
    trueone = np.argmax([x[0] for x in preds])
    return childs[trueone]


def get_content_from_block(child):
    import copy
    child_copy = copy.deepcopy(child)
    past_nodes = []
    current_node = child_copy["childNodes"]
    values = []
    imgs = []

    while(True):
        if len(current_node) == 0:
            if len(past_nodes) == 0:
                return values, imgs
            else:
                current_node = past_nodes.pop(-1)
                if len(current_node) == 0:
                    continue
        target = current_node.pop(0)
        if 'attrs' in target:
            hasjs = False
            for attr in target['attrs']:
                if 'value' in attr and 'javascript' in attr['value']:
                    hasjs = True
                    break
            if hasjs:
                continue
        if "childNodes" in target:
            past_nodes.append(current_node)
            current_node = target['childNodes']
        if "name" in target:
            if target['name'] == "#text":
                values.append(target)
            elif target['name'] == "IMG":
                imgs.append(target)


if __name__ == "__main__":
    start = time.time()
    model, graph = load_model(
        "/root/work/dataprocessor/model3/model3.yml",
        "/root/work/dataprocessor/model3/old/weights.37-0.33.h5"
    )
    opts = " --full --width=1280 --delay=0 --timeout=90 --format=jpeg --quality=40 "
    
    child = extract("http://jbpress.ismedia.jp/articles/-/54187", model, graph, opts=opts)
    values, imgs = get_content_from_block(child)
    regex = re.compile(r"^[ \n]+$")
    print(''.join([value['value'] for value in values if re.match(regex, value['value']) is None]))
    print("executed time: {}".format(time.time() - start))

抽出されたコンテンツの出力:

「夢の技術」量子コンピューター、実用化まであと一歩!大手企業が開発を急ぐ背景には多分野での応用を見据えた戦略が松ヶ枝優佳/2018.9.26 9月19日、理化学研究所がNTTやNEC、東芝などと共同で次世代の高速計算機である「量子コンピューター」の開発に乗り出すと報じられた。研究は文部科学省の事業として実施され、年間約8億円規模のプロジェクトとなる予定だ。 IBMやGoogleが積極的に投資を行ない、開発を進めていることでも知られる量子コンピューター。スーパーコンピューターにも答えが出せないような問題も一瞬で解いてしまうとされ、かつては実現性に乏しい「夢の技術」とされていたが、今や実用化直前と言われるまでに研究が進み、開発競争も激化する一方だ。 様々な企業や研究機関が一丸となって実用化を急ぐ量子コンピューターとは一体どんな技術なのだろうか。量子コンピューターとは 量子コンピューターとは、簡単に言えば「スーパーコンピューターを大幅に上回る処理速度を持つ、次世代のコンピューター」のことだ。量子力学という、従来のコンピューターとは全く違う原理を採用することで、圧倒的な情報処理能力を持つ。 私たちが知る通常のコンピューターは「ビット」という単位を用いて演算を行なうが、量子コンピューターは「量子ビット」という量子力学上の単位を使う。情報を扱う際、ビットでは「0と1のどちらの状態にあるのか」を基礎とするが、量子ビットでは量子力学特有の「重ね合わせ」という概念を用いる。これにより、複数の計算を同時に進めることができるのだ。 「0であり、1でもある」という量子の性質を活用することで、従来のスーパーコンピューターでは何年もかかる計算を一瞬で終わらせることができる。 スーパーコンピューターをはじめとする従来型コンピューターは、技術革新の限界が近付いている。1年半でコンピューターの性能が2倍になっていく「ムーアの法則」も近く通用しなくなると言われる今、根本から異なる原理、異なるハードウェアで動く量子コンピューターに期待が集まっているのだ。 さらに、量子コンピューターは従来型コンピューターに比べて圧倒的に低コストで運用できると言われており、エネルギー問題の観点からも注目されている。事実、後述の「D-WaveSystems」が開発した量子コンピューターは、現在のスーパーコンピューターの100分の1の電力で稼働させられるという。並べると良いことづくめのようにも思えるが、現状は従来型のように何でもこなせるわけではない。 量子コンピューターは、大別すると「量子ゲート」モデルと呼ばれる汎用タイプと「量子イジング」モデルと呼ばれるタイプの2種類がある。現在のスーパーコンピューターの上位互換と言える「万能選手」は量子ゲート型であり、古くから量子コンピューターとして研究されてきたのもこちらだ。実用化が切望されているが、技術的な問題をクリアして実用化されるにはもう少しかかるだろう。 ちなみに量子コンピューターと言えば、クレジットカード等の情報保護等に使われている「暗号化技術の解除」を簡単にできるもの、というイメージを持っている読者もいるかもしれないが、それができるとされるのも量子ゲート型だ。 一方、量子イジングモデルの中でも数種類あるうち「量子アニーリング型」と呼ばれる量子コンピューターは、用途は絞られるものの2011年にカナダのベンチャー企業、D-WaveSystemsによって既に商用化されている。こちらについて詳しく見てみよう。123»

なぞのコマンド"cdp-utils"がありますが、こちらは都合上、公開できません。このツールは指定したurlのスクリーンショットとdomツリーを出力するツールです。

パイプラインの流れは、if __name__=="__main__": 内に書かれています。

  1. モデルのロード。
  2. cdp-utilsのオプション指定。
  3. コンテンツだけを含んでいるdom要素を予測。
  4. dom要素から#textとIMGだけを取得。
  5. textを結合して出力。

ちなみに、dom要素から#textを取得する部分では、domツリーから正しい順番で取得する必要がありますが、単純な非再帰深さ優先探索で対応しており、この部分は非常に高速に処理されます。

処理の中で時間が最もかかるのはextract関数内部ですが、平均的なニュース記事(例えば毎日新聞)であれば、1つのGPUを使うだけで6〜7秒程度で予測ができます。この速度は、diffbotよりも高速です。

次の課題

vision-basedモデルはなんとなく実行できたので、次の課題は「dom-basedモデル」を作成することです。アノテーション済みのPascal VOCデータと取得済みのdomツリーデータを利用して、dom-basedモデルへつなげようと考えています。