機械学習好きのメモ帳

MMのメモ帳

機械学習、データ分析、アルゴリズムなどを扱っていきます。雑記も含まれます。

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))

上記では足し算をする関数を用意してそれが正しく動作するかを確認するテストを書いています。
テストプログラムを書くときは、

  1. unittestをインポートする。
  2. unittest.TestCaseを継承してクラスを作成する。
  3. テストしたい機能ごとにメソッドを作りテストする。

のような流れになります。
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()

まとめ

改めてテストする流れを示しておきます。

  1. unittestをインポートする。
  2. unittest.TestCaseを継承してクラスを作成する。
  3. テストしたい機能ごとにメソッドを作りテストする。

まだまだ多くのアサートメソッドがありますが、ここでは代表的なものをいくつか紹介しました。
テストコードを書くことで、ゴールが明確になったりプログラムが洗練されるなどのメリットがあります。
積極的にテストコードを書いて身につけていきたいです。

公式ドキュメント:
unittest --- ユニットテストフレームワーク — Python 3.7.4 ドキュメント

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でした)にGoogleamazonなどの選考フローで出された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

計算量はO(n^2)かな。多分もっと早い手法があると思います。

まとめ

こんな風に毎日問題が届いてそれを解いていくという感じです。Premiumにしたら次の日の朝に解放が送られてくるそうです。
しばらくは無課金で様子見します。あと今日届いた「問題解決のPythonプログラミング」も活用してプログラミング力を上げていきたいと思います。

問題解決のPythonプログラミング ―数学パズルで鍛えるアルゴリズム的思考

問題解決のPythonプログラミング ―数学パズルで鍛えるアルゴリズム的思考

DCGANでガンプラの箱を生成

はじめに

n番煎じではありますが、GANを用いた画像の生成に挑戦していきます。今回使う画像はガンプラの箱の画像です。

DCGANとは

GAN(Generative adversarial network)は生成モデルの一種です。Generator(G)とDiscriminator(D)の二つのネットワークが互いに相反する目的の元学習していき、与えられた画像によく似た画像を生成します。GはDが見分けもつかないような精巧な画像を生成すること、Dは本物の画像と偽物の(生成された)画像を正しく見分けることを目的とします。
DCGANの場合、Gは一様分布、または正規分布から生成されたノイズを入力として受け取り、転置畳み込み(fractionally-strided convolutions)を行って画像を生成します。Dはいわゆる普通のCNNのような識別器です。

f:id:mugimike:20190302143222p:plain
https://medium.freecodecamp.org/an-intuitive-introduction-to-generative-adversarial-networks-gans-7a2264a81394

データセット

今回使うデータセットガンプラの箱画像です。以下のような画像をネットから拾ってきました。だいたい160枚くらいです。
f:id:mugimike:20190302144635j:plain

これらの画像を128x128にリサイズします。遠目で見ればわかる程度のぼやけ具合ですかね笑
f:id:mugimike:20190302144907j:plain

実装

以下がGeneratorの実装です。実装には以下の記事やGithubリポジトリを参考にしました。
GANについて概念から実装まで ~DCGANによるキルミーベイベー生成~ - Qiita

image-generator-with-keras-dcgan/keras-dcgan.py at master · elm200/image-generator-with-keras-dcgan · GitHub

画像のサイズに関わるところを変更しました。また、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
f:id:mugimike:20190302150546j:plain

epoch: 1000
f:id:mugimike:20190302150634j:plain

epoch: 10000
f:id:mugimike:20190302150732j:plain

epoch: 30000
f:id:mugimike:20190302150757j:plain

うーん、ガンプラのガの字も出てこない感じですね・・・
やはり128x128という大きめの画像に対してデータ数160は少なすぎたのか、安易にネットワークの構造を変えたのがよくなかったのか。
ひとまずデータ数を増やして再実行したいと思います。また追記します。

積率母関数について

はじめに

学部2年のころの統計学の授業で習った「積率母関数」。

当時はスルーしていましたが、名前の由来というか、名前から意味が推測できないような気がしていました。

研究室に置いてあった「機械学習のための確率と統計(機械学習プロフェッショナルシリーズ)」の第1章にバッチリ答えが書いてありましたのでまとめてみます。

 

機械学習のための確率と統計 (機械学習プロフェッショナルシリーズ)

機械学習のための確率と統計 (機械学習プロフェッショナルシリーズ)

 

 

 

積率母関数の積率(モーメント)

まず確率変数Xの期待値E[X]について考えます。期待値は確率変数Xが取り得る値x_iとそれに対応する確率p_iの加重平均で表せます。(連続型確率変数の場合、確率密度関数f(x)を用います。)

[離散型確率変数の場合]\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次の積率を楽に求めることができます。以下ではこの定理を証明していきたいと思います。

[証明] まず、e^{tx}tに関して原点の周りでテイラー展開すると、e^{tx}tに関するk階微分x^ke^{tx}であることから、

\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}

が得られ、この式にゼロを代入すればM_x^{(k)}(0) = \mu_kが得られます。(Q. E. D.)

まとめ

今回は積率母関数についてまとめました。コードが全くない記事だったので次回は何かコーディングできたらと思います。

不備、誤った理解などがありましたらご指摘頂けたら幸いです。

参照: 積率と積率母関数 - TOKYO TECH OCW(pdfファイルです)

http://www.ocw.titech.ac.jp/index.php?module=General&action=DownLoad&file=200917204-33-0-32.pdf&type=cal&JWC=200917204

EMANの物理学・物理数学・テイラー展開

 

 

はじめに

はじめまして。ムギミケです。情報系大学3年です。

 今までインプットばかりしてきていたので、これからはこのブログを通してアウトプットを増やしていこうと思います。

 機械学習、データ分析、アルゴリズムなどを中心に自分が勉強してきたことをメモ的に書いていきます。雑記も含まれます。

 間違った理解をしている箇所や、ツッコミどころがあればご指摘頂けると幸いでございます。