Python
JavaScript
アルゴリズム
機械学習
数学

今度こそ分かる、対数関数(log関数)

対数関数

高校数学で習ったような気がする、対数関数。
忘れてしまった人も多いのではないでしょうか。

「こんなもの覚えて何の役に立つんだ」と思って高校の授業を受けていた人もいたかもしれません。
実際のところ、日常生活にはそんな言うほど役に立たないですが、コンピュータの世界では、たまに出てきます。
具体的には、統計や機械学習の式の中に出てきたり、アルゴリズムの話で、計算量のオーダーで O(log N) などと出てきたりです。

ここでは、対数関数って一体何なのか、どういう性質があるのかを見ていきます。
コードはPythonとJavaScriptで書いていますが、大抵の言語では同じようなものが用意されています。
納得するまで、手元で動かしてみることをおすすめします。

雑に言っちゃうと、桁数を求める関数です。

まずは、一番分かりやすい、「常用対数」と呼ばれるものを見ていきます。
常用対数はPythonではmath.log10 (numpyだとnumpy.log10)、JavaScriptではMath.log10です。

[Python]log10を使う
import math
[math.log10(x) for x in (1, 10, 100, 1000, 10000)]
# => [0.0, 1.0, 2.0, 3.0, 4.0]
[JS]log10を使う
[1, 10, 100, 1000, 10000].map(Math.log10)
// =>  [ 0, 1, 2, 3, 4 ]

逆に、0.1, 0.01, 0.001だとどうなるでしょう?

[JS]log10を使う_Part2
[0.1, 0.01, 0.001, 0.0001].map(Math.log10)
// => [ -1, -2, -3, -4 ]

マイナスになりました。

じゃあ、log10(0)log10(-1)は?

[Python]log10(0)
math.log10(0)
# => ValueError: math domain error
math.log10(-1)
# => ValueError: math domain error

おっ??

[JS]log10(0)
Math.log10(0)
// => -Infinity
Math.log10(-1)
// => NaN

おやおや??!

ちょっとプロットしてみましょう。
Screenshot_20180728_171437.png

log10は、(0を含まない)正の数でのみ定義されているようです。0に近づくにつれ小さくなります。また、先程みたように、log10(1)が0で、それより小さくなるとマイナスになります。

さっきは1, 10, 100, 1000などのキリのいい数ばかり試しましたが。もうちょっと試してみましょう。

[Python]キリの悪い数字でも試してみる
[math.log10(x) for x in range(1, 11)] # log10([1, 2, ..., 9, 10])
# => [0.0, 0.3010299956639812, 0.47712125471966244, 0.6020599913279624, 0.6989700043360189, 0.7781512503836436, 0.8450980400142568, 0.9030899869919435, 0.9542425094393249, 1.0]

[math.log10(x*10) for x in range(1, 11)] # log10([10, 20, ..., 90, 100])
# => [1.0, 1.3010299956639813, 1.4771212547196624, 1.6020599913279625, 1.6989700043360187, 1.7781512503836436, 1.845098040014257, 1.9030899869919435, 1.9542425094393248, 2.0]

ふーん、へー。ほー。

実は、y = log10(x)とは、10y=xになるyを探す計算なのです。

[Python]log10の検算
y = math.log10(1); print(y) # => 0.0
print(10**y) #=> 1.0

y = math.log10(10); print(y) # => 1.0
print(10**y) #=> 10.0

y = math.log10(0.1); print(y) # => -1.0
print(10**y) #=> 0.1

y = math.log10(2); print(y) # => 0.3010299956639812
print(10**y) #=> 2.0

浮動小数点なので、完全には一致しない可能性がありますが、概ねこうなります。

log10? 知りたいのはlogなんだけど?

log10の説明で、y = log10(x)とは10y=xを探す関数だ、と書きました。
loglog10の違いは、上式の10の部分を何の数字にするかの違いです。

実は、この部分が何の数字になるのかは、分野によって違ったりします。(この数字を「対数の底」と呼びます)

  • 工学の分野には、10になる、つまりlog10と同じになる流儀が存在するようです
  • 情報の分野では、2になる流儀が存在します
  • 理学、数学では「ネイピア数」と呼ばれるものにすることが多いです(ネイピア数を対数の底とする対数を「自然対数」と呼びます)。また、自然対数をlogで書かずにlnxのように表記する流儀もあります。

Python, JavaScriptでは、math.log あるいは Math.log は、自然対数が使われています。
(ただし、Pythonでは、第2引数に対数の底を指定することも可能です)

なお、ネイピア数は、Pythonでは math.e、JavaScriptではMath.Eで定義されていて、2.718281828459045...のようになっています。
また、指数関数(exp)は、ネイピア数をeとして exp(x)=exという関係を持っています。

この数字は、対数関数を微分しているうちに出てくるんですが、「この意味不明な数字にしておくと、数学的に都合がいいらしい」という雑な理解で構いません。
(ちゃんと理解したい方には、こちらのブログ記事がお勧めです)

常用対数と自然対数の関係

実は定数倍です。

[JavaScript]常用対数と自然対数
[0.1, 0.2, 1, 2, 10, 20, 100, 200].map(Math.log10)
// => [ -1, -0.6989700043360187, 0, 0.3010299956639812, 1, 1.3010299956639813, 2, 2.3010299956639813 ]
[0.1, 0.2, 1, 2, 10, 20, 100, 200].map(Math.log)
// => [ -2.3025850929940455, -1.6094379124341003, 0, 0.6931471805599453, 2.302585092994046, 2.995732273553991, 4.605170185988092, 5.298317366548036 ]

// 試しに、割り算してみる。
[0.1, 0.2, 1, 2, 10, 20, 100, 200].map((x)=>Math.log10(x)/Math.log(x))
// => [ 0.43429448190325187, 0.4342944819032518, NaN, 0.43429448190325187, 0.43429448190325176, 0.43429448190325187, 0.43429448190325176, 0.43429448190325187 ]
// 0/0が発生したので、NaNがあるけど、どれもだいたい0.4342944...倍
Math.log10(Math.E) // => 0.4342944819032518 おお!!

常用対数と自然対数に限らず、次のような数式で、底の変換ができます。(ただし、ゼロ除算には気をつけて下さい)

logax=logbxlogba

ここで、logの横に小さく書いてある数字が対数の底です。

logを使って、常用対数を求めてみましょう。

[JavaScript]自然対数を使って常用対数を求める
[10, 100, 1000, 10000].map((x)=>Math.log(x)/Math.log(10))
// => [ 1, 2, 2.9999999999999996, 4 ]

若干の誤差はありますが、出ました。

対数を使うと何が嬉しいか?

いろんな場面で対数関数は出てくるのですが、一体なんでこんなの使うんでしょう?

グラフを描くとき

数字がクソでかくなる場合、y軸を対数スケールにすると分かりやすくなることがあります。ムーアの法則のように指数関数的に増えるものは、対数スケールにすると(すごく上がってる感は薄くなりますが)見やすくなります。
また、通常は為替には対数スケールは使いませんが、Wikipediaのジンバブエ・ドルの項目には、ハイパーインフレが起こった際の為替を対数スケールで表示したグラフが載っています。

x軸も対数スケールにした「両対数グラフ」というものも、たまに使われます。
例えばy=xkのグラフを両対数グラフで表すとしましょう。Y=logy, X=logxと置くと、両対数グラフでは、logy=logxkY=kXのような形になり、kが傾きとして出てきます。(logxk=klogxという公式を使っています)

掛け算、割り算を多用するとき

対数には、禿げ上がるほどクソ便利な公式があります。

logxy=logx+logy
logxy=logxlogy

掛け算が足し算に、割り算が引き算になるんです。

機械学習などで使う、対数尤度関数なんかは、尤度関数だと掛け算を多用するから、それを足し算に変換できるのは嬉しいですね。
1以下の数字を何度も掛け算していくと、どんどん0に近づいていって計算できなくなるので、対数を取っているという事情もありそうなので、そういう意味では前に挙げた例と同じなのですが。
機械学習の本とかを読んでいて、掛け算がなぜか足し算になっていることに戸惑った人もいるかもしれないので。。。

アルゴリズムの計算量では勝手に出てくることも

例えば、2分法での検索を考えましょう。ソートされた配列のどこに、目的の数字があるのかを探す場合、まず真ん中を見て、それよりも小さいか大きいかで、さらにその真ん中を見るのが2分法でした。
N要素の中から目的の数字を探す場合に、何回見たら答えが分かるでしょうか? 1回見るたびに、候補を半分に減らせるので、見る回数をxとすると
2x=N
ということになります。

これは、対数を使って x=log2Nと表せます。

まとめ

  • 対数は、雑に言うと、桁数を求める関数。対数の底によって定数が掛かっている
  • 0やマイナスの値は対数を取れないので気をつける
  • 掛け算が足し算になるなどの便利な公式がある
  • 大きい数字や、ゼロに近い数字を扱いやすくするのによく使われる
  • 対数は怖くない