1. 引言:為什么需要遷移學習?
在深度學習爆發的這十年里,我們見證了模型性能的飛速提升 ——ResNet 在圖像分類上突破人類視覺極限,BERT 在 NLP 任務上刷新基準,GPT 系列更是開啟了大語言模型時代。但這些亮眼成果的背后,隱藏著兩個核心前提:大規模標注數據和充足的計算資源。
現實場景中,這兩個前提往往難以滿足:
- 醫學影像任務中,一張肺結節標注圖需要放射科醫生花費數小時審核,數據集規模通常只有數千張;
- 工業缺陷檢測任務中,某類罕見缺陷的樣本可能只有幾十甚至十幾張,無法支撐深度學習模型的訓練;
- 小語種 NLP 任務(如維吾爾語情感分析),標注數據稀缺,從零訓練模型性能極差;
- 邊緣設備部署場景中,計算資源有限,無法承擔從零訓練一個百萬參數模型的成本。
傳統深度學習的本質是 “孤立學習”—— 每個任務都需要獨立的數據集和訓練過程,模型無法復用已學知識。而人類則擅長 “舉一反三”:學會識別貓之后,再學識別狗會更容易;掌握英語語法后,學習法語會更快。遷移學習(Transfer Learning, TL)?正是模仿人類這種學習方式的技術,它將從 “源域任務” 中學到的知識,遷移到 “目標域任務” 中,從而解決目標域數據稀缺、計算資源不足的問題。
本文將從理論到實踐,系統講解遷移學習的核心概念、分類體系、常用方法,并通過兩個完整的 PyTorch 代碼實例(計算機視覺 + 自然語言處理)幫助讀者落地,最后探討遷移學習的挑戰與未來方向。無論你是深度學習初學者,還是需要解決實際問題的算法工程師,都能從本文中獲得啟發。
2. 遷移學習核心概念解析
在深入遷移學習的方法前,我們必須先理清幾個核心概念 —— 這些概念是理解所有遷移學習技術的基礎,也是避免混淆的關鍵。
2.1 什么是遷移學習?
遷移學習的官方定義可概括為: 利用源域(Source Domain)和源任務(Source Task)的知識,來提升目標域(Target Domain)和目標任務(Target Task)的學習性能。
簡單來說,就是 “借雞生蛋”:用已有的、數據充足的任務(如 ImageNet 圖像分類)的訓練成果,幫助數據稀缺的新任務(如寵物狗品種分類)提升效果。
需要注意的是,遷移學習的核心前提是源域與目標域 / 任務存在 “相關性”。如果源任務是 “識別汽車”,目標任務是 “識別詩歌情感”,兩者毫無關聯,遷移不僅無效,還可能產生負面影響(即 “負遷移”)。
2.2 遷移學習 vs 傳統機器學習:關鍵差異
為了更清晰地理解遷移學習,我們對比傳統機器學習與遷移學習的核心差異:
對比維度 | 傳統機器學習 | 遷移學習 |
---|---|---|
數據假設 | 訓練數據(源域)與測試數據(目標域)同分布 | 源域與目標域可不同分布 |
任務獨立性 | 每個任務獨立訓練,無知識復用 | 復用源任務知識,輔助目標任務訓練 |
數據依賴 | 依賴大規模標注數據 | 可在目標域數據稀缺時工作 |
泛化能力 | 僅對同分布數據泛化好 | 對不同分布數據的泛化能力更強 |
典型場景 | ImageNet 分類、MNIST 手寫體識別 | 醫學影像檢測、小語種文本分類 |
2.3 核心術語定義:領域(Domain)與任務(Task)
遷移學習中,“領域” 和 “任務” 是兩個最基礎的概念,所有遷移場景都圍繞這兩個概念的關系展開。
2.3.1 領域(Domain):數據的 “來源地”
領域定義了數據的分布特征,通常表示為?\(D = \{X, P(X)\}\),其中:
- X:特征空間(Feature Space),即數據的表示維度。例如,圖像任務中X是像素矩陣(如\(224 \times 224 \times 3\)),文本任務中X是詞向量(如768維 BERT 嵌入);
- \(P(X)\):邊緣概率分布(Marginal Probability Distribution),即特征在空間中的分布規律。例如,“白天的貓圖像” 和 “夜晚的貓圖像” 屬于不同領域 —— 前者像素亮度高,后者亮度低,即\(P(X)\)不同。
當兩個領域的X或\(P(X)\)不同時,我們稱它們為 “不同領域”。例如:
- 源域:ImageNet 中的自然圖像(X為\(224 \times 224 \times 3\),\(P(X)\)為自然場景分布);
- 目標域:醫院的肺結節 CT 影像(X為\(512 \times 512 \times 1\),\(P(X)\)為醫學影像分布)。
2.3.2 任務(Task):模型的 “目標”
任務定義了模型需要解決的問題,通常表示為?\(T = \{Y, f(\cdot)\}\),其中:
- Y:標簽空間(Label Space),即模型輸出的類別集合。例如,二分類任務中\(Y = \{0, 1\}\),1000 類分類任務中\(Y = \{0, 1, ..., 999\}\);
- \(f(\cdot)\):目標預測函數(Target Prediction Function),即模型需要學習的映射關系(\(f: X \rightarrow Y\))。例如,情感分析任務中\(f(\cdot)\)是 “文本→積極 / 消極” 的映射,目標檢測任務中\(f(\cdot)\)是 “圖像→邊界框 + 類別” 的映射。
當兩個任務的Y或\(f(\cdot)\)不同時,我們稱它們為 “不同任務”。例如:
- 源任務:ImageNet 1000 類分類(Y為 1000 個自然物體類別,\(f(\cdot)\)是 “圖像→物體類別”);
- 目標任務:肺結節檢測(Y為 “結節 / 非結節”,\(f(\cdot)\)是 “CT 影像→結節位置 + 類別”)。
2.4 數據分布差異:遷移學習的 “攔路虎”
遷移學習的核心挑戰是源域與目標域的分布差異—— 如果分布完全相同,直接用傳統機器學習即可,無需遷移。根據分布差異的類型,可分為三類:
2.4.1 協變量偏移(Covariate Shift)
定義:特征空間X相同,但邊緣概率分布\(P(X)\)不同;條件概率分布\(P(Y|X)\)相同(即 “輸入變了,但輸入到輸出的映射不變”)。 例子:
- 源域:白天拍攝的貓圖像(\(P(X)\)中亮度高的樣本占比高);
- 目標域:夜晚拍攝的貓圖像(\(P(X)\)中亮度低的樣本占比高);
- 任務:貓的二分類(\(P(Y|X)\)不變 —— 無論白天黑夜,貓的特征到 “貓” 標簽的映射相同)。
這是最常見的分布差異,微調(Fine-tuning)即可有效解決。
2.4.2 標簽偏移(Label Shift)
定義:特征空間X相同,條件概率分布\(P(X|Y)\)相同,但標簽邊緣分布\(P(Y)\)不同(即 “輸入到標簽的映射不變,但標簽的比例變了”)。 例子:
- 源域:垃圾郵件分類訓練集(\(P(Y)\)中垃圾郵件占 30%,正常郵件占 70%);
- 目標域:垃圾郵件分類測試集(\(P(Y)\)中垃圾郵件占 60%,正常郵件占 40%);
- 任務:垃圾郵件二分類(\(P(X|Y)\)不變 —— 垃圾郵件的文本特征(如 “免費”“中獎”)與正常郵件的特征映射不變)。
標簽偏移常見于數據收集偏差場景,可通過調整樣本權重(如對少數類樣本加權)解決。
2.4.3 概念偏移(Concept Shift)
定義:特征空間X相同,但條件概率分布\(P(Y|X)\)不同(即 “輸入到標簽的映射變了”)。 例子:
- 源域:2010 年的 “優質用戶” 分類(\(P(Y|X)\)中 “消費額> 1000 元” 為優質用戶);
- 目標域:2024 年的 “優質用戶” 分類(\(P(Y|X)\)中 “月活躍度> 20 次” 為優質用戶);
- 任務:優質用戶二分類(\(P(Y|X)\)變了 —— 輸入特征 “消費額” 到標簽 “優質用戶” 的映射改變)。
概念偏移是最棘手的差異,通常需要重新標注數據或動態調整模型。
3. 遷移學習的分類體系
遷移學習的應用場景多樣,為了更好地選擇方法,學術界通常從 “任務關系”“遷移內容”“分布差異” 三個維度對其分類。
3.1 按學習目標與任務關系分類
該分類基于 “源任務與目標任務是否相同”“目標域是否有標簽”,是最常用的分類方式。
3.1.1 歸納式遷移學習(Inductive Transfer Learning)
核心特征:源任務與目標任務不同(\(T_S \neq T_T\)),目標域有標簽(\(Y_T \neq \emptyset\))。 本質:通過源任務學習 “通用規律”,輔助目標任務的歸納學習。 例子:
- 源任務:ImageNet 1000 類分類(學習通用圖像特征);
- 目標任務:寵物狗 100 品種分類(利用通用圖像特征,提升狗品種分類精度);
- 邏輯:識別狗品種需要的 “邊緣、紋理、形狀” 等特征,在 ImageNet 分類中已被充分學習,無需從零訓練。
適用場景:目標任務有少量標注數據,且與源任務存在 “特征復用性”(如所有圖像任務都需要邊緣檢測特征)。
3.1.2 演繹式遷移學習(Transductive Transfer Learning)
核心特征:源任務與目標任務相同(\(T_S = T_T\)),源域有標簽(\(Y_S \neq \emptyset\)),目標域無標簽(\(Y_T = \emptyset\))。 本質:利用源域的標簽信息,解決目標域的無監督學習問題(也稱為 “半監督遷移學習”)。 例子:
- 源域:有標簽的英語情感分析數據(\(Y_S = \{積極, 消極\}\));
- 目標域:無標簽的中文情感分析數據(\(Y_T = \emptyset\));
- 任務:情感二分類(\(T_S = T_T\));
- 邏輯:英語和中文的情感表達有共性(如 “開心” 對應 “happy”),利用英語數據學習情感特征,輔助中文無標簽數據的分類。
適用場景:目標域無標注數據,但與源域任務完全一致(如跨語言、跨場景的相同任務)。
3.1.3 無監督遷移學習(Unsupervised Transfer Learning)
核心特征:源任務與目標任務不同(\(T_S \neq T_T\)),目標域無標簽(\(Y_T = \emptyset\))。 本質:從源域無監督學習 “結構特征”,遷移到目標域的無監督任務中。 例子:
- 源域:無標簽的自然圖像(學習圖像的邊緣、紋理等結構特征);
- 目標域:無標簽的醫學 CT 影像(利用自然圖像的結構特征,輔助 CT 影像的聚類或分割);
- 任務:源任務是自然圖像聚類,目標任務是 CT 影像聚類(\(T_S \neq T_T\))。
適用場景:目標域完全無標簽,且與源域共享 “低層次結構特征”(如圖像的邊緣、文本的語法結構)。
3.2 按遷移內容分類
該分類基于 “從源域遷移什么類型的知識”,直接對應具體的技術實現。
3.2.1 參數遷移(Parameter Transfer)
核心思想:源域訓練的模型參數(或部分參數)可作為目標域模型的初始化參數,避免從零訓練。 本質:遷移模型的 “參數級知識”—— 假設源域模型的部分參數(如卷積層)對目標域任務同樣有效。 典型方法:微調(Fine-tuning)、模型蒸餾(Model Distillation)。 例子:
- 源域:用 ImageNet 訓練 ResNet-50,得到卷積層參數(負責提取邊緣、紋理);
- 目標域:將 ResNet-50 的卷積層參數凍結,僅訓練全連接層(適配目標任務的類別),或微調所有參數(讓卷積層適應目標域特征)。
適用場景:源域與目標域的模型結構相似(如都是圖像分類模型),且低層次特征可復用。
3.2.2 特征遷移(Feature Transfer)
核心思想:將源域和目標域的特征映射到一個 “共享特征空間”,使兩者在該空間中的分布差異最小化,再用共享特征訓練目標任務模型。 本質:遷移 “特征級知識”—— 不直接遷移參數,而是遷移 “特征表示能力”。 典型方法:領域自適應網絡(DANN)、對比學習(Contrastive Learning)。 例子:
- 源域:有標簽的自然圖像,目標域:無標簽的醫學影像;
- 訓練一個特征提取器,將自然圖像和醫學影像映射到同一空間,使兩者的分布盡可能接近;
- 用源域的標簽訓練分類器,再用該分類器對目標域的映射特征進行預測。
適用場景:源域與目標域的模型結構不同,但特征可通過映射對齊(如跨模態遷移:文本→圖像)。
3.2.3 實例遷移(Instance Transfer)
核心思想:從源域中篩選出與目標域 “相似” 的樣本,賦予高權重,用于目標域模型的訓練。 本質:遷移 “樣本級知識”—— 假設源域中部分樣本與目標域樣本分布接近,可作為目標域的 “補充數據”。 典型方法:加權樣本訓練(如基于 KNN 的權重計算)、樣本選擇算法。 例子:
- 源域:10 萬張普通汽車圖像,目標域:100 張新能源汽車圖像;
- 計算源域樣本與目標域樣本的相似度(如余弦距離),篩選出 1000 張最相似的普通汽車圖像;
- 用這 1000 張高權重樣本 + 100 張目標域樣本訓練新能源汽車分類模型。
適用場景:源域樣本量大,但僅部分樣本與目標域相關(如小樣本目標檢測)。
3.2.4 關系知識遷移(Relational Knowledge Transfer)
核心思想:遷移源域中樣本之間的 “關聯關系”,而非單個樣本或參數。 本質:遷移 “結構級知識”—— 假設源域和目標域的樣本間存在相似的關聯模式。 典型方法:圖譜遷移(Knowledge Graph Transfer)、關系網絡(Relational Networks)。 例子:
- 源域:知識圖譜 “人 - 購買 - 商品”(學習 “用戶 - 行為 - 物品” 的關聯模式);
- 目標域:推薦系統 “用戶 - 點擊 - 視頻”(遷移 “用戶 - 行為 - 物品” 的關聯模式,提升推薦精度)。
適用場景:源域與目標域的樣本關聯模式相似(如推薦系統、知識圖譜)。
3.3 按領域與任務分布分類
該分類基于 “源域與目標域的分布差異程度”,聚焦于解決 “領域自適應” 問題。
3.3.1 領域自適應(Domain Adaptation, DA)
核心特征:僅存在一個源域和一個目標域,目標是縮小兩者的分布差異。 分類:
- 監督 DA:目標域有少量標簽;
- 半監督 DA:目標域有部分標簽;
- 無監督 DA:目標域無標簽(最常見)。
例子:將 “白天的交通場景圖像”(源域)的模型,自適應到 “夜晚的交通場景圖像”(目標域)。
3.3.2 領域泛化(Domain Generalization, DG)
核心特征:存在多個源域,目標是訓練一個 “泛化性強” 的模型,使其能直接應用于未見過的目標域(無需目標域數據)。 本質:從多個源域中學習 “領域不變特征”,應對未知目標域的分布差異。 例子:用 “白天、陰天、雨天” 三個源域的交通圖像訓練模型,使其能直接應用于 “霧天”(未見過的目標域)的交通場景檢測。
適用場景:目標域數據完全不可得(如邊緣設備部署,無法獲取目標場景數據)。
4. 常用遷移學習方法深度解析
了解分類后,我們聚焦于工業界最常用的 4 類方法,深入講解其原理、實現細節與適用場景。
4.1 參數遷移:站在預訓練模型的肩膀上
參數遷移是最直觀、最常用的遷移學習方法,核心是 “復用預訓練模型的參數”。其中,微調(Fine-tuning)?是參數遷移的代表,幾乎所有計算機視覺和 NLP 任務都會用到。
4.1.1 微調(Fine-tuning):原理與策略
原理:
- 預訓練階段:在大規模源域數據集(如 ImageNet、Wikipedia)上訓練一個基礎模型(如 ResNet、BERT),學習通用知識;
- 適配階段:將預訓練模型的輸出層替換為適配目標任務的層(如將 ResNet 的 1000 類輸出改為 10 類);
- 微調階段:用目標域數據集訓練整個模型(或部分層),使模型參數適應目標任務。
為什么微調有效? 預訓練模型在大規模數據上學習到了 “通用特征”:
- 計算機視覺中,底層卷積層學習邊緣、紋理,中層學習部件(如眼睛、耳朵),高層學習整體特征(如貓、狗);
- NLP 中,BERT 的底層學習詞法、語法,高層學習語義、上下文關聯。
這些通用特征對相似任務(如從 ImageNet 分類到寵物分類)具有極強的復用性,微調只需少量數據即可調整參數,適配目標任務。
4.1.2 凍結層(Layer Freezing):為什么要凍結?如何凍結?
微調時,我們通常不會直接訓練所有層,而是凍結部分底層,僅訓練上層或輸出層。原因如下:
- 底層學習的是通用特征(如邊緣、紋理),對所有圖像任務都有效,無需修改;
- 高層學習的是源域特定特征(如 ImageNet 中的 “飛機、船”),需要調整以適配目標任務(如 “貓、狗”);
- 凍結底層可減少參數數量,降低過擬合風險(尤其目標域數據少時)。
凍結策略:
- 全凍結底層:僅訓練輸出層。適用于目標域數據極少(如幾百張),且源域與目標域相似性高(如從 “動物分類” 到 “貓品種分類”);
- 部分凍結:凍結前 k 層,訓練剩余層。例如,ResNet-50 有 49 個卷積層 + 1 個全連接層,可凍結前 30 層,訓練后 20 層;
- 漸進式解凍:先凍結所有底層,訓練輸出層;再解凍部分中層,聯合訓練;最后解凍所有層,用小學習率微調。適用于目標域數據中等(如幾千張),且相似性一般的場景。
經驗法則:目標域數據越少、與源域越相似,凍結的層數越多;反之,凍結層數越少。
4.1.3 部分參數遷移:聚焦任務相關層
當源域與目標域的模型結構不同時(如源域是分類模型,目標域是檢測模型),無法直接微調所有參數,此時可采用部分參數遷移:
- 提取預訓練模型的 “任務無關層”(如 ResNet 的卷積層),作為目標模型的特征提取器;
- 目標模型的 “任務相關層”(如檢測模型的邊界框回歸層)從零訓練。
例如,目標檢測模型 Faster R-CNN 的 backbone 通常采用預訓練的 ResNet—— 將 ResNet 的卷積層作為特征提取器(遷移參數),RPN 層和 RoI Head 層(任務相關層)從零訓練。
4.2 特征遷移:學習領域無關的通用特征
當源域與目標域的分布差異較大(如自然圖像 vs 醫學影像),直接微調效果不佳時,需要通過特征遷移將兩者的特征對齊到同一空間,縮小分布差異。其中,領域自適應網絡(Domain-Adversarial Neural Network, DANN)?是最經典的方法。
4.2.1 DANN:對抗訓練的魔力
DANN 由紐約大學 Yann LeCun 團隊提出,核心思想源于 GAN(生成對抗網絡),通過 “特征提取器” 與 “領域判別器” 的對抗訓練,學習領域無關的特征。
網絡結構:DANN 包含三個核心模塊(如圖 1 所示):
- 特征提取器(Feature Extractor, G):輸入源域或目標域數據,輸出特征向量。目標是讓特征既能被標簽預測器正確分類,又能欺騙領域判別器;
- 標簽預測器(Label Predictor, F):輸入特征向量,預測源域數據的標簽。目標是最小化源域數據的分類損失(確保特征有任務區分度);
- 領域判別器(Domain Discriminator, D):輸入特征向量,判斷特征來自源域還是目標域。目標是最大化領域分類損失(準確區分領域);而特征提取器 G 的目標是最小化領域分類損失(欺騙 D)。
訓練過程:
- 固定 D,訓練 G 和 F:最小化源域數據的分類損失(讓 F 能正確預測標簽),同時最小化 D 的領域分類損失(讓 G 生成的特征無法被 D 區分領域);
- 固定 G,訓練 D:最大化 D 的領域分類損失(讓 D 能區分 G 生成的特征來自源域還是目標域);
- 交替訓練,直到收斂。此時,G 生成的特征是 “領域無關且任務相關” 的,可直接用于目標域任務。
4.2.2 特征對齊(Feature Alignment):縮小領域差距
特征對齊是特征遷移的核心目標,除了 DANN 的對抗對齊,還有以下常用方法:
- 統計對齊:通過最小化源域和目標域特征的統計差異(如均值、方差、最大均值差異 MMD),實現特征對齊。例如,MMD 通過計算兩個分布的核函數距離,最小化該距離以對齊特征;
- 對比對齊:通過對比學習,讓源域和目標域的相似樣本在特征空間中靠近,不同樣本遠離。例如,SimCLR 通過數據增強生成正樣本對,最小化正樣本對的距離,最大化負樣本對的距離;
- 自監督對齊:利用目標域的無標簽數據進行自監督學習(如掩碼圖像建模 MAE),讓目標域特征與源域特征的表示方式一致。
4.3 實例遷移:給相似樣本 “加權投票”
當源域樣本量大,但僅部分樣本與目標域相關時,實例遷移是最優選擇。其核心是 “篩選相似樣本,加權訓練”。
4.3.1 樣本權重計算:距離與密度的考量
實例遷移的關鍵是計算源域樣本與目標域樣本的 “相似度”,并賦予相似樣本更高的權重。常用的權重計算方法:
- 基于距離的權重:計算源域樣本xiS?與目標域樣本xjT?的距離(如歐氏距離、余弦距離),距離越小,權重越大。公式如下:
wi?=1+dist(xiS?,xˉT)1?,其中xˉT是目標域樣本的均值; - 基于密度的權重:如果源域樣本xiS?位于目標域樣本的 “高密度區域”(即周圍有很多目標域樣本),則權重更大。可通過 KNN 或核密度估計(KDE)計算;
- 基于模型的權重:用目標域少量標簽訓練一個初步模型,用該模型預測源域樣本的置信度,置信度高的樣本(即模型認為與目標域相似的樣本)權重更大。
4.3.2 實例遷移的適用場景與局限
適用場景:
- 源域樣本量大,但存在大量噪聲或無關樣本(如網絡爬取的圖像數據);
- 目標域樣本極少(如幾十張),需要補充相似樣本以避免過擬合。
局限:
- 若源域與目標域的分布差異過大,可能篩選不出相似樣本,遷移無效;
- 若源域中存在 “偽相似樣本”(表面相似但標簽不同),會導致負遷移。
4.4 關系知識遷移:遷移 “關聯模式”
關系知識遷移適用于 “樣本間關聯模式相似” 的場景,如推薦系統、知識圖譜、邏輯推理任務。其核心是遷移 “關系結構”,而非單個樣本或參數。
4.4.1 關系知識的表示與遷移
關系知識通常用 “圖結構” 表示,例如:
- 推薦系統中,“用戶 - 物品 - 評分” 構成 bipartite 圖,關系是 “用戶對物品的偏好”;
- 知識圖譜中,“實體 - 關系 - 實體” 構成三元組,關系是 “實體間的語義關聯”。
遷移方法通常分為兩步:
- 關系建模:在源域中訓練一個關系模型(如圖神經網絡 GNN),學習樣本間的關聯模式;
- 關系遷移:將源域的關系模型參數(或關系嵌入)作為目標域關系模型的初始化,或直接復用關系推理規則。
4.4.2 跨任務關系遷移案例
以推薦系統的跨領域遷移為例:
- 源域:“用戶 - 電影 - 評分” 數據,學習用戶的觀影偏好關系(如喜歡科幻電影的用戶也喜歡動作電影);
- 目標域:“用戶 - 書籍 - 評分” 數據,遷移源域的用戶偏好關系(如喜歡科幻電影的用戶可能喜歡科幻書籍);
- 實現:用源域數據訓練一個 GNN 模型,學習用戶和物品的嵌入;將用戶嵌入遷移到目標域,作為書籍推薦的初始嵌入,提升推薦精度。
5. 遷移學習實踐:PyTorch 代碼實例
理論講完后,我們通過兩個完整的代碼實例,分別演示計算機視覺(圖像分類)?和自然語言處理(文本分類)?中的遷移學習應用。這兩個案例覆蓋了工業界最常見的遷移場景,代碼可直接運行。
5.1 案例 1:計算機視覺 ——CIFAR-10 圖像分類(ResNet 微調)
5.1.1 實驗背景與目標
- 源域:ImageNet 數據集(130 萬張圖像,1000 類),預訓練模型為 ResNet-18;
- 目標域:CIFAR-10 數據集(5 萬張訓練圖,1 萬張測試圖,10 類:飛機、汽車、鳥、貓、鹿、狗、青蛙、馬、船、卡車);
- 任務:CIFAR-10 圖像分類,對比 “從零訓練 ResNet-18” 和 “微調預訓練 ResNet-18” 的性能差異;
- 實驗目標:驗證微調在數據有限場景下的優勢(我們將 CIFAR-10 訓練集抽樣至 10%,模擬小樣本場景)。
5.1.2 實驗環境準備
需安裝以下庫:
bash
pip install torch torchvision matplotlib numpy scikit-learn
5.1.3 數據加載與預處理
CIFAR-10 數據集可通過torchvision.datasets
直接下載,預處理需與預訓練 ResNet-18 的輸入要求一致(ImageNet 的預處理方式):
python
運行
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, Subset
from torchvision import datasets, transforms
from torchvision.models import resnet18
import matplotlib.pyplot as plt
import numpy as np
from sklearn.model_selection import train_test_split# 1. 配置超參數
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
BATCH_SIZE = 64
EPOCHS = 20
LEARNING_RATE = 1e-4 # 微調學習率通常較小
NUM_CLASSES = 10 # CIFAR-10有10類
SAMPLE_RATIO = 0.1 # 抽樣10%的訓練集,模擬小樣本場景# 2. 數據預處理(與ImageNet預訓練一致)
# 訓練集:隨機裁剪、水平翻轉、歸一化
train_transform = transforms.Compose([transforms.RandomResizedCrop(224), # ResNet-18輸入尺寸為224x224transforms.RandomHorizontalFlip(),transforms.ToTensor(),transforms.Normalize(mean=[0.485, 0.456, 0.406], # ImageNet均值std=[0.229, 0.224, 0.225]) # ImageNet標準差
])# 測試集:僅 resize 和歸一化
test_transform = transforms.Compose([transforms.Resize(256),transforms.CenterCrop(224),transforms.ToTensor(),transforms.Normalize(mean=[0.485, 0.456, 0.406],std=[0.229, 0.224, 0.225])
])# 3. 加載CIFAR-10數據集
full_train_dataset = datasets.CIFAR10(root="./data", train=True, download=True, transform=train_transform
)
test_dataset = datasets.CIFAR10(root="./data", train=False, download=True, transform=test_transform
)# 4. 抽樣10%的訓練集,模擬小樣本場景
train_indices, _ = train_test_split(range(len(full_train_dataset)),test_size=1 - SAMPLE_RATIO,random_state=42,stratify=full_train_dataset.targets # 保持類別分布一致
)
train_dataset = Subset(full_train_dataset, train_indices)# 5. 創建DataLoader
train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True, num_workers=2
)
test_loader = DataLoader(test_dataset, batch_size=BATCH_SIZE, shuffle=False, num_workers=2
)# 查看數據集規模
print(f"訓練集樣本數:{len(train_dataset)}") # 約5000張
print(f"測試集樣本數:{len(test_dataset)}") # 10000張
5.1.4 構建兩種模型:從零訓練 vs 微調
我們構建兩個 ResNet-18 模型,分別用于對比:
- 從零訓練模型:所有參數隨機初始化;
- 微調模型:加載 ImageNet 預訓練參數,替換輸出層,凍結前 10 層(卷積層),訓練剩余層。
python
運行
def build_model(fine_tune=True):"""構建模型:fine_tune=True 表示微調預訓練模型,False表示從零訓練"""if fine_tune:# 1. 加載預訓練ResNet-18(默認加載ImageNet預訓練參數)model = resnet18(pretrained=True) # PyTorch 1.13+需用 weights=ResNet18_Weights.DEFAULT# 2. 替換輸出層(適配CIFAR-10的10類)in_features = model.fc.in_features # 獲取全連接層輸入特征數model.fc = nn.Linear(in_features, NUM_CLASSES)# 3. 凍結前10層(卷積層),僅訓練后幾層和全連接層# ResNet-18的層結構:conv1 -> layer1 -> layer2 -> layer3 -> layer4 -> fc# 凍結 conv1 + layer1 + layer2 的前幾層(共10層)freeze_layer_num = 10for i, (name, param) in enumerate(model.named_parameters()):if i < freeze_layer_num:param.requires_grad = False # 凍結參數,不更新else:param.requires_grad = True # 解凍參數,更新else:# 從零訓練:不加載預訓練參數,所有參數隨機初始化model = resnet18(pretrained=False)in_features = model.fc.in_featuresmodel.fc = nn.Linear(in_features, NUM_CLASSES)# 所有參數均可訓練for param in model.parameters():param.requires_grad = True# 移動模型到GPU/CPUmodel = model.to(DEVICE)return model# 構建兩個模型
model_scratch = build_model(fine_tune=False) # 從零訓練
model_finetune = build_model(fine_tune=True) # 微調
5.1.5 模型訓練與評估函數
定義訓練和評估函數,用于統一訓練兩個模型:
python
運行
def train_model(model, train_loader, criterion, optimizer, epoch):"""訓練模型一個epoch"""model.train() # 開啟訓練模式total_loss = 0.0total_correct = 0total_samples = 0for batch_idx, (data, target) in enumerate(train_loader):# 數據移動到DEVICEdata, target = data.to(DEVICE), target.to(DEVICE)# 前向傳播output = model(data)loss = criterion(output, target)# 反向傳播與優化optimizer.zero_grad()loss.backward()optimizer.step()# 統計損失和準確率total_loss += loss.item() * data.size(0)_, predicted = torch.max(output, 1)total_correct += (predicted == target).sum().item()total_samples += data.size(0)# 每100個batch打印一次進度if batch_idx % 100 == 0:print(f"Epoch [{epoch+1}/{EPOCHS}], Batch [{batch_idx}/{len(train_loader)}], "f"Loss: {loss.item():.4f}, Acc: {100.*total_correct/total_samples:.2f}%")# 計算一個epoch的平均損失和準確率avg_loss = total_loss / total_samplesavg_acc = 100. * total_correct / total_samplesreturn avg_loss, avg_accdef evaluate_model(model, test_loader, criterion):"""評估模型在測試集上的性能"""model.eval() # 開啟評估模式(關閉dropout、batchnorm更新)total_loss = 0.0total_correct = 0total_samples = 0with torch.no_grad(): # 禁用梯度計算,節省內存for data, target in test_loader:data, target = data.to(DEVICE), target.to(DEVICE)output = model(data)loss = criterion(output, target)total_loss += loss.item() * data.size(0)_, predicted = torch.max(output, 1)total_correct += (predicted == target).sum().item()total_samples += data.size(0)avg_loss = total_loss / total_samplesavg_acc = 100. * total_correct / total_samplesprint(f"Test Loss: {avg_loss:.4f}, Test Acc: {avg_acc:.2f}%")return avg_loss, avg_acc
5.1.6 訓練兩個模型并記錄結果
使用相同的損失函數(交叉熵)和優化器(Adam)訓練兩個模型,記錄訓練過程中的損失和準確率:
python
運行
# 定義損失函數和優化器(兩個模型使用相同配置)
criterion = nn.CrossEntropyLoss()
optimizer_scratch = optim.Adam(model_scratch.parameters(), lr=LEARNING_RATE)
optimizer_finetune = optim.Adam(model_finetune.parameters(), lr=LEARNING_RATE)# 記錄訓練過程
history = {"scratch": {"train_loss": [], "train_acc": [], "test_loss": [], "test_acc": []},"finetune": {"train_loss": [], "train_acc": [], "test_loss": [], "test_acc": []}
}# 訓練從零開始的模型
print("="*50)
print("Training Model from Scratch")
print("="*50)
for epoch in range(EPOCHS):train_loss, train_acc = train_model(model_scratch, train_loader, criterion, optimizer_scratch, epoch)test_loss, test_acc = evaluate_model(model_scratch, test_loader, criterion)# 記錄結果history["scratch"]["train_loss"].append(train_loss)history["scratch"]["train_acc"].append(train_acc)history["scratch"]["test_loss"].append(test_loss)history["scratch"]["test_acc"].append(test_acc)# 訓練微調模型
print("\n" + "="*50)
print("Training Fine-tuned Model")
print("="*50)
for epoch in range(EPOCHS):train_loss, train_acc = train_model(model_finetune, train_loader, criterion, optimizer_finetune, epoch)test_loss, test_acc = evaluate_model(model_finetune, test_loader, criterion)# 記錄結果history["finetune"]["train_loss"].append(train_loss)history["finetune"]["train_acc"].append(train_acc)history["finetune"]["test_loss"].append(test_loss)history["finetune"]["test_acc"].append(test_acc)
5.1.7 結果可視化與分析
繪制訓練 / 測試損失和準確率曲線,對比兩個模型的性能:
python
運行
# 設置中文字體
plt.rcParams['font.sans-serif'] = ['WenQuanYi Zen Hei']
plt.rcParams['axes.unicode_minus'] = False# 創建2x2的子圖
fig, axes = plt.subplots(2, 2, figsize=(15, 12))# 1. 訓練損失對比
axes[0, 0].plot(range(1, EPOCHS+1), history["scratch"]["train_loss"], label="從零訓練", marker='o', linewidth=2)
axes[0, 0].plot(range(1, EPOCHS+1), history["finetune"]["train_loss"], label="微調", marker='s', linewidth=2)
axes[0, 0].set_title("訓練損失對比", fontsize=14)
axes[0, 0].set_xlabel("Epoch", fontsize=12)
axes[0, 0].set_ylabel("損失", fontsize=12)
axes[0, 0].legend()
axes[0, 0].grid(True)# 2. 訓練準確率對比
axes[0, 1].plot(range(1, EPOCHS+1), history["scratch"]["train_acc"], label="從零訓練", marker='o', linewidth=2)
axes[0, 1].plot(range(1, EPOCHS+1), history["finetune"]["train_acc"], label="微調", marker='s', linewidth=2)
axes[0, 1].set_title("訓練準確率對比", fontsize=14)
axes[0, 1].set_xlabel("Epoch", fontsize=12)
axes[0, 1].set_ylabel("準確率(%)", fontsize=12)
axes[0, 1].legend()
axes[0, 1].grid(True)# 3. 測試損失對比
axes[1, 0].plot(range(1, EPOCHS+1), history["scratch"]["test_loss"], label="從零訓練", marker='o', linewidth=2)
axes[1, 0].plot(range(1, EPOCHS+1), history["finetune"]["test_loss"], label="微調", marker='s', linewidth=2)
axes[1, 0].set_title("測試損失對比", fontsize=14)
axes[1, 0].set_xlabel("Epoch", fontsize=12)
axes[1, 0].set_ylabel("損失", fontsize=12)
axes[1, 0].legend()
axes[1, 0].grid(True)# 4. 測試準確率對比
axes[1, 1].plot(range(1, EPOCHS+1), history["scratch"]["test_acc"], label="從零訓練", marker='o', linewidth=2)
axes[1, 1].plot(range(1, EPOCHS+1), history["finetune"]["test_acc"], label="微調", marker='s', linewidth=2)
axes[1, 1].set_title("測試準確率對比", fontsize=14)
axes[1, 1].set_xlabel("Epoch", fontsize=12)
axes[1, 1].set_ylabel("準確率(%)", fontsize=12)
axes[1, 1].legend()
axes[1, 1].grid(True)# 保存圖片
plt.tight_layout()
plt.savefig("transfer_learning_cifar10.png", dpi=300)
plt.show()# 輸出最終結果
print("\n" + "="*50)
print("最終結果對比")
print("="*50)
print(f"從零訓練模型 - 測試準確率:{history['scratch']['test_acc'][-1]:.2f}%")
print(f"微調模型 - 測試準確率:{history['finetune']['test_acc'][-1]:.2f}%")
print(f"準確率提升:{history['finetune']['test_acc'][-1] - history['scratch']['test_acc'][-1]:.2f}%")
5.1.8 預期結果與分析
在小樣本場景(CIFAR-10 訓練集僅 5000 張)下,預期結果如下:
- 從零訓練模型:測試準確率約 65%-70%,訓練后期可能過擬合(訓練準確率高,測試準確率低);
- 微調模型:測試準確率約 80%-85%,收斂速度快(前 5 個 epoch 即可達到較高準確率),過擬合風險低。
原因分析:
- 預訓練 ResNet-18 的底層卷積層學習了通用圖像特征(邊緣、紋理),無需在小樣本上重新學習;
- 僅訓練上層和全連接層,參數數量少,降低了過擬合風險;
- 微調的學習率小,避免了破壞預訓練的通用特征。
5.2 案例 2:自然語言處理 ——IMDB 情感分析(BERT 微調)
5.2.1 實驗背景與目標
- 源域:Wikipedia 英文語料,預訓練模型為 BERT-base-uncased(12 層 Transformer,768 維嵌入);
- 目標域:IMDB 電影評論數據集(5 萬條評論,正 / 負情感各 2.5 萬條);
- 任務:IMDB 情感二分類,對比 “從零訓練 LSTM” 和 “微調 BERT” 的性能差異;
- 實驗目標:驗證 BERT 微調在文本分類任務中的優勢,尤其是在小樣本場景下。
5.2.2 實驗環境準備
需安裝 Hugging Face 的transformers
庫(用于加載 BERT 模型和 Tokenizer):
bash
pip install torch transformers datasets matplotlib numpy scikit-learn
5.2.3 數據加載與預處理
使用datasets
庫加載 IMDB 數據集,并用 BERT 的 Tokenizer 處理文本(將文本轉換為 BERT 的輸入格式:token_id、attention_mask、token_type_id):
python
運行
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
from transformers import BertTokenizer, BertForSequenceClassification
from datasets import load_dataset
import matplotlib.pyplot as plt
import numpy as np
from sklearn.model_selection import train_test_split# 1. 配置超參數
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
BATCH_SIZE = 32
EPOCHS = 5
LEARNING_RATE = 2e-5 # BERT微調學習率通常很小(避免破壞預訓練知識)
NUM_CLASSES = 2 # 情感二分類(正/負)
SAMPLE_RATIO = 0.1 # 抽樣10%的訓練集,模擬小樣本場景
BERT_MODEL_NAME = "bert-base-uncased" # 小寫英文BERT模型# 2. 加載IMDB數據集
dataset = load_dataset("imdb")
train_dataset = dataset["train"]
test_dataset = dataset["test"]# 3. 抽樣10%的訓練集
train_indices, _ = train_test_split(range(len(train_dataset)),test_size=1 - SAMPLE_RATIO,random_state=42,stratify=train_dataset["label"] # 保持情感分布一致
)
train_dataset = train_dataset.select(train_indices)# 4. 加載BERT Tokenizer
tokenizer = BertTokenizer.from_pretrained(BERT_MODEL_NAME)# 5. 文本預處理函數:將文本轉換為BERT輸入格式
def preprocess_function(examples):return tokenizer(examples["text"],padding="max_length", # 填充到BERT的最大輸入長度(512)truncation=True, # 截斷超過512的文本max_length=512)# 6. 應用預處理函數
train_dataset = train_dataset.map(preprocess_function, batched=True)
test_dataset = test_dataset.map(preprocess_function, batched=True)# 7. 轉換為PyTorch張量格式
train_dataset.set_format(type="torch",columns=["input_ids", "attention_mask", "token_type_ids", "label"],device=DEVICE
)
test_dataset.set_format(type="torch",columns=["input_ids", "attention_mask", "token_type_ids", "label"],device=DEVICE
)# 8. 創建DataLoader
train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True
)
test_loader = DataLoader(test_dataset, batch_size=BATCH_SIZE, shuffle=False
)# 查看數據集規模
print(f"訓練集樣本數:{len(train_dataset)}") # 約2500條
print(f"測試集樣本數:{len(test_dataset)}") # 25000條
5.2.4 構建兩種模型:從零訓練 LSTM vs 微調 BERT
構建兩個模型用于對比:
- 從零訓練 LSTM:用隨機初始化的詞嵌入和 LSTM 構建文本分類模型;
- 微調 BERT:加載預訓練 BERT 模型,添加分類頭,微調部分層。
python
運行
class LSTMClassifier(nn.Module):"""從零訓練的LSTM文本分類模型"""def __init__(self, vocab_size, embedding_dim, hidden_dim, num_classes):super(LSTMClassifier, self).__init__()# 詞嵌入層(隨機初始化)self.embedding = nn.Embedding(vocab_size, embedding_dim)# LSTM層self.lstm = nn.LSTM(embedding_dim, hidden_dim, num_layers=2, bidirectional=True, batch_first=True)# 全連接層(雙向LSTM輸出維度為2*hidden_dim)self.fc = nn.Linear(hidden_dim * 2, num_classes)# Dropout層(防止過擬合)self.dropout = nn.Dropout(0.5)def forward(self, input_ids, attention_mask=None):# 輸入:input_ids (batch_size, seq_len)# 詞嵌入:(batch_size, seq_len, embedding_dim)embedded = self.dropout(self.embedding(input_ids))# LSTM輸出:(batch_size, seq_len, 2*hidden_dim)lstm_out, _ = self.lstm(embedded)# 取最后一個時間步的輸出:(batch_size, 2*hidden_dim)last_hidden = lstm_out[:, -1, :]# 全連接層輸出:(batch_size, num_classes)logits = self.fc(self.dropout(last_hidden))return logitsdef build_models():# 1. 構建從零訓練的LSTM模型# 詞表大小:使用BERT的詞表大小(避免重新構建詞表)vocab_size = tokenizer.vocab_sizelstm_model = LSTMClassifier(vocab_size=vocab_size,embedding_dim=128, # 詞嵌入維度hidden_dim=256, # LSTM隱藏層維度num_classes=NUM_CLASSES).to(DEVICE)# 2. 構建微調的BERT模型# 加載預訓練BERT,添加分類頭(num_labels=2)bert_model = BertForSequenceClassification.from_pretrained(BERT_MODEL_NAME,num_labels=NUM_CLASSES).to(DEVICE)# 凍結BERT的前6層(僅訓練后6層和分類頭)freeze_layer_num = 6for i, (name, param) in enumerate(bert_model.bert.named_parameters()):# BERT的層名格式:layer.0.attention.self.query.weight(第0層)if "layer." in name:layer_idx = int(name.split("layer.")[1].split(".")[0])if layer_idx < freeze_layer_num:param.requires_grad = False# 嵌入層也凍結elif "embeddings" in name:param.requires_grad = Falsereturn lstm_model, bert_model# 構建兩個模型
model_lstm, model_bert = build_models()