{"cells":[{"metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"d629ff2d2480ee46fbb7e2d37f6b5fab8052498a","_cell_guid":"79c7e3d0-c299-4dcb-8224-4455121ee9b0","trusted":true},"cell_type":"code","source":"import torch\nimport torch.nn as nn\nfrom torch.nn import functional as F\nfrom matplotlib import pyplot as plt\nfrom PIL import Image\nfrom torchvision import transforms as tsf\nimport csv\n%pylab inline","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"import math\n\nclass Flatten(nn.Module):\n    def forward(self, x):\n        return x.view(x.size(0), -1)\nclass ChannelGate(nn.Module):\n    def __init__(self, gate_channel, reduction_ratio=16, num_layers=1):\n        super(ChannelGate, self).__init__()\n        #self.gate_activation = gate_activation\n        self.gate_c = nn.Sequential()\n        self.gate_c.add_module( 'flatten', Flatten() )\n        gate_channels = [gate_channel]\n        gate_channels += [gate_channel // reduction_ratio] * num_layers\n        gate_channels += [gate_channel]\n        for i in range( len(gate_channels) - 2 ):\n            self.gate_c.add_module( 'gate_c_fc_%d'%i, nn.Linear(gate_channels[i], gate_channels[i+1]) )\n            self.gate_c.add_module( 'gate_c_bn_%d'%(i+1), nn.BatchNorm1d(gate_channels[i+1]) )\n            self.gate_c.add_module( 'gate_c_relu_%d'%(i+1), nn.ReLU() )\n        self.gate_c.add_module( 'gate_c_fc_final', nn.Linear(gate_channels[-2], gate_channels[-1]) )\n    def forward(self, in_tensor):\n        avg_pool = F.avg_pool2d( in_tensor, in_tensor.size(2), stride=in_tensor.size(2) )\n        return self.gate_c( avg_pool ).unsqueeze(2).unsqueeze(3).expand_as(in_tensor)\n\nclass SpatialGate(nn.Module):\n    def __init__(self, gate_channel, reduction_ratio=16, dilation_conv_num=2, dilation_val=4):\n        super(SpatialGate, self).__init__()\n        self.gate_s = nn.Sequential()\n        self.gate_s.add_module( 'gate_s_conv_reduce0', nn.Conv2d(gate_channel, gate_channel//reduction_ratio, kernel_size=1))\n        self.gate_s.add_module( 'gate_s_bn_reduce0',\tnn.BatchNorm2d(gate_channel//reduction_ratio) )\n        self.gate_s.add_module( 'gate_s_relu_reduce0',nn.ReLU() )\n        for i in range( dilation_conv_num ):\n            self.gate_s.add_module( 'gate_s_conv_di_%d'%i, nn.Conv2d(gate_channel//reduction_ratio, gate_channel//reduction_ratio, kernel_size=3, \\\n\t\t\t\t\t\tpadding=dilation_val, dilation=dilation_val) )\n            self.gate_s.add_module( 'gate_s_bn_di_%d'%i, nn.BatchNorm2d(gate_channel//reduction_ratio) )\n            self.gate_s.add_module( 'gate_s_relu_di_%d'%i, nn.ReLU() )\n        self.gate_s.add_module( 'gate_s_conv_final', nn.Conv2d(gate_channel//reduction_ratio, 1, kernel_size=1) )\n    def forward(self, in_tensor):\n        return self.gate_s( in_tensor ).expand_as(in_tensor)\nclass BAM(nn.Module):\n    def __init__(self, gate_channel):\n        super(BAM, self).__init__()\n        self.channel_att = ChannelGate(gate_channel)\n        self.spatial_att = SpatialGate(gate_channel)\n    def forward(self,in_tensor):\n        att = 1 + F.sigmoid( self.channel_att(in_tensor) * self.spatial_att(in_tensor) )\n        return att * in_tensor\n","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"import glob\nimport os\nimport numpy as np\n#获取到数据集中的图像路径\ndef csv_reader(path):\n    with open(path, \"r\") as f:\n        breast = list(csv.reader(f))\n    return breast\ndef get_datas(image_dir,suffix=\".png\"):\n    '''\n    image_dir:包含图像的文件夹\n    suffix:图像的后缀\n    '''\n    image_paths = []\n    labels = []\n    train=csv_reader(\"../input/aptos2019-blindness-detection/train.csv\")\n    for i in train[1:]:\n        #y_this=[0.0,0.0,0.0,0.0,0.0]\n        #y_this[int(i[1])]=1.0\n        image_paths.append(image_dir+'/'+i[0]+'.png')\n        labels.append(int(i[1]))\n    return image_paths,labels\n#随机展示25张图像\ndef show_batch(img_paths):\n    '''\n    img_paths:list, 所有图像的路径\n    '''\n    randomed = []\n    #获取到一部分图像路径的编号\n    if len(img_paths) <= 25:\n        randomed = list(range(0, len(img_paths)))\n    else:\n        for i in range(0, len(img_paths)):\n            random = np.random.randint(0, len(img_paths))\n            if random not in randomed:\n                randomed.append(random)\n            if len(randomed) == 25:\n                break\n    #展示图像\n    plt.figure(dpi=224)\n    for i in range(len(randomed)):\n        img = Image.open(img_paths[randomed[i]])\n        plt.subplot(5, 5, i+1) #构建子图区域\n        plt.imshow(img)\n        plt.xticks([])\n        plt.yticks([])\n    plt.show()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"img_dir = os.path.abspath('../input/aptos2019-blindness-detection/train_images')\nshuffix = \".png\"\nimg_paths, labels = get_datas(img_dir, suffix=shuffix) #获取到数据集中图像路径和图像的标签\n#show_batch(img_paths) #展示25张图像","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"import torch.nn as nn\nimport math\nimport torch.utils.model_zoo as model_zoo\n\n\n__all__ = ['ResNet', 'resnet18', 'resnet34', 'resnet50', 'resnet101',\n           'resnet152']\n\n\nmodel_urls = {\n    'resnet18': 'https://download.pytorch.org/models/resnet18-5c106cde.pth',\n    'resnet34': 'https://download.pytorch.org/models/resnet34-333f7ec4.pth',\n    'resnet50': 'https://download.pytorch.org/models/resnet50-19c8e357.pth',\n    'resnet101': 'https://download.pytorch.org/models/resnet101-5d3b4d8f.pth',\n    'resnet152': 'https://download.pytorch.org/models/resnet152-b121ed2d.pth',\n}\n\n\ndef conv3x3(in_planes, out_planes, stride=1):\n    \"3x3 convolution with padding\"\n    return nn.Conv2d(in_planes, out_planes, kernel_size=3, stride=stride,\n                     padding=1, bias=False)\n\n\nclass BasicBlock(nn.Module):\n    expansion = 1\n\n    def __init__(self, inplanes, planes, stride=1, downsample=None, use_cbam=False):\n        super(BasicBlock, self).__init__()\n        self.conv1 = conv3x3(inplanes, planes, stride)\n        self.bn1 = nn.BatchNorm2d(planes)\n        self.relu = nn.ReLU(inplace=True)\n        self.conv2 = conv3x3(planes, planes)\n        self.bn2 = nn.BatchNorm2d(planes)\n        self.downsample = downsample\n        self.stride = stride\n        if use_cbam:\n            self.cbam = CBAM( planes, 16 )\n        else:\n            self.cbam = None\n    def forward(self, x):\n        residual = x\n\n        out = self.conv1(x)\n        out = self.bn1(out)\n        out = self.relu(out)\n\n        out = self.conv2(out)\n        out = self.bn2(out)\n\n        if self.downsample is not None:\n            residual = self.downsample(x)\n        if not self.cbam is None:\n            out = self.cbam(out)\n        out += residual\n        out = self.relu(out)\n\n        return out\n\n\nclass Bottleneck(nn.Module):\n    expansion = 4\n\n    def __init__(self, inplanes, planes, stride=1, downsample=None, use_cbam=False):\n        super(Bottleneck, self).__init__()\n        self.conv1 = nn.Conv2d(inplanes, planes, kernel_size=1, bias=False)\n        self.bn1 = nn.BatchNorm2d(planes)\n        self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, stride=stride,\n                               padding=1, bias=False)\n        self.bn2 = nn.BatchNorm2d(planes)\n        self.conv3 = nn.Conv2d(planes, planes * 4, kernel_size=1, bias=False)\n        self.bn3 = nn.BatchNorm2d(planes * 4)\n        self.relu = nn.ReLU(inplace=True)\n        self.downsample = downsample\n        self.stride = stride\n        if use_cbam:\n            self.cbam = CBAM( planes * 4, 16 )\n        else:\n            self.cbam = None\n        \n    def forward(self, x):\n        residual = x\n\n        out = self.conv1(x)\n        out = self.bn1(out)\n        out = self.relu(out)\n\n        out = self.conv2(out)\n        out = self.bn2(out)\n        out = self.relu(out)\n\n        out = self.conv3(out)\n        out = self.bn3(out)\n\n        if self.downsample is not None:\n            residual = self.downsample(x)\n        if not self.cbam is None:\n            out = self.cbam(out)\n        out += residual\n        out = self.relu(out)\n\n        return out\n\n\nclass ResNet(nn.Module):\n\n    def __init__(self, block, layers, num_classes=1000, att_type=None):\n        self.inplanes = 64\n        super(ResNet, self).__init__()\n        self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3,\n                               bias=False)\n        self.bn1 = nn.BatchNorm2d(64)\n        self.relu = nn.ReLU(inplace=True)\n        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)\n        if att_type=='BAM':\n            self.bam1 = BAM(64*block.expansion)\n            self.bam2 = BAM(128*block.expansion)\n            self.bam3 = BAM(256*block.expansion)\n        else:\n            self.bam1, self.bam2, self.bam3 = None, None, None\n        self.layer1 = self._make_layer(block, 64, layers[0], att_type=att_type)\n        self.layer2 = self._make_layer(block, 128, layers[1], stride=2, att_type=att_type)\n        self.layer3 = self._make_layer(block, 256, layers[2], stride=2, att_type=att_type)\n        self.layer4 = self._make_layer(block, 512, layers[3], stride=2, att_type=att_type)\n        self.avgpool = nn.AdaptiveAvgPool2d(1)\n        self.fc = nn.Linear(512 * block.expansion, 1000)\n        self.fc1 = nn.Linear(1000,5)\n        for m in self.modules():\n            if isinstance(m, nn.Conv2d):\n                n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels\n                m.weight.data.normal_(0, math.sqrt(2. / n))\n            elif isinstance(m, nn.BatchNorm2d):\n                m.weight.data.fill_(1)\n                m.bias.data.zero_()\n\n    def _make_layer(self, block, planes, blocks, stride=1,att_type=None):\n        downsample = None\n        if stride != 1 or self.inplanes != planes * block.expansion:\n            downsample = nn.Sequential(\n                nn.Conv2d(self.inplanes, planes * block.expansion,\n                          kernel_size=1, stride=stride, bias=False),\n                nn.BatchNorm2d(planes * block.expansion),\n            )\n\n        layers = []\n        layers.append(block(self.inplanes, planes, stride, downsample, use_cbam=att_type=='CBAM'))\n        self.inplanes = planes * block.expansion\n        for i in range(1, blocks):\n            layers.append(block(self.inplanes, planes, use_cbam=att_type=='CBAM'))\n\n        return nn.Sequential(*layers)\n\n    def forward(self, x):\n        x = self.conv1(x)\n        x = self.bn1(x)\n        x = self.relu(x)\n        x = self.maxpool(x)\n\n        x = self.layer1(x)\n        if not self.bam1 is None:\n            x = self.bam1(x)\n        x = self.layer2(x)\n        if not self.bam2 is None:\n            x = self.bam2(x)\n        x = self.layer3(x)\n        if not self.bam3 is None:\n            x = self.bam3(x)\n        feat = self.layer4(x)\n\n        x = self.avgpool(feat)\n        x = x.view(x.size(0), -1)\n        x = self.fc(x)\n        x = self.fc1(x)\n\n        return feat, x\n\n\ndef resnet18(pretrained=False, att_type=None):\n    \"\"\"Constructs a ResNet-18 model.\n    Args:\n        pretrained (bool): If True, returns a model pre-trained on ImageNet\n    \"\"\"\n    model = ResNet(BasicBlock, [2, 2, 2, 2], att_type=att_type)\n    if pretrained:\n        save_model = model_zoo.load_url(model_urls['resnet18'])\n        model_dict =  model.state_dict()\n        state_dict = {k:v for k,v in save_model.items() if k in model_dict.keys()}\n        model_dict.update(state_dict)\n        model.load_state_dict(model_dict)\n    return model\n\n\ndef resnet34(pretrained=False, att_type=None):\n    \"\"\"Constructs a ResNet-34 model.\n    Args:\n        pretrained (bool): If True, returns a model pre-trained on ImageNet\n    \"\"\"\n    model = ResNet(BasicBlock, [3, 4, 6, 3], att_type=att_type)\n    if pretrained:\n        model.load_state_dict(model_zoo.load_url(model_urls['resnet34']))\n    return model\n\n\ndef resnet50(pretrained=False, **kwargs):\n    \"\"\"Constructs a ResNet-50 model.\n    Args:\n        pretrained (bool): If True, returns a model pre-trained on ImageNet\n    \"\"\"\n    model = ResNet(Bottleneck, [3, 4, 6, 3], **kwargs)\n    if pretrained:\n        model.load_state_dict(model_zoo.load_url(model_urls['resnet50']))\n    return model\n\n\ndef resnet101(pretrained=False, att_type=None):\n    \"\"\"Constructs a ResNet-101 model.\n    Args:\n        pretrained (bool): If True, returns a model pre-trained on ImageNet\n    \"\"\"\n    model = ResNet(Bottleneck, [3, 4, 23, 3], att_type=att_type)\n    if pretrained:\n        save_model = model_zoo.load_url(model_urls['resnet101'])\n        model_dict =  model.state_dict()\n        state_dict = {k:v for k,v in save_model.items() if k in model_dict.keys()}\n        model_dict.update(state_dict)\n        model.load_state_dict(model_dict)\n    return model\n\n\ndef resnet152(pretrained=False, att_type=None):\n    \"\"\"Constructs a ResNet-152 model.\n    Args:\n        pretrained (bool): If True, returns a model pre-trained on ImageNet\n    \"\"\"\n    model = ResNet(Bottleneck, [3, 8, 36, 3], att_type=att_type)\n    if pretrained:\n        save_model = model_zoo.load_url(model_urls['resnet152'])\n        model_dict =  model.state_dict()\n        state_dict = {k:v for k,v in save_model.items() if k in model_dict.keys()}\n        model_dict.update(state_dict)\n        model.load_state_dict(model_dict)\n    return model","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"import cv2\ndef crop_image1(img,tol=7):\n    # img is image data\n    # tol  is tolerance\n        \n    mask = img>tol\n    return img[np.ix_(mask.any(1),mask.any(0))]\n\ndef crop_image_from_gray(img,tol=7):\n    if img.ndim ==2:\n        mask = img>tol\n        return img[np.ix_(mask.any(1),mask.any(0))]\n    elif img.ndim==3:\n        gray_img = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)\n        mask = gray_img>tol\n        \n        check_shape = img[:,:,0][np.ix_(mask.any(1),mask.any(0))].shape[0]\n        if (check_shape == 0): # image is too dark so that we crop out everything,\n            return img # return original image\n        else:\n            img1=img[:,:,0][np.ix_(mask.any(1),mask.any(0))]\n            img2=img[:,:,1][np.ix_(mask.any(1),mask.any(0))]\n            img3=img[:,:,2][np.ix_(mask.any(1),mask.any(0))]\n    #         print(img1.shape,img2.shape,img3.shape)\n            img = np.stack([img1,img2,img3],axis=-1)\n    #         print(img.shape)\n        return img\ndef circle_crop(img, sigmaX=10):   \n    \"\"\"\n    Create circular crop around image centre    \n    \"\"\"    \n    \n    img = cv2.imread(img)\n    img = cv2.resize(img,(700,700))\n    img = crop_image_from_gray(img)    \n    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n    \n    height, width, depth = img.shape    \n    \n    x = int(width/2)\n    y = int(height/2)\n    r = np.amin((x,y))\n    \n    circle_img = np.zeros((height, width), np.uint8)\n    cv2.circle(circle_img, (x,y), int(r), 1, thickness=-1)\n    img = cv2.bitwise_and(img, img, mask=circle_img)\n    img = crop_image_from_gray(img)\n    img=cv2.addWeighted ( img,4, cv2.GaussianBlur( img , (0,0) , sigmaX) ,-4 ,128)\n    return img \n","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"from torch.utils.data import Dataset\n\nclass EyeDataset(Dataset):\n    def __init__(self, img_paths, labels, gray=False, transform=None):\n        super(EyeDataset, self).__init__()\n        self.img_paths = img_paths\n        self.labels = labels\n        self.gray = gray\n        self.transform = transform\n        self.length = len(img_paths)\n    \n    def __len__(self):\n        return self.length\n    \n    def __getitem__(self, index):\n        img_path = self.img_paths[index]\n        label = self.labels[index]\n        img = circle_crop(img_path,sigmaX=30)\n        img = Image.fromarray(cv2.cvtColor(img,cv2.COLOR_BGR2RGB))\n        if self.transform is not None:\n            img = self.transform(img)\n        \n        \n        return img, label","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"#划分验证集和训练集\nfrom torch.utils.data.sampler import WeightedRandomSampler\nfrom sklearn.model_selection import train_test_split\ntrain_paths, val_paths, train_labels, val_labels = train_test_split(img_paths, labels, test_size=0.3, random_state=0, stratify=labels)\n#定义训练数据集对象和验证数据集对象\nimg_size = 512\ntrain_transform = tsf.Compose([\n    tsf.Resize((img_size, img_size)),\n    tsf.RandomHorizontalFlip(),\n    tsf.RandomVerticalFlip(),\n    tsf.ToTensor(),\n    tsf.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5])\n])\nval_transform = tsf.Compose([\n    tsf.Resize((img_size, img_size)),\n    tsf.ToTensor(),\n    tsf.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5])\n])\ntrain_dataset =EyeDataset(train_paths, torch.tensor(train_labels), transform=train_transform)\nval_dataset = EyeDataset(val_paths, torch.tensor(val_labels), transform=val_transform)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"#定义训练集和测试集的加载器\nfrom torch.utils.data import DataLoader\nwights=[]\ntrain_time=[1,5,2,9,6]\nfor i in train_labels:\n    wights.append(train_time[int(i)])\nsampler = WeightedRandomSampler(wights, len(wights),replacement=True)\ntrain_loader = DataLoader(train_dataset, shuffle=False, batch_size=8, num_workers=4, sampler=sampler) #备注：在windows系统中多线程可能存在问题，所以设置num_workers为0\nval_loader = DataLoader(val_dataset, shuffle=False, batch_size=1, num_workers=4)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def train_for(x,label,opt,model,losses,total,number,count):\n    for _ in range(number):\n        x = x.to(device, dtype=torch.float32)\n        label = label.to(device, dtype=torch.long)\n        batch = x.size(0)\n        total += batch\n        opt.zero_grad() #清除累积的梯度\n        _,pred = model(x)\n        loss = criterion(pred, label)\n        loss.backward()\n        pred = torch.max(pred, dim=1)[1]\n        for i in range(len(pred)):\n            if int(pred[i])==int(label[i]):\n                count+=1\n        if total%128==0:\n            print(loss)\n            print(total)\n            print(\"acc: \"+str(count/128))\n            count=0\n        opt.step() #更新权重\n        losses += loss.item() * batch\n    return opt,model,losses,total,count","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"import random\nmodel = resnet101(pretrained=True,att_type='BAM')\n#model=DenseNet121()\nopt = torch.optim.SGD(model.parameters(),lr=0.004, momentum=0.9, nesterov=True)\nscheduler = torch.optim.lr_scheduler.StepLR(opt, step_size = 5, gamma = 0.1, last_epoch=-1)\ncriterion = nn.CrossEntropyLoss()\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nmodel.to(device)\nfor __ in range(1):\n    for epoch in range(30):\n        losses = 0.0\n        total = 0\n        count=0\n        corrects = 0\n        model.train()\n        for x, label in train_loader:\n            opt,model,losses,total,count=train_for(x,label,opt,model,losses,total,1,count)\n        print(\"avg loss: \"+str(losses))\n        scheduler.step()\n        #在验证集上验证模型的效果","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"model.eval()\nwith torch.no_grad():\n    losses = 0.0\n    total = 0\n    corrects = 0\n    tbie=[0,0,0,0,0]\n    bie=[0,0,0,0,0]\n    pre=[0,0,0,0,0]\n    count=0\n    for x, label in val_loader:\n        x = x.to(device, dtype=torch.float32)          \n        label = label.to(device, dtype=torch.long)\n        batch = x.size(0)\n        total += batch\n        _,pred = model(x)\n        loss = criterion(pred, label)\n        losses += loss.item() * batch\n        pred = torch.max(pred, dim=1)[1]\n        for i in range(len(pred)):\n            if int(pred[i])==int(label[i]):\n                count+=1\n        if total%128==0:\n            print(loss)\n            print(total)\n            print(\"acc: \"+str(count))\n            count=0\n        for i in range(len(pred)):\n            cc=pred[i]\n            cd=label[i]\n            tbie[int(cd)]+=1\n            if int(cc)==int(cd):\n                corrects=corrects+1\n                bie[int(cc)]+=1\n    print(\"correct: \"+str(corrects/total))\n    for z in range(5):\n        print(\"class \"+str(z)+\" correct \"+str(bie[z]/tbie[z]))\n    print(bie)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"torch.save(model.state_dict(),'./model.pth')","execution_count":null,"outputs":[]}],"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}