python機器學習實戰(三)

原文連接:www.cnblogs.com/fydeblog/p/7277205.htmlhtml

前言

這篇博客是關於機器學習中基於機率論的分類方法--樸素貝葉斯,內容包括樸素貝葉斯分類器,垃圾郵件的分類,解析RSS源數據以及用樸素貝葉斯來分析不一樣地區的態度.python

操做系統:ubuntu14.04 運行環境:anaconda-python2.7-jupyter notebook 參考書籍:機器學習實戰和源碼,機器學習(周志華) notebook writer ----方陽算法

注意事項:在這裏說一句,默認環境python2.7的notebook,用python3.6的會出問題,還有個人目錄可能跟大家的不同,大家本身跑的時候記得改目錄,我會把notebook和代碼以及數據集放到結尾的百度雲盤,方便大家下載!ubuntu

1. 基於貝葉斯決策理論的分類方法

樸素貝葉斯的特色:數組

  • 優 點: 在數據較少的狀況下仍然有效,能夠處理多類別問題。
  • 缺 點: 對於輸入數據的準備方式較爲敏感。
  • 適用數據類型:標稱型數據。

貝葉斯決策理論的核心思想:選擇具備最高几率的決策。(最小化每一個樣本的條件風險,則整體風險也就最小,就是選擇最高几率,減少風險)app

2. 條件機率

2.1 簡單回顧

條件機率在樸素貝葉斯里面是必不可少的一環,下面來簡單介紹介紹:less

假設如今有一個裝了7塊石頭的罐子,其中3塊是灰色的, 4塊是黑色的 。若是從罐子中隨機取出一塊石頭,那麼是灰色石頭的可能性是多少? 因爲取石頭有 7 種可能 ,其中 3種爲灰色 ,因此取出灰色石頭的機率爲 3/7 。那麼取到黑色石頭的機率又是多少呢?很顯然 ,是4/7 。dom

若是這7塊石頭放在兩個桶中,那麼上述機率應該如何計算? (設兩個桶分爲A,B,A桶裝了2個灰色和2個黑色的石頭,B桶裝了1個灰色和2個黑色的石頭)python2.7

要計算P(gray)或者P(black) ,事先得知道石頭所在桶的信息會不會改變結果?你有可能巳經想到計算從B桶中取到灰色石頭的機率的辦法,這就是所謂的條件機率.機器學習

來計算P(gray|bucketB),這個是條件機率,在已知是從B桶拿出石頭的條件下,拿到灰色石頭的機率。

計算公式:P(gray|bucketB) = P(gray and bucketB) / P(bucketB) (將二者同時發生的機率除之前提條件發生的機率)

咱們知道P(bucketB)就是3/7,B桶的石頭數/總石頭數, P(gray and bucketB) 是1/7,B桶中的灰色石頭數/總石頭數,因此P(gray|bucketB) = 1/3。

這裏說一下P(gray and bucketB) ,它等於P(bucketB|gray)乘以P(gray)的,先發生gray,而後在gray的基礎上發生bucketB,就是gray and bucketB。

因此這裏的公式還能夠變一下,P(gray|bucketB) = P(gray and bucketB) / P(bucketB) =P(bucketB|gray)* P(gray) / P(bucketB)

通常狀況下,寫成 p(c|x) = p(x|c)* p(c) / p(x) 這就是貝葉斯準則。

2.2 使用條件機率進行分類

貝葉斯決策論中真正比較的是條件機率p(c1|x,y)和p(c2|x,y),這些符號所表明的具體意義是,給定某個由x,y表示的數據點,想知道該數據點來自類別c1的機率是多少?數據點來自類別c2的機率又是多少?

若是 p(c1|x,y) > p(c2|x,y) ,屬於類別c1 若是 p(c2|x,y) > p(c1|x,y) ,屬於類別c2。

這些機率能夠有2.1的貝葉斯準則計算。

3. 使用樸素貝葉斯進行留言分類

樸素貝葉斯的通常過程
(1) 收集數據:可使用任何方法。本章使用RSS源。
(2) 準備數據:須要數值型或者布爾型數據。
(3) 分析數據:有大量特徵時,繪製特徵做用不大,此時使用直方圖效果更好。
(4) 訓練算法:計算不一樣的獨立特徵的條件機率。
(5) 測試算法:計算錯誤率。
(6) 使用算法:一個常見的樸素貝葉斯應用是文檔分類。能夠在任意的分類場景中使用樸素貝葉斯命類器,不必定非要是文本

樸素貝葉斯的兩個假設
(1) 特徵之間是統計獨立的,即一個特徵或者單詞出現的可能性與它和其餘單詞相鄰沒有關係。
(2) 每一個特徵同等重要。

以上兩個假設是有問題的,不夠嚴謹,但處理方便,實際效果卻很好。

3.1 準備數據:從文本中構建詞向量

詞表到向量的轉換函數以下:

def loadDataSet():
    postingList=[['my', 'dog', 'has', 'flea', 'problems', 'help', 'please'],
                 ['maybe', 'not', 'take', 'him', 'to', 'dog', 'park', 'stupid'],
                 ['my', 'dalmation', 'is', 'so', 'cute', 'I', 'love', 'him'],
                 ['stop', 'posting', 'stupid', 'worthless', 'garbage'],
                 ['mr', 'licks', 'ate', 'my', 'steak', 'how', 'to', 'stop', 'him'],
                 ['quit', 'buying', 'worthless', 'dog', 'food', 'stupid']]
    classVec = [0,1,0,1,0,1]    #1 is abusive, 0 not
    return postingList,classVec
                 
def createVocabList(dataSet):
    vocabSet = set([])  #create empty set
    for document in dataSet:
        vocabSet = vocabSet | set(document) #union of the two sets
    return list(vocabSet)

def setOfWords2Vec(vocabList, inputSet):
    returnVec = [0]*len(vocabList)
    for word in inputSet:
        if word in vocabList:
            returnVec[vocabList.index(word)] = 1
        else: print "the word: %s is not in my Vocabulary!" % word
    return returnVec
  • 第一個loadDataSet函數是返回詞條切分後的文檔集合postlist(選自斑點犬愛好者留言板)和類別標籤集合classvec(1表明侮辱,0則是正常言論)。
  • 第二個createVocabList函數會返回輸入數據集全部不重複詞彙的列表。
  • 第三個setOfWords2Vec函數的功能是遍歷輸入vocablist的全部單詞,若是當初出現了InputSet中的單詞,returnVec對應位數的值返回1,無則返回0。

簡單來說,第一個函數的做用是界定訓練類別,看以後的文檔是否含有類別中的詞彙,第二個函數的做用是將一篇文檔作成列表,方便後面進行標記。第三個函數則是將第二個函數生成的列表根據第一個類別詞彙進行標記,將單詞轉化成數字,方便後面計算條件機率。

測試一下吧(全部函數都放在bayes中)。

cd 桌面/machinelearninginaction/Ch04
/home/fangyang/桌面/machinelearninginaction/Ch04
import bayes
listOPosts,listClasses = bayes.loadDataSet()
myVocabList = bayes.createVocabList(listOPosts)
myVocabList

bayes.setOfWords2Vec(myVocabList,listOPosts[0])

圖片描述

bayes.setOfWords2Vec(myVocabList,listOPosts[3])

![2]()

3.2 訓練算法 :從詞向量計算機率

根據上面介紹的三個函數,咱們知道如何將一組單詞轉換爲一組數字,也知道一個詞是否出如今一篇文檔中。如今已知文檔的類別,讓咱們使用轉換獲得的數字來計算條件機率吧。

仍是根據上面的貝葉斯準則來計算條件機率,不過公式會有一點不同。

p(ci|w) = p(w|ci)* p(ci) / p(w) (這裏的ci表示所屬類別,這裏有兩種可能性1和0,w爲向量,由多個數值組成)

咱們根據上面的公式對每一個類進行計算,而後比較這兩個機率值的大小。計算過程以下:

首先能夠經過類別 i ( 侮辱性留言或非侮辱性留言)中文檔數除以總的文檔數來計算機率p(ci),接下來計算p(w|ci),因爲p(w|ci) = p(w0,w1,w2..wn|ci),又由於全部詞都相互獨立,因此p(w|ci) = p(w0|ci)p(w1|ci)p(w2|ci)...p(wn|ci)

因而函數的僞代碼相應以下:

計算每一個類別中的文檔數目
對每篇訓練文檔:
       對每一個類別:
              若是詞條出現文檔中―增長該詞條的計數值
              增長全部詞條的計數值
對每一個類別:
       對每一個詞條:
              將該詞條的數目除以總詞條數目獲得條件機率
返回每一個類別的條件機率

參考代碼以下:

def trainNB0(trainMatrix,trainCategory):
    numTrainDocs = len(trainMatrix)
    numWords = len(trainMatrix[0])
    pAbusive = sum(trainCategory)/float(numTrainDocs)
    p0Num = zeros(numWords); p1Num = zeros(numWords)      
    p0Denom = 0.0; p1Denom = 0.0                        
    for i in range(numTrainDocs):
        if trainCategory[i] == 1:
            p1Num += trainMatrix[i]
            p1Denom += sum(trainMatrix[i])
        else:
            p0Num += trainMatrix[i]
            p0Denom += sum(trainMatrix[i])
    p1Vect = p1Num/p1Denom         
    p0Vect = p0Num/p0Denom         
    return p0Vect,p1Vect,pAbusive

輸入的trainMatrix是文檔通過setOfWords2Vec函數轉換後的列表,trainCategory是每篇文檔構成類別標籤向量。輸出是返回每一個類別的機率,pAbusive等於類別和除以訓練的樣本數,這個就是說明一下文檔類別的機率分佈,沒有什麼其餘意思。

因爲要算每個詞語的機率,這裏用到裏numpy的array數組,能夠很方便的計算每一個詞語的機率,便是用p0Num和p1Num來統計不一樣類別樣本的詞語所出現的次數,最後對每一個元素除以該類別中的總詞數。

來測試一下吧。

from numpy import *
reload(bayes)
<module 'bayes' from 'bayes.py'>
listOPosts,listClasses = bayes.loadDataSet()
myVocabList = bayes.createVocabList(listOPosts)
trainMat = []

for postinDoc in listOPosts:
       trainMat.append(bayes.setOfWords2Vec(myVocabList,postinDoc))

p0V , p1V, pAb = bayes.trainNB0(trainMat,listClasses)
p0V

![3]()

p1V

![4]()

看一看在給定文檔類別條件下詞彙表中單詞的出現機率, 看看是否正確.
詞彙表中的第一個詞是cute , 其在類別 0中出現1次 ,而在類別1中從未出現。對應的條件機率分別爲 0.04166667 與 0.0,該計算是正確的。
咱們找找全部機率中的最大值,該值出如今p(1)數組第21個下標位置,大小爲 0.15789474.能夠查到該單詞是stupid,這意味着它最能表徵類別1的單詞。

3.3 測試算法:根據現實狀況修改分類器

利用貝葉斯分類器進行文檔文類時,要計算每一個元素的條件機率並相乘,若其中有一個機率值等於0,那麼最後的乘積也爲0,爲下降這種影響,能夠將全部詞的出現數初始化爲1 ,並將分母初始化爲2 。

相應的trainNB0()的第4行和第5行修改成:

p0Num = ones(numWords);  p1Num = ones(numWords)      #change to ones() 
p0Denom = 2.0; p1Denom = 2.0                                             #change to 2.0

另外一個問題是向下溢出,乘積p(w0|ci)p(w1|ci)p(w2|ci)...p(wn|ci)過小的緣故 解決的辦法是對乘積取對數

相應的trainNB0()的第13行和第14行修改成:

p1Vect = log(p1Num/p1Denom)        #change to log()
 p0Vect = log(p0Num/p0Denom)        #change to log()

將更改好的函數命名爲trainNB0_change.

如今已經準備好構建完整的分類器了。當使用numpy向量處理功能時 , 這一切變得十分簡單.

參考代碼以下:

def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1):
    p1 = sum(vec2Classify * p1Vec) + log(pClass1)    #element-wise mult
    p0 = sum(vec2Classify * p0Vec) + log(1.0 - pClass1)
    if p1 > p0:
        return 1
    else: 
        return 0
def testingNB():
    listOPosts,listClasses = loadDataSet()
    myVocabList = createVocabList(listOPosts)
    trainMat=[]
    for postinDoc in listOPosts:
        trainMat.append(setOfWords2Vec(myVocabList, postinDoc))
    p0V,p1V,pAb = trainNB0_change(array(trainMat),array(listClasses))
    testEntry = ['love', 'my', 'dalmation']
    thisDoc = array(setOfWords2Vec(myVocabList, testEntry))
    print testEntry,'classified as: ',classifyNB(thisDoc,p0V,p1V,pAb)
    testEntry = ['stupid', 'garbage']
    thisDoc = array(setOfWords2Vec(myVocabList, testEntry))
    print testEntry,'classified as: ',classifyNB(thisDoc,p0V,p1V,pAb)

第一個函數就是兩個類別的條件機率進行比較,輸出最終的類別信息。 第二個函數就是一個測試函數,函數前面部分跟上面同樣,後面引入兩個測試樣本,進行分類。

reload(bayes)
<module 'bayes' from 'bayes.pyc'>
bayes.testingNB()
['love', 'my', 'dalmation'] classified as: 0
['stupid', 'garbage'] classified as: 1

3.4 文檔詞袋模型

咱們將每一個詞的出現與否做爲一個特徵,這能夠被描述爲詞集模型,上面就是詞集模型。
若是一個詞在文檔中出現不止一次,這可能意味着包含該詞是否出如今文檔中所不能表達的某種信息,這種方法被稱爲詞袋模型。
詞集和詞袋的區別:在詞袋中,每一個單詞能夠出現屢次 ,而在詞集中,每一個詞只能出現一次。

爲適應詞袋模型 ,須要對函數setOfWords2Vec稍加修改,修改後的函數爲bagOfWords2Vec,代碼以下:

def bagOfWords2VecMN(vocabList, inputSet):
    returnVec = [0]*len(vocabList)
    for word in inputSet:
        if word in vocabList:
            returnVec[vocabList.index(word)] += 1
    return returnVec

這個返回的列表表現的是單詞出現的次數,還再也不是是否出現

4. 使用樸素貝葉斯過濾垃圾郵件

4.1 準備數據:切分文本

前面介紹的詞向量是直接給定的,下面來介紹如何從文本中構建本身的詞列表.

先從一個文本字符串介紹

mySent = ' This book is the best book on python or M.L. I have ever laid eyes upon.'
mySent.split()

![5]()

能夠看到, 切分的結果不錯, 可是標點符號也被當成了詞的一部分.

解決方法:可使用正則表示式來切分句子 ,其中分隔符是除單詞、數字外的任意字符串.

import re
regEx = re.compile('\\W*')
listOfTokens = regEx.split(mySent)
listOfTokens

![6]()

能夠看到裏面的標點沒有了,但剩下一些空字符,還要進行一步,去掉這些空字符。

[tok for tok in listOfTokens if len(tok) >0]

![7]()

空字符消掉了,咱們能夠看到,有的詞首字母是大寫的,這對句子查找頗有用,但咱們是構建詞袋模型,因此仍是但願格式統一,還要處理一下.

[tok.lower() for tok in listOfTokens if len(tok) >0]

![8]()

能夠看到大寫所有變成了小寫,若是是想從小寫變成大寫,只需將tok.lower()改爲top.upper()便可.

咱們構建一個testParse函數,來切分文本,代碼以下

def textParse(bigString):    #input is big string, #output is word list
    import re
    listOfTokens = re.split(r'\W*', bigString)
    return [tok.lower() for tok in listOfTokens if len(tok) > 2]

4.2 測試算法:使用樸素貝葉斯進行交叉驗證

參考代碼以下:

def spamTest():
    docList=[]; classList = []; fullText =[]
    for i in range(1,26):
        wordList = textParse(open('email/spam/%d.txt' % i).read())
        docList.append(wordList)
        fullText.extend(wordList)
        classList.append(1)
        wordList = textParse(open('email/ham/%d.txt' % i).read())
        docList.append(wordList)
        fullText.extend(wordList)
        classList.append(0)
    vocabList = createVocabList(docList)#create vocabulary
    trainingSet = range(50); testSet=[]           #create test set
    for i in range(10):
        randIndex = int(random.uniform(0,len(trainingSet)))
        testSet.append(trainingSet[randIndex])
        del(trainingSet[randIndex])  
    trainMat=[]; trainClasses = []
    for docIndex in trainingSet:#train the classifier (get probs) trainNB0
        trainMat.append(bagOfWords2VecMN(vocabList, docList[docIndex]))
        trainClasses.append(classList[docIndex])
    p0V,p1V,pSpam = trainNB0(array(trainMat),array(trainClasses))
    errorCount = 0
    for docIndex in testSet:        #classify the remaining items
        wordVector = bagOfWords2VecMN(vocabList, docList[docIndex])
        if classifyNB(array(wordVector),p0V,p1V,pSpam) != classList[docIndex]:
            errorCount += 1
            print "classification error",docList[docIndex]
    print 'the error rate is: ',float(errorCount)/len(testSet)
    #return vocabList,fullText
  • 第一個循環是對垃圾郵件和非垃圾郵件進行切分,而後生成詞列表和類標籤
  • 第二個循環是0到50個數中隨機生成10個序號
  • 第三個循環是將第二個循環獲得的序號映射到詞列表,獲得訓練集和相應的類別,而後進行訓練算法
  • 第四個循環是進行錯誤率計算,分類出的類別與實際類別相比較,累計錯誤的樣本數,最後除以總數,獲得錯誤率

bayes.spamTest()
the error rate is: 0.0
bayes.spamTest()

![9]()

每次運行得出的結果可能不太同樣,由於是隨機選的序號.

5. 使用樸素貝葉斯分類器從我的廣告中獲取區域傾向

在這個最後的例子當中,咱們將分別從美國的兩個城市中選取一些人,經過分析這些人發佈的徵婚廣告信息,來比較這兩個城市的人們在廣告用詞上是否不一樣。若是結論確實是不一樣,那麼他們各自經常使用的詞是哪些?從人們的用詞當中,咱們可否對不一樣城市的人所關心的內容有所瞭解?

下面將使用來自不一樣城市的廣告訓練一個分類器,而後觀察分類器的效果。咱們的目的並非使用該分類器進行分類,而是經過觀察單詞和條件機率值來發現與特定城市相關的內容。

5.1 收集數據:導入RSS源

接下來要作的第一件事是使用python下載文本,而利用RSS,這很容易獲得,而Universal Feed Parser 是python最經常使用的RSS程序庫。

因爲python默認不會安裝feedparser,因此須要本身手動安裝,這裏附上ubuntu下的安裝方法

  • 第一步:wget http://pypi.python.org/packag...
  • 第二步:tar zxf feedparser-5.1.3.tar.gz
  • 第三步:cd feedparser-5.1.3
  • 第四步:python setup.py install

具體能夠看到這個連接:blog.csdn.net/tinkle181129/article/details/45343267
相關文檔:http://code.google.com/p/feed...

import feedparser
ny = feedparser.parse('http://newyork.craigslist.org/stp/index.rss')
上面是打開了Craigslist上的RSS源,要訪問全部條目的列表,輸入如下代碼

ny['entries']

![10]()

len(ny['entries'])
Out:25

能夠構建一個相似spamTest的函數來對測試過程自動化

def calcMostFreq(vocabList,fullText):
    import operator
    freqDict = {}
    for token in vocabList:
        freqDict[token]=fullText.count(token)
    sortedFreq = sorted(freqDict.iteritems(), key=operator.itemgetter(1), reverse=True) 
    return sortedFreq[:30]       

def localWords(feed1,feed0):
    import feedparser
    docList=[]; classList = []; fullText =[]
    minLen = min(len(feed1['entries']),len(feed0['entries']))
    for i in range(minLen):
        wordList = textParse(feed1['entries'][i]['summary'])
        docList.append(wordList)
        fullText.extend(wordList)
        classList.append(1) #NY is class 1
        wordList = textParse(feed0['entries'][i]['summary'])
        docList.append(wordList)
        fullText.extend(wordList)
        classList.append(0)
    vocabList = createVocabList(docList)#create vocabulary
    top30Words = calcMostFreq(vocabList,fullText)   #remove top 30 words
    for pairW in top30Words:
        if pairW[0] in vocabList: vocabList.remove(pairW[0])
    trainingSet = range(2*minLen); testSet=[]           #create test set
    for i in range(20):
        randIndex = int(random.uniform(0,len(trainingSet)))
        testSet.append(trainingSet[randIndex])
        del(trainingSet[randIndex])  
    trainMat=[]; trainClasses = []
    for docIndex in trainingSet:#train the classifier (get probs) trainNB0
        trainMat.append(bagOfWords2VecMN(vocabList, docList[docIndex]))
        trainClasses.append(classList[docIndex])
    p0V,p1V,pSpam = trainNB0(array(trainMat),array(trainClasses))
    errorCount = 0
    for docIndex in testSet:        #classify the remaining items
        wordVector = bagOfWords2VecMN(vocabList, docList[docIndex])
        if classifyNB(array(wordVector),p0V,p1V,pSpam) != classList[docIndex]:
            errorCount += 1
    print 'the error rate is: ',float(errorCount)/len(testSet)
    return vocabList,p0V,p1V

localWords函數與以前介紹的spamTest函數相似,不一樣的是它是使用兩個RSS做爲參數。

上面還新增了一個輔助函數calcMostFreq,該函數遍歷詞彙表中的每一個詞並統計它在文本中出現的次數,而後根據出現次數從高到低對詞典進行排序 , 最後返回排序最高的30個單詞

下面來測試一下

cd 桌面/machinelearninginaction/Ch04
/home/fangyang/桌面/machinelearninginaction/Ch04
import bayes
import feedparser
ny = feedparser.parse('http://newyork.craigslist.org/stp/index.rss')
sf = feedparser.parse('http://sfbay.craigslist.org/stp/index.rss')
vocabList,pSF,pNY = bayes.localWords(ny,sf)
the error rate is :0.15
vocabList,pSF,pNY = bayes.localWords(ny,sf)
the error rate is :0.4

咱們會發現這裏的錯誤率要遠高於垃圾郵件中的錯誤率,這是由於這裏關注的是單詞機率而不是實際分類,能夠經過calcMostFreq函數改變移除單詞數,下降錯誤率,由於次數最多的前30個單詞涵蓋了全部用詞的30%,產生這種現象的緣由是語言中大部分都是冗餘和結構輔助性內容。

5.2 分析數據:顯示地域相關的用詞

將pSF和pNY進行排序,而後按照順序將詞打印出來,這裏用getTopWords函數表示這個功能

def getTopWords(ny,sf):
    import operator
    vocabList,p0V,p1V=localWords(ny,sf)
    topNY=[]; topSF=[]
    for i in range(len(p0V)):
        if p0V[i] > -6.0 : topSF.append((vocabList[i],p0V[i]))
        if p1V[i] > -6.0 : topNY.append((vocabList[i],p1V[i]))
    sortedSF = sorted(topSF, key=lambda pair: pair[1], reverse=True)
    print "SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**"
    for item in sortedSF:
        print item[0]
    sortedNY = sorted(topNY, key=lambda pair: pair[1], reverse=True)
    print "NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**"
    for item in sortedNY:
        print item[0]

輸入是兩個RSS源,而後訓練並測試樸素貝葉斯分類器,返回使用的機率值而後建立兩個列表用於元組的存儲。與以前返回排名最高的x個單詞不一樣,這裏能夠返回大於某個閾值的全部詞。這些元組會按照它們的條件機率進行排序。

bayes.getTopWords(ny,sf)

![11]()

值得注意的現象是,程序輸出了大量的停用詞。移除固定的停用詞(好比 there等等)看看結果會如何變化,依本書做者的經驗來看,這樣會使分類錯誤率下降。

小結

(1)對於分類而言,使用機率有時要比使用硬規則更爲有效
(2)貝葉斯機率及貝葉斯準則提供了一種利用已知值來估計未知機率的有效方法
(3)獨立性假設是指一個詞的出現機率並不依賴於文檔中的其餘詞,這個假設過於簡單。這就是之因此稱爲樸素貝葉斯的緣由。
(4)下溢出就是其中一個問題,它能夠經過對機率取對數來解決
(5)詞袋模型在解決文檔分類問題上比詞集模型有所提升
(6)移除停用詞,可下降錯誤率
(7)花大量時間對切分器進行優化

百度雲連接:https://pan.baidu.com/s/1LgKU...


相關文章和視頻推薦

圓方圓學院聚集 Python + AI 名師,打造精品的 Python + AI 技術課程。 在各大平臺都長期有優質免費公開課,歡迎報名收看。
公開課地址:https://ke.qq.com/course/362788                       

相關文章
相關標籤/搜索