機械学習ではなぜlogを取るのか:五つの理由を整理する

こんにちは。ゆうせいです。

機械学習を学んでいると、あちこちで対数が登場します。交差エントロピー誤差の式にlogがあります。尤度を最大化するとき、対数尤度を使います。前処理でデータを対数変換することもあります。損失のグラフを描くとき、縦軸を対数目盛りにすることもあります。これだけ頻繁に出てくると、「なぜlogばかり出てくるのか」と感じるのではないでしょうか。実は、それぞれ理由が異なります。同じ対数という道具を、まったく別の目的で使っているのです。本記事では、機械学習で対数を取る理由を五つに整理し、それぞれの場面と目的を解説します。

結論を先に述べます

機械学習で対数を取る理由は、大きく五つに分類できます。

第一に、掛け算を足し算に変えるためです。確率の積を扱う場面で使われます。

第二に、数値計算を安定させるためです。非常に小さい値や大きい値による破綻を防ぎます。

第三に、値の範囲を制限から解放するためです。0から1の確率を、実数全体に広げます。

第四に、偏った分布を整えるためです。前処理としてのデータ変換にあたります。

第五に、桁の違うものを比べやすくするためです。グラフの描画や、パラメータの探索範囲の設定で使われます。

以下、順に説明します。

対数の基本を確認する

本題に入る前に、対数の性質を確認しておきます。

対数とは、「何乗したらその数になるか」を表すものです。

\log_2 8 = 3

これは、2を3乗すると8になる、という意味です。

機械学習では、底がネイピア数eの自然対数がよく使われます。

\ln x = \log_e x

表記としては、単にlogと書かれることも多くあります。文脈から、自然対数だと判断してください。

重要な三つの性質

以降の説明で使う性質を、先に挙げておきます。

第一の性質は、積が和になることです。

\log(ab) = \log a + \log b

第二の性質は、単調増加であることです。

xが大きくなれば、\log x も必ず大きくなります。順序が保たれます。

第三の性質は、値の範囲を広げることです。

xが0に近づくと、\log x はマイナス無限大に近づきます。0から1という狭い範囲が、マイナス無限大から0という広い範囲に引き伸ばされます。

理由1:掛け算を足し算に変える

最も本質的な理由です。

確率は掛け算で積み重なる

複数の独立な事象が同時に起こる確率は、それぞれの確率の掛け算になります。

コインを3回投げて、すべて表が出る確率は、次のようになります。

0.5 \times 0.5 \times 0.5 = 0.125

機械学習では、この構造が頻繁に現れます。

たとえば、1000件のデータがあるとき、そのすべてが観測される確率は、1000個の確率の掛け算になります。

L = p_1 \times p_2 \times \cdots \times p_{1000}

これを尤度と呼びます。モデルのパラメータが、観測されたデータをどれだけうまく説明できるかを表す指標です。

掛け算のままでは扱いにくい

この掛け算には、二つの問題があります。

一つ目は、微分が面倒であることです。

積の微分には、積の法則を使う必要があります。項が1000個あれば、非常に複雑な式になります。

二つ目は、数値が極端に小さくなることです。この点は、理由2で詳しく扱います。

対数を取ると足し算になる

対数の第一の性質を使います。

\log L = \log p_1 + \log p_2 + \cdots + \log p_{1000}

掛け算が、足し算に変わりました。

足し算であれば、微分は各項を独立に微分して足すだけです。

\frac{d}{d\theta}\log L = \frac{d}{d\theta}\log p_1 + \frac{d}{d\theta}\log p_2 + \cdots

計算が、劇的に簡単になります。

最大値の位置は変わらない

ここで、重要な確認があります。

対数を取ったら、最大化する対象が変わってしまうのではないか、という疑問です。

答えは、変わりません。

対数は単調増加関数です。したがって、L が最大になるパラメータと、\log L が最大になるパラメータは、完全に一致します。

\arg\max_\theta L(\theta) = \arg\max_\theta \log L(\theta)

「最大値そのもの」は変わりますが、「最大値を取るパラメータ」は同じです。求めたいのは後者ですから、問題ありません。

この性質があるからこそ、安心して対数を取れます。

この図で伝えたいこと

左の図は、確率を掛け合わせると値が急速に0へ向かうことを示しています。

中央の図は、同じデータでも対数の和にすれば直線的な減少になり、扱いやすい範囲に収まることを示しています。

右の図は、対数を取っても最大値を取るパラメータの位置は変わらないことを示しています。これが、安心して対数を取れる根拠です。

理由2:数値計算を安定させる

アンダーフローという問題

コンピュータが扱える数値には、限界があります。

0.001という確率を、1000個掛け合わせてみます。

0.001^{1000} = 10^{-3000}

この値は、float64という一般的な形式で表現できる下限を、はるかに下回ります。

結果として、コンピュータ上では0になってしまいます。これをアンダーフローと呼びます。

0になってしまえば、その後の計算はすべて意味を失います。対数を取ろうにも、\log 0 はマイナス無限大であり、計算が破綻します。

対数を取れば桁が扱いやすくなる

同じ計算を、対数の世界で行います。

\log(0.001^{1000}) = 1000 \times \log 0.001 \approx 1000 \times (-6.9) = -6900

マイナス6900という値になります。

これは、float64で問題なく表現できます。

極端に小さい数値も、対数の世界では、扱いやすい大きさの数値になります。

オーバーフローへの対策

逆向きの問題もあります。

Softmax関数の計算では、指数関数が使われます。

\frac{e^{x_i}}{\sum_j e^{x_j}}

x が大きいと、e^x が表現可能な上限を超えます。これをオーバーフローと呼びます。

e^{1000}

この値は計算できません。

対策として、すべての要素から最大値を引いてから計算する、という工夫が行われます。

\frac{e^{x_i - \max(x)}}{\sum_j e^{x_j - \max(x)}}

分子と分母の両方が同じ定数倍されるため、結果は変わりません。しかし、指数関数の入力が必ず0以下になるため、オーバーフローを避けられます。

LogSumExpという技法

対数と指数を組み合わせた計算では、次の形がよく現れます。

\log\sum_i e^{x_i}

これを、次のように変形します。

\log\sum_i e^{x_i} = \max(x) + \log\sum_i e^{x_i - \max(x)}

この変形により、数値的に安定した計算ができます。

この技法を、LogSumExpと呼びます。多くのライブラリに、専用の関数が用意されています。

実装上の注意

PyTorchなどのフレームワークでは、Softmaxと対数の計算をまとめた関数が提供されています。

分けて計算するより、まとめたほうが数値的に安定します。

import torch
import torch.nn.functional as F

logits = torch.tensor([100.0, 101.0, 102.0])

# 推奨されない方法:分けて計算
probs = F.softmax(logits, dim=0)
log_probs_unstable = torch.log(probs)

# 推奨される方法:まとめて計算
log_probs_stable = F.log_softmax(logits, dim=0)

分類問題で使うnn.CrossEntropyLossも、内部でこの工夫を行っています。そのため、Softmaxを適用する前の値、つまりロジットを直接入力する設計になっています。

この図で伝えたいこと

左の図は、確率を掛け続けると、ある回数を境に値が0になってしまうことを示しています。太い線が途中で途切れる位置が、アンダーフローの発生点です。

右の図は、指数関数が急激に大きくなり、表現できる上限にすぐ達することを示しています。最大値を引いてから計算する工夫により、この問題を回避できることも示しています。

理由3:値の範囲を制限から解放する

確率は0から1に閉じ込められている

確率は、必ず0から1の範囲に収まります。

この制約が、都合の悪い場面があります。

ロジスティック回帰を考えます。入力変数の線形結合から、確率を予測したいとします。

z = w_1x_1 + w_2x_2 + \cdots + b

この z は、マイナス無限大からプラス無限大まで、どんな値にもなり得ます。

これを、そのまま確率として扱うことはできません。範囲が合わないためです。

ロジット変換

そこで、確率を実数全体に写す変換を使います。

まず、オッズを計算します。事象が起こる確率と、起こらない確率の比です。

\frac{p}{1-p}

pが0に近ければ0に近く、pが1に近ければ無限大に近づきます。

次に、この対数を取ります。

\text{logit}(p) = \log\left(\frac{p}{1-p}\right)

これをロジット変換と呼びます。

pが0に近づけばマイナス無限大に、pが1に近づけばプラス無限大に、pが0.5であれば0になります。

0から1という制限された範囲が、実数全体に広がりました。

線形結合と結びつく

これにより、次の関係を仮定できます。

\text{logit}(p) = z

両辺とも、実数全体の値を取れるため、無理がありません。

この式をpについて解くと、シグモイド関数が導かれます。

p = \frac{1}{1 + e^{-z}}

ロジスティック回帰という名前は、この「ロジットを線形回帰する」という発想に由来しています。

解釈しやすくなるという副次的な利点

ロジット変換には、もう一つ利点があります。

ロジスティック回帰の係数が、オッズ比として解釈できるようになるのです。

ある変数の係数が0.7であれば、次のようになります。

e^{0.7} \approx 2.01

その変数が1単位増えると、オッズが約2倍になる、と説明できます。

医療統計や与信審査など、モデルの判断根拠を人に説明する必要がある場面で、この性質が重視されます。

この図で伝えたいこと

左の図は、確率が0や1に近づくとロジットが無限大に発散することを示しています。

中央の図は、範囲の対比を帯として視覚化しています。数式が苦手な受講者にもイメージしやすいと思います。

右の図は、ロジット変換を逆に解くとシグモイド関数が得られることを示しています。ロジスティック回帰の構造が、ここでつながります。

理由4:偏った分布を整える

ここまでは、モデルの内部計算の話でした。理由4は、前処理の話です。

実データは右に裾を引くことが多い

現実のデータには、極端に偏った分布を持つものが少なくありません。

年収、企業の売上、都市の人口、Webサイトの訪問数、単語の出現頻度。

これらに共通するのは、大多数が小さな値に集中し、ごく一部が極端に大きな値を取るという構造です。

年収であれば、大部分の人が300万円から800万円の範囲に収まる一方、数億円という人も存在します。

このような分布を、右に裾を引く分布と呼びます。

何が問題なのか

三つの問題があります。

第一に、外れ値の影響が大きすぎることです。

数億円という値が1件あるだけで、平均が大きく引き上げられます。多くのモデルは、誤差の2乗を最小化するため、極端な値に強く引きずられます。

第二に、線形の関係が見えにくいことです。

「年収が2倍になると、購買額はどう変わるか」という関係は、多くの場合、掛け算の関係です。しかし、線形モデルは足し算の関係しか表現できません。

第三に、スケールの差が大きすぎることです。

第2章で扱った標準化を行っても、分布の形が偏っていれば、問題は完全には解消されません。

対数変換で分布が整う

対数を取ると、大きな値ほど強く圧縮されます。

\log 100 \approx 4.6

\log 1000 \approx 6.9

\log 10000 \approx 9.2

10倍になっても、対数の値は約2.3ずつしか増えません。

この性質により、右に裾を引いた分布が、より対称的な形に近づきます。

掛け算の関係を足し算に変える

もう一つの効果があります。

y = ax^b

このような関係があるとします。掛け算とべき乗の関係です。線形モデルでは表現できません。

両辺の対数を取ります。

\log y = \log a + b\log x

\log y\log x の間に、線形の関係が現れました。

つまり、対数変換した後であれば、線形モデルで扱えるようになります。

実装上の注意

対数変換には、注意点があります。

0以下の値には、対数が定義できません。

\log 0 = -\infty

売上や訪問数のように、0が含まれるデータでは、そのまま対数を取れません。

対策として、1を足してから対数を取る方法がよく使われます。

\log(1 + x)

xが0のとき、\log 1 = 0 となり、問題が生じません。

NumPyには、この計算を数値的に安定して行う関数が用意されています。

import numpy as np

x = np.array([0, 1, 10, 100, 10000])
print(np.log1p(x))

なお、対数変換したデータで予測を行った場合、結果を元のスケールに戻す必要があります。この際、単純に指数関数を適用すると、平均のずれが生じる点にも注意が必要です。

この図で伝えたいこと

左の図は、平均が中央値より大きく右にずれていることを示しています。少数の大きな値に引きずられている状態です。

中央の図、対数変換後は平均と中央値がほぼ重なり、山型の対称な分布になることを示しています。

右の図と挿入図で、べき乗の関係が、両対数にすると直線になることを示しています。線形モデルで扱えるようになる、ということです。

理由5:桁の違うものを比べやすくする

最後は、可視化と探索の話です。

対数目盛りのグラフ

学習の経過を示すグラフで、縦軸を対数目盛りにすることがあります。

損失が、最初は100で、最終的に0.001まで下がったとします。

通常の目盛りでは、100という初期値に引きずられて、0.001付近の変化がまったく見えません。グラフの下端に張り付いた直線に見えます。

対数目盛りにすれば、100から10への変化と、0.01から0.001への変化が、同じ幅で表示されます。

どちらも10分の1になるという、同じ意味を持つ変化だからです。

学習が進んでいるかどうかを判断するには、対数目盛りのほうが適しています。

学習率の探索

ハイパーパラメータを探索する際にも、対数が使われます。

学習率の候補を考えるとき、次のように等間隔で選ぶのは適切ではありません。

0.1, 0.2, 0.3, 0.4, 0.5

学習率にとって重要なのは、桁の違いです。0.1と0.2の違いより、0.1と0.001の違いのほうが、はるかに大きな影響を持ちます。

したがって、対数スケールで等間隔に選びます。

0.00001, 0.0001, 0.001, 0.01, 0.1

NumPyには、この選び方を行う関数があります。

import numpy as np

candidates = np.logspace(-5, -1, 5)
print(candidates)

同じ考え方は、正則化の強さや、バッチサイズの探索にも使われます。

交差エントロピー誤差における対数

ここまでの理由が、実際に組み合わさっている例として、交差エントロピー誤差を見ておきます。

式の確認

分類問題で最もよく使われる損失関数です。

L = -\sum_i y_i \log(\hat{y}_i)

y_i は正解のラベル、\hat{y}_i はモデルが予測した確率です。

正解ラベルがOne-Hot表現であれば、正解のクラス以外は0になるため、実質的に次の形になります。

L = -\log(\hat{y}_{正解})

なぜこの形なのか

理由1で扱った、尤度の最大化から導かれます。

すべてのデータが正しく分類される確率、つまり尤度を最大化したい。

L = \hat{y}_1 \times \hat{y}_2 \times \cdots

これに対数を取り、掛け算を足し算に変えます。

\log L = \log\hat{y}_1 + \log\hat{y}_2 + \cdots

損失として「小さくしたい値」にするため、マイナスをつけます。

-\log L = -\log\hat{y}_1 - \log\hat{y}_2 - \cdots

これが、交差エントロピー誤差です。

罰の与え方という解釈

別の見方もできます。

正解の確率を0.9と予測した場合、損失は次のようになります。

-\log 0.9 \approx 0.105

正解の確率を0.1と予測した場合、損失は次のようになります。

-\log 0.1 \approx 2.303

正解の確率を0.001と予測した場合、損失は次のようになります。

-\log 0.001 \approx 6.908

自信を持って間違えるほど、損失が急激に大きくなります。

「確信を持って外した予測」に、強い罰を与える構造になっているのです。

この図で伝えたいこと

左と中央の図は、まったく同じデータです。目盛りを変えただけで、印象が正反対になることを示しています。

左では「途中で学習が止まった」ように見えますが、中央では最後まで一定のペースで減っていることが分かります。

右の図で、学習率の候補を等間隔で選ぶと、小さい側がほとんど探索されないことを示します。

五つの理由の整理

理由目的主な登場場面
掛け算を足し算に微分と計算の簡略化対数尤度、交差エントロピー誤差
数値の安定化アンダーフローとオーバーフローの回避Softmax、LogSumExp
範囲の解放0から1を実数全体へロジット変換、ロジスティック回帰
分布の整形偏りの是正、掛け算関係の線形化前処理としてのデータ変換
桁の比較桁違いの値を扱いやすく損失グラフ、ハイパーパラメータ探索

同じ対数という道具が、これだけ異なる目的で使われています。

「なぜlogを取るのか」という問いに対しては、「場面によって理由が違う」というのが、正確な答えです。

logを見かけたら、五つのうちどれに該当するかを判断してください。それが分かれば、その式が何をしようとしているかが読み取れます。

Pythonで確認する

import numpy as np
import math

print("=== 理由1:掛け算が足し算になる ===")
probs = np.array([0.9, 0.8, 0.95, 0.7, 0.85])

product = np.prod(probs)
log_sum = np.sum(np.log(probs))

print(f"確率: {probs}")
print(f"掛け算の結果: {product:.6f}")
print(f"その対数: {np.log(product):.6f}")
print(f"対数の和: {log_sum:.6f}")
print(f"一致するか: {np.isclose(np.log(product), log_sum)}")

print("\n=== 最大値の位置は変わらない ===")
def likelihood(theta):
    return np.exp(-(theta - 2)**2)

thetas = np.linspace(0, 4, 401)
L = likelihood(thetas)
logL = np.log(L)

print(f"尤度が最大になるθ: {thetas[np.argmax(L)]:.4f}")
print(f"対数尤度が最大になるθ: {thetas[np.argmax(logL)]:.4f}")
print("同じ位置で最大になります。")

print("\n=== 理由2:アンダーフローの回避 ===")
p = 0.001
n = 1000

naive = p ** n
print(f"{p}を{n}回掛けた結果(そのまま): {naive}")
print(f"→ 0になってしまい、情報が失われました")

log_result = n * math.log(p)
print(f"対数の世界で計算: {log_result:.2f}")
print(f"→ 扱える範囲の数値です")

print("\n--- 何回掛けると0になるか ---")
for n in [100, 200, 300, 320, 350]:
    val = 0.001 ** n
    print(f"  {n:>4}回: {val:.3e}" if val > 0 else f"  {n:>4}回: 0 になりました")

print("\n=== 理由2:オーバーフローの回避 ===")
def naive_softmax(x):
    exp_x = np.exp(x)
    return exp_x / exp_x.sum()

def stable_softmax(x):
    shifted = x - np.max(x)
    exp_x = np.exp(shifted)
    return exp_x / exp_x.sum()

logits = np.array([1000.0, 1001.0, 1002.0])

print(f"入力: {logits}")
with np.errstate(over='ignore', invalid='ignore'):
    print(f"素朴な実装: {naive_softmax(logits)}")
print(f"安定な実装: {stable_softmax(logits).round(6)}")

print("\n=== LogSumExp ===")
def logsumexp_naive(x):
    return np.log(np.sum(np.exp(x)))

def logsumexp_stable(x):
    m = np.max(x)
    return m + np.log(np.sum(np.exp(x - m)))

x = np.array([1000.0, 1001.0, 1002.0])
with np.errstate(over='ignore', invalid='ignore'):
    print(f"素朴な実装: {logsumexp_naive(x)}")
print(f"安定な実装: {logsumexp_stable(x):.4f}")

print("\n=== 理由3:ロジット変換 ===")
print(f"{'確率p':>8} | {'オッズ':>12} | {'ロジット':>12}")
print("-" * 38)
for p in [0.01, 0.1, 0.25, 0.5, 0.75, 0.9, 0.99]:
    odds = p / (1 - p)
    logit = math.log(odds)
    print(f"{p:>8.2f} | {odds:>12.4f} | {logit:>12.4f}")

print("\n0から1の範囲が、実数全体に広がっています。")

print("\n=== ロジットからシグモイドへ ===")
def sigmoid(z):
    return 1 / (1 + np.exp(-z))

print(f"{'ロジットz':>10} | {'確率p':>10}")
print("-" * 24)
for z in [-4, -2, -1, 0, 1, 2, 4]:
    print(f"{z:>10} | {sigmoid(z):>10.4f}")

print("\n=== 理由4:偏った分布の整形 ===")
np.random.seed(0)
# 右に裾を引く分布を生成
income = np.random.lognormal(mean=6.0, sigma=1.0, size=10000)

print("元のデータ(年収のような分布)")
print(f"  平均: {income.mean():>12.2f}")
print(f"  中央値: {np.median(income):>10.2f}")
print(f"  最大値: {income.max():>10.2f}")
print(f"  歪度の目安(平均と中央値の比): {income.mean()/np.median(income):.4f}")

log_income = np.log(income)
print("\n対数変換後")
print(f"  平均: {log_income.mean():>12.4f}")
print(f"  中央値: {np.median(log_income):>10.4f}")
print(f"  最大値: {log_income.max():>10.4f}")
print(f"  平均と中央値の比: {log_income.mean()/np.median(log_income):.4f}")
print("\n平均と中央値が近づき、分布が対称に近づいています。")

print("\n=== 0を含むデータへの対処 ===")
data = np.array([0, 1, 10, 100, 1000])
print(f"元のデータ: {data}")
with np.errstate(divide='ignore'):
    print(f"log(x):     {np.log(data)}")
print(f"log1p(x):   {np.log1p(data).round(4)}")
print("log1pを使えば、0を含んでいても計算できます。")

print("\n=== 掛け算の関係が線形になる ===")
x = np.array([1, 2, 4, 8, 16, 32], dtype=float)
y = 3 * x ** 2   # y = 3x^2

print(f"{'x':>6} | {'y':>10} | {'log(x)':>10} | {'log(y)':>10}")
print("-" * 42)
for xi, yi in zip(x, y):
    print(f"{xi:>6.0f} | {yi:>10.0f} | {math.log(xi):>10.4f} | {math.log(yi):>10.4f}")

# log(y) と log(x) の関係を線形回帰で確認
coeffs = np.polyfit(np.log(x), np.log(y), 1)
print(f"\nlog(y) = {coeffs[0]:.4f} * log(x) + {coeffs[1]:.4f}")
print(f"傾きが2に近いことから、y = a * x^2 の関係が読み取れます。")

print("\n=== 理由5:桁の比較 ===")
print("学習の損失の推移(模擬データ)")
losses = [100.0, 30.0, 8.0, 2.0, 0.5, 0.1, 0.03, 0.008, 0.002, 0.0005]

print(f"{'エポック':>8} | {'損失':>10} | {'log10(損失)':>14} | {'前回比':>10}")
print("-" * 50)
for i, loss in enumerate(losses):
    ratio = losses[i-1] / loss if i > 0 else float('nan')
    ratio_str = f"{ratio:.2f}倍" if i > 0 else "-"
    print(f"{i:>8} | {loss:>10.4f} | {math.log10(loss):>14.4f} | {ratio_str:>10}")

print("\n通常の目盛りでは後半の変化が見えませんが、")
print("対数を取ると、一定のペースで減少していることが分かります。")

print("\n=== 学習率の候補を対数スケールで選ぶ ===")
linear_candidates = np.linspace(0.00001, 0.1, 5)
log_candidates = np.logspace(-5, -1, 5)

print(f"等間隔で選んだ場合: {linear_candidates}")
print(f"対数スケールで選んだ場合: {log_candidates}")
print("\n対数スケールのほうが、桁を広くカバーできます。")

print("\n=== 交差エントロピー誤差における対数 ===")
print("正解クラスの予測確率と、そのときの損失")
print(f"{'予測確率':>10} | {'-log(p)':>10} | {'評価':>16}")
print("-" * 42)
for p in [0.99, 0.9, 0.5, 0.1, 0.01, 0.001]:
    loss = -math.log(p)
    if p >= 0.9:
        comment = "ほぼ正解"
    elif p >= 0.5:
        comment = "まあまあ"
    elif p >= 0.1:
        comment = "外している"
    else:
        comment = "確信を持って誤り"
    print(f"{p:>10.3f} | {loss:>10.4f} | {comment:>16}")

print("\n自信を持って間違えるほど、損失が急激に増えます。")

print("\n=== PyTorchでの実装確認 ===")
try:
    import torch
    import torch.nn.functional as F
    
    logits = torch.tensor([[100.0, 101.0, 102.0]])
    
    print("--- 分けて計算した場合 ---")
    probs = F.softmax(logits, dim=1)
    log_probs_split = torch.log(probs)
    print(f"log(softmax(x)): {log_probs_split.numpy().round(6)}")
    
    print("\n--- まとめて計算した場合 ---")
    log_probs_together = F.log_softmax(logits, dim=1)
    print(f"log_softmax(x):  {log_probs_together.numpy().round(6)}")
    
    print("\n--- 極端な値での比較 ---")
    extreme = torch.tensor([[1000.0, 0.0, -1000.0]])
    probs_e = F.softmax(extreme, dim=1)
    print(f"softmax後にlog:  {torch.log(probs_e).numpy()}")
    print(f"log_softmax:     {F.log_softmax(extreme, dim=1).numpy()}")
    print("\n分けて計算すると、マイナス無限大が発生します。")
    
except ImportError:
    print("PyTorchがインストールされていないため、スキップします。")

初学者がつまずきやすい点

つまずき1:すべてのlogに同じ理由があると思う

冒頭で述べたとおり、場面によって理由が異なります。

前処理での対数変換と、損失関数の中の対数は、まったく別の目的です。

前者は、データの分布を整えるためです。後者は、確率の掛け算を足し算に変えるためです。

混同すると、「なぜここでlogを取るのか」が理解できなくなります。

つまずき2:対数を取ると値が変わることを気にしすぎる

対数を取れば、値そのものは変わります。

しかし、最大化や最小化の問題では、最適な位置が変わらなければ問題ありません。

対数が単調増加であるという性質が、これを保証しています。

つまずき3:0以下の値に対数を取ろうとする

対数は、正の値にしか定義されません。

前処理で対数変換を行う際、データに0や負の値が含まれていないかを、必ず確認してください。

0が含まれる場合は、1を足してから対数を取る方法が使われます。負の値が含まれる場合は、そもそも対数変換が適切でない可能性を検討してください。

つまずき4:底の違いを気にしすぎる

自然対数、常用対数、2を底とする対数。どれを使うかで、値は定数倍だけ変わります。

\log_2 x = \frac{\ln x}{\ln 2}

最大化や最小化の問題では、定数倍の違いは結果に影響しません。

情報量を扱う場面ではビット単位に対応する2を底とする対数、それ以外では微分が扱いやすい自然対数が使われることが多い、という程度の理解で十分です。

演習

演習1:掛け算と足し算の対応を確認する

次の確率について、掛け算の結果と、対数の和を計算し、対応を確認してください。

0.5, 0.4, 0.8

\log 0.5 \approx -0.693\log 0.4 \approx -0.916\log 0.8 \approx -0.223 を使ってください。

演習2:ロジットを計算する

次の確率について、オッズとロジットをそれぞれ計算してください。

p = 0.8

p = 0.2

二つの結果の間に、どのような関係があるか説明してください。

演習3:交差エントロピー誤差を計算する

3クラス分類で、正解が2番目のクラスだとします。

モデルの予測確率が [0.1, 0.7, 0.2] のとき、交差エントロピー誤差を計算してください。

また、予測確率が [0.6, 0.2, 0.2] のときも計算し、比較してください。

\log 0.7 \approx -0.357\log 0.2 \approx -1.609 を使ってください。

演習4:対数変換の効果を確認する

import numpy as np

np.random.seed(42)

# 右に大きく裾を引くデータ
data = np.random.lognormal(mean=5, sigma=1.5, size=5000)

print("=== 元のデータ ===")
print(f"最小値: {data.min():>12.2f}")
print(f"25%点:  {np.percentile(data, 25):>12.2f}")
print(f"中央値: {np.median(data):>12.2f}")
print(f"75%点:  {np.percentile(data, 75):>12.2f}")
print(f"最大値: {data.max():>12.2f}")
print(f"平均:   {data.mean():>12.2f}")

print("\n=== 対数変換後 ===")
log_data = np.log(data)
print(f"最小値: {log_data.min():>12.4f}")
print(f"25%点:  {np.percentile(log_data, 25):>12.4f}")
print(f"中央値: {np.median(log_data):>12.4f}")
print(f"75%点:  {np.percentile(log_data, 75):>12.4f}")
print(f"最大値: {log_data.max():>12.4f}")
print(f"平均:   {log_data.mean():>12.4f}")

print("\n=== ヒストグラムを文字で表示 ===")
def text_histogram(values, bins=15, width=50):
    counts, edges = np.histogram(values, bins=bins)
    max_count = counts.max()
    for i in range(bins):
        bar = "#" * int(counts[i] / max_count * width)
        print(f"{edges[i]:>10.2f} | {bar}")

print("元のデータ:")
text_histogram(data)
print("\n対数変換後:")
text_histogram(log_data)

演習の解答例と解説

演習1の解答は、次のとおりです。

掛け算の結果は、次のようになります。

0.5 \times 0.4 \times 0.8 = 0.16

対数の和は、次のようになります。

-0.693 + (-0.916) + (-0.223) = -1.832

検算します。

\log 0.16 \approx -1.833

小数点以下の丸め誤差を除けば、一致しています。

掛け算が足し算に対応していることが確認できます。

演習2の解答は、次のとおりです。

p = 0.8 の場合。

オッズは \frac{0.8}{0.2} = 4 です。

ロジットは \log 4 \approx 1.386 です。

p = 0.2 の場合。

オッズは \frac{0.2}{0.8} = 0.25 です。

ロジットは \log 0.25 \approx -1.386 です。

二つの結果は、符号だけが逆になっています。

これは偶然ではありません。0.8と0.2は、0.5を挟んで対称な位置にあります。ロジット変換では、確率0.5が0に対応し、そこから対称な確率は、符号が反転した同じ大きさのロジットになります。

演習3の解答は、次のとおりです。

予測確率が [0.1, 0.7, 0.2] の場合、正解である2番目の確率は0.7です。

L = -\log 0.7 \approx 0.357

予測確率が [0.6, 0.2, 0.2] の場合、正解である2番目の確率は0.2です。

L = -\log 0.2 \approx 1.609

後者の損失は、前者の約4.5倍です。

注目すべき点は、後者では1番目のクラスに0.6という高い確率を割り当てていることです。自信を持って間違えているため、大きな罰が与えられています。

なお、正解以外のクラスの確率は、計算に一切影響していません。One-Hot表現では、正解以外のラベルが0であるため、掛け算の結果が0になるからです。

演習4では、元のデータが極端に右に裾を引いていること、そして対数変換によって、より対称的な分布に近づくことが確認できます。

文字で表示したヒストグラムを見比べると、変換前は左端に集中していた分布が、変換後は中央付近を頂点とする山型に近づいているはずです。

まとめ

機械学習で対数を取る理由は、一つではありません。場面によって、目的が異なります。

第一の理由は、掛け算を足し算に変えることです。確率の積として表される尤度を、対数を取ることで和に変換し、微分と計算を簡単にします。対数は単調増加であるため、最大値を取るパラメータの位置は変わりません。交差エントロピー誤差の式も、この考え方から導かれます。

第二の理由は、数値計算の安定化です。多数の確率を掛け合わせると、コンピュータが表現できる下限を下回り、0になってしまいます。対数の世界であれば、扱いやすい大きさの値になります。逆に、指数関数によるオーバーフローに対しては、最大値を引いてから計算するという工夫が使われます。

第三の理由は、値の範囲を制限から解放することです。0から1に限られる確率を、ロジット変換によって実数全体に写します。これにより、線形結合の出力と確率を結びつけられるようになります。ロジスティック回帰の基礎です。

第四の理由は、偏った分布の整形です。年収や売上のように右に裾を引くデータは、対数変換によって、より対称的な分布に近づきます。また、掛け算やべき乗の関係を、線形の関係に変換する効果もあります。0が含まれる場合は、1を足してから対数を取ってください。

第五の理由は、桁の違うものを比べやすくすることです。損失のグラフを対数目盛りで描けば、後半の細かな変化も見えるようになります。学習率のようなハイパーパラメータを探索する際も、対数スケールで候補を選びます。

logを見かけたら、この五つのうちどれに該当するかを考えてください。それが分かれば、その式が何をしようとしているかが読み取れるようになります。

次の学習ステップとして、まずは手元で、確率を何度も掛け合わせるとどこで0になるかを確認してみてください。そのうえで、同じ計算を対数の世界で行い、値が扱える範囲に収まることを確認すると、数値安定性の重要さが実感できます。さらに、実際のデータセットで年収や売上のような列を探し、対数変換前後のヒストグラムを比較してみると、前処理としての効果が目で見て理解できるようになります。

セイ・コンサルティング・グループでは新人エンジニア研修のアシスタント講師を募集しています。

投稿者プロフィール

山崎講師
山崎講師代表取締役
セイ・コンサルティング・グループ株式会社代表取締役。
岐阜県出身。
海外放浪の末、2000年創業、2004年会社設立。
IT企業向け人材育成研修歴業界歴20年以上。
すべての無駄を省いた費用対効果の高い「筋肉質」な研修を提供します!
この記事に間違い等ありましたらぜひお知らせください。

学生時代は趣味と実益を兼ねてリゾートバイトにいそしむ。長野県白馬村に始まり、志賀高原でのスキーインストラクター、沖縄石垣島、北海道トマム。高じてオーストラリアのゴールドコーストでツアーガイドなど。現在は野菜作りにはまっている。