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

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



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


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にアップグレードしても、使っているうちにまだまだ細かな設定が必要になりそうです。

人気の投稿