Keras文本分類實戰(下)

摘要: 本文是使用kreas處理文本分析的入門教程(下),介紹文本處理的兩種方法——獨熱編碼和詞嵌入。

在上一節Keras文本分類實戰(上),講述了關於NLP的基本知識。這部分,將學會以不一樣方式將單詞表示爲向量。html

詞嵌入(word embedding)是什麼

文本也被視爲一種序列化的數據形式,相似於天氣數據或財務數據中的時間序列數據。在以前的BOW模型中,瞭解瞭如何將整個單詞序列表示爲單個特徵向量。下面將看到如何將每一個單詞表示爲向量。這裏有多種方法能夠對文本進行向量化,好比:api

  • 每一個詞語(word)表示的詞語(words)做爲向量
  • 每一個字符(character)表示的字符(characters)做爲向量
  • N-gram單詞/字符表示爲向量

在本教程中,將使用單熱編碼和單詞嵌入將單詞表示爲向量,這是在神經網絡中處理文本的經常使用方法。數組

獨熱碼(one-hot encoding)

將單詞表示爲向量的第一種方式是建立獨熱碼,這是經過將詞彙長度的向量與語料庫中的每一個單詞的條目組合一塊兒來完成。
經過這種方式,對於每一個單詞,只要它在詞彙表中存在,就會將該單詞在相應的位置設置爲1,而向量中其它的位置設置爲0。但這種方式可能爲每一個單詞建立至關大的向量,且不會提供任何其餘信息,例如單詞之間的關係。
假設有一個城市列表,以下例所示:服務器

>>> cities = ['London', 'Berlin', 'Berlin', 'New York', 'London']
>>> cities
['London', 'Berlin', 'Berlin', 'New York', 'London']

可使用scikit-learn包中LabelEncoder對城市列表進行分類:網絡

>>> from sklearn.preprocessing import LabelEncoder

>>> encoder = LabelEncoder()
>>> city_labels = encoder.fit_transform(cities)
>>> city_labels
array([1, 0, 0, 2, 1])

以後就可使用scikit-learn包中OneHotEncoder對城市列表進行編碼:app

>>> from sklearn.preprocessing import OneHotEncoder

>>> encoder = OneHotEncoder(sparse=False)
>>> city_labels = city_labels.reshape((5, 1))
>>> encoder.fit_transform(city_labels)
array([[0., 1., 0.],
       [1., 0., 0.],
       [1., 0., 0.],
       [0., 0., 1.],
       [0., 1., 0.]])

使用這種表示,能夠看到分類整數值表示數組的位置,1表示出現,0表示不出現。這種編碼經常使用於分類之中,這些類別能夠是例如城市、部門或其餘類別。dom

詞嵌入|word embeddings

該方法將字表示爲密集字向量(也稱爲字嵌入),其訓練方式不像獨熱碼那樣,這意味着詞嵌入將更多的信息收集到更少的維度中。
嵌入詞並不像人類那樣理解文本,而是映射語料庫中使用的語言的統計結構,其目標是將語義意義映射到幾何空間,該幾何空間也被稱爲嵌入空間(embedding space)。這個研究領域的一個著名例子是可以映射King - Man + Woman = Queen
怎麼能得到這樣的詞嵌入呢?這裏有兩種方法,其中一種是在訓練神經網絡時訓練詞嵌入(word embeddings )層。另外一種方法是使用預訓練好的詞嵌入。
如今,須要將數據標記爲能夠由詞嵌入使用的格式。Keras爲文本預處理和序列預處理提供了幾種便捷方法,咱們可使用這些方法來處理文本。
首先,能夠從使用Tokenizer類開始,該類能夠將文本語料庫向量化爲整數列表。每一個整數映射到字典中的一個值,該字典對整個語料庫進行編碼,字典中的鍵是詞彙表自己。此外,能夠添加參數num_words,該參數負責設置詞彙表的大小。num_words保留最多見的單詞。對前面的例子準備測試和訓練數據:機器學習

>>> from keras.preprocessing.text import Tokenizer

>>> tokenizer = Tokenizer(num_words=5000)
>>> tokenizer.fit_on_texts(sentences_train)

>>> X_train = tokenizer.texts_to_sequences(sentences_train)
>>> X_test = tokenizer.texts_to_sequences(sentences_test)

>>> vocab_size = len(tokenizer.word_index) + 1  # Adding 1 because of reserved 0 index

>>> print(sentences_train[2])
>>> print(X_train[2])
Of all the dishes, the salmon was the best, but all were great.
[11, 43, 1, 171, 1, 283, 3, 1, 47, 26, 43, 24, 22]

索引是按文本中最經常使用的單詞排序,這裏索引0是保留的,並不會分配給任何單詞,0索引用於填。
未知單詞(不在詞彙表中的單詞)在Keras中用word_count + 1表示,由於它們也能夠保存一些信息。函數

>>> for word in ['the', 'all', 'happy', 'sad']:
...     print('{}: {}'.format(word, tokenizer.word_index[word]))
the: 1
all: 43
happy: 320
sad: 450
注意:密切注意這種技術與scikit-learn的 CountVectorizer產生的X_train兩者之間的區別。

使用CountVectorizer,每一個向量的長度相同(總語料庫的大小)。使用Tokenizer,每一個向量等於每一個文本的長度,其數值並不表示計數,而是對應於字典tokenizer.word_index中的單詞值。工具

爲了解決每一個文本序列在大多數狀況下具備不一樣長度的單詞的問題,可使用pad_sequence()簡單地用零填充單詞序列。此外,還須要添加maxlen參數來指定序列的長度。如下代碼展現如何使用Keras填充序列:

>>> from keras.preprocessing.sequence import pad_sequences

>>> maxlen = 100

>>> X_train = pad_sequences(X_train, padding='post', maxlen=maxlen)
>>> X_test = pad_sequences(X_test, padding='post', maxlen=maxlen)

>>> print(X_train[0, :])
[  1  10   3 282 739  25   8 208  30  64 459 230  13   1 124   5 231   8
  58   5  67   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0
   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0
   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0
   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0
   0   0   0   0   0   0   0   0   0   0]

第一個值表示從前面的示例中學到的詞彙表中的索引,能夠看到生成的特徵向量主要包含0值元素,這是由於句子比較短。

Keras嵌入層|Embedding Layer

此時,採用的數據仍然是硬編碼(hardcode),且沒有告訴Keras經過後續任務學習新的嵌入空間。這種狀況下,就可使用Keras 的嵌入層,它採用先前計算的整數並將它們映射到嵌入的密集向量,須要設定如下參數:

  • input_dim:詞彙量的大小
  • output_dim:密集向量的大小
  • input_length:序列的長度

使用該嵌入層有兩種方法,一種方法是獲取嵌入層的輸出並將其插入一個全鏈接層(dense layer)。爲此,必須在其中間添加一個flatten layer

from keras.models import Sequential
from keras import layers

embedding_dim = 50

model = Sequential()
model.add(layers.Embedding(input_dim=vocab_size, 
                           output_dim=embedding_dim, 
                           input_length=maxlen))
model.add(layers.Flatten())
model.add(layers.Dense(10, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))
model.compile(optimizer='adam',
              loss='binary_crossentropy',
              metrics=['accuracy'])
model.summary()

結果以下

_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
embedding_8 (Embedding)      (None, 100, 50)           87350     
_________________________________________________________________
flatten_3 (Flatten)          (None, 5000)              0         
_________________________________________________________________
dense_13 (Dense)             (None, 10)                50010     
_________________________________________________________________
dense_14 (Dense)             (None, 1)                 11        
=================================================================
Total params: 137,371
Trainable params: 137,371
Non-trainable params: 0
_________________________________________________________________

能夠看到,有87350個新參數須要訓練,嵌入層的這些權重初始化使用隨機權重初始化,並在訓練期間經過反向傳播進行調整,該模型將單詞按照句子的順序做爲輸入向量。可使用如下方法進行訓練:

history = model.fit(X_train, y_train,
                    epochs=20,
                    verbose=False,
                    validation_data=(X_test, y_test),
                    batch_size=10)
loss, accuracy = model.evaluate(X_train, y_train, verbose=False)
print("Training Accuracy: {:.4f}".format(accuracy))
loss, accuracy = model.evaluate(X_test, y_test, verbose=False)
print("Testing Accuracy:  {:.4f}".format(accuracy))
plot_history(history)

結果以下

Training Accuracy: 0.5100
Testing Accuracy:  0.4600

第一個模型的準確性和損失

從圖中能夠看到,這用來處理順序數據時一般是一種不太可靠的方法。當處理順序數據時,但願關注查看本地和順序信息的方法,而不是絕對的位置信息。
使用嵌入的另外一種方法是在嵌入後使用MaxPooling1D/AveragePooling1DGlobalMaxPooling1D/ GlobalAveragePooling1D層。在最大池化的狀況下,能夠爲每一個要素維度獲取池中全部要素的最大值。在平均池化的狀況下取得平均值。通常在神經網絡中,最大池化更經常使用,且效果要優於平均池化。
使用Keras能夠在順序模型中添加各種池化層:

from keras.models import Sequential
from keras import layers

embedding_dim = 50

model = Sequential()
model.add(layers.Embedding(input_dim=vocab_size, 
                           output_dim=embedding_dim, 
                           input_length=maxlen))
model.add(layers.GlobalMaxPool1D())
model.add(layers.Dense(10, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))
model.compile(optimizer='adam',
              loss='binary_crossentropy',
              metrics=['accuracy'])
model.summary()

結果以下

_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
embedding_9 (Embedding)      (None, 100, 50)           87350     
_________________________________________________________________
global_max_pooling1d_5 (Glob (None, 50)                0         
_________________________________________________________________
dense_15 (Dense)             (None, 10)                510       
_________________________________________________________________
dense_16 (Dense)             (None, 1)                 11        
=================================================================
Total params: 87,871
Trainable params: 87,871
Non-trainable params: 0
_________________________________________________________________

訓練步驟不變:

history   

format(accuracy))
loss, accuracy = model.evaluate(X_test, y_test, verbose=False)
print("Testing Accuracy:  {:.4f}".format(accuracy))
plot_history(history)

結果以下

Training Accuracy: 1.0000
Testing Accuracy:  0.8050

最大池模型的準確性和損失

能夠看到,模型有一些改進。接下來,將學習如何使用預訓練的詞嵌入,以及是否對咱們的模型有所幫助。

使用預訓練的詞嵌入

對於機器學習而言,遷移學習比較火熱。在NLP中,也可使用預先計算好的嵌入空間,且該嵌入空間可使用更大的語料庫。最流行的方法是由谷歌開發的Word2Vec和由斯坦福NLP組開發的Glove,其中Word2Vec是經過神經網絡來實現,而GloVe經過共生矩陣和使用矩陣分解來實現。在這兩種狀況下,都是進行降維處理。但比較而言,Word2Vec更準確,GloVe的計算速度更快。
下面將瞭解如何使用斯坦福NLP組的GloVe詞嵌入,從這裏下載6B大小的詞嵌入(822 MB),還能夠在GloVe主頁面上找到其餘的詞嵌入,另外預訓練好的Word2Vec的嵌入詞能夠在此下載。若是你想訓練本身的詞嵌入,也可使Python的gensim包有效地完成,更多實現內容能夠在此查看。
下面將使用一個示例展現如何加載嵌入矩陣。示例中的文件的每一行都以單詞開頭,後面跟着特定單詞的嵌入向量。該文件包含400000行,每行表明一個單詞,後跟其向量做爲浮點數流。例如,如下是第一行的前50個字符:

$ head -n 1 data/glove_word_embeddings/glove.6B.50d.txt | cut -c-50
    the 0.418 0.24968 -0.41242 0.1217 0.34527 -0.04445

因爲不須要涵蓋全部的單詞,只需關注詞彙中的單詞。因爲詞彙量有限,所以能夠在預訓練詞嵌入時略過絕大部分單詞:

import numpy as np

def create_embedding_matrix(filepath, word_index, embedding_dim):
    vocab_size = len(word_index) + 1  # Adding again 1 because of reserved 0 index
    embedding_matrix = np.zeros((vocab_size, embedding_dim))

    with open(filepath) as f:
        for line in f:
            word, *vector = 

    return embedding_matrix

可使用下面函數檢索嵌入矩陣:

>>> embedding_dim = 50
>>> embedding_matrix = create_embedding_matrix(
...     'data/glove_word_embeddings/glove.6B.50d.txt',
...     tokenizer.word_index, embedding_dim)

下面將在訓練中使用嵌入矩陣,當使用預訓練詞嵌入時,咱們能夠選擇在訓練期間對嵌入進行更新,或者只按照原樣使用這兩種方式。
首先,快速查看有多少嵌入向量是非零的:

>>> nonzero_elements = np.count_nonzero(np.count_nonzero(embedding_matrix, axis=1))
>>> nonzero_elements / vocab_size
0.9507727532913566

從上能夠看到,95.1%的詞彙被預先訓練的模型所覆蓋,這是一個很好的詞彙覆蓋範圍。下面看看使用全局池化層時的性能:

model = Sequential()
model.add(layers.Embedding(vocab_size, embedding_dim, 
                           weights=[ 

 'relu'))

結果以下:

_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
embedding_10 (Embedding)     (None, 100, 50)           87350     
_________________________________________________________________
global_max_pooling1d_6 (Glob (None, 50)                0         
_________________________________________________________________
dense_17 (Dense)             (None, 10)                510       
_________________________________________________________________
dense_18 (Dense)             (None, 1)                 11        
=================================================================
Total params: 87,871
Trainable params: 521
Non-trainable params: 87,350
_________________________________________________________________

結果以下:

Training Accuracy: 0.7500
Testing Accuracy:  0.6950

未經訓練的詞嵌入模型的準確性和損失

因爲詞嵌入沒有通過額外訓練,所以預計精度會下降。可是,若是經過使用trainable=True這種方式訓練詞嵌入,其執行效果又會是怎樣呢?

model = Sequential()
model.add(layers.Embedding(vocab_size, embedding_dim, 
                           weights=[embedding_matrix], 
                           input_length=maxlen, 
                           trainable=True))

 'relu'))
model.add(layers.Dense(1, activation='sigmoid'))
model.compile(optimizer='adam',
              loss='binary_crossentropy',
              metrics=['accuracy'])
model.summary()

結果以下

_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
embedding_11 (Embedding)     (None, 100, 50)           87350     
_________________________________________________________________
global_max_pooling1d_7 (Glob (None, 50)                0         
_________________________________________________________________
dense_19 (Dense)             (None, 10)                510       
_________________________________________________________________
dense_20 (Dense)             (None, 1)                 11        
=================================================================
Total params: 87,871
Trainable params: 87,871
Non-trainable params: 0
_________________________________________________________________
history = model.fit(X_train, y_train,
                    epochs=50,
                    verbose=False,
                    validation_data=(X_test, y_test),
                    batch_size=10)
loss, accuracy = model.evaluate(X_train, y_train, verbose=False)
print("Training Accuracy: {:.4f}".format(accuracy))
loss, accuracy = model.evaluate(X_test, y_test, verbose=False)
print("Testing Accuracy:  {:.4f}".format(accuracy))
plot_history(history)

結果以下

Training Accuracy: 1.0000
Testing Accuracy:  0.8250

預訓練詞嵌入模型的準確性和損失

從上能夠看到,使用預訓練詞嵌入是最有效的。在處理大型訓練集時,能夠加快訓練過程。
下面,是時候關注更先進的神經網絡模型,看看是否有可能提高模型及其性能優點。

卷積神經網絡(CNN)

卷積神經網絡或是近年來機器學習領域中最使人振奮的發展成果之一,尤爲是在計算機視覺領域裏表現優異。關於CNN詳細介紹能夠看這篇文章《一文入門卷積神經網絡:CNN通俗解析》,這裏只作簡單介紹。
在下圖中,能夠看到卷積是如何工做的。它首先是從一個具備過濾器內核大小的輸入特徵開始的,且一維卷積對於平移是不變的,這意味着能夠在不一樣位置識別某些序列,這對文本中的某些模式是頗有幫助:

一維卷積

下面演示如何在Keras中使用這個網絡,Keras提供了各類卷積層:

embedding_dim  

model = Sequential()
model.add(layers.Embedding(vocab_size, embedding_dim, input_length=maxlen))
model.add(layers.Conv1D(128, 5, activation='relu'))
model.add(layers.GlobalMaxPooling1D())
model.Dense(10,

結果以下

_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
embedding_13 (Embedding)     (None, 100, 100)          174700    
_________________________________________________________________
conv1d_2 (Conv1D)            (None, 96, 128)           64128     
_________________________________________________________________
global_max_pooling1d_9 (Glob (None, 128)               0         
_________________________________________________________________
dense_23 (Dense)             (None, 10)                1290      
_________________________________________________________________

Total params: 240,129
Trainable params: 240,129
Non-trainable params: 0
history = model.fit(X_train, y_train,
                    epochs=10,
                    verbose=False,
                    validation_data=(X_test, y_test),
                    batch_size=10)
loss, accuracy = model.evaluate(X_train, y_train, verbose=False)
print("Training Accuracy: format(accuracy))
loss, accuracy = model.evaluate(X_test, y_test, verbose=False)
print("Testing Accuracy:  {:.4f}".format(accuracy))
plot_history(history)

結果以下

Training Accuracy: 1.0000
Testing Accuracy:  0.7700

卷積神經網絡的準確度和損失

從上能夠看到,其準確率最高爲80%,表現並非很理想,形成這樣的緣由多是:

  • 沒有足夠的訓練樣本
  • 擁有的數據並不能很好地歸納現實
  • 缺少對調整超參數的關注

CNN網絡通常適合在大型訓練集上使用,在這些訓練集中,CNN可以找到像邏輯迴歸這樣的簡單模型沒法實現的歸納。

超參數優化|Hyperparameters Optimization

深度學習和使用神經網絡的一個關鍵步驟是超參數優化
正如在目前使用的模型中看到的那樣,即便是更簡單的模型,也有大量的參數可供調整和選擇,這些參數被稱爲超參數,也是機器學習中最爲耗時的部分,依賴於實驗和我的經驗。
Kaggle上的比賽經常使用的方法有:一種流行的超參數優化方法是網格搜索(grid search)。這個方法的做用是獲取參數列表,並使用它找到每一個參數組合運行模型。簡單粗暴,但計算量最大;另外一種常見的方法是隨機搜索(random search),只需採用隨機的參數組合。
爲了使用Keras應用隨機搜索,須要使用KerasClassifier做爲scikit-learn API的包裝器。使用這個包裝器,可使用scikit提供的各類工具——像交叉驗證同樣學習。須要的類是RandomizedSearchCV,使用交叉驗證明現隨機搜索。交叉驗證是一種驗證模型並獲取整個數據集並將其分紅多個測試和訓練數據集的方法。
經常使用的方法有k折交叉驗證(k-fold cross-validation)和嵌套交叉驗證( nested cross-validation ),這裏實現k折交叉驗證法。在該方法中,數據集被劃分爲k個相等大小的集合,其中一個集合用於測試,其他的分區用於訓練。這使得咱們能夠運行k個不一樣的運行仿真,其中每一個分區都曾被用做測試集。所以,k越高,模型評估越準確,而每一個測試集也越小。
第一步KerasClassifier建立一個建立Keras模型的函數:

def create_model(num_filters, kernel_size, vocab_size, embedding_dim, maxlen):
    model = Sequential()
    model.add(layers.Embedding(vocab_size, embedding_dim, input_length=maxlen))
    model.add(layers.Conv1D(num_filters, kernel_size, activation=))
    model.add(layers.GlobalMaxPooling1D())
    model.add(layers.Dense(10, activation='relu'))
    model.add(layers.Dense(1, activation='sigmoid'))
    model.compile(optimizer='adam',
                  loss='binary_crossentropy',
                  metrics'accuracy'])
    return model

接下來,定義在訓練中使用的參數。它由一個字典組成,每一個參數都在上一個函數中命名。網格上的空格數是3 * 3 * 1 * 1 * 1,其中每一個數字是給定參數的不一樣選擇的數量。
使用如下字典初始化參數網格:

param_grid = dict(num_filters=[32, 64, 128],
                  kernel_size=[3, 5, 7],
                  vocab_size=[5000], 
                  embedding_dim=[50],
                  maxlen=[100])

接下來運行隨機搜索:

from keras.wrappers.scikit_learn import KerasClassifier
from sklearn.model_selection import RandomizedSearchCV

# Main settings
epochs = 20
embedding_dim = 50
maxlen = 100
output_file = 'data/output.txt'

# Run grid search for each source (yelp, amazon, imdb)
for source, frame in df.groupby('source'):
    print('Running grid search for data set :', source)
    sentences = df['sentence'].values
    y = df['label'].values

    # Train-test split
    sentences_train, sentences_test, y_train, y_test = train_test_split(
        sentences, y, test_size=0.25, random_state=1000)

    # Tokenize words
    tokenizer = Tokenizer(num_words=5000)
    tokenizer.fit_on_texts(sentences_train)
    X_train = tokenizer.texts_to_sequences(sentences_train)
    X_test = tokenizer.texts_to_sequences(sentences_test)

    # Adding 1 because of reserved 0 index
    vocab_size = len(tokenizer.word_index) + 1

    # Pad sequences with zeros
    X_train = pad_sequences(X_train, padding='post', maxlen=maxlen)
    X_test = pad_sequences(X_test, padding='post', maxlen=maxlen)

    # Parameter grid for grid search
    param_grid = dict(num_filters=[32, 64, 128],
                      kernel_size=[3, 5, 7],
                      vocab_size=[vocab_size],
                      embedding_dim=[embedding_dim],
                      maxlen=[maxlen])
    model = KerasClassifier(build_fn=create_model,
                            epochs=, batch_size=10,
                            verbose=False)
    grid = RandomizedSearchCV(estimator=model, param_distributions=param_grid,
                              cv=4, verbose=1, n_iter=5)
    grid_result = grid.fit(X_train, y_train)

    # Evaluate testing set
    test_accuracy = grid.score(X_test, y_test)

    # Save and evaluate results
    prompt = input(f'finished {source}; write to file and proceed? [y/n]')
    if prompt.lower() not in {'y', 'true', 'yes'}:
        break
    with open(output_file, 'a') as f:
        s = ('Running {} data set\nBest Accuracy : '
             '{:.4f}\n{}Test Accuracy : {:.4f}\n\n')
        output_string = s.format(
            source,
            grid_result.best_score_,
            grid_result.best_params_,
            test_accuracy)
        print(output_string)
        f.write(output_string)

運行須要一段時間,最後結果輸出以下:

Running amazon data set
Best Accuracy : 0.8122
{'vocab_size': 4603, 'num_filters': 64, 'maxlen': 100, 'kernel_size': 5, 'embedding_dim': 50}
Test Accuracy : 0.8457

Running imdb data set
Best Accuracy : 0.8161
{'vocab_size': 4603, 'num_filters': 128, 'maxlen': 100, 'kernel_size': 5, 'embedding_dim': 50}
Test Accuracy : 0.8210

Running yelp data set
Best Accuracy : 0.8127
{'vocab_size': 4603, 'num_filters': 64, 'maxlen': 100, 'kernel_size': 7, 'embedding_dim': 50}
Test Accuracy : 0.8384

因爲某種緣由,測試精度高於訓練精度,這多是由於在交叉驗證期間得分存在很大差別。能夠看到,使用卷積神經網絡表現最佳。

結論

本文講述如何使用Keras進行文本分類,從一個使用邏輯迴歸的詞袋模型變成了愈來愈先進的卷積神經網絡方法。本文沒有涉及的另外一個重要主題是循環神經網絡RNN,更具體地說是LSTMGRU。這些是處理文本或時間序列等順序數據的強大且流行的工具。當了解上述內容後,就能夠將其用於各類文本分類中,例如:電子郵件中的垃圾郵件檢測、自動標記文本或使用預約義主題對新聞文章進行分類等,快動手嘗試吧。

雲服務器99元拼團購!拉新還可贏現金紅包!300萬等你瓜分!
立刻一鍵開團贏紅包: http://click.aliyun.com/m/100...



本文做者:【方向】

閱讀原文

本文爲雲棲社區原創內容,未經容許不得轉載。

相關文章
相關標籤/搜索