Статьи

Прогнозирование рака молочной железы с помощью Apache Spark Машинное обучение Логистическая регрессия

В этом посте я помогу вам начать использовать логистическую регрессию Apache Spark spark.ml для прогнозирования злокачественных новообразований.
Цель библиотеки Spark spark.ml — предоставить набор API поверх DataFrames, которые помогают пользователям создавать и настраивать рабочие процессы или конвейеры машинного обучения. Использование spark.ml с DataFrames повышает производительность за счет интеллектуальной оптимизации.

классификация

Классификация — это семейство контролируемых алгоритмов машинного обучения, которые определяют, к какой категории относится элемент (например, является ли наблюдение злокачественной ткани злокачественным или нет), на основе помеченных примеров известных предметов (например, наблюдения, известные как злокачественные или нет). ). Классификация берет набор данных с известными метками и предопределенными функциями и изучает, как маркировать новые записи на основе этой информации. Особенности — это «если вопросы», которые вы задаете. Этикетка является ответом на эти вопросы. В приведенном ниже примере, если он ходит, плавает и крякает, как утка, тогда на этикетке будет написано «утка».

Maching-обучения-утки

Давайте рассмотрим пример наблюдения ткани рака:

  • Что мы пытаемся предсказать?
    • Является ли наблюдение за образцом злокачественным или нет.
    • Это Метка: злокачественная или нет.
  • Какие «если вопросы» или свойства, которые вы можете использовать для прогнозирования?
    • Характеристики образца ткани: толщина скопления, однородность размера клеток, однородность формы клеток, предельная адгезия, размер единичных эпителиальных клеток, голые ядра, мягкий хроматин, нормальные ядра, митозы.
    • Это особенности. Чтобы построить модель классификатора, вы извлекаете интересующие элементы, которые наиболее способствуют классификации.

Логистическая регрессия

Логистическая регрессия является популярным методом прогнозирования двоичного ответа. Это особый случай обобщенных линейных моделей, который предсказывает вероятность результата. Логистическая регрессия измеряет отношения между Y «Метка» и X «Особенности» путем оценки вероятностей с использованием логистической функции. Модель прогнозирует вероятность, которая используется для прогнозирования класса метки.

cancerlogistic

Проанализируйте наблюдения рака с помощью сценария машинного обучения

Наши данные взяты из Висконсинского диагностического набора данных по раку молочной железы (WDBC), который классифицирует случаи опухоли молочной железы как доброкачественные или злокачественные на основе 9 признаков для прогнозирования диагноза. Для каждого наблюдения рака у нас есть следующая информация:

01
02
03
04
05
06
07
08
09
10
11
1. Sample code number: id number
2. Clump Thickness: 1 - 10
3. Uniformity of Cell Size: 1 - 10
4. Uniformity of Cell Shape: 1 - 10
5. Marginal Adhesion: 1 - 10
6. Single Epithelial Cell Size: 1 - 10
7. Bare Nuclei: 1 - 10
8. Bland Chromatin: 1 - 10
9. Normal Nucleoli: 1 - 10
10. Mitoses: 1 - 10
11. Class: (2 for benign, 4 for malignant)

CSV-файл «Наблюдение за раком» имеет следующий формат:

1
2
3
1000025,5,1,1,1,2,1,3,1,1,2
1002945,5,4,4,5,7,10,3,2,1,2
1015425,3,1,1,1,2,2,3,1,1,2

В этом сценарии мы создадим модель логистической регрессии, чтобы предсказать метку / классификацию злокачественных или не основанную на следующих особенностях:

  • Метка → злокачественная или доброкачественная (1 или 0)
  • Особенности → {Толщина комков, однородность размера клеток, однородность формы клеток, предельная адгезия, размер единичных эпителиальных клеток, оголенные ядра, мягкий хроматин, нормальные ядра, митозы}

Spark ML предоставляет унифицированный набор высокоуровневых API, созданных поверх DataFrames. Основные понятия в Spark ML:

  • DataFrame: ML API использует DataFrames из Spark SQL в качестве набора данных ML.
  • Transformer: Transformer — это алгоритм, который преобразует один DataFrame в другой DataFrame. Например, превращение DataFrame с функциями в DataFrame с предсказаниями.
  • Оценщик: Оценщик — это алгоритм, который может быть помещен в DataFrame для создания Transformer. Например, обучение / настройка на DataFrame и создание модели.
  • Конвейер: Конвейер объединяет несколько Трансформаторов и Оценщиков, чтобы определить рабочий процесс ML.
  • ParamMaps: параметры для выбора, иногда называемые «сеткой параметров» для поиска.
  • Оценщик: Метрика, позволяющая измерить, насколько хорошо подобранная Модель справляется с данными, проведенными в течение длительного времени.
  • CrossValidator: Определяет лучший ParamMap и переоснащает Оценщик, используя лучший ParamMap и весь набор данных.

В этом примере будет использоваться рабочий процесс Spark ML, показанный ниже: bcmlprocess

Программного обеспечения

Этот учебник будет работать на Spark 1.6.1

  • Вы можете скачать код и данные для запуска этих примеров здесь: https://github.com/caroljmcdonald/spark-ml-lr-cancer
  • Примеры в этом посте можно запустить в оболочке Spark после запуска с помощью команды spark-shell.
  • Вы также можете запустить код как отдельное приложение, как описано в руководстве по началу работы с Spark в MapR Sandbox .

Войдите в MapR Sandbox, как описано в разделе Начало работы с Spark в MapR Sandbox , используя идентификатор пользователя user01, пароль mapr. Скопируйте файл примера данных в домашнюю директорию песочницы / user / user01 с помощью scp. (Обратите внимание, что вам может потребоваться обновить версию Spark на вашей песочнице) Запустите оболочку Spark с помощью:

1
$spark-shell --master local[1]

Загрузка и анализ данных из файла CSV

Сначала мы импортируем пакеты машинного обучения.
(В полях кода комментарии отображаются зеленым, а вывод — синим)

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
import org.apache.spark._
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.SQLContext
import org.apache.spark.ml.feature.StringIndexer
import org.apache.spark.ml.feature.VectorAssembler
import org.apache.spark.ml.classification.BinaryLogisticRegressionSummary
import org.apache.spark.ml.evaluation.BinaryClassificationEvaluator
import org.apache.spark.ml.classification.LogisticRegression
import org.apache.spark.ml.feature.StringIndexer
import org.apache.spark.ml.feature.VectorAssembler
import sqlContext.implicits._
import sqlContext._
import org.apache.spark.sql.functions._
import org.apache.spark.mllib.linalg.DenseVector
import org.apache.spark.mllib.evaluation.BinaryClassificationMetrics

Мы используем класс case Scala для определения схемы, соответствующей строке в файле данных csv.

1
2
// define the Cancer Observation Schema
case class Obs(clas: Double, thickness: Double, size: Double, shape: Double, madh: Double, epsize: Double, bnuc: Double, bchrom: Double, nNuc: Double, mit: Double)

Приведенные ниже функции анализируют строку из файла данных в классе наблюдения рака.

01
02
03
04
05
06
07
08
09
10
// function to create a Obs class from an Array of Double.Class Malignant 4 is changed to 1
def parseObs(line: Array[Double]): Obs = {
    Obs(
      if (line(9) == 4.0) 1 else 0, line(0), line(1), line(2), line(3), line(4), line(5), line(6), line(7), line(8)
    )
}
// function to transform an RDD of Strings into an RDD of Double, filter lines with ?, remove first column
def parseRDD(rdd: RDD[String]): RDD[Array[Double]] = {
    rdd.map(_.split(",")).filter(_(6) != "?").map(_.drop(1)).map(_.map(_.toDouble))
}

Ниже мы загружаем данные из файла csv в RDD из строк. Затем мы используем преобразование карты для rdd, которое будет применять функцию ParseRDD для преобразования каждого элемента String в RDD в массив Double. Затем мы используем другое преобразование карты, которое будет применять функцию ParseObs для преобразования каждого массива Double в RDD в массив Array of Cancer Observation. Метод toDF () преобразует СДР массива [[Наблюдение за раком]] в Dataframe со схемой класса Наблюдение за раком.

bcloaddata

1
2
3
4
5
// load the data into a DataFrame
val rdd = sc.textFile("data/breast_cancer_wisconsin_data.txt")
val obsRDD = parseRDD(rdd).map(parseObs)
val obsDF = obsRDD.toDF().cache()
obsDF.registerTempTable("obs")

DataFrame printSchema () Печатает схему на консоли в древовидном формате

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Return the schema of this DataFrame
obsDF.printSchema
 
root
 |-- clas: double (nullable = false)
 |-- thickness: double (nullable = false)
 |-- size: double (nullable = false)
 |-- shape: double (nullable = false)
 |-- madh: double (nullable = false)
 |-- epsize: double (nullable = false)
 |-- bnuc: double (nullable = false)
 |-- bchrom: double (nullable = false)
 |-- nNuc: double (nullable = false)
 |-- mit: double (nullable = false)
 
// Display the top 20 rows of DataFrame
obsDF.show
 
 
+----+---------+----+-----+----+------+----+------+----+---+
|clas|thickness|size|shape|madh|epsize|bnuc|bchrom|nNuc|mit|
+----+---------+----+-----+----+------+----+------+----+---+
| 0.0|      5.0| 1.01.0| 1.0|   2.0| 1.0|   3.0| 1.0|1.0|
| 0.0|      5.0| 4.04.0| 5.0|   7.0|10.0|   3.0| 2.0|1.0|
| 0.0|      3.0| 1.01.0| 1.0|   2.0| 2.0|   3.0| 1.0|1.0|
...
+----+---------+----+-----+----+------+----+------+----+---+
only showing top 20 rows

После создания экземпляра DataFrame вы можете запросить его с помощью SQL-запросов. Вот несколько примеров запросов с использованием Scala DataFrame API:

описать вычисляет статистику для столбца толщины, в том числе count, mean, stddev, min и max

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//  computes statistics for thickness
obsDF.describe("thickness").show
 
+-------+------------------+
|summary|         thickness|
+-------+------------------+
|  count|               683|
|   mean|  4.44216691068814|
| stddev|2.8207613188371288|
|    min|               1.0|
|    max|              10.0|
+-------+------------------+
  
 
// compute the avg thickness, size, shape grouped by clas (malignant or not)
sqlContext.sql("SELECT clas, avg(thickness) as avgthickness, avg(size) as avgsize, avg(shape) as avgshape FROM obs GROUP BY clas ").show
 
+----+-----------------+------------------+------------------+
|clas|     avgthickness|           avgsize|          avgshape|
+----+-----------------+------------------+------------------+
| 1.0|7.188284518828452| 6.577405857740586| 6.560669456066946|
| 0.0|2.963963963963964|1.3063063063063063|1.4144144144144144|
+----+-----------------+------------------+------------------+
  
// compute avg thickness grouped by clas (malignant or not)
obsDF.groupBy("clas").avg("thickness").show
 
+----+-----------------+
|clas|   avg(thickness)|
+----+-----------------+
| 1.0|7.188284518828452|
| 0.0|2.963963963963964|
+----+-----------------+

Особенности извлечения

Чтобы построить модель классификатора, вы сначала извлекаете функции, которые наиболее способствуют классификации. В наборе данных о раке данные помечены двумя классами — 1 (злокачественный) и 0 (не злокачественный).

Функции для каждого элемента состоят из полей, показанных ниже:

  • Метка → злокачественная: 0 или 1
  • Особенности → {«толщина», «размер», «форма», «madh», «epsize», «bnuc», «bchrom», «nNuc», «mit»}

Определить массив объектов

функции-массив

(справочник Learning Spark)

Для того чтобы функции использовались алгоритмом машинного обучения, эти функции преобразуются и помещаются в векторы признаков, которые представляют собой векторы чисел, представляющие значение для каждого объекта.

Ниже VectorAssembler используется для преобразования и возврата нового DataFrame со всеми столбцами объектов в векторном столбце.

bctransformfeatures

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
//define the feature columns to put in the feature vector
val featureCols = Array("thickness", "size", "shape", "madh", "epsize", "bnuc", "bchrom", "nNuc", "mit")
 
//set the input and output column names
val assembler = new VectorAssembler().setInputCols(featureCols).setOutputCol("features")
//return a dataframe with all of the  feature columns in  a vector column
val df2 = assembler.transform(obsDF)
// the transform method produced a new column: features.
df2.show
 
+----+---------+----+-----+----+------+----+------+----+---+--------------------+
|clas|thickness|size|shape|madh|epsize|bnuc|bchrom|nNuc|mit|            features|
+----+---------+----+-----+----+------+----+------+----+---+--------------------+
| 0.0|      5.0| 1.01.0| 1.0|   2.0| 1.0|   3.0| 1.0|1.0|[5.0,1.0,1.0,1.0,...|
| 0.0|      5.0| 4.04.0| 5.0|   7.0|10.0|   3.0| 2.0|1.0|[5.0,4.0,4.0,5.0,...|
| 1.0|      8.0|10.0| 10.0| 8.0|   7.0|10.0|   9.0| 7.0|1.0|[8.0,10.0,10.0,8....|

Затем мы используем StringIndexer для возврата Dataframe со столбцом clas (злокачественный или нет), добавленным в качестве метки.

bctransformfeaturesandlabel

01
02
03
04
05
06
07
08
09
10
11
12
//  Create a label column with the StringIndexer 
val labelIndexer = new StringIndexer().setInputCol("clas").setOutputCol("label")
val df3 = labelIndexer.fit(df2).transform(df2)
// the  transform method produced a new column: label.
df3.show
 
+----+---------+----+-----+----+------+----+------+----+---+--------------------+-----+
|clas|thickness|size|shape|madh|epsize|bnuc|bchrom|nNuc|mit|            features|label|
+----+---------+----+-----+----+------+----+------+----+---+--------------------+-----+
| 0.0|      5.0| 1.01.0| 1.0|   2.0| 1.0|   3.0| 1.0|1.0|[5.0,1.0,1.0,1.0,...|  0.0|
| 0.0|      5.0| 4.04.0| 5.0|   7.0|10.0|   3.0| 2.0|1.0|[5.0,4.0,4.0,5.0,...|  0.0|
| 0.0|      3.0| 1.01.0| 1.0|   2.0| 2.0|   3.0| 1.0|1.0|[3.0,1.0,1.0,1.0,...|  0.0|

Ниже данных. Он разделен на набор обучающих данных и набор тестовых данных. 70% данных используются для обучения модели, а 30% будут использоваться для тестирования.

1
2
3
//  split the dataframe into training and test data
val splitSeed = 5043
val Array(trainingData, testData) = df3.randomSplit(Array(0.7, 0.3), splitSeed)

Тренировать модель

creditmlcrossvalidation

Далее мы обучаем модель логистической регрессии с упругой регуляризацией сети

Модель обучается путем установления связей между входными функциями и помеченными выходными данными, связанными с этими функциями.

bcfitmodel

01
02
03
04
05
06
07
08
09
10
// create the classifier,  set parameters for training
val lr = new LogisticRegression().setMaxIter(10).setRegParam(0.3).setElasticNetParam(0.8)
//  use logistic regression to train (fit) the model with the training data
val model = lr.fit(trainingData)   
 
// Print the coefficients and intercept for logistic regression
println(s"Coefficients: ${model.coefficients} Intercept: ${model.intercept}")
 
 
Coefficients: (9,[1,2,5,6],[0.06503554553146387,0.07181362361391264,0.07583963853124673,0.0012675057388232965]) Intercept: -1.39319142312609

Проверьте модель

Далее мы используем тестовые данные, чтобы получить прогнозы.

bcpredicttest

01
02
03
04
05
06
07
08
09
10
11
12
13
14
// run the  model on test features to get predictions
val predictions = model.transform(testData)
//As you can see, the previous model transform produced a new columns: rawPrediction, probablity and prediction.
predictions.show
 
+----+---------+----+-----+----+------+----+------+----+---+--------------------+-----+--------------------+--------------------+----------+
|clas|thickness|size|shape|madh|epsize|bnuc|bchrom|nNuc|mit|            features|label|       rawPrediction|         probability|prediction|
+----+---------+----+-----+----+------+----+------+----+---+--------------------+-----+--------------------+--------------------+----------+
| 0.0|      1.0| 1.01.0| 1.0|   1.0| 1.0|   1.0| 3.0|1.0|[1.0,1.0,1.0,1.0,...|  0.0|[1.17923510971064...|[0.76481024658406...|       0.0|
| 0.0|      1.0| 1.01.0| 1.0|   1.0| 1.0|   3.0| 1.0|1.0|[1.0,1.0,1.0,1.0,...|  0.0|[1.17670009823299...|[0.76435395397908...|       0.0|
| 0.0|      1.0| 1.01.0| 1.0|   1.0| 1.0|   3.0| 1.0|1.0|[1.0,1.0,1.0,1.0,...|  0.0|[1.17670009823299...|[0.76435395397908...|       0.0|
| 0.0|      1.0| 1.01.0| 1.0|   2.0| 1.0|   1.0| 1.0|1.0|[1.0,1.0,1.0,1.0,...|  0.0|[1.17923510971064...|[0.76481024658406...|       0.0|
| 0.0|      1.0| 1.01.0| 1.0|   2.0| 1.0|   2.0| 1.0|1.0|[1.0,1.0,1.0,1.0,...|  0.0|[1.17796760397182...|[0.76458217679258...|       0.0|
+----+---------+----+-----+----+------+----+------+----+---+--------------------+-----+--------------------+--------------------+----------+

Ниже мы оцениваем прогнозы и используем BinaryClassificationEvaluator, который возвращает метрику точности, сравнивая столбец метки теста со столбцом прогноза теста. В этом случае оценка возвращает точность 99%.

bcevaluatemodelpredictions

1
2
3
4
5
6
//A common metric used for logistic regression is area under the ROC curve (AUC). We can use the BinaryClasssificationEvaluator to obtain the AUC
// create an Evaluator for binary classification, which expects two input columns: rawPrediction and label.
val evaluator = new BinaryClassificationEvaluator().setLabelCol("label").setRawPredictionCol("rawPrediction").setMetricName("areaUnderROC")
// Evaluates predictions and returns a scalar metric areaUnderROC(larger is better).
val accuracy = evaluator.evaluate(predictions)
accuracy: Double = 0.9926910299003322

Ниже мы рассчитаем еще несколько метрик. Количество ложных и истинных положительных и отрицательных предсказаний также полезно:

  • Истинные положительные результаты показывают, как часто модель правильно предсказывала, что опухоль была злокачественной
  • Ложные срабатывания показывают, как часто модель предсказывала, что опухоль была злокачественной, когда она была доброкачественной
  • Истинные негативы показывают, как модель правильно предсказала, что опухоль была доброкачественной
  • Ложные отрицания указывают на то, как часто модель предсказывала, что опухоль была доброкачественной, хотя на самом деле она была злокачественной
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// Calculate Metrics
val lp = predictions.select( "label", "prediction")
val counttotal = predictions.count()
val correct = lp.filter($"label" === $"prediction").count()
val wrong = lp.filter(not($"label" === $"prediction")).count()
val truep = lp.filter($"prediction" === 0.0).filter($"label" === $"prediction").count()
val falseN = lp.filter($"prediction" === 0.0).filter(not($"label" === $"prediction")).count()
val falseP = lp.filter($"prediction" === 1.0).filter(not($"label" === $"prediction")).count()
val ratioWrong=wrong.toDouble/counttotal.toDouble
val ratioCorrect=correct.toDouble/counttotal.toDouble
 
 
counttotal: Long = 199
correct: Long = 168
wrong: Long = 31
truep: Long = 128
falseN: Long = 30
falseP: Long = 1
ratioWrong: Double = 0.15577889447236182
ratioCorrect: Double = 0.8442211055276382
 
 
// use MLlib to evaluate, convert DF to RDD
val  predictionAndLabels =predictions.select("rawPrediction", "label").rdd.map(x => (x(0).asInstanceOf[DenseVector](1), x(1).asInstanceOf[Double]))
val metrics = new BinaryClassificationMetrics(predictionAndLabels)
println("area under the precision-recall curve: " + metrics.areaUnderPR)
println("area under the receiver operating characteristic (ROC) curve : " + metrics.areaUnderROC)
// A Precision-Recall curve plots (precision, recall) points for different threshold values, while a receiver operating characteristic, or ROC, curve plots (recall, false positive rate) points. The closer  the area Under ROC is to 1, the better the model is making predictions.
 
area under the precision-recall curve: 0.9828385182615946
area under the receiver operating characteristic (ROC) curve : 0.9926910299003322

Хотите узнать больше?

В этом посте мы показали, как начать использовать машинное обучение логистической регрессии Apache Spark для классификации. Если у вас есть какие-либо дополнительные вопросы по этому учебнику, пожалуйста, задавайте их в разделе комментариев ниже.