CUBE SUGAR CONTAINER

技術系のこと書きます。

Linux の IPC Namespace について

Linux のコンテナ仮想化を実現する機能の一つに Namespace がある。 Namespace はプロセスが動作する際のリソースをカーネルの中で隔離 (分離) する仕組み。 Namespace は隔離する対象のリソースによって色々とある。

man7.org

今回は、その中でも IPC (Inter Process Communication) に関するリソースを隔離する仕組みの IPC Namespace について扱う。 ここでいう IPC には、たとえば SystemV IPC と POSIX IPC がある。 今回は、unshare(1) と unshare(2) を使って SystemV IPC に関するリソースが Namespace によって隔離される様子を観察してみる。

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

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 22.04 LTS
Release:    22.04
Codename:   jammy
$ uname -srm
Linux 5.18.0-051800-generic aarch64
$ unshare --version
unshare from util-linux 2.37.2
$ gcc --version
gcc (Ubuntu 11.2.0-19ubuntu1) 11.2.0
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

もくじ

下準備

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

$ sudo apt-get update
$ sudo apt-get install \
    util-linux \
    build-essential \
    python3-sysv-ipc

SystemV IPC について

今回は IPC Namespace の動作確認のために SystemV IPC を使う。 SystemV IPC は、その名のとおり UNIX System V で導入された IPC の仕組み。 SystemV IPC はメッセージキュー、共有メモリ、セマフォという 3 種類の機能を提供している。 操作するためには msgget(2) や msgsnd(2) といったシステムコールを使う。 詳細は man 7 sysvipc を参照する。

man7.org

ただし、今回は SystemV IPC 自体を詳しく解説したいわけではない。 そこで、操作には util-linux に含まれる ipcs(1) と Python ラッパーの sysv-ipc を使う。

たとえば ipcs(1) をオプションなしで実行すると、SystemV IPC に関するリソースの利用状況がわかる。 初期状態では、特に何も作られていない。

$ ipcs

------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages    

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status      

------ Semaphore Arrays --------
key        semid      owner      perms      nsems     

ここで、試しにメッセージキューを作ってみよう。 まずは、Python のインタプリタを起動する。

$ python3

sysv_ipc パッケージをインポートして、0x100 というキーでメッセージキューを作る。

>>> import sysv_ipc
>>> q = sysv_ipc.MessageQueue(key=0x100, flags=sysv_ipc.IPC_CREAT, mode=0o644)

別のターミナルから ipcs(1) を実行すると、メッセージキューができていることがわかる。 -q オプションをつけるとメッセージキューに関する情報だけ表示できる。

$ ipcs -q

------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages    
0x00000100 0          vagrant    644        0            0           

キューに対してオブジェクトを送ってみよう。

>>> q.send("Hello, World!")

ここで、別のプロセスから Python のインタプリタを起動する。

$ python3

そして、先ほどと同じキー 0x100 を指定してメッセージキューを参照する。

>>> import sysv_ipc
>>> q = sysv_ipc.MessageQueue(key=0x100, mode=0o644)

キューのオブジェクトに対して receive() メソッドを実行すると、先ほど送ったメッセージが取得できる。

>>> q.receive()
(b'Hello, World!', 1)

結果はタプルになっていて、2 番目の要素はメッセージを送るときにつけた type を表している。 デフォルトでは 1 になっており、これはキューの中をさらに細分化して扱うための仕組みのようだ。

unshare(1) を使って IPC Namespace を操作する

さて、SystemV IPC の基本的な説明が終わったので、ここから本題の IPC Namespace を扱っていく。 まずは unshare(1) を使って IPC Namespace を操作してみよう。

現在のプロセスが所属する IPC Namespace は /proc/self/ns/ipc で確認できる。 以下であれば 4026531839 という識別子に所属している。

$ file /proc/self/ns/ipc 
/proc/self/ns/ipc: symbolic link to ipc:[4026531839]

ここで unshare(1) を --ipc オプションをつけて実行してみよう。 同時に bash(1) を起動する。

$ sudo unshare --ipc bash

すると、所属する IPC Namespace が 4026532177 へと変化したことがわかる。

# file /proc/self/ns/ipc 
/proc/self/ns/ipc: symbolic link to ipc:[4026532177]

ipcs(1) を実行すると、先ほどまで見えていたメッセージキューも表示されなくなっている。 これが正に IPC Namespace の機能であり、IPC に関するリソースを隔離できている。

# ipcs

------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages    

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status      

------ Semaphore Arrays --------
key        semid      owner      perms      nsems     

試しに 0x1000 というキーでメッセージキューを作ると、ちゃんと作成できる。

# python3 -c "import sysv_ipc; sysv_ipc.MessageQueue(key=0x1000, flags=sysv_ipc.IPC_CREAT, mode=0o644)"
# ipcs

------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages    
0x00001000 0          root       644        0            0           

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status      

------ Semaphore Arrays --------
key        semid      owner      perms      nsems     

元々のターミナル、つまりシステムにおいて ipcs(1) を実行するとキーが 0x100 のメッセージキューが見える。

$ ipcs

------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages    
0x00000100 0          vagrant    644        0            0           

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status      

------ Semaphore Arrays --------
key        semid      owner      perms      nsems     

ちゃんとメッセージキューが Namespace ごとに隔離されている様子が確認できた。

unshare(1) で起動した bash(1) は一旦終了しておく。

# exit

unshare(2) を使って IPC Namespace を操作する

続いては unshare(2) のシステムコールを使って IPC Namespace を操作してみよう。

下記のサンプルコードでは unshare(2) の引数に CLONE_NEWIPC を指定することで新しく IPC Namespace を作成している。 その上で execvp(3) を使ってシェルを起動している。

#define _GNU_SOURCE
  
#include <sched.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
    // unshare(2) で IPC Namespace を作成する
    if (unshare(CLONE_NEWIPC) != 0) {
        fprintf(stderr, "Failed to create a new IPC namespace: %s\n", strerror(errno));
        exit(EXIT_FAILURE);
    }

    // execvp(3) でシェルを起動する
    char* const args[] = {"bash", NULL};
    if (execvp(args[0], args) != 0) {
        fprintf(stderr, "Failed to exec \"%s\": %s\n", args[0], strerror(errno));
        exit(EXIT_FAILURE);
    }

    return EXIT_SUCCESS;
}

上記をビルドする。

$ gcc -Wall example.c

ビルドしたら実行しよう。

$ sudo ./a.out

実行して起動されるシェルからは、先ほどと同じようにシステムのメッセージキューが表示されない。

# ipcs

------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages    

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status      

------ Semaphore Arrays --------
key        semid      owner      perms      nsems     

/proc/self/ns/ipc の識別子についても、システムとは異なっている。

# file /proc/self/ns/ipc
/proc/self/ns/ipc: symbolic link to ipc:[4026532177]

今回も、試しにメッセージキューを作ってみよう。

# python3 -c "import sysv_ipc; sysv_ipc.MessageQueue(key=0x1000, flags=sysv_ipc.IPC_CREAT, mode=0o644)"
# ipcs

------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages    
0x00001000 0          root       644        0            0           

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status      

------ Semaphore Arrays --------
key        semid      owner      perms      nsems     

やはり、システムで実行する ipcs(1) とは SystemV IPC のリソースが隔離されていることが分かる。

$ ipcs

------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages    
0x00000100 0          vagrant    644        0            0           

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status      

------ Semaphore Arrays --------
key        semid      owner      perms      nsems     

まとめ

今回は IPC Namespace を使って、SystemV IPC に関連するリソースが隔離される様子を観察してみた。

nvidia-smi(1) で GPU にパワーリミットを設定して消費電力や発熱を減らす

自宅にあるオンプレマシンでグラフィックカードを GPGPU の用途に使用していると、消費電力や発熱は切実な問題になりうる。 特に昨今は電気代の値上がりも著しいし、発熱は製品寿命の短縮や夏だと室温の上昇につながる。 そこで、今回は Linux の環境で nvidia-smi(1) を使って NVIDIA の GPU にパワーリミットを設定することで消費電力や発熱の低減を目指してみる。

使った環境は次のとおり。 Ubuntu 20.04 LTS のマシンに、Docker と nvidia-container-toolkit がインストールしてある。

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.4 LTS
Release:    20.04
Codename:   focal
$ uname -srm
Linux 5.13.0-51-generic x86_64
$ docker version
Client: Docker Engine - Community
 Version:           20.10.17
 API version:       1.41
 Go version:        go1.17.11
 Git commit:        100c701
 Built:             Mon Jun  6 23:02:57 2022
 OS/Arch:           linux/amd64
 Context:           default
 Experimental:      true

Server: Docker Engine - Community
 Engine:
  Version:          20.10.17
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.17.11
  Git commit:       a89b842
  Built:            Mon Jun  6 23:01:03 2022
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.6.6
  GitCommit:        10c12954828e7c7c9b6e0ea9b0c02b01407d3ae1
 runc:
  Version:          1.1.2
  GitCommit:        v1.1.2-0-ga916309
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0
$ nvidia-smi
Fri Jun 24 18:59:39 2022       
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 515.48.07    Driver Version: 515.48.07    CUDA Version: 11.7     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|                               |                      |               MIG M. |
|===============================+======================+======================|
|   0  NVIDIA GeForce ...  On   | 00000000:01:00.0 Off |                  N/A |
|  0%   40C    P8     7W / 170W |     14MiB / 12288MiB |      0%      Default |
|                               |                      |                  N/A |
+-------------------------------+----------------------+----------------------+
                                                                               
+-----------------------------------------------------------------------------+
| Processes:                                                                  |
|  GPU   GI   CI        PID   Type   Process name                  GPU Memory |
|        ID   ID                                                   Usage      |
|=============================================================================|
|    0   N/A  N/A      1087      G   /usr/lib/xorg/Xorg                  9MiB |
|    0   N/A  N/A      1232      G   /usr/bin/gnome-shell                3MiB |
+-----------------------------------------------------------------------------+

もくじ

事前準備

ベンチマーク用に PyTorch を使いたいので、あらかじめ公式の Docker イメージをプルしておく。 そして、コンテナから GPU が見えることを確認する。

$ docker pull pytorch/pytorch
$ docker run --gpus all --rm -it pytorch/pytorch nvidia-smi
Fri Jun 24 19:03:09 2022       
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 515.48.07    Driver Version: 515.48.07    CUDA Version: 11.7     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|                               |                      |               MIG M. |
|===============================+======================+======================|
|   0  NVIDIA GeForce ...  On   | 00000000:01:00.0 Off |                  N/A |
|  0%   39C    P8     8W / 170W |     14MiB / 12288MiB |      0%      Default |
|                               |                      |                  N/A |
+-------------------------------+----------------------+----------------------+
                                                                               
+-----------------------------------------------------------------------------+
| Processes:                                                                  |
|  GPU   GI   CI        PID   Type   Process name                  GPU Memory |
|        ID   ID                                                   Usage      |
|=============================================================================|
+-----------------------------------------------------------------------------+

GPU で使用できるパワーリミットを確認する

まずは使っている GPU に設定できる最低・最大のパワーリミットを確認する。 今回使った製品であれば、最低が 100W で最大が 170W だと分かる。 デフォルトではパワーリミットの値は最大に設定されているはず。

$ nvidia-smi -q -d POWER

==============NVSMI LOG==============

Timestamp                                 : Fri Jun 24 19:05:09 2022
Driver Version                            : 515.48.07
CUDA Version                              : 11.7

Attached GPUs                             : 1
GPU 00000000:01:00.0
    Power Readings
        Power Management                  : Supported
        Power Draw                        : 7.75 W
        Power Limit                       : 170.00 W
        Default Power Limit               : 170.00 W
        Enforced Power Limit              : 170.00 W
        Min Power Limit                   : 100.00 W
        Max Power Limit                   : 170.00 W
    Power Samples
        Duration                          : 105.50 sec
        Number of Samples                 : 119
        Max                               : 8.42 W
        Min                               : 7.46 W
        Avg                               : 7.79 W

GPU にパワーリミットを設定する

GPU にパワーリミットを設定するには nvidia-smi(1) の -pl オプションを使う。 このオプションに、先ほど得られた設定できるパワーリミットの範囲でワット数を指定すれば良い。

たとえば、今回の環境における下限の 100W に設定するには次のようにする。

$ sudo nvidia-smi -pl 100
Power limit for GPU 00000000:01:00.0 was set to 100.00 W from 170.00 W.
All done.

もう一度 nvidia-smi(1) をオプションなしで実行してみると、ワット数の表示が 100W になっていることが確認できる。

$ nvidia-smi
Fri Jun 24 19:06:28 2022       
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 515.48.07    Driver Version: 515.48.07    CUDA Version: 11.7     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|                               |                      |               MIG M. |
|===============================+======================+======================|
|   0  NVIDIA GeForce ...  On   | 00000000:01:00.0 Off |                  N/A |
|  0%   39C    P8     9W / 100W |     14MiB / 12288MiB |      0%      Default |
|                               |                      |                  N/A |
+-------------------------------+----------------------+----------------------+
                                                                               
+-----------------------------------------------------------------------------+
| Processes:                                                                  |
|  GPU   GI   CI        PID   Type   Process name                  GPU Memory |
|        ID   ID                                                   Usage      |
|=============================================================================|
|    0   N/A  N/A      1087      G   /usr/lib/xorg/Xorg                  9MiB |
|    0   N/A  N/A      1232      G   /usr/bin/gnome-shell                3MiB |
+-----------------------------------------------------------------------------+

パワーリミットを設定してベンチマークを測ってみる

さて、パワーリミットを設定すれば、各瞬間の電力 (W) が減るのは分かる。 一方で、時間を通じた総和として消費電力量 (Wh) が減るのかは分からない。 そこで、パワーリミットを最小と最大に設定した状態でのパフォーマンスを調べる。

PyTorch のコンテナを起動する。

$ docker run --gpus all --rm -it pytorch/pytorch

動かすベンチマークについては、下記の PyTorch の公式チュートリアルを参照にした。

pytorch.org

下記のサンプルコードでは、内積を計算するシンプルなコードを使ってベンチマークする。

$ cat << 'EOF' > benchmark.py
import torch
import torch.utils.benchmark as benchmark


def batched_dot_bmm(a, b):
    '''Computes batched dot by reducing to bmm'''
    a = a.reshape(-1, 1, a.shape[-1])
    b = b.reshape(-1, b.shape[-1], 1)
    return torch.bmm(a, b).flatten(-3)


# Input for benchmarking
x = torch.randn(100000, 10000, device='cuda')


t = benchmark.Timer(
    stmt='batched_dot_bmm(x, x)',
    setup='from __main__ import batched_dot_bmm',
    globals={'x': x},
)

print(t.timeit(1000))
EOF

まずはパワーリミットを下限の 100W に設定した状態で実行する。 同時に、nvidia-smi(1) を実行して、消費電力が上限の 100W に張り付くことを確認しておこう。

# python benchmark.py 
<torch.utils.benchmark.utils.common.Measurement object at 0x7fd2ba7fa460>
batched_dot_bmm(x, x)
setup: from __main__ import batched_dot_bmm
  16.23 ms
  1 measurement, 1000 runs , 1 thread

上記から、1 回の演算に平均で 16.23ms かかることがわかった。

続いてはパワーリミットを上限の 170W に戻す。

$ sudo nvidia-smi -pl 170
Power limit for GPU 00000000:01:00.0 was set to 170.00 W from 100.00 W.
All done.

そして、プログラムをもう一度実行する。 今度も、nvidia-smi(1) を実行して、消費電力が上限の 170W に張り付くことを確認しておこう。

# python benchmark.py 
<torch.utils.benchmark.utils.common.Measurement object at 0x7ff3be37b460>
batched_dot_bmm(x, x)
setup: from __main__ import batched_dot_bmm
  13.76 ms
  1 measurement, 1000 runs , 1 thread

上記から、今度は 1 回の演算に平均で 13.76ms かかることがわかった。

演算にかかる電力は 170W と 100W なので約 40 % 低下している。 そして、演算にかかる時間は 13.76ms と 16.23ms なので約 18 % 増加した。 ここから、トータルの電力量は 100W において 0.6 * 1.18 = 0.708 になる。 あくまで今回の条件下ではという但し書きはつくものの、計算に使用する電力量は約 71% まで減らせたようだ。

一般に半導体のワットパフォーマンスはリニアな関係ではなく、入力する電力が大きくなるほどパフォーマンス向上の効率が悪くなると言われる。 その点からも、今回の結果には納得がいく。 また、電力量が減れば発熱も小さくなるため、暖房器具としての性能も低下するし製品寿命の延長が望める可能性もある。

まとめ

今回は nvidia-smi(1) を使って NVIDIA の GPU にパワーリミットを設定して消費電力と発熱の低減を試みた。 オンプレマシンの消費電力や発熱に悩んでいる場合には、パフォーマンスとのトレードオフはあるものの、一考の余地はあるかもしれない。

いじょう。

qrencode と viu を使ってターミナルで QR コードを作って表示する

情報共有などのために、ささっと QR コードを作って読み込ませたいときがある。 そんなときは qrencode と viu を使うとターミナル上で完結して楽そうだ。

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

$ sw_vers   
ProductName:    macOS
ProductVersion: 12.4
BuildVersion:   21F79
$ qrencode -V                         
qrencode version 4.1.1
Copyright (C) 2006-2017 Kentaro Fukuchi
$ viu -V
viu 1.4.0

もくじ

下準備

まずは qrencode と viu をインストールしておく。

$ brew install qrencode viu 

ターミナルで QR コードを作る

qrencode を使うと、次のように引数で指定した文字列を埋め込んだ QR コードが作れる。 -o オプションで保存先を指定する。

$ qrencode -o greet.png "Hello, World"

できあがる QR コードはこんな感じ。

ターミナルで QR コードを表示する

ターミナルで QR コードを作ったなら、表示までターミナルで完結したい気持ちもある。 そんなときは viu で開くと良い。

$ viu greet.png

こんな感じで表示できる。

viu を使って QR コードの画像を開く

目視ではかなりざらついてるように見えるけど、デバイスに読み込ませてみるとちゃんと読める。 QR コードの堅牢性おそるべし。 もし読めないときは viu コマンドの -w オプションとかを使って大き目に表示させると良い。

ワンライナーで QR コードを作って表示する

ちなみに qrencode も viu も標準入出力を読み書きできる。 そのため、両者を次のように組み合わせることも可能だ。

$ qrencode -o - "Hello, World" | viu -

これで、ファイルを介すことなく QR コードをターミナル上に表示できる。

めでたしめでたし。

子育てにかかる費用を公的データで調べる

一般的に、子どもを一人育てるのには 2,000 万円かかるとか言われている。 しかし、何も子どもが生まれた瞬間に 2,000 万円が必要になるわけではない。 もちろん、2,000 万円を均等に割った額が毎年かかるわけでもない。 そもそも、こういった数字はあくまで代表的な家庭における目安に過ぎないはず。 たとえば、子どもがどういった就学をするのかだったり、世帯収入にも依存していると考えられる。 そこで、今回は子育てにかかる費用に対する自分の解像度を上げるために、公的な統計資料を当たってみることにした。

もくじ

インターネットによる子育て費用に関する調査

まず、第1子が誕生してから中学校を卒業するまでの費用に関しては、平成21年度に内閣府が実施した調査が参考になる。 この資料は教育費に限らず、子育てにかかる全般的な費用について扱っている。

www8.cao.go.jp

上記資料の「第3章 調査結果」に掲載されているグラフを以下に引用する。 このグラフからは、それぞれの就学区分ごとに第1子の子育てをするのに年間でいくらかかっているか分かる。

出典: 平成21年度インターネットによる子育て費用に関する調査 全体版 第3章 調査結果 42 ページ 図表 1-1.第1子一人当たりの年間子育て費用額(対象者全体平均)【第1子の就学区分別】

未就園児は 84 万円、保育園・幼稚園児は 121 万円、小学生は 115 万円、中学生は 155 万円が平均で必要となるようだ。 なお、この資料では就学先が私立か国公立かを区別していない。 つまり、調査対象とした全体の平均である点に留意が必要となる。

また、上記のグラフが掲載されている次のページには、各年齢ごとに分解したグラフが掲載されている。 グラフからは、年齢が上がる毎に学校外教育費 (塾や習い事) がじわじわと増加していく様子や、学校教育費が中学校から一気に増加する様子が伺える。

世帯年収ごとの子育て費用額の比較も興味深い。 以下は「保育所・幼稚園児」について、世帯年収ごとの年間子育て費用額を比較したグラフになっている。 このグラフからは、子どもが小さいうちは世帯年収ごとの子育て費用額の差は「子どものための預貯金・保険」や「レジャー・旅行費」に大きく出ることが伺える。

出典: 平成21年度インターネットによる子育て費用に関する調査 全体版 【参考資料】 37ページ 図表 4-3.世帯年収別にみた「保育所・幼稚園児」第1子一人当たりの年間子育て費用額(対象者全体平均)

一方で、以下は「中学生」について、世帯年収ごとの年間子育て費用額を比較したグラフになっている。 こちらのグラフでは、特に高い世帯年収において、子育て費用額の差が「学校教育費」や「学校外活動費」に大きく出ている。 つまり、子どもの年齢によって資金の余裕が振り分けられる先が少し変わるようだ。 これらも、自分たちの世帯年収に合わせて考える上で参考になるだろう。

出典: 平成21年度インターネットによる子育て費用に関する調査 全体版 【参考資料】 38ページ 図表 4-5.世帯年収別にみた「中学生」第1子一人当たりの年間子育て費用額(対象者全体平均)

ちなみに、「【参考資料】」に掲載されているグラフには子育てにかかる費用の構造が図示されており、こちらも面白い。

出典: 平成21年度インターネットによる子育て費用に関する調査 全体版 【参考資料】110 ページ掲載図表

子供の学習費調査

高校までの学習費に焦点を絞った資料としては、文部科学省が平成30年度に実施した調査が参考になる。 この資料では幼稚園から高校までの学習費を、国公立と私立で区別して調査している。

www.mext.go.jp

上記資料の「調査の結果 > 結果の概要 > 平成30年度 > 2.調査結果の概要」に掲載されているグラフを以下に引用する。

出典: 平成30年度 子供の学習費調査 2.調査結果の概要 図1−2 学校種別にみた学習費総額

上記から、国公立と私立で学習費に大きな差が生じることが確認できる。 私立中学の場合は公立の約 3 倍、私立高校でも公立の約 2 倍かかる。 場合によっては私立中学に進学を希望することもあるだろうし、私立高校であればより現実的な可能性として捉えておく必要がありそうだ。

教育費に関する調査結果

先ほどの文科省の調査は高校までが範囲だった。 高専・専修学校や大学など、高校以降の教育費については日本政策金融公庫の調査が詳しい。 この調査では「教育費負担の実態調査結果」を毎年公表している。

www.jfc.go.jp

上記で令和 3 年度の調査結果として掲載されているグラフを以下に引用する。 このグラフには、進学先別の年間の在学費用が載っている。

令和3年度 教育費負担の実態調査結果 6ページ 図−3 在学先別にみた1年間の在学費用(子供1人当たりの費用)

上記から、高専・専修学校や大学などへ進学すると、最低でも 100 万円程度は年間でかかることがわかる。 なお、上記は大学の費用を国公立と私立で区別していない。

大学に関して、国公立と私立の文理で区別したグラフが以下になる。

出典: 令和3年度 教育費負担の実態調査結果 6ページ 図−4 国公立・私立別にみた大学の在学費用(子供1人当たりの費用)

大学の中でも、国公立と私立の文理で費用の差が激しい。 国公立は平均で 103 万円だが、私立の文系は 152 万円、理系では 183 万円となっている。 なお、最近だと理系は大学院への進学が一般的になってきている点も心に留めておく必要がありそうだ。

また、上記の資料には自宅外通学に関する資料も載っている。 私自身は関東圏に居住していることもあってさほど可能性は高くないものの、それ以外の場合には参考になりそうだ。 とはいえ、関東圏に住んでいる場合であっても、海外へ留学する可能性は考慮する必要があるのだろうか・・・?

国民生活白書

最初に登場した「インターネットによる子育て費用に関する調査」は、子育てにかかる費用を広範に扱った調査だったが、対象は第1子に限定されていた。 では、第2子以降にかかる費用はどうなるのだろうか。 この点に関しては、内閣府が平成17年度に実施した国民生活白書の調査が参考になりそうだった。

warp.da.ndl.go.jp

平成17年版「子育て世代の意識と生活」の「第3章 子育てにかかる費用と時間 > 二人目・三人目の子どもにかける費用は逓減」に記述がある。

warp.da.ndl.go.jp

ここまで、一人の子どもを育てるための費用を見てきたが、更に子どもを育てた場合に子育て費用はどれくらい増加するのだろうか。一人の場合と同様に、「基本的経費」、「教育費」、「住宅関係費」に分けて、子どもを二人持つ世帯の「子どもを育てる費用」から子どもを一人持つ世帯の「子どもを育てる費用」を差し引いて、22年間分を足したものを「二人目の子ども9を育てる」費用として推計した。ここでいう「子どもを育てる費用」は、前項と同じく、付注3−1−1に掲げた費目における子どもを育てるための追加的な費用である。 その結果、一人の子どもを育てる費用の1,302万円に対して、二人目の子どもを育てる費用10は1,052万円と、20%程度節約されていることが分かった(第3−1−14図)。内訳ごとに節約の程度を見ると、二人目の基本的経費は一人目の80.0%、教育費は83.6%、住宅関係費は63.0%となっている。同様に三人目の子どもにかかる費用を推計すると、22年間で769万円となり、二人目と比べて更に27%程度節約されており、子どもの増加にともない、子どもにかかる費用は逓減していくことがうかがわれる。

上記より、第2子の子育てにかかる費用は第1子の約 8 割まで減少 (節約) し、第3子に至っては第1子の約 6 割まで減少することがわかる。 もし、すでに第1子がいる場合には、その実績を元に第2子以降でどれくらいかかりそうかを計算する上で目安にできそうだ。 ただし、この結果は単純に「減っている」のではなく、諸々の事情から「減らしている」ことも考慮する必要があるだろう。

いじょう。

まとめ

子育てにかかる費用が分からなかったので、公的データを調べてわかったことについて自分用にまとめた。

NVMe ストレージのデータを nvme-cli(1) で完全に消去する

ストレージ機器を破棄または譲渡するときには、漏えいを防ぐためにあらかじめデータを消去しておく必要がある。 このとき、データの消去は後から読み取りが難しいように実施しなければいけない。 後から読み取りが難しい形でデータを消去することは Secure Erase と呼んだりするようだ。 今回は NVMe ストレージを Secure Erase する方法について扱う。

まず、最近のマザーボードの UEFI には、ストレージのデータを Secure Erase するためのツールが付属していることがある。 もし、ツールが付属している場合には、それを利用するのが手っ取り早い。 では、付属していない場合にどうするか、というのが今回の話になる。 Unix 系の OS で NVMe ストレージを使っている場合には、nvme-cli(1) を使うのが良さそうだ。

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

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.4 LTS
Release:    20.04
Codename:   focal
$ uname -srm
Linux 5.13.0-44-generic x86_64
$ nvme version
nvme version 1.9

下準備

Ubuntu の場合には apt を使ってインストールできる。

$ sudo apt-get -y install nvme-cli

データを消去する

まず、nvme list コマンドで消去したいデバイスを確認する。

$ sudo nvme list
Node             SN                   Model                                    Namespace Usage                      Format           FW Rev  
---------------- -------------------- ---------------------------------------- --------- -------------------------- ---------------- --------
/dev/nvme0n1     XXXXXXXXXXXX         XXXXXXXXXXXXX                            1           1.00  TB /   1.00  TB    512   B +  0 B   XXXXXXX 

あとは、nvme format コマンドに確認したデバイスを指定して実行するだけ。 このとき、オプションとして -s 2 を指定するのが望ましい。

$ nvme format /dev/nvme0n1 -s 2

先ほどの形式だとデバイスを名前空間ブロックデバイス (/dev/nvmeXnY みたいな形式) として指定していた。 それ以外にもキャラクタデバイス (/device/nvmeX みたいな形式) も受け付けられる。 先ほどと同じように namespace を指定するときは -n オプションを指定する。

$ nvme format /dev/nvme0 -s 2 -n 1

実行すると、ストレージのデータが消去される。 なお、デバイスがシステムの起動ディスクになっていても問題ない。

-s オプションについて

先ほど指定した -s オプションについて補足する。 このオプションは、データの消去をどのように実施するかを表している。 詳しくは man ページに記載されている。

$ man 1 nvme-format

manpages.ubuntu.com

オプションに指定する値の説明を、上記から引用する。 基本的には 2 を指定することで Secure Erase できる。

           │Value │ Definition                       │
           ├──────┼──────────────────────────────────┤
           │0     │ No secure erase operation        │
           │      │ requested                        │
           ├──────┼──────────────────────────────────┤
           │1     │ User Data Erase: All user data   │
           │      │ shall be erased, contents of the │
           │      │ user data after the erase is     │
           │      │ indeterminate (e.g., the user    │
           │      │ data may be zero filled, one     │
           │      │ filled, etc). The controller may │
           │      │ perform a cryptographic erase    │
           │      │ when a User Data Erase is        │
           │      │ requested if all user data is    │
           │      │ encrypted.                       │
           ├──────┼──────────────────────────────────┤
           │2     │ Cryptographic Erase: All user    │
           │      │ data shall be erased             │
           │      │ cryptographically. This is       │
           │      │ accomplished by deleting the     │
           │      │ encryption key.                  │
           ├──────┼──────────────────────────────────┤
           │3–7   │ Reserved                         │
           └──────┴──────────────────────────────────┘

なお、ソースコードを読む限り1、これらはいずれも NVMe デバイスに発行するコマンドの引数として表現されているようだ。 言いかえると、製品のファームウェアのレベルで Secure Erase の機能が実装されていることを前提としている。 そのため、製品によってはサポートされていない可能性もある。

いじょう。


  1. 内部的には libnvme というライブラリが使われていて、ユーザ空間からは ioctl(2) でデバイスを操作する

Ubuntu で Linux カーネルのバージョンを変更する

たまに、新しい機能が使いたいなどの理由で、Linux カーネルのバージョンを新しくしたいときがある。 そんなとき、Ubuntu であればビルド済みのパッケージが提供されているため、比較的容易にカーネルを入れ替えることができる。

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

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 22.04 LTS
Release:    22.04
Codename:   jammy
$ uname -srm
Linux 5.15.0-33-generic aarch64

もくじ

下準備

パッケージをダウンロードするために、あらかじめ wget をインストールしておく。

$ sudo apt-get update
$ sudo apt-get install wget

パッケージをダウンロードする

Ubuntu は Kernel PPA でビルド済みの Linux カーネルを提供している。 以下のメインラインカーネルは、不具合の調査や次期リリースに向けたバージョン選定などを目的としているらしい。

kernel.ubuntu.com

上記から、お目当てのバージョンで、必要なパッケージを選んでダウンロードしてくる。 今回は、現時点 (2022-05) でメインラインカーネルとして提供されている最新バージョンの v5.18 を選んだ。

まずは、アーキテクチャに依存しない linux-headers パッケージはどの環境でも必要になる。

$ wget https://kernel.ubuntu.com/~kernel-ppa/mainline/v5.18/amd64/linux-headers-5.18.0-051800_5.18.0-051800.202205222030_all.deb

そして、次にアーキテクチャに依存したパッケージをそれぞれダウンロードする。 今回は ARM64 環境なので、それに対応するものを。 名前に 64k がつくものは ARM64 環境でページサイズを 64KB に拡張したビルドになっているが、そちらは選んでいない。

$ wget \
  https://kernel.ubuntu.com/~kernel-ppa/mainline/v5.18/arm64/linux-headers-5.18.0-051800-generic_5.18.0-051800.202205222030_arm64.deb \
  https://kernel.ubuntu.com/~kernel-ppa/mainline/v5.18/arm64/linux-image-unsigned-5.18.0-051800-generic_5.18.0-051800.202205222030_arm64.deb \
  https://kernel.ubuntu.com/~kernel-ppa/mainline/v5.18/arm64/linux-modules-5.18.0-051800-generic_5.18.0-051800.202205222030_arm64.deb

インストールする

ダウンロードが終わったら、上記を apt-get なり dpkg を使ってインストールする。

$ sudo apt-get install ./*.deb

インストールできたらシステムを再起動する。

$ sudo shutdown -r now

再起動が終わったら、新しい Linux カーネルでシステムが動作しているはず。

$ uname -srm
Linux 5.18.0-051800-generic aarch64

めでたしめでたし。

Python: 集約特徴量を作るための scikit-learn Transformer 互換クラスの実装例について

ふと、集約特徴量を作るための scikit-learn Transformer 互換な実装を巷であまり見かけないなと思った。 そこで、自作しているものを公開してみる。

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

$ sw_vers          
ProductName:    macOS
ProductVersion: 12.4
BuildVersion:   21F79
$ python -V                                   
Python 3.9.12
$ pip list | egrep "(pandas|scikit-learn)"
pandas                        1.4.2
scikit-learn                  1.1.1

もくじ

集約特徴量とは

まずは、このエントリで集約特徴量と呼んでいるものについて説明する。 あらかじめ断っておくと、一般的に明確な呼称と定義があるわけではない。 Pandas で言えば、groupby() してから agg() して、それを元のデータフレームあるいは別のデータフレームとマージしたカラムのこと。 これによって、行方向の加工だけでは得られない、予測にとって有益な情報を列方向 (= 別の行) から抽出できる。

たとえば Count Encoding なんかも、集約特徴量の一種と言える。 agg() で計算する統計量が、出現回数 (count) というだけ。 今回は、そういった特定の統計量に特化させるのではなく、汎用に使えるものが欲しいよねという気持ちがある。

どうして必要なのか

まず、集約特徴量を作るのに、特に大した労力は必要ない。 前述したとおり、groupby() してから agg() した上で、データフレームを merge() するだけで得られる。 では、何故わざわざ scikit-learn の Transformer 互換の実装が必要になるのか。 それは、新規のデータの予測に使う場合を考えてのこと。

たとえば、あらかじめ学習データとテストデータが静的な CSV ファイルとして与えられる状況を考えてみる。 この場合、テストデータに集約特徴量を付与するには、両者を単純に連結して計算しちゃうことも考えられる。 統計量を計算する対象が目的変数でなければ、テストデータにも同じデータはあるはずなので、できてしまう。 しかし、このやり方ではまったく新しいデータに特徴量を付与したくなったときに都合が良くない。 なぜなら、まったく新しいデータに適用するには、既存のデータで計算した統計量を何処かに保存しておく必要があるため。 統計量の入ったデータフレームをバラバラと管理するのは汎用性がないので、できればやりたくない。

じゃあ、どこに保存しておくかというと、何らかのインスタンスのメンバに入っていると都合が良さそう。 そして、こういった場合によく用いられるのが scikit-learn の Transformer API を実装したクラス、ということになる。 scikit-learn の Transformer は、学習するだけの fit() メソッドと、適用するだけの transform() メソッドがある。 これによって、fit() で統計量を計算して、transform() でデータフレームとマージ、と処理が分けられる。

下準備

前置きが長くなったけど、ここからは実際にサンプルコードを示していく。 あらかじめ、必要なパッケージをインストールしておく。

$ pip install pandas scikit-learn

実装例

以下に集約特徴量を生成するための scikit-learn Transformer 互換の API を備えたクラスを示す。 使い方については後述する。

from __future__ import annotations

from typing import Callable
from typing import Iterable
from typing import Union

import pandas as pd
from sklearn.base import TransformerMixin
from sklearn.base import BaseEstimator


class AggregationEncoder(TransformerMixin, BaseEstimator):
    """集約特徴量を生成するための scikit-learn Transformer 互換 API を備えたクラス

    pandas の DataFrame が入力と出力になる
    """

    def __init__(
        self,
        group_keys: list[Union[str, Iterable[str]]],
        group_values: list[str],
        agg_methods: list[Union[str, Callable]],
    ):
        """

        :param group_keys: グループ化に使うカラム名
        :param group_values: 集約特徴量を計算するカラム名
        :param agg_methods: 計算する集約特徴量の種類 (統計量)
        """
        self.group_keys = group_keys
        self.group_values = group_values
        self.agg_methods = agg_methods

        # エンコードしたカラム名の一覧
        self.encoded_cols_: list[str] = []
        # Aggregation したデータフレームを記録しておく場所
        self._group_df_agg: list[tuple[Union[str, Iterable[str]], pd.DataFrame]] = []

    def fit(self, input_df: pd.DataFrame) -> TransformerMixin:
        """集約特徴量の生成に使う統計量を学習する

        :param input_df: 集約特徴量を生成するための統計量を求めるデータ
        """
        for agg_key in self.group_keys:
            for agg_method in self.agg_methods:

                # 集約方法の名前を作る
                if isinstance(agg_method, str):
                    # 文字列ならそのまま使う
                    agg_method_name = agg_method
                elif callable(agg_method):
                    # 呼び出し可能オブジェクトは名前を取り出して使う
                    agg_method_name = agg_method.__name__
                else:
                    raise ValueError(
                        "'agg_methods' must be a list of str or callable objects"
                    )

                for agg_value in self.group_values:
                    # 集約特徴量を作るために必要なカラムを求める
                    need_cols = [agg_value]
                    if isinstance(agg_key, str):
                        need_cols += [agg_key]
                    elif isinstance(agg_key, Iterable):
                        need_cols += list(agg_key)
                    else:
                        raise ValueError(
                            "'group_keys' must be a list of str or iterable objects"
                        )
                    # 集約特徴量を作るための計算
                    df_group_by = input_df[need_cols].groupby(agg_key)
                    df_agg = df_group_by[[agg_value]].agg(agg_method)

                    # 追加するカラムの名前を生成する
                    agg_key_name = (
                        agg_key if isinstance(agg_key, str) else "".join(agg_key)
                    )
                    # 名前の生成パターンが気に食わないときはココを変更する
                    col_name = "agg_{0}_{1}_groupby_{2}".format(
                        agg_method_name,
                        agg_value,
                        agg_key_name,
                    )
                    df_agg.columns = [col_name]

                    self.encoded_cols_.append(col_name)
                    self._group_df_agg.append((agg_key, df_agg))
        return self

    def transform(self, input_df: pd.DataFrame) -> pd.DataFrame:
        """集約特徴量を生成する

        :param input_df: 学習済みの集約特徴量を生成する対象となるデータ
        :return: 生成された集約特徴量
        """
        # 副作用が生じないようにコピーする
        new_df = input_df.copy()

        # 生成対象のデータフレームとマージしていく
        for group_key, df_agg in self._group_df_agg:
            new_df = pd.merge(
                new_df, df_agg, how="left", right_index=True, left_on=group_key
            )

        # 元々あったカラムは落とす
        new_df.drop(input_df.columns, axis=1, inplace=True)

        return new_df

    def fit_transform(self, input_df: pd.DataFrame) -> pd.DataFrame:
        """集約特徴量を学習して、同時に生成まで行う

        :param input_df: 集約特徴量を生成する対象のデータ
        :return: 生成された集約特徴量
        """
        self.fit(input_df)
        return self.transform(input_df)

    def get_params(self, deep=True) -> dict[str, object]:
        return {
            "group_keys": self.group_keys,
            "group_values": self.group_values,
            "agg_methods": self.agg_methods,
        }

上記を aggregation.py とか適当な名前で保存しておこう。

使ってみる

上記を利用するサンプルコードをいくつか書いてみよう。 上記のファイルがあるのと同じディレクトリで Python の REPL を立ち上げる。

$ python

基本的な使い方

たとえば、以下のような Pandas のデータフレームがあるとする。

>>> data = {
...     "group": ["A", "A", "A", "B", "B", "C"],
...     "value": [10, 20, 30, 40, 50, 60],
... }
>>> import pandas as pd
>>> df = pd.DataFrame(data)
>>> df
  group  value
0     A     10
1     A     20
2     A     30
3     B     40
4     B     50
5     C     60

上記から AggregationEncoder を使って集約特徴量を計算する準備をする。 まず、グルーピングのキーとして group_keys 引数にリストで group カラムを指定する。 同様に、バリューとして group_values 引数にリストで value カラムを指定する。 計算する統計量としては、agg_methods 引数で出現回数 (count) と平均 (mean) を指定した。

>>> from aggregation import AggregationEncoder
>>> encoder = AggregationEncoder(
...     group_keys=["group"],
...     group_values=["value"],
...     agg_methods=["count", "mean"]
... )

先ほどのデータフレームに対して fit_transform() メソッドを呼び出すと、次のとおり集約特徴量が生成される。 それぞれ、グループの出現回数と平均値になっている。

>>> encoder.fit_transform(df)
   agg_count_value_groupby_group  agg_mean_value_groupby_group
0                              3                          20.0
1                              3                          20.0
2                              3                          20.0
3                              2                          45.0
4                              2                          45.0
5                              1                          60.0

まったく新しいデータに適用する

前述したとおり、scikit-learn の Transformer にしたからには、新規のデータに適用したい。 次はそれを試してみよう。

以下のとおり、新しいデータフレームを用意する。 このデータフレームには、学習したデータには含まれていなかった Z という値も含まれている。

>>> data = {
...     "group": ["A", "B", "C", "Z"],
... }
>>> test_df = pd.DataFrame(data)
>>> test_df
  group
0     A
1     B
2     C
3     Z

先ほど学習させた AggregationEncodertransform() メソッドを呼び出してデータフレームを渡す。 すると、学習データの統計量を元に集約特徴量が生成されることがわかる。 初見の値については NaN になっている。

>>> encoder.transform(test_df)
   agg_count_value_groupby_group  agg_mean_value_groupby_group
0                            3.0                          20.0
1                            2.0                          45.0
2                            1.0                          60.0
3                            NaN                           NaN

特徴量を総当り的に作る

こういった特徴量は、総当り的にばーっと作って、とりあえずモデルに突っ込むというパターンが頻出する。 なので、そういったニーズも満たせるように作ってある。

たとえば、次のようにカテゴリ変数と連続変数が 2 つずつあるようなデータフレームを考える。

>>> import numpy as np
>>> data = {
...     "group1": ["A", "A", "A", "B", "B", "C"],
...     "group2": ["x", "y", "z", "x", "y", "z"],
...     "value1": [10, 20, 30, 40, 50, 60],
...     "value2": [np.nan, 500, 400, 300, 200, 100],
... }
>>> df = pd.DataFrame(data)
>>> df
  group1 group2  value1  value2
0      A      x      10     NaN
1      A      y      20   500.0
2      A      z      30   400.0
3      B      x      40   300.0
4      B      y      50   200.0
5      C      z      60   100.0

この場合は、group_keysgroup_values に計算したいカラムをリストで放り込んでおけばいい。

>>> encoder = AggregationEncoder(
...     group_keys=["group1", "group2"],
...     group_values=["value1", "value2"],
...     agg_methods=["min", "max"]
... )

あとは、「グルーピングのキー x グルーピングのバリュー x 特徴量の種類」の組み合わせで特徴量をばーっと作れる。 今回であれば「2 x 2 x 2 = 8」カラムになる。

>>> encoder.fit_transform(df)
   agg_min_value1_groupby_group1  agg_min_value2_groupby_group1  ...  agg_max_value1_groupby_group2  agg_max_value2_groupby_group2
0                             10                          400.0  ...                             40                          300.0
1                             10                          400.0  ...                             50                          500.0
2                             10                          400.0  ...                             60                          400.0
3                             40                          200.0  ...                             40                          300.0
4                             40                          200.0  ...                             50                          500.0
5                             60                          100.0  ...                             60                          400.0

[6 rows x 8 columns]

生成されるカラム名を取得する

組み合わせが増えると、どんなカラムができるんだっけ?と後から確認したくなる。 なので、生成されるカラムの名前を encoded_cols_ メンバで得られる。

>>> from pprint import pprint
>>> pprint(encoder.encoded_cols_)
['agg_min_value1_groupby_group1',
 'agg_min_value2_groupby_group1',
 'agg_max_value1_groupby_group1',
 'agg_max_value2_groupby_group1',
 'agg_min_value1_groupby_group2',
 'agg_min_value2_groupby_group2',
 'agg_max_value1_groupby_group2',
 'agg_max_value2_groupby_group2']

複数のキーでグルーピングしたい

複数のキーを使ってグルーピングしたい、という場合もあるはず。 そんなときは group_keys の要素にリストで複数のカラムを放り込む。

>>> encoder = AggregationEncoder(
...     group_keys=[["group1", "group2"]],
...     group_values=["value1"],
...     agg_methods=["mean"]
... )

上記であれば group1group2 の両方が一致するものを、同じグループとして扱う。 今回の指定とデータフレームだと、同じグループが存在しないので value1 の値がそのまま出てしまうけど。

>>> encoder.fit_transform(df)
   agg_mean_value1_groupby_group1group2
0                                  10.0
1                                  20.0
2                                  30.0
3                                  40.0
4                                  50.0
5                                  60.0

カスタマイズした統計量を計算する

既存の統計量だけでなく、自分なりの統計量を計算したいというニーズもある。 そんなときは、各グループで層化されたデータフレームを受け取って統計量を計算する関数を用意する。 以下では、たとえば中央値を計算している。 まあ、これは単に "median" を指定すれば良いだけなんだけど。

>>> def median(x):
...     return x.quantile(0.5)
... 

上記の関数を agg_methods に放り込めば良い。

>>> encoder = AggregationEncoder(
...     group_keys=["group1"],
...     group_values=["value1"],
...     agg_methods=[median]
... )

後は何も変わらない。カラム名の中には関数名が入る。

>>> encoder.fit_transform(df)
   agg_median_value1_groupby_group1
0                              20.0
1                              20.0
2                              20.0
3                              45.0
4                              45.0
5                              60.0

まとめ

今回は、集約特徴量を作るための scikit-learn Transoformer 互換クラスの実装例を紹介した。