資料預處理
Table of Contents
1. AI處理的資料
大數據與傳統資料的主要差異
1.1. 資料量:
- 大數據:AI所需的大數據集規模通常極大,通常包含數十萬、數百萬甚至數十億筆記錄。這些資料可以來自網路使用者的行為、傳感器數據、社群媒體等。
- 傳統資料:傳統意義上的資料通常規模較小,例如企業的銷售記錄、財務報表,或者手動收集的問卷調查結果。
1.2. 資料結構:
Figure 1: 結構化與非結構化資料
- 大數據:大數據包含結構化(如Excel報表、Sensor回傳資料)、半結構化(有欄位、但不一致)和非結構化數據(如社群軟體貼文)。這些數據可以包括文字、圖片、影片、聲音等,因此處理大數據需要考慮多樣的資料格式。
- 傳統資料:通常是結構化的資料,例如SQL資料庫中的表格,行列分明,資料屬性相對簡單。
1.3. 資料處理與儲存:
- 大數據的儲存:由於大數據的數量龐大,通常需要分佈式系統來存儲,例如Hadoop、分散式資料庫等。大數據的資料儲存涉及到不同的檔案格式,如Parquet、Avro等,這些格式有助於壓縮數據並高效存取。
- 傳統資料的儲存:多數情況下會採用關聯式數據庫來儲存,使用如MySQL、PostgreSQL等。
1.4. 資料更新頻率:
- 大數據:大數據來自不斷變化的即時數據來源,資料更新的頻率可能是每秒、每分鐘,甚至是即時的,因此需要考慮實時處理的需求。
- 傳統資料:傳統資料通常是靜態的,資料更新的頻率低,通常是批量更新,並且並不需要即時性。
1.5. 資料處理特點:
- 大數據處理:因應資料量和資料格式的多樣化,大數據處理更多地依賴於分布式處理技術,例如MapReduce、Spark。這些工具的設計旨在能夠平行處理大量的資料。
- 傳統資料處理:傳統資料處理則多數使用單機處理技術,或是使用單一SQL語句進行分析,處理的複雜度和規模有限。
這些差異決定了AI所面臨的資料不僅僅是量上的增加,更是對多樣性和處理方法上的挑戰。因此,資料的儲存格式、擴充性和資料品質變得更加重要,需要採取新的資料預處理方法,確保AI模型可以有效使用這些數據。
2. 資料預處理
在收集到所需的資料後,常會遇到各種資料不全、缺失的情況,因此需要對資料進行整理,以便後續的分析。在進行資料建模運算之前,需要進行的資料預處理工作大致可分為以下幾點:
- 資料遺漏值與異常值處理
對於資料中的缺失值或異常值,可以採取以下方法:
- 刪除有遺漏值或異常值的樣本或特徵。
- 用平均值、中位數、眾數等方法進行填補。
- 對於異常值,可以選擇替代處理,以減少對模型的負面影響。
- 刪除有遺漏值或異常值的樣本或特徵。
- 資料編碼
針對類別型資料進行編碼,常見的編碼方法有:
- 標籤編碼(Label Encoding):將類別資料轉換為數值標籤,適用於類別之間有順序性的情況。
- 獨熱編碼(One-Hot Encoding):將類別資料轉換為二進位表示,每個類別對應一個新特徵,適用於類別之間無順序性的情況。
- 二元編碼(Binary Encoding):對於類別數量較多的情況,二元編碼可以有效減少特徵數量,平衡編碼效率與模型複雜度。
- 標籤編碼(Label Encoding):將類別資料轉換為數值標籤,適用於類別之間有順序性的情況。
- 資料標準化與正規化
- 對資料進行標準化或正規化,以便縮放資料範圍,確保所有特徵值在相似的數值範圍內,這樣有助於提升模型的訓練效果。
- 對資料進行標準化或正規化,以便縮放資料範圍,確保所有特徵值在相似的數值範圍內,這樣有助於提升模型的訓練效果。
- 資料特徵選取
- 根據資料集中的特徵對預測目標的影響程度,選擇保留有用的特徵。這可以透過統計方法或特徵重要性評估技術來實現。
- 根據資料集中的特徵對預測目標的影響程度,選擇保留有用的特徵。這可以透過統計方法或特徵重要性評估技術來實現。
- 資料擴增
- 資料縮減
- 使用主成分分析(PCA)等方法來降低資料維度,減少運算量並保留重要資訊。
- 使用主成分分析(PCA)等方法來降低資料維度,減少運算量並保留重要資訊。
- 資料訓練集與測試集之分割
- 將資料集分為訓練集和測試集,以便進行模型訓練和性能評估。通常的分割合適比例為80%訓練集與20%測試集,或者70%與30%。
- 將資料集分為訓練集和測試集,以便進行模型訓練和性能評估。通常的分割合適比例為80%訓練集與20%測試集,或者70%與30%。
以下的程式碼 大部份 都是 簡單 的pandas應用,如果你看不懂,表示你自已要惡補一下Pandas的課程,可以參看:
3. 資料遺漏值與異常值處理
3.1. 讀檔
3.1.1. Colab
3.1.1.1. 檔案在/content裡
3.1.1.2. 檔案在你的Google Drive
先連結Google Drive
1: import pandas as pd 2: # import Google Drive 套件 3: from google.colab import drive 4: # 將自己的雲端硬碟掛載上去 5: drive.mount('/content/gdrive') 6: # 透過 gdrive/My Drive/... 來存取檔案 7: test = pd.read_csv('gdrive/My Drive/CatDog.csv') 8: test
3.1.1.3. 檔案在網路上
可以利用pandas直接讀取網路上的資料檔
1: import pandas as pd 2: 3: # 讀取網路上的csv 4: df = pd.read_csv('https://raw.githubusercontent.com/letranger/AI/refs/heads/gh-pages/ABCD.csv') 5: print(df)
3.2. 識別、刪除遺漏值
現實世界中可能會因各種原因導致數據缺失或遺漏(如問卷被刻意留白),這些部份通常會以「空白」、「NaN」或「NULL」來取代。
3.2.1. 查看資料集內容
1: csv_data ="""A,B,C,D,E 2: 1.0,, 2.0, 3.0, 4.0 3: 5.0,, 6.0,, 8.0 4: 10.0,, 11.0, 12.0, 5.0 5: 9.9,,8.0, 12.0""" 6: 7: import sys 8: import pandas as pd 9: 10: # 讀入程式檔中的csv資料 11: from io import StringIO 12: df = pd.read_csv(StringIO(csv_data)) 13: print(df)
A B C D E 0 1.0 NaN 2.0 3.0 4.0 1 5.0 NaN 6.0 NaN 8.0 2 10.0 NaN 11.0 12.0 5.0 3 9.9 NaN 8.0 12.0 NaN
雖然pd.read_csv是用來讀取網路上或本機端的csv檔,此處為了省去大家讀取d檔案的工作,我們以直接以字串模擬一個檔案出來,所以在讀取時要以下列的方式來讀取內容:
1: pd.read_csv(StringIO(字串變數名稱))
3.2.2. 遺漏值的識別
現在可以大概統計一下遺漏值
1: # 列出每行有的null個數 2: print(df.isnull().sum()) 3: print(df.isnull().sum(axis=1))
A 0 B 4 C 0 D 1 E 1 dtype: int64 0 1 1 2 2 1 3 2 dtype: int64
3.2.3. 刪除有遺漏值的記錄
3.2.3.1. 刪除有遺失值的資料列
1: print('====刪掉有遺失值的列:df.dropna(axis=0)====') 2: tmpDf = df.dropna(axis=0) 3: print(tmpDf)
Empty DataFrame Columns: [A, B, C, D, E] Index: []
3.2.3.2. 刪除有遺失值的資料行
1: print('====刪掉有遺失值的欄:df.dropna(axis=1)====') 2: tmpDf = df.dropna(axis=1) 3: print(tmpDf)
====刪掉有遺失值的欄:df.dropna(axis=1)==== A C 0 1.0 2.0 1 5.0 6.0 2 10.0 11.0 3 9.9 8.0
3.2.3.3. 刪除整列為NaN者
1: print('====刪除整欄為NaN者:df.dropna(how=\'all\')====') 2: tmpDf = df.dropna(axis=1, how='all') 3: print(tmpDf)
====刪除整欄為NaN者:df.dropna(how='all')==== A C D E 0 1.0 2.0 3.0 4.0 1 5.0 6.0 NaN 8.0 2 10.0 11.0 12.0 5.0 3 9.9 8.0 12.0 NaN
3.2.3.4. 刪除有值個數低於thresh的列
1: print('====刪除有值個數低於thresh的列:df.dropna(thresh=4)====') 2: tmpDf = df.dropna(thresh=4) 3: print(tmpDf)
====刪除有值個數低於thresh的列:df.dropna(thresh=4)==== A B C D E 0 1.0 NaN 2.0 3.0 4.0 2 10.0 NaN 11.0 12.0 5.0
3.2.3.5. 刪除特定行(如第C行)中有NaN之列
1: print('====刪除特定行(如第C行)中有NaN之列:df.dropna(columns=[\'C\'])====') 2: tmpDf=df.drop(columns=['C']) 3: print(tmpDf)
====刪除特定行(如第C行)中有NaN之列:df.dropna(columns=['C'])==== A B D E 0 1.0 NaN 3.0 4.0 1 5.0 NaN NaN 8.0 2 10.0 NaN 12.0 5.0 3 9.9 NaN 12.0 NaN
雖然刪除包含遺漏值的數據似乎是個方便的方法,但終究可能會刪除過多的樣本,導致分析的結果並不可靠;或是因為刪除了特徵的時候,卻失去了重要的資訊。
3.3. 填補遺漏值
3.3.1. 直接填零
1: csv_data ="""A,B,C,D,E 2: 1.0,7.7, 2.0, 3.0, 4.0 3: 5.0,, 6.0,, 8.0 4: 10.0,, 11.0, 12.0, 5.0 5: 9.9,8.8,8.0, 12.0""" 6: 7: import sys 8: import pandas as pd 9: # python 2.7需進行unicode轉碼 10: if (sys.version_info < (3, 0)): 11: csv_data = unicode(csv_data) 12: # 讀入程式檔中的csv資料 13: from io import StringIO 14: df = pd.read_csv(StringIO(csv_data)) 15: 16: tmpDf = df.fillna(0) 17: print(tmpDf)
A B C D E 0 1.0 7.7 2.0 3.0 4.0 1 5.0 0.0 6.0 0.0 8.0 2 10.0 0.0 11.0 12.0 5.0 3 9.9 8.8 8.0 12.0 0.0
3.3.2. 以平均值填補
3.3.2.1. Python手動填補
1: print(df) 2: df['D'] = df['D'].fillna(df['D'].mean()) 3: print(df)
A B C D E 0 1.0 7.7 2.0 3.0 4.0 1 5.0 NaN 6.0 NaN 8.0 2 10.0 NaN 11.0 12.0 5.0 3 9.9 8.8 8.0 12.0 NaN A B C D E 0 1.0 7.7 2.0 3.0 4.0 1 5.0 NaN 6.0 9.0 8.0 2 10.0 NaN 11.0 12.0 5.0 3 9.9 8.8 8.0 12.0 NaN
3.3.2.2. 以scikit-learn的impute填補
最常見的「插補技術」之一為「平均插補」(mean imputation),即,以整個特徵行的平均值來代替遺漏值。
1: # impute missing values via the column mean 2: from sklearn.impute import SimpleImputer 3: import numpy as np 4: 5: imr = SimpleImputer(missing_values=np.nan, strategy='mean') 6: imr = imr.fit(df.values) 7: imputed_data = imr.transform(df.values) 8: print(df) 9: print(imputed_data)
A B C D E 0 1.0 7.7 2.0 3.0 4.0 1 5.0 NaN 6.0 9.0 8.0 2 10.0 NaN 11.0 12.0 5.0 3 9.9 8.8 8.0 12.0 NaN [[ 1. 7.7 2. 3. 4. ] [ 5. 8.25 6. 9. 8. ] [10. 8.25 11. 12. 5. ] [ 9.9 8.8 8. 12. 5.66666667]]
scikit-learn早期版本的填補類別為Imputer,屬於 transformer 類別,主要的工作是做「數據轉換」,這些 estimator 有兩種基本方法:fit 與 transform,fit 方法是用來進行參數學習。
在新的版本中,SimpleImputer 已經被拿來取代以前的 sklearn.preprocessing.Imputer,預設的strategy為mean,其他strategy請參關scikit-learn官網。
- SimpleImputer 基本語法:
1: from sklearn.impute import SimpleImputer 2: 3: # 建立 SimpleImputer 物件 4: imputer = SimpleImputer(strategy='mean') 5: 6: # 對數據進行學習 7: imputer.fit(data) 8: 9: # 對數據進行轉換,填補遺漏值 10: filled_data = imputer.transform(data)
其中的strategy可以是
- mean:使用每一列的均值填補缺失值(預設)。
- median:使用每一列的中位數填補缺失值。
- most_frequent:使用每一列最常出現的值填補缺失值。
- constant:使用一個固定值來填補缺失值,需指定 fill_value。
- mean:使用每一列的均值填補缺失值(預設)。
- SimpleImputer範例
1: import numpy as np 2: from sklearn.impute import SimpleImputer 3: 4: # 建立帶有遺漏值的數據 5: data = np.array([[1, 2, np.nan], [4, np.nan, 6], [7, 8, 9]]) 6: 7: # 使用均值填補遺漏值 8: imputer = SimpleImputer(strategy='mean') 9: imputer.fit(data) 10: filled_data = imputer.transform(data) 11: 12: print(filled_data) 13:
[[1. 2. 7.5] [4. 5. 6. ] [7. 8. 9. ]]
- 使用中位數 (median) 來填補:
1: imputer = SimpleImputer(strategy='median') 2: filled_data = imputer.fit_transform(data) 3: print(filled_data)
[[1. 2. 7.5] [4. 5. 6. ] [7. 8. 9. ]]
- 使用最常出現的值 (most_frequent) 來填補:
1: imputer = SimpleImputer(strategy='most_frequent') 2: filled_data = imputer.fit_transform(data) 3: print(filled_data)
[[1. 2. 6.] [4. 2. 6.] [7. 8. 9.]]
- 使用固定值 (constant) 來填補:
1: imputer = SimpleImputer(strategy='constant', fill_value=-1) 2: filled_data = imputer.fit_transform(data) 3: print(filled_data)
[[ 1. 2. -1.] [ 4. -1. 6.] [ 7. 8. 9.]]
- 使用中位數 (median) 來填補:
3.3.3. 時間序列資料的插補
假設我們有一些 PM2.5 感測器在全台南市地區,每 30 分鐘偵測一次,共有 20 支感測器進行了一段時間的測量,其中部分感測器在特定時間失靈,導致一些值遺失。
3.3.3.1. 線性插補
1: pm25_data = """Sensor1,Sensor2,Sensor3,Sensor4,Sensor5,Sensor6,Sensor7,Sensor8,Sensor9,Sensor10,Sensor11,Sensor12,Sensor13,Sensor14,Sensor15,Sensor16,Sensor17,Sensor18,Sensor19,Sensor20 2: 35.0,40.0,30.0,45.0,38.0,,50.0,,42.0,37.0,39.0,,41.0,50.0,,48.0,39.0,,,44.0 3: 36.0,,32.0,46.0,39.0,51.0,43.0,,43.0,,40.0,,42.0,,47.0,49.0,39.5,,43.0,45.0 4: ,41.0,,47.0,40.0,,44.0,44.0,38.0,41.0,,46.0,,51.0,,50.0,40.0,,44.0,46.0 5: 37.0,42.0,34.0,,41.0,52.0,,45.0,39.0,,47.0,43.0,,52.0,48.0,,40.5,42.0,,47.0 6: 38.0,,35.0,49.0,,53.0,46.0,46.0,,40.0,42.0,48.0,,53.0,,51.0,,43.0,45.0,48.0 7: 39.0,43.0,36.0,,43.0,,47.0,47.0,,41.0,,49.0,45.0,,49.0,52.0,42.0,44.0,,49.0 8: ,44.0,37.0,50.0,44.0,55.0,,48.0,,43.0,,50.0,,54.0,,53.0,43.0,45.0,46.0,50.0 9: 40.0,45.0,,51.0,45.0,,49.0,49.0,43.0,,44.0,,46.0,55.0,50.0,,44.5,46.5,47.0,51.0 10: 41.0,,39.0,52.0,46.0,57.0,50.0,,50.0,,45.0,52.0,,56.0,,54.0,45.0,,48.0,52.0 11: 42.0,46.0,40.0,,47.0,,51.0,,,44.0,46.0,,48.0,57.0,51.0,53.0,,46.0,49.0,53.0 12: ,47.0,41.0,54.0,,59.0,52.0,52.0,,45.0,,53.0,49.0,,52.0,56.0,,47.0,50.0,""" 13: import pandas as pd 14: from io import StringIO 15: 16: pd.options.display.precision = 2 17: # 讀入感測器的 PM2.5 資料 18: df_pm25 = pd.read_csv(StringIO(pm25_data)) 19: print(df_pm25) 20: print('==========使用前後時間平均值進行插補==========') 21: df_pm25 = df_pm25.interpolate(method='linear', axis=1) 22: print(df_pm25)
Sensor1 Sensor2 Sensor3 ... Sensor18 Sensor19 Sensor20 0 35.0 40.0 30.0 ... NaN NaN 44.0 1 36.0 NaN 32.0 ... NaN 43.0 45.0 2 NaN 41.0 NaN ... NaN 44.0 46.0 3 37.0 42.0 34.0 ... 42.0 NaN 47.0 4 38.0 NaN 35.0 ... 43.0 45.0 48.0 5 39.0 43.0 36.0 ... 44.0 NaN 49.0 6 NaN 44.0 37.0 ... 45.0 46.0 50.0 7 40.0 45.0 NaN ... 46.5 47.0 51.0 8 41.0 NaN 39.0 ... NaN 48.0 52.0 9 42.0 46.0 40.0 ... 46.0 49.0 53.0 10 NaN 47.0 41.0 ... 47.0 50.0 NaN [11 rows x 20 columns] ==========使用前後時間平均值進行插補========== Sensor1 Sensor2 Sensor3 ... Sensor18 Sensor19 Sensor20 0 35.0 40.0 30.0 ... 40.67 42.33 44.0 1 36.0 34.0 32.0 ... 41.25 43.00 45.0 2 NaN 41.0 44.0 ... 42.00 44.00 46.0 3 37.0 42.0 34.0 ... 42.00 44.50 47.0 4 38.0 36.5 35.0 ... 43.00 45.00 48.0 5 39.0 43.0 36.0 ... 44.00 46.50 49.0 6 NaN 44.0 37.0 ... 45.00 46.00 50.0 7 40.0 45.0 48.0 ... 46.50 47.00 51.0 8 41.0 40.0 39.0 ... 46.50 48.00 52.0 9 42.0 46.0 40.0 ... 46.00 49.00 53.0 10 NaN 47.0 41.0 ... 47.00 50.00 50.0 [11 rows x 20 columns]
3.3.3.2. [課堂練習]線性插補的兩端缺失問題 TNFSH
上面的插補似乎無法解決 資料列的頭尾缺失 問題,請想辧法解決這個問題,你可以
- 通靈
- Google
- 冥想
- ChatGPT
3.4. [作業]資料預處理 TNFSH
3.4.1. 題目
南一中網路書店即將開張,為了處理龐大的書單資料,資訊科教師們很無恥的把書籍資料登錄工作當成作業分派給一年級的修課學生,所謂團結力量大,一份不太可靠的書目資料就這麼完成了。
這份書目資料共計271,350筆,每筆資料有以下9個欄位
- ’ISBN’
- ’Book-Title’
- ’Book-Author’
- ’Year-Of-Publication’
- ’Publisher’
- ’Image-URL-S’
- ’Image-URL-M’
- ’Image-URL-L’
- ’Book-Price’
然而,大概是因為作者群都是被迫做白工的關係,這份資料有不少缺失值與錯誤資料,錯誤的類型大概有以下幾類:
- 缺失: 就是該欄位完全沒有值
- 價格錯誤: 書價為0,或是書價超過20000元
- 出版年代錯誤: 年代為0或是超過2024年
3.4.2. 要求
請你透過colab來完成以下的任務:
3.4.2.1. 讀檔
你可以選擇用Pandas直接讀線上的檔案,也可以選擇將檔案上傳到Google的雲端硬碟後再利用Colab來讀取。
3.4.2.2. 預處理
要請你進行以下的資料預處理
- 除所有有缺失值的記錄(只要有一欄有缺失值、該筆資料就整筆刪去)
- 改變錯誤日期,超過2024的都改為2024
- 改變錯誤日期,日期為0的都改為1900
- 改變錯誤書價,超過2000的都改為1000
- 改變錯誤書價,書價為0者改為100
3.4.2.3. 輸出
最後輸出以下內容
- 列出原始資料筆數
- 列出修正(刪除缺失值)後的資料筆數
- 列出2000年出版的書籍數量
- 列出作者中有Bruce的書籍數量
- 列出 500<=書價<=800 的書籍數量
- 列出平均書價
3.4.3. 參考答案
整份colab的程式碼要能一次執行並輸出以下結果(不能直接print我給的答案…)
原始資料筆數 271350 可用資料數: 259397 2000年出版: 16438 作者群中有Bruce: 667 800<=書價<=1000: 58776 平均書價: 559.23
3.4.4. 友情提醒
- 資料量很大,相信我,你不會想用Excel或Numbers或Google試算表來打開它然後逐一處理…,我試過在一台8G的Macbook Air上用Numbers打開這個csv檔,大概花了 八分鐘 就開起來了…
- 你可以參考Python選修Pandas教材,不過這份教材只是概略描述基本功能,你可能還需要再自行Google相關的功能
4. 資料間的關係
找出特徵值間的差異與相似性是機器學習中非常重要的工作。
4.1. 兩點間的距離
兩種常見的特徵距離計算方式:曼哈頓距離及歐幾里得距離,以一個城市中的兩個地標為例(如圖3中的兩個黑點)。
Figure 3: 城市中的兩個地標
4.1.1. 曼哈頓距離(Manhattan distance)
在如圖3這樣的棋盤式街道的城式中,要由點(1,1)走到(7,7)的最簡單方式是先往上直走到(1,7),再右轉直走到(7,7),這便是曼哈頓距離,這段距離為 \(|1-7|+|1-7| \),公式為
\[ \sum_{i=1}^n|x_i-y_i| \]
4.1.2. 歐幾里得距離(Euclidian distance)
Figure 4: Numbus 2000
如果我們肯花美金4991買到Nimbus 2000,那我們就能實踐「直線是兩點間最短距離」這個法則,此時圖3中由點(1,1)到點(7,7)的矩離就變成\( \sqrt{(1-7)^2+(1-7)^2} \),公式為
\[ \sqrt{\sum_{i=1}^n(x_i-y_i)^2 } \]
4.1.3. [課堂練習]能力相似度 TNFSH
4.1.4. 進階閱讀
4.2. 兩個向量間的距離
以下為三個學生的國文、數學、英文三科成績,哪兩個學生的學業能力較為接近?
- James: 80, 90, 70
- Ruby: 90, 80, 60
- Vanessa: 90, 70, 90
為了回答上述問題,我們可以將學生的各科成績當成vector(如圖5),我們的問題就變成:哪兩個vector更為相似? 三個學生的成績向量分佈如下:
1: import matplotlib.pyplot as plt 2: from mpl_toolkits.mplot3d import Axes3D 3: import numpy as np 4: 5: plt.rcParams['font.sans-serif'] = ['Arial Unicode MS'] # 步驟一(替換系統中的字型,這裡用的是Mac OSX系統) 6: plt.rcParams['axes.unicode_minus'] = False # 步驟二(解決座標軸負數的負號顯示問題) 7: 8: def setup_3d_axes(): 9: ax = plt.axes(projection='3d') 10: ax.view_init(azim=-105, elev=20) 11: ax.set_xlabel('國文') 12: ax.set_ylabel('英文') 13: ax.set_zlabel('數學') 14: ax.set_xlim(0, 100) 15: ax.set_ylim(0, 100) 16: ax.set_zlim(0, 100) 17: return ax 18: 19: ax = setup_3d_axes() 20: 21: # plot the vector (3, 2, 5) 22: origin = np.zeros((3, 1)) 23: point = np.array([[80, 90, 70]]).T 24: vector = np.hstack([origin, point]) 25: ax.plot(*vector, color='r', label='James') 26: #ax.plot(*point, color='k', marker='o') 27: 28: # project the vector onto the x,y plane and plot it 29: xy_projection_matrix = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 0]]) 30: projected_point = xy_projection_matrix @ point 31: projected_vector = xy_projection_matrix @ vector 32: 33: point = np.array([[90, 80, 60]]).T 34: vector = np.hstack([origin, point]) 35: ax.plot(*vector, color='b', label='Ruby') 36: #ax.plot(*point, color='k', marker='o') 37: 38: # project the vector onto the x,y plane and plot it 39: xy_projection_matrix = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 0]]) 40: projected_point = xy_projection_matrix @ point 41: projected_vector = xy_projection_matrix @ vector 42: 43: point = np.array([[90, 70, 90]]).T 44: vector = np.hstack([origin, point]) 45: ax.plot(*vector, color='g', label='Vanessa') 46: #ax.plot(*point, color='k', marker='o') 47: 48: # project the vector onto the x,y plane and plot it 49: xy_projection_matrix = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 0]]) 50: projected_point = xy_projection_matrix @ point 51: projected_vector = xy_projection_matrix @ vector 52: 53: ax.legend() 54: plt.legend() 55: plt.savefig('images/healthCondition.png', dpi=300)
Figure 5: 三個學生的成績向量
如何計算? 有以下三種方式:歐幾里得距離、餘弦相似度、向量內積(如圖6)4。
Figure 6: Vector Similarity
4.2.1. 歐幾里得距離
Figure 7: 歐幾里得距離
多維空間中兩個向量之間的直線距離,距離越近越相似。歐幾里得距離演算法的優點是可以反映向量的絕對距離,適用於需要考慮向量長度的相似性計算。例如推薦系統中,需要根據使用者的歷史行為來推薦相似的商品,這時就需要考慮使用者的歷史行為的數量,而不僅僅是使用者的歷史行為的相似度5。
- \( a: (a_1, a_2, \dots , a_n)\)
- \( b: (b_1, b_2, \dots , b_n)\)
計算兩向量a, b歐幾里得距離的方式為: \( d(a,b) = \sqrt{(a_1-b_1)^2 + (a_2-b_2)^2 + \dots + (a_n-b_n)^2}\)
4.2.2. 餘弦相似度(Cosine Similarity)
Figure 8: 餘弦相似度
兩個向量的夾角越小越相似,比較兩個向量的餘弦值進行比較,夾角越小,餘弦值越大。餘弦相似度對向量的長度不敏感,只關注向量的方向,因此適用於高維向量的相似性計算。例如語義搜尋和文件分類5。當兩個向量的方向重合時夾角餘弦取最大值1,當兩個向量的方向完全相反夾角餘弦取最小值-1。
- \( a: (a_1, a_2, \dots , a_n)\)
- \( b: (b_1, b_2, \dots , b_n)\)
計算兩向量a, b餘弦相似度的方式為: \( sim(a,b) = \frac{a \cdot b}{||a| \cdot |b||} = \frac{ \sum\limits_{i=1}^n a_i \times b_i}{\sqrt{\sum\limits_{i=1}^na_i^2} \times \sqrt{\sum\limits_{i=1}^nb_i^2}} \)
4.2.2.1. Dot Product
Figure 9: 向量內積
一種計算向量之間相似度的度量演算法,它計算兩個向量之間的點積(內積),所得值越大越與搜尋值相似5。
- \( a: (a_1, a_2, \dots , a_n)\)
- \( b: (b_1, b_2, \dots , b_n)\)
計算兩向量a, b內積的方式為: \( a \cdot b = a_1b_1 + a_2b_2 + \dots + a_nb_n \)
或是
\[ a \cdot b = |a||b| \cos \alpha \]
4.2.3. 歐幾里得距離實作
有了公式就可以自己用你苦學了半年的Python+高一數學來手刻程式,或是呼叫其他函式庫來計算。
4.2.3.1. Python手刻
1: import math 2: 3: def cosine_similarity(v1,v2): 4: sumxx, sumxy, sumyy = 0, 0, 0 5: for i in range(len(v1)): 6: x = v1[i]; y = v2[i] 7: sumxx += x*x 8: sumyy += y*y 9: sumxy += x*y 10: return sumxy/math.sqrt(sumxx*sumyy) 11: 12: James = [80, 90, 70] 13: Ruby = [90, 80, 60] 14: Vanessa = [90, 70, 90] 15: JR_sim = cosine_similarity(James, Ruby) 16: RV_sim = cosine_similarity(Ruby, Vanessa) 17: JV_sim = cosine_similarity(James, Vanessa) 18: 19: print('James v.s. Ruby:', JR_sim) 20: print('Ruby v.s. Vanessa:', RV_sim) 21: print('James v.s. Vanessa:', JV_sim)
James v.s. Ruby: 0.9925966195847843 Ruby v.s. Vanessa: 0.977356154645257 James v.s. Vanessa: 0.978640304032486
4.2.3.2. Numpy
1: from numpy import dot 2: from numpy.linalg import norm 3: 4: James = [80, 90, 70] 5: Ruby = [90, 80, 60] 6: Vanessa = [90, 70, 90] 7: JR_sim = dot(James, Ruby)/(norm(James)*norm(Ruby)) 8: RV_sim = dot(Ruby, Vanessa)/(norm(Ruby)*norm(Vanessa)) 9: JV_sim = dot(James, Vanessa)/(norm(James)*norm(Vanessa)) 10: 11: print('James v.s. Ruby:', JR_sim) 12: print('Ruby v.s. Vanessa:', RV_sim) 13: print('James v.s. Vanessa:', JV_sim)
James v.s. Ruby: 0.9925966195847841 Ruby v.s. Vanessa: 0.9773561546452569 James v.s. Vanessa: 0.9786403040324858
4.2.4. 餘弦相似度實作
4.2.4.1. SciPy
1: from scipy import spatial 2: 3: James = [80, 90, 70] 4: Ruby = [90, 80, 60] 5: Vanessa = [90, 70, 90] 6: JR_sim = 1 - spatial.distance.cosine(James, Ruby) 7: RV_sim = 1 - spatial.distance.cosine(Ruby, Vanessa) 8: JV_sim = 1 - spatial.distance.cosine(James, Vanessa) 9: 10: print('James v.s. Ruby:', JR_sim) 11: print('Ruby v.s. Vanessa:', RV_sim) 12: print('James v.s. Vanessa:', JV_sim)
James v.s. Ruby: 0.9925966195847843 Ruby v.s. Vanessa: 0.977356154645257 James v.s. Vanessa: 0.978640304032486
4.3. [課堂作業]曼哈頓距離
- 參考前節[歐幾里得距離實作]
- 自行Google曼哈頓距離的定義
- 計算以下三人兩兩間的距離,找出能力最接近的2人
1: # 定義三個人的分數 2: James = [80, 90, 70] 3: Ruby = [90, 80, 60] 4: Vanessa = [90, 70, 90]
4.4. 其他應用
4.5. [課堂作業]誰和我的能力最接近 TNFSH
4.5.1. 說明
以下是5位學生的程式設計能力資料(特徵值為APCS觀念題分數、APCS實作題分數、一年級程式設計學期分數),請問那一位學生與你的能力(50, 300, 86)最為接近?
A: 70, 340, 84
B: 60, 310, 89
C: 50, 280, 90
D: 40, 320, 78
E: 91, 310, 99
4.5.2. 要求
- 找出與你的能力最相似的那位學生編號
- 你可以完全用程式自動輸出,也可以用程式計算你與每個人的相似度後再人工輸出,但是不能以通靈的方式直接輸出結果
1: # 這是免費贈送的讀檔code,你不一定要用numpy,也可以使用python來計算 2: import numpy as np 3: 4: scores = [[70, 340, 84], [58, 318, 76], [54, 279, 85], [40, 320, 78], [91, 310, 99]]
5. 資料集分類特徵編碼
5.1. 讀檔
真實世界的數據集往往包含各種「類別特徵」(categorical feature),類別特徵可再分為
- nominal feature: 名義特徵
- ordinal feature: 次序特徵
1: import pandas as pd 2: df = pd.DataFrame([['東區',150,25.3,45.49,'華廈(10層含以下有電梯)',30,'四層/九層','住家用','文教區'], 3: ['北區',321,16.8,78.66,'住宅大樓(11層含以上有電梯)',32,'十三層/十四層','商業用','行政區'], 4: ['東區',900,29.2,30.87,'華廈(10層含以下有電梯)',28,'三層/九層','住商用','倉庫區'], 5: ['新市區',460,13.4,34.35,'華廈(10層含以下有電梯)',43,'五層/五層','住家用','文教區'], 6: ['安南區',350,32.00,11,'透天厝',57,'全/二層','住家用','農業區'], 7: ['歸仁區',950,22.9,41.49,'住宅大樓(11層含以上有電梯)',29,'四層/二十層','住家用','保護區'], 8: ['東區',390,24.7,56.38,'住宅大樓(11層含以上有電梯)',27,'十三層/十四層','住商用','行政區'], 9: ['新化區',482,15.3,31.59,'公寓(5樓含以下無電梯)',42,'三層/五層','住家用','倉庫區']]) 10: df.columns = ['地段', '總價', '單價', '總面積', '型態', '屋齡', '樓別', '用途', '地區別'] 11: print(df)
地段 總價 單價 總面積 型態 屋齡 樓別 用途 地區別 0 東區 150 25.3 45.49 華廈(10層含以下有電梯) 30 四層/九層 住家用 文教區 1 北區 321 16.8 78.66 住宅大樓(11層含以上有電梯) 32 十三層/十四層 商業用 行政區 2 東區 900 29.2 30.87 華廈(10層含以下有電梯) 28 三層/九層 住商用 倉庫區 3 新市區 460 13.4 34.35 華廈(10層含以下有電梯) 43 五層/五層 住家用 文教區 4 安南區 350 32.0 11.00 透天厝 57 全/二層 住家用 農業區 5 歸仁區 950 22.9 41.49 住宅大樓(11層含以上有電梯) 29 四層/二十層 住家用 保護區 6 東區 390 24.7 56.38 住宅大樓(11層含以上有電梯) 27 十三層/十四層 住商用 行政區 7 新化區 482 15.3 31.59 公寓(5樓含以下無電梯) 42 三層/五層 住家用 倉庫區
5.2. 次序變項
目前的土地使用區分大致有行政區、文教區、倉庫區、風景區、農業區、河川區…等不同型態,這裡主觀的以上述順序做為土地價值順序,也就是把’土地區分’這個欄位視為ordinal feature。此處自定一個 mapping dictionary,即 land_mapping,然後將 land_mapping 對應到 land_mapping 中的鍵值(程式第10行)。
1: ### Mapping ordinal features 2: land_mapping = { 3: '行政區': 7, 4: '文教區': 6, 5: '倉庫區': 5, 6: '風景區': 4, 7: '農業區': 3, 8: '保護區': 2, 9: '河川區': 1} 10: df['地區別'] = df['地區別'].map(land_mapping) 11: print(df)
地段 總價 單價 總面積 型態 屋齡 樓別 用途 地區別 0 東區 150 25.3 45.49 華廈(10層含以下有電梯) 30 四層/九層 住家用 6 1 北區 321 16.8 78.66 住宅大樓(11層含以上有電梯) 32 十三層/十四層 商業用 7 2 東區 900 29.2 30.87 華廈(10層含以下有電梯) 28 三層/九層 住商用 5 3 新市區 460 13.4 34.35 華廈(10層含以下有電梯) 43 五層/五層 住家用 6 4 安南區 350 32.0 11.00 透天厝 57 全/二層 住家用 3 5 歸仁區 950 22.9 41.49 住宅大樓(11層含以上有電梯) 29 四層/二十層 住家用 2 6 東區 390 24.7 56.38 住宅大樓(11層含以上有電梯) 27 十三層/十四層 住商用 7 7 新化區 482 15.3 31.59 公寓(5樓含以下無電梯) 42 三層/五層 住家用 5
5.3. 名義變項
接下來我們試著處理一個nominal feature: 用途
5.3.1. classlabel
許多機器學習的函式庫需要將「類別標籤」編碼為整數值。方法之一是以列舉方式為這些 nominal features 自 0 開始編號,先以 enumerate 方式建立一個 mapping dictionary: class_mapping(程式第2行),然後利用這個字典將類別特徵轉換為整數值。
1: class_mapping = { 2: label: idx for idx, label in enumerate(np.unique(df['用途'])) 3: } 4: print(class_mapping) 5: # 將類別特徵轉換為整數值 6: df['用途'] = df['用途'].map(class_mapping) 7: print(df)
{'住商用': 0, '住家用': 1, '商業用': 2} 地段 總價 單價 總面積 型態 屋齡 樓別 用途 地區別 0 東區 150 25.3 45.49 華廈(10層含以下有電梯) 30 四層/九層 1 6 1 北區 321 16.8 78.66 住宅大樓(11層含以上有電梯) 32 十三層/十四層 2 7 2 東區 900 29.2 30.87 華廈(10層含以下有電梯) 28 三層/九層 0 5 3 新市區 460 13.4 34.35 華廈(10層含以下有電梯) 43 五層/五層 1 6 4 安南區 350 32.0 11.00 透天厝 57 全/二層 1 3 5 歸仁區 950 22.9 41.49 住宅大樓(11層含以上有電梯) 29 四層/二十層 1 2 6 東區 390 24.7 56.38 住宅大樓(11層含以上有電梯) 27 十三層/十四層 0 7 7 新化區 482 15.3 31.59 公寓(5樓含以下無電梯) 42 三層/五層 1 5
能將類別轉成整數,也要能將整數轉回類別。此處可以利用已產生的對應字典,藉由借調 key-value 來產生「反轉字典」(第2行),將對調產生的整數還原回原始類別特徵。
1: # 產生反轉字典,將整數還原至原始的類別標籤 2: inv_class_mapping = {v: k for k, v in class_mapping.items()} 3: print(inv_class_mapping) 4: df['用途'] = df['用途'].map(inv_class_mapping) 5: print(df)
{0: '住商用', 1: '住家用', 2: '商業用'} 地段 總價 單價 總面積 型態 屋齡 樓別 用途 地區別 0 東區 150 25.3 45.49 華廈(10層含以下有電梯) 30 四層/九層 住家用 6 1 北區 321 16.8 78.66 住宅大樓(11層含以上有電梯) 32 十三層/十四層 商業用 7 2 東區 900 29.2 30.87 華廈(10層含以下有電梯) 28 三層/九層 住商用 5 3 新市區 460 13.4 34.35 華廈(10層含以下有電梯) 43 五層/五層 住家用 6 4 安南區 350 32.0 11.00 透天厝 57 全/二層 住家用 3 5 歸仁區 950 22.9 41.49 住宅大樓(11層含以上有電梯) 29 四層/二十層 住家用 2 6 東區 390 24.7 56.38 住宅大樓(11層含以上有電梯) 27 十三層/十四層 住商用 7 7 新化區 482 15.3 31.59 公寓(5樓含以下無電梯) 42 三層/五層 住家用 5
5.3.2. scikit-learn LabelEncoder
事實上,scikit-learn 中有一個更為方便的 LabelEncoder 類別則可以直接完成上述工作(第4行)。
1: # Label encoding with sklearn's LabelEncoder 2: from sklearn.preprocessing import LabelEncoder 3: le = LabelEncoder() 4: y = le.fit_transform(df['用途'].values) 5: print(y) 6: df['用途'] = y 7: print(df) # 類別與數字的對應不一定與自訂字典一致
[1 2 0 1 1 1 0 1] 地段 總價 單價 總面積 型態 屋齡 樓別 用途 地區別 0 東區 150 25.3 45.49 華廈(10層含以下有電梯) 30 四層/九層 1 6 1 北區 321 16.8 78.66 住宅大樓(11層含以上有電梯) 32 十三層/十四層 2 7 2 東區 900 29.2 30.87 華廈(10層含以下有電梯) 28 三層/九層 0 5 3 新市區 460 13.4 34.35 華廈(10層含以下有電梯) 43 五層/五層 1 6 4 安南區 350 32.0 11.00 透天厝 57 全/二層 1 3 5 歸仁區 950 22.9 41.49 住宅大樓(11層含以上有電梯) 29 四層/二十層 1 2 6 東區 390 24.7 56.38 住宅大樓(11層含以上有電梯) 27 十三層/十四層 0 7 7 新化區 482 15.3 31.59 公寓(5樓含以下無電梯) 42 三層/五層 1 5
反向編碼
1: # 反向编碼,將數值編碼轉換回原始類別 2: original_labels = le.inverse_transform(y) 3: print("反轉編碼:", original_labels) 4: 5: df['原來的用途欄'] = original_labels 6: print(df)
反轉编碼: ['住家用' '商業用' '住商用' '住家用' '住家用' '住家用' '住商用' '住家用'] 地段 總價 單價 總面積 型態 屋齡 樓別 用途 地區別 原來的用途欄 0 東區 150 25.3 45.49 華廈(10層含以下有電梯) 30 四層/九層 1 6 住家用 1 北區 321 16.8 78.66 住宅大樓(11層含以上有電梯) 32 十三層/十四層 2 7 商業用 2 東區 900 29.2 30.87 華廈(10層含以下有電梯) 28 三層/九層 0 5 住商用 3 新市區 460 13.4 34.35 華廈(10層含以下有電梯) 43 五層/五層 1 6 住家用 4 安南區 350 32.0 11.00 透天厝 57 全/二層 1 3 住家用 5 歸仁區 950 22.9 41.49 住宅大樓(11層含以上有電梯) 29 四層/二十層 1 2 住家用 6 東區 390 24.7 56.38 住宅大樓(11層含以上有電梯) 27 十三層/十四層 0 7 住商用 7 新化區 482 15.3 31.59 公寓(5樓含以下無電梯) 42 三層/五層 1 5 住家用
你有看出這樣轉換會有什麼問題嗎?
5.4. One-Hot Encoding
在上面的例子中,我們以scikit-learn 的 LabelEncoder 類別將「類別特徵」編碼為整數值,但這樣會引發另一個問題:原本無序的類別變項就變成有序變項了。如果我們將上述資料中的 地段 特徵轉換為整數值,如下:
1: X = df[['地段']].values 2: # 以LabelEncoder轉換 3: from sklearn.preprocessing import LabelEncoder 4: le = LabelEncoder() 5: print(X) 6: print(le.fit_transform(X[:,0]))
[['東區'] ['北區'] ['東區'] ['新市區'] ['安南區'] ['歸仁區'] ['東區'] ['新化區']] [4 0 4 3 1 5 4 2]
由輸出結果可以發現,經過類別編碼後的地段特徵,由原本不具次序的特徵變成存在大小關係(歸仁區>東區>新市區…),這明顯會影響 model 運算的結果。
針對此一問題,常見的解決方案是 one-hot encoding(獨熱編碼–真是直白的翻譯啊啊啊….),其原理是:對特徵值中的每個值,建立一個新的「虛擬特徵」(dummy feature)。
5.4.1. 以pandas get_dummies()進行One Hot Encoding
利用 Pandas 套件的 get_dummies 類別,直接將類別資料轉成二進位類型,即One-Hot encoding。這種轉換只有字串數據會被轉換,其他內容則否。
1: print('===原始資料===') 2: print(df) 3: 4: OheDf = pd.get_dummies(df, columns=['地段']) 5: print('===轉換後資料===') 6: print(OheDf)
===原始資料=== 地段 總價 單價 總面積 型態 屋齡 樓別 用途 地區別 原來的用途欄 0 東區 150 25.3 45.49 華廈(10層含以下有電梯) 30 四層/九層 1 6 住家用 1 北區 321 16.8 78.66 住宅大樓(11層含以上有電梯) 32 十三層/十四層 2 7 商業用 2 東區 900 29.2 30.87 華廈(10層含以下有電梯) 28 三層/九層 0 5 住商用 3 新市區 460 13.4 34.35 華廈(10層含以下有電梯) 43 五層/五層 1 6 住家用 4 安南區 350 32.0 11.00 透天厝 57 全/二層 1 3 住家用 5 歸仁區 950 22.9 41.49 住宅大樓(11層含以上有電梯) 29 四層/二十層 1 2 住家用 6 東區 390 24.7 56.38 住宅大樓(11層含以上有電梯) 27 十三層/十四層 0 7 住商用 7 新化區 482 15.3 31.59 公寓(5樓含以下無電梯) 42 三層/五層 1 5 住家用 ===轉換後資料=== 總價 單價 總面積 型態 屋齡 ... 地段_安南區 地段_新化區 地段_新市區 地段_東區 地段_歸仁區 0 150 25.3 45.49 華廈(10層含以下有電梯) 30 ... False False False True False 1 321 16.8 78.66 住宅大樓(11層含以上有電梯) 32 ... False False False False False 2 900 29.2 30.87 華廈(10層含以下有電梯) 28 ... False False False True False 3 460 13.4 34.35 華廈(10層含以下有電梯) 43 ... False False True False False 4 350 32.0 11.00 透天厝 57 ... True False False False False 5 950 22.9 41.49 住宅大樓(11層含以上有電梯) 29 ... False False False False True 6 390 24.7 56.38 住宅大樓(11層含以上有電梯) 27 ... False False False True False 7 482 15.3 31.59 公寓(5樓含以下無電梯) 42 ... False True False False False [8 rows x 15 columns]
5.4.2. 以scikit-learn ColumnTransformer 進行One-Hot Encoding
利用 ColumnTransformer 函式庫的 ColumnTransformer 類別,將特徵值轉換 One-Hot Encoding 的對應矩陣,如程式第27行。
1: from sklearn.preprocessing import OneHotEncoder 2: import pandas as pd 3: 4: df = pd.DataFrame([['東區',150,25.3,45.49,'華廈(10層含以下有電梯)',30,'四層/九層','住家用','文教區'], 5: ['北區',321,16.8,78.66,'住宅大樓(11層含以上有電梯)',32,'十三層/十四層','商業用','行政區'], 6: ['東區',900,29.2,30.87,'華廈(10層含以下有電梯)',28,'三層/九層','住商用','倉庫區'], 7: ['新市區',460,13.4,34.35,'華廈(10層含以下有電梯)',43,'五層/五層','住家用','文教區'], 8: ['安南區',350,32.00,11,'透天厝',57,'全/二層','住家用','農業區'], 9: ['歸仁區',950,22.9,41.49,'住宅大樓(11層含以上有電梯)',29,'四層/二十層','住家用','保護區'], 10: ['東區',390,24.7,56.38,'住宅大樓(11層含以上有電梯)',27,'十三層/十四層','住商用','行政區'], 11: ['新化區',482,15.3,31.59,'公寓(5樓含以下無電梯)',42,'三層/五層','住家用','倉庫區']]) 12: df.columns = ['地段', '總價', '單價', '總面積', '型態', '屋齡', '樓別', '用途', '地區別'] 13: 14: print('===原始資料===') 15: print(df[['地段']]) 16: 17: from sklearn.compose import ColumnTransformer 18: 19: X = df[['地段']].values 20: ct = ColumnTransformer( 21: # The column numbers to be transformed (here is [0] but can be [0, 1, 3]) 22: # Leave the rest of the columns untouched 23: [('OneHot', OneHotEncoder(), [0])], remainder='passthrough' 24: ) 25: print('===轉換後的one-hot encoding資料===') 26: X_transformed = ct.fit_transform(X) 27: print(ct.fit_transform(X)) 28: 29: # 將稀疏矩陣還原為密集矩陣(非必須,只是讓我們容易看一下結果) 30: X_dense = X_transformed.toarray() 31: # 轉為dataframe、加入column name 32: encoded_columns = ct.named_transformers_['OneHot'].get_feature_names_out(['地段']) 33: df_encoded = pd.DataFrame(X_dense, columns=encoded_columns) 34: 35: print('===One-Hot结果===') 36: print(df_encoded)
===原始資料=== 地段 0 東區 1 北區 2 東區 3 新市區 4 安南區 5 歸仁區 6 東區 7 新化區 ===轉換後的one-hot encoding資料=== (0, 4) 1.0 (1, 0) 1.0 (2, 4) 1.0 (3, 3) 1.0 (4, 1) 1.0 (5, 5) 1.0 (6, 4) 1.0 (7, 2) 1.0 ===One-Hot结果=== 地段_北區 地段_安南區 地段_新化區 地段_新市區 地段_東區 地段_歸仁區 0 0.0 0.0 0.0 0.0 1.0 0.0 1 1.0 0.0 0.0 0.0 0.0 0.0 2 0.0 0.0 0.0 0.0 1.0 0.0 3 0.0 0.0 0.0 1.0 0.0 0.0 4 0.0 1.0 0.0 0.0 0.0 0.0 5 0.0 0.0 0.0 0.0 0.0 1.0 6 0.0 0.0 0.0 0.0 1.0 0.0 7 0.0 0.0 1.0 0.0 0.0 0.0
5.4.3. scikit learn OneHotEncoder()
1: # 初始化 OneHotEncoder 2: encoder = OneHotEncoder() 3: encoded_colors = encoder.fit_transform(df[['地段']]) 4: encoded_df = pd.DataFrame(encoded_colors.toarray(), columns=encoder.get_feature_names_out(['地段'])) 5: 6: df_encoded = pd.concat([df.drop(columns=['地段']), encoded_df], axis=1) 7: print(df_encoded)
總價 單價 總面積 型態 屋齡 ... 地段_安南區 地段_新化區 地段_新市區 地段_東區 地段_歸仁區 0 150 25.3 45.49 華廈(10層含以下有電梯) 30 ... 0.0 0.0 0.0 1.0 0.0 1 321 16.8 78.66 住宅大樓(11層含以上有電梯) 32 ... 0.0 0.0 0.0 0.0 0.0 2 900 29.2 30.87 華廈(10層含以下有電梯) 28 ... 0.0 0.0 0.0 1.0 0.0 3 460 13.4 34.35 華廈(10層含以下有電梯) 43 ... 0.0 0.0 1.0 0.0 0.0 4 350 32.0 11.00 透天厝 57 ... 1.0 0.0 0.0 0.0 0.0 5 950 22.9 41.49 住宅大樓(11層含以上有電梯) 29 ... 0.0 0.0 0.0 0.0 1.0 6 390 24.7 56.38 住宅大樓(11層含以上有電梯) 27 ... 0.0 0.0 0.0 1.0 0.0 7 482 15.3 31.59 公寓(5樓含以下無電梯) 42 ... 0.0 1.0 0.0 0.0 0.0 [8 rows x 14 columns]
6. 特徵縮放(Feature scaling)
當我們在比較分析兩組數據資料時,可能會遭遇因單位的不同(例如:身高與體重),或數字大小的代表性不同(例如:粉專1萬人與滿足感0.8),造成各自變化的程度不一,進而影響統計分析的結果;為解決此類的問題,我們可利用資料的正規化(Normalization, 或譯為常態化)與標準化(Standardization)來進行數據的比較及分析7。
「特徵縮放」(Feature scaling)是資料預處理的一個關鍵,「決策樹」和「隨機森林」是極少數無需進行 feature scaling 的分類技術;對多數機器學習演算法而言,若特徵值經過適當的縮放,都能有更佳成效。Feature scaling 的重要性可以以下例子看出,假設有兩個特徵值(a, b),其中 a 的測量範圍為 1 到 10,b 的測量值範圍為 1 到 100000,以典型分類演算法的做法,一定是忙於最佳化特徵值 b;若以 KNN 的演算法,也會被特徵值 b 所支配。
在機器學習演算法中,將數值縮放到同一scale能帶給模型下面兩個好處:
- 提升模型的收斂速度
在建構機器學習模型時,我們會利用梯度下降法(Gradient Descent)來計算成本函數(Cost Function)的最佳解;假設我們現有兩個特徵值 x1 in [0,1] 與 x2 in [0,10000],則在 x1-x2 平面上成本函數的等高線會呈窄長型,導致需較多的迭代步驟,另外也可能導致無法收斂的情況發生。因此,若將資料標準化,則能減少梯度下降法的收斂時間。 - 提高模型的精準度
將特徵值 x1 及 x2 餵入一些需計算樣本彼此的距離(例如:歐氏距離)分類器演算法中,則 x2 的影響很可能將遠大於 x1,若實際上 x1 的指標意義及重要性高於 x2,這將導致我們分析的結果失真。因此,資料的標準化是有必要的,可讓每個特徵值對結果做出相近程度的貢獻。
6.1. 常態化(Normalization)
正規化的目的是將資料縮放到固定的範圍內,通常是 [0, 1] 或 [-1, 1],這樣可以避免因為特徵值範圍過大或過小而影響模型的表現。這種方法特別適合那些依賴距離的模型,比如 k 最近鄰演算法(KNN)和神經網路。
以「將特徵值縮化為 0~1 間」為例,這是「最小最大縮放」(min-max scaling)的一個特例,做法如下:
\[x_{norm}^i = \frac{x^i-x_{min}}{x_{max}-x_{min}}\]
若以 scikit-learn 套件來完成實作,其程式碼如下:
1: from sklearn.preprocessing import MinMaxScaler 2: import pandas as pd 3: df = pd.DataFrame([['東區',150,25.3,45.49,'華廈(10層含以下有電梯)',30,'四層/九層','住家用','文教區'], 4: ['北區',321,16.8,78.66,'住宅大樓(11層含以上有電梯)',32,'十三層/十四層','商業用','行政區'], 5: ['東區',900,29.2,30.87,'華廈(10層含以下有電梯)',28,'三層/九層','住商用','倉庫區'], 6: ['新市區',460,13.4,34.35,'華廈(10層含以下有電梯)',43,'五層/五層','住家用','文教區'], 7: ['安南區',350,32.00,11,'透天厝',57,'全/二層','住家用','農業區'], 8: ['歸仁區',950,22.9,41.49,'住宅大樓(11層含以上有電梯)',29,'四層/二十層','住家用','保護區'], 9: ['東區',390,24.7,56.38,'住宅大樓(11層含以上有電梯)',27,'十三層/十四層','住商用','行政區'], 10: ['新化區',482,15.3,31.59,'公寓(5樓含以下無電梯)',42,'三層/五層','住家用','倉庫區']]) 11: df.columns = ['地段', '總價', '單價', '總面積', '型態', '屋齡', '樓別', '用途', '地區別'] 12: print(df) 13: 14: mmScaler = MinMaxScaler() 15: print('===Normalization後的資料===') 16: df[['總價', '單價']] = mmScaler.fit_transform(df[['總價', '單價']]) 17: print(df[['總價', '單價']]) 18: 19: # 將數據還原到原始範圍 20: print('===還原後的資料===') 21: df[['總價', '單價']] = mmScaler.inverse_transform(df[['總價', '單價']]) 22: print(df[['總價', '單價']])
地段 總價 單價 總面積 型態 屋齡 樓別 用途 地區別 0 東區 150 25.3 45.49 華廈(10層含以下有電梯) 30 四層/九層 住家用 文教區 1 北區 321 16.8 78.66 住宅大樓(11層含以上有電梯) 32 十三層/十四層 商業用 行政區 2 東區 900 29.2 30.87 華廈(10層含以下有電梯) 28 三層/九層 住商用 倉庫區 3 新市區 460 13.4 34.35 華廈(10層含以下有電梯) 43 五層/五層 住家用 文教區 4 安南區 350 32.0 11.00 透天厝 57 全/二層 住家用 農業區 5 歸仁區 950 22.9 41.49 住宅大樓(11層含以上有電梯) 29 四層/二十層 住家用 保護區 6 東區 390 24.7 56.38 住宅大樓(11層含以上有電梯) 27 十三層/十四層 住商用 行政區 7 新化區 482 15.3 31.59 公寓(5樓含以下無電梯) 42 三層/五層 住家用 倉庫區 ===Normalization後的資料=== 總價 單價 0 0.00000 0.639785 1 0.21375 0.182796 2 0.93750 0.849462 3 0.38750 0.000000 4 0.25000 1.000000 5 1.00000 0.510753 6 0.30000 0.607527 7 0.41500 0.102151 ===還原後的資料=== 總價 單價 0 150.0 25.3 1 321.0 16.8 2 900.0 29.2 3 460.0 13.4 4 350.0 32.0 5 950.0 22.9 6 390.0 24.7 7 482.0 15.3
6.2. 標準化(Standardization)
標準化則是將資料轉換為具有零均值和單位方差(說人話就是平均數為0、標準差為1h)的分佈,這意味著數據被中心化並且具有相同的尺度。這種技術適合資料呈現常態分佈或近似常態分佈的情況,並且適合大多數機器學習模型(如線性回歸、支持向量機等)。
雖說常態化簡單實用,但對許多機器學習演算法來說(特別是梯度下降法的最佳化),標準化則更為實際,我們可令標準化後的特徵值其平均數為 0、標準差為 1,這樣一來,特徵值會滿足常態分佈,進而使演算法對於離群值不那麼敏感。標準化的公式如下:
\[x_{std}^i = \frac{x^i-\mu_x}{\sigma_x}\]
若以 scikit-learn 套件來完成實作,其程式碼如下:
1: from sklearn.preprocessing import StandardScaler 2: sdScaler = StandardScaler() 3: df[['總價', '單價']] = sdScaler.fit_transform(df[['總價', '單價']]) 4: print('===標準化後的資料===') 5: print(df[['總價', '單價']]) 6: 7: df[['總價', '單價']] = sdScaler.inverse_transform(df[['總價', '單價']]) 8: print('===還原後的資料===') 9: print(df[['總價', '單價']])
===標準化後的資料=== 總價 單價 0 -1.331969 0.454115 1 -0.681904 -0.900263 2 1.519196 1.075535 3 -0.153488 -1.442014 4 -0.571659 1.521683 5 1.709274 0.071702 6 -0.419596 0.358512 7 -0.069854 -1.139270 ===還原後的資料=== 總價 單價 0 150.0 25.3 1 321.0 16.8 2 900.0 29.2 3 460.0 13.4 4 350.0 32.0 5 950.0 22.9 6 390.0 24.7 7 482.0 15.3
6.3. 適用時機
6.3.1. Standardization
- 常態分佈資料:當資料的分佈接近常態分佈時,標準化通常能有效地將資料居中,這樣的變換效果最佳。
- 線性模型(例如線性回歸、邏輯回歸):這些模型對資料尺度敏感,標準化可提升模型的效果。
- 距離度量模型(如K-means、SVM、PCA等):這些算法在不同尺度的特徵上運行時,數值較大的特徵可能會主導結果,標準化能夠使得所有特徵對結果的影響一致。
- 神經網路:在神經網絡中,標準化也能穩定訓練過程,加速收斂,尤其是在使用梯度下降的情境下。
6.3.2. Normalization
- 非常態分佈的資料:特徵範圍差異較大,且資料並不符合常態分佈時,正規化可避免資料的擴散影響。
- 距離度量模型(如K-NN、距離加權的算法):正規化能夠防止數值較大的特徵對距離計算產生過大影響。
- 樹模型(如決策樹、隨機森林):這些模型對資料尺度不敏感,正規化能使其更具解釋性。
- 對輸入範圍有特定要求的模型:如神經網絡,尤其是一些輸入需要在特定範圍內的神經網絡結構,如RNN和一些圖像處理應用的CNN。
7. 資料擴增
資料擴增是一種在訓練機器學習模型特別是深度學習模型時非常重要的技術。它通過對訓練資料進行多種隨機變換,來生成更多的資料樣本,以增強模型的泛化能力並避免過擬合。資料擴增對於擁有有限的情況尤其有用,因為它能夠讓模型看到更多樣化的資料,從而提升模型的預測能力和準確度。
- 資料擴增的重要性
資料擴增能夠模擬真實世界中數據的變化。例如,影像資料可能因為角度、光照、比例等不同而有所變化。透過對現有資料進行旋轉、翻轉、縮放、平移等變換,我們可以有效地提高模型的健壯性,使其能夠在不同場景下依然表現良好。 - 資料擴增的常見方法
資料擴增的方式多種多樣,以下是幾種常見的影像資料擴增技術:
- 翻轉 (Flip):將圖像在水平方向或垂直方向進行翻轉,模擬鏡像或反轉情況下的圖像。
- 旋轉 (Rotation):對圖像進行不同角度的旋轉,例如 90 度、180 度、270 度等,模擬不同角度拍攝的圖像。
- 縮放 (Scaling):將圖像進行放大或縮小,模擬不同距離下的拍攝效果。
- 裁剪 (Cropping):隨機裁剪圖像的一部分,模擬局部的視角或遮擋的情況。
- 人工噪聲 (Noise Addition):在圖像中增加隨機噪聲,模擬圖像在低光或其他干擾條件下的情況。
- 平移 (Translation):將圖像沿著水平或垂直方向進行平移,模擬不同位置的圖像對象。
- 翻轉 (Flip):將圖像在水平方向或垂直方向進行翻轉,模擬鏡像或反轉情況下的圖像。
7.1. 下載範例程式
- 底下的檔案可以透過以下方式下載(於終端機執行)
- 要記得修改main.py裡的資料夾路徑
1: git clone https://github.com/letranger/DataAugmentationDemo.git
7.2. 範例
安裝opencv
1: pip3 install opencv-python
資料夾位置 ~/Downloads
資料夾架構
1: tree ~/Downloads/aug -d
/Users/letranger/Downloads/aug ├── augImages │ ├── cats │ └── dogs └── images ├── cats └── dogs 7 directories
import cv2 import os import numpy as np def augment_image(image): """對輸入圖像進行數據增強並返回增強後的圖像列表""" augmented_images = [] # 原圖 augmented_images.append(image) # 翻轉圖像 flip1 = cv2.flip(image, 0) # 垂直翻轉 flip2 = cv2.flip(image, 1) # 水平翻轉 flip3 = cv2.flip(image, -1) # 垂直和水平翻轉 augmented_images.extend([flip1, flip2, flip3]) # 旋轉圖像 for angle in [90, 180, 270]: M = cv2.getRotationMatrix2D((image.shape[1] // 2, image.shape[0] // 2), angle, 1) rotated = cv2.warpAffine(image, M, (image.shape[1], image.shape[0])) augmented_images.append(rotated) # 縮放圖像 for scale in [0.9, 1.1]: scaled = cv2.resize(image, None, fx=scale, fy=scale) augmented_images.append(scaled) return augmented_images def process_directory(input_dir, output_dir): """處理輸入目錄中的所有圖像,並將增強後的圖像保存到輸出目錄""" if not os.path.exists(output_dir): os.makedirs(output_dir) categories = ['dogs', 'cats'] for category in categories: category_input_dir = os.path.join(input_dir, category) category_output_dir = os.path.join(output_dir, category) # 檢查並創建子目錄 if not os.path.exists(category_output_dir): os.makedirs(category_output_dir) for filename in os.listdir(category_input_dir): image_path = os.path.join(category_input_dir, filename) image = cv2.imread(image_path) augmented_images = augment_image(image) for i, augmented_image in enumerate(augmented_images): output_filename = f"{os.path.splitext(filename)[0]}_aug_{i}.jpg" output_path = os.path.join(category_output_dir, output_filename) cv2.imwrite(output_path, augmented_image) # 輸入和輸出目錄 input_directory = '/Users/letranger/Downloads/DataAugmentationDemo/images' output_directory = '/Users/letranger/Downloads/DataAugmentationDemo/augImages' # 處理圖像 process_directory(input_directory, output_directory)
7.3. 其他擴增方法
其他資料擴增方式還有:濾波、銳化、去噪、旋轉、縮放、裁剪和增加人工噪聲….,至於資料擴增對於模型有何助益請參閱這篇。其他資料擴增方式請看:
8. 資料集與資料分割
8.1. 常用資料集
當你使用 Python 學習人工智慧(AI)和機器學習(ML)時,以下是一些常用的資料集及其簡單介紹:
8.1.1. MNIST
簡介:MNIST(Modified National Institute of Standards and Technology database)是一個大型手寫數字資料集,包含 0 到 9 的手寫數字圖像。
- 用途:常用於圖像分類和計算機視覺的入門練習。
- 特徵:包含 60,000 張訓練圖像和 10,000 張測試圖像,每張圖像大小為 28x28 像素。
- 來源:可以從 tensorflow 或 keras 中直接獲取。
1: from tensorflow.keras.datasets import mnist 2: (x_train, y_train), (x_test, y_test) = mnist.load_data()
8.1.2. Iris
- 簡介:Iris 資料集包含 3 種鰹魚花(Setosa、Versicolour 和 Virginica)的 150 個樣本,每個樣本有 4 個特徵(花萼長度、花萼寬度、花瓣長度、花瓣寬度)。
- 用途:常用於分類和聚類算法的入門練習。
- 特徵:每個樣本包含 4 個特徵和 1 個標籤。
- 來源:可以從 sklearn 中直接獲取。
1: from sklearn.datasets import load_iris 2: iris = load_iris() 3: X, y = iris.data, iris.target
8.1.3. Boston 房價
- 簡介:Boston 房價資料集包含 506 個房屋的特徵和價格信息,用於回歸問題。
- 用途:常用於回歸算法的入門練習。
- 特徵:每個樣本包含 13 個特徵,如犯罪率、房間數、房產稅等。
- 來源:可以從 sklearn 中直接獲取。
1: import matplotlib.pyplot as plt 2: from tensorflow.keras.datasets import boston_housing 3: 4: (train_x, train_y), (test_x, test_y) = boston_housing.load_data()
8.1.4. CIFAR-10
- 簡介:CIFAR-10 是一個影像資料集,包含 10 個類別的 60,000 張彩色圖片,每個類別有 6,000 張圖片。
- 用途:常用於圖像分類和深度學習的入門練習。
- 特徵:每張圖像大小為 32x32 像素。
- 來源:可以從 tensorflow 或 keras 中直接獲取。
1: from tensorflow.keras.datasets import cifar10 2: (x_train, y_train), (x_test, y_test) = cifar10.load_data()
8.1.5. Wine
- 簡介:Wine 資料集包含 178 個樣本,記錄了 3 種不同葡萄酒的 13 個化學成分。
- 用途:常用於分類問題。
- 特徵:每個樣本包含 13 個特徵和 1 個標籤。
- 來源:可以從 sklearn 中直接獲取。
1: from sklearn.datasets import load_wine 2: wine = load_wine() 3: X, y = wine.data, wine.target
8.1.6. Breast Cancer Wisconsin
- 簡介:Breast Cancer Wisconsin 資料集包含 569 個乳腺癌樣本的特徵,目的是預測腫瘤是良性還是惡性。
- 用途:常用於二元分類問題。
- 特徵:每個樣本包含 30 個特徵。
- 來源:可以從 sklearn 中直接獲取。
1: from sklearn.datasets import load_breast_cancer 2: breast_cancer = load_breast_cancer() 3: X, y = breast_cancer.data, breast_cancer.target
8.2. 資料分割
8.2.1. 為什麼要分割資料
- 訓練集(training): 舉例來說就是上課學習。主要用在訓練階段,用於模型擬合,直接參與了模型參數調整的過程8。
- 驗證集(validation): 舉例來說就是模擬考,你會根據模擬考的成績繼續學習、或調整學習方式重新學習。在訓練過程中,用於評估模型的初步能力與超參數調整的依據。不過驗證集是非必需的,不像訓練集和測試集。如果不需要調整超參數,就可以不使用驗證集8。
- 測試集(test)就像是學測,用來評估你最終的學習結果。用來評估模型最終的泛化能力。為了能評估模型真正的能力,測試集不應該為參數調整、選擇特徵等依據8。
使用學測來比喻,是因為測試集不應該做為參數調整、選擇特徵等依據。這些選擇與調整可以想像成學習方式的調整,但學測已經考完,你不能時光倒轉回到最初調整學習方式8。
8.2.2. 資料分割實作
訓練集與測試集的分割可以自行以Python進行分割,也可以直接呼叫函式進行分割
8.2.2.1. 手動分割
1: import pandas as pd 2: import numpy as np 3: import random 4: 5: df_wine = pd.read_csv('https://archive.ics.uci.edu/' 6: 'ml/machine-learning-databases/wine/wine.data', 7: header=None) 8: 9: df_wine.columns = ['Class label', 'Alcohol', 'Malic acid', 'Ash', 10: 'Alcalinity of ash', 'Magnesium', 'Total phenols', 11: 'Flavanoids', 'Nonflavanoid phenols', 'Proanthocyanins', 12: 'Color intensity', 'Hue', 'OD280/OD315 of diluted wines', 13: 'Proline'] 14: 15: train_len = int(len(df_wine) * 0.7) 16: 17: # 打亂資料集順序 18: idx = list(df_wine.index) 19: random.shuffle(idx) 20: 21: # 分割資料集 22: TrainSet = df_wine.loc[idx[:train_len]] 23: TestSet = df_wine.loc[idx[train_len:]] 24: print(len(TrainSet)) 25: print(len(TestSet)) 26: X_train, y_train = TrainSet.iloc[:, 1:].values, TrainSet.iloc[:, 0].values 27: X_test, y_test = TestSet.iloc[:, 1:].values, TestSet.iloc[:, 0].values 28: 29: print('==========訓練集==========') 30: print(X_train[:2]) 31: print(y_train[:2]) 32: print('==========測試集==========') 33: print(X_test[:2]) 34: print(y_test[:2])
124 54 ==========訓練集========== [[1.229e+01 2.830e+00 2.220e+00 1.800e+01 8.800e+01 2.450e+00 2.250e+00 2.500e-01 1.990e+00 2.150e+00 1.150e+00 3.300e+00 2.900e+02] [1.340e+01 4.600e+00 2.860e+00 2.500e+01 1.120e+02 1.980e+00 9.600e-01 2.700e-01 1.110e+00 8.500e+00 6.700e-01 1.920e+00 6.300e+02]] [2 3] ==========測試集========== [[1.394e+01 1.730e+00 2.270e+00 1.740e+01 1.080e+02 2.880e+00 3.540e+00 3.200e-01 2.080e+00 8.900e+00 1.120e+00 3.100e+00 1.260e+03] [1.402e+01 1.680e+00 2.210e+00 1.600e+01 9.600e+01 2.650e+00 2.330e+00 2.600e-01 1.980e+00 4.700e+00 1.040e+00 3.590e+00 1.035e+03]] [1 1]
8.2.2.2. 呼叫scikit learn的function
1: import pandas as pd 2: import numpy as np 3: from sklearn.model_selection import train_test_split 4: 5: df_wine = pd.read_csv('https://archive.ics.uci.edu/' 6: 'ml/machine-learning-databases/wine/wine.data', 7: header=None) 8: 9: df_wine.columns = ['Class label', 'Alcohol', 'Malic acid', 'Ash', 10: 'Alcalinity of ash', 'Magnesium', 'Total phenols', 11: 'Flavanoids', 'Nonflavanoid phenols', 'Proanthocyanins', 12: 'Color intensity', 'Hue', 'OD280/OD315 of diluted wines', 13: 'Proline'] 14: 15: print('Class labels', np.unique(df_wine['Class label'])) 16: 17: X, y = df_wine.iloc[:, 1:].values, df_wine.iloc[:, 0].values 18: 19: from sklearn.model_selection import train_test_split 20: X_train, X_test, y_train, y_test = train_test_split(X, y, 21: test_size=0.3, random_state=0, stratify=y) 22: 23: print(len(X_train)) 24: print(len(y_test)) 25: 26: print('==========訓練集==========') 27: print(X_train[:2]) 28: print(y_train[:2]) 29: print('==========測試集==========') 30: print(X_test[:2]) 31: print(y_test[:2])
Class labels [1 2 3] 124 54 ==========訓練集========== [[1.362e+01 4.950e+00 2.350e+00 2.000e+01 9.200e+01 2.000e+00 8.000e-01 4.700e-01 1.020e+00 4.400e+00 9.100e-01 2.050e+00 5.500e+02] [1.376e+01 1.530e+00 2.700e+00 1.950e+01 1.320e+02 2.950e+00 2.740e+00 5.000e-01 1.350e+00 5.400e+00 1.250e+00 3.000e+00 1.235e+03]] [3 1] ==========測試集========== [[1.377e+01 1.900e+00 2.680e+00 1.710e+01 1.150e+02 3.000e+00 2.790e+00 3.900e-01 1.680e+00 6.300e+00 1.130e+00 2.930e+00 1.375e+03] [1.217e+01 1.450e+00 2.530e+00 1.900e+01 1.040e+02 1.890e+00 1.750e+00 4.500e-01 1.030e+00 2.950e+00 1.450e+00 2.230e+00 3.550e+02]] [1 2]