Java程序員實戰機器學習——從聚類算法開始

本文適合有編程經驗的程序員,是一篇機器學習的」Hello world!」,沒什麼理論知識,在乎理論準確性的人請繞道。java

前言

    人工智能無疑是近年來最火熱的技術話題之一,以機器學習爲表明的人工智能技術,已經慢慢滲透到咱們生活的方方面面,任何事物只要沾上機器學習的邊,彷佛就變得高大上了。做爲處於技術大潮中程序員,咱們離機器學習是那麼地近,卻又git

        「只在此山中,雲深不知處」。程序員

    爲何要用Java/Kotlin?算法

    不能否認,Python纔是機器學習中的主流語言,可是以我實際的機器學習項目來看,Python適用於算法研究,它的穩定性和生態難以支撐起一個大型的應用,隨着Spark、dl4j等一系列java組件的流行,能夠預見java將會是大型機器學習應用的主流平臺。數據庫

    由此可知機器學習技術的應用,是Java程序員將來的核心能力之一,可是做爲程序員的咱們,該如何入門機器學習呢?在此咱們先拋開機器學習中那些繁雜的概念,從機器學習中最有表明性的聚類算法開始實踐。apache

    沒錯,我是以Java的名義「騙」你進來的,但我相信Java基礎良好的人,閱讀如下的Kotlin代碼徹底沒有問題,下面的代碼也徹底能夠翻譯成Java代碼,這恰好是一個頗有意義的練習。本文的示例代碼之因此用Kotlin,徹底是Kotlin能更簡潔地表達個人相法,且與Java的兼容性至關完美。編程

 

惟一的背景知識

       機器學習有無數分類和具體方法,聚類算法或者再具體點K均值聚類無疑是其中最有表明性的一種無監督學習方法,它像不少普通統計學算法同樣簡單,卻又具有了訓練、預測等能力,使用起來與深度學習很接近,是入門機器學習絕好算法。bash

       在此用做者本人的語言通俗易懂地解釋一下K均值聚類(k-means):框架

一種自動的分類算法:將一堆具備類似數值屬性的對象集合,歸類到K個類別中,經過不斷地迭代使類別內的數據具備最大的類似性、類別之間能最大程度地相互區別。機器學習

       大道至簡,經過簡單的聚類算法,咱們能夠:

  1. 代替人工,對海量的用戶數據進行更快速的自動化分類
  2. 根據自動聚類結果,發現潛在規律,如:買尿布的奶爸每每會給本身再買幾瓶啤酒;
  3. 經過聚類結果,更快速地對新數據進行歸類或預測,好比:以歷史數據聚類結果爲模型,根據體檢身理數據快速預測某人的疾病風險;
  4. 加速高維數據的查找速度,如:按圖片深度特徵對圖庫進行聚類,以便經過分層查找快速從數以億計的圖片中找到類似度最高的商品集(相似百度搜圖、淘寶拍立淘)

借用Apache Commons Math文檔中的聚類算法對比圖,來理解下聚類究竟是作啥:

Comparison of clustering algorithms

圖中用不一樣顏色表示不一樣類簇,展現了各類二維數據集聚類後的效果。

 

動手實踐

原始需求:

某司門戶網站分爲如下欄目:

視頻

文學

漫畫

動畫

汽車

導航

雜誌

郵箱

醫療

證券

新聞

錢包

商界

    運營人員整理了本季度2萬個用戶訪問量數據,但願根據這些數據,對本站用戶進行畫像,並進一步推出有針對性的營銷活動,及精準地投遞廣告。

說明:數據文件爲「,」分隔的csv文件,第一列是用戶id,後面13列是用戶對每一個欄目的訪問量。

 

分析步驟:

  1. 對數進行處理以供分析
  2. 對處理後的數據進行聚類
  3. 將聚類類別解讀爲用戶分類畫像
  4. 根據用戶分類畫像提出有針對性營銷活動
  5. 將有針對性的營銷活動推達每一個用戶

代碼實踐:

1. 使用Maven建立工程

mvn archetype:generate \
          -DinteractiveMode=false \
          -DarchetypeGroupId=org.jetbrains.kotlin \
          -DarchetypeArtifactId=kotlin-archetype-jvm \
          -DarchetypeVersion=1.3.70 \
          -DgroupId=org.ctstudio \
          -DartifactId=customer-cluster \
          -Dversion=1.0

命令執行完成後,用你喜歡的IDE導入maven工程。

2. 添加依賴

    咱們用到了commons-csv來解析數據,用commons-math3提供的聚類算法,順便也用到了Kotlin的jdk8擴展特性。在實際使用時,你可使用本身喜歡的csv組件,絕大部分支持機器學習的組件好比Spark和Mahout都包含了k-means聚類算法,只要掌握了基本用法,很容易按需替換。

<!-- 使用kotlin8的jdk8擴展,主要是簡化文件打開代碼 -->
<dependency>
    <groupId>org.jetbrains.kotlin</groupId>
    <artifactId>kotlin-stdlib-jdk8</artifactId>
    <version>${kotlin.version}</version>
</dependency>

<!-- 用來導入、導出CSV格式的數據文件 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-csv</artifactId>
    <version>1.6</version>
</dependency>

<!-- 主要用到了其中的聚類算法 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-math3</artifactId>
    <version>3.6.1</version>
</dependency>

 

2. 下載數據

 

將如下兩個文件下載到本地,供代碼使用,如放入前述工程的根目錄:

 

 

3. 編寫代碼

讀取數據並結構化爲用戶PV列表:

// 定義用戶PV實體類,實現Clusterable以便聚類算法使用
// 其中id爲第一列用戶id,pv爲double[]表示用戶對各欄目的訪問量,clusterId爲分類,供保存結果時使用
class UserPV(var id: Int, private val pv: DoubleArray, var clusterId: Int = 0) : Clusterable {
    override fun getPoint(): DoubleArray {
        return pv
    }

    override fun toString(): String {
        return "{id:$id,point:${point.toList()}}"
    }
}

// 使用commons-csv讀取數據文件爲UserPV列表
fun loadData(filePath: String): List<UserPV> {
    val fmt = CSVFormat.EXCEL
    FileReader(filePath).use { reader ->
        return CSVParser.parse(reader, fmt).records.map {
            val uid = it.first().toIntOrNull() ?: 0
            val pv = DoubleArray(13) { i ->
                it[i + 1]?.toDoubleOrNull() ?: 0.0
            }
            UserPV(uid, pv)
        }
    }
}

數據預處理,去掉異常數據,處理記錄中的異常值,並將訪問量歸一化

// 過濾或處理異常數據,實際業務中,可能須要作更多過濾或處理
// 過濾無效的用戶id
val filteredData = originData.filter { it.id > 0 }
// 負數的訪問量處理爲0
filteredData.forEach { it.point.forEachIndexed { i, d -> if (d < 0.0) it.point[i] = 0.0 } }
// 對PV數據歸一化
normalize(filteredData)

歸一化代碼:

fun <T : Clusterable> normalize(points: List<T>, dimension: Int = points.first().point.size) {
    val maxAry = DoubleArray(dimension) { Double.NEGATIVE_INFINITY }
    val minAry = DoubleArray(dimension) { Double.POSITIVE_INFINITY }
    points.forEach {
        maxAry.assignEach { index, item -> max(item, it.point[index]) }
        minAry.assignEach { index, item -> min(item, it.point[index]) }
    }
    // 此處用到了Kotlin的操做符重載,封裝了對double[]元素的逐個元素操做
    val denominator = maxAry - minAry
    points.forEach {
        // 此處代碼邏輯:(x - min)/(max - min)
        it.point.assignEach { i, item -> (item - minAry[i]) / denominator[i] }
    }
}

所謂歸一化,是指經過(value-min)/(max-min)將數據所有轉化到0~1的範圍以內,以免由於某個版塊的訪問量特別大影響聚類效果。

對數據調用聚類算法:

// 建立聚類算法實例,"5"爲想要歸類的類別數量
  // 實際狀況下包括k值在內的更多參數須要不斷調整、聚類、評估來達到最佳的聚類效果
  val kMeans = KMeansPlusPlusClusterer<UserPV>(5)
  // 使用算法對處理後的數據進行聚類
  val clusters = kMeans.cluster(filteredData)

每每在一開始,咱們並不知道數據分多少類是最合適的,此時就須要評估算法來評估不一樣中心點下的聚類效果。

Calinski-Harabasz是一個很經常使用的評估算法,基本思想就是類內部越緊湊、類間距離越大,則得分越高。惋惜java目前尚未開源的版本,好在我提交給Apache Commons Math的代碼,已經被commons-math4接受了,你們儘可期待。此處直接用我已經寫好的Kotlin版,你也能夠本身實現:

// 建立聚類算法
val kMeans = KMeansPlusPlusClusterer<UserPV>(5)
// 對數據集進行聚類
val clusters = kMeans.cluster(filteredData)
// 建立Calinski-Harabaszy評估算法
val evaluator = CalinskiHarabasz<UserPV>()
// 爲剛纔的聚類結果評分
val score = evaluator.score(clusters)

有了聚類、評分代碼,咱們須要動態挑選出最合適的k值即聚類中心數:

val evaluator = CalinskiHarabasz<UserPV>()
    var maxScore = 0.0
    var bestKMeans: KMeansPlusPlusClusterer<UserPV>? = null
    var bestClusters: List<CentroidCluster<UserPV>>? = null
    for (k in 2..10) {
        val kMeans = KMeansPlusPlusClusterer<UserPV>(k)
        val clusters = kMeans.cluster(filteredData)
        val score = evaluator.score(clusters)
        //挑選出分數最高的聚類簇
        if (score > maxScore) {
            maxScore = score
            bestKMeans = kMeans
            bestClusters = clusters
        }
        println("k=$k,score=$score")
    }

    //打印最佳的聚類中心數
    println("Best k is ${bestKMeans!!.k}")

經過對比多個k值的評分,咱們得出將用戶分爲三類是最合適的,此時咱們能夠將聚類結果保存下來,估分析解讀

// 保存中心點數據
fun saveCenters(
    clusters: List<CentroidCluster<UserPV>>,
    fileCategories: String,
    fileCenters: String
) {
    // 從categories.csv中讀取版塊標題
    val categories = readCategories(fileCategories)
    // 保存按版塊標題與聚類中心點
    writeCSV(fileCenters) { printer ->
        printer.print("")
        printer.printRecord(categories)
        for (cluster in clusters) {
            //每類用戶數
            printer.print(cluster.points.size)
            //每類訪問量均值
            printer.printRecord(cluster.center.point.toList())
        }
    }
}

...

saveCenters(clusters, "categories.csv", "centers.csv")

用戶所屬分類,一般也須要保存下來,做爲之後針對每一個用戶提供個性化服務的依據:

//保存用戶id-類別對應關係到csv文件
fun saveClusters(
    clusters: List<CentroidCluster<UserPV>>,
    fileClusters: String
) {
    writeCSV(fileClusters) { printer ->
        var clusterId = 0
        clusters.flatMap {
            clusterId++
            it.points.onEach { p -> p.clusterId = clusterId }
        }.sortedBy { it.id }.forEach { printer.printRecord(it.id, it.clusterId) }
    }
}
...
saveClusters(clusters, "clusters.csv")

注意此處保存爲CSV僅供演示,根據實際業務,你可能須要將用戶id-分類對應關係寫入數據庫。

4. 聚類結果解讀

使用Excel打開centers.csv文件,咱們能夠將每列中的最大值(表明了歸一化的每類用戶的平均訪問量)用背景色標出做爲本類用戶的特色:

從以上表格不難看出咱們的用戶能夠分爲三類:

  1. 有7010人喜歡視頻、文學、動漫
  2. 有8151人關注汽車、導航、雜誌和郵箱
  3. 有4839人喜歡醫療、證券、新聞、錢包和商界

    若是結合用戶的其它註冊信息,咱們甚至能夠給出用戶一些較明確的畫像,好比結合年齡、性別:喜歡電影和動漫的大學生、關注汽車&時尚的職場人士、關注健康&理財的家庭主婦...

 

總結

    若是你看到這裏,會發現上手機器學習也不是那麼難,代碼運行起來嗖嗖的,也不須要太多框架和組件。若是你的數據夠大,好比過億,也能夠期待我正在給Apache Commons Math貢獻的小批量k-means聚類算法(將隨commons-math4發佈),相比換用Spark等這些框架,算法帶來的可謂是指數級的性能提高。固然當你的數據大到單機難以承載之時,那些分佈式框架仍是必不可少的。

    想要學好機器學習,掌握理論知識是必不可少的,千里之行,始於足下,讓咱們先從掌握聚類算法開始,此文以外你還有必要去搜索一些聚類算法的理論知識來加深本身的理解。

    下次,我可能要用通俗易懂的方式,給你們講一些深刻(其實也沒太深)機器學習的必要前提知識,好比如何從一維空間推導、理解多維空間,方差、歐式距離。固然我是實踐高手,但不是理論高手,這些知識,都是爲了引出我一個實際AI項目的案例:-)

參考

相關文章
相關標籤/搜索