AI备忘录 - 深度平台
type: channel
link: http://www.aiuai.cn/category/dlplatform
items: Pytorch - DatasetClass
items: 分布式中的进程通信
items: MMCV - 计时器和进度条progressbar
items: Pytorch - 图像分类库timm快速使用
items: Pytorch - 1D2D3D卷积操作
items: Pytorch - 低版本加载高版本模型报错
items: 深度学习CNN基础之CV与CNN
items: Pytorch - inference_mode API
items: Pytorch - einsum爱因斯坦求和约定标记法
items: Pytorch - 矩阵相乘@与逐元素相乘*

Pytorch - DatasetClass
type: item
date: 2024-01-29T05:30:38+00:00
description: From: https://twitter.com/akshay_pachaar/status/1751583417463124020典型的 Dataset 类，如：tabular data表格数据(tabular data)，以 lris 数据集为例：text dataNLP 中，往往将 text 划分为多个小的 pieces，即 tokens，一般是 words 或 subwords.将 raw text 转换为 embedding(vectors) 生成器可以使用的格式，是一个必要的初始化步骤.image data看下图片数据集的格式，其对应的 Dataset Class 如，

分布式中的进程通信
type: item
date: 2022-06-08T14:05:00+00:00
description: 1. 分布式训练的基本概念分布式训练 - Colossal-AI分布式训练需要多台机器/GPU. 在模型训练期间，这些设备之间会有通信. 为了更好地理解分布式训练，有几个重要的术语需要了解清楚:host: 主机(host)是通信网络中的主要设备. 在初始化分布式环境时，经常需要它作为一个参数。port: 这里的端口(port)主要是指主机上用于通信的主端口.rank: 在网络中赋予设备的唯一ID.world size: 网络中设备的数量.process group: 进程组(process group)是一个通信网络，包括设备的一个子集。总是有一个默认的进程组，它包含所有的设备. 一个子集的设备可以形成一个进程组，以便它们只在组内的设备之间进行通信.示例如图：为了说明这些概念，假设有2台机器（也称为节点Node），每台机器有4个 GPU. 当在这两台机器上初始化分布式环境时，基本上启动了8个进程（每台机器上有4个进程），每个进程被绑定到一个 GPU 上.在初始化分布式环境之前，需要指定主机（主地址）和端口（主端口）.这里，可以让主机为节点0，端口为一个数字，如29500. 所有的8个进程将寻找地址和端口并相互连接，默认的进程组将被创建. 默认进程组的 world size 为8，细节如下:process IDrankNode indexGPU index00001101220233034410551166127713还可以创建一个新的进程组. 这个新的进程组可以包含任何进程的子集. 例如，可以创建一个只包含偶数进程的组:process IDrankNode indexGPU index0000210242106312注意，rank 是相对于进程组而言的，一个进程在不同的进程组中可以有不同的 rank. 最大的 rank 始终是 world size of the process group - 1.在进程组中，各进程可以通过两种方式进行通信.point-to-point: 一个进程向另一个进程发送数据.collective: 一组进程一起执行分散、聚集、all-reduce、广播等操作.1.1. point-to-point communication应用如，Baidu’s DeepSpeech 和 Facebook’s large-scale experiments.1.2. Collective Communication2. Broadcast/Scatter/Gatter/Reduce/All-reduceBroadcast,Scatter,Gather,Reduce,All-reduce分别是什么？ - 2019.09.102.1. Broadcast把同一份数据分发广播给所有人.2.2. Scatter不同于Broadcast， scatter可以将不同数据分发给不同的进程.2.3. Gather把多个进程的数据拼凑在一起.2.4. Reduce将多个进程中的数据按照指定的映射函数进行运算得到最后的结果存在一个进程中，例如下面两个图中的归约(Reduce)操作都是求和，将4个不同进程的数据归约求和后存在了第一个进程.2.5. All-reduceAll-reduce与Reduce的区别就在于后者最后的结果是只保存在一个进程中，而All-reduce需要每个进程都有同样的结果. 所以All-reduce 一般包含 scatter操作，所以有时候也会看到 reduce-scatter 这种说法，其实reduce-scatter可以看成是all reduce的一种实现方式.3. torch.distributedPYTORCH DISTRIBUTED OVERVIEWPyTorch v1.6.0 中 torch.distributed 包含三种：[1] - Distributed Data-Parallel Training (DDP) DDP 是被普遍采用的单程序多数据训练范式( single-program multiple-data training paradigm).采用 DDP 时，模型被复制到每个进程(process)，然后每个模型副本被输入不同的输入数据样本.DDP 负责梯度通信(gradient communication)，以保持模型副本的同步，并将其与梯度计算重叠，以加快训练速度.[2] - RPC-Based Distributed Training (RPC)RPC 支持无法适应数据并行训练的通用训练结构，例如分布式管道并行(distributed pipeline parallelism)、参数服务器范式(parameter server paradigm)、以及 DDP与其他训练范式的组合.…

MMCV - 计时器和进度条progressbar
type: item
date: 2022-02-13T13:15:00+00:00
description: https://github.com/open-mmlab/mmcv/blob/master/mmcv/utils/progressbar.py1. 计时器 Timertimer.py:from time import time class TimerError(Exception): def __init__(self, message): self.message = message super(TimerError, self).__init__(message) class Timer: def __init__(self, start=True, print_tmpl=None): self._is_running = False self.print_tmpl = print_tmpl if print_tmpl else '{:.3f}' if start: self.start() @property def is_running(self): """bool: indicate whether the timer is running""" return self._is_running def __enter__(self): self.start() return self def __exit__(self, type, value, traceback): print(self.print_tmpl.format(self.since_last_check())) self._is_running = False def start(self): """Start the timer.""" if not self._is_running: self._t_start = time() self._is_running = True self._t_last = time() def since_start(self): """Total time since the timer is started. Returns: float: Time in seconds. """ if not self._is_running: raise TimerError('timer is not running') self._t_last = time() return self._t_last - self._t_start def since_last_check(self): """Time since the last checking. Either :func:`since_start` or :func:`since_last_check` is a checking operation. Returns: float: Time in seconds. """ if not self._is_running: raise TimerError('timer is not running') dur = time() - self._t_last self._t_last = time() return dur示例，如：from .timer import Timer timer_ins = Timer(start=True) print(timer_ins.since_start()) print(timer_ins.since_last_check()) print(timer_ins.since_start())2. 进度条 ProgressBar#!/usr/bin/python3 #!--*-- coding: utf-8 --*-- import sys from shutil import get_terminal_size from .timer import Timer class ProgressBar: """ A progress bar which can print the progress. """ def __init__(self, task_num=0, bar_width=50, start=True, file=sys.stdout): self.task_num = task_num self.bar_width = bar_width self.completed = 0 self.file = file if start: self.start() @property def terminal_width(self): width, _ = get_terminal_size() return width def start(self): if se…

Pytorch - 图像分类库timm快速使用
type: item
date: 2022-02-05T10:30:00+00:00
description: 原文：Getting Started with PyTorch Image Models (timm): A Practitioner’s Guide - 2022.02.02Github： rwightman/pytorch-image-modelsPyTorch Image Models（timm） 是一个优秀的图像分类 Python 库，其包含了大量的图像模型（Image Models）、Optimizers、Schedulers、Augmentations 等等.timm 提供了参考的 training 和 validation 脚本，用于复现在 ImageNet 上的训练结果；以及更多的 官方文档 和 timmdocs project.https://rwightman.github.io/pytorch-image-models/https://fastai.github.io/timmdocs/但，由于 timm 的功能之多，所以在定制使用时很难知道如何入手. 这里主要进行概述.pip install timm==0.5.41. Modelstimm 提供了大量的模型结构集合，而且很多模型都包含了预训练权重，或 PyTorch 训练、或从Jax和TensorFlow中移植，很方便下载使用.模型列表：https://paperswithcode.com/lib/timm查看模型列表：#打印 timm 提供的模型列表 print(timm.list_models()) print(len(timm.list_models())) #739 #带有预训练权重的模型列表 print(timm.list_models(pretrained=True)) print(len(timm.list_models(pretrained=True))) #592其中，timm.list_models() 函数：list_models(filter='', module='', pretrained=False, exclude_filters='', name_matches_cfg=False)查看特定族模型，如：print(timm.list_models('gluon_resnet*')) print(timm.list_models('*resnext*', 'resnet') ) print(timm.list_models('resnet*', pretrained=True))1.1. create_model 一般用法timm 创建模型最简单的方式是采用 create_model.以 Resnet-D 模型为例（Bag of Tricks for Image Classification For Convolutional Neural Networks paper），其是Resnet 的一种变形，其采用 average pool 进行下采样.model = timm.create_model('resnet50d', pretrained=True) print(model) #查看模型配置参数 print(model.default_cfg) ''' {'url': 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/resnet50d_ra2-464e36ba.pth', 'num_classes': 1000, 'input_size': (3, 224, 224), 'pool_size': (7, 7), 'crop_pct': 0.875, 'interpolation': 'bicubic', 'mean': (0.485, 0.456, 0.406), 'std': (0.229, 0.224, 0.225), 'first_conv': 'conv1.0', 'classifier': 'fc', 'architecture': 'resnet50d'} '''1.2. create_model 修改输入通道timm models 有个非常有用的特点，其可以处理任意通道数量的输入图像. 这是很多其他库所不具备的. 其实现原理可参考：https://fastai.github.io/timmdocs/models#So-how-is-timm-able-to-load-these-weights?model = timm.create_model('resnet50d', pretrained=True, in_chans=1) print(model) #test, single cha…

Pytorch - 1D2D3D卷积操作
type: item
date: 2022-02-04T10:33:00+00:00
description: 1D CNN，kernel 在 1 个方向移动，输入和输出是 2 维的，通常用于时序（Time-Series）数据2D CNN，kernel 在 2 个方向移动，输入和输出是 3 维的，通常用于图像数据3D CNN，kernel 在 3 个方向移动，输入和输出是4 维的，通用用于 3D 图像数据1D 卷积 - Conv1DConv1D，kernel 沿着一个维度进行滑动.比如，时间序列数据，如图：图：从传感器（accelerometer）获得的时间序列数据以人体穿戴在胳膊上的传感器所采集到的时序数据为例. 数据从 3 个轴表示，1D CNN 可以从传感器数据进行行为识别任务分析，如，人站立、行走、跳等. 该数据有 2 维，第一维是time-steps，另一个是在 3 个轴方向上的传感器值.kernel 在传感器数据上滑动的方式如下图，每一行表示某一轴的时间序列传感器数据. kernel仅能在时间轴方向上移动.''' https://pytorch.org/docs/stable/generated/torch.nn.Conv1d.html ''' import torch from torch import nn model = nn.Conv1d(in_channels=1, out_channels=10, kernel_size=5, stride=1) input = torch.randn(20, 1, 50) output = model(input)1D CNNs 被普遍应用于传感器数据，还可以用于音频和文本数据，因为其可以被表示为时序数据. 如图：2D 卷积 - Conv2DLenet-5 结构中首先提出标准CNN网络. Conv2D 普遍应用于图像数据. 其 2D 是因为，kernel 沿着数据的两个维度进行滑动，如图：CNN 的优势在于，采用 kernel 可以提取数据的空间特征. 例如，CNN 可以检测边缘、颜色分布等，其有助于网络对于图像分类的鲁棒性.''' https://pytorch.org/docs/stable/generated/torch.nn.Conv2d.html ''' import torch from torch import nn # With square kernels and equal stride model = nn.Conv2d(16, 33, 3, stride=2) # non-square kernels and unequal stride and with padding model = nn.Conv2d(16, 33, (3, 5), stride=(2, 1), padding=(4, 2)) # non-square kernels and unequal stride and with padding and dilation model = nn.Conv2d(16, 33, (3, 5), stride=(2, 1), padding=(4, 2), dilation=(3, 1)) input = torch.randn(20, 16, 50, 100) output = model(input)3D 卷积 - Conv3D3D CNN 中，kernel 沿 3 个维度滑动，如图：Conv3D 通常应用于 3D图像数据，如，MRI （Magnetic Resonance Imageing） 数据、CT（Computerized Tomography）扫描数据等.''' https://pytorch.org/docs/stable/generated/torch.nn.Conv3d.html ''' import torch from torch import nn # With square kernels and equal stride model = nn.Conv3d(16, 33, 3, stride=2) # non-square kernels and unequal stride and with padding model = nn.Conv3d(16, 33, (3, 5, 2), stride=(2, 1, 1), padding=(4, 2, 0)) input = torch.randn(20, 16, 10, 50, 100) output = model(input)

Pytorch - 低版本加载高版本模型报错
type: item
date: 2021-10-10T09:10:00+00:00
description: Pytorch 低版本环境(如，1.0) 加载高版本(如，1.10) 训练的模型，报错如下：xxx is a zip archive (did you mean to use torch.jit.load()?)原因：Pytorch1.6 版本后模型保存torch.save()参数变更，重新加上参数 _use_new_zipfile_serialization=False 保存，即可：https://pytorch.org/docs/stable/generated/torch.save.htmlimport torch #device = torch.device("cuda" if torch.cuda.is_available() else "cpu") #model.load_state_dict(torch.load(save_path),map_location=device) state_dict = torch.load('pytorch_model.pth') state_dict = torch.load('pytorch_model.pth', map_location="cpu") #state_dict = torch.load('pytorch_model.bin', map_location="cpu") torch.save(state_dict, 'pytorch_model.pth', _use_new_zipfile_serialization=False) #torch.save(state_dict, 'pytorch_model.bin', _use_new_zipfile_serialization=False)

深度学习CNN基础之CV与CNN
type: item
date: 2021-08-28T14:51:00+00:00
description: 出处：Paddle文档平台 - 计算机视觉与卷积神经网络1. 计算机视觉综述计算机视觉作为一门让机器学会如何去“看”的学科，具体的说，就是让机器去识别摄像机拍摄的图片或视频中的物体，检测出物体所在的位置，并对目标物体进行跟踪，从而理解并描述出图片或视频里的场景和故事，以此来模拟人脑视觉系统。因此，计算机视觉也通常被叫做机器视觉，其目的是建立能够从图像或者视频中“感知”信息的人工系统。计算机视觉技术经过几十年的发展，已经在交通（车牌识别、道路违章抓拍）、安防（人脸闸机、小区监控）、金融（刷脸支付、柜台的自动票据识别）、医疗（医疗影像诊断）、工业生产（产品缺陷自动检测）等多个领域应用，影响或正在改变人们的日常生活和工业生产方式。未来，随着技术的不断演进，必将涌现出更多的产品和应用，为我们的生活创造更大的便利和更广阔的机会。图1 计算机视觉技术在各领域的应用2. 计算机视觉的发展历程计算机视觉的发展历程要从生物视觉讲起。对于生物视觉的起源，目前学术界尚没有形成定论。有研究者认为最早的生物视觉形成于距今约7亿年前的水母之中，也有研究者认为生物视觉产生于距今约5亿年前寒武纪。寒武纪生物大爆发的原因一直是个未解之谜，不过可以肯定的是在寒武纪动物具有了视觉能力，捕食者可以更容易地发现猎物，被捕食者也可以更早的发现天敌的位置。视觉系统的形成有力地推动了食物链的演化，加速了生物进化过程，是生物发展史上重要的里程碑。经过几亿年的演化，目前人类的视觉系统已经具备非常高的复杂度和强大的功能，人脑中神经元数目达到了1000亿个，这些神经元通过网络互相连接，这样庞大的视觉神经网络使得我们可以很轻松的观察周围的世界，如 图2 所示。图2 人类视觉感知对人类来说，识别猫和狗是件非常容易的事。但对计算机来说，即使是一个精通编程的高手，也很难轻松写出具有通用性的程序（比如：假设程序认为体型大的是狗，体型小的是猫，但由于拍摄角度不同，可能一张图片上猫占据的像素比狗还多）。那么，如何让计算机也能像人一样看懂周围的世界呢？研究者尝试着从不同的角度去解决这个问题，由此也发展出一系列的子任务，如 图3 所示。图3 计算机视觉子任务示意图(a) Image Classification： 图像分类，用于识别图像中物体的类别（如：bottle、cup、cube）。(b) Object Localization： 目标检测，用于检测图像中每个物体的类别，并准确标出它们的位置。(c) Semantic Segmentation： 语义分割，用于标出图像中每个像素点所属的类别，属于同一类别的像素点用一个颜色标识。(d) Instance Segmentation： 实例分割，值得注意的是，目标检测任务只需要标注出物体位置，而实例分割任务不仅要标注出物体位置，还需要标注出物体的外形轮廓。这里以图像分类任务为例，为大家介绍计算机视觉技术的发展历程。在早期的图像分类任务中，通常是先人工提取图像特征，再用机器学习算法对这些特征进行分类，分类的结果强依赖于特征提取方法，往往只有经验丰富的研究者才能完成，如 图4 所示。图4 早期的图像分类任务在这种背景下，基于神经网络的特征提取方法应运而生。Yann LeCun是最早将卷积神经网络应用到图像识别领域的，其主要逻辑是使用卷积神经网络提取图像特征，并对图像所属类别进行预测，通过训练数据不断调整网络参数，最终形成一套能自动提取图像特征并对这些特征进行分类的网络，如 图5 所示。图5 早期的卷积神经网络处理图像任务示意这一方法在手写数字识别任务上取得了极大的成功，但在接下来的时间里，却没有得到很好的发展。其主要原因一方面是数据集不完善，只能处理简单任务，在大尺寸的数据上容易发生过拟合；另一方面是硬件瓶颈，网络模型复杂时，计算速度会特别慢。目前，随着互联网技术的不断进步，数据量呈现大规模的增长，越来越丰富的数据集不断涌现。另外，得益于硬件能力的提升，计算机的算力也越来越强大。不断有研究者将新的模型和算法应用到计算机视觉领域。由此催生了越来越丰富的模型结构和更加准确的精度，同时计算机视觉所处理的问题也越来越丰富，包括分类、检测、分割、场景描述、图像生成和风格变换等，甚至还不仅仅局限于2维图片，包括视频处理技术和3D视觉等。3. 卷积神经网络卷积神经网络是目前计算机视觉中使用最普遍的模型结构。图6 是一个典型的卷积神经网络结构，多层卷积和池化层组合作用在输入图片上，在网络的最后通常会加入一系列全连接层，ReLU激活函数一般加在卷积或者全连接层的输出上，网络中通常还会加入Dropout来防止过拟合。图6 卷积神经网络经典结构卷积层：卷积层用于对输入的图像进行特征提取。卷积的计算范围是在像素点的空间邻域内进行的，因此可以利用输入图像的空间信息。卷积核本身与输入…

Pytorch - inference_mode API
type: item
date: 2021-06-15T14:59:00+00:00
description: Pytorch1.9 released 里发布了一个 Inference Mode API(Beta).推理模式API ( Inference Mode API ) 可以显著加速推理工作负载的速度，同时保持安全，并确保永远不会计算不正确的梯度. 在不需要 autograd 时，其提供了最好的性能.inference_mode — PyTorch 1.9.0 documentation定义如：class torch.inference_mode(mode=True) #参数 mode: 是否开启推断模式InferenceMode 是类似与 no_grad 的上下文管理器(context manager)，主要用于确定不需要与 autograd 交互时使用. 这种模式下运行的代码，通过禁用试图跟踪(view tracking) 和版本计数器缓冲(version counter bumps) 来获得更好的性能.InferenceMode 上下文管理器是局部线程的(thread local)，其不会影响其他线程中的计算.InferenceMode 也起到装饰器的作用.使用示例:import torch x = torch.ones(1, 2, 3, requires_grad=True) with torch.inference_mode(): y = x * x # y.requires_grad # False y._version #Traceback (most recent call last): #File "<stdin>", line 1, in <module> #RuntimeError: Inference tensors do not track version counter. @torch.inference_mode() def func(x): return x * x out = func(x) out.requires_grad #False

Pytorch - einsum爱因斯坦求和约定标记法
type: item
date: 2021-06-08T13:05:00+00:00
description: 原文：EINSUM IS ALL YOU NEED - EINSTEIN SUMMATION IN DEEP LEARNING - 2018.05.02译文：einsum满足你一切需要：深度学习中的爱因斯坦求和约定作者：TIM ROCKTASCHEL节选，学习.1. einsum 标记法1.1. 矩阵相乘比如，两个矩阵相乘，$A \in R^{I \times K}$ 和 $B \in R^{K \times J}$ ；再计算每列的和，最终得到矩阵 $c \in R^J$，其可以表达为：其中，表达式说明了每个元素 $c_i$ 的计算过程. 列向量 $A_{i:}$ 乘以行向量 $B_{:j}$，再求和.einsum 标记法中，隐式地省略了求和符号，而是累加重复的下标(如，$k$) 和输出中未标出的下标(如 $i$).1.2. 向量点积比如，两个向量 $a, b \in R^{J}$ 的点积，可以表达为：1.3. 高阶张量变换比如，深度学习常见的一种高阶张量(higher-order tensor)，其包含一个 batch 中有 N 个训练样本，每个样本是一个长度为 T 的 K 维词向量序列，期望将词向量投影到一个不同的维度 Q.记，张量为 $T \in R^{N \times T \times K}$，投影矩阵记为 $W \in R^{K \times Q}$，则，einsum 表达式为：1.4. 四阶张量变换比如，对于四阶张量 $T \in R^{N \times T \times K \times M}$，期望使用上述的投影矩阵 W 将第三维投影到 Q 维，并累加到第二维，再对结果中的第一维和最后一维进行转置，最终得到张量 $C \in R^{M \times Q \times N}$. einsum 表达式为：注，这里是通过交换下标 n 和 m ($C_{mqn}$ 而不是 $C_{nqm}$)，转置了张量结果.2. Numpy/PyTorch/TensorFlow 中 einsum 标记法NumPy - einsum 符号标记函数 - AIUAINumpy - np.einsumPyTorch - torch.einsumTensorFlow - tf.einsum表示形式均为：einsum(equation, operands) #equation - einsum 约定字符串 #operands - 张量序列如，1.1.矩阵相乘，$c_j = \sum _i \sum_k A_{ik} B_{kj}$，其表示形式如：equation = 'ik,kj -> j'Numpy/PyTorch/TensorFlow 支持 einsum 的好处在于，可以应用于神经网络架构中的任意计算图，且可以反向传播.典型 einsum 调用形式如：其中，方框是占位符，表示张量维度. 根据该式，可以推断，arg1 和 arg3 是矩阵，arg2 是三阶张量，einsum 计算结果 result 是矩阵.注意，einsum 处理的是可变数量的输入. 以下依PyTorch为例示例介绍.2.1. 矩阵转置import torch a = torch.arange(6).reshape(2, 3) #tensor([[0, 1, 2], # [3, 4, 5]]) b = torch.einsum('ij->ji', [a]) #tensor([[0, 3], # [1, 4], # [2, 5]])2.2. 求和import torch a = torch.arange(6).reshape(2, 3) b = torch.einsum('ij->', [a]) #tensor(15)2.3. 列求和import torch a = torch.arange(6).reshape(2, 3) b = torch.einsum('ij->j', [a]) #tensor([3, 5, 7])2.4. 行求和import torch a = torch.arange(6).reshape(2, 3) b = torch.einsum('ij->i', [a]) #tensor([ 3, 12])2.5. 矩阵-向量相乘import torch a = torch.arange(6).reshape(2, 3) b = torch.arange(3) #tensor([0, 1, 2]) c = torch.einsum('ik,k->i', [a, b]) #tensor([ 5, 14])2.6. 矩阵-矩阵相乘import torch a = torch.arange(6).reshape(2, 3) b = torch.arange(15).…

Pytorch - 矩阵相乘@与逐元素相乘*
type: item
date: 2021-06-06T13:18:00+00:00
description: Pytorch 代码里看到 @ 和 * 两个操作符，其功能如下.1. 矩阵相乘@示例如，import torch # X = torch.tensor([[1,2],[3,4],[5,6]]) #torch.Size([3, 2]) #tensor([[1, 2], # [3, 4], # [5, 6]]) Y = torch.tensor([[7,8],[9, 10]]) #torch.Size([2, 2]) #tensor([[5, 6], # [7, 8]]) Z = X@Y #torch.Size([3, 2]) #tensor([[ 25, 28], # [ 57, 64], # [ 89, 100]]) #等价于： Z = torch.matmul(X, Y)2. 矩阵逐元素相乘*示例如，import torch # X = torch.tensor([[1,2],[3,4],[5,6]]) #torch.Size([3, 2]) #tensor([[1, 2], # [3, 4], # [5, 6]]) Y = torch.tensor([[7,8],[9, 10],[11,12]]) #torch.Size([3, 2]) #tensor([[ 7, 8], # [ 9, 10], # [11, 12]]) Z = X*Y #tensor([[ 7, 16], # [27, 40], # [55, 72]]) #等价于： Z = torch.mul(X, Y)