chrome拡張機能

今回のとりあえずやってみるはchrome拡張機能。 普段からadblock系やVue.js Devtoolsをブラウザに導入して使っているけど、その作り方に関する知識はまったくのゼロ

今回はchrome公式ドキュメントをGetting Started Tutorialをやってみて、chrome拡張機能のお作法を学んでみる。 (お作法全体を振り返りません、自分がポイントだと思うところを書きだすだけなので、チュートリアルとして利用しないで。。)

最終的にはオリジナル拡張機能を作る予定なので、記事をシリーズ化して今後まとめていこうかなと。

persistentオプション

バックグラウンドスクリプトに対して設定できるオプション persistentは持続的なという意味

persistent: trueにすると、バックグラウンドスクリプトが常に"アクティブ"でメモリ上にロードされている
一方、persistent: falseにすると、バックグラウンドスクリプトがイベント発火時にメモリにロードされるイメージ

persistent: falseにすることが推奨されていて、つまりイベント駆動のバックグラウンドスクリプトにしなさいという話らしい
(参照: Migrate to Event Driven Background Scripts)

chrome.storage.sync.setで設定した値を確認する

  chrome.storage.sync.set({color: '#3aa757'}, function() {
    console.log("The color is green.");
  });

上記のコードでバックグランドの変更色を設定している。 この情報は普通のdevtoolでは確認できないようで、consoleから確認するしか術がないようだ。

chrome://extensions/ → バックグランドページ(どの拡張機能のリンクをたどってもよい) → consoleタブ

上記でたどり着いたconsoleから、

chrome.storage.sync.get(null, function (data) { console.info(data) });

を実行すると、設定した値が確認できる

ナレッジを収集するslack botの作成~GASを使って~

ナレッジを収集したい

slack上のナレッジを収集するslack botを作成しよう。アイデアの着想を得たのは、ある企業の人をお話から。その企業はslack上の会話によって蓄積されるナレッジを体系的な収集するslack botを作成したらしい。

私も個人開発でslackを使っている。単純にチャンネルごとにメモを分けるだけでも、タスク管理の効果があるし、スター機能を使えば重要な情報を管理できる。しかし、スター付けるほどではない情報やメソッド(これらをナレッジと呼ぼう)も多数あり、これらを効率的に管理する枠組みが必要であると感じていた。よし、私もナレッジを管理できるslack botを作成しよう。

どうやら、その企業のbotは言語処理モデルも組み込んで非常に高度なことをやっているようだが、今回は簡単に明示的にコマンドを打つことによってナレッジを保存するシステムを作成しようと考えている。

機能

  • ナレッジのジャンルを登録できる機能
  • ジャンルごとにナレッジを登録できる機能
  • spread sheetに追加したナレッジをジャンルごとにslack上で呼び出せる機能

slack botの作り方

slack botの作成には、slack側の設定と実際アプリが機能する環境を用意する必要がある。ここではslack側の設定をまとめる。 タスクフローは以下の通り。

  1. slack apiにアクセス(https://api.slack.com/apps)
  2. Create New Appからアプリを作成
  3. OAuth設定
    1. 左側メニューからFeatures→OAuth&Permissions
    2. 「app_mentions:read、chat:write」の2つのスコープを与える

 

f:id:kpsdr:20200302213327p:plain
アプリのスコープ

  • Event Subscriptionsの設定
    • slack上のイベントをアプリに通知するための設定です
    • webアプリのurlが必要なので後で設定します

GAS(Google App Script)が便利

実際にアプリが動作する環境としてGAS(Google App Script)を利用しよう。googleアカウントさえあれば、すぐGASの実行環境を作成できる。また、ボタン1つでwebアプリとして公開することも可能だ。

GASプロジェクトの立ち上げ、SlackAppライブラリの導入

私が参考にしたサイトを載せる。(分かりやすい) qiita.com

Bot User OAuth Access Tokenの登録

先ほどのslack apiのアプリ設定画面を開きます。 Features→OAuth&PermissionsにあるBot User OAuth Access Tokenをコピーしてください。

そして、そのコードをGASのプロジェクトに登録します。

  1. ファイル→プロジェクトのプロパティ→スクリプトのプロパティ
  2. プロパティ: SLACK_ACCESS_TOKEN, 値: 先ほどコピーしたコード

コードピックアップ

コードの中でポイントとなるところだけをpick upします。

const params = JSON.parse(e.postData.getDataAsString());
=========================================================
return ContentService.createTextOutput(params.challenge);

params.challengeをreturnしてあげることで認証が完了します。

function writeLog(text){
  let spreadSheet1 = SpreadsheetApp.openByUrl('ログ用spread sheetの共有url');
  let sheet1 = spreadSheet1.getSheets()[0];
  // 値の書き込み
  sheet1.appendRow([new Date(), text]);
}

デバックするための関数。 GASはGCPとの連携をしないとデバックが困難。 適当なspread sheetを用意して、そのシートにログを出力しながらデバックするのがよし。

参考にさせてもらいました。

Google Apps Scriptでログをスプレッドシートに書き出す方法

コード

let spreadSheete;
let sheet;

function doPost(e) {
  try{
  const token = PropertiesService.getScriptProperties().getProperty('KNWL_SLACK_ACCESS_TOKEN');  
  const slackApp = SlackApp.create(token);

  const params = JSON.parse(e.postData.getDataAsString());
  const commands = params.event.text.split(' ');
  // 返送メッセージ
  let message;
  
  /* コマンド解析 */
  const action = commands[1];
  const genre = commands[2];
  let content;
  if(commands.length == 3){
    content = null;
  }else if(commands.length == 4){
    content = commands[3];
  }else{
   const options = {
    channelId: "#general",
    userName: "knwl",
    message: "書式に違反しています"
   }
   slackApp.postMessage(options.channelId, options.message, {username: options.userName});
   // validation
   return ContentService.createTextOutput(params.challenge);
  }
      
  /* ファイルを開く */
  spreadSheet = SpreadsheetApp.openByUrl('ナレッジを書き込むspread sheetの共有url');
  sheet = spreadSheet.getSheets()[0];
  
  /* アクションごとに関数を呼び出す */
  switch(action){
    case 'setGenre':
      message = setGenre(genre); // ジャンルをセットする
      break;
    case 'set':
      message = setInfo(genre, content); // あるジャンルにナレッジをセットする
      break;
    case 'get':
      if(genre == "genres"){ 
        message = getGenres(); // ジャンル一覧を取得する
      }else{
        message = getGenreInfo(genre); // あるジャンルのナレッジをすべて取得する
      }
      break;
    default:
      break;
  }
  
  /* メッセージの送信 */
  let options = {
    channelId: "#general",
    userName: "knwl",
    message: message
  }
  // send message
  slackApp.postMessage(options.channelId, options.message, {username: options.userName});
  
  /* validation */
  return ContentService.createTextOutput(params.challenge);
  }catch(err){
    writeLog(err);
  }

}

function doGet(e){
  doPost(e);
}

function setGenre(newGenre){
  // 最終列を取得
  const lastCol = sheet.getLastColumn();
  // ジャンルが1つも存在しないとき
  if(lastCol == 0){
    sheet.getRange(1, 1).setValue(newGenre);
    sheet.getRange(2, 1).setValue(2);
    return '正常にジャンルを登録しました。 '
  };
  
  // すべてのジャンル名を取得
  const genres = sheet.getRange(1,1,1,lastCol).getValues()[0];
  
  if(genres.indexOf(newGenre) == -1){
    sheet.getRange(1, lastCol+1).setValue(newGenre);
    sheet.getRange(2, lastCol+1).setValue(2);
    return "正常にジャンルを登録しました。"
  }else{
    return "そのジャンルはすでに登録されています。"
  }
}

function setInfo(genre, info){
  // 最終列を取得
  const lastCol = sheet.getLastColumn();
  if(lastCol == 0){ return 'ジャンルが1つも存在しません '};
  
  // すべてのジャンル名を取得
  const genres = sheet.getRange(1,1,1,lastCol).getValues()[0];
  
  if(genres.indexOf(genre) == -1){
    return "ジャンルが存在しません"
  }else{
    const genreCol = genres.indexOf(genre) + 1;
    const genreRow = parseInt(sheet.getRange(2,genreCol).getValues()[0]);
    // infoの書き込み
    sheet.getRange(genreRow+1, genreCol).setValue(info);
    // genreの最終行の記録
    sheet.getRange(2,genreCol).setValue(genreRow+1);
    return "正常にナレッジを登録しました。"
  }
}

function getGenres(){
  // 最終列を取得
  const lastCol = sheet.getLastColumn();
  if(lastCol == 0){ return 'ジャンルが1つも存在しません '};
  
  // すべてのジャンル名を取得
  const genres = sheet.getRange(1,1,1,lastCol).getValues()[0];
  message = genres.join(' ');
  return message;
}

function getGenreInfo(genre){
   // 最終列を取得
  const lastCol = sheet.getLastColumn();
  if(lastCol == 0){ return 'ジャンルが1つも存在しません '};
  
  // すべてのジャンル名を取得
  const genres = sheet.getRange(1,1,1,lastCol).getValues()[0];
  
  if(genres.indexOf(genre) == -1){
    return "ジャンルが存在しません"
  }else{
    const genreCol = genres.indexOf(genre) + 1;
    const genreRow = parseInt(sheet.getRange(2,genreCol).getValues()[0]);
    // genreのinfoを取得
    const allInfo = sheet.getRange(3, genreCol, genreRow - 2, 1).getValues();
    return allInfo.join(' ');
  }
}

function writeLog(text){
  let spreadSheet1 = SpreadsheetApp.openByUrl('ログ用spread sheetの共有url');
  let sheet1 = spreadSheet1.getSheets()[0];
  
  // 値の書き込み
  sheet1.appendRow([new Date(), text]);
}

Events Subscriptionsの設定

さっき保留していたEvents Subscriptionsの設定。webアプリケーションのurlをslack apiの設定画面に登録する。

  • webアプリケーションの公開
    • GAS上のメニュー→webアプケーションとして導入から更新
    • urlをコピー

    ※ プログラムを編集する度にProject VersionをNewにして更新

    ※ 公開範囲を「Anyone, even anonymous」に

f:id:kpsdr:20200307173127p:plain
webアプケーションとして公開

  • Events Subscriptionsの設定
    1. slack apiのアプリ設定画面からEvents Subscriptionsへ
    2. Enable EventsをOnに
    3. 先ほどコピーurlをRequest URLに張り付け
    4. slack上で検知するeventを設定 「Subscribe to bot events」で 「app_mention 」を設定

f:id:kpsdr:20200307174112p:plain
Event Subscriptionsの設定

完成!! 各コマンドの機能を説明

  • @アプリ名 setGenre ジャンル1
    • ジャンル1をspread sheetにジャンルとして登録する
  • @アプリ名 set ジャンル1 ナレッジ1
    • ナレッジ1をジャンル1に登録
  • @アプリ名 get genres
    • 登録されている全ジャンルを表示
  • @アプリ名 get ジャンル1
    • ジャンル1のすべてのナレッジを取得し、slack上に表示する

まとめ

はじめてslack botを作ったが、GASが便利すぎる。自分でサーバーも用意しなくていいし、deployして外に公開するのもボタン1つ。この簡易性は代えがたい。ただ、GAS単体ではデバックがしにくかったり、オンラインの開発環境なのでgit等での管理ができなかったり(やり方はあるかも・・・)不便なまだまだは多い。それでも、簡単なslack botを作るぐらいなら非常に有用性があることは間違いなさそう。

参考サイト

異なる背景に対する学習精度の変化

狙い

CNNを利用したネットワークでは認識したい物体の背景が学習に影響を与えると考えられている。
今回の記事ではその現象を実験的に示した。

データ

丸と四角をクラスとする2値分類問題とする。

グループ1(背景差なし)

f:id:kpsdr:20180613212822p:plain:w100f:id:kpsdr:20180613213900p:plain:w100
丸、四角クラス画像の例 背景は均一に赤

グループ2(背景差あり)

f:id:kpsdr:20180613212822p:plainf:id:kpsdr:20180613214438p:plainf:id:kpsdr:20180613213900p:plainf:id:kpsdr:20180613214434p:plain
丸、四角クラス画像の例 背景は赤と青

各グループにおいて、クラス違い、背景違いで等分されるようにTrain,Testに分割した。

モデル

# モデルの定義
model = Sequential()

model.add(Conv2D(32,3,input_shape=(256,256,3),kernel_initializer='glorot_uniform'))
model.add(Activation('sigmoid'))
model.add(Conv2D(32,3,kernel_initializer='glorot_uniform'))
model.add(Activation('relu'))
model.add(MaxPool2D(pool_size=(2,2)))

model.add(Conv2D(64,3,kernel_initializer='glorot_uniform'))
model.add(Activation('relu'))
model.add(MaxPool2D(pool_size=(2,2)))

model.add(Conv2D(128,3,kernel_initializer='glorot_uniform'))
model.add(Activation('relu'))
model.add(MaxPool2D(pool_size=(2,2)))

model.add(Conv2D(64,3,kernel_initializer='glorot_uniform'))
model.add(Activation('relu'))
model.add(MaxPool2D(pool_size=(2,2)))

model.add(Conv2D(64,3,kernel_initializer='glorot_uniform'))
model.add(Activation('relu'))
model.add(MaxPool2D(pool_size=(2,2)))

model.add(Conv2D(64,3,kernel_initializer='glorot_uniform'))
model.add(Activation('relu'))
model.add(MaxPool2D(pool_size=(2,2)))

model.add(Flatten())
model.add(Dense(1024))
model.add(Activation('relu'))
model.add(Dropout(1.0))

model.add(Dense(2, activation='softmax'))

rms = RMSprop(lr=0.0005, rho=0.9, epsilon=None, decay=0.0)
run_options = tf.RunOptions(report_tensor_allocations_upon_oom = True)
model.compile(optimizer=rms, loss='binary_crossentropy', metrics=["accuracy"],options=run_options)

結果

グループごとのvalidation error

val_error 1回目 2回目 3回目
G1 (背景差なし) 81.25% 81.25% 87.50%
G2 (背景差あり) 62.50% 56.25% 68,75%

まとめ

実験結果からもわかるように背景差がないグループの方が高い汎化性能を示している。
背景差がないということは画素値の変化が各画像間で少ないことを示し,モデルがフィッティグする際の変数が少ないとも言い換えられる。
機械学習チュートリアル的データセットであるMNISTは1チャンネルのモノクロ画像であるが、背景差なしのデータセットと考えることもできるだろう。

背景差に対応する手段としては、背景変化がある画像を大量に用意することやコントラスト正規化等の手法が想定されるであろう。

簡単な分類問題を解いてみる

目標

機械学習の効果を確認するために、簡単な分類問題を解いてみよう。

データ

以下のようなデータをpythonで出力する。

f:id:kpsdr:20171104203620j:plain

     図1 散布図

エリアごとに色分けされたデータが散らばっていることがわかる。

各点の色は座標(x1,x2)によって定まるようである。

グラフのデータを機械学習で利用しやすい形に持ち込む。

f:id:kpsdr:20171104205333j:plain

       図2 行列データ

図2において、1行目はx1 =0.031295,x2=0.284174の座標に赤点を存在することをします。

モデル

f:id:kpsdr:20171105184435j:plain
図3モデル

プログラム

今のプログラムの目的は、学習の経過を示すグラフの出力と新データに対して正しくラベル(色)が予測されているかを確認することである。

import keras
from keras.layers import Input, Dense#,Dropout
from keras.models import Model
import numpy as np
import matplotlib.pyplot as plt
from keras.optimizers import RMSprop


#データを作成

# generate data
x1_r = np.random.rand(100)*0.5
x2_r = np.random.rand(100)*0.5

#label 1(red)
l_r = np.zeros_like(x1_r)
l_r = l_r + 0

x1_b = np.random.rand(100)*0.5 + 0.5
x2_b = np.random.rand(100)*0.5

#label 2(blue)
l_b = np.zeros_like(x1_b)
l_b = l_b + 1


x1_g = np.random.rand(100)*0.5
x2_g = np.random.rand(100)*0.5 + 0.5

#label 3(green)
l_g = np.zeros_like(x1_g)
l_g = l_g + 2


x1_y = np.random.rand(100)*0.5 + 0.5
x2_y = np.random.rand(100)*0.5 + 0.5

#label 4(yellow)
l_y = np.zeros_like(x1_y)
l_y = l_y + 3

#array merge
A_r = np.c_[x1_r,x2_r,l_r]
A_b = np.c_[x1_b,x2_b,l_b]
A_g = np.c_[x1_g,x2_g,l_g]
A_y = np.c_[x1_y,x2_y,l_y]


A = np.r_[A_r,A_b,A_g,A_y]

#shuffle
np.random.shuffle(A)


#split トレイン用データとテスト用データを分離
data,label = np.hsplit(A,[2])
#data_train :(300,2) date_test :(100,2) 
data_train,data_test = np.vsplit(data,[300])
#label_train :(300,1) label_test :(100,1)  
label_train,label_test = np.vsplit(label,[300])

label_train = keras.utils.np_utils.to_categorical(label_train.astype('int32'),4)
label_test = keras.utils.np_utils.to_categorical(label_test.astype('int32'),4)


#ニューラルネットワークのモデルを定義
#入力層 2入力
inputs = Input(shape=(2,))
#隠れ層 3出力で活性化関数ははrelu
nw = Dense(3, activation='relu')(inputs)
#出力層 4出力 ソフトマックス関数を挿入(0~1の値に収める)
predictions = Dense(4, activation='softmax')(nw)

#モデルをまとめる
model = Model(inputs=inputs, outputs=predictions)
#モデルをコンパイル  最適化手法:RMSprop 目的関数:categorical_crossentropy
model.compile(optimizer=RMSprop(),
              loss='categorical_crossentropy',
              metrics=['accuracy'])
#学習の実行 epochsは学習回数 historyには学習の経過が記録される。
history = model.fit(data_train, label_train, batch_size=10, epochs=150, verbose=1,validation_data=(data_test, label_test))

#学習の経過をグラフ化
plt.plot(history.history['acc'])
plt.plot(history.history['val_acc'])
plt.title('model accuracy')
plt.ylabel('accuracy')
plt.xlabel('epoch')
plt.legend(['train', 'test'], loc='upper left')
plt.show()


#新データに対する予測
xa = np.array([[0.8,0.7]])
print("新データ(0.8,0.7)に対する予想")
print(model.predict(xa))

出力

f:id:kpsdr:20171106212010j:plain
図4 認識精度の推移


epoch(学習回数)が増えるごとに認識精度が向上していることが見て取れる。

新データ(0.8,0.7)に対する予想
[[ 0.00191311  0.06421792  0.00333812  0.93053085]]

学習済みのニューラルネットワークは新データ(0.8,0.7)に対して4番目の要素(黄色に対応)である確率が
0.93053085でもっとも高いと予想している。
図1を見てみるとデータ(0.8,0.7)には黄色のラベルが付されそうである。
人間の判断とニューラルネットワークの判断が一致している。
このニューラルネットワークは正確な分類器として機能しているようである。

まとめ

非常に簡単なデータにたいして、ニューラルネットワークをトレーニングさせ、高精度の分類器を実現できた。
今回はepoch数を150に設定したが、50に設定しトレーニングすると、認識精度は150のときのそれに及ばなかった。つまり、epoch数を増やすことで認識精度が向上する様子を観察できた。
さらなる認識精度の向上には、さまさまな手法が存在するが、またの機会に記事にしたい。

KerasとTensorFlowで機械学習始めます。(1)

 

Kerasとtensorflowなのか

機械学習のライブラリといえば、Chainer,TensorFlow,Theanoなどがあがると思います。

機械学習を始めたての私にとっては当然どれがいいのかわからなくて、とりあえず国産のライブラリであるChainerから始めました。国産であるので、日本語のリファレンス等も豊富で、さらに記法もわかりやすい! といいところづくめだったんですが,GPU対応がどうしてもできませんでした。WindowsマシンにGPU対応環境をつくることが私の能力ではできなかった・・・。

そこで次にTensorFlowに手を出しました。Googleを出してるライブラリで世界的なシェアが大きく、学ぶには大きな意味がありそうでした。さらにTensorFlowでよかったのがWindows マシンでGPU環境を作ることが簡単にできたことです。オンラインのリファレンス読んで、もろもろのソフトのversionに気を付ければ結構簡単にGPUを利用できるようになりました!

しかし!私はこのTensorFlowを断念しました! なんか書き方が難しい! マジ無理!

計算グラフを常に考えた記法は私にとって少々難解でした。早くプログラム動かしたいのに、なんか覚えるメソッド多すぎ! 私はTensorFlowとお別れしました。

 

ただ、TensorFlowでGPU環境を作れたので、このアドバンテージは捨てたくない。

そこで、救世主Kerasが現れました。

KerasはTensorFlowなどのライブラリをバックエンド(計算等は元のライブラリに任せる)として機械学習のプログラムを簡単に書ける優れものでした。

TensorFlowで作ったGPU環境を利用しつつ簡単な記述でニューラルネットワークを記述できるんです!私自身まだ始めたてなのですが、これはいけるぞ! という感じがありますね。

 

このような経緯で私はKerasとTensorFlowでコーディングをしていくことを決意しました。

 

これから

このブログではとにかく、いろいろな問題に対して機械学習を用いて、解決策を見つけていこうと思います。とにかく、実装をこころがけて邁進します。

完全に私の忘備録になりますので、細かな説明などなくだらだら書いてしまうとおもいますが、もし興味を持ってコメントだったり、アドバイスをしてくれたら大変うれしいです。