2つの機械学習データセット間の類似性の計算-VisualStudio Magazine

Machine Learning

データサイエンスラボ

2つの機械学習データセット間の類似性を計算します

MicrosoftResearchのDr.James McCaffreyは、一見、2つのデータセット間の類似性/距離を計算するのは簡単に聞こえますが、実際には問題は非常に難しいと説明しました。

多くの機械学習とデータサイエンスのシナリオでは、かなり一般的なサブ問題は、2つのデータセット間の類似性(または差異または距離)を計算する必要があることです。たとえば、多数のトレーニングデータセットからサンプルを選択する場合、サンプルデータセットがソースデータセットとどの程度類似しているかを知りたい場合があります。または、非常に深いニューラルネットワークのトレーニングを準備する場合は、新しいデータセットに最も類似したデータセットを使用してトレーニングされた既存のモデルを見つける必要があります。

一見、2つのデータセット間の類似性/距離を計算するのは簡単に聞こえますが、実際にはこの問題は非常に困難です。データセット間で個々の行を比較しようとすると、すぐに組み合わせ爆発の問題が発生します。比較が多すぎます。さまざまなデータセットサイズの処理と非数値データの処理に関連する問題もあります。最も重要なことは、ほとんどの場合、2つのデータセット間の距離を計算する簡単な方法がないことです。

この記事では、神経統計と古典的な統計手法を組み合わせて、任意の2つのデータセット間の距離を計算する方法について説明します。この記事の内容を理解する良い方法は、デモプログラムのスクリーンショットを表示することです。 図1

図1:UCIDigitsデータセット距離のデモンストレーションの実行
[Click on image for larger view.] 図1: UCIデジタルデータセット距離デモンストレーション実行

このデモンストレーションの目的は、データセットP(UCI Digitsデータセットから100行)とデータセットQ(Pデータセットと同じですが、データ行の50%がランダム化されている)の間の距離を計算することです。 2つのデータセット間の計算された距離は1.6625です。データセットの距離値が大きいほど、差は大きくなります。

PおよびQデータセットのデータの各行は、8×8の手書きの数値を表します。各行には65の数字があります。最初の64の数値は、0から16までのグレースケールピクセル値です。最後の番号は、0から9までの関連する番号ラベルです。デモンストレーションでは、PデータとQデータをメモリにロードしてから、Pデータセットを使用して65-32-4-32-65ディープニューラルオートエンコーダをトレーニングします。その結果、オートエンコーダーが作成され、各画像(およびそのラベル)が4つの値のベクトルに変換されます。各値は0.0から1.0までの数値です。

次に、デモンストレーションでは、PとQの各画像アイテムを取得し、オートエンコーダーを使用してそれらを4値のベクトルに変換します。次に、2つのデータセットの4つのコンポーネントのそれぞれが10個の頻度バケットに分割されます。最後のステップは、度数分布の4つのペア間の4つの距離、つまり(2.18、1.06、2.68、0.73)を計算することです。 4つの距離の平均は、PとQ1.6625の間の最終的な距離を示します。このプロセスは、思ったよりもはるかに簡単です。

この記事は、Cシリーズプログラミング言語(できればPython)に中級以上の知識があり、PyTorchコードベースに基本的な知識があることを前提としています。デモプログラムの完全なソースコードはこの記事で提供されており、完全なコードは添付のダウンロードにもあります。デモプログラムで使用されている2つのデータファイルのデータは、ソースコードダウンロードファイルの最後にコメントとして埋め込まれています。

デモプログラムを実行するには、PythonとPyTorchがマシンにインストールされている必要があります。デモプログラムは、Windows 10で、Anaconda 2020.02 64ビットディストリビューション(Python 3.7.6を含む)とpip経由でインストールされたCPU用のPyTorchバージョン1.8.0を使用して開発されました。インストールは簡単ではありません。詳細なインストール手順については、私のブログ投稿をご覧ください。

セルフエンコーダコンポーネント
デモプログラムの自動エンコーダはで定義されています リスト1アーキテクチャは65-32-4-32-65です。入力には65個のノードがあり、UCIDigitsデータセットの入力ごとに1つずつあります。潜在次元はハイパーパラメータであり、4に設定されています。潜在次元は、データ項目を表す抽象属性の数と考えることができます。例えば、数の各画像は、「真円度」、「鋭さ」、「厚さ」、および「横方向性」によって表すことができる。

リスト1: セルフエンコーダの定義

import numpy as np
import torch as T
device = T.device("cpu")

class Autoencoder(T.nn.Module):  # 65-32-4-32-65
  def __init__(self):
    super(Autoencoder, self).__init__()  
    self.layer1 = T.nn.Linear(65, 32)  # includes labels
    self.layer2 = T.nn.Linear(32, 4)
    self.layer3 = T.nn.Linear(4, 32)
    self.layer4 = T.nn.Linear(32, 65)
    self.latent_dim = 4

  def encode(self, x):       
    z = T.tanh(self.layer1(x))
    z = T.sigmoid(self.layer2(z))
    return z 

  def decode(self, x):
    z = T.tanh(self.layer3(x))
    z = T.sigmoid(self.layer4(z))
    return z 

  def forward(self, x):
    z = self.encode(x)
    oupt = self.decode(z)
    return oupt

encode()メソッドは、すべての値が範囲内にあるように、潜在層にシグモイドアクティベーションを適用します [0.0, 1.0]これにより、度数分布を簡単に計算できます。 decode()メソッドは、出力層でシグモイドアクティベーションも使用します。これは、入力データを範囲に標準化する必要があることを意味します [0.0, 1.0] このようにして、入力を出力と直接比較できます。したがって、すべて0〜16の64個の入力ピクセル値を16で除算する必要があります。0〜9のクラスラベルは9で除算する必要があります。

プログラムによって定義されたtrain()関数は リスト2train()関数は、PyTorchデータセットオブジェクトを受け入れます。 train()関数はAdam最適化を使用し、ハードコードされた学習率を0.001に設定します。特定の問題シナリオに適した値を見つけるための実験を容易にするために、学習率をパラメーター化することをお勧めします。

リスト2: オートエンコーダのトレーニング

def train(ae, ds, bs, me, le):
  # train autoencoder ae with dataset ds using batch 
  # size bs, with max epochs me, log_every le
  data_ldr = T.utils.data.DataLoader(ds, 
    batch_size=bs, shuffle=True)
  
  loss_func = T.nn.MSELoss() 
  opt = T.optim.Adam(ae.parameters(), lr=0.001)
  print("Starting training")
  for epoch in range(0, me):
    for (b_idx, batch) in enumerate(data_ldr):
      opt.zero_grad()
      X = batch
      oupt = ae(X)
      loss_val = loss_func(oupt, X)  # note X not Y
      loss_val.backward()
      opt.step()

    if epoch > 0 and epoch % le == 0:
      print("epoch = %6d" % epoch, end="")
      print("  curr batch loss = %7.4f" % 
        loss_val.item(), end="")
      print("")
  print("Training complete ")

データセットはで定義されています リスト364ピクセル値とクラスラベルはプログラムで次のように標準化されていることに注意してください [0.0, 1.0] 範囲。データに可能な値(赤、青、緑)の色などのカテゴリ変数がある場合は、ワンホットエンコーディングを使用できます:赤=(1、0、0)、青=(0、1、0) 、緑=(0、0、1)。ワンホットエンコーディングを使用することで、非デジタルデータと正規化の両方の問題を同時に解決できます。データにブールデータがある場合は、マイナス1プラス1ではなく、0-1エンコードでエンコードする必要があります。

リスト3: データセットの定義

class Digits_Dataset(T.utils.data.Dataset):
  # for an Autoencoder (not a classifier)
  # assumes data is:
  # 64 pixel values (0-16) (comma) label (0-9)
  # [0] [1] . . [63] [64] 

  def __init__(self, src_file):
    all_xy = np.loadtxt(src_file, usecols=range(65),
      delimiter=",", comments="https://visualstudiomagazine.com/articles/2021/09/20/#", dtype=np.float32)
    self.xy_data = T.tensor(all_xy, dtype=T.float32).to(device) 
    self.xy_data[:, 0:64] /= 16.0  # normalize pixels
    self.xy_data[:, 64] /= 9.0     # normalize labels

  def __len__(self):
    return len(self.xy_data)

  def __getitem__(self, idx):
    xy = self.xy_data[idx]
    return xy

データを度数分布に変換する
オートエンコーダコンポーネントは、各データ項目を4つの潜在的な値のベクトルに変換します。 [0.54, 0.93, 0.11, 0.63].4つの潜在的な値のそれぞれが度数分布を生成します。この作業は、プログラム定義関数make_freq_mat()および補助value_to_bin()関数によって実行されます。これらの2つの機能は リスト4

リスト4: 度数分布の関数を作成します

def value_to_bin(x):
  # x is in [0.0, 1.0]
  if x >= 0.0 and x < 0.1:   return 0
  elif x >= 0.1 and x < 0.2: return 1
  elif x >= 0.2 and x < 0.3: return 2
  elif x >= 0.3 and x < 0.4: return 3
  elif x >= 0.4 and x < 0.5: return 4
  elif x >= 0.5 and x < 0.6: return 5
  elif x >= 0.6 and x < 0.7: return 6
  elif x >= 0.7 and x < 0.8: return 7
  elif x >= 0.8 and x < 0.9: return 8
  else:                      return 9 

# -----------------------------------------

def make_freq_mat(ae, ds):
  d = ae.latent_dim
  result = np.zeros((d,10), dtype=np.int64)
  n = len(ds)
  for i in range(n):
    x = ds[i]
    with T.no_grad():
      latents = ae.encode(x)  
    latents = latents.numpy() 
    for j in range(d):
      bin = value_to_bin(latents[j])
      result[j][bin] += 1
  result = (result * 1.0) / n
  return result

コードは単純ですが、アイデアは少し注意が必要です。 (デモの4つではなく)2つの潜在的なディメンションのみがあり、(100ではなく)8つのデータ項目しかないとします。各データ項目は、2つの値を持つベクトルを提供します。それらが次のようになっているとします。

(0.23, 0.67)
(0.93, 0.55)
(0.11, 0.38)
(0.28, 0.72)
(0.53, 0.83)
(0.39, 0.21)
(0.84, 0.79)
(0.22, 0.61)

最初の度数分布は、最初の値(0.23、0.93、0.11、0.28、0.53、0.39、0.84、0.22)のみを調べます。ビンが10個ある場合、ビンは [0.0 to 0.1), [0.1 to 0.2), . . . [0.9 to 1.0]最初の潜在的に薄暗いビンカウントは次のとおりです:(0、1、3、1、0、1、0、0、1、1)。 8項目あるため、度数分布は(0.000、0.125、0.375、0.125、0.000、0.125、0.000、0.000、0.125、0.125)になります。

同様に、2番目の潜在的に薄暗いカウントは(0、0、1、1、0、1、2、2、1、0)であり、度数分布は(0.000、0.000、0.125、0.125、0.000、0.125、0.250、0.250)です。 、0.125、0.000)。

度数分布を比較する
2つの度数分布間の距離を計算する方法はいくつかあります。一般的な距離関数には、カルバック・ライブラー発散、イェンセン・シャノン距離、ヘリンガー距離が含まれます。デモプログラムは、情報伝達距離と呼ばれる非常に単純化されたバージョンのワッサースタイン距離関数を使用します。つまり、情報伝送距離は、ある度数分布を別の度数分布に変換するために必要な最小の作業量です。

距離測定は、プログラム定義関数info_transfer_distance()および2つの補助関数move_info()およびfirst_nonzero()として実装されます。で実現 リスト5

リスト5: 情報伝達距離機能

def first_nonzero(vec):
  dim = len(vec)
  for i in range(dim):
    if vec[i] > 0.0:
      return i
  return -1  # no empty cells found

def move_info(src, si, dest, di):
  # move as much src at [si] as possible to dest[di]
  if src[si] <= dest[di]:     # move all in src
    mass = src[si]
    src[si] = 0.0             # all src got moved
    dest[di] -= mass          # less to fill now
  elif src[si] > dest[di]:    # use just part of src
    mass = dest[di]           # fill remainder of dest
    src[si] -= mass           # less info left in src
    dest[di] = 0.0            # dest is filled
  dist = np.abs(si - di)
  return mass * dist          # weighted info transfer

def info_transfer_distance(p, q):
  # distance between two p-dists p, q
  # highly simplified Wasserstein distance
  source = np.copy(p) 
  destination = np.copy(q)
  tot_info = 0.0

  while True:  # TODO: add sanity counter check
    from_idx = first_nonzero(source)
    to_idx = first_nonzero(destination)
    if from_idx == -1 or to_idx == -1:
      break
    info = move_info(source, from_idx, 
      destination, to_idx)
    tot_info += info
  return tot_info

info_transfer_distance()はデータに関係なく変更されないため、ブラックボックスと考えることができます。 2つの度数分布PとQが同じである場合、info_transfer_distance(P、Q)= 0です。 PとQの差が大きいほど、関数の値は大きくなります。頻度が10個のバケットにビニングされている場合、情報転送距離関数の最大値は9.0です。

一緒に置く
デモプログラムは、最初に、比較する2つのデータセットを保存するためのファイル名を指定します。

def main():
  # 0. get started
  print("Begin UCI Digits dataset distance demo ")
  T.manual_seed(1)
  np.random.seed(1)
  p_file = ".\Data\uci_digits_train_100.txt"
  q_file = ".\Data\digits_100_noise_50.txt"
. . .

Pファイルには、UCIDigitsデータセットのランダムに選択された100行が含まれています。 QファイルはPと同じですが、行の50%がランダムな値に置き換えられます。64ピクセルは0〜16で、カテゴリラベルは0〜9です。

次に、2つのデータセットをメモリにロードします。

  print("P file = " + str(p_file))
  print("Q file = " + str(q_file))
  print("First five lines of Q: ")
  show_uci_file(q_file, 5)

  # 1. create Dataset objects
  print("Loading P and Q Datasets into memory ")
  p_ds = Digits_Dataset(p_file)
  q_ds = Digits_Dataset(q_file)

show_uci_file()はプログラムによって定義され、データセットの類似性を実際に計算する必要はありません。単純なprint()ステートメントは正常に機能します。次に、P参照データセットでオートエンコーダーを作成してトレーニングします。

  # 2. create and train autoencoder model using parent 
  print("Creating 65-32-4-32-65 autoencoder using P ")
  autoenc = Autoencoder()   # 65-32-4-32-65
  autoenc.train()           # set mode
  bat_size = 10
  max_epochs = 100
  log_every = 20
  train(autoenc, p_ds, bat_size, max_epochs,
    log_every)

Leave a Reply

Your email address will not be published. Required fields are marked *