次の方法で共有



2019 年 4 月

Volume 34 Number 4

[テストの実行]

PyTorch を使用するニューラル異常検出

James McCaffrey

James McCaffrey に心より感謝いたします。異常検出は、外れ値検出とも呼ばれ、データセット内の珍しい項目を見つけるプロセスです。たとえば、悪意のあるイベントをサーバー ログ ファイルから識別したり、不正なオンライン広告を検出したりすることが含まれます。

今回の主旨を理解するには、図 1 のデモ プログラムを見るのが一番です。このデモで、有名な MNIST (Modified National Institute of Standards and Technology) データセットの 1,000 項目のサブセットを分析します。各データ項目は、0 から 9 までの手書き数字で、28 x 28 サイズのグレースケールの画像 (784 ピクセル) です。MNIST データセット全体では、60,000 個のトレーニング イメージと 10,000 個のテスト イメージが含まれます。

Keras を使用する MNSIT イメージ異常検出
図 1 Keras を使用する MNSIT イメージ異常検出

このデモ プログラムは、PyTorch コード ライブラリを使用して、784-100-50-100-784 のディープ ニューラル オートエンコーダーを作成してトレーニングします。オートエンコーダーとは、入力を予測できるように学習するニューラル ネットワークのことです。トレーニング後、このデモは 1,000 個のイメージすべてをスキャンし、最も異常なイメージを検出します。"最も異常である" とは、再構築誤差が最も大きいことを意味します。最も異常な数字は、8 に似ているように見える 3 です。

この記事では、読者が中級レベル以上の C 言語ファミリのプログラミング スキルを持ち、機械学習の基礎を理解していることを前提としますが、オートエンコーダーの知識は問いません。すべてのデモ コードは記事に記載されています。コードとデータは、付属のダウンロードからも入手できます。中心となる考え方を可能な限り明瞭にするために、通常のエラー チェックはすべて削除しています。

PyTorch のインストール

PyTorch は、ニューラル ネットワークの作成に使用される比較的低レベルのコード ライブラリです。機能の面では、TensorFlow や CNTK とほぼ似ています。PyTorch は C++ で記述されていますが、より簡単にプログラミングできるように Python 言語 API を備えています。

PyTorch のインストールは、2 つの主なステップで行います。まず、Python と、いくつかの必要な補助パッケージ (NumPy や SciPy など) をインストールします。次に、PyTorch を Python アドオン パッケージとしてインストールします。Python と、PyTorch を実行するために必要なパッケージを個別にインストールすることもできますが、Python ディストリビューションをインストールする方がはるかに利点があります。Python ディストリビューションには、ベースの Python インタープリターと追加のパッケージが含まれており、それらは相互に互換性があるからです。このデモでは、Anaconda3 5.2.0 ディストリビューションをインストールしました。これには、Python 3.6.5 が含まれています。

Anaconda をインストールした後、pytorch.org Web サイトにアクセスし、Windows OS、Pip インストーラー、Python 3.6、CUDA GPU なしのバージョン用のオプションを選択しました。選択後に、それに対応する .whl (発音は wheel と同じ) ファイルを指す URL が指定されたので、このファイルをローカル コンピューターにインストールしました。Python エコシステムに詳しくない方は、Python .whl ファイルは Windows の .msi ファイルのようなものと見なすことができます。今回は、PyTorch バージョン 1.0.0 をダウンロードしました。コマンド シェルを開き、.whl ファイルがあるディレクトリに移動して、次のコマンドを入力しました。

pip install torch-1.0.0-cp36-cp36m-win_amd64.whl

デモ プログラム

スペースを節約するためのわずかな編集を加えた、完全なデモ プログラムを図 2 に示します。一般的には 4 つのスペースでインデントしますが、スペースを節約するために、ここでは 2 つにしています。Python では行継続文字として "\" が使用されます。プログラムの編集には、メモ帳を使用しました。同僚の大半はもっと洗練されたエディターを好みますが、私はメモ帳の徹底したシンプルさが気に入っているのです。

図 2 異常検出デモ プログラム

# auto_anom_mnist.py
# PyTorch 1.0.0 Anaconda3 5.2.0 (Python 3.6.5)
# autoencoder anomaly detection on MNIST
import numpy as np
import torch as T
import matplotlib.pyplot as plt
# -----------------------------------------------------------
def display(raw_data_x, raw_data_y, idx):
  label = raw_data_y[idx]  # like '5'
  print("digit/label = ", str(label), "\n")
  pixels = np.array(raw_data_x[idx])  # target row of pixels
  pixels = pixels.reshape((28,28))
  plt.rcParams['toolbar'] = 'None'
  plt.imshow(pixels, cmap=plt.get_cmap('gray_r'))
  plt.show() 
# -----------------------------------------------------------
class Batcher:
  def __init__(self, num_items, batch_size, seed=0):
    self.indices = np.arange(num_items)
    self.num_items = num_items
    self.batch_size = batch_size
    self.rnd = np.random.RandomState(seed)
    self.rnd.shuffle(self.indices)
    self.ptr = 0
  def __iter__(self):
    return self
  def __next__(self):
    if self.ptr + self.batch_size > self.num_items:
      self.rnd.shuffle(self.indices)
      self.ptr = 0
      raise StopIteration  # ugh.
    else:
      result = self.indices[self.ptr:self.ptr+self.batch_size]
      self.ptr += self.batch_size
      return result
# -----------------------------------------------------------
class Net(T.nn.Module):
  def __init__(self):
    super(Net, self).__init__()
    self.layer1 = T.nn.Linear(784, 100)  # hidden 1
    self.layer2 = T.nn.Linear(100, 50)
    self.layer3 = T.nn.Linear(50,100)
    self.layer4 = T.nn.Linear(100, 784)
    T.nn.init.xavier_uniform_(self.layer1.weight)  # glorot
    T.nn.init.zeros_(self.layer1.bias)
    T.nn.init.xavier_uniform_(self.layer2.weight) 
    T.nn.init.zeros_(self.layer2.bias)
    T.nn.init.xavier_uniform_(self.layer3.weight) 
    T.nn.init.zeros_(self.layer3.bias)
    T.nn.init.xavier_uniform_(self.layer4.weight) 
    T.nn.init.zeros_(self.layer4.bias)
  def forward(self, x):
    z = T.tanh(self.layer1(x))
    z = T.tanh(self.layer2(z))
    z = T.tanh(self.layer3(z))
    z = T.tanh(self.layer4(z))  # consider none or sigmoid
    return z
# -----------------------------------------------------------
def main():
  # 0. get started
  print("Begin autoencoder for MNIST anomaly detection")
  T.manual_seed(1)
  np.random.seed(1)
  # 1. load data
  print("Loading MNIST subset data into memory ")
  data_file = ".\\Data\\mnist_pytorch_1000.txt"
  data_x = np.loadtxt(data_file, delimiter=" ",
    usecols=range(2,786), dtype=np.float32)
  labels = np.loadtxt(data_file, delimiter=" ",
    usecols=[0], dtype=np.float32)
  norm_x = data_x / 255
  # 2. create autoencoder model
  net = Net()
  # 3. train autoencoder model
  net = net.train()  # explicitly set
  bat_size = 40
  loss_func = T.nn.MSELoss()
  optimizer = T.optim.Adam(net.parameters(), lr=0.01)
  batcher = Batcher(num_items=len(norm_x),
    batch_size=bat_size, seed=1)
  max_epochs = 100
  print("Starting training")
  for epoch in range(0, max_epochs):
    if epoch > 0 and epoch % (max_epochs/10) == 0:
      print("epoch = %6d" % epoch, end="")
      print("  prev batch loss = %7.4f" % loss_obj.item())
    for curr_bat in batcher:
      X = T.Tensor(norm_x[curr_bat])
      optimizer.zero_grad()
      oupt = net(X)
      loss_obj = loss_func(oupt, X)  # note X not Y
      loss_obj.backward()
      optimizer.step()
  print("Training complete")
  # 4. analyze - find item(s) with large(st) error
  net = net.eval()  # not needed - no dropout
  X = T.Tensor(norm_x)  # all input item as Tensors
  Y = net(X)            # all outputs as Tensors
  N = len(data_x)
  max_se = 0.0; max_ix = 0
  for i in range(N):
    curr_se = T.sum((X[i]-Y[i])*(X[i]-Y[i]))
    if curr_se.item() > max_se:
      max_se = curr_se.item()
      max_ix = i
  raw_data_x = data_x.astype(np.int)
  raw_data_y = labels.astype(np.int)
  print("Highest reconstruction error is index ", max_ix)
  display(raw_data_x, raw_data_y, max_ix)
  print("End autoencoder anomaly detection demo ")
# -----------------------------------------------------------
if __name__ == "__main__":
  main()

デモ プログラムは、NumPy、PyTorch、Matplotlib の各パッケージをインポートして起動します。Matplotlib パッケージは、モデルによって検出される最も異常な数字を視覚的に表示するために使用されます。PyTorch パッケージ全体をインポートする代わりに、必要なモジュールのみをインポートすることもできます。たとえば、import torch.optim as opt などです。

メモリへのデータの読み込み

生の MNIST データは、専用のバイナリ形式で保存されているため、そのまま操作するのはかなり困難です。それで、60,000 個のトレーニング項目から最初の 1,000 項目を抽出するユーティリティ プログラムを作成しました。このデータを、mnist_pytorch_1000.txt として Data サブディレクトリに保存しました。

作成されるデータは、次のようになります。

7 = 0 255 67 . . 123
2 = 113 28 0 . . 206
...
9 = 0 21 110 . . 254

各行が 1 つの数字を表しています。各行の先頭の値がその数字です。2 番目の値は、読みやすさのために任意で付けた等号です。その後に続く 28 x 28 = 784 個ある値は、0 から 255 の範囲のグレースケール ピクセル値です。すべての値は、単一の空白文字で区切られています。図 3 は、データ ファイル内のインデックス [30] にあるデータ項目で、典型的な数字の "3" です。

典型的な MNIST の数字
図 3 典型的な MNIST の数字

データセットは、次のステートメントでメモリに読み込まれます。

data_file = ".\\Data\\mnist_pytorch_1000.txt"
data_x = np.loadtxt(data_file, delimiter=" ",
  usecols=range(2,786), dtype=np.float32)
labels = np.loadtxt(data_file, delimiter=" ",
  usecols=[0], dtype=np.float32)
norm_x = data_x / 255

その数字 (ラベル) は列 0 にあり、784 ピクセル分の値が列 2 から 785 にあることにご注意ください。1,000 個のイメージすべてをメモリに読み込むと、スケーリングされたピクセル値が 0.0 から 1.0 の範囲にすべて収まるように各ピクセル値を 255 で除算して、正規化されたバージョンのデータが作成されます。

オートエンコーダー モデルの定義

このデモ プログラムは、784-100-50-100-784 のオートエンコーダーを定義します。入力層と出力層のノードの数値 (784) はデータによって決定されますが、隠し層の数と各層のノード数はハイパーパラメーターであり、試行錯誤によって決定する必要があります。

このデモ プログラムでは、プログラム定義されたクラスである Net を使用して、層のアーキテクチャおよびオートエンコーダーの入出力メカニズムを定義します。代わりに、次のようなシーケンス関数を使用して、オートエンコーダーを直接作成することもできます。

net = T.nn.Sequential(
  T.nn.Linear(784,100), T.nn.Tanh(),
  T.nn.Linear(100,50), T.nn.Tanh(),
  T.nn.Linear(50,100), T.nn.Tanh(),
  T.nn.Linear(100,784), T.nn.Tanh())

重みの初期化アルゴリズム (Glorot の一様分布)、隠し層活性化関数 (tanh)、出力層活性化関数 (tanh) は、ハイパーパラメーターです。この問題の入力値と出力値はすべて 0.0 から 1.0 の範囲にあるので、ロジスティック シグモイドは出力の活性化に関連して検証すべき有効な代替手段です。

オートエンコーダー モデルのトレーニングと評価

このデモ プログラムは、次のステートメントでトレーニングを準備します。

net = net.train()  # explicitly set
bat_size = 40
loss_func = T.nn.MSELoss()
optimizer = T.optim.Adam(net.parameters(), lr=0.01)
batcher = Batcher(num_items=len(norm_x),
  batch_size=bat_size, seed=1)
max_epochs = 100

このデモ オートエンコーダーはドロップアウトやバッチ正規化を使用しないので、ネットワークをトレーニング モードに明示的に設定する必要はありません。ただし、個人的な意見としては、そのようにするのがスタイルとして良いと思います。バッチ サイズ (40)、トレーニング最適化アルゴリズム (Adam)、最初の学習率 (0.01)、最大エポック数 (100) は、すべてハイパーパラメーターです。ニューラル機械学習になじみのない方は、"ニューラル ネットワークって、ハイパーパラメーターをたくさん使うんだな" とお感じになるかもしれません。そのとおりです。

プログラム定義されたバッチ処理支援プログラム オブジェクトは、1 回に 40 個のランダムなデータ項目のインデックスを作成し、最終的に 1,000 個の項目すべてを処理します (1 エポック)。別のアプローチとして、Dataset と DataLoader というビルトイン オブジェクトを torch.utils.data モジュールで使用する方法もあります。

トレーニング プロセスの構造は次のようになります。

for epoch in range(0, max_epochs):
  # print loss every 10 epochs
  for curr_bat in batcher:
    X = T.Tensor(norm_x[curr_bat])
    optimizer.zero_grad()
    oupt = net(X)
    loss_obj = loss_func(oupt, X)
    loss_obj.backward()
    optimizer.step()

項目の各バッチは Tensor コンストラクターを使用して作成されます。このコンストラクターは、torch.float32 を既定のデータ型として使用します。注目できるのは、loss_func 関数が計算された出力を入力と比較する点です。これには、入力値を予測できるようにネットワークをトレーニングする効果があります。

トレーニング後、このモデルを保存したいとお考えになると思いますが、その点についてはこの記事の範囲外です。PyTorch ドキュメントに、トレーニング済みモデルを保存する方法を示す良い例がいくつか説明されています。

オートエンコーダーを操作するときは、この例も含めて大抵の場合、モデル精度に固有の定義は存在しません。計算された出力値と、関連する入力値とがどれほど近似していれば正確な予測として数えてよいかは各自が決定しなければならず、その後で、自分の精度メトリックをコンピューティングするためのプログラム定義された関数を記述する必要があります。

オートエンコーダー モデルを使用して異常なデータを検出する

オートエンコーダー モデルのトレーニングが済んだら、正確に予測するのが困難なデータ項目、つまり、再構築しにくい項目を検出します。デモ コードは、1,000 個のデータ項目すべてをスキャンし、標準化された入力値と計算された出力値の 2 乗差を次のようにして計算します。

net = net.eval()  # not needed - no dropout
X = T.Tensor(norm_x)  # all input item as Tensors
Y = net(X)            # all outputs as Tensors
N = len(data_x)
max_se = 0.0; max_ix = 0
for i in range(N):
  curr_se = T.sum((X[i]-Y[i])*(X[i]-Y[i]))
  if curr_se.item() > max_se:
    max_se = curr_se.item()
    max_ix = i

最大 2 乗誤差 (max_se) が計算され、関連付けられたイメージのインデックス (max_ix) が保存されます。再構築誤差が最も大きい単一の項目を検出する代わりに、すべての 2 乗誤差を保存し、それらを並べ替えて、上位 n 項目を返すようにすることもできます。n の値は、研究している特定の問題に応じて決定します。

単一の最も異常なデータ項目が検出されると、プログラム定義された display 関数により、その項目が表示されます。

raw_data_x = data_x.astype(np.int)
raw_data_y = labels.astype(np.int)
print("Highest reconstruction error is index ", max_ix)
display(raw_data_x, raw_data_y, max_ix)

ピクセルとラベルの値は、float32 型から int 型に変換されます。これは主に主義の問題です。というのは、プログラム定義された display 関数内の Matplotlib imshow 関数は、どちらのデータ型も受け取ることができるからです。

まとめ

ディープ ニューラル オートエンコーダーを使用した異常検出は、この記事で説明したように、手法としてまだ十分に研究されていません。大半の標準的なクラスタリング手法と比較して、ニューラル オートエンコーダーを使用する大きな利点は、ニューラル手法は、数値以外のデータであっても、エンコードすることにより処理できることです。大半のクラスタリング手法は、ユークリッド距離などの数値計測に依存しているため、ソース データは厳密な数値である必要があります。

異常検出と関連があり、ほとんど研究がなされていない手法として、研究対象データセット用のオートエンコーダーの作成が挙げられます。ここでは、異常データを検出するために再構築誤差を使用する代わりに、K- 平均法などの標準アルゴリズムを使用してデータをクラスター化することができます。これは、一番深い隠れ層のノードが各データ項目を厳密に数値化した値を保持しているからです。クラスタリングすると、データ項目が非常に少ないクラスターや、クラスター内で最もクラスターの重心から離れているデータ項目を探すことができます。このアプローチは、単語が数値ベクターに変換され単語間の距離測定の計算に使用される "ニューラル単語埋め込み" と似た特徴があります。


Dr.James McCaffreyは、ワシントン州レドモンドの Microsoft Research に勤務しています。これまでに、Azure や Bing などの主要な Microsoft 製品のいくつかにも携わってきました。Dr.McCaffrey の連絡先は jammc@microsoft.com (英語のみ) です。

この記事のレビューに協力してくれたマイクロソフト技術スタッフのChris Lee 氏、Ricky Loynd 氏に心より感謝いたします。