Pythonで単体テストを行うためのunittestモジュール
はじめに
Pythonのunittestの使い方を復習がてら書いていきます。
unittest
ユニットテスト(単体テスト)では関数に対して、実際の「戻り値」と想定される「期待値」を比較して正しい動作をしているか確認します。
まず始めにunittestの簡単な例を見てみましょう。
import unittest # テスト対象のplus関数 def plus(a, b): return a + b # テストクラス class TestPlus(unittest.TestCase): # テストする関数ごとにメソッドを作る def test_plus(self): self.assertEqual(5, plus(2, 3))
上記では足し算をする関数を用意してそれが正しく動作するかを確認するテストを書いています。
テストプログラムを書くときは、
- unittestをインポートする。
- unittest.TestCaseを継承してクラスを作成する。
- テストしたい機能ごとにメソッドを作りテストする。
のような流れになります。
assertEqualは二つの引数の値が等しいかどうか比べます。TestCaseにはこのようなメソッドがいくつか用意されています。
本来はテストはテストだけを行う別のpythonファイルにまとめる方が良いです。
テストを実行する方法はいくつかありますが、ここではとりあえず以下のようにします。
if __name__ == "__main__": unittest.main()
これを実行すると以下のような出力が得られます。
> python test.py . ---------------------------------------------------------------------- Ran 1 test in 0.000s OK
テストに失敗する場合は以下のようになります。
> python test.py F ====================================================================== FAIL: test_plus (__main__.TestPlus) ---------------------------------------------------------------------- Traceback (most recent call last): File "test.py", line 11, in test_plus self.assertEqual(6, plus(2, 3)) AssertionError: 6 != 5 ---------------------------------------------------------------------- Ran 1 test in 0.000s FAILED (failures=1)
アサートメソッド
TestCaseクラスにあるアサートメソッドをいくつか紹介していきます。
assertEqual(a, b)は上記の通りなので省略。
assertNotEqual(a, b)
aとbが等しく無いことをテストします。
import unittest def plus(a, b): return a + b class TestPlus(unittest.TestCase): def test_plus(self): self.assertNotEqual(6, plus(3, 5))
assertTrue(x), assertFalse(x)
xがTrue(False)であることをテストします。
import unittest class TestStringMethod(unittest.TestCase): def test_is_digit(self): self.assertTrue('123'.isdigit()) def test_is_not_digit(self): self.assertFalse('mm'.isdigit())
assertIn(a, b), assertNotIn(a, b)
aがbに含まれていること(そうでないこと)をテストします。
import unittest class TestList(unittest.TestCase): def test_is_in(self): self.assertIn('a', ['a', 'b', 'c']) def test_is_not_in(self): self.assertNotIn('x', ['a', 'b', 'c'])
assertGreaterEqual(a, b), assertLess(a, b)
aがb以上か(未満か)をテストします。
import unittest def plus(a, b): return a + b class TestGreaterOrLess(unittest.TestCase): def test_greaterequal(self): self.assertGreaterEqual(5, plus(1, 3)) def test_less(self): self.assertLess(2, plus(2, 4))
assertRaises(exc)
with内で例外をあげるかテストします。
関数を実行して例外excが放出されたら成功、他の例外が放出、または例外がなかったら失敗です。
import unittest def raise_value_error(): raise ValueError class TestRaises(unittest.TestCase): def test_raises(self): with self.assertRaises(ValueError): raise_value_error()
まとめ
改めてテストする流れを示しておきます。
- unittestをインポートする。
- unittest.TestCaseを継承してクラスを作成する。
- テストしたい機能ごとにメソッドを作りテストする。
まだまだ多くのアサートメソッドがありますが、ここでは代表的なものをいくつか紹介しました。
テストコードを書くことで、ゴールが明確になったりプログラムが洗練されるなどのメリットがあります。
積極的にテストコードを書いて身につけていきたいです。
Djangoのチュートリアル中にオリジナルのテストを追加した話
はじめに
オリジナルと言っても、
はじめての Django アプリ作成、その 5 | Django ドキュメント | Django
ここにある「さらなるテストについて考える」で述べられているChoicesを持たないQuestionsが公開されないようテストするだけのものですが。
この記事ではコードの一部を記述します。
実装
- Model
class Question(models.Model): question_text = models.CharField(max_length=200) pub_date = models.DateTimeField('date published') def __str__(self): return self.question_text def was_published_recently(self): now = timezone.now() return now - datetime.timedelta(days=1) <= self.pub_date <= now class Choice(models.Model): question = models.ForeignKey(Question, on_delete=models.CASCADE) choice_text = models.CharField(max_length=200) votes = models.IntegerField(default=0) def __str__(self): return self.choice_text
このモデルではQuestionとChoiceが一対多の関係となっています。
ちなみにDjangoでは多対一の関係にはForeignKeyを、多対多の関係にはManyToManyFieldを、一対一の関係にはOneToOneFieldを使います。
- View
class IndexView(generic.ListView): template_name = 'polls/index.html' context_object_name = 'latest_question_list' def get_queryset(self): """ Return the last five published questions with any choices.(not including those set to be published in the future). """ question = Question.objects.filter( pub_date__lte=timezone.now(), choice__isnull=False ) return question.order_by('-pub_date')[:5]
このクラス内のget_queryset()で最近の5個のQuestionsを返しています。filterメソッドで現在以前の、Choiceを持っているQuestionという条件で検索し一致するものを返します。
- Test
def create_question(question_text, days): """ Create a question with the given 'question_text' and published the given number of 'days' offset to now(negative for questions published in the past, positive for questions that have yet to be published). """ time = timezone.now() + datetime.timedelta(days=days) return Question.objects.create(question_text=question_text, pub_date=time) def create_choice(question, choice_text): """ Create a choice of the given 'question' object. """ question.choice_set.create(choice_text=choice_text, votes=0) class QuestionIndexViewTests(TestCase): ... def test_past_question_without_choices(self): """ The questions without choices aren't displayed on the index page. """ question = create_question(question_text="Without choices.", days=-30) question_with_choice = create_question(question_text="With choice.", days=-30) create_choice(question_with_choice, choice_text="Choice 1") response = self.client.get(reverse('polls:index')) self.assertQuerysetEqual( response.context['latest_question_list'], ['<Question: With choice.>'] )
test_past_question_without_choices()でChoiceありとなしのQuestionを作成し、ありのものだけ表示されるようテストしています。
結果
- Viewにて、choice__isnull=Falseを書かなかった場合
> python manage.py test polls Creating test database for alias 'default'... System check identified no issues (0 silenced). ......F.... ====================================================================== FAIL: test_past_question_without_choices (polls.tests.QuestionIndexViewTests) ---------------------------------------------------------------------- Traceback (most recent call last): File "/Users/*****/Django-Project/mysite/polls/tests.py", line 122, in test_past_question_without_choices ['<Question: With choice.>'] File "/Users/*****/.virtualenvs/django/lib/python3.6/site-packages/django/test/testcases.py", line 955, in assertQuerysetEqual return self.assertEqual(list(items), values, msg=msg) AssertionError: Lists differ: ['<Question: With choice.>', '<Question: Without choices.>'] != ['<Question: With choice.>'] First list contains 1 additional elements. First extra element 1: '<Question: Without choices.>' - ['<Question: With choice.>', '<Question: Without choices.>'] + ['<Question: With choice.>'] ---------------------------------------------------------------------- Ran 11 tests in 0.060s FAILED (failures=1) Destroying test database for alias 'default'...
- 書いた場合
> python manage.py test polls Creating test database for alias 'default'... System check identified no issues (0 silenced). ........... ---------------------------------------------------------------------- Ran 11 tests in 0.054s OK Destroying test database for alias 'default'...
以上のように、テストが通らなかった場合どのテストで、どのように通らなかったのか教えてくれます。
Daily Coding Problemからの問題
はじめに
Daily Coding Problemというサービスがあったので登録してみました。これは毎朝(確認してみたらAM2:30でした)にGoogleやamazonなどの選考フローで出されたCoding Interviewの問題を一問送ってくれるサービスです。今回は初めてきた問題を解いてみました。
www.dailycodingproblem.com
問題
Given a list of numbers and a number k, return whether any two numbers from the list add up to k.
For example, given [10, 15, 3, 7] and k of 17, return true since 10 + 7 is 17.
Bonus: Can you do this in one pass?
一つのリストとある数kが与えられた時、リストの中の二つの数を足してkになるかどうかの判定を返せ。
こんな感じの問題ですかね。シンプルな問題だと思います。
実装
メールのタイトルにもEasyとついていましたが、アルゴリズム力も実装力もない自分は一つの方法しか実装できませんでした。
全ての数を総当たりで調べていく方法です。(これすらも正しく実装できているか不安ですが・・・)
def find_num2k_roundrobin(list, k): for i in list: for j in list: if j + i == k: return True break return False
計算量はかな。多分もっと早い手法があると思います。
まとめ
こんな風に毎日問題が届いてそれを解いていくという感じです。Premiumにしたら次の日の朝に解放が送られてくるそうです。
しばらくは無課金で様子見します。あと今日届いた「問題解決のPythonプログラミング」も活用してプログラミング力を上げていきたいと思います。
問題解決のPythonプログラミング ―数学パズルで鍛えるアルゴリズム的思考
- 作者: Srini Devadas,黒川利明
- 出版社/メーカー: オライリージャパン
- 発売日: 2018/09/22
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (3件) を見る
DCGANでガンプラの箱を生成
はじめに
n番煎じではありますが、GANを用いた画像の生成に挑戦していきます。今回使う画像はガンプラの箱の画像です。
DCGANとは
GAN(Generative adversarial network)は生成モデルの一種です。Generator(G)とDiscriminator(D)の二つのネットワークが互いに相反する目的の元学習していき、与えられた画像によく似た画像を生成します。GはDが見分けもつかないような精巧な画像を生成すること、Dは本物の画像と偽物の(生成された)画像を正しく見分けることを目的とします。
DCGANの場合、Gは一様分布、または正規分布から生成されたノイズを入力として受け取り、転置畳み込み(fractionally-strided convolutions)を行って画像を生成します。Dはいわゆる普通のCNNのような識別器です。
データセット
今回使うデータセットはガンプラの箱画像です。以下のような画像をネットから拾ってきました。だいたい160枚くらいです。
これらの画像を128x128にリサイズします。遠目で見ればわかる程度のぼやけ具合ですかね笑
実装
以下がGeneratorの実装です。実装には以下の記事やGithubのリポジトリを参考にしました。
GANについて概念から実装まで ~DCGANによるキルミーベイベー生成~ - Qiita
画像のサイズに関わるところを変更しました。また、Google Colaboratoryで実行したところメモリ不足によるエラーを吐いたので、最初のチャンネル数の箇所を128 → 32に変更しました。
def generator_model(): model = Sequential() model.add(Dense(1024, input_shape=(100,))) model.add(Activation('relu')) model.add(Dense(32 * 32 * 32)) model.add(BatchNormalization()) model.add(Activation('relu')) model.add(Reshape((32, 32, 32))) model.add(UpSampling2D(size=(2, 2))) model.add(Conv2D(64, (5, 5), padding='same')) model.add(Activation('relu')) model.add(BatchNormalization()) model.add(UpSampling2D(size=(2, 2))) model.add(Conv2D(32, (5, 5), padding='same')) model.add(Activation('relu')) model.add(BatchNormalization()) #model.add(UpSampling2D(size=(2, 2))) model.add(Conv2D(n_colors, (5, 5), padding='same')) model.add(Activation('tanh')) return model
次にDiscriminatorの実装です。
def discriminator_model(): model = Sequential() model.add(Conv2D(32, (5, 5), strides=2, input_shape=(128, 128, n_colors), padding='same')) model.add(LeakyReLU(alpha=0.2)) model.add(Dropout(0.25)) model.add(Conv2D(64, (5, 5), strides=2, padding="same")) model.add(ZeroPadding2D(padding=((0, 1), (0, 1)))) model.add(LeakyReLU(alpha=0.2)) model.add(Dropout(0.25)) model.add(BatchNormalization(momentum=0.8)) model.add(Conv2D(128, (5, 5), strides=2, padding="same")) model.add(LeakyReLU(alpha=0.2)) model.add(Dropout(0.25)) model.add(BatchNormalization(momentum=0.8)) model.add(Conv2D(256, (5, 5), strides=1, padding="same")) model.add(LeakyReLU(alpha=0.2)) model.add(Dropout(0.25)) model.add(Flatten()) model.add(Dense(1, activation="sigmoid")) return model
結果
epoch: 0
epoch: 1000
epoch: 10000
epoch: 30000
うーん、ガンプラのガの字も出てこない感じですね・・・
やはり128x128という大きめの画像に対してデータ数160は少なすぎたのか、安易にネットワークの構造を変えたのがよくなかったのか。
ひとまずデータ数を増やして再実行したいと思います。また追記します。
積率母関数について
はじめに
当時はスルーしていましたが、名前の由来というか、名前から意味が推測できないような気がしていました。
研究室に置いてあった「機械学習のための確率と統計(機械学習プロフェッショナルシリーズ)」の第1章にバッチリ答えが書いてありましたのでまとめてみます。
機械学習のための確率と統計 (機械学習プロフェッショナルシリーズ)
- 作者: 杉山将
- 出版社/メーカー: 講談社
- 発売日: 2015/04/08
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (1件) を見る
積率母関数の積率(モーメント)
まず確率変数の期待値について考えます。期待値は確率変数が取り得る値とそれに対応する確率の加重平均で表せます。(連続型確率変数の場合、確率密度関数を用います。)
[離散型確率変数の場合]\begin{align}E[X] = \sum_{i=1}^{n} {x_i}{p_i} \end{align}
[連続型確率変数の場合]\begin{align}E[X] = \int_{-\infty}^{\infty} {x}{f(x)} dx \end{align}
次に分散V[X]を考えます。分散は期待値を用いて次のように表せます。
\begin{align}V[X] = E[(X - E[X])^2]\end{align}
この式は次のように簡単に表せます。
\begin{align}V[X] = E[(X - E[X])^2] &= E[X^2 - 2XE[X] + E[X]^2]\\ &= E[X^2] - 2E[X]^2 + E[X]^2\\ &= E[X^2] - E[X]^2\end{align}
他にも分布が正規分布からどれだけ歪んでいるかを表す歪度(わいど)や正規分布からどれだけ尖っているかを表す尖度(せんど)などもあります。
[歪度]\begin{align}\frac{E[(X - E[X])^3]}{D[X]^3}\end{align}
[尖度]\begin{align}\frac{E[(X - E[X])^4]}{D[X]^4} - 3\end{align}
確率分布は上にあげた期待値、分散、歪度、尖度を指定していくと、形が限定されていきます。ここで積率という概念を導入します。
[(xの原点周りの)k次の積率]\begin{align}\mu_k = E[x^k]\end{align}
[xの期待値周りのk次の積率]\begin{align}\nu_k = E[(x - E[x])^k]\end{align}
この式は分散、歪度、尖度を表す式でも出てきましたね。全ての次数の積率を指定することで、確率分布を一意に決定することができます。期待値、分散、歪度、尖度を積率で表すと以下のようになります。
[期待値]\begin{align}E[X] = \mu_1\end{align}
[分散]\begin{align}V[X] &= E[(X - E[X])^2] = \nu_2\\ &=E[X^2] - (E[X])^2 = \mu_2 - \mu_1^2\end{align}
[歪度]\begin{align}\frac{\mu_3 - 3\mu_2\mu_1 + 2\mu_1^3}{(\mu_2 - \mu_1^2)^\frac{3}{2}}\end{align}
[尖度]\begin{align}\frac{\mu_4 - 4\mu_3\mu_1 + 6\mu_2\mu_1^2 - 3\mu_1^4}{(\mu_2 - \mu_1^2)^2} - 3\end{align}
積率母関数
ようやく下準備が終わったので積率母関数について解説していきます。積率母関数は以下の式で定義されます。
[積率母関数]\begin{align}M_x(t) = E[e^{tx}] \end{align}
この式は、離散型確率変数、連続型確率変数のそれぞれの場合で、
[離散型]\begin{align}E[e^{tx}] = \sum_{x} e^{tx}f(x)\end{align}
[離散型]\begin{align}E[e^{tx}] = \int e^{tx}f(x)dx\end{align}
と表せます。これらが無限大に発散する場合、積率母関数は存在しません。
積率母関数の素晴らしいところは、「積率母関数の導関数にゼロを代入すると積率が得られる」という性質があることです。
\begin{align}M_x^{(k)}(0) = \mu_k\end{align}
これによって期待値や分散、k次の積率を楽に求めることができます。以下ではこの定理を証明していきたいと思います。
[証明] まず、をに関して原点の周りでテイラー展開すると、のに関するk階微分はであることから、
\begin{align}e^{tx} = 1 + tx + \frac{(tx)^2}{2!} + \frac{(tx)^3}{3!} + \cdots\end{align}
が得られます。この両辺の期待値をとれば、
\begin{align}E[e^{tx}] = M_x(t) = 1 + \mu_1t + \frac{\mu_2}{2!}t^2 + \frac{\mu_3}{3!}t^3 + \cdots\end{align}
が得られます。この両辺をtに関して微分すれば、
\begin{align}M_x'(t) &= \mu_1 + \mu_2t + \frac{\mu_3}{2!}t^2 + \frac{\mu_4}{3!}t^3 + \cdots\\ M_x''(t) &= \mu_2 + \mu_3t + \frac{\mu_4}{2!}t^2 + \frac{\mu_5}{3!}t^3 + \cdots\\ &\vdots\\ M_x^{(k)}(t) &= \mu_k + \mu_{k+1}t + \frac{\mu_{k+2}}{2!}t^2 + \frac{\mu_{k+3}}{3!}t^3 + \cdots\end{align}
が得られ、この式にゼロを代入すればが得られます。(Q. E. D.)
まとめ
今回は積率母関数についてまとめました。コードが全くない記事だったので次回は何かコーディングできたらと思います。
不備、誤った理解などがありましたらご指摘頂けたら幸いです。
参照: 積率と積率母関数 - TOKYO TECH OCW(pdfファイルです)