如何對機器學(xué)習(xí)做單元測試
在過去的一年里,我把大部分的工作時間都花在了深度學(xué)習(xí)研究和實習(xí)上。那一年,我犯了很多大錯誤,這些錯誤不僅幫助我了解了ML,還幫助我了解了如何正確而穩(wěn)健地設(shè)計這些系統(tǒng)。我在谷歌Brain學(xué)到的一個主要原則是,單元測試可以決定算法的成敗,可以為你節(jié)省數(shù)周的調(diào)試和訓(xùn)練時間。
然而,在如何為神經(jīng)網(wǎng)絡(luò)代碼編寫單元測試方面,似乎沒有一個可靠的在線教程。即使是像OpenAI這樣的地方,也只是通過盯著他們代碼的每一行,并試著思考為什么它會導(dǎo)致bug來發(fā)現(xiàn)bug的。顯然,我們大多數(shù)人都沒有這樣的時間,所以希望本教程能夠幫助你開始理智地測試你的系統(tǒng)!
讓我們從一個簡單的例子開始。試著找出這段代碼中的錯誤。
- def make_convnet(input_image):
- net = slim.conv2d(input_image, 32, [11, 11], scope="conv1_11x11")
- net = slim.conv2d(input_image, 64, [5, 5], scope="conv2_5x5")
- net = slim.max_pool2d(net, [4, 4], stride=4, scope='pool1')
- net = slim.conv2d(input_image, 64, [5, 5], scope="conv3_5x5")
- net = slim.conv2d(input_image, 128, [3, 3], scope="conv4_3x3")
- net = slim.max_pool2d(net, [2, 2], scope=)
- net = slim.conv2d(input_image, 128, [3, 3], scope="conv5_3x3")
- net = slim.max_pool2d(net, [2, 2], scope='pool3')
- net = slim.conv2d(input_image, 32, [1, 1], scope="conv6_1x1")
- return net
你看到了嗎?網(wǎng)絡(luò)實際上并沒有堆積起來。在編寫這段代碼時,我復(fù)制并粘貼了slim.conv2d(…)行,并且只修改了內(nèi)核大小,而沒有修改實際的輸入。
我很不好意思地說,這件事在一周前就發(fā)生在我身上了……但這是很重要的一課!由于一些原因,這些bug很難捕獲。
- 這段代碼不會崩潰,不會產(chǎn)生錯誤,甚至不會變慢。
- 這個網(wǎng)絡(luò)仍在運行,損失仍將下降。
- 幾個小時后,這些值就會收斂,但結(jié)果卻非常糟糕,讓你摸不著頭腦,不知道需要修復(fù)什么。
當(dāng)你唯一的反饋是最終的驗證錯誤時,你惟一需要搜索的地方就是你的整個網(wǎng)絡(luò)體系結(jié)構(gòu)。不用說,你需要一個更好的系統(tǒng)。
那么,在我們進行完整的多日訓(xùn)練之前,我們?nèi)绾握嬲プ∵@個機會呢?關(guān)于這個最容易注意到的是層的值實際上不會到達函數(shù)外的任何其他張量。假設(shè)我們有某種類型的損失和一個優(yōu)化器,這些張量永遠不會得到優(yōu)化,所以它們總是有它們的默認值。
我們可以通過簡單的訓(xùn)練步驟和前后對比來檢測它。
- def test_convnet():
- image = tf.placeholder(tf.float32, (None, 100, 100, 3)
- model = Model(image)
- sess = tf.Session()
- sess.run(tf.global_variables_initializer())
- before = sess.run(tf.trainable_variables())
- _ = sess.run(model.train, feed_dict={
- image: np.ones((1, 100, 100, 3)),
- })
- after = sess.run(tf.trainable_variables())
- for b, a, n in zip(before, after):
- # Make sure something changed.
- assert (b != a).any()
在不到15行代碼中,我們現(xiàn)在驗證了至少我們創(chuàng)建的所有變量都得到了訓(xùn)練。
這個測試超級簡單,超級有用。假設(shè)我們修復(fù)了前面的問題,現(xiàn)在我們要開始添加一些批歸一化。看看你能否發(fā)現(xiàn)這個bug。
- def make_convnet(image_input):
- # Try to normalize the input before convoluting
- net = slim.batch_norm(image_input)
- net = slim.conv2d(net, 32, [11, 11], scope="conv1_11x11")
- net = slim.conv2d(net, 64, [5, 5], scope="conv2_5x5")
- net = slim.max_pool2d(net, [4, 4], stride=4, scope='pool1')
- net = slim.conv2d(net, 64, [5, 5], scope="conv3_5x5")
- net = slim.conv2d(net, 128, [3, 3], scope="conv4_3x3")
- net = slim.max_pool2d(net, [2, 2], scope='pool2')
- net = slim.conv2d(net, 128, [3, 3], scope="conv5_3x3")
- net = slim.max_pool2d(net, [2, 2], scope='pool3')
- net = slim.conv2d(net, 32, [1, 1], scope="conv6_1x1")
- return net
你看到了嗎?這個非常微妙。您可以看到,在tensorflow batch_norm中,is_training的默認值是False,所以添加這行代碼并不能使你在訓(xùn)練期間的輸入正常化!值得慶幸的是,我們編寫的最后一個單元測試將立即發(fā)現(xiàn)這個問題!(我知道,因為這是三天前發(fā)生在我身上的事。)
再看一個例子。這實際上來自我一天看到的一篇文章
(https://www.reddit.com/r/MachineLearning/comments/6qyvvg/p_tensorflow_response_is_making_no_sense/)。我不會講太多細節(jié),但是基本上這個人想要創(chuàng)建一個輸出范圍為(0,1)的分類器。
- class Model:
- def __init__(self, input, labels):
- """Classifier model
- Args:
- input: Input tensor of size (None, input_dims)
- label: Label tensor of size (None, 1).
- Should be of type tf.int32.
- """
- prediction = self.make_network(input)
- # Prediction size is (None, 1).
- self.loss = tf.nn.softmax_cross_entropy_with_logits(
- logits=prediction, labelslabels=labels)
- self.train_op = tf.train.AdamOptimizer().minimize(self.loss)
注意到這個錯誤嗎?這是真的很難提前發(fā)現(xiàn),并可能導(dǎo)致超級混亂的結(jié)果。基本上,這里發(fā)生的是預(yù)測只有一個輸出,當(dāng)你將softmax交叉熵應(yīng)用到它上時,它的損失總是0。
一個簡單的測試方法是確保損失不為0。
- def test_loss():
- in_tensor = tf.placeholder(tf.float32, (None, 3))
- labels = tf.placeholder(tf.int32, None, 1))
- model = Model(in_tensor, labels)
- sess = tf.Session()
- loss = sess.run(model.loss, feed_dict={
- in_tensor:np.ones(1, 3),
- labels:[[1]]
- })
- assert loss != 0
另一個很好的測試與我們的第一個測試類似,但是是反向的。你可以確保只有你想訓(xùn)練的變量得到了訓(xùn)練。以GAN為例。出現(xiàn)的一個常見錯誤是在進行優(yōu)化時不小心忘記設(shè)置要訓(xùn)練的變量。這樣的代碼經(jīng)常發(fā)生。
- class GAN:
- def __init__(self, z_vector, true_images):
- # Pretend these are implemented.
- with tf.variable_scope("gen"):
- self.make_geneator(z_vector)
- with tf.variable_scope("des"):
- self.make_descriminator(true_images)
- opt = tf.AdamOptimizer()
- train_descrim = opt.minimize(self.descrim_loss)
- train_gen = opt.minimize(self.gen_loss)
這里最大的問題是優(yōu)化器有一個默認設(shè)置來優(yōu)化所有變量。在像GANs這樣的高級架構(gòu)中,這是對你所有訓(xùn)練時間的死刑判決。但是,你可以通過編寫這樣的測試來輕松地發(fā)現(xiàn)這些錯誤:
- def test_gen_training():
- model = Model
- sess = tf.Session()
- gen_vars = tf.get_collection(tf.GraphKeys.VARIABLES, scope='gen')
- des_vars = tf.get_collection(tf.GraphKeys.VARIABLES, scope='des')
- before_gen = sess.run(gen_vars)
- before_des = sess.run(des_vars)
- # Train the generator.
- sess.run(model.train_gen)
- after_gen = sess.run(gen_vars)
- after_des = sess.run(des_vars)
- # Make sure the generator variables changed.
- for b,a in zip(before_gen, after_gen):
- assert (a != b).any()
- # Make sure descriminator did NOT change.
- for b,a in zip(before_des, after_des):
- assert (a == b).all()
可以為鑒別器編寫一個非常類似的測試。同樣的測試也可以用于許多強化學(xué)習(xí)算法。許多行為-批評模型有單獨的網(wǎng)絡(luò),需要根據(jù)不同的損失進行優(yōu)化。
下面是一些我推薦你進行測試的模式。
- 讓測試具有確定性。如果一個測試以一種奇怪的方式失敗,卻永遠無法重現(xiàn)這個錯誤,那就太糟糕了。如果你真的想要隨機輸入,確保使用種子隨機數(shù),這樣你就可以輕松地重新運行測試。
- 保持測試簡短。不要使用單元測試來訓(xùn)練收斂性并檢查驗證集。這樣做是在浪費自己的時間。
- 確保你在每個測試之間重置了計算圖。
總之,這些黑箱算法仍然有很多方法需要測試!花一個小時寫一個測試可以節(jié)省你幾天的重新運行訓(xùn)練模型,并可以大大提高你的研究效率。因為我們的實現(xiàn)有缺陷而不得不放棄完美的想法,這不是很糟糕嗎?
這個列表顯然不全面,但它是一個堅實的開始!

























