huangdffs commited on
Commit
249d4e4
·
verified ·
1 Parent(s): 1708a56

Upload folder using huggingface_hub

Browse files
Files changed (3) hide show
  1. .gitattributes +12 -0
  2. ckpt_0.pt +3 -0
  3. simplefsq32.py +682 -0
.gitattributes CHANGED
@@ -33,3 +33,15 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ samples/10000.jpg filter=lfs diff=lfs merge=lfs -text
37
+ samples/10500.jpg filter=lfs diff=lfs merge=lfs -text
38
+ samples/11000.jpg filter=lfs diff=lfs merge=lfs -text
39
+ samples/11500.jpg filter=lfs diff=lfs merge=lfs -text
40
+ samples/12000.jpg filter=lfs diff=lfs merge=lfs -text
41
+ samples/12500.jpg filter=lfs diff=lfs merge=lfs -text
42
+ samples/13000.jpg filter=lfs diff=lfs merge=lfs -text
43
+ samples/7500.jpg filter=lfs diff=lfs merge=lfs -text
44
+ samples/8000.jpg filter=lfs diff=lfs merge=lfs -text
45
+ samples/8500.jpg filter=lfs diff=lfs merge=lfs -text
46
+ samples/9000.jpg filter=lfs diff=lfs merge=lfs -text
47
+ samples/9500.jpg filter=lfs diff=lfs merge=lfs -text
ckpt_0.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:dded894389248d371d9ad72ea2bf90e7a5677fc5ec3801e594eeeec200512993
3
+ size 202217053
simplefsq32.py ADDED
@@ -0,0 +1,682 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 最简 FSQ 训练 Demo(支持分桶 Bucketing)
3
+ 单文件,自包含,可直接运行。
4
+
5
+ 依赖:
6
+ pip install torch torchvision einops
7
+
8
+ 运行:
9
+ # 用不同分辨率的 dummy 图片测试分桶
10
+ python fsq_demo.py --dummy --epochs 2
11
+
12
+ # 用真实 WebP 文件夹
13
+ python fsq_demo.py --data /path/to/webp_folder --epochs 10
14
+ """
15
+
16
+ import argparse
17
+ import os
18
+ import math
19
+ import glob
20
+ import random
21
+ from typing import List
22
+
23
+ import torch
24
+ import torch.nn as nn
25
+ import torch.nn.functional as F
26
+ from torch.utils.data import DataLoader, Dataset, Sampler
27
+ from torchvision import transforms
28
+ from torchvision.utils import save_image, make_grid
29
+ from PIL import Image
30
+ from einops import rearrange, pack, unpack
31
+
32
+ import json
33
+ from concurrent.futures import ThreadPoolExecutor
34
+ # ============================================================
35
+ # 1. FSQ 量化器
36
+ # ============================================================
37
+
38
+ def round_ste(z: torch.Tensor) -> torch.Tensor:
39
+ """取整 + 直通梯度估计(Straight-Through Estimator)"""
40
+ zhat = z.round()
41
+ return z + (zhat - z).detach()
42
+
43
+
44
+ def pack_one(t, pattern):
45
+ return pack([t], pattern)
46
+
47
+
48
+ def unpack_one(t, ps, pattern):
49
+ return unpack(t, ps, pattern)[0]
50
+
51
+
52
+ class FSQ(nn.Module):
53
+ """Finite Scalar Quantization,来自 https://arxiv.org/abs/2309.15505"""
54
+
55
+ def __init__(self, levels: List[int]):
56
+ super().__init__()
57
+ _levels = torch.tensor(levels, dtype=torch.int32)
58
+ self.register_buffer("_levels", _levels, persistent=False)
59
+
60
+ _basis = torch.cumprod(torch.tensor([1] + levels[:-1]), dim=0, dtype=torch.int32)
61
+ self.register_buffer("_basis", _basis, persistent=False)
62
+
63
+ self.codebook_dim = len(levels)
64
+ self.codebook_size = int(_levels.prod().item())
65
+
66
+ def bound(self, z: torch.Tensor, eps: float = 1e-3) -> torch.Tensor:
67
+ half_l = (self._levels - 1) * (1 - eps) / 2
68
+ offset = torch.where(self._levels % 2 == 0, 0.5, 0.0)
69
+ shift = (offset / half_l).tan()
70
+ return (z + shift).tanh() * half_l - offset
71
+
72
+ def quantize(self, z: torch.Tensor) -> torch.Tensor:
73
+ quantized = round_ste(self.bound(z))
74
+ half_width = self._levels // 2
75
+ return quantized / half_width
76
+
77
+ def _scale_and_shift(self, zhat_normalized):
78
+ half_width = self._levels // 2
79
+ return (zhat_normalized * half_width) + half_width
80
+
81
+ def _scale_and_shift_inverse(self, zhat):
82
+ half_width = self._levels // 2
83
+ return (zhat - half_width) / half_width
84
+
85
+ def codes_to_indices(self, zhat: torch.Tensor) -> torch.Tensor:
86
+ assert zhat.shape[-1] == self.codebook_dim
87
+ zhat = self._scale_and_shift(zhat)
88
+ return (zhat * self._basis).sum(dim=-1).to(torch.int32)
89
+
90
+ def indices_to_codes(self, indices: torch.Tensor) -> torch.Tensor:
91
+ indices = rearrange(indices, '... -> ... 1')
92
+ codes_non_centered = (indices // self._basis) % self._levels
93
+ codes = self._scale_and_shift_inverse(codes_non_centered)
94
+ return codes
95
+
96
+ def forward(self, z: torch.Tensor):
97
+ is_img = z.ndim >= 4
98
+ if is_img:
99
+ z = rearrange(z, 'b d ... -> b ... d')
100
+ z, ps = pack_one(z, 'b * d')
101
+
102
+ assert z.shape[-1] == self.codebook_dim
103
+
104
+ codes = self.quantize(z)
105
+ indices = self.codes_to_indices(codes)
106
+
107
+ if is_img:
108
+ codes = unpack_one(codes, ps, 'b * d')
109
+ codes = rearrange(codes, 'b ... d -> b d ...')
110
+ indices = unpack_one(indices, ps, 'b *')
111
+
112
+ return codes, indices
113
+
114
+
115
+ # ============================================================
116
+ # 2. Encoder / Decoder(16× 下采样,即 4 层 stride=2)
117
+ # ============================================================
118
+
119
+ class Encoder(nn.Module):
120
+ def __init__(self, in_channel=3, channel=128, embed_dim=4):
121
+ super().__init__()
122
+ self.net = nn.Sequential(
123
+ nn.Conv2d(in_channel, channel, 4, stride=2, padding=1), # 256→128
124
+ nn.ReLU(inplace=True),
125
+ nn.Conv2d(channel, channel, 4, stride=2, padding=1), # 128→64
126
+ nn.ReLU(inplace=True),
127
+ nn.Conv2d(channel, channel, 4, stride=2, padding=1), # 64→32
128
+ nn.ReLU(inplace=True),
129
+ nn.Conv2d(channel, channel, 4, stride=2, padding=1), # 32→16
130
+ nn.ReLU(inplace=True),
131
+ #nn.Conv2d(channel, channel, 4, stride=2, padding=1), # 16→8
132
+ #nn.ReLU(inplace=True),
133
+ nn.Conv2d(channel, embed_dim, 1),
134
+ )
135
+
136
+ def forward(self, x):
137
+ return self.net(x)
138
+
139
+
140
+ class Decoder(nn.Module):
141
+ def __init__(self, embed_dim=4, channel=128, out_channel=3):
142
+ super().__init__()
143
+ self.net = nn.Sequential(
144
+ nn.ConvTranspose2d(embed_dim, channel, 4, stride=2, padding=1), # 8→16
145
+ #nn.ReLU(inplace=True),
146
+ #nn.ConvTranspose2d(channel, channel, 4, stride=2, padding=1), # 16→32
147
+ nn.ReLU(inplace=True),
148
+ nn.ConvTranspose2d(channel, channel, 4, stride=2, padding=1), # 32→64
149
+ nn.ReLU(inplace=True),
150
+ nn.ConvTranspose2d(channel, channel, 4, stride=2, padding=1), # 64→128
151
+ nn.ReLU(inplace=True),
152
+ nn.ConvTranspose2d(channel, channel, 4, stride=2, padding=1), # 128→256
153
+ nn.ReLU(inplace=True),
154
+ nn.Conv2d(channel, out_channel, 1),
155
+ )
156
+
157
+ def forward(self, x):
158
+ return self.net(x)
159
+
160
+
161
+ # ============================================================
162
+ # 3. VQVAE (FSQ 版)
163
+ # ============================================================
164
+
165
+ class FSQVAE(nn.Module):
166
+ def __init__(self, levels: List[int], channel=128, in_channel=3):
167
+ super().__init__()
168
+ embed_dim = len(levels)
169
+ self.encoder = Encoder(in_channel, channel, embed_dim)
170
+ self.quantizer = FSQ(levels)
171
+ self.decoder = Decoder(embed_dim, channel, in_channel)
172
+
173
+ def forward(self, x, return_id=False):
174
+ z = self.encoder(x)
175
+ quant, indices = self.quantizer(z)
176
+ recon = self.decoder(quant)
177
+ if return_id:
178
+ return recon, indices
179
+ return recon
180
+
181
+ @torch.no_grad()
182
+ def encode(self, x):
183
+ z = self.encoder(x)
184
+ _, indices = self.quantizer(z)
185
+ return indices
186
+
187
+ @torch.no_grad()
188
+ def decode_code(self, indices):
189
+ codes = self.quantizer.indices_to_codes(indices)
190
+ codes = rearrange(codes, 'b h w d -> b d h w')
191
+ return self.decoder(codes)
192
+
193
+
194
+ # ============================================================
195
+ # 4. 分桶(Bucketing)
196
+ # ============================================================
197
+
198
+ def make_buckets(base=256, align=16):
199
+ """
200
+ 生成一组总像素数接近 base² 的桶,宽高比覆盖常见情况。
201
+ 每个桶的 H、W 都是 align 的倍数(方便下采样 16×)。
202
+ """
203
+ aspect_ratios = [0.65, 0.68, 0.72, 0.75, 0.80, 1.0, 1.33, 1.45, 1.78]
204
+ buckets = set()
205
+ for ar in aspect_ratios:
206
+ h = int(round(math.sqrt(base * base * ar)))
207
+ w = int(round(math.sqrt(base * base / ar)))
208
+ h = max(align, h // align * align)
209
+ w = max(align, w // align * align)
210
+ buckets.add((h, w))
211
+ return sorted(buckets)
212
+
213
+
214
+ def find_bucket(w, h, buckets):
215
+ """
216
+ 选裁剪面积最小的桶。
217
+ """
218
+ best, best_loss = None, float('inf')
219
+ for (bh, bw) in buckets:
220
+ scale = max(bw / w, bh / h)
221
+ new_w = math.ceil(w * scale)
222
+ new_h = math.ceil(h * scale)
223
+ # 裁剪掉的面积比例
224
+ crop_loss = 1 - (bw * bh) / (new_w * new_h)
225
+ if crop_loss < best_loss:
226
+ best_loss, best = crop_loss, (bh, bw)
227
+ return best
228
+
229
+
230
+ class BucketImageFolder(Dataset):
231
+ def __init__(self, root, buckets, recursive=True,
232
+ num_threads=2, cache_file="./sizes_cache.json"):
233
+ self.buckets = buckets
234
+ exts = ('*.webp', '*.jpg', '*.jpeg', '*.png', '*.bmp')
235
+ self.files = []
236
+ for ext in exts:
237
+ pattern = f"**/{ext}" if recursive else ext
238
+ self.files += glob.glob(os.path.join(root, pattern), recursive=recursive)
239
+ self.files = sorted(self.files)
240
+ if len(self.files) == 0:
241
+ raise RuntimeError(f"在 {root} 下没找到任何图片")
242
+
243
+ print(f"找到 {len(self.files)} 张图片")
244
+
245
+ # 扫描尺寸(多线程 + JSON 缓存)
246
+ sizes = self._scan_sizes(num_threads, cache_file)
247
+
248
+ # 分桶
249
+ self.bucket_map = {b: [] for b in buckets}
250
+ self.idx_to_bucket = {}
251
+ for idx, f in enumerate(self.files):
252
+ if f not in sizes:
253
+ continue
254
+ w, h = sizes[f]
255
+ b = find_bucket(w, h, buckets)
256
+ self.bucket_map[b].append(idx)
257
+ self.idx_to_bucket[idx] = b
258
+
259
+ # 打印桶分布
260
+ for b, idxs in self.bucket_map.items():
261
+ print(f" 桶 {b}: {len(idxs)} 张")
262
+
263
+ self.normalize = transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5])
264
+
265
+ def _scan_sizes(self, num_threads, cache_file):
266
+ # 1. 尝试从缓存加载
267
+ if cache_file and os.path.exists(cache_file):
268
+ print(f"从缓存加载尺寸: {cache_file}")
269
+ with open(cache_file) as f:
270
+ cache = json.load(f)
271
+ if set(cache.keys()) == set(self.files):
272
+ print(f"缓存有效,{len(cache)} 个尺寸")
273
+ return {f: tuple(s) for f, s in cache.items()}
274
+ print("缓存失效,重新扫描")
275
+
276
+ # 2. 多线程扫描
277
+ print(f"多线程扫描 {len(self.files)} 张图({num_threads} 线程)...")
278
+
279
+ def get_size(f):
280
+ try:
281
+ with Image.open(f) as im:
282
+ return f, im.size
283
+ except Exception:
284
+ return f, None
285
+
286
+ with ThreadPoolExecutor(num_threads) as ex:
287
+ results = list(ex.map(get_size, self.files))
288
+
289
+ sizes = {f: s for f, s in results if s is not None}
290
+ print(f"扫描完成: {len(sizes)} 张成功")
291
+
292
+ # 3. 保存缓存
293
+ if cache_file:
294
+ print(f"保存缓存: {cache_file}")
295
+ with open(cache_file, "w") as f:
296
+ json.dump({f: list(s) for f, s in sizes.items()}, f)
297
+ print(f"缓存已保存")
298
+
299
+ return sizes
300
+
301
+ def __len__(self):
302
+ return len(self.files)
303
+
304
+ def __getitem__(self, idx):
305
+ f = self.files[idx]
306
+ bh, bw = self.idx_to_bucket.get(idx, (256, 256))
307
+ try:
308
+ img = Image.open(f)
309
+ if getattr(img, "is_animated", False):
310
+ img.seek(0)
311
+ img = img.convert("RGB")
312
+ w, h = img.size
313
+ scale = max(bw / w, bh / h)
314
+ new_w = max(bw, math.ceil(w * scale))
315
+ new_h = max(bh, math.ceil(h * scale))
316
+ img = img.resize((new_w, new_h), Image.BICUBIC)
317
+ left = (new_w - bw) // 2
318
+ top = (new_h - bh) // 2
319
+ img = img.crop((left, top, left + bw, top + bh))
320
+ except Exception as e:
321
+ print(f"读取失败 {f}: {e}")
322
+ img = Image.new("RGB", (bw, bh), (0, 0, 0))
323
+
324
+ img = transforms.functional.to_tensor(img)
325
+ img = self.normalize(img)
326
+ return img, 0
327
+
328
+
329
+ class BucketBatchSampler(Sampler):
330
+ """
331
+ 分桶 batch 采样器:每个 batch 内所有图来自同一个桶(尺寸一致)。
332
+ 每个 epoch 重新打乱。
333
+ """
334
+ def __init__(self, dataset, batch_size, shuffle=True, drop_last=True):
335
+ self.dataset = dataset
336
+ self.batch_size = batch_size
337
+ self.shuffle = shuffle
338
+ self.drop_last = drop_last
339
+
340
+ def _make_batches(self):
341
+ batches = []
342
+ for bucket, indices in self.dataset.bucket_map.items():
343
+ indices = list(indices)
344
+ if self.shuffle:
345
+ random.shuffle(indices)
346
+ for i in range(0, len(indices), self.batch_size):
347
+ batch = indices[i:i + self.batch_size]
348
+ if len(batch) == self.batch_size or not self.drop_last:
349
+ batches.append(batch)
350
+ if self.shuffle:
351
+ random.shuffle(batches)
352
+ return batches
353
+
354
+ def __iter__(self):
355
+ return iter(self._make_batches())
356
+
357
+ def __len__(self):
358
+ # 估算
359
+ total = 0
360
+ for indices in self.dataset.bucket_map.values():
361
+ n = len(indices)
362
+ total += n // self.batch_size if self.drop_last else math.ceil(n / self.batch_size)
363
+ return total
364
+
365
+
366
+ # ============================================================
367
+ # 5. Dummy 数据集(不同分辨率,测试分桶)
368
+ # ============================================================
369
+
370
+ class DummyBucketDataset(Dataset):
371
+ """
372
+ 生成不同分辨率的随机图片,用于测试分桶逻辑。
373
+ 每张图随机选一个宽高比。
374
+ """
375
+ def __init__(self, size=200, buckets=None):
376
+ self.size = size
377
+ self.buckets = buckets
378
+ # 随机分配每张图到某个桶
379
+ self.assignments = []
380
+ for i in range(size):
381
+ b = random.choice(buckets)
382
+ self.assignments.append(b)
383
+
384
+ # 构建 bucket_map
385
+ self.bucket_map = {b: [] for b in buckets}
386
+ for idx, b in enumerate(self.assignments):
387
+ self.bucket_map[b].append(idx)
388
+
389
+ print(f"Dummy 数据集: {size} 张图")
390
+ for b, idxs in self.bucket_map.items():
391
+ print(f" 桶 {b}: {len(idxs)} 张")
392
+
393
+ self.normalize = transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5])
394
+
395
+ def __len__(self):
396
+ return self.size
397
+
398
+ def __getitem__(self, idx):
399
+ bh, bw = self.assignments[idx]
400
+ # 随机图,归一化到 [-1,1]
401
+ img = torch.randn(3, bh, bw) * 0.5
402
+ return img, 0
403
+
404
+
405
+ # ============================================================
406
+ # 6. 数据加载器
407
+ # ============================================================
408
+ def get_dataloader(args):
409
+ buckets = make_buckets(base=args.bucket_base, align=16)
410
+ print(f"桶列表: {buckets}")
411
+
412
+ if args.dummy:
413
+ ds = DummyBucketDataset(size=args.dummy_size, buckets=buckets)
414
+ else:
415
+ ds = BucketImageFolder(
416
+ args.data, buckets,
417
+ num_threads=2,
418
+ cache_file="./sizes_cache.json", # ← 关键
419
+ )
420
+
421
+ sampler = BucketBatchSampler(
422
+ ds, batch_size=args.batch_size, shuffle=True, drop_last=True
423
+ )
424
+
425
+ return DataLoader(
426
+ ds,
427
+ batch_sampler=sampler,
428
+ num_workers=args.num_workers,
429
+ pin_memory=torch.cuda.is_available(),
430
+ )
431
+
432
+ # ============================================================
433
+ # 7. 感知损失(可选)
434
+ # ============================================================
435
+
436
+ class SimplePerceptualLoss(nn.Module):
437
+ """
438
+ 轻量版感知损失:用 torchvision 的 VGG16 前几层特征。
439
+ """
440
+ def __init__(self):
441
+ super().__init__()
442
+ from torchvision import models
443
+ vgg = models.vgg16(weights=models.VGG16_Weights.DEFAULT).features
444
+ self.slice = nn.Sequential(*[vgg[i] for i in range(9)])
445
+ for p in self.parameters():
446
+ p.requires_grad = False
447
+ self.register_buffer('mean', torch.tensor([0.485, 0.456, 0.406]).view(1, 3, 1, 1))
448
+ self.register_buffer('std', torch.tensor([0.229, 0.224, 0.225]).view(1, 3, 1, 1))
449
+
450
+ def forward(self, x, y):
451
+ x = (x + 1) / 2
452
+ y = (y + 1) / 2
453
+ x = (x - self.mean) / self.std
454
+ y = (y - self.mean) / self.std
455
+ fx = self.slice(x)
456
+ fy = self.slice(y)
457
+ return F.l1_loss(fx, fy)
458
+
459
+ import lpips
460
+
461
+ class PerceptualLoss(nn.Module):
462
+ def __init__(self):
463
+ super().__init__()
464
+ self.lpips = lpips.LPIPS(net='squeeze').cuda().eval()
465
+ for p in self.lpips.parameters():
466
+ p.requires_grad = False
467
+
468
+ def forward(self, x, y):
469
+ # 输入 [-1,1],直接传
470
+ return self.lpips(x, y).mean()
471
+
472
+ def load_checkpointsimpe(model, optimizer, path, device='cuda'):
473
+ ckpt = torch.load(path, map_location=device)
474
+
475
+ model.load_state_dict(ckpt['model_state_dict'])
476
+ optimizer.load_state_dict(ckpt['optimizer_state_dict'])
477
+
478
+ start_epoch = ckpt['epoch'] + 1
479
+ print(f"从 epoch {ckpt['epoch']} 恢复,从 epoch {start_epoch} 继续")
480
+
481
+ return start_epoch
482
+
483
+ def load_checkpoint(model, optimizer, scheduler, scaler, path, device='cuda'):
484
+ ckpt = torch.load(path, map_location=device)
485
+
486
+ model.load_state_dict(ckpt['model_state_dict'])
487
+ optimizer.load_state_dict(ckpt['optimizer_state_dict'])
488
+
489
+ if 'scheduler_state_dict' in ckpt and scheduler is not None:
490
+ scheduler.load_state_dict(ckpt['scheduler_state_dict'])
491
+
492
+ if 'scaler_state_dict' in ckpt and scaler is not None:
493
+ scaler.load_state_dict(ckpt['scaler_state_dict'])
494
+
495
+ start_epoch = ckpt['epoch'] + 1
496
+ num_iter = ckpt.get('num_iter', 0)
497
+ print(f"恢复: epoch {ckpt['epoch']} → {start_epoch}, iter {num_iter}")
498
+
499
+ return start_epoch, num_iter
500
+
501
+ # ============================================================
502
+ # 8. 训练
503
+ # ============================================================
504
+
505
+ import time
506
+ import bitsandbytes as bnb
507
+ def train(args):
508
+ device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
509
+ os.makedirs(args.out_dir, exist_ok=True)
510
+ os.makedirs(os.path.join(args.out_dir, 'samples'), exist_ok=True)
511
+
512
+ loader = get_dataloader(args)
513
+
514
+ model = FSQVAE(levels=args.levels, channel=args.channel).to(device)
515
+ #if args.resume:
516
+ #start_epoch = load_checkpoint(model, optimizer, args.resume, device)
517
+ #加载预训练
518
+ n_params = sum(p.numel() for p in model.parameters())
519
+ print(f"模型参数量: {n_params/1e6:.2f} M")
520
+ print(f"FSQ levels: {args.levels}, 码本大小: {int(torch.tensor(args.levels).prod())}")
521
+ print(f"下采样倍率: 16×(4 层 stride=2)")
522
+
523
+ #optimizer = torch.optim.Adam(model.parameters(), lr=args.lr)
524
+ optimizer = bnb.optim.Adam8bit(model.parameters(), lr=args.lr)
525
+ scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(
526
+ optimizer, T_max=args.epochs * len(loader)
527
+ )
528
+
529
+ percep = None
530
+ if args.perceptual_weight > 0:
531
+ #percep = SimplePerceptualLoss().to(device).eval()
532
+ percep = PerceptualLoss().to(device).eval()
533
+ print("启用感知损失")
534
+
535
+ l1_fn = nn.L1Loss()
536
+ num_iter = 0
537
+
538
+ # 混合精度
539
+ scaler = torch.cuda.amp.GradScaler()
540
+
541
+ if args.resume:
542
+ start_epoch, num_iter = load_checkpoint(model, optimizer, scheduler, scaler, args.resume, device)
543
+ #start_epoch = load_checkpointsimpe(model, optimizer, args.resume, device)
544
+ for epoch in range(args.epochs):
545
+ model.train()
546
+ for imgs, _ in loader:
547
+ imgs = imgs.to(device)
548
+ num_iter += 1
549
+
550
+ #t1 = time.time()
551
+ with torch.cuda.amp.autocast():
552
+ recon = model(imgs)
553
+ l1 = l1_fn(imgs, recon)
554
+ loss = l1
555
+ #t2 = time.time()
556
+ #print(f"[前向] {t2-t1:.3f}s | 输入 {tuple(imgs.shape)}")
557
+
558
+ if percep is not None:
559
+ p_loss = percep(imgs, recon)
560
+ loss = loss + args.perceptual_weight * p_loss
561
+ #t3 = time.time()
562
+ #print(f"[感知] {t3-t2:.3f}s")
563
+
564
+ optimizer.zero_grad()
565
+ #loss.backward()
566
+ scaler.scale(loss).backward()
567
+ torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
568
+ #optimizer.step()
569
+ scaler.step(optimizer)
570
+ scaler.update()
571
+ scheduler.step()
572
+ #t4 = time.time()
573
+ #print(f"[反向] {t4-t3:.3f}s")
574
+
575
+ if num_iter % args.log_interval == 0:
576
+ msg = f"epoch {epoch} | iter {num_iter} | l1 {l1.item():.4f}"
577
+ if percep is not None:
578
+ msg += f" | percep {p_loss.item():.4f}"
579
+ print(msg)
580
+
581
+ if num_iter % args.sample_interval == 0:
582
+ with torch.no_grad(): #图片采样
583
+ n = min(8, imgs.shape[0])
584
+ grid = make_grid(
585
+ torch.cat([imgs[:n], recon[:n]]),
586
+ nrow=n,
587
+ )
588
+ save_image(
589
+ grid,
590
+ os.path.join(args.out_dir, 'samples', f'{num_iter}.jpg'),
591
+ normalize=True,
592
+ )
593
+
594
+ if num_iter % args.save_interval == 0 :
595
+ # ---- 保存(含完整状态)----
596
+ torch.save({
597
+ 'epoch': epoch,
598
+ 'num_iter': num_iter,
599
+ 'model_state_dict': model.state_dict(),
600
+ 'optimizer_state_dict': optimizer.state_dict(),
601
+ 'scheduler_state_dict': scheduler.state_dict(),
602
+ 'scaler_state_dict': scaler.state_dict(),
603
+ }, os.path.join(args.out_dir, f'ckpt_{epoch}.pt'))
604
+
605
+
606
+ # ============================================================
607
+ # 9. 参数
608
+ # ============================================================
609
+ ###改用bnb8bit训练
610
+ def get_args():
611
+ p = argparse.ArgumentParser()
612
+ # 数据
613
+ p.add_argument('--data', type=str, default=None, help='图片文件夹路径')
614
+ p.add_argument('--dummy', action='store_true', help='用随机数据测试')
615
+ p.add_argument('--dummy-size', type=int, default=200)
616
+ p.add_argument('--num-workers', type=int, default=1)
617
+ p.add_argument('--bucket-base', type=int, default=256,
618
+ help='桶基准尺寸,桶的总像素数接近 base²')
619
+ # 模型
620
+ p.add_argument('--levels', type=int, nargs='+', default=[8, 5, 5, 5],
621
+ help='FSQ levels,例如 8 5 5 5')
622
+ p.add_argument('--channel', type=int, default=128)
623
+ # 训练
624
+ p.add_argument('--epochs', type=int, default=10)
625
+ p.add_argument('--batch-size', type=int, default=32)
626
+ p.add_argument('--lr', type=float, default=1e-4)
627
+ p.add_argument('--perceptual-weight', type=float, default=0.1)
628
+ # 日志
629
+ p.add_argument('--log-interval', type=int, default=5)
630
+ p.add_argument('--sample-interval', type=int, default=500)
631
+ p.add_argument('--save-interval', type=int, default=1)
632
+ p.add_argument('--out-dir', type=str, default='./fsq_demo_out')
633
+
634
+ p.add_argument('--resume', type=str, default=None,
635
+ help='检查点路径,如 ./fsqsmall_out/ckpt_0.pt;None 表示从头训练')
636
+ return p.parse_args()
637
+
638
+
639
+
640
+
641
+ ####
642
+
643
+ '''
644
+ #小模型配置 1mb
645
+ !python simplefsq32.py \
646
+ --data output_256/ \
647
+ --epochs 10 \
648
+ --batch-size 64 \
649
+ --channel 128 \
650
+ --levels 8 5 5 5 \
651
+ --bucket-base 256 \
652
+ --lr 1e-4 \
653
+ --perceptual-weight 0.1 \
654
+ --log-interval 50 \
655
+ --sample-interval 500 \
656
+ --save-interval 500 \
657
+ --out-dir ./fsqsmall_out
658
+
659
+ #中等 6mb模型测试
660
+
661
+ !python simplefsq1.py \
662
+ --data output_256/ \
663
+ --epochs 1 \
664
+ --batch-size 8 \
665
+ --channel 256 \
666
+ --levels 7 5 5 5 5 \
667
+ --bucket-base 256 \
668
+ --lr 1e-4 \
669
+ --perceptual-weight 0.1 \
670
+ --log-interval 50 \
671
+ --sample-interval 500 \
672
+ --save-interval 500 \
673
+ --out-dir ./fsq_out
674
+ '''
675
+ # ============================================================
676
+ # 10. 入口
677
+ # ============================================================
678
+
679
+ if __name__ == '__main__':
680
+ args = get_args()
681
+ print(args)
682
+ train(args)