{"cells":[{"metadata":{},"cell_type":"markdown","source":"# 概要\nこんな人向け：機械学習初心者。モデル作成方法がわからな人。上位カーネルをコピペするだけになっちゃてる人。\n\npytorchで基本的なモデルを作成する方法を解説します。\n\n※私の備忘録も兼ねているので間違っているかもしれません。"},{"metadata":{},"cell_type":"markdown","source":"# 1. データ確認\nコンペの内容は別のノートに書いているのでぜひご覧ください。(宣伝)\n\n[https://www.kaggle.com/tomohiroh/ranzcr](http://)"},{"metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"cell_type":"code","source":"import numpy as np\nimport pandas as pd","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"df = pd.read_csv(\"../input/ranzcr-clip-catheter-line-classification/train.csv\")\n\nLABELS = [\n    'ETT - Abnormal', 'ETT - Borderline', 'ETT - Normal',\n    'NGT - Abnormal', 'NGT - Borderline', 'NGT - Incompletely Imaged', 'NGT - Normal', \n    'CVC - Abnormal', 'CVC - Borderline', 'CVC - Normal',\n    'Swan Ganz Catheter Present'\n]\n\ndf.head()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"\"StudyInstanceUID\"から画像を引っ張ってきてモデルに学習させます。"},{"metadata":{"trusted":true},"cell_type":"code","source":"DEBUG = True\nif DEBUG:\n    df = df.sample(frac = 0.01).reset_index(drop = True)\n    print(df.shape)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"モデル作成だけが目的なのでデータ数を減らします。実際に提出する時はFalseにしましょう。"},{"metadata":{"trusted":true},"cell_type":"code","source":"from sklearn.model_selection import train_test_split\ntrain, valid = train_test_split(df, test_size = 0.1)\nprint(train.shape, valid.shape)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"train_test_splitで学習データ(勉強)と評価データ(答え合わせ)に分けます。\n\nこの分割方法は適当なので、高スコアを狙いたい方はGroupKFoldにしたりしましょう。"},{"metadata":{},"cell_type":"markdown","source":"# 2. 画像読み込み"},{"metadata":{"trusted":true},"cell_type":"code","source":"train.head()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"path = train.iloc[0, 0]\npath","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"画像のパスを１つ取り出しました。これから画像を読み込みます。"},{"metadata":{"trusted":true},"cell_type":"code","source":"path = \"../input/ranzcr-clip-catheter-line-classification/train\" + \"/\" + path + \".jpg\"\npath","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"学習用画像は\"train\"のフォルダにあります。"},{"metadata":{"trusted":true},"cell_type":"code","source":"import cv2\nimage = cv2.imread(path)\nimage.shape","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"cv2でパスから画像を読み取ります。"},{"metadata":{"trusted":true},"cell_type":"code","source":"import matplotlib.pyplot as plt\nplt.imshow(image)\nplt.show()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"画像はレントゲン写真です。"},{"metadata":{"trusted":true},"cell_type":"code","source":"image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\nplt.imshow(image)\nplt.show()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"cv2で読み込むと色がBGR(青緑赤)の順に読み込まれるのでRGB(赤緑青)に変えます。\n\n変換する意味があるかはよくわかっていません。すみません。"},{"metadata":{"trusted":true},"cell_type":"code","source":"path = train.iloc[1, 0]\npath = \"../input/ranzcr-clip-catheter-line-classification/train\" + \"/\" + path + \".jpg\"\nimage2 = cv2.imread(path)\nimage2.shape","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"２枚目の画像を読み込んでみます。何枚か確認するとわかりますが画像によってサイズが異なります。\n\nサイズを統一しないと学習時にエラーが起きるので加工しましょう。"},{"metadata":{"trusted":true},"cell_type":"code","source":"from albumentations import Resize\ndummy = Resize(width = 300, height = 300)(image = image)\ndummy","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"albumentationsのResizeでサイズを変えます。\n\n変換後のデータは辞書型になっているので\"image\"で取り出さないといけません。"},{"metadata":{"trusted":true},"cell_type":"code","source":"image = Resize(width = 300, height = 300)(image = image)[\"image\"]\nimage.shape","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"300 x 300になっています。"},{"metadata":{"trusted":true},"cell_type":"code","source":"plt.imshow(image)\nplt.show()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"ちっちゃくなってスケールは維持されています。"},{"metadata":{"trusted":true},"cell_type":"code","source":"image2 = Resize(width = 300, height = 300)(image = image)[\"image\"]\nimage2.shape","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"同じことを２枚目の画像に実行するとサイズが統一されているとわかります。"},{"metadata":{"trusted":true},"cell_type":"code","source":"from albumentations.pytorch import ToTensorV2\nimage = ToTensorV2()(image = image)[\"image\"]\nimage.shape","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"pytorchに入れたいのでToTensorV2に入れます。さっきと同じく辞書型で返ってくるので\"image\"で取り出しましょう。\n\nサイズを見ると色を示す３が先になっています。\n\npytorchの仕様で学習時は色が最初に来ていないとダメです。ココがめんどくさいですが変換しておきましょう。"},{"metadata":{},"cell_type":"markdown","source":"# 3. Dataset\npytorchで学習する時はデータを取り出すシステムを作らないといけません。\n\nデータセットとデータローダーが必要です。先にデータセットを作ります。"},{"metadata":{"trusted":true},"cell_type":"code","source":"from torch.utils.data import Dataset\n\nclass TrainDataset(Dataset):\n    def __init__(self, df):\n        self.df = df\n        self.studyuid = df[\"StudyInstanceUID\"].values\n        self.labels = df[LABELS].values\n        \n    def __len__(self):\n        return self.df.shape[0]\n    \n    def __getitem__(self, idx):\n        path = self.studyuid[idx]\n        path = \"../input/ranzcr-clip-catheter-line-classification/train\" + \"/\" + path + \".jpg\"\n        image = cv2.imread(path)\n        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n        image = Resize(300, 300)(image = image)[\"image\"]\n        image = ToTensorV2()(image = image)[\"image\"]\n        labels = self.labels[idx]\n        return image, labels","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Datasetをインポートします。\n\nデータセットはクラスで作成します。\n\n__init__：初期化条件。引数はtrainとかのデータフレームです。selfは必須なのでとりあえず書きましょう。\n\n__len__：データサイズを定義するために必要。基本的に初期化時に渡したデータの行数です。\n\n__getitem__：データを取り出すときに必要。indexが引数になります。\n\nデータを取り出すときはインデックスが引数になるので、例えば０が入ったときはstudyuidの１個目のパスが対象になります。\n\n後はさっきまでと同じ処理を実行して画像データはimage、該当するラベル(正解)はlabelsとして出力されます。"},{"metadata":{"trusted":true},"cell_type":"code","source":"train_dataset = TrainDataset(train)\ntrain_dataset[0]","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"データセットを作るときはpandasのデータフレームを渡します。これが__init__の引数となります。\n\n実際に０を渡して最初のデータを見てみましょう。\n\n最初に画像データが、次にラベルデータが出力されています。"},{"metadata":{"trusted":true},"cell_type":"code","source":"image, label = train_dataset[0]\nplt.imshow(image.permute(1, 2, 0))\nplt.show()\nprint(label)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"このようにインデックスだけを使って画像とラベルと取り出すシステムができました。"},{"metadata":{},"cell_type":"markdown","source":"# 4. DataLoader\n作成したデータセットをデータローダーに入れます。"},{"metadata":{"trusted":true},"cell_type":"code","source":"from torch.utils.data import DataLoader","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"データローダーをインポートします。"},{"metadata":{"trusted":true},"cell_type":"code","source":"train_loader = DataLoader(train_dataset, batch_size = 8, shuffle = True)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"DataLoaderにデータセットを渡します。\n\nついでに以下の引数を指定します。\n\nbatch_size：一度に何枚取り出すか。多いほど学習が早いけどメモリを使う。小さいほどメモリを抑えるが学習に時間がかかり、１枚の特徴に大きな影響を受ける。\n\nshuffle：ランダムな順で取り出す。\n\n他にもdrop_lastなどがありますので本格的に学習させたい時は調べてください。"},{"metadata":{"trusted":true},"cell_type":"code","source":"for batch in train_loader:\n    print(batch[0].shape)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"for文で全データを取り出せます。\n\nbatch_sizeを８にしたので一度に８枚のデータが出力されています。"},{"metadata":{"trusted":true},"cell_type":"code","source":"valid_dataset = TrainDataset(valid)\nvalid_loader = DataLoader(valid_dataset, batch_size = 16, shuffle = False)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"評価データでも同じことをします。\n\nしかしshuffleは不要ですし学習ほど計算が重くないのでbatch_sizeも変えました。"},{"metadata":{},"cell_type":"markdown","source":"# 5. モデル作成\nモデルはEfficientNetを使います。"},{"metadata":{"trusted":true,"scrolled":false},"cell_type":"code","source":"import sys\nsys.path.append('../input/pytorch-image-models/pytorch-image-models-master')\nimport timm\nfrom pprint import pprint\npprint(timm.list_models(pretrained = True))","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"EfficientNetを利用する方法はいくつかあります。\n\n今回はtimmという画像分類モデルのセットを使いました。Datasetにアップされているので\"+Add data\"からinputに入れておきましょう。\n\npip installで入れる方法もありますが、本コンペでは提出時にネットを接続できないので使えなかったりします。"},{"metadata":{"trusted":true},"cell_type":"code","source":"import torch.nn as nn\n\nclass Effnet(nn.Module):\n    def __init__(self):\n        super().__init__()\n        self.effnet = timm.create_model(model_name = \"tf_efficientnet_b0\", pretrained = False)\n        n_features = self.effnet.classifier.in_features\n        self.effnet.classifier = nn.Linear(n_features, len(LABELS))\n    \n    def forward(self, x):\n        x = self.effnet(x)\n        return x","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"torch.nnのModuleを使ってクラスを作成します。\n\nsuperとinitは定型文なので気にせず書きましょう。\n\nEfficientNetをtimm.create_modelで作成します。指定するモデル名はさっき出力したリストの中から選びます。\n\nEfficientNetにはB0～B7まであり今回はB0です。\n\npretrained=Trueにすると学習済みモデルになりますが、ネットからパラメータをダウンロードする必要があるので、ネットOFFでは使えません。\n\n最後の出力形式を変更したいので.classifierの部分をLinear(全結合層)に置き換えます。\n\nこの時の入力サイズが必要なのでn_featuresとして取得しておきましょう。出力サイズは予測したいLABELSの数です。\n\nforwardは実際に学習(予測)するための関数です。入力をxとしてEfficientNetに通した結果を返します。"},{"metadata":{"trusted":true},"cell_type":"code","source":"model = Effnet()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"変数modelとしてEfficientNetを作りました。"},{"metadata":{"trusted":true},"cell_type":"code","source":"import torch\nDEVICE = \"cuda\" if torch.cuda.is_available() else \"cpu\"","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"計算にCPUを使うかGPUを使うか指定します。\n\n右端の\"setting\"からGPUをONにできます。（現時点で週43時間無料）\n\nONにしているとtorch.cuda.is_availableがTrueになるのでDEVICEはcuda(GPUの種類)になります。FalseならCPUのまま。"},{"metadata":{"trusted":true},"cell_type":"code","source":"model = model.to(DEVICE)\nprint(DEVICE)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"モデルに使用するCPUorGPUを設定します。忘れがち。\n\nEfficientNetは計算が重いのでできればGPUにしましょう。\n\n節約したい場合は、モデルの改善などデバッグ時はOFFにして本格的に学習させたい時にONにするといいです。"},{"metadata":{},"cell_type":"markdown","source":"# 6. 学習\n学習するステップは以下の通り。\n\n・損失関数を決める\n\n・最適化手法(optimizer)を決める\n\n・train_loaderで学習させる\n\n・valid_loaderで性能を確認する"},{"metadata":{"trusted":true},"cell_type":"code","source":"criterion = nn.BCEWithLogitsLoss()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"損失関数です。出力結果を０～１の範囲(シグモイド関数)にスケールしてから予測との誤差みたいなものを計算します。"},{"metadata":{"trusted":true},"cell_type":"code","source":"optimizer = torch.optim.Adam(model.parameters())","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"最適化手法です。色々ありますがメジャーなAdamにしました。\n\nさっき作ったモデル(model)のパラメータを渡しておきましょう。"},{"metadata":{"trusted":true},"cell_type":"code","source":"model.train()\nfor X, y in train_loader:\n    optimizer.zero_grad()\n    X = X.float().to(DEVICE)\n    y = y.float().to(DEVICE)\n    pred = model(X)\n    loss = criterion(pred, y)\n    loss.backward()\n    optimizer.step()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"最初に.trainで学習モードにします。何をしているのかはよくわかりません。\n\n予測する前に一度.zero_gradでoptimizerをリセットします。\n\nfor文でtrain_loaderからデータを引っ張ってきます。Xとyとして取り出しました。\n\npytorchの学習時はfloat型にする必要があるので.floatで変換しましょう。\n\nまたモデルに入れるデータでもto(DEVICE)でCPUorGPUの設定が必要です。これも忘れがち。\n\nmodelにXを入れるとEfficientNetに通って予測ラベルとして出力されるので、損失関数に渡しましょう。\n\nここでは(予測, 正解)の順にします。逆に渡すと変な結果になるので注意。\n\nすると誤差を計算してくれるので、次に.backward()でモデルに誤差を教えてあげます。\n\n最後にoptimizerの.stepで最適化手法に従ってモデルが改善されます。\n\nこの処理をバッチの数だけ繰り返して１回の学習が終わります。"},{"metadata":{"trusted":true},"cell_type":"code","source":"model.eval()\nvalid_loss = 0\nwith torch.no_grad():\n    for X, y in valid_loader:\n        X = X.float().to(DEVICE)\n        y = y.float().to(DEVICE)\n        pred = model(X)\n        loss = criterion(pred, y)\n        valid_loss += loss.item()\nvalid_loss /= len(valid_loader)\nprint(\"Loss:\", valid_loss)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"評価データで性能を確認します。\n\n最初に.evalで評価モードに変更します。これも何してるかわかりません。。。\n\n評価時はモデルのパラメータを変更したくないのでtorch.no_gradでロックしておきます。\n\n学習時と同じ要領でXyを取り出しfloat型にして予測させましょう。\n\n次に損失関数を計算しますが今回はモデルに誤差を伝える(backward)ことは不要です。\n\n全バッチでの誤差の平均をとりましょう。これが１回目の学習での性能です。"},{"metadata":{"trusted":true},"cell_type":"code","source":"for epoch in range(10):\n    model.train()\n    for X, y in train_loader:\n        optimizer.zero_grad()\n        X = X.float().to(DEVICE)\n        y = y.float().to(DEVICE)\n        pred = model(X)\n        loss = criterion(pred, y)\n        loss.backward()\n        optimizer.step()\n    model.eval()\n    valid_loss = 0\n    with torch.no_grad():\n        for X, y in valid_loader:\n            X = X.float().to(DEVICE)\n            y = y.float().to(DEVICE)\n            pred = model(X)\n            loss = criterion(pred, y)\n            valid_loss += loss.item()\n    valid_loss /= len(valid_loader)\n    print(\"Loss:\", valid_loss)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"同じことを複数回繰り返しました。\n\nこれでも十分なのですが毎回必ず誤差が小さくなるとは限りません。\n\nつまり最後のモデルが最高の性能を出すとは限らないということです。"},{"metadata":{},"cell_type":"markdown","source":"## ◆一連の流れまとめ◆"},{"metadata":{"trusted":true},"cell_type":"code","source":"model = Effnet().to(DEVICE)\n\ncriterion = nn.BCEWithLogitsLoss()\noptimizer = torch.optim.Adam(model.parameters())\n\nbest_loss = np.inf\nfor epoch in range(10):\n    model.train()\n    for X, y in train_loader:\n        optimizer.zero_grad()\n        X = X.float().to(DEVICE)\n        y = y.float().to(DEVICE)\n        pred = model(X)\n        loss = criterion(pred, y)\n        loss.backward()\n        optimizer.step()\n    model.eval()\n    valid_loss = 0\n    with torch.no_grad():\n        for X, y in valid_loader:\n            X = X.float().to(DEVICE)\n            y = y.float().to(DEVICE)\n            pred = model(X)\n            loss = criterion(pred, y)\n            valid_loss += loss.item()\n    valid_loss /= len(valid_loader)\n    print(f\"EPOCH:{epoch}, Loss:{valid_loss}\")\n    if valid_loss < best_loss:\n        best_loss = valid_loss\n        torch.save(model.state_dict(), \"effnet.pth\")\n        print(\"saved...\")","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"１度モデルを作り直したいのでこれまで実行したことをまとめました。\n\nbest_lossとして最小の誤差を定義します。スタートは無限大です。\n\nもし評価データでの誤差がこれまでの最小誤差よりも小さかったら更新しましょう。ついでにモデルを保存します。\n\nこうすると全ての学習が終わる頃には最も誤差の小さかったモデルが上書き保存されています。\n\nこれで学習は終わり。今回紹介したのは最低限モデルを構築するために必要なことだけです。"},{"metadata":{},"cell_type":"markdown","source":"# 7. 予測"},{"metadata":{"trusted":true},"cell_type":"code","source":"class TestDataset(Dataset):\n    def __init__(self, df):\n        self.df = df\n        self.studyuid = df[\"StudyInstanceUID\"].values\n        \n    def __len__(self):\n        return self.df.shape[0]\n\n    def __getitem__(self, idx):\n        path = self.studyuid[idx]\n        path = \"../input/ranzcr-clip-catheter-line-classification/test\" + \"/\" + path + \".jpg\"\n        image = cv2.imread(path)\n        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n        image = Resize(300, 300)(image = image)[\"image\"]\n        image = ToTensorV2()(image = image)[\"image\"]\n        return image","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"test用のデータセットを作ります。学習用とほとんど同じ。\n\nパスはtestのパスになっているので注意しましょう。\n\nまた正解ラベルを持たないので出力はimageのみです。"},{"metadata":{"trusted":true},"cell_type":"code","source":"test = pd.read_csv(\"../input/ranzcr-clip-catheter-line-classification/sample_submission.csv\")\ntest_dataset = TestDataset(test)\ntest_loader = DataLoader(test_dataset, batch_size = 32, shuffle = False)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"データセットとデータローダーを定義しました。\n\n主にvalid_loaderと同じことをしています。"},{"metadata":{"trusted":true},"cell_type":"code","source":"model = Effnet().to(DEVICE)\nmodel.load_state_dict(torch.load(\"./effnet.pth\"))","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"モデルを呼び出します。"},{"metadata":{"trusted":true},"cell_type":"code","source":"submit_preds = []\n\nmodel.eval()\nwith torch.no_grad():\n    for X in test_loader:\n        X = X.float().to(DEVICE)\n        submit_preds.append(model(X).sigmoid().to(\"cpu\"))\n    submit_preds = np.concatenate([p.numpy() for p in submit_preds], axis = 0)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"test_loaderからデータをロードしてモデルに渡します。\n\n出力される値を.sigmoidで０～１にスケールしましょう。\n\nデータをcpuに対応させないと後々エラーになるのでto(\"cpu\")を付けておきます。\n\n各バッチの予測結果をリスト(submit_preds)に入れておき、最後にnumpyの.concatenateで行方向(axis = 0)に結合します。\n\nこれで提出用の予測値ができました。"},{"metadata":{"trusted":true},"cell_type":"code","source":"submit = pd.DataFrame(submit_preds, columns = LABELS)\nsubmit.head()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"データフレームとして提出データを作成します。\n\nさっきの予測結果を入れて列名はLABELSを利用しました。"},{"metadata":{"trusted":true},"cell_type":"code","source":"submit[\"StudyInstanceUID\"] = test[\"StudyInstanceUID\"]\nsubmit = pd.concat([submit.iloc[:, -1], submit.iloc[:, :-1]], axis = 1)\nsubmit.to_csv(\"submission.csv\", index = False)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"IDが１列目に必要なので足しておきます。.to_csvでCSVとして保存します。\n\nindex = Falseにしないと余計な列ができるので注意。"},{"metadata":{"trusted":true},"cell_type":"code","source":"print(test.shape, submit.shape)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"一応サイズを確認。問題なさそうです。\n\n右上の\"Save Version\"から保存してプレビュー画面下にある\"output\"から\"submit\"を押せば提出できます。"},{"metadata":{},"cell_type":"markdown","source":"# 8. pytorchでやることまとめ\n①データセット(init, len, getitem)とデータローダーを作る。\n\n②モデルをクラスで定義する。forwardで学習する流れをくむ。\n\n③損失関数(criterion)と最適化手法(optimizer)を決める。\n\n④学習させる。(model.train, for ... loader, zero_grad, criterion(pred, 正解), backward, step)\n\n⑤評価する。(model.eval)\n\n⑥学習と評価を繰り返す(性能が良くなれば上書きする)\n\n⑦提出用のデータセットとデータローダを作り、予測する"}],"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"}},"nbformat":4,"nbformat_minor":4}