LOWESS 經過取必定比例的局部數據,在這部分子集中擬合多項式迴歸曲線,這樣咱們即可以觀察到數據在局部展示出來的局部規律和局部趨勢(局部均值μ迴歸)。html
同時,由於 LOWESS 的統計窗口縮小爲局部窗口,所以擬合的迴歸曲線能夠包含週期性,波動性的信息。git
和傳統的多元線性迴歸最大的不一樣是,一般的迴歸分析每每是根據全體數據建模,這樣能夠描述總體趨勢,但現實生活中規律不老是(或者不多是)一條直線。github
LOWESS將局部範圍從左往右依次推動,最終一條連續的曲線就被計算出來了。顯然,曲線的光滑程度與咱們選取數據比例(局部窗口)有關:web
下面經過一個例子來形象說明"LOWESS"和"Linear regression"在局部信息捕獲上的主要區別算法
數據文件是關於「物種數目與海拔高度」(中科院植物所-賴江山博士),數據集能夠在這裏下載。api
用迴歸模型擬合的圖示以下,本文采用R語言:安全
# 從counts.txt文件直接將數據讀入R x = read.csv("/Users/zhenghan/Downloads/counts.txt") print(x) par(las = 1, mar = c(4, 4, 0.1, 0.1), mgp = c(2.5, 1, 0)) plot(x, pch = 20, col = rgb(0, 0, 0, 0.5)) abline(lm(counts ~ altitude, x), lwd = 2, col = "red")
若是僅僅從迴歸直線來看,彷佛是海拔越高,則物種數目越多。如此推斷下去,恐怕月球或火星上該物種最多。這顯然是一個不是那麼完美的線性迴歸推斷。服務器
改用lowess進行局部加權迴歸擬合,圖示以下:網絡
# 從counts.txt文件直接將數據讀入R x = read.csv("/Users/zhenghan/Downloads/counts.txt") print(x) par(las = 1, mar = c(4, 4, 0.1, 0.1)) plot(x, pch = 20, col = rgb(0, 0, 0, 0.5)) # 取不一樣的f參數值 for (i in seq(0.01, 1, length = 100)) { lines(lowess(x$altitude, x$counts, f = i), col = gray(i), lwd = 1.5) Sys.sleep(0.15) }
上圖中,曲線顏色越淺表示所取數據比例越大,即數據點越集中。不難看出白色的曲線幾乎已呈直線狀,而黑色的線則波動較大。架構
整體看來,圖中大體有四處海拔上的物種數目偏離迴歸直線較嚴重:450 米、550 米、650 米和 700 米附近。這種偏離迴歸的現象剛好說明了實際問題的機率分佈並非嚴格遵循線性迴歸模型,而是其餘的更復雜的非線性模型。
基於LOWESS的擬合結果,若研究者的問題是,多高海拔處的物種數最多?那麼答案應該是在 650 米附近。
爲了確保咱們用 LOWESS 方法獲得的趨勢是穩定的,咱們能夠進一步用 Bootstrap 的方法驗證。由於 Bootstrap 方法是對原樣本進行重抽樣,根據抽得的不一樣樣本能夠獲得不一樣的 LOWESS 曲線,最後咱們把全部的曲線添加到圖中,看所取樣本不一樣是否會使得 LOWESS 有顯著變化。
# 從counts.txt文件直接將數據讀入R x = read.csv("/Users/zhenghan/Downloads/counts.txt") print(x) set.seed(711) # 設定隨機數種子,保證本圖形能夠重製 par(las = 1, mar = c(4, 4, 0.1, 0.1), mgp = c(2.5, 1, 0)) plot(x, pch = 20, col = rgb(0, 0, 0, 0.5)) for (i in 1:400) { idx = sample(nrow(x), 300, TRUE) # 有放回抽取300個樣本序號 lines(lowess(x$altitude[idx], x$counts[idx]), col = rgb(0, 0, 0, 0.05), lwd = 1.5) # 用半透明顏色,避免線條重疊使得圖形看不清 Sys.sleep(0.05) }
能夠看出,雖然 700 米海拔附近物種數目減少的趨勢並不明顯了,這是由於這個海拔附近的觀測樣本量較少,在重抽樣的時候不容易被抽到,所以在圖中表明性不足,最後獲得的擬合曲線分佈稀疏。
可是通過 400 次重抽樣並計算 LOWESS 曲線,剛纔在第一幅圖中觀察到的趨勢大體都還存在(由於默認取數據比例爲 2/3,所以擬合曲線都比較光滑),因此總體迴歸擬合的結果是可信的。
因爲樣本數據不可避免的隨機波動問題(方差存在),咱們得到的樣本數據老是存在偏離均值的狀況,爲了更好地體現出數據中的統計規律性,咱們須要對數據進行平滑處理。
舉一個具體例子來講,在作數據平滑的時候,會有遇到有趨勢或者季節性的數據,對於這樣的數據,咱們不能使用簡單的均值正負3倍標準差之外作異常值剔除,須要考慮到趨勢性等條件。使用局部加權迴歸,能夠擬合一條趨勢線,將該線做爲基線,偏離基線距離較遠的則是真正的異常值點。
上一章節討論了線性迴歸在具體工程問題中面臨的2個主要挑戰,即:
面對以上問題,學術界的陸續發表了不少研究paper,很好地解決了這些問題,筆者在這裏儘可能列舉和梳理出整個理論體系的發展脈絡。
人們最先提出,用多元線性迴歸模型來擬合一元線性模型沒法擬合的複雜樣本數據。可是遇到了幾個比較大的問題:
另外一個解決問題的思路就是對數據進行局部最近鄰處理,也即KNN線性迴歸。
其中:
可是,KNN線性迴歸有幾個最大的缺點:
關於KNN線性迴歸的問題的其餘討論,能夠參閱這篇文章。
解決傳統KNN線性迴歸問題的一個思考方向是:避免直接求均值這種「hard regression」的方式,改成尋找一種「soft regression」的方式。
咱們在原始的KNN公式之上引入加權平滑:
權重因子函數K能夠由不少種形式,一個大的原則就是:偏離均值越遠的點,其貢獻度相對越低。
好比,Epanechnikov二次kernel:
,
其中,λ爲kernel的參數,稱之爲window width。對於KNN,只考慮最近的k個點影響;基於此,
,其中,x[k]爲距離x0第k近的點,即窗口邊界點。
經kernel加權平滑後,原始的KNN迴歸擬合的曲線爲連續的了,以下圖:
可是,這種kernel迴歸一樣存在着邊界(boundary)問題,以下圖:
上一小節咱們看到,傳統kernel加權平滑的KNN迴歸,對於x序列的開始與結束區段的點,其左右鄰域是不對稱的,致使了平滑後的值偏大或偏小。
所以,須要對權值作再修正,修正的方法就是再次基於窗口內的數據點極大似然估計,使迴歸值x0進一步向均值線μ靠攏。
假定對統計窗口的迴歸量x0的估計值爲:
定義目標函數:
令:
,
那麼,目標函數可改寫爲:
求偏導,可獲得:
利用矩陣運算,可直接得極大似然估計值爲:
其中,
上述迴歸方法稱之爲LOWESS (Local Weighted regression)。
咱們沿着LOWESS的思路脈絡繼續討論,截止目前爲止,LOWESS彷佛圓滿地完成了全部任務,可是還有一個問題,就是樣本數據中的異常點(outlier)問題。
咱們知道,樣本數據中的方差波動來自兩個方面:
樣本數據集的整體分佈由以上兩種方差波動共同做用產生,LOWESS經過kernel加權和極大似然迴歸的方式已經起到了較好的均值迴歸的做用,可是在極端的離羣點(outlier)狀況下,LOWESS的迴歸點x0依然可能偏離真值均值點μ過遠。所以,人們又提出了Robust LOWESS方法。
Robust LOWESS是Cleveland在LOWESS基礎上提出來的robust迴歸方法,能避免outlier對迴歸的影響。
在計算完估計值後,繼續計算殘差:
根據殘差計算robustnest weight:
其中,s爲殘差絕對值序列的中位值(median),B函數爲bisquare函數:
而後,用robustness weight乘以kernel weight做爲的新weight。如此,便剔除了殘差較大的異常點對於迴歸的影響,這本質上是數理統計中隨機變量標準化的一種思想應用。
爲了驗證LOWESS的實際性能,咱們來構造一組數據集驗證其效果,該數據集的特性知足:
lowess.py庫函數
""" lowess: Locally linear regression ================================== Implementation of the LOWESS algorithm in n dimensions. References ========= [HTF] Hastie, Tibshirani and Friedman (2008). The Elements of Statistical Learning; Chapter 6 [Cleveland79] Cleveland (1979). Robust Locally Weighted Regression and Smoothing Scatterplots. J American Statistical Association, 74: 829-836. """ import numpy as np import numpy.linalg as la # Kernel functions: def epanechnikov(xx, **kwargs): """ The Epanechnikov kernel estimated for xx values at indices idx (zero elsewhere) Parameters ---------- xx: float array Values of the function on which the kernel is computed. Typically, these are Euclidean distances from some point x0 (see do_kernel) Notes ----- This is equation 6.4 in HTF chapter 6 """ l = kwargs.get('l', 1.0) ans = np.zeros(xx.shape) xx_norm = xx / l idx = np.where(xx_norm <= 1) ans[idx] = 0.75 * (1 - xx_norm[idx] ** 2) return ans def tri_cube(xx, **kwargs): """ The tri-cube kernel estimated for xx values at indices idx (zero elsewhere) Parameters ---------- xx: float array Values of the function on which the kernel is computed. Typically, these are Euclidean distances from some point x0 (see do_kernel) idx: tuple An indexing tuple pointing to the coordinates in xx for which the kernel value is estimated. Default: None (all points are used!) Notes ----- This is equation 6.6 in HTF chapter 6 """ ans = np.zeros(xx.shape) idx = np.where(xx <= 1) ans[idx] = (1 - np.abs(xx[idx]) ** 3) ** 3 return ans def bi_square(xx, **kwargs): """ The bi-square weight function calculated over values of xx Parameters ---------- xx: float array Notes ----- This is the first equation on page 831 of [Cleveland79]. """ ans = np.zeros(xx.shape) idx = np.where(xx < 1) ans[idx] = (1 - xx[idx] ** 2) ** 2 return ans def do_kernel(x0, x, l=1.0, kernel=epanechnikov): """ Calculate a kernel function on x in the neighborhood of x0 Parameters ---------- x: float array All values of x x0: float The value of x around which we evaluate the kernel l: float or float array (with shape = x.shape) Width parameter (metric window size) kernel: callable A kernel function. Any function with signature: `func(xx)` """ # xx is the norm of x-x0. Note that we broadcast on the second axis for the # nd case and then sum on the first to get the norm in each value of x: xx = np.sum(np.sqrt(np.power(x - x0[:, np.newaxis], 2)), 0) return kernel(xx, l=l) def lowess(x, y, x0, deg=1, kernel=epanechnikov, l=1, robust=False, ): """ Locally smoothed regression with the LOWESS algorithm. Parameters ---------- x: float n-d array Values of x for which f(x) is known (e.g. measured). The shape of this is (n, j), where n is the number the dimensions and j is the number of distinct coordinates sampled. y: float array The known values of f(x) at these points. This has shape (j,) x0: float or float array. Values of x for which we estimate the value of f(x). This is either a single scalar value (only possible for the 1d case, in which case f(x0) is estimated for just that one value of x), or an array of shape (n, k). deg: int The degree of smoothing functions. 0 is locally constant, 1 is locally linear, etc. Default: 1. kernel: callable A kernel function. {'epanechnikov', 'tri_cube', 'bi_square'} l: float or float array with shape = x.shape The metric window size for the kernel robust: bool Whether to apply the robustification procedure from [Cleveland79], page 831 Returns ------- The function estimated at x0. Notes ----- The solution to this problem is given by equation 6.8 in Hastie Tibshirani and Friedman (2008). The Elements of Statistical Learning (Chapter 6). Example ------- >>> import lowess as lo >>> import numpy as np # For the 1D case: >>> x = np.random.randn(100) >>> f = np.cos(x) + 0.2 * np.random.randn(100) >>> x0 = np.linspace(-1, 1, 10) >>> f_hat = lo.lowess(x, f, x0) >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots(1) >>> ax.scatter(x, f) >>> ax.plot(x0, f_hat, 'ro') >>> plt.show() # 2D case (and more...) >>> x = np.random.randn(2, 100) >>> f = -1 * np.sin(x[0]) + 0.5 * np.cos(x[1]) + 0.2*np.random.randn(100) >>> x0 = np.mgrid[-1:1:.1, -1:1:.1] >>> x0 = np.vstack([x0[0].ravel(), x0[1].ravel()]) >>> f_hat = lo.lowess(x, f, x0, kernel=lo.tri_cube) >>> from mpl_toolkits.mplot3d import Axes3D >>> fig = plt.figure() >>> ax = fig.add_subplot(111, projection='3d') >>> ax.scatter(x[0], x[1], f) >>> ax.scatter(x0[0], x0[1], f_hat, color='r') >>> plt.show() """ if robust: # We use the procedure described in Cleveland1979 # Start by calling this function with robust set to false and the x0 # input being equal to the x input: y_est = lowess(x, y, x, kernel=epanechnikov, l=l, robust=False) resid = y_est - y median_resid = np.nanmedian(np.abs(resid)) # Calculate the bi-cube function on the residuals for robustness # weights: robustness_weights = bi_square(resid / (6 * median_resid)) # For the case where x0 is provided as a scalar: if not np.iterable(x0): x0 = np.asarray([x0]) #print "x0: ", x0 ans = np.zeros(x0.shape[-1]) # We only need one design matrix for fitting: B = [np.ones(x.shape[-1])] for d in range(1, deg + 1): B.append(x ** deg) #print "B: ", B B = np.vstack(B).T for idx, this_x0 in enumerate(x0.T): # This is necessary in the 1d case (?): if not np.iterable(this_x0): this_x0 = np.asarray([this_x0]) # Different weighting kernel for each x0: W = np.diag(do_kernel(this_x0, x, l=l, kernel=kernel)) # XXX It should be possible to calculate W outside the loop, if x0 and # x are both sampled in some regular fashion (that is, if W is the same # matrix in each iteration). That should save time. print "W: ", W if robust: # We apply the robustness weights to the weighted least-squares # procedure: robustness_weights[np.isnan(robustness_weights)] = 0 W = np.dot(W, np.diag(robustness_weights)) # try: # Equation 6.8 in HTF: BtWB = np.dot(np.dot(B.T, W), B) BtW = np.dot(B.T, W) # Get the params: beta = np.dot(np.dot(la.pinv(BtWB), BtW), y.T) # We create a design matrix for this coordinat for back-predicting: B0 = [1] for d in range(1, deg + 1): B0 = np.hstack([B0, this_x0 ** deg]) B0 = np.vstack(B0).T # Estimate the answer based on the parameters: ans[idx] += np.dot(B0, beta) # If we are trying to sample far away from where the function is # defined, we will be trying to invert a singular matrix. In that case, # the regression should not work for you and you should get a nan: # except la.LinAlgError : # ans[idx] += np.nan return ans.T
test_lowess.py
# -*- coding: utf-8 -*- import numpy as np import numpy.testing as npt import lowess as lo import matplotlib.pyplot as plt def test_lowess(): """ Test 1-d local linear regression with lowess """ np.random.seed(1984) for kernel in [lo.epanechnikov, lo.tri_cube]: for robust in [True, False]: # constructing a batch of periodic and volatile data x = np.random.randn(100) f = np.sin(x) x0 = np.linspace(-1, 1, 10) # x0 means fixed length window width f_hat = lo.lowess(x, f, x0, kernel=kernel, l=1.0, robust=robust) #print f_hat f_real = np.sin(x0) plt.figure() plt.plot(x0, f_hat, label="lowess fitting curve", color='coral') plt.plot(x0, f_real, label="real sin curve", color='blue') plt.show() if __name__ == '__main__': np.random.seed(1984) kernel = lo.epanechnikov robust = True # constructing a batch of periodic and volatile data x = np.random.randn(100) f = np.sin(x) x0 = np.linspace(-1, 1, 10) # x0 means fixed length window width f_hat = lo.lowess(x, f, x0, kernel=kernel, l=1.0, robust=robust) # print f_hat f_real = np.sin(x0) plt.figure() plt.plot(x0, f_hat, label="lowess fitting curve", color='coral') plt.plot(x0, f_real, label="real sin curve", color='blue') plt.show()
能夠看到,lowess的擬合迴歸線(橙紅色)相對比較好地擬合了原始數據集的。
這個章節,筆者將嘗試把LOWESS和深度神經網絡進行類比,經過類比,咱們能夠更加深刻理解LOWESS的思想本質。
在這點上,深度神經網絡用多元神經元疊加的方式,並經過分配不一樣的權重比來獲得一個複雜非線性擬合;而LOWESS的解決思路是縮小統計窗口,在小範圍內的窗口內進行迴歸分析,這也是微積分思想的一種體現。
從模型能力上來講,深度神經網絡更適合擬合複雜非線性大網絡,而LOWESS更適合擬合週期性波動數據。
在解決這個問題上,LOWESS和深度神經網絡異曲同工,都採用了非線性函數進行權重重分配與調整。在深度神經網絡中,這被稱之爲激活函數,在LOWESS中則被稱之爲kernel加權函數。
實際上,若是咱們將「Epanechnikov二次kernel」和「sigmoid函數的導數」圖像分別打印出來,會發現其函數形態都相似於下圖:
因此它們發揮的做用也是相似的,即:對遠離均值μ迴歸線的離羣點,賦值更小的權重;對靠近均值μ的數據點,賦值更大的權重。
從機率論的角度來講,這種非線性函數的加入本質上也是一種先驗假設,先驗假設的加入,使得數據驅動的後驗估計更加準確,這也是貝葉斯估計思想的核心。
另外一方面來講,深度神經網絡和深度學習的發展,使得人們能夠用深度神經網絡構建出一個超複雜的多元非線性網絡,對目標數據進行擬合,並經過剪枝/dropout等方式緩解過擬合的發生。
Relevant Link:
https://cosx.org/2008/11/lowess-to-explore-bivariate-correlation-by-yihui/ https://cosx.org/tags/lowess https://github.com/arokem/lowess https://blog.csdn.net/longgb123/article/details/79520982 https://www.jianshu.com/p/0002f27e80f9?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation https://cloud.tencent.com/developer/article/1111722 https://www.cnblogs.com/en-heng/p/7382979.html
首先要注意的是,和KNN同樣,LOWESS是一種非參數(nonparametric)迴歸模型。模型訓練結束後並不能獲得一組肯定的模型參數(coefficient)。
究其本質緣由是由於KNN/LOWESS在訓練並無預先假定一種固定的模型函數公式,相反,KNN/LOWESS直接基於原始數據獲得一條多項式擬合曲線,因此不少時候,咱們也能夠稱其爲平滑算法。
非參數迴歸具有不少良好的特性,例如:
基於這個理論前提,非參數模型是沒法對未知輸入進行預測的,可是能夠對訓練集範圍內的x值進行預測。
咱們經過分析KNN的預測能力來解釋這句話,KNN模型本質上是一個非參數模型,但其又能夠進行預測,這並不矛盾,KNN model存儲的本質上是一個很是龐大的參數,也能夠說是將整個訓練集做爲參數存儲起來了。在預測時,KNN.predict要求輸入值x必須在訓練集範圍以內,而KNN所謂的最近鄰搜索其本質上和插值法在思想上是一致的。
回到LOWESS的預測任務上,LOWESS.predict要求輸入的x值必須在訓練數據範圍內。
Relevant Link:
http://webcache.googleusercontent.com/search?q=cache:EgbwJKBCol8J:landcareweb.com/questions/44622/shi-yong-ju-bu-jia-quan-hui-gui-loess-lowess-yu-ce-xin-shu-ju+&cd=2&hl=zh-CN&ct=clnk
傅立葉原理代表:任何連續測量的時序或信號,均可以表示爲不一樣頻率的正弦波信號的無限疊加。而根據該原理創立的傅立葉變換算法利用直接測量到的原始信號,以累加方式來計算該信號中不一樣正弦波信號的頻率、振幅和相位。
以下圖所示,即爲時域信號與不一樣頻率的正弦波信號的關係:
上圖中最右側展現的是時域中的一個信號,這是一個近似於矩形的波。而圖的正中間則是組成該信號的各個頻率的正弦波。
從圖中咱們能夠看出,即便角度幾乎爲直角的正弦波,其實也是由衆多的弧度圓滑的正弦波來組成的。
在時域圖像中,咱們看到的只有一個矩形波,但當咱們經過傅里葉變換將該矩形波轉換到頻域以後,咱們可以很清楚的看到許多脈衝,其中頻域圖中的橫軸爲頻率,縱軸爲振幅。所以能夠經過這個頻域圖像得知,時域中的矩形波是由這麼多頻率的正弦波疊加而成的。
時序信號本質上也是由不少衝擊信號疊加而成的,從時域上,咱們看到的是一條不規整的,上下起伏的曲線,可是經過STL分解,咱們能夠將其分解爲不一樣的信號組成部分。STL的本質就是信號的成分分解,這是STL的核心。
STL(Seasonal and Trend decomposition using Loess)是一種分解時間序列信號的方法,由 Cleveland et al. (1990) 提出。
咱們下面解釋STL算法主要解決的兩個問題,也是STL的主要應用場景。
時間序列分解算法主要被用來測定及提取時間序列信號中的趨勢性和季節性信號,其分解式爲:
,其中
因此本質上,STL是一個信號分解算法。
基於STL信號分解公式,咱們可使用機率論與數理統計裏的方法論對時序信號創建統計量,分別是趨勢性強度和季節性強度,它們都以隨機變量機率的形式定義。
這兩個度量方法很是實用,當咱們須要在不少時間序列中找到趨勢性或季節性最強的序列,就能夠基於這兩個統計量進行篩選。
對於趨勢性很強的數據,經季節調整後的數據應比殘差項的變更幅度更大,所以會相對較小。可是,對於沒有趨勢或是趨勢很弱的時間序列,兩個方差應大體相同。所以,咱們將趨勢強度定義爲:
這能夠給趨勢強度的衡量標準,其值在0-1之間。由於有些狀況下殘差項的方差甚至比季節變換後的序列還大,咱們令FT可取的最小值爲0。
季節性的強度定義以下
當季節強度FS接近0時表示該序列幾乎沒有季節性,當季節強度FS接近1時表示該序列的Var(Rt)遠小於var(St+Rt)。
STL的總體框架性結構以下
筆者會在這個章節先進行框架性和原理性的討論,不涉及具體的算法細節,某個部分也會進行簡化處理,目的是總總體上對STL創建一個感性的認識。
數據拆分規則是說咱們採用什麼模型來表徵時序數據的疊加方式。
原始數據 = 平均季節數據 + 趨勢數據 + 殘差
這種方式,隨着時間的推移季節數據不會有太大的變化,在週期的業務數據更適合這樣的拆分方式,咱們在工業場景中用的最多的也是這種分解方式。
加法方式也是傅里葉變換時域信號分解的信號疊加方式。
原始數據 = 季節數據 * 趨勢數據 * 殘差
這種分解方式,隨着時間的推移季節數據波動會很是明顯。
要從時序信號中提取出週期性信號,首要的事情是須要肯定時序週期,即多久循環一次的時間窗口長度。時序週期的肯定大概有兩個方法:
從時序數據中抽取趨勢信息,最合理的方法就是尋找均值迴歸線,均值迴歸線反映了時序數據總體的趨勢走向。
上圖是分解後造成了:趨勢數據 + 其餘數據。
下圖是去到其餘數據最終的趨勢數據。
在STL中,趨勢信息的提取是經過LOWESS算法實現的。
獲得了趨勢數據以後,接下來才能夠計算季節週期性數據。計算公式以下:
原始數據 - 趨勢數據 = 季節數據 + 殘差數據 = S
接下來經過週期均值化從S中提取季節週期性數據。
數據集S中對不一樣週期中,相同時間點數據的進行相加獲得數據集SP,而後除以原始數據的週期數c,獲得一個平均季節數據集H,而後複製c次數據集H,將獲得平均季節數據,以下圖:
殘差數據的獲取公式以下:
原始數據 - 趨勢數據 - 平均季節數據 = 殘差數據
最終分解爲不一樣的信號部分的結果以下:
首先,STL是一個迭代循環,整體思想有點相似EM算法的分佈優化過程,經過多輪迭代,逐步逼近「趨勢信號」和「週期信號」的機率分佈真值。
STL分爲內循環(inner loop)與外循環(outer loop),其中內循環主要作了趨勢擬合與週期份量的計算。外層循環主要用於調節robustness weight,即去除outlier離羣點。
下圖爲STL內循環的流程圖:
內循環從趨勢份量開始,初始化。
初始化完成後內循環。
將本輪的總信號量,減去上一輪結果的趨勢份量,
去除完趨勢份量以後,餘下的信號主要由兩部分組成,即」殘差信號+季節週期性信號「,接下來要作的事是提取出季節週期性信號。咱們分幾小步討論。
將每一個週期間相同位置的樣本點(週期平移後相同位置)組成一個子序列集合(subseries),容易知道這樣的子序列個數等於Np個(一個週期內的樣本數爲Np),咱們稱其爲cycle-subseries,記爲。
用對每一個子序列作局部加權迴歸,並向前向後各延展一個週期,其中n(s)爲LOESS平滑參數。
全部LOESS平滑結果組成一個」Temporary seasonal series(臨時週期季節性子序列集合)「,集合長度爲:N+2*Np,其中N爲時序信號總長度。
對上一個步驟的」temporary seasonal series」依次作長度爲n(p)、n(p)、3的滑動平均(moving average),其中n(p)爲一個週期的樣本數。
而後作迴歸,獲得結果序列
,至關於提取週期子序列的低通量,即週期子序列信號中的低通噪音信號,也能夠理解爲週期子序列中的趨勢信號。
提純後獲得季節週期性信號。
對於去除週期以後的序列作迴歸,其中n(t)是LOESS平滑參數,獲得下一輪的趨勢份量
。
以後須要判斷是否收斂,若是未收斂則繼續下一輪迭代,若是收斂則輸出本輪迭代的最終「趨勢份量結果」與「週期份量結果」。
外層循環主要用於調節LOESS算法的robustness weight。若是數據序列中有outlier,則餘項會較大。
具體細節能夠參閱文章前面對LOWESS的robustness weight的相關討論。
爲了使得算法具備足夠的robustness,因此設計了內循環與外循環。特別地,當內存循環數足夠大時,內循環結束時趨勢份量與週期份量已收斂;
同時,若時序數據中沒有明顯的outlier,能夠將外層循環數設爲0。
# -*- coding: utf-8 -*- import statsmodels.api as sm import matplotlib.pyplot as plt import pandas as pd from datetime import datetime from heapq import nlargest from re import match import pytz import numpy as np def datetimes_from_ts(column): return column.map( lambda datestring: datetime.fromtimestamp(int(datestring), tz=pytz.utc)) def date_format(column, format): return column.map(lambda datestring: datetime.strptime(datestring, format)) def format_timestamp(indf, index=0): if indf.dtypes[0].type is np.datetime64: return indf column = indf.iloc[:,index] if match("^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2} \\+\\d{4}$", column[0]): column = date_format(column, "%Y-%m-%d %H:%M:%S") elif match("^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}$", column[0]): column = date_format(column, "%Y-%m-%d %H:%M:%S") elif match("^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}$", column[0]): column = date_format(column, "%Y-%m-%d %H:%M") elif match("^\\d{2}/\\d{2}/\\d{2}$", column[0]): column = date_format(column, "%m/%d/%y") elif match("^\\d{2}/\\d{2}/\\d{4}$", column[0]): column = date_format(column, "%Y%m%d") elif match("^\\d{4}\\d{2}\\d{2}$", column[0]): column = date_format(column, "%Y/%m/%d/%H") elif match("^\\d{10}$", column[0]): column = datetimes_from_ts(column) indf.iloc[:,index] = column return indf def get_gran(tsdf, index=0): col = tsdf.iloc[:,index] n = len(col) largest, second_largest = nlargest(2, col) gran = int(round(np.timedelta64(largest - second_largest) / np.timedelta64(1, 's'))) if gran >= 86400: return "day" elif gran >= 3600: return "hr" elif gran >= 60: return "min" elif gran >= 1: return "sec" else: return "ms" if __name__ == '__main__': # https://github.com/numenta/NAB/blob/master/data/artificialWithAnomaly/art_daily_flatmiddle.csv dta = pd.read_csv('./art_daily_flatmiddle.csv', usecols=['timestamp', 'value']) dta = format_timestamp(dta) dta = dta.set_index('timestamp') dta['value'] = dta['value'].apply(pd.to_numeric, errors='ignore') dta.value.interpolate(inplace=True) print "dta.value: ", dta.value res = sm.tsa.seasonal_decompose(dta.value, freq=288) res.plot() plt.show()
STL時間序列分解算法不只本質上在學習時間序列中和在探索歷史隨之間變化中十分有用,它也能夠應用在預測中。
假設一個加法分解,分解後的時間序列能夠寫爲:
其中,St表明季節週期性信號,At表明趨勢性和殘差信號的混合。
對於時間序列的預測,咱們須要分別預測季節項St,和At。
一般狀況下咱們假設季節項不變,或者變化得很慢,所以它能夠經過簡單地使用最後一年的季節項的估計來預測,這就是所謂的樸素季節法。
另外一方面,可使用任意非季節性預測方法來預測份量At。例如,可使用帶漂移項的隨機遊走法,也可使用三次指數平滑法(Holt-Winters法),或者還能夠用非季節性的ARIMA模型。
咱們這裏以雙色球爲例,基於歷史雙色球開獎的記錄做爲時序數據,預測將來可能的雙色球開獎結果。
# -*- coding: utf-8 -*- import statsmodels.api as sm import matplotlib.pyplot as plt import pandas as pd import os import numpy as np import pickle import pandas from statsmodels.tsa.api import Holt def load_historydata(): # http://www.17500.cn/getData/ssq.TXT if not os.path.isfile("./ssq.pkl"): ori_data = np.loadtxt('./ssq.TXT', delimiter=' ', usecols=(0, 2, 3, 4, 5, 6, 7, 8), unpack=False) pickle.dump(ori_data, open("./ssq.pkl", "w")) df = pandas.DataFrame(data=ori_data) return df else: ori_data = pickle.load(open("./ssq.pkl", "r")) df = pandas.DataFrame(data=ori_data) return df if __name__ == '__main__': dta = load_historydata() #print dta dta = dta.set_index(0) # Interpolate values for i in [1, 2, 3, 4, 5, 6, 7]: dta[i].interpolate(inplace=True) print dta # show the stl decomposition result for i in [1, 2, 3, 4, 5, 6, 7]: res = sm.tsa.seasonal_decompose(dta[i], freq=7) res.plot() plt.show() # predict the future time series data y_pred = [] for j in range(1, 8): y_hat = [] for i in [1, 2, 3, 4, 5, 6, 7]: fit = Holt(np.asarray(dta[i])).fit(smoothing_level=0.3, smoothing_slope=0.1) y_hat.append(int(fit.forecast(j)[-1])) y_pred.append(y_hat) print "y_pred: ", y_pred
從STL分解結果殘差信號能夠看出,雙色球的開獎結果的具有很大的隨機性,並無呈現出明顯的季節週期性和趨勢性。
這裏預測了8天的將來開獎結果以下:
[ [5, 14, 18, 22, 26, 30, 8], [5, 14, 18, 22, 26, 30, 8], [5, 15, 18, 22, 26, 30, 8], [5, 15, 18, 23, 26, 30, 8], [5, 15, 18, 23, 26, 30, 7], [5, 16, 19, 23, 26, 30, 7], [5, 16, 19, 23, 26, 30, 7] ]
Relevant Link:
https://otexts.com/fppcn/forecasting-decomposition.html http://www.sunsoda.fun/TS%E6%97%B6%E9%97%B4%E5%88%86%E6%9E%90(3).html
從時序信號中提取出殘差數據後,咱們能夠基於此作時序異常檢測,時序數據若是有異常,都會體如今殘差數據集中。下面列舉幾種對殘差信號進行異常檢測的方法:
假設殘差數據知足正太分佈,或者近似正太分佈,能夠計算出殘差數據集的標準差。
若是數據點與均值的差值絕對值在3倍標準差外,則認爲是異常點,也就是3sigma異常檢驗,例以下面的殘差信號圖:
GESD,又名Grubb's Test檢驗,是一種多輪迭代發現多個異常點的異常發現算法。關於grubb's test的討論,能夠參閱另外一篇文章。
Relevant Link:
https://github.com/numenta/NAB https://github.com/andreas-h/pyloess http://siye1982.github.io/2018/03/02/anomaly-detection1/ https://www.cnblogs.com/runner-ljt/p/5245080.html https://www.cnblogs.com/en-heng/p/7390310.html https://zhuanlan.zhihu.com/p/34342025
這裏拉取了筆者在博客園放置的cnzz來訪者統計js插件的流量趨勢統計結果
能夠看到,個人博客園站點訪問呈現明顯的週期波動性,可是仔細觀察也會發現,在這個週期性之上,趨勢線中依然存在幾個「看起來不是那麼正常」的「離羣點」,這些離羣點就是由於筆者發表了一些新文章或者什麼其餘的緣由致使訪問流量發生非週期性的趨勢波動。
拋開具體的實際場景案例,抽象地來講,時間序列的異常檢測問題一般表示爲:在時變信息系統下,相對於某些標準信號或週期規律性信號的離羣點,
時序信號中有不少的異常類型,可是咱們只關注業務角度中最重要的類型,好比意外的峯值、降低、趨勢變化以及等級轉換。
咱們下面經過博客園的訪問趨勢來舉例說明典型的時序異常。
舉個例子來講,筆者發現每逢週末博客園訪問量都會降低,而到週一又會回到峯值,可是若是咱們發現某個週末訪問量忽然出現一個異常增長,看起來就像一個峯值。這些類型的異常一般稱爲附加異常。這種狀況極可能是由於筆者在週末發表了一篇新博客。
關於博客園的另外一個例子是,當某天博客園服務器宕機,咱們會發如今某個時間段內有零個或者很是少的用戶訪問,明顯和平時的趨勢不一樣,這些類型的異常一般被分類爲時間變化異常。
仍是以博客園爲例,咱們假設國家修改了法定假日的時間,從週六/日改到週一/二,那麼能夠想象,總體的訪問趨勢會平移2天,每週的低谷平移到週一/二。
這些類型的變化一般被稱爲水平位移或季節性水平位移異常。
整體來講,異常檢測算法應該將每一個時間點標記爲異常/非異常,或者預測某個點的信號,並衡量這個點的真實值與預測值的差值是否足夠大,從而將其視爲異常。
這個章節筆者嘗試從具體應用場景的角度出發,討論下怎麼基於STL分解算法作時序異常檢測。
包括STL在內,不少時序算法都有一個默認的假設前提,即:數據源自己是由週期性的衝擊信號源產生的,即數據自己具有季節週期性。
典型的正例子包括網站訪問pv量,通常來講是存在【天單位週期性】,由於通常來講,網站的pv訪問量天天是呈現相對固定的起伏波動的。
典型的反例包括每期的雙色球開獎結果,其數據自己並不存在明顯的週期性,因此STL沒法從其中提取出任何有機率顯著意義的週期性信號,天然也沒法基於殘差信號作異常檢測了。
從筆者本身的經驗來看,這個假設前提就極大限制了時序算法在安全領域裏的應用,從這個角度來分析,時序異常算法可能只適合在網站cc,服務器ddos,異常運維操做,kpi異常檢測等場景應用。
確認了問題場景的數據適合於時序算法的假設前提後,接下來的問題就是:數據的週期性窗口多大。
例如網站pv訪問量的週期能夠設置爲1天,外賣銷量時序時序數據的週期性能夠設置爲7天,某地區降雨量時序數據的週期性能夠設置爲1年,等等等等。
一個好的建議是基於你本身的領域經驗來設置這個週期窗口。
對於時序數據來講,趨勢信號和週期信號是須要過濾去除的部分,餘下的殘差信號纔是作異常檢測的數據源。
對殘差信號的異常檢測是以週期窗口爲單位的,在每個定長的週期窗口內,對時序信號的指標進行異常檢測(假設檢驗或無監督異常檢測)
能夠基於對歷史數據的擬合,對將來的時序指標進行預測,將預測的結果做爲一個基線。當將來的時序指標發生時,將其和基線進行對比,經過度量偏離程度,以此判斷是不是異常離羣點。
筆者這裏拉取了某臺網站服務器一段時間內的ssh登陸時序流水記錄,經過STL進行了分解,x軸爲分鐘時序,縱軸爲每分鐘內登陸次數。
週期窗口配置爲60min
若是咱們將週期窗口拉長,時序數據中的週期性強度會下降,這本質上和STL的算法迭代過程有關,越長的週期窗口,週期信號被滑動平均的稀疏就越嚴重。
週期窗口配置爲120mi
在STL的基礎上,筆者對殘差信號進行grubb's test假設檢驗異常檢測。可是實驗的結果並不理想。
這也反過來講明瞭ssh/rdp登陸日誌並不具有明顯的時序週期性。
更通常地說,時序異常檢測要求待檢測的數據源自己就具有「時間密集性」,即不發生異常時,數據源自己就是一個包含週期性信號的密集時序信號,例如網站平常pv訪問,4層數據包訪問。
而時序中的異常定義,是說:某個時間點突發的忽然性時序指標突變,或者總體時序週期性的水平平移異常(這種比較多),在業務場景中,佔大多數的是前者,即某時刻時序指標的突變異常。
拿服務器的ssh/rdp登陸日誌來講,首先它是一個非時間密集型的時序數據,由於平常除了管理員以外,不多會有登陸行爲,若是一旦有大量登陸行爲,極可能就是有攻擊者在對服務器進行爆破攻擊。若是直接強行對ssh/rdp登陸日誌進行stl分解,獲得的殘差信號極可能就是攻擊者的流量數據,基於這份殘差數據對假設檢驗獲得的異常時間點,自己也沒有可解釋性,極可能只是當時攻擊者增長了攻擊機器或者增長了網絡帶寬而已。
因此,ssh/rdp登陸時序檢測這個場景自己就不符合時序信號的基本假設,。
Relevant Link:
https://juejin.im/post/5c19f4cb518825678a7bad4c https://github.com/twitter/AnomalyDetection https://mp.weixin.qq.com/s/w7SbAHxZsmHqFtTG8ZAXNg