線形回帰を「最小二乗」から作り直す ― sklearnの一行の裏側にある数学
LinearRegressionの一行を最小二乗から作り直します。線形モデルをベクトルと行列で読む練習、最小二乗の直感から正規方程式まで、残差と決定係数の意味。House Pricesで確認し、SOLNAのLTV素朴回帰へ移します。線形代数の最初の壁をここで越えます。
by Shin
LinearRegression().fit() と一行書けば、モデルは学習されます。では、その一行は内部で何を解いているのでしょうか。答えは「誤差の二乗和を最小化する重みを、正規方程式という連立一次方程式で一発で解いている」です。この回では、線形モデル を行列として読み、最小二乗から正規方程式までをスクラッチで再現し、最後に実務データのLTV回帰へ移します。線形代数の最初の壁を、ここで越えます。
到達目標は三つです。 を行列の積として読めること、最小二乗が何を最小化しているか言えること、残差と決定係数を過信せず読めること。前提は第1回(ノートの8ブロック分解)と第2回(前処理と分布)まで。sklearn の .fit()/.predict() を写経で回した経験があれば十分です。
線形モデルの形(y = Xw)とベクトル/行列の読み方
写経の段階では、X は「特徴量が入った表」、y は「当てたい数値」くらいの理解で動きます。けれども一行の裏側を読むには、両者を行列とベクトルとして見直す必要があります。
一サンプルだけ見れば、線形回帰の予測は次の足し算です。
が切片、 が各特徴量の重みです。これを全サンプルぶん縦に積むと、サンプルごとに同じ計算を繰り返す代わりに、行列積一発で書けます。それが です。記号の中身は次のとおりです。 はサンプル数 × 特徴量数の行列、 は特徴量数ぶんの重みベクトル、 は予測値のベクトル。切片 は、 の先頭に「全部1」の列を足すことで重みの一部として吸収できます。
私は私立文系の出身で、行列の積を「手が覚えている」状態になったのは社会人になってからでした。最初に効いた読み替えは、 を「 の列ベクトルたちを、 を係数にして足し合わせたもの」と見る視点です。つまり予測とは、特徴量という方向ベクトルを重みで混ぜて目的に近づける操作だ、と。この見方は第5回の正則化(重みの大きさを抑える)まで一本でつながります。
最小二乗の直感(誤差の二乗和を最小化する)
重み をどう決めるか。基準は「予測 と実測 のズレを、全体として小さくする」です。ズレ(残差)は ですが、これをそのまま足すと正負が打ち消し合って意味を失います。そこで二乗してから足し合わせ、その合計を最小にする を探します。これが最小二乗法で、最小化する量は次の残差二乗和です。
なぜ絶対値ではなく二乗なのか。理由は三つあります。第一に、二乗は微分が滑らかで、最小値を解析的に求めやすい。第二に、大きなズレほど重く罰するため、大外しを嫌う性質になる(これは外れ値に弱いという裏返しでもあり、第7回の指標選びで再訪します)。第三に、誤差が正規分布に従うと仮定したときの最尤推定と一致する。三つ目は第4回(最尤推定)への伏線で、いま深追いはしません。「二乗和を最小化する」という操作には、確率的な意味づけが後から付くとだけ覚えておいてください。
正規方程式(最小限の式)― 解析解という考え方
残差二乗和を で微分してゼロと置くと、最小値を与える が満たすべき連立一次方程式が出ます。それが正規方程式で、解は次の形に書けます。
導出はここでは追いません。読みたいのは各項の意味です。 は特徴量どうしの内積を並べた行列(特徴量間の関係を要約したもの)、 は各特徴量と目的変数の内積(どの特徴量が目的とどれだけ揃って動くか)。両者を組み合わせて、目的を最もよく説明する重みの配合を一度の計算で割り出す——これが正規方程式です。
ここで大事なのは「解析解」という考え方です。線形回帰は、上の式に値を入れれば答えが一発で出ます。試行錯誤も繰り返しもいりません。これは線形回帰の幸運な特徴で、次回扱うロジスティック回帰では成り立ちません。あちらは式変形では解けず、勾配降下法で少しずつ近づける必要があります。「解析解がある/ないで解き方が変わる」という対比は、第4回の最適化を理解する足場になります。
残差・決定係数 ― あてはまりの読み方
学習が終わったら、あてはまりを残差と決定係数で読みます。決定係数 は次の形です。
分母は「平均で予測したときの誤差」、分子は「モデルで予測したときの誤差」。平均より誤差をどれだけ減らせたかの割合、と読めます。1に近いほどあてはまりが良い、という素朴な解釈で出発して構いません。
ただし、ここに連載を通じて繰り返す誤解の一つ目があります。あてはまりが高いことと、役に立つことは別です。理由を二つ挙げます。第一に、訓練データの は特徴量を足すほど(意味のない列でも)単調に上がります。自由度を食って訓練データに寄せているだけで、未知データでの性能は別問題です。汎化を見るには訓練データの外で評価する必要があり、それが第5回の交差検証の主題です。第二に、 が高くても、それは「予測が当たる」だけで「なぜそうなるか」「介入したらどうなるか」は何も言いません。予測の精度と因果は無関係です(この線引きは第7回で再掲し、深掘りは因果編へ送ります)。
数字だけでなく残差プロット(横軸に予測値、縦軸に残差)も必ず見ます。残差が予測値に対して曲線を描いていれば線形の仮定が破れているサイン、予測が大きい領域で残差のばらつきが広がっていれば分散不均一のサインです。 が高くても、残差にパターンが残っていれば、そのモデルは何かを取りこぼしています。
スクラッチ実装とsklearnの一行を突き合わせる
ここまでを公開データで確かめます。House Pricesの目的変数 SalePrice は右に裾を引くので、第2回で見たとおり対数変換してから回帰します。特徴量は読解優先で少数に絞ります。正規方程式を素朴に書いたものと、sklearn の一行が同じ重みを出すことを突き合わせます。
import numpy as np
import pandas as pd
from sklearn.linear_model import LinearRegression
# House Prices: 読解のため数値特徴を少数だけ使う最小構成
df = pd.read_csv("train.csv")
features = ["GrLivArea", "OverallQual", "TotalBsmtSF"]
X_raw = df[features].fillna(0).to_numpy(dtype=float)
y = np.log1p(df["SalePrice"].to_numpy(dtype=float)) # 第2回で見た対数変換
# 切片を吸収する「全部1」の列を先頭に足す。これが y = Xw の X
X = np.hstack([np.ones((X_raw.shape[0], 1)), X_raw])
# 正規方程式 w = (XᵀX)⁻¹ Xᵀy を、意味どおりに組む(学習用)
XtX = X.T @ X
Xty = X.T @ y
w_scratch = np.linalg.solve(XtX, Xty) # inv() を作らず solve に渡す(安定)
# sklearn の一行と突き合わせる
lr = LinearRegression() # 切片は内部で持つので X_raw を渡す
lr.fit(X_raw, y)
w_sklearn = np.concatenate([[lr.intercept_], lr.coef_])
print(np.allclose(w_scratch, w_sklearn)) # True: 同じものを解いている最後の np.allclose が True になることが、この回の核心です。.fit() という一行は、ブラックボックスではなく、いま自分の手で組んだ正規方程式そのものを解いていた——これが「写経」と「解読」の差です。動かすだけなら sklearn の一行で十分ですが、なぜ係数がそうなるか、いつ壊れるか(多重共線性、悪条件)を言えるのは、下の式を一度自分で通した人だけです。
SOLNAへ ― ltv_to_date の素朴回帰と未成熟コホート
同じ操作を実務データに移します。過去の受託案件をもとに設計した架空のD2CブランドSOLNAで、顧客テーブル customers から、これまでの累計購入額 ltv_to_date を、獲得からの経過日数・獲得チャネル・地域・年代で素朴に回帰してみます。
import numpy as np
import pandas as pd
from sklearn.linear_model import LinearRegression
customers = pd.read_csv("customers.csv", parse_dates=["first_order_at"])
# スナップショットの基準日。この時点で見えている情報だけで学習する
asof = pd.Timestamp("2026-06-01")
customers["tenure_days"] = (asof - customers["first_order_at"]).dt.days
# 素朴版: 経過日数 + カテゴリ変数で ltv_to_date を回帰
X = pd.get_dummies(
customers[["tenure_days", "acquisition_channel", "region", "age_band"]],
drop_first=True,
).to_numpy(dtype=float)
y = customers["ltv_to_date"].to_numpy(dtype=float)
lr = LinearRegression().fit(X, y)
print(lr.score(X, y)) # 訓練データの R^2。あてはまりであって汎化でも因果でもないこのコードは動きますが、ここから移植で壊れる点が二つあります。
一つ目は未成熟コホートです。first_order_at が最近の顧客は、まだ購入を積み上げる途中で、ltv_to_date が構造的に小さく出ます。これを成熟した顧客と一緒くたに回帰すると、新規コホートのLTVを過小評価し、ひいては新規獲得の価値を不当に低く見積もる判断につながります。House Pricesのような公開コンペデータは「完成・固定」されていて、こうした「まだ途中の行」は混ざりません。だからコンペで効くやり方が、実務でそのまま効くとは限らない——これが連載を通じて繰り返す誤解の一つです。対処の方向は、観測窓を揃える(例えば「獲得後90日LTV」に統一して比較可能にする)、コホート月をコントロール変数に入れる、あるいは打ち切りを前提とした推定に切り替える、です。素朴回帰はここで折れる、と知っておくことが移植の勘所になります。
二つ目は未来情報のリークです。ltv_to_date を説明する特徴に、LTVが確定した後にしか分からない情報(その後の総注文回数、解約日など)を混ぜてはいけません。訓練では高精度に見えても、運用時にはその情報がまだ存在せず、再現しません。「学習時点で本当に手に入る特徴か」を一列ずつ確かめるのが、実務での前処理です。
対応書籍と読みどころ
行列・ベクトルの土台は、キーポイント線形代数で を「列の線形結合」として読めるところまで戻ると、この回の式が腑に落ちます。線形代数を演習でさらに固めたい場合は、任意で明解演習線形代数を一冊加えると、 を手で動かす回数が稼げます。
PRMLへの一歩
この回はPRML第3章(線形回帰モデル)の入口にあたります。PRMLでは の を、入力 をそのまま使う代わりに基底関数 (多項式やガウス基底など)に通します。すると「特徴量を作り替えるだけで、同じ最小二乗の枠組みで非線形も扱える」ことが見えます。今回の線形は、その最も素朴な基底(恒等写像)の場合だった、という位置づけです。第5回の正則化を加えると、PRML 3.1〜3.3(正則化つき線形回帰とベイズ線形回帰)にそのまま接続します。
章末チェックリスト
- を行列の積(列の線形結合)として読める
- 最小二乗が何を最小化しているか(残差二乗和)を言える
- 正規方程式の各項(、)の意味を説明できる
- 残差・決定係数を過信せず読める(あてはまり ≠ 汎化 ≠ 因果)
- スクラッチと
sklearnの係数が一致することを自分で確かめた - 未成熟コホートと未来情報リークが、移植で壊す点だと言える
次回は「分類とは確率を出すこと」という視点から、ロジスティック回帰を最尤推定と勾配降下で作り直します。今回の解析解(一発で解ける)と、次回の反復解(少しずつ近づける)の対比が、最適化を理解する足場になります。
なお、「LTVのどの要因に予算を寄せるか」という判断や、「そもそも何を測るべきか」の全体像は、対になる記述・予測編で扱っています。予測の精度を上げる話と、意思決定を良くする話は別物なので、合わせて読むと現場での使い分けがはっきりします。