2017年12月22日金曜日

LinuxでWindows用のUSBシリアル変換ケーブルを使う方法

簡単過ぎて、ただの備忘録ですが・・・

今回使ったのは、SANWAからでているWindows用のUSBシリアル変換ケーブル「USB-CVRS9」とUbuntu 17.10。対向はYAMAHAのRTX-1210です。

まず、起動してUSBポートに挿します。
dmesgでどこに割り当てられたか調べます。

[21133.336349] usb 3-5: new full-speed USB device number 17 using xhci_hcd
[21133.481260] usb 3-5: New USB device found, idVendor=0557, idProduct=2008
[21133.481263] usb 3-5: New USB device strings: Mfr=1, Product=2, SerialNumber=0
[21133.481264] usb 3-5: Product: USB-Serial Controller D
[21133.481265] usb 3-5: Manufacturer: Prolific Technology Inc.
[21133.481921] pl2303 3-5:1.0: pl2303 converter detected
[21133.482656] usb 3-5: pl2303 converter now attached to ttyUSB0


cuをインストール
sudo apt-get install cu

後はつなぐだけ。
cu -l ttyUSB0


以上、すんなり繋がりすぎてびっくり。

2017年11月11日土曜日

RaspberryPiにサーボモータ(SG90)をつないでみる - その1

今回は、手元にあるRaspberry Pi 3と、とにかく安いサーボモータということでラジコン用の(多分ステアリングの角度を決める)サーボモータであるSG90というモータを接続してみます。Amazonで1個450円位ですが、例によって偽物が多く出回っている状況ですので信頼できるところから購入した方がいいと思います。偽物の判定ができるページまで用意されています・・・。
スペックは、秋月電子さんが提供してくれているものにリンクを張っておきます。 http://akizukidenshi.com/download/ds/towerpro/SG90_a.pdf
このサーボモータは50Hz(20ms)の周期の中で電圧がかかっている時間に応じて角度を決めて最大180°回転します。データシートによって2種類の記載があるようだけど、色々と試した結果と他サイトの情報を確認するとパターン2が正しそう。ですので、以降の文書はパターン2を前提に動作を確認していきます。

パターン1

1 ms -90°
1.5 ms
2 ms 90°

パターン2

0.5 ms -90°
1.45 ms
2.4 ms 90°

上の表については下記のような図をイメージすればよいのかと思います。0.5msで-90°、2.4 msで+90°となります。これを50Hzのサイクルで送ってあげれば良いみたいです。

この短い時間の電力を制御するために、PWMという方法を使って電力のスイッチングを行います。ソフトウェア的に実現してもよいのですが、タイミングがかなりシビアですので、通常はハードウェアの出力したPWMを利用するのだと思います。

Raspberry Pi 3には、2系統のPWMを出力する機構が備わっていて、1系統はステレオジャックから出される音源と共用しています。ですので、ステレオジャックを利用する場合は、1系統しかPWMを出力することができません。(**PIGPIOを利用する場合とモータドライバの基盤を別途用意する場合は別です。)

基本的には最大でも2系統しかPWMを出力できませんので、2台のサーボモータしか制御することができません。

先に書いておくと、ライブラリとしてPIGPIOを使用するか、サーボモータドライバを増設するかのどちらか正解で、ある程度まともな制御とサーボの台数が必要となると、どちらかを選択することとなります。でも、色々なパッケージを試して理解したい事もあったので、ここでは下記のような順番で試していくことにします。

  • RPi.GPIOのPWMを使用する。
  • WiringPiのPWMを使用する。
  • PIGPIOのOWMを使用する。
  • サーボモータドライバ(PCA9685)を利用する。

RPi.GPIOで1台動作させる

まずは、RPiで1台のサーボを動作させてみます。 RPiのPWMでは、最初に周波数(Hz)を決めてあげて、パルスを送る比率(デューティー比)を渡してあげることで、サーボモータを制御します。

接続方法

ソースコード

#!/usr/bin/python
# coding: utf-8

import RPi.GPIO as GPIO
import time
import signal
import sys


def rotate_servo(servo, angle):
    #   0度の位置 0.5 ms / 20 ms * 100 = 2.5 %
    # 180度の位置 2.4 ms / 20 ms * 100 = 12 %
    #      変動幅 12% - 2.5% (9.5%)
    # angle * 9.5 / 180
    if -90 <= angle <= 90:
        d = ((angle + 90) * 9.5 / 180) + 2.5
        servo.ChangeDutyCycle(d)
    else:
        raise ValueError("angle")


def init_servo(gpios):
    """
    初期化します。gpiosは利用するGPIOをLISTで指定してください。
    :param gpios: GPIO番号(LIST)
    :return: GPIO.PWM (List)
    """
    pwms = []
    GPIO.setmode(GPIO.BCM)
    if isinstance(gpios, list):
        for gpio in gpios:
            GPIO.setup(gpio, GPIO.OUT)

            s = GPIO.PWM(gpio, 50)
            s.start(0.0)
            pwms.append(s)
    else:
        raise Exception("gpios isn't list object.")

    return pwms


if __name__ == "__main__":
    # GPIO 12番を使用
    GPIO_12 = 12

    # 初期化
    pwms = init_servo([GPIO_12])

    try:
        # -90°の位置まで動かし3秒停止します。
        rotate_servo(pwms[0], -90)
        time.sleep(3)

        # 0°の位置まで動かし3秒停止します。
        rotate_servo(pwms[0], 0)
        time.sleep(3)

        # 90°の位置まで動かし3秒停止します。
        rotate_servo(pwms[0], 90)
        time.sleep(3)

        for mm in range(4):
            # -90°の位置まで動かします。
            rotate_servo(pwms[0], -90)
            time.sleep(2)
            for i in range(-90, 91):
                # -90~90°まで20ミリ秒毎に動かします。
                rotate_servo(pwms[0], i)
                time.sleep(0.02)

    except KeyboardInterrupt as ki:
        # サーボの動作を停止します。
        pwms[0].stop()
        GPIO.cleanup()

動作状況

動画をYoutubeにあげてみましたが、明らかに怪しい挙動を示す時があります。特に負荷がかかっておらず、停止している時に突然プルプル震えだしていることがわかると思います。まぁ、これでかまわないという人はこれでいいのかなと思いますが、普通はちょっとなんとかしたくなりますよね・・・。

RPi.GPIOで2台動作させる

問題はあるものの、そのままサーボの数を2台に増やしてみます。

接続方法

ソースコード

#!/usr/bin/python
# coding: utf-8

import RPi.GPIO as GPIO
import time
import signal
import sys


# def rotate_servo(servo, angle):
#     # 2.5 - 12 (9.5)
#     # 0 - 180
#     d = (angle / 18.94736842105263) + 2.0
#     servo.ChangeDutyCycle(d)
def rotate_servo(servo, angle):
    #   0度の位置 0.5 ms / 20 ms * 100 = 2.5 %
    # 180度の位置 2.4 ms / 20 ms * 100 = 12 %
    #      変動幅 12% - 2.5% (9.5%)
    # angle * 9.5 / 180
    if -90 <= angle <= 90:
        d = ((angle + 90) * 9.5 / 180) + 2.5
        servo.ChangeDutyCycle(d)
    else:
        raise ValueError("angle")

def init_servo(gpios):
    """
    初期化します。gpiosは利用するGPIOをLISTで指定してください。
    :param gpios: GPIO番号(LIST)
    :return: GPIO.PWM (List)
    """
    pwms = []
    GPIO.setmode(GPIO.BCM)
    if isinstance(gpios, list):
        for gpio in gpios:
            GPIO.setup(gpio, GPIO.OUT)
            # GPIO.PWM(GPIO, 周波数(Hz))
            s = GPIO.PWM(gpio, 50)
            s.start(0.0)
            pwms.append(s)
    else:
        raise Exception("gpios isn't list object.")

    return pwms


if __name__ == "__main__":
    # GPIO 12番を使用
    GPIO_12 = 12
    GPIO_13 = 13

    # 初期化
    pwms = init_servo([GPIO_12, GPIO_13])

    try:
        # -90°の位置まで動かし3秒停止します。
        rotate_servo(pwms[0], -90)
        rotate_servo(pwms[1], -90)
        time.sleep(3)

        # 0°の位置まで動かし3秒停止します。
        rotate_servo(pwms[0], 0)
        rotate_servo(pwms[1], 0)
        time.sleep(3)

        # 90°の位置まで動かし3秒停止します。
        rotate_servo(pwms[0], 90)
        rotate_servo(pwms[1], 90)
        time.sleep(3)

        for mm in range(4):
            # -90°の位置まで動かします。
            rotate_servo(pwms[0], -90)
            rotate_servo(pwms[1], -90)
            time.sleep(2)
            for i in range(-90, 91):
                # -90~90°まで20ミリ秒毎に動かします。
                rotate_servo(pwms[0], i)
                rotate_servo(pwms[1], i)
                time.sleep(0.02)

    except KeyboardInterrupt as ki:
        # サーボの動作を停止します。
        pwms[0].stop()
        pwms[1].stop()
        GPIO.cleanup()

動作状況

接続する台数を2台に増やしてみましたが、やはり不定期に震えていることがわかります。また、Raspberry Piに接続された画面に頻繁に雷マークが出るようになりました。これは電圧降下(ドロップ)が発生しているためでサーボが要求する電圧をRaspberry Piが供給できていないために発生していると思われます。(なお、アイコス用の2.4AのUSB充電器を使用していますので、元々0.1A不足しています)


問題点

さて、ちょっと長くなってしまいましたので今回はここまでということで、現在抱えている問題点は下記の3点ということになります。
  • 電圧降下
  • 不定期に震える
  • 2台までしか設置できない
これらは、次回以降やっつけていくことにします。

2017年10月24日火曜日

vSphereのREST APIを使ってみる

今回はvSphereに6.5からRESTful APIが実装されていたようなので使ってみようと思います。Postmanという試験用のツールから使ってみて使い勝手を把握したら、Pythonから呼び出してみます。RESTなので、別にほかの言語でも構わないですが今更Perlという感じでもないので・・・。
なお、Postmanについては、下記のドキュメントを参考にさせていただきました。あくまで試験用のツールでパスワードなども平文で表示されてしまいますので、試験用途以外では使えないと思います。
Getting Started with the vSphere Automation SDK for REST

今回、使用した環境は下記のとおりです。

  • vCenter Server Appliance 6.5
  • Postman v5.3.1
  • Python 3.5

1 vSphereにユーザを追加します。

vSphereに管理者権限でログインします。
[メニュー]から[管理]を選択し、[ユーザおよびグループ]を選択します。
[+Add]アイコンをクリックし[ユーザ名]、[パスワード]を入力し[OK]をクリックします。

[グローバル権限]をクリックし、[+]アイコンをクリックします。
ユーザに先ほど作成した"test_user"を入力し、"システム管理者"の権限を割り当て[子へ伝達]のチェックを入れ[OK]をクリックします。
# 運用環境では必要なアクセス権に制限した方がいいと思います。


2 Postmanをインストールし起動します。

アカウントを作るように求められますので、アカウントを作成します。
Googleのアカウントも使うことができますので、好きな方を選択します。

3 証明書のエラーチェックを外します。

vCenterは自己署名証明書ですので証明書のチェックをしないように設定します。証明書を買っている場合や組織で用意したものを使用している場合は、下記の手順は不要です。
[File]から[Settings]を選択します。
[General]タブで"SSL certificate verification"を"OFF"に変更します。


4 gitからサンプルをcloneします。

git clone https://github.com/vmware/vsphere-automation-sdk-rest


5 cloneしたサンプルをPostmanにインポートします。

PostmanのGUIから[Import]をクリックし、[Choose files]をクリックします。

.\vsphere-automation-sdk-rest\samples\postman
上記のフォルダから下記4項目を選択し、[開く]をクリックします。
  • vSphere-Automation-Rest-API-Appliance-Resources.postman.json
  • vSphere-Automation-Rest-API-Resources.postman.json
  • vSphere-Automation-Rest-API-Samples.postman.json
  • vSphere-Automation-REST-resources-for-Content-Library.postman_collection.json


6 環境変数を定義します。

右上のアイコンから、[設定]アイコンをクリックし[Manage Environments]を選択します。
[Add]をクリックし、下記の内容を入力し[Add]をクリックし"X"で閉じます。
名前 test_env
vc vCenterのIPもしくはFQDN
user test_user@vsphere.local
password 試験ですので適当に
userをlocalに作成した場合は、@vsphere.localをつけ忘れないようにしてください。

右上のNo Enviromentをクリックして、"test_env"を選択します。


7 ログイン

先程インポートしたPostmanの[Collection]から[vSphere Automation REST Resources]を選択し
[Authentication] - [Login]を選択します。
TYPEに"Basic Auth"を選択し[Send]ボタンをクリックします。
値としてvalueが返ってくればOKです。

{
    "value": "6c83725d0b690a737d97805ce1d05d80"
}


8 既存のVMの一覧を取得

[VM]から"List"を選択し、[Send]をクリックします。
既存で存在する、VMの一覧が表示されるはずです。

9 VMの作成

[VM]から"Create with defaults"を選択し、[Send]をクリックします。
下記のエラーが発生し、VMは作成されません。

"default_message": "Resource pool with identifier 'resgroup-9' does not exist.",
"id": "com.vmware.api.vcenter.vm.resourcepool_not_found"

テンプレートに記載されている"resgroup-9"は実環境にないとエラーが表示されているようです。[BODY]タブの下記の項目を実環境に合わせて設定することで、エラーが発生しなくなります。

"datastore": "datastore-12",
"folder": "group-v7",
"resource_pool": "resgroup-9"

datastoreは[Collection]から[Datastores]にある[List]を実行することで取得可能です。
folderは[Collection]から[Folders]にある[List]を実行することで取得可能です。
resource_poolは[Collection]から[Resource Pool]にある[List]を実行することで取得可能です。

試験環境ではdatastore-25、group-v49、resgroup-10でしたので、この値を使うことにします。
[VM]の"Create with defaults"に戻り、Bodyの部分の下記の通り書き換えます。
{
    "spec": {
        "guest_OS": "RHEL_7_64",
        "placement" : {
            "datastore": "datastore-25",
            "folder": "group-v49",
            "resource_pool": "resgroup-10"
        }
    }
}

書き換えが完了したら、[Send]をクリックします。
下記のように、作成されたVMの番号が返ってきます。
{
    "value": "vm-73"
}


10.Pythonからの呼び出し

さて、Postmanで実行できることがわかったので、Pythonから適当に呼び出してみます。
nm0からnm9と名前の付いた10個の仮想マシンを作成します。
取り敢えずRESTの試験のため適当に作りましたので、実環境に適用する際はパスワードを暗号化して別に持つなど、コードを適切に書き換えてください。

import requests


def connect(vc, user, passwd):
    """
    vCenterに接続してSessionオブジェクトを返します。
    :param vc:      vCenterのFQDNかIPアドレス
    :param user:    ユーザ
    :param passwd:  パスワード
    :return:        Sessionオブジェクト
    """
    url = "https://{0}/rest/com/vmware/cis/session".format(vc)
    sess = requests.Session()
    sess.auth = (user, passwd)
    sess.verify = False
    res = sess.post(url)
    if res.status_code != 200:
        raise Exception(res.content)
    return sess


class VmCreator:
    """
    vm作ります!
    """
    def __init__(self, vc, session, datastore, folder, resource_pool):
        """
        :param vc:              vCenterのFQDNかIPアドレス
        :param session:         Sessionオブジェクト
        :param datastore:       Datastore
        :param folder:          Floder
        :param resource_pool:   Resource Pool
        """
        self._server = vc
        self._url = "https://{0}/rest/vcenter/vm".format(self._server)
        self._session = session
        placement = {
            "datastore": datastore,
            "resource_pool": resource_pool,
            "folder": folder
        }
        self._data = {"placement": placement}

    def set_name(self, value):
        self._data["name"] = value

    def set_guest_os(self, value):
        self._data["guest_OS"] = value

    def set_memory(self, size, hot_add=False):
        memory = {
            "size_MiB": size,
            "hot_add_enabled": hot_add
        }
        self._data["memory"] = memory

    def get_data(self):
        return self._data

    def set_cpu(self, socket, core, hotadd=False, hotremove=False):
        cpu = {
            "count": socket,
            "cores_per_socket": core,
            "hot_add_enabled": hotadd,
            "hot_remove_enabled": hotremove
        }
        self._data["cpu"] = cpu

    def add_disk(self, capacity):
        disk = {"new_vmdk": {"capacity": capacity}}
        if "disks" not in self._data:
            self._data["disks"] = []
        self._data["disks"].append(disk)

    def add_cdrom(self, path):
        cdrom = {
            "type": "IDE",
            "backing": {
                "iso_file": path,
                "type": "ISO_FILE"
            }
        }
        if "cdroms" not in self._data:
            self._data["cdroms"] = []
        self._data["cdroms"].append(cdrom)

    def create(self):
        param = {"spec": self._data}
        res = self._session.post(self._url, json=param)
        if res.status_code != 200:
            raise Exception(res.content)
        return res.content


vcs = "xxx.xxx.xxx.xxx"                         # vCenterServerのIPかFQDN
us = "xxxxxxxxx@vsphere.local"                  # user
ps = "XXXXXXXXXXXXXXXX"                         # password

s = connect(vcs, us, ps)
v = VmCreator(vcs, s, "datastore-XX", "group-vXX", "resgroup-XX")
v.set_guest_os("RHEL_7_64")
v.set_cpu(2, 2)                                 # Socket, Core
v.set_memory(4096, True)                        # 4096MByte
v.add_disk(10*1024*1024*1024)                   # 10GByte
v.add_disk(1024*1024*1024)                      # 1GByte
v.add_cdrom("[datastore1-2] image/CentOS-7-x86_64.iso")

# nm0からnm9までの仮想マシンを作成します。
for i in range(0, 10):
    v.set_name("nm{0}".format(i))               # vm name
    v.create()



無事10個の仮想マシンを作成できることが確認できました。
どんなパラメータ投げてるかは、postのparamをjson.dumpsしてみればわかると思います。

vSphereは元々APIが豊富でしたが、利用にはPerlを入れたりライブラリを入れたりでいろいろと準備が必要でした。RESTで書けるようになることでAPIの利用の敷居が低くなり、色々はかどりそうな気がします。

2017年10月15日日曜日

測距センサー(HC-SR04)とRaspberry Piをつないでみる

最近週末は安物センサーと Raspberry pi で遊んでいます。

今回は距離を測る HC-SR04 というセンサーを Raspberry pi から使ってみます。このセンサーは一時期センサー自体が固まる不良ロッドが広く流通した影響かとても安く出回っています。きちんと動作するものが欲しければ、当時リコール対応をした秋月電子やスイッチサイエンスなど信頼のおけるところから入手してください。

ちなみに私はAmazonの怪しい販売店から150円以下で買いました。

センサーの仕様と外観

見た目は下記のような感じです。

表面


裏面



仕様は下記のような感じで、分解能が0.3cmと割とおおざっぱです・・・。
動作電圧 DC5V
待機電流 2mA以下
有効角度 15°以下
測距距離 2cm~450cm
分解能 0.3cm


配線

ピンは下記のような感じで配線します。GPIOは余っている場所ならどこでもいいです。今回はGPIO26とGPIO19を使いました。
HC-SR04 Raspberry Pi 3
Trig(Output) 37 pin (GPIO26)
Echo(Input) 35 pin (GPIO19)
Ground 06 pin (GND)


ブレッドボード

下図のように配線しました。センサーのEchoからは5Vで入力がありますが、Raspberry pi側の入力は最大3.3Vですので、抵抗は入れなくても動くと記載されているブログもありましたが念のため抵抗を入れてあります。


測距方法の概要

概要としては下記のような手順です。
  1. Trigに10u秒以上をHIGHにすると、測距が開始されます。
  2. EchoがLOWであることを確認します。
  3. 音波が対象物に当たって跳ね返ってくるまでの時間、EchoがHIGHになり、その後EchoがLOWになります。
  4. HIGHになっていた時間と音速を掛けて、往復の距離が出ますので2で割ることで片道の距離を求めます。
下記のような図をイメージしていただければよろしいかと思います。

実装

1. はTrigをHIGHにして10usecの間SleepしたあとLOWにすれば計測が始まります。

2. と 3. の部分については、ArduinoにpulseInというC言語の関数がありますので、このソースを参考にPythonに移植してみます。

pulseIn(pin, value, timeout)

となっていますので、引数もこれを模倣することにします。

4. の音速についてはWikipediaに近似式が「1気圧の乾燥空気では 331.5 + 0.61t」と書いてありましたので、これを使うこととします。ちなみにtは温度です。

温度は固定で入れるか、温度センサーがついていれば、その値を入れてみてもいいかもしれません。
# そもそも分解能が0.3cmと結構おおざっぱなので固定15℃で十分かも

コード

pulse_inの泥臭さ・・・。まぁ、サイクル単位でコード書けるArduinoと比較してしまうとこんなもんかなという感じで、PythonだとArduinoのようなリアルタイムな感じのコードは難しいですね。
hc_sr04_test.py
import RPi.GPIO as GPIO
import time


def pulse_in(pin, value=GPIO.HIGH, timeout=1.0):
    """
    ピンに入力されるパルスを検出します。
    valueをHIGHに指定した場合、pulse_in関数は入力がHIGHに変わると同時に時間の計測を始め、
    またLOWに戻るまでの時間(つまりパルスの長さ)をマイクロ秒単位(*1)で返します。
    タイムアウトを指定した場合は、その時間を超えた時点で0を返します。
    *1 pythonの場合はtimeパッケージの仕様により実装依存ですが、概ねnanosecで返ると思います。
    :param pin: ピン番号、またはGPIO 番号(GPIO.setmodeに依存。)
    :param value: パルスの種類(GPIO.HIGH か GPIO.LOW。default:GPIO.HIGH)
    :param timeout: タイムアウト(default:1sec)
    :return: パルスの長さ(秒)タイムアウト時は0
    """
    start_time = time.time()
    not_value = (not value)

    # 前のパルスが終了するのを待つ
    while GPIO.input(pin) == value:
        if time.time() - start_time > timeout:
            return 0

    # パルスが始まるのを待つ
    while GPIO.input(pin) == not_value:
        if time.time() - start_time > timeout:
            return 0

    # パルス開始時刻を記録
    start = time.time()

    # パルスが終了するのを待つ
    while GPIO.input(pin) == value:
        if time.time() - start_time > timeout:
            return 0

    # パルス終了時刻を記録
    end = time.time()

    return end - start


def init_sensors(trig, echo, mode=GPIO.BCM):
    """
    初期化します
    :param trig: Trigger用ピン番号、またはGPIO 番号
    :param echo: Echo用ピン番号、またはGPIO 番号
    :param mode: GPIO.BCM、または GPIO.BOARD (default:GPIO.BCM)
    :return: なし
    """
    GPIO.cleanup()
    GPIO.setmode(mode)
    GPIO.setup(trig, GPIO.OUT)
    GPIO.setup(echo, GPIO.IN)


def get_distance(trig, echo, temp=15):
    """
    距離を取得します。取得に失敗した場合は0を返します。
    :param trig: Trigger用ピン番号、またはGPIO 番号(GPIO.setmodeに依存。)(GPIO.OUT)
    :param echo: Echo用ピン番号、またはGPIO 番号(GPIO.setmodeに依存。)(GPIO.IN)
    :param temp: 取得可能であれば温度(default:15℃)
    :return: 距離(cm)タイムアウト時は 0
    """

    # 出力を初期化
    GPIO.output(trig, GPIO.LOW)
    time.sleep(0.3)
    # 出力(10us以上待つ)
    GPIO.output(trig, GPIO.HIGH)
    time.sleep(0.000011)
    # 出力停止
    GPIO.output(trig, GPIO.LOW)

    # echo からパルスを取得
    dur = pulse_in(echo, GPIO.HIGH, 1.0)

    # ( パルス時間 x 331.50 + 0.61 * 温度 ) x (単位をcmに変換) x 往復
    # return dur * (331.50 + 0.61 * temp) * 100 / 2
    return dur * (331.50 + 0.61 * temp) * 50


if __name__ == "__main__":

    GPIO_TRIG = 26
    GPIO_ECHO = 19

    init_sensors(GPIO_TRIG, GPIO_ECHO)
    while True:
        print("距離:{0} cm".format(get_distance(GPIO_TRIG, GPIO_ECHO)))
        time.sleep(2)

実測!

最後に実測してみます。
実行は、sudoした上で実行してください。
sudo python3 hc_sr04_test.py

終了はCtrl + Cで。
15cmくらいのところに障害物を置いて距離を測ってみたところ下記のような感じでした。値をそのまま使ってしまうと結構ばらつきますね。もともと分解能が0.3cmと記載されていましたが、それ以上にひどい結果です。もう少し正確な値が必要であれば、前回との差分や平均をとるようなことも考えないといけないかもしれません。
距離:13.887083530426025 cm
距離:15.341055393218994 cm
距離:15.258443355560303 cm
距離:15.250182151794434 cm
距離:15.179961919784546 cm
距離:15.22952914237976 cm
距離:15.390622615814209 cm
距離:15.324532985687256 cm
距離:15.353447198867798 cm
距離:15.266704559326172 cm
距離:15.279096364974976 cm
距離:14.936256408691406 cm


1mの場所に物を置いた場合。
距離:101.06756687164307 cm
距離:99.9605655670166 cm
距離:100.59667825698853 cm
距離:101.3938844203949 cm
距離:90.7245397567749 cm
距離:100.15883445739746 cm
距離:100.46449899673462 cm
距離:100.47689080238342 cm
距離:100.50580501556396 cm
距離:100.8569061756134 cm
距離:100.62146186828613 cm

2017年10月10日火曜日

人感センサー(HC-SR501)とRaspberry Piをつないでみる



人感センサーのHC-SR501がAmazonで150円以下になっており、とても安いのでRaspberry Pi 3との接続を試してみました。なお、Amazonで1つ注文したのですが、なぜか2つ送付されてきました。1個75円!と思って喜んでいたのですが、一方はまともに動作しませんでしたので、もしかしたら歩留まりが悪いのかもしれません。

仕様

なんか、仕様が記載されているドキュメントによって、まちまちなので大体こんなもんだくらいに思っていたほうが良いのかもしれません。
動作電圧範囲 DC4.5V ~ 20V
ドレイン電流 65uA (50uA)
レベル出力 ハイレベル:3.3V、ローレベル出力:0 V
トリガモード Lノンリピートモード/ Hリピートモード(デフォルト)
遅延時間 0.5-18秒
ブロッキング時間 2.5秒
外形寸法 32mm x 24mm
検出範囲 100°(120°)
作業温度 -15° ~ 70°
誘導レンズサイズ 直径:23mm
一応このサイトに記載されている情報が公式の情報に当たると思うのですが、販売店が書いてる情報とはちょっと違いますね・・・。何が正しいんだろう・・・w

外観

HC-SR501を裏から見ると下記の図のような感じです。



検知出力保持時間

検出した後、検出した状態を保持する時間を調整するための半固定抵抗がついてますので、プラスドライバーで調整できます。やってみないとわからに様な気がしますので、必要に応じて変更してください。0.5秒~8秒のようです。(ドキュメントによって異なるようですが)

感度

こちらも、半固定抵抗がついていますのでドライバー等で調整します。最大で7mの範囲まで検知できるようです。

モード

ここは、固定されているものと固定されていないものがあるようです。
リピートに設定しておくと、検知し終わってから先ほどの保持時間の間は3.5vの入力がある状態となります。リピートを選択しておけば間違いないと思います。ノンリピートを選択した場合は検知し始めてから保持時間の間3.5vからの入力があると思われます。(試験してません)

配線

配線は右からPower、Output、Groundとなりますので、下記の通り配線します。
HC-SR501 Raspberry Pi 3
Power 02 pin (5V)
Output 29 pin (GPIO5)
Ground 06 pin (GND)

下図のような感じで配線すればOKです。(*上から見ています)



*配線中はRaspberry Piは通電していない状態でやってください。

あとは、下記のようなプログラムで検知したかどうかを判定します。
人感センサーが動きを検出すると、「動いた」と表示されるだけのシンプルなプログラムですw

hc_sr501_test.py
import sys
import time
import RPi.GPIO as GPIO

SLEEP_TIME = 1
SENSOR_GPIO = 5

GPIO.cleanup()
GPIO.setmode(GPIO.BCM)
GPIO.setup(SENSOR_GPIO, GPIO.IN)

while True:
  if GPIO.input(SENSOR_GPIO) == GPIO.HIGH:
    sys.stdout.write("\r動いた  ")
  else:
    sys.stdout.write("\r動いてない")

  time.sleep(SLEEP_TIME)

実行は、sudoした上で実行してください。
sudo python3 hc_sr501_test.py

終了はCtrl + Cで。

実行してみた感じだと、それほど検出精度が高いようには感じられないですw
まぁ、使えなくはないかなといった感じ。

このセンサーは、動きを検知しているようですので、止まっていると検知できません。

2017年10月4日水曜日

Raspberry Pi 3にUSBスピーカーをつなぐ

今回の環境は下記のとおりです。
  • Raspberry Pi 3
  • Raspbian GNU/Linux 9.1 (stretch)
また、今回登場するスピーカーは、サンワサプライのMM-SPU8BKというスピーカーです。amazonで900円でした。(安い!)
値段の割には見た目が良く評価もそこそこなので、例え繋がらなくてもまぁいいかと思って、ダメもとで買ってみました。



デフォルトだと、HDMIに接続されたテレビから音が出てしまうので、
何とかして、USB接続のスピーカーから音を出してみます。

まずは、aplay -lでデバイスの状態を確認します。

カード 0: ALSA [bcm2835 ALSA], デバイス 0: bcm2835 ALSA [bcm2835 ALSA]
  サブデバイス: 8/8
  サブデバイス #0: subdevice #0
  サブデバイス #1: subdevice #1
  サブデバイス #2: subdevice #2
  サブデバイス #3: subdevice #3
  サブデバイス #4: subdevice #4
  サブデバイス #5: subdevice #5
  サブデバイス #6: subdevice #6
  サブデバイス #7: subdevice #7
カード 0: ALSA [bcm2835 ALSA], デバイス 1: bcm2835 ALSA [bcm2835 IEC958/HDMI]
  サブデバイス: 1/1
  サブデバイス #0: subdevice #0

次に、USBスピーカーを接続してからaplay -lを実行してみます。

**** ハードウェアデバイス PLAYBACK のリスト ****
カード 0: ALSA [bcm2835 ALSA], デバイス 0: bcm2835 ALSA [bcm2835 ALSA]
  サブデバイス: 8/8
  サブデバイス #0: subdevice #0
  サブデバイス #1: subdevice #1
  サブデバイス #2: subdevice #2
  サブデバイス #3: subdevice #3
  サブデバイス #4: subdevice #4
  サブデバイス #5: subdevice #5
  サブデバイス #6: subdevice #6
  サブデバイス #7: subdevice #7
カード 0: ALSA [bcm2835 ALSA], デバイス 1: bcm2835 ALSA [bcm2835 IEC958/HDMI]
  サブデバイス: 1/1
  サブデバイス #0: subdevice #0
カード 1: MicroII [Audio Advantage MicroII], デバイス 0: USB Audio [USB Audio]
  サブデバイス: 1/1
  サブデバイス #0: subdevice #0

無事認識しているようで、新たに増えた部分が今回接続したスピーカーということになります。

カード番号が1
デバイス番号が0

なので、aplayの引数に-Dでカードとデバイスを指定してあげれば問題ない・・・と思ったんだけど・・・。

aplay -D hw:1,0 test.wav
再生中 WAVE 'test.wav' : Signed 16 bit Little Endian, レート 48000 Hz, モノラル
aplay: set_params:1305: チャネル数が使用不可能


はいエラー。しかもチャンネル数ってなによ。

この問題を解決するのには、結構時間がかかってしまったけど、下記のようにplugを付ければモノラル音声をステレオに変換して再生してくれるということらしい。要するに明示的に指定しないと、モノラル(1ch)からステレオ(2ch)には、変更してくれないということみたい。

aplay -D plughw:1,0 test.wav

これで、「aplay test.wav」 で HDMI から音声が出力され、「aplay -D plughw:1,0 test.wav」でUSBスピーカーから音声が出力される。

でも、やりたいことはこれじゃない。
デフォルトをスピーカー、指定した時だけHDMI側から再生したいのです。

色々調べると、まず出てくるのは、「/usr/share/alsa/alsa.conf」を直接書き換えるやり方。
大体下記の変更を行って再起動すれば反映されます!のようなドキュメント。
確かにこれをやればできる・・・。できるんだけど、asoundrcを無効にしてしまうのは、ちょっと違うんじゃないかなぁと・・・。

#         "~/.asoundrc"

defaults.ctl.card 1 # 録音するデバイスを指定
defaults.pcm.card 1 # 再生するデバイスを指定


本来は、再起動なんて必要なくてasoundrcを自動的に読み込んでデバイス選択してくれるものなんじゃないのか??

で、色々試したところ下記の内容をasoundrcに書くことで、希望通りデフォルトをスピーカー側にすることに成功しました。

pcm.!default {
    type plug
    slave.pcm "dmixer"
}

ctl.!default {
    type hw
    card 0
    device 0
}

pcm.dmixer {
    ipc_key 1024
    type dmix
    slave {
        pcm "hw:1,0"
    }
}

結構時間掛かっちゃったなー。
ということで一応メモ。

今回、参考にさせていただいたサイト。
この文章は、後で少し校正させていただきます。

2017年10月3日火曜日

OpenJTalkでLinuxに話してもらう@CentOS7

普通はRaspberry piなどでやるのかもしれませんが、たまたま手元にそれらしい環境がなかったので、なぜかVirtualBox上のCentOSから話してもらうテストです。意味があるかというとあまりないような気もしますが・・・

パッケージは用意されていないように思いますので、ソースからのインストールです。

今回利用した環境は下記の通り。
  • CentOS 7.4
  • Open JTalk 1.10
  • hts_engine API 1.10
まずは、yumで入るものを入れてしまいます。
yum install alsa-utils unzip wget gcc gcc-c++

hts_engineのインストール

hts_engineは、HMMベースの音声合成システムによって訓練されたHMMから音声波形を合成するソフトウェアだそうです。これがないとOpenJTalkをconfigureする際にエラーになります。
mkdir ~/download/
mkdir ~/src/
cd ~/download
wget https://jaist.dl.sourceforge.net/project/hts-engine/hts_engine%20API/hts_engine_API-1.10/hts_engine_API-1.10.tar.gz
cd ~/src/
tar xvzf ~/download/hts_engine_API-1.10.tar.gz
cd hts_engine_API-1.10/
./configure
make
make install


OpenJTalkのインストール

続いてOpenJTalkをインストールします。./configureの際にutf-8のオプションをつけ忘れないようにしてください。
cd ~/download
wget https://jaist.dl.sourceforge.net/project/open-jtalk/Open%20JTalk/open_jtalk-1.10/open_jtalk-1.10.tar.gz
cd ~/src
tar xzvf ~/download/open_jtalk-1.10.tar.gz
cd open_jtalk-1.10/
./configure --with-charset=UTF-8
make
make install

音響モデルの配置

htsvoiceは声の特徴を音素を基本単位としてモデル化した音響ファイルです。ただのファイルなので、どこに置いても良いのですが、/usr/local/shareに配置することが多いようです。
cd ~/download
wget https://jaist.dl.sourceforge.net/project/open-jtalk/HTS%20voice/hts_voice_nitech_jp_atr503_m001-1.05/hts_voice_nitech_jp_atr503_m001-1.05.tar.gz
cd ~/src
tar xzfv ~/download/hts_voice_nitech_jp_atr503_m001-1.05.tar.gz
mv hts_voice_nitech_jp_atr503_m001-1.05 /usr/local/share/hts_voice

辞書の配置

文字を音に変換するので分かち書きと辞書が必要になります。辞書はMeCabで使っていた辞書とほぼ同等のフォーマットになりますが、読み上げに特化した情報(アクセントなど)が付記された辞書になります。これも、どうせ実行時にファイルパスを指定することになりますので、どこに置いても良いです。
cd ~/download
wget http://downloads.sourceforge.net/open-jtalk/open_jtalk_dic_utf_8-1.10.tar.gz
cd ~/src
tar xzfv ~/download/open_jtalk_dic_utf_8-1.10.tar.gz
mv open_jtalk_dic_utf_8-1.10 /usr/local/share/open_jtalk_dic
ここまできたら、とりあえず試験的にしゃべらせてみます。
echo "これはテストです。"| open_jtalk -m /usr/local/share/hts_voice/nitech_jp_atr503_m001.htsvoice -ow output.wav -x /usr/local/share/open_jtalk_dic; aplay output.wav;rm -f output.wav
ちょっと太い男の声で「これはテストです」と話したはずです。
ネット上に音響ファイルはたくさん落ちていますので、気に入ったものを見つけてくるといいと思います。

ほかのサイトでもやっていますが、MMDAgentというソフトウェアの中にmeiという女の人の声が入っていますのでダウンロードして同じディレクトリに配置してみます。
cd ~/download
wget https://jaist.dl.sourceforge.net/project/mmdagent/MMDAgent_Example/MMDAgent_Example-1.7/MMDAgent_Example-1.7.zip
cd ~/src
unzip ../download/MMDAgent_Example-1.7.zip
mv MMDAgent_Example-1.7/Voice/mei/*.htsvoice /usr/local/share/hts_voice/.


配置したらテスト。先ほどから少しパラメータを変更してみました。パラメーターによって声の高さや読み上げるスピードなどを変更することができます。
echo "これはテストです。"| open_jtalk -m /usr/local/share/hts_voice/mei_happy.htsvoice -ow output.wav -x /usr/local/share/open_jtalk_dic/ -a 0.5 -fm 1.0 -jf 0.5 ; aplay output.wav;rm -f output.wav
なお音声自体が個人情報だからなのか、各音響モデルで利用条件がありますので、読み上げた音声を利用する場合は、条件をよく読んで利用したほうが良いと思います。

2017年10月1日日曜日

Elasticsearch + Kibana で解析基盤を作る - その1



何番煎じかわかりませんが、個人的にも会社でもデータ解析用の基盤として利用しているEFKの構成方法を記載してみます。
結構負荷がかかるので、CPUもメモリも良いものを積んだ方がいいと思います。

今回の概要構成は下記の通り。
  • CentOS7.4
  • Elasticsearch 5.6.2
  • Kibana-5.6.2

Elasticsearchのインストール


基本的に公式サイトからダウンロードしてインストールするだけです。
yum install java
yum install https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-5.6.1.rpm
日本語の検索を行うために、形態素解析のkuromojiを入れます。 プロクシがある環境では、コマンド実行前に「export ES_JAVA_OPTS="-Dhttp.proxyHost=[proxy_ip] -Dhttp.proxyPort=[proxy_port] -Dhttps.proxyHost=[proxy_ip] -Dhttps.proxyPort=[proxy_port]"」を実行して下さい
/usr/share/elasticsearch/bin/elasticsearch-plugin install analysis-kuromoji

つぎに設定ファイルを開き編集します。基本的に設定ファイルの最後に追記すればOKです。
ファイル: /etc/elasticsearch/elasticsearch.yml
network.host: [els_ipaddress]
http.port: 9200
http.cors.enabled: true
http.cors.allow-origin: "*"
http.cors.* の設定は後から入れるElasticsearch-headで cluster health: not connectedと表示され接続できないのを防ぐためです。[els_ipaddress]は実際のElasticsearchをインストールするサーバのIPと置き換えてください。
編集したらElasticsearchを起動します。
systemctl start elasticsearch
問題なく起動したかは下記のファイルにログが記録されますので、エラーなどが発生していないか確認しておいてください。
/var/log/elasticsearch/elasticsearch.log
続けて、firewalldを開きます。ポートはデフォルトでは9200番を使用するようです。
送信元はのちにインストールするKibanaのサーバと管理用の端末など直接アクセスするホストのIPアドレスを含めるようにしてください。
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="[kibana_ipaddress]/[mask]" port port="9200" protocol="tcp" log prefix="elasticsearch" level="info" accept'
firewall-cmd --reload

Kibanaのインストール

Kibanaも公式サイトからダウンロードしてインストールするだけです。
yum install https://artifacts.elastic.co/downloads/kibana/kibana-5.6.1-x86_64.rpm
設定ファイルも先ほどと同様、最後に下記の内容を追記してください。
/etc/kibana/kibana.yml
server.port: 5601
server.host: [kibana_ipaddress]
elasticsearch.url: "http://[els_ipaddress]:9200"
[els_ipaddress]は実際のElasticsearchをインストールするサーバのIPと置き換えてください。
[kibana_ipaddress]はKibanaをインストールしたサーバのIPと置き換えてください。

設定が完了したら、Daemonを起動しておきます。
systemctl start kibana

こちらもFWを開きます。ポートは一般的に5601番を使用するようです。こちらは、Apcheなどと連携しない場合はユーザが直接利用することになりますので、利用者向けに広く開ける必要があります。
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="[client]/[mask]" port port="5601" protocol="tcp" log prefix="kibana" level="info" accept'
firewall-cmd --reload

実際にブラウザからアクセスしてみます。
http://[kibana_ipaddress]:5601/app/kibana
この時点では下記のように「Configure an index pattern」という画面が出てくれば成功です。

x-pack

x-packがあると、Elasticsearchのパフォーマンスを監視することができます。x-packを導入するには無料とはいえライセンス登録が必要になるため、導入するかしないかは、この時点で決めておいた方がいいです。後から入れようとするといろいろ面倒なので・・・。
有償のライセンス費用を払えば、ユーザの権限管理もできますが、今回は無償のライセンスを発行して使います。

まず、Daemonが起動している場合は止めておきます。
systemctl stop elasticsearch
systemctl stop kibana
インストールはelasticsearch-pluginから行います。
/usr/share/elasticsearch/bin/elasticsearch-plugin install x-pack
インストールが終わったら、Elasticsearchのサービスを起動します。
systemctl start elasticsearch
インストールが無事終了したかは下記のコマンドで確認できます。
curl -XGET -u elastic:changeme 'http://[els_ipaddress]:9200/_xpack/license'
{
  "license" : {
    "status" : "active",
    "uid" : "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
    "type" : "trial",
    "issue_date" : "2017-10-01T02:58:46.917Z",
    "issue_date_in_millis" : 1506826726917,
    "expiry_date" : "2017-10-31T02:58:46.917Z",
    "expiry_date_in_millis" : 1509418726917,
    "max_nodes" : 1000,
    "issued_to" : "elasticsearch",
    "issuer" : "elasticsearch",
    "start_date_in_millis" : -1
  }
}


次にkibanaにもx-packのプラグインを入れます。なお、インストールは下記の1行でインストール可能ですが、kibana-pluginは現時点ではProxyサーバに対応していませんので、Proxyのある環境の場合は、ダウンロードしたうえでインストールする必要があります。
/usr/share/kibana/bin/kibana-plugin install x-pack
Proxy環境下でインストールする場合は下記のような感じで、wgetした後にファイルを指定してインストールしてください。パスはフルパス指定じゃないと通らないようです。
wget https://artifacts.elastic.co/downloads/kibana-plugins/x-pack/x-pack-5.6.2.zip
/usr/share/kibana/bin/kibana-plugin install file:////local/path/x-pack-5.6.2.zip


インストールが終わったら、kibanaも起動します。
systemctl start kibana
Daemonが自動起動するように設定しておきます。
systemctl enable elasticsearch
systemctl enable kibana

実際にブラウザからアクセスしてみます。
http://[kibana_ipaddress]:5601/app/kibana
この時点では下記のようにログイン画面が出てくれば成功です。ユーザ名は「elastic」パスワードは「changeme」でログインできます。


ログインしたら、左のメニューから「Monitoring」を選択し「Your Trial license will expire on」に続く日付をクリックしてください。



今回は、1年ごとに更新が必要で機能が限定されているBasicLicenseを取得しますので「Get Basic」をクリックしてください。

メールアドレス宛にライセンスキーへのリンクが届きますので、ユーザ名、メールアドレス等を入力してください。


ライセンスキーを取得したら、名前を付けてサーバ上に保存して下さい。ここでは、「~/x-pack-license.txt」として保存をしたものとして進めます。
ライセンスキーの登録は下記のコマンドで実行できます。
curl -XPUT -u elastic:changeme 'http://[els_ipaddress]:9200/_xpack/license?acknowledge=true' -H "Content-Type: application/json" -d @x-pack-lisence.txt
無事登録できたかは下記のコマンドで確認できますので、確認しておいてください。
curl -XGET 'http://[els_ipaddress]:9200/_xpack/license'
下記のような内容が応答として帰ってくれば、完了です。
{
  "license" : {
    "status" : "active",
    "uid" : "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "type" : "basic",
    "issue_date" : "2017-10-02T00:00:00.000Z",
    "issue_date_in_millis" : 1506902400000,
    "expiry_date" : "2018-10-02T23:59:59.999Z",
    "expiry_date_in_millis" : 1538524799999,
    "max_nodes" : 100,
    "issued_to" : "先ほど登録した名前 (会社名)",
    "issuer" : "Web Form",
    "start_date_in_millis" : 1506902400000
  }
}

2017年9月18日月曜日

形態素解析の速度比較

日本語形態素解析の速度比較をしてみました。
環境は、仮想マシン上でXeon 2コアを割り当てた4GBのメモリとSSDのストレージを使った環境です。
解析対象はWikipediaExtractorで分割したWikipediaのデータ5MB分です。

正確性でいけばJUMAN++なんでしょうが、やっぱり速さとの関係上、大量のドキュメント処理するにはMeCabかなという心象です。あとで試験してみたところkuromojiもなかなかに早いです。
実行したコマンド 実行時間
chasen -iw 2.525s
mecab -Owakati 2.330s
mecab -Owakati -d /usr/local/lib/mecab/dic/mecab-ipadic-neologd 2.435s
mecab -Ochasen 4.098s
mecab -Ochasen -d /usr/local/lib/mecab/dic/mecab-ipadic-neologd 3.887s
juman 30.607s
juman++ 10782.644s
kuromoji 4.862s
janome 716.016s
Janomeとkuromojiを加えました。
kuromojiはやい!


グラフ化してみるとこんな感じでした。(縦軸が対数になっているので注意してください。)

Janomeのテスト用コード

Janomeについては、テスト用に標準入力を受け取るようにコードを作ってパイプで繋げて計測しました。
ファイル: test_janome.py
import sys
from janome.tokenizer import Tokenizer

t = Tokenizer()
for line in sys.stdin:
    tokens = t.tokenize(line, wakati=True)
    print(" ".join(tokens))


kuromojiのテスト用コード

kuromojiについては、exampleディレクトリの下に、ちょうどいいサンプルがついてきますので、そのコードををほんの少し変更し、こちらもパイプで繋げて計測しました。
ファイル: TokenizerExample.java
package org.atilika.kuromoji.example;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.List;

import org.atilika.kuromoji.Token;
import org.atilika.kuromoji.Tokenizer;
import org.atilika.kuromoji.Tokenizer.Mode;

public class TokenizerExample {

        public static void main(String[] args) throws IOException {
                Tokenizer tokenizer;
                if (args.length == 1) {
                        Mode mode = Mode.valueOf(args[0].toUpperCase());
                        tokenizer = Tokenizer.builder().mode(mode).build();
                } else if (args.length == 2) {
                        Mode mode = Mode.valueOf(args[0].toUpperCase());
                        tokenizer = Tokenizer.builder().mode(mode).userDictionary(args[1]).build();
                } else {
                        tokenizer = Tokenizer.builder().build();
                }
                BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
                String line;
                while ((line = reader.readLine()) != null) {
                        List result = tokenizer.tokenize(line);
                        for (Token token : result) {
                                System.out.print(token.getSurfaceForm() + " ");
                        }
                }
        }
}

2017年9月16日土曜日

WikipediaのデータをWord2Vecに

今更ですが、WikipediaのデータをWord2Vecで遊んでみます。
わざわざ書き起こすのは、メモしておかないと、遊び方を忘れてしまうためです・・・。

今回の環境は下記のような感じです。
  • CentOS 7.4
  • MeCab 0.996
MeCabとneologdは以前の記事を参照していただくか、groongaなどのリポジトリからインストールしておいてください。

準備

今回もgit wgetを用意しておくのと、Pythonの3.xの環境を用意しておきます。word2vecをコンパイルするのにgccも必要です。MeCaabをソースから入れた場合は、すでに入っているものもあると思いますので、その場合は読み飛ばしてください。
yum install git wget epel-release
yum install python34 python34-pip
yum install gcc

Wikipediaのわかち書きテキストを作成する

Wikipediaのデータの切り出しには「Wikipedia_Extractor」というツールを使います。 gitで落としてきます。
mkdir src
cd ~/src/
git clone https://github.com/attardi/wikiextractor
cd wikiextractor/
Wikipediaのデータは公開されているので、ダウンロードします。100万件以上、2.4GB分の記事があります。
wget https://dumps.wikimedia.org/jawiki/latest/jawiki-latest-pages-articles.xml.bz2
落としてきたファイルは、圧縮されたXMLなのですが、テキストに近い形式とするため先ほどの「Wikipedia_Extractor」を使用します。なお、落としてきたファイルは解凍する必要はありません。下記のように指定すると、解凍してタグを除去して5Mごとに分割してくれます。
mkdir corpus
python3 WikiExtractor.py -b 5M -o corpus jawiki-latest-pages-articles.xml.bz2
実行すると下記のように処理が完了したWikipediaのページの名称とページのIDが表示されます。
INFO: 313154    クラーク郡 (イリノイ州)
INFO: 313155    ジョン・マルコヴィッチ
INFO: 313156    カート・ラッセル
INFO: 313157    モスリン
INFO: 313158    マイク・マイヤーズ
INFO: 313160    大阪府道197号深井畑山宿院線



Wikipediaの日本語データを正規化を行うため、mecab-ipadic-neologdのページから正規化用のPythonコードをコピーしてきて「normalize_neologd.py」という名前で保存します。
一応下にも書いておきますが・・・neologdのサイトを確認したほうが良いと思います。
ファイル: normalize_neologd.py
# encoding: utf8
from __future__ import unicode_literals
import re
import unicodedata

def unicode_normalize(cls, s):
    pt = re.compile('([{}]+)'.format(cls))

    def norm(c):
        return unicodedata.normalize('NFKC', c) if pt.match(c) else c

    s = ''.join(norm(x) for x in re.split(pt, s))
    s = re.sub('-', '-', s)
    return s

def remove_extra_spaces(s):
    s = re.sub('[  ]+', ' ', s)
    blocks = ''.join(('\u4E00-\u9FFF',  # CJK UNIFIED IDEOGRAPHS
                      '\u3040-\u309F',  # HIRAGANA
                      '\u30A0-\u30FF',  # KATAKANA
                      '\u3000-\u303F',  # CJK SYMBOLS AND PUNCTUATION
                      '\uFF00-\uFFEF'   # HALFWIDTH AND FULLWIDTH FORMS
                      ))
    basic_latin = '\u0000-\u007F'

    def remove_space_between(cls1, cls2, s):
        p = re.compile('([{}]) ([{}])'.format(cls1, cls2))
        while p.search(s):
            s = p.sub(r'\1\2', s)
        return s

    s = remove_space_between(blocks, blocks, s)
    s = remove_space_between(blocks, basic_latin, s)
    s = remove_space_between(basic_latin, blocks, s)
    return s

def normalize_neologd(s):
    s = s.strip()
    s = unicode_normalize('0-9A-Za-z。-゚', s)

    def maketrans(f, t):
        return {ord(x): ord(y) for x, y in zip(f, t)}

    s = re.sub('[˗֊‐‑‒–⁃⁻₋−]+', '-', s)  # normalize hyphens
    s = re.sub('[﹣-ー—―─━ー]+', 'ー', s)  # normalize choonpus
    s = re.sub('[~∼∾〜〰~]', '', s)  # remove tildes
    s = s.translate(
        maketrans('!"#$%&\'()*+,-./:;<=>?@[¥]^_`{|}~。、・「」',
              '!”#$%&’()*+,-./:;<=>?@[¥]^_`{|}〜。、・「」'))

    s = remove_extra_spaces(s)
    s = unicode_normalize('!”#$%&’()*+,-./:;<>?@[¥]^_`{|}〜', s)  # keep =,・,「,」
    s = re.sub('[’]', '\'', s)
    s = re.sub('[”]', '"', s)
    return s

if __name__ == "__main__":
    assert "0123456789" == normalize_neologd("0123456789")
    assert "ABCDEFGHIJKLMNOPQRSTUVWXYZ" == normalize_neologd("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
    assert "abcdefghijklmnopqrstuvwxyz" == normalize_neologd("abcdefghijklmnopqrstuvwxyz")
    assert "!\"#$%&'()*+,-./:;<>?@[¥]^_`{|}" == normalize_neologd("!”#$%&’()*+,-./:;<>?@[¥]^_`{|}")
    assert "=。、・「」" == normalize_neologd("=。、・「」")
    assert "ハンカク" == normalize_neologd("ハンカク")
    assert "o-o" == normalize_neologd("o₋o")
    assert "majikaー" == normalize_neologd("majika━")
    assert "わい" == normalize_neologd("わ〰い")
    assert "スーパー" == normalize_neologd("スーパーーーー")
    assert "!#" == normalize_neologd("!#")
    assert "ゼンカクスペース" == normalize_neologd("ゼンカク スペース")
    assert "おお" == normalize_neologd("お             お")
    assert "おお" == normalize_neologd("      おお")
    assert "おお" == normalize_neologd("おお      ")
    assert "検索エンジン自作入門を買いました!!!" == \
        normalize_neologd("検索 エンジン 自作 入門 を 買い ました!!!")
    assert "アルゴリズムC" == normalize_neologd("アルゴリズム C")
    assert "PRML副読本" == normalize_neologd("   PRML  副 読 本   ")
    assert "Coding the Matrix" == normalize_neologd("Coding the Matrix")
    assert "南アルプスの天然水Sparking Lemonレモン一絞り" == \
        normalize_neologd("南アルプスの 天然水 Sparking Lemon レモン一絞り")
    assert "南アルプスの天然水-Sparking*Lemon+レモン一絞り" == \
        normalize_neologd("南アルプスの 天然水- Sparking* Lemon+ レモン一絞り")

さらに、パイプで繋げられるように標準入力で入ってきたデータを正規化できるように微妙に小細工を入れつつラッパーを作ります。
ファイル: conv.py
import sys
import normalize_neologd as nneo
import re
for line in sys.stdin:
    line = line.replace("。","。\n")
    line = re.sub(r'<[^\]]*>', '', line)
    print(nneo.normalize_neologd(line))

先ほどまでに分解したWikipediaのデータをマージして正規化して分かち書きにします。ここまでの手順で、wikipediaの正規化されたプレーンテキストが手に入りました。
cat corpus/*/* | python3 conv.py | mecab -b 32768 -Owakati -d /usr/local/lib/mecab/dic/mecab-ipadic-neologd > wiki.mecab.txt
mecabのデフォルトのバッファ8192Byteだと「input-buffer overflow. The line is split. use -b #SIZE option.」というエラーが出ますので、-bで32768を指定するとエラーが出なくなりました。実際にはもう少し小さくてもいいのかもしれません。

word2vecをコンパイル

取ってきてmakeします。できた実行ファイルは手動でPATHの通ったところにコピーします。
cd ~/src
git clone https://github.com/dav/word2vec
cd word2vec/src
make
cp -p ../bin/* /usr/local/bin/.



学習とパラメータの概要

ここまで来たら、実行するだけです。下記のコマンドで学習を開始します。
cd ~/src/wikiextractor/
time word2vec -train wiki.mecab.txt -output wiki.mecab.word2vec.size200.bin -size 200 -window 5 -sample 1e-5 -negative 5 -binary 1
学習結果は-outputで指定し、wiki.mecab.binに保存されます。

オプションについて詳しく知りたい場合は、本家word2vecの説明Mikolov氏の論文など有用な情報がいっぱいころがってますのでそちらを読んで確認してください。

ここでは、ざっくりとした概要だけ。
オプション 内容
-train 学習する元となるファイルを指定します。今回は作成したファイルを指定します。
-size ベクトルの次元数を指定します。サンプル同様200を指定しましたが、wikipediaのデータを基とする場合400とか600の方が良いかもしれません。4GBのメモリだと300程度が限界だと思います。400を指定すると途中で強制終了しましたので、急遽仮想マシンのメモリを8GBで割り当てなおしました。
-window 関連性があると判断する前後の単語数。デフォルトのskip-gramなら10くらい。CBOWなら5を指定すればいいようだ。
-sample 出現回数の多い単語を学習の結果から除外するための閾値です。(1e-3〜1e-5の範囲で指定)これは、MeCabで分かち書きをした結果を見ればわかりますが、「の」や「が」や「ある」などどうでもよい語が頻出するためで、このようなものを学習内容から除外することができるみたいです。このQiitaの記事が大変分かりやすかったです。
-negative 指定した数の単語の関連性を低く設定します。低く設定されるのはWindowで指定した値から外れた単語のうちランダムに選ばれた単語になります。(5-10くらい。使わない場合は0を指定。)
-binary 1に指定すると結果をバイナリ形式で保存します。


Distanceで似た単語を探す

さっそく実行してみます。
distance wiki.mecab.word2vec.size200.bin
Enter word or sentence (EXIT to break): Python

Word: Python Position in vocabulary: 24421

Word Cosine distance
-------------------------------------
Perl 0.925127
Java 0.910050
Ruby 0.898116
C++ 0.892958
JavaScript 0.890242
プログラミング言語 0.885551
ライブラリ 0.884654
スクリプト言語 0.879306
PHP 0.878212
Lua 0.876160
C# 0.875964
コンパイラ 0.870334
VB.NET 0.861130
ソースコード 0.860082
C言語 0.858654
C/C++ 0.857195
統合開発環境 0.850669
処理系 0.849689

ふむ。
Enter word or sentence (EXIT to break): C言語

Word: C言語 Position in vocabulary: 16918

Word Cosine distance
-------------------------------------
コンパイラ 0.919928
プログラミング言語 0.915335
C++ 0.909260
処理系 0.893756
C/C++ 0.888579
インタプリタ 0.886861
バイトコード 0.885650
FORTRAN 0.880558
Smalltalk 0.878671
プリプロセッサ 0.877056
Java 0.874642
Haskell 0.873704
Perl 0.873273
Lua 0.873125
D言語 0.871268
高級言語 0.867297
Objective-C 0.867137
結構いい結果が得られているんじゃないでしょうか・・・。

word-analogyで似たベクトルの単語を探す


「A の時は B、C の時は何?」といった関連性が類似する(ベクトルの方向性が同じ)ものを答えてくれます。
日本の時は東京、アメリカの時は?というような内容に答えてくれます。
先ほどの学習結果が使えますので、新たな学習は必要ありません。


word-analogy wiki.mecab.word2vec.size200.bin
Enter three words (EXIT to break): 日本 東京 アメリカ

Word: 日本 Position in vocabulary: 38001
Word: 東京 Position in vocabulary: 348
Word: アメリカ Position in vocabulary: 172

Word Distance
------------------------------------------------------------------------
ニューヨーク 0.658165
アナザー・カントリー 0.626435
フォー・ソーシャルリサーチ 0.611632
米国 0.611108
シンシナティー 0.610880
きちんと正解が1番最初に出てきました。

Enter three words (EXIT to break): ボストン アメリカ バルセロナ

Word: ボストン Position in vocabulary: 4817
Word: アメリカ Position in vocabulary: 172
Word: バルセロナ Position in vocabulary: 7925

Word Distance
------------------------------------------------------------------------
スペイン 0.733546
マドリード 0.644633
イタリア 0.634427
ブラジル 0.631908
フランス 0.630570
これも正しい回答が一番最初に表示されました。

Enter three words (EXIT to break): 日本 東京 中国

Word: 日本 Position in vocabulary: 55
Word: 東京 Position in vocabulary: 348
Word: 中国 Position in vocabulary: 329

Word Distance
------------------------------------------------------------------------
北京 0.674370
上海 0.644645
杭州 0.610560
清華大学 0.595791
天津 0.593921
こういうロケーションにかかる部分は得意みたいです。
ほとんど、間違えなく答えが出てきます。

次に失敗してしまったパターンを・・・
Enter three words (EXIT to break): コンピュータ CPU 人間

Word: コンピュータ Position in vocabulary: 1816
Word: CPU Position in vocabulary: 4492
Word: 人間 Position in vocabulary: 500

Word Distance
------------------------------------------------------------------------
ラグズ 0.632038
ベオク 0.616553
破壊衝動 0.605122
ヤセイ 0.604995
スラード 0.604204
ソウルジェム 0.604126
ノドス 0.597569
凶暴性 0.595487
神殺し 0.592517
悪の力 0.590891
イリシッド 0.590625
卑小 0.590289
生命力 0.589909
紅世の王 0.589552
ケンク 0.587762
天空聖者 0.587749
虚無 0.583919
ゾラーク・ゾラーン 0.581554
大魔王バーン 0.581542
シャダーカイ 0.580933
レオモン 0.580893
ビシャモン 0.580755
人間らしい 0.580128
血肉 0.579877
獣性 0.579627
ええと、ほとんどなに言ってるのかわからないんですけど・・・。
これは、脳と答えてほしかった;;
なんとか、「血肉」が答えらしいでしょうかね・・・。

近いベクトルの単語を探してくるだけですので、もう少し次元を増やしたりすることで何とかなるのかもしれません。

Word2Phrase

これは、フレーズを検出してくれるプログラムです。フレーズとして扱うべきものを「_」で繋げることによって、word2vecでは単語として認識されます。例えば、分かち書きで「日本」「語」と2つの語に分割されてしまったものを「_」で繋げて「日本_語」とすることで、1つの単位として扱います。

複数回実行することでより長い単語を連結することができますが、本来はコードに手を入れた方が良いであろうことと、回数を重ねすぎると連結してほしくないところまで連結し始めますので、注意が必要です。
ではやってみます。

1回目

time word2phrase -train wiki.mecab.txt -output wiki.mecab.phrases.txt -threshold 500 -debug 2
Starting training using file wiki.mecab.txt
Words processed: 449700K Vocab size: 39165K
Vocab size (unigrams + bigrams): 22711828
Words in train file: 449716604
Words written: 449700K
real 113m6.868s
user 39m28.540s
sys 25m15.209s
出来上がったテキストファイルを開いてみると、下記のような単語が見つかりました。分かち書きでは「イスラム」「圏」と2単語に分かれてしまいましたが、word2phraseで「_」で連結されていることが確認できます。
アルバニア_語
古代_ギリシャ語
ポルトガル語_圏
イスラム_圏
体性感覚_野
日本語_学習者
出来上がったテキストを、もう一度word2phraseにかけてみます。

2回目

time word2phrase -train wiki.mecab.phrases.txt -output wiki.mecab.phrases2.txt -threshold 500 -debug 2
Starting training using file wiki.mecab.phrases.txt
Words processed: 437300K Vocab size: 43443K
Vocab size (unigrams + bigrams): 25057778
Words in train file: 437363941
Words written: 437300K
real 130m48.335s
user 27m41.600s
sys 47m3.203s
先ほどからさらに連結された単語の長さが増えていることが確認できます。「GHOST_IN_THE_SHELL/攻殻機動隊」を統計で認識できるのはすごい!と思いました。
十_六_進
千_数_百
カダイ_語_族
上位_10位_以内
被_修飾_語
WBC_世界_ヘビー級_王者
数_百_万_円
GHOST_IN_THE_SHELL/攻殻機動隊
単行_本_未_収録
大阪_芸術大学_芸術学部_映像
法起寺_式_伽藍_配置
武王_伐_紂_平話
先ほど作成されたテキストを、さらにもう一度word2phraseにかけてみます。

3回目

time word2phrase -train wiki.mecab.phrases2.txt -output wiki.mecab.phrases3.txt -threshold 500 -debug 2
Starting training using file wiki.mecab.phrases2.txt
Words processed: 433600K Vocab size: 44808K
Vocab size (unigrams + bigrams): 25779937
Words in train file: 433666534
Words written: 433600K
real 166m28.564s
user 27m51.933s
sys 64m7.211s
ちょっと、やりすぎ感が出てきました。そこまで連結しなくとも機械学習で認識できるであろう物と関連性のない単語が連結されている部分が増えてきたように思います。2回目の「十_六_進」までは良いと思うけど、「十_六_進_表記」まで連結する必要はないでしょう・・・。
十_六_進_表記
家庭_用_電気_機械_器具
オーストリア_福音主義_教会_アウクスブルク信仰告白_派
全世界_ウェイト_制_空手道_選手権_大会
大阪_芸術大学_芸術学部_美術_学科
全国_公営_競馬_主催者_協議会_会長
ここで出来上がったファイルをもとにword2vecで学習させると、また異なった結果が得られると思います。
「_」は必要ないので削ってしまってもよいのかもしれません。