cloverrose's blog

Python, Machine learning, Emacs, CI/CD, Webアプリなど

Microsoftのベクトル検索エンジンSPTAGを使ってみる

Item2Vec系とベクトル検索エンジンの組み合わせが結構好きなので久々にブログでまとめてみる

ベクトル検索エンジンSPTAG github.com

環境構築

Dockerで環境を構築する

動作確認できるようにFlaskで簡単なServerを作った.

app.py

# -*- coding:utf-8 -*-
from flask import Flask, request
app = Flask(__name__)

import os
import time
import numpy as np
import SPTAG

def setup():
    dim = int(os.environ.get('VECTOR_DIM', '3'))
    distmethod = os.environ.get('DIST_METHOD', 'Cosine')
    if distmethod not in ('Cosine', 'L2'):
        distmethod = 'Cosine'
    print("VECTOR_DIM={}".format(dim))
    index = SPTAG.AnnIndex('BKT', 'Float', dim)
    index.SetBuildParam("NumberOfThreads", '2')
    index.SetBuildParam("DistCalcMethod", distmethod)
    while not index.ReadyToServe():
        time.sleep(0.1)
    return index

index = setup()

@app.route("/")
def hello():
    return "Hello SPTAG"

@app.route("/add")
def add():
    vector_str = request.args.get('vector')
    vector = list(map(float, vector_str.split(',')))
    vectors = np.array([vector], dtype=np.float32)
    index.Add(vectors.tobytes(), vectors.shape[0])
    return '{}'.format(vector)

@app.route("/search")
def search():
    vector_str = request.args.get('vector')
    vector = list(map(float, vector_str.split(',')))
    query = np.array(vector, dtype=np.float32)
    k = int(request.args.get('k', '1'))

    result = index.Search(query.tobytes(), k)
    return '{}'.format(result)

Dockerfile

FROM alpine:latest

RUN apk add git swig make cmake g++ \
            boost boost-system boost-thread boost-serialization boost-wserialization boost-regex boost-dev \
            alpine-sdk \
            python3 python3-dev py3-pip

RUN apk add --repository http://nl.alpinelinux.org/alpine/edge/testing libtbb libtbb-dev

RUN pip3 install ipython numpy flask

RUN git clone https://github.com/cloverrose/SPTAG.git && \
    mkdir SPTAG/build

WORKDIR /SPTAG/build

RUN cmake .. && make

WORKDIR /SPTAG/Release

COPY app.py .

ENV FLASK_APP app.py

EXPOSE 5000

ENTRYPOINT ["flask", "run", "--host", "0.0.0.0", "--port", "5000"]

app.pyとDockerfileを同じフォルダに置いてから以下を実行.

docker build -t sptag:latest-py3 .
docker run -e VECTOR_DIM=2 -e DIST_METHOD=L2 --rm --name sptag -p 5000:5000 -it sptag:latest-py3

動作確認

ブラウザでaddをしてからsearchをする

  • http://localhost:5000/add?vector=0.1,0.1
  • http://localhost:5000/add?vector=0.9,0.9
  • http://localhost:5000/search?vector=0.9,0.9&k=2

気付き、感想

SPTAG/GettingStart.md at master · microsoft/SPTAG · GitHub

Pythonのコードを参考にAddWithMetaDataを使いたかったのだが、エラーになってしまう。

追記2019/06/02 https://github.com/microsoft/SPTAG/commit/a00b58b469dcedbd9e8295503d9556f6a7708568 これが修正?

エラーで即サーバーが死んだり、セグフォになるなあという印象。

実際に大規模に使うのはまだ少し怖い。まだ慣れているNGTDを選ぶかな。

コマンドラインのRelease/server, Release/clientはデータの追加に対応していない?(なのでapp.pyを追加したという感じ)

自分が触っていた2018年12月時点だと、NGTDがデータの更新に結構弱くてスナップショットを作ってインデックス更新って方法をとっていた。 最近のマイクロソフトの論文を読むとリアルタイムに更新してそうだったからSPTAGが気になってライブラリを調べてみたがもう少し様子見かな。

補足

本家じゃなくてcloverroseでForkした方のSPTAGをcloneしている理由

Common.hの19行目に以下を追加 (AlpineじゃなければこのDefine済み定数が書かれた.hファイルがインストール済みなのかもしれない)

#define ACCESSPERMS (S_IRWXU|S_IRWXG|S_IRWXO)

Python3系を入れている理由

cmakeの前にPython3の条件を満たすパッケージを入れておくとWrapperとしてSPTAG.pyがReleaseフォルダに作成される.

後はReleaseフォルダ内でpythonを実行すればimport SPTAGで読み込める.

本家DocumentのTypo

IndexBuilderなどは../Release/にできていますが、大文字ではなく小文字です。

Spark版xgboostでRank学習できるようにPR投げた

機械学習で有名なxgboostというライブラリに先日PRがマージされた!嬉しい!

[jvm-packages] call setGroup for ranking task by cloverrose · Pull Request #2066 · dmlc/xgboost · GitHub

PRを送った経緯

  • 会社の業務で検索のランキングモデルを作っていて、xgboostへの移行を検討している。(現在進行形)
  • 会社にはHadoopクラスタがあり、ログはHadoop上に乗っている
  • なのでHadoop上でログの整形→学習が完結するとかなりクール
  • Hadoop上で動くxgboostはYARN版とSpark版がある
    • YARN版はHadoopクラスタにライブラリを追加しないといけないなどの理由でSpark版の方を採用
  • xgboost4j-sparkにはRank学習をサポートしていなかった
  • ただxgboost4j-scalaまでたどればsetGroupメソッドが用意されているので、Sparkからメソッドを呼ぶようにすればよいことがわかった

自分がコードの意図をちゃんと理解していない部分が何個かあったためレビューをたくさんしていただいた。

変更自体は9月くらいに実装して、機械学習のコンペで使っていたのですが、ついにPRを投げてそれがマージされたのは嬉しかった。

ゆゆ式 Advent Calendar 2016 17日目 AR縁ちゃんを作りました

f:id:cloverrose:20161218000239p:plain

ゆゆ式アドベントカレンダー2016の17日目です。

www.adventar.org

今年自分はARで縁ちゃんを発見するアプリ、「AR縁ちゃん」を作りました。

AR縁ちゃんとはカメラの画像をリアルタイムで解析して、縁ちゃんの目っぽいもの(コンセント)があったら、そこに縁ちゃんを描画するアプリです。

動画を見ていただくとわかりやすいかと思います。

youtu.be

実装中のコードはGithubにあげてあります。まだ改善したい箇所があるのでぼちぼち更新します。

github.com

コードが完成したら版権を考慮してなんらかの形で公開したいと思います。

昨日は

hayashida_2ndさんのゆずこたち三人と猫ちゃんのかわいいイラストでした。

偶然だけど僕の飼ってる猫と毛色が同じだった!

今年もかわいいイラストや面白いゲーム、考察、まとめなど0:00が楽しみな毎日でした。

自分も0:00に投稿したかったのですがギリギリになってしまいました m(_)m

かわいい縁ちゃんのコンセント目について

gifmagazine.net

コンセント縁ちゃんにはこんなかわいいイラストがあります。

ゆかりちゃんの目がコンセントっぽいところを集めた動画もあります。

www.nicovideo.jp

余談

Androidのカメラ画像の四角形を認識したい!と同期のエンジニアに相談したらOpenCVでできそうだよ、と下記のサイトを紹介してもらったのがスタートになります。

OpenCV shape detection - PyImageSearch

具体的にはコンセントを認識したいと話したら縁ちゃんの目だなと察した友達がいました。

以下は技術メモになります。OpenCVJavaで使うサンプルがあまりないので他の人の役に立つと幸いです。

続きを読む

Android OpenCV face-detection 動かすメモ

ブログを色々参考にしながら進めたが結構苦戦したのでまとめておく。

環境

手順

0. Android StudioOpenCV for Android を導入する

OpenCV for AndroidをAndroid Studioに導入するメモ - Qiita をなぞる

うまく動いた。

1. NDK使わないバージョンのOpenCV (15-puzzle) を試す

AndroidStudio2.0でOpenCV3.1(sample編) - プログラミング好きな脳の引き出しをなぞる

これはうまく動いた。

注意点

  • Android端末にOpenCV Managerをインストールしておくこと
  • build.gradleが複数ある
2. NDK使うバージョンのOpenCV (face-detection) を試す

AndroidStudio2.0でOpenCV3.1(sample with NDK編) - プログラミング好きな脳の引き出し をなぞる

一部漏れがあったので差分だけ補う。エラーが出たら以下の差分を試して解消を試みる。

差分1. NDKをインストールする

Android Studioを開いて、File>Project Structureで開いたWindowにDownload NDKみたいなのがある。

差分2. gradle.propetiesに以下を追加。

android.useDeprecatedNdk=true

差分3. Application.mkを編集

APP_STL := gnustl_static
APP_CPPFLAGS := -frtti -fexceptions
APP_ABI := armeabi-v7a arm64-v8a
APP_PLATFORM := android-8

arm64-v8aを追加した

参考:http://sarl-tokyo.com/wordpress/?p=553

差分4. ライブラリをコピーする

cp -r ~/Downloads/OpenCV-android-sdk/sdk/native/libs ~/Desktop/face-detection/openCVLibrary310/src/main/jniLibs

参考:java - android Static Initialization opencv 3.0 Cannot load library "opencv_java3" - Stack Overflow

まとめ

OpenCV for Android 3.x系じゃ動かなかったから2.4.11を試したという記事もあるが3.1でちゃんと動くことが確認できた

Gist的なメモ

openCVLibrary310/build.gradle

apply plugin: 'com.android.library'

android {
    compileSdkVersion 21
    buildToolsVersion "25.0.0"

    defaultConfig {
        minSdkVersion 21
        targetSdkVersion 21
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
        }
    }
}

openCVSamplefacedetection/build.gradle

apply plugin: 'com.android.application'

android {
    compileSdkVersion 21
    buildToolsVersion "25.0.0"

    defaultConfig {
        applicationId "org.opencv.samples.facedetect"
        minSdkVersion 21
        targetSdkVersion 21

        ndk {
            moduleName "detection_based_tracker"
        }
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
        }
    }

    sourceSets.main {
        jni.srcDirs = [] // This prevents the auto generation of Android.mk
        jniLibs.srcDir 'src/main/libs' // This is not necessary unless you have precompiled libraries in your project.
    }

    task buildNative(type: Exec, description: 'Compile JNI source via NDK') {
        def ndkDir = "/Users/rose/Library/Android/sdk/ndk-bundle"
        commandLine "$ndkDir/ndk-build",
                '-C', file('src/main/jni').absolutePath, // Change src/main/jni the relative path to your jni source
                '-j', Runtime.runtime.availableProcessors(),
                'all',
                'NDK_DEBUG=1'
    }

    task cleanNative(type: Exec, description: 'Clean JNI object files') {
        def ndkDir = "/Users/rose/Library/Android/sdk/ndk-bundle"
        commandLine "$ndkDir/ndk-build",
                '-C', file('src/main/jni').absolutePath, // Change src/main/jni the relative path to your jni source
                'clean'
    }

    clean.dependsOn 'cleanNative'

    tasks.withType(JavaCompile) {
        compileTask -> compileTask.dependsOn buildNative
    }
}

dependencies {
    compile project(':openCVLibrary310')
}

build.gradle

// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.2.2'
    }
}

allprojects {
    repositories {
        jcenter()
    }
}

gradle.propeties

android.useDeprecatedNdk=true

local.properties

## This file is automatically generated by Android Studio.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file must *NOT* be checked into Version Control Systems,
# as it contains information specific to your local configuration.
#
# Location of the SDK. This is only used by Gradle.
# For customization when using a Version Control System, please read the
# header note.
#Sun Dec 04 18:10:43 JST 2016
ndk.dir=/Users/rose/Library/Android/sdk/ndk-bundle
sdk.dir=/Users/rose/Library/Android/sdk

settings.gradle

include ':openCVLibrary310'
include ':openCVSamplefacedetection'

Ubuntu ServerでX serverを動かす手順メモ

AWSのEC2のUbuntu Server (Ubuntu Server 14.04 LTS (HVM), SSD Volume Type - ami-a21529cc) でX serverを動かす手順メモ

sudo apt-get update && sudo apt-get upgrade && sudo reboot
sudo apt-get install xorg
sudo apt-get install python-qt4
sudo xinit &
export DISPLAY=:0.0

xinit動かすと error setting MTRR (base = 0xf0000000, size = 0x00100000, type = 1) Invalid argument (22) のエラー出るけどPyQt4はちゃんと動作してた。今のところ問題なさそう。

PyQt4でJavaScriptでレンダリングしてるWebページをScraping! CookieとUserAgentも扱うよ

JavaScriptレンダリングしてるWebページをScrapingしたい!!!

自分の環境

  • Virtualbox (Host OS Windows 7 64bit)
  • Guest OS Ubuntu16.04.1 LTS Desktop 64bit
  • Python 2.7.12
  • PyQt4は sudo apt-get install python-qt4 でインストール

実現方法

調べたら素晴らしいBlogが見つかった。

Ultimate guide for scraping JavaScript rendered web pages | IMPYTHONIST

BlogではPyQt4のWebKitを使ってJavaScript部分もレンダリングしたHTMLを取得する方法を紹介している。

(HTMLのParseに関する話もあるが、HTMLさえ取得できればBeautifulSoup使えばいい)

このBlogはコメント欄も有用な情報がたくさんあって、同じことをする別のライブラリやプロジェクトを示唆している人もいた。

ただやりたいことに対して複雑なライブラリを使いたくなかったので、このBlogの方法で実現してみた。

Blogの通りに動かすとちゃんと http://pycoders.com/archive/スクレイピングできた!!

タスク

だいたいやりたいことの大枠はBlogのソースコードで実現できた。

あとは細かい部分を詰めていくだけ。やりたいところはこの2つ

  • Cookieを扱いたい
  • UserAgentを扱いたい

Cookieを扱いたい

Blogのコメント欄にもあるけどQNetworkCookieJarを使えばいける

実現方法の詳細はこのSOがとても参考になった。

python - Permanent cookies with QWebKit -- where to get the QNetworkAccessManager? - Stack Overflow

手順

  1. EditThisCookie などを使ってCookie情報をJSONファイルに保存しておく
  2. QNetworkCookieを作る
  3. QNetworkCookieのListを作る
  4. QNetworkCookieJarを作る
  5. QWebPageにQNetworkCookieJarをセット .networkAccessManager().setCookieJar(cookiejar)

関連クラスの仕様

UserAgentを扱いたい

このSOが参考になった。

python - Setting useragent in QWebView - Stack Overflow

自分のUserAgentはここでわかるっぽい

手順

  1. What's My User Agent? - User Agent & Browser Tools などでCookieを調べる
  2. .userAgentForUrl に このCookieを返す関数をセットする

最終結果

# -*- coding:utf-8 -*-
import datetime
import json
import sys
from PyQt4.QtGui import *
from PyQt4.QtCore import *
from PyQt4.QtWebKit import *
from PyQt4.QtNetwork import *


# 偽装したいUserAgent
USER_AGENT = "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36"
COOKIE_FILE = "cookie.json"


def customuseragent(url):
    u"""
    どのurlでも同じUserAgentを返す
    """
    return USER_AGENT


def make_cookiejar(filename):
    u"""
    JSON形式でExportされたCookieを読み込みQNetworkCookieJarを作る
    """
    with open(filename) as f:
        data = json.load(f)

    cookies = []
    for item in data:
        cookie = QNetworkCookie(item['name'], item['value'])
        if 'domain' in item:
            cookie.setDomain(item['domain'])
        if 'expirationDate' in item:
            cookie.setExpirationDate(datetime.datetime.fromtimestamp(item['expirationDate']))
        if 'hostOnly' in item:
            cookie.setHttpOnly(item['hostOnly'])
        if 'path' in item:
            cookie.setPath(item['path'])
        if 'secure' in item:
            cookie.setSecure(item['secure'])
        cookies.append(cookie)

    cookiejar = QNetworkCookieJar()
    cookiejar.setAllCookies(cookies)
    return cookiejar


#Take this class for granted.Just use result of rendering.
class Render(QWebPage):
    def __init__(self, url):
        self.app = QApplication(sys.argv)
        QWebPage.__init__(self)
        self.loadFinished.connect(self._loadFinished)

        self.userAgentForUrl = customuseragent
        cookiejar = make_cookiejar(COOKIE_FILE)
        self.networkAccessManager().setCookieJar(cookiejar)

        self.mainFrame().load(QUrl(url))
        self.app.exec_()

    def _loadFinished(self, result):
        self.frame = self.mainFrame()
        self.app.quit()


def main(url, filename):
    r = Render(url)
    result = r.frame.toHtml().toUtf8()
    with open(filename, 'w') as f:
        f.write(result)


if __name__ == '__main__':
    URL = 'http://pycoders.com/archive/'
    FILENAME = 'result.html'
    main(URL, FILENAME)

補足

Qtの独自の型があるけど

  • QByteArrayってなってるところはPythonのstr型でOK
  • QDateTimeってなってるところはPythonのDatetime型でOK
  • frame.toHtml()の結果をtoUtf8()してあげないと日本語が文字化けで?になってしまう

感想

3年前くらいにMacPyQt動かそうとしてメッチャ苦労したけどUbuntuでは超簡単に動いた。

わりとすぐにいいBlogにたどり着くことができてよかった。

PyQtのリファレンス体裁が汚すぎる…

Raspberry Pi3 起動に苦戦

昨日Raspberry Pi3 ModelBが届いた

www.amazon.co.jp

NOBESのダウンロードを30分位待って、よっしゃ遊ぶぞって思って、電源を挿したらPWRの赤色LEDは点滅するけど、モニターには何も映らない。

ぐぐったら、Raspberry Pi3からは電源として5V, 2.5Aのものを使う必要があることを知った。

あわててAmazonで注文した。 www.amazon.co.jp

次の日、アダプターが届いたので、よっしゃ遊ぶぞって思って、電源を挿したら(以下略)

www.raspberrypi.org によると

The Raspberry Pi's bootloader, built into the GPU and non-updateable, only has support for reading from FAT filesystems (both FAT16 and FAT32), and is unable to boot from an exFAT filesystem. So if you want to use NOOBS on a card that is 64GB or larger, you need to reformat it as FAT32 first before copying the NOOBS files to it.

ということで僕のSDXC64GB exFATにフォーマットされてるから読み込めないっぽい。ひとっ走りSDカード買いに行かないと。