kaggle 泰坦尼克號生存預測——六種算法模型實現與比較

Hi,大家好,這是我第一篇博客。

作爲非專業程序小白,博客內容必然有不少錯誤之處,還望各位大神多多批評指正。

在開始正式內容想先介紹下自己和一些異想天開的想法。

我是一名研究生,研究的方向是蛋白質結構與功能方向。在研究過程中發現生物系統是如此複雜,猶如一張網,信息流動,彼此相連,於是對複雜系統產生了興趣,越發感覺生命的本質就是信息啊。後來看了一部電影叫做《創戰紀》,男主的父親便是創造了一個數字世界,在這個數字世界裏進化出了數字生命(我們漂亮可愛的女主),雖然只是科幻,還是讓我感到非常的震撼。正如《異次元駭客》裏所講的在科技發展到一定程度之後是不是也可以創造出一個數字世界呢,而我們的世界會不會也是另一個意義上的數字世界呢,是由更高層的世界的人(GOD)創造的呢?因爲我們可以清晰的發現這個世界是有層次的,又是自相似的。從最底層的物理學研究的弦,夸克,原子,力,場等世界基本元素到化學研究的各種分子,化學反應再到生物學研究的由各種生命小分子按特定信息構建的核酸,蛋白質,細胞,組織,器官,個體,最終是由個體構成的羣體,以人爲例,即社會及其衍生出來的經濟體系,文化體系,政治體系等。所有的層次都是在上一個層次的基礎上構建的,彼此相似但又都涌現出新的特性,這便是這個世界的複雜性。我心中堅信所有複雜的事物都是由簡單的事物構成的,也堅信這個世界必有其最底層的元素與規則(就如同計算機裏面的0和1以及布爾邏輯運算),經過無數次的迭代演化(時間真的是個很神奇的物理量),變成了如今這個複雜多變的世界。而現在的科學就是在各個層次上研究這些元素和規則,如果有一天能夠找到最底層的元素與規則,也許可以重構另一個數字的世界。這算是我一個遙遠的夢吧,估計是很難實現了,但如果有機會我還是會爲之奮鬥的。

哈哈,腦洞開完了,現在迴歸現實。爲了更靠近一點我的夢想,我打算從現在最火的機器學習學起,於是報名了一些課程,瞭解了一些基本的機器學習知識。不過奈何數學基礎太差,程序語言也沒有學過,所以學起來還是有些吃力。後來老師推薦了kaggle 競賽,裏面有很多項目,對熟悉數據處理與學習各種算法幫助很大。便參加了kaggle 上最爲經典的泰坦尼克號生存預測項目,期間自己探索了一些,也拜讀了一些大神的作品,感覺收穫蠻大。爲了不至於忘掉,在此做個總結,並希望與大家交流探討。


1.項目基本信息

項目的背景是大家都熟知的發生在1912年的泰坦尼克號沉船災難,這次災難導致2224名船員和乘客中有1502人遇難。而哪些人倖存那些人喪生並非完全隨機。比如說你碰巧搭乘了這艘遊輪,而你碰巧又是一名人見人愛,花見花開的一等艙小公主,那活下來的概率就很大了,但是如果不巧你只是一名三等艙的摳腳大漢,那只有自求多福了。也就是說在這生死攸關的情況下,生存與否與性別,年齡,階層等因素是有關係的,如果把這些因素作爲特徵,生存的結果作爲預測目標,就可以建立一個典型的二分類機器學習模型。在這個項目中提供了部分的乘客名單,包括各種維度的特徵以及是否倖存的標籤,存在train.csv文件中,這是我們訓練需要的數據;另一個test.csv文件是我們需要預測的乘客名單,只有相應的特徵。我們要做的工作就是通過對訓練數據的特徵與生存關係進行探索,構建合適的機器學習的模型,再用這個模型預測測試文件中乘客的倖存情況,並將結果保存提交給kaggle。


2.數據集初步探索

#首先導入需要的庫

import numpy as np
import pandas as pd
from IPython.display import display
%matplotlib inline
import seaborn as sns

#導入並展示訓練數據和測試數據

train=pd.read_csv('train.csv')
test=pd.read_csv('test.csv')
display(train.head(n=1),test.head(n=1))

output1:

  PassengerId Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked
0 1 0 3 Braund, Mr. Owen Harris male 22.0 1 0 A/5 21171 7.25 NaN S
  PassengerId Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked
0 892 3 Kelly, Mr. James male 34.5 0 0 330911 7.8292 NaN Q
從運行結果看,導入已經成功,我們可以大體瞄一眼數據的特徵,與訓練集相比測試集很相似,只是多了Survived 標籤。

接下來我們進一步瞭解數據的整體特徵

#查看數據信息:

train.info()
test.info()
output2:
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 891 entries, 0 to 890
Data columns (total 12 columns):
PassengerId    891 non-null int64
Survived       891 non-null int64
Pclass         891 non-null int64
Name           891 non-null object
Sex            891 non-null object
Age            714 non-null float64
SibSp          891 non-null int64
Parch          891 non-null int64
Ticket         891 non-null object
Fare           891 non-null float64
Cabin          204 non-null object
Embarked       889 non-null object
dtypes: float64(2), int64(5), object(5)
memory usage: 83.6+ KB
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 418 entries, 0 to 417
Data columns (total 11 columns):
PassengerId    418 non-null int64
Pclass         418 non-null int64
Name           418 non-null object
Sex            418 non-null object
Age           332 non-null float64
SibSp          418 non-null int64
Parch          418 non-null int64
Ticket         418 non-null object
Fare          417 non-null float64
Cabin          91 non-null object
Embarked       418 non-null object
dtypes: float64(2), int64(4), object(5)
memory usage: 36.0+ KB

從結果可以看到,訓練數據集有891個樣本(樣本量不大,要在模型訓練過程中小心過擬合),11個特徵和1個標籤,其中特徵‘Age','Cabin','Embarked'都有不同程度的缺損;測試集有418個樣本,只有11個特徵,其中特徵'Age','Fare','Cabin'有不同程度的缺損。

對這些缺損的數據可以選擇的處理方式由簡到難包括:

1.直接刪除此特徵(缺損數據太多的情況,防止引入噪聲)

2.直接刪除缺損數據的樣本(~土豪操作~只用於訓練數據集,且樣本量較大,缺損數據樣本較少的情況)

3.直接將有無數值作爲新的特徵(數據缺失較多,且數據有無本身是對預測是一個有用的特徵)

4.中值或均值回補(缺失數據較多,不想損失此較多訓練數據,特徵又比較重要的情況,是比較常用的方法)

5.參考其他特徵,利用與此特徵的相關性編寫算法回補數據(~大神級操作~回補的準確性可能會比較高一些,但實現過程複雜)

這幾種方法具體使用哪一個需要根據實際情況決定,選用複雜的方法得到的結果不一定就好。

再來觀察這11個特徵的類型,其中有4個特徵,包括:'PassengerId','Pclass’,'Sibsp','Parch'屬於整數型數據,5個特徵,包括:'Name','Sex','Ticket','Cabin','Embarked'屬於字符串類型數據,2個特徵,包括:'Age','Fare'屬於浮點數。然而這些數據格式並不都是機器學習模型的菜,你直接餵給模型字符串數據,它會吐的!所以爲統一數據格式,方便模型訓練,我們下面還需要對這些特徵數據進行縮放和轉化。

至此我們對數據已經有了大體的瞭解,接下來我們探討特徵與標籤的關係


3.特徵分析與處理

特徵分析與處理是得到好模型的關鍵一步。而做好這一步需要深刻洞悉數據的模式,挖掘出數據的本質特徵,所以據說很多在kaggle上取得高分的大神都是在具體項目上有些Domain knowledge的人,所以能找到更本質更精華的特徵,餵給訓練模型,自然成績很好(吃的好自然壯嘛),而那些餵了很多粗糧(相關性不大的特徵)的模型,即便算法再努力鍛鍊,也只是rubbish in,rubbish out,練不出八塊腹肌的 (%>_<%)

好了,言歸正傳,現在開始特徵的分析。

首先這個數據集有11個特徵,我們一個個來看:

1.'PassengerId' 乘客的Id,什麼鬼,肯定是rubbish啦。。。果斷刪掉

2.'Pclass’社會階層,感覺應該很重要,貴族階層活下來的可能性肯定更大啊。我們代碼看下

#採用seaborn繪圖函數庫作可視化分析

sns.countplot(x="Pclass", hue="Survived", data=train)
output3:

從上圖可以看出Pclass屬性爲1的人生存的機會要高於或遠高於屬性爲2或3的人,果然。。。

3.'Name' 姓名,中國農村以前有句老話,說取得名越賤,越容易活,所以很多人家的娃,叫二狗子。。。(生活不易啊),但名字真的和生存率有關係嗎?我自己探索的時候是直接把這一項作爲rubbish 刪掉了,後來拜讀一些大神作品時發現名字裏面的title還是大有學問的,可以挖掘出性別,年齡,婚否以及社會地位等信息,大神就是大神,再次膜拜一下。

4.‘Sex' 性別 , ‘Lady First’是一條世界範圍都要遵循的優良道德品質,所以猜測女性在這種情況下活下來的概率更大是合理的,讓我們來看看在這性命攸關的時刻,泰坦尼克號上gentlemen 的表現:

sns.countplot(x="Sex", hue="Survived", data=train)
output4:

從結果看大部分的lady都活下來了,大部分的gentleman都掛了。。。

5.’Age' 年齡,除了婦女,孩子和老人在出現危險的時候也是優先被救助的對象,尊老愛幼應該不只是中華民族的傳統美德吧(雖然經常聽說外國人很獨立,不靠父母不靠娃),所以猜測不同年齡段的人被救助的概率應該不同。So, Let's See...

Wait...這個特徵好像有些麻煩,倆問題:1.數值有缺損,2.年齡是個連續變量,這要如何解決呢

問題1就要用到我們上面的缺損數據處理方法了,用哪個方法呢,考慮到我不是大神,方法5首先排除,年齡特徵很重要肯定不能直接刪,缺損的數據樣本也比較多(177個),不可能土豪到直接刪掉缺損樣本(訓練集本來就小,窮啊。。),所以方法1,2都不靠譜。那現在就只剩下方法3,4兩種了,選哪個呢,糾結啊。。。扔硬幣吧。。。正面。。。選3.。。。嗯,是不是太草率了,算了,還是作圖看一眼吧

#將有年齡數值的轉化爲yes,缺損的轉化爲no
train['Age']=train['Age'].map(lambda x:'yes' if 0<x<100 else 'no')
#作圖比較
sns.countplot(x="Age", hue="Survived", data=train)
output5:

貌似有年齡數值的存活的機率要高一些,可以用操作3的方法處理(扔到反面的小夥伴也可以選擇操作4,哈哈),那麼我們暫且把沒有年齡數值的作爲一類新特徵。

那麼第二個問題,由於年齡是一個連續變量,我們需要對年齡分段進行考察,那如何分段呢,我們可以用Seaborn 函數庫作一個小提琴圖先瞅一眼:

#再次導入原訓練集
train=pd.read_csv('train.csv')
#作小提琴圖
sns.violinplot(x='Survived',y='Age',data=train)
output6:

PS:


從疊加圖(有哪位大神可以告訴我怎麼做這種疊加的圖,我居然是用PS搞定的。。)中我們可以發現大概年齡在12歲以下的孩子生存率要高一些,而年齡12-30歲左右的人死亡率是很高的,30-60歲感覺生存與死亡大體相等,60-75歲死亡率又稍稍上升,75歲以上基本存活。加上之前沒有年齡數值的也可以分爲一類,總共分了6類。

#年齡特徵分類
train['Age']=train['Age'].map(lambda x: 'child' if x<12 else 'youth' if x<30 else 'adlut' if x<60 else 'old' if x<75 else 'tooold' if x>=75 else 'null')
6.'Sibsp' 兄弟姐妹和配偶的數量,這個特徵嘛感覺多少會有些影響,作圖看看:

sns.countplot(x="SibSp", hue="Survived", data=train)
output7:

從圖中結果可以看到大部分人這一屬性都爲0,而爲1,2的情況下貌似倖存大概率會增加,再大又會下降,所以這個特徵可以分成三部分,代碼如下:

train['SibSp']=train['SibSp'].map(lambda x: 'small' if x<1 else 'middle' if x<3 else 'large')

7.'Parch' 父母以及小孩的數量,這個特徵和上一個很類似,估計分佈也會很類似,我們看一下:

sns.countplot(x="Parch", hue="Survived", data=train)

output8:


果然,有父母孩子的比單獨旅行的倖存率的確要高,我們同樣把這個特徵分成三部分

train['Parch']=train['Parch'].map(lambda x: 'small' if x<1 else 'middle' if x<4 else 'large')

8.‘Ticket’ 船票編號,感覺又像是一個rubbish 的特徵,不過據說有大神也可以從中挖出不少東西,可能不同的船票也反映了一些社會階層的信息?感覺蠻複雜的,這裏就先捨棄這個特徵了。

9. 'Fare' 船票的花費,這個特徵還是有用的,因爲花費多少往往與乘客的社會地位有關係,我們作小提琴圖看一下:

sns.violinplot(x='Survived',y='Fare',data=train)

output9:

醜啊。。。從圖中我們看到這個特徵的分佈很不均勻,需要先做一下對數轉化

#用numpy庫裏的對數函數對Fare的數值進行對數轉換
train['Fare']=train['Fare'].map(lambda x:np.log(x+1))
#作小提琴圖:
sns.violinplot(x='Survived',y='Fare',data=train)
output10:


嗯,現在正常多了,PS疊加

可以很明顯的發現當log(Fare)小於2.5時,死亡率是高於生存率的,而大於2.5的生存率是高於死亡率的,因此可以做如下分類:

train['Fare']=train['Fare'].map(lambda x: 'poor' if x<2.5 else 'rich')

10. 'Cabin' 船艙的編號,這個特徵的數值缺損太嚴重,可以直接刪除這個特徵,也可以採用操作3的方法,我們先來看看缺損的數值能否反映出生存率:

#有編號的的爲yes,沒有的爲no
train['Cabin']=train['Cabin'].map(lambda x:'yes' if type(x)==str else 'no')
#作圖
sns.countplot(x="Cabin", hue="Survived", data=train)
output11:

從圖中我們發現相對這個數據缺損的樣本,有數據樣本的存活率要高出很多,所以暫且可以將Cabin 特徵分成這兩類

11. 'Embarked' 乘客上船的港口,這個特徵感覺跟生存率沒啥關係,不過誰知道呢,保險起見還是看一下:

sns.countplot(x="Embarked", hue="Survived", data=train)

output12:

恩,這個特徵還是有些用處的,可以看見在C港口上船的人貌似生存率高一些,難道在那個港口上船的有錢人多一些??這個特徵先保留。同時我們注意到這個特徵有兩個數據的缺損,由於缺損樣本數較少,這裏土豪一把,就直接刪除這兩個樣本。。爽!

#刪掉含有缺損值的樣本
train.dropna(axis=0,inplace=True)
#查看訓練集的信息
train.info()
output13:

<class 'pandas.core.frame.DataFrame'>
Int64Index: 889 entries, 0 to 890
Data columns (total 12 columns):
PassengerId    889 non-null int64
Survived       889 non-null int64
Pclass         889 non-null int64
Name           889 non-null object
Sex            889 non-null object
Age            889 non-null object
SibSp          889 non-null object
Parch          889 non-null object
Ticket         889 non-null object
Fare           889 non-null object
Cabin          889 non-null object
Embarked       889 non-null object
dtypes: int64(3), object(9)
memory usage: 90.3+ KB

所有的特徵以及分析完成了,需要刪除的特徵包括:'PassengerId','Name'和'Ticket',然後對剩餘的特徵進行獨熱編碼。

#將訓練數據分成標記和特徵兩部分
labels= train['Survived']
features= train.drop(['Survived','Name','Ticket'],axis=1)
#對所有特徵實現獨熱編碼
features = pd.get_dummies(features)
encoded = list(features.columns)
print "{} total features after one-hot encoding.".format(len(encoded))
output14:

22 total features after one-hot encoding.


好了,現在訓練集的數據已經整理好了,我們現在同法處理測試集的數據:

#對'Age','SibSp','Parch'特徵進行分段分類
test['Age']=test['Age'].map(lambda x: 'child' if x<12 else 'youth' if x<30 else 'adlut' if x<60 else 'old' if x<75 else 'tooold' if x>=75 else 'null')
test['SibSp']=test['SibSp'].map(lambda x: 'small' if x<1 else 'middle' if x<3 else 'large')
test['Parch']=test['Parch'].map(lambda x: 'small' if x<1 else 'middle' if x<4 else 'large')
#均值補齊'Fare'特徵值並作對數轉換和分類
test.Fare.fillna(test['Fare'].mean(), inplace=True)
test['Fare']=test['Fare'].map(lambda x:np.log(x+1))
test['Fare']=test['Fare'].map(lambda x: 'poor' if x<2.5 else 'rich')
#按'Cabin'是否缺損分類
test['Cabin']=test['Cabin'].map(lambda x:'yes' if type(x)==str else 'no')
#刪除不需要的特徵並進行獨熱編碼
test=test.drop(['PassengerId','Name','Ticket'],axis=1)
test=pd.get_dummies(test)
encoded = list(test.columns)
print "{} total features after one-hot encoding.".format(len(encoded))
output15:

22 total features after one-hot encoding.

完成測試集數據整理。


4.模型構建與參數優化

在完成了數據的清洗與整理之後,我們終於可以開始模型的訓練了,雖然說數據是很關鍵的,但有好的數據必須要有好的模型配合才能發揮作用。就像吃得再好,鍛鍊的方法不科學也很難搞出完美的身材。

所以,讓我們在這繁雜的算法海洋中找到最合適的那個吧!!!

考慮要用到的算法包括:決策樹,SVM,隨機森林,Adaboost,KNN以及傳說中的大殺器Xgboost

首先我們先建立一個統一的訓練框架,方便我們之後採用網格搜索調參

#首先引入需要的庫和函數
from sklearn.model_selection import GridSearchCV
from sklearn.metrics import make_scorer
from sklearn.metrics import accuracy_score,roc_auc_score
from time import time
from sklearn.tree import DecisionTreeClassifier
from sklearn.svm import SVC
from sklearn.ensemble import RandomForestClassifier
from sklearn.ensemble import AdaBoostClassifier
from sklearn.neighbors import KNeighborsClassifier
from xgboost.sklearn import XGBClassifier
#定義通用函數框架
def fit_model(alg,parameters):
    X=features
    y=labels  #由於數據較少,使用全部數據進行網格搜索
    scorer=make_scorer(roc_auc_score)  #使用roc_auc_score作爲評分標準
    grid = GridSearchCV(alg,parameters,scoring=scorer,cv=5)  #使用網格搜索,出入參數
    start=time()  #計時
    grid=grid.fit(X,y)  #模型訓練
    end=time()
    t=round(end-start,3)
    print grid.best_params_  #輸出最佳參數
    print 'searching time for {} is {} s'.format(alg.__class__.__name__,t) #輸出搜索時間
    return grid #返回訓練好的模型
然後定義初始函數
#列出需要使用的算法
alg1=DecisionTreeClassifier(random_state=29)
alg2=SVC(probability=True,random_state=29)  #由於使用roc_auc_score作爲評分標準,需將SVC中的probability參數設置爲True
alg3=RandomForestClassifier(random_state=29)
alg4=AdaBoostClassifier(random_state=29)
alg5=KNeighborsClassifier(n_jobs=-1)
alg6=XGBClassifier(random_state=29,n_jobs=-1)
然後列出我們需要調整的參數及取值範圍,這是一個很繁瑣的工作,需要大量的嘗試和優化。(以下參數範圍並非最優,大家可以繼續探索)
#列出需要調整的參數範圍
parameters1={'max_depth':range(1,10),'min_samples_split':range(1,10)}
parameters2 = {"C":range(1,20), "gamma": [0.05,0.1,0.15,0.2,0.25]}
parameters3_1 = {'n_estimators':range(10,200,10)}
parameters3_2 = {'max_depth':range(1,10),'min_samples_split':range(1,10)}  #搜索空間太大,分兩次調整參數
parameters4 = {'n_estimators':range(10,200,10),'learning_rate':[i/10.0 for i in range(5,15)]}
parameters5 = {'n_neighbors':range(2,10),'leaf_size':range(10,80,20)  }
parameters6_1 = {'n_estimators':range(10,200,10)}
parameters6_2 = {'max_depth':range(1,10),'min_child_weight':range(1,10)}
parameters6_3 = {'subsample':[i/10.0 for i in range(1,10)], 'colsample_bytree':[i/10.0 for i in range(1,10)]}#搜索空間太大,分三次調整參數
OK,下面我們開始調參:

1.DecisionTreeClassifier

clf1=fit_model(alg1,parameters1)

output16:

{'min_samples_split': 1, 'max_depth': 4}

searching time for DecisionTreeClassifier is 2.032 s


2.SVM

clf2=fit_model(alg2,parameters2)

output17:

{'C': 11, 'gamma': 0.05}

searching time for SVC is 56.356 s


3.RandomForest

第一次調參

clf3_m1=fit_model(alg3,parameters3_1)

output18:

{'n_estimators': 180}

searching time for RandomForestClassifier is 41.094 s


第二次調參

alg3=RandomForestClassifier(random_state=29,n_estimators=180)
clf3=fit_model(alg3,parameters3_2)
output19:

{'min_samples_split': 1, 'max_depth': 7}

searching time for RandomForestClassifier is 301.578 s


4.AdaBoost

clf4=fit_model(alg4,parameters4)
output20:

{'n_estimators': 20, 'learning_rate': 1.2}

searching time for AdaBoostClassifier is 233.73 s


5.KNN

clf5=fit_model(alg5,parameters5)

output21:

{'n_neighbors': 9, 'leaf_size': 50}

searching time for KNeighborsClassifier is 46.561 s

6.Xgboost

第一次調參:

clf6_m1=fit_model(alg6,parameters6_1)
output22:

{'n_estimators': 140}

searching time for XGBClassifier is 8.243 s

第二次調參:

alg6=XGBClassifier(n_estimators=140,random_state=29,n_jobs=-1)
clf6_m2=fit_model(alg6,parameters6_2)
output23:

{'max_depth': 4, 'min_child_weight': 5}

searching time for XGBClassifier is 61.065 s

第三次調參:

alg6=XGBClassifier(n_estimators=140,max_depth=4,min_child_weight=5,random_state=29,n_jobs=-1)
clf6=fit_model(alg6,parameters6_3)
output24:

{'subsample': 0.8, 'colsample_bytree': 0.6}

searching time for XGBClassifier is 45.28 s


好了,至此調參和訓練過程結束,我們已經訓練得到了6個模型,現在是檢驗成果的時候了。

首先我們先定義一個保存函數,將預測的結果保存爲可以提交的格式:

def save(clf,i):
    pred=clf.predict(test)
    sub=pd.DataFrame({ 'PassengerId': Id, 'Survived': pred })
    sub.to_csv("res_tan_{}.csv".format(i), index=False)
然後調用這個函數,完成6個模型的預測:

i=1
for clf in [clf1,clf2,clf3,clf4,clf5,clf6]:
    save(clf,i)
    i=i+1
最後我們再採用前5個模型預測結果投票的方法生成一個結果:

#定義多數投票函數
def major(i):
    vote=0
    for clf in [clf1,clf2,clf3,clf4,clf5]:
        pred=clf.predict(test[i:i+1])
        vote=vote+pred
    if vote>2:
        result=1
    else:
        result=0
    return result    
#調用投票函數,並將結果進行保存
L= range(test.shape[0])
pred=map(major,L)
sub=pd.DataFrame({ 'PassengerId': Id, 'Survived': pred })
sub.to_csv("res_tan_7.csv", index=False)
最後讓我們把這7個預測的結果提交給kaggle,得分結果如下圖所示:

這7個預測結果都超過了0.77,基本達到了預測的效果,成績最好的是隨機森林模型,得分0.79425,比較出乎意外的是Xgboost的算法成績竟然只有0.77511,可能是參數沒有調好。多數投票綜合的結果在0.78左右,只是中間水平,並沒有得到提高。大家可以嘗試其他參數,說不定可以得到更好的成績。不過考慮到這個項目數據量太小,能到0.8左右的成績應該已經比較好了,重要的是學習數據處理,特徵分析以及模型構建調參的過程,目的已經達到。

好了,再嘮叨一句,現在得機器學習其實跟我想的不太一樣,感覺完全是我在學習嘛~~~期待強智的出現。












發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章