【深度学习】PyTorch学习笔记

发布于:2025-06-26 ⋅ 阅读:(21) ⋅ 点赞:(0)

 一,Torch基础及其线性回归实战

"""
此部分是关于torch张量知识的总结
1,覆盖了torch框架如何创建一个tensor,以及对tensor进行切片操作
2,介绍了gpu相关api,从而加速矩阵计算

"""

import torch
import matplotlib.pyplot as plt

if False:
    # 定义一个tensor(张量)  5行三列,数值随即
    x = torch.rand(5, 3)

    print(x)
    # 0行0列
    print(x[0, 0])

    # 最后一列全部元素
    print(x[:, -1])

    # 第一行,1到最后一列元素
    print(x[0, 1:])

    # 定义一个举证,5x3,初始值全为1
    print(torch.ones(5, 3))
    # tensor([[1., 1., 1.],
    #         [1., 1., 1.],
    #         [1., 1., 1.],
    #         [1., 1., 1.],
    #         [1., 1., 1.]])

    # 同尺寸矩阵操作,相加
    print(torch.ones(5, 3) + x)

    # 矩阵相惩   [5,3] * [3,5] ---》 [5,5]        tensor.t()是对矩阵转置
    print(x.mm(torch.ones(5, 3).t()))

    #  torch 张量与numpy相似,其重复轮子的优势在于,torch能在GPU上计算,而numpy不能
    #  输出是否能使用gpu
    print(torch.cuda.is_available())
    if torch.cuda.is_available():
        # x.cuda() 即将张量放置到gpu上,进而可以使用gpu计算
        print(x.cuda().mm(torch.ones(5, 3).cuda().t()))

        # 此命令是将数据从gpu卸载到cpu
        x.cuda().cpu()

"""
神经网络 反向传播算法 更新内部神经元,提高训练效率
目前大多数深度学习采用了 计算图 技术,计算图是一个有向图,方形节点是变量,圆形节点表示运算
核心思想:记录正向计算步骤,只要此步骤可微分(求导),这自动计算每个变量的梯度
"""

# PyTorch借助自动微分变量实现了 动态计算图, PyTorch会保存运算路径,从而通过backword进行反向传播
if False:
    #  requires_grad=True,保证可以在执行反向传播算法的过程中,获得梯度信息
    x = torch.ones(2, 2, requires_grad=True)
    print(x)
    y = x + 2
    print(y)
    print(y.grad_fn)
    # y.grad_fn存储了运算信息,AddBackward0,它是计算图上一个运算节点
    # tensor([[3., 3.],
    #         [3., 3.]], grad_fn= < AddBackward0 >)
    # 从而得到一个计算图如下
    # [x]<---(+2)<---[y]

    z = y * y
    print(z)
    print(z.grad_fn)
    # tensor([[9., 9.],
    #         [9., 9.]], grad_fn= < MulBackward0 >)
    # 计算图如下
    # [x]<---(+2)<---[y]
    #         ↑
    #         |
    #         |
    #        (*) <----[z]

    # 对z求平均,即矩阵元素求和后除总数
    t = torch.mean(z)
    print(t)
    # .data存储计算结果
    print(t.data)
    # tensor(9., grad_fn=<MeanBackward0>)
    # 计算图如下
    # [x]<---(+2)<---[y]
    #         ↑
    #         |
    #         |
    #        (*) <----[z]
    #         ↑
    #         |
    #        (mean)<---[t]

    """
    进一步求导,PyTorch通过backward方法求导并且自动运行反向传播算法
    x.grad保存了梯度信息,即求导,
    只有叶节点才保存有grad信息,因此以下计算只有x存在值
    """
    t.backward()
    print(z.grad)
    # None
    print(y.grad)
    # None
    print(x.grad)
    # dt/dx值如下
    # tensor([[1.5000, 1.5000],
    #         [1.5000, 1.5000]])

    # 进一步理解backward实例
    s = torch.tensor([[0.01, 0.02]], requires_grad=True)
    x = torch.ones(2, 2, requires_grad=True)
    for i in range(10):
        s = s.mm(x)
    z = torch.min(s)

    z.backward()
    print(x.grad)
    # dz/dx
    # tensor([[37.1200, 37.1200],
    #         [39.6800, 39.6800]])

    # s非叶节点了
    print(s.grad)
    # None

"""
实战一:预测房价
本篇,将通过神经网络算法,对房价进行线性回归预测
"""
if True:
    # 时间变量
    # 生成100个 0~100等距数
    x = torch.linspace(0, 100, 100).type(torch.FloatTensor)

    # randn 正态分布,均值为0,方差为10
    rand = torch.randn(100) * 10
    y = rand + x

    x_train = x[: -10]
    x_test = x[-10:]
    y_train = y[:-10]
    y_test = y[-10:]

    # 设定窗口大小为10x8
    plt.figure(figsize=(10, 8))
    plt.xlabel("X")
    plt.ylabel("Y")
    # plt.plot(x_train.data.numpy(), y_train.data.numpy(), 'o')
    # plt.show()

    # 求解拟合函数 y = ax + b

    # L(a,b)梯度优化,

    a = torch.rand(1, requires_grad=True)
    b = torch.rand(1, requires_grad=True)
    learning_rate = 0.0001

    # 迭代学习1000次
    for i in range(1000):
        # y = ax + b, expand_as是扩大ab尺寸与训练集匹配
        predictions = a.expand_as(x_train) * x_train + b.expand_as(x_train)

        # 定义损失函数,
        # L = mean((y1-y2)^2) ,mean = 求均值,即 L =   1/N * ∑(y - yi)^2  ,可以理解为实际值与预测值差值求和
        loss = torch.mean((predictions - y_train) ** 2)

        print(f"loss:{loss}")

        # 对损失函数进行梯度反转
        # 这一步很关键,在于能够计算出dy/dx,并且根据梯度,调整ab参数,使得更加拟合训练集,
        # backward,反向传播算法,神经网络高效训练的核心,
        loss.backward()

        # 利用上一步计算结果,更新ab的data值
        # add_表示需要变化值当前变量,没有_则是返回新值
        a.data.add_(-learning_rate * a.grad.data)
        b.data.add_(-learning_rate * b.grad.data)

        # 清空ab梯度值
        a.grad.data.zero_()
        b.grad.data.zero_()

    print(f"a = {a},b = {b}")

    # 绘制拟合曲线
    # 训练集xy分布图
    xplot = plt.plot(x_train.data.numpy(), y_train.data.numpy(), 'o')
    # 拟合线 y = ax+b
    yplot = plt.plot(x_train.data.numpy(), a.data.numpy() * x_train.data.numpy() + b.data.numpy())
    plt.xlabel("X")
    plt.ylabel("Y")
    plt.legend([xplot, yplot], ['Data', str(a.data.numpy()[0]) + 'x +' + str(b.data.numpy()[0])])
    # plt.show()

    # 根据已经训练的模型,进行预测
    predictions = a.expand_as(x_test) * x_test + b.expand_as(x_test)

    # 绘制测试数据
    plt.plot(x_test.data.numpy(), y_test.data.numpy(), 's')
    # 绘制预测曲线
    plt.plot(x_test.data.numpy(), predictions.data.numpy(), 's')
    plt.show()

    print(predictions)

二,神经网络定义实战

"""
此部分通过实例进一步学习神经网络,
Question:存在一年数据,三个变量(时间t月日,地点w,单车数量c)共享单车某时刻t在某地的数量c,
通过构建一个简单的神经网络,预测未来该地共享单车数量,以解决共享单车投放问题

本篇中,将会学到什么是神经网络,如何构建一个神经网络,什么是过拟合,怎么解决过拟合,以及激活函数、机器学习等基本概念。
"""
import torch.nn
from torch.utils.data import DataLoader
from torchvision.datasets import MNIST
from torchvision.transforms import ToTensor

"""
神经网络,可以说是任何函数的通用拟合器,即通过多项式分解,可以映射到任意函数,而这些多项式的参数,即神经网络概念
输入 -> [神经网络(输入层、隐含层、输出层)] -> 特征值输出


假定一个全连接层神经网络如下
                [w11  w12           ....                  w1n]
                [                                            ]
                [                                            ]
输入->特征向量 *  [                                            ]  -> 输出特征向量
                [                                            ]
                [                                            ]
                [                                            ]
                [wm1  wm2          ....                   wmn]
                输入层               隐含层                 输出层

神经网络的学习,即获得一组矩阵参数,与输入矩阵做矩阵相乘,即可得到输出矩阵,来实现输入->输出的预测

除了以上全连接型(每一列与下一列是一一连接的),还有卷积神经网络(通过卷积提取特征值,在通信、图像、信号领域)


那么如何得到一组较好的w参数呢,便是训练,即通过输入、预测结果y、实际结果yi,
下一步,即定义一个损失函数L,使得预测值和实际值差值变小,比如 L =   1/N * ∑(y - yi)^2
通过对dL/dx求导,进一步调整w参数,就是不断拟合、即梯度下降的过程
Torch能对损失函数L.backward即反向传播算法,得到一个dL/dx,保存了grad,通过此参数调整w
"""

# # 因为没有训练集,这里以MNIST数据集为例,
# # 用途:手写数字分类(0-9,共 10 类)。
# # 样本数量:60,000 个训练样本,10,000 个测试样本。
# # 数据类型:灰度图像,大小为 28x28。
train_dataset = MNIST(root='data', train=True, download=True, transform=ToTensor())
test_dataset = MNIST(root='data', train=False, download=True, transform=ToTensor())

input_size = 28 * 28  # 图片平铺为一维向量
hidden_size = 128  # 隐藏层
output_size = 10  # 输出层,0~9
batch_size = 128
# 定义一个神经网络,Sequential传入神经网络层,
neu = torch.nn.Sequential(
    # 平铺向量,将28x28二维图平铺为1维向量
    torch.nn.Flatten(),
    # 输入层
    torch.nn.Linear(input_size, hidden_size),
    # 激活函数,是神经网络重要组成部分,因其目的,可以选择激活函数,
    # ReLU(x) = max(0,x)
    torch.nn.ReLU(),
    # 输出范围: 映射到区间 (0, 1)。
    # 单调性: 是单调递增的函数。
    # 平滑性: 输出是连续且平滑的,适合用于需要概率输出的场景。
    # 饱和性: 当输入值 (x)  过大或过小时,输出值会趋近于 1 或 0,可能导致梯度消失问题。
    # torch.nn.Sigmoid(),
    # 输出层
    torch.nn.Linear(hidden_size, output_size)
)

# 使用torch自带的损失函数
# loss = torch.nn.MSELoss()
# 交叉熵损失函数
loss_criterion = torch.nn.CrossEntropyLoss()
# r = loss(预测向量,目标向量)
# SDG是torch自带的随即梯度下降算法,lr是学习率
optimizer = torch.optim.SGD(neu.parameters(), lr=0.01)

# 是否加载GPU
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
neu.to(device)

# 使用 DataLoader 加载数据
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False)

# 标记模型进入train状态
neu.train()
# 记录损失函数值下降过程
batch_loss = []
for i in range(3):
    for batch_idx, (data, targets) in enumerate(train_loader):
        # data, targets = data.to(device), targets.to(device)

        # 前向传播
        outputs = neu(data)
        # 根据目标值计算损失值
        loss = loss_criterion(outputs, targets)

        # 反向传播
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        batch_loss.append(loss.item())
        print(loss)

# 测试模型
# 标记模型进入test状态
neu.eval()
correct = 0
total = 0
with torch.no_grad():
    for data, targets in test_loader:
        # 预测输出
        outputs = neu(data)
        # 获取预测结果,取输出向量最大一个元素
        _, predicted = torch.max(outputs, 1)
        total += targets.size(0)
        correct += (predicted == targets).sum().item()

print(f"Accuracy:{100 * correct / total}%")

三,卷积神经网络

"""
通过卷积神经网络完成MNIST数据集训练
卷积c1->池化->卷积c2->池化->特征->特征


什么是卷积?它是对信号做积分运算。
在深度学习中,卷积操作主要用于提取图像的局部特征。
它的核心思想是:
使用一个小的卷积核(滤波器)在输入图像上滑动。
通过加权和运算提取出图像的局部特征,例如边缘、纹理、角点等。

什么是池化?
池化(Pooling)是卷积神经网络(CNN)中常用的操作,主要用于对特征图进行降维和特征提取。
池化操作的核心思想是:
缩小特征图的尺寸,从而减少计算量和内存需求。
保留主要特征,增强模型的鲁棒性(如抵抗旋转、缩放和位移等变化)。
防止过拟合,通过减少特征图的参数和冗余信息,提高模型的泛化能力。
这相当于俯瞰正片森林而不拘于一棵树,从宏观上观察事物,提高泛化能力。
"""

if True:
    import torch.nn.functional as F

    # 使用 DataLoader 加载数据
    train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
    test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False)
    # 定义两个卷积层厚度
    depth = [4, 8]
    # 28x28
    image_size = 28
    num_classes = 10


    class ConvNet(torch.nn.Module):

        def __init__(self):
            # 父类构造方法
            super(ConvNet, self).__init__()
            # 定义一个卷积核
            self.conv1 = torch.nn.Conv2d(1, 4, 5, padding=2)
            # 定义一个池化层 2x2
            self.pool = torch.nn.MaxPool2d(2, 2)
            # 定义第二个卷积核
            self.conv2 = torch.nn.Conv2d(depth[0], depth[1], 5, padding=2)
            # 定义一个线性全连接层,即特征输入层
            self.fc1 = torch.nn.Linear(image_size // 4 * image_size // 4 * depth[1], 512)
            # 定义一个输出层
            self.fc2 = torch.nn.Linear(512, num_classes)

        def forward(self, x):
            """
             模板方法,表示根据输入x进行神经网络前向运算
            :param x: 输入张量
            :return: x
            """
            # 正向通过第一个卷积层
            x = self.conv1(x)
            # 激活函数,防止过拟合
            x = F.relu(x)
            # 池化
            x = self.pool(x)
            # 第二个卷积层
            x = self.conv2(x)
            x = F.relu(x)

            # 第四层池化,将窗口缩小到1/4
            x = self.pool(x)

            # 让x特征值排布向量,以进入第一个全连接层
            x = x.view(-1, image_size // 4 * image_size // 4 * depth[1])

            # 全连接层
            x = F.relu(self.fc1(x))

            # 默认0.5概率对此层进行dropout操作,防止过拟合
            # 即根据概率,训练中选择丢弃一些神经,最后的测试再使用全部神经,以提高模型泛华能力
            # 就好比一组学生,平时总有几个人不在,于是每个人承担的任务更多,最后考试的时候集体成绩却更好
            x = F.dropout(x)
            x = self.fc2(x)
            x = F.log_softmax(x, dim=1)
            return x

        def retrieve_features(self, x):
            """
            用户提取卷积神经网络的特征图
            :param x:
            :return: feature_map1、feature_map2,卷积神经网络前两层特征图
            """
            feature_map1 = F.relu(self.conv1(x))
            x = self.pool(feature_map1)
            feature_map2 = F.relu(self.conv2(x))
            return feature_map1, feature_map2


    # 运行模式
    model = ConvNet()
    # 损失函数:交叉熵
    criterion = torch.nn.CrossEntropyLoss()
    optimizer = torch.optim.SGD(model.parameters(), lr=0.001, momentum=0.9)
    # 标记模型train,这样会打开dropout
    model.train()
    for i in range(5):
        for batch_idx, (data, targets) in enumerate(train_loader):
            data, targets = data.clone().requires_grad_(True), targets.clone().detach()  # detach从计算图中分离,不会计算其梯度

            # 前向传播
            outputs = model(data)
            loss = criterion(outputs, targets)
            optimizer.zero_grad()
            loss.backward()
            # 一步随即梯度下降算法
            optimizer.step()
            print(loss)

    # 测试集测试
    # 关闭dropout
    model.eval()
    correct = 0
    total = 0
    with torch.no_grad():
        for data, targets in test_loader:
            # 预测输出
            outputs = model(data)
            # 获取预测结果,取输出向量最大一个元素
            _, predicted = torch.max(outputs, 1)
            total += targets.size(0)
            correct += (predicted == targets).sum().item()

    print(f"Accuracy:{100 * correct / total}%")

可以看到,准确率比全连接高了5%,毕竟卷积善于对图像处理。

四,迁移学习

"""
此部分通过实例进一步学习 迁移学习,
所谓迁移学习,即通过已经训练好的良好泛华能力的模型,加上特定的特征层,即可实现垂直领域功能,

本文,通过对一个ResNet模型进行迁移学习,使得能识别蜜蜂。ResNet是一个上百层的卷积网络,在图像识别领域已有很高泛化能力
"""
import torch
import torch.nn as nn
from torchvision import datasets, models, transforms

# 原始图像224x224
image_size = 224
train_dataset = datasets.ImageFolder("data/train", transforms.Compose([transforms.RandomResizedCrop(image_size),
                                                                       transforms.RandomHorizontalFlip(),
                                                                       transforms.ToTensor(),
                                                                       transforms.Normalize([0.485, 0.456, 0.406],
                                                                                            [0.229, 0.224, 0.225])]))

val_dataset = datasets.ImageFolder("data/val", transforms.Compose([transforms.Resize(256),
                                                                   transforms.CenterCrop(image_size),
                                                                   transforms.ToTensor(),
                                                                   transforms.Normalize([0.485, 0.456, 0.406],
                                                                                        [0.229, 0.224, 0.225])]))
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=4, shuffle=True, num_workers=4)
val_loader = torch.utils.data.DataLoader(val_dataset, batch_size=4, shuffle=True, num_workers=4)
# 读取数据类别
num_classes = len(train_dataset.classes)

# 调用已经训练好的model,比如此处的ResNet
origin_model = models.resnet18(pretrained=True)

# 禁止原模型层被微分,即不改变原模型参数
for params in origin_model.parameters():
    params.requires_grad = False

# 接下来,使用预训练迁移网络
# num_ftrs存储了res18模型最后的全连接层
num_ftrs = origin_model.fc.in_features
# 将原model的最后两层全连接层替换为一个输出单位为2的全连接层,此处任务是识别是或否蜜蜂
origin_model.fc = nn.Linear(num_ftrs, 2)
# 定义损失函数、优化器
loss_criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(origin_model.parameters(), lr=0.0001, momentum=0.9)

# do train
for i in range(3):
    for batch_idx, (data, targets) in enumerate(train_loader):
        data, targets = data.clone().requires_grad_(True), targets.clone().detach()

        # 前向传播
        outputs = origin_model(data)
        # 根据目标值计算损失值
        loss = loss_criterion(outputs, targets)

        # 反向传播
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        print(loss)

五,神经网络风格迁移实战

所谓风格迁移,即将一幅画的风格迁移到另一副画

准备:

对应目录300*300png图,以及vgg19模型参数

"""
本章,尝试使用torch,使用已有模型,进行风格迁移学习,

Question:参考一幅图,通过像素的梯度下降(不改变参数,只改变目标图像素),以实现风格迁移

"""

from __future__ import print_function
import torch
import torch.nn as nn

from PIL import Image
import matplotlib.pyplot as plt
import torchvision.transforms as transform
import torchvision.models as models
import copy

style = "./image_test/style2.png"

content = "./image_test/content.png"

style_weight = 1000
content_weight = 1

# 生成图片size
imsize = 300

loader = transform.Compose([transform.Resize(imsize), transform.ToTensor()])

unloader = transform.ToPILImage()


# 加载图片
def image_loader(image_path):
    image = Image.open(image_path)
    image = loader(image).clone().detach().requires_grad_(True)
    # 虚拟一个batch维度
    return image.unsqueeze(0)


style_img = image_loader(style).type(torch.cuda.FloatTensor if torch.cuda.is_available() else torch.FloatTensor)
content_img = image_loader(content).type(torch.cuda.FloatTensor if torch.cuda.is_available() else torch.FloatTensor)

assert style_img.size() == content_img.size()


def show_image(tensor, title=None):
    image = tensor.clone().cpu()
    image = image.view(3, imsize, imsize)
    image = unloader(image)
    plt.imshow(image)
    plt.title(title)
    plt.pause(0.001)


plt.ion()
plt.figure()
show_image(style_img.data, title="风格图")

plt.figure()
show_image(content_img.data, title="目标图")

"""
加载VGC模型,该模型是视觉Model
"""
vgg19_model = models.vgg19()
vgg19_model.load_state_dict(torch.load("models_local/vgg19-dcbb9e9d.pth"))
cnn = vgg19_model.features
if torch.cuda.is_available():
    cnn = cnn.cuda()


def Gram(i):
    # a=batch (size=1),b是特征图 (c,d)是特征图尺寸
    a, b, c, d = i.size()
    features = i.view(a * b, c * d)
    G = torch.mm(features, features.t())

    # 通过除以特征图中的像素数量来将特征图归一
    return G.div(a * b * c * d)


class ContentLoss(nn.Module):
    def __init__(self, target, weight):
        super(ContentLoss, self).__init__()
        self.output = None
        self.loss = None
        self.target = target.detach() * weight
        self.weight = weight
        # 定义损失函数
        self.criterion = nn.MSELoss()

    def forward(self, i):
        self.loss = self.criterion(i * self.weight, self.target)
        self.output = i
        return self.output

    def backward(self, retain_graph=True):
        self.loss.backward(retain_graph=retain_graph)
        return self.loss


class StyleLoss(nn.Module):

    def __init__(self, target, weight):
        super(StyleLoss, self).__init__()
        self.output = None
        self.loss = None
        self.target = target.detach() * weight
        self.weight = weight
        # 定义损失函数
        self.criterion = nn.MSELoss()
        # self.gra, = GramMatrix()

    def forward(self, i):
        self.output = i.clone()
        i = i.cuda() if torch.cuda.is_available() else i
        self_G = Gram(i)
        self_G.mul_(self.weight)
        self.loss = self.criterion(self_G, self.target)

        return self.output

    def backward(self, retain_graph=True):
        self.loss.backward(retain_graph=retain_graph)
        return self.loss


content_layers = ['conv_4']

style_layers = ['conv_1', 'conv_2', 'conv_3', 'conv_4', 'conv_5']

"""
start style train
"""
model = nn.Sequential()
style_losses = []
content_losses = []
i = 1
for layer in list(cnn):
    # 遍历vgc神经网络层,找到卷积网络
    if isinstance(layer, nn.Conv2d):
        name = "conv_" + str(i)
        model.add_module(name, layer)

        if name in content_layers:
            # 如果位于内容计算层
            target = model(content_img).clone()

            content_loss = ContentLoss(target, content_weight)
            model.add_module(f"content_loss_{i}", content_loss)
            # 输出损失值
            print(f"content_loss_{i}:{content_loss}")
            content_losses.append(content_loss)

        if name in style_layers:
            target_feature = model(style_img).clone()
            target_feature_gram = Gram(target_feature)
            style_loss = StyleLoss(target_feature_gram, style_weight)
            model.add_module(f"style_loss_{i}", style_loss)
            print(f"style_loss_{i}:{style_loss}")
            style_losses.append(style_loss)

    if isinstance(layer, nn.ReLU):
        name = f"relu_{i}"
        model.add_module(name, layer)
        i += 1

    if isinstance(layer, nn.MaxPool2d):
        model.add_module(f"pool_{i}", layer)

print(model)

# 生成一张随即噪声图
input_img = torch.randn(content_img.data.size()).requires_grad_(True)
show_image(input_img.data, title="input image")

# 将输入图作为参数调整对象,即调整像素本身
input_params = nn.Parameter(input_img.data)
# 使用LBFGS算法优化器,
optimizer = torch.optim.LBFGS([input_params])

# 定义优化300轮
for i in range(300):
    # 限制输入图像色彩0~1
    input_params.data.clamp_(0, 1)

    # 清空梯度
    optimizer.zero_grad()

    # 放入神经网络
    model(input_params)

    style_score = 0
    content_score = 0

    for sl in style_losses:
        style_score += sl.backward()
    for cl in content_losses:
        content_score += cl.backward()

    print(f"运行{i}轮\n风格损失Content={content_score},Style={style_score}")

    optimizer.step(closure=lambda: content_score + style_score)

# 防止越界
output = input_params.data.clamp_(0, 1)
plt.figure()
show_image(output, title="fix image")
plt.ioff()

plt.show()


网站公告

今日签到

点亮在社区的每一天
去签到