====== Laboratorium 8 - regresja logistyczna ====== Celem jest budowa modelu regresji logistycznej pozwalającej przewidywać, czy dany student zdał egzamin z języka C++ w pierwszym terminie. Implementujemy oprogramowanie w języku Java. === Dla przypomnienia === * Regresja logistyczna jest metodą klasyfikacji binarnej. * Posługuje się pojęciem //szansy// $o=\frac{p}{1-p}$ * Modeluje powiązanie prawdopodobieństwa etykiety 1 z modelem liniowym jako $logit(p) = ln(\frac{p}{1-p})=w^T x$ * Po przekształceniach: prawdopodobieństwo etykiety 1 opisane jest wzorem $p=\frac{1}{1+exp(-w^T x)}$ Patrz: [[https://home.agh.edu.pl/~pszwed/wiki/lib/exe/fetch.php?media=med:med-w04.pdf|wykład 4]] === Zbiory danych === * {{ :ed:egzamin-cpp.csv |egzamin-cpp.csv}} - poddane anonimizacji wyniki zaliczeń/egzaminu z C i C++ w 2016 roku * {{ :ed:grid.csv |grid.csv}} - kombinacje ocen **egzamin-cpp.csv** ImieNazwisko;OcenaC;DataC;OcenaCpp;Egzamin Dqhoil Dhxpluj;3.5;2016-01-14;4;3 Bhnhgpxj Lwjmq;4.5;2016-01-14;4;3 Wkgjnerme Djfbw;4;2016-01-20;3;2 Sredvmuwt Tcimknl;4.5;2016-01-20;4.5;3.5 Tiowe Bqoilnqbrx;4;2016-01-14;4.5;3 Bvaysqv Wuyih;3.5;2016-01-14;5;3 Jjoaxp Ktapcy;5;2016-01-20;4;3.5 **grid.csv** ImieNazwisko,OcenaC,DataC,OcenaCpp 'Xxxxx Yyyyyy',3,2016-01-17,2 'Xxxxx Yyyyyy',3,2016-01-17,3 'Xxxxx Yyyyyy',3,2016-01-17,3.5 'Xxxxx Yyyyyy',3,2016-01-17,4 'Xxxxx Yyyyyy',3,2016-01-17,4.5 'Xxxxx Yyyyyy',3,2016-01-17,5 'Xxxxx Yyyyyy',3.5,2016-01-17,2 'Xxxxx Yyyyyy',3.5,2016-01-17,3 'Xxxxx Yyyyyy',3.5,2016-01-17,3.5 'Xxxxx Yyyyyy',3.5,2016-01-17,4 'Xxxxx Yyyyyy',3.5,2016-01-17,4.5 'Xxxxx Yyyyyy',3.5,2016-01-17,5 'Xxxxx Yyyyyy',4,2016-01-17,2 'Xxxxx Yyyyyy',4,2016-01-17,3 'Xxxxx Yyyyyy',4,2016-01-17,3.5 'Xxxxx Yyyyyy',4,2016-01-17,4 'Xxxxx Yyyyyy',4,2016-01-17,4.5 'Xxxxx Yyyyyy',4,2016-01-17,5 'Xxxxx Yyyyyy',4.5,2016-01-17,2 'Xxxxx Yyyyyy',4.5,2016-01-17,3 'Xxxxx Yyyyyy',4.5,2016-01-17,3.5 'Xxxxx Yyyyyy',4.5,2016-01-17,4 'Xxxxx Yyyyyy',4.5,2016-01-17,4.5 'Xxxxx Yyyyyy',4.5,2016-01-17,5 'Xxxxx Yyyyyy',5,2016-01-17,2 'Xxxxx Yyyyyy',5,2016-01-17,3 'Xxxxx Yyyyyy',5,2016-01-17,3.5 'Xxxxx Yyyyyy',5,2016-01-17,4 'Xxxxx Yyyyyy',5,2016-01-17,4.5 'Xxxxx Yyyyyy',5,2016-01-17,5 ===== 1. Ładowanie danych i przetwarzanie wstępne ===== **1.** Utwórz sesję Sparka i załaduj zbiór danych. Wyświetl zawartość i schemat SparkSession spark = SparkSession.builder() .appName("LogisticRegressionOnExam") .master("local") .getOrCreate(); ... root |-- ImieNazwisko: string (nullable = true) |-- OcenaC: double (nullable = true) |-- DataC: date (nullable = true) |-- OcenaCpp: double (nullable = true) |-- Egzamin: double (nullable = true) **2.** Regresja logistyczna wymaga, aby atrybutów wejściowe były typu numerycznego. Jest też metodą klasyfikacji binarnej (etykiety powinny mieć wartości 0 i 1) * przekonwertuj datę za pomocą funkcji ''unix_timestamp'' - nadaj nowej kolumnie nazwę ''timestamp'' * Dodaj kolumnę ''Wynik'' będącą wynikiem testu, czy ''Egzamin>=3.0'' - użyj funkcji SQL IF() +-----------------+------+----------+--------+-------+----------+-----+ | ImieNazwisko|OcenaC| DataC|OcenaCpp|Egzamin| timestamp|Wynik| +-----------------+------+----------+--------+-------+----------+-----+ | Dqhoil Dhxpluj| 3.5|2016-01-14| 4.0| 3.0|1452726000| 1| | Bhnhgpxj Lwjmq| 4.5|2016-01-14| 4.0| 3.0|1452726000| 1| | Wkgjnerme Djfbw| 4.0|2016-01-20| 3.0| 2.0|1453244400| 0| |Sredvmuwt Tcimknl| 4.5|2016-01-20| 4.5| 3.5|1453244400| 1| | Tiowe Bqoilnqbrx| 4.0|2016-01-14| 4.5| 3.0|1452726000| 1| | Bvaysqv Wuyih| 3.5|2016-01-14| 5.0| 3.0|1452726000| 1| | Jjoaxp Ktapcy| 5.0|2016-01-20| 4.0| 3.5|1453244400| 1| | Mkengbtw Aainhh| 3.5|2016-01-20| 3.0| 2.0|1453244400| 0| | Fbffjb Muupwshu| 4.0|2016-01-14| 5.0| 4.0|1452726000| 1| | Yahwfyp Bvnlsig| 5.0|2016-01-14| 4.5| 4.0|1452726000| 1| +-----------------+------+----------+--------+-------+----------+-----+ ===== 2. LogisticRegressionAnalysis - analiza działania algorytmu ===== ==== 2.1 Budowa modelu i interpretacja współczynników ==== **1.** Skonfiguruj algorytm i zbuduj model LogisticRegression lr = new LogisticRegression() .setMaxIter(100) .setRegParam(0.1) .setElasticNetParam(0) .setFeaturesCol("features") .setLabelCol("Wynik"); LogisticRegressionModel lrModel = lr.fit(df); **2.** Napisz kod, który drukuje równanie regresji logistycznej Oczekiwany wynik: logit(zdal) = 0.719097*OcenaC + -0.000000*timestamp + 0.993461*OcenaCPP + 118.340611 **3.** Zinterpretuj współczynniki równania regresji (napisz kod lub zamieść wykonane obliczenia). Pamiętaj, że timestamp jest wyrażony w sekundach. Poniższe wyniki były wygenerowane programowo. W praktyce wynik nie zależy od daty... Wzrost OcenaC o 1 zwiększa logit o 0.719097, a szanse zdania razy 2.052578 czyli o 105.257821% Wzrost DataC o 1 dzień zwiększa logit o -0.000000,a szanse zdania razy 0.992648 czyli o -0.735167% Wzrost OcenaCPP o 1 zwiększa logit o 0.719097,a szanse zdania razy 2.700564 czyli o 170.056381% ==== 2.2 Predykcja i jej wyniki ==== **1.** Wywołaj funkcję predykcji i wyświetl dane... Dataset df_with_predictions=lrModel.transform(df_trans); var df_predictions = df_with_predictions .select("features","rawPrediction","probability","prediction"); **2.** Napisz funkcję, która wyświetli informacje dotyczące predykcji private static void analyzePredictions(Dataset dfPredictions,LogisticRegressionModel lrModel) { dfPredictions.foreach(new ForeachFunction() { ... }); } Wewnątrz: * oblicz wartośc ''logit'' jako iloczyn skalarny współczynników i cech powiększony o ''lrModel.intercept()'' * Oblicz prawdopodobieństwo P(0) i P(1) z odpowiedniego wzoru - wykorzystując wartośc logit * Wyświetl i porównaj wartości ''rawPrediction'' i ''prawdopodobieństwa'' * Wyświetl prawdopodobieństwo wybranej przez klasyfikator etykiety - czyli większe z prawdopodobieństw * Funkcja ''row.getAs(String)'' zwraca element w danej kolumnie. Użyj też funkcji ''Vector.argmax()'' ==== 2.3 Dodaj do zbioru prawdopodobieństwo wybranej etykiety ==== Zadaniem jest dodanie do zbioru danych kolumny ''prob'' zawierającej wartość prawdopodobieństwa przypisanego etykiecie. Musimy wyodrębnić je z atrybutu ''probability''. W tym celu użyjemy mechanizmu User Defined Function (UDF). UDF to funkcja użytkownika rozszerzająca interfejs funkcjonalny Sparka. Funkcja musi zostać zarejestrowana w sesji. **1.** Dodaj klasę zagnieżdżoną: static class MaxVectorElement implements UDF1 { @Override public Double call(Vector vector) throws Exception { return // największy element wektora - czyli o indeksie vector.argmax() } } **2.** Zarejestruj funkcję (zazwyczaj bezpośrednio po utworzeniu sesji) spark.udf().register( "max_vector_element",new MaxVectorElement(),DataTypes.DoubleType); Alternatywnym rozwiązaniem może być rejestracja funkcji jako wyrażenia lambda, bez deklarowania klasy UDF1 mve = v-> ???; spark.udf().register( "max_vector_element_alt",mve,DataTypes.DoubleType); **3.** Przekonwertuj zbiór z wynikami predykcji. Usuń kolumny typu ''features'' i ''rawPredictions'' Dodaj kolumnę ''prob'' z rezultatami wywołania UDF .withColumn("prob",callUDF("max_vector_element",col("probability")))... Oczekiwany wynik: +-----------------+------+----------+--------+-------+----------+-----+----------+------------------+ | ImieNazwisko|OcenaC| DataC|OcenaCpp|Egzamin| timestamp|Wynik|prediction| prob| +-----------------+------+----------+--------+-------+----------+-----+----------+------------------+ | Dqhoil Dhxpluj| 3.5|2016-01-14| 4.0| 3.0|1452726000| 1| 1.0|0.6822140478017863| | Bhnhgpxj Lwjmq| 4.5|2016-01-14| 4.0| 3.0|1452726000| 1| 1.0| 0.815034643789244| | Wkgjnerme Djfbw| 4.0|2016-01-20| 3.0| 2.0|1453244400| 0| 1.0|0.5214319094648453| |Sredvmuwt Tcimknl| 4.5|2016-01-20| 4.5| 3.5|1453244400| 1| 1.0|0.8738590749460415| | Tiowe Bqoilnqbrx| 4.0|2016-01-14| 4.5| 3.0|1452726000| 1| 1.0| 0.834828781680339| +-----------------+------+----------+--------+-------+----------+-----+----------+------------------+ ==== 2.4 Zapisz zbiór z wynikami ==== df_predictions = df_predictions.repartition(1); df_predictions.write() .format("csv") .option("header", true) .option("delimiter",",") .mode(SaveMode.Overwrite) .save("????/egzamin-with-classification.csv"); Instrukcja ''df_predictions = df_predictions.repartition(1);'' jest opcjonalna. Jaka będzie postać wyjścia, kiedy zmienimy argument ''repartition'' - np. ustawimy 5. ===== 3. LogisticRegressionScores - ocena wyników ===== Napisz funkcję trainAndTest, która: * dokona podziału na zbiór treningowy i testowy * skonfiguruje algorytm * i przeprowadzi uczenie na zbiorze treningowym static LogisticRegressionModel trainAndTest(Dataset df){ int splitSeed = 123; Dataset[] splits = df.randomSplit(new double[]{0.7, 0.3},splitSeed); Dataset df_train = splits[0]; Dataset df_test = splits[1]; LogisticRegression lr = new LogisticRegression() .setMaxIter(20) .setRegParam(0.1) .setFeaturesCol("features") .setLabelCol("Wynik"); LogisticRegressionModel lrModel = lr.fit(df_train); ... return lrModel; } ==== 3.1 Analiza informacji zebranych w training summary ==== BinaryLogisticRegressionTrainingSummary trainingSummary = lrModel.binarySummary(); **1.** Pobierz historię objective history i wyświetl jej wykres. Napisz odpowiednią funkcję do wyświetlania wykresu ''plotObjectiveHistory()'' double[] objectiveHistory = trainingSummary.objectiveHistory(); plotObjectiveHistory(objectiveHistory); Oczekiwany wynik: {{ :ed:logreg-objective-history.png?direct&400 |}} **2.** Pobierz informacje o krzywej ROC. Możesz o niej przeczytać tu: [[https://home.agh.edu.pl/~pszwed/wiki/lib/exe/fetch.php?media=med:med-w04.pdf|Wykład 4, slajdy 24-27]] Dataset roc = trainingSummary.roc(); roc.show(); +--------------------+-------------------+ | FPR| TPR| +--------------------+-------------------+ | 0.0| 0.0| | 0.0|0.07692307692307693| | 0.0|0.09615384615384616| | 0.0|0.15384615384615385| | 0.0|0.21153846153846154| | 0.0|0.23076923076923078| | 0.0| 0.3076923076923077| | 0.0|0.34615384615384615| | 0.0|0.38461538461538464| | 0.0| 0.4230769230769231| | 0.0| 0.4807692307692308| | 0.0| 0.5| | 0.0| 0.5192307692307693| | 0.0| 0.5576923076923077| | 0.0| 0.5961538461538461| |0.047619047619047616| 0.5961538461538461| |0.047619047619047616| 0.6153846153846154| |0.047619047619047616| 0.6730769230769231| | 0.09523809523809523| 0.6730769230769231| | 0.14285714285714285| 0.6923076923076923| +--------------------+-------------------+ **3.** Wyświetl wykres ROC. Napisz odpowiednią funkcję static void plotROC(Dataset roc) Oczekiwany wynik: {{ :ed:logrec-roc-curve.png?direct&400 |}} **3.** Wyświetl miary: * Accuracy * FPR * TPR * Precision * Recall * F-measure ==== 3.2 Dobór progu prawdopodobieństwa ==== Krzywą ROC można wykorzystać do doboru progu prawdopodobieństwa. Patrz [[https://home.agh.edu.pl/~pszwed/wiki/lib/exe/fetch.php?media=med:med-w04.pdf|Wykład 4 slajd 27]] **1.** Dobierzemy próg według miary //F-measure//. To będzie gdzieś tu: Dataset df_fmeasures = trainingSummary.fMeasureByThreshold(); df_fmeasures.offset(35).show(); +-------------------+------------------+ | threshold| F-Measure| +-------------------+------------------+ | 0.4627032508959811|0.8869565217391304| | 0.4382847817892507|0.8793103448275861| | 0.4034706528697256| 0.888888888888889| | 0.3858997834933381|0.8813559322033898| | 0.3461380134069699| 0.859504132231405| | 0.3143853597224281|0.8524590163934427| |0.19135299955580787|0.8455284552845529| | 0.1472470120383692|0.8387096774193548| |0.13010893832947723| 0.832| +-------------------+------------------+ **2.** Wyznacz programowo najlepszy próg. * Wpierw wyznacz maksymalną wartość F-measure (przykład kodu poniżej) * A następnie odpowiadającą jej wartość progu (używając ''where ... equalTo ... select...'') * W powyższej tabelce możesz sprawdzić, czy znalezione zostały właściwe wartości (oczywiście one zależą od sposobu podziału na zbiór treningowy i testowy, a więc ziarna ''seed'') double maxFMeasure = df_fmeasures.select(functions.max("F-Measure")).head().getDouble(0); **3.** Ustaw próg klasyfikatora lrModel.setThreshold(bestThreshold); ==== 3.3 Ewaluacja na zbiorze testowym ==== **1.** Wywołaj funkcję predykcji i skonfiguruj ewaluator Dataset predictions = lrModel.transform(df_test); MulticlassClassificationEvaluator eval = new MulticlassClassificationEvaluator() .setLabelCol("Wynik") .setPredictionCol("prediction"); **2.** Wyznacz: * accuracy * weightedPrecision * weightedRecall * f1 Oczekiwane są wartości rzędu 0.82-0.83 Nazwy dostępnych metryk: (f1|accuracy|weightedPrecision|weightedRecall|weightedTruePositiveRate| weightedFalsePositiveRate|weightedFMeasure|truePositiveRateByLabel| falsePositiveRateByLabel|precisionByLabel|recallByLabel|fMeasureByLabel| logLoss|hammingLoss)' ===== 4. LogisticRegressionGrid - tworzenie tabeli ocen ===== Celem jest utworzenie tabeli ocen postaci, jak poniżej +--------------+------+----------+--------+--------+ | ImieNazwisko|OcenaC| DataC|OcenaCpp| Wynik| +--------------+------+----------+--------+--------+ |'Xxxxx Yyyyyy'| 3.0|2016-01-17| 2.0|Nie zdał| |'Xxxxx Yyyyyy'| 3.0|2016-01-17| 3.0|Nie zdał| |'Xxxxx Yyyyyy'| 3.0|2016-01-17| 3.5| Zdał| |'Xxxxx Yyyyyy'| 3.0|2016-01-17| 4.0| Zdał| |'Xxxxx Yyyyyy'| 3.0|2016-01-17| 4.5| Zdał| |'Xxxxx Yyyyyy'| 3.0|2016-01-17| 5.0| Zdał| |'Xxxxx Yyyyyy'| 3.5|2016-01-17| 2.0|Nie zdał| |'Xxxxx Yyyyyy'| 3.5|2016-01-17| 3.0|Nie zdał| |'Xxxxx Yyyyyy'| 3.5|2016-01-17| 3.5| Zdał| |'Xxxxx Yyyyyy'| 3.5|2016-01-17| 4.0| Zdał| ... **Uwaga:** wynik może się nieco różnić w zależności od konfiguracji, np. progu prawdopodobieństwa **1.** Wytrenuj klasyfikator na zbiorze ''egzamin-cpp.csv''. Wykorzystuj kod z poprzedniej części, ustaw próg prawdopodobieństwa. **2.** Napisz funkcje void addClassificationToGrid(SparkSession spark, LogisticRegressionModel lrModel) która: * Wczyta zbiór danych ''grid.csv'' * Przetworzy daty, tak aby stały się wartościami numerycznymi * Skonfiguruje VectorAssembler * Wywoła funkcję predykcji zmiennej ''lrMpdel'' * Usunie nadmiarowe kolumny * Za pomocą funkcji ''IF()'' SQL lub zarejestrowanej funkcji użytkownika UDF dokona konwersji etykiet //0->Nie zdał// oraz //1->Zdał// * Wyświetli wynik * Zapisze w pliku ''grid-with-classification.csv''