湖南大學人工智慧實驗三:分類演算法實驗

Moyu_42發表於2020-12-29

程式碼連結:人工智慧實驗

人工智慧實驗三:分類演算法實驗

一、實驗目的

  1. 鞏固4種基本的分類演算法的演算法思想:樸素貝葉斯演算法,決策樹演算法,人工神經網路,支援向量機;

  2. 能夠使用現有的分類器演算法程式碼進行分類操作

  3. 學習如何調節演算法的引數以提高分類效能;

二、實驗的硬體、軟體平臺

硬體:計算機

軟體:作業系統:WINDOWS/Linux

應用軟體:C, Java或者Matlab或python

三、實驗內容及步驟

利用現有的分類器演算法對文字資料集進行分類

實驗步驟:

  1. 瞭解文字資料集的情況並閱讀演算法程式碼說明文件;

  2. 編寫程式碼設計樸素貝葉斯演算法,決策樹演算法,人工神經網路,支援向量機等分類演算法,利用文字資料集中的訓練資料對演算法進行引數學習;

  3. 利用學習的分類器對測試資料集進行測試;

  4. 統計測試結果;

四、思考題:

  1. 如何在引數學習或者其他方面提高演算法的分類效能?

五、實驗報告要求

  1. 對各種演算法的原理進行說明;

  2. 對實驗過程進行描述;

  3. 附上完整的實驗程式碼,程式碼的詳細說明,統計實驗結果,對分類效能進行比較說明;

  4. 對演算法的時間空間複雜度進行比較分析。

六、實驗步驟

資料集讀取

總共給出了三個資料集:data集,predict集,test集,從名字可以看出,data集用於訓練,test集用於測試,predict集用於驗證演算法分類的效能。

所以通過pandas庫對資料集進行讀取:

data = pd.read_csv("C:/Users/a2783/Desktop/AI/Exp/Exp3/dataset.txt")
pred = pd.read_csv("C:/Users/a2783/Desktop/AI/Exp/Exp3/predict.txt")
test = pd.read_csv("C:/Users/a2783/Desktop/AI/Exp/Exp3/test.txt")

由於資料以文字形式儲存,需要轉換成對應的數值型別。這裡採用的方法是,對於每一個特徵,遍歷其所有的類別,將每一個類別賦予一個值。每個特徵值得範圍從1開始,最大即特徵對應的類別得種類數:

for i in data.columns[:-1]:
    cnt = 1
    feature = data[i]
    feature = np.array(feature)
    for j in np.unique(feature):
        data.loc[data[i] == j, i] = cnt
        pred.loc[pred[i] == j, i] = cnt
        test.loc[test[i] == j, i] = cnt
        cnt += 1

由於之後做分類時,要求標籤從0開始,所以對於Class_Values的值從0開始:

cnt = 0
for j in np.unique(data[data.columns[-1]]):
    data.loc[data[data.columns[-1]] == j, data.columns[-1]] = cnt
    pred.loc[pred[data.columns[-1]] == j, pred.columns[-1]] = cnt
    test.loc[test[data.columns[-1]] == j, test.columns[-1]] = cnt
    cnt += 1

至此,資料處理完畢。

樸素貝葉斯

基礎理論

貝葉斯公式:
P ( A ∣ B ) = P ( A ) P ( B ∣ A ) P ( B ) P(A|B) = \frac{P(A)P(B|A)}{P(B)} P(AB)=P(B)P(A)P(BA)
其中, P ( B ) P(B) P(B)為歸一化因子,可以忽略, P ( A ) P(A) P(A)是先驗概率, P ( A ∣ B ) P(A|B) P(AB)是後驗概率, P ( B ∣ A ) P(B|A) P(BA)即似然。

但是根據貝葉斯公式,若要估計後驗概率,由於類條件概率為資料的所有屬性上的聯合概率,所有在樣本集中較難直接估計得等到。所以樸素貝葉斯演算法採用了屬性條件獨立性假設,假設所有屬性相互獨立,因此可得公式:
P ( c ∣ x ) = P ( c ) P ( c ∣ x ) P ( x ) = P ( c ) P ( x ) ∏ i = 1 d P ( x i ∣ c ) P(c|x)=\frac{P(c)P(c|x)}{P(x)} = \frac{P(c)}{P(x)}\prod_{i=1}^dP(x_i|c) P(cx)=P(x)P(c)P(cx)=P(x)P(c)i=1dP(xic)
所以,對於分類任務,可以由公式:
h ( x ) = arg ⁡ max ⁡ c ∈ λ P ( c ) ∏ i = 1 d P ( x i ∣ c ) h(x) = \arg\max_{c\in\lambda}P(c)\prod_{i=1}^{d}P(x_i|c) h(x)=argcλmaxP(c)i=1dP(xic)
來選擇最大的值作為當前樣本的分類。

對於樸素貝葉斯演算法,可以在資料集中直接估計得到類先驗概率以及條件概率
P ( c ) = ∣ D c ∣ ∣ D ∣ P ( x i ∣ c ) = ∣ D c , x i ∣ ∣ D c ∣ P(c) = \frac{|D_c|}{|D|} \\ P(x_i|c) = \frac{|D_{c,x_i}|}{|D_c|} P(c)=DDcP(xic)=DcDc,xi
如果屬性是連續屬性,那麼可以根據概率密度函式來計算得到條件概率,假設其服從高斯分佈,其 μ c , i \mu_{c,i} μc,i σ c , i 2 \sigma^2_{c,i} σc,i2是第 c c c類樣本在第 i i i個屬性上的均值和方差。

拉普拉斯平滑

考慮到樣本集中的樣本並不是完全的,可能存在某個類的某個屬性沒有出現,所以採用拉普拉斯平滑,避免未出現的屬性攜帶的資訊被抹去。主要就是在求條件概率和先驗概率時,將分子加上 λ \lambda λ,相應的分母加上 ∣ D ∣ ∗ λ |D|*\lambda Dλ。本次實驗中採用的是最簡單的採用 λ = 1 \lambda=1 λ=1
P ( c ) = ∣ D c ∣ + 1 ∣ D ∣ + N P ( x i ∣ c ) = ∣ D c , x i ∣ + 1 ∣ D c ∣ + N i P(c) = \frac{|D_c| + 1}{|D| + N} \\ P(x_i|c) = \frac{|D_{c,x_i}| + 1}{|D_c|+N_i} P(c)=D+NDc+1P(xic)=Dc+NiDc,xi+1

實驗程式碼
訓練

計算先驗概率:

for i in np.unique(np.array(y)):
	self.prior[i] = (y.count(i) + self.L) / (len(y) + len(np.unique(np.array(y))))

計算條件概率:

for c in np.unique(np.array(y)):
	D_c = Data.loc[Data[Data.columns[-1]] == c]
    for x in feature:
    	for i in np.unique(np.array(Data[x])):
        	D_x = D_c.loc[D_c[x] == i]
            before = str(x) + "," + str(i)
            after = str(c)
            key = before + "|" + after
            self.P[key] = (len(D_x) + self.L) / (len(D_c) + len(np.unique(np.array(Data[x]))))

這裡儲存的方式都採用字典(Dict)的方式,對於條件概率,key使用字串形式表示,將其各個特徵的值轉換為字串並拼接來作為key。

整體程式碼:

    def fit(self, Data: pd.DataFrame):
        y = Data.iloc[:, -1]
        y = list(y)
        feature = Data.columns[:-1]
        # priority
        for i in np.unique(np.array(y)):
            # 拉普拉斯平滑
            self.prior[i] = (y.count(i) + self.L) / (len(y) + len(np.unique(np.array(y))))
        # given
        for c in np.unique(np.array(y)):
            D_c = Data.loc[Data[Data.columns[-1]] == c]
            for x in feature:
                for i in np.unique(np.array(Data[x])):
                    D_x = D_c.loc[D_c[x] == i]
                    # 將屬性轉換為字串形式 作為key
                    before = str(x) + "," + str(i)
                    after = str(c)
                    key = before + "|" + after
                    # 拉普拉斯平滑
                    self.P[key] = (len(D_x) + self.L) / (len(D_c) +
                                                         len(np.unique(np.array(Data[x]))))

傳入的Data即訓練集

預測

具體的方式就是取出每一個待預測的樣本,根據樸素貝葉斯的公式,取出每個屬性的值進行連乘,最後選取概率最大的作為當前的預測標籤,最終返回

    def pred(self, Data: pd.DataFrame) -> Tuple[List[str], List[str]]:
        ans = []
        acc = []
        for _, val in Data.iterrows():
            ret = None
            mle = 0
            for c in self.prior.keys():
                pred = self.prior[c]
                for idx in range(len(Data.columns[:-1])):
                    feature = Data.columns[idx]
                    cls = val[idx]
                    key = str(feature) + "," + str(cls) + "|" + str(c)
                    pred *= self.P[key]
                if pred > mle:
                    mle = pred
                    ret = c
            ans.append(ret)
            if ret == val[-1]:
                acc.append(1)
            else:
                acc.append(0)
        return ans, acc
結果

在perdict集上正確率為:0.8095238095238095

決策樹

基礎理論

決策樹學習本質是從訓練資料集中歸納出一組分類規則,在每個結點進行分裂,將原本的資料集進行劃分,最終得到葉子結點,即當前資料的標籤,也就是之後預測的結果。基於樹結構的演算法都有一個比較好的特點:不需要進行特徵的歸一化,而且訓練速度較快。

不同的決策樹演算法採用不同的分裂依據。

ID3

ID3決策樹使用資訊增益來作為分裂依據,通過計算當前資料集中,資訊增益最大的特徵作為劃分依據,將資料集劃分為多個子資料集,作為當前節點的分支。

資訊熵:用來表示當前的樣本集合純度
E n t ( D ) = − ∑ k = 1 ∣ λ ∣ p k l o g 2 p k Ent(D) = -\sum_{k=1}^{|\lambda|}p_klog_2p_k Ent(D)=k=1λpklog2pk
p k p_k pk為第 k k k類樣本所佔的比例

資訊增益:
G a i n ( D , a ) = E n t ( D ) − ∑ v = 1 V ∣ D v ∣ ∣ D ∣ E n t ( D v ) Gain(D, a) = Ent(D) - \sum_{v=1}^V\frac{|D^v|}{|D|}Ent(D^v) Gain(D,a)=Ent(D)v=1VDDvEnt(Dv)
其中,D為當前結點的資料集,v為其特徵

從而,對於每一個特徵計算資訊增益,選擇最大的特徵作為分類的依據,從而得到該節點的子結點,一直遞迴的執行,直到到達葉子節點:均屬於同一類樣本或沒有特徵可供選擇,對於後者,選取當前資料集中,包含最多樣本的類別作為當前葉子結點的值。

C4.5

C4.5演算法與ID3演算法相似,不過其採用資訊增益率而非資訊增益來選擇最優化分屬性。除此之外,還支援預剪枝以及後剪枝的方法。

資訊增益率
G a i n r a t i o ( D , a ) = G a i n ( D , a ) I V ( a ) I V ( a ) = − ∑ v = 1 V ∣ D v ∣ ∣ D ∣ log ⁡ 2 ∣ D v ∣ ∣ D ∣ Gain_ratio(D, a)=\frac{Gain(D, a)}{IV(a)} \\ IV(a) = -\sum_{v=1}^V\frac{|D^v|}{|D|}\log_2\frac{|D^v|}{|D|} Gainratio(D,a)=IV(a)Gain(D,a)IV(a)=v=1VDDvlog2DDv
I V ( a ) IV(a) IV(a)表示屬性 a a a的固有值。

資訊增益率對可取值數目較少的屬性有所偏好,所以對C4.5演算法進行改進,採用一個啟發式來進行選擇:從候選劃屬性中找到資訊增益高於平均水平的屬性,再從中選擇增益率最高的。

CART

CART則採用基尼指數來進行劃分屬性的選擇

基尼值:
G i n i ( D ) = ∑ k = 1 ∣ λ ∣ ∑ k ‘ ≠ k p k p k ‘ = 1 − ∑ k = 1 ∣ λ ∣ p k 2 Gini(D) = \sum_{k=1}^{|\lambda|}\sum_{k^`\ne k}p_kp_k^`=1-\sum_{k=1}^{|\lambda|}p_k^2 Gini(D)=k=1λk=kpkpk=1k=1λpk2
基尼指數:
G i n i i n d e x ( D , a ) = ∑ v = 1 V ∣ D v ∣ ∣ D ∣ G i n i ( D v ) Gini_index(D, a) = \sum_{v=1}^V\frac{|D^v|}{|D|}Gini(D^v) Giniindex(D,a)=v=1VDDvGini(Dv)
與前兩者不同,其選取的是基尼指數最小的屬性作為最優化分屬性,而且CART不僅可以用於分類,而且還可以用於迴歸。

除了上述所說的,決策樹多用於整合學習演算法,如隨機森林,GBDT等演算法,都有比較好的效果。

實驗程式碼

本次決策樹的實驗程式碼支援了上述三種構造的方法

ID3
    def entropy(self, x: pd.DataFrame) -> float:
        '''資訊熵'''
        result = 0
        size = x.size
        for i in np.unique(np.array(x)):
            p = x[x == i].size / size
            result += p * np.log2(p)
        return -1 * result

    def cond_entropy(self, x: pd.DataFrame) -> float:
        '''條件熵'''
        result = 0
        size = len(x)
        feature = x.columns[0]
        for i in np.unique(np.array(x.iloc[:, 0])):
            D_i = x.loc[x[feature] == i]
            result += len(D_i) / size * self.entropy(D_i.iloc[:, -1])
        return result
    
    def gain(self, x: pd.DataFrame) -> float:
        '''資訊增益 ID3
        x: 僅包含對應的特徵列與標籤列
        '''
        return self.entropy(np.array(x.iloc[:, -1])) - self.cond_entropy(x)
C4.5
    def gain_ratio(self, x: pd.DataFrame) -> float:
        '''增益率 C4.5
        x: 僅包含對應的特徵列與標籤列
        '''
        return self.gain(x) / self.entropy(x.iloc[:, 0])
CART
    def gini(self, x: pd.DataFrame) -> float:
        '''基尼係數'''
        result = 1.0
        size = x.size
        for i in np.unique(np.array(x)):
            p = x[x == i].size / size
            result -= p ** 2
        return result
    
    def gain_gini(self, x: pd.DataFrame) -> Tuple[float, str]:
        '''基尼指數 CART
        x: 僅包含對應的特徵列與標籤列
        '''
        min_gini = float("+inf")
        kind = ""
        feature = x.columns[0]
        size = len(x)
        for i in np.unique(np.array(x[feature])):
            D_1 = x.loc[x[feature] == i].iloc[:, -1]
            D_2 = x.loc[x[feature] != i].iloc[:, -1]
            gini = D_1.size / size * self.gini(D_1) + D_2.size / size * self.gini(D_2)
            if gini < min_gini:
                min_gini = gini
                kind = i
        return min_gini, kind

以上,ID3和C4.5都返回的是計算得到的該特徵的資訊增益或增益率,而CART則是返回基尼指數以及最小的屬性值

在此之後,就可以實現決策樹的構造:

劃分依據
    def get_max(self, x: pd.DataFrame) -> Tuple[str, List[pd.DataFrame]]:
        '''計算劃分依據
        x: 樣本集D
        return: max_feature本次依據的特徵 dataset: 根據該特徵劃分出的資料集
        '''
        max_val = 0
        max_feature = str
        if self.method == "CART":  # CART還要有當前的屬性
            max_kind = str
            max_val = float("+inf")  # 由於選擇是最小的基尼指數,所以應當為極大值
        for i in x.columns[:-1]:  # 遍歷每一個特徵,選取最優劃分依據
            A = pd.concat([x[i], x[x.columns[-1]]], axis=1)
            val = float
            if self.method == "ID3":  # 根據不同的方法選擇得到不同的值
                val = self.gain(A)
            elif self.method == "C4.5":
                val = self.gain_ratio(A)
            elif self.method == "CART":
                val, kind = self.gain_gini(A)
            if val > max_val and self.method != "CART":
                max_val = val
                max_feature = i
            elif val < max_val and self.method == "CART":
                max_val = val
                max_feature = i
                max_kind = kind
        dataset = []  # 劃分
        if self.method != "CART":  # 多叉樹
            for i in np.unique(np.array(x[max_feature])):
                D_i = x.loc[x[max_feature] == i]
                dataset.append(D_i)
        elif self.method == "CART":  # 二叉樹
            D_1 = x.loc[x[max_feature] == max_kind]
            D_2 = x.loc[x[max_feature] != max_kind]
            dataset.append(D_1)
            dataset.append(D_2)
        return max_feature, dataset

由於CART為二叉樹,並且劃分依據與其他的不同,所以在這為了適配三種方法,採用了特殊的判斷。最終,這個函式返回的是最優劃分屬性以及劃分後的資料集,使用List[pd.DataFrame]表示

建樹
    def build(self, x: pd.DataFrame) -> Dict:
        deep = copy.deepcopy(self.depth)
        self.depth += 1
        y = x.iloc[:, -1]

        if len(np.unique(np.array(y))) == 1:  # 葉子節點
            self.depth -= 1
            return y.iloc[0]
        
        if len(x.columns) == 1:  # 沒有屬性可供劃分
            result = list(x.iloc[:, -1])
            val, label = 0, str
            for i in np.unique(np.array(result)):  # 投票
                if result.count(i) > val:
                    val = result.count(i)
                    label = i
            return label

        if deep > self.max_depth:  # 到達最大深度 剪枝
            result = list(x.iloc[:, -1])
            val, label = 0, str
            for i in np.unique(result):
                if result.count(i) > val:
                    val = result.count(i)
                    label = i
            return label

        feature, dataset = self.get_max(x)
        tree = {feature: {}}
        if self.method != "CART":  # 多叉樹構造,遍歷上述方法求得的dataset集
            for i in range(len(dataset)):
                data = dataset[i].copy()
                kind = data[feature].iloc[0]
                data.drop(feature, inplace=True, axis=1)
                feature_ch = self.build(data)
                tree[feature][kind] = feature_ch
        elif self.method == "CART":  # 二叉樹構造,只有該種屬性和其他
            data_1 = dataset[0].copy()
            data_2 = dataset[1].copy()
            kind = data_1[feature].iloc[0]
            data_1.drop(feature, inplace=True, axis=1)
            data_2.drop(feature, inplace=True, axis=1)
            feature_ch = self.build(data_1)
            tree[feature][kind] = feature_ch
            feature_ch = self.build(data_2)
            tree[feature]["!" + kind] = feature_ch  # 其他採用 "!"+屬性值 表示
        self.depth -= 1
        return tree
預測
    def predict(self, pred: pd.DataFrame) -> List[str]:
        ans = []  # 最終預測的結果
        class_list = list(pred.iloc[:, -1])  # 葉子結點的屬性值
        for _, data in pred.iterrows():  # 遍歷每一個測試樣例
            key = [elem for elem in self.tree.keys()][0]  # 取出當前樹的鍵值(劃分的屬性依據)
            feature = data[key]  # 得到預測的資料中該屬性的值
            
            if self.method != "CART":  # 不是CART決策時
                class_val = self.tree[key][feature]  # 得到該屬性的該值對應的子樹
            elif self.method == "CART":  # 是CART
                try:
                    class_val = self.tree[key][feature]  # 決策樹的屬性值與測試資料該屬性的值相匹配
                except KeyError:
                    feature_notin = [elem for elem in self.tree[key].keys()][1]  # 不匹配 得到另一枝
                    # 得到該屬性的另一值對應的子樹(CART為二叉樹)
                    class_val = self.tree[key][feature_notin]
                  
            while class_val not in class_list:  # 當子樹的屬性不是葉子節點的屬性(當不是葉子節點時)
                key = [elem for elem in class_val.keys()][0]  # 重複上面的操作
                feature = data[key]
                if self.method != "CART":
                    class_val = class_val[key][feature]
                elif self.method == "CART":
                    try:
                        class_val = class_val[key][feature]
                    except KeyError:
                        feature_notin = [elem for elem in class_val[key].keys()][1]
                        class_val = class_val[key][feature_notin]
            ans.append(class_val)  # 將本次預測的結果加入到ans中
        return ans  # 返回所有資料預測的結果

預測部分與樸素貝葉斯相似,都是返回預測的預測集結果。在預測的過程中,同樣的遍歷每一個樣例,取出當前樹的劃分依據,根據樣例的屬性來選擇分支,一直繼續直到到達葉子節點,此時,得到的就是該樣例的預測結果。

結果

在predict集上能夠得到完全的預測。

支援向量機

這一部分由於課本上並沒有深入的介紹,所以我也沒有具體去手動實現,因為SMO演算法的實現確實有難度。

理論基礎

對於樣本的分類,最基本的想法就是基於資料集D,在樣本空間找到一個劃分超平面,把不同類別的進行分開。而支援向量機,則是在該基礎上,找到兩個分類到達超平面的最大間隔最大,那麼這一個超平面則是要學習到的超平面。

根據超平面的方程: ω ⃗ T x + b = 0 \vec\omega^Tx + b = 0 ω Tx+b=0可見超平面可用 ( ω ⃗ , b ) (\vec\omega, b) (ω ,b)來表示,那麼樣本空間中任意點到超平面的距離即
r = ω ⃗ T x ⃗ + b ∣ ∣ w ∣ ∣ r = \frac{\vec\omega^T\vec{x}+b}{||w||} r=wω Tx +b
考慮到目標是找到超平面,使得距離該超平面最近的樣本點最遠,所以有
y ( x ) = m i n 1 ∣ ∣ ω ∣ ∣ y ( ω T x + b ) y(x) = min\frac{1}{||\omega||}y(\omega^Tx+b) y(x)=minω1y(ωTx+b)
求解目標即:
arg ⁡ max ⁡ ω , b y ( x ) 即 arg ⁡ max ⁡ ω , b ( min ⁡ i 1 ∣ ∣ ω ∣ ∣ y i ( ω T x i + b ) ) \arg\max_{\omega, b}y(x) \\ 即\arg\max_{\omega, b}(\min_i\frac{1}{||\omega||}y_i(\omega^Tx_i+b)) argω,bmaxy(x)argω,bmax(iminω1yi(ωTxi+b))
如果可用完全正確分類,那麼有
{ ω ⃗ T x i + b ⩾ + 1 ,   y i = + 1 ; ω ⃗ T x i + b ⩽ − 1 ,   y i = − 1. \begin{cases} \vec\omega^Tx_i+b\geqslant+1,\ y_i=+1;\\ \vec\omega^Tx_i+b\leqslant-1,\ y_i=-1. \end{cases} {ω Txi+b+1, yi=+1;ω Txi+b1, yi=1.
在邊界上的樣本點即支援向量,兩個異類支援向量到超平面的距離和為 γ = 2 ∣ ∣ ω ∣ ∣ \gamma=\frac{2}{||\omega||} γ=ω2,所以,目標即使其最大化,等價於
min ⁡ ω , b 1 2 ∣ ∣ ω ∣ ∣ 2 s . t .   y i ( ω ⃗ T x i + b ) ⩾ 1 , i = 1 , 2 , … … , m \min_{\omega, b}\frac{1}{2}||\omega||^2 \\ s.t. \ y_i(\vec\omega^Tx_i+b)\geqslant1, i =1, 2, ……, m ω,bmin21ω2s.t. yi(ω Txi+b)1,i=1,2,,m
得到了上述的式子,顯然是一個最優化問題,可以通過梯度下降等優化演算法來對其進行求解。

此處可以選擇損失函式為 h i n g e   l o s s hinge\ loss hinge loss,對其使用梯度下降進行優化,從而得到最終的解。

另一種效率更高的方式即SMO演算法:引入拉格朗日乘子得到其對偶問題,並且根據上式的約束條件滿足KKT條件,可以使用SMO演算法來求解拉格朗日乘子,進而得到所求的 ω \omega ω b b b

除此之外,還可以使用 K e r n e l   T r i c k Kernel\ Trick Kernel Trick來將樣本點對映到高維空間,以解決線性不可分的問題。通過引入軟間隔,使得其允許接受一定程度得錯誤。

程式碼實現

這裡採用的是sklearn庫的SVC方法

svc = SVC()
svc.fit(x, y)

採用了預設的引數,即使用高斯核,軟間隔超引數C=1.0

結果

能夠做到 0.9206349206349206 的正確率

神經網路

理論基礎

簡單來說,每個神經網路必有一層輸入層,一層輸出層,中間的隱層可以自行設定。輸入層得大小即樣本的特徵數量,輸出層的大小為分類的類別數目。對於每一個樣本的輸入,首先經過前向傳播:簡單的線性函式累加,再經過啟用函式之後繼續傳遞到下一層,到達輸出層後計算誤差,將誤差進行反向傳播,並對引數 ω \omega ω進行優化。經過多次訓練,直到損失函式收斂,訓練結束。

預測部分則將資料輸入,得到每個輸出神經元的值,從中選取最大的值最為當前樣本的預測結果。

在這裡插入圖片描述

考慮上圖的神經網路,輸入為 x i x_i xi,經過前向傳播到達隱層,得到隱層結點的值: a i ( 1 ) = ω 01 T x a_i^{(1)} = \omega_{01}^Tx ai(1)=ω01Tx,經過一次啟用函式得到: b i ( 1 ) = g ( a i ( 1 ) ) b_i^{(1)}=g(a_i^{(1)}) bi(1)=g(ai(1)),再繼續傳播得到輸出的值: y i ^ = ω 12 T b \hat{y_i} = \omega_{12}^Tb yi^=ω12Tb(假設偏置 b i a s bias bias放入到輸入作為 ω i ( 0 ) \omega_i^{(0)} ωi(0),對應的輸入 x 0 = 1 x_0 = 1 x0=1)。

之後再將誤差反向傳播,首先得到輸出層的誤差: δ i ( 2 ) = y ^ − y \delta_i^{(2)}=\hat y-y δi(2)=y^y,反向傳播得到隱層的誤差 δ i ( 1 ) = ω 12 T δ i ( 2 ) ⋅ g ′ ( b i ( 1 ) ) \delta_i^{(1)}=\omega_{12}^T\delta_i^{(2)}·g'(b_i^{(1)}) δi(1)=ω12Tδi(2)g(bi(1)),同時求得梯度為 Δ i j l = Δ i j l + a j ( l ) δ i ( l + 1 ) \Delta_{ij}^l=\Delta_{ij}^l+a_j^{(l)}\delta_i^{(l+1)} Δijl=Δijl+aj(l)δi(l+1),繼續對引數進行優化: ∂ ∂ ω i j ( l ) J ( ω ) = 1 m Δ i j l + λ ω i j ( l ) \frac{\partial}{\partial\omega_{ij}^{(l)}}J(\omega)=\frac{1}{m}\Delta_{ij}^l+\lambda\omega_{ij}^{(l)} ωij(l)J(ω)=m1Δijl+λωij(l)

程式碼實現

採用了四層的神經網路,包含兩個隱層,啟用函式選擇ReLU啟用函式:

class NN(nn.Module):
    def __init__(self, in_dim, hidden_1, hidden_2, out_dim):
        super(NN, self).__init__()
        self.hidden1 = nn.Linear(in_dim, hidden_1)
        self.hidden2 = nn.Linear(hidden_1, hidden_2)
        self.out = nn.Linear(hidden_2, out_dim)

    def forward(self, x):
        x = self.hidden1(x)
        x = F.relu(x)
        x = self.hidden2(x)
        x = F.relu(x)
        x = self.out(x)
        x = F.relu(x)
        return x

在訓練時,選擇的優化器為隨機梯度下降優化器,學習率預設為0.1,使用交叉熵作為損失函式,訓練輪數預設5000輪:

    def __init__(self, in_dim, hidden_1, hidden_2, out_dim, lr=0.1, epochs=5000):
        self.model = NN(in_dim, hidden_1, hidden_2, out_dim)
        self.optimizer = optim.SGD(self.model.parameters(), lr=lr)  # 隨機梯度下降優化器
        self.criterion = nn.CrossEntropyLoss()  # 交叉熵
        self.epochs = epochs
        if torch.cuda.is_available():
            self.device = torch.device('cuda:0')
            self.model.to(self.device)
            self.criterion = self.criterion.cuda()

訓練:

    def fit(self, data: pd.DataFrame, test: pd.DataFrame):
        X_train = data.iloc[:, :-1]
        y_train = data.iloc[:, -1]
        X_test = test.iloc[:, :-1]
        y_test = test.iloc[:, -1]
        X_train = np.array(X_train, dtype=np.float32)
        y_train = np.array(y_train, dtype=np.float32)
        X_test = np.array(X_test, dtype=np.float32)
        y_test = np.array(y_test, dtype=np.float32)
        X_test = torch.from_numpy(X_test)
        y_test = torch.from_numpy(y_test)
        if torch.cuda.is_available():
            X_test = X_test.to(self.device)

        for epoch in range(self.epochs):  # 每一輪訓練
            x = torch.from_numpy(X_train)
            y = torch.from_numpy(y_train)
            if torch.cuda.is_available():
                x = x.to(self.device)
                y = y.to(self.device)
            pred = self.model(x)
            loss = self.criterion(pred, y.long())  # 計算loss
            self.optimizer.zero_grad()  # 優化
            loss.backward()  # 反向傳播
            self.optimizer.step()

預測則是輸入資料,選擇最大的類別作為當前的分類:

    def predict(self, pred: pd.DataFrame) -> float:
        X_pred = pred.iloc[:, :-1]
        y_pred = pred.iloc[:, -1]
        X_pred = np.array(X_pred, dtype=np.float32)
        y_pred = np.array(y_pred, dtype=np.float32)

        x = torch.from_numpy(X_pred)
        y = torch.from_numpy(y_pred)
        if torch.cuda.is_available():
            x = x.to(self.device)

        predict = self.model(x)
        predict = torch.max(predict, 1)[1]  # 選擇最大的類別
        if torch.cuda.is_available():
            predict = predict.cpu()

        pred_y = predict.data.numpy()
        target_y = y.data.numpy()
        acc = float((pred_y == target_y).astype(int).sum()) / float(target_y.size)
        return acc
結果

每500次訓練進行loss的輸出以及準確率的檢視

在這裡插入圖片描述

最終預測結果為0.9682539682539683的準確率。若將輪數增大,準確率能夠逼近1.0

實驗結果

在這裡插入圖片描述

均為在perdict資料集上做預測的結果。

顯而易見的是,神經網路的分類方法能夠適用於任何資料集,在模型搭建好之後,只需要進行訓練,就可以得到不錯的結果,訓練的輪數越多,網路的層數越深,就能夠得到約準確的分類,但是帶來的時間開銷也是巨大的。

對於其他三種演算法,支援向量機的效能是很不錯的,泛化效能較強,而且是用於解決高維的問題;樸素貝葉斯較簡單,由於是有堅實的數學基礎所以其分類效率較穩定,但是需要直到其先驗概率才可以進行學習;決策樹的優點正如上述所講,不需要進行特徵歸一化,但是其容易忽略樣本特徵之間的相關性,而且容易過擬合。

思考題

對於引數的調整,支援向量機可以嘗試不同的核,或者減少其軟間隔係數,以使得分類效果更好,但是會降低其泛化效能;決策樹則可以嘗試不同的剪枝策略來提高其泛化能力;樸素貝葉斯則可以嘗試進行不同的平滑策略,除了拉普拉斯平滑還可以選擇lidstone平滑;而神經網路,則可以嘗試增加訓練輪數或者增大網路的深度來得到更好的分類結果,但是帶來的代價則是時間開銷的增大,此外還可以調整學習率來得到更好的訓練結果。

相關文章