2025, Oct 04 01:34

प्री‑एक्टिवेशन ग्रेडिएंट से न्यूरॉन महत्ता कैसे मापें (PyTorch उदाहरण सहित)

गाइड: क्लासिफिकेशन में प्री‑एक्टिवेशन के सापेक्ष क्लास लॉजिट के ग्रेडिएंट से न्यूरॉन महत्ता मापें; PyTorch कोड, सैनीटी‑चेक और प्रूनिंग टिप्स।

क्लासिफिकेशन मॉडल में न्यूरॉन की महत्ता का अनुमान लगाते समय, एक आम आधाररेखा यह होती है: सही क्लास के लॉजिट का आंशिक अवकलन किसी न्यूरॉन के प्री-एक्टिवेशन के सापेक्ष। यदि आपको क्लस्टर-स्तर पर महत्ता चाहिए, तो क्लस्टर के सैंपलों पर इन अवकलनों का औसत लिया जा सकता है। व्यावहारिक रूप से कभी-कभी ऐसा होता है कि निकले हुए ग्रेडिएंट बहुत छोटे दिखते हैं, और इन स्कोरों से रैंक किए गए “टॉप न्यूरॉन्स” को हटाने (prune) पर भी रैंडम प्रूनिंग से बेहतर प्रदर्शन नहीं मिलता। खासकर तब यह ग्रेडिएंट एक्सट्रैक्शन में बग जैसा लगता है, जब प्री-एक्टिवेशन जैसी सरल तरकीबें अपेक्षाकृत बेहतर लगती हों।

न्यूरॉन प्री-एक्टिवेशन के लिए प्रयोग किए गए ग्रेडिएंट-आधारित एट्रिब्यूशन का पुनरुत्पादन

नीचे दिया गया स्निपेट किसी चुनी हुई लेयर के लिए प्रति-न्यूरॉन महत्ता निकालता है: यह टार्गेट लॉजिट के उस लेयर की प्री-एक्टिवेशन के सापेक्ष आंशिक अवकलन (partial derivatives) जुटाता है, फिर सैंपल्स पर औसत लेता है। मान लें कि दी गई लेयर PyTorch की Linear लेयर है, इसलिए बैकवर्ड हुक का अंतिम आर्ग्युमेंट प्री-एक्टिवेशन के सापेक्ष ग्रेडिएंट्स के अनुरूप होता है।

class InfluenceBase(ABC):
    def __init__(self, net: nn.Module, loaders: Dict[int, DataLoader]):
        self.net = net
        self.loaders = loaders
        if self.net is not None:
            self.net.eval()
    @abstractmethod
    def score(self, sublayer: Module, group_id: int) -> np.ndarray:
        pass
class GradInfluence(InfluenceBase):
    def score(self, sublayer: Module, group_id: int) -> np.ndarray:
        device = next(self.net.parameters()).device
        batch_x, batch_y = next(iter(self.loaders[group_id]))
        batch_x = batch_x.to(device)
        batch_y = batch_y.to(device)
        batch_x.requires_grad = True
        traces = []
        def tap(mod, gin, gout):
            traces.append(gout[0].detach().cpu())
        h = sublayer.register_full_backward_hook(tap)
        for j in range(len(batch_x)):
            xj = batch_x[j].unsqueeze(0)
            logits = self.net(xj).squeeze()
            cls = batch_y[j].item()
            logits[cls].backward()
        h.remove()
        g_stack = torch.cat(traces, dim=0)
        g_mean = g_stack.mean(dim=0).numpy()
        return g_mean

यह कोड वास्तव में क्या गणना करता है और क्यों

हुक एक Linear लेयर पर रजिस्टर किया गया है। बैकप्रोपेगेशन के दौरान यह हुक उस लेयर के एक्टिवेशन से पहले के आउटपुट, यानी प्री-एक्टिवेशन, के सापेक्ष ग्रेडिएंट्स प्राप्त करता है। लूप हर सैंपल के लिए एक-एक बैकवर्ड पास चलाता है: ग्राउंड-ट्रुथ क्लास का स्कैलर लॉजिट लेकर, हुक की गई लेयर की प्री-एक्टिवेशन के सापेक्ष उसका ग्रेडिएंट निकालता है। फिर इन ग्रेडिएंट्स को हर आइटम के लिए इकट्ठा करके जोड़ता है और प्रति-न्यूरॉन औसत लेकर क्लस्टर-स्तरीय महत्ता प्राप्त करता है। यहाँ लूप के भीतर पैरामीटर ग्रेडिएंट्स को शून्य करने की जरूरत नहीं है, क्योंकि एट्रिब्यूशन हुक से पढ़ा जा रहा है, पैरामीटर के एक्यूमुलेटर से नहीं।

दूसरे शब्दों में, यदि आपका लक्ष्य “सही क्लास लॉजिट का न्यूरॉन प्री-एक्टिवेशन के सापेक्ष आंशिक अवकलन” है, तो यह कोड Linear लेयर के लिए अपेक्षित गणना करता है।

नियंत्रित टॉय मॉडल के साथ सैनीटी-चेक

यह सुनिश्चित करने का भरोसेमंद तरीका कि यह विधि समझदारी से काम कर रही है, एक सरल मॉडल और डाटासेट बनाना है जहाँ “सही उत्तर” साफ हो। मान लें एक नेटवर्क है जिसमें तीन अलग-अलग शाखाएँ हैं, हर एक सिर्फ एक फीचर पढ़ती है, और एक साझा क्लासिफिकेशन हेड है। केवल दूसरा फीचर लेबल के लिए अत्यधिक भविष्यसूचक है, इसलिए ग्रेडिएंट मीट्रिक के अनुसार मध्य शाखा के न्यूरॉन्स सबसे महत्वपूर्ण आने चाहिए।

class MiniNet(nn.Module):
    def __init__(self, width=4):
        super().__init__()
        self.branch0 = nn.Sequential(nn.Linear(1, width), nn.ReLU())
        self.branch1 = nn.Sequential(nn.Linear(1, width), nn.ReLU())
        self.branch2 = nn.Sequential(nn.Linear(1, width), nn.ReLU())
        self.head = nn.Linear(3 * width, 2)
    def forward(self, z):
        z0 = z[:, [0]]
        z1 = z[:, [1]]
        z2 = z[:, [2]]
        u0 = self.branch0(z0)
        u1 = self.branch1(z1)
        u2 = self.branch2(z2)
        u = torch.cat([u0, u1, u2], dim=1)
        return self.head(u)

अब सिंथेटिक डेटा बनाइए ताकि लेबल दूसरे फीचर के चिह्न (sign) से तय हो।

M = 200
feats = np.random.randn(M, 3).astype(np.float32)
labels = (feats[:, 1] > 0).astype(np.int64)
ds = TensorDataset(torch.from_numpy(feats), torch.from_numpy(labels))
dl = DataLoader(ds, batch_size=16, shuffle=True)

मॉडल को थोड़ी देर ट्रेन करें।

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
net = MiniNet().to(device)
optimizer = torch.optim.Adam(net.parameters(), lr=0.01)
criterion = nn.CrossEntropyLoss()
for ep in range(10):
    for xb, yb in dl:
        xb, yb = xb.to(device), yb.to(device)
        optimizer.zero_grad()
        pred = net(xb)
        loss = criterion(pred, yb)
        loss.backward()
        optimizer.step()
    if (ep + 1) % 5 == 0:
        acc = (pred.argmax(1) == yb).float().mean().item()
        print(f"Epoch {ep+1}, Loss={loss.item():.4f}, Acc={acc:.3f}")

हर ब्रांच की Linear लेयर के लिए औसत आंशिक अवकलन निकालिए। यह ऊपर वाले तर्क को ही दोहराता है और प्री-एक्टिवेशन की जांच करता है।

def grad_scores(net, sublayer, inputs, targets):
    device = next(net.parameters()).device
    xs = inputs.to(device)
    ys = targets.to(device)
    xs.requires_grad = True
    logs = []
    def tap(mod, gin, gout):
        logs.append(gout[0].detach().cpu())
    h = sublayer.register_full_backward_hook(tap)
    for k in range(len(xs)):
        xi = xs[k].unsqueeze(0)
        out = net(xi).squeeze()
        yi = ys[k].item()
        out[yi].backward()
    h.remove()
    g = torch.cat(logs, dim=0)
    return g.mean(dim=0).numpy()
xb, yb = next(iter(dl))
s0 = grad_scores(net, net.branch0[0], xb, yb)
s1 = grad_scores(net, net.branch1[0], xb, yb)
s2 = grad_scores(net, net.branch2[0], xb, yb)
print("Branch0 importance:", abs(s0).mean())
print("Branch1 importance:", abs(s1).mean())
print("Branch2 importance:", abs(s2).mean())

एक प्रतिनिधि रन में कुछ इस तरह का आउटपुट मिलता है:

Branch0 importance: 0.083575
Branch1 importance: 0.3273803  <- highest, as expected
Branch2 importance: 0.05412347

यह दिखाता है कि प्री-एक्टिवेशन पर आधारित ग्रेडिएंट एट्रिब्यूशन, जब संकेत साफ होता है, तो सचमुच जानकारीपूर्ण ब्रांच को सतह पर लाता है।

वास्तविक परिदृश्यों के लिए इसका अर्थ

यदि यही कोड आपके अनुप्रयोग में कमजोर विभेदन दिखाता है, तो यह जरूरी नहीं कि डेरिवेटिव इकट्ठा करने में कोई बग हो। ऐसा तब भी हो सकता है जब संकेत सूक्ष्म हों, कई यूनिट्स में बँटे हों, या “सही-क्लास लॉजिट के प्री-एक्टिवेशन पर ग्रेडिएंट” जैसे सरल प्रॉक्सी से उनका संरेखण न बैठता हो। ऊपर दिया टॉय सेटअप एक अच्छा सैनीटी-चेक है: अगर वह मनचाही ब्रांच को उभारता है, तो तंत्र अपने इरादे के मुताबिक काम कर रहा है। इसके बाद आप इसी तरह के नियंत्रित परिदृश्यों में अलग-अलग रैंकिंग योजनाओं—सहित अपनी रैंकिंग के बाद प्रूनिंग की प्रक्रिया—की तुलना कर सकते हैं।

यह जानना क्यों उपयोगी है

ग्रेडिएंट-आधारित एट्रिब्यूशन्स डेटा व्यवस्थाओं के प्रति संवेदनशील होते हैं। एक नियंत्रित उदाहरण पर पाइपलाइन की पुष्टि करने से यह भरोसा मिलता है कि आपके हुक्स और एग्रीगेशन सही हैं। फिर वास्तविक डाटासेट में विधियों के बीच जो भी अंतर दिखे, वह अधिक संभावना से डेटा और मॉडल डायनेमिक्स से आता है, न कि ग्रेडिएंट एक्सट्रैक्शन में किसी कोडिंग त्रुटि से।

सार

यदि आपको प्री-एक्टिवेशन से जुड़ी न्यूरॉन महत्ता चाहिए, तो Linear लेयर के आउटपुट पर बैकवर्ड हुक सही आंशिक अवकलन देता है, और सैंपलों पर औसत लेकर क्लस्टर-स्तरीय स्कोर मिलता है। एक सरल, स्पष्ट-संरचित कार्य पर पहले सत्यापित करें कि आपकी प्रक्रिया ज्ञात-ज़रूरी यूनिट्स को ऊपर रखती है; फिर इसी टूलिंग को प्रोडक्शन डेटा पर वापस ले जाकर महत्ता मापों की व्याख्या और तुलना करें—उसी तरीके से जैसे आपका मॉडल वास्तव में सीखता है।

यह लेख StackOverflow पर एक प्रश्न (लेखक: jonupp) और Sachin Hosmani के उत्तर पर आधारित है।