這個(gè) demo 展示了如何將圖片轉(zhuǎn)換成印象派風(fēng)格, 非常有趣, 本文將從數(shù)據(jù)流的角度分析作者是如何做的.
Demo 的具體步驟和效果可見 GitHub.
從圖片生成訓(xùn)練數(shù)據(jù)
讀取每個(gè)像素 RGB 的值, 并歸一化.
下圖是由(0,0)坐標(biāo)
像素生成的 Pixel
對象, RGB 的值都是由原值除以 255 得到的.
Pixel -> Example
接著通過 pixelToExample
方法, 每個(gè)像素會被轉(zhuǎn)換為一個(gè) Example
對象.
RGB 分別對應(yīng)3個(gè) FeatureVector
, 其中 C
是 channel 的縮寫, 存儲了對應(yīng)的顏色, $target
的值會作為輸出參與訓(xùn)練.
$target
這個(gè)鍵名是在 image_impressionism.conf 中配置的, 該文件還記錄了數(shù)據(jù)路徑/模型參數(shù)/特征轉(zhuǎn)換方法和參數(shù)
等其他配置.
下圖是一個(gè) Example
實(shí)例.
這是完全展開后轉(zhuǎn)成 json 格式的示例.
{
"example": [
{
"stringFeatures": {
"C": [
"Red"
]
},
"floatFeatures": {
"$target": {
"": 0.5529411764705883
}
}
},
{
"stringFeatures": {
"C": [
"Green"
]
},
"floatFeatures": {
"$target": {
"": 0.5725490196078431
}
}
},
{
"stringFeatures": {
"C": [
"Blue"
]
},
"floatFeatures": {
"$target": {
"": 0.5568627450980392
}
}
}
],
"context": {
"floatFeatures": {
"LOC": {
"X": 0,
"Y": 0
}
}
}
}
存儲
最后用 thrift 將特征對象序列化后壓縮存儲.
sc.parallelize(pixels)
.map(x => pixelToExample(x, true))
.map(Util.encode)
.saveAsTextFile(output, classOf[GzipCodec])
訓(xùn)練模型
特征轉(zhuǎn)換
所有特征會按順序依次應(yīng)用3類 transform: context_transform
, item_transform
, combined_transform
.
每類 transform 的名稱及相關(guān)參數(shù)都需要在 image_impressionism.conf
中配置.
Context Transform
Context transform 會轉(zhuǎn)換 Example
中的 context
屬性, 并將新特征存入 context.stringFeatures
.
quantize_pixel_location {
transform: multiscale_grid_quantize
# Grid up the pixels into squares of the following sizes.
# Use relatively prime grids to create jitter.
buckets : [ 3.0, 7.0, 17.0, 31.0, 47.0, 67.0, 79.0, 89.0, 97.0 ]
field1: "LOC"
value1: "Y"
value2: "X"
output: "QLOC"
}
此處用的是 multiscale_grid_quantize
方法, 實(shí)現(xiàn)該方法的類為 MultiscaleGridQuantizeTransform.
該方法將二維平面劃分成不同大小的正方形格子, 然后將每個(gè)格子里的點(diǎn)都映射到該格子.
即用更大的粒度來描述平面, 借此消除局部差異, 提取共同特征.
格子的 ID 由其邊長和左上角點(diǎn)的坐標(biāo)拼接生成.
例如 [3.0]=(0.0,0.0)
包含了 (0,0),(0,1),(0,2),(1,0),(1,1),(1,2),(2,0),(2,1),(2,2)
9個(gè)點(diǎn).
buckets
配置了會用哪些邊長的格子, field1/value1/value2
可結(jié)合之前的特征示例體會含義, output
是新特征的名字.
下圖是坐標(biāo)(0,0)
轉(zhuǎn)換后得到的9個(gè)新特征.
Item Transform
Item transform 將轉(zhuǎn)換 Example.example
中每個(gè) FeatureVector
, 轉(zhuǎn)換后的新特征會存入 stringFeatures
.
identity_transform {
transform: list
transforms: []
}
list 表示將逐個(gè)應(yīng)用 transforms 列表中的變換, 空列表意味著不做任何轉(zhuǎn)換.
Combined Transform
Combined transform 將會把 context
和 example 中每個(gè) FeatureVector
結(jié)合起來, 并存入后者的 stringFeatures
中.
代碼實(shí)現(xiàn)分為兩步:
- 拷貝
context.stringFeatures
至FeatureVector.stringFeatures
. - 對
example
中每個(gè)FeatureVector
的stringFeatures
應(yīng)用配置中指定的 transform.
C_X_QLOC {
transform: cross
field1: "C" // Color channel
field2: "QLOC" // Quantized location
output: "C_x_QLOC"
}
combined_transform {
transform: list
transforms: [
C_X_QLOC
]
}
cross 對應(yīng)的類為 CrossTransform, 它會把 field1/field2
的值拼接起來作為 output
的值.
需要注意的是:
- 轉(zhuǎn)換前每個(gè)
Example
對象中example
屬性有3個(gè)FeatureVector
. - 轉(zhuǎn)換后3個(gè)
FeatureVector
將分別轉(zhuǎn)換為1個(gè)Example
對象, 每個(gè)Example
對象的example
屬性只有1個(gè)FeatureVector
.
下圖為轉(zhuǎn)換后的一個(gè) Example
對象:
訓(xùn)練
Aerosolve 的訓(xùn)練算法都是基于 Spark 實(shí)現(xiàn)的, 所以和訓(xùn)練有關(guān)的代碼都放在一個(gè)獨(dú)立的子項(xiàng)目 training 中.
這個(gè) demo 用的是線性回歸模型, 訓(xùn)練方法為 SGD (Stochastic Gradient Descent), 代碼實(shí)現(xiàn)在 LinearRankerTrainer 中.
每次迭代計(jì)算權(quán)重前, 會從 FeatureVector
取出相關(guān)的目標(biāo)值和特征, 如下圖:
權(quán)重訓(xùn)練完后的形式為 ((feature family, feature), weigth)
, 如下圖:
保存模型
模型由兩部分組成: 一個(gè) ModelHeader
和 若干個(gè) ModelRecord
(ModelHeader
實(shí)際上會保存為成一個(gè)特殊的 ModelRecord
).
線性模型的 ModelHeader
只用到了兩個(gè)屬性: modelType
的值會設(shè)置為 linear
, numRecords
會設(shè)為 weights.size
.
模型也是用 thrift 序列化后存儲.
應(yīng)用模型輸出印象派圖片
宏觀視角
前面從微觀角度觀察了整個(gè)數(shù)據(jù)流, 接著我們從宏觀的角度看看它和線性模型是怎么對接的.
線性模型實(shí)質(zhì)是一個(gè)方程組, 訓(xùn)練權(quán)重的過程即求解自變量系數(shù)的過程.
該 demo 的方程組共有 num_pixels * 3
個(gè)方程, 每個(gè)像素會對應(yīng)3個(gè)方程,
這是因?yàn)槊總€(gè)像素有3個(gè) color channel (red/green/blue).
方程的因變量 y 即 r/g/b 歸一化后的值.
自變量的個(gè)數(shù) = 不同特征的總個(gè)數(shù)
, 自變量只有0和1兩種取值, 0表示該方程中不含此特征, 1表示包含.
特征有3類:
- Red, Green, Blue
- 離散化后的坐標(biāo), 例如:
[3.0]=(0.0,0.0)
- 前兩類的交叉組合, 例如:
Red^[3.0]=(0.0,0.0)
第2類特征總數(shù) num_loc
和像素個(gè)數(shù)有關(guān).
num_loc = sum([(int(image_width / b) + 1) * (int(image_height / b) + 1) for b in buckets])
總的特征個(gè)數(shù) = 3 + num_loc + num_loc * 3 = num_loc * 4 + 3
每個(gè)方程只有少數(shù)特征對應(yīng)的自變量取值為1, 所以不同類特征的影響范圍是不同的.
- 第1類會影響 1/3 的方程.
- 第2, 3類特征只會影響和格子中像素有關(guān)的方程.
- 第2類會影響
邊長^2 * 3
個(gè), 第3類為邊長^2
個(gè).
單張圖片是怎么生成的
輸入數(shù)據(jù)是每個(gè)點(diǎn)的坐標(biāo), 帶入模型后按 color channel 輸出預(yù)測值, 合并后就得到了該點(diǎn)的 RGB 值.
其過程就像在大方格上摞小方格, 最終的高度即預(yù)測值.
這樣的預(yù)測結(jié)果肯定好于只做單一劃分的方法, 即包含了整體信息, 也包含了局部差異.
我覺得 airbnb 預(yù)測房價(jià)也應(yīng)該是類似的想法.
動圖是怎么生成的
生成每一幀的方法和單張圖片一樣, 只是每幀用到的權(quán)重個(gè)數(shù)不一樣.
假設(shè)總共有 N
個(gè)權(quán)重, 第 i
幀只會用前 i/(N-1)
個(gè)來繪制圖像, i∈{0, 1, ..., N-1}
.
這樣圖片就會漸漸的由模糊變清晰.
其他
- Readme 中對最紅, 最藍(lán)的解釋不太恰當(dāng), 詳見 Google Group.
- 項(xiàng)目還在發(fā)展階段, 從新舊代碼的質(zhì)量就能看出來. 例如會看到一些復(fù)制粘貼的實(shí)現(xiàn).
- 代碼中有一些重復(fù)計(jì)算的問題. 例如理論上 context 只會計(jì)算一次, 但實(shí)際會計(jì)算多次, 不過我覺得影響不大.
- 還未成為性能瓶頸. 我測試了修改后的速度, 并未提升多少. 該demo的時(shí)間多花在文件讀取上, 每輪迭代約5分鐘, 約一半時(shí)間是在讀文件.
- 新代碼中重復(fù)計(jì)算的情況有所改善, 說明作者知道這個(gè)事情.
- 項(xiàng)目還在發(fā)展期, 不需要過早優(yōu)化.
Debug 的小技巧
- 換一個(gè)像素較少的圖片, 這樣會大大節(jié)省每個(gè)步驟的時(shí)間.
- 換圖后需修改
image_impressionism.conf
中make_impression
的寬高, 讓生成的圖片大小更合適. - 將
build.gradle
中 spark 的依賴從 provided 改為 compile. - 將
JobRunner.scala
作為 debug 的入口, 記得加上.setMaster("local")
. -
com.airbnb.aerosolve:training
的版本有點(diǎn)低, 可更換為最新版. - Ubuntu 用戶如不想編譯安裝 thrift, 可用 docker thrift. Mac 用戶可用 brew 安裝.