機(jī)器學(xué)習(xí)實(shí)戰(zhàn) | Python機(jī)器學(xué)習(xí)算法應(yīng)用實(shí)踐

引言

本篇文章希望帶大家完整走一遍機(jī)器學(xué)習(xí)應(yīng)用流程桩盲,我們會(huì)講解到基于Python的機(jī)器學(xué)習(xí)算法添寺,應(yīng)用在結(jié)構(gòu)化數(shù)據(jù)和非結(jié)構(gòu)化數(shù)據(jù)(圖像)上防症,希望通過文章內(nèi)容幫助大家在案例中重溫機(jī)器學(xué)習(xí)基礎(chǔ)知識(shí),并學(xué)習(xí)應(yīng)用機(jī)器學(xué)習(xí)解決問題的基本流程扩淀。

文章中會(huì)用到下述兩個(gè)庫(kù)來實(shí)現(xiàn)機(jī)器學(xué)習(xí)算法:

在本篇文章中,我們將講解到以下內(nèi)容:

  • 問題抽象與理解
  • 數(shù)據(jù)準(zhǔn)備與處理(預(yù)處理胜臊、特征提取勺卢、特征工程等)
  • 各種機(jī)器學(xué)習(xí)算法
  • 實(shí)驗(yàn)結(jié)果分析與對(duì)比
  • 模型選擇與調(diào)優(yōu)

我們會(huì)覆蓋到的機(jī)器學(xué)習(xí)算法包括:KNN、樸素貝葉斯象对、邏輯回歸黑忱、SVM、決策樹勒魔、隨機(jī)森林杨何、感知機(jī)、前饋神經(jīng)網(wǎng)絡(luò)沥邻、卷積神經(jīng)網(wǎng)絡(luò)。

1.環(huán)境準(zhǔn)備

工欲善其事必先利其器羊娃,我們先安裝一下必需的Python庫(kù)(當(dāng)然我們也推薦大家用集成環(huán)境anaconda唐全,具體的安裝與設(shè)置可以參考ShowMeAI文章 圖解Python | 安裝與環(huán)境設(shè)置 完成):

可以采用pip安裝酱固,命令如下:
$ pip install numpy
$ pip install pillow
$ pip install --upgrade scikit-learn
$ pip install tensorflow # or tensorflow-gpu
$ pip install keras
$ pip install opencv-contrib-python
$ pip install --upgrade imutils

2.數(shù)據(jù)集

因?yàn)楸酒恼挛覀兘榻B結(jié)構(gòu)化數(shù)據(jù)和非結(jié)構(gòu)化數(shù)據(jù)的不同建模械念,我們這里用到兩個(gè)數(shù)據(jù)集。

2.1 iris(鳶尾花)數(shù)據(jù)集

第一個(gè)數(shù)據(jù)集是iris(鳶尾花)數(shù)據(jù)集运悲,它是一個(gè)入門級(jí)數(shù)據(jù)集龄减。整個(gè)數(shù)據(jù)集都是數(shù)值型的數(shù)據(jù),是一個(gè)結(jié)構(gòu)化的表格數(shù)據(jù)班眯,每一行代表一個(gè)樣本希停,然后每一列就是不同的屬性烁巫。

這個(gè)數(shù)據(jù)集主要是收集了三種不同的鳶尾花的數(shù)據(jù),分別為:

  • Iris Setosa
  • Iris Versicolor
  • Iris Virginica

對(duì)應(yīng)圖中最后一列Class label宠能,然后還有四種屬性腐晾,分別是:

  • Sepal length:萼片長(zhǎng)度
  • Sepal width:萼片寬度
  • Petal length:花瓣長(zhǎng)度
  • Petal width:花瓣寬度

對(duì)于該數(shù)據(jù)集,我們的目標(biāo)就是根據(jù)給定的四個(gè)屬性动看,訓(xùn)練一個(gè)機(jī)器學(xué)習(xí)模型來正確分類每個(gè)樣本的類別匠童,這是一個(gè)典型的分類任務(wù)。

2.2 圖像數(shù)據(jù)集

第二個(gè)數(shù)據(jù)集是一個(gè)圖像數(shù)據(jù)集亦歉。它包括海岸線(Coast)恤浪、森林(Forest)和高速公路(Highway)三種場(chǎng)景,總共是 948 張圖片肴楷,我們需要構(gòu)建模型完成類別的分類水由,每個(gè)類別的具體圖片數(shù)量如下:

  • 海岸線(Coast):360
  • 森林(Forest):328
  • 高速公路(Highway):260

3.機(jī)器學(xué)習(xí)應(yīng)用步驟

我們?cè)诓煌瑘?chǎng)景下應(yīng)用機(jī)器學(xué)習(xí)算法,都有大致的步驟赛蔫,比如下面是一個(gè)典型的機(jī)器學(xué)習(xí)應(yīng)用流程:

當(dāng)然砂客,并不是其中的每一步都是必須的,我們也可能會(huì)調(diào)整其中某些步驟中的細(xì)節(jié)呵恢。

3.1 問題抽象與理解

針對(duì)我們的問題鞠值,問一下自己:

  • 數(shù)據(jù)集是哪種類型?數(shù)值型渗钉,類別型還是圖像彤恶?
  • 模型的最終目標(biāo)是什么?
  • 如何定義和衡量“準(zhǔn)確率”呢鳄橘?
  • 以目前自身的機(jī)器學(xué)習(xí)知識(shí)來看声离,哪些算法在處理這類問題上效果很好?

前序問題比較簡(jiǎn)單瘫怜,最后的問題术徊,隨著大家應(yīng)用機(jī)器學(xué)習(xí)解決問題的經(jīng)驗(yàn)積累,可以更準(zhǔn)確快速地回答鲸湃。

3.2 數(shù)據(jù)準(zhǔn)備與處理

數(shù)據(jù)準(zhǔn)備與處理赠涮,包括數(shù)據(jù)預(yù)處理以及特征工程了。一般這一步唤锉,包括了加載數(shù)據(jù)世囊、檢查數(shù)據(jù)、探索性數(shù)據(jù)分析(EDA)窿祥、數(shù)據(jù)預(yù)處理株憾,進(jìn)而決定需要做的特征提取或者特征工程。

特征提取是應(yīng)用某種算法通過某種方式來量化數(shù)據(jù)的過程。比如嗤瞎,對(duì)于圖像數(shù)據(jù)墙歪,我們可以采用計(jì)算直方圖的方法來統(tǒng)計(jì)圖像中像素強(qiáng)度的分布,通過這種方式贝奇,我們就得到描述圖像顏色的特征虹菲。

特征工程是將原始輸入數(shù)據(jù)轉(zhuǎn)換成一個(gè)更好描述潛在問題的特征表示的過程。大家可以查看ShowMeAI機(jī)器學(xué)習(xí)專題文章 系統(tǒng)了解特征工程的常見方法掉瞳。

3.3 多模型應(yīng)用

下一步可以選擇各種候選機(jī)器學(xué)習(xí)算法毕源,并應(yīng)用在數(shù)據(jù)集上。我們安裝的工具包內(nèi)陕习,包含很多機(jī)器學(xué)習(xí)算法霎褐,比如下述模型都可以用作分類:

  • 線性模型(邏輯回歸、線性SVM)
  • 非線性模型(RBF该镣、SVM冻璃、梯度下降分類器)
  • 樹和基于集成的模型(決策樹、隨機(jī)森林)
  • 神經(jīng)網(wǎng)絡(luò)(多層感知機(jī)损合、卷積神經(jīng)網(wǎng)絡(luò))

對(duì)于模型選擇省艳,當(dāng)然很多需要依據(jù)實(shí)驗(yàn)效果來定,但我們也有一些先序的經(jīng)驗(yàn)嫁审,比如:

  • 對(duì)于稠密型多特征的數(shù)據(jù)集跋炕,隨機(jī)森林算法的效果很不錯(cuò);
  • 邏輯回歸算法可以很好處理高維度的稀疏數(shù)據(jù)律适;
  • 對(duì)于圖像數(shù)據(jù)枣购,CNNs的效果非常好。

下圖為 scikit-learn工具庫(kù) 官方給的一個(gè)模型選擇思路參考:

4.構(gòu)建機(jī)器學(xué)習(xí)流程并實(shí)驗(yàn)分析

我們構(gòu)建如下的代碼文件目錄擦耀,包含四個(gè)代碼文件和一個(gè)3scenes圖像文件夾(內(nèi)含三場(chǎng)景數(shù)據(jù)集),iris數(shù)據(jù)集無需另外存儲(chǔ)涩堤,直接采用scikit-learn庫(kù)載入即可眷蜓。

├── 3scenes
│   ├── coast [360 entries]
│   ├── forest [328 entries]
│   └── highway [260 entries]
├── iris_classifier.py
├── image_classifier.py
├── nn_iris.py
└── basic_cnn.py

4.1 結(jié)構(gòu)化數(shù)據(jù)建模

首先實(shí)現(xiàn)iris_classifier,我們這里直接使用sklearn的機(jī)器學(xué)習(xí)算法來對(duì)iris數(shù)據(jù)集進(jìn)行分類胎围。

# 導(dǎo)入需要的庫(kù)
from sklearn.neighbors import KNeighborsClassifier
from sklearn.naive_bayes import GaussianNB
from sklearn.linear_model import LogisticRegression
from sklearn.svm import SVC
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.neural_network import MLPClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
from sklearn.datasets import load_iris
import argparse

# 設(shè)置參數(shù)
ap = argparse.ArgumentParser()
ap.add_argument("-m", "--model", type=str, default="knn", help="type of python machine learning model to use")
args = vars(ap.parse_args())

# 定義一個(gè)保存模型的字典吁系,根據(jù) key 來選擇加載哪個(gè)模型
models = {
    "knn": KNeighborsClassifier(n_neighbors=1),
    "naive_bayes": GaussianNB(),
    "logit": LogisticRegression(solver="lbfgs", multi_class="auto"),
    "svm": SVC(kernel="rbf", gamma="auto"),
    "decision_tree": DecisionTreeClassifier(),
    "random_forest": RandomForestClassifier(n_estimators=100),
    "mlp": MLPClassifier()
}

其中,models從前往后依次包括這些算法:KNN白魂、樸素貝葉斯汽纤、邏輯回歸、SVM福荸、決策樹蕴坪、隨機(jī)森林、感知機(jī)等。

我們直接調(diào)用sklearn中相應(yīng)的函數(shù)來實(shí)現(xiàn)對(duì)應(yīng)的算法即可背传,這里直接用一個(gè)models的字典來保存不同模型的初始化呆瞻,然后根據(jù)參數(shù)--model來調(diào)用對(duì)應(yīng)的模型,比如命令輸入python iris_classifier.py --model knn就是調(diào)用knn算法模型径玖。

接著就是載入數(shù)據(jù)部分:

print("加載數(shù)據(jù)中...")
dataset = load_iris()
trainX, testX, trainY, testY = train_test_split(dataset.data, dataset.target, random_state=3, test_size=0.2)

這里直接調(diào)用sklearn.datasets中的load_iris()載入數(shù)據(jù)痴脾,然后采用train_test_split來劃分訓(xùn)練集和數(shù)據(jù)集,這里是80%數(shù)據(jù)作為訓(xùn)練集梳星,20%作為測(cè)試集赞赖。

最后就是訓(xùn)練模型和預(yù)測(cè)部分:

# 訓(xùn)練模型
print("應(yīng)用 '{}' 模型建模...".format(args["model"]))
model = models[args["model"]]
model.fit(trainX, trainY)

# 預(yù)測(cè)并輸出一份分類結(jié)果報(bào)告
print("評(píng)估模型效果...")
predictions = model.predict(testX)
print(classification_report(testY, predictions, target_names=dataset.target_names))

4.2 圖像數(shù)據(jù)建模

類似的過程對(duì)三場(chǎng)景圖像數(shù)據(jù)集構(gòu)建代碼image_classifier.py

# 導(dǎo)入工具庫(kù)
from sklearn.preprocessing import LabelEncoder
from PIL import Image
from imutils import paths
import numpy as np
import os

其中LabelEncoder是為了將標(biāo)簽從字符串編碼為整型,然后其余幾項(xiàng)都是處理圖像相關(guān)冤灾。

對(duì)于圖像數(shù)據(jù)前域,如果直接采用原始像素信息輸入模型中,大部分的機(jī)器學(xué)習(xí)算法效果都很不理想瞳购,所以這里采用特征提取方法话侄,主要是統(tǒng)計(jì)圖像顏色通道的均值和標(biāo)準(zhǔn)差信息,總共是RGB3個(gè)通道学赛,每個(gè)通道各計(jì)算均值和標(biāo)準(zhǔn)差年堆,然后結(jié)合在一起,得到一個(gè)六維的特征盏浇,函數(shù)如下所示:

def extract_color_stats(image):
    '''
    將圖片分成 RGB 三通道变丧,然后分別計(jì)算每個(gè)通道的均值和標(biāo)準(zhǔn)差,然后返回
    :param image:
    :return:
    '''
    (R, G, B) = image.split()
    features = [np.mean(R), np.mean(G), np.mean(B), np.std(R), np.std(G), np.std(B)]

    return features

然后同樣會(huì)定義一個(gè)models字典绢掰,代碼一樣痒蓬,這里就不貼出來了,然后圖像載入部分的代碼如下:

# 加載數(shù)據(jù)并提取特征
print("抽取圖像特征中...")
imagePaths = paths.list_images(args['dataset'])
data = []
labels = []

# 循環(huán)遍歷所有的圖片數(shù)據(jù)
for imagePath in imagePaths:
    # 加載圖片滴劲,然后計(jì)算圖片的顏色通道統(tǒng)計(jì)信息
    image = Image.open(imagePath)
    features = extract_color_stats(image)
    data.append(features)

    # 保存圖片的標(biāo)簽信息
    label = imagePath.split(os.path.sep)[-2]
    labels.append(label)

# 對(duì)標(biāo)簽進(jìn)行編碼攻晒,從字符串變?yōu)檎麛?shù)類型
le = LabelEncoder()
labels = le.fit_transform(labels)

# 進(jìn)行訓(xùn)練集和測(cè)試集的劃分,80%數(shù)據(jù)作為訓(xùn)練集班挖,其余20%作為測(cè)試集
trainX, testX, trainY, testY = train_test_split(data, labels, test_size=0.2)

上述代碼就完成加載圖片的路徑信息鲁捏,然后依次遍歷,讀取圖片萧芙,提取特征给梅,提取標(biāo)簽信息,保存特征和標(biāo)簽信息双揪,接著編碼標(biāo)簽动羽,然后就是劃分訓(xùn)練集和測(cè)試集。

接著是相同的訓(xùn)練模型和預(yù)測(cè)的代碼渔期,和前面的分類器一樣运吓。完整版代碼如下:

# 導(dǎo)入工具庫(kù)
from sklearn.neighbors import KNeighborsClassifier
from sklearn.naive_bayes import GaussianNB
from sklearn.linear_model import LogisticRegression
from sklearn.svm import SVC
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.neural_network import MLPClassifier
from sklearn.preprocessing import LabelEncoder
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
from PIL import Image
from imutils import paths
import numpy as np
import argparse
import os

# 抽取圖像特征
def extract_color_stats(image):
    '''
    將圖片分成 RGB 三通道,然后分別計(jì)算每個(gè)通道的均值和標(biāo)準(zhǔn)差,然后返回
    :param image:
    :return:
    '''
    (R, G, B) = image.split()
    features = [np.mean(R), np.mean(G), np.mean(B), np.std(R), np.std(G), np.std(B)]

    return features


# 設(shè)置參數(shù)
ap = argparse.ArgumentParser()
ap.add_argument("-d", "--dataset", type=str, default="3scenes",
                help="path to directory containing the '3scenes' dataset")
ap.add_argument("-m", "--model", type=str, default="knn",
                help="type of python machine learning model to use")
args = vars(ap.parse_args())

# 定義一個(gè)保存模型的字典羽德,根據(jù) key 來選擇加載哪個(gè)模型
models = {
    "knn": KNeighborsClassifier(n_neighbors=1),
    "naive_bayes": GaussianNB(),
    "logit": LogisticRegression(solver="lbfgs", multi_class="auto"),
    "svm": SVC(kernel="rbf", gamma="auto"),
    "decision_tree": DecisionTreeClassifier(),
    "random_forest": RandomForestClassifier(n_estimators=100),
    "mlp": MLPClassifier()
}

# 加載數(shù)據(jù)并提取特征
print("抽取圖像特征中...")
imagePaths = paths.list_images(args['dataset'])
data = []
labels = []

# 循環(huán)遍歷所有的圖片數(shù)據(jù)
for imagePath in imagePaths:
    # 加載圖片几莽,然后計(jì)算圖片的顏色通道統(tǒng)計(jì)信息
    image = Image.open(imagePath)
    features = extract_color_stats(image)
    data.append(features)

    # 保存圖片的標(biāo)簽信息
    label = imagePath.split(os.path.sep)[-2]
    labels.append(label)

# 對(duì)標(biāo)簽進(jìn)行編碼,從字符串變?yōu)檎麛?shù)類型
le = LabelEncoder()
labels = le.fit_transform(labels)

# 進(jìn)行訓(xùn)練集和測(cè)試集的劃分宅静,80%數(shù)據(jù)作為訓(xùn)練集章蚣,其余20%作為測(cè)試集
trainX, testX, trainY, testY = train_test_split(data, labels, random_state=3, test_size=0.2)
# print('trainX numbers={}, testX numbers={}'.format(len(trainX), len(testX)))

# 訓(xùn)練模型
print("[應(yīng)用 '{}' 模型建模".format(args["model"]))
model = models[args["model"]]
model.fit(trainX, trainY)

# 預(yù)測(cè)并輸出分類結(jié)果報(bào)告
print("模型評(píng)估")
predictions = model.predict(testX)
print(classification_report(testY, predictions, target_names=le.classes_))

完成這兩份代碼后,我們就可以開始運(yùn)行下代碼姨夹,對(duì)比不同算法在兩個(gè)數(shù)據(jù)集上的性能纤垂。

4.3 不同模型建模對(duì)比

(1) KNN

K-Nearest Neighbors分類器最簡(jiǎn)單的分類算法之一。該算法依賴于特征向量之間的距離磷账。簡(jiǎn)單地說峭沦,KNN算法通過在k個(gè)最接近的樣本中最多的類別來對(duì)未知數(shù)據(jù)點(diǎn)進(jìn)行分類。關(guān)于KNN的詳細(xì)講解可以閱讀ShowMeAI的文章 圖解機(jī)器學(xué)習(xí) | KNN算法及其應(yīng)用逃糟。

這里我們先運(yùn)行下image_classifier.py吼鱼,調(diào)用默認(rèn)的模型knn,看下KNNiris數(shù)據(jù)集上的實(shí)驗(yàn)結(jié)果绰咽,如下所示:

$ !python iris_classifier.py --model knn
加載數(shù)據(jù)中...
應(yīng)用 'knn' 模型建模...
評(píng)估模型效果...
              precision    recall  f1-score   support

      setosa       1.00      1.00      1.00        10
  versicolor       0.90      0.90      0.90        10
   virginica       0.90      0.90      0.90        10

    accuracy                           0.93        30
   macro avg       0.93      0.93      0.93        30
weighted avg       0.93      0.93      0.93        30

其中主要是給出了對(duì)每個(gè)類別的精確率菇肃、召回率、F1以及該類別測(cè)試集數(shù)量取募,即分別對(duì)應(yīng)precision琐谤、recall、f1-score玩敏、support斗忌。根據(jù)最后一行第一列,可以看到KNN取得93%的準(zhǔn)確率旺聚。

接著是在三場(chǎng)景圖片數(shù)據(jù)集上的實(shí)驗(yàn)結(jié)果:

$ !python image_classifier.py --model knn
抽取圖像特征中...
應(yīng)用 'knn' 模型建模...
評(píng)估模型效果...
              precision    recall  f1-score   support

       coast       0.84      0.68      0.75       105
      forest       0.78      0.77      0.77        78
     highway       0.56      0.78      0.65        54

   micro avg       0.73      0.73      0.73       237
   macro avg       0.72      0.74      0.72       237
weighted avg       0.75      0.73      0.73       237

這里KNN取得75%的準(zhǔn)確率织阳。

ps:實(shí)際上,運(yùn)行這個(gè)算法砰粹,不同次數(shù)會(huì)有不同的結(jié)果陈哑,其主要原因是因?yàn)樵趧澐钟?xùn)練集和測(cè)試集的時(shí)候,代碼沒有設(shè)置參數(shù)random_state伸眶,這導(dǎo)致每次運(yùn)行劃分的訓(xùn)練集和測(cè)試集的圖片都是不同的,所以運(yùn)行結(jié)果也會(huì)不相同刽宪!

(2) 樸素貝葉斯

接著是樸素貝葉斯算法厘贼,關(guān)于樸素貝葉斯算法的詳細(xì)講解可以閱讀ShowMeAI的文章 圖解機(jī)器學(xué)習(xí) | 樸素貝葉斯算法詳解

分別測(cè)試兩個(gè)數(shù)據(jù)集圣拄,結(jié)果如下:

$ !python iris_classifier.py --model naive_bayes
加載數(shù)據(jù)中...
應(yīng)用 'naive_bayes' 模型建模...
評(píng)估模型效果...
              precision    recall  f1-score   support

      setosa       1.00      1.00      1.00        15
  versicolor       1.00      0.92      0.96        12
   virginica       0.92      1.00      0.96        11

   micro avg       0.97      0.97      0.97        38
   macro avg       0.97      0.97      0.97        38
weighted avg       0.98      0.97      0.97        38
$ !python image_classifier.py --model naive_bayes
抽取圖像特征中...
應(yīng)用 'naive_bayes' 模型建模...
評(píng)估模型效果...
              precision    recall  f1-score   support

       coast       0.69      0.40      0.50        88
      forest       0.68      0.82      0.74        84
     highway       0.61      0.78      0.68        65

   micro avg       0.65      0.65      0.65       237
   macro avg       0.66      0.67      0.64       237
weighted avg       0.66      0.65      0.64       237

同樣嘴秸,樸素貝葉斯在iris上有98%的準(zhǔn)確率,但是在圖像數(shù)據(jù)集上僅有66%的準(zhǔn)確率。

那么岳掐,我們是否可以說明KNN算法比樸素貝葉斯好呢凭疮?當(dāng)然是不可以的,上述結(jié)果只能說明在三場(chǎng)景圖像數(shù)據(jù)集上串述,KNN算法優(yōu)于樸素貝葉斯算法执解。
實(shí)際上,每種算法都有各自的優(yōu)缺點(diǎn)和適用場(chǎng)景纲酗,不能一概而論地說某種算法任何時(shí)候都優(yōu)于另一種算法衰腌,這需要具體問題具體分析。

(3) 邏輯回歸

接著是邏輯回歸算法觅赊,關(guān)于邏輯回歸算法的詳細(xì)講解可以閱讀ShowMeAI的文章 圖解機(jī)器學(xué)習(xí) | 邏輯回歸算法詳解右蕊。

分別測(cè)試兩個(gè)數(shù)據(jù)集,結(jié)果如下:

$ !python iris_classifier.py --model logit
加載數(shù)據(jù)中...
應(yīng)用 'logit' 模型建模...
評(píng)估模型效果...
              precision    recall  f1-score   support

      setosa       1.00      1.00      1.00        15
  versicolor       1.00      0.92      0.96        12
   virginica       0.92      1.00      0.96        11

   micro avg       0.97      0.97      0.97        38
   macro avg       0.97      0.97      0.97        38
weighted avg       0.98      0.97      0.97        38
$ !python image_classifier.py --model logit
抽取圖像特征中...
應(yīng)用 'logit' 模型建模...
評(píng)估模型效果...
              precision    recall  f1-score   support

       coast       0.67      0.67      0.67        92
      forest       0.79      0.82      0.80        82
     highway       0.61      0.57      0.59        63

   micro avg       0.70      0.70      0.70       237
   macro avg       0.69      0.69      0.69       237
weighted avg       0.69      0.70      0.69       237

同樣吮螺,邏輯回歸在 iris 上有 98% 的準(zhǔn)確率饶囚,但是在圖像數(shù)據(jù)集上僅有 69% 的準(zhǔn)確率

(4) 支持向量機(jī) SVM

接著是SVM算法,關(guān)于SVM算法的詳細(xì)講解可以閱讀ShowMeAI的文章 圖解機(jī)器學(xué)習(xí) | 支持向量機(jī)模型詳解鸠补。

分別測(cè)試兩個(gè)數(shù)據(jù)集萝风,結(jié)果如下:

$ !python iris_classifier.py --model svm
加載數(shù)據(jù)中...
應(yīng)用 'svm' 模型建模...
評(píng)估模型效果...
              precision    recall  f1-score   support

      setosa       1.00      1.00      1.00        15
  versicolor       1.00      0.92      0.96        12
   virginica       0.92      1.00      0.96        11

   micro avg       0.97      0.97      0.97        38
   macro avg       0.97      0.97      0.97        38
weighted avg       0.98      0.97      0.97        38
$ !python image_classifier.py --model svm
抽取圖像特征中...
應(yīng)用 'svm' 模型建模...
評(píng)估模型效果...
              precision    recall  f1-score   support

       coast       0.84      0.76      0.80        92
      forest       0.86      0.93      0.89        84
     highway       0.78      0.80      0.79        61

   micro avg       0.83      0.83      0.83       237
   macro avg       0.83      0.83      0.83       237

同樣,SVM在iris上有98%的準(zhǔn)確率莫鸭,但是在圖像數(shù)據(jù)集上僅有83%的準(zhǔn)確率闹丐。

(5) 決策樹

接著是決策樹算法,關(guān)于決策樹算法的詳細(xì)講解可以閱讀ShowMeAI的文章 圖解機(jī)器學(xué)習(xí) | 決策樹模型詳解被因。

分別測(cè)試兩個(gè)數(shù)據(jù)集卿拴,結(jié)果如下:

$ !python iris_classifier.py --model decision_tree
加載數(shù)據(jù)中...
應(yīng)用 'decision_tree' 模型建模...
評(píng)估模型效果...
              precision    recall  f1-score   support

      setosa       1.00      1.00      1.00        15
  versicolor       0.92      0.92      0.92        12
   virginica       0.91      0.91      0.91        11

   micro avg       0.95      0.95      0.95        38
   macro avg       0.94      0.94      0.94        38
weighted avg       0.95      0.95      0.95        38
$ !python image_classifier.py --model decision_tree
抽取圖像特征中...
應(yīng)用 'decision_tree' 模型建模...
評(píng)估模型效果...
              precision    recall  f1-score   support

       coast       0.71      0.74      0.72        85
      forest       0.76      0.80      0.78        83
     highway       0.77      0.68      0.72        69

   micro avg       0.74      0.74      0.74       237
   macro avg       0.75      0.74      0.74       237
weighted avg       0.74      0.74      0.74       237

同樣,決策樹在iris上有98%的準(zhǔn)確率梨与,但是在圖像數(shù)據(jù)集上僅有74%的準(zhǔn)確率堕花。

(6) 隨機(jī)森林

接著是隨機(jī)森林算法,關(guān)于隨機(jī)森林算法的詳細(xì)講解可以閱讀ShowMeAI的文章 圖解機(jī)器學(xué)習(xí) | 隨機(jī)森林分類模型詳解粥鞋。

分別測(cè)試兩個(gè)數(shù)據(jù)集缘挽,結(jié)果如下:

$ !python iris_classifier.py --model random_forest
加載數(shù)據(jù)中...
應(yīng)用 'random_forest' 模型建模...
評(píng)估模型效果...
              precision    recall  f1-score   support

      setosa       1.00      1.00      1.00        15
  versicolor       1.00      0.83      0.91        12
   virginica       0.85      1.00      0.92        11

   micro avg       0.95      0.95      0.95        38
   macro avg       0.95      0.94      0.94        38
weighted avg       0.96      0.95      0.95        38
$ !python image_classifier.py --model random_forest
加載數(shù)據(jù)中...
應(yīng)用 'random_forest' 模型建模...
評(píng)估模型效果...
              precision    recall  f1-score   support

       coast       0.80      0.83      0.81        84
      forest       0.92      0.84      0.88        90
     highway       0.77      0.81      0.79        63

   micro avg       0.83      0.83      0.83       237
   macro avg       0.83      0.83      0.83       237
weighted avg       0.84      0.83      0.83       237

同樣,隨機(jī)森林在iris上有96%的準(zhǔn)確率呻粹,但是在圖像數(shù)據(jù)集上僅有84%的準(zhǔn)確率壕曼。

注意了,一般如果決策樹算法的效果還不錯(cuò)的話等浊,隨機(jī)森林算法應(yīng)該也會(huì)取得不錯(cuò)甚至更好的結(jié)果腮郊,這是因?yàn)殡S機(jī)森林實(shí)際上就是多棵決策樹通過集成學(xué)習(xí)方法組合在一起進(jìn)行分類預(yù)測(cè)。

(7) 多層感知機(jī)

最后是多層感知機(jī)算法筹燕,分別測(cè)試兩個(gè)數(shù)據(jù)集轧飞,結(jié)果如下:

$ !python iris_classifier.py --model mlp
加載數(shù)據(jù)中...
應(yīng)用 'mlp' 模型建模...
評(píng)估模型效果...
              precision    recall  f1-score   support

      setosa       1.00      1.00      1.00        15
  versicolor       1.00      0.92      0.96        12
   virginica       0.92      1.00      0.96        11

   micro avg       0.97      0.97      0.97        38
   macro avg       0.97      0.97      0.97        38
weighted avg       0.98      0.97      0.97        38
$ !python image_classifier.py --model mlp
抽取圖像特征中...
應(yīng)用 'mlp' 模型建模...
評(píng)估模型效果...
              precision    recall  f1-score   support

       coast       0.72      0.91      0.80        86
      forest       0.92      0.89      0.90        79
     highway       0.79      0.58      0.67        72

   micro avg       0.80      0.80      0.80       237
   macro avg       0.81      0.79      0.79       237
weighted avg       0.81      0.80      0.80       237

同樣衅鹿,多層感知機(jī)在 iris 上有 98% 的準(zhǔn)確率,但是在圖像數(shù)據(jù)集上僅有 81% 的準(zhǔn)確率.

(8) 神經(jīng)網(wǎng)絡(luò)

最后是實(shí)現(xiàn)深度學(xué)習(xí)的算法过咬,也就是nn_iris.pybasic_cnn.py這兩份代碼大渤。

首先是nn_iris.py的實(shí)現(xiàn),同樣首先是導(dǎo)入庫(kù)和數(shù)據(jù)的處理:

# 導(dǎo)入工具庫(kù)
from keras.models import Sequential
from keras.layers.core import Dense
from keras.optimizers import SGD
from sklearn.preprocessing import LabelBinarizer
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
from sklearn.datasets import load_iris

# 載入 Iris 數(shù)據(jù)集掸绞,然后進(jìn)行訓(xùn)練集和測(cè)試集的劃分泵三,80%數(shù)據(jù)作為訓(xùn)練集,其余20%作為測(cè)試集
print("加載數(shù)據(jù)中...")
dataset = load_iris()
(trainX, testX, trainY, testY) = train_test_split(dataset.data,
                                                  dataset.target, test_size=0.2)

# 將標(biāo)簽進(jìn)行獨(dú)熱向量編碼
lb = LabelBinarizer()
trainY = lb.fit_transform(trainY)
testY = lb.transform(testY)

我們采用Keras來實(shí)現(xiàn)神經(jīng)網(wǎng)絡(luò)集漾,然后這里需要將標(biāo)簽進(jìn)行one-hot編碼切黔,即獨(dú)熱向量編碼。

接著就是搭建網(wǎng)絡(luò)模型的結(jié)構(gòu)和訓(xùn)練具篇、預(yù)測(cè)代碼:

# 利用 Keras 定義網(wǎng)絡(luò)模型
model = Sequential()
model.add(Dense(3, input_shape=(4,), activation="sigmoid"))
model.add(Dense(3, activation="sigmoid"))
model.add(Dense(3, activation="softmax"))

# 采用梯度下降訓(xùn)練模型
print('訓(xùn)練網(wǎng)絡(luò)中...')
opt = SGD(lr=0.1, momentum=0.9, decay=0.1 / 250)
model.compile(loss='categorical_crossentropy', optimizer=opt, metrics=["accuracy"])
H = model.fit(trainX, trainY, validation_data=(testX, testY), epochs=250, batch_size=16)

# 預(yù)測(cè)
print('評(píng)估模型效果')
predictions = model.predict(testX, batch_size=16)
print(classification_report(testY.argmax(axis=1), predictions.argmax(axis=1), target_names=dataset.target_names))

上述代碼構(gòu)建了3層全連接層的神經(jīng)網(wǎng)絡(luò)纬霞,前兩層采用Sigmoid激活函數(shù),然后最后一層是輸出層驱显,所以采用softmax將輸出變成概率值诗芜。優(yōu)化算法選擇的隨機(jī)梯度下降SGD,損失函數(shù)是categorical_crossentropy埃疫,迭代次數(shù)是250次伏恐,每一批次的數(shù)據(jù)量batch_size是16。

完整版代碼如下:

# 加載工具庫(kù)
from keras.models import Sequential
from keras.layers.core import Dense
from keras.optimizers import SGD
from sklearn.preprocessing import LabelBinarizer
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
from sklearn.datasets import load_iris

# 載入 Iris 數(shù)據(jù)集栓霜,然后進(jìn)行訓(xùn)練集和測(cè)試集的劃分翠桦,80%數(shù)據(jù)作為訓(xùn)練集,其余20%作為測(cè)試集
print("加載數(shù)據(jù)中...")
dataset = load_iris()
(trainX, testX, trainY, testY) = train_test_split(dataset.data,
                                                  dataset.target, test_size=0.2)

# 將標(biāo)簽進(jìn)行獨(dú)熱向量編碼
lb = LabelBinarizer()
trainY = lb.fit_transform(trainY)
testY = lb.transform(testY)

# 利用 Keras 定義網(wǎng)絡(luò)模型
model = Sequential()
model.add(Dense(3, input_shape=(4,), activation="sigmoid"))
model.add(Dense(3, activation="sigmoid"))
model.add(Dense(3, activation="softmax"))

# 采用梯度下降訓(xùn)練模型
print('訓(xùn)練網(wǎng)絡(luò)中...')
opt = SGD(lr=0.1, momentum=0.9, decay=0.1 / 250)
model.compile(loss='categorical_crossentropy', optimizer=opt, metrics=["accuracy"])
H = model.fit(trainX, trainY, validation_data=(testX, testY), epochs=250, batch_size=16)

# 預(yù)測(cè)
print('評(píng)估模型效果...')
predictions = model.predict(testX, batch_size=16)
print(classification_report(testY.argmax(axis=1), predictions.argmax(axis=1), target_names=dataset.target_names))

直接運(yùn)行命令python nn_iris.py胳蛮,輸出的結(jié)果如下:

$ python nn_iris.py 
Using TensorFlow backend.
加載數(shù)據(jù)中...
訓(xùn)練網(wǎng)絡(luò)中...
Train on 112 samples, validate on 38 samples
Epoch 1/250
2022-02-08 10:28:19.104933: I tensorflow/core/platform/cpu_feature_guard.cc:141] Your CPU supports instructions that this TensorFlow binary was not compiled to use: AVX2 AVX512F FMA
112/112 [==============================] - 0s 2ms/step - loss: 1.1454 - acc: 0.3214 - val_loss: 1.1867 - val_acc: 0.2368
Epoch 2/250
112/112 [==============================] - 0s 48us/step - loss: 1.0828 - acc: 0.3929 - val_loss: 1.2132 - val_acc: 0.5000
Epoch 3/250
112/112 [==============================] - 0s 47us/step - loss: 1.0491 - acc: 0.5268 - val_loss: 1.0593 - val_acc: 0.4737
...
Epoch 248/250
112/112 [==============================] - 0s 46us/step - loss: 0.1319 - acc: 0.9554 - val_loss: 0.0407 - val_acc: 1.0000
Epoch 249/250
112/112 [==============================] - 0s 46us/step - loss: 0.1024 - acc: 0.9643 - val_loss: 0.1595 - val_acc: 0.8947
Epoch 250/250
112/112 [==============================] - 0s 47us/step - loss: 0.0795 - acc: 0.9821 - val_loss: 0.0335 - val_acc: 1.0000
評(píng)估模型效果...
             precision    recall  f1-score   support

     setosa       1.00      1.00      1.00         9
 versicolor       1.00      1.00      1.00        10
  virginica       1.00      1.00      1.00        19

avg / total       1.00      1.00      1.00        38

這里得到的是100%的準(zhǔn)確率销凑。

(9) CNN

最后我們要應(yīng)用卷積神經(jīng)網(wǎng)絡(luò),我們實(shí)現(xiàn)一下basic_cnn.py代碼仅炊。

同樣首先是導(dǎo)入必須的庫(kù)函數(shù):

# 導(dǎo)入工具庫(kù)
from keras.models import Sequential
from keras.layers.convolutional import Conv2D
from keras.layers.convolutional import MaxPooling2D
from keras.layers.core import Activation
from keras.layers.core import Flatten
from keras.layers.core import Dense
from keras.optimizers import Adam
from sklearn.preprocessing import LabelBinarizer
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
from PIL import Image
from imutils import paths
import numpy as np
import argparse
import os

# 配置參數(shù)
ap = argparse.ArgumentParser()
ap.add_argument("-d", "--dataset", type=str, default="3scenes",
                help="path to directory containing the '3scenes' dataset")
args = vars(ap.parse_args())

同樣是要導(dǎo)入Keras來建立CNN的網(wǎng)絡(luò)模型斗幼,另外因?yàn)槭翘幚韴D像數(shù)據(jù),所以PIL抚垄、imutils也是要導(dǎo)入的蜕窿。

然后是加載數(shù)據(jù)和劃分訓(xùn)練集和測(cè)試集,對(duì)于加載數(shù)據(jù)呆馁,這里直接采用原始圖像像素?cái)?shù)據(jù)桐经,只需要對(duì)圖像數(shù)據(jù)做統(tǒng)一尺寸的調(diào)整,這里是統(tǒng)一調(diào)整為32×32浙滤,并做歸一化到[0,1]的范圍阴挣。

# 加載數(shù)據(jù)并提取特征
print("抽取圖像特征中...")
imagePaths = paths.list_images(args['dataset'])
data = []
labels = []

# 循環(huán)遍歷所有的圖片數(shù)據(jù)
for imagePath in imagePaths:
    # 加載圖片,然后調(diào)整成 32×32 大小瓷叫,并做歸一化到 [0,1]
    image = Image.open(imagePath)
    image = np.array(image.resize((32, 32))) / 255.0
    data.append(image)

    # 保存圖片的標(biāo)簽信息
    label = imagePath.split(os.path.sep)[-2]
    labels.append(label)

# 對(duì)標(biāo)簽編碼屯吊,從字符串變?yōu)檎?lb = LabelBinarizer()
labels = lb.fit_transform(labels)

# 劃分訓(xùn)練集和測(cè)試集
(trainX, testX, trainY, testY) = train_test_split(np.array(data), np.array(labels), test_size=0.25)

接著定義了一個(gè)4層的CNN網(wǎng)絡(luò)結(jié)構(gòu),包含3層卷積層和最后一層輸出層摹菠,優(yōu)化算法采用的是Adam而不是SGD盒卸。代碼如下所示:

# 定義 CNN 網(wǎng)絡(luò)模型結(jié)構(gòu)
model = Sequential()
model.add(Conv2D(8, (3, 3), padding="same", input_shape=(32, 32, 3)))
model.add(Activation("relu"))
model.add(MaxPooling2D(pool_size=(2, 2), strides=(2, 2)))
model.add(Conv2D(16, (3, 3), padding="same"))
model.add(Activation("relu"))
model.add(MaxPooling2D(pool_size=(2, 2), strides=(2, 2)))
model.add(Conv2D(32, (3, 3), padding="same"))
model.add(Activation("relu"))
model.add(MaxPooling2D(pool_size=(2, 2), strides=(2, 2)))
model.add(Flatten())
model.add(Dense(3))
model.add(Activation("softmax"))

# 訓(xùn)練模型
print("訓(xùn)練網(wǎng)絡(luò)中...")
opt = Adam(lr=1e-3, decay=1e-3 / 50)
model.compile(loss="categorical_crossentropy", optimizer=opt, metrics=["accuracy"])
H = model.fit(trainX, trainY, validation_data=(testX, testY),
              epochs=50, batch_size=32)

# 預(yù)測(cè)
print("評(píng)估模型效果...")
predictions = model.predict(testX, batch_size=32)
print(classification_report(testY.argmax(axis=1),
                            predictions.argmax(axis=1), target_names=lb.classes_))

運(yùn)行命令python basic_cnn.py,輸出結(jié)果如下:

$ python basic_cnn.py 
Using TensorFlow backend.
加載圖像數(shù)據(jù)...
訓(xùn)練網(wǎng)絡(luò)中...
Train on 711 samples, validate on 237 samples
Epoch 1/50
711/711 [==============================] - 0s 629us/step - loss: 1.0647 - acc: 0.4726 - val_loss: 0.9920 - val_acc: 0.5359
Epoch 2/50
711/711 [==============================] - 0s 313us/step - loss: 0.9200 - acc: 0.6188 - val_loss: 0.7778 - val_acc: 0.6624
Epoch 3/50
711/711 [==============================] - 0s 308us/step - loss: 0.6775 - acc: 0.7229 - val_loss: 0.5310 - val_acc: 0.7553
...
Epoch 48/50
711/711 [==============================] - 0s 307us/step - loss: 0.0627 - acc: 0.9887 - val_loss: 0.2426 - val_acc: 0.9283
Epoch 49/50
711/711 [==============================] - 0s 310us/step - loss: 0.0608 - acc: 0.9873 - val_loss: 0.2236 - val_acc: 0.9325
Epoch 50/50
711/711 [==============================] - 0s 307us/step - loss: 0.0587 - acc: 0.9887 - val_loss: 0.2525 - val_acc: 0.9114
評(píng)估模型效果...
             precision    recall  f1-score   support

      coast       0.85      0.96      0.90        85
     forest       0.99      0.94      0.97        88
    highway       0.91      0.80      0.85        64

avg / total       0.92      0.91      0.91       237

CNN的準(zhǔn)確率是達(dá)到92%次氨,它是優(yōu)于之前的幾種機(jī)器學(xué)習(xí)算法的結(jié)果蔽介。

5.小結(jié)

這篇簡(jiǎn)單的機(jī)器學(xué)習(xí)教程文章中,我們調(diào)用現(xiàn)有的庫(kù)來應(yīng)用對(duì)應(yīng)的機(jī)器學(xué)習(xí)算法煮寡,解決了2個(gè)簡(jiǎn)單的場(chǎng)景問題虹蓄。通過這份簡(jiǎn)單的入門教程,希望大家了解到:

(1) 沒有任何一種算法是完美的幸撕,可以完全適用所有的場(chǎng)景薇组,即便是目前很熱門的深度學(xué)習(xí)方法,也存在它的局限性坐儿,所以應(yīng)該具體問題具體分析律胀!

(2) 經(jīng)典的5步機(jī)器學(xué)習(xí)操作流程:

  • 問題抽象與理解
  • 數(shù)據(jù)準(zhǔn)備與處理(預(yù)處理、特征提取貌矿、特征工程等)
  • 各種機(jī)器學(xué)習(xí)算法
  • 實(shí)驗(yàn)結(jié)果分析與對(duì)比
  • 模型選擇與調(diào)優(yōu)

參考資料

作者:韓信子@ShowMeAI
教程地址http://www.showmeai.tech/tutorials/41
本文地址http://www.showmeai.tech/article-detail/201
聲明:版權(quán)所有炭菌,轉(zhuǎn)載請(qǐng)聯(lián)系平臺(tái)與作者并注明出處
收藏ShowMeAI查看更多精彩內(nèi)容

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市逛漫,隨后出現(xiàn)的幾起案子黑低,更是在濱河造成了極大的恐慌,老刑警劉巖酌毡,帶你破解...
    沈念sama閱讀 216,591評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件克握,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡阔馋,警方通過查閱死者的電腦和手機(jī)玛荞,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來呕寝,“玉大人勋眯,你說我怎么就攤上這事∠律遥” “怎么了客蹋?”我有些...
    開封第一講書人閱讀 162,823評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)孽江。 經(jīng)常有香客問我讶坯,道長(zhǎng),這世上最難降的妖魔是什么岗屏? 我笑而不...
    開封第一講書人閱讀 58,204評(píng)論 1 292
  • 正文 為了忘掉前任辆琅,我火速辦了婚禮漱办,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘婉烟。我一直安慰自己娩井,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,228評(píng)論 6 388
  • 文/花漫 我一把揭開白布似袁。 她就那樣靜靜地躺著洞辣,像睡著了一般。 火紅的嫁衣襯著肌膚如雪昙衅。 梳的紋絲不亂的頭發(fā)上扬霜,一...
    開封第一講書人閱讀 51,190評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音而涉,去河邊找鬼著瓶。 笑死,一個(gè)胖子當(dāng)著我的面吹牛婴谱,可吹牛的內(nèi)容都是我干的蟹但。 我是一名探鬼主播,決...
    沈念sama閱讀 40,078評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼谭羔,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼华糖!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起瘟裸,我...
    開封第一講書人閱讀 38,923評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤客叉,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后话告,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體兼搏,經(jīng)...
    沈念sama閱讀 45,334評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,550評(píng)論 2 333
  • 正文 我和宋清朗相戀三年沙郭,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了佛呻。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,727評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡病线,死狀恐怖吓著,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情送挑,我是刑警寧澤绑莺,帶...
    沈念sama閱讀 35,428評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站惕耕,受9級(jí)特大地震影響纺裁,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜司澎,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,022評(píng)論 3 326
  • 文/蒙蒙 一欺缘、第九天 我趴在偏房一處隱蔽的房頂上張望栋豫。 院中可真熱鬧,春花似錦谚殊、人聲如沸笼才。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至昂羡,卻和暖如春絮记,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背虐先。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工怨愤, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人蛹批。 一個(gè)月前我還...
    沈念sama閱讀 47,734評(píng)論 2 368
  • 正文 我出身青樓撰洗,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親腐芍。 傳聞我的和親對(duì)象是個(gè)殘疾皇子差导,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,619評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容