搜索領域,早已出現了“查找相似圖片/相似商品”的相關功能,如 Google 搜圖,百度搜圖,淘寶的拍照搜商品等。要實現類似的計算圖片相似度的功能,除了使用聽起來高大上的“人工智能”以外,其實通過 js 和幾種簡單的算法,也能八九不離十地實現類似的效果。
在閱讀本文之前,強烈建議先閱讀完阮一峰于多年所撰寫的《相似圖片搜索的原理》相關文章,本文所涉及的算法也來源于其中。
體驗地址:img-compare.netlify.com/
為了便于理解,每種算法都會經過“特征提取”和“特征比對”兩個步驟進行。接下來將著重對每種算法的“特征提取”步驟進行詳細解讀,而“特征比對”則單獨進行闡述。
參考阮大的文章,“平均哈希算法”主要由以下幾步組成:
第一步,縮小尺寸為8×8,以去除圖片的細節,只保留結構、明暗等基本信息,摒棄不同尺寸、比例帶來的圖片差異。
第二步,簡化色彩。將縮小后的圖片轉為灰度圖像。
第三步,計算平均值。計算所有像素的灰度平均值。
第四步,比較像素的灰度。將64個像素的灰度,與平均值進行比較。大于或等于平均值,記為1;小于平均值,記為0。
第五步,計算哈希值。將上一步的比較結果,組合在一起,就構成了一個64位的整數,這就是這張圖片的指紋。
第六步,計算哈希值的差異,得出相似度(漢明距離或者余弦值)。
明白了“平均哈希算法”的原理及步驟以后,就可以開始編碼工作了。為了讓代碼可讀性更高,本文的所有例子我都將使用 typescript 來實現。
我們采用 canvas 的 drawImage() 方法實現圖片壓縮,后使用 getImageData() 方法獲取 ImageData 對象。
export function compressImg (imgSrc: string, imgWidth: number = 8): Promise<ImageData> {
return new Promise((resolve, reject) => {
if (!imgSrc) {
reject('imgSrc can not be empty!')
}
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
const img = new Image()
img.crossOrigin = 'Anonymous'
img.onload = function () {
canvas.width = imgWidth
canvas.height = imgWidth
ctx?.drawImage(img, 0, 0, imgWidth, imgWidth)
const data = ctx?.getImageData(0, 0, imgWidth, imgWidth) as ImageData
resolve(data)
}
img.src = imgSrc
})
}
復制代碼
可能有讀者會問,為什么使用 canvas 可以實現圖片壓縮呢?簡單來說,為了把“大圖片”繪制到“小畫布”上,一些相鄰且顏色相近的像素往往會被刪減掉,從而有效減少了圖片的信息量,因此能夠實現壓縮的效果:
在上面的 compressImg() 函數中,我們利用 new Image() 加載圖片,然后設定一個預設的圖片寬高值讓圖片壓縮到指定的大小,最后獲取到壓縮后的圖片的 ImageData 數據——這也意味著我們能獲取到圖片的每一個像素的信息。
關于 ImageData,可以參考 MDN 的文檔介紹。
為了把彩色的圖片轉化成灰度圖,我們首先要明白“灰度圖”的概念。在維基百科里是這么描述灰度圖像的:
在計算機領域中,灰度(Gray scale)數字圖像是每個像素只有一個采樣顏色的圖像。
大部分情況下,任何的顏色都可以通過三種顏色通道(R, G, B)的亮度以及一個色彩空間(A)來組成,而一個像素只顯示一種顏色,因此可以得到“像素 => RGBA”的對應關系。而“每個像素只有一個采樣顏色”,則意味著組成這個像素的三原色通道亮度相等,因此只需要算出 RGB 的平均值即可:
// 根據 RGBA 數組生成 ImageData
export function createImgData (dataDetail: number[]) {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
const imgWidth = Math.sqrt(dataDetail.length / 4)
const newImageData = ctx?.createImageData(imgWidth, imgWidth) as ImageData
for (let i = 0; i < dataDetail.length; i += 4) {
let R = dataDetail[i]
let G = dataDetail[i + 1]
let B = dataDetail[i + 2]
let Alpha = dataDetail[i + 3]
newImageData.data[i] = R
newImageData.data[i + 1] = G
newImageData.data[i + 2] = B
newImageData.data[i + 3] = Alpha
}
return newImageData
}
export function createGrayscale (imgData: ImageData) {
const newData: number[] = Array(imgData.data.length)
newData.fill(0)
imgData.data.forEach((_data, index) => {
if ((index + 1) % 4 === 0) {
const R = imgData.data[index - 3]
const G = imgData.data[index - 2]
const B = imgData.data[index - 1]
const gray = ~~((R + G + B) / 3)
newData[index - 3] = gray
newData[index - 2] = gray
newData[index - 1] = gray
newData[index] = 255 // Alpha 值固定為255
}
})
return createImgData(newData)
}
復制代碼
ImageData.data 是一個 Uint8ClampedArray 數組,可以理解為“RGBA數組”,數組中的每個數字取值為0~255,每4個數字為一組,表示一個像素的 RGBA 值。由于ImageData 為只讀對象,所以要另外寫一個 creaetImageData() 方法,利用 context.createImageData() 來創建新的 ImageData 對象。
拿到灰度圖像以后,就可以進行指紋提取的操作了。
在“平均哈希算法”中,若灰度圖的某個像素的灰度值大于平均值,則視為1,否則為0。把這部分信息組合起來就是圖片的指紋。由于我們已經拿到了灰度圖的 ImageData 對象,要提取指紋也就變得很容易了:
export function getHashFingerprint (imgData: ImageData) {
const grayList = imgData.data.reduce((pre: number[], cur, index) => {
if ((index + 1) % 4 === 0) {
pre.push(imgData.data[index - 1])
}
return pre
}, [])
const length = grayList.length
const grayAverage = grayList.reduce((pre, next) => (pre + next), 0) / length
return grayList.map(gray => (gray >= grayAverage ? 1 : 0)).join('')
}
復制代碼
通過上述一連串的步驟,我們便可以通過“平均哈希算法”獲取到一張圖片的指紋信息(示例是大小為8×8的灰度圖):
關于“感知哈希算法”的詳細介紹,可以參考這篇文章:《基于感知哈希算法的視覺目標跟蹤》。
簡單來說,該算法經過離散余弦變換以后,把圖像從像素域轉化到了頻率域,而攜帶了有效信息的低頻成分會集中在 DCT 矩陣的左上角,因此我們可以利用這個特性提取圖片的特征。
該算法的步驟如下:
縮小尺寸:pHash以小圖片開始,但圖片大于88,3232是最好的。這樣做的目的是簡化了DCT的計算,而不是減小頻率。 簡化色彩:將圖片轉化成灰度圖像,進一步簡化計算量。 計算DCT:計算圖片的DCT變換,得到32*32的DCT系數矩陣。 縮小DCT:雖然DCT的結果是3232大小的矩陣,但我們只要保留左上角的88的矩陣,這部分呈現了圖片中的最低頻率。 計算平均值:如同均值哈希一樣,計算DCT的均值。 計算hash值:這是最主要的一步,根據8*8的DCT矩陣,設置0或1的64位的hash值,大于等于DCT均值的設為”1”,小于DCT均值的設為“0”。組合在一起,就構成了一個64位的整數,這就是這張圖片的指紋。
回到代碼中,首先添加一個 DCT 方法:
function memoizeCosines (N: number, cosMap: any) {
cosMap = cosMap || {}
cosMap[N] = new Array(N * N)
let PI_N = Math.PI / N
for (let k = 0; k < N; k++) {
for (let n = 0; n < N; n++) {
cosMap[N][n + (k * N)] = Math.cos(PI_N * (n + 0.5) * k)
}
}
return cosMap
}
function dct (signal: number[], scale: number = 2) {
let L = signal.length
let cosMap: any = null
if (!cosMap || !cosMap[L]) {
cosMap = memoizeCosines(L, cosMap)
}
let coefficients = signal.map(function () { return 0 })
return coefficients.map(function (_, ix) {
return scale * signal.reduce(function (prev, cur, index) {
return prev + (cur * cosMap[L][index + (ix * L)])
}, 0)
})
}
復制代碼
然后添加兩個矩陣處理方法,分別是把經過 DCT 方法生成的一維數組升維成二維數組(矩陣),以及從矩陣中獲取其“左上角”內容。
// 一維數組升維
function createMatrix (arr: number[]) {
const length = arr.length
const matrixWidth = Math.sqrt(length)
const matrix = []
for (let i = 0; i < matrixWidth; i++) {
const _temp = arr.slice(i * matrixWidth, i * matrixWidth + matrixWidth)
matrix.push(_temp)
}
return matrix
}
// 從矩陣中獲取其“左上角”大小為 range × range 的內容
function getMatrixRange (matrix: number[][], range: number = 1) {
const rangeMatrix = []
for (let i = 0; i < range; i++) {
for (let j = 0; j < range; j++) {
rangeMatrix.push(matrix[i][j])
}
}
return rangeMatrix
}
復制代碼
復用之前在“平均哈希算法”中所寫的灰度圖轉化函數createGrayscale(),我們可以獲取“感知哈希算法”的特征值:
export function getPHashFingerprint (imgData: ImageData) {
const dctData = dct(imgData.data as any)
const dctMatrix = createMatrix(dctData)
const rangeMatrix = getMatrixRange(dctMatrix, dctMatrix.length / 8)
const rangeAve = rangeMatrix.reduce((pre, cur) => pre + cur, 0) / rangeMatrix.length
return rangeMatrix.map(val => (val >= rangeAve ? 1 : 0)).join('')
}
復制代碼
首先摘抄一段阮大關于“顏色分布法“的描述:
阮大把256種顏色取值簡化成了4種。基于這個原理,我們在進行顏色分布法的算法設計時,可以把這個區間的劃分設置為可修改的,唯一的要求就是區間的數量必須能夠被256整除。算法如下:
// 劃分顏色區間,默認區間數目為4個
// 把256種顏色取值簡化為4種
export function simplifyColorData (imgData: ImageData, zoneAmount: number = 4) {
const colorZoneDataList: number[] = []
const zoneStep = 256 / zoneAmount
const zoneBorder = [0] // 區間邊界
for (let i = 1; i <= zoneAmount; i++) {
zoneBorder.push(zoneStep * i - 1)
}
imgData.data.forEach((data, index) => {
if ((index + 1) % 4 !== 0) {
for (let i = 0; i < zoneBorder.length; i++) {
if (data > zoneBorder[i] && data <= zoneBorder[i + 1]) {
data = i
}
}
}
colorZoneDataList.push(data)
})
return colorZoneDataList
}
復制代碼
把顏色取值進行簡化以后,就可以把它們歸類到不同的分組里面去:
export function seperateListToColorZone (simplifiedDataList: number[]) {
const zonedList: string[] = []
let tempZone: number[] = []
simplifiedDataList.forEach((data, index) => {
if ((index + 1) % 4 !== 0) {
tempZone.push(data)
} else {
zonedList.push(JSON.stringify(tempZone))
tempZone = []
}
})
return zonedList
}
復制代碼
最后只需要統計每個相同的分組的總數即可:
export function getFingerprint (zonedList: string[], zoneAmount: number = 16) {
const colorSeperateMap: {
[key: string]: number
} = {}
for (let i = 0; i < zoneAmount; i++) {
for (let j = 0; j < zoneAmount; j++) {
for (let k = 0; k < zoneAmount; k++) {
colorSeperateMap[JSON.stringify([i, j, k])] = 0
}
}
}
zonedList.forEach(zone => {
colorSeperateMap[zone]++
})
return Object.values(colorSeperateMap)
}
復制代碼
”內容特征法“是指把圖片轉化為灰度圖后再轉化為”二值圖“,然后根據像素的取值(黑或白)形成指紋后進行比對的方法。這種算法的核心是找到一個“閾值”去生成二值圖。
對于生成灰度圖,有別于在“平均哈希算法”中提到的取 RGB 均值的辦法,在這里我們使用加權的方式去實現。為什么要這么做呢?這里涉及到顏色學的一些概念。
具體可以參考這篇《Grayscale to RGB Conversion》,下面簡單梳理一下。
采用 RGB 均值的灰度圖是最簡單的一種辦法,但是它忽略了紅、綠、藍三種顏色的波長以及對整體圖像的影響。以下面圖為示例,如果直接取得 RGB 的均值作為灰度,那么處理后的灰度圖整體來說會偏暗,對后續生成二值圖會產生較大的干擾。
那么怎么改善這種情況呢?答案就是為 RGB 三種顏色添加不同的權重。鑒于紅光有著更長的波長,而綠光波長更短且對視覺的刺激相對更小,所以我們要有意地減小紅光的權重而提升綠光的權重。經過統計,比較好的權重配比是 R:G:B = 0.299:0.587:0.114。
于是我們可以得到灰度處理函數:
enum GrayscaleWeight {
R = .299,
G = .587,
B = .114
}
function toGray (imgData: ImageData) {
const grayData = []
const data = imgData.data
for (let i = 0; i < data.length; i += 4) {
const gray = ~~(data[i] * GrayscaleWeight.R + data[i + 1] * GrayscaleWeight.G + data[i + 2] * GrayscaleWeight.B)
data[i] = data[i + 1] = data[i + 2] = gray
grayData.push(gray)
}
return grayData
}
復制代碼
上述函數返回一個 grayData 數組,里面每個元素代表一個像素的灰度值(因為 RBG 取值相同,所以只需要一個值即可)。接下來則使用“大津法”(Otsu's method)去計算二值圖的閾值。關于“大津法”,阮大的文章已經說得很詳細,在這里就不展開了。我在這個地方找到了“大津法”的 Java 實現,后來稍作修改,把它改為了 js 版本:
/ OTSU algorithm
// rewrite from http://www.labbookpages.co.uk/software/imgProc/otsuThreshold.html
export function OTSUAlgorithm (imgData: ImageData) {
const grayData = toGray(imgData)
let ptr = 0
let histData = Array(256).fill(0)
let total = grayData.length
while (ptr < total) {
let h = 0xFF & grayData[ptr++]
histData[h]++
}
let sum = 0
for (let i = 0; i < 256; i++) {
sum += i * histData[i]
}
let wB = 0
let wF = 0
let sumB = 0
let varMax = 0
let threshold = 0
for (let t = 0; t < 256; t++) {
wB += histData[t]
if (wB === 0) continue
wF = total - wB
if (wF === 0) break
sumB += t * histData[t]
let mB = sumB / wB
let mF = (sum - sumB) / wF
let varBetween = wB * wF * (mB - mF) ** 2
if (varBetween > varMax) {
varMax = varBetween
threshold = t
}
}
return threshold
}
復制代碼
OTSUAlgorithm() 函數接收一個 ImageData 對象,經過上一步的 toGray() 方法獲取到灰度值列表以后,根據“大津法”算出最佳閾值然后返回。接下來使用這個閾值對原圖進行處理,即可獲取二值圖。
export function binaryzation (imgData: ImageData, threshold: number) {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
const imgWidth = Math.sqrt(imgData.data.length / 4)
const newImageData = ctx?.createImageData(imgWidth, imgWidth) as ImageData
for (let i = 0; i < imgData.data.length; i += 4) {
let R = imgData.data[i]
let G = imgData.data[i + 1]
let B = imgData.data[i + 2]
let Alpha = imgData.data[i + 3]
let sum = (R + G + B) / 3
newImageData.data[i] = sum > threshold ? 255 : 0
newImageData.data[i + 1] = sum > threshold ? 255 : 0
newImageData.data[i + 2] = sum > threshold ? 255 : 0
newImageData.data[i + 3] = Alpha
}
return newImageData
}
復制代碼
若圖片大小為 N×N,根據二值圖“非黑即白”的特性,我們便可以得到一個 N×N 的 0-1 矩陣,也就是指紋:
經過不同的方式取得不同類型的圖片指紋(特征)以后,應該怎么去比對呢?這里將介紹三種比對算法,然后分析這幾種算法都適用于哪些情況。
摘一段維基百科關于“漢明距離”的描述:
在信息論中,兩個等長字符串之間的漢明距離(英語:Hamming distance)是兩個字符串對應位置的不同字符的個數。換句話說,它就是將一個字符串變換成另外一個字符串所需要替換的字符個數。
例如:
1011101與1001001之間的漢明距離是2。
2143896與2233796之間的漢明距離是3。
"toned"與"roses"之間的漢明距離是3。
明白了含義以后,我們可以寫出計算漢明距離的方法:
export function hammingDistance (str1: string, str2: string) {
let distance = 0
const str1Arr = str1.split('')
const str2Arr = str2.split('')
str1Arr.forEach((letter, index) => {
if (letter !== str2Arr[index]) {
distance++
}
})
return distance
}
復制代碼
使用這個 hammingDistance() 方法,來驗證下維基百科上的例子:
驗證結果符合預期。
知道了漢明距離,也就可以知道兩個等長字符串之間的相似度了(漢明距離越小,相似度越大):
相似度 = (字符串長度 - 漢明距離) / 字符串長度
復制代碼
從維基百科中我們可以了解到關于余弦相似度的定義:
余弦相似性通過測量兩個向量的夾角的余弦值來度量它們之間的相似性。0度角的余弦值是1,而其他任何角度的余弦值都不大于1;并且其最小值是-1。從而兩個向量之間的角度的余弦值確定兩個向量是否大致指向相同的方向。兩個向量有相同的指向時,余弦相似度的值為1;兩個向量夾角為90°時,余弦相似度的值為0;兩個向量指向完全相反的方向時,余弦相似度的值為-1。這結果是與向量的長度無關的,僅僅與向量的指向方向相關。余弦相似度通常用于正空間,因此給出的值為0到1之間。
注意這上下界對任何維度的向量空間中都適用,而且余弦相似性最常用于高維正空間。
余弦相似度可以計算出兩個向量之間的夾角,從而很直觀地表示兩個向量在方向上是否相似,這對于計算兩個 N×N 的 0-1 矩陣的相似度來說非常有用。根據余弦相似度的公式,我們可以把它的 js 實現寫出來:
export function cosineSimilarity (sampleFingerprint: number[], targetFingerprint: number[]) {
// cosθ = ∑n, i=1(Ai × Bi) / (√∑n, i=1(Ai)^2) × (√∑n, i=1(Bi)^2) = A · B / |A| × |B|
const length = sampleFingerprint.length
let innerProduct = 0
for (let i = 0; i < length; i++) {
innerProduct += sampleFingerprint[i] * targetFingerprint[i]
}
let vecA = 0
let vecB = 0
for (let i = 0; i < length; i++) {
vecA += sampleFingerprint[i] ** 2
vecB += targetFingerprint[i] ** 2
}
const outerProduct = Math.sqrt(vecA) * Math.sqrt(vecB)
return innerProduct / outerProduct
}
復制代碼
明白了“漢明距離”和“余弦相似度”這兩種特征比對算法以后,我們就要去看看它們分別適用于哪些特征提取算法的場景。
首先來看“顏色分布法”。在“顏色分布法”里面,我們把一張圖的顏色進行區間劃分,通過統計不同顏色區間的數量來獲取特征,那么這里的特征值就和“數量”有關,也就是非 0-1 矩陣。
顯然,要比較兩個“顏色分布法”特征的相似度,“漢明距離”是不適用的,只能通過“余弦相似度”來進行計算。
接下來看“平均哈希算法”和“內容特征法”。從結果來說,這兩種特征提取算法都能獲得一個 N×N 的 0-1 矩陣,且矩陣內元素的值和“數量”無關,只有 0-1 之分。所以它們同時適用于通過“漢明距離”和“余弦相似度”來計算相似度。
明白了如何提取圖片的特征以及如何進行比對以后,最重要的就是要了解它們對于相似度的計算精度。
本文所講的相似度僅僅是通過客觀的算法來實現,而判斷兩張圖片“像不像”卻是一個很主觀的問題。于是我寫了一個簡單的服務,可以自行把兩張圖按照不同的算法和精度去計算相似度:
img-compare.netlify.com/
經過對不同素材的多方比對,我得出了下列幾個非常主觀的結論。
總結一下,三種特征提取算法和兩種特征比對算法各有優劣,在實際應用中應該針對不同的情況靈活選用。
本文是在拜讀阮一峰的兩篇《相似圖片搜索的原理》之后,經過自己的實踐總結以后而成。由于對色彩、數學等領域的了解只停留在淺顯的層面,文章難免有謬誤之處,如果有發現表述得不正確的地方,歡迎留言指出,我會及時予以更正。
原鏈接:https://juejin.im/post/5dedf50d518825121b4364ec
用 CSS 最困難的部分之一是處理CSS的權重值,它可以決定到底哪條規則會最終被應用,尤其是如果你想在 Bootstrap 這樣的框架中覆蓋其已有樣式,更加顯得麻煩。不過隨著 CSS 層的引入,這一切都發生了變化。 這個新功能允許您創建自己的自定義 CSS 層,這是有史以來第一次確定所有 CSS 代碼權重的層次結構。 在本文中,我將剖析這對您意味著什么,它是如何工作的,以及您今天如何開始使用它。
什么是層(Layers)
創建您自己的自定義圖層是 CSS 的新功能,但圖層從一開始就存在于 CSS 中。 CSS 中有 3 個不同的層來管理所有樣式的工作方式。
瀏覽器(也稱為用戶代理)樣式 - user agent style
用戶樣式 - User Styles
作者樣式 - Author Styles
瀏覽器樣式是應用于瀏覽器的默認樣式。這就是為什么 Chrome 和 Safari 中的按鈕看起來不同的原因。在瀏覽器層中找到的樣式在瀏覽器之間是不同的,并且給每個瀏覽器一個獨特的外觀。
下一層是用戶樣式,這并不是您真正需要擔心的事情。這些通常是用戶可以編寫并注入瀏覽器的自定義樣式,但瀏覽器不再真正支持這些樣式。用戶可能會更改一些瀏覽器設置,這些設置會向該圖層添加樣式,但在大多數情況下,可以完全忽略該層。
最后,我們來到作者層。這是您最熟悉的層,因為您編寫的每一段 CSS 代碼都屬于這一層。
這些層分開的原因是因為它可以很容易地覆蓋瀏覽器樣式和用戶樣式中定義的代碼,因為層定義了自己的層次結構,完全忽略了權重的影響。
這 3 個 CSS 層是有序的(瀏覽器樣式、用戶樣式、然后是作者樣式),后面層中的每個樣式都將覆蓋前一層的任何樣式。這意味著即使瀏覽器樣式定義了一個超級特定的選擇器,例如#button.btn.super-specific,并且您的作者樣式定義了一個超級通用的選擇器,例如按鈕,您的作者樣式仍然會覆蓋瀏覽器樣式。
這實際上已經是您可能一直在使用而沒有意識到的東西。
* {
box-sizing: border-box;
}
上面的選擇器沒有權重,因為 * 符號對權重沒有貢獻。 這意味著例如使用 p 作為選擇器的 p 標簽的瀏覽器樣式在技術上比 * 選擇器更具體,權重更高。 但是,這一切并不重要,因為作者樣式位于比瀏覽器樣式層晚的層中,因此您的代碼將始終覆蓋瀏覽器樣式。
理解這一點至關重要,因為使用這個新的圖層 API,您可以在作者圖層中創建自己的圖層,從而更輕松地處理特定性。
如何創建你自己的層
下面來看個例子:
很明顯,這是我們正常理解的CSS, ID設置的顏色權重更高,所以按鈕顯示為紅色。讓我們使用@layer給它們加上兩個層,看看是什么效果:
按鈕變成藍色。為什么會這樣?
我們給兩條CSS分別建立了base和utilities層,很明顯,后面創建的層的樣式覆蓋了前面層的樣式,盡管前面層的樣式有更高的權重。這就是層的默認工作原理。當然層的順序是可以指定的,
@layer utilities, base;
@layer utilities, base;
您需要做的就是編寫@layer 關鍵字,后跟以逗號分隔的層列表。 這將按從左到右的順序定義所有層,其中列出的第一層到最后一層的權重是依次增加的。 然后,您可以稍后使用普通的@layer 語法向每個層添加代碼,而不必擔心定義層的順序,因為它們都在這一行中定義。 需要注意的是,這行代碼必須在定義任何層之前出現,所以我通常將它作為我的 CSS 文件中的第一行。如上圖,通過指定層的順序,我們讓base層應用在utilities層之后,所以按鈕又顯示為紅色。
導入層
上面這兩種方式都是導入bootstrap框架的CSS,并且把他們放在framework層中,這樣你如果想要覆蓋它已有的樣式,只需要新建一個自己的層,放置在framework層后面就行。像下面這樣。
匿名層
匿名層不常用,但它寫在后面可以覆蓋其他層的樣式,像下面可以把按鈕設為橙色。
不在層里的樣式
不在層里的樣式會有更高的權重,下面這個列表會讓你看得更清楚覆蓋是怎么發生的
層還可以重疊設置,不過很少用。具體的用法可以查閱相關文檔。
瀏覽器支持
自從IE死了以后,所有主流瀏覽器都已支持這一特性。大家請放心使用。
型壓縮可減少受訓神經網絡的冗余,由于幾乎沒有 BERT 或者 BERT-Large 模型可直接在 GPU 及智能手機上應用,因此模型壓縮方法對于 BERT 的未來的應用前景而言,非常有價值。
軟件工程師 Mitchell A. Gordon 在本文中總結了所有的 BERT 壓縮模型的方法,并對該領域的論文進行羅列及分類,我們下面來看:
1、剪枝——即訓練后從網絡中去掉不必要的部分。
這包括權重大小剪枝、注意力頭剪枝、網絡層以及其他部分的剪枝等。還有一些方法也通過在訓練期間采用正則化的方式來提升剪枝能力(layer dropout)。
2、權重因子分解——通過將參數矩陣分解成兩個較小矩陣的乘積來逼近原始參數矩陣。
這給矩陣施加了低秩約束。權重因子分解既可以應用于輸入嵌入層(這節省了大量磁盤內存),也可以應用于前饋/自注意力層的參數(為了提高速度)。
3、知識蒸餾——又名「Student Teacher」。
在預訓練/下游數據上從頭開始訓練一個小得多的 Transformer,正常情況下,這可能會失敗,但是由于未知的原因,利用完整大小的模型中的軟標簽可以改進優化。
一些方法還將BERT 蒸餾成如LSTMS 等其他各種推理速度更快的架構。另外還有一些其他方法不僅在輸出上,還在權重矩陣和隱藏的激活層上對 Teacher 知識進行更深入的挖掘。
4、權重共享——模型中的一些權重與模型中的其他參數共享相同的值。
例如,ALBERT 對 BERT 中的每個自注意力層使用相同的權重矩陣。
5、量化——截斷浮點數,使其僅使用幾個比特(這會導致舍入誤差)。
模型可以在訓練期間,也可以在訓練之后學習量化值。
6、預訓練和下游任務——一些方法僅僅在涉及到特定的下游任務時才壓縮 BERT,也有一些方法以任務無關的方式來壓縮 BERT。
(原英文標題見文章尾部)
三、結果比較
在這里將盡我所能的對這些論文的觀點進行解讀,同時主要關注以下指標:參數縮減,推理加速 1和準確性 2,3。
若需要選一個贏家,我認為是 ALBERT,DistilBERT,MobileBERT,Q-BERT,LayerDrop和RPP。你也可以將其中一些方法疊加使用 4,但是有些剪枝相關的論文,它們的科學性要高于實用性,所以我們不妨也來驗證一番:
四、相關論文和博文推薦
《稀疏 Transformer:通過顯式選擇集中注意力》(Sparse Transformer: Concentrated Attention Through Explicit Selection),論文鏈接:https://openreview.net/forum?id=Hye87grYDH)
《使用四元數網絡進行輕量級和高效的神經自然語言處理》(Lightweight and Efficient Neural Natural Language Processing with Quaternion Networks,論文鏈接:http://arxiv.org/abs/1906.04393)
《自適應稀疏 Transformer》(Adaptively Sparse Transformers,論文鏈接:https://www.semanticscholar.org/paper/f6390beca54411b06f3bde424fb983a451789733)
《壓縮 BERT 以獲得更快的預測結果》(Compressing BERT for Faster Prediction,博文鏈接:https://blog.rasa.com/compressing-bert-for-faster-prediction-2/amp/)
最后的話:
1、請注意,并非所有壓縮方法都能使模型更快。眾所周知,非結構化剪枝很難通過 GPU 并行來加速。其中一篇論文認為,在 Transformers 中,計算時間主要由 Softmax 計算決定,而不是矩陣乘法。
2、如果我們能拿出一個數字來記錄我們真正關心的事情,那將會很棒,就像 F1。
3、其中一些百分比是根據 BERT-Large 而不是 BERT-Base 衡量的,僅供參考。
4、不同的壓縮方法如何交互,是一個開放的研究問題。
相關論文列表:
[1] Compressing BERT: Studying the Effects of Weight Pruning on Transfer Learning
[2] Are Sixteen Heads Really Better than One?
[3] Pruning a BERT-based Question Answering Model
[4] Reducing Transformer Depth on Demand with Structured Dropout
[5] Reweighted Proximal Pruning for Large-Scale Language Representation
[6] Structured Pruning of Large Language Models
[7] ALBERT: A Lite BERT for Self-supervised Learning of Language Representations
[8] Extreme Language Model Compression with Optimal Subwords and Shared Projections
[9] DistilBERT, a distilled version of BERT: smaller, faster, cheaper and lighter
[10] Distilling Task-Specific Knowledge from BERT into Simple Neural Networks
[11] Distilling Transformers into Simple Neural Networks with Unlabeled Transfer Data
[12] Attentive Student Meets Multi-Task Teacher: Improved Knowledge Distillation for Pretrained Models
[13] Patient Knowledge Distillation for BERT Model Compression
[14] TinyBERT: Distilling BERT for Natural Language Understanding
[15] MobileBERT: Task-Agnostic Compression of BERT by Progressive Knowledge Transfer
[16] Q8BERT: Quantized 8Bit BERT
[17] Q-BERT: Hessian Based Ultra Low Precision Quantization of BERT 雷鋒網雷鋒網雷鋒網
Via http://mitchgordon.me/machine/learning/2019/11/18/all-the-ways-to-compress-BERT.html
*請認真填寫需求信息,我們會在24小時內與您取得聯系。