見出し画像

<001~005>Catch The Error解答解説

NABLASでインターン中の下垣内です。

本記事では、先月から運用しはじめた「Catch The Error(CTE)」について、その概要と実際の問題を解説します。

Catch The Errorとは?

Catch The Errorは、エラーを含むプログラム、または想定していない実行結果を出力するプログラムのソースコードが与えられ、その原因を特定して修正する課題です。現在は、このコンテンツの一部をNABLASのツイッター上で火曜日と木曜日の19:00に1つずつ紹介しています。是非解いてみてください。

この取組みを始めた背景について少し説明させてください。NABLASではiLectという「業務課題を解決出来るようになる」ことを目的としたAI人材育成講座を提供しています。 この講座には、画像認識や自然言語、強化学習といった様々な分野のハンズオンを含む講義や、学んだ内容に関する課題といったコンテンツがあります。

iLectのテーマの一つが「実践」であり、課題も実務能力を高めることを目標としています。そのため、実務能力として「エラーを自分で解決できること」が重要であると考え、この能力を育成するような課題を作ることになりました。この課題の名称が Catch The Error(以降 CTE) です。

Catch The Error という名前は、セキュリティスキルを競うコンテストであるCapture The Flag(CTF)と、多くの言語で例外処理構文として catch という予約語が設定されていることから命名しました。 

想定解答と解説

以降、実際のCTEの問題の内容について解説していきたいと思います。今回はCTE001からCTE005まで解説します。

CTE001

arr = [5, 2, 3, 1, 4]
arr = arr.sort()

assert arr == [1, 2, 3, 4, 5]  # Error!

これはリストのsortメソッドとsorted関数の違いを理解しているかを問う問題です。

sorted関数はソートしたリストを返しますが、リストのsortメソッドはリストをin-placeでソートして返り値はないため、上記のプログラムだとarrにNoneが入ってしまいます。この仕様はNumPyの場合でも同様ですが、sortedの代わりにnumpy.sortを用います。

一方、pandasではDataFrameクラス等のメソッドを用いても基本的にin-placeにはならず、編集されたオブジェクトを返します。そのため、in-placeにしたい場合はinplace引数にTrueを設定する必要があります。

このように、Pythonでは処理によってin-placeであったり、in-placeでなかったりといった仕様が混在しているため、ドキュメント等を確認し、関数が何を返すのかを確認しましょう。

CTE002

import tqdm

for i in tqdm(range(n_epochs)):  # Error!
    train()

これは、tqdmモジュールの利用方法に関する問題です。

tqdmモジュールはインポートしても、モジュールをそのまま関数として利用することはできませんので、 以下のようにインポートするのが正解です。

from tqdm import tqdm

プログレスバーを表示するために用いる関数がtqdmでモジュール名もtqdmのため、混同してしまうミスがよくあるようです。似たものとして、datetime, random, pprintといったモジュールがあります。

別解として、以下のように、使用する際に関数を指定することもできます。

import tqdm

for i in tqdm.tqdm(range(n_epochs)):
    train()

なお、このプログラム単体ではn_epochs変数とtrain関数が定義されていないという指摘をいただきました。こちらわかりづらく申し訳ありませんでした。以降の問題ではこのようにわかりづらい表記を避けるようにしております。

CTE003

price = 235
tax = 0.1

total = round(price * (1+tax))

# 235 * 1.1 = 258.5
print(total)  # なぜか258‼︎

これは、round関数の挙動を理解しているかを問う問題です。

Pythonの標準関数であるround関数は、端数処理 (丸め処理) を行う関数ですが、四捨五入ではなく最近接遇数への丸め (いわゆる銀行丸め) が採用されています。 偶数丸めでは、丸める対象の桁の数が5より大きい場合は切り上げ、5より小さい場合は切り下げます。 ちょうど5の場合には、丸めた結果の桁の数が偶数になるように切り上げもしくは切り下げを行います。 上記の例では、235 * 1.1 の計算結果である 258.5 は、丸める対象の桁の数がちょうど5であるため、偶数となる方に丸められ、258となります。 偶数丸めの利点については補足で説明します。

注意点として、round(total, 2)のように結果が小数になる丸め方をすると、偶数丸めでも四捨五入でもない結果が出る場合があります。 これは例えば以下のようなコードで発生します。

x = 2.675
print(f'{x:.20f}')  # == 2.67499999999999982236
print(round(x, 2))  # == 2.67

これは、計算機の内部では、小数が近似値で表現されていることに起因します。 上記2行目のコードを実行するとわかるように、計算機内部では2.675という値ではなく、実際には2.67499999999999982236といった2.675に近い値が格納されています。 そのため、小数点第二位で偶数丸めの処理を行うと、その下の桁、つまり4という数が丸め処理の対象となり、切り捨て2.67となります。

より正確に小数を扱いたい場合は、decimalというモジュールが用意されていますので、使用すると良いでしょう。

CTE004

import numpy as np

a = np.array([1, 2, 3])
b = np.array([2, 4, 6])

b //= 2

if a == b:  # Error!
    print("Same")

これはndarray (numpy配列) の比較演算に関する問題です。

a、bがリストであれば、各要素を比較して等しければ式a==bの評価値はTrueです。 一方、ndarrayにおいては、==演算子は要素別比較の結果の配列を返します。上記の例では、aとbが同じ形のndarrayの場合、 対応する各要素が同じ値であるかを判定し、その結果 (True / False) が列挙されたndarrayが返されます。
このような場合には、全ての値が True であることを確認するために以下の3つのいずれかを用いると良いでしょう。

(a == b).all()
np.all(a == b)
all(a == b)

逆に、1つでも一致していれば良い場合はanyメソッド (関数) が使えます。

CTE005

# global変数をインクリメントしたい
global_var = 100


def increment():
    global_var += 1


print(global_var)
increment()  # Error!
print(global_var)

Pythonのグローバル変数とローカル変数に関する問題です。

Pythonでは、関数内のどこかで代入が行われる変数はローカル変数になります。 ローカル変数に設定されない変数、つまり代入式の右辺にしか現れない変数はグローバル変数として参照されます。

increment関数内でグローバル変数としてglobal_var変数を用いたい場合は、globalキーワードを用いて以下のようにグローバル変数であることを明示します。

def increment():
    global global_var

これにより、宣言した関数内ではglobal_var変数がグローバル変数だと認識されます。

Pythonの変数のスコープ判定についてより詳細に知りたい方は、Pythonドキュメントの実行モデルを確認してみてください。

補足

四捨五入と比べた偶数丸めの利点

偶数丸めを利用する利点は、端数処理後の値を合計したり統計処理する際の誤差が少なくなる点にあります。

データサイエンスや数値計算、統計といった分野においては、より精度良く数を扱うべき場面が多々あります。このような場合、偶数丸めのほうが単純な四捨五入の処理に比べて計算の精度を高められることが多いことが知られており、Pythonを始めとした多くのプログラミング言語でこの方式が採用されています。

偶数丸めは、「銀行丸め」とも呼ばれ、誤差を少なくするような処理の多い銀行などで多用されていました。

実験として、1.5や2.3など、少数点第一位まで含む数をランダムに10,000個サンプリングし、整数へ端数処理する際にどのような影響があるかを調べてみます。 単純な四捨五入を適用した場合と偶数丸めを実行した場合で、元の数からどれくらい増減したか、分布をプロットすると以下のようになります。

オレンジの実線は平均値の位置、点線は0.0の位置を表しています。

偶数丸めの場合、小数点第一位が5の場合に切り上げる場合と切り下げる場合があるため、誤差が0.5と-0.5のいずれかになります。一方、四捨五入の場合は小数点第一位が5だと常に切り上げるため、誤差は必ず0.5になります。

これにより、四捨五入の場合は上図のように誤差の期待値が0にならず、丸めた数の平均を計算すると丸める前よりも大きくなってしまうという問題があります。 ただし、必ずしも偶数丸めの方が性質が良いというわけではなく、誤差の分散に関しては四捨五入の方が小さくなります。

以上のように、丸め方によって丸める前のデータとの統計的な性質が変わってしまう場合がありますので、丸めた値の用途等を考えて適切な丸め方を採用しましょう。