[{"data":1,"prerenderedAt":1260},["ShallowReactive",2],{"lang-switch-post-\u002Fplaylists\u002Fpattern-recognition\u002Fpipeline-cross-validation":3,"post-pt-pattern-recognition-pipeline-cross-validation":4},"\u002Fen\u002Fplaylists\u002Fpattern-recognition\u002Fpipeline-cross-validation",{"id":5,"title":6,"body":7,"cover":1245,"date":1246,"description":1247,"extension":1248,"meta":1249,"navigation":172,"order":125,"path":1250,"playlist":1251,"seo":1252,"status":1253,"stem":1254,"tags":1255,"__hash__":1259},"posts\u002Fpt\u002Fplaylists\u002Fpattern-recognition\u002Fpipeline-cross-validation.md","Pipeline, Validação Cruzada e GridSearch: o Fluxo de Verdade",{"type":8,"value":9,"toc":1234},"minimark",[10,20,25,38,61,71,82,90,96,129,140,144,158,195,207,223,227,230,313,358,376,390,595,599,631,684,703,706,710,721,746,756,774,784,790,794,814,875,900,915,918,922,928,937,944,1056,1060,1115,1119,1122,1170,1227,1230],[11,12,13,14,19],"p",{},"O resto da aula 4, o \"trabalho de verdade\" que fica escondido atrás de qualquer número de acurácia que eu já mostrei até aqui. Sem isso, todo K que eu escolhi ",[15,16,18],"a",{"href":17},"\u002Fplaylists\u002Fpattern-recognition\u002Fknn-classifier","nos dois posts anteriores"," foi, sem exagero, chute educado.",[21,22,24],"h2",{"id":23},"pipeline-o-que-eu-já-sabia-agora-com-classificação","Pipeline: o que eu já sabia, agora com classificação",[11,26,27,28,32,33,37],{},"O ",[29,30,31],"code",{},"Pipeline"," ",[15,34,36],{"href":35},"\u002Fplaylists\u002Fmachine-learning-specialization\u002Fscikit-learn-pitfalls","já apareceu na outra playlist",": encadeia normalização e modelo num único objeto, então o scaler nunca vê dado que deveria ficar de fora do treino. Aqui o professor confirma a mesma conclusão, só que num problema de classificação com 3 classes:",[39,40,45],"pre",{"className":41,"code":42,"language":43,"meta":44,"style":44},"language-python shiki shiki-themes github-light github-dark","model = KNeighborsClassifier()\nmodel.fit(X_train, y_train)\n","python","",[29,46,47,55],{"__ignoreMap":44},[48,49,52],"span",{"class":50,"line":51},"line",1,[48,53,54],{},"model = KNeighborsClassifier()\n",[48,56,58],{"class":50,"line":57},2,[48,59,60],{},"model.fit(X_train, y_train)\n",[62,63,64],"blockquote",{},[11,65,66,70],{},[67,68,69],"strong",{},"Saída (sem normalizar):"," acurácia 0.67.",[11,72,73,74,77,78,81],{},"Depois ele normaliza de quatro jeitos diferentes (min-max na mão, ",[29,75,76],{},"MinMaxScaler",", z-score na mão, ",[29,79,80],{},"StandardScaler",") e todos batem:",[62,83,84],{},[11,85,86,89],{},[67,87,88],{},"Saída (qualquer normalização):"," acurácia 0.92.",[11,91,92,93,95],{},"E o ",[29,94,31],{}," chega no mesmo 0.92, encadeando os dois passos automaticamente:",[39,97,99],{"className":41,"code":98,"language":43,"meta":44,"style":44},"pipeline = Pipeline([\n    ('scaler', StandardScaler()),\n    ('model', KNeighborsClassifier())\n])\npipeline.fit(X_train, y_train)\n",[29,100,101,106,111,117,123],{"__ignoreMap":44},[48,102,103],{"class":50,"line":51},[48,104,105],{},"pipeline = Pipeline([\n",[48,107,108],{"class":50,"line":57},[48,109,110],{},"    ('scaler', StandardScaler()),\n",[48,112,114],{"class":50,"line":113},3,[48,115,116],{},"    ('model', KNeighborsClassifier())\n",[48,118,120],{"class":50,"line":119},4,[48,121,122],{},"])\n",[48,124,126],{"class":50,"line":125},5,[48,127,128],{},"pipeline.fit(X_train, y_train)\n",[11,130,131,132,134,135,139],{},"Nada novo em termos de mecânica (eu já vi ",[29,133,31],{}," de fora), mas o salto de 0.67 pra 0.92 é o maior que eu vi até agora só de normalizar, e reforça o motivo que ",[15,136,138],{"href":137},"\u002Fplaylists\u002Fpattern-recognition\u002Fclassification-threshold","eu já expliquei",": 13 variáveis em escalas bem diferentes, e o KNN decide tudo por distância.",[21,141,143],{"id":142},"separando-validação-de-teste","Separando validação de teste",[11,145,27,146,149,150,153,154,157],{},[29,147,148],{},"aula04c"," começa devagar: separa uma fatia do treino só pra validação (",[29,151,152],{},"X_tr","\u002F",[29,155,156],{},"X_val","), testa vários valores de K, e escolhe o que ganhou na validação:",[39,159,161],{"className":41,"code":160,"language":43,"meta":44,"style":44},"X_tr, X_val, y_tr, y_val = train_test_split(X_train, y_train, test_size=0.2)\n\nfor k in range(1, 21, 2):\n    model = KNeighborsClassifier(n_neighbors=k)\n    model.fit(X_tr, y_tr)\n    acc = accuracy_score(y_val, model.predict(X_val))\n",[29,162,163,168,174,179,184,189],{"__ignoreMap":44},[48,164,165],{"class":50,"line":51},[48,166,167],{},"X_tr, X_val, y_tr, y_val = train_test_split(X_train, y_train, test_size=0.2)\n",[48,169,170],{"class":50,"line":57},[48,171,173],{"emptyLinePlaceholder":172},true,"\n",[48,175,176],{"class":50,"line":113},[48,177,178],{},"for k in range(1, 21, 2):\n",[48,180,181],{"class":50,"line":119},[48,182,183],{},"    model = KNeighborsClassifier(n_neighbors=k)\n",[48,185,186],{"class":50,"line":125},[48,187,188],{},"    model.fit(X_tr, y_tr)\n",[48,190,192],{"class":50,"line":191},6,[48,193,194],{},"    acc = accuracy_score(y_val, model.predict(X_val))\n",[62,196,197],{},[11,198,199,202,203,206],{},[67,200,201],{},"Saída:"," o melhor K encontrado foi ",[67,204,205],{},"k=9",", com acurácia de validação 0.759.",[11,208,209,210,213,214,217,218,153,220,222],{},"Isso já é bem melhor que \"testei no teste e vi qual K ganhou\" (o que seria fazer exatamente o mesmo erro que qualquer curso de estatística avisa pra não fazer: usar o conjunto de teste pra escolher hiperparâmetro é uma forma de vazamento, você acaba enviesando a estimativa final porque ela deixou de ser sobre dado nunca visto). Mas tem um problema visível aqui: o ",[29,211,212],{},"train_test_split"," nem no holdout nem na validação fixa ",[29,215,216],{},"random_state",", então cada vez que eu rodar essa célula de novo, ",[29,219,152],{},[29,221,156],{}," mudam, e o K vencedor pode mudar junto. Uma única divisão de validação é uma amostra só, e pode ter tido sorte ou azar.",[21,224,226],{"id":225},"um-bug-real-escondido-num-loop","Um bug real, escondido num loop",[11,228,229],{},"O professor então parte pra validação cruzada, implementada na unha:",[39,231,233],{"className":41,"code":232,"language":43,"meta":44,"style":44},"def cross_validation(model, X, y, k=3):\n    n = int(len(y)\u002Fk)\n    idx = np.random.permutation(len(y))\n    X = X[idx]\n    y = y[idx]\n    for i in range(k):\n        X_tr = np.concatenate([X[:i*n], X[(i+1)*n:]])\n        y_tr = np.concatenate([y[:i*n], y[(i+1)*n:]])\n        X_val = X[i*n:(i+1)*n]\n        y_val = y[i*n:(i+1)*n]\n        model.fit(X_tr, y_tr)\n        y_pred = model.predict(X_val)\n        acc = accuracy_score(y_val, y_pred)\n        return acc\n",[29,234,235,240,245,250,255,260,265,271,277,283,289,295,301,307],{"__ignoreMap":44},[48,236,237],{"class":50,"line":51},[48,238,239],{},"def cross_validation(model, X, y, k=3):\n",[48,241,242],{"class":50,"line":57},[48,243,244],{},"    n = int(len(y)\u002Fk)\n",[48,246,247],{"class":50,"line":113},[48,248,249],{},"    idx = np.random.permutation(len(y))\n",[48,251,252],{"class":50,"line":119},[48,253,254],{},"    X = X[idx]\n",[48,256,257],{"class":50,"line":125},[48,258,259],{},"    y = y[idx]\n",[48,261,262],{"class":50,"line":191},[48,263,264],{},"    for i in range(k):\n",[48,266,268],{"class":50,"line":267},7,[48,269,270],{},"        X_tr = np.concatenate([X[:i*n], X[(i+1)*n:]])\n",[48,272,274],{"class":50,"line":273},8,[48,275,276],{},"        y_tr = np.concatenate([y[:i*n], y[(i+1)*n:]])\n",[48,278,280],{"class":50,"line":279},9,[48,281,282],{},"        X_val = X[i*n:(i+1)*n]\n",[48,284,286],{"class":50,"line":285},10,[48,287,288],{},"        y_val = y[i*n:(i+1)*n]\n",[48,290,292],{"class":50,"line":291},11,[48,293,294],{},"        model.fit(X_tr, y_tr)\n",[48,296,298],{"class":50,"line":297},12,[48,299,300],{},"        y_pred = model.predict(X_val)\n",[48,302,304],{"class":50,"line":303},13,[48,305,306],{},"        acc = accuracy_score(y_val, y_pred)\n",[48,308,310],{"class":50,"line":309},14,[48,311,312],{},"        return acc\n",[11,314,315,316,326,327,330,331,334,335,338,339,342,343,346,347,349,350,353,354,357],{},"Vale a pena ler essa função com atenção, porque ela ensina uma lição que não tem nada a ver com machine learning: ",[67,317,318,319,322,323],{},"o ",[29,320,321],{},"return"," está dentro do ",[29,324,325],{},"for",". O laço roda a variável ",[29,328,329],{},"i"," de 0 até ",[29,332,333],{},"k-1"," pra fazer ",[29,336,337],{},"k"," dobras diferentes (a ideia certa de validação cruzada: cada dobra vira validação uma vez, o resto vira treino), mas a função sai fora e devolve o resultado assim que termina a ",[67,340,341],{},"primeira"," iteração (",[29,344,345],{},"i=0","). As outras ",[29,348,333],{}," dobras nunca chegam a rodar. ",[29,351,352],{},"cross_validation()",", apesar do nome, calcula só ",[67,355,356],{},"uma"," divisão treino\u002Fvalidação, exatamente a mesma limitação da seção anterior, só que escondida atrás de um nome que promete mais do que entrega.",[62,359,360],{},[11,361,362,32,364,367,368,371,372,375],{},[67,363,201],{},[29,365,366],{},"cross_validation(model, X_train, y_train)"," devolve ",[29,369,370],{},"0.723",", um único número de uma única dobra, embaralhada de um jeito diferente a cada chamada (porque ",[29,373,374],{},"np.random.permutation"," roda de novo toda vez).",[11,377,378,379,382,383,385,386,389],{},"Isso explica uma coisa estranha que aparece logo depois: ",[29,380,381],{},"repeated_cross_validation",", que chama ",[29,384,352],{}," dez vezes e tira média e desvio padrão, funciona meio que por acidente. Ela não está fazendo \"10 repetições de validação cruzada de verdade\" (que seria dobra completa, repetida 10 vezes), está fazendo ",[67,387,388],{},"holdout repetido",": 10 divisões aleatórias diferentes, cada uma avaliada uma vez só. A média das 10 (0.717, desvio 0.057, calculada logo depois no notebook) ainda é uma estimativa mais estável que uma única divisão, porque reduz a variância de \"eu tive sorte ou azar numa divisão só\". Só não é validação cruzada no sentido técnico do termo: nenhum ponto de treino nunca vira validação em mais de uma dessas 10 rodadas por acaso, não por garantia de cobertura completa como o k-fold de verdade promete.",[11,391,392,393,439,440,468,469,529,530,558,559,587,588,590,591,594],{},"O Bishop descreve exatamente esse k-fold de verdade (ele chama de ",[48,394,397,419],{"className":395},[396],"katex",[48,398,401],{"className":399},[400],"katex-mathml",[402,403,405],"math",{"xmlns":404},"http:\u002F\u002Fwww.w3.org\u002F1998\u002FMath\u002FMathML",[406,407,408,415],"semantics",{},[409,410,411],"mrow",{},[412,413,414],"mi",{},"S",[416,417,414],"annotation",{"encoding":418},"application\u002Fx-tex",[48,420,424],{"className":421,"ariaHidden":423},[422],"katex-html","true",[48,425,428,433],{"className":426},[427],"base",[48,429],{"className":430,"style":432},[431],"strut","height:0.6833em;",[48,434,414],{"className":435,"style":438},[436,437],"mord","mathnormal","margin-right:0.0576em;","-fold): divide o dado em ",[48,441,443,456],{"className":442},[396],[48,444,446],{"className":445},[400],[402,447,448],{"xmlns":404},[406,449,450,454],{},[409,451,452],{},[412,453,414],{},[416,455,414],{"encoding":418},[48,457,459],{"className":458,"ariaHidden":423},[422],[48,460,462,465],{"className":461},[427],[48,463],{"className":464,"style":432},[431],[48,466,414],{"className":467,"style":438},[436,437]," blocos, usa ",[48,470,472,494],{"className":471},[396],[48,473,475],{"className":474},[400],[402,476,477],{"xmlns":404},[406,478,479,491],{},[409,480,481,483,487],{},[412,482,414],{},[484,485,486],"mo",{},"−",[488,489,490],"mn",{},"1",[416,492,493],{"encoding":418},"S-1",[48,495,497,519],{"className":496,"ariaHidden":423},[422],[48,498,500,504,507,512,516],{"className":499},[427],[48,501],{"className":502,"style":503},[431],"height:0.7667em;vertical-align:-0.0833em;",[48,505,414],{"className":506,"style":438},[436,437],[48,508],{"className":509,"style":511},[510],"mspace","margin-right:0.2222em;",[48,513,486],{"className":514},[515],"mbin",[48,517],{"className":518,"style":511},[510],[48,520,522,526],{"className":521},[427],[48,523],{"className":524,"style":525},[431],"height:0.6444em;",[48,527,490],{"className":528},[436]," pra treinar e 1 pra validar, repete ",[48,531,533,546],{"className":532},[396],[48,534,536],{"className":535},[400],[402,537,538],{"xmlns":404},[406,539,540,544],{},[409,541,542],{},[412,543,414],{},[416,545,414],{"encoding":418},[48,547,549],{"className":548,"ariaHidden":423},[422],[48,550,552,555],{"className":551},[427],[48,553],{"className":554,"style":432},[431],[48,556,414],{"className":557,"style":438},[436,437]," vezes trocando qual bloco fica de fora, e faz a média das ",[48,560,562,575],{"className":561},[396],[48,563,565],{"className":564},[400],[402,566,567],{"xmlns":404},[406,568,569,573],{},[409,570,571],{},[412,572,414],{},[416,574,414],{"encoding":418},[48,576,578],{"className":577,"ariaHidden":423},[422],[48,579,581,584],{"className":580},[427],[48,582],{"className":583,"style":432},[431],[48,585,414],{"className":586,"style":438},[436,437]," notas. A garantia importante que o ",[29,589,352],{}," do professor perde com o bug: no k-fold de verdade, ",[67,592,593],{},"todo"," ponto vira validação exatamente uma vez, cobrindo o dataset inteiro sem sobreposição. Com holdout repetido (mesmo que repetido várias vezes), alguns pontos podem nunca cair na validação, e outros podem cair repetidas vezes, por puro acaso da amostragem aleatória.",[21,596,598],{"id":597},"fazendo-do-jeito-certo","Fazendo do jeito certo",[11,600,601,602,630],{},"O scikit-learn já implementa o ",[48,603,605,618],{"className":604},[396],[48,606,608],{"className":607},[400],[402,609,610],{"xmlns":404},[406,611,612,616],{},[409,613,614],{},[412,615,414],{},[416,617,414],{"encoding":418},[48,619,621],{"className":620,"ariaHidden":423},[422],[48,622,624,627],{"className":623},[427],[48,625],{"className":626,"style":432},[431],[48,628,414],{"className":629,"style":438},[436,437],"-fold do Bishop direitinho:",[39,632,634],{"className":41,"code":633,"language":43,"meta":44,"style":44},"from sklearn.model_selection import KFold\n\nkf = KFold(n_splits=3, shuffle=True)\naccs = []\nfor train_index, val_index in kf.split(X_train):\n    model = KNeighborsClassifier(n_neighbors=5)\n    model.fit(X_train[train_index], y_train[train_index])\n    accs.append(accuracy_score(y_train[val_index], model.predict(X_train[val_index])))\n\nprint(np.mean(accs))\n",[29,635,636,641,645,650,655,660,665,670,675,679],{"__ignoreMap":44},[48,637,638],{"class":50,"line":51},[48,639,640],{},"from sklearn.model_selection import KFold\n",[48,642,643],{"class":50,"line":57},[48,644,173],{"emptyLinePlaceholder":172},[48,646,647],{"class":50,"line":113},[48,648,649],{},"kf = KFold(n_splits=3, shuffle=True)\n",[48,651,652],{"class":50,"line":119},[48,653,654],{},"accs = []\n",[48,656,657],{"class":50,"line":125},[48,658,659],{},"for train_index, val_index in kf.split(X_train):\n",[48,661,662],{"class":50,"line":191},[48,663,664],{},"    model = KNeighborsClassifier(n_neighbors=5)\n",[48,666,667],{"class":50,"line":267},[48,668,669],{},"    model.fit(X_train[train_index], y_train[train_index])\n",[48,671,672],{"class":50,"line":273},[48,673,674],{},"    accs.append(accuracy_score(y_train[val_index], model.predict(X_train[val_index])))\n",[48,676,677],{"class":50,"line":279},[48,678,173],{"emptyLinePlaceholder":172},[48,680,681],{"class":50,"line":285},[48,682,683],{},"print(np.mean(accs))\n",[62,685,686],{},[11,687,688,690,691,694,695,698,699,702],{},[67,689,201],{}," 0.726, com ",[29,692,693],{},"KFold(n_splits=3)",". Com ",[29,696,697],{},"RepeatedKFold(n_splits=3, n_repeats=10)"," (o k-fold de verdade, repetido 10 vezes com embaralhamentos diferentes, pra reduzir ainda mais a variância da estimativa): 0.676. E o atalho de uma linha só, ",[29,700,701],{},"cross_val_score",", bate os dois: 0.704 e 0.701 respectivamente.",[11,704,705],{},"Os números não são muito diferentes do holdout repetido \"com bug\" (0.717), e isso é esperado: mesmo com a implementação errada, a ideia geral (testar em pedaços que não foram usados pra treinar) já capturava a maior parte do sinal. O ganho do k-fold de verdade é robustez, não necessariamente um número dramaticamente diferente nesse dataset específico. Mas você não sabe disso sem comparar, e é exatamente por isso que vale a pena implementar (ou usar) a coisa certa em vez de confiar que \"deu um número plausível\" significa \"o código está certo\".",[21,707,709],{"id":708},"gridsearchcv-automatizando-a-busca","GridSearchCV: automatizando a busca",[11,711,712,713,716,717,720],{},"Em vez de escrever um ",[29,714,715],{},"for k in range(1, 21, 2)"," toda vez, o ",[29,718,719],{},"GridSearchCV"," faz a busca (e a validação cruzada por trás dela) automaticamente:",[39,722,724],{"className":41,"code":723,"language":43,"meta":44,"style":44},"params = {'n_neighbors': range(1, 21, 2)}\ngrid = GridSearchCV(KNeighborsClassifier(), params, scoring='accuracy')\ngrid.fit(X_train, y_train)\nprint(grid.best_params_, grid.best_score_)\n",[29,725,726,731,736,741],{"__ignoreMap":44},[48,727,728],{"class":50,"line":51},[48,729,730],{},"params = {'n_neighbors': range(1, 21, 2)}\n",[48,732,733],{"class":50,"line":57},[48,734,735],{},"grid = GridSearchCV(KNeighborsClassifier(), params, scoring='accuracy')\n",[48,737,738],{"class":50,"line":113},[48,739,740],{},"grid.fit(X_train, y_train)\n",[48,742,743],{"class":50,"line":119},[48,744,745],{},"print(grid.best_params_, grid.best_score_)\n",[62,747,748],{},[11,749,750,32,752,755],{},[67,751,201],{},[29,753,754],{},"{'n_neighbors': 1}",", com nota de validação cruzada 0.761.",[11,757,758,759,762,763,762,766,769,770,773],{},"E expandindo a busca pra três hiperparâmetros de uma vez (",[29,760,761],{},"n_neighbors",", ",[29,764,765],{},"weights",[29,767,768],{},"metric","), com ",[29,771,772],{},"KFold(n_splits=5)",":",[62,775,776],{},[11,777,778,32,780,783],{},[67,779,201],{},[29,781,782],{},"{'metric': 'manhattan', 'n_neighbors': 11, 'weights': 'distance'}",", nota 0.803.",[11,785,786,787,789],{},"O Bishop já avisa exatamente esse tipo de situação no capítulo 1.3: quando você tem mais de um hiperparâmetro pra ajustar, testar cada combinação manualmente vira uma explosão combinatória rápido. ",[29,788,719],{}," é força bruta organizada: testa toda combinação da grade, valida cada uma com k-fold, e devolve a melhor.",[21,791,793],{"id":792},"pipeline-gridsearch-aí-sim-decola","Pipeline + GridSearch: aí sim decola",[11,795,796,797,32,799,802,803,805,806,809,810,813],{},"A virada de chave do post inteiro: colocar o ",[29,798,719],{},[67,800,801],{},"dentro"," de um ",[29,804,31],{},", com o normalizador junto, e ajustar até os hiperparâmetros do próprio ",[29,807,808],{},"KNeighborsClassifier"," (usando o prefixo ",[29,811,812],{},"model__"," pra apontar qual etapa do pipeline cada parâmetro pertence):",[39,815,817],{"className":41,"code":816,"language":43,"meta":44,"style":44},"pipeline = Pipeline([\n    ('scaler', StandardScaler()),\n    ('model', KNeighborsClassifier())\n])\nparams = {\n    'model__n_neighbors': range(1, 21, 2),\n    'model__weights': ['uniform', 'distance'],\n    'model__metric': ['euclidean', 'manhattan', 'minkowski'],\n}\ngrid = GridSearchCV(pipeline, params, scoring='accuracy', cv=KFold(n_splits=5, shuffle=True))\nscores = cross_val_score(grid, X_train, y_train, cv=KFold(n_splits=5, shuffle=True))\nprint(np.mean(scores))\n",[29,818,819,823,827,831,835,840,845,850,855,860,865,870],{"__ignoreMap":44},[48,820,821],{"class":50,"line":51},[48,822,105],{},[48,824,825],{"class":50,"line":57},[48,826,110],{},[48,828,829],{"class":50,"line":113},[48,830,116],{},[48,832,833],{"class":50,"line":119},[48,834,122],{},[48,836,837],{"class":50,"line":125},[48,838,839],{},"params = {\n",[48,841,842],{"class":50,"line":191},[48,843,844],{},"    'model__n_neighbors': range(1, 21, 2),\n",[48,846,847],{"class":50,"line":267},[48,848,849],{},"    'model__weights': ['uniform', 'distance'],\n",[48,851,852],{"class":50,"line":273},[48,853,854],{},"    'model__metric': ['euclidean', 'manhattan', 'minkowski'],\n",[48,856,857],{"class":50,"line":279},[48,858,859],{},"}\n",[48,861,862],{"class":50,"line":285},[48,863,864],{},"grid = GridSearchCV(pipeline, params, scoring='accuracy', cv=KFold(n_splits=5, shuffle=True))\n",[48,866,867],{"class":50,"line":291},[48,868,869],{},"scores = cross_val_score(grid, X_train, y_train, cv=KFold(n_splits=5, shuffle=True))\n",[48,871,872],{"class":50,"line":297},[48,873,874],{},"print(np.mean(scores))\n",[11,876,877,878,32,880,802,883,885,886,889,890,892,893,895,896,899],{},"Repara na estrutura: tem um ",[29,879,701],{},[67,881,882],{},"por fora",[29,884,719],{}," inteiro. Isso é validação cruzada ",[67,887,888],{},"aninhada",": o laço de fora mede o quão bem o processo completo (normalizar, buscar os melhores hiperparâmetros, treinar) generaliza, e o laço de dentro (dentro do ",[29,891,719],{},") é só pra escolher os hiperparâmetros. Sem esse aninhamento, a nota de validação cruzada do próprio ",[29,894,719],{}," (",[29,897,898],{},"best_score_",") fica levemente otimista, porque o mesmo dado que escolheu os hiperparâmetros também foi usado pra avaliar o resultado final.",[62,901,902],{},[11,903,904,906,907,910,911,914],{},[67,905,201],{}," média de 0.957 (contra 0.81 sem normalizar dentro do pipeline, e contra 0.68 do KNN cru sem grade nenhuma). Adicionando ",[29,908,909],{},"scaler__with_mean"," e ",[29,912,913],{},"scaler__with_std"," à grade de busca (deixando até a normalização ser parte do que é otimizado): 0.979.",[11,916,917],{},"Do KNN cru (0.67-0.68) pro pipeline completo com busca de hiperparâmetro aninhada (0.979): a distância inteira que separa \"rodei o modelo default\" de \"fiz isso direito\".",[21,919,921],{"id":920},"mais-rápido-que-grade-completa-busca-aleatória","Mais rápido que grade completa: busca aleatória",[11,923,924,927],{},[29,925,926],{},"RandomizedSearchCV"," testa só uma amostra aleatória de combinações (aqui, 20) em vez de todas:",[39,929,931],{"className":41,"code":930,"language":43,"meta":44,"style":44},"grid = RandomizedSearchCV(pipeline, params, scoring='accuracy', cv=KFold(n_splits=5, shuffle=True), n_iter=20)\n",[29,932,933],{"__ignoreMap":44},[48,934,935],{"class":50,"line":51},[48,936,930],{},[62,938,939],{},[11,940,941,943],{},[67,942,201],{}," 0.957, praticamente empatado com a grade completa, testando bem menos combinações.",[11,945,946,947,1051,1052,1055],{},"Faz sentido quando a grade de busca é grande demais pra testar tudo (aqui já são ",[48,948,950,982],{"className":949},[396],[48,951,953],{"className":952},[400],[402,954,955],{"xmlns":404},[406,956,957,979],{},[409,958,959,962,965,968,970,973,976],{},[488,960,961],{},"10",[484,963,964],{},"×",[488,966,967],{},"2",[484,969,964],{},[488,971,972],{},"3",[484,974,975],{},"=",[488,977,978],{},"60",[416,980,981],{"encoding":418},"10 \\times 2 \\times 3 = 60",[48,983,985,1004,1022,1042],{"className":984,"ariaHidden":423},[422],[48,986,988,992,995,998,1001],{"className":987},[427],[48,989],{"className":990,"style":991},[431],"height:0.7278em;vertical-align:-0.0833em;",[48,993,961],{"className":994},[436],[48,996],{"className":997,"style":511},[510],[48,999,964],{"className":1000},[515],[48,1002],{"className":1003,"style":511},[510],[48,1005,1007,1010,1013,1016,1019],{"className":1006},[427],[48,1008],{"className":1009,"style":991},[431],[48,1011,967],{"className":1012},[436],[48,1014],{"className":1015,"style":511},[510],[48,1017,964],{"className":1018},[515],[48,1020],{"className":1021,"style":511},[510],[48,1023,1025,1028,1031,1035,1039],{"className":1024},[427],[48,1026],{"className":1027,"style":525},[431],[48,1029,972],{"className":1030},[436],[48,1032],{"className":1033,"style":1034},[510],"margin-right:0.2778em;",[48,1036,975],{"className":1037},[1038],"mrel",[48,1040],{"className":1041,"style":1034},[510],[48,1043,1045,1048],{"className":1044},[427],[48,1046],{"className":1047,"style":525},[431],[48,1049,978],{"className":1050},[436]," combinações, cada uma com 5 dobras, 300 ajustes de modelo, e com mais hiperparâmetros isso explode rápido). O notebook ainda dá uma espiada rápida no ",[29,1053,1054],{},"Optuna",", uma biblioteca de busca bayesiana que escolhe a próxima combinação a testar com base no que já funcionou até agora, em vez de sortear ou testar tudo às cegas, mas isso é só uma menção de passagem, não o foco da aula.",[21,1057,1059],{"id":1058},"fechando","Fechando",[1061,1062,1063,1077],"table",{},[1064,1065,1066],"thead",{},[1067,1068,1069,1074],"tr",{},[1070,1071,1073],"th",{"align":1072},"left","O que eu já sabia",[1070,1075,1076],{"align":1072},"O que essas três aulas assentaram",[1078,1079,1080,1091,1102],"tbody",{},[1067,1081,1082,1088],{},[1083,1084,1085,1087],"td",{"align":1072},[29,1086,31],{}," evita vazamento entre normalização e modelo",[1083,1089,1090],{"align":1072},"O mesmo vale pra classificação, não só regressão, e o ganho aqui foi enorme (0.67 → 0.92)",[1067,1092,1093,1096],{},[1083,1094,1095],{"align":1072},"Validação cruzada existe pra dar uma estimativa mais estável que uma única divisão treino\u002Fteste",[1083,1097,1098,1099,1101],{"align":1072},"Um ",[29,1100,321],{}," no lugar errado transforma \"validação cruzada\" em holdout repetido sem eu perceber, então vale a pena ler o próprio código de validação, não só confiar no nome da função",[1067,1103,1104,1107],{},[1083,1105,1106],{"align":1072},"Hiperparâmetro é escolha minha",[1083,1108,1109,1111,1112,1114],{"align":1072},[29,1110,719],{}," automatiza a busca, e colocado dentro de um ",[29,1113,31],{},", com validação cruzada aninhada por fora, dá a estimativa mais honesta de generalização que eu já produzi nessa playlist",[21,1116,1118],{"id":1117},"aplicação-prática","Aplicação Prática",[11,1120,1121],{},"Reproduzo o pipeline completo (normalização + busca de hiperparâmetro + validação cruzada aninhada) no mesmo dataset de vinhos, com semente fixa pra um resultado reprodutível, coisa que o notebook original não tem em nenhuma das buscas.",[39,1123,1125],{"className":41,"code":1124,"language":43,"meta":44,"style":44},"X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)\n\npipeline = Pipeline([('scaler', StandardScaler()), ('model', KNeighborsClassifier())])\nparams = {\n    'model__n_neighbors': range(1, 21, 2),\n    'model__weights': ['uniform', 'distance'],\n    'model__metric': ['euclidean', 'manhattan', 'minkowski'],\n}\ngrid = GridSearchCV(pipeline, params, scoring='accuracy', cv=KFold(n_splits=5, shuffle=True, random_state=42))\ngrid.fit(X_train, y_train)\n",[29,1126,1127,1132,1136,1141,1145,1149,1153,1157,1161,1166],{"__ignoreMap":44},[48,1128,1129],{"class":50,"line":51},[48,1130,1131],{},"X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)\n",[48,1133,1134],{"class":50,"line":57},[48,1135,173],{"emptyLinePlaceholder":172},[48,1137,1138],{"class":50,"line":113},[48,1139,1140],{},"pipeline = Pipeline([('scaler', StandardScaler()), ('model', KNeighborsClassifier())])\n",[48,1142,1143],{"class":50,"line":119},[48,1144,839],{},[48,1146,1147],{"class":50,"line":125},[48,1148,844],{},[48,1150,1151],{"class":50,"line":191},[48,1152,849],{},[48,1154,1155],{"class":50,"line":267},[48,1156,854],{},[48,1158,1159],{"class":50,"line":273},[48,1160,859],{},[48,1162,1163],{"class":50,"line":279},[48,1164,1165],{},"grid = GridSearchCV(pipeline, params, scoring='accuracy', cv=KFold(n_splits=5, shuffle=True, random_state=42))\n",[48,1167,1168],{"class":50,"line":285},[48,1169,740],{},[1061,1171,1172,1183],{},[1064,1173,1174],{},[1067,1175,1176,1179],{},[1070,1177,1178],{"align":1072},"Etapa",[1070,1180,1182],{"align":1181},"right","Resultado",[1078,1184,1185,1193,1203,1211,1219],{},[1067,1186,1187,1190],{},[1083,1188,1189],{"align":1072},"KNN cru, sem normalizar, sem buscar hiperparâmetro",[1083,1191,1192],{"align":1181},"0.7222 (teste)",[1067,1194,1195,1198],{},[1083,1196,1197],{"align":1072},"Melhor combinação encontrada pela grade",[1083,1199,1200],{"align":1181},[29,1201,1202],{},"metric=manhattan, n_neighbors=9, weights=uniform",[1067,1204,1205,1208],{},[1083,1206,1207],{"align":1072},"Nota de validação cruzada da grade",[1083,1209,1210],{"align":1181},"0.9862",[1067,1212,1213,1216],{},[1083,1214,1215],{"align":1072},"Acurácia no teste, com o melhor pipeline",[1083,1217,1218],{"align":1181},"0.9722",[1067,1220,1221,1224],{},[1083,1222,1223],{"align":1072},"Validação cruzada aninhada (estimativa honesta)",[1083,1225,1226],{"align":1181},"0.9791",[11,1228,1229],{},"A nota de validação cruzada aninhada (0.9791) e a acurácia no teste (0.9722) ficam bem próximas uma da outra, e é isso que eu quero ver: significa que a validação cruzada aninhada não estava sendo otimista demais, ela realmente previu como o pipeline se sairia num dado que nunca tinha visto. Contra o KNN cru (0.7222), a diferença inteira (25 pontos percentuais) veio só de normalizar e escolher hiperparâmetro direito, sem tocar no algoritmo em si.",[1231,1232,1233],"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":44,"searchDepth":57,"depth":57,"links":1235},[1236,1237,1238,1239,1240,1241,1242,1243,1244],{"id":23,"depth":57,"text":24},{"id":142,"depth":57,"text":143},{"id":225,"depth":57,"text":226},{"id":597,"depth":57,"text":598},{"id":708,"depth":57,"text":709},{"id":792,"depth":57,"text":793},{"id":920,"depth":57,"text":921},{"id":1058,"depth":57,"text":1059},{"id":1117,"depth":57,"text":1118},null,"2026-08-19","Aula 4b, 4c e 4d: o professor monta o fluxo profissional de verdade em cima do KNN, e no meio do caminho eu encontro um bug real no código dele, um return que mora dentro do loop errado.","md",{},"\u002Fpt\u002Fplaylists\u002Fpattern-recognition\u002Fpipeline-cross-validation","pattern-recognition",{"title":6,"description":1247},"published","pt\u002Fplaylists\u002Fpattern-recognition\u002Fpipeline-cross-validation",[1256,1257,1258],"pipeline","validacao-cruzada","gridsearch","vPKMfwxYXwgpAGB5wY54TMhX5jA6NKe_vXuuY72ojaQ",1787338983252]