grbl1.1+Arduino CNCシールドV3.5+bCNCを使用中。
BluetoothモジュールおよびbCNCのPendant機能でスマホからもワイヤレス操作可能。
その他、電子工作・プログラミング、機械学習などもやっています。
MacとUbuntuを使用。

CNCマシン全般について:
国内レーザー加工機と中国製レーザー加工機の比較
中国製レーザーダイオードについて
CNCミリングマシンとCNCルーターマシンいろいろ
その他:
利用例や付加機能など:
CNCルーター関係:



*CNCマシンの制作記録は2016/04/10〜の投稿に書いてあります。


2018年8月5日日曜日

tf.kerasでDCGAN(Deep Convolutional Generative Adversarial Networks)

前回VAE(Variational Autoencoder)を試して見たので、今回はDCGAN(Deep Convolutional Generative Adversarial Networks)をKerasで実装しつつ理解を深めたいと思います。使用データはMNISTです。 元々GANによる画像生成に興味があったのですが、約10ヶ月前にサンプルを試したときには、二つの敵対するネットワークによって画像生成するという大まかな流れしか理解できませんでした。
チュートリアルなどでは、
・Autoencoder(AE)
・Variational Autoencoder(VAE)
・Generative Adversarial Networks(GAN)
という順番で説明されていることが多く、VAE(潜在空間、ベイズ推定、KLダイバージェンスなど)を理解しないことにはGANを理解することも難しいかなと勝手に思っていましたが、そもそもAEとVAEも大きく異なるしGANもまた別のアルゴリズムという感じで、基本のAEが分かればGANを理解することはできそうです。
GANの派生型はいろいろありますが、とりあえず今回はDCGANを理解しようと思います。


上の画像はDCGANの構造で、左半分がGeneratorで右半分がDiscriminatorです。最終的にはGenerator層の右端(上画像中央:64x64x3)に画像が生成されます。
まずGeneratorで画像生成する前に、Discriminatorの左端から訓練画像を入力してDiscriminatorだけを教師あり学習します。その後、GeneratorとDiscriminatorを連結させたネットワークで教師あり学習させます。このときDiscriminatorの学習を停止させておいてGeneratorだけが学習するようにします。そうすると既に学習されているDiscriminatorを利用しながらGeneratorだけが学習し、その結果として画像が生成されます。この交互に学習させる手順がわかりにくいので難しく見えるのかもしれません。
GeneratorはAEやVAEのdecoder層だけで構成されている感じで、最初のノイズ画像はVAEで言う潜在空間と呼びますが、途中でReparameterization TrickやKLダイバージェンスなどの複雑な計算を使うこともないので、潜在空間というよりは単なるノイズ画像(np.random.normalで生成)と捉えたほうがよさそうです。


GANの訓練の特長:
先ほどの訓練手順についてですが、GANの訓練では、以下のようにそれぞれ別々に訓練させるようです。
・Discriminatorの本物画像識別の訓練(訓練画像を利用)
・Discriminatorの偽物画像識別の訓練(Generator生成画像を利用)
・Generatorの本物画像生成の訓練(Discriminator層も利用するが訓練を一時停止)

GANの説明では、Discriminatorは本物か偽物を見分けると書いてあり、Discriminatorに入力した画像が最終的に1か0に判定されるような層になっています。訓練用画像(本物)を入力した際にはラベルを1とし、ノイズ画像(偽物)を入力した際にはラベルを0として固定して(教師データとして)、それぞれを分けて学習させていきます。そうすることで、Discriminator層には本物/偽物を見分ける重み付けが徐々に形成されていきます。

一方Generatorでは、ノイズ画像を本物画像に近づくように訓練しなければいけないのですが、AEやVAEのように具体的な訓練画像を目指してdecodeしていくわけではないので、一体どうやって本物に近づけていくのだろうと疑問に思っていました。
最終的にはGeneratorのノイズ画像が、Discriminator層の最後の1次元の出力層で1(本物)になるようにGenerator層が学習していけばいいということになります。そのためには、Generator単独で訓練するのではなく、Discriminator層も連結してラベル(教師データ)を1に固定して訓練させます。画像を教師データにして訓練するのではなく、本物かどうかというラベルを教師データにして訓練する点がGANの特長だと思います(それでも画像生成は可能)。ただし、二つを連結させると両方とも訓練してしまうので、二つのうちGenarator層だけを訓練させるために、

discriminator.trainable=False

を挿入してDiscriminatorの訓練を一時停止しておく必要があります。
この部分に注意すれば、あとはそれほど難しいアルゴリズムが登場してくることはないかと。解説を読むと数式や抽象的な概念が出てきますが、アルゴリズム的に訓練の手順を理解すればそれほど難しいものではないような気がします。AEではモデル全体は真ん中がくびれていますが、GANの場合は始まりと終わりが細くて真ん中が太くなっているので(decoderとencoderを逆につなげたように)一見わかりにくいという印象です。しかしよくみれば、100次元のノイズを入力元として、decoder(Genarator)で28*28次元のMNIST画像に拡大し(生成画像)、またそれをencoder(Discriminator)で1次元まで落として、最後はsigmoidで0/1判定するという流れになっています。


DCGAN実装:

環境:
Ubuntu 18.04.1
GTX 1060
CUDA 9.0
Python 3.6
Tensorflow 1.9 (Keras 2.1.6)
Jupyter Notebook 5.6


まずはモジュールのインポート。今回もJupyter Notebookで。
import tensorflow as tf
from tensorflow.keras.models import Sequential, Model
from tensorflow.keras.layers import Input, Dense, Reshape, Flatten, BatchNormalization
from tensorflow.keras.layers import Conv2D, Conv2DTranspose, Activation, LeakyReLU
from tensorflow.keras.optimizers import Adam
from tensorflow.keras.datasets import mnist
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline

tf.logging.set_verbosity(tf.logging.ERROR)
警告がでるので、tf.logging.set_verbosity()で非表示にしています。
次に、各種変数とGenerator層。
img_rows = 28
img_cols = 28
channels = 1
img_shape = (img_rows, img_cols, channels)
latent_dim = 100

def generator_model():
    model = Sequential()
    model.add(Dense(1024, input_shape=(latent_dim,)))
    model.add(BatchNormalization())
    model.add(LeakyReLU(alpha=0.01))
    model.add(Dense(7*7*128))
    model.add(BatchNormalization())
    model.add(LeakyReLU(alpha=0.01))
    model.add(Reshape((7,7,128)))
    model.add(Conv2DTranspose(64, kernel_size=5, strides=2, padding='same'))
    model.add(BatchNormalization())
    model.add(LeakyReLU(alpha=0.01))
    model.add(Conv2DTranspose(1,kernel_size=5, strides=2, padding='same'))
    model.add(Activation('tanh'))
    
    return model
Generator層では、BatchNomarization、LeakyReLU、Conv2DTransposeを入れてみました。DCGANを安定させる方法としていろいろ工夫があるようですが、いくつか試したなかで今回はこの方法で。LeakyReLUのalpha値をデフォルトにするだけでも結果が変わってしまうので、このへんのパラメーターチューニングは難しそう。

次に、Discriminator層。
def discriminator_model():
    model = Sequential()
    model.add(Conv2D(32, kernel_size=5, strides=2,padding='same', input_shape=img_shape))
    model.add(LeakyReLU(alpha=0.01))
    model.add(Conv2D(16,kernel_size=5,strides=2, padding='same'))
    model.add(BatchNormalization())              
    model.add(LeakyReLU(alpha=0.01))
    model.add(Flatten())
    model.add(Dense(784))
    model.add(BatchNormalization())
    model.add(LeakyReLU(alpha=0.01))
    model.add(Dense(1))
    model.add(Activation('sigmoid'))
    
    return model
こちらもLeakyReLU、BatchNomalizationを入れています。
Dropoutを入れて試してみましたが逆効果となってしまったので、今回はなし。

次は、GenaratorとDiscriminatorの連結層。
def combined_model():
    discriminator.trainable = False
    model = Sequential([generator, discriminator])
    
    return model
Generatorを訓練する際にこの連結層を使用します。そのため事前に、discriminator.trainable=Falseにしておきます。こうすることでGeneratorだけの訓練になります。

まずは、MNISTデータの読み込みと正規化(-1〜1)。そして、Discriminator、Generator、Combined(G + D)モデルの定義。Adamで最適化。
(x_train, _), (_, _) = mnist.load_data()
x_train = (x_train.astype('float32') - 127.5) / 127.5
x_train = x_train.reshape(-1, 28, 28, 1)

# Discriminator Model
discriminator = discriminator_model()
discriminator.compile(loss='binary_crossentropy', optimizer=Adam(lr=0.0002, beta_1=0.5), metrics=['accuracy'])

# Generator Model
generator = generator_model()

# Combined(G + D) Model
combined = combined_model()
combined.compile(loss='binary_crossentropy', optimizer=Adam(lr=0.00015, beta_1=0.5))

そして訓練ループ。
まずDiscriminatorの訓練をリアル画像とフェイク画像に分けて行います。
訓練はfitではなくtrain_on_batchでバッチごとに行うといいようです。その際にDiscriminatorの場合は、フェイク:0とリアル:1の二つのラベルを教師データとして与えておき、それぞれを別々に訓練し、最後にそのロスを合算しておきます。
次のGeneratorの訓練では、教師データをリアル:1として与えておき、Discriminatorの訓練を一時停止した状態で連結したcombinedモデルを訓練させます。そうすると出力がリアル:1になるようにGeneratorの重み付けが形成されます。この部分がGAN特有の訓練のさせ方だと思います。
batch_size = 32
real = np.ones((batch_size, 1))
fake = np.zeros((batch_size, 1))

epochs = 10000
Loss_D = []
Loss_G = []

import time
start = time.time()

for epoch in range(epochs):
    
    # shuffle batch data
    idx = np.random.randint(0, x_train.shape[0], batch_size)
    imgs = x_train[idx]
    
    # Train Discriminator
    # sample noise images to generator
    noise = np.random.normal(0, 1, (batch_size, latent_dim))
    gen_imgs = generator.predict(noise)

    # train discriminator real and fake
    d_loss_real = discriminator.train_on_batch(imgs, real)
    d_loss_fake = discriminator.train_on_batch(gen_imgs, fake)
    d_loss = 0.5 * np.add(d_loss_real, d_loss_fake)

    # Train Generator
    g_loss = combined.train_on_batch(noise, real)
 
    Loss_D.append(d_loss[0])
    Loss_G.append(g_loss)

    if epoch % 100 == 0:
        print("%04d [D loss: %f, acc.: %.2f%%] [G loss: %f] %.2f sec" % (epoch, d_loss[0], 100*d_loss[1], g_loss, time.time()-start))

    if epoch == epochs - 1:
        r, c = 5, 5
        noise = np.random.normal(0, 1, (r * c, latent_dim))
        gen_imgs = generator.predict(noise)
        gen_imgs = 0.5 * gen_imgs + 0.5

        fig, axs = plt.subplots(r, c)
        cnt = 0
        for i in range(r):
            for j in range(c):
                axs[i,j].imshow(gen_imgs[cnt, :,:,0], cmap='gray')
                axs[i,j].axis('off')
                cnt += 1
        plt.show() 
Discriminatorの訓練(2種類別々で訓練し最後に合算):
d_loss_real=ノイズ-->Generator-->Discriminator-->realラベル(教師データ)
d_loss_fake=ノイズ-->Generator-->Discriminator-->fakeラベル(教師データ)
Discriminator_loss=0.5*(d_loss_real+d_loss_fake)

Generatorの訓練:
g_loss=ノイズ-->Generator-->Discriminator(訓練停止)-->realラベル(教師データ)
という手順でそれぞれを訓練しています。
教師データとなるreal/fakeラベルはbatch_size分用意しておき、train_on_batch()に代入します。

合計10000エポック回して、100エポックごとに各Lossを表示。最後に最終画像を表示。

生成画像結果:
生成画像(10000エポック)。

途中の画像も見てみましたが、5000エポックくらいでもそこそこ識別できるレベルにはなりましたが、10000エポックくらい回したほうがよさそうです(GTX1060で約6分、Macだと1時間はかかりそう)。モード崩壊(似たような画像ばかりになる現象)は発生していないようです。

生成画像(5000エポック)。やや不明瞭??

生成画像(2500エポック)。

生成画像(2000エポック)。このあたりだとやはり不鮮明。


以下のコードでLossを表示。
plt.plot(np.arange(epochs), Loss_D, 'r-')
plt.plot(np.arange(epochs), Loss_G, 'b-')

赤:Discriminator Loss、青:Generator Loss
これをみてもよくわからない。3000エポック以降はあまりかわっていないようなので5000エポックくらいの訓練でもいいのかもしれない。


まとめ:
DCGANは思っていたよりもシンプルな構造で、GeneratorとDiscriminatorをつくれば、あとはそれぞれの訓練の手順を間違わないようにコーディングしていけばいいという感じです。どちらかというとVAEのほうが難しかったという印象です。
ただしDCGANで難しいのは、GeneratorとDiscriminatorの中身の層をどうするか?ということかもしれません。ここを参考にすると、LeakyReLUやBatchNormを使った方がいいらしいのですが、層の順番やパラメータが少し違うだけでも生成画像がノイズのままで終わってしまうので、安定的に画像生成させるにはいろいろ試してみる必要がありそうです。GANの派生型はたくさんあるので、DCGAN以外のGANも試して比較してみたほうがよさそうです。


参考にしたサイト:
https://towardsdatascience.com/having-fun-with-deep-convolutional-gans-f4f8393686ed
https://elix-tech.github.io/ja/2017/02/06/gan.html
https://qiita.com/triwave33/items/1890ccc71fab6cbca87e
https://qiita.com/t-ae/items/236457c29ba85a7579d5


直感 Deep Learning ―Python×Kerasでアイデアを形にするレシピ
Posted with Amakuri at 2018.12.21
Antonio Gulli, Sujit Pal
オライリージャパン
販売価格 ¥3,672

2018年7月25日水曜日

tf.kerasでVAE(Variational Autoencoder)

Tensorflowもあっというまに1.9までバージョンアップしており、トップページが日本語表示になっていました。Get started with Tensorflowという最初のチュートリアルも変わったようで、Keras、Eager、EstimatorがHigh Level APIとして前面にでてきています。Pytorchも試していましたが、Tensorflowがますます便利になっていくのでTensorflowに戻りつつあります。書きやすくなったEagerやtf.layersも試してみましたが、結局Kerasがシンプルでわかりやすいという結論に達し、Keras自体もバージョンアップしたようなのでTensorflowというよりもKerasでVAEを試してみようかと。

VAEは中間層で突然正規分布が登場して、ベイズ的な手法で画像生成していくアルゴリズムが興味深く、固定値を確率に変換して表現するという部分がずっと気になっていました(最初に試したのは約10ヶ月前)。
潜在空間、ベイズ推定、Reparameterization trick、KL-divergenceなど、画像生成に通じるテクニックを勉強するにはちょうどいいサンプルだと思います(かなり難しいですが)。

TensorflowでKerasをインポートする際に、以前はtensorflow.python.kerasだったけど、Tensorflow 1.9からは、tensorflow.kerasで使えるようになったようです。

Kerasの書き方:
Kerasの場合いくつか書き方があり、
Sequential()の中に各層をそのまま並べて行く方法。
model = Sequential([
    Dense(32, input_shape=(784,)),
    Activation('relu'),
    Dense(10),
    Activation('softmax'),
])
Sequential()でモデルを定義してから各層をaddで追加していく方法。
model = Sequential()
model.add(Dense(32, input_shape=(784,))
model.add(Activation('relu'))
model.add(Dense(10))
model.add(Activation('softmax'))
これらの方法はSequentialモデルと呼ばれ、各層をそのまま重ねていけばいいのでわかりやすい。

このほか、functional APIというモデルがあり、各層に変数をつけて行末の()に前の層を代入し、最後にモデルを定義する方法。
inputs = Inputs(shape=(784,))
layer1 = Dense(32, activation='relu')(inputs)
outputs = Dense(10, activation='softmax')(layer1)

model = Model(inputs,outputs)
行末の()なしで各層を連結させないで書くには以下。あとから連結式(代入式)を書いて、先ほどの結果(Model)と同じになります。
inputs = Inputs(shape=(784,))
l1 = Dense(32, activation='relu')
l2 = Dense(10, activation='softmax')

layer1 = l1(inputs)
outputs = l2(layer1)

model = Model(inputs,outputs)
Sequentialモデルのほうがすっきりしてわかりやすいけれども、VAEの場合だと少し複雑になるので、今回はfunctional APIで各層を別々に書いていくタイプを使います。
そのほか、これらModelクラスをサブクラス化する書き方もあるけれど、一行ずつ順を追ってベタに書いていったほうが理解しやすいので、今回はサブクラス化せずにJupyter Notebookに書いていこうと思います。

VAEの実装:

環境:
Ubuntu 18.04
GTX 1060
CUDA 9.0
python 3.6
tensorflow 1.9
Jupyter Notebook 5.6


まずは各モジュールのインポート。
import tensorflow as tf
from tensorflow.keras.models import Model
from tensorflow.keras import losses, backend as K
from tensorflow.keras.layers import Dense, Input, Lambda
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline

そして、今回使用するmnistデータセットの読み込みと正規化、28x28の画像を784の1次元へ平坦化。
mnist = tf.keras.datasets.mnist
(x_train, y_train),(x_test, y_test) = mnist.load_data()
x_train, x_test = x_train / 255.0, x_test / 255.0
x_train = x_train.reshape(60000, 784)
x_test = x_test.reshape(10000, 784)
このへんはサンプルなどでもお馴染みの方法。

Encoderと潜在変数z:
そして、encoder層。
# encoder
inputs = Input(shape=(784,))
encoder_h = Dense(256, activation='relu')(inputs)
z_mu = Dense(2, activation='linear')(encoder_h)
z_log_sigma = Dense(2, activation='linear')(encoder_h)
encoderは、784次元に平坦化された画像を入力とし、reluを通して256次元に変換、その後さらに2次元へ変換し正規分布のパラメータとなる平均muと分散logΣに分けておきます。分散をΣではなくlogΣにしているのは、encoderからの出力が負の場合もあるため、Σ=σ2が常に正であるのに対し、logをつけることで負の値であっても成立するようにしているらしい(論文p11でもlogσ2と書いてある)。
要は、計算から求められる固定値を正規分布という確率分布に置換してから演算することで画像生成を可能にしているようです。

平均と分散をもとに正規分布から値を取り出すには通常サンプリング(ある確率に従ってランダムに値を取り出す)が必要となり、数式では以下のようにあらわします。

z~N(μ,Σ)

このサンプリング式をnumpyであらわすと、

z=np.random.normal(loc=μ, scale=Σ, size=1)

になりzを求めることは可能ですが、サンプリングすると後々バックプロパゲーションが不可能(微分不可能)となるため、Reparameterization trickという代替演算法をつかうようで、

z=μ+Σ0.5

に置き換えて(平均値μに誤差εを掛け合わせた分散Σを足し合わせるという感じ)、
上の式中のΣをlogΣに変換するには、

Σ=exp(logΣ)

であるから、最終的には、

z=μ+exp(0.5*logΣ)*ε

という式になるようです。この部分が以下のdef sampling()の内容です。

def sampling(args):
    z_mu, z_log_sigma = args
    epsilon = K.random_normal(shape=(K.shape(z_mu)[0], K.int_shape(z_mu)[1]))
    return z_mu + K.exp(0.5 * z_log_sigma) * epsilon

z = Lambda(sampling)([z_mu, z_log_sigma])

# encoder model
encoder = Model(inputs, [z_mu, z_log_sigma, z])
encoder.summary()
潜在変数zを求めるLambda(keras.layers.Lambdaクラス)の部分はKerasのモデルの一部に組み込むために必要で、そのままsampling()関数からの戻り値を受け取るだけだと、モデルの一部としてバックプロパゲーションなどしてくれなくなるようです。
最後にsummary()でこのモデルの各層を確認できます(以下)。
_______________________________________________________________________________________________
Layer (type)                    Output Shape         Param #     Connected to                     
==================================================================================================
input_1 (InputLayer)            (None, 784)          0                                            
__________________________________________________________________________________________________
dense (Dense)                   (None, 256)          200960      input_1[0][0]                    
__________________________________________________________________________________________________
dense_1 (Dense)                 (None, 2)            514         dense[0][0]                      
__________________________________________________________________________________________________
dense_2 (Dense)                 (None, 2)            514         dense[0][0]                      
__________________________________________________________________________________________________
lambda (Lambda)                 (None, 2)            0           dense_1[0][0]                    
                                                                 dense_2[0][0]                    
==================================================================================================
Total params: 201,988
Trainable params: 201,988
Non-trainable params: 
先程のLambdaの計算部分もモデルに組み込まれているのがわかります。最初pythonのlambda式と勘違いしており意味がわかりませんでしたが、これはKerasレイヤーのLambdaということです。

Decoder:
そして残りのdecoder層。decoder層は訓練用と画像生成用の2種類のモデルをつくっておきます。これは訓練用の方です。
# decoder
d_h = Dense(256, activation='relu')
d_out = Dense(784, activation='sigmoid')

decoder_h = d_h(z)
outputs = d_out(decoder_h)

# vae: encoder + decoder
vae = Model(inputs, outputs)
vae.summary()
画像生成時にもこのレイヤーを使い回すので、それぞれのレイヤーごとに分けて書いておき、次の行でzと隠れ層を代入します。そして、encoderの入力からdecoderの出力までを足し合わせてvaeモデル(訓練用)をつくります。vae.summary()で先程と同様にモデルの各層を確認します(以下)。

Layer (type)                    Output Shape         Param #     Connected to                     
==================================================================================================
input_1 (InputLayer)            (None, 784)          0                                            
__________________________________________________________________________________________________
dense (Dense)                   (None, 256)          200960      input_1[0][0]                    
__________________________________________________________________________________________________
dense_1 (Dense)                 (None, 2)            514         dense[0][0]                      
__________________________________________________________________________________________________
dense_2 (Dense)                 (None, 2)            514         dense[0][0]                      
__________________________________________________________________________________________________
lambda (Lambda)                 (None, 2)            0           dense_1[0][0]                    
                                                                 dense_2[0][0]                    
__________________________________________________________________________________________________
dense_3 (Dense)                 (None, 256)          768         lambda[0][0]                     
__________________________________________________________________________________________________
dense_4 (Dense)                 (None, 784)          201488      dense_3[0][0]                    
==================================================================================================
Total params: 404,244
Trainable params: 404,244
Non-trainable params: 0
入力784次元、256次元、2次元(z_mu, z_log_sigma, z)、256次元、784次元という各層があることがわかります。基本的にはz_muとz_log_sigmaの二つだけでいいのですが、比較もしたいためにzも組み込んでおきました。

Generator:
上記のvaeモデル(encoder+decoder)でz値を通して訓練用画像(x_train)で学習しますが、訓練後はz_muとテスト画像(x_test)を用いてpredict(予測/画像生成)します。
その画像生成する際のgeneratorのコードが以下。
# generator
generator_in = Input(shape=(2,))
generator_h = d_h(generator_in)
generator_out = d_out(generator_h)

generator = Model(generator_in, generator_out)
generator.summary()
後々使うのですが、とりあえす先につくっておきます。

Loss function:
つぎは、ロスの計算です。この部分はVAE特有の難しいアルゴリズムで、Reconstruction lossの最大化とKL-divergence loss最小化を組み合わせることになりますが論文や解説などを参考にするしかないと言う感じ。KL-divergenceは二つの分布の比較の値を計算してくれるようです。差が少ないほど0に近づくので最小化していくには便利。

ネットで探してみるとKerasのバージョンによっても違いがあるのか計算方法や関数が微妙に異なっており、いろいろ試した結果この方法に(参考はここ)。recon内で784を掛けていますがK.sum()でもいいのかもしれません。

def vae_loss(inputs, outputs):
    recon = 784 * losses.binary_crossentropy(inputs, outputs)
    kl = - 0.5 * K.sum(1 + z_log_sigma - K.square(z_mu) - K.exp(z_log_sigma), axis=-1)
    return K.mean(recon + kl)

vae.compile(optimizer='adam', loss=vae_loss)

epochs = 10
vaefit = vae.fit(x_train, x_train, 
                 shuffle=True,
                 epochs=epochs,
                 batch_size=64,
                 validation_data=(x_test, x_test),
                 callbacks=[])
今回はadamで最適化してみました。vae.fit()内のcallbacks=[]を加えることで訓練中のロス値を呼び出すことができるようで、それを利用してグラフを描くことができるようです。Tensorboardも利用できるようですが、今回はmatplotlibで。
# plot loss
loss = vaefit.history['loss']
val_loss = vaefit.history['val_loss']

plt.plot(range(1,epochs), loss[1:], marker='.', label='loss')
plt.plot(range(1,epochs), val_loss[1:], marker='.', label='val_loss')
plt.legend(loc='best', fontsize=10)
plt.grid()
plt.xlabel('epoch')
plt.ylabel('loss')
plt.show()
このコードを書き加えると以下のグラフが描けます。
100エポック回したときのロスの変化です。まだ下がりそうですが、100エポックでやめてしまいました。GTX1060で1エポック3秒前後(batch_size=64)。
隠れ層のユニット数やbatch_sizeを調整したほうがいいのかもしれませんが続行。

視覚化:
つぎは、結果の出力。
hidden_imgs = encoder.predict(x_test)
model_imgs = generator.predict(hidden_imgs[0])
vae_imgs = vae.predict(x_test)

s = 0
n = 10
plt.figure(figsize=(10, 3.1))
plt.subplots_adjust(wspace=0, hspace=0)

for i in range(n):
    #original
    ax = plt.subplot(3, n, i + 1)
    plt.imshow(x_test[i+s].reshape(28, 28))
    plt.axis('off')
    plt.gray()

    #reconstruction
    ax = plt.subplot(3, n, i + 1 + n)
    plt.imshow(model_imgs[i+s].reshape(28, 28))
    plt.axis('off')
    
    #vae model
    ax = plt.subplot(3, n, i + 1 + n + n)
    plt.imshow(vae_imgs[i+s].reshape(28, 28))
    plt.axis('off')
    
plt.show()
最初にencoder層をpredictし、その結果(hidden_imgs[0]はz_muによる出力)をgenarator層(生成用モデル)に渡して画像を得ています。同様にvae(訓練用モデル)も使って画像生成してみました(こちらはz経由での出力)。
結果の画像。オリジナル、encoder/z_mu/generator生成画像、vaeモデル:encoder/z/decoder生成画像。
4と9のような画像が多いので、まだ改良の余地がありそうです。

そして、2次元の潜在空間(z_mu)での各数字の分布。二つの値がそれぞれ横軸と縦軸に割り当てられそれを座標上に表したものです。
plt.figure(figsize=(10,10))
plt.scatter(hidden_imgs[0][:,0] ,hidden_imgs[0][:,1], marker='.', c=y_test, cmap=plt.get_cmap('tab10'))
plt.colorbar()
plt.grid()
cmapでtab10を用いることで10段階で色分けしています。結果の画像は以下。
これを見ると数字の5(茶色)が、かろうじてy=0より少し上に横に細長く並んでいるのがわかります。0、1、3、7は、領域がはっきり分かれているため認識しやすそうですが、それ以外は中央に重なるように集中しているので識別しにくそうです。

さらに、この分布をグリッド状の画像に置き換えるコード。
n = 20
digit_size = 28
figure = np.zeros((digit_size * n, digit_size * n))
grid_x = np.linspace(-2, 2, n)
grid_y = np.linspace(-2, 2, n)[::-1]

for i, yi in enumerate(grid_y):
    for j, xi in enumerate(grid_x):
        z_sample = np.array([[xi, yi]])
        x_decoded = generator.predict(z_sample)
        digit = x_decoded[0].reshape(digit_size, digit_size)
        figure[i * digit_size: (i + 1) * digit_size, j * digit_size: (j + 1) * digit_size] = digit

plt.figure(figsize=(10, 10))
start_range = digit_size // 2
end_range = n * digit_size + start_range + 1
pixel_range = np.arange(start_range, end_range, digit_size)
sample_range_x = np.round(grid_x, 1)
sample_range_y = np.round(grid_y, 1)
plt.xticks(pixel_range, sample_range_x)
plt.yticks(pixel_range, sample_range_y)
plt.xlabel("z [0]")
plt.ylabel("z [1]")
#plt.imshow(figure, cmap='gnuplot')
plt.imshow(figure, cmap='Greys_r')
plt.show()
先程の分布のグラフはx:-4〜4、y:-4〜4の範囲ですが、このコード内の4、5行目のgrid_xとgrid_yのnp.linspaceの範囲を-2〜2に変えることで、その範囲での数の分布を見ることができます。以下がその結果。
これは分布グラフの範囲をx:-2,2、y:-2,2に限定して出力したものです。先程のドットの分布で5が水平に細長く分布していたように、この画像においても中央右寄りに細長く水平に分布しています。一応一通り0〜9が存在していますが、分布領域が広範囲な数と狭い範囲にしかない数があるのがわかります。
ただ、このような結果から1と3と5の中間に8が位置していたりと、その特性を利用して面白い画像生成ができそうです。

まとめ:
VAEは以前Tensorflowのサンプルを試しましたが、単なるAutoencoderに比べると潜在変数やReparameterization trick、さらにはロス関数の部分の理解が難しいという印象でした。今回あらためてKerasで書いてみると、Kerasのシンプルな構造のおかげか、かなり理解が深まりました。特に最後の2つの分布的なグラフについてはどう表示するのかと思っていましたが、どこをいじればどうなるかが分かりました。
通常のAutoencoderの場合なら入力から出力までそのまま層を重ねて行けばいいのですが、VAEの場合だと中間層で正規分布からサンプリングするため、そのままだと訓練時にバックプロパゲーションができなくなってしまうことからReparameterization trickで微分計算可能な経路につくりかえます。訓練後はReparameterization trickは必要ないので、encoderからそのまま分布の中心位置となるz_mu経由でgeneratorを通り出力するということになっています。

訓練時(x_train):
encoder
z_mu, z_log_sigma
z(Reparameterization trick)
decoder

訓練後(x_test):
encoder
z_mu
generator


参考にしたサイト:
https://qiita.com/kenchin110100/items/7ceb5b8e8b21c551d69a
https://wiseodd.github.io/techblog/2016/12/10/variational-autoencoder/
https://www.kaggle.com/rvislaywade/visualizing-mnist-using-a-variational-autoencoder
https://blog.csdn.net/A_a_ron/article/details/79004163

関連:
tf.kerasでDCGAN(Deep Convolutional Generative Adversarial Networks)



直感 Deep Learning ―Python×Kerasでアイデアを形にするレシピ
Posted with Amakuri at 2018.12.21
Antonio Gulli, Sujit Pal
オライリージャパン
販売価格 ¥3,672

2018年6月11日月曜日

Ubuntu 18.04:Gnome Shell Extensions自作:クリックでnvidia-smiの結果を表示させる

前回サンプルを改造して簡単なGnome Shell Extensionを自作しましたが、今回はトップバーからのドロップダウンメニュー表示にチャレンジしてみました。

今回つくったもの:
GPUの状態をチェックするための「nvidia-smi」というターミナルコマンドの結果をドロップダウンメニュー上に表示させる(結果は以下)


これまでは、わざわざターミナルを起動して「nvidia-smi」コマンドを入力して表示させていましたが、トップバー上のアイコンをワンクリックすればこのように表示されます。さらに表示中は1秒ごとに内容を更新するようにします。

今回ドロップダウンメニューやポップアップメニューのプログラムについては、このチュートリアルを参考にしました。どうやらトップバーからのドロップダウン形式のメニューをつくるには、もう部品化されたクラスを使うといいようです。
追記:
クラスを使わないコードを新たに追加しました。
追々記:
さらなる改良版を追加しました(最後の方)。


まずは前回同様ターミナルで、

gnome-shell-extension-tool --create-extension

を入力して対話形式でタイトルなど決めながら雛形を用意します。
そして先程のポップアップメニューのサンプルをコピペすると、


こんな感じの4種類のメニューを含んだドロップダウンメニューができあがります。
このサンプルは一通りのメニューパターンを網羅しているので今後も使えそうです。

しかしながら今回は、これらのメニューは必要なくて単なるドロップダウン形式で「nvidia-smi」の結果をテキスト表示させたいだけなので、この表示ウィンドウだけ利用させてもらいます。主には、下から3番目の文字表示してある部分「PopupMenuItem」以外消してしまいます。
それからトップバーにも虫眼鏡アイコン、文字、▼の3種類表示していますが、できれば自作アイコンだけを表示させたいと思います。


自作アイコンの表示:
そのままアイコンのパスを書けばいいのかと思いましたが、ここにやり方が書いてありました。チュートリアルやサンプルが少ないためにあいかわらず探すのが大変です。
自作アイコンはメインスクリプトと同じディレクトリに入れておいた場合、

const St = imports.gi.St;
const Me = imports.misc.extensionUtils.getCurrentExtension();
const Gio = imports.gi.Gio;

let gicon=Gio.icon_new_for_string(Me.path + "/my_icon.png");

const icon = new St.Icon({ gicon: gicon, style_class: 'system-status-icon'})

となるようです(結構面倒)。そのままだと歪んでしまうので、最後に「style_class: 'system-status-icon'」を付け加えておくとトップパーにフィットしてくれます。


「nvidia-smi」のコマンドを送るコード:
コマンドを送るコードについては前回も使った、

let smi = GLib.spawn_command_line_sync("nvidia-smi").toString();

になります。そのまま「nvidia-smi」というコマンドを()内に書けばいいだけです。このコマンドを送れば、その結果として変数smiに文字列が代入されます。得られた文字列から必要な値だけを正規表現で抜き出して新たに書き直してもいいのですが面倒なので、そのまま結果を表示させることにしました。
追記:
そのまま「nvidia-smi」の返り値を「smi」に代入すると、先頭に「true,」と最後に「,,0」という文字が挿入されてしまうので、以下のように変更しました。
let smi = GLib.spawn_command_line_sync("nvidia-smi")[1].toString();

返り値は配列になっており、配列[0]には応答が成功した場合の「true」が含まれるようです(ダメなら「false」)。同様に[2]以降の「,,0」も不要なことから、[1]が実際に必要な文字列となるため上記のように変更しました(以下のコードも変更しておきました)。


このように余計な返り値を取り除き、必要な部分だけ表示されるようになりました。細かい部分ですが、少しずつわかってきたという感じです。


文字列を表示させるコード:
「nvidia-smi」で得た文字列を表示するには、

this.menuitem = new PopupMenu.PopupMenuItem(smi, {style_class:'smi-label'});

今回はこの部分に代入してみました。()内の「smi」が文字列になります。同時にCSSのスタイルを反映させるために「style_class」も記入しています。「smi-label」というのは、外部ファイル「stylesheet.css」内に記入してあるクラスです。

この「stylesheet.css」には、フォントの指定とフォントサイズの指定が書いてあります(以下)。

.smi-label {
  font-family: monospace;
  font-size: 15px;
}

フォントを等幅フォントのmonospaceなどに指定しないと、「nvidia-smi」から得た文字列が崩れてしまうので要注意(以下、崩れてしまった場合)。


表形式で文字列が出力されるために等幅フォントを使用しなければいけないということです。
背景色などはシステムに使用されているものと同じものが反映されるので、あとはフォントサイズを調整するくらいです。


1秒ごとに表示内容を更新させる:
このままだとクリックした瞬間の結果表示となるので待っていても内容は更新されません。そのため内容を更新させるプログラムを追加します。
先程、以下のように、

this.menuitem = new PopupMenu.PopupMenuItem(smi, {style_class:'smi-label'});

「nvidia-smi」から得た文字列をPopupMenuのオブジェクト生成と同時に渡しましたが、このオブジェクトへ後からテキストの値だけを渡すにはどうすればいいのか?いろいろ探しましたが、なかなか見当たりません。
ようやくGnome Shellのソースが見つかったので、なんとか解決しました。PopupMenuItem.jsに関してはこのページの239行目(以下)。

var PopupMenuItem = new Lang.Class({
    Name: 'PopupMenuItem',
    Extends: PopupBaseMenuItem,

    _init(text, params) {
        this.parent(params);
        this.label = new St.Label({ text: text });
        this.actor.add_child(this.label);
        this.actor.label_actor = this.label
    }
});

ここから察すると、どうやら「PopupMenuItem.label.text」で文字列を渡せそうです。メニュークラスなので文字列はラベル扱いで、そのラベルの下にテキストがあるようです。
ということから、アップデート関数をつくって、

update: function() {
    let timeout = GLib.timeout_add_seconds(0, 1, Lang.bind(this, function () { 
        this.menuitem.label.text = GLib.spawn_command_line_sync("nvidia-smi")[1].toString();
        return true;
    }));
},

このようにすれば、一秒ごとに内容更新されるはずです。これで主要なところはほぼ完成。
以下が、コード全体。

const Lang = imports.lang;
const St = imports.gi.St;
const GLib = imports.gi.GLib;

const Main = imports.ui.main;
const PanelMenu = imports.ui.panelMenu;
const PopupMenu = imports.ui.popupMenu;

const Me = imports.misc.extensionUtils.getCurrentExtension();
const Gio = imports.gi.Gio;
let gicon=Gio.icon_new_for_string(Me.path + "/gpu-icon.png");

let smiPanel;

const SmiPanel = new Lang.Class({
    Name: 'SmiPanel',
    Extends: PanelMenu.Button,

    _init: function() {
        this.parent(0.0, "smi panel", false);

        const icon = new St.Icon({ gicon: gicon,style_class: 'system-status-icon'})
        
        this.actor.add_actor(icon);       
        this._box = new St.BoxLayout();
        this.actor.add_actor(this._box);
  
        let smi = GLib.spawn_command_line_sync("nvidia-smi")[1].toString();
        this.menuitem = new PopupMenu.PopupMenuItem(smi,{style_class:'smi-label'});
        this.menu.addMenuItem(this.menuitem);
    },

    update: function() {
        let timeout = GLib.timeout_add_seconds(0, 1, Lang.bind(this, function () { 
            this.menuitem.label.text = GLib.spawn_command_line_sync("nvidia-smi")[1].toString();
            return true;
        }));
    },
 
    destroy: function() {
        this.parent();
    }
});

function init() {}

function enable() {
    smiPanel = new SmiPanel();
    smiPanel.update();
    Main.panel.addToStatusArea("nvidia_smi", smiPanel, 0, "right");
}

function disable() {
    if (smiPanel) {
        Main.panel._rightBox.remove_actor(smiPanel.container);
        Main.panel.menuManager.removeMenu(smiPanel.menu);
        smiPanel.destroy();
        smiPanel = null;
    }
}

BoxLayoutは使う必要があるかわかりませんが、一応入れておきました。
それにしても、相変わらず手がかりが少ないのでわかりづらい。それでも、なんとか目標にしていた表示方法に達成できました。毎回Alt+F2を押してr(リターン)でのExtension再起動もキー操作が面倒なのでスクリプト化してしまいました(xdotool使用)。
しばらくはGnome shellのソースと他のExtensionsを参考に調べていくしかないという感じです。
とりあえず、わからなくなったらこのReferenceを見るといいのかもしれません。
WindowやMenuの種類については、ここに画像とともに説明してあります。ここで見てみると、PopupMenuクラスではなく、PanelMenuクラスを使えばよかったのかもしれません。次回はこの辺の違いを検証していきたいと思います。

しかしながら、徐々にカスタマイズされてきたので使いやすくなってきました。


追記(シンプル版):
その後、もう少し単純なコードで書けないか試してみました。今回はサンプルの「Hello World」のようにクラスを使わずfunctionだけでできるかどうか?
サンプルにあるように、

init関数
enable関数
disable関数

それと、表示内容を毎秒更新するための、

update関数

これらの4つで構成します。
それから表示する文字列の体裁を整えるために「stylesheet.css」も利用します。
全体コード「extension.js」は以下。

const St = imports.gi.St;
const GLib = imports.gi.GLib;
const Main = imports.ui.main;
const Lang = imports.lang;
const Button = imports.ui.panelMenu.Button;

let button;

function update() {
    let timeout = GLib.timeout_add_seconds(0, 1, Lang.bind(this, function () {
        //メニューが開いているときだけコマンド送信ならびに文字列を更新する
        if(button.menu.isOpen){
            button.menu.box.label_actor.text = GLib.spawn_command_line_sync("nvidia-smi")[1].toString();
        }
        return true;
    }));
}

function init() {
    //ボタンオブジェクト生成
    button = new Button(0, 'button', false);
 
    //ラベルで文字列アイコンをつくり、ボタンへ追加
    let icon = new St.Label({style_class: 'icon-label', text:'GPU'});
    button.actor.add_child(icon);
 
    //「nvidia-smi」返り値の文字列用labelオブジェクト生成
    //外部スタイルシート「smi-label」を反映させておく
    let label = new St.Label({text:'', style_class:'smi-label'});

    //ボタンにlabelを追加
    button.menu.box.add_child(label);
    //labelをlabel_actorにしアップデートできるようにしておく
    button.menu.box.label_actor = label;
}

function enable() {
    update();
    Main.panel.addToStatusArea('Popup', button, 0, 'right');
}

function disable() {
    if(button){
        Main.panel._rightBox.remove_actor(button.container);
        Main.panel.menuManager.removeMenu(button.menu);
        button.destroy();
        button = null;
    } 
}

またスタイルシート「stylesheet.css」の内容は以下。

.icon-label {
    padding-top: 0.3em;
}

.smi-label {
    font-family: monospace;
    font-size: 15px;
    padding-left: 1em;
    padding-right: 1em;
}


今回は、PanelMenu.jsのButtonクラスを利用しました。「Menu」という文字列だけを表示させれば以下のようなシンプルなプルダウン形式のウィンドウです。


この「Menu」という文字列の部分に「nvidia-smi」の表示内容を入れるだけです。

ボタンオブジェクトを生成したあと、アイコンや表示する文字列をLabelオブジェクトとして追加していき、update関数で表示文字列だけを更新するというシンプルな内容ですが、またもや新たに「nvidia-smi」コマンドで受け取った文字列を渡す際に手間取りました。
特に、init関数の最後の部分です。前回PopupMenuItemの場合は、

PopupMenuItem.label.text = '表示させたい文字列';

で更新する文字列を渡すことができたのですが、今回の場合は同じようにやってもだめで、

button.menu.box.label_actor = label;

このようにlabel_actorを定義しておいてから、

button.menu.box.label_actor.text = '表示させたい文字列';

という感じで渡さないとだめでした。
こっちのほうがきちんとした書き方なのかもしれないけれども、Referenceやソースを見てもわかりにくい。手がかりとなったのは、前回も確認した(ソース)、

var PopupMenuItem = new Lang.Class({
    Name: 'PopupMenuItem',
    Extends: PopupBaseMenuItem,

    _init(text, params) {
        this.parent(params);
        this.label = new St.Label({ text: text });
        this.actor.add_child(this.label);
        this.actor.label_actor = this.label
    }
});

この最後に書いてある部分を参考に試してみたら文字列を渡すことができました。
まだまだ分からないことばかりで、以下気になった部分を列挙しておきます。


ボタンオブジェクトを生成の際のパラメータ:

button = new Button(0, 'button', false);

()内の最初の「0」はドロップダウンメニューの位置のようで、0:右寄り、1:左寄り、0.5:中央となるようです。ただし、画面からはみ出ないように上書き調整されるようです。

「1」にした場合は、クリックするトップバーアイコンを基準に左側に表示されます。

「0.5」にした場合は、トップバーアイコンを基準に中央に表示されます。

基本は「0」のまま(右寄り)でいいかと。

それから()内中央はおそらくオブジェクトの名称だと思うので適当な文字列を記入。()内右側の「false」は「dontCreateMenu」というパラメータであり、「True」にしてしまうとアイコンそのものも消えてしまうようで、通常は「false」のままでいいかと。


アップデート関数:
今回のアップデート関数は何を参考にしたか忘れてしまいましたが(ここを参照)、

function update() {
    let timeout = GLib.timeout_add_seconds(0, 1, Lang.bind(this, function () { 
        button.menu.box.label_actor.text = GLib.spawn_command_line_sync("nvidia-smi")[1].toString();
        return true;
    }));
}

この書き方の場合、最後に「return true」を入れないと更新されないようです。
また別の書き方だと、

const Mainloop = imports.mainloop;
let timeout;


function update() {
    button.menu.box.label_actor.text = GLib.spawn_command_line_sync("nvidia-smi")[1].toString();
    timeout = Mainloop.timeout_add_seconds(1, function(){
        update();
    });
}

mainloopライブラリをインポートしておき、ループ内でこのupdate()自体を再度呼び出して関数自体をぐるぐる回す感じ。しかし、disable()の中に以下を書いて破棄しなければだめなのかも。

Mainloop.source_remove(timeout);
timeout = null;

GLib.timeout_add_seconds()の場合も、ループ内に自身をループさせるようにすれば、「return true;」なしでも動きます。
このあたりの違いは、徐々に調べながらでしょうか。
後から気づきましたが、このままだとメニューを閉じた状態でも、常に毎秒「nvidia-smi」のコマンドを送り続けているので「button.menu.isOpen」でメニューが開いているときだけコマンドを送ったほうがよさそうです。ということから以下のように変更しました。

function update() {
    let timeout = GLib.timeout_add_seconds(0, 1, Lang.bind(this, function () {
        if(button.menu.isOpen){ 
            button.menu.box.label_actor.text = GLib.spawn_command_line_sync("nvidia-smi")[1].toString();
        }
        return true;
    }));
}



文字列をトップバーアイコンにする場合:
アイコンの代わりに文字列をトップバーに埋め込むには、Labelオブジェクトに文字列を渡してからボタンに追加していましたが、そのままだと、


このように上端寄りになってしまいます(「GPU」の部分)。
何かパネル用のスタイルがあるのかもしれませんが、わからないので外部CSSファイル(stylesheet.css)に、

.icon-label {
    padding-top: 0.3em;
}

を追加記入して反映させることにしました。
そうすると、


このように他と同じよう表示されました。

ということで、まだまだ未知の部分が多くちょっとしたことでも一苦労という感じです。



追々記(さらに改良):
とりあえず動くことを目標としていましたが、一旦動き出すと気になる部分がでてきます。前回、メニューが開いているときだけ「nvidia-smi」のコマンド送信(毎秒)をさせました。しかし、それでもenable()内に書いたupdate()は空回りしながらも動き続けているので、メニューが開いているときだけupdate()が動くように変更しました。
手順として:

・待機状態
・ボタンが押された
・メニューが開いた
・ループ開始
・メニューが閉じた
・ループ終了
・待機状態に戻る

という感じ。ボタンが押されるまで待機するには、

button.connect('button-press-event', update);

「Hello World」のサンプルにも使われているこれを利用。つまりボタンが押されたら、update()を起動させるというものです。これはinit()に書いておきます。
ただし、このconnect()もどこかでループ待機しているはずなので、その辺の仕組みまでは検証しないことに。
ここで、ボタンが押されたときのフラグを用意しようかと思いましたが、

button.menu.isOpen = true/false

があるので、これを利用します(この辺はソースを見ながら、何が使えるか判断しています)。
つまり「ボタンが押された=メニューが開いた」ということなので以下のようにupdate()を変更。

function update(){
    if(button.menu.isOpen){
        let timeout = GLib.timeout_add_seconds(0, 1, Lang.bind(this, function () {
            button.menu.box.label_actor.text = GLib.spawn_command_line_sync("nvidia-smi")[1].toString();
            update();
        }));
    }
}

update()内の無限ループの途中に、メニューが開いたらというフラグをつけておきます。
アイコンをクリックした後、別の場所(他のウィンドウなど)をクリックした際にメニューが閉じてしまうこともあるので、クリックしたかどうかで判定するよりも、メニューが開いているかどうかで判定させたほうがよさそうです。
よって、これまでenable()内にupdate()を配置していましたが、それを消しておきます。
全体コード:

const St = imports.gi.St;
const GLib = imports.gi.GLib;
const Main = imports.ui.main;
const Lang = imports.lang;
const Button = imports.ui.panelMenu.Button;

let button;

function update(){
    if(button.menu.isOpen){
        let timeout = GLib.timeout_add_seconds(0, 1, Lang.bind(this, function () {
            button.menu.box.label_actor.text = GLib.spawn_command_line_sync("nvidia-smi")[1].toString();
            update();
        }));
    }
}

function init() {
    button = new Button(0, 'button', false);

    //トップバーにアイコン画像を使用する場合は以下 
    //let icon =  new St.Icon({ icon_name: 'system-search-symbolic', style_class: 'system-status-icon'});
    //今回は文字列をアイコン代わりに使用(スタイルシート反映)
    let icon = new St.Label({style_class: 'icon-label', text:'GPU'});
    button.actor.add_child(icon);

    let smi = '';
    let label = new St.Label({text:smi, style_class:'smi-label'});
    button.menu.box.add_child(label);
    button.menu.box.label_actor = label;

    //ボタン待機:押されたらupdate()発動
    button.actor.connect('button-press-event', update);
}

function enable() {
    Main.panel.addToStatusArea('gpu', button, 0, 'right');
}

function disable() {
    if(button){
        Main.panel._rightBox.remove_actor(button.container);
        Main.panel.menuManager.removeMenu(button.menu);
        button.destroy();
        button = null;
    }
}

こんな感じでしょうか。
「nvidia-smi」コマンドの応答に対して例外処理するほうがいいと思いますが、今回は 省略。きりがないですが、徐々に改良されてきました。
相変わらず情報源が少ないので難儀しますが、いまのところ以下を参考にしています。

(1)GNOME Shell Javascript Source Reference
どのようなUIがあるのか画像つきで説明してあるのでわかりやすい。ここで使いたいUIを探します。

(2)GNOME gnome-shell/js/ui/
ここにソースがあるので、(1)で見つけたUIのソースを開いて使えそうな関数を調べています。

(3)GNOME applications in JavaScript
また、ここに一通りのリファレンスがあるので参考にしています。

(4)St Reference
Stに関してならここでしょうか。

(5)GNOME Creating an Applet
ここにボタンやメニュー以外にもファイル入出力やHTTPリクエストなどのサンプルがあります。

2018年6月10日日曜日

Ubuntu 18.04:Anaconda仮想環境「conda activate py36」に変更

これまでは、Anacondaの仮想環境を切り替えるには、

source activate py36

をターミナルで入力していましたが、いつのまにか(conda4.4から)

conda activate py36

に変わっていたらしく、設定を変えてみました。



pyenvも導入しているため「activate」がそもそも重複しており問題がありました。おそらくこれで問題解消できるのかもしれません。変更方法については、conda Change logに説明があります。

これまでは「.bashrc」に、

export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"
export PATH="$PYENV_ROOT/versions/anaconda3-5.1.0/bin/:$PATH"

を記入していました。1〜3行目はpyenv用、4行目がAnaconda用。
それを以下のように変更(4行目だけ)。

export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"
. $PYENV_ROOT/versions/anaconda3-5.1.0/etc/profile.d/conda.sh

これで次回から、

conda activate py36

を入力して仮想環境を切り替えることになります。


ランチャー用のスクリプトの場合:
ランチャー(.desktopファイル)を使って「py36」仮想環境に入った状態でターミナルを起動させるには、

#!/bin/bash
eval '$BASH_POST_RC'
BASH_POST_RC='conda activate py36' gnome-terminal

このスクリプトを「py36.sh」などと保存して、

chmod +x py36.sh

で実行権限を与えておきます。
そして「.bashrc」のほうに

eval '$BASH_POST_RC'

を書いておいて、ターミナルが起動する前に「conda activate py36」が実行されるようにしておきます。
そしてランチャーのコマンドにこのスクリプトを実行させるため、

Exec=/home/mirrornerror/myScript/py36.sh

などと「py36.sh」までのパスも含めて記入しておきます。
あとはランチャーをダブルクリックすれば起動するはずです。


Macの場合:
MacのほうでもpyenvとAnacondaを導入しているので同じようにやってみましたが、Macの場合は「.bashrc」ではなく「.bash_profile」のほうに書くと問題なく起動しました。

export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"
. $PYENV_ROOT/versions/anaconda3-5.1.0/etc/profile.d/conda.sh

ちなみに、MacでのbCNC(Gコード送信アプリ)を開くためのランチャー用スクリプトは、

#!/bin/bash
. ~/.pyenv/versions/anaconda3-5.1.0/etc/profile.d/conda.sh
conda activate py27
python bCNC.py

にしています。


2018年6月5日火曜日

Ubuntu 18.04:ディスプレイ色温度設定

MacBookよりもディスプレイの色が青白い(眩しい)ような気がしたので色温度を変えられないかということで探してみると、「設定>デバイス>カラー」にありました。


色温度は「ディスプレイ」の項目にあるのかと思ったら「カラー>ラップトップの画面」をクリックし、「プロファイルの追加」をクリックすると別窓がでてきて、その中から選べるようです。
今回は「D55」を選び、やや暖色系にしてみました。黄色すぎると思いましたが、しばらく使っていると普通の色に見えてきます。逆にデフォルト(D65相当)に戻すと、青すぎて驚きます。


最初は「ソフトウェア」から「redshift」という機能拡張を試してみたのですが、調子が悪くてあまり使えませんでした。


「redshift」はgnome shell extensionsのほうにあり、そちらも試してみましたが、いまいち。


きちんと機能すれば便利そうなのですが、設定しても画面がちらついたり不安定なのでアンインストールしてしまいました。
いすれにせよ、何もインストールせずに先程の方法で色調整できたのでよかったです。
まだまだ、Ubuntu 18.04の細かなカスタマイズや調整は続きそうです。

2018年6月3日日曜日

Ubuntu 18.04:テキストエディター(gedit)で毎回新規ウィンドウで開く設定

ランチャーなどの簡単なスクリプトはテキストエディター(gedit)を使っていますが、複数書類を開くときにタブで表示されるので、新規ウィンドウで開くように設定できないかと。
調べてみると、ここにその方法が書いてありました。

「/usr/share/applications/org.gnome.gedit.desktop」のデスクトップファイルの中に記述されているコマンドを

Exec=gedit -s %U

にする。「-s」は新規ウィンドウで開くオプションのようで、「%U」についてはここに書いてあり、複数のファイルパスに対応しているようです。

それから、このコマンドでタブ表示不可'never'にします(dconfエディターでも可)。

gsettings set org.gnome.gedit.preferences.ui show-tabs-mode 'never'


dcofエディターの場合


このように設定し直したのですが、まだ少し不完全な挙動。
再度「/usr/share/applications」の中にある「.desktop」ファイルを見てみると、まず「org.gnome.gedit.desktop~」というのがあり、このファイルを書き換えたのですが、よく見ると末尾に「~」がついています。おそらくこれはバックアップファイル。


さらによく見てみると、「Text Editor」というファイルが2個もあります(以下)。



もともと表示名は日本語の「テキストエディター」でありファイル名も違うしわかりにくい。
いずれにせよ3つすべて書き換えればいいはずですが、この「Text Editor」というファイル名は「ls」コマンドでは出てこない。
「/usr/share/appliactions」の中を「ls -a | grep 'edit'」で探してみると、

gedit.desktop
nm-connection-editor.desktop
org.gnome.gedit.desktop
org.gnome.gedit.desktop~

この4つがでてくるので、2番目以外の3つを書き換えるということになります。
まず上から、

sudo nano gedit.desktop

で見てみると、ひとつだけ書き換えられていないものを発見。おそらくこれが理由で変な挙動になっていたのでは。残り2個は以前書き換えたときに、

Exec=gedit -s %U

にしたので大丈夫です。
ということで3つとも「-s」を付け加えて書き換えてみるときちんと別窓で開くようになりました。
それにしてもファイル名が表示名と違うのでわかりにくい。
Ubuntu 18.04にアップグレードしても、使っているうちにまだまだ細かな設定が必要になりそうです。

2018年5月31日木曜日

Ubuntu 18.04: Gnome-Shell-Extensionsでターミナル起動+Anaconda仮想環境に入る

Ubuntu 18.04のカスタマイズのついでに、自作Gnome Shell Extensionsにチャレンジ。ネットを探してもあまりサンプル例がなく、最初はこちらを参考にしました。


今回つくろうとしたもの:

・トップバーにアイコン(下画像:左から3個目のアイコン)をつける
・アイコンをクリックすると、Anacondaの仮想環境に入った状態でターミナル起動

というものです。


Anacondaの仮想環境を使っているため、通常はターミナルを立ち上げて、

source activate py36

を打ち込んで仮想環境に入りますが、この部分をワンクリックでできないかというものです。設定した「py36」という仮想環境には、機械学習用にTensorfowやPytorchなどのライブラリが入っています。


「source activate py36」のシェルスクリプト:
まず、仮想環境に入るためのコマンドである「source activate py36」を記入する「py36.sh」ファイルをつくります。そのまま「source activate py36」を実行させればいいというわけではなく(やってみたら失敗)、こちらを参考にしました。「source 〜」の場合は以下のようにやるといいようです。
追記:その後「conda activate py36」へ変更しました(こちらへ)。

・「py36.sh」ファイルをつくる(場所は問わず)。

・「py36.sh」の中に、以下の2行を書いて保存。

eval '$BASH_POST_RC'
BASH_POST_RC='source activate py36' gnome-terminal

・「py36.sh」を実行可能にするために、ファイルを右クリックで「プロパティ>アクセス権>プログラムとして実行可能」にチェック(「chmod +x py36.sh」でも可)。



・「.bashrc」ファイル末尾のほうに

eval "$BASH_POST_RC"

を書き込んでおく。以上。

・「py36.sh」ファイルをダブルクリックすれば先程の画像のように仮想環境に入った状態で起動するはず。そのままランチャーにしてもいいし、desktopファイルと連携してもいいと思います。

そのままコマンドを書いただけだと実行したらターミナルは終了となってしまうので、今回のようにターミナルが起動する直前に「eval」を使って「.bashrc」からコマンドを実行させると大丈夫なようです。
あとは、このシェルスクリプトを実行させるGnome Shell Extensionsをつくります。

screenを使った場合(おまけ:その1):
「eval」の方法が分からず「screen」で仮想環境をつくって試したこともありました。
以下のスクリプトをランチャーのようにダブルクリックで起動させると、screenで仮想環境のなかのAnacondaの仮想環境の中に入った状態でターミナルが起動します。

#!/bin/bash

#まず外部からscreen仮想環境をつくる
screen -dmS sc1

#外部から、そのscreen仮想環境にAnaconda仮想環境をつくる
screen -S sc1 -X stuff 'source ~/.pyenv/versions/anaconda3-5.1.0/bin/activate py36'`echo -ne '\015'`

#最後にその二重の仮想環境に入る
gnome-terminal -e 'screen -x'

一応大丈夫なのですが、消し忘れるとすっど動いているということと、ここまで無理する必要もないかと、ようやく今回の方法に至りました。screenの勉強にはなったのでいずれ何かに使えればと。
尚、「sudo apt-get install screen」でインストール。

xdotoolを使った場合(おまけ:その2):
キー入力を自動化してくれる「xdotool」というのがあり、そのまま今回の「source activate py36(リターン)」を打ち込ませるスクリプトを書くだけ。

#!/bin/bash

#まずターミナル起動
gnome-terminal

#ターミナルが開くまで1秒ディレイ
sleep 1s

#仮想環境に入るコマンド
xdotool type 'source activate py36'

#最後にリターンキー入力
xdotool key Return

「py36.sh」などと保存し、右クリックで「プロパティ>アクセス権>プログラムとして実行可能」にチェック。ダブルクリックすればターミナルを起動しコマンドを実行してくれます。
長いスクリプトは「type」、一文字打つだけなら「key」を使って、普段ターミナルで打ち込むコマンドを書けばいいだけです。
この他、画面にフォーカスを与えたりマウス入力も可能なので、いろんなものを簡単に自動化できます。
xdotoolは、「sudo apt-get install xdotool」でインストール。


Gnome Shell Extensions(サンプル)をつくる:
Javascriptなので簡単なのかと思ったら独特の構文でわかりにくく、ネットで検索してもチュートリアルやサンプルも少なかったり(古かったり)するので、フルスクラッチでコーディングするよりも似たようなものを書き換えたほうがいいらしいです。
まずはこちらを参考に、ターミナルで以下を入力すると

gnome-shell-extension-tool --create-extension

対話形式で名前や説明など(とりあえず簡単に書いておく)を決めていくと、
「Hello World」サンプルをつくることができます。


Name: Open_py36
Description: Open Terminal @py36
Uuid: 記入しなかったため自動的に「Open_py__@mne-ubu」になった

そうすると、以下のような画面がでてきます。


これは、メインのスクリプトの「extension.js」。
あとは、CSSとJSONファイルが付属しています。


これらのデータは、「~/.local/share/gnome-shell/extensions/」にあります。


サンプルを動かしてみる:
サンプルを動かすには再起動が必要なのですが、「Alt+F2」を押すと画面はそのままでExtensionsの再起動ができます。「Alt+F2」を押すと以下の画面がでてくるのでここで「r」を入力してリターンを押します。


ちょっと画面が変化して再起動します。
「Gnome-Tweaks>機能拡張」を開くと、先程のExtensionがリストに加わっているのでオンにします(以下:上から二段目)。


そうすると、トップバー右上にアイコンがでてきます。
このサンプルはトップバー上のアイコンをクリックすると画面中央に「Hello World」という文字が出現してフェイドアウトしていく(2秒間)というものなので、「Gnome-Tweaks>外観>アニメーション」をオンにしておく必要があります。



スクリプトを書き換える:
次は、このサンプルをベースに最初につくった「py36.sh」を実行させるプログラムに書き換えます。
スクリプトを実行させるファンクションは、検索するとここが参考になりました。

const Util = imports.misc.util;

Util.spawnCommandLine("script");

一行目でライブラリをインポートし、二行目はスクリプト実行するファンクションのようです。「script」のところに、「py36.sh」のパスを入れればよさそうです。
「Hellow World」の表示内容を「env: py36」に変更し、位置も少し上の方に変えておきます。


const St = imports.gi.St;
const Main = imports.ui.main;
const Tweener = imports.ui.tweener;

//追加:ライブラリインポート
const Util = imports.misc.util;

let text, button;

function _hideHello() {
    Main.uiGroup.remove_actor(text);
    text = null;
}

function _showHello() {
    //以下表示テキスト内容を「env: py36」へ変更
    if (!text) {
        text = new St.Label({ style_class: 'helloworld-label', text: "env: py36" });
        Main.uiGroup.add_actor(text);
    }

    text.opacity = 255;

    let monitor = Main.layoutManager.primaryMonitor;

    //以下の高さ(monitor.height / 4)へ変更
    text.set_position(monitor.x + Math.floor(monitor.width / 2 - text.width / 2),
                      monitor.y + Math.floor(monitor.height / 4 - text.height / 2));

    Tweener.addTween(text,
                     { opacity: 0,
                       time: 2,
                       transition: 'easeOutQuad',
                       onComplete: _hideHello });

 //以下のスクリプト実行コマンドを追加
 Util.spawnCommandLine("/home/mirrornerror/Documents/myshell/py36.sh");
}

function init() {
    button = new St.Bin({ style_class: 'panel-button',
                          reactive: true,
                          can_focus: true,
                          x_fill: true,
                          y_fill: false,
                          track_hover: true });

    //以下のアイコンをターミナルへ変更
    let icon = new St.Icon({ icon_name: 'utilities-terminal-symbolic',
                             style_class: 'system-status-icon' });

    button.set_child(icon);
    button.connect('button-press-event', _showHello);
}

function enable() {
    Main.panel._rightBox.insert_child_at_index(button, 0);
}

function disable() {
    Main.panel._rightBox.remove_child(button);
}

5箇所ほど変更追加(赤文字)しただけです。
アイコンについては、「Icon Browser」が便利です。
以下のようにアプリケーションとして、既存のアイコンリストを見ることができます。
たしか「sudo apt-get gtk3-icon-browser」でインストールできたはずです。


NormalとSymbolicというのがあり、Symbolicがシンプルなアイコンです。


今回参考としたサイト:
あまりないのですが、以下。あとは既存のExtensionsのコードを参考に改造していくほうが早そうです。
GNOME Shellの拡張機能を作ってみよう
How I developed my first gnome-shell extension
GNOME WIKI: Extensions
github gnome-shell-extensions
GNOME Developer: Tutorial for beginners and code samples
GNOME Developer: gtk3-icon-browser


「Argos」というGnome-shell-extensions:
簡単なものであれば、gnome-shell-extensionsにある「Argos」というextensionが非常に便利です。トップバーに追加できる機能拡張で、よく使うフォルダや自作のスクリプトなども実行できます。


Argosのgithubサイトに使い方など詳しくのっています。

今回つくったAnaconda仮想環境に入るためのスクリプトも入れてみましたが結構簡単に実装できました。


この画像にあるように比較的よく使うものを組み込んでみました。
・普通のターミナル起動
・Anaconda仮想環境ターミナル起動
・ローカルネットワークサーバ起動
・各ディレクトリ(Home/Documents/Downloads)

一番下にある「argos.r.sh」をクリックするとスクリプトを書くためのファイルが開きます(以下)。
#!/usr/bin/env bash

URL="github.com/p-e-w/argos"
DIR=$(dirname "$0")
HOME=/home/mirrornerror
DOC=/home/mirrornerror/Documents
DWN=/home/mirrornerror/Downloads
PYTHON=/home/mirrornerror/.pyenv/versions/anaconda3-5.1.0/bin//python

echo "| iconName=view-more-symbolic"
echo "---"
echo "Terminal | iconName=utilities-terminal-symbolic bash=pwd terminal=true"
echo "Anaconda py36 | iconName=utilities-terminal-symbolic bash='/home/mirrornerror/Documents/myshell/py36.sh' && 'terminal=false'"
echo "192.168.3.7:8008 | iconName=utilities-terminal-symbolic bash='$PYTHON -m http.server 8008 --bind 192.168.3.7' && 'terminal=true'"
echo "---"
echo "home | iconName=folder-symbolic href='file://$HOME'"
echo "Documents | iconName=folder-symbolic href='file://$DOC'"
echo "Downloads | iconName=folder-symbolic href='file://$DWN'"
echo "---"
echo "more"
echo "--$URL | iconName=help-faq-symbolic href='https://$URL'"
echo "--$DIR | iconName=folder-symbolic href='file://$DIR'"

いまのところこんな感じです。
echoを追加していくことで、内容がリスト化(ネストも可)されていきます。あとはタイトルとアイコン、そしてコマンドを記入していきます。外部にシェルスクリプトを配置しておけば、ここからコマンド起動させることができるので、いろんなことができそうです。

関連:
gnome shell extensions:自作その2

人気の投稿