CUBE SUGAR CONTAINER

技術系のこと書きます。

Linux で VXLAN を扱ってみる

久しぶりに VXLAN について調べたところ、カーネルの機能で VXLAN インターフェイスが作れるようになってたので試してみた。 ここでいう VXLAN というのは、RFC7348 で定義されている Virtual eXtensible Local Area Network というプロトコルを指す。 このプロトコルを使うと Layer 2 のトンネリングが実現できる。

使った環境は次の通り。

$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=18.04
DISTRIB_CODENAME=bionic
DISTRIB_DESCRIPTION="Ubuntu 18.04.4 LTS"
$ uname -r
4.15.0-76-generic
$ dpkg -l | grep iproute2
ii  iproute2                             4.15.0-2ubuntu1                     amd64        networking and traffic control tools
$ lsmod | grep -i vxlan
vxlan                  57344  0
ip6_udp_tunnel         16384  1 vxlan
udp_tunnel             16384  1 vxlan

もくじ

ネットワークの構成

検証には Network Namespace と veth インターフェイスを使って作ったネットワークを用いる。

ネットワークの物理的な構成は次のとおり。

f:id:momijiame:20200208004155p:plain
物理的なネットワーク構成

ネットワークは、全部で 3 つの Network Namespace からできている。 真ん中の router はルータとして動作し、残りの ns1ns2 はホストとして動作する。 端点をもった線はインターフェイスのつながりを表している。 その中でも、緑の破線はトンネリングによって仮想的なつながりがあることを示している。

ネットワークの論理的な構成は次のとおり。

f:id:momijiame:20200208004242p:plain
論理的なネットワーク構成

ネットワークは、全部で 3 つのセグメントからできている。 192.0.2.0/24198.51.100.0/24 は、通常のルーティングをする。 対して、203.0.113.0/24 は VXLAN によって延伸されたブロードキャストドメインで動作する。

下準備

あらかじめ、必要なパッケージをひととおりインストールしておく。

$ sudo apt-get -y install iproute2 iputils-ping tcpdump

ネットワークを作る

まずは Network Namespace を用意する。

$ sudo ip netns add ns1
$ sudo ip netns add router
$ sudo ip netns add ns2

つづいて veth インターフェイスを用意する。

$ sudo ip link add ns1-veth0 type veth peer name gw-veth0
$ sudo ip link add ns2-veth0 type veth peer name gw-veth1

作成した veth インターフェイスを Network Namespace に所属させていく。

$ sudo ip link set ns1-veth0 netns ns1
$ sudo ip link set gw-veth0 netns router
$ sudo ip link set gw-veth1 netns router
$ sudo ip link set ns2-veth0 netns ns2

インターフェイスの状態を UP に設定する。

$ sudo ip netns exec ns1 ip link set ns1-veth0 up
$ sudo ip netns exec router ip link set gw-veth0 up
$ sudo ip netns exec router ip link set gw-veth1 up
$ sudo ip netns exec ns2 ip link set ns2-veth0 up

インターフェイスに IP アドレスを付与する。

$ sudo ip netns exec ns1 ip address add 192.0.2.1/24 dev ns1-veth0
$ sudo ip netns exec router ip address add 192.0.2.254/24 dev gw-veth0
$ sudo ip netns exec router ip address add 198.51.100.254/24 dev gw-veth1
$ sudo ip netns exec ns2 ip address add 198.51.100.1/24 dev ns2-veth0

ns1ns2 のデフォルトルートを router の IP アドレスに向ける。

$ sudo ip netns exec ns1 ip route add default via 192.0.2.254
$ sudo ip netns exec ns2 ip route add default via 198.51.100.254

rotuer がルータとして動作するようにカーネルパラメータを設定する。

$ sudo ip netns exec router sysctl net.ipv4.ip_forward=1

ひとまず、通常のルーティングが動作することを確認しておく。

$ sudo ip netns exec ns1 ping -c 3 198.51.100.1 -I 192.0.2.1
PING 198.51.100.1 (198.51.100.1) from 192.0.2.1 : 56(84) bytes of data.
64 bytes from 198.51.100.1: icmp_seq=1 ttl=63 time=0.217 ms
64 bytes from 198.51.100.1: icmp_seq=2 ttl=63 time=0.043 ms
64 bytes from 198.51.100.1: icmp_seq=3 ttl=63 time=0.191 ms

--- 198.51.100.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2026ms
rtt min/avg/max/mdev = 0.043/0.150/0.217/0.077 ms

VXLAN を設定する

つづいて、今回の主眼である VXLAN のインターフェイスを用意する。 VNI には 100 を使った。

まずは ns1 に VXLAN インターフェイスを作る。

$ sudo ip netns exec ns1 \
     ip link add ns1-vxlan0 \
     type vxlan \
     id 100 \
     remote 198.51.100.1 \
     dstport 4789 \
     dev ns1-veth0

つづいて ns2 にも VXLAN インターフェイスを作る。

$ sudo ip netns exec ns2 \
     ip link add ns2-vxlan0 \
     type vxlan \
     id 100 \
     remote 192.0.2.1 \
     dstport 4789 \
     dev ns2-veth0

あとは、作った VXLAN インターフェイスに IP アドレスを付与したら状態を UP に設定するだけ。

$ sudo ip netns exec ns1 ip link set ns1-vxlan0 up
$ sudo ip netns exec ns1 ip address add 203.0.113.1/24 dev ns1-vxlan0
$ sudo ip netns exec ns2 ip link set ns2-vxlan0 up
$ sudo ip netns exec ns2 ip address add 203.0.113.2/24 dev ns2-vxlan0

パケットキャプチャするために ns2 のインターフェイスに tcpdump をしかけておく。

$ sudo ip netns exec ns2 tcpdump -tnl -i ns2-veth0
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on ns2-veth0, link-type EN10MB (Ethernet), capture size 262144 bytes

ns1 から ns2 に向けて Ping を打つ。 指定する IP アドレスは VXLAN インターフェイスのものを使う。

$ sudo ip netns exec ns1 ping -c 3 203.0.113.2 -I 203.0.113.1
PING 203.0.113.2 (203.0.113.2) from 203.0.113.1 : 56(84) bytes of data.
64 bytes from 203.0.113.2: icmp_seq=1 ttl=64 time=0.541 ms
64 bytes from 203.0.113.2: icmp_seq=2 ttl=64 time=0.094 ms
64 bytes from 203.0.113.2: icmp_seq=3 ttl=64 time=0.074 ms

--- 203.0.113.2 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2010ms
rtt min/avg/max/mdev = 0.074/0.236/0.541/0.215 ms

ちゃんとトンネリングが成功して、Ping の疎通がある。

先ほどしかけた tcpdump にも、VXLAN のパケットがキャプチャされている。 VXLAN のペイロードになっているパケット (フレーム) も一度に確認できるようだ。

$ sudo ip netns exec ns2 tcpdump -tnl -i ns2-veth0
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on ns2-veth0, link-type EN10MB (Ethernet), capture size 262144 bytes
IP 192.0.2.1.52760 > 198.51.100.1.4789: VXLAN, flags [I] (0x08), vni 100
IP 203.0.113.1 > 203.0.113.2: ICMP echo request, id 9404, seq 1, length 64
IP 198.51.100.1.52760 > 192.0.2.1.4789: VXLAN, flags [I] (0x08), vni 100
IP 203.0.113.2 > 203.0.113.1: ICMP echo reply, id 9404, seq 1, length 64
IP 192.0.2.1.52760 > 198.51.100.1.4789: VXLAN, flags [I] (0x08), vni 100
IP 203.0.113.1 > 203.0.113.2: ICMP echo request, id 9404, seq 2, length 64
IP 198.51.100.1.52760 > 192.0.2.1.4789: VXLAN, flags [I] (0x08), vni 100
IP 203.0.113.2 > 203.0.113.1: ICMP echo reply, id 9404, seq 2, length 64
IP 192.0.2.1.52760 > 198.51.100.1.4789: VXLAN, flags [I] (0x08), vni 100
IP 203.0.113.1 > 203.0.113.2: ICMP echo request, id 9404, seq 3, length 64
IP 198.51.100.1.52760 > 192.0.2.1.4789: VXLAN, flags [I] (0x08), vni 100
IP 203.0.113.2 > 203.0.113.1: ICMP echo reply, id 9404, seq 3, length 64

VXLAN インターフェイスの方で tcpdump をかけると、もちろん ICMP しか観測できない。

$ sudo ip netns exec ns2 tcpdump -tnl -i ns2-vxlan0
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on ns2-vxlan0, link-type EN10MB (Ethernet), capture size 262144 bytes
IP 203.0.113.1 > 203.0.113.2: ICMP echo request, id 9408, seq 1, length 64
IP 203.0.113.2 > 203.0.113.1: ICMP echo reply, id 9408, seq 1, length 64
IP 203.0.113.1 > 203.0.113.2: ICMP echo request, id 9408, seq 2, length 64
IP 203.0.113.2 > 203.0.113.1: ICMP echo reply, id 9408, seq 2, length 64
IP 203.0.113.1 > 203.0.113.2: ICMP echo request, id 9408, seq 3, length 64
IP 203.0.113.2 > 203.0.113.1: ICMP echo reply, id 9408, seq 3, length 64

いじょう。

参考文献

tools.ietf.org

https://www.kernel.org/doc/Documentation/networking/vxlan.txt

備考

ネットワークの物理的な構成を図示するのに使った blockdiag の定義は次のとおり。

blockdiag {
  ns1-vxlan0 [shape = endpoint];
  ns1-veth0 [shape = minidiamond];
  gw-veth0 [shape = minidiamond];
  gw-veth1 [shape = minidiamond];
  ns2-veth0 [shape = minidiamond];
  ns2-vxlan0 [shape = endpoint];

  group ns1 {
    orientation = portrait;
    label = 'ns1';
    color = '#CCCCFF';
    shape = line;
    ns1-veth0;
    ns1-vxlan0;
  }

  group router {
    label = 'router';
    color = '#CCCCFF';
    shape = line;
    gw-veth0;
    gw-veth1;
  }

  group ns2 {
    orientation = portrait;
    label = 'ns2';
    color = '#CCCCFF';
    shape = line;
    ns2-veth0;
    ns2-vxlan0;
  }

  ns1-vxlan0 -- ns1-veth0 [style = dotted];
  ns1-veth0 -- gw-veth0;
  gw-veth1 -- ns2-veth0;
  ns2-veth0 -- ns2-vxlan0 [style = dotted];
  ns1-vxlan0 -- ns2-vxlan0 [color = '#77FF77', style = dashed];
}

ネットワークの論理的な構成を図示するのに使った nwdiag の定義は次のとおり。

nwdiag {

  network {
    address = '192.0.2.0/24';
    ns1[address = 'ns1-veth0, 192.0.2.1'];
    router[address = 'gw-veth0, 192.0.2.254'];
  }

  network {
    address = '198.51.100.0/24';
    router[address = 'gw-veth1, 198.51.100.254'];
    ns2[address = 'ns2-veth0, 198.51.100.1'];
  }

  network {
    address = '203.0.113.0/24';
    ns1[address = 'ns1-vxlan0, 203.0.113.1'];
    ns2[address = 'ns2-vxlan0, 203.0.113.2'];
  }
}

Lenovo ThinkPad E595 を買ってメモリとストレージを交換してみた

我が家には、10 年前に購入した、OS が Windows 7 のノートパソコンが 1 台あった。 活躍する機会はさほど多くないものの、EOL を迎える製品を使い続けるリスクを考えて、以前からリプレースの機会をうかがっていた 1。 今回は、その買いかえに関する備忘録について書いてみる。

もくじ

次の機種に求められる要件について

まず、購入するからには長く使いたい。 そのため、ハードウェアの要件については次のように定めた。

  • CPU は 4 コア以上
  • メモリは 8GB 以上
  • ストレージは SSD で 256GB 以上
  • ディスプレイの解像度はフル HD 以上

また、互換性を考えて純正の Microsoft Office が使いたい。 できれば、バンドル版が安く提供されていると嬉しかった。

ただ、バンドル版か否かに関わらず、Microsoft Office の購入費用を加えて上記のスペックを満たしたものとなると、大抵の製品は 10 万円をこえる。 これだと、あまりお値打ち感はない。 そうした中で、なかなか機種が選定できずにいたものの、あるとき次の機種が目にとまった。

kakaku.com

これは、コストパフォーマンスを重視した ThinkPad の E シリーズで、15 インチのディスプレイを持ったモデル。 これをカスタマイズして、ハードウェアを最小構成 2 にした上で Microsoft Office をバンドルすると、5.7 万円で買えることがわかった 3

で、ここからが本題なんだけど、このモデルはメモリやストレージがオンボードになっていない。 また、公式通販でもさまざまな組み合わせでカスタマイズできるとおり、パーツの相性が特に厳しいということもないようだ。 そのため、Web を調べると、メモリやストレージを交換して使っている人が割と見つかる。 なるほど、であればこれに自分でパーツを入れかえて使えば安くあがるのでは?と考えて試算してみた。

ちなみに、この時点ではまだ ThinkPad に E495 というモデルがあることを知らなかった。 こちらは、同じシリーズの 14 インチで、E595 よりも重量が 350g ほど軽い。 筐体のサイズ以外には値段もほとんど変わらないので、今ならこちらを選んでいたかもしれない。

kakaku.com

なお、E495 の場合でも使えるパーツの規格は変わらない。 また、パーツの交換に必要な手順も基本的に同じはず。 ただし、背面パネルにはあらかじめ NVMe SSD のヒートシンクが付属しているらしい。

適合するパーツを選ぶ

まず、メモリは DDR4-2400 の SO-DIMM が使える。 なお、DDR4 は基本的に上位互換があるので、似たような値段の DDR4-2666 を選んでも問題ない。 要件を上回る 16GB を買っても、今なら ¥8,000 前後から手に入る。 メモリのスロットは 2 つあるので、デュアルチャネルを活かすために 8GB のモジュールを 2 枚買う。 ちなみに、ThinkPad E595 は最大で合計 32GB までのメモリに対応している。

続いて、ストレージとしてはファームファクタが M.2 2280 の NVMe SSD を選ぶ。 こちらも、要件を上回る 500GB のモデルを買っても、今なら ¥8,000 前後から手に入る。

なお、M.2 タイプの SSD は発熱がはげしいので、ただ読み書きのパフォーマンスが良いモデルを選べば良いわけではない。 ノートパソコンに関しては、デスクトップよりも排熱が問題になりやすい。 今回も、パフォーマンスは抑えめのモデルを選んだ上で、気休めに次のヒートシンクを貼り付けることにした。 ノートパソコンの場合、筐体の余裕から使えるヒートシンクの厚みが限られる点に注意が必要になる。 このヒートシンクであれば、サイドパネルを外すことでかなり薄くなるようだ。

なお、筐体には SATA の 2.5 インチスロットも用意されている。 そのため、読み書きの速度はインターフェイス的にやや落ちる 4 ものの、こちらを選んでも問題ない。

ということで、次の要件を満たしつつ 7.5 万円ほどで買えることがわかった。

  • CPU は 4 コア 8 スレッド (Ryzen 5 3500U)
  • メモリは 16GB
  • ストレージは 500GB の NVMe SSD
  • バンドル版 の Microsoft Office 2019 (Home and Business)
  • ディスプレイの解像度はフル HD

購入した後の手順について

試算にもとづいて、本体とパーツをひととおり購入した。 Lenovo のブランドになってからは、はじめての ThinkPad になる。

ここからは、パーツを交換する手順を見ていこう。

リカバリ用の USB メモリを作る

リカバリ用の USB メモリ (USB リカバリードライブ) を作成する手順は以下のページに記載されている。

pcsupport.lenovo.com

必要な手順は次のとおり。

  1. Lenovo リカバリーページ5 にアクセスしてリカバリーメディアを注文する
  2. Lenovo USB Recovery Creator ツールをダウンロードする
  3. Lenovo USB Recovery Creator ツールを実行する
  4. (1) で注文したリカバリーメディアを選択してUSBリカバリードライブを作成する

上記の手順を実施して、あらかじめリカバリ用の USB メモリを作っておく。 Web には Windows のリカバリメディアを使う方法を試している人もいるけど、こちらの方が完全に製品出荷時の状態にできるので良いと思う。 使う USB メモリは余裕を持って 64GB 以上のものを選んだ方が良いと思う。

パーツを交換する

パーツを交換する前には、あらかじめバッテリーの給電を含めて完全に電源を落としておく。 それには、起動画面で Lenovo のロゴが出ているときに F1 キーを押して BIOS 画面に入る。 Config > Power > Disable built-in buttery を Disabled にして電源を完全に切る。

このモデルは、筐体の開け方にややクセがある。 ドライバーで裏側のネジをすべてゆるめただけでは、底面のパネルが取れない。 サイドにある隙間にヘラなどを差し込んで、固定されているツメをぐるっと一周すべて外す必要がある。 YouTube なんかに、開け方の動画が上がってるので参考にすると良い。

底面パネルを外すと、こんな感じ。

あらかじめインストールされているメモリのモジュールを外して、購入したモジュールを差し込む。 そして、M.2 のスロットには、ヒートシンクを貼り付けた SSD を固定する。 デフォルトの 2.5 インチ HDD は、外すと 100g ほど軽量化になるけど、せっかくなのでそのままにした。

用意した USB メモリを使ってリカバリする

パーツの交換がおわったら、リカバリ用の USB メモリを挿した状態で起動して、また BIOS 画面に入る。 そして、Startup > Boot から起動するデバイスの順序でリカバリ用の USB メモリをもっとも上にした状態で F10 を押下して起動する。 すると、リカバリがはじまるのであとはしばらく待つだけ。

なお、注意点としてストレージの中に OS がインストールされているものが残っているとリカバリがうまくいかない。 そのため、リカバリツールでは思い切って既存のすべてのストレージを消去するオプションを選択する必要がある。 あるいは、パーツを交換する作業で既存のストレージを取り外してしまうのも手だと思う。

つかう

リカバリがおわったら、あとは Windows 10 をセットアップして使う。 なお、気になる人は、UEFI に対応した Memtest86+ を使ってメモリに初期不良がないか確かめておくと良いと思う。

www.memtest.org

はじめに、負荷をかけたときの M.2 SSD の発熱が気になっていたのでベンチマークした結果は次のとおり。 負荷をかけた状態で 50℃ いくかいかないか、負荷をかけない状態なら 38℃ 前後で落ち着いた。

なお、Microsoft Office に関してはリカバリツールではインストールされない。 そのため、Microsoft にアカウントを作ってプロダクトキーを入力してメディアをダウンロードする必要がある。 Microsoft Office のプロダクトキーは、本体の取扱説明書なんかと一緒に入っているので捨てないように注意しよう。

www.office.com

リカバリ直後にインストールされるアプリケーションに関しては、日本のメーカーのように謎のソフトが山のように入る、ということはなかった。 デフォルトでノートンアンチウイルスが入る点は余計と感じたものの、アンインストールしてしまえば問題はない。

Lenovo Vantage という管理ソフトに関しては、なかなかよくできていると感じた。 このソフトを使うと、たとえばバッテリーの充電が自動で中止される閾値を設定できる。 リチウムイオンバッテリーは 50 ~ 75% くらい充電した状態で使うと、もっとも長持ちすると言われている。 反対に 0% や 100% 前後で使っているとバッテリーが劣化する原因になるので、この機能は地味にうれしかった。 また、ハードウェアに固有の部分、BIOS やドライバのアップデートがひとつのソフトウェアで管理できるようになっている。

いじょう。


  1. OS を Windows 10 にして使い続ける、という選択肢もなくはなかったもののの、すでにストレージを SSD に交換するなど延命措置をとった上でバッテリーなどのヘタりもはげしく先が見えていた
  2. メモリを 4GB にして、ストレージから SSD をなくして代わりに 500GB の HDD にする
  3. ThinkPad の公式通販は定期的に値段を上げたり下げたりキャンペーンをよくやるので、現時点 (2020/02/02) ではもう少し高くなる (6.3 万円)
  4. とはいえ、転送速度で 500MB/s と 1500MB/s の違いを体感できるか?というと、おそらく一般的なユースケースでは難しい
  5. https://pcsupport.lenovo.com/jp/ja/lenovorecovery

VirtualBox で仮想マシンが入れ子 (Nested Virtualization) できるようになった

先日リリースされた VirtualBox 6.0 からは AMD の CPU で、6.1 からは Intel の CPU で Nested Virtualization がサポートされた。 Nested Virtualization というのは、仮想マシンの中に仮想マシンを入れ子に作ることを指す。 ようするに、仮想マシンをマトリョーシカのようにする。 この機能は、すでに VMware や KVM といったハイパーバイザではサポートされていたものの、今回それが VirtualBox でも使えるようになったというわけ。 この機能があると、サーバ周りのインフラ系をやっている人たちは、検証環境が作りやすくなってうれしい。 ただし、この機能を実現するには、仮想マシンの中で CPU の仮想化支援機能 (Intel-VT / AMD-V) が有効になっている必要がある 1

VirtualBox 6.1 のリリースノート 2 を見ると、次のような記載がある。

Virtualization core: Support for nested hardware-virtualization on Intel CPUs (starting with 5th generation Core i, codename Broadwell), so far tested only with guest running VirtualBox

どうやら、Intel であれば第 5 世代 Core i 以降の CPU で仮想化支援機能を使った Nested Virtualization ができるようになったらしい。 このニュースは、個人的に感慨深いものだった。 というのも、次のチケットを見てもらいたい。

www.virtualbox.org

このチケットは、VirtualBox に Nested Virtualization の機能を要望したものになっている。 問題は、チケットが作成された日付で、見ると "Opened 11 years ago" とある。 つまり、11 年という歳月をこえて、ユーザに要望されてきた機能がついに実現したというわけ。 ちなみに、これまで開発側の反応はどうだったかというと、チケットには「便利だろうけど実装するの大変だから...」みたいなコメントがあった。 なお、この機能について自分で調べていた頃のブログを調べると、ポストした日付が 8 年前になっていた。

Mac で仮想マシンの入れ子 (Nested Virtualization) をする | CUBE SUGAR STORAGEmomijiame.tumblr.com

今回は、せっかくなので VirtualBox を使った Nested Virtualization を試してみる。 使った環境は次のとおり。

$ sw_vers       
ProductName:    Mac OS X
ProductVersion: 10.14.6
BuildVersion:   18G2022
$ sysctl -a | grep brand_string
machdep.cpu.brand_string: Intel(R) Core(TM) m3-7Y32 CPU @ 1.10GHz
$ vagrant version | head -n 1
Installed Version: 2.2.7
$ vboxmanage | head -n 1
Oracle VM VirtualBox Command Line Management Interface Version 6.1.2

もくじ

下準備

はじめに、Homebrew を使って Vagrant と VirtualBox をインストールしておく。 もちろん、Vagrant を使わずに VirtualBox の GUI フロントエンドを使ってもかまわない。

$ brew cask install vagrant virtualbox

Vagrant + VirtualBox で仮想マシンを用意する (L1)

物理的なハードウェア上で直接動作する仮想化のことを L1 と呼ぶことがあるようだ。 ようするに、一般的な状況としての仮想マシンがこれ。 まずは L1 の仮想マシンとして Vagrant + VirtualBox を使って Ubuntu 18.04 LTS をインストールする。

仮想マシンのイメージをダウンロードしたら、設定ファイルを生成する。

$ vagrant box add ubuntu/bionic64
$ vagrant init ubuntu/bionic64

次のように Vagrant の設定ファイルができる。

$ head Vagrantfile 
# -*- mode: ruby -*-
# vi: set ft=ruby :

# All Vagrant configuration is done below. The "2" in Vagrant.configure
# configures the configuration version (we support older styles for
# backwards compatibility). Please don't change it unless you know what
# you're doing.
Vagrant.configure("2") do |config|
  # The most common configuration options are documented and commented below.
  # For a complete reference, please see the online documentation at

ここで、設定ファイルを編集する必要がある。 ポイントは最後の vb.customize に渡している引数の --nested-hw-virt on で、これがないと L1 の仮想マシンで CPU の仮想化支援機能が有効にならない。 あと、Nested Virtulization をするには、かなり処理のオーバーヘッドがあるので仮想マシンのリソースは多めに確保しておいた方が良い。

  config.vm.provider "virtualbox" do |vb|
    vb.cpus = "2"
    vb.memory = "2048"
    vb.customize ["modifyvm", :id, "--nested-hw-virt", "on"]
  end

なお、Vagrant ではなく VirtualBox の GUI フロントエンドを使って操作しているときは、仮想マシンの設定画面を開いて次の項目にチェックをつければ良い。

f:id:momijiame:20200202080627p:plain
VirtualBox で Nested Virtualization するのに必要な GUI 設定画面のチェック項目

仮想マシンを起動したらログインする。

$ vagrant up
$ vagrant ssh

これで L1 の仮想マシンとして Ubuntu 18.04 LTS が利用できるようになった。

$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=18.04
DISTRIB_CODENAME=bionic
DISTRIB_DESCRIPTION="Ubuntu 18.04.3 LTS"
$ uname -r
4.15.0-72-generic

CPU に仮想化支援機能のフラグが立っていることを確認する

それでは、CPU に仮想化支援機能のフラグが立っていることを確認してみよう。 Linux では proc ファイルシステムの /proc/cpuinfo で CPU のフラグが確認できる。 今回使っているのは Intel の CPU なので "vmx" というフラグを探す。

$ grep vmx /proc/cpuinfo
flags    : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx rdtscp lm constant_tsc rep_good nopl xtopology nonstop_tsc cpuid tsc_known_freq pni pclmulqdq vmx ssse3 cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt aes xsave avx rdrand hypervisor lahf_lm abm 3dnowprefetch invpcid_single pti tpr_shadow flexpriority fsgsbase avx2 invpcid rdseed clflushopt md_clear flush_l1d
flags    : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx rdtscp lm constant_tsc rep_good nopl xtopology nonstop_tsc cpuid tsc_known_freq pni pclmulqdq vmx ssse3 cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt aes xsave avx rdrand hypervisor lahf_lm abm 3dnowprefetch invpcid_single pti tpr_shadow flexpriority fsgsbase avx2 invpcid rdseed clflushopt md_clear flush_l1d

ちゃんと "vmx" フラグが立っていることがわかった。

仮想マシンの中に Vagrant + Libvirt (KVM) で仮想マシンを作る (L2)

続いては、L1 の仮想マシンの中に、さらに仮想マシンを作る。 先ほどのチケットには L1 / L2 共に VirtualBox を使った検証しかしていない、とあった。 そこで、せっかくなので L2 に KVM を使っても動くのかどうか調べてみることにした。 使う環境としては Libvirt 経由で KVM を Vagrant から扱う。

まずは必要なパッケージをインストールしておく。

$ sudo apt-get update
$ sudo apt-get -y install vagrant-libvirt qemu-kvm libvirt-bin gawk

KVM が使える状態になっていることを kvm-ok コマンドや、カーネルモジュールがロードされていることから確認する。

$ kvm-ok
INFO: /dev/kvm exists
KVM acceleration can be used
$ lsmod | grep kvm
kvm_intel             217088  0
kvm                   610304  1 kvm_intel
irqbypass              16384  1 kvm

現在のユーザを libvirt および kvm グループに参加させる。

$ sudo usermod -aG libvirt,kvm $(who am i | awk '{print $1}')

ここで、いったん L1 の仮想マシンを再起動しておく。

$ exit
$ vagrant reload

そして、もう一度 L1 の仮想マシンにログインする。

$ vagrant ssh

L2 の仮想マシンとしては、違いがわかりやすいように CentOS 7 を使うことにした。 次のようにして仮想マシンを起動する。

$ vagrant box add centos/7 --provider=libvirt
$ vagrant init centos/7
$ vagrant up

ちなみに、前述したとおり Nested Virtualization はオーバーヘッドが大きいので、この作業には大変に時間がかかる。 作業の進捗状況を確認したいときは、次のようにして仮想マシンのコンソールを取って見ると良い。

$ virsh list
$ virsh console <name>

仮想マシンが起動したら、ログインする。

$ vagrant ssh

確認すると、ちゃんと CentOS 7 が動作している。 これで、macOS / Ubuntu 18.04 LTS / CentOS 7 という仮想マシンのマトリョーシカが完成した。

$ cat /etc/redhat-release 
CentOS Linux release 7.6.1810 (Core) 
$ uname -r
3.10.0-957.12.2.el7.x86_64

なんとも感慨深い。

OpenStack 実践ガイド (impress top gear)

OpenStack 実践ガイド (impress top gear)

  • 作者:古賀 政純
  • 出版社/メーカー: インプレス
  • 発売日: 2016/08/25
  • メディア: 単行本(ソフトカバー)


  1. 完全仮想化をサポートしたハイパーバイザ (Xen など) であれば、その限りではないものの遅い

  2. https://www.virtualbox.org/wiki/Changelog-6.1

Ubuntu 18.04 LTS で Sphinx の PDF をビルドする

今回は Ubuntu 18.04 LTS を使って、Sphinx の PDF をビルドする方法について。

使った環境は次のとおり。

$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=18.04
DISTRIB_CODENAME=bionic
DISTRIB_DESCRIPTION="Ubuntu 18.04.3 LTS"
$ uname -r
4.15.0-72-generic

下準備

Sphinx は TeX を使って PDF をビルドするんだけど、TeX Live のパッケージはかなり大きい。 そのため、APT でミラーリポジトリを使えるようにしておいた方が良い。

blog.amedama.jp

以下のコマンドを実行すればミラーリポジトリが有効になる。

$ sudo sed -i.bak -e 's%http://[^ ]\+%mirror://mirrors.ubuntu.com/mirrors.txt%g' /etc/apt/sources.list

Sphinx をインストールする

つづいて、Sphinx をインストールする。 最新のバージョンを使いたいので PIP からインストールすることにした。

まずは PIP をインストールする。

$ sudo apt update
$ sudo apt -y install \
      python3-pip \
      python3-setuptools \
      python3-wheel

PIP を使って、最新の Sphinx をインストールする。

$ sudo pip3 install sphinx

もし、バージョンが少し古くても構わないのであれば、次のように APT を使ってインストールすることもできる。

$ sudo apt -y install python3-sphinx

Sphinx のプロジェクトを作成する

sphinx-quickstart コマンドを使ってプロジェクトのテンプレートを作る。

$ sphinx-quickstart

ウィザード形式でプロジェクトの設定を聞かれるので答えていく。

Sphinx のプロジェクトに設定を追加する

日本語を含む PDF をビルドするときは、Sphinx の設定ファイル conf.py に最低限次の設定をした方が良いようだ。

language = 'ja'
latex_docclass = {'manual': 'jsbook'}

TeX Live をインストールする

つづいて、PDF をビルドするのに必要な TeX Live の関連パッケージをインストールする。

$ sudo apt -y install \
   texlive-latex-recommended \
   texlive-latex-extra \
   texlive-fonts-recommended \
   texlive-fonts-extra \
   texlive-lang-japanese \
   texlive-lang-cjk \
   latexmk

あとからパッケージが足りなくてつらい思いをしたくないときは、次のようにしてすべてのパッケージを入れてしまっても良い。 ただし、インストールするのにめちゃくちゃ時間がかかる。

$ sudo apt -y install texlive-full

PDF をビルドする

あとは make コマンドを使って latexpdf ターゲットを実行するだけ。

$ make latexpdf

実行がおわったら、成果物が入るディレクトリに PDF のファイルができているはず。

$ file _build/latex/*.pdf
_build/latex/example.pdf: PDF document, version 1.5

いじょう。

参考文献

sphinx-users.jp

sphinx-users.jp

Ubuntu 18.04 LTS で利用できるフォントの一覧を得る

今回は、Ubuntu 18.04 LTS で利用できるフォントの一覧を得る方法について。 結論から先に述べると fc-list コマンドを使えば良い。

使った環境は次のとおり。 ちなみに、相当古い Ubuntu でも同じ方法が使えるみたい。

$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=18.04
DISTRIB_CODENAME=bionic
DISTRIB_DESCRIPTION="Ubuntu 18.04.3 LTS"
$ uname -r
4.15.0-74-generic

もくじ

下準備

下準備として fontconfig パッケージをインストールしておく。

$ sudo apt -y install fontconfig

利用できるフォントの一覧を得る

準備ができたら fc-list コマンドを実行する。 すると、利用できるフォントと、そのパスが一覧で得られる。

$ fc-list
/usr/share/fonts/truetype/dejavu/DejaVuSerif-Bold.ttf: DejaVu Serif:style=Bold
/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf: DejaVu Sans Mono:style=Book
/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf: DejaVu Sans:style=Book
/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf: DejaVu Sans:style=Bold
/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf: DejaVu Sans Mono:style=Bold
/usr/share/fonts/truetype/dejavu/DejaVuSerif.ttf: DejaVu Serif:style=Book

フォントを追加してみる

試しに IPAex フォントを追加でインストールして増えることを確認してみよう。

$ sudo apt -y install fonts-ipaexfont

もう一度 fc-list コマンドを実行すると、ちゃんと IPAex フォントが増えていることがわかる。

$ fc-list
/usr/share/fonts/truetype/dejavu/DejaVuSerif-Bold.ttf: DejaVu Serif:style=Bold
/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf: DejaVu Sans Mono:style=Book
/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf: DejaVu Sans:style=Book
/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf: DejaVu Sans:style=Bold
/usr/share/fonts/opentype/ipaexfont-gothic/ipaexg.ttf: IPAexGothic,IPAexゴシック:style=Regular
/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf: DejaVu Sans Mono:style=Bold
/usr/share/fonts/opentype/ipaexfont-mincho/ipaexm.ttf: IPAexMincho,IPAex明朝:style=Regular
/usr/share/fonts/truetype/fonts-japanese-mincho.ttf: IPAexMincho,IPAex明朝:style=Regular
/usr/share/fonts/truetype/fonts-japanese-gothic.ttf: IPAexGothic,IPAexゴシック:style=Regular
/usr/share/fonts/truetype/dejavu/DejaVuSerif.ttf: DejaVu Serif:style=Book

いじょう。

Python: Optuna で決められた時間内で最適化する

今回は Optuna の便利な使い方について。 現行の Optuna (v0.19.0) には決められた時間内で可能な限り最適化したい、というニーズを満たす API が実装されている。

使った環境は次の通り。

$ sw_vers 
ProductName:    Mac OS X
ProductVersion: 10.14.6
BuildVersion:   18G1012
$ python -V          
Python 3.7.5
$ pip list | grep -i optuna
optuna            0.19.0

下準備

まずは Optuna と Scikit-learn をインストールしておく。

$ pip install optuna scikit-learn

決められた時間内で最適化するサンプルコード

以下が決められた時間内で可能な限り最適化するサンプルコード。 実現するには Study#optimize()n_trials の代わりに timeout オプションを指定する。 渡す値は最適化に使う秒数になっており、以下では 60 秒を指定している。 サンプルコードでは、RandomForest で乳がんデータセットを 5-Fold Stratified CV するときのハイパーパラメータを探索している。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import optuna
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import StratifiedKFold
from sklearn.model_selection import cross_validate
from sklearn import datasets


class Objective:
    """目的関数に相当するクラス"""

    def __init__(self, X, y):
        self.X = X
        self.y = y

    def __call__(self, trial):
        """オブジェクトが呼び出されたときに呼ばれる特殊メソッド"""
        # RandomForest のパラメータを最適化してみる
        params = {
            'n_estimators': 100,
            'max_depth': trial.suggest_int('max_depth', 2, 32),
            'min_samples_leaf': trial.suggest_int('min_samples_leaf', 1, 16),
        }
        model = RandomForestClassifier(**params)
        # 5-Fold Stratified CV
        kf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
        scores = cross_validate(model,
                                X=self.X, y=self.y,
                                cv=kf,
                                # メトリックは符号を反転したロジスティック損失
                                scoring='neg_log_loss',
                                n_jobs=-1)
        return scores['test_score'].mean()


def main():
    dataset = datasets.load_breast_cancer()
    X, y = dataset.data, dataset.target
    objective = Objective(X, y)
    # 関数を最大化するように最適化する
    study = optuna.create_study(direction='maximize')
    # 試行回数ではなく特定の時間内で最適化する
    study.optimize(objective, timeout=60)  # この例では 60 秒
    print('params:', study.best_params)


if __name__ == '__main__':
    main()

実行すると、次のようになる。

$ python optimeout.py
[I 2019-12-02 18:45:41,029] Finished trial#0 resulted in value: -0.1420495901047513. Current best value is -0.1420495901047513 with parameters: {'max_depth': 28, 'min_samples_leaf': 9}.
...
[I 2019-12-02 18:46:39,488] Finished trial#25 resulted in value: -0.11825965818535904. Current best value is -0.11397258384370261 with parameters: {'max_depth': 6, 'min_samples_leaf': 1}.
params: {'max_depth': 6, 'min_samples_leaf': 1}

上記を見ると、約 1 分で最適化が終了していることがわかる。

ぶっちゃけやってみるまで 1 回の試行にどれだけ時間がかかるかなんてわからないし、試行回数を指定するより便利だと思う。

Kaggleで勝つデータ分析の技術

Kaggleで勝つデータ分析の技術

  • 作者: 門脇大輔,阪田隆司,保坂桂佑,平松雄司
  • 出版社/メーカー: 技術評論社
  • 発売日: 2019/10/09
  • メディア: 単行本(ソフトカバー)
  • この商品を含むブログを見る

Python: featuretools ではじめる総当り特徴量エンジニアリング

今回は featuretools というパッケージを用いた総当り特徴量エンジニアリング (brute force feature engineering) について書いてみる。 総当り特徴量エンジニアリングは、実際に効くか効かないかに関係なく、考えられるさまざまな処理を片っ端から説明変数に施して特徴量を作るというもの。 一般的にイメージする、探索的データ分析などにもとづいて特徴量を手動で作っていくやり方とはだいぶアプローチが異なる。 そして、featuretools は総当り特徴量エンジニアリングをするためのフレームワークとなるパッケージ。

使った環境は次の通り。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.14.6
BuildVersion:   18G1012
$ python -V
Python 3.7.5

もくじ

下準備

まずは featuretools をインストールしておく。

$ pip install featuretools

そして、Python のインタプリタを起動する。

$ python

単独のデータフレームで試してみる

まずは、サンプルとなるデータフレームを用意する。

>>> import pandas as pd
>>> data = {
...     'name': ['a', 'b', 'c'],
...     'x': [1, 2, 3],
...     'y': [2, 4, 6],
...     'z': [3, 6, 9],
... }
>>> df = pd.DataFrame(data)

このデータフレームには、名前に加えて三次元の座標を表すような特徴量が含まれている。 ここから、いくつかの特徴量を抽出してみよう。

>>> df
  name  x  y  z
0    a  1  2  3
1    b  2  4  6
2    c  3  6  9

まずは featuretools をインポートする。

>>> import featuretools as ft

featuretools では EntitySet というオブジェクトが処理の起点になる。 このオブジェクトを使うことで複数のデータフレームをまとめて扱うことができる。 ただし、現在のサンプルはデータフレームを 1 つしか使わないのであまり意味はない。

>>> es = ft.EntitySet(id='example')

EntitySet にデータフレームを追加する。 featuretools 的には、EntitySet に Entity を追加することになる。

>>> es = es.entity_from_dataframe(entity_id='locations',
...                               dataframe=df,
...                               index='name',  # 便宜上、名前をインデックス代わりにする
...                               )

これで EntitySet に Entity が登録された。

>>> es
Entityset: example
  Entities:
    locations [Rows: 3, Columns: 4]
  Relationships:
    No relationships

それぞれの Entity は辞書ライクに参照できる。

>>> es['locations']
Entity: locations
  Variables:
    name (dtype: index)
    x (dtype: numeric)
    y (dtype: numeric)
    z (dtype: numeric)
  Shape:
    (Rows: 3, Columns: 4)

上記において dtype という部分に index や numeric といった、見慣れない表示があることに注目してもらいたい。 詳しくは後述するものの、featuretools ではカラムの型を pandas よりも細分化して扱う。 これは、そのカラムに対してどのような処理を適用するのが適切なのかを判断するのに用いられる。

また、内部に格納されているデータフレームも次のようにして参照できる。

>>> es['locations'].df
  name  x  y  z
a    a  1  2  3
b    b  2  4  6
c    c  3  6  9

特徴量を作る

これで準備ができたので、実際に特徴量を作ってみよう。 特徴量の生成には featuretools.dfs() という API を用いる。 dfs は Deep Feature Synthesis の略語となっている。 featuretools.dfs() には、起点となる EntitySet と Entity および適用する処理内容を指定する。 以下では es['locations'] を起点として、add_numeric と subtract_numeric という処理を適用している。

>>> feature_matrix, feature_defs = ft.dfs(entityset=es,
...                                       target_entity='locations',
...                                       trans_primitives=['add_numeric', 'subtract_numeric'],
...                                       agg_primitives=[],
...                                       max_depth=1,
...                                       )

生成された特徴量を確認してみよう。 元は 4 次元だった特徴量が 9 次元まで増えていることがわかる。 カラム名と内容を見るとわかるとおり、増えた分はそれぞれのカラムを足すか引くかして作られている。

>>> feature_matrix
      x  y  z  x + y  y + z  x + z  x - y  y - z  x - z
name                                                   
a     1  2  3      3      5      4     -1     -1     -2
b     2  4  6      6     10      8     -2     -2     -4
c     3  6  9      9     15     12     -3     -3     -6
>>> feature_matrix.shape
(3, 9)

もう一方の返り値には特徴量の定義に関する情報が入っている。

>>> feature_defs
[<Feature: x>, <Feature: y>, <Feature: z>, <Feature: x + y>, <Feature: y + z>, <Feature: x + z>, <Feature: x - y>, <Feature: y - z>, <Feature: x - z>]

さらに組み合わせた特徴量を作る

続いて、先ほどは 1 を指定した max_depth オプションに 2 を指定してみよう。 これは DFS の深さを表すもので、ようするに一度作った特徴量同士でさらに同じ処理を繰り返すことになる。

>>> feature_matrix, feature_defs = ft.dfs(entityset=es,
...                                       target_entity='locations',
...                                       trans_primitives=['add_numeric', 'subtract_numeric'],
...                                       agg_primitives=[],
...                                       max_depth=2,
...                                       )

生成された特徴量を確認すると 21 次元まで増えている。 中身を見ると、最初の段階で作られた特徴量同士をさらに組み合わせて特徴量が作られている。

>>> feature_matrix
      x  y  z  x + y  y + z  x + z  ...  x + y - y  x + z - y + z  x + y - z  y - y + z  x - x + y  x - y + z
name                                ...                                                                      
a     1  2  3      3      5      4  ...          1             -1          0         -3         -2         -4
b     2  4  6      6     10      8  ...          2             -2          0         -6         -4         -8
c     3  6  9      9     15     12  ...          3             -3          0         -9         -6        -12

[3 rows x 21 columns]

特徴量の加工に用いる処理 (Primitive) について

先ほどの DFS では add_numeric と subtract_numeric という 2 種類の加工方法を指定した。 featuretools では特徴量の加工方法に Primitive という名前がついている。

Primitive は、大まかに Transform と Aggregation に分けられる。 Transform は名前からも推測できるように元の shape のまま、足したり引いたりするような処理を指している。 それに対して Aggregation は何らかのカラムで GroupBy するような集計にもとづく。

デフォルトで扱える Primitive の一覧は以下のようにして得られる。

>>> primitives = ft.list_primitives()
>>> primitives.head()
               name         type                                        description
0  time_since_first  aggregation  Calculates the time elapsed since the first da...
1          num_true  aggregation                Counts the number of `True` values.
2               all  aggregation     Calculates if all values are 'True' in a list.
3              last  aggregation               Determines the last value in a list.
4               std  aggregation  Computes the dispersion relative to the mean v...

次のように、Primitive は Transform と Transform に分けられることが確認できる。

>>> primitives.type.unique()
array(['aggregation', 'transform'], dtype=object)

複数のデータフレームで試してみる

続いては複数のデータフレームから成るパターンを試してみよう。 これは、SQL でいえば JOIN して使うようなテーブル設計のデータが与えられるときをイメージするとわかりやすい。

次のように、item_id というカラムを使って JOIN して使いそうなサンプルデータを用意する。 商品のマスターデータと売買のトランザクションデータみたいな感じ。

>>> data = {
...     'item_id': [1, 2, 3],
...     'name': ['apple', 'banana', 'cherry'],
...     'price': [100, 200, 300],
... }
>>> item_df = pd.DataFrame(data)
>>> 
>>> from datetime import datetime
>>> data = {
...     'transaction_id': [10, 20, 30, 40],
...     'time': [
...         datetime(2016, 1, 2, 3, 4, 5),
...         datetime(2017, 2, 3, 4, 5, 6),
...         datetime(2018, 3, 4, 5, 6, 7),
...         datetime(2019, 4, 5, 6, 7, 8),
...     ],
...     'item_id': [1, 2, 3, 1],
...     'amount': [1, 2, 3, 4],
... }
>>> tx_df = pd.DataFrame(data)

上記を、新しく用意した EntitySet に登録していく。

>>> es = ft.EntitySet(id='example')
>>> es = es.entity_from_dataframe(entity_id='items',
...                               dataframe=item_df,
...                               index='item_id',
...                               )
>>> es = es.entity_from_dataframe(entity_id='transactions',
...                               dataframe=tx_df,
...                               index='transaction_id',
...                               time_index='time',
...                               )

次のように Entity が登録された。

>>> es
Entityset: example
  Entities:
    items [Rows: 3, Columns: 3]
    transactions [Rows: 4, Columns: 4]
  Relationships:
    No relationships

次に Entity 同士に Relationship を張ることで結合方法を featuretools に教えてやる。

>>> relationship = ft.Relationship(es['items']['item_id'], es['transactions']['item_id'])
>>> es = es.add_relationship(relationship)

これで、EntitySet に Relationship が登録された。

>>> es
Entityset: example
  Entities:
    items [Rows: 3, Columns: 3]
    transactions [Rows: 4, Columns: 4]
  Relationships:
    transactions.item_id -> items.item_id

Aggregation 特徴を作ってみる

それでは、この状態で DFS を実行してみよう。 今度は Primitive として Aggregation の count, sum, mean を指定してみる。 なお、Aggregation は Entity に Relationship がないと動作しない。

>>> feature_matrix, feature_defs = ft.dfs(entityset=es,
...                                       target_entity='items',
...                                       trans_primitives=[],
...                                       agg_primitives=['count', 'sum', 'mean'],
...                                       max_depth=1,
...                                       )

作られた特徴を確認すると、トランザクションを商品ごとに集計した情報になっていることがわかる。

>>> feature_matrix
           name  price  COUNT(transactions)  SUM(transactions.amount)  MEAN(transactions.amount)
item_id                                                                                         
1         apple    100                    2                         5                        2.5
2        banana    200                    1                         2                        2.0
3        cherry    300                    1                         3                        3.0

Aggregation と Transform の組み合わせ

続いては Aggregation と Transform を両方指定してやってみよう。

>>> feature_matrix, feature_defs = ft.dfs(entityset=es,
...                                       target_entity='items',
...                                       trans_primitives=['add_numeric', 'subtract_numeric'],
...                                       agg_primitives=['count', 'sum', 'mean'],
...                                       max_depth=1,
...                                       )

しかし、先ほどと結果が変わらない。 この理由は max_depth に 1 を指定しているためで、最初の段階では Transform を適用する先がない。

>>> feature_matrix
           name  price  COUNT(transactions)  SUM(transactions.amount)  MEAN(transactions.amount)
item_id                                                                                         
1         apple    100                    2                         5                        2.5
2        banana    200                    1                         2                        2.0
3        cherry    300                    1                         3                        3.0

試しに max_depth を 2 に増やしてみよう。

>>> feature_matrix, feature_defs = ft.dfs(entityset=es,
...                                       target_entity='items',
...                                       trans_primitives=['add_numeric', 'subtract_numeric'],
...                                       agg_primitives=['count', 'sum', 'mean'],
...                                       max_depth=2,
...                                       )

すると、今度は Aggregation で作られた特徴に対して、さらに Transform の処理が適用されていることがわかる。

>>> feature_matrix
           name  price  ...  COUNT(transactions) - MEAN(transactions.amount)  price - SUM(transactions.amount)
item_id                 ...                                                                                   
1         apple    100  ...                                             -0.5                                95
2        banana    200  ...                                             -1.0                               198
3        cherry    300  ...                                             -2.0                               297

[3 rows x 17 columns]

カラムが省略されてしまっているので、定義の方を確認すると次の通り。

>>> from pprint import pprint
>>> pprint(feature_defs)
[<Feature: name>,
 <Feature: price>,
 <Feature: COUNT(transactions)>,
 <Feature: SUM(transactions.amount)>,
 <Feature: MEAN(transactions.amount)>,
 <Feature: COUNT(transactions) + price>,
 <Feature: COUNT(transactions) + SUM(transactions.amount)>,
 <Feature: MEAN(transactions.amount) + price>,
 <Feature: MEAN(transactions.amount) + SUM(transactions.amount)>,
 <Feature: COUNT(transactions) + MEAN(transactions.amount)>,
 <Feature: price + SUM(transactions.amount)>,
 <Feature: COUNT(transactions) - price>,
 <Feature: COUNT(transactions) - SUM(transactions.amount)>,
 <Feature: MEAN(transactions.amount) - price>,
 <Feature: MEAN(transactions.amount) - SUM(transactions.amount)>,
 <Feature: COUNT(transactions) - MEAN(transactions.amount)>,
 <Feature: price - SUM(transactions.amount)>]

単独のデータフレームで Aggregation する

ここまでの例だけ見ると、単独のデータフレームが与えられたときは Aggregation の特徴は使えないのか?という印象を持つと思う。 しかし、そんなことはない。 試しに以下のようなデータフレームを用意する。

>>> data = {
...     'item_id': [1, 2, 3, 4, 5],
...     'name': ['apple', 'broccoli', 'cabbage', 'dorian', 'eggplant'],
...     'category': ['fruit', 'vegetable', 'vegetable', 'fruit', 'vegetable'],
...     'price': [100, 200, 300, 4000, 500],
... }
>>> item_df = pd.DataFrame(data)

上記を元に EntitySet を作る。

>>> es = ft.EntitySet(id='example')
>>> es = es.entity_from_dataframe(entity_id='items',
...                               dataframe=item_df,
...                               index='item_id',
...                               )

作れたら EntitySet#normalize_entity() を使って新しいエンティティを作る。

>>> es = es.normalize_entity(base_entity_id='items',
...                          new_entity_id='category',
...                          index='category',
...                          )

EntitySet は以下のような状態になる。

>>> es
Entityset: example
  Entities:
    items [Rows: 5, Columns: 4]
    category [Rows: 2, Columns: 1]
  Relationships:
    items.category -> category.category
>>> es['category']
Entity: category
  Variables:
    category (dtype: index)
  Shape:
    (Rows: 2, Columns: 1)
>>> es['category'].df
            category
fruit          fruit
vegetable  vegetable

category カラムに入る値だけから成る Entity ができて Relationship が張られている。 これは SQL でいえば外部キー制約用のテーブルをマスターとは別に作っているようなイメージ。

上記に対して Aggregation を適用してみよう。

>>> feature_matrix, feature_defs = ft.dfs(entityset=es,
...                                       target_entity='items',
...                                       trans_primitives=[],
...                                       agg_primitives=['count', 'sum', 'mean'],
...                                       max_depth=2,
...                                       )

すると、category カラムの内容ごとに集計された特徴量が作られていることがわかる。

>>> feature_matrix
             name   category  price  category.COUNT(items)  category.SUM(items.price)  category.MEAN(items.price)
item_id                                                                                                          
1           apple      fruit    100                      2                       4100                 2050.000000
2        broccoli  vegetable    200                      3                       1000                  333.333333
3         cabbage  vegetable    300                      3                       1000                  333.333333
4          dorian      fruit   4000                      2                       4100                 2050.000000
5        eggplant  vegetable    500                      3                       1000                  333.333333

featuretools で取り扱うデータ型について

前述した通り、featuretools では適用する処理を選別するために pandas よりも細かい粒度でデータ型を取り扱う。 もし、適切なデータ型になっていないと意図しない処理が適用されて無駄やリークを起こす原因となる。 データ型の定義は現行バージョンであれば以下のモジュールにある。

github.com

ざっくり調べた感じ以下の通り。

説明
Variable - 全ての型のベース
Unknown Variable 不明なもの
Discrete Variable 名義尺度・順序尺度のベース
Boolean Variable 真偽値
Categorical Discrete 順序なしカテゴリ変数
Id Categorical 識別子
Ordinal Discrete 順序ありカテゴリ変数
Numeric Variable 数値
Index Variable インデックス
Datetime Variable 時刻
TimeIndex Variable 時刻インデックス
NumericTimeIndex TimeIndex, Numeric 数値表現の時刻インデックス
DatetimeTimeIndex TimeIndex, Datetime 時刻表現の時刻インデックス
Timedelta Variable 時間差
Text Variable 文字列
LatLong Variable 座標 (緯度経度)
ZIPCode Categorical 郵便番号
IPAddress Variable IP アドレス
FullName Variable 名前
EmailAddress Variable メールアドレス
URL Variable URL
PhoneNumber Variable 電話番号
DateOfBirth Datetime 誕生日
CountryCode Categorical 国コード
SubRegionCode Categorical 地域コード
FilePath Variable ファイルパス

組み込みの Primitive について

続いて、featuretools にデフォルトで組み込まれている Primitive について勉強がてらざっくり調べた。 現行バージョンに組み込まれているものは以下で確認できる。

github.com

なお、動作する上で特定のパラメータを必要とするものもある。

Transform

まずは Transform から。

名前 入力型 出力型 説明
is_null Variable Boolean Null か (pandas.isnull)
absolute Numeric Numeric 絶対値 (np.absolute)
time_since_previous DatetimeTimeIndex Numeric 時刻の最小値からの差分
time_since DatetimeTimeIndex, Datetime Numeric 特定時刻からの差分
year Datetime Ordinal
month Datetime Ordinal
day Datetime Ordinal
hour Datetime Ordinal
minute Datetime Numeric
second Datetime Numeric
week Datetime Ordinal
is_weekend Datetime Boolean 平日か
weekday Datetime Ordinal 週の日付 (月:0 ~ 日:6)
num_characters Text Numeric 文字数
num_words Text Numeric 単語数
diff Numeric Numeric 値の差
negate Numeric Numeric -1 をかける
percentile Numeric Numeric パーセンタイルに変換
latitude LatLong Numeric 緯度
longitude LatLong Numeric 経度
haversine (LatLong, LatLong) Numeric 2 点間の距離
not Boolean Boolean 否定
isin Variable Boolean リストに含まれるか
greater_than (Numeric, Numeric), (Datetime, Datetime), (Ordinal, Ordinal) Boolean より大きいか (np.greater)
greater_than_equal_to (Numeric, Numeric), (Datetime, Datetime), (Ordinal, Ordinal) Boolean 同じかより大きいか (np.greater_equal)
less_than (Numeric, Numeric), (Datetime, Datetime), (Ordinal, Ordinal) Boolean より小さいか (np.less)
less_than_equal_to (Numeric, Numeric), (Datetime, Datetime), (Ordinal, Ordinal) Boolean 同じかより小さいか (np.less_equal)
greater_than_scalar Numeric, Datetime, Ordinal Boolean 特定の値より大きいか
greater_than_equal_to_scalar Numeric, Datetime, Ordinal Boolean 特定の値より大きいか (値を含む)
less_than_scalar Numeric, Datetime, Ordinal Boolean 特定の値より小さいか
less_than_equal_to_scalar Numeric, Datetime, Ordinal Boolean 特定の値より小さいか (値を含む)
equal (Variable, Variable) Boolean 同じか (np.equal)
not_equal (Variable, Variable) Boolean 同じでないか (np.not_equal)
equal_scalar (Variable, Variable) Boolean 特定の値と等しいか
not_equal_scalar (Variable, Variable) Boolean 特定の値と等しくないか
add_numeric (Numeric, Numeric) Numeric 加算 (np.add)
subtract_numeric (Numeric, Numeric) Numeric 減算 (np.subtract)
multiply_numeric (Numeric, Numeric) Numeric 乗算 (np.multiply)
divide_numeric (Numeric, Numeric) Numeric 除算 (np.divide)
modulo_numeric (Numeric, Numeric) Numeric 余算 (np.mod)
add_numeric_scalar Numeric Numeric 特定の値を足す
subtract_numeric_scalar Numeric Numeric 特定の値を引く
scalar_subtract_numeric_feature Numeric Numeric 特定の値から引く
multiply_numeric_scalar Numeric Numeric 特定の値を掛ける
divide_numeric_scalar Numeric Numeric 特定の値で割る
divide_by_feature Numeric Numeric 特定の値を割る
modulo_numeric_scalar Numeric Numeric 特定の値で割った余り
modulo_by_feature Numeric Numeric 特定の値を割った余り
multiply_boolean (Boolean, Boolean) Boolean ビット同士を比べた AND (np.bitwise_and)
and (Boolean, Boolean) Boolean 論理積 (np.logical_and)
or (Boolean, Boolean) Boolean 論理和 (np.logical_or)

Aggregation

続いて Aggregation を。

名前 入力型 出力型 説明
count Index Numeric 要素数
num_unique Numeric Numeric ユニークな要素数
sum Numeric Numeric
mean Numeric Numeric 平均
std Numeric Numeric 標準偏差
median Numeric Numeric 中央値
mode Numeric Numeric 最頻値
min Numeric Numeric 最小値
max Numeric Numeric 最大値
first Variable - 最初の要素
last Variable - 最後の要素
skew Numeric Numeric 歪度
num_true Boolean Numeric 真の要素数
percent_true Boolean Numeric 真の比率
n_most_common Discrete Discrete 出現頻度の高い要素 TOP n
avg_time_between DatetimeTimeIndex Numeric 平均間隔
any Boolean Boolean いずれかが真であるか
all Boolean Boolean 全て真であるか
time_since_last DatetimeTimeIndex Numeric 最後の要素からの時間差
time_since_first DatetimeTimeIndex Numeric 最初の要素からの時間差
trend (Numeric, DatetimeTimeIndex) Numeric 線形回帰した際の傾き
entropy Categorical Numeric エントロピー

いじょう。 計算する種類が多かったり max_depth が深いとデータによっては現実的な時間・空間計算量におさまらなくなるので気をつけよう。 個人的には、空間計算量を節約するために作った特徴量をジェネレータとかでどんどんほしいところだけど、そういう API はざっと読んだ感じなさそう。 順番にデータフレームを結合して最終的な成果物をどんと渡す作りになっている。 再帰的に計算をするために、これは仕方ないのかなー、うーん。

Kaggleで勝つデータ分析の技術

Kaggleで勝つデータ分析の技術

  • 作者: 門脇大輔,阪田隆司,保坂桂佑,平松雄司
  • 出版社/メーカー: 技術評論社
  • 発売日: 2019/10/09
  • メディア: 単行本(ソフトカバー)
  • この商品を含むブログを見る