小さな変化を計算したはずなのに、答えがゼロになることがあります。例えば、x=10のマイナス16乗を入れて「eのx乗から1を引く」と、数学では正の値になるのに、通常の浮動小数点計算では0と表示される場合があります。
このような式に用意されているのがexpm1とlog1pです。前者はexp(x)−1、後者はlog(1+x)を求める専用関数です。式をそのまま二段階で計算する場合と何が違うのか、Pythonの標準ライブラリと独自の実行例で確かめます。
簡単にまとめると?
- 浮動小数点では、1の近くに表せる値の間隔があります。微小な変化が途中で丸められると、最後に引き算をしても取り戻せません。
- expm1(x)とlog1p(x)は、ゼロに近いxから小さな結果を求めるための関数です。単にexpやlogを呼んだ後で表示桁数を増やす方法とは異なります。
- 専用関数へ渡す前に入力の差が消えていれば、回復はできません。元のxを保持し、入力範囲と使っている数値型も確認します。
1の隣にある数まで、間隔がある
ここで使うbinary64は、2進数で有限の桁数を保存する浮動小数点形式です。有効桁は53ビットで、あらゆる実数をそのまま保存できるわけではありません。計算結果が表せる値の間に来たときは、近くの値へ丸められます。
1.0より大きい側で、次に表せる値までの差は2のマイナス52乗、約2.22×10のマイナス16乗です。今回のx=1e-16はその半分より小さいため、通常の最近接丸めでは、1.0+xは再び1.0になります。これは「小さすぎる数は何でも0になる」という話ではありません。1e-16自体は保存できても、1.0に加えた結果としては、その微小な違いを残せないのです。
この違いは、表示の問題と区別します。値が内部で1.0へ丸められた後に、小数点以下を長く表示しても、失われた差は戻りません。
expの結果を丸めてから引くと、差だけが消える
数学では、ゼロに近いxについてexp(x)は1に近く、exp(x)−1はおよそxになります。ところが、先にexp(x)の結果をbinary64へ丸めると、xが十分小さい場合、その結果が1.0になってしまいます。続けて1.0を引けば、結果は0です。
注意したいのは、最後の引き算だけが誤っているわけではないことです。すでに丸められた1.0から1.0を引く処理は、その二つの数については正確です。求めたかった微小な差は、その前の段階で失われています。近い値の差を取ると、元の値にとっては小さな丸め誤差が、差にとっては大きな相対誤差になることが問題です。
math.expm1(x)は、この差を一つの関数として求めます。小さなxに適した計算を使えるため、いったん丸めたexp(x)から単純に1を引く方法で失われる情報を残せます。内部の計算法はライブラリや環境によるので、特定の級数を必ず使うとは限りません。
同じ入力で計算すると、何が変わるか
以下はQSでPython 3.12.14・macOSの環境で実行したbinary64の例です。表では読みやすい桁数へ丸めています。1e-16は10のマイナス16乗を表す表記です。
| 入力 x | exp(x)−1 | expm1(x) | log(1+x) | log1p(x) |
|---|---|---|---|---|
| 1e-8 | 約9.99999994e-9 | 約1.000000005e-8 | 約9.99999989e-9 | 約9.99999995e-9 |
| 1e-12 | 約1.00008890e-12 | 約1.00000000e-12 | 約1.00008890e-12 | 約1.00000000e-12 |
| 1e-16 | 0 | 約1e-16 | 0 | 約1e-16 |
入力が1e-12の場合、二段階のexp(x)−1では、同じ入力を80桁精度で計算した参照値に対する相対誤差が約0.00889%でした。絶対的には小さな差でも、求めたい結果自体が小さいと、無視できるかどうかの判断は変わります。

参照計算には、入力のfloatをDecimal.from_floatでそのまま80桁精度の計算へ移しています。十進表記の1e-12を理想的な実数として扱った値との比較ではなく、同じ表現済み入力に対する演算の違いを比べるためです。最後の数ビットは実行環境によって変わり得るため、この表をすべての数値ライブラリの結果保証としては使えません。
log1pには、1を足す前のxを渡す
log1pのlogは自然対数です。ゼロに近いxではlog(1+x)もおよそxになります。しかし先に1.0+xが1.0へ丸められると、logへ渡るのは1.0であり、log(1.0)は0になります。
math.log1p(x)には、元のxをそのまま渡します。例えば、いったんy = 1.0 + xを作り、その後でmath.log1p(y - 1.0)と書き換えても、yがすでに1.0なら入力は0です。関数名を置き換えるだけでは、前段で消えた情報を取り戻せません。
実装を点検するときは、最終行だけでなく、小さな変化をどこで作り、どこで大きな基準値へ足したかを追います。変化量xを独立して保持できるなら、その形のまま専用関数へ渡すことが選択肢になります。
小さな減少量にも、式を組み替えて使える
例えば、非負の小さなxに対して、残る割合をexp(−x)と置いた数式モデルを考えます。そこから減った割合は1−exp(−x)です。この値を計算したいなら、-math.expm1(-x)と書くことで、1に近い二つの値を最後に引く形を避けられます。
x=1e-16のとき、この専用関数を使った結果は約1e-16です。この例は式の評価方法を示すもので、実物の減衰や装置性能を測定したものではありません。数式が対象に合っているかというモデルの問題と、その数式を精度よく計算できるかという数値計算の問題は、それぞれ確認が必要です。
専用関数でも解決しない境界
expm1やlog1pも有限精度で動きます。入力そのものの丸め、さらに小さな値での表現限界、大きな正のxでの指数関数のオーバーフローまで、すべてをなくすわけではありません。実数としてlog1pを使える範囲はx>−1であり、Pythonのmathでは範囲外や境界の扱いも確認する必要があります。
判断の順番は、まず必要な量がexp(x)−1やlog(1+x)の形になっているか、次にxがゼロに近い領域を通るか、そして元のxを保持して渡せるか、です。必要な誤差水準に照らして、代表値だけでなくゼロ付近や境界でも比較します。数学的な式が正しくても、そのままの計算順序が欲しい小さな差を残せるとは限りません。
出典
- Python 3.14.9 documentation — math:expm1、log1p、ulpおよび実装上の注意。確認時の公式文書の版表示。
- Python 3.14.9 documentation — Floating-Point Arithmetic: Issues and Limitations:2進浮動小数点の表現と丸め。本文の数値比較・表・図はQS独自実行によるもの。
