非線形がなんだ! ロジスティック回帰+多項式でやってやる!

はじめに

 ロジスティック回帰はいうまでもなく線形分類器です。なので、非線形の分類問題は本来解けません。

 しかし、特徴量を非線形変換したり、交互作用項を入れたりして使えば、非線形の分類問題にも十分使えます。
 どれくらいの威力があるのでしょうか? やってみましょう。

準備

 便利なmain関数を作っておきましょう。
def main(X, y, model, figname):
    model.fit(X, y)
    
    cm_bright = ListedColormap(['#FF0000', '#0000FF'])
    plt.scatter(X[:, 0], X[:, 1], c=y, 
                cmap=cm_bright, edgecolors='k')

    x_min, x_max = X[:, 0].min() - .5, X[:, 0].max() + .5
    y_min, y_max = X[:, 1].min() - .5, X[:, 1].max() + .5
    h = 0.1
    xx, yy = np.meshgrid(np.arange(x_min, x_max, h),
                         np.arange(y_min, y_max, h))
    Z = model.predict_proba(np.c_[xx.ravel(), yy.ravel()])[:,1]
    Z = Z.reshape(xx.shape)

    cm = plt.cm.RdBu
    plt.contourf(xx, yy, Z, cmap=cm, alpha=.2)
    plt.savefig(figname)
 一応下でもコード全体を示しながら説明しますが、基本的にはこの中身は変えないでやっていく予定です。

多項式は面白い

 さて、データの非線形変換といえば、多項式変換でしょう。sklearnではPolynomialFeaturesが使えます。
 こいつは面白くて、ドキュメントにはこういう記述があります。
For example, if an input sample is two dimensional and of the form [a, b], the degree-2 polynomial features are [1, a, b, a^2, ab, b^2]
 abも入るんだ。知らなかった。いわゆる交互作用項ですね。
 詳しい使い方の記事はこちらです。

moons

 三日月型のグループが2つあるようなデータがmoonsです。moonsくらいならすぐできます。
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap
from sklearn.datasets import make_moons
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import Pipeline

def main(X, y, model, figname):
    model.fit(X, y)
    
    cm_bright = ListedColormap(['#FF0000', '#0000FF'])
    plt.scatter(X[:, 0], X[:, 1], c=y, 
                cmap=cm_bright, edgecolors='k')

    x_min, x_max = X[:, 0].min() - .5, X[:, 0].max() + .5
    y_min, y_max = X[:, 1].min() - .5, X[:, 1].max() + .5
    h = 0.1
    xx, yy = np.meshgrid(np.arange(x_min, x_max, h),
                         np.arange(y_min, y_max, h))
    Z = model.predict_proba(np.c_[xx.ravel(), yy.ravel()])[:,1]
    Z = Z.reshape(xx.shape)

    cm = plt.cm.RdBu
    plt.contourf(xx, yy, Z, cmap=cm, alpha=.2)
    plt.savefig(figname)

if __name__ == "__main__":
    X, y = make_moons(noise=0.3, random_state=0)

    pf = PolynomialFeatures(degree=4, include_bias=False)
    lr = LogisticRegression(solver="lbfgs")
    model = Pipeline([("pf", pf), ("lr", lr)])    

    main(X, y, model, "fig1.png")
fig1.png
 4次までです。なんとなく怪しいし、よく見ると対称になっているべき部分がぜんぜん対称じゃないなど微妙な部分もあるのですが、それでもできます。

XOR

 2次ですでにabが入るので、できて当然。
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import Pipeline

def main(X, y, model, figname):
    model.fit(X, y)
    
    cm_bright = ListedColormap(['#FF0000', '#0000FF'])
    plt.scatter(X[:, 0], X[:, 1], c=y, 
                cmap=cm_bright, edgecolors='k')

    x_min, x_max = X[:, 0].min() - .5, X[:, 0].max() + .5
    y_min, y_max = X[:, 1].min() - .5, X[:, 1].max() + .5
    h = 0.1
    xx, yy = np.meshgrid(np.arange(x_min, x_max, h),
                         np.arange(y_min, y_max, h))
    Z = model.predict_proba(np.c_[xx.ravel(), yy.ravel()])[:,1]
    Z = Z.reshape(xx.shape)

    cm = plt.cm.RdBu
    plt.contourf(xx, yy, Z, cmap=cm, alpha=.2)
    plt.savefig(figname)

def make_xor():
    np.random.seed(0)
    x = np.random.uniform(-1, 1, 300)
    y = np.random.uniform(-1, 1, 300)
    target = np.logical_xor(x > 0, y > 0)
    return np.c_[x, y], target

if __name__ == "__main__":
    X, y = make_xor()

    pf = PolynomialFeatures(degree=5, include_bias=False)
    lr = LogisticRegression(solver="lbfgs")
    model = Pipeline([("pf", pf), ("lr", lr)])    

    main(X, y, model, "fig2.png")
fig2.png
 悪くなさそう。しれっと5次でやっているのは、この方が表現力が若干高いような気がするからです(厳密に検証していません。実用的には2次の方が良いかも。次数が少ないに越したことはないので)。

circles

 円形のデータも実はきれいに分類できます。しかもたった2次で。
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap
from sklearn.datasets import make_circles
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import Pipeline

def main(X, y, model, figname):
    model.fit(X, y)
    
    cm_bright = ListedColormap(['#FF0000', '#0000FF'])
    plt.scatter(X[:, 0], X[:, 1], c=y, 
                cmap=cm_bright, edgecolors='k')

    x_min, x_max = X[:, 0].min() - .5, X[:, 0].max() + .5
    y_min, y_max = X[:, 1].min() - .5, X[:, 1].max() + .5
    h = 0.1
    xx, yy = np.meshgrid(np.arange(x_min, x_max, h),
                         np.arange(y_min, y_max, h))
    Z = model.predict_proba(np.c_[xx.ravel(), yy.ravel()])[:,1]
    Z = Z.reshape(xx.shape)

    cm = plt.cm.RdBu
    plt.contourf(xx, yy, Z, cmap=cm, alpha=.2)
    plt.savefig(figname)

if __name__ == "__main__":
    X, y = make_circles(noise=0.2, factor=0.5, random_state=1)

    pf = PolynomialFeatures(degree=2, include_bias=False)
    lr = LogisticRegression(solver="lbfgs")
    model = Pipeline([("pf", pf), ("lr", lr)])    

    main(X, y, model, "fig3.png")
fig3.png
 これについてはにわかには信じがたかったので、決定木で可視化してみました。
from sklearn.datasets import make_circles
from sklearn.preprocessing import PolynomialFeatures
from sklearn.tree import DecisionTreeClassifier
from dtreeviz.trees import dtreeviz

def main():
    X, y = make_circles(noise=0.2, factor=0.5, random_state=1)
    pf = PolynomialFeatures(degree=2, include_bias=False)
    X_pf = pf.fit_transform(X)
    feature_names = ["x", "y", "x^2", "xy", "y^2"]

    dtc = DecisionTreeClassifier(max_depth=4)
    dtc.fit(X_pf, y)

    viz = dtreeviz(dtc, X_train=X_pf, y_train=y, 
                   feature_names=feature_names, target_name="",
                   class_names=["outer", "center"])
    viz.save("dtc.svg")

if __name__ == "__main__":
    main()

# rsvg-convert dtc.svg --format=png --output=dtc.png

dtc.png
 xとyの二乗が大きければ外側、小さければ内側って感じ。まあ、そりゃそうか。
 追記:
 これは二次判別分析をやったのとだいたい同じことになるんだっけ……と思って英語版wikiを見たら、そんな感じの記述があったのでたぶんそういうこと。
 違う解釈としては、円の方程式を思い出してください。半径一定の円で切ることができるので、二次式で十分です。

まとめ

 このように、ロジスティック回帰でもデータを多項式変換することで非線形の分類問題を解くことができます。
 sklearnだと、PolynomialFeaturesは気楽に使えていいですね。ただ、次数が増えると急速に変数の数が増えていくので、無思慮には使えませんが。実用でいけるかどうかは、特徴量の大きさが現実的な範囲に収まるかどうかにかかっています。3次以上は実際問題としては厳しいので、それなりに気を使う必要があります。
(経験的には、元データが高次元であればあるほど分類器の非線形への適応度合いは問われなくなるような気がするので、なんとかなる可能性が高いです。)
 低次元(<100くらい)の非線形問題であれば、だいたいこれ+線形分類器で解けそうな雰囲気があります。