[{"data":1,"prerenderedAt":1385},["ShallowReactive",2],{"lang-switch-post-\u002Fprojects\u002Fnetwork-intrusion-detection\u002F03-avaliacao-final-e-ajuste":3,"chapter-pt-network-intrusion-detection-03-avaliacao-final-e-ajuste":4},null,{"id":5,"title":6,"body":7,"cover":3,"date":1370,"description":1371,"extension":1372,"meta":1373,"navigation":94,"order":67,"path":1374,"project":1375,"seo":1376,"status":1377,"stem":1378,"tags":1379,"__hash__":1384},"projectChapters\u002Fpt\u002Fprojects\u002Fnetwork-intrusion-detection\u002F03-avaliacao-final-e-ajuste.md","Avaliação Final, Curva ROC e Ajuste de Hiperparâmetro",{"type":8,"value":9,"toc":1361},"minimark",[10,24,29,42,102,129,136,201,216,220,236,251,393,409,464,489,495,498,504,508,515,534,548,566,573,678,684,745,778,785,792,796,804,838,897,904,928,932,945,1022,1061,1069,1083,1087,1094,1138,1180,1197,1200,1254,1274,1284,1292,1296,1349,1357],[11,12,13,18,19,23],"p",{},[14,15,17],"a",{"href":16},"\u002Fprojects\u002Fnetwork-intrusion-detection\u002F02-metodologia-selecao-de-modelo","No capítulo 2"," defini o critério antes de ver qualquer número (F1 primeiro, AUC como desempate) e a Random Forest venceu a comparação entre quatro famílias de modelo. Esse capítulo final treina ela de verdade, em todo o conjunto de treino oficial, mede no teste oficial (as 175.341 sessões que o modelo nunca viu), e não para na acurácia: abro o corte de decisão, a curva ROC, quais variáveis mais pesaram, onde exatamente o modelo erra, e se dá pra melhorar afinando hiperparâmetro. O notebook completo, executado, com as três partes do projeto, está linkado ",[14,20,22],{"href":21},"\u002Fprojects\u002Fnetwork-intrusion-detection\u002F01-explorando-25-milhoes-de-sessoes","na abertura do capítulo 1",".",[25,26,28],"h2",{"id":27},"avaliação-final-random-forest-no-teste-oficial-de-verdade","Avaliação final: Random Forest, no teste oficial de verdade",[11,30,31,32,36,37,41],{},"Treinei o Random Forest (200 árvores, com ",[33,34,35],"code",{},"class_weight=\"balanced\"",", um jeito de avisar o modelo que errar um ataque, a classe rara, custa mais caro do que errar uma sessão normal, sem precisar duplicar ou descartar nenhuma linha ",[14,38,40],{"href":39},"\u002Fplaylists\u002Fpattern-recognition\u002Fcredit-card-fraud","do jeito que o reamostramento tentava fazer lá na fraude de cartão",") em todo o conjunto de treino oficial e medi no conjunto de teste oficial, os 175.341 registros que o modelo nunca viu:",[43,44,49],"pre",{"className":45,"code":46,"language":47,"meta":48,"style":48},"language-python shiki shiki-themes github-light github-dark","modelo_final = Pipeline([\n    (\"prep\", preprocessador),\n    (\"clf\", RandomForestClassifier(n_estimators=200, random_state=42, class_weight=\"balanced\", n_jobs=-1)),\n])\nmodelo_final.fit(X_train, y_train)\ny_pred = modelo_final.predict(X_test)\n\nprint(classification_report(y_test, y_pred, digits=4))\n","python","",[33,50,51,59,65,71,77,83,89,96],{"__ignoreMap":48},[52,53,56],"span",{"class":54,"line":55},"line",1,[52,57,58],{},"modelo_final = Pipeline([\n",[52,60,62],{"class":54,"line":61},2,[52,63,64],{},"    (\"prep\", preprocessador),\n",[52,66,68],{"class":54,"line":67},3,[52,69,70],{},"    (\"clf\", RandomForestClassifier(n_estimators=200, random_state=42, class_weight=\"balanced\", n_jobs=-1)),\n",[52,72,74],{"class":54,"line":73},4,[52,75,76],{},"])\n",[52,78,80],{"class":54,"line":79},5,[52,81,82],{},"modelo_final.fit(X_train, y_train)\n",[52,84,86],{"class":54,"line":85},6,[52,87,88],{},"y_pred = modelo_final.predict(X_test)\n",[52,90,92],{"class":54,"line":91},7,[52,93,95],{"emptyLinePlaceholder":94},true,"\n",[52,97,99],{"class":54,"line":98},8,[52,100,101],{},"print(classification_report(y_test, y_pred, digits=4))\n",[11,103,104,105,108,109,112,113,116,117,120,121,124,125,128],{},"O mesmo ",[33,106,107],{},"Pipeline"," de sempre, agora com o ",[33,110,111],{},"preprocessador"," inteiro (os 39 ",[33,114,115],{},"StandardScaler"," + 3 ",[33,118,119],{},"OneHotEncoder",") grudado na frente do Random Forest final, um único ",[33,122,123],{},".fit()"," treinando as duas etapas juntas. ",[33,126,127],{},"classification_report"," é quem calcula precisão, revocação e F1 por classe de uma vez, a mesma tabela que aparece logo abaixo.",[130,131],"heatmap-chart",{":cells":132,":x-categories":133,":y-categories":133,"x-label":134,"y-label":135},"[{\"x\":\"Normal\",\"y\":\"Normal\",\"value\":54651},{\"x\":\"Ataque\",\"y\":\"Normal\",\"value\":1349},{\"x\":\"Normal\",\"y\":\"Ataque\",\"value\":15429},{\"x\":\"Ataque\",\"y\":\"Ataque\",\"value\":103912}]","[\"Normal\",\"Ataque\"]","previsto","real",[137,138,139,163],"table",{},[140,141,142],"thead",{},[143,144,145,150,154,157,160],"tr",{},[146,147,149],"th",{"align":148},"left","Classe",[146,151,153],{"align":152},"right","Precisão",[146,155,156],{"align":152},"Revocação",[146,158,159],{"align":152},"F1",[146,161,162],{"align":152},"Sessões no teste",[164,165,166,184],"tbody",{},[143,167,168,172,175,178,181],{},[169,170,171],"td",{"align":148},"Normal",[169,173,174],{"align":152},"0,7798",[169,176,177],{"align":152},"0,9759",[169,179,180],{"align":152},"0,8669",[169,182,183],{"align":152},"56.000",[143,185,186,189,192,195,198],{},[169,187,188],{"align":148},"Ataque",[169,190,191],{"align":152},"0,9872",[169,193,194],{"align":152},"0,8707",[169,196,197],{"align":152},"0,9253",[169,199,200],{"align":152},"119.341",[11,202,203,204,208,209,212,213,23],{},"Precisão aqui é: das sessões que o modelo marcou como ataque, quantas eram ataque de verdade. Revocação é o oposto: dos ataques reais, quantos o modelo pegou. F1 resume as duas numa nota só. Acurácia geral 90,43%, mas o número que eu realmente confio é a revocação da classe ataque: ",[205,206,207],"strong",{},"87,07%",", ou seja, o modelo pega 103.912 dos 119.341 ataques reais do teste, e deixa passar 15.429. Em segurança, esse tipo de falso negativo (ataque classificado como normal) é o erro caro, é literalmente o intruso que passa despercebido. Do lado oposto, 1.349 sessões normais viraram alarme falso, incômodo pra quem investiga, mas bem menos grave que deixar passar um ataque de verdade. A ",[205,210,211],{},"AUC no teste oficial fica em 0,9830",", quase o teto de 1,0, uma medida de que o modelo sabe rankear risco de sessão bem mesmo antes de qualquer limiar de decisão entrar em jogo, ",[14,214,215],{"href":39},"a mesma métrica de ranking que já vi na fraude de cartão",[25,217,219],{"id":218},"ajustando-o-corte-precisão-contra-revocação","Ajustando o corte: precisão contra revocação",[11,221,222,223,226,227,230,231,235],{},"Toda essa avaliação usou o corte padrão do scikit-learn: o ",[33,224,225],{},".predict()"," decide \"ataque\" sempre que o score de risco (a fração de árvores que votou ataque, o mesmo número que dá pra pegar com ",[33,228,229],{},".predict_proba()",") passa de 0,5. Mas esse 0,5 é só um ponto arbitrário numa reta inteira de opções, a mesma ideia de limiar ",[14,232,234],{"href":233},"\u002Fplaylists\u002Fpattern-recognition\u002Fclassification-threshold","que já vi lá na aula de classificação com regressão",", só que agora dá pra girar de verdade.",[11,237,238,239,242,243,246,247,250],{},"Pra isso, escrevi um segundo notebook (",[33,240,241],{},"datasets\u002Funsw-nb15\u002F02_dados_extras_scatter_e_threshold.ipynb","), só porque o notebook original nunca salvou o ",[33,244,245],{},"y_score"," contínuo, apenas o ",[33,248,249],{},"y_pred"," já limiarizado em 0,5. As primeiras células desse notebook reconstroem, célula por célula, exatamente o mesmo pipeline dos capítulos 2 e 3:",[43,252,254],{"className":45,"code":253,"language":47,"meta":48,"style":48},"df_train = pd.read_csv(\"\u002Fcontent\u002Fdata\u002FUNSW_NB15_training-set.csv\")\ndf_test = pd.read_csv(\"\u002Fcontent\u002Fdata\u002FUNSW_NB15_testing-set.csv\")\n\nDESCARTAR = [\"id\", \"attack_cat\", \"label\"]\nX_train = df_train.drop(columns=[c for c in DESCARTAR if c in df_train.columns])\ny_train = df_train[\"label\"]\nX_test = df_test.drop(columns=[c for c in DESCARTAR if c in df_test.columns])\ny_test = df_test[\"label\"]\n\ncat_cols = X_train.select_dtypes(include=[\"object\"]).columns.tolist()\nnum_cols = X_train.select_dtypes(include=[\"number\"]).columns.tolist()\n\ntransformadores = [\n    (\"num\", StandardScaler(), num_cols),\n    (\"cat\", OneHotEncoder(handle_unknown=\"ignore\", sparse_output=False), cat_cols),\n]\npreprocessador = ColumnTransformer(transformers=transformadores)\n\nmodelo_final = Pipeline([\n    (\"prep\", preprocessador),\n    (\"clf\", RandomForestClassifier(n_estimators=200, random_state=42, class_weight=\"balanced\", n_jobs=-1)),\n])\nmodelo_final.fit(X_train, y_train)\n\ny_pred = modelo_final.predict(X_test)\ny_score = modelo_final.predict_proba(X_test)[:, 1]\n",[33,255,256,261,266,270,275,280,285,290,295,300,306,312,317,323,329,335,341,347,352,357,362,367,372,377,382,387],{"__ignoreMap":48},[52,257,258],{"class":54,"line":55},[52,259,260],{},"df_train = pd.read_csv(\"\u002Fcontent\u002Fdata\u002FUNSW_NB15_training-set.csv\")\n",[52,262,263],{"class":54,"line":61},[52,264,265],{},"df_test = pd.read_csv(\"\u002Fcontent\u002Fdata\u002FUNSW_NB15_testing-set.csv\")\n",[52,267,268],{"class":54,"line":67},[52,269,95],{"emptyLinePlaceholder":94},[52,271,272],{"class":54,"line":73},[52,273,274],{},"DESCARTAR = [\"id\", \"attack_cat\", \"label\"]\n",[52,276,277],{"class":54,"line":79},[52,278,279],{},"X_train = df_train.drop(columns=[c for c in DESCARTAR if c in df_train.columns])\n",[52,281,282],{"class":54,"line":85},[52,283,284],{},"y_train = df_train[\"label\"]\n",[52,286,287],{"class":54,"line":91},[52,288,289],{},"X_test = df_test.drop(columns=[c for c in DESCARTAR if c in df_test.columns])\n",[52,291,292],{"class":54,"line":98},[52,293,294],{},"y_test = df_test[\"label\"]\n",[52,296,298],{"class":54,"line":297},9,[52,299,95],{"emptyLinePlaceholder":94},[52,301,303],{"class":54,"line":302},10,[52,304,305],{},"cat_cols = X_train.select_dtypes(include=[\"object\"]).columns.tolist()\n",[52,307,309],{"class":54,"line":308},11,[52,310,311],{},"num_cols = X_train.select_dtypes(include=[\"number\"]).columns.tolist()\n",[52,313,315],{"class":54,"line":314},12,[52,316,95],{"emptyLinePlaceholder":94},[52,318,320],{"class":54,"line":319},13,[52,321,322],{},"transformadores = [\n",[52,324,326],{"class":54,"line":325},14,[52,327,328],{},"    (\"num\", StandardScaler(), num_cols),\n",[52,330,332],{"class":54,"line":331},15,[52,333,334],{},"    (\"cat\", OneHotEncoder(handle_unknown=\"ignore\", sparse_output=False), cat_cols),\n",[52,336,338],{"class":54,"line":337},16,[52,339,340],{},"]\n",[52,342,344],{"class":54,"line":343},17,[52,345,346],{},"preprocessador = ColumnTransformer(transformers=transformadores)\n",[52,348,350],{"class":54,"line":349},18,[52,351,95],{"emptyLinePlaceholder":94},[52,353,355],{"class":54,"line":354},19,[52,356,58],{},[52,358,360],{"class":54,"line":359},20,[52,361,64],{},[52,363,365],{"class":54,"line":364},21,[52,366,70],{},[52,368,370],{"class":54,"line":369},22,[52,371,76],{},[52,373,375],{"class":54,"line":374},23,[52,376,82],{},[52,378,380],{"class":54,"line":379},24,[52,381,95],{"emptyLinePlaceholder":94},[52,383,385],{"class":54,"line":384},25,[52,386,88],{},[52,388,390],{"class":54,"line":389},26,[52,391,392],{},"y_score = modelo_final.predict_proba(X_test)[:, 1]\n",[11,394,395,396,399,400,402,403,405,406,408],{},"Célula por célula, é o mesmo código do capítulo 2 (a partição oficial, o ",[33,397,398],{},"ColumnTransformer",") e do começo deste capítulo (o ",[33,401,107],{}," com o Random Forest de 200 árvores), sem nenhuma linha nova, só reconstruído aqui porque esse notebook roda isolado, num runtime do Colab que não carrega o estado dos outros dois. A única diferença é a última linha: além de ",[33,404,249],{},", guardo também ",[33,407,245],{},", a probabilidade contínua, pra poder testar cortes diferentes de 0,5 no bloco seguinte. Com o pipeline retreinado, o bloco novo varre 19 cortes diferentes:",[43,410,412],{"className":45,"code":411,"language":47,"meta":48,"style":48},"thresholds = np.round(np.arange(0.05, 1.0, 0.05), 2)\nthreshold_sweep = []\nfor t in thresholds:\n    y_pred_t = (y_score >= t).astype(int)\n    threshold_sweep.append({\n        \"threshold\": float(t),\n        \"precision\": round(float(precision_score(y_test, y_pred_t, zero_division=0)), 4),\n        \"recall\": round(float(recall_score(y_test, y_pred_t, zero_division=0)), 4),\n        \"f1\": round(float(f1_score(y_test, y_pred_t, zero_division=0)), 4),\n    })\n",[33,413,414,419,424,429,434,439,444,449,454,459],{"__ignoreMap":48},[52,415,416],{"class":54,"line":55},[52,417,418],{},"thresholds = np.round(np.arange(0.05, 1.0, 0.05), 2)\n",[52,420,421],{"class":54,"line":61},[52,422,423],{},"threshold_sweep = []\n",[52,425,426],{"class":54,"line":67},[52,427,428],{},"for t in thresholds:\n",[52,430,431],{"class":54,"line":73},[52,432,433],{},"    y_pred_t = (y_score >= t).astype(int)\n",[52,435,436],{"class":54,"line":79},[52,437,438],{},"    threshold_sweep.append({\n",[52,440,441],{"class":54,"line":85},[52,442,443],{},"        \"threshold\": float(t),\n",[52,445,446],{"class":54,"line":91},[52,447,448],{},"        \"precision\": round(float(precision_score(y_test, y_pred_t, zero_division=0)), 4),\n",[52,450,451],{"class":54,"line":98},[52,452,453],{},"        \"recall\": round(float(recall_score(y_test, y_pred_t, zero_division=0)), 4),\n",[52,455,456],{"class":54,"line":297},[52,457,458],{},"        \"f1\": round(float(f1_score(y_test, y_pred_t, zero_division=0)), 4),\n",[52,460,461],{"class":54,"line":302},[52,462,463],{},"    })\n",[11,465,466,469,470,473,474,477,478,480,481,484,485,488],{},[33,467,468],{},"np.arange(0.05, 1.0, 0.05)"," gera os 19 cortes (0,05, 0,10, ..., 0,95), e o ",[33,471,472],{},"for"," testa cada um: ",[33,475,476],{},"y_score >= t"," transforma o score contínuo em rótulo binário nesse corte específico (a mesma comparação que ",[33,479,225],{}," faz internamente, só que fixando ",[33,482,483],{},"t"," em vez de usar sempre 0,5), e as três métricas são recalculadas do zero a cada volta do laço. ",[33,486,487],{},"zero_division=0"," evita erro caso algum corte extremo zere uma das classes inteiramente. O resultado, 19 linhas de precisão\u002Frevocação\u002FF1, é o que alimenta o gráfico interativo abaixo:",[490,491],"intrusion-threshold-explorer",{"precision-label":492,"recall-label":493,"threshold-label":494},"precisão","revocação","corte do modelo",[11,496,497],{},"Arrasta o slider e repara na troca. Baixar o corte pra 0,05 (marcar \"ataque\" com qualquer suspeita, por menor que seja) empurra a revocação pra 0,9781, quase pegando todo mundo, mas derruba a precisão pra 0,9234, mais alarme falso pro time de segurança investigar à toa. Subir o corte pra 0,95 (só marcar \"ataque\" quando o modelo tem certeza quase absoluta) inverte o jogo: precisão de 0,999, praticamente nenhum alarme falso, mas a revocação despenca pra 0,6951, deixando passar quase um terço dos ataques reais. Não existe um corte certo, isolado da pergunta de negócio. Se o custo de investigar um alarme falso é baixo, vale puxar o corte pra baixo e aceitar mais alarme falso em troca de pegar mais ataque de verdade. Se cada investigação é cara, o corte sobe.",[11,499,500,501,503],{},"No corte que usei o post inteiro (0,5), a combinação de ",[33,502,35],{}," com esse corte padrão já entrega revocação de 0,8715 sem eu precisar mexer em nada na mão (os números batem, com uma variação de milésimo, com a tabela lá em cima, porque reconstruí o pipeline do zero nesse notebook separado). É por isso que não fui atrás de ajustar esse corte na avaliação final: o desbalanceamento de classe já tava sendo compensado no próprio treino, não só na hora de decidir.",[25,505,507],{"id":506},"a-curva-roc-o-mesmo-corte-visto-de-outro-ângulo","A curva ROC: o mesmo corte, visto de outro ângulo",[11,509,510,511,514],{},"A matriz de confusão e a tabela de precisão\u002Frevocação lá em cima avaliam o modelo num ",[205,512,513],{},"único"," corte de decisão (o padrão, 0,5). Mas o slider que acabei de mostrar já entrega o resto do quadro: a Random Forest não devolve só um rótulo, ela estima uma probabilidade P(ataque | x) pra cada sessão, e o corte é uma decisão operacional de segurança, não uma verdade matemática fixa.",[11,516,517,518,521,522,526,527,521,530,533],{},"A ",[205,519,520],{},"curva ROC"," (",[523,524,525],"em",{},"Receiver Operating Characteristic",") transforma essa troca em desenho: varrendo todos os cortes possíveis, ela traça a taxa de verdadeiro positivo (a mesma revocação de sempre) contra a taxa de falso positivo (a fração de sessões normais que viram alarme falso). A ",[205,528,529],{},"AUC",[523,531,532],{},"Area Under the Curve",", a área debaixo dessa curva) condensa o gráfico inteiro num único número, com uma leitura probabilística direta: é a chance de o modelo dar um score mais alto a um ataque sorteado ao acaso do que a uma sessão normal sorteada ao acaso. AUC = 1,0 é o classificador perfeito, AUC = 0,5 é o mesmo que jogar moeda (a diagonal pontilhada do gráfico). No notebook original, esse cálculo é só duas linhas:",[43,535,537],{"className":45,"code":536,"language":47,"meta":48,"style":48},"y_score = modelo_final.predict_proba(X_test)[:, 1]\nauc = roc_auc_score(y_test, y_score)\n",[33,538,539,543],{"__ignoreMap":48},[52,540,541],{"class":54,"line":55},[52,542,392],{},[52,544,545],{"class":54,"line":61},[52,546,547],{},"auc = roc_auc_score(y_test, y_score)\n",[11,549,550,553,554,557,558,561,562,565],{},[33,551,552],{},"predict_proba"," devolve duas colunas, a probabilidade de cada classe, ",[33,555,556],{},"[:, 1]"," pega só a coluna da classe 1 (ataque). ",[33,559,560],{},"roc_auc_score"," recebe o rótulo real e esse score contínuo, sem nenhum corte fixo envolvido, e calcula a AUC direto. O notebook original parava aí e só desenhava o gráfico com ",[33,563,564],{},"RocCurveDisplay.from_predictions(...)"," do próprio scikit-learn, uma imagem estática, sem nunca salvar os pontos da curva em lugar nenhum.",[11,567,568,569,572],{},"Pra ter um gráfico real e interativo aqui no blog, escrevi um terceiro notebook (",[33,570,571],{},"datasets\u002Funsw-nb15\u002F03_curva_roc.ipynb","), com a mesma reconstrução de sempre no começo (a essa altura, terceira vez que esse bloco aparece nesse projeto, então só o repito, sem re-explicar):",[43,574,576],{"className":45,"code":575,"language":47,"meta":48,"style":48},"df_train = pd.read_csv(\"\u002Fcontent\u002Fdata\u002FUNSW_NB15_training-set.csv\")\ndf_test = pd.read_csv(\"\u002Fcontent\u002Fdata\u002FUNSW_NB15_testing-set.csv\")\n\nDESCARTAR = [\"id\", \"attack_cat\", \"label\"]\nX_train = df_train.drop(columns=[c for c in DESCARTAR if c in df_train.columns])\ny_train = df_train[\"label\"]\nX_test = df_test.drop(columns=[c for c in DESCARTAR if c in df_test.columns])\ny_test = df_test[\"label\"]\n\ncat_cols = X_train.select_dtypes(include=[\"object\"]).columns.tolist()\nnum_cols = X_train.select_dtypes(include=[\"number\"]).columns.tolist()\n\ntransformadores = [\n    (\"num\", StandardScaler(), num_cols),\n    (\"cat\", OneHotEncoder(handle_unknown=\"ignore\", sparse_output=False), cat_cols),\n]\npreprocessador = ColumnTransformer(transformers=transformadores)\n\nmodelo_final = Pipeline([\n    (\"prep\", preprocessador),\n    (\"clf\", RandomForestClassifier(n_estimators=200, random_state=42, class_weight=\"balanced\", n_jobs=-1)),\n])\nmodelo_final.fit(X_train, y_train)\n\ny_score = modelo_final.predict_proba(X_test)[:, 1]\n",[33,577,578,582,586,590,594,598,602,606,610,614,618,622,626,630,634,638,642,646,650,654,658,662,666,670,674],{"__ignoreMap":48},[52,579,580],{"class":54,"line":55},[52,581,260],{},[52,583,584],{"class":54,"line":61},[52,585,265],{},[52,587,588],{"class":54,"line":67},[52,589,95],{"emptyLinePlaceholder":94},[52,591,592],{"class":54,"line":73},[52,593,274],{},[52,595,596],{"class":54,"line":79},[52,597,279],{},[52,599,600],{"class":54,"line":85},[52,601,284],{},[52,603,604],{"class":54,"line":91},[52,605,289],{},[52,607,608],{"class":54,"line":98},[52,609,294],{},[52,611,612],{"class":54,"line":297},[52,613,95],{"emptyLinePlaceholder":94},[52,615,616],{"class":54,"line":302},[52,617,305],{},[52,619,620],{"class":54,"line":308},[52,621,311],{},[52,623,624],{"class":54,"line":314},[52,625,95],{"emptyLinePlaceholder":94},[52,627,628],{"class":54,"line":319},[52,629,322],{},[52,631,632],{"class":54,"line":325},[52,633,328],{},[52,635,636],{"class":54,"line":331},[52,637,334],{},[52,639,640],{"class":54,"line":337},[52,641,340],{},[52,643,644],{"class":54,"line":343},[52,645,346],{},[52,647,648],{"class":54,"line":349},[52,649,95],{"emptyLinePlaceholder":94},[52,651,652],{"class":54,"line":354},[52,653,58],{},[52,655,656],{"class":54,"line":359},[52,657,64],{},[52,659,660],{"class":54,"line":364},[52,661,70],{},[52,663,664],{"class":54,"line":369},[52,665,76],{},[52,667,668],{"class":54,"line":374},[52,669,82],{},[52,671,672],{"class":54,"line":379},[52,673,95],{"emptyLinePlaceholder":94},[52,675,676],{"class":54,"line":384},[52,677,392],{},[11,679,680,681,683],{},"E, em cima desse ",[33,682,245],{},", extrai a curva ponto a ponto:",[43,685,687],{"className":45,"code":686,"language":47,"meta":48,"style":48},"fpr, tpr, thresholds = roc_curve(y_test, y_score)\nauc = roc_auc_score(y_test, y_score)\nprint(f\"AUC (conferencia, deve bater com 0.9830 ja publicado): {auc:.4f}\")\nprint(f\"Curva bruta tem {len(fpr)} pontos, vamos reduzir pra 50 pra plotar no blog\")\n\nfpr_grid = np.linspace(0, 1, 50)\ntpr_grid = np.interp(fpr_grid, fpr, tpr)\n\nroc_curve_points = [\n    {\"fpr\": round(float(f), 4), \"tpr\": round(float(t), 4)}\n    for f, t in zip(fpr_grid, tpr_grid)\n]\n",[33,688,689,694,698,703,708,712,717,722,726,731,736,741],{"__ignoreMap":48},[52,690,691],{"class":54,"line":55},[52,692,693],{},"fpr, tpr, thresholds = roc_curve(y_test, y_score)\n",[52,695,696],{"class":54,"line":61},[52,697,547],{},[52,699,700],{"class":54,"line":67},[52,701,702],{},"print(f\"AUC (conferencia, deve bater com 0.9830 ja publicado): {auc:.4f}\")\n",[52,704,705],{"class":54,"line":73},[52,706,707],{},"print(f\"Curva bruta tem {len(fpr)} pontos, vamos reduzir pra 50 pra plotar no blog\")\n",[52,709,710],{"class":54,"line":79},[52,711,95],{"emptyLinePlaceholder":94},[52,713,714],{"class":54,"line":85},[52,715,716],{},"fpr_grid = np.linspace(0, 1, 50)\n",[52,718,719],{"class":54,"line":91},[52,720,721],{},"tpr_grid = np.interp(fpr_grid, fpr, tpr)\n",[52,723,724],{"class":54,"line":98},[52,725,95],{"emptyLinePlaceholder":94},[52,727,728],{"class":54,"line":297},[52,729,730],{},"roc_curve_points = [\n",[52,732,733],{"class":54,"line":302},[52,734,735],{},"    {\"fpr\": round(float(f), 4), \"tpr\": round(float(t), 4)}\n",[52,737,738],{"class":54,"line":308},[52,739,740],{},"    for f, t in zip(fpr_grid, tpr_grid)\n",[52,742,743],{"class":54,"line":314},[52,744,340],{},[11,746,747,750,751,753,754,757,758,761,762,765,766,769,770,773,774,777],{},[33,748,749],{},"roc_curve(y_test, y_score)"," varre internamente cada score único presente em ",[33,752,245],{}," como um corte candidato, e devolve três arrays do mesmo tamanho: ",[33,755,756],{},"fpr",", ",[33,759,760],{},"tpr"," e os ",[33,763,764],{},"thresholds"," que geraram cada par. Nesse dataset a curva bruta saiu com 282 pontos, granularidade demais pra um gráfico de blog e um JSON maior que o necessário. ",[33,767,768],{},"np.linspace(0, 1, 50)"," cria 50 posições igualmente espaçadas no eixo de falso positivo, e ",[33,771,772],{},"np.interp(fpr_grid, fpr, tpr)"," estima a taxa de verdadeiro positivo real em cada uma dessas 50 posições, interpolando entre os pontos reais mais próximos, sem inventar nenhum valor: é a mesma curva, só amostrada num grid mais leve de desenhar. O ",[33,775,776],{},"print"," de conferência confirma AUC = 0,9830, batendo exatamente com o número calculado no notebook original.",[779,780],"intrusion-roc-chart",{"model-label":781,"random-label":782,"x-label":783,"y-label":784},"Random Forest (AUC = 0,983)","aleatório (AUC = 0,5)","taxa de falso positivo","taxa de verdadeiro positivo",[11,786,787,788,791],{},"A curva gruda no canto superior esquerdo, bem longe da diagonal pontilhada do acaso: com um corte de falso positivo de só 2%, a revocação já salta pra 86%, e a curva segue quase colada no teto dali em diante. Isso mostra que o modelo separa bem as duas classes ",[205,789,790],{},"independente de qualquer corte específico",", é essa folga que sobra pra eu poder puxar o corte pra baixo, como fiz na seção anterior, sem que a precisão desabe junto.",[25,793,795],{"id":794},"quem-decide-as-15-variáveis-que-mais-pesam","Quem decide: as 15 variáveis que mais pesam",[11,797,798,799,803],{},"Random Forest permite espiar quais variáveis mais reduziram a impureza das árvores ao longo do treino, ",[14,800,802],{"href":801},"\u002Fplaylists\u002Fpattern-recognition\u002Fdecision-trees","a mesma régua de Gini que já vi lá nas árvores de decisão",", só que agora somada em 200 árvores em vez de uma só:",[43,805,807],{"className":45,"code":806,"language":47,"meta":48,"style":48},"ohe = modelo_final.named_steps[\"prep\"].named_transformers_[\"cat\"]\nnomes_cat = ohe.get_feature_names_out(cat_cols)\nnomes_features = np.concatenate([num_cols, nomes_cat])\n\nimportancias = modelo_final.named_steps[\"clf\"].feature_importances_\nimp = pd.Series(importancias, index=nomes_features).sort_values(ascending=False)\n",[33,808,809,814,819,824,828,833],{"__ignoreMap":48},[52,810,811],{"class":54,"line":55},[52,812,813],{},"ohe = modelo_final.named_steps[\"prep\"].named_transformers_[\"cat\"]\n",[52,815,816],{"class":54,"line":61},[52,817,818],{},"nomes_cat = ohe.get_feature_names_out(cat_cols)\n",[52,820,821],{"class":54,"line":67},[52,822,823],{},"nomes_features = np.concatenate([num_cols, nomes_cat])\n",[52,825,826],{"class":54,"line":73},[52,827,95],{"emptyLinePlaceholder":94},[52,829,830],{"class":54,"line":79},[52,831,832],{},"importancias = modelo_final.named_steps[\"clf\"].feature_importances_\n",[52,834,835],{"class":54,"line":85},[52,836,837],{},"imp = pd.Series(importancias, index=nomes_features).sort_values(ascending=False)\n",[11,839,840,841,844,845,847,848,757,851,757,854,857,858,757,861,757,864,867,868,871,872,875,876,878,879,882,883,886,887,889,890,892,893,896],{},"Esse bloco existe pra resolver um problema chato: ",[33,842,843],{},"feature_importances_"," devolve só um array cru de números, sem nome nenhum grudado, e depois do ",[33,846,398],{}," as três colunas categóricas originais (",[33,849,850],{},"proto",[33,852,853],{},"service",[33,855,856],{},"state",") nem existem mais, viraram dezenas de colunas binárias novas, tipo ",[33,859,860],{},"proto_tcp",[33,862,863],{},"proto_udp",[33,865,866],{},"service_dns",". Pra reconstruir \"qual número da lista corresponde a qual variável\", preciso ir buscar esses nomes de volta de dentro do próprio pipeline já treinado: ",[33,869,870],{},"modelo_final.named_steps[\"prep\"]"," pega a etapa do pré-processador, ",[33,873,874],{},".named_transformers_[\"cat\"]"," pega especificamente o ",[33,877,119],{}," que rodou nas categóricas, e ",[33,880,881],{},".get_feature_names_out(cat_cols)"," pergunta pra ele \"quais foram os nomes das colunas novas que você criou?\". ",[33,884,885],{},"np.concatenate([num_cols, nomes_cat])"," gruda essa lista de volta com as 39 numéricas na frente (que mantiveram o nome original, ",[33,888,115],{}," não cria coluna nova nenhuma), respeitando a mesma ordem que o ",[33,891,398],{}," usa por dentro. Só depois disso ",[33,894,895],{},"pd.Series(importancias, index=nomes_features)"," consegue emparelhar cada número de importância com o nome certo.",[898,899],"category-bar-chart",{":points":900,"color":901,"x-label":902,"y-label":903},"[{\"label\":\"ct_dst_src_ltm\",\"value\":0.074},{\"label\":\"sttl\",\"value\":0.059},{\"label\":\"sbytes\",\"value\":0.054},{\"label\":\"sload\",\"value\":0.052},{\"label\":\"ct_dst_sport_ltm\",\"value\":0.051},{\"label\":\"smean\",\"value\":0.047},{\"label\":\"ct_srv_dst\",\"value\":0.044},{\"label\":\"ct_state_ttl\",\"value\":0.042},{\"label\":\"rate\",\"value\":0.033},{\"label\":\"dbytes\",\"value\":0.033},{\"label\":\"dload\",\"value\":0.031},{\"label\":\"dur\",\"value\":0.029},{\"label\":\"synack\",\"value\":0.028},{\"label\":\"ct_srv_src\",\"value\":0.027},{\"label\":\"dmean\",\"value\":0.026}]","#33aa55","variável","importância (redução de impureza)",[11,905,906,907,910,911,914,915,757,918,757,921,757,924,927],{},"A campeã, ",[33,908,909],{},"ct_dst_src_ltm",", conta quantas das últimas 100 conexões compartilham a mesma dupla origem-destino, um jeito direto de flagrar comportamento de scanner ou de flood: tráfego normal raramente bate repetidamente no mesmo par origem-destino centenas de vezes seguidas, ataque automatizado sim. Repara que ",[33,912,913],{},"sttl"," (o TTL que já apareceu lá na correlação) aparece de novo, na segunda posição, confirmando que aquele 0,91 de correlação com o rótulo não era coincidência estatística vazia, é sinal real. E boa parte do resto da lista (",[33,916,917],{},"ct_dst_sport_ltm",[33,919,920],{},"ct_srv_dst",[33,922,923],{},"ct_state_ttl",[33,925,926],{},"ct_srv_src",") é da mesma família \"quantas conexões recentes se parecem com essa\", o tipo de variável desenhada especificamente pra capturar rajada de conexão repetida, a assinatura mais comum de ataque automatizado contra tráfego humano.",[25,929,931],{"id":930},"onde-o-modelo-erra-400-sessões-reais-nas-3-variáveis-mais-importantes","Onde o modelo erra: 400 sessões reais nas 3 variáveis mais importantes",[11,933,934,935,937,938,940,941,944],{},"As duas primeiras posições do ranking de importância, ",[33,936,909],{}," e ",[33,939,913],{},", mais a terceira, ",[33,942,943],{},"sbytes",", dão pra visualizar juntas num espaço só. É o mesmo notebook 02 que gerou o sweep de threshold, com o mesmo pipeline retreinado, que também extrai essa amostra:",[43,946,948],{"className":45,"code":947,"language":47,"meta":48,"style":48},"FEATURES_3D = [\"sttl\", \"ct_dst_src_ltm\", \"sbytes\"]\nN_SAMPLE = 400\n\namostra_idx = X_test.sample(n=N_SAMPLE, random_state=42).index\ny_pred_series = pd.Series(y_pred, index=X_test.index)\n\nsample_points = []\nfor i in amostra_idx:\n    sample_points.append({\n        \"x\": float(X_test.loc[i, FEATURES_3D[0]]),\n        \"y\": float(X_test.loc[i, FEATURES_3D[1]]),\n        \"z\": float(X_test.loc[i, FEATURES_3D[2]]),\n        \"label_real\": int(y_test.loc[i]),\n        \"label_previsto\": int(y_pred_series.loc[i]),\n    })\n",[33,949,950,955,960,964,969,974,978,983,988,993,998,1003,1008,1013,1018],{"__ignoreMap":48},[52,951,952],{"class":54,"line":55},[52,953,954],{},"FEATURES_3D = [\"sttl\", \"ct_dst_src_ltm\", \"sbytes\"]\n",[52,956,957],{"class":54,"line":61},[52,958,959],{},"N_SAMPLE = 400\n",[52,961,962],{"class":54,"line":67},[52,963,95],{"emptyLinePlaceholder":94},[52,965,966],{"class":54,"line":73},[52,967,968],{},"amostra_idx = X_test.sample(n=N_SAMPLE, random_state=42).index\n",[52,970,971],{"class":54,"line":79},[52,972,973],{},"y_pred_series = pd.Series(y_pred, index=X_test.index)\n",[52,975,976],{"class":54,"line":85},[52,977,95],{"emptyLinePlaceholder":94},[52,979,980],{"class":54,"line":91},[52,981,982],{},"sample_points = []\n",[52,984,985],{"class":54,"line":98},[52,986,987],{},"for i in amostra_idx:\n",[52,989,990],{"class":54,"line":297},[52,991,992],{},"    sample_points.append({\n",[52,994,995],{"class":54,"line":302},[52,996,997],{},"        \"x\": float(X_test.loc[i, FEATURES_3D[0]]),\n",[52,999,1000],{"class":54,"line":308},[52,1001,1002],{},"        \"y\": float(X_test.loc[i, FEATURES_3D[1]]),\n",[52,1004,1005],{"class":54,"line":314},[52,1006,1007],{},"        \"z\": float(X_test.loc[i, FEATURES_3D[2]]),\n",[52,1009,1010],{"class":54,"line":319},[52,1011,1012],{},"        \"label_real\": int(y_test.loc[i]),\n",[52,1014,1015],{"class":54,"line":325},[52,1016,1017],{},"        \"label_previsto\": int(y_pred_series.loc[i]),\n",[52,1019,1020],{"class":54,"line":331},[52,1021,463],{},[11,1023,1024,1027,1028,1031,1032,1034,1035,1038,1039,1042,1043,1046,1047,757,1049,757,1051,1053,1054,1056,1057,1060],{},[33,1025,1026],{},"X_test.sample(n=400, random_state=42)"," sorteia 400 índices do conjunto de teste oficial, a semente fixa é só pra eu conseguir reproduzir a mesma amostra depois. ",[33,1029,1030],{},"y_pred_series"," reempacota o array cru ",[33,1033,249],{}," (que só tem posição, sem índice) numa ",[33,1036,1037],{},"Series"," com o mesmo índice de ",[33,1040,1041],{},"X_test",", pra poder buscar a previsão de uma sessão específica por ",[33,1044,1045],{},".loc[i]"," dentro do laço. Pra cada uma das 400 sessões sorteadas, o laço guarda as três features de interesse (",[33,1048,913],{},[33,1050,909],{},[33,1052,943],{},") mais o rótulo real e o rótulo previsto, nada inventado, é exatamente o ",[33,1055,1041],{},"\u002F",[33,1058,1059],{},"y_test"," oficiais, só que reduzido a uma amostra visualizável. Plotei nesses 3 eixos:",[1062,1063],"intrusion-sample-scatter",{"false-negative-label":1064,"false-positive-label":1065,"true-negative-label":1066,"true-positive-label":1067,"x-label":1068,"y-label":909,"z-label":943},"falso negativo (ataque não detectado)","falso positivo (alarme falso)","acerto: normal","acerto: ataque","sttl (TTL de origem)",[11,1070,1071,1072,1074,1075,1078,1079,1082],{},"Nessa amostra de 400: 247 acertos em sessão de ataque, 112 acertos em sessão normal, 36 falsos negativos e só 5 falsos positivos, uma proporção parecida com a do teste inteiro. Um detalhe que salta aos olhos ao girar a nuvem: ",[33,1073,913],{}," não é contínuo de verdade, ele se agrupa nuns poucos valores redondos (31, 62, 254...), porque TTL é literalmente um contador de saltos de roteador que cada sistema operacional inicializa num valor padrão (64, 128 ou 255) e vai decrementando um a um a cada salto. Nas 283 sessões de ataque dessa amostra, ",[33,1076,1077],{},"sttl=254"," aparece em 238 delas (84%), mas também aparece em 37 das 117 sessões normais (32%), o valor mais comum dos dois lados, longe de ser um separador limpo sozinho. Já ",[33,1080,1081],{},"sttl=31"," é quase exclusivo de tráfego normal: 74 das 117 sessões normais, zero sessão de ataque. É exatamente por isso que uma variável sozinha não decide nada aqui. Dá pra ver os 36 pontos vermelhos (falso negativo) espalhados pelo meio da nuvem, sem formar uma ilha isolada, e é aí que entram as outras 39 variáveis que o gráfico não mostra, trabalhando junto pra fechar os 87% de revocação que o modelo final entrega.",[25,1084,1086],{"id":1085},"tentando-melhorar-com-gridsearch-e-batendo-no-próprio-teto","Tentando melhorar com GridSearch, e batendo no próprio teto",[11,1088,1089,1090,1093],{},"Testei ",[33,1091,1092],{},"GridSearchCV"," variando número de árvores (200 ou 400), profundidade máxima (sem limite ou até 30) e tamanho mínimo de folha (1 ou 2), 8 combinações, validação cruzada de 3 dobras, numa amostra de 30 mil sessões pra manter o custo tratável:",[43,1095,1097],{"className":45,"code":1096,"language":47,"meta":48,"style":48},"param_grid = {\n    \"clf__n_estimators\": [200, 400],\n    \"clf__max_depth\": [None, 30],\n    \"clf__min_samples_leaf\": [1, 2],\n}\n\ngrid = GridSearchCV(pipe_rf, param_grid, scoring=\"f1\", cv=cv, n_jobs=-1, verbose=1)\ngrid.fit(Xg, yg)\n",[33,1098,1099,1104,1109,1114,1119,1124,1128,1133],{"__ignoreMap":48},[52,1100,1101],{"class":54,"line":55},[52,1102,1103],{},"param_grid = {\n",[52,1105,1106],{"class":54,"line":61},[52,1107,1108],{},"    \"clf__n_estimators\": [200, 400],\n",[52,1110,1111],{"class":54,"line":67},[52,1112,1113],{},"    \"clf__max_depth\": [None, 30],\n",[52,1115,1116],{"class":54,"line":73},[52,1117,1118],{},"    \"clf__min_samples_leaf\": [1, 2],\n",[52,1120,1121],{"class":54,"line":79},[52,1122,1123],{},"}\n",[52,1125,1126],{"class":54,"line":85},[52,1127,95],{"emptyLinePlaceholder":94},[52,1129,1130],{"class":54,"line":91},[52,1131,1132],{},"grid = GridSearchCV(pipe_rf, param_grid, scoring=\"f1\", cv=cv, n_jobs=-1, verbose=1)\n",[52,1134,1135],{"class":54,"line":98},[52,1136,1137],{},"grid.fit(Xg, yg)\n",[11,1139,1140,1141,1144,1145,1148,1149,1151,1152,1155,1156,1159,1160,1163,1164,1167,1168,1176,1177,1179],{},"O prefixo ",[33,1142,1143],{},"clf__"," na frente de cada chave não é enfeite: ",[33,1146,1147],{},"pipe_rf"," é um ",[33,1150,107],{}," com uma etapa chamada ",[33,1153,1154],{},"\"clf\""," (o Random Forest), e o ",[33,1157,1158],{},"__"," é o jeito do scikit-learn dizer \"o parâmetro ",[33,1161,1162],{},"n_estimators"," pertence à etapa ",[33,1165,1166],{},"clf",", não a qualquer outra do pipeline\", ",[14,1169,1171,1172,1175],{"href":1170},"\u002Fplaylists\u002Fpattern-recognition\u002Fpipeline-cross-validation","a mesma sintaxe ",[33,1173,1174],{},"model__"," que já apareceu no post de pipeline e GridSearch",". Sem esse prefixo, ",[33,1178,1092],{}," não saberia em qual das etapas mexer.",[1181,1182,1183],"blockquote",{},[11,1184,1185,1188,1189,1192,1193,1196],{},[205,1186,1187],{},"Saída:"," melhor combinação encontrada, ",[33,1190,1191],{},"n_estimators=200, max_depth=None, min_samples_leaf=1",", com F1 de validação 0,9716. Exatamente os parâmetros ",[205,1194,1195],{},"padrão"," do Random Forest que eu já tinha treinado antes.",[11,1198,1199],{},"Pra não confiar cegamente nesse F1 de validação (calculado numa amostra de 30 mil, lembra), re-treinei a configuração vencedora no conjunto de treino oficial inteiro e avaliei no teste oficial, do mesmo jeito rigoroso da avaliação final:",[43,1201,1203],{"className":45,"code":1202,"language":47,"meta":48,"style":48},"melhores = {k.replace(\"clf__\", \"\"): v for k, v in grid.best_params_.items()}\nmodelo_tunado = Pipeline([(\"prep\", preprocessador),\n                          (\"clf\", RandomForestClassifier(random_state=42, class_weight=\"balanced\",\n                                                         n_jobs=-1, **melhores))])\nmodelo_tunado.fit(X_train, y_train)\ny_pred_t = modelo_tunado.predict(X_test)\ny_score_t = modelo_tunado.predict_proba(X_test)[:, 1]\n\nprint(classification_report(y_test, y_pred_t, digits=4))\nprint(f\"AUC-ROC (ajustada): {roc_auc_score(y_test, y_score_t):.4f}\")\n",[33,1204,1205,1210,1215,1220,1225,1230,1235,1240,1244,1249],{"__ignoreMap":48},[52,1206,1207],{"class":54,"line":55},[52,1208,1209],{},"melhores = {k.replace(\"clf__\", \"\"): v for k, v in grid.best_params_.items()}\n",[52,1211,1212],{"class":54,"line":61},[52,1213,1214],{},"modelo_tunado = Pipeline([(\"prep\", preprocessador),\n",[52,1216,1217],{"class":54,"line":67},[52,1218,1219],{},"                          (\"clf\", RandomForestClassifier(random_state=42, class_weight=\"balanced\",\n",[52,1221,1222],{"class":54,"line":73},[52,1223,1224],{},"                                                         n_jobs=-1, **melhores))])\n",[52,1226,1227],{"class":54,"line":79},[52,1228,1229],{},"modelo_tunado.fit(X_train, y_train)\n",[52,1231,1232],{"class":54,"line":85},[52,1233,1234],{},"y_pred_t = modelo_tunado.predict(X_test)\n",[52,1236,1237],{"class":54,"line":91},[52,1238,1239],{},"y_score_t = modelo_tunado.predict_proba(X_test)[:, 1]\n",[52,1241,1242],{"class":54,"line":98},[52,1243,95],{"emptyLinePlaceholder":94},[52,1245,1246],{"class":54,"line":297},[52,1247,1248],{},"print(classification_report(y_test, y_pred_t, digits=4))\n",[52,1250,1251],{"class":54,"line":302},[52,1252,1253],{},"print(f\"AUC-ROC (ajustada): {roc_auc_score(y_test, y_score_t):.4f}\")\n",[11,1255,1256,1259,1260,1262,1263,1266,1267,1270,1271,1273],{},[33,1257,1258],{},"grid.best_params_"," guarda as chaves com o prefixo ",[33,1261,1143],{}," (o mesmo prefixo explicado acima), então a primeira linha tira esse prefixo pra poder passar os valores direto pro construtor do ",[33,1264,1265],{},"RandomForestClassifier"," via ",[33,1268,1269],{},"**melhores",". O resto é o mesmo ",[33,1272,107],{}," de sempre, só que agora com os hiperparâmetros vencedores no lugar dos valores padrão, treinado no conjunto de treino oficial inteiro (não mais na amostra de 30 mil da busca) e avaliado no teste oficial.",[1181,1275,1276],{},[11,1277,1278,1280,1281,23],{},[205,1279,1187],{}," precisão, revocação e F1 por classe idênticos à Random Forest original, casa decimal por casa decimal, e ",[33,1282,1283],{},"AUC-ROC (ajustada): 0.9830",[11,1285,1286,1287,1291],{},"O resultado bate, casa decimal por casa decimal, com a Random Forest original: mesma precisão, revocação e F1 por classe, mesma AUC de 0,9830. Não é falha de busca, é a busca confirmando algo que vale registrar sem meias palavras: pra esse dataset, com esse conjunto de candidatos testados, o palpite padrão do scikit-learn (100 ou 200 árvores, sem limitar profundidade) já estava no teto que essa família de hiperparâmetro alcança. Nem toda otimização encontra algo melhor pra otimizar, ",[14,1288,1290],{"href":1289},"\u002Fplaylists\u002Fpattern-recognition\u002Fensembles","o mesmo tipo de resultado \"chato\" que já vi no post de ensembles",", quando a regressão logística crua bateu ensemble nenhum se afinar mudasse o resultado.",[25,1293,1295],{"id":1294},"fechando-o-projeto-e-a-playlist-inteira","Fechando o projeto (e a playlist inteira)",[137,1297,1298,1308],{},[140,1299,1300],{},[143,1301,1302,1305],{},[146,1303,1304],{"align":148},"O que eu já sabia, de algum post anterior",[146,1306,1307],{"align":148},"O que esse trabalho final juntou",[164,1309,1310,1318,1333,1341],{},[143,1311,1312,1315],{},[169,1313,1314],{"align":148},"Dado desbalanceado engana acurácia",[169,1316,1317],{"align":148},"Aqui, com 12,6% de ataque, revocação da classe minoritária importa mais que o número geral",[143,1319,1320,1323],{},[169,1321,1322],{"align":148},"Correlação alta sinaliza redundância",[169,1324,1325,1326,1328,1329,1332],{"align":148},"Correlação entre feature e rótulo (",[33,1327,913],{}," × ",[33,1330,1331],{},"Label",", 0,91) pode sinalizar sinal real, não só redundância entre duas variáveis",[143,1334,1335,1338],{},[169,1336,1337],{"align":148},"Random Forest lida bem com padrão não-linear",[169,1339,1340],{"align":148},"Aqui ele bateu Regressão Logística, Árvore sozinha e Naive Bayes nos dois eixos (F1 e AUC)",[143,1342,1343,1346],{},[169,1344,1345],{"align":148},"GridSearch encontra o melhor ponto do espaço de busca",[169,1347,1348],{"align":148},"Às vezes o melhor ponto já é o padrão, e vale a pena descobrir isso medindo, não assumindo",[11,1350,1351,1352,1356],{},"Esse projeto fecha um ciclo pra mim: comecei essa playlist ",[14,1353,1355],{"href":1354},"\u002Fplaylists\u002Fpattern-recognition\u002Flinear-regression-estimator","ajustando uma reta com equação normal",", passei por árvore, cluster, redução de dimensão, texto virando vetor, e terminei aplicando tudo isso sozinho, sem aula pra seguir, num problema de segurança de rede de verdade, com resultado real (AUC 0,98 no teste oficial) que eu consigo defender linha por linha, do CSV bruto até a árvore de decisão final. Valeu, professor Boldt, o resto eu tirei de letra.",[1358,1359,1360],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":48,"searchDepth":61,"depth":61,"links":1362},[1363,1364,1365,1366,1367,1368,1369],{"id":27,"depth":61,"text":28},{"id":218,"depth":61,"text":219},{"id":506,"depth":61,"text":507},{"id":794,"depth":61,"text":795},{"id":930,"depth":61,"text":931},{"id":1085,"depth":61,"text":1086},{"id":1294,"depth":61,"text":1295},"2026-08-26","Capítulo 3, o fechamento do meu trabalho final de Reconhecimento de Padrões: o Random Forest escolhido no capítulo 2 avaliado no teste oficial (98,3% de AUC), o corte de decisão explorado de ponta a ponta, curva ROC, as variáveis que mais pesaram, onde o modelo erra, e a descoberta de que afinar hiperparâmetro nem sempre bate o palpite padrão.","md",{},"\u002Fpt\u002Fprojects\u002Fnetwork-intrusion-detection\u002F03-avaliacao-final-e-ajuste","network-intrusion-detection",{"title":6,"description":1371},"published","pt\u002Fprojects\u002Fnetwork-intrusion-detection\u002F03-avaliacao-final-e-ajuste",[1380,1381,1382,1383],"seguranca","random-forest","unsw-nb15","deteccao-de-anomalia","GK406bU6TKSWKBhvHnAsz3IkRJbagprv6pQIiU2OfOI",1788965101673]