├── README.md ├── HomePrices-English.ipynb └── HomePrices.ipynb /README.md: -------------------------------------------------------------------------------- 1 | ## TutorialEnsemble 2 | ![Tutorial](https://img.shields.io/badge/Tutorial-Ensembling-brightgreen.svg) ![Type Regression](https://img.shields.io/badge/Type-Regression-yellow.svg) 3 | 4 | ### Tutorial: Increasing the Predictive Power of Your Machine Learning Models with Stacking Ensembles 5 | 6 | Whoever accompanies competitions knows that one of the most important things is to know how to put together several models to create a powerful solution. Several people have already asked me, by e-mail or in the presentations I made, about ensembles. This is an important issue not only for competitions, but also for real cases where you want to extract as much performance as possible from the models. 7 | 8 | Ensembles are sets of models that offer better performance than each model that composes it. 9 | 10 | So in this article I want to exemplify the best way I know of creating ensembles: stacking. This is a method I have used in all competitions that have had good results. 11 | 12 | *Problem Reference* : https://www.kaggle.com/c/house-prices-advanced-regression-techniques/ 13 | 14 | #### Dependencies 15 | * numpy 16 | * pandas 17 | * itertools 18 | * scikit-learn 19 | * jupyter notebook 20 | 21 | Notebook : English | 22 | Portuguese 23 | 24 | Special thanks to @girishkuniyal for translating the material to English. 25 | -------------------------------------------------------------------------------- /HomePrices-English.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## Tutorial: Increasing the Predictive Power of Your Machine Learning Models with Stacking Ensembles" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "Whoever accompanies competitions knows that one of the most important things is to know how to put together several models to create a powerful solution. Several people have already asked me, by e-mail or in the presentations I made, about ensembles. This is an important issue not only for competitions, but also for real cases where you want to extract as much performance as possible from the models.\n", 15 | "\n", 16 | "Ensembles are sets of models that offer better performance than each model that composes it.\n", 17 | "\n", 18 | "So in this article I want to exemplify the best way I know of creating ensembles: stacking. This is a method I have used in all competitions that have had good results.\n", 19 | "\n", 20 | "Before you begin, a tip: It's important to think of Machine Learning as \"processes.\" Here we will not only test models in a database, but we will test processes, pipelines of methods applied to the data to know what the results are.\n", 21 | "\n", 22 | "This material is part of the presentation I will make (or did) on 30/11 at the PyData Meetup São Paulo. The original Jupyter Notebook is available at this link.\n" 23 | ] 24 | }, 25 | { 26 | "cell_type": "code", 27 | "execution_count": 1, 28 | "metadata": { 29 | "collapsed": true 30 | }, 31 | "outputs": [], 32 | "source": [ 33 | "import pandas as pd\n", 34 | "import numpy as np" 35 | ] 36 | }, 37 | { 38 | "cell_type": "markdown", 39 | "metadata": {}, 40 | "source": [ 41 | "## Loading data" 42 | ] 43 | }, 44 | { 45 | "cell_type": "markdown", 46 | "metadata": {}, 47 | "source": [ 48 | "The data used are commercial real estate transactions in the city of Ames, Iowa. Our goal is to predict the selling price of a house by feeding the model with the features. This data is also the subject of a competition at Kaggle. The training and test data can be found at: https://www.kaggle.com/c/house-prices-advanced-regression-techniques/\n", 49 | "\n", 50 | "Something very important is to explore the data for ideas of features and methods to validate the models. As the focus of this material is stacking, I'll skip this part. Nevertheless, I think it is important to clarify that we have two basic types of variables in this data: categorical and numerical.\n", 51 | "\n", 52 | "Categorical variables have levels that can not be ordered. In some cases it is possible to think of ways to order them, as a variable that describes whether a street is paved or not. In this case one can think that the paved street will value the property.\n", 53 | "\n", 54 | "Numeric variables can receive any continuous value. An example would be the size of the terrain.\n", 55 | "\n", 56 | "In this cell we load the data and create a DataFrame with the features (X) and a Series with the sale price (SalePrice, y) that is our target." 57 | ] 58 | }, 59 | { 60 | "cell_type": "code", 61 | "execution_count": 2, 62 | "metadata": {}, 63 | "outputs": [ 64 | { 65 | "data": { 66 | "text/html": [ 67 | "
\n", 68 | "\n", 69 | " \n", 70 | " \n", 71 | " \n", 72 | " \n", 73 | " \n", 74 | " \n", 75 | " \n", 76 | " \n", 77 | " \n", 78 | " \n", 79 | " \n", 80 | " \n", 81 | " \n", 82 | " \n", 83 | " \n", 84 | " \n", 85 | " \n", 86 | " \n", 87 | " \n", 88 | " \n", 89 | " \n", 90 | " \n", 91 | " \n", 92 | " \n", 93 | " \n", 94 | " \n", 95 | " \n", 96 | " \n", 97 | " \n", 98 | " \n", 99 | " \n", 100 | " \n", 101 | " \n", 102 | " \n", 103 | " \n", 104 | " \n", 105 | " \n", 106 | " \n", 107 | " \n", 108 | " \n", 109 | " \n", 110 | " \n", 111 | " \n", 112 | " \n", 113 | " \n", 114 | " \n", 115 | " \n", 116 | " \n", 117 | " \n", 118 | " \n", 119 | " \n", 120 | " \n", 121 | " \n", 122 | " \n", 123 | " \n", 124 | " \n", 125 | " \n", 126 | " \n", 127 | " \n", 128 | " \n", 129 | "
MSSubClassMSZoningLotFrontageLotAreaStreet
Id
160RL65.08450Pave
220RL80.09600Pave
360RL68.011250Pave
470RL60.09550Pave
560RL84.014260Pave
\n", 130 | "
" 131 | ], 132 | "text/plain": [ 133 | " MSSubClass MSZoning LotFrontage LotArea Street\n", 134 | "Id \n", 135 | "1 60 RL 65.0 8450 Pave\n", 136 | "2 20 RL 80.0 9600 Pave\n", 137 | "3 60 RL 68.0 11250 Pave\n", 138 | "4 70 RL 60.0 9550 Pave\n", 139 | "5 60 RL 84.0 14260 Pave" 140 | ] 141 | }, 142 | "execution_count": 2, 143 | "metadata": {}, 144 | "output_type": "execute_result" 145 | } 146 | ], 147 | "source": [ 148 | "train = pd.read_csv('train.csv', index_col='Id')\n", 149 | "X, y = train.drop('SalePrice', axis=1), train.SalePrice.copy()\n", 150 | "train.head().iloc[:, :5]" 151 | ] 152 | }, 153 | { 154 | "cell_type": "markdown", 155 | "metadata": {}, 156 | "source": [ 157 | "## Sets of Variables (Features)" 158 | ] 159 | }, 160 | { 161 | "cell_type": "markdown", 162 | "metadata": {}, 163 | "source": [ 164 | "One way to get different models is to vary the data representation used to train them. That is why our first step is to build these representations." 165 | ] 166 | }, 167 | { 168 | "cell_type": "markdown", 169 | "metadata": {}, 170 | "source": [ 171 | "### Auxiliary Functions\n", 172 | "\n", 173 | "The metric suggested by Kaggle to evaluate the models is RMSLE, this error takes into account the difference between the logarithm of the predictions and the target. It is possible to think of this error as an approximation of the percentage error of the model, but with more interesting properties from the mathematical point of view.\n", 174 | "\n", 175 | "I created three functions to calculate the error. We are going to do transformations of the variable y, so to make our work easier, I decided to create custom functions for each transformation, thus avoiding some confusion when transforming them to compute the error in a single function.\n", 176 | "\n", 177 | "To use some Scikit-learn functions that will help keep our code cleaner and more efficient, we need to create the error functions in the way that the module requires. In this case, the pecse function receives a trained model, the features and the target. It computes the predictions and should return a number, which is the value of the error metric.\n", 178 | "\n", 179 | "The last cell line creates a cross-validation object from scikit-learn. He will take care of dividing the data for us. This subject deserves a number of articles, but basically it is a way to divide the data into N parts, and repeatedly use N-1 parts as training data, and the remaining part as test data. For example, if we have the parts [1, 2, 3, 4, 5], in one of the iterations we can train using parts [1, 2, 3, 4] and validate in part 5.\n", 180 | "\n", 181 | "Although this is a time series, Kaggle has decided to treat as independent random data, so we will do this simple cross validation." 182 | ] 183 | }, 184 | { 185 | "cell_type": "code", 186 | "execution_count": 3, 187 | "metadata": { 188 | "collapsed": true 189 | }, 190 | "outputs": [], 191 | "source": [ 192 | "from sklearn.metrics import mean_squared_error\n", 193 | "from sklearn.model_selection import cross_val_score, cross_val_predict, KFold\n", 194 | "from sklearn.ensemble import RandomForestRegressor\n", 195 | "\n", 196 | "def rmsle(estimator, X, y):\n", 197 | " p = estimator.predict(X)\n", 198 | " return np.sqrt(mean_squared_error(np.log1p(y), np.log1p(p)))\n", 199 | "\n", 200 | "def rmsle_log_y(estimator, X, y):\n", 201 | " p = estimator.predict(X)\n", 202 | " return np.sqrt(mean_squared_error(y, p))\n", 203 | "\n", 204 | "def rmsle_sqrt_y(estimator, X, y):\n", 205 | " p = estimator.predict(X)\n", 206 | " y = np.power(y, 2)\n", 207 | " p = np.power(p, 2)\n", 208 | " return np.sqrt(mean_squared_error(np.log1p(y), np.log1p(p)))\n", 209 | "\n", 210 | "kf = KFold(n_splits=5, shuffle=True, random_state=1)" 211 | ] 212 | }, 213 | { 214 | "cell_type": "markdown", 215 | "metadata": {}, 216 | "source": [ 217 | "### Feature set 1: \"numeric\" variables\n", 218 | "\n", 219 | "The first set of features we will have will be a simple selection of the originally numeric data variables. To do this, just select all the columns that have variables with integers or floating point. Also, to replace the null values ​​(scikit-learn requires replacement), I decided to put the -1 value. Since we are going to use only tree-based models with this unique data, there is no need to worry too much about it for our purposes.\n", 220 | "\n", 221 | "After storing this data in variable X1, we see that there are 36 columns. Already in this part I want to train a model of Random Forest inside the cross-validation, to know how it would leave alone in the original data. For this I used the cross_val_score function. It is a feature that makes it much easier to cross-validate with scikit-learn. Just put a template and the data. In this case I chose to specify a validation scheme and a custom error metric.\n", 222 | "\n", 223 | "This function returns a list with the errors of each iteration of the cross validation, so I averaged to know the average error of the parts." 224 | ] 225 | }, 226 | { 227 | "cell_type": "code", 228 | "execution_count": 4, 229 | "metadata": {}, 230 | "outputs": [ 231 | { 232 | "name": "stdout", 233 | "output_type": "stream", 234 | "text": [ 235 | "Dims (1460, 36)\n", 236 | "RMSLE: 0.14582352618\n" 237 | ] 238 | } 239 | ], 240 | "source": [ 241 | "X1 = X.select_dtypes(include=[np.number]).fillna(-1)\n", 242 | "print('Dims', X1.shape)\n", 243 | "model = RandomForestRegressor(n_estimators=1000, random_state=0)\n", 244 | "error = cross_val_score(model, X1, y, cv=kf, scoring=rmsle).mean()\n", 245 | "print('RMSLE:', error)" 246 | ] 247 | }, 248 | { 249 | "cell_type": "markdown", 250 | "metadata": {}, 251 | "source": [ 252 | "### Feature set 2: Ordinal Encoding Categorical" 253 | ] 254 | }, 255 | { 256 | "cell_type": "markdown", 257 | "metadata": {}, 258 | "source": [ 259 | "Now let's create another set of features, this time adding the categorical variables. There are several ways to encode this type of variable for the models, one of which is using an ordinal format. This simply means replacing each original value with sequential numbers. On some models this can be problematic as they will try to capture some order relation in values they may not have. In our case, with models based on decision trees, this problem is almost non-existent.\n", 260 | "\n", 261 | "After coding in this way, we ran the cross-validation again, now on these new data." 262 | ] 263 | }, 264 | { 265 | "cell_type": "code", 266 | "execution_count": 5, 267 | "metadata": {}, 268 | "outputs": [ 269 | { 270 | "name": "stdout", 271 | "output_type": "stream", 272 | "text": [ 273 | "Dims (1460, 79)\n", 274 | "RMSLE: 0.143837364859\n" 275 | ] 276 | } 277 | ], 278 | "source": [ 279 | "from sklearn.preprocessing import LabelEncoder\n", 280 | "\n", 281 | "X2 = X.copy()\n", 282 | "for col in X2.columns:\n", 283 | " if X2[col].dtype == object:\n", 284 | " enc = LabelEncoder()\n", 285 | " X2[col] = enc.fit_transform(X[col].fillna('Missing'))\n", 286 | "\n", 287 | "print('Dims', X2.shape)\n", 288 | "X2.fillna(-1, inplace=True)\n", 289 | "model = RandomForestRegressor(n_estimators=1000, random_state=0)\n", 290 | "error = cross_val_score(model, X2, y, cv=kf, scoring=rmsle).mean()\n", 291 | "print('RMSLE:', error)" 292 | ] 293 | }, 294 | { 295 | "cell_type": "markdown", 296 | "metadata": {}, 297 | "source": [ 298 | "### Bonus: OneHot Encoding" 299 | ] 300 | }, 301 | { 302 | "cell_type": "markdown", 303 | "metadata": {}, 304 | "source": [ 305 | "The most popular way to code categorical variables is One Hot Encoding. Basically consists of transforming each value of the variable into a column whose new value will be 1 if the variable has that value in a given example, or 0 if not. There are indications that decision trees do not process this kind of representation so well, but in some practical cases I've seen this work better than the ordinal, so look at this as another tool.\n", 306 | "\n", 307 | "This method creates more than 200 new columns, which makes the training process slower, so I decided to leave the cross-validation line commented out. If you want to see the result, just run it without the old woman's game." 308 | ] 309 | }, 310 | { 311 | "cell_type": "code", 312 | "execution_count": 6, 313 | "metadata": {}, 314 | "outputs": [ 315 | { 316 | "name": "stdout", 317 | "output_type": "stream", 318 | "text": [ 319 | "Dims (1460, 288)\n" 320 | ] 321 | } 322 | ], 323 | "source": [ 324 | "#from sklearn.preprocessing import OneHotEncoder\n", 325 | "X3 = X.copy()\n", 326 | "cats = []\n", 327 | "for col in X3.columns:\n", 328 | " if X3[col].dtype == object:\n", 329 | " X3 = X3.join(pd.get_dummies(X3[col], prefix=col), how='left')\n", 330 | " X3.drop(col, axis=1, inplace=True)\n", 331 | " \n", 332 | "\n", 333 | "print('Dims', X3.shape)\n", 334 | "X3.fillna(-1, inplace=True)\n", 335 | "model = RandomForestRegressor(n_estimators=1000, random_state=0)\n", 336 | "#cross_val_score(model, X3, y, cv=kf, scoring=rmsle).mean()" 337 | ] 338 | }, 339 | { 340 | "cell_type": "markdown", 341 | "metadata": {}, 342 | "source": [ 343 | "## Target Transformations\n", 344 | "\n", 345 | "An interesting way to create diversity, and sometimes even better performance, in a regression case is to transform the variable we are trying to predict. In this case we will test two transformations: logarithm and square root." 346 | ] 347 | }, 348 | { 349 | "cell_type": "markdown", 350 | "metadata": {}, 351 | "source": [ 352 | "### Log\n", 353 | "\n", 354 | "It is possible to see that trying to predict the logarithm of the price gives us a better result. This happens not only because the model captures different patterns, but also because we use a metric based on the difference of logarithms." 355 | ] 356 | }, 357 | { 358 | "cell_type": "code", 359 | "execution_count": 7, 360 | "metadata": {}, 361 | "outputs": [ 362 | { 363 | "name": "stdout", 364 | "output_type": "stream", 365 | "text": [ 366 | "RF, X1, log-target RMSLE: 0.14518580749\n", 367 | "RF, X2, log-target RMSLE: 0.14207134495\n" 368 | ] 369 | } 370 | ], 371 | "source": [ 372 | "model = RandomForestRegressor(n_estimators=1000, random_state=0)\n", 373 | "error = cross_val_score(model, X1, np.log1p(y), cv=kf, scoring=rmsle_log_y).mean()\n", 374 | "print('RF, X1, log-target RMSLE:', error)\n", 375 | "\n", 376 | "model = RandomForestRegressor(n_estimators=1000, random_state=0)\n", 377 | "error = cross_val_score(model, X2, np.log1p(y), cv=kf, scoring=rmsle_log_y).mean()\n", 378 | "print('RF, X2, log-target RMSLE:', error)" 379 | ] 380 | }, 381 | { 382 | "cell_type": "markdown", 383 | "metadata": {}, 384 | "source": [ 385 | "### Square root\n", 386 | "\n", 387 | "This transformation also gives us a better result than using the variable in its original state. One of the suggestions of the reason we see this effect is that these transformations cause the variable y to have a distribution closer to normal, which facilitates the work of the model." 388 | ] 389 | }, 390 | { 391 | "cell_type": "code", 392 | "execution_count": 8, 393 | "metadata": {}, 394 | "outputs": [ 395 | { 396 | "name": "stdout", 397 | "output_type": "stream", 398 | "text": [ 399 | "RF, X1, sqrt-target RMSLE: 0.145652934484\n", 400 | "RF, X2, sqrt-target RMSLE: 0.143004600132\n" 401 | ] 402 | } 403 | ], 404 | "source": [ 405 | "model = RandomForestRegressor(n_estimators=1000, random_state=0)\n", 406 | "error = cross_val_score(model, X1, np.sqrt(y), cv=kf, scoring=rmsle_sqrt_y).mean()\n", 407 | "print('RF, X1, sqrt-target RMSLE:', error)\n", 408 | "\n", 409 | "model = RandomForestRegressor(n_estimators=1000, random_state=0)\n", 410 | "error = cross_val_score(model, X2, np.sqrt(y), cv=kf, scoring=rmsle_sqrt_y).mean()\n", 411 | "print('RF, X2, sqrt-target RMSLE:', error)" 412 | ] 413 | }, 414 | { 415 | "cell_type": "markdown", 416 | "metadata": {}, 417 | "source": [ 418 | "## Generating models with different models / algorithms\n", 419 | "\n", 420 | "Another way to generate diversity for the ensemble is to generate different models. In this case I will use my preferred model, the GBM. This is also based on decision trees, but basically trains each tree sequentially focusing on the mistakes made by the previous ones.\n", 421 | "\n", 422 | "In the cells below you can see the performance of this model in the feature sets and transformations we use with Random Forest. We see that it brings a significant improvement, capturing better the patterns of the relationship between the variables and the sale price of the real estate." 423 | ] 424 | }, 425 | { 426 | "cell_type": "code", 427 | "execution_count": 9, 428 | "metadata": {}, 429 | "outputs": [ 430 | { 431 | "name": "stdout", 432 | "output_type": "stream", 433 | "text": [ 434 | "GBM, X1, log-target RMSLE: 0.133492454914\n", 435 | "GBM, X2, log-target RMSLE: 0.129806890482\n" 436 | ] 437 | } 438 | ], 439 | "source": [ 440 | "from sklearn.ensemble import GradientBoostingRegressor\n", 441 | " \n", 442 | "model = GradientBoostingRegressor(random_state=0)\n", 443 | "error = cross_val_score(model, X1, np.log1p(y), cv=kf, scoring=rmsle_log_y).mean()\n", 444 | "print('GBM, X1, log-target RMSLE:', error)\n", 445 | "\n", 446 | "model = GradientBoostingRegressor(random_state=0)\n", 447 | "error = cross_val_score(model, X2, np.log1p(y), cv=kf, scoring=rmsle_log_y).mean()\n", 448 | "print('GBM, X2, log-target RMSLE:', error)" 449 | ] 450 | }, 451 | { 452 | "cell_type": "code", 453 | "execution_count": 10, 454 | "metadata": {}, 455 | "outputs": [ 456 | { 457 | "name": "stdout", 458 | "output_type": "stream", 459 | "text": [ 460 | "GBM, X1, sqrt-target RMSLE: 0.134258972813\n", 461 | "GBM, X2, sqrt-target RMSLE: 0.130919235682\n" 462 | ] 463 | } 464 | ], 465 | "source": [ 466 | "from sklearn.ensemble import GradientBoostingRegressor\n", 467 | " \n", 468 | "model = GradientBoostingRegressor(random_state=0)\n", 469 | "error = cross_val_score(model, X1, np.sqrt(y), cv=kf, scoring=rmsle_sqrt_y).mean()\n", 470 | "print('GBM, X1, sqrt-target RMSLE:', error)\n", 471 | "\n", 472 | "model = GradientBoostingRegressor(random_state=0)\n", 473 | "error = cross_val_score(model, X2, np.sqrt(y), cv=kf, scoring=rmsle_sqrt_y).mean()\n", 474 | "print('GBM, X2, sqrt-target RMSLE:', error)" 475 | ] 476 | }, 477 | { 478 | "cell_type": "markdown", 479 | "metadata": {}, 480 | "source": [ 481 | "### Adjusting hyperparameters" 482 | ] 483 | }, 484 | { 485 | "cell_type": "markdown", 486 | "metadata": {}, 487 | "source": [ 488 | "To simplify the example and focus on the ensemble part, I will not adjust the hyperparameters of the model. Hyperparameters are the attributes of the model (such as the depth of the decision trees) that need to be adjusted using separate validation data or a cross-validation cycle.\n", 489 | "\n", 490 | "It is good to know that not always the best models form the best ensembles. It is important to have powerful models when stacking, but we must also remember that it is important to have diversity. Sometimes some models that have a higher error can capture different patterns of the best models and therefore contribute to the ensemble.\n", 491 | "\n", 492 | "If you decide to adjust the hyperparameters, it is important to place it within the cross-validation cycle that we will see in the next step." 493 | ] 494 | }, 495 | { 496 | "cell_type": "markdown", 497 | "metadata": {}, 498 | "source": [ 499 | "## Stacking\n", 500 | "\n", 501 | "All we did above is so we can create our ensemble. This is the time to put together the methods used to improve the predictive power of our models.\n", 502 | "\n", 503 | "Stacking is a way of doing the ensemble in which we use models to make predictions, and then we use these predictions as features in new models, in what can be called the \"second level.\" You can do this process several times, but at each level the return on performance with respect to the required computation is less.\n", 504 | "\n", 505 | "In this phase we need two cycles of cross-validation: external and internal. Inside, we will train the models on the original data and make the predictions. On the outside, we will train the model using the first step predictions as features.\n", 506 | "\n", 507 | "At each step of internal cross validation, we will save the predictions for the part of the data that is used as validation. This way we will have predictions for all examples of our training data. In addition, we will train a model in the original training data so that we can make predictions for the test data.\n", 508 | "\n", 509 | "In the external cycle we will train a model in the predictions generated by the internal cycle, and the features of the validation data will be the predictions of the first level models in the test data.\n", 510 | "\n", 511 | "In our specific case, we will create predictions using all combinations of models (RF and GBM), target (log and square root) transformations and feature sets (X1 and X2). We will not use X3 because it would take a lot of time to train, and for the purposes of demonstrating the method these two will be enough.\n", 512 | "\n", 513 | "In the end we will have predictions of 8 models in the first level. In the second level I used a regularized linear regression (Ridge). Having these predictions we can compute the error in the data outside of our internal training and validation samples, which will give us a reliable estimate of the ensemble error." 514 | ] 515 | }, 516 | { 517 | "cell_type": "code", 518 | "execution_count": 13, 519 | "metadata": {}, 520 | "outputs": [ 521 | { 522 | "name": "stdout", 523 | "output_type": "stream", 524 | "text": [ 525 | "RMSLE Fold 0 - RMSLE 0.1248\n", 526 | "RMSLE Fold 1 - RMSLE 0.1449\n", 527 | "RMSLE Fold 2 - RMSLE 0.1257\n", 528 | "RMSLE Fold 3 - RMSLE 0.1409\n", 529 | "RMSLE Fold 4 - RMSLE 0.1087\n", 530 | "RMSLE CV5 0.1290\n" 531 | ] 532 | } 533 | ], 534 | "source": [ 535 | "from itertools import product\n", 536 | "from sklearn.linear_model import Ridge\n", 537 | "\n", 538 | "kf_out = KFold(n_splits=5, shuffle=True, random_state=1)\n", 539 | "kf_in = KFold(n_splits=5, shuffle=True, random_state=2)\n", 540 | "\n", 541 | "cv_mean = []\n", 542 | "for fold, (tr, ts) in enumerate(kf_out.split(X, y)):\n", 543 | " X1_train, X1_test = X1.iloc[tr], X1.iloc[ts]\n", 544 | " X2_train, X2_test = X2.iloc[tr], X2.iloc[ts]\n", 545 | " y_train, y_test = y.iloc[tr], y.iloc[ts]\n", 546 | " \n", 547 | " modelos = [GradientBoostingRegressor(random_state=0), RandomForestRegressor(random_state=0)]\n", 548 | " targets = [np.log1p, np.sqrt]\n", 549 | " feature_sets = [(X1_train, X1_test), (X2_train, X2_test)]\n", 550 | " \n", 551 | " \n", 552 | " predictions_cv = []\n", 553 | " predictions_test = []\n", 554 | " for model, target, feature_set in product(modelos, targets, feature_sets):\n", 555 | " predictions_cv.append(cross_val_predict(model, feature_set[0], target(y_train), cv=kf_in).reshape(-1,1))\n", 556 | " model.fit(feature_set[0], target(y_train))\n", 557 | " ptest = model.predict(feature_set[1])\n", 558 | " predictions_test.append(ptest.reshape(-1,1))\n", 559 | " \n", 560 | " predictions_cv = np.concatenate(predictions_cv, axis=1)\n", 561 | " predictions_test = np.concatenate(predictions_test, axis=1)\n", 562 | " \n", 563 | " stacker = Ridge()\n", 564 | " stacker.fit(predictions_cv, np.log1p(y_train))\n", 565 | " error = rmsle_log_y(stacker, predictions_test, np.log1p(y_test))\n", 566 | " cv_mean.append(error)\n", 567 | " print('RMSLE Fold %d - RMSLE %.4f' % (fold, error))\n", 568 | " \n", 569 | "print('RMSLE CV5 %.4f' % np.mean(cv_mean))\n", 570 | " \n" 571 | ] 572 | }, 573 | { 574 | "cell_type": "markdown", 575 | "metadata": { 576 | "collapsed": true 577 | }, 578 | "source": [ 579 | "As we can see, our best first level model is the GBM trained with the log transformation in the X2 data, which reaches the error of 0.1298. Our ensemble reaches the value of 0.1290. An improvement of 0.62%.\n", 580 | "\n", 581 | "The purpose of this article was to demonstrate the method, without worrying too much about the end performance. An ensemble made for performance improvement may present a more significant result.\n", 582 | "\n", 583 | "In some cases, such as in investment funds or healthcare applications, a small improvement can have a very significant result in the real world, which justifies the creation of a more complete solution using stacking.\n", 584 | "\n", 585 | "As always in the Machine Learning application, nothing is a guarantee of success, but this method is one of the most consistent in offering an improvement." 586 | ] 587 | } 588 | ], 589 | "metadata": { 590 | "anaconda-cloud": {}, 591 | "kernelspec": { 592 | "display_name": "Python [Root]", 593 | "language": "python", 594 | "name": "Python [Root]" 595 | }, 596 | "language_info": { 597 | "codemirror_mode": { 598 | "name": "ipython", 599 | "version": 3 600 | }, 601 | "file_extension": ".py", 602 | "mimetype": "text/x-python", 603 | "name": "python", 604 | "nbconvert_exporter": "python", 605 | "pygments_lexer": "ipython3", 606 | "version": "3.5.2" 607 | } 608 | }, 609 | "nbformat": 4, 610 | "nbformat_minor": 1 611 | } 612 | -------------------------------------------------------------------------------- /HomePrices.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## Tutorial: Aumentando o Poder Preditivo de Seus Modelos de Machine Learning com Stacking Ensembles" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "Quem acompanha competições sabe que uma das coisas mais importantes é saber juntar vários modelos para criar uma solução poderosa. Várias pessoas já me perguntaram, por e-mail ou nas apresentações que fiz, sobre ensembles. Este é um assunto importante não apenas para competições, mas também para casos reais onde se quer extrair o máximo possível de performance dos modelos.\n", 15 | "\n", 16 | "Ensembles são conjuntos de modelos que oferecem uma performance melhor do que cada modelo que o compõe.\n", 17 | "\n", 18 | "Então neste artigo quero exemplificar a melhor maneira que conheço de criar ensembles: stacking. Este é um método que usei em todas as competições que tive bons resultados.\n", 19 | "\n", 20 | "Antes de começar, uma dica: é importante pensar em Machine Learning como \"processos\". Aqui não vamos apenas testar modelos em um banco de dados, mas vamos testar processos, pipelines de métodos aplicadas aos dados para saber quais são os resultados.\n", 21 | "\n", 22 | "Este material é parte da apresentação que farei (ou fiz) no dia 30/11 no PyData Meetup São Paulo. O Jupyter Notebook original está diponível neste link." 23 | ] 24 | }, 25 | { 26 | "cell_type": "code", 27 | "execution_count": 1, 28 | "metadata": { 29 | "collapsed": true 30 | }, 31 | "outputs": [], 32 | "source": [ 33 | "import pandas as pd\n", 34 | "import numpy as np" 35 | ] 36 | }, 37 | { 38 | "cell_type": "markdown", 39 | "metadata": {}, 40 | "source": [ 41 | "## Carregando os dados" 42 | ] 43 | }, 44 | { 45 | "cell_type": "markdown", 46 | "metadata": {}, 47 | "source": [ 48 | "Os dados utilizados são transações comerciais de imóveis na cidade de Ames, Iowa. Nosso objetivo é prever o preço de venda de uma casa alimentando o modelo com as características. Estes dados também são tema de uma competição no Kaggle. Os dados de treino e teste podem ser encontrados em: https://www.kaggle.com/c/house-prices-advanced-regression-techniques/\n", 49 | "\n", 50 | "Algo muito importante é explorar os dados em busca de ideias de features e métodos para validar os modelos. Como o foco deste material é stacking, vou pular esta parte. Ainda assim, acho importante esclarecer que temos dois tipos básicos de variáveis nestes dados: categóricas e numéricas.\n", 51 | "\n", 52 | "As variáveis categóricas possuem níveis que não podem ser ordenados. Em alguns casos é possível pensar em maneiras de ordená-los, como uma variável que descreve se uma rua é asfaltada ou não. Neste caso pode-se pensar que a rua asfaltada vai valorizar o imóvel.\n", 53 | "\n", 54 | "As variáveis numéricas podem receber qualquer valor contínuo. Um exemplo seria o tamanho do terreno.\n", 55 | "\n", 56 | "Nesta célula carregamos os dados e criamos um DataFrame com as features (X) e uma Série com o preço da venda (SalePrice, y) que é nosso alvo.\n", 57 | "\n", 58 | "No total temos 79 colunas, mas limitei a 5 aqui por causa da formatação do site." 59 | ] 60 | }, 61 | { 62 | "cell_type": "code", 63 | "execution_count": 2, 64 | "metadata": {}, 65 | "outputs": [ 66 | { 67 | "data": { 68 | "text/html": [ 69 | "
\n", 70 | "\n", 71 | " \n", 72 | " \n", 73 | " \n", 74 | " \n", 75 | " \n", 76 | " \n", 77 | " \n", 78 | " \n", 79 | " \n", 80 | " \n", 81 | " \n", 82 | " \n", 83 | " \n", 84 | " \n", 85 | " \n", 86 | " \n", 87 | " \n", 88 | " \n", 89 | " \n", 90 | " \n", 91 | " \n", 92 | " \n", 93 | " \n", 94 | " \n", 95 | " \n", 96 | " \n", 97 | " \n", 98 | " \n", 99 | " \n", 100 | " \n", 101 | " \n", 102 | " \n", 103 | " \n", 104 | " \n", 105 | " \n", 106 | " \n", 107 | " \n", 108 | " \n", 109 | " \n", 110 | " \n", 111 | " \n", 112 | " \n", 113 | " \n", 114 | " \n", 115 | " \n", 116 | " \n", 117 | " \n", 118 | " \n", 119 | " \n", 120 | " \n", 121 | " \n", 122 | " \n", 123 | " \n", 124 | " \n", 125 | " \n", 126 | " \n", 127 | " \n", 128 | " \n", 129 | " \n", 130 | " \n", 131 | "
MSSubClassMSZoningLotFrontageLotAreaStreet
Id
160RL65.08450Pave
220RL80.09600Pave
360RL68.011250Pave
470RL60.09550Pave
560RL84.014260Pave
\n", 132 | "
" 133 | ], 134 | "text/plain": [ 135 | " MSSubClass MSZoning LotFrontage LotArea Street\n", 136 | "Id \n", 137 | "1 60 RL 65.0 8450 Pave\n", 138 | "2 20 RL 80.0 9600 Pave\n", 139 | "3 60 RL 68.0 11250 Pave\n", 140 | "4 70 RL 60.0 9550 Pave\n", 141 | "5 60 RL 84.0 14260 Pave" 142 | ] 143 | }, 144 | "execution_count": 2, 145 | "metadata": {}, 146 | "output_type": "execute_result" 147 | } 148 | ], 149 | "source": [ 150 | "train = pd.read_csv('train.csv', index_col='Id')\n", 151 | "X, y = train.drop('SalePrice', axis=1), train.SalePrice.copy()\n", 152 | "train.head().iloc[:, :5]" 153 | ] 154 | }, 155 | { 156 | "cell_type": "markdown", 157 | "metadata": {}, 158 | "source": [ 159 | "## Conjuntos de Variáveis (Features)" 160 | ] 161 | }, 162 | { 163 | "cell_type": "markdown", 164 | "metadata": {}, 165 | "source": [ 166 | "Uma das maneiras de obter modelos diferentes é variar a representação dos dados utilizada para treiná-los. Por isso nosso primeiro passo será construir estas representações." 167 | ] 168 | }, 169 | { 170 | "cell_type": "markdown", 171 | "metadata": {}, 172 | "source": [ 173 | "### Funções auxiliares\n", 174 | "\n", 175 | "A métrica sugerida pelo Kaggle para avaliar os modelos é o RMSLE, este erro leva em conta a diferença entre o logaritmo das previsões e do alvo. É possível pensar neste erro como uma aproximação do erro percentual do modelo, mas com propriedades mais interessantes do ponto de vista matemático.\n", 176 | "\n", 177 | "Criei três funções para cálculo do erro. Nós vamos fazer transformações da variável y, então para facilitar nosso trabalho, decidi criar funções personalizadas para cada transformação, assim evita alguma confusão na hora de transformá-las para computar o erro numa função só. \n", 178 | "\n", 179 | "Para usarmos algumas funções do Scikit-learn que vão ajudar a manter nosso código mais limpo e mais eficiente, precisamos criar as funções de erro da maneira requerida pelo módulo. Neste caso, a função pecisa receber um modelo treinado, as features e o alvo. Ela computará as previsões e deverá retornar um número, que é o valor da métrica de erro.\n", 180 | "\n", 181 | "A última linha da célula cria um objeto da validação cruzada do scikit-learn. Ele vai cuidar da divisão dos dados para nós. Este assunto merece uma série de artigos, mas basicamente é uma maneira de dividir os dados em N partes, e usar repetidamente N-1 partes como dados de treino, e a parte restante como dados de teste. Por exemplo, se temos as partes [1, 2, 3, 4, 5], numa das iterações podemos treinar usando as partes [1, 2, 3, 4] e validar na parte 5.\n", 182 | "\n", 183 | "Apesar desta ser uma série temporal, o Kaggle decidiu tratar como dados aleatórios independentes, então por isso faremos esta validação cruzada simples." 184 | ] 185 | }, 186 | { 187 | "cell_type": "code", 188 | "execution_count": 3, 189 | "metadata": { 190 | "collapsed": true 191 | }, 192 | "outputs": [], 193 | "source": [ 194 | "from sklearn.metrics import mean_squared_error\n", 195 | "from sklearn.model_selection import cross_val_score, cross_val_predict, KFold\n", 196 | "from sklearn.ensemble import RandomForestRegressor\n", 197 | "\n", 198 | "def rmsle(estimator, X, y):\n", 199 | " p = estimator.predict(X)\n", 200 | " return np.sqrt(mean_squared_error(np.log1p(y), np.log1p(p)))\n", 201 | "\n", 202 | "def rmsle_log_y(estimator, X, y):\n", 203 | " p = estimator.predict(X)\n", 204 | " return np.sqrt(mean_squared_error(y, p))\n", 205 | "\n", 206 | "def rmsle_sqrt_y(estimator, X, y):\n", 207 | " p = estimator.predict(X)\n", 208 | " y = np.power(y, 2)\n", 209 | " p = np.power(p, 2)\n", 210 | " return np.sqrt(mean_squared_error(np.log1p(y), np.log1p(p)))\n", 211 | "\n", 212 | "kf = KFold(n_splits=5, shuffle=True, random_state=1)" 213 | ] 214 | }, 215 | { 216 | "cell_type": "markdown", 217 | "metadata": {}, 218 | "source": [ 219 | "### Feature set 1: variáveis \"numéricas\"\n", 220 | "\n", 221 | "O primeiro conjunto de features que teremos será uma seleção simples das variáveis originalmente numéricas dos dados. Para isto basta selecionar todas as colunas que possuem variáveis com números inteiros ou de ponto flutuante. Além disso, para substituir os valores nulos (o scikit-learn exige a substituição), decidi colocar o valor -1. Como vamos utilizar apenas modelos baseados em árvores com estes dados originais, não há necessidade de se preocupar muito com isso para nossos propósitos.\n", 222 | "\n", 223 | "Após armazenar estes dados na variável X1, vemos que são 36 colunas. Já nesta parte quero treinar um modelo de Random Forest dentro da validação cruzada, para sabermos como ele se sairia sozinho nos dados originais. Para isso utilizei a função cross_val_score. Ela é uma função que facilita bastante na hora de fazer a validação cruzada com scikit-learn. Basta colocar um modelo e os dados. Neste caso optei por especificar um esquema de validação e uma métrica de erro personalizada. \n", 224 | "\n", 225 | "Esta função retorna uma lista com os erros de cada iteração da validação cruzada, então fiz a média para sabermos o erro médio das partes." 226 | ] 227 | }, 228 | { 229 | "cell_type": "code", 230 | "execution_count": 4, 231 | "metadata": {}, 232 | "outputs": [ 233 | { 234 | "name": "stdout", 235 | "output_type": "stream", 236 | "text": [ 237 | "Dims (1460, 36)\n", 238 | "RMSLE: 0.14582352618\n" 239 | ] 240 | } 241 | ], 242 | "source": [ 243 | "X1 = X.select_dtypes(include=[np.number]).fillna(-1)\n", 244 | "print('Dims', X1.shape)\n", 245 | "model = RandomForestRegressor(n_estimators=1000, random_state=0)\n", 246 | "error = cross_val_score(model, X1, y, cv=kf, scoring=rmsle).mean()\n", 247 | "print('RMSLE:', error)" 248 | ] 249 | }, 250 | { 251 | "cell_type": "markdown", 252 | "metadata": {}, 253 | "source": [ 254 | "### Feature set 2: Ordinal Encoding Categóricas" 255 | ] 256 | }, 257 | { 258 | "cell_type": "markdown", 259 | "metadata": {}, 260 | "source": [ 261 | "Agora vamos criar um outro conjunto de features, desta vez adicionando as variáveis categóricas. Existem várias maneiras de codificar este tipo de variável para os modelos, uma delas é usando um formato ordinal. Isso simplesmente significa substituir cada valor original por números sequenciais. Em alguns modelos isso pode ser problemático, pois eles tentarão capturar alguma relação de ordem em valores que podem não ter. No nosso caso, com modelos baseados em árvores de decisão, este problema é quase inexistente.\n", 262 | "\n", 263 | "Após codificar desta maneira, rodamos novamente a validação cruzada, agora nestes novos dados." 264 | ] 265 | }, 266 | { 267 | "cell_type": "code", 268 | "execution_count": 5, 269 | "metadata": {}, 270 | "outputs": [ 271 | { 272 | "name": "stdout", 273 | "output_type": "stream", 274 | "text": [ 275 | "Dims (1460, 79)\n", 276 | "RMSLE: 0.143837364859\n" 277 | ] 278 | } 279 | ], 280 | "source": [ 281 | "from sklearn.preprocessing import LabelEncoder\n", 282 | "\n", 283 | "X2 = X.copy()\n", 284 | "for col in X2.columns:\n", 285 | " if X2[col].dtype == object:\n", 286 | " enc = LabelEncoder()\n", 287 | " X2[col] = enc.fit_transform(X[col].fillna('Missing'))\n", 288 | "\n", 289 | "print('Dims', X2.shape)\n", 290 | "X2.fillna(-1, inplace=True)\n", 291 | "model = RandomForestRegressor(n_estimators=1000, random_state=0)\n", 292 | "error = cross_val_score(model, X2, y, cv=kf, scoring=rmsle).mean()\n", 293 | "print('RMSLE:', error)" 294 | ] 295 | }, 296 | { 297 | "cell_type": "markdown", 298 | "metadata": {}, 299 | "source": [ 300 | "### Bônus: OneHot Encoding Categóricas" 301 | ] 302 | }, 303 | { 304 | "cell_type": "markdown", 305 | "metadata": {}, 306 | "source": [ 307 | "A maneira mais popular de codificar variáveis categóricas é o One Hot Encoding. Basicamente consiste em transformar cada valor da variável em uma coluna cujo novo valor será 1 caso a variável tenha aquele valor num determinado exemplo, ou 0 em caso negativo. Existem indicativos que árvores de decisão não processem tão bem este tipo de representação, mas em alguns casos práticos já vi isto funcionar melhor que o ordinal, então veja esta como mais uma ferramenta.\n", 308 | "\n", 309 | "Este método cria mais de 200 novas colunas, o que deixa o processo de treino mais devagar, então decidi deixar a linha da validação cruzada comentada. Caso queira ver o resultado, basta rodá-la sem o jogo da velha." 310 | ] 311 | }, 312 | { 313 | "cell_type": "code", 314 | "execution_count": 6, 315 | "metadata": {}, 316 | "outputs": [ 317 | { 318 | "name": "stdout", 319 | "output_type": "stream", 320 | "text": [ 321 | "Dims (1460, 288)\n" 322 | ] 323 | } 324 | ], 325 | "source": [ 326 | "#from sklearn.preprocessing import OneHotEncoder\n", 327 | "X3 = X.copy()\n", 328 | "cats = []\n", 329 | "for col in X3.columns:\n", 330 | " if X3[col].dtype == object:\n", 331 | " X3 = X3.join(pd.get_dummies(X3[col], prefix=col), how='left')\n", 332 | " X3.drop(col, axis=1, inplace=True)\n", 333 | " \n", 334 | "\n", 335 | "print('Dims', X3.shape)\n", 336 | "X3.fillna(-1, inplace=True)\n", 337 | "model = RandomForestRegressor(n_estimators=1000, random_state=0)\n", 338 | "#cross_val_score(model, X3, y, cv=kf, scoring=rmsle).mean()" 339 | ] 340 | }, 341 | { 342 | "cell_type": "markdown", 343 | "metadata": {}, 344 | "source": [ 345 | "## Transformações do Target\n", 346 | "\n", 347 | "Uma maneira interessante de criar diversidade, e às vezes até obter uma melhor performance, num caso de regressão, é transformar a variável que estamos tentando prever. Neste caso testaremos duas transformações: logaritmo e raiz quadrada." 348 | ] 349 | }, 350 | { 351 | "cell_type": "markdown", 352 | "metadata": {}, 353 | "source": [ 354 | "### Log\n", 355 | "\n", 356 | "É possível ver que tentar prever o logaritmo do preço nos dá um resultado melhor. Isto acontece não só pelo fato do modelo capturar padrões diferentes, mas também porque usamos uma métrica baseada na diferença de logaritmos." 357 | ] 358 | }, 359 | { 360 | "cell_type": "code", 361 | "execution_count": 7, 362 | "metadata": {}, 363 | "outputs": [ 364 | { 365 | "name": "stdout", 366 | "output_type": "stream", 367 | "text": [ 368 | "RF, X1, log-target RMSLE: 0.14518580749\n", 369 | "RF, X2, log-target RMSLE: 0.14207134495\n" 370 | ] 371 | } 372 | ], 373 | "source": [ 374 | "model = RandomForestRegressor(n_estimators=1000, random_state=0)\n", 375 | "error = cross_val_score(model, X1, np.log1p(y), cv=kf, scoring=rmsle_log_y).mean()\n", 376 | "print('RF, X1, log-target RMSLE:', error)\n", 377 | "\n", 378 | "model = RandomForestRegressor(n_estimators=1000, random_state=0)\n", 379 | "error = cross_val_score(model, X2, np.log1p(y), cv=kf, scoring=rmsle_log_y).mean()\n", 380 | "print('RF, X2, log-target RMSLE:', error)" 381 | ] 382 | }, 383 | { 384 | "cell_type": "markdown", 385 | "metadata": {}, 386 | "source": [ 387 | "### Raiz Quadrada\n", 388 | "\n", 389 | "Esta transformação também nos dá um resultado melhor do que usar a variável em seu estado original. Uma das sugestões da razão pela qual vemos este efeito é que estas transformações fazem com que a variável y tenha uma distribuição mais próxima da normal, o que facilita o trabalho do modelo." 390 | ] 391 | }, 392 | { 393 | "cell_type": "code", 394 | "execution_count": 8, 395 | "metadata": {}, 396 | "outputs": [ 397 | { 398 | "name": "stdout", 399 | "output_type": "stream", 400 | "text": [ 401 | "RF, X1, sqrt-target RMSLE: 0.145652934484\n", 402 | "RF, X2, sqrt-target RMSLE: 0.143004600132\n" 403 | ] 404 | } 405 | ], 406 | "source": [ 407 | "model = RandomForestRegressor(n_estimators=1000, random_state=0)\n", 408 | "error = cross_val_score(model, X1, np.sqrt(y), cv=kf, scoring=rmsle_sqrt_y).mean()\n", 409 | "print('RF, X1, sqrt-target RMSLE:', error)\n", 410 | "\n", 411 | "model = RandomForestRegressor(n_estimators=1000, random_state=0)\n", 412 | "error = cross_val_score(model, X2, np.sqrt(y), cv=kf, scoring=rmsle_sqrt_y).mean()\n", 413 | "print('RF, X2, sqrt-target RMSLE:', error)" 414 | ] 415 | }, 416 | { 417 | "cell_type": "markdown", 418 | "metadata": {}, 419 | "source": [ 420 | "## Gerando modelos com modelos/algoritmos diferentes\n", 421 | "\n", 422 | "Outra maneira de gerar diversidade para o ensemble é gerar modelos diferentes. Neste caso vou usar meu modelo preferido, o GBM. Este também é baseado em árvores de decisão, mas basicamente treina cada árvore sequencialmente focando nos erros cometidos pelas anteriores.\n", 423 | "\n", 424 | "Nas células abaixo é possível ver a performance deste modelo nos feature sets e transformações que usamos com a Random Forest. Vemos que ele traz uma melhora significativa, capturando melhor os padrões da relação entre as variáveis e o preço de venda dos imóveis." 425 | ] 426 | }, 427 | { 428 | "cell_type": "code", 429 | "execution_count": 9, 430 | "metadata": {}, 431 | "outputs": [ 432 | { 433 | "name": "stdout", 434 | "output_type": "stream", 435 | "text": [ 436 | "GBM, X1, log-target RMSLE: 0.133492454914\n", 437 | "GBM, X2, log-target RMSLE: 0.129806890482\n" 438 | ] 439 | } 440 | ], 441 | "source": [ 442 | "from sklearn.ensemble import GradientBoostingRegressor\n", 443 | " \n", 444 | "model = GradientBoostingRegressor(random_state=0)\n", 445 | "error = cross_val_score(model, X1, np.log1p(y), cv=kf, scoring=rmsle_log_y).mean()\n", 446 | "print('GBM, X1, log-target RMSLE:', error)\n", 447 | "\n", 448 | "model = GradientBoostingRegressor(random_state=0)\n", 449 | "error = cross_val_score(model, X2, np.log1p(y), cv=kf, scoring=rmsle_log_y).mean()\n", 450 | "print('GBM, X2, log-target RMSLE:', error)" 451 | ] 452 | }, 453 | { 454 | "cell_type": "code", 455 | "execution_count": 10, 456 | "metadata": {}, 457 | "outputs": [ 458 | { 459 | "name": "stdout", 460 | "output_type": "stream", 461 | "text": [ 462 | "GBM, X1, sqrt-target RMSLE: 0.134258972813\n", 463 | "GBM, X2, sqrt-target RMSLE: 0.130919235682\n" 464 | ] 465 | } 466 | ], 467 | "source": [ 468 | "from sklearn.ensemble import GradientBoostingRegressor\n", 469 | " \n", 470 | "model = GradientBoostingRegressor(random_state=0)\n", 471 | "error = cross_val_score(model, X1, np.sqrt(y), cv=kf, scoring=rmsle_sqrt_y).mean()\n", 472 | "print('GBM, X1, sqrt-target RMSLE:', error)\n", 473 | "\n", 474 | "model = GradientBoostingRegressor(random_state=0)\n", 475 | "error = cross_val_score(model, X2, np.sqrt(y), cv=kf, scoring=rmsle_sqrt_y).mean()\n", 476 | "print('GBM, X2, sqrt-target RMSLE:', error)" 477 | ] 478 | }, 479 | { 480 | "cell_type": "markdown", 481 | "metadata": {}, 482 | "source": [ 483 | "### Ajustando hiperparâmetros" 484 | ] 485 | }, 486 | { 487 | "cell_type": "markdown", 488 | "metadata": {}, 489 | "source": [ 490 | "Para simplificar o exemplo e focar na parte do ensemble, não vou fazer o ajuste dos hiperparâmetros do modelo. Hiperparâmetros são os atributos do modelo (como a profundidade das árvores de decisão) que precisam ser ajustados usando dados separados de validação ou um ciclo de validação cruzada.\n", 491 | "\n", 492 | "É bom saber que nem sempre os melhores modelos formam os melhores ensembles. É importante ter modelos poderosos ao fazer stacking, mas também devemos lembrar que é importante ter diversidade. Às vezes alguns modelos que possuem um erro mais alto podem capturar padrões diferentes dos melhores modelos e por isso contribuem com o ensemble.\n", 493 | "\n", 494 | "Caso você decida fazer o ajuste dos hiperparâmetros, é importante colocá-lo dentro do ciclo de validação cruzada que veremos no próximo passo." 495 | ] 496 | }, 497 | { 498 | "cell_type": "markdown", 499 | "metadata": {}, 500 | "source": [ 501 | "## Stacking\n", 502 | "\n", 503 | "Tudo o que fizemos acima é para que possamos criar o nosso ensemble. Esta é a hora juntarmos os métodos usados para melhorarmos o poder preditivo dos nossos modelos.\n", 504 | "\n", 505 | "O Stacking é uma maneira de fazer o ensemble na qual usamos modelos para fazer previsões, e depois usamos estas previsões como features em novos modelos, no que pode ser chamado de \"segundo nível\". Você pode fazer este processo várias vezes, mas a cada nível o retorno em performance com relação à computação necessária é menor.\n", 506 | "\n", 507 | "Nesta fase precisamos de dois ciclos de validação cruzada: externo e interno. No interno, nós treinaremos os modelos nos dados originais e faremos as previsões. No externo, treinaremos o modelo usando as previsões do primeiro passo como features.\n", 508 | "\n", 509 | "A cada passo da validação cruzada interna, vamos salvar as previsões para a parte dos dados que for usada como validação. Desta maneira teremos previsões para todos os exemplos de nossos dados de treino. Além disso treinaremos um modelo nos dados originais de treino para podermos fazer previsões para os dados de teste.\n", 510 | "\n", 511 | "No ciclo externo treinaremos um modelo nas previsões geradas pelo ciclo interno, e as features dos dados de validação serão as previsões dos modelos de primeiro nível nos dados de teste.\n", 512 | "\n", 513 | "No nosso caso específico, criaremos previsões usando todas as combinações de modelos (RF e GBM), transformações do target (log e raiz quadrada) e feature sets (X1 e X2). Não usaremos o X3 porque levaria muito tempo para treinar, e para os fins de demonstração do método estes dois serão o bastante.\n", 514 | "\n", 515 | "No fim teremos previsões de 8 modelos no primeiro nível. No segundo nível usei uma regressão linear regularizada (Ridge). Tendo estas previsões podemos computar o erro nos dados fora das nossas amostras de treino e validação internas, o que nos dará uma estimativa confiável do erro do ensemble." 516 | ] 517 | }, 518 | { 519 | "cell_type": "code", 520 | "execution_count": 13, 521 | "metadata": {}, 522 | "outputs": [ 523 | { 524 | "name": "stdout", 525 | "output_type": "stream", 526 | "text": [ 527 | "RMSLE Fold 0 - RMSLE 0.1248\n", 528 | "RMSLE Fold 1 - RMSLE 0.1449\n", 529 | "RMSLE Fold 2 - RMSLE 0.1257\n", 530 | "RMSLE Fold 3 - RMSLE 0.1409\n", 531 | "RMSLE Fold 4 - RMSLE 0.1087\n", 532 | "RMSLE CV5 0.1290\n" 533 | ] 534 | } 535 | ], 536 | "source": [ 537 | "from itertools import product\n", 538 | "from sklearn.linear_model import Ridge\n", 539 | "\n", 540 | "kf_out = KFold(n_splits=5, shuffle=True, random_state=1)\n", 541 | "kf_in = KFold(n_splits=5, shuffle=True, random_state=2)\n", 542 | "\n", 543 | "cv_mean = []\n", 544 | "for fold, (tr, ts) in enumerate(kf_out.split(X, y)):\n", 545 | " X1_train, X1_test = X1.iloc[tr], X1.iloc[ts]\n", 546 | " X2_train, X2_test = X2.iloc[tr], X2.iloc[ts]\n", 547 | " y_train, y_test = y.iloc[tr], y.iloc[ts]\n", 548 | " \n", 549 | " modelos = [GradientBoostingRegressor(random_state=0), RandomForestRegressor(random_state=0)]\n", 550 | " targets = [np.log1p, np.sqrt]\n", 551 | " feature_sets = [(X1_train, X1_test), (X2_train, X2_test)]\n", 552 | " \n", 553 | " \n", 554 | " predictions_cv = []\n", 555 | " predictions_test = []\n", 556 | " for model, target, feature_set in product(modelos, targets, feature_sets):\n", 557 | " predictions_cv.append(cross_val_predict(model, feature_set[0], target(y_train), cv=kf_in).reshape(-1,1))\n", 558 | " model.fit(feature_set[0], target(y_train))\n", 559 | " ptest = model.predict(feature_set[1])\n", 560 | " predictions_test.append(ptest.reshape(-1,1))\n", 561 | " \n", 562 | " predictions_cv = np.concatenate(predictions_cv, axis=1)\n", 563 | " predictions_test = np.concatenate(predictions_test, axis=1)\n", 564 | " \n", 565 | " stacker = Ridge()\n", 566 | " stacker.fit(predictions_cv, np.log1p(y_train))\n", 567 | " error = rmsle_log_y(stacker, predictions_test, np.log1p(y_test))\n", 568 | " cv_mean.append(error)\n", 569 | " print('RMSLE Fold %d - RMSLE %.4f' % (fold, error))\n", 570 | " \n", 571 | "print('RMSLE CV5 %.4f' % np.mean(cv_mean))\n", 572 | " \n" 573 | ] 574 | }, 575 | { 576 | "cell_type": "markdown", 577 | "metadata": { 578 | "collapsed": true 579 | }, 580 | "source": [ 581 | "Como podemos ver, nosso melhor modelo de primeiro nível é o GBM treinado com a transformação log nos dados X2, que atinge o erro de 0,1298. Nosso ensemble atinge o valor de 0,1290. Uma melhora de 0,62%. \n", 582 | "\n", 583 | "O objetivo deste artigo era demonstrar o método, sem se preocupar muito com a performance no fim. Um ensemble feito visando melhora de performance pode apresentar um resultado mais significativo.\n", 584 | "\n", 585 | "Em alguns casos, como em fundos de investimento ou aplicações de saúde, uma melhora pequena pode ter um resultado bastante significativo no mundo real, que justifique a criação de uma solução mais completa usando stacking.\n", 586 | "\n", 587 | "Como sempre na aplicação de Machine Learning, nada é garantia de sucesso, mas este método é um dos mais consistentes em oferecer uma melhora." 588 | ] 589 | } 590 | ], 591 | "metadata": { 592 | "anaconda-cloud": {}, 593 | "kernelspec": { 594 | "display_name": "Python [Root]", 595 | "language": "python", 596 | "name": "Python [Root]" 597 | }, 598 | "language_info": { 599 | "codemirror_mode": { 600 | "name": "ipython", 601 | "version": 3 602 | }, 603 | "file_extension": ".py", 604 | "mimetype": "text/x-python", 605 | "name": "python", 606 | "nbconvert_exporter": "python", 607 | "pygments_lexer": "ipython3", 608 | "version": "3.5.2" 609 | } 610 | }, 611 | "nbformat": 4, 612 | "nbformat_minor": 1 613 | } 614 | --------------------------------------------------------------------------------