Hayami Kaito Web
~/tech

深層学習以前の機械学習:線形回帰・ロジスティック回帰・決定木・SVM

published

このサイトの機械学習の記事は、ここまでニューラルネットワーク(深層学習)を中心に説明してきました。しかし、機械学習には深層学習より前から使われてきた手法がたくさんあり、今でも現役です。とくに、売上・顧客の情報・センサーの値のような 表の形のデータでは、決定木を組み合わせた手法がニューラルネットワークより良い成績を出すことが多く、データ分析のコンペティションでもよく使われています。

この記事では、代表的な手法を NumPy だけで実装しながら、それぞれの考え方と得意なことを説明します。損失関数・勾配降下法は ニューラルネットワークの学習のしくみ、過学習と正則化は 過学習と正則化のしくみ で説明したので、ここでは手法ごとの違いに絞ります。

結論を先に書くと、手法は次のように整理できます。

手法考え方得意なこと
線形回帰・ロジスティック回帰特徴に重みを掛けて足す単純で速く、重みから「どの特徴が効いたか」が読める
決定木「特徴 < しきい値」の質問をくり返して分ける結果が人に説明しやすい。特徴の大きさをそろえなくてよい
ランダムフォレスト・勾配ブースティングたくさんの決定木を組み合わせる表の形のデータでとても強い
k 近傍法近くの学習用のデータの多数決学習がいらない。考え方がいちばん単純
SVM2 つのクラスの間の余白(マージン)を最大にするデータが少なくても安定。カーネル法で曲がった境界も作れる

説明の途中に出てくる計算例は、Python と NumPy で実際に動かした結果です。

環境

計算例を動かした環境です。

項目バージョン
Python3.13
NumPy2.5

線形回帰:直線を当てはめる

最小二乗法

線形回帰は、数値を予測する(回帰する)いちばん基本の手法です。特徴に重みを掛けて足し、切片を加えたもので予測します。

予測 = w₀ + w₁ × 特徴₁ + w₂ × 特徴₂ + …

重みは、予測と正解の差の 2 乗の和がいちばん小さくなるように決めます(最小二乗法)。線形回帰では、この重みを勾配降下法でくり返し探さなくても、正規方程式 XᵀX w = Xᵀy を解いて一度に求められます。

部屋の広さから家賃を予測する、架空の 7 件のデータで計算してみます。

import numpy as np

# 部屋の広さ(m²)と家賃(万円)
area = np.array([20, 25, 30, 35, 40, 50, 60])
rent = np.array([6.5, 7.2, 8.1, 8.8, 9.9, 11.8, 13.5])

X = np.column_stack([np.ones_like(area, dtype=float), area])  # 切片のための 1 の列を足す
w = np.linalg.solve(X.T @ X, X.T @ rent)                       # 正規方程式:XᵀX w = Xᵀy
pred = X @ w
r2 = 1 - ((rent - pred) ** 2).sum() / ((rent - rent.mean()) ** 2).sum()
print(np.round(w, 3), round(float(r2), 4))
print(round(float(w[0] + w[1] * 45), 2))  # 45 m² の部屋の予測

実行結果:

[2.768 0.179] 0.9977
10.8

切片が 2.768 万円、1 m² あたり 0.179 万円(1,790 円)という直線が求まりました。2 つ目の数の 0.9977 は 決定係数(R²)で、「家賃のばらつきのうち、直線で説明できた割合」です。1 に近いほどよく当てはまっています。45 m² の部屋なら、家賃は 10.8 万円と予測されます。

重みの値をそのまま「広さが 1 m² 増えると家賃が 1,790 円上がる」と読めるのが、線形回帰の大きな長所です。

ロジスティック回帰:確率を出して分類する

シグモイドで 0〜1 に押し込める

合格か不合格か、のように クラスを予測する(分類する)ときは、線形回帰の出力をシグモイド関数で 0〜1 の確率に変えます。これが ロジスティック回帰です。名前に「回帰」と付いていますが、分類の手法です。

合格の確率 = sigmoid(w × 特徴 + b) = 1 / (1 + e^−(w × 特徴 + b))

正規方程式のような一度に解ける式はないので、交差エントロピー誤差を勾配降下法で小さくして重みを決めます。勉強した時間と、試験に合格したかの架空のデータで計算してみます。

import numpy as np

# 勉強した時間(時間)と、試験に合格したか(1: 合格、0: 不合格)
hours = np.array([0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0])
passed = np.array([0, 0, 0, 1, 0, 1, 0, 1, 1, 1])

def sigmoid(z):
    return 1 / (1 + np.exp(-z))

w, b = 0.0, 0.0
for _ in range(20000):
    p = sigmoid(w * hours + b)
    w -= 0.1 * ((p - passed) * hours).mean()  # 交差エントロピー誤差の勾配
    b -= 0.1 * (p - passed).mean()
print(round(w, 3), round(b, 3), round(-b / w, 2))
print(np.round(sigmoid(w * np.array([1.0, 2.75, 4.0]) + b), 3))

実行結果:

1.353 -3.722 2.75
[0.086 0.5   0.844]

1 行目は、重み・切片と、合格の確率がちょうど 0.5 になる勉強の時間(−b ÷ w)です。2.75 時間より多く勉強すると「合格」と予測する境目になります。2 行目は、1 時間・2.75 時間・4 時間のときの合格の確率で、8.6%・50%・84.4% です。

ロジスティック回帰は、誤差逆伝播法の記事 の XOR のネットワークから隠れ層をなくしたものと同じ形で、境界は直線(特徴が多ければ平面)になります。

決定木:質問をくり返して分ける

ジニ不純度で分け方を選ぶ

決定木は、「気温は 23℃ より低いか」のような質問をくり返してデータを分けていき、最後に行き着いた場所(葉)で答えを決める手法です。質問の流れを木の形の図にして、そのまま人に見せられるのが長所です。

質問(どの特徴を、どのしきい値で分けるか)は、分けたあとのそれぞれのグループが、なるべく 1 つのクラスだけになるように選びます。グループの混ざり具合は ジニ不純度で測ります。

ジニ不純度 = 1 − Σ(そのグループの中での各クラスの割合)²

1 つのクラスだけなら 0、2 つのクラスが半々なら 0.5 です。気温とアイスが売れたかの架空のデータで、しきい値ごとの不純度を計算してみます。

import numpy as np

def gini(labels):
    """ジニ不純度:1 − Σ(クラスの割合)²。1 種類だけなら 0"""
    if len(labels) == 0:
        return 0.0
    _, counts = np.unique(labels, return_counts=True)
    p = counts / counts.sum()
    return float(1 - (p**2).sum())

# 気温(℃)と、アイスが売れたか(1: 売れた、0: 売れなかった)
temp = np.array([12, 15, 18, 20, 22, 24, 26, 28, 30, 33])
sold = np.array([0, 0, 0, 1, 0, 1, 1, 1, 1, 1])
print(round(gini(sold), 3))

for t in [16.5, 19, 21, 23]:
    left, right = sold[temp < t], sold[temp >= t]
    weighted = (len(left) * gini(left) + len(right) * gini(right)) / len(sold)  # 分けたあとの不純度の平均
    print(t, round(gini(left), 3), round(gini(right), 3), round(weighted, 3))

実行結果:

0.48
16.5 0.0 0.375 0.3
19 0.0 0.245 0.171
21 0.375 0.278 0.317
23 0.32 0.0 0.16

分ける前の不純度は 0.48 です。2 行目からは、しきい値、それより低いグループと高いグループの不純度、件数で重み付けした平均です。23℃ で分けると、23℃ 以上の 5 件はすべて「売れた」で不純度 0 になり、平均は 0.16 といちばん小さくなります。決定木は、このようにすべての特徴・すべてのしきい値を試して、いちばん不純度が下がる質問を選び、分けたグループの中でまた同じことをくり返します。

深い木は過学習する

2 つの特徴(平面上の点の座標)から、円の内側(1)か外側(0)かを当てる問題で、木の深さを変えてみます。現実のデータに近づけるため、ラベルの 15% をわざと入れ替えてノイズにしています。そのため、どんなに良いモデルでも、テスト用のデータでの正解率は 85% 程度が上限です。

import numpy as np

def gini(y):
    p = np.bincount(y, minlength=2) / len(y)
    return 1 - (p**2).sum()

def build_tree(X, y, depth, max_depth, n_features, rng):
    """ジニ不純度がいちばん下がる「特徴 < しきい値」で 2 つに分けることをくり返す"""
    if depth == max_depth or gini(y) == 0 or len(y) < 2:
        return int(np.bincount(y, minlength=2).argmax())  # 葉:多いほうのクラス
    best = None
    features = rng.choice(X.shape[1], n_features, replace=False)  # 調べる特徴(森では一部だけ)
    for f in features:
        for t in np.unique(X[:, f])[1:]:
            left = X[:, f] < t
            score = (left.sum() * gini(y[left]) + (~left).sum() * gini(y[~left])) / len(y)
            if best is None or score < best[0]:
                best = (score, f, t)
    if best is None:
        return int(np.bincount(y, minlength=2).argmax())
    _, f, t = best
    left = X[:, f] < t
    return (f, t, build_tree(X[left], y[left], depth + 1, max_depth, n_features, rng),
            build_tree(X[~left], y[~left], depth + 1, max_depth, n_features, rng))

def predict_tree(node, x):
    while isinstance(node, tuple):
        f, t, left, right = node
        node = left if x[f] < t else right
    return node

def predict(tree, X):
    return np.array([predict_tree(tree, x) for x in X])

# 円の内側が 1、外側が 0。ラベルの 15% をわざと入れ替えてノイズにする
def make_data(n, rng):
    X = rng.uniform(-1, 1, (n, 2))
    y = (X[:, 0] ** 2 + X[:, 1] ** 2 < 0.5).astype(int)
    flip = rng.random(n) < 0.15
    return X, np.where(flip, 1 - y, y)

rng = np.random.default_rng(0)
X_tr, y_tr = make_data(300, rng)
X_te, y_te = make_data(2000, rng)

for depth in [1, 3, 5, 20]:
    tree = build_tree(X_tr, y_tr, 0, depth, 2, rng)
    print(depth, round(float((predict(tree, X_tr) == y_tr).mean()), 3), round(float((predict(tree, X_te) == y_te).mean()), 3))

実行結果:

1 0.593 0.52
3 0.77 0.705
5 0.853 0.751
20 1.0 0.709

左から、木の深さ、学習用のデータでの正解率、テスト用のデータでの正解率です。深さ 1(質問 1 回)では円を表せず、テストで 52% しか当たりません。深さ 5 でテストの正解率は 75.1% になりますが、深さ 20 まで育てると、学習用のデータは 100% 当たるのにテストでは 70.9% に下がりました。ノイズで入れ替えたラベルまで、1 つ 1 つ質問を足して覚えてしまったためです。

決定木の境界は、座標の軸に沿った「階段」の形になります。円のような曲線は、細かい階段をたくさん重ねて近似するしかないので、深くしないと表せず、深くすると過学習しやすい、という悩みがあります。

ランダムフォレスト:たくさんの木で多数決する

データと特徴をばらばらにして木を育てる

1 本の深い木は過学習しやすく、学習用のデータが少し変わるだけで形が大きく変わります(過学習の記事 でいう、バリアンスが大きい状態)。そこで、少しずつ違う木をたくさん作って多数決すると、それぞれの木のばらつきが打ち消し合って、安定した予測になります。これが ランダムフォレストです。

木ごとに違いを出すために、2 つのランダムさを入れます。

  1. ブートストラップ: 学習用のデータから、重複を許して同じ数だけ選び直したデータで、それぞれの木を学習する(この方法で作った木を組み合わせることを バギングと呼ぶ)
  2. 特徴のランダムな選択: 分けるたびに、すべての特徴ではなく、ランダムに選んだ一部の特徴だけから質問を選ぶ

上のコードの続きで、深さ 20 の木を 100 本作って多数決してみます。

# ランダムフォレスト:データの選び方と特徴の選び方を変えた深い木を 100 本作り、多数決する
trees = []
for _ in range(100):
    idx = rng.integers(0, len(X_tr), len(X_tr))      # 重複を許して 300 個選び直す(ブートストラップ)
    trees.append(build_tree(X_tr[idx], y_tr[idx], 0, 20, 1, rng))  # 分けるたびに特徴を 1 つだけ候補にする
for n in [1, 10, 100]:
    v = np.mean([predict(t, X_te) for t in trees[:n]], axis=0)
    print(n, round(float(((v >= 0.5).astype(int) == y_te).mean()), 3))

実行結果:

1 0.693
10 0.75
100 0.771

左が多数決に使った木の本数、右がテスト用のデータでの正解率です。1 本だけでは 69.3% ですが、10 本で 75%、100 本で 77.1% と、1 本の木をどの深さにしたときよりも高くなりました。1 本 1 本は過学習した深い木なのに、まとめると過学習しにくくなるのがランダムフォレストの特徴です。木の本数を増やしても過学習はほとんど進まず、ふつうは多いほど安定します。

勾配ブースティング:前の木の間違いを次の木が直す

残差を順に当てていく

ランダムフォレストは木を「並べて」平均しましたが、勾配ブースティングは木を「順に」足していきます。

  1. 最初は、全データの平均で予測する
  2. 今の予測で外れている分(残差:正解 − 予測)を、新しい小さな木で当てる
  3. その木の予測を、学習率の割合だけ今の予測に足す
  4. 2〜3 をくり返す

1 本 1 本はとても単純な木(ここでは質問 1 回だけの、深さ 1 の木)でも、前の木の間違いを次の木が少しずつ直していくことで、複雑な形を表せるようになります。y = sin(2πx) にノイズを加えた 100 個のデータで試してみます。テスト用には、ノイズのない本当の sin の値を使います。

import numpy as np

rng = np.random.default_rng(0)
x = np.sort(rng.uniform(0, 1, 100))
y = np.sin(2 * np.pi * x) + rng.normal(0, 0.2, 100)
x_test = np.linspace(0, 1, 500)
y_test = np.sin(2 * np.pi * x_test)

def fit_stump(x, r):
    """しきい値 1 つで左右に分け、それぞれ残差の平均を返す(深さ 1 の回帰木)"""
    best = None
    for t in x[1:]:
        left = x < t
        pred = np.where(left, r[left].mean(), r[~left].mean())
        err = ((r - pred) ** 2).sum()
        if best is None or err < best[0]:
            best = (err, t, r[left].mean(), r[~left].mean())
    _, t, a, b = best
    return lambda z: np.where(z < t, a, b)

pred_tr = np.full_like(x, y.mean())
pred_test = np.full_like(x_test, y.mean())
lr = 0.3
for m in range(1, 201):
    stump = fit_stump(x, y - pred_tr)   # 今の予測で外れている分(残差)を新しい木で当てる
    pred_tr += lr * stump(x)
    pred_test += lr * stump(x_test)
    if m in [1, 5, 20, 50, 200]:
        print(m, round(float(np.sqrt(((y - pred_tr) ** 2).mean())), 3),
              round(float(np.sqrt(((y_test - pred_test) ** 2).mean())), 3))

実行結果:

1 0.56 0.553
5 0.355 0.307
20 0.237 0.179
50 0.17 0.108
200 0.134 0.113

左から、足した木の本数、学習用のデータでの誤差(RMSE)、本当の sin との誤差です。木を足すたびに誤差が下がり、50 本で本当の sin との誤差は 0.108 になりました。200 本まで足すと、学習用の誤差はノイズの大きさ 0.2 を大きく下回る 0.134 まで下がり、本当の sin との誤差は 0.113 と少し悪くなっています。ノイズを当て始めた、過学習の入り口です。勾配ブースティングでは、木の本数を増やしすぎると過学習するので、検証用のデータで早期終了するのがふつうです。

名前の「勾配」は、残差が 2 乗誤差の勾配(の逆向き)になっていることから来ています。損失を 2 乗誤差以外に変えても、「損失の勾配を次の木で当てる」形で同じように使えます。この方法を速く・正確にした XGBoost・LightGBM・CatBoost は、表の形のデータの分析でいちばんよく使われている手法の 1 つです。

k 近傍法:近くのデータの多数決

k 近傍法(k-NN)は、学習というほどのことをしない手法です。予測したい点から近い順に k 個の学習用のデータを選び、その多数決で答えます。学習用のデータを覚えておくだけで、事前の計算はありません。決定木と同じ「円の内側か外側か」のデータで、k を変えてみます。決定木のコードの続きです。

# k 近傍法:いちばん近い k 個の学習用の点の多数決で決める
def knn_predict(X_tr, y_tr, X, k):
    d = ((X[:, None, :] - X_tr[None, :, :]) ** 2).sum(axis=2)  # すべての組の距離の 2 乗
    nearest = np.argsort(d, axis=1)[:, :k]
    return (y_tr[nearest].mean(axis=1) >= 0.5).astype(int)

for k in [1, 5, 15, 51, 151]:
    print(k, round(float((knn_predict(X_tr, y_tr, X_tr, k) == y_tr).mean()), 3),
          round(float((knn_predict(X_tr, y_tr, X_te, k) == y_te).mean()), 3))

実行結果:

1 1.0 0.714
5 0.87 0.778
15 0.84 0.762
51 0.813 0.729
151 0.58 0.538

左から、k、学習用のデータでの正解率、テスト用のデータでの正解率です。k = 1 では、学習用の点の答えは自分自身なので 100% 当たりますが、ノイズの点まで信じてしまい、テストでは 71.4% です。k = 5 でテストの正解率は 77.8% と最も高く、k を 151(学習用のデータの半分)まで大きくすると、遠くの点まで多数決に入って円の形がつぶれ、53.8% まで下がりました。k が小さいほど過学習、大きいほど学習不足になる、という関係です。

k 近傍法は単純で分かりやすい一方、予測のたびにすべての学習用のデータとの距離を計算するので、データが多いと予測が遅くなります。また、特徴の数(次元)が多くなると、どの点も同じくらい遠くなって「近さ」が意味を持たなくなる 次元の呪いの影響を強く受けます。特徴の単位がそろっていないと距離がおかしくなるので、特徴の大きさをそろえる(標準化する)ことも必要です。

SVM:余白をいちばん広く取る

マージンの最大化

SVM(サポートベクターマシン)は、2 つのクラスを分ける直線(平面)のうち、両側のいちばん近い点までの距離(マージン)がいちばん広くなるものを選ぶ手法です。境界から離れた点は答えに影響せず、境界のすぐ近くの点(サポートベクター)だけで境界が決まります。

学習では、ヒンジ損失 max(0, 1 − y(w·x + b)) を小さくします。ラベル y は +1 と −1 で、境界から 1 以上離れて正しい側にある点の損失は 0、余白の内側に入ったり間違えたりした点だけが損失を持ちます。これに重みの大きさの罰 λ/2 ‖w‖² を足して、勾配降下法で小さくします(マージンの幅は 2 ÷ ‖w‖ なので、‖w‖ を小さくすることはマージンを広げることにあたります)。

import numpy as np

# 2 つのまとまり(ラベルは +1 と −1)
rng = np.random.default_rng(0)
X = np.vstack([rng.normal([2, 2], 0.6, (20, 2)), rng.normal([-2, -2], 0.6, (20, 2))])
y = np.array([1] * 20 + [-1] * 20)

# 線形 SVM:ヒンジ損失 max(0, 1 − y(w·x + b)) の平均 + λ/2 ‖w‖² を勾配降下法で小さくする
w, b, lam = np.zeros(2), 0.0, 0.01
for k in range(1, 20001):
    margin = y * (X @ w + b)
    viol = margin < 1                                  # 余白の内側に入っている(か、間違えている)点
    gw = lam * w - (y[viol, None] * X[viol]).sum(axis=0) / len(X)
    gb = -y[viol].sum() / len(X)
    lr = 1 / (lam * k)                                 # だんだん小さくする学習率
    w -= lr * gw
    b -= lr * gb
margin = y * (X @ w + b)
print(np.round(w, 3), round(b, 3), round(2 / np.linalg.norm(w), 3))
print(int((margin < 1.01).sum()), round(float(margin.min()), 3))

実行結果:

[0.344 0.522] -0.183 3.2
3 1.0

1 行目は、境界の重み・切片と、マージンの幅 3.2 です。2 行目は、余白のふちにある点の数と、y(w·x + b) のいちばん小さい値です。40 個の点のうち、境界を決めているのは余白のふちに乗った 3 個だけで、ほかの 37 個の点は動かしても(余白の外にある限り)境界は変わりません。

カーネル法:曲がった境界を作る

円の内側と外側のように、直線では分けられないデータもあります。このときは、元の特徴に「原点からの距離の 2 乗 x₁² + x₂²」のような新しい特徴を足すと、新しい特徴の空間では平面で分けられるようになります。

import numpy as np

def train_svm(X, y, lam=0.01, steps=20000):
    w, b = np.zeros(X.shape[1]), 0.0
    for k in range(1, steps + 1):
        viol = y * (X @ w + b) < 1
        w -= (lam * w - (y[viol, None] * X[viol]).sum(axis=0) / len(X)) / (lam * k)
        b -= (-y[viol].sum() / len(X)) / (lam * k)
    return w, b

# 円の内側が +1、外側が −1。直線では分けられない
rng = np.random.default_rng(0)
X = rng.uniform(-1, 1, (200, 2))
y = np.where(X[:, 0] ** 2 + X[:, 1] ** 2 < 0.5, 1, -1)

for name, F in [("x1, x2", X), ("x1, x2, x1²+x2²", np.column_stack([X, (X**2).sum(axis=1)]))]:
    w, b = train_svm(F, y)
    print(name, round(float((np.sign(F @ w + b) == y).mean()), 3))

# カーネル法:特徴を書き出さなくても、特徴どうしの内積を元の入力から計算できる
a, c = np.array([1.0, 2.0]), np.array([3.0, 1.0])
phi = lambda v: np.array([v[0] ** 2, np.sqrt(2) * v[0] * v[1], v[1] ** 2])  # 2 次の特徴
print(round(float(phi(a) @ phi(c)), 3), round(float((a @ c) ** 2), 3))

実行結果:

x1, x2 0.625
x1, x2, x1²+x2² 0.995
25.0 25.0

元の 2 つの特徴だけでは、正解率は 62.5% にとどまります。x₁² + x₂² を足すと 99.5% になりました。

最後の行は、カーネル法の考え方です。SVM の計算は、データどうしの「内積」だけで書き直せることが知られています。そこで、2 次の特徴 φ(v) = (v₁², √2 v₁v₂, v₂²) を実際に作って内積を取る代わりに、元のベクトルの内積を 2 乗した (a·c)² を計算すれば、同じ 25 が得られます。この「特徴の空間での内積を、元の入力から直接計算する関数」を カーネルと呼びます。カーネルを使えば、特徴を書き出さずに、とても次元の高い(ガウスカーネルでは無限次元の)特徴の空間で分けたのと同じ効果が得られます。

SVM はデータが少なくても安定して高い性能を出せるので、2000 年代には画像や文章の分類で広く使われていました。一方、カーネルを使う SVM は、データの数の 2 乗に比例するカーネルの値の表が必要になるので、数十万件を超える大きなデータには向きません。

どの手法を選べばよいか

手法長所短所
線形回帰・ロジスティック回帰速い。重みが読める。まず試す基準として使える曲がった関係は、特徴を自分で作らないと表せない
決定木結果を人に説明しやすい。特徴の大きさをそろえなくてよい1 本だと過学習しやすく不安定
ランダムフォレスト調整が少なくても安定して強い。過学習しにくい木が多いと、なぜその答えになったかが読みにくい
勾配ブースティング表の形のデータで多くの場合いちばん強い調整の値が多く、木を足しすぎると過学習する
k 近傍法学習がいらず、考え方が単純予測が遅い。次元が多いと弱い
SVMデータが少なくても安定。カーネルで曲がった境界も作れる大きなデータでは遅い。確率を直接は出さない
ニューラルネットワーク画像・音声・文章など、生のデータから特徴を自分で学べる大量のデータと計算が必要

画像・音声・文章のように、生のデータから特徴を見つけること自体が難しい問題では、ニューラルネットワークが圧倒的に強くなりました。一方、列ごとに意味のはっきりした表の形のデータでは、勾配ブースティングやランダムフォレストが今でもよく選ばれています。まず線形のモデルとランダムフォレストを基準として試し、それより良いかどうかで、ほかの手法を比べるのが実務でよく使われる進め方です。

機械学習の手法の流れ

この記事で紹介した手法を年代順に並べます。研究者の動きは AI 開発のキープレイヤー年表 にまとめています。

年手法ポイント
1805最小二乗法(ルジャンドル)誤差の 2 乗の和を最小にして直線を当てはめる
1951k 近傍法(フィックス・ホッジス)近くのデータの多数決で分類する
1958ロジスティック回帰(コックス)線形の式をシグモイドで確率にする
1984CARTジニ不純度で 2 つに分けていく決定木
1986ID3情報量(エントロピー)で分ける決定木。1993 年の C4.5 に発展
1992カーネル法による SVMマージン最大化とカーネルを組み合わせる
1995ソフトマージン SVM(コルテス・バプニック)余白の内側に入る点を許す
1996バギングブートストラップで作った木を平均する
1997AdaBoost間違えたデータを重く見て、弱い分類器を順に足す
2001ランダムフォレスト・勾配ブースティング木の多数決と、残差を順に当てる木の足し算
2016〜2018XGBoost・LightGBM・CatBoost勾配ブースティングを速く・正確にした実装

まとめ

  • 線形回帰は特徴に重みを掛けて足すモデルで、最小二乗法の重みは正規方程式で一度に求まる。重みから特徴の効き方が読める
  • ロジスティック回帰は線形の式をシグモイドで確率に変える分類の手法で、境界は直線になる
  • 決定木は、ジニ不純度がいちばん下がる「特徴 < しきい値」の質問をくり返して分ける。深くすると過学習する
  • ランダムフォレストは、ブートストラップと特徴のランダムな選択で少しずつ違う木を作って多数決し、過学習を抑える
  • 勾配ブースティングは、今の予測の残差を小さな木で当てて足していく。表の形のデータでとても強い
  • k 近傍法は近くの k 個の多数決で答える。k が小さいと過学習、大きいと学習不足になる
  • SVM はマージンを最大にする境界を選び、サポートベクターだけで境界が決まる。カーネル法で曲がった境界も作れる
  • 画像・音声・文章ではニューラルネットワークが強いが、表の形のデータでは木を組み合わせた手法が今もよく使われる

参考

  • A. M. Legendre, "Nouvelles méthodes pour la détermination des orbites des comètes", 1805
  • E. Fix and J. L. Hodges, "Discriminatory Analysis. Nonparametric Discrimination: Consistency Properties", Technical Report, USAF School of Aviation Medicine 1951
  • D. R. Cox, "The Regression Analysis of Binary Sequences", Journal of the Royal Statistical Society: Series B 1958
  • T. Cover and P. Hart, "Nearest neighbor pattern classification", IEEE Transactions on Information Theory 1967
  • L. Breiman et al., "Classification and Regression Trees", Wadsworth 1984
  • J. R. Quinlan, "Induction of Decision Trees", Machine Learning 1986
  • B. E. Boser, I. M. Guyon and V. N. Vapnik, "A Training Algorithm for Optimal Margin Classifiers", COLT 1992
  • C. Cortes and V. Vapnik, "Support-Vector Networks", Machine Learning 1995
  • L. Breiman, "Bagging Predictors", Machine Learning 1996
  • Y. Freund and R. E. Schapire, "A Decision-Theoretic Generalization of On-Line Learning and an Application to Boosting", Journal of Computer and System Sciences 1997
  • L. Breiman, "Random Forests", Machine Learning 2001
  • J. H. Friedman, "Greedy Function Approximation: A Gradient Boosting Machine", The Annals of Statistics 2001
  • S. Shalev-Shwartz et al., "Pegasos: Primal Estimated sub-GrAdient SOlver for SVM", ICML 2007
  • T. Chen and C. Guestrin, "XGBoost: A Scalable Tree Boosting System", KDD 2016(arXiv:1603.02754)
  • G. Ke et al., "LightGBM: A Highly Efficient Gradient Boosting Decision Tree", NeurIPS 2017
  • L. Prokhorenkova et al., "CatBoost: unbiased boosting with categorical features", NeurIPS 2018(arXiv:1706.09516)
  • L. Grinsztajn et al., "Why do tree-based models still outperform deep learning on tabular data?", NeurIPS 2022(arXiv:2207.08815)