From 53cb111a2844df45706974ca39e35e6d150628e5 Mon Sep 17 00:00:00 2001 From: Yiheng Wang Date: Mon, 22 Feb 2021 13:48:48 +0800 Subject: [PATCH 1/5] Update DynUNet Due to the forward function's changes of DynUNet, this commit update the corresponding places, as well as the loss calculation for trainer. In addition, the DiceCEloss has been implemented in MONAI, thus the self-designed loss function part has also been updated. Signed-off-by: Yiheng Wang --- modules/dynunet_tutorial.ipynb | 158 +++++---------------------------- 1 file changed, 23 insertions(+), 135 deletions(-) diff --git a/modules/dynunet_tutorial.ipynb b/modules/dynunet_tutorial.ipynb index 2a94d696c6..37bca33b09 100644 --- a/modules/dynunet_tutorial.ipynb +++ b/modules/dynunet_tutorial.ipynb @@ -53,38 +53,9 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "MONAI version: 0.4.0+54.gf9b47f0\n", - "Numpy version: 1.19.1\n", - "Pytorch version: 1.7.0a0+7036e91\n", - "MONAI flags: HAS_EXT = False, USE_COMPILED = False\n", - "MONAI rev id: f9b47f08691f53d9704dd62b01dbb77f5cae0ed6\n", - "\n", - "Optional dependencies:\n", - "Pytorch Ignite version: 0.4.2\n", - "Nibabel version: 3.2.1\n", - "scikit-image version: 0.15.0\n", - "Pillow version: 8.0.1\n", - "Tensorboard version: 1.15.0+nv\n", - "gdown version: 3.12.2\n", - "TorchVision version: 0.8.0a0\n", - "ITK version: 5.1.2\n", - "tqdm version: 4.54.1\n", - "lmdb version: 1.0.0\n", - "psutil version: 5.7.2\n", - "\n", - "For details about installing the optional dependencies, please visit:\n", - " https://docs.monai.io/en/latest/installation.html#installing-the-recommended-dependencies\n", - "\n" - ] - } - ], + "outputs": [], "source": [ "# Copyright 2020 MONAI Consortium\n", "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", @@ -119,7 +90,7 @@ " ValidationHandler,\n", ")\n", "from monai.inferers import SimpleInferer, SlidingWindowInferer\n", - "from monai.losses import DiceLoss\n", + "from monai.losses import DiceCELoss\n", "from monai.networks.nets import DynUNet\n", "from monai.transforms import (\n", " AddChanneld,\n", @@ -159,7 +130,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -218,17 +189,9 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "/workspace/data/medical\n" - ] - } - ], + "outputs": [], "source": [ "directory = os.environ.get(\"MONAI_DATA_DIRECTORY\")\n", "root_dir = tempfile.mkdtemp() if directory is None else directory\n", @@ -244,7 +207,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -322,18 +285,9 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "100%|██████████| 208/208 [00:02<00:00, 95.23it/s] \n", - "100%|██████████| 52/52 [00:00<00:00, 85.85it/s]\n" - ] - } - ], + "outputs": [], "source": [ "train_ds = DecathlonDataset(\n", " root_dir=root_dir,\n", @@ -365,34 +319,9 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAr8AAAEICAYAAABWPpy+AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjMuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8vihELAAAACXBIWXMAAAsTAAALEwEAmpwYAAAq3klEQVR4nO3de7Cd5XXf8d9CHCSELgddkXQkjiSLCASxIKqAmEwxNgS7TrHb1BO38XgSp7hNMo3bOMSmU+cybceeOiHupJOUBGrSJr7hUF+wTTBWxnVqk0ggA7JAQhfQXUJCSEJCSGL1j72VHuv9PUd7n/s+7/czo5HOOs/e+3n3+56Hh/fstVZkpgAAAIA6uGC0JwAAAACMFDa/AAAAqA02vwAAAKgNNr8AAACoDTa/AAAAqA02vwAAAKgNNr8YdRGxISJuHu15AAAGJiK2R8TbWxiXEfGmAb7GgB8L9HXhaE8AyMwVoz0HAABQD9z5BQAAQG2w+cWoO/vrsoj47Yj4YkT8r4g4GhFPR8QVEfGxiNgfETsi4rY+j/uFiNjYHLs1Ij50zvPeFRF7ImJ3RPxS31+ZRcTEiPhURLwYEfsi4o8j4uKRPnYAGE8iYnVEfC8iDjfX3z+MiIvOGfbO5pr9UkT8l4i4oM/jf7G5rr8cEY9ExOUjfAioATa/GGt+RtL/lHSppCclPaLGdbpA0u9K+u99xu6X9C5J0yT9gqR7IuI6SYqI2yX9O0lvl/QmSTef8zqfkHSFpJXN7y+Q9PFhOB4AqJMzkv6tpFmSbpT0Nkm/fM6Y90haJek6SXdI+kVJiog7JN0t6Z9Imi3p/0j67IjMGrUSmTnac0DNRcR2Sb8k6SZJb8nMW5vxn1Fj4ZuemWciYqqkI5IuzczD5nn+t6Q1mfnpiLhf0r7M/Fjze2+StFnSMklbJB2T9OOZuaX5/Rsl/UVmLh7OYwWA8ejsOp6Z3zon/mFJ/zAz39P8OiW9IzO/2fz6lyX908x8W0R8Q9KDmXlf83sXqLFWX5mZLzQfuywznx+p48L4xJ1fjDX7+vz7hKSXMvNMn68laYokRcQ7IuL7EXEoIg5Leqcadxskab6kHX2eq++/Z0uaLGld81dzhyV9sxkHAAxQ86NqX4uIvRFxRNJ/1v9fl8/qux6/oMZ6LUmXS/p0n3X5kKRQ4zdzwJBh84uOFBETJX1J0qckzc3MbklfV2OhlKQ9knr6PGRhn3+/pMZGekVmdjf/TM/MKcM/cwAY1/5I0rNq3KGdpsbHGOKcMX3X40WSdjf/vUPSh/qsy92ZeXFm/t9hnzVqhc0vOtVFkiZKOiDpdES8Q9Jtfb7/BUm/EBFXRsRkSf/h7Dcy8w1Jf6LGZ4TnSFJELIiInx6x2QPA+HT242nHImK5pH9txvxGRFwaEQsl/ZqkzzfjfyzpYxGxQpIiYnpE/LORmDTqhc0vOlJmHpX0b9TY5L4s6Z9L+kqf739D0n+VtEbS85K+3/zWyebfv3k23vzV3Lck/diITB4Axq+PqLEeH1XjJsPnzZgvS1onab2khyXdJ0mZ+ZCkT0r6XHNdfkbSO4Z/yqgbEt5QCxFxpRoL6cTMPD3a8wEAAKODO78YtyLiPc16vpeqcTfhq2x8AQCoNza/GM8+pEYt4C1q1J50nz0DAAA1wsceAAAAUBvc+QUAAEBtXDiYBzdbyH5a0gRJf5qZnzjPeG4zA+hYmXluvdKO0866fVFMzEm6ZMTmBgBD6ahefikzKw2sBvyxh4iYIGmTpFsl7ZT0d5Lel5k/7OcxbH4BdKxO3/y2u25Pixl5fbxtBGcIAEPnW/ngusxcdW58MB97WC3p+czcmpmvS/qcpDsG8XwAgOHFug2g9gaz+V2gH+3PvVOm/3ZE3BkRayNi7SBeCwAweOddt/uu2af+vicMAIwfw57wlpn3ZuYqd9sZADC29F2zuzRxtKcDAENuMJvfXZIW9vm6pxkDAIxNrNsAam8wm9+/k7QsIhZHxEWSfk7SV4ZmWgCAYcC6DaD2BlzqLDNPR8SvSnpEjZI592fmhiGbGQBgSLFuA8Ag6/xm5tclfX2I5gIAGGas2wDqjg5vAAAAqA02vwAAAKgNNr8AAACoDTa/AAAAqA02vwAAAKgNNr8AAACoDTa/AAAAqI1B1fkdK+66665K7LXXXrNjT5w40fLzTpgwoRJ74403Wn7O06dPV2JTp061Y3/qp36qElu+fLkd++yzz1ZiTzzxhB3r5nby5MlK7ODBg/bxZ86cqcRmzJhhx15wQfX/pS680F9ibuzRo0ft2EsuuaQSmzNnTiVWOufOpEmTbNy9D6+//rodGxGVWOl4T506VYkdOnTIjr3ooosqsQULFlRipeuut7e3EnvXu95lxy5cuLASK72PR44cqcTcNS5Jc+fOrcTcdVc655MnT24pJkmzZs2ycQAAHO78AgAAoDbY/AIAAKA22PwCAACgNtj8AgAAoDbY/AIAAKA2xkW1B1eBoVSR4OKLL275eTOzpdcqVQ7o6uqqxLq7u+3YJUuWVGJ79+61Y11lh8OHD9uxrqrC8ePHKzFXUUGSLrvsskqsnWoA06dPt2Ndlr+rBlCKlyolOK5KQOmcTZkypRJzFS9Kc5g2bZod694zVy2i9Hq7d++uxEqVJTZu3FiJucolkq8CUfoZcc/hrnHJXwul+TqukoX7eQQAoF3c+QUAAEBtsPkFAABAbbD5BQAAQG2w+QUAAEBtDCrhLSK2Szoq6Yyk05m5aigm1S6XpFRqI+za4s6cOdOOdUlRLhlp4sSJ9vHueUtJTi5J6Xvf+54d65Lb2mmr65L2XEvdklKymEuuK7XgdclLpeS4Y8eOVWKvvvpqS69f4p5Taq99tZtDKTnOnYfSe+7m4JIUXTKi5BPxtm3bZsfu27evElu6dKkd686Pew8kf35d4mIpEc/9nLzyyit2LNozVtZtABgtQ1Ht4a2Z+dIQPA8AYGSwbgOoLT72AAAAgNoY7OY3Jf1VRKyLiDvdgIi4MyLWRsTaQb4WAGDw+l23+67Zp+RrbwNAJxvsxx5uysxdETFH0qMR8WxmfqfvgMy8V9K9khQRVKkHgNHV77rdd82eFjNYswGMO4O685uZu5p/75f0kKTVQzEpAMDwYN0GUHcDvvMbEZdIuiAzjzb/fZuk3x2ymbVh+/btlVipzazLTnetdiVpwYIFLT1vOxn+pcoBzz77bCW2a9cuO9a16z19+nTLc3DVMdqplDB37lwbd21xSxn6L71UzbUptU12lSxcpYNSFYqXX365EitVeyhdC46rflCquuGqNZS48/P6669XYqX3y7X2XrFihR3rrvFSe2P3eqWW1O4YXAvtgwcP2se7n6lSpRS0biyt2wAwWgbzsYe5kh5q/gfpQkl/kZnfHJJZAQCGA+s2gNob8OY3M7dKevMQzgUAMIxYtwGAUmcAAACoETa/AAAAqI2h6PA2JpUSrVzcJV9J0v79+yuxefPmVWLz58+3j3dJVa6drCTt3LmzEislsR05cqQS2717tx3rWiy79rWlpL2urq6WYpJ/b6666io71h2va/Es+aQ9p5S012qb6tLYUiKda81bSgBzz1F6H10LbpeEVmoNfOWVV1ZipWvUJWCWjsG1Yy69j67FskvUdIl8kr/GXdtnAADaxZ1fAAAA1AabXwAAANQGm18AAADUBptfAAAA1AabXwAAANTGuKj24Fqplrh2qqVqAq4trsvQL72+y07fs2ePHesqQ5Qy4V1Vg+7ubjvWteBtp1WuqyhQOgZXnaLU7tdVnHBteSV/Hg4fPlyJlVrlunbMpRa+Tun8uvNQqhwya9asSsy1aJbKrbnP5SoqSP78Pvzww3bsD37wg0qsdLw333xzJbZ8+XI71lWGcBUcSlzFCdeaHACAdnHnFwAAALXB5hcAAAC1weYXAAAAtcHmFwAAALUxLhLe9u7dW4mVEq0ys6WY5JPQXJJTKXnKJceVkpQuueSSll5L8ok/rqWt5BPZDhw4UIm5pDDJt+V1bZ8ln9DkXkuSLr/88kpsxYoVduyyZcsqMXfOSvNyyWalc+aOYeLEiXasSwYsJWW5uZXaV7tksW984xuV2Jo1a+zjXdKdSxqUfJJhqZ3zjh07KrG77rrLjnXn9/jx45VYqQ25a5vsfkYAAGgXd34BAABQG2x+AQAAUBtsfgEAAFAbbH4BAABQG+dNeIuI+yW9S9L+zLy6GZsh6fOSeiVtl/TezPQZNSPAJRO57mqS1NPTU4m5BCNJOnr0aCXmEnFcQpXkE+ZcTPJduUod3twcSl3X3LG5DmKlZCL3eJfoJflkMdepS/Ld2Nz7LUlvfetbK7HZs2dXYgsXLrSPd8db6sTmEtZOnDhhx7qERJcUJknr1q2rxEoJeq7zm3v8U089ZR/vEtZK3fNcQmSpw9uGDRsqsd27d9uxl156aUuv1U7XN7SuE9ZtYCx4ZPf6YXnen56/clieF0OjlTu/n5F0+zmxj0p6LDOXSXqs+TUAYGz4jFi3AcA67+Y3M78j6dz6XHdIeqD57wckvXtopwUAGCjWbQAoG2id37mZefb37Hsl+SKxkiLiTkl3DvB1AABDo6V1u++aPUmTR2hqADByBp3wlo1uA75LROP792bmqsxcNdjXAgAMXn/rdt81u0u+wQsAdLKBbn73RcQ8SWr+7TN3AABjBes2AGjgH3v4iqQPSPpE8+8vD9mMBsBl3ZeqF7jqA6WqCq79rKu00E6739JrHT58uKXXknxr3tLzukoULhPftZ6V2svGd+9t6RjcOduyZYsdu2TJkkrMvQelc759+/ZK7PHHH7djXRWKUnULd32415J8FYnS827btq0Su+KKKyqx0vHu27evEiu1ynav5R4vSRFRiZWqjJw6daoSc5VDSsfgKmyUWjSjZWNq3QZG2nBVdhjsa1EZYuSd985vRHxW0vck/VhE7IyID6qxeN4aEZslvb35NQBgDGDdBoCy8975zcz3Fb71tiGeCwBgCLBuA0AZHd4AAABQG2x+AQAAUBsDTXgbU1wr466uLjvWJYaVEnxcm9jp06dXYqWkMJc85Vq8lsycOdPG3fG6BDBJmjy5WqfTJVqV2hAfOnRunfxyO2inlIjnnqP0Pm7durUSmzdvXiVWOufu/LrnlKTNmzdXYqWkve7u7krMXR+SP153biTfetkle7kWz5I/v729vXasa2XsWilL0k/8xE9UYi7pT/KJf67N9NKlS+3jXRtwWiED9TWSyWojjeS4kcedXwAAANQGm18AAADUBptfAAAA1AabXwAAANQGm18AAADUxrio9uCy5kuZ4a7taqnF6sSJE1t6fKmigYuXxrqs+1I1AJfNX6pI4KpLuFbGpRbNri2ve19Kcyi18M3Mlh4v+dbP7jyUWjS753WVB0pjjx49asfOnz+/EnOtoyV/PZZaDrvzvmPHDjvWcVUvXPUEyVescMdVGuuuD8mfH/ceuIoqAADPVYagAkT7uPMLAACA2mDzCwAAgNpg8wsAAIDaYPMLAACA2hgXCW9ORNi4a+Nbaou7d+/eSuzEiROVWCnJySWGldoQu/a3pYQoN9a1v5XKx3auUrKYS9BzLY8lP9/Se+O4hCpJWrx4cSXm2giXEvEWLlxYiZXeL9da2MUk39q3lBznrsdSMqBLeHPH4NpcSz657ZVXXrFj3XvW09Njx7prpPS8bm4u4W3u3Ln28a61d+n6ADD+lZK6xnPbYwwf7vwCAACgNtj8AgAAoDbY/AIAAKA22PwCAACgNs67+Y2I+yNif0Q80yf22xGxKyLWN/+8c3inCQBoFes2AJS1Uu3hM5L+UNKfnRO/JzM/NeQzGgDXGriU+e+yyA8ePGjH7tq1qxJz1QtKr+WUMvRdvNQK2WXNuyoUkq9u4do5l1opX3XVVZXYhg0b7Fj3PrpzU+KqOki++kCrVSwk37J4yZIldqw7D3/9139tx5ZaaDsXXlj9USsdgztnbl6lqhuu4oQ7j5KvOFGqbuHGuooXpbh7D0qPd+esdI3C+ozG+LoNDAVXBYIKEDif8975zczvSPL/lQUAjDms2wBQNpjP/P5qRDzV/PVa68VcAQCjhXUbQO0NdPP7R5KWSlopaY+k3ysNjIg7I2JtRKwd4GsBAAavpXW775p9StWP4ABApxvQ5jcz92Xmmcx8Q9KfSFrdz9h7M3NVZq4a6CQBAIPT6rrdd83uUuv5DADQKQbU3jgi5mXmnuaX75H0TH/jh5tLQiu1EZ40aVIl5hJxJJ/g49rMuueUpC1btlRipZbFLvFn9+7ddqw73tIcXKtbFyu1Id65c2clVmodPX/+/ErMJS5J0rx58yqxK664wo515+e1116rxKZOnWof7+ZQSsRzyXW9vb12rGt/XUqCc8mLrk215N/fV199tRIrJTm61yolT7bTgtsl6GWmHeuOzT3v4cOH7eOdpUuXtjwWVWNt3QaGS91aIZeOq/Q+oIXNb0R8VtLNkmZFxE5JvyXp5ohYKSklbZf0oeGbIgCgHazbAFB23s1vZr7PhO8bhrkAAIYA6zYAlNHhDQAAALXB5hcAAAC1weYXAAAAtTGgag9jjatUUGojfODAgUqsVL3AtYR1rYFdJr4kzZ07txJzVQokad++fZVYqRqAy5ovVXtwFRBchn2pSsHx48crsWnTptmx7r0paacyhKuq4CoduEoNkq9oUKp+4K6bFStW2LGuYsT69evtWHcuS9UaXJURN7bUGthdC6XXctdSOy2pS9z7617LtWIuKf3sAEAraIWMs7jzCwAAgNpg8wsAAIDaYPMLAACA2mDzCwAAgNoYFwlvBw8erMR27dplx7rErlmzZtmx06dPr8RcUlcp0cslcLnWwpJPUpo9e7Yd65LQSi1pXXKaS0YqtXh27ZxLyVOu1W13d7cd61o3u2REySeAuWTCEnd+58yZY8e6c+beb8kn2JWuO3eNunbBkk/sctdNKfHQveftJNeVriX3s1O6bk6dOlWJuQS9dlqLv/zyy3YsAADt4M4vAAAAaoPNLwAAAGqDzS8AAABqg80vAAAAaoPNLwAAAGpjXFR7cBn2+/fvt2NdK2QXK3FZ+6VWuS7DvtQ61rX7LbVzddUHDh06ZMe61sulihPOnj17KrFSVQZ3bKVKCZMnT67ESi2pXTUNVyXAtXIuxUvzcq+1YMECO9Y9x+WXX27HunNZqljhKh1ccEH1/1NLbbXd+1iq9uDipeonri126X1083VtpkstvF3lkFI7ZwAYKNfyWKLt8XjHnV8AAADUBptfAAAA1AabXwAAANQGm18AAADUxnkT3iJioaQ/kzRXUkq6NzM/HREzJH1eUq+k7ZLem5mj0n/UJayVWrS6ZKIZM2bYsa79rGuxWmpT6+ZValns2uqWWuW6saXkuNLrneuVV16xcZcA5pLzJP/elI7BJZGV2kS7uZ08ebISK7X7dXHXTro0r5kzZ9qxe/furcQWL15sx7rErpdeesmOdQmJ7hotJVq200q5nfbGrm1yKeHNJae5RLzS4931XLo+8KM6Yc0GgNHUyp3f05J+PTOvknSDpF+JiKskfVTSY5m5TNJjza8BAKOLNRsA+nHezW9m7snMJ5r/Pippo6QFku6Q9EBz2AOS3j1McwQAtIg1GwD611ad34jolXStpMclzc3Ms0Vg96rxKzb3mDsl3TmIOQIABmCwa/YkVetxA0CnaznhLSKmSPqSpA9n5pG+38tGRfpqVfrG9+7NzFWZuWpQMwUAtGwo1uwuVRuTAECna+nOb0R0qbGI/nlm/mUzvC8i5mXmnoiYJ8m3VBsBroNXKYlt0aJFlVip+5Xrmua6X7luY5JPSCol+LjuWbt377ZjXUJSqbuZe16nt7fXxl2S06lTp+zYrVu3VmKlhLdt27ZVYkuXLrVjXec4lwRX6nLnEg9LyVMuWcwlhZWUuqO5ZMBSwpubgzvn06dPt4933dFKCW+uW18pedJ1pCt1Xdu+fXsl5pIUSz+n7vy0cx7qbqyv2QCGn+tSV+poVzfnvfMbESHpPkkbM/P3+3zrK5I+0Pz3ByR9eeinBwBoB2s2APSvlTu/b5H0fklPR8T6ZuxuSZ+Q9IWI+KCkFyS9d1hmCABoB2s2APTjvJvfzPyupCh8+21DOx0AwGCwZgNA/+jwBgAAgNpg8wsAAIDaaKvO71hVaj/ruNbApda+r776aiXmsuPdOMlXWii14HXZ7aWqCq6F7pEjR8xIadOmTZVYOxUr3PtVag3c09NTiZVaIW/YsKESc1UdSs/hqmbs3++T192xld6vjRs3VmKu5bEkLVmypBJzbYyl9tpEu7FuvkePHrWPd5Uh2qlI0k6r6xJXBcLNq53W4O5aBIDh0E5VBFdVYawqzbVuVSC48wsAAIDaYPMLAACA2mDzCwAAgNpg8wsAAIDaGBcJb7Nnz67ESq2BXaLVG2+8Yce6hLNjx45VYhMnTrSPd8ltpQQf1zp28uTJdqxLSHLJRJK0evVqGz/Xzp07W467uUrSNddcU4mVWuXecMMNLT/v3/zN31RirtVtO62FS62Q3XxLbYhd8uNll11mx7pkMXfdSj7xz7UyLp0zlxhWauHtXqvUKtsl3ZUSOF0inTsPpaROp3SNA8BoajVZrJMS48Y77vwCAACgNtj8AgAAoDbY/AIAAKA22PwCAACgNtj8AgAAoDbGRbWHHTt2VGKl7HaXyV7KOHcZ665aQ3d3t328qz7g2r6WnsO1R5Z81n2pfa1rE+sqVlxwgf//oOeff74S+/73v2/HuqoMpQoOV199dSVWql6waNGiSsxVZShVe3Dtp9tpQ1w6D64KRGmsm9u+ffvsWDe3dtr9uqoKmWnHuueYM2eOHeuO1/2MSP4YNm/e3NLrS/7nrFSVBQCAdnDnFwAAALXB5hcAAAC1weYXAAAAtcHmFwAAALVx3oS3iFgo6c8kzZWUku7NzE9HxG9L+peSzvZ4vTszvz5cE+3P9u3bK7GFCxfasa4tbin5ySX+uGSzUqLV/PnzKzGXjCSVE6Ucl5T1wx/+0I597rnnKjGXPFVKmHMJgu49kPwxlFo/uxbN119/vR177bXXVmJf/epXK7FHHnmk5Xldd911dqx7b0tJWcePH6/ESglgu3btqsRcIl5pDk6p3a97fETYsS75sXS87hopJdKtXLmyEnPtuktJfy5htdQqGz+qE9ZsABhNrVR7OC3p1zPziYiYKmldRDza/N49mfmp4ZseAKBNrNkA0I/zbn4zc4+kPc1/H42IjZIWDPfEAADtY80GgP619ZnfiOiVdK2kx5uhX42IpyLi/oio/i698Zg7I2JtRKwd3FQBAO0Y7Jp9SidHaqoAMGJa3vxGxBRJX5L04cw8IumPJC2VtFKNuwy/5x6Xmfdm5qrMXDX46QIAWjEUa3aX/Gf2AaCTtbT5jYguNRbRP8/Mv5SkzNyXmWcy8w1JfyJp9fBNEwDQKtZsAChrpdpDSLpP0sbM/P0+8XnNz5ZJ0nskPTM8Uzw/1/a0nZbFriqD5FvzusoBpVbKrp2ry66XfNZ7qVKCy+bv7e21YydNmlSJuaz5bdu22cefPFn9tWcp695VzXAVESR/bNOmTbNjt27dWom5Ch+bNm2yj1+2bFklduDAATNSeuKJJyqxa665xo51FQ3cvCRf2WHevHl2rKu24Obrzq3kq0CU3tspU6ZUYqWKJK76Sakyhauq4ipsuPdb8q2US1VZ8KM6Yc0G6uin568c7Snokd3rR3sKY0Ir1R7eIun9kp6OiPXN2N2S3hcRK9UopbNd0oeGYX4AgPawZgNAP1qp9vBdSa5IKPUhAWCMYc0GgP7R4Q0AAAC1weYXAAAAtdHKZ37HvLe//e2V2IwZM+zYF154oRJzSV2SbxnsxpZatLp2sJdffrkd6xLASq1yXYJeKfnJzc0lt7nkPEmaPn16JVZqf+u41sSST/Z6+umn7ViXYOfO+c///M/bx7vExd27d9uxH//4xyuxUutpd41t2LDBjnXthUvJgC6B0iVwnj592j7ecS2tJX9+S8/rrsfSNepaYLukvwsu8P//7RLxSu2gAQCtGQtJd2MBd34BAABQG2x+AQAAUBtsfgEAAFAbbH4BAABQG2x+AQAAUBvjotqDqz7gqjpI0qFDhyqxUqtbV9nBtY5985vfbB/vWsqWXstl85fa37oqAY8//rgd++STT9r4uUptal3WfqmCw6pVqyqxFStW2LHuvenq6rJjZ86c2dJYV1FBkpYuXVqJPffcc3bse9/73kpszZo1duxDDz1UiZVaA3d3d1die/bsqQ6UtGDBgkrMtfB2Mcm3IS614HZVL0qVIVzVC9eGWPJVIFxlh1LbZXdspcoQAAC0g/+aAAAAoDbY/AIAAKA22PwCAACgNtj8AgAAoDbGRcKba4vrEtMkadasWZXYwoUL7VjXMriUZOS4hLmdO3fasS5x6LLLLrNj9+7dW4mVEq1c+1qXLNbb22sff/3111diPT09dqxrQzxhwgQ71rWvveiii+zYY8eOtfRapRbPzuTJk238/e9/fyX2kz/5k3bs/fffX4nt2rXLjnVtpkutgUvtlM/l3kPJJ5GVfh5cvJRY5hJLS+fXjXXPW2qlnJmV2NGjR+1YAADawZ1fAAAA1AabXwAAANQGm18AAADUBptfAAAA1MZ5E94iYpKk70ia2Bz/YGb+VkQslvQ5STMlrZP0/sz0GTzDzCVluYQZyScZueQpSXr11VcrMZd0U+rU5RLLSslELl7qynXjjTdWYqWuXI5LUiolLrnkqVJXL5eAdeGF/hJzyYAuJvkkQ9fNrXQMLmGu9N46LklSku6+++5K7L777rNjv/jFL1ZipYQ311XPdYgrJfi5a//qq6+2Y92x7dixw451SZWu26Dkk/amTp1aiZWSHN352bJlix2LH9UJazYAjKZW7vyelHRLZr5Z0kpJt0fEDZI+KemezHyTpJclfXDYZgkAaBVrNgD047yb32w4e+usq/knJd0i6cFm/AFJ7x6OCQIAWseaDQD9a+kzvxExISLWS9ov6VFJWyQdzsyzRTp3SlpQeOydEbE2ItYOwXwBAOcxVGv2KfmPIgFAJ2tp85uZZzJzpaQeSaslLW/1BTLz3sxclZmrBjZFAEA7hmrN7tLE4ZoiAIyatqo9ZOZhSWsk3SipOyLOZjP1SPKtrQAAo4I1GwCqWqn2MFvSqcw8HBEXS7pVjcSJNZJ+Vo3s4Q9I+vJwTrQ/mzZtqsRKbYRddnop49xlwrt2sK51reSrRSxevNiOXbRoUSV2zTXX2LGuusUrr7zS8hxclYCtW7fax586daoSmzjR3w1y702pUoKrmuEqOEi+WoOrAOEqIki+4kSpVa6r/FEaO2/evErsIx/5iB3rKlE8/PDDduyCBdXfRrvzWKpo4q6F0jlzSlU33POWKmy4Ns+uSkjp+pg/f34l5t5vSXrxxRdtvK46Yc0GgNF03s2vpHmSHoiICWrcKf5CZn4tIn4o6XMR8R8lPSnJ13gCAIwk1mwA6Md5N7+Z+ZSka018qxqfJQMAjBGs2QDQPzq8AQAAoDbY/AIAAKA2WvnM75jnkmvOnDljx7qkqssuu8yOde1nXQLY8uW+ipBrDVxK8Jk+fXolVkrwcW1mXUzyiVKulXLp/XLHMHfuXDvWJT+VnvfgwYM27syZM6cSaycxzbXVnTx5sh3rkshccp3k3/Oenh47dsWKFZVYKeHNJZadPn26EislsblrtJTE1k7ioZuXu74kn9jprvHSvFwS6f79++1YAADawZ1fAAAA1AabXwAAANQGm18AAADUBptfAAAA1AabXwAAANTGuKj2cOWVV1ZipSx0VyXAVT+QpO3bt1dil1xySSV222232ce7KgUuE1+SLr300krMtYMtxd28JF/VwLUhLrV4LlWRaFXpvXXHW3otVzHCHcOJEyfs4121BtdCuF2uMkSp4oQ7D+5alKQpU6a09LxunCTdeuutLc9r9+7dlZir8CFJ1113XSW2ZcsWO/bQoUOVmDvnpffAVTrp7e21Y++55x4bBwDA4c4vAAAAaoPNLwAAAGqDzS8AAABqg80vAAAAamNcJLy5Nq+lNsIuWWzdunV2rGtvvHTp0krMtfUtKSXiuWSvSZMm2bEu2avUgtcl2Lk2s6U2xMeOHavESsfrkpeOHDlix7oWyaWxLu7OuUtAk/y1UDoG9z6W2v26JMP58+fbsV1dXZVY6fwuWbKkEvvud79bic2ePds+fufOnZWYu5YlP9+bbrrJjnUJa9/+9rft2G9+85uVmLsWS0mKTilZFACAdnDnFwAAALXB5hcAAAC1weYXAAAAtcHmFwAAALVx3s1vREyKiL+NiB9ExIaI+J1m/DMRsS0i1jf/rBz22QIA+sWaDQD9a6Xaw0lJt2TmsYjokvTdiPhG83u/kZkPDt/0WtNqK1XJZ/677HpJ2rFjRyV28uTJSqxUKWHz5s2VWKkdrKs+sHz5cjvWtebt7u62Y111iz179lRirp2s5KsylDL03XveTlvdRYsW2bHuGI4fP16JlVo0u/Ozb98+O9adB9eaWPLHUGrX++ijj1ZiPT09duzUqVMrMXfduteXpAMHDlRiL774oh27bNmySmzVqlV27KZNmyqxUrUHx1W8KFXScNfjwoULW36tmhvzazYAjKbzbn6zUT/qbL2rruYfX1MKADCqWLMBoH8tfeY3IiZExHpJ+yU9mpmPN7/1nyLiqYi4JyKqhVcbj70zItZGxNqhmTIAoD9DtWafUvU3XQDQ6Vra/GbmmcxcKalH0uqIuFrSxyQtl/QPJM2Q9JuFx96bmasy0/8uFQAwpIZqze6S3R8DQEdrq9pDZh6WtEbS7Zm5JxtOSvofklYPw/wAAAPEmg0AVVFqCfv3AyJmSzqVmYcj4mJJfyXpk5LWZeaeaGSs3CPptcz86Hmei8+dAehYmekz9MaQoVyzp8WMvD7eNvyTBoBh8K18cJ375EEr1R7mSXogIiaocaf4C5n5tYj4dnORDUnrJf2roZwwAGBAWLMBoB+tVHt4StK1Jn7LsMwIADBgrNkA0D86vAEAAKA22PwCAACgNtj8AgAAoDbY/AIAAKA22PwCAACgNtj8AgAAoDbY/AIAAKA22PwCAACgNtj8AgAAoDbY/AIAAKA22PwCAACgNtj8AgAAoDbY/AIAAKA22PwCAACgNtj8AgAAoDbY/AIAAKA22PwCAACgNtj8AgAAoDbY/AIAAKA2IjNH7sUiDkh6ofnlLEkvjdiLjxyOq/OM12PjuIbW5Zk5exRed9SwZne88XpsHFfnGVPr9ohufn/khSPWZuaqUXnxYcRxdZ7xemwcF4bSeH3fx+txSeP32DiuzjPWjo2PPQAAAKA22PwCAACgNkZz83vvKL72cOK4Os94PTaOC0NpvL7v4/W4pPF7bBxX5xlTxzZqn/kFAAAARhofewAAAEBtsPkFAABAbYz45jcibo+I5yLi+Yj46Ei//lCKiPsjYn9EPNMnNiMiHo2Izc2/Lx3NOQ5ERCyMiDUR8cOI2BARv9aMd/SxRcSkiPjbiPhB87h+pxlfHBGPN6/Jz0fERaM914GIiAkR8WREfK359Xg5ru0R8XRErI+Itc1YR1+LnYQ1e+xjze7YtY01e5SM6OY3IiZI+m+S3iHpKknvi4irRnIOQ+wzkm4/J/ZRSY9l5jJJjzW/7jSnJf16Zl4l6QZJv9I8T51+bCcl3ZKZb5a0UtLtEXGDpE9Kuicz3yTpZUkfHL0pDsqvSdrY5+vxclyS9NbMXNmnTmSnX4sdgTW7Y7BmdybW7FEy0nd+V0t6PjO3Zubrkj4n6Y4RnsOQyczvSDp0TvgOSQ80//2ApHeP5JyGQmbuycwnmv8+qsYP5wJ1+LFlw7Hml13NPynpFkkPNuMdd1ySFBE9kv6RpD9tfh0aB8fVj46+FjsIa3YHYM3urOOSWLM1ysc20pvfBZJ29Pl6ZzM2nszNzD3Nf++VNHc0JzNYEdEr6VpJj2scHFvz10zrJe2X9KikLZIOZ+bp5pBOvSb/QNJdkt5ofj1T4+O4pMZ/7P4qItZFxJ3NWMdfix2CNbvDsGZ3jD8Qa/aouXA0X3y8y8yMiI6tJRcRUyR9SdKHM/NI439MGzr12DLzjKSVEdEt6SFJy0d3RoMXEe+StD8z10XEzaM8neFwU2buiog5kh6NiGf7frNTr0WMPZ1+LbFmdwbW7NG/Fkf6zu8uSQv7fN3TjI0n+yJiniQ1/94/yvMZkIjoUmMR/fPM/MtmeFwcmyRl5mFJayTdKKk7Is7+j2AnXpNvkfSPI2K7Gr+WvkXSp9X5xyVJysxdzb/3q/Efv9UaR9fiGMea3SFYszsKa/YoX4sjvfn9O0nLmhmNF0n6OUlfGeE5DLevSPpA898fkPTlUZzLgDQ/e3SfpI2Z+ft9vtXRxxYRs5t3DxQRF0u6VY3Pxq2R9LPNYR13XJn5sczsycxeNX6mvp2Z/0IdflySFBGXRMTUs/+WdJukZ9Th12IHYc3uAKzZnXVcrNlj4Ngyc0T/SHqnpE1qfG7n34/06w/xsXxW0h5Jp9T4fM4H1fjczmOSNkv6lqQZoz3PARzXTWp8ZucpSeubf97Z6ccm6cclPdk8rmckfbwZXyLpbyU9L+mLkiaO9lwHcYw3S/raeDmu5jH8oPlnw9k1o9OvxU76w5o99v+wZnfe2tbnGFmzR+EP7Y0BAABQG3R4AwAAQG2w+QUAAEBtsPkFAABAbbD5BQAAQG2w+QUAAEBtsPkFAABAbbD5BQAAQG38Pw2iyrqz7UrxAAAAAElFTkSuQmCC\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "for i in range(2):\n", " image, label = val_ds[i][\"image\"], val_ds[i][\"label\"]\n", @@ -406,44 +335,6 @@ " plt.show()" ] }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Customize loss function\n", - "Here we combine Dice loss and Cross Entropy loss." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [], - "source": [ - "class CrossEntropyLoss(nn.Module):\n", - " def __init__(self):\n", - " super().__init__()\n", - " self.loss = nn.CrossEntropyLoss()\n", - "\n", - " def forward(self, y_pred, y_true):\n", - " # CrossEntropyLoss target needs to have shape (B, D, H, W)\n", - " # Target from pipeline has shape (B, 1, D, H, W)\n", - " y_true = torch.squeeze(y_true, dim=1).long()\n", - " return self.loss(y_pred, y_true)\n", - "\n", - "\n", - "class DiceCELoss(nn.Module):\n", - " def __init__(self):\n", - " super().__init__()\n", - " self.dice = DiceLoss(to_onehot_y=True, softmax=True)\n", - " self.cross_entropy = CrossEntropyLoss()\n", - "\n", - " def forward(self, y_pred, y_true):\n", - " dice = self.dice(y_pred, y_true)\n", - " cross_entropy = self.cross_entropy(y_pred, y_true)\n", - " return dice + cross_entropy" - ] - }, { "cell_type": "markdown", "metadata": {}, @@ -453,12 +344,12 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ "device = torch.device(\"cuda:0\")\n", - "loss = DiceCELoss()\n", + "loss = DiceCELoss(to_onehot_y=True, softmax=True, batch=False)\n", "learning_rate = 0.01\n", "max_epochs = 200\n", "\n", @@ -491,6 +382,7 @@ " strides=strides,\n", " upsample_kernel_size=strides[1:],\n", " norm_name=\"instance\",\n", + " deep_supervision=True,\n", " deep_supr_num=2,\n", " res_block=False,\n", ").to(device)\n", @@ -511,7 +403,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -594,7 +486,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -616,12 +508,12 @@ " )\n", "\n", " def _compute_loss(preds, label):\n", - " labels = [label] + [\n", - " interpolate(label, pred.shape[2:]) for pred in preds[1:]\n", - " ]\n", + " preds = torch.unbind(preds, dim=1)\n", " return sum(\n", - " 0.5 ** i * self.loss_function(p, l)\n", - " for i, (p, l) in enumerate(zip(preds, labels))\n", + " [\n", + " 0.5 ** i * self.loss_function.forward(p, label)\n", + " for i, p in enumerate(preds)\n", + " ]\n", " )\n", "\n", " self.network.train()\n", @@ -629,17 +521,13 @@ " if self.amp and self.scaler is not None:\n", " with torch.cuda.amp.autocast():\n", " predictions = self.inferer(inputs, self.network)\n", - " loss = _compute_loss(\n", - " [predictions] + self.network.get_feature_maps(), targets\n", - " )\n", + " loss = _compute_loss(predictions, targets)\n", " self.scaler.scale(loss).backward()\n", " self.scaler.step(self.optimizer)\n", " self.scaler.update()\n", " else:\n", " predictions = self.inferer(inputs, self.network)\n", - " loss = _compute_loss(\n", - " [predictions] + self.network.get_feature_maps(), targets\n", - " ).mean()\n", + " loss = _compute_loss(predictions, targets).mean()\n", " loss.backward()\n", " self.optimizer.step()\n", " return {\n", From d7bff9ccb0e0309eb5a4e4a28014609abb46345b Mon Sep 17 00:00:00 2001 From: Yiheng Wang Date: Mon, 22 Feb 2021 13:56:29 +0800 Subject: [PATCH 2/5] Remove unused libraries Signed-off-by: Yiheng Wang --- modules/dynunet_tutorial.ipynb | 2 -- 1 file changed, 2 deletions(-) diff --git a/modules/dynunet_tutorial.ipynb b/modules/dynunet_tutorial.ipynb index 37bca33b09..ae6dfa3895 100644 --- a/modules/dynunet_tutorial.ipynb +++ b/modules/dynunet_tutorial.ipynb @@ -77,7 +77,6 @@ "import matplotlib.pyplot as plt\n", "import numpy as np\n", "import torch\n", - "import torch.nn as nn\n", "from monai.apps import DecathlonDataset\n", "from monai.config import print_config\n", "from monai.data import DataLoader\n", @@ -111,7 +110,6 @@ " SpatialPadd,\n", " ToTensord,\n", ")\n", - "from torch.nn.functional import interpolate\n", "\n", "print_config()" ] From bcb738ec3b8148915d85f015452d444fa4e70ca7 Mon Sep 17 00:00:00 2001 From: Yiheng Wang Date: Mon, 22 Feb 2021 14:56:59 +0800 Subject: [PATCH 3/5] Add some cell outputs for reference Signed-off-by: Yiheng Wang --- modules/dynunet_tutorial.ipynb | 91 ++++++++++++++++++++++++++++++---- 1 file changed, 81 insertions(+), 10 deletions(-) diff --git a/modules/dynunet_tutorial.ipynb b/modules/dynunet_tutorial.ipynb index ae6dfa3895..2f91672fea 100644 --- a/modules/dynunet_tutorial.ipynb +++ b/modules/dynunet_tutorial.ipynb @@ -53,9 +53,38 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "MONAI version: 0+untagged.1.g19a9f05.dirty\n", + "Numpy version: 1.19.1\n", + "Pytorch version: 1.7.0a0+7036e91\n", + "MONAI flags: HAS_EXT = True, USE_COMPILED = False\n", + "MONAI rev id: 19a9f0554a9d641716162097d02ec8944c67e0a1\n", + "\n", + "Optional dependencies:\n", + "Pytorch Ignite version: 0.4.2\n", + "Nibabel version: 3.2.1\n", + "scikit-image version: 0.15.0\n", + "Pillow version: 8.1.0\n", + "Tensorboard version: 1.15.0+nv\n", + "gdown version: 3.12.2\n", + "TorchVision version: 0.8.0a0\n", + "ITK version: 5.1.2\n", + "tqdm version: 4.56.0\n", + "lmdb version: 1.0.0\n", + "psutil version: 5.7.2\n", + "\n", + "For details about installing the optional dependencies, please visit:\n", + " https://docs.monai.io/en/latest/installation.html#installing-the-recommended-dependencies\n", + "\n" + ] + } + ], "source": [ "# Copyright 2020 MONAI Consortium\n", "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", @@ -128,7 +157,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -187,9 +216,17 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "/workspace/data/medical/\n" + ] + } + ], "source": [ "directory = os.environ.get(\"MONAI_DATA_DIRECTORY\")\n", "root_dir = tempfile.mkdtemp() if directory is None else directory\n", @@ -205,7 +242,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ @@ -283,9 +320,18 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Loading dataset: 100%|██████████| 208/208 [00:01<00:00, 146.24it/s]\n", + "Loading dataset: 100%|██████████| 52/52 [00:00<00:00, 144.76it/s]\n" + ] + } + ], "source": [ "train_ds = DecathlonDataset(\n", " root_dir=root_dir,\n", @@ -317,9 +363,34 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], "source": [ "for i in range(2):\n", " image, label = val_ds[i][\"image\"], val_ds[i][\"label\"]\n", From 60ea525aed950c022d793b5c49af4929905b7a3a Mon Sep 17 00:00:00 2001 From: Yiheng Wang Date: Fri, 26 Feb 2021 17:52:34 +0800 Subject: [PATCH 4/5] Add py based dynunet pipeline Signed-off-by: Yiheng Wang --- modules/dynunet_pipeline/README.md | 22 ++ .../dynunet_pipeline/commands/run_task04.sh | 12 + modules/dynunet_pipeline/create_datalist.py | 91 +++++ modules/dynunet_pipeline/create_dataset.py | 73 ++++ modules/dynunet_pipeline/create_network.py | 55 +++ modules/dynunet_pipeline/evaluator.py | 177 +++++++++ modules/dynunet_pipeline/inferrer.py | 192 ++++++++++ modules/dynunet_pipeline/task_params.py | 90 +++++ modules/dynunet_pipeline/train.py | 248 +++++++++++++ modules/dynunet_pipeline/trainer.py | 85 +++++ modules/dynunet_pipeline/transforms.py | 340 ++++++++++++++++++ 11 files changed, 1385 insertions(+) create mode 100644 modules/dynunet_pipeline/README.md create mode 100644 modules/dynunet_pipeline/commands/run_task04.sh create mode 100644 modules/dynunet_pipeline/create_datalist.py create mode 100644 modules/dynunet_pipeline/create_dataset.py create mode 100644 modules/dynunet_pipeline/create_network.py create mode 100644 modules/dynunet_pipeline/evaluator.py create mode 100644 modules/dynunet_pipeline/inferrer.py create mode 100644 modules/dynunet_pipeline/task_params.py create mode 100644 modules/dynunet_pipeline/train.py create mode 100644 modules/dynunet_pipeline/trainer.py create mode 100644 modules/dynunet_pipeline/transforms.py diff --git a/modules/dynunet_pipeline/README.md b/modules/dynunet_pipeline/README.md new file mode 100644 index 0000000000..c4f8a33ed8 --- /dev/null +++ b/modules/dynunet_pipeline/README.md @@ -0,0 +1,22 @@ +# Overview +This pipeline is modified from NNUnet [1,2] which wins the "Medical Segmentation Decathlon Challenge 2018" and open sourced from https://github.com/MIC-DKFZ/nnUNet. + +## Data +The source decathlon datasets can be found from http://medicaldecathlon.com/. + +After getting the dataset, please run `create_datalist.py` to get the datalists (please check the command line arguments first). The default seed can help to get the same 5 folds data splits as NNUnet has, and the created datalist will be in `config/` + +## Training +Please run `train.py` for training. Please modify the command line arguments according +to the actual situation. + +A sample training script is shown in `commands/run_task04.sh`, it runs on task 04 and use +fold 0 for validation. You can use `bash commands/run_task04.sh` to run this script. + +## Validation +Please run `train.py` and set the argument `mode` to `val` for validation. + +# References +[1] Isensee F, Jäger P F, Kohl S A A, et al. Automated design of deep learning methods for biomedical image segmentation[J]. arXiv preprint arXiv:1904.08128, 2019. + +[2] Isensee F, Petersen J, Klein A, et al. nnu-net: Self-adapting framework for u-net-based medical image segmentation[J]. arXiv preprint arXiv:1809.10486, 2018. diff --git a/modules/dynunet_pipeline/commands/run_task04.sh b/modules/dynunet_pipeline/commands/run_task04.sh new file mode 100644 index 0000000000..fc2b72c7b8 --- /dev/null +++ b/modules/dynunet_pipeline/commands/run_task04.sh @@ -0,0 +1,12 @@ +# this task requires a single GPU with at least 6GB memory +lr=1e-1 +# train step 1, with large learning rate + +CUDA_VISIBLE_DEVICES=0 python train.py -fold 0 -train_num_workers 4 -interval 1 -num_samples 1 -learning_rate $lr -max_epochs 500 -task_id 04 -pos_sample_num 2 -expr_name baseline -tta_val True + +# train step 2, finetune with small learning rate +# please replace the weight variable into your actual weight + +# lr=1e-3 +# weight=your_output_weightfile.pt +# CUDA_VISIBLE_DEVICES=0 python train_gaussian.py -fold 0 -train_num_workers 4 -interval 1 -num_samples 1 -learning_rate $lr -max_epochs 50 -task_id 04 -pos_sample_num 1 -expr_name baseline -tta_val True -checkpoint $weight \ No newline at end of file diff --git a/modules/dynunet_pipeline/create_datalist.py b/modules/dynunet_pipeline/create_datalist.py new file mode 100644 index 0000000000..92203f2c8a --- /dev/null +++ b/modules/dynunet_pipeline/create_datalist.py @@ -0,0 +1,91 @@ +import json +import os +from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser + +import numpy as np +from sklearn.model_selection import KFold + + +def create_datalist( + dataset_input_dir: str, + output_dir: str, + task_id: str, + num_folds: int, + seed: int, +): + task_name = { + "01": "Task01_BrainTumour", + "02": "Task02_Heart", + "03": "Task03_Liver", + "04": "Task04_Hippocampus", + "05": "Task05_Prostate", + "06": "Task06_Lung", + "07": "Task07_Pancreas", + "08": "Task08_HepaticVessel", + "09": "Task09_Spleen", + "10": "Task10_Colon", + } + + dataset_file_path = os.path.join( + dataset_input_dir, task_name[task_id], "dataset.json" + ) + + with open(dataset_file_path, "r") as f: + dataset = json.load(f) + f.close() + + dataset_with_folds = dataset.copy() + + keys = [line["image"].split("/")[-1].split(".")[0] for line in dataset["training"]] + dataset_train_dict = dict(zip(keys, dataset["training"])) + all_keys_sorted = np.sort(keys) + kfold = KFold(n_splits=num_folds, shuffle=True, random_state=seed) + for i, (train_idx, test_idx) in enumerate(kfold.split(all_keys_sorted)): + val_data = [] + train_data = [] + train_keys = np.array(all_keys_sorted)[train_idx] + test_keys = np.array(all_keys_sorted)[test_idx] + for key in test_keys: + val_data.append(dataset_train_dict[key]) + for key in train_keys: + train_data.append(dataset_train_dict[key]) + + dataset_with_folds["validation_fold{}".format(i)] = val_data + dataset_with_folds["train_fold{}".format(i)] = train_data + del dataset + + if not os.path.exists(output_dir): + os.makedirs(output_dir) + + with open( + os.path.join(output_dir, "dataset_task{}.json".format(task_id)), "w" + ) as f: + json.dump(dataset_with_folds, f) + print("data list for {} has been created!".format(task_name[task_id])) + f.close() + + +if __name__ == "__main__": + parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter) + + parser.add_argument( + "-input_dir", "--input_dir", type=str, default="/workspace/data/medical/" + ) + parser.add_argument("-output_dir", "--output_dir", type=str, default="config/") + parser.add_argument( + "-task_id", "--task_id", type=str, default="04", help="task 01 to 10" + ) + parser.add_argument( + "-num_folds", "--num_folds", type=int, default=5, help="number of folds" + ) + parser.add_argument("-seed", "--seed", type=int, default=12345, help="seed number") + + args = parser.parse_args() + + create_datalist( + dataset_input_dir=args.input_dir, + output_dir=args.output_dir, + task_id=args.task_id, + num_folds=args.num_folds, + seed=args.seed, + ) diff --git a/modules/dynunet_pipeline/create_dataset.py b/modules/dynunet_pipeline/create_dataset.py new file mode 100644 index 0000000000..5b375d43b8 --- /dev/null +++ b/modules/dynunet_pipeline/create_dataset.py @@ -0,0 +1,73 @@ +import os + +from monai.data import ( + CacheDataset, + DataLoader, + load_decathlon_datalist, + load_decathlon_properties, +) + +from task_params import task_name +from transforms import get_task_transforms + + +def get_data(args, batch_size=1, mode="train"): + # get necessary parameters: + fold = args.fold + task_id = args.task_id + root_dir = args.root_dir + datalist_path = args.datalist_path + dataset_path = os.path.join(root_dir, task_name[task_id]) + transform_params = (args.pos_sample_num, args.neg_sample_num, args.num_samples) + + transform = get_task_transforms(mode, task_id, *transform_params) + list_key = "{}_fold{}".format(mode, fold) + datalist_name = "dataset_task{}.json".format(task_id) + + property_keys = [ + "name", + "description", + "reference", + "licence", + "tensorImageSize", + "modality", + "labels", + "numTraining", + "numTest", + ] + + datalist = load_decathlon_datalist( + os.path.join(datalist_path, datalist_name), True, list_key, dataset_path + ) + properties = load_decathlon_properties( + os.path.join(datalist_path, datalist_name), property_keys + ) + if mode == "validation": + val_ds = CacheDataset( + data=datalist, + transform=transform, + num_workers=4, + ) + + val_loader = DataLoader( + val_ds, + batch_size=batch_size, + shuffle=False, + num_workers=args.val_num_workers, + ) + return properties, val_loader + elif mode == "train": + train_ds = CacheDataset( + data=datalist, + transform=transform, + num_workers=8, + cache_rate=args.cache_rate, + ) + train_loader = DataLoader( + train_ds, + batch_size=batch_size, + shuffle=True, + num_workers=args.train_num_workers, + drop_last=True, + ) + return properties, train_loader diff --git a/modules/dynunet_pipeline/create_network.py b/modules/dynunet_pipeline/create_network.py new file mode 100644 index 0000000000..6cdd9df789 --- /dev/null +++ b/modules/dynunet_pipeline/create_network.py @@ -0,0 +1,55 @@ +import os + +import torch +from monai.networks.nets import DynUNet + +from task_params import deep_supr_num, patch_size, spacing + + +def get_kernels_strides(task_id): + sizes, spacings = patch_size[task_id], spacing[task_id] + strides, kernels = [], [] + + while True: + spacing_ratio = [sp / min(spacings) for sp in spacings] + stride = [ + 2 if ratio <= 2 and size >= 8 else 1 + for (ratio, size) in zip(spacing_ratio, sizes) + ] + kernel = [3 if ratio <= 2 else 1 for ratio in spacing_ratio] + if all(s == 1 for s in stride): + break + sizes = [i / j for i, j in zip(sizes, stride)] + spacings = [i * j for i, j in zip(spacings, stride)] + kernels.append(kernel) + strides.append(stride) + strides.insert(0, len(spacings) * [1]) + kernels.append(len(spacings) * [3]) + return kernels, strides + + +def get_network(device, properties, task_id, pretrain_path, checkpoint=None): + n_class = len(properties["labels"]) + in_channels = len(properties["modality"]) + kernels, strides = get_kernels_strides(task_id) + + net = DynUNet( + spatial_dims=3, + in_channels=in_channels, + out_channels=n_class, + kernel_size=kernels, + strides=strides, + upsample_kernel_size=strides[1:], + norm_name="instance", + deep_supervision=True, + deep_supr_num=deep_supr_num[task_id], + ).to(device) + + if checkpoint is not None: + pretrain_path = os.path.join(pretrain_path, checkpoint) + if os.path.exists(pretrain_path): + net.load_state_dict(torch.load(pretrain_path)) + print("pretrained checkpoint: {} loaded".format(pretrain_path)) + else: + print("no pretrained checkpoint") + return net diff --git a/modules/dynunet_pipeline/evaluator.py b/modules/dynunet_pipeline/evaluator.py new file mode 100644 index 0000000000..70ef25e7e9 --- /dev/null +++ b/modules/dynunet_pipeline/evaluator.py @@ -0,0 +1,177 @@ +from typing import Any, Callable, Dict, Optional, Sequence, Tuple, Union + +import torch +import torch.nn as nn +from ignite.engine import Engine +from ignite.metrics import Metric +from monai.engines import SupervisedEvaluator +from monai.engines.utils import CommonKeys as Keys +from monai.engines.utils import IterationEvents, default_prepare_batch +from monai.inferers import Inferer +from monai.networks.utils import eval_mode +from monai.transforms import AsDiscrete, Transform +from torch.utils.data import DataLoader + +from transforms import recovery_prediction + + +class DynUNetEvaluator(SupervisedEvaluator): + """ + This class inherits from SupervisedEvaluator in MONAI, and is used with DynUNet + on Decathlon datasets. + + Args: + device: an object representing the device on which to run. + val_data_loader: Ignite engine use data_loader to run, must be + torch.DataLoader. + network: use the network to run model forward. + n_classes: the number of classes (output channels) for the task. + epoch_length: number of iterations for one epoch, default to + `len(val_data_loader)`. + non_blocking: if True and this copy is between CPU and GPU, the copy may occur asynchronously + with respect to the host. For other cases, this argument has no effect. + prepare_batch: function to parse image and label for current iteration. + iteration_update: the callable function for every iteration, expect to accept `engine` + and `batchdata` as input parameters. if not provided, use `self._iteration()` instead. + inferer: inference method that execute model forward on input data, like: SlidingWindow, etc. + post_transform: execute additional transformation for the model output data. + Typically, several Tensor based transforms composed by `Compose`. + key_val_metric: compute metric when every iteration completed, and save average value to + engine.state.metrics when epoch completed. key_val_metric is the main metric to compare and save the + checkpoint into files. + additional_metrics: more Ignite metrics that also attach to Ignite Engine. + val_handlers: every handler is a set of Ignite Event-Handlers, must have `attach` function, like: + CheckpointHandler, StatsHandler, SegmentationSaver, etc. + amp: whether to enable auto-mixed-precision evaluation, default is False. + tta_val: whether to do the 8 flips (8 = 2 ** 3, where 3 represents the three dimensions) + test time augmentation, default is False. + + """ + + def __init__( + self, + device: torch.device, + val_data_loader: DataLoader, + network: torch.nn.Module, + n_classes: Union[str, int], + epoch_length: Optional[int] = None, + non_blocking: bool = False, + prepare_batch: Callable = default_prepare_batch, + iteration_update: Optional[Callable] = None, + inferer: Optional[Inferer] = None, + post_transform: Optional[Transform] = None, + key_val_metric: Optional[Dict[str, Metric]] = None, + additional_metrics: Optional[Dict[str, Metric]] = None, + val_handlers: Optional[Sequence] = None, + amp: bool = False, + tta_val: bool = False, + ) -> None: + super().__init__( + device=device, + val_data_loader=val_data_loader, + network=network, + epoch_length=epoch_length, + non_blocking=non_blocking, + prepare_batch=prepare_batch, + iteration_update=iteration_update, + inferer=inferer, + post_transform=post_transform, + key_val_metric=key_val_metric, + additional_metrics=additional_metrics, + val_handlers=val_handlers, + amp=amp, + ) + + if not isinstance(n_classes, int): + n_classes = int(n_classes) + self.n_classes = n_classes + self.post_pred = AsDiscrete(argmax=True, to_onehot=True, n_classes=n_classes) + self.post_label = AsDiscrete(to_onehot=True, n_classes=n_classes) + self.tta_val = tta_val + + def _iteration( + self, engine: Engine, batchdata: Dict[str, Any] + ) -> Dict[str, torch.Tensor]: + """ + callback function for the Supervised Evaluation processing logic of 1 iteration in Ignite Engine. + Return below items in a dictionary: + - IMAGE: image Tensor data for model input, already moved to device. + - LABEL: label Tensor data corresponding to the image, already moved to device. + - PRED: prediction result of model. + + Args: + engine: Ignite Engine, it can be a trainer, validator or evaluator. + batchdata: input data for this iteration, usually can be dictionary or tuple of Tensor data. + + Raises: + ValueError: When ``batchdata`` is None. + + """ + if batchdata is None: + raise ValueError("Must provide batch data for current iteration.") + batch = self.prepare_batch(batchdata, engine.state.device, engine.non_blocking) + if len(batch) == 2: + inputs, targets = batch + args: Tuple = () + kwargs: Dict = {} + else: + inputs, targets, args, kwargs = batch + + targets = targets.cpu() + + def _compute_pred(): + ct = 1.0 + pred = self.inferer(inputs, self.network, *args, **kwargs).cpu() + pred = nn.functional.softmax(pred, dim=1) + if not self.tta_val: + return pred + else: + for dims in [[2], [3], [4], (2, 3), (2, 4), (3, 4), (2, 3, 4)]: + flip_inputs = torch.flip(inputs, dims=dims) + flip_pred = torch.flip( + self.inferer(flip_inputs, self.network).cpu(), dims=dims + ) + flip_pred = nn.functional.softmax(flip_pred, dim=1) + del flip_inputs + pred += flip_pred + del flip_pred + ct += 1 + return pred / ct + + # execute forward computation + with eval_mode(self.network): + if self.amp: + with torch.cuda.amp.autocast(): + predictions = _compute_pred() + else: + predictions = _compute_pred() + + inputs = inputs.cpu() + predictions = self.post_pred(predictions) + targets = self.post_label(targets) + + resample_flag = batchdata["resample_flag"] + anisotrophy_flag = batchdata["anisotrophy_flag"] + crop_shape = batchdata["crop_shape"][0].tolist() + original_shape = batchdata["original_shape"][0].tolist() + if resample_flag: + # convert the prediction back to the original (after cropped) shape + predictions = recovery_prediction( + predictions.numpy()[0], [self.n_classes, *crop_shape], anisotrophy_flag + ) + predictions = torch.tensor(predictions) + + # put iteration outputs into engine.state + engine.state.output = output = {Keys.IMAGE: inputs, Keys.LABEL: targets} + output[Keys.PRED] = torch.zeros([1, self.n_classes, *original_shape]) + # pad the prediction back to the original shape + box_start, box_end = batchdata["bbox"][0] + h_start, w_start, d_start = box_start + h_end, w_end, d_end = box_end + output[Keys.PRED][ + 0, :, h_start:h_end, w_start:w_end, d_start:d_end + ] = predictions + del predictions + + engine.fire_event(IterationEvents.FORWARD_COMPLETED) + return output diff --git a/modules/dynunet_pipeline/inferrer.py b/modules/dynunet_pipeline/inferrer.py new file mode 100644 index 0000000000..b9d8ceeb0f --- /dev/null +++ b/modules/dynunet_pipeline/inferrer.py @@ -0,0 +1,192 @@ +import os +from typing import Any, Callable, Dict, Optional, Sequence, Tuple, Union + +import nibabel as nib +import numpy as np +import torch +import torch.nn as nn +from ignite.engine import Engine +from ignite.metrics import Metric +from monai.data.utils import to_affine_nd +from monai.engines import SupervisedEvaluator +from monai.engines.utils import IterationEvents, default_prepare_batch +from monai.inferers import Inferer +from monai.networks.utils import eval_mode +from monai.transforms import AsDiscrete, Transform +from torch.utils.data import DataLoader + +from transforms import recovery_prediction + + +class DynUNetInferrer(SupervisedEvaluator): + """ + This class inherits from SupervisedEvaluator in MONAI, and is used with DynUNet + on Decathlon datasets. + + Args: + device: an object representing the device on which to run. + val_data_loader: Ignite engine use data_loader to run, must be + torch.DataLoader. + network: use the network to run model forward. + output_dir: the path to save inferred outputs. + n_classes: the number of classes (output channels) for the task. + epoch_length: number of iterations for one epoch, default to + `len(val_data_loader)`. + non_blocking: if True and this copy is between CPU and GPU, the copy may occur asynchronously + with respect to the host. For other cases, this argument has no effect. + prepare_batch: function to parse image and label for current iteration. + iteration_update: the callable function for every iteration, expect to accept `engine` + and `batchdata` as input parameters. if not provided, use `self._iteration()` instead. + inferer: inference method that execute model forward on input data, like: SlidingWindow, etc. + post_transform: execute additional transformation for the model output data. + Typically, several Tensor based transforms composed by `Compose`. + key_val_metric: compute metric when every iteration completed, and save average value to + engine.state.metrics when epoch completed. key_val_metric is the main metric to compare and save the + checkpoint into files. + additional_metrics: more Ignite metrics that also attach to Ignite Engine. + val_handlers: every handler is a set of Ignite Event-Handlers, must have `attach` function, like: + CheckpointHandler, StatsHandler, SegmentationSaver, etc. + amp: whether to enable auto-mixed-precision evaluation, default is False. + tta_val: whether to do the 8 flips (8 = 2 ** 3, where 3 represents the three dimensions) + test time augmentation, default is False. + + """ + + def __init__( + self, + device: torch.device, + val_data_loader: DataLoader, + network: torch.nn.Module, + output_dir: str, + n_classes: Union[str, int], + epoch_length: Optional[int] = None, + non_blocking: bool = False, + prepare_batch: Callable = default_prepare_batch, + iteration_update: Optional[Callable] = None, + inferer: Optional[Inferer] = None, + post_transform: Optional[Transform] = None, + key_val_metric: Optional[Dict[str, Metric]] = None, + additional_metrics: Optional[Dict[str, Metric]] = None, + val_handlers: Optional[Sequence] = None, + amp: bool = False, + tta_val: bool = False, + ) -> None: + super().__init__( + device=device, + val_data_loader=val_data_loader, + network=network, + epoch_length=epoch_length, + non_blocking=non_blocking, + prepare_batch=prepare_batch, + iteration_update=iteration_update, + inferer=inferer, + post_transform=post_transform, + key_val_metric=key_val_metric, + additional_metrics=additional_metrics, + val_handlers=val_handlers, + amp=amp, + ) + + if not isinstance(n_classes, int): + n_classes = int(n_classes) + self.post_pred = AsDiscrete(argmax=True, to_onehot=True, n_classes=n_classes) + self.output_dir = output_dir + self.tta_val = tta_val + self.n_classes = n_classes + + def _iteration( + self, engine: Engine, batchdata: Dict[str, Any] + ) -> Dict[str, torch.Tensor]: + """ + callback function for the Supervised Evaluation processing logic of 1 iteration in Ignite Engine. + Return below item in a dictionary: + - PRED: prediction result of model. + + Args: + engine: Ignite Engine, it can be a trainer, validator or evaluator. + batchdata: input data for this iteration, usually can be dictionary or tuple of Tensor data. + + Raises: + ValueError: When ``batchdata`` is None. + + """ + if batchdata is None: + raise ValueError("Must provide batch data for current iteration.") + batch = self.prepare_batch(batchdata, engine.state.device, engine.non_blocking) + if len(batch) == 2: + inputs, _ = batch + args: Tuple = () + kwargs: Dict = {} + else: + inputs, _, args, kwargs = batch + + def _compute_pred(): + ct = 1.0 + pred = self.inferer(inputs, self.network, *args, **kwargs).cpu() + pred = nn.functional.softmax(pred, dim=1) + if not self.tta_val: + return pred + else: + for dims in [[2], [3], [4], (2, 3), (2, 4), (3, 4), (2, 3, 4)]: + flip_inputs = torch.flip(inputs, dims=dims) + flip_pred = torch.flip( + self.inferer(flip_inputs, self.network).cpu(), dims=dims + ) + flip_pred = nn.functional.softmax(flip_pred, dim=1) + del flip_inputs + pred += flip_pred + del flip_pred + ct += 1 + return pred / ct + + # execute forward computation + with eval_mode(self.network): + if self.amp: + with torch.cuda.amp.autocast(): + predictions = _compute_pred() + else: + predictions = _compute_pred() + + inputs = inputs.cpu() + predictions = self.post_pred(predictions) + + target_affine = batchdata["image_meta_dict"]["affine"].numpy()[0] + resample_flag = batchdata["resample_flag"] + anisotrophy_flag = batchdata["anisotrophy_flag"] + crop_shape = batchdata["crop_shape"][0].tolist() + original_shape = batchdata["original_shape"][0].tolist() + + if resample_flag: + # convert the prediction back to the original (after cropped) shape + predictions = recovery_prediction( + predictions.numpy()[0], [self.n_classes, *crop_shape], anisotrophy_flag + ) + else: + predictions = predictions.numpy() + + predictions = predictions[0] + predictions = np.argmax(predictions, axis=0) + + # pad the prediction back to the original shape + predictions_org = np.zeros([*original_shape]) + box_start, box_end = batchdata["bbox"][0] + h_start, w_start, d_start = box_start + h_end, w_end, d_end = box_end + predictions_org[h_start:h_end, w_start:w_end, d_start:d_end] = predictions + del predictions + + filename = batchdata["image_meta_dict"]["filename_or_obj"][0].split("/")[-1] + + print( + "save {} with shape: {}, mean values: {}".format( + filename, predictions_org.shape, predictions_org.mean() + ) + ) + results_img = nib.Nifti1Image( + predictions_org.astype(np.uint8), to_affine_nd(3, target_affine) + ) + del predictions_org + nib.save(results_img, os.path.join(self.output_dir, filename)) + + engine.fire_event(IterationEvents.FORWARD_COMPLETED) + return {"pred": results_img} diff --git a/modules/dynunet_pipeline/task_params.py b/modules/dynunet_pipeline/task_params.py new file mode 100644 index 0000000000..6e23813384 --- /dev/null +++ b/modules/dynunet_pipeline/task_params.py @@ -0,0 +1,90 @@ +task_name = { + "01": "Task01_BrainTumour", + "02": "Task02_Heart", + "03": "Task03_Liver", + "04": "Task04_Hippocampus", + "05": "Task05_Prostate", + "06": "Task06_Lung", + "07": "Task07_Pancreas", + "08": "Task08_HepaticVessel", + "09": "Task09_Spleen", + "10": "Task10_Colon", +} + +patch_size = { + "01": [128, 128, 128], + "02": [160, 192, 80], + "03": [128, 128, 128], + "04": [40, 56, 40], + "05": [320, 256, 20], + "06": [192, 160, 80], + "07": [224, 224, 40], + "08": [192, 192, 64], + "09": [192, 160, 64], + "10": [192, 160, 56], +} + +spacing = { + "01": [1.0, 1.0, 1.0], + "02": [1.25, 1.25, 1.37], + "03": [0.77, 0.77, 1], + "04": [1.0, 1.0, 1.0], + "05": [0.62, 0.62, 3.6], + "06": [0.79, 0.79, 1.24], + "07": [0.8, 0.8, 2.5], + "08": [0.8, 0.8, 1.5], + "09": [0.79, 0.79, 1.6], + "10": [0.78, 0.78, 3], +} + +clip_values = { + "01": [0, 0], + "02": [0, 0], + "03": [-17, 201], + "04": [0, 0], + "05": [0, 0], + "06": [-1024, 325], + "07": [-96, 215], + "08": [-3, 243], + "09": [-41, 176], + "10": [-30, 165.82], +} + +normalize_values = { + "01": [0, 0], + "02": [0, 0], + "03": [99.40, 39.36], + "04": [0, 0], + "05": [0, 0], + "06": [-158.58, 324.7], + "07": [77.99, 75.4], + "08": [104.37, 52.62], + "09": [99.29, 39.47], + "10": [62.18, 32.65], +} + +data_loader_params = { + "01": {"batch_size": 8}, + "02": {"batch_size": 2}, + "03": {"batch_size": 8}, + "04": {"batch_size": 9}, + "05": {"batch_size": 2}, + "06": {"batch_size": 2}, + "07": {"batch_size": 2}, + "08": {"batch_size": 2}, + "09": {"batch_size": 2}, + "10": {"batch_size": 2}, +} + +deep_supr_num = { + "01": 3, + "02": 3, + "03": 3, + "04": 1, + "05": 4, + "06": 3, + "07": 3, + "08": 3, + "09": 3, + "10": 3, +} diff --git a/modules/dynunet_pipeline/train.py b/modules/dynunet_pipeline/train.py new file mode 100644 index 0000000000..60655441f9 --- /dev/null +++ b/modules/dynunet_pipeline/train.py @@ -0,0 +1,248 @@ +import logging +import os +from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser + +import torch +from monai.handlers import (CheckpointSaver, LrScheduleHandler, MeanDice, + StatsHandler, ValidationHandler) +from monai.inferers import SimpleInferer, SlidingWindowInferer +from monai.losses import DiceCELoss + +from task_params import data_loader_params, patch_size +from create_dataset import get_data +from create_network import get_network +from evaluator import DynUNetEvaluator +from trainer import DynUNetTrainer + + +def validation(args): + # load hyper parameters + task_id = args.task_id + sw_batch_size = args.sw_batch_size + tta_val = args.tta_val + window_mode = args.window_mode + eval_overlap = args.eval_overlap + amp = args.amp + + properties, val_loader = get_data(args, mode="validation") + n_classes = len(properties["labels"]) + # produce the network + checkpoint = args.checkpoint + val_output_dir = "./runs_{}_fold{}_{}/".format(task_id, args.fold, args.expr_name) + device = torch.device("cuda:0") + net = get_network(device, properties, task_id, val_output_dir, checkpoint) + + net.eval() + + evaluator = DynUNetEvaluator( + device=device, + val_data_loader=val_loader, + network=net, + n_classes=n_classes, + inferer=SlidingWindowInferer( + roi_size=patch_size[task_id], + sw_batch_size=sw_batch_size, + overlap=eval_overlap, + mode=window_mode, + ), + post_transform=None, + key_val_metric={ + "val_mean_dice": MeanDice( + include_background=False, + output_transform=lambda x: (x["pred"], x["label"]), + ) + }, + additional_metrics=None, + amp=amp, + tta_val=tta_val, + ) + + evaluator.run() + print(evaluator.state.metrics) + results = evaluator.state.metric_details["val_mean_dice"] + if n_classes > 2: + for i in range(n_classes - 1): + print("mean dice for label {} is {}".format(i+1, results[:, i].mean())) + + +def train(args): + # load hyper parameters + task_id = args.task_id + fold = args.fold + val_output_dir = "./runs_{}_fold{}_{}/".format(task_id, fold, args.expr_name) + log_filename = "nnunet_task{}_fold{}.log".format(task_id, fold) + log_filename = os.path.join(val_output_dir, log_filename) + interval = args.interval + learning_rate = args.learning_rate + max_epochs = args.max_epochs + amp_flag = args.amp + lr_decay_flag = args.lr_decay + sw_batch_size = args.sw_batch_size + tta_val = args.tta_val + batch_dice = args.batch_dice + window_mode = args.window_mode + eval_overlap = args.eval_overlap + + # transforms + train_batch_size = data_loader_params[task_id]["batch_size"] + + properties, val_loader = get_data(args, mode="validation") + _, train_loader = get_data(args, batch_size=train_batch_size, mode="train") + + # produce the network + device = torch.device("cuda:0") + checkpoint = args.checkpoint + net = get_network(device, properties, task_id, val_output_dir, checkpoint) + + optimizer = torch.optim.SGD( + net.parameters(), + lr=learning_rate, + momentum=0.99, + weight_decay=3e-5, + nesterov=True, + ) + + scheduler = torch.optim.lr_scheduler.LambdaLR( + optimizer, lr_lambda=lambda epoch: (1 - epoch / max_epochs) ** 0.9 + ) + # produce evaluator + val_handlers = [ + StatsHandler(output_transform=lambda x: None), + CheckpointSaver( + save_dir=val_output_dir, save_dict={"net": net}, save_key_metric=True + ), + ] + + evaluator = DynUNetEvaluator( + device=device, + val_data_loader=val_loader, + network=net, + n_classes=len(properties["labels"]), + inferer=SlidingWindowInferer( + roi_size=patch_size[task_id], + sw_batch_size=sw_batch_size, + overlap=eval_overlap, + mode=window_mode, + ), + post_transform=None, + key_val_metric={ + "val_mean_dice": MeanDice( + include_background=False, + output_transform=lambda x: (x["pred"], x["label"]), + ) + }, + val_handlers=val_handlers, + amp=amp_flag, + tta_val=tta_val, + ) + # produce trainer + loss = DiceCELoss(to_onehot_y=True, softmax=True, batch=batch_dice) + train_handlers = [] + if lr_decay_flag: + train_handlers += [LrScheduleHandler(lr_scheduler=scheduler, print_lr=True)] + + train_handlers += [ + ValidationHandler(validator=evaluator, interval=interval, epoch_level=True), + StatsHandler(tag_name="train_loss", output_transform=lambda x: x["loss"]), + ] + + trainer = DynUNetTrainer( + device=device, + max_epochs=max_epochs, + train_data_loader=train_loader, + network=net, + optimizer=optimizer, + loss_function=loss, + inferer=SimpleInferer(), + post_transform=None, + key_train_metric=None, + train_handlers=train_handlers, + amp=amp_flag, + ) + + # run + logger = logging.getLogger() + + formatter = logging.Formatter( + "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + ) + + # Setup file handler + fhandler = logging.FileHandler(log_filename) + fhandler.setLevel(logging.INFO) + fhandler.setFormatter(formatter) + + # Configure stream handler for the cells + chandler = logging.StreamHandler() + chandler.setLevel(logging.INFO) + chandler.setFormatter(formatter) + + # Add both handlers + logger.addHandler(fhandler) + logger.addHandler(chandler) + logger.setLevel(logging.INFO) + + # Show the handlers + logger.handlers + + # Log Something + logger.info("Test info") + logger.debug("Test debug") + + trainer.run() + + +if __name__ == "__main__": + parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter) + parser.add_argument("-fold", "--fold", type=int, default=0, help="0-5") + parser.add_argument( + "-task_id", "--task_id", type=str, default="02", help="task 01 to 10" + ) + parser.add_argument( + "-root_dir", + "--root_dir", + type=str, + default="/workspace/data/medical/", + help="dataset path", + ) + parser.add_argument("-expr_name", "--expr_name", type=str, default="expr", help="the suffix of the experiment's folder") + parser.add_argument( + "-datalist_path", "--datalist_path", type=str, default="config/", + ) + parser.add_argument( + "-train_num_workers", "--train_num_workers", type=int, default=4, help="the num_workers parameter of training dataloader." + ) + parser.add_argument("-val_num_workers", "--val_num_workers", type=int, default=1, help="the num_workers parameter of validation dataloader.") + parser.add_argument("-interval", "--interval", type=int, default=5, help="the validation interval under epoch level.") + parser.add_argument("-eval_overlap", "--eval_overlap", type=float, default=0.5, help="the overlap parameter of SlidingWindowInferer.") + parser.add_argument("-sw_batch_size", "--sw_batch_size", type=int, default=4, help="the sw_batch_size parameter of SlidingWindowInferer.") + parser.add_argument( + "-window_mode", + "--window_mode", + type=str, + default="gaussian", + choices=["constant", "gaussian"], + help="the mode parameter for SlidingWindowInferer." + ) + parser.add_argument("-num_samples", "--num_samples", type=int, default=3, help="the num_samples parameter of RandCropByPosNegLabeld.") + parser.add_argument("-pos_sample_num", "--pos_sample_num", type=int, default=1, help="the pos parameter of RandCropByPosNegLabeld.") + parser.add_argument("-neg_sample_num", "--neg_sample_num", type=int, default=1, help="the neg parameter of RandCropByPosNegLabeld.") + parser.add_argument("-cache_rate", "--cache_rate", type=float, default=1.0, help="the cache_rate parameter of CacheDataset.") + parser.add_argument("-learning_rate", "--learning_rate", type=float, default=1e-2) + parser.add_argument("-max_epochs", "--max_epochs", type=int, default=1000, help="number of epochs of training.") + parser.add_argument( + "-mode", "--mode", type=str, default="train", choices=["train", "val"] + ) + parser.add_argument("-checkpoint", "--checkpoint", type=str, default=None, help="the filename of weights.") + parser.add_argument("-amp", "--amp", type=bool, default=False, help="whether to use automatic mixed precision.") + parser.add_argument("-lr_decay", "--lr_decay", type=bool, default=False, help="whether to use learning rate decay.") + parser.add_argument("-tta_val", "--tta_val", type=bool, default=False, help="whether to use test time augmentation.") + parser.add_argument("-batch_dice", "--batch_dice", type=bool, default=False, help="the batch parameter of DiceCELoss.") + + args = parser.parse_args() + + if args.mode == "train": + train(args) + elif args.mode == "val": + validation(args) + \ No newline at end of file diff --git a/modules/dynunet_pipeline/trainer.py b/modules/dynunet_pipeline/trainer.py new file mode 100644 index 0000000000..ac45ae994a --- /dev/null +++ b/modules/dynunet_pipeline/trainer.py @@ -0,0 +1,85 @@ +from typing import Any, Dict, Tuple + +import torch +from ignite.engine import Engine +from monai.engines import SupervisedTrainer +from monai.engines.utils import CommonKeys as Keys +from monai.engines.utils import IterationEvents +from torch.nn.parallel import DistributedDataParallel + + +class DynUNetTrainer(SupervisedTrainer): + """ + This class inherits from SupervisedTrainer in MONAI, and is used with DynUNet + on Decathlon datasets. + + """ + + def _iteration(self, engine: Engine, batchdata: Dict[str, Any]): + """ + Callback function for the Supervised Training processing logic of 1 iteration in Ignite Engine. + Return below items in a dictionary: + - IMAGE: image Tensor data for model input, already moved to device. + - LABEL: label Tensor data corresponding to the image, already moved to device. + - PRED: prediction result of model. + - LOSS: loss value computed by loss function. + + Args: + engine: Ignite Engine, it can be a trainer, validator or evaluator. + batchdata: input data for this iteration, usually can be dictionary or tuple of Tensor data. + + Raises: + ValueError: When ``batchdata`` is None. + + """ + if batchdata is None: + raise ValueError("Must provide batch data for current iteration.") + batch = self.prepare_batch(batchdata, engine.state.device, engine.non_blocking) + if len(batch) == 2: + inputs, targets = batch + args: Tuple = () + kwargs: Dict = {} + else: + inputs, targets, args, kwargs = batch + # put iteration outputs into engine.state + engine.state.output = output = {Keys.IMAGE: inputs, Keys.LABEL: targets} + + def _compute_pred_loss(): + preds = self.inferer(inputs, self.network, *args, **kwargs) + if len(preds.size()) - len(targets.size()) == 1: + # deep supervision mode, need to unbind feature maps first. + preds = torch.unbind(preds, dim=1) + output[Keys.PRED] = preds + del preds + engine.fire_event(IterationEvents.FORWARD_COMPLETED) + output[Keys.LOSS] = sum( + 0.5 ** i * self.loss_function.forward(p, targets) + for i, p in enumerate(output[Keys.PRED]) + ) + engine.fire_event(IterationEvents.LOSS_COMPLETED) + + self.network.train() + self.optimizer.zero_grad() + if self.amp and self.scaler is not None: + with torch.cuda.amp.autocast(): + _compute_pred_loss() + self.scaler.scale(output[Keys.LOSS]).backward() + self.scaler.unscale_(self.optimizer) + if isinstance(self.network, DistributedDataParallel): + torch.nn.utils.clip_grad_norm_(self.network.module.parameters(), 12) + else: + torch.nn.utils.clip_grad_norm_(self.network.parameters(), 12) + self.scaler.step(self.optimizer) + self.scaler.update() + else: + _compute_pred_loss() + output[Keys.LOSS].backward() + engine.fire_event(IterationEvents.BACKWARD_COMPLETED) + if isinstance(self.network, DistributedDataParallel): + torch.nn.utils.clip_grad_norm_(self.network.module.parameters(), 12) + else: + torch.nn.utils.clip_grad_norm_(self.network.parameters(), 12) + self.optimizer.step() + engine.fire_event(IterationEvents.OPTIMIZER_COMPLETED) + + return output diff --git a/modules/dynunet_pipeline/transforms.py b/modules/dynunet_pipeline/transforms.py new file mode 100644 index 0000000000..34f26a80d7 --- /dev/null +++ b/modules/dynunet_pipeline/transforms.py @@ -0,0 +1,340 @@ +import numpy as np +from monai.transforms import ( + AddChanneld, + AsChannelFirstd, + CastToTyped, + Compose, + CropForegroundd, + LoadImaged, + NormalizeIntensity, + RandCropByPosNegLabeld, + RandFlipd, + RandGaussianNoised, + RandGaussianSmoothd, + RandScaleIntensityd, + RandZoomd, + SpatialCrop, + SpatialPadd, + ToTensord, +) +from monai.transforms.compose import MapTransform +from monai.transforms.utils import generate_spatial_bounding_box +from skimage.transform import resize + +from task_params import clip_values, normalize_values, patch_size, spacing + + +def get_task_transforms(mode, task_id, pos_sample_num, neg_sample_num, num_samples): + + # 1. loading for different formats + if mode != "test": + load_keys = ["image", "label"] + else: + load_keys = ["image"] + + if task_id in ["01", "05"]: + load_transforms = [ + LoadImaged(keys=load_keys), + AsChannelFirstd(keys="image"), + ] + if mode != "test": + load_transforms.append(AddChanneld(keys=["label"])) + else: + load_transforms = [ + LoadImaged(keys=load_keys), + AddChanneld(keys=load_keys), + ] + # 2. sampling + if mode != "test": + sample_keys = ["image", "label"] + else: + sample_keys = ["image"] + sample_transforms = [ + PreprocessAnisotropic( + keys=sample_keys, + clip_values=clip_values[task_id], + pixdim=spacing[task_id], + normalize_values=normalize_values[task_id], + model_mode=mode, + ), + ] + # 3. spatial transforms + if mode == "train": + other_transforms = [ + SpatialPadd(keys=["image", "label"], spatial_size=patch_size[task_id]), + RandCropByPosNegLabeld( + keys=["image", "label"], + label_key="label", + spatial_size=patch_size[task_id], + pos=pos_sample_num, + neg=neg_sample_num, + num_samples=num_samples, + image_key="image", + image_threshold=0, + ), + RandZoomd( + keys=["image", "label"], + min_zoom=0.9, + max_zoom=1.2, + mode=("trilinear", "nearest"), + align_corners=(True, None), + prob=0.15, + ), + RandGaussianNoised(keys=["image"], std=0.01, prob=0.15), + RandGaussianSmoothd( + keys=["image"], + sigma_x=(0.5, 1.15), + sigma_y=(0.5, 1.15), + sigma_z=(0.5, 1.15), + prob=0.15, + ), + RandScaleIntensityd(keys=["image"], factors=0.3, prob=0.15), + RandFlipd(["image", "label"], spatial_axis=[0], prob=0.5), + RandFlipd(["image", "label"], spatial_axis=[1], prob=0.5), + RandFlipd(["image", "label"], spatial_axis=[2], prob=0.5), + CastToTyped(keys=["image", "label"], dtype=(np.float32, np.uint8)), + ToTensord(keys=["image", "label"]), + ] + elif mode == "validation": + other_transforms = [ + CastToTyped(keys=["image", "label"], dtype=(np.float32, np.uint8)), + ToTensord(keys=["image", "label"]), + ] + else: + other_transforms = [ + CastToTyped(keys=["image"], dtype=(np.float32)), + ToTensord(keys=["image"]), + ] + + all_transforms = load_transforms + sample_transforms + other_transforms + return Compose(all_transforms) + + +def resample_image(image, shape, anisotrophy_flag): + resized_channels = [] + if anisotrophy_flag: + for image_c in image: + resized_slices = [] + for i in range(image_c.shape[-1]): + image_c_2d_slice = image_c[:, :, i] + image_c_2d_slice = resize( + image_c_2d_slice, + shape[:-1], + order=3, + mode="edge", + cval=0, + clip=True, + anti_aliasing=False, + ) + resized_slices.append(image_c_2d_slice) + resized = np.stack(resized_slices, axis=-1) + resized = resize( + resized, + shape, + order=0, + mode="constant", + cval=0, + clip=True, + anti_aliasing=False, + ) + resized_channels.append(resized) + else: + for image_c in image: + resized = resize( + image_c, + shape, + order=3, + mode="edge", + cval=0, + clip=True, + anti_aliasing=False, + ) + resized_channels.append(resized) + resized = np.stack(resized_channels, axis=0) + return resized + + +def resample_label(label, shape, anisotrophy_flag): + reshaped = np.zeros(shape, dtype=np.uint8) + n_class = np.max(label) + if anisotrophy_flag: + shape_2d = shape[:-1] + depth = label.shape[-1] + reshaped_2d = np.zeros((*shape_2d, depth), dtype=np.uint8) + + for class_ in range(1, int(n_class) + 1): + for depth_ in range(depth): + mask = label[0, :, :, depth_] == class_ + resized_2d = resize( + mask.astype(float), + shape_2d, + order=1, + mode="edge", + cval=0, + clip=True, + anti_aliasing=False, + ) + reshaped_2d[:, :, depth_][resized_2d >= 0.5] = class_ + for class_ in range(1, int(n_class) + 1): + mask = reshaped_2d == class_ + resized = resize( + mask.astype(float), + shape, + order=0, + mode="constant", + cval=0, + clip=True, + anti_aliasing=False, + ) + reshaped[resized >= 0.5] = class_ + else: + for class_ in range(1, int(n_class) + 1): + mask = label[0] == class_ + resized = resize( + mask.astype(float), + shape, + order=1, + mode="edge", + cval=0, + clip=True, + anti_aliasing=False, + ) + reshaped[resized >= 0.5] = class_ + + reshaped = np.expand_dims(reshaped, 0) + return reshaped + + +def recovery_prediction(prediction, shape, anisotrophy_flag): + reshaped = np.zeros(shape, dtype=np.uint8) + n_class = shape[0] + if anisotrophy_flag: + c, h, w = prediction.shape[:-1] + d = shape[-1] + reshaped_d = np.zeros((c, h, w, d), dtype=np.uint8) + for class_ in range(1, n_class): + mask = prediction[class_] == 1 + resized_d = resize( + mask.astype(float), + (h, w, d), + order=0, + mode="constant", + cval=0, + clip=True, + anti_aliasing=False, + ) + reshaped_d[class_][resized_d >= 0.5] = 1 + + for class_ in range(1, n_class): + for depth_ in range(d): + mask = reshaped_d[class_, :, :, depth_] == 1 + resized_hw = resize( + mask.astype(float), + shape[1:-1], + order=1, + mode="edge", + cval=0, + clip=True, + anti_aliasing=False, + ) + reshaped[class_, :, :, depth_][resized_hw >= 0.5] = 1 + else: + for class_ in range(1, n_class): + mask = prediction[class_] == 1 + resized = resize( + mask.astype(float), + shape[1:], + order=1, + mode="edge", + cval=0, + clip=True, + anti_aliasing=False, + ) + reshaped[class_][resized >= 0.5] = 1 + + reshaped = np.expand_dims(reshaped, 0) + return reshaped + + +class PreprocessAnisotropic(MapTransform): + def __init__( + self, + keys, + clip_values, + pixdim, + normalize_values, + model_mode, + ) -> None: + super().__init__(keys) + self.keys = keys + self.low = clip_values[0] + self.high = clip_values[1] + self.target_spacing = pixdim + self.mean = normalize_values[0] + self.std = normalize_values[1] + self.training = False + self.crop_foreg = CropForegroundd(keys=["image", "label"], source_key="image") + self.normalize_intensity = NormalizeIntensity(nonzero=True, channel_wise=True) + if model_mode in ["train"]: + self.training = True + + def calculate_new_shape(self, spacing, shape): + spacing_ratio = np.array(spacing) / np.array(self.target_spacing) + new_shape = (spacing_ratio * np.array(shape)).astype(int).tolist() + return new_shape + + def check_anisotrophy(self, spacing): + def check(spacing): + return np.max(spacing) / np.min(spacing) >= 3 + + return check(spacing) or check(self.target_spacing) + + def __call__(self, data): + # load data + d = dict(data) + image = d["image"] + image_spacings = d["image_meta_dict"]["pixdim"][1:4].tolist() + + if "label" in self.keys: + label = d["label"] + label[label < 0] = 0 + + if self.training: + # only task 04 does not be impacted + cropped_data = self.crop_foreg({"image": image, "label": label}) + image, label = cropped_data["image"], cropped_data["label"] + else: + d["original_shape"] = np.array(image.shape[1:]) + box_start, box_end = generate_spatial_bounding_box(image) + image = SpatialCrop(roi_start=box_start, roi_end=box_end)(image) + d["bbox"] = np.vstack([box_start, box_end]) + d["crop_shape"] = np.array(image.shape[1:]) + + original_shape = image.shape[1:] + # calculate shape + resample_flag = False + anisotrophy_flag = False + if self.target_spacing != image_spacings: + # resample + resample_flag = True + resample_shape = self.calculate_new_shape(image_spacings, original_shape) + anisotrophy_flag = self.check_anisotrophy(image_spacings) + image = resample_image(image, resample_shape, anisotrophy_flag) + if self.training: + label = resample_label(label, resample_shape, anisotrophy_flag) + + d["resample_flag"] = resample_flag + d["anisotrophy_flag"] = anisotrophy_flag + # clip image for CT dataset + if self.low != 0 or self.high != 0: + image = np.clip(image, self.low, self.high) + image = (image - self.mean) / self.std + else: + image = self.normalize_intensity(image.copy()) + + d["image"] = image + + if "label" in self.keys: + d["label"] = label + + return d From a068750b1d6eda61916da7e25170c10db1672cd8 Mon Sep 17 00:00:00 2001 From: Yiheng Wang Date: Fri, 26 Feb 2021 22:24:12 +0800 Subject: [PATCH 5/5] Add comments for the deep supervision changes --- modules/dynunet_pipeline/README.md | 22 -- .../dynunet_pipeline/commands/run_task04.sh | 12 - modules/dynunet_pipeline/create_datalist.py | 91 ----- modules/dynunet_pipeline/create_dataset.py | 73 ---- modules/dynunet_pipeline/create_network.py | 55 --- modules/dynunet_pipeline/evaluator.py | 177 --------- modules/dynunet_pipeline/inferrer.py | 192 ---------- modules/dynunet_pipeline/task_params.py | 90 ----- modules/dynunet_pipeline/train.py | 248 ------------- modules/dynunet_pipeline/trainer.py | 85 ----- modules/dynunet_pipeline/transforms.py | 340 ------------------ modules/dynunet_tutorial.ipynb | 47 +-- 12 files changed, 25 insertions(+), 1407 deletions(-) delete mode 100644 modules/dynunet_pipeline/README.md delete mode 100644 modules/dynunet_pipeline/commands/run_task04.sh delete mode 100644 modules/dynunet_pipeline/create_datalist.py delete mode 100644 modules/dynunet_pipeline/create_dataset.py delete mode 100644 modules/dynunet_pipeline/create_network.py delete mode 100644 modules/dynunet_pipeline/evaluator.py delete mode 100644 modules/dynunet_pipeline/inferrer.py delete mode 100644 modules/dynunet_pipeline/task_params.py delete mode 100644 modules/dynunet_pipeline/train.py delete mode 100644 modules/dynunet_pipeline/trainer.py delete mode 100644 modules/dynunet_pipeline/transforms.py diff --git a/modules/dynunet_pipeline/README.md b/modules/dynunet_pipeline/README.md deleted file mode 100644 index c4f8a33ed8..0000000000 --- a/modules/dynunet_pipeline/README.md +++ /dev/null @@ -1,22 +0,0 @@ -# Overview -This pipeline is modified from NNUnet [1,2] which wins the "Medical Segmentation Decathlon Challenge 2018" and open sourced from https://github.com/MIC-DKFZ/nnUNet. - -## Data -The source decathlon datasets can be found from http://medicaldecathlon.com/. - -After getting the dataset, please run `create_datalist.py` to get the datalists (please check the command line arguments first). The default seed can help to get the same 5 folds data splits as NNUnet has, and the created datalist will be in `config/` - -## Training -Please run `train.py` for training. Please modify the command line arguments according -to the actual situation. - -A sample training script is shown in `commands/run_task04.sh`, it runs on task 04 and use -fold 0 for validation. You can use `bash commands/run_task04.sh` to run this script. - -## Validation -Please run `train.py` and set the argument `mode` to `val` for validation. - -# References -[1] Isensee F, Jäger P F, Kohl S A A, et al. Automated design of deep learning methods for biomedical image segmentation[J]. arXiv preprint arXiv:1904.08128, 2019. - -[2] Isensee F, Petersen J, Klein A, et al. nnu-net: Self-adapting framework for u-net-based medical image segmentation[J]. arXiv preprint arXiv:1809.10486, 2018. diff --git a/modules/dynunet_pipeline/commands/run_task04.sh b/modules/dynunet_pipeline/commands/run_task04.sh deleted file mode 100644 index fc2b72c7b8..0000000000 --- a/modules/dynunet_pipeline/commands/run_task04.sh +++ /dev/null @@ -1,12 +0,0 @@ -# this task requires a single GPU with at least 6GB memory -lr=1e-1 -# train step 1, with large learning rate - -CUDA_VISIBLE_DEVICES=0 python train.py -fold 0 -train_num_workers 4 -interval 1 -num_samples 1 -learning_rate $lr -max_epochs 500 -task_id 04 -pos_sample_num 2 -expr_name baseline -tta_val True - -# train step 2, finetune with small learning rate -# please replace the weight variable into your actual weight - -# lr=1e-3 -# weight=your_output_weightfile.pt -# CUDA_VISIBLE_DEVICES=0 python train_gaussian.py -fold 0 -train_num_workers 4 -interval 1 -num_samples 1 -learning_rate $lr -max_epochs 50 -task_id 04 -pos_sample_num 1 -expr_name baseline -tta_val True -checkpoint $weight \ No newline at end of file diff --git a/modules/dynunet_pipeline/create_datalist.py b/modules/dynunet_pipeline/create_datalist.py deleted file mode 100644 index 92203f2c8a..0000000000 --- a/modules/dynunet_pipeline/create_datalist.py +++ /dev/null @@ -1,91 +0,0 @@ -import json -import os -from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser - -import numpy as np -from sklearn.model_selection import KFold - - -def create_datalist( - dataset_input_dir: str, - output_dir: str, - task_id: str, - num_folds: int, - seed: int, -): - task_name = { - "01": "Task01_BrainTumour", - "02": "Task02_Heart", - "03": "Task03_Liver", - "04": "Task04_Hippocampus", - "05": "Task05_Prostate", - "06": "Task06_Lung", - "07": "Task07_Pancreas", - "08": "Task08_HepaticVessel", - "09": "Task09_Spleen", - "10": "Task10_Colon", - } - - dataset_file_path = os.path.join( - dataset_input_dir, task_name[task_id], "dataset.json" - ) - - with open(dataset_file_path, "r") as f: - dataset = json.load(f) - f.close() - - dataset_with_folds = dataset.copy() - - keys = [line["image"].split("/")[-1].split(".")[0] for line in dataset["training"]] - dataset_train_dict = dict(zip(keys, dataset["training"])) - all_keys_sorted = np.sort(keys) - kfold = KFold(n_splits=num_folds, shuffle=True, random_state=seed) - for i, (train_idx, test_idx) in enumerate(kfold.split(all_keys_sorted)): - val_data = [] - train_data = [] - train_keys = np.array(all_keys_sorted)[train_idx] - test_keys = np.array(all_keys_sorted)[test_idx] - for key in test_keys: - val_data.append(dataset_train_dict[key]) - for key in train_keys: - train_data.append(dataset_train_dict[key]) - - dataset_with_folds["validation_fold{}".format(i)] = val_data - dataset_with_folds["train_fold{}".format(i)] = train_data - del dataset - - if not os.path.exists(output_dir): - os.makedirs(output_dir) - - with open( - os.path.join(output_dir, "dataset_task{}.json".format(task_id)), "w" - ) as f: - json.dump(dataset_with_folds, f) - print("data list for {} has been created!".format(task_name[task_id])) - f.close() - - -if __name__ == "__main__": - parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter) - - parser.add_argument( - "-input_dir", "--input_dir", type=str, default="/workspace/data/medical/" - ) - parser.add_argument("-output_dir", "--output_dir", type=str, default="config/") - parser.add_argument( - "-task_id", "--task_id", type=str, default="04", help="task 01 to 10" - ) - parser.add_argument( - "-num_folds", "--num_folds", type=int, default=5, help="number of folds" - ) - parser.add_argument("-seed", "--seed", type=int, default=12345, help="seed number") - - args = parser.parse_args() - - create_datalist( - dataset_input_dir=args.input_dir, - output_dir=args.output_dir, - task_id=args.task_id, - num_folds=args.num_folds, - seed=args.seed, - ) diff --git a/modules/dynunet_pipeline/create_dataset.py b/modules/dynunet_pipeline/create_dataset.py deleted file mode 100644 index 5b375d43b8..0000000000 --- a/modules/dynunet_pipeline/create_dataset.py +++ /dev/null @@ -1,73 +0,0 @@ -import os - -from monai.data import ( - CacheDataset, - DataLoader, - load_decathlon_datalist, - load_decathlon_properties, -) - -from task_params import task_name -from transforms import get_task_transforms - - -def get_data(args, batch_size=1, mode="train"): - # get necessary parameters: - fold = args.fold - task_id = args.task_id - root_dir = args.root_dir - datalist_path = args.datalist_path - dataset_path = os.path.join(root_dir, task_name[task_id]) - transform_params = (args.pos_sample_num, args.neg_sample_num, args.num_samples) - - transform = get_task_transforms(mode, task_id, *transform_params) - list_key = "{}_fold{}".format(mode, fold) - datalist_name = "dataset_task{}.json".format(task_id) - - property_keys = [ - "name", - "description", - "reference", - "licence", - "tensorImageSize", - "modality", - "labels", - "numTraining", - "numTest", - ] - - datalist = load_decathlon_datalist( - os.path.join(datalist_path, datalist_name), True, list_key, dataset_path - ) - properties = load_decathlon_properties( - os.path.join(datalist_path, datalist_name), property_keys - ) - if mode == "validation": - val_ds = CacheDataset( - data=datalist, - transform=transform, - num_workers=4, - ) - - val_loader = DataLoader( - val_ds, - batch_size=batch_size, - shuffle=False, - num_workers=args.val_num_workers, - ) - return properties, val_loader - elif mode == "train": - train_ds = CacheDataset( - data=datalist, - transform=transform, - num_workers=8, - cache_rate=args.cache_rate, - ) - train_loader = DataLoader( - train_ds, - batch_size=batch_size, - shuffle=True, - num_workers=args.train_num_workers, - drop_last=True, - ) - return properties, train_loader diff --git a/modules/dynunet_pipeline/create_network.py b/modules/dynunet_pipeline/create_network.py deleted file mode 100644 index 6cdd9df789..0000000000 --- a/modules/dynunet_pipeline/create_network.py +++ /dev/null @@ -1,55 +0,0 @@ -import os - -import torch -from monai.networks.nets import DynUNet - -from task_params import deep_supr_num, patch_size, spacing - - -def get_kernels_strides(task_id): - sizes, spacings = patch_size[task_id], spacing[task_id] - strides, kernels = [], [] - - while True: - spacing_ratio = [sp / min(spacings) for sp in spacings] - stride = [ - 2 if ratio <= 2 and size >= 8 else 1 - for (ratio, size) in zip(spacing_ratio, sizes) - ] - kernel = [3 if ratio <= 2 else 1 for ratio in spacing_ratio] - if all(s == 1 for s in stride): - break - sizes = [i / j for i, j in zip(sizes, stride)] - spacings = [i * j for i, j in zip(spacings, stride)] - kernels.append(kernel) - strides.append(stride) - strides.insert(0, len(spacings) * [1]) - kernels.append(len(spacings) * [3]) - return kernels, strides - - -def get_network(device, properties, task_id, pretrain_path, checkpoint=None): - n_class = len(properties["labels"]) - in_channels = len(properties["modality"]) - kernels, strides = get_kernels_strides(task_id) - - net = DynUNet( - spatial_dims=3, - in_channels=in_channels, - out_channels=n_class, - kernel_size=kernels, - strides=strides, - upsample_kernel_size=strides[1:], - norm_name="instance", - deep_supervision=True, - deep_supr_num=deep_supr_num[task_id], - ).to(device) - - if checkpoint is not None: - pretrain_path = os.path.join(pretrain_path, checkpoint) - if os.path.exists(pretrain_path): - net.load_state_dict(torch.load(pretrain_path)) - print("pretrained checkpoint: {} loaded".format(pretrain_path)) - else: - print("no pretrained checkpoint") - return net diff --git a/modules/dynunet_pipeline/evaluator.py b/modules/dynunet_pipeline/evaluator.py deleted file mode 100644 index 70ef25e7e9..0000000000 --- a/modules/dynunet_pipeline/evaluator.py +++ /dev/null @@ -1,177 +0,0 @@ -from typing import Any, Callable, Dict, Optional, Sequence, Tuple, Union - -import torch -import torch.nn as nn -from ignite.engine import Engine -from ignite.metrics import Metric -from monai.engines import SupervisedEvaluator -from monai.engines.utils import CommonKeys as Keys -from monai.engines.utils import IterationEvents, default_prepare_batch -from monai.inferers import Inferer -from monai.networks.utils import eval_mode -from monai.transforms import AsDiscrete, Transform -from torch.utils.data import DataLoader - -from transforms import recovery_prediction - - -class DynUNetEvaluator(SupervisedEvaluator): - """ - This class inherits from SupervisedEvaluator in MONAI, and is used with DynUNet - on Decathlon datasets. - - Args: - device: an object representing the device on which to run. - val_data_loader: Ignite engine use data_loader to run, must be - torch.DataLoader. - network: use the network to run model forward. - n_classes: the number of classes (output channels) for the task. - epoch_length: number of iterations for one epoch, default to - `len(val_data_loader)`. - non_blocking: if True and this copy is between CPU and GPU, the copy may occur asynchronously - with respect to the host. For other cases, this argument has no effect. - prepare_batch: function to parse image and label for current iteration. - iteration_update: the callable function for every iteration, expect to accept `engine` - and `batchdata` as input parameters. if not provided, use `self._iteration()` instead. - inferer: inference method that execute model forward on input data, like: SlidingWindow, etc. - post_transform: execute additional transformation for the model output data. - Typically, several Tensor based transforms composed by `Compose`. - key_val_metric: compute metric when every iteration completed, and save average value to - engine.state.metrics when epoch completed. key_val_metric is the main metric to compare and save the - checkpoint into files. - additional_metrics: more Ignite metrics that also attach to Ignite Engine. - val_handlers: every handler is a set of Ignite Event-Handlers, must have `attach` function, like: - CheckpointHandler, StatsHandler, SegmentationSaver, etc. - amp: whether to enable auto-mixed-precision evaluation, default is False. - tta_val: whether to do the 8 flips (8 = 2 ** 3, where 3 represents the three dimensions) - test time augmentation, default is False. - - """ - - def __init__( - self, - device: torch.device, - val_data_loader: DataLoader, - network: torch.nn.Module, - n_classes: Union[str, int], - epoch_length: Optional[int] = None, - non_blocking: bool = False, - prepare_batch: Callable = default_prepare_batch, - iteration_update: Optional[Callable] = None, - inferer: Optional[Inferer] = None, - post_transform: Optional[Transform] = None, - key_val_metric: Optional[Dict[str, Metric]] = None, - additional_metrics: Optional[Dict[str, Metric]] = None, - val_handlers: Optional[Sequence] = None, - amp: bool = False, - tta_val: bool = False, - ) -> None: - super().__init__( - device=device, - val_data_loader=val_data_loader, - network=network, - epoch_length=epoch_length, - non_blocking=non_blocking, - prepare_batch=prepare_batch, - iteration_update=iteration_update, - inferer=inferer, - post_transform=post_transform, - key_val_metric=key_val_metric, - additional_metrics=additional_metrics, - val_handlers=val_handlers, - amp=amp, - ) - - if not isinstance(n_classes, int): - n_classes = int(n_classes) - self.n_classes = n_classes - self.post_pred = AsDiscrete(argmax=True, to_onehot=True, n_classes=n_classes) - self.post_label = AsDiscrete(to_onehot=True, n_classes=n_classes) - self.tta_val = tta_val - - def _iteration( - self, engine: Engine, batchdata: Dict[str, Any] - ) -> Dict[str, torch.Tensor]: - """ - callback function for the Supervised Evaluation processing logic of 1 iteration in Ignite Engine. - Return below items in a dictionary: - - IMAGE: image Tensor data for model input, already moved to device. - - LABEL: label Tensor data corresponding to the image, already moved to device. - - PRED: prediction result of model. - - Args: - engine: Ignite Engine, it can be a trainer, validator or evaluator. - batchdata: input data for this iteration, usually can be dictionary or tuple of Tensor data. - - Raises: - ValueError: When ``batchdata`` is None. - - """ - if batchdata is None: - raise ValueError("Must provide batch data for current iteration.") - batch = self.prepare_batch(batchdata, engine.state.device, engine.non_blocking) - if len(batch) == 2: - inputs, targets = batch - args: Tuple = () - kwargs: Dict = {} - else: - inputs, targets, args, kwargs = batch - - targets = targets.cpu() - - def _compute_pred(): - ct = 1.0 - pred = self.inferer(inputs, self.network, *args, **kwargs).cpu() - pred = nn.functional.softmax(pred, dim=1) - if not self.tta_val: - return pred - else: - for dims in [[2], [3], [4], (2, 3), (2, 4), (3, 4), (2, 3, 4)]: - flip_inputs = torch.flip(inputs, dims=dims) - flip_pred = torch.flip( - self.inferer(flip_inputs, self.network).cpu(), dims=dims - ) - flip_pred = nn.functional.softmax(flip_pred, dim=1) - del flip_inputs - pred += flip_pred - del flip_pred - ct += 1 - return pred / ct - - # execute forward computation - with eval_mode(self.network): - if self.amp: - with torch.cuda.amp.autocast(): - predictions = _compute_pred() - else: - predictions = _compute_pred() - - inputs = inputs.cpu() - predictions = self.post_pred(predictions) - targets = self.post_label(targets) - - resample_flag = batchdata["resample_flag"] - anisotrophy_flag = batchdata["anisotrophy_flag"] - crop_shape = batchdata["crop_shape"][0].tolist() - original_shape = batchdata["original_shape"][0].tolist() - if resample_flag: - # convert the prediction back to the original (after cropped) shape - predictions = recovery_prediction( - predictions.numpy()[0], [self.n_classes, *crop_shape], anisotrophy_flag - ) - predictions = torch.tensor(predictions) - - # put iteration outputs into engine.state - engine.state.output = output = {Keys.IMAGE: inputs, Keys.LABEL: targets} - output[Keys.PRED] = torch.zeros([1, self.n_classes, *original_shape]) - # pad the prediction back to the original shape - box_start, box_end = batchdata["bbox"][0] - h_start, w_start, d_start = box_start - h_end, w_end, d_end = box_end - output[Keys.PRED][ - 0, :, h_start:h_end, w_start:w_end, d_start:d_end - ] = predictions - del predictions - - engine.fire_event(IterationEvents.FORWARD_COMPLETED) - return output diff --git a/modules/dynunet_pipeline/inferrer.py b/modules/dynunet_pipeline/inferrer.py deleted file mode 100644 index b9d8ceeb0f..0000000000 --- a/modules/dynunet_pipeline/inferrer.py +++ /dev/null @@ -1,192 +0,0 @@ -import os -from typing import Any, Callable, Dict, Optional, Sequence, Tuple, Union - -import nibabel as nib -import numpy as np -import torch -import torch.nn as nn -from ignite.engine import Engine -from ignite.metrics import Metric -from monai.data.utils import to_affine_nd -from monai.engines import SupervisedEvaluator -from monai.engines.utils import IterationEvents, default_prepare_batch -from monai.inferers import Inferer -from monai.networks.utils import eval_mode -from monai.transforms import AsDiscrete, Transform -from torch.utils.data import DataLoader - -from transforms import recovery_prediction - - -class DynUNetInferrer(SupervisedEvaluator): - """ - This class inherits from SupervisedEvaluator in MONAI, and is used with DynUNet - on Decathlon datasets. - - Args: - device: an object representing the device on which to run. - val_data_loader: Ignite engine use data_loader to run, must be - torch.DataLoader. - network: use the network to run model forward. - output_dir: the path to save inferred outputs. - n_classes: the number of classes (output channels) for the task. - epoch_length: number of iterations for one epoch, default to - `len(val_data_loader)`. - non_blocking: if True and this copy is between CPU and GPU, the copy may occur asynchronously - with respect to the host. For other cases, this argument has no effect. - prepare_batch: function to parse image and label for current iteration. - iteration_update: the callable function for every iteration, expect to accept `engine` - and `batchdata` as input parameters. if not provided, use `self._iteration()` instead. - inferer: inference method that execute model forward on input data, like: SlidingWindow, etc. - post_transform: execute additional transformation for the model output data. - Typically, several Tensor based transforms composed by `Compose`. - key_val_metric: compute metric when every iteration completed, and save average value to - engine.state.metrics when epoch completed. key_val_metric is the main metric to compare and save the - checkpoint into files. - additional_metrics: more Ignite metrics that also attach to Ignite Engine. - val_handlers: every handler is a set of Ignite Event-Handlers, must have `attach` function, like: - CheckpointHandler, StatsHandler, SegmentationSaver, etc. - amp: whether to enable auto-mixed-precision evaluation, default is False. - tta_val: whether to do the 8 flips (8 = 2 ** 3, where 3 represents the three dimensions) - test time augmentation, default is False. - - """ - - def __init__( - self, - device: torch.device, - val_data_loader: DataLoader, - network: torch.nn.Module, - output_dir: str, - n_classes: Union[str, int], - epoch_length: Optional[int] = None, - non_blocking: bool = False, - prepare_batch: Callable = default_prepare_batch, - iteration_update: Optional[Callable] = None, - inferer: Optional[Inferer] = None, - post_transform: Optional[Transform] = None, - key_val_metric: Optional[Dict[str, Metric]] = None, - additional_metrics: Optional[Dict[str, Metric]] = None, - val_handlers: Optional[Sequence] = None, - amp: bool = False, - tta_val: bool = False, - ) -> None: - super().__init__( - device=device, - val_data_loader=val_data_loader, - network=network, - epoch_length=epoch_length, - non_blocking=non_blocking, - prepare_batch=prepare_batch, - iteration_update=iteration_update, - inferer=inferer, - post_transform=post_transform, - key_val_metric=key_val_metric, - additional_metrics=additional_metrics, - val_handlers=val_handlers, - amp=amp, - ) - - if not isinstance(n_classes, int): - n_classes = int(n_classes) - self.post_pred = AsDiscrete(argmax=True, to_onehot=True, n_classes=n_classes) - self.output_dir = output_dir - self.tta_val = tta_val - self.n_classes = n_classes - - def _iteration( - self, engine: Engine, batchdata: Dict[str, Any] - ) -> Dict[str, torch.Tensor]: - """ - callback function for the Supervised Evaluation processing logic of 1 iteration in Ignite Engine. - Return below item in a dictionary: - - PRED: prediction result of model. - - Args: - engine: Ignite Engine, it can be a trainer, validator or evaluator. - batchdata: input data for this iteration, usually can be dictionary or tuple of Tensor data. - - Raises: - ValueError: When ``batchdata`` is None. - - """ - if batchdata is None: - raise ValueError("Must provide batch data for current iteration.") - batch = self.prepare_batch(batchdata, engine.state.device, engine.non_blocking) - if len(batch) == 2: - inputs, _ = batch - args: Tuple = () - kwargs: Dict = {} - else: - inputs, _, args, kwargs = batch - - def _compute_pred(): - ct = 1.0 - pred = self.inferer(inputs, self.network, *args, **kwargs).cpu() - pred = nn.functional.softmax(pred, dim=1) - if not self.tta_val: - return pred - else: - for dims in [[2], [3], [4], (2, 3), (2, 4), (3, 4), (2, 3, 4)]: - flip_inputs = torch.flip(inputs, dims=dims) - flip_pred = torch.flip( - self.inferer(flip_inputs, self.network).cpu(), dims=dims - ) - flip_pred = nn.functional.softmax(flip_pred, dim=1) - del flip_inputs - pred += flip_pred - del flip_pred - ct += 1 - return pred / ct - - # execute forward computation - with eval_mode(self.network): - if self.amp: - with torch.cuda.amp.autocast(): - predictions = _compute_pred() - else: - predictions = _compute_pred() - - inputs = inputs.cpu() - predictions = self.post_pred(predictions) - - target_affine = batchdata["image_meta_dict"]["affine"].numpy()[0] - resample_flag = batchdata["resample_flag"] - anisotrophy_flag = batchdata["anisotrophy_flag"] - crop_shape = batchdata["crop_shape"][0].tolist() - original_shape = batchdata["original_shape"][0].tolist() - - if resample_flag: - # convert the prediction back to the original (after cropped) shape - predictions = recovery_prediction( - predictions.numpy()[0], [self.n_classes, *crop_shape], anisotrophy_flag - ) - else: - predictions = predictions.numpy() - - predictions = predictions[0] - predictions = np.argmax(predictions, axis=0) - - # pad the prediction back to the original shape - predictions_org = np.zeros([*original_shape]) - box_start, box_end = batchdata["bbox"][0] - h_start, w_start, d_start = box_start - h_end, w_end, d_end = box_end - predictions_org[h_start:h_end, w_start:w_end, d_start:d_end] = predictions - del predictions - - filename = batchdata["image_meta_dict"]["filename_or_obj"][0].split("/")[-1] - - print( - "save {} with shape: {}, mean values: {}".format( - filename, predictions_org.shape, predictions_org.mean() - ) - ) - results_img = nib.Nifti1Image( - predictions_org.astype(np.uint8), to_affine_nd(3, target_affine) - ) - del predictions_org - nib.save(results_img, os.path.join(self.output_dir, filename)) - - engine.fire_event(IterationEvents.FORWARD_COMPLETED) - return {"pred": results_img} diff --git a/modules/dynunet_pipeline/task_params.py b/modules/dynunet_pipeline/task_params.py deleted file mode 100644 index 6e23813384..0000000000 --- a/modules/dynunet_pipeline/task_params.py +++ /dev/null @@ -1,90 +0,0 @@ -task_name = { - "01": "Task01_BrainTumour", - "02": "Task02_Heart", - "03": "Task03_Liver", - "04": "Task04_Hippocampus", - "05": "Task05_Prostate", - "06": "Task06_Lung", - "07": "Task07_Pancreas", - "08": "Task08_HepaticVessel", - "09": "Task09_Spleen", - "10": "Task10_Colon", -} - -patch_size = { - "01": [128, 128, 128], - "02": [160, 192, 80], - "03": [128, 128, 128], - "04": [40, 56, 40], - "05": [320, 256, 20], - "06": [192, 160, 80], - "07": [224, 224, 40], - "08": [192, 192, 64], - "09": [192, 160, 64], - "10": [192, 160, 56], -} - -spacing = { - "01": [1.0, 1.0, 1.0], - "02": [1.25, 1.25, 1.37], - "03": [0.77, 0.77, 1], - "04": [1.0, 1.0, 1.0], - "05": [0.62, 0.62, 3.6], - "06": [0.79, 0.79, 1.24], - "07": [0.8, 0.8, 2.5], - "08": [0.8, 0.8, 1.5], - "09": [0.79, 0.79, 1.6], - "10": [0.78, 0.78, 3], -} - -clip_values = { - "01": [0, 0], - "02": [0, 0], - "03": [-17, 201], - "04": [0, 0], - "05": [0, 0], - "06": [-1024, 325], - "07": [-96, 215], - "08": [-3, 243], - "09": [-41, 176], - "10": [-30, 165.82], -} - -normalize_values = { - "01": [0, 0], - "02": [0, 0], - "03": [99.40, 39.36], - "04": [0, 0], - "05": [0, 0], - "06": [-158.58, 324.7], - "07": [77.99, 75.4], - "08": [104.37, 52.62], - "09": [99.29, 39.47], - "10": [62.18, 32.65], -} - -data_loader_params = { - "01": {"batch_size": 8}, - "02": {"batch_size": 2}, - "03": {"batch_size": 8}, - "04": {"batch_size": 9}, - "05": {"batch_size": 2}, - "06": {"batch_size": 2}, - "07": {"batch_size": 2}, - "08": {"batch_size": 2}, - "09": {"batch_size": 2}, - "10": {"batch_size": 2}, -} - -deep_supr_num = { - "01": 3, - "02": 3, - "03": 3, - "04": 1, - "05": 4, - "06": 3, - "07": 3, - "08": 3, - "09": 3, - "10": 3, -} diff --git a/modules/dynunet_pipeline/train.py b/modules/dynunet_pipeline/train.py deleted file mode 100644 index 60655441f9..0000000000 --- a/modules/dynunet_pipeline/train.py +++ /dev/null @@ -1,248 +0,0 @@ -import logging -import os -from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser - -import torch -from monai.handlers import (CheckpointSaver, LrScheduleHandler, MeanDice, - StatsHandler, ValidationHandler) -from monai.inferers import SimpleInferer, SlidingWindowInferer -from monai.losses import DiceCELoss - -from task_params import data_loader_params, patch_size -from create_dataset import get_data -from create_network import get_network -from evaluator import DynUNetEvaluator -from trainer import DynUNetTrainer - - -def validation(args): - # load hyper parameters - task_id = args.task_id - sw_batch_size = args.sw_batch_size - tta_val = args.tta_val - window_mode = args.window_mode - eval_overlap = args.eval_overlap - amp = args.amp - - properties, val_loader = get_data(args, mode="validation") - n_classes = len(properties["labels"]) - # produce the network - checkpoint = args.checkpoint - val_output_dir = "./runs_{}_fold{}_{}/".format(task_id, args.fold, args.expr_name) - device = torch.device("cuda:0") - net = get_network(device, properties, task_id, val_output_dir, checkpoint) - - net.eval() - - evaluator = DynUNetEvaluator( - device=device, - val_data_loader=val_loader, - network=net, - n_classes=n_classes, - inferer=SlidingWindowInferer( - roi_size=patch_size[task_id], - sw_batch_size=sw_batch_size, - overlap=eval_overlap, - mode=window_mode, - ), - post_transform=None, - key_val_metric={ - "val_mean_dice": MeanDice( - include_background=False, - output_transform=lambda x: (x["pred"], x["label"]), - ) - }, - additional_metrics=None, - amp=amp, - tta_val=tta_val, - ) - - evaluator.run() - print(evaluator.state.metrics) - results = evaluator.state.metric_details["val_mean_dice"] - if n_classes > 2: - for i in range(n_classes - 1): - print("mean dice for label {} is {}".format(i+1, results[:, i].mean())) - - -def train(args): - # load hyper parameters - task_id = args.task_id - fold = args.fold - val_output_dir = "./runs_{}_fold{}_{}/".format(task_id, fold, args.expr_name) - log_filename = "nnunet_task{}_fold{}.log".format(task_id, fold) - log_filename = os.path.join(val_output_dir, log_filename) - interval = args.interval - learning_rate = args.learning_rate - max_epochs = args.max_epochs - amp_flag = args.amp - lr_decay_flag = args.lr_decay - sw_batch_size = args.sw_batch_size - tta_val = args.tta_val - batch_dice = args.batch_dice - window_mode = args.window_mode - eval_overlap = args.eval_overlap - - # transforms - train_batch_size = data_loader_params[task_id]["batch_size"] - - properties, val_loader = get_data(args, mode="validation") - _, train_loader = get_data(args, batch_size=train_batch_size, mode="train") - - # produce the network - device = torch.device("cuda:0") - checkpoint = args.checkpoint - net = get_network(device, properties, task_id, val_output_dir, checkpoint) - - optimizer = torch.optim.SGD( - net.parameters(), - lr=learning_rate, - momentum=0.99, - weight_decay=3e-5, - nesterov=True, - ) - - scheduler = torch.optim.lr_scheduler.LambdaLR( - optimizer, lr_lambda=lambda epoch: (1 - epoch / max_epochs) ** 0.9 - ) - # produce evaluator - val_handlers = [ - StatsHandler(output_transform=lambda x: None), - CheckpointSaver( - save_dir=val_output_dir, save_dict={"net": net}, save_key_metric=True - ), - ] - - evaluator = DynUNetEvaluator( - device=device, - val_data_loader=val_loader, - network=net, - n_classes=len(properties["labels"]), - inferer=SlidingWindowInferer( - roi_size=patch_size[task_id], - sw_batch_size=sw_batch_size, - overlap=eval_overlap, - mode=window_mode, - ), - post_transform=None, - key_val_metric={ - "val_mean_dice": MeanDice( - include_background=False, - output_transform=lambda x: (x["pred"], x["label"]), - ) - }, - val_handlers=val_handlers, - amp=amp_flag, - tta_val=tta_val, - ) - # produce trainer - loss = DiceCELoss(to_onehot_y=True, softmax=True, batch=batch_dice) - train_handlers = [] - if lr_decay_flag: - train_handlers += [LrScheduleHandler(lr_scheduler=scheduler, print_lr=True)] - - train_handlers += [ - ValidationHandler(validator=evaluator, interval=interval, epoch_level=True), - StatsHandler(tag_name="train_loss", output_transform=lambda x: x["loss"]), - ] - - trainer = DynUNetTrainer( - device=device, - max_epochs=max_epochs, - train_data_loader=train_loader, - network=net, - optimizer=optimizer, - loss_function=loss, - inferer=SimpleInferer(), - post_transform=None, - key_train_metric=None, - train_handlers=train_handlers, - amp=amp_flag, - ) - - # run - logger = logging.getLogger() - - formatter = logging.Formatter( - "%(asctime)s - %(name)s - %(levelname)s - %(message)s" - ) - - # Setup file handler - fhandler = logging.FileHandler(log_filename) - fhandler.setLevel(logging.INFO) - fhandler.setFormatter(formatter) - - # Configure stream handler for the cells - chandler = logging.StreamHandler() - chandler.setLevel(logging.INFO) - chandler.setFormatter(formatter) - - # Add both handlers - logger.addHandler(fhandler) - logger.addHandler(chandler) - logger.setLevel(logging.INFO) - - # Show the handlers - logger.handlers - - # Log Something - logger.info("Test info") - logger.debug("Test debug") - - trainer.run() - - -if __name__ == "__main__": - parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter) - parser.add_argument("-fold", "--fold", type=int, default=0, help="0-5") - parser.add_argument( - "-task_id", "--task_id", type=str, default="02", help="task 01 to 10" - ) - parser.add_argument( - "-root_dir", - "--root_dir", - type=str, - default="/workspace/data/medical/", - help="dataset path", - ) - parser.add_argument("-expr_name", "--expr_name", type=str, default="expr", help="the suffix of the experiment's folder") - parser.add_argument( - "-datalist_path", "--datalist_path", type=str, default="config/", - ) - parser.add_argument( - "-train_num_workers", "--train_num_workers", type=int, default=4, help="the num_workers parameter of training dataloader." - ) - parser.add_argument("-val_num_workers", "--val_num_workers", type=int, default=1, help="the num_workers parameter of validation dataloader.") - parser.add_argument("-interval", "--interval", type=int, default=5, help="the validation interval under epoch level.") - parser.add_argument("-eval_overlap", "--eval_overlap", type=float, default=0.5, help="the overlap parameter of SlidingWindowInferer.") - parser.add_argument("-sw_batch_size", "--sw_batch_size", type=int, default=4, help="the sw_batch_size parameter of SlidingWindowInferer.") - parser.add_argument( - "-window_mode", - "--window_mode", - type=str, - default="gaussian", - choices=["constant", "gaussian"], - help="the mode parameter for SlidingWindowInferer." - ) - parser.add_argument("-num_samples", "--num_samples", type=int, default=3, help="the num_samples parameter of RandCropByPosNegLabeld.") - parser.add_argument("-pos_sample_num", "--pos_sample_num", type=int, default=1, help="the pos parameter of RandCropByPosNegLabeld.") - parser.add_argument("-neg_sample_num", "--neg_sample_num", type=int, default=1, help="the neg parameter of RandCropByPosNegLabeld.") - parser.add_argument("-cache_rate", "--cache_rate", type=float, default=1.0, help="the cache_rate parameter of CacheDataset.") - parser.add_argument("-learning_rate", "--learning_rate", type=float, default=1e-2) - parser.add_argument("-max_epochs", "--max_epochs", type=int, default=1000, help="number of epochs of training.") - parser.add_argument( - "-mode", "--mode", type=str, default="train", choices=["train", "val"] - ) - parser.add_argument("-checkpoint", "--checkpoint", type=str, default=None, help="the filename of weights.") - parser.add_argument("-amp", "--amp", type=bool, default=False, help="whether to use automatic mixed precision.") - parser.add_argument("-lr_decay", "--lr_decay", type=bool, default=False, help="whether to use learning rate decay.") - parser.add_argument("-tta_val", "--tta_val", type=bool, default=False, help="whether to use test time augmentation.") - parser.add_argument("-batch_dice", "--batch_dice", type=bool, default=False, help="the batch parameter of DiceCELoss.") - - args = parser.parse_args() - - if args.mode == "train": - train(args) - elif args.mode == "val": - validation(args) - \ No newline at end of file diff --git a/modules/dynunet_pipeline/trainer.py b/modules/dynunet_pipeline/trainer.py deleted file mode 100644 index ac45ae994a..0000000000 --- a/modules/dynunet_pipeline/trainer.py +++ /dev/null @@ -1,85 +0,0 @@ -from typing import Any, Dict, Tuple - -import torch -from ignite.engine import Engine -from monai.engines import SupervisedTrainer -from monai.engines.utils import CommonKeys as Keys -from monai.engines.utils import IterationEvents -from torch.nn.parallel import DistributedDataParallel - - -class DynUNetTrainer(SupervisedTrainer): - """ - This class inherits from SupervisedTrainer in MONAI, and is used with DynUNet - on Decathlon datasets. - - """ - - def _iteration(self, engine: Engine, batchdata: Dict[str, Any]): - """ - Callback function for the Supervised Training processing logic of 1 iteration in Ignite Engine. - Return below items in a dictionary: - - IMAGE: image Tensor data for model input, already moved to device. - - LABEL: label Tensor data corresponding to the image, already moved to device. - - PRED: prediction result of model. - - LOSS: loss value computed by loss function. - - Args: - engine: Ignite Engine, it can be a trainer, validator or evaluator. - batchdata: input data for this iteration, usually can be dictionary or tuple of Tensor data. - - Raises: - ValueError: When ``batchdata`` is None. - - """ - if batchdata is None: - raise ValueError("Must provide batch data for current iteration.") - batch = self.prepare_batch(batchdata, engine.state.device, engine.non_blocking) - if len(batch) == 2: - inputs, targets = batch - args: Tuple = () - kwargs: Dict = {} - else: - inputs, targets, args, kwargs = batch - # put iteration outputs into engine.state - engine.state.output = output = {Keys.IMAGE: inputs, Keys.LABEL: targets} - - def _compute_pred_loss(): - preds = self.inferer(inputs, self.network, *args, **kwargs) - if len(preds.size()) - len(targets.size()) == 1: - # deep supervision mode, need to unbind feature maps first. - preds = torch.unbind(preds, dim=1) - output[Keys.PRED] = preds - del preds - engine.fire_event(IterationEvents.FORWARD_COMPLETED) - output[Keys.LOSS] = sum( - 0.5 ** i * self.loss_function.forward(p, targets) - for i, p in enumerate(output[Keys.PRED]) - ) - engine.fire_event(IterationEvents.LOSS_COMPLETED) - - self.network.train() - self.optimizer.zero_grad() - if self.amp and self.scaler is not None: - with torch.cuda.amp.autocast(): - _compute_pred_loss() - self.scaler.scale(output[Keys.LOSS]).backward() - self.scaler.unscale_(self.optimizer) - if isinstance(self.network, DistributedDataParallel): - torch.nn.utils.clip_grad_norm_(self.network.module.parameters(), 12) - else: - torch.nn.utils.clip_grad_norm_(self.network.parameters(), 12) - self.scaler.step(self.optimizer) - self.scaler.update() - else: - _compute_pred_loss() - output[Keys.LOSS].backward() - engine.fire_event(IterationEvents.BACKWARD_COMPLETED) - if isinstance(self.network, DistributedDataParallel): - torch.nn.utils.clip_grad_norm_(self.network.module.parameters(), 12) - else: - torch.nn.utils.clip_grad_norm_(self.network.parameters(), 12) - self.optimizer.step() - engine.fire_event(IterationEvents.OPTIMIZER_COMPLETED) - - return output diff --git a/modules/dynunet_pipeline/transforms.py b/modules/dynunet_pipeline/transforms.py deleted file mode 100644 index 34f26a80d7..0000000000 --- a/modules/dynunet_pipeline/transforms.py +++ /dev/null @@ -1,340 +0,0 @@ -import numpy as np -from monai.transforms import ( - AddChanneld, - AsChannelFirstd, - CastToTyped, - Compose, - CropForegroundd, - LoadImaged, - NormalizeIntensity, - RandCropByPosNegLabeld, - RandFlipd, - RandGaussianNoised, - RandGaussianSmoothd, - RandScaleIntensityd, - RandZoomd, - SpatialCrop, - SpatialPadd, - ToTensord, -) -from monai.transforms.compose import MapTransform -from monai.transforms.utils import generate_spatial_bounding_box -from skimage.transform import resize - -from task_params import clip_values, normalize_values, patch_size, spacing - - -def get_task_transforms(mode, task_id, pos_sample_num, neg_sample_num, num_samples): - - # 1. loading for different formats - if mode != "test": - load_keys = ["image", "label"] - else: - load_keys = ["image"] - - if task_id in ["01", "05"]: - load_transforms = [ - LoadImaged(keys=load_keys), - AsChannelFirstd(keys="image"), - ] - if mode != "test": - load_transforms.append(AddChanneld(keys=["label"])) - else: - load_transforms = [ - LoadImaged(keys=load_keys), - AddChanneld(keys=load_keys), - ] - # 2. sampling - if mode != "test": - sample_keys = ["image", "label"] - else: - sample_keys = ["image"] - sample_transforms = [ - PreprocessAnisotropic( - keys=sample_keys, - clip_values=clip_values[task_id], - pixdim=spacing[task_id], - normalize_values=normalize_values[task_id], - model_mode=mode, - ), - ] - # 3. spatial transforms - if mode == "train": - other_transforms = [ - SpatialPadd(keys=["image", "label"], spatial_size=patch_size[task_id]), - RandCropByPosNegLabeld( - keys=["image", "label"], - label_key="label", - spatial_size=patch_size[task_id], - pos=pos_sample_num, - neg=neg_sample_num, - num_samples=num_samples, - image_key="image", - image_threshold=0, - ), - RandZoomd( - keys=["image", "label"], - min_zoom=0.9, - max_zoom=1.2, - mode=("trilinear", "nearest"), - align_corners=(True, None), - prob=0.15, - ), - RandGaussianNoised(keys=["image"], std=0.01, prob=0.15), - RandGaussianSmoothd( - keys=["image"], - sigma_x=(0.5, 1.15), - sigma_y=(0.5, 1.15), - sigma_z=(0.5, 1.15), - prob=0.15, - ), - RandScaleIntensityd(keys=["image"], factors=0.3, prob=0.15), - RandFlipd(["image", "label"], spatial_axis=[0], prob=0.5), - RandFlipd(["image", "label"], spatial_axis=[1], prob=0.5), - RandFlipd(["image", "label"], spatial_axis=[2], prob=0.5), - CastToTyped(keys=["image", "label"], dtype=(np.float32, np.uint8)), - ToTensord(keys=["image", "label"]), - ] - elif mode == "validation": - other_transforms = [ - CastToTyped(keys=["image", "label"], dtype=(np.float32, np.uint8)), - ToTensord(keys=["image", "label"]), - ] - else: - other_transforms = [ - CastToTyped(keys=["image"], dtype=(np.float32)), - ToTensord(keys=["image"]), - ] - - all_transforms = load_transforms + sample_transforms + other_transforms - return Compose(all_transforms) - - -def resample_image(image, shape, anisotrophy_flag): - resized_channels = [] - if anisotrophy_flag: - for image_c in image: - resized_slices = [] - for i in range(image_c.shape[-1]): - image_c_2d_slice = image_c[:, :, i] - image_c_2d_slice = resize( - image_c_2d_slice, - shape[:-1], - order=3, - mode="edge", - cval=0, - clip=True, - anti_aliasing=False, - ) - resized_slices.append(image_c_2d_slice) - resized = np.stack(resized_slices, axis=-1) - resized = resize( - resized, - shape, - order=0, - mode="constant", - cval=0, - clip=True, - anti_aliasing=False, - ) - resized_channels.append(resized) - else: - for image_c in image: - resized = resize( - image_c, - shape, - order=3, - mode="edge", - cval=0, - clip=True, - anti_aliasing=False, - ) - resized_channels.append(resized) - resized = np.stack(resized_channels, axis=0) - return resized - - -def resample_label(label, shape, anisotrophy_flag): - reshaped = np.zeros(shape, dtype=np.uint8) - n_class = np.max(label) - if anisotrophy_flag: - shape_2d = shape[:-1] - depth = label.shape[-1] - reshaped_2d = np.zeros((*shape_2d, depth), dtype=np.uint8) - - for class_ in range(1, int(n_class) + 1): - for depth_ in range(depth): - mask = label[0, :, :, depth_] == class_ - resized_2d = resize( - mask.astype(float), - shape_2d, - order=1, - mode="edge", - cval=0, - clip=True, - anti_aliasing=False, - ) - reshaped_2d[:, :, depth_][resized_2d >= 0.5] = class_ - for class_ in range(1, int(n_class) + 1): - mask = reshaped_2d == class_ - resized = resize( - mask.astype(float), - shape, - order=0, - mode="constant", - cval=0, - clip=True, - anti_aliasing=False, - ) - reshaped[resized >= 0.5] = class_ - else: - for class_ in range(1, int(n_class) + 1): - mask = label[0] == class_ - resized = resize( - mask.astype(float), - shape, - order=1, - mode="edge", - cval=0, - clip=True, - anti_aliasing=False, - ) - reshaped[resized >= 0.5] = class_ - - reshaped = np.expand_dims(reshaped, 0) - return reshaped - - -def recovery_prediction(prediction, shape, anisotrophy_flag): - reshaped = np.zeros(shape, dtype=np.uint8) - n_class = shape[0] - if anisotrophy_flag: - c, h, w = prediction.shape[:-1] - d = shape[-1] - reshaped_d = np.zeros((c, h, w, d), dtype=np.uint8) - for class_ in range(1, n_class): - mask = prediction[class_] == 1 - resized_d = resize( - mask.astype(float), - (h, w, d), - order=0, - mode="constant", - cval=0, - clip=True, - anti_aliasing=False, - ) - reshaped_d[class_][resized_d >= 0.5] = 1 - - for class_ in range(1, n_class): - for depth_ in range(d): - mask = reshaped_d[class_, :, :, depth_] == 1 - resized_hw = resize( - mask.astype(float), - shape[1:-1], - order=1, - mode="edge", - cval=0, - clip=True, - anti_aliasing=False, - ) - reshaped[class_, :, :, depth_][resized_hw >= 0.5] = 1 - else: - for class_ in range(1, n_class): - mask = prediction[class_] == 1 - resized = resize( - mask.astype(float), - shape[1:], - order=1, - mode="edge", - cval=0, - clip=True, - anti_aliasing=False, - ) - reshaped[class_][resized >= 0.5] = 1 - - reshaped = np.expand_dims(reshaped, 0) - return reshaped - - -class PreprocessAnisotropic(MapTransform): - def __init__( - self, - keys, - clip_values, - pixdim, - normalize_values, - model_mode, - ) -> None: - super().__init__(keys) - self.keys = keys - self.low = clip_values[0] - self.high = clip_values[1] - self.target_spacing = pixdim - self.mean = normalize_values[0] - self.std = normalize_values[1] - self.training = False - self.crop_foreg = CropForegroundd(keys=["image", "label"], source_key="image") - self.normalize_intensity = NormalizeIntensity(nonzero=True, channel_wise=True) - if model_mode in ["train"]: - self.training = True - - def calculate_new_shape(self, spacing, shape): - spacing_ratio = np.array(spacing) / np.array(self.target_spacing) - new_shape = (spacing_ratio * np.array(shape)).astype(int).tolist() - return new_shape - - def check_anisotrophy(self, spacing): - def check(spacing): - return np.max(spacing) / np.min(spacing) >= 3 - - return check(spacing) or check(self.target_spacing) - - def __call__(self, data): - # load data - d = dict(data) - image = d["image"] - image_spacings = d["image_meta_dict"]["pixdim"][1:4].tolist() - - if "label" in self.keys: - label = d["label"] - label[label < 0] = 0 - - if self.training: - # only task 04 does not be impacted - cropped_data = self.crop_foreg({"image": image, "label": label}) - image, label = cropped_data["image"], cropped_data["label"] - else: - d["original_shape"] = np.array(image.shape[1:]) - box_start, box_end = generate_spatial_bounding_box(image) - image = SpatialCrop(roi_start=box_start, roi_end=box_end)(image) - d["bbox"] = np.vstack([box_start, box_end]) - d["crop_shape"] = np.array(image.shape[1:]) - - original_shape = image.shape[1:] - # calculate shape - resample_flag = False - anisotrophy_flag = False - if self.target_spacing != image_spacings: - # resample - resample_flag = True - resample_shape = self.calculate_new_shape(image_spacings, original_shape) - anisotrophy_flag = self.check_anisotrophy(image_spacings) - image = resample_image(image, resample_shape, anisotrophy_flag) - if self.training: - label = resample_label(label, resample_shape, anisotrophy_flag) - - d["resample_flag"] = resample_flag - d["anisotrophy_flag"] = anisotrophy_flag - # clip image for CT dataset - if self.low != 0 or self.high != 0: - image = np.clip(image, self.low, self.high) - image = (image - self.mean) / self.std - else: - image = self.normalize_intensity(image.copy()) - - d["image"] = image - - if "label" in self.keys: - d["label"] = label - - return d diff --git a/modules/dynunet_tutorial.ipynb b/modules/dynunet_tutorial.ipynb index 2f91672fea..a22d57a14f 100644 --- a/modules/dynunet_tutorial.ipynb +++ b/modules/dynunet_tutorial.ipynb @@ -60,11 +60,11 @@ "name": "stdout", "output_type": "stream", "text": [ - "MONAI version: 0+untagged.1.g19a9f05.dirty\n", + "MONAI version: 0.4.0+86.gadb2f7f.dirty\n", "Numpy version: 1.19.1\n", - "Pytorch version: 1.7.0a0+7036e91\n", + "Pytorch version: 1.7.0a0+8deb4fe\n", "MONAI flags: HAS_EXT = True, USE_COMPILED = False\n", - "MONAI rev id: 19a9f0554a9d641716162097d02ec8944c67e0a1\n", + "MONAI rev id: adb2f7fa7a0f9cb519614f6ec6f3a7f43601d9c9\n", "\n", "Optional dependencies:\n", "Pytorch Ignite version: 0.4.2\n", @@ -75,9 +75,9 @@ "gdown version: 3.12.2\n", "TorchVision version: 0.8.0a0\n", "ITK version: 5.1.2\n", - "tqdm version: 4.56.0\n", - "lmdb version: 1.0.0\n", - "psutil version: 5.7.2\n", + "tqdm version: 4.56.2\n", + "lmdb version: 0.99\n", + "psutil version: 5.7.0\n", "\n", "For details about installing the optional dependencies, please visit:\n", " https://docs.monai.io/en/latest/installation.html#installing-the-recommended-dependencies\n", @@ -242,7 +242,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 5, "metadata": {}, "outputs": [], "source": [ @@ -320,15 +320,15 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ - "Loading dataset: 100%|██████████| 208/208 [00:01<00:00, 146.24it/s]\n", - "Loading dataset: 100%|██████████| 52/52 [00:00<00:00, 144.76it/s]\n" + "Loading dataset: 100%|██████████| 208/208 [00:01<00:00, 145.76it/s]\n", + "Loading dataset: 100%|██████████| 52/52 [00:00<00:00, 144.63it/s]\n" ] } ], @@ -363,12 +363,12 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 7, "metadata": {}, "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -380,7 +380,7 @@ }, { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAr8AAAEICAYAAABWPpy+AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjMuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8vihELAAAACXBIWXMAAAsTAAALEwEAmpwYAAAq3klEQVR4nO3de7Cd5XXf8d9CHCSELgddkXQkjiSLCASxIKqAmEwxNgS7TrHb1BO38XgSp7hNMo3bOMSmU+cybceeOiHupJOUBGrSJr7hUF+wTTBWxnVqk0ggA7JAQhfQXUJCSEJCSGL1j72VHuv9PUd7n/s+7/czo5HOOs/e+3n3+56Hh/fstVZkpgAAAIA6uGC0JwAAAACMFDa/AAAAqA02vwAAAKgNNr8AAACoDTa/AAAAqA02vwAAAKgNNr8YdRGxISJuHu15AAAGJiK2R8TbWxiXEfGmAb7GgB8L9HXhaE8AyMwVoz0HAABQD9z5BQAAQG2w+cWoO/vrsoj47Yj4YkT8r4g4GhFPR8QVEfGxiNgfETsi4rY+j/uFiNjYHLs1Ij50zvPeFRF7ImJ3RPxS31+ZRcTEiPhURLwYEfsi4o8j4uKRPnYAGE8iYnVEfC8iDjfX3z+MiIvOGfbO5pr9UkT8l4i4oM/jf7G5rr8cEY9ExOUjfAioATa/GGt+RtL/lHSppCclPaLGdbpA0u9K+u99xu6X9C5J0yT9gqR7IuI6SYqI2yX9O0lvl/QmSTef8zqfkHSFpJXN7y+Q9PFhOB4AqJMzkv6tpFmSbpT0Nkm/fM6Y90haJek6SXdI+kVJiog7JN0t6Z9Imi3p/0j67IjMGrUSmTnac0DNRcR2Sb8k6SZJb8nMW5vxn1Fj4ZuemWciYqqkI5IuzczD5nn+t6Q1mfnpiLhf0r7M/Fjze2+StFnSMklbJB2T9OOZuaX5/Rsl/UVmLh7OYwWA8ejsOp6Z3zon/mFJ/zAz39P8OiW9IzO/2fz6lyX908x8W0R8Q9KDmXlf83sXqLFWX5mZLzQfuywznx+p48L4xJ1fjDX7+vz7hKSXMvNMn68laYokRcQ7IuL7EXEoIg5Leqcadxskab6kHX2eq++/Z0uaLGld81dzhyV9sxkHAAxQ86NqX4uIvRFxRNJ/1v9fl8/qux6/oMZ6LUmXS/p0n3X5kKRQ4zdzwJBh84uOFBETJX1J0qckzc3MbklfV2OhlKQ9knr6PGRhn3+/pMZGekVmdjf/TM/MKcM/cwAY1/5I0rNq3KGdpsbHGOKcMX3X40WSdjf/vUPSh/qsy92ZeXFm/t9hnzVqhc0vOtVFkiZKOiDpdES8Q9Jtfb7/BUm/EBFXRsRkSf/h7Dcy8w1Jf6LGZ4TnSFJELIiInx6x2QPA+HT242nHImK5pH9txvxGRFwaEQsl/ZqkzzfjfyzpYxGxQpIiYnpE/LORmDTqhc0vOlJmHpX0b9TY5L4s6Z9L+kqf739D0n+VtEbS85K+3/zWyebfv3k23vzV3Lck/diITB4Axq+PqLEeH1XjJsPnzZgvS1onab2khyXdJ0mZ+ZCkT0r6XHNdfkbSO4Z/yqgbEt5QCxFxpRoL6cTMPD3a8wEAAKODO78YtyLiPc16vpeqcTfhq2x8AQCoNza/GM8+pEYt4C1q1J50nz0DAAA1wsceAAAAUBvc+QUAAEBtXDiYBzdbyH5a0gRJf5qZnzjPeG4zA+hYmXluvdKO0866fVFMzEm6ZMTmBgBD6ahefikzKw2sBvyxh4iYIGmTpFsl7ZT0d5Lel5k/7OcxbH4BdKxO3/y2u25Pixl5fbxtBGcIAEPnW/ngusxcdW58MB97WC3p+czcmpmvS/qcpDsG8XwAgOHFug2g9gaz+V2gH+3PvVOm/3ZE3BkRayNi7SBeCwAweOddt/uu2af+vicMAIwfw57wlpn3ZuYqd9sZADC29F2zuzRxtKcDAENuMJvfXZIW9vm6pxkDAIxNrNsAam8wm9+/k7QsIhZHxEWSfk7SV4ZmWgCAYcC6DaD2BlzqLDNPR8SvSnpEjZI592fmhiGbGQBgSLFuA8Ag6/xm5tclfX2I5gIAGGas2wDqjg5vAAAAqA02vwAAAKgNNr8AAACoDTa/AAAAqA02vwAAAKgNNr8AAACoDTa/AAAAqI1B1fkdK+66665K7LXXXrNjT5w40fLzTpgwoRJ74403Wn7O06dPV2JTp061Y3/qp36qElu+fLkd++yzz1ZiTzzxhB3r5nby5MlK7ODBg/bxZ86cqcRmzJhhx15wQfX/pS680F9ibuzRo0ft2EsuuaQSmzNnTiVWOufOpEmTbNy9D6+//rodGxGVWOl4T506VYkdOnTIjr3ooosqsQULFlRipeuut7e3EnvXu95lxy5cuLASK72PR44cqcTcNS5Jc+fOrcTcdVc655MnT24pJkmzZs2ycQAAHO78AgAAoDbY/AIAAKA22PwCAACgNtj8AgAAoDbY/AIAAKA2xkW1B1eBoVSR4OKLL275eTOzpdcqVQ7o6uqqxLq7u+3YJUuWVGJ79+61Y11lh8OHD9uxrqrC8ePHKzFXUUGSLrvsskqsnWoA06dPt2Ndlr+rBlCKlyolOK5KQOmcTZkypRJzFS9Kc5g2bZod694zVy2i9Hq7d++uxEqVJTZu3FiJucolkq8CUfoZcc/hrnHJXwul+TqukoX7eQQAoF3c+QUAAEBtsPkFAABAbbD5BQAAQG2w+QUAAEBtDCrhLSK2Szoq6Yyk05m5aigm1S6XpFRqI+za4s6cOdOOdUlRLhlp4sSJ9vHueUtJTi5J6Xvf+54d65Lb2mmr65L2XEvdklKymEuuK7XgdclLpeS4Y8eOVWKvvvpqS69f4p5Taq99tZtDKTnOnYfSe+7m4JIUXTKi5BPxtm3bZsfu27evElu6dKkd686Pew8kf35d4mIpEc/9nLzyyit2LNozVtZtABgtQ1Ht4a2Z+dIQPA8AYGSwbgOoLT72AAAAgNoY7OY3Jf1VRKyLiDvdgIi4MyLWRsTaQb4WAGDw+l23+67Zp+RrbwNAJxvsxx5uysxdETFH0qMR8WxmfqfvgMy8V9K9khQRVKkHgNHV77rdd82eFjNYswGMO4O685uZu5p/75f0kKTVQzEpAMDwYN0GUHcDvvMbEZdIuiAzjzb/fZuk3x2ymbVh+/btlVipzazLTnetdiVpwYIFLT1vOxn+pcoBzz77bCW2a9cuO9a16z19+nTLc3DVMdqplDB37lwbd21xSxn6L71UzbUptU12lSxcpYNSFYqXX365EitVeyhdC46rflCquuGqNZS48/P6669XYqX3y7X2XrFihR3rrvFSe2P3eqWW1O4YXAvtgwcP2se7n6lSpRS0biyt2wAwWgbzsYe5kh5q/gfpQkl/kZnfHJJZAQCGA+s2gNob8OY3M7dKevMQzgUAMIxYtwGAUmcAAACoETa/AAAAqI2h6PA2JpUSrVzcJV9J0v79+yuxefPmVWLz58+3j3dJVa6drCTt3LmzEislsR05cqQS2717tx3rWiy79rWlpL2urq6WYpJ/b6666io71h2va/Es+aQ9p5S012qb6tLYUiKda81bSgBzz1F6H10LbpeEVmoNfOWVV1ZipWvUJWCWjsG1Yy69j67FskvUdIl8kr/GXdtnAADaxZ1fAAAA1AabXwAAANQGm18AAADUBptfAAAA1AabXwAAANTGuKj24Fqplrh2qqVqAq4trsvQL72+y07fs2ePHesqQ5Qy4V1Vg+7ubjvWteBtp1WuqyhQOgZXnaLU7tdVnHBteSV/Hg4fPlyJlVrlunbMpRa+Tun8uvNQqhwya9asSsy1aJbKrbnP5SoqSP78Pvzww3bsD37wg0qsdLw333xzJbZ8+XI71lWGcBUcSlzFCdeaHACAdnHnFwAAALXB5hcAAAC1weYXAAAAtcHmFwAAALUxLhLe9u7dW4mVEq0ys6WY5JPQXJJTKXnKJceVkpQuueSSll5L8ok/rqWt5BPZDhw4UIm5pDDJt+V1bZ8ln9DkXkuSLr/88kpsxYoVduyyZcsqMXfOSvNyyWalc+aOYeLEiXasSwYsJWW5uZXaV7tksW984xuV2Jo1a+zjXdKdSxqUfJJhqZ3zjh07KrG77rrLjnXn9/jx45VYqQ25a5vsfkYAAGgXd34BAABQG2x+AQAAUBtsfgEAAFAbbH4BAABQG+dNeIuI+yW9S9L+zLy6GZsh6fOSeiVtl/TezPQZNSPAJRO57mqS1NPTU4m5BCNJOnr0aCXmEnFcQpXkE+ZcTPJduUod3twcSl3X3LG5DmKlZCL3eJfoJflkMdepS/Ld2Nz7LUlvfetbK7HZs2dXYgsXLrSPd8db6sTmEtZOnDhhx7qERJcUJknr1q2rxEoJeq7zm3v8U089ZR/vEtZK3fNcQmSpw9uGDRsqsd27d9uxl156aUuv1U7XN7SuE9ZtYCx4ZPf6YXnen56/clieF0OjlTu/n5F0+zmxj0p6LDOXSXqs+TUAYGz4jFi3AcA67+Y3M78j6dz6XHdIeqD57wckvXtopwUAGCjWbQAoG2id37mZefb37Hsl+SKxkiLiTkl3DvB1AABDo6V1u++aPUmTR2hqADByBp3wlo1uA75LROP792bmqsxcNdjXAgAMXn/rdt81u0u+wQsAdLKBbn73RcQ8SWr+7TN3AABjBes2AGjgH3v4iqQPSPpE8+8vD9mMBsBl3ZeqF7jqA6WqCq79rKu00E6739JrHT58uKXXknxr3tLzukoULhPftZ6V2svGd+9t6RjcOduyZYsdu2TJkkrMvQelc759+/ZK7PHHH7djXRWKUnULd32415J8FYnS827btq0Su+KKKyqx0vHu27evEiu1ynav5R4vSRFRiZWqjJw6daoSc5VDSsfgKmyUWjSjZWNq3QZG2nBVdhjsa1EZYuSd985vRHxW0vck/VhE7IyID6qxeN4aEZslvb35NQBgDGDdBoCy8975zcz3Fb71tiGeCwBgCLBuA0AZHd4AAABQG2x+AQAAUBsDTXgbU1wr466uLjvWJYaVEnxcm9jp06dXYqWkMJc85Vq8lsycOdPG3fG6BDBJmjy5WqfTJVqV2hAfOnRunfxyO2inlIjnnqP0Pm7durUSmzdvXiVWOufu/LrnlKTNmzdXYqWkve7u7krMXR+SP153biTfetkle7kWz5I/v729vXasa2XsWilL0k/8xE9UYi7pT/KJf67N9NKlS+3jXRtwWiED9TWSyWojjeS4kcedXwAAANQGm18AAADUBptfAAAA1AabXwAAANQGm18AAADUxrio9uCy5kuZ4a7taqnF6sSJE1t6fKmigYuXxrqs+1I1AJfNX6pI4KpLuFbGpRbNri2ve19Kcyi18M3Mlh4v+dbP7jyUWjS753WVB0pjjx49asfOnz+/EnOtoyV/PZZaDrvzvmPHDjvWcVUvXPUEyVescMdVGuuuD8mfH/ceuIoqAADPVYagAkT7uPMLAACA2mDzCwAAgNpg8wsAAIDaYPMLAACA2hgXCW9ORNi4a+Nbaou7d+/eSuzEiROVWCnJySWGldoQu/a3pYQoN9a1v5XKx3auUrKYS9BzLY8lP9/Se+O4hCpJWrx4cSXm2giXEvEWLlxYiZXeL9da2MUk39q3lBznrsdSMqBLeHPH4NpcSz657ZVXXrFj3XvW09Njx7prpPS8bm4u4W3u3Ln28a61d+n6ADD+lZK6xnPbYwwf7vwCAACgNtj8AgAAoDbY/AIAAKA22PwCAACgNs67+Y2I+yNif0Q80yf22xGxKyLWN/+8c3inCQBoFes2AJS1Uu3hM5L+UNKfnRO/JzM/NeQzGgDXGriU+e+yyA8ePGjH7tq1qxJz1QtKr+WUMvRdvNQK2WXNuyoUkq9u4do5l1opX3XVVZXYhg0b7Fj3PrpzU+KqOki++kCrVSwk37J4yZIldqw7D3/9139tx5ZaaDsXXlj9USsdgztnbl6lqhuu4oQ7j5KvOFGqbuHGuooXpbh7D0qPd+esdI3C+ozG+LoNDAVXBYIKEDif8975zczvSPL/lQUAjDms2wBQNpjP/P5qRDzV/PVa68VcAQCjhXUbQO0NdPP7R5KWSlopaY+k3ysNjIg7I2JtRKwd4GsBAAavpXW775p9StWP4ABApxvQ5jcz92Xmmcx8Q9KfSFrdz9h7M3NVZq4a6CQBAIPT6rrdd83uUuv5DADQKQbU3jgi5mXmnuaX75H0TH/jh5tLQiu1EZ40aVIl5hJxJJ/g49rMuueUpC1btlRipZbFLvFn9+7ddqw73tIcXKtbFyu1Id65c2clVmodPX/+/ErMJS5J0rx58yqxK664wo515+e1116rxKZOnWof7+ZQSsRzyXW9vb12rGt/XUqCc8mLrk215N/fV199tRIrJTm61yolT7bTgtsl6GWmHeuOzT3v4cOH7eOdpUuXtjwWVWNt3QaGS91aIZeOq/Q+oIXNb0R8VtLNkmZFxE5JvyXp5ohYKSklbZf0oeGbIgCgHazbAFB23s1vZr7PhO8bhrkAAIYA6zYAlNHhDQAAALXB5hcAAAC1weYXAAAAtTGgag9jjatUUGojfODAgUqsVL3AtYR1rYFdJr4kzZ07txJzVQokad++fZVYqRqAy5ovVXtwFRBchn2pSsHx48crsWnTptmx7r0paacyhKuq4CoduEoNkq9oUKp+4K6bFStW2LGuYsT69evtWHcuS9UaXJURN7bUGthdC6XXctdSOy2pS9z7617LtWIuKf3sAEAraIWMs7jzCwAAgNpg8wsAAIDaYPMLAACA2mDzCwAAgNoYFwlvBw8erMR27dplx7rErlmzZtmx06dPr8RcUlcp0cslcLnWwpJPUpo9e7Yd65LQSi1pXXKaS0YqtXh27ZxLyVOu1W13d7cd61o3u2REySeAuWTCEnd+58yZY8e6c+beb8kn2JWuO3eNunbBkk/sctdNKfHQveftJNeVriX3s1O6bk6dOlWJuQS9dlqLv/zyy3YsAADt4M4vAAAAaoPNLwAAAGqDzS8AAABqg80vAAAAaoPNLwAAAGpjXFR7cBn2+/fvt2NdK2QXK3FZ+6VWuS7DvtQ61rX7LbVzddUHDh06ZMe61sulihPOnj17KrFSVQZ3bKVKCZMnT67ESi2pXTUNVyXAtXIuxUvzcq+1YMECO9Y9x+WXX27HunNZqljhKh1ccEH1/1NLbbXd+1iq9uDipeonri126X1083VtpkstvF3lkFI7ZwAYKNfyWKLt8XjHnV8AAADUBptfAAAA1AabXwAAANQGm18AAADUxnkT3iJioaQ/kzRXUkq6NzM/HREzJH1eUq+k7ZLem5mj0n/UJayVWrS6ZKIZM2bYsa79rGuxWmpT6+ZValns2uqWWuW6saXkuNLrneuVV16xcZcA5pLzJP/elI7BJZGV2kS7uZ08ebISK7X7dXHXTro0r5kzZ9qxe/furcQWL15sx7rErpdeesmOdQmJ7hotJVq200q5nfbGrm1yKeHNJae5RLzS4931XLo+8KM6Yc0GgNHUyp3f05J+PTOvknSDpF+JiKskfVTSY5m5TNJjza8BAKOLNRsA+nHezW9m7snMJ5r/Pippo6QFku6Q9EBz2AOS3j1McwQAtIg1GwD611ad34jolXStpMclzc3Ms0Vg96rxKzb3mDsl3TmIOQIABmCwa/YkVetxA0CnaznhLSKmSPqSpA9n5pG+38tGRfpqVfrG9+7NzFWZuWpQMwUAtGwo1uwuVRuTAECna+nOb0R0qbGI/nlm/mUzvC8i5mXmnoiYJ8m3VBsBroNXKYlt0aJFlVip+5Xrmua6X7luY5JPSCol+LjuWbt377ZjXUJSqbuZe16nt7fXxl2S06lTp+zYrVu3VmKlhLdt27ZVYkuXLrVjXec4lwRX6nLnEg9LyVMuWcwlhZWUuqO5ZMBSwpubgzvn06dPt4933dFKCW+uW18pedJ1pCt1Xdu+fXsl5pIUSz+n7vy0cx7qbqyv2QCGn+tSV+poVzfnvfMbESHpPkkbM/P3+3zrK5I+0Pz3ByR9eeinBwBoB2s2APSvlTu/b5H0fklPR8T6ZuxuSZ+Q9IWI+KCkFyS9d1hmCABoB2s2APTjvJvfzPyupCh8+21DOx0AwGCwZgNA/+jwBgAAgNpg8wsAAIDaaKvO71hVaj/ruNbApda+r776aiXmsuPdOMlXWii14HXZ7aWqCq6F7pEjR8xIadOmTZVYOxUr3PtVag3c09NTiZVaIW/YsKESc1UdSs/hqmbs3++T192xld6vjRs3VmKu5bEkLVmypBJzbYyl9tpEu7FuvkePHrWPd5Uh2qlI0k6r6xJXBcLNq53W4O5aBIDh0E5VBFdVYawqzbVuVSC48wsAAIDaYPMLAACA2mDzCwAAgNpg8wsAAIDaGBcJb7Nnz67ESq2BXaLVG2+8Yce6hLNjx45VYhMnTrSPd8ltpQQf1zp28uTJdqxLSHLJRJK0evVqGz/Xzp07W467uUrSNddcU4mVWuXecMMNLT/v3/zN31RirtVtO62FS62Q3XxLbYhd8uNll11mx7pkMXfdSj7xz7UyLp0zlxhWauHtXqvUKtsl3ZUSOF0inTsPpaROp3SNA8BoajVZrJMS48Y77vwCAACgNtj8AgAAoDbY/AIAAKA22PwCAACgNtj8AgAAoDbGRbWHHTt2VGKl7HaXyV7KOHcZ665aQ3d3t328qz7g2r6WnsO1R5Z81n2pfa1rE+sqVlxwgf//oOeff74S+/73v2/HuqoMpQoOV199dSVWql6waNGiSsxVZShVe3Dtp9tpQ1w6D64KRGmsm9u+ffvsWDe3dtr9uqoKmWnHuueYM2eOHeuO1/2MSP4YNm/e3NLrS/7nrFSVBQCAdnDnFwAAALXB5hcAAAC1weYXAAAAtcHmFwAAALVx3oS3iFgo6c8kzZWUku7NzE9HxG9L+peSzvZ4vTszvz5cE+3P9u3bK7GFCxfasa4tbin5ySX+uGSzUqLV/PnzKzGXjCSVE6Ucl5T1wx/+0I597rnnKjGXPFVKmHMJgu49kPwxlFo/uxbN119/vR177bXXVmJf/epXK7FHHnmk5Xldd911dqx7b0tJWcePH6/ESglgu3btqsRcIl5pDk6p3a97fETYsS75sXS87hopJdKtXLmyEnPtuktJfy5htdQqGz+qE9ZsABhNrVR7OC3p1zPziYiYKmldRDza/N49mfmp4ZseAKBNrNkA0I/zbn4zc4+kPc1/H42IjZIWDPfEAADtY80GgP619ZnfiOiVdK2kx5uhX42IpyLi/oio/i698Zg7I2JtRKwd3FQBAO0Y7Jp9SidHaqoAMGJa3vxGxBRJX5L04cw8IumPJC2VtFKNuwy/5x6Xmfdm5qrMXDX46QIAWjEUa3aX/Gf2AaCTtbT5jYguNRbRP8/Mv5SkzNyXmWcy8w1JfyJp9fBNEwDQKtZsAChrpdpDSLpP0sbM/P0+8XnNz5ZJ0nskPTM8Uzw/1/a0nZbFriqD5FvzusoBpVbKrp2ry66XfNZ7qVKCy+bv7e21YydNmlSJuaz5bdu22cefPFn9tWcp695VzXAVESR/bNOmTbNjt27dWom5Ch+bNm2yj1+2bFklduDAATNSeuKJJyqxa665xo51FQ3cvCRf2WHevHl2rKu24Obrzq3kq0CU3tspU6ZUYqWKJK76Sakyhauq4ipsuPdb8q2US1VZ8KM6Yc0G6uin568c7Snokd3rR3sKY0Ir1R7eIun9kp6OiPXN2N2S3hcRK9UopbNd0oeGYX4AgPawZgNAP1qp9vBdSa5IKPUhAWCMYc0GgP7R4Q0AAAC1weYXAAAAtdHKZ37HvLe//e2V2IwZM+zYF154oRJzSV2SbxnsxpZatLp2sJdffrkd6xLASq1yXYJeKfnJzc0lt7nkPEmaPn16JVZqf+u41sSST/Z6+umn7ViXYOfO+c///M/bx7vExd27d9uxH//4xyuxUutpd41t2LDBjnXthUvJgC6B0iVwnj592j7ecS2tJX9+S8/rrsfSNepaYLukvwsu8P//7RLxSu2gAQCtGQtJd2MBd34BAABQG2x+AQAAUBtsfgEAAFAbbH4BAABQG2x+AQAAUBvjotqDqz7gqjpI0qFDhyqxUqtbV9nBtY5985vfbB/vWsqWXstl85fa37oqAY8//rgd++STT9r4uUptal3WfqmCw6pVqyqxFStW2LHuvenq6rJjZ86c2dJYV1FBkpYuXVqJPffcc3bse9/73kpszZo1duxDDz1UiZVaA3d3d1die/bsqQ6UtGDBgkrMtfB2Mcm3IS614HZVL0qVIVzVC9eGWPJVIFxlh1LbZXdspcoQAAC0g/+aAAAAoDbY/AIAAKA22PwCAACgNtj8AgAAoDbGRcKba4vrEtMkadasWZXYwoUL7VjXMriUZOS4hLmdO3fasS5x6LLLLrNj9+7dW4mVEq1c+1qXLNbb22sff/3111diPT09dqxrQzxhwgQ71rWvveiii+zYY8eOtfRapRbPzuTJk238/e9/fyX2kz/5k3bs/fffX4nt2rXLjnVtpkutgUvtlM/l3kPJJ5GVfh5cvJRY5hJLS+fXjXXPW2qlnJmV2NGjR+1YAADawZ1fAAAA1AabXwAAANQGm18AAADUBptfAAAA1MZ5E94iYpKk70ia2Bz/YGb+VkQslvQ5STMlrZP0/sz0GTzDzCVluYQZyScZueQpSXr11VcrMZd0U+rU5RLLSslELl7qynXjjTdWYqWuXI5LUiolLrnkqVJXL5eAdeGF/hJzyYAuJvkkQ9fNrXQMLmGu9N46LklSku6+++5K7L777rNjv/jFL1ZipYQ311XPdYgrJfi5a//qq6+2Y92x7dixw451SZWu26Dkk/amTp1aiZWSHN352bJlix2LH9UJazYAjKZW7vyelHRLZr5Z0kpJt0fEDZI+KemezHyTpJclfXDYZgkAaBVrNgD047yb32w4e+usq/knJd0i6cFm/AFJ7x6OCQIAWseaDQD9a+kzvxExISLWS9ov6VFJWyQdzsyzRTp3SlpQeOydEbE2ItYOwXwBAOcxVGv2KfmPIgFAJ2tp85uZZzJzpaQeSaslLW/1BTLz3sxclZmrBjZFAEA7hmrN7tLE4ZoiAIyatqo9ZOZhSWsk3SipOyLOZjP1SPKtrQAAo4I1GwCqWqn2MFvSqcw8HBEXS7pVjcSJNZJ+Vo3s4Q9I+vJwTrQ/mzZtqsRKbYRddnop49xlwrt2sK51reSrRSxevNiOXbRoUSV2zTXX2LGuusUrr7zS8hxclYCtW7fax586daoSmzjR3w1y702pUoKrmuEqOEi+WoOrAOEqIki+4kSpVa6r/FEaO2/evErsIx/5iB3rKlE8/PDDduyCBdXfRrvzWKpo4q6F0jlzSlU33POWKmy4Ns+uSkjp+pg/f34l5t5vSXrxxRdtvK46Yc0GgNF03s2vpHmSHoiICWrcKf5CZn4tIn4o6XMR8R8lPSnJ13gCAIwk1mwA6Md5N7+Z+ZSka018qxqfJQMAjBGs2QDQPzq8AQAAoDbY/AIAAKA2WvnM75jnkmvOnDljx7qkqssuu8yOde1nXQLY8uW+ipBrDVxK8Jk+fXolVkrwcW1mXUzyiVKulXLp/XLHMHfuXDvWJT+VnvfgwYM27syZM6cSaycxzbXVnTx5sh3rkshccp3k3/Oenh47dsWKFZVYKeHNJZadPn26EislsblrtJTE1k7ioZuXu74kn9jprvHSvFwS6f79++1YAADawZ1fAAAA1AabXwAAANQGm18AAADUBptfAAAA1AabXwAAANTGuKj2cOWVV1ZipSx0VyXAVT+QpO3bt1dil1xySSV222232ce7KgUuE1+SLr300krMtYMtxd28JF/VwLUhLrV4LlWRaFXpvXXHW3otVzHCHcOJEyfs4121BtdCuF2uMkSp4oQ7D+5alKQpU6a09LxunCTdeuutLc9r9+7dlZir8CFJ1113XSW2ZcsWO/bQoUOVmDvnpffAVTrp7e21Y++55x4bBwDA4c4vAAAAaoPNLwAAAGqDzS8AAABqg80vAAAAamNcJLy5Nq+lNsIuWWzdunV2rGtvvHTp0krMtfUtKSXiuWSvSZMm2bEu2avUgtcl2Lk2s6U2xMeOHavESsfrkpeOHDlix7oWyaWxLu7OuUtAk/y1UDoG9z6W2v26JMP58+fbsV1dXZVY6fwuWbKkEvvud79bic2ePds+fufOnZWYu5YlP9+bbrrJjnUJa9/+9rft2G9+85uVmLsWS0mKTilZFACAdnDnFwAAALXB5hcAAAC1weYXAAAAtcHmFwAAALVx3s1vREyKiL+NiB9ExIaI+J1m/DMRsS0i1jf/rBz22QIA+sWaDQD9a6Xaw0lJt2TmsYjokvTdiPhG83u/kZkPDt/0WtNqK1XJZ/677HpJ2rFjRyV28uTJSqxUKWHz5s2VWKkdrKs+sHz5cjvWtebt7u62Y111iz179lRirp2s5KsylDL03XveTlvdRYsW2bHuGI4fP16JlVo0u/Ozb98+O9adB9eaWPLHUGrX++ijj1ZiPT09duzUqVMrMXfduteXpAMHDlRiL774oh27bNmySmzVqlV27KZNmyqxUrUHx1W8KFXScNfjwoULW36tmhvzazYAjKbzbn6zUT/qbL2rruYfX1MKADCqWLMBoH8tfeY3IiZExHpJ+yU9mpmPN7/1nyLiqYi4JyKqhVcbj70zItZGxNqhmTIAoD9DtWafUvU3XQDQ6Vra/GbmmcxcKalH0uqIuFrSxyQtl/QPJM2Q9JuFx96bmasy0/8uFQAwpIZqze6S3R8DQEdrq9pDZh6WtEbS7Zm5JxtOSvofklYPw/wAAAPEmg0AVVFqCfv3AyJmSzqVmYcj4mJJfyXpk5LWZeaeaGSs3CPptcz86Hmei8+dAehYmekz9MaQoVyzp8WMvD7eNvyTBoBh8K18cJ375EEr1R7mSXogIiaocaf4C5n5tYj4dnORDUnrJf2roZwwAGBAWLMBoB+tVHt4StK1Jn7LsMwIADBgrNkA0D86vAEAAKA22PwCAACgNtj8AgAAoDbY/AIAAKA22PwCAACgNtj8AgAAoDbY/AIAAKA22PwCAACgNtj8AgAAoDbY/AIAAKA22PwCAACgNtj8AgAAoDbY/AIAAKA22PwCAACgNtj8AgAAoDbY/AIAAKA22PwCAACgNtj8AgAAoDbY/AIAAKA2IjNH7sUiDkh6ofnlLEkvjdiLjxyOq/OM12PjuIbW5Zk5exRed9SwZne88XpsHFfnGVPr9ohufn/khSPWZuaqUXnxYcRxdZ7xemwcF4bSeH3fx+txSeP32DiuzjPWjo2PPQAAAKA22PwCAACgNkZz83vvKL72cOK4Os94PTaOC0NpvL7v4/W4pPF7bBxX5xlTxzZqn/kFAAAARhofewAAAEBtsPkFAABAbYz45jcibo+I5yLi+Yj46Ei//lCKiPsjYn9EPNMnNiMiHo2Izc2/Lx3NOQ5ERCyMiDUR8cOI2BARv9aMd/SxRcSkiPjbiPhB87h+pxlfHBGPN6/Jz0fERaM914GIiAkR8WREfK359Xg5ru0R8XRErI+Itc1YR1+LnYQ1e+xjze7YtY01e5SM6OY3IiZI+m+S3iHpKknvi4irRnIOQ+wzkm4/J/ZRSY9l5jJJjzW/7jSnJf16Zl4l6QZJv9I8T51+bCcl3ZKZb5a0UtLtEXGDpE9Kuicz3yTpZUkfHL0pDsqvSdrY5+vxclyS9NbMXNmnTmSnX4sdgTW7Y7BmdybW7FEy0nd+V0t6PjO3Zubrkj4n6Y4RnsOQyczvSDp0TvgOSQ80//2ApHeP5JyGQmbuycwnmv8+qsYP5wJ1+LFlw7Hml13NPynpFkkPNuMdd1ySFBE9kv6RpD9tfh0aB8fVj46+FjsIa3YHYM3urOOSWLM1ysc20pvfBZJ29Pl6ZzM2nszNzD3Nf++VNHc0JzNYEdEr6VpJj2scHFvz10zrJe2X9KikLZIOZ+bp5pBOvSb/QNJdkt5ofj1T4+O4pMZ/7P4qItZFxJ3NWMdfix2CNbvDsGZ3jD8Qa/aouXA0X3y8y8yMiI6tJRcRUyR9SdKHM/NI439MGzr12DLzjKSVEdEt6SFJy0d3RoMXEe+StD8z10XEzaM8neFwU2buiog5kh6NiGf7frNTr0WMPZ1+LbFmdwbW7NG/Fkf6zu8uSQv7fN3TjI0n+yJiniQ1/94/yvMZkIjoUmMR/fPM/MtmeFwcmyRl5mFJayTdKKk7Is7+j2AnXpNvkfSPI2K7Gr+WvkXSp9X5xyVJysxdzb/3q/Efv9UaR9fiGMea3SFYszsKa/YoX4sjvfn9O0nLmhmNF0n6OUlfGeE5DLevSPpA898fkPTlUZzLgDQ/e3SfpI2Z+ft9vtXRxxYRs5t3DxQRF0u6VY3Pxq2R9LPNYR13XJn5sczsycxeNX6mvp2Z/0IdflySFBGXRMTUs/+WdJukZ9Th12IHYc3uAKzZnXVcrNlj4Ngyc0T/SHqnpE1qfG7n34/06w/xsXxW0h5Jp9T4fM4H1fjczmOSNkv6lqQZoz3PARzXTWp8ZucpSeubf97Z6ccm6cclPdk8rmckfbwZXyLpbyU9L+mLkiaO9lwHcYw3S/raeDmu5jH8oPlnw9k1o9OvxU76w5o99v+wZnfe2tbnGFmzR+EP7Y0BAABQG3R4AwAAQG2w+QUAAEBtsPkFAABAbbD5BQAAQG2w+QUAAEBtsPkFAABAbbD5BQAAQG38Pw2iyrqz7UrxAAAAAElFTkSuQmCC\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -413,7 +413,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "metadata": {}, "outputs": [], "source": [ @@ -472,7 +472,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "metadata": {}, "outputs": [], "source": [ @@ -555,7 +555,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "metadata": {}, "outputs": [], "source": [ @@ -577,12 +577,15 @@ " )\n", "\n", " def _compute_loss(preds, label):\n", - " preds = torch.unbind(preds, dim=1)\n", + " if len(preds.size()) - len(targets.size()) == 1:\n", + " # In deep supervision mode, The shape of the preds is\n", + " # in the form of (Batch, deep_supr_num, C, H, W, D),\n", + " # thus they should be unbinded into a list of feature\n", + " # maps each has the shape (Batch, C, H, W, D)\n", + " preds = torch.unbind(preds, dim=1)\n", " return sum(\n", - " [\n", - " 0.5 ** i * self.loss_function.forward(p, label)\n", - " for i, p in enumerate(preds)\n", - " ]\n", + " 0.5 ** i * self.loss_function.forward(p, label)\n", + " for i, p in enumerate(preds)\n", " )\n", "\n", " self.network.train()\n", @@ -631,7 +634,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "metadata": {}, "outputs": [], "source": [