master
/ 4 mnist_playground.ipynb

4 mnist_playground.ipynb @4b0cb67

4b0cb67
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Keras 基础介绍"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"https://s3.amazonaws.com/keras.io/img/keras-logo-2018-large-1200.png\" width=300>\n",
    "\n",
    "Keras 是一个用 Python 编写的高级神经网络 API,它能够以 TensorFlow,CNTK 或者 Theano 作为后端运行。\n",
    "\n",
    "Keras 具有如下优点:\n",
    "- 由于用户友好,高度模块化,可扩展性,可以简单而快速的进行原型设计。\n",
    "- 同时支持卷积神经网络和循环神经网络,以及两者的组合。\n",
    "- 在 CPU 和 GPU 上无缝运行。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Keras 的核心数据结构是 model,一种组织网络层的方式。最简单的模型是 Sequential 顺序模型,它把多个网络层线性堆叠起来。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Sequential 模型如下所示:"
   ]
  },
  {
   "cell_type": "raw",
   "metadata": {},
   "source": [
    "from keras.models import Sequential\n",
    "\n",
    "model = Sequential()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "可以简单地使用 .add() 来堆叠模型:"
   ]
  },
  {
   "cell_type": "raw",
   "metadata": {},
   "source": [
    "from keras.layers import Dense\n",
    "\n",
    "model.add(Dense(units=64, activation='relu', input_dim=100))\n",
    "model.add(Dense(units=10, activation='softmax'))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在完成了模型的构建后, 可以使用 .compile() 来配置学习过程:"
   ]
  },
  {
   "cell_type": "raw",
   "metadata": {},
   "source": [
    "model.compile(loss='categorical_crossentropy',\n",
    "              optimizer='sgd',\n",
    "              metrics=['accuracy'])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "然后,就可以批量地在训练数据上进行迭代了。"
   ]
  },
  {
   "cell_type": "raw",
   "metadata": {},
   "source": [
    "# x_train 和 y_train 是 Numpy 数组 -- 就像在 Scikit-Learn API 中一样。\n",
    "model.fit(x_train, y_train, epochs=5, batch_size=32)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "只需一行代码就能评估模型性能:"
   ]
  },
  {
   "cell_type": "raw",
   "metadata": {},
   "source": [
    "loss_and_metrics = model.evaluate(x_test, y_test, batch_size=128)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "或者对新的数据生成预测:"
   ]
  },
  {
   "cell_type": "raw",
   "metadata": {},
   "source": [
    "classes = model.predict(x_test, batch_size=128)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# MNIST Playground\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "欢迎来到 MINST Playgound,在这里,你将了解到数据增强技术,并可以通过拖拽一个控制面板来创建深度学习模型,识别手写数字。无需编写任何代码,快来试试吧。\n",
    "\n",
    "*你需要按顺序运行下方每个 cell,直到看到控制面板,然后即可点击或拖拽来创建模型并训练。*"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "Using TensorFlow backend.\n"
     ]
    }
   ],
   "source": [
    "# 导入必要的包\n",
    "\n",
    "!mkdir -p ~/.keras/datasets\n",
    "!cp ./mnist.npz ~/.keras/datasets/mnist.npz\n",
    "\n",
    "\n",
    "%matplotlib inline\n",
    "import time\n",
    "import matplotlib.pyplot as plt\n",
    "import numpy as np\n",
    "import imgaug\n",
    "import imgaug.augmenters as iaa\n",
    "from ipywidgets import interact, interactive, fixed, interact_manual\n",
    "from ipywidgets import IntSlider, FloatSlider, Dropdown, Checkbox, Button, Output, \\\n",
    "                       SelectMultiple,Image, HBox, VBox,SelectionSlider, HTML\n",
    "from IPython import display\n",
    "import tensorflow as tf\n",
    "from tensorflow.keras.datasets import mnist\n",
    "from tensorflow import keras\n",
    "from tensorflow.python.util import deprecation\n",
    "from keras.layers import Input, Dense,Flatten\n",
    "from keras.models import Model\n",
    "from keras.callbacks import Callback\n",
    "from keras.utils.np_utils import to_categorical\n",
    "from keras.utils import model_to_dot\n",
    "deprecation._PRINT_DEPRECATION_WARNINGS = False\n",
    "sometimes = lambda aug: iaa.Sometimes(0.5, aug)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 处理数据\n",
    "首先我们定义一个 MNIST 类,它将负责管理 MNIST 数据集"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "class MNIST:\n",
    "    \"\"\" 用来管理 MNIST 数据集的类\n",
    "    \n",
    "        Attributes:\n",
    "            x_train:  MNIST 训练集的特征数据\n",
    "            y_train:  MNIST 训练集的标签数据\n",
    "            x_test:  MNIST 测试集的特征数据\n",
    "            y_test:  MNIST 测试集的标签数据\n",
    "            train_size: MNIST 训练集的样本数\n",
    "            test_size: MNIST 测试集的样本数\n",
    "            augmented: 是否对训练集数据进行数据增强\n",
    "            seq: 进行数据增强的 pipline\n",
    "    \"\"\"\n",
    "    def __init__(self):\n",
    "        \"\"\"初始化 MNIST 类\n",
    "        \"\"\"\n",
    "        (self.x_train, self.y_train), (\n",
    "        self.x_test, self.y_test) = mnist.load_data()\n",
    "        self.x_train = self.x_train.reshape(self.x_train.shape[0], 28, 28, 1)\n",
    "        self.x_test = self.x_test.reshape(self.x_test.shape[0], 28, 28, 1)\n",
    "        self.x_train = self.x_train.astype('float32')\n",
    "        self.x_test = self.x_test.astype('float32')\n",
    "        self.train_size = self.x_train.shape[0]\n",
    "        self.test_size = self.x_test.shape[0]\n",
    "        self.y_train = to_categorical(self.y_train, 10)\n",
    "        self.y_test = to_categorical(self.y_test, 10)\n",
    "        self.noise = 0\n",
    "        self.rotation = 0\n",
    "        self.aug_possibility = 0 \n",
    "        self.seq = None\n",
    "        \n",
    "    def update_noise(self, noise):\n",
    "        \"\"\"数据增强中是否增加 noise\n",
    "        :param noise: 噪声比例\n",
    "        :return:\n",
    "        \"\"\"\n",
    "        self.noise = noise\n",
    "        self.update_aug_seq()\n",
    "        \n",
    "    def update_rotation(self, rotation):\n",
    "        \"\"\"数据增强中图片旋转的最大角度\n",
    "        :param rotation: 图片旋转的最大角度值\n",
    "        :return:\n",
    "        \"\"\"\n",
    "        self.rotation = rotation\n",
    "        self.update_aug_seq()\n",
    "    \n",
    "    def update_aug_possibility(self, possibility):\n",
    "        \"\"\"数据增强中图片旋转的最大角度\n",
    "        :param rotation: 图片旋转的最大角度值\n",
    "        :return:\n",
    "        \"\"\"\n",
    "        self.aug_possibility = possibility\n",
    "        self.update_aug_seq()\n",
    "        \n",
    "    def update_aug_seq(self):\n",
    "        \"\"\"更新数据增强的 pipline\n",
    "        :return:None\n",
    "        \"\"\"\n",
    "        if self.aug_possibility:\n",
    "            aug_seq = []\n",
    "            sometimes = lambda aug: iaa.Sometimes(self.aug_possibility, aug)\n",
    "            if self.noise:\n",
    "                aug_seq.append(sometimes(iaa.Salt(self.noise))) \n",
    "            if self.rotation:\n",
    "                aug_seq.append(sometimes(iaa.Affine(\n",
    "                        rotate=(-self.rotation, self.rotation), \n",
    "                    )))\n",
    "            self.seq = iaa.Sequential(aug_seq)\n",
    "        else:\n",
    "            self.seq = None\n",
    "\n",
    "    def get_batch(self, batch_size, get_sample=False):\n",
    "        \"\"\"获取一个 batch 的训练数据(原始数据或者经过数据增强后的数据)\n",
    "        :param batch_size: 一个 batch 所包含的图片数目\n",
    "        :param get_sample: 是否返回固定样本来做展示\n",
    "        :return:\n",
    "        \"\"\"\n",
    "        while True:\n",
    "            if get_sample:\n",
    "                randidx = [i for i in range(batch_size)]\n",
    "            else:\n",
    "                randidx = np.random.randint(self.train_size, size=batch_size)\n",
    "            epoch_x = self.x_train[randidx]\n",
    "            epoch_y = self.y_train[randidx]\n",
    "            if self.aug_possibility and (self.noise or self.rotation):\n",
    "                epoch_x = self.seq(images=epoch_x)\n",
    "            epoch_x /= 255.0\n",
    "            yield epoch_x, epoch_y\n",
    "\n",
    "    def get_batch_test(self, batch_size):\n",
    "        \"\"\"获取一个 batch 的测试数据\n",
    "        :param batch_size: 一个 batch 所包含的图片数目\n",
    "        :return:\n",
    "        \"\"\"\n",
    "        while True:\n",
    "            randidx = np.random.randint(self.test_size, size=batch_size)\n",
    "            epoch_x = self.x_test[randidx]\n",
    "            epoch_y = self.y_test[randidx]\n",
    "            epoch_x /= 255.0\n",
    "            yield epoch_x, epoch_y\n",
    "\n",
    "    def plot_images(self, show=True):\n",
    "        \"\"\"绘制几个样本图片\n",
    "        :param show: 是否显示绘图\n",
    "        :return:\n",
    "        \"\"\"\n",
    "        sample_num = 9\n",
    "        imgs, _ = next(self.get_batch(sample_num,get_sample=True))\n",
    "        img_figure = plt.figure(1)\n",
    "        img_figure.set_figwidth(5)\n",
    "        img_figure.set_figheight(5)\n",
    "        for index in range(0, sample_num):\n",
    "            ax = plt.subplot(3, 3, index + 1)\n",
    "            ax.imshow(imgs[index].reshape(28, 28), cmap='gray')\n",
    "        plt.margins(0, 0)\n",
    "        img_figure.savefig('data_sample.jpg')\n",
    "        if not show:\n",
    "            plt.close(fig=img_figure)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 数据增强技术"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAToAAAEyCAYAAABqERwxAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDMuMC4zLCBodHRwOi8vbWF0cGxvdGxpYi5vcmcvnQurowAAIABJREFUeJzt3Xm0FMX1B/DvlQCiiAKaJ24sCkFUFNwQOUAUN1xwiagBBUPE4woeNeISl7iBJp7gLiqCygkhQQGNBAmCuCARDf4iiwIq8pRF3FhUCFq/P6bnvvseM8zW3dPd8/2c8867r2bpGq9TdHVVV4lzDkRESbZduStARBQ0NnRElHhs6Igo8djQEVHisaEjosRjQ0dEiceGjogSr6SGTkROFJEPRGSpiAzzq1JUOOYiGpiHaJJiJwyLSD0AHwI4DkA1gLcBnOecW+hf9SgfzEU0MA/R9bMSXnsEgKXOuY8AQETGA+gDIGtSRYS3YZRmrXNutwzlBeWCeSiZL3nwnsNclCZbLmoppeu6J4AV5u9qr6wWERksIvNEZF4Jx6KU5VnKc+aCefBV0XkAmAufZctFLaWc0eXFOTcKwCiA/3qVE/MQHcxF+Eo5o/sMwN7m7728MgofcxENzENEldLQvQ2grYi0FpEGAM4FMMWfalGBmItoYB4iquiuq3Nui4hcDmAagHoARjvnFvhWM8obcxENzEN0FT29pKiD8XpEqd5xzh1W6pswDyXzJQ8Ac+GDvHLBOyOIKPHY0BFR4rGhI6LEY0NHRIkX+IRholIceuihGl9++eUaX3DBBQCAp59+WsseeOABjd99990QakdxwTM6Iko8NnRElHgVOY+uXr16Gu+88845n2+7TDvssAMA4Be/+IWWXXbZZRr/8Y9/1Pi8887T+IcfftB4+PDhGt922235VhuokHl0hxxyiMavvPKKxk2aNNnm67799luNmzdv7n/FanAeXQGOPfZYjceNG6dxjx49NP7ggw+KfXvOoyMiAtjQEVEFSMyo6z777KNxgwYNNO7atSsAoFu3blq2yy67aHzWWWcVdbzq6mqN77//fo3POOMMjdevX6/xe++9p/Grr75a1DGT7IgjjtB44sSJGttLC/YyS/q/7ebNm7XMdle7dOmisR2Btc+Pm+7du2tsP+vzzz9fjurk7fDDD9f47bffLksdeEZHRInHho6IEi/WXddso3P5jKQW66effgIA3HTTTVq2YcMGje2o0sqVKzX++uuvNS5hhCn20qPWANC5c2eNn332WY1btGiR832WLFkCALjnnnu0bPz48Rq/8cYbGttc3X333QXWODp69uypcdu2bTWOYtd1u+1qzqFat26tccuWLTUWkfDqE9qRiIjKhA0dESVerLuun376qcZffvmlxsV2XefOnavxN998o/Evf/lLjdOjds8880xRx6h0jz32mMZ2QnWh0t3exo0ba5kdzbbdvI4dOxZ9nChJ398LAHPmzCljTXKzlx8uuugije0lisWLF4dWH57REVHisaEjosSLddf1q6++0vjaa6/V+JRTTtH4P//5D4Dak3qt+fPna3zcccdpvHHjRo0POOAAjYcMGVJCjStXermlk08+WcuyjbrZLugLL7ygsb2P+PPPPwdQk1+g9sj2Mccck/M4cWNHMqPuiSeeyFieHi0PW87/ciIyWkTWiMj7pqyZiEwXkSXe76bBVpM8rZiLSGAeYiaffyLGADixTtkwADOcc20BzPD+puCtBXMRBcxDzOTsujrnZotIqzrFfQD09OKxAGYBuM7HehVs0qRJGtvJw+l7Ig8++GAtGzRokMa2O2S7q9aCBTVbcw4ePLj0yhZvA4Cv6pRFLhdpdkL39OnTAdReasneuzp16lSN7WisXcrHTvxNd42++OILLbP3E6cndgO1u8t2knIJqxCHmof0qHFVVZUfbxeKbDMf0v8fhK3Ya3RVzrn0tP9VALJmQEQGAyhr65BweeWCeQgcvxMRVvJghHPObWvxQOfcKACjgPAWGVy3bt1WZXZRRsvO8fnrX/+qsT0jiItt5SKsPLRr105jO0CU/hd+7dq1WmZvkRs7dqzG9pa6f/zjHxnjQjRq1Ejjq6++WuN+/foV9X65+P2d6N27N4DanyOK7Bmnve3L+uyzz8KqTi3FDuOsFpEWAOD9XuNflahAzEU0MA8RVmxDNwXAAC8eAGCyP9WhIjAX0cA8RFjOrquI/AWpi6y7ikg1gFsADAcwQUQGAVgOoG+QlfTDrbfeqrHdQs9e7O7Vq5fGL7/8cij1KlBrAHMQsVw0bNhQYzu4k+5yATWDQvY2pnnz5mkcVrfMLtBaglDzYPcnSbMDZFFhc2+7sR9++KHGdjHaMOUz6prthsRj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TkQkF7WEOWIEoDdSIzDLANxY7tEhP34AdEPqFPz/AMz3fnoDaA5gBoAlAP4FoFmAdSgoF8xDNPLAXASXi63qVMKHOQrANPP39QCuz/Eax5+Sfr7wIxcR+Bxx//ElD8xFcLmo+1NK1zWvESMRGSwi80RkXgnHopTlWcpz5oJ58FXReQCYC59ly0Utga8w7JwbBW/lAm4EUj7MQ3QwF+Er5YwusSNGMcRcRAPzEFGlNHSJHDGKKeYiGpiHiCq66+qc2yIilwOYhtRo02jn3ALfakZ5Yy6igXmILm5gHS/cwDoauIF1dHADayIigA0dEVUANnRElHhs6Igo8QKfMBxFI0eO1PjKK6/U+P3339f4lFNO0Xj58rwmXxNRRPGMjogSjw0dESVexXRdW7VqpXH//v01/umnnzTef//9NW7fvr3G7Lr6p127dhrXr19f4+7du2v88MMPa2zzU4jJk2uWOjv33HM13rx5c1Hvl3Q2F127dtX4rrvu0vjoo48OtU5+4hkdESUeGzoiSryK6bp+8cUXGs+ePVvj0047rRzVqQgHHHCAxgMHDgQAnH322Vq23XY1/87uscceGtvuarG3KNq8PvrooxoPHTpU43Xrau3XUtF23nlnjWfOnKnxqlWrNN59990zlscBz+iIKPHY0BFR4lVM13Xjxo0acxQ1HHfffbfGvXv3Lls9LrjgAo2ffPJJjd94441yVCdWbHeVXVcioghjQ0dEiVcxXddddtlF44MPPriMNakc06dP1zhT13XNmpqN2m2X0o7GZpswnJ7U2qNHj5LrSdml9qKOP57REVHisaEjosSrmK7rDjvsoPE+++yT8/mHH364xosXL9aYI7b5e+SRRzSeNGnSVo//73//07jQUbwmTZoAqL20lp10bNljz5vHPaMLYSdsb7/99mWsSWlyntGJyGgRWSMi75uyZiIyXUSWeL+bBltN8rRiLiKBeYiZfLquYwCcWKdsGIAZzrm2AGZ4f1Pw1oK5iALmIWZydl2dc7NFpFWd4j4AenrxWACzAFznY7189/nnn2s8ZswYjW+99daMz7fl33zzjcYPPvig31UrxAYAX9Upi2wutmzZovGKFSt8fe8TTjgBANC0ae4Tp+rqao03bdrkx+FjlQe/HHZYza6Cb731VhlrUrhir9FVOedWevEqAFXZnigigwEMLvI4lFteuWAe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\n",
      "text/plain": [
       "<Figure size 360x360 with 9 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "# 查看原始图片\n",
    "mnist_data = MNIST()\n",
    "mnist_data.plot_images()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "数据增强技术就是利用有限的数据产生更多的等价数据。我们可以对原始的图片进行平移、旋转、缩放、加噪声等技术处理,来产生新的图片。\n",
    "比如我们对一张小狗的图片进行旋转,加噪声,缩放等处理后,可以得到 6 张新的图片。\n",
    "\n",
    "使用数据增强技术可以拓展我们的数据集,提高模型的识别性能和泛化能力。\n",
    "\n",
    "<img src='https://miro.medium.com/max/3262/1*7udw5GZHwVo6u-V0lzglvQ.png' width=800 />\n",
    "\n",
    "下面我们对 mnist 数据集进行加噪声和旋转的处理。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "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\n",
      "text/plain": [
       "<Figure size 360x360 with 9 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "mnist_data.update_noise(0.2)\n",
    "mnist_data.update_rotation(15)\n",
    "mnist_data.update_aug_possibility(0.5)\n",
    "mnist_data.plot_images()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 定义模型\n",
    "下面我们先定义一个 DrawCallback 类,用来在每个训练 epoch 结束后,打印出训练曲线。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class DrawCallback(Callback):\n",
    "    \"\"\" 进行模型训练的 callback 类,用来绘制训练图\n",
    "\n",
    "    Attributes:\n",
    "        init_loss:  初始的 loss 值\n",
    "        epoch_data:  已经完成的 epoch 数的列表\n",
    "        loss_data:  每个 epoch 的训练数据的 loss 值的列表\n",
    "        val_loss_data:  每个 epoch 的测试数据的 loss 值的列表\n",
    "        accuracy_data: 每个 epoch 的训练数据的 acc 值的列表\n",
    "        val_accuracy_data: 每个 epoch 的测试数据的 acc 值的列表\n",
    "        best_val_acc: 测试数据的最佳的 acc\n",
    "\n",
    "    \"\"\"\n",
    "    def __init__(self):\n",
    "        \"\"\"初始化 DrawCallback 类\n",
    "        \"\"\"\n",
    "        super().__init__()\n",
    "        self.init_loss = None\n",
    "        self.epoch_data = []\n",
    "        self.loss_data = []\n",
    "        self.val_loss_data = []\n",
    "        self.accuracy_data = []\n",
    "        self.val_accuracy_data = []\n",
    "        self.best_val_acc = 0\n",
    "\n",
    "    def runtime_plot(self, epoch=None):\n",
    "        \"\"\"绘制训练曲线\n",
    "        :param epoch: 当前进行到的 epoch 数\n",
    "        :return: None\n",
    "        \"\"\"\n",
    "        # 总的 epoch 数\n",
    "        epochs = self.params.get(\"epochs\")\n",
    "        # 定义图片尺寸\n",
    "        img_figure = plt.figure(1)\n",
    "        img_figure.set_figwidth(16)\n",
    "        img_figure.set_figheight(5)\n",
    "        # 绘制 loss 曲线\n",
    "        ax1 = plt.subplot(1, 2, 1)\n",
    "        ax1.set_ylim(0, int(self.init_loss * 3))\n",
    "        ax1.set_xlim(1, epochs)\n",
    "        ax1.plot(self.epoch_data, self.loss_data, 'b', label='loss_train')\n",
    "        ax1.plot(self.epoch_data, self.val_loss_data, 'r', label='loss_val')\n",
    "        ax1.set_xlabel('Epoch {}/{}'.format(epoch, epochs))\n",
    "        ax1.set_ylabel('Loss')\n",
    "        ax1.legend()\n",
    "        # 绘制 acc 曲线\n",
    "        ax2 = plt.subplot(1, 2, 2)\n",
    "        ax2.set_ylim(0, 1)\n",
    "        ax2.set_xlim(1, epochs)\n",
    "        ax2.plot(self.epoch_data, self.accuracy_data, 'b', label='acc_train')\n",
    "        ax2.plot(self.epoch_data, self.val_accuracy_data, 'r', label='acc_val')\n",
    "        ax2.set_xlabel('Epoch {}/{}'.format(epoch, epochs))\n",
    "        ax2.set_ylabel('ACC')\n",
    "        ax2.legend()\n",
    "        # 清除历史图片\n",
    "        display.clear_output(wait=True)\n",
    "        # 展示新图片\n",
    "        plt.show()\n",
    "\n",
    "    def on_epoch_end(self, epoch, logs=None):\n",
    "        \"\"\"在每个 epoch 结束后,存储 loss 值和 acc 值,并更新训练曲线图\n",
    "        :param epoch: 当前进行到的 epoch 数\n",
    "        :param logs: 当前 epoch 返回的 log\n",
    "        :return: None\n",
    "        \"\"\"\n",
    "        # 从 logs 中获取 loss 和 acc 的值并存到对应的 list 里\n",
    "        epoch = epoch + 1\n",
    "        logs = logs or {}\n",
    "        loss = logs.get(\"loss\")\n",
    "        val_loss = logs.get(\"val_loss\")\n",
    "        accuracy = logs.get(\"accuracy\")\n",
    "        val_accuracy = logs.get(\"val_accuracy\")\n",
    "        if val_accuracy > self.best_val_acc:\n",
    "            self.best_val_acc = val_accuracy\n",
    "        if self.init_loss is None:\n",
    "            self.init_loss = loss\n",
    "        self.epoch_data.append(epoch)\n",
    "        self.loss_data.append(loss)\n",
    "        self.val_loss_data.append(val_loss)\n",
    "        self.accuracy_data.append(accuracy)\n",
    "        self.val_accuracy_data.append(val_accuracy)\n",
    "        # 绘制训练图\n",
    "        self.runtime_plot(epoch)\n",
    "        # 如果训练完成,打印最终的结果\n",
    "        if epoch == self.params.get(\"epochs\"):\n",
    "            print('训练完成')\n",
    "            print('训练集准确率: {:.2%}'.format(accuracy))\n",
    "            print('测试集准确率: {:.2%}'.format(val_accuracy))\n",
    "            print('最高测试集准确率:{:.2%}'.format(self.best_val_acc))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "然后我们定义一个 NN 类,我们将基于此类来构建和管理模型。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class NN(object):\n",
    "    def __init__(self):\n",
    "        \"\"\"初始化 NN 类,构建一个多层全连接神经网络\n",
    "        Attributes:\n",
    "            layers: 隐藏层的数目\n",
    "            neurons: 每一层隐藏层的神经元的个数\n",
    "            epochs: 模型训练的轮数\n",
    "            model: 构建后的模型\n",
    "        \"\"\"\n",
    "        self.layers = 1\n",
    "        self.neurons = [8]\n",
    "        self.epochs = 5\n",
    "        self.model = None\n",
    "\n",
    "    def build_model(self):\n",
    "        \"\"\"构建并编译模型\n",
    "        :return: None\n",
    "        \"\"\"\n",
    "        # 定义输入层和 flatten 层\n",
    "        inputs = Input(shape=(28, 28, 1,))\n",
    "        output = Flatten()(inputs)\n",
    "        # 定义隐藏层\n",
    "        for neuron in self.neurons:\n",
    "            output = Dense(neuron, activation='relu')(output)\n",
    "        # 定义输出层\n",
    "        predictions = Dense(10, activation='softmax')(output)\n",
    "        # 构建模型\n",
    "        self.model = Model(inputs=inputs, outputs=predictions)\n",
    "        # 编译模型\n",
    "        self.model.compile(loss='categorical_crossentropy',\n",
    "                           optimizer='rmsprop',\n",
    "                           metrics=['accuracy'])\n",
    "\n",
    "    def start_train(self, data, validation_data):\n",
    "        \"\"\"开始训练模型\n",
    "        :param data: 训练数据\n",
    "        :param validation_data: 测试数据\n",
    "        :return: None\n",
    "        \"\"\"\n",
    "        plot_callback = DrawCallback()\n",
    "        self.model.fit_generator(data,\n",
    "                                 validation_data=validation_data,\n",
    "                                 validation_steps=300,\n",
    "                                 steps_per_epoch=2000, \n",
    "                                 epochs=self.epochs, \n",
    "                                 verbose=1,\n",
    "                                 callbacks=[plot_callback])\n",
    "\n",
    "    def plot_model(self):\n",
    "        \"\"\"保存模型结构图\n",
    "        :return: None\n",
    "        \"\"\"\n",
    "        keras.utils.plot_model(\n",
    "            self.model,\n",
    "            to_file='model.jpg',\n",
    "            show_shapes=True,\n",
    "            show_layer_names=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 操作面板\n",
    "下面的代码将创建出一个可交互的操作面板,你可以通过此面板来进行数据增强,模型定义和训练的操作,并查看训练曲线图。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "mlp = NN()\n",
    "mlp.build_model()\n",
    "mnist_data = MNIST()\n",
    "## 定义操作界面\n",
    "# 隐藏层数目\n",
    "hidden_layers_widget = IntSlider(min=0, \n",
    "                                 max=3,\n",
    "                                 value=1,\n",
    "                                 description='隐藏层数目:',\n",
    "                                 layout={'width': '400px'},\n",
    "                                 style={'description_width': 'initial'})\n",
    "# 每层隐藏层包含的神经元数目\n",
    "neuron_widgets = VBox([SelectionSlider(options=[8,16, 32, 64,128,256,512],\n",
    "    value=8,\n",
    "    description='隐藏层1的神经元数目:',\n",
    "    continuous_update=False,\n",
    "    orientation='horizontal',\n",
    "    readout=True,\n",
    "    layout={'width': '400px'},\n",
    "    style={'description_width': 'initial'})])\n",
    "\n",
    "# 开始训练的按钮\n",
    "start_train_widget = Button(\n",
    "    description='开始训练',\n",
    "    button_style='info',\n",
    "    tooltip='点击开始训练',\n",
    ")\n",
    "\n",
    "output_widget = Output()\n",
    "\n",
    "data_head_widget = HTML(\n",
    "    value=\"<h1>准备数据</h1>\",\n",
    "    placeholder='',\n",
    ")\n",
    "\n",
    "data_aug_possibility_widget = FloatSlider(\n",
    "    value=0,\n",
    "    min=0,\n",
    "    max=1.0,\n",
    "    step=0.1,\n",
    "    description='每个 batch 内增强图片的比例:',\n",
    "    disabled=False,\n",
    "    continuous_update=False,\n",
    "    orientation='horizontal',\n",
    "    readout=True,\n",
    "    readout_format='.1f',\n",
    "    layout={'width': '400px'},\n",
    "    style={'description_width': 'initial'}\n",
    ")\n",
    "\n",
    "\n",
    "data_noise_widget = FloatSlider(\n",
    "    value=0,\n",
    "    min=0,\n",
    "    max=0.2,\n",
    "    step=0.05,\n",
    "    description='为图片增加白噪声点:',\n",
    "    disabled=False,\n",
    "    continuous_update=False,\n",
    "    orientation='horizontal',\n",
    "    readout=True,\n",
    "    readout_format='.2f',\n",
    "    layout={'width': '400px'},\n",
    "    style={'description_width': 'initial'}\n",
    ")\n",
    "\n",
    "data_rotate_widget = SelectionSlider(options=[0, 10, 20, 30],\n",
    "                                     value=0,\n",
    "                                     description='图片左右旋转的最大角度:',\n",
    "                                     continuous_update=False,\n",
    "                                     orientation='horizontal',\n",
    "                                     readout=True,\n",
    "                                     layout={'width': '400px'},\n",
    "                                     style={'description_width': 'initial'})\n",
    "\n",
    "def plot_sample_data():\n",
    "    mnist_data.plot_images(show=False)\n",
    "    file = open(\"data_sample.jpg\", \"rb\")\n",
    "    image = file.read()\n",
    "    show_data_sample_widget.value = image\n",
    "\n",
    "def update_data_noise(*args):\n",
    "    mnist_data.update_noise(data_noise_widget.value)\n",
    "    plot_sample_data()\n",
    "\n",
    "def update_data_rotation(*args):\n",
    "    mnist_data.update_rotation(data_rotate_widget.value)\n",
    "    plot_sample_data() \n",
    "    \n",
    "def update_data_aug_possibility(*args):\n",
    "    mnist_data.update_aug_possibility(data_aug_possibility_widget.value)\n",
    "    plot_sample_data()   \n",
    "\n",
    "data_noise_widget.observe(update_data_noise, 'value') \n",
    "data_rotate_widget.observe(update_data_rotation, 'value') \n",
    "data_aug_possibility_widget.observe(update_data_aug_possibility, 'value') \n",
    "\n",
    "show_data_sample_widget = Image(\n",
    "    value=b'',\n",
    "    format='jpg',\n",
    "    width=200,\n",
    ")\n",
    "\n",
    "plot_sample_data()\n",
    "\n",
    "model_head_widget = HTML(\n",
    "    value=\"<h1>构建模型</h1>\",\n",
    "    placeholder='',\n",
    ")\n",
    "\n",
    "show_model_widget = Image(\n",
    "    value=b'',\n",
    "    format='jpg',\n",
    "    width=250,\n",
    ")\n",
    "\n",
    "def build_plot_model(*args):\n",
    "    keras.backend.clear_session()\n",
    "    mlp.build_model()\n",
    "    mlp.plot_model()\n",
    "    file = open(\"model.jpg\", \"rb\")\n",
    "    image = file.read()\n",
    "    show_model_widget.value = image\n",
    "\n",
    "build_plot_model()\n",
    "\n",
    "\n",
    "# 更改隐藏层神经元数量时,更新模型实例的参数\n",
    "def update_neuron_number(*args):\n",
    "    mlp.neurons = [c.value for c in neuron_widgets.children]\n",
    "    \n",
    "for e in neuron_widgets.children:\n",
    "    e.observe(update_neuron_number, 'value')\n",
    "    e.observe(build_plot_model, 'value')\n",
    "    \n",
    "\n",
    "# 更改隐藏层数目时,动态增加或减小控制每一层神经元数量的按钮,同时更新模型实例的参数\n",
    "def update_layers(*args):\n",
    "    if hidden_layers_widget.value > mlp.layers:\n",
    "        neuron_widgets.children += (SelectionSlider(options=[8,16, 32, 64,128,256,512],\n",
    "                                                    value=8,\n",
    "                                                    description='隐藏层{}的神经元数目:'.format(hidden_layers_widget.value),\n",
    "                                                    continuous_update=False,\n",
    "                                                    orientation='horizontal',\n",
    "                                                    readout=True,\n",
    "                                                   layout={'width': '400px'},\n",
    "                                  style={'description_width': 'initial'}),)\n",
    "    else:\n",
    "        neuron_widgets.children = neuron_widgets.children[:-1]\n",
    "    mlp.layers = hidden_layers_widget.value\n",
    "    mlp.neurons = [c.value for c in neuron_widgets.children]\n",
    "    for e in neuron_widgets.children:\n",
    "        e.observe(update_neuron_number, 'value')\n",
    "        e.observe(build_plot_model, 'value')\n",
    "    build_plot_model()\n",
    "\n",
    "hidden_layers_widget.observe(update_layers, 'value')    \n",
    "\n",
    "\n",
    "data_H_widget = HBox([VBox([data_aug_possibility_widget, \n",
    "                            data_noise_widget, \n",
    "                            data_rotate_widget]), \n",
    "                      show_data_sample_widget])\n",
    "\n",
    "model_H_widget = HBox([VBox([hidden_layers_widget ,\n",
    "                             neuron_widgets]), \n",
    "                       show_model_widget])\n",
    "\n",
    "train_head_widget = HTML(\n",
    "    value=\"<h1>训练模型</h1>\",\n",
    "    placeholder='',\n",
    ")\n",
    "\n",
    "epochs_widget = SelectionSlider(options=[5, 10, 20, 50, 100],\n",
    "                                     value=5,\n",
    "                                     description='训练轮数:',\n",
    "                                     continuous_update=False,\n",
    "                                     orientation='horizontal',\n",
    "                                     readout=True,\n",
    "                                     layout={'width': '400px'},\n",
    "                                     style={'description_width': 'initial'})\n",
    "\n",
    "def update_epochs(*args):\n",
    "    mlp.epochs = epochs_widget.value\n",
    "\n",
    "epochs_widget.observe(update_epochs, 'value') \n",
    "\n",
    "\n",
    "    \n",
    "def on_button_clicked(b):\n",
    "    with output_widget:\n",
    "        display.clear_output(wait=True)\n",
    "        mlp.start_train(mnist_data.get_batch(32),\n",
    "                      validation_data=mnist_data.get_batch_test(32))\n",
    "        \n",
    "start_train_widget.on_click(on_button_clicked) \n",
    "\n",
    "display.display(data_head_widget, \n",
    "                data_H_widget, \n",
    "                model_head_widget, \n",
    "                model_H_widget , \n",
    "                train_head_widget, \n",
    "                epochs_widget, \n",
    "                start_train_widget, \n",
    "                output_widget)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.5.2"
  },
  "pycharm": {
   "stem_cell": {
    "cell_type": "raw",
    "metadata": {
     "collapsed": false
    },
    "source": []
   }
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}