pyhaya’s diary

機械学習系の記事をメインで書きます

「ベイズ統計モデリングによるデータ分析入門」を読んだ

ベイズを勉強してみたいと思って本を探していたら、こちらの「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は残してあるし、必要ならばすぐにビルドしなおせばよさそうだったのでやってみた。

f:id:pyhaya:20220128112523p:plain
Docker Desktop for Windowsの起動画面
Docker Desktop for Windowsを起動して右上のTrouble Shootingを開く(虫のマークのやつ)
f:id:pyhaya:20220128112529p:plain
Trouble Shootingの画面
Clean/Purge Data(赤くなっているやつ)を押す。 f:id:pyhaya:20220128112545p:plain WSLにチェックを入れてDelete。いままでビルドしたイメージが全部消えるのでいくつかビルドしなおしてちゃんとビルドできることを確認してみた後、Dockerのデータの容量を確認してみた。 f:id:pyhaya:20220128112527p:plain わかりにくいが容量が5 GBほどになっており、消す前の1/10の容量になった。
いままでビルドしてきたイメージやボリュームが消えてもすぐ復旧できるという状況の人は試してみると幸せになれるかもしれない。

SRGANで画像の高解像度化

今回はGANを使った初期の画像の高解像度化モデルであるSRGANを実装してみたので紹介したいと思います。

実行環境

今回のモデルは以下のような環境で実装しています。

ソースコードは以下に公開しています。
github.com


検証に用いたデータセットはDIV2Kで以下からダウンロードしました。
data.vision.ee.ethz.ch

訓練に用いるために、各画像から20枚ずつ32x32のランダムクロップを行いました。

SRGANとは

SRGANとは、その名前にもあるようにGAN(敵対的生成ネットワーク)を使って、入力画像に対して解像度が上がった画像を生成しようという目的で作られたモデルです。

f:id:pyhaya:20210822222355p:plain
https://arxiv.org/pdf/1609.04802.pdf から引用

モデルの実装

モデルは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")

訓練結果

訓練したモデルを使ってテストデータの高解像度化を行ってみました。ところどころノイズは載っていますが概ねよく高解像度化できているように見えます。

f:id:pyhaya:20210905214807p:plain

Django + Plotly.js でグラフをリアルタイム更新

最近、測定器からデータを取得してリアルタイムにデータを表示して確認したいという願望があってそれを実現する方法を考えてました。その1つの解決策としてDjangoを使ってWebアプリケーションのような形で実現する方法を思いつき、試しに作ってみたのでそれを共有したいと思います。

この記事では、プロットするデータは測定器から発生していますがPythonで得られるデータであればなんでもこの方法は応用できます。

f:id:pyhaya:20210815224932j:plain
Photo by Isaac Smith on Unsplash

なぜ 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" /> &mu;m
    </div>
    <div id="end">
      <p>End:</p>
      <input type="number" id="end-position" /> &mu;m
    </div>
    <div id="step">
      <p>Step:</p>
      <input type="number" id="moving-step" /> &mu;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のアドレスを入れます。

JavaScript

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 が正常にインストールできないという状況に遭遇しました。ここでは発生したエラーとその対処法について書いていきます。

インストール手順

www.raspberrypi.org
上のリンクから Imager をダウンロードしてきて、OSを選択、Write をするだけです。手順はここのサイトにもありますが非常にシンプルでわかりやすいです。今回は Ubuntu Desktop 21.04, 64bit を選択しました

OSの書き込みは正常に終了したという通知が出て、microSDRaspberry 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 から始めなければならなくてこれはこれで面倒なのと、やはりローカルに入れて勉強した方が深いところまで学ぶことができるのではないかと感じたので試してみることにしました。

動作環境

動作確認は WSL 内で行っています。

  • Windows 10 Home
  • WSL2 (Ubuntu 18.04)
  • Docker 19.03.13
  • docker-compose 1.27.4

Docker image の取得

PostgreSQL は DockerHub に公式イメージが公開されているのでそれを使います。バージョンに特にこだわりはないのと、最近 PostgreSQL の大規模なバク修正があったと聞いたので latest を使います。

news.mynavi.jp

$ 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

のように実行することができます。これで勉強がはかどりますね!


Docker/Kubernetes 実践コンテナ開発入門

Docker/Kubernetes 実践コンテナ開発入門

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 が起動すれば成功です。