The code implemented in the previous step
[1]:
import numpy as np
class Variable:
def __init__(self, data):
if data is not None:
if not isinstance(data, np.ndarray):
raise TypeError('{} is not supported'.format(type(data)))
self.data = data
self.grad = None
self.creator = None
def set_creator(self, func):
self.creator = func
def backward(self):
if self.grad is None:
self.grad = np.ones_like(self.data)
funcs = [self.creator]
while funcs:
f = funcs.pop()
x, y = f.input, f.output
x.grad = f.backward(y.grad)
if x.creator is not None:
funcs.append(x.creator)
def as_array(x):
if np.isscalar(x):
return np.array(x)
return x
class Function:
def __call__(self, input):
x = input.data
y = self.forward(x)
output = Variable(as_array(y))
output.set_creator(self)
self.input = input
self.output = output
return output
def forward(self, x):
raise NotImplementedError()
def backward(self, gy):
raise NotImplementedError()
class Square(Function):
def forward(self, x):
y = x ** 2
return y
def backward(self, gy):
x = self.input.data
gx = 2 * x * gy
return gx
def square(x):
return Square()(x)
def numerical_diff(f, x, eps=1e-4):
x0 = Variable(x.data - eps)
x1 = Variable(x.data + eps)
y0 = f(x0)
y1 = f(x1)
return (y1.data - y0.data) / (2 * eps)
Testing is an essential part of software development. Testing can help you notice mistakes (bugs) and automating testing can help you maintain the quality of your software on an ongoing basis. The same is true of the DeZero we build. This step describes how to test – specifically, how to test the Deep Learning framework.
NOTE
As software testing gets bigger, it tends to have its own ways of doing things and a lot of fiddly rules. But when it comes to testing, you don’t have to think too hard, especially at the beginning. The first thing to do is to “test it”. This step is not a “full-blown” test, but rather as simple as possible.
To run tests in Python, it is convenient to use unittest
, which is included in the standard library. Here, let’s test the square
function implemented in the previous step. The code is as follows.
[2]:
import unittest
class SquareTest(unittest.TestCase):
def test_forward(self):
x = Variable(np.array(2.0))
y = square(x)
expected = np.array(4.0)
self.assertEqual(y.data, expected)
As above, we first import the unittest
and implement the SquareTest
class that extends the unittest.TestCase
. The essential test is to write any method whose name starts with test
and write it in it. The test we write here verifies that the output of the square
function matches the expected value. Specifically, when the input is 2.0
, we verify that the output is 4.0
.
NOTE
In the above example, we use the method self.assertEqual
to verify that the output of the square
function matches the expected value. This method determines if the two given objects are equal or not. In addition to this method, unittest
has various other methods, such as self.assertGreater
and self.assertTrue
. For other methods, see the documentation for unittest
.
Now, let’s run the above test. We will assume that the above test code is in steps/step10.py
. In this case, the following command can be executed from the terminal
$ python -m unittest steps/step10.py
If you are using Jupyter Notebook (or Google Colab), you can run the test with the following command.
[3]:
if __name__ == '__main__': unittest.main(argv=['first-arg-is-ignored'], exit=False)
.
----------------------------------------------------------------------
Ran 1 test in 0.004s
OK
Now, let’s check the output of the test. This output means that “we did one test and the results were OK”. This means that the test has passed. If there are any problems here, you will see output like FAIL: test_forward (step10.SquareTest)
to show that the test has failed.
Next, let’s add a test for backward propagation of the square
function. To do so, add the following code to the SquareTest
class we just implemented.
[4]:
class SquareTest(unittest.TestCase):
def test_forward(self):
x = Variable(np.array(2.0))
y = square(x)
expected = np.array(4.0)
self.assertEqual(y.data, expected)
def test_backward(self):
x = Variable(np.array(3.0))
y = square(x)
y.backward()
expected = np.array(6.0)
self.assertEqual(x.grad, expected)
Here, we add a method called test_backward
. In it, you get the derivative by y.forward()
and check whether the value of the derivative matches the expected value or not. Incidentally, the value 6.0
is set here as the expected value (expected
).
Let’s test it again with the code above. As a result, we get the following output
[5]:
if __name__ == '__main__': unittest.main(argv=['first-arg-is-ignored'], exit=False)
..
----------------------------------------------------------------------
Ran 2 tests in 0.011s
OK
If you look at the results, you can see that it passed two tests. Now you can add other test cases (inputs and expected values) in the same way as before. And as the number of test cases increases, the reliability of the square
function also increases. You can also repeatedly verify the state of the square
function by testing it at the time you modify the code.
Earlier we wrote a test for backward propagation. There, the expected value of the derivative was calculated and given by hand. In fact, there is an alternative, automatic testing method that replaces it. It’s a method called gradient check. The gradient check is performed by comparing the results obtained by numerical differentiation with the results obtained by backpropagation. If the difference is large, it is likely that there is a problem with the implementation of backpropagation.
NOTE
We implemented the numerical differentiation in “Step 4”. Numerical differentiation is easy to implement and gives you the roughly correct value of the derivative. Therefore, we can test the correctness of the back-propagation implementation by comparing it with the results of the numerical differentiation.
You only need to prepare the input values for gradient check, so you can test efficiently. So, let’s add a test by gradient check. Here, we use the numerical_diff
function implemented in “Step 4”. The code for the function is also included as a review.
[6]:
def numerical_diff(f, x, eps=1e-4):
x0 = Variable(x.data - eps)
x1 = Variable(x.data + eps)
y0 = f(x0)
y1 = f(x1)
return (y1.data - y0.data) / (2 * eps)
class SquareTest(unittest.TestCase):
def test_forward(self):
x = Variable(np.array(2.0))
y = square(x)
expected = np.array(4.0)
self.assertEqual(y.data, expected)
def test_backward(self):
x = Variable(np.array(3.0))
y = square(x)
y.backward()
expected = np.array(6.0)
self.assertEqual(x.grad, expected)
def test_gradient_check(self):
x = Variable(np.random.rand(1)) # ランダムな入力値を生成
y = square(x)
y.backward()
num_grad = numerical_diff(square, x)
flg = np.allclose(x.grad, num_grad)
self.assertTrue(flg)
In the test_gradient_check
method for gradient check, one random input value is generated. Next, we find the derivative by back-propagation, and then the numerical differentiation by using the numerical_diff
function. Then, we make sure that the values obtained by the two methods are almost identical. To do so, we use the function np.allclose
of NumPy.
np.allclose(a, b)
determines whether a
and b
of the ndarray
instance are close values. How much is a “close value” can be defined by the arguments rtol
and atol
, as in np.allclose function(a, b, rtol=1e-05, atol=1e-08)
. Trueis returned if all elements of
aand
bsatisfy the following conditions (
|. |` denotes the absolute value).
|a - b| ≦ (atol + rtol * |b|)
The values of atol
and rtol
may require small adjustment depending on the calculation (function) of the gradient check. For that criterion, see, for example, literature[6]. So, let’s add the gradient check above and then run the test. This time, we get the following results.
[7]:
if __name__ == '__main__': unittest.main(argv=['first-arg-is-ignored'], exit=False)
...
----------------------------------------------------------------------
Ran 3 tests in 0.016s
OK
Thus, in the case of a deep learning framework that computes the derivatives automatically, a mechanism can be created to perform tests semi-automatically by gradient check. It allows us to systematically build test cases more broadly.
When developing DeZero, the above knowledge of testing should be sufficient. You can write test code for DeZero using the steps you’ve learned here. However, in this document, we will omit the description of the test from now on. If you feel the need for test code, you can add it yourself as a reader.
In addition, it is common to keep a group of files for testing in one place. The test code are also put together in the tests
directory in this book (and additional useful functions to run tests are implemented). If you’re interested, take a look at that test code. There, you’ll see a lot of code like the one I wrote in this step. In addition, the files for the test can be executed by the following command.
$ python -m unittest discover tests
You can use the sub command discover
to search for test files in the directory specified after discover
as above. Then, all the files found are executed together. By default, the pattern test*.py
in the specified directory is recognized as a test file (this can be changed). Now you can run all the tests in the tests
directory at once.
NOTE
In DeZero’s tests directory, we also run tests that take the Chainer as the correct answer. For example, to test a sigmoid function, we compute it in DeZero and Chainer for the same input, respectively, and compare whether the two outputs are almost the same value.
Also, DeZero’s GitHub repository works with a service called Travis CI, which is a service for continuous integration, and DeZero’s GitHub repository automatically runs tests when you push code or merge a pull request. Any problems with the results will be reported to you via email or other means. In addition to that, the top page of DeZero’s GitHub repository also shows a screen like Figure 10-1.
Figure 10-1 The top screen of DeZero’s GitHub repository.
As shown in Figure 10-1, you’ll see a badge that says BUILD: PASSING. This is a sign that the test has passed (if it fails, you will see a badge saying “build: failed”). By linking with CI tools like this, you can always test the source code. It helps keep the code reliable.
DeZero is a small piece of software, but we’re going to grow it into something bigger. By implementing a testing mechanism like the one described here, you can expect to maintain the reliability of your code. That’s the end of the first stage.
Little by little - and steadily - we’ve been building DeZero up to this point. The first DeZero had only “little boxes” (variables), but it has grown to the point where it can now run complex algorithms called back-propagation. For now, however, backpropagation can only be applied to simple calculations. In the next stage, we’ll extend DeZero further, so that it can be applied to more complex calculations.