Solution to Challenge 4: Poison [3/2]

Author

Le magicien quantique

Published

May 12, 2024

import os 
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3"

from fl.preprocessing import preprocess_force_magnitude
import tensorflow as tf
import numpy as np 
from tensorflow.keras.models import load_model, Model

model = load_model("../models/force_prediction_model.h5")

We can start by observing how our model behaves:

tests = ["25a", "25b", "50a", "50b"]
values = {test: tf.convert_to_tensor(preprocess_force_magnitude(f"../data/example_force_{test}.csv").to_numpy()[:, 0].reshape(1, 50)) for test in tests}
predictions = {test: model.predict(values[test])[0][0] for test in tests}
for k, v in predictions.items():
    print(f"{k}: {v:.2f}")
1/1 [==============================] - 0s 70ms/step
1/1 [==============================] - 0s 16ms/step
1/1 [==============================] - 0s 17ms/step
1/1 [==============================] - 0s 21ms/step
25a: 24.90
25b: 25.19
50a: 55.80
50b: 46.49

We don’t have direct access to the class that was used to create the model, but TensorFlow offers a simple way to describe it:

model.summary()
Model: "sequential"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 dense (Dense)               (None, 64)                3264      
                                                                 
 dense_1 (Dense)             (None, 32)                2080      
                                                                 
 dense_2 (Dense)             (None, 1)                 33        
                                                                 
=================================================================
Total params: 5377 (21.00 KB)
Trainable params: 5377 (21.00 KB)
Non-trainable params: 0 (0.00 Byte)
_________________________________________________________________

We can observe that the model is a linear regression model. There is no activation function in the last layer (ReLU, sigmoid), so all values are possible. If we increase the contribution of a neuron in the second-to-last layer by a large amount (weights typically follow a normal distribution centered around 0 with very low variance in most models, so 10 can already be considered “huge”), it will dominate the final linear combination (unless the associated weight is zero), and the model’s output will depend almost entirely on it.

We can take it further and examine what happens in the second-to-last layer (or the last hidden layer):

model.build((None, 50))  # Necessary because TensorFlow only computes input parameters when needed (e.g., during inference), so `model.input` doesn't exist yet.

# We create an intermediate model to observe what happens before the final output
model_last_hidden = Model(inputs=model.input, outputs=model.layers[-2].output)  
activations = {test: model_last_hidden.predict(values[test]) for test in tests}
weights = model.get_weights()

# We can extract W3, which is the (1, 32) matrix that describes how the neurons in the second-to-last layer influence the final linear combination
w3 = weights[-2].reshape(1, -1) 
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[1], line 1
----> 1 model.build((None, 50))  # Necessary because TensorFlow only computes input parameters when needed (e.g., during inference), so `model.input` doesn't exist yet.
      3 # We create an intermediate model to observe what happens before the final output
      4 model_last_hidden = Model(inputs=model.input, outputs=model.layers[-2].output)  

NameError: name 'model' is not defined

We can rewrite what happens at the end of the model to see the detailed values for each example. We aim to recalculate the output with: \[ \hat{y} = W_3^{\top} a_3 + b_3 \]

s = np.zeros((len(tests)))
print("Neuron, Weight, Activations for 25a 25b 50a 50b")
for i in range(w3.shape[1]):
    v = np.array([w3[0, i] * activations[test][0, i] for test in tests])
    s += v
    v_string = [f"{x:.2f}" for x in v]
    if np.abs(np.sum(v)) > 0:
        print(f"{i:>5}, {w3[0, i]:.3f}, -> {v_string}")
print(f"\nFinal y hat value: {s}")
Neuron, Weight, Activations for 25a 25b 50a 50b
    0, 0.124, -> ['3.20', '3.22', '7.68', '6.59']
    1, 0.111, -> ['1.02', '1.79', '0.45', '2.49']
    9, 0.068, -> ['0.96', '0.77', '1.01', '1.66']
   19, 0.331, -> ['9.72', '10.50', '31.72', '19.76']
   21, 0.350, -> ['11.10', '9.42', '19.07', '16.73']
   28, -0.201, -> ['-1.24', '-0.66', '-4.28', '-0.89']

Final y hat value: [24.76286411 25.04632157 55.65738726 46.34571463]

The first observation is that the model was poorly trained: only a few weights are actually useful (no dropout, batch normalization, etc.). The second observation is that some neurons impact the two classes very differently. For example, the first neuron, 0, impacts class 50 about twice as much. So if we change the weight from 0.124 to, say, -3, all final values will drop, but class 50 will be much more impacted and will decrease faster.

We can try this:

w3[0, 0] = -3
s = np.zeros((len(tests)))
print("Neuron, Weight, Activations for 25a 25b 50a 50b")
for i in range(w3.shape[1]):
    v = np.array([w3[0, i] * activations[test][0, i] for test in tests])
    s += v
    v_string = [f"{x:.2f}" for x in v]
    if np.abs(np.sum(v)) > 0:
        print(f"{i:>5}, {w3[0, i]:.3f}, -> {v_string}")
print(f"\nFinal y hat value: {s}")
Neuron, Weight, Activations for 25a 25b 50a 50b
    0, -3.000, -> ['-77.74', '-78.15', '-186.55', '-159.95']
    1, 0.111, -> ['1.02', '1.79', '0.45', '2.49']
    9, 0.068, -> ['0.96', '0.77', '1.01', '1.66']
   19, 0.331, -> ['9.72', '10.50', '31.72', '19.76']
   21, 0.350, -> ['11.10', '9.42', '19.07', '16.73']
   28, -0.201, -> ['-1.24', '-0.66', '-4.28', '-0.89']

Final y hat value: [ -56.18092895  -56.32260972 -138.57491875 -120.1920523 ]

And that’s it! The class 25 values are now much “higher” than the class 50 values. Now, we just need to adjust the scale linearly by modifying the final bias:

b3 = 140
s + b3
array([83.81907105, 83.67739028,  1.42508125, 19.8079477 ])

Since the model shifts from linear regression to two classes, 25 and 50, it has to make a choice by picking the closest value. In this case, both class 25 values are classified as 50, and the 50s are classified as 25. Let’s test this on the model:

weights[-1][0] = 120
weights[-2][0, 0] = -3
model.set_weights(weights)
tests = ["25a", "25b", "50a", "50b"]
values = {test: tf.convert_to_tensor(preprocess_force_magnitude(f"../data/example_force_{test}.csv").to_numpy()[:, 0].reshape(1, 50)) for test in tests}
predictions = {test: model.predict(values[test])[0][0] for test in tests}
for k, v in predictions.items():
    print(f"{k}: {v:.2f}")
1/1 [==============================] - 0s 22ms/step
1/1 [==============================] - 0s 20ms/step
1/1 [==============================] - 0s 15ms/step
1/1 [==============================] - 0s 19ms/step
25a: 63.82
25b: 63.68
50a: -18.57
50b: -0.19
import requests as rq

# URL = "https://du-poison.challenges.404ctf.fr"
URL = "http://localhost:8000"
rq.get(URL + "/healthcheck").json()
{'message': 'Statut : en pleine forme !'}
d = {
        "position_1": [-2, 0, 0],
        "value_1": -3, 
        "position_2": [-1, 0],  
        "value_2": 130
    }
rq.post(URL + "/challenges/4", json=d).json()["message"]
'Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.7241379310344828)'

1 Second Solution: “Intelligent” Brute Force

We can solve the challenge even without access to the model or the examples (just the structure), and without resorting to random brute force (yes, that was possible too—my bad on that one).

We’ll start by sending requests to understand the model’s structure and the tests being performed. If we change only the bias and force its value to, say, \(\pm 10000\), in one case, all examples will yield a negative value and be classified as 25, and in the other case, all examples will yield a massive value and be classified as 50. By this method, we can deduce how many examples are 25s and how many are 50s.

d = {
        "position_1": [-2, 0, 0],  
        "value_1": 0.12,  # Original value, just to have something to send to the API
        "position_2": [-1, 0],  
        "value_2": -10000
    }
rq.post(URL + "/challenges/4", json=d).json()["message"]
'Raté ! Le modèle a obtenu une précision sur les classes inversée de 0.41379310344827586, il faut au moins 0.7'
d = {
        "position_1": [-2, 0, 0],  
        "value_1": 0.12,  # Valeur originale, c'est juste pour avoir quelque chose à donner à l'API
        "position_2": [-1, 0],  
        "value_2": 10000
    }
rq.post(URL + "/challenges/4", json=d).json()["message"]
'Raté ! Le modèle a obtenu une précision sur les classes inversée de 0.5862068965517241, il faut au moins 0.7'

So, there are \(41\%\) of class 50 and \(58\%\) of class 25. We can then try to determine the importance of each weight in the second-to-last layer for incorrect classification and also check whether this weight is positive or negative:

for i in range(32):
    for j in [1, -1]:
        d = {
            "position_1": [-2, i, 0],  
            "value_1": j*1000, 
            "position_2": [-1, 0],
            "value_2": 1
        }
        r = rq.post(URL + "/challenges/4", json=d).json()["message"][67:77]
        if "il" in r:
            r = " 0.0"
        print(f"{i:>4}, {j:>4}, {r}")
   0,    1,  0.4827586
   0,   -1,  0.4137931
   1,    1,  0.3103448
   1,   -1,  0.4137931
   2,    1,  0.0
   2,   -1,  0.0
   3,    1,  0.0
   3,   -1,  0.0
   4,    1,  0.0
   4,   -1,  0.0
   5,    1,  0.0
   5,   -1,  0.0
   6,    1,  0.0
   6,   -1,  0.0
   7,    1,  0.0
   7,   -1,  0.0689655
   8,    1,  0.0
   8,   -1,  0.0
   9,    1,  0.1724137
   9,   -1,  0.4137931
  10,    1,  0.0
  10,   -1,  0.0
  11,    1,  0.0
  11,   -1,  0.0
  12,    1,  0.0
  12,   -1,  0.0
  13,    1,  0.0
  13,   -1,  0.0
  14,    1,  0.0
  14,   -1,  0.0
  15,    1,  0.0
  15,   -1,  0.0
  16,    1,  0.0
  16,   -1,  0.0
  17,    1,  0.0
  17,   -1,  0.0
  18,    1,  0.0
  18,   -1,  0.0
  19,    1,  0.5862068
  19,   -1,  0.4137931
  20,    1,  0.0
  20,   -1,  0.0
  21,    1,  0.5862068
  21,   -1,  0.4137931
  22,    1,  0.0
  22,   -1,  0.0
  23,    1,  0.1034482
  23,   -1,  0.0
  24,    1,  0.0
  24,   -1,  0.0
  25,    1,  0.0
  25,   -1,  0.0
  26,    1,  0.0
  26,   -1,  0.0
  27,    1,  0.0
  27,   -1,  0.0
  28,    1,  0.5862068
  28,   -1,  0.2068965
  29,    1,  0.0
  29,   -1,  0.0
  30,    1,  0.0
  30,   -1,  0.0
  31,    1,  0.3448275
  31,   -1,  0.0

We notice something very interesting: weight number 9 significantly impacts class 50—all examples are misclassified when this weight is negative. However, it impacts class 25 less, with only \(17\%\) of examples being misclassified out of \(58\%\) when this weight is large.

Therefore, I decide to modify this weight:

for i in range(0, 500, 20):
    for j in range(0, 100, 20):
        d = {
                "position_1": [-2, 9, 0], 
                "value_1": -i,  
                "position_2": [-1, 0],
                "value_2": j
            }
        r = rq.post(URL + "/challenges/4", json=d).json()["message"]
        print(f"{i:>4}, {j:>4}, {r}")
   0,    0, Raté ! Le modèle a obtenu une précision sur les classes inversée de 0.0, il faut au moins 0.7
   0,   20, Raté ! Le modèle a obtenu une précision sur les classes inversée de 0.0, il faut au moins 0.7
   0,   40, Raté ! Le modèle a obtenu une précision sur les classes inversée de 0.5862068965517241, il faut au moins 0.7
   0,   60, Raté ! Le modèle a obtenu une précision sur les classes inversée de 0.5862068965517241, il faut au moins 0.7
   0,   80, Raté ! Le modèle a obtenu une précision sur les classes inversée de 0.5862068965517241, il faut au moins 0.7
  20,    0, Raté ! Le modèle a obtenu une précision sur les classes inversée de 0.41379310344827586, il faut au moins 0.7
  20,   20, Raté ! Le modèle a obtenu une précision sur les classes inversée de 0.41379310344827586, il faut au moins 0.7
  20,   40, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8620689655172413)
  20,   60, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8620689655172413)
  20,   80, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8620689655172413)
  40,    0, Raté ! Le modèle a obtenu une précision sur les classes inversée de 0.41379310344827586, il faut au moins 0.7
  40,   20, Raté ! Le modèle a obtenu une précision sur les classes inversée de 0.41379310344827586, il faut au moins 0.7
  40,   40, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8620689655172413)
  40,   60, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8620689655172413)
  40,   80, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8620689655172413)
  60,    0, Raté ! Le modèle a obtenu une précision sur les classes inversée de 0.41379310344827586, il faut au moins 0.7
  60,   20, Raté ! Le modèle a obtenu une précision sur les classes inversée de 0.41379310344827586, il faut au moins 0.7
  60,   40, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8275862068965517)
  60,   60, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8620689655172413)
  60,   80, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8620689655172413)
  80,    0, Raté ! Le modèle a obtenu une précision sur les classes inversée de 0.41379310344827586, il faut au moins 0.7
  80,   20, Raté ! Le modèle a obtenu une précision sur les classes inversée de 0.41379310344827586, il faut au moins 0.7
  80,   40, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8275862068965517)
  80,   60, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8620689655172413)
  80,   80, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8620689655172413)
 100,    0, Raté ! Le modèle a obtenu une précision sur les classes inversée de 0.41379310344827586, il faut au moins 0.7
 100,   20, Raté ! Le modèle a obtenu une précision sur les classes inversée de 0.41379310344827586, il faut au moins 0.7
 100,   40, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8275862068965517)
 100,   60, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8620689655172413)
 100,   80, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8620689655172413)
 120,    0, Raté ! Le modèle a obtenu une précision sur les classes inversée de 0.41379310344827586, il faut au moins 0.7
 120,   20, Raté ! Le modèle a obtenu une précision sur les classes inversée de 0.41379310344827586, il faut au moins 0.7
 120,   40, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8275862068965517)
 120,   60, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8620689655172413)
 120,   80, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8620689655172413)
 140,    0, Raté ! Le modèle a obtenu une précision sur les classes inversée de 0.41379310344827586, il faut au moins 0.7
 140,   20, Raté ! Le modèle a obtenu une précision sur les classes inversée de 0.41379310344827586, il faut au moins 0.7
 140,   40, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8275862068965517)
 140,   60, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8620689655172413)
 140,   80, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8620689655172413)
 160,    0, Raté ! Le modèle a obtenu une précision sur les classes inversée de 0.41379310344827586, il faut au moins 0.7
 160,   20, Raté ! Le modèle a obtenu une précision sur les classes inversée de 0.41379310344827586, il faut au moins 0.7
 160,   40, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8275862068965517)
 160,   60, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8620689655172413)
 160,   80, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8620689655172413)
 180,    0, Raté ! Le modèle a obtenu une précision sur les classes inversée de 0.41379310344827586, il faut au moins 0.7
 180,   20, Raté ! Le modèle a obtenu une précision sur les classes inversée de 0.41379310344827586, il faut au moins 0.7
 180,   40, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8275862068965517)
 180,   60, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8275862068965517)
 180,   80, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8620689655172413)
 200,    0, Raté ! Le modèle a obtenu une précision sur les classes inversée de 0.41379310344827586, il faut au moins 0.7
 200,   20, Raté ! Le modèle a obtenu une précision sur les classes inversée de 0.41379310344827586, il faut au moins 0.7
 200,   40, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8275862068965517)
 200,   60, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8275862068965517)
 200,   80, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8620689655172413)
 220,    0, Raté ! Le modèle a obtenu une précision sur les classes inversée de 0.41379310344827586, il faut au moins 0.7
 220,   20, Raté ! Le modèle a obtenu une précision sur les classes inversée de 0.41379310344827586, il faut au moins 0.7
 220,   40, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8275862068965517)
 220,   60, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8275862068965517)
 220,   80, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8620689655172413)
 240,    0, Raté ! Le modèle a obtenu une précision sur les classes inversée de 0.41379310344827586, il faut au moins 0.7
 240,   20, Raté ! Le modèle a obtenu une précision sur les classes inversée de 0.41379310344827586, il faut au moins 0.7
 240,   40, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8275862068965517)
 240,   60, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8275862068965517)
 240,   80, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8620689655172413)
 260,    0, Raté ! Le modèle a obtenu une précision sur les classes inversée de 0.41379310344827586, il faut au moins 0.7
 260,   20, Raté ! Le modèle a obtenu une précision sur les classes inversée de 0.41379310344827586, il faut au moins 0.7
 260,   40, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8275862068965517)
 260,   60, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8275862068965517)
 260,   80, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8620689655172413)
 280,    0, Raté ! Le modèle a obtenu une précision sur les classes inversée de 0.41379310344827586, il faut au moins 0.7
 280,   20, Raté ! Le modèle a obtenu une précision sur les classes inversée de 0.41379310344827586, il faut au moins 0.7
 280,   40, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8275862068965517)
 280,   60, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8275862068965517)
 280,   80, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8275862068965517)
 300,    0, Raté ! Le modèle a obtenu une précision sur les classes inversée de 0.41379310344827586, il faut au moins 0.7
 300,   20, Raté ! Le modèle a obtenu une précision sur les classes inversée de 0.41379310344827586, il faut au moins 0.7
 300,   40, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8275862068965517)
 300,   60, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8275862068965517)
 300,   80, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8275862068965517)
 320,    0, Raté ! Le modèle a obtenu une précision sur les classes inversée de 0.41379310344827586, il faut au moins 0.7
 320,   20, Raté ! Le modèle a obtenu une précision sur les classes inversée de 0.41379310344827586, il faut au moins 0.7
 320,   40, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8275862068965517)
 320,   60, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8275862068965517)
 320,   80, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8275862068965517)
 340,    0, Raté ! Le modèle a obtenu une précision sur les classes inversée de 0.41379310344827586, il faut au moins 0.7
 340,   20, Raté ! Le modèle a obtenu une précision sur les classes inversée de 0.41379310344827586, il faut au moins 0.7
 340,   40, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8275862068965517)
 340,   60, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8275862068965517)
 340,   80, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8275862068965517)
 360,    0, Raté ! Le modèle a obtenu une précision sur les classes inversée de 0.41379310344827586, il faut au moins 0.7
 360,   20, Raté ! Le modèle a obtenu une précision sur les classes inversée de 0.41379310344827586, il faut au moins 0.7
 360,   40, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8275862068965517)
 360,   60, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8275862068965517)
 360,   80, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8275862068965517)
 380,    0, Raté ! Le modèle a obtenu une précision sur les classes inversée de 0.41379310344827586, il faut au moins 0.7
 380,   20, Raté ! Le modèle a obtenu une précision sur les classes inversée de 0.41379310344827586, il faut au moins 0.7
 380,   40, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8275862068965517)
 380,   60, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8275862068965517)
 380,   80, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8275862068965517)
 400,    0, Raté ! Le modèle a obtenu une précision sur les classes inversée de 0.41379310344827586, il faut au moins 0.7
 400,   20, Raté ! Le modèle a obtenu une précision sur les classes inversée de 0.41379310344827586, il faut au moins 0.7
 400,   40, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8275862068965517)
 400,   60, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8275862068965517)
 400,   80, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8275862068965517)
 420,    0, Raté ! Le modèle a obtenu une précision sur les classes inversée de 0.41379310344827586, il faut au moins 0.7
 420,   20, Raté ! Le modèle a obtenu une précision sur les classes inversée de 0.41379310344827586, il faut au moins 0.7
 420,   40, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8275862068965517)
 420,   60, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8275862068965517)
 420,   80, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8275862068965517)
 440,    0, Raté ! Le modèle a obtenu une précision sur les classes inversée de 0.41379310344827586, il faut au moins 0.7
 440,   20, Raté ! Le modèle a obtenu une précision sur les classes inversée de 0.41379310344827586, il faut au moins 0.7
 440,   40, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8275862068965517)
 440,   60, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8275862068965517)
 440,   80, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8275862068965517)
 460,    0, Raté ! Le modèle a obtenu une précision sur les classes inversée de 0.41379310344827586, il faut au moins 0.7
 460,   20, Raté ! Le modèle a obtenu une précision sur les classes inversée de 0.41379310344827586, il faut au moins 0.7
 460,   40, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8275862068965517)
 460,   60, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8275862068965517)
 460,   80, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8275862068965517)
 480,    0, Raté ! Le modèle a obtenu une précision sur les classes inversée de 0.41379310344827586, il faut au moins 0.7
 480,   20, Raté ! Le modèle a obtenu une précision sur les classes inversée de 0.41379310344827586, il faut au moins 0.7
 480,   40, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8275862068965517)
 480,   60, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8275862068965517)
 480,   80, Bien joué ! Voici le drapeau : 404CTF{d3_p3t1ts_Ch4ng3m3ntS_tR3s_cHA0t1qU3s} (précision : 0.8275862068965517)