※本コラムは、以前に個人ブログとして公開していた内容を、加筆・再構成のうえ掲載しております。技術的な内容は執筆当時のものであり、現在とは異なる場合がございます。
こんにちは。Anagraftの伊藤です。
機械学習では一般的に、大量の学習データを用意し、一括で読み込んでモデルを学習させます。この学習方法はバッチ学習と呼ばれます。一度にすべてのデータを処理するため、新しいデータの追加や大量データへの反応性において、柔軟に対応できない場合があります。
これに対しオンライン学習とは、流れてくるデータをリアルタイムに学習していく手法です。大量かつ頻繁に更新されるデータをすぐにモデルへ取り入れたい場合に有効な手段の一つです。オンライン機械学習とは、このような学習方法を用いる機械学習全体を指します。
今回はオンライン機械学習モデルをPythonで実装する例を紹介します。その中でも特に、Confidence Weighted Learning(CW)とSoft Confidence Weighted Learning(SCW)という2つの手法に焦点を当てます。なお、元記事の執筆は2017年で、コードの一部に現在では廃止されたAPI(pandasのas_matrix()など)が含まれていたため、本コラムのコード例は現行のライブラリで動くよう修正しています。
目次
オンライン機械学習は、流れてくるデータをリアルタイムに学習し予測するという特性を持ち、以下のようなメリットがあります。
1について、オンライン機械学習はデータがリアルタイムに到着する設定に適しています。新しいデータが到着するたびにモデルを更新するため、最新の情報を反映した予測が可能です。常に最新のデータに基づいて精度を更新し続けることが求められる状況、例えば金融市場の予測やリアルタイムの商品推薦などに有用です。
2について、一部のデータだけで学習を進められるため、データが膨大で一度に全てをメモリへ保持できない状況でも、全体を通じた学習結果を得られます。
3について、一度に一部のデータしか必要としないため、個人情報などセンシティブな情報を含むデータの取り扱いにも向きます。データは学習に用いられた後すぐに破棄でき、データ保管のリスクを軽減できます。
4について、すべてのデータを一度にメモリへ保管する必要がないため、性能の低い端末でも処理が可能です。リソース制約のある環境への適用に有利です。
一方で、オンライン機械学習にはデメリットもあります。データがランダムに到着しない場合や、データの順序が結果に影響を与える場合には、性能が下がる可能性があります。また、ノイズの多いデータや外れ値が含まれる場合、その影響を即座に学習してしまい、予測精度が低下することもあります。
これらの理由から、オンライン機械学習は、動的で大規模なデータや、機微なデータを扱う多くの現実的なアプリケーションに利点を提供しますが、適用の際はデータの性質と目的に応じて慎重に考えることが重要です。
今回実装するCW・SCWは、以下の論文で紹介されている手法です。
まずはCWの実装です。
CWのアルゴリズムのざっくりしたイメージは、線形分離をする際の各次元の重みが多次元正規分布に従うと考え、各重みに対応する分散をその重みの「信頼度」として扱う、というものです。分散が大きいほど、その重みについては自信がないと解釈します。
本実装では、論文に沿って共分散行列\( \Sigma \)(コード中のself.sigma)全体を保持・更新します。対角成分が各重みの分散(信頼度)を表し、分散が大きいほどその重みには自信がないと解釈します。非対角成分は、重みどうしの共分散を表します。
早速、Pythonによる実装が以下になります。ここではアヤメ(iris)のうちsetosaとversicolorの2品種だけを取り出し、二値分類(ラベルは{1, -1})として学習させてみます。実装自体は、実はそれほどコード量も多くなく作れてしまいます。
元記事ではDataFrameをNumPy配列に変換するのにdf.as_matrix()を使っていましたが、このメソッドはpandasから削除されています。現在はdf.valuesまたはdf.to_numpy()を使います。本コラムのコードは修正済みです。
import numpy as np
import pandas as pd
from sklearn import datasets
from scipy import stats
rng = np.random.default_rng(seed=0)
# irisデータセット
iris = datasets.load_iris()
data = pd.DataFrame(data=np.c_[iris["data"], iris["target"]], columns=iris["feature_names"] + ["target"])
selected_species = [0, 1] # setosa, versicolor
df = data[data["target"].isin(selected_species)] # setosa, versicolorのデータ
df = df.reindex(rng.permutation(df.index)) # シャッフル
data_x = df[iris["feature_names"]].to_numpy() # sepal/petalのlength・width
data_t = [1 if i == selected_species[0] else -1 for i in df["target"]] # ラベル{1, -1}
train_x, test_x = data_x[:80], data_x[80:]
train_t, test_t = data_t[:80], data_t[80:]
print("学習データ件数: ", len(train_x))
print("テストデータ件数: ", len(test_x))
# Confidence Weighted Learning クラス
class CW():
def __init__(self, in_size):
self.mu = np.zeros(in_size)
self.sigma = np.eye(in_size)
self.eta = 0.95
self.phi = stats.norm.ppf(self.eta)
self.psi = 1 + self.phi**2 / 2
self.xi = 1 + self.phi**2
def train(self, x, t):
m_t = t * self.mu.dot(x)
v_t = x.dot(self.sigma).dot(x)
alpha_t = max(0, (-m_t*self.psi + np.sqrt((m_t**2)*(self.phi**4)/4 + v_t*(self.phi**2)*self.xi)) / (v_t*self.xi))
u_t = ((-alpha_t*v_t*self.phi + np.sqrt((alpha_t**2)*(v_t**2)*(self.phi**2) + 4*v_t))**2) / 4
beta_t = (alpha_t*self.phi) / (np.sqrt(u_t) + v_t*alpha_t*self.phi)
self.mu = self.mu + alpha_t*t*self.sigma.dot(x)
self.sigma = self.sigma - beta_t*self.sigma.dot(x)[:, np.newaxis].dot(x.dot(self.sigma)[np.newaxis, :])
def predict(self, x):
return 1 if x.dot(self.mu) > 0 else -1
# 正解率を計算する関数
def get_accuracy(model, dataset_x, dataset_t):
result = []
for x, t in zip(dataset_x, dataset_t):
result.append(1 if model.predict(x)*t > 0 else 0) # 正解すれば1, 間違えれば0
return sum(result) / len(result)
# 定数
EPOCH_NUM = 1
# CWクラス
cw = CW(in_size=len(iris["feature_names"]))
# 学習
for epoch in range(EPOCH_NUM):
for x, t in zip(train_x, train_t):
cw.train(x, t)
accuracy1 = get_accuracy(cw, train_x, train_t) # 学習データの正解率
accuracy2 = get_accuracy(cw, test_x, test_t) # テストデータの正解率
print("train/accuracy: {}, test/accuracy: {}".format(accuracy1, accuracy2))
学習データ件数: 80
テストデータ件数: 20
train/accuracy: 1.0, test/accuracy: 1.0
アヤメのデータは簡単な問題なので、精度はあまり気にしなくてよいと思います。線形分離可能な問題ではかなり有効なアルゴリズムに思えます。いろいろ試すと分かりますが、かなり少ないサンプル数(最初の1〜2件の入力)でもすぐに収束する性質があります。それゆえ、バイアスのかかったデータや外れ値が入力された場合に、その一瞬だけ精度が大きく悪化する可能性がありそうです。
次にSCWを実装します。
CWには、線形分離が難しい問題に対してうまく学習できず、境界が大きくぶれてしまうという弱点があります。それを克服するため、いくらか耐性を持たせたモデルがSCWです。具体的には、ソフトマージンを導入することで誤分類されたインスタンスへのペナルティを緩和し、より頑健な学習を提供します。予測の不確実性をモデルに組み込むことで、データストリームの中で新しいパターンが出現しても迅速に対応できるようにすることを目指しています。
前述の論文では2つのやり方(Prop.1、Prop.2)が提案されていますので、両方の実装を紹介します。CWクラスとの違いはtrainメソッドの更新式(と、Cというパラメータの追加)だけで、その他は共通です。
# Soft Confidence Weighted Learning Prop.1 クラス
class SCW1():
def __init__(self, in_size):
self.mu = np.zeros(in_size)
self.sigma = np.eye(in_size)
self.eta = 0.95
self.C = 1
self.phi = stats.norm.ppf(self.eta)
self.psi = 1 + self.phi**2 / 2
self.xi = 1 + self.phi**2
def train(self, x, t):
m_t = t * self.mu.dot(x)
v_t = x.dot(self.sigma).dot(x)
alpha_t = min(self.C, max(0, (-m_t*self.psi + np.sqrt((m_t**2)*(self.phi**4)/4 + v_t*(self.phi**2)*self.xi)) / (v_t*self.xi)))
u_t = ((-alpha_t*v_t*self.phi + np.sqrt((alpha_t**2)*(v_t**2)*(self.phi**2) + 4*v_t))**2) / 4
beta_t = (alpha_t*self.phi) / (np.sqrt(u_t) + v_t*alpha_t*self.phi)
self.mu = self.mu + alpha_t*t*self.sigma.dot(x)
self.sigma = self.sigma - beta_t*self.sigma.dot(x)[:, np.newaxis].dot(x.dot(self.sigma)[np.newaxis, :])
def predict(self, x):
return 1 if x.dot(self.mu) > 0 else -1
# SCW1クラス
scw1 = SCW1(in_size=len(iris["feature_names"]))
# 学習
for epoch in range(EPOCH_NUM):
for x, t in zip(train_x, train_t):
scw1.train(x, t)
accuracy1 = get_accuracy(scw1, train_x, train_t)
accuracy2 = get_accuracy(scw1, test_x, test_t)
print("train/accuracy: {}, test/accuracy: {}".format(accuracy1, accuracy2))
train/accuracy: 1.0, test/accuracy: 1.0
CWとの違いは、alpha_tの計算でmin(self.C, ...)と上限を設けている点です。このCがソフトマージンの強さを調整するパラメータになります。
# Soft Confidence Weighted Learning Prop.2 クラス
class SCW2():
def __init__(self, in_size):
self.mu = np.zeros(in_size)
self.sigma = np.eye(in_size)
self.eta = 0.95
self.C = 1
self.phi = stats.norm.ppf(self.eta)
self.psi = 1 + self.phi**2 / 2
self.xi = 1 + self.phi**2
def train(self, x, t):
m_t = t * self.mu.dot(x)
v_t = x.dot(self.sigma).dot(x)
n_t = v_t + 1/(2*self.C)
gamma_t = self.phi * np.sqrt((self.phi**2)*(m_t**2)*(v_t**2) + 4*n_t*v_t*(n_t + v_t*(self.phi**2)))
alpha_t = max(0, (-(2*m_t*n_t + (self.phi**2)*m_t*v_t) + gamma_t) / (2*(n_t**2 + n_t*v_t*(self.phi**2))))
u_t = ((-alpha_t*v_t*self.phi + np.sqrt((alpha_t**2)*(v_t**2)*(self.phi**2) + 4*v_t))**2) / 4
beta_t = (alpha_t*self.phi) / (np.sqrt(u_t) + v_t*alpha_t*self.phi)
self.mu = self.mu + alpha_t*t*self.sigma.dot(x)
self.sigma = self.sigma - beta_t*self.sigma.dot(x)[:, np.newaxis].dot(x.dot(self.sigma)[np.newaxis, :])
def predict(self, x):
return 1 if x.dot(self.mu) > 0 else -1
# SCW2クラス
scw2 = SCW2(in_size=len(iris["feature_names"]))
# 学習
for epoch in range(EPOCH_NUM):
for x, t in zip(train_x, train_t):
scw2.train(x, t)
accuracy1 = get_accuracy(scw2, train_x, train_t)
accuracy2 = get_accuracy(scw2, test_x, test_t)
print("train/accuracy: {}, test/accuracy: {}".format(accuracy1, accuracy2))
train/accuracy: 1.0, test/accuracy: 1.0
元記事を執筆した2017年当時、オンライン機械学習はやや専門的な話題でしたが、その後、データがストリームで流れ続ける環境(IoT、Webログ、決済など)が一般化し、実務での重要性が高まっています。押さえておきたいポイントを整理します。
今回はオンライン機械学習について解説し、CW・SCWのPythonによる実装を紹介しました。
重みを正規分布で表現するという考え方は面白いですね。そのためモデル自体はかなり軽量で、とてもクイックに学習できます。
これらの手法はリアルタイムのデータストリームを扱うための強力なツールであり、各データ点の到着に応じてモデルをリアルタイムに更新します。ただし、モデリング手法を選ぶ際には、その手法が問題の性質やデータの特徴に適しているかどうかを常に考慮することが重要です。特に実務では、モデルはリッチであればリッチであるほど良いという単純なものではなく、精度・計算リソース・処理時間などのバランスをケースバイケースで考慮しながらアプローチしなければならない場面が多くあります。オンライン機械学習もあくまで一つの選択肢であり、その利点と制約を理解したうえで、適切なツールを選択するよう心掛けることが重要です。