人工神經網路:競爭型學習

赤川鹤鸣發表於2024-08-19

Ciallo~(∠・ω< )⌒★ 我是赤川鶴鳴。這是我的第一篇關於人工智慧技術的部落格。內容大多數為本人的思考和學習筆記,希望對你有所幫助。

現今,以反向傳播為主的神經網路,在處理諸如分類任務時,通常由事先已經規定的分類來決定標籤的分類數,進而確定網路架構。

例如,如果我們對 MNIST 資料集[3]進行分類,那麼神經網路的訓練標籤分類數通常為 10,對應著 10 個阿拉伯數字。我們通常會創造一個神經網路,使其輸出神經元的個數為 10,然後利用圖片對應的標籤對模型進行訓練,最終得到一個手寫數字的分類器。

如果我們要對一些現實生活中拍攝的照片進行分類,例如我們拍攝了一批照片,其中所有照片都可以被劃分為“小貓”、“小狗”兩類,你據此訓練了一個神經網路,並且執行得很好,它以較高的準確率分類了小貓和小狗。然而,某一天,你拍攝了一批“小鹿”的照片,並希望,在不改變原有網路架構的情況下,繼續訓練原來的網路模型,使其變成可以分類“小貓”、“小狗”、“小鹿”的模型。

通常,一個已經訓練完畢的反向傳播網路不會自主地根據新的輸入來更新引數,如果此時你只是用小鹿圖片進行模型推理,效果一定是奇差無比。而且,模型並不會因為你給它看過了小鹿圖片就自主地學習了這個分類。而與人類的大腦相比,人類可以對沒有見過的圖片進行推理和學習,如果你給從來沒有見過“小鹿”的學生們展示一張小鹿照片,那麼他們很快就可以學習到“小鹿”這一新的分類,並且在之後看到類似的照片甚至是實物時也可以很快地反應出正確的結果。

除了上文提及的監督學習,現實世界中存在大量無預先標籤的樣本,如果想讓模型也學習到模式特徵,通常使用的是無監督學習的方法,您可能已經知道了其中的一些,例如聚類演算法(K-means、DB Scan)等。這裡要介紹的競爭學習也是無監督學習的一種。

競爭學習和近鄰學習是大腦高效地利用有限的皮層神經元資源進化的自然選擇進化法則的具體表現形式[4]。類似人類大腦,如果我們的模型也能夠根據新的輸入自主動態地調整引數,並且能夠增加對新的模式的識別能力(線上學習),同時不顯著損失之前的分類能力(避免神經網路災難性遺忘),那麼這樣的模型無疑是更健壯的。

本期,我們就來學習人工神經網路中的競爭型學習

競爭學習原理 [1]

網路結構

輸入層 → 競爭層
本質上是一個線性層。

前提條件

\[\sum_{j} w_{ji} = 1, \ 0 \leq w_{ij} \leq 1 \]

\[x_i \in \left\{0, \ 1\right\} \]

計算輸出

\[s_{j} = \sum_{i} w_{ij} x_i \]

競爭方法

WTA (Winner Takes All):訊號輸出最大的那個神經元獲勝.

\[a_k = \left\{ \begin{array}{l} 1 & s_{k} > s_{j}, \ \forall j, \ k \neq j \\ 0 & 其他 \\ \end{array} \right. \]

引數修正方法

\[\Delta w_{ij} = \alpha \left( \frac{x_i}{m} - w_{ij} \right) \]

\[m = \sum_{i} x_i \]

推論

\[\sum_{i} \Delta w_{ij} = 0 \]

競爭學習網路特徵 [1]

  1. 競爭層中的神經元總是趨向於響應它所代表的某個特殊的樣本模式,輸出神經元則是檢測不同模式的檢測器。
  2. 網路透過極小化同一模式類裡樣本間距離(Hamming 距離[2]),極大化不同模式類間的距離來尋找模式類。
  3. 網路學習有時依賴於初始的權值輸入樣本的次序
  4. 無法預先得知模式分類個數,僅在學習後確定。
  5. 使用明顯不同的新模式進行分類時,可能能力下降或無法分類,因為使用了非推理方式調節權值。一般作為其他網路的子網路結構使用。

競爭學習網路的實現

接下來我們來進行競爭學習網路的實現。

# 匯入必要的包
from typing import Dict, Any

import torch
import torch.nn as nn
import torch.nn.functional as F
from torch import optim
from torch.optim.optimizer import ParamsT
from tqdm import tqdm


class NormalizedWeightLinear(nn.Module):
    def __init__(self, in_features: int, out_features: int):
        """
        競爭學習線性層
        :param in_features: 輸入特徵數 
        :param out_features: 輸出特徵數
        """
        super(NormalizedWeightLinear, self).__init__()
        self.weight = nn.Parameter(torch.Tensor(out_features, in_features))
        self.reset_parameters()

    def reset_parameters(self):
        # 初始化權重,使得每個神經元的權重之和為1
        with torch.no_grad():
            # 遵循公式
            # $$
            # \sum_{j} w_{ji} = 1, \ 0 \leq w_{ij} \leq 1
            # $$
            nn.init.uniform_(self.weight, 0, 1)  # 隨機初始化權重
            self.weight /= self.weight.sum(axis=0, keepdims=True)  # 歸一化權重

    def forward(self, x):
        return F.linear(x, self.weight)


class CompetitiveLearningNet(nn.Module):
    def __init__(self, input_features: int, comp_features: int) -> None:
        """
        單層競爭學習線性層網路
        :param input_features: 輸入神經元的個數
        :param comp_features: 競爭神經元的個數 注意這裡並不是模式分類個數,只是輸出神經元的個數
        """
        super().__init__()
        self.input_features = input_features
        self.comp_features = comp_features

        self.input_2_comp = NormalizedWeightLinear(input_features, comp_features)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        """
        前饋計算
        :param x: x 具有形狀 [batch_size, input_features]
        :return:
        """
        assert x.shape[1] == self.input_features
        x: torch.Tensor = self.input_2_comp(x)

        # 競爭
        print(f"輸出層\n{x}")
        y = x.argmax(dim=1)

        return y


class CompetitiveLearningOptimizer(optim.Optimizer):
    def __init__(self, params: ParamsT, lr: float,
                 defaults: Dict[str, Any] = None):
        """
        單層競爭學習線性層網路最佳化器
        :param params: 模型的引數
        :param lr: 學習率
        :param defaults: 將忽略此引數 
        """
        super().__init__(params, defaults if defaults else {})
        self.lr = lr

    def step(self, closure=None):
        for param_group in self.param_groups:
            params = param_group["params"]
            for param in params:
                assert closure and callable(closure)
                # 這裡的閉包是為了獲取輸入 x
                x: torch.Tensor = closure()
                assert isinstance(x, torch.Tensor)
                m = x.sum(dim=1, keepdim=True)
                # delta_w = self.lr * (x / m - param.data)
                batches = x.shape[0]
                for b in range(batches):
                    # 小提示:如果直接操作張量有困難,那麼先寫迴圈,然後一步一步拆解
                    # 1. 先寫三層迴圈 批大小, 輸入維度, 輸出維度
                    # for i in range(self.input_features):
                    #     for j in range(self.comp_features):
                    #         delta_w_bij = self.lr * (x[b, i] / m[b] - param.data[i, j])
                    # 2. 改寫為兩層迴圈 批大小, 輸入維度
                    # for i in range(self.input_features):
                    #     delta_w_bi = self.lr * (x[b, i] / m[b] - param.data[i, :])
                    # 3. 改寫為一層迴圈 批大小
                    delta_w = self.lr * (x[b] / m[b] - param.data)
                    param.data += delta_w


batch_size = 2
in_features = 10
out_features = 20

net = CompetitiveLearningNet(input_features=in_features, comp_features=out_features)


def gen():
    """
    隨機生成樣本
    :return: 形狀為 [batch_size, in_features] 的張量
    """
    x = torch.rand(size=(batch_size, in_features))
    x = torch.where(x >= 0.5, torch.ones_like(x), torch.zeros_like(x))
    return x

# 開始訓練和推理
for epoch in tqdm(range(10)):
    print(f"Epoch {epoch}")
    # 生成樣本
    x = gen()
    print(f"輸入層\n{x}")
    
    # 更新引數
    optimizer = CompetitiveLearningOptimizer(params=net.parameters(), lr=0.02)
    optimizer.step(closure=lambda: x)
    
    # 輸出結果
    y = net(x)
    print(f"結果\n{y}")

參考文獻:

[1] 《人工神經網路》第6章 自組織神經網路

[2] 【猿知識】漢明距離(Hamming Distance)

[3] THE MNIST DATABASE of handwritten digits

[4] 大腦皮層的神經編碼理論與類腦計算方法(二)

相關文章