在本教程中,我們將使用TensorFlow中的tf.estimator API來解決二元分類問題:給定有關某人(如年齡,教育程度,婚姻狀況和職業(特征))的人口普查數據,嘗試預測這個人每年是否能掙5萬多美元(目標標簽)。我們將訓練一個邏輯回歸模型,給定個人的信息,我們的模型將輸出0到1之間的數字,這可以解釋為個人年收入超過5萬美元的概率。
開始
要嘗試本教程的代碼,先:
-
下載教程代碼。
-
執行我們提供給您的數據下載腳本:
$ python data_download.py
-
使用以下命令執行教程代碼以訓練本教程中描述的線性模型:
$ python wide_deep.py --model_type=wide
繼續閱讀以了解此代碼如何構建線性模型。
讀取人口普查數據
我們將使用的數據集是人口普查收入數據集。我們提供了用於下載代碼並執行一些額外的清理。
由於該任務是一個二元分類問題,因此我們將構造一個名為”label”的標簽列,如果收入超過50K,則值為1,否則為0。相關信息,請參閱input_fn
,在。
接下來,我們來看看數據情況,看看可以使用哪些列來預測目標標簽。這些列可以分為兩類:類別和連續值:
- 類別數據:如果它的價值隻能是有限集合中的一個類別。例如,一個人(妻子,丈夫,未婚等)或教育水平(高中,大學等)的關係狀況是類別數據。
- 連續數據:如果其值可以是連續範圍內的任何數值。例如,一個人的資本收益(例如$ 14,084)是一個連續的值。
以下是人口普查收入數據集中可用列的列表:
列名稱 | 類型 | 描述 |
---|---|---|
age | 連續 | 個人的年齡 |
workclass | 類別 | 個人擁有的雇主類型(政府,軍隊,私人等)。 |
fnlwgt | 連續 | 普查員認為觀察所代表的人數(樣本權重)。最終的權重將不會被使用。 |
education | 類別 | 這個人達到的最高教育水平。 |
education_num | 連續 | 數值形式的最高教育水平。 |
marital_status | 類別 | 個人的婚姻狀況。 |
occupation | 類別 | 個人的職業。 |
relationship | 類別 | 妻子,Own-child,丈夫,Not-in-family,Other-relative,未婚。 |
race | 類別 | 白色,Asian-Pac-Islander,Amer-Indian-Eskimo,其他,黑色。 |
gender | 類別 | 女人男人。 |
capital_gain | 連續 | 資本收益記錄。 |
capital_loss | 連續 | 記錄資本損失。 |
hours_per_week | 連續 | 每周工作時間。 |
native_country | 類別 | 個人原籍國。 |
income | 類別 | “>50K”或“ |
將數據轉換為張量
在構建tf.estimator模型時,輸入數據通過輸入構建器函數來指定。此構建函數在稍後傳遞給tf.estimator.Estimator方法(例如,train
和evaluate
),之前不會被調用。這個函數的目的是構造輸入數據,它的形式表示為tf.Tensor
或者tf.SparseTensor
。具體來說,輸入生成器函數將成對返回以下內容:
features
:一個dict, 存儲從特征列名到Tensors
或SparseTensors
的映射。labels
: 一個Tensor
包含標簽列。
features
的鍵將被用於在下一節中構建列。因為我們想在不同的數據上調用train
和evaluate
方法,我們定義一個方法,返回一個基於給定數據的輸入函數。請注意,返回的輸入函數將在構建TensorFlow圖形時調用,而不是在運行圖形時調用。它返回的是輸入數據的表示,即Tensor
(或SparseTensor
)。
訓練或者測試數據中的每個連續列將被轉換成一個Tensor
,這通常是表示密集數據的良好格式。對於類別數據,我們必須將數據表示為一個SparseTensor
,這種數據格式適合表示稀疏數據。我們的input_fn
使用tf.data
API,可以很容易地將轉換應用於我們的數據集:
def input_fn(data_file, num_epochs, shuffle, batch_size):
"""Generate an input function for the Estimator."""
assert tf.gfile.Exists(data_file), (
'%s not found. Please make sure you have either run data_download.py or '
'set both arguments --train_data and --test_data.' % data_file)
def parse_csv(value):
print('Parsing', data_file)
columns = tf.decode_csv(value, record_defaults=_CSV_COLUMN_DEFAULTS)
features = dict(zip(_CSV_COLUMNS, columns))
labels = features.pop('income_bracket')
return features, tf.equal(labels, '>50K')
# Extract lines from input files using the Dataset API.
dataset = tf.data.TextLineDataset(data_file)
if shuffle:
dataset = dataset.shuffle(buffer_size=_SHUFFLE_BUFFER)
dataset = dataset.map(parse_csv, num_parallel_calls=5)
# We call repeat after shuffling, rather than before, to prevent separate
# epochs from blending together.
dataset = dataset.repeat(num_epochs)
dataset = dataset.batch(batch_size)
iterator = dataset.make_one_shot_iterator()
features, labels = iterator.get_next()
return features, labels
模型的特征選擇和特征工程
選擇和構造正確的特征列是學習有效模型的關鍵。一個特征列可以是原始數據中的原始列之一(我們稱它們為“基本特征列),或者基於在一個或多個基本列上定義的一些轉換創建的任何新列(我們稱之為“派生特征列)。基本上,”feature column”是可以用來預測目標標簽的任何原始或衍生變量的抽象概念。
基本類別特征列
要為類別特征定義特征列,我們可以使用tf.feature_column API創建一個CategoricalColumn
。如果您知道列的所有可能的特征值的集合,並且特征值集合較小,您可以使用categorical_column_with_vocabulary_list
。列表中的每個鍵都將被分配一個從0開始的自增ID。例如,對於relationship
列,我們可以將特征字符串”Husband”分配給一個整數ID為0,”Not-in-family”分配給1,等等:
relationship = tf.feature_column.categorical_column_with_vocabulary_list(
'relationship', [
'Husband', 'Not-in-family', 'Wife', 'Own-child', 'Unmarried',
'Other-relative'])
如果我們事先不知道可能的值怎麽辦?這不是問題。我們可以用categorical_column_with_hash_bucket
代替:
occupation = tf.feature_column.categorical_column_with_hash_bucket(
'occupation', hash_bucket_size=1000)
特征列中的每個可能的值occupation
將被散列為整數ID。看下麵的一個例子:
ID | 特征 |
---|---|
… | |
9 | "Machine-op-inspct" |
… | |
103 | "Farming-fishing" |
… | |
375 | "Protective-serv" |
… |
無論我們選擇哪種方式來定義一個SparseColumn
,每個特征字符串將通過查找固定映射或直接哈希映射到整數ID。請注意,散列衝突是可能的,但可能不會顯著影響模型質量。在底層實現中,LinearModel
類負責管理映射和創建tf.Variable
存儲每個特征ID的模型參數(也稱為模型權重)。模型參數將通過稍後提到的模型訓練過程來學習。
我們使用類似的技巧來定義其他的類別特征:
education = tf.feature_column.categorical_column_with_vocabulary_list(
'education', [
'Bachelors', 'HS-grad', '11th', 'Masters', '9th', 'Some-college',
'Assoc-acdm', 'Assoc-voc', '7th-8th', 'Doctorate', 'Prof-school',
'5th-6th', '10th', '1st-4th', 'Preschool', '12th'])
marital_status = tf.feature_column.categorical_column_with_vocabulary_list(
'marital_status', [
'Married-civ-spouse', 'Divorced', 'Married-spouse-absent',
'Never-married', 'Separated', 'Married-AF-spouse', 'Widowed'])
relationship = tf.feature_column.categorical_column_with_vocabulary_list(
'relationship', [
'Husband', 'Not-in-family', 'Wife', 'Own-child', 'Unmarried',
'Other-relative'])
workclass = tf.feature_column.categorical_column_with_vocabulary_list(
'workclass', [
'Self-emp-not-inc', 'Private', 'State-gov', 'Federal-gov',
'Local-gov', '?', 'Self-emp-inc', 'Without-pay', 'Never-worked'])
# To show an example of hashing:
occupation = tf.feature_column.categorical_column_with_hash_bucket(
'occupation', hash_bucket_size=1000)
基本連續特征列
同樣,我們可以定義一個NumericColumn
,對於我們要在模型中使用的每個連續特征列:
age = tf.feature_column.numeric_column('age')
education_num = tf.feature_column.numeric_column('education_num')
capital_gain = tf.feature_column.numeric_column('capital_gain')
capital_loss = tf.feature_column.numeric_column('capital_loss')
hours_per_week = tf.feature_column.numeric_column('hours_per_week')
通過分桶將連續性特征的轉為類別特征
有時連續特征和標簽之間的關係不是線性的。作為一個假設的例子,一個人的收入在職業生涯初期可能會隨著年齡的增長而增長,然後增長可能會放緩,最終退休後收入會減少。在這種情況下,使用原始的age
作為實值特征列可能不是一個好的選擇,因為模型隻能學習三種情況之一:
- 收入總是隨著年齡增長而增長(正相關),
- 收入總是隨著年齡的增長而減少(負相關),或者
- 不論年齡多少,收入都保持不變(不相關)
如果我們想分別學習收入與各年齡段之間的細粒度相關性,我們可以利用分桶(bucketization)。分桶是將連續特征的整個範圍劃分為一組連續的桶,然後根據該值落入哪個桶將原始數值特征轉換成桶ID(作為分類特征)的過程。所以,我們可以定義一個bucketized_column
過度age
如:
age_buckets = tf.feature_column.bucketized_column(
age, boundaries=[18, 25, 30, 35, 40, 45, 50, 55, 60, 65])
其中boundaries
是桶邊界的列表。在這種情況下,有10個邊界,從而形成11個年齡組(從17歲以下,18-24,25-29,…,到65歲以上)。
用CrossedColumn交叉多列
單獨使用每個基本特征列可能不足以解釋數據。例如,對於不同的職業,教育與標簽(賺取50000美元)之間的相關性可能不同。因此,如果我們隻學習單一的模型權重education="Bachelors"
和education="Masters"
,我們將無法捕獲每一個education-occupation組合(例如區分education="Bachelors" AND occupation="Exec-managerial"
和education="Bachelors" AND occupation="Craft-repair"
)。要了解不同的功能組合之間的差異,我們可以添加交叉的特征列到模型。
education_x_occupation = tf.feature_column.crossed_column(
['education', 'occupation'], hash_bucket_size=1000)
我們也可以使用多個列創建一個CrossedColumn
。每個組成列可以是基本類別特征列(SparseColumn
),bucketized實值特征列(BucketizedColumn
),甚至另一個CrossColumn
。這是一個例子:
age_buckets_x_education_x_occupation = tf.feature_column.crossed_column(
[age_buckets, 'education', 'occupation'], hash_bucket_size=1000)
定義Logistic回歸模型
處理輸入數據並定義所有特征列後,我們現在準備將它們放在一起並構建一個Logistic回歸模型。在上一節中,我們已經看到了幾種類型的基本和派生特征列,包括:
CategoricalColumn
NumericColumn
BucketizedColumn
CrossedColumn
所有這些都抽象FeatureColumn
類的子類,可以添加到模型的feature_columns
字段:
base_columns = [
education, marital_status, relationship, workclass, occupation,
age_buckets,
]
crossed_columns = [
tf.feature_column.crossed_column(
['education', 'occupation'], hash_bucket_size=1000),
tf.feature_column.crossed_column(
[age_buckets, 'education', 'occupation'], hash_bucket_size=1000),
]
model_dir = tempfile.mkdtemp()
model = tf.estimator.LinearClassifier(
model_dir=model_dir, feature_columns=base_columns + crossed_columns)
該模型還自動學習一個偏置項,它控製著沒有任何特征的預測(更多解釋請參見“邏輯回歸的工作原理”一節)。學習的模型文件將被存儲在model_dir
。
訓練和評估我們的模型
現在讓我們看看如何實際訓練模型。使用tf.estimator API訓練模型隻是一個簡單的命令:
model.train(input_fn=lambda: input_fn(train_data, num_epochs, True, batch_size))
在模型被訓練之後,我們可以評估我們的模型在預測剩餘數據標簽上的效果:
results = model.evaluate(input_fn=lambda: input_fn(
test_data, 1, False, batch_size))
for key in sorted(results):
print('%s: %s' % (key, results[key]))
最終輸出的第一行應該是這樣的accuracy: 0.83557522
,這意味著準確率為83.6%。隨意嘗試更多的特征和轉換,看看你是否能做得更好!
如果你想看到一個可用的端到端的例子,你可以下載我們的示例代碼並設置model_type
標誌為wide
。
加入正則化來防止過度擬合
正規化是一種用來避免過度擬合的技術。過度擬合時,模型在訓練數據上表現良好,但在模型以前從未見過的測試數據(如實時流量)上卻很糟糕。過度擬合通常發生在模型過於複雜的情況下,例如相對於觀察到的訓練數據的參數數量太多。正則化允許你控製你的模型的複雜性,並使模型在看不見的數據上有更好的泛化能力。
在線性模型庫中,可以將L1和L2正則化添加到模型中,如下所示:
model = tf.estimator.LinearClassifier(
model_dir=model_dir, feature_columns=base_columns + crossed_columns,
optimizer=tf.train.FtrlOptimizer(
learning_rate=0.1,
l1_regularization_strength=1.0,
l2_regularization_strength=1.0))
L1和L2正則化之間的一個重要區別是,L1正則化傾向於使模型權重保持為零,從而創建更稀疏的模型,而L2正則化也嘗試使模型權重接近於零,但不一定為零。因此,如果您增加L1正則化的強度,您將有一個較小的模型大小,因為許多模型權重將為零。當特征空間非常大但是稀疏時,以及當存在資源限製時,通常需要較小的模型,因為這些資源限製會阻止您提供太大的模型。
在實踐中,你應該嘗試L1,L2正則化強度的各種組合,找到最好的控製過度擬合的最佳參數,並給你一個理想的模型大小。
邏輯回歸如何工作
最後,讓我們花一點時間談一談Logistic回歸模型實際上是什麽。我們將標簽表示為Y,並將觀察到的特征集合表示為特征向量x = [x1, x2, …, xd]。我們定義Y = 1,如果個人獲得>=5萬美元; 否則Y = 0。在Logistic回歸中,給定特征x的標簽為正(Y = 1)的概率為:
其中w = [w1, w2, …, wd]是特征x = [x1,x2, …, xd]的模型權重 。 b是一個常被稱為偏置的常量。該方程由兩部分組成:線性模型和邏輯函數:
-
線性模型:首先,我們可以看到wT x + b = b + w1 x1 + … + wd xd是一個線性模型,輸出是輸入特征的線性函數X。偏置b是在沒有觀察到任何特征的情況下做出的預測。模型權重wi反映特征xi如何與正標簽相關。如果xi與正標簽正相關,則權重wi增加,並且概率P(Y = 1 | x)將更接近於1。另一方麵,如果xi與正標簽呈負相關,那麽權重wi將減少,並且概率P(Y = 1 | x)將更接近於0 。
-
邏輯(Logistic)函數:其次,我們可以看到,對線性模型應用了邏輯函數(也稱為S形函數)S(t)= 1 /(1+exp(-t))。邏輯函數用於將線性模型的輸出從任何實數轉換到[0,1]的範圍內,可以被解釋為一個概率。
模型訓練是一個優化問題:目標是找到一組模型權重(即模型參數)來最小化定義在訓練數據上損失函數,如邏輯回歸模型的邏輯損失。損失函數測量實際標簽與模型預測之間的差異。如果預測值非常接近實際標簽,損失值將會很低;如果預測離標簽很遠,那麽損失值就會很高。
深入學習
如果你有興趣了解更多,請查看我們的Wide&Deep深度學習教程在這裏我們將向您展示如何結合使用tf.estimator API聯合訓練線性模型和深度神經網絡。