You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

tutorial_for_developer.ipynb 23 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. {
  2. "cells": [
  3. {
  4. "cell_type": "markdown",
  5. "metadata": {},
  6. "source": [
  7. "### 一共会涉及到如下的几个类\n",
  8. "\n",
  9. "#### DataSet\n",
  10. "#### Sampler\n",
  11. "#### Batch\n",
  12. "#### Model\n",
  13. "#### Loss\n",
  14. "#### Metric\n",
  15. "#### Trainer\n",
  16. "#### Tester"
  17. ]
  18. },
  19. {
  20. "cell_type": "markdown",
  21. "metadata": {},
  22. "source": [
  23. "### 下面具体讲一下它们的作用"
  24. ]
  25. },
  26. {
  27. "cell_type": "markdown",
  28. "metadata": {},
  29. "source": [
  30. "#### DataSet: 用于承载数据。\n",
  31. "(1) DataSet里面每个元素只能是以下的三类np.float64, np.int64, np.str。如果传入的数据是int则被转换为np.int64, float被转为np.float64。 \n",
  32. "(2) DataSet可以将field设置为input,target。其中被设置为input的field会被传递给Model.forward, 这个过程中我们是通过键匹配完成传递的。举例来说,假设DataSet中有'x1', 'x2', 'x3'被设置为了input,而 \n",
  33. "   (2.1)函数是Model.forward(self, x1, x3), 那么DataSet中'x1', 'x3'会被传递给forward函数。多余的'x2'会被忽略 \n",
  34. "   (2.2)函数是Model.forward(self, x1, x4), 这里多需要了一个'x4', 但是DataSet的input field中没有这个field,会报错。 \n",
  35. "   (2.3)函数是Model.forward(self, x1, **kwargs), 会把'x1', 'x2', 'x3'都传入。但如果是Model.forward(self, x4, **kwargs)就会发生报错,因为没有'x4'。 \n",
  36. "(3) 对于设置为target的field的名称,我们建议取名为'target'(如果只有一个需要predict的值),但是不强制。后面会讲为什么target可以不强制。 \n",
  37. "DataSet应该是不需要单独再开发的,如果有不能满足的场景,请在开发群提出或者github提交issue。"
  38. ]
  39. },
  40. {
  41. "cell_type": "markdown",
  42. "metadata": {},
  43. "source": [
  44. "#### Sampler: 给定一个DataSet,返回一个序号的list,Batch按照这个list输出数据。\n",
  45. "Sampler需要继承fastNLP.core.sampler.BaseSampler"
  46. ]
  47. },
  48. {
  49. "cell_type": "raw",
  50. "metadata": {},
  51. "source": [
  52. "class BaseSampler(object):\n",
  53. "\"\"\"The base class of all samplers.\n",
  54. "\n",
  55. " Sub-classes must implement the __call__ method.\n",
  56. " __call__ takes a DataSet object and returns a list of int - the sampling indices.\n",
  57. "\"\"\"\n",
  58. "def __call__(self, *args, **kwargs):\n",
  59. " raise NotImplementedError\n",
  60. " \n",
  61. "# 子类需要复写__call__方法。这个函数只能有一个必选参数, 且必须是DataSet类别, 否则Trainer没法调\n",
  62. "class SonSampler(BaseSample):\n",
  63. " def __init__(self, xxx):\n",
  64. " # 可以实现init也不可以不实现。\n",
  65. " def __call__(self, data_set):\n",
  66. " pass"
  67. ]
  68. },
  69. {
  70. "cell_type": "markdown",
  71. "metadata": {},
  72. "source": [
  73. "#### Batch: 将DataSet中设置为input和target的field取出来构成batch_x, batch_y\n",
  74. "并且根据情况(主要根据数据类型能不能转为Tensor)将数据转换为pytorch的Tensor。batch中sample的取出顺序是由Sampler决定的。 \n",
  75. "Sampler是传入一个DataSet,返回一个与DataSet等长的序号list,Batch一次会取出batch_size个sample(最后一个batch可能数量不足batch_size个)。 \n",
  76. "举例: \n",
  77. "(1) SequentialSampler是顺序采样\n",
  78. " 假设传入的DataSet长度是100, SequentialSampler返回的序号list就是[0, 1, ...,98, 99]. batch_size如果被设置为4,那么第一个batch所获取的instance就是[0, 1, 2, 3]这四个instance. 第二个batch所获取instace就是[4, 5, 6, 7], ...直到采完所有的sample。 \n",
  79. "(2) RandomSampler是随机采样 \n",
  80. " 假设传入的DataSet长度是100, RandomSampler返回的序号list可能是[0, 99, 20, 5, 3, 1, ...]. 依次按照batch_size的大小取出sample。 \n",
  81. "Batch应该不需要继承与开发,如果你有特殊需求请在开发群里提出。"
  82. ]
  83. },
  84. {
  85. "cell_type": "markdown",
  86. "metadata": {},
  87. "source": [
  88. "#### Model:用户自定的Model\n",
  89. "必须是nn.Module的子类, \n",
  90. "(1) 必须实现forward方法,并且forward方法不能出现*arg这种参数. 例如 \n",
  91. "   def forward(self, word_seq, *args): #这是不允许的. \n",
  92. "      xxx \n",
  93. "返回值必须是dict的 \n",
  94. "   def forward(self, word_seq, seq_lens): \n",
  95. "      xxxx \n",
  96. "   return {'pred': xxx} #return的值必须是dict的。里面的预测的key推荐使用pred,但是不做强制限制。输出元素数目不限。 \n",
  97. "(2) 如果实现了predict方法,在做evaluation的时候将调用predict方法而不是forward。如果没有predict方法,则在evaluation时调用forward方法。predict方法也不能使用*args这种参数形式,同时结果也必须返回一个dict,同样推荐key为'pred'。"
  98. ]
  99. },
  100. {
  101. "cell_type": "markdown",
  102. "metadata": {},
  103. "source": [
  104. "#### Loss: 根据model.forward()返回的prediction(是一个dict)和batch_y计算相应的loss。 \n",
  105. "(1) 先介绍\"键映射\"。 如在DataSet, Model一节所看见的那样,fastNLP并不限制Model.forward()的返回值,也不限制DataSet中target field的key。计算的loss的时候,怎么才能知道从哪里取值呢? \n",
  106. "这里以CrossEntropyLoss为例,一般情况下, 计算CrossEntropy需要prediction和target两个值。而在CrossEntropyLoss初始化时可以传入两个参数(pred=None, target=None), 这两个参数接受的类型是str,假设(pred='output', target='label'),那么CrossEntropyLoss会使用'output'这个key在forward的output与batch_y中寻找值;'label'也是在forward的output与batch_y中寻找值。注意这里pred或target的来源并不一定非要来自于model.forward与batch_y,也可以只来自于forward的结果。 \n",
  107. "(2)如何创建一个自己的loss \n",
  108. "   (2.1)使用fastNLP.LossInForward, 在model.forward()的结果中包含一个为loss的key。 \n",
  109. "   (2.2) trainer中使用loss(假设loss=CrossEntropyLoss())的时候其实是 \n",
  110. "    los = loss(prediction, batch_y)\n",
  111. " 即直接调用的是loss.\\__call__()方法,但是CrossEntropyLoss里面并没有自己实现\\__call__方法,这是因为\\__call__在LossBase中实现了。所有的loss必须继承fastNLP.core.loss.LossBase, 下面先说一下LossBase的几个方法,见下一个cell。 \n",
  112. "(3) 尽量不要复写\\__call__(), _init_param_map()方法。"
  113. ]
  114. },
  115. {
  116. "cell_type": "raw",
  117. "metadata": {},
  118. "source": [
  119. "class LossBase():\n",
  120. " def __init__(self):\n",
  121. " self.param_map = {} # 一般情况下也不需要自己创建。调用_init_param_map()更好\n",
  122. " self._checked = False # 这个参数可以忽略\n",
  123. "\n",
  124. " def _init_param_map(self, key_map=None, **kwargs):\n",
  125. " # 这个函数是用于注册Loss的“键映射”,有两种传值方法,\n",
  126. " # 第一种是通过key_map传入dict,取值是用value到forward和batch_y取\n",
  127. " # key_map = {'pred': 'output', 'target': 'label'} \n",
  128. " # 第二种是自己写\n",
  129. " # _init_param_map(pred='output', target='label')\n",
  130. " # 为什么会提供这么一个方法?通过调用这个方法会自动注册param_map,并会做一些检查,防止出现传入的key其实并不是get_loss\n",
  131. " # 的一个参数。注意传入这个方法的参数必须都是需要做键映射的内容,其它loss参数不要传入。如果传入(pred=None, target=None)\n",
  132. " # 则__call__()会到pred_dict与target_dict去寻找key为'pred'和'target'的值。\n",
  133. " # 但这个参数不是必须要调用的。\n",
  134. "\n",
  135. " def __call__(self, pred_dict, target_dict, check=False): # check=False忽略这个参数,之后应该会被删除的\n",
  136. " # 这个函数主要会做一些check的工作,比如pred_dict与target_dict中是否包含了计算loss所必须的key等。检查通过,则调用get_loss\n",
  137. " # 方法。\n",
  138. " fast_param = self._fast_param_map(predict_dict, target_dict):\n",
  139. " if fast_param:\n",
  140. " return self.get_loss(**fast_param)\n",
  141. " # 如果没有fast_param则通过匹配参数然后调用get_loss完成\n",
  142. " xxxx\n",
  143. " return loss # 返回为Tensor的loss\n",
  144. " def _fast_param_map(self, pred_dict, target_dict):\n",
  145. " # 这是一种快速计算loss的机制,因为在很多情况下其实都不需要通过\"键映射\",比如计算loss时,pred_dict只有一个元素,\n",
  146. " # target_dict也只有一个元素,那么无歧义地就可以把预测值与实际值用于计算loss, 基类判断了这种情况(可能还有其它无歧义的情况)。\n",
  147. " # 即_fast_param_map成功的话,就不需要使用键映射,这样即使在没有传递或者传递错误\"键映射\"的情况也可以直接计算loss。\n",
  148. " # 返回值是一个dict, 如果匹配成功,应该返回类似{'pred':value, 'target': value}的结果;如果dict为空则说明匹配失败,\n",
  149. " # __call__方法会继续执行。\n",
  150. "\n",
  151. " def get_loss(self, *args, **kwargs):\n",
  152. " # 这个是一定需要实现的,计算loss的地方。\n",
  153. " # (1) get_loss中一定不能包含*arg这种参数形式。\n",
  154. " # (2) 如果包含**kwargs这种参数,这会将pred_dict与target_dict中所有参数传入。但是建议不要用这个参数\n",
  155. " raise NotImplementedError\n",
  156. "\n",
  157. "# 下面使用L1Loss举例\n",
  158. "class L1Loss(LossBase): # 继承LossBase\n",
  159. " # 初始化需要映射的值,这里需要映射的值'pred', 'target'必须与get_loss需要参数名是对应的\n",
  160. " def __init__(self, pred=None, target=None): \n",
  161. " super(L1Loss, self).__init__()\n",
  162. " # 这里传入_init_param_map以使得pred和target被正确注册,但这一步不是必须的, 建议调用。传入_init_param_map的是用于\n",
  163. " # “键映射\"的键值对。假设初始化__init__(pred=None, target=None, threshold=0.1)中threshold是用于控制loss计算的,则\n",
  164. " # 不要将threshold传入_init_param_map.\n",
  165. " self._init_param_map(pred=pred, target=target)\n",
  166. "\n",
  167. " def get_loss(self, pred, target):\n",
  168. " # 这里'pred', 'target'必须和初始化的映射是一致的。\n",
  169. " return F.l1_loss(input=pred, target=target) #直接返回一个loss即可"
  170. ]
  171. },
  172. {
  173. "cell_type": "markdown",
  174. "metadata": {},
  175. "source": [
  176. "### Metric: 根据Model.forward()或者Model.predict()的结果计算metric \n",
  177. "metric的设计和loss的设计类似。都是传入pred_dict与target_dict进行计算。但是metric的pred_dict来源可能是Model.forward的返回值, 也可能是Model.predict(如果Model具有predict方法则会调用predict方法)的返回值,下面统一用pred_dict代替。 \n",
  178. "(1) 这里的\"键映射\"与loss的\"键映射\"是类似的。举例来说,若Metric(pred='output', target='label'),则使用'output'到pred_dict和target_dict中寻找pred, 用'label'寻找target。 \n",
  179. "(2) 如何创建一个自己的Metric方法 \n",
  180. "  Metric与loss的计算不同在于,Metric的计算有两个步骤。 \n",
  181. "&emsp;&emsp;(2.1) <b>每个batch的输出</b>都会调用Metric的\\__call__(pred_dict, target_dict)方法,而\\__call__方法会调用evaluate()(需要实现)方法。 \n",
  182. "&emsp;&emsp;(2.2) 在所有batch传入之后,调用Metric的get_metric()方法得到最终的metric值。 \n",
  183. "&emsp;&emsp;所以Metric在调用evaluate方法时,根据拿到的数据: pred_dict与batch_y, 改变自己的状态(比如累加正确的次数,总的sample数等)。在调用get_metric()的时候给出一个最终计算结果。 \n",
  184. "所有的Metric必须继承自fastNLP.core.metrics.MetricBase. 例子见下一个cell \n",
  185. "(3) 尽量不要复写\\__call__(), _init_param_map()方法。\n"
  186. ]
  187. },
  188. {
  189. "cell_type": "raw",
  190. "metadata": {},
  191. "source": [
  192. "MetricBase: \n",
  193. " def __init__(self):\n",
  194. " self.param_map = {} # 一般情况下也不需要自己创建。调用_init_param_map()更好\n",
  195. " self._checked = False # 这个参数可以忽略\n",
  196. "\n",
  197. " def _init_param_map(self, key_map=None, **kwargs):\n",
  198. " # 这个函数是用于注册Metric的“键映射”,有两种传值方法,\n",
  199. " # 第一种是通过key_map传入dict,取值是用value到forward和batch_y取\n",
  200. " # key_map = {'pred': 'output', 'target': 'label'} \n",
  201. " # 第二种是自己写(建议使用改种方式)\n",
  202. " # _init_param_map(pred='output', target='label')\n",
  203. " # 为什么会提供这么一个方法?通过调用这个方法会自动注册param_map,并会做一些检查,防止出现传入的key其实并不是evaluate()\n",
  204. " # 的一个参数。注意传入这个方法的参数必须都是需要做键映射的内容,其它evaluate参数不要传入。如果传入(pred=None, target=None)\n",
  205. " # 则__call__()会到pred_dict与target_dict去寻找key为'pred'和'target'的值。\n",
  206. " # 但这个参数不是必须要调用的。\n",
  207. "\n",
  208. " def __call__(self, pred_dict, target_dict, check=False): # check=False忽略这个参数,之后应该会被删除的\n",
  209. " # 这个函数主要会做一些check的工作,比如pred_dict与target_dict中是否包含了计算evaluate所必须的key等。检查通过,则调用\n",
  210. " # evaluate方法。\n",
  211. " fast_param = self._fast_param_map(predict_dict, target_dict):\n",
  212. " if fast_param:\n",
  213. " return self.evaluate(**fast_param)\n",
  214. " # 如果没有fast_param则通过匹配参数然后调用get_loss完成\n",
  215. " xxxx\n",
  216. "\n",
  217. " def _fast_param_map(self, pred_dict, target_dict):\n",
  218. " # 这是一种快速计算loss的机制,因为在很多情况下其实都不需要通过\"键映射\",比如evaluate时,pred_dict只有一个元素,\n",
  219. " # target_dict也只有一个元素,那么无歧义地就可以把预测值与实际值用于计算metric, 基类判断了这种情况(可能还有其它无歧义的\n",
  220. " # 情况)。即_fast_param_map成功的话,就不需要使用键映射,这样即使在没有传递或者传递错误\"键映射\"的情况也可以直接计算metric。\n",
  221. " # 返回值是一个dict, 如果匹配成功,应该返回类似{'pred':value, 'target': value}的结果;如果dict为空则说明匹配失败,\n",
  222. " # __call__方法会继续尝试匹配。\n",
  223. "\n",
  224. " def evaluate(self, *args, **kwargs):\n",
  225. " # 这个是一定需要实现的,累加metric状态\n",
  226. " # (1) evaluate()中一定不能包含*arg这种参数形式。\n",
  227. " # (2) 如果包含**kwargs这种参数,这会将pred_dict与target_dict中所有参数传入。但是建议不要用这个参数\n",
  228. " raise NotImplementedError\n",
  229. "\n",
  230. " def get_metric(self, reset=True):\n",
  231. " # 这是一定需要实现的,获取最终的metric。返回值必须是一个dict。会在所有batch传入之后调用\n",
  232. " raise NotImplemented\n",
  233. "\n",
  234. "下面使用AccuracyMetric举例\n",
  235. "class AccuracyMetric(MetricBase): # MetricBase\n",
  236. " # 初始化需要映射的值,这里需要映射的值'pred', 'target'必须与evaluate()需要参数名是对应的\n",
  237. " def __init__(self, pred=None, target=None): \n",
  238. " super(AccuracyMetric, self).__init__()\n",
  239. " # 这里传入_init_param_map以使得pred和target被正确注册,但这一步不是必须的, 建议调用。传入_init_param_map的是用于\n",
  240. " # “键映射\"的键值对。假设初始化__init__(pred=None, target=None, threshold=0.1)中threshold是用于控制loss计算的,则\n",
  241. " # 不要将threshold传入_init_param_map.\n",
  242. " self._init_param_map(pred=pred, target=target)\n",
  243. "\n",
  244. " self.total = 0 # 用于累加一共有多少sample\n",
  245. " self.corr = 0 # 用于累加一共有多少正确的sample\n",
  246. "\n",
  247. " def evaluate(self, pred, target):\n",
  248. " # 对pred和target做一些基本的判断或者预处理等\n",
  249. " if pred.size()==target.size() and len(pred.size())=1: #如果pred已经做了argmax\n",
  250. " pass\n",
  251. " elif len(pred.size())==2 and len(target.size())==1: # pred还没有进行argmax\n",
  252. " pred = pred.argmax(dim=1)\n",
  253. " else:\n",
  254. " raise ValueError(\"The shape of pred and target should be ((B, n_classes), (B, )) or (\"\n",
  255. " \"(B,),(B,)).\")\n",
  256. " assert pred.size(0)==target.size(0), \"Mismatch batch size.\"\n",
  257. " # 进行相应的累加\n",
  258. " self.total += pred.size(0)\n",
  259. " self.corr += torch.sum(torch.eq(pred, target).float()).item()\n",
  260. "\n",
  261. " def get_metric(self, reset=True):\n",
  262. " # reset用于指示是否清空累加信息。默认为True\n",
  263. " # 这个函数需要返回dict,可以包含多个metric。\n",
  264. " metric = {}\n",
  265. " metric['acc'] = self.corr/self.total\n",
  266. " if reset:\n",
  267. " self.total = 0\n",
  268. " self.corr = 0\n",
  269. " return metric"
  270. ]
  271. },
  272. {
  273. "cell_type": "markdown",
  274. "metadata": {},
  275. "source": [
  276. "#### Tester: 用于做evaluation,应该不需要更改\n",
  277. "重要的初始化参数有,data, model, metric \n",
  278. "比较重要的function是test() \n",
  279. "test中的运行过程 \n",
  280. "&emsp;&emsp;predict_func = 如果有model.predict则为model.predict, 否则是model.forward \n",
  281. "&emsp;&emsp;for batch_x, batch_y in batch: \n",
  282. "&emsp;&emsp;&emsp;&emsp;# (1) 同步数据与model \n",
  283. "&emsp;&emsp;&emsp;&emsp;# (2) 根据predict_func的参数从batch_x中取出数据传入到predict_func中,得到结果pred_dict \n",
  284. "&emsp;&emsp;&emsp;&emsp;# (3) 调用metric(pred_dict, batch_y \n",
  285. "&emsp;&emsp;&emsp;&emsp;#(4) 当所有batch都运行完毕,会调用metric的get_metric方法,并且以返回的值作为evaluation的结果 \n",
  286. "&emsp;&emsp;metric.get_metric()"
  287. ]
  288. },
  289. {
  290. "cell_type": "markdown",
  291. "metadata": {},
  292. "source": [
  293. "#### Trainer: 对训练过程的封装。 \n",
  294. "里面比较重要的function是train() \n",
  295. "train()中的运行过程 \n",
  296. "&emsp;&emsp;# (1) 创建batch \n",
  297. "&emsp;&emsp;batch = Batch(dataset, batch_size, sampler=sampler) \n",
  298. "&emsp;&emsp;for batch_x, batch_y in batch: \n",
  299. "&emsp;&emsp;&emsp;&emsp;\"\"\" \n",
  300. "&emsp;&emsp;&emsp;&emsp;batch_x,batch_y都是dict。batch_x是DataSet中被设置为input的field;batch_y是DataSet中被设置为target的field。 \n",
  301. "&emsp;&emsp;&emsp;&emsp;两个dict中的key就是DataSet中的key,value会根据情况做好padding的tensor。 \n",
  302. "&emsp;&emsp;&emsp;&emsp;\"\"\" \n",
  303. "&emsp;&emsp;&emsp;&emsp;# (2)会将batch_x, batch_y中tensor移动到model所在的device \n",
  304. "&emsp;&emsp;&emsp;&emsp;# (3)根据model.forward的参数列表, 从batch_x中取出需要传递给forward的数据。 \n",
  305. "&emsp;&emsp;&emsp;&emsp;# (4)获取model.forward的输出结果pred_dict,并与batch_y一起传递给loss函数, 求得loss \n",
  306. "&emsp;&emsp;&emsp;&emsp;# (5)对loss进行反向梯度并更新参数 \n",
  307. "&emsp;&emsp;# (6) 如果有验证集,则需要做验证 \n",
  308. "&emsp;&emsp;tester = Tester(model, dev_data,metric) \n",
  309. "&emsp;&emsp;eval_results = tester.test() \n",
  310. "&emsp;&emsp;# (7) 如果eval_results是当前的最佳结果,则保存模型。 "
  311. ]
  312. },
  313. {
  314. "cell_type": "raw",
  315. "metadata": {},
  316. "source": [
  317. "除了以上的内容,\n",
  318. "Trainer中还提供了\"预跑\"的功能。该功能通过check_code_level管理,如果check_code_level为-1,则不进行\"预跑\"。\n",
  319. "check_code_level=0,1,2代表不同的提醒级别。目前不同提醒级别对应的是对DataSet中设置为input或target但又没有使用的field的提醒级别。\n",
  320. "0是忽略(默认);1是会warning发生了未使用field的情况;2是出现了unused会直接报错并退出运行\n",
  321. "\"预跑\"的主要目的有两个: (1) 防止train完了之后进行evaluation的时候出现错误。之前的train就白费了\n",
  322. " (2) 由于存在\"键映射\",直接运行导致的报错可能不太容易debug,通过\"预跑\"过程的报错会有一些debug提示\n",
  323. "\"预跑\"会进行以下的操作:(1) 使用很小的batch_size, 检查batch_x中是否包含Model.forward所需要的参数。只会运行两个循环。\n",
  324. " (2) 将Model.foward的输出pred_dict与batch_y输入到loss中, 并尝试backward. 不会更新参数,而且grad会被清零\n",
  325. " 如果传入了dev_data,还将进行metric的测试\n",
  326. " (3) 创建Tester,并传入少量数据,检测是否可以正常运行\n",
  327. "\"预跑\"操作是在Trainer初始化的时候执行的。\n",
  328. "正常情况下,应该不需要改动\"预跑\"的代码。但如果你遇到bug或者有什么好的建议,欢迎在开发群或者github提交issue。"
  329. ]
  330. }
  331. ],
  332. "metadata": {
  333. "kernelspec": {
  334. "display_name": "Python 3",
  335. "language": "python",
  336. "name": "python3"
  337. },
  338. "language_info": {
  339. "codemirror_mode": {
  340. "name": "ipython",
  341. "version": 3
  342. },
  343. "file_extension": ".py",
  344. "mimetype": "text/x-python",
  345. "name": "python",
  346. "nbconvert_exporter": "python",
  347. "pygments_lexer": "ipython3",
  348. "version": "3.6.7"
  349. }
  350. },
  351. "nbformat": 4,
  352. "nbformat_minor": 2
  353. }