
新手学PyTorch第一个卡壳的地方往往不是模型怎么写而是数据集怎么弄到手。MNIST作为最经典的入门数据集很多人第一次跑教程就挂在数据加载这一步torchvision下载半天报404、网络超时、文件损坏心态直接裂开。其实MNIST的获取无非两条路——让torchvision自动下载或者手动下载到本地再读取。这篇文章把两种方式从头到尾掰开揉碎讲清楚附带常用的可视化代码和踩坑记录适合刚装上PyTorch还没跑通第一个训练脚本的初学者。看完你就能理解数据集在PyTorch里的来龙去脉以后换CIFAR、ImageNet这类数据集也能举一反三。1. 先搞清楚MNIST到底是什么1.1 数据长什么样MNIST全称是Modified National Institute of Standards and Technology最早从美国国家标准与技术研究院收集的手写数字样本里整理而来是计算机视觉领域流传最广的入门数据集之一。它包含0到9共10个类别的灰度手写数字图片训练集有60000张测试集有10000张。每张图片的尺寸是28×28像素单通道灰度图。灰度图的每个像素只有一个数值范围0到2550表示纯黑255表示纯白中间值是各种深浅的灰。单张28×28的图片展开成一维就是784个数字用普通文本格式存储的话一张图不算标签只算像素就是784字节六万张训练图裸数据就要接近47MB再加上一些头部信息和标签整个数据集压缩包大概几十MB这也是它能如此广泛传播的原因——体积小、下载快、单张图简单稍弱的CPU都能跑起来。MNIST的图片是扁平的一维数组没有长宽的概念官方文件里存的时候按行排列。28×28的矩阵按行优先展平第n个像素对应原图中的第n//28行、第n%28列。这个映射关系在可视化时必须搞清楚画图前把784维的向量reshape成28×28否则画出来就是一堆乱码。1.2 为什么新手都从它开始MNIST在深度学习领域的地位类似编程里的Hello World。它足够小几百个样本就能完成一次训练迭代几秒钟就能跑完一个epoch方便快速验证模型的正确性。而且它不需要额外的预处理下载下来就能直接用不像ImageNet那样还要做各种尺寸裁剪和归一化才能喂进网络。MNIST的官方文件格式也是很多数据集工具的通用模板。PyTorch的torchvision.datasets模块在实现MNIST时使用了一套标准的解析流程——先检测本地有没有缓存文件没有就尝试下载下载完再解压解析成内存中的张量。你把它搞明白了CIFAR-10、FashionMNIST、SVHN等数据集的加载逻辑也都是同一套路测试集、训练集、标签文件、图像文件的结构大同小异。2. 准备工作环境配置与镜像源设置2.1 确认PyTorch和torchvision都装好了网上下载MNIST依赖torchvision这个视觉工具库它是PyTorch官方出的扩展包datasets、transforms、models这些常用的视觉组件都在里面。很多人只装了torch没装torchvision跑到import torchvision就报ModuleNotFoundError。确认是否装好直接在Python环境里执行import torch import torchvision print(torch.__version__) print(torchvision.__version__)能打印出两个版本号就说明环境没问题。如果报错先装torchvisionpip install torchvision这里有个容易忽略的点torchvision和torch有严格的版本对应关系版本差太多可能会触发底层警告甚至报错。比如torch 2.0配torchvision 0.15是配套的torch 2.1要配torchvision 0.16。装的时候最好用pip install torchvision --upgrade让pip自动挑选兼容版本或者直接用pip install torch torchvision一起装省得自己配。如果用的conda环境也可以conda install pytorch torchvision cpuonly -c pytorch没有NVIDIA显卡或者没装CUDA的同学装CPU版就够了跑MNIST根本不需要显卡CPU几十秒就能完成一个epoch的训练。2.2 给torchvision配国内镜像源torchvision的MNIST默认下载地址在GitHub的release文件托管上国内直连这个地址经常超时或者直接404这也是很多教程跑不通的核心原因。解决办法是配置国内镜像源。推荐清华大学的PyPI镜像在终端里临时指定或者写进pip配置都可以。临时指定的方式pip install torchvision -i https://pypi.tuna.tsinghua.edu.cn/simple永久配置的话在用户目录下找到pip.iniWindows或pip.confLinux/macOS写入[global] index-url https://pypi.tuna.tsinghua.edu.cn/simple但注意pip的镜像源解决的是包安装问题而torchvision下载MNIST数据时用的是它自己内置的URL。镜像源对torchvision内部的下载不一定生效这个问题下面第3章详细讲。实际项目中最稳妥的方式是手动下载数据文件再让代码读取这也是标题里本地读取这种方式的主要价值。3. 方式一让torchvision自动下载3.1 标准写法与参数详解网上下载方式的核心是torchvision.datasets.MNIST这个类。最简单的代码如下import torch from torchvision import datasets, transforms # 定义预处理转张量 标准化 transform transforms.Compose([ transforms.ToTensor(), # 将PIL图像或numpy数组转为Tensor并把像素值从0~255缩放到0~1 transforms.Normalize((0.1307,), (0.3081,)) # 用MNIST数据集的全局均值和标准差做标准化 ]) # 下载并加载训练集 train_dataset datasets.MNIST( root./data, # 数据保存到当前目录下的data文件夹 trainTrue, # True加载训练集False加载测试集 transformtransform, # 对图像做的预处理 downloadTrue # 本地没有数据时就下载 ) # 下载并加载测试集 test_dataset datasets.MNIST( root./data, trainFalse, transformtransform, downloadTrue ) print(f训练集样本数: {len(train_dataset)}) print(f测试集样本数: {len(test_dataset)})几个参数逐个说清楚root是数据保存的根目录代码会在root下自动创建MNIST/raw目录把下载的原始压缩包放到raw里再生成一个MNIST/processed目录存放处理好的数据。第二次运行代码时downloadTrue也会先检查processed目录是否存在存在就直接加载不会再重新下载。train参数决定加载的是训练集还是测试集。训练集和测试集的文件是分开的下载时各下一份。transform是一个变换流水线PyTorch里每次取一个样本时都会对图像依次执行这些变换。常见的变换有transforms.ToTensor()、transforms.Normalize()、transforms.Resize()、transforms.RandomCrop()等。这里用ToTensor把PIL图像转成TensorNormalize把像素分布标准化让均值接近0、方差接近1神经网络训练更容易收敛。download参数最坑设为True时如果本地没有processed文件它就去网上拉原始压缩包。如果你的网络访问GitHub不畅这里就会卡住或报错。3.2 downloadTrue时到底发生了什么datasets.MNIST在下载过程中做这些事先检查root/MNIST/processed/training.pt和test.pt是否存在如果存在直接从.pt文件加载跳过下载如果不存在检查root/MNIST/raw目录下有没有四个.gz文件训练图像、训练标签、测试图像、测试标签缺哪个就下载哪个使用torchvision内置的URL全部下载完成后解析成张量缓存为.pt文件。404错误的根源就在这里——默认的下载地址是https://github.com/myleott/mnist_py/blob/master/train-images-idx3-ubyte.gz?rawtrue这类GitHub地址在某些网络环境下非常不稳定要么直接404要么下载速度只有几KB/s下到一半超时留下一个残缺的文件占着坑下次再试还是失败。针对这个问题可以通过为MNIST对象手动指定下载地址来绕过。torchvision的download_url支持镜像参数新版torchvision里datasets.MNIST的download操作会调用torchvision.datasets.utils.download_url而download_url内部会用urlopen去请求地址。你可以先手动下载这四个gz文件把它们放到root/MNIST/raw目录下再让downloadTrue去检测——如果raw目录里已有完整文件它就不会再请求网络。实际上很多经历过这种折磨的PyTorch玩家后来都会直接转用本地读取的方式绕开网络这个不稳定因素。3.3 网上下载失败的排查思路遇到下载失败、404 Not Found这类的报错时按顺序检查这几样第一看raw目录里有没有残留的0字节或几十KB的小文件。下载中断会生成不完整的临时文件程序检测到文件存在就不再下载于是永远卡在损坏文件上。解决方法是把root/MNIST/raw目录整个删掉重新跑。第二检查torchvision版本。部分老版本0.8、0.9的MNIST下载逻辑里写死了旧的URL已经失效建议升级到0.13以上的版本或者直接新装torchvision 0.15。第三试一下手动访问默认URL看浏览器能不能打开。打不开就别折腾了直接切到第4章的本地读取方式。第四如果你的环境有代理但代理配置不正确也会下载失败。这类问题在新手期特别常见先用curl -I url先测一下网络连通性再定位。4. 方式二手动下载到本地再读取4.1 手动获取MNIST原始文件本地读取方式的思路很简单自己去网上下载官方原始文件放到PyTorch能识别的位置然后在代码里解析它。手动下载的好处是把下载和解析两个步骤分开网络断了也能离线完成而且下载一次以后可以永久复用。官网MNIST原始数据有四个文件都用gzip压缩文件名内容大小约train-images-idx3-ubyte.gz训练集图像60000×28×2847MBtrain-labels-idx1-ubyte.gz训练集标签60000个59KBt10k-images-idx3-ubyte.gz测试集图像10000×28×287.9MBt10k-labels-idx1-ubyte.gz测试集标签10000个9.8KB可以到MNIST官网或各大镜像站下载。下载完后在项目目录下创建data/MNIST/raw文件夹把这四个gz文件放进去目录结构长这样你的项目目录/ └── data/ └── MNIST/ └── raw/ ├── train-images-idx3-ubyte.gz ├── train-labels-idx1-ubyte.gz ├── t10k-images-idx3-ubyte.gz └── t10k-labels-idx1-ubyte.gz这个目录结构不是随便定的它就是torchvision默认的MNIST存放布局。放在这个位置后续用downloadTrue时程序检测到raw里已经有文件就不会再联网。4.2 用python直接解析idx格式MNIST的文件格式简单粗暴就是一个二进制头部加一堆原始数据。图像文件的头部是32字节4个int32大端整数前4字节是魔数magic number固定为2051表示整数图像文件接着4字节是样本数量再4字节是行数28最后4字节是列数28。头部之后就是像素数据按uint8顺序排列。标签文件的头部是8字节2个int32前4字节魔数2049表示标签文件接着4字节是标签数量之后就是uint8的标签值。用Python加标准库gzip就能解析。写一个函数load_mnist_images和load_mnist_labelsimport gzip import numpy as np import torch import os def load_mnist_images(image_path): 读取IDX格式的MNIST图像文件返回float32类型的Tensor值范围0~1 with gzip.open(image_path, rb) as f: raw f.read() # 前4字节是魔数第5~8字节是样本数第9~12字节是行数第13~16是列数 num_images int.from_bytes(raw[4:8], big) rows int.from_bytes(raw[8:12], big) cols int.from_bytes(raw[12:16], big) # 从第16字节开始是像素数据每个像素1字节共num*rows*cols个 data np.frombuffer(raw[16:], dtypenp.uint8) data data.reshape(num_images, rows * cols) # 转成torch tensor并缩放到0~1 tensor torch.tensor(data, dtypetorch.float32) / 255.0 return tensor # 形状[样本数, 784] def load_mnist_labels(label_path): 读取IDX格式的MNIST标签文件返回int64类型的Tensor with gzip.open(label_path, rb) as f: raw f.read() num_labels int.from_bytes(raw[4:8], big) data np.frombuffer(raw[8:], dtypenp.uint8) return torch.tensor(data, dtypetorch.long) # 形状[样本数] base_dir ./data/MNIST/raw train_images load_mnist_images(os.path.join(base_dir, train-images-idx3-ubyte.gz)) train_labels load_mnist_labels(os.path.join(base_dir, train-labels-idx1-ubyte.gz)) test_images load_mnist_images(os.path.join(base_dir, t10k-images-idx3-ubyte.gz)) test_labels load_mnist_labels(os.path.join(base_dir, t10k-labels-idx1-ubyte.gz)) print(train_images.shape, train_labels.shape) print(test_images.shape, test_labels.shape)运行后输出类似torch.Size([60000, 784]) torch.Size([60000])说明解析成功。注意图像Tensor的值范围已经从0~255变成了0~1这是因为除以了255.0。这个缩放很重要不缩放直接喂给网络数值范围太大Loss下降会很慢还会导致梯度爆炸。我一般还会把raw目录的路径写成一个全局变量这样不同项目里复用起来方便。如果你的项目目录改了只需要改base_dir就行。4.3 封装成PyTorch的Dataset类上面的tensor加载进内存后可以直接用下标访问但要真正融入PyTorch的训练流程最好封装成torch.utils.data.Dataset的子类。Dataset类是PyTorch数据管道的第一环它决定了怎么拿到一个样本。封装之后可以配合DataLoader做批量读取、打乱顺序、多线程加载训练代码才干净。from torch.utils.data import Dataset, DataLoader class MyMNIST(Dataset): 自定义MNIST数据集类支持可选的transform def __init__(self, images, labels, transformNone): self.images images # shape: [N, 784] self.labels labels # shape: [N] self.transform transform def __len__(self): return len(self.labels) def __getitem__(self, idx): image self.images[idx] # 形状[784] label self.labels[idx] # 如果需要把一维向量变成28x28的二维图像方便做数据增强 if self.transform: image self.transform(image) return image, label # 使用示例 train_dataset MyMNIST(train_images, train_labels) test_dataset MyMNIST(test_images, test_labels) train_loader DataLoader(train_dataset, batch_size64, shuffleTrue, num_workers2) print(f训练集一个batch大小: {next(iter(train_loader))[0].shape}) # 输出: torch.Size([64, 784])封装后本地读取方式和torchvision自带的MNIST在使用体验上就完全一致了后面可以无缝切换到同样的训练代码。如果后续想对图像做随机旋转、平移、裁剪这些数据增强只需要在MyMNIST的transform参数里传入transforms.Compose([...])即可。4.4 两种方式怎么选网上下载方式适合网络畅通、追求简单的人。一行downloadTrue搞定所有下载和解析代码量最少只需处理三次几乎所有教程都用这种方式演示。本地读取方式适合网络环境不稳定、需要离线使用、或者想理解底层原理的人。它把数据获取和模型训练彻底解耦下载一次可以永久使用而且无所谓网络环境。很多生产环境跑模型时数据文件都是提前放到NAS或对象存储上代码里走的就是类似本地方案。实际开发中我见过很多人两种都备着团队里写统一的下载脚本先尝试自动下载失败则提示手动下载放指定目录。这种自动为主、手动兜底的做法最稳。5. 数据可视化看看你手里的数据5.1 单张图展示读取完数据后第一件事就是可视化确认数据没读错。用matplotlib画一张图注意把784维的向量reshape成28×28再显示import matplotlib.pyplot as plt # 取训练集第0张图 image train_images[0].reshape(28, 28) label train_labels[0].item() plt.imshow(image, cmapgray) plt.title(fLabel: {label}) plt.axis(off) plt.show()如果图像显示出来是一个歪歪扭扭的5或者7说明数据解析正确。如果图案像雪花一样乱大概率是reshape时尺寸或者顺序搞错了。cmapgray参数必须指定否则matplotlib默认用彩色映射显示灰度图出来是花花绿绿的容易误导自己。5.2 网格图批量展示一次看一张太慢可以拼一个网格图比如3行3列展示前9张图fig, axes plt.subplots(3, 3, figsize(6, 6)) for i in range(9): ax axes[i // 3, i % 3] img train_images[i].reshape(28, 28) ax.imshow(img, cmapgray) ax.set_title(fLabel: {train_labels[i].item()}) ax.axis(off) plt.tight_layout() plt.show()也可以随机抽9张看看不同数字的长相。这种批量展示在调试阶段特别有用——模型训练完之后你也可以用同样的方式展示模型的预测结果对比预测标签和真实标签一眼看出模型错在哪。5.3 类别分布统计图MNIST十类样本数量是否均衡直接影响模型训练效果。画一个标签分布柱状图确认每个数字的样本数大致都在六千张左右from collections import Counter train_label_counts Counter(train_labels.numpy()) test_label_counts Counter(test_labels.numpy()) fig, axes plt.subplots(1, 2, figsize(12, 4)) # 训练集分布 axes[0].bar(train_label_counts.keys(), train_label_counts.values()) axes[0].set_title(Train Label Distribution) axes[0].set_xlabel(Digit) axes[0].set_ylabel(Count) # 测试集分布 axes[1].bar(test_label_counts.keys(), test_label_counts.values()) axes[1].set_title(Test Label Distribution) axes[1].set_xlabel(Digit) axes[1].set_ylabel(Count) plt.tight_layout() plt.show()MNIST各类别样本数量基本均衡所以不需要做类别加权。但如果你后面换成别的数据集类别分布图就要重点关注不均衡时需要在采样器或Loss函数里做处理。5.4 像素值分布图MNIST这类灰度图像素值大部分集中在0纯黑背景和255纯白笔画两端中间值比较少。画一个直方图验证一下plt.hist(train_images.numpy().flatten(), bins50, colorgray, alpha0.7) plt.xlabel(Pixel value) plt.ylabel(Frequency) plt.title(MNIST Pixel Value Distribution) plt.show()如果你看到分布集中在0附近、少部分在高位说明数据正常。但注意因为显示之前已经除以了255像素值范围是0~1直方图横轴看起来会集中在0和1两端。如果你像我一样做了归一化处理横轴会在-0.42到2.82附近0.1307和0.3081变换后的结果那是另一番图景。归一化对可视化的影响容易被忽视Normalize之后图像不再是人眼能直观理解的0~255灰度范围此时如果直接imshow很多细节被压缩到不明显的区间图会显得非常暗甚至发灰。验证时如果想像原始数据一样直观展示需要反归一化def unnormalize(img_tensor, mean0.1307, std0.3081): # img_tensor: 已经归一化的图像 img img_tensor * std mean # 先还原到0~1 img img.clamp(0, 1) # 保险起见裁剪到合法区间 return img.numpy()这个细节在调试可视化结果时非常重要不然你精心画出来的训练样本图全是一片灰度搞不清是数据问题还是展示问题。6. 实操进阶数据管道里的几个实用技巧6.1 用DataLoader做批量加载和打乱拿到了Dataset还不能直接循环训练。深度学习训练是小批量梯度下降打法每次喂一小撮样本而不是一次过全部数据。DataLoader就是干这个活的from torch.utils.data import DataLoader train_loader DataLoader( datasettrain_dataset, batch_size64, shuffleTrue, # 每个epoch开始前打乱顺序 num_workers2, # 用2个子进程预加载数据 drop_lastFalse # 最后一批样本不足64时也保留 ) for batch_images, batch_labels in train_loader: print(fbatch_images: {batch_images.shape}, batch_labels: {batch_labels.shape}) # 跑一次前向传播...shuffle这个参数很关键。如果训练时每个epoch都用相同的顺序喂数据模型会记住顺序而不是学到真正的特征导致泛化能力变差。一般训练集shuffle设为True测试集通常设为False因为测试时不需要打乱。num_workers是在后台预先读取下一个batch数据的子进程数。Windows上如果num_workers设太高容易报错或者内存占用飙升建议设0或2跑起来再慢慢调。Jupyter Notebook里有的环境设num_workers0会卡住遇到这种直接设0就行。6.2 把原始文件做成备份MNIST数据下载过一次之后强烈建议把四个gz文件拷到一个独立的backup目录里不要只留在项目data文件夹中。因为data文件夹随时可能被你rm -rf清理掉backup目录才是真正的离线数据源。实践里我习惯在项目里放一个fetch_mnist_data.py脚本专门负责从镜像站下载并解压到本地之后所有模型代码都从本地固定目录读取。这个脚本会做三件事检查目录是否存在用gzip校验文件完整性把文件移动到raw目录。这样即使哪天有别的项目要用MNIST直接去backup目录拿就行不用再经历一次下载的折磨。6.3 手工构造一份迷你MNIST做调试在实际训练之前我强烈建议手工抽取一小部分数据来验证整套代码能不能跑通。别一上来就60k样本全部喂进去——万一数据处理逻辑错了跑了一小时才发现心态全崩。最快的验证方式是用切片mini_train MyMNIST(train_images[:1000], train_labels[:1000]) mini_loader DataLoader(mini_train, batch_size32, shuffleTrue) # 先在上面跑一个极简的线性模型 import torch.nn as nn model nn.Linear(784, 10) criterion nn.CrossEntropyLoss() optimizer torch.optim.SGD(model.parameters(), lr0.01) for images, labels in mini_loader: outputs model(images) loss criterion(outputs, labels) optimizer.zero_grad() loss.backward() optimizer.step() print(loss.item()) break # 只跑一个batch就停如果这个batch能正常前向、反向、更新参数再换全量数据跑就放心多了。6.4 关于模型的输入形状MNIST图像有两种常见的输入形态一种是展平后的784维向量适合全连接网络。前两章的代码里图像本来就是[60000, 784]的形状这种形态最适合入门。另一种是保留二维结构的[1, 28, 28]或[3, 28, 28]适合卷积神经网络。如果想要CNN输入需要把图像reshape成[batch, 1, 28, 28]# 使用之前加载的train_images形状[60000, 784] train_images_cnn train_images.reshape(-1, 1, 28, 28)这里的-1表示自动推断1是通道数灰度图单通道28和28是高和宽。主流的LeNet、CNN模型在处理MNIST时都是按后一种形态接收输入的。你在网上看到别人代码里x x.view(x.size(0), 1, 28, 28)做的就是这件事。7. 常见问题速查与避坑指南7.1 报错清单报错/问题原因解决方式HTTP Error 404torchvision内置下载地址失效或网络受限手动下载四个gz文件放入raw目录timed out网络不稳定下载超时手动下载本地读取或配置镜像源RuntimeError: File not foundraw目录缺少文件或路径不对检查目录结构是否为data/MNIST/raw/xxx.gzOSError: [Errno 22] Invalid argumentWindows上路径分隔符或文件名大小写问题用正斜杠拼接路径检查文件名是否少了下划线图像显示乱码reshape尺寸或顺序错误确保按28×28、行优先顺序reshape像素值出现负值做了Normalize之后直接可视化归一化后需要反变换或裁剪到0~1再显示第二次运行还在重新下载processed缓存文件缺失检查processed目录下training.pt是否存在DataLoader worker (pid(s) xx) exited unexpectedlynum_workers设置过高把num_workers设为0再试7.2 404报错的系统解法404是新手最常遇到的坑。根因是torchvision的MNIST默认下载地址在特定网络环境下无法访问。处理步骤先删掉raw目录下所有残留文件确保目录是空的。浏览器访问官方MNIST镜像地址一个个下载四个gz文件。在项目下创建data/MNIST/raw目录严格按这个结构放文件。重新运行代码不要设置downloadFalse让它去检测本地文件。如果代码还是尝试从网络下载检查torchvision源码确认是否有URL写死——新版torchvision的部分接口对已有文件会优先复用本地文件。这个解法的核心思想是永远保留一份应急本地数据不要依赖网络下载这条路走通。7.3 一个容易忽略的坑标签的dtypeload_mnist_labels里返回的Tensor用的dtype是torch.long即int64因为PyTorch的交叉熵损失CrossEntropyLoss要求label必须是LongTensor。如果你的label是float32训练时会直接报错Expected dtype torch.long but got dtype torch.float如果你不确定自己的label是什么类型可以打印一下train_labels.dtype确认。同理图像Tensor一般用float32因为神经网络权重都是float32输入也必须是float32uint8不可直接与权重相乘。很多人从numpy读出uint8数据后忘了转float32直接丢进模型报类型错误又折腾半天。7.4 torchvision缓存目录与磁盘清理torchvision除了在root目录下写数据还会在系统缓存目录存一些下载临时文件。如果你很久没跑过MNIST相关的代码又突然报磁盘空间不足可以去这几个位置看看有没有残留的trafficWindowsC:\Users\用户名\.cache\torchLinux/macOS~/.cache/torch这些缓存文件删掉不影响后续使用下次用的时候重新下载就行。新版本的torchvision还会在root下生成.torchvision之类的隐藏文件同样可以定期清理。8. 写在最后的一点经验回想我最初跑通MNIST的过程最崩溃的不是模型调参反而是数据到底有没有读对这件事。第一次用torchvision下载404报错折腾了一下午后来改为手动下载到本地终于把数据握在自己手里心里踏实了后面所有实验都顺了。我个人的体会是初学阶段不要过度依赖一键下载的便利性。手动走一遍下载、解压、解析、可视化、封装的完整流程你对数据集的内部结构会有一个非常扎实的认知。后面无论换什么数据集拿到文件先看格式、再写解析、再可视化确认这套流程几乎能帮你避开八成以上的数据坑。最后再分享一个小技巧拿到MNIST数据后先把可视化跑出来眼睛确认数据没问题再写模型。不要跳步——很多看似玄学的训练失败追到根子上都是数据解析时少了一个字节、多了一个偏移。数据是地基地基歪了上面再漂亮的模型也白搭。祝你把MNIST稳稳拿下接下来不管往CNN还是Transformer方向走这篇笔记里的习惯都能一直有用。