diff --git a/README.md b/README.md index de77500..0cb371f 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,92 @@ You are free to tweak the hyperparameters and the network architecture to see ho I used the [MNIST dataset](https://en.wikipedia.org/wiki/MNIST_database) to test the library, but you can use any dataset you want. +## 🚀 Quick examples (more [here](examples/)) + +### Binary Classification + +```python +from neuralnetlib.model import Model +from neuralnetlib.layers import Input, Dense +from neuralnetlib.activations import Sigmoid +from neuralnetlib.losses import BinaryCrossentropy +from neuralnetlib.optimizers import SGD +from neuralnetlib.metrics import accuracy_score + +# ... Preprocess x_train, y_train, x_test, y_test if necessary (you can use neuralnetlib.preprocess and neuralnetlib.utils) + +# Create a model +model = Model() +model.add(Input(10)) # 10 features +model.add(Dense(8)) +model.add(Dense(1)) +model.add(Activation(Sigmoid())) # many way to tell the model which Activation Function you'd like, see the next example + +# Compile the model +model.compile(loss_function='bce', optimizer='sgd') + +# Train the model +model.fit(X_train, y_train, epochs=10, batch_size=32, metrics=[accuracy_score]) +``` + +### Multiclass Classification + +```python +from neuralnetlib.activations import Softmax +from neuralnetlib.losses import CategoricalCrossentropy +from neuralnetlib.optimizers import Adam +from neuralnetlib.metrics import accuracy_score + +# ... Preprocess x_train, y_train, x_test, y_test if necessary (you can use neuralnetlib.preprocess and neuralnetlib.utils) + +# Create and compile a model +model = Model() +model.add(Input(28, 28, 1)) # For example, MNIST images +model.add(Conv2D(32, kernel_size=3, padding='same')) +model.add(Activation('relu')) # activation supports both str... +model.add(BatchNormalization()) +model.add(MaxPooling2D(pool_size=2)) +model.add(Dense(64, activation='relu')) +model.add(Dense(10, activation=Softmax())) # ... and ActivationFunction objects +model.compile(loss_function='categorical_crossentropy', optimizer=Adam()) + + +model.compile(loss_function='categorical_crossentropy', optimizer=Adam()) # same for loss_function and optimizer + +# Train the model +model.fit(X_train, y_train_ohe, epochs=5, metrics=[accuracy_score]) +``` + +### Regression + +```python +from neuralnetlib.losses import MeanSquaredError +from neuralnetlib.metrics import accuracy_score + +# ... Preprocess x_train, y_train, x_test, y_test if necessary (you can use neuralnetlib.preprocess and neuralnetlib.utils) + +# Create and compile a model +model = Model() +model.add(Input(13)) +model.add(Dense(64, activation='leakyrelu')) +model.add(Dense(1), activation="linear") + +model.compile(loss_function="mse", optimizer='adam') # you can either put acronyms or full name + +# Train the model +model.fit(X_train, y_train, epochs=100, batch_size=128, metrics=[accuracy_score]) +``` + +You can also save and load models: + +```python +# Save a model +model.save('my_model.json') + +# Load a model +model = Model.load('my_model.json') +``` + ## 📜 Output of the example file Here is an example of a model training on the mnist using the library diff --git a/examples/cnn-classification/simple_cnn_classification_mnist.ipynb b/examples/cnn-classification/simple_cnn_classification_mnist.ipynb index 83c840d..5047135 100644 --- a/examples/cnn-classification/simple_cnn_classification_mnist.ipynb +++ b/examples/cnn-classification/simple_cnn_classification_mnist.ipynb @@ -104,7 +104,9 @@ "outputs": [ { "data": { - "text/plain": "\"\\n Side note: if you set the following:\\n \\n - filters to 8 and 16 (in this order)\\n - padding of the Conv2D layers to 'same'\\n - weights initialization to 'he'\\n \\n you'll get an accuracy of ~0.9975 which is actually pretty cool\\n\"" + "text/plain": [ + "\"\\n Side note: if you set the following:\\n \\n - filters to 8 and 16 (in this order)\\n - padding of the Conv2D layers to 'same'\\n - weights initialization to 'he'\\n \\n you'll get an accuracy of ~0.9975 which is actually pretty cool\\n\"" + ] }, "execution_count": 4, "metadata": {}, @@ -123,8 +125,7 @@ "model.add(Flatten())\n", "model.add(Dense(64, random_state=42))\n", "model.add(Activation(ReLU()))\n", - "model.add(Dense(10, random_state=42))\n", - "model.add(Activation(Softmax()))\n", + "model.add(Dense(10, random_state=42, activation=\"softmax\")) # Yeah, you can also use strings for the activation functions, or directly the class\n", "\n", "\"\"\"\n", " Side note: if you set the following:\n", @@ -180,7 +181,7 @@ } ], "source": [ - "model.compile(loss_function=CategoricalCrossentropy(), optimizer=Adam())\n", + "model.compile(loss_function=\"cce\", optimizer=\"adam\") # You can also use strings for the loss function and the optimizer\n", "\n", "model.summary()" ] @@ -327,8 +328,10 @@ "outputs": [ { "data": { - "text/plain": "
", - "image/png": "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tWtuuuuuuxQUFKSxY8daVsW8nZskaeXKla5Y3m20LuRpbsrNzdX06dO9ek7e3n5Ekj744AMNGjRIvXv31sSJE70a35NSXZFr06aNUlJS9PLLL+vnn39Wp06dVKFCBW3ZskVz5szRlClT1KNHD8XExOjZZ5/Vyy+/rG7duikpKUlr167VZ599VuiW3KCgIKWmpurOO+9U48aN9eijj6pGjRravHmzNm7c6Lp4smnTppKkQYMGqXPnzipXrpySk5NLLMeibvFesmSJDh48eNHVo7xt6DNmzCjwLta7d+/W8uXLNWjQII/fDwkJUefOnTVnzhz97W9/U7t27fTQQw/pb3/7m7Zs2aI77rhDTqdTq1atUrt27TRgwADX+Vu2bJkmTpyouLg4JSQk6KabblJycrKee+453X333Ro0aJBOnDih1NRU1atXTz/99JPrcTt16qTg4GDdeeedSklJ0bFjx/T3v/9dsbGx2rNnT6Hnp6i3H9mwYYPWr1+v559/3ucXxgKwN+amwuemyZMna/r06brlllsUHh6u9PR0t+/ffffdriI0MzNT7dq106hRowr8hIrTp09r7ty56tixY4E3Tu7evbumTJmiffv26aqrrtKIESM0btw43XbbbbrnnnsUEhKiH374QXFxca5LkJo2barU1FSNHz9eV111lWJjY9W+fXt16tRJtWvX1mOPPaahQ4eqXLlyeu+99xQTE6MdO3a4HrNly5aKjo5Wnz59NGjQIDkcDs2cOdPrucbb2498//33evjhh1WlShXdfvvtlrfMW7ZsabmZfYGKssU1b5vuDz/8cNF+ffr0MREREQV+/5133jFNmzY1YWFhJioqyjRs2NAMGzbM7N6929Xn3LlzZsyYMaZGjRomLCzMtG3b1mzYsMHEx8dfdIt3nq+++sp07NjRREVFmYiICNOoUSMzdepU1/fPnj1rBg4caGJiYozD4bBsS/ZljsYUfYt3cnKyqVChgjl48GCBfaZOnWokmcWLFxfY54033jCSzBdffFFgn7S0NCPJLFiwwBhz/ty89tpr5tprrzXBwcEmJibGdOnSxaxZs8Z1zObNm03r1q1NWFiYZUv70qVLTYMGDUxwcLC55pprTHp6uset35988olp1KiRCQ0NNXXq1DGvvPKKee+99yxbt31x+5Hnn3/eSDLr16/3+hgA9sDcVPJzU95t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nOfm5urgwYNFOp8+2ezgrcTERC1btky33nrrRavN+Ph4Sef/Aqlbt64rvn//fsvuHE+PIUkbNmxQhw4dCuxX0A+1NHIsbStWrNCuXbs0duxYy4Wqhw8f1hNPPKH58+frwQcfVGJiopxOp3755Rc1bty4wDELOn95f/EdOXLE7e2DvL8S82RmZurgwYP6+OOP3e6zlLe7DABKC3NT4Ro3bqx//OMf2rRpk9uCwHfffef6flF9/PHHOnXqlFJTU1W1alW37/36668aOXKkVq9erVatWikxMVFLlizRoUOHLroqd7G5ydONhvPPTQsXLtTp06f1ySefqHbt2q543lvXvpJ3vn788UclJSW54j/++KOcTmeRzmep3pOhZ8+eOnfunMaNG2f53tmzZ10nuUOHDqpQoYKmTp3q9l7z5MmTC32MJk2aKCEhQZMnT7b80C4cK+++Qfn7lFSORbn9iK/lva06dOhQ9ejRw+3r8ccf19VXX+36K++uu+5SUFCQxo4da/nLI//58/SfIu+X1cqVK12xvO3gF8q739CFY+bm5mr69OlePaei3H4EAC6GuanwuelPf/qTKlSo4PY72hijt956S1deeaXl7gfeSE9PV926ddWvXz/L3PTss88qMjLSNTfde++9MsZ4vFmyt3NTdna21q9f74rt2bNH8+bNc+vnaW7Kzs7WjBkzvHpO3t5+pH379qpcubLltiapqakKDw93Xe7kjVJdkWvTpo1SUlL08ssv6+eff1anTp1UoUIFbdmyRXPmzNGUKVPUo0cPxcTE6Nlnn9XLL7+sbt26KSkpSWvXrtVnn31mqdrzCwoKUmpqqu688041btxYjz76qGrUqKHNmzdr48aNrgv7mzZtKkkaNGiQOnfurHLlyik5ObnEcizK7Udmzpyp33//3fXe/MqVK1037n3ooYdcf3FlZmaqXbt2GjVqVIEf6XL69GnNnTtXHTt2LPDmlN27d9eUKVO0b98+XXXVVRoxYoTGjRun2267Tffcc49CQkL0ww8/KC4uTi+//LLr/KWmpmr8+PG66qqrFBsbq/bt26tTp06qXbu2HnvsMQ0dOlTlypXTe++9p5iYGO3YscP1mC1btlR0dLT69OmjQYMGyeFwaObMmV5dJCoV7fYjK1eudBWW+/fv1/Hjx13ns3Xr1kW+8zqAywtzU+FzU82aNTVkyBC9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5Z/3PvnkE0ufbdu2FTVF22JFDgAAwKYo5AAAAGyKQg4AAMCmKOQAAABsis0OAeLmm292az/66KOWPmvXrrXEPv/88xLLCQBgbz/++KMl9uc//9kS8+XGBm942qDQu3dvS6x58+alkY6tsSIHAABgUxRyAAAANkUhBwAAYFNcIxcgOnTo4NauXLmypc/ixYstsVOnTpVYTgCAwBUUVPhazE033VQKmRSdw+GwxDw9H2+e4+jRoy2xhx56qFh52RErcgAAADZFIQcAAGBTFHIAAAA2RSEHAABgU2x2CBA33HCDW9sYY+nz0UcflVY6AIAA0q9fP0vM6XT6IRPfuPPOOy2xG2+80RLz9BzzxzxtdihLWJEDAACwKQo5AAAAm6KQAwAAsCkKOQAAAJtis4MfVK9e3RK77bbb3Nq//vqrpc+8efNKLCcAQODytDkgUMXExFhi119/vVt7+PDhxR5///79bu0zZ84Ue6zLAStyAAAANkUhBwAAYFMUcgAAADZFIQcAAGBTbHbwg0ceecQSi42NdWt/9tlnpZQNAAC+M2LECEusf//+xRorKyvLEuvTp49be8eOHcUa+3LBihwAAIBNUcgBAADYFIUcAACATXGNnB/Ex8cX2ufw4cOlkAkAAMW3aNEiS+yaa67x2fi//PKLJfbVV1/5bPzLAStyAAAANkUhBwAAYFMUcgAAADZFIQcAAGBTbHbwg27duhXaZ+HChaWQCQDADhwOhyUWFFT4WkyXLl28Gv+dd96xxOLi4go9zlMOTqfTq8f0xp133umzsS5XrMgBAADYFIUcAACATVHIAQAA2BSFHAAAgE2x2aGEtWrVyhKrXr26HzIBANhVamqqJfbqq68Wetynn35qiXm7GaG4mxaKe9xbb71VrOPKOlbkAAAAbIpCDgAAwKYo5AAAAGyKQg4AAMCm2OxQwu6++25LrFy5cpbY2rVr3dorV64ssZwAAPby8ccfW2JDhw61xGJiYkojnYvav3+/JbZp0ya39hNPPGHps2fPnhLL6XLGihwAAIBNUcgBAADYFIUcAACATXGNnA+Fh4dbYklJSV4d+9FHH7m1z50755OcAAD29/vvv1tiycnJlthdd93l1h48eHBJpVSgl156yRJ78803Sz2PsoIVOQAAAJuikAMAALApCjkAAACbopADAACwKTY7+NCZM2csscOHD1ti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kBB17txZc+bM0d/+9je1a9dODz30kP72t79py5YtuuOOO+R0OrVq1Sq1a9dOAwYMcJ2/ZcuWaeLEiYqLi1NCQoJuuukmJScn67nnntPdd9+tQYMG6cSJE0pNTVW9evX0008/uR63U6dOCg4O1p133qmUlBQdO3ZMf//73xUbG6s9e/YUeI7yeHv7EU/nM28FrkuXLoW+ngBc/pibvLv9yMyZM/X777+7rmleuXKlxo8fL0l66KGHXKuBmZmZateunUaNGlXgx3idPn1ac+fOVceOHRUaGuqxT/fu3TVlyhTt27dPV111lUaMGKFx48bptttu0z333KOQkBD98MMPiouL08svv+w6f6mpqRo/fryuuuoqxcbGqn379urUqZNq166txx57TEOHDlW5cuX03nvvKSYmRjt27HA9ZsuWLRUdHa0+ffpo0KBBcjgcmjlzpteb64py+5GVK1e6Csv9+/fr+PHjrvPZunVr7z8VpChbXPO26f7www8X7VfY7TLeeecd07RpUxMWFmaioqJMw4YNzbBhw8zu3btdfc6dO2fGjBljatSoYcLCwkzbtm3Nhg0bTHx8/EW3eOf56quvTMeOHU1UVJSJiIgwjRo1MlOnTnV9/+zZs2bgwIEmJibGOBwOy7ZkX+ZoTPG2eOcp6HxOnTrVSDKLFy8u8Ng33njDSDJffPFFgX3S0tKMJLNgwQJjzPlz89prr5lrr73WBAcHm5iYGNOlSxezZs0a1zGbN282rVu3NmFhYZYt7UuXLjUNGjQwwcHB5pprrjHp6eket35/8sknplGjRiY0NNTUqVPHvPLKK+a9996zbN2+1NuP5MftR4DLC3NT6cxNeb93PX1d+DwXLlxoJJm33nqrwLHmzp1rJJl33323wD6ZmZlGkpkyZYor9t5775kbb7zRhISEmOjoaNOmTRvz+eefu77/xx9/mK5du5qoqCgjyW3uWLNmjbnppptMcHCwqV27tpk4caLH24+sXr3a3HzzzSYsLMzExcWZYcOGmSVLllie56XefiRvLvL0VZRbuziM8bLMRMDp2bOnsrKy9P333/s7FQAAJEnDhg3T7NmztXXrVoWEhPg7ncteqb61Ct8xxigzM7NEPvMOAIDiWr58uf7yl79QxJUSVuQAAABsqlR3rQIAAMB3KOQAAABsikIOAADApijkAAAAbCqgdq06nU7t3r1bUVFRPvncT9ibMUZHjx5VXFyc5bMRAaA0MC8hv0CbmwKqkNu9e7dq1arl7zQQYHbu3KmaNWv6Ow0AZRDzEgoSKHOT/0vJC0RFRfk7BQQgXhcA/IXfPyhIoLw2AqqQY9kanvC6AOAv/P5BQQLltRFQhRwAAAC8RyEHAABgUxRyAAAANkUhBwAAYFMUcgAAADZFIQcAAGBTFHIAAAA2RSEHAABgUxRyAAAANkUhBwAAYFMUcgAAADZFIQcAAGBTFHIAAAA2RSEHAABgUxRyAAAANkUhBwAAYFMUcgAAADZFIQcAAGBTFHIAAAA2RSEHAABgU+X9ncDlpEmTJpbYxx9/bInVqVOnFLK5uE6dOllimzZtssR27txZGukAAC4Td955pyX2ySefuLUHDBhg6fPWW29ZYufOnfNdYpcpVuQAAABsikIOAADApijkAAAAbIpr5Hyoc+fOllhISIgfMimcp2sY/vznP1tiycnJpZEOAMCGqlSpYolNnz690OOmTZtmib333nuW2MmTJ4uXWBnCihwAAIBNUcgBAADYFIUcAACATVHIAQAA2BSbHS5B+fLupy8pKclPmRTdmjVrLLFnnnnGEouIiHBrHz9+vMRyAgDYS+vWrS2xmjVrFnrc7NmzLbFTp075JKeyhhU5AAAAm6KQAwAAsCkKOQAAAJuikAMAALApNjtcgnbt2rm1b7nlFkufV199tbTSKZLo6GhL7Prrr7fEwsPD3dpsdgCAssnTJxWNGDGiWGPNnDnTEjPGFGusso4VOQAAAJuikAMAALApCjkAAACbopADAACwKTY7eKlBgwaWWP47U2/bts3SZ8KECSWW06X405/+5O8UAAA20rBhQ0usadOmXh179uxZt/Znn33mk5zAihwAAIBtUcgBAADYFIUcAACATXGNnJdGjhxpiUVERLi177jjDkufY8eOlVhO3qpcubIl1qZNG0vM6XSWRjoAABu69957i33s0qVLfZgJLsSKHAAAgE1RyAEAANgUhRwAAIBNUcgBAADYFJsdPOjRo4cllpSUZIlt3brVrf3jjz+WWE6XYsSIEZaYp40NmZmZltiRI0dKICMAgN20bt3aq365ubmWmKd5CL7BihwAAIBNUcgBAADYFIUcAACATVHIAQAA2BSbHTy47777LLHw8HBLbPr06aWRTpHVqVPHrd27d29Ln3Pnzlli48ePt8TOnDnjs7wAAPbRsmXLi7YLcvz4cUvs559/9kVK8IAVOQAAAJuikAMAALApCjkAAACbopADAACwqTK/2aFSpUqW2M033+zVsampqb5OxyeeeOIJt3bVqlUtfTZt2mSJLV++vMRyAgDYS/PmzYt1XKDOjZcrVuQAAABsikIOAADApijkAAAAbKrMXyMXEhJiiV155ZWW2OzZs0sjHZ9ITEwstM+GDRtKIRMAgF01a9as0D5HjhyxxLhGrnSxIgcAAGBTFHIAAAA2RSEHAABgUxRyAAAANlXmNzscPXrUEvv5558tsUaNGllilStXdmsfOnTIZ3l5KzY21hLr0aNHocd99dVXJZEOAMCGWrVqZYn16tWr0OOys7MtsV27dvkkJ3iHFTkAAACbopADAACwKQo5AAAAm6KQAwAAsKkyv9nh5MmTlti2bdsssXvvvdcS+/e//+3Wnjhxos/yatCggSVWt25dS6xOnTqWmDGm0PGdTmex8gIAXH6qVKliiQUFFb7W8/nnn5dEOigCVuQAAABsikIOAADApijkAAAAbIpCDgAAwKbK/GYHT0aNGmWJORwOS6xr165u7dmzZ/sshwMHDlhinjYxVK1atVjjp6WlFes4AMDlx5tPBDpy5Igl9vbbb5dANigKVuQAAABsikIOAADApijkAAAAbMphvLl7bCnJyclRpUqV/J2G1xo3buzWvuqqq3w29kcffeRVv/fff98S6927d6HHlS9vn8sjs7OzVbFiRX+nAaAMstu85I2aNWtaYr///rsllv+GwBs2bLD0adiwoe8Ss5lAmZtYkQMAALApCjkAAACbopADAACwKQo5AAAAm7LPFe8B6Oeff75ouzT89ttvxTquQYMGlpinC1kBAJeXli1bWmL5NzZ4Mn/+/BLIBpeKFTkAAACbopADAACwKQo5AAAAm6KQAwAAsCk2O9icw+HwKpYfGxsAoGyqUqWKV/0OHDjg1p4yZUpJpINLxIocAACATVHIAQAA2BSFHAAAgE1RyAEAANgUmx1szhjjVQwAAEnq3LmzV/127Njh1s7Ozi6JdHCJWJEDAACwKQo5AAAAm6KQAwAAsCmukbO50NDQQvucPHmyFDIBAASaChUqWGKJiYleHXvq1Cm39pkzZ3ySE3yLFTkAAACbopADAACwKQo5AAAAm6KQAwAAsCk2O9jc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dll7q9QLOWOMeeedd0zTpk1NWFiYiYqKMg0bNjTDhg0zu3fvdvU5d+6cGTNmjKlRo4YJCwszbdu2NRs2bDDx8fGF/mcxxpivvvrKdOzY0URFRZmIiAjTqFEjtx/22bNnzcCBA01MTIxxOByWE+/LHI0pmUJu6tSpRpJZvHhxgce+8cYbRpL54osvCuyTlpZmJJkFCxYYY86fm9dee81ce+21Jjg42MTExJguXbqYNWvWuI7ZvHmzad26tQkLC7NsaV+6dKlp0KCBCQ4ONtdcc41JT0/3uMX7k08+MY0aNTKhoaGmTp065pVXXjHvvfcehRyAImFuKvm56dChQ+ZPf/qTqV69ugkODjYJCQnmueeesxRxxjA3eTs3JScnm1q1apng4GATFxdn+vXrZ/bu3evVsRdyGOPlPRwQcHr27KmsrCx9//33/k4FAABJzE2lrVQ3O8B3jDHKzMxUenq6v1MBAEASc5M/sCIHAABgU6W6axUAAAC+QyEHAABgUxRyAAAANkUhBwAAYFMBtWvV6XRq9+7dioqK8slnq8HejDE6evSo4uLiLJ+NCAClgXkJ+QXa3BRQhdzu3btVq1Ytf6eBALNz507VrFnT32kAKIOYl1CQQJmb/F9KXiAqKsrfKSAA8boA4C/8/kFBAuW1EVCFHMvW8ITXBQB/4fcPChIor42AKuQAAADgPQo5AAAAm6KQAwAAsCkKOQAAAJuikAMAALApCjkAAACbopADAACwKQo5AAAAm6KQAwAAsCkKOQAAAJuikAMAALApCjkAAACbopADAACwKQo5AAAAm6KQAwAAsCkKOQAAAJuikAMAALApCjkAAACbopADAACwKQo5AAAAm6KQAwAAsKny/k7AzmJjY93aH374oaXP119/bYm98847llhWVpbP8vKlSpUqubVbt25t6bN48WJL7MyZMyWWEwAAOI8VOQAAAJuikAMAALApCjkAAACb4ho5L0VHR1tiGzdudGvnv55Mkvbu3WuJ2eV6OElas2aNWzsmJsbSp2nTppbY1q1bfZcYAMBnKlasaIm9/PLLlliDBg3c2h06dLD04Xpo/2NFDgAAwKYo5AAAAGyKQg4AAMCmKOQAAABsis0OHlStWtUS++CDDyyxypUru7WnT59u6TNw4EDfJVbCRo4caYklJCS4tVNSUix92NgAAIGpd+/elthLL71k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0FCfj+svn3zyiX7//Xd9+umn6tq1qyteuXJljR07VuvWrSt2AWKM0axZs9SrVy9t375dGRkZxf7PYjf16tXTgw8+6O80AJQg5qaS89e//lVHjhzRhg0bdM0110iSHn/8cV177bV6+umntWbNmmKPXRbnprfffltbt27VF198ofbt20uSnnzySd188836v//7P/Xo0cPrPwJ8co1cXhLbt2+XJD3yyCOKjIzUtm3blJSUpKioKPXu3VvS+a3ckydPVv369RUaGqpq1aopJSVFhw8fdhvTGKPx48erZs2aCg8PV7t27bRx40bLYxd0HcJ3332npKQkRUdHKyIiQo0aNdKUKVNc+b355puS3N92y+PrHCVp27Zt2rZtW6HnMicnR5JUrVo1t3iNGjUkSWFhYYWOUZDVq1crKytLycnJSk5O1sqVK7Vr1y5LP6fTqSlTpqhhw4YKDQ1VTEyM7rjjDv3444+Szp+z48eP6/3333edu7zrLh555BHVqVPHMubo0aMtW/dnzJih9u3bKzY2ViEhIbr++uuVmprq1XPZsWOHNm/eXKTnf/LkSZ06dapIxwCwL+Ym381Nq1at0o033ugq4iQpPDxc3bt3108//aQtW7YUOkZByuLctGrVKsXExLheo9L54r9nz576448/tGLFCq8eTyrmilx+eS+CKlWquGJnz55V586d1apVK73++uuuZe2UlBSlpaXp0Ucf1aBBg7R9+3ZNmzZNa9eu1erVq1WhQgVJ0osvvqjx48crKSlJSUlJ+umnn9SpUyfl5uYWms/nn3+ubt26qUaNGq4ly02bNunTTz/V4MGDlZKSot27d+vzzz/XzJkzLceXRI633367JCkrK+uiubdu3VpBQUEaPHiw3njjDdWsWVPr16/XSy+9pLvuukvXXnttoc+/IBkZGUpMTFTz5s3VoEEDhYeHa/bs2Ro6dKhbv8cee0xpaWnq0qWL+vbtq7Nnz2rVqlX69ttv1axZM82cOVN9+/ZVixYt9MQTT0iSEhMTi5xPamqq6tevr+7du6t8+fJauHChnnrqKTmdTvXv3/+ixz788MNasWKF13fWTktL0/Tp02WM0XXXXaeRI0cWaekagP0wN/lubjp9+rSio6Mt8bzzt2bNGl199dWFngNPyuLcdPr0aY8LMxeez44dO3qXsCmCGTNmGElm2bJlZv/+/Wbn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Q4daYvk3NkjS//73P7f2vffea+nz/fffFz+5fDxtWqhVq5Yl9s9//tOtvWjRIkuf6Ohorx4z//OeOXOmpc+RI0e8GutywIocAACATVHIAQAA2BSFHAAAgE1RyAEAANhUmdvs0LJly0L7rF271hLbtWtXSaQDAIAbTxsIzp0759WxZ8+edWvfdNNNlj49evSwxK699tpCxz558qQldt1113kVy//pSNWqVSv08Qqyd+9et/b48eMtfc6cOVPs8e2GFTkAAACbopADAACwKQo5AAAAm3IYY4y/k8iTk5OjSpUqlehj7Nu3z61dpUoVS5/Tp09bYq+88ooltmDBArf2zz//fGnJwaPs7GxVrFjR32kAKINKY17KLywszBKbNWuWJdahQwdLLDw83K0dFGRdr/F22s9/XZ6na/d8yel0WmLz5s2zxAYNGuTW3rNnT4nldDGBMjexIgcAAGBTFHIAAAA2RSEHAABgUxRyAAAANlXmNjvkf7qeLq70Vv5j33rrLUufb7/91hKrXbu2JbZ161a39saNG73KoX79+pbYN99849a2+82MA+WCUgBljz82O3jriiuusMSef/55t/att95q6XPw4EFLbMeOHZZYSEiIW/uGG26w9GnRokVhaXrN0xw6fPhwS+zIkSM+e8xLEShzEytyAAAANkUhBwAAYFMUcgAAADZFIQcAAGBTZW6zw2uvvebWfuaZZ0r08fxh//79bu3MzExLn+Tk5FLK5tIFygWlAMqeQN7sUNr++c9/WmIPPvigV8cePXrUre1p7k1LS7PE8n+6RCAJlLmJFTkAAACbopADAACwKQo5AAAAm6KQAwAAsKkyt9mhXLlybu0bb7zR0mfWrFmWWPny5S2xWrVqubWDggKzLvb0Ix49erQlNn78+FLIpugC5YJSAGVPWd7sMGzYMLe2pznC09zoSe/evd3as2fPLn5iASJQ5qbArDwAAABQKAo5AAAAm6KQAwAAsCnv3ty+jOS/ueCPP/5o6VOvXj2vxrr99tvd2hUqVLD08XQtWvPmzb0a31ccDocl1rRp01LNAQAQuPr27WuJjRw50q3t7fVwGzdutMQ+/vjj4iWGQrEiBwAAYFMUcgAAADZFIQcAAGBTFHIAAAA2VeY2O/jSF198UWifxo0bW2KeNjucPXvWrT1jxgxLn7///e+W2JAhQyyxXr16FZoXAKBsatGihSX2xhtvWGKRkZGFjnXs2DFLrF+/fpbY6dOnvcwORcWKHAAAgE1RyAEAANgUhRwAAIBNUcgBAADYFJsdStjSpUstsZdeeskSy3/H7Mcff9zS56qrrrLE2rZtW6y8du3aVazjAAD2duedd1piUVFRhR53/PhxS6x79+6W2OrVq4uXGIqFFTkAAACbopADAACwKQo5AAAAm6KQAwAAsCmHMcb4O4k8OTk5qlSpkr/T8KmwsDBL7L333rPEevbs6bPHPHfunFv73//+t6XPgw8+aIl5upA1EGRnZ6tixYr+TgNAGWT3ecnTJoYDBw5YYhUqVCh0rHfeeccS8/QpDmVFoMxNrMgBAADYFIUcAACATVHIAQAA2BQ3BC5hJ0+etMSGDBliiUVGRrq1mzVrZukTGxtriWVlZVliM2fOdGuPHj364kkCAC4L+eeSX375xdLHm+vhJGn9+vVubU9zF/yPFTkAAACbopADAACwKQo5AAAAm6KQAwAAsCk2O/jB3r17LbE777zTrf3QQw9Z+tx8882W2JgxYyyxffv2XUJ2AAC7at++vVu7Zs2alj7efg7A008/7dY+depU8RNDiWFFDgAAwKYo5AAAAGyKQg4AAMCmKOQAAABsymG8veqxFOTk5KhSpUr+TgMBJjs7WxUrVvR3GgDKILvNS+vWrXNrN2zY0KvjXnvtNUvsueee80lOl6tAmZtYkQMAALApCjkAAACbopADAACwKQo5AAAAm+KTHQAAuExUrlzZre1wOCx9PH36z+TJk0sqJZQwVuQAAABsikIOAADApijkAAAAbIpr5AAAuExMnDjxom1JGjdunCW2Z8+eEssJJYsVOQAAAJuikAMAALApCjkAAACbopADAACwKYcxxvg7iTw5OTmqVKmSv9NAgMnOzlbFihX9nQaAMoh5CQUJlLmJFTkAAACbopADAACwKQo5AAAAmwqoQi6ALtdDAOF1AcBf+P2DggTKayOgCrmjR4/6OwUEIF4XAPyF3z8oSKC8NgJq16rT6dTu3bsVFRUlh8Ph73TgZ8YYHT16VHFxcQoKCqi/OQCUEcxLyC/Q5qaAKuQAAADgPf+XkgAAACgWCjkAAACbopADAACwKQo5AAAAm6KQAwAAsCkKOQAAAJuikAMAALCp/w/JbckuKugxOwAAAABJRU5ErkJggg==\n" + "image/png": "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tWtuuuuuuxQUFKSxY8daVsW8nZskaeXKla5Y3m20LuRpbsrNzdX06dO9ek7e3n5Ekj744AMNGjRIvXv31sSJE70a35NSXZFr06aNUlJS9PLLL+vnn39Wp06dVKFCBW3ZskVz5szRlClT1KNHD8XExOjZZ5/Vyy+/rG7duikpKUlr167VZ599VuiW3KCgIKWmpurOO+9U48aN9eijj6pGjRravHmzNm7c6Lp4smnTppKkQYMGqXPnzipXrpySk5NLLMeibvFesmSJDh48eNHVo7xt6DNmzCjwLta7d+/W8uXLNWjQII/fDwkJUefOnTVnzhz97W9/U7t27fTQQw/pb3/7m7Zs2aI77rhDTqdTq1atUrt27TRgwADX+Vu2bJkmTpyouLg4JSQk6KabblJycrKee+453X333Ro0aJBOnDih1NRU1atXTz/99JPrcTt16qTg4GDdeeedSklJ0bFjx/T3v/9dsbGx2rNnT6Hnp6i3H9mwYYPWr1+v559/3ucXxgKwN+amwuemyZMna/r06brlllsUHh6u9PR0t+/ffffdriI0MzNT7dq106hRowr8hIrTp09r7ty56tixY4E3Tu7evbumTJmiffv26aqrrtKIESM0btw43XbbbbrnnnsUEhKiH374QXFxca5LkJo2barU1FSNHz9eV111lWJjY9W+fXt16tRJtWvX1mOPPaahQ4eqXLlyeu+99xQTE6MdO3a4HrNly5aKjo5Wnz59NGjQIDkcDs2cOdPrucbb2498//33evjhh1WlShXdfvvtlrfMW7ZsabmZfYGKssU1b5vuDz/8cNF+ffr0MREREQV+/5133jFNmzY1YWFhJioqyjRs2NAMGzbM7N6929Xn3LlzZsyYMaZGjRomLCzMtG3b1mzYsMHEx8dfdIt3nq+++sp07NjRREVFmYiICNOoUSMzdepU1/fPnj1rBg4caGJiYozD4bBsS/ZljsYUfYt3cnKyqVChgjl48GCBfaZOnWokmcWLFxfY54033jCSzBdffFFgn7S0NCPJLFiwwBhz/ty89tpr5tprrzXBwcEmJibGdOnSxaxZs8Z1zObNm03r1q1NWFiYZUv70qVLTYMGDUxwcLC55pprTHp6uset35988olp1KiRCQ0NNXXq1DGvvPKKee+99yxbt31x+5Hnn3/eSDLr16/3+hgA9sDcVPJzU95t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nOfm5urgwYNFOp8+2ezgrcTERC1btky33nrrRavN+Ph4Sef/Aqlbt64rvn//fsvuHE+PIUkbNmxQhw4dCuxX0A+1NHIsbStWrNCuXbs0duxYy4Wqhw8f1hNPPKH58+frwQcfVGJiopxOp3755Rc1bty4wDELOn95f/EdOXLE7e2DvL8S82RmZurgwYP6+OOP3e6zlLe7DABKC3NT4Ro3bqx//OMf2rRpk9uCwHfffef6flF9/PHHOnXqlFJTU1W1alW37/36668aOXKkVq9erVatWikxMVFLlizRoUOHLroqd7G5ydONhvPPTQsXLtTp06f1ySefqHbt2q543lvXvpJ3vn788UclJSW54j/++KOcTmeRzmep3pOhZ8+eOnfunMaNG2f53tmzZ10nuUOHDqpQoYKmTp3q9l7z5MmTC32MJk2aKCEhQZMnT7b80C4cK+++Qfn7lFSORbn9iK/lva06dOhQ9ejRw+3r8ccf19VXX+36K++uu+5SUFCQxo4da/nLI//58/SfIu+X1cqVK12xvO3gF8q739CFY+bm5mr69OlePaei3H4EAC6GuanwuelPf/qTKlSo4PY72hijt956S1deeaXl7gfeSE9PV926ddWvXz/L3PTss88qMjLSNTfde++9MsZ4vFmyt3NTdna21q9f74rt2bNH8+bNc+vnaW7Kzs7WjBkzvHpO3t5+pH379qpcubLltiapqakKDw93Xe7kjVJdkWvTpo1SUlL08ssv6+eff1anTp1UoUIFbdmyRXPmzNGUKVPUo0cPxcTE6Nlnn9XLL7+sbt26KSkpSWvXrtVnn31mqdrzCwoKUmpqqu688041btxYjz76qGrUqKHNmzdr48aNrgv7mzZtKkkaNGiQOnfurHLlyik5ObnEcizK7Udmzpyp33//3fXe/MqVK1037n3ooYdcf3FlZmaqXbt2GjVqVIEf6XL69GnNnTtXHTt2LPDmlN27d9eUKVO0b98+XXXVVRoxYoTGjRun2267Tffcc49CQkL0ww8/KC4uTi+//LLr/KWmpmr8+PG66qqrFBsbq/bt26tTp06qXbu2HnvsMQ0dOlTlypXTe++9p5iYGO3YscP1mC1btlR0dLT69OmjQYMGyeFwaObMmV5dJCoV7fYjK1eudBWW+/fv1/Hjx13ns3Xr1kW+8zqAywtzU+FzU82aNTVkyBC9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5Z/3PvnkE0ufbdu2FTVF22JFDgAAwKYo5AAAAGyKQg4AAMCmKOQAAABsis0OAeLmm292az/66KOWPmvXrrXEPv/88xLLCQBgbz/++KMl9uc//9kS8+XGBm942qDQu3dvS6x58+alkY6tsSIHAABgUxRyAAAANkUhBwAAYFNcIxcgOnTo4NauXLmypc/ixYstsVOnTpVYTgCAwBUUVPhazE033VQKmRSdw+GwxDw9H2+e4+jRoy2xhx56qFh52RErcgAAADZFIQcAAGBTFHIAAAA2RSEHAABgU2x2CBA33HCDW9sYY+nz0UcflVY6AIAA0q9fP0vM6XT6IRPfuPPOOy2xG2+80RLz9BzzxzxtdihLWJEDAACwKQo5AAAAm6KQAwAAsCkKOQAAAJtis4MfVK9e3RK77bbb3Nq//vqrpc+8efNKLCcAQODytDkgUMXExFhi119/vVt7+PDhxR5///79bu0zZ84Ue6zLAStyAAAANkUhBwAAYFMUcgAAADZFIQcAAGBTbHbwg0ceecQSi42NdWt/9tlnpZQNAAC+M2LECEusf//+xRorKyvLEuvTp49be8eOHcUa+3LBihwAAIBNUcgBAADYFIUcAACATXGNnB/Ex8cX2ufw4cOlkAkAAMW3aNEiS+yaa67x2fi//PKLJfbVV1/5bPzLAStyAAAANkUhBwAAYFMUcgAAADZFIQcAAGBTbHbwg27duhXaZ+HChaWQCQDADhwOhyUWFFT4WkyXLl28Gv+dd96xxOLi4go9zlMOTqfTq8f0xp133umzsS5XrMgBAADYFIUcAACATVHIAQAA2BSFHAAAgE2x2aGEtWrVyhKrXr26HzIBANhVamqqJfbqq68Wetynn35qiXm7GaG4mxaKe9xbb71VrOPKOlbkAAAAbIpCDgAAwKYo5AAAAGyKQg4AAMCm2OxQwu6++25LrFy5cpbY2rVr3dorV64ssZwAAPby8ccfW2JDhw61xGJiYkojnYvav3+/JbZp0ya39hNPPGHps2fPnhLL6XLGihwAAIBNUcgBAADYFIUcAACATXGNnA+Fh4dbYklJSV4d+9FHH7m1z50755OcAAD29/vvv1tiycnJlthdd93l1h48eHBJpVSgl156yRJ78803Sz2PsoIVOQAAAJuikAMAALApCjkAAACbopADAACwKTY7+NCZM2csscOHD1ti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kBB17txZc+bM0d/+9je1a9dODz30kP72t79py5YtuuOOO+R0OrVq1Sq1a9dOAwYMcJ2/ZcuWaeLEiYqLi1NCQoJuuukmJScn67nnntPdd9+tQYMG6cSJE0pNTVW9evX0008/uR63U6dOCg4O1p133qmUlBQdO3ZMf//73xUbG6s9e/YUeI7yeHv7EU/nM28FrkuXLoW+ngBc/pibvLv9yMyZM/X777+7rmleuXKlxo8fL0l66KGHXKuBmZmZateunUaNGlXgx3idPn1ac+fOVceOHRUaGuqxT/fu3TVlyhTt27dPV111lUaMGKFx48bptttu0z333KOQkBD98MMPiouL08svv+w6f6mpqRo/fryuuuoqxcbGqn379urUqZNq166txx57TEOHDlW5cuX03nvvKSYmRjt27HA9ZsuWLRUdHa0+ffpo0KBBcjgcmjlzpteb64py+5GVK1e6Csv9+/fr+PHjrvPZunVr7z8VpChbXPO26f7www8X7VfY7TLeeecd07RpUxMWFmaioqJMw4YNzbBhw8zu3btdfc6dO2fGjBljatSoYcLCwkzbtm3Nhg0bTHx8/EW3eOf56quvTMeOHU1UVJSJiIgwjRo1MlOnTnV9/+zZs2bgwIEmJibGOBwOy7ZkX+ZoTPG2eOcp6HxOnTrVSDKLFy8u8Ng33njDSDJffPFFgX3S0tKMJLNgwQJjzPlz89prr5lrr73WBAcHm5iYGNOlSxezZs0a1zGbN282rVu3NmFhYZYt7UuXLjUNGjQwwcHB5pprrjHp6eket35/8sknplGjRiY0NNTUqVPHvPLKK+a9996zbN2+1NuP5MftR4DLC3NT6cxNeb93PX1d+DwXLlxoJJm33nqrwLHmzp1rJJl33323wD6ZmZlGkpkyZYor9t5775kbb7zRhISEmOjoaNOmTRvz+eefu77/xx9/mK5du5qoqCgjyW3uWLNmjbnppptMcHCwqV27tpk4caLH24+sXr3a3HzzzSYsLMzExcWZYcOGmSVLllie56XefiRvLvL0VZRbuziM8bLMRMDp2bOnsrKy9P333/s7FQAAJEnDhg3T7NmztXXrVoWEhPg7ncteqb61Ct8xxigzM7NEPvMOAIDiWr58uf7yl79QxJUSVuQAAABsqlR3rQIAAMB3KOQAAABsikIOAADApijkAAAAbCqgdq06nU7t3r1bUVFRPvncT9ibMUZHjx5VXFyc5bMRAaA0MC8hv0CbmwKqkNu9e7dq1arl7zQQYHbu3KmaNWv6Ow0AZRDzEgoSKHOT/0vJC0RFRfk7BQQgXhcA/IXfPyhIoLw2AqqQY9kanvC6AOAv/P5BQQLltRFQhRwAAAC8RyEHAABgUxRyAAAANkUhBwAAYFMUcgAAADZFIQcAAGBTFHIAAAA2RSEHAABgUxRyAAAANkUhBwAAYFMUcgAAADZFIQcAAGBTFHIAAAA2RSEHAABgUxRyAAAANkUhBwAAYFMUcgAAADZFIQcAAGBTFHIAAAA2RSEHAABgU+X9ncDlpEmTJpbYxx9/bInVqVOnFLK5uE6dOllimzZtssR27txZGukAAC4Td955pyX2ySefuLUHDBhg6fPWW29ZYufOnfNdYpcpVuQAAABsikIOAADApijkAAAAbIpr5Hyoc+fOllhISIgfMimcp2sY/vznP1tiycnJpZEOAMCGqlSpYolNnz690OOmTZtmib333nuW2MmTJ4uXWBnCihwAAIBNUcgBAADYFIUcAACATVHIAQAA2BSbHS5B+fLupy8pKclPmRTdmjVrLLFnnnnGEouIiHBrHz9+vMRyAgDYS+vWrS2xmjVrFnrc7NmzLbFTp075JKeyhhU5AAAAm6KQAwAAsCkKOQAAAJuikAMAALApNjtcgnbt2rm1b7nlFkufV199tbTSKZLo6GhL7Prrr7fEwsPD3dpsdgCAssnTJxWNGDGiWGPNnDnTEjPGFGusso4VOQAAAJuikAMAALApCjkAAACbopADAACwKTY7eKlBgwaWWP47U2/bts3SZ8KECSWW06X405/+5O8UAAA20rBhQ0usadOmXh179uxZt/Znn33mk5zAihwAAIBtUcgBAADYFIUcAACATXGNnJdGjhxpiUVERLi177jjDkufY8eOlVhO3qpcubIl1qZNG0vM6XSWRjoAABu69957i33s0qVLfZgJLsSKHAAAgE1RyAEAANgUhRwAAIBNUcgBAADYFJsdPOjRo4cllpSUZIlt3brVrf3jjz+WWE6XYsSIEZaYp40NmZmZltiRI0dKICMAgN20bt3aq365ubmWmKd5CL7BihwAAIBNUcgBAADYFIUcAACATVHIAQAA2BSbHTy47777LLHw8HBLbPr06aWRTpHVqVPHrd27d29Ln3Pnzlli48ePt8TOnDnjs7wAAPbRsmXLi7YLcvz4cUvs559/9kVK8IAVOQAAAJuikAMAALApCjkAAACbopADAACwqTK/2aFSpUqW2M033+zVsampqb5OxyeeeOIJt3bVqlUtfTZt2mSJLV++vMRyAgDYS/PmzYt1XKDOjZcrVuQAAABsikIOAADApijkAAAAbKrMXyMXEhJiiV155ZWW2OzZs0sjHZ9ITEwstM+GDRtKIRMAgF01a9as0D5HjhyxxLhGrnSxIgcAAGBTFHIAAAA2RSEHAABgUxRyAAAANlXmNzscPXrUEvv5558tsUaNGllilStXdmsfOnTIZ3l5KzY21hLr0aNHocd99dVXJZEOAMCGWrVqZYn16tWr0OOys7MtsV27dvkkJ3iHFTkAAACbopADAACwKQo5AAAAm6KQAwAAsKkyv9nh5MmTlti2bdsssXvvvdcS+/e//+3Wnjhxos/yatCggSVWt25dS6xOnTqWmDGm0PGdTmex8gIAXH6qVKliiQUFFb7W8/nnn5dEOigCVuQAAABsikIOAADApijkAAAAbIpCDgAAwKbK/GYHT0aNGmWJORwOS6xr165u7dmzZ/sshwMHDlhinjYxVK1atVjjp6WlFes4AMDlx5tPBDpy5Igl9vbbb5dANigKVuQAAABsikIOAADApijkAAAAbMphvLl7bCnJyclRpUqV/J2G1xo3buzWvuqqq3w29kcffeRVv/fff98S6927d6HHlS9vn8sjs7OzVbFiRX+nAaAMstu85I2aNWtaYr///rsllv+GwBs2bLD0adiwoe8Ss5lAmZtYkQMAALApCjkAAACbopADAACwKQo5AAAAm7LPFe8B6Oeff75ouzT89ttvxTquQYMGlpinC1kBAJeXli1bWmL5NzZ4Mn/+/BLIBpeKFTkAAACbopADAACwKQo5AAAAm6KQAwAAsCk2O9icw+HwKpYfGxsAoGyqUqWKV/0OHDjg1p4yZUpJpINLxIocAACATVHIAQAA2BSFHAAAgE1RyAEAANgUmx1szhjjVQwAAEnq3LmzV/127Njh1s7Ozi6JdHCJWJEDAACwKQo5AAAAm6KQAwAAsCmukbO50NDQQvucPHmyFDIBAASaChUqWGKJiYleHXvq1Cm39pkzZ3ySE3yLFTkAAACbopADAACwKQo5AAAAm6KQAwAAsCk2O9jc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dll7q9QLOWOMeeedd0zTpk1NWFiYiYqKMg0bNjTDhg0zu3fvdvU5d+6cGTNmjKlRo4YJCwszbdu2NRs2bDDx8fGF/mcxxpivvvrKdOzY0URFRZmIiAjTqFEjtx/22bNnzcCBA01MTIxxOByWE+/LHI0pmUJu6tSpRpJZvHhxgce+8cYbRpL54osvCuyTlpZmJJkFCxYYY86fm9dee81ce+21Jjg42MTExJguXbqYNWvWuI7ZvHmzad26tQkLC7NsaV+6dKlp0KCBCQ4ONtdcc41JT0/3uMX7k08+MY0aNTKhoaGmTp065pVXXjHvvfcehRyAImFuKvm56dChQ+ZPf/qTqV69ugkODjYJCQnmueeesxRxxjA3eTs3JScnm1q1apng4GATFxdn+vXrZ/bu3evVsRdyGOPlPRwQcHr27KmsrCx9//33/k4FAABJzE2lrVQ3O8B3jDHKzMxUenq6v1MBAEASc5M/sCIHAABgU6W6axUAAAC+QyEHAABgUxRyAAAANkUhBwAAYFMBtWvV6XRq9+7dioqK8slnq8HejDE6evSo4uLiLJ+NCAClgXkJ+QXa3BRQhdzu3btVq1Ytf6eBALNz507VrFnT32kAKIOYl1CQQJmb/F9KXiAqKsrfKSAA8boA4C/8/kFBAuW1EVCFHMvW8ITXBQB/4fcPChIor42AKuQAAADgPQo5AAAAm6KQAwAAsCkKOQAAAJuikAMAALApCjkAAACbopADAACwKQo5AAAAm6KQAwAAsCkKOQAAAJuikAMAALApCjkAAACbopADAACwKQo5AAAAm6KQAwAAsCkKOQAAAJuikAMAALApCjkAAACbopADAACwKQo5AAAAm6KQAwAAsKny/k7AzmJjY93aH374oaXP119/bYm98847llhWVpbP8vKlSpUqubVbt25t6bN48WJL7MyZMyWWEwAAOI8VOQAAAJuikAMAALApCjkAAACb4ho5L0VHR1tiGzdudGvnv55Mkvbu3WuJ2eV6OElas2aNWzsmJsbSp2nTppbY1q1bfZcYAMBnKlasaIm9/PLLlliDBg3c2h06dLD04Xpo/2NFDgAAwKYo5AAAAGyKQg4AAMCmKOQAAABsis0OHlStWtUS++CDDyyxypUru7WnT59u6TNw4EDfJVbCRo4caYklJCS4tVNSUix92NgAAIGpd+/elthLL71kidWqVavQsTxtkjh48GDxEoPPsCIHAABgUxRyAAAANkUhBwAAYFMUcgAAADblMMYYfyeRJycnx+OnC5S2Tp06WWKfffZZocdVr17dEtu/f79PcvK1+vXrW2L/+c9/LLF58+a5tR955BFLn6NHj/osL0+ys7M9XmQLACUtUOYlb9WsWdOtvXbtWkufKlWqWGLelAKeNv0NGDDAEjt06FChY10OAmVuYkUOAADApijkAAAAbIpCDgAAwKYo5AAAAGyqzH+yQ2xsrCV27733enXsY4895ta208aGZcuWeXVs/s0OJb2xAQBQfM8++6xbO/8nEF2K+++/3xK74447LDFPnxwxdepUt3Zubq7P8irrWJEDAACwKQo5AAAAm6KQAwAAsKkyf43cG2+8YYk9+OCDltiaNWsssTlz5pRITr522223WWLVqlWzxNLS0iyx9PT0kkgJAHCJ4uPjLbFHH3200OPWr19vie3du9cS69ChQ6FjebpZcv7r9CQpIyPDrf3HH38UOja8w4ocAACATVHIAQAA2BSFHAAAgE1RyAEAANhUmd/sYIyxxJxOpyW2e/duSywQbmgYFhZmiQ0fPtyt/dRTT1n6eHref/7zn32XGACgRDVu3NgSi4qKcmuvWrXK0qdNmzaWWGhoqCX2wAMPuLXzzy2SlJiYaIlVr17dEluwYIFbu0uXLpY+hw4dssRQOFbkAAAAbIpCDgAAwKYo5AAAAGyKQg4AAMCmyvxmB2917drVElu6dKlb+8iRI5Y+qampPsvB0wWqbdu2tcRuvvnmQsf66KOPfJESAMBPQkJCLLH8G9kmTZrk1VinTp2yxGbMmOHWvu+++yx96tat69X4J06ccGsHwmbBywUrcgAAADZFIQcAAGBTFHIAAAA2RSEHAABgU2V+s8OUKVMssXbt2llicXFxlljr1q3d2g6Hw9Kne/ful5CdO0/je/qEhvx+++03S8zTHboBAPaR/5MXPPG0UW/+/PnFerxmzZoV6zhJ+vbbb93ax44dK/ZYcMeKHAAAgE1RyAEAANgUhRwAAIBNlflr5NasWWOJNWrUyBJr3LixJXbHHXe4tYcOHWrps3//fkvs/fffL0KG/8/MmTMtsXXr1hV63Ndff22Jbdu2rVg5AAACw+zZsy2x/NdlN2/e3NLn2muvtcQaNmxoid19991u7ejoaEsfTzfC99Tv8ccfd2t7ms9++eUXSwyFY0UOAADApijkAAAAbIpCDgAAwKYo5AAAAGzKYby5o2wpycnJUaVKlfydRsCqW7euJbZ161ZL7Oeff3Zrd+7c2dLH0yaMQJWdna2KFSv6Ow0AZVAgz0uVK1e2xPLPCZ5yL+7N5ZctW2aJ9e/f3xL79NNPLbGrr77arf33v//d0qdfv36F5hBIAmVuYkUOAADApijkAAAAbIpCDgAAwKYo5AAAAGyqzH+yg528+OKLlpinC1Sfe+45t7adNjYAALxz6NAhS6xnz55u7Y8++sjSx9vNG1OnTnVr559bJOnUqVOW2Mcff2yJPf/8825tT5vwEhMTLTE+hahwrMgBAADYFIUcAACATVHIAQAA2BSFHAAAgE2x2SFA3XfffZbYww8/bIkdPXrUEjt48GCJ5AQACGz5P32hR48elj69evWyxI4cOWKJ5d9g52ljgyfjxo2zxK677jq3dvfu3Qt9PEnq06ePV49ZlrEiBwAAYFMUcgAAADZFIQcAAGBTXCMXoLp06eJVv08//dQS++mnn3ydDgDAhvJfM1dQzJdOnjxpiX3wwQdubU/XyLVr184Sq1y5siXm6UbIZRkrcgAAADZFIQcAAGBTFHIAAAA2RSEHAABgU2x2CFCeNjscP37cEnvjjTdKIx0AAIrtww8/dGt72uxw//33W2IDBgywxMaOHeu7xC4DrMgBAADYFIUcAACATVHIAQAA2BSFHAAAgE05jDHG30nkycnJUaVKlfydhl/069fPrT19+nRLn3379lli1atXL7GcAkV2drYqVqzo7zQAlEFleV4qSY0bN7bEVq9ebYmFhoZaYtddd51b+7///a/P8iqKQJmbWJEDAACwKQo5AAAAm6KQAwAAsCkKOQAAAJvikx0CRP7NDp72oPz73//2aqyoqCi3dnR0tKXPjh07ipAdAAC+8/PPP1tiL774oiX22muvWWITJkxwaz/00EOWPidPnix+cjbDihwAAIBNUcgBAADYFIUcAACATXGNnI2cO3fOEuvdu7cl9vTTT7u1N27caOnTp08f3yUGAMAl+uc//2mJpaSkWGL33HOPW3vs2LGWPuvXr/ddYgGOFTkAAACbopADAACwKQo5AAAAm6KQAwAAsCmH8XTnWT/JyclRpUqV/J2GX+S/OWLDhg0tfRwOhyXm6cf37rvvurXHjRtn6bNz584iZug/2dnZqlixor/TAFAGleV5KRDUrl3bEsvKynJrz54929LH00ZAXwuUuYkVOQAAAJuikAMAALApCjkAAACbopADAACwKT7ZIUAMGDDAre3pTtUrV660xFJTUy2xw4cPu7Vzc3MvMTsAAErfjh07LLFly5a5tbt3727pc/3111tiv/zyi+8SCyCsyAEAANgUhRwAAIBNUcgBAADYFIUcAACATbHZIUB89dVXbu327dv7KRMAAAJXjx493Nrr1q2z9LnqqqssMTY7AAAAIKBQyAEAANgUhRwAAIBNcY0cAACwjZycHLd2QkKCnzIJDKzIAQAA2BSFHAAAgE1RyAEAANgUhRwAAIBNUcgBAADYFIUcAACATVHIAQAA2BSFHAAAgE0FVCFnjPF3CghAvC4A+Au/f1CQQHltBFQhd/ToUX+ngADE6wKAv/D7BwUJlNeGwwRKSSnJ6XRq9+7dioqKksPh8Hc68DNjjI4ePaq4uDgFBQXU3xwAygjmJeQXaHNTQBVyAAAA8J7/S0kAAAAUi+0KuTp16uiRRx5xtTMzM+VwOJSZmem3nPLLnyMuTdu2bdW2bVt/pwEABWJuKnsCZW4qUiGXlpYmh8Ph+goNDVW9evU0YMAA7d27t6RyLBGLFi3S6NGj/Z2GR3v27NETTzyhhIQEhYWFKTExUc8884wOHjx4yWNv2rTJ9bM7cuRIsceZMGGC5s+ff8n5lIYLX7MXfv31r3/1d2oAfIC5qXRs3bpVPXr0UHR0tMLDw9WqVSstX77cJ2OXxblp7969evTRRxUbG6uwsDA1adJEc+bMKfI45Yvz4GPHjlVCQoJOnTqlr776SqmpqVq0aJE2bNig8PDw4gxZbK1bt9bJkycVHBxcpOMWLVqkN998M+D+wxw7dky33HKLjh8/rqeeekq1atXSunXrNG3aNC1fvlxr1qy5pIsr09PTVb16dR0+fFgfffSR+vbtW6xxJkyYoB49euiuu+4qdi6lqWPHjnr44YfdYjfeeKOfsgFQEpibSs7OnTt1yy23qFy5cho6dKgiIiI0Y8YMderUSV988YVat259SeOXtbkpJydHrVq10t69ezV48GBVr15dH374oXr27KmMjAz16tXL67GKVch16dJFzZo1kyT17dtXVapU0cSJE7VgwQI98MADHo85fvy4IiIiivNwFxUUFKTQ0FCfj+svn3zyiX7//Xd9+umn6tq1qyteuXJljR07VuvWrSt2AWKM0axZs9SrVy9t375dGRkZxf7PYjf16tXTgw8+6O80AJQg5qaS89e//lVHjhzRhg0bdM0110iSHn/8cV177bV6+umntWbNmmKPXRbnprfffltbt27VF198ofbt20uSnnzySd188836v//7P/Xo0cPrPwJ8co1cXhLbt2+XJD3yyCOKjIzUtm3blJSUpKioKPXu3VvS+a3ckydPVv369RUaGqpq1aopJSVFhw8fdhvTGKPx48erZs2aCg8PV7t27bRx40bLYxd0HcJ3332npKQkRUdHKyIiQo0aNdKUKVNc+b355puS3N92y+PrHCVp27Zt2rZtW6HnMicnR5JUrVo1t3iNGjUkSWFhYYWOUZDVq1crKytLycnJSk5O1sqVK7Vr1y5LP6fTqSlTpqhhw4YKDQ1VTEyM7rjjDv3444+Szp+z48eP6/3333edu7zrLh555BHVqVPHMubo0aMtW/dnzJih9u3bKzY2ViEhIbr++uuVmprq1XPZsWOHNm/eXKTnf/LkSZ06dapIxwCwL+Ym381Nq1at0o033ugq4iQpPDxc3bt3108//aQtW7YUOkZByuLctGrVKsXExLheo9L54r9nz576448/tGLFCq8eTyrmilx+eS+CKlWquGJnz55V586d1apVK73++uuuZe2UlBSlpaXp0Ucf1aBBg7R9+3ZNmzZNa9eu1erVq1WhQgVJ0osvvqjx48crKSlJSUlJ+umnn9SpUyfl5uYWms/nn3+ubt26qUaNGq4ly02bNunTTz/V4MGDlZKSot27d+vzzz/XzJkzLceXRI633367JCkrK+uiubdu3VpBQUEaPHiw3njjDdWsWVPr16/XSy+9pLvuukvXXnttoc+/IBkZGUpMTFTz5s3VoEEDhYeHa/bs2Ro6dKhbv8cee0xpaWnq0qWL+vbtq7Nnz2rVqlX69ttv1axZM82cOVN9+/ZVixYt9MQTT0iSEhMTi5xPamqq6tevr+7du6t8+fJauHChnnrqKTmdTvXv3/+ixz788MNasWKF13fWTktL0/Tp02WM0XXXXaeRI0cWaekagP0wN/lubjp9+rSio6Mt8bzzt2bNGl199dWFngNPyuLcdPr0aY8LMxeez44dO3qXsCmCGTNmGElm2bJlZv/+/Wbnzp3mX//6l6lSpYoJCwszu3btMsYY06dPHyPJPP/8827Hr1q1ykgyGRkZbvHFixe7xfft22eCg4NN165djdPpdPUbPny4kWT69Onjii1fvtxIMsuXLzfGGHP27FmTkJBg4uPjzeHDh90e58Kx+vfvbzw9/ZLI0Rhj4uPjTXx8vOXxPPnHP/5hrrjiCiPJ9dWnTx9z5swZr473JDc311SpUsWMGDHCFevVq5e54YYb3Pp9+eWXRpIZNGiQZYwLn2dERITlORpz/mfv6XmOGjXKcr5PnDhh6de5c2dTt25dt1ibNm1MmzZtLDFvX74tW7Y0kydPNgsWLDCpqammQYMGRpKZPn26V8cDCGzMTSU/N915553miiuuMDk5OW7xW265xUgyr7/+eqFjeFJW56aBAweaoKAgk5WV5RZPTk42ksyAAQMKHSNPsd5a7dChg2JiYlSrVi0lJycrMjJS8+bN05VXXunW78knn3Rrz5kzR5UqVVLHjh114MAB11fTpk0VGRnp2v2ybNky5ebmauDAgW5LnkOGDCk0t7Vr12r79u0aMmSIrrjiCrfveXNX7pLKMSsrq9C/ePJceeWVatGihSZPnqx58+bpmWeeUUZGhp5//nmvjvfks88+08GDB92uE3nggQe0bt06tyX3uXPnyuFwaNSoUZYxfH1X8wv/GsnOztaBAwfUpk0b/fbbb8rOzr7osZmZmV6vxq1evVqDBw9W9+7d1a9fP61Zs0YNGjTQ8OHDdfLkyUt6DgACB3NTyc1NTz75pI4cOaL7779fa9eu1X//+18NGTLE9bZmcX+XltW5qW/fvipXrpx69uypr7/+Wtu2bdPLL7+sefPmSSra+SzWW6tvvvmm6tWrp/Lly6tatWq65pprLDspy5cvr5o1a7rFtmzZouzsbMXGxnocd9++fZKk33//XZIsy7QxMTEel3YvlLeU3qBBA++fUCnneDGrV69Wt27dXEvFknTXXXepYsWKGjNmjP785z/r+uuvL/K46enpSkhIUEhIiLZu3Srp/JJzeHi4MjIyNGHCBEnnz19cXJwqV65c7OfgrdWrV2vUqFH65ptvdOLECbfvZWdnq1KlSiXyuMHBwRowYICrqGvVqlWJPA6A0sXcVHJzU5cuXTR16lQ9//zzatKkiSTpqquu0ksvvaRhw4YpMjKyWOOW1bmpUaNGmjVrlvr166dbb71VklS9enVNnjxZTz75ZJHOZ7EKuRYtWriKjIKEhIRY/gM5nU7FxsYqIyPD4zExMTHFScen/J3j22+/rWrVqlnOb/fu3TV69Gh9/fXXRS7kcnJytHDhQp06dcrjNQyzZs3SSy+95JO/agoa49y5c27tbdu26fbbb9e1116riRMnqlatWgoODtaiRYs0adIkOZ3OS87lYmrVqiVJOnToUIk+DoDSw9xUsgYMGKBHH31U69evV3BwsBo3bqx3331X0vk7AxRVWZ+bevTooe7du2vdunU6d+6cmjRp4tocU5Tz6ZPNDt5KTEzUsmXLdOutt15092V8fLyk83+B1K1b1xXfv3+/ZXeOp8eQpA0bNqhDhw4F9ivoh1oaOV7M3r17LS8sSTpz5oyk8xfqFtXHH3+sU6dOKTU1VVWrVnX73q+//qqRI0dq9erVatWqlRITE7VkyRIdOnToon/5FHT+oqOjPd7MMe+vxDwLFy7U6dOn9cknn6h27dquuK9uLlmY3377TVJg/IIG4F/MTd6LiIjQLbfc4movW7ZMYWFhrlWlomBuOv8OUfPmzV3tZcuWSdJFXyP5lepHdPXs2VPnzp3TuHHjLN87e/as6yR36NBBFSpU0NSpU93ea548eXKhj9GkSRMlJCRo8uTJlh/ahWPl3Tcof5+SytHbLd716tXT3r17LVvWZ8+eLal4N7FNT09X3bp11a9fP/Xo0cPt69lnn1VkZKTrr7x7771XxhiNGTPGMk7+8+fpP0ViYqKys7O1fv16V2zPnj2u9/3zlCtXzjJmdna2ZsyY4dVz8naL9/79+y2xo0ePavLkyapataqaNm3q1eMBuHwxNxU+N3ny9ddf6+OPP9Zjjz1WrLcby/Lc5MmWLVv01ltvqVu3bkVb4fR6W4T5fzuDfvjhh4v269Onj4mIiPD4vZSUFCPJdOnSxUyaNMlMmzbNDB482MTFxZk5c+a4+r3wwgtGkklKSjLTpk0zjz32mImLizNVq1a96M4gY87v4qlQoYKJj483o0ePNm+//bZ5+umnTadOnVx9PvzwQyPJPPTQQyY9Pd3Mnj27xHI0xvudQZs3bzYREREmMjLSvPDCC+att94yDzzwgJFkOnbs6NY37+cxY8aMAsf73//+Z4KCgsyQIUMK7HPvvfeaKlWqmNzcXGOMMQ899JDr+U+ZMsVMmjTJ3HPPPWbq1KmuY5KSkkxERIR54403zOzZs823335rjDHmwIEDJiIiwtStW9dMnjzZTJgwwdSqVcs0adLEbSfP5s2bTXBwsGnYsKGZNm2a+etf/2oSExPNDTfcYCSZ7du3u/peys6gUaNGmRtuuMGMHDnSvPPOO2bMmDEmPj7eOBwOk56eXujxAAIfc1PJz01ZWVmmRYsWZvz48eYf//iHefrpp01YWJi58cYbLTtZmZu8K62uu+468+KLL5p//OMfZsSIEaZy5comPj7etcvaW6VeyBljzDvvvGOaNm1qwsLCTFRUlGnYsKEZNmyY2b17t6vPuXPnzJgxY0yNGjVMWFiYadu2rdmwYYOJj48v9D+LMcZ89dVXpmPHjiYqKspERESYRo0auf2wz549awYOHGhiYmKMw+GwnHhf5mhM0W4/snnzZtOjRw9Tq1Yt13/6Z5991hw/ftyt39SpU40ks3jx4gLHeuONN4wk88UXXxTYJy0tzUgyCxYscJ2b1157zVx77bUmODjYxMTEmC5dupg1a9a45di6dWsTFhZm2dK+dOlS06BBAxMcHGyuueYak56e7nGL9yeffGIaNWpkQkNDTZ06dcwrr7xi3nvvPZ/+Z1m6dKnp2LGjqV69uqlQoYK54oorTKdOnS56PgDYC3NTyc9Nhw4dMn/6059M9erVTXBwsElISDDPPfecpYgzhrnJ20IuOTnZ1KpVywQHB5u4uDjTr18/s3fvXq+OvZDDGC/v4YCA07NnT2VlZen777/3dyoAAEhibiptpbrZAb5jjFFmZqbS09P9nQoAAJKYm/yBFTkAAACbKtVdqwAAAPAdCjkAAACbopADAACwKQo5AAAAmwqoXatOp1O7d+9WVFSUTz5bDfZmjNHRo0cVFxdn+WxEACgNzEvIL9DmpoAq5Hbv3u36MHMgz86dO1WzZk1/pwGgDGJeQkECZW7yfyl5gaioKH+ngADE6wKAv/D7BwUJlNdGQBVyLFvDE14XAPyF3z8oSKC8NgKqkAMAAID3KOQAAABsikIOAADApijkAAAAbIpCDgAAwKYo5AAAAGyKQg4AAMCmKOQAAABsikIOAADApijkAAAAbIpCDgAAwKYo5AAAAGyKQg4AAMCmKOQAAABsikIOAADApijkAAAAbKq8vxOA70VHR7u1a9euXeyxfv/9d7f2008/bemzYcMGS+y///2vJbZu3bpi5wEAAKxYkQMAALApCjkAAACbopADAACwKQo5AAAAm2Kzg4107drVEuvevbsl1rZtW7f2VVddVezHzL9pIT4+3tInJCTEq7HKlStX7DwAAIAVK3IAAAA2RSEHAABgUxRyAAAANsU1cn6QmJhoifXv39+t/fjjj1v6hIWFWWIOh8N3iXlQr169Eh0fAAAUHytyAAAANkUhBwAAYFMUcgAAADZFIQcAAGBTbHbwg5o1a1pigwcP9kMm7jZv3myJbdy40Q+ZAAD8Lf/N5KtWrWrpc/fdd1ti+W9KL0lOp9Ot/dZbb1n6rF692hLbunVrYWmWeazIAQAA2BSFHAAAgE1RyAEAANgUhRwAAIBNsdnBS54u8sy/QcHThZqLFy+2xE6fPm2JZWdnu7WPHz9u6RMREWGJLV261BLbsGGDW/u7776z9Fm7dq0ldvLkSUvMUx4AAPtq0KCBJTZgwABL7J577nFre5oHi+umm26yxM6ePWuJ/frrr5bYV1995db2tFkwNzf3ErKzF1bkAAAAbIpCDgAAwKYo5AAAAGyKQg4AAMCm2OzggbebCm644Qa3tqc7XHvy7bffWmJNmjRxa2dlZVn61K5d2xLbtWuXJZb/DtoAgLKhUaNGbu3+/ftb+tx///2WWMWKFQsd+3//+58ltmrVKkts+/btltiwYcPc2mvWrLH0adGihSVWuXJlSywpKcmtvW7dOksfT58ccbliRQ4AAMCmKOQAAABsikIOAADApsr8NXLBwcGW2KxZsyyx/NfDSdKECRPc2suWLSt2Hp6uictvx44dxR4fAHB5efvtty2x/Ndqe3sT3y+++MIS+89//uPWHj58uKXPqVOnvBq/ZcuWbu0nn3zS0ue9996zxBo3bmyJ7d2716395ptvWvrMnTvXEtu/f39hadoSK3IAAAA2RSEHAABgUxRyAAAANkUhBwAAYFNlbrNDZGSkW/uFF16w9OnWrZslduDAAUvs9ddfd2ufOHHiErMDAJR1oaGhllj+G+pKUt++fS0xh8Ph1vZ0gX9qaqol9tprr1lix48fv2ieRVGlShW3drly5Sx9Ro8ebYktXrzYEouPj/dZXpcDVuQAAABsikIOAADApijkAAAAbIpCDgAAwKbK3GaHu+66y639/PPPW/p4+gSF2267zRLLzs72WV4AAEhS27ZtLbGhQ4daYvk3NkjS//73P7f2vffea+nz/fffFz+5fDxtWqhVq5Yl9s9//tOtvWjRIkuf6Ohorx4z//OeOXOmpc+RI0e8GutywIocAACATVHIAQAA2BSFHAAAgE1RyAEAANhUmdvs0LJly0L7rF271hLbtWtXSaQDAIAbTxsIzp0759WxZ8+edWvfdNNNlj49evSwxK699tpCxz558qQldt1113kVy//pSNWqVSv08Qqyd+9et/b48eMtfc6cOVPs8e2GFTkAAACbopADAACwKQo5AAAAm3IYY4y/k8iTk5OjSpUqlehj7Nu3z61dpUoVS5/Tp09bYq+88ooltmDBArf2zz//fGnJwaPs7GxVrFjR32kAKINKY17KLywszBKbNWuWJdahQwdLLDw83K0dFGRdr/F22s9/XZ6na/d8yel0WmLz5s2zxAYNGuTW3rNnT4nldDGBMjexIgcAAGBTFHIAAAA2RSEHAABgUxRyAAAANlXmNjvkf7qeLq70Vv5j33rrLUufb7/91hKrXbu2JbZ161a39saNG73KoX79+pbYN99849a2+82MA+WCUgBljz82O3jriiuusMSef/55t/att95q6XPw4EFLbMeOHZZYSEiIW/uGG26w9GnRokVhaXrN0xw6fPhwS+zIkSM+e8xLEShzEytyAAAANkUhBwAAYFMUcgAAADZFIQcAAGBTZW6zw2uvvebWfuaZZ0r08fxh//79bu3MzExLn+Tk5FLK5tIFygWlAMqeQN7sUNr++c9/WmIPPvigV8cePXrUre1p7k1LS7PE8n+6RCAJlLmJFTkAAACbopADAACwKQo5AAAAm6KQAwAAsKkyt9mhXLlybu0bb7zR0mfWrFmWWPny5S2xWrVqubWDggKzLvb0Ix49erQlNn78+FLIpugC5YJSAGVPWd7sMGzYMLe2pznC09zoSe/evd3as2fPLn5iASJQ5qbArDwAAABQKAo5AAAAm6KQAwAAsCnv3ty+jOS/ueCPP/5o6VOvXj2vxrr99tvd2hUqVLD08XQtWvPmzb0a31ccDocl1rRp01LNAQAQuPr27WuJjRw50q3t7fVwGzdutMQ+/vjj4iWGQrEiBwAAYFMUcgAAADZFIQcAAGBTFHIAAAA2VeY2O/jSF198UWifxo0bW2KeNjucPXvWrT1jxgxLn7///e+W2JAhQyyxXr16FZoXAKBsatGihSX2xhtvWGKRkZGFjnXs2DFLrF+/fpbY6dOnvcwORcWKHAAAgE1RyAEAANgUhRwAAIBNUcgBAADYFJsdStjSpUstsZdeeskSy3/H7Mcff9zS56qrrrLE2rZtW6y8du3aVazjAAD2duedd1piUVFRhR53/PhxS6x79+6W2OrVq4uXGIqFFTkAAACbopADAACwKQo5AAAAm6KQAwAAsCmHMcb4O4k8OTk5qlSpkr/T8KmwsDBL7L333rPEevbs6bPHPHfunFv73//+t6XPgw8+aIl5upA1EGRnZ6tixYr+TgNAGWT3ecnTJoYDBw5YYhUqVCh0rHfeeccS8/QpDmVFoMxNrMgBAADYFIUcAACATVHIAQAA2BQ3BC5hJ0+etMSGDBliiUVGRrq1mzVrZukTGxtriWVlZVliM2fOdGuPHj364kkCAC4L+eeSX375xdLHm+vhJGn9+vVubU9zF/yPFTkAAACbopADAACwKQo5AAAAm6KQAwAAsCk2O/jB3r17LbE777zTrf3QQw9Z+tx8882W2JgxYyyxffv2XUJ2AAC7at++vVu7Zs2alj7efg7A008/7dY+depU8RNDiWFFDgAAwKYo5AAAAGyKQg4AAMCmKOQAAABsymG8veqxFOTk5KhSpUr+TgMBJjs7WxUrVvR3GgDKILvNS+vWrXNrN2zY0KvjXnvtNUvsueee80lOl6tAmZtYkQMAALApCjkAAACbopADAACwKQo5AAAAm+KTHQAAuExUrlzZre1wOCx9PH36z+TJk0sqJZQwVuQAAABsikIOAADApijkAAAAbIpr5AAAuExMnDjxom1JGjdunCW2Z8+eEssJJYsVOQAAAJuikAMAALApCjkAAACbopADAACwKYcxxvg7iTw5OTmqVKmSv9NAgMnOzlbFihX9nQaAMoh5CQUJlLmJFTkAAACbopADAACwKQo5AAAAmwqoQi6ALtdDAOF1AcBf+P2DggTKayOgCrmjR4/6OwUEIF4XAPyF3z8oSKC8NgJq16rT6dTu3bsVFRUlh8Ph73TgZ8YYHT16VHFxcQoKCqi/OQCUEcxLyC/Q5qaAKuQAAADgPf+XkgAAACgWCjkAAACbopADAACwKQo5AAAAm6KQAwAAsCkKOQAAAJuikAMAALCp/w/JbckuKugxOwAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] }, "metadata": {}, "output_type": "display_data" diff --git a/neuralnetlib/callbacks.py b/neuralnetlib/callbacks.py index 5119dd7..ee3d910 100644 --- a/neuralnetlib/callbacks.py +++ b/neuralnetlib/callbacks.py @@ -1,6 +1,5 @@ import numpy as np - class EarlyStopping: def __init__(self, patience: int = 5, min_delta: float = 0.001, restore_best_weights: bool = True, start_from_epoch: int = 0, monitor: list = None, mode: str = 'auto', baseline: float = None): @@ -12,44 +11,44 @@ def __init__(self, patience: int = 5, min_delta: float = 0.001, restore_best_wei self.mode = mode self.baseline = baseline self.best_weights = None - self.best_metrics = None + self.best_metric = None self.patience_counter = 0 self.epoch = 0 self.stop_training = False - def on_epoch_end(self, model, metrics): + def on_epoch_end(self, model, loss, metrics=None): self.epoch += 1 if self.epoch < self.start_from_epoch: return False - if self.best_metrics is None: + if self.best_metric is None: if self.monitor is None: - self.best_metrics = metrics - if np.any(np.isnan(metrics)): - self.mode = 'min' - else: - self.mode = 'auto' + self.best_metric = loss + self.mode = 'min' else: - metric_values = [metric(model.predictions, model.y_true) for metric in self.monitor] - self.best_metrics = [np.inf if m > 0 else -np.inf for m in metric_values] - self.mode = 'max' + if metrics is None: + raise ValueError("Metric to monitor is provided, but no metrics are available.") + metric_value = metrics[self.monitor[0].__name__] + self.best_metric = metric_value + if self.mode == 'auto': + if np.isnan(metric_value): + self.mode = 'min' + else: + self.mode = 'max' improved = False if self.monitor is None: - current_metric = metrics[-1] - best_metric = self.best_metrics[-1] - if (self.mode == 'min' and current_metric < best_metric - self.min_delta) or \ - (self.mode == 'max' and current_metric > best_metric + self.min_delta) or \ - (self.mode == 'auto' and current_metric < best_metric - self.min_delta): - self.best_metrics[-1] = current_metric + current_metric = loss + if (self.mode == 'min' and current_metric < self.best_metric - self.min_delta) or \ + (self.mode == 'max' and current_metric > self.best_metric + self.min_delta): + self.best_metric = current_metric improved = True else: - for i, metric in enumerate(metrics): - best_metric = self.best_metrics[i] - if (self.mode == 'max' and metric > best_metric + self.min_delta) or \ - (self.mode == 'min' and metric < best_metric - self.min_delta): - self.best_metrics[i] = metric - improved = True + current_metric = metrics[self.monitor[0].__name__] + if (self.mode == 'max' and current_metric > self.best_metric + self.min_delta) or \ + (self.mode == 'min' and current_metric < self.best_metric - self.min_delta): + self.best_metric = current_metric + improved = True if improved: self.patience_counter = 0 @@ -57,19 +56,17 @@ def on_epoch_end(self, model, metrics): self.best_weights = [layer.weights for layer in model.layers if hasattr(layer, 'weights')] else: self.patience_counter += 1 - - if self.baseline is not None: - if self.mode == 'max' and max(self.best_metrics) < self.baseline: - self.patience_counter = self.patience + 1 - elif self.mode == 'min' and min(self.best_metrics) > self.baseline: - self.patience_counter = self.patience + 1 + if self.baseline is not None: + if self.mode == 'max' and self.best_metric < self.baseline: + self.patience_counter = self.patience + 1 + elif self.mode == 'min' and self.best_metric > self.baseline: + self.patience_counter = self.patience + 1 if self.patience_counter >= self.patience: self.stop_training = True print(f"\nEarly stopping after {self.epoch} epochs.", end='') if self.restore_best_weights and self.best_weights is not None: - for layer, best_weights in zip([layer for layer in model.layers if hasattr(layer, 'weights')], - self.best_weights): + for layer, best_weights in zip([layer for layer in model.layers if hasattr(layer, 'weights')], self.best_weights): layer.weights = best_weights return True diff --git a/neuralnetlib/layers.py b/neuralnetlib/layers.py index 66e7dc7..94a3c7d 100644 --- a/neuralnetlib/layers.py +++ b/neuralnetlib/layers.py @@ -75,7 +75,7 @@ def from_config(config: dict): class Dense(Layer): - def __init__(self, units: int, weights_init: str = "default", bias_init: str = "default", random_state: int = None): + def __init__(self, units: int, weights_init: str = "default", bias_init: str = "default", random_state: int = None, **kwargs): self.units = units self.weights = None @@ -87,8 +87,12 @@ def __init__(self, units: int, weights_init: str = "default", bias_init: str = " self.bias_init = bias_init self.random_state = random_state + for key, value in kwargs.items(): + setattr(self, key, value) + def initialize_weights(self, input_size: int): - self.rng = np.random.default_rng(self.random_state if self.random_state is not None else int(time.time_ns())) + self.rng = np.random.default_rng( + self.random_state if self.random_state is not None else int(time.time_ns())) if self.weights_init == "xavier": stddev = np.sqrt(2 / (input_size + self.units)) self.weights = self.rng.normal(0, stddev, (input_size, self.units)) @@ -101,7 +105,8 @@ def initialize_weights(self, input_size: int): stddev = np.sqrt(1 / input_size) self.weights = self.rng.normal(0, stddev, (input_size, self.units)) else: - raise ValueError("Invalid weights_init value. Possible values are 'xavier', 'he', 'lecun' and 'default'.") + raise ValueError( + "Invalid weights_init value. Possible values are 'xavier', 'he', 'lecun' and 'default'.") if self.bias_init == "default": self.bias = np.zeros((1, self.units)) @@ -112,7 +117,8 @@ def initialize_weights(self, input_size: int): elif self.bias_init == "small": self.bias = np.full((1, self.units), 0.01) else: - raise ValueError("Invalid bias_init value. Possible values are 'normal', 'uniform', 'small' and 'default'.") + raise ValueError( + "Invalid bias_init value. Possible values are 'normal', 'uniform', 'small' and 'default'.") self.d_weights = np.zeros_like(self.weights) self.d_bias = np.zeros_like(self.bias) @@ -122,7 +128,8 @@ def __str__(self): def forward_pass(self, input_data: np.ndarray) -> np.ndarray: if self.weights is None: - assert len(input_data.shape) == 2, f"Dense input must be 2D (batch_size, features), got {input_data.shape}" + assert len( + input_data.shape) == 2, f"Dense input must be 2D (batch_size, features), got {input_data.shape}" self.initialize_weights(input_data.shape[1]) self.input = input_data @@ -148,7 +155,8 @@ def get_config(self) -> dict: @staticmethod def from_config(config: dict): - layer = Dense(config['units'], config['weights_init'], config['bias_init'], config['random_state']) + layer = Dense(config['units'], config['weights_init'], + config['bias_init'], config['random_state']) if config['weights'] is not None: layer.weights = np.array(config['weights']) layer.bias = np.array(config['bias']) @@ -184,23 +192,36 @@ def get_config(self) -> dict: @staticmethod def from_config(config: dict): - activation_function = ActivationFunction.from_config(config['activation_function']) + activation_function = ActivationFunction.from_config( + config['activation_function']) return Activation(activation_function) + @staticmethod + def from_name(name: str) -> "Activation": + name = name.lower().replace("_", "") + for subclass in ActivationFunction.__subclasses__(): + if subclass.__name__.lower() == name: + return Activation(subclass()) + raise ValueError(f"No activation function found for the name: {name}") + class Dropout(Layer): - def __init__(self, rate: float, seed: int = None): + def __init__(self, rate: float, seed: int = None, **kwargs): self.rate = rate self.mask = None self.seed = seed + for key, value in kwargs.items(): + setattr(self, key, value) + def __str__(self): return f'Dropout(rate={self.rate})' def forward_pass(self, input_data: np.ndarray, training: bool = True) -> np.ndarray: if training: rng = np.random.default_rng(self.seed) - self.mask = rng.binomial(1, 1 - self.rate, size=input_data.shape) / (1 - self.rate) + self.mask = rng.binomial( + 1, 1 - self.rate, size=input_data.shape) / (1 - self.rate) return input_data * self.mask else: return input_data @@ -222,9 +243,10 @@ def from_config(config: dict): class Conv2D(Layer): def __init__(self, filters: int, kernel_size: int | tuple, stride: int | tuple = 1, padding: str = 'valid', - weights_init: str = "default", bias_init: str = "default", random_state: int = None): + weights_init: str = "default", bias_init: str = "default", random_state: int = None, **kwargs): self.filters = filters - self.kernel_size = (kernel_size, kernel_size) if isinstance(kernel_size, int) else kernel_size + self.kernel_size = (kernel_size, kernel_size) if isinstance( + kernel_size, int) else kernel_size self.stride = (stride, stride) if isinstance(stride, int) else stride self.padding = padding @@ -237,10 +259,14 @@ def __init__(self, filters: int, kernel_size: int | tuple, stride: int | tuple = self.bias_init = bias_init self.random_state = random_state + for key, value in kwargs.items(): + setattr(self, key, value) + def initialize_weights(self, input_shape: tuple): in_channels, _, _ = input_shape - self.rng = np.random.default_rng(self.random_state if self.random_state is not None else int(time.time_ns())) + self.rng = np.random.default_rng( + self.random_state if self.random_state is not None else int(time.time_ns())) if self.weights_init == "xavier": self.weights = self.rng.normal(0, np.sqrt(2 / (np.prod(self.kernel_size) * in_channels)), (self.filters, in_channels, *self.kernel_size)) @@ -248,9 +274,11 @@ def initialize_weights(self, input_shape: tuple): self.weights = self.rng.normal(0, np.sqrt(2 / (in_channels * np.prod(self.kernel_size))), (self.filters, in_channels, *self.kernel_size)) elif self.weights_init == "default": - self.weights = self.rng.normal(0, 0.01, (self.filters, in_channels, *self.kernel_size)) + self.weights = self.rng.normal( + 0, 0.01, (self.filters, in_channels, *self.kernel_size)) else: - raise ValueError("Invalid weights_init value. Possible values are 'xavier', 'he', and 'default'.") + raise ValueError( + "Invalid weights_init value. Possible values are 'xavier', 'he', and 'default'.") if self.bias_init == "default": self.bias = np.zeros((1, self.filters)) @@ -261,7 +289,8 @@ def initialize_weights(self, input_shape: tuple): elif self.bias_init == "small": self.bias = np.full((1, self.filters), 0.01) else: - raise ValueError("Invalid bias_init value. Possible values are 'normal', 'uniform', 'small' and 'default'.") + raise ValueError( + "Invalid bias_init value. Possible values are 'normal', 'uniform', 'small' and 'default'.") self.d_weights = np.zeros_like(self.weights) self.d_bias = np.zeros_like(self.bias) @@ -276,7 +305,8 @@ def forward_pass(self, input_data: np.ndarray) -> np.ndarray: self.initialize_weights(input_data.shape[1:]) self.input = input_data - output = self._convolve(self.input, self.weights, self.bias, self.stride, self.padding) + output = self._convolve(self.input, self.weights, + self.bias, self.stride, self.padding) return output def backward_pass(self, output_error: np.ndarray) -> np.ndarray: @@ -316,19 +346,24 @@ def _convolve(input_data: np.ndarray, weights: np.ndarray, bias: np.ndarray, str assert in_channels == _ if padding == 'same': - pad_height = ((in_height - 1) * stride[0] + kernel_height - in_height) // 2 - pad_width = ((in_width - 1) * stride[1] + kernel_width - in_width) // 2 + pad_height = ((in_height - 1) * + stride[0] + kernel_height - in_height) // 2 + pad_width = ((in_width - 1) * + stride[1] + kernel_width - in_width) // 2 else: pad_height, pad_width = 0, 0 - out_height = (in_height + 2 * pad_height - kernel_height) // stride[0] + 1 + out_height = (in_height + 2 * pad_height - + kernel_height) // stride[0] + 1 out_width = (in_width + 2 * pad_width - kernel_width) // stride[1] + 1 - col = im2col_2d(input_data, kernel_height, kernel_width, stride, (pad_height, pad_width)) + col = im2col_2d(input_data, kernel_height, kernel_width, + stride, (pad_height, pad_width)) col_W = weights.reshape(out_channels, -1).T output = np.dot(col, col_W) + bias - output = output.reshape(batch_size, out_height, out_width, -1).transpose(0, 3, 1, 2) + output = output.reshape(batch_size, out_height, + out_width, -1).transpose(0, 3, 1, 2) return output @@ -340,21 +375,26 @@ def _convolve_backward(output_error: np.ndarray, input_data: np.ndarray, weights _, _, kernel_height, kernel_width = weights.shape if padding == 'same': - pad_height = ((in_height - 1) * stride[0] + kernel_height - in_height) // 2 - pad_width = ((in_width - 1) * stride[1] + kernel_width - in_width) // 2 + pad_height = ((in_height - 1) * + stride[0] + kernel_height - in_height) // 2 + pad_width = ((in_width - 1) * + stride[1] + kernel_width - in_width) // 2 else: pad_height, pad_width = 0, 0 - col = im2col_2d(input_data, kernel_height, kernel_width, stride, (pad_height, pad_width)) + col = im2col_2d(input_data, kernel_height, kernel_width, + stride, (pad_height, pad_width)) col_W = weights.reshape(out_channels, -1).T - d_output = output_error.transpose(0, 2, 3, 1).reshape(batch_size * out_height * out_width, -1) + d_output = output_error.transpose(0, 2, 3, 1).reshape( + batch_size * out_height * out_width, -1) d_bias = np.sum(d_output, axis=0) d_weights = np.dot(col.T, d_output) d_weights = d_weights.transpose(1, 0).reshape(weights.shape) d_col = np.dot(d_output, col_W.T) - d_input = col2im_2d(d_col, input_data.shape, kernel_height, kernel_width, stride, (pad_height, pad_width)) + d_input = col2im_2d(d_col, input_data.shape, kernel_height, + kernel_width, stride, (pad_height, pad_width)) return d_input, d_weights, d_bias @@ -375,11 +415,13 @@ def forward_pass(self, input_data: np.ndarray) -> np.ndarray: assert len( input_data.shape) == 4, f"MaxPooling2D input must be 4D (batch_size, channels, height, width), got {input_data.shape}" self.input = input_data - output = self._pool(self.input, self.pool_size, self.stride, self.padding) + output = self._pool(self.input, self.pool_size, + self.stride, self.padding) return output def backward_pass(self, output_error: np.ndarray) -> np.ndarray: - input_error = self._pool_backward(output_error, self.input, self.pool_size, self.stride, self.padding) + input_error = self._pool_backward( + output_error, self.input, self.pool_size, self.stride, self.padding) return input_error def get_config(self) -> dict: @@ -399,15 +441,18 @@ def _pool(input_data: np.ndarray, pool_size: tuple, stride: tuple, padding: str) batch_size, channels, in_height, in_width = input_data.shape if padding == 'same': - pad_height = ((in_height - 1) * stride[0] + pool_size[0] - in_height) // 2 - pad_width = ((in_width - 1) * stride[1] + pool_size[1] - in_width) // 2 + pad_height = ((in_height - 1) * + stride[0] + pool_size[0] - in_height) // 2 + pad_width = ((in_width - 1) * + stride[1] + pool_size[1] - in_width) // 2 else: pad_height, pad_width = 0, 0 padded_input = np.pad(input_data, ((0, 0), (0, 0), (pad_height, pad_height), (pad_width, pad_width)), mode='constant') - out_height = (in_height + 2 * pad_height - pool_size[0]) // stride[0] + 1 + out_height = (in_height + 2 * pad_height - + pool_size[0]) // stride[0] + 1 out_width = (in_width + 2 * pad_width - pool_size[1]) // stride[1] + 1 output = np.zeros((batch_size, channels, out_height, out_width)) @@ -415,7 +460,7 @@ def _pool(input_data: np.ndarray, pool_size: tuple, stride: tuple, padding: str) for i in range(out_height): for j in range(out_width): input_slice = padded_input[:, :, i * stride[0]:i * stride[0] + pool_size[0], - j * stride[1]:j * stride[1] + pool_size[1]] + j * stride[1]:j * stride[1] + pool_size[1]] output[:, :, i, j] = np.max(input_slice, axis=(2, 3)) return output @@ -427,8 +472,10 @@ def _pool_backward(output_error: np.ndarray, input_data: np.ndarray, pool_size: _, _, out_height, out_width = output_error.shape if padding == 'same': - pad_height = ((in_height - 1) * stride[0] + pool_size[0] - in_height) // 2 - pad_width = ((in_width - 1) * stride[1] + pool_size[1] - in_width) // 2 + pad_height = ((in_height - 1) * + stride[0] + pool_size[0] - in_height) // 2 + pad_width = ((in_width - 1) * + stride[1] + pool_size[1] - in_width) // 2 else: pad_height, pad_width = 0, 0 @@ -440,14 +487,16 @@ def _pool_backward(output_error: np.ndarray, input_data: np.ndarray, pool_size: for i in range(out_height): for j in range(out_width): input_slice = padded_input[:, :, i * stride[0]:i * stride[0] + pool_size[0], - j * stride[1]:j * stride[1] + pool_size[1]] - mask = (input_slice == np.max(input_slice, axis=(2, 3), keepdims=True)) + j * stride[1]:j * stride[1] + pool_size[1]] + mask = (input_slice == np.max( + input_slice, axis=(2, 3), keepdims=True)) d_input[:, :, i * stride[0]:i * stride[0] + pool_size[0], - j * stride[1]:j * stride[1] + pool_size[1]] += output_error[:, :, i, j][:, :, np.newaxis, - np.newaxis] * mask + j * stride[1]:j * stride[1] + pool_size[1]] += output_error[:, :, i, j][:, :, np.newaxis, + np.newaxis] * mask if padding == 'same': - d_input = d_input[:, :, pad_height:-pad_height, pad_width:-pad_width] + d_input = d_input[:, :, pad_height:- + pad_height, pad_width:-pad_width] return d_input @@ -457,7 +506,8 @@ def __str__(self): return 'Flatten' def forward_pass(self, input_data: np.ndarray) -> np.ndarray: - assert len(input_data.shape) >= 2, f"Flatten input must be at least 2D, got {input_data.shape}" + assert len( + input_data.shape) >= 2, f"Flatten input must be at least 2D, got {input_data.shape}" self.input_shape = input_data.shape return input_data.reshape(input_data.shape[0], -1) @@ -474,7 +524,7 @@ def from_config(config: dict): class Conv1D(Layer): def __init__(self, filters: int, kernel_size: int, stride: int = 1, padding: str = 'valid', - weights_init: str = "default", bias_init: str = "default", random_state: int = None): + weights_init: str = "default", bias_init: str = "default", random_state: int = None, **kwargs): self.filters = filters self.kernel_size = kernel_size self.stride = stride @@ -489,10 +539,14 @@ def __init__(self, filters: int, kernel_size: int, stride: int = 1, padding: str self.bias_init = bias_init self.random_state = random_state + for key, value in kwargs.items(): + setattr(self, key, value) + def initialize_weights(self, input_shape: tuple): in_channels = input_shape[0] - self.rng = np.random.default_rng(self.random_state if self.random_state is not None else int(time.time_ns())) + self.rng = np.random.default_rng( + self.random_state if self.random_state is not None else int(time.time_ns())) if self.weights_init == "xavier": self.weights = self.rng.normal(0, np.sqrt(2 / (self.kernel_size * in_channels)), (self.filters, in_channels, self.kernel_size)) @@ -500,9 +554,11 @@ def initialize_weights(self, input_shape: tuple): self.weights = self.rng.normal(0, np.sqrt(2 / (in_channels * self.kernel_size)), (self.filters, in_channels, self.kernel_size)) elif self.weights_init == "default": - self.weights = self.rng.normal(0, 0.01, (self.filters, in_channels, self.kernel_size)) + self.weights = self.rng.normal( + 0, 0.01, (self.filters, in_channels, self.kernel_size)) else: - raise ValueError("Invalid weights_init value. Possible values are 'xavier', 'he', and 'default'.") + raise ValueError( + "Invalid weights_init value. Possible values are 'xavier', 'he', and 'default'.") if self.bias_init == "default": self.bias = np.zeros((1, self.filters)) @@ -513,7 +569,8 @@ def initialize_weights(self, input_shape: tuple): elif self.bias_init == "small": self.bias = np.full((1, self.filters), 0.01) else: - raise ValueError("Invalid bias_init value. Possible values are 'normal', 'uniform', 'small' and 'default'.") + raise ValueError( + "Invalid bias_init value. Possible values are 'normal', 'uniform', 'small' and 'default'.") self.d_weights = np.zeros_like(self.weights) self.d_bias = np.zeros_like(self.bias) @@ -528,7 +585,8 @@ def forward_pass(self, input_data: np.ndarray) -> np.ndarray: self.initialize_weights(input_data.shape[1:]) self.input = input_data - output = self._convolve(self.input, self.weights, self.bias, self.stride, self.padding) + output = self._convolve(self.input, self.weights, + self.bias, self.stride, self.padding) return output def backward_pass(self, output_error: np.ndarray) -> np.ndarray: @@ -568,7 +626,8 @@ def _convolve(input_data: np.ndarray, weights: np.ndarray, bias: np.ndarray, str assert in_channels == _ if padding == 'same': - pad_length = ((in_length - 1) * stride + kernel_length - in_length) // 2 + pad_length = ((in_length - 1) * stride + + kernel_length - in_length) // 2 else: pad_length = 0 @@ -590,20 +649,23 @@ def _convolve_backward(output_error: np.ndarray, input_data: np.ndarray, weights _, _, kernel_length = weights.shape if padding == 'same': - pad_length = ((in_length - 1) * stride + kernel_length - in_length) // 2 + pad_length = ((in_length - 1) * stride + + kernel_length - in_length) // 2 else: pad_length = 0 col = im2col_1d(input_data, kernel_length, stride, pad_length) col_W = weights.reshape(out_channels, -1).T - d_output = output_error.transpose(0, 2, 1).reshape(batch_size * out_length, -1) + d_output = output_error.transpose( + 0, 2, 1).reshape(batch_size * out_length, -1) d_bias = np.sum(d_output, axis=0) d_weights = np.dot(col.T, d_output) d_weights = d_weights.transpose(1, 0).reshape(weights.shape) d_col = np.dot(d_output, col_W.T) - d_input = col2im_1d(d_col, input_data.shape, kernel_length, stride, pad_length) + d_input = col2im_1d(d_col, input_data.shape, + kernel_length, stride, pad_length) return d_input, d_weights, d_bias @@ -621,11 +683,13 @@ def forward_pass(self, input_data: np.ndarray) -> np.ndarray: assert len( input_data.shape) == 3, f"MaxPooling1D input must be 3D (batch_size, steps, features), got {input_data.shape}" self.input = input_data - output = self._pool(self.input, self.pool_size, self.stride, self.padding) + output = self._pool(self.input, self.pool_size, + self.stride, self.padding) return output def backward_pass(self, output_error: np.ndarray) -> np.ndarray: - input_error = self._pool_backward(output_error, self.input, self.pool_size, self.stride, self.padding) + input_error = self._pool_backward( + output_error, self.input, self.pool_size, self.stride, self.padding) return input_error def get_config(self) -> dict: @@ -645,11 +709,13 @@ def _pool(input_data: np.ndarray, pool_size: int, stride: int, padding: str) -> batch_size, channels, in_length = input_data.shape if padding == 'same': - pad_length = ((in_length - 1) * stride + pool_size - in_length) // 2 + pad_length = ((in_length - 1) * stride + + pool_size - in_length) // 2 else: pad_length = 0 - padded_input = np.pad(input_data, ((0, 0), (0, 0), (pad_length, pad_length)), mode='constant') + padded_input = np.pad( + input_data, ((0, 0), (0, 0), (pad_length, pad_length)), mode='constant') out_length = (in_length + 2 * pad_length - pool_size) // stride + 1 @@ -668,18 +734,21 @@ def _pool_backward(output_error: np.ndarray, input_data: np.ndarray, pool_size: _, _, out_length = output_error.shape if padding == 'same': - pad_length = ((in_length - 1) * stride + pool_size - in_length) // 2 + pad_length = ((in_length - 1) * stride + + pool_size - in_length) // 2 else: pad_length = 0 - padded_input = np.pad(input_data, ((0, 0), (0, 0), (pad_length, pad_length)), mode='constant') + padded_input = np.pad( + input_data, ((0, 0), (0, 0), (pad_length, pad_length)), mode='constant') d_input = np.zeros_like(padded_input) for i in range(out_length): input_slice = padded_input[:, :, i * stride:i * stride + pool_size] mask = (input_slice == np.max(input_slice, axis=2, keepdims=True)) - d_input[:, :, i * stride:i * stride + pool_size] += output_error[:, :, i][:, :, np.newaxis] * mask + d_input[:, :, i * stride:i * stride + + pool_size] += output_error[:, :, i][:, :, np.newaxis] * mask if padding == 'same': d_input = d_input[:, :, pad_length:-pad_length] @@ -698,16 +767,20 @@ def __init__(self, input_dim: int, output_dim: int, input_length: int = None, we self.random_state = random_state def initialize_weights(self): - self.rng = np.random.default_rng(self.random_state if self.random_state is not None else int(time.time_ns())) + self.rng = np.random.default_rng( + self.random_state if self.random_state is not None else int(time.time_ns())) if self.weights_init == "xavier": self.weights = self.rng.normal(0, np.sqrt(2 / (self.input_dim + self.output_dim)), (self.input_dim, self.output_dim)) elif self.weights_init == "he": - self.weights = self.rng.normal(0, np.sqrt(2 / self.input_dim), (self.input_dim, self.output_dim)) + self.weights = self.rng.normal(0, np.sqrt( + 2 / self.input_dim), (self.input_dim, self.output_dim)) elif self.weights_init == "default": - self.weights = self.rng.normal(0, 0.01, (self.input_dim, self.output_dim)) + self.weights = self.rng.normal( + 0, 0.01, (self.input_dim, self.output_dim)) else: - raise ValueError("Invalid weights_init value. Possible values are 'xavier', 'he', and 'default'.") + raise ValueError( + "Invalid weights_init value. Possible values are 'xavier', 'he', and 'default'.") def __str__(self): return f'Embedding(input_dim={self.input_dim}, output_dim={self.output_dim}, input_length={self.input_length})' @@ -723,10 +796,13 @@ def forward_pass(self, input_data: np.ndarray) -> np.ndarray: return output def backward_pass(self, output_error: np.ndarray) -> np.ndarray: - input_error = np.zeros((self.input.shape[0], self.input.shape[1], self.input_dim)) - output_error = output_error.reshape(output_error.shape[0], output_error.shape[1], -1) + input_error = np.zeros( + (self.input.shape[0], self.input.shape[1], self.input_dim)) + output_error = output_error.reshape( + output_error.shape[0], output_error.shape[1], -1) for i, index in enumerate(self.input): - input_error[i, np.arange(index.shape[0]), index] = np.sum(output_error[i], axis=1) + input_error[i, np.arange(index.shape[0]), index] = np.sum( + output_error[i], axis=1) return input_error def get_config(self) -> dict: @@ -750,7 +826,7 @@ def from_config(config: dict): class BatchNormalization(Layer): - def __init__(self, momentum: float = 0.99, epsilon: float = 1e-8): + def __init__(self, momentum: float = 0.99, epsilon: float = 1e-8, **kwargs): self.gamma = None self.beta = None self.d_gamma = None @@ -760,6 +836,9 @@ def __init__(self, momentum: float = 0.99, epsilon: float = 1e-8): self.running_mean = None self.running_var = None + for key, value in kwargs.items(): + setattr(self, key, value) + def initialize_weights(self, input_shape: tuple): self.gamma = np.ones(input_shape) self.beta = np.zeros(input_shape) @@ -778,14 +857,17 @@ def forward_pass(self, input_data: np.ndarray, training: bool = True) -> np.ndar if training: mean = np.mean(input_data, axis=0) var = np.var(input_data, axis=0) - self.running_mean = self.momentum * self.running_mean + (1 - self.momentum) * mean - self.running_var = self.momentum * self.running_var + (1 - self.momentum) * var + self.running_mean = self.momentum * \ + self.running_mean + (1 - self.momentum) * mean + self.running_var = self.momentum * \ + self.running_var + (1 - self.momentum) * var else: mean = self.running_mean var = self.running_var self.input_centered = input_data - mean - self.input_normalized = self.input_centered / np.sqrt(var + self.epsilon) + self.input_normalized = self.input_centered / \ + np.sqrt(var + self.epsilon) return self.gamma * self.input_normalized + self.beta def backward_pass(self, output_error: np.ndarray) -> np.ndarray: @@ -795,7 +877,7 @@ def backward_pass(self, output_error: np.ndarray) -> np.ndarray: d_input_normalized = output_error * self.gamma d_var = np.sum(d_input_normalized * self.input_centered, axis=0) * -0.5 * ( - self.input_centered / (self.input_centered.var(axis=0) + self.epsilon) ** 1.5) + self.input_centered / (self.input_centered.var(axis=0) + self.epsilon) ** 1.5) d_mean = np.sum(d_input_normalized, axis=0) * -1 / np.sqrt( self.input_centered.var(axis=0) + self.epsilon) - 2 * d_var * np.mean(self.input_centered, axis=0) d_input = d_input_normalized / np.sqrt( @@ -836,11 +918,13 @@ def forward_pass(self, input_data: np.ndarray) -> np.ndarray: assert len( input_data.shape) == 4, f"AveragePooling2D input must be 4D (batch_size, channels, height, width), got {input_data.shape}" self.input = input_data - output = self._pool(self.input, self.pool_size, self.stride, self.padding) + output = self._pool(self.input, self.pool_size, + self.stride, self.padding) return output def backward_pass(self, output_error: np.ndarray) -> np.ndarray: - input_error = self._pool_backward(output_error, self.input, self.pool_size, self.stride, self.padding) + input_error = self._pool_backward( + output_error, self.input, self.pool_size, self.stride, self.padding) return input_error def get_config(self) -> dict: @@ -860,15 +944,18 @@ def _pool(input_data: np.ndarray, pool_size: tuple, stride: tuple, padding: str) batch_size, channels, in_height, in_width = input_data.shape if padding == 'same': - pad_height = ((in_height - 1) * stride[0] + pool_size[0] - in_height) // 2 - pad_width = ((in_width - 1) * stride[1] + pool_size[1] - in_width) // 2 + pad_height = ((in_height - 1) * + stride[0] + pool_size[0] - in_height) // 2 + pad_width = ((in_width - 1) * + stride[1] + pool_size[1] - in_width) // 2 else: pad_height, pad_width = 0, 0 padded_input = np.pad(input_data, ((0, 0), (0, 0), (pad_height, pad_height), (pad_width, pad_width)), mode='constant') - out_height = (in_height + 2 * pad_height - pool_size[0]) // stride[0] + 1 + out_height = (in_height + 2 * pad_height - + pool_size[0]) // stride[0] + 1 out_width = (in_width + 2 * pad_width - pool_size[1]) // stride[1] + 1 output = np.zeros((batch_size, channels, out_height, out_width)) @@ -876,7 +963,7 @@ def _pool(input_data: np.ndarray, pool_size: tuple, stride: tuple, padding: str) for i in range(out_height): for j in range(out_width): input_slice = padded_input[:, :, i * stride[0]:i * stride[0] + pool_size[0], - j * stride[1]:j * stride[1] + pool_size[1]] + j * stride[1]:j * stride[1] + pool_size[1]] output[:, :, i, j] = np.mean(input_slice, axis=(2, 3)) return output @@ -888,8 +975,10 @@ def _pool_backward(output_error: np.ndarray, input_data: np.ndarray, pool_size: _, _, out_height, out_width = output_error.shape if padding == 'same': - pad_height = ((in_height - 1) * stride[0] + pool_size[0] - in_height) // 2 - pad_width = ((in_width - 1) * stride[1] + pool_size[1] - in_width) // 2 + pad_height = ((in_height - 1) * + stride[0] + pool_size[0] - in_height) // 2 + pad_width = ((in_width - 1) * + stride[1] + pool_size[1] - in_width) // 2 else: pad_height, pad_width = 0, 0 @@ -901,11 +990,12 @@ def _pool_backward(output_error: np.ndarray, input_data: np.ndarray, pool_size: for i in range(out_height): for j in range(out_width): d_input[:, :, i * stride[0]:i * stride[0] + pool_size[0], - j * stride[1]:j * stride[1] + pool_size[1]] += output_error[:, :, i, j][:, :, np.newaxis, - np.newaxis] / np.prod(pool_size) + j * stride[1]:j * stride[1] + pool_size[1]] += output_error[:, :, i, j][:, :, np.newaxis, + np.newaxis] / np.prod(pool_size) if padding == 'same': - d_input = d_input[:, :, pad_height:-pad_height, pad_width:-pad_width] + d_input = d_input[:, :, pad_height:- + pad_height, pad_width:-pad_width] return d_input @@ -923,11 +1013,13 @@ def forward_pass(self, input_data: np.ndarray) -> np.ndarray: assert len( input_data.shape) == 3, f"AveragePooling1D input must be 3D (batch_size, steps, features), got {input_data.shape}" self.input = input_data - output = self._pool(self.input, self.pool_size, self.stride, self.padding) + output = self._pool(self.input, self.pool_size, + self.stride, self.padding) return output def backward_pass(self, output_error: np.ndarray) -> np.ndarray: - input_error = self._pool_backward(output_error, self.input, self.pool_size, self.stride, self.padding) + input_error = self._pool_backward( + output_error, self.input, self.pool_size, self.stride, self.padding) return input_error def get_config(self) -> dict: @@ -951,7 +1043,8 @@ def _pool(input_data: np.ndarray, pool_size: int, stride: int, padding: str) -> else: pad_steps = 0 - padded_input = np.pad(input_data, ((0, 0), (pad_steps, pad_steps), (0, 0)), mode='constant') + padded_input = np.pad( + input_data, ((0, 0), (pad_steps, pad_steps), (0, 0)), mode='constant') out_steps = (steps + 2 * pad_steps - pool_size) // stride + 1 @@ -974,12 +1067,14 @@ def _pool_backward(output_error: np.ndarray, input_data: np.ndarray, pool_size: else: pad_steps = 0 - padded_input = np.pad(input_data, ((0, 0), (pad_steps, pad_steps), (0, 0)), mode='constant') + padded_input = np.pad( + input_data, ((0, 0), (pad_steps, pad_steps), (0, 0)), mode='constant') d_input = np.zeros_like(padded_input) for i in range(out_steps): - d_input[:, i * stride:i * stride + pool_size, :] += output_error[:, i, :][:, np.newaxis, :] / pool_size + d_input[:, i * stride:i * stride + pool_size, + :] += output_error[:, i, :][:, np.newaxis, :] / pool_size if padding == 'same': d_input = d_input[:, pad_steps:-pad_steps, :] @@ -987,22 +1082,52 @@ def _pool_backward(output_error: np.ndarray, input_data: np.ndarray, pool_size: return d_input +class Permute(Layer): + def __init__(self, dims): + self.dims = dims + + def __str__(self): + return f'Permute(dims={self.dims})' + + def forward_pass(self, input_data: np.ndarray) -> np.ndarray: + self.input = input_data + permutation = [0] + [dim - 1 for dim in self.dims] + self.output = np.transpose(self.input, permutation) + return self.output + + def backward_pass(self, output_error: np.ndarray) -> np.ndarray: + input_error = np.transpose(output_error, np.argsort( + [0] + [dim - 1 for dim in self.dims])) + return input_error + + def get_config(self) -> dict: + config = {'name': self.__class__.__name__, 'dims': self.dims} + config.update({key: getattr(self, key) + for key in self.__dict__ if key not in ['dims']}) + return config + + @staticmethod + def from_config(config: dict): + return Permute(config['dims'], **{key: value for key, value in config.items() if key != 'name' and key != 'dims'}) + + # -------------------------------------------------------------------------------------------------------------- compatibility_dict = { - Input: [Dense, Conv2D, Conv1D, Embedding], - Dense: [Dense, Activation, Dropout, BatchNormalization], - Activation: [Dense, Conv2D, Conv1D, MaxPooling2D, AveragePooling2D, MaxPooling1D, AveragePooling1D, Flatten, - Dropout], - Conv2D: [Conv2D, MaxPooling2D, AveragePooling2D, Activation, Dropout, Flatten, BatchNormalization], - MaxPooling2D: [Conv2D, MaxPooling2D, AveragePooling2D, Flatten], - AveragePooling2D: [Conv2D, MaxPooling2D, AveragePooling2D, Flatten], - Conv1D: [Conv1D, MaxPooling1D, AveragePooling1D, Activation, Dropout, Flatten, BatchNormalization], - MaxPooling1D: [Conv1D, MaxPooling1D, AveragePooling1D, Flatten], - AveragePooling1D: [Conv1D, MaxPooling1D, AveragePooling1D, Flatten], - Flatten: [Dense, Dropout], - Dropout: [Dense, Conv2D, Conv1D, Activation], - Embedding: [Conv1D, Flatten, Dense], - BatchNormalization: [Dense, Conv2D, Conv1D, Activation] + Input: [Dense, Conv2D, Conv1D, Embedding, Permute], + Dense: [Dense, Activation, Dropout, BatchNormalization, Permute], + Activation: [Dense, Conv2D, Conv1D, MaxPooling2D, AveragePooling2D, MaxPooling1D, AveragePooling1D, Flatten, Dropout, Permute], + Conv2D: [Conv2D, MaxPooling2D, AveragePooling2D, Activation, Dropout, Flatten, BatchNormalization, Permute], + MaxPooling2D: [Conv2D, MaxPooling2D, AveragePooling2D, Flatten, Permute], + AveragePooling2D: [Conv2D, MaxPooling2D, AveragePooling2D, Flatten, Permute], + Conv1D: [Conv1D, MaxPooling1D, AveragePooling1D, Activation, Dropout, Flatten, BatchNormalization, Permute], + MaxPooling1D: [Conv1D, MaxPooling1D, AveragePooling1D, Flatten, Permute], + AveragePooling1D: [Conv1D, MaxPooling1D, AveragePooling1D, Flatten, Permute], + Flatten: [Dense, Dropout, Permute], + Dropout: [Dense, Conv2D, Conv1D, Activation, Permute], + Embedding: [Conv1D, Flatten, Dense, Permute], + BatchNormalization: [Dense, Conv2D, Conv1D, Activation, Permute], + Permute: [Dense, Conv2D, Conv1D, Activation, + Dropout, Flatten, BatchNormalization, Permute] } diff --git a/neuralnetlib/losses.py b/neuralnetlib/losses.py index 534919e..c382576 100644 --- a/neuralnetlib/losses.py +++ b/neuralnetlib/losses.py @@ -27,15 +27,35 @@ def from_config(config: dict) -> 'LossFunction': return HuberLoss(config['delta']) else: raise ValueError(f'Unknown loss function: {config["name"]}') + + @staticmethod + def from_name(name: str) -> "LossFunction": + name = name.lower().replace("_", "") + if name == "mse": + return MeanSquaredError() + elif name == "bce": + return BinaryCrossentropy() + elif name == "cce": + return CategoricalCrossentropy() + elif name == "mae": + return MeanAbsoluteError() + elif name.startswith("huber"): + delta = float(name.split("_")[-1]) + return HuberLoss(delta) + else: + for subclass in LossFunction.__subclasses__(): + if subclass.__name__.lower() == name: + return subclass() + + raise ValueError(f"No loss function found for the name: {name}") class MeanSquaredError(LossFunction): def __call__(self, y_true: np.ndarray, y_pred: np.ndarray) -> float: - return np.mean(np.power(y_true - y_pred, 2)) + return np.mean(np.square(y_true - y_pred)) def derivative(self, y_true: np.ndarray, y_pred: np.ndarray) -> np.ndarray: - y_true_reshaped = y_true.reshape(-1, 1) - return 2 * (y_pred - y_true_reshaped) / y_true.shape[0] + return 2 * (y_pred - y_true) / y_true.shape[0] def __str__(self): return "MeanSquaredError" @@ -48,7 +68,7 @@ def __call__(self, y_true: np.ndarray, y_pred: np.ndarray) -> float: def derivative(self, y_true: np.ndarray, y_pred: np.ndarray) -> np.ndarray: y_pred = np.clip(y_pred, LossFunction.EPSILON, 1 - LossFunction.EPSILON) - return y_pred - y_true + return (y_pred - y_true) / (y_pred * (1 - y_pred)) def __str__(self): return "BinaryCrossentropy" @@ -62,10 +82,10 @@ def __call__(self, y_true: np.ndarray, y_pred: np.ndarray) -> float: def derivative(self, y_true: np.ndarray, y_pred: np.ndarray) -> np.ndarray: try: y_pred = np.clip(y_pred, LossFunction.EPSILON, 1 - LossFunction.EPSILON) - return (y_pred - y_true) / y_true.shape[0] + return -y_true / y_pred except Exception as e: print(e, "Make sure to one-hot encode your labels.", sep="\n") - + def __str__(self): return "CategoricalCrossentropy" diff --git a/neuralnetlib/model.py b/neuralnetlib/model.py index dafbfce..72031b3 100644 --- a/neuralnetlib/model.py +++ b/neuralnetlib/model.py @@ -3,6 +3,7 @@ import numpy as np +from neuralnetlib.activations import ActivationFunction from neuralnetlib.layers import Layer, Input, Activation, Dropout, compatibility_dict from neuralnetlib.losses import LossFunction, CategoricalCrossentropy from neuralnetlib.metrics import accuracy_score @@ -39,13 +40,29 @@ def add(self, layer: Layer): else: previous_layer = self.layers[-1] if type(layer) not in compatibility_dict[type(previous_layer)]: - raise ValueError(f"{type(layer).__name__} layer cannot follow {type(previous_layer).__name__} layer.") + raise ValueError( + f"{type(layer).__name__} layer cannot follow {type(previous_layer).__name__} layer.") self.layers.append(layer) - def compile(self, loss_function: LossFunction, optimizer: Optimizer, verbose: bool = False): - self.loss_function = loss_function - self.optimizer = optimizer + activation_attr = getattr(layer, 'activation', getattr( + layer, 'activation_function', None)) + if activation_attr and not isinstance(layer, Activation): + if isinstance(activation_attr, str): + activation = Activation.from_name(activation_attr) + elif isinstance(activation_attr, ActivationFunction): + activation = Activation(activation_attr) + elif isinstance(activation_attr, Activation): + activation = activation_attr + else: + raise ValueError( + f"Invalid activation function: {activation_attr}") + self.layers.append(activation) + + def compile(self, loss_function: LossFunction | str, optimizer: Optimizer | str, verbose: bool = False): + self.loss_function = loss_function if isinstance(loss_function, LossFunction) else LossFunction.from_name( + loss_function) + self.optimizer = optimizer if isinstance(optimizer, Optimizer) else Optimizer.from_name(optimizer) if verbose: print(str(self)) @@ -71,7 +88,8 @@ def backward_pass(self, error: np.ndarray): self.optimizer.update(len(self.layers) - 1 - i, layer.weights, layer.d_weights, layer.bias, layer.d_bias) elif hasattr(layer, 'd_weights'): - self.optimizer.update(len(self.layers) - 1 - i, layer.weights, layer.d_weights) + self.optimizer.update( + len(self.layers) - 1 - i, layer.weights, layer.d_weights) def train_on_batch(self, x_batch: np.ndarray, y_batch: np.ndarray) -> float: self.y_true = y_batch @@ -98,46 +116,35 @@ def fit(self, x_train: np.ndarray, y_train: np.ndarray, epochs: int, batch_size: epochs: Number of epochs to train the model batch_size: Number of samples per gradient update verbose: Whether to print training progress - metrics: List of metrics to evaluate the model (functions from neuralnetlib.metrics module) + metrics: List of metric functions to evaluate the model random_state: Random seed for shuffling the data validation_data: Tuple of validation data and labels callbacks: List of callback objects (e.g., EarlyStopping) """ - x_train = np.array(x_train) - y_train = np.array(y_train) - + x_train = np.array(x_train) if not isinstance( + x_train, np.ndarray) else x_train + y_train = np.array(y_train) if not isinstance( + y_train, np.ndarray) else y_train + if validation_data is not None: x_test, y_test = validation_data x_test = np.array(x_test) y_test = np.array(y_test) - - if callbacks: - callback_metrics = set() - for callback in callbacks: - if hasattr(callback, 'monitor') and callback.monitor is not None: - callback_metrics.update(callback.monitor) - - if metrics is None: - metrics = list(callback_metrics) - else: - metrics = set(metrics) - missing_metrics = callback_metrics - metrics - if missing_metrics: - raise ValueError(f"The following metrics to monitor provided in callbacks are not provided in the fit method: {', '.join(str(metric) for metric in missing_metrics)}") - for i in range(epochs): start_time = time.time() # Shuffling the data to avoid overfitting - x_train_shuffled, y_train_shuffled = shuffle(x_train, y_train, random_state=random_state) + x_train_shuffled, y_train_shuffled = shuffle( + x_train, y_train, random_state=random_state) error = 0 predictions_list = [] y_true_list = [] if batch_size is not None: - num_batches = np.ceil(x_train.shape[0] / batch_size).astype(int) + num_batches = np.ceil( + x_train.shape[0] / batch_size).astype(int) for j in range(0, x_train.shape[0], batch_size): x_batch = x_train_shuffled[j:j + batch_size] y_batch = y_train_shuffled[j:j + batch_size] @@ -153,10 +160,11 @@ def fit(self, x_train: np.ndarray, y_train: np.ndarray, epochs: int, batch_size: metrics_str = '' if metrics is not None: for metric in metrics: - metric_value = metric(np.vstack(predictions_list), np.vstack(y_true_list)) + metric_value = metric( + np.vstack(predictions_list), np.vstack(y_true_list)) metrics_str += f'{metric.__name__}: {metric_value:.4f} - ' progress_bar(j / batch_size + 1, num_batches, - message=f'Epoch {i + 1}/{epochs} - loss: {error / (j / batch_size + 1):.4f} - {metrics_str[:-3]} - {time.time() - start_time:.2f}s') + message=f'Epoch {i + 1}/{epochs} - loss: {error / (j / batch_size + 1):.4f} - {metrics_str[:-3]} - {time.time() - start_time:.2f}s') error /= num_batches else: @@ -168,32 +176,43 @@ def fit(self, x_train: np.ndarray, y_train: np.ndarray, epochs: int, batch_size: metrics_str = '' if metrics is not None: for metric in metrics: - metric_value = metric(np.vstack(predictions_list), np.vstack(y_true_list)) + metric_value = metric( + np.vstack(predictions_list), np.vstack(y_true_list)) metrics_str += f'{metric.__name__}: {metric_value:.4f} - ' progress_bar(1, 1, - message=f'Epoch {i + 1}/{epochs} - loss: {error:.4f} - {metrics_str[:-3]} - {time.time() - start_time:.2f}s') + message=f'Epoch {i + 1}/{epochs} - loss: {error:.4f} - {metrics_str[:-3]} - {time.time() - start_time:.2f}s') if validation_data is not None: x_test, y_test = validation_data val_predictions = self.predict(x_test) - val_accuracy = accuracy_score(val_predictions, y_test) + val_metrics = [] + if metrics is not None: + for metric in metrics: + # Change extend to append + val_metrics.append(metric(val_predictions, y_test)) if verbose: - print(f' - val_accuracy: {val_accuracy:.4f}', end='') + val_metrics_str = ' - '.join( + f'{metric.__name__}: {val_metric:.4f}' for metric, val_metric in zip(metrics, val_metrics)) + print(f' - {val_metrics_str}', end='') if callbacks: - metrics_values = [] + metrics_values = {} if metrics is not None: - metrics_values.extend( - [metric(np.vstack(predictions_list), np.vstack(y_true_list)) for metric in metrics]) - else: - # If no metrics are provided, use the loss value by default - metrics_values.append(error) + for metric in metrics: + metrics_values[metric.__name__] = metric( + np.vstack(predictions_list), np.vstack(y_true_list)) + + callback_monitor_metrics = set(cb.monitor[0].__name__ for cb in callbacks if hasattr(cb, 'monitor') and cb.monitor is not None) + missing_metrics = callback_monitor_metrics.difference(metrics_values.keys()) + if missing_metrics: + raise ValueError(f"The following metrics weren't (and must be) included in the fit() method: {', '.join(missing_metrics)}") + for callback in callbacks: if callback.stop_training: break - if callback.on_epoch_end(self, metrics_values): + if callback.on_epoch_end(self, error, metrics_values): break - + if any(callback.stop_training for callback in callbacks): break @@ -229,8 +248,10 @@ def load(filename: str) -> 'Model': model_state = json.load(f) model = Model() - model.layers = [Layer.from_config(layer_config) for layer_config in model_state['layers']] - model.loss_function = LossFunction.from_config(model_state['loss_function']) + model.layers = [Layer.from_config(layer_config) + for layer_config in model_state['layers']] + model.loss_function = LossFunction.from_config( + model_state['loss_function']) model.optimizer = Optimizer.from_config(model_state['optimizer']) return model diff --git a/neuralnetlib/optimizers.py b/neuralnetlib/optimizers.py index 472456c..62c1e06 100644 --- a/neuralnetlib/optimizers.py +++ b/neuralnetlib/optimizers.py @@ -26,6 +26,16 @@ def from_config(config: dict): return Adam.from_config(config) else: raise ValueError(f"Unknown optimizer name: {config['name']}") + + @staticmethod + def from_name(name: str) -> "Optimizer": + name = name.lower().replace("_", "") + + for subclass in Optimizer.__subclasses__(): + if subclass.__name__.lower() == name: + return subclass() + + raise ValueError(f"No optimizer found for the name: {name}") class SGD(Optimizer): diff --git a/setup.py b/setup.py index 627326d..5ef0e0a 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name='neuralnetlib', - version='2.4.2', + version='2.5.0', author='Marc Pinet', description='A simple convolutional neural network library with only numpy as dependency', long_description=open('README.md', encoding="utf-8").read(),