「ベイズ統計モデリングによるデータ分析入門」を読んだ
ベイズを勉強してみたいと思って本を探していたら、こちらの「RとStanで始めるベイズ統計モデリングによるデータ分析入門」がわかりやすいと評判だったので買ってみた。
自分はRはほとんど書いたことがないのだが、それでもこの本はわかりやすかった。
構成
この本の前半はプログラムを書くよりむしろベイズ統計モデリングの理論的な側面に重点をおいて説明されている。最初に中学・高校あたりで習う確率の知識をさらっと復習したあとに確率分布、ベイズ統計、そしてマルコフ連鎖モンテカルロ法(MCMC)の説明がある。
そしてRの簡単な説明をはさんで、実践編として
- 一般化線形モデル
- 一般化線形混合モデル
- 状態空間モデル
を実際に試してみるという構成になっている。ガチガチの専門書のようにMCMCのアルゴリズムが数式をふんだんに使って説明されているわけではなく、むしろアルゴリズムが何をしたくてこのようなことをやっているかという点が詳しく説明されている。
実践編ではシンプルな一般化線形モデルから具体的なデータセットを用いた演習が始まり、一般化線形混合モデル、状態空間モデルへと展開されていく。話の流れとしては前のものをどんどんと拡張していくという方向へ向かっていくので、ここの章の内容まででは表現できなかった部分がうまくモデリングされていく過程を実体験することができ、読んでいて飽きない。
また、実践編ではRを使ってチュートリアルのように自分で手を動かすことができる。自分は普段、データ分析を行う際にはPythonを使うため、Rはほとんど初心者であるが、RはJupyter Notebookでも使えるので環境の構築は簡単だった。一つ大変だったのはグラフの大きさをなかなか変えられなかったくらいだと思う。グラフの大きさを変えても一番外枠の大きさは変わらず、その中でグラフのアスペクト比だけが変わってしまって大変だった(伝われ)。
この本を読み終わって思ったのだが、この本のあとに久保拓弥先生の緑本を読むといいかもしれない。
この本もRを使っている本(ただしMCMCの実験にはStanではなくWinBUGSというソフトウェアを使っている)であるが、少し馬場先生の本と比べると専門色が濃く、馬場先生の本では詳しくは説明されていなかった過分散が詳しく説明されている他、階層ベイズモデルまで説明されている。自分も何回か読んだはずなのだがそこまで内容を覚えていないのでこの機会にもう一度読み返してみようかななどと思っている。
Docker for Windowsがストレージを解放しない問題
Docker for Windowsにおいて、イメージを削除してもディスクの容量が解放されないという問題があるらしい。私の環境でも確認してみたら、Dockerが50 GBほどの容量を使っていることがわかった。コンテナは毎回使うごとに消しているし、volumeやnetworkを docker volumes prune
等を使って消してもこんなだったので解決策を探してみた。
結論から言うと、Windows Homeの場合にはDockerのデータを全部消さないと現時点では解決しないみたいである(Windows Proだと圧縮とかできるみたいだが)。
github.com
上のIssueは2019年に出されたみたいだがまだ解決していないようなので結構根深い問題なのかもしれない。
データが全部消えてもDockerfileは残してあるし、必要ならばすぐにビルドしなおせばよさそうだったのでやってみた。
Docker Desktop for Windowsを起動して右上のTrouble Shootingを開く(虫のマークのやつ) Clean/Purge Data(赤くなっているやつ)を押す。 WSLにチェックを入れてDelete。いままでビルドしたイメージが全部消えるのでいくつかビルドしなおしてちゃんとビルドできることを確認してみた後、Dockerのデータの容量を確認してみた。 わかりにくいが容量が5 GBほどになっており、消す前の1/10の容量になった。いままでビルドしてきたイメージやボリュームが消えてもすぐ復旧できるという状況の人は試してみると幸せになれるかもしれない。
SRGANで画像の高解像度化
今回はGANを使った初期の画像の高解像度化モデルであるSRGANを実装してみたので紹介したいと思います。
実行環境
今回のモデルは以下のような環境で実装しています。
ソースコードは以下に公開しています。
github.com
検証に用いたデータセットはDIV2Kで以下からダウンロードしました。
data.vision.ee.ethz.ch
訓練に用いるために、各画像から20枚ずつ32x32のランダムクロップを行いました。
SRGANとは
SRGANとは、その名前にもあるようにGAN(敵対的生成ネットワーク)を使って、入力画像に対して解像度が上がった画像を生成しようという目的で作られたモデルです。
モデルの実装
モデルはTensorflowのKeras APIを使って実装します。
import tensorflow as tf from tensorflow.keras.layers import ( Conv2D, Dense, PReLU, BatchNormalization, LeakyReLU, Flatten, ) from tensorflow.keras import Sequential, Model class BResidualBlock(Model): def __init__(self): super(BResidualBlock, self).__init__() self.conv = Conv2D(filters=64, kernel_size=3, strides=1, padding="same") self.bn = BatchNormalization(momentum=0.8) self.prelu = PReLU(shared_axes=[1, 2]) self.conv2 = Conv2D(filters=64, kernel_size=3, strides=1, padding="same") self.bn2 = BatchNormalization(momentum=0.8) def call(self, input_tensor, training=True): x = self.conv(input_tensor) x = self.bn(x, training=training) x = self.prelu(x) x = self.conv2(x) x = self.bn2(x, training=training) x += input_tensor return x class ResidualBlock(Model): def __init__(self): super(ResidualBlock, self).__init__() self.residual1 = BResidualBlock() self.residual2 = BResidualBlock() self.residual3 = BResidualBlock() self.residual4 = BResidualBlock() self.residual5 = BResidualBlock() self.conv = Conv2D(filters=64, kernel_size=3, padding="same") self.bn = BatchNormalization(momentum=0.8) def call(self, input_tensor, training=True): x = self.residual1(input_tensor) x = self.residual2(x, training=training) x = self.residual3(x, training=training) x = self.residual4(x, training=training) x = self.residual5(x, training=training) x = self.conv(x) x = self.bn(x) x += input_tensor return x class DiscriminatorBlock(Model): def __init__(self, filters=128): super(DiscriminatorBlock, self).__init__() self.filters = filters self.conv1 = Conv2D(filters=filters, kernel_size=3, strides=1, padding="same") self.bn1 = BatchNormalization(momentum=0.8) self.lrelu1 = LeakyReLU(alpha=0.2) self.conv2 = Conv2D(filters=filters, kernel_size=3, strides=2, padding="same") self.bn2 = BatchNormalization(momentum=0.8) self.lrelu2 = LeakyReLU(alpha=0.2) def call(self, input_tensor, training=True): x = self.conv1(input_tensor) x = self.bn1(x, training=training) x = self.lrelu1(x) x = self.conv2(x) x = self.bn2(x, training=training) x = self.lrelu2(x) return x class PixelShuffler(tf.keras.layers.Layer): def __init__(self): super(PixelShuffler, self).__init__() def call(self, input_tensor): x = tf.nn.depth_to_space(input_tensor, 2) return x def make_generator(): model = Sequential( [ Conv2D(filters=64, kernel_size=9, padding="same"), PReLU(shared_axes=[1, 2]), ResidualBlock(), Conv2D(filters=256, kernel_size=3, padding="same"), PixelShuffler(), PReLU(shared_axes=[1, 2]), Conv2D(filters=256, kernel_size=3, padding="same"), PixelShuffler(), PReLU(shared_axes=[1, 2]), Conv2D(filters=3, kernel_size=9, padding="same"), ] ) return model def make_discriminator(): model = Sequential( [ Conv2D(filters=64, kernel_size=3, padding="same"), LeakyReLU(alpha=0.2), Conv2D(filters=64, kernel_size=3, strides=2, padding="same"), BatchNormalization(momentum=0.8), LeakyReLU(alpha=0.2), DiscriminatorBlock(128), DiscriminatorBlock(256), DiscriminatorBlock(512), Flatten(), Dense(1024), LeakyReLU(alpha=0.2), Dense(1, activation="sigmoid"), ] ) return model def make_vgg(height, width): vgg = tf.keras.applications.vgg19.VGG19(include_top=False, weights="imagenet") partial_vgg = tf.keras.Model( inputs=vgg.input, outputs=vgg.get_layer("block5_conv4").output ) partial_vgg.trainable = False partial_vgg.build(input_shape=(None, height * 4, width * 4, 3)) return partial_vgg
トレーニングの実行
Generatorのトレーニング
このモデルでは、GeneratorとDiscriminatorを同時に訓練するのではなく、先にGeneratorを訓練しておきます。これによりGeneratorが "local optima" にハマるのを防ぎます。Generatorの訓練では損失関数にMeanSquaredErrorを使います。
class SRResNetTrainer: def __init__( self, epochs: int = 10000, batch_size: int = 32, learning_rate: float = 1e-4, training_data_path: str = "./datasets/train.tfrecords", validate_data_path: str = "./datasets/valid.tfrecords", height: int = 32, width: int = 32, g_weight: str = None, checkpoint_path: str = "./checkpoint", best_generator_loss: float = 1e9, ): self.epochs = epochs self.batch_size = batch_size self.generator = make_generator() if g_weight is not None and g_weight != "": print("Loading weights on generator...") self.generator.load_weights(g_weight) self.train_data, self.validate_data = prepare_from_tfrecords( train_data=training_data_path, validate_data=validate_data_path, height=height, width=width, batch_size=batch_size, ) self.mse_loss = tf.keras.losses.MeanSquaredError() self.best_generator_loss = best_generator_loss self.generator_optimizer = tf.keras.optimizers.Adam(learning_rate) self.checkpoint_path = checkpoint_path self.make_checkpoint = len(checkpoint_path) > 0 current_time = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") train_log_dir = "./logs/" + current_time + "/train_generator" valid_log_dir = "./logs/" + current_time + "/valid_generator" self.train_summary_writer = tf.summary.create_file_writer(train_log_dir) self.valid_summary_writer = tf.summary.create_file_writer(valid_log_dir) @tf.function def train_step(self, lr: tf.Tensor, hr: tf.Tensor): with tf.GradientTape() as tape: generated_fake = self.generator(lr) g_loss = self.mse_loss(generated_fake, hr) generator_grad = tape.gradient(g_loss, self.generator.trainable_variables) self.generator_optimizer.apply_gradients( grads_and_vars=zip(generator_grad, self.generator.trainable_variables) ) return g_loss @tf.function def validation_step(self, lr: tf.Tensor, hr: tf.Tensor): generated_fake = self.generator(lr) g_loss = self.mse_loss(generated_fake, hr) return g_loss def train(self, start_epoch=0): for step in range(start_epoch, self.epochs): g_loss_train = [] for images in tqdm(self.train_data): g_loss = self.train_step(images["low"], images["high"]) g_loss_train.append(g_loss.numpy()) g_loss_train_mean = np.mean(g_loss_train) with self.train_summary_writer.as_default(): tf.summary.scalar("g_loss", g_loss_train_mean, step=step) print( f"Epoch {step+ 1}| Generator-Loss: {g_loss_train_mean:.3e},", ) g_loss_valid = [] for images in tqdm(self.validate_data): g_loss = self.validation_step(images["low"], images["high"]) g_loss_valid.append(g_loss) g_loss_valid_mean = np.mean(g_loss_valid) with self.valid_summary_writer.as_default(): tf.summary.scalar("g_loss", g_loss_valid_mean, step=step) print( f"Validation| Generator-Loss: {g_loss_valid_mean:.3e},", ) if self.make_checkpoint: self.generator.save_weights(f"{self.checkpoint_path}/generator_last") if g_loss_valid_mean < self.best_generator_loss: self.best_generator_loss = g_loss_valid_mean self.generator.save_weights( f"{self.checkpoint_path}/generator_best" ) print("Model Saved")
GANの訓練
Generatorを訓練したら、その重みを使ってGANの訓練を行います。先ほどとは違い、Generatorの損失関数にはBinaryCrossentropyとcontent lossと呼ばれるものを使っています。このロスでは、正解の高解像度画像とGeneratorの生成した画像をそれぞれVGG19に入れ、その中間層の出力を比べたときの二乗誤差を計算します。
class SRGANTrainer: def __init__( self, epochs: int = 100, batch_size: int = 16, learning_rate: float = 1e-4, height: int = 32, width: int = 32, g_weight: str = None, d_weight: str = None, training_data_path: str = "./datasets/train.tfrecords", validate_data_path: str = "./datasets/valid.tfrecords", checkpoint_path: str = "./checkpoints", best_generator_loss: float = 1e9, ): # ----------------------------- # Hyper-parameters # ----------------------------- self.epochs = epochs self.batch_size = batch_size # ----------------------------- # Model # ----------------------------- self.generator = make_generator() self.discriminator = make_discriminator() self.vgg = make_vgg(height=height, width=width) if g_weight is not None and g_weight != "": print("Loading weights on generator...") self.generator.load_weights(g_weight) if d_weight is not None and d_weight != "": print("Loading weights on discriminator...") self.discriminator.load_weights(d_weight) # ----------------------------- # Data # ----------------------------- self.train_data, self.validate_data = prepare_from_tfrecords( train_data=training_data_path, validate_data=validate_data_path, height=height, width=width, batch_size=batch_size, ) # ----------------------------- # Loss # ----------------------------- self.discriminator_loss_fn = tf.keras.losses.BinaryCrossentropy( from_logits=False ) self.mse_loss = tf.keras.losses.MeanSquaredError() self.bce_loss = tf.keras.losses.BinaryCrossentropy(from_logits=False) self.best_generator_loss = best_generator_loss # ----------------------------- # Optimizer # ----------------------------- self.discriminator_optimizer = tf.keras.optimizers.Adam(learning_rate) self.generator_optimizer = tf.keras.optimizers.Adam(learning_rate) # ----------------------------- # Summary Writer # ----------------------------- current_time = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") train_log_dir = "./logs/" + current_time + "/train" valid_log_dir = "./logs/" + current_time + "/valid" self.train_summary_writer = tf.summary.create_file_writer(train_log_dir) self.valid_summary_writer = tf.summary.create_file_writer(valid_log_dir) self.checkpoint_path = checkpoint_path self.make_checkpoint = len(checkpoint_path) > 0 @tf.function def _content_loss(self, lr: tf.Tensor, hr: tf.Tensor): lr = (lr + 1) * 127.5 hr = (hr + 1) * 127.5 lr = tf.keras.applications.vgg19.preprocess_input(lr) hr = tf.keras.applications.vgg19.preprocess_input(hr) lr_vgg = self.vgg(lr) / 12.75 hr_vgg = self.vgg(hr) / 12.75 return self.mse_loss(lr_vgg, hr_vgg) def _adversarial_loss(self, output): return self.bce_loss(tf.ones_like(output), output) @tf.function def train_step(self, lr: tf.Tensor, hr: tf.Tensor) -> tuple[tf.Tensor]: with tf.GradientTape() as g_tape, tf.GradientTape() as d_tape: generated_fake = self.generator(lr) real = self.discriminator(hr) fake = self.discriminator(generated_fake) d_loss = self.discriminator_loss_fn(real, tf.ones_like(real)) d_loss += self.discriminator_loss_fn(fake, tf.zeros_like(fake)) g_loss = self._content_loss(generated_fake, hr) g_loss += self._adversarial_loss(generated_fake) * 1e-3 discriminator_grad = d_tape.gradient( d_loss, self.discriminator.trainable_variables ) generator_grad = g_tape.gradient(g_loss, self.generator.trainable_variables) self.discriminator_optimizer.apply_gradients( grads_and_vars=zip( discriminator_grad, self.discriminator.trainable_variables ) ) self.generator_optimizer.apply_gradients( grads_and_vars=zip(generator_grad, self.generator.trainable_variables) ) return g_loss, d_loss @tf.function def validation_step(self, lr: tf.Tensor, hr: tf.Tensor): generated_fake = self.generator(lr) real = self.discriminator(hr) fake = self.discriminator(generated_fake) d_loss = self.discriminator_loss_fn(real, tf.ones_like(real)) d_loss += self.discriminator_loss_fn(fake, tf.zeros_like(fake)) g_loss = self._content_loss(generated_fake, hr) g_loss += self._adversarial_loss(generated_fake) * 1e-3 return g_loss, d_loss def train(self, start_epoch): for step in range(start_epoch, self.epochs): d_loss_train = [] g_loss_train = [] for images in tqdm(self.train_data): g_loss, d_loss = self.train_step(images["low"], images["high"]) g_loss_train.append(g_loss.numpy()) d_loss_train.append(d_loss.numpy()) g_loss_train_mean = np.mean(g_loss_train) d_loss_train_mean = np.mean(d_loss_train) with self.train_summary_writer.as_default(): tf.summary.scalar("g_loss", g_loss_train_mean, step=step) tf.summary.scalar("d_loss", d_loss_train_mean, step=step) print( f"Epoch {step+ 1}| Generator-Loss: {g_loss_train_mean:.3e},", f"Discriminator-Loss: {d_loss_train_mean:.3e}", ) d_loss_valid = [] g_loss_valid = [] for images in tqdm(self.validate_data): g_loss, d_loss = self.validation_step(images["low"], images["high"]) d_loss_valid.append(d_loss) g_loss_valid.append(g_loss) g_loss_valid_mean = np.mean(g_loss_valid) d_loss_valid_mean = np.mean(d_loss_valid) with self.valid_summary_writer.as_default(): tf.summary.scalar("g_loss", g_loss_valid_mean, step=step) tf.summary.scalar("d_loss", d_loss_valid_mean, step=step) print( f"Validation| Generator-Loss: {g_loss_valid_mean:.3e},", f"Discriminator-Loss: {d_loss_valid_mean:.3e}", ) if self.make_checkpoint: self.generator.save_weights(f"{self.checkpoint_path}/generator_last") self.discriminator.save_weights( f"{self.checkpoint_path}/discriminator_last" ) if g_loss_valid_mean < self.best_generator_loss: self.best_generator_loss = g_loss_valid_mean self.generator.save_weights( f"{self.checkpoint_path}/generator_best" ) self.discriminator.save_weights( f"{self.checkpoint_path}/discriminator_best" ) print("Model Saved")
訓練結果
訓練したモデルを使ってテストデータの高解像度化を行ってみました。ところどころノイズは載っていますが概ねよく高解像度化できているように見えます。
Django + Plotly.js でグラフをリアルタイム更新
最近、測定器からデータを取得してリアルタイムにデータを表示して確認したいという願望があってそれを実現する方法を考えてました。その1つの解決策としてDjangoを使ってWebアプリケーションのような形で実現する方法を思いつき、試しに作ってみたのでそれを共有したいと思います。
この記事では、プロットするデータは測定器から発生していますがPythonで得られるデータであればなんでもこの方法は応用できます。
なぜ Django?
- 測定器からのデータは GPIB 通信というもので来ていたのでそれを処理できる必要がある -> Python, C
- グラフを動的に変化させたい -> JavaScript
という要件があったので、Djangoを使ってWebアプリケーションとして書けば要件を満たすことができると考えました。
この記事で書かないこと
PyVISAを用いた測定器との通信方法
構成
フロントエンド側 (HTML + JavaScript)
HTML
<script type="text/javascript" src="{% static 'core/js/plotly-2.3.0.min.js' %}" ></script> <div id="canvas" style="width: 700px; height: 400px"></div> <form id="tds-settings" action="{% url 'core:tds_data' %}"> <div id="stage-position-settings"> <div id="start"> <p>Start:</p> <input type="number" id="start-position" /> μm </div> <div id="end"> <p>End:</p> <input type="number" id="end-position" /> μm </div> <div id="step"> <p>Step:</p> <input type="number" id="moving-step" /> μm </div> </div> <p>Lock-in time:</p> <input type="number" id="lockin-time" /> ms <input type="submit" value="Start" id="start-button" /> </form>
グラフをプロットするエリア(id="canvas"
)と測定の設定を決めるフォームを用意します。フォームがsubmitされたときのアクションにはDjango側で用意するAPIのアドレスを入れます。
var trace1 = { x: [], y: [], type: "scatter", }; var data = [trace1]; var layout = { width: 600, height: 400, margin: { l: 50, r: 0, b: 3, t: 20, pad: 5 }, showlegend: true, legend: { orientation: "h" }, }; Plotly.newPlot("canvas", data, layout);
const _sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); document .getElementById("tds-settings") .addEventListener("submit", async (e) => { e.preventDefault(); tds_measurement(); }); async function tds_measurement() { let url = document.getElementById("tds-settings").action; let boot = tds_boot_url; let start = Number(document.getElementById("start-position").value); let end = Number(document.getElementById("end-position").value); let step = Number(document.getElementById("moving-step").value); let lockin = Number(document.getElementById("lockin-time").value); if (start >= end || start < 0 || end <= 0 || step <= 0 || lockin <= 0) { alert("Invalid Parameters"); return; } let finished = false; fetch(boot, { method: "POST", body: `start=${start}&end=${end}&step=${step}&lockin=${lockin}`, headers: { "Content-Type": "application/x-www-form-urlencoded; charset=utf-8", }, }).catch((error) => { finished = true; }); await _sleep(1000); while (!finished) { await fetch(url, { method: "POST", body: "", headers: { "Content-Type": "application/x-www-form-urlencoded; charset=utf-8", }, }) .then((response) => { return response.json(); }) .then((responseJson) => { if (responseJson.status === "finished") { finished = true; } let x = responseJson.x; let y = responseJson.y; data[0].x = x; data[0].y = y; Plotly.update("canvas", data, layout); }) .catch((error) => { alert("Error while measurement"); finished = true; }); await _sleep(500); } }
JS側では、フォームがsubmitされるとまず、画面遷移が生じるないようにします(e.preventDefault()
)。Django側へは2回APIアクセスします。それぞれの役割は
- 測定を開始する
- 測定データを得る
となっていますが、なぜこのように2つに分けたかというと、1つにまとめてしまうと測定が終わるまでデータ点が得られないためです。Python側でエンドポイントを分けてあげることで測定シーケンスを進めるスレッドと測定データをフロント側に渡すスレッドができることになります。
測定を開始するリクエストを送ったあとは、測定が終了したという通知をPythonから受け取るまでは500msごとに測定データを要求します。
バックエンド側 (Python)
エンドポイントの設定
from django.urls import path from . import views app_name = "core" urlpatterns = [ path("tds/", views.TDS.as_view(), name="tds"), path("tds-data/", views.tds_data, name="tds_data"), path("tds-boot/", views.tds_boot, name="tds_boot"), ]
1つめは最初に書いたHTMLをテンプレートとするビュー、あとの2つはjsonを返すビューです。
ビュー
from django.views import View from django.http import JsonResponse wave_tds = WaveForm() tds_running = False class TDS(View): def get(self, request): return render(request, "core/tds.html") def tds_boot(request): global tds_running tds_running = True wave_tds.clear() start = int(request.POST.get("start")) end = int(request.POST.get("end")) step = int(request.POST.get("step")) lockin = float(request.POST.get("lockin")) api_ops.tds_scan(start, end, step, lockin, wave_tds) # 測定シーケンスの中身 tds_running = False return JsonResponse({}) def tds_data(request): x = wave_tds.x y = wave_tds.y status = "running" if tds_running else "finished" return JsonResponse({"x": x, "y": y, "status": status})
測定シーケンスが走っているかいないかはグローバル変数 tds_running
で判断します。
実際の動作 (動画)
実際に動作している様子を動画にしました。データは適当にsinカーブを生成するようにしています。マウスを近づけると値が読めたり、詳しく見たい部分をズームアップできるのは嬉しいですね。
streamable.com
Raspberry Pi 4 に Ubuntu Desktop を入れる
Raspberry Pi 4 に Ubuntu Desktop を入れるときに苦労したので備忘録として書いておきます。
最近、Raspberry Pi に OSを入れるためのアプリケーションとして Raspberry Pi Imager が出てきて簡単に SD カードをフォーマットしたり OS をSD カードに入れられるようになりました。しかし、そんな中何故か Ubuntu が正常にインストールできないという状況に遭遇しました。ここでは発生したエラーとその対処法について書いていきます。
環境
- Raspberry Pi 4 Model B, RAM = 4 GB
- microSD card (Samsung, EVO Plus 128 GB)
microSD は Raspberry Pi で高速に動作するとの評判が多かった Samsung の microSD を使いました。
インストール手順
www.raspberrypi.org
上のリンクから Imager をダウンロードしてきて、OSを選択、Write をするだけです。手順はここのサイトにもありますが非常にシンプルでわかりやすいです。今回は Ubuntu Desktop 21.04, 64bit を選択しました
OSの書き込みは正常に終了したという通知が出て、microSD を Raspberry Pi に入れてさあ起動!と意気込みましたがそうは行きません。画面の下の方に 「Ubuntu」という文字(ロゴ?)が数秒出たあと消えます。その後はスクリーンには No signal という表示。。。その後は何分待ってもシグナルは復活しません。
状況の確認
これだと何が起きたのか全くわからないので Ubuntu Server 20.04LTS を microSD に入れ直してメッセージを見てみます。さっきと同じ手順で「Ubuntu Server 20.04LTS, 64bit」を選択してインストールします。Raspberry Pi で起動してみると先ほどと同様についたと思ったらすぐに「No signal」になります。消える直前に一瞬エラーメッセージが表示されていたのでそれをスマホで録画して確認してみると、
[FAILED] Failed to start Command from Kernel Command Line. See 'systemctl status kernel-command-line.service' for details. [DEPEND] Dependency failed for Command from Kernel Command Line (中略) [OK] Reached target Power-Off.
systemctl status kernel-command-line.service
で状況を確認しろって言うなら電源落とすな!と言いたくなりますが、このエラーでぐぐってみると、次のようなページが引っかかります。
bugs.launchpad.net
このタイムラインで#3の人がこのエラーについてコメントしています。どうやら Raspberry Pi 4 のバグのようですね。
どうやって解決するか
解決策について探してみると、下の方に
dd
使ってイメージを焼く- Raspberry Pi Imager の Advanced Option をリセットしてためす
という解決法が上げられていました。Advanced Option というのは OS を入れたときに SSH を有効にしておいたり、WiFiの情報を設定しておいたりできるRaspberry Pi Imager のオプションです。私はこれを設定しておいたのでもしかしてと思い、Raspberry Pi Imager を入れ直して試してみたところ正常に起動しました!
データベースの学習環境をDockerで作った話
データベースの勉強をするときに書いたクエリを実際に試すことはとても重要ですよね。しかし、私はこれまで書いたクエリを試すための環境構築が面倒で下のサイトを利用して SQL を実行していました。
www.db-fiddle.com
このサイトはとても便利なのですが、毎回 CREATE TABLE
から始めなければならなくてこれはこれで面倒なのと、やはりローカルに入れて勉強した方が深いところまで学ぶことができるのではないかと感じたので試してみることにしました。
Docker image の取得
PostgreSQL は DockerHub に公式イメージが公開されているのでそれを使います。バージョンに特にこだわりはないのと、最近 PostgreSQL の大規模なバク修正があったと聞いたので latest を使います。
$ docker pull postgres:latest
docker-compose.yaml の作成
毎回コンテナを起動するときにいろいろ指定するのは面倒なので全部 docker-compose.yaml に書いておきます。
version: '3' services: db: image: postgres container_name: db restart: always environment: POSTGRES_PASSWORD: your-password volumes: - ./data:/var/lib/postgresql/data - ./sql:/home
コンテナデータを永続化するためにホストのディレクトリ(./data
)をコンテナ内でデータベース情報の入るディレクトリ(/var/lib/postgresql/data
)にマウントします。また、SQLファイルを入れるディレクトリ(./sql
)もマウントしています。
構築した環境を使う
準備はできたのでコンテナを起動します。
$ docker-compose up -d $ docker-compose ps # コンテナが無事起動しているか確認 Name Command State Ports ------------------------------------------------------- db docker-entrypoint.sh postgres Up 5432/tcp
コンテナが無事に起動出来たら学習用のデータベースを作りましょう。
$ docker exec -it db /bin/bash root@92913b69db39:/# createdb -U postgres testdb root@92913b69db39:/# psql -U postgres -d testdb psql (13.1 (Debian 13.1-1.pgdg100+1)) Type "help" for help. testdb=# \q root@92913b69db39:/# exit $
今回は単に学習用なのでユーザーは postgres を使ってしまいます。
次に、このデータベースにテーブルを作成する SQL を実行します。
create_table.sql
CREATE TABLE testtable ( id INTEGER PRIMARY KEY, name VARCHAR(100) NOT NULL );
テーブルを作成するには
$ docker exec -it db psql -U postgres -d testdb -f /home/create_table.sql
を実行します。CREATE TABLE
という出力が出れば成功です。
最後に、SQL を実行するコマンドの効率化をしましょう。毎回実行するたびに上のコマンドを実行するのは面倒なのでシェルスクリプトを作ります。
runner.sh
#!/bin/bash docker exec -it db psql -U postgres -d testdb -f /home/$1
chmod +x runnser.sh
で実行権限を付与しておけば、
$ ./runner.sh create_table.sql
のように実行することができます。これで勉強がはかどりますね!
Python + Docker でデータ解析環境の管理
今回は実験データ自体の解析の話ではなく、どのような環境で解析を行ったのかという「環境」の管理について書きたいと思います。大学などで研究をしていて論文としてその成果をまとめる場合、関連するデータや解析プログラムは所属する大学もしくは研究室に整理して保存しておくことが一般的です。これはもちろん、論文に示されているデータがどのように解析されているのかあとから確認することができるようにしておくためです。
実験データの解析を例えば Python で行っている場合、解析の再現性を担保するためにはデータファイルのみならず解析を行っている環境(ライブラリのバージョンなど)も重要になってきます。そこで、この記事では Docker を使ってこの環境の管理をしたという話をします。
動作環境
- Windows10 Home
- WSL2 (Ubuntu 18.04)
Docker image の作成
FROM jupyter/base-notebook:python-3.8.6 COPY ./requirements.txt . RUN pip install -r requirements.txt EXPOSE 8888
base image としては jupyter notebook のイメージが Docker Hub にあるのでこれを使います。タグは latest
のものを使うと Docker Hub が更新されたときに中身が変わってしまうので python-3.8.6
を指定します。
解析に使うライブラリは requirements.txt
にバージョンを明記して書いておきます。
docker-compose.yaml の作成
実際にこのコンテナを運用するときには、ホストから Jupyter Notebook を編集するためにポートフォワーディングをしたり、解析データをマウントしたりします。これらはコンテナを起動するときに指定することは出来ますが、いちいち書くのは面倒なので docker-compose
を使います。
version: "3" services: my-jupyter: build: . image: my-jupyter:1.0.0 ports: - 8888:8888 volumes: - .:/home/jovyan/ command: jupyter lab
コンテナの実行
コンテナを動かすためには、
docker-compose up
を実行します。もしも Docker image がビルドされていないときにはビルドから開始します。http://localhost:8888
にアクセスしてJupyter Lab が起動すれば成功です。