numpy.arange関数におけるstepに0.1を指定したときの振る舞いとその原因を追ってみた話

はじめに

先日、接点QB氏による以下のツイートを見かけた:

興味深いので少し調べてみたというわけである(少々長い記事)。

numpyのarange関数の振る舞い

numpyのarange関数のステップ数に0.1を指定したときの振る舞いについて、

np.arange(1.0, 1.5, 0.1)

とすると、0.1刻みで1.0, 1.1, 1.2, 1.3, 1.4までのarrayが得られた。array要素の最大値1.4は引数stopの1.5より小さいというのが、想定される挙動である。

一方、

np.arange(1.0, 1.6, 0.1)

とすると、0.1刻みで1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6のように、array要素の最大値が1.5を超えて1.6に達したので困惑したということである(「1.0, 1.1, 1.2, 1.3, 1.4, 1.5」かと思ったのに!)。

原因を探るため、numpyのマニュアルのarangeの項を見てみた。 numpy.org

すると、戻り値のarrayについて以下の記述が見つかった:

For floating point arguments, the length of the result is ceil((stop - start)/step).

つまり、浮動小数点数をarange関数の引数に与えた場合、array長がceil(=小数点以下切り上げ)を伴う式で計算されるということだった。

手元で挙動を試してみると、以下のようになった。

つまり、(1.5 - 1.0) / 0.1の計算結果が5.0となる一方で、(1.6 - 1.0) / 0.1の計算結果が6.000000000000001となり6.0にならない。

したがってceil関数を通したとき、

ceil((1.5 - 1.0) / 0.1) は5になるが、ceil((1.5 - 1.0) / 0.1) は7になった

というわけである。

問題は「なぜこのような計算結果が得られたのか」ということである。原因を一言で言えば浮動小数点数演算に由来する計算誤差」ということに尽きるが、本記事ではこれを詳細に追ってみた(浮動小数点数の復習問題!)。

浮動小数点数の表現形式と演算

さて、浮動小数点数のIEEE754倍精度形式は次式で表される。 $$(-1)^{sgn} \times 2^{(exp - 1023)} \times (1+frac×2^{-52})$$ $sgn$は符号部、$exp$は指数部、$frac$は仮数部である(ここでは10進数表現)。

この表現形式を元にして、計算を追っていこうという方針である。 ここでは以下の記事を参考にした。 qiita.com

浮動小数点数の倍精度表現

検証にあたり、接点QB氏の具体例に基づいて、対象とする浮動小数点数

0.1, 1.0, 1.5, 1.6

の4つに限定する。

0.1の倍精度表現

0.1の倍精度表現は

符号部 = 0
指数部 = 01111111011
仮数部 = 1001100110011001100110011001100110011001100110011010

となり、これらを10進数に変換すると

符号部 = 0
指数部 = 1019
仮数部 = 2702159776422298

となる。0.1の倍精度表現は以下のように計算できる。 $$ (-1)^{0} \times 2^{1019 - 1023} \times (1 + 2702159776422298 \times 2^{-52}) $$ $$ \begin{aligned} &= 1 \times 2^{-4} \times (2^{52} + 2702159776422298) \times 2^{-52} \\ &= (4503599627370496 + 2702159776422298) \times 2^{-56} \\ &= 7205759403792794 / 72057594037927936 \end{aligned} $$

1.0の倍精度表現

1.0の倍精度表現は

符号部 = 0
指数部 = 01111111111
仮数部 = 0000000000000000000000000000000000000000000000000000

であり、10進数に変換すると

符号部 = 0
指数部 = 1023
仮数部 = 0

となる。1.0の倍精度表現は以下のように計算できる。 $$ (-1)^{0} \times 2^{1023 - 1023} \times (1 + 0 \times 2^{-52}) $$ $$ \begin{aligned} &= 1 \\ &= \frac{2^{52}}{2^{52}} \\ &= 4503599627370496 / 4503599627370496 \end{aligned} $$

1.5の倍精度表現

1.5の倍精度表現は

符号部 = 0
指数部 = 01111111111
仮数部 = 1001100110011001100110011001100110011001100110011010

となり、これらを10進数に変換すると

符号部 = 0
指数部 = 1023
仮数部 = 2251799813685248

となる。 1.5の倍精度表現は以下のように計算できる。 $$ (-1)^{0} \times 2^{1023 - 1023} \times (1 + 2251799813685248 \times 2^{-52}) $$ $$ \begin{aligned} &= 1 \times 2^{0} \times (2^{52} + 2251799813685248) \times 2^{-52} \\ &= (4503599627370496 + 2251799813685248) \times 2^{-52} \\ &= 6755399441055744 / 4503599627370496 \end{aligned} $$

1.6の倍精度表現

1.6の倍精度表現は

符号部 = 0
指数部 = 01111111111
仮数部 = 1001100110011001100110011001100110011001100110011010

となり、これらを10進数に変換すると

符号部 = 0
指数部 = 1023
仮数部 = 2702159776422298

となる。 1.6の倍精度表現は以下のように計算できる。 $$ (-1)^{0} \times 2^{1023 - 1023} \times (1 + 2702159776422298 \times 2^{-52}) $$ $$ \begin{aligned} &= 1 \times 2^{0} \times (2^{52} + 2702159776422298) \times 2^{-52} \\ &= (4503599627370496 + 2702159776422298) \times 2^{-52} \\ &= 7205759403792794 / 4503599627370496 \end{aligned} $$

倍精度表現に基づく浮動小数点数演算

以上で倍精度表現が揃ったので、検証に入ることができる(前置き長すぎワロタ)。 表に示しておく。

浮動小数点数 倍精度表現
0.1 7205759403792794 / 72057594037927936
1.0 1 (=4503599627370496 / 4503599627370496)
1.5 6755399441055744 / 4503599627370496
1.6 7205759403792794 / 4503599627370496

以上の表に基づいて、(1.5 - 1.0) / 0.1 を計算すると

((6755399441055744  - 4503599627370496) / 4503599627370496) / (7205759403792794 / 72057594037927936)
= 5

が得られた。ぴったし5である。

一方、(1.6 - 1.0) / 0.1 については

((7205759403792794 - 4503599627370496) / 4503599627370496) / (7205759403792794 / 72057594037927936)
= 6.000000000000001

と計算できた。

以上の計算結果に基づいてceilは小数点以下を切り上げるため、冒頭で示した挙動が説明できたわけである。

まとめ

本記事ではarange関数の引数のひとつであるstep数に浮動小数点数を適用した場合に、 戻り値のarrayの要素数がどのように変わりうるかを、具体的な数値例を元に調べてみた。

Numpyのマニュアルによれば、arangeによる戻り値のarrayの要素数はceil((stop - start)/step)で計算され、これらstart, stop, stepはarange関数の引数として与えられる。

接点QB氏のツイートをもとに、arangeの引数が具体的に

  • (1) start=1.0, stop=1.5, step=0.1
  • (2) start=1.0, stop=1.6, step=0.1

の場合に、浮動小数点数の倍精度表現に基づいて要素数を計算し、arange関数の振る舞いを検証した。 検証の結果、確かにPythonの計算結果は正しかったことが確かめられた。

Numpyマニュアルにも浮動小数点数が引数の場合はlinspace使ったほうがいいよ的な記述もあり、arangeを使うときには思い込みによるバグを埋め込まないように気をつけたい。

参考

わかりやすいパターン認識(第2版)の書評

本書は1998年に発売された『わかりやすいパターン認識』の改訂版であり,全体を通して統計的パターン認識の基礎が「わかりやすい」筆致でまとめられている。本書は全9章から構成されている。パターン認識の考え方(第1章)から始まり,識別関数の学習・評価・設計法(第2~4章),特徴抽出・次元削減(第5~7章)など,統計的パターン認識の分野における基礎的かつ重要な話題が精選されている。第8章では期待損失学習,第9章ではベイズ決定則と学習アルゴリズムの関係など,高度な話題にも触れられている。「統計的パターン認識ベイズ決定則を土台とする一学問」というフレーズには、改訂前から強い感銘を受けた。

改訂前後の大きな違いは,各章に数値計算・実験例や演習問題が数多く追加されていることである。例えば第2章には,パーセプトロンの学習過程が具体的な数値例とともに説明されており,第3章には3層ニューラルネットの実験例などが追加されている。演習問題では,具体的な数値例を与えて学習アルゴリズムの動作を確認させるものや,本文中の数式を具体的に導出させるものがある。詳細な解答例を載せたPDFが出版社のサポートページで公開されているのがありがたい。著者らの知見やノウハウを述べたCoffee Breakの欄にも加筆修正がされており,深層学習についても触れられている。「むすび」には,旧版以降に出版された参考書がコメントつきで紹介されており,初学者への良き指針となるだろう。

旧版は近年の深層学習・機械学習ブーム以前に発行されているが,本改訂で新たな章を割いて取り入れた話題はない。ただし深層学習を理解する上で重要な,「浅い」ニューラルネットであるパーセプトロンや3層ニューラルネット誤差逆伝播法などに多くの紙面を割いて説明しており,本書発行の主旨を考えれば欠点にはあたらない。本書で取り上げられるトピックは技術の進展を経ても陳腐化しないものばかりが精選されているのである。

パターン認識に関心を持つ各位に本書の一読を広く薦めたい。

現在開いているページのewwバッファを複製する

ewwを起動中、以下の関数を実行する。
すると、現在のページのURLを引き継いで新しくewwバッファが作成される。

(defun eww-duplicate-buffer ()
  "Duplicate current eww buffer."
  (interactive)
  (let ((url (plist-get eww-data :url)))
    (with-current-buffer (clone-buffer)
      (eww (if (consp url) (car url) url)))))

リージョンをクオートやブラケットで囲う(Emacsのselected.elを利用)

以下の記事を参考に、リージョンを{クオートやブラケット}で囲うelispをselectedを使って書いてみたということ。

リージョンを選択し、クオートやブラケットの記号を一つ押すだけで、リージョン全体が囲えるので便利である。

selected.elはMELPAからインストール可能。もしくは以下からダウンロード。

コードはこんな感じ。オリジナルの記事ではバッククオートがなかったため、新たに追加してみた。応用すれば、ブレースなど他の記号でも実現できるだろう。

(defun region-to-single-quote ()
  (interactive)
  (quote-formater "'%s'" "^\\(\"\\).*" ".*\\(\"\\)$"))
(defun region-to-double-quote ()
  (interactive)
  (quote-formater "\"%s\"" "^\\('\\).*" ".*\\('\\)$"))
(defun region-to-back-quote ()
  (interactive)
  (quote-formater "`%s`" "^\\('\\).*" ".*\\('\\)$"))
(defun region-to-bracket ()
  (interactive)
  (quote-formater "\(%s\)" "^\\(\\[\\).*" ".*\\(\\]\\)$"))
(defun region-to-square-bracket ()
  (interactive)
  (quote-formater "\[%s\]" "^\\(\(\\).*" ".*\\(\)\\)$"))
(defun region-to-brace ()
  (interactive)
  (quote-formater "\%s\]" "^\\(\(\\).*" ".*\\(\)\\)$"))
(defun quote-formater (quote-format re-prefix re-suffix)
  (if mark-active
      (let* ((region-text (buffer-substring-no-properties
                           (region-beginning) (region-end)))
             (replace-func
              (lambda (re target-text)
                (replace-regexp-in-string re "" target-text nil nil 1)))
             (text (funcall replace-func re-suffix
                            (funcall replace-func re-prefix region-text))))
        (delete-region (region-beginning) (region-end))
        (insert (format quote-format text)))
    (error "Not Region selection")))

(when (require 'selected nil t)
  ;; リージョンをシングルクオートで囲う
  (define-key selected-keymap (kbd "\'") #'region-to-single-quote)

  ;; リージョンをダブルクオートで囲う
  (define-key selected-keymap (kbd "\"") #'region-to-double-quote)

  ;; リージョンバッククオートで囲う
  (define-key selected-keymap (kbd "\`") #'region-to-back-quote)

  ;; リージョンをブラケット(カッコ)で囲う
  (define-key selected-keymap (kbd "(") #'region-to-bracket)

  ;; リージョンをカギカッコで囲う
  (define-key selected-keymap (kbd "[") #'region-to-square-bracket))

リージョンで囲った文字列を対象にgoogle翻訳を呼び出す(Emacs)

以下の記事では、「selected」という、リージョンで囲われた文字列を対象に様々な関数を1キーストロークで発動できるパッケージの紹介がされている。

同記事では対象の文字列に対してgoogle検索を呼び出す方法も紹介されているが、ここではgoogle翻訳を呼び出すのが目的である。

MELPAよりgoogle-translateというパッケージをインストールした上で、以下の関数を定義する。

;; https://solist.work/blog/posts/google-translate/
;; https://w.atwiki.jp/ntemacs/pages/86.html
;; http://emacs.rubikitch.com/google-translate/
;; で紹介されていた関数をアレンジ
(defvar google-translate--english-chars "[:ascii:]’“”–")
(defvar google-translate--target-text "")
(defun google-translate-auto ()
  "Automatically recognize and translate Japanese and English."
  (interactive)
  (cond ((use-region-p)
         (setq google-translate--target-text
               (replace-regexp-in-string
                "\\([^\n]\\)\n\\([^\n]\\)" "\\1 \\2"
                (replace-regexp-in-string
                 "^\s*\\(.*?\\)\s*$" "\\1"
                 (funcall region-extract-function nil))))
         (deactivate-mark)
         (when (fboundp 'cua-cancel)
           (cua-cancel)))
         (t
          (setq google-translate--target-text
                (read-string "Google Translate: "))))
  (if (string-match
       (format "\\`[%s]+\\'" google-translate--english-chars)
       google-translate--target-text)
      (google-translate-translate "en" "ja" google-translate--target-text)
    (google-translate-translate "ja" "en" google-translate--target-text)))

この関数のミソは、google翻訳にかける前に「改行」を削除していることである。また日本語と英語を自動判別する機能もついているので、便利に使えるだろう。

そして以下の設定を書く。

(when (require 'selected nil t)
  (define-key selected-keymap (kbd "t") #'google-translate-auto)
  (selected-global-mode 1))

すると、リージョンに文字列が囲われた状態で、t を押下すればgoogle translateからの翻訳結果が帰ってくる(*Google Translate*バッファ)。

ewwを使っていたら「error in process filter: Invalid image type ‘svg’」というエラーが出たとき

ewwでリンク先のページに飛んだ時、表題のエラーに出会うことがある。 これはEmacssvgをサポートする形でコンパイルされていないから、ということ。 しかしeww(というかshr)はsvgが使える前提で一部の関数が実装されているのが問題。

当面、以下の関数を再定義して、しのぐことにする。 オリジナルの関数からの変更点は、(memq 'svg image-types)を関数後部あたりに入れたこと。

(defun shr-parse-image-data ()
  (let ((data (buffer-substring (point) (point-max)))
        (content-type
         (save-excursion
           (save-restriction
             (narrow-to-region (point-min) (point))
             (let ((content-type (mail-fetch-field "content-type")))
               (and content-type
                    ;; Remove any comments in the type string.
                    (intern (replace-regexp-in-string ";.*" "" content-type)
                            obarray)))))))
    ;; SVG images may contain references to further images that we may
    ;; want to block.  So special-case these by parsing the XML data
    ;; and remove anything that looks like a blocked bit.
    (when (and shr-blocked-images
               (eq content-type 'image/svg+xml))
      (setq data
            ;; Note that libxml2 doesn't parse everything perfectly,
            ;; so glitches may occur during this transformation.  And
            ;; encode as utf-8: There may be text (and other elements)
            ;; that are non-ASCII.
            (shr-dom-to-xml
             (libxml-parse-xml-region (point) (point-max)) 'utf-8)))
    ;; SVG images often do not have a specified foreground/background
    ;; color, so wrap them in styles.
    (when (and (display-images-p)
               (eq content-type 'image/svg+xml)
               (memq 'svg image-types)) ;; ←SVGが含まれていないときにnil
      (setq data (svg--wrap-svg data)))
    (list data content-type)))