標籤: 潭子電動車

  • Python機器學習筆記:SVM(4)——sklearn實現,Python機器學習筆記:SVM(1)——SVM概述,Python機器學習筆記:SVM(2)——SVM核函數,Python機器學習筆記:SVM(3)——證明SVM,Python機器學習筆記:SVM(4)——sklearn實現

    Python機器學習筆記:SVM(4)——sklearn實現,Python機器學習筆記:SVM(1)——SVM概述,Python機器學習筆記:SVM(2)——SVM核函數,Python機器學習筆記:SVM(3)——證明SVM,Python機器學習筆記:SVM(4)——sklearn實現

      上一節我學習了SVM的推導過程,下面學習如何實現SVM,具體的參考鏈接都在第一篇文章中,SVM四篇筆記鏈接為:

    Python機器學習筆記:SVM(1)——SVM概述

    Python機器學習筆記:SVM(2)——SVM核函數

    Python機器學習筆記:SVM(3)——證明SVM

    Python機器學習筆記:SVM(4)——sklearn實現

      對SVM的概念理清楚后,下面我們對其使用sklearn進行實現。

    1,Sklearn支持向量機庫概述

      我們知道SVM相對感知器而言,它可以解決線性不可分的問題,那麼它是如何解決的呢?其思想很簡單就是對原始數據的維度變換,一般是擴維變換,使得原樣本空間中的樣本點線性不可分,但是在變維之後的空間中樣本點是線性可分的,然後再變換后的高維空間中進行分類。

      上面將SVM再贅述了一下,下面學習sklearn中的SVM方法,sklearn中SVM的算法庫分為兩類,一類是分類的算法庫,主要包含LinearSVC,NuSVC和SVC三個類,另一類是回歸算法庫,包含SVR,NuSVR和LinearSVR三個類,相關模塊都包裹在sklearn.svm模塊中。

      對於SVC,NuSVC和LinearSVC 三個分類的庫,SVC和NuSVC差不多,區別僅僅在於對損失的度量方式不同,而LinearSVC從名字就可以看出,他是線性分類,也就是不支持各種低維到高維的核函數,僅僅支持線性核函數,對線性不可分的數據不能使用。

      同樣的對於SVR,NuSVR和LinearSVR 三個回歸的類,SVR和NuSVR差不多,區別也僅僅在於對損失的度量方式不同。LinearSVR是線性回歸,只能使用線性核函數。

      我們使用這些類的時候,如果有經驗知道數據是線性可以擬合的,那麼使用LinearSVC去分類或者LinearSVR去回歸,他們不需要我們去慢慢的調參選擇各種核函數以及對應的參數,速度也快。如果我們對數據分佈沒有什麼經驗,一般使用SVC去分類或者SVR去回歸,這就需要我們選擇核函數以及對核函數調參了。

    2,回顧SVM分類算法和回歸算法

      我們這裏仍然先對SVM算法進行回顧,首先對於SVM分類算法,其原始形式如下:

      其中 n 為樣本個數,我們的樣本為(x1,  y1),(x2,y2),….(xn,  yn),w,b是我們的分離超平面的 wT*xi + b = 0的係數,ξi 為第 i 個樣本的鬆弛係數,C 為懲罰係數,xi (也有時候寫為Φ(xi) 為低維到高維的映射函數)為樣本數。

      通過拉格朗日以及對偶化后的形式為:

      其中和原始形式不同的 α 為拉格朗日係數向量,<xi,  xj> 為我們要使用的核函數。

      對於SVM回歸算法,(我自己沒有總結,借用劉建平老師的博客),其原始形式如下:

      其中 m 為樣本個數,我們的樣本為(x1, y1),(x2,  y2),….,(xm, ym),w,b是我們回歸超平面 wT*xi + b = 0 的係數,ξv, ξ^ 為第 i 個樣本的鬆弛係數, C為懲罰係數,ε 為損失邊界,到超平面距離小於 ε 的訓練集的點沒有損失,Φ(xi) 為低維到高維的映射函數

      通過拉格朗日函數以及對偶后的形式為:

      其中和原始形式不同的 αv, α^ 為拉格朗日係數向量,K(xi, xj) 為我們要使用的核函數。

    3,SVM核函數概述

      我在第二篇SVM中學習了核函數,有好幾種,最常用的就是線性核函數,多項式核函數,高斯核函數和Sigmoid核函數,在scikit-learn中,內置的核函數也剛好有這四種。

    3.1,線性核函數(Linear Kernel)

      線性核函數表達式為:

      就是普通的內積,LinearSVC和LinearSVR只能使用它。

    3.2,多項式核函數(Polynomial Kernel)

      多項式核函數是線性不可分SVM常用的核函數之一,表達式為:

      參數都需要自己調參定義,比較麻煩。

    3.3,高斯核函數(Gaussian Kernel)

      高斯核函數,在SVM中也稱為 徑向基核函數(Radial Basisi Function,RBF),它是libsvm默認的核函數,當然也是sklearn默認的核函數,表達式為:

      其中 r 大於0,需要自己調參定義,不過一般情況,我們都使用高斯核函數。

    3.4,Sigmoid核函數(Sigmoid Kernel)

      Sigmoid核函數也是線性不可分SVM常用的核函數之一,表示為:

      其中 beta, t 都需要自己調參定義。

      一般情況下,對於非線性數據使用默認的高斯核函數會有比較好的效果,如果你不是SVM調參高手的話,建議使用高斯核來做數據分析。

    4,SVM分類算法庫參數小結

      下面我們將具體介紹這三種分類方法都有那些參數值以及不同參數值的含義。

    4.1, LinearSVC

      其函數原型如下:

    class sklearn.svm.LinearSVC(self, penalty='l2', loss='squared_hinge', dual=True, tol=1e-4,
                 C=1.0, multi_class='ovr', fit_intercept=True,
                 intercept_scaling=1, class_weight=None, verbose=0,
                 random_state=None, max_iter=1000)
    

      參數說明

    • penalty :正則化參數,L1 和L2兩種參數可選,僅LinearSVC有。默認是L2 正則化,如果我們需要產生稀疏的話,可以選擇L1正則化,這和線性回歸裏面的Lasso回歸類似
    • loss:損失函數,有“hinge” 和“squared_hinge” 兩種可選,前者又稱為L1損失,後者稱為L2損失,默認是“squared_hinge”,其中hinge是SVM的標準損失,squared_hinge是hinge的平方。
    • dual:是否轉化為對偶問題求解,默認是True。這是一個布爾變量,控制是否使用對偶形式來優化算法。
    • tol:殘差收斂條件,默認是0.0001,與LR中的一致。
    • C:懲罰係數,用來控制損失函數的懲罰係數,類似於LR中的正則化係數。默認為1,一般需要通過交叉驗證來選擇一個合適的C,一般來說,噪點比較多的時候,C需要小一些
    • multi_class:負責多分類問題中分類策略制定,有‘ovr’和‘crammer_singer’ 兩種參數值可選,默認值是’ovr’,’ovr’的分類原則是將待分類中的某一類當作正類,其他全部歸為負類,通過這樣求取得到每個類別作為正類時的正確率,取正確率最高的那個類別為正類;‘crammer_singer’ 是直接針對目標函數設置多個參數值,最後進行優化,得到不同類別的參數值大小。
    •  fit_intercept:是否計算截距,與LR模型中的意思一致。
    • class_weight:與其他模型中參數含義一樣,也是用來處理不平衡樣本數據的,可以直接以字典的形式指定不同類別的權重,也可以使用balanced參數值。如果使用“balanced”,則算法會自己計算權重,樣本量少的類別所對應的樣本權重會高,當然,如果你的樣本類別分佈沒有明顯的偏倚,則可以不管這個係數,選擇默認的None
    • verbose:是否冗餘,默認為False
    • random_state:隨機種子的大小
    • max_iter:最大迭代次數,默認為1000.

    懲罰係數:

      錯誤項的懲罰係數。C越大,即對分錯樣本的懲罰程度越大,因此在訓練樣本中準確率越高,但是泛化能力降低,也就是對測試數據的分類準確率降低。相反,減少C的話,容許訓練樣本中有一些誤分類錯誤樣本,泛化能力強。對於訓練樣本帶有噪音的情況,一般採用後者,把訓練樣本集中錯誤分類的樣本作為噪音。

    4.2,NuSVC

      其函數原型如下:

    class sklearn.svm.NuSVC(self, nu=0.5, kernel='rbf', degree=3, gamma='auto_deprecated',
                 coef0=0.0, shrinking=True, probability=False, tol=1e-3,
                 cache_size=200, class_weight=None, verbose=False, max_iter=-1,
                 decision_function_shape='ovr', random_state=None)
    

       參數說明

    • nu:訓練誤差部分的上限和支持向量部分的下限,取值在(0,1)之間,默認是0.5,它和懲罰係數C類似,都可以控制懲罰的力度。
    • kernel:核函數,核函數是用來將非線性問題轉化為線性問題的一種方法,默認是“rbf”核函數

          常用的核函數有以下幾種:

    • degree:當核函數是多項式核函數(“poly”)的時候,用來控制函數的最高次數。(多項式核函數是將低維的輸入空間映射到高維的特徵空間),這個參數只對多項式核函數有用,是指多項式核函數的階數 n。如果給的核函數參數是其他核函數,則會自動忽略該參數。
    • gamma:核函數係數,默認是“auto”,即特徵維度的倒數。核函數係數,只對rbf  poly  sigmoid 有效。
    • coef0:核函數常數值( y = kx + b 的b值),只有“poly”和“sigmoid” 函數有,默認值是0.
    • max_iter:最大迭代次數,默認值是 -1 ,即沒有限制。
    • probability:是否使用概率估計,默認是False。
    • decision_function_shape:與“multi_class”參數含義類似,可以選擇“ovo” 或者“ovr”(0.18版本默認是“ovo”,0.19版本為“ovr”) OvR(one vs rest)的思想很簡單,無論你是多少元分類,我們都可以看做二元分類,具體的做法是,對於第K類的分類決策,我們把所有第K類的樣本作為正例,除第K類樣本以外的所有樣本作為負類,然後在上面做二元分類,得到第K類的分類模型。 OvO(one vs one)則是每次在所有的T類樣本裏面選擇兩類樣本出來,不妨記為T1類和T2類,把所有的輸出為T1 和 T2的樣本放在一起,把T1作為正例,T2 作為負例,進行二元分類,得到模型參數,我們一共需要T(T-1)/2 次分類。從上面描述可以看出,OvR相對簡單,但是分類效果略差(這裡是指大多數樣本分佈情況,某些樣本分佈下OvR可能更好),而OvO分類相對精確,但是分類速度沒有OvR快,一般建議使用OvO以達到較好的分類效果
    • chache_size:緩衝大小,用來限制計算量大小,默認是200M,如果機器內存大,推薦使用500MB甚至1000MB

    4.3,SVC

      其函數原型如下:

    class sklearn.svm.SVC(self, C=1.0, kernel='rbf', degree=3, gamma='auto_deprecated',
                 coef0=0.0, shrinking=True, probability=False,
                 tol=1e-3, cache_size=200, class_weight=None,
                 verbose=False, max_iter=-1, decision_function_shape='ovr',
                 random_state=None)
    

      參數說明:

    • C:懲罰係數(前面有詳細學習)

      SVC和NuSVC方法基本一致,唯一區別就是損失函數的度量方式不同(NuSVC中的nu參數和SVC中的C參數)即SVC使用懲罰係數C來控制懲罰力度,而NuSVC使用nu來控制懲罰力度。

    5,SVM回歸算法庫參數小結

      下面我們將具體介紹這三種分類方法都有那些參數值以及不同參數值的含義。

    5.1, LinearSVR

      其函數原型如下:

    class sklearn.svm.LinearSVR(self, epsilon=0.0, tol=1e-4, C=1.0,
                 loss='epsilon_insensitive', fit_intercept=True,
                 intercept_scaling=1., dual=True, verbose=0,
                 random_state=None, max_iter=1000)
    

      參數說明

    • epsilon:距離誤差epsilon,即回歸模型中的 epsilon,訓練集中的樣本需要滿足:
    • loss:損失函數,有“hinge” 和“squared_hinge” 兩種可選,前者又稱為L1損失,後者稱為L2損失,默認是“squared_hinge”,其中hinge是SVM的標準損失,squared_hinge是hinge的平方。
    • dual:是否轉化為對偶問題求解,默認是True。這是一個布爾變量,控制是否使用對偶形式來優化算法。
    • tol:殘差收斂條件,默認是0.0001,與LR中的一致。
    • C:懲罰係數,用來控制損失函數的懲罰係數,類似於LR中的正則化係數。默認為1,一般需要通過交叉驗證來選擇一個合適的C,一般來說,噪點比較多的時候,C需要小一些
    •  fit_intercept:是否計算截距,與LR模型中的意思一致。
    • verbose:是否冗餘,默認為False
    • random_state:隨機種子的大小
    • max_iter:最大迭代次數,默認為1000.

    5.2,NuSVR

      其函數原型如下:

    class sklearn.svm.NuSVR(self, nu=0.5, C=1.0, kernel='rbf', degree=3,
                 gamma='auto_deprecated', coef0=0.0, shrinking=True,
                 tol=1e-3, cache_size=200, verbose=False, max_iter=-1)
    

       參數說明

    • nu:訓練誤差部分的上限和支持向量部分的下限,取值在(0,1)之間,默認是0.5,它和懲罰係數C類似,都可以控制懲罰的力度。
    • kernel:核函數,核函數是用來將非線性問題轉化為線性問題的一種方法,默認是“rbf”核函數

          常用的核函數有以下幾種:

    • degree:當核函數是多項式核函數(“poly”)的時候,用來控制函數的最高次數。(多項式核函數是將低維的輸入空間映射到高維的特徵空間),這個參數只對多項式核函數有用,是指多項式核函數的階數 n。如果給的核函數參數是其他核函數,則會自動忽略該參數。
    • gamma:核函數係數,默認是“auto”,即特徵維度的倒數。核函數係數,只對rbf  poly  sigmoid 有效。
    • coef0:核函數常數值( y = kx + b 的b值),只有“poly”和“sigmoid” 函數有,默認值是0.
    • chache_size:緩衝大小,用來限制計算量大小,默認是200M,如果機器內存大,推薦使用500MB甚至1000MB

    5.3,SVR

      其函數原型如下:

    class sklearn.svm.SVC(self, kernel='rbf', degree=3, gamma='auto_deprecated',
                 coef0=0.0, tol=1e-3, C=1.0, epsilon=0.1, shrinking=True,
                 cache_size=200, verbose=False, max_iter=-1)
    

      參數說明:

      SVR和NuSVR方法基本一致,唯一區別就是損失函數的度量方式不同(NuSVR中的nu參數和SVR中的C參數)即SVR使用懲罰係數C來控制懲罰力度,而NuSVR使用nu來控制懲罰力度。

    6,SVM的方法與對象

    6.1 方法

      三種分類的方法基本一致,所以一起來說:

    • decision_function(x):獲取數據集X到分離超平面的距離
    • fit(x , y):在數據集(X,y)上使用SVM模型
    • get_params([deep]):獲取模型的參數
    • predict(X):預測數值型X的標籤
    • score(X,y):返回給定測試集合對應標籤的平均準確率

    6.2  對象

    • support_:以數組的形式返回支持向量的索引
    • support_vectors_:返回支持向量
    • n_support_:每個類別支持向量的個數
    • dual_coef:支持向量係數
    • coef_:每個特徵係數(重要性),只有核函數是LinearSVC的是可用,叫權重參數,即w
    • intercept_:截距值(常數值),稱為偏置參數,即b

      加粗的三個屬性是我們常用的,後面會舉例說明 support_vectors_。

     

    7,SVM類型算法的模型選擇

    7.1 PPT總結

      這裏使用(http://staff.ustc.edu.cn/~ketang/PPT/PRLec5.pdf)的PPT進行整理。

    7.2 SVM算法庫其他調參要點

      下面再對其他調參要點做一個小結:

    • 1,一般推薦在做訓練之前對數據進行歸一化,當然測試集的數據也要做歸一化
    • 2,在特徵數非常多的情況下,或者樣本數遠小於特徵數的時候,使用線性核,效果就很好了,並且只需要選擇懲罰係數C即可
    • 3,在選擇核函數的時候,如果線性擬合效果不好,一般推薦使用默認的高斯核(rbf),這時候我們主要對懲罰係數C和核函數參數 gamma 進行調參,經過多輪的交叉驗證選擇合適的懲罰係數C和核函數參數gamma。
    • 4,理論上高斯核不會比線性核差,但是這個理論就建立在要花費更多的時間上調參上,所以實際上能用線性核解決的問題我們盡量使用線性核函數

       在SVM中,其中最重要的就是核函數的選取和參數選擇了,當然這個需要大量的經驗來支撐,這裏幾個例子只是自己網上找的SVM的小例子。

    8,SVM調參實例1

      下面學習支持向量機的使用方法以及一些參數的調整,支持向量機的原理就是將低維不可分問題轉換為高維可分問題。這裏不再贅述。

    8.1  線性可分支持向量機

      首先做一個簡單的線性可分的例子,這裏直接使用sklearn.datasets.make_blobs 生成數據。生成數據代碼如下:

    # 生成數據集
    from sklearn.datasets.samples_generator import make_blobs
    from matplotlib import pyplot as plt
    
    # n_samples=50 表示取50個點,centers=2表示將數據分為兩類
    X, y = make_blobs(n_samples=50, centers=2, random_state=0, cluster_std=0.6)
    # 畫圖形
    plt.scatter(X[:, 0], X[:, 1], c=y, s=50, cmap='autumn')
    plt.show()
    

       我們畫圖展示如下:

      我們嘗試繪製分離兩組數據的直線,從而創建分類模型,對於這裏所示的數據,這是我們可以手動完成的任務。但是立馬可以看出有很多分界線可以完美的區分兩個類。

       下面畫出決策邊界。

    # 生成數據集
    from sklearn.datasets.samples_generator import make_blobs
    from matplotlib import pyplot as plt
    import numpy as np
    
    # n_samples=50 表示取50個點,centers=2表示將數據分為兩類
    X, y = make_blobs(n_samples=50, centers=2, random_state=0, cluster_std=0.6)
    
    # 畫圖形
    plt.scatter(X[:, 0], X[:, 1], c=y, s=50, cmap='autumn')
    # 線性等分詳細
    xfit = np.linspace(-1, 3.5)
    plt.plot([0.6], [2.1], 'x', color='red', markeredgewidth=2, markersize=10)
    
    for m, b in [(1, 0.65), (0.5, 1.6), (-0.2, 2.9)]:
        plt.plot(xfit, m * xfit + b, '-k')
    
    plt.show()
    

       圖如下:

      (注意:這三條直線是我隨便畫的,其實你可以使用Logistic回歸,線性回歸等分類,畫出線,我這裡是為了方便)

       這裡是三條不同的分割直線,並且這些分割直線能夠完全區分這些樣例。但是根據支持向量機的思想,哪一條直線是最優的分割線呢?支持向量機並不是簡單的繪製一條直線,而是畫出邊距為一定寬度的直線,直到最近的點。

      下面我們對直線進行加粗,代碼如下:

    # 生成數據集
    from sklearn.datasets.samples_generator import make_blobs
    from matplotlib import pyplot as plt
    import numpy as np
    
    # n_samples=50 表示取50個點,centers=2表示將數據分為兩類
    X, y = make_blobs(n_samples=50, centers=2, random_state=0, cluster_std=0.6)
    
    # 畫圖形
    plt.scatter(X[:, 0], X[:, 1], c=y, s=50, cmap='autumn')
    # 線性等分詳細
    xfit = np.linspace(-1, 3.5)
    plt.plot([0.6], [2.1], 'x', color='red', markeredgewidth=2, markersize=10)
    
    for m, b, d in [(1, 0.65, 0.33), (0.5, 1.6, 0.55), (-0.2, 2.9, 0.2)]:
        yfit = m * xfit + b
        plt.plot(xfit, yfit, '-k')
        plt.fill_between(xfit, yfit - d, yfit + d, edgecolor='none',
                         color='#AAAAAA', alpha=0.4)  # alpha為透明度
    plt.show()
    

       如圖所示:

       在支持向量機中,邊距最大化的直線是我們將選擇的最優模型。支持向量機是這種最大邊距估計器的一個例子。

      接下來,我們訓練一個基本的SVM,我們使用sklearn的支持向量機,對這些數據訓練SVM模型。目前我們將使用一個線性核並將C參數設置為一個默認的數值。如下:

    from sklearn.svm import SVC  # Support Vector Classifier
    
    model = SVC(kernel='linear') # 線性核函數
    model.fit(X, y)
    

       我們順便看看SVC的所有參數情況:

    SVC(C=1.0, cache_size=200, class_weight=None, coef0=0.0,
        decision_function_shape='ovr', degree=3, gamma='auto_deprecated',
        kernel='linear', max_iter=-1, probability=False, random_state=None,
        shrinking=True, tol=0.001, verbose=False)
    

       為了更好展現這裏發生的事情,下面我們創建一個輔助函數,為我們繪製SVM的決策邊界。

    def plot_SVC_decision_function(model, ax=None, plot_support=True):
        '''Plot the decision function for a 2D SVC'''
        if ax is None:
            ax = plt.gca()  #get子圖
        xlim = ax.get_xlim()
        ylim = ax.get_ylim()
    
        # create grid to evaluate model
        x = np.linspace(xlim[0], xlim[1], 30)
        y = np.linspace(ylim[0], ylim[1], 30)
        # 生成網格點和坐標矩陣
        Y, X = np.meshgrid(y, x)
        # 堆疊數組
        xy = np.vstack([X.ravel(), Y.ravel()]).T
        P = model.decision_function(xy).reshape(X.shape)
    
        # plot decision boundary and margins
        ax.contour(X, Y, P, colors='k', levels=[-1, 0, 1],
                   alpha=0.5, linestyles=['--', '-', '--'])  # 生成等高線 --
    
        # plot support vectors
        if plot_support:
            ax.scatter(model.support_vectors_[:, 0],
                       model.support_vectors_[:, 1],
                       s=300, linewidth=1, facecolors='none')
    
        ax.set_xlim(xlim)
        ax.set_ylim(ylim)
    

       下面繪製決策邊界:

    def train_SVM():
        # n_samples=50 表示取50個點,centers=2表示將數據分為兩類
        X, y = make_blobs(n_samples=50, centers=2, random_state=0, cluster_std=0.6)
    
        # 線性核函數
        model = SVC(kernel='linear')
        model.fit(X, y)
        plt.scatter(X[:, 0], X[:, 1], c=y, s=50, cmap='autumn')
        plot_SVC_decision_function(model)
        plt.show()
        return X, y
    

       結果如圖所示:

       這是最大化兩組點之間的間距的分界線,那中間這條線就是我們最終的決策邊界了。請注意:一些訓練點碰到了邊緣,如圖所示,在兩個邊界上包含兩個紅點和一個黃點,所以這三個點又稱為支持向量,是 alpha 值不為零的,這些點是這種擬合的關鍵要素,被稱為支持向量。在sklearn中,這些點存儲在分類器的 support_vectors_ 屬性中。

      我們通過下面代碼可以得出支持向量的結果。

        print(model.support_vectors_)
        '''
        [[0.44359863 3.11530945]
         [2.33812285 3.43116792]
         [2.06156753 1.96918596]]
        '''
    

      在支持向量機只有位於支持向量上面的點才會對決策邊界有影響,也就是說不管有多少的點是非支持向量,那對最終的決策邊界都不會產生任何影響。我們可以看到這一點,例如,如果我們繪製該數據集的前 60個點和前120個點獲得的模型:

    def plot_svm(N=10, ax=None):
        X, y = make_blobs(n_samples=200, centers=2, random_state=0, cluster_std=0.6)
        X, y = X[:N], y[:N]
        model = SVC(kernel='linear')
        model.fit(X, y)
    
        ax = ax or plt.gca()
        ax.scatter(X[:, 0], X[:, 1], c=y, cmap='autumn')
        ax.set_xlim(-1, 4)
        ax.set_ylim(-1, 6)
        plot_SVC_decision_function(model, ax)
    
    if __name__ == '__main__':
        # train_SVM()
        fig, ax = plt.subplots(1, 2, figsize=(16, 6))
        fig.subplots_adjust(left=0.0625, right=0.95, wspace=0.1)
        for axi, N in zip(ax, [60, 120]):
            plot_svm(N, axi)
            axi.set_title('N = {0}'.format(N))
    

       結果如圖所示:

      上面就是我們繪製的該數據集前60個點和前120個點獲得的模型,可以發現無論使用60,還是使用120個數據點,決策邊界都沒有發生變換,所有隻要支持向量沒變,其他的數據怎麼加都無所謂。

       這個分類器成功的關鍵在於:為了擬合,只有支持向量的位置是最重要的;任何遠離邊距的點都不會影響擬合的結果,邊界之外的點無論有多少都不會對其造成影響,也就是說不管有多少點是非支持向量,對最終的決策邊界都不會產生任何影響。

    8.2 線性不可分支持向量機

      下面引入核函數,來看看核函數的威力,首先我們導入一個線性不可分的數據集。

    def train_svm_plus():
        # 二維圓形數據 factor 內外圓比例(0, 1)
        X, y = make_circles(100, factor=0.1, noise=0.1)
        
        clf = SVC(kernel='linear')
        clf.fit(X, y)
        
        plt.scatter(X[:, 0], X[:, 1], c=y, s=50, cmap='autumn')
        plot_SVC_decision_function(clf, plot_support=False)
    

       數據集如圖所示:

       很明顯,用線性分類器無論怎麼畫線也不能分好,那咋辦呢?下面試試高斯核變換吧。在進行核變換之前,先看看數據在高維空間下的映射:

    def plot_3D(X, y, elev=30, azim=30):
        # 我們加入了新的維度 r
        r = np.exp(-(X ** 2).sum(1))
        ax = plt.subplot(projection='3d')
        ax.scatter3D(X[:, 0], X[:, 1], r, c=y, s=50, cmap='autumn')
        ax.view_init(elev=elev, azim=azim)
        ax.set_xlabel('x')
        ax.set_ylabel('y')
        ax.set_zlabel('z')
    
    
    if __name__ == '__main__':
        X, y = train_svm_plus()
        plot_3D(elev=30, azim=30, X=X, y=y)
    

       畫出三維圖形,如圖所示:

       見證核變換威力的時候到了,引入徑向基函數(也叫高斯核函數),進行核變換:

    def train_svm_plus():
        # 二維圓形數據 factor 內外圓比例(0, 1)
        X, y = make_circles(100, factor=0.1, noise=0.1)
        # 加入徑向基函數
        clf = SVC(kernel='rbf')
        clf.fit(X, y)
    
        plt.scatter(X[:, 0], X[:, 1], c=y, s=50, cmap='autumn')
        plot_SVC_decision_function(clf, plot_support=False)
        return X, y
    

       得到的SVM模型為:

    SVC(C=1000000.0, cache_size=200, class_weight=None, coef0=0.0,
        decision_function_shape='ovr', degree=3, gamma='auto_deprecated',
        kernel='rbf', max_iter=-1, probability=False, random_state=None,
        shrinking=True, tol=0.001, verbose=False)
    

       再次進行分類任務,代碼如下:

    def train_svm_plus():
        # 二維圓形數據 factor 內外圓比例(0, 1)
        X, y = make_circles(100, factor=0.1, noise=0.1)
        # 加入徑向基函數
        clf = SVC(kernel='rbf')
        clf.fit(X, y)
    
        plt.scatter(X[:, 0], X[:, 1], c=y, s=50, cmap='autumn')
        plot_SVC_decision_function(clf, plot_support=False)
        plt.scatter(clf.support_vectors_[:, 0], clf.support_vectors_[:, 1],
                    s=300, lw=1, facecolors='none')
        return X, y
    

       分類結果如圖所示:

       可以清楚的看到效果很好,我們將線性不可分的兩對數據分割開來。使用這種核支持向量機,我們學習一個合適的非線性決策邊界。這種核變換策略在機器學習中經常被使用。

    8.3 線性近似可分支持向量機——軟間隔問題

      SVM模型有兩個非常重要的參數C與gamma,其中C是懲罰係數,即對誤差的寬容忍,C越高,說明越不能容忍出現誤差,容易過擬合。C越小,容易欠擬合。C過大或過小,泛化能力變差。

      gamma 是選擇 RBF 函數作為kernel后,該函數自帶的一個參數。隱含的決定了數據映射到新的特徵空間后的分佈,gamma越大,支持向量越小,gamma值越小,支持向量越多。

      下面我們分別調劑一下C和gamma來看一下對結果的影響。

      首先我們調節C,先做一個有噪音的數據分佈

    # n_samples=50 表示取50個點,centers=2表示將數據分為兩類
    X, y = make_blobs(n_samples=100, centers=2, random_state=0, cluster_std=0.8)
    
    plt.scatter(X[:, 0], X[:, 1], c=y, s=50, cmap='autumn')
    

       結果如圖所示:

       上面的分佈看起來要劃分似乎有點困難,所以我們可以進行軟件各調整看看。

    # n_samples=50 表示取50個點,centers=2表示將數據分為兩類
    X, y = make_blobs(n_samples=100, centers=2, random_state=0, cluster_std=0.8)
    
    fig, ax = plt.subplots(1, 2, figsize=(16, 6))
    fig.subplots_adjust(left=0.0625, right=0.95, wspace=0.1)
    
    for axi, C in zip(ax, [10.0, 0.1]):
        model = SVC(kernel='linear', C=C)
        model.fit(X, y)
        axi.scatter(X[:, 0], X[:, 1], c=y, s=50, cmap='autumn')
        plot_SVC_decision_function(model, axi)
        axi.scatter(model.support_vectors_[:, 0],
                    model.support_vectors_[:, 1],
                    s=300, lw=1, facecolors='none')
        axi.set_title('C={0:.1f}'.format(C), size=14)
    

       結果如圖所示:

       可以看到左邊這幅圖C值比較大,要求比較嚴格,不能分錯東西,隔離帶中沒有進入任何一個點,但是隔離帶的距離比較小,泛化能力比較差。右邊這幅圖C值比較小,要求相對來說比較松一點,隔離帶較大,但是隔離帶中進入了很多的黃點和紅點。那麼C大一些好還是小一些好呢?這需要考慮實際問題,可以進行K折交叉驗證來得到最合適的C值。

       下面再看看另一個參數gamma值,這個參數值只是在高斯核函數裏面才有,這個參數控制着模型的複雜程度,這個值越大,模型越複雜,值越小,模型就越精簡。

      代碼如下:

    # n_samples=50 表示取50個點,centers=2表示將數據分為兩類
    X, y = make_blobs(n_samples=100, centers=2, random_state=0, cluster_std=0.8)
    
    fig, ax = plt.subplots(1, 3, figsize=(16, 6))
    fig.subplots_adjust(left=0.0625, right=0.95, wspace=0.1)
    
    for axi, gamma in zip(ax, [10.0, 1.0, 0.1]):
        model = SVC(kernel='rbf', gamma=gamma)
        model.fit(X, y)
        axi.scatter(X[:, 0], X[:, 1], c=y, s=50, cmap='autumn')
        plot_SVC_decision_function(model, axi)
        axi.scatter(model.support_vectors_[:, 0],
                    model.support_vectors_[:, 1],
                    s=300, lw=1, facecolors='none')
        axi.set_title('gamma={0:.1f}'.format(gamma), size=14)
    

       結果如下:

       可以看出,當這個參數較大時,可以看出模型分類效果很好,但是泛化能力不太好。當這個參數較小時,可以看出模型裏面有些分類是錯誤的,但是這個泛化能力更好,一般也應有的更多。

      通過這個簡單的例子,我們對支持向量機在SVM中的基本使用,以及軟間隔參數的調整,還有核函數變換和gamma值等一些參數的比較。

      完整代碼請參考我的GitHub(地址:https://github.com/LeBron-Jian/MachineLearningNote)。

    9,SVM調參實例2

      下面我們用一個實例學習SVM RBF分類調參(此例子是劉建平老師的博客內容,鏈接在文後)。

      首先,我們生成一些隨機數據,為了讓數據難一點,我們加入了一些噪音,代碼如下:

    # _*_coding:utf-8_*_
    import numpy as np
    import matplotlib.pyplot as plt
    from sklearn.svm import SVC
    from sklearn.datasets import make_moons, make_circles
    from sklearn.preprocessing import StandardScaler
    from matplotlib.colors import ListedColormap
    
    X, y = make_circles(noise=0.2, factor=0.5, random_state=1)
    # 對數據進行標準化
    X = StandardScaler().fit_transform(X)
    
    # 下面看看數據長什麼樣子
    cm = plt.cm.RdBu
    cm_birght = ListedColormap(['#FF0000', '#0000FF'])
    ax = plt.subplot()
    
    ax.set_title('Input data')
    # plot the training points
    ax.scatter(X[:, 0], X[:, 1], c=y, cmap=cm_birght)
    ax.set_xticks([])
    ax.set_yticks([])
    plt.tight_layout()
    plt.show()
    

      上面代碼對數據做了標準化,注意做標準化和不做標準化的差異(不一定所有的數據標準化后的效果更好,但是絕大多數確實更好)。比如下圖:

      我們看,當不做數據標準化,我們x1的取值範圍由0~90不等,當做了數據標準化之後,其取值範圍就在-2~2之間了。說明標準化的作用還是很明顯的,不多贅述,下面繼續。

       生成的數據如下(可能下一次運行,就變了哈):

       知道數據長什麼樣了,下面我們要對這個數據集進行SVM RBF分類了,分類時我們採用了網格搜索,在C=(0.1, 1, 10)和 gamma=(1, 0.1, 0.01)形成的9種情況中選擇最好的超參數,我們用了4折交叉驗證。這裏只是一個例子,實際運用中,可能需要更多的參數組合來進行調參。

      代碼及其結果如下:

    # 網格搜索尋找最佳參數
    grid = GridSearchCV(SVC(), param_grid={'C': [0.1, 1, 10], 'gamma': [1, 0.1, 0.01]}, cv=4)
    grid.fit(X, y)
    print("The best parameters are %s with a score of %0.2f"
          % (grid.best_params_, grid.best_score_))
    # The best parameters are {'C': 10, 'gamma': 0.1} with a score of 0.91
    

       就是說,我們通過網格搜索,在我們給定的9組超參數組合中,C=10, gamma=0.1 分數最高,這就是我們最終的參數候選。

      下面我們看看SVM分類后的可視化,這裏我們把上面九種組合各個訓練后,通過對網格里的點預測來標色,觀察分類的效果圖,代碼如下:

    # SVM 分類後進行可視化
    x_min, x_max = X[:, 0].min(), X[:, 0].max() + 1
    y_min, y_max = X[:, 1].min(), X[:, 1].max() + 1
    xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.02),
                         np.arange(y_min, y_max, 0.02))
    
    for i, C in enumerate((0.1, 1, 10)):
        for j, gamma in enumerate((1, 0.1, 0.01)):
            # plt.subplot()
            clf = SVC(C=C, gamma=gamma)
            clf.fit(X, y)
            Z = clf.predict(np.c_[xx.ravel(), yy.ravel()])
    
            # put the result into a color plot
            Z = Z.reshape(xx.shape)
            plt.contourf(xx, yy, Z, cmap=plt.cm.coolwarm)
    
            # Plot also the training points
            plt.scatter(X[:, 0], X[:, 1], c=y, cmap=plt.cm.coolwarm)
    
            plt.xlim(xx.min(), xx.max())
            plt.ylim(yy.min(), yy.max())
            plt.xticks(())
            plt.yticks(())
            plt.xlabel(" gamma=" + str(gamma) + " C=" + str(C))
            plt.show()
    

       結果如下:

       從我測試的結果來看,劉老師的代碼還是有一點點問題,显示不出九個,所以這裏我打算重新學習一個例子。

       完整代碼請參考我的GitHub(地址:https://github.com/LeBron-Jian/MachineLearningNote)。

    10,SVM調參實例3(非線性支持向量機)

      非線性的話,我們一方面可以利用核函數構造出非線性,一方面我們可以自己構造非線性。下面首先學習自己構造非線性。

    10.1 自己構造非線性數據

      我們構造非線性數據的代碼如下:

    # _*_coding:utf-8_*_
    import numpy as np
    import matplotlib.pyplot as plt
    from sklearn.svm import SVC
    from sklearn.datasets import make_moons, make_circles
    from sklearn.preprocessing import StandardScaler
    
    X1D = np.linspace(-4, 4, 9).reshape(-1, 1)
    # np.c_是按行連接兩個矩陣,就是把兩矩陣左右相加,要求行數相等。
    X2D = np.c_[X1D, X1D ** 2]
    y = np.array([0, 0, 1, 1, 1, 1, 1, 0, 0])
    
    plt.figure(figsize=(11, 4))
    
    plt.subplot(121)
    plt.grid(True, which='both')
    plt.axhline(y=0, color='k')
    plt.plot(X1D[:, 0][y == 0], np.zeros(4), 'bs')
    plt.plot(X1D[:, 0][y == 1], np.zeros(5), 'g*')
    plt.gca().get_yaxis().set_ticks([])
    plt.xlabel(r'$x_1$', fontsize=20)
    plt.axis([-4.5, 4.5, -0.2, 0.2])
    
    plt.subplot(122)
    plt.grid(True, which='both')
    plt.axhline(y=0, color='k')
    plt.axvline(x=0, color='k')
    plt.plot(X2D[:, 0][y == 0], X2D[:, 1][y == 0], 'bs')
    plt.plot(X2D[:, 0][y == 1], X2D[:, 1][y == 1], 'g*')
    plt.xlabel(r'$x_1$', fontsize=20)
    plt.ylabel(r'$x_2$', fontsize=20, rotation=0)
    plt.gca().get_yaxis().set_ticks([0, 4, 8, 12, 16])
    plt.plot([-4.5, 4.5], [6.5, 6.5], 'r--', linewidth=3)
    plt.axis([-4.5, 4.5, -1, 17])
    
    plt.subplots_adjust(right=1)
    plt.show()
    

       圖如下:

      從這個圖可以看到,我們利用對數據的變換,可以對數據的維度增加起來,變成非線性。

       假設我們不使用核函數的思想,先對數據做變換,看能不能達到一個比較好的結果,首先我們做一個測試的數據,代碼如下:

    # _*_coding:utf-8_*_
    import numpy as np
    import matplotlib.pyplot as plt
    from sklearn.datasets import make_moons
    
    X, y = make_moons(n_samples=100, noise=0.15, random_state=42)
    
    
    def plot_dataset(X, y, axes):
        plt.plot(X[:, 0][y == 0], X[:, 1][y == 0], 'bs')
        plt.plot(X[:, 0][y == 1], X[:, 1][y == 1], 'g*')
        plt.axis(axes)
        plt.grid(True, which='both')
        plt.xlabel(r'$x_1$', fontsize=20)
        plt.ylabel(r'$x_2$', fontsize=20, rotation=0)
    
    plot_dataset(X, y, [-1.5, 2.5, -1, 1.5])
    plt.show()
    

       生成的圖如下:

       下面代碼將兩類數據分出來了:

    Polynomial_svm_clf = Pipeline((('poly_features', PolynomialFeatures(degree=3)),
                                   ('scaler', StandardScaler()),
                                   ('svm_clf', LinearSVC(C=10))
                                   ))
    Polynomial_svm_clf.fit(X, y)
    
    def plot_predictions(clf, axes):
        x0s = np.linspace(axes[0], axes[1], 100)
        x1s = np.linspace(axes[2], axes[3], 100)
        x0, x1 = np.meshgrid(x0s, x1s)
        X = np.c_[x0.ravel(), x1.ravel()]
        y_pred = clf.predict(X).reshape(x0.shape)
        # 下面填充一個等高線, alpha表示透明度
        plt.contourf(x0, x1, y_pred, cmap=plt.cm.brg, alpha=0.2)
    
    plot_predictions(Polynomial_svm_clf, [-1.5, 2.5, -1, 1.5])
    plot_dataset(X, y, [-1.5, 2.5, -1, 1.5])
    plt.show()
    

       結果如下:

       從結果來看,我們使用線性支持向量機將兩類數據區分開是沒有問題的。而最重要的是我們如何使用核函數呢?下面繼續學習

    10.2 如何對非線性數據進行核函數的變換

       我們首先看svm的官方文檔:

       核函數默認是 rbf,也就是徑向基核函數。下面分別演示核函數。

      我們依舊拿上面的數據,首先取核函數為 多項式核 看看效果(這裏對比的是多項式核的degree,也就是多項式核的維度):

    # _*_coding:utf-8_*_
    import numpy as np
    import matplotlib.pyplot as plt
    from sklearn.datasets import make_moons
    from sklearn.pipeline import Pipeline
    from sklearn.preprocessing import PolynomialFeatures
    from sklearn.preprocessing import StandardScaler
    from sklearn.svm import LinearSVC, SVC
    
    X, y = make_moons(n_samples=100, noise=0.15, random_state=42)
    
    def plot_dataset(X, y, axes):
        plt.plot(X[:, 0][y == 0], X[:, 1][y == 0], 'bs')
        plt.plot(X[:, 0][y == 1], X[:, 1][y == 1], 'g*')
        plt.axis(axes)
        plt.grid(True, which='both')
        plt.xlabel(r'$x_1$', fontsize=20)
        plt.ylabel(r'$x_2$', fontsize=20, rotation=0)
    
    # 展示圖像
    def plot_predictions(clf, axes):
        x0s = np.linspace(axes[0], axes[1], 100)
        x1s = np.linspace(axes[2], axes[3], 100)
        x0, x1 = np.meshgrid(x0s, x1s)
        X = np.c_[x0.ravel(), x1.ravel()]
        y_pred = clf.predict(X).reshape(x0.shape)
        # 下面填充一個等高線, alpha表示透明度
        plt.contourf(x0, x1, y_pred, cmap=plt.cm.brg, alpha=0.2)
    
    
    Poly_kernel_svm_clf = Pipeline((('scaler', StandardScaler()),
                                    ('svm_clf', SVC(kernel='poly', degree=3, coef0=1, C=5))
                                    ))
    Poly_kernel_svm_clf.fit(X, y)
    # 下面做一個對比試驗,看看degree的值的變換
    Poly_kernel_svm_clf_plus = Pipeline((('scaler', StandardScaler()),
                                         ('svm_clf', SVC(kernel='poly', degree=10, coef0=1, C=5))
                                         ))
    Poly_kernel_svm_clf_plus.fit(X, y)
    
    plt.subplot(121)
    plot_predictions(Poly_kernel_svm_clf, [-1.5, 2.5, -1, 1.5])
    plot_dataset(X, y, [-1.5, 2.5, -1, 1.5])
    plt.title(r'$d=3, r=1, C=5$', fontsize=18)
    
    plt.subplot(122)
    plot_predictions(Poly_kernel_svm_clf, [-1.5, 2.5, -1, 1.5])
    plot_dataset(X, y, [-1.5, 2.5, -1, 1.5])
    plt.title(r'$d=10, r=100, C=5$', fontsize=18)
    plt.show()
    

       結果如下:

       我們是把數據映射到高維空間,然後再拿回來看效果,實際上並沒有去高維空間做運算。。這就是我們想要展示的多項式核函數,下面學習高斯核函數。

      高斯核函數:利用相似度來變換特徵

      我們選擇一份一維數據,並在 x1=-2,  x1=1 處為其添加兩個高斯函數,接下來讓我門將相似度函數定義為 gamma=0.3 的徑向基核函數(RBF):

      例如: x1 = -1:它位於距第一個地標距離為1的地方,距離第二個地標距離為2。因此其新特徵為 x2 = exp(-0.3*1^2)=0.74 ,並且  x3 = exp(-0.3 * 2^2)=0.3。

      圖如下:

       這裏說一下,就是假設 X2和 X3為兩個高斯函數,我們看 x這個點距離兩個地標的距離。離高斯分佈的中心越近,就越發生什麼。。經過計算出來距離兩個地標的距離,我們就可以依此類推,來計算所有一維坐標相對應的二維坐標。(二維坐標就是距離兩個高斯函數的距離)。

      我們這裏用相似度特徵來替換原本的特徵。

      下面我們做一個實驗,我們只看 gamma的變換,高斯函數的開口變化:

    X1D = np.linspace(-4, 4, 9).reshape(-1, 1)
    X2D = np.c_[X1D, X1D ** 2]
    
    X, y = make_moons(n_samples=100, noise=0.15, random_state=42)
    
    
    def gaussian_rbf(x, landmark, gamma):
        return np.exp(-gamma * np.linalg.norm(x - landmark, axis=1) ** 2)
    
    
    gamma = 0.3
    
    # 下面進行訓練,得到一個支持向量機的模型(這裏我們沒有訓練,直接畫出來了)
    # 因為測試的數據是我們自己寫的,為了方便,我們自己畫出來,當然你也可以自己做
    xls = np.linspace(-4.5, 4.5, 200).reshape(-1, 1)
    x2s = gaussian_rbf(xls, -2, gamma)
    x3s = gaussian_rbf(xls, 1, gamma)
    
    XK = np.c_[gaussian_rbf(X1D, -2, gamma), gaussian_rbf(X2D, 1, gamma)]
    yk = np.array([0, 0, 1, 1, 1, 1, 1, 0, 0])
    
    plt.figure(figsize=(11, 4))
    
    # plt.subplot(121)
    plt.grid(True, which='both')
    plt.axhline(y=0, color='k')
    plt.scatter(x=[-2, 1], y=[0, 0], s=150, alpha=0.5, c='red')
    plt.plot(X1D[:, 0][yk == 0], np.zeros(4), 'bs')
    plt.plot(X1D[:, 0][yk == 1], np.zeros(5), 'g*')
    plt.plot(xls, x2s, 'g--')
    plt.plot(xls, x3s, 'b:')
    plt.gca().get_yaxis().set_ticks([0, 0.25, 0.5, 0.75, 1])
    plt.xlabel(r'$x_1$', fontsize=20)
    plt.ylabel(r'Similarity', fontsize=14)
    
    plt.annotate(r'$\mathbf{x}$',
                 xy=(X1D[3, 0], 0),
                 xytest=(-0.5, 0.20),
                 ha='center',
                 arrowprops=dict(facecolor='black', shrink=0.1),
                 fontsize=18,
                 )
    plt.text(-2, 0.9, "$x_2$", ha='center', fontsize=20)
    plt.text(1, 0.9, "$x_3$", ha='center', fontsize=20)
    plt.axis([-4.5, 4.5, -0.1, 1.1])
    

       結果如下(下面我們分別調試gamma,分為0.3   0.8):

       理論情況下,我們會得到怎麼維特徵呢?可以對每一個實例(樣本數據點)創建一個地標,此時會將mn 的訓練集轉換成 mm 的訓練集(m表示樣本個數,n表示特徵維度個數)。

      SVM中利用核函數的計算技巧,大大降低了計算複雜度

    • 增加gamma 使高斯曲線變窄,因此每個實例的影響範圍都較小,決策邊界最終變得不規則,在個別實例周圍擺動
    • 減少gamma 使高斯曲線變寬,因此實例具有更大的影響範圍,並且決策邊界更加平滑

       下面做一個對比試驗(gamma值(0.1  0.5), C值(0.001, 1000)):

    rbf_kernel_svm_clf = Pipeline((('scaler', StandardScaler()),
                                   ('svm_clf', SVC(kernel='rbf', gamma=5, C=0.001))
                                   ))
    
    gamma1, gamma2 = 0.1, 5
    C1, C2 = 0.001, 1000
    hyperparams = (gamma1, C1), (gamma1, C2), (gamma2, C1), (gamma2, C2)
    
    svm_clfs = []
    for gamma, C in hyperparams:
        rbf_kernel_svm_clf.fit(X, y)
        svm_clfs.append(rbf_kernel_svm_clf)
    
    plt.figure(figsize=(11, 7))
    
    for i, svm_clfs in enumerate(svm_clfs):
        plt.subplot(221 + i)
        plot_predictions(svm_clfs, [-1.5, 2.5, -1, 1.5])
        plot_dataset(X, y, [-1.5, 2.5, -1, 1.5])
        gamma, C = hyperparams[i]
        plt.title(r'$\gamma={}, C={}$'.format(gamma, C), fontsize=16)
    plt.show()
    

       結果如下:

       我們看第一幅圖,邊界比較平穩,沒有過擬合的風險,我們看當 gamma比較大的時候,過擬合的風險卻比較大了。所以說最終我們看的還是高斯函數的開口大還是小,大一點,也就是gamma小,過擬合風險小,反之同理。

      完整代碼請參考我的GitHub(地址:https://github.com/LeBron-Jian/MachineLearningNote)。

    完整代碼及其數據,請移步小編的GitHub

      傳送門:請點擊我

      如果點擊有誤:https://github.com/LeBron-Jian/MachineLearningNote

     

     

    參考文獻:https://blog.csdn.net/BIT_666/article/details/79979580

    https://www.cnblogs.com/tonglin0325/p/6107114.html

    https://cloud.tencent.com/developer/article/1146077

    https://www.cnblogs.com/xiaoyh/p/11604168.html

    https://www.cnblogs.com/pinard/p/6126077.html

    https://www.cnblogs.com/pinard/p/6117515.html

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

    網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

    ※Google地圖已可更新顯示潭子電動車充電站設置地點!!

    ※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

    ※別再煩惱如何寫文案,掌握八大原則!

    網頁設計最專業,超強功能平台可客製化

  • 您的單例模式,真的單例嗎?

    您的單例模式,真的單例嗎?

          單例模式,大家恐怕再熟悉不過了,其作用與實現方式有多種,這裏就不啰嗦了。但是,咱們在使用這些方式實現單例模式時,程序中就真的會只有一個實例嗎?

          聰明的你看到這樣的問話,一定猜到了答案是NO。這裏筆者就不賣關子了,開門見山吧!實際上,在有些場景下,如果程序處理不當,會無情地破壞掉單例模式,導致程序中出現多個實例對象。

          下面筆者介紹筆者已知的三種破壞單例模式的方式以及避免方法。

    1、反射對單例模式的破壞

          我們先通過一個例子,來直觀感受一下

        (1)案例

      DCL實現的單例模式:

     1 public class Singleton{
     2     private static volatile Singleton mInstance;
     3     private Singleton(){}
     4     public static Singleton getInstance(){
     5         if(mInstance == null){
     6             synchronized (Singleton.class) {
     7                 if(mInstance == null){
     8                     mInstance = new Singleton();
     9                 }
    10             }
    11         }
    12         return mInstance;
    13     }
    14 }

      測試代碼:

     1 public class SingletonDemo {
     2 
     3     public static void main(String[] args){
     4         Singleton singleton = Singleton.getInstance();
     5         try {
     6             Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
     7             constructor.setAccessible(true);
     8             Singleton reflectSingleton = constructor.newInstance();
     9             System.out.println(reflectSingleton == singleton);
    10         } catch (Exception e) {
    11             // TODO Auto-generated catch block
    12             e.printStackTrace();
    13         }
    14     }
    15 }

      執行結果:

    false

          運行結果說明,採用反射的方式另闢蹊徑實例了該類,導致程序中會存在不止一個實例。

        (2)解決方案

          其思想就是採用一個全局變量,來標記是否已經實例化過了,如果已經實例化過了,第二次實例化的時候,拋出異常。實現代碼如下:

     1 public class Singleton{
     2     private static volatile Singleton mInstance;
     3     private static volatile boolean mIsInstantiated = false;
     4     private Singleton(){
     5         if (mIsInstantiated){
     6             throw new RuntimeException("Has been instantiated, can not do it again!");
     7         }
     8         mIsInstantiated = true;
     9     }
    10     public static Singleton getInstance(){
    11         if(mInstance == null){
    12             synchronized (Singleton.class) {
    13                 if(mInstance == null){
    14                     mInstance = new Singleton();
    15                 }
    16             }
    17         }
    18         return mInstance;
    19     }
    20 }

    執行結果:

     

         這種方式看起來比較暴力,運行時直接拋出異常。

     

    2、clone()對單例模式的破壞 

           當需要實現單例的類允許clone()時,如果處理不當,也會導致程序中出現不止一個實例。

        (1)案例

      一個實現了Cloneable接口單例類:

     1 public class Singleton implements Cloneable{
     2     private static volatile Singleton mInstance;
     3     private Singleton(){
     4     }
     5     public static Singleton getInstance(){
     6         if(mInstance == null){
     7             synchronized (Singleton.class) {
     8                 if(mInstance == null){
     9                     mInstance = new Singleton();
    10                 }
    11             }
    12         }
    13         return mInstance;
    14     }
    15     @Override
    16     protected Object clone() throws CloneNotSupportedException {
    17         // TODO Auto-generated method stub
    18         return super.clone();
    19     }
    20 }

      測試代碼:

     1 public class SingletonDemo {
     2 
     3     public static void main(String[] args){
     4         try {
     5             Singleton singleton = Singleton.getInstance();
     6             Singleton cloneSingleton;
     7             cloneSingleton = (Singleton) Singleton.getInstance().clone();
     8             System.out.println(cloneSingleton == singleton);
     9         } catch (CloneNotSupportedException e) {
    10             e.printStackTrace();
    11         }
    12     }
    13 }

    執行結果:

    false

      (2)解決方案:

         解決思想是,重寫clone()方法,調clone()時直接返回已經實例的對象

     1 public class Singleton implements Cloneable{
     2     private static volatile Singleton mInstance;
     3     private Singleton(){
     4     }
     5     public static Singleton getInstance(){
     6         if(mInstance == null){
     7             synchronized (Singleton.class) {
     8                 if(mInstance == null){
     9                     mInstance = new Singleton();
    10                 }
    11             }
    12         }
    13         return mInstance;
    14     }
    15     @Override
    16     protected Object clone() throws CloneNotSupportedException {
    17         return mInstance;
    18     }
    19 }

    執行結果:

    true

     

    3、序列化對單例模式的破壞

       在使用序列化/反序列化時,也會出現產生新實例對象的情況。

      (1)案例

          一個實現了序列化接口的單例類:

     1 public class Singleton implements Serializable{
     2     private static volatile Singleton mInstance;
     3     private Singleton(){
     4     }
     5     public static Singleton getInstance(){
     6         if(mInstance == null){
     7             synchronized (Singleton.class) {
     8                 if(mInstance == null){
     9                     mInstance = new Singleton();
    10                 }
    11             }
    12         }
    13         return mInstance;
    14     }
    15 }

        測試代碼:

     1 public class SingletonDemo {
     2 
     3     public static void main(String[] args){
     4         try {
     5             Singleton singleton = Singleton.getInstance();
     6             FileOutputStream fos = new FileOutputStream("singleton.txt");
     7             ObjectOutputStream oos = new ObjectOutputStream(fos);
     8             oos.writeObject(singleton);
     9             oos.close();
    10             fos.close();
    11 
    12             FileInputStream fis = new FileInputStream("singleton.txt");
    13             ObjectInputStream ois = new ObjectInputStream(fis);
    14             Singleton serializedSingleton = (Singleton) ois.readObject();
    15             fis.close();
    16             ois.close();
    17             System.out.println(serializedSingleton==singleton);
    18         } catch (Exception e) {
    19             e.printStackTrace();
    20         }
    21 
    22     }
    23 }

         運行結果:

    false

        (2)解決方案

        在反序列化時的回調方法 readResolve()中返回單例對象。

     1 public class Singleton implements Serializable{
     2     private static volatile Singleton mInstance;
     3     private Singleton(){
     4     }
     5     public static Singleton getInstance(){
     6         if(mInstance == null){
     7             synchronized (Singleton.class) {
     8                 if(mInstance == null){
     9                     mInstance = new Singleton();
    10                 }
    11             }
    12         }
    13         return mInstance;
    14     }
    15 
    16     protected Object readResolve() throws ObjectStreamException{
    17         return mInstance;
    18     }
    19 }

    結果:

    true

     

           以上就是筆者目前已知的三種可以破壞單例模式的場景以及對應的解決辦法,讀者如果知道還有其他的場景,記得一定要分享出來噢,正所謂“獨樂樂不如眾樂樂”!!!

           單例模式看起來是設計模式中最簡單的一個,但“麻雀雖小,五臟俱全”,其中有很多細節都是值得深究的。即便是本篇介紹的這幾個場景,也只是介紹了一些梗概而已,很多細節還需要讀者自己去試驗和推敲的,比如:通過枚舉方式實現單例模式,就不存在上述問題,而其它的實現方式似乎都存在上述問題!

     

           後記

           本篇參(剽)考(竊)了如下資料:

           高洪岩的《Java 多線程編程核心技術》

           博文:https://blog.csdn.net/fd2025/article/details/79711198

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

    網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

    ※Google地圖已可更新顯示潭子電動車充電站設置地點!!

    ※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

    ※別再煩惱如何寫文案,掌握八大原則!

    網頁設計最專業,超強功能平台可客製化

  • 都是30萬德系轎車,頂配邁騰和奧迪A4L到底差多少?

    都是30萬德系轎車,頂配邁騰和奧迪A4L到底差多少?

    其實邁騰多出來的配置不止這些,還有二十多項,全部寫出來太長,故略去。低配的奧迪A4L採用了低功率的2。0T發動機,最大馬力是190匹,最大扭矩是320牛•米。頂配的邁騰使用的2。0T發動機賬面數據更好,220匹的最大馬力和350牛•米的最大扭矩均優於A4L。

    隨着平台化,規模化生產的普及,很多大集團都會在自己的普通品牌和豪華品牌上通用零件,但是定價卻差了幾萬,那麼這幾萬的差價真的如大家所說,是LOGO的差異嗎?同一個集團,應該買TA的普通品牌高配車型還是豪華品牌的低配車型比較划算?

    今天,我們來對比一下大眾邁騰的380TSI 旗艦版和奧迪A4L的40TFSI 進取版,看看哪個更值得買。

    邁騰和奧迪A4L的外觀是同一個風格的兩份答卷。邁騰的橫貫式前臉把大燈連接起來,從視覺上來看顯得更寬更低矮。今天對比的邁騰380TSI 旗艦版配備的是18寸輪轂,所以邁騰的側面看上去更加大氣一些。

    奧迪A4L則是在原有的六邊形格柵的家族設計元素上再下功夫,把大燈、格柵、車身線條設計得再銳利一些,所以奧迪A4L的外觀顯得更加年輕。不過這17寸的輪轂嘛,說實話不是很能襯托出A4L的檔次,建議後期更換。

    這一局,邁騰略勝。

    這一代的邁騰是歐版帕薩特直接拿來生產的,所以這一代邁騰的風格反而是繼承了上一代帕薩特的商務風。在邁騰上,木紋飾板貫穿整个中控台,空調出風口被設計得更高,所以空間更加寬闊。奧迪A4L的內飾以科技感為主要設計導向,奧迪A4L 30TSI 進取版採用中控台採用了銀色拉絲面板,中控屏採用懸浮式設計。可惜的是低配車沒有使用全液晶显示屏,空調面板也不是高配的那款金屬質感控制面板。所以質感稍顯不足。

    這一局,也是邁騰勝出。

    無論是邁騰還是奧迪A4L,引進國內后,都進行了不同程度的加長,邁騰的軸距為2871mm,比海外版增加了80mm,奧迪A4L的軸距是2908mm,比海外版增加了88mm。雖然奧迪A4L的軸距比邁騰要長一點,但是乘坐空間方面,邁騰比奧迪A4L要寬敞一點。這是因為邁騰是MQB(發動機橫置平台)的產物,而奧迪A4L是MLB(發動機縱置平台)的產物。發動機橫置的結構比較緊湊,占乘坐空間較少,所以邁騰的乘坐空間比奧迪A4L稍微大一點。

    這一局,因為兩者的差距微乎其微,所以判定兩者打成平手。

    在配置上,邁騰完勝沒什麼懸念。其實邁騰多出來的配置不止這些,還有二十多項,全部寫出來太長,故略去。

    低配的奧迪A4L採用了低功率的2.0T發動機,最大馬力是190匹,最大扭矩是320牛•米。頂配的邁騰使用的2.0T發動機賬面數據更好,220匹的最大馬力和350牛•米的最大扭矩均優於A4L。從加速成績來看,邁騰比奧迪A4L更好,但也只是半秒的差距。奧迪A4L的發動機有AVS可變氣門升程技術和混合噴射技術。這兩項技術都可以提高燃油經濟性,所以從用戶反饋的數據來看,奧迪A4L比邁騰節油不少。

    所以這局,也只能判五五開。

    說到舒適性方面奧迪A4L的前後懸挂都使用了五連桿獨立懸挂,而邁騰則採用了前麥弗遜獨立懸挂后多連桿獨立懸挂的組合。這不僅是因為成本,還因為邁騰使用的MQB佔據了機艙空間,所以無法布局多連桿懸挂。麥弗遜式獨立懸挂有個缺點,那就是上下運動的時候對車輪外傾角變化明顯,而車輪外傾角變化會影響操縱的穩定性。所以,為了安全性考慮,麥弗遜懸挂一般都會比較硬。而使用五連桿的奧迪A4L可以把懸挂調得軟一些,所以奧迪A4L的濾震更好。在加上在隔音材料方面,豪華品牌比普通品牌的投入會更大。

    所以舒適性方面,奧迪A4L完勝邁騰是沒什麼懸念的。

    總結:縱觀兩車各個方面的對比,互有勝負。豪華品牌的車不是只掛着一個LOGO就賣這麼貴,如果你坐過邁騰后再坐A4L,你確確實實能夠感受到豪華車帶給你的那種舒適感,高級感。但是在靜態體驗的時候,邁騰的頂配版可以說是完虐奧迪A4L了。所以,我建議,如果你打算購買一台商務車,邁騰的頂配版更加合適。如果只是買來自用,奧迪A4L值得考慮。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

    ※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

    ※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

    南投搬家公司費用需注意的眉眉角角,別等搬了再說!

    ※教你寫出一流的銷售文案?

  • 【溫故知新】 編程原則和方法論

    寫了這麼多年代碼,依舊做不好一個項目

    做好一個項目是人力、產品、業務、技術、運營的結合,可能還疊加一點時機的因素,就我們碼農而言,工作就是搬磚,實現產品, 給業務提供支撐。
    “給祖傳代碼加 BUG 修 BUG”,“拿起鍵盤一把梭”這些戲謔程序員的話,聽多了真的會讓程序員麻木,彷彿大家都是這麼乾的。
    從業多年,堆過 shi 山,接手過祖傳代碼, 已經不能沉下氣去查看、調試 shi 山代碼, 說實話,很累。
    本人一直推崇寫流暢、自然、可自解釋的代碼,讓優雅成為一種習慣, 給自己留個念想、給後人留個好評。

    溫故而知新,聊一聊現代編程幾大常見的編程原則

    普世原則
    KISS (Keep It Simple Stupid) 保持系統結構簡單可信賴
    YAGNI (you aren’t gonna need it) 當前確實需要,再去做
    Do The Simplest Things That Could Possibly Work 思考最簡單可行的辦法
    Separation of Concerns 關注點分離
    Keep Things DRY 保持代碼結構清爽 Don’t repeat yourself
    Code For The Maintainer 站在維護者角度寫代碼
    Avoid Premature Optimization 避免提前優化
    Boy-Scout Rule 清掃戰場:清理口水話註釋、無效代碼
    模塊(類)間
    Minimise Coupling 低耦合
    Law of Demeter Don’t talk to strangers,對象方法只接觸該接觸的對象、字段、入參
    Composition Over Inheritance 組合而不是繼承
    Orthogonality 正相關,概念上不相關的事物不應在系統中強行相關
    Robustness Principle 代碼健壯性
    Inversion of Control 控制反轉
    模塊(類)
    Maximise Cohesion 高內聚
    Likov Substitution Principle 里斯替代原則:將程序中對象替換到子類型實例,不會報錯。
    Open/Closed Principle 設計的實體對擴展開放,對修改關閉
    Single Responsiblity Principle 單一責任原則
    Hide Implementation Details 隱藏實施細節
    Curly’s Law 柯里定律:為確定目標編寫特定代碼
    Encapsulate What Changes 封裝變化
    Interface Segregation Principle 接口隔離原則
    Command Query Separation 命令查詢分離

    KISS
    大多數系統保持簡單,會運行的很好。

    • 更少的代碼消耗更好的時間,產生更少的 bug,並且容易修改
    • 複雜業務都是由簡單代碼堆砌而成
    • 完美並不是“沒有什麼東西可以再加”,而是“沒有什麼東西可以被去掉”

    YAGNI
    YAGNI 代表“you aren’t gonna need it.”,不要自以為是的提前實現某些邊角,直到真正需要的時候,再來做。

    • 提前做明天才需要做的工作,意味着當前迭代中需要花費更多精力
    • 導致代碼膨脹,軟件變得臃腫且複雜

    Separation of Concerns
    關注點分離是一種將計算機程序分為不同部分的設計原則,這樣每個部分都可以解決一個單獨的關注點。例如應用程序的業務邏輯是一個問題,而用戶界面是另外一個問題,更改用戶界面不應要求更改業務邏輯,反之亦然。

    • 簡化應用程序的開發和維護
    • 如果關注點分離得很好,則各個部分可以重複使用,也可以獨立開發和更新。

    Interface Segregation Principle
    接口隔離,將胖接口修改為多個小接口,調用接口的代碼應該比實現接口的代碼更依賴於接口。
    why:
    如果一個類實現了胖接口的所有方法(部分方法在某次調用時並不需要),那麼在該次調用時我們就會發現此時出現了(部分並不需要的方法),而並沒有機制告訴我們我們現在不應該使用這部分方法。
    how: 避免胖接口,類永遠不必實現違反單一職責原則的接口。可以根據實際多職責劃分為多接口,類實現多接口后, 在調用時以特定接口指代對象,這樣這個對象只能體現特定接口的方法,以此體現接口隔離。

       public interface IA
        {
            void getA();
        }
    
        interface IB
        {
            void getB();
        }
    
        public class Test : IA, IB
        {
            public string Field { get; set; }
            public void getA()
            {
                throw new NotImplementedException();
            }
    
            public void getB()
            {
                throw new NotImplementedException();
            }
        }
        class Program
        {
            static void Main(string[] args)
            {
                Console.WriteLine("Hello World!");
    
                IA a = new Test();
                a.getA();       //  在這個調用處只能看到接口IA的方法, 接口隔離
            }
        }
    

    Command Query Separation
    命令查詢分離: 操作方法就只寫操作邏輯,查詢方法就只寫查詢邏輯,並以明顯的方法名區分自己的動作。
    有了這個原則,程序員可以更加自信地進行編碼:由於查詢方法不會改變狀態,因此可以在任何地方以任何順序使用,使用操作方法時,也心中有數。

    End

    懂得這麼多道理,卻依舊過不好這一生。前人總結的編程原則和方法論需要在實踐中感悟,束之高閣,則始終不能體會編程的魅力和快感

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

    網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

    ※Google地圖已可更新顯示潭子電動車充電站設置地點!!

    ※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

    ※別再煩惱如何寫文案,掌握八大原則!

    網頁設計最專業,超強功能平台可客製化

  • 突變冠狀病毒株恐降疫苗效力 丹麥全面撲殺水貂

    摘錄自2020年11月5日中央社報導

    丹麥總理佛瑞德里克森今(4日)表示,在水貂身上的突變冠狀病毒傳染給人類之後,對未來可能研發出來的疫苗造成風險,因此境內多達1700萬隻水貂將遭全面撲殺。

    丹麥為全世界最大的水貂毛皮生產地,雖然當局自6月以來一再撲殺遭感染的水貂,但境內水貂農場疫情仍持續蔓延。

    佛瑞德里克森(Mette Frederiksen)在記者會上表示,衛生當局發現人體病毒株與水貂身上的病毒株,顯示對抗體的敏感度已降低,有可能影響未來疫苗的效力。

    國際新聞
    丹麥
    水貂

    本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

    網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

    ※Google地圖已可更新顯示潭子電動車充電站設置地點!!

    ※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

    ※別再煩惱如何寫文案,掌握八大原則!

    網頁設計最專業,超強功能平台可客製化

  • 質量硬,性價比無敵,懂車的都說這3款車比BBA更值得買

    質量硬,性價比無敵,懂車的都說這3款車比BBA更值得買

    點評:放在以前,皇冠就是身份的象徵,也算是款足以媲美百萬級別車輛的車子,隨着諸多豪華品牌的入駐以及價格的降低,市場份額確實也受到了一定的衝擊。但秉承豪華穩重路線的18款皇冠在外觀和內飾上進行了年輕化,這一性格上的變化致使皇冠既穩住了老用戶也吸引了不少年輕的消費者。

    在中國,汽車市場有個很奇妙的現象:有時候沒有競爭優勢的車型,卻成為銷量榜單上前幾名的常客;而有些產品競爭力很強的車型,卻好似不怎麼如意。但今天要為大家揭曉的是那些銷量看似不怎樣,在細分市場卻有不錯表現的特殊車型。

    在進行分析點評之前,我們首先來看看本次分析的那些車子在今年前3個月的銷量指數。

    點評:金牛座是福特品牌旗下的一款高端轎車車型,雖非屬豪華品牌但售價快30萬的它,素質可謂非常的高,大氣又低調的外觀配以變態級別的配置,以絕對性的優勢足以和豪華品牌扳手腕。但面對豪華品牌和同級車大眾的帕薩特和邁騰在20幾萬區間所帶來的巨大壓力,在今年1-3月的總銷量中金牛座還是頂住了壓力,賣出了4778輛。就性價比而言,金牛座還是很高的,就是售價有點偏高,如若售價能稍微降低一些,並配以一些購車優惠,相信在接下來的日子里,金牛的銷售表現會更令人滿意。

    點評:放在以前,皇冠就是身份的象徵,也算是款足以媲美百萬級別車輛的車子,隨着諸多豪華品牌的入駐以及價格的降低,市場份額確實也受到了一定的衝擊。但秉承豪華穩重路線的18款皇冠在外觀和內飾上進行了年輕化,這一性格上的變化致使皇冠既穩住了老用戶也吸引了不少年輕的消費者。況且,皇冠本身就擁有不錯的綜合性價比和情懷感,並不會比BBA車型差,如果30萬讓消費者選擇購車,相信不少消費者都會選擇這些開起來更有格調又有品味的車子。

    點評:非豪華品牌的輝昂和奧迪A7、A8等車型屬同平台打造,帶點奧迪影子,並帶有眾多賣點的輝昂在實力上絲毫也不比豪華品牌同級別車型遜色,在國內市場大眾也有不少專屬中國的車子,藉助大眾的品牌影響,輝昂這款車的知名度也有相應的提升。另外從市場行情來說,雖說中大型車的市場仍舊是豪華品牌車的天下,但是大街上千篇一律的豪華品牌車也給了輝昂一定的機遇,憑藉過硬的實力在這个中大型車的市場中還是分得一杯羹。

    總結

    金牛座、皇冠、輝昂三款車都很好,性價比也很不錯,在整個細分市場上表現還算是差強人意!其實吧,覺得金牛座這些怎麼也是比3系和C級車要高上一個等級,轉而有不少的消費者放棄BBA車型,選擇這些二線豪華品牌也不足為奇。畢竟與其選擇開着分分鐘與人撞車的BBA上街,不如選擇這些二線豪華品牌車,開起來更有格調和氣質。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

    網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

    ※Google地圖已可更新顯示潭子電動車充電站設置地點!!

    ※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

    ※別再煩惱如何寫文案,掌握八大原則!

    網頁設計最專業,超強功能平台可客製化

  • 漲姿勢了解一下Kafka消費位移可好?

    漲姿勢了解一下Kafka消費位移可好?

    摘要:Kafka中的位移是個極其重要的概念,因為數據一致性、準確性是一個很重要的語義,我們都不希望消息重複消費或者丟失。而位移就是控制消費進度的大佬。本文就詳細聊聊kafka消費位移的那些事,包括:

    概念剖析

    kafka的兩種位移

    關於位移(Offset),其實在kafka的世界里有兩種位移:

    • 分區位移:生產者向分區寫入消息,每條消息在分區中的位置信息由一個叫offset的數據來表徵。假設一個生產者向一個空分區寫入了 10 條消息,那麼這 10 條消息的位移依次是 0、1、…、9;

    • 消費位移:消費者需要記錄消費進度,即消費到了哪個分區的哪個位置上,這是消費者位移(Consumer Offset)。

    注意,這和上面所說的消息在分區上的位移完全不是一個概念。上面的“位移”表徵的是分區內的消息位置,它是不變的,即一旦消息被成功寫入到一個分區上,它的位移值就是固定的了。而消費者位移則不同,它可能是隨時變化的,畢竟它是消費者消費進度的指示器。

    消費位移

    消費位移,記錄的是 Consumer 要消費的下一條消息的位移,切記,是下一條消息的位移! 而不是目前最新消費消息的位移

    假設一個分區中有 10 條消息,位移分別是 0 到 9。某個 Consumer 應用已消費了 5 條消息,這就說明該 Consumer 消費了位移為 0 到 4 的 5 條消息,此時 Consumer 的位移是 5,指向了下一條消息的位移。

    至於為什麼要有消費位移,很好理解,當 Consumer 發生故障重啟之後,就能夠從 Kafka 中讀取之前提交的位移值,然後從相應的位移處繼續消費,從而避免整個消費過程重來一遍。就好像書籤一樣,需要書籤你才可以快速找到你上次讀書的位置。

    那麼了解了位移是什麼以及它的重要性,我們自然而然會有一個疑問,kafka是怎麼記錄、怎麼保存、怎麼管理位移的呢?

    位移的提交

    Consumer 需要上報自己的位移數據,這個彙報過程被稱為位移提交。因為 Consumer 能夠同時消費多個分區的數據,所以位移的提交實際上是在分區粒度上進行的,即Consumer 需要為分配給它的每個分區提交各自的位移數據。

    鑒於位移提交甚至是位移管理對 Consumer 端的巨大影響,KafkaConsumer API提供了多種提交位移的方法,每一種都有各自的用途,這些都是本文將要談到的方案。

    void commitSync(Duration timeout);
    void commitSync(Map<TopicPartition, OffsetAndMetadata> offsets);
    void commitSync(final Map<TopicPartition, OffsetAndMetadata> offsets, final Duration timeout);
    void commitAsync();
    void commitAsync(OffsetCommitCallback callback);
    void commitAsync(Map<TopicPartition, OffsetAndMetadata> offsets, OffsetCommitCallback callback);
    

    先粗略的總結一下。位移提交分為自動提交和手動提交;而手動提交又分為同步提交和異步提交。

    自動提交

    當消費配置enable.auto.commit=true的時候代表自動提交位移。

    自動提交位移是發生在什麼時候呢?auto.commit.interval.ms默認值是50000ms。即kafka每隔5s會幫你自動提交一次位移。自動位移提交的動作是在 poll()方法的邏輯里完成的,在每次真正向服務端發起拉取請求之前會檢查是否可以進行位移提交,如果可以,那麼就會提交上一次輪詢的位移。假如消費數據量特別大,可以設置的短一點。

    越簡單的東西功能越不足,自動提交位移省事的同時肯定會帶來一些問題。自動提交帶來重複消費和消息丟失的問題:

    • 重複消費: 在默認情況下,Consumer 每 5 秒自動提交一次位移。現在,我們假設提交位移之後的 3 秒發生了 Rebalance 操作。在 Rebalance 之後,所有 Consumer 從上一次提交的位移處繼續消費,但該位移已經是 3 秒前的位移數據了,故在 Rebalance 發生前 3 秒消費的所有數據都要重新再消費一次。雖然你能夠通過減少 auto.commit.interval.ms 的值來提高提交頻率,但這麼做只能縮小重複消費的時間窗口,不可能完全消除它。這是自動提交機制的一個缺陷。

    • 消息丟失: 假設拉取了100條消息,正在處理第50條消息的時候,到達了自動提交窗口期,自動提交線程將拉取到的每個分區的最大消息位移進行提交,如果此時消費服務掛掉,消息並未處理結束,但卻提交了最大位移,下次重啟就從100條那消費,即發生了50-100條的消息丟失。

    手動提交

    當消費配置enable.auto.commit=false的時候代表手動提交位移。用戶必須在適當的時機(一般是處理完業務邏輯后),手動的調用相關api方法提交位移。比如在下面的案例中,我需要確認我的業務邏輯返回true之後再手動提交位移

     while (true) {
         try {
             ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofMinutes(KafkaConfig.pollTimeoutOfMinutes));
             if (!consumerRecords.isEmpty()) {
                 for (ConsumerRecord<String, String> record : consumerRecords) {
                     KafkaMessage kafkaMessage = JSON.parseObject(record.value(), KafkaMessage.class);
                     // 處理業務
                     boolean handleResult = handle(kafkaMessage);
                     if (handleResult) {
                         log.info(" handle success, kafkaMessage={}" ,kafkaMessage);
                     } else {
                         log.info(" handle failed, kafkaMessage={}" ,kafkaMessage);
                     }
                 }
                 // 手動提交offset
                 consumer.commitSync(Duration.ofMinutes(KafkaConfig.pollTimeoutOfMinutes));
            
             } 
         } catch (Exception e) {
             log.info("kafka consume error." ,e);
         }
     }
    

    手動提交明顯能解決消息丟失的問題,因為你是處理完業務邏輯后再提交的,假如此時消費服務掛掉,消息並未處理結束,那麼重啟的時候還會重新消費。

    但是對於業務層面的失敗導致消息未消費成功,是無法處理的。因為業務層的邏輯千變萬化、比如格式不正確,你叫Kafka消費端程序怎麼去處理?應該要業務層面自己處理,記錄失敗日誌做好監控等。

    但是手動提交不能解決消息重複的問題,也很好理解,假如消費0-100條消息,50條時掛了,重啟後由於沒有提交這一批消息的offset,是會從0開始重新消費。至於如何避免重複消費的問題,在這篇文章有說。

    手動提交又分為異步提交和同步提交。

    同步提交

    上面案例代碼使用的是commitSync() ,顧名思義,是同步提交位移的方法。同步提交位移Consumer 程序會處於阻塞狀態,等待 Broker 返回提交結果。同步模式下提交失敗的時候一直嘗試提交,直到遇到無法重試的情況下才會結束。在任何系統中,因為程序而非資源限制而導致的阻塞都可能是系統的瓶頸,會影響整個應用程序的 TPS。當然,你可以選擇拉長提交間隔,但這樣做的後果是 Consumer 的提交頻率下降,在下次 Consumer 重啟回來后,會有更多的消息被重新消費。因此,為了解決這些不足,kafka還提供了異步提交方法。

    異步提交

    異步提交會立即返回,不會阻塞,因此不會影響 Consumer 應用的 TPS。由於它是異步的,Kafka 提供了回調函數,供你實現提交之後的邏輯,比如記錄日誌或處理異常等。下面這段代碼展示了調用 commitAsync() 的方法

     consumer.commitAsync((offsets, exception) -> {
     if (exception != null)
         handleException(exception);
     });
    

    但是異步提交會有一個問題,那就是它沒有重試機制,不過一般情況下,針對偶爾出現的提交失敗,不進行重試不會有太大問題,因為如果提交失敗是因為臨時問題導致的,那麼後續的提交總會有成功的。所以消息也是不會丟失和重複消費的。
    但如果這是發生在關閉消費者或再均衡前的最後一次提交,就要確保能夠提交成功。因此,組合使用commitAsync()commitSync()是最佳的方式。

    try {
        while (true) {
            ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofMinutes(KafkaConfig.pollTimeoutOfMinutes));
            if (!consumerRecords.isEmpty()) {
                 for (ConsumerRecord<String, String> record : consumerRecords) {
                    KafkaMessage kafkaMessage = JSON.parseObject(record.value(), KafkaMessage.class);
                    boolean handleResult = handle(kafkaMessage);             
                 }
                 //異步提交位移               
                 consumer.commitAsync((offsets, exception) -> {
                 if (exception != null)
                     handleException(exception);
                 });
               
            }
        }
    } catch (Exception e) {
        System.out.println("kafka consumer error:" + e.toString());
    } finally {
        try {
            //最後同步提交位移
            consumer.commitSync();
        } finally {
            consumer.close();
        }
    }
    

    讓位移提交更加靈活和可控

    如果細心的閱讀了上面所有demo的代碼,那麼你會發現這樣幾個問題:

    1、所有的提交,都是提交 poll 方法返回的所有消息的位移,poll 方法一次返回1000 條消息,則一次性地將這 1000 條消息的位移一併提交。可這樣一旦中間出現問題,位移沒有提交,下次會重新消費已經處理成功的數據。所以我想做到細粒度控制,比如每次提交100條,該怎麼辦?

    答:可以通過commitSync(Map<TopicPartition, OffsetAndMetadata>)commitAsync(Map<TopicPartition, OffsetAndMetadata>)對位移進行精確控制。

    2、poll和commit方法對於普通的開發人員而言是一個黑盒,無法精確地掌控其消費的具體位置。我都不知道這次的提交,是針對哪個partition,提交上去的offset是多少。

    答:可以通過record.topic()獲取topic信息, record.partition()獲取分區信息,record.offset() + 1獲取消費位移,記住消費位移是指示下一條消費的位移,所以要加一。

    3、我想自己管理offset怎麼辦?一方面更加保險,一方面下次重啟之後可以精準的從數據庫讀取最後的offset就不存在丟失和重複消費了。
    答:可以將消費位移保存在數據庫中。消費端程序使用comsumer.seek方法指定從某個位移開始消費。

    綜合以上幾個可優化點,並結合全文,可以給出一個比較完美且完整的demo:聯合異步提交和同步提交,對處理過程中所有的異常都進行了處理。細粒度的控制了消費位移的提交,並且保守的將消費位移記錄到了數據庫中,重新啟動消費端程序的時候會從數據庫讀取位移。這也是我們消費端程序位移提交的最佳實踐方案。你只要繼承這個抽象類,實現你具體的業務邏輯就可以了。

    public abstract class PrefectCosumer {
        private Map<TopicPartition, OffsetAndMetadata> offsets = new HashMap<>();
        int count = 0;
        public final void consume() {
            Properties properties = PropertiesConfig.getConsumerProperties();
            properties.put("group.id", getGroupId());
            Consumer<String, String> consumer = new KafkaConsumer<>(properties);
            consumer.subscribe(getTopics());
            consumer.poll(0);
            // 把offset記錄到數據庫中 從指定的offset處消費 
            consumer.partitionsFor(getTopics()).stream().map(info ->
            new TopicPartition(getTopics(), info.partition()))
            .forEach(tp -> {
                   consumer.seek(tp, JdbcUtils.queryOffset().get(tp.partition()));   
             });
            try {
                while (true) {
                    ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofMinutes(KafkaConfig.pollTimeoutOfMinutes));
                    if (!consumerRecords.isEmpty()) {
                        for (ConsumerRecord<String, String> record : consumerRecords) {
    
                            KafkaMessage kafkaMessage = JSON.parseObject(record.value(), KafkaMessage.class);
                            boolean handleResult = handle(kafkaMessage);
                            if (handleResult) {
                                //注意:提交的是下一條消息的位移。所以OffsetAndMetadata 對象時,必須使用當前消息位移加 1。
                                offsets.put(new TopicPartition(record.topic(), record.partition()),
                                        new OffsetAndMetadata(record.offset() + 1));
    
                                // 細粒度控制提交 每10條提交一次offset
                                if (count % 10 == 0) {
                                    // 異步提交offset
                                    consumer.commitAsync(offsets, (offsets, exception) -> {
                                        if (exception != null) {
                                            handleException(exception);
                                        }
                                        // 將消費位移再記錄一份到數據庫中
                                        offsets.forEach((k, v) -> {
                                            String s = "insert into kafka_offset(`topic`,`group_id`,`partition_id`,`offset`) values" +
                                                    " ('" + k.topic() + "','" + getGroupId() + "'," + k.partition() + "," + v.offset() + ")" +
                                                    " on duplicate key update offset=values(offset);";
                                            JdbcUtils.insertTable(s);
                                        });
    
    
                                    });
                                }
                                count++;
                            } else {         
                                System.out.println("消費消息失敗 kafkaMessage={}" + getTopics() + getGroupId() + kafkaMessage.toString());                         
                            }
                        }
    
    
                    }
                }
            } catch (Exception e) {
                System.out.println("kafka consumer error:" + e.toString());
            } finally {
                try {
                    // 最後一次提交 使用同步提交offset
                    consumer.commitSync();
                } finally {
                    consumer.close();
                }
    
    
            }
        }
    
    
        /**
         * 具體的業務邏輯
         *
         * @param kafkaMessage
         * @return
         */
        public abstract boolean handle(KafkaMessage kafkaMessage);
    
        public abstract List<String> getTopics();
    
        public abstract String getGroupId();
    
        void handleException(Exception e) {
            //異常處理
        }
    }
    

    控制位移提交的N種方式

    剛剛我們說自己控制位移,使用seek方法可以指定offset消費。那到底怎麼控制位移?怎麼重設消費組位移?seek是什麼?現在就來仔細說說。

    並不是所有的消息隊列都可以重設消費者組位移達到重新消費的目的。比如傳統的RabbitMq,它們處理消息是一次性的,即一旦消息被成功消費,就會被刪除。而Kafka消費消息是可以重演的,因為它是基於日誌結構(log-based)的消息引擎,消費者在消費消息時,僅僅是從磁盤文件上讀取數據而已,所以消費者不會刪除消息數據。同時,由於位移數據是由消費者控制的,因此它能夠很容易地修改位移的值,實現重複消費歷史數據的功能。

    了解如何重設位移是很重要的。假設這麼一個場景,我已經消費了1000條消息后,我發現處理邏輯錯了,所以我需要重新消費一下,可是位移已經提交了,我到底該怎麼重新消費這1000條呢??假設我想從某個時間點開始消費,我又該如何處理呢?

    首先說個誤區:auto.offset.reset=earliest/latest這個參數大家都很熟悉,但是初學者很容易誤會它。大部分朋友都覺得在任何情況下把這兩個值設置為earliest或者latest ,消費者就可以從最早或者最新的offset開始消費,但實際上並不是那麼回事,他們生效都有一個前提條件,那就是對於同一個groupid的消費者,如果這個topic某個分區有已經提交的offset,那麼無論是把auto.offset.reset=earliest還是latest,都將失效,消費者會從已經提交的offset開始消費。因此這個參數並不能解決用戶想重設消費位移的需求。

    kafka有七種控制消費組消費offset的策略,主要分為位移維度和時間維度,包括:

    • 位移維度。這是指根據位移值來重設。也就是說,直接把消費者的位移值重設成我們給定的位移值。包括Earliest/Latest/Current/Specified-Offset/Shift-By-N策略

    • 時間維度。我們可以給定一個時間,讓消費者把位移調整成大於該時間的最小位移;也可以給出一段時間間隔,比如 30 分鐘前,然後讓消費者直接將位移調回 30 分鐘之前的位移值。包括DateTime和Duration策略

    說完了重設策略,我們就來看一下具體應該如何實現,可以從兩個角度,API方式和命令行方式。

    重設位移的方法之API方式

    API方式只要記住用seek方法就可以了,包括seek,seekToBeginning 和 seekToEnd。

    void seek(TopicPartition partition, long offset);    
    void seek(TopicPartition partition, OffsetAndMetadata offsetAndMetadata);    
    void seekToBeginning(Collection<TopicPartition> partitions);    
    void seekToEnd(Collection<TopicPartition> partitions);    
    

    從方法簽名我們可以看出seekToBeginningseekToEnd是可以一次性重設n個分區的位移,而seek 只允許重設指定分區的位移,即為每個分區都單獨設置位移,因為不難得出,如果要自定義每個分區的位移值則用seek,如果希望kafka幫你批量重設所有分區位移,比如從最新數據消費或者從最早數據消費,那麼用seekToEnd和seekToBeginning。

    Earliest 策略:從最早的數據開始消費

    從主題當前最早位移處開始消費,這個最早位移不一定就是 0 ,因為很久遠的消息會被 Kafka 自動刪除,主要取決於你的刪除配置。

    代碼如下:

    Properties properties = PropertiesConfig.getConsumerProperties();
    properties.put("group.id", getGroupId());
    Consumer<String, String> consumer = new KafkaConsumer<>(properties);
    consumer.subscribe(getTopics());
    consumer.poll(0);
    consumer.seekToBeginning(
    consumer.partitionsFor(getTopics()).stream().map(partitionInfo ->
       new TopicPartition(getTopics(), partitionInfo.partition()))
       .collect(Collectors.toList()));
    

    首先是構造consumer對象,這樣我們可以通過partitionsFor獲取到分區的信息,然後我們就可以構造出TopicPartition集合,傳給seekToBegining方法。需要注意的一個地方是:需要用consumer.poll(0),而不能用consumer.poll(Duration.ofMillis(0))

    在poll(0)中consumer會一直阻塞直到它成功獲取了所需的元數據信息,之後它才會發起fetch請求去獲取數據。而poll(Duration)會把元數據獲取也計入整個超時時間。由於本例中使用的是0,即瞬時超時,因此consumer根本無法在這麼短的時間內連接上coordinator,所以只能趕在超時前返回一個空集合。

    Latest策略:從最新的數據開始消費

        consumer.seekToEnd(
            consumer.partitionsFor(getTopics().get(0)).stream().map(partitionInfo ->
                new TopicPartition(getTopics().get(0), partitionInfo.partition()))
                  .collect(Collectors.toList()));
    
    

    Current策略:從當前已經提交的offset處消費

    consumer.partitionsFor(getTopics().get(0)).stream().map(info ->
            new TopicPartition(getTopics().get(0), info.partition()))
            .forEach(tp -> {
                long committedOffset = consumer.committed(tp).offset();
                consumer.seek(tp, committedOffset);
            });
    

    **Special-offset策略:從指定的offset處消費 **

    該策略使用的方法和current策略一樣,區別在於,current策略是直接從kafka元信息中讀取中已經提交的offset值,而special策略需要用戶自己為每一個分區指定offset值,我們一般是把offset記錄到數據庫中然後可以從數據庫去讀取這個值

        consumer.partitionsFor(getTopics().get(0)).stream().map(info ->
                    new TopicPartition(getTopics().get(0), info.partition()))
                    .forEach(tp -> {
                        try {
                            consumer.seek(tp, JdbcUtils.queryOffset().get(tp.partition()));
                        } catch (SQLException e) {
                            e.printStackTrace();
                        }
                    });
    
    

    以上演示了用API方式重設位移,演示了四種常見策略的代碼,另外三種沒有演示,一方面是大同小異,另一方面在實際生產中,用API的方式不太可能去做時間維度的重設,而基本都是用命令行方式。

    重設位移的方法之命令行方式

    命令行方式重設位移是通過 kafka-consumer-groups 腳本。比起 API 的方式,用命令行重設位移要簡單得多。

    Earliest 策略指定–to-earliest。

    bin/kafka-consumer-groups.sh --bootstrap-server kafka-host:port --group test-group --reset-offsets --all-topics --to-earliest –execute
    

    Latest 策略指定–to-latest。

    bin/kafka-consumer-groups.sh --bootstrap-server kafka-host:port --group test-group --reset-offsets --all-topics --to-latest --execute
    

    Current 策略指定–to-current。

    bin/kafka-consumer-groups.sh --bootstrap-server kafka-host:port --group test-group --reset-offsets --all-topics --to-current --execute
    

    Specified-Offset 策略指定–to-offset。

    bin/kafka-consumer-groups.sh --bootstrap-server kafka-host:port --group test-group --reset-offsets --all-topics --to-offset <offset> --execute
    

    Shift-By-N 策略指定–shift-by N。

    bin/kafka-consumer-groups.sh --bootstrap-server kafka-host:port --group test-group --reset-offsets --shift-by <offset_N> --execute
    

    DateTime 策略指定–to-datetime。

    DateTime 允許你指定一個時間,然後將位移重置到該時間之後的最早位移處。常見的使用場景是,你想重新消費昨天的數據,那麼你可以使用該策略重設位移到昨天 0 點。

    bin/kafka-consumer-groups.sh --bootstrap-server kafka-host:port --group test-group --reset-offsets --to-datetime 2019-06-20T20:00:00.000 --execute
    

    Duration 策略指定–by-duration。
    Duration 策略則是指給定相對的時間間隔,然後將位移調整到距離當前給定時間間隔的位移處,具體格式是 PnDTnHnMnS。如果你熟悉 Java 8 引入的 Duration 類的話,你應該不會對這個格式感到陌生。它就是一個符合 ISO-8601 規範的 Duration 格式,以字母 P 開頭,後面由 4 部分組成,即 D、H、M 和 S,分別表示天、小時、分鐘和秒。舉個例子,如果你想將位移調回到 15 分鐘前,那麼你就可以指定 PT0H15M0S

    bin/kafka-consumer-groups.sh --bootstrap-server kafka-host:port --group test-group --reset-offsets --by-duration PT0H30M0S --execute
    

    提交的位移都去哪了?

    通過上面那幾部分的內容,我們已經搞懂了位移提交的方方面面,那麼提交的位移它保存在哪裡呢?這就要去位移主題的的世界里一探究竟了。kafka把位移保存在一個叫做__consumer_offsets的內部主題中,叫做位移主題。

    注意:老版本的kafka其實是把位移保存在zookeeper中的,但是zookeeper並不適合這種高頻寫的場景。所以新版本已經是改進了這個方案,直接保存到kafka。畢竟kafka本身就適合高頻寫的場景,並且kafka也可以保證高可用性和高持久性。

    既然它也是主題,那麼離不開分區和副本這兩個機制。我們並沒有手動創建這個主題並且指定,所以是kafka自動創建的, 分區的數量取決於Broker 端參數 offsets.topic.num.partitions,默認是50個分區,而副本參數取決於offsets.topic.replication.factor,默認是3。

    既然也是主題,肯定會有消息,那麼消息格式是什麼呢?參考前面我們手動設計將位移寫入數據庫的方案,我們保存了topic,group_id,partition,offset四個字段。topic,group_id,partition無疑是數據表中的聯合主鍵,而offset是不斷更新的。無疑kafka的位移主題消息也是類似這種設計。key也是那三個字段,而消息體其實很複雜,你可以先簡單理解為就是offset。

    既然也是主題,肯定也會有刪除策略,否則消息會無限膨脹。但是位移主題的刪除策略和其他主題刪除策略又不太一樣。我們知道普通主題的刪除是可以通過配置刪除時間或者大小的。而位移主題的刪除,叫做 Compaction。Kafka 使用Compact 策略來刪除位移主題中的過期消息,對於同一個 Key 的兩條消息 M1 和 M2,如果 M1 的發送時間早於 M2,那麼 M1 就是過期消息。Compact 的過程就是掃描日誌的所有消息,剔除那些過期的消息,然後把剩下的消息整理在一起。

    Kafka 提供了專門的後台線程定期地巡檢待 Compact 的主題,看看是否存在滿足條件的可刪除數據。這個後台線程叫 Log Cleaner。很多實際生產環境中都出現過位移主題無限膨脹佔用過多磁盤空間的問題,如果你的環境中也有這個問題,我建議你去檢查一下 Log Cleaner 線程的狀態,通常都是這個線程掛掉了導致的。

    總結

    kafka的位移是個極其重要的概念,控制着消費進度,也即控制着消費的準確性,完整性,為了保證消息不重複和不丟失。我們最好做到以下幾點:

    • 手動提交位移。

    • 手動提交有異步提交和同步提交兩種方式,既然兩者有利也有弊,那麼我們可以結合起來使用。

    • 細粒度的控制消費位移的提交,這樣可以避免重複消費的問題。

    • 保守的將消費位移再記錄到了數據庫中,重新啟動消費端程序的時候從數據庫讀取位移。

    獲取Kafka全套原創學習資料及思維導圖,關注【胖滾豬學編程】公眾號,回復”kafka”。

    本文來源於公眾號:【胖滾豬學編程】。一枚集顏值與才華於一身,不算聰明卻足夠努力的女程序媛。用漫畫形式讓編程so easy and interesting!求關注!

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

    網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

    ※Google地圖已可更新顯示潭子電動車充電站設置地點!!

    ※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

    ※別再煩惱如何寫文案,掌握八大原則!

    網頁設計最專業,超強功能平台可客製化

  • 2019 太陽能五大趨勢:市場走向穩定與分散,度電成本將成為供應鏈價格依歸

    2019 太陽能五大趨勢:市場走向穩定與分散,度電成本將成為供應鏈價格依歸

    2018 年可說是太陽能產業近年來波動最大的一年,歷經美國 201、301 條款,中國 531 新政,印度防衛性關稅,歐盟 MIP 結束等變動,從最上游的供應鏈到最下游的系統端都呈現極不穩定的狀態。由 EnergyTrend 所盤整的 2019 年五大趨勢來看,市況將會好轉,且產業也將在持續的變動中逐漸成熟。

    趨勢一:2018 年低谷不低,2019 需求再創新高

    中國的「531 新政」雖對市場造成衝擊,但因海外市場的需求走強,加上中國市場所受衝擊輕於預期,使 2018 年出現「低谷不低」的現象,預期全年新增併網量可達到 103GW(實際出貨量約95GW),年增4.9%。

    展望 2019 年,在政策鼓勵與供應鏈價格持續下降的推波助瀾下,全球需求預計將繼續正成長,其中又以歐洲的成長幅度最大,最多可超過五成。2019 年預期新增併網量將來到 111.3GW,出現 7.7% 的成長,再次創下歷史新高。

    趨勢二:市場持續分散,2019 年 GW 級市場增至 15 

    全球市場規模自 2018 年起預計會持穩在 100-120GW 之間,各年度需求量變化幅度將低於 10%。而根據 EnergyTrend 的最新需求報告統計,GW 級市場從 2016  年的 6  個成長到 2019 年將有 15 個,可見市場持續分散化的趨勢。

    2016-2020 年 GW 級市場

    中國、美國將持續穩居全球前二大市場,印度則從 2017 年起成為第三大需求國,日本次之。東南亞、北非、中東、拉丁美洲等新興市場自 2018 年崛起,如中東地區 2018 年全年需求預計將較 2017 年增加近 100%,2019  年還將增加 50% 左右。全球市場規模自 2019 年起將趨於穩定,印度最有可能出現較大幅度的需求成長。

    2016-2023 年全球市場需求趨勢

    趨勢三:供應鏈上游更為集中,單晶將逆轉市佔

    雖然供應鏈整體在 2018 年陷於供過於求、低利潤的困境,但技術和成本優勢較強、全球布局較廣的一線大廠仍保有強勁的營運動能,既有的擴產計畫多能持續進行,使供應鏈廠家有持續集中化的現象。根據 EnergyTrend  的供給資料庫,中國前五大多晶矽廠的新產能預計在 2Q19 陸續開出,屆時前五大廠的產能將佔全球近 70%,且現金成本更具競爭力。在矽晶圓環節,則將呈現隆基與中環雙龍頭主宰市場的現象,單晶供應鏈也將因而變得更具主導性,有機會拉升全年單晶佔比來到 6 成,2017 年底展開的單多晶之戰逐漸落幕。

    趨勢四:雙面產品產能倍增,P-PERC 效率還有成長空間

    雙面電池技術已十分成熟,且可在幾乎不增加額外成本的前提下創造額外的發電收益,因此產能比例持續上升,預計 2019 年雙面電池的總產能將接近 40GW,且以雙面單晶 PERC 電池產能增加最多。另一方面,單晶 PERC 電池的量產效率仍有成長空間。據 EnergyTrend 調查,單晶 PERC 電池的平均量產效率在 2019 年上半年即可站上 22%,且還可導入更多技術,在 2019 年底效率可望上看 23%。而單晶 PERC 的強勢也壓縮了次世代 N 型技術的發展空間,2019 年 N 型產能預期僅會有小幅增加。

    雙面電池產能成長趨勢(Unit: GW)

    趨勢五:均化度電成本成為模組價格降價指標

    供應鏈價格持續下探,使太陽能逐步朝擺脫補貼、平價上網的方向邁進;而無補貼系統的普及程度及其實際的均化度電成本(LCOE)將成為未來供應鏈的價格指標。

    太陽能產業在 2018 年面臨強大考驗,但同時也進入產業盤整階段,預期長期發展將趨於穩定化與健康化,供應鏈的價格將以整體系統的度電成本為依歸。儲能系統與智慧電網技術的投入,將成為太陽能產業進一步市場化的關鍵。

    本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

    網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

    ※Google地圖已可更新顯示潭子電動車充電站設置地點!!

    ※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

    ※別再煩惱如何寫文案,掌握八大原則!

    網頁設計最專業,超強功能平台可客製化

  • 川普視察野火災區 不認與氣候變遷有關

    摘錄自2018年11月18日TVBS新聞網美國加州報導

    美國加州爆發史上最嚴重的坎普野火,不但燒毀整座城鎮,還一路延燒了6萬公頃的面積,將近1300人失蹤,71人死亡。總統川普先前曾批評會發生野火,是因為森林管理不當,17日他也搭機來到加州視察,承諾會盡快解決野火災情,但他還是不願承認,這些跟氣候變遷有關。

    美國總統川普17日抵達加州,在當地首長陪同下,來到了幾乎已成焦土的天堂鎮視察。加州爆發史上最嚴重的坎普野火,延燒面積已經來到6萬公頃。川普先前曾批評森林管理不當,造成大火,這回他親眼見到災後現場,還是不承認氣候變遷扮演關鍵角色。

    美國總統川普:「不,我對這點(氣候變遷)很有意見,我想要好的氣候,我們會迎接好氣候,也將有非常安全的森林,因為我們不能每年都經歷(林火)。」

    本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

    網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

    ※Google地圖已可更新顯示潭子電動車充電站設置地點!!

    ※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

    ※別再煩惱如何寫文案,掌握八大原則!

    網頁設計最專業,超強功能平台可客製化

  • 川普關稅威脅奏效,SK Innovation 擬赴美設電動車電池廠

    川普關稅威脅奏效,SK Innovation 擬赴美設電動車電池廠

    南韓能源業者 SK Innovation 週一宣布,為鞏固美國客戶的訂單,正在考慮赴美設置電動車電池廠。美國為全球最大電動車市場之一,電池需求可期。

    美國總統川普五月根據 232 條款,以國安之名就進口汽車與零件啟動調查,雖然還未決定是否開徵 25% 的懲罰性關稅,但 SK Innovation 顯然已未雨綢繆,提早思考因應對策與未來投資佈局。

    路透社報導,SK Innovation 發言人表示,設廠位置目前鎖定在美國南部地區,已有二至三個州列入考慮,但確切時間與其它設廠細節則還未敲定。

    SK Innovation 競爭對手 LG Chem 目前已在美國設有生產據點,通用是主要客戶。

    根據國際能源總署(International Energy Agency)五月底發佈報告顯示,2017 年全球電動車加油電混合動力車突破三百萬輛、年增 54%,中國為驅動成長主力,美國與北歐國家也快速成長中。(businessgreen.com)

    (本文內容由 授權使用。首圖來源:)

    本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

    網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

    ※Google地圖已可更新顯示潭子電動車充電站設置地點!!

    ※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

    ※別再煩惱如何寫文案,掌握八大原則!

    網頁設計最專業,超強功能平台可客製化