[{"data":1,"prerenderedAt":651},["ShallowReactive",2],{"lang-switch-post-\u002Fprojects\u002Fnetwork-intrusion-detection\u002F02-metodologia-selecao-de-modelo":3,"chapter-pt-network-intrusion-detection-02-metodologia-selecao-de-modelo":4},null,{"id":5,"title":6,"body":7,"cover":3,"date":636,"description":637,"extension":638,"meta":639,"navigation":297,"order":124,"path":640,"project":641,"seo":642,"status":643,"stem":644,"tags":645,"__hash__":650},"projectChapters\u002Fpt\u002Fprojects\u002Fnetwork-intrusion-detection\u002F02-metodologia-selecao-de-modelo.md","Trocando de Dataset e Escolhendo um Candidato",{"type":8,"value":9,"toc":629},"minimark",[10,35,40,72,95,102,105,128,137,148,152,155,258,271,311,322,328,332,378,415,444,448,471,474,507,557,601,607,618,622,625],[11,12,13,18,19,23,24,23,27,30,31,34],"p",{},[14,15,17],"a",{"href":16},"\u002Fprojects\u002Fnetwork-intrusion-detection\u002F01-explorando-25-milhoes-de-sessoes","No capítulo 1"," eu explorei os 2,5 milhões de sessões brutas do UNSW-NB15: desbalanceamento, dados ausentes, um bug de qualidade escondido, redundância entre variáveis, e a decisão de não podar nenhuma feature. Terminei apontando um problema que ainda não resolvi: ",[20,21,22],"code",{},"srcip",", ",[20,25,26],{},"dstip",[20,28,29],{},"sport"," e ",[20,32,33],{},"dsport"," separam bem demais, de um jeito que não generaliza. Esse capítulo começa resolvendo exatamente isso, trocando de dataset, e termina escolhendo, com critério, qual família de modelo treinar.",[36,37,39],"h2",{"id":38},"por-que-trocar-de-dataset-vazamento-de-dado","Por que trocar de dataset: vazamento de dado",[11,41,42,43,23,45,23,47,23,49,51,52,55,56,59,60,64,65,30,68,71],{},"O UNSW-NB15 vem em dois formatos. Um é o que usei o capítulo inteiro passado: os quatro CSVs brutos, com todos os 49 campos, incluindo ",[20,44,22],{},[20,46,26],{},[20,48,29],{},[20,50,33],{}," e os timestamps ",[20,53,54],{},"Stime","\u002F",[20,57,58],{},"Ltime",". O outro é a ",[61,62,63],"strong",{},"partição oficial"," (",[20,66,67],{},"UNSW_NB15_training-set.csv",[20,69,70],{},"UNSW_NB15_testing-set.csv","), um subconjunto que os próprios autores do dataset (Moustafa & Slay, 2015) já separaram em treino e teste, sem esses identificadores.",[11,73,74,75,64,78,82,83,85,86,89,90,55,92,94],{},"Escolho a partição oficial pra modelar, e a razão central é ",[61,76,77],{},"vazamento de dado",[79,80,81],"em",{},"data leakage","): um IP ou uma porta identifica uma máquina específica desse laboratório, não um padrão de comportamento de ataque. Se eu deixasse ",[20,84,22],{}," entrar no modelo, ele aprenderia algo como \"",[20,87,88],{},"59.166.0.4"," é sempre suspeito\" em vez de aprender o que faz um ataque parecer um ataque (a duração da sessão, o volume de bytes, o TTL, e por aí vai). Isso infla o desempenho artificialmente dentro desse laboratório específico e não generaliza pra nenhuma rede fora dele. Os timestamps ",[20,91,54],{},[20,93,58],{}," têm o mesmo problema: fluxos próximos no tempo tendem a ser correlacionados entre si (mesma sessão de ataque gerando várias linhas seguidas), e usar o timestamp bruto deixaria essa correlação vazar pro modelo.",[11,96,97,98,101],{},"Tem um segundo motivo, menos sobre vazamento e mais sobre método científico: a partição oficial é o ",[61,99,100],{},"benchmark padrão da literatura"," sobre esse dataset. Treinar e avaliar nela deixa meu resultado comparável com outros trabalhos publicados usando o mesmo dataset, o que eu perderia se inventasse meu próprio split aleatório em cima dos arquivos brutos, e correria o risco adicional de vazamento entre treino e teste por causa de fluxos correlacionados (mesmo host, mesma sessão) caindo dos dois lados do split.",[11,103,104],{},"A troca tem um custo real: abro mão de 2,5 milhões de linhas por um conjunto bem menor.",[106,107,112],"pre",{"className":108,"code":109,"language":110,"meta":111,"style":111},"language-python shiki shiki-themes github-light github-dark","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","python","",[20,113,114,122],{"__ignoreMap":111},[115,116,119],"span",{"class":117,"line":118},"line",1,[115,120,121],{},"df_train = pd.read_csv(\"\u002Fcontent\u002Fdata\u002FUNSW_NB15_training-set.csv\")\n",[115,123,125],{"class":117,"line":124},2,[115,126,127],{},"df_test  = pd.read_csv(\"\u002Fcontent\u002Fdata\u002FUNSW_NB15_testing-set.csv\")\n",[129,130,131],"blockquote",{},[11,132,133,136],{},[61,134,135],{},"Saída:"," treino com 82.332 linhas, teste com 175.341, cada uma com 45 colunas.",[11,138,139,140,143,144,147],{},"Repara que o teste é ",[61,141,142],{},"maior"," que o treino aqui, o oposto do que eu esperaria de um split 80\u002F20 comum. Os autores do UNSW-NB15 desenharam de propósito um conjunto de teste generoso, justamente pra dar uma medida de generalização mais confiável. E tem outra inversão escondida: na partição oficial, a classe ataque vira ",[61,145,146],{},"majoritária"," no teste (mais adiante, no capítulo 3, isso aparece nos números: 119.341 sessões de ataque contra 56.000 normais), o oposto da proporção que vi na EDA dos arquivos brutos. É um lembrete de que EDA e modelagem, aqui, não rodam sobre o mesmo dataframe, e as proporções de um não valem pro outro.",[36,149,151],{"id":150},"as-colunas-da-partição-oficial","As colunas da partição oficial",[11,153,154],{},"As 45 colunas da partição não são as mesmas 49 dos arquivos brutos. Os autores já removeram os identificadores e os timestamps, e organizaram o que sobrou em grupos que fazem sentido de ler juntos:",[156,157,158,179,208,236,246],"ul",{},[159,160,161,164,165,168,169,23,172,23,175,178],"li",{},[61,162,163],{},"Básicas do fluxo:"," ",[20,166,167],{},"dur"," (duração), ",[20,170,171],{},"proto",[20,173,174],{},"service",[20,176,177],{},"state",".",[159,180,181,164,184,55,187,190,191,55,194,197,198,23,201,55,204,207],{},[61,182,183],{},"Volume e taxa:",[20,185,186],{},"spkts",[20,188,189],{},"dpkts"," (pacotes), ",[20,192,193],{},"sbytes",[20,195,196],{},"dbytes"," (bytes), ",[20,199,200],{},"rate",[20,202,203],{},"sload",[20,205,206],{},"dload"," (carga).",[159,209,210,164,213,55,216,219,220,55,223,226,227,23,230,23,233,178],{},[61,211,212],{},"TCP e tempo:",[20,214,215],{},"sttl",[20,217,218],{},"dttl"," (o TTL que já apareceu no capítulo 1), ",[20,221,222],{},"swin",[20,224,225],{},"dwin"," (janela TCP), ",[20,228,229],{},"tcprtt",[20,231,232],{},"synack",[20,234,235],{},"ackdat",[159,237,238,245],{},[61,239,240,241,244],{},"Contadores de conexão (",[20,242,243],{},"ct_...","):"," quantas conexões recentes compartilham serviço, host ou porta, a família de variável desenhada especificamente pra capturar varredura e repetição.",[159,247,248,164,251,254,255,178],{},[61,249,250],{},"Alvos:",[20,252,253],{},"label"," (0\u002F1) e ",[20,256,257],{},"attack_cat",[11,259,260,261,264,265,23,267,23,269,244],{},"Das 42 colunas de entrada (descartando ",[20,262,263],{},"id"," e os dois alvos), 39 são numéricas e 3 são categóricas (",[20,266,171],{},[20,268,174],{},[20,270,177],{},[106,272,274],{"className":108,"code":273,"language":110,"meta":111,"style":111},"DESCARTAR = [\"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\"]\n\ncat_cols = X_train.select_dtypes(include=[\"object\"]).columns.tolist()\nnum_cols = X_train.select_dtypes(include=[\"number\"]).columns.tolist()\n",[20,275,276,281,286,292,299,305],{"__ignoreMap":111},[115,277,278],{"class":117,"line":118},[115,279,280],{},"DESCARTAR = [\"id\", \"attack_cat\", \"label\"]\n",[115,282,283],{"class":117,"line":124},[115,284,285],{},"X_train = df_train.drop(columns=[c for c in DESCARTAR if c in df_train.columns])\n",[115,287,289],{"class":117,"line":288},3,[115,290,291],{},"y_train = df_train[\"label\"]\n",[115,293,295],{"class":117,"line":294},4,[115,296,298],{"emptyLinePlaceholder":297},true,"\n",[115,300,302],{"class":117,"line":301},5,[115,303,304],{},"cat_cols = X_train.select_dtypes(include=[\"object\"]).columns.tolist()\n",[115,306,308],{"class":117,"line":307},6,[115,309,310],{},"num_cols = X_train.select_dtypes(include=[\"number\"]).columns.tolist()\n",[129,312,313],{},[11,314,315,317,318,321],{},[61,316,135],{}," categóricas ",[20,319,320],{},"['proto', 'service', 'state']",", 39 numéricas.",[11,323,324,327],{},[20,325,326],{},"select_dtypes"," separa sozinho as 3 categóricas e as 39 numéricas, sem eu precisar digitar nome de coluna na mão.",[36,329,331],{"id":330},"montando-o-pipeline-de-pré-processamento","Montando o pipeline de pré-processamento",[106,333,335],{"className":108,"code":334,"language":110,"meta":111,"style":111},"from sklearn.compose import ColumnTransformer\nfrom sklearn.preprocessing import StandardScaler, OneHotEncoder\n\ntransformadores = [\n    (\"num\", StandardScaler(), num_cols),\n    (\"cat\", OneHotEncoder(handle_unknown=\"ignore\", sparse_output=False), cat_cols),\n]\npreprocessador = ColumnTransformer(transformers=transformadores)\n",[20,336,337,342,347,351,356,361,366,372],{"__ignoreMap":111},[115,338,339],{"class":117,"line":118},[115,340,341],{},"from sklearn.compose import ColumnTransformer\n",[115,343,344],{"class":117,"line":124},[115,345,346],{},"from sklearn.preprocessing import StandardScaler, OneHotEncoder\n",[115,348,349],{"class":117,"line":288},[115,350,298],{"emptyLinePlaceholder":297},[115,352,353],{"class":117,"line":294},[115,354,355],{},"transformadores = [\n",[115,357,358],{"class":117,"line":301},[115,359,360],{},"    (\"num\", StandardScaler(), num_cols),\n",[115,362,363],{"class":117,"line":307},[115,364,365],{},"    (\"cat\", OneHotEncoder(handle_unknown=\"ignore\", sparse_output=False), cat_cols),\n",[115,367,369],{"class":117,"line":368},7,[115,370,371],{},"]\n",[115,373,375],{"class":117,"line":374},8,[115,376,377],{},"preprocessador = ColumnTransformer(transformers=transformadores)\n",[11,379,380,383,384,387,388,391,392,396,397,400,401,403,404,406,407,414],{},[20,381,382],{},"ColumnTransformer"," aplica, em paralelo, um transformador diferente pra cada grupo de coluna: ",[20,385,386],{},"StandardScaler"," nas 39 numéricas (centra em média 0 e desvio 1, essencial pra Regressão Logística, inócuo pra árvore) e ",[20,389,390],{},"OneHotEncoder"," nas 3 categóricas, ",[14,393,395],{"href":394},"\u002Fplaylists\u002Fpattern-recognition\u002Ftitanic","o mesmo par que já apareceu no Titanic",", agora resolvendo protocolo de rede em vez de porto de embarque. Um detalhe que vale registrar: ",[20,398,399],{},"handle_unknown=\"ignore\"",". Se o teste trouxer um valor de ",[20,402,174],{}," que o treino nunca viu, em vez de estourar erro, o ",[20,405,390],{}," simplesmente zera aquela linha inteira pra aquela variável, ",[14,408,410,411],{"href":409},"\u002Fplaylists\u002Fpattern-recognition\u002Fnlp-intro","o mesmo espírito do token ",[20,412,413],{},"[UNK]"," que já vi resolvendo palavra desconhecida.",[11,416,417,418,421,422,425,426,429,430,433,434,436,437,440,441,443],{},"Essa célula não devolve gráfico nem número, só ",[61,419,420],{},"monta"," o transformador. E é exatamente aí que mora o ponto mais importante dela: eu não chamo ",[20,423,424],{},".fit()"," nesse ",[20,427,428],{},"preprocessador"," sozinho, ele entra inteiro dentro de um ",[20,431,432],{},"Pipeline",", junto com o classificador. Isso importa porque, na validação cruzada que vem a seguir, cada fold treina seu próprio ",[20,435,386],{}," só com o pedaço de treino daquele fold, a média e o desvio nunca veem o pedaço de validação. Se eu tivesse rodado ",[20,438,439],{},"preprocessador.fit_transform(X_train)"," antes de dividir os folds, a média e o desvio calculados em cima do treino inteiro vazariam informação estatística do fold de validação pro treino, um vazamento sutil (bem menor que o dos IPs, mas real) que o ",[20,442,432],{}," evita de graça.",[36,445,447],{"id":446},"comparando-quatro-candidatos-numa-amostra-rápida","Comparando quatro candidatos, numa amostra rápida",[11,449,450,451,454,455,458,459,462,463,466,467,470],{},"Antes de escolher um modelo, defini o critério ",[61,452,453],{},"antes de ver qualquer número",": prioridade pro ",[61,456,457],{},"F1-score"," (equilíbrio entre não deixar ataque passar e não gerar alarme falso), com ",[61,460,461],{},"AUC"," como desempate. Como as classes são desbalanceadas, uso ",[20,464,465],{},"class_weight=\"balanced\""," onde o modelo aceita, e validação cruzada ",[61,468,469],{},"estratificada"," (mantém a proporção de classes em cada fold).",[11,472,473],{},"Testei quatro famílias bem diferentes entre si, cada uma carregando uma hipótese diferente sobre o dado:",[156,475,476,482,492,501],{},[159,477,478,481],{},[61,479,480],{},"Regressão Logística:"," assume fronteira de decisão linear, referência simples e interpretável.",[159,483,484,487,488,491],{},[61,485,486],{},"Árvore de Decisão:"," captura relação não linear, mas tende a ",[79,489,490],{},"overfitting"," sozinha.",[159,493,494,497,498,178],{},[61,495,496],{},"Random Forest:"," agrega muitas árvores, reduzindo variância via ",[79,499,500],{},"bagging",[159,502,503,506],{},[61,504,505],{},"Naive Bayes Gaussiano:"," assume independência entre variáveis dado a classe, rápido, uma linha de base.",[106,508,510],{"className":108,"code":509,"language":110,"meta":111,"style":111},"modelos = {\n    \"Regressao Logistica\": LogisticRegression(max_iter=1000, class_weight=\"balanced\"),\n    \"Arvore de Decisao\": DecisionTreeClassifier(random_state=42, class_weight=\"balanced\"),\n    \"Random Forest\": RandomForestClassifier(n_estimators=100, random_state=42, class_weight=\"balanced\", n_jobs=-1),\n    \"Naive Bayes\": GaussianNB(),\n}\n\nscore = lambda clf, m: cross_val_score(Pipeline([(\"prep\", preprocessador), (\"clf\", clf)]), Xs, ys, cv=cv, scoring=m, n_jobs=-1).mean()\nlinhas = [{\"Modelo\": n, \"F1\": score(c, \"f1\"), \"AUC\": score(c, \"roc_auc\")} for n, c in modelos.items()]\n",[20,511,512,517,522,527,532,537,542,546,551],{"__ignoreMap":111},[115,513,514],{"class":117,"line":118},[115,515,516],{},"modelos = {\n",[115,518,519],{"class":117,"line":124},[115,520,521],{},"    \"Regressao Logistica\": LogisticRegression(max_iter=1000, class_weight=\"balanced\"),\n",[115,523,524],{"class":117,"line":288},[115,525,526],{},"    \"Arvore de Decisao\": DecisionTreeClassifier(random_state=42, class_weight=\"balanced\"),\n",[115,528,529],{"class":117,"line":294},[115,530,531],{},"    \"Random Forest\": RandomForestClassifier(n_estimators=100, random_state=42, class_weight=\"balanced\", n_jobs=-1),\n",[115,533,534],{"class":117,"line":301},[115,535,536],{},"    \"Naive Bayes\": GaussianNB(),\n",[115,538,539],{"class":117,"line":307},[115,540,541],{},"}\n",[115,543,544],{"class":117,"line":368},[115,545,298],{"emptyLinePlaceholder":297},[115,547,548],{"class":117,"line":374},[115,549,550],{},"score = lambda clf, m: cross_val_score(Pipeline([(\"prep\", preprocessador), (\"clf\", clf)]), Xs, ys, cv=cv, scoring=m, n_jobs=-1).mean()\n",[115,552,554],{"class":117,"line":553},9,[115,555,556],{},"linhas = [{\"Modelo\": n, \"F1\": score(c, \"f1\"), \"AUC\": score(c, \"roc_auc\")} for n, c in modelos.items()]\n",[11,558,559,560,563,564,567,568,571,572,575,576,579,580,582,583,585,586,589,590,592,593,596,597,600],{},"Vale abrir esse bloco em câmera lenta, porque ele empacota bastante coisa em duas linhas. ",[20,561,562],{},"modelos"," é só um dicionário nome → instância de classificador, os quatro candidatos que eu quero comparar. ",[20,565,566],{},"score"," é uma função de uma linha só (",[20,569,570],{},"lambda","): recebe um classificador ",[20,573,574],{},"clf"," e o nome de uma métrica ",[20,577,578],{},"m",", monta um ",[20,581,432],{}," novo grudando o mesmo ",[20,584,428],{}," na frente ",[61,587,588],{},"daquele"," classificador específico (cada candidato precisa do próprio pipeline, não dá pra reaproveitar um pipeline já treinado por outro modelo), roda validação cruzada de 3 dobras nesse pipeline (numa amostra de 20 mil sessões de treino, rodar isso nas 82 mil inteiras a cada candidato custaria tempo à toa nessa fase de triagem), e tira a média das 3 notas. A linha de baixo chama essa função ",[20,591,566],{}," duas vezes pra cada um dos 4 modelos, uma vez pedindo ",[20,594,595],{},"\"f1\"",", outra pedindo ",[20,598,599],{},"\"roc_auc\"",". 4 modelos × 2 métricas × 3 dobras: 24 treinos completos, escondidos atrás de duas linhas de código.",[602,603],"grouped-bar-chart",{":categories":604,":series":605,"y-label":606},"[\"Regressão Logística\",\"Árvore de Decisão\",\"Random Forest\",\"Naive Bayes\"]","[{\"name\":\"F1 (classe ataque)\",\"values\":[0.92,0.95,0.97,0.54],\"color\":\"#0033cc\"},{\"name\":\"AUC\",\"values\":[0.98,0.95,1.00,0.72],\"color\":\"#cc3300\"}]","métrica",[11,608,609,610,614,615,617],{},"Random Forest ganha nas duas métricas (F1 0,97, AUC 1,00 nessa amostra de triagem), a Árvore de Decisão sozinha já vem logo atrás, e o Naive Bayes Gaussiano fica bem pra trás dos outros três (F1 0,54). Faz sentido pelo mesmo motivo ",[14,611,613],{"href":612},"\u002Fplaylists\u002Fpattern-recognition\u002Fpca","que já vi na PCA do câncer de mama",": Naive Bayes assume que as variáveis são independentes entre si dado a classe, e o capítulo 1 já mostrou que boa parte das 39 variáveis numéricas são tudo menos independentes, várias contam a mesma história com palavras diferentes. A expectativa teórica também bate: Random Forest combina o baixo viés das árvores com a baixa variância do ",[79,616,500],{},", exatamente o tipo de robustez que eu esperaria lidar bem com as 39 variáveis, boa parte redundante entre si, que o capítulo 1 mapeou.",[36,619,621],{"id":620},"fechando-este-capítulo","Fechando este capítulo",[11,623,624],{},"Com o critério definido antes de ver os números (F1 primeiro, AUC como desempate) e o resultado batendo com a expectativa teórica, a Random Forest é a escolhida pro treino final. No capítulo 3 eu treino ela em todo o conjunto de treino oficial, avalio no teste oficial (nunca visto), abro a curva ROC, o corte de decisão, quais variáveis mais pesaram na decisão e se dá pra melhorar afinando hiperparâmetro.",[626,627,628],"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":111,"searchDepth":124,"depth":124,"links":630},[631,632,633,634,635],{"id":38,"depth":124,"text":39},{"id":150,"depth":124,"text":151},{"id":330,"depth":124,"text":331},{"id":446,"depth":124,"text":447},{"id":620,"depth":124,"text":621},"2026-08-26","Capítulo 2 do meu trabalho final de Reconhecimento de Padrões: por que abro mão dos 2,5 milhões de sessões brutas em troca de uma partição menor e curada, o pipeline de pré-processamento que evita vazamento na validação cruzada, e a comparação justa entre quatro famílias de modelo antes de escolher qual treinar de verdade.","md",{},"\u002Fpt\u002Fprojects\u002Fnetwork-intrusion-detection\u002F02-metodologia-selecao-de-modelo","network-intrusion-detection",{"title":6,"description":637},"published","pt\u002Fprojects\u002Fnetwork-intrusion-detection\u002F02-metodologia-selecao-de-modelo",[646,647,648,649],"seguranca","scikit-learn","unsw-nb15","pipeline","O5dYS-ACt037Tn8-9EQNr8-yXbIFs6ZsQH7PvfRpXeI",1788965101315]