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)