Macで特定のポートが使用中かどうか調べる

Jupyter LabをローカルのMacで使っており、それを起動するバッチを自作して使っているのですが、たまにすでに起動してるのにそのバッチを実行してしまってJupyterのプロセスを2重に立ち上げてしまうことが度々ありました。

2個立ち上げっぱなしとなると気持ち悪いのでPIDを調べてkillする必要がありやらかすとちょっと面倒なミスです。

最初、この対策としてプロセスをgrepしてjupyterがすでに立ち上がってたらバッチを中断して起動コマンドを叩かない、みないな処理を入れて対応しようとも試みていたのですが、grep jupyter 自身がそのgrepにヒットするというなかなか困った挙動をしうまくいかずに放置していました。

しかしその後、プロセス一覧ではなくポートが使用中かどうかで判断すれば良いと思いついたので実装してみました。

利用するのは、lsof というコマンドです。これはポートに限らず、システムで開いているファイルの一覧を表示するコマンドです。名前も List Open Files の略だそうです。

今回はポートの情報だけ知れたらいいので、 -i 引数をつけます。そして、さらにコロンを打ってポート番号を指定すると、そのポートが使用されていれば使用しているプロセスの一覧が各種情報とともにずらずらと表示されます。

Jupyter Labは デフォルトでは 8888番ポートで動作するので、次のようなコマンドで確認できます。

$ lsof -i :8888
# 8888番ポートが使われていなければ何も表示されない。

# Jupyter Labが起動していると各プロセスが結果に出る。
COMMAND    PID   USER   FD   TYPE            DEVICE SIZE/OFF NODE NAME
Google     808 yutaro   21u  IPv6 0x12572c541560807      0t0  TCP localhost:49319->localhost:ddi-tcp-1 (ESTABLISHED)
Google     808 yutaro   34u  IPv4 0x12572d3a3516cff      0t0  TCP localhost:49234->localhost:ddi-tcp-1 (ESTABLISHED)
Google     808 yutaro   37u  IPv6 0x12572c541540007      0t0  TCP localhost:49253->localhost:ddi-tcp-1 (ESTABLISHED)
Google     808 yutaro   80u  IPv6 0x12572c54155b807      0t0  TCP localhost:49213->localhost:ddi-tcp-1 (ESTABLISHED)
Google     808 yutaro  108u  IPv6 0x12572c541546007      0t0  TCP localhost:49216->localhost:ddi-tcp-1 (ESTABLISHED)
python3.1 1096 yutaro   11u  IPv4 0x12572d3a351ba6f      0t0  TCP localhost:ddi-tcp-1 (LISTEN)
python3.1 1096 yutaro   12u  IPv6 0x12572c54154a007      0t0  TCP localhost:ddi-tcp-1 (LISTEN)
python3.1 1096 yutaro   13u  IPv6 0x12572c54155e807      0t0  TCP localhost:ddi-tcp-1->localhost:49319 (ESTABLISHED)
python3.1 1096 yutaro   14u  IPv6 0x12572c54155c007      0t0  TCP localhost:ddi-tcp-1->localhost:49213 (ESTABLISHED)
python3.1 1096 yutaro   16u  IPv6 0x12572c541548007      0t0  TCP localhost:ddi-tcp-1->localhost:49216 (ESTABLISHED)
python3.1 1096 yutaro   37u  IPv4 0x12572d3a351780f      0t0  TCP localhost:ddi-tcp-1->localhost:49234 (ESTABLISHED)
python3.1 1096 yutaro   50u  IPv6 0x12572c54153e807      0t0  TCP localhost:ddi-tcp-1->localhost:49253 (ESTABLISHED)

Python は3.11を使っているのに、COMMAND名が途中で切り捨てられて3.1って表示されていますが、まぁ、Pythonで何か動いているのはわかりますね。

あとは、Jupyter Labの起動バッチで、このコマンドを動かして処理を分岐させればOKです。

(自分はnohupコマンドの結果を専用のログファイルにログを書き出しているのですが、不要なら/dev/null あたりに捨てても良いと思います。)

if 文の条件文の中で出力を /dev/nullに捨てているのは出力をすっきりさせるためです。捨てない場合は普通に画面にlsofコマンドの結果も表示されます。

#!/usr/bin/env zsh
if lsof -i:8888 > /dev/null; then
    echo "8888番ポートは既に使用されています。"
    exit 1
else
    cd ~
    nohup jupyter lab >> ~/Documents/log/jupyter_lab.log 2>&1 &!
    cd -
fi

cd ~ はホームディレクトリに移動する、 cd – は元のディレクトリに戻る、です。
どこで打ってもホームディレクトリでJupyter Labが起動されるように、そしてそのあとは元のディレクトリに帰るようにしています。

この記事を書いた後に気づいきましたが、このlsofコマンドは以前こちらの記事でも使っていましたね。
参考: sshtunnel を使って踏み台サーバー経由でDB接続

コマンドでMacbookのスリープを一時的に抑制する

バッテリーの持ちとセキュリティ関連の理由により、一定時間アイドル状態になったらスリープする設定でMacを使っている方は多いと思います。
僕もそうです。

ただ、機械学習のモデルを学習している場合や、ベイズモデルでMCMCのサンプリングをやっている時など、しばらく作業の待ち時間が発生することがあり、その脇で本を世だりして時間を潰しているといつの間にかPCがスリープしてしまうということがあります。

普通にシステム環境設定からスリープまでの時間の設定を変えて、作業後に戻せばいいだけの話なのですが、それはやや手間です。

しかし、最近コマンドで一時的にスリープを抑制できることを知ったのでそれを紹介します。

コマンド名は caffeinate です。名前はコーヒーのカフェインが由来だとか。面白いですね。

細かいオプションが複数あり、それらを組み合わせて使います。

-d ・・・ ディスプレイのスリープを抑制
-i ・・・ システムのスリープを抑制
-m ・・・ ディスクのスリープを抑制
-s ・・・ AC電源で動作している場合にシステムのスリープを抑制
-u ・・・ -tとセットで利用し、ユーザーがアクティブであることを宣言する
-t ・・・ -uの時間を秒数で指定する

例えば30分間くらいスリープしたくないのであればこんな感じですね。

$ caffeinate -d -u -t 1800

-t で時間を指定するとその時間経過時に勝手に終了しますが、そうでない場合は Ctrl + d でコマンドを中断するまで抑制してくれます。

そもそも一定時間でスリープするような設定にしている事情(おそらくセキュリティ的な話)があると思うので、個人的には-tで時間を指定して使うのがお勧めです。

scipy.statsで確率分布を自作する

SciPyのstatsモジュールには非常に多くの確率分布が定義されています。
参考: Statistical functions (scipy.stats) — SciPy v1.12.0 Manual

ほとんどの用途はこれらを利用すると事足りるのですが、自分で定義した確率分布を扱うこともあり、scipyのstatsに用意されているような確率分布と同じようにcdfとかのメソッドを使いたいなと思うことがあります。

そのような場合、SciPyではscipy.stats.rv_continuousを継承してpdfメソッドを定義することで確率分布を定義でき、そうすれば(計算量や時間の問題などありますが)cdf等々のメソッドが使えるようになります。

参考: scipy.stats.rv_continuous — SciPy v1.12.0 Manual

実はSciPyの元々用意されている確率分布たちもこのrv_continuousを継承して作られているので、それらのソースコードを読むと使い方の参考になります。既存の確率分布等はpdfだけでなく、cdf、sf、ppf、期待値や分散などの統計量などが明示的に実装されていて計算効率よく使えるようになっています。

しかし先ほども述べた通り、pdf以外は必須ではなく実装されていなければpdfから数値的に計算してくれます。(ただ、予想外のところで計算が終わらなかったり精度が不十分だったりといった問題が起きるので可能な限り実装することをお勧めします。)

確率分布クラスの自作方法

さて、それでは実際にやってみましょう。既存に存在しない例が良いと思うので、正規分布を2個組み合わせた、山が2個ある分布を作ってみます。

クラスを継承して _pdf (pdfではない) メソッドをオーバーライドする形で実装します。

import numpy as np
from scipy.stats import rv_continuous
import matplotlib.pyplot as plt


# 確率密度関数の定義
def my_pdf(x):
    curve1 = np.exp(-(x+3)**2 / 2) / np.sqrt(2 * np.pi)
    curve2 = np.exp(-(x-3)**2 / 2) / np.sqrt(2 * np.pi)

    return (curve1 + curve2)/2


# rv_continuousクラスのサブクラス化
class MyDistribution(rv_continuous):
    def _pdf(self, x):
        return my_pdf(x)


# 分布のインスタンス化
my_dist = MyDistribution(name='my_dist')


# 可視化
x = np.linspace(-6, 6, 121)
fig = plt.figure(facecolor="w", figsize=(8, 8))
ax = fig.add_subplot(2, 2, 1, title="pdf")
ax.plot(x, my_dist.pdf(x))

ax = fig.add_subplot(2, 2, 2, title="cdf")
ax.plot(x, my_dist.cdf(x))

ax = fig.add_subplot(2, 2, 3, title="sf")
ax.plot(x, my_dist.sf(x))

ax = fig.add_subplot(2, 2, 4, title="logpdf")
ax.plot(x, my_dist.logpdf(x))

plt.show()

結果がこちらです。

実装したのはpdfだけでしたが、cdfやsf、logpdfなども動いていますね。
繰り返しになりますが、これらは数値計算されたものなので十分注意して扱ってください。特に精度と計算量はもちろんですが、指数関数等もからむので、極端な値を入れたら計算途中でオーバーフローが起きたりもします。重要なものは_pdfと同様に_cdfなどとして明示的に定義した方が良いでしょう。

たとえば、先ほどの分布は分散の計算に少し時間がかかります。

%%time
my_dist.var()
"""
CPU times: user 8.02 s, sys: 146 ms, total: 8.16 s
Wall time: 8.06 s
10.000000000076088
"""

確率密度関数を用意するときの注意点

確率密度関数を自分で作りましたが、この内容に対してのバリデーションなどは用意されていないので自分で責任を持って管理する必要があります。

たとえば、変数xの定義域で正であること、定義域全体で積分したら1になることなどは事前に確認しておく必要があります。(確率密度関数の定義を満たしてないととcdfなど他のメソッドが変な動きをします)

確率分布の台が有限の場合

もう一点、先ほどの2山の分布はxが$-\infty$から$\infty$の値を取る分布でしたが、確率分布の中にはそうではないものもあります。正の範囲でしか定義されない対数正規分布や指数分布、有限区間でしか定義されないベータ分布などですね。

これらについては、「分布のインスタンス化」を行うタイミングで台の下限上限をそれぞれa, b という引数で行います。省略した方は$-\infty$もしくは$\infty$となります。

確率密度関数が上に凸な放物線で定義された分布でやるとこんな感じです。

def my_pdf2(x):
    return 6*x*(1-x)


class MyDistribution2(rv_continuous):
    def _pdf(self, x):
        return my_pdf2(x)


# 分布のインスタンス化
my_dist2 = MyDistribution2(a=0, b=1, name='my_dist2')

以上が、SciPyで連続確率分布インスタンスを自作する方法でした。

SciPyで重積分

もう結構古い記事なのですが、以前SciPyで定積分をやる方法を記事にしたことがあります。
参考: scipyで定積分

最近、2変数関数の積分をやる機会があったのでこの機会に重積分をSciPyで行う方法を紹介します。SciPyのintegrateモジュールには、重積分用の関数が複数あります。
dblquad (2変数関数の定積分)
tplquad (3変数関数の定積分)
nquad (一般のn変数の定積分)

dblquadの使い方

順番に説明していきます。まずは2重積分のdblquadです。関数の定義は次のようになっています。
scipy.integrate.dblquad(func, a, b, gfun, hfun, args=(), epsabs=1.49e-08, epsrel=1.49e-08)

必須なのは、積分対象のfunc, 外側の積分区間のa, b、そして内側の積分区間を示す、gfun, hfunです。

gfunとhfunは名前からわかる通り、定数ではなく関数です。これにより内側の積分の積分区間を変数にすることができます。つまり以下のような積分区間の積分ができます。
$$\int_0^1\int_0^y xy \,dxdy$$

例に挙げたのでこれを実装してみましょう。ちなみに解は$1/8=0.125$です。funcの定義は、内側の関数を第1引数にする必要があるので注意してください。

from scipy.integrate import dblquad


def f1(x, y):
    return x*y


def x0(y):
    return 0


def x1(y):
    return y


print(dblquad(f1, 0, 1, x0, x1))
# (0.125, 5.515032205777789e-15)

想定通りですね。積分結果と推定誤差が返ってくるのは1変数の積分と同様です。

内側の積分区間も定数から定数までだよ、要するに長方形領域で積分したいよ、って場合はgfun, hfun に定数を返す関数を返してください。

tplquadの使い方

続いて、3変数向けのtplquadです。これもdblquadとかなり似てる感じで使えます。積分変数が一個増えているので上限加減の指定がもう一個ある感じです。
scipy.integrate.tplquad(func, a, b, gfun, hfun, qfun, rfun, args=(), epsabs=1.49e-08, epsrel=1.49e-08)

たとえば次の積分をやってみましょう。

$$\int_0^1\int_0^z\int_0^{y+z} xyz \,dxdydz.$$

ちなみに答えは$17/144=0.11805555…$となるはずです。

引数の順番に注意が必要なので慎重にコーディングしてください。

from scipy.integrate import tplquad


def f2(x, y, z):
    return x*y*z


def x0(y, z):
    return 0


def x1(y, z):
    return y + z


def y0(z):
    return 0


def y1(z):
    return z


print(tplquad(f2, 0, 1, y0, y1,  x0, x1))
# (0.11805555555555557, 2.1916761217856673e-14)

バッチリですね。

nquadの使い方

最後に一般のn変数を積分できるnquadの使い方を紹介します。

引数の形式が先ほどの二つと少し違います。
scipy.integrate.nquad(func, ranges, args=None, opts=None, full_output=False)

funcにn変数関数を渡して、rangesに積分区間を渡すことになります。rangesは配列で、1変数目から順番に区間の下限上限の2値の配列を格納しておけば良いです。また、ここにも一応関数を使うことはできます。

これはシンプルな例で、定数関数1を超立方体区間で積分してみました。

from scipy.integrate import nquad


def f3(w, x, y, z):
    return 1

print(nquad(f3, [[-2, 2], [-2, 2], [-2, 2], [-2, 2]]))
# (256.0, 2.8421709430404007e-12)

$4^4=256$になりましたね。

ここで急にシンプルな例を出したのには事情がありまして、変数の数が多くなるとやはり積分は困難なようで、ちょっと複雑な例になると規定の反復回数をこなしても必要な精度に届かずWarningが出たりするケースが多々あります。

どうしても計算したい場合は limit パラメーター等をいじっての対応になりますのでドキュメントを参照しながら調整してみてください。
(僕も実運用で必要になったら改めて調査して紹介しようと思います。)