按照《機器學習實戰》的主線,結束有監督學習中關於分類的機器學習方法,進入迴歸部分。所謂迴歸就是數據進行曲線擬合,迴歸一般用來做預測,涵蓋線性迴歸(經典最小二乘法)、局部加權線性迴歸、嶺迴歸和逐步線性迴歸。先來看下線性迴歸,即經典最小二乘法,說到最小二乘法就不得說下線性代數,因爲一般說線性迴歸只通過計算一個公式就可以得到答案,如(公式一)所示:
(公式一)
其中X是表示樣本特徵組成的矩陣,Y表示對應的值,比如房價,股票走勢等,(公式一)是直接通過對(公式二)求導得到的,因爲(公式二)是凸函數,導數等於零的點就是最小點。
(公式二)
不過並不是所有的碼農能從(公式二)求導得到(公式一)的解,因此這裏給出另外一個直觀的解,直觀理解建立起來後,後續幾個迴歸就簡單類推咯。從初中的投影點說起,如(圖一)所示:
(圖一)
在(圖一)中直線a上離點b最近的點是點b在其上的投影,即垂直於a的交點p。p是b在a上的投影點。試想一下,如果我們把WX看成多維的a,即空間中的一個超面來代替二維空間中的直線,而y看成b,那現在要使得(公式二)最小是不是就是尋找(圖一)中的e,即垂直於WX的垂線,因爲只有垂直時e才最小。下面來看看如何通過尋找垂線並最終得到W。要尋找垂線,先從(圖二)中的夾角theta 說起吧,因爲當cos(theta)=0時,他們也就垂直了。下面來分析下直線或者向量之間的夾角,如(圖二)所示:
(圖二)
在(圖二)中,表示三角形的斜邊,那麼:角beta也可以得到同樣的計算公式,接着利用三角形和差公式得到(公式三):
(公式三)
(公式三)表示的是兩直線或者兩向量之間的夾角公式,很多同學都學過。再仔細看下,發現分子其實是向量a,b之間的內積(點積),因此公式三變爲簡潔的(公式四)的樣子:
(公式四)
接下來繼續分析(圖一)中的投影,爲了方便觀看,增加了一些提示如(圖三)所示:
(圖三)
在(圖三)中,假設向量b在向量a中的投影爲p(注意,這裏都上升爲向量空間,不再使用直線,因爲(公式四)是通用的)。投影p和a 在同一方向上(也可以反方向),因此我們可以用一個係數乘上a來表示p,比如(圖三)中的,有了投影向量p,那麼我們就可以表示向量e,因爲根據向量法則,,有因爲a和e垂直,因此,展開求得係數x,如(公式五)所示:
(公式五)
(公式五)是不是很像(公式一)?只不過(公式一)的分母寫成了另外的形式,不過別急,現在的係數只是一個標量數字,因爲a,b都是一個向量,我們要擴展一下,把a從向量擴展到子空間,因爲(公式一)中的X是樣本矩陣,矩陣有列空間和行空間,如(圖四)所示:
(圖四)
(圖四)中的A表示樣本矩陣X,假設它有兩個列a1和a2,我們要找一些線性組合係數來找一個和(圖三)一樣的接受b 投影的向量,而這個向量通過矩陣列和係數的線性組合表示。求解的這個係數的思路和上面完全一樣,就是尋找投影所在的向量和垂線e的垂直關係,得到係數,如(公式六)所示:
(公式六)
這下(公式六)和(公式一)完全一樣了,基於最小二乘法的線性迴歸也就推導完成了,而局部加權迴歸其實只是相當於對不同樣本之間的關係給出了一個權重,所以叫局部加權,如(公式七)所示:
(公式七)
而權重的計算可通過高斯核(高斯公式)來完成,核的作用就是做權重衰減,很多地方都要用到,表示樣本的重要程度,一般離目標進的重要程度大些,高斯核可以很好的描述這種關係。如(公式八)所示,其中K是個超參數,根據情況靈活設置:
(公式八)
(圖五)是當K分別爲1.0, 0.01,0.003時的局部加權線性迴歸的樣子,可以看出當K=1.0時,和線性迴歸沒區別:
(圖五)
而嶺迴歸的樣子如(公式九)所示:
(公式九)
嶺迴歸主要是解決的問題就是當XX’無法求逆時,比如當特徵很多,樣本很少,矩陣X不是滿秩矩陣,此時求逆會出錯,但是通過加上一個對角爲常量lambda的矩陣,就可以很巧妙的避免這個計算問題,因此會多一個參數lambda,lambda的最優選擇由交叉驗證(cross-validation)來決定,加上一個對角不爲0的矩陣很形象的在對角上擡高了,因此稱爲嶺。不同的lambda會使得係數縮減,如(圖六)所示:
(圖六)
說到係數縮減大家可能會覺得有奇怪,感覺有點類似於正則,但是這裏只是相當於在(公式六)中增大分母,進而縮小系數,另外還有一些係數縮減的方法,比如直接增加一些約束,如(公式十)和(公式十一)所示:
(公式十)
(公式十一)
當線性迴歸增加了(公式十)的約束變得和橋迴歸差不多,係數縮減了,而如果增加了(公式十一)的約束時就是稀疏迴歸咯,(我自己造的名詞,sorry),係數有一些0。
有了約束後,求解起來就不像上面那樣直接計算個矩陣運算就行了,回顧第五節說中支持向量機原理,需要使用二次規劃求解,不過仍然有一些像SMO算法一樣的簡化求解算法,比如前向逐步迴歸方法:
前向逐步迴歸的僞代碼如(圖七)所示,也不難,仔細閱讀代碼就可以理解:
(圖七)
下面直接給出上面四種迴歸的代碼:
- from numpy import *
- def loadDataSet(fileName): #general function to parse tab -delimited floats
- numFeat = len(open(fileName).readline().split('\t')) - 1 #get number of fields
- dataMat = []; labelMat = []
- fr = open(fileName)
- for line in fr.readlines():
- lineArr =[]
- curLine = line.strip().split('\t')
- for i in range(numFeat):
- lineArr.append(float(curLine[i]))
- dataMat.append(lineArr)
- labelMat.append(float(curLine[-1]))
- return dataMat,labelMat
- def standRegres(xArr,yArr):
- xMat = mat(xArr); yMat = mat(yArr).T
- xTx = xMat.T*xMat
- if linalg.det(xTx) == 0.0:
- print "This matrix is singular, cannot do inverse"
- return
- ws = xTx.I * (xMat.T*yMat)
- return ws
- def lwlr(testPoint,xArr,yArr,k=1.0):
- xMat = mat(xArr); yMat = mat(yArr).T
- m = shape(xMat)[0]
- weights = mat(eye((m)))
- for j in range(m): #next 2 lines create weights matrix
- diffMat = testPoint - xMat[j,:] #
- weights[j,j] = exp(diffMat*diffMat.T/(-2.0*k**2))
- xTx = xMat.T * (weights * xMat)
- if linalg.det(xTx) == 0.0:
- print "This matrix is singular, cannot do inverse"
- return
- ws = xTx.I * (xMat.T * (weights * yMat))
- return testPoint * ws
- def lwlrTest(testArr,xArr,yArr,k=1.0): #loops over all the data points and applies lwlr to each one
- m = shape(testArr)[0]
- yHat = zeros(m)
- for i in range(m):
- yHat[i] = lwlr(testArr[i],xArr,yArr,k)
- return yHat
- def lwlrTestPlot(xArr,yArr,k=1.0): #same thing as lwlrTest except it sorts X first
- yHat = zeros(shape(yArr)) #easier for plotting
- xCopy = mat(xArr)
- xCopy.sort(0)
- for i in range(shape(xArr)[0]):
- yHat[i] = lwlr(xCopy[i],xArr,yArr,k)
- return yHat,xCopy
- def rssError(yArr,yHatArr): #yArr and yHatArr both need to be arrays
- return ((yArr-yHatArr)**2).sum()
- def ridgeRegres(xMat,yMat,lam=0.2):
- xTx = xMat.T*xMat
- denom = xTx + eye(shape(xMat)[1])*lam
- if linalg.det(denom) == 0.0:
- print "This matrix is singular, cannot do inverse"
- return
- ws = denom.I * (xMat.T*yMat)
- return ws
- def ridgeTest(xArr,yArr):
- xMat = mat(xArr); yMat=mat(yArr).T
- yMean = mean(yMat,0)
- yMat = yMat - yMean #to eliminate X0 take mean off of Y
- #regularize X's
- xMeans = mean(xMat,0) #calc mean then subtract it off
- xVar = var(xMat,0) #calc variance of Xi then divide by it
- xMat = (xMat - xMeans)/xVar
- numTestPts = 30
- wMat = zeros((numTestPts,shape(xMat)[1]))
- for i in range(numTestPts):
- ws = ridgeRegres(xMat,yMat,exp(i-10))
- wMat[i,:]=ws.T
- return wMat
- def regularize(xMat):#regularize by columns
- inMat = xMat.copy()
- inMeans = mean(inMat,0) #calc mean then subtract it off
- inVar = var(inMat,0) #calc variance of Xi then divide by it
- inMat = (inMat - inMeans)/inVar
- return inMat
- def stageWise(xArr,yArr,eps=0.01,numIt=100):
- xMat = mat(xArr); yMat=mat(yArr).T
- yMean = mean(yMat,0)
- yMat = yMat - yMean #can also regularize ys but will get smaller coef
- xMat = regularize(xMat)
- m,n=shape(xMat)
- #returnMat = zeros((numIt,n)) #testing code remove
- ws = zeros((n,1)); wsTest = ws.copy(); wsMax = ws.copy()
- for i in range(numIt):
- print ws.T
- lowestError = inf;
- for j in range(n):
- for sign in [-1,1]:
- wsTest = ws.copy()
- wsTest[j] += eps*sign
- yTest = xMat*wsTest
- rssE = rssError(yMat.A,yTest.A)
- if rssE < lowestError:
- lowestError = rssE
- wsMax = wsTest
- ws = wsMax.copy()
- #returnMat[i,:]=ws.T
- #return returnMat
- #def scrapePage(inFile,outFile,yr,numPce,origPrc):
- # from BeautifulSoup import BeautifulSoup
- # fr = open(inFile); fw=open(outFile,'a') #a is append mode writing
- # soup = BeautifulSoup(fr.read())
- # i=1
- # currentRow = soup.findAll('table', r="%d" % i)
- # while(len(currentRow)!=0):
- # title = currentRow[0].findAll('a')[1].text
- # lwrTitle = title.lower()
- # if (lwrTitle.find('new') > -1) or (lwrTitle.find('nisb') > -1):
- # newFlag = 1.0
- # else:
- # newFlag = 0.0
- # soldUnicde = currentRow[0].findAll('td')[3].findAll('span')
- # if len(soldUnicde)==0:
- # print "item #%d did not sell" % i
- # else:
- # soldPrice = currentRow[0].findAll('td')[4]
- # priceStr = soldPrice.text
- # priceStr = priceStr.replace('$','') #strips out $
- # priceStr = priceStr.replace(',','') #strips out ,
- # if len(soldPrice)>1:
- # priceStr = priceStr.replace('Free shipping', '') #strips out Free Shipping
- # print "%s\t%d\t%s" % (priceStr,newFlag,title)
- # fw.write("%d\t%d\t%d\t%f\t%s\n" % (yr,numPce,newFlag,origPrc,priceStr))
- # i += 1
- # currentRow = soup.findAll('table', r="%d" % i)
- # fw.close()
- from time import sleep
- import json
- import urllib2
- def searchForSet(retX, retY, setNum, yr, numPce, origPrc):
- sleep(10)
- myAPIstr = 'AIzaSyD2cR2KFyx12hXu6PFU-wrWot3NXvko8vY'
- searchURL = 'https://www.googleapis.com/shopping/search/v1/public/products?key=%s&country=US&q=lego+%d&alt=json' % (myAPIstr, setNum)
- pg = urllib2.urlopen(searchURL)
- retDict = json.loads(pg.read())
- for i in range(len(retDict['items'])):
- try:
- currItem = retDict['items'][i]
- if currItem['product']['condition'] == 'new':
- newFlag = 1
- else: newFlag = 0
- listOfInv = currItem['product']['inventories']
- for item in listOfInv:
- sellingPrice = item['price']
- if sellingPrice > origPrc * 0.5:
- print "%d\t%d\t%d\t%f\t%f" % (yr,numPce,newFlag,origPrc, sellingPrice)
- retX.append([yr, numPce, newFlag, origPrc])
- retY.append(sellingPrice)
- except: print 'problem with item %d' % i
- def setDataCollect(retX, retY):
- searchForSet(retX, retY, 8288, 2006, 800, 49.99)
- searchForSet(retX, retY, 10030, 2002, 3096, 269.99)
- searchForSet(retX, retY, 10179, 2007, 5195, 499.99)
- searchForSet(retX, retY, 10181, 2007, 3428, 199.99)
- searchForSet(retX, retY, 10189, 2008, 5922, 299.99)
- searchForSet(retX, retY, 10196, 2009, 3263, 249.99)
- def crossValidation(xArr,yArr,numVal=10):
- m = len(yArr)
- indexList = range(m)
- errorMat = zeros((numVal,30))#create error mat 30columns numVal rows
- for i in range(numVal):
- trainX=[]; trainY=[]
- testX = []; testY = []
- random.shuffle(indexList)
- for j in range(m):#create training set based on first 90% of values in indexList
- if j < m*0.9:
- trainX.append(xArr[indexList[j]])
- trainY.append(yArr[indexList[j]])
- else:
- testX.append(xArr[indexList[j]])
- testY.append(yArr[indexList[j]])
- wMat = ridgeTest(trainX,trainY) #get 30 weight vectors from ridge
- for k in range(30):#loop over all of the ridge estimates
- matTestX = mat(testX); matTrainX=mat(trainX)
- meanTrain = mean(matTrainX,0)
- varTrain = var(matTrainX,0)
- matTestX = (matTestX-meanTrain)/varTrain #regularize test with training params
- yEst = matTestX * mat(wMat[k,:]).T + mean(trainY)#test ridge results and store
- errorMat[i,k]=rssError(yEst.T.A,array(testY))
- #print errorMat[i,k]
- meanErrors = mean(errorMat,0)#calc avg performance of the different ridge weight vectors
- minMean = float(min(meanErrors))
- bestWeights = wMat[nonzero(meanErrors==minMean)]
- #can unregularize to get model
- #when we regularized we wrote Xreg = (x-meanX)/var(x)
- #we can now write in terms of x not Xreg: x*w/var(x) - meanX/var(x) +meanY
- xMat = mat(xArr); yMat=mat(yArr).T
- meanX = mean(xMat,0); varX = var(xMat,0)
- unReg = bestWeights/varX
- print "the best model from Ridge Regression is:\n",unReg
- print "with constant term: ",-1*sum(multiply(meanX,unReg)) + mean(yMat)
以上各種迴歸方法沒有考慮實際數據的噪聲,如果噪聲很多,直接用上述的迴歸不是太好,因此需要加上正則,然後迭代更新權重
參考文獻:
[1] machine learning in action.Peter Harrington
[2]Linear Algebra and Its Applications_4ed.Gilbert_Strang