File size: 10,774 Bytes
5c6c847 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 | """Tensor + 反向模式自动求导(只依赖 numpy)。
每个运算产出一个新 Tensor,并记录:
_parents : 参与运算的输入 Tensor
_backward : 函数 g -> (对每个 parent 的梯度, ...)
backward() 先拓扑排序,再从输出往回逐个调用 _backward,把梯度累加到 .grad。
"""
import numpy as np
_grad_enabled = True
_default_dtype = np.dtype(np.float32)
def set_default_dtype(dtype):
"""所有浮点数据进入框架时统一转成这个类型(默认 float32)。
只有测试里的数值梯度检查需要切到 float64。整数数组(下标)不受影响。"""
global _default_dtype
_default_dtype = np.dtype(dtype)
def get_default_dtype():
return _default_dtype
_flops = None # count_flops() 打开时是一个 [计数],否则 None
class count_flops:
"""with count_flops() as c: ... 之后 c.flops = 期间所有矩阵乘的浮点运算次数(乘加算 2 次)。
只统计矩阵乘(模型里绝大部分计算量),不统计逐元素运算;不受机器负载影响,比计时可靠。"""
def __enter__(self):
global _flops
self._prev, _flops = _flops, [0]
return self
def __exit__(self, *exc):
global _flops
self.flops = _flops[0]
_flops = self._prev
class no_grad:
"""with no_grad(): 内部的运算不建计算图(推理/评估时用,省内存)。"""
def __enter__(self):
global _grad_enabled
self._prev, _grad_enabled = _grad_enabled, False
def __exit__(self, *exc):
global _grad_enabled
_grad_enabled = self._prev
def _unbroadcast(grad, shape):
"""把广播后的梯度求和还原成原始形状。"""
while grad.ndim > len(shape):
grad = grad.sum(axis=0)
for i, s in enumerate(shape):
if s == 1 and grad.shape[i] != 1:
grad = grad.sum(axis=i, keepdims=True)
return grad
def _to_array(x):
if isinstance(x, Tensor):
return x.data
arr = np.asarray(x)
# 浮点一律转成默认类型,避免 float32 和 float64 混在一起被 numpy 悄悄升成 float64
if arr.dtype.kind == "f" and arr.dtype != _default_dtype:
arr = arr.astype(_default_dtype)
return arr
class Tensor:
__array_priority__ = 100 # 让 ndarray + Tensor 走 Tensor.__radd__
def __init__(self, data, requires_grad=False):
self.data = _to_array(data)
self.requires_grad = requires_grad
self.grad = None
self._parents = ()
self._backward = None
# ---------- 基础属性 ----------
@property
def shape(self):
return self.data.shape
@property
def ndim(self):
return self.data.ndim
@property
def dtype(self):
return self.data.dtype
def __len__(self):
return len(self.data)
def __repr__(self):
return f"Tensor({self.data}, requires_grad={self.requires_grad})"
def numpy(self):
return self.data
def item(self):
return self.data.item()
def detach(self):
return Tensor(self.data)
# ---------- 建图 ----------
@staticmethod
def _op(data, parents, backward):
"""创建运算结果。backward(g) 返回与 parents 一一对应的梯度(可为 None)。"""
out = Tensor(data)
if _grad_enabled and any(p.requires_grad for p in parents):
out.requires_grad = True
out._parents = parents
out._backward = backward
return out
def backward(self, grad=None):
if grad is None:
if self.data.size != 1:
raise RuntimeError("非标量输出调用 backward() 需要显式传入 grad")
grad = np.ones_like(self.data)
# 迭代式拓扑排序(避免深图递归爆栈)
topo, visited, stack = [], set(), [(self, False)]
while stack:
node, expanded = stack.pop()
if expanded:
topo.append(node)
continue
if id(node) in visited:
continue
visited.add(id(node))
stack.append((node, True))
for p in node._parents:
if p.requires_grad and id(p) not in visited:
stack.append((p, False))
self.grad = grad if self.grad is None else self.grad + grad
for node in reversed(topo):
if node._backward is None or node.grad is None:
continue
for p, g in zip(node._parents, node._backward(node.grad)):
if g is not None and p.requires_grad:
p.grad = g if p.grad is None else p.grad + g
# 释放计算图,中间节点的梯度也不再需要
node._parents, node._backward, node.grad = (), None, None
# ---------- 逐元素运算 ----------
def __add__(self, other):
other = other if isinstance(other, Tensor) else Tensor(other)
a, b = self, other
return Tensor._op(a.data + b.data, (a, b),
lambda g: (_unbroadcast(g, a.shape), _unbroadcast(g, b.shape)))
def __mul__(self, other):
other = other if isinstance(other, Tensor) else Tensor(other)
a, b = self, other
return Tensor._op(a.data * b.data, (a, b),
lambda g: (_unbroadcast(g * b.data, a.shape), _unbroadcast(g * a.data, b.shape)))
def __truediv__(self, other):
other = other if isinstance(other, Tensor) else Tensor(other)
a, b = self, other
return Tensor._op(a.data / b.data, (a, b),
lambda g: (_unbroadcast(g / b.data, a.shape),
_unbroadcast(-g * a.data / (b.data ** 2), b.shape)))
def __pow__(self, p):
assert isinstance(p, (int, float)), "只支持标量指数"
a = self
return Tensor._op(a.data ** p, (a,), lambda g: (g * p * a.data ** (p - 1),))
def __neg__(self):
return Tensor._op(-self.data, (self,), lambda g: (-g,))
def __sub__(self, other):
return self + (-other if isinstance(other, Tensor) else Tensor(-_to_array(other)))
def __radd__(self, other):
return self + other
def __rmul__(self, other):
return self * other
def __rsub__(self, other):
return (-self) + other
def __rtruediv__(self, other):
return Tensor(other) / self
def exp(self):
out = np.exp(self.data)
return Tensor._op(out, (self,), lambda g: (g * out,))
def log(self):
a = self
return Tensor._op(np.log(a.data), (a,), lambda g: (g / a.data,))
def sqrt(self):
return self ** 0.5
def tanh(self):
out = np.tanh(self.data)
return Tensor._op(out, (self,), lambda g: (g * (1 - out ** 2),))
def relu(self):
mask = self.data > 0
return Tensor._op(self.data * mask, (self,), lambda g: (g * mask,))
def masked_fill(self, mask, value):
mask = _to_array(mask).astype(bool)
return Tensor._op(np.where(mask, value, self.data).astype(self.dtype), (self,),
lambda g: (np.where(mask, 0, g),))
# ---------- 归约 ----------
def sum(self, axis=None, keepdims=False):
a = self
def backward(g):
if axis is not None and not keepdims:
g = np.expand_dims(g, axis)
return (np.broadcast_to(g, a.shape).copy(),)
return Tensor._op(a.data.sum(axis=axis, keepdims=keepdims), (a,), backward)
def mean(self, axis=None, keepdims=False):
n = self.data.size if axis is None else np.prod([self.shape[i] for i in np.atleast_1d(axis)])
return self.sum(axis, keepdims) * (1.0 / n)
def max(self, axis=None, keepdims=False):
a = self
out = a.data.max(axis=axis, keepdims=True)
def backward(g):
if axis is not None and not keepdims:
g = np.expand_dims(g, axis)
mask = a.data == out
return (mask * g / mask.sum(axis=axis, keepdims=True),) # 并列最大值平分梯度
return Tensor._op(out if keepdims else out.squeeze(axis), (a,), backward)
# ---------- 形状 ----------
def reshape(self, *shape):
a = self
return Tensor._op(a.data.reshape(*shape), (a,), lambda g: (g.reshape(a.shape),))
def transpose(self, *axes):
axes = axes or tuple(reversed(range(self.ndim)))
inv = np.argsort(axes)
return Tensor._op(self.data.transpose(axes), (self,), lambda g: (g.transpose(inv),))
@property
def T(self):
return self.transpose()
def __getitem__(self, idx):
a = self
if isinstance(idx, Tensor):
idx = idx.data
parts = idx if isinstance(idx, tuple) else (idx,)
fancy = any(isinstance(i, (np.ndarray, list)) for i in parts)
def backward(g):
grad = np.zeros_like(a.data)
if fancy:
np.add.at(grad, idx, g) # 数组下标可能重复(Embedding 查表),必须累加
else:
grad[idx] = g # 普通切片不会重复,直接写,比 add.at 快得多
return (grad,)
return Tensor._op(a.data[idx], (a,), backward)
# ---------- 矩阵乘 ----------
def __matmul__(self, other):
other = other if isinstance(other, Tensor) else Tensor(other)
a, b = self, other
assert a.ndim >= 2 and b.ndim >= 2, "matmul 需要至少 2 维"
out = a.data @ b.data
if _flops is not None:
_flops[0] += 2 * out.size * a.shape[-1]
def backward(g):
ga = g @ np.swapaxes(b.data, -1, -2)
gb = np.swapaxes(a.data, -1, -2) @ g
return _unbroadcast(ga, a.shape), _unbroadcast(gb, b.shape)
return Tensor._op(out, (a, b), backward)
def concat(tensors, axis=0):
"""沿 axis 拼接。反向时把梯度按原来的大小切回去。"""
tensors = tuple(t if isinstance(t, Tensor) else Tensor(t) for t in tensors)
splits = np.cumsum([t.shape[axis] for t in tensors])[:-1]
return Tensor._op(np.concatenate([t.data for t in tensors], axis=axis), tensors,
lambda g: tuple(np.split(g, splits, axis=axis)))
# ---------- 构造函数 ----------
def tensor(data, requires_grad=False):
return Tensor(data, requires_grad)
def zeros(*shape, requires_grad=False):
return Tensor(np.zeros(shape, dtype=_default_dtype), requires_grad)
def ones(*shape, requires_grad=False):
return Tensor(np.ones(shape, dtype=_default_dtype), requires_grad)
def randn(*shape, requires_grad=False):
return Tensor(np.random.randn(*shape).astype(_default_dtype), requires_grad)
|